From 57525714fe8d942b25a12f62185cd22e37da15c9 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 14:50:26 +0530 Subject: [PATCH 1/7] fix(hosted): advertise skills and update in hosted help and completion Both are routed to the local program before hosted dispatch and already run under hosted configuration, but help listed skills as local-only and omitted update, and completion is derived from the advertised array so it offered neither. Drops the choose-local-mode footer from root help, where it over-claimed. The string stays on the daemon and doctor errors, where it is accurate. --- src/completion-shared.test.ts | 40 +++++++++++++++++++++++++++++++++++ src/completion-shared.ts | 4 ++-- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/completion-shared.test.ts diff --git a/src/completion-shared.test.ts b/src/completion-shared.test.ts new file mode 100644 index 00000000..2a4d336e --- /dev/null +++ b/src/completion-shared.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { + HOSTED_BUILTIN_COMMANDS, + HOSTED_ROOT_HELP, + LOCAL_ONLY_COMMAND_HELP, +} from './completion-shared.js'; +import { formatRootHelp } from './command-presentation.js'; + +describe('hosted root help', () => { + const advertised = HOSTED_ROOT_HELP.commands.map(command => command.name.split(/\s/, 1)[0]!); + const localOnly = (HOSTED_ROOT_HELP.localOnlyCommands ?? []).map(command => command.name); + + it.each(['skills', 'update'])('advertises %s, which already runs locally in hosted mode', (name) => { + expect(advertised).toContain(name); + expect(localOnly).not.toContain(name); + }); + + it.each(['skills', 'update'])('offers %s in hosted completion', (name) => { + expect(HOSTED_BUILTIN_COMMANDS).toContain(name); + }); + + it('still lists daemon as local-only', () => { + expect(localOnly).toContain('daemon'); + }); + + it('never lists a command as both advertised and local-only', () => { + expect(advertised.filter(name => localOnly.includes(name))).toEqual([]); + }); + + it('drops the choose-local-mode footer from root help', () => { + expect(HOSTED_ROOT_HELP.localOnlyExplanation).toBeUndefined(); + expect(formatRootHelp(HOSTED_ROOT_HELP)).not.toContain(LOCAL_ONLY_COMMAND_HELP); + }); + + it('keeps the footer string exported for the daemon error hint', () => { + expect(LOCAL_ONLY_COMMAND_HELP).toBe( + 'Run `webcmd setup` and choose local mode to use local-only commands.', + ); + }); +}); diff --git a/src/completion-shared.ts b/src/completion-shared.ts index 64e01d16..e4689ea4 100644 --- a/src/completion-shared.ts +++ b/src/completion-shared.ts @@ -46,6 +46,8 @@ export const HOSTED_ROOT_HELP: RootHelpPresentation = { { name: 'list', description: 'List all available hosted CLI commands' }, { name: 'profile', description: 'Manage hosted browser profiles' }, { name: 'setup', description: 'Configure local or hosted mode' }, + { name: 'skills', description: 'Manage bundled Webcmd skills on this computer' }, + { name: 'update', description: 'Update the installed Webcmd CLI on this computer' }, { name: 'web', description: 'Fetch URLs locally without launching a browser. Use after a blocked, 403, or Cloudflare response.' }, ], localOnlyCommands: [ @@ -57,11 +59,9 @@ export const HOSTED_ROOT_HELP: RootHelpPresentation = { { name: 'doctor', description: 'Diagnose local browser bridge connectivity' }, { name: 'external', description: 'Manage local CLI passthrough commands' }, { name: 'plugin', description: 'Manage plugins installed on this computer' }, - { name: 'skills', description: 'Manage bundled skills on this computer' }, { name: 'validate', description: 'Validate local CLI definitions' }, { name: 'verify', description: 'Validate and smoke-test local adapters' }, ], - localOnlyExplanation: LOCAL_ONLY_COMMAND_HELP, }; export const HOSTED_BUILTIN_COMMANDS = HOSTED_ROOT_HELP.commands From b791f67848d3e441541b566beac2d202d78c5f6e Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 15:04:57 +0530 Subject: [PATCH 2/7] fix(hosted-tests): update snapshots for HOSTED_ROOT_HELP shape change Task 1 added `skills` and `update` to the advertised hosted commands and dropped the local-mode footer from root help. Two hosted test files still asserted the old shape. Co-Authored-By: Claude Opus 4.7 --- src/hosted/manifest.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hosted/manifest.test.ts b/src/hosted/manifest.test.ts index ea0ef8d3..e5eda35e 100644 --- a/src/hosted/manifest.test.ts +++ b/src/hosted/manifest.test.ts @@ -151,7 +151,6 @@ describe('hosted manifest helpers', () => { expect(stdout.text()).toMatch(/profile\s+Manage hosted browser profiles/); expect(stdout.text()).toContain('--profile '); expect(stdout.text()).toContain('Local-only commands:'); - expect(stdout.text()).toContain('Run `webcmd setup` and choose local mode to use local-only commands.'); }); it('completes private hosted manifest commands without local discovery', async () => { @@ -226,7 +225,7 @@ describe('hosted manifest helpers', () => { fetchImpl: async () => new Response(JSON.stringify({ ok: true, manifest }), { status: 200 }), }); - expect(stdout.text().trim().split('\n')).toEqual(['artifact', 'browser', 'completion', 'github', 'list', 'profile', 'setup', 'web']); + expect(stdout.text().trim().split('\n')).toEqual(['artifact', 'browser', 'completion', 'github', 'list', 'profile', 'setup', 'skills', 'update', 'web']); const siteHelp = sink(); await runHostedCli(['web', '--help'], { From 99a8b170eb0caa41a9a908b26c1199020a671e05 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 15:46:41 +0530 Subject: [PATCH 3/7] docs: changelog for hosted advertise skills/update Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f558430b..962fce19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Added + +- Hosted help and completion advertise `skills` and `update`, which already ran under hosted configuration. + +### Changed + +- Hosted root help no longer appends "Run `webcmd setup` and choose local mode to use local-only commands." The advice remains on the `daemon` and `doctor` errors, where it is accurate. + ## [0.7.7](https://github.com/agentrhq/webcmd/compare/webcmd-v0.7.6...webcmd-v0.7.7) (2026-08-26) ### Improvements From cafa63f71a31c0f5189adbffa970d6155c20f99f Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 16:30:58 +0530 Subject: [PATCH 4/7] Revert "docs: changelog for hosted advertise skills/update" This reverts commit 99a8b170eb0caa41a9a908b26c1199020a671e05. --- CHANGELOG.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 962fce19..f558430b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,5 @@ # Changelog -## Unreleased - -### Added - -- Hosted help and completion advertise `skills` and `update`, which already ran under hosted configuration. - -### Changed - -- Hosted root help no longer appends "Run `webcmd setup` and choose local mode to use local-only commands." The advice remains on the `daemon` and `doctor` errors, where it is accurate. - ## [0.7.7](https://github.com/agentrhq/webcmd/compare/webcmd-v0.7.6...webcmd-v0.7.7) (2026-08-26) ### Improvements From 236c3fcfdbef697e449e74ac309715788c93532e Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 17:13:13 +0530 Subject: [PATCH 5/7] fix(hosted): scope client command discovery by entrypoint --- src/completion-shared.ts | 22 ++++++++++-- src/hosted/programmatic-differential.test.ts | 1 - src/hosted/programmatic.test.ts | 38 ++++++++++++++++++++ src/hosted/programmatic.ts | 1 + src/hosted/runner.ts | 23 ++++++++---- src/main.ts | 5 ++- 6 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/completion-shared.ts b/src/completion-shared.ts index e4689ea4..700c7ae2 100644 --- a/src/completion-shared.ts +++ b/src/completion-shared.ts @@ -64,8 +64,26 @@ export const HOSTED_ROOT_HELP: RootHelpPresentation = { ], }; -export const HOSTED_BUILTIN_COMMANDS = HOSTED_ROOT_HELP.commands - .map((command) => command.name.split(/\s/, 1)[0]!); +const LOCAL_CLIENT_ROOT_COMMANDS = new Set(['skills', 'update']); + +export function getHostedRootHelp(hasLocalClientCommandHandlers = true): RootHelpPresentation { + if (hasLocalClientCommandHandlers) return HOSTED_ROOT_HELP; + return { + ...HOSTED_ROOT_HELP, + commands: HOSTED_ROOT_HELP.commands.filter(command => !LOCAL_CLIENT_ROOT_COMMANDS.has(command.name.split(/\s/, 1)[0]!)), + }; +} + +export function getHostedBuiltinCommands(hasLocalClientCommandHandlers = true): string[] { + return getHostedRootHelp(hasLocalClientCommandHandlers).commands + .map((command) => command.name.split(/\s/, 1)[0]!); +} + +export function isLocalClientRootCommand(command: string | undefined): boolean { + return command !== undefined && LOCAL_CLIENT_ROOT_COMMANDS.has(command); +} + +export const HOSTED_BUILTIN_COMMANDS = getHostedBuiltinCommands(); // ── Shell script generators ──────────────────────────────────────────────── diff --git a/src/hosted/programmatic-differential.test.ts b/src/hosted/programmatic-differential.test.ts index 459507b9..d8ee4aec 100644 --- a/src/hosted/programmatic-differential.test.ts +++ b/src/hosted/programmatic-differential.test.ts @@ -58,7 +58,6 @@ function runProgrammatic(argv: string[], files?: readonly HostedVirtualFile[]) { } const FIXTURES: { name: string; argv: string[]; files?: readonly HostedVirtualFile[] }[] = [ - { name: 'root help', argv: ['--help'] }, { name: 'version', argv: ['--version'] }, { name: 'list', argv: ['list'] }, { name: 'list json', argv: ['list', '-f', 'json'] }, diff --git a/src/hosted/programmatic.test.ts b/src/hosted/programmatic.test.ts index 1e49a587..6140d6ff 100644 --- a/src/hosted/programmatic.test.ts +++ b/src/hosted/programmatic.test.ts @@ -31,6 +31,44 @@ function fakeCloud(handler?: (url: string, init?: RequestInit) => Response): typ describe('runHostedProgrammatic', () => { afterEach(() => vi.restoreAllMocks()); + it('does not advertise client-owned commands in root help', async () => { + const result = await runHostedProgrammatic({ + argv: ['--help'], + apiBaseUrl: 'http://127.0.0.1:8787', + accessToken: 'oauth-access-token', + fetchImpl: fakeCloud(), + }); + + expect(result).toMatchObject({ exitCode: 0, stderr: '' }); + expect(result.stdout).not.toContain('skills'); + expect(result.stdout).not.toContain('update'); + }); + + it('does not offer client-owned commands in completion', async () => { + const result = await runHostedProgrammatic({ + argv: ['--get-completions', '--cursor', '0'], + apiBaseUrl: 'http://127.0.0.1:8787', + accessToken: 'oauth-access-token', + fetchImpl: fakeCloud(), + }); + + expect(result).toMatchObject({ exitCode: 0, stderr: '' }); + expect(result.stdout.split('\n')).not.toEqual(expect.arrayContaining(['skills', 'update'])); + }); + + it.each(['skills', 'update'])('rejects %s help instead of returning generic root help', async (command) => { + const result = await runHostedProgrammatic({ + argv: [command, '--help'], + apiBaseUrl: 'http://127.0.0.1:8787', + accessToken: 'oauth-access-token', + fetchImpl: fakeCloud(), + }); + + expect(result.exitCode).not.toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).not.toContain('Usage:'); + }); + it('returns captured stdout and a zero exit code', async () => { const result = await runHostedProgrammatic({ argv: ['list', '-f', 'json'], diff --git a/src/hosted/programmatic.ts b/src/hosted/programmatic.ts index 4c6e250c..7b46e15e 100644 --- a/src/hosted/programmatic.ts +++ b/src/hosted/programmatic.ts @@ -90,6 +90,7 @@ export async function runHostedProgrammatic( ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}), ...(options.now ? { now: options.now } : {}), ...(options.enableServerWebFetch === true ? { enableServerWebFetch: true } : {}), + hasLocalClientCommandHandlers: false, onTrustedCommandResolution: resolution => { trustedResolution = resolution; }, }); diff --git a/src/hosted/runner.ts b/src/hosted/runner.ts index 19c58c44..d4e98758 100644 --- a/src/hosted/runner.ts +++ b/src/hosted/runner.ts @@ -16,8 +16,9 @@ import { BrowserSessionArgvError, rejectMisplacedSessionSelectorArgv, rejectPosi import { addOutputFormatOption, CommanderStructuralError, MissingRequiredPositionalError, outputFormatIsExplicit, parseOutputFormat, requestedOutputFormat, resolveCommandFromArgv, structuralErrorFromCommander } from '../command-surface.js'; import { filterCommandsByTag, formatRootHelp, getCommandCompletionCandidates } from '../command-presentation.js'; import { - HOSTED_BUILTIN_COMMANDS, - HOSTED_ROOT_HELP, + getHostedBuiltinCommands, + getHostedRootHelp, + isLocalClientRootCommand, LOCAL_ONLY_COMMAND_HELP, } from '../completion-shared.js'; import { splitAdapterCommandKey } from '../adapter-source.js'; @@ -87,6 +88,8 @@ export interface HostedRunnerOptions { now?: () => number; /** Explicitly grants the hosted runner public-network-only web fetch authority. */ enableServerWebFetch?: boolean; + /** True when the installed executable can handle client-owned root commands locally. */ + hasLocalClientCommandHandlers?: boolean; /** @internal Receives sanitized manifest resolution metadata for embedders. */ onTrustedCommandResolution?: (resolution: TrustedCommandResolution) => void; /** Supplies `--stdin` content without reading `process.stdin`. */ @@ -189,6 +192,7 @@ export async function runHostedCli(argv: string[], opts: HostedRunnerOptions = { opts.homeDir ?? opts.env?.HOME ?? homedir(), io, opts.enableServerWebFetch === true, + opts.hasLocalClientCommandHandlers !== false, opts.signal, opts.onTrustedCommandResolution, ); @@ -251,12 +255,14 @@ async function dispatchHosted( homeDir: string, io: HostedDispatchIo = { fileIo: realHostedFileIo }, enableServerWebFetch = false, + hasLocalClientCommandHandlers = true, signal?: AbortSignal, onResolvedCommand?: (resolution: TrustedCommandResolution) => void, ): Promise { + const rootHelp = getHostedRootHelp(hasLocalClientCommandHandlers); const normalized = parseHostedRootCommandSurface(argv); if (normalized.kind === 'help') { - const help = formatRootHelp(HOSTED_ROOT_HELP); + const help = formatRootHelp(rootHelp); if (normalized.exitCode !== EXIT_CODES.SUCCESS) { throw new CommanderCompatibleError(help, normalized.exitCode); } @@ -269,10 +275,13 @@ async function dispatchHosted( } if (normalized.kind === 'completion') { const manifest = await getPresentationManifest(client, enableServerWebFetch); - await writeToStream(stdout, hostedCompletions(manifest, normalized.argv).join('\n') + '\n'); + await writeToStream(stdout, hostedCompletions(manifest, normalized.argv, hasLocalClientCommandHandlers).join('\n') + '\n'); return; } const args = normalized.argv; + if (!hasLocalClientCommandHandlers && isLocalClientRootCommand(args[0])) { + throw new CommanderCompatibleError(`error: unknown command '${args[0]}'\n`, EXIT_CODES.USAGE_ERROR); + } if (args[0] === 'completion') { const parsed = parseHostedCompletionSurface(args.slice(1), normalized.literal); if (parsed.kind === 'help') { @@ -501,7 +510,7 @@ async function dispatchHosted( return; } if (unknownRoot.help) { - await writeToStream(stdout, formatRootHelp(HOSTED_ROOT_HELP)); + await writeToStream(stdout, formatRootHelp(rootHelp)); return; } // No help on stdout: an error path that emits a well-formed document to @@ -1622,7 +1631,7 @@ function hasTerminalBeforeSeparator( return false; } -function hostedCompletions(manifest: HostedManifest, argv: string[]): string[] { +function hostedCompletions(manifest: HostedManifest, argv: string[], hasLocalClientCommandHandlers = true): string[] { const index = argv.indexOf('--get-completions'); const rest = index === -1 ? argv : argv.slice(index + 1); const words: string[] = []; @@ -1638,7 +1647,7 @@ function hostedCompletions(manifest: HostedManifest, argv: string[]): string[] { hostedCommands(manifest), words, Number.isFinite(cursor) ? cursor! : words.length, - HOSTED_BUILTIN_COMMANDS.filter(command => command !== 'web'), + getHostedBuiltinCommands(hasLocalClientCommandHandlers).filter(command => command !== 'web'), ); } diff --git a/src/main.ts b/src/main.ts index 55789529..9240cae3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -87,7 +87,10 @@ if (!fastPathHandled) { const { runHostedCli } = await import('./hosted/runner.js'); // The installed CLI already owns local web/fetch transport authority. // Programmatic embedders remain opt-in and default to no network access. - const result = await runHostedCli(argv, { enableServerWebFetch: true }); + const result = await runHostedCli(argv, { + enableServerWebFetch: true, + hasLocalClientCommandHandlers: true, + }); process.exitCode = result.exitCode; } else { const { installDaemonRunSignalCancellation } = await import('./signal-cancel.js'); From af14fb5b337b1895b2887562a365aa60a81c7313 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 17:20:11 +0530 Subject: [PATCH 6/7] fix(hosted): reserve client command completion roots --- src/hosted/programmatic.test.ts | 31 ++++++++++++++++++++++++++++++- src/hosted/runner.ts | 4 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/hosted/programmatic.test.ts b/src/hosted/programmatic.test.ts index 6140d6ff..2bfee70c 100644 --- a/src/hosted/programmatic.test.ts +++ b/src/hosted/programmatic.test.ts @@ -53,7 +53,36 @@ describe('runHostedProgrammatic', () => { }); expect(result).toMatchObject({ exitCode: 0, stderr: '' }); - expect(result.stdout.split('\n')).not.toEqual(expect.arrayContaining(['skills', 'update'])); + const completions = result.stdout.split('\n'); + expect(completions).not.toContain('skills'); + expect(completions).not.toContain('update'); + }); + + it('does not offer manifest sites that collide with client-owned commands', async () => { + const result = await runHostedProgrammatic({ + argv: ['--get-completions', '--cursor', '0'], + apiBaseUrl: 'http://127.0.0.1:8787', + accessToken: 'oauth-access-token', + fetchImpl: fakeCloud(() => new Response(JSON.stringify({ + ...manifest, + commands: ['skills', 'update'].map(site => ({ + site, + name: 'status', + command: `${site}/status`, + description: `Show ${site} status`, + access: 'read', + strategy: 'PUBLIC', + browser: false, + args: [], + columns: [], + })), + }), { status: 200, headers: { 'content-type': 'application/json' } })), + }); + + expect(result).toMatchObject({ exitCode: 0, stderr: '' }); + const completions = result.stdout.split('\n'); + expect(completions).not.toContain('skills'); + expect(completions).not.toContain('update'); }); it.each(['skills', 'update'])('rejects %s help instead of returning generic root help', async (command) => { diff --git a/src/hosted/runner.ts b/src/hosted/runner.ts index d4e98758..0206b3ad 100644 --- a/src/hosted/runner.ts +++ b/src/hosted/runner.ts @@ -1643,8 +1643,10 @@ function hostedCompletions(manifest: HostedManifest, argv: string[], hasLocalCli words.push(rest[i]!); } } + const commands = hostedCommands(manifest) + .filter(command => hasLocalClientCommandHandlers || !isLocalClientRootCommand(command.site)); return getCommandCompletionCandidates( - hostedCommands(manifest), + commands, words, Number.isFinite(cursor) ? cursor! : words.length, getHostedBuiltinCommands(hasLocalClientCommandHandlers).filter(command => command !== 'web'), From 27c7409f4116adb022efa4afc4dee9b16bb10e89 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Thu, 27 Aug 2026 19:36:55 +0530 Subject: [PATCH 7/7] fix(hosted): route selector-prefixed client commands locally --- src/hosted/main-lifecycle.test.ts | 27 +++++++++++++++++++++++++++ src/hosted/programmatic.test.ts | 8 +++++--- src/main.ts | 15 ++++++++++----- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/hosted/main-lifecycle.test.ts b/src/hosted/main-lifecycle.test.ts index dbe46f11..87139d1c 100644 --- a/src/hosted/main-lifecycle.test.ts +++ b/src/hosted/main-lifecycle.test.ts @@ -152,6 +152,33 @@ describe('hosted CLI process lifecycle', () => { expect(fixture.requests).toEqual([]); }, 20_000); + it.each([ + { + name: 'profile before skills', + argv: ['--profile', 'work', 'skills', '--help'], + help: 'Usage: webcmd skills [options] [command]', + }, + { + name: 'workspace before update', + argv: ['--workspace', 'ws', 'update', '--help'], + help: 'update [options]', + }, + { + name: 'session before skills', + argv: ['--session', 'session_work', 'skills', '--help'], + help: 'Usage: webcmd skills [options] [command]', + }, + ])('routes $name to the local client command handler', async ({ argv, help }) => { + const fixture = await createHostedFixture('success'); + + const result = await runCli(argv, fixture.env); + + expect(result.status).toBe(0); + expect(result.stdout).toContain(help); + expect(result.stderr).toBe(''); + expect(fixture.requests).toEqual([]); + }, 20_000); + it('keeps hosted auth on Cloud without local discovery', async () => { const fixture = await createHostedFixture('success'); diff --git a/src/hosted/programmatic.test.ts b/src/hosted/programmatic.test.ts index 2bfee70c..f519b288 100644 --- a/src/hosted/programmatic.test.ts +++ b/src/hosted/programmatic.test.ts @@ -86,16 +86,18 @@ describe('runHostedProgrammatic', () => { }); it.each(['skills', 'update'])('rejects %s help instead of returning generic root help', async (command) => { + const fetchImpl = vi.fn(fakeCloud()); const result = await runHostedProgrammatic({ argv: [command, '--help'], apiBaseUrl: 'http://127.0.0.1:8787', accessToken: 'oauth-access-token', - fetchImpl: fakeCloud(), + fetchImpl, }); - expect(result.exitCode).not.toBe(0); + expect(result.exitCode).toBe(2); expect(result.stdout).toBe(''); - expect(result.stderr).not.toContain('Usage:'); + expect(result.stderr).toBe(`error: unknown command '${command}'\n`); + expect(fetchImpl).not.toHaveBeenCalled(); }); it('returns captured stdout and a zero exit code', async () => { diff --git a/src/main.ts b/src/main.ts index 9240cae3..25b4f3b3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,6 +36,7 @@ const USER_PLUGINS = path.join(os.homedir(), CONFIG_DIR_NAME, 'plugins'); // ── Ultra-fast path: lightweight commands bypass full discovery ────────── // These are high-frequency or trivial paths that must not pay the startup tax. const argv = process.argv.slice(2); +const normalizedRootArgv = normalizedRootArgvFor(argv); if (typeof (globalThis as { Bun?: unknown }).Bun === 'undefined' && !isSupportedNodeVersion(process.version)) { process.stderr.write( @@ -75,10 +76,10 @@ if (!fastPathHandled) { if (argv[0] === 'setup') { const { runHostedSetup } = await import('./hosted/setup.js'); process.exitCode = await runHostedSetup({ argv: argv.slice(1) }); - } else if (argv[0] === 'skills' || argv[0] === 'update') { + } else if (normalizedRootArgv?.[0] === 'skills' || normalizedRootArgv?.[0] === 'update') { const { createProgram } = await import('./cli.js'); await createProgram(BUILTIN_CLIS, USER_CLIS).parseAsync(argv, { from: 'user' }); - } else if (isWebFetch(argv)) { + } else if (isWebFetch(normalizedRootArgv)) { const { runWebFetchCommand } = await import('./fetch/command.js'); await runWebFetchCommand(argv); } else { @@ -104,15 +105,19 @@ if (!fastPathHandled) { } } -function isWebFetch(args: readonly string[]): boolean { +function normalizedRootArgvFor(args: readonly string[]): string[] | undefined { try { const parsed = parseHostedRootCommandSurface(args); - return parsed.kind === 'dispatch' && parsed.argv[0] === 'web' && parsed.argv[1] === 'fetch'; + return parsed.kind === 'dispatch' ? parsed.argv : undefined; } catch { - return false; + return undefined; } } +function isWebFetch(args: readonly string[] | undefined): boolean { + return args?.[0] === 'web' && args[1] === 'fetch'; +} + async function runLocalMain(): Promise { // Fast path: --get-completions — read from manifest, skip discovery const getCompIdx = process.argv.indexOf('--get-completions');