CLI Creation
Overview
Create runnable DreamCLI starter CLIs and extend them with typed command patterns. This skill covers user-facing app code built on DreamCLI, not DreamCLI framework internals.
Targets DreamCLI 4.0. Version 3 removed the DSL, made the default command the
root surface, and added a large typed-flag surface. Version 4 gave both
factories the same sources, so .stdin(), .env(), .config(), and
.prompt() are available on flags and positionals alike. Snippets below assume
both.
Quick Start
- Choose a starter mode:
single: one root command (cli(name).default(command)).multi: grouped command surface (group('...').command(...)).
- Generate starter files:
python scripts/scaffold_cli.py --name mycli --mode single --out .- Tests are generated by default; add
--no-testonly when explicitly requested. - Test template is auto-detected: Bun without Vitest uses
bun:test; otherwise Vitest.
- Run and validate generated files:
- Use the printed path from the scaffolder output, for example
bun ./mycli.ts --help. - Run the generated test unless
--no-testwas used.
- Use the printed path from the scaffolder output, for example
- Extend behavior with references:
references/pattern-cookbook.md— copy-ready, type-checked snippets.references/consumer-workflow.md— request to validated CLI.references/runtime-notes.md— Bun/Node/Deno execution.
Looking Things Up
Prefer these over recalling API shapes from memory; they reflect the installed or published version rather than training data.
The API, offline-ish, no repo needed. If deno is on the system this works
regardless of whether the project installed from npm or JSR:
deno doc jsr:@kjanat/dreamcli 2>/dev/null # root entry: builders, parsing, help, errors, every public type
deno doc jsr:@kjanat/dreamcli/completion 2>/dev/null # one subpath at a time; also /config, /json-schema, /prompt, /testkit, /runtime, /version, /schema
deno doc --json jsr:@kjanat/dreamcli 2>/dev/null # machine-readable, for scripted lookups
deno doc --filter=CLIBuilder jsr:@kjanat/dreamcli 2>/dev/null # one symbol
2>/dev/null matters: deno writes download and type-check progress to stderr,
which otherwise swamps the documentation output.
Pin a version with jsr:@kjanat/dreamcli@4.0.0 when the project is not on
latest. --filter takes a declaration name; it prints nothing for a name that
does not exist, which is itself a useful signal.
The docs site, as markdown. Every page is authored markdown served under
/raw/, and any page URL returns markdown under content negotiation:
curl -s https://dreamcli.kjanat.dev/llms.txt # index of every page, one line each
curl -s https://dreamcli.kjanat.dev/llms-full.txt # every page concatenated (~250 kB)
curl -s https://dreamcli.kjanat.dev/raw/guide/flags # one page, authored markdown
curl -sH 'Accept: text/markdown' https://dreamcli.kjanat.dev/guide/flags
Start from llms.txt to find the right page, then fetch that page rather than
pulling llms-full.txt into context.
Grounding Sources
Paths are relative to the dreamcli repository root.
examples/basic.ts— single-command defaults.examples/multi-command.ts— grouped-command defaults.examples/testing.ts—runCommand()patterns.examples/flag-types.ts— the v3 typed-flag family.examples/parser-control.ts— negation, duplicates, spelling parity.examples/output-extras.ts— colors, hyperlinks,setExitCode.examples/standard-schema.ts— Standard Schema validation.examples/help-config.ts— help themes, flag order, routable default.examples/gh/— a full multi-command app used as a walkthrough.docs/guide/getting-started.md— baseline consumer narrative.docs/guide/walkthrough.md— end-to-end CLI composition.docs/guide/upgrading-v3.md— what changed from 2.x, for migrations.
Workflow Decision Tree
- Simple one-command utility →
--mode single. - Nested command groups (git/gh style) →
--mode multi. - Tests wanted from the start → do nothing, they are scaffolded by default.
- Tests explicitly unwanted → add
--no-test. - npm/tsx or Deno instructions → keep generated code unchanged and give the
runtime alternatives from
references/runtime-notes.md. - Migrating an existing 2.x CLI → read
docs/guide/upgrading-v3.mdfirst; the default-command andfinitechanges silently alter behavior.
Extend the Starter
Values. Prefer a purpose-built kind over flag.string() / arg.string()
plus parsing. Both factories carry string(), number(), boolean(),
enum(...), custom(...), keyValue(), url(), path(), date(),
duration(), and bytes(). flag additionally carries array() and
count(); the arg form of flag.array() is .variadic(). Express validation
declaratively with constraints ({ int, min, max }, { nonEmpty, pattern },
chainable on both builders) or a Standard Schema passed to .standard() or to
flag.custom() / arg.custom(), not with hand-written checks in the action.
Defaults. A .default() value is validated where the chain declares it, so
a default that violates its own constraints, validator, or collection shape
throws INVALID_DEFAULT at build time. A collection default takes the shape the
input resolves to: an array for flag.array() and a variadic arg, a record for
keyValue(), a non-negative integer for flag.count().
Collections. flag.array(), flag.keyValue(), arg.keyValue(), and
.variadic() aggregate from every source under one set of rules. Each source
decodes under its own policy, set by .split({ cli, env, stdin }): whole CLI
tokens by default, comma-delimited env values, line-delimited stdin, and native
arrays and objects from config. .separator() sets the CLI policy alone and is
no longer inherited by env or config. .unique() dedupes a list, and
.duplicateKeys('last' | 'first' | 'error') decides a repeated key on every
source, naming the source that carried it. A validator on the element builder
checks each element; one on the collection builder checks the finished value.
On the arg surface, .separator() and .split() require .variadic() or
arg.keyValue(), .unique() requires a variadic list, and .duplicateKeys()
requires arg.keyValue(). The compiler refuses every other shape and
createArgSchema() throws INVALID_SCHEMA.
Argument order. A variadic argument takes every remaining positional, so it
is the last one a command can declare. Anything registered behind it throws
INVALID_BUILDER_STATE.
Sources. Both factories declare the same sources. Chain .stdin(),
.env(), .config(), .prompt(), .default() on a flag or an argument and
let one resolution order (argv, stdin, env, config, prompt, default) do the
work. Count and key-value flags and key-value arguments are not promptable.
.stdin() takes { when, consume, trim }; one command has one exclusive
stdin consumer unless every stdin input passes { consume: 'broadcast' }. A -
occurrence on a collection splices the decoded buffer in at that position, so
--tag before --tag - --tag after over a\nb\n gives
['before', 'a', 'b', 'after'], and a variadic argument reads its tail the same
way. A - typed beside other occurrences with nothing piped fails with
MISSING_STDIN; a lone -, and a scalar -, fall through instead.
{ trim: true } drops one trailing line terminator from a single value, which
is what arg.path({ mustExist: true }).stdin({ trim: true }) wants. Help names
each binding: [stdin], [stdin: '-'], or [stdin: when omitted]. Stdin is
available to scalar, array, key-value, and variadic inputs; count flags cannot
read it, and key-value arguments cannot prompt.
Provenance. A handler receives sources beside flags and args, keyed
the same way, holding the stage that produced each value (cli, stdin, env
with its envVar, config with its configPath, prompt, default).
wasExplicit(sources.flags.x) is the predicate for "supplied rather than
defaulted"; never drop .default() to detect that, since it also drops
defaultValue from the exported schema.
Cross-flag rules. Put them in .derive(), which runs after resolution and
before the action, and return derived state to widen ctx.
Diagnostics. Sensitivity, not source, controls disclosure. Mark every
credential-bearing input .sensitive(); that redacts the value in messages and
omits details.value whether it came from argv, stdin, env, config, a prompt,
or a default. A non-sensitive value may be shown from any of those sources, so
flag.string().env('API_TOKEN') without .sensitive() prints the token in a
validation failure. The framework cannot redact text your own code writes: a
flag.custom() parse function's thrown message and a Standard Schema issue
message are shown verbatim, so write them to describe the expectation rather
than to interpolate the value.
Output. out.log() for results, out.status() for progress notes (stderr,
suppressed by --quiet), out.table() for lists, out.json() behind
out.jsonMode, out.color/osc8() for styling, out.setExitCode() when a
command must report normally but exit non-zero.
Testing. runCommand() from @kjanat/dreamcli/testkit, with answers for
prompts and stat/mkdir when flag.path() or arg.path() checks must run.
Assert output including trailing newlines.
Resource Map
scripts/scaffold_cli.py— generate Bun-first starter files and tests.assets/templates/*.tpl— source templates used by the scaffolder.references/pattern-cookbook.md— snippets by topic; all type-checked.references/consumer-workflow.md— end-to-end flow from request to validation.references/runtime-notes.md— runtime and package-manager execution guidance.
Guardrails
- Do not modify DreamCLI core internals for consumer-app requests.
- Keep generated imports on the documented package entrypoints:
@kjanat/dreamcliand its/testkit,/runtime,/completion,/config,/json-schema,/prompt,/schema, and/versionsubpaths; never reach into#internals/*ordist/. - Preserve the typed resolution flow: argv, stdin, env, config, prompt, default.
- Keep stdout machine-clean: progress and status go to stderr via
out.status(), never interleaved without.json(). .default(cmd)is the root surface and is not routable by name; add{ route: true }when a user expectsmycli <name>to work too.- Prefer Bun commands first; include npm/tsx and Deno alternatives when asked.