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