@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
@@ -1,103 +1,320 @@
1
- import { writeFileSync } from 'node:fs';
2
- import yaml from 'js-yaml';
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { Command, CommanderError } from 'commander';
5
+ import { configureCompletionCommandSurface, configureListCommandSurface } from '../builtin-command-surface.js';
3
6
  import { BrowserSessionArgvError, rewriteBrowserArgv } from '../cli-argv-preprocess.js';
4
- import { ConfigError, EXIT_CODES, toEnvelope } from '../errors.js';
5
- import { render as renderOutput } from '../output.js';
6
- import { HostedClient } from './client.js';
7
+ import { CommanderStructuralError, MissingRequiredPositionalError } from '../command-surface.js';
8
+ import { formatRootHelp, getCommandCompletionCandidates } from '../command-presentation.js';
9
+ import { HOSTED_BUILTIN_COMMANDS, HOSTED_ROOT_HELP, LOCAL_ONLY_COMMAND_HELP, } from '../completion-shared.js';
10
+ import { CliError, ConfigError, EXIT_CODES, toEnvelope } from '../errors.js';
11
+ import { getRequestedHelpFormat, renderStructuredHelp } from '../help.js';
12
+ import { findPackageRoot } from '../package-paths.js';
13
+ import { formatErrorEnvelope, render as renderOutput } from '../output.js';
14
+ import { StreamWriteError, writeToStream } from '../stream-write.js';
15
+ import { PKG_VERSION } from '../version.js';
16
+ import { getCompletionScriptFast } from '../completion-fast.js';
17
+ import { browserCommandCatalog } from '../browser/command-catalog.js';
18
+ import { HostedClient, HostedClientError } from './client.js';
7
19
  import { parseHostedInvocation } from './args.js';
8
- import { findHostedCommand, hostedListRows, isLocalOnlyHostedCommand, renderHostedCommandHelp, renderHostedSiteHelp, siteNames, commandNamesForSite, } from './manifest.js';
20
+ import { HostedBrowserHelp, parseHostedBrowserStructure } from './browser-args.js';
21
+ import { materializeHostedOutputs, prepareHostedFiles, rewriteHostedOutputResultPaths } from './files.js';
22
+ import { findHostedCommand, hostedCommandHelpData, hostedCommands, hostedListPresentation, hostedSiteHelpData, isLocalOnlyHostedCommand, renderHostedCommandHelp, renderHostedSiteHelp, } from './manifest.js';
9
23
  import { isHostedConfig, loadWebcmdConfig } from './config.js';
24
+ import { resolveHostedApiKey } from './credentials.js';
25
+ import { parseHostedRootCommandSurface } from '../root-command-surface.js';
26
+ class CommanderCompatibleError extends Error {
27
+ output;
28
+ exitCode;
29
+ stdoutOutput;
30
+ constructor(output, exitCode, stdoutOutput) {
31
+ super(output.trimEnd());
32
+ this.output = output;
33
+ this.exitCode = exitCode;
34
+ this.stdoutOutput = stdoutOutput;
35
+ }
36
+ }
37
+ const hostedBrowserCommandsByPath = new Map(browserCommandCatalog.map(command => [command.command, command]));
10
38
  export async function runHostedCli(argv, opts = {}) {
11
- const config = opts.config ?? loadWebcmdConfig();
39
+ const config = opts.config ?? loadWebcmdConfig({ env: opts.env, homeDir: opts.homeDir });
12
40
  if (!isHostedConfig(config))
13
41
  return { handled: false, exitCode: EXIT_CODES.SUCCESS };
14
42
  const stdout = opts.stdout ?? process.stdout;
15
43
  const stderr = opts.stderr ?? process.stderr;
16
- const client = new HostedClient({
17
- apiBaseUrl: config.hosted.apiBaseUrl,
18
- apiKey: config.hosted.apiKey,
19
- fetchImpl: opts.fetchImpl,
20
- });
21
44
  try {
22
- await dispatchHosted(argv, client, stdout);
45
+ const credential = await resolveHostedApiKey(config, {
46
+ credentialStore: opts.credentialStore,
47
+ env: opts.env,
48
+ homeDir: opts.homeDir,
49
+ platform: opts.platform,
50
+ randomUUID: opts.randomUUID,
51
+ migrate: opts.config === undefined,
52
+ });
53
+ const client = new HostedClient({
54
+ apiBaseUrl: config.hosted.apiBaseUrl,
55
+ apiKey: credential.apiKey,
56
+ fetchImpl: opts.fetchImpl,
57
+ });
58
+ await dispatchHosted(argv, client, stdout, stderr, opts.now ?? Date.now);
23
59
  return { handled: true, exitCode: EXIT_CODES.SUCCESS };
24
60
  }
25
61
  catch (err) {
26
- stderr.write(yaml.dump(toEnvelope(err), { sortKeys: false, lineWidth: 120, noRefs: true }));
62
+ if (err instanceof StreamWriteError)
63
+ throw err;
64
+ if (err instanceof CliError && err.code === 'UNSUPPORTED_SHELL')
65
+ throw err;
66
+ if (err instanceof CommanderStructuralError) {
67
+ await writeToStream(stderr, err.output);
68
+ return { handled: true, exitCode: err.exitCode };
69
+ }
70
+ if (err instanceof CommanderCompatibleError) {
71
+ await writeToStream(stderr, err.output);
72
+ if (err.stdoutOutput)
73
+ await writeToStream(stdout, err.stdoutOutput);
74
+ return { handled: true, exitCode: err.exitCode };
75
+ }
76
+ if (err instanceof MissingRequiredPositionalError) {
77
+ await writeToStream(stderr, `error: missing required argument '${err.argumentName}'\n`);
78
+ return { handled: true, exitCode: EXIT_CODES.GENERIC_ERROR };
79
+ }
80
+ await writeToStream(stderr, formatErrorEnvelope(toEnvelope(err), {
81
+ cmdName: hostedCommandName(argv),
82
+ traceMode: hostedTraceMode(argv),
83
+ }));
27
84
  return {
28
85
  handled: true,
29
86
  exitCode: errorExitCode(err),
30
87
  };
31
88
  }
32
89
  }
33
- async function dispatchHosted(argv, client, stdout) {
34
- const normalized = stripGlobalOptions(argv);
90
+ async function dispatchHosted(argv, client, stdout, stderr, now) {
91
+ const normalized = parseHostedRootCommandSurface(argv);
92
+ if (normalized.kind === 'help') {
93
+ const help = formatRootHelp(HOSTED_ROOT_HELP);
94
+ if (normalized.exitCode !== EXIT_CODES.SUCCESS) {
95
+ throw new CommanderCompatibleError(help, normalized.exitCode);
96
+ }
97
+ await writeToStream(stdout, help);
98
+ return;
99
+ }
100
+ if (normalized.kind === 'version') {
101
+ await writeToStream(stdout, normalized.output);
102
+ return;
103
+ }
104
+ if (normalized.kind === 'completion') {
105
+ const manifest = await client.getManifest();
106
+ validateManifestContractIdentity(manifest);
107
+ await writeToStream(stdout, hostedCompletions(manifest, normalized.argv).join('\n') + '\n');
108
+ return;
109
+ }
35
110
  const args = normalized.argv;
36
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
37
- stdout.write('Usage: webcmd <site> <command> [options]\n webcmd list\n webcmd setup\n');
111
+ if (args[0] === 'completion') {
112
+ const parsed = parseHostedCompletionSurface(args.slice(1), normalized.literal);
113
+ if (parsed.kind === 'help') {
114
+ await writeToStream(stdout, parsed.output);
115
+ return;
116
+ }
117
+ const script = getCompletionScriptFast(parsed.shell);
118
+ if (script === undefined) {
119
+ // Run the canonical local Commander action for this rare error path so
120
+ // hosted mode preserves even the historical public stack bytes.
121
+ const { createProgram } = await import('../cli.js');
122
+ await createProgram('', '').parseAsync(argv, { from: 'user' });
123
+ throw new CliError('UNSUPPORTED_SHELL', `Unsupported shell: ${parsed.shell}. Supported: bash, zsh, fish`);
124
+ }
125
+ await writeToStream(stdout, script);
38
126
  return;
39
127
  }
40
128
  if (args[0] === 'daemon') {
41
- throw new ConfigError('webcmd daemon is local-only. Hosted mode has no local daemon.', 'Run `webcmd setup` and choose local mode to manage the local daemon.');
129
+ throw new ConfigError('webcmd daemon is local-only. Hosted mode has no local daemon.', LOCAL_ONLY_COMMAND_HELP);
42
130
  }
43
131
  if (args[0] === 'browser') {
44
- await dispatchHostedBrowser(args, normalized.profile, client, stdout);
45
- return;
46
- }
47
- const manifest = await client.getManifest();
48
- if (isCompletionRequest(args)) {
49
- stdout.write(hostedCompletions(manifest, args).join('\n') + '\n');
132
+ const invocation = parseHostedBrowserInvocation(args, normalized.profile);
133
+ const manifest = await client.getManifest();
134
+ validateManifestContractIdentity(manifest);
135
+ await dispatchHostedBrowser(invocation, client, stdout);
50
136
  return;
51
137
  }
52
138
  if (args[0] === 'list') {
53
- renderHostedList(manifest, args.slice(1));
139
+ const parsed = parseHostedListSurface(args.slice(1), normalized.literal);
140
+ if (parsed.kind === 'help') {
141
+ await writeToStream(stdout, parsed.output);
142
+ return;
143
+ }
144
+ const manifest = await client.getManifest();
145
+ validateManifestContractIdentity(manifest);
146
+ await renderHostedList(manifest, parsed.format, parsed.formatExplicit, stdout);
54
147
  return;
55
148
  }
149
+ const manifest = await client.getManifest();
150
+ validateManifestContractIdentity(manifest);
56
151
  const site = args[0];
57
152
  const commandName = args[1];
153
+ const siteExists = manifest.commands.some(command => command.site === site);
154
+ if (!siteExists) {
155
+ const unknownRoot = parseUnknownSiteRootOptions(args, normalized.literal);
156
+ if (unknownRoot.version) {
157
+ await writeToStream(stdout, `${PKG_VERSION}\n`);
158
+ return;
159
+ }
160
+ if (unknownRoot.help) {
161
+ await writeToStream(stdout, formatRootHelp(HOSTED_ROOT_HELP));
162
+ return;
163
+ }
164
+ throw new CommanderCompatibleError(`error: unknown command '${site}'\n`, EXIT_CODES.USAGE_ERROR, formatRootHelp(HOSTED_ROOT_HELP));
165
+ }
58
166
  if (!commandName || commandName === '--help' || commandName === '-h') {
59
- stdout.write(renderHostedSiteHelp(manifest, site));
167
+ const data = hostedSiteHelpData(manifest, site);
168
+ if (!data) {
169
+ throw new CommanderCompatibleError(`error: unknown command '${site}'\n`, EXIT_CODES.USAGE_ERROR, formatRootHelp(HOSTED_ROOT_HELP));
170
+ }
171
+ await writeHostedHelp(stdout, args, data, renderHostedSiteHelp(manifest, site));
60
172
  return;
61
173
  }
62
174
  const command = findHostedCommand(manifest, site, commandName);
63
175
  if (!command) {
64
- throw new ConfigError(`Unknown hosted Webcmd command: ${site}/${commandName}`);
176
+ if (!normalized.literal && hasTerminalBeforeSeparator(args.slice(1), token => token === '--help' || token === '-h')) {
177
+ const data = hostedSiteHelpData(manifest, site);
178
+ if (!data) {
179
+ throw new CommanderCompatibleError(`error: unknown command '${site}'\n`, EXIT_CODES.USAGE_ERROR, formatRootHelp(HOSTED_ROOT_HELP));
180
+ }
181
+ await writeHostedHelp(stdout, args, data, renderHostedSiteHelp(manifest, site));
182
+ return;
183
+ }
184
+ throw new CommanderCompatibleError(`error: unknown command '${commandName}'\n`, EXIT_CODES.GENERIC_ERROR);
65
185
  }
66
186
  if (isLocalOnlyHostedCommand(command)) {
67
- throw new ConfigError(`Command ${command.command} is local-only and is not available in hosted mode.`, 'Run `webcmd setup` and choose local mode to use local-only commands.');
187
+ throw new ConfigError(`Command ${command.command} is local-only and is not available in hosted mode.`, LOCAL_ONLY_COMMAND_HELP);
68
188
  }
69
- const parsed = parseHostedInvocation(command, [...args.slice(2), ...normalized.trailingCommandOptions]);
189
+ const parsed = parseHostedInvocation(command, args.slice(2));
70
190
  if (parsed.help) {
71
- stdout.write(renderHostedCommandHelp(command));
191
+ await writeHostedHelp(stdout, args, hostedCommandHelpData(command), renderHostedCommandHelp(command));
72
192
  return;
73
193
  }
74
- const response = await client.execute({
75
- command: command.command,
76
- args: parsed.args,
77
- format: parsed.format,
78
- trace: parsed.trace,
79
- profile: parsed.profile ?? normalized.profile,
194
+ const startTime = now();
195
+ const response = hasPresentFileArgument(command, parsed.args)
196
+ ? await executeHostedFileCommand({
197
+ client,
198
+ command,
199
+ args: parsed.args,
200
+ format: parsed.format,
201
+ trace: parsed.trace,
202
+ profile: parsed.profile ?? normalized.profile,
203
+ })
204
+ : await client.execute({
205
+ command: command.command,
206
+ args: parsed.args,
207
+ format: parsed.format,
208
+ trace: parsed.trace,
209
+ profile: parsed.profile ?? normalized.profile,
210
+ });
211
+ let format = parsed.format;
212
+ if (!parsed.formatExplicit && format === 'table' && command.defaultFormat) {
213
+ format = command.defaultFormat;
214
+ }
215
+ const elapsed = (now() - startTime) / 1000;
216
+ if (response.result !== null && response.result !== undefined) {
217
+ await renderOutput(response.result, {
218
+ fmt: format,
219
+ fmtExplicit: parsed.formatExplicit,
220
+ columns: response.columns ?? command.columns,
221
+ title: command.command,
222
+ elapsed,
223
+ source: command.command,
224
+ footerExtra: response.footerExtra,
225
+ stdout,
226
+ });
227
+ }
228
+ if (parsed.trace === 'on' && response.trace) {
229
+ await writeToStream(stderr, `Webcmd trace artifact: ${response.trace.receipt}\n`);
230
+ }
231
+ }
232
+ function hasPresentFileArgument(command, args) {
233
+ return command.args.some((arg) => {
234
+ if (!arg.file)
235
+ return false;
236
+ const value = args[arg.name] ?? arg.default;
237
+ return value !== undefined && value !== null && value !== '';
238
+ });
239
+ }
240
+ async function executeHostedFileCommand(input) {
241
+ const prepared = await prepareHostedFiles({
242
+ client: input.client,
243
+ command: input.command,
244
+ args: input.args,
80
245
  });
81
- const result = response.result ?? response.rows ?? response.data ?? null;
82
- renderOutput(result, {
83
- fmt: parsed.format,
84
- fmtExplicit: true,
85
- columns: response.columns ?? command.columns,
86
- title: command.command,
87
- source: 'webcmd cloud',
246
+ const response = await input.client.runPreparedExecution({
247
+ executionId: prepared.executionId,
248
+ command: input.command.command,
249
+ args: prepared.args,
250
+ format: input.format,
251
+ trace: input.trace,
252
+ ...(input.profile !== undefined ? { profile: input.profile } : {}),
88
253
  });
254
+ const materialized = await materializeHostedOutputs({
255
+ client: input.client,
256
+ response,
257
+ outputs: prepared.outputs,
258
+ });
259
+ return {
260
+ ...response,
261
+ result: rewriteHostedOutputResultPaths(response.result, materialized),
262
+ };
89
263
  }
90
- async function dispatchHostedBrowser(argv, profile, client, stdout) {
91
- const invocation = parseHostedBrowserInvocation(argv, profile);
264
+ async function dispatchHostedBrowser(invocation, client, stdout) {
265
+ const args = invocation.action === 'set-file-input'
266
+ ? materializeHostedBrowserUploadArgs(invocation.args)
267
+ : invocation.args;
92
268
  const response = await client.runBrowserAction(invocation.session, {
93
269
  command: invocation.command,
94
270
  action: invocation.action,
95
- args: invocation.args,
271
+ args,
96
272
  ...(invocation.profile !== undefined ? { profile: invocation.profile } : {}),
97
273
  ...(invocation.windowMode !== undefined ? { windowMode: invocation.windowMode } : {}),
98
274
  trace: 'off',
99
275
  });
100
- renderHostedBrowserResponse(stdout, invocation, response);
276
+ await renderHostedBrowserResponse(stdout, invocation, response);
277
+ }
278
+ function materializeHostedBrowserUploadArgs(args) {
279
+ const files = args.files;
280
+ if (!Array.isArray(files))
281
+ return args;
282
+ return {
283
+ ...args,
284
+ files: files.map((file) => {
285
+ if (typeof file !== 'string')
286
+ return file;
287
+ const body = readFileSync(file);
288
+ return {
289
+ $webcmdBrowserUpload: {
290
+ filename: path.basename(file),
291
+ contentType: contentTypeForUpload(file),
292
+ base64: Buffer.from(body).toString('base64'),
293
+ },
294
+ };
295
+ }),
296
+ };
297
+ }
298
+ function contentTypeForUpload(filePath) {
299
+ switch (path.extname(filePath).toLowerCase()) {
300
+ case '.gif':
301
+ return 'image/gif';
302
+ case '.jpg':
303
+ case '.jpeg':
304
+ return 'image/jpeg';
305
+ case '.json':
306
+ return 'application/json';
307
+ case '.pdf':
308
+ return 'application/pdf';
309
+ case '.png':
310
+ return 'image/png';
311
+ case '.txt':
312
+ return 'text/plain';
313
+ case '.webp':
314
+ return 'image/webp';
315
+ default:
316
+ return 'application/octet-stream';
317
+ }
101
318
  }
102
319
  function parseHostedBrowserInvocation(argv, profile) {
103
320
  let rewritten;
@@ -110,37 +327,28 @@ function parseHostedBrowserInvocation(argv, profile) {
110
327
  }
111
328
  throw error;
112
329
  }
330
+ let structure;
331
+ try {
332
+ structure = parseHostedBrowserStructure(rewritten);
333
+ }
334
+ catch (error) {
335
+ if (error instanceof HostedBrowserHelp)
336
+ throw new CommanderCompatibleError('', 0, error.output);
337
+ throw error;
338
+ }
113
339
  if (rewritten[0] !== 'browser') {
114
340
  throw new ConfigError('Hosted browser invocation must start with browser.');
115
341
  }
116
- if (rewritten[1] !== '--session' || !rewritten[2]) {
342
+ if (!structure.session) {
117
343
  throw new ConfigError('<session> is required for hosted browser commands.', 'Use: webcmd browser <session> <command>');
118
344
  }
119
- const session = rewritten[2];
120
- let index = 3;
121
- let windowMode;
122
- while (index < rewritten.length) {
123
- const token = rewritten[index];
124
- if (token === '--window') {
125
- windowMode = parseWindowMode(rewritten[index + 1]);
126
- index += 2;
127
- continue;
128
- }
129
- if (token?.startsWith('--window=')) {
130
- windowMode = parseWindowMode(token.slice('--window='.length));
131
- index += 1;
132
- continue;
133
- }
134
- break;
135
- }
136
- const leaf = rewritten[index];
137
- if (!leaf || leaf === '--help' || leaf === '-h') {
345
+ if (!structure.commandName) {
138
346
  throw new ConfigError('Hosted browser command is required.', 'Use: webcmd browser <session> open <url>, state, screenshot, tab list, or eval <js>.');
139
347
  }
140
- const rest = rewritten.slice(index + 1);
141
- const parsed = parseBrowserLeaf(leaf, rest);
348
+ const windowMode = structure.window === undefined ? undefined : parseWindowMode(structure.window);
349
+ const parsed = parseBrowserLeaf(structure.commandName, structure.positionals, structure.options);
142
350
  return {
143
- session,
351
+ session: structure.session,
144
352
  command: `browser/${parsed.commandName}`,
145
353
  action: parsed.action,
146
354
  args: parsed.args,
@@ -154,245 +362,278 @@ function parseWindowMode(value) {
154
362
  return value;
155
363
  throw new ConfigError('--window must be one of: foreground, background.');
156
364
  }
157
- function parseBrowserLeaf(leaf, argv) {
158
- const parsed = splitOptions(argv);
159
- switch (leaf) {
160
- case 'bind':
365
+ function parseBrowserLeaf(leaf, positionals, options) {
366
+ const contract = hostedBrowserCommandsByPath.get(leaf);
367
+ if (!contract || !contract.action) {
368
+ if (leaf === 'bind' || contract?.sessionPolicy === 'local-only') {
161
369
  throw new ConfigError('Browser bind is not supported in hosted mode.', 'Use browser state or browser tabs to inspect the active hosted page.');
162
- case 'unbind':
163
- case 'close':
164
- return { commandName: leaf, action: 'close-window', args: {} };
165
- case 'open':
166
- return { commandName: 'open', action: 'navigate', args: { url: requiredPositional(parsed.positionals, 0, 'url') } };
167
- case 'back':
168
- return { commandName: 'back', action: 'back', args: {} };
169
- case 'state':
170
- return { commandName: 'state', action: 'snapshot', args: { source: parsed.options.source ?? 'dom' } };
171
- case 'frames':
172
- return { commandName: 'frames', action: 'frames', args: {} };
173
- case 'screenshot': {
174
- const localPath = parsed.positionals[0];
175
- return {
176
- commandName: 'screenshot',
177
- action: 'screenshot',
178
- args: {
179
- fullPage: parsed.options.fullPage === true,
180
- ...(parsed.options.width !== undefined ? { width: parsed.options.width } : {}),
181
- ...(parsed.options.height !== undefined ? { height: parsed.options.height } : {}),
182
- },
183
- ...(localPath !== undefined ? { localPath } : {}),
184
- };
185
370
  }
186
- case 'tab':
187
- return parseBrowserTab(parsed.positionals);
188
- case 'eval':
371
+ throw new ConfigError(`Hosted browser command is not supported yet: ${leaf}`);
372
+ }
373
+ const localPath = leaf === 'screenshot' ? positionals[0] : undefined;
374
+ const args = browserActionArgs(contract, positionals, options);
375
+ return {
376
+ commandName: leaf,
377
+ action: contract.action,
378
+ args,
379
+ ...(localPath !== undefined ? { localPath } : {}),
380
+ };
381
+ }
382
+ function browserActionArgs(contract, positionals, options) {
383
+ const args = compactRecord({ ...options });
384
+ let index = 0;
385
+ for (const positional of contract.positionals) {
386
+ if (positional.variadic) {
387
+ const rest = positionals.slice(index);
388
+ if (rest.length)
389
+ args[positional.name] = rest;
390
+ index = positionals.length;
391
+ continue;
392
+ }
393
+ const value = positionals[index];
394
+ if (value !== undefined)
395
+ args[positional.name] = value;
396
+ index += 1;
397
+ }
398
+ switch (contract.command) {
399
+ case 'screenshot':
400
+ delete args.path;
401
+ return args;
402
+ case 'tab/list':
403
+ return { ...args, op: 'list' };
404
+ case 'tab/new':
189
405
  return {
190
- commandName: 'eval',
191
- action: 'exec',
192
- args: {
193
- js: requiredPositional(parsed.positionals, 0, 'js'),
194
- ...(parsed.options.frame !== undefined ? { frame: parsed.options.frame } : {}),
195
- },
406
+ ...withoutKeys(args, ['url']),
407
+ op: 'new',
408
+ ...(typeof args.url === 'string' && args.url ? { url: args.url } : {}),
196
409
  };
197
- case 'scroll':
410
+ case 'tab/select':
198
411
  return {
199
- commandName: 'scroll',
200
- action: 'scroll',
201
- args: {
202
- direction: requiredPositional(parsed.positionals, 0, 'direction'),
203
- amount: parsed.options.amount ?? 500,
204
- },
412
+ ...withoutKeys(args, ['targetId']),
413
+ op: 'select',
414
+ ...(typeof args.targetId === 'string' && args.targetId ? { target: args.targetId } : {}),
205
415
  };
206
- case 'keys':
207
- return { commandName: 'keys', action: 'press-key', args: { key: requiredPositional(parsed.positionals, 0, 'key') } };
208
- case 'wait':
416
+ case 'tab/close':
209
417
  return {
210
- commandName: 'wait',
211
- action: 'wait',
212
- args: {
213
- type: requiredPositional(parsed.positionals, 0, 'type'),
214
- ...(parsed.positionals[1] !== undefined ? { value: parsed.positionals[1] } : {}),
215
- ...(parsed.options.timeout !== undefined ? { timeout: parsed.options.timeout } : {}),
216
- },
418
+ ...withoutKeys(args, ['targetId']),
419
+ op: 'close',
420
+ ...(typeof args.targetId === 'string' && args.targetId ? { target: args.targetId } : {}),
217
421
  };
218
- case 'click':
219
- return { commandName: 'click', action: 'click', args: { target: requiredPositional(parsed.positionals, 0, 'target') } };
220
422
  case 'type':
221
- return {
222
- commandName: 'type',
223
- action: 'type',
224
- args: {
225
- target: requiredPositional(parsed.positionals, 0, 'target'),
226
- text: requiredPositional(parsed.positionals, 1, 'text'),
227
- },
228
- };
423
+ return rewriteTextTargetArgs(args, options, 'targetOrText', 'text');
229
424
  case 'fill':
230
- return {
231
- commandName: 'fill',
232
- action: 'fill',
233
- args: {
234
- target: requiredPositional(parsed.positionals, 0, 'target'),
235
- text: requiredPositional(parsed.positionals, 1, 'text'),
236
- },
237
- };
425
+ return rewriteTextTargetArgs(args, options, 'targetOrText', 'text');
426
+ case 'select':
427
+ return rewriteTextTargetArgs(args, options, 'targetOrOption', 'option');
238
428
  case 'upload':
239
- return {
240
- commandName: 'upload',
241
- action: 'set-file-input',
242
- args: {
243
- selector: parsed.positionals[0] ?? 'input[type="file"]',
244
- files: parsed.positionals.slice(1),
245
- },
246
- };
247
- case 'console':
248
- return { commandName: 'console', action: 'console', args: parsed.options };
249
- case 'network':
250
- return { commandName: 'network', action: 'network', args: parsed.options };
429
+ return rewriteUploadArgs(args, options);
251
430
  default:
252
- throw new ConfigError(`Hosted browser command is not supported yet: ${leaf}`);
431
+ return args;
253
432
  }
254
433
  }
255
- function parseBrowserTab(positionals) {
256
- const op = positionals[0] ?? 'list';
257
- if (op === 'list')
258
- return { commandName: 'tab/list', action: 'tabs', args: { op: 'list' } };
259
- if (op === 'new')
260
- return { commandName: 'tab/new', action: 'tabs', args: { op: 'new', ...(positionals[1] ? { url: positionals[1] } : {}) } };
261
- if (op === 'select')
262
- return { commandName: 'tab/select', action: 'tabs', args: { op: 'select', target: requiredPositional(positionals, 1, 'targetId') } };
263
- if (op === 'close')
264
- return { commandName: 'tab/close', action: 'tabs', args: { op: 'close', target: requiredPositional(positionals, 1, 'targetId') } };
265
- throw new ConfigError(`Hosted browser tab command is not supported yet: ${op}`);
266
- }
267
- function splitOptions(argv) {
268
- const positionals = [];
269
- const options = {};
270
- let literal = false;
271
- for (let i = 0; i < argv.length; i++) {
272
- const token = argv[i];
273
- if (literal) {
274
- positionals.push(token);
275
- continue;
276
- }
277
- if (token === '--') {
278
- literal = true;
279
- continue;
280
- }
281
- if (!token.startsWith('-') || token === '-') {
282
- positionals.push(token);
283
- continue;
284
- }
285
- if (token.startsWith('--') && token.includes('=')) {
286
- const [rawKey, ...rawValue] = token.slice(2).split('=');
287
- options[toCamelCase(rawKey)] = coerceOptionValue(rawValue.join('='));
288
- continue;
289
- }
290
- const key = token.replace(/^-+/, '');
291
- const next = argv[i + 1];
292
- if (next !== undefined && !next.startsWith('-')) {
293
- options[toCamelCase(key)] = coerceOptionValue(next);
294
- i += 1;
295
- }
296
- else {
297
- options[toCamelCase(key)] = true;
298
- }
434
+ function rewriteTextTargetArgs(args, options, firstPositionalName, valueName) {
435
+ const first = args[firstPositionalName];
436
+ const value = args[valueName];
437
+ const next = withoutKeys(args, [firstPositionalName, valueName]);
438
+ if (hasSemanticLocator(options)) {
439
+ return {
440
+ ...next,
441
+ ...(typeof first === 'string' ? { [valueName]: first } : {}),
442
+ };
299
443
  }
300
- return { positionals, options };
444
+ return {
445
+ ...next,
446
+ ...(typeof first === 'string' ? { target: first } : {}),
447
+ ...(typeof value === 'string' ? { [valueName]: value } : {}),
448
+ };
301
449
  }
302
- function requiredPositional(values, index, label) {
303
- const value = values[index];
304
- if (value === undefined || value === '') {
305
- throw new ConfigError(`Missing required browser argument: ${label}`);
450
+ function rewriteUploadArgs(args, options) {
451
+ const targetOrFile = args.targetOrFile;
452
+ const files = Array.isArray(args.files) ? args.files.filter((entry) => typeof entry === 'string') : [];
453
+ const next = withoutKeys(args, ['targetOrFile', 'files']);
454
+ if (hasSemanticLocator(options)) {
455
+ return {
456
+ ...next,
457
+ files: [
458
+ ...(typeof targetOrFile === 'string' ? [targetOrFile] : []),
459
+ ...files,
460
+ ],
461
+ };
306
462
  }
307
- return value;
463
+ return {
464
+ ...next,
465
+ selector: typeof targetOrFile === 'string' ? targetOrFile : 'input[type="file"]',
466
+ files,
467
+ };
468
+ }
469
+ function hasSemanticLocator(args) {
470
+ return ['role', 'name', 'label', 'text', 'testid'].some(key => args[key] !== undefined);
308
471
  }
309
- function toCamelCase(value) {
310
- return value.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
472
+ function compactRecord(input) {
473
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
311
474
  }
312
- function coerceOptionValue(value) {
313
- if (value === 'true')
314
- return true;
315
- if (value === 'false')
316
- return false;
317
- if (/^-?\d+$/.test(value))
318
- return Number.parseInt(value, 10);
319
- return value;
475
+ function withoutKeys(input, keys) {
476
+ const blocked = new Set(keys);
477
+ return Object.fromEntries(Object.entries(input).filter(([key, value]) => !blocked.has(key) && value !== undefined));
320
478
  }
321
- function renderHostedBrowserResponse(stdout, invocation, response) {
479
+ async function renderHostedBrowserResponse(stdout, invocation, response) {
322
480
  const result = response.result;
323
481
  if (invocation.action === 'snapshot' && result && typeof result === 'object') {
324
482
  const record = result;
325
- stdout.write(`URL: ${typeof record.url === 'string' ? record.url : ''}\n\n`);
326
- stdout.write(`${typeof record.snapshot === 'string' ? record.snapshot : JSON.stringify(record.snapshot, null, 2)}\n`);
483
+ await writeToStream(stdout, `URL: ${typeof record.url === 'string' ? record.url : ''}\n\n`);
484
+ await writeToStream(stdout, `${typeof record.snapshot === 'string' ? record.snapshot : JSON.stringify(record.snapshot, null, 2)}\n`);
327
485
  return;
328
486
  }
329
487
  if (invocation.action === 'screenshot' && result && typeof result === 'object') {
330
488
  const base64 = result.base64;
331
489
  if (typeof base64 === 'string' && invocation.localPath) {
332
490
  writeFileSync(invocation.localPath, Buffer.from(base64, 'base64'));
333
- stdout.write(`Screenshot saved to: ${invocation.localPath}\n`);
491
+ await writeToStream(stdout, `Screenshot saved to: ${invocation.localPath}\n`);
334
492
  return;
335
493
  }
336
494
  if (typeof base64 === 'string') {
337
- stdout.write(`${base64}\n`);
495
+ await writeToStream(stdout, `${base64}\n`);
338
496
  return;
339
497
  }
340
498
  }
341
499
  if (typeof result === 'string') {
342
- stdout.write(`${result}\n`);
500
+ await writeToStream(stdout, `${result}\n`);
343
501
  return;
344
502
  }
345
- stdout.write(`${JSON.stringify(result ?? response, null, 2)}\n`);
503
+ await writeToStream(stdout, `${JSON.stringify(result, null, 2)}\n`);
346
504
  }
347
- function renderHostedList(manifest, argv) {
348
- const fmt = readFormat(argv);
349
- const structured = fmt === 'json' || fmt === 'yaml' || fmt === 'yml';
350
- renderOutput(hostedListRows(manifest, structured), {
505
+ async function renderHostedList(manifest, fmt, explicit, stdout) {
506
+ const presentation = hostedListPresentation(manifest, fmt);
507
+ if (presentation.displayLines) {
508
+ for (const line of presentation.displayLines)
509
+ await writeToStream(stdout, `${line}\n`);
510
+ return;
511
+ }
512
+ await renderOutput(presentation.rows, {
351
513
  fmt,
352
- fmtExplicit: true,
353
- columns: ['command', 'site', 'name', 'aliases', 'description', 'access', 'strategy', 'browser', 'args',
354
- ...(structured ? ['columns', 'domain'] : [])],
355
- title: 'webcmd/list',
356
- source: 'webcmd cloud',
514
+ fmtExplicit: explicit,
515
+ columns: presentation.columns,
516
+ stdout,
517
+ });
518
+ }
519
+ async function writeHostedHelp(stdout, argv, data, text) {
520
+ const format = getRequestedHelpFormat(argv);
521
+ await writeToStream(stdout, format ? renderStructuredHelp(data, format) : text);
522
+ }
523
+ function parseHostedListSurface(argv, literal) {
524
+ let stdout = '';
525
+ let stderr = '';
526
+ let parsedFormat = 'table';
527
+ let formatExplicit = false;
528
+ let actionRan = false;
529
+ const root = new Command('webcmd');
530
+ const list = configureListCommandSurface(root.command('list'));
531
+ const output = {
532
+ writeOut: (value) => { stdout += value; },
533
+ writeErr: (value) => { stderr += value; },
534
+ };
535
+ root.exitOverride().configureOutput(output);
536
+ list.exitOverride().configureOutput(output).action((options) => {
537
+ actionRan = true;
538
+ parsedFormat = options.format;
539
+ formatExplicit = list.getOptionValueSource('format') === 'cli';
357
540
  });
541
+ try {
542
+ root.parse(literal ? ['--', 'list', ...argv] : ['list', ...argv], { from: 'user' });
543
+ }
544
+ catch (error) {
545
+ if (!(error instanceof CommanderError))
546
+ throw error;
547
+ if (error.code === 'commander.helpDisplayed')
548
+ return { kind: 'help', output: stdout };
549
+ throw new CommanderStructuralError(stderr || `${error.message}\n`, error.exitCode);
550
+ }
551
+ if (!actionRan)
552
+ throw new CommanderStructuralError("error: command 'list' did not run\n", 1);
553
+ return { kind: 'run', format: parsedFormat, formatExplicit };
358
554
  }
359
- function readFormat(argv) {
360
- for (let i = 0; i < argv.length; i++) {
361
- if (argv[i] === '-f' || argv[i] === '--format')
362
- return argv[i + 1] ?? 'table';
363
- if (argv[i]?.startsWith('--format='))
364
- return argv[i].slice('--format='.length);
365
- }
366
- return 'table';
555
+ function parseHostedCompletionSurface(argv, literal) {
556
+ let stdout = '';
557
+ let stderr = '';
558
+ let shell;
559
+ const root = new Command('webcmd');
560
+ const completion = configureCompletionCommandSurface(root.command('completion'));
561
+ const output = {
562
+ writeOut: (value) => { stdout += value; },
563
+ writeErr: (value) => { stderr += value; },
564
+ };
565
+ root.exitOverride().configureOutput(output);
566
+ completion.exitOverride().configureOutput(output).action((value) => {
567
+ shell = value;
568
+ });
569
+ try {
570
+ root.parse(literal ? ['--', 'completion', ...argv] : ['completion', ...argv], { from: 'user' });
571
+ }
572
+ catch (error) {
573
+ if (!(error instanceof CommanderError))
574
+ throw error;
575
+ if (error.code === 'commander.helpDisplayed')
576
+ return { kind: 'help', output: stdout };
577
+ throw new CommanderStructuralError(stderr || `${error.message}\n`, error.exitCode);
578
+ }
579
+ if (shell === undefined) {
580
+ throw new CommanderStructuralError("error: missing required argument 'shell'\n", 1);
581
+ }
582
+ return { kind: 'run', shell };
367
583
  }
368
- function stripGlobalOptions(argv) {
369
- const out = [];
370
- const trailingCommandOptions = [];
584
+ function parseUnknownSiteRootOptions(argv, literal) {
585
+ if (literal)
586
+ return { help: false, version: false };
371
587
  let profile;
372
- for (let i = 0; i < argv.length; i++) {
588
+ let help = false;
589
+ for (let i = 1; i < argv.length; i += 1) {
373
590
  const token = argv[i];
591
+ if (token === '--')
592
+ break;
374
593
  if (token === '--profile') {
375
- profile = argv[++i];
594
+ const value = argv[i + 1];
595
+ if (value === undefined) {
596
+ throw new CommanderStructuralError("error: option '--profile <name>' argument missing\n", 1);
597
+ }
598
+ profile = value;
599
+ i += 1;
376
600
  continue;
377
601
  }
378
602
  if (token.startsWith('--profile=')) {
379
603
  profile = token.slice('--profile='.length);
380
604
  continue;
381
605
  }
382
- out.push(token);
606
+ if (token === '--version' || token.startsWith('-V')) {
607
+ return { help: false, version: true, ...(profile !== undefined ? { profile } : {}) };
608
+ }
609
+ if (token === '--help' || token === '-h')
610
+ help = true;
383
611
  }
384
- return { argv: out, trailingCommandOptions, ...(profile ? { profile } : {}) };
612
+ return { help, version: false, ...(profile !== undefined ? { profile } : {}) };
385
613
  }
386
- function isCompletionRequest(argv) {
387
- return argv.includes('--get-completions');
614
+ function hasTerminalBeforeSeparator(argv, predicate) {
615
+ for (const token of argv) {
616
+ if (token === '--')
617
+ return false;
618
+ if (predicate(token))
619
+ return true;
620
+ }
621
+ return false;
388
622
  }
389
623
  function hostedCompletions(manifest, argv) {
390
624
  const index = argv.indexOf('--get-completions');
391
- const words = index === -1 ? argv : argv.slice(index + 1).filter((word) => word !== '--cursor');
392
- const meaningful = words.filter((word) => !/^\d+$/.test(word));
393
- if (meaningful.length <= 1)
394
- return ['list', 'setup', ...siteNames(manifest)];
395
- return commandNamesForSite(manifest, meaningful[0]);
625
+ const rest = index === -1 ? argv : argv.slice(index + 1);
626
+ const words = [];
627
+ let cursor;
628
+ for (let i = 0; i < rest.length; i += 1) {
629
+ if (rest[i] === '--cursor' && i + 1 < rest.length) {
630
+ cursor = Number.parseInt(rest[++i], 10);
631
+ }
632
+ else {
633
+ words.push(rest[i]);
634
+ }
635
+ }
636
+ return getCommandCompletionCandidates(hostedCommands(manifest), words, Number.isFinite(cursor) ? cursor : words.length, HOSTED_BUILTIN_COMMANDS);
396
637
  }
397
638
  function errorExitCode(err) {
398
639
  if (err instanceof ConfigError)
@@ -402,3 +643,63 @@ function errorExitCode(err) {
402
643
  }
403
644
  return EXIT_CODES.GENERIC_ERROR;
404
645
  }
646
+ function validateManifestContractIdentity(manifest) {
647
+ const installed = readInstalledHostedContractIdentity();
648
+ const installedLine = hostedContractCompatibilityLine(installed.webcmdVersion);
649
+ const manifestLine = hostedContractCompatibilityLine(manifest.metadata.webcmdPackageVersion);
650
+ if (manifest.metadata.contractSchemaVersion !== installed.schemaVersion
651
+ || !installedLine
652
+ || !manifestLine
653
+ || manifestLine !== installedLine) {
654
+ throw new HostedClientError('HOSTED_PROTOCOL', 'Webcmd Cloud manifest does not match this installed Webcmd hosted contract.');
655
+ }
656
+ }
657
+ function hostedContractCompatibilityLine(version) {
658
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
659
+ if (!match)
660
+ return undefined;
661
+ return `${match[1]}.${match[2]}.0`;
662
+ }
663
+ function readInstalledHostedContractIdentity() {
664
+ try {
665
+ const moduleFile = fileURLToPath(import.meta.url);
666
+ const packageRoot = findPackageRoot(moduleFile);
667
+ const value = JSON.parse(readFileSync(path.join(packageRoot, 'hosted-contract.json'), 'utf-8'));
668
+ if (!value
669
+ || typeof value !== 'object'
670
+ || typeof value.schemaVersion !== 'number'
671
+ || typeof value.webcmdVersion !== 'string') {
672
+ throw new Error('invalid hosted contract identity');
673
+ }
674
+ return value;
675
+ }
676
+ catch {
677
+ throw new HostedClientError('HOSTED_PROTOCOL', 'The installed Webcmd hosted contract could not be validated.');
678
+ }
679
+ }
680
+ function hostedCommandName(argv) {
681
+ const positionals = [];
682
+ const valueOptions = new Set(['--profile', '-f', '--format', '--trace']);
683
+ for (let i = 0; i < argv.length && positionals.length < 2; i += 1) {
684
+ const token = argv[i];
685
+ if (valueOptions.has(token)) {
686
+ i += 1;
687
+ continue;
688
+ }
689
+ if (token.startsWith('-'))
690
+ continue;
691
+ positionals.push(token);
692
+ }
693
+ if (positionals.length < 2)
694
+ return positionals[0];
695
+ return `${positionals[0]}/${positionals[1]}`;
696
+ }
697
+ function hostedTraceMode(argv) {
698
+ for (let i = 0; i < argv.length; i += 1) {
699
+ if (argv[i] === '--trace')
700
+ return argv[i + 1];
701
+ if (argv[i]?.startsWith('--trace='))
702
+ return argv[i].slice('--trace='.length);
703
+ }
704
+ return undefined;
705
+ }