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/Help.test.ts
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
import { Help, z } from 'incur'
|
|
2
2
|
|
|
3
|
+
describe('redact: short secrets should not leak characters', () => {
|
|
4
|
+
/**
|
|
5
|
+
* The internal `redact()` function is exercised through `formatCommand`
|
|
6
|
+
* by passing an env schema + envSource with a set value.
|
|
7
|
+
*/
|
|
8
|
+
function getRedactedValue(secret: string): string {
|
|
9
|
+
const env = z.object({ SECRET: z.string().describe('a secret') })
|
|
10
|
+
const output = Help.formatCommand('test', {
|
|
11
|
+
env,
|
|
12
|
+
envSource: { SECRET: secret },
|
|
13
|
+
hideGlobalOptions: true,
|
|
14
|
+
})
|
|
15
|
+
const match = output.match(/set:\s*(\S+)/)
|
|
16
|
+
if (!match) throw new Error(`Could not find "set:" in output:\n${output}`)
|
|
17
|
+
return match[1]!
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('1-char secret is fully masked', () => {
|
|
21
|
+
const redacted = getRedactedValue('x')
|
|
22
|
+
expect(redacted).not.toContain('x')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('2-char secret does not leak any character', () => {
|
|
26
|
+
const redacted = getRedactedValue('ab')
|
|
27
|
+
expect(redacted).not.toContain('b')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('3-char secret does not leak any character', () => {
|
|
31
|
+
const redacted = getRedactedValue('abc')
|
|
32
|
+
expect(redacted).not.toContain('c')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('4-char secret does not leak any character', () => {
|
|
36
|
+
const redacted = getRedactedValue('wxyz')
|
|
37
|
+
expect(redacted).not.toContain('z')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
3
41
|
describe('formatCommand', () => {
|
|
4
42
|
test('formats leaf command with args and options', () => {
|
|
5
43
|
const result = Help.formatCommand('gh pr list', {
|
|
@@ -27,13 +65,13 @@ describe('formatCommand', () => {
|
|
|
27
65
|
Global Options:
|
|
28
66
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
29
67
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
68
|
+
--full-output Show full output envelope
|
|
30
69
|
--help Show help
|
|
31
70
|
--llms, --llms-full Print LLM-readable manifest
|
|
32
|
-
--schema Show JSON Schema for
|
|
71
|
+
--schema Show JSON Schema for command
|
|
33
72
|
--token-count Print token count of output (instead of output)
|
|
34
73
|
--token-limit <n> Limit output to n tokens
|
|
35
|
-
--token-offset <n> Skip first n tokens of output
|
|
36
|
-
--verbose Show full output envelope"
|
|
74
|
+
--token-offset <n> Skip first n tokens of output"
|
|
37
75
|
`)
|
|
38
76
|
})
|
|
39
77
|
|
|
@@ -49,13 +87,13 @@ describe('formatCommand', () => {
|
|
|
49
87
|
Global Options:
|
|
50
88
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
51
89
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
90
|
+
--full-output Show full output envelope
|
|
52
91
|
--help Show help
|
|
53
92
|
--llms, --llms-full Print LLM-readable manifest
|
|
54
|
-
--schema Show JSON Schema for
|
|
93
|
+
--schema Show JSON Schema for command
|
|
55
94
|
--token-count Print token count of output (instead of output)
|
|
56
95
|
--token-limit <n> Limit output to n tokens
|
|
57
|
-
--token-offset <n> Skip first n tokens of output
|
|
58
|
-
--verbose Show full output envelope"
|
|
96
|
+
--token-offset <n> Skip first n tokens of output"
|
|
59
97
|
`)
|
|
60
98
|
})
|
|
61
99
|
|
|
@@ -78,16 +116,27 @@ describe('formatCommand', () => {
|
|
|
78
116
|
Global Options:
|
|
79
117
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
80
118
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
119
|
+
--full-output Show full output envelope
|
|
81
120
|
--help Show help
|
|
82
121
|
--llms, --llms-full Print LLM-readable manifest
|
|
83
|
-
--schema Show JSON Schema for
|
|
122
|
+
--schema Show JSON Schema for command
|
|
84
123
|
--token-count Print token count of output (instead of output)
|
|
85
124
|
--token-limit <n> Limit output to n tokens
|
|
86
|
-
--token-offset <n> Skip first n tokens of output
|
|
87
|
-
--verbose Show full output envelope"
|
|
125
|
+
--token-offset <n> Skip first n tokens of output"
|
|
88
126
|
`)
|
|
89
127
|
})
|
|
90
128
|
|
|
129
|
+
test('synopsis uses key name for non-union args and expanded values for enums', () => {
|
|
130
|
+
const result = Help.formatCommand('tool run', {
|
|
131
|
+
args: z.object({
|
|
132
|
+
port: z.number().describe('Port number'),
|
|
133
|
+
verbose: z.boolean().optional().describe('Verbose'),
|
|
134
|
+
mode: z.enum(['fast', 'slow']).describe('Mode'),
|
|
135
|
+
}),
|
|
136
|
+
})
|
|
137
|
+
expect(result).toContain('Usage: tool run <port> [verbose] <fast|slow>')
|
|
138
|
+
})
|
|
139
|
+
|
|
91
140
|
test('shows count type in help for meta count', () => {
|
|
92
141
|
const result = Help.formatCommand('tool run', {
|
|
93
142
|
options: z.object({
|
|
@@ -99,6 +148,55 @@ describe('formatCommand', () => {
|
|
|
99
148
|
expect(result).toContain('Verbosity level')
|
|
100
149
|
})
|
|
101
150
|
|
|
151
|
+
test('omits value placeholders for boolean flag options', () => {
|
|
152
|
+
const result = Help.formatCommand('tool deploy', {
|
|
153
|
+
options: z.object({
|
|
154
|
+
dryRun: z.boolean().optional().describe('Preview without submitting.'),
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const line = result.split('\n').find((line) => line.includes('--dry-run'))
|
|
159
|
+
|
|
160
|
+
expect(line).toBe(' --dry-run Preview without submitting.')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('omits value placeholders for aliased boolean flag options', () => {
|
|
164
|
+
const result = Help.formatCommand('tool deploy', {
|
|
165
|
+
options: z.object({
|
|
166
|
+
dryRun: z.boolean().optional().describe('Preview without submitting.'),
|
|
167
|
+
}),
|
|
168
|
+
alias: { dryRun: 'd' },
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const line = result.split('\n').find((line) => line.includes('--dry-run'))
|
|
172
|
+
|
|
173
|
+
expect(line).toBe(' --dry-run, -d Preview without submitting.')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('omits default false for boolean flag options', () => {
|
|
177
|
+
const result = Help.formatCommand('tool deploy', {
|
|
178
|
+
options: z.object({
|
|
179
|
+
dryRun: z.boolean().default(false).describe('Preview without submitting.'),
|
|
180
|
+
}),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const line = result.split('\n').find((line) => line.includes('--dry-run'))
|
|
184
|
+
|
|
185
|
+
expect(line).toBe(' --dry-run Preview without submitting.')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('shows default true for boolean flag options', () => {
|
|
189
|
+
const result = Help.formatCommand('tool deploy', {
|
|
190
|
+
options: z.object({
|
|
191
|
+
watch: z.boolean().default(true).describe('Watch for changes.'),
|
|
192
|
+
}),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const line = result.split('\n').find((line) => line.includes('--watch'))
|
|
196
|
+
|
|
197
|
+
expect(line).toBe(' --watch Watch for changes. (default: true)')
|
|
198
|
+
})
|
|
199
|
+
|
|
102
200
|
test('shows enum values for z.enum options', () => {
|
|
103
201
|
const result = Help.formatCommand('tool deploy', {
|
|
104
202
|
options: z.object({
|
|
@@ -130,6 +228,36 @@ describe('formatCommand', () => {
|
|
|
130
228
|
expect(result).toContain('[deprecated] Availability zone')
|
|
131
229
|
expect(result).not.toContain('[deprecated] Target region')
|
|
132
230
|
})
|
|
231
|
+
|
|
232
|
+
test('shows config global options when flag name is set', () => {
|
|
233
|
+
const result = Help.formatCommand('tool deploy', {
|
|
234
|
+
configFlag: 'config',
|
|
235
|
+
options: z.object({
|
|
236
|
+
env: z.enum(['staging', 'production']).describe('Target environment'),
|
|
237
|
+
}),
|
|
238
|
+
})
|
|
239
|
+
expect(result).toMatchInlineSnapshot(`
|
|
240
|
+
"tool deploy
|
|
241
|
+
|
|
242
|
+
Usage: tool deploy [options]
|
|
243
|
+
|
|
244
|
+
Options:
|
|
245
|
+
--env <staging|production> Target environment
|
|
246
|
+
|
|
247
|
+
Global Options:
|
|
248
|
+
--config <path> Load JSON option defaults from a file
|
|
249
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
250
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
251
|
+
--full-output Show full output envelope
|
|
252
|
+
--help Show help
|
|
253
|
+
--llms, --llms-full Print LLM-readable manifest
|
|
254
|
+
--no-config Disable JSON option defaults for this run
|
|
255
|
+
--schema Show JSON Schema for command
|
|
256
|
+
--token-count Print token count of output (instead of output)
|
|
257
|
+
--token-limit <n> Limit output to n tokens
|
|
258
|
+
--token-offset <n> Skip first n tokens of output"
|
|
259
|
+
`)
|
|
260
|
+
})
|
|
133
261
|
})
|
|
134
262
|
|
|
135
263
|
describe('formatRoot', () => {
|
|
@@ -155,13 +283,13 @@ describe('formatRoot', () => {
|
|
|
155
283
|
Global Options:
|
|
156
284
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
157
285
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
286
|
+
--full-output Show full output envelope
|
|
158
287
|
--help Show help
|
|
159
288
|
--llms, --llms-full Print LLM-readable manifest
|
|
160
|
-
--schema Show JSON Schema for
|
|
289
|
+
--schema Show JSON Schema for command
|
|
161
290
|
--token-count Print token count of output (instead of output)
|
|
162
291
|
--token-limit <n> Limit output to n tokens
|
|
163
|
-
--token-offset <n> Skip first n tokens of output
|
|
164
|
-
--verbose Show full output envelope"
|
|
292
|
+
--token-offset <n> Skip first n tokens of output"
|
|
165
293
|
`)
|
|
166
294
|
})
|
|
167
295
|
|
|
@@ -180,13 +308,13 @@ describe('formatRoot', () => {
|
|
|
180
308
|
Global Options:
|
|
181
309
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
182
310
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
311
|
+
--full-output Show full output envelope
|
|
183
312
|
--help Show help
|
|
184
313
|
--llms, --llms-full Print LLM-readable manifest
|
|
185
|
-
--schema Show JSON Schema for
|
|
314
|
+
--schema Show JSON Schema for command
|
|
186
315
|
--token-count Print token count of output (instead of output)
|
|
187
316
|
--token-limit <n> Limit output to n tokens
|
|
188
|
-
--token-offset <n> Skip first n tokens of output
|
|
189
|
-
--verbose Show full output envelope"
|
|
317
|
+
--token-offset <n> Skip first n tokens of output"
|
|
190
318
|
`)
|
|
191
319
|
})
|
|
192
320
|
|
|
@@ -209,13 +337,13 @@ describe('formatRoot', () => {
|
|
|
209
337
|
Global Options:
|
|
210
338
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
211
339
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
340
|
+
--full-output Show full output envelope
|
|
212
341
|
--help Show help
|
|
213
342
|
--llms, --llms-full Print LLM-readable manifest
|
|
214
|
-
--schema Show JSON Schema for
|
|
343
|
+
--schema Show JSON Schema for command
|
|
215
344
|
--token-count Print token count of output (instead of output)
|
|
216
345
|
--token-limit <n> Limit output to n tokens
|
|
217
|
-
--token-offset <n> Skip first n tokens of output
|
|
218
|
-
--verbose Show full output envelope"
|
|
346
|
+
--token-offset <n> Skip first n tokens of output"
|
|
219
347
|
`)
|
|
220
348
|
})
|
|
221
349
|
|
|
@@ -238,13 +366,49 @@ describe('formatRoot', () => {
|
|
|
238
366
|
Global Options:
|
|
239
367
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
240
368
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
369
|
+
--full-output Show full output envelope
|
|
370
|
+
--help Show help
|
|
371
|
+
--llms, --llms-full Print LLM-readable manifest
|
|
372
|
+
--schema Show JSON Schema for command
|
|
373
|
+
--token-count Print token count of output (instead of output)
|
|
374
|
+
--token-limit <n> Limit output to n tokens
|
|
375
|
+
--token-offset <n> Skip first n tokens of output"
|
|
376
|
+
`)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('formatRoot shows config global options when flag name is set', () => {
|
|
380
|
+
const result = Help.formatRoot('tool', {
|
|
381
|
+
configFlag: 'config',
|
|
382
|
+
root: true,
|
|
383
|
+
commands: [{ name: 'ping', description: 'Health check' }],
|
|
384
|
+
})
|
|
385
|
+
expect(result).toMatchInlineSnapshot(`
|
|
386
|
+
"tool
|
|
387
|
+
|
|
388
|
+
Usage: tool <command>
|
|
389
|
+
|
|
390
|
+
Commands:
|
|
391
|
+
ping Health check
|
|
392
|
+
|
|
393
|
+
Integrations:
|
|
394
|
+
completions Generate shell completion script
|
|
395
|
+
mcp add Register as MCP server
|
|
396
|
+
skills Sync skill files to agents (add, list)
|
|
397
|
+
|
|
398
|
+
Global Options:
|
|
399
|
+
--config <path> Load JSON option defaults from a file
|
|
400
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
401
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
402
|
+
--full-output Show full output envelope
|
|
241
403
|
--help Show help
|
|
242
404
|
--llms, --llms-full Print LLM-readable manifest
|
|
243
|
-
--
|
|
405
|
+
--mcp Start as MCP stdio server
|
|
406
|
+
--no-config Disable JSON option defaults for this run
|
|
407
|
+
--schema Show JSON Schema for command
|
|
244
408
|
--token-count Print token count of output (instead of output)
|
|
245
409
|
--token-limit <n> Limit output to n tokens
|
|
246
410
|
--token-offset <n> Skip first n tokens of output
|
|
247
|
-
--
|
|
411
|
+
--version Show version"
|
|
248
412
|
`)
|
|
249
413
|
})
|
|
250
414
|
})
|
package/src/Help.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { builtinCommands } from './internal/command.js'
|
|
4
|
+
import { toKebab } from './internal/helpers.js'
|
|
5
|
+
import { defaultEnvSource } from './Parser.js'
|
|
6
|
+
|
|
3
7
|
/** Formats help text for a router CLI or command group. */
|
|
4
8
|
export function formatRoot(name: string, options: formatRoot.Options = {}): string {
|
|
5
|
-
const { aliases, description, version, commands = [], root = false } = options
|
|
9
|
+
const { aliases, configFlag, description, version, commands = [], root = false } = options
|
|
6
10
|
const lines: string[] = []
|
|
7
11
|
|
|
8
12
|
// Header
|
|
@@ -27,7 +31,7 @@ export function formatRoot(name: string, options: formatRoot.Options = {}): stri
|
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
lines.push(...globalOptionsLines(root))
|
|
34
|
+
lines.push(...globalOptionsLines(root, configFlag))
|
|
31
35
|
|
|
32
36
|
return lines.join('\n')
|
|
33
37
|
}
|
|
@@ -36,6 +40,8 @@ export declare namespace formatRoot {
|
|
|
36
40
|
type Options = {
|
|
37
41
|
/** Alternative binary names for this CLI. */
|
|
38
42
|
aliases?: string[] | undefined
|
|
43
|
+
/** Flag name for config file path (e.g. `'config'` renders `--config <path>`). */
|
|
44
|
+
configFlag?: string | undefined
|
|
39
45
|
/** Commands to list. */
|
|
40
46
|
commands?: { name: string; description?: string | undefined }[] | undefined
|
|
41
47
|
/** A short description of the CLI or group. */
|
|
@@ -50,11 +56,13 @@ export declare namespace formatRoot {
|
|
|
50
56
|
export declare namespace formatCommand {
|
|
51
57
|
type Options = {
|
|
52
58
|
/** Map of option names to single-char aliases. */
|
|
53
|
-
alias?: Record<string, string
|
|
59
|
+
alias?: Partial<Record<string, string>> | undefined
|
|
54
60
|
/** Alternative binary names for this CLI. */
|
|
55
61
|
aliases?: string[] | undefined
|
|
56
62
|
/** Zod schema for positional arguments. */
|
|
57
63
|
args?: z.ZodObject<any> | undefined
|
|
64
|
+
/** Flag name for config file path (e.g. `'config'` renders `--config <path>`). */
|
|
65
|
+
configFlag?: string | undefined
|
|
58
66
|
/** Subcommands to list (for CLIs with both a root handler and subcommands). */
|
|
59
67
|
commands?: { name: string; description?: string | undefined }[] | undefined
|
|
60
68
|
/** A short description of what the command does. */
|
|
@@ -67,6 +75,8 @@ export declare namespace formatCommand {
|
|
|
67
75
|
examples?: { command: string; description?: string }[] | undefined
|
|
68
76
|
/** Plain text hint displayed after examples and before global options. */
|
|
69
77
|
hint?: string | undefined
|
|
78
|
+
/** Hide global options section. */
|
|
79
|
+
hideGlobalOptions?: boolean | undefined
|
|
70
80
|
/** Zod schema for named options/flags. */
|
|
71
81
|
options?: z.ZodObject<any> | undefined
|
|
72
82
|
/** Show root-level built-in commands and flags. */
|
|
@@ -90,6 +100,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
90
100
|
const {
|
|
91
101
|
alias,
|
|
92
102
|
aliases,
|
|
103
|
+
configFlag,
|
|
93
104
|
description,
|
|
94
105
|
version,
|
|
95
106
|
args,
|
|
@@ -195,7 +206,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
195
206
|
}
|
|
196
207
|
}
|
|
197
208
|
|
|
198
|
-
lines.push(...globalOptionsLines(root))
|
|
209
|
+
if (!options.hideGlobalOptions) lines.push(...globalOptionsLines(root, configFlag))
|
|
199
210
|
|
|
200
211
|
// Environment Variables
|
|
201
212
|
if (env) {
|
|
@@ -207,7 +218,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
207
218
|
for (const entry of entries) {
|
|
208
219
|
const padding = ' '.repeat(maxLen - entry.name.length)
|
|
209
220
|
const parts: string[] = [entry.description]
|
|
210
|
-
const source = envSource ??
|
|
221
|
+
const source = envSource ?? defaultEnvSource()
|
|
211
222
|
if (entry.name in source) parts.push(`set: ${redact(source[entry.name]!)}`)
|
|
212
223
|
if (entry.defaultValue !== undefined) parts.push(`default: ${entry.defaultValue}`)
|
|
213
224
|
const desc = parts.length > 1 ? `${parts[0]} (${parts.slice(1).join(', ')})` : parts[0]
|
|
@@ -223,8 +234,11 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
223
234
|
function buildSynopsis(name: string, args?: z.ZodObject<any>): string {
|
|
224
235
|
if (!args) return name
|
|
225
236
|
const parts = [name]
|
|
226
|
-
for (const [key, schema] of Object.entries(args.shape))
|
|
227
|
-
|
|
237
|
+
for (const [key, schema] of Object.entries(args.shape)) {
|
|
238
|
+
const type = resolveTypeName(schema)
|
|
239
|
+
const label = type.includes('|') ? type : key
|
|
240
|
+
parts.push((schema as z.ZodType)._zod.optout === 'optional' ? `[${label}]` : `<${label}>`)
|
|
241
|
+
}
|
|
228
242
|
return parts.join(' ')
|
|
229
243
|
}
|
|
230
244
|
|
|
@@ -247,7 +261,10 @@ function envEntries(schema: z.ZodObject<any>) {
|
|
|
247
261
|
}
|
|
248
262
|
|
|
249
263
|
/** Extracts option entries from a Zod object schema. */
|
|
250
|
-
function optionEntries(
|
|
264
|
+
function optionEntries(
|
|
265
|
+
schema: z.ZodObject<any>,
|
|
266
|
+
alias?: Partial<Record<string, string>> | undefined,
|
|
267
|
+
) {
|
|
251
268
|
const entries: {
|
|
252
269
|
flag: string
|
|
253
270
|
description: string
|
|
@@ -258,8 +275,10 @@ function optionEntries(schema: z.ZodObject<any>, alias?: Record<string, string>
|
|
|
258
275
|
const type = resolveTypeName(field)
|
|
259
276
|
const short = alias?.[key]
|
|
260
277
|
const kebab = toKebab(key)
|
|
261
|
-
const
|
|
262
|
-
const
|
|
278
|
+
const valueHint = type === 'boolean' ? '' : ` <${type}>`
|
|
279
|
+
const flag = short ? `--${kebab}, -${short}${valueHint}` : `--${kebab}${valueHint}`
|
|
280
|
+
let defaultValue = extractDefault(field)
|
|
281
|
+
if (type === 'boolean' && defaultValue === false) defaultValue = undefined
|
|
263
282
|
const deprecated = extractDeprecated(field)
|
|
264
283
|
entries.push({ flag, description: (field as any).description ?? '', defaultValue, deprecated })
|
|
265
284
|
}
|
|
@@ -320,30 +339,32 @@ function extractDeprecated(schema: unknown): boolean | undefined {
|
|
|
320
339
|
return meta?.deprecated === true ? true : undefined
|
|
321
340
|
}
|
|
322
341
|
|
|
323
|
-
/** Converts a camelCase string to kebab-case. */
|
|
324
|
-
function toKebab(str: string): string {
|
|
325
|
-
return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
342
|
/** Renders the built-in commands and global options block. Root-only items are hidden for subcommands. */
|
|
329
|
-
function globalOptionsLines(root = false): string[] {
|
|
343
|
+
function globalOptionsLines(root = false, configFlag?: string): string[] {
|
|
330
344
|
const lines: string[] = []
|
|
331
345
|
|
|
332
346
|
if (root) {
|
|
333
|
-
const builtins =
|
|
334
|
-
{ name:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
347
|
+
const builtins = builtinCommands.flatMap((b) => {
|
|
348
|
+
if (!b.subcommands) return [{ name: b.name, desc: b.description }]
|
|
349
|
+
if (b.subcommands.length === 1)
|
|
350
|
+
return [
|
|
351
|
+
{ name: `${b.name} ${b.subcommands[0]!.name}`, desc: b.subcommands[0]!.description },
|
|
352
|
+
]
|
|
353
|
+
const names = b.subcommands.map((s) => s.name).join(', ')
|
|
354
|
+
return [{ name: b.name, desc: `${b.description} (${names})` }]
|
|
355
|
+
})
|
|
338
356
|
const maxCmd = Math.max(...builtins.map((b) => b.name.length))
|
|
339
357
|
lines.push(
|
|
340
358
|
'',
|
|
341
|
-
'
|
|
359
|
+
'Integrations:',
|
|
342
360
|
...builtins.map((b) => ` ${b.name}${' '.repeat(maxCmd - b.name.length)} ${b.desc}`),
|
|
343
361
|
)
|
|
344
362
|
}
|
|
345
363
|
|
|
346
364
|
const flags = [
|
|
365
|
+
...(configFlag
|
|
366
|
+
? [{ flag: `--${configFlag} <path>`, desc: 'Load JSON option defaults from a file' }]
|
|
367
|
+
: []),
|
|
347
368
|
{
|
|
348
369
|
flag: '--filter-output <keys>',
|
|
349
370
|
desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])',
|
|
@@ -352,13 +373,16 @@ function globalOptionsLines(root = false): string[] {
|
|
|
352
373
|
{ flag: '--help', desc: 'Show help' },
|
|
353
374
|
{ flag: '--llms, --llms-full', desc: 'Print LLM-readable manifest' },
|
|
354
375
|
...(root ? [{ flag: '--mcp', desc: 'Start as MCP stdio server' }] : []),
|
|
355
|
-
|
|
376
|
+
...(configFlag
|
|
377
|
+
? [{ flag: `--no-${configFlag}`, desc: 'Disable JSON option defaults for this run' }]
|
|
378
|
+
: []),
|
|
379
|
+
{ flag: '--schema', desc: 'Show JSON Schema for command' },
|
|
356
380
|
{ flag: '--token-count', desc: 'Print token count of output (instead of output)' },
|
|
357
381
|
{ flag: '--token-limit <n>', desc: 'Limit output to n tokens' },
|
|
358
382
|
{ flag: '--token-offset <n>', desc: 'Skip first n tokens of output' },
|
|
359
|
-
{ flag: '--
|
|
383
|
+
{ flag: '--full-output', desc: 'Show full output envelope' },
|
|
360
384
|
...(root ? [{ flag: '--version', desc: 'Show version' }] : []),
|
|
361
|
-
]
|
|
385
|
+
].sort((a, b) => a.flag.localeCompare(b.flag))
|
|
362
386
|
const maxLen = Math.max(...flags.map((f) => f.flag.length))
|
|
363
387
|
lines.push(
|
|
364
388
|
'',
|
|
@@ -369,8 +393,8 @@ function globalOptionsLines(root = false): string[] {
|
|
|
369
393
|
return lines
|
|
370
394
|
}
|
|
371
395
|
|
|
372
|
-
/** Redacts a value, showing only the last
|
|
396
|
+
/** Redacts a value, showing only the last 4 characters for long values. */
|
|
373
397
|
function redact(value: string): string {
|
|
374
|
-
if (value.length <=
|
|
375
|
-
return
|
|
398
|
+
if (value.length <= 4) return '****'
|
|
399
|
+
return `****${value.slice(-4)}`
|
|
376
400
|
}
|
package/src/Mcp.test.ts
CHANGED
|
@@ -103,6 +103,22 @@ describe('Mcp', () => {
|
|
|
103
103
|
expect(res.result.capabilities.tools).toBeDefined()
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
+
test('initialize with 2025-03-26 protocol version', async () => {
|
|
107
|
+
const [res] = await mcpSession(createTestCommands(), [
|
|
108
|
+
{
|
|
109
|
+
id: 1,
|
|
110
|
+
method: 'initialize',
|
|
111
|
+
params: {
|
|
112
|
+
protocolVersion: '2025-03-26',
|
|
113
|
+
capabilities: {},
|
|
114
|
+
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
])
|
|
118
|
+
expect(res.result.serverInfo).toEqual({ name: 'test-cli', version: '1.0.0' })
|
|
119
|
+
expect(res.result.capabilities.tools).toBeDefined()
|
|
120
|
+
})
|
|
121
|
+
|
|
106
122
|
test('tools/list returns all leaf commands as tools', async () => {
|
|
107
123
|
const [, res] = await mcpSession(createTestCommands(), [
|
|
108
124
|
{ id: 1, method: 'initialize', params: initParams },
|
|
@@ -216,6 +232,149 @@ describe('Mcp', () => {
|
|
|
216
232
|
])
|
|
217
233
|
})
|
|
218
234
|
|
|
235
|
+
test('middleware runs for tool calls', async () => {
|
|
236
|
+
const commands = new Map<string, any>()
|
|
237
|
+
commands.set('secret', {
|
|
238
|
+
description: 'Protected command',
|
|
239
|
+
run: () => ({ secret: 'data' }),
|
|
240
|
+
})
|
|
241
|
+
const middlewares = [
|
|
242
|
+
async (_c: any, next: () => Promise<void>) => {
|
|
243
|
+
_c.set('ran', true)
|
|
244
|
+
await next()
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
const input = new PassThrough()
|
|
248
|
+
const output = new PassThrough()
|
|
249
|
+
const chunks: string[] = []
|
|
250
|
+
output.on('data', (chunk: Buffer) => chunks.push(chunk.toString()))
|
|
251
|
+
|
|
252
|
+
const done = Mcp.serve('test-cli', '1.0.0', commands, {
|
|
253
|
+
input,
|
|
254
|
+
output,
|
|
255
|
+
middlewares,
|
|
256
|
+
vars: z.object({ ran: z.boolean().default(false) }),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
input.write(
|
|
260
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
261
|
+
)
|
|
262
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
263
|
+
input.write(
|
|
264
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'secret', arguments: {} } })}\n`,
|
|
265
|
+
)
|
|
266
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
267
|
+
input.end()
|
|
268
|
+
await done
|
|
269
|
+
|
|
270
|
+
const responses = chunks.map((c) => JSON.parse(c.trim()))
|
|
271
|
+
const callRes = responses.find((r: any) => r.id === 2)
|
|
272
|
+
expect(callRes.result.content).toEqual([{ type: 'text', text: '{"secret":"data"}' }])
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('middleware error blocks tool call', async () => {
|
|
276
|
+
const commands = new Map<string, any>()
|
|
277
|
+
commands.set('secret', {
|
|
278
|
+
description: 'Protected',
|
|
279
|
+
run: () => ({ secret: true }),
|
|
280
|
+
})
|
|
281
|
+
const middlewares = [
|
|
282
|
+
(c: any) => {
|
|
283
|
+
c.error({ code: 'FORBIDDEN', message: 'not allowed' })
|
|
284
|
+
},
|
|
285
|
+
]
|
|
286
|
+
const input = new PassThrough()
|
|
287
|
+
const output = new PassThrough()
|
|
288
|
+
const chunks: string[] = []
|
|
289
|
+
output.on('data', (chunk: Buffer) => chunks.push(chunk.toString()))
|
|
290
|
+
|
|
291
|
+
const done = Mcp.serve('test-cli', '1.0.0', commands, {
|
|
292
|
+
input,
|
|
293
|
+
output,
|
|
294
|
+
middlewares,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
input.write(
|
|
298
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
299
|
+
)
|
|
300
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
301
|
+
input.write(
|
|
302
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'secret', arguments: {} } })}\n`,
|
|
303
|
+
)
|
|
304
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
305
|
+
input.end()
|
|
306
|
+
await done
|
|
307
|
+
|
|
308
|
+
const responses = chunks.map((c) => JSON.parse(c.trim()))
|
|
309
|
+
const callRes = responses.find((r: any) => r.id === 2)
|
|
310
|
+
expect(callRes.result.isError).toBe(true)
|
|
311
|
+
expect(callRes.result.content[0].text).toBe('not allowed')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('group middleware runs for nested tool calls', async () => {
|
|
315
|
+
const commands = new Map<string, any>()
|
|
316
|
+
const groupMiddleware = async (c: any, next: () => Promise<void>) => {
|
|
317
|
+
c.set('group', 'admin')
|
|
318
|
+
await next()
|
|
319
|
+
}
|
|
320
|
+
commands.set('admin', {
|
|
321
|
+
_group: true,
|
|
322
|
+
description: 'Admin commands',
|
|
323
|
+
middlewares: [groupMiddleware],
|
|
324
|
+
commands: new Map([
|
|
325
|
+
[
|
|
326
|
+
'status',
|
|
327
|
+
{
|
|
328
|
+
description: 'Admin status',
|
|
329
|
+
run: (c: any) => ({ group: c.var.group }),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
]),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const input = new PassThrough()
|
|
336
|
+
const output = new PassThrough()
|
|
337
|
+
const chunks: string[] = []
|
|
338
|
+
output.on('data', (chunk: Buffer) => chunks.push(chunk.toString()))
|
|
339
|
+
|
|
340
|
+
const done = Mcp.serve('test-cli', '1.0.0', commands, {
|
|
341
|
+
input,
|
|
342
|
+
output,
|
|
343
|
+
vars: z.object({ group: z.string().default('none') }),
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
input.write(
|
|
347
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
348
|
+
)
|
|
349
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
350
|
+
input.write(
|
|
351
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'admin_status', arguments: {} } })}\n`,
|
|
352
|
+
)
|
|
353
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
354
|
+
input.end()
|
|
355
|
+
await done
|
|
356
|
+
|
|
357
|
+
const responses = chunks.map((c) => JSON.parse(c.trim()))
|
|
358
|
+
const callRes = responses.find((r: any) => r.id === 2)
|
|
359
|
+
expect(callRes.result.content).toEqual([{ type: 'text', text: '{"group":"admin"}' }])
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('env schema is parsed for tool calls', async () => {
|
|
363
|
+
const commands = new Map<string, any>()
|
|
364
|
+
commands.set('check-env', {
|
|
365
|
+
description: 'Check env',
|
|
366
|
+
env: z.object({ MY_VAR: z.string().default('default-val') }),
|
|
367
|
+
run: (c: any) => ({ val: c.env.MY_VAR }),
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const [, res] = await mcpSession(commands, [
|
|
371
|
+
{ id: 1, method: 'initialize', params: initParams },
|
|
372
|
+
{ id: 2, method: 'tools/call', params: { name: 'check-env', arguments: {} } },
|
|
373
|
+
])
|
|
374
|
+
const data = JSON.parse(res.result.content[0].text)
|
|
375
|
+
expect(data.val).toBe('default-val')
|
|
376
|
+
})
|
|
377
|
+
|
|
219
378
|
test('streaming command sends progress notifications', async () => {
|
|
220
379
|
const input = new PassThrough()
|
|
221
380
|
const output = new PassThrough()
|