Skip to content

Architecture

Pipeline

input → tokenize → resolve → parse flags → execute → output → loop
  1. Input — read from stdin via readline
  2. Tokenize — split into tokens (quoted strings, escapes)
  3. Resolve — walk command tree matching tokens to node names
  4. Parse flags — remaining tokens → --name value pairs
  5. Execute — matched command receives CommandArguments with typed accessors
  6. Output — command writes to ctx.stdout
  7. Loop — prompt again

Command tree

Commands form a hierarchy. Each node extends Command; namespace nodes extend CommandContainer.

Resolution

Given ["config", "set", "--theme", "dark"]:

  1. "config" → found, has subcommands → descend
  2. "set" → found, leaf → stop, execute with --theme = "dark"

  3. Unknown command → CommandNotFoundError with prefix-matched suggestions

  4. Commands with subcommands call execute() only when no subcommand matches
  5. Remaining tokens are parsed via parseFlags()

Tokenizer

tokenize() splits a raw line into tokens:

  • Whitespace — tokens separated by 1+ spaces/tabs/newlines; leading/trailing ignored
  • Quotes" and ' produce a single token (quotes stripped). One type can appear inside the other: "it's fine"["it's fine"]
  • Escapes — inside quotes, \"", \\\
  • Adjacent charsfoo"bar"ParseError: Unexpected characters before quote: "foo"
  • Empty quotes""[""]
  • Unclosed quotes"helloParseError: Unclosed " quote

Argument parsing

parseFlags() converts unmatched tokens to Record<string, string>:

--theme dark   → { theme: "dark" }
--verbose      → { verbose: "true" }
--fields id, name → { fields: "id, name" }

Bare tokens that don't match a positional definition are grouped onto the most recently written argument with a space separator. This lets --fields id, name produce the value "id, name" without quoting. Tokens with no prior argument and no positional definition still throw.

Wrapped in CommandArguments with typed accessors.

Tab completion

The Completer walks the command tree for matching names. At leaf commands, completes --flag names from definitions().

Error model

Errors propagate to handleError. Registered onError hooks run first — any returning true suppresses the error. Hook throw errors are caught individually; remaining hooks still run. Unconsumed errors print Error: <message> to stdout. The terminal loop never crashes.

ParseError is thrown during tokenization for malformed input (unclosed quotes, adjacent chars).


Commands — defining commands and arguments
Arguments — typed accessors and prompting
Hooks — lifecycle event reference