Skip to content

Flags

Flags are the richest primitive in dreamcli. Each flag declaration configures parsing, type inference, resolution, help text, and shell completions.

Flag Types

String

ts
flagTypes.string;

Number

ts
flagTypes.number;

Numeric constraints

flag.number() accepts optional numeric constraints, either as an options object or via chained methods (they compose — a later chained call overrides an earlier value, including one set in the options object):

ts
flag.number({ min: 0, max: 100, int: true });
flag.number().int().min(0).max(100); // equivalent
flag.number({ min: 0 }).max(100); // composes to { min: 0, max: 100 }
OptionMeaningDefault
minInclusive lower boundnone
maxInclusive upper boundnone
intRequire an integerfalse
finiteReject Infinity / -Infinitytrue

The resolved value type stays number — constraints are enforced at runtime and surfaced in the exported JSON Schema (minimum / maximum, and type: "integer" when int is set), not at the type level. min / max must be finite; passing Infinity / -Infinity / NaN as a bound throws when the flag is declared (omit the field for "no bound").

Finite by default

flag.number() now rejects Infinity and -Infinity (as well as NaN, which was always rejected). Pass finite: false (or .finite(false)) to accept non-finite values.

Constraints are checked in order finite → int → min → max, and apply to every source — CLI, env, config, and prompt. On the first failure, CLI parsing throws INVALID_VALUE while env/config/prompt resolution reports CONSTRAINT_VIOLATED (both exit code 2):

Input (flag.number({ int: true, min: 0, max: 100 }))Result
42accepted
0 / 100accepted (bounds are inclusive)
NaNrejected — invalid number
Infinityrejected — must be a finite number
3.7rejected — must be an integer
-1rejected — must be >= 0
101rejected — must be <= 100

The same options and methods are available on arg.number() for positional arguments.

Boolean

ts
flagTypes.boolean;

Enum

ts
flagTypes.enum;

Array

ts
flagTypes.array;

Custom

ts
flagTypes.custom;

Array flags are the one optional flag kind that still resolve to a value when unset: if no CLI/env/config/prompt/default value is found, they fall back to an empty array [].

For the exact parser rules around repeated flags, short-flag stacking, -- separator handling, and --no-* spellings, see CLI Semantics.

Flag Names

The string you pass to .flag(name, …) is the flag's canonical name, and it is used in two places at once:

  • on the command line as --name;
  • as the key on the flags object inside your handler.

No case conversion happens — the name you declare is the name you read. Single-word names are valid identifiers, so dot access works (flags.region). Hyphenated names are not valid identifiers, so read them with bracket access (flags['node-ipc']).

ts
import { command, flag } from '@kjanat/dreamcli';

command('serve')
  .flag(
    'node-ipc',
    flag.boolean().describe('Use the Node IPC transport'),
  )
  .flag('dry-run', flag.boolean())
  .action(({ flags, out }) => {
    // Hyphenated names are read with bracket access — there is no `flags.nodeIpc`.
    if (flags['node-ipc']) out.log('ipc');
    if (flags['dry-run']) out.log('dry run');
  });

Reach for a hyphenated name when you want the conventional CLI spelling (--node-ipc, --dry-run); reach for a single-word or camelCase name (--nodeIpc) when ergonomic dot access matters more.

Aliases Are CLI Tokens, Not Handler Keys

.alias() adds an alternate spelling on the command line. It resolves back to the canonical name — it never becomes a second property on flags.

ts
import { command, flag } from '@kjanat/dreamcli';

command('serve')
  // Accepts both `--skip-pass` and `--skipPass` on the CLI…
  .flag('skip-pass', flag.boolean().alias('skipPass'))
  .action(({ flags, out }) => {
    // …but the handler has exactly one key: the canonical name.
    if (flags['skip-pass']) out.log('skipping');
  });

So typing --skipPass still arrives as flags['skip-pass'].

Modifiers

Every flag type supports the same modifier chain:

ts
import { flag } from '@kjanat/dreamcli';

flag
  .string()
  // short alias: -r
  .alias('r')
  // help text
  .describe('Target region')
  // default value (narrows type)
  .default('us')
  // must resolve or error
  .required()
  // resolve from env var
  .env('DEPLOY_REGION')
  // resolve from config file
  .config('deploy.region')
  // interactive fallback
  .prompt({ kind: 'input', message: 'Region?' })
  // deprecation warning
  .deprecated('Use --target instead')
  // inherit in subcommands
  .propagate();

Resolution Chain

Each flag resolves through an ordered pipeline. Every step is opt-in:

mermaid
flowchart LR
    A[CLI argv] --> B[Environment variable]
    B --> C[Config file]
    C --> D[Interactive prompt]
    D --> E[Default value]

The first source that provides a value wins. Required flags that don't resolve produce a structured error before the action handler runs.

Example

ts
import { flag } from '@kjanat/dreamcli';

flag
  .enum(['us', 'eu', 'ap'])
  .env('DEPLOY_REGION')
  .config('deploy.region')
  .prompt({ kind: 'select', message: 'Which region?' })
  .default('us');

Resolution order:

  1. --region eu on the command line
  2. DEPLOY_REGION=eu in environment
  3. deploy.region: "eu" in config file
  4. Interactive select prompt (TTY only)
  5. Default value "us"

Required vs Optional

Optional

ts
requiredVsOptional.optional;

Defaulted

ts
requiredVsOptional.defaulted;

Required

ts
requiredVsOptional.required;

Boolean

ts
requiredVsOptional.boolean;

Custom Parsing

ts
import { flag } from '@kjanat/dreamcli';

flag.custom((value) => {
  const url = new URL(String(value));
  if (url.protocol !== 'https:') {
    throw new Error('URL must use HTTPS');
  }
  return url;
});

The parse function receives the raw string value and returns the parsed type. Thrown errors become validation errors with the flag name in context.

Propagation

Flags marked with .propagate() are inherited by all subcommands:

ts
import { cli, command, flag } from '@kjanat/dreamcli';

const nested = command('start')
  .flag('verbose', flag.boolean().alias('v').propagate())
  .action(({ flags, out }) => {
    if (flags.verbose) {
      out.info('Verbose mode enabled');
    }
  });

cli('mycli').command(
  command('deploy')
    .flag('verbose', flag.boolean().alias('v').propagate())
    .command(nested),
);

What's Next?

Released under the MIT License.