diff --git a/src/cli/args.ts b/src/cli/args.ts index e0db5ec..c65272b 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -12,6 +12,36 @@ export interface Args { flags: Record; } +// Boolean options must not consume the next command or search word. Keep +// explicit boolean values working for callers that pass `--remote false`. +const BOOLEAN_FLAGS = new Set([ + 'version', + 'v', + 'help', + 'h', + 'yes', + 'y', + 'json', + 'remote', + 'agents', + 'all', + 'network', + 'unsupervised', + 'draft', + 'salary-unpaid', + 'publish', + 'following', + 'candidate', +]); + +function takesValue(name: string, next: string | undefined): next is string { + return ( + next !== undefined && + !next.startsWith('-') && + (!BOOLEAN_FLAGS.has(name) || /^(true|false|1|0|yes|no)$/i.test(next)) + ); +} + export function parseArgs(argv: string[]): Args { const positional: string[] = []; const flags: Record = {}; @@ -37,7 +67,7 @@ export function parseArgs(argv: string[]): Args { continue; } const next = argv[index + 1]; - if (next !== undefined && !next.startsWith('-')) { + if (takesValue(body, next)) { set(flags, body, next); index += 1; continue; @@ -49,7 +79,7 @@ export function parseArgs(argv: string[]): Args { if (token.startsWith('-') && token.length > 1) { const body = token.slice(1); const next = argv[index + 1]; - if (next !== undefined && !next.startsWith('-')) { + if (takesValue(body, next)) { set(flags, body, next); index += 1; continue; diff --git a/test/cli-args.test.ts b/test/cli-args.test.ts new file mode 100644 index 0000000..631577a --- /dev/null +++ b/test/cli-args.test.ts @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { flagBool, flagList, flagString, parseArgs } from '../dist/cli/args.js'; + +test('boolean search flags preserve the following search words', () => { + const args = parseArgs(['search', '--remote', 'rust', '--agents', 'developer']); + assert.equal(args.command, 'search'); + assert.deepEqual(args.positional, ['rust', 'developer']); + assert.equal(flagBool(args, 'remote'), true); + assert.equal(flagBool(args, 'agents'), true); +}); + +test('global boolean flags preserve the command', () => { + for (const flag of ['--json', '--help', '-h', '--version', '-v', '--yes', '-y']) { + const args = parseArgs([flag, 'search', 'rust']); + assert.equal(args.command, 'search', flag); + assert.deepEqual(args.positional, ['rust'], flag); + } +}); + +test('explicit boolean values remain supported', () => { + for (const value of ['false', '0', 'no', 'FALSE']) { + for (const flag of [['--remote', value], [`--remote=${value}`]]) { + const args = parseArgs(['search', ...flag, 'rust']); + assert.equal(flagBool(args, 'remote'), false); + assert.deepEqual(args.positional, ['rust']); + } + } +}); + +test('value options, repeated fields, and the option terminator retain their meaning', () => { + const args = parseArgs([ + 'apply', + '-s', + 'https://example.com', + '--answer', + 'a=1', + '--answer', + 'b=2', + '--', + '--remote', + 'job-slug', + ]); + assert.equal(flagString(args, 's'), 'https://example.com'); + assert.deepEqual(flagList(args, 'answer'), ['a=1', 'b=2']); + assert.deepEqual(args.positional, ['--remote', 'job-slug']); +});