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
| Capability | Commander / Yargs | Citty / CAC | Cleye | dreamcli |
|---|---|---|---|---|
| Type inference from definition | Manual interfaces | Basic | Good | Full — flags, args, context |
| Resolution chain (CLI → env → config → prompt → default) | DIY glue | CLI only | CLI only | Built-in, per-flag |
| Interactive prompts from schema | Separate library | No | No | Integrated |
| Middleware with typed context | No | No | No | Yes — accumulates via intersection |
| In-process test harness | Shell out | Shell out | Shell out | runCommand() + capture |
| Shell completions from schema | Plugin | No | No | Built-in (bash/zsh) |
Structured output (--json, tables, spinners) | DIY | No | No | Built-in |
| Config file discovery | DIY | No | No | Built-in (XDG paths, 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
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 usor-r eu - Type inference —
flags.regionis"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, oranswersin test harness
Design Principles
- Schema is the law. Everything derives from it: parsing, types, resolution, help, completions.
- Progressive disclosure. A 5-line script stays 5 lines. Complexity scales by composition.
- Deterministic behavior. Resolution rules are explicit and consistent.
- Portable by default. Core avoids runtime-specific APIs; adapters handle the edges.
- Ergonomic, not clever. Predictable DX over type-theory stunts.