incur 0.3.5 → 0.3.7
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 +61 -0
- package/dist/Cli.d.ts +15 -0
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +300 -25
- package/dist/Cli.js.map +1 -1
- package/dist/Filter.js +0 -18
- package/dist/Filter.js.map +1 -1
- package/dist/Help.d.ts +4 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +17 -14
- package/dist/Help.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/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 +2 -0
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +1 -0
- package/dist/internal/command.js.map +1 -1
- 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 +9 -0
- package/dist/internal/helpers.d.ts.map +1 -0
- package/dist/internal/helpers.js +39 -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 +714 -25
- package/src/Cli.ts +353 -27
- package/src/Filter.ts +0 -17
- package/src/Help.test.ts +66 -0
- package/src/Help.ts +20 -13
- package/src/Openapi.test.ts +6 -1
- package/src/Parser.test-d.ts +22 -0
- package/src/Parser.test.ts +89 -0
- package/src/Parser.ts +86 -35
- package/src/bin.ts +21 -2
- package/src/e2e.test.ts +22 -19
- package/src/internal/command.ts +3 -0
- package/src/internal/configSchema.test.ts +193 -0
- package/src/internal/configSchema.ts +66 -0
- package/src/internal/helpers.test.ts +54 -0
- package/src/internal/helpers.ts +41 -0
package/dist/Cli.js
CHANGED
|
@@ -1,12 +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';
|
|
8
11
|
import { builtinCommands, shells } from './internal/command.js';
|
|
9
12
|
import * as Command from './internal/command.js';
|
|
13
|
+
import { isRecord, suggest } from './internal/helpers.js';
|
|
10
14
|
import { detectRunner } from './internal/pm.js';
|
|
11
15
|
import * as Mcp from './Mcp.js';
|
|
12
16
|
import * as Openapi from './Openapi.js';
|
|
@@ -93,6 +97,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
93
97
|
return serveImpl(name, commands, argv, {
|
|
94
98
|
...serveOptions,
|
|
95
99
|
aliases: def.aliases,
|
|
100
|
+
config: def.config,
|
|
96
101
|
description: def.description,
|
|
97
102
|
envSchema: def.env,
|
|
98
103
|
format: def.format,
|
|
@@ -113,6 +118,10 @@ export function create(nameOrDefinition, definition) {
|
|
|
113
118
|
};
|
|
114
119
|
if (rootDef)
|
|
115
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);
|
|
116
125
|
if (def.outputPolicy)
|
|
117
126
|
toOutputPolicy.set(cli, def.outputPolicy);
|
|
118
127
|
toMiddlewares.set(cli, middlewares);
|
|
@@ -124,7 +133,26 @@ export function create(nameOrDefinition, definition) {
|
|
|
124
133
|
async function serveImpl(name, commands, argv, options = {}) {
|
|
125
134
|
const stdout = options.stdout ?? ((s) => process.stdout.write(s));
|
|
126
135
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
127
|
-
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;
|
|
128
156
|
// --mcp: start as MCP stdio server
|
|
129
157
|
if (mcpFlag) {
|
|
130
158
|
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
@@ -176,12 +204,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
176
204
|
}
|
|
177
205
|
return;
|
|
178
206
|
}
|
|
179
|
-
// Human mode: stdout is a TTY.
|
|
180
|
-
const human = process.stdout.isTTY === true;
|
|
181
|
-
function writeln(s) {
|
|
182
|
-
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
183
|
-
}
|
|
184
207
|
// Skills staleness check (skip for built-in commands)
|
|
208
|
+
let skillsCta;
|
|
185
209
|
if (!llms && !llmsFull && !schema && !help && !version) {
|
|
186
210
|
const isSkillsAdd = filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills');
|
|
187
211
|
const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp');
|
|
@@ -193,7 +217,10 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
193
217
|
if (Skill.hash(entries) !== stored) {
|
|
194
218
|
const runner = detectRunner();
|
|
195
219
|
const spec = SyncMcp.detectPackageSpecifier(name);
|
|
196
|
-
|
|
220
|
+
skillsCta = {
|
|
221
|
+
description: 'Skills are out of date:',
|
|
222
|
+
commands: [{ command: `${runner} ${spec} skills add`, description: 'sync outdated skills' }],
|
|
223
|
+
};
|
|
197
224
|
}
|
|
198
225
|
}
|
|
199
226
|
}
|
|
@@ -278,7 +305,28 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
278
305
|
// skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
|
|
279
306
|
const skillsIdx = filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1;
|
|
280
307
|
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills') {
|
|
281
|
-
|
|
308
|
+
const skillsSub = filtered[skillsIdx + 1];
|
|
309
|
+
if (skillsSub && skillsSub !== 'add') {
|
|
310
|
+
const suggestion = suggest(skillsSub, ['add']);
|
|
311
|
+
const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
312
|
+
const message = `'${skillsSub}' is not a command for '${name} skills'.${didYouMean}`;
|
|
313
|
+
const ctaCommands = [];
|
|
314
|
+
if (suggestion) {
|
|
315
|
+
const corrected = argv.map((t) => (t === skillsSub ? suggestion : t));
|
|
316
|
+
ctaCommands.push({ command: `${name} ${corrected.join(' ')}` });
|
|
317
|
+
}
|
|
318
|
+
ctaCommands.push({ command: `${name} skills --help`, description: 'see all available commands' });
|
|
319
|
+
const cta = { description: 'Next steps:', commands: ctaCommands };
|
|
320
|
+
if (human) {
|
|
321
|
+
writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
|
|
322
|
+
writeln(formatHumanCta(cta));
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
writeln(Formatter.format({ code: 'COMMAND_NOT_FOUND', message, cta }, 'toon'));
|
|
326
|
+
exit(1);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!skillsSub) {
|
|
282
330
|
const b = builtinCommands.find((c) => c.name === 'skills');
|
|
283
331
|
writeln(formatBuiltinHelp(name, b));
|
|
284
332
|
return;
|
|
@@ -345,7 +393,28 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
345
393
|
// mcp add: register CLI as MCP server via `npx add-mcp`
|
|
346
394
|
const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1;
|
|
347
395
|
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp') {
|
|
348
|
-
|
|
396
|
+
const mcpSub = filtered[mcpIdx + 1];
|
|
397
|
+
if (mcpSub && mcpSub !== 'add') {
|
|
398
|
+
const suggestion = suggest(mcpSub, ['add']);
|
|
399
|
+
const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
400
|
+
const message = `'${mcpSub}' is not a command for '${name} mcp'.${didYouMean}`;
|
|
401
|
+
const ctaCommands = [];
|
|
402
|
+
if (suggestion) {
|
|
403
|
+
const corrected = argv.map((t) => (t === mcpSub ? suggestion : t));
|
|
404
|
+
ctaCommands.push({ command: `${name} ${corrected.join(' ')}` });
|
|
405
|
+
}
|
|
406
|
+
ctaCommands.push({ command: `${name} mcp --help`, description: 'see all available commands' });
|
|
407
|
+
const cta = { description: 'Next steps:', commands: ctaCommands };
|
|
408
|
+
if (human) {
|
|
409
|
+
writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
|
|
410
|
+
writeln(formatHumanCta(cta));
|
|
411
|
+
}
|
|
412
|
+
else
|
|
413
|
+
writeln(Formatter.format({ code: 'COMMAND_NOT_FOUND', message, cta }, 'toon'));
|
|
414
|
+
exit(1);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!mcpSub) {
|
|
349
418
|
const b = builtinCommands.find((c) => c.name === 'mcp');
|
|
350
419
|
writeln(formatBuiltinHelp(name, b));
|
|
351
420
|
return;
|
|
@@ -412,6 +481,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
412
481
|
writeln(Help.formatCommand(name, {
|
|
413
482
|
alias: cmd.alias,
|
|
414
483
|
aliases: options.aliases,
|
|
484
|
+
configFlag,
|
|
415
485
|
description: cmd.description ?? options.description,
|
|
416
486
|
version: options.version,
|
|
417
487
|
args: cmd.args,
|
|
@@ -432,6 +502,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
432
502
|
else {
|
|
433
503
|
writeln(Help.formatRoot(name, {
|
|
434
504
|
aliases: options.aliases,
|
|
505
|
+
configFlag,
|
|
435
506
|
description: options.description,
|
|
436
507
|
version: options.version,
|
|
437
508
|
commands: collectHelpCommands(commands),
|
|
@@ -474,6 +545,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
474
545
|
writeln(Help.formatCommand(name, {
|
|
475
546
|
alias: cmd.alias,
|
|
476
547
|
aliases: options.aliases,
|
|
548
|
+
configFlag,
|
|
477
549
|
description: cmd.description ?? options.description,
|
|
478
550
|
version: options.version,
|
|
479
551
|
args: cmd.args,
|
|
@@ -490,6 +562,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
490
562
|
else {
|
|
491
563
|
writeln(Help.formatRoot(helpName, {
|
|
492
564
|
aliases: isRoot ? options.aliases : undefined,
|
|
565
|
+
configFlag,
|
|
493
566
|
description: helpDesc,
|
|
494
567
|
version: isRoot ? options.version : undefined,
|
|
495
568
|
commands: collectHelpCommands(helpCmds),
|
|
@@ -507,6 +580,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
507
580
|
writeln(Help.formatCommand(commandName, {
|
|
508
581
|
alias: cmd.alias,
|
|
509
582
|
aliases: isRootCmd ? options.aliases : undefined,
|
|
583
|
+
configFlag,
|
|
510
584
|
description: cmd.description,
|
|
511
585
|
version: isRootCmd ? options.version : undefined,
|
|
512
586
|
args: cmd.args,
|
|
@@ -526,6 +600,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
526
600
|
if (schema) {
|
|
527
601
|
if ('help' in resolved) {
|
|
528
602
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
603
|
+
configFlag,
|
|
529
604
|
description: resolved.description,
|
|
530
605
|
commands: collectHelpCommands(resolved.commands),
|
|
531
606
|
}));
|
|
@@ -533,7 +608,9 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
533
608
|
}
|
|
534
609
|
if ('error' in resolved) {
|
|
535
610
|
const parent = resolved.path ? `${name} ${resolved.path}` : name;
|
|
536
|
-
|
|
611
|
+
const suggestion = suggest(resolved.error, resolved.commands.keys());
|
|
612
|
+
const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
613
|
+
writeln(`Error: '${resolved.error}' is not a command for '${parent}'.${didYouMean}`);
|
|
537
614
|
exit(1);
|
|
538
615
|
return;
|
|
539
616
|
}
|
|
@@ -558,6 +635,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
558
635
|
}
|
|
559
636
|
if ('help' in resolved) {
|
|
560
637
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
638
|
+
configFlag,
|
|
561
639
|
description: resolved.description,
|
|
562
640
|
commands: collectHelpCommands(resolved.commands),
|
|
563
641
|
}));
|
|
@@ -606,6 +684,21 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
606
684
|
function write(output) {
|
|
607
685
|
if (filterPaths && output.ok && output.data != null)
|
|
608
686
|
output = { ...output, data: Filter.apply(output.data, filterPaths) };
|
|
687
|
+
if (skillsCta) {
|
|
688
|
+
const existing = output.meta.cta;
|
|
689
|
+
output = {
|
|
690
|
+
...output,
|
|
691
|
+
meta: {
|
|
692
|
+
...output.meta,
|
|
693
|
+
cta: existing
|
|
694
|
+
? {
|
|
695
|
+
description: existing.description,
|
|
696
|
+
commands: [...existing.commands, ...skillsCta.commands],
|
|
697
|
+
}
|
|
698
|
+
: skillsCta,
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
}
|
|
609
702
|
if (tokenCount) {
|
|
610
703
|
const base = output.ok ? output.data : output.error;
|
|
611
704
|
const formatted = base != null ? Formatter.format(base, format) : '';
|
|
@@ -658,14 +751,26 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
658
751
|
if ('error' in effective) {
|
|
659
752
|
const helpCmd = effective.path ? `${name} ${effective.path} --help` : `${name} --help`;
|
|
660
753
|
const parent = effective.path ? `${name} ${effective.path}` : name;
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
754
|
+
const candidates = 'commands' in effective ? [...effective.commands.keys()] : [];
|
|
755
|
+
if (!effective.path)
|
|
756
|
+
for (const b of builtinCommands)
|
|
757
|
+
candidates.push(b.name);
|
|
758
|
+
const suggestion = suggest(effective.error, candidates);
|
|
759
|
+
const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
760
|
+
const message = `'${effective.error}' is not a command for '${parent}'.${didYouMean}`;
|
|
761
|
+
const ctaCommands = [];
|
|
762
|
+
if (suggestion) {
|
|
763
|
+
const corrected = argv.map((t) => (t === effective.error ? suggestion : t));
|
|
764
|
+
ctaCommands.push({ command: `${name} ${corrected.join(' ')}` });
|
|
765
|
+
}
|
|
766
|
+
ctaCommands.push({ command: helpCmd, description: 'see all available commands' });
|
|
767
|
+
const cta = { description: 'Next steps:', commands: ctaCommands };
|
|
666
768
|
if (human && !verbose) {
|
|
667
769
|
writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
|
|
668
|
-
|
|
770
|
+
const mergedCta = skillsCta
|
|
771
|
+
? { ...cta, commands: [...cta.commands, ...skillsCta.commands] }
|
|
772
|
+
: cta;
|
|
773
|
+
writeln(formatHumanCta(mergedCta));
|
|
669
774
|
exit(1);
|
|
670
775
|
return;
|
|
671
776
|
}
|
|
@@ -817,9 +922,33 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
817
922
|
];
|
|
818
923
|
if (human)
|
|
819
924
|
emitDeprecationWarnings(rest, command.options, command.alias);
|
|
925
|
+
let defaults;
|
|
926
|
+
if (configEnabled) {
|
|
927
|
+
try {
|
|
928
|
+
defaults = await loadCommandOptionDefaults(name, path, {
|
|
929
|
+
configDisabled,
|
|
930
|
+
configPath,
|
|
931
|
+
files: options.config?.files,
|
|
932
|
+
loader: options.config?.loader,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
write({
|
|
937
|
+
ok: false,
|
|
938
|
+
error: {
|
|
939
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
940
|
+
message: error instanceof Error ? error.message : String(error),
|
|
941
|
+
},
|
|
942
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
943
|
+
});
|
|
944
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
820
948
|
const result = await Command.execute(command, {
|
|
821
949
|
agent: !human,
|
|
822
950
|
argv: rest,
|
|
951
|
+
defaults,
|
|
823
952
|
env: options.envSchema,
|
|
824
953
|
envSource: options.env,
|
|
825
954
|
format,
|
|
@@ -868,7 +997,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
868
997
|
writeln(formatHumanValidationError(name, path, command, new ValidationError({
|
|
869
998
|
message: result.error.message,
|
|
870
999
|
fieldErrors: result.error.fieldErrors,
|
|
871
|
-
}), options.env));
|
|
1000
|
+
}), options.env, configFlag));
|
|
872
1001
|
exit(1);
|
|
873
1002
|
return;
|
|
874
1003
|
}
|
|
@@ -1008,15 +1137,19 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1008
1137
|
}, 404);
|
|
1009
1138
|
}
|
|
1010
1139
|
const resolved = resolveCommand(commands, segments);
|
|
1011
|
-
if ('error' in resolved)
|
|
1140
|
+
if ('error' in resolved) {
|
|
1141
|
+
const parent = resolved.path ? `${name} ${resolved.path}` : name;
|
|
1142
|
+
const suggestion = suggest(resolved.error, resolved.commands.keys());
|
|
1143
|
+
const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
1012
1144
|
return jsonResponse({
|
|
1013
1145
|
ok: false,
|
|
1014
1146
|
error: {
|
|
1015
1147
|
code: 'COMMAND_NOT_FOUND',
|
|
1016
|
-
message: `'${resolved.error}' is not a command for '${
|
|
1148
|
+
message: `'${resolved.error}' is not a command for '${parent}'.${didYouMean}`,
|
|
1017
1149
|
},
|
|
1018
1150
|
meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1019
1151
|
}, 404);
|
|
1152
|
+
}
|
|
1020
1153
|
if ('help' in resolved)
|
|
1021
1154
|
return jsonResponse({
|
|
1022
1155
|
ok: false,
|
|
@@ -1126,7 +1259,7 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1126
1259
|
}, 200);
|
|
1127
1260
|
}
|
|
1128
1261
|
/** @internal Formats a validation error for TTY with usage hint. */
|
|
1129
|
-
function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
1262
|
+
function formatHumanValidationError(cli, path, command, error, envSource, configFlag) {
|
|
1130
1263
|
const lines = [];
|
|
1131
1264
|
for (const fe of error.fieldErrors)
|
|
1132
1265
|
lines.push(`Error: missing required argument <${fe.path}>`);
|
|
@@ -1134,6 +1267,7 @@ function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
|
1134
1267
|
lines.push('');
|
|
1135
1268
|
lines.push(Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
|
|
1136
1269
|
alias: command.alias,
|
|
1270
|
+
configFlag,
|
|
1137
1271
|
description: command.description,
|
|
1138
1272
|
args: command.args,
|
|
1139
1273
|
env: command.env,
|
|
@@ -1149,7 +1283,7 @@ function formatHumanValidationError(cli, path, command, error, envSource) {
|
|
|
1149
1283
|
function resolveCommand(commands, tokens) {
|
|
1150
1284
|
const [first, ...rest] = tokens;
|
|
1151
1285
|
if (!first || !commands.has(first))
|
|
1152
|
-
return { error: first ?? '(none)', path: '' };
|
|
1286
|
+
return { error: first ?? '(none)', path: '', commands, rest };
|
|
1153
1287
|
let entry = commands.get(first);
|
|
1154
1288
|
const path = [first];
|
|
1155
1289
|
let remaining = rest;
|
|
@@ -1181,7 +1315,7 @@ function resolveCommand(commands, tokens) {
|
|
|
1181
1315
|
};
|
|
1182
1316
|
const child = entry.commands.get(next);
|
|
1183
1317
|
if (!child) {
|
|
1184
|
-
return { error: next, path: path.join(' ') };
|
|
1318
|
+
return { error: next, path: path.join(' '), commands: entry.commands, rest: remaining.slice(1) };
|
|
1185
1319
|
}
|
|
1186
1320
|
path.push(next);
|
|
1187
1321
|
remaining = remaining.slice(1);
|
|
@@ -1207,7 +1341,7 @@ function resolveCommand(commands, tokens) {
|
|
|
1207
1341
|
};
|
|
1208
1342
|
}
|
|
1209
1343
|
/** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
|
|
1210
|
-
function extractBuiltinFlags(argv) {
|
|
1344
|
+
function extractBuiltinFlags(argv, options = {}) {
|
|
1211
1345
|
let verbose = false;
|
|
1212
1346
|
let llms = false;
|
|
1213
1347
|
let llmsFull = false;
|
|
@@ -1217,11 +1351,16 @@ function extractBuiltinFlags(argv) {
|
|
|
1217
1351
|
let schema = false;
|
|
1218
1352
|
let format = 'toon';
|
|
1219
1353
|
let formatExplicit = false;
|
|
1354
|
+
let configPath;
|
|
1355
|
+
let configDisabled = false;
|
|
1220
1356
|
let filterOutput;
|
|
1221
1357
|
let tokenLimit;
|
|
1222
1358
|
let tokenOffset;
|
|
1223
1359
|
let tokenCount = false;
|
|
1224
1360
|
const rest = [];
|
|
1361
|
+
const cfgFlag = options.configFlag ? `--${options.configFlag}` : undefined;
|
|
1362
|
+
const cfgFlagEq = options.configFlag ? `--${options.configFlag}=` : undefined;
|
|
1363
|
+
const noCfgFlag = options.configFlag ? `--no-${options.configFlag}` : undefined;
|
|
1225
1364
|
for (let i = 0; i < argv.length; i++) {
|
|
1226
1365
|
const token = argv[i];
|
|
1227
1366
|
if (token === '--verbose')
|
|
@@ -1247,6 +1386,25 @@ function extractBuiltinFlags(argv) {
|
|
|
1247
1386
|
formatExplicit = true;
|
|
1248
1387
|
i++;
|
|
1249
1388
|
}
|
|
1389
|
+
else if (cfgFlag && token === cfgFlag) {
|
|
1390
|
+
const value = argv[i + 1];
|
|
1391
|
+
if (value === undefined)
|
|
1392
|
+
throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` });
|
|
1393
|
+
configPath = value;
|
|
1394
|
+
configDisabled = false;
|
|
1395
|
+
i++;
|
|
1396
|
+
}
|
|
1397
|
+
else if (cfgFlagEq && token.startsWith(cfgFlagEq)) {
|
|
1398
|
+
const value = token.slice(cfgFlagEq.length);
|
|
1399
|
+
if (value.length === 0)
|
|
1400
|
+
throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` });
|
|
1401
|
+
configPath = value;
|
|
1402
|
+
configDisabled = false;
|
|
1403
|
+
}
|
|
1404
|
+
else if (noCfgFlag && token === noCfgFlag) {
|
|
1405
|
+
configPath = undefined;
|
|
1406
|
+
configDisabled = true;
|
|
1407
|
+
}
|
|
1250
1408
|
else if (token === '--filter-output' && argv[i + 1]) {
|
|
1251
1409
|
filterOutput = argv[i + 1];
|
|
1252
1410
|
i++;
|
|
@@ -1268,6 +1426,8 @@ function extractBuiltinFlags(argv) {
|
|
|
1268
1426
|
verbose,
|
|
1269
1427
|
format,
|
|
1270
1428
|
formatExplicit,
|
|
1429
|
+
configPath,
|
|
1430
|
+
configDisabled,
|
|
1271
1431
|
filterOutput,
|
|
1272
1432
|
tokenLimit,
|
|
1273
1433
|
tokenOffset,
|
|
@@ -1281,6 +1441,117 @@ function extractBuiltinFlags(argv) {
|
|
|
1281
1441
|
rest,
|
|
1282
1442
|
};
|
|
1283
1443
|
}
|
|
1444
|
+
/** @internal Loads config-backed option defaults for the active command. */
|
|
1445
|
+
async function loadCommandOptionDefaults(cli, path, options = {}) {
|
|
1446
|
+
if (options.configDisabled)
|
|
1447
|
+
return undefined;
|
|
1448
|
+
const { loader } = options;
|
|
1449
|
+
// Resolve the target file path
|
|
1450
|
+
let targetPath;
|
|
1451
|
+
if (options.configPath) {
|
|
1452
|
+
targetPath = resolveConfigPath(options.configPath);
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
const searchPaths = options.files ?? [`${cli}.json`];
|
|
1456
|
+
targetPath = await findFirstExisting(searchPaths);
|
|
1457
|
+
}
|
|
1458
|
+
// Load and parse the config
|
|
1459
|
+
let parsed;
|
|
1460
|
+
if (loader) {
|
|
1461
|
+
const result = await loader(targetPath);
|
|
1462
|
+
if (result === undefined)
|
|
1463
|
+
return undefined;
|
|
1464
|
+
if (!isRecord(result))
|
|
1465
|
+
throw new ParseError({ message: 'Config loader must return a plain object or undefined' });
|
|
1466
|
+
parsed = result;
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
if (!targetPath)
|
|
1470
|
+
return undefined;
|
|
1471
|
+
const result = await readJsonConfig(targetPath, !!options.configPath);
|
|
1472
|
+
if (!result)
|
|
1473
|
+
return undefined;
|
|
1474
|
+
parsed = result;
|
|
1475
|
+
}
|
|
1476
|
+
// Extract the command section from the config tree
|
|
1477
|
+
return extractCommandSection(parsed, cli, path);
|
|
1478
|
+
}
|
|
1479
|
+
/** @internal Resolves a config file path, expanding `~` to home dir. */
|
|
1480
|
+
function resolveConfigPath(filePath) {
|
|
1481
|
+
if (filePath.startsWith('~/') || filePath === '~') {
|
|
1482
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
1483
|
+
}
|
|
1484
|
+
return path.resolve(process.cwd(), filePath);
|
|
1485
|
+
}
|
|
1486
|
+
/** @internal Returns the first readable file from a list of paths, or `undefined`. */
|
|
1487
|
+
async function findFirstExisting(paths) {
|
|
1488
|
+
for (const p of paths) {
|
|
1489
|
+
const resolved = resolveConfigPath(p);
|
|
1490
|
+
try {
|
|
1491
|
+
await fs.access(resolved, fs.constants.R_OK);
|
|
1492
|
+
return resolved;
|
|
1493
|
+
}
|
|
1494
|
+
catch { }
|
|
1495
|
+
}
|
|
1496
|
+
return undefined;
|
|
1497
|
+
}
|
|
1498
|
+
/** @internal Reads and parses a JSON config file. */
|
|
1499
|
+
async function readJsonConfig(targetPath, explicit) {
|
|
1500
|
+
let raw;
|
|
1501
|
+
try {
|
|
1502
|
+
raw = await fs.readFile(targetPath, 'utf8');
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
if (error.code === 'ENOENT') {
|
|
1506
|
+
if (explicit)
|
|
1507
|
+
throw new ParseError({ message: `Config file not found: ${targetPath}` });
|
|
1508
|
+
return undefined;
|
|
1509
|
+
}
|
|
1510
|
+
throw error;
|
|
1511
|
+
}
|
|
1512
|
+
let parsed;
|
|
1513
|
+
try {
|
|
1514
|
+
parsed = JSON.parse(raw);
|
|
1515
|
+
}
|
|
1516
|
+
catch (error) {
|
|
1517
|
+
throw new ParseError({
|
|
1518
|
+
message: `Invalid JSON config file: ${targetPath}`,
|
|
1519
|
+
cause: error instanceof Error ? error : undefined,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (!isRecord(parsed))
|
|
1523
|
+
throw new ParseError({
|
|
1524
|
+
message: `Invalid config file: expected a top-level object in ${targetPath}`,
|
|
1525
|
+
});
|
|
1526
|
+
return parsed;
|
|
1527
|
+
}
|
|
1528
|
+
/** @internal Walks the nested config tree to extract option defaults for a command path. */
|
|
1529
|
+
function extractCommandSection(parsed, cli, path) {
|
|
1530
|
+
const segments = path === cli ? [] : path.split(' ');
|
|
1531
|
+
let node = parsed;
|
|
1532
|
+
for (const seg of segments) {
|
|
1533
|
+
if (!isRecord(node))
|
|
1534
|
+
return undefined;
|
|
1535
|
+
const commands = node.commands;
|
|
1536
|
+
if (!isRecord(commands))
|
|
1537
|
+
return undefined;
|
|
1538
|
+
node = commands[seg];
|
|
1539
|
+
if (node === undefined)
|
|
1540
|
+
return undefined;
|
|
1541
|
+
}
|
|
1542
|
+
if (!isRecord(node))
|
|
1543
|
+
throw new ParseError({
|
|
1544
|
+
message: `Invalid config section for '${path}': expected an object`,
|
|
1545
|
+
});
|
|
1546
|
+
const options = node.options;
|
|
1547
|
+
if (options === undefined)
|
|
1548
|
+
return undefined;
|
|
1549
|
+
if (!isRecord(options))
|
|
1550
|
+
throw new ParseError({
|
|
1551
|
+
message: `Invalid config 'options' for '${path}': expected an object`,
|
|
1552
|
+
});
|
|
1553
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
1554
|
+
}
|
|
1284
1555
|
/** @internal Collects immediate child commands/groups for help output. */
|
|
1285
1556
|
function collectHelpCommands(commands) {
|
|
1286
1557
|
const result = [];
|
|
@@ -1339,7 +1610,11 @@ export const toCommands = new WeakMap();
|
|
|
1339
1610
|
/** @internal Maps CLI instances to their middleware arrays. */
|
|
1340
1611
|
const toMiddlewares = new WeakMap();
|
|
1341
1612
|
/** @internal Maps root CLI instances to their command definitions. */
|
|
1342
|
-
const toRootDefinition = new WeakMap();
|
|
1613
|
+
export const toRootDefinition = new WeakMap();
|
|
1614
|
+
/** @internal Maps CLI instances to their root options schema. */
|
|
1615
|
+
export const toRootOptions = new WeakMap();
|
|
1616
|
+
/** @internal Maps CLI instances to whether config file loading is enabled. */
|
|
1617
|
+
export const toConfigEnabled = new WeakMap();
|
|
1343
1618
|
/** @internal Maps CLI instances to their output policy. */
|
|
1344
1619
|
const toOutputPolicy = new WeakMap();
|
|
1345
1620
|
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|