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