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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
ankitranjan7 merged 1 commit into
mainfrom
fix/cli-usage-error-envelope
Aug 23, 2026
Merged

fix(cli): one error envelope and exit 2 for every usage error#424
ankitranjan7 merged 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.

@ankitranjan7
ankitranjan7 merged commit 78ebdba into mainAug 23, 2026
36 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…#427)
#421 and #424 each passed alone and collide on main. #421's namespace
handler printed its suggestion with console.error and set exitCode, so it
never reached the envelope path #424 built: `--json` got human text and the
`help:` line was spelled two different ways.
The handler now throws a CommanderStructuralError carrying both the human
bytes and the envelope, and both paths use one wording for the help line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ankitranjan7 added a commit that referenced this pull request Aug 23, 2026
…ist (#428)
Cloud's parity suite caught two ways hosted mode drifted from local after
#424/#427.
Hosted still passed includeCapturedStderrForUnknownOption, so it replayed
Commander's captured stderr on top of the line structuralErrorFromCommander
now formats itself, printing `error: unknown option ...` twice.
The `help:` subcommand list also differed: local used declaration order and
included Commander's auto `help` entry, hosted used manifest order without
it. Both are sorted and drop `help` now, leaving only commands hosted mode
genuinely cannot run (LOCAL strategy) as a difference.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 deleted the fix/cli-usage-error-envelope branch September 1, 2026 12:41
Sign up for freeto 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

@ankitranjan7