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
57 changes: 57 additions & 0 deletions src/commanderAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,63 @@ describe('commanderAdapter default formats', () => {
});
});

describe('commanderAdapter dash-prefixed positional args', () => {
const cmd: CliCommand = {
site: 'boss',
name: 'detail',
description: 'BOSS直聘查看职位详情',
browser: true,
args: [
{ name: 'security-id', positional: true, required: true, help: 'Security ID from search results' },
],
func: vi.fn(),
};

beforeEach(() => {
mockExecuteCommand.mockReset();
mockExecuteCommand.mockResolvedValue([]);
mockRenderOutput.mockReset();
delete process.env.OPENCLI_VERBOSE;
process.exitCode = undefined;
});

it('accepts a positional arg that starts with a dash', async () => {
const program = new Command();
const siteCmd = program.command('boss');
registerCommandToProgram(siteCmd, cmd);

await program.parseAsync(['node', 'opencli', 'boss', 'detail', '-123456abdc']);

expect(mockExecuteCommand).toHaveBeenCalled();
const kwargs = mockExecuteCommand.mock.calls[0][1];
expect(kwargs['security-id']).toBe('-123456abdc');
});

it('accepts a dash-prefixed positional arg with options before it', async () => {
const program = new Command();
const siteCmd = program.command('boss');
registerCommandToProgram(siteCmd, cmd);

await program.parseAsync(['node', 'opencli', 'boss', 'detail', '-f', 'json', '-abc123']);

expect(mockExecuteCommand).toHaveBeenCalled();
const kwargs = mockExecuteCommand.mock.calls[0][1];
expect(kwargs['security-id']).toBe('-abc123');
});

it('still works with normal (non-dash) positional args', async () => {
const program = new Command();
const siteCmd = program.command('boss');
registerCommandToProgram(siteCmd, cmd);

await program.parseAsync(['node', 'opencli', 'boss', 'detail', 'abc123']);

expect(mockExecuteCommand).toHaveBeenCalled();
const kwargs = mockExecuteCommand.mock.calls[0][1];
expect(kwargs['security-id']).toBe('abc123');
});
});

describe('commanderAdapter error envelope output', () => {
const cmd: CliCommand = {
site: 'xiaohongshu',
Expand Down
35 changes: 35 additions & 0 deletions src/commanderAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,41 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi

subCmd.addHelpText('after', formatRegistryHelpText(cmd));

// When a command has positional args, protect against dash-prefixed values
// (e.g. `opencli boss detail -123abc`) being misinterpreted as options.
// We override parseOptions to insert '--' before the first unrecognized
// dash-arg so Commander treats it as an operand.
if (positionalArgs.length > 0) {
const origParseOptions = subCmd.parseOptions.bind(subCmd);
subCmd.parseOptions = (argv: string[]) => {
if (!argv.includes('--')) {
const optFlags = new Set<string>();
for (const opt of subCmd.options) {
if (opt.short) optFlags.add(opt.short);
if (opt.long) optFlags.add(opt.long);
}
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--') break;
if (a.startsWith('-') && optFlags.has(a)) {
// Known option — skip its value arg if it expects one
const opt = subCmd.options.find(
(o) => o.short === a || o.long === a,
);
if (opt && opt.required) i++;
continue;
}
if (a.startsWith('-')) {
// Unknown dash-arg in positional position — insert '--' sentinel
argv.splice(i, 0, '--');
break;
}
}
}
return origParseOptions(argv);
};
}

subCmd.action(async (...actionArgs: unknown[]) => {
const actionOpts = actionArgs[positionalArgs.length] ?? {};
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
Expand Down