@agentrhq/webcmd 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +217 -13
  3. package/clis/chatgpt/ask.js +1 -1
  4. package/clis/chatgpt/ask.test.js +11 -0
  5. package/clis/chatgpt/commands.test.js +102 -1
  6. package/clis/chatgpt/deep-research-result.js +45 -3
  7. package/clis/chatgpt/image.js +22 -2
  8. package/clis/chatgpt/project-file-add.js +24 -1
  9. package/clis/chatgpt/utils.js +258 -52
  10. package/clis/chatgpt/utils.test.js +259 -1
  11. package/clis/claude/ask.js +21 -1
  12. package/clis/facebook/feed.js +110 -24
  13. package/clis/facebook/feed.test.js +62 -0
  14. package/clis/facebook/search.js +185 -38
  15. package/clis/facebook/search.test.js +95 -50
  16. package/clis/instagram/explore.js +30 -12
  17. package/clis/instagram/explore.test.js +77 -0
  18. package/clis/instagram/post.js +14 -1
  19. package/clis/instagram/reel.js +13 -1
  20. package/clis/linkedin/company.js +153 -0
  21. package/clis/linkedin/company.test.js +111 -0
  22. package/clis/linkedin/connections.js +135 -0
  23. package/clis/linkedin/connections.test.js +141 -0
  24. package/clis/mercury/reimbursement-draft.js +12 -1
  25. package/clis/twitter/article-evaluate.test.js +27 -0
  26. package/clis/twitter/article.js +73 -16
  27. package/clis/twitter/article.test.js +209 -0
  28. package/clis/twitter/download.js +6 -1
  29. package/clis/twitter/post.js +14 -1
  30. package/clis/twitter/profile.js +1 -1
  31. package/clis/twitter/profile.test.js +8 -0
  32. package/clis/twitter/quote.js +11 -1
  33. package/clis/twitter/reply.js +11 -1
  34. package/dist/src/browser/base-page.d.ts +26 -0
  35. package/dist/src/browser/base-page.js +23 -8
  36. package/dist/src/browser/base-page.test.js +45 -12
  37. package/dist/src/browser/command-catalog.d.ts +6 -0
  38. package/dist/src/browser/command-catalog.js +259 -0
  39. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  40. package/dist/src/browser/command-catalog.test.js +79 -0
  41. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  42. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  43. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  44. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  45. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
  46. package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
  47. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  48. package/dist/src/browser/target-resolver.d.ts +1 -0
  49. package/dist/src/browser/target-resolver.js +99 -4
  50. package/dist/src/browser/target-resolver.test.js +141 -1
  51. package/dist/src/build-manifest.d.ts +6 -0
  52. package/dist/src/build-manifest.js +18 -1
  53. package/dist/src/build-manifest.test.js +57 -1
  54. package/dist/src/builtin-command-surface.d.ts +9 -0
  55. package/dist/src/builtin-command-surface.js +16 -0
  56. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  57. package/dist/src/check-hosted-contract.test.js +56 -0
  58. package/dist/src/cli.js +58 -106
  59. package/dist/src/cli.test.js +71 -1
  60. package/dist/src/command-presentation.d.ts +95 -0
  61. package/dist/src/command-presentation.js +486 -0
  62. package/dist/src/command-presentation.test.d.ts +1 -0
  63. package/dist/src/command-presentation.test.js +97 -0
  64. package/dist/src/command-surface.d.ts +43 -0
  65. package/dist/src/command-surface.js +205 -0
  66. package/dist/src/command-surface.test.d.ts +1 -0
  67. package/dist/src/command-surface.test.js +406 -0
  68. package/dist/src/commanderAdapter.d.ts +5 -1
  69. package/dist/src/commanderAdapter.js +12 -55
  70. package/dist/src/commanderAdapter.test.js +15 -3
  71. package/dist/src/community-plugin-sync.d.ts +11 -0
  72. package/dist/src/community-plugin-sync.js +138 -0
  73. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  74. package/dist/src/community-plugin-sync.test.js +123 -0
  75. package/dist/src/completion-fast.d.ts +4 -4
  76. package/dist/src/completion-fast.js +15 -34
  77. package/dist/src/completion-shared.d.ts +4 -0
  78. package/dist/src/completion-shared.js +38 -0
  79. package/dist/src/completion.js +3 -27
  80. package/dist/src/completion.test.js +31 -2
  81. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  82. package/dist/src/docs-sync-review-cli.test.js +322 -0
  83. package/dist/src/docs-sync-review.d.ts +117 -0
  84. package/dist/src/docs-sync-review.js +475 -0
  85. package/dist/src/docs-sync-review.test.d.ts +1 -0
  86. package/dist/src/docs-sync-review.test.js +324 -0
  87. package/dist/src/errors.d.ts +12 -3
  88. package/dist/src/errors.js +11 -7
  89. package/dist/src/errors.test.js +14 -1
  90. package/dist/src/execution.d.ts +1 -2
  91. package/dist/src/execution.js +4 -45
  92. package/dist/src/generate-release-notes-cli.test.js +7 -2
  93. package/dist/src/help.d.ts +4 -0
  94. package/dist/src/help.js +50 -255
  95. package/dist/src/help.test.js +27 -1
  96. package/dist/src/hosted/args.d.ts +2 -7
  97. package/dist/src/hosted/args.js +2 -99
  98. package/dist/src/hosted/args.test.js +15 -1
  99. package/dist/src/hosted/availability.d.ts +13 -0
  100. package/dist/src/hosted/availability.js +16 -0
  101. package/dist/src/hosted/availability.test.d.ts +1 -0
  102. package/dist/src/hosted/availability.test.js +180 -0
  103. package/dist/src/hosted/browser-args.d.ts +18 -0
  104. package/dist/src/hosted/browser-args.js +152 -0
  105. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  106. package/dist/src/hosted/browser-args.test.js +182 -0
  107. package/dist/src/hosted/client.d.ts +31 -2
  108. package/dist/src/hosted/client.js +441 -53
  109. package/dist/src/hosted/client.test.js +651 -32
  110. package/dist/src/hosted/config.d.ts +7 -2
  111. package/dist/src/hosted/config.js +26 -4
  112. package/dist/src/hosted/config.test.js +34 -1
  113. package/dist/src/hosted/contract.d.ts +94 -0
  114. package/dist/src/hosted/contract.js +208 -0
  115. package/dist/src/hosted/contract.test.d.ts +1 -0
  116. package/dist/src/hosted/contract.test.js +361 -0
  117. package/dist/src/hosted/credentials.d.ts +38 -0
  118. package/dist/src/hosted/credentials.js +248 -0
  119. package/dist/src/hosted/credentials.test.d.ts +1 -0
  120. package/dist/src/hosted/credentials.test.js +93 -0
  121. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  122. package/dist/src/hosted/file-contract.test.js +174 -0
  123. package/dist/src/hosted/files.d.ts +29 -0
  124. package/dist/src/hosted/files.js +296 -0
  125. package/dist/src/hosted/files.test.d.ts +1 -0
  126. package/dist/src/hosted/files.test.js +231 -0
  127. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  128. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  129. package/dist/src/hosted/manifest.d.ts +5 -0
  130. package/dist/src/hosted/manifest.js +21 -66
  131. package/dist/src/hosted/manifest.test.js +120 -2
  132. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  133. package/dist/src/hosted/output-parity.test.js +108 -0
  134. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  135. package/dist/src/hosted/root-command-surface.test.js +646 -0
  136. package/dist/src/hosted/runner.d.ts +7 -0
  137. package/dist/src/hosted/runner.js +554 -253
  138. package/dist/src/hosted/runner.test.js +1471 -53
  139. package/dist/src/hosted/setup.d.ts +3 -2
  140. package/dist/src/hosted/setup.js +45 -15
  141. package/dist/src/hosted/setup.test.js +133 -6
  142. package/dist/src/hosted/types.d.ts +101 -23
  143. package/dist/src/main.js +120 -108
  144. package/dist/src/manifest-types.d.ts +2 -0
  145. package/dist/src/output.d.ts +17 -2
  146. package/dist/src/output.js +88 -81
  147. package/dist/src/output.test.js +141 -51
  148. package/dist/src/pipeline/executor.test.js +1 -0
  149. package/dist/src/pipeline/steps/download.test.js +1 -0
  150. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  151. package/dist/src/plugin-create-cli.test.js +37 -0
  152. package/dist/src/plugin-manifest.d.ts +12 -0
  153. package/dist/src/plugin-manifest.js +16 -0
  154. package/dist/src/plugin-manifest.test.js +19 -1
  155. package/dist/src/plugin-scaffold.d.ts +4 -1
  156. package/dist/src/plugin-scaffold.js +4 -1
  157. package/dist/src/plugin-scaffold.test.js +23 -8
  158. package/dist/src/plugin.js +4 -1
  159. package/dist/src/plugin.test.js +13 -0
  160. package/dist/src/registry.d.ts +2 -0
  161. package/dist/src/release-notes.js +12 -4
  162. package/dist/src/release-notes.test.js +27 -15
  163. package/dist/src/root-command-surface.d.ts +31 -0
  164. package/dist/src/root-command-surface.js +106 -0
  165. package/dist/src/serialization.d.ts +1 -16
  166. package/dist/src/serialization.js +5 -55
  167. package/dist/src/stream-write.d.ts +12 -0
  168. package/dist/src/stream-write.js +91 -0
  169. package/dist/src/stream-write.test.d.ts +1 -0
  170. package/dist/src/stream-write.test.js +186 -0
  171. package/dist/src/types.d.ts +5 -0
  172. package/dist/src/utils.d.ts +1 -1
  173. package/dist/src/utils.js +2 -8
  174. package/dist/src/utils.test.js +50 -0
  175. package/hosted-contract.json +36748 -0
  176. package/package.json +6 -1
  177. package/scripts/check-hosted-contract.mjs +108 -0
  178. package/scripts/docs-sync-review.ts +332 -0
  179. package/scripts/sync-community-plugins.ts +14 -0
  180. package/skills/webcmd-autofix/SKILL.md +26 -37
  181. package/skills/webcmd-usage/SKILL.md +7 -0
@@ -10,50 +10,20 @@
10
10
  * All execution logic lives in execution.ts.
11
11
  */
12
12
  import { log } from './logger.js';
13
- import yaml from 'js-yaml';
14
13
  import { fullName, getRegistry } from './registry.js';
15
- import { render as renderOutput } from './output.js';
14
+ import { formatErrorEnvelope, render as renderOutput } from './output.js';
16
15
  import { executeCommand, prepareCommandArgs } from './execution.js';
16
+ import { configureCommandSurface, parseOutputFormat } from './command-surface.js';
17
17
  import { commandHelpData, formatCommandHelpText, formatCommandListTerm, formatSiteCommandDescription, formatSiteHelpText, getRequestedHelpFormat, renderStructuredHelp, siteHelpData, } from './help.js';
18
18
  import { CliError, EXIT_CODES, toEnvelope, } from './errors.js';
19
- /**
20
- * Register a single CliCommand as a Commander subcommand.
21
- */
22
- export function registerCommandToProgram(siteCmd, cmd) {
19
+ export function registerCommandToProgram(siteCmd, cmd, runtime = {}) {
23
20
  if (siteCmd.commands.some((c) => c.name() === cmd.name))
24
21
  return;
25
22
  const subCmd = siteCmd.command(cmd.name).description(formatSiteCommandDescription(cmd));
26
23
  if (cmd.aliases?.length)
27
24
  subCmd.aliases(cmd.aliases);
28
- // Register positional args first, then named options
29
- const positionalArgs = [];
30
- for (const arg of cmd.args) {
31
- if (arg.positional) {
32
- const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
33
- subCmd.argument(bracket, arg.help ?? '');
34
- positionalArgs.push(arg);
35
- }
36
- else {
37
- const expectsValue = arg.required || arg.valueRequired;
38
- const flag = expectsValue ? `--${arg.name} <value>` : `--${arg.name} [value]`;
39
- if (arg.required)
40
- subCmd.requiredOption(flag, arg.help ?? '');
41
- else if (arg.default != null)
42
- subCmd.option(flag, arg.help ?? '', String(arg.default));
43
- else
44
- subCmd.option(flag, arg.help ?? '');
45
- }
46
- }
47
- subCmd
48
- .option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
49
- .option('--trace <mode>', 'Trace capture: off, on, retain-on-failure', 'off')
50
- .option('-v, --verbose', 'Debug output', false);
51
- if (cmd.browser) {
52
- subCmd
53
- .option('--window <mode>', 'Browser window mode: foreground or background')
54
- .option('--site-session <mode>', 'Adapter site session lifecycle: ephemeral or persistent')
55
- .option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
56
- }
25
+ const positionalArgs = cmd.args.filter((arg) => arg.positional);
26
+ configureCommandSurface(subCmd, cmd);
57
27
  const originalHelpInformation = subCmd.helpInformation.bind(subCmd);
58
28
  subCmd.helpInformation = ((contextOptions) => {
59
29
  const format = getRequestedHelpFormat();
@@ -67,7 +37,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
67
37
  subCmd.action(async (...actionArgs) => {
68
38
  const actionOpts = actionArgs[positionalArgs.length] ?? {};
69
39
  const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts : {};
70
- const startTime = Date.now();
40
+ const now = runtime.now ?? Date.now;
41
+ const startTime = now();
71
42
  // ── Execute + render ────────────────────────────────────────────────
72
43
  try {
73
44
  // ── Collect kwargs ────────────────────────────────────────────────
@@ -99,7 +70,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
99
70
  }
100
71
  const kwargs = prepareCommandArgs(cmd, rawKwargs);
101
72
  const verbose = optionsRecord.verbose === true;
102
- let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
73
+ let format = parseOutputFormat(optionsRecord.format ?? 'table');
103
74
  const formatExplicit = subCmd.getOptionValueSource('format') === 'cli';
104
75
  if (verbose)
105
76
  process.env.WEBCMD_VERBOSE = '1';
@@ -122,14 +93,15 @@ export function registerCommandToProgram(siteCmd, cmd) {
122
93
  if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
123
94
  log.warn('Command returned an empty result.');
124
95
  }
125
- renderOutput(result, {
96
+ await renderOutput(result, {
126
97
  fmt: format,
127
98
  fmtExplicit: formatExplicit,
128
99
  columns: resolved.columns,
129
100
  title: `${resolved.site}/${resolved.name}`,
130
- elapsed: (Date.now() - startTime) / 1000,
101
+ elapsed: (now() - startTime) / 1000,
131
102
  source: fullName(resolved),
132
103
  footerExtra: resolved.footerExtra?.(kwargs),
104
+ ...(runtime.stdout ? { stdout: runtime.stdout } : {}),
133
105
  });
134
106
  }
135
107
  catch (err) {
@@ -145,28 +117,13 @@ function resolveExitCode(err) {
145
117
  return EXIT_CODES.GENERIC_ERROR;
146
118
  }
147
119
  // ── Error rendering ─────────────────────────────────────────────────────────
148
- /** Emit AutoFix hint for repairable adapter errors (skipped if trace already exported). */
149
- function emitAutoFixHint(envelope, cmdName, traceMode) {
150
- if (traceMode === 'on' || traceMode === 'retain-on-failure')
151
- return envelope;
152
- const runnable = cmdName.replace('/', ' ');
153
- return envelope
154
- + `# AutoFix: re-run with --trace=retain-on-failure for trace artifact\n`
155
- + `# webcmd ${runnable} --trace retain-on-failure\n`;
156
- }
157
120
  function renderError(err, cmdName, verbose, traceMode) {
158
121
  const envelope = toEnvelope(err);
159
122
  // In verbose mode, include stack trace for debugging
160
123
  if (verbose && err instanceof Error && err.stack) {
161
124
  envelope.error.stack = err.stack;
162
125
  }
163
- let output = yaml.dump(envelope, { sortKeys: false, lineWidth: 120, noRefs: true });
164
- // Append AutoFix hint for repairable errors
165
- const code = envelope.error.code;
166
- if (code === 'SELECTOR' || code === 'EMPTY_RESULT' || code === 'ADAPTER_LOAD' || code === 'UNKNOWN') {
167
- output = emitAutoFixHint(output, cmdName, traceMode);
168
- }
169
- process.stderr.write(output);
126
+ process.stderr.write(formatErrorEnvelope(envelope, { cmdName, traceMode }));
170
127
  }
171
128
  /**
172
129
  * Register all commands from the registry onto a Commander program.
@@ -12,9 +12,13 @@ vi.mock('./execution.js', async () => {
12
12
  executeCommand: mockExecuteCommand,
13
13
  };
14
14
  });
15
- vi.mock('./output.js', () => ({
16
- render: mockRenderOutput,
17
- }));
15
+ vi.mock('./output.js', async () => {
16
+ const actual = await vi.importActual('./output.js');
17
+ return {
18
+ ...actual,
19
+ render: mockRenderOutput,
20
+ };
21
+ });
18
22
  import { registerCommandToProgram } from './commanderAdapter.js';
19
23
  describe('commanderAdapter arg passing', () => {
20
24
  const cmd = {
@@ -248,6 +252,14 @@ describe('commanderAdapter default formats', () => {
248
252
  await program.parseAsync(['node', 'webcmd', 'gemini', 'ask', '--format', 'json']);
249
253
  expect(mockRenderOutput).toHaveBeenCalledWith([{ response: 'hello' }], expect.objectContaining({ fmt: 'json' }));
250
254
  });
255
+ it('preserves the legacy fallback for an unknown explicit format', async () => {
256
+ const program = new Command();
257
+ const siteCmd = program.command('gemini');
258
+ registerCommandToProgram(siteCmd, cmd);
259
+ await program.parseAsync(['node', 'webcmd', 'gemini', 'ask', '--format', 'xml']);
260
+ expect(mockExecuteCommand).toHaveBeenCalled();
261
+ expect(mockRenderOutput).toHaveBeenCalledWith([{ response: 'hello' }], expect.objectContaining({ fmt: 'xml', fmtExplicit: true }));
262
+ });
251
263
  });
252
264
  describe('commanderAdapter error envelope output', () => {
253
265
  const cmd = {
@@ -0,0 +1,11 @@
1
+ import { type PluginManifest } from './plugin-manifest.js';
2
+ export declare const COMMUNITY_PLUGINS_START = "<!-- webcmd-community-plugins:start -->";
3
+ export declare const COMMUNITY_PLUGINS_END = "<!-- webcmd-community-plugins:end -->";
4
+ export interface CommunityPluginOutputs {
5
+ rootManifest: PluginManifest;
6
+ readme: string;
7
+ }
8
+ /** Build generated catalog outputs without writing them. */
9
+ export declare function buildCommunityPluginOutputs(repoRoot: string): CommunityPluginOutputs;
10
+ /** Build and write the root manifest and README catalog. */
11
+ export declare function writeCommunityPluginOutputs(repoRoot: string): CommunityPluginOutputs;
@@ -0,0 +1,138 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { validatePluginAuthor, } from './plugin-manifest.js';
4
+ export const COMMUNITY_PLUGINS_START = '<!-- webcmd-community-plugins:start -->';
5
+ export const COMMUNITY_PLUGINS_END = '<!-- webcmd-community-plugins:end -->';
6
+ /** Build generated catalog outputs without writing them. */
7
+ export function buildCommunityPluginOutputs(repoRoot) {
8
+ const rootManifestPath = path.join(repoRoot, 'webcmd-plugin.json');
9
+ const rootManifest = readObject(rootManifestPath, 'webcmd-plugin.json');
10
+ const plugins = readRepositoryPlugins(repoRoot);
11
+ const readmePath = path.join(repoRoot, 'README.md');
12
+ const readme = fs.readFileSync(readmePath, 'utf-8');
13
+ const marketplaces = readExternalMarketplaces(repoRoot);
14
+ return {
15
+ rootManifest: { ...rootManifest, plugins },
16
+ readme: replaceGeneratedSection(readme, renderCommunityPlugins(plugins, marketplaces)),
17
+ };
18
+ }
19
+ /** Build and write the root manifest and README catalog. */
20
+ export function writeCommunityPluginOutputs(repoRoot) {
21
+ const generated = buildCommunityPluginOutputs(repoRoot);
22
+ fs.writeFileSync(path.join(repoRoot, 'webcmd-plugin.json'), `${JSON.stringify(generated.rootManifest, null, 2)}\n`);
23
+ fs.writeFileSync(path.join(repoRoot, 'README.md'), generated.readme);
24
+ return generated;
25
+ }
26
+ function readRepositoryPlugins(repoRoot) {
27
+ const pluginsDir = path.join(repoRoot, 'plugins');
28
+ const directoryNames = fs.readdirSync(pluginsDir, { withFileTypes: true })
29
+ .filter((entry) => entry.isDirectory())
30
+ .map((entry) => entry.name)
31
+ .sort((a, b) => a.localeCompare(b));
32
+ const plugins = {};
33
+ for (const directoryName of directoryNames) {
34
+ const relativeManifestPath = `plugins/${directoryName}/webcmd-plugin.json`;
35
+ const manifestPath = path.join(repoRoot, relativeManifestPath);
36
+ const manifest = readObject(manifestPath, relativeManifestPath);
37
+ const name = requiredString(manifest, 'name', relativeManifestPath);
38
+ if (name !== directoryName) {
39
+ throw new Error(`${relativeManifestPath}: name must match directory "${directoryName}"`);
40
+ }
41
+ let author;
42
+ try {
43
+ author = validatePluginAuthor(manifest.author);
44
+ }
45
+ catch (err) {
46
+ throw new Error(`${relativeManifestPath}: ${errorMessage(err)}`);
47
+ }
48
+ plugins[name] = {
49
+ path: `plugins/${directoryName}`,
50
+ version: requiredString(manifest, 'version', relativeManifestPath),
51
+ description: requiredString(manifest, 'description', relativeManifestPath),
52
+ webcmd: requiredString(manifest, 'webcmd', relativeManifestPath),
53
+ author,
54
+ };
55
+ }
56
+ return plugins;
57
+ }
58
+ function readExternalMarketplaces(repoRoot) {
59
+ const catalog = readObject(path.join(repoRoot, 'plugin-catalog.json'), 'plugin-catalog.json');
60
+ if (!Array.isArray(catalog.sources)) {
61
+ throw new Error('plugin-catalog.json: sources must be an array');
62
+ }
63
+ return catalog.sources.flatMap((source, index) => {
64
+ if (!isObject(source) || typeof source.source !== 'string') {
65
+ throw new Error(`plugin-catalog.json: sources[${index}].source must be a string`);
66
+ }
67
+ const match = /^github:([\w.-]+)\/([\w.-]+)$/.exec(source.source);
68
+ if (!match)
69
+ return [];
70
+ const repository = `${match[1]}/${match[2]}`;
71
+ return repository === 'agentrhq/webcmd' ? [] : [repository];
72
+ }).sort((a, b) => a.localeCompare(b));
73
+ }
74
+ function renderCommunityPlugins(plugins, marketplaces) {
75
+ const lines = [
76
+ COMMUNITY_PLUGINS_START,
77
+ '### Community plugins',
78
+ '',
79
+ '| Plugin | Description | Author |',
80
+ '| --- | --- | --- |',
81
+ ];
82
+ for (const [name, plugin] of Object.entries(plugins)) {
83
+ const author = plugin.author;
84
+ lines.push(`| [\`${markdownCell(name)}\`](./plugins/${name}/) | ${markdownCell(plugin.description)} | [${markdownCell(author.name)}](https://github.com/${author.handle}) |`);
85
+ }
86
+ if (marketplaces.length > 0) {
87
+ lines.push('', '### Plugin marketplaces', '', '| Marketplace |', '| --- |');
88
+ for (const marketplace of marketplaces) {
89
+ lines.push(`| [${markdownCell(marketplace)}](https://github.com/${marketplace}) |`);
90
+ }
91
+ }
92
+ lines.push(COMMUNITY_PLUGINS_END);
93
+ return lines.join('\n');
94
+ }
95
+ function replaceGeneratedSection(readme, section) {
96
+ const start = readme.indexOf(COMMUNITY_PLUGINS_START);
97
+ const end = readme.indexOf(COMMUNITY_PLUGINS_END);
98
+ if (start < 0 || end < start) {
99
+ throw new Error(`README.md: expected ${COMMUNITY_PLUGINS_START} and ${COMMUNITY_PLUGINS_END}`);
100
+ }
101
+ if (readme.indexOf(COMMUNITY_PLUGINS_START, start + COMMUNITY_PLUGINS_START.length) >= 0
102
+ || readme.indexOf(COMMUNITY_PLUGINS_END, end + COMMUNITY_PLUGINS_END.length) >= 0) {
103
+ throw new Error('README.md: community plugin markers must appear exactly once');
104
+ }
105
+ return `${readme.slice(0, start)}${section}${readme.slice(end + COMMUNITY_PLUGINS_END.length)}`;
106
+ }
107
+ function readObject(filePath, label) {
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
111
+ }
112
+ catch (err) {
113
+ throw new Error(`${label}: malformed JSON (${errorMessage(err)})`);
114
+ }
115
+ if (!isObject(parsed))
116
+ throw new Error(`${label}: expected an object`);
117
+ return parsed;
118
+ }
119
+ function requiredString(value, key, label) {
120
+ const result = typeof value[key] === 'string' ? value[key].trim() : '';
121
+ if (!result)
122
+ throw new Error(`${label}: ${key} must be a non-empty string`);
123
+ return result;
124
+ }
125
+ function markdownCell(value) {
126
+ return value
127
+ .replace(/\\/g, '\\\\')
128
+ .replace(/\|/g, '\\|')
129
+ .replace(/\[/g, '\\[')
130
+ .replace(/\]/g, '\\]')
131
+ .replace(/\r?\n/g, ' ');
132
+ }
133
+ function isObject(value) {
134
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
135
+ }
136
+ function errorMessage(err) {
137
+ return err instanceof Error ? err.message : String(err);
138
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { buildCommunityPluginOutputs, writeCommunityPluginOutputs } from './community-plugin-sync.js';
6
+ const START_MARKER = '<!-- webcmd-community-plugins:start -->';
7
+ const END_MARKER = '<!-- webcmd-community-plugins:end -->';
8
+ describe('community plugin sync', () => {
9
+ let repoRoot;
10
+ beforeEach(() => {
11
+ repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-plugin-generator-'));
12
+ writeJson('webcmd-plugin.json', {
13
+ name: 'webcmd',
14
+ version: '0.2.0',
15
+ description: 'Webcmd plugin collection',
16
+ webcmd: '>=0.2.0',
17
+ plugins: { stale: { path: 'plugins/stale' } },
18
+ });
19
+ writeJson('plugin-catalog.json', {
20
+ version: 1,
21
+ sources: [
22
+ {
23
+ id: 'agentrhq/webcmd',
24
+ source: 'github:agentrhq/webcmd',
25
+ manifestUrl: 'https://raw.githubusercontent.com/agentrhq/webcmd/main/webcmd-plugin.json',
26
+ },
27
+ {
28
+ id: 'other/plugins',
29
+ source: 'github:other/plugins',
30
+ manifestUrl: 'https://raw.githubusercontent.com/other/plugins/main/webcmd-plugin.json',
31
+ },
32
+ ],
33
+ });
34
+ fs.writeFileSync(path.join(repoRoot, 'README.md'), [
35
+ '# Webcmd',
36
+ '',
37
+ '## Plugins',
38
+ '',
39
+ START_MARKER,
40
+ 'old generated content',
41
+ END_MARKER,
42
+ '',
43
+ '## Writing adapters',
44
+ '',
45
+ ].join('\n'));
46
+ writePlugin('zeta', {
47
+ name: 'zeta',
48
+ version: '0.1.0',
49
+ description: 'Last plugin',
50
+ webcmd: '>=0.2.0',
51
+ author: { name: 'Zed', handle: 'zed-user' },
52
+ });
53
+ writePlugin('alpha', {
54
+ name: 'alpha',
55
+ version: '1.0.0',
56
+ description: 'Forecasts | alerts',
57
+ webcmd: '>=0.3.0',
58
+ author: { name: 'Alice', handle: 'alice' },
59
+ });
60
+ });
61
+ afterEach(() => {
62
+ fs.rmSync(repoRoot, { recursive: true, force: true });
63
+ });
64
+ it('builds a sorted root manifest and three-column README catalog', () => {
65
+ const result = buildCommunityPluginOutputs(repoRoot);
66
+ expect(Object.keys(result.rootManifest.plugins ?? {})).toEqual(['alpha', 'zeta']);
67
+ expect(result.rootManifest).toMatchObject({
68
+ name: 'webcmd',
69
+ version: '0.2.0',
70
+ plugins: {
71
+ alpha: {
72
+ path: 'plugins/alpha',
73
+ version: '1.0.0',
74
+ description: 'Forecasts | alerts',
75
+ webcmd: '>=0.3.0',
76
+ author: { name: 'Alice', handle: 'alice' },
77
+ },
78
+ },
79
+ });
80
+ expect(result.readme).toContain('| Plugin | Description | Author |');
81
+ expect(result.readme).toContain('| [`alpha`](./plugins/alpha/) | Forecasts \\| alerts | [Alice](https://github.com/alice) |');
82
+ expect(result.readme.indexOf('`alpha`')).toBeLessThan(result.readme.indexOf('`zeta`'));
83
+ expect(result.readme).toContain('### Plugin marketplaces');
84
+ expect(result.readme).toContain('[other/plugins](https://github.com/other/plugins)');
85
+ expect(result.readme).not.toContain('[agentrhq/webcmd](https://github.com/agentrhq/webcmd)');
86
+ });
87
+ it('writes the generated files', () => {
88
+ const result = writeCommunityPluginOutputs(repoRoot);
89
+ expect(JSON.parse(fs.readFileSync(path.join(repoRoot, 'webcmd-plugin.json'), 'utf-8'))).toEqual(result.rootManifest);
90
+ expect(fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf-8')).toBe(result.readme);
91
+ });
92
+ it('fails with the plugin path when author metadata is missing', () => {
93
+ writePlugin('alpha', {
94
+ name: 'alpha',
95
+ version: '1.0.0',
96
+ description: 'Forecasts',
97
+ webcmd: '>=0.3.0',
98
+ });
99
+ expect(() => buildCommunityPluginOutputs(repoRoot)).toThrow('plugins/alpha/webcmd-plugin.json: Invalid author');
100
+ });
101
+ it('fails with the plugin path for malformed JSON', () => {
102
+ fs.writeFileSync(path.join(repoRoot, 'plugins/alpha/webcmd-plugin.json'), '{broken');
103
+ expect(() => buildCommunityPluginOutputs(repoRoot)).toThrow('plugins/alpha/webcmd-plugin.json: malformed JSON');
104
+ });
105
+ it('rejects directory and manifest name mismatches', () => {
106
+ writePlugin('alpha', {
107
+ name: 'different',
108
+ version: '1.0.0',
109
+ description: 'Forecasts',
110
+ webcmd: '>=0.3.0',
111
+ author: { name: 'Alice', handle: 'alice' },
112
+ });
113
+ expect(() => buildCommunityPluginOutputs(repoRoot)).toThrow('plugins/alpha/webcmd-plugin.json: name must match directory "alpha"');
114
+ });
115
+ function writeJson(relativePath, value) {
116
+ const filePath = path.join(repoRoot, relativePath);
117
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
118
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
119
+ }
120
+ function writePlugin(name, manifest) {
121
+ writeJson(`plugins/${name}/webcmd-plugin.json`, manifest);
122
+ }
123
+ });
@@ -16,7 +16,7 @@ export declare function hasAllManifests(manifestPaths: string[]): boolean;
16
16
  * bypassing full CLI discovery and adapter loading.
17
17
  */
18
18
  export declare function getCompletionsFromManifest(words: string[], cursor: number, manifestPaths: string[]): string[];
19
- /**
20
- * Print completion script for the given shell. Returns true if handled, false if unknown shell.
21
- */
22
- export declare function printCompletionScriptFast(shell: string): boolean;
19
+ /** Return a generated shell script without taking ownership of stdout. */
20
+ export declare function getCompletionScriptFast(shell: string): string | undefined;
21
+ /** Return a supported shell script or the canonical local completion error. */
22
+ export declare function requireCompletionScriptFast(shell: string): string;
@@ -5,6 +5,8 @@
5
5
  * It only reads pre-compiled cli-manifest.json files synchronously.
6
6
  */
7
7
  import * as fs from 'node:fs';
8
+ import { getCommandCompletionCandidates } from './command-presentation.js';
9
+ import { CliError } from './errors.js';
8
10
  import { BUILTIN_COMMANDS, bashCompletionScript, zshCompletionScript, fishCompletionScript, } from './completion-shared.js';
9
11
  /**
10
12
  * Returns true only if ALL manifest files exist and are readable.
@@ -29,32 +31,9 @@ export function hasAllManifests(manifestPaths) {
29
31
  */
30
32
  export function getCompletionsFromManifest(words, cursor, manifestPaths) {
31
33
  const entries = loadManifestEntries(manifestPaths);
32
- if (entries === null) {
34
+ if (entries === null)
33
35
  return [];
34
- }
35
- if (cursor <= 1) {
36
- const sites = new Set();
37
- for (const entry of entries) {
38
- sites.add(entry.site);
39
- }
40
- return [...BUILTIN_COMMANDS, ...sites].sort();
41
- }
42
- const site = words[0];
43
- if (BUILTIN_COMMANDS.includes(site)) {
44
- return [];
45
- }
46
- if (cursor === 2) {
47
- const subcommands = [];
48
- for (const entry of entries) {
49
- if (entry.site === site) {
50
- subcommands.push(entry.name);
51
- if (entry.aliases?.length)
52
- subcommands.push(...entry.aliases);
53
- }
54
- }
55
- return [...new Set(subcommands)].sort();
56
- }
57
- return [];
36
+ return getCommandCompletionCandidates(entries, words, cursor, BUILTIN_COMMANDS);
58
37
  }
59
38
  // ── Shell script generators (re-exported from shared, no registry dependency) ───────
60
39
  const SHELL_SCRIPTS = {
@@ -62,15 +41,17 @@ const SHELL_SCRIPTS = {
62
41
  zsh: zshCompletionScript,
63
42
  fish: fishCompletionScript,
64
43
  };
65
- /**
66
- * Print completion script for the given shell. Returns true if handled, false if unknown shell.
67
- */
68
- export function printCompletionScriptFast(shell) {
69
- const gen = SHELL_SCRIPTS[shell];
70
- if (!gen)
71
- return false;
72
- process.stdout.write(gen());
73
- return true;
44
+ /** Return a generated shell script without taking ownership of stdout. */
45
+ export function getCompletionScriptFast(shell) {
46
+ return SHELL_SCRIPTS[shell]?.();
47
+ }
48
+ /** Return a supported shell script or the canonical local completion error. */
49
+ export function requireCompletionScriptFast(shell) {
50
+ const script = getCompletionScriptFast(shell);
51
+ if (script === undefined) {
52
+ throw new CliError('UNSUPPORTED_SHELL', `Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
53
+ }
54
+ return script;
74
55
  }
75
56
  function loadManifestEntries(manifestPaths) {
76
57
  const entries = [];
@@ -4,10 +4,14 @@
4
4
  * This module MUST remain lightweight (no registry, no discovery imports).
5
5
  * Both completion.ts (full path) and completion-fast.ts (manifest path) import from here.
6
6
  */
7
+ import type { RootHelpPresentation } from './command-presentation.js';
7
8
  /**
8
9
  * Built-in (non-dynamic) top-level commands.
9
10
  */
10
11
  export declare const BUILTIN_COMMANDS: string[];
12
+ export declare const LOCAL_ONLY_COMMAND_HELP = "Run `webcmd setup` and choose local mode to use local-only commands.";
13
+ export declare const HOSTED_ROOT_HELP: RootHelpPresentation;
14
+ export declare const HOSTED_BUILTIN_COMMANDS: string[];
11
15
  export declare function bashCompletionScript(): string;
12
16
  export declare function zshCompletionScript(): string;
13
17
  export declare function fishCompletionScript(): string;
@@ -20,6 +20,44 @@ export const BUILTIN_COMMANDS = [
20
20
  'external',
21
21
  'completion',
22
22
  ];
23
+ export const LOCAL_ONLY_COMMAND_HELP = 'Run `webcmd setup` and choose local mode to use local-only commands.';
24
+ export const HOSTED_ROOT_HELP = {
25
+ description: 'Make any website your CLI. Zero setup. AI-powered.',
26
+ usage: [
27
+ `${CLI_COMMAND} <site> <command> [args] [options]`,
28
+ `${CLI_COMMAND} browser <session> <command> [args] [options]`,
29
+ `${CLI_COMMAND} list [options]`,
30
+ `${CLI_COMMAND} setup`,
31
+ ],
32
+ options: [
33
+ { flags: '--profile <name>', description: 'Browser profile/context alias for browser runtime commands' },
34
+ { flags: '-V, --version', description: 'Output the version number' },
35
+ { flags: '-h, --help', description: 'Display help for command' },
36
+ ],
37
+ commands: [
38
+ { name: 'browser', description: 'Browser control through a hosted browser session' },
39
+ { name: 'completion <shell>', description: 'Output a shell completion script' },
40
+ { name: 'list', description: 'List all available hosted CLI commands' },
41
+ { name: 'setup', description: 'Configure local or hosted mode' },
42
+ ],
43
+ localOnlyCommands: [
44
+ { name: 'adapter', description: 'Manage adapters installed on this computer' },
45
+ { name: 'antigravity', description: 'Run the local Antigravity proxy' },
46
+ { name: 'auth', description: 'Inspect credentials in the local browser runtime' },
47
+ { name: 'convention-audit', description: 'Audit adapter source files on this computer' },
48
+ { name: 'daemon', description: 'Manage the local Webcmd daemon' },
49
+ { name: 'doctor', description: 'Diagnose local browser bridge connectivity' },
50
+ { name: 'external', description: 'Manage local CLI passthrough commands' },
51
+ { name: 'plugin', description: 'Manage plugins installed on this computer' },
52
+ { name: 'profile', description: 'Manage profiles in the local browser runtime' },
53
+ { name: 'skills', description: 'Manage bundled skills on this computer' },
54
+ { name: 'validate', description: 'Validate local CLI definitions' },
55
+ { name: 'verify', description: 'Validate and smoke-test local adapters' },
56
+ ],
57
+ localOnlyExplanation: LOCAL_ONLY_COMMAND_HELP,
58
+ };
59
+ export const HOSTED_BUILTIN_COMMANDS = HOSTED_ROOT_HELP.commands
60
+ .map((command) => command.name.split(/\s/, 1)[0]);
23
61
  // ── Shell script generators ────────────────────────────────────────────────
24
62
  export function bashCompletionScript() {
25
63
  return `# Bash completion for ${CLI_COMMAND}
@@ -6,6 +6,7 @@
6
6
  * - Dynamic completion logic that returns candidates for the current cursor position
7
7
  */
8
8
  import { getRegistry } from './registry.js';
9
+ import { getCommandCompletionCandidates, toPresentableCommand } from './command-presentation.js';
9
10
  import { CliError } from './errors.js';
10
11
  import { BUILTIN_COMMANDS, bashCompletionScript, zshCompletionScript, fishCompletionScript, } from './completion-shared.js';
11
12
  // Re-export shell scripts so existing callers (cli.ts) don't break
@@ -19,33 +20,8 @@ export { bashCompletionScript, zshCompletionScript, fishCompletionScript };
19
20
  * @param cursor - 1-based position of the word being completed (1 = first arg)
20
21
  */
21
22
  export function getCompletions(words, cursor) {
22
- // cursor === 1 → completing the first argument (site name or built-in command)
23
- if (cursor <= 1) {
24
- const sites = new Set();
25
- for (const [, cmd] of getRegistry()) {
26
- sites.add(cmd.site);
27
- }
28
- return [...BUILTIN_COMMANDS, ...sites].sort();
29
- }
30
- const site = words[0];
31
- // If the first word is a built-in command, no further completion
32
- if (BUILTIN_COMMANDS.includes(site)) {
33
- return [];
34
- }
35
- // cursor === 2 → completing the sub-command name under a site
36
- if (cursor === 2) {
37
- const subcommands = [];
38
- for (const [, cmd] of getRegistry()) {
39
- if (cmd.site === site) {
40
- subcommands.push(cmd.name);
41
- if (cmd.aliases?.length)
42
- subcommands.push(...cmd.aliases);
43
- }
44
- }
45
- return [...new Set(subcommands)].sort();
46
- }
47
- // cursor >= 3 → no further completion
48
- return [];
23
+ const commands = [...new Set(getRegistry().values())].map(toPresentableCommand);
24
+ return getCommandCompletionCandidates(commands, words, cursor, BUILTIN_COMMANDS);
49
25
  }
50
26
  // ── Shell script generators ────────────────────────────────────────────────
51
27
  /**