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.
- package/README.md +11 -11
- package/dist/Cli.d.ts +2 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +234 -359
- 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 +20 -10
- 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 +165 -18
- package/src/Cli.ts +288 -439
- package/src/Completions.test.ts +35 -9
- package/src/Completions.ts +1 -2
- package/src/Help.test.ts +18 -7
- package/src/Help.ts +21 -10
- 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 +40 -27
- package/src/internal/command.ts +425 -0
package/src/e2e.test.ts
CHANGED
|
@@ -963,10 +963,10 @@ describe('help', () => {
|
|
|
963
963
|
stream-throw Stream that throws
|
|
964
964
|
validate-fail Fails validation
|
|
965
965
|
|
|
966
|
-
|
|
966
|
+
Integrations:
|
|
967
967
|
completions Generate shell completion script
|
|
968
|
-
mcp add Register as
|
|
969
|
-
skills add Sync skill files to
|
|
968
|
+
mcp add Register as MCP server
|
|
969
|
+
skills add Sync skill files to agents
|
|
970
970
|
|
|
971
971
|
Global Options:
|
|
972
972
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
@@ -974,7 +974,7 @@ describe('help', () => {
|
|
|
974
974
|
--help Show help
|
|
975
975
|
--llms, --llms-full Print LLM-readable manifest
|
|
976
976
|
--mcp Start as MCP stdio server
|
|
977
|
-
--schema Show JSON Schema for
|
|
977
|
+
--schema Show JSON Schema for command
|
|
978
978
|
--token-count Print token count of output (instead of output)
|
|
979
979
|
--token-limit <n> Limit output to n tokens
|
|
980
980
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -1007,7 +1007,7 @@ describe('help', () => {
|
|
|
1007
1007
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1008
1008
|
--help Show help
|
|
1009
1009
|
--llms, --llms-full Print LLM-readable manifest
|
|
1010
|
-
--schema Show JSON Schema for
|
|
1010
|
+
--schema Show JSON Schema for command
|
|
1011
1011
|
--token-count Print token count of output (instead of output)
|
|
1012
1012
|
--token-limit <n> Limit output to n tokens
|
|
1013
1013
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -1034,7 +1034,7 @@ describe('help', () => {
|
|
|
1034
1034
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1035
1035
|
--help Show help
|
|
1036
1036
|
--llms, --llms-full Print LLM-readable manifest
|
|
1037
|
-
--schema Show JSON Schema for
|
|
1037
|
+
--schema Show JSON Schema for command
|
|
1038
1038
|
--token-count Print token count of output (instead of output)
|
|
1039
1039
|
--token-limit <n> Limit output to n tokens
|
|
1040
1040
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -1060,7 +1060,7 @@ describe('help', () => {
|
|
|
1060
1060
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1061
1061
|
--help Show help
|
|
1062
1062
|
--llms, --llms-full Print LLM-readable manifest
|
|
1063
|
-
--schema Show JSON Schema for
|
|
1063
|
+
--schema Show JSON Schema for command
|
|
1064
1064
|
--token-count Print token count of output (instead of output)
|
|
1065
1065
|
--token-limit <n> Limit output to n tokens
|
|
1066
1066
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -1092,7 +1092,7 @@ describe('help', () => {
|
|
|
1092
1092
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1093
1093
|
--help Show help
|
|
1094
1094
|
--llms, --llms-full Print LLM-readable manifest
|
|
1095
|
-
--schema Show JSON Schema for
|
|
1095
|
+
--schema Show JSON Schema for command
|
|
1096
1096
|
--token-count Print token count of output (instead of output)
|
|
1097
1097
|
--token-limit <n> Limit output to n tokens
|
|
1098
1098
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -1738,10 +1738,10 @@ describe('root command with subcommands', () => {
|
|
|
1738
1738
|
info Show info
|
|
1739
1739
|
version Show version
|
|
1740
1740
|
|
|
1741
|
-
|
|
1741
|
+
Integrations:
|
|
1742
1742
|
completions Generate shell completion script
|
|
1743
|
-
mcp add Register as
|
|
1744
|
-
skills add Sync skill files to
|
|
1743
|
+
mcp add Register as MCP server
|
|
1744
|
+
skills add Sync skill files to agents
|
|
1745
1745
|
|
|
1746
1746
|
Global Options:
|
|
1747
1747
|
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
@@ -1749,7 +1749,7 @@ describe('root command with subcommands', () => {
|
|
|
1749
1749
|
--help Show help
|
|
1750
1750
|
--llms, --llms-full Print LLM-readable manifest
|
|
1751
1751
|
--mcp Start as MCP stdio server
|
|
1752
|
-
--schema Show JSON Schema for
|
|
1752
|
+
--schema Show JSON Schema for command
|
|
1753
1753
|
--token-count Print token count of output (instead of output)
|
|
1754
1754
|
--token-limit <n> Limit output to n tokens
|
|
1755
1755
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -1925,7 +1925,7 @@ describe('env', () => {
|
|
|
1925
1925
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1926
1926
|
--help Show help
|
|
1927
1927
|
--llms, --llms-full Print LLM-readable manifest
|
|
1928
|
-
--schema Show JSON Schema for
|
|
1928
|
+
--schema Show JSON Schema for command
|
|
1929
1929
|
--token-count Print token count of output (instead of output)
|
|
1930
1930
|
--token-limit <n> Limit output to n tokens
|
|
1931
1931
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -2441,6 +2441,18 @@ describe('fetch api', () => {
|
|
|
2441
2441
|
},
|
|
2442
2442
|
"meta": {
|
|
2443
2443
|
"command": "project create",
|
|
2444
|
+
"cta": {
|
|
2445
|
+
"commands": [
|
|
2446
|
+
{
|
|
2447
|
+
"command": "app project get p-new",
|
|
2448
|
+
"description": "View "MyProject"",
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
"command": "app project list",
|
|
2452
|
+
},
|
|
2453
|
+
],
|
|
2454
|
+
"description": "Suggested commands:",
|
|
2455
|
+
},
|
|
2444
2456
|
"duration": "<stripped>",
|
|
2445
2457
|
},
|
|
2446
2458
|
"ok": true,
|
|
@@ -2524,21 +2536,22 @@ describe('fetch api', () => {
|
|
|
2524
2536
|
const cli = createApp()
|
|
2525
2537
|
expect(await fetchJson(cli, new Request('http://localhost/explode-clac')))
|
|
2526
2538
|
.toMatchInlineSnapshot(`
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
"
|
|
2535
|
-
|
|
2539
|
+
{
|
|
2540
|
+
"body": {
|
|
2541
|
+
"error": {
|
|
2542
|
+
"code": "QUOTA_EXCEEDED",
|
|
2543
|
+
"message": "Rate limit exceeded",
|
|
2544
|
+
"retryable": true,
|
|
2545
|
+
},
|
|
2546
|
+
"meta": {
|
|
2547
|
+
"command": "explode-clac",
|
|
2548
|
+
"duration": "<stripped>",
|
|
2549
|
+
},
|
|
2550
|
+
"ok": false,
|
|
2536
2551
|
},
|
|
2537
|
-
"
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
}
|
|
2541
|
-
`)
|
|
2552
|
+
"status": 500,
|
|
2553
|
+
}
|
|
2554
|
+
`)
|
|
2542
2555
|
})
|
|
2543
2556
|
|
|
2544
2557
|
test('validation error → 400', async () => {
|
|
@@ -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
|
+
}[]
|