test: Added --output-format text|json|stream-json flag, standardized ex…

- "src/headless-types.ts"
- "src/headless-events.ts"
- "src/headless.ts"
- "src/help-text.ts"
- "src/tests/headless-cli-surface.test.ts"

GSD-Task: S02/T01
This commit is contained in:
Lex Christopherson 2026-03-26 11:34:21 -06:00
parent 4d218353ac
commit d355ab93fb
5 changed files with 479 additions and 22 deletions

View file

@ -3,8 +3,47 @@
*
* Detects terminal notifications, blocked notifications, milestone-ready signals,
* and classifies commands as quick (single-turn) vs long-running.
*
* Also defines exit code constants and the statusexit-code mapping function.
*/
// ---------------------------------------------------------------------------
// Exit Code Constants
// ---------------------------------------------------------------------------
export const EXIT_SUCCESS = 0
export const EXIT_ERROR = 1
export const EXIT_BLOCKED = 10
export const EXIT_CANCELLED = 11
/**
* Map a headless session status string to its standardized exit code.
*
* success 0
* error 1
* timeout 1
* blocked 10
* cancelled 11
*
* Unknown statuses default to EXIT_ERROR (1).
*/
export function mapStatusToExitCode(status: string): number {
switch (status) {
case 'success':
case 'complete':
return EXIT_SUCCESS
case 'error':
case 'timeout':
return EXIT_ERROR
case 'blocked':
return EXIT_BLOCKED
case 'cancelled':
return EXIT_CANCELLED
default:
return EXIT_ERROR
}
}
// ---------------------------------------------------------------------------
// Completion Detection
// ---------------------------------------------------------------------------

39
src/headless-types.ts Normal file
View file

@ -0,0 +1,39 @@
/**
* Headless Types shared types for the headless orchestrator surface.
*
* Contains the structured result type emitted in --output-format json mode
* and the output format discriminator.
*/
// ---------------------------------------------------------------------------
// Output Format
// ---------------------------------------------------------------------------
export type OutputFormat = 'text' | 'json' | 'stream-json'
export const VALID_OUTPUT_FORMATS: ReadonlySet<string> = new Set(['text', 'json', 'stream-json'])
// ---------------------------------------------------------------------------
// Structured JSON Result
// ---------------------------------------------------------------------------
export interface HeadlessJsonResult {
status: 'success' | 'error' | 'blocked' | 'cancelled' | 'timeout'
exitCode: number
sessionId?: string
duration: number
cost: {
total: number
input_tokens: number
output_tokens: number
cache_read_tokens: number
cache_write_tokens: number
}
toolCalls: number
events: number
milestone?: string
phase?: string
nextAction?: string
artifacts?: string[]
commits?: string[]
}

View file

@ -6,9 +6,10 @@
* progress to stderr.
*
* Exit codes:
* 0 complete (command finished successfully)
* 1 error or timeout
* 2 blocked (command reported a blocker)
* 0 complete (command finished successfully)
* 1 error or timeout
* 10 blocked (command reported a blocker)
* 11 cancelled (SIGINT/SIGTERM received)
*/
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
@ -27,8 +28,16 @@ import {
FIRE_AND_FORGET_METHODS,
IDLE_TIMEOUT_MS,
NEW_MILESTONE_IDLE_TIMEOUT_MS,
EXIT_SUCCESS,
EXIT_ERROR,
EXIT_BLOCKED,
EXIT_CANCELLED,
mapStatusToExitCode,
} from './headless-events.js'
import type { OutputFormat } from './headless-types.js'
import { VALID_OUTPUT_FORMATS } from './headless-types.js'
import {
handleExtensionUIRequest,
formatProgress,
@ -48,6 +57,7 @@ import {
export interface HeadlessOptions {
timeout: number
json: boolean
outputFormat: OutputFormat
model?: string
command: string
commandArgs: string[]
@ -60,6 +70,7 @@ export interface HeadlessOptions {
responseTimeout?: number // timeout for orchestrator response (default 30000ms)
answers?: string // path to answers JSON file
eventFilter?: Set<string> // filter JSONL output to specific event types
resumeSession?: string // session ID to resume (--resume <id>)
}
interface TrackedEvent {
@ -76,6 +87,7 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
const options: HeadlessOptions = {
timeout: 300_000,
json: false,
outputFormat: 'text',
command: 'auto',
commandArgs: [],
}
@ -96,6 +108,17 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
}
} else if (arg === '--json') {
options.json = true
options.outputFormat = 'stream-json'
} else if (arg === '--output-format' && i + 1 < args.length) {
const fmt = args[++i]
if (!VALID_OUTPUT_FORMATS.has(fmt)) {
process.stderr.write(`[headless] Error: --output-format must be one of: text, json, stream-json (got '${fmt}')\n`)
process.exit(1)
}
options.outputFormat = fmt as OutputFormat
if (fmt === 'stream-json' || fmt === 'json') {
options.json = true
}
} else if (arg === '--model' && i + 1 < args.length) {
// --model can also be passed from the main CLI; headless-specific takes precedence
options.model = args[++i]
@ -118,15 +141,23 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
} else if (arg === '--events' && i + 1 < args.length) {
options.eventFilter = new Set(args[++i].split(','))
options.json = true // --events implies --json
if (options.outputFormat === 'text') {
options.outputFormat = 'stream-json'
}
} else if (arg === '--supervised') {
options.supervised = true
options.json = true // supervised implies json
if (options.outputFormat === 'text') {
options.outputFormat = 'stream-json'
}
} else if (arg === '--response-timeout' && i + 1 < args.length) {
options.responseTimeout = parseInt(args[++i], 10)
if (Number.isNaN(options.responseTimeout) || options.responseTimeout <= 0) {
process.stderr.write('[headless] Error: --response-timeout must be a positive integer (milliseconds)\n')
process.exit(1)
}
} else if (arg === '--resume' && i + 1 < args.length) {
options.resumeSession = args[++i]
}
} else if (!positionalStarted) {
positionalStarted = true
@ -151,7 +182,7 @@ export async function runHeadless(options: HeadlessOptions): Promise<void> {
const result = await runHeadlessOnce(options, restartCount)
// Success or blocked — exit normally
if (result.exitCode === 0 || result.exitCode === 2) {
if (result.exitCode === EXIT_SUCCESS || result.exitCode === EXIT_BLOCKED) {
process.exit(result.exitCode)
}
@ -349,7 +380,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
const timeoutTimer = options.timeout > 0
? setTimeout(() => {
process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`)
exitCode = 1
exitCode = EXIT_ERROR
resolveCompletion()
}, options.timeout)
: null
@ -395,7 +426,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
if (injector && !FIRE_AND_FORGET_METHODS.has(String(eventObj.method ?? ''))) {
if (injector.tryHandle(eventObj, stdinWriter)) {
if (completed) {
exitCode = blocked ? 2 : 0
exitCode = blocked ? EXIT_BLOCKED : EXIT_SUCCESS
resolveCompletion()
}
return
@ -421,7 +452,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
// If we detected a terminal notification, resolve after responding
if (completed) {
exitCode = blocked ? 2 : 0
exitCode = blocked ? EXIT_BLOCKED : EXIT_SUCCESS
resolveCompletion()
return
}
@ -442,7 +473,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
const signalHandler = () => {
process.stderr.write('\n[headless] Interrupted, stopping child process...\n')
interrupted = true
exitCode = 1
exitCode = EXIT_CANCELLED
client.stop().finally(() => {
if (timeoutTimer) clearTimeout(timeoutTimer)
if (idleTimer) clearTimeout(idleTimer)
@ -492,10 +523,9 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
if (!completed) {
const msg = `[headless] Child process exited unexpectedly with code ${code ?? 'null'}\n`
process.stderr.write(msg)
exitCode = 1
exitCode = EXIT_ERROR
resolveCompletion()
}
})
} })
if (!options.json) {
process.stderr.write(`[headless] Running /gsd ${options.command}${options.commandArgs.length > 0 ? ' ' + options.commandArgs.join(' ') : ''}...\n`)
@ -507,16 +537,16 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
await client.prompt(command)
} catch (err) {
process.stderr.write(`[headless] Error: Failed to send prompt: ${err instanceof Error ? err.message : String(err)}\n`)
exitCode = 1
exitCode = EXIT_ERROR
}
// Wait for completion
if (exitCode === 0 || exitCode === 2) {
if (exitCode === EXIT_SUCCESS || exitCode === EXIT_BLOCKED) {
await completionPromise
}
// Auto-mode chaining: if --auto and milestone creation succeeded, send /gsd auto
if (isNewMilestone && options.auto && milestoneReady && !blocked && exitCode === 0) {
if (isNewMilestone && options.auto && milestoneReady && !blocked && exitCode === EXIT_SUCCESS) {
if (!options.json) {
process.stderr.write('[headless] Milestone ready — chaining into auto-mode...\n')
}
@ -535,10 +565,10 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
await client.prompt('/gsd auto')
} catch (err) {
process.stderr.write(`[headless] Error: Failed to start auto-mode: ${err instanceof Error ? err.message : String(err)}\n`)
exitCode = 1
exitCode = EXIT_ERROR
}
if (exitCode === 0 || exitCode === 2) {
if (exitCode === EXIT_SUCCESS || exitCode === EXIT_BLOCKED) {
await autoCompletionPromise
}
}
@ -557,7 +587,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
// Summary
const duration = ((Date.now() - startTime) / 1000).toFixed(1)
const status = blocked ? 'blocked' : exitCode === 1 ? (totalEvents === 0 ? 'error' : 'timeout') : 'complete'
const status = blocked ? 'blocked' : exitCode === EXIT_CANCELLED ? 'cancelled' : exitCode === EXIT_ERROR ? (totalEvents === 0 ? 'error' : 'timeout') : 'complete'
process.stderr.write(`[headless] Status: ${status}\n`)
process.stderr.write(`[headless] Duration: ${duration}s\n`)

View file

@ -94,9 +94,12 @@ const SUBCOMMAND_HELP: Record<string, string> = {
'Run /gsd commands without the TUI. Default command: auto',
'',
'Flags:',
' --timeout N Overall timeout in ms (default: 300000)',
' --json JSONL event stream to stdout',
' --model ID Override model',
' --timeout N Overall timeout in ms (default: 300000)',
' --json JSONL event stream to stdout (alias for --output-format stream-json)',
' --output-format <fmt> Output format: text (default), json (structured result), stream-json (JSONL events)',
' --bare Minimal context: skip CLAUDE.md, AGENTS.md, user settings, user skills',
' --resume <id> Resume a prior headless session by ID',
' --model ID Override model',
' --supervised Forward interactive UI requests to orchestrator via stdout/stdin',
' --response-timeout N Timeout (ms) for orchestrator response (default: 30000)',
' --answers <path> Pre-supply answers and secrets (JSON file)',
@ -115,11 +118,19 @@ const SUBCOMMAND_HELP: Record<string, string> = {
' --auto Start auto-mode after milestone creation',
' --verbose Show tool calls in progress output',
'',
'Output formats:',
' text Human-readable progress on stderr (default)',
' json Collect events silently, emit structured HeadlessJsonResult on stdout at exit',
' stream-json Stream JSONL events to stdout in real time (same as --json)',
'',
'Examples:',
' gsd headless Run /gsd auto',
' gsd headless next Run one unit',
' gsd headless --json status Machine-readable status',
' gsd headless --output-format json auto Structured JSON result on stdout',
' gsd headless --json status Machine-readable JSONL stream',
' gsd headless --timeout 60000 With 1-minute timeout',
' gsd headless --bare auto Minimal context (CI/ecosystem use)',
' gsd headless --resume abc123 auto Resume a prior session',
' gsd headless new-milestone --context spec.md Create milestone from file',
' cat spec.md | gsd headless new-milestone --context - From stdin',
' gsd headless new-milestone --context spec.md --auto Create + auto-execute',
@ -128,7 +139,7 @@ const SUBCOMMAND_HELP: Record<string, string> = {
' gsd headless --events agent_end,extension_ui_request auto Filtered event stream',
' gsd headless query Instant JSON state snapshot',
'',
'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
'Exit codes: 0 = success, 1 = error/timeout, 10 = blocked, 11 = cancelled',
].join('\n'),
}

View file

@ -0,0 +1,338 @@
/**
* Tests for S02 CLI surface --output-format, exit codes, HeadlessJsonResult, --resume.
*
* Uses extracted parsing logic (mirrors headless.ts) and direct imports from
* headless-types.ts / headless-events.ts to avoid transitive @gsd/native
* import that breaks in test environment.
*/
import test from 'node:test'
import assert from 'node:assert/strict'
// ─── Import exit code constants & mapStatusToExitCode ──────────────────────
import {
EXIT_SUCCESS,
EXIT_ERROR,
EXIT_BLOCKED,
EXIT_CANCELLED,
mapStatusToExitCode,
} from '../headless-events.js'
import type { OutputFormat, HeadlessJsonResult } from '../headless-types.js'
import { VALID_OUTPUT_FORMATS } from '../headless-types.js'
// ─── Extracted parsing logic (mirrors headless.ts) ─────────────────────────
interface HeadlessOptions {
timeout: number
json: boolean
outputFormat: OutputFormat
model?: string
command: string
commandArgs: string[]
context?: string
contextText?: string
auto?: boolean
verbose?: boolean
maxRestarts?: number
supervised?: boolean
responseTimeout?: number
answers?: string
eventFilter?: Set<string>
resumeSession?: string
}
function parseHeadlessArgs(argv: string[]): HeadlessOptions {
const options: HeadlessOptions = {
timeout: 300_000,
json: false,
outputFormat: 'text',
command: 'auto',
commandArgs: [],
}
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 === '--timeout' && i + 1 < args.length) {
options.timeout = parseInt(args[++i], 10)
} else if (arg === '--json') {
options.json = true
options.outputFormat = 'stream-json'
} else if (arg === '--output-format' && i + 1 < args.length) {
const fmt = args[++i]
if (!VALID_OUTPUT_FORMATS.has(fmt)) {
throw new Error(`Invalid output format: ${fmt}`)
}
options.outputFormat = fmt as OutputFormat
if (fmt === 'stream-json' || fmt === 'json') {
options.json = true
}
} else if (arg === '--model' && i + 1 < args.length) {
options.model = args[++i]
} else if (arg === '--context' && i + 1 < args.length) {
options.context = args[++i]
} else if (arg === '--context-text' && i + 1 < args.length) {
options.contextText = args[++i]
} else if (arg === '--auto') {
options.auto = true
} else if (arg === '--verbose') {
options.verbose = true
} else if (arg === '--max-restarts' && i + 1 < args.length) {
options.maxRestarts = parseInt(args[++i], 10)
} else if (arg === '--answers' && i + 1 < args.length) {
options.answers = args[++i]
} else if (arg === '--events' && i + 1 < args.length) {
options.eventFilter = new Set(args[++i].split(','))
options.json = true
if (options.outputFormat === 'text') {
options.outputFormat = 'stream-json'
}
} else if (arg === '--supervised') {
options.supervised = true
options.json = true
if (options.outputFormat === 'text') {
options.outputFormat = 'stream-json'
}
} else if (arg === '--response-timeout' && i + 1 < args.length) {
options.responseTimeout = parseInt(args[++i], 10)
} else if (arg === '--resume' && i + 1 < args.length) {
options.resumeSession = args[++i]
}
} else if (!positionalStarted) {
positionalStarted = true
options.command = arg
} else {
options.commandArgs.push(arg)
}
}
return options
}
// ─── --output-format flag parsing ──────────────────────────────────────────
test('--output-format text sets outputFormat to text', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--output-format', 'text', 'auto'])
assert.equal(opts.outputFormat, 'text')
assert.equal(opts.json, false)
})
test('--output-format json sets outputFormat to json and json=true', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--output-format', 'json', 'auto'])
assert.equal(opts.outputFormat, 'json')
assert.equal(opts.json, true)
})
test('--output-format stream-json sets outputFormat to stream-json and json=true', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--output-format', 'stream-json', 'auto'])
assert.equal(opts.outputFormat, 'stream-json')
assert.equal(opts.json, true)
})
test('default output format is text', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', 'auto'])
assert.equal(opts.outputFormat, 'text')
assert.equal(opts.json, false)
})
test('invalid --output-format value throws', () => {
assert.throws(
() => parseHeadlessArgs(['node', 'gsd', 'headless', '--output-format', 'yaml', 'auto']),
/Invalid output format: yaml/,
)
})
test('invalid --output-format value (empty) throws', () => {
assert.throws(
() => parseHeadlessArgs(['node', 'gsd', 'headless', '--output-format', 'xml', 'auto']),
/Invalid output format/,
)
})
// ─── --json backward compatibility ─────────────────────────────────────────
test('--json is alias for --output-format stream-json', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--json', 'auto'])
assert.equal(opts.outputFormat, 'stream-json')
assert.equal(opts.json, true)
})
test('--json before --output-format json: last writer wins', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--json', '--output-format', 'json', 'auto'])
assert.equal(opts.outputFormat, 'json')
assert.equal(opts.json, true)
})
// ─── --resume flag ─────────────────────────────────────────────────────────
test('--resume parses session ID', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--resume', 'abc-123', 'auto'])
assert.equal(opts.resumeSession, 'abc-123')
assert.equal(opts.command, 'auto')
})
test('no --resume means undefined', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', 'auto'])
assert.equal(opts.resumeSession, undefined)
})
// ─── Exit code constants ───────────────────────────────────────────────────
test('EXIT_SUCCESS is 0', () => {
assert.equal(EXIT_SUCCESS, 0)
})
test('EXIT_ERROR is 1', () => {
assert.equal(EXIT_ERROR, 1)
})
test('EXIT_BLOCKED is 10', () => {
assert.equal(EXIT_BLOCKED, 10)
})
test('EXIT_CANCELLED is 11', () => {
assert.equal(EXIT_CANCELLED, 11)
})
// ─── mapStatusToExitCode ───────────────────────────────────────────────────
test('mapStatusToExitCode: success → 0', () => {
assert.equal(mapStatusToExitCode('success'), EXIT_SUCCESS)
})
test('mapStatusToExitCode: complete → 0', () => {
assert.equal(mapStatusToExitCode('complete'), EXIT_SUCCESS)
})
test('mapStatusToExitCode: error → 1', () => {
assert.equal(mapStatusToExitCode('error'), EXIT_ERROR)
})
test('mapStatusToExitCode: timeout → 1', () => {
assert.equal(mapStatusToExitCode('timeout'), EXIT_ERROR)
})
test('mapStatusToExitCode: blocked → 10', () => {
assert.equal(mapStatusToExitCode('blocked'), EXIT_BLOCKED)
})
test('mapStatusToExitCode: cancelled → 11', () => {
assert.equal(mapStatusToExitCode('cancelled'), EXIT_CANCELLED)
})
test('mapStatusToExitCode: unknown status defaults to EXIT_ERROR', () => {
assert.equal(mapStatusToExitCode('unknown'), EXIT_ERROR)
assert.equal(mapStatusToExitCode(''), EXIT_ERROR)
})
// ─── HeadlessJsonResult type shape ─────────────────────────────────────────
test('HeadlessJsonResult satisfies expected shape', () => {
// Type-level assertion: construct a valid object and verify it compiles.
// At runtime, verify all required keys exist.
const result: HeadlessJsonResult = {
status: 'success',
exitCode: 0,
duration: 12345,
cost: { total: 0.05, input_tokens: 1000, output_tokens: 500, cache_read_tokens: 200, cache_write_tokens: 100 },
toolCalls: 15,
events: 42,
}
assert.equal(result.status, 'success')
assert.equal(result.exitCode, 0)
assert.equal(typeof result.duration, 'number')
assert.ok(result.cost)
assert.equal(typeof result.cost.total, 'number')
assert.equal(typeof result.cost.input_tokens, 'number')
assert.equal(typeof result.cost.output_tokens, 'number')
assert.equal(typeof result.cost.cache_read_tokens, 'number')
assert.equal(typeof result.cost.cache_write_tokens, 'number')
assert.equal(typeof result.toolCalls, 'number')
assert.equal(typeof result.events, 'number')
})
test('HeadlessJsonResult accepts optional fields', () => {
const result: HeadlessJsonResult = {
status: 'blocked',
exitCode: 10,
sessionId: 'sess-abc',
duration: 5000,
cost: { total: 0, input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
toolCalls: 0,
events: 1,
milestone: 'M001',
phase: 'planning',
nextAction: 'fix blocker',
artifacts: ['ROADMAP.md'],
commits: ['abc1234'],
}
assert.equal(result.sessionId, 'sess-abc')
assert.equal(result.milestone, 'M001')
assert.deepEqual(result.artifacts, ['ROADMAP.md'])
assert.deepEqual(result.commits, ['abc1234'])
})
// ─── VALID_OUTPUT_FORMATS set ──────────────────────────────────────────────
test('VALID_OUTPUT_FORMATS contains exactly text, json, stream-json', () => {
assert.equal(VALID_OUTPUT_FORMATS.size, 3)
assert.ok(VALID_OUTPUT_FORMATS.has('text'))
assert.ok(VALID_OUTPUT_FORMATS.has('json'))
assert.ok(VALID_OUTPUT_FORMATS.has('stream-json'))
})
// ─── Regression: existing flags still parse correctly ──────────────────────
test('--events still works with new outputFormat default', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--events', 'agent_end,tool_execution_start', 'auto'])
assert.ok(opts.eventFilter instanceof Set)
assert.equal(opts.eventFilter!.size, 2)
assert.equal(opts.json, true)
assert.equal(opts.outputFormat, 'stream-json')
})
test('--timeout still works', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--timeout', '60000', 'auto'])
assert.equal(opts.timeout, 60000)
})
test('--supervised still works and implies stream-json', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--supervised', 'auto'])
assert.equal(opts.supervised, true)
assert.equal(opts.json, true)
assert.equal(opts.outputFormat, 'stream-json')
})
test('--answers still works', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--answers', 'answers.json', 'auto'])
assert.equal(opts.answers, 'answers.json')
})
test('positional command parsing still works', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', 'next'])
assert.equal(opts.command, 'next')
})
test('combined flags parse correctly', () => {
const opts = parseHeadlessArgs([
'node', 'gsd', 'headless',
'--output-format', 'json',
'--timeout', '120000',
'--resume', 'sess-xyz',
'--verbose',
'auto',
])
assert.equal(opts.outputFormat, 'json')
assert.equal(opts.json, true)
assert.equal(opts.timeout, 120000)
assert.equal(opts.resumeSession, 'sess-xyz')
assert.equal(opts.verbose, true)
assert.equal(opts.command, 'auto')
})