# cli: the argument parser is a port of argparse Goal: `detz` parses its command line exactly like `../tai.py` (`build_parser()` run by the Python installed here, 3.14). This document is the spec; it states what the code does today and lists the gaps at the end. ## Layout (one class per `.h`/`.cpp` pair, `src/cli/`) | class | file | role | |---|---|---| | `FlagTable` / `FlagSpec` | `flag_spec.*` | the single flag table: option strings, dest, arity, choices, metavar, help. Usage, help and the parser all read it, so they cannot drift | | `TextWrap` | `text_wrap.*` | port of Python `textwrap.wrap` (hyphen breaking, long-word breaking) | | `HelpFormatter` | `help_formatter.*` | port of `argparse.HelpFormatter` (3.14): usage wrapping, help columns | | `ArgParser` | `arg_parser.*` | port of `ArgumentParser._parse_known_args` (pattern string of `A`/`O`/`-`, abbreviations, explicit values, `--`) | | `Parser`, `Command`, `DetzCli`, `Options` | `cli.*` | stable API others compile against; maps `ArgParser` output onto `Options` | `Options`, `Parser::parse/parseOpts/helpText`, `Command::run` keep their names and fields. ## Behavior (mirrors argparse, each point has a differential case) - Unique prefixes of long options resolve (`--vers` is `--version`); an ambiguous prefix fails with `ambiguous option: --ins could match --install, --install-telegram` (matches listed in table order). `--tui-` resolves to `--tui-local` because `--tui` does not start with it. - `--flag=value` for value flags; an explicit value on a switch fails with `argument --yes: ignored explicit argument '1'`. Single-dash clusters (`-hx`) follow argparse. - A flag that needs a value and sees an option-like word, `--`, or the end fails with `argument --profile: expected one argument`. `-1` and `-.5` (anything matching `-\.?\d`) are values, not options. A word with a space in it is a value. - `--debug` takes an optional value (`file` when bare), checked against its choices. - `--cap` and `--mplex` take every remaining word (argparse `REMAINDER`), options included. - `--` ends options; the first `--` is dropped from the prompt, later ones are kept. - `type=int` accepts what Python `int()` accepts for ASCII input (surrounding whitespace, sign, single underscores between digits); anything else is `invalid int value: 'abc'`. - `-h/--help` acts the moment it is reached, before later words are examined, so `-h --bogus` prints help (exit 0) while `--cap-permission-mode zzz -h` is an error (exit 2). `Options` then carries only `help = true`. - Any parse error: full usage text on stderr, then `tai: error: `, exit 2. `Options::error` holds both (usage then the error line, no trailing newline). - Terminal width: `COLUMNS` (if a positive integer), else the width of stdout's tty, else 80; the formatter uses width - 2, like `shutil.get_terminal_size`. - Flags that exist only in the old C++ parser (`--model`, `--driver`, `--task`, `--cwd`, `--argv`, `--binary`, `--max-risk`, `--auto-answer`, `--allow-tool`, `--deny-tool`) are not in `tai.py` and are rejected as unrecognized, like tai.py does. The `Options` fields stay so other modules compile. ## Tests Run all with `make -f tests/cli.mk test` (everything lands in `build/cli`). - `tests/cli_test.cpp`: unit cases (table, abbreviations, errors, REMAINDER, `--`, int, usage, wrap). - `tests/cli_parity.py` + `tests/cli_cases.txt`: every argv line of the list at COLUMNS 80 and 40, `--help` and the usage printed by an error at every width 12..200, and 1500 seeded random argvs, run through `build_parser()` of `../tai.py` (in process, the installed Python) and through `build/cli/cli_driver`; stdout, stderr and exit code must be equal (2080 jobs, 0 differences). - `make -f tests/cli.mk diff`: the generated cases of `tools/parity_diff.py` (`cli.*`, `tests/diff/cases/cli_generated.json`) against `build/cli/cli_main`, a cli-only entry point that mirrors the first lines of `main.cpp`. ## Gaps - Colored help (Python 3.14 colors help when `FORCE_COLOR` is set or stdout is a tty with color): not implemented, plain text only. - Widths count bytes, not code points (all help text is ASCII; user words only appear in error messages, which are not wrapped). - The `--cap-manifest` text and every action after parsing live in `main.cpp`, not here: case `cli.cap_manifest` fails against `cli_main` for that reason (55 of 56 `cli.*` cases pass).