;
};
// ---cut---
argTypes.url;
// ^?
```
#### Path
The value stays a `string`, with optional filesystem checks that run **after
resolution**. See [Filesystem checks](#filesystem-checks).
```ts
arg.path(); // any string
arg.path({ mustExist: true }); // rejects missing paths
arg.path({ type: 'directory' }); // must exist and be a directory
arg.path({ type: 'directory', mustExist: false }); // missing passes; existing must be a directory
arg.path({ type: 'directory', create: true }); // created recursively when missing
```
`arg.path()` resolves as a `string`, so a value read from stdin keeps the buffer
byte for byte, trailing line terminator included. `echo ./docs | mycli` reaches
a `mustExist` check as `'./docs\n'` and fails:
```
Path './docs
' for argument does not exist
```
`.stdin({ trim: true })` drops that terminator, so `echo ./docs | mycli` reaches
the check as `'./docs'`. `printf './docs'` works too.
Help renders a positional by its own name, so `arg.path()` shows ``
rather than the `` placeholder its flag counterpart uses. That keeps
`mycli copy ` readable instead of collapsing to ` `.
#### Date
Accepts strict ISO-8601 (`2026-07-10`, `2026-07-10T14:30:00Z`) and parses into
a `Date`. Lenient `Date.parse` inputs (`'0'`, `'March 5'`) and calendar-invalid
dates (`2026-02-31`) are rejected. Offset-less datetimes (`2026-07-10T14:30`)
are treated as **UTC**, not local time, so `min` / `max` acceptance never
depends on the machine's timezone. Optional inclusive `min` / `max` bounds:
```ts twoslash
import { arg, type InferArg } from '@kjanat/dreamcli';
const dateArg = arg.date({ min: new Date('2020-01-01') });
declare const argTypes: {
date: InferArg;
};
// ---cut---
argTypes.date;
// ^?
```
#### Duration
Accepts `'30s'`, `'5m'`, `'1.5h'`, `'250ms'`, `'2d'`, compounds like `'1h30m'`,
or a bare millisecond count, and resolves to **milliseconds**:
```ts
arg.duration().default(30_000);
// mycli wait 45s → 45000
// mycli wait 1h30m → 5400000
```
#### Bytes
Accepts `'512mb'`, `'1.5gb'`, `'64kb'`, `'100b'` or a bare byte count, and
resolves to **bytes**. Units are binary (`1kb` = 1024) and case-insensitive:
```ts
arg.bytes().default(10 * 1024 ** 2);
// mycli split 512kb → 524288
```
## Cardinality
An argument carries one value, an ordered list, or a record of key-value
entries. There is no positional counterpart to `flag.count()`, which counts how
often a named token appears.
| Form | Declared by | Unset resolves to |
| ------- | -------------------------------------- | ----------------- |
| one | every value kind | `undefined` |
| list | `.variadic()` on any kind | `[]` |
| entries | `arg.keyValue()` | `{}` |
### Variadic Arguments
The last argument can be variadic, collecting all remaining positional values.
It takes every remaining token, so it has to be the last one a command declares:
another positional after it, variadic or not, throws `INVALID_BUILDER_STATE`.
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('copy')
.arg(
'files',
arg.string().variadic().describe('Files to copy'),
)
.action(({ args }) => {
args.files;
// ^?
});
```
```bash
$ mycli copy a.txt b.txt c.txt
# args.files = ['a.txt', 'b.txt', 'c.txt']
```
Anything registered behind a variadic argument could never be filled, so both
construction paths refuse it:
```ts
command('copy').arg('files', arg.string().variadic()).arg('target', arg.string());
```
```
Argument comes after variadic argument , which consumes every remaining positional
Suggestion: Declare before , or drop .variadic() from
```
A second variadic argument is the same error, since the first one leaves it
nothing to collect. `details` carries the command in `command`, the argument
that could never fill in `arg`, and the greedy one in `variadicArg`, so a
definition tree names the nested command that declared the pair. Every earlier
ordering still builds: required then optional then variadic, a lone variadic, a
scalar followed by a `arg.keyValue().variadic()` tail.
### Key-Value Arguments {#key-value-arguments}
`arg.keyValue()` consumes `KEY=VALUE` tokens and resolves to
`Record`, splitting each at the first `=`. The non-variadic form
reads one token; the variadic form aggregates the whole tail into one record:
```ts twoslash
import { arg, type InferArg } from '@kjanat/dreamcli';
const vars = arg.keyValue().variadic();
declare const argTypes: { vars: InferArg };
// ---cut---
argTypes.vars;
// ^?
```
```bash
$ mycli render a=1 b=2 a=3
# args.vars === { a: '3', b: '2' }
```
`.duplicateKeys()` decides what a repeated key means, on every source: `'last'`
(the default), `'first'`, or `'error'`. The per-source table under
[Collections](/guide/flags#collections) applies unchanged, so env values carry
comma-delimited pairs until `.split({ env })` says otherwise, and a config value
is read as a native object.
An element builder gives each entry value its own codec, constraints, filesystem
checks, and validator, exactly as `flag.keyValue(element)` does. Without one,
entry values are strings:
```ts twoslash
import { arg, type InferArg } from '@kjanat/dreamcli';
const replicas = arg.keyValue(arg.number().int().min(0)).variadic();
declare const argTypes: { replicas: InferArg };
// ---cut---
argTypes.replicas;
// ^?
```
```bash
$ mycli scale web=3 api=2
# args.replicas === { web: 3, api: 2 }
$ mycli scale web=-1
# Error: must be >= 0
```
The element is a builder that still describes a value alone. The modifiers that
describe the positional itself, `.optional()`, `.variadic()`, `.env()`,
`.config()`, `.prompt()`, `.stdin()`, `.describe()`, `.deprecated()`, and
`.default()`, take it out of element position at compile time, since an entry
value is never read for any of them.
A key-value argument can read stdin, where the whole buffer decodes into
entries, and its variadic form splices the buffer into the tail it collects:
```ts
arg.keyValue().stdin();
arg.keyValue().variadic().stdin();
```
```bash
$ printf 'A=1\nB=2\n' | mycli run -
# args.vars === { A: '1', B: '2' }
$ printf 'B=2\n' | mycli run A=1 -
# args.vars === { A: '1', B: '2' }
```
### Collections {#collections}
`.variadic()` and `arg.keyValue()` are the two aggregating forms, and both fill
from every source the argument declares under the rules
[Collections](/guide/flags#collections) sets out for flags. `.split()` sets how
each source decodes, `.separator()` sets the CLI delimiter alone, `.unique()`
deduplicates a variadic array, and `.duplicateKeys()` decides a repeated key:
```ts
arg.string().variadic().split({ cli: ',', env: 'json' });
// mycli build a,b c → ['a', 'b', 'c']
// FILES='["a","b"]' → ['a', 'b']
arg.keyValue().separator(',');
// mycli run A=1,B=2 → { A: '1', B: '2' }
```
The CLI delimiter applies to each token whatever the arity, so a non-variadic
`arg.keyValue()` splits its single token the same way the variadic form splits
each token of its tail, and the same way `flag.keyValue()` splits each
occurrence.
These four modifiers are collection modifiers, so they are available on an
argument that aggregates. Each states the shape it needs. `.separator()` and
`.split()` want a variadic argument or `arg.keyValue()`. `.unique()`
deduplicates a list, so it wants a variadic argument of a list kind, and a
key-value argument folds repeated keys through `.duplicateKeys()` instead, which
in turn wants `arg.keyValue()`. The compiler refuses each one elsewhere, and the
definition path throws `INVALID_SCHEMA` at build time:
```ts
arg.string().separator(','); // INVALID_SCHEMA
createArgSchema('string', { unique: true }); // INVALID_SCHEMA
createArgSchema('keyValue', { variadic: true, unique: true }); // INVALID_SCHEMA
```
```
Arg schema field 'separator' requires a collection, received a non-variadic 'string' arg
Suggestion: Add 'variadic: true', declare the arg as kind 'keyValue', or drop 'separator'
```
The values a schema already stores as its own default, `unique: false` and
`duplicateKeys: 'last'`, are exempt on the definition path, so feeding a built
`ArgSchema` back through `createArgSchema()` round-trips. Only the compiler
rejects `.duplicateKeys('last')` on a string argument.
A variadic argument reads stdin like any other, and
[the positional tail](#stdin-tail) describes what a `-` among its tokens does.
## Sources
Arguments walk the same ordered chain flags do, and are CLI-only unless they opt
into extra sources:
```mermaid
flowchart LR
CLI["CLI token"] -->|miss| stdin -->|miss| env -->|miss| config -->|miss| prompt -->|miss| default
```
```ts twoslash
import { arg } from '@kjanat/dreamcli';
arg.string().stdin().env('DEPLOY_TARGET').config('deploy.target').default('local');
```
### Environment-Backed Arguments
Arguments can fall back to environment variables when the positional value is missing:
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('auth')
.arg(
'token',
arg
.string()
.env('API_TOKEN')
.describe('Token from env'),
)
.action(({ args }) => {
args.token;
// ^?
});
```
If `API_TOKEN=secret` and no CLI value is provided, `args.token === 'secret'`.
### Config-Backed Arguments
`.config()` binds an argument to a dotted key in the loaded config object:
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('deploy')
.arg(
'target',
arg.string().config('deploy.target').default('local'),
)
.action(({ args }) => {
args.target;
// ^?
});
```
```bash
# config: { "deploy": { "target": "eu-west" } }
mycli deploy # target = 'eu-west'
mycli deploy ap-south # target = 'ap-south', the CLI token wins
```
The config value is coerced to the argument's declared kind the same way a
flag's is, so `arg.duration().config('y.wait')` accepts both `"1h30m"` and
`5000`. A value that fails non-argv decoding reports `TYPE_MISMATCH`; an enum
mismatch reports `INVALID_ENUM`, and a decoded value that violates a constraint
reports `CONSTRAINT_VIOLATED`. The raw value is reported unless the argument is
marked `.sensitive()`.
### Prompt-Backed Arguments
`.prompt()` attaches an interactive prompt that runs when CLI, stdin, env, and
config all produce nothing:
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('deploy')
.arg(
'target',
arg.string().prompt({ kind: 'input', message: 'Target:' }),
)
.action(({ args }) => {
args.target;
// ^?
});
```
```bash
mycli deploy # asks "Target:"
mycli deploy production # skips the prompt
```
The prompt kinds an argument accepts follow its value and cardinality, matching
the flag table: `string` takes `input` or `select`, `number` takes `input`,
`enum` takes `select` or `input`, `custom` takes every kind, and a variadic
argument takes `multiselect`, the kind `flag.array()` takes. `arg.keyValue()` is
not promptable, as `flag.keyValue()` is not. An incompatible pairing throws
`CONSTRAINT_VIOLATED` naming the argument:
```
Prompt kind 'confirm' is not compatible with number argument . Use 'input' instead
Prompt kind 'input' is not compatible with variadic string argument . Use 'multiselect' instead
```
Prompts run only when a prompter is available. Without one the argument falls
through to its default, or fails as missing.
### STDIN-Backed Arguments
Arguments can also read from piped stdin with `.stdin()`:
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('format')
.arg(
'data',
arg.string().stdin().describe('Read from STDIN'),
)
.action(({ args }) => {
args.data;
// ^?
});
```
Arguments and flags resolve through the same order:
`CLI → stdin → env → config → prompt → default`. With `stdinData: 'hello'` in
tests or piped input at runtime, `args.data === 'hello'`.
The stdin stage sits ahead of env, so an argument set in the environment still
reads a pipe and the pipe wins. Passing the literal sentinel `-` selects stdin
too, but keeps CLI precedence: the bytes come from the pipe and every later
stage stays out of the way. When nothing was piped, both forms fall through to
env, config, prompt, and the default.
The whole buffer becomes the value. Codecs that preserve text terminators keep
it byte for byte, so `echo hi | mycli` gives a string argument `'hi\n'` and a
path keeps the terminator too. Other codecs remove one framing terminator before
decoding, so `echo 30s` reaches `arg.duration()` as `30s`. `arg.keyValue()`
decodes the buffer into entries, one `KEY=VALUE` per line by default.
`{ trim: true }` drops that terminator for a string argument too, which is what
a piped path wants:
```ts
arg.path({ mustExist: true }).stdin({ trim: true });
```
```bash
$ echo ./dist | mycli clean
# args.path === './dist', and the check runs against './dist'
```
Trimming applies to a single value. A collection's terminators separate its
elements, so `.split({ stdin })` decides them and `trim` has nothing to do.
#### Choosing when stdin is read
`.stdin()` takes `{ when, consume, trim }`:
```ts twoslash
import { arg } from '@kjanat/dreamcli';
arg.string().stdin(); // '-' or an omitted slot reads stdin
arg.string().stdin({ when: 'dash' }); // only an explicit '-'
arg.string().stdin({ when: 'missing' }); // only an omitted slot; '-' stays literal
arg.string().stdin({ consume: 'broadcast' }); // shares the buffer with other inputs
arg.string().stdin({ trim: true }); // drops one trailing terminator
```
Stdin is read at most once per invocation, and only when one of these bindings
would actually fire. If the user does not provide `-`, a `when: 'dash'`
argument does not read the stream.
Help names the binding beside the argument's other sources: `[stdin]` for the
default, `[stdin: '-']` for `{ when: 'dash' }`, and `[stdin: when omitted]` for
`{ when: 'missing' }`. See [Source annotations](/guide/help#source-annotations).
#### The positional tail {#stdin-tail}
A variadic argument reads stdin too. A `-` among its tokens splices what the
buffer decodes to into that position, and an empty tail under a binding that
covers a missing input takes the whole buffer:
```ts
command('build').arg('files', arg.string().variadic().stdin());
```
```bash
$ printf 'x\ny\n' | mycli build a - b
# args.files === ['a', 'x', 'y', 'b']
$ printf 'x\ny\n' | mycli build
# args.files === ['x', 'y']
```
Each `-` stands for the whole source, so two of them splice the buffer twice and
`mycli build - -` over `'x\n'` collects `['x', 'x']`. That is the same rule the
flag surface follows for `--tag - --tag -`.
`.split({ stdin })` decodes the tail, so a piped JSON array works the same way a
piped list of lines does:
```ts
command('build').arg('files', arg.string().variadic().stdin().split({ stdin: 'json' }));
```
```bash
$ echo '["p","q"]' | mycli build a - b
# args.files === ['a', 'p', 'q', 'b']
```
A key-value tail aggregates the typed tokens and the pipe together, then folds
repeated keys under `.duplicateKeys()`:
```bash
$ printf 'M=5\n' | mycli run A=1 - Z=9
# args.vars === { A: '1', M: '5', Z: '9' }
```
Only a binding that covers a missing input reads an empty tail. Under
`{ when: 'dash' }` an empty tail never selects stdin, so the stream stays
untouched and a required argument reports itself missing.
A `-` typed beside other tokens with nothing piped fails with `MISSING_STDIN`,
because dropping it would silently shorten the tail:
```
No piped stdin for the '-' occurrence of argument
Suggestion: Pipe a value to stdin, or drop the '-' from
```
A tail of nothing but `-` is the whole value, so with nothing piped it falls
through to env, config, prompt, and the default, exactly as an omitted slot
does.
A stdin-enabled input cannot receive a literal `-` as its value: the token names
the source before anything reads it as text. Pass the value another way, or use
`{ when: 'missing' }`, which leaves `-` as the literal string.
#### Worked Transcripts
Take one command with a stdin-backed argument that also reads an env var:
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('format')
.arg('data', arg.string().stdin().env('DATA').default('fallback'))
.action(({ args }) => {
args.data;
// ^?
});
```
The explicit `-` form is CLI-sourced, so it outranks the env var:
```bash
$ echo 'piped text' | DATA=env-value mycli format -
# args.data === 'piped text\n'
```
Omitting the slot takes the stdin fallback stage, which still sits ahead of env:
```bash
$ echo 'piped text' | DATA=env-value mycli format
# args.data === 'piped text\n'
```
With nothing piped, both forms fall through to env, then to the default:
```bash
$ DATA=env-value mycli format -
# args.data === 'env-value'
$ mycli format
# args.data === 'fallback'
```
Under `{ when: 'missing' }` a typed `-` is the literal string, not a selector:
```ts
arg.string().stdin({ when: 'missing' }).default('fallback');
```
```bash
$ echo 'piped text' | mycli format -
# args.data === '-'
```
#### `.stdin()` Constraints {#stdin-constraints}
One command has one exclusive stdin consumer. Declaring a second stdin input of
any kind, flag or argument, throws `DUPLICATE_STDIN_INPUT` at build time:
```ts
command('convert')
.flag('body', flag.string().stdin())
.arg('input', arg.string().stdin());
```
```
Only one input may consume stdin exclusively; --body already consumes stdin
Suggestion: Keep .stdin() on a single input per command, or declare every stdin input with { consume: 'broadcast' }
```
The message names the input declared first, and `details` carries the offending
input under `flag` or `arg` and the existing one under `existingFlag` or
`existingArg`. Pass `{ consume: 'broadcast' }` on every stdin input to share one
buffer among them:
```ts
command('convert')
.flag('body', flag.string().stdin({ consume: 'broadcast' }))
.arg('input', arg.string().stdin({ consume: 'broadcast' }));
// echo shared | mycli convert → flags.body === 'shared\n', args.input === 'shared\n'
```
If stdin is absent, resolution falls through to the later stages and then to
required versus optional behavior, so use `.optional()` when missing piped input
should resolve to `undefined` instead of a validation error. A required
stdin-backed argument names the routes its binding actually offers:
```
# arg.string().stdin()
Missing required argument
Suggestion: Provide a value for or pipe a value to stdin or pass '-'
# arg.string().stdin({ when: 'dash' })
Suggestion: Provide a value for or pass '-' to read stdin
# arg.string().stdin({ when: 'missing' })
Suggestion: Provide a value for or pipe a value to stdin
```
### Which source won
A handler receives `sources` beside `flags` and `args`, keyed by the same names,
holding the record the winning stage produced:
```ts twoslash
import { arg, command, wasExplicit } from '@kjanat/dreamcli';
command('deploy')
.arg('target', arg.string().env('DEPLOY_TARGET').default('local'))
.action(({ sources, out }) => {
if (!wasExplicit(sources.args.target)) out.warn('deploying to the default target');
});
```
[Value provenance](/guide/semantics#which-source-won) has the full record table,
the two stdin triggers, and worked examples.
## Syntax
An argument has no name to spell and no repetition to police. What it does have
is a position, an optionality, and a placement rule for the variadic tail.
### Declaration order
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
command('deploy')
.arg('target', arg.string().describe('Deploy target'))
.arg(
'version',
arg.string().describe('Version tag').optional(),
)
.action(({ args }) => {
type Args = typeof args;
// ^?
});
```
Arguments are positional, so order matters:
```bash
$ mycli deploy production v1.2.3
# ^^^^^^^^^^ ^^^^^^
# target version
```
### Required vs Optional
Arguments are required by default.
Use `.optional()` to make them optional:
#### Required
```ts twoslash
import { arg, type InferArg } from '@kjanat/dreamcli';
const requiredArg = arg.string();
declare const requiredVsOptional: {
required: InferArg;
};
requiredVsOptional.required;
// ^?
```
#### Optional
```ts twoslash
import { arg, type InferArg } from '@kjanat/dreamcli';
const optionalArg = arg.string().optional();
declare const requiredVsOptional: {
optional: InferArg;
};
requiredVsOptional.optional;
// ^?
```
### What The Arg Factory Does Not Have {#flag-only-surface}
The value, source, validation, and diagnostics axes are the same on both
factories. What is left on `flag` alone is exactly the syntax axis: how a token
is spelled or repeated on the command line. A positional slot has no name to
spell and no repetition to police, so there is nothing for these to configure.
| Flag member | Why it has no arg equivalent |
| --------------- | ----------------------------------------------------------------------------- |
| `flag.count()` | Counts how many times `-v` appears. A positional slot is matched by position. |
| `.negatable()` | Synthesizes a `--no-x` spelling for an existing flag name. |
| `.alias()` | Adds a second CLI spelling for a flag name. |
| `.duplicates()` | Decides what a repeated `--x` means. A positional slot is filled once. |
| `.propagate()` | Inherits a flag into subcommands. Positional slots belong to one signature. |
One member is served by another spelling rather than missing. `flag.array()` has
no `arg.array()`: use `.variadic()`, which collects the remaining positional
values into an array on any argument kind.
```ts
arg.string().variadic(); // string[]
arg.url().variadic(); // URL[]
arg.path({ mustExist: true }).variadic(); // string[], every entry checked
arg.string().variadic().separator(',').unique(); // split each token, dedupe
```
`.prompt()`, `.config()`, `arg.boolean()`, and `arg.keyValue()` were all on that
list once. They are not any more.
## Validation
Constraints, Standard Schema validators, and filesystem checks apply to every
source, so a CLI token, a piped buffer, an environment variable, a config value,
a prompt answer, and the declared default all meet the same bar.
### String constraints {#string-constraints}
`arg.string()` takes the same string constraints as `flag.string()`, either as
an options object or via chained methods (they compose, and a later chained call
overrides an earlier value, including one set in the options object):
```ts
arg.string({ nonEmpty: true, pattern: /^ghp_/ });
arg.string().nonEmpty().pattern(/^ghp_/); // equivalent
arg.string().minLength(3).maxLength(64);
```
Constraints are checked in order **nonEmpty → minLength → maxLength →
pattern** and apply to CLI, stdin, env, config, and prompted values. A
`.default()` value is checked against them too, at the point the chain declares
it, and a violation throws `INVALID_DEFAULT`. On the first failure of a resolved
value, a CLI token throws `INVALID_VALUE` while every other source reports
`CONSTRAINT_VIOLATED` (both exit code `2`). A variadic string argument checks
every value it collects. See [String constraints](/guide/flags#string-constraints)
for the option table, the declaration-time rules, and how the constraints reach
the exported JSON Schema, and
[Defaults are validated](/guide/flags#defaults-are-validated-where-they-are-declared)
for the full rule.
### Numeric constraints {#numeric-constraints}
`arg.number()` takes the same numeric constraints as `flag.number()`: an
options object or chained `.int()` / `.min()` / `.max()` / `.finite()` methods,
which compose. `finite` defaults to `true`. An argv failure uses
`INVALID_VALUE`; a non-argv failure uses the code for the failing resolution
stage (all exit `2`). See
[Numeric constraints](/guide/flags#numeric-constraints) for the full table.
```ts
arg.number({ min: 0, max: 100, int: true });
arg.number().int().min(0).max(100); // equivalent
```
### Filesystem checks {#filesystem-checks}
`arg.path()` checks run **after resolution** through the runtime adapter, so
CLI, stdin, env, config, prompted, and defaulted values are all validated. A
variadic path argument checks every value it collects, not just the first, and
`arg.keyValue(arg.path({ mustExist: true }))` checks every entry value.
`create` is only available with `type: 'directory'` (enforced at the type
level) and still rejects an existing non-directory path.
In process-free execution (`.execute()` / `runCommand()`), pass a `stat`
function via run options to enable the checks (plus `mkdir` for `create`);
without them the checks are skipped and nothing is created.
Failures carry `CONSTRAINT_VIOLATED` and name the argument:
```
Path '/data/report.csv' for argument is a file, expected a directory
```
### Standard Schema {#standard-schema}
`arg.custom()` accepts any Standard Schema v1 validator. Its output type is
inferred, sync and async validators are supported, and validation runs after
resolution, whichever of CLI, stdin, env, config, prompt, or the default
supplied the value.
`.standard()` reads the builder it is called on, so its position in the chain
decides what it validates:
```ts
arg.string().standard(upperCase).variadic(); // each collected element
arg.string().variadic().standard(atLeastOne); // the finished array
```
Before `.variadic()` the argument is still a single value, so the validator is
the element's. After it, the argument aggregates, so the validator sees the
completed array. An element failure names the position
(`[1] failed validation: …`); an aggregate failure names the argument.
## Diagnostics
Argument diagnostics quote failing values from every source by default. Mark
the argument `.sensitive()` to replace framework-controlled value text with
`` and omit raw-derived detail fields:
```ts
command('auth').arg('token', arg.string().minLength(5).stdin().sensitive());
```
```bash
$ printf 'ab' | mycli auth -
# Invalid value '' from stdin for argument : must be at least 5 characters
```
The source is still named in each message: `from env TOKEN`, `from config
auth.token`, or `from prompt`. The sensitivity rule is independent of source,
so a sensitive argv token is redacted and a non-sensitive piped value is shown.
Sensitive arguments also omit automatic default values from help; a safe
explicit `.default(value, { description })` remains visible.
[Diagnostics and redaction](/guide/semantics#diagnostics-and-redaction) is the
canonical contract.
## What's Next?
- [Flags](/guide/flags), the same six axes on the named surface
- [What the arg factory does not have](#flag-only-surface), the flag members
bound to flag syntax
- [Output](/guide/output), structured output channel
---
# Commands
Source: https://dreamcli.kjanat.dev/raw/guide/commands
# Commands
Commands are the primary building block in dreamcli.
Every CLI has at least one.
## Single Command
The simplest CLI is a single command wrapped in `cli()`:
```ts twoslash
import { cli, command, flag, arg } from '@kjanat/dreamcli';
const greet = command('greet')
.description('Greet someone')
.arg('name', arg.string().describe('Who to greet'))
.flag('loud', flag.boolean().alias('l'))
.action(({ args, flags, out }) => {
const msg = `Hello, ${args.name}!`;
out.log(flags.loud ? msg.toUpperCase() : msg);
});
cli('greet').default(greet).run();
```
## Multi-Command CLI
Use `cli()` to compose multiple commands:
```ts twoslash
import { cli, command, flag } from '@kjanat/dreamcli';
const deploy = command('deploy')
.description('Deploy the app')
.action(({ out }) => out.log('deploying...'));
const login = command('login')
.description('Authenticate')
.action(({ out }) => out.log('logging in...'));
cli('mycli')
.version('1.0.0')
.description('My tool')
.command(deploy)
.command(login)
.run();
```
## Command Groups
Nest commands under a group for `cli group subcommand` patterns:
```ts twoslash
import { command, group } from '@kjanat/dreamcli';
const migrate = command('migrate')
.description('Run migrations')
.action(({ out }) => out.log('migrating'));
const seed = command('seed')
.description('Seed database')
.action(({ out }) => out.log('seeding'));
const db = group('db')
.description('Database operations')
.command(migrate)
.command(seed);
// Usage: mycli db migrate, mycli db seed
```
Groups can be nested arbitrarily deep.
## Command Configuration
### Description and Examples
```ts twoslash
import { command } from '@kjanat/dreamcli';
command('deploy')
.description('Deploy to an environment')
.example('deploy production', 'Deploy to prod')
.example(
'deploy staging --force',
'Force deploy to staging',
);
```
An example can also be a function, resolved at render time with the program
`meta` (`name`, `version`). Use it to reference the invoked program name instead
of hardcoding it, so examples stay truthful under symlinks, `inheritName`, and
`npx x` vs a global install:
```ts twoslash
import { command } from '@kjanat/dreamcli';
command('deploy')
.description('Deploy to an environment')
// `m.name` is the actually-invoked program name (help.binName), not a literal
.example((m) => `${m.name} deploy production --force`, 'Force deploy to prod');
```
The string the callback returns is rendered verbatim as the example, in both
`--help` text and `--json` output.
### Default Command
Set a default command that runs when no subcommand is specified:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const mainCommand = command('main');
const other = command('other');
cli('mycli').default(mainCommand).command(other).run();
```
Root behavior depends on what else is visible:
- a single visible default command merges its command help into root help
- visible sibling commands keep root help command-centric
- hidden defaults remain executable but are omitted from root help and root completions
For the exact root, help, and completion rules, see [CLI Semantics](/guide/semantics).
### Version
```ts twoslash
import { cli } from '@kjanat/dreamcli';
cli('mycli').version('1.0.0').run();
```
Adds `--version` / `-V` automatically.
## Action Handler
The `.action()` callback receives a single object with typed fields:
```ts twoslash
import { command } from '@kjanat/dreamcli';
command('deploy').action(
// ---cut-start---
// prettier-ignore
// ---cut-end---
({ args, flags, ctx, meta, out }) => {
//^^^^^^^^^^^^^^^^^^^^^^^^^^^
// args — typed positional arguments
// flags — typed flag values (fully resolved)
// ctx — typed middleware context
// meta — CLI metadata: name (program name), bin (invoked binary name),
// version (program version), command (leaf command name)
// out — output channel
},
);
```
Actions can be `async`:
```ts twoslash
import { arg, command } from '@kjanat/dreamcli';
declare const deploy: (
target: string,
flags: Readonly>,
) => Promise;
command('deploy')
.arg('target', arg.string())
.action(async ({ args, flags, out }) => {
const result = await deploy(args.target, flags);
out.json(result);
});
```
## What's Next?
- Related examples: [Basic CLI](/examples/basic), [Multi-command CLI](/examples/multi-command)
- [Flags](/guide/flags) — all flag types and modifiers
- [Arguments](/guide/arguments) — positional argument types
- [Middleware](/guide/middleware) — typed context propagation
- [CLI Semantics](/guide/semantics) — exact root and resolution behavior
---
# Shell Completions
Source: https://dreamcli.kjanat.dev/raw/guide/completions
# Shell Completions
dreamcli generates completion scripts from the command schema —
always in sync with your CLI definition.
## Generating Scripts
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
import { generateCompletion } from '@kjanat/dreamcli/completion';
const serve = command('serve');
const myCli = cli('mycli').default(serve);
generateCompletion(myCli.schema, 'bash');
generateCompletion(myCli.schema, 'zsh');
generateCompletion(myCli.schema, 'fish');
generateCompletion(myCli.schema, 'powershell');
generateCompletion(myCli.schema, 'bash', {
rootMode: 'surface',
});
```
## Supported Shells
| Shell | Status |
| ---------- | --------- |
| bash | Supported |
| zsh | Supported |
| fish | Supported |
| powershell | Supported |
`generateCompletion()` currently supports `bash`, `zsh`, `fish`, and `powershell`. It lives on the
`@kjanat/dreamcli/completion` subpath; the built-in `.completions()` loads it on demand, so a program
that never renders a script does not pay for the generators.
The public `Shell` union and exported `SHELLS` tuple match this shipped set exactly.
See the [Support Matrix](/reference/support-matrix) for the current audited shell surface and the
tracked follow-up work.
## Adding a Completion Command
A common pattern is to add a `completions` subcommand:
```ts twoslash
import { arg, cli, command } from '@kjanat/dreamcli';
import { generateCompletion } from '@kjanat/dreamcli/completion';
const myCli = cli('mycli');
const completions = command('completions')
.description('Generate shell completion script')
.arg(
'shell',
arg
.custom(
(raw): 'bash' | 'zsh' | 'fish' | 'powershell' => {
if (
raw !== 'bash' &&
raw !== 'zsh' &&
raw !== 'fish' &&
raw !== 'powershell'
) {
throw new Error(
'Expected bash, zsh, fish, or powershell',
);
}
return raw;
},
)
.describe('Target shell'),
)
.action(({ args, out }) => {
const script = generateCompletion(
myCli.schema,
args.shell,
);
out.log(script);
});
myCli.command(completions);
```
dreamcli also includes a built-in `.completions()` helper on `cli()`:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const serve = command('serve');
const status = command('status');
cli('mycli')
.default(serve)
.command(status)
.completions({ rootMode: 'surface' });
```
Users install completions by sourcing the output:
```bash
# bash
mycli completions bash >> ~/.bashrc
# zsh
mycli completions zsh >> ~/.zshrc
# fish
mycli completions fish > ~/.config/fish/completions/mycli.fish
# powershell
mycli completions powershell | Out-String | Invoke-Expression
```
## What Completes
Completion scripts generated from schema include:
- Command and subcommand names
- Flag names and aliases
- Enum flag values (choices)
- Argument descriptions
## Root Completion Modes
When your CLI has a default command, root completion can expose different surfaces:
- `'subcommands'` (default) keeps hybrid CLIs command-centric at the root
- `'surface'` also exposes the default command's root-usable flags at the root
For a single visible default command, default mode still exposes that command's flags at the root.
Hidden defaults stay executable but are omitted from root completions.
For the exact root-surface rules and examples, see [CLI Semantics](/guide/semantics).
## What's Next?
- [Interactive Prompts](/guide/prompts) — prompt integration
- [Runtime Support](/guide/runtime) — cross-runtime behavior
- [CLI Semantics](/guide/semantics) — exact root help and completion surface rules
- [Support Matrix](/reference/support-matrix) — current support truth
---
# Config Files
Source: https://dreamcli.kjanat.dev/raw/guide/config
# Config Files
dreamcli discovers and loads configuration files from standard locations.
Config values participate in the flag resolution chain.
## Linking Flags to Config
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag.enum(['us', 'eu', 'ap']).config('deploy.region');
```
This resolves `deploy.region` from the config file using dot-notation.
If CLI argv and env var don't provide a value, the config file is checked next.
Config never overrides a value that was explicitly provided earlier in the chain.\
For the full precedence rules and examples, see [CLI Semantics](/guide/semantics).
## Enabling Config Discovery
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const deploy = command('deploy');
cli('mycli')
.config('mycli') // app name for file discovery
.command(deploy)
.run();
```
This searches standard locations for config files named `.mycli.json`, `mycli.config.json`, etc.
### Search Paths
Config discovery is platform-aware. The first match wins; files are never
merged across locations.
1. `--config ` or `--config=` (explicit override, skips discovery)
2. Project scope — for the working directory and each ancestor up to the
filesystem root, nearest first:
1. `.mycli.json`
2. `mycli.config.json`
3. `.config/mycli.json`
3. User scope, in order:
- Unix: `$XDG_CONFIG_HOME/mycli/config.json`, falling back to
`~/.config/mycli/config.json`
- macOS: the Unix locations, then
`~/Library/Application Support/mycli/config.json`
- Windows: `%APPDATA%\\mycli\\config.json`, falling back to
`%USERPROFILE%\\AppData\\Roaming\\mycli\\config.json`
4. System scope — `/etc/mycli/config.json` on Linux and macOS
When calling `discoverConfig()` directly, `baseDir` anchors the project-scope
walk to a directory other than `cwd` (useful for editor integrations operating
on a specific file), and custom `searchPaths` replace the whole default list.
## Custom Formats
JSON is built-in.
Add YAML, TOML, or any other format via `configFormat()`:
::: code-group
```ts twoslash [Bun built-ins]
import { cli } from '@kjanat/dreamcli';
import { configFormat } from '@kjanat/dreamcli/config';
cli('mycli')
.config('mycli')
.configLoader(
configFormat(['yaml', 'yml'], Bun.YAML.parse),
)
.configLoader(configFormat(['toml'], Bun.TOML.parse))
.run();
```
```ts twoslash [npm packages]
import { cli } from '@kjanat/dreamcli';
import { configFormat } from '@kjanat/dreamcli/config';
import { parse as parseYaml } from 'yaml';
import { parse as parseTOML } from '@iarna/toml';
cli('mycli')
.config('mycli')
.configLoader(configFormat(['yaml', 'yml'], parseYaml))
.configLoader(configFormat(['toml'], parseTOML))
.run();
```
:::
`configFormat(exts, parser)` creates a loader config from the extension list and parse function,
and `configLoader(loader)` registers that loader with the CLI.
Each extension should only be registered once per chain — registering the same extension
with different parsers causes duplicate loading.
The parsed value still has to be a plain object, so YAML scalars,
arrays, `null`, or multi-document YAML that parses to an array will fail as `CONFIG_PARSE_ERROR`.
## What's Next?
- [Shell Completions](/guide/completions) — generate completion scripts
- [Flags](/guide/flags) — full flag resolution chain
- [CLI Semantics](/guide/semantics) — exact precedence and masking behavior
---
# Errors
Source: https://dreamcli.kjanat.dev/raw/guide/errors
# Errors
dreamcli provides structured errors with codes, suggestions, and JSON serialization.
## CLIError
```ts twoslash
import { CLIError } from '@kjanat/dreamcli';
const target = 'production';
const region = 'us';
throw new CLIError('Deployment failed', {
code: 'DEPLOY_FAILED',
exitCode: 1,
suggest: 'Check your credentials with `mycli login`',
details: { target, region },
});
```
### Fields
| Field | Type | Description |
| ---------- | -------------------- | ------------------------------------------ |
| `message` | `string` | Human-readable error message |
| `code` | `string` | Stable machine identifier |
| `exitCode` | `number` | Process exit code (default varies by type) |
| `suggest` | `string` | Actionable hint for the user |
| `details` | `unknown` | Structured payload for JSON output |
| `cause` | `Error \| undefined` | Underlying cause (optional) |
### Type Safety
```ts twoslash
import { CLIError } from '@kjanat/dreamcli';
// @errors: 2322
new CLIError('Deployment failed', {
code: 'DEPLOY_FAILED',
exitCode: '1',
});
```
## Error Types
```ts twoslash
import {
CLIError,
ParseError,
ValidationError,
} from '@kjanat/dreamcli';
```
- **`CLIError`** — base error for application-level failures
- **`ParseError`** — argv parsing failures (unknown flags, bad syntax)
- **`ValidationError`** — value validation failures (wrong type, missing required)
Parse and validation errors include "did you mean?" suggestions automatically.
## Type Guards
```ts twoslash
import {
cli,
isCLIError,
isParseError,
isValidationError,
} from '@kjanat/dreamcli';
const myCli = cli('mycli');
try {
await myCli.run();
} catch (err) {
if (isParseError(err)) {
// handle parse error
} else if (isValidationError(err)) {
// handle validation error
} else if (isCLIError(err)) {
// handle generic CLI error
}
}
```
## JSON Mode
In `--json` mode, errors serialize to structured JSON:
```json
{
"error": {
"message": "Deployment failed",
"code": "DEPLOY_FAILED",
"suggest": "Check your credentials with `mycli login`",
"details": { "target": "production", "region": "us" }
}
}
```
## What's Next?
- [Middleware](/guide/middleware) — error handling in middleware chains
- [Output](/guide/output) — output channel behavior
---
# Flags
Source: https://dreamcli.kjanat.dev/raw/guide/flags
# Flags
Flags are the richest primitive in `dreamcli`.
Each flag declaration configures parsing, type inference, resolution, help text, and shell completions.
## Six Axes
A flag declaration decides six separate things. They compose freely, and every
section on this page belongs to exactly one of them:
| Axis | What it decides | Declared with |
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------ |
| [Value](#flag-types) | what one value is and what type it resolves to | `flag.string()`, `flag.number()`, `flag.url()`, `flag.custom()`, and the rest |
| [Cardinality](#cardinality) | how many values the flag carries and how they combine | `flag.array()`, `flag.keyValue()`, `flag.count()`, `.split()`, `.unique()` |
| [Sources](#sources) | where a value may come from and which one wins | `.stdin()`, `.env()`, `.config()`, `.prompt()`, `.default()`, `.required()` |
| [Syntax](#syntax) | how the flag is spelled and repeated on the command line | the name, `.alias()`, `.negatable()`, `.duplicates()`, `.propagate()` |
| [Validation](#validation) | what a resolved value has to satisfy | constraints, `.standard()`, `flag.path()` filesystem checks |
| [Diagnostics](#diagnostics) | whether diagnostics and help may display the value | `.sensitive()` |
The factory methods are facades over that decomposition, and they stay the way
you write a CLI. The table is here to say which question each one answers.
Four of the six axes are identical on the [argument](/guide/arguments) surface.
Value and validation are the same set of kinds, options, parsers, and error
codes. Sources are the same six stages in the same order. Diagnostics use the
same sensitivity rule. Cardinality differs in how the command line spells a
collection, and syntax is the axis that is genuinely flag-only, which is what
[the arg factory does not have](/guide/arguments#flag-only-surface) enumerates.
Every axis at once:
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag
// value
.string()
// validation
.nonEmpty()
// cardinality is `one` here, which every scalar kind declares
// sources
.stdin()
.env('DEPLOY_REGION')
.config('deploy.region')
.prompt({ kind: 'input', message: 'Region?' })
.default('us')
// diagnostics
.sensitive()
// syntax
.alias('r')
.describe('Target region')
.deprecated('Use --target instead')
.propagate();
```
## Value {#flag-types}
Ten kinds describe what one value is. Each one is a codec plus a resolved type,
and each has an `arg` counterpart taking the same options and producing the same
value.
### String
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const stringFlag = flag.string();
declare const flagTypes: {
string: InferFlag;
};
// ---cut---
flagTypes.string;
// ^?
```
`flag.string()` also accepts [string constraints](#string-constraints).
### Number
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const numberFlag = flag.number();
declare const flagTypes: {
number: InferFlag;
};
// ---cut---
flagTypes.number;
// ^?
```
`flag.number()` also accepts [numeric constraints](#numeric-constraints).
### Boolean
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const booleanFlag = flag.boolean();
declare const flagTypes: {
boolean: InferFlag;
};
// ---cut---
flagTypes.boolean;
// ^?
```
### Enum
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const enumFlag = flag.enum(['us', 'eu', 'ap']);
declare const flagTypes: {
enum: InferFlag;
};
// ---cut---
flagTypes.enum;
// ^?
```
### Custom
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const customFlag = flag.custom((v) => new URL(String(v)));
declare const flagTypes: {
custom: InferFlag;
};
// ---cut---
flagTypes.custom;
// ^?
```
`flag.custom()` takes a parse function or a Standard Schema validator. See
[Custom parsing](#custom-parsing).
### URL
Parses into a `URL`; invalid URLs are rejected with the flag named in the
error. Optionally restrict protocols:
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const urlFlag = flag.url({ protocols: ['https'] });
declare const flagTypes: {
url: InferFlag;
};
// ---cut---
flagTypes.url;
// ^?
```
### Path
The value stays a `string` (help shows ``), with optional filesystem
checks that run **after resolution**. See
[Filesystem checks](#filesystem-checks).
```ts
flag.path(); // any string
flag.path({ mustExist: true }); // rejects missing paths
flag.path({ type: 'directory' }); // must exist and be a directory
flag.path({ type: 'directory', mustExist: false }); // missing passes; existing must be a directory
flag.path({ type: 'directory', create: true }); // created recursively when missing
```
`flag.path()` resolves as a `string`, so a value read from stdin keeps the
buffer byte for byte, trailing line terminator included. `echo ./docs | mycli`
reaches a `mustExist` check as `'./docs\n'` and fails. `.stdin({ trim: true })`
drops that terminator, and `printf './docs'` works too.
### Date
Accepts strict ISO-8601 (`2026-07-10`, `2026-07-10T14:30:00Z`) and parses into
a `Date`. Lenient `Date.parse` inputs (`'0'`, `'March 5'`) and
calendar-invalid dates (`2026-02-31`) are rejected. Offset-less datetimes
(`2026-07-10T14:30`) are treated as **UTC**, not local time, so `min` / `max`
acceptance never depends on the machine's timezone. Optional inclusive
`min` / `max` bounds:
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const dateFlag = flag.date({ min: new Date('2020-01-01') });
declare const flagTypes: {
date: InferFlag;
};
// ---cut---
flagTypes.date;
// ^?
```
### Duration
Accepts `'30s'`, `'5m'`, `'1.5h'`, `'250ms'`, `'2d'`, compounds like
`'1h30m'`, or a bare millisecond count, and resolves to **milliseconds**:
```ts
flag.duration().default(30_000);
// --timeout 45s → 45000
// --timeout 1h30m → 5400000
```
### Bytes
Accepts `'512mb'`, `'1.5gb'`, `'64kb'`, `'100b'` or a bare byte count, and
resolves to **bytes**. Units are binary (`1kb` = 1024) and case-insensitive:
```ts
flag.bytes().default(10 * 1024 ** 2);
// --max-size 512kb → 524288
```
::: tip Positional equivalents
`arg.string()`, `arg.number()`, `arg.boolean()`, `arg.enum()`, `arg.custom()`,
`arg.url()`, `arg.path()`, `arg.date()`, `arg.duration()`, and `arg.bytes()`
take the same options and produce the same values. See
[Purpose-built argument kinds](/guide/arguments#purpose-built-argument-kinds).
:::
## Cardinality
How many values a flag carries is decided separately from what each value
means. Four forms exist, and every source fills whichever one the flag declares:
| Form | Declared by | Unset resolves to |
| ------- | ---------------- | ----------------- |
| one | every value kind | `undefined` |
| list | `flag.array()` | `[]` |
| entries | `flag.keyValue()`| `{}` |
| count | `flag.count()` | `0` |
### Array
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const arrayFlag = flag.array(flag.string());
declare const flagTypes: {
array: InferFlag;
};
// ---cut---
flagTypes.array;
// ^?
```
Optional array flags resolve to `[]` when no source or default provides a value.
Required array flags fail when no value resolves.
The element builder describes the *value shape only*: kinds and their value
constraints (`flag.number({ int: true, min: 0 })`, `.nonEmpty()`, `.pattern()`,
enum values, custom `parseFn`). Flag-level settings such as `.alias()`,
`.env()`, `.default()`, `.prompt()`, and `.describe()` belong on the array
itself and are a **compile error** in element position, since an element schema
would silently ignore them:
```ts
flag.array(flag.number({ min: 1 })).env('PORTS').describe('Ports'); // ✓
// flag.array(flag.number().env('PORTS')) ✗ compile error: env the array, not the element
```
`flag.string()`, `flag.number()`, `flag.boolean()`, `flag.enum()`,
`flag.custom()`, `flag.path()`, `flag.url()`, `flag.date()`, `flag.duration()`,
and `flag.bytes()` are element-eligible. `flag.count()`, `flag.keyValue()`, and
a nested `flag.array()` are rejected in element position, also at compile time.
### Count
Resolves to how many times the flag appears, the classic verbosity pattern.
`-vvv`, `-v -v -v`, and `--verbose --verbose --verbose` all yield `3`; absent
yields `0`. An explicit value (`--verbose=2`, env, config) sets the count
directly. Count flags take no value token and are not promptable:
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const countFlag = flag.count().alias('v');
declare const flagTypes: {
count: InferFlag;
};
// ---cut---
flagTypes.count;
// ^?
```
### Key-Value
Repeated `KEY=VALUE` occurrences merge into a `Record`
(docker/kubectl `--env` style):
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const kvFlag = flag.keyValue().alias('e');
declare const flagTypes: {
env: InferFlag;
};
// ---cut---
flagTypes.env;
// ^?
```
Env values carry comma-delimited pairs (`A=1,B=2`) by default, which means an
env-sourced value cannot itself contain a comma. Change the env policy with
`.split({ env: ';' })` or `.split({ env: 'json' })`, or use a config file, whose
plain object is read natively. See [Collections](#collections) for the full
per-source table, the duplicate-key policies, and reading entries from stdin.
Array and key-value flags are the optional flag kinds that still resolve to a
value when unset: arrays fall back to `[]`, key-value flags to `{}`.
`arg.keyValue()` is the positional counterpart, and it takes the same kind of
element builder. See
[Key-value arguments](/guide/arguments#key-value-arguments).
### Collections {#collections}
`flag.array()` and `flag.keyValue()` are the two collection kinds. Both fill
from every source the flag declares under one set of rules. `flag.count()` sits
on the same axis, carrying an occurrence count rather than values.
The sections below apply to `flag.array()` and `flag.keyValue()` alike.
#### How each source spells a collection
Each source decodes the text it carries under its own policy. `.split()` sets
all three at once, and `.separator()` sets the CLI one on its own:
```ts
flag.array(flag.string()).split({ cli: ',', env: { format: 'json' }, stdin: 'lines' });
// --tag a,b → ['a', 'b']
// TAGS='["a","b"]' → ['a', 'b']
// printf 'a\nb\n' | … → ['a', 'b']
```
| Source | Accepts | Default |
| ------ | --------------------------------------------- | ---------------------------- |
| cli | a delimiter, `'whole'` | `'whole'`, or `.separator()` |
| env | a delimiter, `'whole'`, `'json'` | `','` |
| stdin | a delimiter, `'whole'`, `'lines'`, `'json'` | `'lines'` |
| config | a native array or object; a string uses `env` | native |
The strings `'whole'`, `'lines'`, and `'json'` name their format. Every other
string is the delimiter to split on, so `.split({ env: ';' })` splits env values
on a semicolon. `{ format: 'json' }` and the bare `'json'` mean the same thing.
A format a source does not accept throws `INVALID_SCHEMA` where the flag is
declared, so `.split({ cli: 'json' })` is rejected at build time.
Each `.split()` call sets only the sources it names, so
`.split({ env: ';' }).split({ stdin: 'json' })` keeps both.
JSON is never guessed. An env value parses as JSON only when the binding says
so, which is why `TAGS='["a","b"]'` under the default comma policy resolves to
the two literal segments `['["a"', '"b"]']`.
#### Per-occurrence splitting on the command line
By default each occurrence of a collection flag contributes exactly one element
(`--tag a --tag b`). With a CLI delimiter, each occurrence is also split, so
comma-separated input works alongside repetition. Each element is coerced on its
own, so a failure names the offending element rather than the whole token:
```ts
flag.array(flag.enum(['us', 'eu', 'ap'])).separator(',').unique();
// --region us,eu --region eu → ['us', 'eu']
// --region us,mars → Invalid value 'mars' for flag --region. Allowed: us, eu, ap
```
`.separator(',')` and `.split({ cli: ',' })` write the same field, so pick
either. `.unique()` deduplicates the final resolved array in first-seen order
with `Set` semantics, whichever source produced the values. It is available on
`flag.array()` only.
::: warning A CLI separator is no longer inherited
Through 3.x, `.separator('|')` also decided how an env or config string
decoded. Each source now carries its own policy. Write
`.split({ cli: '|', env: '|' })` where the old coupling was intended.
:::
#### Reading a collection from stdin
`.stdin()` is available on both collection kinds. A `-` occurrence stands for
the whole stdin source at the position it holds, and what the buffer decodes to
under the stdin policy is spliced in there:
```ts
flag.array(flag.string()).stdin();
```
```bash
$ printf 'a\nb\n' | mycli send --tag before --tag - --tag after
# flags.tag === ['before', 'a', 'b', 'after']
$ printf 'a\nb\n' | mycli send --tag - --tag z
# flags.tag === ['a', 'b', 'z']
$ mycli send --tag -
# nothing piped: the flag produces no CLI value and env, config, prompt,
# and the default stay reachable, so an undeclared source leaves []
$ mycli send --tag a --tag -
# nothing piped beside a typed occurrence:
# No piped stdin for the '-' occurrence of flag --tag
```
A `-` the user typed beside other occurrences fails when nothing was piped,
rather than shortening the collection behind their back. The code is
`MISSING_STDIN`. Occurrences of nothing but `-` are the whole value, so they
fall through to the later sources the way an absent flag does.
A flag that declares no stdin binding treats `-` as an ordinary element, and the
stream is never read:
```bash
$ printf 'piped' | mycli send --tag - --tag x # flag.array(flag.string())
# flags.tag === ['-', 'x']
```
An explicit `-` and the implicit fallback decode identically. A spliced read is
CLI-sourced, so it still outranks env, config, prompt, and the default. Two `-`
occurrences splice the buffer twice, since the whole buffer is what each one
stands for: `--tag - --tag -` over `'a\n'` resolves to `['a', 'a']`.
A binding where `-` selects stdin never receives that token as a literal value,
on either surface. The token names the source before anything reads it as text.
A binding with `{ when: 'missing' }` remains stdin-enabled but leaves `-`
literal.
Broadcast consumers each decode the one shared buffer under their own binding,
so a line-split array flag and a JSON key-value flag can read the same pipe:
```ts
command('run')
.flag('tag', flag.array(flag.string()).stdin({ consume: 'broadcast' }))
.flag('vars', flag.keyValue().split({ stdin: 'json' }).stdin({ consume: 'broadcast' }));
// echo '{"A":"1"}' | mycli run
// → flags.tag === ['{"A":"1"}'], flags.vars === { A: '1' }
```
#### Key-value entries
`flag.keyValue()` merges `KEY=VALUE` occurrences into a record, splitting each
at the **first** `=`, so `--env A=b=c` yields `{ A: 'b=c' }`. Optional key-value
flags resolve to `{}` when no source or default provides a value; required flags
fail when no value resolves. An element builder gives each entry value its own
codec and checks:
```ts
flag.keyValue(flag.path({ mustExist: true }));
// --v src=/etc/hosts → every entry value is checked on disk
```
`.duplicateKeys()` decides what a repeated key means, on every source rather
than only across repeated CLI occurrences:
| Policy | Meaning |
| --------- | ------------------------------------------- |
| `'last'` | the later occurrence wins (the default) |
| `'first'` | the earlier occurrence wins |
| `'error'` | a repeat fails with `CONSTRAINT_VIOLATED` |
```ts
command('deploy').flag('v', flag.keyValue().duplicateKeys('first').alias('e').env('VARS'));
// -e A=1 -e A=2 → { A: '1' }
// VARS='A=1,A=2' → { A: '1' }
```
Under `'error'`, the message names the source that carried the repeat and quotes
the key unless the input is sensitive: `Duplicate key 'A' from env VARS for
flag --v` for the environment and `Duplicate key 'A' for flag --v` for
occurrences the user typed. A key spliced in from a pipe reads `from stdin`.
`flag.keyValue().sensitive()` reports `Duplicate key ''` from every
source instead.
JSON decoding does not preserve repeated object member names, so
`.duplicateKeys()` cannot reliably detect them for `.split({ env: 'json' })`.
## Sources
Every flag resolves through the same ordered chain. Each step past the command
line is opt-in, and the first source that provides a value wins:
```mermaid
flowchart LR
A[CLI argv] --> S[Piped stdin]
S --> B[Environment variable]
B --> C[Config file]
C --> D[Interactive prompt]
D --> E[Default value]
```
Required flags that don't resolve produce a structured error before the action
handler runs.
Positional arguments walk the same order; see [Arguments](/guide/arguments).
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag
.enum(['us', 'eu', 'ap'])
.stdin()
.env('DEPLOY_REGION')
.config('deploy.region')
.prompt({ kind: 'select', message: 'Which region?' })
.default('us');
```
Resolution order:
1. `--region eu` on the command line
2. Piped stdin, for a flag that declares `.stdin()`
3. `DEPLOY_REGION=eu` in environment
4. `deploy.region: "eu"` in config file
5. Interactive select prompt (TTY only)
6. Default value `"us"`
### Required vs Optional
Presence is the last question the sources axis answers: what happens when the
whole chain produces nothing.
#### Optional
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const optionalFlag = flag.string();
declare const requiredVsOptional: {
optional: InferFlag;
};
// ---cut---
requiredVsOptional.optional;
// ^?
```
#### Defaulted
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const defaultedFlag = flag.string().default('hello');
declare const requiredVsOptional: {
defaulted: InferFlag;
};
// ---cut---
requiredVsOptional.defaulted;
// ^?
```
#### Required
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const requiredFlag = flag.string().required();
declare const requiredVsOptional: {
required: InferFlag;
};
// ---cut---
requiredVsOptional.required;
// ^?
```
#### Boolean
```ts twoslash
import { flag, type InferFlag } from '@kjanat/dreamcli';
const booleanFlag = flag.boolean();
declare const requiredVsOptional: {
boolean: InferFlag;
};
// ---cut---
requiredVsOptional.boolean;
// ^?
```
### STDIN-Backed Flags {#stdin-backed-flags}
`.stdin()` lets a flag read its value from piped stdin. It is available on every
kind but `count`, which counts occurrences rather than reading a value:
`flag.count().stdin()` does not compile, and both the builder and the equivalent
definition object throw `INVALID_SCHEMA`. A scalar takes the whole buffer; a
collection decodes it into elements, one per line by default, and splices them
where a `-` occurrence sits, which [Collections](#collections) covers in full.
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
command('send')
.flag('body', flag.string().stdin().describe('Message body'))
.action(({ flags }) => {
flags.body;
// ^?
});
```
```bash
echo hi | mycli send # body = 'hi\n'
mycli send --body - # body reads stdin
mycli send --body hello # body = 'hello'
```
The stdin stage sits ahead of env, so a flag set in the environment still reads a
pipe and the pipe wins. Passing the sentinel `-` selects stdin too, but keeps CLI
precedence: the bytes come from the pipe and every later stage stays out of the
way. When nothing was piped, both forms fall through to env, config, prompt, and
the default. A scalar `-` is the whole value, so dropping it loses nothing. That
is where a scalar and a collection part: `--tag a --tag -` with nothing piped
fails because dropping the occurrence would shorten the list.
The whole buffer becomes the value. Codecs that preserve text terminators keep
it byte for byte, so `echo hi | mycli` gives a string flag `'hi\n'` and a path
keeps the terminator too. Decoding codecs remove one framing terminator first,
so `echo true` reaches `flag.boolean()` as `true`.
`{ trim: true }` drops that terminator for a string flag too, so
`echo ./dist | mycli clean --path -` reaches a `mustExist` check as `'./dist'`.
It applies to a single value; a collection's terminators separate its elements
and `.split({ stdin })` decides them.
#### Choosing when stdin is read
`.stdin()` takes `{ when, consume, trim }`:
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag.string().stdin(); // '-' or an absent flag reads stdin
flag.string().stdin({ when: 'dash' }); // only an explicit '-'
flag.string().stdin({ when: 'missing' }); // only an absent flag; '-' stays literal
flag.string().stdin({ consume: 'broadcast' }); // shares the buffer with other inputs
flag.string().stdin({ trim: true }); // drops one trailing terminator
```
Stdin is read at most once per invocation, and only when one of these bindings
would actually fire. If the user does not provide `-`, a `when: 'dash'` flag
does not read the stream.
Help names the binding beside the flag's other sources: `[stdin]` for the
default, `[stdin: '-']` for `{ when: 'dash' }`, and `[stdin: when omitted]` for
`{ when: 'missing' }`. See [Source annotations](/guide/help#source-annotations).
#### Worked Transcripts
Take one flag that reads stdin and an env var:
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
command('send')
.flag('body', flag.string().stdin().env('BODY').default('fallback'))
.action(({ flags }) => {
flags.body;
// ^?
});
```
The explicit `-` form is CLI-sourced, so it outranks the env var:
```bash
$ echo 'piped text' | BODY=env-value mycli send --body -
# flags.body === 'piped text\n'
```
Omitting the flag takes the stdin fallback stage, which still sits ahead of env:
```bash
$ echo 'piped text' | BODY=env-value mycli send
# flags.body === 'piped text\n'
```
With nothing piped, both forms fall through to env, then to the default:
```bash
$ BODY=env-value mycli send --body -
# flags.body === 'env-value'
$ mycli send
# flags.body === 'fallback'
```
Under `{ when: 'missing' }` a typed `-` is the literal string, not a selector:
```ts
flag.string().stdin({ when: 'missing' }).default('fallback');
```
```bash
$ echo 'piped text' | mycli send --body -
# flags.body === '-'
```
Under `{ when: 'dash' }` an omitted flag leaves the pipe unread, so env wins:
```ts
flag.string().stdin({ when: 'dash' }).env('BODY').default('fallback');
```
```bash
$ echo 'piped text' | BODY=env-value mycli send
# flags.body === 'env-value'
```
A required stdin-backed flag with nothing to read names the routes its binding
actually offers:
```
# flag.string().stdin().required()
Missing required flag --body
Suggestion: Provide --body or pipe a value to stdin or pass --body -
# flag.string().stdin({ when: 'dash' }).required()
Suggestion: Provide --body or pass --body - to read stdin
# flag.string().stdin({ when: 'missing' }).required()
Suggestion: Provide --body or pipe a value to stdin
```
#### `.stdin()` Constraints {#stdin-constraints}
One command has one exclusive stdin consumer. Declaring a second stdin input of
any kind, flag or argument, throws `DUPLICATE_STDIN_INPUT` at build time:
```ts
command('convert')
.flag('body', flag.string().stdin())
.arg('input', arg.string().stdin());
```
```
Only one input may consume stdin exclusively; --body already consumes stdin
Suggestion: Keep .stdin() on a single input per command, or declare every stdin input with { consume: 'broadcast' }
```
The message names the input declared first, and `details` carries the offending
input under `flag` or `arg` and the existing one under `existingFlag` or
`existingArg`. Pass `{ consume: 'broadcast' }` on every stdin input to share one
buffer among them:
```ts
command('convert')
.flag('body', flag.string().stdin({ consume: 'broadcast' }))
.arg('input', arg.string().stdin({ consume: 'broadcast' }));
// echo shared | mycli convert → flags.body === 'shared\n', args.input === 'shared\n'
```
`flag.count()` is the one kind that cannot read stdin, since it counts
occurrences rather than reading a value. The builder method is refused by the
compiler, and both it and the definition object throw the same error:
```ts
createFlagSchema('count', { stdin: {} });
```
```
Flag schema field 'stdin' is not available on kind 'count'
Suggestion: Drop 'stdin' or declare the flag as one of: string, number, boolean, enum, custom, array, keyValue
```
### Which source won
A handler receives `sources` beside `flags` and `args`, keyed by the same names,
holding the record the winning stage produced:
```ts twoslash
import { command, flag, wasExplicit } from '@kjanat/dreamcli';
command('build')
.flag('out', flag.string().env('OUT_DIR').default('dist'))
.action(({ flags, sources, out }) => {
if (wasExplicit(sources.flags.out)) out.info(`overriding with ${flags.out}`);
if (sources.flags.out?.stage === 'env') out.info(`from ${sources.flags.out.envVar}`);
});
```
[Value provenance](/guide/semantics#which-source-won) has the full record table,
the two stdin triggers, and worked examples for explicit-wins merges and
`--explain` style diagnostics.
## Syntax
The syntax axis is what a flag has and a positional argument does not: a name to
spell, alternate spellings, a negated form, a rule for repeats, and inheritance
into subcommands.
### 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.
The name you declare is the name you read; handler keys are never case-converted. 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 twoslash
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. Either way users can type both spellings; see
[Spelling parity](#spelling-parity-kebab-camel) below.
### Spelling Parity (kebab ↔ camel) {#spelling-parity-kebab-camel}
On the command line, every flag name and long alias also accepts its kebab↔camel counterpart:
a flag named `dry-run` matches both `--dry-run` and `--dryRun`, and a flag named `dryRun`
matches both spellings too. The handler key is always the canonical name; parity is purely
CLI-token sugar, and help, completions, and "did you mean" suggestions advertise only the
declared spelling.
Two escape hatches:
- **Per pair, automatic**: if a command explicitly defines *both* spellings as separate flags
(`do-this` and `doThis`), parity is disabled for that pair and each spelling matches only its
own flag.
- **Globally**: pass `flags: { caseParity: false }` to the `cli()` factory (or to
`cmd.run()` / `.execute()` options) to accept declared spellings only:
```ts twoslash
import { cli, command, flag } from '@kjanat/dreamcli';
cli('mycli', { flags: { caseParity: false } })
.command(
command('build')
.flag('dry-run', flag.boolean())
.action(() => {}),
);
// $ mycli build --dry-run → ok
// $ mycli build --dryRun → Unknown flag --dryRun
```
Parity is not case-insensitivity: `--DRY-RUN` never matches.
### 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 twoslash
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']`.
### Negatable Booleans
`.negatable()` gives a boolean flag a negated spelling that sets it to `false`, the classic
`--sandbox` / `--no-sandbox` pair as **one logical flag**:
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
command('build')
.flag(
'sandbox',
flag.boolean().default(true).negatable().describe('Run the build sandboxed'),
)
.action(({ flags, out }) => {
if (!flags.sandbox) out.warn('sandbox disabled');
});
// $ mycli build → sandbox = true (default)
// $ mycli build --sandbox → sandbox = true
// $ mycli build --no-sandbox → sandbox = false
```
Semantics:
- Both spellings are the same flag. Under the default `.duplicates('last')`
policy, the last CLI occurrence wins across them. `.duplicates('first')` and
`.duplicates('error')` likewise apply across both spellings.
- The negated spelling is presence-only. `--no-sandbox=true` is an error. Explicit values stay
on the positive spelling (`--sandbox=false`).
- Help renders the pair as one entry: `--[no-]sandbox`.
- Only CLI tokens are affected; env, config, prompt, and default resolution are unchanged.
Customize the spelling with `alias` (rendered as its own form) or keep it parseable but
unadvertised with `hidden`:
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag.boolean().negatable({ alias: 'plain' }); // --color / --plain
flag.boolean().negatable({ hidden: true }); // --no-… parses, help shows only the positive form
```
The negated spelling participates in schema-time collision validation. Defining `no-sandbox` as
its own flag next to a negatable `sandbox` is an error.
### Duplicate Policy {#duplicate-policy}
By default, repeating a singleton flag on the command line is last-write-wins
(`--region us --region eu` → `'eu'`). For flags that are configuration knobs rather than
mergeable inputs, `.duplicates()` makes repeats explicit:
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
command('run')
.flag('spawn', flag.enum(['session', 'same-dir', 'worktree']).duplicates('error'))
.flag('capacity', flag.number().duplicates('first'))
.action(() => {});
// $ mycli run --spawn session --spawn worktree
// # → Error: Flag --spawn may only be specified once (code: DUPLICATE_FLAG)
// $ mycli run --capacity 2 --capacity 9 → capacity = 2 (first wins)
```
- `'last'` sets last occurrence wins (default, historic behavior).
- `'first'` sets first occurrence wins; later occurrences still consume their value token, they just
don't overwrite.
- `'error'` makes a second occurrence throw a `ParseError` with code `DUPLICATE_FLAG` and
`details: { flag, count, values }`.
Occurrences are counted per **logical flag**. Aliases, negated spellings, and
[parity spellings](#spelling-parity-kebab-camel) all count toward the same flag. Only CLI tokens
count: a flag set both on the CLI and via env/config follows the normal precedence chain and is
never a duplicate. `.duplicates()` is unavailable on `array`, `count`, and `keyValue` flags,
which inherently accumulate.
### Propagation
Flags marked with `.propagate()` are inherited by all subcommands:
```ts twoslash
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');
}
});
// ---cut-start---
// ---cut-end---
cli('mycli').command(
command('deploy')
.flag('verbose', flag.boolean().alias('v').propagate())
.command(nested),
);
```
A child command that defines the same flag name masks the ancestor's propagated
flag, and blocks it from reaching deeper descendants. See
[Propagation and masking](/guide/semantics#propagation-and-masking).
For the exact parser rules around repeated flags, short-flag stacking, `--`
separator handling, and `--no-*` spellings, see [CLI Semantics](/guide/semantics).
## Validation
Constraints and Standard Schema validators describe what a resolved value has to
satisfy. Filesystem checks do the same when execution supplies a `stat`
capability; process-free `.execute()` and `runCommand()` calls without one skip
those checks. The rules cover every value source, including defaults.
### String constraints {#string-constraints}
`flag.string()` accepts optional string constraints, either as an options
object or via chained methods (they compose, and a later chained call overrides an
earlier value, including one set in the options object):
```ts
flag.string({ nonEmpty: true, pattern: /^ghp_/ });
flag.string().nonEmpty().pattern(/^ghp_/); // equivalent
flag.string().minLength(3).maxLength(64);
```
| Option | Meaning | Default |
| ----------- | ------------------------------------------- | ------- |
| `nonEmpty` | Reject the empty string `''` | `false` |
| `minLength` | Inclusive minimum length (UTF-16 units) | none |
| `maxLength` | Inclusive maximum length (UTF-16 units) | none |
| `pattern` | RegExp the value must match | none |
Constraints are checked in order **nonEmpty → minLength → maxLength →
pattern** and apply to every source: CLI, stdin, env, config, prompt, and the
declared default. On the first failure, a CLI token throws `INVALID_VALUE` while
every other source reports `CONSTRAINT_VIOLATED` (both exit code `2`). Anchor
patterns with `^`/`$` for full-string matching; length bounds must be
non-negative integers and `minLength` must not exceed `maxLength` (violations
throw where the flag is declared). String constraints are surfaced in the
exported JSON Schema as `minLength` / `maxLength` / `pattern`.
The same options and methods are available on
[`arg.string()`](/guide/arguments#string-constraints) for positional arguments.
### Numeric constraints {#numeric-constraints}
`flag.number()` accepts optional numeric constraints, either as an options
object or via chained methods (they compose, and 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 }
```
| Option | Meaning | Default |
| -------- | ------------------------------- | ------- |
| `min` | Inclusive lower bound | none |
| `max` | Inclusive upper bound | none |
| `int` | Require an integer | `false` |
| `finite` | Reject `Infinity` / `-Infinity` | `true` |
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").
::: warning 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:
| Input (`flag.number({ int: true, min: 0, max: 100 })`) | Result |
| ------------------------------------------------------ | ------------------------------------ |
| `42` | accepted |
| `0` / `100` | accepted (bounds are inclusive) |
| `NaN` | rejected, invalid number |
| `Infinity` | rejected, `must be a finite number` |
| `3.7` | rejected, `must be an integer` |
| `-1` | rejected, `must be >= 0` |
| `101` | rejected, `must be <= 100` |
The same options and methods are available on `arg.number()` for positional
arguments.
### Filesystem checks {#filesystem-checks}
`flag.path()` checks run **after resolution** through the runtime adapter, so
CLI, stdin, env, config, prompted, and defaulted values are all validated.
`create` is only available with `type: 'directory'` (enforced at the type
level) and still rejects an existing non-directory path.
In process-free execution (`.execute()` / `runCommand()`), pass a `stat`
function via run options to enable the checks (plus `mkdir` for `create`);
without them the checks are skipped and nothing is created.
Failures carry `CONSTRAINT_VIOLATED`. An element builder puts the check on each
element, so `flag.array(flag.path({ mustExist: true }))` probes every collected
value and `flag.keyValue(flag.path({ mustExist: true }))` probes every entry
value.
### Custom parsing {#custom-parsing}
```ts twoslash
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, and the
message your function threw is shown verbatim.
### Standard Schema validators
`flag.custom()` also accepts any [Standard Schema v1](https://standardschema.dev/schema)
validator, including Zod, Valibot, and ArkType schemas:
```ts
import { flag } from '@kjanat/dreamcli';
import { z } from 'zod';
const port = flag.custom(z.coerce.number().int().min(1).max(65_535));
// inferred type: number | undefined
```
Source values run through Standard Schema validation after resolution. A
caller-declared default runs through a synchronous validator when it is
declared; asynchronous validators, filesystem checks, and `flag.count()`
validation remain deferred until resolution. Validation issues become
`CONSTRAINT_VIOLATED` errors.
#### Element and aggregate validators
On a collection, where the validator sits decides what it sees. A validator on
the element builder runs once per resolved element or entry value. A validator
on the collection builder runs once on the completed array or record, after
every element has passed its own:
```ts
flag.array(flag.string().standard(upperCase)); // each element
flag.array(flag.string()).standard(atLeastOne); // the finished array
flag.keyValue(flag.string().standard(upperCase)); // each entry value
flag.keyValue().standard(hasApiKey); // the finished record
```
The two produce different messages. An element failure names the position, an
aggregate failure names the flag:
```
--tag[1] failed validation: must be upper case
--vars.KEY failed validation: must be upper case
--tag failed validation: at least one tag is required
```
`.standard()` on a scalar builder is the element validator, since a scalar
carries one value. An aggregate validator on a kind that does not aggregate
throws `INVALID_SCHEMA`.
### Defaults are validated where they are declared
A `.default()` value is already the typed value, so it is validated rather than
decoded. String and number constraints, element and aggregate Standard Schema
validators, and the shape the flag's cardinality requires all apply to it. A
violation whose verdict is available synchronously throws `INVALID_DEFAULT`
where the chain declares it:
```ts
flag.string({ minLength: 3 }).default('ab');
// Default value for a string flag is invalid: must be at least 3 characters
flag.array(flag.number({ min: 0 })).default([-1]);
// Default value for an array flag at 0 is invalid: must be >= 0
flag.count().default(-1);
// Default value for a count flag is invalid: expected a non-negative integer
```
Chain order does not change the verdict. A constraint or validator added after
the default is checked against it too:
```ts
flag.string().default('ab').minLength(3); // same error as the first line above
```
A collection default takes the shape the flag resolves to: an array for
`flag.array()`, a record for `flag.keyValue()`, a non-negative integer for
`flag.count()`.
Three checks stay deferred until resolution: a validator that returns a promise,
the `flag.path()` filesystem checks, and a validator on `flag.count()`, whose
default of `0` the factory declares rather than the caller.
`flag.path({ mustExist: true }).default('/nope')` therefore builds and fails when
the path is probed; `flag.count().standard(atLeastOne)` builds and fails on an
invocation that leaves the count at `0`.
Fix the default, or widen the declaration where the value was intended:
`flag.number({ finite: false }).default(Number.POSITIVE_INFINITY)` holds.
## Diagnostics
By default, a failing value is quoted and stored in `details.value` regardless
of whether it came from argv, stdin, env, config, a prompt, or a default. Mark an
input with `.sensitive()` when framework diagnostics must not retain or display
its values:
```ts
flag.string().pattern(/^ghp_/).env('API_TOKEN').sensitive();
```
Sensitive diagnostics use `` and omit raw-derived fields such as
values, collection keys, validator path segments, filesystem paths, and adapter
causes. The input name, source, expected type, constraint, and allowed enum
values remain available:
```bash
$ API_TOKEN=sk-live-9f2 mycli deploy
Invalid value '' from env API_TOKEN for flag --token: must match /^ghp_/
```
Help also omits the automatic `(default: value)` annotation for a sensitive
input. A safe explicit description still renders:
```ts
flag.string().default('stored-token', { description: 'from keychain' }).sensitive();
```
[Diagnostics and redaction](/guide/semantics#diagnostics-and-redaction) is the
canonical contract, including the developer-authored parser and validator text
that remains verbatim.
## What's Next?
- [Arguments](/guide/arguments), the same six axes on the positional surface
- [What the arg factory does not have](/guide/arguments#flag-only-surface), the
flag members bound to flag syntax
- [Config Files](/guide/config), config file resolution
- [Interactive Prompts](/guide/prompts), prompt integration
- [CLI Semantics](/guide/semantics), exact parser and precedence rules
---
# Getting Started
Source: https://dreamcli.kjanat.dev/raw/guide/getting-started
# Getting Started
## Installation
::: code-group
```bash [npm]
npm install @kjanat/dreamcli
```
```bash [bun]
bun add @kjanat/dreamcli
```
```bash [deno]
deno add jsr:@kjanat/dreamcli
```
:::
Supported minimum runtimes:
Node.js `>= 22.22.2`,
Bun `>= 1.3`,
Deno `>= 2.6.0`.
## Your First Command
```ts twoslash
import { cli, command, flag, arg } from '@kjanat/dreamcli';
const greet = command('greet')
.description('Greet someone')
.arg('name', arg.string().describe('Who to greet'))
.flag(
'loud',
flag
.boolean()
.alias('l')
.describe('Shout the greeting'),
)
.flag(
'times',
flag.number().default(1).describe('Repeat count'),
)
.action(({ args, flags, out }) => {
const repeatCount = Number.isFinite(flags.times)
? Math.max(0, Math.min(100, Math.floor(flags.times)))
: 1;
for (let i = 0; i < repeatCount; i++) {
const msg = `Hello, ${args.name}!`;
out.log(flags.loud ? msg.toUpperCase() : msg);
}
});
cli('greet').default(greet).run();
```
```bash
$ npx tsx greet.ts Alice --loud --times 3
HELLO, ALICE!
HELLO, ALICE!
HELLO, ALICE!
```
By the time `action` runs, `args.name` is `string` and `flags.times` is `number` — fully resolved,
no `undefined` to check.
## Multi-Command CLI
```ts twoslash
import {
cli,
command,
group,
flag,
arg,
} from '@kjanat/dreamcli';
const deploy = command('deploy')
.description('Deploy to an environment')
.arg('target', arg.string())
.flag('force', flag.boolean().alias('f'))
.flag(
'region',
flag.enum(['us', 'eu', 'ap']).env('DEPLOY_REGION'),
)
.action(({ args, flags, out }) => {
out.log(
`Deploying ${args.target} to ${flags.region ?? 'default'}`,
);
});
const login = command('login')
.description('Authenticate with the service')
.flag('token', flag.string())
.action(({ flags, out }) => {
out.log(
flags.token
? 'Authenticated via token'
: 'Authenticated interactively',
);
});
cli('mycli')
.version('1.0.0')
.description('My awesome tool')
.command(deploy)
.command(login)
.run();
```
```bash
$ mycli deploy production --force
Deploying production to default
$ mycli login --token abc123
Authenticated via token
```
## What's Next?
- [Why dreamcli?](/guide/why) — how it compares to existing frameworks
- [Commands](/guide/commands) — command builders, groups, nesting
- [Flags](/guide/flags) — all flag types, modifiers, and resolution
- [Testing](/guide/testing) — in-process test harness
- [CLI Fundamentals](/concepts/anatomy) — new to CLIs? Start with the concepts
---
# Help
Source: https://dreamcli.kjanat.dev/raw/guide/help
# Help
Help text is generated from your schemas — usage line, arguments, flags,
subcommands, and examples all render automatically for `--help`, `-h`,
`help `, and root help.
Those three tokens belong to the root, so a command cannot declare a `help`
flag or an `h` alias. A CLI that needs the token for itself releases it with
`.builtins({ help: 'off' })`. See
[Taking a built-in over](/guide/output#taking-a-built-in-over).
## Source Annotations
Every source an input declares is named beside its description, on the flag table
and the argument table alike:
| Annotation | Declared by |
| ----------------------- | --------------------------------- |
| `[stdin]` | `.stdin()` |
| `[stdin: '-']` | `.stdin({ when: 'dash' })` |
| `[stdin: when omitted]` | `.stdin({ when: 'missing' })` |
| `[env: NAME]` | `.env('NAME')` |
| `[config: a.b]` | `.config('a.b')` |
| `[prompt]` | `.prompt(...)` |
They render in resolution order, after the description and any `[deprecated]`
marker, and before `[required]` or `(default: …)`:
```
Flags:
--body Message body [stdin] [env: BODY] [required]
```
Shell completions carry the plain description, never these annotations.
Sensitive inputs never render an automatic `(default: value)` annotation. Give
the default a safe description when help should name its source without showing
the value:
```ts
flag.string().default('stored-token', { description: 'from keychain' }).sensitive();
```
## Configuring Help
`.help()` sets builder-level defaults; the same fields can be overridden per
call via `execute(argv, { help })`:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
cli('mycli')
.command(command('deploy').action(() => {}))
.help({
// Render the default command's args/flags inline at the root
inlineDefault: true,
// Also list the default command in the Commands: table
showDefaultInCommands: false,
// Force the "Run ' --help'" footer on or off
footer: true,
// Pin the line width (defaults to the terminal width, falling back to 80)
width: 100,
// OSC 8 hyperlinks in the header. Defaults to the channel's resolved
// support: explicit --no-hyperlinks/--hyperlinks flags, then
// NO_HYPERLINKS, FORCE_HYPERLINKS, and finally TTY detection.
hyperlinks: true,
// Order of the Flags: table — 'alphabetical' (default) or 'declaration'
flagOrder: 'declaration',
});
```
### Flag order
By default the `Flags:` table lists short-aliased flags first, then
alphabetically by name. `flagOrder: 'declaration'` instead preserves the order
you called `.flag()`, so you control the layout by ordering your builder calls:
```ts twoslash
import { cli, command, flag } from '@kjanat/dreamcli';
cli('mycli')
.command(
command('serve')
.flag('port', flag.number())
.flag('host', flag.string())
.action(() => {}),
)
// 'port' then 'host', as declared — not alphabetized to 'host', 'port'
.help({ flagOrder: 'declaration' });
```
For full control, `sortFlags` supplies a comparator over flag long names and
wins over `flagOrder`:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
cli('mycli')
.command(command('serve').action(() => {}))
// Reverse-alphabetical
.help({ sortFlags: (a, b) => b.localeCompare(a) });
```
Both are also accepted per call via `execute(argv, { help })` / `run({ help })`.
### Header hyperlinks
The root-help header can wrap the program name and version in OSC 8 terminal
hyperlinks (pointing at the repository and release tag). Explicit
`--no-hyperlinks` / `--hyperlinks` flags take precedence, followed by
`NO_HYPERLINKS`, then `FORCE_HYPERLINKS`, and finally TTY detection. When both
environment variables are set, `NO_HYPERLINKS` wins. The same resolved decision is exposed to handlers as
`out.isHyperlinkSupported`, so code rendering its own `out.color.link(...)`
output can gate on it and keep escapes out of piped or opted-out contexts.
## Theming
Help output is colored by default whenever color is enabled — the same gate as
handler output (`out.color`): stdout is a TTY, `--json` is off, and the
environment allows color (`NO_COLOR` / `FORCE_COLOR` are honored). Piped and
JSON output stays byte-clean, always.
The built-in theme follows clap/cargo conventions:
| Role | Applies to | Default style |
| --------------- | ------------------------------------------------------ | ---------------- |
| `sectionTitle` | `Usage:`, `Flags:`, `Commands:`, … | bold + underline |
| `usageBin` | binary/command path in the usage line | bold |
| `flag` | flag forms (`-f, --force`) | cyan |
| `command` | command names in `Commands:` tables | cyan |
| `arg` | positional tokens (``) | cyan |
| `placeholder` | grammar tokens (``, `[flags]`) | dim |
| `defaultValue` | `(default: …)` | dim |
| `annotation` | `[stdin]`, `[env: X]`, `[required]`, `(default)` | dim |
| `deprecated` | `[deprecated…]` | yellow |
| `headerName` | program name in the root header | bold |
| `headerVersion` | `vX.Y.Z` in the root header | dim |
| `examplePrompt` | the `$` marker in `Examples:` | dim |
Literal descriptions stay unstyled for readability. Function-form flag and
argument descriptions receive the resolved theme at render time, so references
inside prose use the same semantic roles and the same color gate as the table:
```ts twoslash
import { arg, command, flag } from '@kjanat/dreamcli';
command('deploy')
.arg('target', arg.string().describe((t) => `Deploy ${t.arg('')}`))
.flag(
'output',
flag.string().describe(
(t) => `Filename relative to ${t.flag('--out-dir')}`,
),
);
```
Example commands are highlighted per token with the existing roles — the
leading binary via `usageBin` (bold)
and flag tokens (`-x`, `--long`) via `flag` (cyan), values plain. Tokenizing is
quote-aware, so `--scope './a b'` stays one token, and the highlighting respects
the color gate: with color off the command is byte-identical to its plain form.
### Custom Themes
Pass a theme factory to `.help()`. It receives the **gated palette** (the same
`Colors` instance as `out.color`) and returns role overrides merged over the
built-in theme:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
cli('mycli')
.command(command('deploy').action(() => {}))
.help({
theme: (c) => ({
sectionTitle: c.magenta,
flag: c.hex('#ff8800'),
command: (input) => c.bold(c.green(input)),
}),
});
```
Use `descriptionTheme` for a role override that applies only inside
function-form descriptions. It merges over the global theme role by role, so
unspecified roles still inherit the global choice:
```ts twoslash
import { cli, command, flag } from '@kjanat/dreamcli';
cli('mycli')
.command(
command('deploy').flag(
'output',
flag.string().describe((t) => `Relative to ${t.flag('--out-dir')}`),
),
)
.help({
theme: (c) => ({ flag: c.cyan }),
descriptionTheme: (c) => ({ flag: c.blue }),
});
```
Two guarantees make themes safe to write:
- The palette's formatters are identity functions when color is off, so you can
style unconditionally.
- The factory itself is **never invoked** when color is off — even a formatter
that emits raw escape codes cannot leak them into piped output.
One constraint: roles used inside wrap-eligible description text (including
`flag`, `arg`, `placeholder`, `defaultValue`, `annotation`, and `deprecated`)
should stick to foreground colors
and `dim`. A styled span can cross a soft-wrap boundary — colors carry
invisibly across the continuation indent, but `underline`, `inverse`, and
background styles would visibly paint it.
## JSON Help
With `--json`, help emits the CLI's **definition document** instead of text —
the same machine-readable shape produced by `generateSchema()`, so
`mycli --help --json | jq '.commands[].name'` just works:
```sh
$ mycli --help --json # root: { $schema, name, version, defaultCommand?, commands }
$ mycli deploy --help --json # command: { name, description, flags, args, commands }
```
The root document includes the default command's *full* definition under
`defaultCommand` (flags and args included), and carries a `$schema` pointer to
the definition meta-schema for validation and editor tooling. Nothing is
written to stderr — the JSON is the help.
## Programmatic Rendering
`formatHelp()` renders a command's help as a plain string — useful for custom
UIs, docs generation, or tests. Pass `colors` to render a themed variant:
```ts twoslash
import { command, formatHelp } from '@kjanat/dreamcli';
const deploy = command('deploy').description('Deploy the app').action(() => {});
const plain = formatHelp(deploy.schema, { binName: 'mycli' });
```
See also [Output](/guide/output) for how `out.color` gating works at runtime.
---
# Limitations And Workarounds
Source: https://dreamcli.kjanat.dev/raw/guide/limitations
# Limitations And Workarounds
This page consolidates the most important current edges in DreamCLI's shipped surface.
Use it with the [Support Matrix](/reference/support-matrix): the matrix says what is supported, deferred, or experimental; this page explains where the supported surface still has important constraints.
## Non-Interactive Prompting Is Intentionally Off
Automatic prompts run only when a prompter is available and `stdinIsTTY` is `true`.
Pipes, CI, and redirected stdin do not trigger interactive fallbacks.
Why:
- non-interactive execution should stay deterministic;
- a pipeline should not hang because a prompt suddenly appeared.
Workarounds:
- provide the value through CLI flags, env vars, config, stdin-backed inputs, or defaults;
- in tests, inject prompt answers explicitly through the testkit instead of relying on TTY behavior.
References: [Interactive Prompts](/guide/prompts), [CLI Semantics](/guide/semantics)
## Built-In Config Loading Is JSON-Only
DreamCLI ships JSON config discovery by default.
YAML, TOML, and other formats require explicit parser wiring through the config-format extension surface.
Why:
- the project keeps a single runtime dependency, `ansispeck`, in core;
- built-in multi-format parsing would expand maintenance and dependency surface quickly.
Workarounds:
- stay on JSON for the boring path;
- add your own config loader when a different format is required.
References: [Config Files](/guide/config), [Support Matrix](/reference/support-matrix)
## Runtime Support Is Real, But Platform Coverage Is Narrower Than The Claim Surface
Node, Bun, and Deno adapters are shipped.
That does not mean every OS, shell, and CI permutation is exhaustively covered today.
Why:
- runtime support is part of the product;
- broad cross-platform coverage takes longer to verify honestly than adapter support takes to implement.
Workarounds:
- treat the documented minimum runtimes as the supported baseline;
- verify your exact platform/shell combination before relying on it as production truth;
- watch `harden-ci-coverage` for broader CI proof.
References: [Runtime Support](/guide/runtime), [Support Matrix](/reference/support-matrix)
## Generated Hover Is Intentionally Narrow
Hover is only shipped on generated example source blocks.
Guides, shell transcripts, and the wider reference surface stay plain markdown.
Why:
- examples are the highest-value place for code inspection;
- site-wide interactive rendering would add complexity faster than it would add clarity.
Workarounds:
- use generated example pages when you want hoverable code;
- use symbol pages and guide links when you want stable reference reading instead of inline inspection.
References: [Examples](/examples/), [Example Hover](/reference/example-hover-prototype)
## Negated Booleans Are Opt-In Per Flag
DreamCLI accepts boolean flags such as `--verbose`, `--verbose=true`, and `--verbose=false`.
`.negatable()` synthesizes the `--no-verbose` form for a flag that asks for it.
No boolean gets a `--no-` spelling automatically; see [Negatable Booleans](/guide/flags#negatable-booleans).
Why:
- implicit alternate spellings on every boolean make parsing and docs less explicit;
- the library prefers schema-declared behavior over convention-heavy magic.
Workarounds:
- add `.negatable()` to each boolean that wants the pair;
- pass `.negatable({ alias: 'plain' })` when the negated spelling should be something other than `--no-`.
References: [CLI Semantics](/guide/semantics), [Flags](/guide/flags)
## "Exactly One Of" Is Not Built In
DreamCLI has no declarative mutual-exclusion modifier.
There is no `.exclusive()`, no `.conflicts()`, and no required-one-of group — flags are resolved independently of one another.
Why:
- the schema models each flag as a standalone, independently resolved value;
- cross-flag rules are open-ended (exactly-one, at-most-one, all-or-none, implies), and a single declarative knob would cover only a slice of them.
Workaround — enforce the rule in `.derive()`, which runs after resolution and before the action, and throw a `CLIError`.
Count the flags the user actually supplied by reading `sources`, the provenance record beside `flags` and `args`.
`wasExplicit()` is `true` for every stage except `default`, so a flag that carries a `.default()` is still counted correctly:
```ts twoslash
import { CLIError, command, flag, wasExplicit } from '@kjanat/dreamcli';
command('lsp-server')
.flag('stdio', flag.boolean())
.flag('node-ipc', flag.boolean())
.flag('socket', flag.number().default(6009))
.derive(({ sources }) => {
const count =
Number(wasExplicit(sources.flags.stdio)) +
Number(wasExplicit(sources.flags['node-ipc'])) +
Number(wasExplicit(sources.flags.socket));
if (count === 0) {
throw new CLIError('No transport selected.', {
code: 'NO_TRANSPORT',
suggest:
'Pass exactly one of --stdio, --node-ipc, or --socket ',
});
}
if (count > 1) {
throw new CLIError(
'Choose exactly one transport flag.',
{ code: 'TOO_MANY_TRANSPORTS' },
);
}
return {};
})
.action(({ out }) => out.log('starting'));
```
The thrown `CLIError` renders a friendly message on stderr and exits with code `1`.
For a complete, typed version that hands the chosen transport to the action, see the `transport-launcher` example.
Reading the resolved values instead (`flags.socket !== undefined`) works only while every flag in the rule has no declared default.
A defaulted flag always has a value, so the rule stops detecting the conflict, and dropping `.default()` to restore it removes `defaultValue` from the definition document and the `(default: …)` suffix from help.
`sources` is what separates the two questions.
References: [Flags](/guide/flags), [Errors](/guide/errors), [Which source won](/guide/semantics#which-source-won)
## The Framework Is Overkill For Tiny One-Off Scripts
dreamcli is optimized for typed multi-source resolution, middleware, structured output, completions, and in-process testing.
That is a lot of machinery if you only want a few flags and one direct action.
Why:
- the product is designed around coherence across the full command lifecycle, not minimum abstraction count.
Workarounds:
- use dreamcli when you expect the CLI to grow or when typed multi-source behavior matters;
- call `readFlags()` when a script wants typed flags and nothing else: no command, no handler, no output channel;
- use a thinner parser when you only need a tiny wrapper script.
References: [Why dreamcli](/guide/why), [Architecture Rationale](/guide/rationale), [Standalone Flag Evaluation](/guide/read-flags)
## Adoption Guidance Lives Elsewhere On Purpose
This page is about current constraints, not the whole adoption story.
Use these companion pages when you need the rest of that picture:
- [Migration And Adoption](/guide/migration) for when to switch and how to phase the move;
- [Troubleshooting](/guide/troubleshooting) for likely real failure modes and diagnosis steps.
## Related Pages
- [Architecture Rationale](/guide/rationale)
- [Migration And Adoption](/guide/migration)
- [Troubleshooting](/guide/troubleshooting)
- [Support Matrix](/reference/support-matrix)
- [CLI Semantics](/guide/semantics)
- [Runtime Support](/guide/runtime)
- [Testing Commands](/guide/testing)
---
# Middleware
Source: https://dreamcli.kjanat.dev/raw/guide/middleware
# Middleware
Middleware wraps downstream execution and can add typed context to the command handler chain.
Use `derive()` when you need typed resolved flags or args before the action. Use middleware when
you need `next()` around the rest of the pipeline.
## Defining Middleware
```ts twoslash
import { middleware } from '@kjanat/dreamcli';
const timing = middleware<{ startTime: number }>(
async ({ next }) => {
const startTime = Date.now();
await next({ startTime });
},
);
const trace = middleware<{ traceId: string }>(
async ({ next }) =>
next({ traceId: crypto.randomUUID() }),
);
```
The generic parameter declares the context shape this middleware provides.
The `next()` call passes context downstream and continues the chain.
## Stacking Middleware
```ts twoslash
import { command, middleware } from '@kjanat/dreamcli';
const timing = middleware<{ startTime: number }>(
async ({ next }) => next({ startTime: Date.now() }),
);
const trace = middleware<{ traceId: string }>(
async ({ next }) =>
next({ traceId: crypto.randomUUID() }),
);
command('deploy')
.middleware(timing)
// ^^^^^^
.middleware(trace)
// ^^^^^
.action(({ ctx }) => {
const { traceId, startTime } = ctx;
console.log(
`[${traceId}] deployed in ${Date.now() - startTime}ms`,
// ^?
// ^?
);
});
```
Context types intersect: `{ startTime: number } & { traceId: string }`.
Each middleware only needs to know about its own context shape.
## Middleware Parameters
The middleware handler receives:
```ts twoslash
import { middleware } from '@kjanat/dreamcli';
middleware(
async ({ flags, args, ctx, out, meta, next }) => {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// flags — resolved flag values (type-erased)
// args — resolved argument values (type-erased)
// ctx — accumulated middleware context (type-erased)
// out — output channel
// meta — CLI metadata { name, bin, version, command }
// next — continue chain, passing context
return next({});
},
);
```
If you need typed command-scoped access to resolved inputs, prefer `command(...).derive(...)`.
## Error Handling
Middleware can catch and transform errors:
```ts twoslash
import { CLIError, middleware } from '@kjanat/dreamcli';
const errorBoundary = middleware(async ({ next, out }) => {
try {
return await next({});
} catch (err) {
if (err instanceof CLIError) {
out.error(err.message);
}
throw err;
}
});
```
## What's Next?
- Related example: [Middleware patterns](/examples/middleware)
- [Config Files](/guide/config) — configuration file discovery
- [Testing](/guide/testing) — testing middleware behavior
---
# Migration And Adoption
Source: https://dreamcli.kjanat.dev/raw/guide/migration
# Migration And Adoption
This page is for serious evaluators and adopters deciding whether to move an existing CLI onto
dreamcli. Already on dreamcli 2.x? See [Upgrading From 2.x To 3.0](/guide/upgrading-v3) instead, then
[Upgrading From 3.x To 4.0](/guide/upgrading-v4). Already on 3.x? Go straight to the
[4.0 guide](/guide/upgrading-v4).
Use it after [Getting Started](/guide/getting-started) and [Why dreamcli](/guide/why): those pages
explain the shape of the framework; this page explains when switching is worth it and how to do it
without rewriting everything at once.
## Who Should Migrate
dreamcli is a strong fit when your CLI needs more than argv parsing.
Good signals:
- you want typed flags, args, and handler context without manual plumbing;
- values already come from multiple sources like env, config, prompts, or stdin;
- your CLI has several commands and shared behavior is starting to sprawl;
- you want in-process tests instead of subprocess-heavy harnesses;
- you care about generated help, completions, and structured output staying aligned with the same schema.
It is a weaker fit when you only need a tiny one-off parser script.
For that trade-off in product terms, also read [Limitations And Workarounds](/guide/limitations).
## Mental Model Shift
The main migration shift is this:
- parser-first tools usually parse argv, then the app wires env, config, prompts, output, and tests around that result;
- dreamcli expects the command schema to declare those concerns up front.
That means migration usually involves moving logic:
- out of ad hoc option-merging helpers;
- out of command handlers;
- into the schema chain for flags, args, prompts, config, middleware, and output choices.
The benefit is that the same declaration now drives parsing, resolution, help, completions, tests,
and docs-facing truth.
## Migration Path
### 1. Start With One Real Command
Do not rewrite the entire CLI first.
Pick one command with real flags and one or two value sources.
Keep the first migration boring:
- model the command with `command()`;
- move existing options into `flag.*()` and `arg.*()` definitions;
- keep the action body close to the old behavior until tests stay green.
### 2. Move Source Merging Into The Schema
If your current CLI has logic like this in handlers or helpers:
- argv overrides env;
- env overrides config;
- missing values fall back to prompts or defaults.
Move that into flag and arg declarations directly.
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag
.enum(['us', 'eu', 'ap'])
.env('DEPLOY_REGION')
.config('deploy.region')
.prompt({ kind: 'select', message: 'Which region?' })
.default('us');
```
This is usually the highest-value migration step because it removes duplicated precedence logic from
the application layer.
### 3. Replace Glue Tests With `runCommand()`
Once one migrated command exists, move its tests to the in-process harness.
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
import { runCommand } from '@kjanat/dreamcli/testkit';
const deploy = command('deploy')
.flag(
'region',
flag
.enum(['us', 'eu', 'ap'])
.env('DEPLOY_REGION')
.config('deploy.region'),
)
.action(() => {});
const result = await runCommand(deploy, [], {
env: { DEPLOY_REGION: 'eu' },
config: { deploy: { region: 'us' } },
});
```
This is where dreamcli often pays off fastest for maintainers:
- no subprocess boot cost;
- no `process.argv` mutation;
- no separate test-only env/config plumbing.
### 4. Add Middleware And Output Only Where They Help
Do not front-load advanced features.
Adopt them when the current command shape actually benefits:
- middleware for shared typed setup, auth, or derived context;
- `out` helpers when you want consistent text/JSON/table activity behavior;
- completions when the command tree is stable enough to support shell install docs.
### 5. Expand Command By Command
After one migrated command is stable:
- migrate adjacent commands that share the same context or value sources;
- extract shared middleware only after duplication is real;
- leave low-value legacy commands on the old path until there is a reason to move them.
## Tool-Shape Mapping
This is the rough adoption mapping from common CLI styles to dreamcli concepts.
| Existing shape | Typical current home | DreamCLI target surface |
| --------------------------------- | -------------------------------- | ------------------------------------------ |
| option parser declarations | parser setup | `command()`, `flag.*()`, `arg.*()` |
| env fallback helpers | handler or config utility | `.env()` on flags or args |
| config lookup glue | app bootstrap or command helper | `cli().config()` plus `.config()` on flags |
| prompt fallback logic | handler branch or prompt wrapper | `.prompt()` or `.interactive()` |
| command-level shared setup | copied pre-handler logic | middleware / derive |
| JSON mode and table formatting | custom output utility | `out.json()`, `out.table()`, output policy |
| subprocess command tests | test runner shell wrapper | `runCommand()` |
| shell completion hand-maintenance | shell scripts or plugin glue | built-in completions generation |
## Adoption Checklist
Use this checklist for a real migration.
1. Identify one command with meaningful flags and source precedence.
2. Move value-source rules into schema declarations.
3. Recreate its current behavior under `runCommand()` tests.
4. Confirm help text and examples still explain the command clearly.
5. Add completions or structured output only after the command surface settles.
6. Migrate shared setup into middleware only after at least two commands need it.
## When Not To Switch Yet
Delay migration if any of these are true:
- your CLI is intentionally tiny and unlikely to grow;
- your team only wants argv parsing and nothing else;
- you depend on broad legacy runtime coverage below DreamCLI's documented floors;
- your current CLI behavior is still unstable enough that formalizing it in schema would slow you down.
In those cases, a thinner parser may be the more honest choice for now.
## Related Pages
- [Why dreamcli](/guide/why)
- [Architecture Rationale](/guide/rationale)
- [Limitations And Workarounds](/guide/limitations)
- [Troubleshooting](/guide/troubleshooting)
- [Testing Commands](/guide/testing)
- [Support Matrix](/reference/support-matrix)
---
# Output
Source: https://dreamcli.kjanat.dev/raw/guide/output
# Output
Handlers receive `out` instead of `console`.
The output channel adapts to context automatically.
## Basic Output
```ts twoslash
import { command } from '@kjanat/dreamcli';
command('deploy').action(({ out }) => {
out.log('Informational message');
out.warn('Warning message');
out.error('Error message');
});
```
## JSON Output
```ts twoslash
import { createOutput } from '@kjanat/dreamcli';
const out = createOutput();
out.json({ status: 'ok', count: 42 });
```
When the CLI is invoked with `--json`, structured payloads stay on stdout while
plain text (`log`, `info`, `warn`, `error`) routes to stderr.
`--json` also takes an explicit value like any boolean flag: `--json=true`,
`--json=1`, `--json=false`, and `--json=0`. The last occurrence wins, so a
wrapper script can append `--json=false` to turn a default off, and an invalid
value fails with exit code 2.
## Exit Codes Without Error Output
Use `out.setExitCode(code)` when a command should emit normal output but still
return a non-zero status to scripts.
```ts twoslash
import { command } from '@kjanat/dreamcli';
command('status').action(({ out }) => {
out.json({ status: 'degraded' });
out.setExitCode(7);
});
```
`setExitCode()` does not print anything and does not stop execution. Later calls
win, and thrown `CLIError`s still use their own exit codes and error rendering.
## Status Lines and Quiet Mode
`out.status()` prints success and progress notes like `Wrote ` to
**stderr**, so stdout stays clean for piping, and it is suppressed under quiet
verbosity:
```ts twoslash
import { command } from '@kjanat/dreamcli';
command('gen').action(({ out }) => {
out.log('dist/app.js'); // the result, on stdout (stderr in --json)
out.status('Wrote dist/app.js'); // the note, on stderr, silenced by -q
});
```
Every CLI accepts a global `--quiet`/`-q` flag that sets quiet verbosity,
suppressing `info`, `status`, spinners, and progress bars while `log`, `warn`,
and `error` still emit.
Like `--json`, it is a root-level flag: it is stripped before dispatch (so
command schemas never see it) and only counts before the `--` separator; a
literal `-q` after `--` reaches the command as a positional.
The long spelling takes an explicit value the same way: `--quiet=true`,
`--quiet=1`, `--quiet=false`, and `--quiet=0`, with the last occurrence winning
and an invalid value failing with exit code 2. The short `-q` is presence-only,
as short boolean flags are everywhere else.
`--help` and `--version` render ahead of that error, so a mistyped value never
hides the text that documents the flag.
The root owns those tokens: it strips `--json` and `--quiet`/`-q` from argv,
intercepts `--version`/`-V` once the CLI declares a version, and renders help
for `--help`/`-h` before a command's flags are parsed. A command flag spelled
the same way could never be set, so declaring one throws `RESERVED_FLAG`.
Naming a flag `quiet`, `json`, or `help`, aliasing one to `q` or `h`, or giving
one a negated spelling like `.negatable({ alias: 'quiet' })` is rejected by
`.command()`, `.default()`, and `createCLISchema()`, and `version`/`V` join
that set once a version is configured. Because a version can arrive after the
commands are registered, `.version()` and `.manifest(data)` re-check every
registered command too. A version that `.manifest()` reads off the filesystem
lands later still, so `.run()` runs the guard once discovery supplies it and
fails startup with the same error and the same exit code, routed the way every
other startup failure is: message and suggestion on stderr, or the serialized
error on stdout under `--json`. Rename the flag, use `out.status()` for status
output that root `--quiet` suppresses, or release the built-in with
`.builtins()`.
| Method | Stream | Suppressed by quiet |
| -------- | --------------------------- | ------------------- |
| `log` | stdout (stderr in `--json`) | no |
| `info` | stdout (stderr in `--json`) | yes |
| `status` | stderr | yes |
| `warn` | stderr | no |
| `error` | stderr | no |
### Taking a built-in over
Some CLIs need the token for themselves. `--json` names the document the
command operates on, `-q` means something domain-specific, `--help` is a
routable topic browser. `.builtins()` releases a built-in to the commands.
```ts twoslash
import { cli, command, flag } from '@kjanat/dreamcli';
const validate = command('validate')
.flag('json', flag.string().describe('Document to validate'))
.action(({ flags, out }) => {
out.log(`Validating ${flags.json}`);
});
cli('schematool')
.builtins({ json: 'off' })
.command(validate);
```
```sh
$ schematool validate --json schema.yaml
Validating schema.yaml
$ schematool validate --json=schema.yaml
Validating schema.yaml
```
Every built-in starts `'on'`. Setting one to `'off'` releases every spelling it
answers to. The root stops reading and stripping the token, `Global options:`
stops listing it, the `RESERVED_FLAG` guard stops reserving it, and the
command's own flag parses and reaches the handler. `quiet: 'off'` releases both
`--quiet` and `-q`. `help: 'off'` releases `--help` and `-h` at the root and at
the command level, along with the bare `help` token, the `--help` footer hint,
the `Run ' --help' for available commands` suggestion on dispatch errors,
and the synthetic `--help` in generated completion scripts. A bare invocation
is unaffected, so `schematool` with no arguments still renders root help when
the CLI has no default command.
Call `.builtins()` before registering the commands that declare a released
flag. `.command()` checks against the state it has, so the reverse order throws
`RESERVED_FLAG`:
```
Command 'validate' defines a '--json' flag, which is reserved by the root
'--json' flag. The root strips that token before dispatch, so the command can
never receive it
```
Its suggestion names both remedies, `Rename the flag, or release the built-in
with .builtins({ json: 'off' }) before registering the command`. The `quiet`
variant adds `use out.status() for output that root --quiet suppresses`.
Repeated calls merge, the last mode per built-in winning, and taking a built-in
back with `'on'` re-checks every registered command.
`version` and `completions` have no entry. `.version()` and `.completions()`
are opt-in, so a CLI declines those two by not calling them.
Only argv-driven activation is disabled. `RunOptions.jsonMode` and
`RunOptions.verbosity` keep working, so `.execute(argv, { jsonMode: true })`
still renders JSON with `json: 'off'`. In tests, pass the same setting to
`runCommand()` so the harness mirrors the CLI the command is registered on:
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
const validate = command('validate')
.flag('json', flag.string())
.action(({ flags, out }) => {
out.log(`Validating ${flags.json}`);
});
// ---cut---
import { runCommand } from '@kjanat/dreamcli/testkit';
const result = await runCommand(validate, ['--json', 'schema.yaml'], {
builtins: { json: 'off' },
});
```
## Tables
```ts twoslash
import { createOutput } from '@kjanat/dreamcli';
type Row = { name: string; status: string; uptime: number };
const rows = [
{ name: 'web-1', status: 'running', uptime: 72 },
{ name: 'worker-1', status: 'degraded', uptime: 18 },
];
const out = createOutput();
out.table(rows, [
{ key: 'name', header: 'Name' },
{ key: 'status', header: 'Status' },
{ key: 'uptime', header: 'Uptime (h)' },
]);
```
::: tip Use a `type` alias (not `interface`) for table rows.
TypeScript's structural typing requires `Record` compatibility.
:::
## Spinners
```ts twoslash
import { createOutput } from '@kjanat/dreamcli';
const deploy = async () => {};
const out = createOutput();
const spinner = out.spinner('Deploying...');
await deploy();
spinner.succeed('Done');
```
Spinners auto-disable when stdout is not a TTY (CI, piped output).
In quiet and `--json` modes, spinners are suppressed entirely.
## Progress Bars
```ts twoslash
import { createOutput } from '@kjanat/dreamcli';
const tick = async () => {};
const out = createOutput();
const progress = out.progress({
label: 'Uploading',
total: 100,
});
for (let i = 0; i <= 100; i++) {
progress.update(i);
await tick();
}
progress.done('Upload complete');
```
## Output Modes
The output channel automatically adjusts behavior:
| Context | Behavior |
| -------- | ---------------------------------------------------- |
| TTY | Pretty formatting, spinners animate, colors |
| Piped | Minimal stable output, spinners suppressed |
| `--quiet` | `info()`, `status()`, and rendered spinner/progress activity suppressed; `log()` remains enabled |
| `--json` | Structured JSON to stdout, everything else to stderr |
One code path, correct output everywhere.
## Pre-run Render Context
Inside a handler, `out` answers every rendering question: `out.jsonMode`,
`out.verbosity`, `out.isTTY`, `out.color`, `out.isHyperlinkSupported`. Content built **before**
`run()` — a banner, hand-rendered help — has no `out`, and re-deriving the
answers from raw argv goes wrong (`argv.includes('--json')` misreads a
post-`--` literal like `mycli -- --json`).
`resolveRenderContext` runs the same composition `run()` feeds into the output
channel, so pre-run styling matches what will actually render:
```ts twoslash
import { resolveRenderContext } from '@kjanat/dreamcli';
const ctx = resolveRenderContext(process.argv.slice(2), {
isTTY: process.stdout.isTTY === true,
env: process.env,
});
// Identity formatters when color is off — style unconditionally
const banner = ctx.color.bold('mycli');
// `ctx.jsonMode` used pre-separator-aware --json detection
if (!ctx.jsonMode && ctx.verbosity !== 'quiet') console.error(banner);
```
The returned `color` is the same gated palette the channel will expose as
`out.color` (`ctx.color.isColorSupported` is the boolean form), and
`ctx.isHyperlinkSupported` uses the same precedence as the help header: explicit
`--no-hyperlinks` / `--hyperlinks` flags, then `NO_HYPERLINKS`, then
`FORCE_HYPERLINKS`, and finally TTY detection.
For just the `--`-aware flag reads, the primitives are also exported:
`includesBeforeSeparator(argv, '--json')` and its strip counterpart
`stripBeforeSeparator`.
## What's Next?
- Related examples: [JSON mode](/examples/json-mode), [Spinner and progress](/examples/spinner-progress)
- [Errors](/guide/errors) — structured error handling
- [Testing](/guide/testing) — capturing output in tests
---
# Interactive Prompts
Source: https://dreamcli.kjanat.dev/raw/guide/prompts
# Interactive Prompts
dreamcli integrates interactive prompts into the flag resolution chain.
Prompts activate only when needed and only in interactive contexts.
## Per-Flag Prompts
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag.string().prompt({ kind: 'input', message: 'Name?' });
flag
.boolean()
.prompt({ kind: 'confirm', message: 'Sure?' });
flag
.enum(['a', 'b'])
.prompt({ kind: 'select', message: 'Pick one' });
flag.array(flag.string()).prompt({
kind: 'multiselect',
message: 'Pick many',
choices: [{ value: 'a' }, { value: 'b' }],
});
```
Prompts fire only if the flag wasn't resolved by CLI argv, env var, or config. Defaults apply only
after prompts are skipped or unanswered.
For the exact non-interactive rules, cancellation fallthrough, and full
precedence chain, see [CLI Semantics](/guide/semantics).
## Prompt Defaults
`confirm` and `input` prompts accept a `default` that is used when the user
submits an empty line (presses Enter):
```ts twoslash
import { flag } from '@kjanat/dreamcli';
// Enter → false; hint renders as `(y/N)` for a safe destructive default.
flag.boolean().prompt({
kind: 'confirm',
message: 'Delete everything?',
default: false,
});
// Enter → 'Anonymous'; hint renders as `Name (default: Anonymous):`.
flag.string().default('Anonymous').prompt({
kind: 'input',
message: 'Name',
default: 'Anonymous',
});
```
A `confirm` default of `true` (or unset) shows `(Y/n)`; `false` shows `(y/N)`.
For `input`, an empty submission resolves to the prompt-level `default` when set
(skipping `validate`). When **no** prompt-level `default` is set, an empty
submission is treated as "no answer" and resolution falls through to the flag's
`.default()` — so pressing Enter never clobbers a configured default with `""`.
Precedence among defaults is **prompt-level `default` > flag `.default()`**.
## Prompt Types
| Kind | Input | Output |
| ------------- | ---------------- | ---------- |
| `input` | Free text | `string` |
| `confirm` | Yes/No | `boolean` |
| `select` | Single choice | Enum value |
| `multiselect` | Multiple choices | Array |
## Per-Command Interactive Resolver
For conditional prompts that depend on other resolved values:
```ts twoslash
import { command, flag } from '@kjanat/dreamcli';
command('deploy')
.flag('region', flag.enum(['us', 'eu', 'ap']))
.flag('confirm', flag.boolean())
.interactive(({ flags }) => ({
region: !flags.region && {
kind: 'select',
message: 'Which region?',
},
confirm: flags.region === 'us' && {
kind: 'confirm',
message: 'Deploy to US prod?',
},
}));
```
The resolver receives partially resolved flags (after CLI/env/config)
and returns prompt configs for any remaining values.
## Non-Interactive Behavior
When stdin is not a TTY (CI, piped input), prompts are automatically skipped.
Required flags that would have prompted instead produce a structured error with an actionable message.
## Testing Prompts
```ts twoslash
import { promptCmd } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import {
runCommand,
createTestPrompter,
PROMPT_CANCEL,
} from '@kjanat/dreamcli/testkit';
// Provide answers in order
const result = await runCommand(promptCmd, [], {
answers: ['eu'],
});
// Simulate cancellation
const cancelled = await runCommand(promptCmd, [], {
prompter: createTestPrompter([PROMPT_CANCEL]),
});
```
## What's Next?
- Related example: [Interactive prompts](/examples/interactive)
- [Testing](/guide/testing) — full test harness documentation
- [Flags](/guide/flags) — flag resolution chain
- [CLI Semantics](/guide/semantics) — prompt precedence and non-interactive behavior
---
# Architecture Rationale
Source: https://dreamcli.kjanat.dev/raw/guide/rationale
# Architecture Rationale
This page explains why dreamcli is shaped the way it is.
Use it when the API already makes sense, but you want the architectural trade-offs behind it.
For exact behavior, use [CLI Semantics](/guide/semantics) and the [Support Matrix](/reference/support-matrix).
## The Core Bet
dreamcli treats the command schema as the product truth, not just parser input.
One definition is expected to drive:
- parsing;
- type inference;
- resolution from CLI, stdin, env, config, prompts, and defaults;
- help and shell completions;
- test harness overrides;
- generated docs and reference surfaces.
That is the main reason the library has custom parser, resolver, completion, and testkit layers instead of delegating each concern to unrelated tools.
## Why Schema-First Instead of Glue-First
Many CLI stacks compose well if you are happy to wire several independent pieces by hand.
dreamcli optimizes for a different goal: define the command once, then keep types and semantics aligned across the whole path.
Benefits:
- handlers receive already-resolved typed values instead of partially parsed blobs;
- help, completions, and tests stay coupled to the same schema truth;
- advanced inputs like env, config, prompts, and stdin do not need separate adapter code in every app.
Trade-off:
- the framework owns more of the stack, so internal architecture matters more than in a thin parser-only library.
## Why One Explicit Execution Pipeline
The re-foundation moved DreamCLI toward one canonical execution path in `src/core/execution/index.ts`.
CLI entrypoints and `runCommand()` now delegate to the same executor instead of each owning their own version of the flow.
Why that matters:
- planner, resolver, and output changes can be localized more safely;
- contract tests can lock behavior at stage boundaries instead of through incidental file shape;
- docs can point at one owned execution seam instead of explaining overlapping ownership.
This design is documented further in the [Semantic Delta Log](/reference/semantic-delta-log), [Planner Contract](/reference/planner-contract), [Resolver Contract](/reference/resolver-contract), and [Output Contract](/reference/output-contract).
## Why The Resolution Chain Is Built In
dreamcli intentionally ships a first-class resolution chain instead of treating env/config/prompt input as userland glue.
The current supported precedence is:
```mermaid
flowchart LR
s1[CLI] -->|miss| s2[stdin] -->|miss| s3[env] -->|miss| s4[config] -->|miss| s5[prompt] -->|miss| s6[default]
```
Flags and positional arguments walk this one order. Each stage is opt-in per
input, so a source an input never declared is a source no stage can produce.
Why this is built in:
- command handlers should see final values, not source-merging boilerplate;
- prompts only make sense when they participate in the same precedence story as other inputs;
- tests need a stable seam for injecting every supported source.
Trade-off:
- the resolver is a real subsystem, not a tiny helper, so its behavior must stay explicit and contract-tested.
## Why Runtime Adapters Exist
dreamcli supports Node, Bun, and Deno without putting runtime-specific APIs in the core command system.
The adapter layer keeps process, stdio, cwd, env, and package/config lookup concerns out of the schema, planner, and executor layers.
Why that is preferable here:
- core behavior stays portable;
- testkit can inject runtime behavior cleanly;
- Bun and Deno support can stay honest without forking the framework design.
Trade-off:
- the runtime floor is intentionally modern, because supporting older environments would force more conditional compatibility code into a codebase that is trying to stay small and explicit.
## Why Docs Are Half Authored And Half Generated
The docs strategy is intentionally mixed.
Human-written pages explain intent, trade-offs, and limitations.
Generated pages cover examples, changelog mirroring, docs health, API inventory, and symbol reference.
Why not generate everything:
- rationale and limitations need judgment, not extraction;
- authored guides are calmer and easier to read than generated prose;
- generated docs are strongest when they stay factual and source-backed.
Why not hand-write everything:
- example inventories, public exports, and symbol pages would drift too easily;
- source-backed docs give evaluators proof, not just claims.
## Why Hover Is Scoped To Example Pages
Twoslash-style hover is useful where a reader is already looking at real code.
It is not useful enough to justify turning the whole docs site into an IDE.
That is why hover is limited to generated example source blocks.
This keeps the docs:
- informative for serious readers;
- readable without interaction;
- operationally boring at build time.
## Why The Library Stays Opinionated
dreamcli is not trying to be the smallest possible CLI parser.
It is trying to be a coherent product for people who want typed commands, typed middleware, built-in structured output, and a serious in-process test harness.
That means the project is opinionated about:
- schema as the law;
- execution stages being explicit;
- generated truth surfaces being source-backed;
- advanced features staying calm instead of flashy.
If those opinions match your use case, the framework removes a lot of repetitive glue.
If you only need flag parsing plus a tiny action, the extra system may be unnecessary.
## Related Pages
- [Why dreamcli](/guide/why)
- [CLI Semantics](/guide/semantics)
- [Support Matrix](/reference/support-matrix)
- [Semantic Delta Log](/reference/semantic-delta-log)
- [Testing Commands](/guide/testing)
---
# Standalone Flag Evaluation
Source: https://dreamcli.kjanat.dev/raw/guide/read-flags
# Standalone Flag Evaluation
`readFlags()` takes a record of flag builders, evaluates it, and returns the
resolved values. There is no command and no dispatch. The call returns typed
values or throws, with one exception: `--help` prints generated help and exits,
unless the record claims the spelling or the built-in is turned off.
## Build Scripts
A build script wants a few typed options and one direct action. Wrapping that in
a `cli()` program costs a command, an action handler, and an output channel for
behavior the script never uses, and calling `parse()` and `resolve()` by hand
returns `Record` and drops the types the builders carry.
`readFlags()` is the flag layer on its own:
```ts twoslash
// build.ts
import { flag, readFlags } from '@kjanat/dreamcli';
const options = await readFlags({
watch: flag.boolean().alias('w').env('WATCH'),
minify: flag.boolean().env('MINIFY').default(true),
target: flag
.enum(['node', 'browser'])
.env('TARGET')
.default('node'),
});
options.target;
// ^?
```
Each of these invocations reaches the same three values:
```bash
WATCH=true bun build.ts
bun build.ts --watch
bun build.ts -w
```
## The Record API
The object key is the `--name` spelling and the key on the returned record. The
result is typed by `InferFlags`, the operator behind `flags` inside `.action()`,
so kinds, enum literals, and presence rules carry over unchanged:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
const values = await readFlags({
out: flag.string().required(),
tag: flag.string(),
verbose: flag.count().alias('v'),
});
values.tag;
// ^?
```
`out` is `string`, `tag` is `string | undefined`, and `verbose` is `number`.
Evaluation is one pass over argv. The record is the complete flag surface, which
is what makes unknown input detectable and what lets collisions between
canonical names, aliases, and negated spellings be rejected. A per-flag reader
would rescan argv and could not see either.
## Semantics Come From The Command Path
`readFlags()` compiles the record into a command schema and runs the same
parser, coercion, resolver, and validation a command runs. Aliases,
`--flag=value`, short-flag clustering, negated spellings declared with
`.negatable()`, duplicate policy, kebab and camel spelling parity, unknown-flag
rejection with suggestions, string and number constraints, Standard Schema
validators, and `flag.path()` checks all behave as they do inside `.action()`.
Collections behave the same way too. `flag.array()` and `flag.keyValue()`
aggregate repeated occurrences within the selected source, `.split()` gives each source its own
decoding, `.separator()` sets the CLI delimiter alone, `.unique()` and
`.duplicateKeys()` apply to the finished value, and a `-` occurrence splices the
stdin buffer into the position it holds:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
const values = await readFlags(
{ tag: flag.array(flag.string()).stdin() },
{ argv: ['--tag', 'before', '--tag', '-'], stdinData: 'a\nb\n' },
);
values.tag; // ['before', 'a', 'b']
```
An array flag whose only source is an env var splits it on `','` by default,
so `{ TAGS: 'x,y' }` reaches the record as `['x', 'y']`.
[CLI Semantics](/guide/semantics) is the rule set, and it applies here as
written.
## Precedence
CLI argv, then piped stdin, then environment variable, then config, then
interactive prompt, then default. Each stage is opt-in per flag and the first
source that supplies a value wins.
`stdin` behaves exactly as it does inside a command. A `.stdin()` flag reads the
stream on `--flag -`, which stays CLI-sourced, and on an absent flag, which
lands on the stdin stage ahead of env. The stream is read at most once per call,
only through the adapter, and only when a declared binding would fire, so a
record whose `when: 'dash'` flag was never dashed never touches it. Pass
`stdinData` to supply the bytes yourself, including `null` for nothing piped.
`{ trim: true }` drops one trailing `\n`, `\r\n`, or `\r` from a single value
here too:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
const values = await readFlags(
{ p: flag.string().stdin({ trim: true }) },
{ argv: [], stdinData: 'hello\n' },
);
values.p; // 'hello'
```
A `-` occurrence the caller passed beside other occurrences rejects when
`stdinData` is `null`, with the same `REQUIRED_FLAG` a command raises:
```ts
await readFlags(
{ tag: flag.array(flag.string()).stdin() },
{ argv: ['--tag', 'a', '--tag', '-'], stdinData: null },
);
// throws: No piped stdin for the '-' occurrence of flag --tag
```
Occurrences of nothing but `-` fall through instead, so `stdinData: null` with
`argv: ['--tag', '-']` leaves the flag to env, config, prompt, and the default.
Two stages need something from the caller. `config` is a plain object passed in;
nothing is read from disk. `prompter` is a prompt engine passed in; none is
built. A `.prompt()` flag with no prompter falls through to its default, on a
TTY as well as off one.
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
const values = await readFlags(
{ body: flag.string().stdin().env('BODY').default('none') },
{ argv: [], stdinData: 'piped\n', env: { BODY: 'env' } },
);
values.body; // 'piped\n'
```
## Explicit Injection
Supplying `argv` and `env` keeps the call away from the host, which is what a
test wants:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
const values = await readFlags(
{ watch: flag.boolean().alias('w').env('WATCH') },
{ argv: [], env: { WATCH: 'true' } },
);
values.watch; // true
```
`argv` is user arguments only, without the binary and script entries.
A test adapter covers the whole runtime surface in one object, including the
argv slicing a real process goes through:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
import { createTestAdapter } from '@kjanat/dreamcli/testkit';
const adapter = createTestAdapter({
argv: ['node', 'build.ts', '--target', 'browser'],
env: { MINIFY: 'false' },
});
const options = await readFlags(
{
minify: flag.boolean().env('MINIFY').default(true),
target: flag
.enum(['node', 'browser'])
.env('TARGET')
.default('node'),
},
{ adapter },
);
expect(options).toEqual({
minify: false,
target: 'browser',
});
```
## Adapter Defaults
| Fact | Source when the caller omits it |
| ------------------ | ---------------------------------------------------------------------------- |
| `argv` | the adapter's argv past its binary and script entries |
| `env` | the adapter's environment snapshot |
| `stdinData` | the adapter's `readStdin()`, called only when a `.stdin()` binding would fire |
| `stat` and `mkdir` | the adapter's filesystem primitives, used by `flag.path()` checks |
| `config` | nothing, so the config stage is skipped |
| `prompter` | nothing, so the prompt stage is skipped |
The adapter is built the first time a fact the caller left out is needed, so a
call given `argv` and `env` builds none at all unless a `.stdin()` binding
selects the stream or a `flag.path()` check reaches for `stat` or `mkdir`. Pass
`adapter` to pin the runtime instead of detecting one. `createAdapter()` detects
Node, Bun, and Deno, so the defaults work on all three.
## Built-In Help
A `--help` or `-h` before the `--` separator prints generated help for the
record to the adapter's stdout and exits with code 0:
```bash
$ bun build.ts --help
Usage: build.ts [flags]
Flags:
-w, --watch Rebuild on change
--minify (default: true)
```
The usage line names the script from the adapter's argv. Help renders ahead of
parsing, so a malformed value elsewhere in argv never hides the text explaining
the flags.
The built-in yields in two ways. Declaring a flag that answers to `help` or `h`
through its name, an alias, a negated spelling, or a case-parity counterpart
takes both spellings over, and they parse as that flag's own. Passing
`help: 'off'` removes the built-in, and an undeclared `--help` is an unknown
flag again.
## Non-Strict Parsing
`strict: false` drops undeclared argv content instead of rejecting it, so a
script can read its own flags out of an argv it shares with another consumer:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
const values = await readFlags(
{ watch: flag.boolean() },
{ argv: ['build', '--watch', '--unknown'], env: {}, strict: false },
);
values.watch; // true
```
Dropped are unknown long flags together with their inline `=value`, unknown
characters inside a short group, positional arguments, and the `--` separator,
which can only introduce positionals here. The filter walks the parser's own
value consumption, so `--name booga` keeps `booga` as the value of a declared
`--name` while the token after an unknown flag is dropped as a positional.
Leniency covers undeclared input only. Misuse of a declared flag throws in
either mode: a missing value, a failed coercion, a violated constraint, and a
repeat under `.duplicates('error')` all keep their diagnostics.
## Deprecation Notices
`.deprecated()` produces one notice per flag that actually sourced a value, in
resolution order. Each reaches `onDeprecation` as a `DeprecationWarning`:
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
// ---cut---
await readFlags(
{ minify: flag.boolean().deprecated('use --optimize') },
{
argv: ['--minify'],
env: {},
onDeprecation: (warning) => {
console.warn(`--${warning.name}: ${warning.message}`);
},
},
);
```
A command prints these on its warning stream. There is no output channel here,
so a call without `onDeprecation` drops them.
## Where Each Value Came From
The return value is the resolved flags alone, so provenance reaches the caller
through `onSources`, which fires once with the whole record before the values are
returned. It is keyed by the definitions record, and a flag that resolved no
value has no entry:
```ts twoslash
import { flag, readFlags, wasExplicit } from '@kjanat/dreamcli';
// ---cut---
const options = await readFlags(
{ out: flag.string().env('OUT_DIR').default('dist') },
{
argv: ['--out', 'build'],
env: {},
onSources: (sources) => {
if (wasExplicit(sources.out)) console.log(`overridden: ${sources.out?.stage}`);
},
},
);
```
The records are the ones documented under
[Which source won](/guide/semantics#which-source-won), including the two stdin
triggers, so a script can tell a piped value from a typed `-`.
## Errors
Parse failures throw `ParseError` and resolution and constraint failures throw
`ValidationError`. Two kinds of definitions record throw `CLIError` before argv
is read. A record that collides on a name, an alias, or a negated spelling
throws `FLAG_NAME_COLLISION`. A record whose keys cannot all be read throws
`INVALID_SCHEMA`: the definition key `__proto__`, or a replaced prototype, which
covers an object-literal `__proto__` key and `Object.create(base)`.
`Object.create(null)` is fine. Failure never writes output and never calls
`adapter.exit`; only the built-in help path exits, and it exits with 0. The
script owns what happens next:
```ts twoslash
import { flag, isCLIError, readFlags } from '@kjanat/dreamcli';
try {
const options = await readFlags({
target: flag.enum(['node', 'browser']),
});
console.log(options.target);
} catch (error) {
if (isCLIError(error)) {
console.error(error.message);
process.exit(error.exitCode);
}
throw error;
}
```
## Non-Goals
**No positional arguments.** A positional token throws
`UNEXPECTED_POSITIONAL`. This API covers flags.
**No config discovery.** Standalone flag reading has no application name and no
discovery policy to guess from, so `config` is whatever the caller passes.
**No output built-ins.** `--json` and `--quiet` are neither reserved nor
answered, so a record may declare either as an ordinary flag. `help` and `h`
are answered by the built-in help, and declaring either spelling takes them
over without a reservation error.
**No synchronous variant.** Prompts, Standard Schema validators, and filesystem
checks are asynchronous. A synchronous variant would either carry different
semantics or reject part of the flag DSL, and either one means a second
implementation of what this reuses.
**No dispatch, actions, middleware, output channel, or completions.** Those
belong to a CLI. Reach for [`cli()`](/guide/commands) when the script grows into
one.
## Related Pages
- [Flags](/guide/flags) for every flag kind and modifier
- [CLI Semantics](/guide/semantics) for the exact parser and precedence rules
- [Runtime Support](/guide/runtime) for what `createAdapter()` detects
- [Testing Commands](/guide/testing) for the testkit adapter
- [Stability Policy](/reference/stability) for the contract on `readFlags`,
`ReadFlagsOptions`, and `FlagMap`
---
# Runtime Support
Source: https://dreamcli.kjanat.dev/raw/guide/runtime
# Runtime Support
dreamcli runs on Node.js, Bun, and Deno without code changes.
A thin `RuntimeAdapter` interface abstracts the platform-specific edges.
## Supported Runtimes
| Runtime | Status | Package |
| ------------------ | --------- | ------------------------ |
| Node.js >= 22.22.2 | Supported | `@kjanat/dreamcli` (npm) |
| Bun >= 1.3 | Supported | `@kjanat/dreamcli` (npm) |
| Deno >= 2.6.0 | Supported | `@kjanat/dreamcli` (JSR) |
These minimums are the tested support floor, declared in the package `engines` field.
As a dependency, your package manager enforces them at install time; DreamCLI does not
hard-fail the host CLI at runtime when the version is lower.
## How It Works
The core framework never imports platform-specific APIs directly.
Instead, a `RuntimeAdapter` provides:
- `argv` — command-line arguments
- `env` — environment variables
- `cwd` — current working directory
- `stdin` — line reader for interactive prompts
- `readStdin` — full piped stdin reader for `.stdin()` flags and arguments
- `exit` — process exit
- `isTTY` — terminal detection
- `stdinIsTTY` — interactive stdin detection
- `getTerminalSize` — current stdout columns and rows when available
- `onTerminalResize` — terminal resize subscription when supported
- `readFile` / `stat` / `homedir` / `configDir` — filesystem access
Runtime detection is automatic — dreamcli picks the right adapter at startup.
## Terminal Width
When `cli().run()` renders help, DreamCLI uses the runtime adapter's terminal width if stdout is a TTY and no explicit help width is configured. Direct `formatHelp()` calls and `cli().execute()` keep the deterministic 80-column fallback.
Node and Bun use `process.stdout.getWindowSize()` when available, falling back to `process.stdout.columns` / `rows`. Deno uses `Deno.consoleSize()`. Resize subscriptions use stdout `resize` on Node/Bun and `SIGWINCH` on Deno Unix platforms.
Set a fixed width with `.help({ width })` or runtime `help: { width }` when tests or generated output need stable wrapping.
## Explicit Adapter
```ts twoslash
import {
createAdapter,
createNodeAdapter,
} from '@kjanat/dreamcli/runtime';
const adapter = createAdapter(); // auto-detect
const nodeAdapter = createNodeAdapter(); // explicit
```
## Deno Permissions
On Deno, the adapter handles permission-safe access to the Deno namespace.
If permissions are missing, features degrade gracefully with clear error messages.
```bash
deno run --allow-read --allow-env mycli.ts deploy
```
## Testing Runtime Seams
For command behavior tests, `runCommand()` is process-free and injects runtime state directly:
```ts twoslash
import { regionCmd } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import { runCommand } from '@kjanat/dreamcli/testkit';
const result = await runCommand(regionCmd, [], {
env: { MY_REGION: 'test' },
});
```
When you need adapter-level control (`argv`, filesystem reads, exit behavior), use
`createTestAdapter()` with `cli().run({ adapter })`.
## What's Next?
- [Testing](/guide/testing) — in-process test harness
- [Getting Started](/guide/getting-started) — installation per runtime
---
# Schema Export
Source: https://dreamcli.kjanat.dev/raw/guide/schema-export
# Schema Export
dreamcli can export the complete CLI schema as JSON — for tooling,
documentation generation, IDE integration, or config file validation.
## Two Export Formats
### Definition Metadata
`generateSchema()` produces a JSON document describing the full CLI tree:
commands, flags, args, types, constraints, env bindings, prompts, and more.
```ts twoslash
import { writeFileSync } from 'node:fs';
import { cli, command } from '@kjanat/dreamcli';
import { generateSchema } from '@kjanat/dreamcli/json-schema';
const myCli = cli('mycli').command(command('deploy'));
const definition = generateSchema(myCli.schema);
writeFileSync(
'cli-schema.json',
JSON.stringify(definition, null, 2),
);
```
Output includes a `$schema` URL pointing at
`https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json`. The `v1` in
that path is the definition format version, the same one the document reports
as `schemaVersion`. Every release that emits `schemaVersion: 1` resolves to it.
Two mirrors carry the same bytes: the copy inside the installed package, and
`https://cdn.jsdelivr.net/npm/@kjanat/dreamcli/dreamcli.schema.json` on the npm
CDN. Emitted documents always carry the canonical URL in `$schema`, and the
meta-schema pins that exact value. For offline or CI-friendly validation, map
the canonical URL to the local copy in your tooling instead of rewriting the
document — in VS Code:
```json
{
"json.schemas": [
{
"url": "./node_modules/@kjanat/dreamcli/dreamcli.schema.json",
"fileMatch": ["cli-schema.json", "*.definition.json"]
}
]
}
```
The schema is also importable as `@kjanat/dreamcli/schema`.
Standalone documents from `generateCommandSchema()` validate against the dedicated
`https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json#/$defs/commandDocument`
entry point.
Full example output:
```json
{
"$schema": "https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json",
"schemaVersion": 1,
"name": "mycli",
"version": "1.0.0",
"commands": [
{
"name": "deploy",
"description": "Deploy the app",
"flags": {
"region": {
"kind": "enum",
"presence": "defaulted",
"defaultValue": "us",
"enumValues": ["us", "eu", "ap"],
"envVar": "REGION"
}
},
"args": [
{
"name": "target",
"kind": "string",
"presence": "required"
}
],
"commands": []
}
]
}
```
### Input Validation Schema
`generateInputSchema()` produces a JSON Schema (draft 2020-12) that
validates CLI input as a JSON object — useful for config file validation.
```ts twoslash
import { writeFileSync } from 'node:fs';
import { cli, command } from '@kjanat/dreamcli';
import { generateInputSchema } from '@kjanat/dreamcli/json-schema';
const myCli = cli('mycli').command(command('deploy'));
const inputSchema = generateInputSchema(myCli.schema);
writeFileSync(
'input-schema.json',
JSON.stringify(inputSchema, null, 2),
);
```
For multi-command CLIs, the output is a `oneOf` discriminated union with
a `command` property identifying each branch:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"type": "object",
"properties": {
"command": { "const": "deploy" },
"region": {
"type": "string",
"enum": ["us", "eu", "ap"]
},
"target": { "type": "string" }
},
"required": ["command", "region", "target"]
}
]
}
```
You can also pass a single `CommandSchema` for a flat object schema
without the `command` discriminator.
Nested subcommands use dot-delimited paths (`"deploy.rollback"`).
A property for an input marked `.sensitive()` is `writeOnly: true` and has no
`default`, so generated validation schemas do not publish credentials or other
private defaults.
## Adding a Schema Command
```ts twoslash
import { cli, command, flag } from '@kjanat/dreamcli';
import {
generateInputSchema,
generateSchema,
} from '@kjanat/dreamcli/json-schema';
const myCli = cli('mycli');
const schema = command('schema')
.description('Export CLI schema as JSON')
.flag(
'input',
flag
.boolean()
.describe('Output JSON Schema for input validation'),
)
.action(({ flags, out }) => {
const result = flags.input
? generateInputSchema(myCli.schema)
: generateSchema(myCli.schema);
out.json(result);
});
```
## Options
Both functions accept `JsonSchemaOptions`:
| Option | Default | Description |
| ---------------- | ------- | ------------------------------------------------ |
| `includeHidden` | `true` | Include commands marked as hidden |
| `includePrompts` | `true` | Include prompt config on flags (definition only) |
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
import { generateSchema } from '@kjanat/dreamcli/json-schema';
const myCli = cli('mycli').command(command('deploy'));
generateSchema(myCli.schema, { includeHidden: false });
```
## What's Included
### Definition Metadata
Version 1 froze with 4.0. Every field below is optional unless noted, and a
field is written only when the schema carries something other than its default.
`defaultValue` is presence-sensitive: an explicit `.default(...)` is written
even when its value matches the kind's implicit unset value, unless the input is
sensitive.
Per command: `name` (always), `description`, `aliases`, `hidden`, `examples`,
`flags` (always), `args` (always), nested `commands` (always).
Per flag: `kind` and `presence` (both always), `defaultValue`,
`defaultDescription`, `sensitive`, `aliases`, `stdin`, `envVar`, `configPath`,
`description`, `enumValues`,
`numberConstraints`, `stringConstraints`, `elementSchema`, `separator`,
`split`, `duplicateKeys`, `unique`, `pathChecks`, `valueHint`, `prompt`,
`deprecated`, `propagate`, `negation`, `duplicates`.
Per arg: `name`, `kind`, and `presence` (all three always), `variadic`, `stdin`,
`defaultValue`, `defaultDescription`, `sensitive`, `description`, `envVar`,
`configPath`, `enumValues`, `elementSchema`, `numberConstraints`,
`stringConstraints`, `pathChecks`, `valueHint`, `separator`, `split`,
`duplicateKeys`, `unique`, `prompt`, `deprecated`.
The arg surface carries every flag field except the five bound to flag syntax
(`aliases`, `propagate`, `negation`, `duplicates`, and the `count` kind), and
adds `name` and `variadic`. An `elementSchema` on an arg is an
`ArgElementFragmentV1`, which is the arg fragment without the `name` a position
supplies.
`defaultValue` is written whenever a non-sensitive schema carries one that
survives JSON, whatever its `presence`. A definition may set a default without
setting `presence: 'defaulted'`, and resolution uses it either way, so the
document states it either way. A sensitive fragment writes `sensitive: true`
but never `defaultValue`; `defaultDescription` remains safe presentation text.
An element fragment is the whole fragment shape, so it admits fields no element
reader consumes: `stdin`, `envVar`, `configPath`, `prompt`, `defaultValue`, and
`variadic` on an arg element. They are validated as any other field is, they
survive the document, and they change nothing at resolution, which reads sources
from the collection itself. Validation still applies in isolation, so
`variadic: true` on an element makes that element's own default an array.
`sensitive` is excluded because it controls the whole input rather than one
element.
### What's Omitted
Non-serializable runtime values are always excluded:
- Parse functions (`parseFn`)
- Standard Schema validators
- Middleware handlers
- Interactive resolvers
- Action handlers
Value provenance is excluded too, and for a different reason: it describes what
one invocation did rather than what the schema declares. See
[Value provenance](/guide/semantics#which-source-won).
## What's Next?
- [Shell Completions](/guide/completions), another schema-driven export
- [Config Files](/guide/config), validate config with input schemas
---
# CLI Semantics
Source: https://dreamcli.kjanat.dev/raw/guide/semantics
# CLI Semantics
This page is the canonical source of truth for dreamcli's edge-case behavior.
Use it when you need exact parser, resolver, or root-surface semantics rather than a feature
overview.
## Parser Rules
The tokenizer is schema-agnostic. It applies these rules before command-specific parsing starts:
| Raw argv form | Meaning |
| -------------- | --------------------------------------------------------------------------- |
| `--flag` | long flag without an inline value |
| `--flag=value` | long flag with an inline value |
| `-abc` | combined short flags |
| `--` | end of options; everything after becomes positional |
| `-` | stdin sentinel for an input declaring `.stdin()`, otherwise a plain positional |
Examples:
```text
deploy --region=eu prod
```
```json
["--region=eu", "prod"]
```
```text
deploy -- --region eu
```
After `--`, both `--region` and `eu` are treated as positional values.
## Flag Parsing
### Repeated flags
- Scalar flags overwrite earlier values. The last occurrence wins.
- Array flags accumulate in order.
- Key-value flags fold their occurrences into one record.
- Count flags total their occurrences.
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag.string(); // repeated -> last value wins
flag.number(); // repeated -> last value wins
flag.boolean(); // repeated -> true stays true unless an explicit false value is parsed
flag.array(flag.string()); // repeated -> accumulates
flag.keyValue(); // repeated -> folds into one record
flag.count(); // repeated -> totals
```
```bash
deploy --tag v1 --tag v2 --tag v3
```
Resolves to:
```json
{ "tag": ["v1", "v2", "v3"] }
```
### Boolean flags
- Bare boolean flags set the value to `true`.
- Long booleans also accept explicit inline values:
- `--flag=true`
- `--flag=false`
- `--flag=1`
- `--flag=0`
- Short booleans are presence-based. `-v` means `true`.
dreamcli does **not** implement special negated-boolean syntax automatically.
Call `.negatable()` on the boolean to accept `--no-confirm` alongside `--confirm`; see [Negatable Booleans](/guide/flags#negatable-booleans).
### Short-flag stacking
- Combined short booleans expand left to right: `-abc` means `-a -b -c`.
- If a short flag expects a value, it consumes either:
- the rest of the current group, or
- the next positional token if it is the last short flag in the group.
Examples:
```bash
-vo out.txt # -> -v, then -o out.txt
-ofile.txt # -> -o file.txt
-oVfile # -> -o Vfile
```
Once a value-taking short flag consumes the remainder of the group, parsing of that group stops.
## Aggregation And Splitting
An input carries one value, an ordered list, a record of key-value entries, or
an occurrence count. Which of the four it is decides how every source fills it,
and it is decided separately from what each value means.
| Form | Declared by | Unset resolves to |
| --------- | --------------------------------------------- | ----------------- |
| one | every scalar kind | `undefined` |
| list | `flag.array()`, `arg` with `.variadic()` | `[]` |
| entries | `flag.keyValue()`, `arg.keyValue()` | `{}` |
| count | `flag.count()` | `0` |
### Per-source split policies
A source that carries text decodes it into elements under its own policy:
| Source | Accepts | Default |
| ------ | --------------------------------------------- | ---------------------------- |
| cli | a delimiter, `'whole'` | `'whole'`, or `.separator()` |
| env | a delimiter, `'whole'`, `'json'` | `','` |
| stdin | a delimiter, `'whole'`, `'lines'`, `'json'` | `'lines'` |
| config | a native array or object; a string uses `env` | native |
`.split({ cli, env, stdin })` sets them; a call names only the sources it
changes. `.separator()` writes the CLI policy on its own. A format a source does
not accept throws `INVALID_SCHEMA` where the input is declared. JSON is parsed
only where the policy says `'json'`, never guessed from the text.
Line splitting treats a final terminator as framing rather than a new element,
and accepts `\n`, `\r\n`, and `\r`: `'a\nb\n'` gives `['a', 'b']` and
`'a\nb\n\n'` gives `['a', 'b', '']`. Delimiter splitting drops empty segments.
### Aggregation rules
- A list applies `.unique()` after every source has resolved, in first-seen
order with `SameValueZero` semantics.
- Entries split each segment at the **first** `=`, so `A=b=c` is
`{ A: 'b=c' }`. A segment with no `=`, or an empty key, fails: `INVALID_VALUE`
on a CLI token, `TYPE_MISMATCH` from every other source.
- Entries fold under `.duplicateKeys()`: `'last'` (the default), `'first'`, or
`'error'`, which reports `CONSTRAINT_VIOLATED` naming the source that carried
the repeat. The policy applies to every source, not only to repeated CLI
occurrences.
- A count reads an explicit value (`--verbose=2`, env, config) as the count
itself, and must be a non-negative integer.
Under `'error'`, each occurrence keeps its own source through aggregation, so a
collection filled from both the command line and a pipe names whichever one
carried the repeat. Tokens the user typed name no source. A key is half of a
`KEY=VALUE` pair, so it follows the input's sensitivity rule: quoted from every
source by default and replaced with `` when the input is sensitive.
```bash
$ mycli set --v A=1 --v A=2
Duplicate key 'A' for flag --v
$ printf 'A=2\n' | mycli set --v A=1 --v -
Duplicate key 'A' from stdin for flag --v
$ VARS='A=1,A=2' mycli set
Duplicate key 'A' from env VARS for flag --v
# with flag.keyValue().sensitive()
Duplicate key '' from env VARS for flag --v
```
The argument surface words it the same way, with `for argument ` in place
of `for flag --v`.
## Resolution Precedence
### Flags
Flags resolve in this order, and the first source that provides a value wins:
```mermaid
flowchart LR
CLI -->|miss| stdin -->|miss| env -->|miss| config -->|miss| prompt -->|miss| default
```
Example:
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag
.enum(['us', 'eu', 'ap'])
.stdin()
.env('DEPLOY_REGION')
.config('deploy.region')
.prompt({ kind: 'select', message: 'Region?' })
.default('us');
```
Outcomes:
| Available sources | Result |
| ------------------------------------------------- | ------ |
| `--region ap`, env=`eu`, config=`us`, prompt=`eu` | `ap` |
| piped stdin `eu`, env=`ap` | `eu` |
| env=`eu`, config=`ap`, prompt=`us` | `eu` |
| config=`ap`, prompt=`us` | `ap` |
| prompt=`eu` | `eu` |
| no value sources | `us` |
Notes:
- An optional flag that aggregates resolves to its empty form when no source
provides a value: `[]` for an array, `{}` for a key-value flag, `0` for a
count.
- Every other optional flag resolves to `undefined`.
- Required flags fail after the full chain is exhausted.
### Positional arguments
Positional arguments walk the same order as flags, and are CLI-only unless they
opt into extra sources. Only args that opt into `.stdin()`, `.env()`,
`.config()`, or `.prompt()` participate in those extra steps:
```mermaid
flowchart LR
CLI["CLI token"] -->|miss| stdin -->|miss| env -->|miss| config -->|miss| prompt -->|miss| default
```
Example:
```ts twoslash
import { arg } from '@kjanat/dreamcli';
arg.string().stdin().env('DEPLOY_TARGET').config('deploy.target').default('local');
```
Outcomes:
| Available sources | Result |
| -------------------------------------- | --------- |
| CLI token `prod`, stdin, env=`staging` | `prod` |
| piped stdin `prod`, env=`staging` | `prod` |
| env=`staging`, config=`eu` | `staging` |
| config=`eu` | `eu` |
| no value sources | `local` |
### Stdin
An input reads stdin only when it declares `.stdin()`. The binding has three
axes, `when`, `consume`, and `trim`, defaulting to `dash-or-missing`,
`exclusive`, and `false`.
| `when` | `-` typed | Input omitted |
| ------------------- | -------------------- | -------------------- |
| `'dash'` | reads stdin | falls through to env |
| `'missing'` | literal string `'-'` | reads stdin |
| `'dash-or-missing'` | reads stdin | reads stdin |
A `-` that selects stdin resolves on the `cli` stage, so it outranks every later
source. An omitted input reads stdin only for `when: 'missing'` or
`when: 'dash-or-missing'`. With `when: 'dash'`, omission falls through to env,
config, prompt, and default. When a selected stdin route has no piped data, it
produces no value and the walk continues, except for a `-` typed beside other
occurrences of a collection, which fails instead. With `when: 'missing'`, a
typed `-` is literal and does not select stdin.
The stream is read at most once per invocation, and only when a declared binding
would fire. Declaring a second exclusive stdin input on one command, flag or
argument, throws `DUPLICATE_STDIN_INPUT` at build time; every stdin input on a
command that passes `{ consume: 'broadcast' }` receives the same buffer.
That build-time rule covers one command's own inputs. Sibling commands may each
declare their own exclusive consumer, and the invocation decides which one runs.
A `.propagate()` stdin flag reaching a subcommand that also declares one is the
one case where two exclusive consumers meet in a single invocation: the buffer
is still read once and both receive it.
For a scalar input, the whole buffer becomes the value. Codecs that preserve
text terminators keep it byte for byte; this includes strings and paths. Other
codecs drop one trailing `\n`, `\r\n`, or `\r` before decoding, then accept
exactly what an env value accepts. `{ trim: true }` drops that one terminator
for a preserving codec and is what a piped path wants before a `mustExist`
check runs.
For a collection, the buffer decodes into elements under the input's stdin
policy, `'lines'` by default. A `-` occurrence stands for the whole stdin source
at the position it holds, and the decoded elements are spliced in there. A
variadic argument reads its tail the same way:
```bash
$ printf 'a\nb\n' | mycli send --tag before --tag - --tag after
# flags.tag === ['before', 'a', 'b', 'after']
$ printf 'a\nb\n' | mycli build x - y
# args.files === ['x', 'a', 'b', 'y']
```
Splicing rules:
- A spliced read stays on the `cli` stage, so it outranks env, config, prompt,
and the default exactly as a scalar `-` does.
- Entries splice into the same occurrence order and then fold under
`.duplicateKeys()`, so a piped key can be overridden by a later CLI one.
- Two `-` occurrences splice the buffer twice. The buffer is read once and each
occurrence stands for all of it, so `--tag - --tag -` over `'a\n'` resolves to
`['a', 'a']` and `mycli build - -` over `'x\n'` to `['x', 'x']`.
- When every occurrence is `-` and nothing was piped, the input produces no CLI
value, so env, config, prompt, and the default stay reachable.
- A `-` typed beside other occurrences with nothing piped fails with
`MISSING_STDIN`, rather than dropping the occurrence and silently shortening
the collection.
- A scalar `-` with nothing piped is silent by design. It is the whole value, so
dropping it loses nothing, and env, config, prompt, and the default stay
reachable. Only a collection can be shortened by a dropped occurrence, so only
a collection can error. The same asymmetry holds for a collection whose every
occurrence is `-`: nothing is left to shorten, so the walk continues.
```bash
$ mycli send --body - # scalar, nothing piped
# body resolves from env, config, prompt, or its default
$ mycli send --tag a --tag - # collection, nothing piped
# Error: No piped stdin for the '-' occurrence of flag --tag (MISSING_STDIN)
```
- An input that declares no stdin binding treats `-` as an ordinary element and
never reads the stream. An input that does declare one can never receive a
literal `-` as a value, on either surface: the token names the source before
anything reads it as text. `{ when: 'missing' }` is the one binding that
leaves a typed `-` literal.
## Value Provenance {#which-source-won}
Every resolved value records which stage produced it. A handler receives those
records as `sources`, beside `flags` and `args` and keyed by the same names:
```ts twoslash
import { command, flag, wasExplicit } from '@kjanat/dreamcli';
command('build')
.flag('out', flag.string().env('OUT_DIR').default('dist'))
.action(({ flags, sources, out }) => {
if (wasExplicit(sources.flags.out)) out.info(`overriding with ${flags.out}`);
if (sources.flags.out?.stage === 'env') out.info(`from ${sources.flags.out.envVar}`);
});
```
### The records
Seven records exist, one per stage plus the two ways stdin delivers bytes. An
input that resolved no value has no record at all, so a name is present in
`flags` and absent in `sources.flags`:
| Record | What happened | Trigger |
| ------------------------------------------------------- | ----------------------------------------- | ----------------------------------- |
| `{ stage: 'cli' }` | a token the user typed | `--out dist`, or a positional token |
| `{ stage: 'cli', via: 'stdin', trigger: 'dash' }` | an explicit `-` read the stream | `--body -`, or a positional `-` |
| `{ stage: 'stdin', via: 'stdin', trigger: 'fallback' }` | an omitted input read the stream | the input was not typed at all |
| `{ stage: 'env', envVar }` | the named environment variable | `.env('BODY')` |
| `{ stage: 'config', configPath }` | the named config path | `.config('send.body')` |
| `{ stage: 'prompt' }` | an answered prompt | `.prompt({ … })` |
| `{ stage: 'default' }` | the declared default | `.default('dist')` |
| absent | nothing produced a value | an optional input no source filled |
Both stdin records carry `via: 'stdin'`, and they differ on `stage` because
they differ on precedence. A typed `-` is CLI-ranked and outranks env, config,
prompt, and the default. An omitted input takes the `stdin` stage, which sits
between `cli` and `env`. Ask about `stage` for precedence and about `via` for
whether the bytes came from a pipe.
The records are the same on both surfaces. `sources.args.target` narrows
exactly as `sources.flags.body` does.
### `wasExplicit()`
```ts twoslash
import { wasExplicit } from '@kjanat/dreamcli';
// ---cut---
wasExplicit(undefined); // false
wasExplicit({ stage: 'default' }); // false
wasExplicit({ stage: 'cli' }); // true
wasExplicit({ stage: 'stdin', via: 'stdin', trigger: 'fallback' }); // true
wasExplicit({ stage: 'env', envVar: 'OUT_DIR' }); // true
```
It answers "did a source other than the default produce this value". Every
stage but `'default'` counts as supplied, including the stdin fallback, since
bytes did arrive. For the narrower question "did the user type it on this
command line", read `stage === 'cli'` instead.
`wasExplicit()` exists so that explicit-versus-defaulted never requires dropping
`.default()`, which would also drop `defaultValue` from the definition document
and the `(default: …)` suffix from help.
### Explicit-wins merges
A CLI that layers its own values over a project manifest needs to know which
values the user supplied, so a default never outranks the manifest:
```ts twoslash
import { command, flag, wasExplicit } from '@kjanat/dreamcli';
declare function readManifest(): { readonly outDir?: string; readonly minify?: boolean };
// ---cut---
command('build')
.flag('out', flag.string().default('dist'))
.flag('minify', flag.boolean().default(false))
.derive(({ flags, sources }) => {
const manifest = readManifest();
return {
outDir: wasExplicit(sources.flags.out) ? flags.out : manifest.outDir ?? flags.out,
minify: wasExplicit(sources.flags.minify) ? flags.minify : manifest.minify ?? flags.minify,
};
})
.action(({ ctx, out }) => out.info(`building into ${ctx.outDir}`));
```
Comparing against the default value would fail in exactly the case the check
exists for, a user passing a value that happens to equal the default.
### Diagnostics that name the stage
An `--explain` mode wants the label rather than the boolean:
```ts twoslash
import { type ResolutionProvenance, command, flag } from '@kjanat/dreamcli';
function describe(source: ResolutionProvenance | undefined): string {
if (source === undefined) return 'unset';
switch (source.stage) {
case 'cli':
return 'via' in source ? "the '-' you typed, read from stdin" : 'the command line';
case 'stdin':
return 'piped stdin';
case 'env':
return `the environment variable ${source.envVar}`;
case 'config':
return `the config key ${source.configPath}`;
case 'prompt':
return 'your prompt answer';
case 'default':
return 'the declared default';
default:
return source satisfies never;
}
}
command('deploy')
.flag('region', flag.string().env('REGION').config('deploy.region').default('us'))
.flag('explain', flag.boolean())
.action(({ flags, sources, out }) => {
if (flags.explain) out.info(`region came from ${describe(sources.flags.region)}`);
});
```
`ResolutionProvenance` is a closed union, so the `satisfies never` default arm
makes a future stage a compile error rather than a silent fallthrough.
### Other entry points
`resolve()` returns the same records on `ResolveResult.provenance`, keyed by
plain strings because it takes a `CommandSchema` rather than typed builders.
`readFlags()` hands them to its `onSources` receiver, typed against the
definitions record the caller passed:
```ts twoslash
import { flag, readFlags, wasExplicit } from '@kjanat/dreamcli';
// ---cut---
await readFlags(
{ out: flag.string().env('OUT_DIR').default('dist') },
{
argv: [],
env: { OUT_DIR: 'build' },
onSources: (sources) => {
wasExplicit(sources.out); // true
sources.out?.stage; // 'env'
},
},
);
```
Provenance describes what one invocation did, not what a schema declares, so no
definition document carries it.
## Diagnostics and Redaction
One rule governs framework diagnostics on both surfaces: the owning flag or
argument schema decides sensitivity. Values are visible by default from every
source. `.sensitive()` replaces framework-controlled value text with
`` and omits raw-derived fields from structured details.
| Input declaration | Sources | Message value | Raw-derived details |
| ----------------- | ---------------------------------------- | ------------- | ------------------- |
| default | argv, stdin, env, config, prompt, default | visible | present |
| `.sensitive()` | argv, stdin, env, config, prompt, default | `` | absent |
An explicit `-` still records stdin as the source because its bytes came from
the pipe, but that provenance does not decide secrecy.
The same rule reaches every later diagnostic pass. Sensitive collection keys,
Standard Schema issue paths and record keys, filesystem paths, and path-bearing
adapter causes are omitted. Framework-computed collection indices remain, so a
sensitive element failure can still name `--tag[1]` without exposing the value.
Help also omits a sensitive input's automatic default annotation; an explicit
safe `.default(value, { description })` remains visible.
```bash
# flag.string().minLength(9).env('API_TOKEN')
$ API_TOKEN=sk-9f2 mycli deploy
Invalid value 'sk-9f2' from env API_TOKEN for flag --token: must be at least 9 characters
# flag.string().minLength(9).env('API_TOKEN').sensitive()
$ API_TOKEN=sk-9f2 mycli deploy
Invalid value '' from env API_TOKEN for flag --token: must be at least 9 characters
$ mycli deploy --token sk-9f2
Invalid value '' for flag --token: must be at least 9 characters
```
The argument surface words the same failures with `for argument ` in
place of `for flag --token`, and is otherwise byte for byte identical.
Everything that identifies a sensitive failure survives redaction. `details`
carries the input name under `flag` or `arg`, the winning source under `source`,
the `envVar` or `configPath` that named it, the `expected` type, the `constraint`
that failed with its `bound` or `pattern`, and the `allowed` enum values:
```json
{
"flag": "token",
"source": "env",
"envVar": "API_TOKEN",
"expected": "string",
"constraint": "minLength",
"bound": 9
}
```
That `source` field is also what labels an issue inside an aggregate error:
```
Multiple validation errors (1 flag, 1 arg):
- flag --token [env API_TOKEN]: Invalid value '' from env API_TOKEN for flag --token: must be at least 9 characters
- argument [env COUNT]: Invalid number value '' from env COUNT for argument
```
A Standard Schema failure follows the same rule. Its `details` always carries
`issues`, and carries `value` only when the input is not sensitive.
Codes differ by boundary because a CLI token fails during parsing and every
other source fails during resolution. A malformed argv token throws
`INVALID_VALUE`; the same text from env, config, stdin, or a prompt throws
`TYPE_MISMATCH` when it does not decode, `INVALID_ENUM` when it is outside an
enum, and `CONSTRAINT_VIOLATED` when it decodes but violates a constraint.
One channel remains deliberately outside framework redaction: text your own code
writes. A `flag.custom()` parse function's thrown message and a Standard Schema
issue message are shown verbatim, so a parse function that interpolates its
input publishes that input even when the schema is sensitive:
```ts
flag.custom((v) => {
throw new Error(`bad input ${v}`); // reaches the diagnostic verbatim
});
```
Write those messages so they describe the expectation rather than the value.
Built-in date, URL, duration, and byte parsers keep their reasons raw-free so
their surrounding framework diagnostic can apply sensitivity safely.
## Non-Interactive Behavior
Prompting is conditional, not mandatory.
- Prompts run only after CLI, stdin, env, and config resolution fail to provide a value.
- Prompts run only when a prompter is available.
- In normal CLI execution, auto-prompting is enabled only when `stdinIsTTY` is `true`.
- In non-interactive environments, prompt-backed values fall through to defaults or required-value
errors.
Implications:
- CI, pipes, and redirected stdin do not trigger automatic prompts.
- Required prompt-backed flags still fail with a structured validation error when no other source
resolves them.
- Prompt cancellation falls through to the default when one exists; otherwise required validation
still applies.
## Propagation and Masking
Flags marked with `.propagate()` are inherited by descendant commands.
Important masking rules:
- Only ancestor flags marked `propagate: true` are inherited.
- A child command that defines the same flag name masks the ancestor's propagated flag.
- That masking applies even if the child flag does **not** propagate.
- Intermediate overrides block deeper descendants from receiving the ancestor definition.
Example shape:
```mermaid
flowchart TD
root["root
--verbose (propagate)"] -->|inherits| db["db
--verbose (local, masks)"]
db -->|blocked| migrate
```
`migrate` does not inherit root's propagated `--verbose`, because the intermediate `db` command
redefined the same flag name.
## Root and Default-Command Semantics
dreamcli distinguishes between executability and visibility.
Hidden commands stay executable, but they are omitted from root help and shell completions.
### Root help
- No default command: root usage shows ``.
- Visible default command: root usage shows `[command]`.
- Single visible default command: root help merges the root summary with the default command's
detailed help.
- Visible sibling commands: root help stays command-centric and lists commands instead of merging
full default-command help.
- Hidden default command: still executable, but treated as invisible in root help.
Examples:
| Root shape | Help behavior |
| ---------------------------------- | ---------------------------------------- |
| default only, visible | merged root + default help |
| visible default + visible siblings | root command list |
| hidden default + visible sibling | root command list without default |
| no default | root command list with `` usage |
### Root completions
Completion behavior depends on both the root shape and `rootMode`.
- `'subcommands'` is the default.
- `'surface'` additionally exposes the default command's root-usable flags at the CLI root.
- A single visible default command exposes its flags at the root even in `'subcommands'` mode.
- Hidden defaults are not surfaced through root completions.
- Root surface exposure includes the default command's own root-usable flags, not child-only flags
from its subcommands.
Examples:
| Root shape | `rootMode` | Root completion surface |
| ---------------------------------- | ------------- | -------------------------------------------------- |
| default `serve` + sibling `status` | `subcommands` | `serve`, `status`, root built-ins |
| default `serve` + sibling `status` | `surface` | commands + root built-ins + `serve` root flags |
| single visible default `serve` | `subcommands` | command name + root built-ins + `serve` root flags |
### Command resolution with value-flags
When locating the command name in argv, dreamcli skips the token a space-separated value-flag
consumes, so that value is never mistaken for a command name. This is most visible with a default
command, where every token sits at the root level:
```bash
mycli --region status # `status` is the value of --region (even if a command
# named `status` exists), so this runs the default command
mycli --region=status # inline form, same result
mycli status # a bare token dispatches the `status` command
```
The same applies one level down: `mycli db --tag x migrate` resolves `migrate`, not `x`. Boolean
flags take no value, so a following token is still treated as a command name. For example,
`mycli --verbose deploy` runs `deploy`. The `--` separator stops command-name scanning entirely;
everything after it is positional.
## Related Guides
- [Commands](/guide/commands)
- [Flags](/guide/flags)
- [Config Files](/guide/config)
- [Interactive Prompts](/guide/prompts)
- [Shell Completions](/guide/completions)
- [Semantic Delta Log](/reference/semantic-delta-log)
---
# Testing
Source: https://dreamcli.kjanat.dev/raw/guide/testing
# Testing
dreamcli's test harness runs commands in-process with full control over inputs and outputs.
No subprocesses, no `process.argv` mutation, no mocking.
## Basic Usage
```ts twoslash
import { greet } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import { runCommand } from '@kjanat/dreamcli/testkit';
const result = await runCommand(greet, ['Alice', '--loud']);
expect(result.exitCode).toBe(0);
expect(result.stdout).toEqual(['HELLO, ALICE!\n']);
expect(result.stderr).toEqual([]);
expect(result.error).toBeUndefined();
```
## RunOptions
Control every dimension of CLI behavior from tests:
```ts twoslash
import { deploy } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import { runCommand } from '@kjanat/dreamcli/testkit';
await runCommand(deploy, ['production'], {
// environment variables
env: { DEPLOY_REGION: 'eu' },
});
await runCommand(deploy, ['production'], {
// config file values
config: { deploy: { region: 'us' } },
});
await runCommand(deploy, ['production'], {
// prompt answers (consumed in order)
answers: ['ap'],
});
await runCommand(deploy, ['production'], {
// piped stdin for flags and args configured with .stdin()
stdinData: '',
// simulate --json mode
jsonMode: true,
// verbosity level
verbosity: 'quiet',
// simulate TTY output
isTTY: true,
});
```
### Available Options
| Option | Type | Description |
| ----------- | ------------------------------------- | -------------------------------------------------- |
| `env` | `Record` | Environment variables |
| `config` | `Record` | Config file values |
| `stdinData` | `string \| null` | Data supplied to command stdin for `.stdin()` inputs |
| `answers` | `unknown[]` | Prompt answers in order |
| `prompter` | `PromptEngine` | Custom prompt handler |
| `jsonMode` | `boolean` | Simulate `--json` mode |
| `help` | `HelpOptions` | Help formatting options |
| `verbosity` | `Verbosity` | Output verbosity level |
| `isTTY` | `boolean` | Simulate a TTY stdout connection |
## Testing Prompts
```ts twoslash
import { promptCmd } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import {
runCommand,
createTestPrompter,
PROMPT_CANCEL,
} from '@kjanat/dreamcli/testkit';
// Sequential answers
await runCommand(promptCmd, [], {
answers: ['eu'],
});
// Simulate prompt cancellation
await runCommand(promptCmd, [], {
prompter: createTestPrompter([PROMPT_CANCEL]),
});
```
## Asserting Activity Events
Spinners and progress bars emit testable events:
```ts twoslash
import { activityCmd } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import { runCommand } from '@kjanat/dreamcli/testkit';
const result = await runCommand(activityCmd, []);
expect(result.activity).toContainEqual(
expect.objectContaining({ type: 'spinner:start' }),
);
expect(result.activity).toContainEqual(
expect.objectContaining({ type: 'spinner:succeed' }),
);
```
## Captured Output
```ts twoslash
import type { RunResult } from '@kjanat/dreamcli/testkit';
type CapturedOutput = Pick<
RunResult,
'stdout' | 'stderr' | 'exitCode' | 'error'
>;
declare const captured: CapturedOutput;
// ^?
```
`activity` is tracked separately from captured output and is covered in the section above.
## Design Philosophy
- **No lifecycle hooks** — isolation comes from the testkit architecture
- **No snapshots** — all assertions are explicit
- **No mocking** — use `RunOptions` injection instead of `vi.mock()`
- **No `process.argv`** — everything is passed as parameters
## What's Next?
- Related example: [Testing](/examples/testing)
- [Commands](/guide/commands) — building commands to test
- [Output](/guide/output) — output channel behavior
---
# Troubleshooting
Source: https://dreamcli.kjanat.dev/raw/guide/troubleshooting
# Troubleshooting
This page covers the most likely real failure modes when building or evaluating a DreamCLI app.
Use it alongside [CLI Semantics](/guide/semantics) and the [Support Matrix](/reference/support-matrix):
those pages describe product truth and exact rules; this page translates the common failure cases into
quick diagnosis steps.
## Prompts Never Appear
Symptom:
- a flag has `.prompt()` configured, but the CLI errors instead of asking;
- the same command prompts locally but not in CI or when piped.
Cause:
- DreamCLI only auto-prompts when a prompter exists and `stdinIsTTY` is `true`.
Check:
- are you running in CI, a pipe, or redirected stdin context;
- did an earlier source already resolve the value from CLI, env, or config.
Fix:
- provide the value through CLI, env, config, stdin-backed inputs, or a default;
- in tests, inject answers through `runCommand()` instead of relying on terminal behavior.
References: [Interactive Prompts](/guide/prompts), [CLI Semantics](/guide/semantics)
## Config Values Are Ignored
Symptom:
- a config file exists, but the command still uses env, prompt, or default values;
- a config value works for one flag but not another.
Cause:
- config only participates for inputs wired with `.config(path)`;
- config is lower priority than CLI, stdin, and env on both surfaces.
Check:
- the CLI is configured with `cli().config('')`;
- the specific flag or argument uses the expected `.config('a.b.c')` path;
- a higher-priority source did not already win.
Fix:
- add or correct the input's `.config()` path;
- remove the higher-priority value while testing precedence.
References: [Config Files](/guide/config), [CLI Semantics](/guide/semantics)
## Config Parsing Fails For YAML Or TOML
Symptom:
- DreamCLI reports a config parse or load error for a non-JSON file.
Cause:
- built-in config discovery is JSON-only.
Fix:
- stay on JSON for the default path;
- or register a custom loader with `configFormat()` and `configLoader()`.
References: [Config Files](/guide/config), [Limitations And Workarounds](/guide/limitations)
## Piped Stdin Does Not Reach An Input
Symptom:
- you pipe data into the command, but the flag or argument stays empty or falls through to
env/default.
Cause:
- a flag or argument reads stdin only when it declared `.stdin()`;
- a `{ when: 'dash' }` binding reads the stream only for an explicit `-`;
- a `{ when: 'missing' }` binding treats a typed `-` as the literal string.
Check:
- the declaration includes `.stdin()`;
- the binding's `when` matches how the value is being passed;
- the CLI token or `--flag value` did not already satisfy the input first.
Fix:
- opt the input into `.stdin()` if piped data is part of the intended contract;
- widen `when` to the default `'dash-or-missing'` to accept both forms;
- otherwise pass the value explicitly on argv.
References: [CLI Semantics](/guide/semantics), [Arguments](/guide/arguments), [Flags](/guide/flags)
## A Piped Value Carries A Trailing Newline
Symptom:
- a piped string compares unequal to the value you expected.
Cause:
- codecs that preserve text terminators keep the stdin buffer byte for byte by
default. This includes strings and paths, so `flag.path()` and `arg.path()`
keep the terminator unless `{ trim: true }` is set.
- other codecs interpret the text and drop one trailing `\n`, `\r\n`, or `\r`
before decoding. `echo 42` reaches `flag.number()` as `42`, and `echo 30s`
reaches `flag.duration()` as `30s`.
Check:
- whether the binding passed `{ trim: true }`;
- the input's codec. String and path codecs carry the terminator this far, so
`trim` changes nothing on decoding codecs.
- whether the producer appends a newline. `echo` does; `printf` without `\n`
does not.
Fix:
- declare `.stdin({ trim: true })`, which drops one trailing `\n`, `\r\n`, or
`\r` from a single value before any check runs;
- or emit the value without a terminator, for example `printf '%s' './docs' | mycli`;
- or declare the input as a collection, where line splitting treats a final
terminator as framing and drops it.
```ts
arg.path({ mustExist: true }).stdin({ trim: true });
```
```bash
$ echo ./docs | mycli check
# the mustExist check runs against './docs'
```
`trim` applies to a single value. A collection's terminators separate its
elements, so `.split({ stdin })` decides those and `trim` has nothing to do.
References: [CLI Semantics](/guide/semantics), [Flags](/guide/flags#path),
[Arguments](/guide/arguments#path)
## A Piped Collection Loses Or Duplicates Elements
Symptom:
- a `-` occurrence on an array, key-value, or variadic input produces nothing,
or produces more elements than the pipe carried;
- the pipe's elements land in the wrong position in the resolved list.
Cause:
- a `-` occurrence stands for the whole stdin source at the position it holds,
and the decoded elements are spliced in there. Two `-` occurrences therefore
splice the same buffer twice.
- when every occurrence is `-` and nothing was piped, the input produces no CLI
value at all, so a later source or the default supplies the result.
- an input that never declared `.stdin()` treats `-` as an ordinary element and
never reads the stream.
- the buffer decodes under the *stdin* policy, `'lines'` by default, not under
the CLI separator.
Check:
- the declaration includes `.stdin()`, and its `when` accepts a dash;
- how many `-` occurrences the invocation actually passes;
- whether `.split({ stdin })` matches the shape being piped, for example
`'json'` for a piped JSON document.
Fix:
- pass `-` once for one splice;
- set `.split({ stdin: 'json' })` or a delimiter when the pipe is not
line-oriented;
- pass the values on argv when the pipe was not meant to be the source.
References: [Collections](/guide/flags#collections),
[CLI Semantics](/guide/semantics)
## A `-` Occurrence Fails With Nothing Piped
Symptom:
- a command that mixes typed values with a `-` exits 2 before the action runs:
```
No piped stdin for the '-' occurrence of flag --tag
Suggestion: Pipe a value to stdin, or drop the '-' occurrence of --tag
```
```
No piped stdin for the '-' occurrence of argument
Suggestion: Pipe a value to stdin, or drop the '-' from
```
Cause:
- a `-` among other occurrences is one element of the collection, and nothing
was piped for it to stand for. Resolution fails with `MISSING_STDIN` rather
than shortening the collection behind the caller's back.
- occurrences of nothing but `-` behave differently: they are the whole value,
so with nothing piped they fall through to env, config, prompt, and the
default, the way an absent input does.
- a scalar `-` behaves that way too. It is the whole value, so dropping it
loses nothing and resolution falls through.
Check:
- whether the producer feeding the pipe actually wrote anything;
- whether the invocation is a shell that opened no pipe at all;
- whether the typed occurrences beside the `-` were meant to be there.
Fix:
- pipe a value to stdin;
- or drop the `-` and let the remaining occurrences stand alone;
- or declare `{ when: 'missing' }`, which leaves a typed `-` as the literal
string and reads the stream only when the input is absent.
References: [Collections](/guide/flags#collections),
[CLI Semantics](/guide/semantics#stdin)
## A `-` Stdin Selector Is Not A Literal Value
Symptom:
- an input whose binding treats `-` as a stdin selector cannot hold the
one-character string `-`; the token reads the stream, or fails because nothing
was piped.
Cause:
- with `when: 'dash'` or `when: 'dash-or-missing'`, the token names the source
before anything reads it as text, so `-` is not data. With `when: 'missing'`,
a typed `-` remains a literal value.
Check:
- whether the value the caller wants really is a bare `-`, rather than a path
or a name that begins with one;
- which `when` the binding declares.
Fix:
- declare `{ when: 'missing' }`, which reads the stream only for an absent
input and leaves a typed `-` literal;
- or drop `.stdin()` from that input and read the stream on a different one;
- or pass the value through `.env()`, `.config()`, or a config file.
There is no escape syntax for a stdin-enabled input. `--` ends flag parsing, so
it does not make a following `-` literal either.
References: [Flags](/guide/flags#stdin-backed-flags),
[Arguments](/guide/arguments#stdin-backed-arguments),
[CLI Semantics](/guide/semantics#stdin)
## An Error Message Says `` Instead Of The Value
Symptom:
- a validation error names the input, the source, and the reason, but prints
`''` where the offending value used to be;
- `error.details.value` is missing on the same error.
Cause:
- the flag or argument is marked `.sensitive()`. Sensitivity applies equally to
argv, stdin, env, config, prompt, and default values;
- sensitive framework diagnostics omit raw-derived values, collection keys,
validator paths, filesystem paths, and adapter causes.
Check:
- whether the declaration calls `.sensitive()` or a definition sets
`sensitive: true`;
- which source produced the value. `details.source` names it, and
`details.envVar` or `details.configPath` names the binding, but source does not
decide redaction;
- whether the reason is enough on its own. `expected`, `constraint`, `bound`,
and `allowed` all survive redaction.
Fix:
- read the value from the source you control rather than from the diagnostic;
- remove `.sensitive()` or call `.sensitive(false)` only when the value is safe
to write to terminals, logs, and CI transcripts;
- for a `flag.custom()` parse function, write the thrown message to describe the
expectation rather than interpolating the input, since your own message is
shown verbatim and the framework cannot redact it.
References: [Diagnostics and redaction](/guide/semantics#diagnostics-and-redaction),
[Upgrading to 4.0](/guide/upgrading-v4#diagnostic-secrecy-is-explicit)
## A Defaulted Input Looks Like The User Supplied It
Symptom:
- a mutually-exclusive rule fires when the user passed nothing;
- a merge over a project manifest uses the CLI default instead of the manifest
value;
- `flags.x !== undefined` is true for every invocation.
Cause:
- a `.default()` value is a real resolved value, so reading `flags` alone cannot
separate "the user supplied it" from "the chain fell through to the default".
Check:
- `sources.flags.x` in the handler, or `sources.args.x` for a positional. An
input the default filled reports `{ stage: 'default' }`, and one no source
filled has no record at all.
Fix:
- use `wasExplicit(sources.flags.x)`, which is false for `'default'` and for an
absent record and true for every other stage;
- read `sources.flags.x?.stage === 'cli'` when the question is narrower, "did
the user type it on this command line";
- do not drop `.default()` to detect this. That also drops `defaultValue` from
the definition document and the `(default: …)` suffix from help.
References: [Value provenance](/guide/semantics#which-source-won),
[Limitations And Workarounds](/guide/limitations)
## An Argument Declared After A Variadic One Throws
Symptom:
- building the command throws before any argv is read:
```
Argument comes after variadic argument , which consumes every remaining positional
Suggestion: Declare before , or drop .variadic() from
```
Cause:
- a variadic argument takes every remaining positional token, so anything
registered behind it could never be filled, and a second variadic one would
have nothing left to collect.
Check:
- the order of the `.arg()` calls, or of the `args` entries in a definition;
- `details`, which carries the command in `command`, the argument that could
never fill in `arg`, and the greedy one in `variadicArg`.
Fix:
- move the variadic argument last;
- or drop `.variadic()` from the earlier one.
The code is `INVALID_BUILDER_STATE` on both construction paths, and a definition
tree reports the nested command that declared the pair.
References: [Variadic Arguments](/guide/arguments#variadic-arguments),
[Upgrading to 4.0](/guide/upgrading-v4)
## A Collection Modifier Throws On An Argument
Symptom:
- `.separator()`, `.split()`, `.unique()`, or `.duplicateKeys()` is refused by
the compiler, or a definition throws `INVALID_SCHEMA`:
```
Arg schema field 'separator' requires a collection, received a non-variadic 'string' arg
Suggestion: Add 'variadic: true', declare the arg as kind 'keyValue', or drop 'separator'
```
```
Arg schema field 'unique' requires a variadic arg of a list kind
Suggestion: Add 'variadic: true' on a list kind, or drop 'unique'
```
Cause:
- these four are collection modifiers, and an argument that aggregates nothing
has no elements to split, dedupe, or fold. Each states the shape it needs:
`.separator()` and `.split()` want a variadic argument or `arg.keyValue()`,
`.unique()` a variadic argument of a list kind, and `.duplicateKeys()`
`arg.keyValue()`.
Check:
- whether `.variadic()` sits ahead of the modifier in the chain;
- for `.unique()`, that the kind is a list rather than `arg.keyValue()`, which
folds repeated keys through `.duplicateKeys()` instead.
Fix:
- add `.variadic()` before the modifier;
- or declare the argument as `arg.keyValue()`;
- or drop the call, which changed nothing on a single-value argument anyway.
References: [Collections](/guide/arguments#collections),
[Upgrading to 4.0](/guide/upgrading-v4)
## Two Inputs Both Want Stdin
Symptom:
- building the command throws `DUPLICATE_STDIN_INPUT` before any argv is read.
Cause:
- one command has one exclusive stdin consumer, and a second `.stdin()` input of either surface
claims a stream that is already spoken for.
Fix:
- keep `.stdin()` on a single input;
- or declare every stdin input on that command with `{ consume: 'broadcast' }`, which hands the
same buffer to each of them.
References: [Arguments](/guide/arguments#stdin-constraints), [Flags](/guide/flags#stdin-constraints)
## `--json` Changes The Output Shape
Symptom:
- spinner or progress output disappears;
- decorative output does not show up when stdout is piped;
- logs look different in tests than in an interactive terminal.
Cause:
- DreamCLI intentionally changes output policy in JSON mode and non-TTY contexts.
Fix:
- treat JSON mode as a machine-readable surface, not a styled terminal surface;
- test interactive and non-interactive output separately when both matter;
- use the captured `stdout`, `stderr`, and `activity` arrays from `runCommand()` to assert exact behavior.
References: [Output](/guide/output), [Testing Commands](/guide/testing), [Output Contract](/reference/output-contract)
## Completion Script Installs, But Suggestions Look Wrong
Symptom:
- the generated completion script loads, but expected commands or flags are missing;
- root-level completion behaves differently than expected.
Cause:
- hidden commands stay executable but are omitted from help and completions;
- root completion behavior depends on default-command visibility and root mode;
- the wrong shell script may have been installed for the active shell.
Check:
- which shell script you generated and installed;
- whether the command or flag is intentionally hidden;
- whether root behavior depends on a visible default command.
Fix:
- regenerate completions for the exact target shell;
- confirm the command-tree visibility rules in your schema;
- review root/default-command completion semantics before assuming generation is broken.
References: [Shell Completions](/guide/completions), [CLI Semantics](/guide/semantics)
## Tests Behave Differently From Real CLI Runs
Symptom:
- a command passes in `runCommand()` but behaves differently from manual terminal usage;
- prompt or TTY-sensitive behavior does not line up.
Cause:
- the test harness is in-process and fully controlled by `RunOptions`.
Check:
- whether the test set `jsonMode`, `isTTY`, `stdinData`, `env`, `config`, or `answers`;
- whether the real CLI run has different stdin or terminal conditions.
Fix:
- make the test conditions explicit instead of relying on defaults;
- add separate cases for interactive TTY and non-interactive execution when behavior diverges by design.
References: [Testing Commands](/guide/testing), [Runtime Support](/guide/runtime)
## Still Stuck?
Use this order:
1. Check [CLI Semantics](/guide/semantics) for precedence or root-surface rules.
2. Confirm in [Support Matrix](/reference/support-matrix) that the surface is actually shipped.
3. Review [Limitations And Workarounds](/guide/limitations) for intentional constraints.
4. Reduce the command to one failing flag or arg and reproduce it under `runCommand()`.
## Related Pages
- [CLI Semantics](/guide/semantics)
- [Limitations And Workarounds](/guide/limitations)
- [Migration And Adoption](/guide/migration)
- [Testing Commands](/guide/testing)
- [Support Matrix](/reference/support-matrix)
---
# Upgrading From 2.x To 3.0
Source: https://dreamcli.kjanat.dev/raw/guide/upgrading-v3
# Upgrading From 2.x To 3.0
This page covers moving an existing dreamcli 2.5.0 CLI to 3.0.0. For adopting
dreamcli from another framework, see [Migration And Adoption](/guide/migration).
Most 2.x CLIs run on 3.0 unchanged. The breaking changes cluster around the
default command, number parsing, and a few removed exports; everything else in
this page is behavioral polish you may notice in output, plus the new surface
worth adopting.
## Breaking Changes
### The default command is the root surface, not a named subcommand
`.default(cmd)` now registers the command only as the default. It is no longer
added to `schema.commands`, so:
- `mycli mycmd` no longer routes to it by name; the token is treated as the
default command's first positional.
- It is omitted from the root `Commands:` list.
To restore 2.x behavior where the default is also a routable, listed command:
```ts
cli('mycli').default(serve, { route: true });
```
To only re-list it in `Commands:` without name routing:
```ts
cli('mycli').help({ showDefaultInCommands: true });
```
### Number flags and args reject `Infinity` by default
`finite` now defaults to `true`, so `--retries Infinity` errors instead of
passing `Infinity` to your handler. `NaN` was already rejected. Opt back in per
flag:
```ts
flag.number().finite(false);
```
### `createBunAdapter` is removed
Bun exposes a Node-compatible `process`, so the Bun adapter was a passthrough.
Replace explicit uses with `createNodeAdapter` (identical behavior) or rely on
`createAdapter()` auto-detection. `detectRuntime()` still reports `'bun'`.
### `buildConfigSearchPaths` takes an options object
```ts
// 2.x
buildConfigSearchPaths('mycli', cwd, configDir, loaders);
// 3.0
buildConfigSearchPaths('mycli', {
baseDir: cwd,
userConfigDirs: [configDir],
systemConfigDirs: ['/etc'],
loaders,
});
```
### npm package always resolves built `dist`
The `bun`/`deno` → `src/*.ts` export conditions are gone and `src` is not
published to npm (JSR still ships source). Deep imports into `src/` from the
npm package no longer resolve; import from the public entrypoints.
### `flag.array()` element position is type-restricted
Flag-level modifiers on array elements (`.alias()`, `.env()`, `.default()`,
`.prompt()`, …) were silently ignored in 2.x and are now compile-time errors,
as are `flag.path()` / `flag.count()` / `flag.keyValue()` / nested arrays in
element position. Move the modifier to the array flag itself.
### Runtime version check no longer hard-fails
2.x threw at adapter construction when the Node/Bun/Deno version was below
dreamcli's minimum, crashing the host CLI even on `--help`. The check is gone;
your package manager's `engines` handling owns runtime policy now. If you
relied on it as a guard, enforce your own minimum at startup.
## Behavioral Changes To Review
- **Kebab ↔ camel flag parity is on by default** — `--doThis` matches a flag
named `do-this` and vice versa. Disable globally with
`cli('mycli', { flags: { caseParity: false } })` if you depended on strict
spelling.
- **Config discovery probes more locations** — project scope now walks ancestor
directories and `.config/`, user scope adds `~/Library/Application Support`
on macOS, and `/etc/{app}` is a system fallback. First match still wins, but
a file in a newly probed location can now win where 2.x found nothing. See
[Config Files](/guide/config) for the exact order.
- **Help output changed** — root help lists built-in flags under
`Global options:`, the default command renders inline, examples are
syntax-highlighted, sections are color-themed on TTYs, and width follows the
live terminal in `.run()`. Golden-help tests from 2.x will need re-recording.
- **`--help` matches like `--version`** — a help flag preceded by other flags
(`mycli --verbose --help`) now shows help instead of falling through.
- **`--json` respects the `--` separator everywhere** — `mycli cmd -- --json`
is a positional, not JSON mode, in `.run()`, `.execute()`, and `runCommand()`
alike.
## New In 3.0
Adopt at your own pace; none of these are required:
- **Validation**: [Standard Schema](https://standardschema.dev) support in
`flag.custom(schema)` / `arg.custom(schema)`; string and number constraints
(`.nonEmpty()`, `.pattern()`, `.int()`, `.min()`, …).
- **New flag kinds**: `flag.url()`, `flag.path()` (existence/type checks, and
`create: true` to mkdir missing directories), `flag.date()`,
`flag.duration()`, `flag.bytes()`, `flag.count()`, `flag.keyValue()`.
- **Parser control**: `.negatable()` for `--no-foo`, `.duplicates('error')`,
array `.separator(',')` and `.unique()`.
- **Output**: `out.color` (gated ANSI palette), `out.status()` (stderr status
lines), global `--quiet`/`-q`, `out.isHyperlinkSupported`,
`resolveRenderContext()` for pre-run render decisions.
- **Help**: themes, `flagOrder`/`sortFlags`, function-form `.example()`
receiving the real program name, JSON help (`--help --json`).
- **Completions**: `.completions({ as: 'flag' })` with shell auto-detection.
- **Config**: expanded discovery scopes and `discoverConfig({ baseDir })`.
- **Diagnostics**: `@kjanat/dreamcli/version` exposes `DREAMCLI_VERSION` /
`DREAMCLI_REVISION`.
See the [CHANGELOG](https://github.com/kjanat/dreamcli/blob/master/CHANGELOG.md)
for the complete rc-by-rc record.
---
# Upgrading From 3.x To 4.0
Source: https://dreamcli.kjanat.dev/raw/guide/upgrading-v4
# Upgrading From 3.x To 4.0
This page covers moving an existing dreamcli 3.x CLI to 4.0.0. Coming from 2.x,
read [Upgrading From 2.x To 3.0](/guide/upgrading-v3) first. For adopting
dreamcli from another framework, see
[Migration And Adoption](/guide/migration).
A CLI built entirely from the fluent builders (`cli()`, `command()`, `flag`,
`arg`) mostly runs on 4.0 unchanged. Two things changed on that path. A command
flag spelled like a root-owned flag is now rejected when you register the
command, and `.flag('__proto__')` or `.arg('__proto__')` now throws
`INVALID_SCHEMA`. The rest of the breaking changes land on code that assembles
schema objects by hand, hand-rolls an `Out`, reaches into `CLISchema.commands`,
or passes framework-populated fields into execution options. Every category and
its compatibility rules are listed on the
[Stability Policy](/reference/stability) page.
## Breaking Changes
### A command flag cannot spell a root-owned flag
The root owns `--json`, `--quiet` / `-q`, and `--help` / `-h`, and
`--version` / `-V` joins them once the CLI declares a version. It strips the
two output tokens from argv before dispatch, intercepts `--version` / `-V`, and
renders help before a command's flags are parsed. In 3.x a command could
declare one of those spellings anyway. It built, it ran, it showed up in help,
and its flag sat at the default value on every invocation:
```ts
// 3.x
const build = command('build')
.flag('quiet', flag.boolean().describe('Suppress build logs'))
.action(({ flags, out }) => {
out.log(`quiet=${flags.quiet}`);
});
await cli('mycli').command(build).run();
// $ mycli build --quiet
// quiet=false
```
4.0 rejects the registration with a `CLIError` carrying code `RESERVED_FLAG`:
```
Command 'build' defines a '--quiet' flag, which is reserved by the root
'--quiet' flag. The root strips that token before dispatch, so the command
can never receive it
```
The error's suggestion names the remedies, `Rename the flag, use out.status()
for output that root --quiet suppresses, or release the built-in with
.builtins({ quiet: 'off' }) before registering the command`.
Renaming and `out.status()` cover a command that never wanted the token:
```ts
// 4.0
const build = command('build').action(({ out }) => {
out.status('Building'); // stderr, suppressed by root --quiet
out.log('dist/app.js');
});
```
A command that really does own the token keeps its flag and takes the built-in
over instead. `--json` naming a document and `-q` meaning something
domain-specific are the usual cases:
```ts
// 4.0
cli('mycli')
.builtins({ quiet: 'off' })
.command(build);
```
`.builtins()` has to come first, since `.command()` checks against the state it
has. See [Taking a built-in over](/guide/output#taking-a-built-in-over) for
what `'off'` releases.
The check reads each flag's canonical name, every alias including hidden ones,
and a custom negated spelling from `.negatable({ alias: 'quiet' })`, through
the whole subcommand tree. `.command()`, `.default()`, `.version()`,
`.manifest(data)`, and `createCLISchema()` all run it, so a `version` flag
throws whether the command is registered before or after `.version()`. A
version that `.manifest()` reads off the filesystem arrives past every one of
those, so `.run()` runs the same guard again once discovery supplies it. The
startup fails with the identical error, reported like any other `.run()`
startup failure: message and suggestion on stderr, the serialized error on
stdout under `--json`, and the error's exit code. `--completions` is reserved
on the same terms once `.completions({ as: 'flag' })` or
`CLIDefinition.completionsFlag` is set.
Near misses stay legal: `quietMode` and `jsonOutput` as names, `-Q` and `-j` as
aliases, the default `--no-` negated spelling, and a `version` flag on a
CLI that declares no version.
### `Out` gained a required `verbosity` member
An object literal typed as `Out` stops compiling until it declares `verbosity`.
Handlers read `out.verbosity` for the active level, and
`resolveRenderContext()` exposes the same decision for content built before
`.run()`, including `--`-aware `--quiet` / `-q` detection.
Hand-rolled test doubles are the common casualty. Take a real channel from the
testkit and spy on it:
```ts
import { createCaptureOutput } from '@kjanat/dreamcli/testkit';
const [out] = createCaptureOutput();
vi.spyOn(out, 'info');
```
Do not spread a channel. Its methods are installed as non-enumerable bound own
properties, so `{ ...out, info: vi.fn() }` carries none of them.
### `Out` and `RenderContext` are sealed
Both interfaces carry a private brand, so implementing or structurally
constructing them outside the framework no longer type-checks. Obtain instances
from action parameters, `createOutput()`, `createCaptureOutput()`, or
`resolveRenderContext()`.
Helpers that only need part of the channel take a capability type:
```ts twoslash
import type { Out } from '@kjanat/dreamcli';
// ---cut---
function renderRows(out: Pick, rows: readonly string[]) {
for (const row of rows) out.info(row);
}
```
Both are non-exhaustive values, so minor releases may add readonly members.
Exhaustive mappings over their keys (`Record`) will break on
a minor.
### Schemas are sealed and built by factories
`FlagSchema`, `ArgSchema`, `CommandSchema`, `CLISchema`, and `ConfigSettings`
carry the same type-only brand. An object literal assembled by hand is no longer
assignable where a schema is expected. Four factories build them:
`createFlagSchema()`, `createArgSchema()`, `createCommandSchema()`, and
`createCLISchema()`. Each takes a plain definition object listing only the
fields you care about, and fills in the rest:
```ts
// 3.x
const schema: CommandSchema = {
name: 'deploy',
description: undefined,
flags: { force: createSchema('boolean') },
args: [],
commands: [],
hidden: false,
// ...every remaining field
};
// 4.0
const schema = createCommandSchema({
name: 'deploy',
flags: { force: { kind: 'boolean' } },
});
```
The brand has no runtime value, so nothing about the objects themselves changed.
Spreading a built schema keeps the brand (`{ ...schema, hidden: true }` stays
assignable), `structuredClone()` and JSON round-trips are unaffected, and
feeding a built schema back through its factory returns a deep-equal schema.
Nested fields accept either rung, so a code generator can hand
`createCommandSchema()` or `createCLISchema()` one complete tree of plain
objects:
```ts twoslash
import { createCLISchema } from '@kjanat/dreamcli';
// ---cut---
const schema = createCLISchema({
name: 'mycli',
version: '1.0.0',
commands: [
{
name: 'deploy',
flags: { region: { kind: 'enum', enumValues: ['us', 'eu', 'ap'] } },
args: [{ name: 'target', schema: { kind: 'string' } }],
},
],
});
```
The definition types are exported from the package root: `FlagDefinition`,
`ArgDefinition`, `CommandDefinition`, `CLIDefinition`,
`ConfigSettingsDefinition`, the per-kind members (`EnumFlagDefinition`,
`ArrayFlagDefinition`, and the rest), and the `*DefinitionOverrides` helpers.
### `createSchema()` is now `createFlagSchema()`
The positional form is a rename. Both factories additionally accept a single
definition object, and both validate fields against the kind:
```ts twoslash
import { createFlagSchema } from '@kjanat/dreamcli';
// ---cut---
const region = createFlagSchema('enum', { enumValues: ['us', 'eu', 'ap'] });
const tags = createFlagSchema({
kind: 'array',
elementSchema: { kind: 'string' },
separator: ',',
});
```
A field belonging to another kind, such as `enumValues` on a `boolean` flag,
fails to compile and throws `INVALID_SCHEMA` at runtime.
`createArgSchema()` picked up the same rule: its second parameter changed from
`Partial` to `ArgDefinitionOverrides`, and `enumValues` is
required on an `enum` arg.
### `createCommandSchema()` validates what the builder validates
A definition whose flags share a name, an alias, or a negated spelling on one
command throws `FLAG_NAME_COLLISION`, and a flag spelled the same way as one
propagated from an ancestor command throws `PROPAGATED_FLAG_COLLISION`.
```ts
// 3.x: built fine; help listed -v on both flags and -v set only --version
createCommandSchema({
name: 'run',
flags: {
verbose: { kind: 'boolean', aliases: ['v'] },
version: { kind: 'boolean', aliases: ['v'] },
},
});
// 4.0: throws FLAG_NAME_COLLISION
```
In 3.x both flags still parsed under their canonical names, so what the
collision cost was the shared spelling. Help advertised `-v` on both flags and
the parser answered it with `--version`.
The arg invariants moved with them. A second stdin-backed input on the same
command throws `DUPLICATE_STDIN_INPUT` (renamed from v3's
`DUPLICATE_STDIN_ARG`), and a positional declared after a variadic one throws
`INVALID_BUILDER_STATE`, on both construction paths. In 3.x
a hand-assembled `CommandSchema` could declare either, and two stdin-backed args
each resolved to the whole of stdin while a positional behind a variadic one
silently never filled. The placement rule is new to `.arg()` as well, and
[its own section](#a-positional-after-a-variadic-one-is-a-build-error) covers
what to change.
A flag or arg named `__proto__` throws `INVALID_SCHEMA`. Both land in a record
keyed by name, where that key sets a prototype rather than an entry, so the flag
or the value the user typed used to disappear.
```ts
// 3.x: built fine, then the positional value never reached the handler
createCommandSchema({
name: 'run',
args: [{ name: '__proto__', schema: { kind: 'string' } }],
});
// 4.0: throws INVALID_SCHEMA
```
A `flags` record whose prototype is replaced throws `INVALID_SCHEMA` as well.
Only its own keys are read, so an object-literal `__proto__` key and a record
built with `Object.create(base)` both hide a flag the caller meant to declare,
and 3.x built the command without either one:
```ts
// 3.x: built fine, then --shared was an unknown flag
createCommandSchema({
name: 'run',
flags: Object.create({ shared: { kind: 'boolean' } }),
});
// 4.0: throws INVALID_SCHEMA
```
`Object.create(null)` stays legal. Its prototype carries nothing, so a flag put
on such a record is an own key and is read.
The whole tree is checked, nested subcommands included, and `createCLISchema()`
inherits the checks through it. `command()` already refused the collisions at
`.flag()` and `.command()`, and refused a second stdin-backed arg at `.arg()`
under the old code `DUPLICATE_STDIN_ARG`. For those, only definitions composed
as data change: one that used to build and then misbehave at parse time now
fails at construction. Argument placement is the exception, since `.arg()`
accepted a positional after a variadic one in 3.x.
`__proto__` changes on the builder too. `.arg('__proto__')` used to build and
then swallow the positional value; it now throws `INVALID_SCHEMA`.
`.flag('__proto__')` already threw, with code `FLAG_NAME_COLLISION` naming a flag
the command never declared, and now throws `INVALID_SCHEMA` like the factory.
Names such as `constructor`, `toString`, and `valueOf` are unaffected and always
were legal as data. `.flag()` used to reject them as duplicates of a flag the
command never declared; it now accepts them, so a schema `command()` produces
and a schema `createCommandSchema()` produces agree on every name.
### `CommandSchema.middleware` is removed
The executor builds the handler chain from the builder's ordered execution
steps, so registration order, handler identity, and runtime behavior are
unchanged. `.middleware()` on `CommandBuilder` works exactly as before.
Code that read `schema.middleware` has no replacement field. Assert on behavior
instead, by running the command and checking what the middleware did.
`CommandDefinition` has no `middleware` key and `createCommandSchema()` emits
none.
### `CLISchema` describes the program without the execution graph
`CLISchema.commands` and `CLISchema.defaultCommand` used to hold `ErasedCommand`
wrappers carrying the action handler and an `_execute` function. Both now hold
`CommandSchema`, and the compiled execution graph lives beside the schema:
```ts
// 3.x
app.schema.commands[0].schema.name;
// 4.0
app.schema.commands[0].name;
```
`ErasedCommand` is gone. `CLISchema.plugins` is gone as well, since plugins are
execution state; `.plugin()` registration, order, and hook behavior are
unchanged.
### `CLIBuilder` is factory-only
`CLIBuilder`'s constructor is private, so `new CLIBuilder(schema)` no longer
compiles. Call `cli(name)` or `cli({ ... })` for an executable program, and
`createCLISchema()` when you want a description with no handlers attached. Every
builder method still returns a new builder.
`createCLISchema()` throws `INVALID_SCHEMA` on an empty name, and `cli('')`
does the same instead of building a nameless program.
### `.execute()` and `.run()` take different option types
`.execute()` takes the new `CLIExecuteOptions`, which carries the process-free
option surface and has no `adapter` member. `CLIRunOptions` extends it with
`adapter`, and `.run()` is the only method that accepts one.
```ts
// 3.x
await app.execute(argv, { adapter, env });
// 4.0
await app.execute(argv, { env });
await app.run({ adapter });
```
### Internal execution fields left `RunOptions`
`out`, `captured`, `mergedSchema`, `meta`, and `plugins` were framework-populated
fields marked `@internal` that shipped on the public option types. They now live
on unexported internal execution options, so passing them to `runCommand()`,
`.execute()`, or `.run()` stops compiling.
`runCommand()` captures stdout, stderr, and activity events itself and returns
them on `RunResult`, so tests assert on the result rather than on an injected
channel:
```ts
const result = await runCommand(deploy, ['production']);
expect(result.stdout).toEqual(['Deploying production\n']);
expect(result.activity).toEqual([{ type: 'spinner:start', text: 'Deploying' }]);
```
Code that genuinely needs its own writers builds a channel with
`createOutput({ stdout, stderr })` and drives it directly. `OutputOptions.stdout`
and `OutputOptions.stderr` are the supported injection seam.
### The 2.5-era manifest API is removed
Deprecated since 2.5, removed here. Every removal has a direct replacement:
| Removed | Replacement |
| ---------------------------------------- | -------------------------------------------------------------------------- |
| `.packageJson(settings)` | `.manifest(settings)`, which defaults to `files: ['package.json']` |
| `.packageJson(data)` | `.manifest(data)`, taking the same pre-loaded data object |
| `.denoJson(settings)` | `.manifest({ files: ['deno.json', 'deno.jsonc', 'jsr.json'], ...settings })` |
| `ManifestPresetSettings` | `ManifestSettings`, which adds `files` |
| `PackageJsonSettings` | `ResolvedManifestSettings` |
| `discoverPackageJson(adapter, startDir)` | `discoverManifest(adapter, { startDir, files: ['package.json'] })` |
```ts
// 3.x
cli('mycli').denoJson({ inferName: true });
// 4.0
cli('mycli').manifest({
files: ['deno.json', 'deno.jsonc', 'jsr.json'],
inferName: true,
});
```
`files` already defaults to `['package.json']` on both `.manifest()` and
`discoverManifest()`, so the `package.json` migrations are a rename. The
`CLISchema.packageJsonSettings` field keeps its name and shape. See
[`.manifest()`](/reference/main) for the full settings surface.
### `CommandConfig` and the root `AnyCommandBuilder` export are removed
`CommandConfig` described the builder's type-level accumulator shape and no
signature referenced it. `AnyCommandBuilder` is erasure machinery that appears
only on `CommandBuilder`'s underscore members, so the package root no longer
exports the name. Neither has a replacement; code naming them was reaching into
internals.
### Definition documents are versioned
`generateSchema()` and `generateCommandSchema()` emit `schemaVersion: 1`, so a
consumer can tell which format it is reading before parsing the rest. Both
return typed documents (`DefinitionDocumentV1` and
`CommandDefinitionDocumentV1`, aliased as `DefinitionDocument` and
`CommandDefinitionDocument`) in place of `Record`. The returned
documents stay assignable to `Record`, so existing consumer
signatures keep compiling.
Fragments nested inside a document (`CommandDefinitionFragmentV1`,
`FlagDefinitionFragmentV1`, `ArgDefinitionFragmentV1`, and the rest) carry no
`schemaVersion` of their own and take the version of the document they sit in.
`generateInputSchema()` is standard JSON Schema and stays outside the family,
typed as `InputSchemaDocument`.
The canonical `$schema` and `$id` moved to
`https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json`, replacing
`https://cdn.jsdelivr.net/npm/@kjanat/dreamcli/dreamcli.schema.json`. The `v1`
segment tracks the definition format, so it stays valid for every package
release that emits `schemaVersion: 1`. The `@kjanat/dreamcli/schema` subpath and
the jsDelivr URL keep serving the same bytes as mirrors. Documents pinned to the
old URL still validate against a local copy, but they no longer match the
meta-schema's `$schema` constant. For offline validation, map the canonical URL
to the local copy in your tooling; [Schema Export](/guide/schema-export) shows
the VS Code form.
`--help --json` output changed accordingly. Command-level help serializes
through `generateCommandSchema()`, so `schemaVersion` is now its first key. Root
help serializes through `generateSchema()`, so `$schema` still leads, carries
the new URL, and `schemaVersion` follows it. Snapshot tests over that output
need re-recording.
### `ArgSchema.stdinMode` is now `ArgSchema.stdin`
The boolean carried no room for the trigger and sharing modes the unified source
model needs, so it is replaced by `stdin: StdinBinding | undefined`. `.stdin()`
is unchanged for callers who pass no options, and keeps its exact resolution
behavior.
```ts
// 3.x — reading a built schema
if (schema.stdinMode) { /* … */ }
// 4.0
if (schema.stdin !== undefined) { /* … */ }
```
```ts
// 3.x — a definition passed to createArgSchema() or createCommandSchema()
createArgSchema('string', { stdinMode: true });
// 4.0
createArgSchema('string', { stdin: {} });
createArgSchema('string', { stdin: { when: 'dash', consume: 'broadcast' } });
```
The definition document changes to match. An arg fragment emits
`stdin: { when, consume, trim }` where `stdinMode: true` used to appear, at
`schemaVersion: 1`, which has not shipped. All three fields are always written,
so a reader never has to know the builder's defaults. Tooling reading the
fragment reads the object; tooling that only asked whether stdin was enabled
checks for the key's presence.
### `DUPLICATE_STDIN_ARG` is now `DUPLICATE_STDIN_INPUT`
The one-exclusive-consumer rule now covers flags as well as args, so the code
names an input rather than an argument. Error handling matching the old code
matches the new one; the message reads `Only one input may consume stdin
exclusively; already consumes stdin`, naming the input that was declared
first. `details` names the offending input under `flag` or `arg` and the
existing one under `existingFlag` or `existingArg`.
### An explicit `-` with nothing piped fails inside a collection
A `-` the user typed beside other occurrences used to be dropped when nothing
was piped, so `--tag a --tag - --tag b` with an empty pipe resolved to
`['a', 'b']` and the caller never learned the pipe had been empty. It now fails:
```
No piped stdin for the '-' occurrence of flag --tag
Suggestion: Pipe a value to stdin, or drop the '-' occurrence of --tag
```
The code is `MISSING_STDIN` on both surfaces, and the positional tail wording is
`No piped stdin for the '-' occurrence of argument `. `details` carries
`flag` or `arg` plus `source: 'stdin'`. Code matching `REQUIRED_FLAG` or
`REQUIRED_ARG` to catch this case matches the dedicated code instead; those two
keep their own meaning, a required input no source filled.
Two shapes are unchanged. Occurrences of nothing but `-` are the whole value, so
with nothing piped they still fall through to env, config, prompt, and the
default. So does a scalar `-`, since dropping it loses nothing. Only a
collection could be silently shortened, so only a collection can error.
A script that relied on the old drop has two ways forward: pipe a value, or stop
passing `-` when there is nothing to read. A `{ when: 'missing' }` binding never
enters this path, since it leaves a typed `-` literal.
### `.stdin()` and `.variadic()` compose
The pairing threw `INVALID_BUILDER_STATE` with the message `Argument
cannot be both variadic and stdin-backed`. It is now legal, and a `-` among the
tail tokens splices what the buffer decodes to into that position:
```ts
command('build').arg('files', arg.string().variadic().stdin());
```
```bash
$ printf 'x\ny\n' | mycli build a - b
# args.files === ['a', 'x', 'y', 'b']
$ printf 'x\ny\n' | mycli build
# args.files === ['x', 'y']
```
An empty tail counts as an absent input, so a binding that covers a missing one
takes the whole buffer. Code that caught the throw has nothing left to catch;
delete the `try`. Nothing that built in 3.x resolves differently, since the
pairing could not be built.
### Stdin values reach non-string codecs without their trailing terminator
The stdin source hands the whole buffer over byte for byte, so a `string` input
still receives `'hi\n'` from `echo hi | mycli`. Every other codec interprets the
text, where a terminator a pipe appended is framing rather than value, so one
trailing `\n`, `\r\n`, or `\r` is dropped before decoding.
That makes three cases work which used to fail: `echo true | mycli` resolves a
boolean input to `true`, `echo 1h30m` reaches `arg.duration()`, and `echo eu`
reaches an enum input. A number input is unaffected, since `Number()` already
ignored the terminator. Code that piped a value with a terminator to a custom
parse function now sees it trimmed; pipe through a `string` input and split it
yourself where the terminator was load-bearing.
Stdin also stops borrowing the prompt widening table. It accepts exactly what an
env value accepts, so the prompt-only `y` and `n` boolean spellings are
rejected; write `true` / `false`, `1` / `0`, or `yes` / `no`.
### Declared defaults are validated
A `.default()` value is the typed value, so it is validated rather than decoded.
String and number constraints, element and aggregate Standard Schema validators,
the shape the cardinality requires, and the collection rules all apply to it.
Purely synchronous violations throw `INVALID_DEFAULT` where the chain declares
them:
```ts
flag.string({ minLength: 3 }).default('ab'); // throws INVALID_DEFAULT
flag.number().default(Number.POSITIVE_INFINITY); // throws INVALID_DEFAULT
arg.string().default('ab').minLength(3); // throws, whichever order you write
```
Fix the default, or widen the declaration where the value was intended:
`flag.number({ finite: false }).default(Number.POSITIVE_INFINITY)` still holds.
Asynchronous validators and `flag.path()` filesystem checks stay at resolution
time, where defaults already went through them.
Building a schema from a definition object skips the compiler, so the same pass
also checks the value against the codec itself:
```ts
createFlagSchema({ kind: 'enum', enumValues: ['us', 'eu'], defaultValue: 'ap' });
// throws INVALID_DEFAULT: expected one of: us, eu
```
### `.default()` on a variadic arg takes the array
The type parameter followed the element type, so the only value that compiled
was a single element. It now follows what the arg resolves to:
```ts
// 3.x: compiled, then resolved to the bare string
arg.string().variadic().default('a');
// 4.0: the array the arg produces
arg.string().variadic().default(['a', 'b']);
```
### A positional after a variadic one is a build error
A variadic argument takes every remaining positional token, so anything
registered behind it could never be filled and a second variadic one had nothing
left to collect. Both used to build and then produce an argument that stayed
empty. `.arg()` and `createCommandSchema()` now share one check:
```ts
// 3.x: built fine, then never filled
command('copy').arg('files', arg.string().variadic()).arg('target', arg.string());
// 4.0: throws INVALID_BUILDER_STATE
```
```
Argument comes after variadic argument , which consumes every remaining positional
Suggestion: Declare before , or drop .variadic() from
```
`details` carries the command in `command`, the argument that could never fill
in `arg`, and the greedy one in `variadicArg`, so a definition tree names the
nested command that declared the pair.
Move the variadic argument last. Required, optional, and variadic in that order
still builds, as does a scalar followed by an `arg.keyValue().variadic()` tail.
A command that genuinely needs a trailing value after a list has to take it as a
flag instead, since positional order alone cannot express it.
### The arg collection modifiers require a collection
`.unique()`, `.separator()`, `.split()`, and `.duplicateKeys()` state the shape
they need. `.separator()` and `.split()` want a variadic argument or
`arg.keyValue()`, `.unique()` a variadic argument of a list kind, and
`.duplicateKeys()` `arg.keyValue()`. The compiler refuses each one elsewhere,
and `createArgSchema()` throws `INVALID_SCHEMA`:
```ts
// 3.x: built fine, and the field was stored and never read
createArgSchema('string', { unique: true });
createArgSchema('string', { separator: ',' });
// 4.0: throws INVALID_SCHEMA
```
```
Arg schema field 'unique' requires a variadic arg of a list kind
Suggestion: Add 'variadic: true' on a list kind, or drop 'unique'
```
Add `variadic: true`, declare the argument as `keyValue`, or drop the field,
which changed nothing on a single-value argument anyway. The values a schema
already stores as its own default, `unique: false` and `duplicateKeys: 'last'`,
stay accepted on any kind, so feeding a built `ArgSchema` back through
`createArgSchema()` round-trips. That exemption is why `.duplicateKeys('last')`
on a scalar builder is a compile-time error rather than a runtime one: at run
time the call is indistinguishable from a round-tripped default. The same holds
for `createFlagSchema()` and `.unique(false)`.
The four builder methods are new in 4.0, so only definitions composed as data
change. This mirrors the `array | keyValue` restriction the flag surface already
had.
### `.separator()` sets the CLI delimiter alone
Env and config string values used to inherit the CLI separator, so
`.separator('|')` silently changed how an env var decoded. Each source now
carries its own policy: env values split on `','`, and the stdin buffer splits on
line terminators.
```ts
// 3.x: one delimiter everywhere
flag.array(flag.string()).separator('|').env('REGIONS');
// 4.0: name each source you meant
flag.array(flag.string()).split({ cli: '|', env: '|' }).env('REGIONS');
```
Code that only ever passed CLI tokens needs no change.
### Diagnostic secrecy is explicit
4.0 uses one sensitivity rule on flags and arguments. A rejected value is shown
from argv, stdin, env, config, a prompt, or a default unless the input is marked
`.sensitive()`:
```ts
flag.string().pattern(/^ghp_/).env('API_TOKEN').sensitive();
arg.string().env('API_TOKEN').sensitive();
```
```
Invalid value '' from env API_TOKEN for flag --token: must match /^ghp_/
```
Audit every secret-bearing input during migration. Without `.sensitive()`, the
message and `details.value` contain the rejected value even when it came from an
environment variable, config file, pipe, prompt, or explicit `-`. This changes
the old argument behavior and the blanket redaction used by 4.0 prereleases.
Sensitive diagnostics omit raw-derived values, collection keys, Standard Schema
paths and record keys, filesystem paths, and path-bearing adapter causes. They
retain the input name, source (`source`, plus `envVar` or `configPath`), expected
type, constraint metadata, and allowed enum values. Framework-computed collection
indices remain useful without exposing the value.
Custom parser and Standard Schema messages remain verbatim because application
code authored them. Do not interpolate an input into those messages when the
schema is sensitive. Built-in URL, date, duration, and byte parsers use raw-free
reasons.
`FlagSchema` and `ArgSchema` now carry the normalized `sensitive: boolean` field,
and definition documents serialize it. Sensitive defaults are omitted from
automatic help annotations; pass a safe description to
`.default(value, { description: 'from keychain' })` when help should explain the
source without printing the value.
### `FlagSchema` and `ArgSchema` carry the cardinality axis
`FlagSchema` gains `split`, `duplicateKeys`, and `aggregateStandard`, and
`ArgSchema` gains `separator`, `split`, `duplicateKeys`, `unique`, and
`aggregateStandard`. Code that spells out a whole schema literal adds them;
`createFlagSchema()` and `createArgSchema()` fill them in. `elementSchema` and
`separator` are valid on `keyValue` as well as `array` flags, `standard` is valid
on every kind, `aggregateStandard` only on a kind that aggregates, and `stdin` is
valid on scalar and collection kinds.
### Optional features moved to subpath entries
The root entry no longer exports the completion generators, the definition and input schema
generators, config and manifest discovery, or the terminal prompter. Each lives on its own subpath
and `cli()` loads it on demand, so a program that never renders a completion script, serializes
`--help --json`, discovers config, or prompts does not load that code.
| v3 import from `@kjanat/dreamcli` | v4 import |
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `generateCompletion`, `generateBashCompletion`, `generateZshCompletion`, `generateFishCompletion`, `generatePowerShellCompletion` | `@kjanat/dreamcli/completion` |
| `generateSchema`, `generateCommandSchema`, `generateInputSchema`, `definitionMetaSchema` | `@kjanat/dreamcli/json-schema` |
| `discoverConfig`, `discoverManifest`, `inferCliName`, `buildConfigSearchPaths`, `configFormat`, `packageRepositoryUrl` | `@kjanat/dreamcli/config` |
| `createTerminalPrompter` | `@kjanat/dreamcli/prompt` |
Every type stays on the root entry, as do `SHELLS`, `DEFINITION_SCHEMA_URL`,
`DEFINITION_SCHEMA_VERSION`, and `resolvePromptConfig`. `.completions()`, `.config()`,
`.manifest()`, and prompting need no change.
```ts
// v3
import { cli, configFormat, generateCompletion } from '@kjanat/dreamcli';
// v4
import { cli } from '@kjanat/dreamcli';
import { generateCompletion } from '@kjanat/dreamcli/completion';
import { configFormat } from '@kjanat/dreamcli/config';
```
## Behavioral Changes To Review
- **Root `--json` and `--quiet` take an explicit value.** `--json=true`,
`--json=1`, `--json=false`, and `--json=0` set the mode where 3.x failed with
`Unknown flag`, and `--quiet` accepts the same literals. The last occurrence
wins, and an invalid literal exits 2 with the parser's `INVALID_VALUE` error.
Short `-q` stays presence-only. See
[Output](/guide/output#status-lines-and-quiet-mode).
- **Quiet mode suppresses rendered spinner and progress output.** Activity
handles resolve to no-ops under quiet verbosity, including on interactive
TTYs and for explicit static fallbacks. Capture still records lifecycle
events, so `RunResult.activity` assertions are unaffected. Informational rows
routed through `out.info()` are suppressed as before.
## New In 4.0
Adopt at your own pace; none of these are required:
- **Stability policy**: [a reference page](/reference/stability) classifying
every exported type and the rules each category carries into minor releases.
- **Descriptions without an execution graph**: `createCLISchema()` and
`createCommandSchema()` build a normalized schema tree from plain objects, for
code generators, docs tooling, and fixtures that never execute.
- **Kind-checked definitions**: the `*Definition` types make illegal field
combinations unrepresentable, with the same check enforced at runtime for
JavaScript callers.
- **Verbosity in handler code**: `out.verbosity` and
`resolveRenderContext().verbosity` expose the active level for custom
rendering built inside or before a run.
- **One source model for both surfaces**: `arg.config()` and `arg.prompt()` join
`arg.env()`, `flag.string().stdin()` becomes legal, and every input resolves
through `CLI -> stdin -> env -> config -> prompt -> default`. See
[Arguments](/guide/arguments#stdin-backed-arguments).
- **Cardinality as its own axis**: `.split({ cli, env, stdin })` gives each source
its own decoding, `.duplicateKeys()` decides what a repeated key means,
collections read stdin with `-` splicing into occurrence order, and
`arg.boolean()` and `arg.keyValue()` join the arg factories. See
[Flags](/guide/flags) and [Arguments](/guide/arguments).
- **Trimming a piped value**: `.stdin({ trim: true })` drops one trailing `\n`,
`\r\n`, or `\r` from a single value, so `echo ./dist | mycli clean` satisfies
`arg.path({ mustExist: true })`. It defaults to `false`, which is what 3.x
did, so an existing binding is unaffected. See
[Flags](/guide/flags#stdin-backed-flags) and
[Arguments](/guide/arguments#stdin-backed-arguments).
- **Where each value came from**: handlers receive `sources` beside `flags` and
`args`, carrying the stage that produced each value, and `wasExplicit()`
answers explicit-versus-defaulted without dropping `.default()`. `resolve()`
exposes the same records on `ResolveResult.provenance`, and `readFlags()`
through its `onSources` receiver. `ResolutionProvenance` describes one input's
winning stage; use `ResolutionProvenanceRecord` for the complete
`ResolveResult.provenance` object. See
[Which source won](/guide/semantics#which-source-won).
- **Typed entry values on both surfaces**: `arg.keyValue(arg.number())` mirrors
`flag.keyValue(flag.number())`, so `mycli scale web=3` resolves to
`{ web: 3 }` with the element's constraints, path checks, and validators
applied to every entry. See [Arguments](/guide/arguments).
- **Stdin in help**: an input that declares `.stdin()` renders `[stdin]` beside
`[env: X]` and `[config: y]`, or `[stdin: '-']` and `[stdin: when omitted]`
for the narrower triggers. See [Help](/guide/help).
- **Consumer-owned built-in flags**: `.builtins({ help: 'off' })`,
`.builtins({ json: 'off' })`, or `.builtins({ quiet: 'off' })` hands a
root-owned token to the commands, for a CLI whose `--json`, `-q`, or
`--help` means something of its own. See
[Taking a built-in over](/guide/output#taking-a-built-in-over).
See the [CHANGELOG](https://github.com/kjanat/dreamcli/blob/master/CHANGELOG.md)
for the complete record.
---
# Walkthrough: Building a GitHub CLI
Source: https://dreamcli.kjanat.dev/raw/guide/walkthrough
# Walkthrough: Building a GitHub CLI
Let's build something real.
We're going to recreate a miniature version of `gh` — GitHub's official
CLI — using dreamcli.
By the end, you'll have touched every major feature: commands, groups, flags,
arguments, derive, env vars, per-flag prompts, command-level interactive prompts,
tables, JSON mode, spinners, and error handling.
The full source lives in [`examples/gh/src/main.ts`][gh-main] with supporting
modules under [`examples/gh/src/`][gh-src].
Inside this repo, run it with `bun --cwd=examples/gh src/main.ts ...`.
In a standalone project, you'd install `@kjanat/dreamcli` normally — the Bun workspace wiring here is just repo-local convenience.
## What we're building
```bash
$ gh pr list
# TITLE STATE AUTHOR
142 Add dark mode toggle open alice
141 Fix OAuth redirect loop open bob
137 Fix date parsing Safari open dave
$ gh pr list --state all --json
[{"#":142,"title":"Add dark mode toggle",...},...]
$ gh issue triage 89 --decision backlog
? Which labels still fit? bug, ui
#89 Login fails on Firefox
Status: open (backlog)
Labels: bug, ui
$ gh auth login
? Paste your GitHub token: ghp_abc123...
Logged in with token ghp_abc1...3def
```
Commands, flags, prompts, spinners, tables, JSON mode — all in one tool.
Let's build it piece by piece.
## Step 1: A single command
Start with the simplest useful thing — listing pull requests:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const prList = command('list')
.description('List pull requests')
.action(({ out }) => {
out.log('PR #142: Add dark mode toggle');
out.log('PR #141: Fix OAuth redirect loop');
});
cli('gh').command(prList).run();
```
That's a working CLI. But it's hardcoded and flat. Let's fix both.
## Step 2: Data and flags
Real CLIs filter things.
Let's add mock data and flags to filter by state:
```ts twoslash include walkthrough-pr-setup
import { cli, command, flag } from '@kjanat/dreamcli';
type PR = {
readonly number: number;
readonly title: string;
readonly state: 'open' | 'closed' | 'merged';
readonly author: string;
};
const pullRequests: readonly PR[] = [
{
number: 142,
title: 'Add dark mode toggle',
state: 'open',
author: 'alice',
},
{
number: 141,
title: 'Fix OAuth redirect loop',
state: 'open',
author: 'bob',
},
{
number: 140,
title: 'Bump dependencies',
state: 'merged',
author: 'dependabot',
},
{
number: 139,
title: 'Add rate limiting',
state: 'closed',
author: 'carol',
},
];
const prList = command('list')
.description('List pull requests')
.flag(
'state',
flag
.enum(['open', 'closed', 'merged', 'all'])
.default('open')
.alias('s')
.describe('Filter by state'),
)
.flag(
'limit',
flag
.number()
.default(10)
.alias('L')
.describe('Maximum number of results'),
)
.action(({ flags, out }) => {
let results = [...pullRequests];
if (flags.state !== 'all') {
results = results.filter(
(p) => p.state === flags.state,
);
}
results = results.slice(0, flags.limit);
for (const p of results) {
out.log(`#${p.number} ${p.title} (${p.state})`);
}
});
```
Three things to notice:
1. `flag.enum([...])` constrains the value — `flags.state` is `'open' | 'closed' | 'merged' | 'all'`,
not `string`.\
Try passing `--state bogus` and dreamcli rejects it with a "did you mean?" error.
2. `.default('open')` means `flags.state` is always defined — no `undefined` to check.
3. `flag.number()` parses `--limit 5` into the number `5`, not the string `"5"`.
```bash
$ gh pr list
#142 Add dark mode toggle (open)
#141 Fix OAuth redirect loop (open)
$ gh pr list --state all --limit 2
#142 Add dark mode toggle (open)
#141 Fix OAuth redirect loop (open)
```
## Step 3: Tables and JSON mode
Printing lines is fine, but tabular data deserves a table. And scripts need JSON.
Replace the `for` loop with `out.table()`:
```ts twoslash
// Reuses the Step 2 PR setup.
// ---cut-start---
// @include: walkthrough-pr-setup
// ---cut-end---
prList.action(({ flags, out }) => {
let results = [...pullRequests];
if (flags.state !== 'all') {
results = results.filter(
(p) => p.state === flags.state,
);
}
results = results.slice(0, flags.limit);
// out.table() renders a formatted table in TTY, JSON array in --json mode
out.table(
results.map((p) => ({
'#': p.number,
title: p.title,
state: p.state,
author: p.author,
})),
);
});
```
Now you get both:
```bash
$ gh pr list
# TITLE STATE AUTHOR
142 Add dark mode toggle open alice
141 Fix OAuth redirect loop open bob
$ gh pr list --json
[{"#":142,"title":"Add dark mode toggle","state":"open","author":"alice"},...]
```
`--json` is handled automatically by `cli()`. `out.table()` renders a formatted table for humans and emits JSON when `--json` is active.
`out.log()` is suppressed in JSON mode.
For single-object responses (like `pr view`), branch on `out.jsonMode`: emit `out.json(data)` in machine mode, or human text otherwise. Don't mix both surfaces in the same response.
## Step 4: Command groups
A flat list of commands doesn't scale.
`gh` organizes commands into groups — `pr list`, `issue triage`, `auth login`. dreamcli has `group()` for this:
```ts twoslash
import {
cli,
command,
group,
flag,
} from '@kjanat/dreamcli';
// Auth commands
const authLogin = command('login')
.description('Authenticate with GitHub')
.action(({ out }) => {
out.log('Logging in...');
});
const authStatus = command('status')
.description('Show authentication status')
.action(({ out }) => {
out.log('Logged in');
});
// PR commands
const prList = command('list').description(
'List pull requests',
);
// ...flags and action from above...
// Issue commands
const issueList =
command('list').description('List issues');
const issueTriage = command('triage').description(
'Triage an issue',
);
// Groups
const auth = group('auth')
.description('Manage authentication')
.command(authLogin)
.command(authStatus);
const pr = group('pr')
.description('Manage pull requests')
.command(prList);
const issue = group('issue')
.description('Manage issues')
.command(issueList)
.command(issueTriage);
// Assemble
cli('gh')
.version('0.1.0')
.description('A minimal GitHub CLI clone')
.command(auth)
.command(pr)
.command(issue)
.run();
```
```bash
$ gh --help
A minimal GitHub CLI clone
Commands:
auth Manage authentication
pr Manage pull requests
issue Manage issues
$ gh pr --help
Manage pull requests
Commands:
list List pull requests
```
Groups are just commands that contain other commands.
You can nest them as deep as you want.
## Step 5: Arguments
`gh pr view 142` takes a PR number as a positional argument — not a flag, not a named value, just the first thing after `view`:
```ts twoslash
import { arg, CLIError } from '@kjanat/dreamcli';
// Reuses the Step 2 command imports, PR type, and pullRequests mock data.
// ---cut-start---
// @include: walkthrough-pr-setup
// ---cut-end---
const prView = command('view')
.description('View a pull request')
.arg('number', arg.number().describe('PR number'))
.action(({ args, out }) => {
const pr = pullRequests.find(
(p) => p.number === args.number,
);
if (!pr) {
throw new CLIError(
`Pull request #${args.number} not found`,
{
code: 'NOT_FOUND',
exitCode: 1,
suggest: 'Try: gh pr list',
},
);
}
if (out.jsonMode) {
out.json(pr);
return;
}
out.log(`#${pr.number} ${pr.title}`);
out.log(`State: ${pr.state} Author: ${pr.author}`);
});
```
`arg.number()` coerces the shell string to a `number` automatically — `args.number` is typed and validated as numeric at parse time. If someone passes `abc`, they get a parse error before the action ever runs.
The `CLIError` with `suggest` gives the user a helpful nudge when things
go wrong, and in `--json` mode it serializes as structured JSON on stdout so scripts and pipes receive parseable output.
Single-object commands should pick one surface per run: human text by default, JSON when `--json` is active.
## Step 6: Derive Context
Every `pr` and `issue` command needs authentication.
You _could_ check for a token in every single action handler, but that's repetitive and error-prone.
`derive()` solves this cleanly:
```ts twoslash include walkthrough-require-auth
import { CLIError } from '@kjanat/dreamcli';
function requireAuth(token: string | undefined): {
token: string;
} {
const normalizedToken = token?.trim();
if (!normalizedToken) {
throw new CLIError('Authentication required', {
code: 'AUTH_REQUIRED',
suggest: 'Run `gh auth login` or set GH_TOKEN',
exitCode: 1,
});
}
return { token: normalizedToken };
}
```
This assumes each protected command resolves a `token` value first, typically via `flag.string().env('GH_TOKEN')`, so derive can consume resolved input instead of reaching for `process.env` directly.
`derive()` is command-scoped and gets typed resolved flags and args.
Returning `{ token }` merges that value into `ctx` downstream.
Now wire it up:
::: code-group
```ts twoslash [derive()]
import { command, flag } from '@kjanat/dreamcli';
// ---cut-start---
// @include: walkthrough-require-auth
// ---cut-end---
const prList = command('list')
.description('List pull requests')
.flag(
'token',
flag.string().env('GH_TOKEN').describe('GitHub token'),
)
.derive(({ flags }) => requireAuth(flags.token))
.flag(
'state',
flag
.enum(['open', 'closed', 'merged', 'all'])
.default('open'),
)
.action(({ ctx, out }) => {
// ctx.token is typed as `string` — guaranteed by derive
out.info(
`Authenticated with ${ctx.token.slice(0, 8)}...`,
);
});
```
```ts twoslash [middleware()]
import {
CLIError,
command,
flag,
middleware,
} from '@kjanat/dreamcli';
const requireAuth = middleware<{ token: string }>(
({ flags, next }) => {
if (typeof flags.token !== 'string') {
throw new CLIError('Authentication required', {
code: 'AUTH_REQUIRED',
suggest: 'Run `gh auth login` or set GH_TOKEN',
exitCode: 1,
});
}
return next({ token: flags.token });
},
);
const prList = command('list')
.description('List pull requests')
.flag(
'token',
flag.string().env('GH_TOKEN').describe('GitHub token'),
)
.middleware(requireAuth)
.flag(
'state',
flag
.enum(['open', 'closed', 'merged', 'all'])
.default('open'),
)
.action(({ ctx, out }) => {
out.info(
`Authenticated with ${ctx.token.slice(0, 8)}...`,
);
});
```
:::
If no token resolves, the derive handler throws before the action runs.
No token check needed in the handler.
The auth commands (`login`, `status`) don't use derive, so they work without a token.
Technically you could also do this with middleware, but it has to narrow `flags.token` itself
because middleware is reusable and command-agnostic.
Use `derive()` when you need typed resolved input.
Use `middleware()` when you need to wrap downstream execution for timing, logging, retries, cleanup, or error boundaries.
## Step 7: Env vars and prompts
The real `gh auth login` lets you paste a token interactively or set `GH_TOKEN` in your environment.
dreamcli's resolution chain handles this naturally:
```ts twoslash include walkthrough-auth-helpers
import { command, flag } from '@kjanat/dreamcli';
// ---cut-start---
// @include: walkthrough-require-auth
// ---cut-end---
const tokenFlag = (description = 'GitHub token') =>
flag
.string()
.env('GH_TOKEN')
.describe(description)
.prompt({
kind: 'input',
message: 'Paste your GitHub token:',
})
.required();
const authedCommand = (name: string) =>
command(name)
.flag('token', tokenFlag())
.derive(({ flags }) => requireAuth(flags.token));
const authLogin = command('login')
.description('Authenticate with GitHub')
.flag('token', tokenFlag('Authentication token'))
.action(({ flags, out }) => {
const display = `${flags.token.slice(0, 8)}...${flags.token.slice(-4)}`;
out.log(`Logged in with token ${display}`);
});
```
The resolution chain tries each source in order:
1. `--token ghp_abc` (explicit flag) — highest priority
2. `GH_TOKEN=ghp_abc` (env var via `.env()`)
3. Interactive prompt (via `.prompt()`) — only in TTY
4. If none: error (no default, no way to resolve)
So all of these work:
```bash
$ gh auth login --token ghp_abc123 # flag
$ GH_TOKEN=ghp_abc123 gh auth login # env var
$ gh auth login # prompts interactively
? Paste your GitHub token: ghp_abc123...
```
One flag definition. Three ways to provide the value. The user picks what's convenient.
That same `tokenFlag()` helper also powers `auth status` and every protected command, so the example sticks to one input story all the way through.
## Step 8: Guided workflows
Per-flag prompts are great when every command always asks the same question.
`issue triage` needs a different follow-up depending on the primary decision:
- `--decision backlog` should ask which labels still fit
- `--decision close` should ask whether to post a follow-up comment
That's what `.interactive()` is for:
```ts twoslash
import { arg } from '@kjanat/dreamcli';
// ---cut-start---
// @include: walkthrough-auth-helpers
// ---cut-end---
const issueLabelChoices = [
{ value: 'bug' },
{ value: 'ui' },
{ value: 'needs-info' },
] as const;
const issueTriage = authedCommand('triage')
.description('Triage an issue with guided prompts')
.arg('number', arg.number().describe('Issue number'))
.flag(
'decision',
flag
.enum(['backlog', 'close'])
.required()
.describe('How to handle the issue'),
)
.flag(
'label',
flag
.array(flag.string())
.describe(
'Labels to keep when leaving the issue open',
),
)
.flag(
'comment',
flag.boolean().describe('Post a follow-up comment'),
)
.interactive(({ flags }) => {
const labels = flags.label ?? [];
return {
label: flags.decision === 'backlog' &&
labels.length === 0 && {
kind: 'multiselect',
message: 'Which labels still fit?',
choices: issueLabelChoices,
},
comment: flags.decision === 'close' &&
!flags.comment && {
kind: 'confirm',
message: 'Post a follow-up comment?',
},
};
});
```
```bash
$ gh issue triage 89 --decision backlog
? Which labels still fit? bug, ui
#89 Login fails on Firefox
Status: open (backlog)
Labels: bug, ui
$ gh issue triage 89 --decision close
? Post a follow-up comment? yes
#89 Login fails on Firefox
Status: closed
```
The key difference from `.prompt()` is timing.
`.interactive()` runs after CLI/env/config resolution and only decides which prompts to show for the still-missing flags.
Use `.prompt()` for unconditional fallback input.
Use `.interactive()` when the set of prompts itself depends on earlier resolved flags.
That's also why `issue` stays smaller than `pr`: `pr` teaches the core API-shaped commands, and `issue triage` teaches the guided-workflow pattern without repeating `view` and `create` all over again.
## Step 9: Spinners
Creating a PR involves an API call. In a real terminal, you'd show a spinner:
```ts twoslash
// ---cut-start---
// @include: walkthrough-auth-helpers
// ---cut-end---
const prCreate = authedCommand('create')
.description('Create a pull request')
.flag(
'title',
flag
.string()
.alias('t')
.describe('PR title')
.prompt({ kind: 'input', message: 'Title:' })
.required(),
)
.flag(
'body',
flag
.string()
.alias('b')
.describe('PR body')
.prompt({ kind: 'input', message: 'Body:' })
.required(),
)
.flag(
'draft',
flag
.boolean()
.alias('d')
.default(false)
.describe('Create as draft'),
)
.action(async ({ flags, out }) => {
const spinner = out.spinner('Creating pull request...');
// Simulate API call
await new Promise((r) => setTimeout(r, 1500));
spinner.succeed('Pull request created');
const createdPr = {
number: 143,
title: flags.title,
url: 'https://github.com/you/repo/pull/143',
};
if (out.jsonMode) {
out.json(createdPr);
return;
}
out.log(`#${createdPr.number} ${createdPr.title}`);
out.log(createdPr.url);
});
```
`out.spinner()` returns a handle with `.update()`, `.succeed()`, `.stop()`, and `.wrap()`.
In a TTY, you get an animated spinner.
When piped or in `--json` mode, spinners are suppressed automatically — no garbage escape codes in your logs.
## Step 10: Testing
This is where it gets interesting.
You don't want to spawn subprocesses to test a CLI. dreamcli's testkit lets you run commands in-process with full control:
```ts twoslash
import { runCommand } from '@kjanat/dreamcli/testkit';
// ---cut-start---
import { prList } from './examples/gh/src/docs.ts';
// ---cut-end---
// Test that pr list returns open PRs by default
const result = await runCommand(prList, [
'--state',
'open',
]);
expect(result.exitCode).toBe(0);
expect(result.stdout.join('')).toContain('dark mode');
```
You can inject env vars, config, and prompt answers:
```ts twoslash
import { runCommand } from '@kjanat/dreamcli/testkit';
// ---cut-start---
import {
issueTriage,
prList,
} from './examples/gh/src/docs.ts';
// ---cut-end---
// Test that derive blocks unauthenticated access
const noAuth = await runCommand(prList, []);
expect(noAuth.exitCode).toBe(1);
expect(noAuth.stderr.join('')).toContain(
'Authentication required',
);
// Test with a token
const withAuth = await runCommand(prList, [], {
env: { GH_TOKEN: 'ghp_test_token' },
});
expect(withAuth.exitCode).toBe(0);
// Test guided prompts
const triage = await runCommand(
issueTriage,
['89', '--decision', 'backlog'],
{
env: { GH_TOKEN: 'ghp_test_token' },
answers: [['bug', 'ui']],
},
);
expect(triage.exitCode).toBe(0);
expect(triage.stdout.join('')).toContain('Labels: bug, ui');
```
No subprocesses. No `process.argv` mutation. No shell scripts.
Each test is isolated — inject what you need, assert what you expect.
## Putting it together
Here's the final assembly — all the commands wired into groups:
```ts twoslash
import {
cli,
command,
group,
flag,
arg,
CLIError,
} from '@kjanat/dreamcli';
// ---cut-start---
import {
authLogin,
authStatus,
issueList,
issueTriage,
prCreate,
prList,
prView,
} from './examples/gh/src/docs.ts';
// ---cut-end---
const auth = group('auth')
.description('Manage authentication')
.command(authLogin)
.command(authStatus);
const pr = group('pr')
.description('Manage pull requests')
.command(prList)
.command(prView)
.command(prCreate);
const issue = group('issue')
.description('Manage issues')
.command(issueList)
.command(issueTriage);
cli('gh')
.version('0.1.0')
.description('A minimal GitHub CLI clone')
.command(auth)
.command(pr)
.command(issue)
.run();
```
That's a CLI with:
- 7 commands across 3 groups
- Enum, string, number, and boolean flags
- Array flags with multiselect prompts
- Positional arguments
- Auth derive with typed context
- Env var resolution (`GH_TOKEN`)
- Interactive prompts with resolution chain fallback
- Command-level interactive resolver for guided follow-up questions
- Table output with automatic JSON mode
- Spinners with TTY-aware suppression
- Structured errors with suggestions and error codes
- Full testability via in-process test harness
The complete source lives in [`examples/gh/src/main.ts`][gh-main] with the rest
of the example package under [`examples/gh/src/`][gh-src].
## What's next?
- [Commands](/guide/commands) — everything about command builders, nesting, and groups
- [Flags](/guide/flags) — all flag types, modifiers, and the resolution chain in detail
- [Middleware](/guide/middleware) — context accumulation, short-circuit, onion model
- [Testing](/guide/testing) — the full testkit API
[gh-main]: https://github.com/kjanat/dreamcli/blob/master/examples/gh/src/main.ts
[gh-src]: https://github.com/kjanat/dreamcli/tree/master/examples/gh/src
---
# Why dreamcli
Source: https://dreamcli.kjanat.dev/raw/guide/why
# Why dreamcli
Most TypeScript CLI frameworks treat the type system like decoration.
You define flags in one place, then use parsed values somewhere else as a loosely typed blob.
Env vars, config files, and interactive prompts live in separate universes.
Testing means hacking `process.argv`.
dreamcli collapses all of that into a single typed schema.
## Comparison
Approximate comparison of first-party, built-in support as documented by each project.
Third-party plugins and custom glue can extend the other libraries.
| Capability | Commander | Yargs | Citty | CAC | Cleye | dreamcli |
| ------------------------------------------ | ------------------- | ---------------------- | --------------- | ------------- | ------------- | ------------------------------------- |
| Type inference from definition | Manual `.opts()` | Good | Good | Basic | Good | Full — flags, args, context |
| Built-in value sources | CLI, defaults, env | CLI, env, config | CLI, defaults | CLI, defaults | CLI, defaults | CLI, env, config, prompt, default |
| Schema-driven prompts | No | No | No | No | No | Integrated |
| Middleware / hooks | Lifecycle hooks | Middleware | Plugins / hooks | Events | No | Yes — typed middleware |
| Built-in test harness with output capture | No | No | No | No | No | `runCommand()` + capture |
| Shell completions from command definitions | No | Built-in (bash/zsh) | No | No | No | Built-in (bash/zsh) |
| Structured output primitives | DIY | DIY | DIY | DIY | DIY | Built-in (`--json`, tables, spinners) |
| Config file support | DIY | Built-in (`.config()`) | No | No | No | Built-in (XDG discovery, JSON) |
## The Core Insight
The closest analog is what tRPC did to API routes — the individual pieces existed.
The insight was wiring them so types flow end-to-end.
One flag declaration:
```ts twoslash
import { flag } from '@kjanat/dreamcli';
flag
.enum(['us', 'eu', 'ap'])
.alias('r')
.env('DEPLOY_REGION')
.config('deploy.region')
.prompt({ kind: 'select', message: 'Which region?' })
.default('us');
```
This single chain configures:
- **Parsing** — accepts `--region us` or `-r eu`
- **Type inference** — `flags.region` is `"us" | "eu" | "ap"` in the handler
- **Resolution** — CLI → env var → config file → interactive prompt → default
- **Help text** — flag appears with description, alias, default, and choices
- **Shell completions** — tab-complete suggests `us`, `eu`, `ap`
- **Testing** — override via `env`, `config`, or `answers` in test harness
## Design Principles
1. **Schema is the law.** Everything derives from it: parsing, types, resolution, help, completions.
2. **Progressive disclosure.** A 5-line script stays 5 lines. Complexity scales by composition.
3. **Deterministic behavior.** Resolution rules are explicit and consistent.
4. **Portable by default.** Core avoids runtime-specific APIs; adapters handle the edges.
5. **Ergonomic, not clever.** Predictable DX over type-theory stunts.
## Related Pages
- [Architecture Rationale](/guide/rationale)
- [Limitations And Workarounds](/guide/limitations)
- [Migration And Adoption](/guide/migration)
- [Troubleshooting](/guide/troubleshooting)
- [CLI Semantics](/guide/semantics)
---
# API Reference
Source: https://dreamcli.kjanat.dev/raw/reference/api
# API Reference
This page is rebuilt at docs build time from the public entrypoints declared in `package.json`.
Use it to see the complete public surface grouped by subpath and export kind, then jump into the
subpath-specific detail pages.
The reference surface is source-backed: symbol routes and example links are derived from `src/**`
and `examples/**` during docs build.
- Public entrypoints: `{{ data.publicEntrypointCount }}`
- Public symbols indexed: `{{ data.publicSymbolCount }}`
- Symbol pages rendered: `{{ data.symbolPageCount }}`
## Choose an Import
| Import | Use it for | Start here |
| ------------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `@kjanat/dreamcli` | schema builders, CLI assembly, parsing, resolution, errors, help | [`@kjanat/dreamcli`](/reference/main) |
| `@kjanat/dreamcli/testkit` | command tests, output capture, scripted prompts, test adapters | [`@kjanat/dreamcli/testkit`](/reference/testkit) |
| `@kjanat/dreamcli/runtime` | runtime detection, explicit adapters, runtime-only helpers | [`@kjanat/dreamcli/runtime`](/reference/runtime) |
| `@kjanat/dreamcli/completion` | shell completion script generators | [`@kjanat/dreamcli/completion`](/reference/completion) |
| `@kjanat/dreamcli/json-schema` | definition documents, input JSON Schema, the meta-schema value | [`@kjanat/dreamcli/json-schema`](/reference/json-schema) |
| `@kjanat/dreamcli/config` | config file and manifest discovery, custom config formats | [`@kjanat/dreamcli/config`](/reference/config) |
| `@kjanat/dreamcli/prompt` | the terminal prompt engine and prompt config resolution | [`@kjanat/dreamcli/prompt`](/reference/prompt) |
| `@kjanat/dreamcli/schema` | generated CLI definition meta-schema (export mapping differs by package target) | [`@kjanat/dreamcli/schema`](/reference/schema) |
| `@kjanat/dreamcli/version` | the framework's own build version and revision | [`@kjanat/dreamcli/version`](/reference/version) |
## Generated Index
## Related Guides
- [CLI Semantics](/guide/semantics) — exact parser, resolver, and root-surface behavior
- [Examples](/examples/) — source-backed examples with Twoslash hover and related symbol links
- [Planner Contract](/reference/planner-contract) — internal dispatch outcomes and matched-command handoff
- [Resolver Contract](/reference/resolver-contract) — internal precedence, diagnostics, and resolved-value handoff
- [Output Contract](/reference/output-contract) — internal routing, activity mode, and cleanup policy facts
- [Testing Commands](/guide/testing) — higher-level testkit usage patterns
- [Runtime Support](/guide/runtime) — runtime packages and adapter expectations
- [Changelog](/reference/changelog) — release history mirrored inside the docs site
- [Support Matrix](/reference/support-matrix) — audited support status, evidence, and deferred work
## Detailed Reference
- [`@kjanat/dreamcli`](/reference/main) — main package API
- [`@kjanat/dreamcli/testkit`](/reference/testkit) — testing utilities
- [`@kjanat/dreamcli/runtime`](/reference/runtime) — runtime adapters
- [`@kjanat/dreamcli/completion`](/reference/completion) — completion script generators
- [`@kjanat/dreamcli/json-schema`](/reference/json-schema) — definition and input schema generation
- [`@kjanat/dreamcli/config`](/reference/config) — config and manifest discovery
- [`@kjanat/dreamcli/prompt`](/reference/prompt) — prompt engines
- [`@kjanat/dreamcli/schema`](/reference/schema) — the definition meta-schema document
- [`@kjanat/dreamcli/version`](/reference/version) — framework build constants
---
# Changelog
Source: https://dreamcli.kjanat.dev/raw/reference/changelog
# Changelog
This page surfaces the repository changelog directly inside the docs site.
- Source of truth: `CHANGELOG.md`
- Related pages: [API Reference](/reference/api), [Semantic Delta Log](/reference/semantic-delta-log), [Examples](/examples/)
---
# @kjanat/dreamcli/completion
Source: https://dreamcli.kjanat.dev/raw/reference/completion
# @kjanat/dreamcli/completion
Shell completion script generation. `cli().completions()` loads this module on demand; import it
directly to write scripts yourself or to drive completion from custom tooling.
```ts twoslash
import {
generateCompletion,
generateBashCompletion,
generateZshCompletion,
generateFishCompletion,
generatePowerShellCompletion,
SHELLS,
} from '@kjanat/dreamcli/completion';
import type { CompletionOptions, Shell } from '@kjanat/dreamcli/completion';
```
`Shell`, `CompletionOptions`, and `SHELLS` are also exported from the root entry. Importing
`SHELLS` from this subpath also evaluates the generators, so take the root copy when the shell
list is all you need.
`CompletionOptions` is the generator half of what `.completions()` accepts; the builder's own
`CompletionRegistrationOptions` adds `as: 'command' | 'flag'`, which the generators have no use
for.
## `generateCompletion(schema, shell, options?)`
Generate a shell completion script from a command schema.
- `shell`: `'bash'` | `'zsh'` | `'fish'` | `'powershell'`
- `options.functionPrefix?`: override the generated helper function prefix
- `options.rootMode?`: `'subcommands'` | `'surface'`
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
import { generateCompletion } from '@kjanat/dreamcli/completion';
const myCli = cli('mycli').command(command('deploy'));
generateCompletion(myCli.schema, 'zsh');
```
## Per-shell generators
`generateBashCompletion`, `generateZshCompletion`, `generateFishCompletion`, and
`generatePowerShellCompletion` take the same `(schema, options?)` arguments and back
`generateCompletion()`.
## `SHELLS`
The frozen tuple `['bash', 'zsh', 'fish', 'powershell']`, matching the `Shell` union.
---
# @kjanat/dreamcli/config
Source: https://dreamcli.kjanat.dev/raw/reference/config
# @kjanat/dreamcli/config
Config file and package manifest discovery. `cli().run()` loads discovery code on demand when
config or manifest discovery runs. Preloaded `.manifest(data)` keeps discovery cold.
Import this module directly for custom bootstrapping or to register config formats.
```ts twoslash
import {
buildConfigSearchPaths,
configFormat,
discoverConfig,
discoverManifest,
inferCliName,
packageRepositoryUrl,
} from '@kjanat/dreamcli/config';
import type {
ConfigDiscoveryResult,
ConfigSearchPathOptions,
FormatLoader,
PackageJsonData,
} from '@kjanat/dreamcli/config';
```
The option and result types are also exported from the root entry.
## `buildConfigSearchPaths(appName, options)`
Build the default search-path list dreamcli uses for config discovery. This is mainly useful for
debugging, custom bootstrapping, or help text that wants to show the exact probed paths. The
`ConfigSearchPathOptions` argument carries the scope directories: `baseDir` (project ancestor walk
starts here), `userConfigDirs`, `systemConfigDirs`, and `loaders`.
```ts twoslash
import { buildConfigSearchPaths } from '@kjanat/dreamcli/config';
const paths = buildConfigSearchPaths('mycli', {
baseDir: process.cwd(),
userConfigDirs: ['/home/me/.config'],
systemConfigDirs: ['/etc'],
});
```
## `configFormat(extensions, parseFn)`
Create a config format loader from a list of file extensions and a parse function. Pass the result
to `.configLoader(...)` or `discoverConfig(...)` to add YAML, TOML, or other formats on top of the
built-in JSON loader.
```ts twoslash
import { configFormat } from '@kjanat/dreamcli/config';
declare const parseYaml: (s: string) => unknown;
declare const parseTOML: (s: string) => unknown;
// ---cut---
configFormat(['yaml', 'yml'], parseYaml);
configFormat(['toml'], parseTOML);
```
## `discoverConfig(appName, adapter, options?)`
Low-level config discovery helper behind `cli(...).config(...)`. It searches standard paths, reads
the first matching file via the provided adapter, and returns either `{ found: true, ... }` with
parsed config data or `{ found: false }` when no config file exists.
```ts twoslash
import {
configFormat,
discoverConfig,
} from '@kjanat/dreamcli/config';
import { createTestAdapter } from '@kjanat/dreamcli/testkit';
declare const parseYaml: (s: string) => unknown;
declare const parseTOML: (s: string) => unknown;
const adapter = createTestAdapter();
// ---cut---
const result = await discoverConfig('mycli', adapter, {
loaders: [
configFormat(['yaml', 'yml'], parseYaml),
configFormat(['toml'], parseTOML),
],
});
```
## `discoverManifest(adapter, options?)`
Walk up from `options.startDir` (or `adapter.cwd` when omitted) and return the nearest parsed manifest
metadata, or `null` when none is found. This is the helper used by `.manifest()` during `.run()`.
`options.files` chooses the candidate filenames in per-directory priority order (default
`['package.json']`); pass `['deno.json', 'deno.jsonc', 'jsr.json']` for Deno / JSR projects. Files are parsed as
JSON with a JSONC fallback (comments and trailing commas are tolerated), and a config-only manifest
with no recognised metadata is skipped so the walk-up continues.
Pass an absolute filesystem path inside your own package as `startDir` (e.g.
`fileURLToPath(import.meta.url)`) when authoring an installable CLI whose version should reflect its
OWN package rather than the consumer's working directory. Unlike `.manifest({ from })`, which also
accepts `import.meta`, a `file:` URL string, or a `URL` instance and normalizes it, this helper takes a
plain path string, so convert URLs yourself first.
```ts twoslash
import { discoverManifest } from '@kjanat/dreamcli/config';
import { createTestAdapter } from '@kjanat/dreamcli/testkit';
import { fileURLToPath } from 'node:url';
const adapter = createTestAdapter();
// Default: walk up from adapter.cwd looking for package.json
const pkg = await discoverManifest(adapter);
if (pkg !== null) {
console.log(pkg.version);
}
// Deno / JSR: anchored to the CLI's own module, deno.json then jsr.json
const own = await discoverManifest(adapter, {
startDir: fileURLToPath(import.meta.url),
files: ['deno.json', 'deno.jsonc', 'jsr.json'],
});
```
> `discoverPackageJson(adapter, startDir?)` was **removed in v4**. Use
> `discoverManifest(adapter, { startDir, files: ['package.json'] })`.
## `inferCliName(pkg, options?)`
Infer a CLI display name from manifest metadata. It prefers the first key from a `bin` object and
otherwise falls back to the `name` field. By default a leading `@scope/` is stripped; pass
`{ stripScope: false }` to keep the full scoped name (`bin` keys are never scoped, so the option only
affects the `name` fallback — relevant for `deno.json` / `jsr.json`, which have no `bin`).
```ts twoslash
import { inferCliName } from '@kjanat/dreamcli/config';
inferCliName({ bin: { mycli: './dist/cli.js' } }); // 'mycli'
inferCliName({ name: '@scope/mycli' }); // 'mycli'
inferCliName(
{ name: '@scope/mycli' },
{ stripScope: false },
); // '@scope/mycli'
```
## `packageRepositoryUrl(pkg, options?)`
Resolve a manifest's `repository` field to a browsable `https://` URL. Handles the locator formats
npm accepts — the `{ type, url }` object form, `git+`-prefixed and `.git`-suffixed URLs, scp-style
locators (`git@host:u/r.git`), and the `github:`/`gitlab:`/`bitbucket:`/bare `u/r` shorthands.
Returns `undefined` when the field is absent or unrecognised. Used by `.links()` to derive the
header name link.
With `{ require: true }` the return type narrows to `string`, and an absent or unrecognised
locator throws a `CLIError` with code `INVALID_REPOSITORY` instead of returning `undefined`.
```ts twoslash
import { packageRepositoryUrl } from '@kjanat/dreamcli/config';
packageRepositoryUrl({
repository: 'git+https://github.com/me/mycli.git',
});
// 'https://github.com/me/mycli'
packageRepositoryUrl({ repository: 'github:me/mycli' });
// 'https://github.com/me/mycli'
const url: string = packageRepositoryUrl(
{ repository: 'github:me/mycli' },
{ require: true },
);
```
This subpath statically includes config and manifest discovery, so code that only needs
`packageRepositoryUrl` for a manifest it already holds pays for both.
---
# Example Hover
Source: https://dreamcli.kjanat.dev/raw/reference/example-hover-prototype
# Example Hover
DreamCLI now ships Twoslash-backed hover on generated example pages.
- Build path: VitePress `codeTransformers` + `@shikijs/vitepress-twoslash`
- Runtime UI: default Twoslash tooltip layer
- Rollout scope: generated example source blocks only
## Why This Scope Exists
The re-foundation wanted IDE-like hover, but only where it earns its keep.
Examples were the right first target because they are already source-backed and easier to keep calm than prose-heavy guide pages.
## What Ships Now
- Every generated example page now renders its source block with `ts twoslash` during the normal docs build.
- Example imports resolve against the local `dreamcli` source tree instead of a published package.
- The default hover UI is sufficient; no custom tooltip runtime is needed.
## Current Constraints
- Hover stays limited to the generated TypeScript source block on each example page.
- Example pages still lead with summary, usage, and links so readability does not depend on hover.
- Guide pages, shell transcripts, and partial snippets remain non-interactive.
## Origin
This page started as the prototype note for `prototype-hover-path`. The follow-up task `enable-example-hover` moved the same Twoslash path onto the real generated example routes.
## Related Pages
- [Examples](/examples/)
- [API Reference](/reference/api)
- [Support Matrix](/reference/support-matrix)
---
# @kjanat/dreamcli/json-schema
Source: https://dreamcli.kjanat.dev/raw/reference/json-schema
# @kjanat/dreamcli/json-schema
Definition documents and input JSON Schema generation. `--help --json` loads this module on demand;
import it directly to emit documents from build scripts and tooling.
```ts twoslash
import {
generateSchema,
generateCommandSchema,
generateInputSchema,
definitionMetaSchema,
DEFINITION_SCHEMA_URL,
DEFINITION_SCHEMA_VERSION,
} from '@kjanat/dreamcli/json-schema';
import type { DefinitionDocument, InputSchemaDocument } from '@kjanat/dreamcli/json-schema';
```
The document types (`DefinitionDocument`, `CommandDefinitionDocument`, every `*FragmentV1`, and
`InputSchemaDocument`) are also exported from the root entry, so code that only names the shapes
does not need this subpath. The same goes for `DEFINITION_SCHEMA_URL` and
`DEFINITION_SCHEMA_VERSION`: importing them from this subpath also evaluates the generators, so
take the root copies when the constants are all you need.
## `generateSchema(schema, options?, meta?)`
Generate a definition metadata document describing the CLI's structure.
- `schema`: `CLISchema` from `cli.schema`
- `options.includeHidden?`: include hidden commands (default: `true`)
- `options.includePrompts?`: include prompt config on flags (default: `true`)
- `meta?`: `ExampleMeta` (`{ name, version }`) used to resolve function-form examples; defaults to
the schema's own name and version
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
import { generateSchema } from '@kjanat/dreamcli/json-schema';
const myCli = cli('mycli').command(command('deploy'));
const definition = generateSchema(myCli.schema);
```
## `generateInputSchema(schema, options?)`
Generate a JSON Schema (draft 2020-12) for validating CLI input as JSON.
- `schema`: `CLISchema` or `CommandSchema`
- `options.includeHidden?`: include hidden commands (default: `true`)
Accepts a full `CLISchema` (discriminated union across commands) or a
single `CommandSchema` (flat object schema).
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
import { generateInputSchema } from '@kjanat/dreamcli/json-schema';
const myCli = cli('mycli').command(command('deploy'));
const inputSchema = generateInputSchema(myCli.schema);
```
## `generateCommandSchema(schema, options?, meta?)`
Generate a standalone definition document for one `CommandSchema`. `--help --json` on a command
serializes through it. `options` and `meta` match `generateSchema()`; `meta` defaults to the
command name with no version.
## `definitionMetaSchema`
The definition meta-schema as a value, identical to the bytes of
[`@kjanat/dreamcli/schema`](/reference/schema) and the hosted copy at `DEFINITION_SCHEMA_URL`.
## `DEFINITION_SCHEMA_URL` and `DEFINITION_SCHEMA_VERSION`
The `$schema` URL every definition document carries and the format version it reports as
`schemaVersion`. Both constants are also exported from the root entry.
---
# @kjanat/dreamcli
Source: https://dreamcli.kjanat.dev/raw/reference/main
# @kjanat/dreamcli
The main export. Import schema builders, CLI runner, output, parsing, and errors.
Optional features live on their own subpaths and the builder loads them on demand:
[`@kjanat/dreamcli/completion`](/reference/completion), [`@kjanat/dreamcli/json-schema`](/reference/json-schema),
[`@kjanat/dreamcli/config`](/reference/config), and [`@kjanat/dreamcli/prompt`](/reference/prompt).
Key types: [`ActivityEvent`](/reference/symbols/main/ActivityEvent), [`BeforeParseParams`](/reference/symbols/main/BeforeParseParams), [`CLIPluginHooks`](/reference/symbols/main/CLIPluginHooks), [`CommandMeta`](/reference/symbols/main/CommandMeta), [`CommandSchema`](/reference/symbols/main/CommandSchema), [`DeprecationWarning`](/reference/symbols/main/DeprecationWarning), [`DeriveHandler`](/reference/symbols/main/DeriveHandler), [`DeriveParams`](/reference/symbols/main/DeriveParams), [`Out`](/reference/symbols/main/Out), [`PluginCommandContext`](/reference/symbols/main/PluginCommandContext), [`ResolvedCommandParams`](/reference/symbols/main/ResolvedCommandParams), [`RunResult`](/reference/symbols/main/RunResult)
```ts twoslash
import {
cli,
isMainModule,
command,
group,
flag,
arg,
middleware,
plugin,
CLIError,
ParseError,
ValidationError,
isCLIError,
isParseError,
isValidationError,
createOutput,
formatHelp,
parse,
tokenize,
resolve,
readFlags,
} from '@kjanat/dreamcli';
```
## Schema Builders
### `command(name)`
Create a command builder.
```ts twoslash
import { arg, command, flag } from '@kjanat/dreamcli';
const cmd = command('deploy')
.description('Deploy the app')
.arg('target', arg.string())
.flag('force', flag.boolean())
.action(({ args, flags, out }) => {});
```
### `group(name)`
Create a command group (container for subcommands).
```ts twoslash
import { command, group } from '@kjanat/dreamcli';
const migrate = command('migrate');
const seed = command('seed');
const db = group('db')
.description('Database operations')
.command(migrate)
.command(seed);
```
### `cli(name)`
Create a multi-command CLI builder.
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const deploy = command('deploy');
const mainCmd = command('main');
cli('mycli')
.version('1.0.0')
.description('My tool')
.command(deploy)
.default(mainCmd)
.config('mycli')
.manifest({ inferName: true })
.run();
```
### `.manifest(settings?)` / `.manifest(data)`
Source the CLI's `version` and `description` (and optionally its name) from a manifest file —
`package.json`, `deno.json`, or `jsr.json`. There are two complementary forms.
> `.packageJson()` and `.denoJson()` were **removed in v4**. Use `.manifest()`: it defaults to
> `files: ['package.json']`, and `.denoJson()` becomes
> `.manifest({ files: ['deno.json', 'deno.jsonc', 'jsr.json'] })`.
**Discovery (`settings`) form.** During `.run()`, dreamcli walks up from the current working
directory and reads the nearest manifest among `files` (default `['package.json']`); the nearest
directory wins, and `files` order only tiebreaks within a single directory. Files are parsed as JSON
with a JSONC fallback, so `package.json`, `deno.json`, `jsr.json`, and `deno.jsonc` all work —
including files with `//` / block comments or trailing commas (common in `deno.json`). A
config-only manifest carrying no recognised metadata (e.g. a `deno.json` with only `tasks`/`imports`)
is skipped so discovery keeps probing. Pass `{ inferName: true }` to also infer the CLI name from the
package `bin` entry or `name`; `deno.json`/`jsr.json` have no `bin`, so the name comes from `name` —
pass `inferName: { scope: 'keep' }` to retain a leading `@scope/` (stripped by default). Discovery
has no effect in `.execute()`.
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const deploy = command('deploy');
// Deno / JSR CLI: discover deno.json then jsr.json, keep the scoped name:
cli('mycli')
.manifest({
files: ['deno.json', 'deno.jsonc', 'jsr.json'],
inferName: { scope: 'keep' },
})
.command(deploy)
.run();
```
**Anchored discovery (`from`).** Discovery defaults to the consumer's cwd, which is wrong for an
**installable** CLI (`npm i -g`, `bunx`, `npx`, `deno install`) — its version should reflect its OWN
package, not wherever it happens to be invoked. In a conventional Node, Bun, or Deno project, pass
`{ from: import.meta.url }` to anchor the walk-up to the CLI's own module. Path strings, `file:` URL
strings, and `URL` instances are also accepted.
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const deploy = command('deploy');
// Report THIS CLI's version from any working directory:
cli('mycli')
.manifest({ from: import.meta.url })
.command(deploy)
.run();
```
If the project's ambient `ImportMeta` interface omits the runtime-specific `url` property, pass the
whole object instead: `.manifest({ from: import.meta })`. DreamCLI extracts the URL internally. This
compatibility form is useful for cross-runtime packages that cannot augment `ImportMeta` globally.
JSR does not accept global augmentation. Projects whose runtime typings already provide
`import.meta.url` do not need it.
**Pre-loaded (`data`) form.** Pass an already-imported manifest to skip the filesystem entirely.
Bundlers can statically resolve it, locking the reported version at build time. Unlike the discovery
forms, the data form **also works in `.execute()`** — `version`/`description` are merged into the CLI
schema at builder time. Explicit `.version()`/`.description()` still win, and the data form does not
infer the name.
```ts
import { cli, command } from '@kjanat/dreamcli';
import denoCfg from './deno.json' with { type: 'json' };
const deploy = command('deploy');
cli('mycli').manifest(denoCfg).command(deploy).run();
```
### `isMainModule(meta)`
Node, Bun, and Deno expose the conventional entrypoint guard directly. Use it when the runtime's
ambient types are available:
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const deploy = command('deploy');
const app = cli('mycli').command(deploy);
if (import.meta.main) {
app.run();
}
```
`isMainModule(import.meta)` is the compatibility form for projects whose ambient `ImportMeta`
interface omits `main`. It performs the same check without direct property access or global
augmentation, keeping cross-runtime JSR packages type-checkable and publishable:
```ts twoslash
import {
cli,
command,
isMainModule,
} from '@kjanat/dreamcli';
const deploy = command('deploy');
const app = cli('mycli').command(deploy);
if (isMainModule(import.meta)) {
app.run();
}
```
### `.links(links?)`
Make the root-help header clickable with [OSC 8
hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). The program name and
version on the first line of root `--help` output become links in supporting terminals. The
`help.hyperlinks` run option is the direct programmatic override. Otherwise explicit
`--no-hyperlinks` / `--hyperlinks` flags take precedence, followed by `NO_HYPERLINKS`, then
`FORCE_HYPERLINKS`, and finally TTY detection. Escapes remain confined to the header line — usage
lines, the `--help` hint, the commands table, and completion scripts stay plain.
URLs not provided are derived from manifest metadata when `.manifest()` is active (both the
discovery and pre-loaded data forms): the name links to the normalized `repository` URL (falling back
to `homepage`), and the version links to the forge release tag (`{repo}/releases/tag/v{version}` on
GitHub, `{repo}/-/releases/v{version}` on GitLab).
```ts twoslash
import { cli, command } from '@kjanat/dreamcli';
const deploy = command('deploy');
// Derive both links from the manifest repository/homepage:
cli('mycli').manifest().links().command(deploy).run();
// Explicit URLs (no package.json required):
cli('mycli')
.version('1.0.0')
.links({
name: 'https://github.com/me/mycli',
version:
'https://github.com/me/mycli/releases/tag/v1.0.0',
})
.command(deploy)
.run();
```
### `.plugin(definition)`
Register a CLI plugin created with `plugin(...)`. Plugins run in registration order and can observe
execution before parse, after resolve, before action, and after action.
```ts twoslash
import { cli, command, plugin } from '@kjanat/dreamcli';
const deploy = command('deploy');
const tracePlugin = plugin({}, 'trace');
cli('mycli').plugin(tracePlugin).command(deploy);
```
### `flag`
Flag factory with typed builders:
| Factory | Type | Default |
| ---------------------- | ------------------------------- | ------- |
| `flag.string()` | `string \| undefined` | none |
| `flag.number()` | `number \| undefined` | none |
| `flag.boolean()` | `boolean` | `false` |
| `flag.enum(values)` | Union of values \| `undefined` | none |
| `flag.array(inner)` | `T[]` | `[]` |
| `flag.custom(parseFn)` | Return type \| `undefined` | none |
| `flag.url()` | `URL \| undefined` | none |
| `flag.path()` | `string \| undefined` | none |
| `flag.date()` | `Date \| undefined` | none |
| `flag.duration()` | `number \| undefined` (ms) | none |
| `flag.bytes()` | `number \| undefined` | none |
| `flag.count()` | `number` | `0` |
| `flag.keyValue()` | `Record` | `{}` |
See [Flags](/guide/flags#flag-types) for the option objects and constraints each
kind accepts.
### `arg`
Argument factory. Every kind is required unless `.optional()` or `.default()`
says otherwise:
| Factory | Type |
| --------------------- | ------------------------------- |
| `arg.string()` | `string` |
| `arg.number()` | `number` |
| `arg.boolean()` | `boolean` |
| `arg.enum(values)` | Union of provided string values |
| `arg.custom(parseFn)` | Return type |
| `arg.keyValue()` | `Record` |
| `arg.url()` | `URL` |
| `arg.path()` | `string` |
| `arg.date()` | `Date` |
| `arg.duration()` | `number` (ms) |
| `arg.bytes()` | `number` |
`array` and `count` are flag-only. `.variadic()` is the arg form of
`flag.array()`, and a count has no positional meaning. See
[What the arg factory does not have](/guide/arguments#flag-only-surface).
### `middleware(handler)`
Create typed middleware that can wrap downstream execution and add context to the chain.
### `.derive(handler)`
Register a command-scoped typed pre-action handler. Derive runs after resolution, receives typed
`{ args, flags, ctx, out, meta }`, and may either return `void` for validation-only behavior or
return an object to merge additional properties into `ctx`.
```ts twoslash
import { CLIError, command, flag } from '@kjanat/dreamcli';
command('deploy')
.flag('token', flag.string().env('AUTH_TOKEN'))
.derive(({ flags }) => {
if (!flags.token)
throw new CLIError('Not authenticated', {
code: 'AUTH_REQUIRED',
});
return { token: flags.token };
})
.action(({ ctx }) => {
ctx.token;
// ^?
});
```
### `plugin(hooks, name?)`
Create a reusable CLI plugin definition from lifecycle hooks. The returned value can be attached
with `cli(...).plugin(...)` and receives stable hook payloads typed by `BeforeParseParams`,
`ResolvedCommandParams`, and `PluginCommandContext`.
```ts twoslash
import { cli, command, plugin } from '@kjanat/dreamcli';
const deploy = command('deploy');
const trace = plugin(
{
beforeParse: ({ argv, out }) =>
out.info(argv.join(' ')),
afterResolve: ({ flags, args }) =>
console.log({ flags, args }),
},
'trace',
);
cli('mycli').plugin(trace).command(deploy);
```
## Plugin Types
### `CLIPluginHooks`
Lifecycle hook bag for `plugin(...)`. Each hook may be sync or async:
```ts twoslash
import type { CLIPluginHooks } from '@kjanat/dreamcli';
// ^?
```
Use `beforeParse` to inspect raw argv, `afterResolve` to observe resolved args/flags,
`beforeAction` to run immediately before middleware and the action handler, and `afterAction`
to observe successful completion.
### `PluginCommandContext`
Base payload shared by all plugin hooks. It contains the current `command` schema, `meta`
(`CommandMeta`), and `out` channel, so hooks can inspect execution context without reaching into
internal CLI state.
```ts twoslash
import type { PluginCommandContext } from '@kjanat/dreamcli';
// ^?
```
### `BeforeParseParams`
Payload for `beforeParse`. Adds the leaf-command `argv` array to `PluginCommandContext` so plugins
can log, validate, or instrument the exact argument list before parsing starts.
```ts twoslash
import type { BeforeParseParams } from '@kjanat/dreamcli';
// ^?
```
### `ResolvedCommandParams`
Payload for `afterResolve`, `beforeAction`, and `afterAction`. Adds fully resolved `flags`, `args`,
and collected `deprecations` so hooks can inspect the final command inputs.
```ts twoslash
import type { ResolvedCommandParams } from '@kjanat/dreamcli';
// ^?
```
## Execution Types
### `CommandMeta`
Metadata about the running CLI, passed to action handlers and middleware as `meta`. It carries the
CLI `name`, display `bin`, `version`, and current leaf `command`, making it useful for logging,
telemetry, and custom output headers.
```ts twoslash
import type { CommandMeta } from '@kjanat/dreamcli';
// ^?
```
### `RunResult`
Structured result returned by `runCommand(...)` and `cli.execute(...)`. It includes the process
`exitCode`, captured `stdout`/`stderr`, activity lifecycle events, and an `error` field that is
`undefined` on success and a `CLIError` on failure.
```ts twoslash
import type { RunResult } from '@kjanat/dreamcli';
// ^?
```
## Output
### `createOutput(options?)`
Create an output channel.
Typically not called directly — commands receive `out` in the action handler.
Handlers can call `out.setExitCode(code)` to request a process exit code without
printing error output. The command still completes normally and `RunResult.error`
stays `undefined` unless an error is thrown.
## Parsing
### `tokenize(argv)`
Tokenize raw argv into a `Token[]` array.
### `parse(schema, argv)`
Parse argv against a command schema, returning `ParseResult`.
### `resolve(schema, parseResult, options)`
Resolve flag values through the resolution chain.
### `readFlags(definitions, options?)`
Evaluate a record of flag builders outside a CLI and return a
`Promise>` of the resolved values. It runs the same schema, parser, coercion,
resolver, and validation a command runs, with no dispatch, output channel, help, or process exit. See
[Standalone Flag Evaluation](/guide/read-flags).
- `definitions`: `FlagMap`, a record of `FlagBuilder` values keyed by canonical flag name
- `options`: `ReadFlagsOptions`, which extends `ResolveOptions` with `argv`, `adapter`, `parse`, and
`onDeprecation`
```ts twoslash
import { flag, readFlags } from '@kjanat/dreamcli';
const options = await readFlags(
{
watch: flag.boolean().alias('w').env('WATCH'),
target: flag
.enum(['node', 'browser'])
.default('node'),
},
{ argv: ['-w'], env: {} },
);
```
## Help
### `osc8(url, text)`
Wrap `text` in an [OSC 8
hyperlink](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) pointing at `url`
(string or `URL`). Supporting terminals render clickable text; others ignore the escapes. Useful for
linking arbitrary help strings, e.g. `.version(osc8(releaseUrl, '1.0.0'))`.
### `visibleWidth(text)`
Measure the visible column width of `text`, ignoring ANSI CSI (colors) and OSC (hyperlink) escape
sequences. Help formatting uses this internally for padding and wrapping, so colored or linked text
no longer breaks table alignment.
```ts twoslash
import { osc8, visibleWidth } from '@kjanat/dreamcli';
const link = osc8('https://github.com/me/mycli', 'mycli');
visibleWidth(link); // 5
```
## Errors
### `CLIError`
Base error class with `code`, `exitCode`, `suggest`, `details`.
### `ParseError`
Extends `CLIError`. Thrown for argv parsing failures.
### `ValidationError`
Extends `CLIError`. Thrown for value validation failures.
### Type Guards
- `isCLIError(err)` — narrows to `CLIError`
- `isParseError(err)` — narrows to `ParseError`
- `isValidationError(err)` — narrows to `ValidationError`
---
# Output Contract
Source: https://dreamcli.kjanat.dev/raw/reference/output-contract
# Output Contract
This page records the internal output-policy boundary for the `dreamcli-re-foundation` workstream.
It is a stability target for tests and refactors, not a public API guarantee.
## Responsibilities
- choose semantic text-routing facts from `jsonMode`, `isTTY`, and `verbosity`
- choose spinner and progress activity modes before rendering starts
- define which activity handles require tracked cleanup after execution
- keep stdout reserved for structured data when JSON mode is active
## Non-Responsibilities
- render ANSI frames, tables, or concrete text lines
- own stdout or stderr writer functions
- capture activity events for testkit
- decide command dispatch, parsing, or resolution behavior
## Output Policy
The output contract is modeled in `src/core/output/contracts.ts` as:
```ts twoslash
import type { Verbosity } from '@kjanat/dreamcli';
interface OutputPolicy {
readonly jsonMode: boolean;
readonly isTTY: boolean;
readonly verbosity: Verbosity;
}
```
The intent is simple:
- `jsonMode` reserves stdout for `json()` and JSON-form table output
- `isTTY` enables decorative activity rendering only when JSON mode is off
- `verbosity` suppresses `info()`, `status()`, and rendered spinner/progress activity; `log()` remains enabled
## Activity Policy
```ts twoslash
interface ActivityPolicy {
readonly mode: 'noop' | 'static' | 'tty';
readonly stream: 'stderr';
readonly cleanup: 'none' | 'stop' | 'done';
}
```
Field meaning:
| Field | Meaning |
| --------- | ---------------------------------------------------------------------------- |
| `mode` | semantic activity choice before handle construction |
| `stream` | output stream reserved for non-capture activity rendering |
| `cleanup` | terminal method the framework must call if a handler leaks the active handle |
Current cleanup facts:
- spinners use `stop`
- progress handles use `done`
- noop activity requires no cleanup registration
## Contract Facts
The frozen facts in `outputContract` are:
- JSON mode reserves stdout for structured data
- quiet mode suppresses `info()`, `status()`, and rendered spinner/progress activity
- capture records logical activity lifecycle events regardless of verbosity; quiet governs rendered bytes
- non-capture activity uses stderr
- TTY activity requires both `isTTY` and `!jsonMode`
- spinner and progress cleanup semantics are explicit and distinct
## Evidence
- Contract module: `src/core/output/contracts.ts`
- Current output implementation: `src/core/output/index.ts`
- Activity handles: `src/core/output/activity.ts`
- Contract tests: `src/core/output/contracts.test.ts`
- RFC / PRD source: `specs/dreamcli-re-foundation.md`, `specs/dreamcli-re-foundation-prd.md`
## Current Status
- output policy facts are now named in one internal contract module
- `OutputChannel` now keeps an explicit `policy` snapshot and delegates concrete writer and activity-handle selection through internal renderer helpers
- capture behavior still stays in `src/core/output/index.ts`; a full output-layer split remains future work
---
# Planner Contract
Source: https://dreamcli.kjanat.dev/raw/reference/planner-contract
# Planner Contract
This page records the internal planner boundary for the `dreamcli-re-foundation` workstream.
It is a stability target for tests and refactors, not a public API guarantee.
## Responsibilities
- intercept root `--help` and `--version`
- dispatch nested commands
- apply default-command fallback
- merge propagated flags while honoring child shadowing
- build the execution handoff for the matched command
- normalize invocation argv for planner-owned global concerns like root `--json`, and reject an
invalid value for one of them after handling root help and version requests
## Non-Responsibilities
- parse flag or arg values
- read env, config, stdin, or prompt sources
- run middleware or action handlers
- decide concrete output writers or rendering mechanics
## Dispatch Outcome
The planner contract is modeled in `src/core/cli/planner.ts` as:
```ts twoslash
import type {
CLIError,
HelpOptions,
} from '@kjanat/dreamcli';
type CommandExecutionPlan = Record;
type DispatchOutcome =
| {
readonly kind: 'root-help';
readonly help: HelpOptions;
}
| {
readonly kind: 'root-version';
readonly version: string;
}
| {
readonly kind: 'dispatch-error';
readonly error: CLIError;
}
| {
readonly kind: 'match';
readonly plan: CommandExecutionPlan;
};
```
The intent is simple:
- `root-help` and `root-version` short-circuit before command execution
- `dispatch-error` captures CLI-level failures before parse and resolve
- `match` hands one explicit execution plan to the executor seam
## Command Execution Plan
```ts twoslash
import type {
CLIPlugin,
CommandMeta,
CommandSchema,
HelpOptions,
} from '@kjanat/dreamcli';
type CompiledCommand = Record;
type OutputPolicy = Record;
interface CommandExecutionPlan {
readonly command: CompiledCommand;
readonly mergedSchema: CommandSchema;
readonly argv: readonly string[];
readonly meta: CommandMeta;
readonly plugins: readonly CLIPlugin[];
readonly output: OutputPolicy;
readonly help: HelpOptions | undefined;
}
```
Field meaning:
| Field | Meaning |
| -------------- | ------------------------------------------------------------------- |
| `command` | compiled node of the matched leaf command: handler, steps, subcommands |
| `mergedSchema` | leaf schema after propagated ancestor flags are merged and shadowed |
| `argv` | remaining argv after command-name dispatch |
| `meta` | CLI metadata passed into middleware and actions |
| `plugins` | CLI plugin list observed by the execution path |
| `output` | semantic output facts: `jsonMode`, `isTTY`, `verbosity` |
| `help` | help formatting context carried through command execution |
## Evidence
- Contract module: `src/core/cli/planner.ts`
- Current dispatcher: `src/core/cli/dispatch.ts`
- Current CLI orchestration: `src/core/cli/index.ts`
- RFC / PRD source: `specs/dreamcli-re-foundation.md`, `specs/dreamcli-re-foundation-prd.md`
## Current Status
- planner outcomes are named explicitly in code
- raw invocation shaping, root interception, dispatch, and default-command fallback live in `src/core/cli/planner.ts`
- `CLIBuilder.execute()` now consumes planner outcomes as a render-and-execute shell rather than owning command-routing policy
---
# @kjanat/dreamcli/prompt
Source: https://dreamcli.kjanat.dev/raw/reference/prompt
# @kjanat/dreamcli/prompt
Prompt engines. When stdin is a TTY and no prompter was injected, `cli().run()` installs a thin
engine that imports the terminal prompter the first time a prompt is presented, so a run that never
prompts never loads it. Import this module to drive prompts from a custom host or to build an engine
of your own against `PromptEngine`.
```ts twoslash
import { createTerminalPrompter, resolvePromptConfig } from '@kjanat/dreamcli/prompt';
import type { PromptEngine, ReadFn, ResolvedPromptConfig } from '@kjanat/dreamcli/prompt';
```
`PromptEngine`, `ReadFn`, the resolved config types, and `resolvePromptConfig` are also exported
from the root entry. This subpath statically includes the terminal prompter, so code that only
needs `resolvePromptConfig` should take it from the root entry.
## `createTerminalPrompter(read, write)`
Create a line-based prompt engine over injected I/O. `read` returns the next line or `null` on EOF,
which the engine treats as cancellation; `write` receives prompt text. The engine has no raw-mode
dependency, so it runs the same on Node, Bun, and Deno.
```ts twoslash
import { createTerminalPrompter } from '@kjanat/dreamcli/prompt';
import type { ReadFn } from '@kjanat/dreamcli/prompt';
import { createInterface } from 'node:readline';
const rl = createInterface({ input: process.stdin, output: process.stdout });
let closed = false;
rl.once('close', () => {
closed = true;
});
const read: ReadFn = () =>
new Promise((resolve) => {
if (closed) {
resolve(null);
return;
}
const onClose = (): void => resolve(null);
rl.once('close', onClose);
rl.question('', (answer) => {
rl.off('close', onClose);
resolve(answer);
});
});
const prompter = createTerminalPrompter(read, (text) => {
process.stdout.write(text);
});
```
Pass the engine as `prompter` in `CLIRunOptions` or `CLIExecuteOptions` to replace the default.
For tests, `createTestPrompter()` from [`@kjanat/dreamcli/testkit`](/reference/testkit) scripts
answers instead.
## `resolvePromptConfig(config, enumValues)`
Prepare a `ResolvedPromptConfig` from a raw `PromptConfig`, merging a flag's enum values into
`select` and `multiselect` choices when the config omits them. The resolver calls it before an engine
sees the config; call it yourself only when feeding an engine directly.
---
# Resolver Contract
Source: https://dreamcli.kjanat.dev/raw/reference/resolver-contract
# Resolver Contract
This page records the internal resolver boundary for the `dreamcli-re-foundation` workstream.
It is a stability target for tests and refactors, not a public API guarantee.
## Responsibilities
- apply one precedence order to both surfaces:
`cli -> stdin -> env -> config -> prompt -> default`
- read stdin only when resolution will select it
- gate prompts after non-interactive sources have been checked
- coerce and validate sourced values
- collect deprecations from explicit sources
- aggregate validation failures into one thrown `ValidationError`
## Non-Responsibilities
- read from runtime globals like `process.env` or terminal APIs directly
- discover config files or package metadata
- parse argv tokens into `ParseResult`
- run middleware or action handlers
- format terminal-facing error output
## Invocation Boundary
The resolver contract is modeled in `src/core/resolve/contracts.ts` as:
```ts twoslash
import type {
CommandSchema,
DeprecationWarning,
ParseResult,
PromptEngine,
ResolutionProvenanceRecord,
} from '@kjanat/dreamcli';
interface ResolverInvocation {
readonly schema: CommandSchema;
readonly parsed: ParseResult;
readonly options?: ResolveOptions;
}
interface ResolveOptions {
readonly stdinData?: string | null;
readonly env?: Readonly<
Record
>;
readonly config?: Readonly>;
readonly prompter?: PromptEngine;
readonly stat?: (
path: string,
) => Promise<'file' | 'directory' | null>;
readonly mkdir?: (path: string) => Promise;
}
interface ResolveResult {
readonly flags: Readonly>;
readonly args: Readonly>;
readonly deprecations: readonly DeprecationWarning[];
readonly provenance: ResolutionProvenanceRecord;
}
```
The intent is simple:
- callers inject all external state through `ResolveOptions`
- resolver output is resolved values plus structured deprecation facts
- the executor layer owns rendering and handler execution after this point
## Source And Precedence Facts
`schema/source.ts` names the one stable precedence order, and `contracts.ts`
re-exports it:
```ts twoslash
const RESOLUTION_ORDER = [
'cli',
'stdin',
'env',
'config',
'prompt',
'default',
] as const;
```
Both surfaces walk it. An explicit `-` is CLI-sourced with bytes from stdin and
keeps CLI precedence, so it lands on `cli`; the `stdin` stage is the implicit
fallback an absent input takes before env. That order is the behavior contract
tests target.
Resolution also records which stage produced each value, keyed by input name.
The record distinguishes the two ways stdin delivers bytes
(`{ stage: 'cli', via: 'stdin', trigger: 'dash' }` versus
`{ stage: 'stdin', via: 'stdin', trigger: 'fallback' }`) and names the binding
that fired (`{ stage: 'env', envVar }`, `{ stage: 'config', configPath }`).
One input's winning-stage record is public as `ResolutionProvenance`.
`ResolutionProvenanceRecord` is the pair of flag and argument maps returned on
`ResolveResult.provenance`; handlers receive the typed `InputSources` equivalent
as `sources`, and `readFlags()` passes it through `onSources`. The maps are built
with a null prototype, so an undeclared key named `toString` reads back
`undefined` rather than an inherited member; a declared input with that name
returns its own provenance when present. Only inputs that resolved a value have
an entry. See
[Value provenance](/guide/semantics#which-source-won).
## Diagnostic Expectations
- env, config, prompt, and stdin failures carry source-aware detail payloads
- every coercion failure carries `source` naming the stage that produced the value
- diagnostic visibility depends only on the owning schema's `sensitive` field,
never the winning source
- non-sensitive failures report raw values from every source; sensitive failures
omit raw-derived values, keys, paths, and causes
- hard coercion errors stop later fallback for that same field
- multiple validation failures are thrown as one aggregate error with per-error details
- aggregate validation failures also include per-issue summaries with normalized input labels and source labels when the failing source is known
- missing-value errors remain actionable via source-ordered suggestions, naming
only the stdin routes the input's `when` mode actually offers
## Redesign Boundaries
This contract intentionally freezes behavior before deeper resolver work:
- module splitting can move orchestration, coercion, lookup, and error helpers apart
- aggregated diagnostics can improve, but source-aware details and explicit precedence must remain testable
- aggregate wrappers may change presentation, but they must keep nested per-error payloads plus explicit per-issue summaries for flags and args
- any shared flag/arg property model must preserve the current stage ordering unless a later contract explicitly changes it
## Shared Property Model Decision
The current resolver now makes that decision explicit in `src/core/schema/value.ts`:
- the shared flag/arg value model is **coercion-only**
- it covers the overlapping kinds `string`, `number`, `boolean`, `enum`, and
`custom`, and a collection reaches it through the value of its element
- how many values a source carries belongs to `src/core/schema/cardinality.ts`,
which owns splitting, aggregation, and declared-default validation
- it does **not** own precedence order, fallback order, prompt/stdin policy, or required-value validation
That split is intentional.
Both surfaces own `cli -> stdin -> env -> config -> prompt -> default`. The
source axis is shared because the sources themselves are the same set;
`schema/source.ts` normalizes a flag or arg schema onto an ordered
`SourceBinding` list, and `resolve/stages.ts` walks that order once for both.
What stays per-surface is what genuinely differs: how a parsed CLI value reads,
how a raw value coerces, and how diagnostics name the subject.
## Evidence
- Contract module: `src/core/resolve/contracts.ts`
- Shared value model: `src/core/schema/value.ts`
- Current implementation: `src/core/resolve/index.ts`
- Existing behavior tests: `src/core/resolve/*.test.ts`
- RFC / PRD source: `specs/dreamcli-re-foundation.md`, `specs/dreamcli-re-foundation-prd.md`
## Current Status
- resolver input and output boundaries are now named explicitly in code
- precedence and diagnostic expectations now have one internal contract module
- resolver orchestration remains in `src/core/resolve/index.ts`, with flag and arg paths delegated to `flags.ts` and `args.ts`
---
# @kjanat/dreamcli/runtime
Source: https://dreamcli.kjanat.dev/raw/reference/runtime
# @kjanat/dreamcli/runtime
Runtime adapter factory and platform detection.
```ts twoslash
import {
createAdapter,
detectRuntime,
} from '@kjanat/dreamcli/runtime';
import type { RuntimeAdapter, TerminalSize } from '@kjanat/dreamcli/runtime';
```
## `createAdapter()`
Create a runtime adapter via auto-detection.
```ts twoslash
import {
createAdapter,
createNodeAdapter,
createDenoAdapter,
} from '@kjanat/dreamcli/runtime';
const adapter = createAdapter(); // auto-detect
const nodeAdapter = createNodeAdapter(); // explicit Node.js (also used for Bun)
const denoAdapter = createDenoAdapter(); // explicit Deno
```
> Bun exposes a Node-compatible `process`, so it uses the Node adapter — there is no separate Bun factory. Runtime detection still reports `'bun'` (see [`detectRuntime()`](#detectruntime)).
### RuntimeAdapter Interface
| Member | Kind | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------------- |
| `argv` | readonly | Raw command-line arguments |
| `env` | readonly | Environment variables |
| `cwd` | readonly | Current working directory |
| `stdout` | readonly | Stdout writer used by the output channel |
| `stderr` | readonly | Stderr writer used by the output channel |
| `stdin` | readonly | Line reader used for interactive prompts |
| `readStdin()` | method | Read all piped stdin as a single string, or `null` when stdin is a TTY / no data is piped. Called only when a `.stdin()` flag or argument would select the stream |
| `isTTY` | readonly | Whether stdout is connected to a TTY |
| `stdinIsTTY` | readonly | Whether stdin is connected to a TTY |
| `getTerminalSize()` | method | Read current stdout terminal dimensions, or `undefined` when unavailable |
| `onTerminalResize(listener)` | method | Subscribe to terminal resize events when supported |
| `exit(code)` | method | Exit the process |
| `readFile(path)` | method | Read a UTF-8 file for config discovery |
| `homedir` | readonly | User home directory |
| `configDir` | readonly | Platform-specific configuration directory |
`TerminalSize` is `{ columns: number; rows: number }`. Node and Bun read `process.stdout.getWindowSize()` when available, falling back to `columns` / `rows`. Deno uses `Deno.consoleSize()`.
## `detectRuntime()`
Detect the current runtime environment.
```ts twoslash
import { detectRuntime } from '@kjanat/dreamcli/runtime';
import type { Runtime } from '@kjanat/dreamcli/runtime';
const runtime: Runtime = detectRuntime();
// ^?
```
## Supported Runtimes
| Runtime | Adapter | Notes |
| ------------------ | ------------- | ------------------------------ |
| Node.js >= 22.22.2 | `NodeAdapter` | Full support |
| Bun >= 1.3 | `NodeAdapter` | Uses Node-compatible `process` |
| Deno >= 2.6.0 | `DenoAdapter` | Permission-safe Deno namespace |
These minimums are the tested support floor, declared in the package `engines` field. Adapter creation does not validate the runtime version or throw: as a dependency the package manager enforces `engines` at install time, and when DreamCLI is bundled the consuming CLI owns its supported-runtime policy.
---
# Schema
Source: https://dreamcli.kjanat.dev/raw/reference/schema
# Schema
`@kjanat/dreamcli/schema` is the package's published definition-schema export.\
It resolves to the same schema referenced by `generateSchema()` output, with a package-target specific mapping:
- npm/package export (`package.json`): `./schema` -> `dreamcli.schema.json`
- Deno/JSR export (`deno.json`): `./schema` -> `src/schema.ts` (which re-exports the same schema)
Its bytes are identical to the canonical hosted copy at
`https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json`, so use it when you want local or offline validation of dreamcli definition metadata without a network lookup.
## Importing The Schema
```ts twoslash
import schema from '@kjanat/dreamcli/schema';
schema.$schema;
schema.$defs.command;
```
In TypeScript, this works with `resolveJsonModule`.\
Runtime loading behavior depends on the host runtime export target (see notes above).
For Node on the npm/package target, import with `with { type: 'json' }`.
## Common Uses
- Validate the output of `generateSchema(myCli.schema)` in tests or tooling.
- Ship an offline `$schema` target in repos that do not want network lookups.
- Feed the schema into JSON Schema tooling such as Ajv or IDE integrations.
## Pairing With `generateSchema()`
```ts twoslash
import schema from '@kjanat/dreamcli/schema';
import { cli, command } from '@kjanat/dreamcli';
import { generateSchema } from '@kjanat/dreamcli/json-schema';
const app = cli('mycli').command(command('deploy'));
const definition = generateSchema(app.schema);
definition.$schema;
schema.$id;
```
`definition.$schema` points at the canonical hosted URL. Its `v1` segment is the definition format version, matching the document's `schemaVersion`. The package export gives you the same schema locally from the installed package.
## Related Pages
- [Schema Export](/guide/schema-export)
- [@kjanat/dreamcli](/reference/main)
---
# Semantic Delta Log
Source: https://dreamcli.kjanat.dev/raw/reference/semantic-delta-log
# Semantic Delta Log
This page records which re-foundation changes preserved DreamCLI behavior versus which ones
intentionally changed it.
Use it as the semantic review anchor for:
- release notes and changelog drafting;
- follow-up docs like rationale, migration, and troubleshooting;
- refactor review when code motion and user-visible behavior happen together.
It is not a public API contract. It is a maintained execution-semantics log for the
`dreamcli-re-foundation` workstream.
## Preserved Semantics
These behaviors were intentionally kept stable while internal ownership moved.
| Surface | Preserved behavior | Evidence | Notes |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Planner and dispatch | Root `--help`, root `--version`, default-command fallback, nested dispatch, and propagated-flag masking stay the same. | [CLI Semantics](/guide/semantics), [`src/core/cli/planner.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/planner.test.ts), [`src/core/cli/cli-dispatch.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/cli-dispatch.test.ts) | Planner extraction changed ownership, not user-facing dispatch rules. |
| Resolution precedence | Both surfaces walk one order: `CLI -> stdin -> env -> config -> prompt -> default`. An explicit `-` is CLI-sourced with bytes from stdin. | [CLI Semantics](/guide/semantics), [`src/core/resolve/contracts.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/contracts.test.ts), [`src/core/resolve/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/index.ts) | Resolver refactors and module splits were constrained to preserve source order and prompt/stdin gating. |
| Execution lifecycle | Parse -> resolve -> hooks -> derive/middleware -> action -> result assembly still runs in the same semantic order, with cleanup on success, help exits, and failures. | [`src/core/testkit/executor-contract.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/executor-contract.test.ts), [`src/core/execution/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/execution/index.ts) | Centralizing execution in `src/core/execution/` changed ownership, not lifecycle guarantees. |
| Output policy behavior | JSON routing, TTY-aware decorative-output suppression, and activity cleanup semantics stay stable across the output-policy split. | [Output Contract](/reference/output-contract), [`src/core/output/contracts.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/output/contracts.test.ts), [`src/core/output/output.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/output/output.test.ts) | The re-foundation extracted policy seams without widening the public `Out` behavior. |
| Completion planner behavior | Nested command completion, propagated ancestor flags, and root/default-command completion rules remain contract-tested while shell support expanded. | [Shell Completions](/guide/completions), [`src/core/cli/cli-completion-contract.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/cli-completion-contract.test.ts), [`src/core/completion/completion.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/completion.test.ts) | New shells were added without inventing a separate completion planner model. |
## Intentional Redesigns
These are the main semantic deltas the re-foundation intentionally introduced.
| Delta | Before | Now | Evidence | Why |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| Canonical execution ownership | The real parse -> resolve -> execute pipeline was effectively owned by `runCommand()` even though CLI also orchestrated parts of it. | `src/core/execution/index.ts` is the explicit canonical executor. CLI and testkit both delegate to that shared seam. | [`src/core/execution/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/execution/index.ts), [`src/core/cli/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/index.ts), [`src/core/testkit/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/index.ts), [`docs/reference/support-matrix.md`](/reference/support-matrix) | One owned execution path makes future planner, resolver, and output changes easier to localize and test. |
| Resolver aggregate diagnostics | Mixed flag/arg validation failures were aggregated, but the top-level diagnostic was weaker and less source-aware. | Aggregate validation errors now carry per-issue summaries with normalized labels like `flag --port` and `argument ` plus source labels like `env PORT` and `stdin`. | [`src/core/resolve/resolve-aggregation.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/resolve-aggregation.test.ts), [`src/core/resolve/errors.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/errors.ts), [Resolver Contract](/reference/resolver-contract) | This improved the user-facing failure story without breaking nested `ValidationError` JSON used by existing tests and consumers. |
| Completion support truth | The type/docs surface was broader than reality and only `bash` plus `zsh` were actually shipped. | `fish` and `powershell` completion generation are now shipped, tested, and reflected consistently across docs and support-truth surfaces. | [`src/core/completion/shells/fish.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/shells/fish.ts), [`src/core/completion/shells/powershell.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/shells/powershell.ts), [`src/core/cli/cli-completion-contract.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/cli-completion-contract.test.ts), [Support Matrix](/reference/support-matrix) | The supported shell surface is now truthful across code, tests, and docs instead of relying on a wider union type or deferred claims. |
## Remaining Deferred
These areas are still intentionally deferred and should not be described as shipped semantic changes yet.
| Area | Current state | Tracking |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Broader CI/runtime-shell coverage claims | Runtime support is shipped, but broader cross-platform CI coverage is still not claimed as complete. | [Support Matrix](/reference/support-matrix), `harden-ci-coverage` |
## Narrative Follow-Through
The post-stabilization narrative docs set is now published:
- [Architecture Rationale](/guide/rationale)
- [Limitations And Workarounds](/guide/limitations)
- [Migration And Adoption](/guide/migration)
- [Troubleshooting](/guide/troubleshooting)
## Related Pages
- [Support Matrix](/reference/support-matrix)
- [CLI Semantics](/guide/semantics)
- [Planner Contract](/reference/planner-contract)
- [Resolver Contract](/reference/resolver-contract)
- [Output Contract](/reference/output-contract)
---
# Stability Policy
Source: https://dreamcli.kjanat.dev/raw/reference/stability
# Stability Policy
This page classifies every exported type and value by the compatibility contract
it carries. Version numbers follow [SemVer](https://semver.org).
"Breaking" is not one rule. It depends on who constructs a value, who reads it,
and whether the value crosses a serialization boundary. Adding a readonly member
to [`Out`](#sealed-framework-values) is a minor release because only the
framework constructs one. Adding a required member to
[`RuntimeAdapter`](#implementer-ports) is a major release because consumers
implement it; adding an optional member is a minor release. Adding a variant to
[`Shell`](#closed-unions-and-discriminated-results)
is a major release because consumers switch exhaustively over it, while adding
one to [`ErrorCode`](#open-unions) is a patch because the union is open by
construction.
Three questions place a symbol in a category.
1. Who constructs values of this type, the framework or the consumer?
2. Who reads them, and are they expected to enumerate the members?
3. Does the value survive `JSON.stringify` and get read by something outside
this package?
A symbol not named on this page falls into the category whose shape it matches.
When the match is ambiguous, the stricter contract applies.
## Categories
| Category | Examples | Minor releases may |
| ------------------------------------ | --------------------------------------------------------- | ----------------------------------------------- |
| Sealed framework values | `Out`, `RenderContext`, `CommandSchema`, `ConfigSettings` | add readonly members |
| Consumer input options | `RunOptions`, `CLIExecuteOptions`, `OutputOptions` | add optional fields |
| Transparent input definitions | `FlagDefinition`, `CLIDefinition`, `PackageJsonData` | add optional fields |
| Implementer ports | `RuntimeAdapter`, `PromptEngine`, `FormatLoader` | add optional members only |
| Externally governed protocols | `StandardSchemaV1` family | track the upstream specification |
| Structural consumer configuration | `CLIPlugin`, `CLIPluginHooks` | add optional hooks |
| Closed unions, discriminated results | `Verbosity`, `Shell`, `FlagKind`, `ConfigDiscoveryResult` | change nothing |
| Open unions | `ErrorCode` | add members in any release |
| Framework-produced callback payloads | `BeforeParseParams`, `DeprecationWarning` | add readonly members, required ones included |
| Closed constructible DTOs | `RunResult`, `CommandMeta` | change nothing |
| Serialized formats | `DefinitionDocumentV1` family, `CLIErrorJSON` | add optional fields at the same `schemaVersion` |
| Classes and functions | `CLIBuilder`, `cli()`, `parse()` | add optional trailing parameters |
| Internal surface | underscore members, `Internal*` types, brand symbols | change anything, any release |
## Sealed framework values
`Out`, `RenderContext`, `FlagSchema`, `ArgSchema`, `CommandSchema`, `CLISchema`,
`ConfigSettings`, and `Builtins`.
Each carries a brand a consumer cannot spell. `Out` and `RenderContext` carry a
`unique symbol` that no entrypoint exports. The schema family carries a
type-only brand established by its factory. Implementing or structurally
constructing any of them outside the framework fails to type-check.
Obtain instances from the framework:
- `Out` from action parameters, `createOutput()`, or `createCaptureOutput()`.
- `RenderContext` from `resolveRenderContext()`.
- `FlagSchema`, `ArgSchema`, `CommandSchema` from `createFlagSchema()`,
`createArgSchema()`, `createCommandSchema()`, or the corresponding builders.
- `CLISchema` with its nested `ConfigSettings` and `Builtins` from
`createCLISchema()`, or from `cli(...).schema`.
The guarantee is one-directional. New readonly members may appear in a minor
release, and code that reads documented members keeps compiling. Code that
enumerates keys (`Record`) breaks when a member arrives, so
enumeration is outside the contract.
Factories take a plain definition and return the sealed value:
```ts twoslash
import { createCommandSchema } from '@kjanat/dreamcli';
// ---cut---
const deploy = createCommandSchema({
name: 'deploy',
flags: { force: { kind: 'boolean' } },
args: [{ name: 'target', schema: { kind: 'string' } }],
});
```
The brand rides along on a spread, which is the escape hatch for building a
variant of an existing schema:
```ts twoslash
import { createCommandSchema, type CommandSchema } from '@kjanat/dreamcli';
const deploy = createCommandSchema({ name: 'deploy' });
// ---cut---
const internalDeploy: CommandSchema = { ...deploy, hidden: true };
```
A hand-written object literal has no brand to carry, so it is rejected:
```ts twoslash
// @errors: 2741
import type { CommandSchema } from '@kjanat/dreamcli';
// ---cut---
const fake: CommandSchema = {
name: 'deploy',
description: undefined,
aliases: [],
hidden: false,
examples: [],
flags: {},
args: [],
hasAction: true,
interactive: undefined,
commands: [],
};
```
Two further properties hold for the schema family. Feeding a built schema back
through its factory produces a deep-equal schema, so normalization is
idempotent. A field belonging to another kind (`enumValues` on a `boolean` flag)
fails to compile and throws `INVALID_SCHEMA` at runtime for JavaScript callers.
Helpers that need part of a sealed value should accept a capability type built
with `Pick`, which stays correct as members are added:
```ts twoslash
import type { Out } from '@kjanat/dreamcli';
// ---cut---
type InformationalOutput = Pick;
function renderRows(out: InformationalOutput, rows: readonly string[]) {
for (const row of rows) out.info(row);
}
```
For test doubles, take a real channel from the testkit and spy on it:
```ts
const [out] = createCaptureOutput();
vi.spyOn(out, 'info');
```
`Builtins` is the normalized built-in-flag state stored on `CLISchema.builtins`.
Every key is present and typed `BuiltinMode` after normalization, whatever
subset of them the caller supplied. The consumer-facing input form is
`BuiltinsConfig`, which is a
[transparent input definition](#transparent-input-definitions).
The compiled execution graph behind a program is not in this category because it
is not exported at all. See [Internal surface](#internal-surface).
## Consumer input options
Option objects the caller constructs and passes in.
Program and execution options: `CLIOptions`, `CLIExecuteOptions`, `CLIRunOptions`,
`RunOptions` and `RunCommandOptions` (both from `@kjanat/dreamcli/testkit`),
`DefaultCommandOptions`, `ManifestSettings` with its name-inference value type
`InferNameOption`, `ManifestDiscoveryOptions`, `ConfigDiscoveryOptions`,
`ConfigSearchPathOptions`, and `PackageRepositoryUrlOptions`.
Pipeline and rendering options: `ParseOptions`, `ResolveOptions`,
`ReadFlagsOptions`, `OutputOptions`, `RenderContextOptions`, `HelpOptions`,
`HelpTheme`, `CompletionOptions`, `CompletionRegistrationOptions`, and
`JsonSchemaOptions`.
Per-call output options: `SpinnerOptions`, `ProgressOptions`, `TableOptions`, and
`TableColumn`.
Value options accepted by the flag and arg factories: `StringConstraints`,
`NumberConstraints`, `PathFlagOptions`, `UrlFlagOptions`, and `DateFlagOptions`.
All five describe the value rather than flag syntax, so `flag.path()` and
`arg.path()` take one `PathFlagOptions`, `flag.url()` and `arg.url()` take one
`UrlFlagOptions`, and `flag.date()` and `arg.date()` take one `DateFlagOptions`.
Prompt configuration: `PromptConfig`, `PromptConfigBase`, `InputPromptConfig`,
`ConfirmPromptConfig`, `SelectPromptConfig`, `MultiselectPromptConfig`, and the
choice entry `SelectChoice`.
Error constructor options: `CLIErrorOptions`, `ParseErrorOptions`, and
`ValidationErrorOptions`.
Testkit inputs: `TestAdapterOptions`, `TestPrompterOptions`, the queued answer
type `TestAnswer`, and the cancellation sentinel value `PROMPT_CANCEL`.
Every field is optional apart from the discriminants and identifiers each type
needs: `code` on `CLIErrorOptions`, `ParseErrorOptions`, and
`ValidationErrorOptions`; `message` on `PromptConfigBase` and `kind` on each
concrete prompt config; `format` on the `'json'` and `'text'` arms of
`TableOptions`; `type` on the directory arm of `PathFlagOptions`; `value` on
`SelectChoice`; and `key` on `TableColumn`. Minor releases may add optional
fields. They never add a required field, remove a field, or change what an
existing field means. Defaults are part of the contract and change only in a
major release.
`CLIExecuteOptions` and `CLIRunOptions` sit on top of `RunOptions`.
`CLIExecuteOptions` is the process-free surface with every input injected
explicitly. `CLIRunOptions` adds the runtime adapter that `.run()` uses to reach
the process. `RunCommandOptions` extends `RunOptions` the same way for
`runCommand()`, adding the `builtins` state the testkit harness mirrors from
the CLI a command is registered on. The field stays off `RunOptions` itself,
where the CLI schema is authoritative.
`ReadFlagsOptions` takes an optional type parameter, the definitions record
`readFlags()` is evaluating, which types its `onSources` receiver against the
caller's own flag names. It defaults to `FlagMap`, so `ReadFlagsOptions` written
without an argument keeps meaning what it did.
`HelpTheme` is the one entry whose every member is required. A theme reaches the
framework as the `Partial` returned by a `HelpThemeFactory`, so a new
semantic role added to `HelpTheme` in a minor release leaves existing factories
compiling. The factory form is the supported way to supply one.
## Transparent input definitions
Plain object types a consumer writes by hand or a code generator emits.
`FlagDefinition`, `ArgDefinition`, `CommandDefinition`, `CLIDefinition`,
`ConfigSettingsDefinition`, and `BuiltinsConfig`; the shared bases
`FlagDefinitionBase` and `ArgDefinitionBase`; the per-kind variants
`StringFlagDefinition`, `NumberFlagDefinition`, `BooleanFlagDefinition`,
`EnumFlagDefinition`, `ArrayFlagDefinition`, `CustomFlagDefinition`,
`CountFlagDefinition`, `KeyValueFlagDefinition`, `StringArgDefinition`,
`NumberArgDefinition`, `BooleanArgDefinition`, `EnumArgDefinition`,
`CustomArgDefinition`, `KeyValueArgDefinition`; the
kind-indexed maps
`FlagDefinitionByKind` and `ArgDefinitionByKind`; the override forms
`FlagDefinitionOverrides` and `ArgDefinitionOverrides`; the entry type
`CommandArgEntryDefinition`; the values a definition embeds, `PathChecks`,
`FlagNegation`, `StdinOptions`, `SplitOptions`, `SplitSetting`,
`SourceSplitBinding`, `SplitBinding`, `CommandExample`, and its `ExampleCommand`
field type; and the consumer input data types `PackageJsonData` and
`PackageRepository`.
These have no brand, and structural construction is the point. They take the
same additive contract as consumer input options. Optional fields may arrive in
a minor release, required fields only in a major.
`PathChecks`, `FlagNegation`, and `CommandExample` are stored verbatim on the
built schema, so the same type serves as consumer input and as a member of a
sealed value. The input contract is the one that governs them. `StdinOptions`
splits the two roles instead: a caller writes the partial form and the schema
stores the fully populated `StdinBinding`, which takes the same input contract.
The binding has three axes, `when`, `consume`, and `trim`. Every
`StdinOptions` member is optional, so a fourth axis may arrive in a minor
release. Every `StdinBinding` member is required, so the stored form gains one
only in a major.
`SplitOptions` splits them the same way: a caller writes the per-source settings
and the schema stores the CLI delimiter on `separator` and the rest on
`SourceSplitBinding`, which `SplitBinding` resolves with the defaults filled in.
`PathChecks`
appears on both `StringFlagDefinition` and `StringArgDefinition`, alongside
`StringConstraints` on each and `valueHint` on `FlagDefinitionBase` and
`ArgDefinitionBase`.
Two properties are load-bearing for code generators. Definitions are recursive
and accept either rung, so `CommandDefinition.flags` takes a `FlagDefinition` or
an already-built `FlagSchema`, and `CLIDefinition.commands` takes a
`CommandDefinition` or a `CommandSchema`. A generator can therefore emit one
complete structural tree and hand it to `createCLISchema()`. Second, each
definition is a discriminated union indexed by `kind`, so a field belonging to
another kind is unrepresentable rather than silently ignored.
`BuiltinsConfig` is the input to `CLIBuilder.builtins()` and to the `builtins`
field of `CLIDefinition`. Its keys are the members of `BuiltinName`, all
optional, each taking a `BuiltinMode`. An absent key leaves that built-in
`'on'`, so `{}` and an omitted `builtins` field mean the same thing. A new key
arrives with a new `BuiltinName` member, which is a major release under
[Closed unions](#closed-unions-and-discriminated-results).
Adding a member to `FlagKind` or `ArgKind` widens these unions and is a major
release. See [Closed unions](#closed-unions-and-discriminated-results).
## Implementer ports
Interfaces designed to be implemented outside the framework.
`RuntimeAdapter` (from `@kjanat/dreamcli/runtime`) with its `TerminalSize` return
type, `PromptEngine`, `ConfigAdapter`, `PackageJsonAdapter`, `FormatLoader`,
`ReadFn`, and `WriteFn`.
The required surface is frozen within a major version. New capability arrives as
an optional member, so an existing implementation keeps compiling. A redesign
ships as a new interface with the old one supported until the next major.
`ConfigAdapter` and `PackageJsonAdapter` are subsets of `RuntimeAdapter` built
with `Pick`. `PackageJsonAdapter` requires `readFile` and `cwd`.
`ConfigAdapter` requires those plus `configDir`, and takes `userConfigDirs` and
`systemConfigDirs` as optional members. Adding a required key to either subset
breaks every implementer that passes a hand-built object, so the required key
sets are frozen alongside the port's own required surface. An optional member
added to `RuntimeAdapter` reaches these subsets only if the subset picks it.
`TerminalSize` is built by the adapter implementation and read by the framework.
Its two members are required and frozen with the port.
`NodeProcess`, `DenoNamespace`, and `GlobalForDetect` (all from
`@kjanat/dreamcli/runtime`) describe the host objects the built-in adapters
read, and each is the optional parameter of the function that reads it:
`createNodeAdapter(proc?)`, `createDenoAdapter(ns?)`, and
`detectRuntime(globals?)`. A caller supplies one to inject a host, so they take
the implementer-port contract in the reading direction: the framework reads
members it names, and a new required member is a major release. They describe
what the framework needs from a host, not the full API of any runtime.
The function-typed ports are the handler and parser signatures a consumer writes
and the framework calls: `ActionHandler`, `DeriveHandler`, `MiddlewareHandler`,
`InteractiveResolver` with its `InteractiveResult` return type, `ArgParseFn`,
`FlagParseFn`, and `HelpThemeFactory`. Their parameter and return types are
frozen within a major version. The payload object a handler receives grows
instead, under
[Framework-produced callback payloads](#framework-produced-callback-payloads).
A handler that destructures the members it needs keeps compiling when a new one
arrives.
The supported low-level output seam is the injected `stdout` and `stderr` writer
pair on `OutputOptions`, both of type `WriteFn`. Reimplementing `Out` is sealed
off and is not a supported extension point.
## Externally governed protocols
`StandardSchemaV1` and its namespace members `StandardSchemaV1Props`,
`StandardSchemaV1Result`, `StandardSchemaV1SuccessResult`,
`StandardSchemaV1FailureResult`, `StandardSchemaV1Issue`,
`StandardSchemaV1PathSegment`, `StandardSchemaV1Options`, and
`StandardSchemaV1Types`.
DreamCLI vendors these as types only, mirroring the specification at
[standardschema.dev](https://standardschema.dev), so validators from zod,
valibot, arktype, and any other conforming library can be passed to
`flag.custom()` and `arg.custom()` without a runtime dependency.
The contract belongs to that specification. DreamCLI tracks it and does not
extend it. When the upstream spec revises, the vendored types follow, and the
release carrying that change is labelled by the impact the upstream revision has
on DreamCLI consumers.
`Colors` takes the same treatment. It is the palette type behind `out.color` and
`HelpThemeFactory`, re-exported from
[ansispeck](https://www.npmjs.com/package/ansispeck) so a handler can type a
color-aware helper without adding that dependency. Its shape is governed by the
ansispeck version this package depends on, and a DreamCLI release that changes
which palette members exist is labelled by the impact on consumers.
## Structural consumer configuration
`CLIPlugin` and `CLIPluginHooks`.
The consumer constructs these, the framework consumes them. Every hook on
`CLIPluginHooks` is optional, so a new lifecycle hook can arrive in a minor
release without breaking an existing plugin. Adding a required field to
`CLIPlugin` is a major release.
`plugin()` is the recommended constructor. It freezes the hooks object and fills
`name`, and it keeps a plugin compiling when `CLIPlugin` gains a field that the
factory can default. A hand-built object literal has to spell every required
field itself.
The payload types the hooks receive are covered under
[Framework-produced callback payloads](#framework-produced-callback-payloads).
## Closed unions and discriminated results
The kind and mode unions `Verbosity`, `Shell`, `ArgKind`, `FlagKind`,
`PromptKind`, `BuiltinName`, `BuiltinMode`, `ArgPresence`, `FlagPresence`,
`DuplicatePolicy`, `DuplicateKeys`, `SplitFormat`, `StdinWhen`,
`StdinConsume`, `Fallback`, `TableFormat`, `TableStream`, `ParseErrorCode`,
`ValidationErrorCode`, and `Runtime` (from `@kjanat/dreamcli/runtime`).
The discriminated record `ResolutionProvenance`, whose `stage` narrows to the
source that produced a value.
The discriminated results `ActivityEvent`, `Token`, `PromptResult`,
`SplitPolicy`, `NumberConstraintViolation`, `StringConstraintViolation`, and
`ConfigDiscoveryResult` with its members `ConfigFound` and `ConfigNotFound`.
The runtime value forms `SHELLS` and `RUNTIMES` (the latter from
`@kjanat/dreamcli/runtime`), which are the frozen tuples behind `Shell` and
`Runtime`.
These are closed. A consumer may switch exhaustively over them and rely on the
compiler to flag an unhandled case. Adding a variant is a breaking change and
ships in a major release. The same rule covers the value forms, which tooling
iterates to emit one artifact per member.
`ValidationErrorCode` gained `MISSING_STDIN` in 4.0 under that rule: a `-` typed
beside other occurrences of a collection with nothing piped now carries its own
code instead of `REQUIRED_FLAG` or `REQUIRED_ARG`. It also reaches the open
[`ErrorCode`](#open-unions) union, so a consumer with a fallback branch needed no
change.
`ResolutionProvenance` narrows on `stage`, and the `'cli'` arm is two records:
one for a typed token and one for an explicit `-`, which carries `via` and
`trigger`. Both stay at `stage: 'cli'` because both keep CLI precedence, so a
reader that only asks about precedence tests `stage` and a reader that cares
about the pipe tests `via`. Adding a stage is a breaking change; the runtime
tuples `STDIN_WHENS` and `STDIN_CONSUMES` behind `StdinWhen` and `StdinConsume`
are deliberately not exported from any entrypoint, so the types are the contract
and the arrays stay the framework's own.
```ts twoslash
import type { Shell } from '@kjanat/dreamcli';
// ---cut---
function scriptFileName(shell: Shell): string {
switch (shell) {
case 'bash':
return 'mycli.bash';
case 'zsh':
return '_mycli';
case 'fish':
return 'mycli.fish';
case 'powershell':
return 'mycli.ps1';
default:
return shell satisfies never;
}
}
```
`ConfigDiscoveryResult` narrows on its `found` discriminant. The `false` branch
carries no other members, so reading `result.path` before narrowing is a type
error rather than a runtime `undefined`.
## Open unions
`ErrorCode`.
`ErrorCode` is `ParseErrorCode | ValidationErrorCode | (string & {})`. The
literal members drive autocomplete, and the `string` arm keeps the union open.
The framework emits codes outside the two closed unions (`CONFIG_NOT_FOUND`,
`CONFIG_PARSE_ERROR`, `CONFIG_UNKNOWN_FORMAT`, `DUPLICATE_COMMAND`,
`DUPLICATE_DEFAULT`, `RESERVED_FLAG`, `NO_ACTION`, and others), and may add
codes in any release including a patch.
Handle it with a fallback branch rather than an exhaustive switch:
```ts twoslash
import type { ErrorCode } from '@kjanat/dreamcli';
// ---cut---
function exitCodeFor(code: ErrorCode): number {
if (code === 'UNKNOWN_FLAG' || code === 'MISSING_VALUE') return 2;
if (code === 'REQUIRED_FLAG' || code === 'REQUIRED_ARG') return 2;
return 1;
}
```
A consumer may also emit its own codes through `CLIError`, which is why the
union stays open rather than growing a variant per framework release.
## Framework-produced callback payloads
The bags handed to a consumer callback: `ActionParams`, `DeriveParams`,
`MiddlewareParams`, `InteractiveParams`, `BeforeParseParams`,
`PluginCommandContext`, `ResolvedCommandParams`, `ExampleMeta`, and
`ResolvedPromptConfig` with its members `ResolvedSelectPromptConfig` and
`ResolvedMultiselectPromptConfig`.
The values a framework call returns: `SpinnerHandle`, `ProgressHandle`,
`ResolveResult` with its `ResolutionProvenanceRecord`, `DeprecationWarning`,
`Middleware`, and `CapturedOutput` (from `@kjanat/dreamcli/testkit`).
The provenance bag a handler receives, `InputSources`, and the per-surface map
`SourcesOf` behind it. Both are keyed by the caller's own flag and arg names, so
a new stage arrives as a new `ResolutionProvenance` member rather than as a new
key here.
The normalized state a sealed schema carries: `ResolvedManifestSettings`,
`HelpLinks`, and `CommandArgEntry`.
The framework builds all of these. They carry no brand, so a test can assemble
one, but constructibility is not part of the contract. A minor release may add a
required readonly member. Every reader keeps compiling; an object literal that
spells the type exhaustively does not.
Destructure the members a callback needs and let the rest pass through. Code
that builds one of these types by hand should treat that as a
version-pinned convenience rather than a supported surface.
`ResolvedManifestSettings` is the normalized manifest-discovery state stored on
`CLISchema.packageJsonSettings`. The consumer-facing input form is
`ManifestSettings`, which is a
[consumer input option](#consumer-input-options).
`Middleware` comes from `middleware()` and is passed straight to
`CommandBuilder.middleware()`. Its `_handler` and `_output` members belong to the
[internal surface](#internal-surface), so the value is opaque in practice.
`SpinnerHandle` and `ProgressHandle` gain lifecycle methods the same way these
types gain members. Calling code keeps working; a hand-written test double that
implements the interface exhaustively does not.
## Closed constructible DTOs
`RunResult`, `CommandMeta`, and `ParseResult`.
The framework produces these, and consumers construct them on purpose. None
carries a brand and every member is required, so a hand-built object is
assignable. `RunResult` comes out of `runCommand()` and `.execute()`, and a
test fixture standing in for a run builds one. `CommandMeta` reaches a handler
as `meta`, and the [planner contract](/reference/planner-contract) carries it
on the execution plan, so tooling reproducing that seam builds one.
`ParseResult` comes out of `parse()` and goes into `resolve()`, so tooling
driving the low-level pipeline builds one directly.
Every member is required, and the set is closed. Adding a member is a breaking
change and ships in a major release. That is the price of guaranteeing
constructibility in both directions.
Consumers that only read a `RunResult` should still narrow with `Pick` where a
helper needs part of it, which keeps the helper honest about what it depends on.
## Serialized formats
`DefinitionDocumentV1` and its alias `DefinitionDocument`;
`CommandDefinitionDocumentV1` and its alias `CommandDefinitionDocument`; the
fragment types `CommandDefinitionFragmentV1`, `FlagDefinitionFragmentV1`,
`ArgDefinitionFragmentV1`, `ExampleDefinitionFragmentV1`,
`FlagNegationFragmentV1`, `FlagPathChecksFragmentV1`,
`FlagStringConstraintsFragmentV1`, `PromptChoiceFragmentV1`,
`PromptDefinitionFragmentV1`, `SourceSplitFragmentV1`, `SplitPolicyFragmentV1`,
`StdinBindingFragmentV1`, and `ArgElementFragmentV1`; and the error envelope
`CLIErrorJSON`.
`ArgElementFragmentV1` is the value half of a positional entry, which is what an
entries argument's `elementSchema` holds: everything `ArgDefinitionFragmentV1`
carries except the `name` a position supplies. The meta-schema hoists it as
`$defs.argElement` and the named `$defs.arg` spreads the same properties, so the
two cannot drift. A definition document never carries provenance: that is what
one invocation did, not what a schema declares.
`FlagStringConstraintsFragmentV1` and `FlagPathChecksFragmentV1` are the value
fragments of both `FlagDefinitionFragmentV1` and `ArgDefinitionFragmentV1`. Their
names are frozen with the version 1 format and carry no claim about which of the
two embeds them; the meta-schema hoists one definition per fragment and both
parents `$ref` it.
`StdinBindingFragmentV1` carries `when`, `consume`, and `trim`, all three
required and all three always written, so a reader never has to know the
builder's defaults. The meta-schema lists them under `$defs.stdin` with
`additionalProperties: false` and the same three in `required`.
These values leave the process. The package version does not govern their shape.
`schemaVersion` does.
Every standalone definition document carries `schemaVersion: 1`. Fragments carry no
`schemaVersion` and inherit the version of the document they sit in. A change
that breaks a reader of version 1 increments the format to `schemaVersion: 2`
and ships as `DefinitionDocumentV2` alongside `DefinitionDocumentV1`, whatever
the package major happens to be at that time. Additive changes that a version 1
reader can ignore stay at version 1.
```ts twoslash
import { createCLISchema } from '@kjanat/dreamcli';
import { generateSchema } from '@kjanat/dreamcli/json-schema';
// ---cut---
const document = generateSchema(createCLISchema({ name: 'mycli', version: '1.0.0' }));
console.log(document.schemaVersion);
// 1
console.log(document.$schema);
// https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json
```
The canonical meta-schema URL is
`https://dreamcli.kjanat.dev/schemas/definition/v1.schema.json`. It is permanent.
Once published it keeps serving the version 1 meta-schema for as long as the
domain resolves, and it is never repointed at a later format. A version 2 format
would be served at `/schemas/definition/v2.schema.json`.
Two mirrors carry identical bytes for offline and local-first workflows:
- `./node_modules/@kjanat/dreamcli/dreamcli.schema.json`
- `https://cdn.jsdelivr.net/npm/@kjanat/dreamcli/dreamcli.schema.json`
The `definitionMetaSchema` export is the same document as a value, for tooling
that validates definition documents without a network round-trip.
`CLIErrorJSON` is the `--json` error envelope. `name`, `code`, `message`, and
`exitCode` are always present; `suggest` and `details` appear when set. Its
`code` field is the open [`ErrorCode`](#open-unions) union, so a consumer
reading the envelope needs a fallback branch.
`generateInputSchema()` produces standard JSON Schema draft 2020-12, typed as
`InputSchemaDocument`, `InputSchemaBranch`, and `InputSchemaProperty`. That
output is governed by the JSON Schema draft it declares rather than by
`schemaVersion`, and it sits outside the definition-format family.
## Classes and functions
The callable surface, plus the exported type-level operators and constants in the
two subsections below.
Exported classes: `CLIBuilder`, `CommandBuilder`, `FlagBuilder`, `ArgBuilder`,
`CLIError`, `ParseError`, `ValidationError`, and `ExitError` (from
`@kjanat/dreamcli/runtime`).
Exported functions, by area:
- Program construction: `cli`, `command`, `group`, `flag`, `arg`, `middleware`,
`plugin`.
- Schema normalization: `createFlagSchema`, `createArgSchema`,
`createCommandSchema`, `createCLISchema`.
- Low-level pipeline: `tokenize`, `parse`, `resolve`, `readFlags`, `formatHelp`,
`resolveRenderContext`, `resolvePromptConfig`, `includesBeforeSeparator`,
`stripBeforeSeparator`, `getFlagNegatedName`, `resolveExampleCommand`,
`wasExplicit`.
- Serialization, from `@kjanat/dreamcli/json-schema`: `generateSchema`,
`generateCommandSchema`, `generateInputSchema`.
- Discovery, from `@kjanat/dreamcli/config`: `discoverConfig`,
`discoverManifest`, `inferCliName`, `buildConfigSearchPaths`, `configFormat`,
`packageRepositoryUrl`.
- Output and terminal: `createOutput`, `osc8`, `visibleWidth`, and from
`@kjanat/dreamcli/prompt` `createTerminalPrompter`.
- Completion generation, from `@kjanat/dreamcli/completion`:
`generateCompletion`, `generateBashCompletion`, `generateZshCompletion`,
`generateFishCompletion`, `generatePowerShellCompletion`.
- Error narrowing: `isCLIError`, `isParseError`, `isValidationError`.
- Host integration: `isMainModule`, and from `@kjanat/dreamcli/runtime`
`createAdapter`, `createNodeAdapter`, `createDenoAdapter`, `detectRuntime`.
- Testkit, from `@kjanat/dreamcli/testkit`: `runCommand`, `createCaptureOutput`,
`createTestPrompter`, `createTestAdapter`.
The [API reference](/reference/api) lists their signatures.
`flag` and `arg` are callable namespaces typed by `FlagFactory` and `ArgFactory`.
A new factory method on either is a new function and ships in a minor release.
`FlagFactory` holds `string`, `number`, `boolean`, `enum`, `array`, `custom`,
`url`, `path`, `date`, `duration`, `bytes`, `count`, and `keyValue`.
`ArgFactory` holds `string`, `number`, `boolean`, `enum`, `custom`, `keyValue`,
`url`, `path`, `date`, `duration`, and `bytes`; the two it does not carry,
`array` and `count`, are documented under
[what the arg factory does not have](/guide/arguments#flag-only-surface).
Builder modifier methods follow the same rule. `FlagBuilder` and `ArgBuilder`
both carry `.int()`, `.min()`, `.max()`, and `.finite()` on number-kind
builders, and `.nonEmpty()`, `.minLength()`, `.maxLength()`, and `.pattern()` on
string-kind builders. A modifier guarded by a `this` parameter is callable only
on the kinds it names, so widening one to another kind is additive and ships in
a minor release.
A function may gain optional trailing parameters in a minor release, and its
return type may gain members subject to the category that return type belongs
to. Removing an overload, adding a required parameter, reordering parameters, or
narrowing a return type is a major release. Throwing a new error code from an
existing call path is a minor release, since `ErrorCode` is open.
Class constructors follow the same rule as functions, with one exception.
`CLIBuilder` has no public constructor. `cli()` builds an executable program and
`createCLISchema()` builds a description; there is no third way to obtain one.
### Type-level operators
`InferFlag`, `InferFlags`, `InferArg`, `InferArgs`, `InferStandardInput`,
`InferStandardOutput`, `ResolvedValue`, `ResolvedArgValue`, `ArgDefaultValue`,
`WithPresence`, `WithArgPresence`, `WithVariadic`,
`WithoutElementEligibility`, `WithoutArgElementEligibility`, the builder state
types `FlagConfig`,
`ArgConfig`, `StringElementConfig`, and `StringArgElementConfig`, the element
configs a collection factory assumes when given no element builder, and
`FlagMap`, the record of flag builders `readFlags()` evaluates.
These compute a type from another type. Most code reaches them through inference
and never names one. `InferFlags` in an extracted handler signature
is the common exception.
The mapping each performs is frozen within a major version. The type it produces
carries the contract of whatever category that type belongs to. `FlagConfig` and
`ArgConfig` are the compile-time state threaded through a builder chain, and a
tracked property may be added to either in a minor release, since builders are
obtained from `flag` and `arg` rather than written by hand. `ArgConfig` gained
`elementEligible` in 4.0 under that rule, which is what lets `arg.keyValue()`
accept an element builder and refuse one already described as a positional.
`FlagMap` constrains the type parameter of `readFlags()` rather than computing a
type. It is a record of `FlagBuilder` values keyed by flag name, and the caller
writes the record. Accepting more as a member is a minor release; accepting less
is a major.
### Constants
`DREAMCLI_VERSION` and `DREAMCLI_REVISION`, both from
`@kjanat/dreamcli/version`, identify the framework build for diagnostics and bug
reports. They are typed `string` and their values change every release. They are
unrelated to the app version configured through `cli().version(...)`.
`definitionMetaSchema`, from `@kjanat/dreamcli/json-schema`, is the definition
meta-schema document as a value; `DEFINITION_SCHEMA_URL` and
`DEFINITION_SCHEMA_VERSION` name the hosted copy and the format version it
describes. `SHELLS` is the tuple of supported completion targets.
## Internal surface
Members prefixed with an underscore (`_from`, `_subcommands`, `_executionSteps`,
`_flags`, `_args`, `_ctx`, `_config`, `_output`), types named `Internal*`
(`InternalRunOptions`, `InternalCLIExecuteOptions`), and the brand symbols
behind sealed values exist for the framework's own layering and for in-repo
tests. They may change or disappear in any release, including a patch. The
`@internal` TSDoc tag marks the same boundary on individual members.
The compiled execution graph is internal in the strongest available sense.
`CompiledCLI` and `CompiledCommand` are not exported from any entrypoint and do
not appear in the generated declarations. A `CLIBuilder` holds its compiled
graph in module-private state, which is why `cli(...).schema` describes the
program while only the builder can execute it.
---
# Support Matrix
Source: https://dreamcli.kjanat.dev/raw/reference/support-matrix
# Support Matrix
This page is the audited truth source for DreamCLI's current support surface.
Use it for questions like:
- what is fully supported today;
- what is intentionally deferred;
- where the supporting code and tests live;
- which product claims are already true versus planned next.
Last audited: `2026-04-03`
## Status Meanings
| Status | Meaning |
| ------------ | -------------------------------------------------------------------------------------------------- |
| Supported | Shipped today and backed by code, tests, and docs. |
| Deferred | Intentionally not supported yet, or currently represented only by explicit not-supported behavior. |
| Experimental | Present in some form, but not yet stable enough to recommend as part of the supported surface. |
## Important Scope Note
Some TypeScript surfaces can be broader than the supported surface.
Treat this matrix as the current truth source whenever implementation and planned scope could diverge.
For completions specifically, `Shell` and `SHELLS` now match the shipped shell set.
## Execution and Semantics
| Claim | Status | Evidence | Notes |
| ---------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Schema-first parse -> resolve -> execute flow | Supported | [CLI Semantics](/guide/semantics), [`runCommand()`](/reference/testkit), [`src/core/testkit/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/index.ts), [`src/core/cli/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/index.ts), [`src/core/execution/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/execution/index.ts) | The shared executor now owns the canonical command execution flow; both CLI and testkit delegate to it directly. |
| Root help, version, default-command, and nested command behavior | Supported | [CLI Semantics](/guide/semantics), [`src/core/cli/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/index.ts), [`src/core/cli/cli-dispatch.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/cli-dispatch.test.ts) | Planner extraction is planned, not a current user-facing gap. |
| Typed derive and middleware execution | Supported | [Middleware guide](/guide/middleware), [`src/core/schema/command.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/schema/command.ts), [`src/core/testkit/middleware-context-e2e.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/middleware-context-e2e.test.ts) | Execution is centralized in the shared executor while preserving the existing typed builder surface. |
| Explicit planner / executor internal boundary | Deferred | [`specs/dreamcli-re-foundation-prd.md`](https://github.com/kjanat/dreamcli/blob/master/specs/dreamcli-re-foundation-prd.md) | Tracked by `planner-contracts`, `scaffold-core-execution`, and related tasks. |
## Runtime Support
| Claim | Status | Evidence | Notes |
| ---------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Node.js >= 22.22.2 runtime support | Supported | [Runtime Support](/guide/runtime), [`src/runtime/node.ts`](https://github.com/kjanat/dreamcli/blob/master/src/runtime/node.ts), [`docs/reference/runtime.md`](/reference/runtime) | This is the primary adapter implementation. |
| Bun >= 1.3 runtime support | Supported | [Runtime Support](/guide/runtime), [`src/runtime/node.ts`](https://github.com/kjanat/dreamcli/blob/master/src/runtime/node.ts), [`docs/reference/runtime.md`](/reference/runtime) | Bun uses the Node adapter (Node-compatible `process`); detection reports `'bun'`. |
| Deno >= 2.6.0 runtime support | Supported | [Runtime Support](/guide/runtime), [`src/runtime/deno.ts`](https://github.com/kjanat/dreamcli/blob/master/src/runtime/deno.ts), [`docs/reference/runtime.md`](/reference/runtime) | Deno support is permission-aware and published through JSR. |
| Cross-platform CI coverage for all supported runtimes and shells | Deferred | [`docs/reference/runtime.md`](/reference/runtime), [`/.github/workflows/ci.yml`](https://github.com/kjanat/dreamcli/blob/master/.github/workflows/ci.yml) | Runtime support is shipped, but broader platform coverage is still planned. |
## Input Sources and Resolution
| Claim | Status | Evidence | Notes |
| --------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Flag precedence `CLI -> stdin -> env -> config -> prompt -> default` | Supported | [CLI Semantics](/guide/semantics), [Config Files](/guide/config), [Interactive Prompts](/guide/prompts), [`src/core/resolve/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/index.ts) | This is the current supported behavior. Re-foundation work is about internal clarity, not removing the chain. |
| Arg precedence `CLI -> stdin -> env -> config -> prompt -> default` | Supported | [CLI Semantics](/guide/semantics), [`src/core/resolve/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/index.ts) | Only inputs that opt into a source read it; both surfaces declare the same set. |
| Built-in JSON config discovery | Supported | [Config Files](/guide/config), [`src/core/config/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/config/index.ts) | JSON is the only built-in format by design. |
| Custom YAML / TOML / other config formats via `configFormat()` | Supported | [Config Files](/guide/config), [`docs/reference/main.md`](/reference/main) | Additional formats require explicit parser wiring. They are not built-in. |
| Per-flag prompts and command-level interactive prompts | Supported | [Interactive Prompts](/guide/prompts), [`src/core/schema/prompt.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/schema/prompt.ts), [`src/core/resolve/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/resolve/index.ts) | Prompts are integrated into the resolution chain. |
| Automatic prompts in non-interactive stdin contexts | Deferred | [Interactive Prompts](/guide/prompts), [CLI Semantics](/guide/semantics) | DreamCLI intentionally skips prompting when `stdinIsTTY` is false. This is a design choice, not a bug. |
## Completions
| Claim | Status | Evidence | Notes |
| ------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Bash completion generation | Supported | [Shell Completions](/guide/completions), [`src/core/completion/shells/bash.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/shells/bash.ts), [`src/core/completion/completion.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/completion.test.ts) | Included in the supported shell set today. |
| Zsh completion generation | Supported | [Shell Completions](/guide/completions), [`src/core/completion/shells/zsh.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/shells/zsh.ts), [`src/core/completion/completion.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/completion.test.ts) | Included in the supported shell set today. |
| Fish completion generation | Supported | [Shell Completions](/guide/completions), [`src/core/completion/shells/fish.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/shells/fish.ts), [`src/core/completion/completion.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/completion.test.ts) | Included in the supported shell set today. |
| PowerShell completion generation | Supported | [Shell Completions](/guide/completions), [`src/core/completion/shells/powershell.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/shells/powershell.ts), [`src/core/completion/completion.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/completion.test.ts) | Included in the supported shell set today. |
| Built-in `.completions()` helper on `cli()` | Supported | [Shell Completions](/guide/completions), [`src/core/completion/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/completion/index.ts), [`src/core/cli/cli-completions.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/cli/cli-completions.test.ts) | The user-facing helper, `Shell` union, and `SHELLS` tuple now advertise the same four shipped shells. |
## Output
| Claim | Status | Evidence | Notes |
| --------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Text, warning, error, JSON, and table output | Supported | [Output](/guide/output), [Output Contract](/reference/output-contract), [`docs/reference/main.md`](/reference/main), [`src/core/output/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/output/index.ts) | Core output surface is shipped and documented. |
| Spinner and progress support with testable activity events | Supported | [Output](/guide/output), [Output Contract](/reference/output-contract), [Testing](/guide/testing), [`src/core/output/activity.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/output/activity.ts), [`src/core/testkit/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/index.ts) | Activity behavior is part of the supported output channel. |
| TTY-aware suppression of decorative output in piped / JSON mode | Supported | [Output](/guide/output), [Output Contract](/reference/output-contract), [`src/core/output/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/output/index.ts), [`src/core/testkit/output-e2e.test.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/output-e2e.test.ts) | Re-foundation work clarifies the internal policy boundary without changing support. |
## Testing Surface
| Claim | Status | Evidence | Notes |
| -------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| In-process command testing via `runCommand()` | Supported | [Testing](/guide/testing), [`docs/reference/testkit.md`](/reference/testkit), [`src/core/testkit/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/index.ts) | This is one of DreamCLI's strongest first-class features. |
| Injected env/config/stdin/prompt/output test control | Supported | [Testing](/guide/testing), [`docs/reference/testkit.md`](/reference/testkit), [`src/core/testkit/index.ts`](https://github.com/kjanat/dreamcli/blob/master/src/core/testkit/index.ts) | Current support is broad; docs depth is still improving. |
| Consolidated testing guidance for advanced failure modes | Supported | [Testing](/guide/testing), [Troubleshooting](/guide/troubleshooting), [`specs/dreamcli-re-foundation-prd.md`](https://github.com/kjanat/dreamcli/blob/master/specs/dreamcli-re-foundation-prd.md) | The troubleshooting guide now covers the common prompt, stdin, config, and output-mode failure cases serious adopters hit first. |
## Docs and Learning Surfaces
| Claim | Status | Evidence | Notes |
| -------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Authored guides and reference overviews | Supported | [Guide](/guide/getting-started), [API Reference](/reference/api) | The current docs backbone is real and useful. |
| Examples index and per-example pages | Supported | [/examples/](/examples/), [`docs/examples/index.md`](https://github.com/kjanat/dreamcli/blob/master/docs/examples/index.md), [`examples/*.ts`](https://github.com/kjanat/dreamcli/tree/master/examples) | The examples section is source-backed and built with `bun run docs:build`. |
| IDE-like hover on example pages | Supported | [/examples/](/examples/), [`docs/reference/example-hover-prototype.md`](/reference/example-hover-prototype), [`docs/.vitepress/config.ts`](https://github.com/kjanat/dreamcli/blob/master/docs/.vitepress/config.ts) | Hover is shipped on example source blocks; guides and shell transcripts stay plain text. |
| Public changelog page in reference | Supported | [`docs/reference/changelog.md`](/reference/changelog), [`CHANGELOG.md`](https://github.com/kjanat/dreamcli/blob/master/CHANGELOG.md) | The docs route mirrors the repository changelog directly. |
| API index and symbol pages | Supported | [`docs/reference/api.md`](/reference/api), `docs/reference/symbols/**/*.md`, [`specs/dreamcli-re-foundation-prd.md`](https://github.com/kjanat/dreamcli/blob/master/specs/dreamcli-re-foundation-prd.md) | Symbol pages are generated at build time from the checked-in docs sources. |
| Consolidated rationale and limitations pages | Supported | [Architecture Rationale](/guide/rationale), [Limitations And Workarounds](/guide/limitations), [Semantic Delta Log](/reference/semantic-delta-log), [`specs/dreamcli-re-foundation-prd.md`](https://github.com/kjanat/dreamcli/blob/master/specs/dreamcli-re-foundation-prd.md) | These pages explain the settled architecture trade-offs and current edges as the baseline for adoption guidance. |
| Migration and troubleshooting guides | Supported | [Migration And Adoption](/guide/migration), [Troubleshooting](/guide/troubleshooting), [Limitations And Workarounds](/guide/limitations), [`specs/dreamcli-re-foundation-prd.md`](https://github.com/kjanat/dreamcli/blob/master/specs/dreamcli-re-foundation-prd.md) | The narrative docs set now covers evaluation, phased adoption, and likely real failure modes without overstating unsupported surfaces. |
## Related Pages
- [API Reference](/reference/api)
- [Semantic Delta Log](/reference/semantic-delta-log)
- [CLI Semantics](/guide/semantics)
- [Shell Completions](/guide/completions)
- [Runtime Support](/guide/runtime)
- [Testing](/guide/testing)
---
# @kjanat/dreamcli/testkit
Source: https://dreamcli.kjanat.dev/raw/reference/testkit
# @kjanat/dreamcli/testkit
Test utilities for running commands in-process.
```ts twoslash
import {
runCommand,
createCaptureOutput,
createTestPrompter,
createTestAdapter,
PROMPT_CANCEL,
} from '@kjanat/dreamcli/testkit';
```
## `runCommand(command, argv, options?)`
Run a command in-process and return a `RunResult`.
```ts twoslash
import { greet } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import { runCommand } from '@kjanat/dreamcli/testkit';
const result = await runCommand(greet, ['Alice', '--loud']);
```
### Parameters
| Parameter | Type | Description |
| --------- | ---------------- | -------------------------------- |
| `command` | `CommandBuilder` | The command to run |
| `argv` | `string[]` | Simulated command-line arguments |
| `options` | `RunOptions` | Optional configuration |
### RunOptions
| Option | Type | Description |
| ----------- | ------------------------------------- | --------------------------------------- |
| `env` | `Record` | Environment variables |
| `config` | `Record` | Config file values |
| `stdinData` | `string \| null` | Data to pipe to process stdin for tests |
| `answers` | `unknown[]` | Prompt answers in order |
| `prompter` | `PromptEngine` | Custom prompt handler |
| `jsonMode` | `boolean` | Simulate `--json` mode |
| `help` | `HelpOptions` | Help formatting options |
| `verbosity` | `Verbosity` | Output verbosity |
| `isTTY` | `boolean` | Simulate TTY connection |
### RunResult
| Field | Type | Description |
| ---------- | ----------------------- | ---------------------------------------------- |
| `exitCode` | `number` | Process exit code |
| `stdout` | `string[]` | Captured stdout lines |
| `stderr` | `string[]` | Captured stderr lines |
| `error` | `CLIError \| undefined` | Structured error, `undefined` when none thrown |
| `activity` | `ActivityEvent[]` | Spinner/progress events |
`exitCode` can be non-zero while `error` is `undefined` when a handler calls
`out.setExitCode(code)` for normal-output status results.
## `createCaptureOutput()`
Create an output channel that captures all writes for assertions.
## `createTestPrompter(answers)`
Create a prompt engine that returns pre-defined answers.
```ts twoslash
import { createTestPrompter } from '@kjanat/dreamcli/testkit';
const prompter = createTestPrompter([
'eu',
true,
'my-name',
]);
```
## `createTestAdapter(options?)`
Returns a runtime adapter for testing (no real process access).
Use `terminalSize: { columns, rows }` to simulate terminal-aware help wrapping in `cli().run({ adapter })` tests.
## `PROMPT_CANCEL`
Sentinel value to simulate prompt cancellation.
```ts twoslash
import { promptCmd } from './docs/.vitepress/twoslash/testing-fixtures.ts';
// ---cut---
import {
createTestPrompter,
PROMPT_CANCEL,
runCommand,
} from '@kjanat/dreamcli/testkit';
const result = await runCommand(promptCmd, [], {
prompter: createTestPrompter([PROMPT_CANCEL]),
});
```
---
# @kjanat/dreamcli/version
Source: https://dreamcli.kjanat.dev/raw/reference/version
# @kjanat/dreamcli/version
DreamCLI's own build-time version constants. They identify the framework build for diagnostics,
bug reports, and completion-script headers; they are unrelated to the app version configured with
`cli().version(...)` or discovered by `.manifest()`.
```ts twoslash
import { DREAMCLI_REVISION, DREAMCLI_VERSION } from '@kjanat/dreamcli/version';
```
## `DREAMCLI_VERSION`
The package version of the installed build (for example `"4.0.0"`), typed `string`. Reads `"dev"`
when the source tree runs unbundled.
## `DREAMCLI_REVISION`
The git short SHA the build was produced from (for example `"f9b5f1a"`), typed `string`. Reads
`"dev"` when the source tree runs unbundled.
Both values change every release. The subpath keeps them out of root-entry import completions.