@agentrhq/webcmd 0.2.5 → 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/_shared/site-auth.js +3 -3
- package/clis/_shared/site-auth.test.js +4 -4
- 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/slock/whoami.test.js +2 -2
- 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/daemon-client.d.ts +2 -1
- package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
- 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/provider.test.js +36 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +10 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.js +25 -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 +63 -110
- package/dist/src/cli.test.js +82 -6
- 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/commands/auth.js +3 -2
- package/dist/src/commands/auth.test.js +6 -6
- 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 +11 -1
- 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 +4 -0
- package/dist/src/hosted/args.js +4 -0
- package/dist/src/hosted/args.test.d.ts +1 -0
- package/dist/src/hosted/args.test.js +49 -0
- 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 +59 -0
- package/dist/src/hosted/client.js +510 -0
- package/dist/src/hosted/client.test.d.ts +1 -0
- package/dist/src/hosted/client.test.js +738 -0
- package/dist/src/hosted/config.d.ts +55 -0
- package/dist/src/hosted/config.js +112 -0
- package/dist/src/hosted/config.test.d.ts +1 -0
- package/dist/src/hosted/config.test.js +81 -0
- 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 +14 -0
- package/dist/src/hosted/manifest.js +47 -0
- package/dist/src/hosted/manifest.test.d.ts +1 -0
- package/dist/src/hosted/manifest.test.js +164 -0
- 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 +19 -0
- package/dist/src/hosted/runner.js +705 -0
- package/dist/src/hosted/runner.test.d.ts +1 -0
- package/dist/src/hosted/runner.test.js +1607 -0
- package/dist/src/hosted/setup.d.ts +10 -0
- package/dist/src/hosted/setup.js +79 -0
- package/dist/src/hosted/setup.test.d.ts +1 -0
- package/dist/src/hosted/setup.test.js +195 -0
- package/dist/src/hosted/types.d.ts +175 -0
- package/dist/src/hosted/types.js +1 -0
- package/dist/src/main.js +123 -97
- 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.d.ts +6 -1
- package/dist/src/release-notes.js +192 -4
- package/dist/src/release-notes.test.js +143 -4
- 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/generate-release-notes.ts +1 -1
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/smart-search/SKILL.md +14 -3
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +21 -7
- package/clis/antigravity/SKILL.md +0 -38
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ArgumentError } from './errors.js';
|
|
3
|
+
export const OUTPUT_FORMATS = ['table', 'plain', 'json', 'yaml', 'yml', 'md', 'markdown', 'csv'];
|
|
4
|
+
export const TRACE_MODES = ['off', 'on', 'retain-on-failure'];
|
|
5
|
+
const BROWSER_WINDOW_MODES = ['foreground', 'background'];
|
|
6
|
+
const SITE_SESSION_MODES = ['ephemeral', 'persistent'];
|
|
7
|
+
/** Identifies the one parser failure whose public bytes are owned by Commander. */
|
|
8
|
+
export class MissingRequiredPositionalError extends ArgumentError {
|
|
9
|
+
argumentName;
|
|
10
|
+
constructor(argumentName, help) {
|
|
11
|
+
super(`Argument "${argumentName}" is required.`, help);
|
|
12
|
+
this.argumentName = argumentName;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Raw structural failure bytes/status owned by Commander. */
|
|
16
|
+
export class CommanderStructuralError extends Error {
|
|
17
|
+
output;
|
|
18
|
+
exitCode;
|
|
19
|
+
constructor(output, exitCode) {
|
|
20
|
+
super(output.trimEnd());
|
|
21
|
+
this.output = output;
|
|
22
|
+
this.exitCode = exitCode;
|
|
23
|
+
this.name = 'CommanderStructuralError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Register the adapter argument grammar and its shared execution options. */
|
|
27
|
+
export function configureCommandSurface(command, metadata) {
|
|
28
|
+
for (const arg of metadata.args) {
|
|
29
|
+
if (arg.positional) {
|
|
30
|
+
const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
31
|
+
command.argument(bracket, arg.help ?? '');
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const expectsValue = arg.required || arg.valueRequired;
|
|
35
|
+
const flag = expectsValue ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
36
|
+
if (arg.required)
|
|
37
|
+
command.requiredOption(flag, arg.help ?? '');
|
|
38
|
+
else if (arg.default != null)
|
|
39
|
+
command.option(flag, arg.help ?? '', String(arg.default));
|
|
40
|
+
else
|
|
41
|
+
command.option(flag, arg.help ?? '');
|
|
42
|
+
}
|
|
43
|
+
command
|
|
44
|
+
.option('-f, --format <fmt>', `Output format: ${OUTPUT_FORMATS.join(', ')}`, 'table')
|
|
45
|
+
.option('--trace <mode>', `Trace capture: ${TRACE_MODES.join(', ')}`, 'off')
|
|
46
|
+
.option('-v, --verbose', 'Debug output', false);
|
|
47
|
+
if (metadata.browser) {
|
|
48
|
+
command
|
|
49
|
+
.option('--window <mode>', `Browser window mode: ${BROWSER_WINDOW_MODES.join(' or ')}`)
|
|
50
|
+
.option('--site-session <mode>', `Adapter site session lifecycle: ${SITE_SESSION_MODES.join(' or ')}`)
|
|
51
|
+
.option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Parse one adapter invocation without requiring a local Commander program. */
|
|
55
|
+
export function parseCommandSurface(metadata, argv) {
|
|
56
|
+
const positionals = metadata.args.filter((arg) => arg.positional);
|
|
57
|
+
const defaultFormat = metadata.defaultFormat || 'table';
|
|
58
|
+
const input = {};
|
|
59
|
+
const optionSources = {};
|
|
60
|
+
const { root, command, parseArgv } = makeStructuralCommand(metadata, argv);
|
|
61
|
+
let parsedOptions = {};
|
|
62
|
+
let actionRan = false;
|
|
63
|
+
let stderr = '';
|
|
64
|
+
const commanderOutput = {
|
|
65
|
+
writeErr: (value) => { stderr += value; },
|
|
66
|
+
// Hosted mode owns help presentation; Commander is used only for its
|
|
67
|
+
// grammar, precedence, exact structural errors, and exit status.
|
|
68
|
+
writeOut: (_value) => undefined,
|
|
69
|
+
};
|
|
70
|
+
for (let current = command; current; current = current.parent) {
|
|
71
|
+
current.exitOverride().configureOutput(commanderOutput);
|
|
72
|
+
}
|
|
73
|
+
command.action((...actionArgs) => {
|
|
74
|
+
actionRan = true;
|
|
75
|
+
parsedOptions = actionArgs[positionals.length];
|
|
76
|
+
for (let index = 0; index < positionals.length; index += 1) {
|
|
77
|
+
const value = actionArgs[index];
|
|
78
|
+
if (value !== undefined) {
|
|
79
|
+
input[positionals[index].name] = value;
|
|
80
|
+
optionSources[positionals[index].name] = 'cli';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const definition of metadata.args) {
|
|
84
|
+
if (definition.positional)
|
|
85
|
+
continue;
|
|
86
|
+
const camelName = definition.name.replace(/-([a-z])/g, (_match, character) => character.toUpperCase());
|
|
87
|
+
const value = parsedOptions[definition.name] ?? parsedOptions[camelName];
|
|
88
|
+
if (value !== undefined)
|
|
89
|
+
input[definition.name] = value;
|
|
90
|
+
const source = command.getOptionValueSource(camelName) ?? command.getOptionValueSource(definition.name);
|
|
91
|
+
if (source === 'cli' || source === 'default') {
|
|
92
|
+
optionSources[definition.name] = source;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
root.parse(parseArgv, { from: 'user' });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const commander = error;
|
|
101
|
+
if (commander.code === 'commander.helpDisplayed') {
|
|
102
|
+
return {
|
|
103
|
+
args: {},
|
|
104
|
+
optionSources: {},
|
|
105
|
+
format: parseOutputFormat(defaultFormat),
|
|
106
|
+
formatExplicit: false,
|
|
107
|
+
trace: 'off',
|
|
108
|
+
verbose: false,
|
|
109
|
+
help: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const output = stderr || `${typeof commander.message === 'string' ? commander.message : String(error)}\n`;
|
|
113
|
+
throw new CommanderStructuralError(output, typeof commander.exitCode === 'number' ? commander.exitCode : 1);
|
|
114
|
+
}
|
|
115
|
+
if (!actionRan) {
|
|
116
|
+
throw new CommanderStructuralError(`error: command '${command.name()}' did not run\n`, 1);
|
|
117
|
+
}
|
|
118
|
+
// Match the local action boundary: adapter argument coercion occurs before
|
|
119
|
+
// format validation, and trace validation occurs inside executeCommand after
|
|
120
|
+
// both. Commander has already enforced required positionals/options.
|
|
121
|
+
const args = coerceCommandArguments(metadata.args, input);
|
|
122
|
+
const formatExplicit = command.getOptionValueSource('format') === 'cli';
|
|
123
|
+
const format = parseOutputFormat(formatExplicit ? parsedOptions.format : defaultFormat);
|
|
124
|
+
const trace = parseTraceMode(parsedOptions.trace ?? 'off');
|
|
125
|
+
const verbose = parsedOptions.verbose === true;
|
|
126
|
+
return {
|
|
127
|
+
args,
|
|
128
|
+
optionSources,
|
|
129
|
+
format,
|
|
130
|
+
formatExplicit,
|
|
131
|
+
trace,
|
|
132
|
+
verbose,
|
|
133
|
+
help: false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function makeStructuralCommand(metadata, argv) {
|
|
137
|
+
const pathParts = (metadata.command ?? '').split('/').filter(Boolean);
|
|
138
|
+
const site = metadata.site ?? (pathParts.length > 1 ? pathParts[0] : undefined);
|
|
139
|
+
const name = metadata.name ?? pathParts.at(-1) ?? 'command';
|
|
140
|
+
if (!site) {
|
|
141
|
+
const command = new Command(name);
|
|
142
|
+
configureCommandSurface(command, metadata);
|
|
143
|
+
return { root: command, command, parseArgv: [...argv] };
|
|
144
|
+
}
|
|
145
|
+
const root = new Command('webcmd');
|
|
146
|
+
const siteCommand = root.command(site);
|
|
147
|
+
const command = siteCommand.command(name);
|
|
148
|
+
configureCommandSurface(command, metadata);
|
|
149
|
+
return { root, command, parseArgv: [site, name, ...argv] };
|
|
150
|
+
}
|
|
151
|
+
/** Apply the adapter's required/default/type/choice contract to raw values. */
|
|
152
|
+
export function coerceCommandArguments(definitions, input) {
|
|
153
|
+
const result = { ...input };
|
|
154
|
+
for (const definition of definitions) {
|
|
155
|
+
const value = result[definition.name];
|
|
156
|
+
if (definition.required && (value === undefined || value === null || value === '')) {
|
|
157
|
+
throw new ArgumentError(`Argument "${definition.name}" is required.`, definition.help ?? `Provide a value for --${definition.name}`);
|
|
158
|
+
}
|
|
159
|
+
if (value !== undefined && value !== null) {
|
|
160
|
+
if (definition.type === 'int' || definition.type === 'number') {
|
|
161
|
+
const parsed = Number(value);
|
|
162
|
+
if (Number.isNaN(parsed)) {
|
|
163
|
+
throw new ArgumentError(`Argument "${definition.name}" must be a valid number. Received: "${String(value)}"`);
|
|
164
|
+
}
|
|
165
|
+
result[definition.name] = parsed;
|
|
166
|
+
}
|
|
167
|
+
else if (definition.type === 'boolean' || definition.type === 'bool') {
|
|
168
|
+
if (typeof value === 'string') {
|
|
169
|
+
const normalized = value.toLowerCase();
|
|
170
|
+
if (normalized === 'true' || normalized === '1')
|
|
171
|
+
result[definition.name] = true;
|
|
172
|
+
else if (normalized === 'false' || normalized === '0')
|
|
173
|
+
result[definition.name] = false;
|
|
174
|
+
else {
|
|
175
|
+
throw new ArgumentError(`Argument "${definition.name}" must be a boolean (true/false). Received: "${String(value)}"`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
result[definition.name] = Boolean(value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const coercedValue = result[definition.name];
|
|
183
|
+
if (definition.choices && definition.choices.length > 0
|
|
184
|
+
&& !definition.choices.map(String).includes(String(coercedValue))) {
|
|
185
|
+
throw new ArgumentError(`Argument "${definition.name}" must be one of: ${definition.choices.join(', ')}. Received: "${String(coercedValue)}"`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (definition.default !== undefined) {
|
|
189
|
+
// Preserve the historical local contract: defaults are adapter-owned
|
|
190
|
+
// values and are not re-coerced or revalidated by the CLI boundary.
|
|
191
|
+
result[definition.name] = definition.default;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
export function parseOutputFormat(value) {
|
|
197
|
+
// Preserve the long-standing local behavior: unknown format names flow to
|
|
198
|
+
// output.ts, whose default switch branch renders a table.
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
function parseTraceMode(value) {
|
|
202
|
+
if (TRACE_MODES.includes(value))
|
|
203
|
+
return value;
|
|
204
|
+
throw new ArgumentError(`--trace must be one of: ${TRACE_MODES.join(', ')}. Received: "${String(value)}"`);
|
|
205
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { CommanderStructuralError, coerceCommandArguments, configureCommandSurface, parseCommandSurface, parseOutputFormat, } from './command-surface.js';
|
|
4
|
+
const metadata = {
|
|
5
|
+
command: 'demo/search',
|
|
6
|
+
browser: false,
|
|
7
|
+
defaultFormat: 'plain',
|
|
8
|
+
args: [
|
|
9
|
+
{ name: 'query', positional: true, required: true, type: 'string' },
|
|
10
|
+
{ name: 'scope', positional: true, type: 'string' },
|
|
11
|
+
{ name: 'limit', type: 'int', default: 10 },
|
|
12
|
+
{ name: 'ratio', type: 'number' },
|
|
13
|
+
{ name: 'mode', type: 'string', choices: ['open', 'closed'] },
|
|
14
|
+
{ name: 'enabled', type: 'boolean', default: false },
|
|
15
|
+
{ name: 'label', type: 'string', valueRequired: true },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
describe('parseCommandSurface', () => {
|
|
19
|
+
it.each([
|
|
20
|
+
{
|
|
21
|
+
name: 'required and optional positionals',
|
|
22
|
+
argv: ['needle', 'issues'],
|
|
23
|
+
args: { query: 'needle', scope: 'issues', limit: 10, enabled: false },
|
|
24
|
+
sources: { query: 'cli', scope: 'cli', limit: 'default', enabled: 'default' },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'a leading-dash positional after the option separator',
|
|
28
|
+
argv: ['--', '-opaque-id'],
|
|
29
|
+
args: { query: '-opaque-id', limit: 10, enabled: false },
|
|
30
|
+
sources: { query: 'cli', limit: 'default', enabled: 'default' },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'separate and inline named option values',
|
|
34
|
+
argv: ['needle', '--label', 'triage', '--limit=12'],
|
|
35
|
+
args: { query: 'needle', label: 'triage', limit: 12, enabled: false },
|
|
36
|
+
sources: { query: 'cli', label: 'cli', limit: 'cli', enabled: 'default' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'a required option value beginning with a dash',
|
|
40
|
+
argv: ['needle', '--label', '-opaque'],
|
|
41
|
+
args: { query: 'needle', label: '-opaque', limit: 10, enabled: false },
|
|
42
|
+
sources: { query: 'cli', label: 'cli', limit: 'default', enabled: 'default' },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'a valueless boolean flag',
|
|
46
|
+
argv: ['needle', '--enabled'],
|
|
47
|
+
args: { query: 'needle', limit: 10, enabled: true },
|
|
48
|
+
sources: { query: 'cli', limit: 'default', enabled: 'cli' },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'an explicit separate boolean value',
|
|
52
|
+
argv: ['needle', '--enabled', 'false'],
|
|
53
|
+
args: { query: 'needle', limit: 10, enabled: false },
|
|
54
|
+
sources: { query: 'cli', limit: 'default', enabled: 'cli' },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'an explicit inline boolean value',
|
|
58
|
+
argv: ['needle', '--enabled=true'],
|
|
59
|
+
args: { query: 'needle', limit: 10, enabled: true },
|
|
60
|
+
sources: { query: 'cli', limit: 'default', enabled: 'cli' },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'a finite number and a valid choice',
|
|
64
|
+
argv: ['needle', '--ratio=-1.25', '--mode', 'closed'],
|
|
65
|
+
args: { query: 'needle', limit: 10, ratio: -1.25, mode: 'closed', enabled: false },
|
|
66
|
+
sources: { query: 'cli', limit: 'default', ratio: 'cli', mode: 'cli', enabled: 'default' },
|
|
67
|
+
},
|
|
68
|
+
])('parses $name', ({ argv, args, sources }) => {
|
|
69
|
+
expect(parseCommandSurface(metadata, argv)).toEqual({
|
|
70
|
+
args,
|
|
71
|
+
optionSources: sources,
|
|
72
|
+
format: 'plain',
|
|
73
|
+
formatExplicit: false,
|
|
74
|
+
trace: 'off',
|
|
75
|
+
verbose: false,
|
|
76
|
+
help: false,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it.each(['table', 'plain', 'json', 'yaml', 'yml', 'md', 'markdown', 'csv'])('accepts the %s output format', (format) => {
|
|
80
|
+
expect(parseCommandSurface(metadata, ['needle', `--format=${format}`])).toMatchObject({
|
|
81
|
+
format,
|
|
82
|
+
formatExplicit: true,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it.each(['off', 'on', 'retain-on-failure'])('accepts the %s trace mode', (trace) => {
|
|
86
|
+
expect(parseCommandSurface(metadata, ['needle', '--trace', trace])).toMatchObject({ trace });
|
|
87
|
+
});
|
|
88
|
+
it('lets Commander help terminate before action-level global values are observed', () => {
|
|
89
|
+
expect(parseCommandSurface(metadata, ['--verbose', '--help'])).toMatchObject({
|
|
90
|
+
verbose: false,
|
|
91
|
+
help: true,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it.each([
|
|
95
|
+
{ name: 'a missing required positional', argv: [], message: /missing required argument 'query'/i },
|
|
96
|
+
{ name: 'an extra positional', argv: ['needle', 'issues', 'extra'], message: /too many arguments.*search/i },
|
|
97
|
+
{ name: 'a missing required option value', argv: ['needle', '--label'], message: /--label.*argument missing/i },
|
|
98
|
+
{ name: 'an unknown flag', argv: ['needle', '--unknown'], message: /unknown option/i },
|
|
99
|
+
{ name: 'a partial integer', argv: ['needle', '--limit', '12x'], message: /limit.*number/i },
|
|
100
|
+
{ name: 'an invalid choice', argv: ['needle', '--mode', 'merged'], message: /mode.*one of.*open.*closed/i },
|
|
101
|
+
{ name: 'an invalid boolean', argv: ['needle', '--enabled=maybe'], message: /enabled.*boolean/i },
|
|
102
|
+
{ name: 'an invalid trace mode', argv: ['needle', '--trace', 'always'], message: /trace.*one of/i },
|
|
103
|
+
])('rejects $name', ({ argv, message }) => {
|
|
104
|
+
expect(() => parseCommandSurface(metadata, argv)).toThrow(message);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('coerceCommandArguments', () => {
|
|
108
|
+
it('applies typed defaults and preserves non-definition compatibility metadata', () => {
|
|
109
|
+
expect(coerceCommandArguments(metadata.args, {
|
|
110
|
+
query: 'needle',
|
|
111
|
+
__webcmdOptionSources: { limit: 'cli' },
|
|
112
|
+
})).toEqual({
|
|
113
|
+
query: 'needle',
|
|
114
|
+
limit: 10,
|
|
115
|
+
enabled: false,
|
|
116
|
+
__webcmdOptionSources: { limit: 'cli' },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it('preserves adapter-owned defaults without coercing them again', () => {
|
|
120
|
+
expect(coerceCommandArguments([
|
|
121
|
+
{ name: 'limit', type: 'int', default: '10' },
|
|
122
|
+
{ name: 'mode', choices: ['open', 'closed'], default: 'open' },
|
|
123
|
+
], {})).toEqual({ limit: '10', mode: 'open' });
|
|
124
|
+
});
|
|
125
|
+
it('preserves legacy numeric and unknown-format coercion', () => {
|
|
126
|
+
expect(coerceCommandArguments([
|
|
127
|
+
{ name: 'count', type: 'int' },
|
|
128
|
+
{ name: 'ratio', type: 'number' },
|
|
129
|
+
], { count: '1.5', ratio: 'Infinity' })).toEqual({ count: 1.5, ratio: Infinity });
|
|
130
|
+
expect(parseCommandSurface(metadata, ['needle', '--format', 'xml'])).toMatchObject({
|
|
131
|
+
format: 'xml',
|
|
132
|
+
formatExplicit: true,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('configureCommandSurface', () => {
|
|
137
|
+
it.each([
|
|
138
|
+
{ browser: false, expected: [] },
|
|
139
|
+
{ browser: true, expected: ['--window', '--site-session', '--keep-tab'] },
|
|
140
|
+
])('registers browser globals only when browser is $browser', ({ browser, expected }) => {
|
|
141
|
+
const command = new Command('search');
|
|
142
|
+
configureCommandSurface(command, { ...metadata, browser });
|
|
143
|
+
const longFlags = command.options.map((option) => option.long);
|
|
144
|
+
for (const flag of expected)
|
|
145
|
+
expect(longFlags).toContain(flag);
|
|
146
|
+
if (!browser) {
|
|
147
|
+
expect(longFlags).not.toContain('--window');
|
|
148
|
+
expect(longFlags).not.toContain('--site-session');
|
|
149
|
+
expect(longFlags).not.toContain('--keep-tab');
|
|
150
|
+
}
|
|
151
|
+
expect(longFlags).toEqual(expect.arrayContaining([
|
|
152
|
+
'--limit',
|
|
153
|
+
'--ratio',
|
|
154
|
+
'--mode',
|
|
155
|
+
'--enabled',
|
|
156
|
+
'--label',
|
|
157
|
+
'--format',
|
|
158
|
+
'--trace',
|
|
159
|
+
'--verbose',
|
|
160
|
+
]));
|
|
161
|
+
expect(command.registeredArguments.map((argument) => argument.name())).toEqual(['query', 'scope']);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
function classifyError(error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
if (/unknown option/i.test(message))
|
|
167
|
+
return { kind: 'unknown' };
|
|
168
|
+
if (/argument missing|requires a value/i.test(message))
|
|
169
|
+
return { kind: 'missing' };
|
|
170
|
+
return { kind: 'invalid' };
|
|
171
|
+
}
|
|
172
|
+
async function parseWithCommander(surface, argv) {
|
|
173
|
+
const command = new Command('parity')
|
|
174
|
+
.exitOverride()
|
|
175
|
+
.configureOutput({ writeErr: () => undefined, writeOut: () => undefined });
|
|
176
|
+
configureCommandSurface(command, surface);
|
|
177
|
+
const positionals = surface.args.filter((argument) => argument.positional);
|
|
178
|
+
let parsed;
|
|
179
|
+
command.action((...actionArgs) => {
|
|
180
|
+
const options = actionArgs[positionals.length];
|
|
181
|
+
const raw = {};
|
|
182
|
+
for (let index = 0; index < positionals.length; index += 1) {
|
|
183
|
+
if (actionArgs[index] !== undefined)
|
|
184
|
+
raw[positionals[index].name] = actionArgs[index];
|
|
185
|
+
}
|
|
186
|
+
for (const argument of surface.args) {
|
|
187
|
+
if (argument.positional)
|
|
188
|
+
continue;
|
|
189
|
+
const camelName = argument.name.replace(/-([a-z])/g, (_match, character) => character.toUpperCase());
|
|
190
|
+
const value = options[argument.name] ?? options[camelName];
|
|
191
|
+
if (value !== undefined)
|
|
192
|
+
raw[argument.name] = value;
|
|
193
|
+
}
|
|
194
|
+
parsed = coerceCommandArguments(surface.args, raw);
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
await command.parseAsync(argv, { from: 'user' });
|
|
198
|
+
return { kind: 'success', args: parsed ?? {} };
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
return classifyError(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function parseWithShared(surface, argv) {
|
|
205
|
+
try {
|
|
206
|
+
return { kind: 'success', args: parseCommandSurface(surface, argv).args };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return classifyError(error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
describe('Commander parity for dash-prefixed numeric tokens', () => {
|
|
213
|
+
it.each([
|
|
214
|
+
{
|
|
215
|
+
name: 'integer positional accepts a negative integer',
|
|
216
|
+
args: [{ name: 'count', positional: true, required: true, type: 'int' }],
|
|
217
|
+
argv: ['-1'],
|
|
218
|
+
kind: 'success',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'number positional accepts a negative decimal',
|
|
222
|
+
args: [{ name: 'ratio', positional: true, required: true, type: 'number' }],
|
|
223
|
+
argv: ['-1.25'],
|
|
224
|
+
kind: 'success',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'required string positional accepts a strict negative number operand',
|
|
228
|
+
args: [{ name: 'label', positional: true, required: true, type: 'string' }],
|
|
229
|
+
argv: ['-1'],
|
|
230
|
+
kind: 'success',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'optional string positional accepts a strict negative number operand',
|
|
234
|
+
args: [{ name: 'label', positional: true, type: 'string' }],
|
|
235
|
+
argv: ['-1'],
|
|
236
|
+
kind: 'success',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'untyped positional accepts a strict negative number operand',
|
|
240
|
+
args: [{ name: 'label', positional: true, required: true }],
|
|
241
|
+
argv: ['-1'],
|
|
242
|
+
kind: 'success',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'integer positional rejects a malformed numeric prefix as an unknown option',
|
|
246
|
+
args: [{ name: 'count', positional: true, required: true, type: 'int' }],
|
|
247
|
+
argv: ['-1x'],
|
|
248
|
+
kind: 'unknown',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'string positional rejects a malformed numeric prefix as an unknown option',
|
|
252
|
+
args: [{ name: 'label', positional: true, required: true, type: 'string' }],
|
|
253
|
+
argv: ['-1x'],
|
|
254
|
+
kind: 'unknown',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'option separator preserves a malformed dash token as a string positional',
|
|
258
|
+
args: [{ name: 'label', positional: true, required: true, type: 'string' }],
|
|
259
|
+
argv: ['--', '-1x'],
|
|
260
|
+
kind: 'success',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'required integer option consumes a valid negative value',
|
|
264
|
+
args: [{ name: 'count', type: 'int', valueRequired: true }],
|
|
265
|
+
argv: ['--count', '-1'],
|
|
266
|
+
kind: 'success',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'required integer option consumes then validates a malformed dash value',
|
|
270
|
+
args: [{ name: 'count', type: 'int', valueRequired: true }],
|
|
271
|
+
argv: ['--count', '-1x'],
|
|
272
|
+
kind: 'invalid',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'optional integer option consumes a valid negative value',
|
|
276
|
+
args: [{ name: 'count', type: 'int' }],
|
|
277
|
+
argv: ['--count', '-1'],
|
|
278
|
+
kind: 'success',
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'optional integer option leaves a malformed dash value as an unknown option',
|
|
282
|
+
args: [{ name: 'count', type: 'int' }],
|
|
283
|
+
argv: ['--count', '-1x'],
|
|
284
|
+
kind: 'unknown',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'required string option consumes a dash-prefixed value',
|
|
288
|
+
args: [{ name: 'label', type: 'string', valueRequired: true }],
|
|
289
|
+
argv: ['--label', '-opaque'],
|
|
290
|
+
kind: 'success',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'optional string option leaves a dash-prefixed token as an unknown option',
|
|
294
|
+
args: [{ name: 'label', type: 'string' }],
|
|
295
|
+
argv: ['--label', '-opaque'],
|
|
296
|
+
kind: 'unknown',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'required option without a following value reports a missing value',
|
|
300
|
+
args: [{ name: 'count', type: 'int', valueRequired: true }],
|
|
301
|
+
argv: ['--count'],
|
|
302
|
+
kind: 'missing',
|
|
303
|
+
},
|
|
304
|
+
])('$name', async ({ args, argv, kind }) => {
|
|
305
|
+
const surface = { args };
|
|
306
|
+
const commander = await parseWithCommander(surface, [...argv]);
|
|
307
|
+
const shared = parseWithShared(surface, [...argv]);
|
|
308
|
+
expect(commander.kind).toBe(kind);
|
|
309
|
+
expect(shared).toEqual(commander);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
const precedenceSurface = {
|
|
313
|
+
command: 'parity/check',
|
|
314
|
+
site: 'parity',
|
|
315
|
+
name: 'check',
|
|
316
|
+
args: [
|
|
317
|
+
{ name: 'account', positional: true, required: true, type: 'string' },
|
|
318
|
+
{ name: 'scope', positional: true, type: 'string' },
|
|
319
|
+
{ name: 'token', required: true, valueRequired: true, type: 'string' },
|
|
320
|
+
{ name: 'mode', type: 'string', choices: ['valid'] },
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
function captureReferenceSurface(argv) {
|
|
324
|
+
let stderr = '';
|
|
325
|
+
let outcome;
|
|
326
|
+
const root = new Command('webcmd')
|
|
327
|
+
.exitOverride()
|
|
328
|
+
.configureOutput({ writeErr: value => { stderr += value; }, writeOut: () => undefined });
|
|
329
|
+
const command = root.command(precedenceSurface.site).command(precedenceSurface.name);
|
|
330
|
+
configureCommandSurface(command, precedenceSurface);
|
|
331
|
+
const positionals = precedenceSurface.args.filter(argument => argument.positional);
|
|
332
|
+
command.action((...actionArgs) => {
|
|
333
|
+
try {
|
|
334
|
+
const options = actionArgs[positionals.length];
|
|
335
|
+
const raw = {};
|
|
336
|
+
for (let index = 0; index < positionals.length; index += 1) {
|
|
337
|
+
if (actionArgs[index] !== undefined)
|
|
338
|
+
raw[positionals[index].name] = actionArgs[index];
|
|
339
|
+
}
|
|
340
|
+
for (const argument of precedenceSurface.args) {
|
|
341
|
+
if (argument.positional)
|
|
342
|
+
continue;
|
|
343
|
+
const value = options[argument.name];
|
|
344
|
+
if (value !== undefined)
|
|
345
|
+
raw[argument.name] = value;
|
|
346
|
+
}
|
|
347
|
+
const args = coerceCommandArguments(precedenceSurface.args, raw);
|
|
348
|
+
const format = parseOutputFormat(options.format ?? 'table');
|
|
349
|
+
const trace = String(options.trace ?? 'off');
|
|
350
|
+
if (!['off', 'on', 'retain-on-failure'].includes(trace)) {
|
|
351
|
+
throw new Error(`--trace must be one of: off, on, retain-on-failure. Received: "${trace}"`);
|
|
352
|
+
}
|
|
353
|
+
outcome = { kind: 'success', args, format, trace };
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
outcome = { kind: 'semantic', message: error instanceof Error ? error.message : String(error) };
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
try {
|
|
360
|
+
root.parse([precedenceSurface.site, precedenceSurface.name, ...argv], { from: 'user' });
|
|
361
|
+
return outcome ?? { kind: 'success', args: {}, format: 'table', trace: 'off' };
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const commander = error;
|
|
365
|
+
if (commander.code === 'commander.helpDisplayed')
|
|
366
|
+
return { kind: 'help' };
|
|
367
|
+
return { kind: 'structural', stderr, exitCode: commander.exitCode ?? 1 };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function captureSharedSurface(argv) {
|
|
371
|
+
try {
|
|
372
|
+
const parsed = parseCommandSurface(precedenceSurface, argv);
|
|
373
|
+
if (parsed.help)
|
|
374
|
+
return { kind: 'help' };
|
|
375
|
+
return { kind: 'success', args: parsed.args, format: parsed.format, trace: parsed.trace };
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error instanceof CommanderStructuralError) {
|
|
379
|
+
return { kind: 'structural', stderr: error.output, exitCode: error.exitCode };
|
|
380
|
+
}
|
|
381
|
+
return { kind: 'semantic', message: error instanceof Error ? error.message : String(error) };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
describe('complete Commander structural grammar and precedence parity', () => {
|
|
385
|
+
it.each([
|
|
386
|
+
{ name: 'help before unknown option', argv: ['--help', '--unknown'] },
|
|
387
|
+
{ name: 'unknown option before help', argv: ['--unknown', '--help'] },
|
|
388
|
+
{ name: 'help with a missing format value', argv: ['--help', '-f'] },
|
|
389
|
+
{ name: 'help with excess positionals', argv: ['--help', 'one', 'two', 'three'] },
|
|
390
|
+
{ name: 'help with invalid choice, format, and trace', argv: ['--help', '--mode', 'bad', '-f', 'xml', '--trace', 'bad'] },
|
|
391
|
+
{ name: 'missing required named with invalid format', argv: ['account', '-f', 'xml'] },
|
|
392
|
+
{ name: 'missing required named with invalid trace', argv: ['account', '--trace', 'bad'] },
|
|
393
|
+
{ name: 'missing required named with invalid choice', argv: ['account', '--mode', 'bad'] },
|
|
394
|
+
{ name: 'missing positional with invalid format', argv: ['--token', 'secret', '-f', 'xml'] },
|
|
395
|
+
{ name: 'missing positional with invalid trace', argv: ['--token', 'secret', '--trace', 'bad'] },
|
|
396
|
+
{ name: 'missing positional with invalid choice', argv: ['--token', 'secret', '--mode', 'bad'] },
|
|
397
|
+
{ name: 'invalid choice precedes invalid format and trace', argv: ['account', '--token', 'secret', '--mode', 'bad', '-f', 'xml', '--trace', 'bad'] },
|
|
398
|
+
{ name: 'invalid format precedes invalid trace', argv: ['account', '--token', 'secret', '-f', 'xml', '--trace', 'bad'] },
|
|
399
|
+
{ name: 'ordinary unknown option', argv: ['account', '--token', 'secret', '--unknown'] },
|
|
400
|
+
{ name: 'ordinary missing required option value', argv: ['account', '--token'] },
|
|
401
|
+
{ name: 'ordinary excess positional', argv: ['account', 'scope', 'extra', '--token', 'secret'] },
|
|
402
|
+
{ name: 'dash-leading positional after separator', argv: ['--token', 'secret', '--', '-account'] },
|
|
403
|
+
])('$name', ({ argv }) => {
|
|
404
|
+
expect(captureSharedSurface(argv)).toEqual(captureReferenceSurface(argv));
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -14,7 +14,11 @@ import { type CliCommand } from './registry.js';
|
|
|
14
14
|
/**
|
|
15
15
|
* Register a single CliCommand as a Commander subcommand.
|
|
16
16
|
*/
|
|
17
|
-
export
|
|
17
|
+
export interface CommanderAdapterRuntime {
|
|
18
|
+
stdout?: NodeJS.WritableStream;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare function registerCommandToProgram(siteCmd: Command, cmd: CliCommand, runtime?: CommanderAdapterRuntime): void;
|
|
18
22
|
/**
|
|
19
23
|
* Register all commands from the registry onto a Commander program.
|
|
20
24
|
*/
|