incur 0.4.0 → 0.4.1
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/README.md +83 -22
- package/SKILL.md +6 -6
- package/dist/Cli.d.ts +46 -26
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +727 -440
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +4 -3
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js +17 -10
- package/dist/Completions.js.map +1 -1
- package/dist/Fetch.d.ts.map +1 -1
- package/dist/Fetch.js +10 -9
- package/dist/Fetch.js.map +1 -1
- package/dist/Filter.js +0 -18
- package/dist/Filter.js.map +1 -1
- package/dist/Formatter.d.ts.map +1 -1
- package/dist/Formatter.js +6 -1
- package/dist/Formatter.js.map +1 -1
- package/dist/Help.d.ts +7 -1
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +44 -27
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +37 -5
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +71 -72
- package/dist/Mcp.js.map +1 -1
- package/dist/Openapi.d.ts.map +1 -1
- package/dist/Openapi.js +22 -14
- package/dist/Openapi.js.map +1 -1
- package/dist/Parser.d.ts +4 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +70 -38
- package/dist/Parser.js.map +1 -1
- package/dist/Schema.d.ts +5 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +13 -2
- package/dist/Schema.js.map +1 -1
- package/dist/Skill.d.ts +2 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +33 -19
- package/dist/Skill.js.map +1 -1
- package/dist/Skillgen.js +1 -1
- package/dist/Skillgen.js.map +1 -1
- package/dist/SyncSkills.d.ts +44 -0
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +82 -7
- package/dist/SyncSkills.js.map +1 -1
- package/dist/Typegen.js +4 -2
- package/dist/Typegen.js.map +1 -1
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +17 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/command.d.ts +170 -0
- package/dist/internal/command.d.ts.map +1 -0
- package/dist/internal/command.js +292 -0
- package/dist/internal/command.js.map +1 -0
- package/dist/internal/configSchema.d.ts +8 -0
- package/dist/internal/configSchema.d.ts.map +1 -0
- package/dist/internal/configSchema.js +57 -0
- package/dist/internal/configSchema.js.map +1 -0
- package/dist/internal/dereference.d.ts +12 -0
- package/dist/internal/dereference.d.ts.map +1 -0
- package/dist/internal/dereference.js +71 -0
- package/dist/internal/dereference.js.map +1 -0
- package/dist/internal/helpers.d.ts +9 -0
- package/dist/internal/helpers.d.ts.map +1 -0
- package/dist/internal/helpers.js +54 -0
- package/dist/internal/helpers.js.map +1 -0
- package/dist/middleware.d.ts +6 -8
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/examples/npm/.npmrc.json +21 -0
- package/examples/npm/config.schema.json +134 -0
- package/package.json +6 -29
- package/src/Cli.test-d.ts +44 -33
- package/src/Cli.test.ts +1213 -100
- package/src/Cli.ts +876 -568
- package/src/Completions.test.ts +136 -12
- package/src/Completions.ts +18 -13
- package/src/Fetch.test.ts +21 -0
- package/src/Fetch.ts +8 -10
- package/src/Filter.ts +0 -17
- package/src/Formatter.test.ts +15 -2
- package/src/Formatter.ts +5 -1
- package/src/Help.test.ts +184 -20
- package/src/Help.ts +52 -28
- package/src/Mcp.test.ts +159 -0
- package/src/Mcp.ts +108 -86
- package/src/Openapi.test.ts +17 -5
- package/src/Openapi.ts +21 -15
- package/src/Parser.test-d.ts +22 -0
- package/src/Parser.test.ts +89 -0
- package/src/Parser.ts +87 -36
- package/src/Schema.test.ts +29 -0
- package/src/Schema.ts +12 -2
- package/src/Skill.test.ts +87 -6
- package/src/Skill.ts +38 -21
- package/src/Skillgen.ts +1 -1
- package/src/SyncMcp.test.ts +6 -8
- package/src/SyncSkills.test.ts +120 -3
- package/src/SyncSkills.ts +142 -6
- package/src/Typegen.test.ts +15 -0
- package/src/Typegen.ts +4 -2
- package/src/bin.ts +21 -2
- package/src/e2e.test.ts +172 -97
- package/src/internal/command.ts +449 -0
- package/src/internal/configSchema.test.ts +193 -0
- package/src/internal/configSchema.ts +66 -0
- package/src/internal/dereference.test.ts +695 -0
- package/src/internal/dereference.ts +75 -0
- package/src/internal/helpers.test.ts +75 -0
- package/src/internal/helpers.ts +59 -0
- package/src/middleware.ts +5 -12
package/src/Cli.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Cli, Errors, z } from 'incur'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir, tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
|
|
3
6
|
const originalIsTTY = process.stdout.isTTY
|
|
4
7
|
beforeAll(() => {
|
|
@@ -37,6 +40,42 @@ async function serve(
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function createConfigCli(flag?: string) {
|
|
44
|
+
const project = Cli.create('project').command('list', {
|
|
45
|
+
options: z.object({
|
|
46
|
+
label: z.array(z.string()).default([]),
|
|
47
|
+
limit: z.number().default(10),
|
|
48
|
+
}),
|
|
49
|
+
run(c) {
|
|
50
|
+
return c.options
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const cli = Cli.create('test', {
|
|
55
|
+
config: flag !== undefined ? { flag } : {},
|
|
56
|
+
options: z.object({
|
|
57
|
+
rootValue: z.string().default('root-default'),
|
|
58
|
+
}),
|
|
59
|
+
run(c) {
|
|
60
|
+
return c.options
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
cli.command('echo', {
|
|
65
|
+
options: z.object({
|
|
66
|
+
prefix: z.string().default(''),
|
|
67
|
+
upper: z.boolean().default(false),
|
|
68
|
+
}),
|
|
69
|
+
run(c) {
|
|
70
|
+
return c.options
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
cli.command(project)
|
|
75
|
+
|
|
76
|
+
return cli
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
describe('create', () => {
|
|
41
80
|
test('returns cli instance with name', () => {
|
|
42
81
|
const cli = Cli.create('test')
|
|
@@ -62,6 +101,518 @@ describe('command', () => {
|
|
|
62
101
|
})
|
|
63
102
|
})
|
|
64
103
|
|
|
104
|
+
describe('config defaults', () => {
|
|
105
|
+
let cwd: string
|
|
106
|
+
let dir: string
|
|
107
|
+
|
|
108
|
+
beforeEach(async () => {
|
|
109
|
+
cwd = process.cwd()
|
|
110
|
+
dir = await mkdtemp(join(tmpdir(), 'incur-config-'))
|
|
111
|
+
process.chdir(dir)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
afterEach(async () => {
|
|
115
|
+
process.chdir(cwd)
|
|
116
|
+
await rm(dir, { force: true, recursive: true })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('auto-loads <cli>.json for leaf commands', async () => {
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(dir, 'test.json'),
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
commands: {
|
|
124
|
+
echo: {
|
|
125
|
+
options: {
|
|
126
|
+
prefix: 'cfg',
|
|
127
|
+
upper: true,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const { output } = await serve(createConfigCli(), ['echo', '--json'])
|
|
135
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'cfg', upper: true })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('ignores a missing auto config file', async () => {
|
|
139
|
+
const { output } = await serve(createConfigCli(), ['echo', '--json'])
|
|
140
|
+
expect(JSON.parse(output)).toEqual({ prefix: '', upper: false })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('root options coexist with subcommand keys', async () => {
|
|
144
|
+
await writeFile(
|
|
145
|
+
join(dir, 'test.json'),
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
options: { rootValue: 'cfg-root' },
|
|
148
|
+
commands: {
|
|
149
|
+
echo: { options: { prefix: 'cfg' } },
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const rootResult = await serve(createConfigCli(), ['--json'])
|
|
155
|
+
expect(JSON.parse(rootResult.output)).toEqual({ rootValue: 'cfg-root' })
|
|
156
|
+
|
|
157
|
+
const echoResult = await serve(createConfigCli(), ['echo', '--json'])
|
|
158
|
+
expect(JSON.parse(echoResult.output)).toEqual({ prefix: 'cfg', upper: false })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('walks nested command sections in config tree', async () => {
|
|
162
|
+
await writeFile(
|
|
163
|
+
join(dir, 'test.json'),
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
commands: {
|
|
166
|
+
project: {
|
|
167
|
+
commands: {
|
|
168
|
+
list: {
|
|
169
|
+
options: {
|
|
170
|
+
label: ['cfg'],
|
|
171
|
+
limit: 25,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const { output } = await serve(createConfigCli(), ['project', 'list', '--json'])
|
|
181
|
+
expect(JSON.parse(output)).toEqual({ label: ['cfg'], limit: 25 })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('uses an explicit --config path instead of the auto file', async () => {
|
|
185
|
+
await writeFile(
|
|
186
|
+
join(dir, 'test.json'),
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
commands: { echo: { options: { prefix: 'auto' } } },
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
await writeFile(
|
|
192
|
+
join(dir, 'custom.json'),
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
commands: { echo: { options: { prefix: 'custom', upper: true } } },
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const { output } = await serve(createConfigCli('config'), [
|
|
199
|
+
'echo',
|
|
200
|
+
'--config',
|
|
201
|
+
'custom.json',
|
|
202
|
+
'--json',
|
|
203
|
+
])
|
|
204
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'custom', upper: true })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('--no-config disables earlier config flags, and a later --config wins again', async () => {
|
|
208
|
+
await writeFile(
|
|
209
|
+
join(dir, 'one.json'),
|
|
210
|
+
JSON.stringify({
|
|
211
|
+
commands: { echo: { options: { prefix: 'one' } } },
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
await writeFile(
|
|
215
|
+
join(dir, 'two.json'),
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
commands: { echo: { options: { prefix: 'two' } } },
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const first = await serve(createConfigCli('config'), [
|
|
222
|
+
'echo',
|
|
223
|
+
'--config',
|
|
224
|
+
'one.json',
|
|
225
|
+
'--no-config',
|
|
226
|
+
'--json',
|
|
227
|
+
])
|
|
228
|
+
expect(JSON.parse(first.output)).toEqual({ prefix: '', upper: false })
|
|
229
|
+
|
|
230
|
+
const second = await serve(createConfigCli('config'), [
|
|
231
|
+
'echo',
|
|
232
|
+
'--config',
|
|
233
|
+
'one.json',
|
|
234
|
+
'--no-config',
|
|
235
|
+
'--config=two.json',
|
|
236
|
+
'--json',
|
|
237
|
+
])
|
|
238
|
+
expect(JSON.parse(second.output)).toEqual({ prefix: 'two', upper: false })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('fails when an explicit config file is missing', async () => {
|
|
242
|
+
const { exitCode, output } = await serve(createConfigCli('config'), [
|
|
243
|
+
'echo',
|
|
244
|
+
'--config',
|
|
245
|
+
'missing.json',
|
|
246
|
+
])
|
|
247
|
+
expect(exitCode).toBe(1)
|
|
248
|
+
expect(output).toContain('Config file not found')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('fails on invalid JSON config files', async () => {
|
|
252
|
+
await writeFile(join(dir, 'test.json'), '{ invalid')
|
|
253
|
+
|
|
254
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
255
|
+
expect(exitCode).toBe(1)
|
|
256
|
+
expect(output).toContain('Invalid JSON config file')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('fails when the config file top level is not an object', async () => {
|
|
260
|
+
await writeFile(join(dir, 'test.json'), JSON.stringify(['bad']))
|
|
261
|
+
|
|
262
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
263
|
+
expect(exitCode).toBe(1)
|
|
264
|
+
expect(output).toContain('expected a top-level object')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('fails when the selected config section is not an object', async () => {
|
|
268
|
+
await writeFile(join(dir, 'test.json'), JSON.stringify({ commands: { echo: true } }))
|
|
269
|
+
|
|
270
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
271
|
+
expect(exitCode).toBe(1)
|
|
272
|
+
expect(output).toContain("Invalid config section for 'echo'")
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('fails validation when config option values are invalid', async () => {
|
|
276
|
+
await writeFile(
|
|
277
|
+
join(dir, 'test.json'),
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
commands: { echo: { options: { upper: 'nope' } } },
|
|
280
|
+
}),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
284
|
+
expect(exitCode).toBe(1)
|
|
285
|
+
expect(output).toContain('VALIDATION_ERROR')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('argv overrides invalid config values at the CLI layer', async () => {
|
|
289
|
+
await writeFile(
|
|
290
|
+
join(dir, 'test.json'),
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
commands: { echo: { options: { prefix: 123 } } },
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const { output } = await serve(createConfigCli(), ['echo', '--prefix', 'cli', '--json'])
|
|
297
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'cli', upper: false })
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('built-in commands ignore config loading', async () => {
|
|
301
|
+
await writeFile(join(dir, 'test.json'), '{ invalid')
|
|
302
|
+
|
|
303
|
+
const { output, exitCode } = await serve(createConfigCli(), ['--help'])
|
|
304
|
+
expect(exitCode).toBeUndefined()
|
|
305
|
+
expect(output).toContain('Global Options:')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('config without flag does not reserve --config', async () => {
|
|
309
|
+
const cli = Cli.create('test', { config: {} })
|
|
310
|
+
cli.command('echo', {
|
|
311
|
+
options: z.object({ config: z.string().default('') }),
|
|
312
|
+
run(c) {
|
|
313
|
+
return c.options
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const { output } = await serve(cli, ['echo', '--config', 'my-value', '--json'])
|
|
318
|
+
expect(JSON.parse(output)).toEqual({ config: 'my-value' })
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('--help shows config flags only when flag name is set', async () => {
|
|
322
|
+
const { output } = await serve(createConfigCli('config'), ['--help'])
|
|
323
|
+
expect(output).toContain('--config <path>')
|
|
324
|
+
expect(output).toContain('--no-config')
|
|
325
|
+
|
|
326
|
+
const { output: noFlagOutput } = await serve(createConfigCli(), ['--help'])
|
|
327
|
+
expect(noFlagOutput).not.toContain('--config')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('custom flag name is used for config path override', async () => {
|
|
331
|
+
await writeFile(
|
|
332
|
+
join(dir, 'custom.json'),
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
commands: { echo: { options: { prefix: 'custom' } } },
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const { output } = await serve(createConfigCli('settings'), [
|
|
339
|
+
'echo',
|
|
340
|
+
'--settings',
|
|
341
|
+
'custom.json',
|
|
342
|
+
'--json',
|
|
343
|
+
])
|
|
344
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'custom', upper: false })
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('searches files list in order, first match wins', async () => {
|
|
348
|
+
await writeFile(
|
|
349
|
+
join(dir, '.testrc.json'),
|
|
350
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'rc' } } } }),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
const cli = Cli.create('test', {
|
|
354
|
+
config: { files: ['test.json', '.testrc.json'] },
|
|
355
|
+
})
|
|
356
|
+
cli.command('echo', {
|
|
357
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
358
|
+
run: (c) => c.options,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
362
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'rc' })
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('files: [] disables auto-discovery', async () => {
|
|
366
|
+
await writeFile(
|
|
367
|
+
join(dir, 'test.json'),
|
|
368
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'should-not-load' } } } }),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
const cli = Cli.create('test', {
|
|
372
|
+
config: { files: [] },
|
|
373
|
+
})
|
|
374
|
+
cli.command('echo', {
|
|
375
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
376
|
+
run: (c) => c.options,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
380
|
+
expect(JSON.parse(output)).toEqual({ prefix: '' })
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('files supports ~ for home directory', async () => {
|
|
384
|
+
const configDir = join(homedir(), '.config', 'test-incur-files-tilde')
|
|
385
|
+
await mkdir(configDir, { recursive: true })
|
|
386
|
+
try {
|
|
387
|
+
await writeFile(
|
|
388
|
+
join(configDir, 'config.json'),
|
|
389
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'home' } } } }),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
const cli = Cli.create('test', {
|
|
393
|
+
config: { files: ['test.json', '~/.config/test-incur-files-tilde/config.json'] },
|
|
394
|
+
})
|
|
395
|
+
cli.command('echo', {
|
|
396
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
397
|
+
run: (c) => c.options,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
401
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'home' })
|
|
402
|
+
} finally {
|
|
403
|
+
await rm(configDir, { recursive: true, force: true })
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('explicit --flag overrides files list', async () => {
|
|
408
|
+
await writeFile(
|
|
409
|
+
join(dir, '.testrc.json'),
|
|
410
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'rc' } } } }),
|
|
411
|
+
)
|
|
412
|
+
await writeFile(
|
|
413
|
+
join(dir, 'override.json'),
|
|
414
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'override' } } } }),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
const cli = Cli.create('test', {
|
|
418
|
+
config: { flag: 'config', files: ['.testrc.json'] },
|
|
419
|
+
})
|
|
420
|
+
cli.command('echo', {
|
|
421
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
422
|
+
run: (c) => c.options,
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const { output } = await serve(cli, ['echo', '--config', 'override.json', '--json'])
|
|
426
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'override' })
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test('custom loader replaces JSON parsing', async () => {
|
|
430
|
+
await writeFile(join(dir, 'test.ini'), 'prefix=ini-value')
|
|
431
|
+
|
|
432
|
+
const cli = Cli.create('test', {
|
|
433
|
+
config: {
|
|
434
|
+
files: ['test.ini'],
|
|
435
|
+
async loader(path) {
|
|
436
|
+
if (!path) return undefined
|
|
437
|
+
const raw = await readFile(path, 'utf8')
|
|
438
|
+
const obj: Record<string, unknown> = {}
|
|
439
|
+
for (const line of raw.split('\n')) {
|
|
440
|
+
const eq = line.indexOf('=')
|
|
441
|
+
if (eq !== -1) obj[line.slice(0, eq).trim()] = line.slice(eq + 1).trim()
|
|
442
|
+
}
|
|
443
|
+
return { commands: { echo: { options: obj } } }
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
cli.command('echo', {
|
|
448
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
449
|
+
run: (c) => c.options,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
453
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'ini-value' })
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('loader with files: [] receives undefined path', async () => {
|
|
457
|
+
const cli = Cli.create('test', {
|
|
458
|
+
config: {
|
|
459
|
+
files: [],
|
|
460
|
+
loader: async (path) => {
|
|
461
|
+
expect(path).toBeUndefined()
|
|
462
|
+
return { commands: { echo: { options: { prefix: 'from-loader' } } } }
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
cli.command('echo', {
|
|
467
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
468
|
+
run: (c) => c.options,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
472
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'from-loader' })
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('loader returning undefined applies no defaults', async () => {
|
|
476
|
+
const cli = Cli.create('test', {
|
|
477
|
+
config: { files: [], loader: async () => undefined },
|
|
478
|
+
})
|
|
479
|
+
cli.command('echo', {
|
|
480
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
481
|
+
run: (c) => c.options,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
485
|
+
expect(JSON.parse(output)).toEqual({ prefix: '' })
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('--no-flag skips loader entirely', async () => {
|
|
489
|
+
let loaderCalled = false
|
|
490
|
+
const cli = Cli.create('test', {
|
|
491
|
+
config: {
|
|
492
|
+
flag: 'config',
|
|
493
|
+
files: [],
|
|
494
|
+
loader: async () => {
|
|
495
|
+
loaderCalled = true
|
|
496
|
+
return { commands: { echo: { options: { prefix: 'should-not-load' } } } }
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
cli.command('echo', {
|
|
501
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
502
|
+
run: (c) => c.options,
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const { output } = await serve(cli, ['echo', '--no-config', '--json'])
|
|
506
|
+
expect(JSON.parse(output)).toEqual({ prefix: '' })
|
|
507
|
+
expect(loaderCalled).toBe(false)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('loader errors propagate', async () => {
|
|
511
|
+
const cli = Cli.create('test', {
|
|
512
|
+
config: {
|
|
513
|
+
files: [],
|
|
514
|
+
loader: async () => {
|
|
515
|
+
throw new Error('Remote config server unreachable')
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
})
|
|
519
|
+
cli.command('echo', {
|
|
520
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
521
|
+
run: (c) => c.options,
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const { exitCode, output } = await serve(cli, ['echo'])
|
|
525
|
+
expect(exitCode).toBe(1)
|
|
526
|
+
expect(output).toContain('Remote config server unreachable')
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
test('--no-flag disables auto-discovery without prior --flag', async () => {
|
|
530
|
+
await writeFile(
|
|
531
|
+
join(dir, 'test.json'),
|
|
532
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'auto-loaded' } } } }),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
const { output } = await serve(createConfigCli('config'), ['echo', '--no-config', '--json'])
|
|
536
|
+
expect(JSON.parse(output)).toEqual({ prefix: '', upper: false })
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test('--config without a value produces an error', async () => {
|
|
540
|
+
const { exitCode, output } = await serve(createConfigCli('config'), ['echo', '--config'])
|
|
541
|
+
expect(exitCode).toBe(1)
|
|
542
|
+
expect(output).toContain('Missing value for flag')
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test('--config= (empty value) produces an error', async () => {
|
|
546
|
+
const { exitCode, output } = await serve(createConfigCli('config'), ['echo', '--config='])
|
|
547
|
+
expect(exitCode).toBe(1)
|
|
548
|
+
expect(output).toContain('Missing value for flag')
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('--no-settings works with custom flag name', async () => {
|
|
552
|
+
await writeFile(
|
|
553
|
+
join(dir, 'test.json'),
|
|
554
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'auto' } } } }),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const { output } = await serve(createConfigCli('settings'), ['echo', '--no-settings', '--json'])
|
|
558
|
+
expect(JSON.parse(output)).toEqual({ prefix: '', upper: false })
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('camelCase config keys are accepted at cli level', async () => {
|
|
562
|
+
const cli = Cli.create('test', { config: {} })
|
|
563
|
+
cli.command('echo', {
|
|
564
|
+
options: z.object({ saveDev: z.boolean().default(false) }),
|
|
565
|
+
run: (c) => c.options,
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
await writeFile(
|
|
569
|
+
join(dir, 'test.json'),
|
|
570
|
+
JSON.stringify({ commands: { echo: { options: { 'save-dev': true } } } }),
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
574
|
+
expect(JSON.parse(output)).toEqual({ saveDev: true })
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
test('config defaults with only subcommand namespaces yields no option defaults', async () => {
|
|
578
|
+
await writeFile(
|
|
579
|
+
join(dir, 'test.json'),
|
|
580
|
+
JSON.stringify({
|
|
581
|
+
commands: {
|
|
582
|
+
echo: { options: { prefix: 'child' } },
|
|
583
|
+
project: { commands: { list: { options: { limit: 50 } } } },
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
const rootResult = await serve(createConfigCli(), ['--json'])
|
|
589
|
+
expect(JSON.parse(rootResult.output)).toEqual({ rootValue: 'root-default' })
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
test('explicit --flag path is forwarded to custom loader', async () => {
|
|
593
|
+
await writeFile(join(dir, 'custom.dat'), 'prefix=custom-loader')
|
|
594
|
+
|
|
595
|
+
const cli = Cli.create('test', {
|
|
596
|
+
config: {
|
|
597
|
+
flag: 'config',
|
|
598
|
+
async loader(path) {
|
|
599
|
+
if (!path) return undefined
|
|
600
|
+
const raw = await readFile(path, 'utf8')
|
|
601
|
+
const [, value] = raw.split('=')
|
|
602
|
+
return { commands: { echo: { options: { prefix: value!.trim() } } } }
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
})
|
|
606
|
+
cli.command('echo', {
|
|
607
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
608
|
+
run: (c) => c.options,
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
const { output } = await serve(cli, ['echo', '--config', 'custom.dat', '--json'])
|
|
612
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'custom-loader' })
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
65
616
|
describe('serve', () => {
|
|
66
617
|
test('outputs data only by default', async () => {
|
|
67
618
|
const cli = Cli.create('test')
|
|
@@ -79,7 +630,7 @@ describe('serve', () => {
|
|
|
79
630
|
`)
|
|
80
631
|
})
|
|
81
632
|
|
|
82
|
-
test('--
|
|
633
|
+
test('--full-output outputs full envelope', async () => {
|
|
83
634
|
const cli = Cli.create('test')
|
|
84
635
|
cli.command('greet', {
|
|
85
636
|
args: z.object({ name: z.string() }),
|
|
@@ -88,7 +639,7 @@ describe('serve', () => {
|
|
|
88
639
|
},
|
|
89
640
|
})
|
|
90
641
|
|
|
91
|
-
const { output } = await serve(cli, ['greet', 'world', '--
|
|
642
|
+
const { output } = await serve(cli, ['greet', 'world', '--full-output'])
|
|
92
643
|
expect(output).toMatchInlineSnapshot(`
|
|
93
644
|
"ok: true
|
|
94
645
|
data:
|
|
@@ -140,9 +691,9 @@ describe('serve', () => {
|
|
|
140
691
|
"code: COMMAND_NOT_FOUND
|
|
141
692
|
message: 'nonexistent' is not a command for 'test'.
|
|
142
693
|
cta:
|
|
143
|
-
description: "
|
|
144
|
-
commands[1]{command}:
|
|
145
|
-
test --help
|
|
694
|
+
description: "Suggested command:"
|
|
695
|
+
commands[1]{command,description}:
|
|
696
|
+
test --help,see all available commands
|
|
146
697
|
"
|
|
147
698
|
`)
|
|
148
699
|
})
|
|
@@ -157,16 +708,16 @@ describe('serve', () => {
|
|
|
157
708
|
expect(output).toMatchInlineSnapshot(`
|
|
158
709
|
"Error: 'nonexistent' is not a command for 'test'.
|
|
159
710
|
|
|
160
|
-
|
|
161
|
-
test --help
|
|
711
|
+
Suggested command:
|
|
712
|
+
test --help # see all available commands
|
|
162
713
|
"
|
|
163
714
|
`)
|
|
164
715
|
})
|
|
165
716
|
|
|
166
|
-
test('--
|
|
717
|
+
test('--full-output outputs full error envelope for unknown command', async () => {
|
|
167
718
|
const cli = Cli.create('test')
|
|
168
719
|
|
|
169
|
-
const { output, exitCode } = await serve(cli, ['nonexistent', '--
|
|
720
|
+
const { output, exitCode } = await serve(cli, ['nonexistent', '--full-output'])
|
|
170
721
|
expect(exitCode).toBe(1)
|
|
171
722
|
expect(output).toMatchInlineSnapshot(`
|
|
172
723
|
"ok: false
|
|
@@ -176,14 +727,100 @@ describe('serve', () => {
|
|
|
176
727
|
meta:
|
|
177
728
|
command: nonexistent
|
|
178
729
|
cta:
|
|
179
|
-
description: "
|
|
180
|
-
commands[1]{command}:
|
|
181
|
-
test --help
|
|
730
|
+
description: "Suggested command:"
|
|
731
|
+
commands[1]{command,description}:
|
|
732
|
+
test --help,see all available commands
|
|
182
733
|
duration: <stripped>
|
|
183
734
|
"
|
|
184
735
|
`)
|
|
185
736
|
})
|
|
186
737
|
|
|
738
|
+
test('suggests similar command for typos', async () => {
|
|
739
|
+
const cli = Cli.create('test')
|
|
740
|
+
cli.command('deploy', { run: () => ({}) })
|
|
741
|
+
cli.command('status', { run: () => ({}) })
|
|
742
|
+
|
|
743
|
+
const { output, exitCode } = await serve(cli, ['deplyo'])
|
|
744
|
+
expect(exitCode).toBe(1)
|
|
745
|
+
expect(output).toMatchInlineSnapshot(`
|
|
746
|
+
"code: COMMAND_NOT_FOUND
|
|
747
|
+
message: 'deplyo' is not a command for 'test'. Did you mean 'deploy'?
|
|
748
|
+
cta:
|
|
749
|
+
description: "Suggested commands:"
|
|
750
|
+
commands[2]:
|
|
751
|
+
- command: test deploy
|
|
752
|
+
- command: test --help
|
|
753
|
+
description: see all available commands
|
|
754
|
+
"
|
|
755
|
+
`)
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
test('suggests similar command for typos in TTY', async () => {
|
|
759
|
+
;(process.stdout as any).isTTY = true
|
|
760
|
+
const cli = Cli.create('test')
|
|
761
|
+
cli.command('deploy', { run: () => ({}) })
|
|
762
|
+
|
|
763
|
+
const { output, exitCode } = await serve(cli, ['deplyo'])
|
|
764
|
+
;(process.stdout as any).isTTY = false
|
|
765
|
+
expect(exitCode).toBe(1)
|
|
766
|
+
expect(output).toMatchInlineSnapshot(`
|
|
767
|
+
"Error: 'deplyo' is not a command for 'test'. Did you mean 'deploy'?
|
|
768
|
+
|
|
769
|
+
Suggested commands:
|
|
770
|
+
test deploy
|
|
771
|
+
test --help # see all available commands
|
|
772
|
+
"
|
|
773
|
+
`)
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
test('suggests builtin commands for typos', async () => {
|
|
777
|
+
const cli = Cli.create('test')
|
|
778
|
+
cli.command('ping', { run: () => ({}) })
|
|
779
|
+
|
|
780
|
+
const { output, exitCode } = await serve(cli, ['mpc'])
|
|
781
|
+
expect(exitCode).toBe(1)
|
|
782
|
+
expect(output).toContain("Did you mean 'mcp'?")
|
|
783
|
+
expect(output).toContain('test mcp')
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
test('preserves flags in suggestion CTA', async () => {
|
|
787
|
+
const cli = Cli.create('test')
|
|
788
|
+
cli.command('deploy', { run: () => ({}) })
|
|
789
|
+
|
|
790
|
+
const { output } = await serve(cli, ['deplyo', '--full-output'])
|
|
791
|
+
expect(output).toContain('test deploy --full-output')
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
test('no suggestion when input is too far from any command', async () => {
|
|
795
|
+
const cli = Cli.create('test')
|
|
796
|
+
cli.command('deploy', { run: () => ({}) })
|
|
797
|
+
|
|
798
|
+
const { output } = await serve(cli, ['xyz'])
|
|
799
|
+
expect(output).not.toContain('Did you mean')
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
test('suggests similar subcommand for typos', async () => {
|
|
803
|
+
const cli = Cli.create('test')
|
|
804
|
+
const pr = Cli.create('pr')
|
|
805
|
+
.command('list', { run: () => ({}) })
|
|
806
|
+
.command('create', { run: () => ({}) })
|
|
807
|
+
cli.command(pr)
|
|
808
|
+
|
|
809
|
+
const { output, exitCode } = await serve(cli, ['pr', 'craete'])
|
|
810
|
+
expect(exitCode).toBe(1)
|
|
811
|
+
expect(output).toMatchInlineSnapshot(`
|
|
812
|
+
"code: COMMAND_NOT_FOUND
|
|
813
|
+
message: 'craete' is not a command for 'test pr'. Did you mean 'create'?
|
|
814
|
+
cta:
|
|
815
|
+
description: "Suggested commands:"
|
|
816
|
+
commands[2]:
|
|
817
|
+
- command: test pr create
|
|
818
|
+
- command: test pr --help
|
|
819
|
+
description: see all available commands
|
|
820
|
+
"
|
|
821
|
+
`)
|
|
822
|
+
})
|
|
823
|
+
|
|
187
824
|
test('wraps handler errors in error output', async () => {
|
|
188
825
|
const cli = Cli.create('test')
|
|
189
826
|
cli.command('fail', {
|
|
@@ -353,10 +990,10 @@ describe('serve', () => {
|
|
|
353
990
|
expect(JSON.parse(output)).toEqual({ pong: true })
|
|
354
991
|
})
|
|
355
992
|
|
|
356
|
-
test('--
|
|
993
|
+
test('--full-output --format json outputs full envelope as JSON', async () => {
|
|
357
994
|
const cli = Cli.create('test')
|
|
358
995
|
cli.command('ping', { run: () => ({ pong: true }) })
|
|
359
|
-
const { output } = await serve(cli, ['ping', '--
|
|
996
|
+
const { output } = await serve(cli, ['ping', '--full-output', '--format', 'json'])
|
|
360
997
|
const parsed = JSON.parse(output)
|
|
361
998
|
expect(parsed.ok).toBe(true)
|
|
362
999
|
expect(parsed.data).toEqual({ pong: true })
|
|
@@ -641,6 +1278,38 @@ describe('--llms', () => {
|
|
|
641
1278
|
expect(output).toContain('test auth auth logout')
|
|
642
1279
|
expect(output).not.toContain('ping')
|
|
643
1280
|
})
|
|
1281
|
+
|
|
1282
|
+
test('--llms includes root command', async () => {
|
|
1283
|
+
const cli = Cli.create('my-cli', {
|
|
1284
|
+
description: 'Fetch URLs',
|
|
1285
|
+
args: z.object({ url: z.string().describe('URL to fetch') }),
|
|
1286
|
+
options: z.object({ objective: z.string().optional().describe('Narrow content') }),
|
|
1287
|
+
run: ({ args }) => args.url,
|
|
1288
|
+
})
|
|
1289
|
+
cli.command('auth', { description: 'Auth commands', run: () => ({}) })
|
|
1290
|
+
|
|
1291
|
+
const { output } = await serve(cli, ['--llms'])
|
|
1292
|
+
expect(output).toContain('| `my-cli <url>` | Fetch URLs |')
|
|
1293
|
+
expect(output).toContain('| `my-cli auth` | Auth commands |')
|
|
1294
|
+
})
|
|
1295
|
+
|
|
1296
|
+
test('--llms-full includes root command with args/options', async () => {
|
|
1297
|
+
const cli = Cli.create('my-cli', {
|
|
1298
|
+
description: 'Fetch URLs',
|
|
1299
|
+
args: z.object({ url: z.string().describe('URL to fetch') }),
|
|
1300
|
+
options: z.object({ objective: z.string().optional().describe('Narrow content') }),
|
|
1301
|
+
output: z.string().describe('Page content'),
|
|
1302
|
+
run: ({ args }) => args.url,
|
|
1303
|
+
})
|
|
1304
|
+
cli.command('auth', { description: 'Auth commands', run: () => ({}) })
|
|
1305
|
+
|
|
1306
|
+
const { output } = await serve(cli, ['--llms-full'])
|
|
1307
|
+
expect(output).toContain('# my-cli\n\nFetch URLs')
|
|
1308
|
+
expect(output).toContain('| `url` | `string` | yes | URL to fetch |')
|
|
1309
|
+
expect(output).toContain('| `--objective` | `string` | | Narrow content |')
|
|
1310
|
+
expect(output).toContain('# my-cli auth')
|
|
1311
|
+
expect(output).not.toContain('# my-cli \n')
|
|
1312
|
+
})
|
|
644
1313
|
})
|
|
645
1314
|
|
|
646
1315
|
describe('--schema', () => {
|
|
@@ -738,6 +1407,14 @@ describe('--schema', () => {
|
|
|
738
1407
|
expect(exitCode).toBe(1)
|
|
739
1408
|
})
|
|
740
1409
|
|
|
1410
|
+
test('on unknown command suggests similar', async () => {
|
|
1411
|
+
const cli = Cli.create('test')
|
|
1412
|
+
cli.command('greet', { run: () => ({}) })
|
|
1413
|
+
const { output, exitCode } = await serve(cli, ['grete', '--schema'])
|
|
1414
|
+
expect(output).toContain("Did you mean 'greet'?")
|
|
1415
|
+
expect(exitCode).toBe(1)
|
|
1416
|
+
})
|
|
1417
|
+
|
|
741
1418
|
test('on group shows available commands', async () => {
|
|
742
1419
|
const cli = Cli.create('test')
|
|
743
1420
|
const pr = Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
@@ -802,14 +1479,14 @@ describe('subcommands', () => {
|
|
|
802
1479
|
`)
|
|
803
1480
|
})
|
|
804
1481
|
|
|
805
|
-
test('--
|
|
1482
|
+
test('--full-output shows full command path in meta', async () => {
|
|
806
1483
|
const cli = Cli.create('test')
|
|
807
1484
|
const pr = Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
808
1485
|
run: () => ({ count: 0 }),
|
|
809
1486
|
})
|
|
810
1487
|
cli.command(pr)
|
|
811
1488
|
|
|
812
|
-
const { output } = await serve(cli, ['pr', 'list', '--
|
|
1489
|
+
const { output } = await serve(cli, ['pr', 'list', '--full-output'])
|
|
813
1490
|
expect(output).toMatchInlineSnapshot(`
|
|
814
1491
|
"ok: true
|
|
815
1492
|
data:
|
|
@@ -837,7 +1514,7 @@ describe('subcommands', () => {
|
|
|
837
1514
|
`)
|
|
838
1515
|
})
|
|
839
1516
|
|
|
840
|
-
test('nested group shows full path in
|
|
1517
|
+
test('nested group shows full path in full-output meta', async () => {
|
|
841
1518
|
const cli = Cli.create('test')
|
|
842
1519
|
const review = Cli.create('review', { description: 'Reviews' }).command('approve', {
|
|
843
1520
|
run: () => ({ approved: true }),
|
|
@@ -846,7 +1523,7 @@ describe('subcommands', () => {
|
|
|
846
1523
|
pr.command(review)
|
|
847
1524
|
cli.command(pr)
|
|
848
1525
|
|
|
849
|
-
const { output } = await serve(cli, ['pr', 'review', 'approve', '--
|
|
1526
|
+
const { output } = await serve(cli, ['pr', 'review', 'approve', '--full-output'])
|
|
850
1527
|
expect(output).toMatchInlineSnapshot(`
|
|
851
1528
|
"ok: true
|
|
852
1529
|
data:
|
|
@@ -871,9 +1548,9 @@ describe('subcommands', () => {
|
|
|
871
1548
|
"code: COMMAND_NOT_FOUND
|
|
872
1549
|
message: 'unknown' is not a command for 'test pr'.
|
|
873
1550
|
cta:
|
|
874
|
-
description: "
|
|
875
|
-
commands[1]{command}:
|
|
876
|
-
test pr --help
|
|
1551
|
+
description: "Suggested command:"
|
|
1552
|
+
commands[1]{command,description}:
|
|
1553
|
+
test pr --help,see all available commands
|
|
877
1554
|
"
|
|
878
1555
|
`)
|
|
879
1556
|
})
|
|
@@ -892,8 +1569,8 @@ describe('subcommands', () => {
|
|
|
892
1569
|
expect(output).toMatchInlineSnapshot(`
|
|
893
1570
|
"Error: 'unknown' is not a command for 'test pr'.
|
|
894
1571
|
|
|
895
|
-
|
|
896
|
-
test pr --help
|
|
1572
|
+
Suggested command:
|
|
1573
|
+
test pr --help # see all available commands
|
|
897
1574
|
"
|
|
898
1575
|
`)
|
|
899
1576
|
})
|
|
@@ -919,13 +1596,13 @@ describe('subcommands', () => {
|
|
|
919
1596
|
Global Options:
|
|
920
1597
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
921
1598
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1599
|
+
--full-output Show full output envelope
|
|
922
1600
|
--help Show help
|
|
923
1601
|
--llms, --llms-full Print LLM-readable manifest
|
|
924
|
-
--schema Show JSON Schema for
|
|
1602
|
+
--schema Show JSON Schema for command
|
|
925
1603
|
--token-count Print token count of output (instead of output)
|
|
926
1604
|
--token-limit <n> Limit output to n tokens
|
|
927
1605
|
--token-offset <n> Skip first n tokens of output
|
|
928
|
-
--verbose Show full output envelope
|
|
929
1606
|
"
|
|
930
1607
|
`)
|
|
931
1608
|
})
|
|
@@ -1008,7 +1685,7 @@ describe('cta', () => {
|
|
|
1008
1685
|
},
|
|
1009
1686
|
})
|
|
1010
1687
|
|
|
1011
|
-
const { output } = await serve(cli, ['list', '--
|
|
1688
|
+
const { output } = await serve(cli, ['list', '--full-output', '--format', 'json'])
|
|
1012
1689
|
const parsed = JSON.parse(output)
|
|
1013
1690
|
expect(parsed.meta.cta).toEqual({
|
|
1014
1691
|
description: 'Suggested commands:',
|
|
@@ -1029,7 +1706,7 @@ describe('cta', () => {
|
|
|
1029
1706
|
},
|
|
1030
1707
|
})
|
|
1031
1708
|
|
|
1032
|
-
const { output } = await serve(cli, ['list', '--
|
|
1709
|
+
const { output } = await serve(cli, ['list', '--full-output', '--format', 'json'])
|
|
1033
1710
|
const parsed = JSON.parse(output)
|
|
1034
1711
|
expect(parsed.meta.cta.commands).toEqual([
|
|
1035
1712
|
{ command: 'test get 1', description: 'View item 1' },
|
|
@@ -1058,7 +1735,7 @@ describe('cta', () => {
|
|
|
1058
1735
|
},
|
|
1059
1736
|
})
|
|
1060
1737
|
|
|
1061
|
-
const { output } = await serve(cli, ['create', '--
|
|
1738
|
+
const { output } = await serve(cli, ['create', '--full-output', '--format', 'json'])
|
|
1062
1739
|
const parsed = JSON.parse(output)
|
|
1063
1740
|
expect(parsed.meta.cta.commands).toEqual([
|
|
1064
1741
|
{ command: 'test get 1 --limit 10', description: 'View the item' },
|
|
@@ -1078,7 +1755,7 @@ describe('cta', () => {
|
|
|
1078
1755
|
},
|
|
1079
1756
|
})
|
|
1080
1757
|
|
|
1081
|
-
const { output } = await serve(cli, ['list', '--
|
|
1758
|
+
const { output } = await serve(cli, ['list', '--full-output', '--format', 'json'])
|
|
1082
1759
|
const parsed = JSON.parse(output)
|
|
1083
1760
|
expect(parsed.meta.cta.commands).toEqual([{ command: 'test get <id> --format <format>' }])
|
|
1084
1761
|
})
|
|
@@ -1096,7 +1773,7 @@ describe('cta', () => {
|
|
|
1096
1773
|
},
|
|
1097
1774
|
})
|
|
1098
1775
|
|
|
1099
|
-
const { output } = await serve(cli, ['create', '--
|
|
1776
|
+
const { output } = await serve(cli, ['create', '--full-output', '--format', 'json'])
|
|
1100
1777
|
const parsed = JSON.parse(output)
|
|
1101
1778
|
expect(parsed.meta.cta.description).toBe('View the created item:')
|
|
1102
1779
|
})
|
|
@@ -1105,7 +1782,7 @@ describe('cta', () => {
|
|
|
1105
1782
|
const cli = Cli.create('test')
|
|
1106
1783
|
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1107
1784
|
|
|
1108
|
-
const { output } = await serve(cli, ['ping', '--
|
|
1785
|
+
const { output } = await serve(cli, ['ping', '--full-output', '--format', 'json'])
|
|
1109
1786
|
const parsed = JSON.parse(output)
|
|
1110
1787
|
expect(parsed.meta.cta).toBeUndefined()
|
|
1111
1788
|
})
|
|
@@ -1118,7 +1795,7 @@ describe('cta', () => {
|
|
|
1118
1795
|
},
|
|
1119
1796
|
})
|
|
1120
1797
|
|
|
1121
|
-
const { output } = await serve(cli, ['noop', '--
|
|
1798
|
+
const { output } = await serve(cli, ['noop', '--full-output', '--format', 'json'])
|
|
1122
1799
|
const parsed = JSON.parse(output)
|
|
1123
1800
|
expect(parsed.meta.cta).toBeUndefined()
|
|
1124
1801
|
})
|
|
@@ -1138,7 +1815,7 @@ describe('cta', () => {
|
|
|
1138
1815
|
},
|
|
1139
1816
|
})
|
|
1140
1817
|
|
|
1141
|
-
const { output, exitCode } = await serve(cli, ['fail', '--
|
|
1818
|
+
const { output, exitCode } = await serve(cli, ['fail', '--full-output', '--format', 'json'])
|
|
1142
1819
|
expect(exitCode).toBe(1)
|
|
1143
1820
|
const parsed = JSON.parse(output)
|
|
1144
1821
|
expect(parsed.ok).toBe(false)
|
|
@@ -1156,7 +1833,7 @@ describe('cta', () => {
|
|
|
1156
1833
|
},
|
|
1157
1834
|
})
|
|
1158
1835
|
|
|
1159
|
-
const { output, exitCode } = await serve(cli, ['fail', '--
|
|
1836
|
+
const { output, exitCode } = await serve(cli, ['fail', '--full-output', '--format', 'json'])
|
|
1160
1837
|
expect(exitCode).toBe(1)
|
|
1161
1838
|
const parsed = JSON.parse(output)
|
|
1162
1839
|
expect(parsed.meta.cta).toBeUndefined()
|
|
@@ -1170,7 +1847,7 @@ describe('cta', () => {
|
|
|
1170
1847
|
},
|
|
1171
1848
|
})
|
|
1172
1849
|
|
|
1173
|
-
const { output } = await serve(cli, ['fail', '--
|
|
1850
|
+
const { output } = await serve(cli, ['fail', '--full-output', '--format', 'json'])
|
|
1174
1851
|
const parsed = JSON.parse(output)
|
|
1175
1852
|
expect(parsed.ok).toBe(false)
|
|
1176
1853
|
expect(parsed.meta.cta).toBeUndefined()
|
|
@@ -1192,10 +1869,17 @@ describe('cta', () => {
|
|
|
1192
1869
|
})
|
|
1193
1870
|
cli.command(pr)
|
|
1194
1871
|
|
|
1195
|
-
const { output } = await serve(cli, [
|
|
1872
|
+
const { output } = await serve(cli, [
|
|
1873
|
+
'pr',
|
|
1874
|
+
'create',
|
|
1875
|
+
'my-pr',
|
|
1876
|
+
'--full-output',
|
|
1877
|
+
'--format',
|
|
1878
|
+
'json',
|
|
1879
|
+
])
|
|
1196
1880
|
const parsed = JSON.parse(output)
|
|
1197
1881
|
expect(parsed.meta.cta).toEqual({
|
|
1198
|
-
description: 'Suggested
|
|
1882
|
+
description: 'Suggested command:',
|
|
1199
1883
|
commands: [{ command: 'test pr get 42', description: 'View the PR' }],
|
|
1200
1884
|
})
|
|
1201
1885
|
})
|
|
@@ -1232,9 +1916,25 @@ describe('leaf cli', () => {
|
|
|
1232
1916
|
`)
|
|
1233
1917
|
})
|
|
1234
1918
|
|
|
1235
|
-
test('
|
|
1236
|
-
const cli = Cli.create('ping', {
|
|
1919
|
+
test('command option named verbose is parsed by the command', async () => {
|
|
1920
|
+
const cli = Cli.create('ping', {
|
|
1921
|
+
options: z.object({ verbose: z.boolean().default(false) }),
|
|
1922
|
+
run({ options }) {
|
|
1923
|
+
return options
|
|
1924
|
+
},
|
|
1925
|
+
})
|
|
1926
|
+
|
|
1237
1927
|
const { output } = await serve(cli, ['--verbose'])
|
|
1928
|
+
|
|
1929
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1930
|
+
"verbose: true
|
|
1931
|
+
"
|
|
1932
|
+
`)
|
|
1933
|
+
})
|
|
1934
|
+
|
|
1935
|
+
test('--full-output outputs full envelope', async () => {
|
|
1936
|
+
const cli = Cli.create('ping', { run: () => ({ pong: true }) })
|
|
1937
|
+
const { output } = await serve(cli, ['--full-output'])
|
|
1238
1938
|
expect(output).toMatchInlineSnapshot(`
|
|
1239
1939
|
"ok: true
|
|
1240
1940
|
data:
|
|
@@ -1350,22 +2050,22 @@ describe('help', () => {
|
|
|
1350
2050
|
Commands:
|
|
1351
2051
|
ping Health check
|
|
1352
2052
|
|
|
1353
|
-
|
|
2053
|
+
Integrations:
|
|
1354
2054
|
completions Generate shell completion script
|
|
1355
|
-
mcp add Register as
|
|
1356
|
-
skills
|
|
2055
|
+
mcp add Register as MCP server
|
|
2056
|
+
skills Sync skill files to agents (add, list)
|
|
1357
2057
|
|
|
1358
2058
|
Global Options:
|
|
1359
2059
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1360
2060
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2061
|
+
--full-output Show full output envelope
|
|
1361
2062
|
--help Show help
|
|
1362
2063
|
--llms, --llms-full Print LLM-readable manifest
|
|
1363
2064
|
--mcp Start as MCP stdio server
|
|
1364
|
-
--schema Show JSON Schema for
|
|
2065
|
+
--schema Show JSON Schema for command
|
|
1365
2066
|
--token-count Print token count of output (instead of output)
|
|
1366
2067
|
--token-limit <n> Limit output to n tokens
|
|
1367
2068
|
--token-offset <n> Skip first n tokens of output
|
|
1368
|
-
--verbose Show full output envelope
|
|
1369
2069
|
--version Show version
|
|
1370
2070
|
"
|
|
1371
2071
|
`)
|
|
@@ -1388,22 +2088,22 @@ describe('help', () => {
|
|
|
1388
2088
|
Commands:
|
|
1389
2089
|
ping Health check
|
|
1390
2090
|
|
|
1391
|
-
|
|
2091
|
+
Integrations:
|
|
1392
2092
|
completions Generate shell completion script
|
|
1393
|
-
mcp add Register as
|
|
1394
|
-
skills
|
|
2093
|
+
mcp add Register as MCP server
|
|
2094
|
+
skills Sync skill files to agents (add, list)
|
|
1395
2095
|
|
|
1396
2096
|
Global Options:
|
|
1397
2097
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1398
2098
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2099
|
+
--full-output Show full output envelope
|
|
1399
2100
|
--help Show help
|
|
1400
2101
|
--llms, --llms-full Print LLM-readable manifest
|
|
1401
2102
|
--mcp Start as MCP stdio server
|
|
1402
|
-
--schema Show JSON Schema for
|
|
2103
|
+
--schema Show JSON Schema for command
|
|
1403
2104
|
--token-count Print token count of output (instead of output)
|
|
1404
2105
|
--token-limit <n> Limit output to n tokens
|
|
1405
2106
|
--token-offset <n> Skip first n tokens of output
|
|
1406
|
-
--verbose Show full output envelope
|
|
1407
2107
|
--version Show version
|
|
1408
2108
|
"
|
|
1409
2109
|
`)
|
|
@@ -1430,13 +2130,13 @@ describe('help', () => {
|
|
|
1430
2130
|
Global Options:
|
|
1431
2131
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1432
2132
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2133
|
+
--full-output Show full output envelope
|
|
1433
2134
|
--help Show help
|
|
1434
2135
|
--llms, --llms-full Print LLM-readable manifest
|
|
1435
|
-
--schema Show JSON Schema for
|
|
2136
|
+
--schema Show JSON Schema for command
|
|
1436
2137
|
--token-count Print token count of output (instead of output)
|
|
1437
2138
|
--token-limit <n> Limit output to n tokens
|
|
1438
2139
|
--token-offset <n> Skip first n tokens of output
|
|
1439
|
-
--verbose Show full output envelope
|
|
1440
2140
|
"
|
|
1441
2141
|
`)
|
|
1442
2142
|
})
|
|
@@ -1464,13 +2164,13 @@ describe('help', () => {
|
|
|
1464
2164
|
Global Options:
|
|
1465
2165
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1466
2166
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2167
|
+
--full-output Show full output envelope
|
|
1467
2168
|
--help Show help
|
|
1468
2169
|
--llms, --llms-full Print LLM-readable manifest
|
|
1469
|
-
--schema Show JSON Schema for
|
|
2170
|
+
--schema Show JSON Schema for command
|
|
1470
2171
|
--token-count Print token count of output (instead of output)
|
|
1471
2172
|
--token-limit <n> Limit output to n tokens
|
|
1472
2173
|
--token-offset <n> Skip first n tokens of output
|
|
1473
|
-
--verbose Show full output envelope
|
|
1474
2174
|
"
|
|
1475
2175
|
`)
|
|
1476
2176
|
})
|
|
@@ -1551,22 +2251,22 @@ describe('help', () => {
|
|
|
1551
2251
|
Commands:
|
|
1552
2252
|
ping Ping
|
|
1553
2253
|
|
|
1554
|
-
|
|
2254
|
+
Integrations:
|
|
1555
2255
|
completions Generate shell completion script
|
|
1556
|
-
mcp add Register as
|
|
1557
|
-
skills
|
|
2256
|
+
mcp add Register as MCP server
|
|
2257
|
+
skills Sync skill files to agents (add, list)
|
|
1558
2258
|
|
|
1559
2259
|
Global Options:
|
|
1560
2260
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1561
2261
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2262
|
+
--full-output Show full output envelope
|
|
1562
2263
|
--help Show help
|
|
1563
2264
|
--llms, --llms-full Print LLM-readable manifest
|
|
1564
2265
|
--mcp Start as MCP stdio server
|
|
1565
|
-
--schema Show JSON Schema for
|
|
2266
|
+
--schema Show JSON Schema for command
|
|
1566
2267
|
--token-count Print token count of output (instead of output)
|
|
1567
2268
|
--token-limit <n> Limit output to n tokens
|
|
1568
2269
|
--token-offset <n> Skip first n tokens of output
|
|
1569
|
-
--verbose Show full output envelope
|
|
1570
2270
|
--version Show version
|
|
1571
2271
|
"
|
|
1572
2272
|
`)
|
|
@@ -1591,13 +2291,13 @@ describe('help', () => {
|
|
|
1591
2291
|
Global Options:
|
|
1592
2292
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1593
2293
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2294
|
+
--full-output Show full output envelope
|
|
1594
2295
|
--help Show help
|
|
1595
2296
|
--llms, --llms-full Print LLM-readable manifest
|
|
1596
|
-
--schema Show JSON Schema for
|
|
2297
|
+
--schema Show JSON Schema for command
|
|
1597
2298
|
--token-count Print token count of output (instead of output)
|
|
1598
2299
|
--token-limit <n> Limit output to n tokens
|
|
1599
2300
|
--token-offset <n> Skip first n tokens of output
|
|
1600
|
-
--verbose Show full output envelope
|
|
1601
2301
|
"
|
|
1602
2302
|
`)
|
|
1603
2303
|
})
|
|
@@ -1686,13 +2386,13 @@ describe('env', () => {
|
|
|
1686
2386
|
Global Options:
|
|
1687
2387
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1688
2388
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2389
|
+
--full-output Show full output envelope
|
|
1689
2390
|
--help Show help
|
|
1690
2391
|
--llms, --llms-full Print LLM-readable manifest
|
|
1691
|
-
--schema Show JSON Schema for
|
|
2392
|
+
--schema Show JSON Schema for command
|
|
1692
2393
|
--token-count Print token count of output (instead of output)
|
|
1693
2394
|
--token-limit <n> Limit output to n tokens
|
|
1694
2395
|
--token-offset <n> Skip first n tokens of output
|
|
1695
|
-
--verbose Show full output envelope
|
|
1696
2396
|
|
|
1697
2397
|
Environment Variables:
|
|
1698
2398
|
API_TOKEN Auth token
|
|
@@ -1724,16 +2424,16 @@ describe('env', () => {
|
|
|
1724
2424
|
Global Options:
|
|
1725
2425
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1726
2426
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
2427
|
+
--full-output Show full output envelope
|
|
1727
2428
|
--help Show help
|
|
1728
2429
|
--llms, --llms-full Print LLM-readable manifest
|
|
1729
|
-
--schema Show JSON Schema for
|
|
2430
|
+
--schema Show JSON Schema for command
|
|
1730
2431
|
--token-count Print token count of output (instead of output)
|
|
1731
2432
|
--token-limit <n> Limit output to n tokens
|
|
1732
2433
|
--token-offset <n> Skip first n tokens of output
|
|
1733
|
-
--verbose Show full output envelope
|
|
1734
2434
|
|
|
1735
2435
|
Environment Variables:
|
|
1736
|
-
API_TOKEN Auth token (set:
|
|
2436
|
+
API_TOKEN Auth token (set: ****cret)
|
|
1737
2437
|
API_URL API URL (default: https://api.example.com)
|
|
1738
2438
|
"
|
|
1739
2439
|
`)
|
|
@@ -1742,7 +2442,7 @@ describe('env', () => {
|
|
|
1742
2442
|
process.env.API_URL = 'https://custom.example.com'
|
|
1743
2443
|
const { output: output2 } = await serve(cli, ['deploy', '--help'])
|
|
1744
2444
|
expect(output2).toContain(
|
|
1745
|
-
'API_URL API URL (set:
|
|
2445
|
+
'API_URL API URL (set: ****.com, default: https://api.example.com)',
|
|
1746
2446
|
)
|
|
1747
2447
|
} finally {
|
|
1748
2448
|
delete process.env.API_TOKEN
|
|
@@ -1770,7 +2470,7 @@ describe('env', () => {
|
|
|
1770
2470
|
const { output: output2 } = await serve(cli, ['deploy', '--help'], {
|
|
1771
2471
|
env: { API_TOKEN: 'secret' },
|
|
1772
2472
|
})
|
|
1773
|
-
expect(output2).toContain('set:
|
|
2473
|
+
expect(output2).toContain('set: ****cret')
|
|
1774
2474
|
})
|
|
1775
2475
|
|
|
1776
2476
|
test('--llms json includes schema.env', async () => {
|
|
@@ -1838,6 +2538,114 @@ describe('env', () => {
|
|
|
1838
2538
|
})
|
|
1839
2539
|
})
|
|
1840
2540
|
|
|
2541
|
+
describe('built-in commands', () => {
|
|
2542
|
+
test('bare completions shows help', async () => {
|
|
2543
|
+
const cli = Cli.create('test')
|
|
2544
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2545
|
+
const { output } = await serve(cli, ['completions'])
|
|
2546
|
+
expect(output).toContain('Generate shell completion script')
|
|
2547
|
+
})
|
|
2548
|
+
|
|
2549
|
+
test('completions --help shows help', async () => {
|
|
2550
|
+
const cli = Cli.create('test')
|
|
2551
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2552
|
+
const { output } = await serve(cli, ['completions', '--help'])
|
|
2553
|
+
expect(output).toContain('test completions')
|
|
2554
|
+
expect(output).toContain('Generate shell completion script')
|
|
2555
|
+
})
|
|
2556
|
+
|
|
2557
|
+
test('bare mcp shows help with subcommands', async () => {
|
|
2558
|
+
const cli = Cli.create('test')
|
|
2559
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2560
|
+
const { output } = await serve(cli, ['mcp'])
|
|
2561
|
+
expect(output).toContain('test mcp')
|
|
2562
|
+
expect(output).toContain('Register as MCP server')
|
|
2563
|
+
expect(output).toContain('add')
|
|
2564
|
+
})
|
|
2565
|
+
|
|
2566
|
+
test('mcp --help shows help with subcommands', async () => {
|
|
2567
|
+
const cli = Cli.create('test')
|
|
2568
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2569
|
+
const { output } = await serve(cli, ['mcp', '--help'])
|
|
2570
|
+
expect(output).toContain('test mcp')
|
|
2571
|
+
expect(output).toContain('add')
|
|
2572
|
+
})
|
|
2573
|
+
|
|
2574
|
+
test('mcp add --help shows options', async () => {
|
|
2575
|
+
const cli = Cli.create('test')
|
|
2576
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2577
|
+
const { output } = await serve(cli, ['mcp', 'add', '--help'])
|
|
2578
|
+
expect(output).toContain('test mcp add')
|
|
2579
|
+
expect(output).toContain('--command')
|
|
2580
|
+
expect(output).toContain('--no-global')
|
|
2581
|
+
expect(output).toContain('--agent')
|
|
2582
|
+
})
|
|
2583
|
+
|
|
2584
|
+
test('bare skills shows help with subcommands', async () => {
|
|
2585
|
+
const cli = Cli.create('test')
|
|
2586
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2587
|
+
const { output } = await serve(cli, ['skills'])
|
|
2588
|
+
expect(output).toContain('test skills')
|
|
2589
|
+
expect(output).toContain('Sync skill files to agents')
|
|
2590
|
+
expect(output).toContain('add')
|
|
2591
|
+
})
|
|
2592
|
+
|
|
2593
|
+
test('skills --help shows help with subcommands', async () => {
|
|
2594
|
+
const cli = Cli.create('test')
|
|
2595
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2596
|
+
const { output } = await serve(cli, ['skills', '--help'])
|
|
2597
|
+
expect(output).toContain('test skills')
|
|
2598
|
+
expect(output).toContain('add')
|
|
2599
|
+
})
|
|
2600
|
+
|
|
2601
|
+
test('skills typo suggests add', async () => {
|
|
2602
|
+
const cli = Cli.create('test')
|
|
2603
|
+
cli.command('ping', { run: () => ({}) })
|
|
2604
|
+
const { output, exitCode } = await serve(cli, ['skills', 'addd'])
|
|
2605
|
+
expect(exitCode).toBe(1)
|
|
2606
|
+
expect(output).toContain("Did you mean 'add'?")
|
|
2607
|
+
expect(output).toContain('test skills add')
|
|
2608
|
+
expect(output).toContain('test skills --help')
|
|
2609
|
+
})
|
|
2610
|
+
|
|
2611
|
+
test('mcp typo suggests add', async () => {
|
|
2612
|
+
const cli = Cli.create('test')
|
|
2613
|
+
cli.command('ping', { run: () => ({}) })
|
|
2614
|
+
const { output, exitCode } = await serve(cli, ['mcp', 'addd'])
|
|
2615
|
+
expect(exitCode).toBe(1)
|
|
2616
|
+
expect(output).toContain("Did you mean 'add'?")
|
|
2617
|
+
expect(output).toContain('test mcp add')
|
|
2618
|
+
})
|
|
2619
|
+
|
|
2620
|
+
test('skills add --help shows options', async () => {
|
|
2621
|
+
const cli = Cli.create('test')
|
|
2622
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2623
|
+
const { output } = await serve(cli, ['skills', 'add', '--help'])
|
|
2624
|
+
expect(output).toContain('test skills add')
|
|
2625
|
+
expect(output).toContain('--depth')
|
|
2626
|
+
expect(output).toContain('--no-global')
|
|
2627
|
+
})
|
|
2628
|
+
|
|
2629
|
+
test('skills list --help shows description', async () => {
|
|
2630
|
+
const cli = Cli.create('test')
|
|
2631
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
2632
|
+
const { output } = await serve(cli, ['skills', 'list', '--help'])
|
|
2633
|
+
expect(output).toContain('test skills list')
|
|
2634
|
+
expect(output).toContain('List skills')
|
|
2635
|
+
})
|
|
2636
|
+
|
|
2637
|
+
test('skills list shows skills with install status', async () => {
|
|
2638
|
+
const cli = Cli.create('test')
|
|
2639
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
2640
|
+
cli.command('greet', { description: 'Say hello', run: () => ({ hi: true }) })
|
|
2641
|
+
const { output } = await serve(cli, ['skills', 'list'])
|
|
2642
|
+
expect(output).toContain('✗')
|
|
2643
|
+
expect(output).toContain('test-ping')
|
|
2644
|
+
expect(output).toContain('test-greet')
|
|
2645
|
+
expect(output).toContain('installed')
|
|
2646
|
+
})
|
|
2647
|
+
})
|
|
2648
|
+
|
|
1841
2649
|
describe('skills staleness', () => {
|
|
1842
2650
|
let stderrSpy: ReturnType<typeof vi.spyOn>
|
|
1843
2651
|
|
|
@@ -1848,15 +2656,57 @@ describe('skills staleness', () => {
|
|
|
1848
2656
|
|
|
1849
2657
|
afterEach(() => {
|
|
1850
2658
|
stderrSpy.mockRestore()
|
|
2659
|
+
__mockSkillsHash = undefined
|
|
1851
2660
|
})
|
|
1852
2661
|
|
|
1853
|
-
test('
|
|
2662
|
+
test('includes skills CTA when stale', async () => {
|
|
1854
2663
|
__mockSkillsHash = '0000000000000000'
|
|
1855
2664
|
const cli = Cli.create('test')
|
|
1856
2665
|
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
1857
2666
|
|
|
1858
|
-
await serve(cli, ['ping'])
|
|
1859
|
-
expect(
|
|
2667
|
+
const { output } = await serve(cli, ['ping'])
|
|
2668
|
+
expect(output).toContain('Skills are out of date:')
|
|
2669
|
+
expect(output).toContain('skills add')
|
|
2670
|
+
})
|
|
2671
|
+
|
|
2672
|
+
test('uses displayName for stale skills CTA when invoked directly', async () => {
|
|
2673
|
+
const savedArgv1 = process.argv[1]
|
|
2674
|
+
const savedAgent = process.env.npm_config_user_agent
|
|
2675
|
+
const savedExec = process.env.npm_execpath
|
|
2676
|
+
try {
|
|
2677
|
+
process.argv[1] = '/usr/local/bin/mc'
|
|
2678
|
+
delete process.env.npm_config_user_agent
|
|
2679
|
+
delete process.env.npm_execpath
|
|
2680
|
+
|
|
2681
|
+
__mockSkillsHash = '0000000000000000'
|
|
2682
|
+
const cli = Cli.create({ name: 'my-cli', aliases: ['mc'] })
|
|
2683
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
2684
|
+
|
|
2685
|
+
const { output } = await serve(cli, ['ping'])
|
|
2686
|
+
|
|
2687
|
+
expect(output).toContain('mc skills add')
|
|
2688
|
+
expect(output).not.toContain('npx my-cli skills add')
|
|
2689
|
+
} finally {
|
|
2690
|
+
if (savedArgv1 === undefined) process.argv[1] = undefined as any
|
|
2691
|
+
else process.argv[1] = savedArgv1
|
|
2692
|
+
process.env.npm_config_user_agent = savedAgent
|
|
2693
|
+
process.env.npm_execpath = savedExec
|
|
2694
|
+
}
|
|
2695
|
+
})
|
|
2696
|
+
|
|
2697
|
+
test('merges skills CTA with command CTA', async () => {
|
|
2698
|
+
__mockSkillsHash = '0000000000000000'
|
|
2699
|
+
;(process.stdout as any).isTTY = true
|
|
2700
|
+
const cli = Cli.create('test')
|
|
2701
|
+
cli.command('ping', {
|
|
2702
|
+
description: 'Health check',
|
|
2703
|
+
run: (c) => c.ok({ pong: true }, { cta: { commands: ['status'] } }),
|
|
2704
|
+
})
|
|
2705
|
+
|
|
2706
|
+
const { output } = await serve(cli, ['ping'])
|
|
2707
|
+
;(process.stdout as any).isTTY = false
|
|
2708
|
+
expect(output).toContain('status')
|
|
2709
|
+
expect(output).toContain('skills add')
|
|
1860
2710
|
})
|
|
1861
2711
|
|
|
1862
2712
|
test('does not warn when hash matches', async () => {
|
|
@@ -1865,8 +2715,8 @@ describe('skills staleness', () => {
|
|
|
1865
2715
|
const cli = Cli.create('test')
|
|
1866
2716
|
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
1867
2717
|
|
|
1868
|
-
await serve(cli, ['ping'])
|
|
1869
|
-
expect(
|
|
2718
|
+
const { output } = await serve(cli, ['ping'])
|
|
2719
|
+
expect(output).not.toContain('Skills are out of date')
|
|
1870
2720
|
})
|
|
1871
2721
|
|
|
1872
2722
|
test('does not warn when no hash stored', async () => {
|
|
@@ -1874,8 +2724,8 @@ describe('skills staleness', () => {
|
|
|
1874
2724
|
const cli = Cli.create('test')
|
|
1875
2725
|
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1876
2726
|
|
|
1877
|
-
await serve(cli, ['ping'])
|
|
1878
|
-
expect(
|
|
2727
|
+
const { output } = await serve(cli, ['ping'])
|
|
2728
|
+
expect(output).not.toContain('Skills are out of date')
|
|
1879
2729
|
})
|
|
1880
2730
|
|
|
1881
2731
|
test('does not warn for skills add', async () => {
|
|
@@ -1892,8 +2742,8 @@ describe('skills staleness', () => {
|
|
|
1892
2742
|
const cli = Cli.create('test')
|
|
1893
2743
|
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1894
2744
|
|
|
1895
|
-
await serve(cli, ['--help'])
|
|
1896
|
-
expect(
|
|
2745
|
+
const { output } = await serve(cli, ['--help'])
|
|
2746
|
+
expect(output).not.toContain('Skills are out of date')
|
|
1897
2747
|
})
|
|
1898
2748
|
})
|
|
1899
2749
|
|
|
@@ -2114,10 +2964,10 @@ describe('outputPolicy', () => {
|
|
|
2114
2964
|
expect(deploy.output).not.toContain('deploy-123')
|
|
2115
2965
|
expect(deploy.output).toContain('Check status')
|
|
2116
2966
|
|
|
2117
|
-
// deploy --
|
|
2118
|
-
const
|
|
2119
|
-
expect(
|
|
2120
|
-
expect(
|
|
2967
|
+
// deploy --full-output: agent mode shows everything
|
|
2968
|
+
const deployFullOutput = await serve(cli, ['deploy', 'staging', '--full-output'])
|
|
2969
|
+
expect(deployFullOutput.output).toContain('deploy-123')
|
|
2970
|
+
expect(deployFullOutput.output).toContain('staging.example.com')
|
|
2121
2971
|
|
|
2122
2972
|
// deploy --json: agent mode shows data
|
|
2123
2973
|
const deployJson = await serve(cli, ['deploy', 'staging', '--json'])
|
|
@@ -2270,24 +3120,6 @@ describe('outputPolicy', () => {
|
|
|
2270
3120
|
expect(capturedEnv).toEqual({ API_TOKEN: 'secret-123', API_URL: 'https://api.example.com' })
|
|
2271
3121
|
})
|
|
2272
3122
|
|
|
2273
|
-
test('e2e: middleware receives parsed CLI-level options', async () => {
|
|
2274
|
-
let capturedOptions: any
|
|
2275
|
-
const cli = Cli.create('test', {
|
|
2276
|
-
options: z.object({
|
|
2277
|
-
token: z.string().default(''),
|
|
2278
|
-
dry: z.boolean().default(false),
|
|
2279
|
-
}),
|
|
2280
|
-
})
|
|
2281
|
-
.use(async (c, next) => {
|
|
2282
|
-
capturedOptions = c.options
|
|
2283
|
-
await next()
|
|
2284
|
-
})
|
|
2285
|
-
.command('deploy', { run: () => ({ ok: true }) })
|
|
2286
|
-
|
|
2287
|
-
await serve(cli, ['deploy', '--token', 'abc123', '--dry'])
|
|
2288
|
-
expect(capturedOptions).toEqual({ token: 'abc123', dry: true })
|
|
2289
|
-
})
|
|
2290
|
-
|
|
2291
3123
|
test('e2e: CLI-level env validation error before middleware runs', async () => {
|
|
2292
3124
|
const cli = Cli.create('test', {
|
|
2293
3125
|
env: z.object({ API_TOKEN: z.string() }),
|
|
@@ -2891,11 +3723,11 @@ describe('fetch', async () => {
|
|
|
2891
3723
|
expect(JSON.parse(output)).toEqual({ ok: true })
|
|
2892
3724
|
})
|
|
2893
3725
|
|
|
2894
|
-
test('--
|
|
3726
|
+
test('--full-output includes request/response meta', async () => {
|
|
2895
3727
|
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
2896
3728
|
fetch: app.fetch,
|
|
2897
3729
|
})
|
|
2898
|
-
const { output } = await serve(cli, ['api', 'health', '--
|
|
3730
|
+
const { output } = await serve(cli, ['api', 'health', '--full-output', '--format', 'json'])
|
|
2899
3731
|
const parsed = JSON.parse(output)
|
|
2900
3732
|
expect(parsed.ok).toBe(true)
|
|
2901
3733
|
expect(parsed.data).toEqual({ ok: true })
|
|
@@ -2923,6 +3755,15 @@ describe('fetch', async () => {
|
|
|
2923
3755
|
`)
|
|
2924
3756
|
})
|
|
2925
3757
|
|
|
3758
|
+
test('root-level fetch with typo of known command → did you mean', async () => {
|
|
3759
|
+
const cli = Cli.create('api', { description: 'API', fetch: app.fetch }).command('upgrade', {
|
|
3760
|
+
run: () => ({ upgraded: true }),
|
|
3761
|
+
})
|
|
3762
|
+
const { output, exitCode } = await serve(cli, ['upgra'])
|
|
3763
|
+
expect(exitCode).toBe(1)
|
|
3764
|
+
expect(output).toContain("Did you mean 'upgrade'?")
|
|
3765
|
+
})
|
|
3766
|
+
|
|
2926
3767
|
test('root-level fetch with no args → root path', async () => {
|
|
2927
3768
|
const cli = Cli.create('api', { description: 'API', fetch: app.fetch })
|
|
2928
3769
|
// Hono returns 404 for / since we don't have a root route
|
|
@@ -3105,6 +3946,14 @@ describe('fetch', () => {
|
|
|
3105
3946
|
`)
|
|
3106
3947
|
})
|
|
3107
3948
|
|
|
3949
|
+
test('GET /helath → 404 with suggestion', async () => {
|
|
3950
|
+
const cli = Cli.create('test')
|
|
3951
|
+
cli.command('health', { run: () => ({}) })
|
|
3952
|
+
const res = await fetchJson(cli, new Request('http://localhost/helath'))
|
|
3953
|
+
expect(res.status).toBe(404)
|
|
3954
|
+
expect(res.body.error.message).toContain("Did you mean 'health'?")
|
|
3955
|
+
})
|
|
3956
|
+
|
|
3108
3957
|
test('GET / with root command → 200', async () => {
|
|
3109
3958
|
const cli = Cli.create('test', { run: () => ({ root: true }) })
|
|
3110
3959
|
expect(await fetchJson(cli, new Request('http://localhost/'))).toMatchInlineSnapshot(`
|
|
@@ -3393,6 +4242,84 @@ describe('fetch', () => {
|
|
|
3393
4242
|
`)
|
|
3394
4243
|
})
|
|
3395
4244
|
|
|
4245
|
+
test('group middleware runs for nested commands', async () => {
|
|
4246
|
+
const sub = Cli.create('admin', {
|
|
4247
|
+
vars: z.object({ role: z.string().default('none') }),
|
|
4248
|
+
})
|
|
4249
|
+
sub.use(async (c, next) => {
|
|
4250
|
+
c.set('role', 'admin')
|
|
4251
|
+
await next()
|
|
4252
|
+
})
|
|
4253
|
+
sub.command('status', {
|
|
4254
|
+
run: (c) => ({ role: c.var.role }),
|
|
4255
|
+
})
|
|
4256
|
+
const cli = Cli.create('test', {
|
|
4257
|
+
vars: z.object({ role: z.string().default('none') }),
|
|
4258
|
+
})
|
|
4259
|
+
cli.command(sub)
|
|
4260
|
+
expect(await fetchJson(cli, new Request('http://localhost/admin/status')))
|
|
4261
|
+
.toMatchInlineSnapshot(`
|
|
4262
|
+
{
|
|
4263
|
+
"body": {
|
|
4264
|
+
"data": {
|
|
4265
|
+
"role": "admin",
|
|
4266
|
+
},
|
|
4267
|
+
"meta": {
|
|
4268
|
+
"command": "admin status",
|
|
4269
|
+
"duration": "<stripped>",
|
|
4270
|
+
},
|
|
4271
|
+
"ok": true,
|
|
4272
|
+
},
|
|
4273
|
+
"status": 200,
|
|
4274
|
+
}
|
|
4275
|
+
`)
|
|
4276
|
+
})
|
|
4277
|
+
|
|
4278
|
+
test('cli-level env schema is parsed', async () => {
|
|
4279
|
+
const cli = Cli.create('test', {
|
|
4280
|
+
env: z.object({ APP_TOKEN: z.string().default('fallback') }),
|
|
4281
|
+
})
|
|
4282
|
+
cli.use(async (c, next) => {
|
|
4283
|
+
// env should be parsed from envSchema
|
|
4284
|
+
;(globalThis as any).__testEnv = c.env
|
|
4285
|
+
await next()
|
|
4286
|
+
})
|
|
4287
|
+
cli.command('check', { run: () => ({ ok: true }) })
|
|
4288
|
+
await cli.fetch(new Request('http://localhost/check'))
|
|
4289
|
+
expect((globalThis as any).__testEnv).toEqual({ APP_TOKEN: 'fallback' })
|
|
4290
|
+
delete (globalThis as any).__testEnv
|
|
4291
|
+
})
|
|
4292
|
+
|
|
4293
|
+
test('retryable error is propagated', async () => {
|
|
4294
|
+
const cli = Cli.create('test')
|
|
4295
|
+
cli.command('rate-limit', {
|
|
4296
|
+
run: (c) => c.error({ code: 'RATE_LIMITED', message: 'slow down', retryable: true }),
|
|
4297
|
+
})
|
|
4298
|
+
const { body } = await fetchJson(cli, new Request('http://localhost/rate-limit'))
|
|
4299
|
+
expect(body.ok).toBe(false)
|
|
4300
|
+
expect(body.error.retryable).toBe(true)
|
|
4301
|
+
})
|
|
4302
|
+
|
|
4303
|
+
test('cta block is propagated', async () => {
|
|
4304
|
+
const cli = Cli.create('test')
|
|
4305
|
+
cli.command('done', {
|
|
4306
|
+
run: (c) =>
|
|
4307
|
+
c.ok({ id: 1 }, { cta: { commands: ['list'], description: 'Suggested commands:' } }),
|
|
4308
|
+
})
|
|
4309
|
+
const { body } = await fetchJson(cli, new Request('http://localhost/done'))
|
|
4310
|
+
expect(body.ok).toBe(true)
|
|
4311
|
+
expect(body.meta.cta).toMatchInlineSnapshot(`
|
|
4312
|
+
{
|
|
4313
|
+
"commands": [
|
|
4314
|
+
{
|
|
4315
|
+
"command": "test list",
|
|
4316
|
+
},
|
|
4317
|
+
],
|
|
4318
|
+
"description": "Suggested commands:",
|
|
4319
|
+
}
|
|
4320
|
+
`)
|
|
4321
|
+
})
|
|
4322
|
+
|
|
3396
4323
|
describe('mcp over http', () => {
|
|
3397
4324
|
function mcpCli() {
|
|
3398
4325
|
const cli = Cli.create('test', { version: '1.0.0' })
|
|
@@ -3539,3 +4466,189 @@ describe('fetch', () => {
|
|
|
3539
4466
|
})
|
|
3540
4467
|
})
|
|
3541
4468
|
})
|
|
4469
|
+
|
|
4470
|
+
describe('displayName', () => {
|
|
4471
|
+
beforeEach(() => {
|
|
4472
|
+
const savedArgv1 = process.argv[1]
|
|
4473
|
+
return () => {
|
|
4474
|
+
process.argv[1] = savedArgv1!
|
|
4475
|
+
}
|
|
4476
|
+
})
|
|
4477
|
+
|
|
4478
|
+
test('defaults to name when argv[1] is not an alias', async () => {
|
|
4479
|
+
process.argv[1] = '/usr/local/bin/my-cli'
|
|
4480
|
+
const cli = Cli.create({
|
|
4481
|
+
name: 'my-cli',
|
|
4482
|
+
aliases: ['mc'],
|
|
4483
|
+
}).command('ping', {
|
|
4484
|
+
run: (c) => c.ok({ displayName: c.displayName }),
|
|
4485
|
+
})
|
|
4486
|
+
const { output } = await serve(cli, ['ping', '--json'])
|
|
4487
|
+
expect(JSON.parse(output).displayName).toBe('my-cli')
|
|
4488
|
+
})
|
|
4489
|
+
|
|
4490
|
+
test('resolves alias from argv[1]', async () => {
|
|
4491
|
+
process.argv[1] = '/usr/local/bin/mc'
|
|
4492
|
+
const cli = Cli.create({
|
|
4493
|
+
name: 'my-cli',
|
|
4494
|
+
aliases: ['mc'],
|
|
4495
|
+
}).command('ping', {
|
|
4496
|
+
run: (c) => c.ok({ displayName: c.displayName }),
|
|
4497
|
+
})
|
|
4498
|
+
const { output } = await serve(cli, ['ping', '--json'])
|
|
4499
|
+
expect(JSON.parse(output).displayName).toBe('mc')
|
|
4500
|
+
})
|
|
4501
|
+
|
|
4502
|
+
test('falls back to name when argv[1] is undefined', async () => {
|
|
4503
|
+
process.argv[1] = undefined as any
|
|
4504
|
+
const cli = Cli.create({
|
|
4505
|
+
name: 'my-cli',
|
|
4506
|
+
aliases: ['mc'],
|
|
4507
|
+
}).command('ping', {
|
|
4508
|
+
run: (c) => c.ok({ displayName: c.displayName }),
|
|
4509
|
+
})
|
|
4510
|
+
const { output } = await serve(cli, ['ping', '--json'])
|
|
4511
|
+
expect(JSON.parse(output).displayName).toBe('my-cli')
|
|
4512
|
+
})
|
|
4513
|
+
|
|
4514
|
+
test('available in middleware context', async () => {
|
|
4515
|
+
process.argv[1] = '/usr/local/bin/mc'
|
|
4516
|
+
let middlewareDisplayName: string | undefined
|
|
4517
|
+
const cli = Cli.create({
|
|
4518
|
+
name: 'my-cli',
|
|
4519
|
+
aliases: ['mc'],
|
|
4520
|
+
})
|
|
4521
|
+
.use((c, next) => {
|
|
4522
|
+
middlewareDisplayName = c.displayName
|
|
4523
|
+
return next()
|
|
4524
|
+
})
|
|
4525
|
+
.command('ping', {
|
|
4526
|
+
run: (c) => c.ok({ ok: true }),
|
|
4527
|
+
})
|
|
4528
|
+
await serve(cli, ['ping', '--json'])
|
|
4529
|
+
expect(middlewareDisplayName).toBe('mc')
|
|
4530
|
+
})
|
|
4531
|
+
|
|
4532
|
+
test('available in root run context', async () => {
|
|
4533
|
+
process.argv[1] = '/usr/local/bin/mc'
|
|
4534
|
+
const cli = Cli.create({
|
|
4535
|
+
name: 'my-cli',
|
|
4536
|
+
aliases: ['mc'],
|
|
4537
|
+
run: (c) => c.ok({ displayName: c.displayName }),
|
|
4538
|
+
})
|
|
4539
|
+
const { output } = await serve(cli, ['--json'])
|
|
4540
|
+
expect(JSON.parse(output).displayName).toBe('mc')
|
|
4541
|
+
})
|
|
4542
|
+
|
|
4543
|
+
test('cta commands use displayName', async () => {
|
|
4544
|
+
process.argv[1] = '/usr/local/bin/mc'
|
|
4545
|
+
const cli = Cli.create({
|
|
4546
|
+
name: 'my-cli',
|
|
4547
|
+
aliases: ['mc'],
|
|
4548
|
+
}).command('ping', {
|
|
4549
|
+
run: (c) => c.ok({ ok: true }, { cta: { commands: ['login'] } }),
|
|
4550
|
+
})
|
|
4551
|
+
const { output } = await serve(cli, ['ping', '--json', '--full-output'])
|
|
4552
|
+
const parsed = JSON.parse(output)
|
|
4553
|
+
expect(parsed.meta.cta.commands[0].command).toBe('mc login')
|
|
4554
|
+
})
|
|
4555
|
+
})
|
|
4556
|
+
|
|
4557
|
+
test('--format rejects invalid format values', async () => {
|
|
4558
|
+
const cli = Cli.create('test').command('hello', {
|
|
4559
|
+
run: (c) => c.ok({ message: 'hi' }),
|
|
4560
|
+
})
|
|
4561
|
+
|
|
4562
|
+
const { exitCode, output } = await serve(cli, ['hello', '--format', 'xml'])
|
|
4563
|
+
expect(exitCode).toBe(1)
|
|
4564
|
+
expect(output).toMatch(/invalid|unsupported|unknown.*format/i)
|
|
4565
|
+
})
|
|
4566
|
+
|
|
4567
|
+
test('--token-limit with non-numeric value errors', async () => {
|
|
4568
|
+
const cli = Cli.create('test').command('hello', {
|
|
4569
|
+
run: (c) => c.ok({ message: 'hello world' }),
|
|
4570
|
+
})
|
|
4571
|
+
|
|
4572
|
+
const { exitCode, output } = await serve(cli, ['hello', '--token-limit', 'foo', '--json'])
|
|
4573
|
+
expect(exitCode).toBe(1)
|
|
4574
|
+
expect(output).not.toContain('NaN')
|
|
4575
|
+
})
|
|
4576
|
+
|
|
4577
|
+
test('--token-offset with non-numeric value errors', async () => {
|
|
4578
|
+
const cli = Cli.create('test').command('hello', {
|
|
4579
|
+
run: (c) => c.ok({ message: 'hello world' }),
|
|
4580
|
+
})
|
|
4581
|
+
|
|
4582
|
+
const { exitCode, output } = await serve(cli, ['hello', '--token-offset', 'foo', '--json'])
|
|
4583
|
+
expect(exitCode).toBe(1)
|
|
4584
|
+
expect(output).not.toContain('NaN')
|
|
4585
|
+
})
|
|
4586
|
+
|
|
4587
|
+
describe('command aliases', () => {
|
|
4588
|
+
function makeAliasedCli() {
|
|
4589
|
+
return Cli.create('gh').command('extension', {
|
|
4590
|
+
aliases: ['extensions', 'ext'],
|
|
4591
|
+
description: 'Manage extensions',
|
|
4592
|
+
run: () => ({ result: 'ok' }),
|
|
4593
|
+
})
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
test('resolves canonical command name', async () => {
|
|
4597
|
+
const { output } = await serve(makeAliasedCli(), ['extension'])
|
|
4598
|
+
expect(output).toContain('ok')
|
|
4599
|
+
})
|
|
4600
|
+
|
|
4601
|
+
test('resolves alias name', async () => {
|
|
4602
|
+
const { output } = await serve(makeAliasedCli(), ['extensions'])
|
|
4603
|
+
expect(output).toContain('ok')
|
|
4604
|
+
})
|
|
4605
|
+
|
|
4606
|
+
test('resolves short alias name', async () => {
|
|
4607
|
+
const { output } = await serve(makeAliasedCli(), ['ext'])
|
|
4608
|
+
expect(output).toContain('ok')
|
|
4609
|
+
})
|
|
4610
|
+
|
|
4611
|
+
test('root help does not show aliases', async () => {
|
|
4612
|
+
const { output } = await serve(makeAliasedCli(), ['--help'])
|
|
4613
|
+
const commandsSection = output.split('Commands:')[1]!.split('Integrations:')[0]!
|
|
4614
|
+
const names = commandsSection
|
|
4615
|
+
.trim()
|
|
4616
|
+
.split('\n')
|
|
4617
|
+
.map((l) => l.trim().split(/\s{2,}/)[0]!)
|
|
4618
|
+
expect(names).toContain('extension')
|
|
4619
|
+
expect(names).not.toContain('extensions')
|
|
4620
|
+
expect(names).not.toContain('ext')
|
|
4621
|
+
})
|
|
4622
|
+
|
|
4623
|
+
test('command help shows aliases line', async () => {
|
|
4624
|
+
const { output } = await serve(makeAliasedCli(), ['extension', '--help'])
|
|
4625
|
+
expect(output).toContain('Aliases: extensions, ext')
|
|
4626
|
+
})
|
|
4627
|
+
|
|
4628
|
+
test('aliases work inside command groups', async () => {
|
|
4629
|
+
const sub = Cli.create('repo', { description: 'Manage repos' }).command('list', {
|
|
4630
|
+
aliases: ['ls'],
|
|
4631
|
+
description: 'List repos',
|
|
4632
|
+
run: () => ({ repos: [] }),
|
|
4633
|
+
})
|
|
4634
|
+
const cli = Cli.create('gh').command(sub)
|
|
4635
|
+
const { output } = await serve(cli, ['repo', 'ls'])
|
|
4636
|
+
expect(output).toContain('repos')
|
|
4637
|
+
})
|
|
4638
|
+
|
|
4639
|
+
test('did-you-mean suggests aliases', async () => {
|
|
4640
|
+
const { output } = await serve(makeAliasedCli(), ['exten'])
|
|
4641
|
+
expect(output).toMatch(/did you mean.*extension/i)
|
|
4642
|
+
})
|
|
4643
|
+
|
|
4644
|
+
test('root CLI aliases register as command aliases', async () => {
|
|
4645
|
+
const update = Cli.create('update', {
|
|
4646
|
+
aliases: ['upgrade'],
|
|
4647
|
+
description: 'Update packages',
|
|
4648
|
+
run: () => ({ result: 'updated' }),
|
|
4649
|
+
})
|
|
4650
|
+
const cli = Cli.create('pkg').command(update)
|
|
4651
|
+
const { output } = await serve(cli, ['upgrade'])
|
|
4652
|
+
expect(output).toContain('updated')
|
|
4653
|
+
})
|
|
4654
|
+
})
|