01 · Problem
What is it for?
String splitting, conversion and command execution must be separate so invalid input never reaches a handler.
02 · Mental model
How should you think about it?
A command is a schema plus a handler; middleware wraps dispatch without changing parsing rules.
Dispatches validated argv commands through awaited middleware. Pass argv arrays to preserve shell quoting. Interactive input and shell completion are separate adapters.
03 · Control flow
What happens in which order?
- Resolve the command by name.
- Parse positional arguments and --flag=value tokens.
- Convert values and validate required fields.
- Run middleware in order.
- Await the command handler.
04 · Building blocks
The important contracts
Cli / CommandHandlerRegister and dispatch commands explicitly.ICommand / IArgument / IFlagDescribe a handler and validated inputs.use(middleware)Await middleware around the command.05 · Build it yourself
Implement a small core
Start with this small independent implementation, then use the steps below to make its contract explicit.
- Resolve the command by name.
- Parse positional arguments and --flag=value tokens.
- Convert values and validate required fields.
- Run middleware in order.
- Await the command handler.
type Command = {
name: string;
parse(tokens: string[]): Record<string, unknown>;
run(context: Record<string, unknown>): Promise<void>;
};
async function runCli(argv: string[], commands: Map<string, Command>) {
const [name, ...tokens] = argv;
const command = commands.get(name);
if (!command) throw new Error("Unknown command: " + name);
const context = command.parse(tokens);
await command.run(context);
}06 · Verify
What should you test?
- Reject unknown commands, flags and extra arguments.
- Preserve spaces in argv values.
- Verify middleware order and reject repeated next calls.
Try it yourself
Turn the first verification claim into a failing test, implement the smallest fix, then add a boundary case.
Reject unknown commands, flags and extra arguments.
Compare your approach
Your check should fail when the stated invariant is deliberately broken, and pass again after the fix. Keep setup deterministic and assert an observable result.
07 · Limits
Trade-offs and failure modes
- String input supports whitespace splitting only.
- Shell completion and interactive prompts are separate extensions.
- No CLI starts during import.
Pass argv arrays to preserve shell quoting. Interactive input and shell completion are separate adapters.
08 · Keep exploring
Source code and related ideas
yarn tsx packages/cli/examples/basic.ts