incur 0.3.3 → 0.3.5

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.
@@ -0,0 +1,275 @@
1
+ import { z } from 'zod';
2
+ import { IncurError, ValidationError } from '../Errors.js';
3
+ import * as Parser from '../Parser.js';
4
+ /** @internal Sentinel symbol for `ok()` and `error()` return values. */
5
+ const sentinel = Symbol.for('incur.sentinel');
6
+ /** @internal Unified command execution used by CLI, HTTP, and MCP transports. */
7
+ export async function execute(command, options) {
8
+ const { argv, inputOptions, agent, format, formatExplicit, name, path, version, envSource = process.env, env: envSchema, vars: varsSchema, middlewares = [], } = options;
9
+ const parseMode = options.parseMode ?? 'argv';
10
+ const varsMap = varsSchema ? varsSchema.parse({}) : {};
11
+ let result;
12
+ // For streaming with middleware: runCommand suspends on streamConsumed so middleware "after"
13
+ // runs after the stream is consumed. The wrapped generator resolves it in its finally block.
14
+ // resultReady signals that result has been set (for streams, before the chain finishes).
15
+ let streamConsumed;
16
+ let resolveStreamConsumed;
17
+ let resolveResultReady;
18
+ const resultReady = new Promise((r) => {
19
+ resolveResultReady = r;
20
+ });
21
+ const runCommand = async () => {
22
+ // Parse args and options
23
+ let args;
24
+ let parsedOptions;
25
+ if (parseMode === 'argv') {
26
+ // CLI mode: parse both args and options from argv tokens
27
+ const parsed = Parser.parse(argv, {
28
+ alias: command.alias,
29
+ args: command.args,
30
+ options: command.options,
31
+ });
32
+ args = parsed.args;
33
+ parsedOptions = parsed.options;
34
+ }
35
+ else if (parseMode === 'split') {
36
+ // HTTP mode: positional args from URL path segments, options from body/query
37
+ const parsed = Parser.parse(argv, { args: command.args });
38
+ args = parsed.args;
39
+ parsedOptions = command.options ? command.options.parse(inputOptions) : {};
40
+ }
41
+ else {
42
+ // MCP mode: all params come from inputOptions, split into args vs options
43
+ const split = splitParams(inputOptions, command);
44
+ args = command.args ? command.args.parse(split.args) : {};
45
+ parsedOptions = command.options ? command.options.parse(split.options) : {};
46
+ }
47
+ // Parse env
48
+ const commandEnv = command.env ? Parser.parseEnv(command.env, envSource) : {};
49
+ // Build sentinel helpers
50
+ const okFn = (data, meta = {}) => ({ [sentinel]: 'ok', data, cta: meta.cta });
51
+ const errorFn = (opts) => ({ [sentinel]: 'error', ...opts });
52
+ const raw = command.run({
53
+ agent,
54
+ args,
55
+ env: commandEnv,
56
+ error: errorFn,
57
+ format,
58
+ formatExplicit,
59
+ name,
60
+ ok: okFn,
61
+ options: parsedOptions,
62
+ var: varsMap,
63
+ version,
64
+ });
65
+ // Streaming: wrap the generator so middleware "after" runs after consumption.
66
+ // When middleware is active, runCommand suspends until the stream is fully consumed,
67
+ // keeping the middleware chain alive around the stream's lifetime.
68
+ if (isAsyncGenerator(raw)) {
69
+ if (middlewares.length > 0) {
70
+ streamConsumed = new Promise((r) => {
71
+ resolveStreamConsumed = r;
72
+ });
73
+ async function* wrapped() {
74
+ try {
75
+ yield* raw;
76
+ }
77
+ finally {
78
+ resolveStreamConsumed();
79
+ }
80
+ }
81
+ result = { stream: wrapped() };
82
+ resolveResultReady();
83
+ await streamConsumed;
84
+ }
85
+ else {
86
+ result = { stream: raw };
87
+ }
88
+ return;
89
+ }
90
+ const awaited = await raw;
91
+ if (isSentinel(awaited)) {
92
+ if (awaited[sentinel] === 'ok') {
93
+ const ok = awaited;
94
+ result = { ok: true, data: ok.data, ...(ok.cta ? { cta: ok.cta } : undefined) };
95
+ }
96
+ else {
97
+ const err = awaited;
98
+ result = {
99
+ ok: false,
100
+ error: {
101
+ code: err.code,
102
+ message: err.message,
103
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
104
+ },
105
+ ...(err.cta ? { cta: err.cta } : undefined),
106
+ ...(err.exitCode !== undefined ? { exitCode: err.exitCode } : undefined),
107
+ };
108
+ }
109
+ return;
110
+ }
111
+ result = { ok: true, data: awaited };
112
+ };
113
+ try {
114
+ // Parse CLI-level env
115
+ const cliEnv = envSchema ? Parser.parseEnv(envSchema, envSource) : {};
116
+ if (middlewares.length > 0) {
117
+ const errorFn = (opts) => {
118
+ // Side-effect: set result directly (handles both `return c.error()` and bare `c.error()`)
119
+ result = {
120
+ ok: false,
121
+ error: {
122
+ code: opts.code,
123
+ message: opts.message,
124
+ ...(opts.retryable !== undefined ? { retryable: opts.retryable } : undefined),
125
+ },
126
+ ...(opts.cta ? { cta: opts.cta } : undefined),
127
+ ...(opts.exitCode !== undefined ? { exitCode: opts.exitCode } : undefined),
128
+ };
129
+ return undefined;
130
+ };
131
+ const mwCtx = {
132
+ agent,
133
+ command: path,
134
+ env: cliEnv,
135
+ error: errorFn,
136
+ format: format,
137
+ formatExplicit,
138
+ name,
139
+ set(key, value) {
140
+ varsMap[key] = value;
141
+ },
142
+ var: varsMap,
143
+ version,
144
+ };
145
+ const composed = middlewares.reduceRight((next, mw) => async () => {
146
+ await mw(mwCtx, next);
147
+ }, runCommand);
148
+ // Start the chain and race against resultReady. For streams with middleware,
149
+ // runCommand suspends on streamConsumed (keeping middleware "after" deferred)
150
+ // but signals resultReady so we can return the stream immediately. The transport
151
+ // consumes the stream, which resolves streamConsumed, letting middleware "after" run.
152
+ const chainPromise = composed();
153
+ await Promise.race([chainPromise, resultReady]);
154
+ if (streamConsumed)
155
+ return result;
156
+ await chainPromise;
157
+ }
158
+ else {
159
+ await runCommand();
160
+ }
161
+ }
162
+ catch (error) {
163
+ if (error instanceof ValidationError)
164
+ return {
165
+ ok: false,
166
+ error: {
167
+ code: 'VALIDATION_ERROR',
168
+ message: error.message,
169
+ fieldErrors: error.fieldErrors,
170
+ },
171
+ };
172
+ return {
173
+ ok: false,
174
+ error: {
175
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
176
+ message: error instanceof Error ? error.message : String(error),
177
+ ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
178
+ },
179
+ ...(error instanceof IncurError && error.exitCode !== undefined
180
+ ? { exitCode: error.exitCode }
181
+ : undefined),
182
+ };
183
+ }
184
+ return result ?? { ok: true, data: undefined };
185
+ }
186
+ /** @internal Splits flat params into args vs options using schema shapes. */
187
+ function splitParams(params, command) {
188
+ const argKeys = new Set(command.args ? Object.keys(command.args.shape) : []);
189
+ const a = {};
190
+ const o = {};
191
+ for (const [key, value] of Object.entries(params))
192
+ if (argKeys.has(key))
193
+ a[key] = value;
194
+ else
195
+ o[key] = value;
196
+ return { args: a, options: o };
197
+ }
198
+ /** @internal Type guard for sentinel results. */
199
+ function isSentinel(value) {
200
+ return typeof value === 'object' && value !== null && sentinel in value;
201
+ }
202
+ /** @internal Type guard for async generators. */
203
+ function isAsyncGenerator(value) {
204
+ return (typeof value === 'object' &&
205
+ value !== null &&
206
+ Symbol.asyncIterator in value &&
207
+ typeof value.next === 'function');
208
+ }
209
+ /** @internal Creates a builtin subcommand with typesafe alias inference. */
210
+ function subcommand(def) {
211
+ return def;
212
+ }
213
+ /** Supported shell names for completions. */
214
+ export const shells = ['bash', 'fish', 'nushell', 'zsh'];
215
+ /** Built-in command metadata shared by help, completions, and handler logic. */
216
+ export const builtinCommands = [
217
+ {
218
+ name: 'completions',
219
+ description: 'Generate shell completion script',
220
+ args: z.object({
221
+ shell: z.enum(shells).describe('Shell to generate completions for'),
222
+ }),
223
+ hint(name) {
224
+ const rows = [
225
+ ['bash', `eval "$(${name} completions bash)"`, '# add to ~/.bashrc'],
226
+ ['fish', `${name} completions fish | source`, '# add to ~/.config/fish/config.fish'],
227
+ ['nushell', `see \`${name} completions nushell\``, '# add to config.nu'],
228
+ ['zsh', `eval "$(${name} completions zsh)"`, '# add to ~/.zshrc'],
229
+ ];
230
+ const shellW = Math.max(...rows.map((r) => r[0].length));
231
+ const cmdW = Math.max(...rows.map((r) => r[1].length));
232
+ return ('Setup:\n' +
233
+ rows
234
+ .map(([s, cmd, comment]) => ` ${s.padEnd(shellW)} ${cmd.padEnd(cmdW)} ${comment}`)
235
+ .join('\n'));
236
+ },
237
+ },
238
+ {
239
+ name: 'mcp',
240
+ description: 'Register as MCP server',
241
+ subcommands: [
242
+ subcommand({
243
+ name: 'add',
244
+ description: 'Register as MCP server',
245
+ alias: { command: 'c' },
246
+ options: z.object({
247
+ agent: z
248
+ .string()
249
+ .optional()
250
+ .describe('Target a specific agent (e.g. claude-code, cursor)'),
251
+ command: z
252
+ .string()
253
+ .optional()
254
+ .describe('Override the command agents will run (e.g. "pnpm my-cli --mcp")'),
255
+ noGlobal: z.boolean().optional().describe('Install to project instead of globally'),
256
+ }),
257
+ }),
258
+ ],
259
+ },
260
+ {
261
+ name: 'skills',
262
+ description: 'Sync skill files to agents',
263
+ subcommands: [
264
+ subcommand({
265
+ name: 'add',
266
+ description: 'Sync skill files to agents',
267
+ options: z.object({
268
+ depth: z.number().optional().describe('Grouping depth for skill files (default: 1)'),
269
+ noGlobal: z.boolean().optional().describe('Install to project instead of globally'),
270
+ }),
271
+ }),
272
+ ],
273
+ },
274
+ ];
275
+ //# sourceMappingURL=command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.js","sourceRoot":"","sources":["../../src/internal/command.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "[!start-pkg]": "",
17
17
  "name": "incur",
18
18
  "type": "module",
19
- "version": "0.3.3",
19
+ "version": "0.3.5",
20
20
  "license": "MIT",
21
21
  "repository": {
22
22
  "type": "git",
package/src/Cli.test.ts CHANGED
@@ -921,7 +921,7 @@ describe('subcommands', () => {
921
921
  --format <toon|json|yaml|md|jsonl> Output format
922
922
  --help Show help
923
923
  --llms, --llms-full Print LLM-readable manifest
924
- --schema Show JSON Schema for a command
924
+ --schema Show JSON Schema for command
925
925
  --token-count Print token count of output (instead of output)
926
926
  --token-limit <n> Limit output to n tokens
927
927
  --token-offset <n> Skip first n tokens of output
@@ -1350,10 +1350,10 @@ describe('help', () => {
1350
1350
  Commands:
1351
1351
  ping Health check
1352
1352
 
1353
- Built-in Commands:
1353
+ Integrations:
1354
1354
  completions Generate shell completion script
1355
- mcp add Register as an MCP server
1356
- skills add Sync skill files to your agent
1355
+ mcp add Register as MCP server
1356
+ skills add Sync skill files to agents
1357
1357
 
1358
1358
  Global Options:
1359
1359
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
@@ -1361,7 +1361,7 @@ describe('help', () => {
1361
1361
  --help Show help
1362
1362
  --llms, --llms-full Print LLM-readable manifest
1363
1363
  --mcp Start as MCP stdio server
1364
- --schema Show JSON Schema for a command
1364
+ --schema Show JSON Schema for command
1365
1365
  --token-count Print token count of output (instead of output)
1366
1366
  --token-limit <n> Limit output to n tokens
1367
1367
  --token-offset <n> Skip first n tokens of output
@@ -1388,10 +1388,10 @@ describe('help', () => {
1388
1388
  Commands:
1389
1389
  ping Health check
1390
1390
 
1391
- Built-in Commands:
1391
+ Integrations:
1392
1392
  completions Generate shell completion script
1393
- mcp add Register as an MCP server
1394
- skills add Sync skill files to your agent
1393
+ mcp add Register as MCP server
1394
+ skills add Sync skill files to agents
1395
1395
 
1396
1396
  Global Options:
1397
1397
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
@@ -1399,7 +1399,7 @@ describe('help', () => {
1399
1399
  --help Show help
1400
1400
  --llms, --llms-full Print LLM-readable manifest
1401
1401
  --mcp Start as MCP stdio server
1402
- --schema Show JSON Schema for a command
1402
+ --schema Show JSON Schema for command
1403
1403
  --token-count Print token count of output (instead of output)
1404
1404
  --token-limit <n> Limit output to n tokens
1405
1405
  --token-offset <n> Skip first n tokens of output
@@ -1432,7 +1432,7 @@ describe('help', () => {
1432
1432
  --format <toon|json|yaml|md|jsonl> Output format
1433
1433
  --help Show help
1434
1434
  --llms, --llms-full Print LLM-readable manifest
1435
- --schema Show JSON Schema for a command
1435
+ --schema Show JSON Schema for command
1436
1436
  --token-count Print token count of output (instead of output)
1437
1437
  --token-limit <n> Limit output to n tokens
1438
1438
  --token-offset <n> Skip first n tokens of output
@@ -1466,7 +1466,7 @@ describe('help', () => {
1466
1466
  --format <toon|json|yaml|md|jsonl> Output format
1467
1467
  --help Show help
1468
1468
  --llms, --llms-full Print LLM-readable manifest
1469
- --schema Show JSON Schema for a command
1469
+ --schema Show JSON Schema for command
1470
1470
  --token-count Print token count of output (instead of output)
1471
1471
  --token-limit <n> Limit output to n tokens
1472
1472
  --token-offset <n> Skip first n tokens of output
@@ -1551,10 +1551,10 @@ describe('help', () => {
1551
1551
  Commands:
1552
1552
  ping Ping
1553
1553
 
1554
- Built-in Commands:
1554
+ Integrations:
1555
1555
  completions Generate shell completion script
1556
- mcp add Register as an MCP server
1557
- skills add Sync skill files to your agent
1556
+ mcp add Register as MCP server
1557
+ skills add Sync skill files to agents
1558
1558
 
1559
1559
  Global Options:
1560
1560
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
@@ -1562,7 +1562,7 @@ describe('help', () => {
1562
1562
  --help Show help
1563
1563
  --llms, --llms-full Print LLM-readable manifest
1564
1564
  --mcp Start as MCP stdio server
1565
- --schema Show JSON Schema for a command
1565
+ --schema Show JSON Schema for command
1566
1566
  --token-count Print token count of output (instead of output)
1567
1567
  --token-limit <n> Limit output to n tokens
1568
1568
  --token-offset <n> Skip first n tokens of output
@@ -1593,7 +1593,7 @@ describe('help', () => {
1593
1593
  --format <toon|json|yaml|md|jsonl> Output format
1594
1594
  --help Show help
1595
1595
  --llms, --llms-full Print LLM-readable manifest
1596
- --schema Show JSON Schema for a command
1596
+ --schema Show JSON Schema for command
1597
1597
  --token-count Print token count of output (instead of output)
1598
1598
  --token-limit <n> Limit output to n tokens
1599
1599
  --token-offset <n> Skip first n tokens of output
@@ -1688,7 +1688,7 @@ describe('env', () => {
1688
1688
  --format <toon|json|yaml|md|jsonl> Output format
1689
1689
  --help Show help
1690
1690
  --llms, --llms-full Print LLM-readable manifest
1691
- --schema Show JSON Schema for a command
1691
+ --schema Show JSON Schema for command
1692
1692
  --token-count Print token count of output (instead of output)
1693
1693
  --token-limit <n> Limit output to n tokens
1694
1694
  --token-offset <n> Skip first n tokens of output
@@ -1726,7 +1726,7 @@ describe('env', () => {
1726
1726
  --format <toon|json|yaml|md|jsonl> Output format
1727
1727
  --help Show help
1728
1728
  --llms, --llms-full Print LLM-readable manifest
1729
- --schema Show JSON Schema for a command
1729
+ --schema Show JSON Schema for command
1730
1730
  --token-count Print token count of output (instead of output)
1731
1731
  --token-limit <n> Limit output to n tokens
1732
1732
  --token-offset <n> Skip first n tokens of output
@@ -1838,6 +1838,76 @@ describe('env', () => {
1838
1838
  })
1839
1839
  })
1840
1840
 
1841
+ describe('built-in commands', () => {
1842
+ test('bare completions shows help', async () => {
1843
+ const cli = Cli.create('test')
1844
+ cli.command('ping', { run: () => ({ pong: true }) })
1845
+ const { output } = await serve(cli, ['completions'])
1846
+ expect(output).toContain('Generate shell completion script')
1847
+ })
1848
+
1849
+ test('completions --help shows help', async () => {
1850
+ const cli = Cli.create('test')
1851
+ cli.command('ping', { run: () => ({ pong: true }) })
1852
+ const { output } = await serve(cli, ['completions', '--help'])
1853
+ expect(output).toContain('test completions')
1854
+ expect(output).toContain('Generate shell completion script')
1855
+ })
1856
+
1857
+ test('bare mcp shows help with subcommands', async () => {
1858
+ const cli = Cli.create('test')
1859
+ cli.command('ping', { run: () => ({ pong: true }) })
1860
+ const { output } = await serve(cli, ['mcp'])
1861
+ expect(output).toContain('test mcp')
1862
+ expect(output).toContain('Register as MCP server')
1863
+ expect(output).toContain('add')
1864
+ })
1865
+
1866
+ test('mcp --help shows help with subcommands', async () => {
1867
+ const cli = Cli.create('test')
1868
+ cli.command('ping', { run: () => ({ pong: true }) })
1869
+ const { output } = await serve(cli, ['mcp', '--help'])
1870
+ expect(output).toContain('test mcp')
1871
+ expect(output).toContain('add')
1872
+ })
1873
+
1874
+ test('mcp add --help shows options', async () => {
1875
+ const cli = Cli.create('test')
1876
+ cli.command('ping', { run: () => ({ pong: true }) })
1877
+ const { output } = await serve(cli, ['mcp', 'add', '--help'])
1878
+ expect(output).toContain('test mcp add')
1879
+ expect(output).toContain('--command')
1880
+ expect(output).toContain('--no-global')
1881
+ expect(output).toContain('--agent')
1882
+ })
1883
+
1884
+ test('bare skills shows help with subcommands', async () => {
1885
+ const cli = Cli.create('test')
1886
+ cli.command('ping', { run: () => ({ pong: true }) })
1887
+ const { output } = await serve(cli, ['skills'])
1888
+ expect(output).toContain('test skills')
1889
+ expect(output).toContain('Sync skill files to agents')
1890
+ expect(output).toContain('add')
1891
+ })
1892
+
1893
+ test('skills --help shows help with subcommands', async () => {
1894
+ const cli = Cli.create('test')
1895
+ cli.command('ping', { run: () => ({ pong: true }) })
1896
+ const { output } = await serve(cli, ['skills', '--help'])
1897
+ expect(output).toContain('test skills')
1898
+ expect(output).toContain('add')
1899
+ })
1900
+
1901
+ test('skills add --help shows options', async () => {
1902
+ const cli = Cli.create('test')
1903
+ cli.command('ping', { run: () => ({ pong: true }) })
1904
+ const { output } = await serve(cli, ['skills', 'add', '--help'])
1905
+ expect(output).toContain('test skills add')
1906
+ expect(output).toContain('--depth')
1907
+ expect(output).toContain('--no-global')
1908
+ })
1909
+ })
1910
+
1841
1911
  describe('skills staleness', () => {
1842
1912
  let stderrSpy: ReturnType<typeof vi.spyOn>
1843
1913
 
@@ -3375,6 +3445,83 @@ describe('fetch', () => {
3375
3445
  `)
3376
3446
  })
3377
3447
 
3448
+ test('group middleware runs for nested commands', async () => {
3449
+ const sub = Cli.create('admin', {
3450
+ vars: z.object({ role: z.string().default('none') }),
3451
+ })
3452
+ sub.use(async (c, next) => {
3453
+ c.set('role', 'admin')
3454
+ await next()
3455
+ })
3456
+ sub.command('status', {
3457
+ run: (c) => ({ role: c.var.role }),
3458
+ })
3459
+ const cli = Cli.create('test', {
3460
+ vars: z.object({ role: z.string().default('none') }),
3461
+ })
3462
+ cli.command(sub)
3463
+ expect(await fetchJson(cli, new Request('http://localhost/admin/status')))
3464
+ .toMatchInlineSnapshot(`
3465
+ {
3466
+ "body": {
3467
+ "data": {
3468
+ "role": "admin",
3469
+ },
3470
+ "meta": {
3471
+ "command": "admin status",
3472
+ "duration": "<stripped>",
3473
+ },
3474
+ "ok": true,
3475
+ },
3476
+ "status": 200,
3477
+ }
3478
+ `)
3479
+ })
3480
+
3481
+ test('cli-level env schema is parsed', async () => {
3482
+ const cli = Cli.create('test', {
3483
+ env: z.object({ APP_TOKEN: z.string().default('fallback') }),
3484
+ })
3485
+ cli.use(async (c, next) => {
3486
+ // env should be parsed from envSchema
3487
+ ;(globalThis as any).__testEnv = c.env
3488
+ await next()
3489
+ })
3490
+ cli.command('check', { run: () => ({ ok: true }) })
3491
+ await cli.fetch(new Request('http://localhost/check'))
3492
+ expect((globalThis as any).__testEnv).toEqual({ APP_TOKEN: 'fallback' })
3493
+ delete (globalThis as any).__testEnv
3494
+ })
3495
+
3496
+ test('retryable error is propagated', async () => {
3497
+ const cli = Cli.create('test')
3498
+ cli.command('rate-limit', {
3499
+ run: (c) => c.error({ code: 'RATE_LIMITED', message: 'slow down', retryable: true }),
3500
+ })
3501
+ const { body } = await fetchJson(cli, new Request('http://localhost/rate-limit'))
3502
+ expect(body.ok).toBe(false)
3503
+ expect(body.error.retryable).toBe(true)
3504
+ })
3505
+
3506
+ test('cta block is propagated', async () => {
3507
+ const cli = Cli.create('test')
3508
+ cli.command('done', {
3509
+ run: (c) => c.ok({ id: 1 }, { cta: { commands: ['list'], description: 'Next steps:' } }),
3510
+ })
3511
+ const { body } = await fetchJson(cli, new Request('http://localhost/done'))
3512
+ expect(body.ok).toBe(true)
3513
+ expect(body.meta.cta).toMatchInlineSnapshot(`
3514
+ {
3515
+ "commands": [
3516
+ {
3517
+ "command": "test list",
3518
+ },
3519
+ ],
3520
+ "description": "Next steps:",
3521
+ }
3522
+ `)
3523
+ })
3524
+
3378
3525
  describe('mcp over http', () => {
3379
3526
  function mcpCli() {
3380
3527
  const cli = Cli.create('test', { version: '1.0.0' })