incur 0.4.7 → 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/Openapi.d.ts +14 -0
- package/dist/Openapi.d.ts.map +1 -1
- package/dist/Openapi.js +85 -1
- package/dist/Openapi.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/Openapi.test.ts +179 -0
- package/src/Openapi.ts +122 -2
- 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/Mcp.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server'
|
|
2
1
|
import type { Readable, Writable } from 'node:stream'
|
|
3
2
|
import { z } from 'zod'
|
|
4
3
|
|
|
5
4
|
import * as Command from './internal/command.js'
|
|
5
|
+
import { formatCtaBlock, type FormattedCtaBlock } from './internal/cta.js'
|
|
6
|
+
import * as Json from './internal/json.js'
|
|
6
7
|
import type { Handler as MiddlewareHandler } from './middleware.js'
|
|
7
8
|
import * as Schema from './Schema.js'
|
|
8
9
|
|
|
@@ -13,7 +14,14 @@ export async function serve(
|
|
|
13
14
|
commands: Map<string, any>,
|
|
14
15
|
options: serve.Options = {},
|
|
15
16
|
): Promise<void> {
|
|
16
|
-
|
|
17
|
+
// Lazy: only runs when actually serving MCP, so plain command runs don't pay for the SDK import.
|
|
18
|
+
const { fromJsonSchema, McpServer, StdioServerTransport } =
|
|
19
|
+
await import('@modelcontextprotocol/server')
|
|
20
|
+
|
|
21
|
+
const server = new McpServer(
|
|
22
|
+
{ name, version },
|
|
23
|
+
options.instructions ? { instructions: options.instructions } : undefined,
|
|
24
|
+
)
|
|
17
25
|
|
|
18
26
|
for (const tool of collectTools(commands, [])) {
|
|
19
27
|
const mergedShape: Record<string, any> = {
|
|
@@ -27,7 +35,9 @@ export async function serve(
|
|
|
27
35
|
{
|
|
28
36
|
...(tool.description ? { description: tool.description } : undefined),
|
|
29
37
|
...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
|
|
30
|
-
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : undefined),
|
|
38
|
+
...(tool.outputSchema ? { outputSchema: fromJsonSchema(tool.outputSchema) } : undefined),
|
|
39
|
+
...(tool.annotations ? { annotations: tool.annotations } : undefined),
|
|
40
|
+
...(tool.instructions ? { _meta: { instructions: tool.instructions } } : undefined),
|
|
31
41
|
} as never,
|
|
32
42
|
async (...callArgs: any[]) => {
|
|
33
43
|
// registerTool passes (args, extra) when inputSchema is set, (extra) when not
|
|
@@ -67,6 +77,8 @@ export declare namespace serve {
|
|
|
67
77
|
vars?: z.ZodObject<any> | undefined
|
|
68
78
|
/** CLI version string. */
|
|
69
79
|
version?: string | undefined
|
|
80
|
+
/** Instructions describing how to use the server and its features. */
|
|
81
|
+
instructions?: string | undefined
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -88,6 +100,7 @@ export async function callTool(
|
|
|
88
100
|
): Promise<{
|
|
89
101
|
content: { type: 'text'; text: string }[]
|
|
90
102
|
structuredContent?: Record<string, unknown>
|
|
103
|
+
_meta?: { cta: FormattedCtaBlock } | undefined
|
|
91
104
|
isError?: boolean
|
|
92
105
|
}> {
|
|
93
106
|
const allMiddleware = [
|
|
@@ -122,7 +135,7 @@ export async function callTool(
|
|
|
122
135
|
if (progressToken !== undefined && options.sendNotification)
|
|
123
136
|
await options.sendNotification({
|
|
124
137
|
method: 'notifications/progress' as const,
|
|
125
|
-
params: { progressToken, progress: ++i, message:
|
|
138
|
+
params: { progressToken, progress: ++i, message: Json.stringify(chunk) },
|
|
126
139
|
})
|
|
127
140
|
}
|
|
128
141
|
} catch (err) {
|
|
@@ -131,21 +144,31 @@ export async function callTool(
|
|
|
131
144
|
isError: true,
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
|
-
return { content: [{ type: 'text', text:
|
|
147
|
+
return { content: [{ type: 'text', text: Json.stringify(chunks) }] }
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
if (!result.ok)
|
|
138
151
|
return {
|
|
139
|
-
content: [
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: result.error.fieldErrors
|
|
156
|
+
? JSON.stringify(result.error)
|
|
157
|
+
: (result.error.message ?? 'Command failed'),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
140
160
|
isError: true,
|
|
141
161
|
}
|
|
142
162
|
|
|
143
163
|
const data = result.data ?? null
|
|
164
|
+
const jsonData = Json.normalize(data)
|
|
165
|
+
const cta = formatCtaBlock(options.name ?? tool.name, result.cta as Command.CtaBlock | undefined)
|
|
144
166
|
return {
|
|
145
|
-
content: [{ type: 'text', text:
|
|
167
|
+
content: [{ type: 'text', text: Json.stringify(jsonData) }],
|
|
146
168
|
...(data !== null && tool.outputSchema
|
|
147
|
-
? { structuredContent:
|
|
169
|
+
? { structuredContent: jsonData as Record<string, unknown> }
|
|
148
170
|
: undefined),
|
|
171
|
+
...(cta ? { _meta: { cta } } : undefined),
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
@@ -161,10 +184,26 @@ export type ToolEntry = {
|
|
|
161
184
|
description?: string | undefined
|
|
162
185
|
inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[] }
|
|
163
186
|
outputSchema?: Record<string, unknown> | undefined
|
|
187
|
+
annotations?: ToolAnnotations | undefined
|
|
188
|
+
instructions?: string | undefined
|
|
164
189
|
command: any
|
|
165
190
|
middlewares?: MiddlewareHandler[] | undefined
|
|
166
191
|
}
|
|
167
192
|
|
|
193
|
+
/** MCP tool annotations that describe tool behavior to clients. */
|
|
194
|
+
export type ToolAnnotations = {
|
|
195
|
+
/** A human-readable title for the tool. */
|
|
196
|
+
title?: string | undefined
|
|
197
|
+
/** If true, the tool does not modify its environment. Default: false. */
|
|
198
|
+
readOnlyHint?: boolean | undefined
|
|
199
|
+
/** If true, the tool may perform destructive updates to its environment. Meaningful only when readOnlyHint is false. Default: true. */
|
|
200
|
+
destructiveHint?: boolean | undefined
|
|
201
|
+
/** If true, calling the tool repeatedly with the same arguments has no additional effect. Meaningful only when readOnlyHint is false. Default: false. */
|
|
202
|
+
idempotentHint?: boolean | undefined
|
|
203
|
+
/** If true, the tool may interact with an open world of external entities. Default: true. */
|
|
204
|
+
openWorldHint?: boolean | undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
168
207
|
/** @internal Recursively collects leaf commands as tool entries. */
|
|
169
208
|
export function collectTools(
|
|
170
209
|
commands: Map<string, any>,
|
|
@@ -189,6 +228,8 @@ export function collectTools(
|
|
|
189
228
|
...(entry.output
|
|
190
229
|
? { outputSchema: Schema.toJsonSchema(entry.output) as Record<string, unknown> }
|
|
191
230
|
: undefined),
|
|
231
|
+
...(entry.mcp?.annotations ? { annotations: entry.mcp.annotations } : undefined),
|
|
232
|
+
...(entry.mcp?.instructions ? { instructions: entry.mcp.instructions } : undefined),
|
|
192
233
|
command: entry,
|
|
193
234
|
...(parentMiddlewares.length > 0 ? { middlewares: parentMiddlewares } : undefined),
|
|
194
235
|
})
|
package/src/Openapi.test.ts
CHANGED
|
@@ -203,6 +203,69 @@ describe('cli integration', () => {
|
|
|
203
203
|
})
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
function createSecurityHeaderCli() {
|
|
207
|
+
return Cli.create('test', { description: 'test' }).command('api', {
|
|
208
|
+
fetch(request) {
|
|
209
|
+
return Response.json({ apiKey: request.headers.get('x-api-key') })
|
|
210
|
+
},
|
|
211
|
+
openapi: {
|
|
212
|
+
openapi: '3.0.0',
|
|
213
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
214
|
+
components: {
|
|
215
|
+
securitySchemes: {
|
|
216
|
+
tokenAuth: {
|
|
217
|
+
type: 'apiKey',
|
|
218
|
+
in: 'header',
|
|
219
|
+
name: 'x-api-key',
|
|
220
|
+
description: 'Access token',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
security: [{ tokenAuth: [] }],
|
|
225
|
+
paths: {
|
|
226
|
+
'/secret': {
|
|
227
|
+
get: {
|
|
228
|
+
operationId: 'getSecret',
|
|
229
|
+
summary: 'Get secret',
|
|
230
|
+
responses: { '200': { description: 'OK' } },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function createBearerAuthCli() {
|
|
239
|
+
return Cli.create('test', { description: 'test' }).command('api', {
|
|
240
|
+
fetch(request) {
|
|
241
|
+
return Response.json({ authorization: request.headers.get('authorization') })
|
|
242
|
+
},
|
|
243
|
+
openapi: {
|
|
244
|
+
openapi: '3.0.0',
|
|
245
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
246
|
+
components: {
|
|
247
|
+
securitySchemes: {
|
|
248
|
+
bearerAuth: {
|
|
249
|
+
type: 'http',
|
|
250
|
+
scheme: 'bearer',
|
|
251
|
+
description: 'Bearer credential',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
security: [{ bearerAuth: [] }],
|
|
256
|
+
paths: {
|
|
257
|
+
'/secret': {
|
|
258
|
+
get: {
|
|
259
|
+
operationId: 'getSecret',
|
|
260
|
+
summary: 'Get secret',
|
|
261
|
+
responses: { '200': { description: 'OK' } },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
206
269
|
test('GET /users via operationId', async () => {
|
|
207
270
|
const { output } = await serve(createCli(), ['api', 'listUsers'])
|
|
208
271
|
expect(output).toContain('Alice')
|
|
@@ -230,6 +293,122 @@ describe('cli integration', () => {
|
|
|
230
293
|
expect(json(output).limit).toBe(5)
|
|
231
294
|
})
|
|
232
295
|
|
|
296
|
+
test('security headers become generated command options', async () => {
|
|
297
|
+
const cli = createSecurityHeaderCli()
|
|
298
|
+
const { output } = await serve(cli, [
|
|
299
|
+
'api',
|
|
300
|
+
'getSecret',
|
|
301
|
+
'--x-api-key',
|
|
302
|
+
'secret',
|
|
303
|
+
'--format',
|
|
304
|
+
'json',
|
|
305
|
+
])
|
|
306
|
+
|
|
307
|
+
expect(json(output).apiKey).toMatchInlineSnapshot(`"secret"`)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('security header options appear in generated command help', async () => {
|
|
311
|
+
const { output } = await serve(createSecurityHeaderCli(), ['api', 'getSecret', '--help'])
|
|
312
|
+
|
|
313
|
+
expect(output).toMatchInlineSnapshot(`
|
|
314
|
+
"test api getSecret — Get secret
|
|
315
|
+
|
|
316
|
+
Usage: test api getSecret [options]
|
|
317
|
+
|
|
318
|
+
Options:
|
|
319
|
+
--x-api-key <string> Access token
|
|
320
|
+
|
|
321
|
+
Global Options:
|
|
322
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
323
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
324
|
+
--full-output Show full output envelope
|
|
325
|
+
--help Show help
|
|
326
|
+
--llms, --llms-full Print LLM-readable manifest
|
|
327
|
+
--schema Show JSON Schema for command
|
|
328
|
+
--token-count Print token count of output (instead of output)
|
|
329
|
+
--token-limit <n> Limit output to n tokens
|
|
330
|
+
--token-offset <n> Skip first n tokens of output
|
|
331
|
+
"
|
|
332
|
+
`)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('bearer auth becomes an authorization option', async () => {
|
|
336
|
+
const { output } = await serve(createBearerAuthCli(), [
|
|
337
|
+
'api',
|
|
338
|
+
'getSecret',
|
|
339
|
+
'--authorization',
|
|
340
|
+
'Bearer secret',
|
|
341
|
+
'--format',
|
|
342
|
+
'json',
|
|
343
|
+
])
|
|
344
|
+
|
|
345
|
+
expect(json(output).authorization).toMatchInlineSnapshot(`"Bearer secret"`)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('bearer auth option appears in generated command help', async () => {
|
|
349
|
+
const { output } = await serve(createBearerAuthCli(), ['api', 'getSecret', '--help'])
|
|
350
|
+
|
|
351
|
+
expect(output).toMatchInlineSnapshot(`
|
|
352
|
+
"test api getSecret — Get secret
|
|
353
|
+
|
|
354
|
+
Usage: test api getSecret [options]
|
|
355
|
+
|
|
356
|
+
Options:
|
|
357
|
+
--authorization <string> Bearer credential
|
|
358
|
+
|
|
359
|
+
Global Options:
|
|
360
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
361
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
362
|
+
--full-output Show full output envelope
|
|
363
|
+
--help Show help
|
|
364
|
+
--llms, --llms-full Print LLM-readable manifest
|
|
365
|
+
--schema Show JSON Schema for command
|
|
366
|
+
--token-count Print token count of output (instead of output)
|
|
367
|
+
--token-limit <n> Limit output to n tokens
|
|
368
|
+
--token-offset <n> Skip first n tokens of output
|
|
369
|
+
"
|
|
370
|
+
`)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('header parameters become generated command options', async () => {
|
|
374
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
375
|
+
fetch(request) {
|
|
376
|
+
return Response.json({ requestId: request.headers.get('x-request-id') })
|
|
377
|
+
},
|
|
378
|
+
openapi: {
|
|
379
|
+
openapi: '3.0.0',
|
|
380
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
381
|
+
paths: {
|
|
382
|
+
'/secret': {
|
|
383
|
+
get: {
|
|
384
|
+
operationId: 'getSecret',
|
|
385
|
+
summary: 'Get secret',
|
|
386
|
+
parameters: [
|
|
387
|
+
{
|
|
388
|
+
name: 'x-request-id',
|
|
389
|
+
in: 'header',
|
|
390
|
+
schema: { type: 'string' },
|
|
391
|
+
description: 'Request ID',
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
responses: { '200': { description: 'OK' } },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
const { output } = await serve(cli, [
|
|
401
|
+
'api',
|
|
402
|
+
'getSecret',
|
|
403
|
+
'--x-request-id',
|
|
404
|
+
'request_test',
|
|
405
|
+
'--format',
|
|
406
|
+
'json',
|
|
407
|
+
])
|
|
408
|
+
|
|
409
|
+
expect(json(output).requestId).toMatchInlineSnapshot(`"request_test"`)
|
|
410
|
+
})
|
|
411
|
+
|
|
233
412
|
test('loads OpenAPI commands from a spec URL string', async () => {
|
|
234
413
|
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
235
414
|
fetch: app.fetch,
|
package/src/Openapi.ts
CHANGED
|
@@ -12,7 +12,17 @@ import { dereference } from './internal/dereference.js'
|
|
|
12
12
|
import * as Schema from './Schema.js'
|
|
13
13
|
|
|
14
14
|
/** A minimal OpenAPI 3.x spec shape. Accepts both hand-written specs and generated ones (e.g. from `@hono/zod-openapi`). */
|
|
15
|
-
export type OpenAPISpec = {
|
|
15
|
+
export type OpenAPISpec = {
|
|
16
|
+
components?:
|
|
17
|
+
| {
|
|
18
|
+
securitySchemes?: Record<string, SecurityScheme> | undefined
|
|
19
|
+
}
|
|
20
|
+
| undefined
|
|
21
|
+
info?: Record<string, unknown> | undefined
|
|
22
|
+
openapi?: string | undefined
|
|
23
|
+
paths?: {} | undefined
|
|
24
|
+
security?: readonly SecurityRequirement[] | undefined
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
/** OpenAPI document source accepted by fetch-backed CLI commands. */
|
|
18
28
|
export type OpenAPISource = OpenAPISpec | string | URL
|
|
@@ -72,6 +82,7 @@ type Operation = {
|
|
|
72
82
|
parameters?: readonly Parameter[] | undefined
|
|
73
83
|
requestBody?: RequestBody | undefined
|
|
74
84
|
responses?: Record<string, unknown> | undefined
|
|
85
|
+
security?: readonly SecurityRequirement[] | undefined
|
|
75
86
|
summary?: string | undefined
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -88,6 +99,20 @@ type RequestBody = {
|
|
|
88
99
|
required?: boolean | undefined
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
type SecurityRequirement = Record<string, readonly string[]>
|
|
103
|
+
|
|
104
|
+
type SecurityScheme = {
|
|
105
|
+
description?: string | undefined
|
|
106
|
+
in?: 'cookie' | 'header' | 'query' | undefined
|
|
107
|
+
name?: string | undefined
|
|
108
|
+
scheme?: string | undefined
|
|
109
|
+
type?: string | undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type HeaderParameter = Parameter & {
|
|
113
|
+
optionName: string
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
/** A fetch handler. */
|
|
92
117
|
type FetchHandler = (req: Request) => Response | Promise<Response>
|
|
93
118
|
|
|
@@ -356,6 +381,10 @@ export async function generateCommands(
|
|
|
356
381
|
|
|
357
382
|
const pathParams = (op.parameters ?? []).filter((p) => p.in === 'path')
|
|
358
383
|
const queryParams = (op.parameters ?? []).filter((p) => p.in === 'query')
|
|
384
|
+
const headerParams = headerOptions([
|
|
385
|
+
...(op.parameters ?? []).filter((p) => p.in === 'header'),
|
|
386
|
+
...securityHeaderParams(resolved, op),
|
|
387
|
+
])
|
|
359
388
|
|
|
360
389
|
const bodySchema = op.requestBody?.content?.['application/json']?.schema
|
|
361
390
|
const bodyProps = (bodySchema?.properties ?? {}) as Record<string, Record<string, unknown>>
|
|
@@ -376,16 +405,28 @@ export async function generateCommands(
|
|
|
376
405
|
|
|
377
406
|
// Build options Zod schema from query params + body properties
|
|
378
407
|
const optShape: Record<string, z.ZodType> = {}
|
|
408
|
+
const usedOptionNames = new Set<string>()
|
|
379
409
|
for (const p of queryParams) {
|
|
380
410
|
let zodType = p.schema ? toZod(p.schema) : z.string()
|
|
381
411
|
if (!p.required) zodType = zodType.optional()
|
|
382
412
|
if (p.description) zodType = zodType.describe(p.description)
|
|
383
413
|
optShape[p.name] = coerceIfNeeded(zodType)
|
|
414
|
+
usedOptionNames.add(p.name)
|
|
384
415
|
}
|
|
385
416
|
for (const [key, schema] of Object.entries(bodyProps)) {
|
|
386
417
|
let zodType = toZod(schema)
|
|
387
418
|
if (!bodyRequired.has(key)) zodType = zodType.optional()
|
|
388
419
|
optShape[key] = zodType
|
|
420
|
+
usedOptionNames.add(key)
|
|
421
|
+
}
|
|
422
|
+
for (const p of headerParams) {
|
|
423
|
+
const optionName = resolveHeaderOptionName(p.optionName, usedOptionNames)
|
|
424
|
+
p.optionName = optionName
|
|
425
|
+
let zodType = p.schema ? toZod(p.schema) : z.string()
|
|
426
|
+
if (!p.required) zodType = zodType.optional()
|
|
427
|
+
zodType = zodType.describe(p.description ?? `${p.name} header`)
|
|
428
|
+
optShape[optionName] = coerceIfNeeded(zodType)
|
|
429
|
+
usedOptionNames.add(optionName)
|
|
389
430
|
}
|
|
390
431
|
const optionsSchema = Object.keys(optShape).length > 0 ? z.object(optShape) : undefined
|
|
391
432
|
|
|
@@ -398,6 +439,7 @@ export async function generateCommands(
|
|
|
398
439
|
fetch,
|
|
399
440
|
httpMethod,
|
|
400
441
|
path,
|
|
442
|
+
headerParams,
|
|
401
443
|
pathParams,
|
|
402
444
|
queryParams,
|
|
403
445
|
bodyProps,
|
|
@@ -438,6 +480,77 @@ function openapiOperations(paths: Record<string, Record<string, unknown>>) {
|
|
|
438
480
|
return operations
|
|
439
481
|
}
|
|
440
482
|
|
|
483
|
+
function securityHeaderParams(spec: OpenAPISpec, operation: Operation): Parameter[] {
|
|
484
|
+
const schemes = spec.components?.securitySchemes ?? {}
|
|
485
|
+
const requirements = operation.security ?? spec.security ?? []
|
|
486
|
+
const headers: Parameter[] = []
|
|
487
|
+
|
|
488
|
+
for (const requirement of requirements)
|
|
489
|
+
for (const name of Object.keys(requirement)) {
|
|
490
|
+
const scheme = schemes[name]
|
|
491
|
+
const parameter = securityHeaderParam(name, scheme)
|
|
492
|
+
if (parameter) headers.push(parameter)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return headers
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function securityHeaderParam(
|
|
499
|
+
name: string,
|
|
500
|
+
scheme: SecurityScheme | undefined,
|
|
501
|
+
): Parameter | undefined {
|
|
502
|
+
if (!scheme) return undefined
|
|
503
|
+
// `apiKey` is OpenAPI's generic name for a credential carried in a
|
|
504
|
+
// header/query/cookie, not an incur- or Cadent-specific API key concept.
|
|
505
|
+
if (scheme.type === 'apiKey' && scheme.in === 'header' && scheme.name)
|
|
506
|
+
return {
|
|
507
|
+
description: scheme.description ?? `${scheme.name} header`,
|
|
508
|
+
in: 'header',
|
|
509
|
+
name: scheme.name,
|
|
510
|
+
required: false,
|
|
511
|
+
schema: { type: 'string' },
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (scheme.type === 'http' && authorizationSchemes.has(scheme.scheme?.toLowerCase() ?? ''))
|
|
515
|
+
return {
|
|
516
|
+
description: scheme.description ?? `${name} authorization header`,
|
|
517
|
+
in: 'header',
|
|
518
|
+
name: 'authorization',
|
|
519
|
+
required: false,
|
|
520
|
+
schema: { type: 'string' },
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return undefined
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const authorizationSchemes = new Set(['basic', 'bearer'])
|
|
527
|
+
|
|
528
|
+
function headerOptions(parameters: Parameter[]): HeaderParameter[] {
|
|
529
|
+
const seen = new Set<string>()
|
|
530
|
+
const headers: HeaderParameter[] = []
|
|
531
|
+
|
|
532
|
+
for (const parameter of parameters) {
|
|
533
|
+
const normalized = parameter.name.toLowerCase()
|
|
534
|
+
if (seen.has(normalized)) continue
|
|
535
|
+
seen.add(normalized)
|
|
536
|
+
headers.push({ ...parameter, optionName: normalized })
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return headers
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function resolveHeaderOptionName(optionName: string, used: Set<string>) {
|
|
543
|
+
if (!used.has(optionName)) return optionName
|
|
544
|
+
|
|
545
|
+
const prefix = `header-${optionName}`
|
|
546
|
+
if (!used.has(prefix)) return prefix
|
|
547
|
+
|
|
548
|
+
for (let index = 2; ; index++) {
|
|
549
|
+
const candidate = `${prefix}-${index}`
|
|
550
|
+
if (!used.has(candidate)) return candidate
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
441
554
|
function getNamespaceInfo(operations: OperationEntry[]) {
|
|
442
555
|
const pathOperations = new Map<string, number>()
|
|
443
556
|
const parentPaths = new Set<string>()
|
|
@@ -565,6 +678,7 @@ function createHandler(config: {
|
|
|
565
678
|
basePath?: string | undefined
|
|
566
679
|
bodyProps: Record<string, Record<string, unknown>>
|
|
567
680
|
fetch: FetchHandler
|
|
681
|
+
headerParams: HeaderParameter[]
|
|
568
682
|
httpMethod: string
|
|
569
683
|
path: string
|
|
570
684
|
pathParams: Parameter[]
|
|
@@ -604,7 +718,13 @@ function createHandler(config: {
|
|
|
604
718
|
query,
|
|
605
719
|
}
|
|
606
720
|
|
|
607
|
-
|
|
721
|
+
for (const p of config.headerParams) {
|
|
722
|
+
const value = options[p.optionName]
|
|
723
|
+
if (value !== undefined) input.headers.set(p.name, String(value))
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (body && !input.headers.has('content-type'))
|
|
727
|
+
input.headers.set('content-type', 'application/json')
|
|
608
728
|
|
|
609
729
|
const request = Fetch.buildRequest(input)
|
|
610
730
|
const response = await config.fetch(request)
|
package/src/Parser.test.ts
CHANGED
|
@@ -376,3 +376,176 @@ describe('parse', () => {
|
|
|
376
376
|
expect(result.options).toEqual({ min: 1, max: 3 })
|
|
377
377
|
})
|
|
378
378
|
})
|
|
379
|
+
|
|
380
|
+
describe('parseGlobals', () => {
|
|
381
|
+
test('extracts known globals and returns rest', () => {
|
|
382
|
+
const schema = z.object({ rpcUrl: z.string() })
|
|
383
|
+
const result = Parser.parseGlobals(['--rpc-url', 'http://example.com', 'deploy'], schema)
|
|
384
|
+
expect(result.parsed).toEqual({ rpcUrl: 'http://example.com' })
|
|
385
|
+
expect(result.rest).toEqual(['deploy'])
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('unknown flags pass through to rest', () => {
|
|
389
|
+
const schema = z.object({ rpcUrl: z.string() })
|
|
390
|
+
const result = Parser.parseGlobals(
|
|
391
|
+
['--rpc-url', 'http://example.com', '--unknown', 'val', 'deploy'],
|
|
392
|
+
schema,
|
|
393
|
+
)
|
|
394
|
+
expect(result.parsed).toEqual({ rpcUrl: 'http://example.com' })
|
|
395
|
+
expect(result.rest).toEqual(['--unknown', 'val', 'deploy'])
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('handles --flag=value syntax', () => {
|
|
399
|
+
const schema = z.object({ rpcUrl: z.string() })
|
|
400
|
+
const result = Parser.parseGlobals(['--rpc-url=http://example.com', 'deploy'], schema)
|
|
401
|
+
expect(result.parsed).toEqual({ rpcUrl: 'http://example.com' })
|
|
402
|
+
expect(result.rest).toEqual(['deploy'])
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('handles short aliases', () => {
|
|
406
|
+
const schema = z.object({ rpcUrl: z.string() })
|
|
407
|
+
const result = Parser.parseGlobals(
|
|
408
|
+
['-r', 'http://example.com', 'deploy'],
|
|
409
|
+
schema,
|
|
410
|
+
{ rpcUrl: 'r' },
|
|
411
|
+
)
|
|
412
|
+
expect(result.parsed).toEqual({ rpcUrl: 'http://example.com' })
|
|
413
|
+
expect(result.rest).toEqual(['deploy'])
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('handles boolean globals', () => {
|
|
417
|
+
const schema = z.object({ verbose: z.boolean().default(false) })
|
|
418
|
+
const result = Parser.parseGlobals(['--verbose', 'deploy'], schema)
|
|
419
|
+
expect(result.parsed).toEqual({ verbose: true })
|
|
420
|
+
expect(result.rest).toEqual(['deploy'])
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('validates against schema', () => {
|
|
424
|
+
const schema = z.object({ count: z.number() })
|
|
425
|
+
expect(() => Parser.parseGlobals(['--count', 'not-a-number'], schema)).toThrow()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test('coerces string to number', () => {
|
|
429
|
+
const schema = z.object({ limit: z.number() })
|
|
430
|
+
const result = Parser.parseGlobals(['--limit', '42', 'deploy'], schema)
|
|
431
|
+
expect(result.parsed).toEqual({ limit: 42 })
|
|
432
|
+
expect(result.rest).toEqual(['deploy'])
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('positionals pass through to rest', () => {
|
|
436
|
+
const schema = z.object({ verbose: z.boolean().default(false) })
|
|
437
|
+
const result = Parser.parseGlobals(['deploy', 'contract', '--verbose'], schema)
|
|
438
|
+
expect(result.parsed).toEqual({ verbose: true })
|
|
439
|
+
expect(result.rest).toEqual(['deploy', 'contract'])
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('-- separator: everything after -- passes through to rest including the --', () => {
|
|
443
|
+
const schema = z.object({ verbose: z.boolean().default(false) })
|
|
444
|
+
const result = Parser.parseGlobals(
|
|
445
|
+
['--verbose', '--', '--unknown', 'positional', '--also-unknown'],
|
|
446
|
+
schema,
|
|
447
|
+
)
|
|
448
|
+
expect(result.parsed).toEqual({ verbose: true })
|
|
449
|
+
expect(result.rest).toEqual(['--', '--unknown', 'positional', '--also-unknown'])
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
test('stacked short aliases: -rv where both are known boolean globals', () => {
|
|
453
|
+
const schema = z.object({
|
|
454
|
+
recursive: z.boolean().default(false),
|
|
455
|
+
verbose: z.boolean().default(false),
|
|
456
|
+
})
|
|
457
|
+
const result = Parser.parseGlobals(['-rv', 'deploy'], schema, {
|
|
458
|
+
recursive: 'r',
|
|
459
|
+
verbose: 'v',
|
|
460
|
+
})
|
|
461
|
+
expect(result.parsed).toEqual({ recursive: true, verbose: true })
|
|
462
|
+
expect(result.rest).toEqual(['deploy'])
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('count options: --verbose --verbose accumulates', () => {
|
|
466
|
+
const schema = z.object({ verbose: z.number().default(0).meta({ count: true }) })
|
|
467
|
+
const result = Parser.parseGlobals(['--verbose', '--verbose', 'deploy'], schema)
|
|
468
|
+
expect(result.parsed).toEqual({ verbose: 2 })
|
|
469
|
+
expect(result.rest).toEqual(['deploy'])
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
test('array options: --tag foo --tag bar collects into array', () => {
|
|
473
|
+
const schema = z.object({ tag: z.array(z.string()).default([]) })
|
|
474
|
+
const result = Parser.parseGlobals(['--tag', 'foo', '--tag', 'bar', 'deploy'], schema)
|
|
475
|
+
expect(result.parsed).toEqual({ tag: ['foo', 'bar'] })
|
|
476
|
+
expect(result.rest).toEqual(['deploy'])
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test('unknown --no-* flags pass through to rest', () => {
|
|
480
|
+
const schema = z.object({ verbose: z.boolean().default(false) })
|
|
481
|
+
const result = Parser.parseGlobals(['--no-color', '--verbose'], schema)
|
|
482
|
+
expect(result.parsed).toEqual({ verbose: true })
|
|
483
|
+
expect(result.rest).toEqual(['--no-color'])
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
test('unknown --flag=value passes through as single token', () => {
|
|
487
|
+
const schema = z.object({ verbose: z.boolean().default(false) })
|
|
488
|
+
const result = Parser.parseGlobals(['--output=json', '--verbose'], schema)
|
|
489
|
+
expect(result.parsed).toEqual({ verbose: true })
|
|
490
|
+
expect(result.rest).toEqual(['--output=json'])
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test('missing value for known flag throws ParseError', () => {
|
|
494
|
+
const schema = z.object({ rpcUrl: z.string() })
|
|
495
|
+
expect(() => Parser.parseGlobals(['--rpc-url'], schema)).toThrow(
|
|
496
|
+
expect.objectContaining({ name: 'Incur.ParseError' }),
|
|
497
|
+
)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('stacked short: count in non-last position', () => {
|
|
501
|
+
const schema = z.object({
|
|
502
|
+
verbose: z.number().default(0).meta({ count: true }),
|
|
503
|
+
recursive: z.boolean().default(false),
|
|
504
|
+
})
|
|
505
|
+
const result = Parser.parseGlobals(['-vr'], schema, { verbose: 'v', recursive: 'r' })
|
|
506
|
+
expect(result.parsed).toEqual({ verbose: 1, recursive: true })
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test('stacked short: non-boolean in non-last position throws', () => {
|
|
510
|
+
const schema = z.object({
|
|
511
|
+
output: z.string(),
|
|
512
|
+
verbose: z.boolean().default(false),
|
|
513
|
+
})
|
|
514
|
+
expect(() => Parser.parseGlobals(['-ov', 'file'], schema, { output: 'o', verbose: 'v' })).toThrow(
|
|
515
|
+
/must be last/,
|
|
516
|
+
)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
test('short flag value-taking as last in stacked alias', () => {
|
|
520
|
+
const schema = z.object({
|
|
521
|
+
verbose: z.boolean().default(false),
|
|
522
|
+
output: z.string(),
|
|
523
|
+
})
|
|
524
|
+
const result = Parser.parseGlobals(['-vo', 'file', 'deploy'], schema, {
|
|
525
|
+
verbose: 'v',
|
|
526
|
+
output: 'o',
|
|
527
|
+
})
|
|
528
|
+
expect(result.parsed).toEqual({ verbose: true, output: 'file' })
|
|
529
|
+
expect(result.rest).toEqual(['deploy'])
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
test('short flag missing value throws ParseError', () => {
|
|
533
|
+
const schema = z.object({ output: z.string() })
|
|
534
|
+
expect(() => Parser.parseGlobals(['-o'], schema, { output: 'o' })).toThrow(
|
|
535
|
+
expect.objectContaining({ name: 'Incur.ParseError' }),
|
|
536
|
+
)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test('known --no- negation for boolean global', () => {
|
|
540
|
+
const schema = z.object({ verbose: z.boolean().default(true) })
|
|
541
|
+
const result = Parser.parseGlobals(['--no-verbose', 'deploy'], schema)
|
|
542
|
+
expect(result.parsed).toEqual({ verbose: false })
|
|
543
|
+
expect(result.rest).toEqual(['deploy'])
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
test('known --flag=value with setOption', () => {
|
|
547
|
+
const schema = z.object({ tag: z.array(z.string()).default([]) })
|
|
548
|
+
const result = Parser.parseGlobals(['--tag=foo', '--tag=bar'], schema)
|
|
549
|
+
expect(result.parsed).toEqual({ tag: ['foo', 'bar'] })
|
|
550
|
+
})
|
|
551
|
+
})
|