Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/command-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,29 @@ describe('configureCommandSurface', () => {
]));
expect(command.registeredArguments.map((argument) => argument.name())).toEqual(['query', 'scope']);
});

it('preserves adapter arguments that collide with shared execution flags', () => {
const command = new Command('search');
const collisions = ['format', 'json', 'trace', 'verbose', 'window', 'site-session', 'keep-tab'];
configureCommandSurface(command, {
...metadata,
browser: true,
args: collisions.map(name => ({ name, type: 'string' })),
});

expect(command.options.map(option => option.long)).toEqual(collisions.map(name => `--${name}`));
expect(parseCommandSurface({
...metadata,
browser: true,
args: [{ name: 'json', type: 'bool', default: false }],
}, ['--json'])).toMatchObject({
args: { json: true },
format: 'plain',
formatExplicit: false,
trace: 'off',
verbose: false,
});
});
});

describe('unknown option contract', () => {
Expand Down
66 changes: 55 additions & 11 deletions src/command-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,21 @@ export function configureCommandSurface(command: Command, metadata: CommandSurfa
else command.option(flag, arg.help ?? '');
}

addOutputFormatOption(command)
.option('--trace <mode>', `Trace capture: ${TRACE_MODES.join(', ')}`, 'off')
.option('-v, --verbose', 'Debug output', false);
addSharedExecutionOptions(command);

if (metadata.browser) {
command
.option('--window <mode>', `Browser window mode: ${BROWSER_WINDOW_MODES.join(' or ')} (default: background)`)
.option('--site-session <mode>', `Adapter site session lifecycle: ${SITE_SESSION_MODES.join(' or ')}`)
.option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
addSharedOption(command, '--window', () => command.option(
'--window <mode>',
`Browser window mode: ${BROWSER_WINDOW_MODES.join(' or ')} (default: background)`,
));
addSharedOption(command, '--site-session', () => command.option(
'--site-session <mode>',
`Adapter site session lifecycle: ${SITE_SESSION_MODES.join(' or ')}`,
));
addSharedOption(command, '--keep-tab', () => command.option(
'--keep-tab <bool>',
'Keep the browser tab lease after the command finishes',
));
}
}

Expand Down Expand Up @@ -324,8 +330,8 @@ export function parseCommandSurface(
const args = coerceCommandArguments(metadata.args, input);
const formatExplicit = outputFormatIsExplicit(command);
const format = parseOutputFormat(formatExplicit ? requestedOutputFormat(command, parsedOptions.format) : defaultFormat);
const trace = parseTraceMode(parsedOptions.trace ?? 'off');
const verbose = parsedOptions.verbose === true;
const trace = isSharedCommandOption(command, '--trace') ? parseTraceMode(parsedOptions.trace ?? 'off') : 'off';
const verbose = isSharedCommandOption(command, '--verbose') && parsedOptions.verbose === true;

return {
args,
Expand Down Expand Up @@ -455,6 +461,41 @@ export function addOutputFormatOption(command: Command, defaultFormat = 'table')
.option('--json', JSON_FORMAT_ALIAS_HELP, false);
}

type CommandWithSharedOptions = Command & { _webcmdSharedOptions?: Set<string> };

function addSharedExecutionOptions(command: Command): void {
const flags = command.options.flatMap(option => [option.short, option.long]).filter(Boolean) as string[];
const shared = new Set<string>();
(command as CommandWithSharedOptions)._webcmdSharedOptions = shared;

if (!flags.includes('--format')) {
command.option(flags.includes('-f') ? '--format <fmt>' : '-f, --format <fmt>', OUTPUT_FORMAT_HELP, 'table');
shared.add('--format');
}
addSharedOption(command, '--json', () => command.option('--json', JSON_FORMAT_ALIAS_HELP, false));
addSharedOption(command, '--trace', () => command.option('--trace <mode>', `Trace capture: ${TRACE_MODES.join(', ')}`, 'off'));
if (!command.options.some(option => option.long === '--verbose')) {
command.option(
command.options.some(option => option.short === '-v') ? '--verbose' : '-v, --verbose',
'Debug output',
false,
);
shared.add('--verbose');
}
}

function addSharedOption(command: Command, flag: string, register: () => void): void {
if (command.options.some(option => option.short === flag || option.long === flag)) return;
register();
((command as CommandWithSharedOptions)._webcmdSharedOptions ??= new Set()).add(flag);
}

/** Whether an option belongs to webcmd rather than to the adapter grammar. */
export function isSharedCommandOption(command: Command, flag: string): boolean {
const shared = (command as CommandWithSharedOptions)._webcmdSharedOptions;
return shared ? shared.has(flag) : command.options.some(option => option.short === flag || option.long === flag);
}

/**
* Give every command in a tree the same output-format grammar.
*
Expand Down Expand Up @@ -487,12 +528,15 @@ export function ensureOutputFormatOptions(command: Command): void {
}

export function outputFormatIsExplicit(command: Command): boolean {
return command.getOptionValueSource('format') === 'cli' || command.getOptionValueSource('json') === 'cli';
return (isSharedCommandOption(command, '--format') && command.getOptionValueSource('format') === 'cli')
|| (isSharedCommandOption(command, '--json') && command.getOptionValueSource('json') === 'cli');
}

/** Resolve `--json` onto `--format json` unless `--format` was also passed. */
export function requestedOutputFormat(command: Command, format: unknown): unknown {
return command.getOptionValueSource('json') === 'cli' && command.getOptionValueSource('format') !== 'cli'
return isSharedCommandOption(command, '--json')
&& command.getOptionValueSource('json') === 'cli'
&& command.getOptionValueSource('format') !== 'cli'
? 'json'
: format;
}
Expand Down
17 changes: 10 additions & 7 deletions src/commanderAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Command } from 'commander';
import { log } from './logger.js';
import { type CliCommand, fullName, getRegistry } from './registry.js';
import { errorEnvelopeFormat, formatErrorEnvelope, render as renderOutput } from './output.js';
import { configureCommandSurface, outputFormatIsExplicit, parseOutputFormat, prepareCommandArgs, requestedOutputFormat } from './command-surface.js';
import { configureCommandSurface, isSharedCommandOption, outputFormatIsExplicit, parseOutputFormat, prepareCommandArgs, requestedOutputFormat } from './command-surface.js';
import {
commandHelpData,
formatCommandHelpText,
Expand Down Expand Up @@ -95,8 +95,11 @@ export function registerCommandToProgram(
}
const kwargs = prepareCommandArgs(cmd, rawKwargs);

const verbose = optionsRecord.verbose === true;
let format = parseOutputFormat(requestedOutputFormat(subCmd, optionsRecord.format ?? 'table'));
const verbose = isSharedCommandOption(subCmd, '--verbose') && optionsRecord.verbose === true;
let format = parseOutputFormat(requestedOutputFormat(
subCmd,
isSharedCommandOption(subCmd, '--format') ? optionsRecord.format ?? 'table' : 'table',
));
const formatExplicit = outputFormatIsExplicit(subCmd);
if (verbose) process.env.WEBCMD_VERBOSE = '1';
const globals = typeof subCmd.optsWithGlobals === 'function' ? subCmd.optsWithGlobals() as Record<string, unknown> : {};
Expand All @@ -106,10 +109,10 @@ export function registerCommandToProgram(
prepared: true,
...(typeof globals.profile === 'string' && globals.profile.trim() ? { profile: globals.profile.trim() } : {}),
...(typeof globals.session === 'string' && globals.session.trim() ? { session: globals.session.trim() } : {}),
...(typeof optionsRecord.trace === 'string' && optionsRecord.trace !== 'off' ? { trace: optionsRecord.trace } : {}),
...(cmd.browser && typeof optionsRecord.window === 'string' ? { windowMode: optionsRecord.window } : {}),
...(cmd.browser && typeof optionsRecord.siteSession === 'string' ? { siteSession: optionsRecord.siteSession } : {}),
...(cmd.browser && typeof optionsRecord.keepTab === 'string' ? { keepTab: optionsRecord.keepTab } : {}),
...(isSharedCommandOption(subCmd, '--trace') && typeof optionsRecord.trace === 'string' && optionsRecord.trace !== 'off' ? { trace: optionsRecord.trace } : {}),
...(cmd.browser && isSharedCommandOption(subCmd, '--window') && typeof optionsRecord.window === 'string' ? { windowMode: optionsRecord.window } : {}),
...(cmd.browser && isSharedCommandOption(subCmd, '--site-session') && typeof optionsRecord.siteSession === 'string' ? { siteSession: optionsRecord.siteSession } : {}),
...(cmd.browser && isSharedCommandOption(subCmd, '--keep-tab') && typeof optionsRecord.keepTab === 'string' ? { keepTab: optionsRecord.keepTab } : {}),
});
if (result === null || result === undefined) {
return;
Expand Down
Loading