incur 0.4.8 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Cli.d.ts +69 -19
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +419 -73
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +2 -1
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js +52 -13
- package/dist/Completions.js.map +1 -1
- package/dist/Formatter.d.ts.map +1 -1
- package/dist/Formatter.js +6 -5
- package/dist/Formatter.js.map +1 -1
- package/dist/Help.d.ts +5 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +19 -5
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +21 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +44 -13
- package/dist/Mcp.js.map +1 -1
- package/dist/Parser.d.ts +14 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +127 -1
- package/dist/Parser.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -2
- package/dist/Skill.js.map +1 -1
- package/dist/Skillgen.d.ts.map +1 -1
- package/dist/Skillgen.js +4 -38
- package/dist/Skillgen.js.map +1 -1
- package/dist/SyncMcp.d.ts.map +1 -1
- package/dist/SyncMcp.js +75 -8
- package/dist/SyncMcp.js.map +1 -1
- package/dist/SyncSkills.d.ts +9 -0
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +13 -74
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.d.ts +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/internal/command.d.ts +31 -19
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +10 -5
- package/dist/internal/command.js.map +1 -1
- package/dist/internal/cta.d.ts +22 -0
- package/dist/internal/cta.d.ts.map +1 -0
- package/dist/internal/cta.js +25 -0
- package/dist/internal/cta.js.map +1 -0
- package/dist/internal/json.d.ts +5 -0
- package/dist/internal/json.d.ts.map +1 -0
- package/dist/internal/json.js +13 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.d.ts.map +1 -0
- package/dist/internal/yaml.js +20 -0
- package/dist/internal/yaml.js.map +1 -0
- package/dist/middleware.d.ts +8 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +1 -1
- package/src/Cli.test-d.ts +73 -0
- package/src/Cli.test.ts +1536 -48
- package/src/Cli.ts +606 -108
- package/src/Completions.test.ts +85 -0
- package/src/Completions.ts +48 -9
- package/src/Formatter.test.ts +17 -0
- package/src/Formatter.ts +7 -5
- package/src/Help.test.ts +32 -1
- package/src/Help.ts +42 -4
- package/src/Mcp.test.ts +289 -0
- package/src/Mcp.ts +68 -13
- package/src/Parser.test.ts +173 -0
- package/src/Parser.ts +132 -1
- package/src/Skill.test.ts +6 -0
- package/src/Skill.ts +8 -5
- package/src/Skillgen.test.ts +55 -6
- package/src/Skillgen.ts +9 -35
- package/src/SyncMcp.test.ts +89 -0
- package/src/SyncMcp.ts +72 -8
- package/src/SyncSkills.test.ts +78 -1
- package/src/SyncSkills.ts +20 -78
- package/src/bun-compile.test.ts +5 -0
- package/src/e2e.test.ts +166 -12
- package/src/internal/command.ts +12 -4
- package/src/internal/cta.ts +58 -0
- package/src/internal/json.ts +16 -0
- package/src/internal/yaml.ts +23 -0
- package/src/lazy-imports.test.ts +94 -0
- package/src/middleware.ts +12 -3
package/src/SyncSkills.ts
CHANGED
|
@@ -2,10 +2,11 @@ import fsSync from 'node:fs'
|
|
|
2
2
|
import fs from 'node:fs/promises'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import { parse as yamlParse } from 'yaml'
|
|
6
5
|
|
|
7
|
-
import {
|
|
6
|
+
import { collectSkillCommands, parseSkillFrontmatter } from './Cli.js'
|
|
8
7
|
import * as Agents from './internal/agents.js'
|
|
8
|
+
import * as Yaml from './internal/yaml.js'
|
|
9
|
+
import type * as Mcp from './Mcp.js'
|
|
9
10
|
import * as Skill from './Skill.js'
|
|
10
11
|
|
|
11
12
|
/** Generates skill files from a command map and installs them natively. */
|
|
@@ -17,9 +18,12 @@ export async function sync(
|
|
|
17
18
|
const { depth = 1, description, global = true } = options
|
|
18
19
|
const cwd = options.cwd ?? (global ? resolvePackageRoot() : process.cwd())
|
|
19
20
|
|
|
21
|
+
// Pre-load yaml for the sync call paths below (`Skill.split`, `parseFrontmatter`).
|
|
22
|
+
await Yaml.load()
|
|
23
|
+
|
|
20
24
|
const groups = new Map<string, string>()
|
|
21
25
|
if (description) groups.set(name, description)
|
|
22
|
-
const entries =
|
|
26
|
+
const entries = collectSkillCommands(commands, [], groups, options.rootCommand)
|
|
23
27
|
const files = Skill.split(name, entries, depth, groups)
|
|
24
28
|
|
|
25
29
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `incur-skills-${name}-`))
|
|
@@ -31,7 +35,7 @@ export async function sync(
|
|
|
31
35
|
: path.join(tmpDir, 'SKILL.md')
|
|
32
36
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
33
37
|
await fs.writeFile(filePath, `${file.content}\n`)
|
|
34
|
-
const meta =
|
|
38
|
+
const meta = parseSkillFrontmatter(file.content)
|
|
35
39
|
skills.push({ name: meta.name ?? (file.dir || name), description: meta.description })
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -42,7 +46,7 @@ export async function sync(
|
|
|
42
46
|
for await (const match of fs.glob(globPattern, { cwd })) {
|
|
43
47
|
try {
|
|
44
48
|
const content = await fs.readFile(path.resolve(cwd, match), 'utf8')
|
|
45
|
-
const meta =
|
|
49
|
+
const meta = parseSkillFrontmatter(content)
|
|
46
50
|
const skillName =
|
|
47
51
|
pattern === '_root' ? (meta.name ?? name) : path.basename(path.dirname(match))
|
|
48
52
|
const dest = path.join(tmpDir, skillName, 'SKILL.md')
|
|
@@ -68,7 +72,7 @@ export async function sync(
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// Write skills hash + names for staleness detection
|
|
71
|
-
const hashEntries =
|
|
75
|
+
const hashEntries = collectSkillCommands(commands, [], new Map(), options.rootCommand)
|
|
72
76
|
writeMeta(
|
|
73
77
|
name,
|
|
74
78
|
Skill.hash(hashEntries),
|
|
@@ -100,8 +104,10 @@ export declare namespace sync {
|
|
|
100
104
|
| {
|
|
101
105
|
description?: string | undefined
|
|
102
106
|
args?: any
|
|
107
|
+
destructive?: boolean | undefined
|
|
103
108
|
env?: any
|
|
104
109
|
hint?: string | undefined
|
|
110
|
+
mcp?: { annotations?: Mcp.ToolAnnotations | undefined } | undefined
|
|
105
111
|
options?: any
|
|
106
112
|
output?: any
|
|
107
113
|
examples?: any[] | undefined
|
|
@@ -137,16 +143,19 @@ export async function list(
|
|
|
137
143
|
const { depth = 1, description } = options
|
|
138
144
|
const cwd = options.cwd ?? process.cwd()
|
|
139
145
|
|
|
146
|
+
// Pre-load yaml for the sync call paths below (`Skill.split`, `parseFrontmatter`).
|
|
147
|
+
await Yaml.load()
|
|
148
|
+
|
|
140
149
|
const groups = new Map<string, string>()
|
|
141
150
|
if (description) groups.set(name, description)
|
|
142
|
-
const entries =
|
|
151
|
+
const entries = collectSkillCommands(commands, [], groups, options.rootCommand)
|
|
143
152
|
const files = Skill.split(name, entries, depth, groups)
|
|
144
153
|
|
|
145
154
|
const skills: list.Skill[] = []
|
|
146
155
|
const installed = readInstalledSkills(name, { cwd })
|
|
147
156
|
|
|
148
157
|
for (const file of files) {
|
|
149
|
-
const meta =
|
|
158
|
+
const meta = parseSkillFrontmatter(file.content)
|
|
150
159
|
const skillName = meta.name ?? (file.dir || name)
|
|
151
160
|
skills.push({
|
|
152
161
|
name: skillName,
|
|
@@ -162,7 +171,7 @@ export async function list(
|
|
|
162
171
|
for await (const match of fs.glob(globPattern, { cwd })) {
|
|
163
172
|
try {
|
|
164
173
|
const content = await fs.readFile(path.resolve(cwd, match), 'utf8')
|
|
165
|
-
const meta =
|
|
174
|
+
const meta = parseSkillFrontmatter(content)
|
|
166
175
|
const skillName =
|
|
167
176
|
pattern === '_root' ? (meta.name ?? name) : path.basename(path.dirname(match))
|
|
168
177
|
if (!skills.some((s) => s.name === skillName)) {
|
|
@@ -204,8 +213,10 @@ export declare namespace list {
|
|
|
204
213
|
| {
|
|
205
214
|
description?: string | undefined
|
|
206
215
|
args?: any
|
|
216
|
+
destructive?: boolean | undefined
|
|
207
217
|
env?: any
|
|
208
218
|
hint?: string | undefined
|
|
219
|
+
mcp?: { annotations?: Mcp.ToolAnnotations | undefined } | undefined
|
|
209
220
|
options?: any
|
|
210
221
|
output?: any
|
|
211
222
|
examples?: any[] | undefined
|
|
@@ -223,75 +234,6 @@ export declare namespace list {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
|
|
226
|
-
/** Recursively collects leaf commands as `Skill.CommandInfo`. */
|
|
227
|
-
function collectEntries(
|
|
228
|
-
commands: Map<string, any>,
|
|
229
|
-
prefix: string[],
|
|
230
|
-
groups: Map<string, string> = new Map(),
|
|
231
|
-
rootCommand?:
|
|
232
|
-
| {
|
|
233
|
-
description?: string | undefined
|
|
234
|
-
args?: any
|
|
235
|
-
env?: any
|
|
236
|
-
hint?: string | undefined
|
|
237
|
-
options?: any
|
|
238
|
-
output?: any
|
|
239
|
-
examples?: any[] | undefined
|
|
240
|
-
}
|
|
241
|
-
| undefined,
|
|
242
|
-
): Skill.CommandInfo[] {
|
|
243
|
-
const result: Skill.CommandInfo[] = []
|
|
244
|
-
if (rootCommand) {
|
|
245
|
-
const cmd: Skill.CommandInfo = {}
|
|
246
|
-
if (rootCommand.description) cmd.description = rootCommand.description
|
|
247
|
-
if (rootCommand.args) cmd.args = rootCommand.args
|
|
248
|
-
if (rootCommand.env) cmd.env = rootCommand.env
|
|
249
|
-
if (rootCommand.hint) cmd.hint = rootCommand.hint
|
|
250
|
-
if (rootCommand.options) cmd.options = rootCommand.options
|
|
251
|
-
if (rootCommand.output) cmd.output = rootCommand.output
|
|
252
|
-
const examples = formatExamples(rootCommand.examples)
|
|
253
|
-
if (examples) cmd.examples = examples
|
|
254
|
-
result.push(cmd)
|
|
255
|
-
}
|
|
256
|
-
for (const [name, entry] of commands) {
|
|
257
|
-
const entryPath = [...prefix, name]
|
|
258
|
-
if ('_group' in entry && entry._group) {
|
|
259
|
-
if (entry.description) groups.set(entryPath.join(' '), entry.description)
|
|
260
|
-
result.push(...collectEntries(entry.commands, entryPath, groups))
|
|
261
|
-
} else {
|
|
262
|
-
const cmd: Skill.CommandInfo = { name: entryPath.join(' ') }
|
|
263
|
-
if (entry.description) cmd.description = entry.description
|
|
264
|
-
if (entry.args) cmd.args = entry.args
|
|
265
|
-
if (entry.env) cmd.env = entry.env
|
|
266
|
-
if (entry.hint) cmd.hint = entry.hint
|
|
267
|
-
if (entry.options) cmd.options = entry.options
|
|
268
|
-
if (entry.output) cmd.output = entry.output
|
|
269
|
-
const examples = formatExamples(entry.examples)
|
|
270
|
-
if (examples) {
|
|
271
|
-
const cmdName = entryPath.join(' ')
|
|
272
|
-
cmd.examples = examples.map((e) => ({
|
|
273
|
-
...e,
|
|
274
|
-
command: e.command ? `${cmdName} ${e.command}` : cmdName,
|
|
275
|
-
}))
|
|
276
|
-
}
|
|
277
|
-
result.push(cmd)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function parseFrontmatter(content: string): {
|
|
284
|
-
description?: string | undefined
|
|
285
|
-
name?: string | undefined
|
|
286
|
-
} {
|
|
287
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
288
|
-
if (!match) return {}
|
|
289
|
-
|
|
290
|
-
const meta = yamlParse(match[1]!)
|
|
291
|
-
if (!meta || typeof meta !== 'object') return {}
|
|
292
|
-
return meta as { description?: string | undefined; name?: string | undefined }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
237
|
/** Resolves the package root from the executing bin script (`process.argv[1]`). Walks up from the bin's directory looking for `package.json`. Falls back to `process.cwd()`. */
|
|
296
238
|
function resolvePackageRoot(): string {
|
|
297
239
|
const bin = process.argv[1]
|
package/src/bun-compile.test.ts
CHANGED
|
@@ -70,6 +70,11 @@ cli.serve()
|
|
|
70
70
|
expect(stdout).toContain('result: HELLO')
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
test('runs command with --format yaml (lazy yaml import)', async () => {
|
|
74
|
+
const { stdout } = await exec(bin, ['ping', '--format', 'yaml'])
|
|
75
|
+
expect(stdout).toContain('pong: true')
|
|
76
|
+
})
|
|
77
|
+
|
|
73
78
|
test('shows help', async () => {
|
|
74
79
|
const { stdout } = await exec(bin, ['--help'])
|
|
75
80
|
expect(stdout).toContain('test-cli')
|
package/src/e2e.test.ts
CHANGED
|
@@ -984,7 +984,7 @@ describe('help', () => {
|
|
|
984
984
|
|
|
985
985
|
Integrations:
|
|
986
986
|
completions Generate shell completion script
|
|
987
|
-
mcp
|
|
987
|
+
mcp Register as MCP server (add, doctor)
|
|
988
988
|
skills Sync skill files to agents (add, list)
|
|
989
989
|
|
|
990
990
|
Global Options:
|
|
@@ -1513,9 +1513,9 @@ describe('--llms', () => {
|
|
|
1513
1513
|
|
|
1514
1514
|
| Command | Description |
|
|
1515
1515
|
|---------|-------------|
|
|
1516
|
-
| \`app auth
|
|
1517
|
-
| \`app auth
|
|
1518
|
-
| \`app auth
|
|
1516
|
+
| \`app auth login\` | Log in to the service |
|
|
1517
|
+
| \`app auth logout\` | Log out of the service |
|
|
1518
|
+
| \`app auth status\` | Show authentication status |
|
|
1519
1519
|
|
|
1520
1520
|
Run \`app auth --llms-full\` for full manifest. Run \`app auth <command> --schema\` for argument details.
|
|
1521
1521
|
"
|
|
@@ -1531,9 +1531,9 @@ describe('--llms', () => {
|
|
|
1531
1531
|
|
|
1532
1532
|
| Command | Description |
|
|
1533
1533
|
|---------|-------------|
|
|
1534
|
-
| \`app project deploy
|
|
1535
|
-
| \`app project deploy
|
|
1536
|
-
| \`app project deploy
|
|
1534
|
+
| \`app project deploy create <env>\` | Create a deployment |
|
|
1535
|
+
| \`app project deploy rollback <deployId>\` | Rollback a deployment |
|
|
1536
|
+
| \`app project deploy status <deployId>\` | Check deployment status |
|
|
1537
1537
|
|
|
1538
1538
|
Run \`app project deploy --llms-full\` for full manifest. Run \`app project deploy <command> --schema\` for argument details.
|
|
1539
1539
|
"
|
|
@@ -1759,7 +1759,7 @@ describe('root command with subcommands', () => {
|
|
|
1759
1759
|
|
|
1760
1760
|
Integrations:
|
|
1761
1761
|
completions Generate shell completion script
|
|
1762
|
-
mcp
|
|
1762
|
+
mcp Register as MCP server (add, doctor)
|
|
1763
1763
|
skills Sync skill files to agents (add, list)
|
|
1764
1764
|
|
|
1765
1765
|
Global Options:
|
|
@@ -2436,7 +2436,7 @@ describe('hosted OpenAPI CLI', () => {
|
|
|
2436
2436
|
|
|
2437
2437
|
Integrations:
|
|
2438
2438
|
completions Generate shell completion script
|
|
2439
|
-
mcp
|
|
2439
|
+
mcp Register as MCP server (add, doctor)
|
|
2440
2440
|
skills Sync skill files to agents (add, list)
|
|
2441
2441
|
|
|
2442
2442
|
Global Options:
|
|
@@ -2517,7 +2517,7 @@ describe('hosted OpenAPI CLI', () => {
|
|
|
2517
2517
|
|
|
2518
2518
|
Integrations:
|
|
2519
2519
|
completions Generate shell completion script
|
|
2520
|
-
mcp
|
|
2520
|
+
mcp Register as MCP server (add, doctor)
|
|
2521
2521
|
skills Sync skill files to agents (add, list)
|
|
2522
2522
|
|
|
2523
2523
|
Global Options:
|
|
@@ -2598,7 +2598,7 @@ describe('hosted OpenAPI CLI', () => {
|
|
|
2598
2598
|
})
|
|
2599
2599
|
})
|
|
2600
2600
|
|
|
2601
|
-
async function fetchJson(cli: Cli.Cli<any, any, any>, req: Request) {
|
|
2601
|
+
async function fetchJson(cli: Cli.Cli<any, any, any, any>, req: Request) {
|
|
2602
2602
|
const res = await cli.fetch(req)
|
|
2603
2603
|
const body = await res.json()
|
|
2604
2604
|
if (body.meta?.duration) body.meta.duration = '<stripped>'
|
|
@@ -2833,6 +2833,8 @@ describe('fetch api', () => {
|
|
|
2833
2833
|
.trim()
|
|
2834
2834
|
.split('\n')
|
|
2835
2835
|
.map((l) => JSON.parse(l))
|
|
2836
|
+
expect(lines[2].meta.duration).toMatch(/^\d+ms$/)
|
|
2837
|
+
lines[2].meta.duration = '<stripped>'
|
|
2836
2838
|
expect(lines).toMatchInlineSnapshot(`
|
|
2837
2839
|
[
|
|
2838
2840
|
{
|
|
@@ -2850,6 +2852,7 @@ describe('fetch api', () => {
|
|
|
2850
2852
|
{
|
|
2851
2853
|
"meta": {
|
|
2852
2854
|
"command": "stream",
|
|
2855
|
+
"duration": "<stripped>",
|
|
2853
2856
|
},
|
|
2854
2857
|
"ok": true,
|
|
2855
2858
|
"type": "done",
|
|
@@ -2947,7 +2950,7 @@ describe('fetch api', () => {
|
|
|
2947
2950
|
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
2948
2951
|
},
|
|
2949
2952
|
})
|
|
2950
|
-
const sessionId = res.headers.get('mcp-session-id')
|
|
2953
|
+
const sessionId = res.headers.get('mcp-session-id') ?? undefined
|
|
2951
2954
|
await mcpRequest(cli, { jsonrpc: '2.0', method: 'notifications/initialized' }, sessionId)
|
|
2952
2955
|
return sessionId
|
|
2953
2956
|
}
|
|
@@ -2965,6 +2968,7 @@ describe('fetch api', () => {
|
|
|
2965
2968
|
},
|
|
2966
2969
|
})
|
|
2967
2970
|
expect(res.status).toBe(200)
|
|
2971
|
+
expect(res.headers.get('mcp-session-id')).toBeNull()
|
|
2968
2972
|
const body = await res.json()
|
|
2969
2973
|
expect({
|
|
2970
2974
|
serverInfo: body.result.serverInfo,
|
|
@@ -2980,6 +2984,41 @@ describe('fetch api', () => {
|
|
|
2980
2984
|
`)
|
|
2981
2985
|
})
|
|
2982
2986
|
|
|
2987
|
+
test('tools/list works without session state', async () => {
|
|
2988
|
+
const cli = createApp()
|
|
2989
|
+
await mcpRequest(cli, {
|
|
2990
|
+
jsonrpc: '2.0',
|
|
2991
|
+
id: 1,
|
|
2992
|
+
method: 'initialize',
|
|
2993
|
+
params: {
|
|
2994
|
+
protocolVersion: '2025-03-26',
|
|
2995
|
+
capabilities: {},
|
|
2996
|
+
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
2997
|
+
},
|
|
2998
|
+
})
|
|
2999
|
+
const res = await mcpRequest(cli, {
|
|
3000
|
+
jsonrpc: '2.0',
|
|
3001
|
+
id: 2,
|
|
3002
|
+
method: 'tools/list',
|
|
3003
|
+
params: {},
|
|
3004
|
+
})
|
|
3005
|
+
expect(res.status).toBe(200)
|
|
3006
|
+
const body = await res.json()
|
|
3007
|
+
expect(body.result.tools.map((t: any) => t.name).sort()).toContain('ping')
|
|
3008
|
+
})
|
|
3009
|
+
|
|
3010
|
+
test('GET /mcp returns method not allowed in stateless mode', async () => {
|
|
3011
|
+
const cli = createApp()
|
|
3012
|
+
const res = await cli.fetch(
|
|
3013
|
+
new Request('http://localhost/mcp', {
|
|
3014
|
+
method: 'GET',
|
|
3015
|
+
headers: { accept: 'text/event-stream' },
|
|
3016
|
+
}),
|
|
3017
|
+
)
|
|
3018
|
+
expect(res.status).toBe(405)
|
|
3019
|
+
expect(res.headers.get('allow')).toBe('POST')
|
|
3020
|
+
})
|
|
3021
|
+
|
|
2983
3022
|
test('tools/list → lists all registered tools', async () => {
|
|
2984
3023
|
const cli = createApp()
|
|
2985
3024
|
const sessionId = await initSession(cli)
|
|
@@ -3172,6 +3211,71 @@ describe('fetch api', () => {
|
|
|
3172
3211
|
})
|
|
3173
3212
|
})
|
|
3174
3213
|
|
|
3214
|
+
describe('globals', () => {
|
|
3215
|
+
test('global flags flow through middleware for nested commands', async () => {
|
|
3216
|
+
const { output } = await serve(createGlobalsApp(), [
|
|
3217
|
+
'deploy',
|
|
3218
|
+
'status',
|
|
3219
|
+
'--api-token',
|
|
3220
|
+
'secret',
|
|
3221
|
+
'--profile',
|
|
3222
|
+
'production',
|
|
3223
|
+
'--format',
|
|
3224
|
+
'json',
|
|
3225
|
+
])
|
|
3226
|
+
expect(json(output)).toEqual({
|
|
3227
|
+
command: 'deploy status',
|
|
3228
|
+
profile: 'production',
|
|
3229
|
+
token: 'secret',
|
|
3230
|
+
})
|
|
3231
|
+
})
|
|
3232
|
+
|
|
3233
|
+
test('informational commands do not require required globals', async () => {
|
|
3234
|
+
const cli = createGlobalsApp()
|
|
3235
|
+
|
|
3236
|
+
const help = await serve(cli, ['--help'])
|
|
3237
|
+
expect(help.exitCode).toBeUndefined()
|
|
3238
|
+
expect(help.output).toContain('Custom Global Options:')
|
|
3239
|
+
expect(help.output).toContain('--api-token')
|
|
3240
|
+
|
|
3241
|
+
const schema = await serve(cli, ['whoami', '--schema', '--format', 'json'])
|
|
3242
|
+
expect(schema.exitCode).toBeUndefined()
|
|
3243
|
+
expect(json(schema.output).globals.properties.apiToken).toBeDefined()
|
|
3244
|
+
|
|
3245
|
+
const llms = await serve(cli, ['--llms', '--format', 'json'])
|
|
3246
|
+
expect(llms.exitCode).toBeUndefined()
|
|
3247
|
+
expect(json(llms.output).globals.properties.apiToken).toBeDefined()
|
|
3248
|
+
|
|
3249
|
+
const version = await serve(cli, ['--version'])
|
|
3250
|
+
expect(version.exitCode).toBeUndefined()
|
|
3251
|
+
expect(version.output).toBe('1.0.0\n')
|
|
3252
|
+
})
|
|
3253
|
+
|
|
3254
|
+
test('fetch requests expose globals to middleware and command query params to handlers', async () => {
|
|
3255
|
+
const result = await fetchJson(
|
|
3256
|
+
createGlobalsApp(),
|
|
3257
|
+
new Request('http://localhost/search?apiToken=secret&profile=staging&limit=2'),
|
|
3258
|
+
)
|
|
3259
|
+
expect(result).toMatchInlineSnapshot(`
|
|
3260
|
+
{
|
|
3261
|
+
"body": {
|
|
3262
|
+
"data": {
|
|
3263
|
+
"limit": 2,
|
|
3264
|
+
"profile": "staging",
|
|
3265
|
+
"token": "secret",
|
|
3266
|
+
},
|
|
3267
|
+
"meta": {
|
|
3268
|
+
"command": "search",
|
|
3269
|
+
"duration": "<stripped>",
|
|
3270
|
+
},
|
|
3271
|
+
"ok": true,
|
|
3272
|
+
},
|
|
3273
|
+
"status": 200,
|
|
3274
|
+
}
|
|
3275
|
+
`)
|
|
3276
|
+
})
|
|
3277
|
+
})
|
|
3278
|
+
|
|
3175
3279
|
describe('.well-known/skills', () => {
|
|
3176
3280
|
async function fetchSkills(cli: Cli.Cli<any, any, any>, path: string) {
|
|
3177
3281
|
const res = await cli.fetch(new Request(`http://localhost${path}`))
|
|
@@ -3358,6 +3462,56 @@ function createMiddlewareApp() {
|
|
|
3358
3462
|
return { cli, order }
|
|
3359
3463
|
}
|
|
3360
3464
|
|
|
3465
|
+
const globalsVars = z.object({
|
|
3466
|
+
apiToken: z.string().default(''),
|
|
3467
|
+
profile: z.string().default(''),
|
|
3468
|
+
})
|
|
3469
|
+
|
|
3470
|
+
function createGlobalsApp() {
|
|
3471
|
+
const deploy = Cli.create('deploy', {
|
|
3472
|
+
description: 'Deployment commands',
|
|
3473
|
+
vars: globalsVars,
|
|
3474
|
+
}).command('status', {
|
|
3475
|
+
description: 'Show deployment status',
|
|
3476
|
+
run(c) {
|
|
3477
|
+
return {
|
|
3478
|
+
command: 'deploy status',
|
|
3479
|
+
profile: c.var.profile,
|
|
3480
|
+
token: c.var.apiToken,
|
|
3481
|
+
}
|
|
3482
|
+
},
|
|
3483
|
+
})
|
|
3484
|
+
|
|
3485
|
+
return Cli.create('global-app', {
|
|
3486
|
+
version: '1.0.0',
|
|
3487
|
+
globals: z.object({
|
|
3488
|
+
apiToken: z.string().describe('API token'),
|
|
3489
|
+
profile: z.string().default('dev').describe('Profile name'),
|
|
3490
|
+
}),
|
|
3491
|
+
globalAlias: { apiToken: 't' },
|
|
3492
|
+
vars: globalsVars,
|
|
3493
|
+
})
|
|
3494
|
+
.use(async (c, next) => {
|
|
3495
|
+
c.set('apiToken', c.globals.apiToken)
|
|
3496
|
+
c.set('profile', c.globals.profile)
|
|
3497
|
+
await next()
|
|
3498
|
+
})
|
|
3499
|
+
.command('whoami', {
|
|
3500
|
+
description: 'Show active profile',
|
|
3501
|
+
run(c) {
|
|
3502
|
+
return { profile: c.var.profile, token: c.var.apiToken }
|
|
3503
|
+
},
|
|
3504
|
+
})
|
|
3505
|
+
.command('search', {
|
|
3506
|
+
description: 'Search resources',
|
|
3507
|
+
options: z.object({ limit: z.coerce.number().default(10) }),
|
|
3508
|
+
run(c) {
|
|
3509
|
+
return { limit: c.options.limit, profile: c.var.profile, token: c.var.apiToken }
|
|
3510
|
+
},
|
|
3511
|
+
})
|
|
3512
|
+
.command(deploy)
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3361
3515
|
function createApp() {
|
|
3362
3516
|
const auth = Cli.create('auth', { description: 'Authentication commands' })
|
|
3363
3517
|
.command('login', {
|
package/src/internal/command.ts
CHANGED
|
@@ -44,6 +44,7 @@ export async function execute(command: any, options: execute.Options): Promise<e
|
|
|
44
44
|
version,
|
|
45
45
|
envSource = process.env,
|
|
46
46
|
env: envSchema,
|
|
47
|
+
globals = {},
|
|
47
48
|
vars: varsSchema,
|
|
48
49
|
middlewares = [],
|
|
49
50
|
} = options
|
|
@@ -81,12 +82,12 @@ export async function execute(command: any, options: execute.Options): Promise<e
|
|
|
81
82
|
// HTTP mode: positional args from URL path segments, options from body/query
|
|
82
83
|
const parsed = Parser.parse(argv, { args: command.args })
|
|
83
84
|
args = parsed.args
|
|
84
|
-
parsedOptions = command.options ? command.options
|
|
85
|
+
parsedOptions = command.options ? Parser.zodParse(command.options, inputOptions) : {}
|
|
85
86
|
} else {
|
|
86
87
|
// MCP mode: all params come from inputOptions, split into args vs options
|
|
87
88
|
const split = splitParams(inputOptions, command)
|
|
88
|
-
args = command.args ? command.args
|
|
89
|
-
parsedOptions = command.options ? command.options
|
|
89
|
+
args = command.args ? Parser.zodParse(command.args, split.args) : {}
|
|
90
|
+
parsedOptions = command.options ? Parser.zodParse(command.options, split.options) : {}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
// Parse env
|
|
@@ -128,7 +129,7 @@ export async function execute(command: any, options: execute.Options): Promise<e
|
|
|
128
129
|
})
|
|
129
130
|
async function* wrapped() {
|
|
130
131
|
try {
|
|
131
|
-
yield* raw as AsyncGenerator<unknown, unknown, unknown>
|
|
132
|
+
return yield* raw as AsyncGenerator<unknown, unknown, unknown>
|
|
132
133
|
} finally {
|
|
133
134
|
resolveStreamConsumed!()
|
|
134
135
|
}
|
|
@@ -201,6 +202,7 @@ export async function execute(command: any, options: execute.Options): Promise<e
|
|
|
201
202
|
error: errorFn,
|
|
202
203
|
format: format as any,
|
|
203
204
|
formatExplicit,
|
|
205
|
+
globals,
|
|
204
206
|
name,
|
|
205
207
|
set(key: string, value: unknown) {
|
|
206
208
|
varsMap[key] = value
|
|
@@ -285,6 +287,8 @@ export declare namespace execute {
|
|
|
285
287
|
format: string
|
|
286
288
|
/** Whether the format was explicitly requested. */
|
|
287
289
|
formatExplicit: boolean
|
|
290
|
+
/** Parsed global options. Defaults to `{}` when not provided. */
|
|
291
|
+
globals?: Record<string, unknown> | undefined
|
|
288
292
|
/** Raw parsed options (from query params, JSON body, or MCP params). For CLI, pass `{}`. */
|
|
289
293
|
inputOptions: Record<string, unknown>
|
|
290
294
|
/** Middleware handlers (root + group + command, already collected). */
|
|
@@ -415,6 +419,10 @@ export const builtinCommands = [
|
|
|
415
419
|
noGlobal: z.boolean().optional().describe('Install to project instead of globally'),
|
|
416
420
|
}),
|
|
417
421
|
}),
|
|
422
|
+
subcommand({
|
|
423
|
+
name: 'doctor',
|
|
424
|
+
description: 'Validate MCP server startup and tool listing',
|
|
425
|
+
}),
|
|
418
426
|
],
|
|
419
427
|
},
|
|
420
428
|
{
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @internal A CTA block before command names are expanded. */
|
|
2
|
+
export type CtaBlock = {
|
|
3
|
+
commands: unknown[]
|
|
4
|
+
description?: string | undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** @internal A formatted CTA block as it appears in output metadata. */
|
|
8
|
+
export type FormattedCtaBlock = {
|
|
9
|
+
/** Formatted command suggestions. */
|
|
10
|
+
commands: FormattedCta[]
|
|
11
|
+
/** Human-readable label for the CTA block. */
|
|
12
|
+
description: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @internal A formatted CTA as it appears in output metadata. */
|
|
16
|
+
export type FormattedCta = {
|
|
17
|
+
/** The full command string with args and options folded in. */
|
|
18
|
+
command: string
|
|
19
|
+
/** A short description of what the command does. */
|
|
20
|
+
description?: string | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Cta =
|
|
24
|
+
| string
|
|
25
|
+
| {
|
|
26
|
+
args?: Record<string, unknown> | undefined
|
|
27
|
+
command: string
|
|
28
|
+
description?: string | undefined
|
|
29
|
+
options?: Record<string, unknown> | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @internal Formats a CTA block into the output metadata shape. */
|
|
33
|
+
export function formatCtaBlock(
|
|
34
|
+
name: string,
|
|
35
|
+
block: CtaBlock | undefined,
|
|
36
|
+
): FormattedCtaBlock | undefined {
|
|
37
|
+
if (!block || block.commands.length === 0) return undefined
|
|
38
|
+
return {
|
|
39
|
+
description:
|
|
40
|
+
block.description ??
|
|
41
|
+
(block.commands.length === 1 ? 'Suggested command:' : 'Suggested commands:'),
|
|
42
|
+
commands: block.commands.map((c) => formatCta(name, c as Cta)),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @internal Formats a CTA by prefixing the CLI name. */
|
|
47
|
+
function formatCta(name: string, cta: Cta): FormattedCta {
|
|
48
|
+
if (typeof cta === 'string') return { command: `${name} ${cta}` }
|
|
49
|
+
const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `
|
|
50
|
+
let cmd = `${prefix}${cta.command}`
|
|
51
|
+
if (cta.args)
|
|
52
|
+
for (const [key, value] of Object.entries(cta.args))
|
|
53
|
+
cmd += value === true ? ` <${key}>` : ` ${value}`
|
|
54
|
+
if (cta.options)
|
|
55
|
+
for (const [key, value] of Object.entries(cta.options))
|
|
56
|
+
cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`
|
|
57
|
+
return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) }
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Serializes JSON with BigInt values represented as decimal strings. */
|
|
2
|
+
export function stringify(value: unknown, space?: number): string {
|
|
3
|
+
return JSON.stringify(
|
|
4
|
+
value,
|
|
5
|
+
(_key, value) => {
|
|
6
|
+
if (typeof value === 'bigint') return value.toString()
|
|
7
|
+
return value
|
|
8
|
+
},
|
|
9
|
+
space,
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Converts a value to JSON-compatible data. */
|
|
14
|
+
export function normalize(value: unknown): unknown {
|
|
15
|
+
return JSON.parse(stringify(value))
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import type * as yaml from 'yaml'
|
|
3
|
+
|
|
4
|
+
/** @internal Cached `yaml` module, shared by `load()` and `loadSync()`. */
|
|
5
|
+
let cached: typeof yaml | undefined
|
|
6
|
+
|
|
7
|
+
/** Loads the `yaml` module on demand, so runs that never touch YAML don't pay its startup cost. */
|
|
8
|
+
export async function load(): Promise<typeof yaml> {
|
|
9
|
+
cached ??= await import('yaml')
|
|
10
|
+
return cached
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Synchronous variant of `load()` for sync call paths (e.g. `Formatter.format`).
|
|
15
|
+
*
|
|
16
|
+
* Falls back to `require` when `load()` hasn't run yet. Async paths should call `load()`
|
|
17
|
+
* first so environments without synchronous module resolution (e.g. compiled binaries,
|
|
18
|
+
* bundled workers) never hit the fallback.
|
|
19
|
+
*/
|
|
20
|
+
export function loadSync(): typeof yaml {
|
|
21
|
+
cached ??= createRequire(import.meta.url)('yaml') as typeof yaml
|
|
22
|
+
return cached
|
|
23
|
+
}
|