incur 0.3.4 → 0.3.6
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 +62 -1
- package/dist/Cli.d.ts +17 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +435 -365
- 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/Filter.js +0 -18
- package/dist/Filter.js.map +1 -1
- package/dist/Help.d.ts +6 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +35 -22
- 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/Parser.d.ts +2 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +69 -37
- package/dist/Parser.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/bin.d.ts +1 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +17 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/command.d.ts +118 -0
- package/dist/internal/command.d.ts.map +1 -0
- package/dist/internal/command.js +276 -0
- package/dist/internal/command.js.map +1 -0
- package/dist/internal/configSchema.d.ts +8 -0
- package/dist/internal/configSchema.d.ts.map +1 -0
- package/dist/internal/configSchema.js +57 -0
- package/dist/internal/configSchema.js.map +1 -0
- package/dist/internal/helpers.d.ts +5 -0
- package/dist/internal/helpers.d.ts.map +1 -0
- package/dist/internal/helpers.js +9 -0
- package/dist/internal/helpers.js.map +1 -0
- package/examples/npm/.npmrc.json +21 -0
- package/examples/npm/config.schema.json +137 -0
- package/package.json +1 -1
- package/src/Cli.test-d.ts +39 -0
- package/src/Cli.test.ts +704 -6
- package/src/Cli.ts +551 -448
- package/src/Completions.test.ts +35 -9
- package/src/Completions.ts +1 -2
- package/src/Filter.ts +0 -17
- package/src/Help.test.ts +77 -0
- package/src/Help.ts +39 -21
- package/src/Mcp.test.ts +143 -0
- package/src/Mcp.ts +92 -84
- package/src/Parser.test-d.ts +22 -0
- package/src/Parser.test.ts +89 -0
- package/src/Parser.ts +86 -35
- package/src/Skill.ts +5 -1
- package/src/SyncSkills.ts +11 -1
- package/src/bin.ts +21 -2
- package/src/e2e.test.ts +30 -17
- package/src/internal/command.ts +428 -0
- package/src/internal/configSchema.test.ts +193 -0
- package/src/internal/configSchema.ts +66 -0
- package/src/internal/helpers.ts +9 -0
package/dist/Cli.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
1
4
|
import { estimateTokenCount, sliceByTokens } from 'tokenx';
|
|
2
5
|
import * as Completions from './Completions.js';
|
|
3
|
-
import { IncurError, ValidationError } from './Errors.js';
|
|
6
|
+
import { IncurError, ParseError, ValidationError } from './Errors.js';
|
|
4
7
|
import * as Fetch from './Fetch.js';
|
|
5
8
|
import * as Filter from './Filter.js';
|
|
6
9
|
import * as Formatter from './Formatter.js';
|
|
7
10
|
import * as Help from './Help.js';
|
|
11
|
+
import { builtinCommands, shells } from './internal/command.js';
|
|
12
|
+
import * as Command from './internal/command.js';
|
|
13
|
+
import { isRecord } from './internal/helpers.js';
|
|
8
14
|
import { detectRunner } from './internal/pm.js';
|
|
9
15
|
import * as Mcp from './Mcp.js';
|
|
10
16
|
import * as Openapi from './Openapi.js';
|
|
@@ -76,10 +82,13 @@ export function create(nameOrDefinition, definition) {
|
|
|
76
82
|
if (pending.length > 0)
|
|
77
83
|
await Promise.all(pending);
|
|
78
84
|
return fetchImpl(name, commands, req, {
|
|
85
|
+
envSchema: def.env,
|
|
79
86
|
mcpHandler,
|
|
80
87
|
middlewares,
|
|
88
|
+
name,
|
|
81
89
|
rootCommand: rootDef,
|
|
82
90
|
vars: def.vars,
|
|
91
|
+
version: def.version,
|
|
83
92
|
});
|
|
84
93
|
},
|
|
85
94
|
async serve(argv = process.argv.slice(2), serveOptions = {}) {
|
|
@@ -88,6 +97,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
88
97
|
return serveImpl(name, commands, argv, {
|
|
89
98
|
...serveOptions,
|
|
90
99
|
aliases: def.aliases,
|
|
100
|
+
config: def.config,
|
|
91
101
|
description: def.description,
|
|
92
102
|
envSchema: def.env,
|
|
93
103
|
format: def.format,
|
|
@@ -108,6 +118,10 @@ export function create(nameOrDefinition, definition) {
|
|
|
108
118
|
};
|
|
109
119
|
if (rootDef)
|
|
110
120
|
toRootDefinition.set(cli, rootDef);
|
|
121
|
+
if (def.options)
|
|
122
|
+
toRootOptions.set(cli, def.options);
|
|
123
|
+
if (def.config !== undefined)
|
|
124
|
+
toConfigEnabled.set(cli, true);
|
|
111
125
|
if (def.outputPolicy)
|
|
112
126
|
toOutputPolicy.set(cli, def.outputPolicy);
|
|
113
127
|
toMiddlewares.set(cli, middlewares);
|
|
@@ -119,10 +133,34 @@ export function create(nameOrDefinition, definition) {
|
|
|
119
133
|
async function serveImpl(name, commands, argv, options = {}) {
|
|
120
134
|
const stdout = options.stdout ?? ((s) => process.stdout.write(s));
|
|
121
135
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
122
|
-
const
|
|
136
|
+
const human = process.stdout.isTTY === true;
|
|
137
|
+
const configEnabled = options.config !== undefined;
|
|
138
|
+
const configFlag = options.config?.flag;
|
|
139
|
+
function writeln(s) {
|
|
140
|
+
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
141
|
+
}
|
|
142
|
+
let builtinFlags;
|
|
143
|
+
try {
|
|
144
|
+
builtinFlags = extractBuiltinFlags(argv, { configFlag });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
if (human)
|
|
149
|
+
writeln(formatHumanError({ code: 'UNKNOWN', message }));
|
|
150
|
+
else
|
|
151
|
+
writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'));
|
|
152
|
+
exit(1);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const { verbose, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest: filtered, } = builtinFlags;
|
|
123
156
|
// --mcp: start as MCP stdio server
|
|
124
157
|
if (mcpFlag) {
|
|
125
|
-
await Mcp.serve(name, options.version ?? '0.0.0', commands
|
|
158
|
+
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
159
|
+
middlewares: options.middlewares,
|
|
160
|
+
env: options.envSchema,
|
|
161
|
+
vars: options.vars,
|
|
162
|
+
version: options.version,
|
|
163
|
+
});
|
|
126
164
|
return;
|
|
127
165
|
}
|
|
128
166
|
// COMPLETE: dynamic shell completions (called by shell hook at tab-press)
|
|
@@ -139,17 +177,33 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
139
177
|
else {
|
|
140
178
|
const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
|
|
141
179
|
const candidates = Completions.complete(commands, options.rootCommand, words, index);
|
|
180
|
+
// Add built-in commands (completions, mcp, skills) to completions
|
|
181
|
+
const current = words[index] ?? '';
|
|
182
|
+
const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
|
|
183
|
+
if (nonFlags.length <= 1) {
|
|
184
|
+
for (const b of builtinCommands) {
|
|
185
|
+
if (b.name.startsWith(current) && !candidates.some((c) => c.value === b.name))
|
|
186
|
+
candidates.push({
|
|
187
|
+
value: b.name,
|
|
188
|
+
description: b.description,
|
|
189
|
+
...(b.subcommands ? { noSpace: true } : undefined),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (nonFlags.length === 2) {
|
|
194
|
+
const parent = nonFlags[nonFlags.length - 1];
|
|
195
|
+
const builtin = builtinCommands.find((b) => b.name === parent && b.subcommands);
|
|
196
|
+
if (builtin?.subcommands)
|
|
197
|
+
for (const sub of builtin.subcommands)
|
|
198
|
+
if (sub.name.startsWith(current))
|
|
199
|
+
candidates.push({ value: sub.name, description: sub.description });
|
|
200
|
+
}
|
|
142
201
|
const out = Completions.format(completeShell, candidates);
|
|
143
202
|
if (out)
|
|
144
203
|
stdout(out);
|
|
145
204
|
}
|
|
146
205
|
return;
|
|
147
206
|
}
|
|
148
|
-
// Human mode: stdout is a TTY.
|
|
149
|
-
const human = process.stdout.isTTY === true;
|
|
150
|
-
function writeln(s) {
|
|
151
|
-
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
152
|
-
}
|
|
153
207
|
// Skills staleness check (skip for built-in commands)
|
|
154
208
|
if (!llms && !llmsFull && !schema && !help && !version) {
|
|
155
209
|
const isSkillsAdd = filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills');
|
|
@@ -219,41 +273,23 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
219
273
|
// not a completions invocation
|
|
220
274
|
return -1;
|
|
221
275
|
})();
|
|
276
|
+
// TODO: refactor built-in command handlers (completions, skills, mcp) into a generic dispatch loop on `builtinCommands`
|
|
222
277
|
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'));
|
|
278
|
+
const shell = filtered[completionsIdx + 1];
|
|
279
|
+
if (help || !shell) {
|
|
280
|
+
const b = builtinCommands.find((c) => c.name === 'completions');
|
|
281
|
+
writeln(Help.formatCommand(`${name} completions`, {
|
|
282
|
+
args: b.args,
|
|
283
|
+
description: b.description,
|
|
284
|
+
hideGlobalOptions: true,
|
|
285
|
+
hint: b.hint?.(name),
|
|
286
|
+
}));
|
|
248
287
|
return;
|
|
249
288
|
}
|
|
250
|
-
|
|
251
|
-
if (!shell || !['bash', 'fish', 'nushell', 'zsh'].includes(shell)) {
|
|
289
|
+
if (!shells.includes(shell)) {
|
|
252
290
|
writeln(formatHumanError({
|
|
253
291
|
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>`,
|
|
292
|
+
message: `Unknown shell '${shell}'. Supported: ${shells.join(', ')}`,
|
|
257
293
|
}));
|
|
258
294
|
exit(1);
|
|
259
295
|
return;
|
|
@@ -264,17 +300,15 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
264
300
|
}
|
|
265
301
|
// skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
|
|
266
302
|
const skillsIdx = filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1;
|
|
267
|
-
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills'
|
|
303
|
+
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills') {
|
|
304
|
+
if (filtered[skillsIdx + 1] !== 'add') {
|
|
305
|
+
const b = builtinCommands.find((c) => c.name === 'skills');
|
|
306
|
+
writeln(formatBuiltinHelp(name, b));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
268
309
|
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'));
|
|
310
|
+
const b = builtinCommands.find((c) => c.name === 'skills');
|
|
311
|
+
writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
|
|
278
312
|
return;
|
|
279
313
|
}
|
|
280
314
|
const rest = filtered.slice(skillsIdx + 2);
|
|
@@ -333,18 +367,15 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
333
367
|
}
|
|
334
368
|
// mcp add: register CLI as MCP server via `npx add-mcp`
|
|
335
369
|
const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1;
|
|
336
|
-
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp'
|
|
370
|
+
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp') {
|
|
371
|
+
if (filtered[mcpIdx + 1] !== 'add') {
|
|
372
|
+
const b = builtinCommands.find((c) => c.name === 'mcp');
|
|
373
|
+
writeln(formatBuiltinHelp(name, b));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
337
376
|
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'));
|
|
377
|
+
const b = builtinCommands.find((c) => c.name === 'mcp');
|
|
378
|
+
writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
|
|
348
379
|
return;
|
|
349
380
|
}
|
|
350
381
|
const rest = filtered.slice(mcpIdx + 2);
|
|
@@ -404,6 +435,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
404
435
|
writeln(Help.formatCommand(name, {
|
|
405
436
|
alias: cmd.alias,
|
|
406
437
|
aliases: options.aliases,
|
|
438
|
+
configFlag,
|
|
407
439
|
description: cmd.description ?? options.description,
|
|
408
440
|
version: options.version,
|
|
409
441
|
args: cmd.args,
|
|
@@ -424,6 +456,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
424
456
|
else {
|
|
425
457
|
writeln(Help.formatRoot(name, {
|
|
426
458
|
aliases: options.aliases,
|
|
459
|
+
configFlag,
|
|
427
460
|
description: options.description,
|
|
428
461
|
version: options.version,
|
|
429
462
|
commands: collectHelpCommands(commands),
|
|
@@ -466,6 +499,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
466
499
|
writeln(Help.formatCommand(name, {
|
|
467
500
|
alias: cmd.alias,
|
|
468
501
|
aliases: options.aliases,
|
|
502
|
+
configFlag,
|
|
469
503
|
description: cmd.description ?? options.description,
|
|
470
504
|
version: options.version,
|
|
471
505
|
args: cmd.args,
|
|
@@ -482,6 +516,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
482
516
|
else {
|
|
483
517
|
writeln(Help.formatRoot(helpName, {
|
|
484
518
|
aliases: isRoot ? options.aliases : undefined,
|
|
519
|
+
configFlag,
|
|
485
520
|
description: helpDesc,
|
|
486
521
|
version: isRoot ? options.version : undefined,
|
|
487
522
|
commands: collectHelpCommands(helpCmds),
|
|
@@ -499,6 +534,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
499
534
|
writeln(Help.formatCommand(commandName, {
|
|
500
535
|
alias: cmd.alias,
|
|
501
536
|
aliases: isRootCmd ? options.aliases : undefined,
|
|
537
|
+
configFlag,
|
|
502
538
|
description: cmd.description,
|
|
503
539
|
version: isRootCmd ? options.version : undefined,
|
|
504
540
|
args: cmd.args,
|
|
@@ -518,6 +554,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
518
554
|
if (schema) {
|
|
519
555
|
if ('help' in resolved) {
|
|
520
556
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
557
|
+
configFlag,
|
|
521
558
|
description: resolved.description,
|
|
522
559
|
commands: collectHelpCommands(resolved.commands),
|
|
523
560
|
}));
|
|
@@ -550,6 +587,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
550
587
|
}
|
|
551
588
|
if ('help' in resolved) {
|
|
552
589
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
590
|
+
configFlag,
|
|
553
591
|
description: resolved.description,
|
|
554
592
|
commands: collectHelpCommands(resolved.commands),
|
|
555
593
|
}));
|
|
@@ -807,178 +845,110 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
807
845
|
: []),
|
|
808
846
|
...(command.middleware ?? []),
|
|
809
847
|
];
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
emitDeprecationWarnings(rest, command.options, command.alias);
|
|
821
|
-
const env = command.env ? Parser.parseEnv(command.env, envSource) : {};
|
|
822
|
-
const okFn = (data, meta = {}) => {
|
|
823
|
-
return { [sentinel]: 'ok', data, cta: meta.cta };
|
|
824
|
-
};
|
|
825
|
-
const errorFn = (opts) => {
|
|
826
|
-
return { [sentinel]: 'error', ...opts };
|
|
827
|
-
};
|
|
828
|
-
const result = command.run({
|
|
829
|
-
agent: !human,
|
|
830
|
-
args,
|
|
831
|
-
env,
|
|
832
|
-
error: errorFn,
|
|
833
|
-
format,
|
|
834
|
-
formatExplicit,
|
|
835
|
-
name,
|
|
836
|
-
ok: okFn,
|
|
837
|
-
options: parsedOptions,
|
|
838
|
-
var: varsMap,
|
|
839
|
-
version: options.version,
|
|
840
|
-
});
|
|
841
|
-
// Streaming path — async generator
|
|
842
|
-
if (isAsyncGenerator(result)) {
|
|
843
|
-
await handleStreaming(result, {
|
|
844
|
-
name,
|
|
845
|
-
path,
|
|
846
|
-
start,
|
|
847
|
-
format,
|
|
848
|
-
formatExplicit,
|
|
849
|
-
human,
|
|
850
|
-
renderOutput,
|
|
851
|
-
verbose,
|
|
852
|
-
truncate,
|
|
853
|
-
write,
|
|
854
|
-
writeln,
|
|
855
|
-
exit,
|
|
848
|
+
if (human)
|
|
849
|
+
emitDeprecationWarnings(rest, command.options, command.alias);
|
|
850
|
+
let defaults;
|
|
851
|
+
if (configEnabled) {
|
|
852
|
+
try {
|
|
853
|
+
defaults = await loadCommandOptionDefaults(name, path, {
|
|
854
|
+
configDisabled,
|
|
855
|
+
configPath,
|
|
856
|
+
files: options.config?.files,
|
|
857
|
+
loader: options.config?.loader,
|
|
856
858
|
});
|
|
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
859
|
}
|
|
891
|
-
|
|
860
|
+
catch (error) {
|
|
892
861
|
write({
|
|
893
|
-
ok:
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
duration: `${Math.round(performance.now() - start)}ms`,
|
|
862
|
+
ok: false,
|
|
863
|
+
error: {
|
|
864
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
865
|
+
message: error instanceof Error ? error.message : String(error),
|
|
898
866
|
},
|
|
867
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
899
868
|
});
|
|
869
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
870
|
+
return;
|
|
900
871
|
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
872
|
+
}
|
|
873
|
+
const result = await Command.execute(command, {
|
|
874
|
+
agent: !human,
|
|
875
|
+
argv: rest,
|
|
876
|
+
defaults,
|
|
877
|
+
env: options.envSchema,
|
|
878
|
+
envSource: options.env,
|
|
879
|
+
format,
|
|
880
|
+
formatExplicit,
|
|
881
|
+
inputOptions: {},
|
|
882
|
+
middlewares: allMiddleware,
|
|
883
|
+
name,
|
|
884
|
+
path,
|
|
885
|
+
vars: options.vars,
|
|
886
|
+
version: options.version,
|
|
887
|
+
});
|
|
888
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
889
|
+
// Streaming path — async generator
|
|
890
|
+
if ('stream' in result) {
|
|
891
|
+
await handleStreaming(result.stream, {
|
|
892
|
+
name,
|
|
893
|
+
path,
|
|
894
|
+
start,
|
|
895
|
+
format,
|
|
896
|
+
formatExplicit,
|
|
897
|
+
human,
|
|
898
|
+
renderOutput,
|
|
899
|
+
verbose,
|
|
900
|
+
truncate,
|
|
901
|
+
write,
|
|
902
|
+
writeln,
|
|
903
|
+
exit,
|
|
904
|
+
});
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (result.ok) {
|
|
908
|
+
const cta = formatCtaBlock(name, result.cta);
|
|
909
|
+
write({
|
|
910
|
+
ok: true,
|
|
911
|
+
data: result.data,
|
|
912
|
+
meta: {
|
|
910
913
|
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
|
-
}
|
|
914
|
+
duration,
|
|
915
|
+
...(cta ? { cta } : undefined),
|
|
916
|
+
},
|
|
917
|
+
});
|
|
950
918
|
}
|
|
951
|
-
|
|
952
|
-
const
|
|
919
|
+
else {
|
|
920
|
+
const cta = formatCtaBlock(name, result.cta);
|
|
921
|
+
if (human && !formatExplicit && result.error.fieldErrors) {
|
|
922
|
+
writeln(formatHumanValidationError(name, path, command, new ValidationError({
|
|
923
|
+
message: result.error.message,
|
|
924
|
+
fieldErrors: result.error.fieldErrors,
|
|
925
|
+
}), options.env, configFlag));
|
|
926
|
+
exit(1);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
write({
|
|
953
930
|
ok: false,
|
|
954
931
|
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),
|
|
932
|
+
code: result.error.code,
|
|
933
|
+
message: result.error.message,
|
|
934
|
+
...(result.error.retryable !== undefined
|
|
935
|
+
? { retryable: result.error.retryable }
|
|
936
|
+
: undefined),
|
|
937
|
+
...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
|
|
963
938
|
},
|
|
964
939
|
meta: {
|
|
965
940
|
command: path,
|
|
966
|
-
duration
|
|
941
|
+
duration,
|
|
942
|
+
...(cta ? { cta } : undefined),
|
|
967
943
|
},
|
|
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);
|
|
944
|
+
});
|
|
945
|
+
exit(result.exitCode ?? 1);
|
|
976
946
|
}
|
|
977
947
|
}
|
|
978
948
|
/** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
|
|
979
949
|
function createMcpHttpHandler(name, version) {
|
|
980
950
|
let transport;
|
|
981
|
-
return async (req, commands) => {
|
|
951
|
+
return async (req, commands, mcpOptions) => {
|
|
982
952
|
if (!transport) {
|
|
983
953
|
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
984
954
|
const { WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
|
|
@@ -994,7 +964,13 @@ function createMcpHttpHandler(name, version) {
|
|
|
994
964
|
...(hasInput ? { inputSchema: mergedShape } : undefined),
|
|
995
965
|
}, async (...callArgs) => {
|
|
996
966
|
const params = hasInput ? callArgs[0] : {};
|
|
997
|
-
return Mcp.callTool(tool, params
|
|
967
|
+
return Mcp.callTool(tool, params, {
|
|
968
|
+
name,
|
|
969
|
+
version,
|
|
970
|
+
middlewares: mcpOptions?.middlewares,
|
|
971
|
+
env: mcpOptions?.env,
|
|
972
|
+
vars: mcpOptions?.vars,
|
|
973
|
+
});
|
|
998
974
|
});
|
|
999
975
|
}
|
|
1000
976
|
transport = new WebStandardStreamableHTTPServerTransport({
|
|
@@ -1013,7 +989,11 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1013
989
|
const segments = url.pathname.split('/').filter(Boolean);
|
|
1014
990
|
// MCP over HTTP: route /mcp to the MCP transport
|
|
1015
991
|
if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
|
|
1016
|
-
return options.mcpHandler(req, commands
|
|
992
|
+
return options.mcpHandler(req, commands, {
|
|
993
|
+
middlewares: options.middlewares,
|
|
994
|
+
env: options.envSchema,
|
|
995
|
+
vars: options.vars,
|
|
996
|
+
});
|
|
1017
997
|
// .well-known/skills/ — Agent Skills Discovery (RFC)
|
|
1018
998
|
if (segments[0] === '.well-known' &&
|
|
1019
999
|
segments[1] === 'skills' &&
|
|
@@ -1103,7 +1083,11 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1103
1083
|
if ('fetchGateway' in resolved)
|
|
1104
1084
|
return resolved.fetchGateway.fetch(req);
|
|
1105
1085
|
const { command, path, rest } = resolved;
|
|
1106
|
-
|
|
1086
|
+
const groupMiddlewares = 'middlewares' in resolved ? resolved.middlewares : [];
|
|
1087
|
+
return executeCommand(path, command, rest, inputOptions, start, {
|
|
1088
|
+
...options,
|
|
1089
|
+
groupMiddlewares,
|
|
1090
|
+
});
|
|
1107
1091
|
}
|
|
1108
1092
|
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
1109
1093
|
async function executeCommand(path, command, rest, inputOptions, start, options) {
|
|
@@ -1113,156 +1097,90 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1113
1097
|
headers: { 'content-type': 'application/json' },
|
|
1114
1098
|
});
|
|
1115
1099
|
}
|
|
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'));
|
|
1100
|
+
const allMiddleware = [
|
|
1101
|
+
...(options.middlewares ?? []),
|
|
1102
|
+
...(options.groupMiddlewares ?? []),
|
|
1103
|
+
...(command.middleware ?? []),
|
|
1104
|
+
];
|
|
1105
|
+
const result = await Command.execute(command, {
|
|
1106
|
+
agent: true,
|
|
1107
|
+
argv: rest,
|
|
1108
|
+
env: options.envSchema,
|
|
1109
|
+
format: 'json',
|
|
1110
|
+
formatExplicit: true,
|
|
1111
|
+
inputOptions,
|
|
1112
|
+
middlewares: allMiddleware,
|
|
1113
|
+
name: options.name ?? path,
|
|
1114
|
+
parseMode: 'split',
|
|
1115
|
+
path,
|
|
1116
|
+
vars: options.vars,
|
|
1117
|
+
version: options.version,
|
|
1118
|
+
});
|
|
1119
|
+
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1120
|
+
// Streaming path — async generator → NDJSON response
|
|
1121
|
+
if ('stream' in result) {
|
|
1122
|
+
const stream = new ReadableStream({
|
|
1123
|
+
async start(controller) {
|
|
1124
|
+
const encoder = new TextEncoder();
|
|
1125
|
+
try {
|
|
1126
|
+
for await (const value of result.stream) {
|
|
1127
|
+
controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
|
|
1184
1128
|
}
|
|
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
|
-
}
|
|
1129
|
+
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1130
|
+
type: 'done',
|
|
1131
|
+
ok: true,
|
|
1132
|
+
meta: { command: path },
|
|
1133
|
+
}) + '\n'));
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
controller.enqueue(encoder.encode(JSON.stringify({
|
|
1137
|
+
type: 'error',
|
|
1138
|
+
ok: false,
|
|
1139
|
+
error: {
|
|
1140
|
+
code: 'UNKNOWN',
|
|
1141
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1142
|
+
},
|
|
1143
|
+
}) + '\n'));
|
|
1144
|
+
}
|
|
1145
|
+
controller.close();
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
return new Response(stream, {
|
|
1149
|
+
status: 200,
|
|
1150
|
+
headers: { 'content-type': 'application/x-ndjson' },
|
|
1151
|
+
});
|
|
1244
1152
|
}
|
|
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);
|
|
1153
|
+
if (!result.ok) {
|
|
1154
|
+
const cta = formatCtaBlock(options.name ?? path, result.cta);
|
|
1253
1155
|
return jsonResponse({
|
|
1254
1156
|
ok: false,
|
|
1255
1157
|
error: {
|
|
1256
|
-
code: error
|
|
1257
|
-
message: error
|
|
1158
|
+
code: result.error.code,
|
|
1159
|
+
message: result.error.message,
|
|
1160
|
+
...(result.error.retryable !== undefined
|
|
1161
|
+
? { retryable: result.error.retryable }
|
|
1162
|
+
: undefined),
|
|
1163
|
+
},
|
|
1164
|
+
meta: {
|
|
1165
|
+
command: path,
|
|
1166
|
+
duration,
|
|
1167
|
+
...(cta ? { cta } : undefined),
|
|
1258
1168
|
},
|
|
1259
|
-
|
|
1260
|
-
}, 500);
|
|
1169
|
+
}, result.error.code === 'VALIDATION_ERROR' ? 400 : 500);
|
|
1261
1170
|
}
|
|
1262
|
-
|
|
1171
|
+
const cta = formatCtaBlock(options.name ?? path, result.cta);
|
|
1172
|
+
return jsonResponse({
|
|
1173
|
+
ok: true,
|
|
1174
|
+
data: result.data,
|
|
1175
|
+
meta: {
|
|
1176
|
+
command: path,
|
|
1177
|
+
duration,
|
|
1178
|
+
...(cta ? { cta } : undefined),
|
|
1179
|
+
},
|
|
1180
|
+
}, 200);
|
|
1263
1181
|
}
|
|
1264
1182
|
/** @internal Formats a validation error for TTY with usage hint. */
|
|
1265
|
-
function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
1183
|
+
function formatHumanValidationError(cli, path, command, error, envSource, configFlag) {
|
|
1266
1184
|
const lines = [];
|
|
1267
1185
|
for (const fe of error.fieldErrors)
|
|
1268
1186
|
lines.push(`Error: missing required argument <${fe.path}>`);
|
|
@@ -1270,6 +1188,7 @@ function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
|
1270
1188
|
lines.push('');
|
|
1271
1189
|
lines.push(Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
|
|
1272
1190
|
alias: command.alias,
|
|
1191
|
+
configFlag,
|
|
1273
1192
|
description: command.description,
|
|
1274
1193
|
args: command.args,
|
|
1275
1194
|
env: command.env,
|
|
@@ -1343,7 +1262,7 @@ function resolveCommand(commands, tokens) {
|
|
|
1343
1262
|
};
|
|
1344
1263
|
}
|
|
1345
1264
|
/** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
|
|
1346
|
-
function extractBuiltinFlags(argv) {
|
|
1265
|
+
function extractBuiltinFlags(argv, options = {}) {
|
|
1347
1266
|
let verbose = false;
|
|
1348
1267
|
let llms = false;
|
|
1349
1268
|
let llmsFull = false;
|
|
@@ -1353,11 +1272,16 @@ function extractBuiltinFlags(argv) {
|
|
|
1353
1272
|
let schema = false;
|
|
1354
1273
|
let format = 'toon';
|
|
1355
1274
|
let formatExplicit = false;
|
|
1275
|
+
let configPath;
|
|
1276
|
+
let configDisabled = false;
|
|
1356
1277
|
let filterOutput;
|
|
1357
1278
|
let tokenLimit;
|
|
1358
1279
|
let tokenOffset;
|
|
1359
1280
|
let tokenCount = false;
|
|
1360
1281
|
const rest = [];
|
|
1282
|
+
const cfgFlag = options.configFlag ? `--${options.configFlag}` : undefined;
|
|
1283
|
+
const cfgFlagEq = options.configFlag ? `--${options.configFlag}=` : undefined;
|
|
1284
|
+
const noCfgFlag = options.configFlag ? `--no-${options.configFlag}` : undefined;
|
|
1361
1285
|
for (let i = 0; i < argv.length; i++) {
|
|
1362
1286
|
const token = argv[i];
|
|
1363
1287
|
if (token === '--verbose')
|
|
@@ -1383,6 +1307,25 @@ function extractBuiltinFlags(argv) {
|
|
|
1383
1307
|
formatExplicit = true;
|
|
1384
1308
|
i++;
|
|
1385
1309
|
}
|
|
1310
|
+
else if (cfgFlag && token === cfgFlag) {
|
|
1311
|
+
const value = argv[i + 1];
|
|
1312
|
+
if (value === undefined)
|
|
1313
|
+
throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` });
|
|
1314
|
+
configPath = value;
|
|
1315
|
+
configDisabled = false;
|
|
1316
|
+
i++;
|
|
1317
|
+
}
|
|
1318
|
+
else if (cfgFlagEq && token.startsWith(cfgFlagEq)) {
|
|
1319
|
+
const value = token.slice(cfgFlagEq.length);
|
|
1320
|
+
if (value.length === 0)
|
|
1321
|
+
throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` });
|
|
1322
|
+
configPath = value;
|
|
1323
|
+
configDisabled = false;
|
|
1324
|
+
}
|
|
1325
|
+
else if (noCfgFlag && token === noCfgFlag) {
|
|
1326
|
+
configPath = undefined;
|
|
1327
|
+
configDisabled = true;
|
|
1328
|
+
}
|
|
1386
1329
|
else if (token === '--filter-output' && argv[i + 1]) {
|
|
1387
1330
|
filterOutput = argv[i + 1];
|
|
1388
1331
|
i++;
|
|
@@ -1404,6 +1347,8 @@ function extractBuiltinFlags(argv) {
|
|
|
1404
1347
|
verbose,
|
|
1405
1348
|
format,
|
|
1406
1349
|
formatExplicit,
|
|
1350
|
+
configPath,
|
|
1351
|
+
configDisabled,
|
|
1407
1352
|
filterOutput,
|
|
1408
1353
|
tokenLimit,
|
|
1409
1354
|
tokenOffset,
|
|
@@ -1417,6 +1362,117 @@ function extractBuiltinFlags(argv) {
|
|
|
1417
1362
|
rest,
|
|
1418
1363
|
};
|
|
1419
1364
|
}
|
|
1365
|
+
/** @internal Loads config-backed option defaults for the active command. */
|
|
1366
|
+
async function loadCommandOptionDefaults(cli, path, options = {}) {
|
|
1367
|
+
if (options.configDisabled)
|
|
1368
|
+
return undefined;
|
|
1369
|
+
const { loader } = options;
|
|
1370
|
+
// Resolve the target file path
|
|
1371
|
+
let targetPath;
|
|
1372
|
+
if (options.configPath) {
|
|
1373
|
+
targetPath = resolveConfigPath(options.configPath);
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
const searchPaths = options.files ?? [`${cli}.json`];
|
|
1377
|
+
targetPath = await findFirstExisting(searchPaths);
|
|
1378
|
+
}
|
|
1379
|
+
// Load and parse the config
|
|
1380
|
+
let parsed;
|
|
1381
|
+
if (loader) {
|
|
1382
|
+
const result = await loader(targetPath);
|
|
1383
|
+
if (result === undefined)
|
|
1384
|
+
return undefined;
|
|
1385
|
+
if (!isRecord(result))
|
|
1386
|
+
throw new ParseError({ message: 'Config loader must return a plain object or undefined' });
|
|
1387
|
+
parsed = result;
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
if (!targetPath)
|
|
1391
|
+
return undefined;
|
|
1392
|
+
const result = await readJsonConfig(targetPath, !!options.configPath);
|
|
1393
|
+
if (!result)
|
|
1394
|
+
return undefined;
|
|
1395
|
+
parsed = result;
|
|
1396
|
+
}
|
|
1397
|
+
// Extract the command section from the config tree
|
|
1398
|
+
return extractCommandSection(parsed, cli, path);
|
|
1399
|
+
}
|
|
1400
|
+
/** @internal Resolves a config file path, expanding `~` to home dir. */
|
|
1401
|
+
function resolveConfigPath(filePath) {
|
|
1402
|
+
if (filePath.startsWith('~/') || filePath === '~') {
|
|
1403
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
1404
|
+
}
|
|
1405
|
+
return path.resolve(process.cwd(), filePath);
|
|
1406
|
+
}
|
|
1407
|
+
/** @internal Returns the first readable file from a list of paths, or `undefined`. */
|
|
1408
|
+
async function findFirstExisting(paths) {
|
|
1409
|
+
for (const p of paths) {
|
|
1410
|
+
const resolved = resolveConfigPath(p);
|
|
1411
|
+
try {
|
|
1412
|
+
await fs.access(resolved, fs.constants.R_OK);
|
|
1413
|
+
return resolved;
|
|
1414
|
+
}
|
|
1415
|
+
catch { }
|
|
1416
|
+
}
|
|
1417
|
+
return undefined;
|
|
1418
|
+
}
|
|
1419
|
+
/** @internal Reads and parses a JSON config file. */
|
|
1420
|
+
async function readJsonConfig(targetPath, explicit) {
|
|
1421
|
+
let raw;
|
|
1422
|
+
try {
|
|
1423
|
+
raw = await fs.readFile(targetPath, 'utf8');
|
|
1424
|
+
}
|
|
1425
|
+
catch (error) {
|
|
1426
|
+
if (error.code === 'ENOENT') {
|
|
1427
|
+
if (explicit)
|
|
1428
|
+
throw new ParseError({ message: `Config file not found: ${targetPath}` });
|
|
1429
|
+
return undefined;
|
|
1430
|
+
}
|
|
1431
|
+
throw error;
|
|
1432
|
+
}
|
|
1433
|
+
let parsed;
|
|
1434
|
+
try {
|
|
1435
|
+
parsed = JSON.parse(raw);
|
|
1436
|
+
}
|
|
1437
|
+
catch (error) {
|
|
1438
|
+
throw new ParseError({
|
|
1439
|
+
message: `Invalid JSON config file: ${targetPath}`,
|
|
1440
|
+
cause: error instanceof Error ? error : undefined,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
if (!isRecord(parsed))
|
|
1444
|
+
throw new ParseError({
|
|
1445
|
+
message: `Invalid config file: expected a top-level object in ${targetPath}`,
|
|
1446
|
+
});
|
|
1447
|
+
return parsed;
|
|
1448
|
+
}
|
|
1449
|
+
/** @internal Walks the nested config tree to extract option defaults for a command path. */
|
|
1450
|
+
function extractCommandSection(parsed, cli, path) {
|
|
1451
|
+
const segments = path === cli ? [] : path.split(' ');
|
|
1452
|
+
let node = parsed;
|
|
1453
|
+
for (const seg of segments) {
|
|
1454
|
+
if (!isRecord(node))
|
|
1455
|
+
return undefined;
|
|
1456
|
+
const commands = node.commands;
|
|
1457
|
+
if (!isRecord(commands))
|
|
1458
|
+
return undefined;
|
|
1459
|
+
node = commands[seg];
|
|
1460
|
+
if (node === undefined)
|
|
1461
|
+
return undefined;
|
|
1462
|
+
}
|
|
1463
|
+
if (!isRecord(node))
|
|
1464
|
+
throw new ParseError({
|
|
1465
|
+
message: `Invalid config section for '${path}': expected an object`,
|
|
1466
|
+
});
|
|
1467
|
+
const options = node.options;
|
|
1468
|
+
if (options === undefined)
|
|
1469
|
+
return undefined;
|
|
1470
|
+
if (!isRecord(options))
|
|
1471
|
+
throw new ParseError({
|
|
1472
|
+
message: `Invalid config 'options' for '${path}': expected an object`,
|
|
1473
|
+
});
|
|
1474
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
1475
|
+
}
|
|
1420
1476
|
/** @internal Collects immediate child commands/groups for help output. */
|
|
1421
1477
|
function collectHelpCommands(commands) {
|
|
1422
1478
|
const result = [];
|
|
@@ -1425,6 +1481,23 @@ function collectHelpCommands(commands) {
|
|
|
1425
1481
|
}
|
|
1426
1482
|
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
1427
1483
|
}
|
|
1484
|
+
/** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
|
|
1485
|
+
function formatBuiltinHelp(cli, builtin) {
|
|
1486
|
+
return Help.formatRoot(`${cli} ${builtin.name}`, {
|
|
1487
|
+
description: builtin.description,
|
|
1488
|
+
commands: builtin.subcommands?.map((s) => ({ name: s.name, description: s.description })),
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
/** @internal Formats subcommand-level help for a built-in command (e.g. `cli skills add --help`). */
|
|
1492
|
+
function formatBuiltinSubcommandHelp(cli, builtin, subName) {
|
|
1493
|
+
const sub = builtin.subcommands?.find((s) => s.name === subName);
|
|
1494
|
+
return Help.formatCommand(`${cli} ${builtin.name} ${subName}`, {
|
|
1495
|
+
alias: sub?.alias,
|
|
1496
|
+
description: sub?.description,
|
|
1497
|
+
hideGlobalOptions: true,
|
|
1498
|
+
options: sub?.options,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1428
1501
|
/** @internal Formats help text for a fetch gateway command. */
|
|
1429
1502
|
function formatFetchHelp(name, description) {
|
|
1430
1503
|
const lines = [];
|
|
@@ -1458,7 +1531,11 @@ export const toCommands = new WeakMap();
|
|
|
1458
1531
|
/** @internal Maps CLI instances to their middleware arrays. */
|
|
1459
1532
|
const toMiddlewares = new WeakMap();
|
|
1460
1533
|
/** @internal Maps root CLI instances to their command definitions. */
|
|
1461
|
-
const toRootDefinition = new WeakMap();
|
|
1534
|
+
export const toRootDefinition = new WeakMap();
|
|
1535
|
+
/** @internal Maps CLI instances to their root options schema. */
|
|
1536
|
+
export const toRootOptions = new WeakMap();
|
|
1537
|
+
/** @internal Maps CLI instances to whether config file loading is enabled. */
|
|
1538
|
+
export const toConfigEnabled = new WeakMap();
|
|
1462
1539
|
/** @internal Maps CLI instances to their output policy. */
|
|
1463
1540
|
const toOutputPolicy = new WeakMap();
|
|
1464
1541
|
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
@@ -1491,13 +1568,6 @@ function hasRequiredArgs(args) {
|
|
|
1491
1568
|
function isSentinel(value) {
|
|
1492
1569
|
return typeof value === 'object' && value !== null && sentinel in value;
|
|
1493
1570
|
}
|
|
1494
|
-
/** @internal Type guard for async generators returned by streaming `run` handlers. */
|
|
1495
|
-
function isAsyncGenerator(value) {
|
|
1496
|
-
return (typeof value === 'object' &&
|
|
1497
|
-
value !== null &&
|
|
1498
|
-
Symbol.asyncIterator in value &&
|
|
1499
|
-
typeof value.next === 'function');
|
|
1500
|
-
}
|
|
1501
1571
|
/** @internal Handles streaming output from an async generator `run` handler. */
|
|
1502
1572
|
async function handleStreaming(generator, ctx) {
|
|
1503
1573
|
// Incremental: no explicit format (default toon), or explicit jsonl
|