goke 6.10.0 → 6.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,7 +54,7 @@ function stripStackTrace(text: string): string {
54
54
  }
55
55
 
56
56
  describe('error formatting', () => {
57
- test('unknown option prints formatted error to stderr', () => {
57
+ test('unknown option prints formatted error to stderr', async () => {
58
58
  const stderr = createTestOutputStream()
59
59
  const cli = goke('mycli', { stderr, exit: () => {} })
60
60
 
@@ -64,13 +64,13 @@ describe('error formatting', () => {
64
64
  .action(() => {})
65
65
 
66
66
  try {
67
- cli.parse('node bin build --unknown'.split(' '))
67
+ await cli.parse('node bin build --unknown'.split(' '))
68
68
  } catch {}
69
69
 
70
70
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: Unknown option \`--unknown\`"`)
71
71
  })
72
72
 
73
- test('missing required option value prints formatted error to stderr', () => {
73
+ test('missing required option value prints formatted error to stderr', async () => {
74
74
  const stderr = createTestOutputStream()
75
75
  const cli = goke('mycli', { stderr, exit: () => {} })
76
76
 
@@ -80,26 +80,26 @@ describe('error formatting', () => {
80
80
  .action(() => {})
81
81
 
82
82
  try {
83
- cli.parse('node bin serve --port'.split(' '))
83
+ await cli.parse('node bin serve --port'.split(' '))
84
84
  } catch {}
85
85
 
86
86
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: option \`--port <port>\` value is missing"`)
87
87
  })
88
88
 
89
- test('schema coercion error prints formatted error to stderr', () => {
89
+ test('schema coercion error prints formatted error to stderr', async () => {
90
90
  const stderr = createTestOutputStream()
91
91
  const cli = goke('mycli', { stderr, exit: () => {} })
92
92
 
93
93
  cli.option('--port <port>', z.number().describe('Port'))
94
94
 
95
95
  try {
96
- cli.parse('node bin --port abc'.split(' '))
96
+ await cli.parse('node bin --port abc'.split(' '))
97
97
  } catch {}
98
98
 
99
99
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: Invalid value for --port: expected number, got "abc""`)
100
100
  })
101
101
 
102
- test('error includes help hint when help is enabled', () => {
102
+ test('error includes help hint when help is enabled', async () => {
103
103
  const stderr = createTestOutputStream()
104
104
  const cli = goke('mycli', { stderr, exit: () => {} })
105
105
 
@@ -111,7 +111,7 @@ describe('error formatting', () => {
111
111
  .action(() => {})
112
112
 
113
113
  try {
114
- cli.parse('node bin serve --port'.split(' '))
114
+ await cli.parse('node bin serve --port'.split(' '))
115
115
  } catch {}
116
116
 
117
117
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`
@@ -131,7 +131,7 @@ describe('error formatting', () => {
131
131
  throw new Error('connection refused')
132
132
  })
133
133
 
134
- cli.parse('node bin deploy'.split(' '))
134
+ await cli.parse('node bin deploy'.split(' '))
135
135
 
136
136
  // Wait for the async rejection to be handled
137
137
  await new Promise(resolve => setTimeout(resolve, 10))
@@ -140,7 +140,7 @@ describe('error formatting', () => {
140
140
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: connection refused"`)
141
141
  })
142
142
 
143
- test('error output includes stack trace', () => {
143
+ test('error output includes stack trace', async () => {
144
144
  const stderr = createTestOutputStream()
145
145
  const cli = goke('mycli', { stderr, exit: () => {} })
146
146
 
@@ -149,7 +149,7 @@ describe('error formatting', () => {
149
149
  .action(() => {})
150
150
 
151
151
  try {
152
- cli.parse('node bin build --unknown'.split(' '))
152
+ await cli.parse('node bin build --unknown'.split(' '))
153
153
  } catch {}
154
154
 
155
155
  // Verify that stderr contains "error:" prefix and a stack trace with "at" lines
@@ -161,7 +161,7 @@ describe('error formatting', () => {
161
161
  })
162
162
 
163
163
  describe('anonymous action naming', () => {
164
- test('inline anonymous function gets named after the command', () => {
164
+ test('inline anonymous function gets named after the command', async () => {
165
165
  const cli = gokeTestable('mycli')
166
166
  const cmd = cli.command('deploy', 'Deploy app')
167
167
  // Inline arrow functions passed directly to .action() have no name,
@@ -170,14 +170,14 @@ describe('anonymous action naming', () => {
170
170
  expect(cmd.commandAction!.name).toBe('command:deploy')
171
171
  })
172
172
 
173
- test('inline anonymous function on multi-word command gets full name', () => {
173
+ test('inline anonymous function on multi-word command gets full name', async () => {
174
174
  const cli = gokeTestable('mycli')
175
175
  const cmd = cli.command('db migrate', 'Run migrations')
176
176
  cmd.action(() => {})
177
177
  expect(cmd.commandAction!.name).toBe('command:db migrate')
178
178
  })
179
179
 
180
- test('named function keeps its original name', () => {
180
+ test('named function keeps its original name', async () => {
181
181
  const cli = gokeTestable('mycli')
182
182
  const cmd = cli.command('build', 'Build app')
183
183
  function myBuildAction() {}
@@ -185,7 +185,7 @@ describe('anonymous action naming', () => {
185
185
  expect(cmd.commandAction!.name).toBe('myBuildAction')
186
186
  })
187
187
 
188
- test('default command action gets "command:default" name', () => {
188
+ test('default command action gets "command:default" name', async () => {
189
189
  const cli = gokeTestable('mycli')
190
190
  const cmd = cli.command('', 'Default command')
191
191
  cmd.action(() => {})
@@ -194,6 +194,22 @@ describe('anonymous action naming', () => {
194
194
  })
195
195
 
196
196
  describe('injected fs', () => {
197
+ test('parse waits for async command actions before resolving', async () => {
198
+ const stdout = createTestOutputStream()
199
+ const cli = gokeTestable('mycli', { stdout })
200
+
201
+ cli
202
+ .command('deploy', 'Deploy app')
203
+ .action(async (options, { console }) => {
204
+ await new Promise(resolve => setTimeout(resolve, 10))
205
+ console.log('deploy complete')
206
+ })
207
+
208
+ await cli.parse(['node', 'bin', 'deploy'])
209
+
210
+ expect(stdout.text).toBe('deploy complete\n')
211
+ })
212
+
197
213
  test('command actions can use the default node fs for cli storage', async () => {
198
214
  const stdout = createTestOutputStream()
199
215
  const cli = gokeTestable('mycli', { stdout })
@@ -212,7 +228,7 @@ describe('injected fs', () => {
212
228
  console.log('saved credentials')
213
229
  })
214
230
 
215
- cli.parse(['node', 'bin', 'login', '--token', 'abc123'], { run: false })
231
+ await cli.parse(['node', 'bin', 'login', '--token', 'abc123'], { run: false })
216
232
  await cli.runMatchedCommand()
217
233
 
218
234
  expect(stdout.text).toBe('saved credentials\n')
@@ -246,7 +262,7 @@ describe('injected process context', () => {
246
262
  }))
247
263
  })
248
264
 
249
- cli.parse(['node', 'bin', 'context'], { run: false })
265
+ await cli.parse(['node', 'bin', 'context'], { run: false })
250
266
  await cli.runMatchedCommand()
251
267
 
252
268
  expect(stdout.text).toBe(
@@ -275,7 +291,7 @@ describe('injected process context', () => {
275
291
  console.log(process.env.TOKEN)
276
292
  })
277
293
 
278
- cli.parse(['node', 'bin', 'context'], { run: false })
294
+ await cli.parse(['node', 'bin', 'context'], { run: false })
279
295
  await cli.runMatchedCommand()
280
296
 
281
297
  expect(stdout.text).toBe('after\n')
@@ -283,10 +299,10 @@ describe('injected process context', () => {
283
299
  })
284
300
  })
285
301
 
286
- test('double dashes', () => {
302
+ test('double dashes', async () => {
287
303
  const cli = goke()
288
304
 
289
- const { args, options } = cli.parse([
305
+ const { args, options } = await cli.parse([
290
306
  'node',
291
307
  'bin',
292
308
  'foo',
@@ -300,14 +316,14 @@ test('double dashes', () => {
300
316
  expect(options['--']).toEqual(['npm', 'test'])
301
317
  })
302
318
 
303
- test('dot-nested options', () => {
319
+ test('dot-nested options', async () => {
304
320
  const cli = goke()
305
321
 
306
322
  cli
307
323
  .option('--externals <external>', 'Add externals')
308
324
  .option('--scale [level]', 'Scaling level')
309
325
 
310
- const { options: options1 } = cli.parse(
326
+ const { options: options1 } = await cli.parse(
311
327
  `node bin --externals.env.prod production --scale`.split(' ')
312
328
  )
313
329
  expect(options1.externals).toEqual({ env: { prod: 'production' } })
@@ -317,96 +333,96 @@ test('dot-nested options', () => {
317
333
  })
318
334
 
319
335
  describe('schema-based options', () => {
320
- test('schema coerces string to number', () => {
336
+ test('schema coerces string to number', async () => {
321
337
  const cli = goke()
322
338
 
323
339
  cli.option('--port <port>', z.number().describe('Port number'))
324
340
 
325
- const { options } = cli.parse('node bin --port 3000'.split(' '))
341
+ const { options } = await cli.parse('node bin --port 3000'.split(' '))
326
342
  expect(options.port).toBe(3000)
327
343
  expect(typeof options.port).toBe('number')
328
344
  })
329
345
 
330
- test('schema preserves string (no auto-conversion to number)', () => {
346
+ test('schema preserves string (no auto-conversion to number)', async () => {
331
347
  const cli = goke()
332
348
 
333
349
  cli.option('--id <id>', z.string().describe('ID'))
334
350
 
335
- const { options } = cli.parse('node bin --id 00123'.split(' '))
351
+ const { options } = await cli.parse('node bin --id 00123'.split(' '))
336
352
  expect(options.id).toBe('00123')
337
353
  expect(typeof options.id).toBe('string')
338
354
  })
339
355
 
340
- test('schema coerces string to integer', () => {
356
+ test('schema coerces string to integer', async () => {
341
357
  const cli = goke()
342
358
 
343
359
  cli.option('--count <count>', z.int().describe('Count'))
344
360
 
345
- const { options } = cli.parse('node bin --count 42'.split(' '))
361
+ const { options } = await cli.parse('node bin --count 42'.split(' '))
346
362
  expect(options.count).toBe(42)
347
363
  })
348
364
 
349
- test('schema parses JSON object', () => {
365
+ test('schema parses JSON object', async () => {
350
366
  const cli = goke()
351
367
 
352
368
  cli.option('--config <config>', z.looseObject({}).describe('Config'))
353
369
 
354
- const { options } = cli.parse(['node', 'bin', '--config', '{"a":1}'])
370
+ const { options } = await cli.parse(['node', 'bin', '--config', '{"a":1}'])
355
371
  expect(options.config).toEqual({ a: 1 })
356
372
  })
357
373
 
358
- test('schema parses JSON array', () => {
374
+ test('schema parses JSON array', async () => {
359
375
  const cli = goke()
360
376
 
361
377
  cli.option('--items <items>', z.array(z.unknown()).describe('Items'))
362
378
 
363
- const { options } = cli.parse(['node', 'bin', '--items', '[1,2,3]'])
379
+ const { options } = await cli.parse(['node', 'bin', '--items', '[1,2,3]'])
364
380
  expect(options.items).toEqual([1, 2, 3])
365
381
  })
366
382
 
367
- test('schema throws on invalid number', () => {
383
+ test('schema throws on invalid number', async () => {
368
384
  const cli = gokeTestable()
369
385
 
370
386
  cli.option('--port <port>', z.number().describe('Port number'))
371
387
 
372
- expect(() => cli.parse('node bin --port abc'.split(' ')))
373
- .toThrow('expected number, got "abc"')
388
+ await expect(cli.parse('node bin --port abc'.split(' ')))
389
+ .rejects.toThrow('expected number, got "abc"')
374
390
  })
375
391
 
376
- test('schema with union type ["number", "string"]', () => {
392
+ test('schema with union type ["number", "string"]', async () => {
377
393
  const cli = goke()
378
394
 
379
395
  cli.option('--val <val>', z.union([z.number(), z.string()]).describe('Value'))
380
396
 
381
- const { options: opts1 } = cli.parse('node bin --val 123'.split(' '))
397
+ const { options: opts1 } = await cli.parse('node bin --val 123'.split(' '))
382
398
  expect(opts1.val).toBe(123)
383
399
 
384
- const { options: opts2 } = cli.parse('node bin --val abc'.split(' '))
400
+ const { options: opts2 } = await cli.parse('node bin --val abc'.split(' '))
385
401
  expect(opts2.val).toBe('abc')
386
402
  })
387
403
 
388
- test('options without schema keep values as strings', () => {
404
+ test('options without schema keep values as strings', async () => {
389
405
  const cli = goke()
390
406
 
391
407
  cli.option('--port <port>', 'Port number')
392
408
 
393
409
  // Without schema, mri no longer auto-converts — value stays as string.
394
410
  // Use a schema to get typed values.
395
- const { options } = cli.parse('node bin --port 3000'.split(' '))
411
+ const { options } = await cli.parse('node bin --port 3000'.split(' '))
396
412
  expect(options.port).toBe('3000')
397
413
  expect(typeof options.port).toBe('string')
398
414
  })
399
415
 
400
- test('schema with default value', () => {
416
+ test('schema with default value', async () => {
401
417
  const cli = goke()
402
418
 
403
419
  cli.option('--port [port]', z.number().default(8080).describe('Port number'))
404
420
 
405
- const { options } = cli.parse('node bin'.split(' '))
421
+ const { options } = await cli.parse('node bin'.split(' '))
406
422
  expect(options.port).toBe(8080)
407
423
  })
408
424
 
409
- test('schema on subcommand options', () => {
425
+ test('schema on subcommand options', async () => {
410
426
  const cli = goke()
411
427
  let result: any = {}
412
428
 
@@ -418,75 +434,75 @@ describe('schema-based options', () => {
418
434
  result = options
419
435
  })
420
436
 
421
- cli.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true })
437
+ await cli.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true })
422
438
  expect(result.port).toBe(3000)
423
439
  expect(result.host).toBe('localhost')
424
440
  })
425
441
  })
426
442
 
427
443
  describe('no-schema behavior (mri no longer auto-converts)', () => {
428
- test('numeric string stays as string without schema', () => {
444
+ test('numeric string stays as string without schema', async () => {
429
445
  const cli = goke()
430
446
  cli.option('--port <port>', 'Port')
431
- const { options } = cli.parse('node bin --port 3000'.split(' '))
447
+ const { options } = await cli.parse('node bin --port 3000'.split(' '))
432
448
  expect(options.port).toBe('3000')
433
449
  })
434
450
 
435
- test('leading zeros preserved without schema', () => {
451
+ test('leading zeros preserved without schema', async () => {
436
452
  const cli = goke()
437
453
  cli.option('--id <id>', 'ID')
438
- const { options } = cli.parse('node bin --id 00123'.split(' '))
454
+ const { options } = await cli.parse('node bin --id 00123'.split(' '))
439
455
  expect(options.id).toBe('00123')
440
456
  })
441
457
 
442
- test('phone number preserved without schema', () => {
458
+ test('phone number preserved without schema', async () => {
443
459
  const cli = goke()
444
460
  cli.option('--phone <phone>', 'Phone')
445
- const { options } = cli.parse('node bin --phone +1234567890'.split(' '))
461
+ const { options } = await cli.parse('node bin --phone +1234567890'.split(' '))
446
462
  expect(options.phone).toBe('+1234567890')
447
463
  })
448
464
 
449
- test('boolean flags still work without schema', () => {
465
+ test('boolean flags still work without schema', async () => {
450
466
  const cli = goke()
451
467
  cli.option('--verbose', 'Verbose')
452
- const { options } = cli.parse('node bin --verbose'.split(' '))
468
+ const { options } = await cli.parse('node bin --verbose'.split(' '))
453
469
  expect(options.verbose).toBe(true)
454
470
  })
455
471
 
456
- test('optional value flag returns empty string when no value given', () => {
472
+ test('optional value flag returns empty string when no value given', async () => {
457
473
  // Bare `--format` is normalized from the mri `true` sentinel to `''` so
458
474
  // callers see a uniform `string | undefined` shape. `''` still lets them
459
475
  // distinguish "flag present but no value" from "flag omitted entirely".
460
476
  const cli = goke()
461
477
  cli.option('--format [fmt]', 'Format')
462
- const { options } = cli.parse('node bin --format'.split(' '))
478
+ const { options } = await cli.parse('node bin --format'.split(' '))
463
479
  expect(options.format).toBe('')
464
480
  })
465
481
 
466
- test('optional value flag returns string when value given', () => {
482
+ test('optional value flag returns string when value given', async () => {
467
483
  const cli = goke()
468
484
  cli.option('--format [fmt]', 'Format')
469
- const { options } = cli.parse('node bin --format json'.split(' '))
485
+ const { options } = await cli.parse('node bin --format json'.split(' '))
470
486
  expect(options.format).toBe('json')
471
487
  })
472
488
 
473
- test('hex string stays as string without schema', () => {
489
+ test('hex string stays as string without schema', async () => {
474
490
  const cli = goke()
475
491
  cli.option('--color <color>', 'Color')
476
- const { options } = cli.parse('node bin --color 0xff00ff'.split(' '))
492
+ const { options } = await cli.parse('node bin --color 0xff00ff'.split(' '))
477
493
  expect(options.color).toBe('0xff00ff')
478
494
  })
479
495
 
480
- test('scientific notation stays as string without schema', () => {
496
+ test('scientific notation stays as string without schema', async () => {
481
497
  const cli = goke()
482
498
  cli.option('--val <val>', 'Value')
483
- const { options } = cli.parse('node bin --val 1e10'.split(' '))
499
+ const { options } = await cli.parse('node bin --val 1e10'.split(' '))
484
500
  expect(options.val).toBe('1e10')
485
501
  })
486
502
  })
487
503
 
488
504
  describe('typical CLI usage examples', () => {
489
- test('web server CLI with typed options', () => {
505
+ test('web server CLI with typed options', async () => {
490
506
  const cli = goke('myserver')
491
507
  let config: any = {}
492
508
 
@@ -499,7 +515,7 @@ describe('typical CLI usage examples', () => {
499
515
  .option('--log', 'Enable logging')
500
516
  .action((options) => { config = options })
501
517
 
502
- cli.parse('node bin start --port 8080 --host 0.0.0.0 --workers 4 --cors'.split(' '), { run: true })
518
+ await cli.parse('node bin start --port 8080 --host 0.0.0.0 --workers 4 --cors'.split(' '), { run: true })
503
519
 
504
520
  expect(config.port).toBe(8080)
505
521
  expect(typeof config.port).toBe('number')
@@ -509,7 +525,7 @@ describe('typical CLI usage examples', () => {
509
525
  expect(config.cors).toBe(true)
510
526
  })
511
527
 
512
- test('web server CLI with defaults (no args)', () => {
528
+ test('web server CLI with defaults (no args)', async () => {
513
529
  const cli = goke('myserver')
514
530
  let config: any = {}
515
531
 
@@ -519,13 +535,13 @@ describe('typical CLI usage examples', () => {
519
535
  .option('--host [host]', z.string().default('localhost').describe('Host'))
520
536
  .action((options) => { config = options })
521
537
 
522
- cli.parse('node bin start'.split(' '), { run: true })
538
+ await cli.parse('node bin start'.split(' '), { run: true })
523
539
 
524
540
  expect(config.port).toBe(3000)
525
541
  expect(config.host).toBe('localhost')
526
542
  })
527
543
 
528
- test('database CLI with JSON config option', () => {
544
+ test('database CLI with JSON config option', async () => {
529
545
  const cli = goke('dbcli')
530
546
  let config: any = {}
531
547
 
@@ -535,13 +551,13 @@ describe('typical CLI usage examples', () => {
535
551
  .option('--dry-run', 'Preview without executing')
536
552
  .action((options) => { config = options })
537
553
 
538
- cli.parse(['node', 'bin', 'migrate', '--connection', '{"host":"localhost","port":5432}', '--dry-run'], { run: true })
554
+ await cli.parse(['node', 'bin', 'migrate', '--connection', '{"host":"localhost","port":5432}', '--dry-run'], { run: true })
539
555
 
540
556
  expect(config.connection).toEqual({ host: 'localhost', port: 5432 })
541
557
  expect(config.dryRun).toBe(true)
542
558
  })
543
559
 
544
- test('file processing CLI with positional args + typed options', () => {
560
+ test('file processing CLI with positional args + typed options', async () => {
545
561
  const cli = goke('fileproc')
546
562
  let result: any = {}
547
563
 
@@ -553,7 +569,7 @@ describe('typical CLI usage examples', () => {
553
569
  result = { input, output, ...options }
554
570
  })
555
571
 
556
- cli.parse('node bin convert photo.bmp photo.jpg --quality 85 --format jpg'.split(' '), { run: true })
572
+ await cli.parse('node bin convert photo.bmp photo.jpg --quality 85 --format jpg'.split(' '), { run: true })
557
573
 
558
574
  expect(result.input).toBe('photo.bmp')
559
575
  expect(result.output).toBe('photo.jpg')
@@ -562,7 +578,7 @@ describe('typical CLI usage examples', () => {
562
578
  expect(result.format).toBe('jpg')
563
579
  })
564
580
 
565
- test('API client CLI preserving string IDs', () => {
581
+ test('API client CLI preserving string IDs', async () => {
566
582
  const cli = goke('apicli')
567
583
  let result: any = {}
568
584
 
@@ -574,27 +590,27 @@ describe('typical CLI usage examples', () => {
574
590
  })
575
591
 
576
592
  // userId "00123" should NOT be coerced to number 123
577
- cli.parse(['node', 'bin', 'get-user', '00123', '--fields', '["name","email"]'], { run: true })
593
+ await cli.parse(['node', 'bin', 'get-user', '00123', '--fields', '["name","email"]'], { run: true })
578
594
 
579
595
  expect(result.userId).toBe('00123')
580
596
  expect(result.fields).toEqual(['name', 'email'])
581
597
  })
582
598
 
583
- test('nullable option with union type', () => {
599
+ test('nullable option with union type', async () => {
584
600
  const cli = goke()
585
601
  cli.option('--timeout <timeout>', z.nullable(z.number()).describe('Timeout'))
586
602
 
587
- const { options: opts1 } = cli.parse('node bin --timeout 5000'.split(' '))
603
+ const { options: opts1 } = await cli.parse('node bin --timeout 5000'.split(' '))
588
604
  expect(opts1.timeout).toBe(5000)
589
605
 
590
606
  // Empty string coerces to null for null type
591
- const { options: opts2 } = cli.parse(['node', 'bin', '--timeout', ''])
607
+ const { options: opts2 } = await cli.parse(['node', 'bin', '--timeout', ''])
592
608
  expect(opts2.timeout).toBe(null)
593
609
  })
594
610
  })
595
611
 
596
612
  describe('regression: oracle-found issues', () => {
597
- test('required option with schema still throws when value missing', () => {
613
+ test('required option with schema still throws when value missing', async () => {
598
614
  const cli = gokeTestable()
599
615
  let actionCalled = false
600
616
 
@@ -604,104 +620,103 @@ describe('regression: oracle-found issues', () => {
604
620
  .action(() => { actionCalled = true })
605
621
 
606
622
  // --port without a value should throw "value is missing"
607
- expect(() => {
608
- cli.parse('node bin serve --port'.split(' '), { run: true })
609
- }).toThrow('value is missing')
623
+ await expect(cli.parse('node bin serve --port'.split(' '), { run: true }))
624
+ .rejects.toThrow('value is missing')
610
625
  expect(actionCalled).toBe(false)
611
626
  })
612
627
 
613
- test('repeated flags with non-array schema throws', () => {
628
+ test('repeated flags with non-array schema throws', async () => {
614
629
  const cli = gokeTestable()
615
630
 
616
631
  cli.option('--tag <tag>', z.string().describe('Tags'))
617
632
 
618
- expect(() => cli.parse('node bin --tag foo --tag bar'.split(' ')))
619
- .toThrow('does not accept multiple values')
633
+ await expect(cli.parse('node bin --tag foo --tag bar'.split(' ')))
634
+ .rejects.toThrow('does not accept multiple values')
620
635
  })
621
636
 
622
- test('repeated flags with number schema throws', () => {
637
+ test('repeated flags with number schema throws', async () => {
623
638
  const cli = gokeTestable()
624
639
 
625
640
  cli.option('--id <id>', z.number().describe('ID'))
626
641
 
627
- expect(() => cli.parse('node bin --id 1 --id 2'.split(' ')))
628
- .toThrow('does not accept multiple values')
642
+ await expect(cli.parse('node bin --id 1 --id 2'.split(' ')))
643
+ .rejects.toThrow('does not accept multiple values')
629
644
  })
630
645
 
631
- test('repeated flags with array schema collects values', () => {
646
+ test('repeated flags with array schema collects values', async () => {
632
647
  const cli = goke()
633
648
 
634
649
  cli.option('--tag <tag>', z.array(z.string()).describe('Tags'))
635
650
 
636
- const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '))
651
+ const { options } = await cli.parse('node bin --tag foo --tag bar'.split(' '))
637
652
  expect(options.tag).toEqual(['foo', 'bar'])
638
653
  })
639
654
 
640
- test('repeated flags with array+items schema coerces each element', () => {
655
+ test('repeated flags with array+items schema coerces each element', async () => {
641
656
  const cli = goke()
642
657
 
643
658
  cli.option('--id <id>', z.array(z.number()).describe('IDs'))
644
659
 
645
- const { options } = cli.parse('node bin --id 1 --id 2 --id 3'.split(' '))
660
+ const { options } = await cli.parse('node bin --id 1 --id 2 --id 3'.split(' '))
646
661
  expect(options.id).toEqual([1, 2, 3])
647
662
  })
648
663
 
649
- test('single value with array schema wraps in array', () => {
664
+ test('single value with array schema wraps in array', async () => {
650
665
  const cli = goke()
651
666
 
652
667
  cli.option('--tag <tag>', z.array(z.string()).describe('Tags'))
653
668
 
654
- const { options } = cli.parse('node bin --tag foo'.split(' '))
669
+ const { options } = await cli.parse('node bin --tag foo'.split(' '))
655
670
  expect(options.tag).toEqual(['foo'])
656
671
  })
657
672
 
658
- test('single value with array+number items schema wraps and coerces', () => {
673
+ test('single value with array+number items schema wraps and coerces', async () => {
659
674
  const cli = goke()
660
675
 
661
676
  cli.option('--id <id>', z.array(z.number()).describe('IDs'))
662
677
 
663
- const { options } = cli.parse('node bin --id 42'.split(' '))
678
+ const { options } = await cli.parse('node bin --id 42'.split(' '))
664
679
  expect(options.id).toEqual([42])
665
680
  })
666
681
 
667
- test('JSON array string with array schema parses correctly', () => {
682
+ test('JSON array string with array schema parses correctly', async () => {
668
683
  const cli = goke()
669
684
 
670
685
  cli.option('--ids <ids>', z.array(z.number()).describe('IDs'))
671
686
 
672
- const { options } = cli.parse(['node', 'bin', '--ids', '[1,2,3]'])
687
+ const { options } = await cli.parse(['node', 'bin', '--ids', '[1,2,3]'])
673
688
  expect(options.ids).toEqual([1, 2, 3])
674
689
  })
675
690
 
676
- test('repeated flags without schema still produce array (no schema = no restriction)', () => {
691
+ test('repeated flags without schema still produce array (no schema = no restriction)', async () => {
677
692
  const cli = goke()
678
693
 
679
694
  cli.option('--tag <tag>', 'Tags')
680
695
 
681
- const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '))
696
+ const { options } = await cli.parse('node bin --tag foo --tag bar'.split(' '))
682
697
  expect(options.tag).toEqual(['foo', 'bar'])
683
698
  })
684
699
 
685
- test('repeated optional value option without schema produces array', () => {
700
+ test('repeated optional value option without schema produces array', async () => {
686
701
  const cli = goke()
687
702
 
688
703
  cli.option('--tag [tag]', 'Tags')
689
704
 
690
- const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '))
705
+ const { options } = await cli.parse('node bin --tag foo --tag bar'.split(' '))
691
706
  expect(options.tag).toEqual(['foo', 'bar'])
692
707
  })
693
708
 
694
- test('repeated alias option without schema produces array', () => {
709
+ test('repeated alias option without schema produces array', async () => {
695
710
  const cli = goke()
696
711
 
697
712
  cli.option('-t, --tag <tag>', 'Tags')
698
713
 
699
- const { options } = cli.parse('node bin -t foo -t bar -t baz'.split(' '))
714
+ const { options } = await cli.parse('node bin -t foo -t bar -t baz'.split(' '))
700
715
  expect(options.tag).toEqual(['foo', 'bar', 'baz'])
701
716
  expect(options.t).toEqual(['foo', 'bar', 'baz'])
702
717
  })
703
718
 
704
- test('repeated option without schema on subcommand produces array', () => {
719
+ test('repeated option without schema on subcommand produces array', async () => {
705
720
  const cli = goke()
706
721
  let result: any = {}
707
722
 
@@ -710,34 +725,34 @@ describe('regression: oracle-found issues', () => {
710
725
  .option('--exclude <path>', 'Paths to exclude')
711
726
  .action((options) => { result = options })
712
727
 
713
- cli.parse('node bin build --exclude node_modules --exclude dist --exclude .git'.split(' '), { run: true })
728
+ await cli.parse('node bin build --exclude node_modules --exclude dist --exclude .git'.split(' '), { run: true })
714
729
  expect(result.exclude).toEqual(['node_modules', 'dist', '.git'])
715
730
  })
716
731
 
717
- test('single value without schema stays as string (not wrapped in array)', () => {
732
+ test('single value without schema stays as string (not wrapped in array)', async () => {
718
733
  const cli = goke()
719
734
 
720
735
  cli.option('--tag <tag>', 'Tags')
721
736
 
722
- const { options } = cli.parse('node bin --tag foo'.split(' '))
737
+ const { options } = await cli.parse('node bin --tag foo'.split(' '))
723
738
  expect(options.tag).toBe('foo')
724
739
  })
725
740
 
726
- test('const null coercion works', () => {
741
+ test('const null coercion works', async () => {
727
742
  expect(coerceBySchema('', { const: null }, 'val')).toBe(null)
728
743
  })
729
744
 
730
- test('optional value option with schema returns undefined when no value given', () => {
745
+ test('optional value option with schema returns undefined when no value given', async () => {
731
746
  const cli = goke()
732
747
 
733
748
  cli.option('--count [count]', z.number().describe('Count'))
734
749
 
735
750
  // --count without value → schema expects number, none given → undefined
736
- const { options } = cli.parse('node bin --count'.split(' '))
751
+ const { options } = await cli.parse('node bin --count'.split(' '))
737
752
  expect(options.count).toBe(undefined)
738
753
  })
739
754
 
740
- test('optional value option without schema normalizes bare flag to empty string', () => {
755
+ test('optional value option without schema normalizes bare flag to empty string', async () => {
741
756
  const cli = goke()
742
757
 
743
758
  cli.option('--count [count]', 'Count')
@@ -748,31 +763,31 @@ describe('regression: oracle-found issues', () => {
748
763
  // - (omitted) → undefined (flag absent)
749
764
  // This lets callers use a single `typeof options.count === 'string'`
750
765
  // check and distinguish the three cases via `=== ''` if they need to.
751
- const { options } = cli.parse('node bin --count'.split(' '))
766
+ const { options } = await cli.parse('node bin --count'.split(' '))
752
767
  expect(options.count).toBe('')
753
768
  })
754
769
 
755
- test('optional value option with schema coerces when value given', () => {
770
+ test('optional value option with schema coerces when value given', async () => {
756
771
  const cli = goke()
757
772
 
758
773
  cli.option('--count [count]', z.number().describe('Count'))
759
774
 
760
- const { options } = cli.parse('node bin --count 42'.split(' '))
775
+ const { options } = await cli.parse('node bin --count 42'.split(' '))
761
776
  expect(options.count).toBe(42)
762
777
  })
763
778
 
764
- test('optional value option with schema default returns default when omitted', () => {
779
+ test('optional value option with schema default returns default when omitted', async () => {
765
780
  // `z.number().default(30)` has input `number | undefined` → output `number`,
766
781
  // so goke marks this option as effectively required and must surface the
767
782
  // default value at runtime when the flag is omitted.
768
783
  const cli = goke()
769
784
  cli.option('--limit [n]', z.number().default(30).describe('Max items'))
770
785
 
771
- const { options } = cli.parse('node bin'.split(' '))
786
+ const { options } = await cli.parse('node bin'.split(' '))
772
787
  expect(options.limit).toBe(30)
773
788
  })
774
789
 
775
- test('optional value option with schema default returns default when passed bare', () => {
790
+ test('optional value option with schema default returns default when passed bare', async () => {
776
791
  // Bare `--limit` is mri's "flag present, no value" sentinel. Without a
777
792
  // default, goke replaces it with `undefined`. With a default, goke must
778
793
  // preserve the preset default value instead of clobbering it, so the
@@ -781,19 +796,19 @@ describe('regression: oracle-found issues', () => {
781
796
  const cli = goke()
782
797
  cli.option('--limit [n]', z.number().default(30).describe('Max items'))
783
798
 
784
- const { options } = cli.parse('node bin --limit'.split(' '))
799
+ const { options } = await cli.parse('node bin --limit'.split(' '))
785
800
  expect(options.limit).toBe(30)
786
801
  })
787
802
 
788
- test('optional value option with schema default coerces explicit value', () => {
803
+ test('optional value option with schema default coerces explicit value', async () => {
789
804
  const cli = goke()
790
805
  cli.option('--limit [n]', z.number().default(30).describe('Max items'))
791
806
 
792
- const { options } = cli.parse('node bin --limit 5'.split(' '))
807
+ const { options } = await cli.parse('node bin --limit 5'.split(' '))
793
808
  expect(options.limit).toBe(5)
794
809
  })
795
810
 
796
- test('multiple optional options with defaults all preserve their defaults', () => {
811
+ test('multiple optional options with defaults all preserve their defaults', async () => {
797
812
  // Regression test for the runtime-overwrite bug: when several schema-backed
798
813
  // optional flags have defaults, passing one bare should not clobber the
799
814
  // others, and the bare one should keep its own default.
@@ -803,65 +818,65 @@ describe('regression: oracle-found issues', () => {
803
818
  .option('--sort [mode]', z.enum(['asc', 'desc']).default('asc'))
804
819
  .option('--host [host]', z.string().default('localhost'))
805
820
 
806
- const { options } = cli.parse('node bin --sort'.split(' '))
821
+ const { options } = await cli.parse('node bin --sort'.split(' '))
807
822
  expect(options.limit).toBe(30)
808
823
  expect(options.sort).toBe('asc')
809
824
  expect(options.host).toBe('localhost')
810
825
  })
811
826
 
812
- test('alias + schema coercion works', () => {
827
+ test('alias + schema coercion works', async () => {
813
828
  const cli = goke()
814
829
 
815
830
  cli.option('-p, --port <port>', z.number().describe('Port'))
816
831
 
817
- const { options } = cli.parse('node bin -p 3000'.split(' '))
832
+ const { options } = await cli.parse('node bin -p 3000'.split(' '))
818
833
  expect(options.port).toBe(3000)
819
834
  expect(options.p).toBe(3000)
820
835
  })
821
836
 
822
- test('union type ["array", "null"] with repeated flags', () => {
837
+ test('union type ["array", "null"] with repeated flags', async () => {
823
838
  const cli = goke()
824
839
 
825
840
  cli.option('--tags <tags>', z.nullable(z.array(z.string())).describe('Tags'))
826
841
 
827
- const { options } = cli.parse('node bin --tags foo --tags bar'.split(' '))
842
+ const { options } = await cli.parse('node bin --tags foo --tags bar'.split(' '))
828
843
  expect(options.tags).toEqual(['foo', 'bar'])
829
844
  })
830
845
  })
831
846
 
832
847
  describe('edge cases: schema + defaults interaction', () => {
833
- test('default value from schema is used when option not passed', () => {
848
+ test('default value from schema is used when option not passed', async () => {
834
849
  const cli = goke()
835
850
 
836
851
  cli.option('--port [port]', z.number().default(8080).describe('Port'))
837
852
 
838
- const { options } = cli.parse('node bin'.split(' '))
853
+ const { options } = await cli.parse('node bin'.split(' '))
839
854
  expect(options.port).toBe(8080)
840
855
  })
841
856
 
842
- test('default value is used when option not passed, schema value when passed', () => {
857
+ test('default value is used when option not passed, schema value when passed', async () => {
843
858
  const cli = goke()
844
859
 
845
860
  cli.option('--port [port]', z.number().default(8080).describe('Port'))
846
861
 
847
- const { options: opts1 } = cli.parse('node bin'.split(' '))
862
+ const { options: opts1 } = await cli.parse('node bin'.split(' '))
848
863
  expect(opts1.port).toBe(8080)
849
864
 
850
- const { options: opts2 } = cli.parse('node bin --port 3000'.split(' '))
865
+ const { options: opts2 } = await cli.parse('node bin --port 3000'.split(' '))
851
866
  expect(opts2.port).toBe(3000)
852
867
  })
853
868
 
854
- test('optional value + default + schema: three-way interaction', () => {
869
+ test('optional value + default + schema: three-way interaction', async () => {
855
870
  const cli = goke()
856
871
 
857
872
  cli.option('--count [count]', z.number().default(10).describe('Count'))
858
873
 
859
874
  // Not passed at all → default
860
- const { options: opts1 } = cli.parse('node bin'.split(' '))
875
+ const { options: opts1 } = await cli.parse('node bin'.split(' '))
861
876
  expect(opts1.count).toBe(10)
862
877
 
863
878
  // Passed with value → coerced
864
- const { options: opts2 } = cli.parse('node bin --count 42'.split(' '))
879
+ const { options: opts2 } = await cli.parse('node bin --count 42'.split(' '))
865
880
  expect(opts2.count).toBe(42)
866
881
 
867
882
  // Passed without value → default preserved. Before goke 6.7.0 this test
@@ -869,109 +884,109 @@ describe('edge cases: schema + defaults interaction', () => {
869
884
  // preset default. With the HasSchemaDefault type inference, the runtime
870
885
  // must keep the default so that the type-level promise ("options.count
871
886
  // is always a number") holds for all three input states.
872
- const { options: opts3 } = cli.parse('node bin --count'.split(' '))
887
+ const { options: opts3 } = await cli.parse('node bin --count'.split(' '))
873
888
  expect(opts3.count).toBe(10)
874
889
  })
875
890
  })
876
891
 
877
892
  describe('edge cases: boolean flags + schema', () => {
878
- test('boolean flag (no brackets) with number schema — mri returns boolean', () => {
893
+ test('boolean flag (no brackets) with number schema — mri returns boolean', async () => {
879
894
  const cli = goke()
880
895
 
881
896
  // This is a questionable usage: boolean flag + number schema
882
897
  // mri returns true/false for boolean flags, schema tries to coerce boolean→number
883
898
  cli.option('--verbose', z.number().describe('Verbose'))
884
899
 
885
- const { options } = cli.parse('node bin --verbose'.split(' '))
900
+ const { options } = await cli.parse('node bin --verbose'.split(' '))
886
901
  // Boolean true → coerced to 1 by number schema
887
902
  expect(options.verbose).toBe(1)
888
903
  })
889
904
 
890
- test('boolean string value with boolean schema on value option', () => {
905
+ test('boolean string value with boolean schema on value option', async () => {
891
906
  const cli = goke()
892
907
 
893
908
  cli.option('--flag <flag>', z.boolean().describe('A flag'))
894
909
 
895
- const { options: opts1 } = cli.parse('node bin --flag true'.split(' '))
910
+ const { options: opts1 } = await cli.parse('node bin --flag true'.split(' '))
896
911
  expect(opts1.flag).toBe(true)
897
912
 
898
- const { options: opts2 } = cli.parse('node bin --flag false'.split(' '))
913
+ const { options: opts2 } = await cli.parse('node bin --flag false'.split(' '))
899
914
  expect(opts2.flag).toBe(false)
900
915
  })
901
916
 
902
- test('invalid boolean string with boolean schema throws', () => {
917
+ test('invalid boolean string with boolean schema throws', async () => {
903
918
  const cli = gokeTestable()
904
919
 
905
920
  cli.option('--flag <flag>', z.boolean().describe('A flag'))
906
921
 
907
- expect(() => cli.parse('node bin --flag yes'.split(' ')))
908
- .toThrow('expected true or false')
922
+ await expect(cli.parse('node bin --flag yes'.split(' ')))
923
+ .rejects.toThrow('expected true or false')
909
924
  })
910
925
  })
911
926
 
912
927
  describe('edge cases: dot-nested options + schema', () => {
913
- test('dot-nested option with number schema coerces value', () => {
928
+ test('dot-nested option with number schema coerces value', async () => {
914
929
  const cli = goke()
915
930
 
916
931
  cli.option('--config.port <port>', z.number().describe('Port'))
917
932
 
918
- const { options } = cli.parse('node bin --config.port 3000'.split(' '))
933
+ const { options } = await cli.parse('node bin --config.port 3000'.split(' '))
919
934
  expect(options.config).toEqual({ port: 3000 })
920
935
  })
921
936
 
922
- test('dot-nested default uses nested object shape', () => {
937
+ test('dot-nested default uses nested object shape', async () => {
923
938
  const cli = goke()
924
939
 
925
940
  cli.option('--config.port [port]', z.number().default(8080).describe('Port'))
926
941
 
927
- const { options } = cli.parse('node bin'.split(' '))
942
+ const { options } = await cli.parse('node bin'.split(' '))
928
943
  expect(options.config).toEqual({ port: 8080 })
929
944
  })
930
945
  })
931
946
 
932
947
  describe('edge cases: kebab-case + schema', () => {
933
- test('kebab-case option coerced via schema and accessible as camelCase', () => {
948
+ test('kebab-case option coerced via schema and accessible as camelCase', async () => {
934
949
  const cli = goke()
935
950
 
936
951
  cli.option('--max-retries <count>', z.number().describe('Max retries'))
937
952
 
938
- const { options } = cli.parse('node bin --max-retries 5'.split(' '))
953
+ const { options } = await cli.parse('node bin --max-retries 5'.split(' '))
939
954
  expect(options.maxRetries).toBe(5)
940
955
  expect(typeof options.maxRetries).toBe('number')
941
956
  })
942
957
  })
943
958
 
944
959
  describe('edge cases: empty string values', () => {
945
- test('empty string with string schema stays empty string', () => {
960
+ test('empty string with string schema stays empty string', async () => {
946
961
  const cli = goke()
947
962
 
948
963
  cli.option('--name <name>', z.string().describe('Name'))
949
964
 
950
- const { options } = cli.parse(['node', 'bin', '--name', ''])
965
+ const { options } = await cli.parse(['node', 'bin', '--name', ''])
951
966
  expect(options.name).toBe('')
952
967
  })
953
968
 
954
- test('empty string with number schema throws', () => {
969
+ test('empty string with number schema throws', async () => {
955
970
  const cli = gokeTestable()
956
971
 
957
972
  cli.option('--port <port>', z.number().describe('Port'))
958
973
 
959
- expect(() => cli.parse(['node', 'bin', '--port', '']))
960
- .toThrow('expected number, got empty string')
974
+ await expect(cli.parse(['node', 'bin', '--port', '']))
975
+ .rejects.toThrow('expected number, got empty string')
961
976
  })
962
977
 
963
- test('empty string with nullable number schema returns null', () => {
978
+ test('empty string with nullable number schema returns null', async () => {
964
979
  const cli = goke()
965
980
 
966
981
  cli.option('--timeout <timeout>', z.nullable(z.number()).describe('Timeout'))
967
982
 
968
- const { options } = cli.parse(['node', 'bin', '--timeout', ''])
983
+ const { options } = await cli.parse(['node', 'bin', '--timeout', ''])
969
984
  expect(options.timeout).toBe(null)
970
985
  })
971
986
  })
972
987
 
973
988
  describe('edge cases: global options with schema in subcommands', () => {
974
- test('global option schema applies to subcommand parsing', () => {
989
+ test('global option schema applies to subcommand parsing', async () => {
975
990
  const cli = goke()
976
991
  let result: any = {}
977
992
 
@@ -981,53 +996,53 @@ describe('edge cases: global options with schema in subcommands', () => {
981
996
  .command('serve', 'Start server')
982
997
  .action((options) => { result = options })
983
998
 
984
- cli.parse('node bin serve --port 3000'.split(' '), { run: true })
999
+ await cli.parse('node bin serve --port 3000'.split(' '), { run: true })
985
1000
  expect(result.port).toBe(3000)
986
1001
  expect(typeof result.port).toBe('number')
987
1002
  })
988
1003
  })
989
1004
 
990
1005
  describe('edge cases: short alias + schema', () => {
991
- test('short alias repeated with array schema', () => {
1006
+ test('short alias repeated with array schema', async () => {
992
1007
  const cli = goke()
993
1008
 
994
1009
  cli.option('-t, --tag <tag>', z.array(z.string()).describe('Tags'))
995
1010
 
996
- const { options } = cli.parse('node bin -t foo -t bar'.split(' '))
1011
+ const { options } = await cli.parse('node bin -t foo -t bar'.split(' '))
997
1012
  expect(options.tag).toEqual(['foo', 'bar'])
998
1013
  expect(options.t).toEqual(['foo', 'bar'])
999
1014
  })
1000
1015
 
1001
- test('short alias single value with array schema wraps', () => {
1016
+ test('short alias single value with array schema wraps', async () => {
1002
1017
  const cli = goke()
1003
1018
 
1004
1019
  cli.option('-t, --tag <tag>', z.array(z.string()).describe('Tags'))
1005
1020
 
1006
- const { options } = cli.parse('node bin -t foo'.split(' '))
1021
+ const { options } = await cli.parse('node bin -t foo'.split(' '))
1007
1022
  expect(options.tag).toEqual(['foo'])
1008
1023
  })
1009
1024
 
1010
- test('short alias with number schema coerces', () => {
1025
+ test('short alias with number schema coerces', async () => {
1011
1026
  const cli = goke()
1012
1027
 
1013
1028
  cli.option('-p, --port <port>', z.number().describe('Port'))
1014
1029
 
1015
- const { options } = cli.parse('node bin -p 8080'.split(' '))
1030
+ const { options } = await cli.parse('node bin -p 8080'.split(' '))
1016
1031
  expect(options.port).toBe(8080)
1017
1032
  expect(options.p).toBe(8080)
1018
1033
  })
1019
1034
 
1020
- test('short alias repeated with non-array schema throws', () => {
1035
+ test('short alias repeated with non-array schema throws', async () => {
1021
1036
  const cli = gokeTestable()
1022
1037
 
1023
1038
  cli.option('-p, --port <port>', z.number().describe('Port'))
1024
1039
 
1025
- expect(() => cli.parse('node bin -p 3000 -p 4000'.split(' ')))
1026
- .toThrow('does not accept multiple values')
1040
+ await expect(cli.parse('node bin -p 3000 -p 4000'.split(' ')))
1041
+ .rejects.toThrow('does not accept multiple values')
1027
1042
  })
1028
1043
  })
1029
1044
 
1030
- test('throw on unknown options', () => {
1045
+ test('throw on unknown options', async () => {
1031
1046
  const cli = gokeTestable()
1032
1047
 
1033
1048
  cli
@@ -1036,13 +1051,12 @@ test('throw on unknown options', () => {
1036
1051
  .option('--aB', 'ab')
1037
1052
  .action(() => {})
1038
1053
 
1039
- expect(() => {
1040
- cli.parse(`node bin build app.js --fooBar --a-b --xx`.split(' '))
1041
- }).toThrowError('Unknown option `--xx`')
1054
+ await expect(cli.parse(`node bin build app.js --fooBar --a-b --xx`.split(' ')))
1055
+ .rejects.toThrowError('Unknown option `--xx`')
1042
1056
  })
1043
1057
 
1044
1058
  describe('space-separated subcommands', () => {
1045
- test('basic subcommand matching', () => {
1059
+ test('basic subcommand matching', async () => {
1046
1060
  const cli = goke()
1047
1061
  let matched = ''
1048
1062
 
@@ -1050,12 +1064,12 @@ describe('space-separated subcommands', () => {
1050
1064
  matched = 'mcp login'
1051
1065
  })
1052
1066
 
1053
- cli.parse(['node', 'bin', 'mcp', 'login'], { run: true })
1067
+ await cli.parse(['node', 'bin', 'mcp', 'login'], { run: true })
1054
1068
  expect(matched).toBe('mcp login')
1055
1069
  expect(cli.matchedCommandName).toBe('mcp login')
1056
1070
  })
1057
1071
 
1058
- test('subcommand with positional args', () => {
1072
+ test('subcommand with positional args', async () => {
1059
1073
  const cli = goke()
1060
1074
  let receivedId = ''
1061
1075
 
@@ -1063,12 +1077,12 @@ describe('space-separated subcommands', () => {
1063
1077
  receivedId = id
1064
1078
  })
1065
1079
 
1066
- cli.parse(['node', 'bin', 'mcp', 'getNodeXml', '123'], { run: true })
1080
+ await cli.parse(['node', 'bin', 'mcp', 'getNodeXml', '123'], { run: true })
1067
1081
  expect(receivedId).toBe('123')
1068
1082
  expect(cli.matchedCommandName).toBe('mcp getNodeXml')
1069
1083
  })
1070
1084
 
1071
- test('subcommand with options', () => {
1085
+ test('subcommand with options', async () => {
1072
1086
  const cli = goke()
1073
1087
  let result: any = {}
1074
1088
 
@@ -1079,13 +1093,13 @@ describe('space-separated subcommands', () => {
1079
1093
  result = { id, format: options.format }
1080
1094
  })
1081
1095
 
1082
- cli.parse(['node', 'bin', 'mcp', 'export', 'abc', '--format', 'json'], {
1096
+ await cli.parse(['node', 'bin', 'mcp', 'export', 'abc', '--format', 'json'], {
1083
1097
  run: true,
1084
1098
  })
1085
1099
  expect(result).toEqual({ id: 'abc', format: 'json' })
1086
1100
  })
1087
1101
 
1088
- test('greedy matching - longer commands match first', () => {
1102
+ test('greedy matching - longer commands match first', async () => {
1089
1103
  const cli = goke()
1090
1104
  let matched = ''
1091
1105
 
@@ -1097,11 +1111,11 @@ describe('space-separated subcommands', () => {
1097
1111
  matched = 'mcp login'
1098
1112
  })
1099
1113
 
1100
- cli.parse(['node', 'bin', 'mcp', 'login'], { run: true })
1114
+ await cli.parse(['node', 'bin', 'mcp', 'login'], { run: true })
1101
1115
  expect(matched).toBe('mcp login')
1102
1116
  })
1103
1117
 
1104
- test('three-level subcommand', () => {
1118
+ test('three-level subcommand', async () => {
1105
1119
  const cli = goke()
1106
1120
  let matched = ''
1107
1121
 
@@ -1109,12 +1123,12 @@ describe('space-separated subcommands', () => {
1109
1123
  matched = 'git remote add'
1110
1124
  })
1111
1125
 
1112
- cli.parse(['node', 'bin', 'git', 'remote', 'add'], { run: true })
1126
+ await cli.parse(['node', 'bin', 'git', 'remote', 'add'], { run: true })
1113
1127
  expect(matched).toBe('git remote add')
1114
1128
  expect(cli.matchedCommandName).toBe('git remote add')
1115
1129
  })
1116
1130
 
1117
- test('single-word commands still work (backward compatibility)', () => {
1131
+ test('single-word commands still work (backward compatibility)', async () => {
1118
1132
  const cli = goke()
1119
1133
  let matched = ''
1120
1134
 
@@ -1122,12 +1136,12 @@ describe('space-separated subcommands', () => {
1122
1136
  matched = 'build'
1123
1137
  })
1124
1138
 
1125
- cli.parse(['node', 'bin', 'build'], { run: true })
1139
+ await cli.parse(['node', 'bin', 'build'], { run: true })
1126
1140
  expect(matched).toBe('build')
1127
1141
  expect(cli.matchedCommandName).toBe('build')
1128
1142
  })
1129
1143
 
1130
- test('subcommand does not match when args are insufficient', () => {
1144
+ test('subcommand does not match when args are insufficient', async () => {
1131
1145
  const cli = goke()
1132
1146
  let matched = ''
1133
1147
 
@@ -1139,11 +1153,11 @@ describe('space-separated subcommands', () => {
1139
1153
  matched = 'mcp base'
1140
1154
  })
1141
1155
 
1142
- cli.parse(['node', 'bin', 'mcp'], { run: true })
1156
+ await cli.parse(['node', 'bin', 'mcp'], { run: true })
1143
1157
  expect(matched).toBe('mcp base')
1144
1158
  })
1145
1159
 
1146
- test('default command should not match if args are prefix of another command', () => {
1160
+ test('default command should not match if args are prefix of another command', async () => {
1147
1161
  const cli = goke()
1148
1162
  let matched = ''
1149
1163
 
@@ -1155,12 +1169,12 @@ describe('space-separated subcommands', () => {
1155
1169
  matched = 'default'
1156
1170
  })
1157
1171
 
1158
- cli.parse(['node', 'bin', 'mcp'], { run: true })
1172
+ await cli.parse(['node', 'bin', 'mcp'], { run: true })
1159
1173
  expect(matched).toBe('')
1160
1174
  expect(cli.matchedCommand).toBeUndefined()
1161
1175
  })
1162
1176
 
1163
- test('default command should match when args do not prefix any command', () => {
1177
+ test('default command should match when args do not prefix any command', async () => {
1164
1178
  const cli = goke()
1165
1179
  let matched = ''
1166
1180
  let receivedArg = ''
@@ -1174,12 +1188,12 @@ describe('space-separated subcommands', () => {
1174
1188
  receivedArg = file
1175
1189
  })
1176
1190
 
1177
- cli.parse(['node', 'bin', 'foo'], { run: true })
1191
+ await cli.parse(['node', 'bin', 'foo'], { run: true })
1178
1192
  expect(matched).toBe('default')
1179
1193
  expect(receivedArg).toBe('foo')
1180
1194
  })
1181
1195
 
1182
- test('help output with subcommands', () => {
1196
+ test('help output with subcommands', async () => {
1183
1197
  let output = ''
1184
1198
  const cli = goke('mycli', {
1185
1199
  stdout: { write(data) { output += data } },
@@ -1194,7 +1208,7 @@ describe('space-separated subcommands', () => {
1194
1208
 
1195
1209
  cli.help()
1196
1210
  // parse with --help triggers outputHelp() internally, which writes to our captured stdout
1197
- cli.parse(['node', 'bin', '--help'], { run: false })
1211
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1198
1212
 
1199
1213
  expect(stripAnsi(output)).toMatchInlineSnapshot(`
1200
1214
  "mycli
@@ -1231,7 +1245,7 @@ describe('space-separated subcommands', () => {
1231
1245
  `)
1232
1246
  })
1233
1247
 
1234
- test('unknown subcommand shows filtered help for prefix', () => {
1248
+ test('unknown subcommand shows filtered help for prefix', async () => {
1235
1249
  let output = ''
1236
1250
  const cli = goke('mycli', {
1237
1251
  stdout: { write(data) { output += data } },
@@ -1245,7 +1259,7 @@ describe('space-separated subcommands', () => {
1245
1259
  cli.help()
1246
1260
 
1247
1261
  // User types "mcp nonexistent" - should show help for mcp commands
1248
- cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true })
1262
+ await cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true })
1249
1263
 
1250
1264
  expect(cli.matchedCommand).toBeUndefined()
1251
1265
  const normalizedOutput = stripAnsi(output)
@@ -1257,7 +1271,7 @@ describe('space-separated subcommands', () => {
1257
1271
  expect(normalizedOutput).not.toContain('build')
1258
1272
  })
1259
1273
 
1260
- test('unknown command without prefix does not show filtered help', () => {
1274
+ test('unknown command without prefix does not show filtered help', async () => {
1261
1275
  let output = ''
1262
1276
  const cli = goke('mycli', {
1263
1277
  stdout: { write(data) { output += data } },
@@ -1269,13 +1283,13 @@ describe('space-separated subcommands', () => {
1269
1283
  cli.help()
1270
1284
 
1271
1285
  // User types "foo" - no commands start with "foo"
1272
- cli.parse(['node', 'bin', 'foo'], { run: true })
1286
+ await cli.parse(['node', 'bin', 'foo'], { run: true })
1273
1287
 
1274
1288
  // Should not show filtered help since "foo" is not a prefix of any command
1275
1289
  expect(stripAnsi(output)).not.toContain('Available "foo" commands')
1276
1290
  })
1277
1291
 
1278
- test('unknown command without prefix outputs root help', () => {
1292
+ test('unknown command without prefix outputs root help', async () => {
1279
1293
  let output = ''
1280
1294
  const cli = goke('mycli', {
1281
1295
  stdout: { write(data) { output += data } },
@@ -1287,7 +1301,7 @@ describe('space-separated subcommands', () => {
1287
1301
  cli.help()
1288
1302
 
1289
1303
  // User types an unknown command that does not match any prefix group
1290
- cli.parse(['node', 'bin', 'something'], { run: true })
1304
+ await cli.parse(['node', 'bin', 'something'], { run: true })
1291
1305
 
1292
1306
  expect(cli.matchedCommand).toBeUndefined()
1293
1307
  expect(stripAnsi(output)).toContain('Usage:')
@@ -1296,7 +1310,7 @@ describe('space-separated subcommands', () => {
1296
1310
  expect(stripAnsi(output)).toContain('build')
1297
1311
  })
1298
1312
 
1299
- test('no args without default command outputs root help', () => {
1313
+ test('no args without default command outputs root help', async () => {
1300
1314
  const stdout = createTestOutputStream()
1301
1315
  const cli = goke('mycli', { stdout })
1302
1316
 
@@ -1304,7 +1318,7 @@ describe('space-separated subcommands', () => {
1304
1318
  cli.command('build', 'Build project')
1305
1319
  cli.help()
1306
1320
 
1307
- cli.parse(['node', 'bin'], { run: true })
1321
+ await cli.parse(['node', 'bin'], { run: true })
1308
1322
 
1309
1323
  expect(stdout.text).toContain('Usage:')
1310
1324
  expect(stdout.text).toContain('$ mycli <command> [options]')
@@ -1312,7 +1326,81 @@ describe('space-separated subcommands', () => {
1312
1326
  expect(stdout.text).toContain('build')
1313
1327
  })
1314
1328
 
1315
- test('prefix --help shows filtered help for matching command group', () => {
1329
+ test('default command with no args rejects unknown positional args', async () => {
1330
+ const stdout = createTestOutputStream()
1331
+ let defaultRan = false
1332
+ let unknownFired = false
1333
+ const cli = gokeTestable('playwriter', { stdout })
1334
+
1335
+ cli.command('', 'Start the MCP server').action(async () => { defaultRan = true })
1336
+ cli.command('session new', 'Create session').action(() => {})
1337
+ cli.help()
1338
+ cli.on('command:*', () => { unknownFired = true })
1339
+
1340
+ await cli.parse(['node', 'bin', 'run'], { run: true })
1341
+
1342
+ expect(defaultRan).toBe(false)
1343
+ expect(unknownFired).toBe(true)
1344
+ expect(cli.matchedCommand).toBeUndefined()
1345
+ })
1346
+
1347
+ test('default command with no args still runs when no args passed', async () => {
1348
+ let defaultRan = false
1349
+ const cli = gokeTestable('playwriter')
1350
+
1351
+ cli.command('', 'Start the MCP server').action(async () => { defaultRan = true })
1352
+ cli.command('session new', 'Create session').action(() => {})
1353
+
1354
+ await cli.parse(['node', 'bin'], { run: true })
1355
+
1356
+ expect(defaultRan).toBe(true)
1357
+ })
1358
+
1359
+ test('default command with no args still works with -- separator', async () => {
1360
+ let defaultRan = false
1361
+ let receivedOptions: any = null
1362
+ const cli = gokeTestable('playwriter')
1363
+
1364
+ cli.command('', 'Start the MCP server').action(async (options) => {
1365
+ defaultRan = true
1366
+ receivedOptions = options
1367
+ })
1368
+
1369
+ await cli.parse(['node', 'bin', '--', 'extra', 'args'], { run: true })
1370
+
1371
+ expect(defaultRan).toBe(true)
1372
+ expect(receivedOptions['--']).toEqual(['extra', 'args'])
1373
+ })
1374
+
1375
+ test('default command WITH positional args still accepts args', async () => {
1376
+ let receivedScript: string | undefined
1377
+ const cli = gokeTestable('runner')
1378
+
1379
+ cli.command('[script]', 'Run a script').action(async (script) => {
1380
+ receivedScript = script
1381
+ })
1382
+
1383
+ await cli.parse(['node', 'bin', 'deploy'], { run: true })
1384
+
1385
+ expect(receivedScript).toBe('deploy')
1386
+ })
1387
+
1388
+ test('default command rejects unknown nonexistent command', async () => {
1389
+ let defaultRan = false
1390
+ let unknownFired = false
1391
+ const cli = gokeTestable('mycli')
1392
+
1393
+ cli.command('', 'Default').action(async () => { defaultRan = true })
1394
+ cli.command('build', 'Build').action(() => {})
1395
+ cli.on('command:*', () => { unknownFired = true })
1396
+
1397
+ await cli.parse(['node', 'bin', 'nonexistent'], { run: true })
1398
+
1399
+ expect(defaultRan).toBe(false)
1400
+ expect(unknownFired).toBe(true)
1401
+ })
1402
+
1403
+ test('prefix --help shows filtered help for matching command group', async () => {
1316
1404
  let output = ''
1317
1405
  const cli = goke('mycli', {
1318
1406
  stdout: { write(data) { output += data } },
@@ -1324,7 +1412,7 @@ describe('space-separated subcommands', () => {
1324
1412
  cli.command('build', 'Build project')
1325
1413
 
1326
1414
  cli.help()
1327
- cli.parse(['node', 'bin', 'mcp', '--help'], { run: true })
1415
+ await cli.parse(['node', 'bin', 'mcp', '--help'], { run: true })
1328
1416
 
1329
1417
  const normalizedOutput = stripAnsi(output)
1330
1418
  expect(normalizedOutput).toMatchInlineSnapshot(`
@@ -1343,7 +1431,7 @@ describe('space-separated subcommands', () => {
1343
1431
  })
1344
1432
 
1345
1433
  describe('many commands with root command (empty string)', () => {
1346
- test('root command runs when no subcommand given', () => {
1434
+ test('root command runs when no subcommand given', async () => {
1347
1435
  const cli = goke('deploy')
1348
1436
  let matched = ''
1349
1437
 
@@ -1359,11 +1447,11 @@ describe('many commands with root command (empty string)', () => {
1359
1447
  matched = 'login'
1360
1448
  })
1361
1449
 
1362
- cli.parse(['node', 'bin'], { run: true })
1450
+ await cli.parse(['node', 'bin'], { run: true })
1363
1451
  expect(matched).toBe('root')
1364
1452
  })
1365
1453
 
1366
- test('root command receives options', () => {
1454
+ test('root command receives options', async () => {
1367
1455
  const cli = goke('deploy')
1368
1456
  let result: any = {}
1369
1457
 
@@ -1378,12 +1466,12 @@ describe('many commands with root command (empty string)', () => {
1378
1466
  cli.command('init', 'Initialize project').action(() => {})
1379
1467
  cli.command('login', 'Authenticate').action(() => {})
1380
1468
 
1381
- cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: true })
1469
+ await cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: true })
1382
1470
  expect(result.env).toBe('staging')
1383
1471
  expect(result.dryRun).toBe(true)
1384
1472
  })
1385
1473
 
1386
- test('root command uses defaults when no options given', () => {
1474
+ test('root command uses defaults when no options given', async () => {
1387
1475
  const cli = goke('deploy')
1388
1476
  let result: any = {}
1389
1477
 
@@ -1396,11 +1484,11 @@ describe('many commands with root command (empty string)', () => {
1396
1484
 
1397
1485
  cli.command('init', 'Initialize project').action(() => {})
1398
1486
 
1399
- cli.parse(['node', 'bin'], { run: true })
1487
+ await cli.parse(['node', 'bin'], { run: true })
1400
1488
  expect(result.env).toBe('production')
1401
1489
  })
1402
1490
 
1403
- test('subcommands take priority over root command', () => {
1491
+ test('subcommands take priority over root command', async () => {
1404
1492
  const cli = goke('deploy')
1405
1493
  let matched = ''
1406
1494
 
@@ -1420,11 +1508,11 @@ describe('many commands with root command (empty string)', () => {
1420
1508
  matched = 'status'
1421
1509
  })
1422
1510
 
1423
- cli.parse(['node', 'bin', 'status'], { run: true })
1511
+ await cli.parse(['node', 'bin', 'status'], { run: true })
1424
1512
  expect(matched).toBe('status')
1425
1513
  })
1426
1514
 
1427
- test('subcommand with args works alongside root command', () => {
1515
+ test('subcommand with args works alongside root command', async () => {
1428
1516
  const cli = goke('deploy')
1429
1517
  let rootCalled = false
1430
1518
  let logsResult: any = {}
@@ -1441,14 +1529,14 @@ describe('many commands with root command (empty string)', () => {
1441
1529
  logsResult = { deploymentId, ...options }
1442
1530
  })
1443
1531
 
1444
- cli.parse(['node', 'bin', 'logs', 'abc123', '--follow', '--lines', '50'], { run: true })
1532
+ await cli.parse(['node', 'bin', 'logs', 'abc123', '--follow', '--lines', '50'], { run: true })
1445
1533
  expect(rootCalled).toBe(false)
1446
1534
  expect(logsResult.deploymentId).toBe('abc123')
1447
1535
  expect(logsResult.follow).toBe(true)
1448
1536
  expect(logsResult.lines).toBe(50)
1449
1537
  })
1450
1538
 
1451
- test('help shows root and all subcommands', () => {
1539
+ test('help shows root and all subcommands', async () => {
1452
1540
  const stdout = createTestOutputStream()
1453
1541
  const cli = goke('deploy', { stdout })
1454
1542
 
@@ -1463,7 +1551,7 @@ describe('many commands with root command (empty string)', () => {
1463
1551
  cli.command('logs <deploymentId>', 'Stream logs for a deployment')
1464
1552
 
1465
1553
  cli.help()
1466
- cli.parse(['node', 'bin', '--help'], { run: false })
1554
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1467
1555
 
1468
1556
  expect(stdout.text).toContain('init')
1469
1557
  expect(stdout.text).toContain('login')
@@ -1474,7 +1562,7 @@ describe('many commands with root command (empty string)', () => {
1474
1562
  expect(stdout.text).toContain('Stream logs for a deployment')
1475
1563
  })
1476
1564
 
1477
- test('root help with many commands renders examples section after options', () => {
1565
+ test('root help with many commands renders examples section after options', async () => {
1478
1566
  const stdout = createTestOutputStream()
1479
1567
  const cli = goke('deploy', { stdout })
1480
1568
 
@@ -1492,7 +1580,7 @@ describe('many commands with root command (empty string)', () => {
1492
1580
  cli.command('logs <deploymentId>', 'Stream logs for a deployment')
1493
1581
 
1494
1582
  cli.help()
1495
- cli.parse(['node', 'bin', '--help'], { run: false })
1583
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1496
1584
 
1497
1585
  expect(stdout.text).toMatchInlineSnapshot(`
1498
1586
  "deploy
@@ -1534,7 +1622,7 @@ describe('many commands with root command (empty string)', () => {
1534
1622
  `)
1535
1623
  })
1536
1624
 
1537
- test('subcommand help renders command examples at the end', () => {
1625
+ test('subcommand help renders command examples at the end', async () => {
1538
1626
  const stdout = createTestOutputStream()
1539
1627
  const cli = goke('deploy', { stdout, columns: 80 })
1540
1628
 
@@ -1552,7 +1640,7 @@ describe('many commands with root command (empty string)', () => {
1552
1640
  .example('deploy logs dep_123 --follow')
1553
1641
 
1554
1642
  cli.help()
1555
- cli.parse(['node', 'bin', 'logs', '--help'], { run: false })
1643
+ await cli.parse(['node', 'bin', 'logs', '--help'], { run: false })
1556
1644
 
1557
1645
  expect(stdout.text).toMatchInlineSnapshot(`
1558
1646
  "deploy
@@ -1581,7 +1669,7 @@ describe('many commands with root command (empty string)', () => {
1581
1669
  `)
1582
1670
  })
1583
1671
 
1584
- test('root help labels default command with cli name and does not duplicate global options', () => {
1672
+ test('root help labels default command with cli name and does not duplicate global options', async () => {
1585
1673
  const stdout = createTestOutputStream()
1586
1674
  const cli = goke('deploy', { stdout })
1587
1675
 
@@ -1594,7 +1682,7 @@ describe('many commands with root command (empty string)', () => {
1594
1682
  cli.command('status', 'Show deployment status')
1595
1683
 
1596
1684
  cli.help()
1597
- cli.parse(['node', 'bin', '--help'], { run: false })
1685
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1598
1686
 
1599
1687
  expect(stdout.text).toMatchInlineSnapshot(`
1600
1688
  "deploy
@@ -1619,7 +1707,7 @@ describe('many commands with root command (empty string)', () => {
1619
1707
  `)
1620
1708
  })
1621
1709
 
1622
- test('root help wraps long command descriptions snapshot', () => {
1710
+ test('root help wraps long command descriptions snapshot', async () => {
1623
1711
  const stdout = createTestOutputStream()
1624
1712
  const cli = goke('mycli', { stdout, columns: 56 })
1625
1713
 
@@ -1636,7 +1724,7 @@ describe('many commands with root command (empty string)', () => {
1636
1724
  ).option('--id <id>', 'Notion URL or UUID to fetch')
1637
1725
 
1638
1726
  cli.help()
1639
- cli.parse(['node', 'bin', '--help'], { run: false })
1727
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1640
1728
 
1641
1729
  expect(stdout.text).toMatchInlineSnapshot(`
1642
1730
  "mycli
@@ -1673,7 +1761,7 @@ describe('many commands with root command (empty string)', () => {
1673
1761
  `)
1674
1762
  })
1675
1763
 
1676
- test('root help aligns command descriptions with mixed command lengths', () => {
1764
+ test('root help aligns command descriptions with mixed command lengths', async () => {
1677
1765
  const stdout = createTestOutputStream()
1678
1766
  const cli = goke('gtui', { stdout, columns: 120 })
1679
1767
 
@@ -1683,7 +1771,7 @@ describe('many commands with root command (empty string)', () => {
1683
1771
  cli.command('attachment get <messageId> <attachmentId>', 'Download an attachment')
1684
1772
 
1685
1773
  cli.help()
1686
- cli.parse(['node', 'bin', '--help'], { run: false })
1774
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1687
1775
 
1688
1776
  expect(stdout.text).toMatchInlineSnapshot(`
1689
1777
  "gtui
@@ -1716,7 +1804,7 @@ describe('many commands with root command (empty string)', () => {
1716
1804
  `)
1717
1805
  })
1718
1806
 
1719
- test('root help wraps all multi-line description lines', () => {
1807
+ test('root help wraps all multi-line description lines', async () => {
1720
1808
  const stdout = createTestOutputStream()
1721
1809
  const cli = goke('mycli', { stdout, columns: 64 })
1722
1810
 
@@ -1725,13 +1813,13 @@ describe('many commands with root command (empty string)', () => {
1725
1813
  'Create a new page.\n {"title":"Example"}\n {"done":true}',
1726
1814
  )
1727
1815
  cli.help()
1728
- cli.parse(['node', 'bin', '--help'], { run: false })
1816
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1729
1817
 
1730
1818
  expect(stdout.text).toContain('{"title":"Example"}')
1731
1819
  expect(stdout.text).toContain('{"done":true}')
1732
1820
  })
1733
1821
 
1734
- test('root help snapshot when columns is undefined (no wrapping fallback)', () => {
1822
+ test('root help snapshot when columns is undefined (no wrapping fallback)', async () => {
1735
1823
  const stdout = createTestOutputStream()
1736
1824
  const originalColumns = process.stdout.columns
1737
1825
 
@@ -1751,7 +1839,7 @@ describe('many commands with root command (empty string)', () => {
1751
1839
  .option('--limit [limit]', z.number().default(10).describe('Maximum number of results to return'))
1752
1840
 
1753
1841
  cli.help()
1754
- cli.parse(['node', 'bin', '--help'], { run: false })
1842
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1755
1843
 
1756
1844
  expect(stdout.text).toMatchInlineSnapshot(`
1757
1845
  "mycli
@@ -1780,7 +1868,7 @@ describe('many commands with root command (empty string)', () => {
1780
1868
  }
1781
1869
  })
1782
1870
 
1783
- test('many subcommands all resolve correctly', () => {
1871
+ test('many subcommands all resolve correctly', async () => {
1784
1872
  const cli = goke('deploy')
1785
1873
  let matched = ''
1786
1874
 
@@ -1794,47 +1882,47 @@ describe('many commands with root command (empty string)', () => {
1794
1882
  cli.command('config set <key> <value>', 'Set config').action(() => { matched = 'config set' })
1795
1883
 
1796
1884
  // Test each command resolves to the right one
1797
- cli.parse(['node', 'bin'], { run: true })
1885
+ await cli.parse(['node', 'bin'], { run: true })
1798
1886
  expect(matched).toBe('root')
1799
1887
 
1800
1888
  matched = ''
1801
- cli.parse(['node', 'bin', 'init'], { run: true })
1889
+ await cli.parse(['node', 'bin', 'init'], { run: true })
1802
1890
  expect(matched).toBe('init')
1803
1891
 
1804
1892
  matched = ''
1805
- cli.parse(['node', 'bin', 'login'], { run: true })
1893
+ await cli.parse(['node', 'bin', 'login'], { run: true })
1806
1894
  expect(matched).toBe('login')
1807
1895
 
1808
1896
  matched = ''
1809
- cli.parse(['node', 'bin', 'logout'], { run: true })
1897
+ await cli.parse(['node', 'bin', 'logout'], { run: true })
1810
1898
  expect(matched).toBe('logout')
1811
1899
 
1812
1900
  matched = ''
1813
- cli.parse(['node', 'bin', 'status'], { run: true })
1901
+ await cli.parse(['node', 'bin', 'status'], { run: true })
1814
1902
  expect(matched).toBe('status')
1815
1903
 
1816
1904
  matched = ''
1817
- cli.parse(['node', 'bin', 'logs', 'dep-123'], { run: true })
1905
+ await cli.parse(['node', 'bin', 'logs', 'dep-123'], { run: true })
1818
1906
  expect(matched).toBe('logs')
1819
1907
 
1820
1908
  matched = ''
1821
- cli.parse(['node', 'bin', 'rollback', 'dep-456'], { run: true })
1909
+ await cli.parse(['node', 'bin', 'rollback', 'dep-456'], { run: true })
1822
1910
  expect(matched).toBe('rollback')
1823
1911
 
1824
1912
  matched = ''
1825
- cli.parse(['node', 'bin', 'config', 'set', 'region', 'us-east-1'], { run: true })
1913
+ await cli.parse(['node', 'bin', 'config', 'set', 'region', 'us-east-1'], { run: true })
1826
1914
  expect(matched).toBe('config set')
1827
1915
  })
1828
1916
  })
1829
1917
 
1830
1918
  describe('stdout/stderr/argv injection', () => {
1831
- test('stdout captures help output', () => {
1919
+ test('stdout captures help output', async () => {
1832
1920
  const stdout = createTestOutputStream()
1833
1921
  const cli = goke('mycli', { stdout })
1834
1922
 
1835
1923
  cli.command('serve', 'Start server')
1836
1924
  cli.help()
1837
- cli.parse(['node', 'bin', '--help'], { run: false })
1925
+ await cli.parse(['node', 'bin', '--help'], { run: false })
1838
1926
  cli.outputHelp()
1839
1927
 
1840
1928
  expect(stdout.text).toContain('mycli')
@@ -1842,18 +1930,18 @@ describe('stdout/stderr/argv injection', () => {
1842
1930
  expect(stdout.text).toContain('Start server')
1843
1931
  })
1844
1932
 
1845
- test('stdout captures version output', () => {
1933
+ test('stdout captures version output', async () => {
1846
1934
  const stdout = createTestOutputStream()
1847
1935
  const cli = goke('mycli', { stdout })
1848
1936
 
1849
1937
  cli.version('1.2.3')
1850
- cli.parse(['node', 'bin', '--version'], { run: false })
1938
+ await cli.parse(['node', 'bin', '--version'], { run: false })
1851
1939
  cli.outputVersion()
1852
1940
 
1853
1941
  expect(stdout.text).toContain('mycli/1.2.3')
1854
1942
  })
1855
1943
 
1856
- test('stdout captures prefix help for unknown subcommands', () => {
1944
+ test('stdout captures prefix help for unknown subcommands', async () => {
1857
1945
  const stdout = createTestOutputStream()
1858
1946
  const cli = goke('mycli', { stdout })
1859
1947
 
@@ -1861,14 +1949,14 @@ describe('stdout/stderr/argv injection', () => {
1861
1949
  cli.command('mcp logout', 'Logout from MCP')
1862
1950
  cli.help()
1863
1951
 
1864
- cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true })
1952
+ await cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true })
1865
1953
 
1866
1954
  expect(stdout.text).toContain('Unknown command: mcp nonexistent')
1867
1955
  expect(stdout.text).toContain('mcp login')
1868
1956
  expect(stdout.text).toContain('mcp logout')
1869
1957
  })
1870
1958
 
1871
- test('stderr is separate from stdout', () => {
1959
+ test('stderr is separate from stdout', async () => {
1872
1960
  const stdout = createTestOutputStream()
1873
1961
  const stderr = createTestOutputStream()
1874
1962
  const cli = goke('mycli', { stdout, stderr })
@@ -1880,7 +1968,7 @@ describe('stdout/stderr/argv injection', () => {
1880
1968
  expect(stderr.text).toBe('hello stderr\n')
1881
1969
  })
1882
1970
 
1883
- test('argv option is used as default in parse()', () => {
1971
+ test('argv option is used as default in parse()', async () => {
1884
1972
  const cli = goke('mycli', {
1885
1973
  argv: ['node', 'bin', 'serve', '--port', '3000'],
1886
1974
  })
@@ -1892,12 +1980,12 @@ describe('stdout/stderr/argv injection', () => {
1892
1980
  .action((options) => { result = options })
1893
1981
 
1894
1982
  // parse() without args uses the injected argv
1895
- cli.parse()
1983
+ await cli.parse()
1896
1984
 
1897
1985
  expect(result.port).toBe(3000)
1898
1986
  })
1899
1987
 
1900
- test('parse(customArgv) overrides injected argv', () => {
1988
+ test('parse(customArgv) overrides injected argv', async () => {
1901
1989
  const cli = goke('mycli', {
1902
1990
  argv: ['node', 'bin', 'serve', '--port', '3000'],
1903
1991
  })
@@ -1909,12 +1997,12 @@ describe('stdout/stderr/argv injection', () => {
1909
1997
  .action((options) => { result = options })
1910
1998
 
1911
1999
  // Explicit argv overrides the default
1912
- cli.parse(['node', 'bin', 'serve', '--port', '8080'])
2000
+ await cli.parse(['node', 'bin', 'serve', '--port', '8080'])
1913
2001
 
1914
2002
  expect(result.port).toBe(8080)
1915
2003
  })
1916
2004
 
1917
- test('default behavior without options uses process.stdout', () => {
2005
+ test('default behavior without options uses process.stdout', async () => {
1918
2006
  const cli = goke('mycli')
1919
2007
 
1920
2008
  // stdout/stderr should be process.stdout/process.stderr by default
@@ -1922,7 +2010,7 @@ describe('stdout/stderr/argv injection', () => {
1922
2010
  expect(cli.stderr).toBe(process.stderr)
1923
2011
  })
1924
2012
 
1925
- test('createConsole routes log to stdout and error to stderr', () => {
2013
+ test('createConsole routes log to stdout and error to stderr', async () => {
1926
2014
  const stdout = createTestOutputStream()
1927
2015
  const stderr = createTestOutputStream()
1928
2016
  const con = createConsole(stdout, stderr)
@@ -1934,7 +2022,7 @@ describe('stdout/stderr/argv injection', () => {
1934
2022
  expect(stderr.text).toBe('err1 err2\n')
1935
2023
  })
1936
2024
 
1937
- test('createConsole log with no args writes empty line', () => {
2025
+ test('createConsole log with no args writes empty line', async () => {
1938
2026
  const stdout = createTestOutputStream()
1939
2027
  const stderr = createTestOutputStream()
1940
2028
  const con = createConsole(stdout, stderr)
@@ -1946,7 +2034,7 @@ describe('stdout/stderr/argv injection', () => {
1946
2034
  })
1947
2035
 
1948
2036
  describe('schema description and default extraction', () => {
1949
- test('description is extracted from schema and shown in help', () => {
2037
+ test('description is extracted from schema and shown in help', async () => {
1950
2038
  const stdout = createTestOutputStream()
1951
2039
  const cli = goke('mycli', { stdout })
1952
2040
 
@@ -1955,12 +2043,12 @@ describe('schema description and default extraction', () => {
1955
2043
  .option('--port <port>', z.number().describe('Port to listen on'))
1956
2044
 
1957
2045
  cli.help()
1958
- cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
2046
+ await cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
1959
2047
 
1960
2048
  expect(stdout.text).toContain('Port to listen on')
1961
2049
  })
1962
2050
 
1963
- test('default is extracted from schema and shown in help', () => {
2051
+ test('default is extracted from schema and shown in help', async () => {
1964
2052
  const stdout = createTestOutputStream()
1965
2053
  const cli = goke('mycli', { stdout })
1966
2054
 
@@ -1969,12 +2057,12 @@ describe('schema description and default extraction', () => {
1969
2057
  .option('--port [port]', z.number().default(3000).describe('Port'))
1970
2058
 
1971
2059
  cli.help()
1972
- cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
2060
+ await cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
1973
2061
 
1974
2062
  expect(stdout.text).toContain('(default: 3000)')
1975
2063
  })
1976
2064
 
1977
- test('deprecated options are hidden from help output', () => {
2065
+ test('deprecated options are hidden from help output', async () => {
1978
2066
  const stdout = createTestOutputStream()
1979
2067
  const cli = goke('mycli', { stdout })
1980
2068
 
@@ -1984,7 +2072,7 @@ describe('schema description and default extraction', () => {
1984
2072
  .option('--new <value>', z.string().describe('Normal option'))
1985
2073
 
1986
2074
  cli.help()
1987
- cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
2075
+ await cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
1988
2076
 
1989
2077
  // Normal option should be visible
1990
2078
  expect(stdout.text).toContain('--new')
@@ -1994,7 +2082,7 @@ describe('schema description and default extraction', () => {
1994
2082
  expect(stdout.text).not.toContain('Old option')
1995
2083
  })
1996
2084
 
1997
- test('deprecated option still works for parsing (just hidden from help)', () => {
2085
+ test('deprecated option still works for parsing (just hidden from help)', async () => {
1998
2086
  const cli = gokeTestable('mycli')
1999
2087
 
2000
2088
  let result: any = {}
@@ -2003,13 +2091,13 @@ describe('schema description and default extraction', () => {
2003
2091
  .option('--old <value>', z.string().meta({ deprecated: true, description: 'Old option' }))
2004
2092
  .action((options) => { result = options })
2005
2093
 
2006
- cli.parse(['node', 'bin', 'serve', '--old', 'legacy-value'])
2094
+ await cli.parse(['node', 'bin', 'serve', '--old', 'legacy-value'])
2007
2095
 
2008
2096
  // Deprecated option should still be parsed and usable
2009
2097
  expect(result.old).toBe('legacy-value')
2010
2098
  })
2011
2099
 
2012
- test('deprecated options hidden from global help', () => {
2100
+ test('deprecated options hidden from global help', async () => {
2013
2101
  const stdout = createTestOutputStream()
2014
2102
  const cli = goke('mycli', { stdout })
2015
2103
 
@@ -2017,7 +2105,7 @@ describe('schema description and default extraction', () => {
2017
2105
  cli.option('--current [value]', z.string().describe('Current option'))
2018
2106
 
2019
2107
  cli.help()
2020
- cli.parse(['node', 'bin', '--help'], { run: false })
2108
+ await cli.parse(['node', 'bin', '--help'], { run: false })
2021
2109
 
2022
2110
  expect(stdout.text).toContain('--current')
2023
2111
  expect(stdout.text).toContain('Current option')
@@ -2025,7 +2113,7 @@ describe('schema description and default extraction', () => {
2025
2113
  expect(stdout.text).not.toContain('Deprecated global')
2026
2114
  })
2027
2115
 
2028
- test('hidden commands are not shown in help output', () => {
2116
+ test('hidden commands are not shown in help output', async () => {
2029
2117
  const stdout = createTestOutputStream()
2030
2118
  const cli = goke('mycli', { stdout })
2031
2119
 
@@ -2033,7 +2121,7 @@ describe('schema description and default extraction', () => {
2033
2121
  cli.command('secret', 'A hidden command').hidden()
2034
2122
 
2035
2123
  cli.help()
2036
- cli.parse(['node', 'bin', '--help'], { run: false })
2124
+ await cli.parse(['node', 'bin', '--help'], { run: false })
2037
2125
 
2038
2126
  expect(stdout.text).toContain('visible')
2039
2127
  expect(stdout.text).toContain('A visible command')
@@ -2041,7 +2129,7 @@ describe('schema description and default extraction', () => {
2041
2129
  expect(stdout.text).not.toContain('A hidden command')
2042
2130
  })
2043
2131
 
2044
- test('hidden command still parses and runs', () => {
2132
+ test('hidden command still parses and runs', async () => {
2045
2133
  const cli = gokeTestable('mycli')
2046
2134
 
2047
2135
  let result: any = {}
@@ -2051,14 +2139,14 @@ describe('schema description and default extraction', () => {
2051
2139
  .option('--value <v>', z.string().describe('some value'))
2052
2140
  .action((options) => { result = options })
2053
2141
 
2054
- cli.parse(['node', 'bin', 'secret', '--value', 'hello'])
2142
+ await cli.parse(['node', 'bin', 'secret', '--value', 'hello'])
2055
2143
 
2056
2144
  expect(result.value).toBe('hello')
2057
2145
  })
2058
2146
  })
2059
2147
 
2060
2148
  describe('helpText()', () => {
2061
- test('returns help string without printing', () => {
2149
+ test('returns help string without printing', async () => {
2062
2150
  const stdout = createTestOutputStream()
2063
2151
  const cli = goke('mycli', { stdout })
2064
2152
 
@@ -2066,7 +2154,7 @@ describe('helpText()', () => {
2066
2154
  cli.option('--port <port>', 'Port number')
2067
2155
  cli.help()
2068
2156
  // parse a known command so help is not auto-triggered
2069
- cli.parse(['node', 'bin', 'serve'], { run: false })
2157
+ await cli.parse(['node', 'bin', 'serve'], { run: false })
2070
2158
 
2071
2159
  // reset stdout after parse
2072
2160
  stdout.lines.length = 0
@@ -2081,7 +2169,7 @@ describe('helpText()', () => {
2081
2169
  expect(stdout.text).toBe('')
2082
2170
  })
2083
2171
 
2084
- test('returns same content as outputHelp', () => {
2172
+ test('returns same content as outputHelp', async () => {
2085
2173
  const stdout = createTestOutputStream()
2086
2174
  const cli = goke('mycli', { stdout })
2087
2175
 
@@ -2089,7 +2177,7 @@ describe('helpText()', () => {
2089
2177
  cli.option('--watch [watch]', 'Watch mode')
2090
2178
  cli.help()
2091
2179
  // parse a known command so help is not auto-triggered
2092
- cli.parse(['node', 'bin', 'build'], { run: false })
2180
+ await cli.parse(['node', 'bin', 'build'], { run: false })
2093
2181
 
2094
2182
  // reset stdout after parse
2095
2183
  stdout.lines.length = 0
@@ -2102,14 +2190,14 @@ describe('helpText()', () => {
2102
2190
  expect(helpTextResult).toBe(outputHelpResult)
2103
2191
  })
2104
2192
 
2105
- test('returns subcommand help when command is matched', () => {
2193
+ test('returns subcommand help when command is matched', async () => {
2106
2194
  const cli = goke('mycli')
2107
2195
 
2108
2196
  cli.command('deploy <env>', 'Deploy to environment')
2109
2197
  .option('--force', 'Force deploy')
2110
2198
 
2111
2199
  cli.help()
2112
- cli.parse(['node', 'bin', 'deploy', '--help'], { run: false })
2200
+ await cli.parse(['node', 'bin', 'deploy', '--help'], { run: false })
2113
2201
 
2114
2202
  const text = stripAnsi(cli.helpText())
2115
2203
 
@@ -2118,7 +2206,7 @@ describe('helpText()', () => {
2118
2206
  expect(text).toContain('Force deploy')
2119
2207
  })
2120
2208
 
2121
- test('works without calling parse', () => {
2209
+ test('works without calling parse', async () => {
2122
2210
  const cli = goke('mycli')
2123
2211
 
2124
2212
  cli.command('test', 'Run tests')
@@ -2136,7 +2224,7 @@ describe('helpText()', () => {
2136
2224
  })
2137
2225
 
2138
2226
  describe('middleware', () => {
2139
- test('middleware runs before command action', () => {
2227
+ test('middleware runs before command action', async () => {
2140
2228
  const cli = goke('mycli')
2141
2229
  const order: string[] = []
2142
2230
 
@@ -2152,11 +2240,11 @@ describe('middleware', () => {
2152
2240
  order.push('action')
2153
2241
  })
2154
2242
 
2155
- cli.parse(['node', 'bin', 'build'], { run: true })
2243
+ await cli.parse(['node', 'bin', 'build'], { run: true })
2156
2244
  expect(order).toEqual(['middleware', 'action'])
2157
2245
  })
2158
2246
 
2159
- test('multiple middleware run in registration order', () => {
2247
+ test('multiple middleware run in registration order', async () => {
2160
2248
  const cli = goke('mycli')
2161
2249
  const order: string[] = []
2162
2250
 
@@ -2169,11 +2257,11 @@ describe('middleware', () => {
2169
2257
  .command('deploy', 'Deploy')
2170
2258
  .action(() => { order.push('action') })
2171
2259
 
2172
- cli.parse(['node', 'bin', 'deploy'], { run: true })
2260
+ await cli.parse(['node', 'bin', 'deploy'], { run: true })
2173
2261
  expect(order).toEqual(['mw1', 'mw2', 'mw3', 'action'])
2174
2262
  })
2175
2263
 
2176
- test('middleware receives parsed global options', () => {
2264
+ test('middleware receives parsed global options', async () => {
2177
2265
  const cli = goke('mycli')
2178
2266
  let received: any = null
2179
2267
 
@@ -2187,11 +2275,11 @@ describe('middleware', () => {
2187
2275
  .command('build', 'Build')
2188
2276
  .action(() => {})
2189
2277
 
2190
- cli.parse(['node', 'bin', 'build', '--verbose'], { run: true })
2278
+ await cli.parse(['node', 'bin', 'build', '--verbose'], { run: true })
2191
2279
  expect(received.verbose).toBe(true)
2192
2280
  })
2193
2281
 
2194
- test('middleware receives schema-coerced global options', () => {
2282
+ test('middleware receives schema-coerced global options', async () => {
2195
2283
  const cli = goke('mycli')
2196
2284
  let received: any = null
2197
2285
 
@@ -2205,7 +2293,7 @@ describe('middleware', () => {
2205
2293
  .command('serve', 'Serve')
2206
2294
  .action(() => {})
2207
2295
 
2208
- cli.parse(['node', 'bin', 'serve', '--port', '3000'], { run: true })
2296
+ await cli.parse(['node', 'bin', 'serve', '--port', '3000'], { run: true })
2209
2297
  expect(received.port).toBe(3000)
2210
2298
  expect(typeof received.port).toBe('number')
2211
2299
  })
@@ -2223,7 +2311,7 @@ describe('middleware', () => {
2223
2311
  .command('run', 'Run')
2224
2312
  .action(() => { order.push('action') })
2225
2313
 
2226
- cli.parse(['node', 'bin', 'run'], { run: true })
2314
+ await cli.parse(['node', 'bin', 'run'], { run: true })
2227
2315
 
2228
2316
  // Wait for async chain to complete
2229
2317
  await new Promise((r) => setTimeout(r, 50))
@@ -2243,14 +2331,14 @@ describe('middleware', () => {
2243
2331
  .command('deploy', 'Deploy')
2244
2332
  .action(() => {})
2245
2333
 
2246
- cli.parse(['node', 'bin', 'deploy'], { run: true })
2334
+ await cli.parse(['node', 'bin', 'deploy'], { run: true })
2247
2335
 
2248
2336
  await new Promise((r) => setTimeout(r, 10))
2249
2337
  expect(exitCode).toBe(1)
2250
2338
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware failed"`)
2251
2339
  })
2252
2340
 
2253
- test('middleware does not run with { run: false }', () => {
2341
+ test('middleware does not run with { run: false }', async () => {
2254
2342
  const cli = goke('mycli')
2255
2343
  let middlewareCalled = false
2256
2344
 
@@ -2260,11 +2348,11 @@ describe('middleware', () => {
2260
2348
  .command('build', 'Build')
2261
2349
  .action(() => {})
2262
2350
 
2263
- cli.parse(['node', 'bin', 'build'], { run: false })
2351
+ await cli.parse(['node', 'bin', 'build'], { run: false })
2264
2352
  expect(middlewareCalled).toBe(false)
2265
2353
  })
2266
2354
 
2267
- test('middleware does not run for help', () => {
2355
+ test('middleware does not run for help', async () => {
2268
2356
  const stdout = createTestOutputStream()
2269
2357
  const cli = goke('mycli', { stdout })
2270
2358
  let middlewareCalled = false
@@ -2276,11 +2364,11 @@ describe('middleware', () => {
2276
2364
  .command('build', 'Build')
2277
2365
  .action(() => {})
2278
2366
 
2279
- cli.parse(['node', 'bin', '--help'], { run: true })
2367
+ await cli.parse(['node', 'bin', '--help'], { run: true })
2280
2368
  expect(middlewareCalled).toBe(false)
2281
2369
  })
2282
2370
 
2283
- test('middleware does not run when no command matched', () => {
2371
+ test('middleware does not run when no command matched', async () => {
2284
2372
  const stdout = createTestOutputStream()
2285
2373
  const cli = goke('mycli', { stdout })
2286
2374
  let middlewareCalled = false
@@ -2292,11 +2380,11 @@ describe('middleware', () => {
2292
2380
  .command('build', 'Build')
2293
2381
  .action(() => {})
2294
2382
 
2295
- cli.parse(['node', 'bin', 'nonexistent'], { run: true })
2383
+ await cli.parse(['node', 'bin', 'nonexistent'], { run: true })
2296
2384
  expect(middlewareCalled).toBe(false)
2297
2385
  })
2298
2386
 
2299
- test('middleware runs for default command', () => {
2387
+ test('middleware runs for default command', async () => {
2300
2388
  const cli = goke('mycli')
2301
2389
  const order: string[] = []
2302
2390
 
@@ -2306,11 +2394,11 @@ describe('middleware', () => {
2306
2394
  .command('', 'Default')
2307
2395
  .action(() => { order.push('action') })
2308
2396
 
2309
- cli.parse(['node', 'bin'], { run: true })
2397
+ await cli.parse(['node', 'bin'], { run: true })
2310
2398
  expect(order).toEqual(['mw', 'action'])
2311
2399
  })
2312
2400
 
2313
- test('sync middleware error is caught and formatted', () => {
2401
+ test('sync middleware error is caught and formatted', async () => {
2314
2402
  const stderr = createTestOutputStream()
2315
2403
  let exitCode: number | undefined
2316
2404
  const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code } })
@@ -2323,13 +2411,13 @@ describe('middleware', () => {
2323
2411
  .command('deploy', 'Deploy')
2324
2412
  .action(() => {})
2325
2413
 
2326
- cli.parse(['node', 'bin', 'deploy'], { run: true })
2414
+ await cli.parse(['node', 'bin', 'deploy'], { run: true })
2327
2415
 
2328
2416
  expect(exitCode).toBe(1)
2329
2417
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware exploded"`)
2330
2418
  })
2331
2419
 
2332
- test('sync middleware error short-circuits command action', () => {
2420
+ test('sync middleware error short-circuits command action', async () => {
2333
2421
  const stderr = createTestOutputStream()
2334
2422
  const cli = goke('mycli', { stderr, exit: () => {} })
2335
2423
  let actionCalled = false
@@ -2342,7 +2430,7 @@ describe('middleware', () => {
2342
2430
  .command('build', 'Build')
2343
2431
  .action(() => { actionCalled = true })
2344
2432
 
2345
- cli.parse(['node', 'bin', 'build'], { run: true })
2433
+ await cli.parse(['node', 'bin', 'build'], { run: true })
2346
2434
 
2347
2435
  expect(actionCalled).toBe(false)
2348
2436
  })
@@ -2363,7 +2451,7 @@ describe('middleware', () => {
2363
2451
  .command('run', 'Run')
2364
2452
  .action(() => { order.push('action') })
2365
2453
 
2366
- cli.parse(['node', 'bin', 'run'], { run: true })
2454
+ await cli.parse(['node', 'bin', 'run'], { run: true })
2367
2455
 
2368
2456
  await new Promise((r) => setTimeout(r, 50))
2369
2457
  expect(order).toEqual(['sync1', 'async', 'sync2', 'action'])
@@ -2371,7 +2459,7 @@ describe('middleware', () => {
2371
2459
  })
2372
2460
 
2373
2461
  describe('use() with sub-CLI composition', () => {
2374
- test('basic composition: sub-CLI command runs via parent', () => {
2462
+ test('basic composition: sub-CLI command runs via parent', async () => {
2375
2463
  const parent = goke('mycli')
2376
2464
  const sub = goke()
2377
2465
  let matched = ''
@@ -2381,11 +2469,11 @@ describe('use() with sub-CLI composition', () => {
2381
2469
  .action(() => { matched = 'deploy' })
2382
2470
 
2383
2471
  parent.use(sub)
2384
- parent.parse(['node', 'bin', 'deploy'], { run: true })
2472
+ await parent.parse(['node', 'bin', 'deploy'], { run: true })
2385
2473
  expect(matched).toBe('deploy')
2386
2474
  })
2387
2475
 
2388
- test('multiple sub-CLIs composed together', () => {
2476
+ test('multiple sub-CLIs composed together', async () => {
2389
2477
  const parent = goke('mycli')
2390
2478
  const subA = goke()
2391
2479
  const subB = goke()
@@ -2396,15 +2484,15 @@ describe('use() with sub-CLI composition', () => {
2396
2484
 
2397
2485
  parent.use(subA).use(subB)
2398
2486
 
2399
- parent.parse(['node', 'bin', 'login'], { run: true })
2487
+ await parent.parse(['node', 'bin', 'login'], { run: true })
2400
2488
  expect(matched).toBe('login')
2401
2489
 
2402
2490
  matched = ''
2403
- parent.parse(['node', 'bin', 'deploy'], { run: true })
2491
+ await parent.parse(['node', 'bin', 'deploy'], { run: true })
2404
2492
  expect(matched).toBe('deploy')
2405
2493
  })
2406
2494
 
2407
- test('sub-CLI command with options and schema coercion', () => {
2495
+ test('sub-CLI command with options and schema coercion', async () => {
2408
2496
  const parent = goke('mycli')
2409
2497
  const sub = goke()
2410
2498
  let result: any = {}
@@ -2416,14 +2504,14 @@ describe('use() with sub-CLI composition', () => {
2416
2504
  .action((options) => { result = options })
2417
2505
 
2418
2506
  parent.use(sub)
2419
- parent.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true })
2507
+ await parent.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true })
2420
2508
 
2421
2509
  expect(result.port).toBe(3000)
2422
2510
  expect(typeof result.port).toBe('number')
2423
2511
  expect(result.host).toBe('localhost')
2424
2512
  })
2425
2513
 
2426
- test('sub-CLI command with positional args', () => {
2514
+ test('sub-CLI command with positional args', async () => {
2427
2515
  const parent = goke('mycli')
2428
2516
  const sub = goke()
2429
2517
  let receivedId = ''
@@ -2433,12 +2521,12 @@ describe('use() with sub-CLI composition', () => {
2433
2521
  .action((id) => { receivedId = id })
2434
2522
 
2435
2523
  parent.use(sub)
2436
- parent.parse(['node', 'bin', 'get', 'abc123'], { run: true })
2524
+ await parent.parse(['node', 'bin', 'get', 'abc123'], { run: true })
2437
2525
 
2438
2526
  expect(receivedId).toBe('abc123')
2439
2527
  })
2440
2528
 
2441
- test('sub-CLI with multi-word commands', () => {
2529
+ test('sub-CLI with multi-word commands', async () => {
2442
2530
  const parent = goke('mycli')
2443
2531
  const sub = goke()
2444
2532
  let matched = ''
@@ -2448,15 +2536,15 @@ describe('use() with sub-CLI composition', () => {
2448
2536
 
2449
2537
  parent.use(sub)
2450
2538
 
2451
- parent.parse(['node', 'bin', 'mcp', 'login'], { run: true })
2539
+ await parent.parse(['node', 'bin', 'mcp', 'login'], { run: true })
2452
2540
  expect(matched).toBe('mcp login')
2453
2541
 
2454
2542
  matched = ''
2455
- parent.parse(['node', 'bin', 'mcp', 'logout'], { run: true })
2543
+ await parent.parse(['node', 'bin', 'mcp', 'logout'], { run: true })
2456
2544
  expect(matched).toBe('mcp logout')
2457
2545
  })
2458
2546
 
2459
- test('help output includes composed commands', () => {
2547
+ test('help output includes composed commands', async () => {
2460
2548
  const stdout = createTestOutputStream()
2461
2549
  const parent = goke('mycli', { stdout })
2462
2550
  const sub = goke()
@@ -2467,14 +2555,14 @@ describe('use() with sub-CLI composition', () => {
2467
2555
  parent.command('init', 'Initialize project')
2468
2556
  parent.use(sub)
2469
2557
  parent.help()
2470
- parent.parse(['node', 'bin', '--help'], { run: false })
2558
+ await parent.parse(['node', 'bin', '--help'], { run: false })
2471
2559
 
2472
2560
  expect(stdout.text).toContain('init')
2473
2561
  expect(stdout.text).toContain('selfhost')
2474
2562
  expect(stdout.text).toContain('Set up on your own workspace')
2475
2563
  })
2476
2564
 
2477
- test('sub-CLI middlewares are NOT copied to parent', () => {
2565
+ test('sub-CLI middlewares are NOT copied to parent', async () => {
2478
2566
  const parent = goke('mycli')
2479
2567
  const sub = goke()
2480
2568
  let subMiddlewareCalled = false
@@ -2486,13 +2574,13 @@ describe('use() with sub-CLI composition', () => {
2486
2574
  parent.use(() => { order.push('parent-mw') })
2487
2575
  parent.use(sub)
2488
2576
 
2489
- parent.parse(['node', 'bin', 'deploy'], { run: true })
2577
+ await parent.parse(['node', 'bin', 'deploy'], { run: true })
2490
2578
 
2491
2579
  expect(subMiddlewareCalled).toBe(false)
2492
2580
  expect(order).toEqual(['parent-mw', 'deploy'])
2493
2581
  })
2494
2582
 
2495
- test('parent global options are available to composed commands', () => {
2583
+ test('parent global options are available to composed commands', async () => {
2496
2584
  const parent = goke('mycli')
2497
2585
  const sub = goke()
2498
2586
  let result: any = {}
@@ -2505,13 +2593,13 @@ describe('use() with sub-CLI composition', () => {
2505
2593
  .action((options) => { result = options })
2506
2594
 
2507
2595
  parent.use(sub)
2508
- parent.parse('node bin build --verbose --target production'.split(' '), { run: true })
2596
+ await parent.parse('node bin build --verbose --target production'.split(' '), { run: true })
2509
2597
 
2510
2598
  expect(result.verbose).toBe(true)
2511
2599
  expect(result.target).toBe('production')
2512
2600
  })
2513
2601
 
2514
- test('composed commands coexist with inline commands', () => {
2602
+ test('composed commands coexist with inline commands', async () => {
2515
2603
  const parent = goke('mycli')
2516
2604
  const sub = goke()
2517
2605
  let matched = ''
@@ -2523,21 +2611,21 @@ describe('use() with sub-CLI composition', () => {
2523
2611
 
2524
2612
  parent.use(sub)
2525
2613
 
2526
- parent.parse(['node', 'bin', 'init'], { run: true })
2614
+ await parent.parse(['node', 'bin', 'init'], { run: true })
2527
2615
  expect(matched).toBe('init')
2528
2616
 
2529
2617
  matched = ''
2530
- parent.parse(['node', 'bin', 'deploy'], { run: true })
2618
+ await parent.parse(['node', 'bin', 'deploy'], { run: true })
2531
2619
  expect(matched).toBe('deploy')
2532
2620
 
2533
2621
  matched = ''
2534
- parent.parse(['node', 'bin', 'rollback'], { run: true })
2622
+ await parent.parse(['node', 'bin', 'rollback'], { run: true })
2535
2623
  expect(matched).toBe('rollback')
2536
2624
  })
2537
2625
  })
2538
2626
 
2539
2627
  describe('getAction()', () => {
2540
- test('returns the action callable with correct behavior', () => {
2628
+ test('returns the action callable with correct behavior', async () => {
2541
2629
  const stdout = createTestOutputStream()
2542
2630
  const cli = goke('mycli', { stdout, exit: () => {} })
2543
2631
 
@@ -2554,7 +2642,7 @@ describe('getAction()', () => {
2554
2642
  expect(stdout.text).toBe('Deploying to staging\n')
2555
2643
  })
2556
2644
 
2557
- test('works with positional args', () => {
2645
+ test('works with positional args', async () => {
2558
2646
  const stdout = createTestOutputStream()
2559
2647
  const cli = goke('mycli', { stdout, exit: () => {} })
2560
2648
 
@@ -2571,7 +2659,7 @@ describe('getAction()', () => {
2571
2659
  expect(stdout.text).toBe('abc123:json\n')
2572
2660
  })
2573
2661
 
2574
- test('throws when no action is registered', () => {
2662
+ test('throws when no action is registered', async () => {
2575
2663
  const cli = goke('mycli')
2576
2664
  const cmd = cli.command('noop', 'No action')
2577
2665
  expect(() => cmd.getAction()).toThrow(/No action registered/)