Skip to content

fix(cli): emit JSON error envelopes for argument-parse errors under --output json#25

Closed
Davidson3556 wants to merge 1 commit into
TestSprite:mainfrom
Davidson3556:fix/json-output-parse-errors
Closed

fix(cli): emit JSON error envelopes for argument-parse errors under --output json#25
Davidson3556 wants to merge 1 commit into
TestSprite:mainfrom
Davidson3556:fix/json-output-parse-errors

Conversation

@Davidson3556

Copy link
Copy Markdown

Fixes #24

Describe the changes you have made in this PR -

--output json is the contract a machine consumer (CI, coding agent) relies on: on failure, JSON.parse(stderr). That held for any error that reached a command action, but not for Commander's own argument-parse errors — unknown command, unknown option, missing/excess argument — which printed plain text even under --output json. A single malformed invocation crashed any consumer parsing stderr as JSON.

Changes:

  • Resolve the requested --output mode directly from argv. A parse error can be thrown before Commander binds the global --output option (e.g. an unknown command, or --output placed after the failing token), so scanning argv is the only order-independent way to know what the caller asked for.
  • In JSON mode, suppress Commander's plain-text outputError write and emit a structured VALIDATION_ERROR envelope (exit 5 — same family and exit code as before) from the central catch, carrying the message and the commanderCode in details.
  • Apply the output configuration to every command in the tree — subcommands do not inherit the root's, so a one-line root config would miss project list --bogus / test code get.
  • Text mode is unchanged: the friendly global-flag rephrasing and Commander's "Did you mean …?" hints still print. --help / --version still exit 0 with human-readable text.

Demo/Screenshot for feature changes and bug fixes -

Before (main) — plain text breaks JSON.parse:

$ testsprite --output json frobnicate
error: unknown command 'frobnicate'
$ testsprite --output json project list --bogus
error: unknown option '--bogus'
$ testsprite --output json test code get
error: missing required argument 'test-id'

After (this branch) — structured envelopes:

$ testsprite --output json frobnicate
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "unknown command 'frobnicate'",
    "nextAction": "Run `testsprite --help`, or `testsprite <command> --help`, for usage.",
    "requestId": "local",
    "details": { "commanderCode": "commander.unknownCommand" }
  }
}
# exit 5

$ testsprite --output json project list --bogus   # subcommand -> still JSON
{ "error": { "code": "VALIDATION_ERROR", "message": "unknown option '--bogus'", ... } }

Text mode and help are untouched:

$ testsprite frobnicate
error: unknown command 'frobnicate'            # plain text, exit 5

$ testsprite test list --debugg
error: unknown option '--debugg'
(Did you mean --debug?)

$ testsprite --output json --help              # exit 0, human-readable
Usage: testsprite [options] [command]
...

Tests:

npm test
# 1453 passed
npm run typecheck && npm run lint && npm run format:check
# all clean

Code Understanding and AI Usage

Did you use AI assistance (ChatGPT, Claude, Copilot, etc.) to write any part of this code?

  • No, I wrote all the code myself
  • Yes, I used AI assistance (continue below)

If you used AI assistance:

  • I have reviewed every single line of the AI-generated code
  • I can explain the purpose and logic of each function/component I added
  • I have tested edge cases and understand how the code handles them
  • I have modified the AI output to follow this project's coding standards and conventions

Explain your implementation approach:

Problem: the JSON output contract was enforced for command-action errors but not for the argument-parse layer that runs first. Commander writes parse errors via its outputError hook (plain text) and then throws a CommanderError that the entry point's catch maps to exit 5 — so the plain text was already on stderr before the catch could format anything.

Alternatives considered:

  1. Re-implement argument parsing / pre-validate argv to catch these before Commander. Rejected: brittle and duplicates Commander's logic.
  2. Have outputError itself emit the JSON envelope. Rejected: outputError only sees the message string, not a clean code; the catch already classifies the CommanderError (help/version vs parse error) and owns exit-code mapping, so it is the right place to emit. outputError only needs to suppress the duplicate plain-text write in JSON mode.
  3. (chosen) Two cooperating pieces: a mode-aware outputError (suppress in JSON, rephrase in text) applied to every command, plus a JSON branch in the existing CommanderError handler in the catch.

Key pieces: outputModeFromArgv(argv) scans for --output <mode> / --output=<mode> (returns text for an unknown value or when absent) — needed because program.opts() is unreliable when parsing failed early. configureErrorOutput(cmd) recursively installs the mode-aware outputError (mirroring the existing applyExitOverrideDeep, since addCommand() subcommands don't inherit it). The catch strips any leading error: prefix from err.message and emits { code: VALIDATION_ERROR, message, nextAction, requestId: "local", details: { commanderCode } }.

Edge cases handled/tested: unknown command; unknown option on a subcommand (proves the deep config, not just root); missing required argument on a nested subcommand (test code get); --output placed after the failing token; text mode still plain text (regression guard); --help under --output json still exits 0 with human text (help/version are checked before the JSON branch, and don't go through outputError).


Checklist before requesting a review

  • I have added proper PR title and linked to the issue
  • I have performed a self-review of my code
  • I can explain the purpose of every function, class, and logic block I added
  • I understand why my changes work and have tested them thoroughly
  • I have considered potential edge cases and how my code handles them
  • If it is a core feature, I have added thorough tests
  • My code follows the project's style guidelines and conventions

Note: Please check Allow edits from maintainers if you would like us to assist in the PR.

…-output json

`--output json` is the contract machine consumers (CI, coding agents) rely
on: parse stderr as JSON on failure. But Commander's own parse errors —
unknown command, unknown option, missing/excess argument — were written as
plain text even under `--output json`, so a single malformed invocation
crashed any consumer doing `JSON.parse(stderr)`. Commands that reached their
action already emitted JSON; only the pre-action parse layer did not.

Make the error path honor the mode:
  - Resolve the requested `--output` mode directly from argv (order-
    independent, and available even when the parse fails before Commander
    binds the global flag).
  - In JSON mode, suppress Commander's plain-text `outputError` write and let
    the central catch emit a structured `VALIDATION_ERROR` envelope (exit 5,
    same family/exit code as before) carrying the message and the
    `commanderCode`. Apply the output config to every command in the tree,
    since subcommands do not inherit the root's configuration.
  - Text mode is unchanged: the friendly rephrasing and Commander's
    "Did you mean …?" hints still print. Help/version still exit 0 with
    human-readable text.

Adds subprocess regressions covering unknown command, unknown option on a
subcommand (deep config), missing argument on a nested subcommand, the
flag-after-failing-token ordering, and the text-mode / --help guards.
@Davidson3556

Copy link
Copy Markdown
Author

Closing as a duplicate of #22, which was opened earlier and covers the same fix (JSON envelopes for Commander parse errors under --output json). Deferring to that one — thanks @crypticsaiyan. One small idea you might fold in if useful: adding the Commander error code to details (e.g. details.commanderCode: "commander.unknownOption") gives machine consumers something stable to branch on without parsing the message string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--output json is broken for argument-parse errors (unknown command/option, missing arg print plain text)

1 participant