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.
Files changed (88) hide show
  1. package/dist/Cli.d.ts +65 -19
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +294 -66
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/Completions.d.ts +2 -1
  6. package/dist/Completions.d.ts.map +1 -1
  7. package/dist/Completions.js +52 -13
  8. package/dist/Completions.js.map +1 -1
  9. package/dist/Formatter.d.ts.map +1 -1
  10. package/dist/Formatter.js +6 -5
  11. package/dist/Formatter.js.map +1 -1
  12. package/dist/Help.d.ts +5 -0
  13. package/dist/Help.d.ts.map +1 -1
  14. package/dist/Help.js +19 -5
  15. package/dist/Help.js.map +1 -1
  16. package/dist/Mcp.d.ts +21 -0
  17. package/dist/Mcp.d.ts.map +1 -1
  18. package/dist/Mcp.js +25 -8
  19. package/dist/Mcp.js.map +1 -1
  20. package/dist/Parser.d.ts +14 -0
  21. package/dist/Parser.d.ts.map +1 -1
  22. package/dist/Parser.js +127 -1
  23. package/dist/Parser.js.map +1 -1
  24. package/dist/Skill.d.ts.map +1 -1
  25. package/dist/Skill.js +5 -2
  26. package/dist/Skill.js.map +1 -1
  27. package/dist/Skillgen.d.ts.map +1 -1
  28. package/dist/Skillgen.js +4 -38
  29. package/dist/Skillgen.js.map +1 -1
  30. package/dist/SyncMcp.d.ts.map +1 -1
  31. package/dist/SyncMcp.js +75 -8
  32. package/dist/SyncMcp.js.map +1 -1
  33. package/dist/SyncSkills.d.ts +9 -0
  34. package/dist/SyncSkills.d.ts.map +1 -1
  35. package/dist/SyncSkills.js +13 -74
  36. package/dist/SyncSkills.js.map +1 -1
  37. package/dist/bin.d.ts +1 -1
  38. package/dist/bin.d.ts.map +1 -1
  39. package/dist/internal/command.d.ts +2 -0
  40. package/dist/internal/command.d.ts.map +1 -1
  41. package/dist/internal/command.js +6 -5
  42. package/dist/internal/command.js.map +1 -1
  43. package/dist/internal/cta.d.ts +22 -0
  44. package/dist/internal/cta.d.ts.map +1 -0
  45. package/dist/internal/cta.js +25 -0
  46. package/dist/internal/cta.js.map +1 -0
  47. package/dist/internal/json.d.ts +5 -0
  48. package/dist/internal/json.d.ts.map +1 -0
  49. package/dist/internal/json.js +13 -0
  50. package/dist/internal/json.js.map +1 -0
  51. package/dist/internal/yaml.d.ts +12 -0
  52. package/dist/internal/yaml.d.ts.map +1 -0
  53. package/dist/internal/yaml.js +20 -0
  54. package/dist/internal/yaml.js.map +1 -0
  55. package/dist/middleware.d.ts +8 -4
  56. package/dist/middleware.d.ts.map +1 -1
  57. package/dist/middleware.js +1 -1
  58. package/dist/middleware.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/Cli.test-d.ts +71 -0
  61. package/src/Cli.test.ts +1164 -65
  62. package/src/Cli.ts +456 -101
  63. package/src/Completions.test.ts +84 -0
  64. package/src/Completions.ts +48 -9
  65. package/src/Formatter.test.ts +17 -0
  66. package/src/Formatter.ts +7 -5
  67. package/src/Help.test.ts +31 -0
  68. package/src/Help.ts +42 -4
  69. package/src/Mcp.test.ts +193 -0
  70. package/src/Mcp.ts +49 -8
  71. package/src/Parser.test.ts +173 -0
  72. package/src/Parser.ts +132 -1
  73. package/src/Skill.test.ts +6 -0
  74. package/src/Skill.ts +8 -5
  75. package/src/Skillgen.test.ts +55 -6
  76. package/src/Skillgen.ts +9 -35
  77. package/src/SyncMcp.test.ts +89 -0
  78. package/src/SyncMcp.ts +72 -8
  79. package/src/SyncSkills.test.ts +78 -1
  80. package/src/SyncSkills.ts +20 -78
  81. package/src/bun-compile.test.ts +5 -0
  82. package/src/e2e.test.ts +162 -8
  83. package/src/internal/command.ts +8 -4
  84. package/src/internal/cta.ts +58 -0
  85. package/src/internal/json.ts +16 -0
  86. package/src/internal/yaml.ts +23 -0
  87. package/src/lazy-imports.test.ts +94 -0
  88. 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 auth login\` | Log in to the service |
1517
- | \`app auth auth logout\` | Log out of the service |
1518
- | \`app auth auth status\` | Show authentication status |
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 project deploy create <env>\` | Create a deployment |
1535
- | \`app project deploy project deploy rollback <deployId>\` | Rollback a deployment |
1536
- | \`app project deploy project deploy status <deployId>\` | Check deployment status |
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', {
@@ -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.parse(inputOptions) : {}
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.parse(split.args) : {}
89
- parsedOptions = command.options ? command.options.parse(split.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
- > = (context: Context<vars, env>, next: () => Promise<void>) => Promise<void> | void
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 env schema for typed `c.env`. */
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
- >(handler: Handler<vars, env>): Handler<vars, env> {
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
  }