incur 0.1.17 → 0.2.1
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 +204 -9
- package/SKILL.md +173 -0
- package/dist/Cli.d.ts +39 -6
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +536 -43
- package/dist/Cli.js.map +1 -1
- package/dist/Errors.d.ts +4 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +3 -0
- package/dist/Errors.js.map +1 -1
- package/dist/Fetch.d.ts +26 -0
- package/dist/Fetch.d.ts.map +1 -0
- package/dist/Fetch.js +150 -0
- package/dist/Fetch.js.map +1 -0
- package/dist/Filter.d.ts +14 -0
- package/dist/Filter.d.ts.map +1 -0
- package/dist/Filter.js +134 -0
- package/dist/Filter.js.map +1 -0
- package/dist/Help.js +2 -0
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +26 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -2
- package/dist/Mcp.js.map +1 -1
- package/dist/Openapi.d.ts +20 -0
- package/dist/Openapi.d.ts.map +1 -0
- package/dist/Openapi.js +136 -0
- package/dist/Openapi.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +8 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +4 -1
- package/src/Cli.test-d.ts +27 -2
- package/src/Cli.test.ts +1007 -0
- package/src/Cli.ts +676 -47
- package/src/Errors.ts +5 -0
- package/src/Fetch.test.ts +274 -0
- package/src/Fetch.ts +170 -0
- package/src/Filter.test.ts +237 -0
- package/src/Filter.ts +139 -0
- package/src/Help.test.ts +14 -0
- package/src/Help.ts +2 -0
- package/src/Mcp.ts +3 -3
- package/src/Openapi.test.ts +320 -0
- package/src/Openapi.ts +196 -0
- package/src/e2e.test.ts +778 -0
- package/src/index.ts +3 -0
- package/src/middleware.ts +9 -2
package/src/Cli.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
import * as Completions from './Completions.js'
|
|
4
|
+
import * as Filter from './Filter.js'
|
|
4
5
|
import type { FieldError } from './Errors.js'
|
|
5
6
|
import { IncurError, ValidationError } from './Errors.js'
|
|
7
|
+
import * as Fetch from './Fetch.js'
|
|
8
|
+
import * as Openapi from './Openapi.js'
|
|
6
9
|
import * as Formatter from './Formatter.js'
|
|
7
10
|
import * as Help from './Help.js'
|
|
8
11
|
import { detectRunner } from './internal/pm.js'
|
|
@@ -56,6 +59,11 @@ export type Cli<
|
|
|
56
59
|
vars,
|
|
57
60
|
env
|
|
58
61
|
>
|
|
62
|
+
/** Mounts a fetch handler as a command, optionally with OpenAPI spec for typed subcommands. */
|
|
63
|
+
<const name extends string>(
|
|
64
|
+
name: name,
|
|
65
|
+
definition: { basePath?: string | undefined; description?: string | undefined; fetch: FetchHandler; openapi?: Openapi.OpenAPISpec | undefined; outputPolicy?: OutputPolicy | undefined },
|
|
66
|
+
): Cli<commands, vars, env>
|
|
59
67
|
}
|
|
60
68
|
/** A short description of the CLI. */
|
|
61
69
|
description?: string | undefined
|
|
@@ -63,6 +71,8 @@ export type Cli<
|
|
|
63
71
|
env: env
|
|
64
72
|
/** The name of the CLI application. */
|
|
65
73
|
name: string
|
|
74
|
+
/** Handles an incoming HTTP request, resolves the matching command, and returns a JSON Response. */
|
|
75
|
+
fetch(req: Request): Promise<Response>
|
|
66
76
|
/** Parses argv, runs the matched command, and writes the output envelope to stdout. */
|
|
67
77
|
serve(argv?: string[], options?: serve.Options): Promise<void>
|
|
68
78
|
/** Registers middleware that runs around every command. */
|
|
@@ -174,9 +184,12 @@ export function create(
|
|
|
174
184
|
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name
|
|
175
185
|
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition
|
|
176
186
|
const rootDef = 'run' in def ? (def as CommandDefinition<any, any, any>) : undefined
|
|
187
|
+
const rootFetch = 'fetch' in def ? (def.fetch as FetchHandler) : undefined
|
|
177
188
|
|
|
178
189
|
const commands = new Map<string, CommandEntry>()
|
|
179
190
|
const middlewares: MiddlewareHandler[] = []
|
|
191
|
+
const pending: Promise<void>[] = []
|
|
192
|
+
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0')
|
|
180
193
|
|
|
181
194
|
const cli: Cli = {
|
|
182
195
|
name,
|
|
@@ -186,6 +199,30 @@ export function create(
|
|
|
186
199
|
|
|
187
200
|
command(nameOrCli: any, def?: any): any {
|
|
188
201
|
if (typeof nameOrCli === 'string') {
|
|
202
|
+
if (def && 'fetch' in def && typeof def.fetch === 'function') {
|
|
203
|
+
// OpenAPI + fetch → generate typed command group (async, resolved before serve)
|
|
204
|
+
if (def.openapi) {
|
|
205
|
+
pending.push(
|
|
206
|
+
Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then((generated) => {
|
|
207
|
+
commands.set(nameOrCli, {
|
|
208
|
+
_group: true,
|
|
209
|
+
description: def.description,
|
|
210
|
+
commands: generated as Map<string, CommandEntry>,
|
|
211
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
212
|
+
} as InternalGroup)
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
return cli
|
|
216
|
+
}
|
|
217
|
+
commands.set(nameOrCli, {
|
|
218
|
+
_fetch: true,
|
|
219
|
+
basePath: def.basePath,
|
|
220
|
+
description: def.description,
|
|
221
|
+
fetch: def.fetch,
|
|
222
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
223
|
+
} as InternalFetchGateway)
|
|
224
|
+
return cli
|
|
225
|
+
}
|
|
189
226
|
commands.set(nameOrCli, def)
|
|
190
227
|
return cli
|
|
191
228
|
}
|
|
@@ -208,7 +245,18 @@ export function create(
|
|
|
208
245
|
return cli
|
|
209
246
|
},
|
|
210
247
|
|
|
248
|
+
async fetch(req: Request) {
|
|
249
|
+
if (pending.length > 0) await Promise.all(pending)
|
|
250
|
+
return fetchImpl(name, commands, req, {
|
|
251
|
+
mcpHandler,
|
|
252
|
+
middlewares,
|
|
253
|
+
rootCommand: rootDef,
|
|
254
|
+
vars: def.vars,
|
|
255
|
+
})
|
|
256
|
+
},
|
|
257
|
+
|
|
211
258
|
async serve(argv = process.argv.slice(2), serveOptions: serve.Options = {}) {
|
|
259
|
+
if (pending.length > 0) await Promise.all(pending)
|
|
212
260
|
return serveImpl(name, commands, argv, {
|
|
213
261
|
...serveOptions,
|
|
214
262
|
aliases: def.aliases,
|
|
@@ -219,6 +267,7 @@ export function create(
|
|
|
219
267
|
middlewares,
|
|
220
268
|
outputPolicy: def.outputPolicy,
|
|
221
269
|
rootCommand: rootDef,
|
|
270
|
+
rootFetch,
|
|
222
271
|
sync: def.sync,
|
|
223
272
|
vars: def.vars,
|
|
224
273
|
version: def.version,
|
|
@@ -261,6 +310,8 @@ export declare namespace create {
|
|
|
261
310
|
env?: env | undefined
|
|
262
311
|
/** Usage examples for this command. */
|
|
263
312
|
examples?: Example<args, options>[] | undefined
|
|
313
|
+
/** A fetch handler to use as the root command. All argv tokens are interpreted as path segments and curl-style flags. */
|
|
314
|
+
fetch?: FetchHandler | undefined
|
|
264
315
|
/** Default output format. Overridden by `--format` or `--json`. */
|
|
265
316
|
format?: Formatter.Format | undefined
|
|
266
317
|
/** Zod schema for named options/flags. */
|
|
@@ -287,17 +338,22 @@ export declare namespace create {
|
|
|
287
338
|
agent: boolean
|
|
288
339
|
/** Positional arguments. */
|
|
289
340
|
args: InferOutput<args>
|
|
290
|
-
/** The CLI name. */
|
|
291
|
-
name: string
|
|
292
341
|
/** Parsed environment variables. */
|
|
293
342
|
env: InferOutput<env>
|
|
294
343
|
/** Return an error result with optional CTAs. */
|
|
295
344
|
error: (options: {
|
|
296
345
|
code: string
|
|
297
346
|
cta?: CtaBlock | undefined
|
|
347
|
+
exitCode?: number | undefined
|
|
298
348
|
message: string
|
|
299
349
|
retryable?: boolean | undefined
|
|
300
350
|
}) => never
|
|
351
|
+
/** The resolved output format (e.g. `'toon'`, `'json'`, `'jsonl'`). */
|
|
352
|
+
format: Formatter.Format
|
|
353
|
+
/** Whether the user explicitly passed `--format` or `--json`. */
|
|
354
|
+
formatExplicit: boolean
|
|
355
|
+
/** The CLI name. */
|
|
356
|
+
name: string
|
|
301
357
|
/** Return a success result with optional metadata (e.g. CTAs). */
|
|
302
358
|
ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
|
|
303
359
|
options: InferOutput<options>
|
|
@@ -362,10 +418,12 @@ async function serveImpl(
|
|
|
362
418
|
verbose,
|
|
363
419
|
format: formatFlag,
|
|
364
420
|
formatExplicit,
|
|
421
|
+
filterOutput,
|
|
365
422
|
llms,
|
|
366
423
|
mcp: mcpFlag,
|
|
367
424
|
help,
|
|
368
425
|
version,
|
|
426
|
+
schema,
|
|
369
427
|
rest: filtered,
|
|
370
428
|
} = extractBuiltinFlags(argv)
|
|
371
429
|
|
|
@@ -402,7 +460,7 @@ async function serveImpl(
|
|
|
402
460
|
}
|
|
403
461
|
|
|
404
462
|
// Skills staleness check (skip for built-in commands)
|
|
405
|
-
if (!llms && !help && !version) {
|
|
463
|
+
if (!llms && !schema && !help && !version) {
|
|
406
464
|
const isSkillsAdd =
|
|
407
465
|
filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills')
|
|
408
466
|
const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp')
|
|
@@ -689,8 +747,8 @@ async function serveImpl(
|
|
|
689
747
|
)
|
|
690
748
|
return
|
|
691
749
|
}
|
|
692
|
-
if (options.rootCommand) {
|
|
693
|
-
// Root command with no args — treat as root invocation
|
|
750
|
+
if (options.rootCommand || options.rootFetch) {
|
|
751
|
+
// Root command/fetch with no args — treat as root invocation
|
|
694
752
|
} else {
|
|
695
753
|
writeln(
|
|
696
754
|
Help.formatRoot(name, {
|
|
@@ -708,7 +766,16 @@ async function serveImpl(
|
|
|
708
766
|
const resolved =
|
|
709
767
|
filtered.length === 0 && options.rootCommand
|
|
710
768
|
? { command: options.rootCommand, path: name, rest: [] as string[] }
|
|
711
|
-
:
|
|
769
|
+
: filtered.length === 0 && options.rootFetch
|
|
770
|
+
? { fetchGateway: { _fetch: true as const, fetch: options.rootFetch, description: options.description }, middlewares: [] as MiddlewareHandler[], path: name, rest: [] as string[] }
|
|
771
|
+
: resolveCommand(commands, filtered)
|
|
772
|
+
|
|
773
|
+
// --help on a fetch gateway → show fetch-specific help
|
|
774
|
+
if (help && 'fetchGateway' in resolved) {
|
|
775
|
+
const commandName = resolved.path === name ? name : `${name} ${resolved.path}`
|
|
776
|
+
writeln(formatFetchHelp(commandName, resolved.fetchGateway.description))
|
|
777
|
+
return
|
|
778
|
+
}
|
|
712
779
|
|
|
713
780
|
// --help after a command → show help for that command
|
|
714
781
|
if (help) {
|
|
@@ -749,7 +816,8 @@ async function serveImpl(
|
|
|
749
816
|
}),
|
|
750
817
|
)
|
|
751
818
|
}
|
|
752
|
-
} else {
|
|
819
|
+
} else if ('command' in resolved) {
|
|
820
|
+
const cmd = resolved.command
|
|
753
821
|
const isRootCmd = resolved.path === name
|
|
754
822
|
const commandName = isRootCmd ? name : `${name} ${resolved.path}`
|
|
755
823
|
const helpSubcommands =
|
|
@@ -758,17 +826,17 @@ async function serveImpl(
|
|
|
758
826
|
: undefined
|
|
759
827
|
writeln(
|
|
760
828
|
Help.formatCommand(commandName, {
|
|
761
|
-
alias:
|
|
829
|
+
alias: cmd.alias as Record<string, string> | undefined,
|
|
762
830
|
aliases: isRootCmd ? options.aliases : undefined,
|
|
763
|
-
description:
|
|
831
|
+
description: cmd.description,
|
|
764
832
|
version: isRootCmd ? options.version : undefined,
|
|
765
|
-
args:
|
|
766
|
-
env:
|
|
833
|
+
args: cmd.args,
|
|
834
|
+
env: cmd.env,
|
|
767
835
|
envSource: options.env,
|
|
768
|
-
hint:
|
|
769
|
-
options:
|
|
770
|
-
examples: formatExamples(
|
|
771
|
-
usage:
|
|
836
|
+
hint: cmd.hint,
|
|
837
|
+
options: cmd.options,
|
|
838
|
+
examples: formatExamples(cmd.examples),
|
|
839
|
+
usage: cmd.usage,
|
|
772
840
|
commands: helpSubcommands,
|
|
773
841
|
root: isRootCmd,
|
|
774
842
|
}),
|
|
@@ -777,6 +845,39 @@ async function serveImpl(
|
|
|
777
845
|
return
|
|
778
846
|
}
|
|
779
847
|
|
|
848
|
+
// --schema: output JSON Schema for a command's args, env, options, output
|
|
849
|
+
if (schema) {
|
|
850
|
+
if ('help' in resolved) {
|
|
851
|
+
writeln(
|
|
852
|
+
Help.formatRoot(`${name} ${resolved.path}`, {
|
|
853
|
+
description: resolved.description,
|
|
854
|
+
commands: collectHelpCommands(resolved.commands),
|
|
855
|
+
}),
|
|
856
|
+
)
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
if ('error' in resolved) {
|
|
860
|
+
const parent = resolved.path ? `${name} ${resolved.path}` : name
|
|
861
|
+
writeln(`Error: '${resolved.error}' is not a command for '${parent}'.`)
|
|
862
|
+
exit(1)
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
if ('fetchGateway' in resolved) {
|
|
866
|
+
writeln('--schema is not supported for fetch commands.')
|
|
867
|
+
exit(1)
|
|
868
|
+
return
|
|
869
|
+
}
|
|
870
|
+
const cmd = resolved.command
|
|
871
|
+
const format = formatExplicit ? formatFlag : 'toon'
|
|
872
|
+
const result: Record<string, unknown> = {}
|
|
873
|
+
if (cmd.args) result.args = Schema.toJsonSchema(cmd.args)
|
|
874
|
+
if (cmd.env) result.env = Schema.toJsonSchema(cmd.env)
|
|
875
|
+
if (cmd.options) result.options = Schema.toJsonSchema(cmd.options)
|
|
876
|
+
if (cmd.output) result.output = Schema.toJsonSchema(cmd.output)
|
|
877
|
+
writeln(Formatter.format(result, format))
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
|
|
780
881
|
if ('help' in resolved) {
|
|
781
882
|
writeln(
|
|
782
883
|
Help.formatRoot(`${name} ${resolved.path}`, {
|
|
@@ -790,21 +891,27 @@ async function serveImpl(
|
|
|
790
891
|
const start = performance.now()
|
|
791
892
|
|
|
792
893
|
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
793
|
-
const resolvedFormat = 'command' in resolved && resolved.command.format
|
|
894
|
+
const resolvedFormat = 'command' in resolved && (resolved as any).command.format
|
|
794
895
|
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
|
|
795
896
|
|
|
796
|
-
// Fall back to root
|
|
897
|
+
// Fall back to root fetch when no subcommand matches
|
|
797
898
|
const effective =
|
|
798
|
-
'error' in resolved && options.
|
|
799
|
-
? {
|
|
800
|
-
: resolved
|
|
899
|
+
'error' in resolved && options.rootFetch && !resolved.path
|
|
900
|
+
? { fetchGateway: { _fetch: true as const, fetch: options.rootFetch, description: options.description }, middlewares: [] as MiddlewareHandler[], path: name, rest: filtered }
|
|
901
|
+
: 'error' in resolved && options.rootCommand && !resolved.path
|
|
902
|
+
? { command: options.rootCommand, path: name, rest: filtered }
|
|
903
|
+
: resolved
|
|
801
904
|
|
|
802
905
|
// Resolve outputPolicy: command/group → CLI-level → default ('all')
|
|
803
906
|
const effectiveOutputPolicy =
|
|
804
907
|
('outputPolicy' in resolved && resolved.outputPolicy) || options.outputPolicy
|
|
805
908
|
const renderOutput = !(human && !formatExplicit && effectiveOutputPolicy === 'agent-only')
|
|
806
909
|
|
|
910
|
+
const filterPaths = filterOutput ? Filter.parse(filterOutput) : undefined
|
|
911
|
+
|
|
807
912
|
function write(output: Output) {
|
|
913
|
+
if (filterPaths && output.ok && output.data != null)
|
|
914
|
+
output = { ...output, data: Filter.apply(output.data, filterPaths) }
|
|
808
915
|
const cta = output.meta.cta
|
|
809
916
|
if (human && !verbose) {
|
|
810
917
|
if (output.ok && output.data != null && renderOutput)
|
|
@@ -852,6 +959,119 @@ async function serveImpl(
|
|
|
852
959
|
return
|
|
853
960
|
}
|
|
854
961
|
|
|
962
|
+
// Fetch gateway execution path
|
|
963
|
+
if ('fetchGateway' in effective) {
|
|
964
|
+
const { fetchGateway, path, rest: fetchRest } = effective
|
|
965
|
+
const fetchMiddleware = [
|
|
966
|
+
...(options.middlewares ?? []),
|
|
967
|
+
...((effective as any).middlewares ?? []),
|
|
968
|
+
]
|
|
969
|
+
|
|
970
|
+
const runFetch = async () => {
|
|
971
|
+
const input = Fetch.parseArgv(fetchRest)
|
|
972
|
+
if (fetchGateway.basePath) input.path = fetchGateway.basePath + input.path
|
|
973
|
+
const request = Fetch.buildRequest(input)
|
|
974
|
+
const response = await fetchGateway.fetch(request)
|
|
975
|
+
|
|
976
|
+
// Streaming path — NDJSON responses pipe through handleStreaming
|
|
977
|
+
if (Fetch.isStreamingResponse(response)) {
|
|
978
|
+
const generator = Fetch.parseStreamingResponse(response)
|
|
979
|
+
await handleStreaming(generator, {
|
|
980
|
+
name,
|
|
981
|
+
path,
|
|
982
|
+
start,
|
|
983
|
+
format,
|
|
984
|
+
formatExplicit,
|
|
985
|
+
human,
|
|
986
|
+
renderOutput,
|
|
987
|
+
verbose,
|
|
988
|
+
write,
|
|
989
|
+
writeln,
|
|
990
|
+
exit,
|
|
991
|
+
})
|
|
992
|
+
return
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const output = await Fetch.parseResponse(response)
|
|
996
|
+
|
|
997
|
+
if (output.ok) {
|
|
998
|
+
write({
|
|
999
|
+
ok: true,
|
|
1000
|
+
data: output.data,
|
|
1001
|
+
meta: {
|
|
1002
|
+
command: path,
|
|
1003
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
1004
|
+
},
|
|
1005
|
+
})
|
|
1006
|
+
} else {
|
|
1007
|
+
write({
|
|
1008
|
+
ok: false,
|
|
1009
|
+
error: {
|
|
1010
|
+
code: `HTTP_${output.status}`,
|
|
1011
|
+
message: typeof output.data === 'object' && output.data !== null && 'message' in output.data
|
|
1012
|
+
? String((output.data as any).message)
|
|
1013
|
+
: typeof output.data === 'string' ? output.data : `HTTP ${output.status}`,
|
|
1014
|
+
},
|
|
1015
|
+
meta: {
|
|
1016
|
+
command: path,
|
|
1017
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
1018
|
+
},
|
|
1019
|
+
})
|
|
1020
|
+
exit(1)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, options.env ?? process.env) : {}
|
|
1026
|
+
if (fetchMiddleware.length > 0) {
|
|
1027
|
+
const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
|
|
1028
|
+
const errorFn = (opts: { code: string; exitCode?: number | undefined; message: string; retryable?: boolean | undefined; cta?: CtaBlock | undefined }): never =>
|
|
1029
|
+
({ [sentinel]: 'error', ...opts }) as never
|
|
1030
|
+
const mwCtx: MiddlewareContext = {
|
|
1031
|
+
agent: !human,
|
|
1032
|
+
command: path,
|
|
1033
|
+
env: cliEnv,
|
|
1034
|
+
error: errorFn,
|
|
1035
|
+
format,
|
|
1036
|
+
formatExplicit,
|
|
1037
|
+
name,
|
|
1038
|
+
set(key: string, value: unknown) { varsMap[key] = value },
|
|
1039
|
+
var: varsMap,
|
|
1040
|
+
version: options.version,
|
|
1041
|
+
}
|
|
1042
|
+
const handleMwSentinel = (result: unknown) => {
|
|
1043
|
+
if (!isSentinel(result) || result[sentinel] !== 'error') return
|
|
1044
|
+
const err = result as ErrorResult
|
|
1045
|
+
const cta = formatCtaBlock(name, err.cta)
|
|
1046
|
+
write({
|
|
1047
|
+
ok: false,
|
|
1048
|
+
error: { code: err.code, message: err.message, ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined) },
|
|
1049
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms`, ...(cta ? { cta } : undefined) },
|
|
1050
|
+
})
|
|
1051
|
+
exit(err.exitCode ?? 1)
|
|
1052
|
+
}
|
|
1053
|
+
const composed = fetchMiddleware.reduceRight(
|
|
1054
|
+
(next: () => Promise<void>, mw) => async () => { handleMwSentinel(await mw(mwCtx, next)) },
|
|
1055
|
+
runFetch,
|
|
1056
|
+
)
|
|
1057
|
+
await composed()
|
|
1058
|
+
} else {
|
|
1059
|
+
await runFetch()
|
|
1060
|
+
}
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
write({
|
|
1063
|
+
ok: false,
|
|
1064
|
+
error: {
|
|
1065
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1066
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1067
|
+
},
|
|
1068
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1069
|
+
})
|
|
1070
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1)
|
|
1071
|
+
}
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
|
|
855
1075
|
const { command, path, rest } = effective
|
|
856
1076
|
|
|
857
1077
|
// Collect middleware: root CLI + groups traversed + per-command
|
|
@@ -888,6 +1108,7 @@ async function serveImpl(
|
|
|
888
1108
|
}
|
|
889
1109
|
const errorFn = (opts: {
|
|
890
1110
|
code: string
|
|
1111
|
+
exitCode?: number | undefined
|
|
891
1112
|
message: string
|
|
892
1113
|
retryable?: boolean | undefined
|
|
893
1114
|
cta?: CtaBlock | undefined
|
|
@@ -899,10 +1120,12 @@ async function serveImpl(
|
|
|
899
1120
|
agent: !human,
|
|
900
1121
|
args,
|
|
901
1122
|
env,
|
|
1123
|
+
error: errorFn,
|
|
1124
|
+
format,
|
|
1125
|
+
formatExplicit,
|
|
902
1126
|
name,
|
|
903
|
-
options: parsedOptions,
|
|
904
1127
|
ok: okFn,
|
|
905
|
-
|
|
1128
|
+
options: parsedOptions,
|
|
906
1129
|
var: varsMap,
|
|
907
1130
|
version: options.version,
|
|
908
1131
|
})
|
|
@@ -940,12 +1163,13 @@ async function serveImpl(
|
|
|
940
1163
|
},
|
|
941
1164
|
})
|
|
942
1165
|
} else {
|
|
1166
|
+
const err = awaited as ErrorResult
|
|
943
1167
|
write({
|
|
944
1168
|
ok: false,
|
|
945
1169
|
error: {
|
|
946
|
-
code:
|
|
947
|
-
message:
|
|
948
|
-
...(
|
|
1170
|
+
code: err.code,
|
|
1171
|
+
message: err.message,
|
|
1172
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
949
1173
|
},
|
|
950
1174
|
meta: {
|
|
951
1175
|
command: path,
|
|
@@ -953,7 +1177,7 @@ async function serveImpl(
|
|
|
953
1177
|
...(cta ? { cta } : undefined),
|
|
954
1178
|
},
|
|
955
1179
|
})
|
|
956
|
-
exit(1)
|
|
1180
|
+
exit(err.exitCode ?? 1)
|
|
957
1181
|
}
|
|
958
1182
|
} else {
|
|
959
1183
|
write({
|
|
@@ -973,6 +1197,7 @@ async function serveImpl(
|
|
|
973
1197
|
if (allMiddleware.length > 0) {
|
|
974
1198
|
const errorFn = (opts: {
|
|
975
1199
|
code: string
|
|
1200
|
+
exitCode?: number | undefined
|
|
976
1201
|
message: string
|
|
977
1202
|
retryable?: boolean | undefined
|
|
978
1203
|
cta?: CtaBlock | undefined
|
|
@@ -984,6 +1209,8 @@ async function serveImpl(
|
|
|
984
1209
|
command: path,
|
|
985
1210
|
env: cliEnv,
|
|
986
1211
|
error: errorFn,
|
|
1212
|
+
format,
|
|
1213
|
+
formatExplicit,
|
|
987
1214
|
name,
|
|
988
1215
|
set(key: string, value: unknown) {
|
|
989
1216
|
varsMap[key] = value
|
|
@@ -993,13 +1220,14 @@ async function serveImpl(
|
|
|
993
1220
|
}
|
|
994
1221
|
const handleMwSentinel = (result: unknown) => {
|
|
995
1222
|
if (!isSentinel(result) || result[sentinel] !== 'error') return
|
|
996
|
-
const
|
|
1223
|
+
const err = result as ErrorResult
|
|
1224
|
+
const cta = formatCtaBlock(name, err.cta)
|
|
997
1225
|
write({
|
|
998
1226
|
ok: false,
|
|
999
1227
|
error: {
|
|
1000
|
-
code:
|
|
1001
|
-
message:
|
|
1002
|
-
...(
|
|
1228
|
+
code: err.code,
|
|
1229
|
+
message: err.message,
|
|
1230
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
1003
1231
|
},
|
|
1004
1232
|
meta: {
|
|
1005
1233
|
command: path,
|
|
@@ -1007,7 +1235,7 @@ async function serveImpl(
|
|
|
1007
1235
|
...(cta ? { cta } : undefined),
|
|
1008
1236
|
},
|
|
1009
1237
|
})
|
|
1010
|
-
exit(1)
|
|
1238
|
+
exit(err.exitCode ?? 1)
|
|
1011
1239
|
}
|
|
1012
1240
|
const composed = allMiddleware.reduceRight(
|
|
1013
1241
|
(next: () => Promise<void>, mw) => async () => {
|
|
@@ -1046,8 +1274,321 @@ async function serveImpl(
|
|
|
1046
1274
|
}
|
|
1047
1275
|
|
|
1048
1276
|
write(errorOutput)
|
|
1049
|
-
exit(1)
|
|
1277
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1)
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** @internal Options for fetchImpl. */
|
|
1282
|
+
declare namespace fetchImpl {
|
|
1283
|
+
type Options = {
|
|
1284
|
+
mcpHandler?: ((req: Request, commands: Map<string, CommandEntry>) => Promise<Response>) | undefined
|
|
1285
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
1286
|
+
rootCommand?: CommandDefinition<any, any, any> | undefined
|
|
1287
|
+
vars?: z.ZodObject<any> | undefined
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
|
|
1292
|
+
function createMcpHttpHandler(name: string, version: string) {
|
|
1293
|
+
let transport: any
|
|
1294
|
+
|
|
1295
|
+
return async (req: Request, commands: Map<string, CommandEntry>): Promise<Response> => {
|
|
1296
|
+
if (!transport) {
|
|
1297
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
|
|
1298
|
+
const { WebStandardStreamableHTTPServerTransport } = await import(
|
|
1299
|
+
'@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
const server = new McpServer({ name, version })
|
|
1303
|
+
|
|
1304
|
+
for (const tool of Mcp.collectTools(commands, [])) {
|
|
1305
|
+
const mergedShape: Record<string, any> = {
|
|
1306
|
+
...tool.command.args?.shape,
|
|
1307
|
+
...tool.command.options?.shape,
|
|
1308
|
+
}
|
|
1309
|
+
const hasInput = Object.keys(mergedShape).length > 0
|
|
1310
|
+
|
|
1311
|
+
server.registerTool(
|
|
1312
|
+
tool.name,
|
|
1313
|
+
{
|
|
1314
|
+
...(tool.description ? { description: tool.description } : undefined),
|
|
1315
|
+
...(hasInput ? { inputSchema: mergedShape } : undefined),
|
|
1316
|
+
},
|
|
1317
|
+
async (...callArgs: any[]) => {
|
|
1318
|
+
const params = hasInput ? (callArgs[0] as Record<string, unknown>) : {}
|
|
1319
|
+
return Mcp.callTool(tool, params)
|
|
1320
|
+
},
|
|
1321
|
+
)
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
transport = new WebStandardStreamableHTTPServerTransport({
|
|
1325
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
1326
|
+
enableJsonResponse: true,
|
|
1327
|
+
})
|
|
1328
|
+
await server.connect(transport)
|
|
1329
|
+
}
|
|
1330
|
+
return transport.handleRequest(req)
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** @internal Handles an HTTP request by resolving a command and returning a JSON Response. */
|
|
1335
|
+
async function fetchImpl(
|
|
1336
|
+
name: string,
|
|
1337
|
+
commands: Map<string, CommandEntry>,
|
|
1338
|
+
req: Request,
|
|
1339
|
+
options: fetchImpl.Options = {},
|
|
1340
|
+
): Promise<Response> {
|
|
1341
|
+
const start = performance.now()
|
|
1342
|
+
|
|
1343
|
+
const url = new URL(req.url)
|
|
1344
|
+
const segments = url.pathname.split('/').filter(Boolean)
|
|
1345
|
+
|
|
1346
|
+
// MCP over HTTP: route /mcp to the MCP transport
|
|
1347
|
+
if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
|
|
1348
|
+
return options.mcpHandler(req, commands)
|
|
1349
|
+
|
|
1350
|
+
// .well-known/skills/ — Agent Skills Discovery (RFC)
|
|
1351
|
+
if (
|
|
1352
|
+
segments[0] === '.well-known' &&
|
|
1353
|
+
segments[1] === 'skills' &&
|
|
1354
|
+
segments.length >= 3 &&
|
|
1355
|
+
req.method === 'GET'
|
|
1356
|
+
) {
|
|
1357
|
+
const groups = new Map<string, string>()
|
|
1358
|
+
const cmds = collectSkillCommands(commands, [], groups)
|
|
1359
|
+
|
|
1360
|
+
// GET /.well-known/skills/index.json
|
|
1361
|
+
if (segments[2] === 'index.json' && segments.length === 3) {
|
|
1362
|
+
const files = Skill.split(name, cmds, 1, groups)
|
|
1363
|
+
const skills = files.map((f) => {
|
|
1364
|
+
const descMatch = f.content.match(/^description:\s*(.+)$/m)
|
|
1365
|
+
return {
|
|
1366
|
+
name: f.dir || name,
|
|
1367
|
+
description: descMatch?.[1] ?? '',
|
|
1368
|
+
files: ['SKILL.md'],
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
return new Response(JSON.stringify({ skills }), {
|
|
1372
|
+
status: 200,
|
|
1373
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=300' },
|
|
1374
|
+
})
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// GET /.well-known/skills/{skill-name}/SKILL.md
|
|
1378
|
+
if (segments.length === 4 && segments[3] === 'SKILL.md') {
|
|
1379
|
+
const skillName = segments[2]!
|
|
1380
|
+
const files = Skill.split(name, cmds, 1, groups)
|
|
1381
|
+
const file = files.find((f) => (f.dir || name) === skillName)
|
|
1382
|
+
if (file)
|
|
1383
|
+
return new Response(file.content, {
|
|
1384
|
+
status: 200,
|
|
1385
|
+
headers: { 'content-type': 'text/markdown', 'cache-control': 'public, max-age=300' },
|
|
1386
|
+
})
|
|
1387
|
+
return new Response('Not Found', { status: 404 })
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
return new Response('Not Found', { status: 404 })
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Parse options from search params (GET) or body (non-GET)
|
|
1394
|
+
let inputOptions: Record<string, unknown> = {}
|
|
1395
|
+
if (req.method === 'GET')
|
|
1396
|
+
for (const [key, value] of url.searchParams) inputOptions[key] = value
|
|
1397
|
+
else {
|
|
1398
|
+
try {
|
|
1399
|
+
const contentType = req.headers.get('content-type') ?? ''
|
|
1400
|
+
if (contentType.includes('application/json')) inputOptions = (await req.json()) as Record<string, unknown>
|
|
1401
|
+
} catch {}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function jsonResponse(body: unknown, status: number) {
|
|
1405
|
+
return new Response(JSON.stringify(body), {
|
|
1406
|
+
status,
|
|
1407
|
+
headers: { 'content-type': 'application/json' },
|
|
1408
|
+
})
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Resolve command from path segments
|
|
1412
|
+
if (segments.length === 0) {
|
|
1413
|
+
// Root path
|
|
1414
|
+
if (options.rootCommand)
|
|
1415
|
+
return executeCommand(name, options.rootCommand, [], inputOptions, start, options)
|
|
1416
|
+
return jsonResponse(
|
|
1417
|
+
{ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: 'No root command defined.' }, meta: { command: '/', duration: `${Math.round(performance.now() - start)}ms` } },
|
|
1418
|
+
404,
|
|
1419
|
+
)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const resolved = resolveCommand(commands, segments)
|
|
1423
|
+
|
|
1424
|
+
if ('error' in resolved)
|
|
1425
|
+
return jsonResponse(
|
|
1426
|
+
{ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: `'${resolved.error}' is not a command for '${resolved.path ? `${name} ${resolved.path}` : name}'.` }, meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` } },
|
|
1427
|
+
404,
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
if ('help' in resolved)
|
|
1431
|
+
return jsonResponse(
|
|
1432
|
+
{ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: `'${resolved.path}' is a command group. Specify a subcommand.` }, meta: { command: resolved.path, duration: `${Math.round(performance.now() - start)}ms` } },
|
|
1433
|
+
404,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
if ('fetchGateway' in resolved)
|
|
1437
|
+
return resolved.fetchGateway.fetch(req)
|
|
1438
|
+
|
|
1439
|
+
const { command, path, rest } = resolved
|
|
1440
|
+
return executeCommand(path, command, rest, inputOptions, start, options)
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
1444
|
+
async function executeCommand(
|
|
1445
|
+
path: string,
|
|
1446
|
+
command: CommandDefinition<any, any, any>,
|
|
1447
|
+
rest: string[],
|
|
1448
|
+
inputOptions: Record<string, unknown>,
|
|
1449
|
+
start: number,
|
|
1450
|
+
options: fetchImpl.Options,
|
|
1451
|
+
): Promise<Response> {
|
|
1452
|
+
function jsonResponse(body: unknown, status: number) {
|
|
1453
|
+
return new Response(JSON.stringify(body), {
|
|
1454
|
+
status,
|
|
1455
|
+
headers: { 'content-type': 'application/json' },
|
|
1456
|
+
})
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const sentinel_ = Symbol.for('incur.sentinel')
|
|
1460
|
+
const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
|
|
1461
|
+
let response: Response | undefined
|
|
1462
|
+
|
|
1463
|
+
const runCommand = async () => {
|
|
1464
|
+
const { args } = Parser.parse(rest, { args: command.args })
|
|
1465
|
+
const parsedOptions = command.options ? command.options.parse(inputOptions) : {}
|
|
1466
|
+
|
|
1467
|
+
const okFn = (data: unknown): never => ({ [sentinel_]: 'ok', data }) as never
|
|
1468
|
+
const errorFn = (opts: { code: string; message: string; exitCode?: number | undefined }): never =>
|
|
1469
|
+
({ [sentinel_]: 'error', ...opts }) as never
|
|
1470
|
+
|
|
1471
|
+
const result = command.run({
|
|
1472
|
+
agent: true,
|
|
1473
|
+
args,
|
|
1474
|
+
env: {},
|
|
1475
|
+
error: errorFn,
|
|
1476
|
+
format: 'json',
|
|
1477
|
+
formatExplicit: true,
|
|
1478
|
+
name: path,
|
|
1479
|
+
ok: okFn,
|
|
1480
|
+
options: parsedOptions,
|
|
1481
|
+
var: varsMap,
|
|
1482
|
+
version: undefined,
|
|
1483
|
+
})
|
|
1484
|
+
|
|
1485
|
+
// Streaming path — async generator → NDJSON response
|
|
1486
|
+
if (isAsyncGenerator(result)) {
|
|
1487
|
+
const stream = new ReadableStream({
|
|
1488
|
+
async start(controller) {
|
|
1489
|
+
const encoder = new TextEncoder()
|
|
1490
|
+
try {
|
|
1491
|
+
let returnValue: unknown
|
|
1492
|
+
while (true) {
|
|
1493
|
+
const { value, done } = await result.next()
|
|
1494
|
+
if (done) {
|
|
1495
|
+
returnValue = value
|
|
1496
|
+
break
|
|
1497
|
+
}
|
|
1498
|
+
if (isSentinel(value) && (value as any)[sentinel] === 'error') {
|
|
1499
|
+
const tagged = value as any
|
|
1500
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'))
|
|
1501
|
+
controller.close()
|
|
1502
|
+
return
|
|
1503
|
+
}
|
|
1504
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'))
|
|
1505
|
+
}
|
|
1506
|
+
const meta: Record<string, unknown> = { command: path }
|
|
1507
|
+
if (isSentinel(returnValue) && (returnValue as any)[sentinel] === 'error') {
|
|
1508
|
+
const tagged = returnValue as any
|
|
1509
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'))
|
|
1510
|
+
} else {
|
|
1511
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'))
|
|
1512
|
+
}
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: 'UNKNOWN', message: error instanceof Error ? error.message : String(error) } }) + '\n'))
|
|
1515
|
+
}
|
|
1516
|
+
controller.close()
|
|
1517
|
+
},
|
|
1518
|
+
})
|
|
1519
|
+
response = new Response(stream, { status: 200, headers: { 'content-type': 'application/x-ndjson' } })
|
|
1520
|
+
return
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const awaited = await result
|
|
1524
|
+
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1525
|
+
|
|
1526
|
+
if (typeof awaited === 'object' && awaited !== null && sentinel_ in awaited) {
|
|
1527
|
+
const tagged = awaited as any
|
|
1528
|
+
if (tagged[sentinel_] === 'error')
|
|
1529
|
+
response = jsonResponse(
|
|
1530
|
+
{ ok: false, error: { code: tagged.code, message: tagged.message }, meta: { command: path, duration } },
|
|
1531
|
+
500,
|
|
1532
|
+
)
|
|
1533
|
+
else
|
|
1534
|
+
response = jsonResponse(
|
|
1535
|
+
{ ok: true, data: tagged.data, meta: { command: path, duration } },
|
|
1536
|
+
200,
|
|
1537
|
+
)
|
|
1538
|
+
return
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
response = jsonResponse(
|
|
1542
|
+
{ ok: true, data: awaited, meta: { command: path, duration } },
|
|
1543
|
+
200,
|
|
1544
|
+
)
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
try {
|
|
1548
|
+
const allMiddleware = options.middlewares ?? []
|
|
1549
|
+
if (allMiddleware.length > 0) {
|
|
1550
|
+
const errorFn = (opts: { code: string; message: string; exitCode?: number | undefined }): never => {
|
|
1551
|
+
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1552
|
+
response = jsonResponse(
|
|
1553
|
+
{ ok: false, error: { code: opts.code, message: opts.message }, meta: { command: path, duration } },
|
|
1554
|
+
500,
|
|
1555
|
+
)
|
|
1556
|
+
return undefined as never
|
|
1557
|
+
}
|
|
1558
|
+
const mwCtx: MiddlewareContext = {
|
|
1559
|
+
agent: true,
|
|
1560
|
+
command: path,
|
|
1561
|
+
env: {},
|
|
1562
|
+
error: errorFn,
|
|
1563
|
+
format: 'json',
|
|
1564
|
+
formatExplicit: true,
|
|
1565
|
+
name: path,
|
|
1566
|
+
set(key: string, value: unknown) { varsMap[key] = value },
|
|
1567
|
+
var: varsMap,
|
|
1568
|
+
version: undefined,
|
|
1569
|
+
}
|
|
1570
|
+
const composed = allMiddleware.reduceRight(
|
|
1571
|
+
(next: () => Promise<void>, mw) => async () => { await mw(mwCtx, next) },
|
|
1572
|
+
runCommand,
|
|
1573
|
+
)
|
|
1574
|
+
await composed()
|
|
1575
|
+
} else {
|
|
1576
|
+
await runCommand()
|
|
1577
|
+
}
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1580
|
+
if (error instanceof ValidationError)
|
|
1581
|
+
return jsonResponse(
|
|
1582
|
+
{ ok: false, error: { code: 'VALIDATION_ERROR', message: error.message }, meta: { command: path, duration } },
|
|
1583
|
+
400,
|
|
1584
|
+
)
|
|
1585
|
+
return jsonResponse(
|
|
1586
|
+
{ ok: false, error: { code: error instanceof IncurError ? error.code : 'UNKNOWN', message: error instanceof Error ? error.message : String(error) }, meta: { command: path, duration } },
|
|
1587
|
+
500,
|
|
1588
|
+
)
|
|
1050
1589
|
}
|
|
1590
|
+
|
|
1591
|
+
return response!
|
|
1051
1592
|
}
|
|
1052
1593
|
|
|
1053
1594
|
/** @internal Formats a validation error for TTY with usage hint. */
|
|
@@ -1090,6 +1631,13 @@ function resolveCommand(
|
|
|
1090
1631
|
path: string
|
|
1091
1632
|
rest: string[]
|
|
1092
1633
|
}
|
|
1634
|
+
| {
|
|
1635
|
+
fetchGateway: InternalFetchGateway
|
|
1636
|
+
middlewares: MiddlewareHandler[]
|
|
1637
|
+
outputPolicy?: OutputPolicy | undefined
|
|
1638
|
+
path: string
|
|
1639
|
+
rest: string[]
|
|
1640
|
+
}
|
|
1093
1641
|
| {
|
|
1094
1642
|
help: true
|
|
1095
1643
|
path: string
|
|
@@ -1107,6 +1655,18 @@ function resolveCommand(
|
|
|
1107
1655
|
let inheritedOutputPolicy: OutputPolicy | undefined
|
|
1108
1656
|
const collectedMiddlewares: MiddlewareHandler[] = []
|
|
1109
1657
|
|
|
1658
|
+
// Fetch gateway — all remaining tokens go to the fetch handler
|
|
1659
|
+
if (isFetchGateway(entry)) {
|
|
1660
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
1661
|
+
return {
|
|
1662
|
+
fetchGateway: entry,
|
|
1663
|
+
middlewares: collectedMiddlewares,
|
|
1664
|
+
path: path.join(' '),
|
|
1665
|
+
rest: remaining,
|
|
1666
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1110
1670
|
while (isGroup(entry)) {
|
|
1111
1671
|
if (entry.outputPolicy) inheritedOutputPolicy = entry.outputPolicy
|
|
1112
1672
|
if (entry.middlewares) collectedMiddlewares.push(...entry.middlewares)
|
|
@@ -1127,6 +1687,17 @@ function resolveCommand(
|
|
|
1127
1687
|
path.push(next)
|
|
1128
1688
|
remaining = remaining.slice(1)
|
|
1129
1689
|
entry = child
|
|
1690
|
+
|
|
1691
|
+
if (isFetchGateway(entry)) {
|
|
1692
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
1693
|
+
return {
|
|
1694
|
+
fetchGateway: entry,
|
|
1695
|
+
middlewares: collectedMiddlewares,
|
|
1696
|
+
path: path.join(' '),
|
|
1697
|
+
rest: remaining,
|
|
1698
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1130
1701
|
}
|
|
1131
1702
|
|
|
1132
1703
|
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
@@ -1161,6 +1732,8 @@ declare namespace serveImpl {
|
|
|
1161
1732
|
| undefined
|
|
1162
1733
|
/** Root command handler, invoked when no subcommand matches. */
|
|
1163
1734
|
rootCommand?: CommandDefinition<any, any, any> | undefined
|
|
1735
|
+
/** Root fetch handler, invoked when no subcommand matches and no rootCommand is set. */
|
|
1736
|
+
rootFetch?: FetchHandler | undefined
|
|
1164
1737
|
sync?:
|
|
1165
1738
|
| {
|
|
1166
1739
|
cwd?: string | undefined
|
|
@@ -1182,8 +1755,10 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1182
1755
|
let mcp = false
|
|
1183
1756
|
let help = false
|
|
1184
1757
|
let version = false
|
|
1758
|
+
let schema = false
|
|
1185
1759
|
let format: Formatter.Format = 'toon'
|
|
1186
1760
|
let formatExplicit = false
|
|
1761
|
+
let filterOutput: string | undefined
|
|
1187
1762
|
const rest: string[] = []
|
|
1188
1763
|
|
|
1189
1764
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -1193,6 +1768,7 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1193
1768
|
else if (token === '--mcp') mcp = true
|
|
1194
1769
|
else if (token === '--help' || token === '-h') help = true
|
|
1195
1770
|
else if (token === '--version') version = true
|
|
1771
|
+
else if (token === '--schema') schema = true
|
|
1196
1772
|
else if (token === '--json') {
|
|
1197
1773
|
format = 'json'
|
|
1198
1774
|
formatExplicit = true
|
|
@@ -1200,10 +1776,13 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1200
1776
|
format = argv[i + 1] as Formatter.Format
|
|
1201
1777
|
formatExplicit = true
|
|
1202
1778
|
i++
|
|
1779
|
+
} else if (token === '--filter-output' && argv[i + 1]) {
|
|
1780
|
+
filterOutput = argv[i + 1]!
|
|
1781
|
+
i++
|
|
1203
1782
|
} else rest.push(token)
|
|
1204
1783
|
}
|
|
1205
1784
|
|
|
1206
|
-
return { verbose, format, formatExplicit, llms, mcp, help, version, rest }
|
|
1785
|
+
return { verbose, format, formatExplicit, filterOutput, llms, mcp, help, version, schema, rest }
|
|
1207
1786
|
}
|
|
1208
1787
|
|
|
1209
1788
|
/** @internal Collects immediate child commands/groups for help output. */
|
|
@@ -1212,24 +1791,45 @@ function collectHelpCommands(
|
|
|
1212
1791
|
): { name: string; description?: string | undefined }[] {
|
|
1213
1792
|
const result: { name: string; description?: string | undefined }[] = []
|
|
1214
1793
|
for (const [name, entry] of commands) {
|
|
1215
|
-
|
|
1216
|
-
else result.push({ name, description: entry.description })
|
|
1794
|
+
result.push({ name, description: entry.description })
|
|
1217
1795
|
}
|
|
1218
1796
|
return result.sort((a, b) => a.name.localeCompare(b.name))
|
|
1219
1797
|
}
|
|
1220
1798
|
|
|
1799
|
+
/** @internal Formats help text for a fetch gateway command. */
|
|
1800
|
+
function formatFetchHelp(name: string, description?: string): string {
|
|
1801
|
+
const lines: string[] = []
|
|
1802
|
+
if (description) lines.push(`${name} — ${description}`)
|
|
1803
|
+
else lines.push(name)
|
|
1804
|
+
lines.push('')
|
|
1805
|
+
lines.push(`Usage: ${name} <path> [options]`)
|
|
1806
|
+
lines.push('')
|
|
1807
|
+
lines.push('Path segments are joined into the request URL path.')
|
|
1808
|
+
lines.push('')
|
|
1809
|
+
lines.push('Options:')
|
|
1810
|
+
lines.push(' -X, --method <METHOD> HTTP method (default: GET, POST if body present)')
|
|
1811
|
+
lines.push(' -H, --header "Key: Val" Set a request header (repeatable)')
|
|
1812
|
+
lines.push(' -d, --data <json> Request body (implies POST)')
|
|
1813
|
+
lines.push(' --body <json> Request body (implies POST)')
|
|
1814
|
+
lines.push(' --<key> <value> Query string parameter')
|
|
1815
|
+
return lines.join('\n')
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1221
1818
|
/** Shape of the commands map accumulated through `.command()` chains. */
|
|
1222
1819
|
export type CommandsMap = Record<
|
|
1223
1820
|
string,
|
|
1224
1821
|
{ args: Record<string, unknown>; options: Record<string, unknown> }
|
|
1225
1822
|
>
|
|
1226
1823
|
|
|
1227
|
-
/** @internal Entry stored in a command map — either a leaf definition or a
|
|
1228
|
-
type CommandEntry = CommandDefinition<any, any, any> | InternalGroup
|
|
1824
|
+
/** @internal Entry stored in a command map — either a leaf definition, a group, or a fetch gateway. */
|
|
1825
|
+
type CommandEntry = CommandDefinition<any, any, any> | InternalGroup | InternalFetchGateway
|
|
1229
1826
|
|
|
1230
1827
|
/** Controls when output data is displayed. `'all'` displays to both humans and agents. `'agent-only'` suppresses data output in human/TTY mode. */
|
|
1231
1828
|
export type OutputPolicy = 'agent-only' | 'all'
|
|
1232
1829
|
|
|
1830
|
+
/** A standard Fetch API handler. */
|
|
1831
|
+
type FetchHandler = (req: Request) => Response | Promise<Response>
|
|
1832
|
+
|
|
1233
1833
|
/** @internal A command group's internal storage. */
|
|
1234
1834
|
type InternalGroup = {
|
|
1235
1835
|
_group: true
|
|
@@ -1239,11 +1839,25 @@ type InternalGroup = {
|
|
|
1239
1839
|
commands: Map<string, CommandEntry>
|
|
1240
1840
|
}
|
|
1241
1841
|
|
|
1842
|
+
/** @internal A fetch gateway entry. */
|
|
1843
|
+
type InternalFetchGateway = {
|
|
1844
|
+
_fetch: true
|
|
1845
|
+
basePath?: string | undefined
|
|
1846
|
+
description?: string | undefined
|
|
1847
|
+
fetch: FetchHandler
|
|
1848
|
+
outputPolicy?: OutputPolicy | undefined
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1242
1851
|
/** @internal Type guard for command groups. */
|
|
1243
1852
|
function isGroup(entry: CommandEntry): entry is InternalGroup {
|
|
1244
1853
|
return '_group' in entry
|
|
1245
1854
|
}
|
|
1246
1855
|
|
|
1856
|
+
/** @internal Type guard for fetch gateways. */
|
|
1857
|
+
function isFetchGateway(entry: CommandEntry): entry is InternalFetchGateway {
|
|
1858
|
+
return '_fetch' in entry
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1247
1861
|
/** @internal Maps CLI instances to their command maps. */
|
|
1248
1862
|
export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
|
|
1249
1863
|
|
|
@@ -1272,6 +1886,7 @@ type ErrorResult = {
|
|
|
1272
1886
|
code: string
|
|
1273
1887
|
message: string
|
|
1274
1888
|
retryable?: boolean | undefined
|
|
1889
|
+
exitCode?: number | undefined
|
|
1275
1890
|
cta?: CtaBlock | undefined
|
|
1276
1891
|
}
|
|
1277
1892
|
|
|
@@ -1377,7 +1992,7 @@ async function handleStreaming(
|
|
|
1377
1992
|
}),
|
|
1378
1993
|
)
|
|
1379
1994
|
else ctx.writeln(formatHumanError({ code: tagged.code, message: tagged.message }))
|
|
1380
|
-
ctx.exit(1)
|
|
1995
|
+
ctx.exit(tagged.exitCode ?? 1)
|
|
1381
1996
|
return
|
|
1382
1997
|
}
|
|
1383
1998
|
}
|
|
@@ -1401,7 +2016,7 @@ async function handleStreaming(
|
|
|
1401
2016
|
}),
|
|
1402
2017
|
)
|
|
1403
2018
|
else ctx.writeln(formatHumanError({ code: err.code, message: err.message }))
|
|
1404
|
-
ctx.exit(1)
|
|
2019
|
+
ctx.exit(err.exitCode ?? 1)
|
|
1405
2020
|
return
|
|
1406
2021
|
}
|
|
1407
2022
|
|
|
@@ -1442,7 +2057,7 @@ async function handleStreaming(
|
|
|
1442
2057
|
message: error instanceof Error ? error.message : String(error),
|
|
1443
2058
|
}),
|
|
1444
2059
|
)
|
|
1445
|
-
ctx.exit(1)
|
|
2060
|
+
ctx.exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1)
|
|
1446
2061
|
}
|
|
1447
2062
|
} else {
|
|
1448
2063
|
// Buffered output: collect all chunks, write as single value
|
|
@@ -1470,7 +2085,7 @@ async function handleStreaming(
|
|
|
1470
2085
|
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1471
2086
|
},
|
|
1472
2087
|
})
|
|
1473
|
-
ctx.exit(1)
|
|
2088
|
+
ctx.exit(tagged.exitCode ?? 1)
|
|
1474
2089
|
return
|
|
1475
2090
|
}
|
|
1476
2091
|
}
|
|
@@ -1491,7 +2106,7 @@ async function handleStreaming(
|
|
|
1491
2106
|
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1492
2107
|
},
|
|
1493
2108
|
})
|
|
1494
|
-
ctx.exit(1)
|
|
2109
|
+
ctx.exit(err.exitCode ?? 1)
|
|
1495
2110
|
return
|
|
1496
2111
|
}
|
|
1497
2112
|
|
|
@@ -1521,7 +2136,7 @@ async function handleStreaming(
|
|
|
1521
2136
|
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1522
2137
|
},
|
|
1523
2138
|
})
|
|
1524
|
-
ctx.exit(1)
|
|
2139
|
+
ctx.exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1)
|
|
1525
2140
|
}
|
|
1526
2141
|
}
|
|
1527
2142
|
}
|
|
@@ -1570,7 +2185,11 @@ function collectCommands(
|
|
|
1570
2185
|
const result: ReturnType<typeof collectCommands> = []
|
|
1571
2186
|
for (const [name, entry] of commands) {
|
|
1572
2187
|
const path = [...prefix, name]
|
|
1573
|
-
if (
|
|
2188
|
+
if (isFetchGateway(entry)) {
|
|
2189
|
+
const cmd: (typeof result)[number] = { name: path.join(' ') }
|
|
2190
|
+
if (entry.description) cmd.description = entry.description
|
|
2191
|
+
result.push(cmd)
|
|
2192
|
+
} else if (isGroup(entry)) {
|
|
1574
2193
|
result.push(...collectCommands(entry.commands, path))
|
|
1575
2194
|
} else {
|
|
1576
2195
|
const cmd: (typeof result)[number] = { name: path.join(' ') }
|
|
@@ -1609,7 +2228,12 @@ function collectSkillCommands(
|
|
|
1609
2228
|
const result: Skill.CommandInfo[] = []
|
|
1610
2229
|
for (const [name, entry] of commands) {
|
|
1611
2230
|
const path = [...prefix, name]
|
|
1612
|
-
if (
|
|
2231
|
+
if (isFetchGateway(entry)) {
|
|
2232
|
+
const cmd: Skill.CommandInfo = { name: path.join(' ') }
|
|
2233
|
+
if (entry.description) cmd.description = entry.description
|
|
2234
|
+
cmd.hint = 'Fetch gateway. Pass path segments and curl-style flags (-X, -H, -d, --key value).'
|
|
2235
|
+
result.push(cmd)
|
|
2236
|
+
} else if (isGroup(entry)) {
|
|
1613
2237
|
if (entry.description) groups.set(path.join(' '), entry.description)
|
|
1614
2238
|
result.push(...collectSkillCommands(entry.commands, path, groups))
|
|
1615
2239
|
} else {
|
|
@@ -1809,17 +2433,22 @@ type CommandDefinition<
|
|
|
1809
2433
|
agent: boolean
|
|
1810
2434
|
/** Positional arguments. */
|
|
1811
2435
|
args: InferOutput<args>
|
|
1812
|
-
/** The CLI name. */
|
|
1813
|
-
name: string
|
|
1814
2436
|
/** Parsed environment variables. */
|
|
1815
2437
|
env: InferOutput<env>
|
|
1816
2438
|
/** Return an error result with optional CTAs. */
|
|
1817
2439
|
error: (options: {
|
|
1818
2440
|
code: string
|
|
1819
2441
|
cta?: CtaBlock | undefined
|
|
2442
|
+
exitCode?: number | undefined
|
|
1820
2443
|
message: string
|
|
1821
2444
|
retryable?: boolean | undefined
|
|
1822
2445
|
}) => never
|
|
2446
|
+
/** The resolved output format (e.g. `'toon'`, `'json'`, `'jsonl'`). */
|
|
2447
|
+
format: Formatter.Format
|
|
2448
|
+
/** Whether the user explicitly passed `--format` or `--json`. */
|
|
2449
|
+
formatExplicit: boolean
|
|
2450
|
+
/** The CLI name. */
|
|
2451
|
+
name: string
|
|
1823
2452
|
/** Return a success result with optional metadata (e.g. CTAs). */
|
|
1824
2453
|
ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
|
|
1825
2454
|
options: InferOutput<options>
|