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/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(target: Record<string, unknown>, data: unknown, segments: Segment[], index: number): void {
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 ? `$ ${name} ${e.command}` : `$ ${name}`).length),
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 ? `$ ${name} ${ex.command}` : `$ ${name}`
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
- { flag: '--filter-output <keys>', desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])' },
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' },
@@ -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 { spec } from '../test/fixtures/openapi-spec.js'
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) => { exitCode = 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
- .command('api', { fetch: app.fetch, openapi: spec })
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', 'listUsers', '--limit', '5', '--format', 'json',
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', 'healthCheck', '--verbose', '--format', 'json',
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
- .command('api', { fetch: openapiApp.fetch, openapi: openapiSpec })
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(), ['api', 'listUsers', '--limit', '5', '--format', 'json'])
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', 'healthCheck', '--verbose', '--format', 'json',
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', 'updateUser', '1', '--name', 'Updated', '--active', 'true', '--format', 'json',
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(), ['api', 'listUsers', '--limit', '3', '--format', 'json'])
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
- .command('api', { fetch: prefixedApp.fetch, basePath: '/api' })
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
- .command('api', { fetch: prefixedApp.fetch, basePath: '/api' })
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
- .command('api', { fetch: prefixedApp.fetch, basePath: '/api' })
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
- .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
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
- .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
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
- .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
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
- .command('api', { fetch: prefixedApp.fetch, openapi: spec, basePath: '/api' })
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({ basePath: options.basePath, fetch, httpMethod, path, pathParams, queryParams, bodyProps }),
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) return isOptional ? z.coerce.number().optional() : z.coerce.number()
184
- if (inner instanceof z.ZodBoolean) return isOptional ? z.coerce.boolean().optional() : z.coerce.boolean()
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(name: string, commands: CommandInfo[], description?: string | undefined): string {
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('', `Run \`${name} --llms-full\` for full manifest. Run \`${name} <command> --schema\` for argument details.`)
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