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/SKILL.md ADDED
@@ -0,0 +1,664 @@
1
+ ---
2
+ name: incur
3
+ description: incur is a TypeScript framework for building CLIs that work for both AI agents and humans. Use when creating new CLIs.
4
+ command: incur
5
+ ---
6
+
7
+ # incur
8
+
9
+ TypeScript framework for building CLIs for agents and human consumption. Strictly typed schemas for arguments and options, structured output envelopes, auto-generated skill files, and agent discovery via Skills, MCP, and `--llms`.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm i incur
15
+ ```
16
+
17
+ ```sh
18
+ pnpm i incur
19
+ ```
20
+
21
+ ```sh
22
+ bun i incur
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```ts
28
+ import { Cli, z } from 'incur'
29
+
30
+ const cli = Cli.create('greet', {
31
+ description: 'A greeting CLI',
32
+ args: z.object({
33
+ name: z.string().describe('Name to greet'),
34
+ }),
35
+ run({ args }) {
36
+ return { message: `hello ${args.name}` }
37
+ },
38
+ })
39
+
40
+ cli.serve()
41
+ ```
42
+
43
+ ```sh
44
+ greet world
45
+ # → message: hello world
46
+ ```
47
+
48
+ ## Creating a CLI
49
+
50
+ `Cli.create()` is the entry point. It has two modes:
51
+
52
+ ### Single-command CLI
53
+
54
+ Pass `run` to create a CLI with no subcommands:
55
+
56
+ ```ts
57
+ const cli = Cli.create('tool', {
58
+ description: 'Does one thing',
59
+ args: z.object({ file: z.string() }),
60
+ run({ args, options }) {
61
+ return { processed: args.file }
62
+ },
63
+ })
64
+ ```
65
+
66
+ ### Router CLI (subcommands)
67
+
68
+ Omit `run` to create a CLI that registers subcommands via `.command()`:
69
+
70
+ ```ts
71
+ const cli = Cli.create('gh', {
72
+ version: '1.0.0',
73
+ description: 'GitHub CLI',
74
+ })
75
+
76
+ cli.command('status', {
77
+ description: 'Show repo status',
78
+ run() {
79
+ return { clean: true }
80
+ },
81
+ })
82
+
83
+ cli.serve()
84
+ ```
85
+
86
+ ## Commands
87
+
88
+ ### Registering commands
89
+
90
+ ```ts
91
+ cli.command('install', {
92
+ description: 'Install a package',
93
+ args: z.object({
94
+ package: z.string().optional().describe('Package name'),
95
+ }),
96
+ options: z.object({
97
+ saveDev: z.boolean().optional().describe('Save as dev dependency'),
98
+ global: z.boolean().optional().describe('Install globally'),
99
+ }),
100
+ alias: { saveDev: 'D', global: 'g' },
101
+ output: z.object({
102
+ added: z.number(),
103
+ packages: z.number(),
104
+ }),
105
+ examples: [
106
+ { args: { package: 'express' }, description: 'Install a package' },
107
+ {
108
+ args: { package: 'vitest' },
109
+ options: { saveDev: true },
110
+ description: 'Install as dev dependency',
111
+ },
112
+ ],
113
+ run({ args, options }) {
114
+ return { added: 1, packages: 451 }
115
+ },
116
+ })
117
+ ```
118
+
119
+ `.command()` is chainable — it returns the CLI instance:
120
+
121
+ ```ts
122
+ cli
123
+ .command('ping', { run: () => ({ pong: true }) })
124
+ .command('version', { run: () => ({ version: '1.0.0' }) })
125
+ ```
126
+
127
+ ### Subcommand groups
128
+
129
+ Create a sub-CLI and mount it as a command group:
130
+
131
+ ```ts
132
+ const cli = Cli.create('gh', { description: 'GitHub CLI' })
133
+
134
+ const pr = Cli.create('pr', { description: 'Pull request commands' })
135
+
136
+ pr.command('list', {
137
+ description: 'List pull requests',
138
+ options: z.object({
139
+ state: z.enum(['open', 'closed', 'all']).default('open'),
140
+ }),
141
+ run({ options }) {
142
+ return { prs: [], state: options.state }
143
+ },
144
+ })
145
+
146
+ pr.command('view', {
147
+ description: 'View a pull request',
148
+ args: z.object({ number: z.number() }),
149
+ run({ args }) {
150
+ return { number: args.number, title: 'Fix bug' }
151
+ },
152
+ })
153
+
154
+ // Mount onto the parent CLI
155
+ cli.command(pr)
156
+
157
+ cli.serve()
158
+ ```
159
+
160
+ ```sh
161
+ gh pr list --state closed
162
+ gh pr view 42
163
+ ```
164
+
165
+ Groups nest arbitrarily:
166
+
167
+ ```ts
168
+ const cli = Cli.create('gh', { description: 'GitHub CLI' })
169
+ const pr = Cli.create('pr', { description: 'Pull requests' })
170
+ const review = Cli.create('review', { description: 'Review commands' })
171
+
172
+ review.command('approve', { run: () => ({ approved: true }) })
173
+ pr.command(review)
174
+ cli.command(pr)
175
+ // → gh pr review approve
176
+ ```
177
+
178
+ ## Arguments & Options
179
+
180
+ All schemas use Zod. Arguments are positional (assigned by schema key order). Options are named flags.
181
+
182
+ ### Arguments
183
+
184
+ ```ts
185
+ args: z.object({
186
+ repo: z.string().describe('Repository in owner/repo format'),
187
+ branch: z.string().optional().describe('Branch name'),
188
+ })
189
+ ```
190
+
191
+ ```sh
192
+ tool clone owner/repo main
193
+ # ^^^^^^^^^^ ^^^^
194
+ # repo branch
195
+ ```
196
+
197
+ ### Options
198
+
199
+ ```ts
200
+ options: z.object({
201
+ state: z.enum(['open', 'closed']).default('open').describe('Filter by state'),
202
+ limit: z.number().default(30).describe('Max results'),
203
+ label: z.array(z.string()).optional().describe('Filter by labels'),
204
+ verbose: z.boolean().optional().describe('Show details'),
205
+ })
206
+ ```
207
+
208
+ Supported parsing:
209
+
210
+ - `--flag value` and `--flag=value`
211
+ - `-f value` short aliases (via `alias` property)
212
+ - `--verbose` boolean flags (`true`), `--no-verbose` (`false`)
213
+ - `--label bug --label feature` array options
214
+ - Automatic type coercion (string → number, string → boolean)
215
+ - Defaults from `.default()`, optionality from `.optional()`
216
+
217
+ ### Aliases
218
+
219
+ ```ts
220
+ alias: { state: 's', limit: 'l' }
221
+ ```
222
+
223
+ ```sh
224
+ tool list -s closed -l 10
225
+ ```
226
+
227
+ ### Environment variables
228
+
229
+ ```ts
230
+ env: z.object({
231
+ NPM_TOKEN: z.string().optional().describe('Auth token'),
232
+ NPM_REGISTRY: z.string().default('https://registry.npmjs.org').describe('Registry URL'),
233
+ })
234
+ ```
235
+
236
+ Environment variables are parsed from `process.env` and validated against the Zod schema.
237
+
238
+ ### Usage patterns
239
+
240
+ Define alternative usage patterns to show in `--help` instead of the auto-generated synopsis:
241
+
242
+ ```ts
243
+ Cli.create('curl.md', {
244
+ args: z.object({ url: z.string() }),
245
+ options: z.object({ objective: z.string().optional() }),
246
+ usage: [
247
+ { args: { url: true } },
248
+ { args: { url: true }, options: { objective: true } },
249
+ { prefix: 'cat file.txt |', suffix: '| head' },
250
+ ],
251
+ run({ args }) {
252
+ return { content: '...' }
253
+ },
254
+ })
255
+ ```
256
+
257
+ Renders in help as:
258
+
259
+ ```
260
+ Usage: curl.md <url>
261
+ curl.md <url> --objective <objective>
262
+ cat file.txt | curl.md | head
263
+ ```
264
+
265
+ Each usage entry supports:
266
+
267
+ | Property | Type | Description |
268
+ | --------- | ---------------------------- | ------------------------------------------------ |
269
+ | `args` | `Partial<Record<key, true>>` | Argument keys to include as `<key>` placeholders |
270
+ | `options` | `Partial<Record<key, true>>` | Option keys to include as `--key <key>` flags |
271
+ | `prefix` | `string` | Text prepended before the command (e.g. piping) |
272
+ | `suffix` | `string` | Text appended after the command |
273
+
274
+ Both `args` and `options` are strictly typed from the Zod schemas — only valid keys are allowed.
275
+
276
+ Usage patterns also work on subcommands via `.command()`.
277
+
278
+ ## Output
279
+
280
+ Every command returns data. incur wraps it in a structured envelope and serializes to the requested format.
281
+
282
+ ### Output schema
283
+
284
+ Define `output` to declare the return shape:
285
+
286
+ ```ts
287
+ cli.command('info', {
288
+ output: z.object({
289
+ name: z.string(),
290
+ version: z.string(),
291
+ }),
292
+ run() {
293
+ return { name: 'express', version: '4.21.2' }
294
+ },
295
+ })
296
+ ```
297
+
298
+ When `output` is provided, TypeScript enforces that `run()` returns the correct shape.
299
+
300
+ ### Formats
301
+
302
+ Control with `--format <fmt>` or `--json`:
303
+
304
+ | Flag | Format | Description |
305
+ | --------------- | -------- | -------------------------------------------- |
306
+ | _(default)_ | TOON | Token-efficient, ~40% fewer tokens than JSON |
307
+ | `--format json` | JSON | `JSON.parse()`-safe |
308
+ | `--format yaml` | YAML | Human-readable |
309
+ | `--format md` | Markdown | Tables for docs/issues |
310
+
311
+ ### Envelope
312
+
313
+ With `--verbose`, the full envelope is emitted:
314
+
315
+ ```sh
316
+ tool info express --verbose
317
+ ```
318
+
319
+ ```
320
+ ok: true
321
+ data:
322
+ name: express
323
+ version: 4.21.2
324
+ meta:
325
+ command: info
326
+ duration: 12ms
327
+ ```
328
+
329
+ Without `--verbose`, only `data` is emitted. On errors, only the `error` block is emitted.
330
+
331
+ ### TTY detection
332
+
333
+ incur adapts output based on whether stdout is a TTY:
334
+
335
+ | Scenario | TTY (human) | Non-TTY (agent/pipe) |
336
+ | --------------------- | ----------------------- | -------------------- |
337
+ | Command output | Formatted data only | TOON envelope |
338
+ | Errors | Human-readable message | Error envelope |
339
+ | `--help` | Pretty help text | Same |
340
+ | `--json` / `--format` | Overrides to structured | Same |
341
+
342
+ ## Structured Errors
343
+
344
+ ### `ok()` and `error()` context helpers
345
+
346
+ Use the context helpers for explicit result control:
347
+
348
+ ```ts
349
+ run({ args, ok, error }) {
350
+ const item = await db.find(args.id)
351
+ if (!item)
352
+ return error({
353
+ code: 'NOT_FOUND',
354
+ message: `Item ${args.id} not found`,
355
+ retryable: false,
356
+ })
357
+ return ok(item)
358
+ }
359
+ ```
360
+
361
+ ### CTAs (Call to Action)
362
+
363
+ Suggest next commands to guide agents on success:
364
+
365
+ ```ts
366
+ run({ args, ok }) {
367
+ const result = { id: 42, name: args.name }
368
+ return ok(result, {
369
+ cta: {
370
+ description: 'Suggested commands:',
371
+ commands: [
372
+ { command: 'get', args: { id: 42 }, description: 'View the item' },
373
+ 'list',
374
+ ],
375
+ },
376
+ })
377
+ }
378
+ ```
379
+
380
+ Or on errors, to help agents self-correct:
381
+
382
+ ```ts
383
+ run({ args, error }) {
384
+ const token = process.env.GH_TOKEN
385
+ if (!token)
386
+ return error({
387
+ code: 'NOT_AUTHENTICATED',
388
+ message: 'GitHub token not found',
389
+ retryable: true,
390
+ cta: {
391
+ description: 'To authenticate:',
392
+ commands: [
393
+ { command: 'auth login', description: 'Log in to GitHub' },
394
+ { command: 'config set', options: { token: true }, description: 'Set token manually' },
395
+ ],
396
+ },
397
+ })
398
+ // ...
399
+ }
400
+ ```
401
+
402
+ ## Agent Discovery
403
+
404
+ ### MCP Server
405
+
406
+ Every incur CLI has built-in Model Context Protocol (MCP) support — exposing commands as MCP tools that agents can call directly.
407
+
408
+ #### `mcp add` built-in command
409
+
410
+ Register the CLI as an MCP server for your agents:
411
+
412
+ ```sh
413
+ my-cli mcp add
414
+ ```
415
+
416
+ This registers the CLI with your agent's MCP config. Works with Claude Code, Cursor, Amp, and others out of the box.
417
+
418
+ Options:
419
+
420
+ | Flag | Description |
421
+ | ----------------- | -------------------------------------------------------- |
422
+ | `-c`, `--command` | Override the command agents will run to start the server |
423
+ | `--agent <agent>` | Target a specific agent (e.g. `claude-code`, `cursor`) |
424
+ | `--no-global` | Install to project instead of globally |
425
+
426
+ #### `--mcp` flag
427
+
428
+ Start the CLI as an MCP stdio server:
429
+
430
+ ```sh
431
+ my-cli --mcp
432
+ ```
433
+
434
+ This exposes all commands as MCP tools over stdin/stdout. Command groups are flattened with underscores (e.g. `pr_list`, `pr_view`). Arguments and options are merged into a single flat input schema.
435
+
436
+ ### Skills
437
+
438
+ All incur-based CLIs can auto-generate and install agent skill files with `skills add`:
439
+
440
+ ```sh
441
+ my-cli skills add
442
+ ```
443
+
444
+ This generates Markdown skill files from your command definitions and installs them so agents discover your CLI automatically.
445
+
446
+ #### Configuration
447
+
448
+ It is also possible to configure `skills add`:
449
+
450
+ ```ts
451
+ const cli = Cli.create('my-cli', {
452
+ sync: {
453
+ depth: 1,
454
+ include: ['_root'],
455
+ suggestions: ['install react as a dependency', 'check for outdated packages'],
456
+ },
457
+ })
458
+ ```
459
+
460
+ | Option | Type | Description |
461
+ | ------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
462
+ | `depth` | `number` | Grouping depth for skill files. `0` = single file, `1` = one per top-level command. Default: `1` |
463
+ | `include` | `string[]` | Glob patterns for additional SKILL.md files to include. Use `'_root'` for the project-level SKILL.md |
464
+ | `suggestions` | `string[]` | Example prompts shown after sync to help users get started |
465
+
466
+ ### `--llms` flag
467
+
468
+ Every incur CLI gets a built-in `--llms` flag that outputs a machine-readable manifest of all commands:
469
+
470
+ ```sh
471
+ tool --llms
472
+ ```
473
+
474
+ Outputs Markdown skill documentation by default.
475
+
476
+ ```md
477
+ # tool install
478
+
479
+ Install a package
480
+
481
+ ## Arguments
482
+
483
+ | Name | Type | Required | Description |
484
+ | --------- | -------- | -------- | ----------------------- |
485
+ | `package` | `string` | no | Package name to install |
486
+
487
+ ## Options
488
+
489
+ | Flag | Type | Default | Description |
490
+ | ----------- | --------- | ------- | ---------------------- |
491
+ | `--saveDev` | `boolean` | | Save as dev dependency |
492
+ | `--global` | `boolean` | | Install globally |
493
+ ```
494
+
495
+ Use `--llms --format json` for JSON schema manifest:
496
+
497
+ ```json
498
+ {
499
+ "version": "incur.v1",
500
+ "commands": [
501
+ {
502
+ "name": "install",
503
+ "description": "Install a package",
504
+ "schema": {
505
+ "args": { "type": "object", "properties": { "package": { "type": "string" } } },
506
+ "options": { "type": "object", "properties": { "saveDev": { "type": "boolean" } } },
507
+ "output": { "type": "object", "properties": { "added": { "type": "number" } } }
508
+ }
509
+ }
510
+ ]
511
+ }
512
+ ```
513
+
514
+ ## Built-in Flags
515
+
516
+ | Flag | Description |
517
+ | ---------------- | -------------------------------------------- |
518
+ | `--help`, `-h` | Show help for the CLI or a specific command |
519
+ | `--version` | Print CLI version |
520
+ | `--llms` | Output agent-readable command manifest |
521
+ | `--mcp` | Start as an MCP stdio server |
522
+ | `--json` | Shorthand for `--format json` |
523
+ | `--format <fmt>` | Output format: `toon`, `json`, `yaml`, `md` |
524
+ | `--verbose` | Include full envelope (`ok`, `data`, `meta`) |
525
+
526
+ ## Examples
527
+
528
+ ### Typed examples on commands
529
+
530
+ ```ts
531
+ cli.command('deploy', {
532
+ args: z.object({ env: z.enum(['staging', 'production']) }),
533
+ options: z.object({ force: z.boolean().optional() }),
534
+ examples: [
535
+ { args: { env: 'staging' }, description: 'Deploy to staging' },
536
+ { args: { env: 'production' }, options: { force: true }, description: 'Force deploy to prod' },
537
+ ],
538
+ run({ args }) {
539
+ return { deployed: args.env }
540
+ },
541
+ })
542
+ ```
543
+
544
+ Examples appear in `--help` output and generated skill files.
545
+
546
+ ### Hints
547
+
548
+ ```ts
549
+ cli.command('publish', {
550
+ hint: 'Requires NPM_TOKEN to be set in your environment.',
551
+ // ...
552
+ })
553
+ ```
554
+
555
+ Hints are displayed after examples in help output and included in skill files.
556
+
557
+ ## Serving
558
+
559
+ Call `.serve()` to parse `process.argv` and run:
560
+
561
+ ```ts
562
+ cli.serve()
563
+ ```
564
+
565
+ For testing, pass custom argv and DI overrides:
566
+
567
+ ```ts
568
+ let output = ''
569
+ await cli.serve(['install', 'express', '--json'], {
570
+ stdout(s) {
571
+ output += s
572
+ },
573
+ exit() {},
574
+ })
575
+ ```
576
+
577
+ ### `serve()` options
578
+
579
+ | Option | Type | Description |
580
+ | -------- | ------------------------------------- | ------------------------------ |
581
+ | `stdout` | `(s: string) => void` | Override stdout writer |
582
+ | `exit` | `(code: number) => void` | Override exit handler |
583
+ | `env` | `Record<string, string \| undefined>` | Override environment variables |
584
+
585
+ ## Type Generation
586
+
587
+ Generate type definitions for your CLI's command map to get typed CTAs:
588
+
589
+ ```sh
590
+ incur gen
591
+ ```
592
+
593
+ This creates a `incur.generated.ts` file that registers your commands on the `Cli.Commands` type, enabling autocomplete on CTA command names, args, and options.
594
+
595
+ ## Full Example
596
+
597
+ ```ts
598
+ import { Cli, z } from 'incur'
599
+
600
+ const cli = Cli.create('npm', {
601
+ version: '10.9.2',
602
+ description: 'The package manager for JavaScript.',
603
+ sync: {
604
+ suggestions: ['install react as a dependency', 'check for outdated packages'],
605
+ },
606
+ })
607
+
608
+ cli.command('install', {
609
+ description: 'Install a package',
610
+ args: z.object({
611
+ package: z.string().optional().describe('Package name to install'),
612
+ }),
613
+ options: z.object({
614
+ saveDev: z.boolean().optional().describe('Save as dev dependency'),
615
+ global: z.boolean().optional().describe('Install globally'),
616
+ }),
617
+ alias: { saveDev: 'D', global: 'g' },
618
+ output: z.object({
619
+ added: z.number().describe('Number of packages added'),
620
+ packages: z.number().describe('Total packages'),
621
+ }),
622
+ examples: [
623
+ { args: { package: 'express' }, description: 'Install a package' },
624
+ {
625
+ args: { package: 'vitest' },
626
+ options: { saveDev: true },
627
+ description: 'Install as dev dependency',
628
+ },
629
+ ],
630
+ run({ args }) {
631
+ if (!args.package) return { added: 120, packages: 450 }
632
+ return { added: 1, packages: 451 }
633
+ },
634
+ })
635
+
636
+ cli.command('outdated', {
637
+ description: 'Check for outdated packages',
638
+ options: z.object({
639
+ global: z.boolean().describe('Check global packages'),
640
+ }),
641
+ alias: { global: 'g' },
642
+ output: z.object({
643
+ packages: z.array(
644
+ z.object({
645
+ name: z.string(),
646
+ current: z.string(),
647
+ wanted: z.string(),
648
+ latest: z.string(),
649
+ }),
650
+ ),
651
+ }),
652
+ run() {
653
+ return {
654
+ packages: [{ name: 'express', current: '4.18.0', wanted: '4.21.2', latest: '4.21.2' }],
655
+ }
656
+ },
657
+ })
658
+
659
+ cli.serve()
660
+
661
+ export default cli
662
+ ```
663
+
664
+ > Always `export default cli` so that `incur gen` can import it and generate types.