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