goke 6.10.0 → 6.12.0

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