ref:main

`--json` is silently ignored by about half the command surface #32

closed Opened by cole.christensen@gmail.com

Links

No links yet.

--json is declared global = true (src/commands/mod.rs:36), so clap accepts it on every subcommand and commands::run sets the global flag (mod.rs:87). But only ~60 of ~140 leaf subcommands ever call output::is_json(). The rest print human text — often with ANSI bold/underline — and exit 0, so a script piping to jq gets garbage with no signal that anything went wrong.

Nouns where no subcommand honors --json

Noun Leaves affected
auth login, status, logout
release all 11
deploy list, create, status, env list, env create
registry token create, token list, token delete
ssh-key list, add, remove
update the whole command

Reproduced against the live server:

$ anvil ssh-key list --json
SSH Keys
ID NAME FINGERPRINT ...

Worse: nouns that honor it inconsistently within themselves

A user cannot form a rule, because sibling read commands disagree:

  • pr list/view/edit honor it; pr reviews does not (src/commands/pr.rs:703-742)
  • ci list/view honor it; ci secrets does not (src/commands/ci.rs:522-552)
  • epic list/view honor it; epic children does not (src/commands/epic.rs:276-302)
  • issue move/link-req/unlink-req emit json_ok envelopes (issue.rs:915, :936, :956) while issue link/unlink immediately above them do not

Suggested fix

Two parts, because the silent success is the actual danger:

  1. Make --json cover every leaf that produces output.
  2. Until then, have a leaf that ignores --json fail loudly rather than print human text — an unhandled --json should be an error, not a no-op.

Found during a CLI-wide consistency audit.

colechristensen cole.christensen@gmail.com commented 2026-07-26 20:07

Fixed by #48 (merged).

Verified on main: every leaf command now returns an `output::Response` that `commands::run` emits under `–json` — including all the nouns listed here (`auth`, `release`, `deploy`, `registry`, `ssh-key`, `update`). The within-noun inconsistencies (`pr reviews`, `ci secrets`, `epic children`, `issue link`/`unlink`) are gone too.

The “fail loudly rather than silently succeed” half is now structural rather than a runtime check: a leaf cannot compile without returning a Response, clippy.toml bans raw println!/print! outside the renderer, and tests/json_contract.rs::every_leaf_is_covered walks the clap tree and fails CI if a new subcommand appears without being wired through the contract. tests/json_fuzz.rs additionally runs 120 leaves under --json against a mock and asserts stdout is exactly one valid JSON value.