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
@@ -47,7 +47,7 @@ function stripStackTrace(text) {
47
47
  .trim();
48
48
  }
49
49
  describe('error formatting', () => {
50
- test('unknown option prints formatted error to stderr', () => {
50
+ test('unknown option prints formatted error to stderr', async () => {
51
51
  const stderr = createTestOutputStream();
52
52
  const cli = goke('mycli', { stderr, exit: () => { } });
53
53
  cli
@@ -55,12 +55,12 @@ describe('error formatting', () => {
55
55
  .option('--port <port>', 'Port')
56
56
  .action(() => { });
57
57
  try {
58
- cli.parse('node bin build --unknown'.split(' '));
58
+ await cli.parse('node bin build --unknown'.split(' '));
59
59
  }
60
60
  catch { }
61
61
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: Unknown option \`--unknown\`"`);
62
62
  });
63
- test('missing required option value prints formatted error to stderr', () => {
63
+ test('missing required option value prints formatted error to stderr', async () => {
64
64
  const stderr = createTestOutputStream();
65
65
  const cli = goke('mycli', { stderr, exit: () => { } });
66
66
  cli
@@ -68,22 +68,22 @@ describe('error formatting', () => {
68
68
  .option('--port <port>', 'Port')
69
69
  .action(() => { });
70
70
  try {
71
- cli.parse('node bin serve --port'.split(' '));
71
+ await cli.parse('node bin serve --port'.split(' '));
72
72
  }
73
73
  catch { }
74
74
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: option \`--port <port>\` value is missing"`);
75
75
  });
76
- test('schema coercion error prints formatted error to stderr', () => {
76
+ test('schema coercion error prints formatted error to stderr', async () => {
77
77
  const stderr = createTestOutputStream();
78
78
  const cli = goke('mycli', { stderr, exit: () => { } });
79
79
  cli.option('--port <port>', z.number().describe('Port'));
80
80
  try {
81
- cli.parse('node bin --port abc'.split(' '));
81
+ await cli.parse('node bin --port abc'.split(' '));
82
82
  }
83
83
  catch { }
84
84
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: Invalid value for --port: expected number, got "abc""`);
85
85
  });
86
- test('error includes help hint when help is enabled', () => {
86
+ test('error includes help hint when help is enabled', async () => {
87
87
  const stderr = createTestOutputStream();
88
88
  const cli = goke('mycli', { stderr, exit: () => { } });
89
89
  cli.help();
@@ -92,7 +92,7 @@ describe('error formatting', () => {
92
92
  .option('--port <port>', 'Port')
93
93
  .action(() => { });
94
94
  try {
95
- cli.parse('node bin serve --port'.split(' '));
95
+ await cli.parse('node bin serve --port'.split(' '));
96
96
  }
97
97
  catch { }
98
98
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`
@@ -109,20 +109,20 @@ describe('error formatting', () => {
109
109
  .action(async () => {
110
110
  throw new Error('connection refused');
111
111
  });
112
- cli.parse('node bin deploy'.split(' '));
112
+ await cli.parse('node bin deploy'.split(' '));
113
113
  // Wait for the async rejection to be handled
114
114
  await new Promise(resolve => setTimeout(resolve, 10));
115
115
  expect(exitCode).toBe(1);
116
116
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: connection refused"`);
117
117
  });
118
- test('error output includes stack trace', () => {
118
+ test('error output includes stack trace', async () => {
119
119
  const stderr = createTestOutputStream();
120
120
  const cli = goke('mycli', { stderr, exit: () => { } });
121
121
  cli
122
122
  .command('build', 'Build app')
123
123
  .action(() => { });
124
124
  try {
125
- cli.parse('node bin build --unknown'.split(' '));
125
+ await cli.parse('node bin build --unknown'.split(' '));
126
126
  }
127
127
  catch { }
128
128
  // Verify that stderr contains "error:" prefix and a stack trace with "at" lines
@@ -132,7 +132,48 @@ describe('error formatting', () => {
132
132
  expect(text).toMatch(/at /);
133
133
  });
134
134
  });
135
+ describe('anonymous action naming', () => {
136
+ test('inline anonymous function gets named after the command', async () => {
137
+ const cli = gokeTestable('mycli');
138
+ const cmd = cli.command('deploy', 'Deploy app');
139
+ // Inline arrow functions passed directly to .action() have no name,
140
+ // so goke assigns one based on the command name for better stack traces.
141
+ cmd.action(() => { });
142
+ expect(cmd.commandAction.name).toBe('command:deploy');
143
+ });
144
+ test('inline anonymous function on multi-word command gets full name', async () => {
145
+ const cli = gokeTestable('mycli');
146
+ const cmd = cli.command('db migrate', 'Run migrations');
147
+ cmd.action(() => { });
148
+ expect(cmd.commandAction.name).toBe('command:db migrate');
149
+ });
150
+ test('named function keeps its original name', async () => {
151
+ const cli = gokeTestable('mycli');
152
+ const cmd = cli.command('build', 'Build app');
153
+ function myBuildAction() { }
154
+ cmd.action(myBuildAction);
155
+ expect(cmd.commandAction.name).toBe('myBuildAction');
156
+ });
157
+ test('default command action gets "command:default" name', async () => {
158
+ const cli = gokeTestable('mycli');
159
+ const cmd = cli.command('', 'Default command');
160
+ cmd.action(() => { });
161
+ expect(cmd.commandAction.name).toBe('command:default');
162
+ });
163
+ });
135
164
  describe('injected fs', () => {
165
+ test('parse waits for async command actions before resolving', async () => {
166
+ const stdout = createTestOutputStream();
167
+ const cli = gokeTestable('mycli', { stdout });
168
+ cli
169
+ .command('deploy', 'Deploy app')
170
+ .action(async (options, { console }) => {
171
+ await new Promise(resolve => setTimeout(resolve, 10));
172
+ console.log('deploy complete');
173
+ });
174
+ await cli.parse(['node', 'bin', 'deploy']);
175
+ expect(stdout.text).toBe('deploy complete\n');
176
+ });
136
177
  test('command actions can use the default node fs for cli storage', async () => {
137
178
  const stdout = createTestOutputStream();
138
179
  const cli = gokeTestable('mycli', { stdout });
@@ -148,7 +189,7 @@ describe('injected fs', () => {
148
189
  await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8');
149
190
  console.log('saved credentials');
150
191
  });
151
- cli.parse(['node', 'bin', 'login', '--token', 'abc123'], { run: false });
192
+ await cli.parse(['node', 'bin', 'login', '--token', 'abc123'], { run: false });
152
193
  await cli.runMatchedCommand();
153
194
  expect(stdout.text).toBe('saved credentials\n');
154
195
  expect(await readFile(join(tempDir, '.mycli/auth.json'), 'utf8')).toBe('{"token":"abc123"}');
@@ -178,7 +219,7 @@ describe('injected process context', () => {
178
219
  token: process.env.GOKE_TEST_TOKEN,
179
220
  }));
180
221
  });
181
- cli.parse(['node', 'bin', 'context'], { run: false });
222
+ await cli.parse(['node', 'bin', 'context'], { run: false });
182
223
  await cli.runMatchedCommand();
183
224
  expect(stdout.text).toBe(`${JSON.stringify({ cwd: process.cwd(), stdin: '', token: 'abc123' })}\n`);
184
225
  }
@@ -203,15 +244,15 @@ describe('injected process context', () => {
203
244
  process.env.TOKEN = 'after';
204
245
  console.log(process.env.TOKEN);
205
246
  });
206
- cli.parse(['node', 'bin', 'context'], { run: false });
247
+ await cli.parse(['node', 'bin', 'context'], { run: false });
207
248
  await cli.runMatchedCommand();
208
249
  expect(stdout.text).toBe('after\n');
209
250
  expect(env.TOKEN).toBe('after');
210
251
  });
211
252
  });
212
- test('double dashes', () => {
253
+ test('double dashes', async () => {
213
254
  const cli = goke();
214
- const { args, options } = cli.parse([
255
+ const { args, options } = await cli.parse([
215
256
  'node',
216
257
  'bin',
217
258
  'foo',
@@ -223,80 +264,80 @@ test('double dashes', () => {
223
264
  expect(args).toEqual(['foo', 'bar']);
224
265
  expect(options['--']).toEqual(['npm', 'test']);
225
266
  });
226
- test('dot-nested options', () => {
267
+ test('dot-nested options', async () => {
227
268
  const cli = goke();
228
269
  cli
229
270
  .option('--externals <external>', 'Add externals')
230
271
  .option('--scale [level]', 'Scaling level');
231
- const { options: options1 } = cli.parse(`node bin --externals.env.prod production --scale`.split(' '));
272
+ const { options: options1 } = await cli.parse(`node bin --externals.env.prod production --scale`.split(' '));
232
273
  expect(options1.externals).toEqual({ env: { prod: 'production' } });
233
274
  // Bare `--scale` normalizes to `''` (new uniform string-or-undefined shape
234
275
  // for untyped optional-value flags).
235
276
  expect(options1.scale).toEqual('');
236
277
  });
237
278
  describe('schema-based options', () => {
238
- test('schema coerces string to number', () => {
279
+ test('schema coerces string to number', async () => {
239
280
  const cli = goke();
240
281
  cli.option('--port <port>', z.number().describe('Port number'));
241
- const { options } = cli.parse('node bin --port 3000'.split(' '));
282
+ const { options } = await cli.parse('node bin --port 3000'.split(' '));
242
283
  expect(options.port).toBe(3000);
243
284
  expect(typeof options.port).toBe('number');
244
285
  });
245
- test('schema preserves string (no auto-conversion to number)', () => {
286
+ test('schema preserves string (no auto-conversion to number)', async () => {
246
287
  const cli = goke();
247
288
  cli.option('--id <id>', z.string().describe('ID'));
248
- const { options } = cli.parse('node bin --id 00123'.split(' '));
289
+ const { options } = await cli.parse('node bin --id 00123'.split(' '));
249
290
  expect(options.id).toBe('00123');
250
291
  expect(typeof options.id).toBe('string');
251
292
  });
252
- test('schema coerces string to integer', () => {
293
+ test('schema coerces string to integer', async () => {
253
294
  const cli = goke();
254
295
  cli.option('--count <count>', z.int().describe('Count'));
255
- const { options } = cli.parse('node bin --count 42'.split(' '));
296
+ const { options } = await cli.parse('node bin --count 42'.split(' '));
256
297
  expect(options.count).toBe(42);
257
298
  });
258
- test('schema parses JSON object', () => {
299
+ test('schema parses JSON object', async () => {
259
300
  const cli = goke();
260
301
  cli.option('--config <config>', z.looseObject({}).describe('Config'));
261
- const { options } = cli.parse(['node', 'bin', '--config', '{"a":1}']);
302
+ const { options } = await cli.parse(['node', 'bin', '--config', '{"a":1}']);
262
303
  expect(options.config).toEqual({ a: 1 });
263
304
  });
264
- test('schema parses JSON array', () => {
305
+ test('schema parses JSON array', async () => {
265
306
  const cli = goke();
266
307
  cli.option('--items <items>', z.array(z.unknown()).describe('Items'));
267
- const { options } = cli.parse(['node', 'bin', '--items', '[1,2,3]']);
308
+ const { options } = await cli.parse(['node', 'bin', '--items', '[1,2,3]']);
268
309
  expect(options.items).toEqual([1, 2, 3]);
269
310
  });
270
- test('schema throws on invalid number', () => {
311
+ test('schema throws on invalid number', async () => {
271
312
  const cli = gokeTestable();
272
313
  cli.option('--port <port>', z.number().describe('Port number'));
273
- expect(() => cli.parse('node bin --port abc'.split(' ')))
274
- .toThrow('expected number, got "abc"');
314
+ await expect(cli.parse('node bin --port abc'.split(' ')))
315
+ .rejects.toThrow('expected number, got "abc"');
275
316
  });
276
- test('schema with union type ["number", "string"]', () => {
317
+ test('schema with union type ["number", "string"]', async () => {
277
318
  const cli = goke();
278
319
  cli.option('--val <val>', z.union([z.number(), z.string()]).describe('Value'));
279
- const { options: opts1 } = cli.parse('node bin --val 123'.split(' '));
320
+ const { options: opts1 } = await cli.parse('node bin --val 123'.split(' '));
280
321
  expect(opts1.val).toBe(123);
281
- const { options: opts2 } = cli.parse('node bin --val abc'.split(' '));
322
+ const { options: opts2 } = await cli.parse('node bin --val abc'.split(' '));
282
323
  expect(opts2.val).toBe('abc');
283
324
  });
284
- test('options without schema keep values as strings', () => {
325
+ test('options without schema keep values as strings', async () => {
285
326
  const cli = goke();
286
327
  cli.option('--port <port>', 'Port number');
287
328
  // Without schema, mri no longer auto-converts — value stays as string.
288
329
  // Use a schema to get typed values.
289
- const { options } = cli.parse('node bin --port 3000'.split(' '));
330
+ const { options } = await cli.parse('node bin --port 3000'.split(' '));
290
331
  expect(options.port).toBe('3000');
291
332
  expect(typeof options.port).toBe('string');
292
333
  });
293
- test('schema with default value', () => {
334
+ test('schema with default value', async () => {
294
335
  const cli = goke();
295
336
  cli.option('--port [port]', z.number().default(8080).describe('Port number'));
296
- const { options } = cli.parse('node bin'.split(' '));
337
+ const { options } = await cli.parse('node bin'.split(' '));
297
338
  expect(options.port).toBe(8080);
298
339
  });
299
- test('schema on subcommand options', () => {
340
+ test('schema on subcommand options', async () => {
300
341
  const cli = goke();
301
342
  let result = {};
302
343
  cli
@@ -306,66 +347,66 @@ describe('schema-based options', () => {
306
347
  .action((options) => {
307
348
  result = options;
308
349
  });
309
- cli.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true });
350
+ await cli.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true });
310
351
  expect(result.port).toBe(3000);
311
352
  expect(result.host).toBe('localhost');
312
353
  });
313
354
  });
314
355
  describe('no-schema behavior (mri no longer auto-converts)', () => {
315
- test('numeric string stays as string without schema', () => {
356
+ test('numeric string stays as string without schema', async () => {
316
357
  const cli = goke();
317
358
  cli.option('--port <port>', 'Port');
318
- const { options } = cli.parse('node bin --port 3000'.split(' '));
359
+ const { options } = await cli.parse('node bin --port 3000'.split(' '));
319
360
  expect(options.port).toBe('3000');
320
361
  });
321
- test('leading zeros preserved without schema', () => {
362
+ test('leading zeros preserved without schema', async () => {
322
363
  const cli = goke();
323
364
  cli.option('--id <id>', 'ID');
324
- const { options } = cli.parse('node bin --id 00123'.split(' '));
365
+ const { options } = await cli.parse('node bin --id 00123'.split(' '));
325
366
  expect(options.id).toBe('00123');
326
367
  });
327
- test('phone number preserved without schema', () => {
368
+ test('phone number preserved without schema', async () => {
328
369
  const cli = goke();
329
370
  cli.option('--phone <phone>', 'Phone');
330
- const { options } = cli.parse('node bin --phone +1234567890'.split(' '));
371
+ const { options } = await cli.parse('node bin --phone +1234567890'.split(' '));
331
372
  expect(options.phone).toBe('+1234567890');
332
373
  });
333
- test('boolean flags still work without schema', () => {
374
+ test('boolean flags still work without schema', async () => {
334
375
  const cli = goke();
335
376
  cli.option('--verbose', 'Verbose');
336
- const { options } = cli.parse('node bin --verbose'.split(' '));
377
+ const { options } = await cli.parse('node bin --verbose'.split(' '));
337
378
  expect(options.verbose).toBe(true);
338
379
  });
339
- test('optional value flag returns empty string when no value given', () => {
380
+ test('optional value flag returns empty string when no value given', async () => {
340
381
  // Bare `--format` is normalized from the mri `true` sentinel to `''` so
341
382
  // callers see a uniform `string | undefined` shape. `''` still lets them
342
383
  // distinguish "flag present but no value" from "flag omitted entirely".
343
384
  const cli = goke();
344
385
  cli.option('--format [fmt]', 'Format');
345
- const { options } = cli.parse('node bin --format'.split(' '));
386
+ const { options } = await cli.parse('node bin --format'.split(' '));
346
387
  expect(options.format).toBe('');
347
388
  });
348
- test('optional value flag returns string when value given', () => {
389
+ test('optional value flag returns string when value given', async () => {
349
390
  const cli = goke();
350
391
  cli.option('--format [fmt]', 'Format');
351
- const { options } = cli.parse('node bin --format json'.split(' '));
392
+ const { options } = await cli.parse('node bin --format json'.split(' '));
352
393
  expect(options.format).toBe('json');
353
394
  });
354
- test('hex string stays as string without schema', () => {
395
+ test('hex string stays as string without schema', async () => {
355
396
  const cli = goke();
356
397
  cli.option('--color <color>', 'Color');
357
- const { options } = cli.parse('node bin --color 0xff00ff'.split(' '));
398
+ const { options } = await cli.parse('node bin --color 0xff00ff'.split(' '));
358
399
  expect(options.color).toBe('0xff00ff');
359
400
  });
360
- test('scientific notation stays as string without schema', () => {
401
+ test('scientific notation stays as string without schema', async () => {
361
402
  const cli = goke();
362
403
  cli.option('--val <val>', 'Value');
363
- const { options } = cli.parse('node bin --val 1e10'.split(' '));
404
+ const { options } = await cli.parse('node bin --val 1e10'.split(' '));
364
405
  expect(options.val).toBe('1e10');
365
406
  });
366
407
  });
367
408
  describe('typical CLI usage examples', () => {
368
- test('web server CLI with typed options', () => {
409
+ test('web server CLI with typed options', async () => {
369
410
  const cli = goke('myserver');
370
411
  let config = {};
371
412
  cli
@@ -376,7 +417,7 @@ describe('typical CLI usage examples', () => {
376
417
  .option('--cors', 'Enable CORS')
377
418
  .option('--log', 'Enable logging')
378
419
  .action((options) => { config = options; });
379
- cli.parse('node bin start --port 8080 --host 0.0.0.0 --workers 4 --cors'.split(' '), { run: true });
420
+ await cli.parse('node bin start --port 8080 --host 0.0.0.0 --workers 4 --cors'.split(' '), { run: true });
380
421
  expect(config.port).toBe(8080);
381
422
  expect(typeof config.port).toBe('number');
382
423
  expect(config.host).toBe('0.0.0.0');
@@ -384,7 +425,7 @@ describe('typical CLI usage examples', () => {
384
425
  expect(typeof config.workers).toBe('number');
385
426
  expect(config.cors).toBe(true);
386
427
  });
387
- test('web server CLI with defaults (no args)', () => {
428
+ test('web server CLI with defaults (no args)', async () => {
388
429
  const cli = goke('myserver');
389
430
  let config = {};
390
431
  cli
@@ -392,11 +433,11 @@ describe('typical CLI usage examples', () => {
392
433
  .option('--port [port]', z.number().default(3000).describe('Port'))
393
434
  .option('--host [host]', z.string().default('localhost').describe('Host'))
394
435
  .action((options) => { config = options; });
395
- cli.parse('node bin start'.split(' '), { run: true });
436
+ await cli.parse('node bin start'.split(' '), { run: true });
396
437
  expect(config.port).toBe(3000);
397
438
  expect(config.host).toBe('localhost');
398
439
  });
399
- test('database CLI with JSON config option', () => {
440
+ test('database CLI with JSON config option', async () => {
400
441
  const cli = goke('dbcli');
401
442
  let config = {};
402
443
  cli
@@ -404,11 +445,11 @@ describe('typical CLI usage examples', () => {
404
445
  .option('--connection <conn>', z.object({ host: z.string(), port: z.number() }).describe('Connection config (JSON)'))
405
446
  .option('--dry-run', 'Preview without executing')
406
447
  .action((options) => { config = options; });
407
- cli.parse(['node', 'bin', 'migrate', '--connection', '{"host":"localhost","port":5432}', '--dry-run'], { run: true });
448
+ await cli.parse(['node', 'bin', 'migrate', '--connection', '{"host":"localhost","port":5432}', '--dry-run'], { run: true });
408
449
  expect(config.connection).toEqual({ host: 'localhost', port: 5432 });
409
450
  expect(config.dryRun).toBe(true);
410
451
  });
411
- test('file processing CLI with positional args + typed options', () => {
452
+ test('file processing CLI with positional args + typed options', async () => {
412
453
  const cli = goke('fileproc');
413
454
  let result = {};
414
455
  cli
@@ -418,14 +459,14 @@ describe('typical CLI usage examples', () => {
418
459
  .action((input, output, options) => {
419
460
  result = { input, output, ...options };
420
461
  });
421
- cli.parse('node bin convert photo.bmp photo.jpg --quality 85 --format jpg'.split(' '), { run: true });
462
+ await cli.parse('node bin convert photo.bmp photo.jpg --quality 85 --format jpg'.split(' '), { run: true });
422
463
  expect(result.input).toBe('photo.bmp');
423
464
  expect(result.output).toBe('photo.jpg');
424
465
  expect(result.quality).toBe(85);
425
466
  expect(typeof result.quality).toBe('number');
426
467
  expect(result.format).toBe('jpg');
427
468
  });
428
- test('API client CLI preserving string IDs', () => {
469
+ test('API client CLI preserving string IDs', async () => {
429
470
  const cli = goke('apicli');
430
471
  let result = {};
431
472
  cli
@@ -435,22 +476,22 @@ describe('typical CLI usage examples', () => {
435
476
  result = { userId, ...options };
436
477
  });
437
478
  // userId "00123" should NOT be coerced to number 123
438
- cli.parse(['node', 'bin', 'get-user', '00123', '--fields', '["name","email"]'], { run: true });
479
+ await cli.parse(['node', 'bin', 'get-user', '00123', '--fields', '["name","email"]'], { run: true });
439
480
  expect(result.userId).toBe('00123');
440
481
  expect(result.fields).toEqual(['name', 'email']);
441
482
  });
442
- test('nullable option with union type', () => {
483
+ test('nullable option with union type', async () => {
443
484
  const cli = goke();
444
485
  cli.option('--timeout <timeout>', z.nullable(z.number()).describe('Timeout'));
445
- const { options: opts1 } = cli.parse('node bin --timeout 5000'.split(' '));
486
+ const { options: opts1 } = await cli.parse('node bin --timeout 5000'.split(' '));
446
487
  expect(opts1.timeout).toBe(5000);
447
488
  // Empty string coerces to null for null type
448
- const { options: opts2 } = cli.parse(['node', 'bin', '--timeout', '']);
489
+ const { options: opts2 } = await cli.parse(['node', 'bin', '--timeout', '']);
449
490
  expect(opts2.timeout).toBe(null);
450
491
  });
451
492
  });
452
493
  describe('regression: oracle-found issues', () => {
453
- test('required option with schema still throws when value missing', () => {
494
+ test('required option with schema still throws when value missing', async () => {
454
495
  const cli = gokeTestable();
455
496
  let actionCalled = false;
456
497
  cli
@@ -458,99 +499,98 @@ describe('regression: oracle-found issues', () => {
458
499
  .option('--port <port>', z.number().describe('Port'))
459
500
  .action(() => { actionCalled = true; });
460
501
  // --port without a value should throw "value is missing"
461
- expect(() => {
462
- cli.parse('node bin serve --port'.split(' '), { run: true });
463
- }).toThrow('value is missing');
502
+ await expect(cli.parse('node bin serve --port'.split(' '), { run: true }))
503
+ .rejects.toThrow('value is missing');
464
504
  expect(actionCalled).toBe(false);
465
505
  });
466
- test('repeated flags with non-array schema throws', () => {
506
+ test('repeated flags with non-array schema throws', async () => {
467
507
  const cli = gokeTestable();
468
508
  cli.option('--tag <tag>', z.string().describe('Tags'));
469
- expect(() => cli.parse('node bin --tag foo --tag bar'.split(' ')))
470
- .toThrow('does not accept multiple values');
509
+ await expect(cli.parse('node bin --tag foo --tag bar'.split(' ')))
510
+ .rejects.toThrow('does not accept multiple values');
471
511
  });
472
- test('repeated flags with number schema throws', () => {
512
+ test('repeated flags with number schema throws', async () => {
473
513
  const cli = gokeTestable();
474
514
  cli.option('--id <id>', z.number().describe('ID'));
475
- expect(() => cli.parse('node bin --id 1 --id 2'.split(' ')))
476
- .toThrow('does not accept multiple values');
515
+ await expect(cli.parse('node bin --id 1 --id 2'.split(' ')))
516
+ .rejects.toThrow('does not accept multiple values');
477
517
  });
478
- test('repeated flags with array schema collects values', () => {
518
+ test('repeated flags with array schema collects values', async () => {
479
519
  const cli = goke();
480
520
  cli.option('--tag <tag>', z.array(z.string()).describe('Tags'));
481
- const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '));
521
+ const { options } = await cli.parse('node bin --tag foo --tag bar'.split(' '));
482
522
  expect(options.tag).toEqual(['foo', 'bar']);
483
523
  });
484
- test('repeated flags with array+items schema coerces each element', () => {
524
+ test('repeated flags with array+items schema coerces each element', async () => {
485
525
  const cli = goke();
486
526
  cli.option('--id <id>', z.array(z.number()).describe('IDs'));
487
- const { options } = cli.parse('node bin --id 1 --id 2 --id 3'.split(' '));
527
+ const { options } = await cli.parse('node bin --id 1 --id 2 --id 3'.split(' '));
488
528
  expect(options.id).toEqual([1, 2, 3]);
489
529
  });
490
- test('single value with array schema wraps in array', () => {
530
+ test('single value with array schema wraps in array', async () => {
491
531
  const cli = goke();
492
532
  cli.option('--tag <tag>', z.array(z.string()).describe('Tags'));
493
- const { options } = cli.parse('node bin --tag foo'.split(' '));
533
+ const { options } = await cli.parse('node bin --tag foo'.split(' '));
494
534
  expect(options.tag).toEqual(['foo']);
495
535
  });
496
- test('single value with array+number items schema wraps and coerces', () => {
536
+ test('single value with array+number items schema wraps and coerces', async () => {
497
537
  const cli = goke();
498
538
  cli.option('--id <id>', z.array(z.number()).describe('IDs'));
499
- const { options } = cli.parse('node bin --id 42'.split(' '));
539
+ const { options } = await cli.parse('node bin --id 42'.split(' '));
500
540
  expect(options.id).toEqual([42]);
501
541
  });
502
- test('JSON array string with array schema parses correctly', () => {
542
+ test('JSON array string with array schema parses correctly', async () => {
503
543
  const cli = goke();
504
544
  cli.option('--ids <ids>', z.array(z.number()).describe('IDs'));
505
- const { options } = cli.parse(['node', 'bin', '--ids', '[1,2,3]']);
545
+ const { options } = await cli.parse(['node', 'bin', '--ids', '[1,2,3]']);
506
546
  expect(options.ids).toEqual([1, 2, 3]);
507
547
  });
508
- test('repeated flags without schema still produce array (no schema = no restriction)', () => {
548
+ test('repeated flags without schema still produce array (no schema = no restriction)', async () => {
509
549
  const cli = goke();
510
550
  cli.option('--tag <tag>', 'Tags');
511
- const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '));
551
+ const { options } = await cli.parse('node bin --tag foo --tag bar'.split(' '));
512
552
  expect(options.tag).toEqual(['foo', 'bar']);
513
553
  });
514
- test('repeated optional value option without schema produces array', () => {
554
+ test('repeated optional value option without schema produces array', async () => {
515
555
  const cli = goke();
516
556
  cli.option('--tag [tag]', 'Tags');
517
- const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '));
557
+ const { options } = await cli.parse('node bin --tag foo --tag bar'.split(' '));
518
558
  expect(options.tag).toEqual(['foo', 'bar']);
519
559
  });
520
- test('repeated alias option without schema produces array', () => {
560
+ test('repeated alias option without schema produces array', async () => {
521
561
  const cli = goke();
522
562
  cli.option('-t, --tag <tag>', 'Tags');
523
- const { options } = cli.parse('node bin -t foo -t bar -t baz'.split(' '));
563
+ const { options } = await cli.parse('node bin -t foo -t bar -t baz'.split(' '));
524
564
  expect(options.tag).toEqual(['foo', 'bar', 'baz']);
525
565
  expect(options.t).toEqual(['foo', 'bar', 'baz']);
526
566
  });
527
- test('repeated option without schema on subcommand produces array', () => {
567
+ test('repeated option without schema on subcommand produces array', async () => {
528
568
  const cli = goke();
529
569
  let result = {};
530
570
  cli
531
571
  .command('build', 'Build')
532
572
  .option('--exclude <path>', 'Paths to exclude')
533
573
  .action((options) => { result = options; });
534
- cli.parse('node bin build --exclude node_modules --exclude dist --exclude .git'.split(' '), { run: true });
574
+ await cli.parse('node bin build --exclude node_modules --exclude dist --exclude .git'.split(' '), { run: true });
535
575
  expect(result.exclude).toEqual(['node_modules', 'dist', '.git']);
536
576
  });
537
- test('single value without schema stays as string (not wrapped in array)', () => {
577
+ test('single value without schema stays as string (not wrapped in array)', async () => {
538
578
  const cli = goke();
539
579
  cli.option('--tag <tag>', 'Tags');
540
- const { options } = cli.parse('node bin --tag foo'.split(' '));
580
+ const { options } = await cli.parse('node bin --tag foo'.split(' '));
541
581
  expect(options.tag).toBe('foo');
542
582
  });
543
- test('const null coercion works', () => {
583
+ test('const null coercion works', async () => {
544
584
  expect(coerceBySchema('', { const: null }, 'val')).toBe(null);
545
585
  });
546
- test('optional value option with schema returns undefined when no value given', () => {
586
+ test('optional value option with schema returns undefined when no value given', async () => {
547
587
  const cli = goke();
548
588
  cli.option('--count [count]', z.number().describe('Count'));
549
589
  // --count without value → schema expects number, none given → undefined
550
- const { options } = cli.parse('node bin --count'.split(' '));
590
+ const { options } = await cli.parse('node bin --count'.split(' '));
551
591
  expect(options.count).toBe(undefined);
552
592
  });
553
- test('optional value option without schema normalizes bare flag to empty string', () => {
593
+ test('optional value option without schema normalizes bare flag to empty string', async () => {
554
594
  const cli = goke();
555
595
  cli.option('--count [count]', 'Count');
556
596
  // Untyped optional-value flags uniformly expose `string | undefined`:
@@ -559,25 +599,25 @@ describe('regression: oracle-found issues', () => {
559
599
  // - (omitted) → undefined (flag absent)
560
600
  // This lets callers use a single `typeof options.count === 'string'`
561
601
  // check and distinguish the three cases via `=== ''` if they need to.
562
- const { options } = cli.parse('node bin --count'.split(' '));
602
+ const { options } = await cli.parse('node bin --count'.split(' '));
563
603
  expect(options.count).toBe('');
564
604
  });
565
- test('optional value option with schema coerces when value given', () => {
605
+ test('optional value option with schema coerces when value given', async () => {
566
606
  const cli = goke();
567
607
  cli.option('--count [count]', z.number().describe('Count'));
568
- const { options } = cli.parse('node bin --count 42'.split(' '));
608
+ const { options } = await cli.parse('node bin --count 42'.split(' '));
569
609
  expect(options.count).toBe(42);
570
610
  });
571
- test('optional value option with schema default returns default when omitted', () => {
611
+ test('optional value option with schema default returns default when omitted', async () => {
572
612
  // `z.number().default(30)` has input `number | undefined` → output `number`,
573
613
  // so goke marks this option as effectively required and must surface the
574
614
  // default value at runtime when the flag is omitted.
575
615
  const cli = goke();
576
616
  cli.option('--limit [n]', z.number().default(30).describe('Max items'));
577
- const { options } = cli.parse('node bin'.split(' '));
617
+ const { options } = await cli.parse('node bin'.split(' '));
578
618
  expect(options.limit).toBe(30);
579
619
  });
580
- test('optional value option with schema default returns default when passed bare', () => {
620
+ test('optional value option with schema default returns default when passed bare', async () => {
581
621
  // Bare `--limit` is mri's "flag present, no value" sentinel. Without a
582
622
  // default, goke replaces it with `undefined`. With a default, goke must
583
623
  // preserve the preset default value instead of clobbering it, so the
@@ -585,16 +625,16 @@ describe('regression: oracle-found issues', () => {
585
625
  // holds for all three input states: omitted, bare, and with-value.
586
626
  const cli = goke();
587
627
  cli.option('--limit [n]', z.number().default(30).describe('Max items'));
588
- const { options } = cli.parse('node bin --limit'.split(' '));
628
+ const { options } = await cli.parse('node bin --limit'.split(' '));
589
629
  expect(options.limit).toBe(30);
590
630
  });
591
- test('optional value option with schema default coerces explicit value', () => {
631
+ test('optional value option with schema default coerces explicit value', async () => {
592
632
  const cli = goke();
593
633
  cli.option('--limit [n]', z.number().default(30).describe('Max items'));
594
- const { options } = cli.parse('node bin --limit 5'.split(' '));
634
+ const { options } = await cli.parse('node bin --limit 5'.split(' '));
595
635
  expect(options.limit).toBe(5);
596
636
  });
597
- test('multiple optional options with defaults all preserve their defaults', () => {
637
+ test('multiple optional options with defaults all preserve their defaults', async () => {
598
638
  // Regression test for the runtime-overwrite bug: when several schema-backed
599
639
  // optional flags have defaults, passing one bare should not clobber the
600
640
  // others, and the bare one should keep its own default.
@@ -603,200 +643,199 @@ describe('regression: oracle-found issues', () => {
603
643
  .option('--limit [n]', z.number().default(30))
604
644
  .option('--sort [mode]', z.enum(['asc', 'desc']).default('asc'))
605
645
  .option('--host [host]', z.string().default('localhost'));
606
- const { options } = cli.parse('node bin --sort'.split(' '));
646
+ const { options } = await cli.parse('node bin --sort'.split(' '));
607
647
  expect(options.limit).toBe(30);
608
648
  expect(options.sort).toBe('asc');
609
649
  expect(options.host).toBe('localhost');
610
650
  });
611
- test('alias + schema coercion works', () => {
651
+ test('alias + schema coercion works', async () => {
612
652
  const cli = goke();
613
653
  cli.option('-p, --port <port>', z.number().describe('Port'));
614
- const { options } = cli.parse('node bin -p 3000'.split(' '));
654
+ const { options } = await cli.parse('node bin -p 3000'.split(' '));
615
655
  expect(options.port).toBe(3000);
616
656
  expect(options.p).toBe(3000);
617
657
  });
618
- test('union type ["array", "null"] with repeated flags', () => {
658
+ test('union type ["array", "null"] with repeated flags', async () => {
619
659
  const cli = goke();
620
660
  cli.option('--tags <tags>', z.nullable(z.array(z.string())).describe('Tags'));
621
- const { options } = cli.parse('node bin --tags foo --tags bar'.split(' '));
661
+ const { options } = await cli.parse('node bin --tags foo --tags bar'.split(' '));
622
662
  expect(options.tags).toEqual(['foo', 'bar']);
623
663
  });
624
664
  });
625
665
  describe('edge cases: schema + defaults interaction', () => {
626
- test('default value from schema is used when option not passed', () => {
666
+ test('default value from schema is used when option not passed', async () => {
627
667
  const cli = goke();
628
668
  cli.option('--port [port]', z.number().default(8080).describe('Port'));
629
- const { options } = cli.parse('node bin'.split(' '));
669
+ const { options } = await cli.parse('node bin'.split(' '));
630
670
  expect(options.port).toBe(8080);
631
671
  });
632
- test('default value is used when option not passed, schema value when passed', () => {
672
+ test('default value is used when option not passed, schema value when passed', async () => {
633
673
  const cli = goke();
634
674
  cli.option('--port [port]', z.number().default(8080).describe('Port'));
635
- const { options: opts1 } = cli.parse('node bin'.split(' '));
675
+ const { options: opts1 } = await cli.parse('node bin'.split(' '));
636
676
  expect(opts1.port).toBe(8080);
637
- const { options: opts2 } = cli.parse('node bin --port 3000'.split(' '));
677
+ const { options: opts2 } = await cli.parse('node bin --port 3000'.split(' '));
638
678
  expect(opts2.port).toBe(3000);
639
679
  });
640
- test('optional value + default + schema: three-way interaction', () => {
680
+ test('optional value + default + schema: three-way interaction', async () => {
641
681
  const cli = goke();
642
682
  cli.option('--count [count]', z.number().default(10).describe('Count'));
643
683
  // Not passed at all → default
644
- const { options: opts1 } = cli.parse('node bin'.split(' '));
684
+ const { options: opts1 } = await cli.parse('node bin'.split(' '));
645
685
  expect(opts1.count).toBe(10);
646
686
  // Passed with value → coerced
647
- const { options: opts2 } = cli.parse('node bin --count 42'.split(' '));
687
+ const { options: opts2 } = await cli.parse('node bin --count 42'.split(' '));
648
688
  expect(opts2.count).toBe(42);
649
689
  // Passed without value → default preserved. Before goke 6.7.0 this test
650
690
  // expected `undefined` because the bare-flag sentinel overwrote the
651
691
  // preset default. With the HasSchemaDefault type inference, the runtime
652
692
  // must keep the default so that the type-level promise ("options.count
653
693
  // is always a number") holds for all three input states.
654
- const { options: opts3 } = cli.parse('node bin --count'.split(' '));
694
+ const { options: opts3 } = await cli.parse('node bin --count'.split(' '));
655
695
  expect(opts3.count).toBe(10);
656
696
  });
657
697
  });
658
698
  describe('edge cases: boolean flags + schema', () => {
659
- test('boolean flag (no brackets) with number schema — mri returns boolean', () => {
699
+ test('boolean flag (no brackets) with number schema — mri returns boolean', async () => {
660
700
  const cli = goke();
661
701
  // This is a questionable usage: boolean flag + number schema
662
702
  // mri returns true/false for boolean flags, schema tries to coerce boolean→number
663
703
  cli.option('--verbose', z.number().describe('Verbose'));
664
- const { options } = cli.parse('node bin --verbose'.split(' '));
704
+ const { options } = await cli.parse('node bin --verbose'.split(' '));
665
705
  // Boolean true → coerced to 1 by number schema
666
706
  expect(options.verbose).toBe(1);
667
707
  });
668
- test('boolean string value with boolean schema on value option', () => {
708
+ test('boolean string value with boolean schema on value option', async () => {
669
709
  const cli = goke();
670
710
  cli.option('--flag <flag>', z.boolean().describe('A flag'));
671
- const { options: opts1 } = cli.parse('node bin --flag true'.split(' '));
711
+ const { options: opts1 } = await cli.parse('node bin --flag true'.split(' '));
672
712
  expect(opts1.flag).toBe(true);
673
- const { options: opts2 } = cli.parse('node bin --flag false'.split(' '));
713
+ const { options: opts2 } = await cli.parse('node bin --flag false'.split(' '));
674
714
  expect(opts2.flag).toBe(false);
675
715
  });
676
- test('invalid boolean string with boolean schema throws', () => {
716
+ test('invalid boolean string with boolean schema throws', async () => {
677
717
  const cli = gokeTestable();
678
718
  cli.option('--flag <flag>', z.boolean().describe('A flag'));
679
- expect(() => cli.parse('node bin --flag yes'.split(' ')))
680
- .toThrow('expected true or false');
719
+ await expect(cli.parse('node bin --flag yes'.split(' ')))
720
+ .rejects.toThrow('expected true or false');
681
721
  });
682
722
  });
683
723
  describe('edge cases: dot-nested options + schema', () => {
684
- test('dot-nested option with number schema coerces value', () => {
724
+ test('dot-nested option with number schema coerces value', async () => {
685
725
  const cli = goke();
686
726
  cli.option('--config.port <port>', z.number().describe('Port'));
687
- const { options } = cli.parse('node bin --config.port 3000'.split(' '));
727
+ const { options } = await cli.parse('node bin --config.port 3000'.split(' '));
688
728
  expect(options.config).toEqual({ port: 3000 });
689
729
  });
690
- test('dot-nested default uses nested object shape', () => {
730
+ test('dot-nested default uses nested object shape', async () => {
691
731
  const cli = goke();
692
732
  cli.option('--config.port [port]', z.number().default(8080).describe('Port'));
693
- const { options } = cli.parse('node bin'.split(' '));
733
+ const { options } = await cli.parse('node bin'.split(' '));
694
734
  expect(options.config).toEqual({ port: 8080 });
695
735
  });
696
736
  });
697
737
  describe('edge cases: kebab-case + schema', () => {
698
- test('kebab-case option coerced via schema and accessible as camelCase', () => {
738
+ test('kebab-case option coerced via schema and accessible as camelCase', async () => {
699
739
  const cli = goke();
700
740
  cli.option('--max-retries <count>', z.number().describe('Max retries'));
701
- const { options } = cli.parse('node bin --max-retries 5'.split(' '));
741
+ const { options } = await cli.parse('node bin --max-retries 5'.split(' '));
702
742
  expect(options.maxRetries).toBe(5);
703
743
  expect(typeof options.maxRetries).toBe('number');
704
744
  });
705
745
  });
706
746
  describe('edge cases: empty string values', () => {
707
- test('empty string with string schema stays empty string', () => {
747
+ test('empty string with string schema stays empty string', async () => {
708
748
  const cli = goke();
709
749
  cli.option('--name <name>', z.string().describe('Name'));
710
- const { options } = cli.parse(['node', 'bin', '--name', '']);
750
+ const { options } = await cli.parse(['node', 'bin', '--name', '']);
711
751
  expect(options.name).toBe('');
712
752
  });
713
- test('empty string with number schema throws', () => {
753
+ test('empty string with number schema throws', async () => {
714
754
  const cli = gokeTestable();
715
755
  cli.option('--port <port>', z.number().describe('Port'));
716
- expect(() => cli.parse(['node', 'bin', '--port', '']))
717
- .toThrow('expected number, got empty string');
756
+ await expect(cli.parse(['node', 'bin', '--port', '']))
757
+ .rejects.toThrow('expected number, got empty string');
718
758
  });
719
- test('empty string with nullable number schema returns null', () => {
759
+ test('empty string with nullable number schema returns null', async () => {
720
760
  const cli = goke();
721
761
  cli.option('--timeout <timeout>', z.nullable(z.number()).describe('Timeout'));
722
- const { options } = cli.parse(['node', 'bin', '--timeout', '']);
762
+ const { options } = await cli.parse(['node', 'bin', '--timeout', '']);
723
763
  expect(options.timeout).toBe(null);
724
764
  });
725
765
  });
726
766
  describe('edge cases: global options with schema in subcommands', () => {
727
- test('global option schema applies to subcommand parsing', () => {
767
+ test('global option schema applies to subcommand parsing', async () => {
728
768
  const cli = goke();
729
769
  let result = {};
730
770
  cli.option('--port <port>', z.number().describe('Port'));
731
771
  cli
732
772
  .command('serve', 'Start server')
733
773
  .action((options) => { result = options; });
734
- cli.parse('node bin serve --port 3000'.split(' '), { run: true });
774
+ await cli.parse('node bin serve --port 3000'.split(' '), { run: true });
735
775
  expect(result.port).toBe(3000);
736
776
  expect(typeof result.port).toBe('number');
737
777
  });
738
778
  });
739
779
  describe('edge cases: short alias + schema', () => {
740
- test('short alias repeated with array schema', () => {
780
+ test('short alias repeated with array schema', async () => {
741
781
  const cli = goke();
742
782
  cli.option('-t, --tag <tag>', z.array(z.string()).describe('Tags'));
743
- const { options } = cli.parse('node bin -t foo -t bar'.split(' '));
783
+ const { options } = await cli.parse('node bin -t foo -t bar'.split(' '));
744
784
  expect(options.tag).toEqual(['foo', 'bar']);
745
785
  expect(options.t).toEqual(['foo', 'bar']);
746
786
  });
747
- test('short alias single value with array schema wraps', () => {
787
+ test('short alias single value with array schema wraps', async () => {
748
788
  const cli = goke();
749
789
  cli.option('-t, --tag <tag>', z.array(z.string()).describe('Tags'));
750
- const { options } = cli.parse('node bin -t foo'.split(' '));
790
+ const { options } = await cli.parse('node bin -t foo'.split(' '));
751
791
  expect(options.tag).toEqual(['foo']);
752
792
  });
753
- test('short alias with number schema coerces', () => {
793
+ test('short alias with number schema coerces', async () => {
754
794
  const cli = goke();
755
795
  cli.option('-p, --port <port>', z.number().describe('Port'));
756
- const { options } = cli.parse('node bin -p 8080'.split(' '));
796
+ const { options } = await cli.parse('node bin -p 8080'.split(' '));
757
797
  expect(options.port).toBe(8080);
758
798
  expect(options.p).toBe(8080);
759
799
  });
760
- test('short alias repeated with non-array schema throws', () => {
800
+ test('short alias repeated with non-array schema throws', async () => {
761
801
  const cli = gokeTestable();
762
802
  cli.option('-p, --port <port>', z.number().describe('Port'));
763
- expect(() => cli.parse('node bin -p 3000 -p 4000'.split(' ')))
764
- .toThrow('does not accept multiple values');
803
+ await expect(cli.parse('node bin -p 3000 -p 4000'.split(' ')))
804
+ .rejects.toThrow('does not accept multiple values');
765
805
  });
766
806
  });
767
- test('throw on unknown options', () => {
807
+ test('throw on unknown options', async () => {
768
808
  const cli = gokeTestable();
769
809
  cli
770
810
  .command('build [entry]', 'Build your app')
771
811
  .option('--foo-bar', 'foo bar')
772
812
  .option('--aB', 'ab')
773
813
  .action(() => { });
774
- expect(() => {
775
- cli.parse(`node bin build app.js --fooBar --a-b --xx`.split(' '));
776
- }).toThrowError('Unknown option `--xx`');
814
+ await expect(cli.parse(`node bin build app.js --fooBar --a-b --xx`.split(' ')))
815
+ .rejects.toThrowError('Unknown option `--xx`');
777
816
  });
778
817
  describe('space-separated subcommands', () => {
779
- test('basic subcommand matching', () => {
818
+ test('basic subcommand matching', async () => {
780
819
  const cli = goke();
781
820
  let matched = '';
782
821
  cli.command('mcp login', 'Login to MCP').action(() => {
783
822
  matched = 'mcp login';
784
823
  });
785
- cli.parse(['node', 'bin', 'mcp', 'login'], { run: true });
824
+ await cli.parse(['node', 'bin', 'mcp', 'login'], { run: true });
786
825
  expect(matched).toBe('mcp login');
787
826
  expect(cli.matchedCommandName).toBe('mcp login');
788
827
  });
789
- test('subcommand with positional args', () => {
828
+ test('subcommand with positional args', async () => {
790
829
  const cli = goke();
791
830
  let receivedId = '';
792
831
  cli.command('mcp getNodeXml <id>', 'Get XML for a node').action((id) => {
793
832
  receivedId = id;
794
833
  });
795
- cli.parse(['node', 'bin', 'mcp', 'getNodeXml', '123'], { run: true });
834
+ await cli.parse(['node', 'bin', 'mcp', 'getNodeXml', '123'], { run: true });
796
835
  expect(receivedId).toBe('123');
797
836
  expect(cli.matchedCommandName).toBe('mcp getNodeXml');
798
837
  });
799
- test('subcommand with options', () => {
838
+ test('subcommand with options', async () => {
800
839
  const cli = goke();
801
840
  let result = {};
802
841
  cli
@@ -805,12 +844,12 @@ describe('space-separated subcommands', () => {
805
844
  .action((id, options) => {
806
845
  result = { id, format: options.format };
807
846
  });
808
- cli.parse(['node', 'bin', 'mcp', 'export', 'abc', '--format', 'json'], {
847
+ await cli.parse(['node', 'bin', 'mcp', 'export', 'abc', '--format', 'json'], {
809
848
  run: true,
810
849
  });
811
850
  expect(result).toEqual({ id: 'abc', format: 'json' });
812
851
  });
813
- test('greedy matching - longer commands match first', () => {
852
+ test('greedy matching - longer commands match first', async () => {
814
853
  const cli = goke();
815
854
  let matched = '';
816
855
  cli.command('mcp', 'MCP base command').action(() => {
@@ -819,30 +858,30 @@ describe('space-separated subcommands', () => {
819
858
  cli.command('mcp login', 'Login to MCP').action(() => {
820
859
  matched = 'mcp login';
821
860
  });
822
- cli.parse(['node', 'bin', 'mcp', 'login'], { run: true });
861
+ await cli.parse(['node', 'bin', 'mcp', 'login'], { run: true });
823
862
  expect(matched).toBe('mcp login');
824
863
  });
825
- test('three-level subcommand', () => {
864
+ test('three-level subcommand', async () => {
826
865
  const cli = goke();
827
866
  let matched = '';
828
867
  cli.command('git remote add', 'Add a remote').action(() => {
829
868
  matched = 'git remote add';
830
869
  });
831
- cli.parse(['node', 'bin', 'git', 'remote', 'add'], { run: true });
870
+ await cli.parse(['node', 'bin', 'git', 'remote', 'add'], { run: true });
832
871
  expect(matched).toBe('git remote add');
833
872
  expect(cli.matchedCommandName).toBe('git remote add');
834
873
  });
835
- test('single-word commands still work (backward compatibility)', () => {
874
+ test('single-word commands still work (backward compatibility)', async () => {
836
875
  const cli = goke();
837
876
  let matched = '';
838
877
  cli.command('build', 'Build the project').action(() => {
839
878
  matched = 'build';
840
879
  });
841
- cli.parse(['node', 'bin', 'build'], { run: true });
880
+ await cli.parse(['node', 'bin', 'build'], { run: true });
842
881
  expect(matched).toBe('build');
843
882
  expect(cli.matchedCommandName).toBe('build');
844
883
  });
845
- test('subcommand does not match when args are insufficient', () => {
884
+ test('subcommand does not match when args are insufficient', async () => {
846
885
  const cli = goke();
847
886
  let matched = '';
848
887
  cli.command('mcp login', 'Login to MCP').action(() => {
@@ -851,10 +890,10 @@ describe('space-separated subcommands', () => {
851
890
  cli.command('mcp', 'MCP base').action(() => {
852
891
  matched = 'mcp base';
853
892
  });
854
- cli.parse(['node', 'bin', 'mcp'], { run: true });
893
+ await cli.parse(['node', 'bin', 'mcp'], { run: true });
855
894
  expect(matched).toBe('mcp base');
856
895
  });
857
- test('default command should not match if args are prefix of another command', () => {
896
+ test('default command should not match if args are prefix of another command', async () => {
858
897
  const cli = goke();
859
898
  let matched = '';
860
899
  cli.command('mcp login', 'Login to MCP').action(() => {
@@ -863,11 +902,11 @@ describe('space-separated subcommands', () => {
863
902
  cli.command('', 'Default command').action(() => {
864
903
  matched = 'default';
865
904
  });
866
- cli.parse(['node', 'bin', 'mcp'], { run: true });
905
+ await cli.parse(['node', 'bin', 'mcp'], { run: true });
867
906
  expect(matched).toBe('');
868
907
  expect(cli.matchedCommand).toBeUndefined();
869
908
  });
870
- test('default command should match when args do not prefix any command', () => {
909
+ test('default command should match when args do not prefix any command', async () => {
871
910
  const cli = goke();
872
911
  let matched = '';
873
912
  let receivedArg = '';
@@ -878,11 +917,11 @@ describe('space-separated subcommands', () => {
878
917
  matched = 'default';
879
918
  receivedArg = file;
880
919
  });
881
- cli.parse(['node', 'bin', 'foo'], { run: true });
920
+ await cli.parse(['node', 'bin', 'foo'], { run: true });
882
921
  expect(matched).toBe('default');
883
922
  expect(receivedArg).toBe('foo');
884
923
  });
885
- test('help output with subcommands', () => {
924
+ test('help output with subcommands', async () => {
886
925
  let output = '';
887
926
  const cli = goke('mycli', {
888
927
  stdout: { write(data) { output += data; } },
@@ -895,7 +934,7 @@ describe('space-separated subcommands', () => {
895
934
  cli.command('build', 'Build the project').option('--watch', 'Watch mode');
896
935
  cli.help();
897
936
  // parse with --help triggers outputHelp() internally, which writes to our captured stdout
898
- cli.parse(['node', 'bin', '--help'], { run: false });
937
+ await cli.parse(['node', 'bin', '--help'], { run: false });
899
938
  expect(stripAnsi(output)).toMatchInlineSnapshot(`
900
939
  "mycli
901
940
 
@@ -930,7 +969,7 @@ describe('space-separated subcommands', () => {
930
969
  "
931
970
  `);
932
971
  });
933
- test('unknown subcommand shows filtered help for prefix', () => {
972
+ test('unknown subcommand shows filtered help for prefix', async () => {
934
973
  let output = '';
935
974
  const cli = goke('mycli', {
936
975
  stdout: { write(data) { output += data; } },
@@ -941,7 +980,7 @@ describe('space-separated subcommands', () => {
941
980
  cli.command('build', 'Build project');
942
981
  cli.help();
943
982
  // User types "mcp nonexistent" - should show help for mcp commands
944
- cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true });
983
+ await cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true });
945
984
  expect(cli.matchedCommand).toBeUndefined();
946
985
  const normalizedOutput = stripAnsi(output);
947
986
  expect(normalizedOutput).toContain('Unknown command: mcp nonexistent');
@@ -951,7 +990,7 @@ describe('space-separated subcommands', () => {
951
990
  expect(normalizedOutput).toContain('mcp status');
952
991
  expect(normalizedOutput).not.toContain('build');
953
992
  });
954
- test('unknown command without prefix does not show filtered help', () => {
993
+ test('unknown command without prefix does not show filtered help', async () => {
955
994
  let output = '';
956
995
  const cli = goke('mycli', {
957
996
  stdout: { write(data) { output += data; } },
@@ -960,11 +999,11 @@ describe('space-separated subcommands', () => {
960
999
  cli.command('build', 'Build project');
961
1000
  cli.help();
962
1001
  // User types "foo" - no commands start with "foo"
963
- cli.parse(['node', 'bin', 'foo'], { run: true });
1002
+ await cli.parse(['node', 'bin', 'foo'], { run: true });
964
1003
  // Should not show filtered help since "foo" is not a prefix of any command
965
1004
  expect(stripAnsi(output)).not.toContain('Available "foo" commands');
966
1005
  });
967
- test('unknown command without prefix outputs root help', () => {
1006
+ test('unknown command without prefix outputs root help', async () => {
968
1007
  let output = '';
969
1008
  const cli = goke('mycli', {
970
1009
  stdout: { write(data) { output += data; } },
@@ -973,26 +1012,80 @@ describe('space-separated subcommands', () => {
973
1012
  cli.command('build', 'Build project');
974
1013
  cli.help();
975
1014
  // User types an unknown command that does not match any prefix group
976
- cli.parse(['node', 'bin', 'something'], { run: true });
1015
+ await cli.parse(['node', 'bin', 'something'], { run: true });
977
1016
  expect(cli.matchedCommand).toBeUndefined();
978
1017
  expect(stripAnsi(output)).toContain('Usage:');
979
1018
  expect(stripAnsi(output)).toContain('$ mycli <command> [options]');
980
1019
  expect(stripAnsi(output)).toContain('mcp login');
981
1020
  expect(stripAnsi(output)).toContain('build');
982
1021
  });
983
- test('no args without default command outputs root help', () => {
1022
+ test('no args without default command outputs root help', async () => {
984
1023
  const stdout = createTestOutputStream();
985
1024
  const cli = goke('mycli', { stdout });
986
1025
  cli.command('mcp login', 'Login to MCP');
987
1026
  cli.command('build', 'Build project');
988
1027
  cli.help();
989
- cli.parse(['node', 'bin'], { run: true });
1028
+ await cli.parse(['node', 'bin'], { run: true });
990
1029
  expect(stdout.text).toContain('Usage:');
991
1030
  expect(stdout.text).toContain('$ mycli <command> [options]');
992
1031
  expect(stdout.text).toContain('mcp login');
993
1032
  expect(stdout.text).toContain('build');
994
1033
  });
995
- test('prefix --help shows filtered help for matching command group', () => {
1034
+ test('default command with no args rejects unknown positional args', async () => {
1035
+ const stdout = createTestOutputStream();
1036
+ let defaultRan = false;
1037
+ let unknownFired = false;
1038
+ const cli = gokeTestable('playwriter', { stdout });
1039
+ cli.command('', 'Start the MCP server').action(async () => { defaultRan = true; });
1040
+ cli.command('session new', 'Create session').action(() => { });
1041
+ cli.help();
1042
+ cli.on('command:*', () => { unknownFired = true; });
1043
+ await cli.parse(['node', 'bin', 'run'], { run: true });
1044
+ expect(defaultRan).toBe(false);
1045
+ expect(unknownFired).toBe(true);
1046
+ expect(cli.matchedCommand).toBeUndefined();
1047
+ });
1048
+ test('default command with no args still runs when no args passed', async () => {
1049
+ let defaultRan = false;
1050
+ const cli = gokeTestable('playwriter');
1051
+ cli.command('', 'Start the MCP server').action(async () => { defaultRan = true; });
1052
+ cli.command('session new', 'Create session').action(() => { });
1053
+ await cli.parse(['node', 'bin'], { run: true });
1054
+ expect(defaultRan).toBe(true);
1055
+ });
1056
+ test('default command with no args still works with -- separator', async () => {
1057
+ let defaultRan = false;
1058
+ let receivedOptions = null;
1059
+ const cli = gokeTestable('playwriter');
1060
+ cli.command('', 'Start the MCP server').action(async (options) => {
1061
+ defaultRan = true;
1062
+ receivedOptions = options;
1063
+ });
1064
+ await cli.parse(['node', 'bin', '--', 'extra', 'args'], { run: true });
1065
+ expect(defaultRan).toBe(true);
1066
+ expect(receivedOptions['--']).toEqual(['extra', 'args']);
1067
+ });
1068
+ test('default command WITH positional args still accepts args', async () => {
1069
+ let receivedScript;
1070
+ const cli = gokeTestable('runner');
1071
+ cli.command('[script]', 'Run a script').action(async (script) => {
1072
+ receivedScript = script;
1073
+ });
1074
+ await cli.parse(['node', 'bin', 'deploy'], { run: true });
1075
+ expect(receivedScript).toBe('deploy');
1076
+ });
1077
+ test('default command rejects unknown nonexistent command', async () => {
1078
+ let defaultRan = false;
1079
+ let unknownFired = false;
1080
+ const cli = gokeTestable('mycli');
1081
+ cli.command('', 'Default').action(async () => { defaultRan = true; });
1082
+ cli.command('build', 'Build').action(() => { });
1083
+ cli.on('command:*', () => { unknownFired = true; });
1084
+ await cli.parse(['node', 'bin', 'nonexistent'], { run: true });
1085
+ expect(defaultRan).toBe(false);
1086
+ expect(unknownFired).toBe(true);
1087
+ });
1088
+ test('prefix --help shows filtered help for matching command group', async () => {
996
1089
  let output = '';
997
1090
  const cli = goke('mycli', {
998
1091
  stdout: { write(data) { output += data; } },
@@ -1002,7 +1095,7 @@ describe('space-separated subcommands', () => {
1002
1095
  cli.command('mcp status', 'Show status');
1003
1096
  cli.command('build', 'Build project');
1004
1097
  cli.help();
1005
- cli.parse(['node', 'bin', 'mcp', '--help'], { run: true });
1098
+ await cli.parse(['node', 'bin', 'mcp', '--help'], { run: true });
1006
1099
  const normalizedOutput = stripAnsi(output);
1007
1100
  expect(normalizedOutput).toMatchInlineSnapshot(`
1008
1101
  "mycli
@@ -1019,7 +1112,7 @@ describe('space-separated subcommands', () => {
1019
1112
  });
1020
1113
  });
1021
1114
  describe('many commands with root command (empty string)', () => {
1022
- test('root command runs when no subcommand given', () => {
1115
+ test('root command runs when no subcommand given', async () => {
1023
1116
  const cli = goke('deploy');
1024
1117
  let matched = '';
1025
1118
  cli.command('', 'Deploy the current project').action(() => {
@@ -1031,10 +1124,10 @@ describe('many commands with root command (empty string)', () => {
1031
1124
  cli.command('login', 'Authenticate').action(() => {
1032
1125
  matched = 'login';
1033
1126
  });
1034
- cli.parse(['node', 'bin'], { run: true });
1127
+ await cli.parse(['node', 'bin'], { run: true });
1035
1128
  expect(matched).toBe('root');
1036
1129
  });
1037
- test('root command receives options', () => {
1130
+ test('root command receives options', async () => {
1038
1131
  const cli = goke('deploy');
1039
1132
  let result = {};
1040
1133
  cli
@@ -1046,11 +1139,11 @@ describe('many commands with root command (empty string)', () => {
1046
1139
  });
1047
1140
  cli.command('init', 'Initialize project').action(() => { });
1048
1141
  cli.command('login', 'Authenticate').action(() => { });
1049
- cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: true });
1142
+ await cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: true });
1050
1143
  expect(result.env).toBe('staging');
1051
1144
  expect(result.dryRun).toBe(true);
1052
1145
  });
1053
- test('root command uses defaults when no options given', () => {
1146
+ test('root command uses defaults when no options given', async () => {
1054
1147
  const cli = goke('deploy');
1055
1148
  let result = {};
1056
1149
  cli
@@ -1060,10 +1153,10 @@ describe('many commands with root command (empty string)', () => {
1060
1153
  result = options;
1061
1154
  });
1062
1155
  cli.command('init', 'Initialize project').action(() => { });
1063
- cli.parse(['node', 'bin'], { run: true });
1156
+ await cli.parse(['node', 'bin'], { run: true });
1064
1157
  expect(result.env).toBe('production');
1065
1158
  });
1066
- test('subcommands take priority over root command', () => {
1159
+ test('subcommands take priority over root command', async () => {
1067
1160
  const cli = goke('deploy');
1068
1161
  let matched = '';
1069
1162
  cli.command('', 'Deploy the current project').action(() => {
@@ -1078,10 +1171,10 @@ describe('many commands with root command (empty string)', () => {
1078
1171
  cli.command('status', 'Show status').action(() => {
1079
1172
  matched = 'status';
1080
1173
  });
1081
- cli.parse(['node', 'bin', 'status'], { run: true });
1174
+ await cli.parse(['node', 'bin', 'status'], { run: true });
1082
1175
  expect(matched).toBe('status');
1083
1176
  });
1084
- test('subcommand with args works alongside root command', () => {
1177
+ test('subcommand with args works alongside root command', async () => {
1085
1178
  const cli = goke('deploy');
1086
1179
  let rootCalled = false;
1087
1180
  let logsResult = {};
@@ -1095,13 +1188,13 @@ describe('many commands with root command (empty string)', () => {
1095
1188
  .action((deploymentId, options) => {
1096
1189
  logsResult = { deploymentId, ...options };
1097
1190
  });
1098
- cli.parse(['node', 'bin', 'logs', 'abc123', '--follow', '--lines', '50'], { run: true });
1191
+ await cli.parse(['node', 'bin', 'logs', 'abc123', '--follow', '--lines', '50'], { run: true });
1099
1192
  expect(rootCalled).toBe(false);
1100
1193
  expect(logsResult.deploymentId).toBe('abc123');
1101
1194
  expect(logsResult.follow).toBe(true);
1102
1195
  expect(logsResult.lines).toBe(50);
1103
1196
  });
1104
- test('help shows root and all subcommands', () => {
1197
+ test('help shows root and all subcommands', async () => {
1105
1198
  const stdout = createTestOutputStream();
1106
1199
  const cli = goke('deploy', { stdout });
1107
1200
  cli
@@ -1113,7 +1206,7 @@ describe('many commands with root command (empty string)', () => {
1113
1206
  cli.command('status', 'Show deployment status');
1114
1207
  cli.command('logs <deploymentId>', 'Stream logs for a deployment');
1115
1208
  cli.help();
1116
- cli.parse(['node', 'bin', '--help'], { run: false });
1209
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1117
1210
  expect(stdout.text).toContain('init');
1118
1211
  expect(stdout.text).toContain('login');
1119
1212
  expect(stdout.text).toContain('logout');
@@ -1122,7 +1215,7 @@ describe('many commands with root command (empty string)', () => {
1122
1215
  expect(stdout.text).toContain('Initialize a new project');
1123
1216
  expect(stdout.text).toContain('Stream logs for a deployment');
1124
1217
  });
1125
- test('root help with many commands renders examples section after options', () => {
1218
+ test('root help with many commands renders examples section after options', async () => {
1126
1219
  const stdout = createTestOutputStream();
1127
1220
  const cli = goke('deploy', { stdout });
1128
1221
  cli
@@ -1137,7 +1230,7 @@ describe('many commands with root command (empty string)', () => {
1137
1230
  cli.command('status', 'Show deployment status');
1138
1231
  cli.command('logs <deploymentId>', 'Stream logs for a deployment');
1139
1232
  cli.help();
1140
- cli.parse(['node', 'bin', '--help'], { run: false });
1233
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1141
1234
  expect(stdout.text).toMatchInlineSnapshot(`
1142
1235
  "deploy
1143
1236
 
@@ -1177,7 +1270,7 @@ describe('many commands with root command (empty string)', () => {
1177
1270
  "
1178
1271
  `);
1179
1272
  });
1180
- test('subcommand help renders command examples at the end', () => {
1273
+ test('subcommand help renders command examples at the end', async () => {
1181
1274
  const stdout = createTestOutputStream();
1182
1275
  const cli = goke('deploy', { stdout, columns: 80 });
1183
1276
  cli.command('', 'Deploy the current project');
@@ -1192,7 +1285,7 @@ describe('many commands with root command (empty string)', () => {
1192
1285
  .example('# Keep following new log lines')
1193
1286
  .example('deploy logs dep_123 --follow');
1194
1287
  cli.help();
1195
- cli.parse(['node', 'bin', 'logs', '--help'], { run: false });
1288
+ await cli.parse(['node', 'bin', 'logs', '--help'], { run: false });
1196
1289
  expect(stdout.text).toMatchInlineSnapshot(`
1197
1290
  "deploy
1198
1291
 
@@ -1219,7 +1312,7 @@ describe('many commands with root command (empty string)', () => {
1219
1312
  "
1220
1313
  `);
1221
1314
  });
1222
- test('root help labels default command with cli name and does not duplicate global options', () => {
1315
+ test('root help labels default command with cli name and does not duplicate global options', async () => {
1223
1316
  const stdout = createTestOutputStream();
1224
1317
  const cli = goke('deploy', { stdout });
1225
1318
  cli.option('--env <env>', 'Target environment');
@@ -1229,7 +1322,7 @@ describe('many commands with root command (empty string)', () => {
1229
1322
  .option('--dry-run', 'Preview without deploying');
1230
1323
  cli.command('status', 'Show deployment status');
1231
1324
  cli.help();
1232
- cli.parse(['node', 'bin', '--help'], { run: false });
1325
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1233
1326
  expect(stdout.text).toMatchInlineSnapshot(`
1234
1327
  "deploy
1235
1328
 
@@ -1252,7 +1345,7 @@ describe('many commands with root command (empty string)', () => {
1252
1345
  "
1253
1346
  `);
1254
1347
  });
1255
- test('root help wraps long command descriptions snapshot', () => {
1348
+ test('root help wraps long command descriptions snapshot', async () => {
1256
1349
  const stdout = createTestOutputStream();
1257
1350
  const cli = goke('mycli', { stdout, columns: 56 });
1258
1351
  cli.command('notion-search', 'Perform a semantic search over Notion workspace content and connected integrations with advanced filtering options, date filters, and creator filters.')
@@ -1260,7 +1353,7 @@ describe('many commands with root command (empty string)', () => {
1260
1353
  .option('--limit [limit]', z.number().default(10).describe('Maximum number of results to return'));
1261
1354
  cli.command('notion-fetch', 'Retrieve a Notion page or database by URL or ID and render the result in enhanced markdown format for terminal output.').option('--id <id>', 'Notion URL or UUID to fetch');
1262
1355
  cli.help();
1263
- cli.parse(['node', 'bin', '--help'], { run: false });
1356
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1264
1357
  expect(stdout.text).toMatchInlineSnapshot(`
1265
1358
  "mycli
1266
1359
 
@@ -1295,7 +1388,7 @@ describe('many commands with root command (empty string)', () => {
1295
1388
  "
1296
1389
  `);
1297
1390
  });
1298
- test('root help aligns command descriptions with mixed command lengths', () => {
1391
+ test('root help aligns command descriptions with mixed command lengths', async () => {
1299
1392
  const stdout = createTestOutputStream();
1300
1393
  const cli = goke('gtui', { stdout, columns: 120 });
1301
1394
  cli.command('auth login', 'Authenticate with Google (opens browser)');
@@ -1303,7 +1396,7 @@ describe('many commands with root command (empty string)', () => {
1303
1396
  cli.command('mail list', 'List email threads').option('--folder [folder]', 'Folder to list');
1304
1397
  cli.command('attachment get <messageId> <attachmentId>', 'Download an attachment');
1305
1398
  cli.help();
1306
- cli.parse(['node', 'bin', '--help'], { run: false });
1399
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1307
1400
  expect(stdout.text).toMatchInlineSnapshot(`
1308
1401
  "gtui
1309
1402
 
@@ -1334,16 +1427,16 @@ describe('many commands with root command (empty string)', () => {
1334
1427
  "
1335
1428
  `);
1336
1429
  });
1337
- test('root help wraps all multi-line description lines', () => {
1430
+ test('root help wraps all multi-line description lines', async () => {
1338
1431
  const stdout = createTestOutputStream();
1339
1432
  const cli = goke('mycli', { stdout, columns: 64 });
1340
1433
  cli.command('notion-create', 'Create a new page.\n {"title":"Example"}\n {"done":true}');
1341
1434
  cli.help();
1342
- cli.parse(['node', 'bin', '--help'], { run: false });
1435
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1343
1436
  expect(stdout.text).toContain('{"title":"Example"}');
1344
1437
  expect(stdout.text).toContain('{"done":true}');
1345
1438
  });
1346
- test('root help snapshot when columns is undefined (no wrapping fallback)', () => {
1439
+ test('root help snapshot when columns is undefined (no wrapping fallback)', async () => {
1347
1440
  const stdout = createTestOutputStream();
1348
1441
  const originalColumns = process.stdout.columns;
1349
1442
  Object.defineProperty(process.stdout, 'columns', {
@@ -1356,7 +1449,7 @@ describe('many commands with root command (empty string)', () => {
1356
1449
  .option('--query <query>', 'Natural language query text to search for')
1357
1450
  .option('--limit [limit]', z.number().default(10).describe('Maximum number of results to return'));
1358
1451
  cli.help();
1359
- cli.parse(['node', 'bin', '--help'], { run: false });
1452
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1360
1453
  expect(stdout.text).toMatchInlineSnapshot(`
1361
1454
  "mycli
1362
1455
 
@@ -1384,7 +1477,7 @@ describe('many commands with root command (empty string)', () => {
1384
1477
  });
1385
1478
  }
1386
1479
  });
1387
- test('many subcommands all resolve correctly', () => {
1480
+ test('many subcommands all resolve correctly', async () => {
1388
1481
  const cli = goke('deploy');
1389
1482
  let matched = '';
1390
1483
  cli.command('', 'Root').action(() => { matched = 'root'; });
@@ -1396,63 +1489,63 @@ describe('many commands with root command (empty string)', () => {
1396
1489
  cli.command('rollback <id>', 'Rollback').action(() => { matched = 'rollback'; });
1397
1490
  cli.command('config set <key> <value>', 'Set config').action(() => { matched = 'config set'; });
1398
1491
  // Test each command resolves to the right one
1399
- cli.parse(['node', 'bin'], { run: true });
1492
+ await cli.parse(['node', 'bin'], { run: true });
1400
1493
  expect(matched).toBe('root');
1401
1494
  matched = '';
1402
- cli.parse(['node', 'bin', 'init'], { run: true });
1495
+ await cli.parse(['node', 'bin', 'init'], { run: true });
1403
1496
  expect(matched).toBe('init');
1404
1497
  matched = '';
1405
- cli.parse(['node', 'bin', 'login'], { run: true });
1498
+ await cli.parse(['node', 'bin', 'login'], { run: true });
1406
1499
  expect(matched).toBe('login');
1407
1500
  matched = '';
1408
- cli.parse(['node', 'bin', 'logout'], { run: true });
1501
+ await cli.parse(['node', 'bin', 'logout'], { run: true });
1409
1502
  expect(matched).toBe('logout');
1410
1503
  matched = '';
1411
- cli.parse(['node', 'bin', 'status'], { run: true });
1504
+ await cli.parse(['node', 'bin', 'status'], { run: true });
1412
1505
  expect(matched).toBe('status');
1413
1506
  matched = '';
1414
- cli.parse(['node', 'bin', 'logs', 'dep-123'], { run: true });
1507
+ await cli.parse(['node', 'bin', 'logs', 'dep-123'], { run: true });
1415
1508
  expect(matched).toBe('logs');
1416
1509
  matched = '';
1417
- cli.parse(['node', 'bin', 'rollback', 'dep-456'], { run: true });
1510
+ await cli.parse(['node', 'bin', 'rollback', 'dep-456'], { run: true });
1418
1511
  expect(matched).toBe('rollback');
1419
1512
  matched = '';
1420
- cli.parse(['node', 'bin', 'config', 'set', 'region', 'us-east-1'], { run: true });
1513
+ await cli.parse(['node', 'bin', 'config', 'set', 'region', 'us-east-1'], { run: true });
1421
1514
  expect(matched).toBe('config set');
1422
1515
  });
1423
1516
  });
1424
1517
  describe('stdout/stderr/argv injection', () => {
1425
- test('stdout captures help output', () => {
1518
+ test('stdout captures help output', async () => {
1426
1519
  const stdout = createTestOutputStream();
1427
1520
  const cli = goke('mycli', { stdout });
1428
1521
  cli.command('serve', 'Start server');
1429
1522
  cli.help();
1430
- cli.parse(['node', 'bin', '--help'], { run: false });
1523
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1431
1524
  cli.outputHelp();
1432
1525
  expect(stdout.text).toContain('mycli');
1433
1526
  expect(stdout.text).toContain('serve');
1434
1527
  expect(stdout.text).toContain('Start server');
1435
1528
  });
1436
- test('stdout captures version output', () => {
1529
+ test('stdout captures version output', async () => {
1437
1530
  const stdout = createTestOutputStream();
1438
1531
  const cli = goke('mycli', { stdout });
1439
1532
  cli.version('1.2.3');
1440
- cli.parse(['node', 'bin', '--version'], { run: false });
1533
+ await cli.parse(['node', 'bin', '--version'], { run: false });
1441
1534
  cli.outputVersion();
1442
1535
  expect(stdout.text).toContain('mycli/1.2.3');
1443
1536
  });
1444
- test('stdout captures prefix help for unknown subcommands', () => {
1537
+ test('stdout captures prefix help for unknown subcommands', async () => {
1445
1538
  const stdout = createTestOutputStream();
1446
1539
  const cli = goke('mycli', { stdout });
1447
1540
  cli.command('mcp login', 'Login to MCP');
1448
1541
  cli.command('mcp logout', 'Logout from MCP');
1449
1542
  cli.help();
1450
- cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true });
1543
+ await cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true });
1451
1544
  expect(stdout.text).toContain('Unknown command: mcp nonexistent');
1452
1545
  expect(stdout.text).toContain('mcp login');
1453
1546
  expect(stdout.text).toContain('mcp logout');
1454
1547
  });
1455
- test('stderr is separate from stdout', () => {
1548
+ test('stderr is separate from stdout', async () => {
1456
1549
  const stdout = createTestOutputStream();
1457
1550
  const stderr = createTestOutputStream();
1458
1551
  const cli = goke('mycli', { stdout, stderr });
@@ -1461,7 +1554,7 @@ describe('stdout/stderr/argv injection', () => {
1461
1554
  expect(stdout.text).toBe('hello stdout\n');
1462
1555
  expect(stderr.text).toBe('hello stderr\n');
1463
1556
  });
1464
- test('argv option is used as default in parse()', () => {
1557
+ test('argv option is used as default in parse()', async () => {
1465
1558
  const cli = goke('mycli', {
1466
1559
  argv: ['node', 'bin', 'serve', '--port', '3000'],
1467
1560
  });
@@ -1471,10 +1564,10 @@ describe('stdout/stderr/argv injection', () => {
1471
1564
  .option('--port <port>', z.number().describe('Port'))
1472
1565
  .action((options) => { result = options; });
1473
1566
  // parse() without args uses the injected argv
1474
- cli.parse();
1567
+ await cli.parse();
1475
1568
  expect(result.port).toBe(3000);
1476
1569
  });
1477
- test('parse(customArgv) overrides injected argv', () => {
1570
+ test('parse(customArgv) overrides injected argv', async () => {
1478
1571
  const cli = goke('mycli', {
1479
1572
  argv: ['node', 'bin', 'serve', '--port', '3000'],
1480
1573
  });
@@ -1484,16 +1577,16 @@ describe('stdout/stderr/argv injection', () => {
1484
1577
  .option('--port <port>', z.number().describe('Port'))
1485
1578
  .action((options) => { result = options; });
1486
1579
  // Explicit argv overrides the default
1487
- cli.parse(['node', 'bin', 'serve', '--port', '8080']);
1580
+ await cli.parse(['node', 'bin', 'serve', '--port', '8080']);
1488
1581
  expect(result.port).toBe(8080);
1489
1582
  });
1490
- test('default behavior without options uses process.stdout', () => {
1583
+ test('default behavior without options uses process.stdout', async () => {
1491
1584
  const cli = goke('mycli');
1492
1585
  // stdout/stderr should be process.stdout/process.stderr by default
1493
1586
  expect(cli.stdout).toBe(process.stdout);
1494
1587
  expect(cli.stderr).toBe(process.stderr);
1495
1588
  });
1496
- test('createConsole routes log to stdout and error to stderr', () => {
1589
+ test('createConsole routes log to stdout and error to stderr', async () => {
1497
1590
  const stdout = createTestOutputStream();
1498
1591
  const stderr = createTestOutputStream();
1499
1592
  const con = createConsole(stdout, stderr);
@@ -1502,7 +1595,7 @@ describe('stdout/stderr/argv injection', () => {
1502
1595
  expect(stdout.text).toBe('msg1 msg2\n');
1503
1596
  expect(stderr.text).toBe('err1 err2\n');
1504
1597
  });
1505
- test('createConsole log with no args writes empty line', () => {
1598
+ test('createConsole log with no args writes empty line', async () => {
1506
1599
  const stdout = createTestOutputStream();
1507
1600
  const stderr = createTestOutputStream();
1508
1601
  const con = createConsole(stdout, stderr);
@@ -1511,27 +1604,27 @@ describe('stdout/stderr/argv injection', () => {
1511
1604
  });
1512
1605
  });
1513
1606
  describe('schema description and default extraction', () => {
1514
- test('description is extracted from schema and shown in help', () => {
1607
+ test('description is extracted from schema and shown in help', async () => {
1515
1608
  const stdout = createTestOutputStream();
1516
1609
  const cli = goke('mycli', { stdout });
1517
1610
  cli
1518
1611
  .command('serve', 'Start server')
1519
1612
  .option('--port <port>', z.number().describe('Port to listen on'));
1520
1613
  cli.help();
1521
- cli.parse(['node', 'bin', 'serve', '--help'], { run: false });
1614
+ await cli.parse(['node', 'bin', 'serve', '--help'], { run: false });
1522
1615
  expect(stdout.text).toContain('Port to listen on');
1523
1616
  });
1524
- test('default is extracted from schema and shown in help', () => {
1617
+ test('default is extracted from schema and shown in help', async () => {
1525
1618
  const stdout = createTestOutputStream();
1526
1619
  const cli = goke('mycli', { stdout });
1527
1620
  cli
1528
1621
  .command('serve', 'Start server')
1529
1622
  .option('--port [port]', z.number().default(3000).describe('Port'));
1530
1623
  cli.help();
1531
- cli.parse(['node', 'bin', 'serve', '--help'], { run: false });
1624
+ await cli.parse(['node', 'bin', 'serve', '--help'], { run: false });
1532
1625
  expect(stdout.text).toContain('(default: 3000)');
1533
1626
  });
1534
- test('deprecated options are hidden from help output', () => {
1627
+ test('deprecated options are hidden from help output', async () => {
1535
1628
  const stdout = createTestOutputStream();
1536
1629
  const cli = goke('mycli', { stdout });
1537
1630
  cli
@@ -1539,7 +1632,7 @@ describe('schema description and default extraction', () => {
1539
1632
  .option('--old <value>', z.string().meta({ deprecated: true, description: 'Old option' }))
1540
1633
  .option('--new <value>', z.string().describe('Normal option'));
1541
1634
  cli.help();
1542
- cli.parse(['node', 'bin', 'serve', '--help'], { run: false });
1635
+ await cli.parse(['node', 'bin', 'serve', '--help'], { run: false });
1543
1636
  // Normal option should be visible
1544
1637
  expect(stdout.text).toContain('--new');
1545
1638
  expect(stdout.text).toContain('Normal option');
@@ -1547,42 +1640,42 @@ describe('schema description and default extraction', () => {
1547
1640
  expect(stdout.text).not.toContain('--old');
1548
1641
  expect(stdout.text).not.toContain('Old option');
1549
1642
  });
1550
- test('deprecated option still works for parsing (just hidden from help)', () => {
1643
+ test('deprecated option still works for parsing (just hidden from help)', async () => {
1551
1644
  const cli = gokeTestable('mycli');
1552
1645
  let result = {};
1553
1646
  cli
1554
1647
  .command('serve', 'Start server')
1555
1648
  .option('--old <value>', z.string().meta({ deprecated: true, description: 'Old option' }))
1556
1649
  .action((options) => { result = options; });
1557
- cli.parse(['node', 'bin', 'serve', '--old', 'legacy-value']);
1650
+ await cli.parse(['node', 'bin', 'serve', '--old', 'legacy-value']);
1558
1651
  // Deprecated option should still be parsed and usable
1559
1652
  expect(result.old).toBe('legacy-value');
1560
1653
  });
1561
- test('deprecated options hidden from global help', () => {
1654
+ test('deprecated options hidden from global help', async () => {
1562
1655
  const stdout = createTestOutputStream();
1563
1656
  const cli = goke('mycli', { stdout });
1564
1657
  cli.option('--legacy [value]', z.string().meta({ deprecated: true, description: 'Deprecated global' }));
1565
1658
  cli.option('--current [value]', z.string().describe('Current option'));
1566
1659
  cli.help();
1567
- cli.parse(['node', 'bin', '--help'], { run: false });
1660
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1568
1661
  expect(stdout.text).toContain('--current');
1569
1662
  expect(stdout.text).toContain('Current option');
1570
1663
  expect(stdout.text).not.toContain('--legacy');
1571
1664
  expect(stdout.text).not.toContain('Deprecated global');
1572
1665
  });
1573
- test('hidden commands are not shown in help output', () => {
1666
+ test('hidden commands are not shown in help output', async () => {
1574
1667
  const stdout = createTestOutputStream();
1575
1668
  const cli = goke('mycli', { stdout });
1576
1669
  cli.command('visible', 'A visible command');
1577
1670
  cli.command('secret', 'A hidden command').hidden();
1578
1671
  cli.help();
1579
- cli.parse(['node', 'bin', '--help'], { run: false });
1672
+ await cli.parse(['node', 'bin', '--help'], { run: false });
1580
1673
  expect(stdout.text).toContain('visible');
1581
1674
  expect(stdout.text).toContain('A visible command');
1582
1675
  expect(stdout.text).not.toContain('secret');
1583
1676
  expect(stdout.text).not.toContain('A hidden command');
1584
1677
  });
1585
- test('hidden command still parses and runs', () => {
1678
+ test('hidden command still parses and runs', async () => {
1586
1679
  const cli = gokeTestable('mycli');
1587
1680
  let result = {};
1588
1681
  cli
@@ -1590,19 +1683,19 @@ describe('schema description and default extraction', () => {
1590
1683
  .hidden()
1591
1684
  .option('--value <v>', z.string().describe('some value'))
1592
1685
  .action((options) => { result = options; });
1593
- cli.parse(['node', 'bin', 'secret', '--value', 'hello']);
1686
+ await cli.parse(['node', 'bin', 'secret', '--value', 'hello']);
1594
1687
  expect(result.value).toBe('hello');
1595
1688
  });
1596
1689
  });
1597
1690
  describe('helpText()', () => {
1598
- test('returns help string without printing', () => {
1691
+ test('returns help string without printing', async () => {
1599
1692
  const stdout = createTestOutputStream();
1600
1693
  const cli = goke('mycli', { stdout });
1601
1694
  cli.command('serve', 'Start server');
1602
1695
  cli.option('--port <port>', 'Port number');
1603
1696
  cli.help();
1604
1697
  // parse a known command so help is not auto-triggered
1605
- cli.parse(['node', 'bin', 'serve'], { run: false });
1698
+ await cli.parse(['node', 'bin', 'serve'], { run: false });
1606
1699
  // reset stdout after parse
1607
1700
  stdout.lines.length = 0;
1608
1701
  const text = stripAnsi(cli.helpText());
@@ -1613,14 +1706,14 @@ describe('helpText()', () => {
1613
1706
  // helpText() does not print to stdout
1614
1707
  expect(stdout.text).toBe('');
1615
1708
  });
1616
- test('returns same content as outputHelp', () => {
1709
+ test('returns same content as outputHelp', async () => {
1617
1710
  const stdout = createTestOutputStream();
1618
1711
  const cli = goke('mycli', { stdout });
1619
1712
  cli.command('build', 'Build project');
1620
1713
  cli.option('--watch [watch]', 'Watch mode');
1621
1714
  cli.help();
1622
1715
  // parse a known command so help is not auto-triggered
1623
- cli.parse(['node', 'bin', 'build'], { run: false });
1716
+ await cli.parse(['node', 'bin', 'build'], { run: false });
1624
1717
  // reset stdout after parse
1625
1718
  stdout.lines.length = 0;
1626
1719
  const helpTextResult = stripAnsi(cli.helpText());
@@ -1629,18 +1722,18 @@ describe('helpText()', () => {
1629
1722
  const outputHelpResult = stdout.text.replace(/\n$/, '');
1630
1723
  expect(helpTextResult).toBe(outputHelpResult);
1631
1724
  });
1632
- test('returns subcommand help when command is matched', () => {
1725
+ test('returns subcommand help when command is matched', async () => {
1633
1726
  const cli = goke('mycli');
1634
1727
  cli.command('deploy <env>', 'Deploy to environment')
1635
1728
  .option('--force', 'Force deploy');
1636
1729
  cli.help();
1637
- cli.parse(['node', 'bin', 'deploy', '--help'], { run: false });
1730
+ await cli.parse(['node', 'bin', 'deploy', '--help'], { run: false });
1638
1731
  const text = stripAnsi(cli.helpText());
1639
1732
  expect(text).toContain('deploy');
1640
1733
  expect(text).toContain('--force');
1641
1734
  expect(text).toContain('Force deploy');
1642
1735
  });
1643
- test('works without calling parse', () => {
1736
+ test('works without calling parse', async () => {
1644
1737
  const cli = goke('mycli');
1645
1738
  cli.command('test', 'Run tests');
1646
1739
  cli.option('--coverage', 'Enable coverage');
@@ -1654,7 +1747,7 @@ describe('helpText()', () => {
1654
1747
  });
1655
1748
  });
1656
1749
  describe('middleware', () => {
1657
- test('middleware runs before command action', () => {
1750
+ test('middleware runs before command action', async () => {
1658
1751
  const cli = goke('mycli');
1659
1752
  const order = [];
1660
1753
  cli
@@ -1667,10 +1760,10 @@ describe('middleware', () => {
1667
1760
  .action(() => {
1668
1761
  order.push('action');
1669
1762
  });
1670
- cli.parse(['node', 'bin', 'build'], { run: true });
1763
+ await cli.parse(['node', 'bin', 'build'], { run: true });
1671
1764
  expect(order).toEqual(['middleware', 'action']);
1672
1765
  });
1673
- test('multiple middleware run in registration order', () => {
1766
+ test('multiple middleware run in registration order', async () => {
1674
1767
  const cli = goke('mycli');
1675
1768
  const order = [];
1676
1769
  cli
@@ -1680,10 +1773,10 @@ describe('middleware', () => {
1680
1773
  cli
1681
1774
  .command('deploy', 'Deploy')
1682
1775
  .action(() => { order.push('action'); });
1683
- cli.parse(['node', 'bin', 'deploy'], { run: true });
1776
+ await cli.parse(['node', 'bin', 'deploy'], { run: true });
1684
1777
  expect(order).toEqual(['mw1', 'mw2', 'mw3', 'action']);
1685
1778
  });
1686
- test('middleware receives parsed global options', () => {
1779
+ test('middleware receives parsed global options', async () => {
1687
1780
  const cli = goke('mycli');
1688
1781
  let received = null;
1689
1782
  cli
@@ -1694,10 +1787,10 @@ describe('middleware', () => {
1694
1787
  cli
1695
1788
  .command('build', 'Build')
1696
1789
  .action(() => { });
1697
- cli.parse(['node', 'bin', 'build', '--verbose'], { run: true });
1790
+ await cli.parse(['node', 'bin', 'build', '--verbose'], { run: true });
1698
1791
  expect(received.verbose).toBe(true);
1699
1792
  });
1700
- test('middleware receives schema-coerced global options', () => {
1793
+ test('middleware receives schema-coerced global options', async () => {
1701
1794
  const cli = goke('mycli');
1702
1795
  let received = null;
1703
1796
  cli
@@ -1708,7 +1801,7 @@ describe('middleware', () => {
1708
1801
  cli
1709
1802
  .command('serve', 'Serve')
1710
1803
  .action(() => { });
1711
- cli.parse(['node', 'bin', 'serve', '--port', '3000'], { run: true });
1804
+ await cli.parse(['node', 'bin', 'serve', '--port', '3000'], { run: true });
1712
1805
  expect(received.port).toBe(3000);
1713
1806
  expect(typeof received.port).toBe('number');
1714
1807
  });
@@ -1722,7 +1815,7 @@ describe('middleware', () => {
1722
1815
  cli
1723
1816
  .command('run', 'Run')
1724
1817
  .action(() => { order.push('action'); });
1725
- cli.parse(['node', 'bin', 'run'], { run: true });
1818
+ await cli.parse(['node', 'bin', 'run'], { run: true });
1726
1819
  // Wait for async chain to complete
1727
1820
  await new Promise((r) => setTimeout(r, 50));
1728
1821
  expect(order).toEqual(['async-mw', 'action']);
@@ -1737,22 +1830,22 @@ describe('middleware', () => {
1737
1830
  cli
1738
1831
  .command('deploy', 'Deploy')
1739
1832
  .action(() => { });
1740
- cli.parse(['node', 'bin', 'deploy'], { run: true });
1833
+ await cli.parse(['node', 'bin', 'deploy'], { run: true });
1741
1834
  await new Promise((r) => setTimeout(r, 10));
1742
1835
  expect(exitCode).toBe(1);
1743
1836
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware failed"`);
1744
1837
  });
1745
- test('middleware does not run with { run: false }', () => {
1838
+ test('middleware does not run with { run: false }', async () => {
1746
1839
  const cli = goke('mycli');
1747
1840
  let middlewareCalled = false;
1748
1841
  cli.use(() => { middlewareCalled = true; });
1749
1842
  cli
1750
1843
  .command('build', 'Build')
1751
1844
  .action(() => { });
1752
- cli.parse(['node', 'bin', 'build'], { run: false });
1845
+ await cli.parse(['node', 'bin', 'build'], { run: false });
1753
1846
  expect(middlewareCalled).toBe(false);
1754
1847
  });
1755
- test('middleware does not run for help', () => {
1848
+ test('middleware does not run for help', async () => {
1756
1849
  const stdout = createTestOutputStream();
1757
1850
  const cli = goke('mycli', { stdout });
1758
1851
  let middlewareCalled = false;
@@ -1761,10 +1854,10 @@ describe('middleware', () => {
1761
1854
  cli
1762
1855
  .command('build', 'Build')
1763
1856
  .action(() => { });
1764
- cli.parse(['node', 'bin', '--help'], { run: true });
1857
+ await cli.parse(['node', 'bin', '--help'], { run: true });
1765
1858
  expect(middlewareCalled).toBe(false);
1766
1859
  });
1767
- test('middleware does not run when no command matched', () => {
1860
+ test('middleware does not run when no command matched', async () => {
1768
1861
  const stdout = createTestOutputStream();
1769
1862
  const cli = goke('mycli', { stdout });
1770
1863
  let middlewareCalled = false;
@@ -1773,20 +1866,20 @@ describe('middleware', () => {
1773
1866
  cli
1774
1867
  .command('build', 'Build')
1775
1868
  .action(() => { });
1776
- cli.parse(['node', 'bin', 'nonexistent'], { run: true });
1869
+ await cli.parse(['node', 'bin', 'nonexistent'], { run: true });
1777
1870
  expect(middlewareCalled).toBe(false);
1778
1871
  });
1779
- test('middleware runs for default command', () => {
1872
+ test('middleware runs for default command', async () => {
1780
1873
  const cli = goke('mycli');
1781
1874
  const order = [];
1782
1875
  cli.use(() => { order.push('mw'); });
1783
1876
  cli
1784
1877
  .command('', 'Default')
1785
1878
  .action(() => { order.push('action'); });
1786
- cli.parse(['node', 'bin'], { run: true });
1879
+ await cli.parse(['node', 'bin'], { run: true });
1787
1880
  expect(order).toEqual(['mw', 'action']);
1788
1881
  });
1789
- test('sync middleware error is caught and formatted', () => {
1882
+ test('sync middleware error is caught and formatted', async () => {
1790
1883
  const stderr = createTestOutputStream();
1791
1884
  let exitCode;
1792
1885
  const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code; } });
@@ -1796,11 +1889,11 @@ describe('middleware', () => {
1796
1889
  cli
1797
1890
  .command('deploy', 'Deploy')
1798
1891
  .action(() => { });
1799
- cli.parse(['node', 'bin', 'deploy'], { run: true });
1892
+ await cli.parse(['node', 'bin', 'deploy'], { run: true });
1800
1893
  expect(exitCode).toBe(1);
1801
1894
  expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware exploded"`);
1802
1895
  });
1803
- test('sync middleware error short-circuits command action', () => {
1896
+ test('sync middleware error short-circuits command action', async () => {
1804
1897
  const stderr = createTestOutputStream();
1805
1898
  const cli = goke('mycli', { stderr, exit: () => { } });
1806
1899
  let actionCalled = false;
@@ -1810,7 +1903,7 @@ describe('middleware', () => {
1810
1903
  cli
1811
1904
  .command('build', 'Build')
1812
1905
  .action(() => { actionCalled = true; });
1813
- cli.parse(['node', 'bin', 'build'], { run: true });
1906
+ await cli.parse(['node', 'bin', 'build'], { run: true });
1814
1907
  expect(actionCalled).toBe(false);
1815
1908
  });
1816
1909
  test('mixed sync and async middleware chain correctly', async () => {
@@ -1826,13 +1919,13 @@ describe('middleware', () => {
1826
1919
  cli
1827
1920
  .command('run', 'Run')
1828
1921
  .action(() => { order.push('action'); });
1829
- cli.parse(['node', 'bin', 'run'], { run: true });
1922
+ await cli.parse(['node', 'bin', 'run'], { run: true });
1830
1923
  await new Promise((r) => setTimeout(r, 50));
1831
1924
  expect(order).toEqual(['sync1', 'async', 'sync2', 'action']);
1832
1925
  });
1833
1926
  });
1834
1927
  describe('use() with sub-CLI composition', () => {
1835
- test('basic composition: sub-CLI command runs via parent', () => {
1928
+ test('basic composition: sub-CLI command runs via parent', async () => {
1836
1929
  const parent = goke('mycli');
1837
1930
  const sub = goke();
1838
1931
  let matched = '';
@@ -1840,10 +1933,10 @@ describe('use() with sub-CLI composition', () => {
1840
1933
  .command('deploy', 'Deploy the app')
1841
1934
  .action(() => { matched = 'deploy'; });
1842
1935
  parent.use(sub);
1843
- parent.parse(['node', 'bin', 'deploy'], { run: true });
1936
+ await parent.parse(['node', 'bin', 'deploy'], { run: true });
1844
1937
  expect(matched).toBe('deploy');
1845
1938
  });
1846
- test('multiple sub-CLIs composed together', () => {
1939
+ test('multiple sub-CLIs composed together', async () => {
1847
1940
  const parent = goke('mycli');
1848
1941
  const subA = goke();
1849
1942
  const subB = goke();
@@ -1851,13 +1944,13 @@ describe('use() with sub-CLI composition', () => {
1851
1944
  subA.command('login', 'Login').action(() => { matched = 'login'; });
1852
1945
  subB.command('deploy', 'Deploy').action(() => { matched = 'deploy'; });
1853
1946
  parent.use(subA).use(subB);
1854
- parent.parse(['node', 'bin', 'login'], { run: true });
1947
+ await parent.parse(['node', 'bin', 'login'], { run: true });
1855
1948
  expect(matched).toBe('login');
1856
1949
  matched = '';
1857
- parent.parse(['node', 'bin', 'deploy'], { run: true });
1950
+ await parent.parse(['node', 'bin', 'deploy'], { run: true });
1858
1951
  expect(matched).toBe('deploy');
1859
1952
  });
1860
- test('sub-CLI command with options and schema coercion', () => {
1953
+ test('sub-CLI command with options and schema coercion', async () => {
1861
1954
  const parent = goke('mycli');
1862
1955
  const sub = goke();
1863
1956
  let result = {};
@@ -1867,12 +1960,12 @@ describe('use() with sub-CLI composition', () => {
1867
1960
  .option('--host <host>', z.string().describe('Host'))
1868
1961
  .action((options) => { result = options; });
1869
1962
  parent.use(sub);
1870
- parent.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true });
1963
+ await parent.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true });
1871
1964
  expect(result.port).toBe(3000);
1872
1965
  expect(typeof result.port).toBe('number');
1873
1966
  expect(result.host).toBe('localhost');
1874
1967
  });
1875
- test('sub-CLI command with positional args', () => {
1968
+ test('sub-CLI command with positional args', async () => {
1876
1969
  const parent = goke('mycli');
1877
1970
  const sub = goke();
1878
1971
  let receivedId = '';
@@ -1880,23 +1973,23 @@ describe('use() with sub-CLI composition', () => {
1880
1973
  .command('get <id>', 'Get a resource')
1881
1974
  .action((id) => { receivedId = id; });
1882
1975
  parent.use(sub);
1883
- parent.parse(['node', 'bin', 'get', 'abc123'], { run: true });
1976
+ await parent.parse(['node', 'bin', 'get', 'abc123'], { run: true });
1884
1977
  expect(receivedId).toBe('abc123');
1885
1978
  });
1886
- test('sub-CLI with multi-word commands', () => {
1979
+ test('sub-CLI with multi-word commands', async () => {
1887
1980
  const parent = goke('mycli');
1888
1981
  const sub = goke();
1889
1982
  let matched = '';
1890
1983
  sub.command('mcp login', 'Login to MCP').action(() => { matched = 'mcp login'; });
1891
1984
  sub.command('mcp logout', 'Logout from MCP').action(() => { matched = 'mcp logout'; });
1892
1985
  parent.use(sub);
1893
- parent.parse(['node', 'bin', 'mcp', 'login'], { run: true });
1986
+ await parent.parse(['node', 'bin', 'mcp', 'login'], { run: true });
1894
1987
  expect(matched).toBe('mcp login');
1895
1988
  matched = '';
1896
- parent.parse(['node', 'bin', 'mcp', 'logout'], { run: true });
1989
+ await parent.parse(['node', 'bin', 'mcp', 'logout'], { run: true });
1897
1990
  expect(matched).toBe('mcp logout');
1898
1991
  });
1899
- test('help output includes composed commands', () => {
1992
+ test('help output includes composed commands', async () => {
1900
1993
  const stdout = createTestOutputStream();
1901
1994
  const parent = goke('mycli', { stdout });
1902
1995
  const sub = goke();
@@ -1905,12 +1998,12 @@ describe('use() with sub-CLI composition', () => {
1905
1998
  parent.command('init', 'Initialize project');
1906
1999
  parent.use(sub);
1907
2000
  parent.help();
1908
- parent.parse(['node', 'bin', '--help'], { run: false });
2001
+ await parent.parse(['node', 'bin', '--help'], { run: false });
1909
2002
  expect(stdout.text).toContain('init');
1910
2003
  expect(stdout.text).toContain('selfhost');
1911
2004
  expect(stdout.text).toContain('Set up on your own workspace');
1912
2005
  });
1913
- test('sub-CLI middlewares are NOT copied to parent', () => {
2006
+ test('sub-CLI middlewares are NOT copied to parent', async () => {
1914
2007
  const parent = goke('mycli');
1915
2008
  const sub = goke();
1916
2009
  let subMiddlewareCalled = false;
@@ -1919,11 +2012,11 @@ describe('use() with sub-CLI composition', () => {
1919
2012
  sub.command('deploy', 'Deploy').action(() => { order.push('deploy'); });
1920
2013
  parent.use(() => { order.push('parent-mw'); });
1921
2014
  parent.use(sub);
1922
- parent.parse(['node', 'bin', 'deploy'], { run: true });
2015
+ await parent.parse(['node', 'bin', 'deploy'], { run: true });
1923
2016
  expect(subMiddlewareCalled).toBe(false);
1924
2017
  expect(order).toEqual(['parent-mw', 'deploy']);
1925
2018
  });
1926
- test('parent global options are available to composed commands', () => {
2019
+ test('parent global options are available to composed commands', async () => {
1927
2020
  const parent = goke('mycli');
1928
2021
  const sub = goke();
1929
2022
  let result = {};
@@ -1933,11 +2026,11 @@ describe('use() with sub-CLI composition', () => {
1933
2026
  .option('--target <target>', 'Build target')
1934
2027
  .action((options) => { result = options; });
1935
2028
  parent.use(sub);
1936
- parent.parse('node bin build --verbose --target production'.split(' '), { run: true });
2029
+ await parent.parse('node bin build --verbose --target production'.split(' '), { run: true });
1937
2030
  expect(result.verbose).toBe(true);
1938
2031
  expect(result.target).toBe('production');
1939
2032
  });
1940
- test('composed commands coexist with inline commands', () => {
2033
+ test('composed commands coexist with inline commands', async () => {
1941
2034
  const parent = goke('mycli');
1942
2035
  const sub = goke();
1943
2036
  let matched = '';
@@ -1945,13 +2038,48 @@ describe('use() with sub-CLI composition', () => {
1945
2038
  sub.command('deploy', 'Deploy').action(() => { matched = 'deploy'; });
1946
2039
  sub.command('rollback', 'Rollback').action(() => { matched = 'rollback'; });
1947
2040
  parent.use(sub);
1948
- parent.parse(['node', 'bin', 'init'], { run: true });
2041
+ await parent.parse(['node', 'bin', 'init'], { run: true });
1949
2042
  expect(matched).toBe('init');
1950
2043
  matched = '';
1951
- parent.parse(['node', 'bin', 'deploy'], { run: true });
2044
+ await parent.parse(['node', 'bin', 'deploy'], { run: true });
1952
2045
  expect(matched).toBe('deploy');
1953
2046
  matched = '';
1954
- parent.parse(['node', 'bin', 'rollback'], { run: true });
2047
+ await parent.parse(['node', 'bin', 'rollback'], { run: true });
1955
2048
  expect(matched).toBe('rollback');
1956
2049
  });
1957
2050
  });
2051
+ describe('getAction()', () => {
2052
+ test('returns the action callable with correct behavior', async () => {
2053
+ const stdout = createTestOutputStream();
2054
+ const cli = goke('mycli', { stdout, exit: () => { } });
2055
+ const cmd = cli
2056
+ .command('deploy', 'Deploy the app')
2057
+ .option('--env <env>', z.enum(['staging', 'production']).describe('Target environment'))
2058
+ .action((options, { console }) => {
2059
+ console.log(`Deploying to ${options.env}`);
2060
+ });
2061
+ const action = cmd.getAction();
2062
+ const ctx = cli.createExecutionContext();
2063
+ action({ env: 'staging', '--': [] }, ctx);
2064
+ expect(stdout.text).toBe('Deploying to staging\n');
2065
+ });
2066
+ test('works with positional args', async () => {
2067
+ const stdout = createTestOutputStream();
2068
+ const cli = goke('mycli', { stdout, exit: () => { } });
2069
+ const cmd = cli
2070
+ .command('get <id>', 'Fetch by id')
2071
+ .option('--format <format>', z.string().describe('Output format'))
2072
+ .action((id, options, { console }) => {
2073
+ console.log(`${id}:${options.format}`);
2074
+ });
2075
+ const action = cmd.getAction();
2076
+ const ctx = cli.createExecutionContext();
2077
+ action('abc123', { format: 'json', '--': [] }, ctx);
2078
+ expect(stdout.text).toBe('abc123:json\n');
2079
+ });
2080
+ test('throws when no action is registered', async () => {
2081
+ const cli = goke('mycli');
2082
+ const cmd = cli.command('noop', 'No action');
2083
+ expect(() => cmd.getAction()).toThrow(/No action registered/);
2084
+ });
2085
+ });