# 0005: Reclaim zig-cli for All Argv Parsing<no value>


## Status

Accepted

## Date

2026-04-28

## Context

zpm depended on `zig-cli` 0.10.0 but bypassed almost all of its functionality. Argv was reparsed by hand in `src/cli/arg_mapper.zig`, `extractFormatFlag`, and `parseUpgradeOptions`; help was rendered by a custom `printHelp` that imitated zig-cli's ANSI palette; and the routing in `main.zig` short-circuited `cli.AppRunner.run` for all 22 tool subcommands and for `upgrade`. The result was ~672 lines of hand-rolled parsing code that duplicated what zig-cli already provides, plus a dependency that was barely doing any work.

The duplication had concrete costs:

- **Type validation gap**: `--max-depth abc` and similar malformed integer flags were silently coerced or dropped instead of being rejected at parse time.
- **Help drift**: The custom `printHelp` was a frozen snapshot. Adding a new tool required hand-editing the help string to match the registry.
- **Two parsers, one source of truth**: Every flag had to be declared in the registry *and* threaded through `arg_mapper`. Inconsistencies were possible and hard to detect.
- **Panic-prone glue**: A custom `dispatcher.zig` panic catcher tried to recover from tool handler crashes, layered on top of zig-cli's own error handling.

## Candidates

| Option | Pros | Cons |
|--------|------|------|
| Keep the bypass (status quo) | No refactor cost; behaviour frozen | Continued duplication; no type validation; help drift remains a hazard |
| Pragmatic reclaim (small `arg_mapper` bridge) | Less code to touch | Still carries a parallel parser; doesn't justify the zig-cli dependency |
| Full reclaim (this ADR) | Single source of truth; type-validated flags; auto-generated help | Visual changes to help/error output; requires module-level slot statics |
| Drop zig-cli entirely | Removes one dependency | Have to rebuild help/error machinery — exactly what zig-cli provides |

## Decision

Refactor every CLI surface to use zig-cli idiomatically:

- `cli.AppRunner.run(&app)` is the only entry point — no kebab-name bypass, no `--version` short-circuit, no unknown-command guard.
- Every flag is a `cli.Option` or `cli.PositionalArg` with a typed slot (`?[]const u8`, `?i64`, the `Channel` enum, `bool`).
- A per-tool comptime generic `ToolCommand(comptime def)` (in `src/cli/tool_command.zig`) synthesizes the slot statics, options, positional args, and exec thunk for each registry entry.
- A new `src/cli/app.zig` assembles the top-level `cli.App` from the registry plus the four built-in subcommands (`init`, `serve`, `upgrade`, `version`).
- zig-cli generates `--help`, all argv-error messages, and the value parsers.
- `--version` and `-v` are auto-generated by zig-cli but currently fall through to the help banner with exit 1 (a known limitation in how zig-cli flushes the version short-circuit). A `version` subcommand was added as the working path, mirroring what the broken flag was meant to print.

### Files affected

- **Deleted**: `src/cli/arg_mapper.zig` (~242 lines), `src/cli/dispatcher.zig` (~205 lines), `printHelp` and ANSI helpers in `src/main.zig` (~50 lines)
- **Added**: `src/cli/tool_command.zig`, `src/cli/app.zig`
- **Rewritten**: `src/main.zig` (the file is 52 lines, of which the `main()` function is 5; the rest is two test functions and the `test {}` import block)
- **Modified**: `src/cli/registry.zig` (new `params: []const ParamSpec` field), `src/cli/upgrade.zig` (drops `parseUpgradeOptions` in favour of `cli.Option` slots)

## Consequences

### What becomes easier

- **Single source of truth**: Each tool's CLI shape lives in one place — the registry's `params` array. Adding a tool generates the matching CLI entry automatically (NFR-004 holds structurally).
- **Type-validated flags**: `--max-depth abc` is rejected at parse time. Unknown options raise a real error from the parser instead of being silently dropped.
- **No help drift**: Help text is rendered by zig-cli straight from the registry. There is no second representation to keep in sync.
- **~150 net lines deleted**: The two deleted modules and the inlined `printHelp`/`parseUpgradeOptions` add up to more code than the new `tool_command.zig` + `app.zig` introduce.

### What becomes harder

- **Visual diff in help and error output**: Help banners and parser error messages now follow zig-cli's formatting rather than the previous custom palette. Functional tests assert keyword presence (e.g. `version`, `init`, `serve`), not exact rendering.
- **Module-level slot statics**: `ToolCommand(comptime def)` declares its option/positional slots at module level for zig-cli to bind into. Running zpm twice in the same process would corrupt those slots — acceptable for a one-shot CLI binary, but it bakes the executable-only constraint into the type system; turning zpm into a library in the future would require redesigning `ToolCommand` to thread per-invocation slots through the call site.
- **`--version`/`-v` fall through to help**: A known zig-cli limitation prevents the auto-generated version short-circuit from flushing cleanly to stdout. The `version` subcommand replaces it as the working path. We accepted this rather than forking zig-cli because (a) the subcommand provides full parity for scripting and installation checks, (b) a fix would require an upstream PR or maintaining a vendor patch, and (c) the same limitation affects parser error messages on stderr — a fix narrow enough to land version printing would not address the broader silent-error class. Functional tests assert on exit codes only for those paths.

## Constitution Compliance

| Principle | Status | Justification |
|-----------|--------|---------------|
| Flat module structure | Compliant | `src/cli/` retains a flat layout; no new abstraction layers introduced |
| Executable-only project | Compliant | No library module changes |
| Tool handler signatures unchanged | Compliant | `ToolCommand` thunks call the same `mcp.tools.Tool` handlers; no handler API drift |
| Inline tests preserved | Compliant | Each `src/cli/*.zig` module owns its inline tests; `tests/functional_cli_test.sh` covers the end-to-end argv contract |
