incur 0.0.0 → 0.0.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/LICENSE +21 -0
- package/README.md +141 -0
- package/SKILL.md +664 -0
- package/dist/Cli.d.ts +255 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +900 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Errors.d.ts +92 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +75 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Formatter.d.ts +5 -0
- package/dist/Formatter.d.ts.map +1 -0
- package/dist/Formatter.js +91 -0
- package/dist/Formatter.js.map +1 -0
- package/dist/Help.d.ts +53 -0
- package/dist/Help.d.ts.map +1 -0
- package/dist/Help.js +231 -0
- package/dist/Help.js.map +1 -0
- package/dist/Mcp.d.ts +13 -0
- package/dist/Mcp.d.ts.map +1 -0
- package/dist/Mcp.js +140 -0
- package/dist/Mcp.js.map +1 -0
- package/dist/Parser.d.ts +24 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +215 -0
- package/dist/Parser.js.map +1 -0
- package/dist/Register.d.ts +19 -0
- package/dist/Register.d.ts.map +1 -0
- package/dist/Register.js +2 -0
- package/dist/Register.js.map +1 -0
- package/dist/Schema.d.ts +4 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +8 -0
- package/dist/Schema.js.map +1 -0
- package/dist/Skill.d.ts +29 -0
- package/dist/Skill.d.ts.map +1 -0
- package/dist/Skill.js +196 -0
- package/dist/Skill.js.map +1 -0
- package/dist/Skillgen.d.ts +3 -0
- package/dist/Skillgen.d.ts.map +1 -0
- package/dist/Skillgen.js +67 -0
- package/dist/Skillgen.js.map +1 -0
- package/dist/SyncMcp.d.ts +23 -0
- package/dist/SyncMcp.d.ts.map +1 -0
- package/dist/SyncMcp.js +100 -0
- package/dist/SyncMcp.js.map +1 -0
- package/dist/SyncSkills.d.ts +38 -0
- package/dist/SyncSkills.d.ts.map +1 -0
- package/dist/SyncSkills.js +163 -0
- package/dist/SyncSkills.js.map +1 -0
- package/dist/Typegen.d.ts +6 -0
- package/dist/Typegen.d.ts.map +1 -0
- package/dist/Typegen.js +92 -0
- package/dist/Typegen.js.map +1 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +30 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/pm.d.ts +3 -0
- package/dist/internal/pm.d.ts.map +1 -0
- package/dist/internal/pm.js +11 -0
- package/dist/internal/pm.js.map +1 -0
- package/dist/internal/types.d.ts +11 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/internal/utils.d.ts +8 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js +51 -0
- package/dist/internal/utils.js.map +1 -0
- package/examples/npm/cli.ts +180 -0
- package/examples/npm/node_modules/.bin/incur.src +21 -0
- package/examples/npm/node_modules/.bin/tsx +21 -0
- package/examples/npm/package.json +14 -0
- package/examples/npm/tsconfig.json +9 -0
- package/examples/presto/cli.ts +246 -0
- package/examples/presto/node_modules/.bin/incur.src +21 -0
- package/examples/presto/node_modules/.bin/tsx +21 -0
- package/examples/presto/package.json +14 -0
- package/examples/presto/tsconfig.json +9 -0
- package/package.json +53 -2
- package/src/Cli.test-d.ts +135 -0
- package/src/Cli.test.ts +1373 -0
- package/src/Cli.ts +1470 -0
- package/src/Errors.test.ts +96 -0
- package/src/Errors.ts +139 -0
- package/src/Formatter.test.ts +245 -0
- package/src/Formatter.ts +106 -0
- package/src/Help.test.ts +124 -0
- package/src/Help.ts +302 -0
- package/src/Mcp.test.ts +254 -0
- package/src/Mcp.ts +195 -0
- package/src/Parser.test-d.ts +45 -0
- package/src/Parser.test.ts +118 -0
- package/src/Parser.ts +247 -0
- package/src/Register.ts +18 -0
- package/src/Schema.test.ts +125 -0
- package/src/Schema.ts +8 -0
- package/src/Skill.test.ts +293 -0
- package/src/Skill.ts +253 -0
- package/src/Skillgen.ts +66 -0
- package/src/SyncMcp.test.ts +75 -0
- package/src/SyncMcp.ts +132 -0
- package/src/SyncSkills.test.ts +92 -0
- package/src/SyncSkills.ts +205 -0
- package/src/Typegen.test.ts +150 -0
- package/src/Typegen.ts +107 -0
- package/src/bin.ts +33 -0
- package/src/e2e.test.ts +1710 -0
- package/src/index.ts +14 -0
- package/src/internal/pm.test.ts +38 -0
- package/src/internal/pm.ts +8 -0
- package/src/internal/types.ts +22 -0
- package/src/internal/utils.ts +50 -0
- package/src/tsconfig.json +8 -0
package/src/Cli.test.ts
ADDED
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
import { Cli, Errors, z } from 'incur'
|
|
2
|
+
|
|
3
|
+
let __mockSkillsHash: string | undefined
|
|
4
|
+
|
|
5
|
+
vi.mock('./SyncSkills.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('./SyncSkills.js')>()
|
|
7
|
+
return { ...actual, readHash: () => __mockSkillsHash }
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
async function serve(
|
|
11
|
+
cli: { serve: Cli.Cli['serve'] },
|
|
12
|
+
argv: string[],
|
|
13
|
+
options: Cli.serve.Options = {},
|
|
14
|
+
) {
|
|
15
|
+
let output = ''
|
|
16
|
+
let exitCode: number | undefined
|
|
17
|
+
await cli.serve(argv, {
|
|
18
|
+
stdout(s) {
|
|
19
|
+
output += s
|
|
20
|
+
},
|
|
21
|
+
exit(code) {
|
|
22
|
+
exitCode = code
|
|
23
|
+
},
|
|
24
|
+
...options,
|
|
25
|
+
})
|
|
26
|
+
return {
|
|
27
|
+
output: output.replace(/duration: \d+ms/, 'duration: <stripped>'),
|
|
28
|
+
exitCode,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('create', () => {
|
|
33
|
+
test('returns cli instance with name', () => {
|
|
34
|
+
const cli = Cli.create('test')
|
|
35
|
+
expect(cli.name).toBe('test')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('accepts version and description options', () => {
|
|
39
|
+
const cli = Cli.create('test', { version: '1.0.0', description: 'A test CLI' })
|
|
40
|
+
expect(cli.name).toBe('test')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('command', () => {
|
|
45
|
+
test('registers a command and is chainable', () => {
|
|
46
|
+
const cli = Cli.create('test')
|
|
47
|
+
const result = cli.command('greet', {
|
|
48
|
+
args: z.object({ name: z.string() }),
|
|
49
|
+
run({ args }) {
|
|
50
|
+
return { message: `hello ${args.name}` }
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
expect(result).toBe(cli)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('serve', () => {
|
|
58
|
+
test('outputs data only by default', async () => {
|
|
59
|
+
const cli = Cli.create('test')
|
|
60
|
+
cli.command('greet', {
|
|
61
|
+
args: z.object({ name: z.string() }),
|
|
62
|
+
run({ args }) {
|
|
63
|
+
return { message: `hello ${args.name}` }
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const { output } = await serve(cli, ['greet', 'world'])
|
|
68
|
+
expect(output).toMatchInlineSnapshot(`
|
|
69
|
+
"message: hello world
|
|
70
|
+
"
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('--verbose outputs full envelope', async () => {
|
|
75
|
+
const cli = Cli.create('test')
|
|
76
|
+
cli.command('greet', {
|
|
77
|
+
args: z.object({ name: z.string() }),
|
|
78
|
+
run({ args }) {
|
|
79
|
+
return { message: `hello ${args.name}` }
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const { output } = await serve(cli, ['greet', 'world', '--verbose'])
|
|
84
|
+
expect(output).toMatchInlineSnapshot(`
|
|
85
|
+
"ok: true
|
|
86
|
+
data:
|
|
87
|
+
message: hello world
|
|
88
|
+
meta:
|
|
89
|
+
command: greet
|
|
90
|
+
duration: <stripped>
|
|
91
|
+
"
|
|
92
|
+
`)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('parses positional args by schema key order', async () => {
|
|
96
|
+
const cli = Cli.create('test')
|
|
97
|
+
let receivedArgs: any
|
|
98
|
+
cli.command('add', {
|
|
99
|
+
args: z.object({ a: z.string(), b: z.string() }),
|
|
100
|
+
run({ args }) {
|
|
101
|
+
receivedArgs = args
|
|
102
|
+
return {}
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
await serve(cli, ['add', 'foo', 'bar'])
|
|
107
|
+
expect(receivedArgs).toEqual({ a: 'foo', b: 'bar' })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('serializes output as TOON', async () => {
|
|
111
|
+
const cli = Cli.create('test')
|
|
112
|
+
cli.command('ping', {
|
|
113
|
+
run() {
|
|
114
|
+
return { pong: true }
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const { output } = await serve(cli, ['ping'])
|
|
119
|
+
expect(() => JSON.parse(output)).toThrow()
|
|
120
|
+
expect(output).toMatchInlineSnapshot(`
|
|
121
|
+
"pong: true
|
|
122
|
+
"
|
|
123
|
+
`)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('outputs error details for unknown command', async () => {
|
|
127
|
+
const cli = Cli.create('test')
|
|
128
|
+
|
|
129
|
+
const { output, exitCode } = await serve(cli, ['nonexistent'])
|
|
130
|
+
expect(exitCode).toBe(1)
|
|
131
|
+
expect(output).toMatchInlineSnapshot(`
|
|
132
|
+
"Error: 'nonexistent' is not a command. See 'test --help' for a list of available commands.
|
|
133
|
+
"
|
|
134
|
+
`)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('--verbose outputs full error envelope for unknown command', async () => {
|
|
138
|
+
const cli = Cli.create('test')
|
|
139
|
+
|
|
140
|
+
const { output, exitCode } = await serve(cli, ['nonexistent', '--verbose'])
|
|
141
|
+
expect(exitCode).toBe(1)
|
|
142
|
+
expect(output).toMatchInlineSnapshot(`
|
|
143
|
+
"ok: false
|
|
144
|
+
error:
|
|
145
|
+
code: COMMAND_NOT_FOUND
|
|
146
|
+
message: 'nonexistent' is not a command. See 'test --help' for a list of available commands.
|
|
147
|
+
meta:
|
|
148
|
+
command: nonexistent
|
|
149
|
+
duration: <stripped>
|
|
150
|
+
"
|
|
151
|
+
`)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('wraps handler errors in error output', async () => {
|
|
155
|
+
const cli = Cli.create('test')
|
|
156
|
+
cli.command('fail', {
|
|
157
|
+
run() {
|
|
158
|
+
throw new Error('boom')
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
163
|
+
expect(exitCode).toBe(1)
|
|
164
|
+
expect(output).toMatchInlineSnapshot(`
|
|
165
|
+
"Error: boom
|
|
166
|
+
"
|
|
167
|
+
`)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('IncurError in run() populates code/retryable', async () => {
|
|
171
|
+
const cli = Cli.create('test')
|
|
172
|
+
cli.command('fail', {
|
|
173
|
+
run() {
|
|
174
|
+
throw new Errors.IncurError({
|
|
175
|
+
code: 'NOT_AUTHENTICATED',
|
|
176
|
+
message: 'Token not found',
|
|
177
|
+
retryable: false,
|
|
178
|
+
})
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
183
|
+
expect(exitCode).toBe(1)
|
|
184
|
+
expect(output).toMatchInlineSnapshot(`
|
|
185
|
+
"Error (NOT_AUTHENTICATED): Token not found
|
|
186
|
+
"
|
|
187
|
+
`)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('ValidationError includes fieldErrors', async () => {
|
|
191
|
+
const cli = Cli.create('test')
|
|
192
|
+
cli.command('greet', {
|
|
193
|
+
args: z.object({ name: z.string() }),
|
|
194
|
+
run({ args }) {
|
|
195
|
+
return { message: `hello ${args.name}` }
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const { output, exitCode } = await serve(cli, ['greet'])
|
|
200
|
+
expect(exitCode).toBe(1)
|
|
201
|
+
expect(output).toContain('Error: missing required argument <name>')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('supports async handlers', async () => {
|
|
205
|
+
const cli = Cli.create('test')
|
|
206
|
+
cli.command('async', {
|
|
207
|
+
async run() {
|
|
208
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
209
|
+
return { done: true }
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const { output } = await serve(cli, ['async'])
|
|
214
|
+
expect(output).toMatchInlineSnapshot(`
|
|
215
|
+
"done: true
|
|
216
|
+
"
|
|
217
|
+
`)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('--format json outputs JSON data', async () => {
|
|
221
|
+
const cli = Cli.create('test')
|
|
222
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
223
|
+
const { output } = await serve(cli, ['ping', '--format', 'json'])
|
|
224
|
+
expect(JSON.parse(output)).toEqual({ pong: true })
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('--json is shorthand for --format json', async () => {
|
|
228
|
+
const cli = Cli.create('test')
|
|
229
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
230
|
+
const { output } = await serve(cli, ['ping', '--json'])
|
|
231
|
+
expect(JSON.parse(output)).toEqual({ pong: true })
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('--verbose --format json outputs full envelope as JSON', async () => {
|
|
235
|
+
const cli = Cli.create('test')
|
|
236
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
237
|
+
const { output } = await serve(cli, ['ping', '--verbose', '--format', 'json'])
|
|
238
|
+
const parsed = JSON.parse(output)
|
|
239
|
+
expect(parsed.ok).toBe(true)
|
|
240
|
+
expect(parsed.data).toEqual({ pong: true })
|
|
241
|
+
expect(parsed.meta.command).toBe('ping')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('error output respects --format json', async () => {
|
|
245
|
+
const cli = Cli.create('test')
|
|
246
|
+
cli.command('fail', {
|
|
247
|
+
run() {
|
|
248
|
+
throw new Error('boom')
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
const { output, exitCode } = await serve(cli, ['fail', '--format', 'json'])
|
|
252
|
+
expect(exitCode).toBe(1)
|
|
253
|
+
const parsed = JSON.parse(output)
|
|
254
|
+
expect(parsed.code).toBe('UNKNOWN')
|
|
255
|
+
expect(parsed.message).toBe('boom')
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('--llms', () => {
|
|
260
|
+
test('outputs manifest with version and commands', async () => {
|
|
261
|
+
const cli = Cli.create('test')
|
|
262
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
263
|
+
|
|
264
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
265
|
+
const manifest = JSON.parse(output)
|
|
266
|
+
expect(manifest.version).toBe('incur.v1')
|
|
267
|
+
expect(manifest.commands).toHaveLength(1)
|
|
268
|
+
expect(manifest.commands[0].name).toBe('ping')
|
|
269
|
+
expect(manifest.commands[0].description).toBe('Health check')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('manifest includes schema.input from args and options', async () => {
|
|
273
|
+
const cli = Cli.create('test')
|
|
274
|
+
cli.command('greet', {
|
|
275
|
+
args: z.object({ name: z.string() }),
|
|
276
|
+
options: z.object({ loud: z.boolean().default(false) }),
|
|
277
|
+
run: ({ args }) => ({ message: `hello ${args.name}` }),
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
281
|
+
const manifest = JSON.parse(output)
|
|
282
|
+
expect(manifest.commands[0].schema.args).toEqual({
|
|
283
|
+
type: 'object',
|
|
284
|
+
properties: { name: { type: 'string' } },
|
|
285
|
+
required: ['name'],
|
|
286
|
+
additionalProperties: false,
|
|
287
|
+
})
|
|
288
|
+
expect(manifest.commands[0].schema.options).toEqual({
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: { loud: { type: 'boolean', default: false } },
|
|
291
|
+
required: ['loud'],
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('manifest includes schema.output when defined', async () => {
|
|
297
|
+
const cli = Cli.create('test')
|
|
298
|
+
cli.command('greet', {
|
|
299
|
+
args: z.object({ name: z.string() }),
|
|
300
|
+
output: z.object({ message: z.string() }),
|
|
301
|
+
run: ({ args }) => ({ message: `hello ${args.name}` }),
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
305
|
+
const manifest = JSON.parse(output)
|
|
306
|
+
expect(manifest.commands[0].schema.output).toEqual({
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: { message: { type: 'string' } },
|
|
309
|
+
required: ['message'],
|
|
310
|
+
additionalProperties: false,
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('manifest omits schema when no schemas defined', async () => {
|
|
315
|
+
const cli = Cli.create('test')
|
|
316
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
317
|
+
|
|
318
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
319
|
+
const manifest = JSON.parse(output)
|
|
320
|
+
expect(manifest.commands[0].schema).toBeUndefined()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('nested commands appear with full path in manifest', async () => {
|
|
324
|
+
const cli = Cli.create('test')
|
|
325
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
326
|
+
.command('list', {
|
|
327
|
+
description: 'List PRs',
|
|
328
|
+
options: z.object({ state: z.enum(['open', 'closed']).default('open') }),
|
|
329
|
+
run: () => ({ items: [] }),
|
|
330
|
+
})
|
|
331
|
+
.command('create', {
|
|
332
|
+
description: 'Create PR',
|
|
333
|
+
args: z.object({ title: z.string() }),
|
|
334
|
+
run: ({ args }) => ({ title: args.title }),
|
|
335
|
+
})
|
|
336
|
+
cli.command(pr)
|
|
337
|
+
|
|
338
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
339
|
+
const manifest = JSON.parse(output)
|
|
340
|
+
expect(manifest.commands).toHaveLength(2)
|
|
341
|
+
expect(manifest.commands[0].name).toBe('pr create')
|
|
342
|
+
expect(manifest.commands[1].name).toBe('pr list')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('deeply nested commands in manifest', async () => {
|
|
346
|
+
const cli = Cli.create('test')
|
|
347
|
+
const review = Cli.create('review', { description: 'Reviews' }).command('approve', {
|
|
348
|
+
description: 'Approve a review',
|
|
349
|
+
run: () => ({ approved: true }),
|
|
350
|
+
})
|
|
351
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
352
|
+
pr.command(review)
|
|
353
|
+
cli.command(pr)
|
|
354
|
+
|
|
355
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
356
|
+
const manifest = JSON.parse(output)
|
|
357
|
+
expect(manifest.commands[0].name).toBe('pr review approve')
|
|
358
|
+
expect(manifest.commands[0].description).toBe('Approve a review')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('defaults to markdown format', async () => {
|
|
362
|
+
const cli = Cli.create('test')
|
|
363
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
364
|
+
|
|
365
|
+
const { output } = await serve(cli, ['--llms'])
|
|
366
|
+
expect(output).toContain('# test ping')
|
|
367
|
+
expect(output).toContain('Health check')
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('respects --format yaml', async () => {
|
|
371
|
+
const cli = Cli.create('test')
|
|
372
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
373
|
+
|
|
374
|
+
const { output } = await serve(cli, ['--llms', '--format', 'yaml'])
|
|
375
|
+
expect(output).toContain('version: incur.v1')
|
|
376
|
+
expect(output).toContain('name: ping')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('full manifest snapshot', async () => {
|
|
380
|
+
const cli = Cli.create('test')
|
|
381
|
+
cli.command('greet', {
|
|
382
|
+
description: 'Greet someone',
|
|
383
|
+
args: z.object({ name: z.string().describe('Name to greet') }),
|
|
384
|
+
options: z.object({ loud: z.boolean().default(false).describe('Shout it') }),
|
|
385
|
+
output: z.object({ message: z.string() }),
|
|
386
|
+
run: ({ args }) => ({ message: `hello ${args.name}` }),
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
390
|
+
expect(JSON.parse(output)).toMatchInlineSnapshot(`
|
|
391
|
+
{
|
|
392
|
+
"commands": [
|
|
393
|
+
{
|
|
394
|
+
"description": "Greet someone",
|
|
395
|
+
"name": "greet",
|
|
396
|
+
"schema": {
|
|
397
|
+
"args": {
|
|
398
|
+
"additionalProperties": false,
|
|
399
|
+
"properties": {
|
|
400
|
+
"name": {
|
|
401
|
+
"description": "Name to greet",
|
|
402
|
+
"type": "string",
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
"required": [
|
|
406
|
+
"name",
|
|
407
|
+
],
|
|
408
|
+
"type": "object",
|
|
409
|
+
},
|
|
410
|
+
"options": {
|
|
411
|
+
"additionalProperties": false,
|
|
412
|
+
"properties": {
|
|
413
|
+
"loud": {
|
|
414
|
+
"default": false,
|
|
415
|
+
"description": "Shout it",
|
|
416
|
+
"type": "boolean",
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
"required": [
|
|
420
|
+
"loud",
|
|
421
|
+
],
|
|
422
|
+
"type": "object",
|
|
423
|
+
},
|
|
424
|
+
"output": {
|
|
425
|
+
"additionalProperties": false,
|
|
426
|
+
"properties": {
|
|
427
|
+
"message": {
|
|
428
|
+
"type": "string",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
"required": [
|
|
432
|
+
"message",
|
|
433
|
+
],
|
|
434
|
+
"type": "object",
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
"version": "incur.v1",
|
|
440
|
+
}
|
|
441
|
+
`)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('--llms --format md outputs skill files', async () => {
|
|
445
|
+
const cli = Cli.create('test')
|
|
446
|
+
cli.command('greet', {
|
|
447
|
+
description: 'Greet someone',
|
|
448
|
+
args: z.object({ name: z.string().describe('Name to greet') }),
|
|
449
|
+
output: z.object({ message: z.string() }),
|
|
450
|
+
run: ({ args }) => ({ message: `hello ${args.name}` }),
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const { output } = await serve(cli, ['--llms', '--format', 'md'])
|
|
454
|
+
expect(output).toContain('# test greet')
|
|
455
|
+
expect(output).toContain('## Arguments')
|
|
456
|
+
expect(output).toContain('## Output')
|
|
457
|
+
expect(output).not.toMatch(/^---$/m)
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
describe('subcommands', () => {
|
|
462
|
+
test('creates a command group with name and description', () => {
|
|
463
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
464
|
+
expect(pr.name).toBe('pr')
|
|
465
|
+
expect(pr.description).toBe('PR management')
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('group registers sub-commands and is chainable', () => {
|
|
469
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
470
|
+
const result = pr.command('list', { run: () => ({ count: 0 }) })
|
|
471
|
+
expect(result).toBe(pr)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
test('routes to sub-command', async () => {
|
|
475
|
+
const cli = Cli.create('test')
|
|
476
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
477
|
+
run: () => ({ count: 0 }),
|
|
478
|
+
})
|
|
479
|
+
cli.command(pr)
|
|
480
|
+
|
|
481
|
+
const { output } = await serve(cli, ['pr', 'list'])
|
|
482
|
+
expect(output).toMatchInlineSnapshot(`
|
|
483
|
+
"count: 0
|
|
484
|
+
"
|
|
485
|
+
`)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('sub-command receives parsed args and options', async () => {
|
|
489
|
+
const cli = Cli.create('test')
|
|
490
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('get', {
|
|
491
|
+
args: z.object({ id: z.string() }),
|
|
492
|
+
options: z.object({ draft: z.boolean().default(false) }),
|
|
493
|
+
run: ({ args, options }) => ({ id: args.id, draft: options.draft }),
|
|
494
|
+
})
|
|
495
|
+
cli.command(pr)
|
|
496
|
+
|
|
497
|
+
const { output } = await serve(cli, ['pr', 'get', '42', '--draft'])
|
|
498
|
+
expect(output).toMatchInlineSnapshot(`
|
|
499
|
+
"id: "42"
|
|
500
|
+
draft: true
|
|
501
|
+
"
|
|
502
|
+
`)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test('--verbose shows full command path in meta', async () => {
|
|
506
|
+
const cli = Cli.create('test')
|
|
507
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
508
|
+
run: () => ({ count: 0 }),
|
|
509
|
+
})
|
|
510
|
+
cli.command(pr)
|
|
511
|
+
|
|
512
|
+
const { output } = await serve(cli, ['pr', 'list', '--verbose'])
|
|
513
|
+
expect(output).toMatchInlineSnapshot(`
|
|
514
|
+
"ok: true
|
|
515
|
+
data:
|
|
516
|
+
count: 0
|
|
517
|
+
meta:
|
|
518
|
+
command: pr list
|
|
519
|
+
duration: <stripped>
|
|
520
|
+
"
|
|
521
|
+
`)
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
test('routes to deeply nested sub-commands', async () => {
|
|
525
|
+
const cli = Cli.create('test')
|
|
526
|
+
const review = Cli.create('review', { description: 'Reviews' }).command('approve', {
|
|
527
|
+
run: () => ({ approved: true }),
|
|
528
|
+
})
|
|
529
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
530
|
+
pr.command(review)
|
|
531
|
+
cli.command(pr)
|
|
532
|
+
|
|
533
|
+
const { output } = await serve(cli, ['pr', 'review', 'approve'])
|
|
534
|
+
expect(output).toMatchInlineSnapshot(`
|
|
535
|
+
"approved: true
|
|
536
|
+
"
|
|
537
|
+
`)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
test('nested group shows full path in verbose meta', async () => {
|
|
541
|
+
const cli = Cli.create('test')
|
|
542
|
+
const review = Cli.create('review', { description: 'Reviews' }).command('approve', {
|
|
543
|
+
run: () => ({ approved: true }),
|
|
544
|
+
})
|
|
545
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
546
|
+
pr.command(review)
|
|
547
|
+
cli.command(pr)
|
|
548
|
+
|
|
549
|
+
const { output } = await serve(cli, ['pr', 'review', 'approve', '--verbose'])
|
|
550
|
+
expect(output).toMatchInlineSnapshot(`
|
|
551
|
+
"ok: true
|
|
552
|
+
data:
|
|
553
|
+
approved: true
|
|
554
|
+
meta:
|
|
555
|
+
command: pr review approve
|
|
556
|
+
duration: <stripped>
|
|
557
|
+
"
|
|
558
|
+
`)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('unknown subcommand lists available commands', async () => {
|
|
562
|
+
const cli = Cli.create('test')
|
|
563
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
564
|
+
.command('list', { run: () => ({}) })
|
|
565
|
+
.command('create', { run: () => ({}) })
|
|
566
|
+
cli.command(pr)
|
|
567
|
+
|
|
568
|
+
const { output, exitCode } = await serve(cli, ['pr', 'unknown'])
|
|
569
|
+
expect(exitCode).toBe(1)
|
|
570
|
+
expect(output).toMatchInlineSnapshot(`
|
|
571
|
+
"Error: 'unknown' is not a command. See 'test pr --help' for a list of available commands.
|
|
572
|
+
"
|
|
573
|
+
`)
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test('group without subcommand shows help', async () => {
|
|
577
|
+
const cli = Cli.create('test')
|
|
578
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
579
|
+
.command('list', { run: () => ({}) })
|
|
580
|
+
.command('create', { run: () => ({}) })
|
|
581
|
+
cli.command(pr)
|
|
582
|
+
|
|
583
|
+
const { output, exitCode } = await serve(cli, ['pr'])
|
|
584
|
+
expect(exitCode).toBeUndefined()
|
|
585
|
+
expect(output).toMatchInlineSnapshot(`
|
|
586
|
+
"test pr — PR management
|
|
587
|
+
|
|
588
|
+
Usage: test pr <command>
|
|
589
|
+
|
|
590
|
+
Commands:
|
|
591
|
+
create
|
|
592
|
+
list
|
|
593
|
+
|
|
594
|
+
Global Options:
|
|
595
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
596
|
+
--help Show help
|
|
597
|
+
--llms Print LLM-readable manifest
|
|
598
|
+
--verbose Show full output envelope
|
|
599
|
+
"
|
|
600
|
+
`)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
test('sub-commands from separate module can be mounted', async () => {
|
|
604
|
+
function createPrCommands() {
|
|
605
|
+
return Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
606
|
+
run: () => ({ count: 0 }),
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const cli = Cli.create('test')
|
|
611
|
+
cli.command(createPrCommands())
|
|
612
|
+
|
|
613
|
+
const { output } = await serve(cli, ['pr', 'list'])
|
|
614
|
+
expect(output).toMatchInlineSnapshot(`
|
|
615
|
+
"count: 0
|
|
616
|
+
"
|
|
617
|
+
`)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
test('error in sub-command wraps in error envelope', async () => {
|
|
621
|
+
const cli = Cli.create('test')
|
|
622
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('fail', {
|
|
623
|
+
run() {
|
|
624
|
+
throw new Error('sub-boom')
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
cli.command(pr)
|
|
628
|
+
|
|
629
|
+
const { output, exitCode } = await serve(cli, ['pr', 'fail'])
|
|
630
|
+
expect(exitCode).toBe(1)
|
|
631
|
+
expect(output).toMatchInlineSnapshot(`
|
|
632
|
+
"Error: sub-boom
|
|
633
|
+
"
|
|
634
|
+
`)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
test('group error respects --format json', async () => {
|
|
638
|
+
const cli = Cli.create('test')
|
|
639
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
640
|
+
run: () => ({}),
|
|
641
|
+
})
|
|
642
|
+
cli.command(pr)
|
|
643
|
+
|
|
644
|
+
const { output, exitCode } = await serve(cli, ['pr', 'unknown', '--format', 'json'])
|
|
645
|
+
expect(exitCode).toBe(1)
|
|
646
|
+
const parsed = JSON.parse(output)
|
|
647
|
+
expect(parsed.code).toBe('COMMAND_NOT_FOUND')
|
|
648
|
+
expect(parsed.message).toContain('unknown')
|
|
649
|
+
})
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
describe('cta', () => {
|
|
653
|
+
test('string shorthand for cta commands', async () => {
|
|
654
|
+
const cli = Cli.create('test')
|
|
655
|
+
cli.command('list', {
|
|
656
|
+
run({ ok }) {
|
|
657
|
+
return ok({ items: [] }, { cta: { commands: ['get 1', 'get 2'] } })
|
|
658
|
+
},
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
const { output } = await serve(cli, ['list', '--verbose', '--format', 'json'])
|
|
662
|
+
const parsed = JSON.parse(output)
|
|
663
|
+
expect(parsed.meta.cta).toEqual({
|
|
664
|
+
description: 'Suggested commands:',
|
|
665
|
+
commands: [{ command: 'test get 1' }, { command: 'test get 2' }],
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test('tuple shorthand with description', async () => {
|
|
670
|
+
const cli = Cli.create('test')
|
|
671
|
+
cli.command('list', {
|
|
672
|
+
run({ ok }) {
|
|
673
|
+
return ok(
|
|
674
|
+
{ items: [] },
|
|
675
|
+
{
|
|
676
|
+
cta: { commands: [{ command: 'get 1', description: 'View item 1' }] },
|
|
677
|
+
},
|
|
678
|
+
)
|
|
679
|
+
},
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
const { output } = await serve(cli, ['list', '--verbose', '--format', 'json'])
|
|
683
|
+
const parsed = JSON.parse(output)
|
|
684
|
+
expect(parsed.meta.cta.commands).toEqual([
|
|
685
|
+
{ command: 'test get 1', description: 'View item 1' },
|
|
686
|
+
])
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
test('tuple form with args/options', async () => {
|
|
690
|
+
const cli = Cli.create('test')
|
|
691
|
+
cli.command('create', {
|
|
692
|
+
run({ ok }) {
|
|
693
|
+
return ok(
|
|
694
|
+
{ id: 1 },
|
|
695
|
+
{
|
|
696
|
+
cta: {
|
|
697
|
+
commands: [
|
|
698
|
+
{
|
|
699
|
+
command: 'get',
|
|
700
|
+
args: { id: 1 },
|
|
701
|
+
options: { limit: 10 },
|
|
702
|
+
description: 'View the item',
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
)
|
|
708
|
+
},
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
const { output } = await serve(cli, ['create', '--verbose', '--format', 'json'])
|
|
712
|
+
const parsed = JSON.parse(output)
|
|
713
|
+
expect(parsed.meta.cta.commands).toEqual([
|
|
714
|
+
{ command: 'test get 1 --limit 10', description: 'View the item' },
|
|
715
|
+
])
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
test('tuple form boolean args format as placeholders', async () => {
|
|
719
|
+
const cli = Cli.create('test')
|
|
720
|
+
cli.command('list', {
|
|
721
|
+
run({ ok }) {
|
|
722
|
+
return ok(
|
|
723
|
+
{ items: [] },
|
|
724
|
+
{
|
|
725
|
+
cta: { commands: [{ command: 'get', args: { id: true }, options: { format: true } }] },
|
|
726
|
+
},
|
|
727
|
+
)
|
|
728
|
+
},
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
const { output } = await serve(cli, ['list', '--verbose', '--format', 'json'])
|
|
732
|
+
const parsed = JSON.parse(output)
|
|
733
|
+
expect(parsed.meta.cta.commands).toEqual([{ command: 'test get <id> --format <format>' }])
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
test('custom cta description', async () => {
|
|
737
|
+
const cli = Cli.create('test')
|
|
738
|
+
cli.command('create', {
|
|
739
|
+
run({ ok }) {
|
|
740
|
+
return ok(
|
|
741
|
+
{ id: 1 },
|
|
742
|
+
{
|
|
743
|
+
cta: { description: 'View the created item:', commands: ['get 1'] },
|
|
744
|
+
},
|
|
745
|
+
)
|
|
746
|
+
},
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
const { output } = await serve(cli, ['create', '--verbose', '--format', 'json'])
|
|
750
|
+
const parsed = JSON.parse(output)
|
|
751
|
+
expect(parsed.meta.cta.description).toBe('View the created item:')
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test('plain return omits meta.cta', async () => {
|
|
755
|
+
const cli = Cli.create('test')
|
|
756
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
757
|
+
|
|
758
|
+
const { output } = await serve(cli, ['ping', '--verbose', '--format', 'json'])
|
|
759
|
+
const parsed = JSON.parse(output)
|
|
760
|
+
expect(parsed.meta.cta).toBeUndefined()
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
test('empty commands array omits meta.cta', async () => {
|
|
764
|
+
const cli = Cli.create('test')
|
|
765
|
+
cli.command('noop', {
|
|
766
|
+
run({ ok }) {
|
|
767
|
+
return ok({ done: true }, { cta: { commands: [] } })
|
|
768
|
+
},
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
const { output } = await serve(cli, ['noop', '--verbose', '--format', 'json'])
|
|
772
|
+
const parsed = JSON.parse(output)
|
|
773
|
+
expect(parsed.meta.cta).toBeUndefined()
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
test('error() with cta', async () => {
|
|
777
|
+
const cli = Cli.create('test')
|
|
778
|
+
cli.command('fail', {
|
|
779
|
+
run({ error }) {
|
|
780
|
+
return error({
|
|
781
|
+
code: 'NOT_AUTHENTICATED',
|
|
782
|
+
message: 'Not logged in',
|
|
783
|
+
cta: {
|
|
784
|
+
description: 'Authenticate to continue:',
|
|
785
|
+
commands: ['auth login'],
|
|
786
|
+
},
|
|
787
|
+
})
|
|
788
|
+
},
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const { output, exitCode } = await serve(cli, ['fail', '--verbose', '--format', 'json'])
|
|
792
|
+
expect(exitCode).toBe(1)
|
|
793
|
+
const parsed = JSON.parse(output)
|
|
794
|
+
expect(parsed.ok).toBe(false)
|
|
795
|
+
expect(parsed.meta.cta).toEqual({
|
|
796
|
+
description: 'Authenticate to continue:',
|
|
797
|
+
commands: [{ command: 'test auth login' }],
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
test('error() without cta omits meta.cta', async () => {
|
|
802
|
+
const cli = Cli.create('test')
|
|
803
|
+
cli.command('fail', {
|
|
804
|
+
run({ error }) {
|
|
805
|
+
return error({ code: 'FAILED', message: 'Something went wrong' })
|
|
806
|
+
},
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
const { output, exitCode } = await serve(cli, ['fail', '--verbose', '--format', 'json'])
|
|
810
|
+
expect(exitCode).toBe(1)
|
|
811
|
+
const parsed = JSON.parse(output)
|
|
812
|
+
expect(parsed.meta.cta).toBeUndefined()
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
test('thrown error does not include cta', async () => {
|
|
816
|
+
const cli = Cli.create('test')
|
|
817
|
+
cli.command('fail', {
|
|
818
|
+
run() {
|
|
819
|
+
throw new Error('boom')
|
|
820
|
+
},
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
const { output } = await serve(cli, ['fail', '--verbose', '--format', 'json'])
|
|
824
|
+
const parsed = JSON.parse(output)
|
|
825
|
+
expect(parsed.ok).toBe(false)
|
|
826
|
+
expect(parsed.meta.cta).toBeUndefined()
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
test('ok() cta works with sub-commands', async () => {
|
|
830
|
+
const cli = Cli.create('test')
|
|
831
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('create', {
|
|
832
|
+
args: z.object({ title: z.string() }),
|
|
833
|
+
output: z.object({ id: z.number(), title: z.string() }),
|
|
834
|
+
run({ args, ok }) {
|
|
835
|
+
return ok(
|
|
836
|
+
{ id: 42, title: args.title },
|
|
837
|
+
{
|
|
838
|
+
cta: { commands: [{ command: 'pr get 42', description: 'View the PR' }] },
|
|
839
|
+
},
|
|
840
|
+
)
|
|
841
|
+
},
|
|
842
|
+
})
|
|
843
|
+
cli.command(pr)
|
|
844
|
+
|
|
845
|
+
const { output } = await serve(cli, ['pr', 'create', 'my-pr', '--verbose', '--format', 'json'])
|
|
846
|
+
const parsed = JSON.parse(output)
|
|
847
|
+
expect(parsed.meta.cta).toEqual({
|
|
848
|
+
description: 'Suggested commands:',
|
|
849
|
+
commands: [{ command: 'test pr get 42', description: 'View the PR' }],
|
|
850
|
+
})
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
describe('leaf cli', () => {
|
|
855
|
+
test('create with run returns a leaf cli (no command method)', () => {
|
|
856
|
+
const cli = Cli.create('ping', { run: () => ({ pong: true }) })
|
|
857
|
+
expect(cli.name).toBe('ping')
|
|
858
|
+
expect('command' in cli).toBe(false)
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
test('serves without a command name in argv', async () => {
|
|
862
|
+
const cli = Cli.create('ping', { run: () => ({ pong: true }) })
|
|
863
|
+
const { output } = await serve(cli, [])
|
|
864
|
+
expect(output).toMatchInlineSnapshot(`
|
|
865
|
+
"pong: true
|
|
866
|
+
"
|
|
867
|
+
`)
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
test('parses args and options', async () => {
|
|
871
|
+
const cli = Cli.create('greet', {
|
|
872
|
+
args: z.object({ name: z.string() }),
|
|
873
|
+
options: z.object({ loud: z.boolean().default(false) }),
|
|
874
|
+
run({ args, options }) {
|
|
875
|
+
return { message: options.loud ? `HELLO ${args.name}` : `hello ${args.name}` }
|
|
876
|
+
},
|
|
877
|
+
})
|
|
878
|
+
const { output } = await serve(cli, ['world', '--loud'])
|
|
879
|
+
expect(output).toMatchInlineSnapshot(`
|
|
880
|
+
"message: HELLO world
|
|
881
|
+
"
|
|
882
|
+
`)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
test('--verbose outputs full envelope', async () => {
|
|
886
|
+
const cli = Cli.create('ping', { run: () => ({ pong: true }) })
|
|
887
|
+
const { output } = await serve(cli, ['--verbose'])
|
|
888
|
+
expect(output).toMatchInlineSnapshot(`
|
|
889
|
+
"ok: true
|
|
890
|
+
data:
|
|
891
|
+
pong: true
|
|
892
|
+
meta:
|
|
893
|
+
command: ping
|
|
894
|
+
duration: <stripped>
|
|
895
|
+
"
|
|
896
|
+
`)
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
test('--format json works', async () => {
|
|
900
|
+
const cli = Cli.create('ping', { run: () => ({ pong: true }) })
|
|
901
|
+
const { output } = await serve(cli, ['--format', 'json'])
|
|
902
|
+
expect(JSON.parse(output)).toEqual({ pong: true })
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
test('errors wrap in error envelope', async () => {
|
|
906
|
+
const cli = Cli.create('fail', {
|
|
907
|
+
run() {
|
|
908
|
+
throw new Error('boom')
|
|
909
|
+
},
|
|
910
|
+
})
|
|
911
|
+
const { output, exitCode } = await serve(cli, [])
|
|
912
|
+
expect(exitCode).toBe(1)
|
|
913
|
+
expect(output).toMatchInlineSnapshot(`
|
|
914
|
+
"Error: boom
|
|
915
|
+
"
|
|
916
|
+
`)
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
test('can be mounted on a parent as a single command', async () => {
|
|
920
|
+
const ping = Cli.create('ping', {
|
|
921
|
+
description: 'Health check',
|
|
922
|
+
run: () => ({ pong: true }),
|
|
923
|
+
})
|
|
924
|
+
const cli = Cli.create('app')
|
|
925
|
+
cli.command(ping)
|
|
926
|
+
|
|
927
|
+
const { output } = await serve(cli, ['ping'])
|
|
928
|
+
expect(output).toMatchInlineSnapshot(`
|
|
929
|
+
"pong: true
|
|
930
|
+
"
|
|
931
|
+
`)
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
test('mounted leaf with args/options works', async () => {
|
|
935
|
+
const greet = Cli.create('greet', {
|
|
936
|
+
args: z.object({ name: z.string() }),
|
|
937
|
+
options: z.object({ loud: z.boolean().default(false) }),
|
|
938
|
+
run({ args, options }) {
|
|
939
|
+
return { message: options.loud ? `HELLO ${args.name}` : `hello ${args.name}` }
|
|
940
|
+
},
|
|
941
|
+
})
|
|
942
|
+
const cli = Cli.create('app')
|
|
943
|
+
cli.command(greet)
|
|
944
|
+
|
|
945
|
+
const { output } = await serve(cli, ['greet', 'world', '--loud'])
|
|
946
|
+
expect(output).toMatchInlineSnapshot(`
|
|
947
|
+
"message: HELLO world
|
|
948
|
+
"
|
|
949
|
+
`)
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
test('mounted leaf appears in --llms manifest', async () => {
|
|
953
|
+
const ping = Cli.create('ping', {
|
|
954
|
+
description: 'Health check',
|
|
955
|
+
run: () => ({ pong: true }),
|
|
956
|
+
})
|
|
957
|
+
const cli = Cli.create('app')
|
|
958
|
+
cli.command(ping)
|
|
959
|
+
|
|
960
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
961
|
+
const manifest = JSON.parse(output)
|
|
962
|
+
expect(manifest.commands).toHaveLength(1)
|
|
963
|
+
expect(manifest.commands[0].name).toBe('ping')
|
|
964
|
+
expect(manifest.commands[0].description).toBe('Health check')
|
|
965
|
+
})
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
describe('help', () => {
|
|
969
|
+
test('router with no subcommand shows help', async () => {
|
|
970
|
+
const cli = Cli.create('tool')
|
|
971
|
+
cli.command('ping', {
|
|
972
|
+
description: 'Health check',
|
|
973
|
+
run: () => ({ pong: true }),
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
const { output, exitCode } = await serve(cli, [])
|
|
977
|
+
expect(exitCode).toBeUndefined()
|
|
978
|
+
expect(output).toMatchInlineSnapshot(`
|
|
979
|
+
"tool
|
|
980
|
+
|
|
981
|
+
Usage: tool <command>
|
|
982
|
+
|
|
983
|
+
Commands:
|
|
984
|
+
ping Health check
|
|
985
|
+
|
|
986
|
+
Built-in Commands:
|
|
987
|
+
mcp add Register as an MCP server
|
|
988
|
+
skills add Sync skill files to your agent
|
|
989
|
+
|
|
990
|
+
Global Options:
|
|
991
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
992
|
+
--help Show help
|
|
993
|
+
--llms Print LLM-readable manifest
|
|
994
|
+
--mcp Start as MCP stdio server
|
|
995
|
+
--verbose Show full output envelope
|
|
996
|
+
--version Show version
|
|
997
|
+
"
|
|
998
|
+
`)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
test('--help on root shows help', async () => {
|
|
1002
|
+
const cli = Cli.create('tool')
|
|
1003
|
+
cli.command('ping', {
|
|
1004
|
+
description: 'Health check',
|
|
1005
|
+
run: () => ({ pong: true }),
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
const { output, exitCode } = await serve(cli, ['--help'])
|
|
1009
|
+
expect(exitCode).toBeUndefined()
|
|
1010
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1011
|
+
"tool
|
|
1012
|
+
|
|
1013
|
+
Usage: tool <command>
|
|
1014
|
+
|
|
1015
|
+
Commands:
|
|
1016
|
+
ping Health check
|
|
1017
|
+
|
|
1018
|
+
Built-in Commands:
|
|
1019
|
+
mcp add Register as an MCP server
|
|
1020
|
+
skills add Sync skill files to your agent
|
|
1021
|
+
|
|
1022
|
+
Global Options:
|
|
1023
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
1024
|
+
--help Show help
|
|
1025
|
+
--llms Print LLM-readable manifest
|
|
1026
|
+
--mcp Start as MCP stdio server
|
|
1027
|
+
--verbose Show full output envelope
|
|
1028
|
+
--version Show version
|
|
1029
|
+
"
|
|
1030
|
+
`)
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
test('--help on leaf shows command help', async () => {
|
|
1034
|
+
const cli = Cli.create('tool')
|
|
1035
|
+
cli.command('greet', {
|
|
1036
|
+
description: 'Greet someone',
|
|
1037
|
+
args: z.object({ name: z.string().describe('Name') }),
|
|
1038
|
+
run: ({ args }) => ({ message: `hi ${args.name}` }),
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
const { output, exitCode } = await serve(cli, ['greet', '--help'])
|
|
1042
|
+
expect(exitCode).toBeUndefined()
|
|
1043
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1044
|
+
"tool greet — Greet someone
|
|
1045
|
+
|
|
1046
|
+
Usage: tool greet <name>
|
|
1047
|
+
|
|
1048
|
+
Arguments:
|
|
1049
|
+
name Name
|
|
1050
|
+
|
|
1051
|
+
Global Options:
|
|
1052
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
1053
|
+
--help Show help
|
|
1054
|
+
--llms Print LLM-readable manifest
|
|
1055
|
+
--verbose Show full output envelope
|
|
1056
|
+
"
|
|
1057
|
+
`)
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
test('group with no subcommand shows help', async () => {
|
|
1061
|
+
const pr = Cli.create('pr', { description: 'Pull request commands' })
|
|
1062
|
+
pr.command('list', {
|
|
1063
|
+
description: 'List PRs',
|
|
1064
|
+
run: () => ({}),
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
const cli = Cli.create('gh')
|
|
1068
|
+
cli.command(pr)
|
|
1069
|
+
|
|
1070
|
+
const { output, exitCode } = await serve(cli, ['pr'])
|
|
1071
|
+
expect(exitCode).toBeUndefined()
|
|
1072
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1073
|
+
"gh pr — Pull request commands
|
|
1074
|
+
|
|
1075
|
+
Usage: gh pr <command>
|
|
1076
|
+
|
|
1077
|
+
Commands:
|
|
1078
|
+
list List PRs
|
|
1079
|
+
|
|
1080
|
+
Global Options:
|
|
1081
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
1082
|
+
--help Show help
|
|
1083
|
+
--llms Print LLM-readable manifest
|
|
1084
|
+
--verbose Show full output envelope
|
|
1085
|
+
"
|
|
1086
|
+
`)
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
test('--version outputs version string', async () => {
|
|
1090
|
+
const cli = Cli.create('tool', { version: '1.2.3' })
|
|
1091
|
+
cli.command('ping', { run: () => ({}) })
|
|
1092
|
+
|
|
1093
|
+
const { output, exitCode } = await serve(cli, ['--version'])
|
|
1094
|
+
expect(exitCode).toBeUndefined()
|
|
1095
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1096
|
+
"1.2.3
|
|
1097
|
+
"
|
|
1098
|
+
`)
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
test('--help takes precedence over --version', async () => {
|
|
1102
|
+
const cli = Cli.create('tool', { version: '1.2.3' })
|
|
1103
|
+
cli.command('ping', { description: 'Ping', run: () => ({}) })
|
|
1104
|
+
|
|
1105
|
+
const { output } = await serve(cli, ['--help', '--version'])
|
|
1106
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1107
|
+
"tool
|
|
1108
|
+
v1.2.3
|
|
1109
|
+
|
|
1110
|
+
Usage: tool <command>
|
|
1111
|
+
|
|
1112
|
+
Commands:
|
|
1113
|
+
ping Ping
|
|
1114
|
+
|
|
1115
|
+
Built-in Commands:
|
|
1116
|
+
mcp add Register as an MCP server
|
|
1117
|
+
skills add Sync skill files to your agent
|
|
1118
|
+
|
|
1119
|
+
Global Options:
|
|
1120
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
1121
|
+
--help Show help
|
|
1122
|
+
--llms Print LLM-readable manifest
|
|
1123
|
+
--mcp Start as MCP stdio server
|
|
1124
|
+
--verbose Show full output envelope
|
|
1125
|
+
--version Show version
|
|
1126
|
+
"
|
|
1127
|
+
`)
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
test('--help shows hint after examples', async () => {
|
|
1131
|
+
const cli = Cli.create('tool')
|
|
1132
|
+
cli.command('deploy', {
|
|
1133
|
+
description: 'Deploy the app',
|
|
1134
|
+
hint: 'Run "tool status" to check deployment progress.',
|
|
1135
|
+
run: () => ({ ok: true }),
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
const { output } = await serve(cli, ['deploy', '--help'])
|
|
1139
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1140
|
+
"tool deploy — Deploy the app
|
|
1141
|
+
|
|
1142
|
+
Usage: tool deploy
|
|
1143
|
+
|
|
1144
|
+
Run "tool status" to check deployment progress.
|
|
1145
|
+
|
|
1146
|
+
Global Options:
|
|
1147
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
1148
|
+
--help Show help
|
|
1149
|
+
--llms Print LLM-readable manifest
|
|
1150
|
+
--verbose Show full output envelope
|
|
1151
|
+
"
|
|
1152
|
+
`)
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
test('--help omits hint when not set', async () => {
|
|
1156
|
+
const cli = Cli.create('tool')
|
|
1157
|
+
cli.command('ping', {
|
|
1158
|
+
description: 'Health check',
|
|
1159
|
+
run: () => ({ pong: true }),
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
const { output } = await serve(cli, ['ping', '--help'])
|
|
1163
|
+
expect(output).not.toContain('hint')
|
|
1164
|
+
})
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
describe('env', () => {
|
|
1168
|
+
test('parses env vars and passes to handler', async () => {
|
|
1169
|
+
const cli = Cli.create('test')
|
|
1170
|
+
let receivedEnv: any
|
|
1171
|
+
cli.command('deploy', {
|
|
1172
|
+
env: z.object({
|
|
1173
|
+
API_TOKEN: z.string().describe('Auth token'),
|
|
1174
|
+
}),
|
|
1175
|
+
run({ env }) {
|
|
1176
|
+
receivedEnv = env
|
|
1177
|
+
return { ok: true }
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
await serve(cli, ['deploy'], { env: { API_TOKEN: 'secret-123' } })
|
|
1182
|
+
expect(receivedEnv).toEqual({ API_TOKEN: 'secret-123' })
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
test('env validation error for missing required var', async () => {
|
|
1186
|
+
const cli = Cli.create('test')
|
|
1187
|
+
cli.command('deploy', {
|
|
1188
|
+
env: z.object({
|
|
1189
|
+
API_TOKEN: z.string().describe('Auth token'),
|
|
1190
|
+
}),
|
|
1191
|
+
run() {
|
|
1192
|
+
return {}
|
|
1193
|
+
},
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
const { output, exitCode } = await serve(cli, ['deploy'], { env: {} })
|
|
1197
|
+
expect(exitCode).toBe(1)
|
|
1198
|
+
expect(output).toContain('Error')
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
test('env with defaults works when var is unset', async () => {
|
|
1202
|
+
const cli = Cli.create('test')
|
|
1203
|
+
let receivedEnv: any
|
|
1204
|
+
cli.command('deploy', {
|
|
1205
|
+
env: z.object({
|
|
1206
|
+
API_URL: z.string().default('https://api.example.com').describe('API URL'),
|
|
1207
|
+
}),
|
|
1208
|
+
run({ env }) {
|
|
1209
|
+
receivedEnv = env
|
|
1210
|
+
return { ok: true }
|
|
1211
|
+
},
|
|
1212
|
+
})
|
|
1213
|
+
|
|
1214
|
+
await serve(cli, ['deploy'], { env: {} })
|
|
1215
|
+
expect(receivedEnv).toEqual({ API_URL: 'https://api.example.com' })
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
test('--help shows environment variables section', async () => {
|
|
1219
|
+
const cli = Cli.create('test')
|
|
1220
|
+
cli.command('deploy', {
|
|
1221
|
+
env: z.object({
|
|
1222
|
+
API_TOKEN: z.string().describe('Auth token'),
|
|
1223
|
+
API_URL: z.string().default('https://api.example.com').describe('API URL'),
|
|
1224
|
+
}),
|
|
1225
|
+
run() {
|
|
1226
|
+
return {}
|
|
1227
|
+
},
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
const { output } = await serve(cli, ['deploy', '--help'])
|
|
1231
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1232
|
+
"test deploy
|
|
1233
|
+
|
|
1234
|
+
Usage: test deploy
|
|
1235
|
+
|
|
1236
|
+
Environment Variables:
|
|
1237
|
+
API_TOKEN Auth token
|
|
1238
|
+
API_URL API URL (default: https://api.example.com)
|
|
1239
|
+
|
|
1240
|
+
Global Options:
|
|
1241
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
1242
|
+
--help Show help
|
|
1243
|
+
--llms Print LLM-readable manifest
|
|
1244
|
+
--verbose Show full output envelope
|
|
1245
|
+
"
|
|
1246
|
+
`)
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
test('--llms json includes schema.env', async () => {
|
|
1250
|
+
const cli = Cli.create('test')
|
|
1251
|
+
cli.command('deploy', {
|
|
1252
|
+
env: z.object({
|
|
1253
|
+
API_TOKEN: z.string().describe('Auth token'),
|
|
1254
|
+
}),
|
|
1255
|
+
run() {
|
|
1256
|
+
return {}
|
|
1257
|
+
},
|
|
1258
|
+
})
|
|
1259
|
+
|
|
1260
|
+
const { output } = await serve(cli, ['--llms', '--format', 'json'])
|
|
1261
|
+
const cmd = JSON.parse(output).commands.find((c: any) => c.name === 'deploy')
|
|
1262
|
+
expect(cmd.schema.env).toMatchInlineSnapshot(`
|
|
1263
|
+
{
|
|
1264
|
+
"additionalProperties": false,
|
|
1265
|
+
"properties": {
|
|
1266
|
+
"API_TOKEN": {
|
|
1267
|
+
"description": "Auth token",
|
|
1268
|
+
"type": "string",
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
"required": [
|
|
1272
|
+
"API_TOKEN",
|
|
1273
|
+
],
|
|
1274
|
+
"type": "object",
|
|
1275
|
+
}
|
|
1276
|
+
`)
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
test('--llms markdown includes environment variables table', async () => {
|
|
1280
|
+
const cli = Cli.create('test')
|
|
1281
|
+
cli.command('deploy', {
|
|
1282
|
+
env: z.object({
|
|
1283
|
+
API_TOKEN: z.string().describe('Auth token'),
|
|
1284
|
+
}),
|
|
1285
|
+
run() {
|
|
1286
|
+
return {}
|
|
1287
|
+
},
|
|
1288
|
+
})
|
|
1289
|
+
|
|
1290
|
+
const { output } = await serve(cli, ['--llms'])
|
|
1291
|
+
expect(output).toContain('Environment Variables')
|
|
1292
|
+
expect(output).toContain('`API_TOKEN`')
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
test('env coerces boolean and number values', async () => {
|
|
1296
|
+
const cli = Cli.create('test')
|
|
1297
|
+
let receivedEnv: any
|
|
1298
|
+
cli.command('deploy', {
|
|
1299
|
+
env: z.object({
|
|
1300
|
+
DEBUG: z.boolean().default(false).describe('Debug mode'),
|
|
1301
|
+
PORT: z.number().default(3000).describe('Port'),
|
|
1302
|
+
}),
|
|
1303
|
+
run({ env }) {
|
|
1304
|
+
receivedEnv = env
|
|
1305
|
+
return { ok: true }
|
|
1306
|
+
},
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
await serve(cli, ['deploy'], { env: { DEBUG: 'true', PORT: '8080' } })
|
|
1310
|
+
expect(receivedEnv).toEqual({ DEBUG: true, PORT: 8080 })
|
|
1311
|
+
})
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
describe('skills staleness', () => {
|
|
1315
|
+
let stderrSpy: ReturnType<typeof vi.spyOn>
|
|
1316
|
+
|
|
1317
|
+
beforeEach(() => {
|
|
1318
|
+
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
|
1319
|
+
__mockSkillsHash = undefined
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
afterEach(() => {
|
|
1323
|
+
stderrSpy.mockRestore()
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
test('warns on stderr when skills are stale', async () => {
|
|
1327
|
+
__mockSkillsHash = '0000000000000000'
|
|
1328
|
+
const cli = Cli.create('test')
|
|
1329
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
1330
|
+
|
|
1331
|
+
await serve(cli, ['ping'])
|
|
1332
|
+
expect(stderrSpy).toHaveBeenCalledWith(
|
|
1333
|
+
expect.stringContaining("Skills are out of date. Run 'pnpx test skills add' to update."),
|
|
1334
|
+
)
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1337
|
+
test('does not warn when hash matches', async () => {
|
|
1338
|
+
const { Skill } = await import('incur')
|
|
1339
|
+
__mockSkillsHash = Skill.hash([{ name: 'ping', description: 'Health check' }])
|
|
1340
|
+
const cli = Cli.create('test')
|
|
1341
|
+
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
1342
|
+
|
|
1343
|
+
await serve(cli, ['ping'])
|
|
1344
|
+
expect(stderrSpy).not.toHaveBeenCalled()
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
test('does not warn when no hash stored', async () => {
|
|
1348
|
+
__mockSkillsHash = undefined
|
|
1349
|
+
const cli = Cli.create('test')
|
|
1350
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1351
|
+
|
|
1352
|
+
await serve(cli, ['ping'])
|
|
1353
|
+
expect(stderrSpy).not.toHaveBeenCalled()
|
|
1354
|
+
})
|
|
1355
|
+
|
|
1356
|
+
test('does not warn for skills add', async () => {
|
|
1357
|
+
__mockSkillsHash = '0000000000000000'
|
|
1358
|
+
const cli = Cli.create('test')
|
|
1359
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1360
|
+
|
|
1361
|
+
await serve(cli, ['skills', 'add'])
|
|
1362
|
+
expect(stderrSpy).not.toHaveBeenCalled()
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
test('does not warn for --help', async () => {
|
|
1366
|
+
__mockSkillsHash = '0000000000000000'
|
|
1367
|
+
const cli = Cli.create('test')
|
|
1368
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1369
|
+
|
|
1370
|
+
await serve(cli, ['--help'])
|
|
1371
|
+
expect(stderrSpy).not.toHaveBeenCalled()
|
|
1372
|
+
})
|
|
1373
|
+
})
|