@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.
- package/README.md +13 -2
- package/cli-manifest.json +217 -13
- 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/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 +180 -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 +36748 -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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { deriveBrowserAvailability, deriveHostedAvailability, } from './availability.js';
|
|
6
|
+
const EXPECTED_LOCAL_TOOLS = [
|
|
7
|
+
'antigravity/recent-paths',
|
|
8
|
+
'antigravity/settings-read',
|
|
9
|
+
'antigravity/state-get',
|
|
10
|
+
'antigravity/state-keys',
|
|
11
|
+
'antigravity/workspaces-list',
|
|
12
|
+
'mercury/reimbursement-plan',
|
|
13
|
+
'trae-solo/extensions-list',
|
|
14
|
+
'trae-solo/recent-workspaces',
|
|
15
|
+
'trae-solo/settings-read',
|
|
16
|
+
'trae-solo/skill-fs-installed',
|
|
17
|
+
'trae-solo/skill-fs-list',
|
|
18
|
+
'trae-solo/skill-fs-show',
|
|
19
|
+
'trae-solo/state-get',
|
|
20
|
+
'trae-solo/state-keys',
|
|
21
|
+
'trae-solo/task-fs-list',
|
|
22
|
+
'trae-solo/task-fs-show',
|
|
23
|
+
'trae-solo/task-fs-turns',
|
|
24
|
+
'trae-solo/user-rules',
|
|
25
|
+
'trae-solo/workspaces-list',
|
|
26
|
+
];
|
|
27
|
+
const EXPECTED_DESKTOP_APPS = [
|
|
28
|
+
'antigravity/add-context',
|
|
29
|
+
'antigravity/cookies',
|
|
30
|
+
'antigravity/copy-code',
|
|
31
|
+
'antigravity/copy-message',
|
|
32
|
+
'antigravity/delete',
|
|
33
|
+
'antigravity/display-options',
|
|
34
|
+
'antigravity/dump',
|
|
35
|
+
'antigravity/extract-code',
|
|
36
|
+
'antigravity/history',
|
|
37
|
+
'antigravity/idb-list',
|
|
38
|
+
'antigravity/mark-read',
|
|
39
|
+
'antigravity/model',
|
|
40
|
+
'antigravity/nav',
|
|
41
|
+
'antigravity/new',
|
|
42
|
+
'antigravity/react',
|
|
43
|
+
'antigravity/read',
|
|
44
|
+
'antigravity/rename',
|
|
45
|
+
'antigravity/revert',
|
|
46
|
+
'antigravity/send',
|
|
47
|
+
'antigravity/settings',
|
|
48
|
+
'antigravity/sidebar-toggle',
|
|
49
|
+
'antigravity/status',
|
|
50
|
+
'antigravity/storage-get',
|
|
51
|
+
'antigravity/storage-keys',
|
|
52
|
+
'antigravity/toggle-aux',
|
|
53
|
+
'antigravity/watch',
|
|
54
|
+
'chatgpt-app/ask',
|
|
55
|
+
'chatgpt-app/model',
|
|
56
|
+
'chatgpt-app/new',
|
|
57
|
+
'chatgpt-app/read',
|
|
58
|
+
'chatgpt-app/send',
|
|
59
|
+
'chatgpt-app/status',
|
|
60
|
+
'chatwise/ask',
|
|
61
|
+
'chatwise/export',
|
|
62
|
+
'chatwise/history',
|
|
63
|
+
'chatwise/model',
|
|
64
|
+
'chatwise/new',
|
|
65
|
+
'chatwise/read',
|
|
66
|
+
'chatwise/screenshot',
|
|
67
|
+
'chatwise/send',
|
|
68
|
+
'chatwise/status',
|
|
69
|
+
'codex/archive',
|
|
70
|
+
'codex/ask',
|
|
71
|
+
'codex/dump',
|
|
72
|
+
'codex/export',
|
|
73
|
+
'codex/extract-diff',
|
|
74
|
+
'codex/history',
|
|
75
|
+
'codex/model',
|
|
76
|
+
'codex/new',
|
|
77
|
+
'codex/pin',
|
|
78
|
+
'codex/projects',
|
|
79
|
+
'codex/read',
|
|
80
|
+
'codex/rename',
|
|
81
|
+
'codex/screenshot',
|
|
82
|
+
'codex/send',
|
|
83
|
+
'codex/status',
|
|
84
|
+
'codex/unpin',
|
|
85
|
+
'cursor/ask',
|
|
86
|
+
'cursor/composer',
|
|
87
|
+
'cursor/dump',
|
|
88
|
+
'cursor/export',
|
|
89
|
+
'cursor/extract-code',
|
|
90
|
+
'cursor/history',
|
|
91
|
+
'cursor/model',
|
|
92
|
+
'cursor/new',
|
|
93
|
+
'cursor/read',
|
|
94
|
+
'cursor/screenshot',
|
|
95
|
+
'cursor/send',
|
|
96
|
+
'cursor/status',
|
|
97
|
+
'discord-app/channels',
|
|
98
|
+
'discord-app/delete',
|
|
99
|
+
'discord-app/goto',
|
|
100
|
+
'discord-app/members',
|
|
101
|
+
'discord-app/read',
|
|
102
|
+
'discord-app/search',
|
|
103
|
+
'discord-app/send',
|
|
104
|
+
'discord-app/servers',
|
|
105
|
+
'discord-app/status',
|
|
106
|
+
'discord-app/thread-read',
|
|
107
|
+
'discord-app/threads',
|
|
108
|
+
'qoder/account',
|
|
109
|
+
'qoder/add-workspace',
|
|
110
|
+
'qoder/ask',
|
|
111
|
+
'qoder/credits',
|
|
112
|
+
'qoder/history',
|
|
113
|
+
'qoder/knowledge',
|
|
114
|
+
'qoder/marketplace',
|
|
115
|
+
'qoder/more-actions',
|
|
116
|
+
'qoder/new',
|
|
117
|
+
'qoder/open-editor',
|
|
118
|
+
'qoder/open-panel',
|
|
119
|
+
'qoder/prompt-enhance',
|
|
120
|
+
'qoder/read',
|
|
121
|
+
'qoder/search',
|
|
122
|
+
'qoder/send',
|
|
123
|
+
'qoder/settings',
|
|
124
|
+
'qoder/sidebar-toggle',
|
|
125
|
+
'qoder/status',
|
|
126
|
+
'qoder/view-all',
|
|
127
|
+
'trae-solo/automation-list',
|
|
128
|
+
'trae-solo/cookies',
|
|
129
|
+
'trae-solo/history',
|
|
130
|
+
'trae-solo/idb-list',
|
|
131
|
+
'trae-solo/mode',
|
|
132
|
+
'trae-solo/model',
|
|
133
|
+
'trae-solo/skill-category',
|
|
134
|
+
'trae-solo/skill-list',
|
|
135
|
+
'trae-solo/skill-search',
|
|
136
|
+
'trae-solo/status',
|
|
137
|
+
'trae-solo/storage-get',
|
|
138
|
+
'trae-solo/storage-keys',
|
|
139
|
+
];
|
|
140
|
+
function exceptionDiff(actual, expected) {
|
|
141
|
+
const actualSet = new Set(actual);
|
|
142
|
+
const expectedSet = new Set(expected);
|
|
143
|
+
return {
|
|
144
|
+
added: actual.filter(name => !expectedSet.has(name)),
|
|
145
|
+
missing: expected.filter(name => !actualSet.has(name)),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
describe('hosted availability', () => {
|
|
149
|
+
it('derives decisions only from normalized strategy and domain metadata', () => {
|
|
150
|
+
expect(deriveHostedAvailability({ strategy: 'local', domain: 'localhost' }))
|
|
151
|
+
.toEqual({ mode: 'local-only', reason: 'local-tool' });
|
|
152
|
+
expect(deriveHostedAvailability({ strategy: 'ui', domain: 'localhost' }))
|
|
153
|
+
.toEqual({ mode: 'local-only', reason: 'desktop-app' });
|
|
154
|
+
expect(deriveHostedAvailability({ strategy: 'cookie', domain: 'example.com' }))
|
|
155
|
+
.toEqual({ mode: 'hosted' });
|
|
156
|
+
expect(deriveBrowserAvailability('bind'))
|
|
157
|
+
.toEqual({ mode: 'local-only', reason: 'browser-bind' });
|
|
158
|
+
expect(deriveBrowserAvailability('open')).toEqual({ mode: 'hosted' });
|
|
159
|
+
});
|
|
160
|
+
it('matches the reviewed local-only adapter exception sets exactly', () => {
|
|
161
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
162
|
+
const entries = JSON.parse(fs.readFileSync(path.join(root, 'cli-manifest.json'), 'utf8'));
|
|
163
|
+
const byReason = new Map([
|
|
164
|
+
['local-tool', []],
|
|
165
|
+
['desktop-app', []],
|
|
166
|
+
]);
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const availability = deriveHostedAvailability(entry);
|
|
169
|
+
if (availability.mode === 'local-only') {
|
|
170
|
+
byReason.get(availability.reason)?.push(`${entry.site}/${entry.name}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const localTools = (byReason.get('local-tool') ?? []).sort();
|
|
174
|
+
const desktopApps = (byReason.get('desktop-app') ?? []).sort();
|
|
175
|
+
expect(exceptionDiff(localTools, EXPECTED_LOCAL_TOOLS)).toEqual({ added: [], missing: [] });
|
|
176
|
+
expect(exceptionDiff(desktopApps, EXPECTED_DESKTOP_APPS)).toEqual({ added: [], missing: [] });
|
|
177
|
+
expect(localTools).toHaveLength(19);
|
|
178
|
+
expect(desktopApps).toHaveLength(111);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class HostedBrowserHelp extends Error {
|
|
2
|
+
readonly output: string;
|
|
3
|
+
constructor(output: string);
|
|
4
|
+
}
|
|
5
|
+
export interface ParsedHostedBrowserStructure {
|
|
6
|
+
commandName?: string;
|
|
7
|
+
positionals: string[];
|
|
8
|
+
options: Record<string, unknown>;
|
|
9
|
+
session?: string;
|
|
10
|
+
window?: string;
|
|
11
|
+
profile?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse the hosted browser argv with the exact canonical Commander grammar.
|
|
15
|
+
* The returned values are the values produced by Commander's action boundary;
|
|
16
|
+
* callers must not reinterpret the original argv with a second parser.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseHostedBrowserStructure(argv: readonly string[]): ParsedHostedBrowserStructure;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
import { browserCommandCatalog, browserOptionFlags, browserOptionValueParser, } from '../browser/command-catalog.js';
|
|
3
|
+
import { CommanderStructuralError } from '../command-surface.js';
|
|
4
|
+
export class HostedBrowserHelp extends Error {
|
|
5
|
+
output;
|
|
6
|
+
constructor(output) {
|
|
7
|
+
super('Hosted browser help requested');
|
|
8
|
+
this.output = output;
|
|
9
|
+
this.name = 'HostedBrowserHelp';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse the hosted browser argv with the exact canonical Commander grammar.
|
|
14
|
+
* The returned values are the values produced by Commander's action boundary;
|
|
15
|
+
* callers must not reinterpret the original argv with a second parser.
|
|
16
|
+
*/
|
|
17
|
+
export function parseHostedBrowserStructure(argv) {
|
|
18
|
+
const root = new Command('webcmd')
|
|
19
|
+
.option('--profile <name>', 'Chrome profile/context alias for browser runtime commands')
|
|
20
|
+
.enablePositionalOptions();
|
|
21
|
+
const browser = root
|
|
22
|
+
.command('browser')
|
|
23
|
+
.addOption(new Option('--session <name>', 'Internal — set automatically from the <session> positional').hideHelp())
|
|
24
|
+
.option('--window <mode>', 'Browser window mode: foreground or background')
|
|
25
|
+
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)')
|
|
26
|
+
.usage('<session> <command> [options]')
|
|
27
|
+
.addHelpText('after', `
|
|
28
|
+
<session> is a required positional: pass the name of the browser session every subcommand should operate on. Reuse the same name across calls to keep the tab/state alive; pick a different name to isolate parallel browser work.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
$ webcmd browser work open https://x.com
|
|
32
|
+
$ webcmd browser work open https://x.com --window background
|
|
33
|
+
$ webcmd browser work click 12
|
|
34
|
+
$ webcmd browser work state
|
|
35
|
+
$ webcmd browser work tab list
|
|
36
|
+
$ webcmd browser work bind --page page-123
|
|
37
|
+
$ webcmd browser work unbind # compatibility command; releases the Cloak session
|
|
38
|
+
`);
|
|
39
|
+
let parsed;
|
|
40
|
+
const namespaces = new Map([['', browser]]);
|
|
41
|
+
const namespaceDescriptions = {
|
|
42
|
+
dialog: 'Handle a blocking JavaScript alert/confirm/prompt dialog',
|
|
43
|
+
get: 'Get page properties',
|
|
44
|
+
tab: 'Tab management — list, create, and close tabs in the browser session',
|
|
45
|
+
};
|
|
46
|
+
for (const contract of browserCommandCatalog) {
|
|
47
|
+
const parts = contract.command.split('/');
|
|
48
|
+
let parent = browser;
|
|
49
|
+
let path = '';
|
|
50
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
51
|
+
path = path ? `${path}/${parts[index]}` : parts[index];
|
|
52
|
+
let namespace = namespaces.get(path);
|
|
53
|
+
if (!namespace) {
|
|
54
|
+
namespace = parent.command(parts[index]);
|
|
55
|
+
const description = namespaceDescriptions[path];
|
|
56
|
+
if (description)
|
|
57
|
+
namespace.description(description);
|
|
58
|
+
namespaces.set(path, namespace);
|
|
59
|
+
}
|
|
60
|
+
parent = namespace;
|
|
61
|
+
}
|
|
62
|
+
const leafName = parts.at(-1);
|
|
63
|
+
const leaf = parent.command(leafName).description(contract.description);
|
|
64
|
+
for (const alias of contract.aliases)
|
|
65
|
+
leaf.alias(alias);
|
|
66
|
+
for (const positional of contract.positionals) {
|
|
67
|
+
const suffix = positional.variadic ? '...' : '';
|
|
68
|
+
leaf.argument(positional.required ? `<${positional.name}${suffix}>` : `[${positional.name}${suffix}]`, positional.description);
|
|
69
|
+
}
|
|
70
|
+
for (const option of contract.options) {
|
|
71
|
+
const flags = browserOptionFlags(option);
|
|
72
|
+
if (option.type === 'boolean') {
|
|
73
|
+
leaf.option(flags, option.description, option.default);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const commanderOption = new Option(flags, option.description);
|
|
77
|
+
if (option.choices?.length)
|
|
78
|
+
commanderOption.choices(option.choices);
|
|
79
|
+
if (option.default !== undefined)
|
|
80
|
+
commanderOption.default(String(option.default));
|
|
81
|
+
const valueParser = browserOptionValueParser(contract.command, option.name);
|
|
82
|
+
if (valueParser)
|
|
83
|
+
commanderOption.argParser(valueParser);
|
|
84
|
+
leaf.addOption(commanderOption);
|
|
85
|
+
}
|
|
86
|
+
leaf.action((...actionArgs) => {
|
|
87
|
+
const options = actionArgs[contract.positionals.length];
|
|
88
|
+
parsed = {
|
|
89
|
+
commandName: contract.command,
|
|
90
|
+
positionals: actionArgs.slice(0, contract.positionals.length).flatMap(value => {
|
|
91
|
+
if (typeof value === 'string')
|
|
92
|
+
return [value];
|
|
93
|
+
if (Array.isArray(value))
|
|
94
|
+
return value.filter((entry) => typeof entry === 'string');
|
|
95
|
+
return [];
|
|
96
|
+
}),
|
|
97
|
+
options: { ...options },
|
|
98
|
+
...readBrowserGlobals(root, browser),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
let stderr = '';
|
|
103
|
+
let stdout = '';
|
|
104
|
+
const output = {
|
|
105
|
+
writeErr: (value) => { stderr += value; },
|
|
106
|
+
writeOut: (value) => { stdout += value; },
|
|
107
|
+
};
|
|
108
|
+
const configure = (command) => {
|
|
109
|
+
command.exitOverride().configureOutput(output);
|
|
110
|
+
for (const child of command.commands)
|
|
111
|
+
configure(child);
|
|
112
|
+
};
|
|
113
|
+
configure(root);
|
|
114
|
+
const browserAwareUsage = (command) => {
|
|
115
|
+
const ancestors = [];
|
|
116
|
+
let ancestor = command.parent;
|
|
117
|
+
while (ancestor) {
|
|
118
|
+
ancestors.unshift(ancestor === browser ? `${ancestor.name()} <session>` : ancestor.name());
|
|
119
|
+
ancestor = ancestor.parent;
|
|
120
|
+
}
|
|
121
|
+
return [...ancestors, command.name(), command.usage()].filter(Boolean).join(' ').trim();
|
|
122
|
+
};
|
|
123
|
+
const configureBrowserUsage = (command) => {
|
|
124
|
+
command.configureHelp({ commandUsage: browserAwareUsage });
|
|
125
|
+
for (const child of command.commands)
|
|
126
|
+
configureBrowserUsage(child);
|
|
127
|
+
};
|
|
128
|
+
configureBrowserUsage(browser);
|
|
129
|
+
try {
|
|
130
|
+
root.parse([...argv], { from: 'user' });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const commander = error;
|
|
134
|
+
if (commander.code === 'commander.helpDisplayed')
|
|
135
|
+
throw new HostedBrowserHelp(stdout);
|
|
136
|
+
throw new CommanderStructuralError(stderr || `${typeof commander.message === 'string' ? commander.message : String(error)}\n`, typeof commander.exitCode === 'number' ? commander.exitCode : 1);
|
|
137
|
+
}
|
|
138
|
+
return parsed ?? {
|
|
139
|
+
positionals: [],
|
|
140
|
+
options: {},
|
|
141
|
+
...readBrowserGlobals(root, browser),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function readBrowserGlobals(root, browser) {
|
|
145
|
+
const rootOptions = root.opts();
|
|
146
|
+
const browserOptions = browser.opts();
|
|
147
|
+
return {
|
|
148
|
+
...(typeof browserOptions.session === 'string' ? { session: browserOptions.session } : {}),
|
|
149
|
+
...(typeof browserOptions.window === 'string' ? { window: browserOptions.window } : {}),
|
|
150
|
+
...(typeof rootOptions.profile === 'string' ? { profile: rootOptions.profile } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { browserCommandCatalog } from '../browser/command-catalog.js';
|
|
3
|
+
import { rewriteBrowserArgv } from '../cli-argv-preprocess.js';
|
|
4
|
+
import { createProgram } from '../cli.js';
|
|
5
|
+
import { CommanderStructuralError } from '../command-surface.js';
|
|
6
|
+
import { HostedBrowserHelp, parseHostedBrowserStructure, } from './browser-args.js';
|
|
7
|
+
function findLocalBrowserLeaf(program, commandPath) {
|
|
8
|
+
const browser = program.commands.find(command => command.name() === 'browser');
|
|
9
|
+
if (!browser)
|
|
10
|
+
throw new Error('Local browser namespace is missing');
|
|
11
|
+
let leaf = browser;
|
|
12
|
+
for (const part of commandPath.split('/')) {
|
|
13
|
+
const child = leaf.commands.find(command => command.name() === part || command.aliases().includes(part));
|
|
14
|
+
if (!child)
|
|
15
|
+
throw new Error(`Local browser command is missing: ${commandPath}`);
|
|
16
|
+
leaf = child;
|
|
17
|
+
}
|
|
18
|
+
return { browser, leaf };
|
|
19
|
+
}
|
|
20
|
+
function flattenPositionals(values) {
|
|
21
|
+
return values.flatMap(value => {
|
|
22
|
+
if (typeof value === 'string')
|
|
23
|
+
return [value];
|
|
24
|
+
if (Array.isArray(value))
|
|
25
|
+
return value.filter((entry) => typeof entry === 'string');
|
|
26
|
+
return [];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function captureLocalBrowserValues(commandPath, argv) {
|
|
30
|
+
const program = createProgram('', '');
|
|
31
|
+
const contract = browserCommandCatalog.find(command => command.command === commandPath);
|
|
32
|
+
if (!contract)
|
|
33
|
+
throw new Error(`Browser contract is missing: ${commandPath}`);
|
|
34
|
+
const { browser, leaf } = findLocalBrowserLeaf(program, commandPath);
|
|
35
|
+
let parsed;
|
|
36
|
+
leaf.action((...actionArgs) => {
|
|
37
|
+
const options = actionArgs[contract.positionals.length];
|
|
38
|
+
const rootOptions = program.opts();
|
|
39
|
+
const browserOptions = browser.opts();
|
|
40
|
+
parsed = {
|
|
41
|
+
commandName: commandPath,
|
|
42
|
+
positionals: flattenPositionals(actionArgs.slice(0, contract.positionals.length)),
|
|
43
|
+
options: { ...options },
|
|
44
|
+
...(typeof browserOptions.session === 'string' ? { session: browserOptions.session } : {}),
|
|
45
|
+
...(typeof browserOptions.window === 'string' ? { window: browserOptions.window } : {}),
|
|
46
|
+
...(typeof rootOptions.profile === 'string' ? { profile: rootOptions.profile } : {}),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const configure = (command) => {
|
|
50
|
+
command.exitOverride().configureOutput({ writeErr: () => undefined, writeOut: () => undefined });
|
|
51
|
+
for (const child of command.commands)
|
|
52
|
+
configure(child);
|
|
53
|
+
};
|
|
54
|
+
configure(program);
|
|
55
|
+
program.parse(rewriteBrowserArgv(argv), { from: 'user' });
|
|
56
|
+
if (!parsed)
|
|
57
|
+
throw new Error(`Local browser action did not run: ${commandPath}`);
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
function allPositionals(commandPath) {
|
|
61
|
+
const contract = browserCommandCatalog.find(command => command.command === commandPath);
|
|
62
|
+
return contract.positionals.flatMap(position => position.variadic
|
|
63
|
+
? [`${position.name}-one`, `${position.name}-two`]
|
|
64
|
+
: [`${position.name}-value`]);
|
|
65
|
+
}
|
|
66
|
+
function parseHosted(argv) {
|
|
67
|
+
return parseHostedBrowserStructure(rewriteBrowserArgv(argv));
|
|
68
|
+
}
|
|
69
|
+
describe('hosted browser canonical Commander values', () => {
|
|
70
|
+
it('matches the actual local action boundary for every catalogued browser leaf', () => {
|
|
71
|
+
for (const contract of browserCommandCatalog) {
|
|
72
|
+
const argv = ['browser', 'work', ...contract.command.split('/'), ...allPositionals(contract.command)];
|
|
73
|
+
expect({ command: contract.command, parsed: parseHosted(argv) }).toEqual({
|
|
74
|
+
command: contract.command,
|
|
75
|
+
parsed: captureLocalBrowserValues(contract.command, argv),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
it.each([
|
|
80
|
+
{
|
|
81
|
+
name: 'negative numeric required option value',
|
|
82
|
+
command: 'scroll',
|
|
83
|
+
tail: ['down', '--amount', '-5'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'dash-leading string option value',
|
|
87
|
+
command: 'state',
|
|
88
|
+
tail: ['--source', '-dash'],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'equals option form',
|
|
92
|
+
command: 'eval',
|
|
93
|
+
tail: ['return 1', '--frame=-2'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'repeated string option uses the last value',
|
|
97
|
+
command: 'scroll',
|
|
98
|
+
tail: ['down', '--amount', '10', '--amount=-5'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'repeated boolean flags remain boolean',
|
|
102
|
+
command: 'screenshot',
|
|
103
|
+
tail: ['shot.png', '--full-page', '--full-page', '--width=10', '--height', '20'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'negative boolean option grammar',
|
|
107
|
+
command: 'verify',
|
|
108
|
+
tail: ['site/command', '--no-fixture', '--no-fixture'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'dash-leading positional after separator',
|
|
112
|
+
command: 'eval',
|
|
113
|
+
tail: ['--', '-script'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'variadic dash-leading positionals after separator',
|
|
117
|
+
command: 'upload',
|
|
118
|
+
tail: ['input[type=file]', '--', '-one.txt', '-two.txt'],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'trailing namespace window is hoisted',
|
|
122
|
+
command: 'state',
|
|
123
|
+
tail: ['--window', 'background'],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'equals namespace window before leaf',
|
|
127
|
+
command: 'state',
|
|
128
|
+
prefix: ['--window=foreground'],
|
|
129
|
+
tail: [],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'repeated namespace window uses the last value',
|
|
133
|
+
command: 'state',
|
|
134
|
+
prefix: ['--window', 'foreground', '--window=background'],
|
|
135
|
+
tail: [],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'string and boolean observation options retain canonical types',
|
|
139
|
+
command: 'network',
|
|
140
|
+
tail: ['--since', '-5', '--follow', '--failed', '--max-body=-10'],
|
|
141
|
+
},
|
|
142
|
+
])('$name', ({ command, prefix = [], tail }) => {
|
|
143
|
+
const argv = ['browser', 'work', ...prefix, ...command.split('/'), ...tail];
|
|
144
|
+
expect(parseHosted(argv)).toEqual(captureLocalBrowserValues(command, argv));
|
|
145
|
+
});
|
|
146
|
+
it.each([
|
|
147
|
+
['ordinary unknown option', ['browser', 'work', 'state', '--unknown']],
|
|
148
|
+
['ordinary missing option value', ['browser', 'work', 'state', '--source']],
|
|
149
|
+
['ordinary excess positional', ['browser', 'work', 'state', 'extra']],
|
|
150
|
+
['missing required positional', ['browser', 'work', 'eval']],
|
|
151
|
+
['help wins over unknown', ['browser', 'work', 'state', '--unknown', '--help']],
|
|
152
|
+
['option missing value beats help', ['browser', 'work', 'state', '--help', '--source']],
|
|
153
|
+
['invalid structurally-coerced screenshot dimension', ['browser', 'work', 'screenshot', '--width=-10']],
|
|
154
|
+
['site-session is not a browser namespace option', ['browser', 'work', '--site-session', 'persistent', 'state']],
|
|
155
|
+
['keep-tab is not a browser namespace option', ['browser', 'work', 'state', '--keep-tab', 'true']],
|
|
156
|
+
])('retains exact local structural outcome: %s', (_name, argv) => {
|
|
157
|
+
const commandPath = argv.includes('eval') ? 'eval' : argv.includes('screenshot') ? 'screenshot' : 'state';
|
|
158
|
+
const capture = (run) => {
|
|
159
|
+
try {
|
|
160
|
+
run();
|
|
161
|
+
return { kind: 'success' };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error instanceof HostedBrowserHelp)
|
|
165
|
+
return { kind: 'help', output: error.output, exitCode: 0 };
|
|
166
|
+
if (error instanceof CommanderStructuralError) {
|
|
167
|
+
return { kind: 'structural', output: error.output, exitCode: error.exitCode };
|
|
168
|
+
}
|
|
169
|
+
const commander = error;
|
|
170
|
+
return {
|
|
171
|
+
kind: commander.code === 'commander.helpDisplayed' ? 'help' : 'structural',
|
|
172
|
+
exitCode: typeof commander.exitCode === 'number' ? commander.exitCode : 1,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const local = capture(() => captureLocalBrowserValues(commandPath, argv));
|
|
177
|
+
const hosted = capture(() => parseHosted(argv));
|
|
178
|
+
expect({ kind: hosted.kind, exitCode: hosted.exitCode }).toEqual({ kind: local.kind, exitCode: local.exitCode });
|
|
179
|
+
if (hosted.kind === 'structural')
|
|
180
|
+
expect(hosted.output).toBeDefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { CliError, type ExitCode } from '../errors.js';
|
|
2
|
-
import type { HostedBrowserActionRequest, HostedBrowserActionResponse, HostedBrowserFinishRequest, HostedBrowserFinishResponse, HostedBrowserRunActionInput, HostedBrowserRunActionResponse, HostedBrowserRunRequest, HostedBrowserRunResponse, HostedExecuteResponse, HostedManifest } from './types.js';
|
|
2
|
+
import type { HostedBrowserActionRequest, HostedBrowserActionResponse, HostedBrowserFinishRequest, HostedBrowserFinishResponse, HostedBrowserRunActionInput, HostedBrowserRunActionResponse, HostedBrowserRunRequest, HostedBrowserRunResponse, HostedExecution, HostedExecuteResponse, HostedPrepareExecutionResponse, HostedProfilesResponse, HostedUploadArtifactResponse, HostedManifest, HostedTraceReceipt } from './types.js';
|
|
3
3
|
export interface HostedClientOptions {
|
|
4
4
|
apiBaseUrl: string;
|
|
5
5
|
apiKey: string;
|
|
6
6
|
fetchImpl?: typeof fetch;
|
|
7
7
|
}
|
|
8
8
|
export declare class HostedClientError extends CliError {
|
|
9
|
-
|
|
9
|
+
readonly execution?: HostedExecution;
|
|
10
|
+
readonly trace?: HostedTraceReceipt;
|
|
11
|
+
constructor(code: string, message: string, help?: string, exitCode?: ExitCode, metadata?: {
|
|
12
|
+
execution?: HostedExecution;
|
|
13
|
+
trace?: HostedTraceReceipt;
|
|
14
|
+
});
|
|
10
15
|
}
|
|
11
16
|
export declare class HostedClient {
|
|
12
17
|
private readonly apiBaseUrl;
|
|
@@ -15,6 +20,7 @@ export declare class HostedClient {
|
|
|
15
20
|
constructor(options: HostedClientOptions);
|
|
16
21
|
getMe(): Promise<unknown>;
|
|
17
22
|
getManifest(): Promise<HostedManifest>;
|
|
23
|
+
listProfiles(): Promise<HostedProfilesResponse>;
|
|
18
24
|
execute(input: {
|
|
19
25
|
command: string;
|
|
20
26
|
args: Record<string, unknown>;
|
|
@@ -22,9 +28,32 @@ export declare class HostedClient {
|
|
|
22
28
|
trace?: string;
|
|
23
29
|
profile?: string;
|
|
24
30
|
}): Promise<HostedExecuteResponse>;
|
|
31
|
+
prepareExecution(input: {
|
|
32
|
+
command: string;
|
|
33
|
+
}): Promise<HostedPrepareExecutionResponse>;
|
|
34
|
+
uploadExecutionArtifact(input: {
|
|
35
|
+
executionId: string;
|
|
36
|
+
argument: string;
|
|
37
|
+
filename: string;
|
|
38
|
+
contentType: string;
|
|
39
|
+
body: Uint8Array;
|
|
40
|
+
}): Promise<HostedUploadArtifactResponse>;
|
|
41
|
+
runPreparedExecution(input: {
|
|
42
|
+
executionId: string;
|
|
43
|
+
command: string;
|
|
44
|
+
args: Record<string, unknown>;
|
|
45
|
+
format?: string;
|
|
46
|
+
trace?: string;
|
|
47
|
+
profile?: string;
|
|
48
|
+
}): Promise<HostedExecuteResponse>;
|
|
49
|
+
downloadExecutionArtifact(input: {
|
|
50
|
+
executionId: string;
|
|
51
|
+
artifactId: string;
|
|
52
|
+
}): Promise<Uint8Array>;
|
|
25
53
|
startBrowserRun(session: string, input: HostedBrowserRunRequest): Promise<HostedBrowserRunResponse>;
|
|
26
54
|
browserAction(session: string, executionId: string, input: HostedBrowserActionRequest): Promise<HostedBrowserActionResponse>;
|
|
27
55
|
finishBrowserRun(session: string, executionId: string, input: HostedBrowserFinishRequest): Promise<HostedBrowserFinishResponse>;
|
|
28
56
|
runBrowserAction(session: string, input: HostedBrowserRunActionInput): Promise<HostedBrowserRunActionResponse>;
|
|
57
|
+
executeBrowserCommand(session: string, input: HostedBrowserRunActionInput): Promise<HostedBrowserRunActionResponse>;
|
|
29
58
|
private request;
|
|
30
59
|
}
|