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/dist/Cli.js
CHANGED
|
@@ -5,6 +5,8 @@ import * as Fetch from './Fetch.js';
|
|
|
5
5
|
import * as Filter from './Filter.js';
|
|
6
6
|
import * as Formatter from './Formatter.js';
|
|
7
7
|
import * as Help from './Help.js';
|
|
8
|
+
import { builtinCommands, shells } from './internal/command.js';
|
|
9
|
+
import * as Command from './internal/command.js';
|
|
8
10
|
import { detectRunner } from './internal/pm.js';
|
|
9
11
|
import * as Mcp from './Mcp.js';
|
|
10
12
|
import * as Openapi from './Openapi.js';
|
|
@@ -76,10 +78,13 @@ export function create(nameOrDefinition, definition) {
|
|
|
76
78
|
if (pending.length > 0)
|
|
77
79
|
await Promise.all(pending);
|
|
78
80
|
return fetchImpl(name, commands, req, {
|
|
81
|
+
envSchema: def.env,
|
|
79
82
|
mcpHandler,
|
|
80
83
|
middlewares,
|
|
84
|
+
name,
|
|
81
85
|
rootCommand: rootDef,
|
|
82
86
|
vars: def.vars,
|
|
87
|
+
version: def.version,
|
|
83
88
|
});
|
|
84
89
|
},
|
|
85
90
|
async serve(argv = process.argv.slice(2), serveOptions = {}) {
|
|
@@ -122,7 +127,12 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
122
127
|
const { verbose, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, rest: filtered, } = extractBuiltinFlags(argv);
|
|
123
128
|
// --mcp: start as MCP stdio server
|
|
124
129
|
if (mcpFlag) {
|
|
125
|
-
await Mcp.serve(name, options.version ?? '0.0.0', commands
|
|
130
|
+
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
131
|
+
middlewares: options.middlewares,
|
|
132
|
+
env: options.envSchema,
|
|
133
|
+
vars: options.vars,
|
|
134
|
+
version: options.version,
|
|
135
|
+
});
|
|
126
136
|
return;
|
|
127
137
|
}
|
|
128
138
|
// COMPLETE: dynamic shell completions (called by shell hook at tab-press)
|
|
@@ -139,6 +149,27 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
139
149
|
else {
|
|
140
150
|
const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
|
|
141
151
|
const candidates = Completions.complete(commands, options.rootCommand, words, index);
|
|
152
|
+
// Add built-in commands (completions, mcp, skills) to completions
|
|
153
|
+
const current = words[index] ?? '';
|
|
154
|
+
const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
|
|
155
|
+
if (nonFlags.length <= 1) {
|
|
156
|
+
for (const b of builtinCommands) {
|
|
157
|
+
if (b.name.startsWith(current) && !candidates.some((c) => c.value === b.name))
|
|
158
|
+
candidates.push({
|
|
159
|
+
value: b.name,
|
|
160
|
+
description: b.description,
|
|
161
|
+
...(b.subcommands ? { noSpace: true } : undefined),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (nonFlags.length === 2) {
|
|
166
|
+
const parent = nonFlags[nonFlags.length - 1];
|
|
167
|
+
const builtin = builtinCommands.find((b) => b.name === parent && b.subcommands);
|
|
168
|
+
if (builtin?.subcommands)
|
|
169
|
+
for (const sub of builtin.subcommands)
|
|
170
|
+
if (sub.name.startsWith(current))
|
|
171
|
+
candidates.push({ value: sub.name, description: sub.description });
|
|
172
|
+
}
|
|
142
173
|
const out = Completions.format(completeShell, candidates);
|
|
143
174
|
if (out)
|
|
144
175
|
stdout(out);
|
|
@@ -219,41 +250,23 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
219
250
|
// not a completions invocation
|
|
220
251
|
return -1;
|
|
221
252
|
})();
|
|
253
|
+
// TODO: refactor built-in command handlers (completions, skills, mcp) into a generic dispatch loop on `builtinCommands`
|
|
222
254
|
if (completionsIdx !== -1 && filtered[completionsIdx] === 'completions') {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
' nushell',
|
|
233
|
-
' zsh',
|
|
234
|
-
'',
|
|
235
|
-
'Setup:',
|
|
236
|
-
...(() => {
|
|
237
|
-
const rows = [
|
|
238
|
-
['bash', `eval "$(${name} completions bash)"`, '# add to ~/.bashrc'],
|
|
239
|
-
['zsh', `eval "$(${name} completions zsh)"`, '# add to ~/.zshrc'],
|
|
240
|
-
['fish', `${name} completions fish | source`, '# add to ~/.config/fish/config.fish'],
|
|
241
|
-
['nushell', `see \`${name} completions nushell\``, '# add to config.nu'],
|
|
242
|
-
];
|
|
243
|
-
const shellW = Math.max(...rows.map((r) => r[0].length));
|
|
244
|
-
const cmdW = Math.max(...rows.map((r) => r[1].length));
|
|
245
|
-
return rows.map(([shell, cmd, comment]) => ` ${shell.padEnd(shellW)} ${cmd.padEnd(cmdW)} ${comment}`);
|
|
246
|
-
})(),
|
|
247
|
-
].join('\n'));
|
|
255
|
+
const shell = filtered[completionsIdx + 1];
|
|
256
|
+
if (help || !shell) {
|
|
257
|
+
const b = builtinCommands.find((c) => c.name === 'completions');
|
|
258
|
+
writeln(Help.formatCommand(`${name} completions`, {
|
|
259
|
+
args: b.args,
|
|
260
|
+
description: b.description,
|
|
261
|
+
hideGlobalOptions: true,
|
|
262
|
+
hint: b.hint?.(name),
|
|
263
|
+
}));
|
|
248
264
|
return;
|
|
249
265
|
}
|
|
250
|
-
|
|
251
|
-
if (!shell || !['bash', 'fish', 'nushell', 'zsh'].includes(shell)) {
|
|
266
|
+
if (!shells.includes(shell)) {
|
|
252
267
|
writeln(formatHumanError({
|
|
253
268
|
code: 'INVALID_SHELL',
|
|
254
|
-
message: shell
|
|
255
|
-
? `Unknown shell '${shell}'. Supported: bash, fish, nushell, zsh`
|
|
256
|
-
: `Missing shell argument. Usage: ${name} completions <bash|fish|nushell|zsh>`,
|
|
269
|
+
message: `Unknown shell '${shell}'. Supported: ${shells.join(', ')}`,
|
|
257
270
|
}));
|
|
258
271
|
exit(1);
|
|
259
272
|
return;
|
|
@@ -264,17 +277,15 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
264
277
|
}
|
|
265
278
|
// skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
|
|
266
279
|
const skillsIdx = filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1;
|
|
267
|
-
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills'
|
|
280
|
+
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills') {
|
|
281
|
+
if (filtered[skillsIdx + 1] !== 'add') {
|
|
282
|
+
const b = builtinCommands.find((c) => c.name === 'skills');
|
|
283
|
+
writeln(formatBuiltinHelp(name, b));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
268
286
|
if (help) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
'',
|
|
272
|
-
`Usage: ${name} skills add [options]`,
|
|
273
|
-
'',
|
|
274
|
-
'Options:',
|
|
275
|
-
' --depth <number> Grouping depth for skill files (default: 1)',
|
|
276
|
-
' --no-global Install to project instead of globally',
|
|
277
|
-
].join('\n'));
|
|
287
|
+
const b = builtinCommands.find((c) => c.name === 'skills');
|
|
288
|
+
writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
|
|
278
289
|
return;
|
|
279
290
|
}
|
|
280
291
|
const rest = filtered.slice(skillsIdx + 2);
|
|
@@ -333,18 +344,15 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
333
344
|
}
|
|
334
345
|
// mcp add: register CLI as MCP server via `npx add-mcp`
|
|
335
346
|
const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1;
|
|
336
|
-
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp'
|
|
347
|
+
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp') {
|
|
348
|
+
if (filtered[mcpIdx + 1] !== 'add') {
|
|
349
|
+
const b = builtinCommands.find((c) => c.name === 'mcp');
|
|
350
|
+
writeln(formatBuiltinHelp(name, b));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
337
353
|
if (help) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
'',
|
|
341
|
-
`Usage: ${name} mcp add [options]`,
|
|
342
|
-
'',
|
|
343
|
-
'Options:',
|
|
344
|
-
' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
|
|
345
|
-
' --no-global Install to project instead of globally',
|
|
346
|
-
' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
|
|
347
|
-
].join('\n'));
|
|
354
|
+
const b = builtinCommands.find((c) => c.name === 'mcp');
|
|
355
|
+
writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
|
|
348
356
|
return;
|
|
349
357
|
}
|
|
350
358
|
const rest = filtered.slice(mcpIdx + 2);
|
|
@@ -807,178 +815,86 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
807
815
|
: []),
|
|
808
816
|
...(command.middleware ?? []),
|
|
809
817
|
];
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
818
|
+
if (human)
|
|
819
|
+
emitDeprecationWarnings(rest, command.options, command.alias);
|
|
820
|
+
const result = await Command.execute(command, {
|
|
821
|
+
agent: !human,
|
|
822
|
+
argv: rest,
|
|
823
|
+
env: options.envSchema,
|
|
824
|
+
envSource: options.env,
|
|
825
|
+
format,
|
|
826
|
+
formatExplicit,
|
|
827
|
+
inputOptions: {},
|
|
828
|
+
middlewares: allMiddleware,
|
|
829
|
+
name,
|
|
830
|
+
path,
|
|
831
|
+
vars: options.vars,
|
|
832
|
+
version: options.version,
|
|
833
|
+
});
|
|
834
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
835
|
+
// Streaming path — async generator
|
|
836
|
+
if ('stream' in result) {
|
|
837
|
+
await handleStreaming(result.stream, {
|
|
838
|
+
name,
|
|
839
|
+
path,
|
|
840
|
+
start,
|
|
833
841
|
format,
|
|
834
842
|
formatExplicit,
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
843
|
+
human,
|
|
844
|
+
renderOutput,
|
|
845
|
+
verbose,
|
|
846
|
+
truncate,
|
|
847
|
+
write,
|
|
848
|
+
writeln,
|
|
849
|
+
exit,
|
|
840
850
|
});
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
human,
|
|
850
|
-
renderOutput,
|
|
851
|
-
verbose,
|
|
852
|
-
truncate,
|
|
853
|
-
write,
|
|
854
|
-
writeln,
|
|
855
|
-
exit,
|
|
856
|
-
});
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
const awaited = await result;
|
|
860
|
-
if (isSentinel(awaited)) {
|
|
861
|
-
const cta = formatCtaBlock(name, awaited.cta);
|
|
862
|
-
if (awaited[sentinel] === 'ok') {
|
|
863
|
-
write({
|
|
864
|
-
ok: true,
|
|
865
|
-
data: awaited.data,
|
|
866
|
-
meta: {
|
|
867
|
-
command: path,
|
|
868
|
-
duration: `${Math.round(performance.now() - start)}ms`,
|
|
869
|
-
...(cta ? { cta } : undefined),
|
|
870
|
-
},
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
const err = awaited;
|
|
875
|
-
write({
|
|
876
|
-
ok: false,
|
|
877
|
-
error: {
|
|
878
|
-
code: err.code,
|
|
879
|
-
message: err.message,
|
|
880
|
-
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
881
|
-
},
|
|
882
|
-
meta: {
|
|
883
|
-
command: path,
|
|
884
|
-
duration: `${Math.round(performance.now() - start)}ms`,
|
|
885
|
-
...(cta ? { cta } : undefined),
|
|
886
|
-
},
|
|
887
|
-
});
|
|
888
|
-
exit(err.exitCode ?? 1);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
write({
|
|
893
|
-
ok: true,
|
|
894
|
-
data: awaited,
|
|
895
|
-
meta: {
|
|
896
|
-
command: path,
|
|
897
|
-
duration: `${Math.round(performance.now() - start)}ms`,
|
|
898
|
-
},
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
};
|
|
902
|
-
try {
|
|
903
|
-
const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, envSource) : {};
|
|
904
|
-
if (allMiddleware.length > 0) {
|
|
905
|
-
const errorFn = (opts) => {
|
|
906
|
-
return { [sentinel]: 'error', ...opts };
|
|
907
|
-
};
|
|
908
|
-
const mwCtx = {
|
|
909
|
-
agent: !human,
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (result.ok) {
|
|
854
|
+
const cta = formatCtaBlock(name, result.cta);
|
|
855
|
+
write({
|
|
856
|
+
ok: true,
|
|
857
|
+
data: result.data,
|
|
858
|
+
meta: {
|
|
910
859
|
command: path,
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
name,
|
|
916
|
-
set(key, value) {
|
|
917
|
-
varsMap[key] = value;
|
|
918
|
-
},
|
|
919
|
-
var: varsMap,
|
|
920
|
-
version: options.version,
|
|
921
|
-
};
|
|
922
|
-
const handleMwSentinel = (result) => {
|
|
923
|
-
if (!isSentinel(result) || result[sentinel] !== 'error')
|
|
924
|
-
return;
|
|
925
|
-
const err = result;
|
|
926
|
-
const cta = formatCtaBlock(name, err.cta);
|
|
927
|
-
write({
|
|
928
|
-
ok: false,
|
|
929
|
-
error: {
|
|
930
|
-
code: err.code,
|
|
931
|
-
message: err.message,
|
|
932
|
-
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
933
|
-
},
|
|
934
|
-
meta: {
|
|
935
|
-
command: path,
|
|
936
|
-
duration: `${Math.round(performance.now() - start)}ms`,
|
|
937
|
-
...(cta ? { cta } : undefined),
|
|
938
|
-
},
|
|
939
|
-
});
|
|
940
|
-
exit(err.exitCode ?? 1);
|
|
941
|
-
};
|
|
942
|
-
const composed = allMiddleware.reduceRight((next, mw) => async () => {
|
|
943
|
-
handleMwSentinel(await mw(mwCtx, next));
|
|
944
|
-
}, runCommand);
|
|
945
|
-
await composed();
|
|
946
|
-
}
|
|
947
|
-
else {
|
|
948
|
-
await runCommand();
|
|
949
|
-
}
|
|
860
|
+
duration,
|
|
861
|
+
...(cta ? { cta } : undefined),
|
|
862
|
+
},
|
|
863
|
+
});
|
|
950
864
|
}
|
|
951
|
-
|
|
952
|
-
const
|
|
865
|
+
else {
|
|
866
|
+
const cta = formatCtaBlock(name, result.cta);
|
|
867
|
+
if (human && !formatExplicit && result.error.fieldErrors) {
|
|
868
|
+
writeln(formatHumanValidationError(name, path, command, new ValidationError({
|
|
869
|
+
message: result.error.message,
|
|
870
|
+
fieldErrors: result.error.fieldErrors,
|
|
871
|
+
}), options.env));
|
|
872
|
+
exit(1);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
write({
|
|
953
876
|
ok: false,
|
|
954
877
|
error: {
|
|
955
|
-
code: error
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
962
|
-
...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
|
|
878
|
+
code: result.error.code,
|
|
879
|
+
message: result.error.message,
|
|
880
|
+
...(result.error.retryable !== undefined
|
|
881
|
+
? { retryable: result.error.retryable }
|
|
882
|
+
: undefined),
|
|
883
|
+
...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
|
|
963
884
|
},
|
|
964
885
|
meta: {
|
|
965
886
|
command: path,
|
|
966
|
-
duration
|
|
887
|
+
duration,
|
|
888
|
+
...(cta ? { cta } : undefined),
|
|
967
889
|
},
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
writeln(formatHumanValidationError(name, path, command, error, options.env));
|
|
971
|
-
exit(1);
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
write(errorOutput);
|
|
975
|
-
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
890
|
+
});
|
|
891
|
+
exit(result.exitCode ?? 1);
|
|
976
892
|
}
|
|
977
893
|
}
|
|
978
894
|
/** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
|
|
979
895
|
function createMcpHttpHandler(name, version) {
|
|
980
896
|
let transport;
|
|
981
|
-
return async (req, commands) => {
|
|
897
|
+
return async (req, commands, mcpOptions) => {
|
|
982
898
|
if (!transport) {
|
|
983
899
|
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
984
900
|
const { WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
|
|
@@ -994,7 +910,13 @@ function createMcpHttpHandler(name, version) {
|
|
|
994
910
|
...(hasInput ? { inputSchema: mergedShape } : undefined),
|
|
995
911
|
}, async (...callArgs) => {
|
|
996
912
|
const params = hasInput ? callArgs[0] : {};
|
|
997
|
-
return Mcp.callTool(tool, params
|
|
913
|
+
return Mcp.callTool(tool, params, {
|
|
914
|
+
name,
|
|
915
|
+
version,
|
|
916
|
+
middlewares: mcpOptions?.middlewares,
|
|
917
|
+
env: mcpOptions?.env,
|
|
918
|
+
vars: mcpOptions?.vars,
|
|
919
|
+
});
|
|
998
920
|
});
|
|
999
921
|
}
|
|
1000
922
|
transport = new WebStandardStreamableHTTPServerTransport({
|
|
@@ -1013,7 +935,11 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1013
935
|
const segments = url.pathname.split('/').filter(Boolean);
|
|
1014
936
|
// MCP over HTTP: route /mcp to the MCP transport
|
|
1015
937
|
if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
|
|
1016
|
-
return options.mcpHandler(req, commands
|
|
938
|
+
return options.mcpHandler(req, commands, {
|
|
939
|
+
middlewares: options.middlewares,
|
|
940
|
+
env: options.envSchema,
|
|
941
|
+
vars: options.vars,
|
|
942
|
+
});
|
|
1017
943
|
// .well-known/skills/ — Agent Skills Discovery (RFC)
|
|
1018
944
|
if (segments[0] === '.well-known' &&
|
|
1019
945
|
segments[1] === 'skills' &&
|
|
@@ -1103,7 +1029,11 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1103
1029
|
if ('fetchGateway' in resolved)
|
|
1104
1030
|
return resolved.fetchGateway.fetch(req);
|
|
1105
1031
|
const { command, path, rest } = resolved;
|
|
1106
|
-
|
|
1032
|
+
const groupMiddlewares = 'middlewares' in resolved ? resolved.middlewares : [];
|
|
1033
|
+
return executeCommand(path, command, rest, inputOptions, start, {
|
|
1034
|
+
...options,
|
|
1035
|
+
groupMiddlewares,
|
|
1036
|
+
});
|
|
1107
1037
|
}
|
|
1108
1038
|
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
1109
1039
|
async function executeCommand(path, command, rest, inputOptions, start, options) {
|
|
@@ -1113,153 +1043,87 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1113
1043
|
headers: { 'content-type': 'application/json' },
|
|
1114
1044
|
});
|
|
1115
1045
|
}
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
while (true) {
|
|
1145
|
-
const { value, done } = await result.next();
|
|
1146
|
-
if (done) {
|
|
1147
|
-
returnValue = value;
|
|
1148
|
-
break;
|
|
1149
|
-
}
|
|
1150
|
-
if (isSentinel(value) && value[sentinel] === 'error') {
|
|
1151
|
-
const tagged = value;
|
|
1152
|
-
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1153
|
-
type: 'error',
|
|
1154
|
-
ok: false,
|
|
1155
|
-
error: { code: tagged.code, message: tagged.message },
|
|
1156
|
-
}) + '\n'));
|
|
1157
|
-
controller.close();
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
|
|
1161
|
-
}
|
|
1162
|
-
const meta = { command: path };
|
|
1163
|
-
if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
|
|
1164
|
-
const tagged = returnValue;
|
|
1165
|
-
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1166
|
-
type: 'error',
|
|
1167
|
-
ok: false,
|
|
1168
|
-
error: { code: tagged.code, message: tagged.message },
|
|
1169
|
-
}) + '\n'));
|
|
1170
|
-
}
|
|
1171
|
-
else {
|
|
1172
|
-
controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'));
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
catch (error) {
|
|
1176
|
-
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1177
|
-
type: 'error',
|
|
1178
|
-
ok: false,
|
|
1179
|
-
error: {
|
|
1180
|
-
code: 'UNKNOWN',
|
|
1181
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1182
|
-
},
|
|
1183
|
-
}) + '\n'));
|
|
1046
|
+
const allMiddleware = [
|
|
1047
|
+
...(options.middlewares ?? []),
|
|
1048
|
+
...(options.groupMiddlewares ?? []),
|
|
1049
|
+
...(command.middleware ?? []),
|
|
1050
|
+
];
|
|
1051
|
+
const result = await Command.execute(command, {
|
|
1052
|
+
agent: true,
|
|
1053
|
+
argv: rest,
|
|
1054
|
+
env: options.envSchema,
|
|
1055
|
+
format: 'json',
|
|
1056
|
+
formatExplicit: true,
|
|
1057
|
+
inputOptions,
|
|
1058
|
+
middlewares: allMiddleware,
|
|
1059
|
+
name: options.name ?? path,
|
|
1060
|
+
parseMode: 'split',
|
|
1061
|
+
path,
|
|
1062
|
+
vars: options.vars,
|
|
1063
|
+
version: options.version,
|
|
1064
|
+
});
|
|
1065
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1066
|
+
// Streaming path — async generator → NDJSON response
|
|
1067
|
+
if ('stream' in result) {
|
|
1068
|
+
const stream = new ReadableStream({
|
|
1069
|
+
async start(controller) {
|
|
1070
|
+
const encoder = new TextEncoder();
|
|
1071
|
+
try {
|
|
1072
|
+
for await (const value of result.stream) {
|
|
1073
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
|
|
1184
1074
|
}
|
|
1185
|
-
controller.
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200);
|
|
1209
|
-
};
|
|
1210
|
-
try {
|
|
1211
|
-
const allMiddleware = options.middlewares ?? [];
|
|
1212
|
-
if (allMiddleware.length > 0) {
|
|
1213
|
-
const errorFn = (opts) => {
|
|
1214
|
-
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1215
|
-
response = jsonResponse({
|
|
1216
|
-
ok: false,
|
|
1217
|
-
error: { code: opts.code, message: opts.message },
|
|
1218
|
-
meta: { command: path, duration },
|
|
1219
|
-
}, 500);
|
|
1220
|
-
return undefined;
|
|
1221
|
-
};
|
|
1222
|
-
const mwCtx = {
|
|
1223
|
-
agent: true,
|
|
1224
|
-
command: path,
|
|
1225
|
-
env: {},
|
|
1226
|
-
error: errorFn,
|
|
1227
|
-
format: 'json',
|
|
1228
|
-
formatExplicit: true,
|
|
1229
|
-
name: path,
|
|
1230
|
-
set(key, value) {
|
|
1231
|
-
varsMap[key] = value;
|
|
1232
|
-
},
|
|
1233
|
-
var: varsMap,
|
|
1234
|
-
version: undefined,
|
|
1235
|
-
};
|
|
1236
|
-
const composed = allMiddleware.reduceRight((next, mw) => async () => {
|
|
1237
|
-
await mw(mwCtx, next);
|
|
1238
|
-
}, runCommand);
|
|
1239
|
-
await composed();
|
|
1240
|
-
}
|
|
1241
|
-
else {
|
|
1242
|
-
await runCommand();
|
|
1243
|
-
}
|
|
1075
|
+
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1076
|
+
type: 'done',
|
|
1077
|
+
ok: true,
|
|
1078
|
+
meta: { command: path },
|
|
1079
|
+
}) + '\n'));
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1083
|
+
type: 'error',
|
|
1084
|
+
ok: false,
|
|
1085
|
+
error: {
|
|
1086
|
+
code: 'UNKNOWN',
|
|
1087
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1088
|
+
},
|
|
1089
|
+
}) + '\n'));
|
|
1090
|
+
}
|
|
1091
|
+
controller.close();
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
return new Response(stream, {
|
|
1095
|
+
status: 200,
|
|
1096
|
+
headers: { 'content-type': 'application/x-ndjson' },
|
|
1097
|
+
});
|
|
1244
1098
|
}
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
if (error instanceof ValidationError)
|
|
1248
|
-
return jsonResponse({
|
|
1249
|
-
ok: false,
|
|
1250
|
-
error: { code: 'VALIDATION_ERROR', message: error.message },
|
|
1251
|
-
meta: { command: path, duration },
|
|
1252
|
-
}, 400);
|
|
1099
|
+
if (!result.ok) {
|
|
1100
|
+
const cta = formatCtaBlock(options.name ?? path, result.cta);
|
|
1253
1101
|
return jsonResponse({
|
|
1254
1102
|
ok: false,
|
|
1255
1103
|
error: {
|
|
1256
|
-
code: error
|
|
1257
|
-
message: error
|
|
1104
|
+
code: result.error.code,
|
|
1105
|
+
message: result.error.message,
|
|
1106
|
+
...(result.error.retryable !== undefined
|
|
1107
|
+
? { retryable: result.error.retryable }
|
|
1108
|
+
: undefined),
|
|
1258
1109
|
},
|
|
1259
|
-
meta: {
|
|
1260
|
-
|
|
1110
|
+
meta: {
|
|
1111
|
+
command: path,
|
|
1112
|
+
duration,
|
|
1113
|
+
...(cta ? { cta } : undefined),
|
|
1114
|
+
},
|
|
1115
|
+
}, result.error.code === 'VALIDATION_ERROR' ? 400 : 500);
|
|
1261
1116
|
}
|
|
1262
|
-
|
|
1117
|
+
const cta = formatCtaBlock(options.name ?? path, result.cta);
|
|
1118
|
+
return jsonResponse({
|
|
1119
|
+
ok: true,
|
|
1120
|
+
data: result.data,
|
|
1121
|
+
meta: {
|
|
1122
|
+
command: path,
|
|
1123
|
+
duration,
|
|
1124
|
+
...(cta ? { cta } : undefined),
|
|
1125
|
+
},
|
|
1126
|
+
}, 200);
|
|
1263
1127
|
}
|
|
1264
1128
|
/** @internal Formats a validation error for TTY with usage hint. */
|
|
1265
1129
|
function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
@@ -1425,6 +1289,23 @@ function collectHelpCommands(commands) {
|
|
|
1425
1289
|
}
|
|
1426
1290
|
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
1427
1291
|
}
|
|
1292
|
+
/** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
|
|
1293
|
+
function formatBuiltinHelp(cli, builtin) {
|
|
1294
|
+
return Help.formatRoot(`${cli} ${builtin.name}`, {
|
|
1295
|
+
description: builtin.description,
|
|
1296
|
+
commands: builtin.subcommands?.map((s) => ({ name: s.name, description: s.description })),
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
/** @internal Formats subcommand-level help for a built-in command (e.g. `cli skills add --help`). */
|
|
1300
|
+
function formatBuiltinSubcommandHelp(cli, builtin, subName) {
|
|
1301
|
+
const sub = builtin.subcommands?.find((s) => s.name === subName);
|
|
1302
|
+
return Help.formatCommand(`${cli} ${builtin.name} ${subName}`, {
|
|
1303
|
+
alias: sub?.alias,
|
|
1304
|
+
description: sub?.description,
|
|
1305
|
+
hideGlobalOptions: true,
|
|
1306
|
+
options: sub?.options,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1428
1309
|
/** @internal Formats help text for a fetch gateway command. */
|
|
1429
1310
|
function formatFetchHelp(name, description) {
|
|
1430
1311
|
const lines = [];
|
|
@@ -1477,8 +1358,9 @@ function formatHumanError(error) {
|
|
|
1477
1358
|
/** @internal Formats a CTA block for human-readable TTY output. */
|
|
1478
1359
|
function formatHumanCta(cta) {
|
|
1479
1360
|
const lines = ['', cta.description];
|
|
1361
|
+
const maxLen = Math.max(...cta.commands.map((c) => c.command.length));
|
|
1480
1362
|
for (const c of cta.commands) {
|
|
1481
|
-
const desc = c.description ? ` # ${c.description}` : '';
|
|
1363
|
+
const desc = c.description ? ` ${''.padEnd(maxLen - c.command.length)}# ${c.description}` : '';
|
|
1482
1364
|
lines.push(` ${c.command}${desc}`);
|
|
1483
1365
|
}
|
|
1484
1366
|
return lines.join('\n');
|
|
@@ -1490,13 +1372,6 @@ function hasRequiredArgs(args) {
|
|
|
1490
1372
|
function isSentinel(value) {
|
|
1491
1373
|
return typeof value === 'object' && value !== null && sentinel in value;
|
|
1492
1374
|
}
|
|
1493
|
-
/** @internal Type guard for async generators returned by streaming `run` handlers. */
|
|
1494
|
-
function isAsyncGenerator(value) {
|
|
1495
|
-
return (typeof value === 'object' &&
|
|
1496
|
-
value !== null &&
|
|
1497
|
-
Symbol.asyncIterator in value &&
|
|
1498
|
-
typeof value.next === 'function');
|
|
1499
|
-
}
|
|
1500
1375
|
/** @internal Handles streaming output from an async generator `run` handler. */
|
|
1501
1376
|
async function handleStreaming(generator, ctx) {
|
|
1502
1377
|
// Incremental: no explicit format (default toon), or explicit jsonl
|