incur 0.0.0 → 0.0.2

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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/SKILL.md +664 -0
  4. package/dist/Cli.d.ts +255 -0
  5. package/dist/Cli.d.ts.map +1 -0
  6. package/dist/Cli.js +900 -0
  7. package/dist/Cli.js.map +1 -0
  8. package/dist/Errors.d.ts +92 -0
  9. package/dist/Errors.d.ts.map +1 -0
  10. package/dist/Errors.js +75 -0
  11. package/dist/Errors.js.map +1 -0
  12. package/dist/Formatter.d.ts +5 -0
  13. package/dist/Formatter.d.ts.map +1 -0
  14. package/dist/Formatter.js +91 -0
  15. package/dist/Formatter.js.map +1 -0
  16. package/dist/Help.d.ts +53 -0
  17. package/dist/Help.d.ts.map +1 -0
  18. package/dist/Help.js +231 -0
  19. package/dist/Help.js.map +1 -0
  20. package/dist/Mcp.d.ts +13 -0
  21. package/dist/Mcp.d.ts.map +1 -0
  22. package/dist/Mcp.js +140 -0
  23. package/dist/Mcp.js.map +1 -0
  24. package/dist/Parser.d.ts +24 -0
  25. package/dist/Parser.d.ts.map +1 -0
  26. package/dist/Parser.js +215 -0
  27. package/dist/Parser.js.map +1 -0
  28. package/dist/Register.d.ts +19 -0
  29. package/dist/Register.d.ts.map +1 -0
  30. package/dist/Register.js +2 -0
  31. package/dist/Register.js.map +1 -0
  32. package/dist/Schema.d.ts +4 -0
  33. package/dist/Schema.d.ts.map +1 -0
  34. package/dist/Schema.js +8 -0
  35. package/dist/Schema.js.map +1 -0
  36. package/dist/Skill.d.ts +29 -0
  37. package/dist/Skill.d.ts.map +1 -0
  38. package/dist/Skill.js +196 -0
  39. package/dist/Skill.js.map +1 -0
  40. package/dist/Skillgen.d.ts +3 -0
  41. package/dist/Skillgen.d.ts.map +1 -0
  42. package/dist/Skillgen.js +67 -0
  43. package/dist/Skillgen.js.map +1 -0
  44. package/dist/SyncMcp.d.ts +23 -0
  45. package/dist/SyncMcp.d.ts.map +1 -0
  46. package/dist/SyncMcp.js +100 -0
  47. package/dist/SyncMcp.js.map +1 -0
  48. package/dist/SyncSkills.d.ts +38 -0
  49. package/dist/SyncSkills.d.ts.map +1 -0
  50. package/dist/SyncSkills.js +163 -0
  51. package/dist/SyncSkills.js.map +1 -0
  52. package/dist/Typegen.d.ts +6 -0
  53. package/dist/Typegen.d.ts.map +1 -0
  54. package/dist/Typegen.js +92 -0
  55. package/dist/Typegen.js.map +1 -0
  56. package/dist/bin.d.ts +14 -0
  57. package/dist/bin.d.ts.map +1 -0
  58. package/dist/bin.js +30 -0
  59. package/dist/bin.js.map +1 -0
  60. package/dist/index.d.ts +15 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +14 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/internal/pm.d.ts +3 -0
  65. package/dist/internal/pm.d.ts.map +1 -0
  66. package/dist/internal/pm.js +11 -0
  67. package/dist/internal/pm.js.map +1 -0
  68. package/dist/internal/types.d.ts +11 -0
  69. package/dist/internal/types.d.ts.map +1 -0
  70. package/dist/internal/types.js +2 -0
  71. package/dist/internal/types.js.map +1 -0
  72. package/dist/internal/utils.d.ts +8 -0
  73. package/dist/internal/utils.d.ts.map +1 -0
  74. package/dist/internal/utils.js +51 -0
  75. package/dist/internal/utils.js.map +1 -0
  76. package/examples/npm/cli.ts +180 -0
  77. package/examples/npm/node_modules/.bin/incur.src +21 -0
  78. package/examples/npm/node_modules/.bin/tsx +21 -0
  79. package/examples/npm/package.json +14 -0
  80. package/examples/npm/tsconfig.json +9 -0
  81. package/examples/presto/cli.ts +246 -0
  82. package/examples/presto/node_modules/.bin/incur.src +21 -0
  83. package/examples/presto/node_modules/.bin/tsx +21 -0
  84. package/examples/presto/package.json +14 -0
  85. package/examples/presto/tsconfig.json +9 -0
  86. package/package.json +53 -2
  87. package/src/Cli.test-d.ts +135 -0
  88. package/src/Cli.test.ts +1373 -0
  89. package/src/Cli.ts +1470 -0
  90. package/src/Errors.test.ts +96 -0
  91. package/src/Errors.ts +139 -0
  92. package/src/Formatter.test.ts +245 -0
  93. package/src/Formatter.ts +106 -0
  94. package/src/Help.test.ts +124 -0
  95. package/src/Help.ts +302 -0
  96. package/src/Mcp.test.ts +254 -0
  97. package/src/Mcp.ts +195 -0
  98. package/src/Parser.test-d.ts +45 -0
  99. package/src/Parser.test.ts +118 -0
  100. package/src/Parser.ts +247 -0
  101. package/src/Register.ts +18 -0
  102. package/src/Schema.test.ts +125 -0
  103. package/src/Schema.ts +8 -0
  104. package/src/Skill.test.ts +293 -0
  105. package/src/Skill.ts +253 -0
  106. package/src/Skillgen.ts +66 -0
  107. package/src/SyncMcp.test.ts +75 -0
  108. package/src/SyncMcp.ts +132 -0
  109. package/src/SyncSkills.test.ts +92 -0
  110. package/src/SyncSkills.ts +205 -0
  111. package/src/Typegen.test.ts +150 -0
  112. package/src/Typegen.ts +107 -0
  113. package/src/bin.ts +33 -0
  114. package/src/e2e.test.ts +1710 -0
  115. package/src/index.ts +14 -0
  116. package/src/internal/pm.test.ts +38 -0
  117. package/src/internal/pm.ts +8 -0
  118. package/src/internal/types.ts +22 -0
  119. package/src/internal/utils.ts +50 -0
  120. package/src/tsconfig.json +8 -0
package/src/Cli.ts ADDED
@@ -0,0 +1,1470 @@
1
+ import type { z } from 'zod'
2
+
3
+ import type { FieldError } from './Errors.js'
4
+ import { IncurError, ValidationError } from './Errors.js'
5
+ import * as Formatter from './Formatter.js'
6
+ import * as Help from './Help.js'
7
+ import { detectRunner } from './internal/pm.js'
8
+ import type { OneOf } from './internal/types.js'
9
+ import * as Mcp from './Mcp.js'
10
+ import * as Parser from './Parser.js'
11
+ import type { Register } from './Register.js'
12
+ import * as Schema from './Schema.js'
13
+ import * as Skill from './Skill.js'
14
+ import * as SyncMcp from './SyncMcp.js'
15
+ import * as SyncSkills from './SyncSkills.js'
16
+
17
+ /** A CLI application instance. Also used as a command group when mounted on a parent CLI. */
18
+ export type Cli<commands extends CommandsMap = {}> = {
19
+ /** Registers a root command or mounts a sub-CLI as a command group. */
20
+ command: {
21
+ /** Registers a command. Returns the CLI instance for chaining. */
22
+ <
23
+ const name extends string,
24
+ const args extends z.ZodObject<any> | undefined = undefined,
25
+ const env extends z.ZodObject<any> | undefined = undefined,
26
+ const options extends z.ZodObject<any> | undefined = undefined,
27
+ const output extends z.ZodType | undefined = undefined,
28
+ >(
29
+ name: name,
30
+ definition: CommandDefinition<args, env, options, output>,
31
+ ): Cli<commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<options> } }>
32
+ /** Mounts a sub-CLI as a command group. */
33
+ <const name extends string, const sub extends CommandsMap>(
34
+ cli: Cli<sub> & { name: name },
35
+ ): Cli<commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] }>
36
+ /** Mounts a root CLI as a single command. */
37
+ <
38
+ const name extends string,
39
+ const args extends z.ZodObject<any> | undefined,
40
+ const opts extends z.ZodObject<any> | undefined,
41
+ >(
42
+ cli: Root<args, opts> & { name: name },
43
+ ): Cli<commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<opts> } }>
44
+ }
45
+ /** A short description of the CLI. */
46
+ description?: string | undefined
47
+ /** The name of the CLI application. */
48
+ name: string
49
+ /** Parses argv, runs the matched command, and writes the output envelope to stdout. */
50
+ serve(argv?: string[], options?: serve.Options): Promise<void>
51
+ }
52
+
53
+ /** Root CLI — a single command with no subcommands. Carries phantom generics for mounting inference. */
54
+ export type Root<
55
+ _args extends z.ZodObject<any> | undefined = undefined,
56
+ _options extends z.ZodObject<any> | undefined = undefined,
57
+ > = Omit<Cli, 'command'>
58
+
59
+ /** Extracts the commands map from the registered type. */
60
+ export type Commands = Register extends { commands: infer commands extends CommandsMap }
61
+ ? commands
62
+ : {}
63
+
64
+ /** Call to action. */
65
+ export type Cta<commands extends CommandsMap = Commands> =
66
+ | ([keyof commands] extends [never] ? string : (keyof commands & string) | (string & {}))
67
+ | ([keyof commands] extends [never]
68
+ ? {
69
+ /** Positional arguments appended as bare values. */
70
+ args?: Record<string, unknown> | undefined
71
+ /** The command name to run. */
72
+ command: string
73
+ /** A short description of what the command does. */
74
+ description?: string | undefined
75
+ /** Named options formatted as `--key value` flags. */
76
+ options?: Record<string, unknown> | undefined
77
+ }
78
+ :
79
+ | {
80
+ [name in keyof commands & string]: {
81
+ /** Positional arguments appended as bare values. */
82
+ args?:
83
+ | { [key in keyof commands[name]['args']]?: commands[name]['args'][key] | true }
84
+ | undefined
85
+ /** The command name to run. */
86
+ command: name
87
+ /** A short description of what the command does. */
88
+ description?: string | undefined
89
+ /** Named options formatted as `--key value` flags. */
90
+ options?:
91
+ | {
92
+ [key in keyof commands[name]['options']]?:
93
+ | commands[name]['options'][key]
94
+ | true
95
+ }
96
+ | undefined
97
+ }
98
+ }[keyof commands & string]
99
+ | {
100
+ /** The command name to run. */
101
+ command: string & {}
102
+ /** A short description of what the command does. */
103
+ description?: string | undefined
104
+ })
105
+
106
+ /** Creates a leaf CLI with a root handler and no subcommands. */
107
+ export function create<
108
+ const args extends z.ZodObject<any> | undefined = undefined,
109
+ const env extends z.ZodObject<any> | undefined = undefined,
110
+ const opts extends z.ZodObject<any> | undefined = undefined,
111
+ const output extends z.ZodType | undefined = undefined,
112
+ >(
113
+ name: string,
114
+ definition: create.Options<args, env, opts, output> & { run: Function },
115
+ ): Root<args, opts>
116
+ /** Creates a router CLI that registers subcommands. */
117
+ export function create<
118
+ const args extends z.ZodObject<any> | undefined = undefined,
119
+ const env extends z.ZodObject<any> | undefined = undefined,
120
+ const opts extends z.ZodObject<any> | undefined = undefined,
121
+ const output extends z.ZodType | undefined = undefined,
122
+ >(name: string, definition?: create.Options<args, env, opts, output>): Cli
123
+ /** Creates a leaf CLI from a single options object (e.g. package.json). */
124
+ export function create<
125
+ const args extends z.ZodObject<any> | undefined = undefined,
126
+ const env extends z.ZodObject<any> | undefined = undefined,
127
+ const opts extends z.ZodObject<any> | undefined = undefined,
128
+ const output extends z.ZodType | undefined = undefined,
129
+ >(
130
+ definition: create.Options<args, env, opts, output> & { name: string; run: Function },
131
+ ): Root<args, opts>
132
+ /** Creates a router CLI from a single options object (e.g. package.json). */
133
+ export function create<
134
+ const args extends z.ZodObject<any> | undefined = undefined,
135
+ const env extends z.ZodObject<any> | undefined = undefined,
136
+ const opts extends z.ZodObject<any> | undefined = undefined,
137
+ const output extends z.ZodType | undefined = undefined,
138
+ >(definition: create.Options<args, env, opts, output> & { name: string }): Cli
139
+ export function create(
140
+ nameOrDefinition: string | (any & { name: string }),
141
+ definition?: any,
142
+ ): Cli | Root {
143
+ const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name
144
+ const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition
145
+ if ('run' in def) {
146
+ const rootDef = def as CommandDefinition<any, any, any>
147
+ const leafCommands = new Map<string, CommandEntry>()
148
+ leafCommands.set(name, rootDef)
149
+
150
+ const leaf: Root = {
151
+ name,
152
+ description: def.description,
153
+ async serve(argv = process.argv.slice(2), options: serve.Options = {}) {
154
+ return serveImpl(name, leafCommands, [name, ...argv], {
155
+ ...options,
156
+ description: def.description,
157
+ format: def.format,
158
+ mcp: def.mcp,
159
+ sync: def.sync,
160
+ version: def.version,
161
+ })
162
+ },
163
+ }
164
+ toRootDefinition.set(leaf, rootDef)
165
+ toCommands.set(leaf as unknown as Cli, leafCommands)
166
+ return leaf
167
+ }
168
+
169
+ const commands = new Map<string, CommandEntry>()
170
+
171
+ const cli: Cli = {
172
+ name,
173
+ description: def.description,
174
+
175
+ command(nameOrCli: any, def?: any): any {
176
+ if (typeof nameOrCli === 'string') {
177
+ commands.set(nameOrCli, def)
178
+ return cli
179
+ }
180
+ const rootDef = toRootDefinition.get(nameOrCli)
181
+ if (rootDef) {
182
+ commands.set(nameOrCli.name, rootDef)
183
+ return cli
184
+ }
185
+ const sub = nameOrCli as Cli
186
+ const subCommands = toCommands.get(sub)!
187
+ commands.set(sub.name, { _group: true, description: sub.description, commands: subCommands })
188
+ return cli
189
+ },
190
+
191
+ async serve(argv = process.argv.slice(2), serveOptions: serve.Options = {}) {
192
+ return serveImpl(name, commands, argv, {
193
+ ...serveOptions,
194
+ description: def.description,
195
+ format: def.format,
196
+ mcp: def.mcp,
197
+ sync: def.sync,
198
+ version: def.version,
199
+ })
200
+ },
201
+ }
202
+
203
+ toCommands.set(cli, commands)
204
+ return cli
205
+ }
206
+
207
+ export declare namespace create {
208
+ /** Options for creating a CLI. Provide `run` for a leaf CLI, omit it for a router. */
209
+ type Options<
210
+ args extends z.ZodObject<any> | undefined = undefined,
211
+ env extends z.ZodObject<any> | undefined = undefined,
212
+ options extends z.ZodObject<any> | undefined = undefined,
213
+ output extends z.ZodType | undefined = undefined,
214
+ > = {
215
+ /** Map of option names to single-char aliases. */
216
+ alias?: options extends z.ZodObject<any>
217
+ ? Partial<Record<keyof z.output<options>, string>>
218
+ : Record<string, string> | undefined
219
+ /** Zod schema for positional arguments. */
220
+ args?: args | undefined
221
+ /** A short description of what the CLI does. */
222
+ description?: string | undefined
223
+ /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
224
+ env?: env | undefined
225
+ /** Usage examples for this command. */
226
+ examples?: Example<args, options>[] | undefined
227
+ /** Default output format. Overridden by `--format` or `--json`. */
228
+ format?: Formatter.Format | undefined
229
+ /** Zod schema for named options/flags. */
230
+ options?: options | undefined
231
+ /** Zod schema for the return value. */
232
+ output?: output | undefined
233
+ /** Alternative usage patterns shown in help output. */
234
+ usage?: Usage<args, options>[] | undefined
235
+ /** The root command handler. When provided, creates a leaf CLI with no subcommands. */
236
+ run?:
237
+ | ((context: {
238
+ args: InferOutput<args>
239
+ /** Parsed environment variables. */
240
+ env: InferOutput<env>
241
+ /** Return an error result with optional CTAs. */
242
+ error: (options: {
243
+ code: string
244
+ cta?: CtaBlock | undefined
245
+ message: string
246
+ retryable?: boolean | undefined
247
+ }) => never
248
+ /** Return a success result with optional metadata (e.g. CTAs). */
249
+ ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
250
+ options: InferOutput<options>
251
+ }) =>
252
+ | InferReturn<output>
253
+ | Promise<InferReturn<output>>
254
+ | AsyncGenerator<InferReturn<output>, unknown, unknown>)
255
+ | undefined
256
+ /** Options for the built-in `mcp add` command. */
257
+ mcp?:
258
+ | {
259
+ /** Target specific agents by default (e.g. `['claude-code', 'cursor']`). */
260
+ agents?: string[] | undefined
261
+ /** Override the command agents will run to start the MCP server. Auto-detected if omitted. */
262
+ command?: string | undefined
263
+ }
264
+ | undefined
265
+ /** Options for the built-in `skills add` command. */
266
+ sync?:
267
+ | {
268
+ /** Working directory for resolving `include` globs. Pass `import.meta.dirname` when running from a bin entry. Defaults to `process.cwd()`. */
269
+ cwd?: string | undefined
270
+ /** Default grouping depth for skill files. Overridden by `--depth`. Defaults to `1`. */
271
+ depth?: number | undefined
272
+ /** Glob patterns for directories containing SKILL.md files to include (e.g. `"skills/*"`, `"my-skill"`). */
273
+ include?: string[] | undefined
274
+ /** Example prompts shown after sync to help users get started. */
275
+ suggestions?: string[] | undefined
276
+ }
277
+ | undefined
278
+ /** The CLI version string. */
279
+ version?: string | undefined
280
+ }
281
+ }
282
+
283
+ export declare namespace serve {
284
+ /** Options for `serve()`, primarily used for testing. */
285
+ type Options = {
286
+ /** Override environment variable source. Defaults to `process.env`. */
287
+ env?: Record<string, string | undefined> | undefined
288
+ /** Override exit handler. Defaults to `process.exit`. */
289
+ exit?: ((code: number) => void) | undefined
290
+ /** Override stdout writer. Defaults to `process.stdout.write`. */
291
+ stdout?: ((s: string) => void) | undefined
292
+ }
293
+ }
294
+
295
+ /** @internal Shared serve implementation for both router and leaf CLIs. */
296
+ // biome-ignore lint/correctness/noUnusedVariables: _
297
+ async function serveImpl(
298
+ name: string,
299
+ commands: Map<string, CommandEntry>,
300
+ argv: string[],
301
+ options: serveImpl.Options = {},
302
+ ) {
303
+ const stdout = options.stdout ?? ((s: string) => process.stdout.write(s))
304
+ const exit = options.exit ?? ((code: number) => process.exit(code))
305
+
306
+ const {
307
+ verbose,
308
+ format: formatFlag,
309
+ formatExplicit,
310
+ llms,
311
+ mcp: mcpFlag,
312
+ help,
313
+ version,
314
+ rest: filtered,
315
+ } = extractBuiltinFlags(argv)
316
+
317
+ // --mcp: start as MCP stdio server
318
+ if (mcpFlag) {
319
+ await Mcp.serve(name, options.version ?? '0.0.0', commands)
320
+ return
321
+ }
322
+
323
+ // Human mode: default unless --verbose or explicit --format/--json override
324
+ const human = !formatExplicit && !verbose
325
+
326
+ function writeln(s: string) {
327
+ stdout(s.endsWith('\n') ? s : `${s}\n`)
328
+ }
329
+
330
+ // Skills staleness check (skip for built-in commands)
331
+ if (!llms && !help && !version) {
332
+ const isSkillsAdd =
333
+ filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills')
334
+ const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp')
335
+ if (!isSkillsAdd && !isMcpAdd) {
336
+ const stored = SyncSkills.readHash(name)
337
+ if (stored) {
338
+ const groups = new Map<string, string>()
339
+ const entries = collectSkillCommands(commands, [], groups)
340
+ if (Skill.hash(entries) !== stored) {
341
+ const runner = detectRunner()
342
+ const spec = SyncMcp.detectPackageSpecifier(name)
343
+ process.stderr.write(
344
+ `⚠ Skills are out of date. Run '${runner} ${spec} skills add' to update.\n\n`,
345
+ )
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ if (llms) {
352
+ // Scope to a subtree if command tokens are provided
353
+ let scopedCommands = commands
354
+ const prefix: string[] = []
355
+ for (const token of filtered) {
356
+ const entry = scopedCommands.get(token)
357
+ if (!entry) break
358
+ if (isGroup(entry)) {
359
+ scopedCommands = entry.commands
360
+ prefix.push(token)
361
+ } else {
362
+ // Leaf command — scope to just this command
363
+ scopedCommands = new Map([[token, entry]])
364
+ break
365
+ }
366
+ }
367
+
368
+ if (!formatExplicit || formatFlag === 'md') {
369
+ const groups = new Map<string, string>()
370
+ const cmds = collectSkillCommands(scopedCommands, prefix, groups)
371
+ const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name
372
+ writeln(Skill.generate(scopedName, cmds, groups))
373
+ return
374
+ }
375
+ writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag))
376
+ return
377
+ }
378
+
379
+ // skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
380
+ const skillsIdx =
381
+ filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1
382
+ if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills' && filtered[skillsIdx + 1] === 'add') {
383
+ if (help) {
384
+ writeln(
385
+ [
386
+ `${name} skills add — Sync skill files to your agent`,
387
+ '',
388
+ `Usage: ${name} skills add [options]`,
389
+ '',
390
+ 'Options:',
391
+ ' --depth <number> Grouping depth for skill files (default: 1)',
392
+ ' --no-global Install to project instead of globally',
393
+ ].join('\n'),
394
+ )
395
+ return
396
+ }
397
+ const rest = filtered.slice(skillsIdx + 2)
398
+ const depthArg = rest.indexOf('--depth')
399
+ const depth = depthArg !== -1 ? Number(rest[depthArg + 1]) : (options.sync?.depth ?? 1)
400
+ const global = rest.includes('--no-global') ? false : undefined
401
+ try {
402
+ if (human) stdout('Syncing...')
403
+ const result = await SyncSkills.sync(name, commands, {
404
+ cwd: options.sync?.cwd,
405
+ depth,
406
+ description: options.description,
407
+ global,
408
+ include: options.sync?.include,
409
+ })
410
+ if (human) {
411
+ stdout('\r\x1b[K')
412
+ const lines: string[] = []
413
+ const skillLabel = (s: (typeof result.skills)[number]) =>
414
+ s.external || s.name === name ? s.name : `${name}-${s.name}`
415
+ const maxLen = Math.max(...result.skills.map((s) => skillLabel(s).length))
416
+ for (const s of result.skills) {
417
+ const label = skillLabel(s)
418
+ const padding = s.description
419
+ ? `${' '.repeat(maxLen - label.length)} ${s.description}`
420
+ : ''
421
+ lines.push(` ✓ ${label}${padding}`)
422
+ }
423
+ lines.push('')
424
+ lines.push(`${result.skills.length} skill${result.skills.length === 1 ? '' : 's'} synced`)
425
+ const suggestions = options.sync?.suggestions
426
+ if (suggestions && suggestions.length > 0) {
427
+ lines.push('')
428
+ lines.push(`Your agent can now use ${name}. Try asking:`)
429
+ for (const s of suggestions) lines.push(` "${s}"`)
430
+ }
431
+ lines.push('')
432
+ lines.push(`Run \`${name} --help\` to see the full command reference.`)
433
+ writeln(lines.join('\n'))
434
+ } else
435
+ writeln(Formatter.format({ skills: result.paths }, formatExplicit ? formatFlag : 'toon'))
436
+ } catch (err) {
437
+ writeln(
438
+ Formatter.format(
439
+ { code: 'SYNC_SKILLS_FAILED', message: err instanceof Error ? err.message : String(err) },
440
+ formatExplicit ? formatFlag : 'toon',
441
+ ),
442
+ )
443
+ exit(1)
444
+ }
445
+ return
446
+ }
447
+
448
+ // mcp add: register CLI as MCP server via `npx add-mcp`
449
+ const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1
450
+ if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp' && filtered[mcpIdx + 1] === 'add') {
451
+ if (help) {
452
+ writeln(
453
+ [
454
+ `${name} mcp add — Register as an MCP server for your agent`,
455
+ '',
456
+ `Usage: ${name} mcp add [options]`,
457
+ '',
458
+ 'Options:',
459
+ ' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
460
+ ' --no-global Install to project instead of globally',
461
+ ' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
462
+ ].join('\n'),
463
+ )
464
+ return
465
+ }
466
+ const rest = filtered.slice(mcpIdx + 2)
467
+ const global = rest.includes('--no-global') ? false : true
468
+
469
+ // Parse --command / -c and --agent flags from argv
470
+ let command = options.mcp?.command
471
+ const agents: string[] = [...(options.mcp?.agents ?? [])]
472
+ for (let i = 0; i < rest.length; i++) {
473
+ if ((rest[i] === '--command' || rest[i] === '-c') && rest[i + 1]) command = rest[++i]!
474
+ else if (rest[i] === '--agent' && rest[i + 1]) agents.push(rest[++i]!)
475
+ }
476
+
477
+ try {
478
+ if (human) stdout('Registering MCP server...')
479
+ const result = await SyncMcp.register(name, {
480
+ command,
481
+ global,
482
+ agents,
483
+ })
484
+ if (human) {
485
+ stdout('\r\x1b[K')
486
+ const lines: string[] = []
487
+ lines.push(`✓ Registered ${name} as MCP server`)
488
+ if (result.agents.length > 0) lines.push(` Agents: ${result.agents.join(', ')}`)
489
+ lines.push('')
490
+ lines.push(`Agents can now use ${name} tools.`)
491
+ const suggestions = options.sync?.suggestions
492
+ if (suggestions && suggestions.length > 0) {
493
+ lines.push('')
494
+ lines.push('Try asking:')
495
+ for (const s of suggestions) lines.push(` "${s}"`)
496
+ }
497
+ writeln(lines.join('\n'))
498
+ } else
499
+ writeln(
500
+ Formatter.format(
501
+ { name, command: result.command, agents: result.agents },
502
+ formatExplicit ? formatFlag : 'toon',
503
+ ),
504
+ )
505
+ } catch (err) {
506
+ writeln(
507
+ Formatter.format(
508
+ { code: 'MCP_ADD_FAILED', message: err instanceof Error ? err.message : String(err) },
509
+ formatExplicit ? formatFlag : 'toon',
510
+ ),
511
+ )
512
+ exit(1)
513
+ }
514
+ return
515
+ }
516
+
517
+ // --help takes precedence over --version
518
+ if (version && !help && options.version) {
519
+ writeln(options.version)
520
+ return
521
+ }
522
+
523
+ if (filtered.length === 0) {
524
+ writeln(
525
+ Help.formatRoot(name, {
526
+ description: options.description,
527
+ version: options.version,
528
+ commands: collectHelpCommands(commands),
529
+ root: true,
530
+ }),
531
+ )
532
+ return
533
+ }
534
+
535
+ const resolved = resolveCommand(commands, filtered)
536
+
537
+ // --help after a command → show help for that command
538
+ if (help) {
539
+ if ('help' in resolved || 'error' in resolved) {
540
+ // group or unknown → show root help for that path
541
+ const helpName = 'help' in resolved ? `${name} ${resolved.path}` : name
542
+ const helpDesc = 'help' in resolved ? resolved.description : options.description
543
+ const helpCmds = 'help' in resolved ? resolved.commands : commands
544
+ const isRoot = helpName === name
545
+ writeln(
546
+ Help.formatRoot(helpName, {
547
+ description: helpDesc,
548
+ version: isRoot ? options.version : undefined,
549
+ commands: collectHelpCommands(helpCmds),
550
+ root: isRoot,
551
+ }),
552
+ )
553
+ } else {
554
+ const isRootCmd = resolved.path === name
555
+ const commandName = isRootCmd ? name : `${name} ${resolved.path}`
556
+ writeln(
557
+ Help.formatCommand(commandName, {
558
+ alias: resolved.command.alias as Record<string, string> | undefined,
559
+ description: resolved.command.description,
560
+ version: isRootCmd ? options.version : undefined,
561
+ args: resolved.command.args,
562
+ env: resolved.command.env,
563
+ hint: resolved.command.hint,
564
+ options: resolved.command.options,
565
+ examples: formatExamples(resolved.command.examples),
566
+ usage: resolved.command.usage,
567
+ root: isRootCmd,
568
+ }),
569
+ )
570
+ }
571
+ return
572
+ }
573
+
574
+ if ('help' in resolved) {
575
+ writeln(
576
+ Help.formatRoot(`${name} ${resolved.path}`, {
577
+ description: resolved.description,
578
+ commands: collectHelpCommands(resolved.commands),
579
+ }),
580
+ )
581
+ return
582
+ }
583
+
584
+ const start = performance.now()
585
+
586
+ // Resolve effective format: explicit --format/--json → command default → CLI default → toon
587
+ const resolvedFormat = 'command' in resolved && resolved.command.format
588
+ const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
589
+
590
+ function write(output: Output) {
591
+ const cta = output.meta.cta
592
+ if (human) {
593
+ if (output.ok) writeln(Formatter.format(output.data, format))
594
+ else writeln(formatHumanError(output.error))
595
+ if (cta) writeln(formatHumanCta(cta))
596
+ return
597
+ }
598
+ if (verbose) return writeln(Formatter.format(output, format))
599
+ const base = output.ok ? output.data : output.error
600
+ if (!cta) return writeln(Formatter.format(base, format))
601
+ const payload =
602
+ typeof base === 'object' && base !== null ? { ...base, cta } : { data: base, cta }
603
+ writeln(Formatter.format(payload, format))
604
+ }
605
+
606
+ if ('error' in resolved) {
607
+ const helpCmd = resolved.path ? `${name} ${resolved.path} --help` : `${name} --help`
608
+ const message = `'${resolved.error}' is not a command. See '${helpCmd}' for a list of available commands.`
609
+ if (human) {
610
+ writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }))
611
+ exit(1)
612
+ return
613
+ }
614
+ write({
615
+ ok: false,
616
+ error: { code: 'COMMAND_NOT_FOUND', message },
617
+ meta: {
618
+ command: resolved.error,
619
+ duration: `${Math.round(performance.now() - start)}ms`,
620
+ },
621
+ })
622
+ exit(1)
623
+ return
624
+ }
625
+
626
+ const { command, path, rest } = resolved
627
+
628
+ try {
629
+ const { args, options: parsedOptions } = Parser.parse(rest, {
630
+ alias: command.alias as Record<string, string> | undefined,
631
+ args: command.args,
632
+ options: command.options,
633
+ })
634
+
635
+ const envSource = options.env ?? process.env
636
+ const env = command.env ? Parser.parseEnv(command.env, envSource) : {}
637
+
638
+ const okFn = (data: unknown, meta: { cta?: CtaBlock | undefined } = {}): never => {
639
+ return { [sentinel]: 'ok', data, cta: meta.cta } as never
640
+ }
641
+ const errorFn = (opts: {
642
+ code: string
643
+ message: string
644
+ retryable?: boolean | undefined
645
+ cta?: CtaBlock | undefined
646
+ }): never => {
647
+ return { [sentinel]: 'error', ...opts } as never
648
+ }
649
+
650
+ const result = command.run({
651
+ args,
652
+ env,
653
+ options: parsedOptions,
654
+ ok: okFn,
655
+ error: errorFn,
656
+ })
657
+
658
+ // Streaming path — async generator
659
+ if (isAsyncGenerator(result)) {
660
+ await handleStreaming(result, {
661
+ name,
662
+ path,
663
+ start,
664
+ format,
665
+ formatExplicit,
666
+ human,
667
+ verbose,
668
+ write,
669
+ writeln,
670
+ exit,
671
+ })
672
+ return
673
+ }
674
+
675
+ const awaited = await result
676
+
677
+ if (isSentinel(awaited)) {
678
+ const cta = formatCtaBlock(name, awaited.cta)
679
+ if (awaited[sentinel] === 'ok') {
680
+ write({
681
+ ok: true,
682
+ data: awaited.data,
683
+ meta: {
684
+ command: path,
685
+ duration: `${Math.round(performance.now() - start)}ms`,
686
+ ...(cta ? { cta } : undefined),
687
+ },
688
+ })
689
+ } else {
690
+ write({
691
+ ok: false,
692
+ error: {
693
+ code: awaited.code,
694
+ message: awaited.message,
695
+ ...(awaited.retryable !== undefined ? { retryable: awaited.retryable } : undefined),
696
+ },
697
+ meta: {
698
+ command: path,
699
+ duration: `${Math.round(performance.now() - start)}ms`,
700
+ ...(cta ? { cta } : undefined),
701
+ },
702
+ })
703
+ exit(1)
704
+ }
705
+ } else {
706
+ write({
707
+ ok: true,
708
+ data: awaited,
709
+ meta: {
710
+ command: path,
711
+ duration: `${Math.round(performance.now() - start)}ms`,
712
+ },
713
+ })
714
+ }
715
+ } catch (error) {
716
+ const errorOutput: Output = {
717
+ ok: false,
718
+ error: {
719
+ code:
720
+ error instanceof IncurError
721
+ ? error.code
722
+ : error instanceof ValidationError
723
+ ? 'VALIDATION_ERROR'
724
+ : 'UNKNOWN',
725
+ message: error instanceof Error ? error.message : String(error),
726
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
727
+ ...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
728
+ },
729
+ meta: {
730
+ command: path,
731
+ duration: `${Math.round(performance.now() - start)}ms`,
732
+ },
733
+ }
734
+
735
+ if (human && error instanceof ValidationError) {
736
+ writeln(formatHumanValidationError(name, path, command, error))
737
+ exit(1)
738
+ return
739
+ }
740
+
741
+ write(errorOutput)
742
+ exit(1)
743
+ }
744
+ }
745
+
746
+ /** @internal Formats a validation error for TTY with usage hint. */
747
+ function formatHumanValidationError(
748
+ cli: string,
749
+ path: string,
750
+ command: CommandDefinition<any, any, any>,
751
+ error: ValidationError,
752
+ ): string {
753
+ const lines: string[] = []
754
+ for (const fe of error.fieldErrors) lines.push(`Error: missing required argument <${fe.path}>`)
755
+ lines.push('See below for usage.')
756
+ lines.push('')
757
+ lines.push(
758
+ Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
759
+ alias: command.alias as Record<string, string> | undefined,
760
+ description: command.description,
761
+ args: command.args,
762
+ env: command.env,
763
+ hint: command.hint,
764
+ options: command.options,
765
+ examples: formatExamples(command.examples),
766
+ usage: command.usage,
767
+ }),
768
+ )
769
+ return lines.join('\n')
770
+ }
771
+
772
+ /** @internal Resolves a command from the tree by walking tokens until a leaf is found. */
773
+ function resolveCommand(
774
+ commands: Map<string, CommandEntry>,
775
+ tokens: string[],
776
+ ):
777
+ | { command: CommandDefinition<any, any, any>; path: string; rest: string[] }
778
+ | {
779
+ help: true
780
+ path: string
781
+ description?: string | undefined
782
+ commands: Map<string, CommandEntry>
783
+ }
784
+ | { error: string; path: string } {
785
+ const [first, ...rest] = tokens
786
+
787
+ if (!first || !commands.has(first)) return { error: first ?? '(none)', path: '' }
788
+
789
+ let entry = commands.get(first)!
790
+ const path = [first]
791
+ let remaining = rest
792
+
793
+ while (isGroup(entry)) {
794
+ const next = remaining[0]
795
+ if (!next)
796
+ return {
797
+ help: true,
798
+ path: path.join(' '),
799
+ description: entry.description,
800
+ commands: entry.commands,
801
+ }
802
+
803
+ const child = entry.commands.get(next)
804
+ if (!child) {
805
+ return { error: next, path: path.join(' ') }
806
+ }
807
+
808
+ path.push(next)
809
+ remaining = remaining.slice(1)
810
+ entry = child
811
+ }
812
+
813
+ return { command: entry, path: path.join(' '), rest: remaining }
814
+ }
815
+
816
+ /** @internal Options for serveImpl, extending public serve.Options with internal metadata. */
817
+ declare namespace serveImpl {
818
+ type Options = serve.Options & {
819
+ description?: string | undefined
820
+ /** CLI-level default output format. */
821
+ format?: Formatter.Format | undefined
822
+ mcp?:
823
+ | {
824
+ agents?: string[] | undefined
825
+ command?: string | undefined
826
+ }
827
+ | undefined
828
+ sync?:
829
+ | {
830
+ cwd?: string | undefined
831
+ depth?: number | undefined
832
+ include?: string[] | undefined
833
+ suggestions?: string[] | undefined
834
+ }
835
+ | undefined
836
+ version?: string | undefined
837
+ }
838
+ }
839
+
840
+ /** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
841
+ function extractBuiltinFlags(argv: string[]) {
842
+ let verbose = false
843
+ let llms = false
844
+ let mcp = false
845
+ let help = false
846
+ let version = false
847
+ let format: Formatter.Format = 'toon'
848
+ let formatExplicit = false
849
+ const rest: string[] = []
850
+
851
+ for (let i = 0; i < argv.length; i++) {
852
+ const token = argv[i]!
853
+ if (token === '--verbose') verbose = true
854
+ else if (token === '--llms') llms = true
855
+ else if (token === '--mcp') mcp = true
856
+ else if (token === '--help' || token === '-h') help = true
857
+ else if (token === '--version') version = true
858
+ else if (token === '--json') {
859
+ format = 'json'
860
+ formatExplicit = true
861
+ } else if (token === '--format' && argv[i + 1]) {
862
+ format = argv[i + 1] as Formatter.Format
863
+ formatExplicit = true
864
+ i++
865
+ } else rest.push(token)
866
+ }
867
+
868
+ return { verbose, format, formatExplicit, llms, mcp, help, version, rest }
869
+ }
870
+
871
+ /** @internal Collects immediate child commands/groups for help output. */
872
+ function collectHelpCommands(
873
+ commands: Map<string, CommandEntry>,
874
+ ): { name: string; description?: string | undefined }[] {
875
+ const result: { name: string; description?: string | undefined }[] = []
876
+ for (const [name, entry] of commands) {
877
+ if (isGroup(entry)) result.push({ name, description: entry.description })
878
+ else result.push({ name, description: entry.description })
879
+ }
880
+ return result.sort((a, b) => a.name.localeCompare(b.name))
881
+ }
882
+
883
+ /** Shape of the commands map accumulated through `.command()` chains. */
884
+ export type CommandsMap = Record<
885
+ string,
886
+ { args: Record<string, unknown>; options: Record<string, unknown> }
887
+ >
888
+
889
+ /** @internal Entry stored in a command map — either a leaf definition or a group. */
890
+ type CommandEntry = CommandDefinition<any, any, any> | InternalGroup
891
+
892
+ /** @internal A command group's internal storage. */
893
+ type InternalGroup = {
894
+ _group: true
895
+ description?: string | undefined
896
+ commands: Map<string, CommandEntry>
897
+ }
898
+
899
+ /** @internal Type guard for command groups. */
900
+ function isGroup(entry: CommandEntry): entry is InternalGroup {
901
+ return '_group' in entry
902
+ }
903
+
904
+ /** @internal Maps CLI instances to their command maps. */
905
+ export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
906
+
907
+ /** @internal Maps root CLI instances to their command definitions. */
908
+ const toRootDefinition = new WeakMap<Root, CommandDefinition<any, any, any>>()
909
+
910
+ /** @internal Sentinel symbol for `ok()` and `error()` return values. */
911
+ const sentinel = Symbol.for('incur.sentinel')
912
+
913
+ /** @internal A tagged ok result returned by the `ok` context helper. */
914
+ type OkResult = {
915
+ [sentinel]: 'ok'
916
+ data: unknown
917
+ cta?: CtaBlock | undefined
918
+ }
919
+
920
+ /** @internal A tagged error result returned by the `error` context helper. */
921
+ type ErrorResult = {
922
+ [sentinel]: 'error'
923
+ code: string
924
+ message: string
925
+ retryable?: boolean | undefined
926
+ cta?: CtaBlock | undefined
927
+ }
928
+
929
+ /** @internal A CTA block with a description and list of suggested commands. */
930
+ type CtaBlock<commands extends CommandsMap = Commands> = {
931
+ /** Commands to suggest. */
932
+ commands: Cta<commands>[]
933
+ /** Human-readable label. Defaults to `"Suggested commands:"`. */
934
+ description?: string | undefined
935
+ }
936
+
937
+ /** @internal Formats an error for human-readable TTY output. */
938
+ function formatHumanError(error: {
939
+ code: string
940
+ message: string
941
+ fieldErrors?: FieldError[] | undefined
942
+ }): string {
943
+ const prefix =
944
+ error.code === 'UNKNOWN' || error.code === 'COMMAND_NOT_FOUND'
945
+ ? 'Error'
946
+ : `Error (${error.code})`
947
+ let out = `${prefix}: ${error.message}`
948
+ if (error.fieldErrors) for (const fe of error.fieldErrors) out += `\n ${fe.path}: ${fe.message}`
949
+ return out
950
+ }
951
+
952
+ /** @internal Formats a CTA block for human-readable TTY output. */
953
+ function formatHumanCta(cta: FormattedCtaBlock): string {
954
+ const lines: string[] = ['', cta.description]
955
+ for (const c of cta.commands) {
956
+ const desc = c.description ? ` ${c.description}` : ''
957
+ lines.push(` ${c.command}${desc}`)
958
+ }
959
+ return lines.join('\n')
960
+ }
961
+
962
+ /** @internal Type guard for sentinel results. */
963
+ function isSentinel(value: unknown): value is OkResult | ErrorResult {
964
+ return typeof value === 'object' && value !== null && sentinel in value
965
+ }
966
+
967
+ /** @internal Type guard for async generators returned by streaming `run` handlers. */
968
+ function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
969
+ return (
970
+ typeof value === 'object' &&
971
+ value !== null &&
972
+ Symbol.asyncIterator in value &&
973
+ typeof (value as any).next === 'function'
974
+ )
975
+ }
976
+
977
+ /** @internal Handles streaming output from an async generator `run` handler. */
978
+ async function handleStreaming(
979
+ generator: AsyncGenerator<unknown, unknown, unknown>,
980
+ ctx: {
981
+ name: string
982
+ path: string
983
+ start: number
984
+ format: Formatter.Format
985
+ formatExplicit: boolean
986
+ human: boolean
987
+ verbose: boolean
988
+ write: (output: Output) => void
989
+ writeln: (s: string) => void
990
+ exit: (code: number) => void
991
+ },
992
+ ) {
993
+ // Incremental: human, no explicit format (default toon), or explicit jsonl
994
+ // Buffered: explicit json/yaml/toon/md
995
+ const useJsonl = !ctx.human && ctx.formatExplicit && ctx.format === 'jsonl'
996
+ const incremental = ctx.human || useJsonl || !ctx.formatExplicit
997
+
998
+ if (incremental) {
999
+ // Incremental output: write each chunk as it arrives
1000
+ try {
1001
+ let returnValue: unknown
1002
+ while (true) {
1003
+ const { value, done } = await generator.next()
1004
+ if (done) {
1005
+ returnValue = value
1006
+ break
1007
+ }
1008
+ if (isSentinel(value)) {
1009
+ const tagged = value as any
1010
+ if (tagged[sentinel] === 'error') {
1011
+ if (useJsonl)
1012
+ ctx.writeln(
1013
+ JSON.stringify({
1014
+ type: 'error',
1015
+ ok: false,
1016
+ error: {
1017
+ code: tagged.code,
1018
+ message: tagged.message,
1019
+ ...(tagged.retryable !== undefined
1020
+ ? { retryable: tagged.retryable }
1021
+ : undefined),
1022
+ },
1023
+ }),
1024
+ )
1025
+ else ctx.writeln(formatHumanError({ code: tagged.code, message: tagged.message }))
1026
+ ctx.exit(1)
1027
+ return
1028
+ }
1029
+ }
1030
+ if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
1031
+ else ctx.writeln(Formatter.format(value, 'toon'))
1032
+ }
1033
+
1034
+ // Handle return value — error() or ok() sentinel
1035
+ if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
1036
+ const err = returnValue as ErrorResult
1037
+ if (useJsonl)
1038
+ ctx.writeln(
1039
+ JSON.stringify({
1040
+ type: 'error',
1041
+ ok: false,
1042
+ error: {
1043
+ code: err.code,
1044
+ message: err.message,
1045
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
1046
+ },
1047
+ }),
1048
+ )
1049
+ else ctx.writeln(formatHumanError({ code: err.code, message: err.message }))
1050
+ ctx.exit(1)
1051
+ return
1052
+ }
1053
+
1054
+ const cta =
1055
+ isSentinel(returnValue) && returnValue[sentinel] === 'ok'
1056
+ ? formatCtaBlock(ctx.name, (returnValue as OkResult).cta)
1057
+ : undefined
1058
+
1059
+ if (useJsonl)
1060
+ ctx.writeln(
1061
+ JSON.stringify({
1062
+ type: 'done',
1063
+ ok: true,
1064
+ meta: {
1065
+ command: ctx.path,
1066
+ duration: `${Math.round(performance.now() - ctx.start)}ms`,
1067
+ ...(cta ? { cta } : undefined),
1068
+ },
1069
+ }),
1070
+ )
1071
+ else if (cta) ctx.writeln(formatHumanCta(cta))
1072
+ } catch (error) {
1073
+ if (useJsonl)
1074
+ ctx.writeln(
1075
+ JSON.stringify({
1076
+ type: 'error',
1077
+ ok: false,
1078
+ error: {
1079
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1080
+ message: error instanceof Error ? error.message : String(error),
1081
+ },
1082
+ }),
1083
+ )
1084
+ else
1085
+ ctx.writeln(
1086
+ formatHumanError({
1087
+ code: 'UNKNOWN',
1088
+ message: error instanceof Error ? error.message : String(error),
1089
+ }),
1090
+ )
1091
+ ctx.exit(1)
1092
+ }
1093
+ } else {
1094
+ // Buffered output: collect all chunks, write as single value
1095
+ const chunks: unknown[] = []
1096
+ try {
1097
+ let returnValue: unknown
1098
+ while (true) {
1099
+ const { value, done } = await generator.next()
1100
+ if (done) {
1101
+ returnValue = value
1102
+ break
1103
+ }
1104
+ if (isSentinel(value)) {
1105
+ const tagged = value as any
1106
+ if (tagged[sentinel] === 'error') {
1107
+ ctx.write({
1108
+ ok: false,
1109
+ error: {
1110
+ code: tagged.code,
1111
+ message: tagged.message,
1112
+ ...(tagged.retryable !== undefined ? { retryable: tagged.retryable } : undefined),
1113
+ },
1114
+ meta: {
1115
+ command: ctx.path,
1116
+ duration: `${Math.round(performance.now() - ctx.start)}ms`,
1117
+ },
1118
+ })
1119
+ ctx.exit(1)
1120
+ return
1121
+ }
1122
+ }
1123
+ chunks.push(value)
1124
+ }
1125
+
1126
+ if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
1127
+ const err = returnValue as ErrorResult
1128
+ ctx.write({
1129
+ ok: false,
1130
+ error: {
1131
+ code: err.code,
1132
+ message: err.message,
1133
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
1134
+ },
1135
+ meta: {
1136
+ command: ctx.path,
1137
+ duration: `${Math.round(performance.now() - ctx.start)}ms`,
1138
+ },
1139
+ })
1140
+ ctx.exit(1)
1141
+ return
1142
+ }
1143
+
1144
+ const cta =
1145
+ isSentinel(returnValue) && returnValue[sentinel] === 'ok'
1146
+ ? formatCtaBlock(ctx.name, (returnValue as OkResult).cta)
1147
+ : undefined
1148
+
1149
+ ctx.write({
1150
+ ok: true,
1151
+ data: chunks,
1152
+ meta: {
1153
+ command: ctx.path,
1154
+ duration: `${Math.round(performance.now() - ctx.start)}ms`,
1155
+ ...(cta ? { cta } : undefined),
1156
+ },
1157
+ })
1158
+ } catch (error) {
1159
+ ctx.write({
1160
+ ok: false,
1161
+ error: {
1162
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1163
+ message: error instanceof Error ? error.message : String(error),
1164
+ },
1165
+ meta: {
1166
+ command: ctx.path,
1167
+ duration: `${Math.round(performance.now() - ctx.start)}ms`,
1168
+ },
1169
+ })
1170
+ ctx.exit(1)
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ /** @internal Formats a CTA block into the output envelope shape. */
1176
+ function formatCtaBlock(name: string, block: CtaBlock | undefined): FormattedCtaBlock | undefined {
1177
+ if (!block || block.commands.length === 0) return undefined
1178
+ return {
1179
+ description: block.description ?? 'Suggested commands:',
1180
+ commands: block.commands.map((c) => formatCta(name, c)),
1181
+ }
1182
+ }
1183
+
1184
+ /** @internal Formats a CTA by prefixing the CLI name. Handles string and object forms. */
1185
+ function formatCta(name: string, cta: Cta): FormattedCta {
1186
+ if (typeof cta === 'string') return { command: `${name} ${cta}` }
1187
+ const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `
1188
+ let cmd = `${prefix}${cta.command}`
1189
+ if (cta.args)
1190
+ for (const [key, value] of Object.entries(cta.args))
1191
+ cmd += value === true ? ` <${key}>` : ` ${value}`
1192
+ if (cta.options)
1193
+ for (const [key, value] of Object.entries(cta.options))
1194
+ cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`
1195
+ return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) }
1196
+ }
1197
+
1198
+ /** @internal Builds the `--llms` manifest from the command tree. */
1199
+ function buildManifest(commands: Map<string, CommandEntry>, prefix: string[] = []) {
1200
+ return {
1201
+ version: 'incur.v1',
1202
+ commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
1203
+ }
1204
+ }
1205
+
1206
+ /** @internal Recursively collects leaf commands with their full paths. */
1207
+ function collectCommands(
1208
+ commands: Map<string, CommandEntry>,
1209
+ prefix: string[],
1210
+ ): {
1211
+ name: string
1212
+ description?: string | undefined
1213
+ schema?: Record<string, unknown> | undefined
1214
+ examples?: { command: string; description?: string | undefined }[] | undefined
1215
+ }[] {
1216
+ const result: ReturnType<typeof collectCommands> = []
1217
+ for (const [name, entry] of commands) {
1218
+ const path = [...prefix, name]
1219
+ if (isGroup(entry)) {
1220
+ result.push(...collectCommands(entry.commands, path))
1221
+ } else {
1222
+ const cmd: (typeof result)[number] = { name: path.join(' ') }
1223
+ if (entry.description) cmd.description = entry.description
1224
+
1225
+ const inputSchema = buildInputSchema(entry.args, entry.env, entry.options)
1226
+ const outputSchema = entry.output ? Schema.toJsonSchema(entry.output) : undefined
1227
+ if (inputSchema || outputSchema) {
1228
+ cmd.schema = {}
1229
+ if (inputSchema?.args) cmd.schema.args = inputSchema.args
1230
+ if (inputSchema?.env) cmd.schema.env = inputSchema.env
1231
+ if (inputSchema?.options) cmd.schema.options = inputSchema.options
1232
+ if (outputSchema) cmd.schema.output = outputSchema
1233
+ }
1234
+
1235
+ const examples = formatExamples(entry.examples)
1236
+ if (examples) {
1237
+ const cmdName = path.join(' ')
1238
+ cmd.examples = examples.map((e) => ({
1239
+ ...e,
1240
+ command: e.command ? `${cmdName} ${e.command}` : cmdName,
1241
+ }))
1242
+ }
1243
+ result.push(cmd)
1244
+ }
1245
+ }
1246
+ return result
1247
+ }
1248
+
1249
+ /** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
1250
+ function collectSkillCommands(
1251
+ commands: Map<string, CommandEntry>,
1252
+ prefix: string[],
1253
+ groups: Map<string, string>,
1254
+ ): Skill.CommandInfo[] {
1255
+ const result: Skill.CommandInfo[] = []
1256
+ for (const [name, entry] of commands) {
1257
+ const path = [...prefix, name]
1258
+ if (isGroup(entry)) {
1259
+ if (entry.description) groups.set(path.join(' '), entry.description)
1260
+ result.push(...collectSkillCommands(entry.commands, path, groups))
1261
+ } else {
1262
+ const cmd: Skill.CommandInfo = { name: path.join(' ') }
1263
+ if (entry.description) cmd.description = entry.description
1264
+ if (entry.args) cmd.args = entry.args
1265
+ if (entry.env) cmd.env = entry.env
1266
+ if (entry.hint) cmd.hint = entry.hint
1267
+ if (entry.options) cmd.options = entry.options
1268
+ if (entry.output) cmd.output = entry.output
1269
+ const examples = formatExamples(entry.examples)
1270
+ if (examples) {
1271
+ const cmdName = path.join(' ')
1272
+ cmd.examples = examples.map((e) => ({
1273
+ ...e,
1274
+ command: e.command ? `${cmdName} ${e.command}` : cmdName,
1275
+ }))
1276
+ }
1277
+ result.push(cmd)
1278
+ }
1279
+ }
1280
+ return result.sort((a, b) => a.name.localeCompare(b.name))
1281
+ }
1282
+
1283
+ /** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
1284
+ export function formatExamples(
1285
+ examples: Example<any, any>[] | undefined,
1286
+ ): { command: string; description?: string }[] | undefined {
1287
+ if (!examples || examples.length === 0) return undefined
1288
+ return examples.map((ex) => {
1289
+ const parts: string[] = []
1290
+ if (ex.args) for (const value of Object.values(ex.args)) parts.push(String(value))
1291
+ if (ex.options)
1292
+ for (const [key, value] of Object.entries(ex.options)) parts.push(`--${key} ${value}`)
1293
+ const result: { command: string; description?: string } = { command: parts.join(' ') }
1294
+ if (ex.description) result.description = ex.description
1295
+ return result
1296
+ })
1297
+ }
1298
+
1299
+ /** @internal Builds separate args, env, and options JSON Schemas. */
1300
+ function buildInputSchema(
1301
+ args: z.ZodObject<any> | undefined,
1302
+ env: z.ZodObject<any> | undefined,
1303
+ options: z.ZodObject<any> | undefined,
1304
+ ):
1305
+ | {
1306
+ args?: Record<string, unknown> | undefined
1307
+ env?: Record<string, unknown> | undefined
1308
+ options?: Record<string, unknown> | undefined
1309
+ }
1310
+ | undefined {
1311
+ if (!args && !env && !options) return undefined
1312
+ const result: {
1313
+ args?: Record<string, unknown> | undefined
1314
+ env?: Record<string, unknown> | undefined
1315
+ options?: Record<string, unknown> | undefined
1316
+ } = {}
1317
+ if (args) result.args = Schema.toJsonSchema(args)
1318
+ if (env) result.env = Schema.toJsonSchema(env)
1319
+ if (options) result.options = Schema.toJsonSchema(options)
1320
+ return result
1321
+ }
1322
+
1323
+ /** @internal A usage example for a command, typed against its args and options schemas. */
1324
+ type Example<
1325
+ args extends z.ZodObject<any> | undefined,
1326
+ options extends z.ZodObject<any> | undefined,
1327
+ > = {
1328
+ /** Positional arguments for this example. */
1329
+ args?: args extends z.ZodObject<any> ? Partial<z.output<args>> | undefined : undefined
1330
+ /** A short description of what this example demonstrates. */
1331
+ description?: string | undefined
1332
+ /** Named options for this example. */
1333
+ options?: options extends z.ZodObject<any> ? Partial<z.output<options>> | undefined : undefined
1334
+ }
1335
+
1336
+ /** @internal A usage pattern shown in help output. */
1337
+ type Usage<
1338
+ args extends z.ZodObject<any> | undefined,
1339
+ options extends z.ZodObject<any> | undefined,
1340
+ > = {
1341
+ /** Positional arguments to include. Use `true` to show as `<name>`. */
1342
+ args?: args extends z.ZodObject<any>
1343
+ ? Partial<Record<keyof z.output<args>, true>> | undefined
1344
+ : undefined
1345
+ /** Named options to include. Use `true` to show as `--name <name>`. */
1346
+ options?: options extends z.ZodObject<any>
1347
+ ? Partial<Record<keyof z.output<options>, true>> | undefined
1348
+ : undefined
1349
+ /** Text prepended before the command (e.g. `"cat file.txt |"`). */
1350
+ prefix?: string | undefined
1351
+ /** Text appended after the command (e.g. `"| head"`). */
1352
+ suffix?: string | undefined
1353
+ }
1354
+
1355
+ /** @internal Inferred output type of a Zod schema, or `{}` when the schema is not provided. */
1356
+ type InferOutput<schema extends z.ZodObject<any> | undefined> =
1357
+ schema extends z.ZodObject<any> ? z.output<schema> : {}
1358
+
1359
+ /** @internal Inferred return type for a command handler. */
1360
+ type InferReturn<output extends z.ZodType | undefined> = output extends z.ZodType
1361
+ ? z.output<output>
1362
+ : unknown
1363
+
1364
+ /** @internal The output envelope written to stdout. */
1365
+ type Output = OneOf<
1366
+ | {
1367
+ /** The command's return data. */
1368
+ data: unknown
1369
+ /** Request metadata. */
1370
+ meta: Output.Meta
1371
+ /** Whether the command succeeded. */
1372
+ ok: true
1373
+ }
1374
+ | {
1375
+ /** Error details. */
1376
+ error: {
1377
+ /** Machine-readable error code. */
1378
+ code: string
1379
+ /** Per-field validation errors. */
1380
+ fieldErrors?: FieldError[] | undefined
1381
+ /** Human-readable error message. */
1382
+ message: string
1383
+ /** Whether the operation can be retried. */
1384
+ retryable?: boolean | undefined
1385
+ }
1386
+ /** Request metadata. */
1387
+ meta: Output.Meta
1388
+ /** Whether the command succeeded. */
1389
+ ok: false
1390
+ }
1391
+ >
1392
+
1393
+ /** @internal */
1394
+ declare namespace Output {
1395
+ /** Shared metadata included in every envelope. */
1396
+ type Meta = {
1397
+ /** The command that was invoked. */
1398
+ command: string
1399
+ /** Suggested next commands. */
1400
+ cta?: FormattedCtaBlock | undefined
1401
+ /** Wall-clock duration of the command. */
1402
+ duration: string
1403
+ }
1404
+ }
1405
+
1406
+ /** @internal Defines a command's schema, handler, and metadata. */
1407
+ type CommandDefinition<
1408
+ args extends z.ZodObject<any> | undefined = undefined,
1409
+ env extends z.ZodObject<any> | undefined = undefined,
1410
+ options extends z.ZodObject<any> | undefined = undefined,
1411
+ output extends z.ZodType | undefined = undefined,
1412
+ > = {
1413
+ /** Map of option names to single-char aliases. */
1414
+ alias?: options extends z.ZodObject<any>
1415
+ ? Partial<Record<keyof z.output<options>, string>>
1416
+ : Record<string, string> | undefined
1417
+ /** Zod schema for positional arguments. */
1418
+ args?: args | undefined
1419
+ /** A short description of what the command does. */
1420
+ description?: string | undefined
1421
+ /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
1422
+ env?: env | undefined
1423
+ /** Usage examples for this command. */
1424
+ examples?: Example<args, options>[] | undefined
1425
+ /** Default output format. Overridden by `--format` or `--json`. */
1426
+ format?: Formatter.Format | undefined
1427
+ /** Plain text hint displayed after examples and before global options. */
1428
+ hint?: string | undefined
1429
+ /** Zod schema for named options/flags. */
1430
+ options?: options | undefined
1431
+ /** Zod schema for the command's return value. */
1432
+ output?: output | undefined
1433
+ /** Alternative usage patterns shown in help output. */
1434
+ usage?: Usage<args, options>[] | undefined
1435
+ /** The command handler. Return a value for single-return, or use `async *run` to stream chunks. */
1436
+ run(context: {
1437
+ args: InferOutput<args>
1438
+ /** Parsed environment variables. */
1439
+ env: InferOutput<env>
1440
+ /** Return an error result with optional CTAs. */
1441
+ error: (options: {
1442
+ code: string
1443
+ cta?: CtaBlock | undefined
1444
+ message: string
1445
+ retryable?: boolean | undefined
1446
+ }) => never
1447
+ /** Return a success result with optional metadata (e.g. CTAs). */
1448
+ ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
1449
+ options: InferOutput<options>
1450
+ }):
1451
+ | InferReturn<output>
1452
+ | Promise<InferReturn<output>>
1453
+ | AsyncGenerator<InferReturn<output>, unknown, unknown>
1454
+ }
1455
+
1456
+ /** @internal A formatted CTA block as it appears in the output envelope. */
1457
+ type FormattedCtaBlock = {
1458
+ /** Formatted command suggestions. */
1459
+ commands: FormattedCta[]
1460
+ /** Human-readable label for the CTA block. */
1461
+ description: string
1462
+ }
1463
+
1464
+ /** @internal A formatted CTA as it appears in the output envelope. */
1465
+ type FormattedCta = {
1466
+ /** The full command string with args and options folded in. */
1467
+ command: string
1468
+ /** A short description of what the command does. */
1469
+ description?: string | undefined
1470
+ }