ADR 0009: Argument Parser — Commander
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-12 |
| Deciders | Project maintainers |
| Supersedes | — |
| Superseded by | — |
| Related ADRs | 0001 (Runtime + language), 0010 (Prompts), 0011 (Schemas) |
Summary
Use Commander as the argument-parsing library for @aidokit/cli. It is the smallest mature option that comfortably handles aidokit's command surface (≤ 15 commands at v1.0), has first-class TypeScript types, and adds negligible install weight. Two alternatives were considered and rejected: oclif (overbuilt for our surface) and a custom parser (re-implements help / validation / parsing for no gain).
Maintainer confirmed Commander on 2026-05-12.
Context
@aidokit/cli exposes 13 commands by v1.0 (per .docs/docs/specs/cli-reference.md §7), with subcommands on mcp, skills, new, search, list, and add/remove adapter. Flags include both global (--yes, --verbose, --json, --no-color, --cwd) and per-command flags. The parser needs to:
- Parse
aido <command> [subcommand] [args] [--flag] [--flag=value] [-f]percli-reference.md§2.2. - Support both
--flag valueand--flag=value. - Recognise
--as a positional terminator. - Honour TTY detection (
cli-reference.md§2.4 routes toaidokit-prompt-driven interactive mode when stdin is a TTY; the parser doesn't need to know). - Surface its own help (
--help,-h) so we don't reinvent help formatting. - Map exit codes through to the values declared in
.docs/ARCHITECTURE.md§16.2. - Have clean TypeScript types so command handlers don't
any-leak.
The CLI is not a plugin host: there is no need for dynamic command discovery, marketplace-installable plugins, or per-command CWD shifting. Adapters and stack packs are imported statically (ADR-0005, ADR-0006); they do not register their own commands.
The choice is constrained by two earlier ADRs:
- ADR-0001 — Node 20+, TypeScript, ESM-only. Any parser must work as an ESM dependency.
- ADR-0002 — pnpm workspace; the parser is a
dependenciesentry of@aidokit/clionly.
Decision
1. Use Commander v12+
import { Command } from 'commander';
const program = new Command()
.name('aidokit')
.description('Bootstrap an AI-assisted development workflow')
.version(VERSION);
program
.command('init')
.description('Bootstrap a project (interactive)')
.option('--adapter <name>', 'Adapter to install')
.option('--conformance <level>', 'Conformance level')
.option('--stack <ids>', 'Comma-separated stack-pack ids')
.option('-y, --yes', 'Skip confirmation prompts')
.option('--dry-run', 'Print plan without writing')
.action(initCommand);
await program.parseAsync(process.argv);
2. Each command lives in its own module
packages/cli/src/commands/<command>.ts exports a handler function. The top-level cli.ts wires commands into the Commander tree. This keeps command logic isolated and testable independent of the parser.
3. Help and version are Commander's
We do not write custom help formatters. Commander's default output is acceptable for v0.1. Customisation (colour, sections) can be added in v0.5 if user feedback warrants it.
4. Exit code mapping is the CLI shell's job, not Commander's
Command handlers return Promise<void> and throw AidoError. A top-level catch in cli.ts maps AidoError.code to exit codes per .docs/ARCHITECTURE.md §16.2. Commander's own parse-error exit (code 1) is acceptable for malformed invocations (exit code 2 in our taxonomy is reserved for "bad invocation"; we wire this through Commander's .exitOverride()).
5. Pinned to a tested minor range
@aidokit/cli pins Commander to ^12.0.0 (or whichever current major is in use at v0.1 release). Major-version upgrades require a Changeset entry and a test pass.
Consequences
Positive
- Smallest mature option. Commander's footprint is well under 100 KB unpacked; install time is negligible.
- TypeScript types ship with the package. No
@types/commanderpeer dependency required. - API surface is small. Contributors can read the entire Commander API in 20 minutes.
- Active maintenance. Commander v12 was released in 2024; the project remains responsive.
- Battle-tested. Used by
npm,vue-cli, and countless tools — failure modes are well understood.
Negative
- Help formatting is plain. Commander's default help is unstyled. Mitigated: acceptable at v0.1; revisit at v0.5 if users want colour or sections.
- No automatic plugin discovery. If we ever want third-party
aido <plugin-command>invocations (post-v2.0 marketplace), Commander would not auto-discover them. Mitigated: that's not on the roadmap until v2.0; we can revisit the parser at that point. - Verbose for very deep subcommand trees. Our deepest tree is
aido mcp add <name>— three levels. Commander handles this comfortably.
Neutral
- No structured logging integration. Commander does not opinionate on output formatting; we layer
picocolors/oraourselves. This is what we want. - Single-file CLI definition is fine for our scale. We don't need oclif's per-file-per-command convention.
Alternatives considered
Alternative 1: oclif
A framework for building CLIs (Salesforce's CLI, Heroku CLI). File-per-command convention, automatic doc generation, built-in plugin system.
Pros
- Excellent for very large CLIs (50+ commands).
- Built-in plugin discovery from
npmpackages. - Automatic Markdown doc generation.
- Strong help formatting and command discovery UX.
Cons
- Much larger dependency tree.
- File-per-command convention adds boilerplate for our ~13-command surface.
- Plugin system is overkill: we don't plan a runtime plugin model until v2.0 (ADR-0005, ADR-0006: adapters/stack packs are static imports).
- Opinionated about project structure in ways that don't compose cleanly with pnpm workspaces; oclif expects
packages/cli/src/commands/*discovery, which would conflict with our@aidokit/clipackage layout.
Reason rejected: oclif is the right tool for Salesforce's CLI; it is wrong-sized for ours. We can revisit if the command surface grows past ~30 commands or we adopt a runtime plugin model.
Alternative 2: Custom parser
A purpose-built parser inside @aidokit/cli with no external dependency.
Pros
- Zero dependencies.
- Total control over parsing behaviour, help format, error messages.
Cons
- Reinvents help formatting, validation, type coercion, short/long flag handling,
--flag=valuevs--flag value, positional terminator (--), and a hundred small edge cases. - Every contributor must learn our parser; no transferable knowledge.
- A first-class TypeScript types story is significant work.
Reason rejected: the operational cost vastly exceeds Commander's install weight. Custom parsers are tempting and almost always wrong for CLI tools at this scale.
Alternative 3: yargs
Long-standing alternative to Commander; richer middleware story.
Pros
- Mature, widely used.
- Built-in
terminalWidth()awareness, more advanced help layout options. - Middleware/positional/coercion features beyond Commander.
Cons
- Larger footprint than Commander.
- API surface is broader — more to teach contributors.
- TypeScript typing is functional but historically less ergonomic than Commander's.
Reason rejected: yargs is a viable choice; we prefer Commander on size and TS ergonomics. If the maintainer prefers yargs, this ADR can be revised before Accepted.
Alternative 4: Citty
Modern, minimal, used by Nuxt. ESM-first.
Pros
- Smaller than Commander.
- Native ESM design.
Cons
- Newer; smaller user base.
- Less mature error messages and help formatting.
Reason rejected: size advantage is marginal; maturity advantage of Commander outweighs it.
Alternative 5: Cmd-ts
Type-first arg parser; strong inference of handler types from definitions.
Pros
- Best-in-class TypeScript inference.
Cons
- Significantly less battle-tested than Commander.
- Small community.
Reason rejected: strong inference is appealing but not worth the maturity gap.
Implementation notes
If accepted:
- Add
commandertopackages/cli/package.jsonas a direct dependency. packages/cli/src/cli.tsbuilds the Commander tree.- Each command exports a handler:
// packages/cli/src/commands/init.ts
import type { ProjectContext } from '@aidokit/core';
export interface InitOptions {
adapter?: string;
conformance?: 'minimum' | 'standard' | 'strict';
stack?: string;
yes?: boolean;
dryRun?: boolean;
}
export async function initCommand(options: InitOptions): Promise<void> {
// ...
}
- Top-level catch in
cli.tsmapsAidoErrorinstances to exit codes; uncaught exceptions exit with code 1.
If a different parser is accepted, the same per-command-handler pattern applies; only the wiring at the top changes.
Validation
This decision is validated when:
- [ ] All
cli-reference.md§7 commands are wired into Commander with matching flags. - [ ]
aidokit --helpandaido <command> --helpprint acceptable output (subjective acceptance from maintainer). - [ ] Exit-code mapping from
AidoError.codeto numeric exit code is implemented at the CLI shell, not inside command handlers. - [ ] No command handler imports Commander directly — handlers receive a typed options object.
References
Internal
- ADR 0001 — Runtime + language
- ADR 0010 — Prompts library (sibling decision)
- ADR 0011 — Schema library (sibling decision)
.docs/docs/specs/cli-reference.md— command surface.docs/ARCHITECTURE.md§21 — open question on argument parser (this ADR closes it).docs/ARCHITECTURE.md§16 — error model and exit codes
External
- Commander — https://github.com/tj/commander.js
- oclif — https://oclif.io/
- yargs — https://yargs.js.org/
- citty — https://github.com/unjs/citty
- cmd-ts — https://cmd-ts.now.sh/