Skip to content

fix(cli): one error envelope and exit 2 for every usage error - #424

Open
ankitranjan7 wants to merge 1 commit into
mainfrom
fix/cli-usage-error-envelope
Open

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 wants to merge 1 commit into
mainfrom
fix/cli-usage-error-envelope

Conversation

@ankitranjan7

Copy link
Copy Markdown
Contributor

The same class of typo produced three different error renderings and two different exit codes, so nothing could parse a webcmd failure reliably. webcmd adapter list rest printed a plain line and a YAML envelope with code: UNKNOWN, exit 1. webcmd adapter path -f json x/y printed the identical line twice with no envelope, exit 2. webcmd web fetch --json ignored --json completely, exit 1.

What changed

  1. Commander structural failures route through the shared envelope path. unknownCommand, unknownOption, missingArgument, missingMandatoryOptionValue, excessArguments, and invalidArgument map to USAGE_ERROR_CODES in src/command-surface.ts and carry an ErrorEnvelope on CommanderStructuralError.
  2. All of them exit 2 with a specific code (UNKNOWN_COMMAND, UNKNOWN_OPTION, MISSING_ARGUMENT, MISSING_OPTION, EXCESS_ARGUMENTS, INVALID_ARGUMENT) instead of exit 1 with UNKNOWN.
  3. The duplicate stderr line is gone. applyUnknownOptionContract now calls configureOutput({ writeErr }) alongside exitOverride, capturing what Commander's default outputError used to write directly to stderr before the throw. Output the handler does not own is replayed verbatim.
  4. -f/--format and --json are honoured on structural errors via requestedMachineFormat in src/output.ts. Humans still get plain error: + help: lines; only an explicit json/yaml request produces an envelope. help: extends the existing unknown-option treatment to unknown subcommands (lists valid subcommands) and missing arguments (restates the usage line).
  5. web fetch and hosted mode use the same contract. runWebFetchCommand bypassed cli.ts entirely and never installed the contract; handleProgramParseError/reportCliError moved to src/cli-error-report.ts so that fast path can reuse them without importing the full command tree. src/hosted/runner.ts prefers err.envelope over the legacy UNKNOWN/exit-1 fallback, keeping local and hosted bytes identical.

commander.help/commander.version are untouched — COMMANDER_DISPLAY_CODES still short-circuits before any envelope work, and webcmd adapter --help still exits 0 with empty stderr.

Before / After

BEFORE                                          AFTER
$ webcmd adapter list rest                      $ webcmd adapter list rest
error: unknown command 'list'                   error: unknown command 'list'
ok: false                                       help: valid subcommands for `webcmd adapter`: status, reset, override, source, path, help
error:                                            exit=2
  code: UNKNOWN
  message: 'error: unknown command ''list'''
  exitCode: 1
  exit=1

$ webcmd adapter path -f json x/y               $ webcmd adapter path -f json x/y
error: unknown option '-f'                      {
error: unknown option '-f'                        "ok": false,
  exit=2                                          "error": {
                                                    "code": "UNKNOWN_OPTION",
                                                    "message": "unknown option '-f'",
                                                    "exitCode": 2
                                                  }
                                                }
                                                  exit=2

$ webcmd web fetch                              $ webcmd web fetch
error: required option '--url <value>' not      error: required option '--url <value>' not specified
specified                                       help: usage: webcmd web fetch [options]
  exit=1                                          exit=2

$ webcmd web fetch --json                       $ webcmd web fetch --json
error: required option '--url <value>' not      {
specified                                         "ok": false,
  exit=1                                          "error": {
                                                    "code": "MISSING_OPTION",
                                                    "message": "required option '--url <value>' not specified",
                                                    "help": "usage: webcmd web fetch [options]",
                                                    "exitCode": 2
                                                  }
                                                }
                                                  exit=2

$ webcmd session close badid                    $ webcmd session close badid
ok: false                                       ok: false
error:                                          error:
  code: INVALID_SESSION_SELECTOR                  code: INVALID_SESSION_SELECTOR
  ...                                             ...
  exitCode: 2                                     exitCode: 2
  exit=2                                          exit=2   (unchanged)

Tests

npm run typecheck clean. npx vitest run --project unit: 154 files, 2713 passed, 1 skipped, 0 failed (baseline on origin/main with dist/ built: 2703 passed, 0 failed). npm run build clean, and every command above was re-run against the built dist/src/main.js.

New coverage in src/cli-error-report.test.ts: each reproduce case asserts exit 2, exactly one error: line on stderr, no envelope for humans, and a valid parsed JSON envelope under --json (plus a YAML case for -f yaml, and adapter --help still exit 0 / empty stderr).

Assertions changed deliberately, all of them encoding the old inconsistency:

  • src/hosted/runner.test.ts — six structural cases moved from exitCode: 1 + trailing code: UNKNOWN envelope to exitCode: 2 + help: line (missing positional, missing required option ×3, excess positional, unknown site command).
  • src/hosted/root-command-surface.test.tscompletion with no shell, list with an excess argument, and unknown-subcommand-of-a-known-site now assert the exit-2 usage bytes for both local and hosted.

Deliberately out of scope: commander.optionMissingArgument (option '--x <v>' argument missing) still exits 1 with the legacy envelope — it was not in the listed set and touching it churns a wider band of hosted parity tests. It is the obvious follow-up.

🤖 Generated with Claude Code

Unknown subcommands, unknown options, missing arguments, missing required
options, and excess arguments all rendered differently: a plain line, a plain
line plus a YAML envelope, or the same line printed twice. Exit codes were 1
or 2 depending on which, and `-f/--format`/`--json` were ignored entirely.

Commander structural failures now route through the same envelope path as
CliError: exit 2, a specific `error.code`, one `error:` line plus a `help:`
line for humans, and a JSON/YAML envelope when a machine format is requested.
`applyUnknownOptionContract` captures Commander's `writeErr` so the default
`outputError` write can no longer reach stderr ahead of the handler, which
removes the duplicated line. The `web fetch` fast path and the hosted runner
use the same contract, so local and hosted bytes stay identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • Some review context was unavailable or reduced.

This review is advisory and does not block merging.

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.

1 participant