From 6525d5f88257317f3adf548a8bc22be9614d2e16 Mon Sep 17 00:00:00 2001 From: beubax Date: Mon, 10 Aug 2026 15:37:43 +0530 Subject: [PATCH 1/2] Refresh monorepo cache on missing sub-plugin install --- src/plugin.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/plugin.ts | 34 ++++++++++++++++++++-------------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/plugin.test.ts b/src/plugin.test.ts index 85264eb..46eae40 100644 --- a/src/plugin.test.ts +++ b/src/plugin.test.ts @@ -1262,6 +1262,47 @@ describe('installPlugin with existing monorepo', () => { expect(npmCalls.some(([, , opts]) => opts?.cwd === repoDir)).toBe(true); expect(fs.realpathSync(pluginLink)).toBe(fs.realpathSync(subDir)); }); + + it('refreshes an existing monorepo cache when the requested sub-plugin is missing locally', () => { + fs.mkdirSync(path.join(repoDir, 'packages', 'alpha'), { recursive: true }); + fs.writeFileSync(path.join(repoDir, 'webcmd-plugin.json'), JSON.stringify({ + plugins: { + alpha: { path: 'packages/alpha' }, + }, + })); + + mockExecFileSync.mockImplementation((cmd, args) => { + if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') { + const cloneDir = String(args[4]); + const subDir = path.join(cloneDir, 'packages', pluginName); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(cloneDir, 'package.json'), JSON.stringify({ + name: repoName, + private: true, + workspaces: ['packages/*'], + })); + fs.writeFileSync(path.join(cloneDir, 'webcmd-plugin.json'), JSON.stringify({ + plugins: { + alpha: { path: 'packages/alpha' }, + [pluginName]: { path: `packages/${pluginName}` }, + }, + })); + fs.writeFileSync(path.join(subDir, 'hello.js'), 'cli({ site: "test", name: "hello", access: "read" })'); + return ''; + } + if (cmd === 'git' && Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') { + return '1234567890abcdef1234567890abcdef12345678\n'; + } + return ''; + }); + + installPlugin(`github:user/${repoName}/${pluginName}`); + + const refreshedSubDir = path.join(repoDir, 'packages', pluginName); + const refreshedManifest = JSON.parse(fs.readFileSync(path.join(repoDir, 'webcmd-plugin.json'), 'utf-8')); + expect(refreshedManifest.plugins[pluginName]).toEqual({ path: `packages/${pluginName}` }); + expect(fs.realpathSync(pluginLink)).toBe(fs.realpathSync(refreshedSubDir)); + }); }); describe('updatePlugin transactional staging', () => { diff --git a/src/plugin.ts b/src/plugin.ts index fce4f14..4208ca9 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -939,8 +939,21 @@ function installMonorepo( const monoreposDir = getMonoreposDir(); const repoDir = path.join(monoreposDir, repoName); const repoAlreadyInstalled = fs.existsSync(repoDir); - const repoRoot = repoAlreadyInstalled ? repoDir : cloneDir; - const effectiveManifest = repoAlreadyInstalled ? readPluginManifest(repoDir) : manifest; + let repoRoot = repoAlreadyInstalled ? repoDir : cloneDir; + let effectiveManifest = repoAlreadyInstalled ? readPluginManifest(repoDir) : manifest; + let publishRepo = repoAlreadyInstalled ? undefined : { stagingDir: cloneDir, parentDir: monoreposDir }; + + if ( + repoAlreadyInstalled + && subPlugin + && (!effectiveManifest?.plugins?.[subPlugin] || effectiveManifest.plugins[subPlugin].disabled) + && manifest.plugins?.[subPlugin] + && !manifest.plugins[subPlugin].disabled + ) { + repoRoot = cloneDir; + effectiveManifest = manifest; + publishRepo = { stagingDir: cloneDir, parentDir: monoreposDir }; + } if (!effectiveManifest || !isMonorepo(effectiveManifest)) { throw new PluginError(`Monorepo manifest missing or invalid at ${repoRoot}`); @@ -1009,23 +1022,16 @@ function installMonorepo( const publishPlugins = eligiblePlugins.map(({ name, entry }) => ({ name, subPath: entry.path })); - if (repoAlreadyInstalled) { - postInstallMonorepoLifecycle( - repoDir, - eligiblePlugins.map((p) => resolveRepoContainedPath(repoDir, p.entry.path)), - ); - } else { - postInstallMonorepoLifecycle( - cloneDir, - eligiblePlugins.map((p) => resolveRepoContainedPath(cloneDir, p.entry.path)), - ); - } + postInstallMonorepoLifecycle( + repoRoot, + eligiblePlugins.map((p) => resolveRepoContainedPath(repoRoot, p.entry.path)), + ); publishMonorepoPlugins( repoDir, PLUGINS_DIR, publishPlugins, - repoAlreadyInstalled ? undefined : { stagingDir: cloneDir, parentDir: monoreposDir }, + publishRepo, (commitHash) => { for (const { name, entry } of eligiblePlugins) { if (commitHash) { From a74efec7b42dbcdcc6ff287e217e110f079e5dfa Mon Sep 17 00:00:00 2001 From: beubax Date: Mon, 10 Aug 2026 15:50:40 +0530 Subject: [PATCH 2/2] docs: refresh plugin discovery and cloud provider guidance --- docs/authentication-and-profiles.mdx | 2 +- docs/cli-reference.mdx | 6 ++++-- docs/local-or-cloud.mdx | 2 +- docs/publish-community-plugin.mdx | 2 +- scripts/migrate-cli-sites.mjs | 2 +- skills/smart-search/SKILL.md | 4 ++-- skills/webcmd-browser/SKILL.md | 2 +- skills/webcmd-usage/SKILL.md | 6 +++--- src/hosted/client.test.ts | 6 +++--- src/hosted/runner.test.ts | 4 ++-- src/migrate-cli-sites.test.ts | 6 ++---- src/plugin-scaffold.test.ts | 2 ++ src/plugin-scaffold.ts | 5 +++-- src/skills.test.ts | 2 +- 14 files changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/authentication-and-profiles.mdx b/docs/authentication-and-profiles.mdx index f2738fc..5f32eb8 100644 --- a/docs/authentication-and-profiles.mdx +++ b/docs/authentication-and-profiles.mdx @@ -46,7 +46,7 @@ WEBCMD_WORKSPACE=user_64256 webcmd --profile work github whoami `--profile ` still selects a persona within the ambient workspace; it never crosses workspace boundaries. -Hosted `profile list` returns profile rows; `delete` returns `{ "ok": true, "deleted": true }`. Public profile fields are `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` while the hosted profile is being provisioned and `available` once it is ready. Kernel identifiers are never exposed as Webcmd API fields. +Hosted `profile list` returns profile rows; `delete` returns `{ "ok": true, "deleted": true }`. Public profile fields are `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` while the hosted profile is being provisioned and `available` once it is ready. Cloud provider identifiers are never exposed as Webcmd API fields. Delete hosted profiles only by immutable ID: diff --git a/docs/cli-reference.mdx b/docs/cli-reference.mdx index 21514b2..6f85e23 100644 --- a/docs/cli-reference.mdx +++ b/docs/cli-reference.mdx @@ -151,7 +151,7 @@ webcmd profile delete profile_abc123 In local mode, arbitrary `--profile ` values lazily create separate local state; local `list`, `rename`, and `use` are unchanged. In hosted mode, `--profile ` selects a persona within the ambient workspace; an omitted selector or `--profile default` lazily creates that workspace's `default` profile. -Hosted `list` returns profile rows scoped to the ambient workspace; `delete` takes an immutable profile ID and returns `{ "ok": true, "deleted": true }`, permanently removing that hosted browser state. Each profile row has `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` or `available`. Kernel identifiers are never exposed. +Hosted `list` returns profile rows scoped to the ambient workspace; `delete` takes an immutable profile ID and returns `{ "ok": true, "deleted": true }`, permanently removing that hosted browser state. Each profile row has `id`, `name`, `workspace`, `default`, `status`, `createdAt`, `updatedAt`, and `lastUsedAt`. `status` is `pending` or `available`. Cloud provider identifiers are never exposed. Prompt example: @@ -168,9 +168,11 @@ the returned `installSource`: ```bash webcmd plugin search ycombinator -f json -webcmd plugin install github:agentrhq/webcmd/ycombinator +webcmd plugin install ``` +Use direct `plugin install github:...` only when you already know the source. For missing or unknown sites, `plugin search` is the freshness boundary; it reads the current catalog instead of relying on package contents. + Hosted mode supports the same `plugin search` and `plugin install` grammar for Webcmd-verified marketplace adapters. Other plugin management commands remain local-only in hosted mode. ## Skills diff --git a/docs/local-or-cloud.mdx b/docs/local-or-cloud.mdx index 9d8f083..e6f6b47 100644 --- a/docs/local-or-cloud.mdx +++ b/docs/local-or-cloud.mdx @@ -19,7 +19,7 @@ Local mode runs Webcmd against this machine's tools, adapters, and browser. Choo ## Webcmd Cloud Alpha -Webcmd Cloud alpha is for supported commands and browser sessions on hosted infrastructure. Kernel provides the hosted browser infrastructure behind Webcmd. +Webcmd Cloud alpha is for supported commands and browser sessions on hosted infrastructure. Hosted browser sessions run through Webcmd Cloud's Browser Use-backed infrastructure for browser lifecycle, proxying, and live viewport access. ## Set Up Hosted Mode diff --git a/docs/publish-community-plugin.mdx b/docs/publish-community-plugin.mdx index 9e8c598..bf5087c 100644 --- a/docs/publish-community-plugin.mdx +++ b/docs/publish-community-plugin.mdx @@ -30,4 +30,4 @@ The agent runs `npm run check-community-plugins`, improves the plugin README and ## What Happens After Merge -Repository tooling adds the approved plugin to the generated root catalog and README community table, making it available through the repository's community plugin listing. +Repository tooling adds the approved plugin to the generated root catalog and README community table, making it discoverable through `webcmd plugin search`. Users install the returned `installSource`. diff --git a/scripts/migrate-cli-sites.mjs b/scripts/migrate-cli-sites.mjs index 02497dd..418b6d4 100644 --- a/scripts/migrate-cli-sites.mjs +++ b/scripts/migrate-cli-sites.mjs @@ -124,7 +124,7 @@ function readme(site, description, commands) { .slice() .sort((a, b) => String(a.name).localeCompare(String(b.name))) .map(command => `| \`webcmd ${site} ${command.name}\` | ${String(command.description ?? '').replaceAll('|', '\\|')} |`); - return `# webcmd-plugin-${site}\n\n${description}.\n\n## Install\n\n\`\`\`bash\nwebcmd plugin install github:agentrhq/webcmd/${site}\n\`\`\`\n\n## Commands\n\n| Command | Description |\n| --- | --- |\n${rows.join('\n')}\n`; + return `# webcmd-plugin-${site}\n\n${description}.\n\n## Install\n\n\`\`\`bash\nwebcmd plugin search ${site} -f json\nwebcmd plugin install \n\`\`\`\n\n## Commands\n\n| Command | Description |\n| --- | --- |\n${rows.join('\n')}\n`; } function readJson(file, fallback) { diff --git a/skills/smart-search/SKILL.md b/skills/smart-search/SKILL.md index d7715d7..8863bf4 100644 --- a/skills/smart-search/SKILL.md +++ b/skills/smart-search/SKILL.md @@ -90,11 +90,11 @@ webcmd plugin search -f json Install promising plugins sequentially, at most three plugins per user request: ```bash -webcmd plugin install +webcmd plugin install webcmd list --tag search -f json ``` -Inspect the newly visible command help. Stop once a suitable command appears. If hosted marketplace installation is unavailable, state that gap and continue with fetched sources. +Inspect the newly visible command help. Stop once a suitable command appears. If installation fails, report the error and continue with fetched sources. Do not add custom marketplaces in this workflow. In hosted mode, only verified hosted marketplace adapters are installable. diff --git a/skills/webcmd-browser/SKILL.md b/skills/webcmd-browser/SKILL.md index a6deb8f..bda8421 100644 --- a/skills/webcmd-browser/SKILL.md +++ b/skills/webcmd-browser/SKILL.md @@ -14,7 +14,7 @@ The first reader of this CLI is an agent, not a human. Use browser output as str Before starting a raw browser session, filter `webcmd list -f json` at the source using request-derived terms across `site`, `name`, `description`, and `columns`; follow `webcmd-usage` for the exact command shape. Any truncation warning means adapter discovery is incomplete: narrow the filter and inspect again. Absence from truncated output never proves that no adapter exists. -Use raw `webcmd browser` only after a complete, non-truncated registry check shows no suitable adapter. If plugin search is relevant and returns a match, offer installation; if it errors, report the error instead of opening the browser. +Use raw `webcmd browser` only after a complete, non-truncated registry check shows no suitable adapter and a plugin search for the missing site or capability returns no match. If plugin search returns a match, offer installation of the returned `installSource`; if it errors, report the error instead of opening the browser. --- diff --git a/skills/webcmd-usage/SKILL.md b/skills/webcmd-usage/SKILL.md index e2f92ff..48a45be 100644 --- a/skills/webcmd-usage/SKILL.md +++ b/skills/webcmd-usage/SKILL.md @@ -181,18 +181,18 @@ Adapters import only `@agentrhq/webcmd/registry` and `@agentrhq/webcmd/errors`. ## Plugins ```bash -webcmd plugin install github:user/repo +webcmd plugin search [query] -f json +webcmd plugin install webcmd plugin list [-f json] webcmd plugin update [name] | --all webcmd plugin uninstall webcmd plugin create -webcmd plugin search [query] -f json webcmd plugin catalog list -f json webcmd plugin catalog add webcmd plugin catalog remove ``` -Plugins are installable extensions pulled from git or local paths. Use `plugin search` for marketplace discovery and `plugin list` for already-installed plugins. Main-repo sites (official and community alike) are exposed through the root plugin catalog manifest; none of them are bundled into the npm package. +Plugins are installable extensions pulled from git or local paths. Use `plugin search` for marketplace discovery and `plugin list` for already-installed plugins. Direct `plugin install github:...` is only for a source you already know; for a missing or unknown site, search first and install the returned `installSource`. Main-repo sites (official and community alike) are exposed through the root plugin catalog manifest; none of them are bundled into the npm package. > **Note:** The repository's `plugins/` directory is not shipped in the npm package. Find the required plugin with `webcmd plugin search`, then install its `installSource` with `webcmd plugin install `. diff --git a/src/hosted/client.test.ts b/src/hosted/client.test.ts index 59e6e4c..15ff695 100644 --- a/src/hosted/client.test.ts +++ b/src/hosted/client.test.ts @@ -3,9 +3,9 @@ import { HostedClient, HostedClientError, resolveWorkspace } from './client.js'; const invalidTraceUrlCases = [ { - name: 'raw absolute Kernel URL with token', + name: 'raw absolute provider URL with token', field: 'liveViewUrl', - value: 'https://kernel.example/session/secret?token=kernel-secret-token', + value: 'https://provider.example/session/secret?token=provider-secret-token', executionId: 'exec_trace', }, { @@ -427,7 +427,7 @@ describe('HostedClient', () => { }); it.each([ - { name: 'private provider field', change: { kernelProfileId: 'private' } }, + { name: 'private provider field', change: { providerProfileId: 'private' } }, { name: 'missing updatedAt', change: { updatedAt: undefined } }, { name: 'non-nullable name shape', change: { name: 7 } }, { name: 'non-nullable workspace shape', change: { workspace: false } }, diff --git a/src/hosted/runner.test.ts b/src/hosted/runner.test.ts index 4474584..a87836f 100644 --- a/src/hosted/runner.test.ts +++ b/src/hosted/runner.test.ts @@ -1526,7 +1526,7 @@ describe('runHostedCli', () => { }); it.each(['success', 'failure'])('rejects a raw provider trace URL before $phase output or attachment', async (phase) => { - const rawUrl = 'https://kernel.example/session/private?token=kernel-secret-token'; + const rawUrl = 'https://provider.example/session/private?token=provider-secret-token'; const stdout = sink(); const stderr = sink(); const success = phase === 'success'; @@ -1561,7 +1561,7 @@ describe('runHostedCli', () => { expect(stdout.text()).toBe(''); expect(stderr.text()).toContain('HOSTED_PROTOCOL'); expect(`${stdout.text()}\n${stderr.text()}`).not.toContain(rawUrl); - expect(`${stdout.text()}\n${stderr.text()}`).not.toContain('kernel-secret-token'); + expect(`${stdout.text()}\n${stderr.text()}`).not.toContain('provider-secret-token'); }); it('accepts a manifest patch bump on the same hosted compatibility line', async () => { diff --git a/src/migrate-cli-sites.test.ts b/src/migrate-cli-sites.test.ts index 6451d18..3e0c4d8 100644 --- a/src/migrate-cli-sites.test.ts +++ b/src/migrate-cli-sites.test.ts @@ -3,7 +3,6 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; -import { _parseSource } from './plugin.js'; const script = path.resolve('scripts/migrate-cli-sites.mjs'); const roots: string[] = []; @@ -73,9 +72,8 @@ describe('migrate-cli-sites', () => { }); const readme = fs.readFileSync(path.join(plugin, 'README.md'), 'utf8'); expect(readme).toContain('| `webcmd example search` | Search examples |'); - const installSource = readme.match(/webcmd plugin install (\S+)/)?.[1]; - expect(installSource).toBe('github:agentrhq/webcmd/example'); - expect(_parseSource(installSource!)).not.toBeNull(); + expect(readme).toContain('webcmd plugin search example -f json'); + expect(readme).toContain('webcmd plugin install '); expect(fs.readFileSync(path.join(root, 'plugins', 'sibling', 'keep.txt'), 'utf8')).toBe('unchanged\n'); expect(fs.readFileSync(path.join(root, 'scripts', 'silent-column-drop-baseline.json'), 'utf8')).toContain('plugins/example/search.js'); expect(fs.readFileSync(path.join(root, 'scripts', 'typed-error-lint-baseline.json'), 'utf8')).toContain('plugins/example/search.js'); diff --git a/src/plugin-scaffold.test.ts b/src/plugin-scaffold.test.ts index 778a1d2..19a86c0 100644 --- a/src/plugin-scaffold.test.ts +++ b/src/plugin-scaffold.test.ts @@ -84,6 +84,8 @@ describe('createPluginScaffold', () => { const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8'); expect(readme).toContain(`webcmd plugin install file://${dir}`); + expect(readme).toContain('webcmd plugin search test-readme -f json'); + expect(readme).toContain('webcmd plugin install '); }); it('rejects invalid names', () => { diff --git a/src/plugin-scaffold.ts b/src/plugin-scaffold.ts index c644fa7..6a8db2f 100644 --- a/src/plugin-scaffold.ts +++ b/src/plugin-scaffold.ts @@ -141,8 +141,9 @@ ${opts.description ?? `A webcmd plugin: ${name}`} # From local development directory webcmd plugin install file://${targetDir} -# From GitHub (after publishing) -webcmd plugin install github:/webcmd-plugin-${name} +# From the catalog (after publishing) +webcmd plugin search ${name} -f json +webcmd plugin install \`\`\` ## Commands diff --git a/src/skills.test.ts b/src/skills.test.ts index 52b3a90..f6d90bc 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -131,7 +131,7 @@ describe('webcmd skills content', () => { expect(skill).toContain('handoff.viewUrl'); expect(skill).toContain('handoff.verifyCommand'); expect(skill).toContain('Webcmd browser:'); - expect(skill).not.toMatch(/\bhosted\b|\bKernel\b|\blocal mode\b|\blocally\b/i); + expect(skill).not.toMatch(/\bhosted\b|\blocal mode\b|\blocally\b/i); } for (const skill of [usage]) { expect(skill).toContain('already_logged_in');