incur 0.4.8 → 0.4.9
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 +65 -19
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +294 -66
- 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 +25 -8
- 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 +2 -0
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +6 -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 +71 -0
- package/src/Cli.test.ts +1164 -65
- package/src/Cli.ts +456 -101
- package/src/Completions.test.ts +84 -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 +31 -0
- package/src/Help.ts +42 -4
- package/src/Mcp.test.ts +193 -0
- package/src/Mcp.ts +49 -8
- 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 +162 -8
- package/src/internal/command.ts +8 -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/e2e.test.ts
CHANGED
|
@@ -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
|
"
|
|
@@ -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). */
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
function exec(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
execFile(
|
|
9
|
+
cmd,
|
|
10
|
+
args,
|
|
11
|
+
{ cwd: join(import.meta.dirname, '..'), timeout: 30_000 },
|
|
12
|
+
(error, stdout, stderr) => {
|
|
13
|
+
if (error) reject(new Error(stderr?.trim() || stdout?.trim() || error.message))
|
|
14
|
+
else resolve({ stdout, stderr })
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let dir: string
|
|
21
|
+
let src: string
|
|
22
|
+
|
|
23
|
+
describe('lazy imports', () => {
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
dir = await mkdtemp(join(tmpdir(), 'incur-lazy-'))
|
|
26
|
+
src = join(dir, 'cli.mts')
|
|
27
|
+
|
|
28
|
+
// Records every module loaded during a CLI run, then prints the heavy ones.
|
|
29
|
+
await writeFile(
|
|
30
|
+
src,
|
|
31
|
+
`
|
|
32
|
+
import { registerHooks } from 'node:module'
|
|
33
|
+
|
|
34
|
+
const loaded: string[] = []
|
|
35
|
+
registerHooks({
|
|
36
|
+
resolve(specifier, context, next) {
|
|
37
|
+
const result = next(specifier, context)
|
|
38
|
+
loaded.push(result.url)
|
|
39
|
+
return result
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const { Cli, z } = await import('${join(import.meta.dirname, 'index.ts')}')
|
|
44
|
+
|
|
45
|
+
const cli = Cli.create('lazy-cli', { version: '1.0.0' })
|
|
46
|
+
|
|
47
|
+
cli.command('ping', {
|
|
48
|
+
description: 'Health check',
|
|
49
|
+
options: z.object({ upper: z.boolean().default(false) }),
|
|
50
|
+
run() {
|
|
51
|
+
return { pong: true }
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await cli.serve(process.argv.slice(2))
|
|
56
|
+
|
|
57
|
+
const heavy = loaded.filter(
|
|
58
|
+
(url) => url.includes('@modelcontextprotocol') || url.includes('node_modules/yaml'),
|
|
59
|
+
)
|
|
60
|
+
console.log(JSON.stringify({ heavy }))
|
|
61
|
+
`,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await rm(dir, { recursive: true, force: true })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function parseHeavy(stdout: string): string[] {
|
|
70
|
+
const line = stdout.trim().split('\n').at(-1)!
|
|
71
|
+
return (JSON.parse(line) as { heavy: string[] }).heavy
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test('plain command run does not load yaml or the MCP SDK', async () => {
|
|
75
|
+
const { stdout } = await exec(process.execPath, ['--import', 'tsx', src, 'ping'])
|
|
76
|
+
expect(stdout).toContain('pong: true')
|
|
77
|
+
expect(parseHeavy(stdout)).toEqual([])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('--format yaml loads yaml on demand, but not the MCP SDK', async () => {
|
|
81
|
+
const { stdout } = await exec(process.execPath, [
|
|
82
|
+
'--import',
|
|
83
|
+
'tsx',
|
|
84
|
+
src,
|
|
85
|
+
'ping',
|
|
86
|
+
'--format',
|
|
87
|
+
'yaml',
|
|
88
|
+
])
|
|
89
|
+
expect(stdout).toContain('pong: true')
|
|
90
|
+
const heavy = parseHeavy(stdout)
|
|
91
|
+
expect(heavy.some((url) => url.includes('node_modules/yaml'))).toBe(true)
|
|
92
|
+
expect(heavy.some((url) => url.includes('@modelcontextprotocol'))).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
})
|
package/src/middleware.ts
CHANGED
|
@@ -10,11 +10,16 @@ type InferVars<vars extends z.ZodObject<any> | undefined> =
|
|
|
10
10
|
type InferEnv<env extends z.ZodObject<any> | undefined> =
|
|
11
11
|
env extends z.ZodObject<any> ? z.output<env> : {}
|
|
12
12
|
|
|
13
|
+
/** @internal Infers the output type of a globals schema, or `{}` if undefined. */
|
|
14
|
+
type InferGlobals<globals extends z.ZodObject<any> | undefined> =
|
|
15
|
+
globals extends z.ZodObject<any> ? z.output<globals> : {}
|
|
16
|
+
|
|
13
17
|
/** Middleware handler that runs before/after command execution. */
|
|
14
18
|
export type Handler<
|
|
15
19
|
vars extends z.ZodObject<any> | undefined = undefined,
|
|
16
20
|
env extends z.ZodObject<any> | undefined = undefined,
|
|
17
|
-
|
|
21
|
+
globals extends z.ZodObject<any> | undefined = undefined,
|
|
22
|
+
> = (context: Context<vars, env, globals>, next: () => Promise<void>) => Promise<void> | void
|
|
18
23
|
|
|
19
24
|
/** CTA block for middleware error/ok responses. */
|
|
20
25
|
type CtaBlock = {
|
|
@@ -28,6 +33,7 @@ type CtaBlock = {
|
|
|
28
33
|
export type Context<
|
|
29
34
|
vars extends z.ZodObject<any> | undefined = undefined,
|
|
30
35
|
env extends z.ZodObject<any> | undefined = undefined,
|
|
36
|
+
globals extends z.ZodObject<any> | undefined = undefined,
|
|
31
37
|
> = {
|
|
32
38
|
/** Whether the consumer is an agent (stdout is not a TTY). */
|
|
33
39
|
agent: boolean
|
|
@@ -49,6 +55,8 @@ export type Context<
|
|
|
49
55
|
format: Formatter.Format
|
|
50
56
|
/** Whether the user explicitly passed `--format` or `--json`. */
|
|
51
57
|
formatExplicit: boolean
|
|
58
|
+
/** Parsed global options from the CLI-level globals schema. */
|
|
59
|
+
globals: InferGlobals<globals>
|
|
52
60
|
/** The CLI name. */
|
|
53
61
|
name: string
|
|
54
62
|
/** Set a typed variable for downstream middleware and handlers. */
|
|
@@ -59,10 +67,11 @@ export type Context<
|
|
|
59
67
|
version: string | undefined
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
/** Creates a strictly typed middleware handler. Pass the vars schema as a generic for typed `c.set()` and `c.var`, and the
|
|
70
|
+
/** Creates a strictly typed middleware handler. Pass the vars schema as a generic for typed `c.set()` and `c.var`, the env schema for typed `c.env`, and the globals schema for typed `c.globals`. */
|
|
63
71
|
export default function middleware<
|
|
64
72
|
const vars extends z.ZodObject<any> | undefined = undefined,
|
|
65
73
|
const env extends z.ZodObject<any> | undefined = undefined,
|
|
66
|
-
|
|
74
|
+
const globals extends z.ZodObject<any> | undefined = undefined,
|
|
75
|
+
>(handler: Handler<vars, env, globals>): Handler<vars, env, globals> {
|
|
67
76
|
return handler
|
|
68
77
|
}
|