incur 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -26
- package/SKILL.md +17 -20
- package/dist/Cli.d.ts +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +132 -24
- package/dist/Cli.js.map +1 -1
- package/dist/Fetch.d.ts.map +1 -1
- package/dist/Fetch.js.map +1 -1
- package/dist/Filter.js.map +1 -1
- package/dist/Help.js +21 -7
- package/dist/Help.js.map +1 -1
- package/dist/Openapi.d.ts.map +1 -1
- package/dist/Openapi.js +10 -2
- package/dist/Openapi.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js.map +1 -1
- package/package.json +1 -1
- package/src/Cli.test.ts +13 -24
- package/src/Cli.ts +211 -63
- package/src/Fetch.test.ts +3 -13
- package/src/Fetch.ts +1 -2
- package/src/Filter.ts +7 -3
- package/src/Help.test.ts +22 -2
- package/src/Help.ts +19 -5
- package/src/Openapi.test.ts +89 -32
- package/src/Openapi.ts +15 -6
- package/src/Skill.test.ts +1 -3
- package/src/Skill.ts +9 -2
- package/src/e2e.test.ts +79 -39
package/src/Fetch.test.ts
CHANGED
|
@@ -66,13 +66,7 @@ describe('parseArgv', () => {
|
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
test('multiple headers', () => {
|
|
69
|
-
const input = Fetch.parseArgv([
|
|
70
|
-
'users',
|
|
71
|
-
'-H',
|
|
72
|
-
'X-A: 1',
|
|
73
|
-
'-H',
|
|
74
|
-
'X-B: 2',
|
|
75
|
-
])
|
|
69
|
+
const input = Fetch.parseArgv(['users', '-H', 'X-A: 1', '-H', 'X-B: 2'])
|
|
76
70
|
expect(input.headers.get('X-A')).toBe('1')
|
|
77
71
|
expect(input.headers.get('X-B')).toBe('2')
|
|
78
72
|
})
|
|
@@ -123,16 +117,12 @@ describe('buildRequest', () => {
|
|
|
123
117
|
})
|
|
124
118
|
|
|
125
119
|
test('builds POST request with body', () => {
|
|
126
|
-
const req = Fetch.buildRequest(
|
|
127
|
-
Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']),
|
|
128
|
-
)
|
|
120
|
+
const req = Fetch.buildRequest(Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']))
|
|
129
121
|
expect(req.method).toBe('POST')
|
|
130
122
|
})
|
|
131
123
|
|
|
132
124
|
test('builds request with headers', () => {
|
|
133
|
-
const req = Fetch.buildRequest(
|
|
134
|
-
Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']),
|
|
135
|
-
)
|
|
125
|
+
const req = Fetch.buildRequest(Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']))
|
|
136
126
|
expect(req.headers.get('X-Api-Key')).toBe('secret')
|
|
137
127
|
})
|
|
138
128
|
})
|
package/src/Fetch.ts
CHANGED
|
@@ -102,8 +102,7 @@ export function buildRequest(input: FetchInput): Request {
|
|
|
102
102
|
|
|
103
103
|
if (input.body !== undefined) {
|
|
104
104
|
init.body = input.body
|
|
105
|
-
if (!input.headers.has('content-type'))
|
|
106
|
-
input.headers.set('content-type', 'application/json')
|
|
105
|
+
if (!input.headers.has('content-type')) input.headers.set('content-type', 'application/json')
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
return new Request(url.toString(), init)
|
package/src/Filter.ts
CHANGED
|
@@ -97,7 +97,12 @@ function resolve(data: unknown, segments: Segment[], index: number): unknown {
|
|
|
97
97
|
return sliced.map((item) => resolve(item, segments, index + 1))
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function merge(
|
|
100
|
+
function merge(
|
|
101
|
+
target: Record<string, unknown>,
|
|
102
|
+
data: unknown,
|
|
103
|
+
segments: Segment[],
|
|
104
|
+
index: number,
|
|
105
|
+
): void {
|
|
101
106
|
if (index >= segments.length || typeof data !== 'object' || data === null) return
|
|
102
107
|
const segment = segments[index]!
|
|
103
108
|
|
|
@@ -129,8 +134,7 @@ function merge(target: Record<string, unknown>, data: unknown, segments: Segment
|
|
|
129
134
|
|
|
130
135
|
// Next segment is a key — recurse into nested object
|
|
131
136
|
if (typeof val !== 'object' || val === null) return
|
|
132
|
-
if (!target[segment.key] || typeof target[segment.key] !== 'object')
|
|
133
|
-
target[segment.key] = {}
|
|
137
|
+
if (!target[segment.key] || typeof target[segment.key] !== 'object') target[segment.key] = {}
|
|
134
138
|
merge(target[segment.key] as Record<string, unknown>, val, segments, index + 1)
|
|
135
139
|
return
|
|
136
140
|
}
|
package/src/Help.test.ts
CHANGED
|
@@ -99,6 +99,26 @@ describe('formatCommand', () => {
|
|
|
99
99
|
expect(result).toContain('Verbosity level')
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
test('shows enum values for z.enum options', () => {
|
|
103
|
+
const result = Help.formatCommand('tool deploy', {
|
|
104
|
+
options: z.object({
|
|
105
|
+
env: z.enum(['staging', 'production']).describe('Target environment'),
|
|
106
|
+
}),
|
|
107
|
+
})
|
|
108
|
+
expect(result).toContain('--env <staging|production>')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('shows literal values for z.union of z.literal options', () => {
|
|
112
|
+
const result = Help.formatCommand('tool deploy', {
|
|
113
|
+
options: z.object({
|
|
114
|
+
level: z
|
|
115
|
+
.union([z.literal('low'), z.literal('medium'), z.literal('high')])
|
|
116
|
+
.describe('Priority level'),
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
expect(result).toContain('--level <low|medium|high>')
|
|
120
|
+
})
|
|
121
|
+
|
|
102
122
|
test('shows [deprecated] tag for deprecated options', () => {
|
|
103
123
|
const result = Help.formatCommand('tool deploy', {
|
|
104
124
|
description: 'Deploy app',
|
|
@@ -179,9 +199,9 @@ describe('formatRoot', () => {
|
|
|
179
199
|
})
|
|
180
200
|
expect(result).toMatchInlineSnapshot(`
|
|
181
201
|
"my-tool@1.0.0 — A test CLI
|
|
182
|
-
Aliases: mt, myt
|
|
183
202
|
|
|
184
203
|
Usage: my-tool <command>
|
|
204
|
+
Aliases: mt, myt
|
|
185
205
|
|
|
186
206
|
Commands:
|
|
187
207
|
fetch Fetch a URL
|
|
@@ -208,9 +228,9 @@ describe('formatRoot', () => {
|
|
|
208
228
|
})
|
|
209
229
|
expect(result).toMatchInlineSnapshot(`
|
|
210
230
|
"my-tool@1.0.0 — A test CLI
|
|
211
|
-
Aliases: mt, myt
|
|
212
231
|
|
|
213
232
|
Usage: my-tool <url>
|
|
233
|
+
Aliases: mt, myt
|
|
214
234
|
|
|
215
235
|
Arguments:
|
|
216
236
|
url URL to fetch
|
package/src/Help.ts
CHANGED
|
@@ -8,11 +8,11 @@ export function formatRoot(name: string, options: formatRoot.Options = {}): stri
|
|
|
8
8
|
// Header
|
|
9
9
|
const title = version ? `${name}@${version}` : name
|
|
10
10
|
lines.push(description ? `${title} \u2014 ${description}` : title)
|
|
11
|
-
if (aliases?.length) lines.push(`Aliases: ${aliases.join(', ')}`)
|
|
12
11
|
lines.push('')
|
|
13
12
|
|
|
14
13
|
// Synopsis
|
|
15
14
|
lines.push(`Usage: ${name} <command>`)
|
|
15
|
+
if (aliases?.length) lines.push(`Aliases: ${aliases.join(', ')}`)
|
|
16
16
|
|
|
17
17
|
// Commands
|
|
18
18
|
if (commands.length > 0) {
|
|
@@ -105,7 +105,6 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
105
105
|
// Header
|
|
106
106
|
const title = version ? `${name}@${version}` : name
|
|
107
107
|
lines.push(description ? `${title} \u2014 ${description}` : title)
|
|
108
|
-
if (aliases?.length) lines.push(`Aliases: ${aliases.join(', ')}`)
|
|
109
108
|
lines.push('')
|
|
110
109
|
|
|
111
110
|
// Synopsis
|
|
@@ -128,6 +127,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
128
127
|
const commandSuffix = options.commands && options.commands.length > 0 ? ' | <command>' : ''
|
|
129
128
|
lines.push(`Usage: ${synopsis}${opts ? ' [options]' : ''}${commandSuffix}`)
|
|
130
129
|
}
|
|
130
|
+
if (aliases?.length) lines.push(`Aliases: ${aliases.join(', ')}`)
|
|
131
131
|
|
|
132
132
|
// Arguments
|
|
133
133
|
if (args) {
|
|
@@ -165,10 +165,10 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
165
165
|
lines.push('')
|
|
166
166
|
lines.push('Examples:')
|
|
167
167
|
const maxLen = Math.max(
|
|
168
|
-
...examples.map((e) => (e.command ? `$
|
|
168
|
+
...examples.map((e) => (e.command ? `${name} ${e.command}` : name).length),
|
|
169
169
|
)
|
|
170
170
|
for (const ex of examples) {
|
|
171
|
-
const cmd = ex.command ? `$
|
|
171
|
+
const cmd = ex.command ? `${name} ${ex.command}` : name
|
|
172
172
|
if (ex.description)
|
|
173
173
|
lines.push(` ${cmd}${' '.repeat(maxLen - cmd.length)} # ${ex.description}`)
|
|
174
174
|
else lines.push(` ${cmd}`)
|
|
@@ -274,6 +274,17 @@ function resolveTypeName(schema: unknown): string {
|
|
|
274
274
|
if (unwrapped instanceof z.ZodNumber) return 'number'
|
|
275
275
|
if (unwrapped instanceof z.ZodBoolean) return 'boolean'
|
|
276
276
|
if (unwrapped instanceof z.ZodArray) return 'array'
|
|
277
|
+
if (unwrapped instanceof z.ZodEnum) {
|
|
278
|
+
const values = Object.values((unwrapped as any)._zod.def.entries) as string[]
|
|
279
|
+
return values.join('|')
|
|
280
|
+
}
|
|
281
|
+
if (unwrapped instanceof z.ZodUnion) {
|
|
282
|
+
const options = (unwrapped as any)._zod?.def?.options as z.ZodType[] | undefined
|
|
283
|
+
if (options?.every((o: z.ZodType) => o instanceof z.ZodLiteral)) {
|
|
284
|
+
const values = options.map((o: z.ZodType) => String((o as any)._zod.def.values[0]))
|
|
285
|
+
return values.join('|')
|
|
286
|
+
}
|
|
287
|
+
}
|
|
277
288
|
return 'value'
|
|
278
289
|
}
|
|
279
290
|
|
|
@@ -333,7 +344,10 @@ function globalOptionsLines(root = false): string[] {
|
|
|
333
344
|
}
|
|
334
345
|
|
|
335
346
|
const flags = [
|
|
336
|
-
{
|
|
347
|
+
{
|
|
348
|
+
flag: '--filter-output <keys>',
|
|
349
|
+
desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])',
|
|
350
|
+
},
|
|
337
351
|
{ flag: '--format <toon|json|yaml|md|jsonl>', desc: 'Output format' },
|
|
338
352
|
{ flag: '--help', desc: 'Show help' },
|
|
339
353
|
{ flag: '--llms, --llms-full', desc: 'Print LLM-readable manifest' },
|
package/src/Openapi.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import * as Cli from './Cli.js'
|
|
4
|
-
import * as Openapi from './Openapi.js'
|
|
5
|
-
import { app } from '../test/fixtures/hono-api.js'
|
|
6
3
|
import { app as prefixedApp } from '../test/fixtures/hono-api-prefixed.js'
|
|
7
|
-
import {
|
|
4
|
+
import { app } from '../test/fixtures/hono-api.js'
|
|
8
5
|
import { app as openapiApp, spec as openapiSpec } from '../test/fixtures/hono-openapi-app.js'
|
|
6
|
+
import { spec } from '../test/fixtures/openapi-spec.js'
|
|
7
|
+
import * as Cli from './Cli.js'
|
|
8
|
+
import * as Openapi from './Openapi.js'
|
|
9
9
|
|
|
10
10
|
function serve(cli: { serve: Cli.Cli['serve'] }, argv: string[]) {
|
|
11
11
|
let output = ''
|
|
@@ -13,7 +13,9 @@ function serve(cli: { serve: Cli.Cli['serve'] }, argv: string[]) {
|
|
|
13
13
|
return cli
|
|
14
14
|
.serve(argv, {
|
|
15
15
|
stdout: (s) => (output += s),
|
|
16
|
-
exit: (c) => {
|
|
16
|
+
exit: (c) => {
|
|
17
|
+
exitCode = c
|
|
18
|
+
},
|
|
17
19
|
})
|
|
18
20
|
.then(() => ({
|
|
19
21
|
output,
|
|
@@ -22,9 +24,7 @@ function serve(cli: { serve: Cli.Cli['serve'] }, argv: string[]) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function json(output: string) {
|
|
25
|
-
return JSON.parse(
|
|
26
|
-
output.replace(/"duration": "[^"]+"/g, '"duration": "<stripped>"'),
|
|
27
|
-
)
|
|
27
|
+
return JSON.parse(output.replace(/"duration": "[^"]+"/g, '"duration": "<stripped>"'))
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
describe('generateCommands', () => {
|
|
@@ -46,8 +46,10 @@ describe('generateCommands', () => {
|
|
|
46
46
|
|
|
47
47
|
describe('cli integration', () => {
|
|
48
48
|
function createCli() {
|
|
49
|
-
return Cli.create('test', { description: 'test' })
|
|
50
|
-
|
|
49
|
+
return Cli.create('test', { description: 'test' }).command('api', {
|
|
50
|
+
fetch: app.fetch,
|
|
51
|
+
openapi: spec,
|
|
52
|
+
})
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
test('GET /users via operationId', async () => {
|
|
@@ -57,7 +59,12 @@ describe('cli integration', () => {
|
|
|
57
59
|
|
|
58
60
|
test('GET /users?limit=5 via options', async () => {
|
|
59
61
|
const { output } = await serve(createCli(), [
|
|
60
|
-
'api',
|
|
62
|
+
'api',
|
|
63
|
+
'listUsers',
|
|
64
|
+
'--limit',
|
|
65
|
+
'5',
|
|
66
|
+
'--format',
|
|
67
|
+
'json',
|
|
61
68
|
])
|
|
62
69
|
expect(json(output).limit).toBe(5)
|
|
63
70
|
})
|
|
@@ -125,7 +132,11 @@ describe('cli integration', () => {
|
|
|
125
132
|
|
|
126
133
|
test('--verbose wraps in envelope', async () => {
|
|
127
134
|
const { output } = await serve(createCli(), [
|
|
128
|
-
'api',
|
|
135
|
+
'api',
|
|
136
|
+
'healthCheck',
|
|
137
|
+
'--verbose',
|
|
138
|
+
'--format',
|
|
139
|
+
'json',
|
|
129
140
|
])
|
|
130
141
|
const parsed = json(output)
|
|
131
142
|
expect(parsed.ok).toBe(true)
|
|
@@ -141,8 +152,10 @@ describe('cli integration', () => {
|
|
|
141
152
|
|
|
142
153
|
describe('@hono/zod-openapi integration', () => {
|
|
143
154
|
function createCli() {
|
|
144
|
-
return Cli.create('test', { description: 'test' })
|
|
145
|
-
|
|
155
|
+
return Cli.create('test', { description: 'test' }).command('api', {
|
|
156
|
+
fetch: openapiApp.fetch,
|
|
157
|
+
openapi: openapiSpec,
|
|
158
|
+
})
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
test('GET /users via listUsers', async () => {
|
|
@@ -151,7 +164,14 @@ describe('@hono/zod-openapi integration', () => {
|
|
|
151
164
|
})
|
|
152
165
|
|
|
153
166
|
test('GET /users?limit=5', async () => {
|
|
154
|
-
const { output } = await serve(createCli(), [
|
|
167
|
+
const { output } = await serve(createCli(), [
|
|
168
|
+
'api',
|
|
169
|
+
'listUsers',
|
|
170
|
+
'--limit',
|
|
171
|
+
'5',
|
|
172
|
+
'--format',
|
|
173
|
+
'json',
|
|
174
|
+
])
|
|
155
175
|
expect(json(output).limit).toBe(5)
|
|
156
176
|
})
|
|
157
177
|
|
|
@@ -224,7 +244,11 @@ describe('@hono/zod-openapi integration', () => {
|
|
|
224
244
|
|
|
225
245
|
test('--verbose wraps in envelope', async () => {
|
|
226
246
|
const { output } = await serve(createCli(), [
|
|
227
|
-
'api',
|
|
247
|
+
'api',
|
|
248
|
+
'healthCheck',
|
|
249
|
+
'--verbose',
|
|
250
|
+
'--format',
|
|
251
|
+
'json',
|
|
228
252
|
])
|
|
229
253
|
const parsed = json(output)
|
|
230
254
|
expect(parsed.ok).toBe(true)
|
|
@@ -248,7 +272,15 @@ describe('@hono/zod-openapi integration', () => {
|
|
|
248
272
|
|
|
249
273
|
test('PUT /users/:id with optional boolean body option', async () => {
|
|
250
274
|
const { output } = await serve(createCli(), [
|
|
251
|
-
'api',
|
|
275
|
+
'api',
|
|
276
|
+
'updateUser',
|
|
277
|
+
'1',
|
|
278
|
+
'--name',
|
|
279
|
+
'Updated',
|
|
280
|
+
'--active',
|
|
281
|
+
'true',
|
|
282
|
+
'--format',
|
|
283
|
+
'json',
|
|
252
284
|
])
|
|
253
285
|
const parsed = json(output)
|
|
254
286
|
expect(parsed.id).toBe(1)
|
|
@@ -257,44 +289,63 @@ describe('@hono/zod-openapi integration', () => {
|
|
|
257
289
|
})
|
|
258
290
|
|
|
259
291
|
test('query param coercion with zod-openapi generated spec', async () => {
|
|
260
|
-
const { output } = await serve(createCli(), [
|
|
292
|
+
const { output } = await serve(createCli(), [
|
|
293
|
+
'api',
|
|
294
|
+
'listUsers',
|
|
295
|
+
'--limit',
|
|
296
|
+
'3',
|
|
297
|
+
'--format',
|
|
298
|
+
'json',
|
|
299
|
+
])
|
|
261
300
|
expect(json(output).limit).toBe(3)
|
|
262
301
|
})
|
|
263
302
|
})
|
|
264
303
|
|
|
265
304
|
describe('basePath', () => {
|
|
266
305
|
test('fetch gateway prepends basePath to request path', async () => {
|
|
267
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
268
|
-
|
|
306
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
307
|
+
fetch: prefixedApp.fetch,
|
|
308
|
+
basePath: '/api',
|
|
309
|
+
})
|
|
269
310
|
const { output } = await serve(cli, ['api', 'users'])
|
|
270
311
|
expect(output).toContain('Alice')
|
|
271
312
|
})
|
|
272
313
|
|
|
273
314
|
test('fetch gateway basePath with query params', async () => {
|
|
274
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
275
|
-
|
|
315
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
316
|
+
fetch: prefixedApp.fetch,
|
|
317
|
+
basePath: '/api',
|
|
318
|
+
})
|
|
276
319
|
const { output } = await serve(cli, ['api', 'users', '--limit', '5', '--format', 'json'])
|
|
277
320
|
expect(json(output).limit).toBe(5)
|
|
278
321
|
})
|
|
279
322
|
|
|
280
323
|
test('fetch gateway basePath with POST', async () => {
|
|
281
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
282
|
-
|
|
324
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
325
|
+
fetch: prefixedApp.fetch,
|
|
326
|
+
basePath: '/api',
|
|
327
|
+
})
|
|
283
328
|
const { output } = await serve(cli, ['api', 'users', '-X', 'POST', '-d', '{"name":"Bob"}'])
|
|
284
329
|
expect(output).toContain('Bob')
|
|
285
330
|
expect(output).toContain('created')
|
|
286
331
|
})
|
|
287
332
|
|
|
288
333
|
test('openapi with basePath prepends to spec paths', async () => {
|
|
289
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
290
|
-
|
|
334
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
335
|
+
fetch: prefixedApp.fetch,
|
|
336
|
+
openapi: spec,
|
|
337
|
+
basePath: '/api',
|
|
338
|
+
})
|
|
291
339
|
const { output } = await serve(cli, ['api', 'listUsers'])
|
|
292
340
|
expect(output).toContain('Alice')
|
|
293
341
|
})
|
|
294
342
|
|
|
295
343
|
test('openapi basePath with path params', async () => {
|
|
296
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
297
|
-
|
|
344
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
345
|
+
fetch: prefixedApp.fetch,
|
|
346
|
+
openapi: spec,
|
|
347
|
+
basePath: '/api',
|
|
348
|
+
})
|
|
298
349
|
const { output } = await serve(cli, ['api', 'getUser', '42'])
|
|
299
350
|
expect(output).toMatchInlineSnapshot(`
|
|
300
351
|
"id: 42
|
|
@@ -304,16 +355,22 @@ describe('basePath', () => {
|
|
|
304
355
|
})
|
|
305
356
|
|
|
306
357
|
test('openapi basePath with body options', async () => {
|
|
307
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
308
|
-
|
|
358
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
359
|
+
fetch: prefixedApp.fetch,
|
|
360
|
+
openapi: spec,
|
|
361
|
+
basePath: '/api',
|
|
362
|
+
})
|
|
309
363
|
const { output } = await serve(cli, ['api', 'createUser', '--name', 'Bob'])
|
|
310
364
|
expect(output).toContain('created')
|
|
311
365
|
expect(output).toContain('Bob')
|
|
312
366
|
})
|
|
313
367
|
|
|
314
368
|
test('openapi basePath with health check', async () => {
|
|
315
|
-
const cli = Cli.create('test', { description: 'test' })
|
|
316
|
-
|
|
369
|
+
const cli = Cli.create('test', { description: 'test' }).command('api', {
|
|
370
|
+
fetch: prefixedApp.fetch,
|
|
371
|
+
openapi: spec,
|
|
372
|
+
basePath: '/api',
|
|
373
|
+
})
|
|
317
374
|
const { output } = await serve(cli, ['api', 'healthCheck', '--format', 'json'])
|
|
318
375
|
expect(json(output)).toEqual({ ok: true })
|
|
319
376
|
})
|
package/src/Openapi.ts
CHANGED
|
@@ -46,7 +46,7 @@ export async function generateCommands(
|
|
|
46
46
|
fetch: FetchHandler,
|
|
47
47
|
options: { basePath?: string | undefined } = {},
|
|
48
48
|
): Promise<Map<string, GeneratedCommand>> {
|
|
49
|
-
const resolved = await dereference(structuredClone(spec) as any) as unknown as OpenAPISpec
|
|
49
|
+
const resolved = (await dereference(structuredClone(spec) as any)) as unknown as OpenAPISpec
|
|
50
50
|
const commands = new Map<string, GeneratedCommand>()
|
|
51
51
|
const paths = (resolved.paths ?? {}) as Record<string, Record<string, unknown>>
|
|
52
52
|
|
|
@@ -96,7 +96,15 @@ export async function generateCommands(
|
|
|
96
96
|
description: op.summary ?? op.description,
|
|
97
97
|
args: argsSchema,
|
|
98
98
|
options: optionsSchema,
|
|
99
|
-
run: createHandler({
|
|
99
|
+
run: createHandler({
|
|
100
|
+
basePath: options.basePath,
|
|
101
|
+
fetch,
|
|
102
|
+
httpMethod,
|
|
103
|
+
path,
|
|
104
|
+
pathParams,
|
|
105
|
+
queryParams,
|
|
106
|
+
bodyProps,
|
|
107
|
+
}),
|
|
100
108
|
})
|
|
101
109
|
}
|
|
102
110
|
}
|
|
@@ -135,8 +143,7 @@ function createHandler(config: {
|
|
|
135
143
|
const bodyKeys = Object.keys(config.bodyProps)
|
|
136
144
|
if (bodyKeys.length > 0) {
|
|
137
145
|
const bodyObj: Record<string, unknown> = {}
|
|
138
|
-
for (const key of bodyKeys)
|
|
139
|
-
if (options[key] !== undefined) bodyObj[key] = options[key]
|
|
146
|
+
for (const key of bodyKeys) if (options[key] !== undefined) bodyObj[key] = options[key]
|
|
140
147
|
if (Object.keys(bodyObj).length > 0) body = JSON.stringify(bodyObj)
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -180,8 +187,10 @@ function coerceIfNeeded(schema: z.ZodType): z.ZodType {
|
|
|
180
187
|
const inner = isOptional ? schema.unwrap() : schema
|
|
181
188
|
|
|
182
189
|
// Direct number/boolean
|
|
183
|
-
if (inner instanceof z.ZodNumber)
|
|
184
|
-
|
|
190
|
+
if (inner instanceof z.ZodNumber)
|
|
191
|
+
return isOptional ? z.coerce.number().optional() : z.coerce.number()
|
|
192
|
+
if (inner instanceof z.ZodBoolean)
|
|
193
|
+
return isOptional ? z.coerce.boolean().optional() : z.coerce.boolean()
|
|
185
194
|
|
|
186
195
|
// Union containing number (e.g. type: ["number", "null"] from OpenAPI 3.1)
|
|
187
196
|
if (inner instanceof z.ZodUnion) {
|
package/src/Skill.test.ts
CHANGED
|
@@ -333,9 +333,7 @@ describe('split', () => {
|
|
|
333
333
|
|
|
334
334
|
test('emits fallback description when no explicit descriptions exist', () => {
|
|
335
335
|
const files = Skill.split('test', [{ name: 'ping' }], 1)
|
|
336
|
-
expect(files[0]!.content).toContain(
|
|
337
|
-
'description: Run `test ping --help` for usage details.',
|
|
338
|
-
)
|
|
336
|
+
expect(files[0]!.content).toContain('description: Run `test ping --help` for usage details.')
|
|
339
337
|
})
|
|
340
338
|
|
|
341
339
|
test('includes requires_bin in frontmatter', () => {
|
package/src/Skill.ts
CHANGED
|
@@ -24,7 +24,11 @@ export type File = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Generates a compact Markdown command index for `--llms`. */
|
|
27
|
-
export function index(
|
|
27
|
+
export function index(
|
|
28
|
+
name: string,
|
|
29
|
+
commands: CommandInfo[],
|
|
30
|
+
description?: string | undefined,
|
|
31
|
+
): string {
|
|
28
32
|
const lines: string[] = [`# ${name}`]
|
|
29
33
|
if (description) lines.push('', description)
|
|
30
34
|
lines.push('')
|
|
@@ -35,7 +39,10 @@ export function index(name: string, commands: CommandInfo[], description?: strin
|
|
|
35
39
|
const desc = cmd.description ?? ''
|
|
36
40
|
lines.push(`| \`${signature}\` | ${desc} |`)
|
|
37
41
|
}
|
|
38
|
-
lines.push(
|
|
42
|
+
lines.push(
|
|
43
|
+
'',
|
|
44
|
+
`Run \`${name} --llms-full\` for full manifest. Run \`${name} <command> --schema\` for argument details.`,
|
|
45
|
+
)
|
|
39
46
|
return lines.join('\n')
|
|
40
47
|
}
|
|
41
48
|
|