Skip to content

fix: make client-owned web fetch discoverable (#252) - #263

Open
ankitranjan7 wants to merge 3 commits into
mainfrom
fix/252-web-fetch-discoverability
Open

fix: make client-owned web fetch discoverable (#252)#263
ankitranjan7 wants to merge 3 commits into
mainfrom
fix/252-web-fetch-discoverability

Conversation

@ankitranjan7

@ankitranjan7 ankitranjan7 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #252.

The problem

webcmd web fetch always works, but the CLI never tells you it exists.

The command is intercepted on the client-owned fast path in src/main.ts:79, before adapter discovery — so it runs on a fresh install with zero plugins. It just isn't listed anywhere, and asking for help throws:

$ webcmd --help                       # no `web` under Commands or Site adapters
$ webcmd list                         # no web fetch
$ webcmd list -f json                 # 0 entries with "site": "web"
$ webcmd --get-completions webcmd web # (empty)
$ webcmd web fetch --help
ArgumentError: --url must be an http or https URL

So the only way to discover a built-in command is to already know it.

What I changed

  1. clis/web/fetch.js — registers the command through a new makeWebFetchCommand() factory, so both build-manifest and the runtime filesystem scan pick it up.
  2. src/discovery.ts — the module pattern now matches make<Pascal>Command(, the same convention src/build-manifest.ts:48 already used.
  3. cli-manifest.json — gains the generated web/fetch entry. It regenerates byte-identical, check:hosted-contract passes, and hosted-contract.json is unchanged because this command is not browser-based.
  4. src/fetch/command.ts — now that help is real, the fast path honours what it advertises: -f/--format for output (markdown still the default), structured --help -f yaml|json, an error on an unsupported format instead of silently falling back to a table, and an error when --timeout/--max-chars is given a flag-shaped value instead of quietly coercing it to 1.

The fast path itself stays exactly where it was — execution never touches the registry, so hosted mode still never cloud-routes this command.

Proof

Surface Before After
webcmd --help no web web under Site adapters
webcmd list absent fetch [public] … [builtin]
webcmd list -f json absent full entry with all four args
webcmd web fetch -h ArgumentError real help
webcmd --get-completions web '' empty fetch, fetch-browser

Verified against a rebuilt dist. Four new tests in src/fetch/command.test.ts cover the -f output path, structured help, the format rejection and the --timeout -5 rejection. Full suite passes except tests/e2e/plugin-management.test.ts, which clones plugins over the network and fails identically on main (fixed separately in #267).

Not in this PR

🤖 Generated with Claude Code

`web fetch` always worked via the main.ts fast path but was invisible to
`--help`, `list`, `cli-manifest.json`, and completions unless the `web`
plugin was installed, and `web fetch -h` threw instead of printing help.

Register the command from clis/web/fetch.js via a makeWebFetchCommand()
factory so build-manifest and filesystem discovery both see it, and keep
execution on the fast path so hosted mode never cloud-routes it.

The fast path now honours the flags its help advertises: -f/--format for
output and structured --help, an error for unsupported formats, and an
error for a flag-shaped --timeout/--max-chars value instead of coercing
it to 1.

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

github-actions Bot commented Aug 10, 2026

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.

@beubax

beubax commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Maintainer review: changes requested

Issue #252 is valid. webcmd web fetch is always available through the client-owned fast path, so hiding it from --help, list, completions, and structured help is an inconsistent command surface. Keeping execution on a fast path while deriving presentation metadata from the same argument definition is a reasonable direction.

Before this lands, the local/hosted ownership contract needs to be explicit.

Blocking Cloud contract ambiguity

This PR adds web/fetch to cli-manifest.json as a normal PUBLIC, non-browser command. During package build, the hosted contract is generated from that manifest. Under the existing availability rules, a PUBLIC command without a local-only/app domain is hosted-capable.

At the same time, main.ts intercepts every web fetch invocation before shouldUseHostedMode(). Thus the normal CLI continues to execute the command locally even when the user selected hosted mode.

Today Cloud sees no root clis/ directory in the 0.6.0 package, so this conflict is masked. PR #271 proposes staging the built-ins under dist/src/clis; once Cloud is updated to read that staged manifest, it can advertise and load web/fetch as a server-side default while the thin CLI still bypasses Cloud for the same grammar.

That creates two different ownership paths:

  • CLI invocation: always local/client-owned;
  • hosted manifest/direct API: potentially server-executed.

This contradicts the stated client-owned behavior in this PR and makes network-origin, timeout, billing, proxy, and SSRF semantics depend on how the command was reached.

Required product decision

Please choose and encode one of these contracts:

  1. Client-owned command: introduce/derive metadata that excludes web/fetch from the hosted adapter manifest/contract while still including it in local help, list, and completions; or
  2. Hosted in hosted mode: move the hosted-mode boundary ahead of this fast path so hosted users call Cloud, and implement/test the Cloud execution contract deliberately.

Do not rely on the current missing-clis packaging bug to keep the command out of Cloud.

The north-star currently emphasizes a thin hosted client and server-side hosted execution, while this PR emphasizes an intentional client-owned exception. Either can work, but the exception must be represented in metadata rather than inferred from main.ts control flow.

Required tests

Please add integration-level assertions for:

  • top-level help lists web;
  • webcmd list -f json contains exactly one web/fetch entry;
  • completions contain fetch;
  • web fetch --help and structured help do not require --url;
  • local execution still uses the intended fast path;
  • hosted-mode execution either remains provably client-owned and absent from the hosted manifest, or routes to Cloud—according to the decision above;
  • the package built with fix: ship web fetch-browser so the blocked-fetch escalation works (#247) #271 remains executable from its installed layout.

The unit tests added here cover the parser/presentation helpers, but the bug concerns cross-surface discoverability and ownership, so at least one end-to-end surface test is warranted.

Scope note

This PR also expands output-format and validation behavior beyond the discoverability bug. Those changes are defensible if retained, but they should be tested as compatibility changes rather than assumed to follow automatically from Commander help. In particular, keep the hand-parsed fast path aligned with accepted common-option spellings.

Please coordinate this with #271 and the Cloud loader change rather than releasing the manifest changes independently.

web fetch is executed by the CLI on every path, including hosted mode.
That was only true because of where main.ts intercepts it, so once Cloud
reads the staged manifest the same grammar could also resolve to a
server-side default with different network, timeout, billing and SSRF
semantics.

Carry the ownership in metadata: commands can declare clientOwned, it
flows through cli() into the manifest, and deriveHostedAvailability marks
such a command local-only with reason client-owned. The command stays in
local help, list and completions; the hosted contract states it is never
served.

Adds e2e coverage for each surface (help, list, completions, help without
--url, structured help, local execution in both modes) and the contract.

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

Copy link
Copy Markdown
Contributor Author

Agreed the ownership had to be encoded rather than inferred. Pushed 6790000, taking option 1: web fetch is a client-owned command.

Reasoning, so you can push back if the north-star says otherwise: the command exists precisely because it needs no browser and no server — a thin local fast path is the feature. Routing it to Cloud (option 2) would add a network hop and a billing/proxy/SSRF surface to the one command that deliberately has none. If you'd rather it become server-owned in hosted mode, say so and I'll redo it as option 2 instead; the metadata flag is the only thing that would change.

The contract, in metadata

  • BaseCliCommand.clientOwned?: boolean — declared on the command, carried through cli() into cli-manifest.json;
  • deriveHostedAvailability() returns { mode: 'local-only', reason: 'client-owned' } for it, ahead of the strategy/domain rules, so PUBLIC + non-browser no longer implies hosted-capable;
  • the generated hosted-contract.json now carries availability: { mode: 'local-only', reason: 'client-owned' } and sessionPolicy: 'local-only' for web/fetch. hosted/runner.ts already refuses local-only commands, so once Cloud reads the staged manifest from fix: ship web fetch-browser so the blocked-fetch escalation works (#247) #271 it cannot advertise or execute a second copy.

Nothing about the decision depends on the missing-clis packaging bug any more — remove that bug (which #271 does) and the contract still says local-only.

Tests

New e2e file tests/e2e/web-fetch-discoverability.test.ts (registered in the e2e project), driving the built binary:

  • top-level --help lists web;
  • list -f json contains exactly one web/fetch entry;
  • --get-completions --cursor 2 web offers fetch;
  • web fetch --help exits 0 and does not demand --url;
  • structured help (--help -f json) likewise;
  • web fetch with no --url fails on the local parser in both local and hosted mode — the hosted fixture points at an unresolvable API base URL, so a Cloud round-trip would show up as a DNS error rather than the --url message;
  • hosted-contract.json marks web/fetch local-only/client-owned.

Plus a unit assertion in availability.test.ts that clientOwned flips availability while a plain PUBLIC command stays hosted.

tsc --noEmit clean, check:hosted-contract passes (generated bytes match committed), 4788 unit+plugin tests pass, 7/7 in the new e2e file.

Scope note

Fair point on the output-format/validation work — the hand-parsed fast path now rejects an unknown -f/--format with the same list the help block advertises, and accepts the same spellings Commander does (-f json, --format=json, -fjson); that's covered in src/fetch/command.test.ts. The installed-layout executability assertion you asked for lives in #271's check:package-bin, which now loads every manifest module from the npm-installed prefix. Happy to sequence this behind #271 for release.

The e2e project only runs a named subset of browser files in CI, so the
new assertions would never have executed there. The plugin project runs in
full on every OS after a build, which is what a test driving the built
binary needs.

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

Copy link
Copy Markdown
Contributor Author

Follow-up dfd970e: moved the new surface tests from tests/e2e/ to clis/web/test/fetch-surface.test.ts (the plugin project). CI's e2e project only runs a named subset of browser files, so as written the assertions would never have run there; the plugin job runs in full on Linux/macOS/Windows after npm run build, which is what a test driving the built binary needs. Same 7 assertions, all passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

webcmd web fetch is invisible to --help, list, completions, and -h despite always being available

2 participants