incur 0.1.17 → 0.2.1

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 (52) hide show
  1. package/README.md +204 -9
  2. package/SKILL.md +173 -0
  3. package/dist/Cli.d.ts +39 -6
  4. package/dist/Cli.d.ts.map +1 -1
  5. package/dist/Cli.js +536 -43
  6. package/dist/Cli.js.map +1 -1
  7. package/dist/Errors.d.ts +4 -0
  8. package/dist/Errors.d.ts.map +1 -1
  9. package/dist/Errors.js +3 -0
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/Fetch.d.ts +26 -0
  12. package/dist/Fetch.d.ts.map +1 -0
  13. package/dist/Fetch.js +150 -0
  14. package/dist/Fetch.js.map +1 -0
  15. package/dist/Filter.d.ts +14 -0
  16. package/dist/Filter.d.ts.map +1 -0
  17. package/dist/Filter.js +134 -0
  18. package/dist/Filter.js.map +1 -0
  19. package/dist/Help.js +2 -0
  20. package/dist/Help.js.map +1 -1
  21. package/dist/Mcp.d.ts +26 -0
  22. package/dist/Mcp.d.ts.map +1 -1
  23. package/dist/Mcp.js +2 -2
  24. package/dist/Mcp.js.map +1 -1
  25. package/dist/Openapi.d.ts +20 -0
  26. package/dist/Openapi.d.ts.map +1 -0
  27. package/dist/Openapi.js +136 -0
  28. package/dist/Openapi.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.d.ts +8 -2
  34. package/dist/middleware.d.ts.map +1 -1
  35. package/dist/middleware.js.map +1 -1
  36. package/package.json +4 -1
  37. package/src/Cli.test-d.ts +27 -2
  38. package/src/Cli.test.ts +1007 -0
  39. package/src/Cli.ts +676 -47
  40. package/src/Errors.ts +5 -0
  41. package/src/Fetch.test.ts +274 -0
  42. package/src/Fetch.ts +170 -0
  43. package/src/Filter.test.ts +237 -0
  44. package/src/Filter.ts +139 -0
  45. package/src/Help.test.ts +14 -0
  46. package/src/Help.ts +2 -0
  47. package/src/Mcp.ts +3 -3
  48. package/src/Openapi.test.ts +320 -0
  49. package/src/Openapi.ts +196 -0
  50. package/src/e2e.test.ts +778 -0
  51. package/src/index.ts +3 -0
  52. package/src/middleware.ts +9 -2
package/src/Filter.ts ADDED
@@ -0,0 +1,139 @@
1
+ /** A single segment in a filter path: either a string key or an array slice. */
2
+ export type Segment = { key: string } | { start: number; end: number }
3
+
4
+ /** A filter path is an ordered list of segments to traverse. */
5
+ export type FilterPath = Segment[]
6
+
7
+ /** Parses a filter expression string into structured filter paths. */
8
+ export function parse(expression: string): FilterPath[] {
9
+ const paths: FilterPath[] = []
10
+ const tokens: string[] = []
11
+ let current = ''
12
+ let depth = 0
13
+
14
+ // Split on commas, but commas inside [...] are part of a slice
15
+ for (let i = 0; i < expression.length; i++) {
16
+ const ch = expression[i]!
17
+ if (ch === '[') depth++
18
+ else if (ch === ']') depth--
19
+
20
+ if (ch === ',' && depth === 0) {
21
+ tokens.push(current)
22
+ current = ''
23
+ } else current += ch
24
+ }
25
+ if (current) tokens.push(current)
26
+
27
+ for (const token of tokens) {
28
+ const path: FilterPath = []
29
+ let remaining = token
30
+
31
+ while (remaining.length > 0) {
32
+ const bracketIdx = remaining.indexOf('[')
33
+
34
+ if (bracketIdx === -1) {
35
+ // No more slices — split remaining by dots
36
+ for (const part of remaining.split('.')) if (part) path.push({ key: part })
37
+ break
38
+ }
39
+
40
+ // Parse dot-separated keys before the bracket
41
+ const before = remaining.slice(0, bracketIdx)
42
+ for (const part of before.split('.')) if (part) path.push({ key: part })
43
+
44
+ // Parse the slice [start,end]
45
+ const closeBracket = remaining.indexOf(']', bracketIdx)
46
+ const inner = remaining.slice(bracketIdx + 1, closeBracket)
47
+ const [startStr, endStr] = inner.split(',')
48
+ path.push({ start: Number(startStr), end: Number(endStr) })
49
+
50
+ remaining = remaining.slice(closeBracket + 1)
51
+ if (remaining.startsWith('.')) remaining = remaining.slice(1)
52
+ }
53
+
54
+ paths.push(path)
55
+ }
56
+
57
+ return paths
58
+ }
59
+
60
+ /** Applies parsed filter paths to a data value, returning a filtered copy. */
61
+ export function apply(data: unknown, paths: FilterPath[]): unknown {
62
+ if (paths.length === 0) return data
63
+
64
+ // Single key selecting a scalar → return the scalar directly
65
+ if (paths.length === 1 && paths[0]!.length === 1 && 'key' in paths[0]![0]!) {
66
+ const key = paths[0]![0]!.key
67
+ if (Array.isArray(data)) return data.map((item) => apply(item, paths))
68
+ if (typeof data === 'object' && data !== null) {
69
+ const val = (data as Record<string, unknown>)[key]
70
+ if (typeof val !== 'object' || val === null) return val
71
+ return { [key]: val }
72
+ }
73
+ return undefined
74
+ }
75
+
76
+ if (Array.isArray(data)) return data.map((item) => apply(item, paths))
77
+
78
+ const result: Record<string, unknown> = {}
79
+ for (const path of paths) merge(result, data, path, 0)
80
+ return result
81
+ }
82
+
83
+ function resolve(data: unknown, segments: Segment[], index: number): unknown {
84
+ if (index >= segments.length) return data
85
+ const segment = segments[index]!
86
+
87
+ if ('key' in segment) {
88
+ if (typeof data !== 'object' || data === null) return undefined
89
+ const val = (data as Record<string, unknown>)[segment.key]
90
+ return resolve(val, segments, index + 1)
91
+ }
92
+
93
+ // slice segment
94
+ if (!Array.isArray(data)) return undefined
95
+ const sliced = data.slice(segment.start, segment.end)
96
+ if (index + 1 >= segments.length) return sliced
97
+ return sliced.map((item) => resolve(item, segments, index + 1))
98
+ }
99
+
100
+ function merge(target: Record<string, unknown>, data: unknown, segments: Segment[], index: number): void {
101
+ if (index >= segments.length || typeof data !== 'object' || data === null) return
102
+ const segment = segments[index]!
103
+
104
+ if ('key' in segment) {
105
+ const val = (data as Record<string, unknown>)[segment.key]
106
+ if (val === undefined) return
107
+
108
+ if (index + 1 >= segments.length) {
109
+ target[segment.key] = val
110
+ return
111
+ }
112
+
113
+ const next = segments[index + 1]!
114
+ if ('start' in next) {
115
+ // Next segment is a slice
116
+ if (!Array.isArray(val)) return
117
+ const sliced = val.slice(next.start, next.end)
118
+ if (index + 2 >= segments.length) {
119
+ target[segment.key] = sliced
120
+ return
121
+ }
122
+ target[segment.key] = sliced.map((item) => {
123
+ const sub: Record<string, unknown> = {}
124
+ merge(sub, item, segments, index + 2)
125
+ return sub
126
+ })
127
+ return
128
+ }
129
+
130
+ // Next segment is a key — recurse into nested object
131
+ if (typeof val !== 'object' || val === null) return
132
+ if (!target[segment.key] || typeof target[segment.key] !== 'object')
133
+ target[segment.key] = {}
134
+ merge(target[segment.key] as Record<string, unknown>, val, segments, index + 1)
135
+ return
136
+ }
137
+
138
+ // slice at root level — shouldn't happen in merge (merge starts from object keys)
139
+ }
package/src/Help.test.ts CHANGED
@@ -25,9 +25,11 @@ describe('formatCommand', () => {
25
25
  --limit <number> Max PRs to return (default: 30)
26
26
 
27
27
  Global Options:
28
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
28
29
  --format <toon|json|yaml|md|jsonl> Output format
29
30
  --help Show help
30
31
  --llms Print LLM-readable manifest
32
+ --schema Show JSON Schema for a command
31
33
  --verbose Show full output envelope"
32
34
  `)
33
35
  })
@@ -42,9 +44,11 @@ describe('formatCommand', () => {
42
44
  Usage: tool ping
43
45
 
44
46
  Global Options:
47
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
45
48
  --format <toon|json|yaml|md|jsonl> Output format
46
49
  --help Show help
47
50
  --llms Print LLM-readable manifest
51
+ --schema Show JSON Schema for a command
48
52
  --verbose Show full output envelope"
49
53
  `)
50
54
  })
@@ -66,9 +70,11 @@ describe('formatCommand', () => {
66
70
  title Title
67
71
 
68
72
  Global Options:
73
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
69
74
  --format <toon|json|yaml|md|jsonl> Output format
70
75
  --help Show help
71
76
  --llms Print LLM-readable manifest
77
+ --schema Show JSON Schema for a command
72
78
  --verbose Show full output envelope"
73
79
  `)
74
80
  })
@@ -107,9 +113,11 @@ describe('formatRoot', () => {
107
113
  issue list List issues
108
114
 
109
115
  Global Options:
116
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
110
117
  --format <toon|json|yaml|md|jsonl> Output format
111
118
  --help Show help
112
119
  --llms Print LLM-readable manifest
120
+ --schema Show JSON Schema for a command
113
121
  --verbose Show full output envelope"
114
122
  `)
115
123
  })
@@ -127,9 +135,11 @@ describe('formatRoot', () => {
127
135
  ping Health check
128
136
 
129
137
  Global Options:
138
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
130
139
  --format <toon|json|yaml|md|jsonl> Output format
131
140
  --help Show help
132
141
  --llms Print LLM-readable manifest
142
+ --schema Show JSON Schema for a command
133
143
  --verbose Show full output envelope"
134
144
  `)
135
145
  })
@@ -151,9 +161,11 @@ describe('formatRoot', () => {
151
161
  fetch Fetch a URL
152
162
 
153
163
  Global Options:
164
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
154
165
  --format <toon|json|yaml|md|jsonl> Output format
155
166
  --help Show help
156
167
  --llms Print LLM-readable manifest
168
+ --schema Show JSON Schema for a command
157
169
  --verbose Show full output envelope"
158
170
  `)
159
171
  })
@@ -175,9 +187,11 @@ describe('formatRoot', () => {
175
187
  url URL to fetch
176
188
 
177
189
  Global Options:
190
+ --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
178
191
  --format <toon|json|yaml|md|jsonl> Output format
179
192
  --help Show help
180
193
  --llms Print LLM-readable manifest
194
+ --schema Show JSON Schema for a command
181
195
  --verbose Show full output envelope"
182
196
  `)
183
197
  })
package/src/Help.ts CHANGED
@@ -326,10 +326,12 @@ function globalOptionsLines(root = false): string[] {
326
326
  }
327
327
 
328
328
  const flags = [
329
+ { flag: '--filter-output <keys>', desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])' },
329
330
  { flag: '--format <toon|json|yaml|md|jsonl>', desc: 'Output format' },
330
331
  { flag: '--help', desc: 'Show help' },
331
332
  { flag: '--llms', desc: 'Print LLM-readable manifest' },
332
333
  ...(root ? [{ flag: '--mcp', desc: 'Start as MCP stdio server' }] : []),
334
+ { flag: '--schema', desc: 'Show JSON Schema for a command' },
333
335
  { flag: '--verbose', desc: 'Show full output envelope' },
334
336
  ...(root ? [{ flag: '--version', desc: 'Show version' }] : []),
335
337
  ]
package/src/Mcp.ts CHANGED
@@ -52,7 +52,7 @@ export declare namespace serve {
52
52
  }
53
53
 
54
54
  /** @internal Executes a tool call and returns a CallToolResult. */
55
- async function callTool(
55
+ export async function callTool(
56
56
  tool: ToolEntry,
57
57
  params: Record<string, unknown>,
58
58
  extra?: {
@@ -135,7 +135,7 @@ function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unkn
135
135
  }
136
136
 
137
137
  /** @internal A resolved tool entry from the command tree. */
138
- type ToolEntry = {
138
+ export type ToolEntry = {
139
139
  name: string
140
140
  description?: string | undefined
141
141
  inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[] }
@@ -143,7 +143,7 @@ type ToolEntry = {
143
143
  }
144
144
 
145
145
  /** @internal Recursively collects leaf commands as tool entries. */
146
- function collectTools(commands: Map<string, any>, prefix: string[]): ToolEntry[] {
146
+ export function collectTools(commands: Map<string, any>, prefix: string[]): ToolEntry[] {
147
147
  const result: ToolEntry[] = []
148
148
  for (const [name, entry] of commands) {
149
149
  const path = [...prefix, name]
@@ -0,0 +1,320 @@
1
+ import { describe, expect, test } from 'vitest'
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
+ import { app as prefixedApp } from '../test/fixtures/hono-api-prefixed.js'
7
+ import { spec } from '../test/fixtures/openapi-spec.js'
8
+ import { app as openapiApp, spec as openapiSpec } from '../test/fixtures/hono-openapi-app.js'
9
+
10
+ function serve(cli: { serve: Cli.Cli['serve'] }, argv: string[]) {
11
+ let output = ''
12
+ let exitCode: number | undefined
13
+ return cli
14
+ .serve(argv, {
15
+ stdout: (s) => (output += s),
16
+ exit: (c) => { exitCode = c },
17
+ })
18
+ .then(() => ({
19
+ output,
20
+ exitCode,
21
+ }))
22
+ }
23
+
24
+ function json(output: string) {
25
+ return JSON.parse(
26
+ output.replace(/"duration": "[^"]+"/g, '"duration": "<stripped>"'),
27
+ )
28
+ }
29
+
30
+ describe('generateCommands', () => {
31
+ test('generates command entries from spec', async () => {
32
+ const commands = await Openapi.generateCommands(spec, app.fetch)
33
+ expect(commands.has('listUsers')).toBe(true)
34
+ expect(commands.has('createUser')).toBe(true)
35
+ expect(commands.has('getUser')).toBe(true)
36
+ expect(commands.has('deleteUser')).toBe(true)
37
+ expect(commands.has('healthCheck')).toBe(true)
38
+ })
39
+
40
+ test('command has description from summary', async () => {
41
+ const commands = await Openapi.generateCommands(spec, app.fetch)
42
+ const cmd = commands.get('listUsers')!
43
+ expect(cmd.description).toBe('List users')
44
+ })
45
+ })
46
+
47
+ describe('cli integration', () => {
48
+ function createCli() {
49
+ return Cli.create('test', { description: 'test' })
50
+ .command('api', { fetch: app.fetch, openapi: spec })
51
+ }
52
+
53
+ test('GET /users via operationId', async () => {
54
+ const { output } = await serve(createCli(), ['api', 'listUsers'])
55
+ expect(output).toContain('Alice')
56
+ })
57
+
58
+ test('GET /users?limit=5 via options', async () => {
59
+ const { output } = await serve(createCli(), [
60
+ 'api', 'listUsers', '--limit', '5', '--format', 'json',
61
+ ])
62
+ expect(json(output).limit).toBe(5)
63
+ })
64
+
65
+ test('GET /users/:id via positional arg', async () => {
66
+ const { output } = await serve(createCli(), ['api', 'getUser', '42'])
67
+ expect(output).toMatchInlineSnapshot(`
68
+ "id: 42
69
+ name: Alice
70
+ "
71
+ `)
72
+ })
73
+
74
+ test('POST /users via createUser with body options', async () => {
75
+ const { output } = await serve(createCli(), ['api', 'createUser', '--name', 'Bob'])
76
+ expect(output).toMatchInlineSnapshot(`
77
+ "created: true
78
+ name: Bob
79
+ "
80
+ `)
81
+ })
82
+
83
+ test('DELETE /users/:id via deleteUser', async () => {
84
+ const { output } = await serve(createCli(), ['api', 'deleteUser', '1'])
85
+ expect(output).toMatchInlineSnapshot(`
86
+ "deleted: true
87
+ id: 1
88
+ "
89
+ `)
90
+ })
91
+
92
+ test('GET /health via healthCheck', async () => {
93
+ const { output } = await serve(createCli(), ['api', 'healthCheck'])
94
+ expect(output).toMatchInlineSnapshot(`
95
+ "ok: true
96
+ "
97
+ `)
98
+ })
99
+
100
+ test('--help on api shows subcommands', async () => {
101
+ const { output } = await serve(createCli(), ['api', '--help'])
102
+ expect(output).toContain('listUsers')
103
+ expect(output).toContain('createUser')
104
+ expect(output).toContain('getUser')
105
+ expect(output).toContain('deleteUser')
106
+ expect(output).toContain('healthCheck')
107
+ })
108
+
109
+ test('--help on specific command shows typed args/options', async () => {
110
+ const { output } = await serve(createCli(), ['api', 'getUser', '--help'])
111
+ expect(output).toContain('id')
112
+ expect(output).toContain('Get a user by ID')
113
+ })
114
+
115
+ test('--help on createUser shows body options', async () => {
116
+ const { output } = await serve(createCli(), ['api', 'createUser', '--help'])
117
+ expect(output).toContain('name')
118
+ expect(output).toContain('Create a user')
119
+ })
120
+
121
+ test('--format json', async () => {
122
+ const { output } = await serve(createCli(), ['api', 'healthCheck', '--format', 'json'])
123
+ expect(json(output)).toEqual({ ok: true })
124
+ })
125
+
126
+ test('--verbose wraps in envelope', async () => {
127
+ const { output } = await serve(createCli(), [
128
+ 'api', 'healthCheck', '--verbose', '--format', 'json',
129
+ ])
130
+ const parsed = json(output)
131
+ expect(parsed.ok).toBe(true)
132
+ expect(parsed.data).toEqual({ ok: true })
133
+ expect(parsed.meta.command).toContain('api')
134
+ })
135
+
136
+ test('missing required path param shows validation error', async () => {
137
+ const { exitCode } = await serve(createCli(), ['api', 'getUser'])
138
+ expect(exitCode).toBe(1)
139
+ })
140
+ })
141
+
142
+ describe('@hono/zod-openapi integration', () => {
143
+ function createCli() {
144
+ return Cli.create('test', { description: 'test' })
145
+ .command('api', { fetch: openapiApp.fetch, openapi: openapiSpec })
146
+ }
147
+
148
+ test('GET /users via listUsers', async () => {
149
+ const { output } = await serve(createCli(), ['api', 'listUsers'])
150
+ expect(output).toContain('Alice')
151
+ })
152
+
153
+ test('GET /users?limit=5', async () => {
154
+ const { output } = await serve(createCli(), ['api', 'listUsers', '--limit', '5', '--format', 'json'])
155
+ expect(json(output).limit).toBe(5)
156
+ })
157
+
158
+ test('GET /users/:id via getUser', async () => {
159
+ const { output } = await serve(createCli(), ['api', 'getUser', '42'])
160
+ expect(output).toMatchInlineSnapshot(`
161
+ "id: 42
162
+ name: Alice
163
+ "
164
+ `)
165
+ })
166
+
167
+ test('POST /users via createUser', async () => {
168
+ const { output } = await serve(createCli(), ['api', 'createUser', '--name', 'Bob'])
169
+ expect(output).toMatchInlineSnapshot(`
170
+ "created: true
171
+ name: Bob
172
+ "
173
+ `)
174
+ })
175
+
176
+ test('DELETE /users/:id via deleteUser', async () => {
177
+ const { output } = await serve(createCli(), ['api', 'deleteUser', '1'])
178
+ expect(output).toMatchInlineSnapshot(`
179
+ "deleted: true
180
+ id: 1
181
+ "
182
+ `)
183
+ })
184
+
185
+ test('GET /health via healthCheck', async () => {
186
+ const { output } = await serve(createCli(), ['api', 'healthCheck'])
187
+ expect(output).toMatchInlineSnapshot(`
188
+ "ok: true
189
+ "
190
+ `)
191
+ })
192
+
193
+ test('--help shows operationId commands', async () => {
194
+ const { output } = await serve(createCli(), ['api', '--help'])
195
+ expect(output).toContain('listUsers')
196
+ expect(output).toContain('getUser')
197
+ expect(output).toContain('createUser')
198
+ expect(output).toContain('deleteUser')
199
+ expect(output).toContain('healthCheck')
200
+ expect(output).toContain('updateUser')
201
+ })
202
+
203
+ test('--help on getUser shows path param', async () => {
204
+ const { output } = await serve(createCli(), ['api', 'getUser', '--help'])
205
+ expect(output).toContain('id')
206
+ })
207
+
208
+ test('--help on createUser shows body options', async () => {
209
+ const { output } = await serve(createCli(), ['api', 'createUser', '--help'])
210
+ expect(output).toContain('name')
211
+ })
212
+
213
+ test('--help on updateUser shows path param and body options', async () => {
214
+ const { output } = await serve(createCli(), ['api', 'updateUser', '--help'])
215
+ expect(output).toContain('id')
216
+ expect(output).toContain('name')
217
+ expect(output).toContain('Update a user')
218
+ })
219
+
220
+ test('--format json', async () => {
221
+ const { output } = await serve(createCli(), ['api', 'healthCheck', '--format', 'json'])
222
+ expect(json(output)).toEqual({ ok: true })
223
+ })
224
+
225
+ test('--verbose wraps in envelope', async () => {
226
+ const { output } = await serve(createCli(), [
227
+ 'api', 'healthCheck', '--verbose', '--format', 'json',
228
+ ])
229
+ const parsed = json(output)
230
+ expect(parsed.ok).toBe(true)
231
+ expect(parsed.data).toEqual({ ok: true })
232
+ expect(parsed.meta.command).toContain('api')
233
+ })
234
+
235
+ test('missing required path param shows validation error', async () => {
236
+ const { exitCode } = await serve(createCli(), ['api', 'getUser'])
237
+ expect(exitCode).toBe(1)
238
+ })
239
+
240
+ test('PUT /users/:id with path param + body options', async () => {
241
+ const { output } = await serve(createCli(), ['api', 'updateUser', '1', '--name', 'Updated'])
242
+ expect(output).toMatchInlineSnapshot(`
243
+ "id: 1
244
+ name: Updated
245
+ "
246
+ `)
247
+ })
248
+
249
+ test('PUT /users/:id with optional boolean body option', async () => {
250
+ const { output } = await serve(createCli(), [
251
+ 'api', 'updateUser', '1', '--name', 'Updated', '--active', 'true', '--format', 'json',
252
+ ])
253
+ const parsed = json(output)
254
+ expect(parsed.id).toBe(1)
255
+ expect(parsed.name).toBe('Updated')
256
+ expect(parsed.active).toBe(true)
257
+ })
258
+
259
+ test('query param coercion with zod-openapi generated spec', async () => {
260
+ const { output } = await serve(createCli(), ['api', 'listUsers', '--limit', '3', '--format', 'json'])
261
+ expect(json(output).limit).toBe(3)
262
+ })
263
+ })
264
+
265
+ describe('basePath', () => {
266
+ test('fetch gateway prepends basePath to request path', async () => {
267
+ const cli = Cli.create('test', { description: 'test' })
268
+ .command('api', { fetch: prefixedApp.fetch, basePath: '/api' })
269
+ const { output } = await serve(cli, ['api', 'users'])
270
+ expect(output).toContain('Alice')
271
+ })
272
+
273
+ test('fetch gateway basePath with query params', async () => {
274
+ const cli = Cli.create('test', { description: 'test' })
275
+ .command('api', { fetch: prefixedApp.fetch, basePath: '/api' })
276
+ const { output } = await serve(cli, ['api', 'users', '--limit', '5', '--format', 'json'])
277
+ expect(json(output).limit).toBe(5)
278
+ })
279
+
280
+ test('fetch gateway basePath with POST', async () => {
281
+ const cli = Cli.create('test', { description: 'test' })
282
+ .command('api', { fetch: prefixedApp.fetch, basePath: '/api' })
283
+ const { output } = await serve(cli, ['api', 'users', '-X', 'POST', '-d', '{"name":"Bob"}'])
284
+ expect(output).toContain('Bob')
285
+ expect(output).toContain('created')
286
+ })
287
+
288
+ test('openapi with basePath prepends to spec paths', async () => {
289
+ const cli = Cli.create('test', { description: 'test' })
290
+ .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
291
+ const { output } = await serve(cli, ['api', 'listUsers'])
292
+ expect(output).toContain('Alice')
293
+ })
294
+
295
+ test('openapi basePath with path params', async () => {
296
+ const cli = Cli.create('test', { description: 'test' })
297
+ .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
298
+ const { output } = await serve(cli, ['api', 'getUser', '42'])
299
+ expect(output).toMatchInlineSnapshot(`
300
+ "id: 42
301
+ name: Alice
302
+ "
303
+ `)
304
+ })
305
+
306
+ test('openapi basePath with body options', async () => {
307
+ const cli = Cli.create('test', { description: 'test' })
308
+ .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
309
+ const { output } = await serve(cli, ['api', 'createUser', '--name', 'Bob'])
310
+ expect(output).toContain('created')
311
+ expect(output).toContain('Bob')
312
+ })
313
+
314
+ test('openapi basePath with health check', async () => {
315
+ const cli = Cli.create('test', { description: 'test' })
316
+ .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
317
+ const { output } = await serve(cli, ['api', 'healthCheck', '--format', 'json'])
318
+ expect(json(output)).toEqual({ ok: true })
319
+ })
320
+ })