fix: Accept flags after positional command in headless arg parser

`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) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-27 15:49:16 -06:00
parent 898e797772
commit 2d41de9b32
2 changed files with 44 additions and 8 deletions

View file

@ -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)

View file

@ -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',