@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
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { CommanderError } from 'commander';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { createProgram } from '../cli.js';
|
|
5
|
+
import { formatRootHelp } from '../command-presentation.js';
|
|
6
|
+
import { HOSTED_ROOT_HELP } from '../completion-shared.js';
|
|
7
|
+
import { CliError } from '../errors.js';
|
|
8
|
+
import { cli, getRegistry, Strategy } from '../registry.js';
|
|
9
|
+
import { PKG_VERSION } from '../version.js';
|
|
10
|
+
import { makeHostedConfig } from './config.js';
|
|
11
|
+
import { parseHostedRootCommandSurface, } from '../root-command-surface.js';
|
|
12
|
+
import { runHostedCli } from './runner.js';
|
|
13
|
+
function sink() {
|
|
14
|
+
let data = '';
|
|
15
|
+
return {
|
|
16
|
+
stream: new Writable({
|
|
17
|
+
write(chunk, _encoding, callback) {
|
|
18
|
+
data += String(chunk);
|
|
19
|
+
callback();
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
text: () => data,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const manifest = {
|
|
26
|
+
userId: 'user_demo',
|
|
27
|
+
metadata: {
|
|
28
|
+
contractSchemaVersion: 1,
|
|
29
|
+
webcmdPackageVersion: '0.3.0',
|
|
30
|
+
generatedAt: '2026-07-08T00:00:00.000Z',
|
|
31
|
+
},
|
|
32
|
+
commands: [{
|
|
33
|
+
site: 'github',
|
|
34
|
+
name: 'whoami',
|
|
35
|
+
command: 'github/whoami',
|
|
36
|
+
description: 'Show GitHub identity',
|
|
37
|
+
access: 'read',
|
|
38
|
+
strategy: 'PUBLIC',
|
|
39
|
+
browser: false,
|
|
40
|
+
args: [],
|
|
41
|
+
columns: ['username'],
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
function manifestResponse() {
|
|
45
|
+
return new Response(JSON.stringify({ ok: true, manifest }), { status: 200 });
|
|
46
|
+
}
|
|
47
|
+
function executionResponse() {
|
|
48
|
+
return new Response(JSON.stringify({
|
|
49
|
+
ok: true,
|
|
50
|
+
result: [],
|
|
51
|
+
execution: { id: 'exec_success', command: 'github/whoami', status: 'succeeded' },
|
|
52
|
+
}), { status: 200 });
|
|
53
|
+
}
|
|
54
|
+
async function runActualLocalRoot(argv) {
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
const previousExitCode = process.exitCode;
|
|
58
|
+
const previousArgv = process.argv;
|
|
59
|
+
const previousStdoutWrite = process.stdout.write;
|
|
60
|
+
const previousStderrWrite = process.stderr.write;
|
|
61
|
+
const previousConsoleError = console.error;
|
|
62
|
+
process.exitCode = undefined;
|
|
63
|
+
process.argv = ['node', 'webcmd', ...argv];
|
|
64
|
+
process.stdout.write = ((chunk, encodingOrCallback, callback) => {
|
|
65
|
+
stdout += String(chunk);
|
|
66
|
+
const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
|
67
|
+
if (typeof done === 'function')
|
|
68
|
+
done();
|
|
69
|
+
return true;
|
|
70
|
+
});
|
|
71
|
+
process.stderr.write = ((chunk, encodingOrCallback, callback) => {
|
|
72
|
+
stderr += String(chunk);
|
|
73
|
+
const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
|
74
|
+
if (typeof done === 'function')
|
|
75
|
+
done();
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
console.error = (...values) => { stderr += `${values.map(String).join(' ')}\n`; };
|
|
79
|
+
const program = createProgram('', '');
|
|
80
|
+
const configureTree = (command) => {
|
|
81
|
+
command.exitOverride().configureOutput({
|
|
82
|
+
writeOut: value => { stdout += value; },
|
|
83
|
+
writeErr: value => { stderr += value; },
|
|
84
|
+
});
|
|
85
|
+
for (const child of command.commands)
|
|
86
|
+
configureTree(child);
|
|
87
|
+
};
|
|
88
|
+
configureTree(program);
|
|
89
|
+
try {
|
|
90
|
+
await program.parseAsync(argv, { from: 'user' });
|
|
91
|
+
return { exitCode: Number(process.exitCode ?? 0), stdout, stderr };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof CommanderError) {
|
|
95
|
+
return { exitCode: error.exitCode, stdout, stderr, errorCode: error.code };
|
|
96
|
+
}
|
|
97
|
+
if (error instanceof CliError) {
|
|
98
|
+
return {
|
|
99
|
+
exitCode: error.exitCode,
|
|
100
|
+
stdout,
|
|
101
|
+
stderr,
|
|
102
|
+
errorCode: error.code,
|
|
103
|
+
errorMessage: error.message,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
process.exitCode = previousExitCode;
|
|
110
|
+
process.argv = previousArgv;
|
|
111
|
+
process.stdout.write = previousStdoutWrite;
|
|
112
|
+
process.stderr.write = previousStderrWrite;
|
|
113
|
+
console.error = previousConsoleError;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function runActualLocalKnownSite(argv) {
|
|
117
|
+
const registry = getRegistry();
|
|
118
|
+
const previous = new Map(registry);
|
|
119
|
+
registry.clear();
|
|
120
|
+
cli({
|
|
121
|
+
site: 'github',
|
|
122
|
+
name: 'whoami',
|
|
123
|
+
description: 'Show GitHub identity',
|
|
124
|
+
access: 'read',
|
|
125
|
+
strategy: Strategy.PUBLIC,
|
|
126
|
+
browser: false,
|
|
127
|
+
args: [],
|
|
128
|
+
columns: ['username'],
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
return await runActualLocalRoot(argv);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
registry.clear();
|
|
135
|
+
for (const [name, command] of previous)
|
|
136
|
+
registry.set(name, command);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function classifyHosted(result) {
|
|
140
|
+
return result.kind;
|
|
141
|
+
}
|
|
142
|
+
const profileForms = [
|
|
143
|
+
{ name: 'no-profile', tokens: [], profile: undefined },
|
|
144
|
+
{ name: 'split-profile', tokens: ['--profile', 'work'], profile: 'work' },
|
|
145
|
+
{ name: 'equals-profile', tokens: ['--profile=work'], profile: 'work' },
|
|
146
|
+
{ name: 'dash-profile', tokens: ['--profile', '-dash'], profile: '-dash' },
|
|
147
|
+
];
|
|
148
|
+
const dispatchForms = [
|
|
149
|
+
{ name: 'setup', tokens: ['setup'] },
|
|
150
|
+
{ name: 'browser', tokens: ['browser', 'work', 'state'] },
|
|
151
|
+
{ name: 'site', tokens: ['github', 'whoami'] },
|
|
152
|
+
];
|
|
153
|
+
const generatedTerminalCorpus = profileForms.flatMap(profile => dispatchForms.flatMap(dispatch => [
|
|
154
|
+
{ name: `${profile.name}/help/${dispatch.name}`, argv: [...profile.tokens, '--help', ...dispatch.tokens], kind: 'help' },
|
|
155
|
+
{ name: `${profile.name}/long-version/${dispatch.name}`, argv: [...profile.tokens, '--version', ...dispatch.tokens], kind: 'version' },
|
|
156
|
+
{ name: `${profile.name}/clustered-version/${dispatch.name}`, argv: [...profile.tokens, '-Vx', ...dispatch.tokens], kind: 'version' },
|
|
157
|
+
]));
|
|
158
|
+
describe('hosted root command surface', () => {
|
|
159
|
+
it.each([
|
|
160
|
+
{ name: 'no args', argv: [], expected: { kind: 'help', exitCode: 1 } },
|
|
161
|
+
{ name: 'profile only', argv: ['--profile', 'work'], expected: { kind: 'help', exitCode: 1 } },
|
|
162
|
+
{ name: 'empty equals profile', argv: ['--profile='], expected: { kind: 'help', exitCode: 1 } },
|
|
163
|
+
{ name: 'dash-leading profile', argv: ['--profile', '-dash'], expected: { kind: 'help', exitCode: 1 } },
|
|
164
|
+
{ name: 'help', argv: ['--help'], expected: { kind: 'help', exitCode: 0 } },
|
|
165
|
+
{ name: 'short help', argv: ['-h'], expected: { kind: 'help', exitCode: 0 } },
|
|
166
|
+
{ name: 'help beats preceding unknown', argv: ['--unknown', '--help'], expected: { kind: 'help', exitCode: 0 } },
|
|
167
|
+
{ name: 'help beats following unknown', argv: ['--help', '--unknown'], expected: { kind: 'help', exitCode: 0 } },
|
|
168
|
+
{ name: 'profile consumes help', argv: ['--profile', '--help'], expected: { kind: 'help', exitCode: 1 } },
|
|
169
|
+
{ name: 'version', argv: ['--version'], expected: { kind: 'version', output: `${PKG_VERSION}\n` } },
|
|
170
|
+
{ name: 'short version', argv: ['-V'], expected: { kind: 'version', output: `${PKG_VERSION}\n` } },
|
|
171
|
+
{ name: 'version-leading cluster', argv: ['-Vx'], expected: { kind: 'version', output: `${PKG_VERSION}\n` } },
|
|
172
|
+
{ name: 'version then help cluster', argv: ['-Vh'], expected: { kind: 'version', output: `${PKG_VERSION}\n` } },
|
|
173
|
+
{ name: 'site token', argv: ['github', 'whoami'], expected: { kind: 'dispatch', argv: ['github', 'whoami'], literal: false } },
|
|
174
|
+
{ name: 'root help after malformed prefix and site', argv: ['--unknown', 'github', '--help'], expected: { kind: 'help', exitCode: 0 } },
|
|
175
|
+
{ name: 'root help with complete trailing profile', argv: ['--help', 'github', '--profile', 'work'], expected: { kind: 'help', exitCode: 0 } },
|
|
176
|
+
{ name: 'site tail options stay at leaf', argv: ['github', 'whoami', '--profile', 'other'], expected: { kind: 'dispatch', argv: ['github', 'whoami', '--profile', 'other'], literal: false } },
|
|
177
|
+
{ name: 'profile before site', argv: ['--profile=work', 'github', 'whoami'], expected: { kind: 'dispatch', argv: ['github', 'whoami'], profile: 'work', literal: false } },
|
|
178
|
+
{ name: 'browser token', argv: ['browser', 'work', 'state'], expected: { kind: 'dispatch', argv: ['browser', 'work', 'state'], literal: false } },
|
|
179
|
+
{ name: 'setup token', argv: ['setup'], expected: { kind: 'dispatch', argv: ['setup'], literal: false } },
|
|
180
|
+
{ name: 'completion command is ordinary root dispatch', argv: ['completion', 'bash'], expected: { kind: 'dispatch', argv: ['completion', 'bash'], literal: false } },
|
|
181
|
+
{ name: 'bare separator', argv: ['--'], expected: { kind: 'help', exitCode: 1 } },
|
|
182
|
+
{ name: 'literal help', argv: ['--', '--help'], expected: { kind: 'dispatch', argv: ['--help'], literal: true } },
|
|
183
|
+
{ name: 'literal version', argv: ['--', '-V'], expected: { kind: 'dispatch', argv: ['-V'], literal: true } },
|
|
184
|
+
{ name: 'profile then literal site', argv: ['--profile', 'work', '--', 'github', '--profile', 'other'], expected: { kind: 'dispatch', argv: ['github', '--profile', 'other'], profile: 'work', literal: true } },
|
|
185
|
+
])('$name', ({ argv, expected }) => {
|
|
186
|
+
expect(parseHostedRootCommandSurface(argv)).toEqual(expected);
|
|
187
|
+
});
|
|
188
|
+
it.each([
|
|
189
|
+
{ name: 'missing profile', argv: ['--profile'], stderr: "error: option '--profile <name>' argument missing\n", code: 'commander.optionMissingArgument' },
|
|
190
|
+
{ name: 'missing profile beats help', argv: ['--help', '--profile'], stderr: "error: option '--profile <name>' argument missing\n", code: 'commander.optionMissingArgument' },
|
|
191
|
+
{ name: 'missing profile beats unknown', argv: ['--unknown', '--profile'], stderr: "error: option '--profile <name>' argument missing\n", code: 'commander.optionMissingArgument' },
|
|
192
|
+
{ name: 'unknown long', argv: ['--unknown'], stderr: "error: unknown option '--unknown'\n", code: 'commander.unknownOption' },
|
|
193
|
+
{ name: 'unknown short', argv: ['-x'], stderr: "error: unknown option '-x'\n", code: 'commander.unknownOption' },
|
|
194
|
+
{ name: 'unknown reverse cluster', argv: ['-xV'], stderr: "error: unknown option '-xV'\n", code: 'commander.unknownOption' },
|
|
195
|
+
{ name: 'help-leading cluster is not split', argv: ['-hV'], stderr: "error: unknown option '-hV'\n", code: 'commander.unknownOption' },
|
|
196
|
+
{ name: 'attached long typo', argv: ['--profilework'], stderr: "error: unknown option '--profilework'\n", code: 'commander.unknownOption' },
|
|
197
|
+
{ name: 'trailing missing profile beats root help', argv: ['--help', 'github', '--profile'], stderr: "error: option '--profile <name>' argument missing\n", code: 'commander.optionMissingArgument' },
|
|
198
|
+
{ name: 'trailing missing profile beats malformed prefix', argv: ['--unknown', 'github', '--profile'], stderr: "error: option '--profile <name>' argument missing\n", code: 'commander.optionMissingArgument' },
|
|
199
|
+
])('matches actual local Commander root error bytes: $name', async ({ argv, stderr, code }) => {
|
|
200
|
+
const local = await runActualLocalRoot(argv);
|
|
201
|
+
expect(local).toMatchObject({ exitCode: 1, stderr, errorCode: code });
|
|
202
|
+
expect(() => parseHostedRootCommandSurface(argv)).toThrowError(expect.objectContaining({ output: stderr, exitCode: 1 }));
|
|
203
|
+
});
|
|
204
|
+
it.each([
|
|
205
|
+
{ argv: [], localCode: 'commander.help', hostedKind: 'help' },
|
|
206
|
+
{ argv: ['--profile', 'work'], localCode: 'commander.help', hostedKind: 'help' },
|
|
207
|
+
{ argv: ['--'], localCode: 'commander.help', hostedKind: 'help' },
|
|
208
|
+
{ argv: ['--help'], localCode: 'commander.helpDisplayed', hostedKind: 'help' },
|
|
209
|
+
{ argv: ['--unknown', '--help'], localCode: 'commander.helpDisplayed', hostedKind: 'help' },
|
|
210
|
+
{ argv: ['--unknown', 'list', '--help'], localCode: 'commander.helpDisplayed', hostedKind: 'help' },
|
|
211
|
+
{ argv: ['-V'], localCode: 'commander.version', hostedKind: 'version' },
|
|
212
|
+
{ argv: ['-Vx'], localCode: 'commander.version', hostedKind: 'version' },
|
|
213
|
+
])('has the same terminal root disposition as the actual local program: $argv', async ({ argv, localCode, hostedKind }) => {
|
|
214
|
+
const local = await runActualLocalRoot(argv);
|
|
215
|
+
const hosted = parseHostedRootCommandSurface(argv);
|
|
216
|
+
expect(local.errorCode).toBe(localCode);
|
|
217
|
+
expect(classifyHosted(hosted)).toBe(hostedKind);
|
|
218
|
+
expect(local.exitCode).toBe(localCode === 'commander.help' ? 1 : 0);
|
|
219
|
+
if (localCode === 'commander.help') {
|
|
220
|
+
expect(local.stdout).toBe('');
|
|
221
|
+
expect(local.stderr).not.toBe('');
|
|
222
|
+
}
|
|
223
|
+
else if (localCode === 'commander.helpDisplayed') {
|
|
224
|
+
expect(local.stdout).not.toBe('');
|
|
225
|
+
expect(local.stderr).toBe('');
|
|
226
|
+
}
|
|
227
|
+
else if (hostedKind === 'version') {
|
|
228
|
+
expect(local.stdout).toBe(`${PKG_VERSION}\n`);
|
|
229
|
+
expect(local.stderr).toBe('');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it.each(generatedTerminalCorpus)('matches actual local terminal disposition across $name', async ({ argv, kind }) => {
|
|
233
|
+
const local = await runActualLocalRoot(argv);
|
|
234
|
+
const hosted = parseHostedRootCommandSurface(argv);
|
|
235
|
+
expect(hosted.kind).toBe(kind);
|
|
236
|
+
expect(local.exitCode).toBe(0);
|
|
237
|
+
expect(local.stderr).toBe('');
|
|
238
|
+
if (kind === 'help') {
|
|
239
|
+
expect(local.errorCode).toBe('commander.helpDisplayed');
|
|
240
|
+
expect(local.stdout).not.toBe('');
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
expect(local.errorCode).toBe('commander.version');
|
|
244
|
+
expect(local.stdout).toBe(`${PKG_VERSION}\n`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it.each(profileForms.flatMap(profile => dispatchForms.map(dispatch => ({ profile, dispatch }))))('preserves ordinary and separator dispatch across $profile.name/$dispatch.name', ({ profile, dispatch }) => {
|
|
248
|
+
expect(parseHostedRootCommandSurface([...profile.tokens, ...dispatch.tokens])).toEqual({
|
|
249
|
+
kind: 'dispatch',
|
|
250
|
+
argv: dispatch.tokens,
|
|
251
|
+
...(profile.profile !== undefined ? { profile: profile.profile } : {}),
|
|
252
|
+
literal: false,
|
|
253
|
+
});
|
|
254
|
+
expect(parseHostedRootCommandSurface([...profile.tokens, '--', ...dispatch.tokens])).toEqual({
|
|
255
|
+
kind: 'dispatch',
|
|
256
|
+
argv: dispatch.tokens,
|
|
257
|
+
...(profile.profile !== undefined ? { profile: profile.profile } : {}),
|
|
258
|
+
literal: true,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
it.each(profileForms.flatMap(profile => dispatchForms.map(dispatch => ({ profile, dispatch }))))('gives completion priority across $profile.name/$dispatch.name', ({ profile, dispatch }) => {
|
|
262
|
+
const argv = [...profile.tokens, ...dispatch.tokens, '--get-completions'];
|
|
263
|
+
expect(parseHostedRootCommandSurface(argv)).toEqual({ kind: 'completion', argv });
|
|
264
|
+
});
|
|
265
|
+
it.each([
|
|
266
|
+
{ name: 'plain', argv: ['--get-completions'] },
|
|
267
|
+
{ name: 'beats help', argv: ['--help', '--get-completions'] },
|
|
268
|
+
{ name: 'beats unknown', argv: ['--unknown', '--get-completions'] },
|
|
269
|
+
{ name: 'beats missing profile', argv: ['--profile', '--get-completions'] },
|
|
270
|
+
{ name: 'after separator', argv: ['--', '--get-completions'] },
|
|
271
|
+
{ name: 'after site tokens', argv: ['github', 'whoami', '--get-completions'] },
|
|
272
|
+
{ name: 'beats non-fast clustered version', argv: ['-Vx', '--get-completions'] },
|
|
273
|
+
])('matches the local main completion fast-path: $name', ({ argv }) => {
|
|
274
|
+
expect(parseHostedRootCommandSurface(argv)).toEqual({ kind: 'completion', argv });
|
|
275
|
+
});
|
|
276
|
+
it('keeps the exact first-token version fast-path ahead of completion', () => {
|
|
277
|
+
expect(parseHostedRootCommandSurface(['-V', '--get-completions'])).toEqual({
|
|
278
|
+
kind: 'version',
|
|
279
|
+
output: `${PKG_VERSION}\n`,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe('hosted root preflight call order', () => {
|
|
284
|
+
it.each([
|
|
285
|
+
{ name: 'no args', argv: [], exitCode: 1, stdout: '', stderr: formatRootHelp(HOSTED_ROOT_HELP) },
|
|
286
|
+
{ name: 'profile only', argv: ['--profile', 'work'], exitCode: 1, stdout: '', stderr: formatRootHelp(HOSTED_ROOT_HELP) },
|
|
287
|
+
{ name: 'bare separator', argv: ['--'], exitCode: 1, stdout: '', stderr: formatRootHelp(HOSTED_ROOT_HELP) },
|
|
288
|
+
{ name: 'help', argv: ['--help'], exitCode: 0, stdout: formatRootHelp(HOSTED_ROOT_HELP), stderr: '' },
|
|
289
|
+
{ name: 'help after malformed prefix and site', argv: ['--unknown', 'github', '--help'], exitCode: 0, stdout: formatRootHelp(HOSTED_ROOT_HELP), stderr: '' },
|
|
290
|
+
{ name: 'profile consumes help into implicit help', argv: ['--profile', '--help'], exitCode: 1, stdout: '', stderr: formatRootHelp(HOSTED_ROOT_HELP) },
|
|
291
|
+
{ name: 'version', argv: ['-Vx'], exitCode: 0, stdout: `${PKG_VERSION}\n`, stderr: '' },
|
|
292
|
+
{ name: 'missing profile', argv: ['--profile'], exitCode: 1, stdout: '', stderr: "error: option '--profile <name>' argument missing\n" },
|
|
293
|
+
{ name: 'trailing missing profile beats help', argv: ['--help', 'github', '--profile'], exitCode: 1, stdout: '', stderr: "error: option '--profile <name>' argument missing\n" },
|
|
294
|
+
{ name: 'trailing missing profile beats unknown', argv: ['--unknown', 'github', '--profile'], exitCode: 1, stdout: '', stderr: "error: option '--profile <name>' argument missing\n" },
|
|
295
|
+
{ name: 'unknown option', argv: ['-xV'], exitCode: 1, stdout: '', stderr: "error: unknown option '-xV'\n" },
|
|
296
|
+
])('$name terminates before Cloud discovery', async ({ argv, exitCode, stdout: expectedStdout, stderr: expectedStderr }) => {
|
|
297
|
+
const stdout = sink();
|
|
298
|
+
const stderr = sink();
|
|
299
|
+
const fetchImpl = vi.fn();
|
|
300
|
+
const result = await runHostedCli(argv, {
|
|
301
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
302
|
+
stdout: stdout.stream,
|
|
303
|
+
stderr: stderr.stream,
|
|
304
|
+
fetchImpl,
|
|
305
|
+
});
|
|
306
|
+
expect(result).toEqual({ handled: true, exitCode });
|
|
307
|
+
expect(stdout.text()).toBe(expectedStdout);
|
|
308
|
+
expect(stderr.text()).toBe(expectedStderr);
|
|
309
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
it.each([
|
|
312
|
+
['--get-completions'],
|
|
313
|
+
['--help', '--get-completions'],
|
|
314
|
+
['--unknown', '--get-completions'],
|
|
315
|
+
['--profile', '--get-completions'],
|
|
316
|
+
['--', '--get-completions'],
|
|
317
|
+
['-Vx', '--get-completions'],
|
|
318
|
+
])('completion preflight performs exactly one manifest request and no execute: %j', async (...argv) => {
|
|
319
|
+
const stdout = sink();
|
|
320
|
+
const stderr = sink();
|
|
321
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
322
|
+
const result = await runHostedCli(argv, {
|
|
323
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
324
|
+
stdout: stdout.stream,
|
|
325
|
+
stderr: stderr.stream,
|
|
326
|
+
fetchImpl,
|
|
327
|
+
});
|
|
328
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
329
|
+
expect(stderr.text()).toBe('');
|
|
330
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
331
|
+
expect(String(fetchImpl.mock.calls[0][0])).toBe('https://api.example.com/v1/manifest');
|
|
332
|
+
});
|
|
333
|
+
it('dispatches a profiled site only after the root preflight', async () => {
|
|
334
|
+
const calls = [];
|
|
335
|
+
const result = await runHostedCli(['--profile', 'work', 'github', 'whoami'], {
|
|
336
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
337
|
+
stdout: sink().stream,
|
|
338
|
+
stderr: sink().stream,
|
|
339
|
+
fetchImpl: async (url, init) => {
|
|
340
|
+
calls.push({
|
|
341
|
+
url: String(url),
|
|
342
|
+
...(init?.body ? { body: JSON.parse(String(init.body)) } : {}),
|
|
343
|
+
});
|
|
344
|
+
return String(url).endsWith('/v1/manifest') ? manifestResponse() : executionResponse();
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
348
|
+
expect(calls.map(call => call.url)).toEqual([
|
|
349
|
+
'https://api.example.com/v1/manifest',
|
|
350
|
+
'https://api.example.com/v1/execute',
|
|
351
|
+
]);
|
|
352
|
+
expect(calls[1]?.body?.profile).toBe('work');
|
|
353
|
+
});
|
|
354
|
+
it.each([
|
|
355
|
+
{ argv: ['missing', '--version'], label: 'long version' },
|
|
356
|
+
{ argv: ['missing', '-V'], label: 'short version' },
|
|
357
|
+
])('matches local trailing $label handling for an unknown site', async ({ argv }) => {
|
|
358
|
+
const local = await runActualLocalRoot(argv);
|
|
359
|
+
const stdout = sink();
|
|
360
|
+
const stderr = sink();
|
|
361
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
362
|
+
const hosted = await runHostedCli(argv, {
|
|
363
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
364
|
+
stdout: stdout.stream,
|
|
365
|
+
stderr: stderr.stream,
|
|
366
|
+
fetchImpl,
|
|
367
|
+
});
|
|
368
|
+
expect(local).toMatchObject({ exitCode: 0, stdout: `${PKG_VERSION}\n`, stderr: '' });
|
|
369
|
+
expect(hosted).toEqual({ handled: true, exitCode: 0 });
|
|
370
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
371
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
372
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
373
|
+
});
|
|
374
|
+
it('preserves the literal separator for an unknown site help-shaped token', async () => {
|
|
375
|
+
const stdout = sink();
|
|
376
|
+
const stderr = sink();
|
|
377
|
+
const result = await runHostedCli(['--', 'missing', '--help'], {
|
|
378
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
379
|
+
stdout: stdout.stream,
|
|
380
|
+
stderr: stderr.stream,
|
|
381
|
+
fetchImpl: async () => manifestResponse(),
|
|
382
|
+
});
|
|
383
|
+
expect(result).toEqual({ handled: true, exitCode: 2 });
|
|
384
|
+
expect(stderr.text()).toBe("error: unknown command 'missing'\n");
|
|
385
|
+
expect(stdout.text()).toBe(formatRootHelp(HOSTED_ROOT_HELP));
|
|
386
|
+
});
|
|
387
|
+
it('preserves the literal separator when list receives a help-shaped excess argument', async () => {
|
|
388
|
+
const local = await runActualLocalRoot(['--', 'list', '--help']);
|
|
389
|
+
const stdout = sink();
|
|
390
|
+
const stderr = sink();
|
|
391
|
+
const hosted = await runHostedCli(['--', 'list', '--help'], {
|
|
392
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
393
|
+
stdout: stdout.stream,
|
|
394
|
+
stderr: stderr.stream,
|
|
395
|
+
fetchImpl: async () => manifestResponse(),
|
|
396
|
+
});
|
|
397
|
+
expect(local).toMatchObject({
|
|
398
|
+
exitCode: 1,
|
|
399
|
+
stdout: '',
|
|
400
|
+
stderr: "error: too many arguments for 'list'. Expected 0 arguments but got 1.\n",
|
|
401
|
+
errorCode: 'commander.excessArguments',
|
|
402
|
+
});
|
|
403
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
404
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
405
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
406
|
+
});
|
|
407
|
+
it.each([
|
|
408
|
+
{ name: 'help', argv: ['list', '--help'] },
|
|
409
|
+
{ name: 'unknown option', argv: ['list', '--unknown'] },
|
|
410
|
+
{ name: 'missing format', argv: ['list', '--format'] },
|
|
411
|
+
{ name: 'excess argument', argv: ['list', 'extra'] },
|
|
412
|
+
])('matches local list structural grammar: $name', async ({ argv }) => {
|
|
413
|
+
const local = await runActualLocalRoot(argv);
|
|
414
|
+
const stdout = sink();
|
|
415
|
+
const stderr = sink();
|
|
416
|
+
const hosted = await runHostedCli(argv, {
|
|
417
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
418
|
+
stdout: stdout.stream,
|
|
419
|
+
stderr: stderr.stream,
|
|
420
|
+
fetchImpl: async () => manifestResponse(),
|
|
421
|
+
});
|
|
422
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
423
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
424
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
425
|
+
});
|
|
426
|
+
it('matches the local missing completion shell error before Cloud discovery', async () => {
|
|
427
|
+
const local = await runActualLocalRoot(['completion']);
|
|
428
|
+
const stdout = sink();
|
|
429
|
+
const stderr = sink();
|
|
430
|
+
const fetchImpl = vi.fn();
|
|
431
|
+
const hosted = await runHostedCli(['completion'], {
|
|
432
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
433
|
+
stdout: stdout.stream,
|
|
434
|
+
stderr: stderr.stream,
|
|
435
|
+
fetchImpl,
|
|
436
|
+
});
|
|
437
|
+
expect(local).toMatchObject({
|
|
438
|
+
exitCode: 1,
|
|
439
|
+
stdout: '',
|
|
440
|
+
stderr: "error: missing required argument 'shell'\n",
|
|
441
|
+
errorCode: 'commander.missingArgument',
|
|
442
|
+
});
|
|
443
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
444
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
445
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
446
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
it.each([
|
|
449
|
+
{ name: 'unknown child help', argv: ['github', 'bogus', '--help'] },
|
|
450
|
+
{ name: 'unknown option then help', argv: ['github', '--unknown', '--help'] },
|
|
451
|
+
{ name: 'structured format then help', argv: ['github', '-f', 'yaml', '--help'] },
|
|
452
|
+
{ name: 'complete profile then help', argv: ['github', '--profile', 'x', '--help'] },
|
|
453
|
+
])('matches local known-site namespace help precedence: $name', async ({ argv }) => {
|
|
454
|
+
const local = await runActualLocalKnownSite(argv);
|
|
455
|
+
const stdout = sink();
|
|
456
|
+
const stderr = sink();
|
|
457
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
458
|
+
const hosted = await runHostedCli(argv, {
|
|
459
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
460
|
+
stdout: stdout.stream,
|
|
461
|
+
stderr: stderr.stream,
|
|
462
|
+
fetchImpl,
|
|
463
|
+
});
|
|
464
|
+
expect(local).toMatchObject({ exitCode: 0, stderr: '', errorCode: 'commander.helpDisplayed' });
|
|
465
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
466
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
467
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
468
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
469
|
+
expect(String(fetchImpl.mock.calls[0][0])).toBe('https://api.example.com/v1/manifest');
|
|
470
|
+
});
|
|
471
|
+
it('keeps known leaf help authoritative over site help', async () => {
|
|
472
|
+
const local = await runActualLocalKnownSite(['github', 'whoami', '--help']);
|
|
473
|
+
const stdout = sink();
|
|
474
|
+
const stderr = sink();
|
|
475
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
476
|
+
const hosted = await runHostedCli(['github', 'whoami', '--help'], {
|
|
477
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
478
|
+
stdout: stdout.stream,
|
|
479
|
+
stderr: stderr.stream,
|
|
480
|
+
fetchImpl,
|
|
481
|
+
});
|
|
482
|
+
expect(local).toMatchObject({ exitCode: 0, stderr: '', errorCode: 'commander.helpDisplayed' });
|
|
483
|
+
expect(hosted).toEqual({ handled: true, exitCode: 0 });
|
|
484
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
485
|
+
expect(stderr.text()).toBe('');
|
|
486
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
487
|
+
});
|
|
488
|
+
it.each([
|
|
489
|
+
{ name: 'help', argv: ['completion', '--help'] },
|
|
490
|
+
{ name: 'leaf version is unknown', argv: ['completion', '-V'] },
|
|
491
|
+
{ name: 'excess shell', argv: ['completion', 'bash', 'extra'] },
|
|
492
|
+
{ name: 'literal missing shell', argv: ['--', 'completion'] },
|
|
493
|
+
])('matches local completion structural grammar: $name', async ({ name, argv }) => {
|
|
494
|
+
const local = await runActualLocalRoot(argv);
|
|
495
|
+
const stdout = sink();
|
|
496
|
+
const stderr = sink();
|
|
497
|
+
const fetchImpl = vi.fn();
|
|
498
|
+
const hosted = await runHostedCli(argv, {
|
|
499
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
500
|
+
stdout: stdout.stream,
|
|
501
|
+
stderr: stderr.stream,
|
|
502
|
+
fetchImpl,
|
|
503
|
+
});
|
|
504
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
505
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
506
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
507
|
+
if (name === 'help')
|
|
508
|
+
expect(local.stdout).toHaveLength(181);
|
|
509
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
510
|
+
});
|
|
511
|
+
it.each([
|
|
512
|
+
{ name: 'unsupported shell', argv: ['completion', 'powershell'], shell: 'powershell' },
|
|
513
|
+
{ name: 'literal help-shaped shell', argv: ['--', 'completion', '--help'], shell: '--help' },
|
|
514
|
+
{ name: 'leaf separator help-shaped shell', argv: ['completion', '--', '--help'], shell: '--help' },
|
|
515
|
+
])('matches local completion runtime validation: $name', async ({ argv, shell }) => {
|
|
516
|
+
const local = await runActualLocalRoot(argv);
|
|
517
|
+
const stdout = sink();
|
|
518
|
+
const stderr = sink();
|
|
519
|
+
const fetchImpl = vi.fn();
|
|
520
|
+
const hosted = runHostedCli(argv, {
|
|
521
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
522
|
+
stdout: stdout.stream,
|
|
523
|
+
stderr: stderr.stream,
|
|
524
|
+
fetchImpl,
|
|
525
|
+
});
|
|
526
|
+
expect(local).toMatchObject({
|
|
527
|
+
exitCode: 1,
|
|
528
|
+
stdout: '',
|
|
529
|
+
stderr: '',
|
|
530
|
+
errorCode: 'UNSUPPORTED_SHELL',
|
|
531
|
+
errorMessage: `Unsupported shell: ${shell}. Supported: bash, zsh, fish`,
|
|
532
|
+
});
|
|
533
|
+
await expect(hosted).rejects.toMatchObject({
|
|
534
|
+
code: local.errorCode,
|
|
535
|
+
message: local.errorMessage,
|
|
536
|
+
exitCode: local.exitCode,
|
|
537
|
+
});
|
|
538
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
539
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
540
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
541
|
+
});
|
|
542
|
+
it('matches local literal completion success bytes without Cloud discovery', async () => {
|
|
543
|
+
const local = await runActualLocalRoot(['--', 'completion', 'bash']);
|
|
544
|
+
const stdout = sink();
|
|
545
|
+
const stderr = sink();
|
|
546
|
+
const fetchImpl = vi.fn();
|
|
547
|
+
const hosted = await runHostedCli(['--', 'completion', 'bash'], {
|
|
548
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
549
|
+
stdout: stdout.stream,
|
|
550
|
+
stderr: stderr.stream,
|
|
551
|
+
fetchImpl,
|
|
552
|
+
});
|
|
553
|
+
expect(local).toMatchObject({ exitCode: 0, stderr: '' });
|
|
554
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
555
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
556
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
557
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
558
|
+
});
|
|
559
|
+
it.each([
|
|
560
|
+
{ name: 'version before missing profile', argv: ['missing', '--version', '--profile'], disposition: 'version' },
|
|
561
|
+
{ name: 'profile consumes version', argv: ['missing', '--profile', '--version'], disposition: 'unknown' },
|
|
562
|
+
{ name: 'help before missing profile', argv: ['missing', '--help', '--profile'], disposition: 'missing-profile' },
|
|
563
|
+
{ name: 'profile then help', argv: ['missing', '--profile', 'x', '--help'], disposition: 'help' },
|
|
564
|
+
{ name: 'help then profile', argv: ['missing', '--help', '--profile', 'x'], disposition: 'help' },
|
|
565
|
+
{ name: 'help then version', argv: ['missing', '--help', '--version'], disposition: 'version' },
|
|
566
|
+
{ name: 'version then help', argv: ['missing', '--version', '--help'], disposition: 'version' },
|
|
567
|
+
])('matches local unknown-site option precedence: $name', async ({ argv, disposition }) => {
|
|
568
|
+
const local = await runActualLocalRoot(argv);
|
|
569
|
+
const stdout = sink();
|
|
570
|
+
const stderr = sink();
|
|
571
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
572
|
+
const hosted = await runHostedCli(argv, {
|
|
573
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
574
|
+
stdout: stdout.stream,
|
|
575
|
+
stderr: stderr.stream,
|
|
576
|
+
fetchImpl,
|
|
577
|
+
});
|
|
578
|
+
expect(hosted.exitCode).toBe(local.exitCode);
|
|
579
|
+
if (disposition === 'version') {
|
|
580
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
581
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
582
|
+
}
|
|
583
|
+
else if (disposition === 'missing-profile') {
|
|
584
|
+
expect(stdout.text()).toBe('');
|
|
585
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
586
|
+
}
|
|
587
|
+
else if (disposition === 'help') {
|
|
588
|
+
expect(stdout.text()).toBe(formatRootHelp(HOSTED_ROOT_HELP));
|
|
589
|
+
expect(stderr.text()).toBe('');
|
|
590
|
+
expect(local.stdout).not.toBe('');
|
|
591
|
+
expect(local.stderr).toBe('');
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
expect(stdout.text()).toBe(formatRootHelp(HOSTED_ROOT_HELP));
|
|
595
|
+
expect(stderr.text()).toBe("error: unknown command 'missing'\n");
|
|
596
|
+
expect(local.stdout).not.toBe('');
|
|
597
|
+
expect(local.stderr).toBe("error: unknown command 'missing'\n");
|
|
598
|
+
}
|
|
599
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
600
|
+
expect(String(fetchImpl.mock.calls[0][0])).toBe('https://api.example.com/v1/manifest');
|
|
601
|
+
});
|
|
602
|
+
it.each([
|
|
603
|
+
{ name: 'long help', argv: ['github', 'bogus', '--', '--help'] },
|
|
604
|
+
{ name: 'short help', argv: ['github', 'bogus', '--', '-h'] },
|
|
605
|
+
])('stops known-site terminal scanning at command-local separator: $name', async ({ argv }) => {
|
|
606
|
+
const local = await runActualLocalKnownSite(argv);
|
|
607
|
+
const stdout = sink();
|
|
608
|
+
const stderr = sink();
|
|
609
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
610
|
+
const hosted = await runHostedCli(argv, {
|
|
611
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
612
|
+
stdout: stdout.stream,
|
|
613
|
+
stderr: stderr.stream,
|
|
614
|
+
fetchImpl,
|
|
615
|
+
});
|
|
616
|
+
expect(local).toMatchObject({ exitCode: 1, stdout: '', stderr: "error: unknown command 'bogus'\n" });
|
|
617
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
618
|
+
expect(stdout.text()).toBe(local.stdout);
|
|
619
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
620
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
621
|
+
expect(String(fetchImpl.mock.calls[0][0])).toBe('https://api.example.com/v1/manifest');
|
|
622
|
+
});
|
|
623
|
+
it.each([
|
|
624
|
+
{ name: 'version', argv: ['missing', '--', '--version'] },
|
|
625
|
+
{ name: 'long help', argv: ['missing', '--', '--help'] },
|
|
626
|
+
{ name: 'short help', argv: ['missing', '--', '-h'] },
|
|
627
|
+
])('stops unknown-site terminal scanning at command-local separator: $name', async ({ argv }) => {
|
|
628
|
+
const local = await runActualLocalRoot(argv);
|
|
629
|
+
const stdout = sink();
|
|
630
|
+
const stderr = sink();
|
|
631
|
+
const fetchImpl = vi.fn(async () => manifestResponse());
|
|
632
|
+
const hosted = await runHostedCli(argv, {
|
|
633
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
634
|
+
stdout: stdout.stream,
|
|
635
|
+
stderr: stderr.stream,
|
|
636
|
+
fetchImpl,
|
|
637
|
+
});
|
|
638
|
+
expect(local).toMatchObject({ exitCode: 2, stderr: "error: unknown command 'missing'\n" });
|
|
639
|
+
expect(local.stdout).not.toBe('');
|
|
640
|
+
expect(hosted).toEqual({ handled: true, exitCode: local.exitCode });
|
|
641
|
+
expect(stdout.text()).toBe(formatRootHelp(HOSTED_ROOT_HELP));
|
|
642
|
+
expect(stderr.text()).toBe(local.stderr);
|
|
643
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
644
|
+
expect(String(fetchImpl.mock.calls[0][0])).toBe('https://api.example.com/v1/manifest');
|
|
645
|
+
});
|
|
646
|
+
});
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { type WebcmdConfig } from './config.js';
|
|
2
|
+
import { type HostedCredentialStore } from './credentials.js';
|
|
2
3
|
export interface HostedRunnerOptions {
|
|
3
4
|
config?: WebcmdConfig;
|
|
5
|
+
credentialStore?: HostedCredentialStore;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
homeDir?: string;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
randomUUID?: () => string;
|
|
4
10
|
fetchImpl?: typeof fetch;
|
|
5
11
|
stdout?: NodeJS.WritableStream;
|
|
6
12
|
stderr?: NodeJS.WritableStream;
|
|
13
|
+
now?: () => number;
|
|
7
14
|
}
|
|
8
15
|
export interface HostedRunResult {
|
|
9
16
|
handled: boolean;
|