From 2d41de9b32e04359c0ccd8eaee74a3e5893d9caa Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Fri, 27 Mar 2026 15:49:16 -0600 Subject: [PATCH] fix: Accept flags after positional command in headless arg parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gsd headless new-milestone --auto --verbose` now works — flags are parsed regardless of position relative to the command word. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/headless.ts | 6 ++-- src/tests/headless-cli-surface.test.ts | 46 +++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/headless.ts b/src/headless.ts index 5e54cac64..095b1a2f2 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -129,13 +129,12 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions { } const args = argv.slice(2) - let positionalStarted = false for (let i = 0; i < args.length; i++) { const arg = args[i] if (arg === 'headless') continue - if (!positionalStarted && arg.startsWith('--')) { + if (arg.startsWith('--')) { if (arg === '--timeout' && i + 1 < args.length) { options.timeout = parseInt(args[++i], 10) if (Number.isNaN(options.timeout) || options.timeout < 0) { @@ -197,8 +196,7 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions { } else if (arg === '--bare') { options.bare = true } - } else if (!positionalStarted) { - positionalStarted = true + } else if (options.command === 'auto') { options.command = arg } else { options.commandArgs.push(arg) diff --git a/src/tests/headless-cli-surface.test.ts b/src/tests/headless-cli-surface.test.ts index 89fab5d44..3bf552a7c 100644 --- a/src/tests/headless-cli-surface.test.ts +++ b/src/tests/headless-cli-surface.test.ts @@ -54,13 +54,12 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions { } const args = argv.slice(2) - let positionalStarted = false for (let i = 0; i < args.length; i++) { const arg = args[i] if (arg === 'headless') continue - if (!positionalStarted && arg.startsWith('--')) { + if (arg.startsWith('--')) { if (arg === '--timeout' && i + 1 < args.length) { options.timeout = parseInt(args[++i], 10) } else if (arg === '--json') { @@ -108,8 +107,7 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions { } else if (arg === '--bare') { options.bare = true } - } else if (!positionalStarted) { - positionalStarted = true + } else if (options.command === 'auto') { options.command = arg } else { options.commandArgs.push(arg) @@ -372,6 +370,46 @@ test('--bare combined with --output-format json', () => { assert.equal(opts.command, 'auto') }) +// ─── Command-first ordering (flags after command) ───────────────────────── + +test('command before flags: new-milestone --context-text --auto --verbose', () => { + const opts = parseHeadlessArgs([ + 'node', 'gsd', 'headless', + 'new-milestone', + '--context-text', 'build something cool', + '--auto', + '--verbose', + ]) + assert.equal(opts.command, 'new-milestone') + assert.equal(opts.contextText, 'build something cool') + assert.equal(opts.auto, true) + assert.equal(opts.verbose, true) +}) + +test('command before flags: next --json --timeout', () => { + const opts = parseHeadlessArgs([ + 'node', 'gsd', 'headless', + 'next', + '--json', + '--timeout', '60000', + ]) + assert.equal(opts.command, 'next') + assert.equal(opts.json, true) + assert.equal(opts.timeout, 60000) +}) + +test('command between flags: --auto new-milestone --verbose', () => { + const opts = parseHeadlessArgs([ + 'node', 'gsd', 'headless', + '--auto', + 'new-milestone', + '--verbose', + ]) + assert.equal(opts.command, 'new-milestone') + assert.equal(opts.auto, true) + assert.equal(opts.verbose, true) +}) + test('--bare does not affect other flags', () => { const opts = parseHeadlessArgs([ 'node', 'gsd', 'headless',