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/dist/Cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as Completions from './Completions.js';
|
|
2
|
+
import * as Filter from './Filter.js';
|
|
2
3
|
import { IncurError, ValidationError } from './Errors.js';
|
|
4
|
+
import * as Fetch from './Fetch.js';
|
|
5
|
+
import * as Openapi from './Openapi.js';
|
|
3
6
|
import * as Formatter from './Formatter.js';
|
|
4
7
|
import * as Help from './Help.js';
|
|
5
8
|
import { detectRunner } from './internal/pm.js';
|
|
@@ -13,8 +16,11 @@ export function create(nameOrDefinition, definition) {
|
|
|
13
16
|
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
|
|
14
17
|
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition;
|
|
15
18
|
const rootDef = 'run' in def ? def : undefined;
|
|
19
|
+
const rootFetch = 'fetch' in def ? def.fetch : undefined;
|
|
16
20
|
const commands = new Map();
|
|
17
21
|
const middlewares = [];
|
|
22
|
+
const pending = [];
|
|
23
|
+
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0');
|
|
18
24
|
const cli = {
|
|
19
25
|
name,
|
|
20
26
|
description: def.description,
|
|
@@ -22,6 +28,28 @@ export function create(nameOrDefinition, definition) {
|
|
|
22
28
|
vars: def.vars,
|
|
23
29
|
command(nameOrCli, def) {
|
|
24
30
|
if (typeof nameOrCli === 'string') {
|
|
31
|
+
if (def && 'fetch' in def && typeof def.fetch === 'function') {
|
|
32
|
+
// OpenAPI + fetch → generate typed command group (async, resolved before serve)
|
|
33
|
+
if (def.openapi) {
|
|
34
|
+
pending.push(Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then((generated) => {
|
|
35
|
+
commands.set(nameOrCli, {
|
|
36
|
+
_group: true,
|
|
37
|
+
description: def.description,
|
|
38
|
+
commands: generated,
|
|
39
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
40
|
+
});
|
|
41
|
+
}));
|
|
42
|
+
return cli;
|
|
43
|
+
}
|
|
44
|
+
commands.set(nameOrCli, {
|
|
45
|
+
_fetch: true,
|
|
46
|
+
basePath: def.basePath,
|
|
47
|
+
description: def.description,
|
|
48
|
+
fetch: def.fetch,
|
|
49
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
50
|
+
});
|
|
51
|
+
return cli;
|
|
52
|
+
}
|
|
25
53
|
commands.set(nameOrCli, def);
|
|
26
54
|
return cli;
|
|
27
55
|
}
|
|
@@ -43,7 +71,19 @@ export function create(nameOrDefinition, definition) {
|
|
|
43
71
|
});
|
|
44
72
|
return cli;
|
|
45
73
|
},
|
|
74
|
+
async fetch(req) {
|
|
75
|
+
if (pending.length > 0)
|
|
76
|
+
await Promise.all(pending);
|
|
77
|
+
return fetchImpl(name, commands, req, {
|
|
78
|
+
mcpHandler,
|
|
79
|
+
middlewares,
|
|
80
|
+
rootCommand: rootDef,
|
|
81
|
+
vars: def.vars,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
46
84
|
async serve(argv = process.argv.slice(2), serveOptions = {}) {
|
|
85
|
+
if (pending.length > 0)
|
|
86
|
+
await Promise.all(pending);
|
|
47
87
|
return serveImpl(name, commands, argv, {
|
|
48
88
|
...serveOptions,
|
|
49
89
|
aliases: def.aliases,
|
|
@@ -54,6 +94,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
54
94
|
middlewares,
|
|
55
95
|
outputPolicy: def.outputPolicy,
|
|
56
96
|
rootCommand: rootDef,
|
|
97
|
+
rootFetch,
|
|
57
98
|
sync: def.sync,
|
|
58
99
|
vars: def.vars,
|
|
59
100
|
version: def.version,
|
|
@@ -77,7 +118,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
77
118
|
async function serveImpl(name, commands, argv, options = {}) {
|
|
78
119
|
const stdout = options.stdout ?? ((s) => process.stdout.write(s));
|
|
79
120
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
80
|
-
const { verbose, format: formatFlag, formatExplicit, llms, mcp: mcpFlag, help, version, rest: filtered, } = extractBuiltinFlags(argv);
|
|
121
|
+
const { verbose, format: formatFlag, formatExplicit, filterOutput, llms, mcp: mcpFlag, help, version, schema, rest: filtered, } = extractBuiltinFlags(argv);
|
|
81
122
|
// --mcp: start as MCP stdio server
|
|
82
123
|
if (mcpFlag) {
|
|
83
124
|
await Mcp.serve(name, options.version ?? '0.0.0', commands);
|
|
@@ -109,7 +150,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
109
150
|
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
110
151
|
}
|
|
111
152
|
// Skills staleness check (skip for built-in commands)
|
|
112
|
-
if (!llms && !help && !version) {
|
|
153
|
+
if (!llms && !schema && !help && !version) {
|
|
113
154
|
const isSkillsAdd = filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills');
|
|
114
155
|
const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp');
|
|
115
156
|
if (!isSkillsAdd && !isMcpAdd) {
|
|
@@ -363,8 +404,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
363
404
|
}));
|
|
364
405
|
return;
|
|
365
406
|
}
|
|
366
|
-
if (options.rootCommand) {
|
|
367
|
-
// Root command with no args — treat as root invocation
|
|
407
|
+
if (options.rootCommand || options.rootFetch) {
|
|
408
|
+
// Root command/fetch with no args — treat as root invocation
|
|
368
409
|
}
|
|
369
410
|
else {
|
|
370
411
|
writeln(Help.formatRoot(name, {
|
|
@@ -379,7 +420,15 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
379
420
|
}
|
|
380
421
|
const resolved = filtered.length === 0 && options.rootCommand
|
|
381
422
|
? { command: options.rootCommand, path: name, rest: [] }
|
|
382
|
-
:
|
|
423
|
+
: filtered.length === 0 && options.rootFetch
|
|
424
|
+
? { fetchGateway: { _fetch: true, fetch: options.rootFetch, description: options.description }, middlewares: [], path: name, rest: [] }
|
|
425
|
+
: resolveCommand(commands, filtered);
|
|
426
|
+
// --help on a fetch gateway → show fetch-specific help
|
|
427
|
+
if (help && 'fetchGateway' in resolved) {
|
|
428
|
+
const commandName = resolved.path === name ? name : `${name} ${resolved.path}`;
|
|
429
|
+
writeln(formatFetchHelp(commandName, resolved.fetchGateway.description));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
383
432
|
// --help after a command → show help for that command
|
|
384
433
|
if (help) {
|
|
385
434
|
if ('help' in resolved || 'error' in resolved) {
|
|
@@ -417,30 +466,65 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
417
466
|
}));
|
|
418
467
|
}
|
|
419
468
|
}
|
|
420
|
-
else {
|
|
469
|
+
else if ('command' in resolved) {
|
|
470
|
+
const cmd = resolved.command;
|
|
421
471
|
const isRootCmd = resolved.path === name;
|
|
422
472
|
const commandName = isRootCmd ? name : `${name} ${resolved.path}`;
|
|
423
473
|
const helpSubcommands = isRootCmd && options.rootCommand && commands.size > 0
|
|
424
474
|
? collectHelpCommands(commands)
|
|
425
475
|
: undefined;
|
|
426
476
|
writeln(Help.formatCommand(commandName, {
|
|
427
|
-
alias:
|
|
477
|
+
alias: cmd.alias,
|
|
428
478
|
aliases: isRootCmd ? options.aliases : undefined,
|
|
429
|
-
description:
|
|
479
|
+
description: cmd.description,
|
|
430
480
|
version: isRootCmd ? options.version : undefined,
|
|
431
|
-
args:
|
|
432
|
-
env:
|
|
481
|
+
args: cmd.args,
|
|
482
|
+
env: cmd.env,
|
|
433
483
|
envSource: options.env,
|
|
434
|
-
hint:
|
|
435
|
-
options:
|
|
436
|
-
examples: formatExamples(
|
|
437
|
-
usage:
|
|
484
|
+
hint: cmd.hint,
|
|
485
|
+
options: cmd.options,
|
|
486
|
+
examples: formatExamples(cmd.examples),
|
|
487
|
+
usage: cmd.usage,
|
|
438
488
|
commands: helpSubcommands,
|
|
439
489
|
root: isRootCmd,
|
|
440
490
|
}));
|
|
441
491
|
}
|
|
442
492
|
return;
|
|
443
493
|
}
|
|
494
|
+
// --schema: output JSON Schema for a command's args, env, options, output
|
|
495
|
+
if (schema) {
|
|
496
|
+
if ('help' in resolved) {
|
|
497
|
+
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
498
|
+
description: resolved.description,
|
|
499
|
+
commands: collectHelpCommands(resolved.commands),
|
|
500
|
+
}));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if ('error' in resolved) {
|
|
504
|
+
const parent = resolved.path ? `${name} ${resolved.path}` : name;
|
|
505
|
+
writeln(`Error: '${resolved.error}' is not a command for '${parent}'.`);
|
|
506
|
+
exit(1);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if ('fetchGateway' in resolved) {
|
|
510
|
+
writeln('--schema is not supported for fetch commands.');
|
|
511
|
+
exit(1);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const cmd = resolved.command;
|
|
515
|
+
const format = formatExplicit ? formatFlag : 'toon';
|
|
516
|
+
const result = {};
|
|
517
|
+
if (cmd.args)
|
|
518
|
+
result.args = Schema.toJsonSchema(cmd.args);
|
|
519
|
+
if (cmd.env)
|
|
520
|
+
result.env = Schema.toJsonSchema(cmd.env);
|
|
521
|
+
if (cmd.options)
|
|
522
|
+
result.options = Schema.toJsonSchema(cmd.options);
|
|
523
|
+
if (cmd.output)
|
|
524
|
+
result.output = Schema.toJsonSchema(cmd.output);
|
|
525
|
+
writeln(Formatter.format(result, format));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
444
528
|
if ('help' in resolved) {
|
|
445
529
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
446
530
|
description: resolved.description,
|
|
@@ -452,14 +536,19 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
452
536
|
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
453
537
|
const resolvedFormat = 'command' in resolved && resolved.command.format;
|
|
454
538
|
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon';
|
|
455
|
-
// Fall back to root
|
|
456
|
-
const effective = 'error' in resolved && options.
|
|
457
|
-
? {
|
|
458
|
-
: resolved
|
|
539
|
+
// Fall back to root fetch when no subcommand matches
|
|
540
|
+
const effective = 'error' in resolved && options.rootFetch && !resolved.path
|
|
541
|
+
? { fetchGateway: { _fetch: true, fetch: options.rootFetch, description: options.description }, middlewares: [], path: name, rest: filtered }
|
|
542
|
+
: 'error' in resolved && options.rootCommand && !resolved.path
|
|
543
|
+
? { command: options.rootCommand, path: name, rest: filtered }
|
|
544
|
+
: resolved;
|
|
459
545
|
// Resolve outputPolicy: command/group → CLI-level → default ('all')
|
|
460
546
|
const effectiveOutputPolicy = ('outputPolicy' in resolved && resolved.outputPolicy) || options.outputPolicy;
|
|
461
547
|
const renderOutput = !(human && !formatExplicit && effectiveOutputPolicy === 'agent-only');
|
|
548
|
+
const filterPaths = filterOutput ? Filter.parse(filterOutput) : undefined;
|
|
462
549
|
function write(output) {
|
|
550
|
+
if (filterPaths && output.ok && output.data != null)
|
|
551
|
+
output = { ...output, data: Filter.apply(output.data, filterPaths) };
|
|
463
552
|
const cta = output.meta.cta;
|
|
464
553
|
if (human && !verbose) {
|
|
465
554
|
if (output.ok && output.data != null && renderOutput)
|
|
@@ -508,6 +597,114 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
508
597
|
exit(1);
|
|
509
598
|
return;
|
|
510
599
|
}
|
|
600
|
+
// Fetch gateway execution path
|
|
601
|
+
if ('fetchGateway' in effective) {
|
|
602
|
+
const { fetchGateway, path, rest: fetchRest } = effective;
|
|
603
|
+
const fetchMiddleware = [
|
|
604
|
+
...(options.middlewares ?? []),
|
|
605
|
+
...(effective.middlewares ?? []),
|
|
606
|
+
];
|
|
607
|
+
const runFetch = async () => {
|
|
608
|
+
const input = Fetch.parseArgv(fetchRest);
|
|
609
|
+
if (fetchGateway.basePath)
|
|
610
|
+
input.path = fetchGateway.basePath + input.path;
|
|
611
|
+
const request = Fetch.buildRequest(input);
|
|
612
|
+
const response = await fetchGateway.fetch(request);
|
|
613
|
+
// Streaming path — NDJSON responses pipe through handleStreaming
|
|
614
|
+
if (Fetch.isStreamingResponse(response)) {
|
|
615
|
+
const generator = Fetch.parseStreamingResponse(response);
|
|
616
|
+
await handleStreaming(generator, {
|
|
617
|
+
name,
|
|
618
|
+
path,
|
|
619
|
+
start,
|
|
620
|
+
format,
|
|
621
|
+
formatExplicit,
|
|
622
|
+
human,
|
|
623
|
+
renderOutput,
|
|
624
|
+
verbose,
|
|
625
|
+
write,
|
|
626
|
+
writeln,
|
|
627
|
+
exit,
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const output = await Fetch.parseResponse(response);
|
|
632
|
+
if (output.ok) {
|
|
633
|
+
write({
|
|
634
|
+
ok: true,
|
|
635
|
+
data: output.data,
|
|
636
|
+
meta: {
|
|
637
|
+
command: path,
|
|
638
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
write({
|
|
644
|
+
ok: false,
|
|
645
|
+
error: {
|
|
646
|
+
code: `HTTP_${output.status}`,
|
|
647
|
+
message: typeof output.data === 'object' && output.data !== null && 'message' in output.data
|
|
648
|
+
? String(output.data.message)
|
|
649
|
+
: typeof output.data === 'string' ? output.data : `HTTP ${output.status}`,
|
|
650
|
+
},
|
|
651
|
+
meta: {
|
|
652
|
+
command: path,
|
|
653
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
exit(1);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
try {
|
|
660
|
+
const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, options.env ?? process.env) : {};
|
|
661
|
+
if (fetchMiddleware.length > 0) {
|
|
662
|
+
const varsMap = options.vars ? options.vars.parse({}) : {};
|
|
663
|
+
const errorFn = (opts) => ({ [sentinel]: 'error', ...opts });
|
|
664
|
+
const mwCtx = {
|
|
665
|
+
agent: !human,
|
|
666
|
+
command: path,
|
|
667
|
+
env: cliEnv,
|
|
668
|
+
error: errorFn,
|
|
669
|
+
format,
|
|
670
|
+
formatExplicit,
|
|
671
|
+
name,
|
|
672
|
+
set(key, value) { varsMap[key] = value; },
|
|
673
|
+
var: varsMap,
|
|
674
|
+
version: options.version,
|
|
675
|
+
};
|
|
676
|
+
const handleMwSentinel = (result) => {
|
|
677
|
+
if (!isSentinel(result) || result[sentinel] !== 'error')
|
|
678
|
+
return;
|
|
679
|
+
const err = result;
|
|
680
|
+
const cta = formatCtaBlock(name, err.cta);
|
|
681
|
+
write({
|
|
682
|
+
ok: false,
|
|
683
|
+
error: { code: err.code, message: err.message, ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined) },
|
|
684
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms`, ...(cta ? { cta } : undefined) },
|
|
685
|
+
});
|
|
686
|
+
exit(err.exitCode ?? 1);
|
|
687
|
+
};
|
|
688
|
+
const composed = fetchMiddleware.reduceRight((next, mw) => async () => { handleMwSentinel(await mw(mwCtx, next)); }, runFetch);
|
|
689
|
+
await composed();
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
await runFetch();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
write({
|
|
697
|
+
ok: false,
|
|
698
|
+
error: {
|
|
699
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
700
|
+
message: error instanceof Error ? error.message : String(error),
|
|
701
|
+
},
|
|
702
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
703
|
+
});
|
|
704
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
511
708
|
const { command, path, rest } = effective;
|
|
512
709
|
// Collect middleware: root CLI + groups traversed + per-command
|
|
513
710
|
const allMiddleware = [
|
|
@@ -539,10 +736,12 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
539
736
|
agent: !human,
|
|
540
737
|
args,
|
|
541
738
|
env,
|
|
739
|
+
error: errorFn,
|
|
740
|
+
format,
|
|
741
|
+
formatExplicit,
|
|
542
742
|
name,
|
|
543
|
-
options: parsedOptions,
|
|
544
743
|
ok: okFn,
|
|
545
|
-
|
|
744
|
+
options: parsedOptions,
|
|
546
745
|
var: varsMap,
|
|
547
746
|
version: options.version,
|
|
548
747
|
});
|
|
@@ -578,12 +777,13 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
578
777
|
});
|
|
579
778
|
}
|
|
580
779
|
else {
|
|
780
|
+
const err = awaited;
|
|
581
781
|
write({
|
|
582
782
|
ok: false,
|
|
583
783
|
error: {
|
|
584
|
-
code:
|
|
585
|
-
message:
|
|
586
|
-
...(
|
|
784
|
+
code: err.code,
|
|
785
|
+
message: err.message,
|
|
786
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
587
787
|
},
|
|
588
788
|
meta: {
|
|
589
789
|
command: path,
|
|
@@ -591,7 +791,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
591
791
|
...(cta ? { cta } : undefined),
|
|
592
792
|
},
|
|
593
793
|
});
|
|
594
|
-
exit(1);
|
|
794
|
+
exit(err.exitCode ?? 1);
|
|
595
795
|
}
|
|
596
796
|
}
|
|
597
797
|
else {
|
|
@@ -616,6 +816,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
616
816
|
command: path,
|
|
617
817
|
env: cliEnv,
|
|
618
818
|
error: errorFn,
|
|
819
|
+
format,
|
|
820
|
+
formatExplicit,
|
|
619
821
|
name,
|
|
620
822
|
set(key, value) {
|
|
621
823
|
varsMap[key] = value;
|
|
@@ -626,13 +828,14 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
626
828
|
const handleMwSentinel = (result) => {
|
|
627
829
|
if (!isSentinel(result) || result[sentinel] !== 'error')
|
|
628
830
|
return;
|
|
629
|
-
const
|
|
831
|
+
const err = result;
|
|
832
|
+
const cta = formatCtaBlock(name, err.cta);
|
|
630
833
|
write({
|
|
631
834
|
ok: false,
|
|
632
835
|
error: {
|
|
633
|
-
code:
|
|
634
|
-
message:
|
|
635
|
-
...(
|
|
836
|
+
code: err.code,
|
|
837
|
+
message: err.message,
|
|
838
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
636
839
|
},
|
|
637
840
|
meta: {
|
|
638
841
|
command: path,
|
|
@@ -640,7 +843,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
640
843
|
...(cta ? { cta } : undefined),
|
|
641
844
|
},
|
|
642
845
|
});
|
|
643
|
-
exit(1);
|
|
846
|
+
exit(err.exitCode ?? 1);
|
|
644
847
|
};
|
|
645
848
|
const composed = allMiddleware.reduceRight((next, mw) => async () => {
|
|
646
849
|
handleMwSentinel(await mw(mwCtx, next));
|
|
@@ -675,9 +878,236 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
675
878
|
return;
|
|
676
879
|
}
|
|
677
880
|
write(errorOutput);
|
|
678
|
-
exit(1);
|
|
881
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
679
882
|
}
|
|
680
883
|
}
|
|
884
|
+
/** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
|
|
885
|
+
function createMcpHttpHandler(name, version) {
|
|
886
|
+
let transport;
|
|
887
|
+
return async (req, commands) => {
|
|
888
|
+
if (!transport) {
|
|
889
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
890
|
+
const { WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
|
|
891
|
+
const server = new McpServer({ name, version });
|
|
892
|
+
for (const tool of Mcp.collectTools(commands, [])) {
|
|
893
|
+
const mergedShape = {
|
|
894
|
+
...tool.command.args?.shape,
|
|
895
|
+
...tool.command.options?.shape,
|
|
896
|
+
};
|
|
897
|
+
const hasInput = Object.keys(mergedShape).length > 0;
|
|
898
|
+
server.registerTool(tool.name, {
|
|
899
|
+
...(tool.description ? { description: tool.description } : undefined),
|
|
900
|
+
...(hasInput ? { inputSchema: mergedShape } : undefined),
|
|
901
|
+
}, async (...callArgs) => {
|
|
902
|
+
const params = hasInput ? callArgs[0] : {};
|
|
903
|
+
return Mcp.callTool(tool, params);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
transport = new WebStandardStreamableHTTPServerTransport({
|
|
907
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
908
|
+
enableJsonResponse: true,
|
|
909
|
+
});
|
|
910
|
+
await server.connect(transport);
|
|
911
|
+
}
|
|
912
|
+
return transport.handleRequest(req);
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/** @internal Handles an HTTP request by resolving a command and returning a JSON Response. */
|
|
916
|
+
async function fetchImpl(name, commands, req, options = {}) {
|
|
917
|
+
const start = performance.now();
|
|
918
|
+
const url = new URL(req.url);
|
|
919
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
920
|
+
// MCP over HTTP: route /mcp to the MCP transport
|
|
921
|
+
if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
|
|
922
|
+
return options.mcpHandler(req, commands);
|
|
923
|
+
// .well-known/skills/ — Agent Skills Discovery (RFC)
|
|
924
|
+
if (segments[0] === '.well-known' &&
|
|
925
|
+
segments[1] === 'skills' &&
|
|
926
|
+
segments.length >= 3 &&
|
|
927
|
+
req.method === 'GET') {
|
|
928
|
+
const groups = new Map();
|
|
929
|
+
const cmds = collectSkillCommands(commands, [], groups);
|
|
930
|
+
// GET /.well-known/skills/index.json
|
|
931
|
+
if (segments[2] === 'index.json' && segments.length === 3) {
|
|
932
|
+
const files = Skill.split(name, cmds, 1, groups);
|
|
933
|
+
const skills = files.map((f) => {
|
|
934
|
+
const descMatch = f.content.match(/^description:\s*(.+)$/m);
|
|
935
|
+
return {
|
|
936
|
+
name: f.dir || name,
|
|
937
|
+
description: descMatch?.[1] ?? '',
|
|
938
|
+
files: ['SKILL.md'],
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
return new Response(JSON.stringify({ skills }), {
|
|
942
|
+
status: 200,
|
|
943
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=300' },
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
// GET /.well-known/skills/{skill-name}/SKILL.md
|
|
947
|
+
if (segments.length === 4 && segments[3] === 'SKILL.md') {
|
|
948
|
+
const skillName = segments[2];
|
|
949
|
+
const files = Skill.split(name, cmds, 1, groups);
|
|
950
|
+
const file = files.find((f) => (f.dir || name) === skillName);
|
|
951
|
+
if (file)
|
|
952
|
+
return new Response(file.content, {
|
|
953
|
+
status: 200,
|
|
954
|
+
headers: { 'content-type': 'text/markdown', 'cache-control': 'public, max-age=300' },
|
|
955
|
+
});
|
|
956
|
+
return new Response('Not Found', { status: 404 });
|
|
957
|
+
}
|
|
958
|
+
return new Response('Not Found', { status: 404 });
|
|
959
|
+
}
|
|
960
|
+
// Parse options from search params (GET) or body (non-GET)
|
|
961
|
+
let inputOptions = {};
|
|
962
|
+
if (req.method === 'GET')
|
|
963
|
+
for (const [key, value] of url.searchParams)
|
|
964
|
+
inputOptions[key] = value;
|
|
965
|
+
else {
|
|
966
|
+
try {
|
|
967
|
+
const contentType = req.headers.get('content-type') ?? '';
|
|
968
|
+
if (contentType.includes('application/json'))
|
|
969
|
+
inputOptions = (await req.json());
|
|
970
|
+
}
|
|
971
|
+
catch { }
|
|
972
|
+
}
|
|
973
|
+
function jsonResponse(body, status) {
|
|
974
|
+
return new Response(JSON.stringify(body), {
|
|
975
|
+
status,
|
|
976
|
+
headers: { 'content-type': 'application/json' },
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
// Resolve command from path segments
|
|
980
|
+
if (segments.length === 0) {
|
|
981
|
+
// Root path
|
|
982
|
+
if (options.rootCommand)
|
|
983
|
+
return executeCommand(name, options.rootCommand, [], inputOptions, start, options);
|
|
984
|
+
return jsonResponse({ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: 'No root command defined.' }, meta: { command: '/', duration: `${Math.round(performance.now() - start)}ms` } }, 404);
|
|
985
|
+
}
|
|
986
|
+
const resolved = resolveCommand(commands, segments);
|
|
987
|
+
if ('error' in resolved)
|
|
988
|
+
return jsonResponse({ 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` } }, 404);
|
|
989
|
+
if ('help' in resolved)
|
|
990
|
+
return jsonResponse({ 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` } }, 404);
|
|
991
|
+
if ('fetchGateway' in resolved)
|
|
992
|
+
return resolved.fetchGateway.fetch(req);
|
|
993
|
+
const { command, path, rest } = resolved;
|
|
994
|
+
return executeCommand(path, command, rest, inputOptions, start, options);
|
|
995
|
+
}
|
|
996
|
+
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
997
|
+
async function executeCommand(path, command, rest, inputOptions, start, options) {
|
|
998
|
+
function jsonResponse(body, status) {
|
|
999
|
+
return new Response(JSON.stringify(body), {
|
|
1000
|
+
status,
|
|
1001
|
+
headers: { 'content-type': 'application/json' },
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
const sentinel_ = Symbol.for('incur.sentinel');
|
|
1005
|
+
const varsMap = options.vars ? options.vars.parse({}) : {};
|
|
1006
|
+
let response;
|
|
1007
|
+
const runCommand = async () => {
|
|
1008
|
+
const { args } = Parser.parse(rest, { args: command.args });
|
|
1009
|
+
const parsedOptions = command.options ? command.options.parse(inputOptions) : {};
|
|
1010
|
+
const okFn = (data) => ({ [sentinel_]: 'ok', data });
|
|
1011
|
+
const errorFn = (opts) => ({ [sentinel_]: 'error', ...opts });
|
|
1012
|
+
const result = command.run({
|
|
1013
|
+
agent: true,
|
|
1014
|
+
args,
|
|
1015
|
+
env: {},
|
|
1016
|
+
error: errorFn,
|
|
1017
|
+
format: 'json',
|
|
1018
|
+
formatExplicit: true,
|
|
1019
|
+
name: path,
|
|
1020
|
+
ok: okFn,
|
|
1021
|
+
options: parsedOptions,
|
|
1022
|
+
var: varsMap,
|
|
1023
|
+
version: undefined,
|
|
1024
|
+
});
|
|
1025
|
+
// Streaming path — async generator → NDJSON response
|
|
1026
|
+
if (isAsyncGenerator(result)) {
|
|
1027
|
+
const stream = new ReadableStream({
|
|
1028
|
+
async start(controller) {
|
|
1029
|
+
const encoder = new TextEncoder();
|
|
1030
|
+
try {
|
|
1031
|
+
let returnValue;
|
|
1032
|
+
while (true) {
|
|
1033
|
+
const { value, done } = await result.next();
|
|
1034
|
+
if (done) {
|
|
1035
|
+
returnValue = value;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
if (isSentinel(value) && value[sentinel] === 'error') {
|
|
1039
|
+
const tagged = value;
|
|
1040
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'));
|
|
1041
|
+
controller.close();
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
|
|
1045
|
+
}
|
|
1046
|
+
const meta = { command: path };
|
|
1047
|
+
if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
|
|
1048
|
+
const tagged = returnValue;
|
|
1049
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'));
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: 'UNKNOWN', message: error instanceof Error ? error.message : String(error) } }) + '\n'));
|
|
1057
|
+
}
|
|
1058
|
+
controller.close();
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
response = new Response(stream, { status: 200, headers: { 'content-type': 'application/x-ndjson' } });
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const awaited = await result;
|
|
1065
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1066
|
+
if (typeof awaited === 'object' && awaited !== null && sentinel_ in awaited) {
|
|
1067
|
+
const tagged = awaited;
|
|
1068
|
+
if (tagged[sentinel_] === 'error')
|
|
1069
|
+
response = jsonResponse({ ok: false, error: { code: tagged.code, message: tagged.message }, meta: { command: path, duration } }, 500);
|
|
1070
|
+
else
|
|
1071
|
+
response = jsonResponse({ ok: true, data: tagged.data, meta: { command: path, duration } }, 200);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200);
|
|
1075
|
+
};
|
|
1076
|
+
try {
|
|
1077
|
+
const allMiddleware = options.middlewares ?? [];
|
|
1078
|
+
if (allMiddleware.length > 0) {
|
|
1079
|
+
const errorFn = (opts) => {
|
|
1080
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1081
|
+
response = jsonResponse({ ok: false, error: { code: opts.code, message: opts.message }, meta: { command: path, duration } }, 500);
|
|
1082
|
+
return undefined;
|
|
1083
|
+
};
|
|
1084
|
+
const mwCtx = {
|
|
1085
|
+
agent: true,
|
|
1086
|
+
command: path,
|
|
1087
|
+
env: {},
|
|
1088
|
+
error: errorFn,
|
|
1089
|
+
format: 'json',
|
|
1090
|
+
formatExplicit: true,
|
|
1091
|
+
name: path,
|
|
1092
|
+
set(key, value) { varsMap[key] = value; },
|
|
1093
|
+
var: varsMap,
|
|
1094
|
+
version: undefined,
|
|
1095
|
+
};
|
|
1096
|
+
const composed = allMiddleware.reduceRight((next, mw) => async () => { await mw(mwCtx, next); }, runCommand);
|
|
1097
|
+
await composed();
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
await runCommand();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1105
|
+
if (error instanceof ValidationError)
|
|
1106
|
+
return jsonResponse({ ok: false, error: { code: 'VALIDATION_ERROR', message: error.message }, meta: { command: path, duration } }, 400);
|
|
1107
|
+
return jsonResponse({ ok: false, error: { code: error instanceof IncurError ? error.code : 'UNKNOWN', message: error instanceof Error ? error.message : String(error) }, meta: { command: path, duration } }, 500);
|
|
1108
|
+
}
|
|
1109
|
+
return response;
|
|
1110
|
+
}
|
|
681
1111
|
/** @internal Formats a validation error for TTY with usage hint. */
|
|
682
1112
|
function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
683
1113
|
const lines = [];
|
|
@@ -708,6 +1138,17 @@ function resolveCommand(commands, tokens) {
|
|
|
708
1138
|
let remaining = rest;
|
|
709
1139
|
let inheritedOutputPolicy;
|
|
710
1140
|
const collectedMiddlewares = [];
|
|
1141
|
+
// Fetch gateway — all remaining tokens go to the fetch handler
|
|
1142
|
+
if (isFetchGateway(entry)) {
|
|
1143
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy;
|
|
1144
|
+
return {
|
|
1145
|
+
fetchGateway: entry,
|
|
1146
|
+
middlewares: collectedMiddlewares,
|
|
1147
|
+
path: path.join(' '),
|
|
1148
|
+
rest: remaining,
|
|
1149
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
711
1152
|
while (isGroup(entry)) {
|
|
712
1153
|
if (entry.outputPolicy)
|
|
713
1154
|
inheritedOutputPolicy = entry.outputPolicy;
|
|
@@ -728,6 +1169,16 @@ function resolveCommand(commands, tokens) {
|
|
|
728
1169
|
path.push(next);
|
|
729
1170
|
remaining = remaining.slice(1);
|
|
730
1171
|
entry = child;
|
|
1172
|
+
if (isFetchGateway(entry)) {
|
|
1173
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy;
|
|
1174
|
+
return {
|
|
1175
|
+
fetchGateway: entry,
|
|
1176
|
+
middlewares: collectedMiddlewares,
|
|
1177
|
+
path: path.join(' '),
|
|
1178
|
+
rest: remaining,
|
|
1179
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
731
1182
|
}
|
|
732
1183
|
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy;
|
|
733
1184
|
return {
|
|
@@ -745,8 +1196,10 @@ function extractBuiltinFlags(argv) {
|
|
|
745
1196
|
let mcp = false;
|
|
746
1197
|
let help = false;
|
|
747
1198
|
let version = false;
|
|
1199
|
+
let schema = false;
|
|
748
1200
|
let format = 'toon';
|
|
749
1201
|
let formatExplicit = false;
|
|
1202
|
+
let filterOutput;
|
|
750
1203
|
const rest = [];
|
|
751
1204
|
for (let i = 0; i < argv.length; i++) {
|
|
752
1205
|
const token = argv[i];
|
|
@@ -760,6 +1213,8 @@ function extractBuiltinFlags(argv) {
|
|
|
760
1213
|
help = true;
|
|
761
1214
|
else if (token === '--version')
|
|
762
1215
|
version = true;
|
|
1216
|
+
else if (token === '--schema')
|
|
1217
|
+
schema = true;
|
|
763
1218
|
else if (token === '--json') {
|
|
764
1219
|
format = 'json';
|
|
765
1220
|
formatExplicit = true;
|
|
@@ -769,26 +1224,51 @@ function extractBuiltinFlags(argv) {
|
|
|
769
1224
|
formatExplicit = true;
|
|
770
1225
|
i++;
|
|
771
1226
|
}
|
|
1227
|
+
else if (token === '--filter-output' && argv[i + 1]) {
|
|
1228
|
+
filterOutput = argv[i + 1];
|
|
1229
|
+
i++;
|
|
1230
|
+
}
|
|
772
1231
|
else
|
|
773
1232
|
rest.push(token);
|
|
774
1233
|
}
|
|
775
|
-
return { verbose, format, formatExplicit, llms, mcp, help, version, rest };
|
|
1234
|
+
return { verbose, format, formatExplicit, filterOutput, llms, mcp, help, version, schema, rest };
|
|
776
1235
|
}
|
|
777
1236
|
/** @internal Collects immediate child commands/groups for help output. */
|
|
778
1237
|
function collectHelpCommands(commands) {
|
|
779
1238
|
const result = [];
|
|
780
1239
|
for (const [name, entry] of commands) {
|
|
781
|
-
|
|
782
|
-
result.push({ name, description: entry.description });
|
|
783
|
-
else
|
|
784
|
-
result.push({ name, description: entry.description });
|
|
1240
|
+
result.push({ name, description: entry.description });
|
|
785
1241
|
}
|
|
786
1242
|
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
787
1243
|
}
|
|
1244
|
+
/** @internal Formats help text for a fetch gateway command. */
|
|
1245
|
+
function formatFetchHelp(name, description) {
|
|
1246
|
+
const lines = [];
|
|
1247
|
+
if (description)
|
|
1248
|
+
lines.push(`${name} — ${description}`);
|
|
1249
|
+
else
|
|
1250
|
+
lines.push(name);
|
|
1251
|
+
lines.push('');
|
|
1252
|
+
lines.push(`Usage: ${name} <path> [options]`);
|
|
1253
|
+
lines.push('');
|
|
1254
|
+
lines.push('Path segments are joined into the request URL path.');
|
|
1255
|
+
lines.push('');
|
|
1256
|
+
lines.push('Options:');
|
|
1257
|
+
lines.push(' -X, --method <METHOD> HTTP method (default: GET, POST if body present)');
|
|
1258
|
+
lines.push(' -H, --header "Key: Val" Set a request header (repeatable)');
|
|
1259
|
+
lines.push(' -d, --data <json> Request body (implies POST)');
|
|
1260
|
+
lines.push(' --body <json> Request body (implies POST)');
|
|
1261
|
+
lines.push(' --<key> <value> Query string parameter');
|
|
1262
|
+
return lines.join('\n');
|
|
1263
|
+
}
|
|
788
1264
|
/** @internal Type guard for command groups. */
|
|
789
1265
|
function isGroup(entry) {
|
|
790
1266
|
return '_group' in entry;
|
|
791
1267
|
}
|
|
1268
|
+
/** @internal Type guard for fetch gateways. */
|
|
1269
|
+
function isFetchGateway(entry) {
|
|
1270
|
+
return '_fetch' in entry;
|
|
1271
|
+
}
|
|
792
1272
|
/** @internal Maps CLI instances to their command maps. */
|
|
793
1273
|
export const toCommands = new WeakMap();
|
|
794
1274
|
/** @internal Maps CLI instances to their middleware arrays. */
|
|
@@ -866,7 +1346,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
866
1346
|
}));
|
|
867
1347
|
else
|
|
868
1348
|
ctx.writeln(formatHumanError({ code: tagged.code, message: tagged.message }));
|
|
869
|
-
ctx.exit(1);
|
|
1349
|
+
ctx.exit(tagged.exitCode ?? 1);
|
|
870
1350
|
return;
|
|
871
1351
|
}
|
|
872
1352
|
}
|
|
@@ -890,7 +1370,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
890
1370
|
}));
|
|
891
1371
|
else
|
|
892
1372
|
ctx.writeln(formatHumanError({ code: err.code, message: err.message }));
|
|
893
|
-
ctx.exit(1);
|
|
1373
|
+
ctx.exit(err.exitCode ?? 1);
|
|
894
1374
|
return;
|
|
895
1375
|
}
|
|
896
1376
|
const cta = isSentinel(returnValue) && returnValue[sentinel] === 'ok'
|
|
@@ -924,7 +1404,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
924
1404
|
code: 'UNKNOWN',
|
|
925
1405
|
message: error instanceof Error ? error.message : String(error),
|
|
926
1406
|
}));
|
|
927
|
-
ctx.exit(1);
|
|
1407
|
+
ctx.exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
928
1408
|
}
|
|
929
1409
|
}
|
|
930
1410
|
else {
|
|
@@ -953,7 +1433,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
953
1433
|
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
954
1434
|
},
|
|
955
1435
|
});
|
|
956
|
-
ctx.exit(1);
|
|
1436
|
+
ctx.exit(tagged.exitCode ?? 1);
|
|
957
1437
|
return;
|
|
958
1438
|
}
|
|
959
1439
|
}
|
|
@@ -973,7 +1453,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
973
1453
|
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
974
1454
|
},
|
|
975
1455
|
});
|
|
976
|
-
ctx.exit(1);
|
|
1456
|
+
ctx.exit(err.exitCode ?? 1);
|
|
977
1457
|
return;
|
|
978
1458
|
}
|
|
979
1459
|
const cta = isSentinel(returnValue) && returnValue[sentinel] === 'ok'
|
|
@@ -1001,7 +1481,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
1001
1481
|
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1002
1482
|
},
|
|
1003
1483
|
});
|
|
1004
|
-
ctx.exit(1);
|
|
1484
|
+
ctx.exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
1005
1485
|
}
|
|
1006
1486
|
}
|
|
1007
1487
|
}
|
|
@@ -1040,7 +1520,13 @@ function collectCommands(commands, prefix) {
|
|
|
1040
1520
|
const result = [];
|
|
1041
1521
|
for (const [name, entry] of commands) {
|
|
1042
1522
|
const path = [...prefix, name];
|
|
1043
|
-
if (
|
|
1523
|
+
if (isFetchGateway(entry)) {
|
|
1524
|
+
const cmd = { name: path.join(' ') };
|
|
1525
|
+
if (entry.description)
|
|
1526
|
+
cmd.description = entry.description;
|
|
1527
|
+
result.push(cmd);
|
|
1528
|
+
}
|
|
1529
|
+
else if (isGroup(entry)) {
|
|
1044
1530
|
result.push(...collectCommands(entry.commands, path));
|
|
1045
1531
|
}
|
|
1046
1532
|
else {
|
|
@@ -1078,7 +1564,14 @@ function collectSkillCommands(commands, prefix, groups) {
|
|
|
1078
1564
|
const result = [];
|
|
1079
1565
|
for (const [name, entry] of commands) {
|
|
1080
1566
|
const path = [...prefix, name];
|
|
1081
|
-
if (
|
|
1567
|
+
if (isFetchGateway(entry)) {
|
|
1568
|
+
const cmd = { name: path.join(' ') };
|
|
1569
|
+
if (entry.description)
|
|
1570
|
+
cmd.description = entry.description;
|
|
1571
|
+
cmd.hint = 'Fetch gateway. Pass path segments and curl-style flags (-X, -H, -d, --key value).';
|
|
1572
|
+
result.push(cmd);
|
|
1573
|
+
}
|
|
1574
|
+
else if (isGroup(entry)) {
|
|
1082
1575
|
if (entry.description)
|
|
1083
1576
|
groups.set(path.join(' '), entry.description);
|
|
1084
1577
|
result.push(...collectSkillCommands(entry.commands, path, groups));
|