diff --git a/src/commanderAdapter.test.ts b/src/commanderAdapter.test.ts index 48ecc723f..1aa85c150 100644 --- a/src/commanderAdapter.test.ts +++ b/src/commanderAdapter.test.ts @@ -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', diff --git a/src/commanderAdapter.ts b/src/commanderAdapter.ts index 47b98c35f..dae256559 100644 --- a/src/commanderAdapter.ts +++ b/src/commanderAdapter.ts @@ -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(); + 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 : {};