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