incur 0.3.4 → 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.
- package/README.md +1 -1
- package/dist/Cli.d.ts +2 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +232 -358
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +1 -2
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js.map +1 -1
- package/dist/Help.d.ts +2 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +19 -9
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +25 -5
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +61 -69
- package/dist/Mcp.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -1
- package/dist/Skill.js.map +1 -1
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +10 -1
- package/dist/SyncSkills.js.map +1 -1
- package/dist/internal/command.d.ts +116 -0
- package/dist/internal/command.d.ts.map +1 -0
- package/dist/internal/command.js +275 -0
- package/dist/internal/command.js.map +1 -0
- package/package.json +1 -1
- package/src/Cli.test.ts +150 -3
- package/src/Cli.ts +286 -438
- package/src/Completions.test.ts +35 -9
- package/src/Completions.ts +1 -2
- package/src/Help.test.ts +11 -0
- package/src/Help.ts +20 -9
- package/src/Mcp.test.ts +143 -0
- package/src/Mcp.ts +92 -84
- package/src/Skill.ts +5 -1
- package/src/SyncSkills.ts +11 -1
- package/src/e2e.test.ts +29 -16
- package/src/internal/command.ts +425 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { FieldError } from '../Errors.js'
|
|
4
|
+
import { IncurError, ValidationError } from '../Errors.js'
|
|
5
|
+
import type { Context as MiddlewareContext, Handler as MiddlewareHandler } from '../middleware.js'
|
|
6
|
+
import * as Parser from '../Parser.js'
|
|
7
|
+
|
|
8
|
+
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
9
|
+
const sentinel = Symbol.for('incur.sentinel')
|
|
10
|
+
|
|
11
|
+
/** @internal CTA block for command output. */
|
|
12
|
+
export type CtaBlock = {
|
|
13
|
+
commands: unknown[]
|
|
14
|
+
description?: string | undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @internal A tagged ok result. */
|
|
18
|
+
type OkResult = {
|
|
19
|
+
[sentinel]: 'ok'
|
|
20
|
+
data: unknown
|
|
21
|
+
cta?: CtaBlock | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @internal A tagged error result. */
|
|
25
|
+
type ErrorResult = {
|
|
26
|
+
[sentinel]: 'error'
|
|
27
|
+
code: string
|
|
28
|
+
message: string
|
|
29
|
+
retryable?: boolean | undefined
|
|
30
|
+
exitCode?: number | undefined
|
|
31
|
+
cta?: CtaBlock | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @internal Unified command execution used by CLI, HTTP, and MCP transports. */
|
|
35
|
+
export async function execute(command: any, options: execute.Options): Promise<execute.Result> {
|
|
36
|
+
const {
|
|
37
|
+
argv,
|
|
38
|
+
inputOptions,
|
|
39
|
+
agent,
|
|
40
|
+
format,
|
|
41
|
+
formatExplicit,
|
|
42
|
+
name,
|
|
43
|
+
path,
|
|
44
|
+
version,
|
|
45
|
+
envSource = process.env,
|
|
46
|
+
env: envSchema,
|
|
47
|
+
vars: varsSchema,
|
|
48
|
+
middlewares = [],
|
|
49
|
+
} = options
|
|
50
|
+
const parseMode = options.parseMode ?? 'argv'
|
|
51
|
+
|
|
52
|
+
const varsMap: Record<string, unknown> = varsSchema ? varsSchema.parse({}) : {}
|
|
53
|
+
let result: execute.Result | undefined
|
|
54
|
+
// For streaming with middleware: runCommand suspends on streamConsumed so middleware "after"
|
|
55
|
+
// runs after the stream is consumed. The wrapped generator resolves it in its finally block.
|
|
56
|
+
// resultReady signals that result has been set (for streams, before the chain finishes).
|
|
57
|
+
let streamConsumed: Promise<void> | undefined
|
|
58
|
+
let resolveStreamConsumed: (() => void) | undefined
|
|
59
|
+
let resolveResultReady: (() => void) | undefined
|
|
60
|
+
const resultReady = new Promise<void>((r) => {
|
|
61
|
+
resolveResultReady = r
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const runCommand = async () => {
|
|
65
|
+
// Parse args and options
|
|
66
|
+
let args: Record<string, unknown>
|
|
67
|
+
let parsedOptions: Record<string, unknown>
|
|
68
|
+
|
|
69
|
+
if (parseMode === 'argv') {
|
|
70
|
+
// CLI mode: parse both args and options from argv tokens
|
|
71
|
+
const parsed = Parser.parse(argv, {
|
|
72
|
+
alias: command.alias as Record<string, string> | undefined,
|
|
73
|
+
args: command.args,
|
|
74
|
+
options: command.options,
|
|
75
|
+
})
|
|
76
|
+
args = parsed.args
|
|
77
|
+
parsedOptions = parsed.options
|
|
78
|
+
} else if (parseMode === 'split') {
|
|
79
|
+
// HTTP mode: positional args from URL path segments, options from body/query
|
|
80
|
+
const parsed = Parser.parse(argv, { args: command.args })
|
|
81
|
+
args = parsed.args
|
|
82
|
+
parsedOptions = command.options ? command.options.parse(inputOptions) : {}
|
|
83
|
+
} else {
|
|
84
|
+
// MCP mode: all params come from inputOptions, split into args vs options
|
|
85
|
+
const split = splitParams(inputOptions, command)
|
|
86
|
+
args = command.args ? command.args.parse(split.args) : {}
|
|
87
|
+
parsedOptions = command.options ? command.options.parse(split.options) : {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse env
|
|
91
|
+
const commandEnv = command.env ? Parser.parseEnv(command.env, envSource) : {}
|
|
92
|
+
|
|
93
|
+
// Build sentinel helpers
|
|
94
|
+
const okFn = (data: unknown, meta: { cta?: CtaBlock | undefined } = {}): never =>
|
|
95
|
+
({ [sentinel]: 'ok', data, cta: meta.cta }) as never
|
|
96
|
+
const errorFn = (opts: {
|
|
97
|
+
code: string
|
|
98
|
+
cta?: CtaBlock | undefined
|
|
99
|
+
exitCode?: number | undefined
|
|
100
|
+
message: string
|
|
101
|
+
retryable?: boolean | undefined
|
|
102
|
+
}): never => ({ [sentinel]: 'error', ...opts }) as never
|
|
103
|
+
|
|
104
|
+
const raw = command.run({
|
|
105
|
+
agent,
|
|
106
|
+
args,
|
|
107
|
+
env: commandEnv,
|
|
108
|
+
error: errorFn,
|
|
109
|
+
format,
|
|
110
|
+
formatExplicit,
|
|
111
|
+
name,
|
|
112
|
+
ok: okFn,
|
|
113
|
+
options: parsedOptions,
|
|
114
|
+
var: varsMap,
|
|
115
|
+
version,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Streaming: wrap the generator so middleware "after" runs after consumption.
|
|
119
|
+
// When middleware is active, runCommand suspends until the stream is fully consumed,
|
|
120
|
+
// keeping the middleware chain alive around the stream's lifetime.
|
|
121
|
+
if (isAsyncGenerator(raw)) {
|
|
122
|
+
if (middlewares.length > 0) {
|
|
123
|
+
streamConsumed = new Promise<void>((r) => {
|
|
124
|
+
resolveStreamConsumed = r
|
|
125
|
+
})
|
|
126
|
+
async function* wrapped() {
|
|
127
|
+
try {
|
|
128
|
+
yield* raw as AsyncGenerator<unknown, unknown, unknown>
|
|
129
|
+
} finally {
|
|
130
|
+
resolveStreamConsumed!()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
result = { stream: wrapped() }
|
|
134
|
+
resolveResultReady!()
|
|
135
|
+
await streamConsumed
|
|
136
|
+
} else {
|
|
137
|
+
result = { stream: raw }
|
|
138
|
+
}
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const awaited = await raw
|
|
143
|
+
|
|
144
|
+
if (isSentinel(awaited)) {
|
|
145
|
+
if (awaited[sentinel] === 'ok') {
|
|
146
|
+
const ok = awaited as OkResult
|
|
147
|
+
result = { ok: true, data: ok.data, ...(ok.cta ? { cta: ok.cta } : undefined) }
|
|
148
|
+
} else {
|
|
149
|
+
const err = awaited as ErrorResult
|
|
150
|
+
result = {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: {
|
|
153
|
+
code: err.code,
|
|
154
|
+
message: err.message,
|
|
155
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
156
|
+
},
|
|
157
|
+
...(err.cta ? { cta: err.cta } : undefined),
|
|
158
|
+
...(err.exitCode !== undefined ? { exitCode: err.exitCode } : undefined),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
result = { ok: true, data: awaited }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Parse CLI-level env
|
|
169
|
+
const cliEnv = envSchema ? Parser.parseEnv(envSchema, envSource) : {}
|
|
170
|
+
|
|
171
|
+
if (middlewares.length > 0) {
|
|
172
|
+
const errorFn = (opts: {
|
|
173
|
+
code: string
|
|
174
|
+
cta?: CtaBlock | undefined
|
|
175
|
+
exitCode?: number | undefined
|
|
176
|
+
message: string
|
|
177
|
+
retryable?: boolean | undefined
|
|
178
|
+
}): never => {
|
|
179
|
+
// Side-effect: set result directly (handles both `return c.error()` and bare `c.error()`)
|
|
180
|
+
result = {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: {
|
|
183
|
+
code: opts.code,
|
|
184
|
+
message: opts.message,
|
|
185
|
+
...(opts.retryable !== undefined ? { retryable: opts.retryable } : undefined),
|
|
186
|
+
},
|
|
187
|
+
...(opts.cta ? { cta: opts.cta } : undefined),
|
|
188
|
+
...(opts.exitCode !== undefined ? { exitCode: opts.exitCode } : undefined),
|
|
189
|
+
}
|
|
190
|
+
return undefined as never
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const mwCtx: MiddlewareContext = {
|
|
194
|
+
agent,
|
|
195
|
+
command: path,
|
|
196
|
+
env: cliEnv,
|
|
197
|
+
error: errorFn,
|
|
198
|
+
format: format as any,
|
|
199
|
+
formatExplicit,
|
|
200
|
+
name,
|
|
201
|
+
set(key: string, value: unknown) {
|
|
202
|
+
varsMap[key] = value
|
|
203
|
+
},
|
|
204
|
+
var: varsMap,
|
|
205
|
+
version,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const composed = middlewares.reduceRight(
|
|
209
|
+
(next: () => Promise<void>, mw) => async () => {
|
|
210
|
+
await mw(mwCtx, next)
|
|
211
|
+
},
|
|
212
|
+
runCommand,
|
|
213
|
+
)
|
|
214
|
+
// Start the chain and race against resultReady. For streams with middleware,
|
|
215
|
+
// runCommand suspends on streamConsumed (keeping middleware "after" deferred)
|
|
216
|
+
// but signals resultReady so we can return the stream immediately. The transport
|
|
217
|
+
// consumes the stream, which resolves streamConsumed, letting middleware "after" run.
|
|
218
|
+
const chainPromise = composed()
|
|
219
|
+
await Promise.race([chainPromise, resultReady])
|
|
220
|
+
if (streamConsumed) return result!
|
|
221
|
+
await chainPromise
|
|
222
|
+
} else {
|
|
223
|
+
await runCommand()
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error instanceof ValidationError)
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: {
|
|
230
|
+
code: 'VALIDATION_ERROR',
|
|
231
|
+
message: error.message,
|
|
232
|
+
fieldErrors: error.fieldErrors,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: {
|
|
238
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
239
|
+
message: error instanceof Error ? error.message : String(error),
|
|
240
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
241
|
+
},
|
|
242
|
+
...(error instanceof IncurError && error.exitCode !== undefined
|
|
243
|
+
? { exitCode: error.exitCode }
|
|
244
|
+
: undefined),
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result ?? { ok: true, data: undefined }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @internal Splits flat params into args vs options using schema shapes. */
|
|
252
|
+
function splitParams(
|
|
253
|
+
params: Record<string, unknown>,
|
|
254
|
+
command: any,
|
|
255
|
+
): { args: Record<string, unknown>; options: Record<string, unknown> } {
|
|
256
|
+
const argKeys = new Set(command.args ? Object.keys(command.args.shape) : [])
|
|
257
|
+
const a: Record<string, unknown> = {}
|
|
258
|
+
const o: Record<string, unknown> = {}
|
|
259
|
+
for (const [key, value] of Object.entries(params))
|
|
260
|
+
if (argKeys.has(key)) a[key] = value
|
|
261
|
+
else o[key] = value
|
|
262
|
+
return { args: a, options: o }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export declare namespace execute {
|
|
266
|
+
/** Options for the unified execute function. */
|
|
267
|
+
type Options = {
|
|
268
|
+
/** Whether the consumer is an agent. */
|
|
269
|
+
agent: boolean
|
|
270
|
+
/** Raw positional tokens (already separated from flags). For HTTP/MCP, pass `[]`. */
|
|
271
|
+
argv: string[]
|
|
272
|
+
/** CLI-level env schema. */
|
|
273
|
+
env?: z.ZodObject<any> | undefined
|
|
274
|
+
/** Source for environment variables. Defaults to `process.env`. */
|
|
275
|
+
envSource?: Record<string, string | undefined> | undefined
|
|
276
|
+
/** The resolved output format. */
|
|
277
|
+
format: string
|
|
278
|
+
/** Whether the format was explicitly requested. */
|
|
279
|
+
formatExplicit: boolean
|
|
280
|
+
/** Raw parsed options (from query params, JSON body, or MCP params). For CLI, pass `{}`. */
|
|
281
|
+
inputOptions: Record<string, unknown>
|
|
282
|
+
/** Middleware handlers (root + group + command, already collected). */
|
|
283
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
284
|
+
/** The CLI name. */
|
|
285
|
+
name: string
|
|
286
|
+
/**
|
|
287
|
+
* How to parse input:
|
|
288
|
+
* - `'argv'` (default): parse both args and options from argv tokens (CLI mode)
|
|
289
|
+
* - `'split'`: args from argv, options from inputOptions (HTTP mode)
|
|
290
|
+
* - `'flat'`: all params from inputOptions, split by schema shapes (MCP mode)
|
|
291
|
+
*/
|
|
292
|
+
parseMode?: 'argv' | 'split' | 'flat' | undefined
|
|
293
|
+
/** The resolved command path. */
|
|
294
|
+
path: string
|
|
295
|
+
/** Vars schema for middleware variables. */
|
|
296
|
+
vars?: z.ZodObject<any> | undefined
|
|
297
|
+
/** CLI version string. */
|
|
298
|
+
version: string | undefined
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Result of executing a command. */
|
|
302
|
+
type Result =
|
|
303
|
+
| { ok: true; data: unknown; cta?: CtaBlock | undefined }
|
|
304
|
+
| {
|
|
305
|
+
ok: false
|
|
306
|
+
error: {
|
|
307
|
+
code: string
|
|
308
|
+
message: string
|
|
309
|
+
retryable?: boolean | undefined
|
|
310
|
+
fieldErrors?: FieldError[] | undefined
|
|
311
|
+
}
|
|
312
|
+
cta?: CtaBlock | undefined
|
|
313
|
+
exitCode?: number | undefined
|
|
314
|
+
}
|
|
315
|
+
| { stream: AsyncGenerator<unknown, unknown, unknown> }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @internal Type guard for sentinel results. */
|
|
319
|
+
function isSentinel(value: unknown): value is OkResult | ErrorResult {
|
|
320
|
+
return typeof value === 'object' && value !== null && sentinel in value
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @internal Type guard for async generators. */
|
|
324
|
+
function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
|
|
325
|
+
return (
|
|
326
|
+
typeof value === 'object' &&
|
|
327
|
+
value !== null &&
|
|
328
|
+
Symbol.asyncIterator in value &&
|
|
329
|
+
typeof (value as any).next === 'function'
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Common metadata shared by command definitions and built-in commands. */
|
|
334
|
+
export type CommandMeta<options extends z.ZodObject<any> | undefined = undefined> = {
|
|
335
|
+
/** Map of option names to single-char aliases. */
|
|
336
|
+
alias?: options extends z.ZodObject<any>
|
|
337
|
+
? Partial<Record<keyof z.output<options>, string>>
|
|
338
|
+
: Record<string, string> | undefined
|
|
339
|
+
/** A short description of what the command does. */
|
|
340
|
+
description?: string | undefined
|
|
341
|
+
/** Zod schema for named options/flags. */
|
|
342
|
+
options?: options | undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** @internal Creates a builtin subcommand with typesafe alias inference. */
|
|
346
|
+
function subcommand<const options extends z.ZodObject<any> | undefined = undefined>(
|
|
347
|
+
def: CommandMeta<options> & { name: string },
|
|
348
|
+
) {
|
|
349
|
+
return def
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Supported shell names for completions. */
|
|
353
|
+
export const shells = ['bash', 'fish', 'nushell', 'zsh'] as const
|
|
354
|
+
|
|
355
|
+
/** A supported shell name. */
|
|
356
|
+
export type Shell = (typeof shells)[number]
|
|
357
|
+
|
|
358
|
+
/** Built-in command metadata shared by help, completions, and handler logic. */
|
|
359
|
+
export const builtinCommands = [
|
|
360
|
+
{
|
|
361
|
+
name: 'completions',
|
|
362
|
+
description: 'Generate shell completion script',
|
|
363
|
+
args: z.object({
|
|
364
|
+
shell: z.enum(shells).describe('Shell to generate completions for'),
|
|
365
|
+
}),
|
|
366
|
+
hint(name) {
|
|
367
|
+
const rows = [
|
|
368
|
+
['bash', `eval "$(${name} completions bash)"`, '# add to ~/.bashrc'],
|
|
369
|
+
['fish', `${name} completions fish | source`, '# add to ~/.config/fish/config.fish'],
|
|
370
|
+
['nushell', `see \`${name} completions nushell\``, '# add to config.nu'],
|
|
371
|
+
['zsh', `eval "$(${name} completions zsh)"`, '# add to ~/.zshrc'],
|
|
372
|
+
] as const
|
|
373
|
+
const shellW = Math.max(...rows.map((r) => r[0].length))
|
|
374
|
+
const cmdW = Math.max(...rows.map((r) => r[1].length))
|
|
375
|
+
return (
|
|
376
|
+
'Setup:\n' +
|
|
377
|
+
rows
|
|
378
|
+
.map(([s, cmd, comment]) => ` ${s.padEnd(shellW)} ${cmd.padEnd(cmdW)} ${comment}`)
|
|
379
|
+
.join('\n')
|
|
380
|
+
)
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'mcp',
|
|
385
|
+
description: 'Register as MCP server',
|
|
386
|
+
subcommands: [
|
|
387
|
+
subcommand({
|
|
388
|
+
name: 'add',
|
|
389
|
+
description: 'Register as MCP server',
|
|
390
|
+
alias: { command: 'c' },
|
|
391
|
+
options: z.object({
|
|
392
|
+
agent: z
|
|
393
|
+
.string()
|
|
394
|
+
.optional()
|
|
395
|
+
.describe('Target a specific agent (e.g. claude-code, cursor)'),
|
|
396
|
+
command: z
|
|
397
|
+
.string()
|
|
398
|
+
.optional()
|
|
399
|
+
.describe('Override the command agents will run (e.g. "pnpm my-cli --mcp")'),
|
|
400
|
+
noGlobal: z.boolean().optional().describe('Install to project instead of globally'),
|
|
401
|
+
}),
|
|
402
|
+
}),
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'skills',
|
|
407
|
+
description: 'Sync skill files to agents',
|
|
408
|
+
subcommands: [
|
|
409
|
+
subcommand({
|
|
410
|
+
name: 'add',
|
|
411
|
+
description: 'Sync skill files to agents',
|
|
412
|
+
options: z.object({
|
|
413
|
+
depth: z.number().optional().describe('Grouping depth for skill files (default: 1)'),
|
|
414
|
+
noGlobal: z.boolean().optional().describe('Install to project instead of globally'),
|
|
415
|
+
}),
|
|
416
|
+
}),
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
] satisfies {
|
|
420
|
+
name: string
|
|
421
|
+
args?: z.ZodObject<any> | undefined
|
|
422
|
+
description: string
|
|
423
|
+
hint?: ((name: string) => string) | undefined
|
|
424
|
+
subcommands?: (CommandMeta<z.ZodObject<any>> & { name: string })[] | undefined
|
|
425
|
+
}[]
|