SuperAPI AI Directory

cli-creation

Coding & development

View repository

Updated 10-15 23:46

About this skill

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

  1. Choose a starter mode:
    • single: one root command (cli(name).default(command)).
    • multi: grouped command surface (group('...').command(...)).
  2. Generate starter files:
    • python scripts/scaffold_cli.py --name mycli --mode single --out .
    • Tests are generated by default; add --no-test only when explicitly requested.
    • Test template is auto-detected: Bun without Vitest uses bun:test; otherwise Vitest.
  3. 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-test was used.
  4. 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                    # full public API (~4k lines)
deno doc jsr:@kjanat/dreamcli/testkit 2>/dev/null            # subpath: testkit, runtime, schema, version
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-rc.2 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.md first; the default-command and finite changes 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. Values resolved through stdin, env, config, or a prompt are redacted in validation messages and omit details.value; this includes stdin selected by an explicit -. Only a literal CLI value is shown. 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 @kjanat/dreamcli and @kjanat/dreamcli/testkit; never reach into #internals/* or dist/.
  • 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 with out.json().
  • .default(cmd) is the root surface and is not routable by name; add { route: true } when a user expects mycli <name> to work too.
  • Prefer Bun commands first; include npm/tsx and Deno alternatives when asked.