fix(cli): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47
, '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): an adapter arg named json must not crash every command (#441) - #442

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash
Open

fix(cli): an adapter arg named json must not crash every command (#441)#442
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/441-adapter-flag-collision-crash

Conversation

@Agnik47

Copy link
Copy Markdown
Contributor

Fixes#441.

The bug

configureCommandSurface registers an adapter's own arguments first, then
added the shared options unconditionally:

addOutputFormatOption(command)// -f/--format, --json.option('--trace <mode>', ...,'off').option('-v, --verbose','Debug output',false);

Commander throws on a duplicate flag, and this runs inside createProgram()
— while the CLI is still being built — so one adapter argument named json
aborted startup for every command. The official LinkedIn plugin ships such
an adapter (plugins/linkedin/thread-snapshot.js:162), so installing it
bricked webcmd. Verified on a clean main worktree at 37036a6 (v0.7.6):

commandbeforeafter
webcmd --versionok (exits before registration)ok
webcmd listcrashok
webcmd doctorcrashok
webcmd plugin listcrashok
webcmd plugin uninstall linkedincrashok

plugin uninstall crashing is what made it a dead end: the only way out was
deleting ~/.webcmd/plugins/linkedin by hand. The failure was also an
unhandled exception — a raw Node stack trace instead of a structured webcmd
error, and the process still exited 0.

The fix

Guard every shared option the adapter path registers, the way
ensureOutputFormatOptions in the same file already guarded its own. Both
paths now go through one helper, so they cannot drift apart again:

functionaddSharedOption(command,flags,description,defaultValue){constoption=newOption(flags,description);consttaken=registeredFlags(command);if((option.short&&taken.has(option.short))||(option.long&&taken.has(option.long)))returnfalse;
...
}

An adapter that names a flag keeps it; webcmd drops its own rather than
refusing to run. The guard covers --format, --json, --trace,
-v/--verbose, and for browser commands --window, --site-session,
--keep-tabthread-snapshot is the only adapter in the repo that collides
today, but any of those names was equally fatal.

Who owns a shadowed --json

Registering nothing is not sufficient on its own. outputFormatIsExplicit and
requestedOutputFormat keyed off getOptionValueSource('json'), which does
not care who registered the option — so --json on thread-snapshot would
have set the adapter's argument and switched the output format.

Argv preprocessing already resolves this collision in the adapter's favour —
knownCommandOptions seeds the shared flags, then lets adapter args overwrite
them — so format resolution now agrees: --json is read as --format json
only on commands where webcmd actually registered the alias. -f json is
untouched and remains the way to ask for JSON output on such a command.

Help

Help follows the same rule, or it advertises a flag that is not registered and
lists the same flag twice with two different meanings. Before, on this branch,
thread-snapshot --help still printed the alias under Common options; now:

Command options:
--thread-url <value> Exact LinkedIn messaging thread URL to open and snapshot
--max-scrolls [value] Maximum upward scroll attempts to load older messages default: 30
--json [value] Return only JSON snapshot string in the snapshot_json field default: false
Common options:
-f, --format <fmt> Output format: table, plain, json, yaml, md, csv default: table
--trace <mode> Trace capture: off, on, retain-on-failure default: off
-v, --verbose Debug output default: false

A command that shadows nothing is unchanged and still lists
--json Alias of --format json. The same filtering is applied to the
structured (--help -f yaml) rendering.

Tests

16 new tests, all failing before this change and passing after (verified by
reverting only src/command-surface.ts and src/command-presentation.ts and
re-running):

  • src/commanderAdapter.test.ts (3) — registering an adapter with a json
    argument does not throw, the adapter keeps the flag, and a sibling command
    that shadows nothing still gets the alias. This is the reported crash at the
    level it actually occurred.
  • src/command-surface.test.ts (7) — registration succeeds for an argument
    named json, format, trace, verbose, window, site-session, or
    keep-tab; every shared option is still registered when nothing collides;
    a shadowed --json reaches the adapter and does not change the output
    format; -f json still does.
  • src/command-presentation.test.ts (4) — shadowed flag listed once with the
    adapter's meaning in text and structured help; other shared options still
    listed; a non-shadowing command unchanged.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory), 45 on
this branch with no failure that is not also on main. One browser-runner
test appeared in a first run and not a second, and passes in isolation twice —
load-flaky, unrelated to this change. tsc --noEmit clean.

Left for a follow-up

Whether an adapter should be allowed to declare an argument that shadows a
reserved flag at all. This PR makes the collision survivable and gives the
adapter the flag; making webcmd validate reject such names up front is a
separate call, and I did not want to break an adapter that already ships one
in the same change that stops it crashing the CLI.

@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

…ntrhq#441)
`configureCommandSurface` registers an adapter's own arguments first, then
added the shared options unconditionally. Commander throws on a duplicate
flag, and this runs while the CLI is being built, so a single adapter argument
named `json` aborted startup for *every* command — `list`, `doctor`, and the
`plugin uninstall` needed to remove the offending plugin, leaving no recovery
path through the CLI. The official LinkedIn plugin ships such an adapter
(`thread-snapshot`), so installing it bricked webcmd.
Guard every shared option the adapter path registers — `--format`, `--json`,
`--trace`, `-v/--verbose`, and the browser trio — the way
`ensureOutputFormatOptions` in the same file already guarded its own, and
reuse one helper for both so the two paths cannot drift again. An adapter that
names a flag keeps it; webcmd drops its own rather than refusing to run.
Format resolution has to agree about who owns a shadowed `--json`, or the flag
would silently do two things: set the adapter's argument *and* switch the
output format. Argv preprocessing already resolves this collision in the
adapter's favour, so `outputFormatIsExplicit`/`requestedOutputFormat` now
honour `--json` as the format alias only on commands where webcmd registered
it. `-f json` is unaffected and remains the way to ask for JSON output there.
Help follows the same rule: a shared option the adapter shadows is no longer
listed under "Common options", in both the text and structured renderings,
since advertising it would name a flag that is not registered and show the
same flag twice with two different meanings.
@Agnik47
Agnik47force-pushed the fix/441-adapter-flag-collision-crash branch from 370d76f to f1ca835CompareAugust 28, 2026 20:48
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.

[Bug]: an adapter arg named json crashes every webcmd command at startup (official linkedin plugin ships one)

1 participant

@Agnik47