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