From 10a9f3fe5d5976490c814ca3c8016346b26d2cb8 Mon Sep 17 00:00:00 2001 From: Agnik47 <140933190+Agnik47@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:18:50 +0530 Subject: [PATCH 1/2] fix(cli): name hosted-only mode instead of sending `artifact` to a plugin hunt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `webcmd artifact` in local mode answered: Site "artifact" is not installed. Search: webcmd plugin search artifact There is no such plugin. `artifact` is a hosted-mode builtin (hosted/runner.ts), and shell completion offers it in both modes (completion-shared.ts), so a local user following either signal was sent after something that cannot be installed. Hosted mode already names the reverse case with LOCAL_ONLY_COMMAND_HELP. Add the mirror, and answer an unknown root token that hosted mode serves with the mode boundary and the way across it. The hosted-only set is derived — hosted root help minus the local root registry — so a hosted command added later is covered without editing this file. `setup` is excluded explicitly: both modes serve it, and it is outside the local registry only because main.ts answers it before Commander parses argv. The exact hosted name is settled ahead of the edit-distance suggestions, since a name that matches exactly is stronger evidence than a fuzzy guess. Unknown sites, near misses, and the intent overrides are untouched, and the usage exit code stays 2. --- plugin-command-manifest.json | 37 ++++++++++++++++++++++++++++++++++++ src/command-suggest.test.ts | 37 +++++++++++++++++++++++++++++++++++- src/command-suggest.ts | 28 ++++++++++++++++++++++++++- src/completion-shared.ts | 2 ++ 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/plugin-command-manifest.json b/plugin-command-manifest.json index c812e0a1..79698b8a 100644 --- a/plugin-command-manifest.json +++ b/plugin-command-manifest.json @@ -20486,6 +20486,43 @@ "sourceFile": "plugins/reddit/comment.js", "navigateBefore": "https://reddit.com" }, + { + "site": "reddit", + "name": "draft-comment", + "description": "Draft a comment on a Reddit post without submitting it", + "access": "write", + "example": "webcmd reddit draft-comment --window foreground", + "domain": "reddit.com", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "post-id", + "type": "string", + "required": true, + "positional": true, + "help": "Post ID (e.g. 1abc123), t3 fullname, or Reddit post URL" + }, + { + "name": "text", + "type": "string", + "required": true, + "positional": true, + "help": "Comment text to leave in the composer" + } + ], + "columns": [ + "status", + "message", + "url" + ], + "type": "js", + "modulePath": "plugins/reddit/draft-comment.js", + "sourceFile": "plugins/reddit/draft-comment.js", + "navigateBefore": false, + "siteSession": "persistent", + "freshPage": true + }, { "site": "reddit", "name": "frontpage", diff --git a/src/command-suggest.test.ts b/src/command-suggest.test.ts index c2a2eb85..ca10105d 100644 --- a/src/command-suggest.test.ts +++ b/src/command-suggest.test.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import { handleProgramParseError } from './cli-error-report.js'; import { createProgram } from './cli.js'; -import { editDistance, isReservedRootCommand, unknownRootCommandMessage, unknownSubcommandMessage } from './command-suggest.js'; +import { editDistance, isHostedOnlyRootCommand, isReservedRootCommand, unknownRootCommandMessage, unknownSubcommandMessage } from './command-suggest.js'; import { HOSTED_ROOT_HELP } from './completion-shared.js'; import { WEBCMD_ROOT_COMMANDS } from './hooks.js'; @@ -41,6 +41,23 @@ describe('unknown root command', () => { expect(message).toContain('Did you mean: webcmd plugin search '); }); + it('names hosted-only mode instead of sending artifact to a plugin hunt', () => { + const message = unknownRootCommandMessage(createProgram('', ''), 'artifact'); + + expect(message).toContain('"artifact" is a hosted-mode command'); + expect(message).toContain('choose hosted mode'); + // the plugin does not exist, so the old advice could only waste a turn + expect(message).not.toContain('plugin search'); + expect(message).not.toContain('is not installed'); + }); + + it('answers the same way regardless of the case typed', () => { + const message = unknownRootCommandMessage(createProgram('', ''), 'Artifact'); + + expect(message).toContain('"Artifact" is a hosted-mode command'); + expect(message).not.toContain('plugin search'); + }); + it('still guides a genuinely unknown token to plugin search', () => { const message = unknownRootCommandMessage(createProgram('', ''), 'zzzqqqwww'); @@ -77,6 +94,24 @@ describe('reserved roots', () => { }); }); +describe('hosted-only roots', () => { + it('is exactly the hosted surface local mode does not register', () => { + // derived, not listed: a hosted command added later is covered for free + for (const name of WEBCMD_ROOT_COMMANDS) expect(isHostedOnlyRootCommand(name)).toBe(false); + expect(isHostedOnlyRootCommand('artifact')).toBe(true); + expect(isHostedOnlyRootCommand('github')).toBe(false); + }); + + it('never claims a locally served command is hosted-only', () => { + // `web` ships in both surfaces; calling it hosted-only would be a new lie + expect(isHostedOnlyRootCommand('web')).toBe(false); + expect(isHostedOnlyRootCommand('browser')).toBe(false); + expect(isHostedOnlyRootCommand('doctor')).toBe(false); + // `setup` is served locally by main.ts before Commander parses argv + expect(isHostedOnlyRootCommand('setup')).toBe(false); + }); +}); + describe('unknown namespace subcommand', () => { it('suggests adapter status and lists the valid subcommands', () => { const message = unknownSubcommandMessage(namespaceOf(createProgram('', ''), 'adapter'), 'list'); diff --git a/src/command-suggest.ts b/src/command-suggest.ts index bc7c46c0..51dd7033 100644 --- a/src/command-suggest.ts +++ b/src/command-suggest.ts @@ -13,7 +13,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { Command } from 'commander'; import { CLI_COMMAND } from './brand.js'; -import { HOSTED_ROOT_HELP } from './completion-shared.js'; +import { HOSTED_ONLY_COMMAND_HELP, HOSTED_ROOT_HELP } from './completion-shared.js'; import { getAdapterLoadFailures, missingPluginGuidance, PLUGINS_DIR, USER_CLIS_DIR } from './discovery.js'; import { WEBCMD_ROOT_COMMANDS } from './hooks.js'; @@ -136,6 +136,15 @@ export function unknownRootCommandMessage( const canonical = CANONICAL_ROOT[name.toLowerCase()]; if (canonical) return `Unknown command "${name}".\nDid you mean: ${canonical}`; + // An exact hosted-command name is stronger evidence than any edit-distance + // guess below it, so this is settled before suggestions run. + if (isHostedOnlyRootCommand(name.toLowerCase())) { + return [ + `"${name}" is a hosted-mode command and this installation is in local mode.`, + HOSTED_ONLY_COMMAND_HELP, + ].join('\n'); + } + const suggestions = suggestCommands(name, commandCandidates(program)); if (suggestions.length > 0) return `Unknown command "${name}".\n${formatSuggestions(suggestions)}`; @@ -164,6 +173,23 @@ export function isReservedRootCommand(name: string): boolean { || HOSTED_ROOT_HELP.commands.some(command => command.name.split(/\s/, 1)[0] === name); } +/** + * A command hosted mode serves that local mode never registers. + * + * Derived rather than listed, so a hosted command added later is covered + * without touching this file. Today that is `artifact`: shell completion offers + * it in both modes, but locally it resolved to "Site is not installed. Search: + * webcmd plugin search artifact" — a hunt for a plugin that cannot exist. + */ +export function isHostedOnlyRootCommand(name: string): boolean { + if (WEBCMD_ROOT_COMMANDS.has(name)) return false; + // `setup` chooses the mode, so both modes serve it. It is absent from + // WEBCMD_ROOT_COMMANDS only because main.ts answers it before Commander sees + // the argv at all, which would otherwise read here as hosted-only. + if (name === 'setup') return false; + return HOSTED_ROOT_HELP.commands.some(command => command.name.split(/\s/, 1)[0] === name); +} + /** Message for an unknown subcommand inside a namespace. Caller writes it to stderr. */ export function unknownSubcommandMessage(namespace: Command, name: string): string { const nsPath = namespace.name(); diff --git a/src/completion-shared.ts b/src/completion-shared.ts index 3990dd96..cb412dde 100644 --- a/src/completion-shared.ts +++ b/src/completion-shared.ts @@ -26,6 +26,8 @@ export const BUILTIN_COMMANDS = [ ]; export const LOCAL_ONLY_COMMAND_HELP = 'Run `webcmd setup` and choose local mode to use local-only commands.'; +/** Mirror of {@link LOCAL_ONLY_COMMAND_HELP} for a hosted command reached from local mode. */ +export const HOSTED_ONLY_COMMAND_HELP = 'Run `webcmd setup` and choose hosted mode to use hosted-only commands.'; const HOSTED_CLIENT_ROOT_COMMANDS: readonly RootHelpCommand[] = [ { name: 'adapter', description: 'Manage hosted adapter sources and overrides' }, From 1247e90cc3b38c94f368b6e28f9105fc797a2762 Mon Sep 17 00:00:00 2001 From: Agnik47 <140933190+Agnik47@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:26:21 +0530 Subject: [PATCH 2/2] docs: mark artifact hosted-only in the top-level command table The Top-Level Commands table listed `artifact` beside `doctor`, `daemon`, and `web` with no mode marker, so a local reader saw it as a peer of commands local mode serves. Use the "Hosted mode only." wording the flag table already uses. Also corrects the comment added with this fix: shell completion does not offer `artifact` in local mode. `HOSTED_CLIENT_ROOT_COMMANDS` feeds only getHostedRootHelp, which is called from hosted/runner.ts, and `webcmd completion bash|zsh` emits no `artifact` entry locally. The reference table is what pointed local users at the command. --- docs/cli-reference.mdx | 2 +- plugin-command-manifest.json | 37 ------------------------------------ src/command-suggest.ts | 7 ++++--- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/docs/cli-reference.mdx b/docs/cli-reference.mdx index d492a2cb..96da742c 100644 --- a/docs/cli-reference.mdx +++ b/docs/cli-reference.mdx @@ -142,7 +142,7 @@ Ordinary `curl` is neither required nor automatically authenticated. | `setup` | Choose local or hosted mode interactively. | | `doctor` | Diagnose browser bridge and daemon connectivity. | | `daemon` | Manage the local Webcmd daemon: status, stop, and restart. | -| `artifact` | Download a hosted execution artifact to `--output`. | +| `artifact` | Hosted mode only. Download a hosted execution artifact to `--output`. | | `browser` | Agent-facing browser runtime for exploration and verification. | | `web` | Local URL fetch helpers. | | `profile` | List, rename, and select browser runtime profiles. | diff --git a/plugin-command-manifest.json b/plugin-command-manifest.json index 79698b8a..c812e0a1 100644 --- a/plugin-command-manifest.json +++ b/plugin-command-manifest.json @@ -20486,43 +20486,6 @@ "sourceFile": "plugins/reddit/comment.js", "navigateBefore": "https://reddit.com" }, - { - "site": "reddit", - "name": "draft-comment", - "description": "Draft a comment on a Reddit post without submitting it", - "access": "write", - "example": "webcmd reddit draft-comment --window foreground", - "domain": "reddit.com", - "strategy": "ui", - "browser": true, - "args": [ - { - "name": "post-id", - "type": "string", - "required": true, - "positional": true, - "help": "Post ID (e.g. 1abc123), t3 fullname, or Reddit post URL" - }, - { - "name": "text", - "type": "string", - "required": true, - "positional": true, - "help": "Comment text to leave in the composer" - } - ], - "columns": [ - "status", - "message", - "url" - ], - "type": "js", - "modulePath": "plugins/reddit/draft-comment.js", - "sourceFile": "plugins/reddit/draft-comment.js", - "navigateBefore": false, - "siteSession": "persistent", - "freshPage": true - }, { "site": "reddit", "name": "frontpage", diff --git a/src/command-suggest.ts b/src/command-suggest.ts index 51dd7033..9389d832 100644 --- a/src/command-suggest.ts +++ b/src/command-suggest.ts @@ -177,9 +177,10 @@ export function isReservedRootCommand(name: string): boolean { * A command hosted mode serves that local mode never registers. * * Derived rather than listed, so a hosted command added later is covered - * without touching this file. Today that is `artifact`: shell completion offers - * it in both modes, but locally it resolved to "Site is not installed. Search: - * webcmd plugin search artifact" — a hunt for a plugin that cannot exist. + * without touching this file. Today that is `artifact`, which the CLI reference + * lists among the top-level commands: locally it resolved to "Site is not + * installed. Search: webcmd plugin search artifact" — a hunt for a plugin that + * cannot exist. */ export function isHostedOnlyRootCommand(name: string): boolean { if (WEBCMD_ROOT_COMMANDS.has(name)) return false;