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