@agentrhq/webcmd 0.3.0 → 0.3.2
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.
- package/README.md +13 -2
- package/cli-manifest.json +228 -24
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/ask.test.js +11 -0
- package/clis/chatgpt/commands.test.js +102 -1
- package/clis/chatgpt/deep-research-result.js +45 -3
- package/clis/chatgpt/image.js +22 -2
- package/clis/chatgpt/project-file-add.js +24 -1
- package/clis/chatgpt/utils.js +258 -52
- package/clis/chatgpt/utils.test.js +259 -1
- package/clis/claude/ask.js +21 -1
- package/clis/facebook/feed.js +110 -24
- package/clis/facebook/feed.test.js +62 -0
- package/clis/facebook/search.js +185 -38
- package/clis/facebook/search.test.js +95 -50
- package/clis/instagram/explore.js +30 -12
- package/clis/instagram/explore.test.js +77 -0
- package/clis/instagram/post.js +14 -1
- package/clis/instagram/reel.js +13 -1
- package/clis/linkedin/company.js +153 -0
- package/clis/linkedin/company.test.js +111 -0
- package/clis/linkedin/connections.js +135 -0
- package/clis/linkedin/connections.test.js +141 -0
- package/clis/mercury/reimbursement-draft.js +12 -1
- package/clis/producthunt/hot.js +1 -2
- package/clis/spotify/spotify.js +11 -11
- package/clis/twitter/article-evaluate.test.js +27 -0
- package/clis/twitter/article.js +73 -16
- package/clis/twitter/article.test.js +209 -0
- package/clis/twitter/download.js +6 -1
- package/clis/twitter/post.js +14 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/profile.test.js +8 -0
- package/clis/twitter/quote.js +11 -1
- package/clis/twitter/reply.js +11 -1
- package/dist/src/browser/base-page.d.ts +26 -0
- package/dist/src/browser/base-page.js +23 -8
- package/dist/src/browser/base-page.test.js +45 -12
- package/dist/src/browser/command-catalog.d.ts +6 -0
- package/dist/src/browser/command-catalog.js +259 -0
- package/dist/src/browser/command-catalog.test.d.ts +1 -0
- package/dist/src/browser/command-catalog.test.js +79 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +99 -4
- package/dist/src/browser/target-resolver.test.js +141 -1
- package/dist/src/build-manifest.d.ts +6 -0
- package/dist/src/build-manifest.js +18 -1
- package/dist/src/build-manifest.test.js +57 -1
- package/dist/src/builtin-command-surface.d.ts +9 -0
- package/dist/src/builtin-command-surface.js +16 -0
- package/dist/src/check-hosted-contract.test.d.ts +1 -0
- package/dist/src/check-hosted-contract.test.js +56 -0
- package/dist/src/cli.js +58 -106
- package/dist/src/cli.test.js +71 -1
- package/dist/src/command-presentation.d.ts +95 -0
- package/dist/src/command-presentation.js +486 -0
- package/dist/src/command-presentation.test.d.ts +1 -0
- package/dist/src/command-presentation.test.js +97 -0
- package/dist/src/command-surface.d.ts +43 -0
- package/dist/src/command-surface.js +205 -0
- package/dist/src/command-surface.test.d.ts +1 -0
- package/dist/src/command-surface.test.js +406 -0
- package/dist/src/commanderAdapter.d.ts +5 -1
- package/dist/src/commanderAdapter.js +12 -55
- package/dist/src/commanderAdapter.test.js +15 -3
- package/dist/src/community-plugin-sync.d.ts +11 -0
- package/dist/src/community-plugin-sync.js +138 -0
- package/dist/src/community-plugin-sync.test.d.ts +1 -0
- package/dist/src/community-plugin-sync.test.js +123 -0
- package/dist/src/completion-fast.d.ts +4 -4
- package/dist/src/completion-fast.js +15 -34
- package/dist/src/completion-shared.d.ts +4 -0
- package/dist/src/completion-shared.js +38 -0
- package/dist/src/completion.js +3 -27
- package/dist/src/completion.test.js +31 -2
- package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
- package/dist/src/docs-sync-review-cli.test.js +322 -0
- package/dist/src/docs-sync-review.d.ts +117 -0
- package/dist/src/docs-sync-review.js +475 -0
- package/dist/src/docs-sync-review.test.d.ts +1 -0
- package/dist/src/docs-sync-review.test.js +324 -0
- package/dist/src/errors.d.ts +12 -3
- package/dist/src/errors.js +11 -7
- package/dist/src/errors.test.js +14 -1
- package/dist/src/execution.d.ts +1 -2
- package/dist/src/execution.js +4 -45
- package/dist/src/generate-release-notes-cli.test.js +7 -2
- package/dist/src/help.d.ts +4 -0
- package/dist/src/help.js +50 -255
- package/dist/src/help.test.js +27 -1
- package/dist/src/hosted/args.d.ts +2 -7
- package/dist/src/hosted/args.js +2 -99
- package/dist/src/hosted/args.test.js +15 -1
- package/dist/src/hosted/availability.d.ts +13 -0
- package/dist/src/hosted/availability.js +16 -0
- package/dist/src/hosted/availability.test.d.ts +1 -0
- package/dist/src/hosted/availability.test.js +191 -0
- package/dist/src/hosted/browser-args.d.ts +18 -0
- package/dist/src/hosted/browser-args.js +152 -0
- package/dist/src/hosted/browser-args.test.d.ts +1 -0
- package/dist/src/hosted/browser-args.test.js +182 -0
- package/dist/src/hosted/client.d.ts +31 -2
- package/dist/src/hosted/client.js +441 -53
- package/dist/src/hosted/client.test.js +651 -32
- package/dist/src/hosted/config.d.ts +7 -2
- package/dist/src/hosted/config.js +26 -4
- package/dist/src/hosted/config.test.js +34 -1
- package/dist/src/hosted/contract.d.ts +94 -0
- package/dist/src/hosted/contract.js +208 -0
- package/dist/src/hosted/contract.test.d.ts +1 -0
- package/dist/src/hosted/contract.test.js +361 -0
- package/dist/src/hosted/credentials.d.ts +38 -0
- package/dist/src/hosted/credentials.js +248 -0
- package/dist/src/hosted/credentials.test.d.ts +1 -0
- package/dist/src/hosted/credentials.test.js +93 -0
- package/dist/src/hosted/file-contract.test.d.ts +1 -0
- package/dist/src/hosted/file-contract.test.js +174 -0
- package/dist/src/hosted/files.d.ts +29 -0
- package/dist/src/hosted/files.js +296 -0
- package/dist/src/hosted/files.test.d.ts +1 -0
- package/dist/src/hosted/files.test.js +231 -0
- package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
- package/dist/src/hosted/main-lifecycle.test.js +213 -0
- package/dist/src/hosted/manifest.d.ts +5 -0
- package/dist/src/hosted/manifest.js +21 -66
- package/dist/src/hosted/manifest.test.js +120 -2
- package/dist/src/hosted/output-parity.test.d.ts +1 -0
- package/dist/src/hosted/output-parity.test.js +108 -0
- package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
- package/dist/src/hosted/root-command-surface.test.js +646 -0
- package/dist/src/hosted/runner.d.ts +7 -0
- package/dist/src/hosted/runner.js +554 -253
- package/dist/src/hosted/runner.test.js +1471 -53
- package/dist/src/hosted/setup.d.ts +3 -2
- package/dist/src/hosted/setup.js +45 -15
- package/dist/src/hosted/setup.test.js +133 -6
- package/dist/src/hosted/types.d.ts +101 -23
- package/dist/src/main.js +120 -108
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/output.d.ts +17 -2
- package/dist/src/output.js +88 -81
- package/dist/src/output.test.js +141 -51
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/plugin-create-cli.test.d.ts +1 -0
- package/dist/src/plugin-create-cli.test.js +37 -0
- package/dist/src/plugin-manifest.d.ts +12 -0
- package/dist/src/plugin-manifest.js +16 -0
- package/dist/src/plugin-manifest.test.js +19 -1
- package/dist/src/plugin-scaffold.d.ts +4 -1
- package/dist/src/plugin-scaffold.js +4 -1
- package/dist/src/plugin-scaffold.test.js +23 -8
- package/dist/src/plugin.js +4 -1
- package/dist/src/plugin.test.js +13 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/release-notes.js +12 -4
- package/dist/src/release-notes.test.js +27 -15
- package/dist/src/root-command-surface.d.ts +31 -0
- package/dist/src/root-command-surface.js +106 -0
- package/dist/src/serialization.d.ts +1 -16
- package/dist/src/serialization.js +5 -55
- package/dist/src/stream-write.d.ts +12 -0
- package/dist/src/stream-write.js +91 -0
- package/dist/src/stream-write.test.d.ts +1 -0
- package/dist/src/stream-write.test.js +186 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -8
- package/dist/src/utils.test.js +50 -0
- package/hosted-contract.json +36759 -0
- package/package.json +6 -1
- package/scripts/check-hosted-contract.mjs +108 -0
- package/scripts/docs-sync-review.ts +332 -0
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +7 -0
|
@@ -1,103 +1,320 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
37
|
-
|
|
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.',
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.`,
|
|
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,
|
|
189
|
+
const parsed = parseHostedInvocation(command, args.slice(2));
|
|
70
190
|
if (parsed.help) {
|
|
71
|
-
stdout
|
|
191
|
+
await writeHostedHelp(stdout, args, hostedCommandHelpData(command), renderHostedCommandHelp(command));
|
|
72
192
|
return;
|
|
73
193
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
91
|
-
const
|
|
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
|
|
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 (
|
|
342
|
+
if (!structure.session) {
|
|
117
343
|
throw new ConfigError('<session> is required for hosted browser commands.', 'Use: webcmd browser <session> <command>');
|
|
118
344
|
}
|
|
119
|
-
|
|
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
|
|
141
|
-
const parsed = parseBrowserLeaf(
|
|
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,
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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 '
|
|
410
|
+
case 'tab/select':
|
|
198
411
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
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 '
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
431
|
+
return args;
|
|
253
432
|
}
|
|
254
433
|
}
|
|
255
|
-
function
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
return {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 {
|
|
444
|
+
return {
|
|
445
|
+
...next,
|
|
446
|
+
...(typeof first === 'string' ? { target: first } : {}),
|
|
447
|
+
...(typeof value === 'string' ? { [valueName]: value } : {}),
|
|
448
|
+
};
|
|
301
449
|
}
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
|
310
|
-
return
|
|
472
|
+
function compactRecord(input) {
|
|
473
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
311
474
|
}
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
326
|
-
stdout
|
|
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
|
|
491
|
+
await writeToStream(stdout, `Screenshot saved to: ${invocation.localPath}\n`);
|
|
334
492
|
return;
|
|
335
493
|
}
|
|
336
494
|
if (typeof base64 === 'string') {
|
|
337
|
-
stdout
|
|
495
|
+
await writeToStream(stdout, `${base64}\n`);
|
|
338
496
|
return;
|
|
339
497
|
}
|
|
340
498
|
}
|
|
341
499
|
if (typeof result === 'string') {
|
|
342
|
-
stdout
|
|
500
|
+
await writeToStream(stdout, `${result}\n`);
|
|
343
501
|
return;
|
|
344
502
|
}
|
|
345
|
-
stdout
|
|
503
|
+
await writeToStream(stdout, `${JSON.stringify(result, null, 2)}\n`);
|
|
346
504
|
}
|
|
347
|
-
function renderHostedList(manifest,
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
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:
|
|
353
|
-
columns:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
584
|
+
function parseUnknownSiteRootOptions(argv, literal) {
|
|
585
|
+
if (literal)
|
|
586
|
+
return { help: false, version: false };
|
|
371
587
|
let profile;
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
612
|
+
return { help, version: false, ...(profile !== undefined ? { profile } : {}) };
|
|
385
613
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
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
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
}
|