argsbarg 3.4.2 → 3.6.0
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/CHANGELOG.md +31 -1
- package/README.md +24 -8
- package/biome.json +29 -6
- package/bun.lock +22 -0
- package/docs/cli-program.md +75 -1
- package/docs/install.md +1 -1
- package/docs/mcp.md +45 -1
- package/docs/templates/cursor/rules/cli-program.mdc +15 -21
- package/index.d.ts +95 -50
- package/justfile +24 -6
- package/package.json +4 -2
- package/scripts/release.ts +26 -9
- package/src/builtins/builtins.test.ts +9 -4
- package/src/builtins/completion-bash.ts +74 -50
- package/src/builtins/completion-fish.ts +3 -8
- package/src/builtins/completion-group.ts +1 -1
- package/src/builtins/completion-zsh.ts +80 -42
- package/src/builtins/dispatch.ts +20 -16
- package/src/builtins/export.ts +19 -10
- package/src/builtins/index.ts +9 -4
- package/src/builtins/install.ts +10 -10
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/presentation.ts +8 -8
- package/src/builtins/scopes.ts +1 -1
- package/src/builtins/version.ts +1 -1
- package/src/completion.ts +4 -4
- package/src/context.ts +91 -15
- package/src/docs/api-guide.test.ts +2 -2
- package/src/docs/api-guide.ts +19 -5
- package/src/docs/builtin.ts +27 -8
- package/src/docs/docs.test.ts +18 -11
- package/src/docs/mcp-guide.ts +108 -25
- package/src/docs/resolve.ts +10 -3
- package/src/docs/save.ts +11 -3
- package/src/formats.test.ts +35 -0
- package/src/formats.ts +135 -0
- package/src/headless.test.ts +8 -16
- package/src/help.ts +73 -43
- package/src/hidden-mcpb.test.ts +7 -6
- package/src/hidden.ts +2 -2
- package/src/index.test.ts +120 -96
- package/src/index.ts +36 -24
- package/src/install/binary.ts +12 -5
- package/src/install/completions.ts +7 -3
- package/src/install/detect-installed.ts +29 -4
- package/src/install/gh-release-update.ts +31 -23
- package/src/install/index.ts +69 -19
- package/src/install/install.test.ts +31 -8
- package/src/install/mcp-codex.test.ts +57 -0
- package/src/install/mcp-codex.ts +125 -0
- package/src/install/mcp-config.ts +12 -5
- package/src/install/mcp-opencode.test.ts +98 -0
- package/src/install/mcp-opencode.ts +149 -0
- package/src/install/paths.ts +29 -3
- package/src/install/plan.ts +73 -6
- package/src/install/shell.ts +1 -4
- package/src/install/status.ts +12 -6
- package/src/install/uninstall.ts +38 -4
- package/src/install/update.test.ts +2 -2
- package/src/install/update.ts +3 -1
- package/src/invoke.ts +12 -9
- package/src/mcp/bundle.ts +36 -8
- package/src/mcp/env.ts +7 -13
- package/src/mcp/server.ts +12 -6
- package/src/mcp/tools.ts +83 -18
- package/src/mcp.ts +3 -3
- package/src/parse.ts +129 -27
- package/src/runtime.ts +22 -12
- package/src/schema.ts +11 -5
- package/src/skill/generate.ts +4 -4
- package/src/skill/install.ts +6 -2
- package/src/types.ts +24 -0
- package/src/validate.ts +75 -16
package/src/parse.ts
CHANGED
|
@@ -7,12 +7,12 @@ It keeps handler dispatch and help on one parser so the CLI behavior stays consi
|
|
|
7
7
|
across every entry path.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { formatValidationError, validateFormatValue } from "./formats.ts";
|
|
11
11
|
import {
|
|
12
|
-
type CliLeaf,
|
|
13
|
-
CliNode,
|
|
14
12
|
CliFallbackMode,
|
|
15
|
-
|
|
13
|
+
type CliLeaf,
|
|
14
|
+
type CliNode,
|
|
15
|
+
type CliOption,
|
|
16
16
|
CliOptionKind,
|
|
17
17
|
isCliLeaf,
|
|
18
18
|
isCliRouter,
|
|
@@ -191,17 +191,30 @@ function consumeOptions(
|
|
|
191
191
|
|
|
192
192
|
if (tok === "--") {
|
|
193
193
|
idx += 1;
|
|
194
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
report: { err: null, stoppedOnUnknown: false, sawDoubleDash: true },
|
|
196
|
+
nextIndex: idx,
|
|
197
|
+
};
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
if (tok.startsWith("--")) {
|
|
198
201
|
const err = consumeLong(tok);
|
|
199
|
-
if (err === "")
|
|
200
|
-
|
|
202
|
+
if (err === "")
|
|
203
|
+
return {
|
|
204
|
+
report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false },
|
|
205
|
+
nextIndex: idx,
|
|
206
|
+
};
|
|
207
|
+
if (err)
|
|
208
|
+
return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
|
|
201
209
|
} else {
|
|
202
210
|
const err = consumeShort(tok);
|
|
203
|
-
if (err === "")
|
|
204
|
-
|
|
211
|
+
if (err === "")
|
|
212
|
+
return {
|
|
213
|
+
report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false },
|
|
214
|
+
nextIndex: idx,
|
|
215
|
+
};
|
|
216
|
+
if (err)
|
|
217
|
+
return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
|
|
@@ -212,7 +225,7 @@ function consumeOptions(
|
|
|
212
225
|
|
|
213
226
|
/** Merges option defs from the program root along the routed command path. */
|
|
214
227
|
export function collectOptionDefs(root: CliNode, path: string[]): CliOption[] {
|
|
215
|
-
|
|
228
|
+
const defs = [...(root.options ?? [])];
|
|
216
229
|
let node: CliNode = root;
|
|
217
230
|
|
|
218
231
|
for (const seg of path) {
|
|
@@ -343,7 +356,16 @@ function finishLeaf(
|
|
|
343
356
|
}
|
|
344
357
|
}
|
|
345
358
|
|
|
346
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
kind: ParseKind.Ok,
|
|
361
|
+
path,
|
|
362
|
+
opts,
|
|
363
|
+
args,
|
|
364
|
+
helpExplicit: false,
|
|
365
|
+
helpPath: [],
|
|
366
|
+
errorMsg: "",
|
|
367
|
+
errorHelpPath: [],
|
|
368
|
+
};
|
|
347
369
|
}
|
|
348
370
|
|
|
349
371
|
// ── Main Parser ───────────────────────────────────────────────────────────────
|
|
@@ -414,7 +436,16 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
414
436
|
cmdName = root.fallbackCommand;
|
|
415
437
|
node = findChild(root.commands, cmdName);
|
|
416
438
|
if (!node) {
|
|
417
|
-
return {
|
|
439
|
+
return {
|
|
440
|
+
kind: ParseKind.Error,
|
|
441
|
+
path: [],
|
|
442
|
+
opts: {},
|
|
443
|
+
args: [],
|
|
444
|
+
helpExplicit: false,
|
|
445
|
+
helpPath: [],
|
|
446
|
+
errorMsg: `Unknown command: ${cmdName}`,
|
|
447
|
+
errorHelpPath: path,
|
|
448
|
+
};
|
|
418
449
|
}
|
|
419
450
|
} else {
|
|
420
451
|
return helpResult([], false);
|
|
@@ -428,16 +459,26 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
428
459
|
i += 1;
|
|
429
460
|
node = childPick;
|
|
430
461
|
} else {
|
|
462
|
+
const fallbackCommand = root.fallbackCommand;
|
|
431
463
|
const canRouteUnknown =
|
|
432
|
-
|
|
464
|
+
fallbackCommand !== undefined &&
|
|
433
465
|
((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown ||
|
|
434
466
|
(root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.UnknownOnly);
|
|
435
467
|
|
|
436
468
|
if (canRouteUnknown) {
|
|
437
|
-
cmdName =
|
|
469
|
+
cmdName = fallbackCommand;
|
|
438
470
|
node = findChild(root.commands, cmdName);
|
|
439
471
|
if (!node) {
|
|
440
|
-
return {
|
|
472
|
+
return {
|
|
473
|
+
kind: ParseKind.Error,
|
|
474
|
+
path: [],
|
|
475
|
+
opts: {},
|
|
476
|
+
args: [],
|
|
477
|
+
helpExplicit: false,
|
|
478
|
+
helpPath: [],
|
|
479
|
+
errorMsg: `Unknown command: ${cmdName}`,
|
|
480
|
+
errorHelpPath: path,
|
|
481
|
+
};
|
|
441
482
|
}
|
|
442
483
|
} else {
|
|
443
484
|
cmdName = peek;
|
|
@@ -451,7 +492,9 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
451
492
|
args: [],
|
|
452
493
|
helpExplicit: false,
|
|
453
494
|
helpPath: [],
|
|
454
|
-
errorMsg: forcePositionals
|
|
495
|
+
errorMsg: forcePositionals
|
|
496
|
+
? `Expected subcommand but got positional: ${cmdName}`
|
|
497
|
+
: `Unknown command: ${cmdName}`,
|
|
455
498
|
errorHelpPath: path,
|
|
456
499
|
};
|
|
457
500
|
}
|
|
@@ -460,7 +503,19 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
460
503
|
}
|
|
461
504
|
|
|
462
505
|
path.push(cmdName);
|
|
463
|
-
|
|
506
|
+
if (!node) {
|
|
507
|
+
return {
|
|
508
|
+
kind: ParseKind.Error,
|
|
509
|
+
path,
|
|
510
|
+
opts: {},
|
|
511
|
+
args: [],
|
|
512
|
+
helpExplicit: false,
|
|
513
|
+
helpPath: [],
|
|
514
|
+
errorMsg: `Unknown command: ${cmdName}`,
|
|
515
|
+
errorHelpPath: path,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
let current = node;
|
|
464
519
|
|
|
465
520
|
// Walk the command tree
|
|
466
521
|
while (true) {
|
|
@@ -508,7 +563,15 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
508
563
|
if (!isCliLeaf(current)) {
|
|
509
564
|
return helpResult(path, false);
|
|
510
565
|
}
|
|
511
|
-
return finishLeaf(
|
|
566
|
+
return finishLeaf(
|
|
567
|
+
current,
|
|
568
|
+
i,
|
|
569
|
+
argv,
|
|
570
|
+
path,
|
|
571
|
+
opts,
|
|
572
|
+
collectOptionDefs(root, path),
|
|
573
|
+
forcePositionals,
|
|
574
|
+
);
|
|
512
575
|
}
|
|
513
576
|
|
|
514
577
|
const tok = argv[i];
|
|
@@ -542,10 +605,10 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
542
605
|
fb !== undefined &&
|
|
543
606
|
(fm === CliFallbackMode.MissingOrUnknown || fm === CliFallbackMode.UnknownOnly);
|
|
544
607
|
|
|
545
|
-
if (canRouteUnknown) {
|
|
546
|
-
const fbNode = findChild(current.commands, fb
|
|
608
|
+
if (canRouteUnknown && fb !== undefined) {
|
|
609
|
+
const fbNode = findChild(current.commands, fb);
|
|
547
610
|
if (fbNode) {
|
|
548
|
-
path.push(fb
|
|
611
|
+
path.push(fb);
|
|
549
612
|
current = fbNode;
|
|
550
613
|
continue;
|
|
551
614
|
}
|
|
@@ -558,7 +621,9 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
558
621
|
args: [],
|
|
559
622
|
helpExplicit: false,
|
|
560
623
|
helpPath: [],
|
|
561
|
-
errorMsg: forcePositionals
|
|
624
|
+
errorMsg: forcePositionals
|
|
625
|
+
? `Expected subcommand but got positional: ${tok}`
|
|
626
|
+
: `Unknown subcommand: ${tok}`,
|
|
562
627
|
errorHelpPath: path,
|
|
563
628
|
};
|
|
564
629
|
}
|
|
@@ -566,7 +631,15 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
566
631
|
if (!isCliLeaf(current)) {
|
|
567
632
|
return helpResult(path, false);
|
|
568
633
|
}
|
|
569
|
-
return finishLeaf(
|
|
634
|
+
return finishLeaf(
|
|
635
|
+
current,
|
|
636
|
+
i,
|
|
637
|
+
argv,
|
|
638
|
+
path,
|
|
639
|
+
opts,
|
|
640
|
+
collectOptionDefs(root, path),
|
|
641
|
+
forcePositionals,
|
|
642
|
+
);
|
|
570
643
|
}
|
|
571
644
|
}
|
|
572
645
|
|
|
@@ -578,7 +651,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
578
651
|
export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
|
|
579
652
|
if (pr.kind !== ParseKind.Ok) return pr;
|
|
580
653
|
|
|
581
|
-
|
|
654
|
+
const defs = [...(root.options ?? [])];
|
|
582
655
|
let node: CliNode = root;
|
|
583
656
|
|
|
584
657
|
for (const seg of pr.path) {
|
|
@@ -611,8 +684,15 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
|
|
|
611
684
|
node = ch;
|
|
612
685
|
}
|
|
613
686
|
|
|
687
|
+
const opts = { ...pr.opts };
|
|
614
688
|
for (const d of defs) {
|
|
615
|
-
if (d.
|
|
689
|
+
if (d.default !== undefined && !(d.name in opts)) {
|
|
690
|
+
opts[d.name] = d.default;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
for (const d of defs) {
|
|
695
|
+
if (d.required && !(d.name in opts)) {
|
|
616
696
|
return {
|
|
617
697
|
kind: ParseKind.Error,
|
|
618
698
|
path: pr.path,
|
|
@@ -626,7 +706,7 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
|
|
|
626
706
|
}
|
|
627
707
|
}
|
|
628
708
|
|
|
629
|
-
for (const [k, v] of Object.entries(
|
|
709
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
630
710
|
const d = findOptionByName(defs, k);
|
|
631
711
|
if (!d) {
|
|
632
712
|
return {
|
|
@@ -669,7 +749,29 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
|
|
|
669
749
|
};
|
|
670
750
|
}
|
|
671
751
|
}
|
|
752
|
+
if (d.kind === CliOptionKind.String && (d.format !== undefined || d.pattern !== undefined)) {
|
|
753
|
+
try {
|
|
754
|
+
validateFormatValue(v, d.format, d.pattern);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
const msg =
|
|
757
|
+
d.format !== undefined
|
|
758
|
+
? formatValidationError(d.format, v)
|
|
759
|
+
: err instanceof Error
|
|
760
|
+
? err.message
|
|
761
|
+
: String(err);
|
|
762
|
+
return {
|
|
763
|
+
kind: ParseKind.Error,
|
|
764
|
+
path: pr.path,
|
|
765
|
+
opts: {},
|
|
766
|
+
args: [],
|
|
767
|
+
helpExplicit: false,
|
|
768
|
+
helpPath: [],
|
|
769
|
+
errorMsg: `Invalid value for option --${k}: ${msg}`,
|
|
770
|
+
errorHelpPath: pr.path,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
672
774
|
}
|
|
673
775
|
|
|
674
|
-
return pr;
|
|
776
|
+
return { ...pr, opts };
|
|
675
777
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -2,26 +2,29 @@
|
|
|
2
2
|
This module runs parsed commands, help, errors, completion, and leaf handlers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { resolveCapabilities } from "./capabilities.ts";
|
|
6
5
|
import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
|
|
7
6
|
import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
|
|
8
|
-
import
|
|
9
|
-
import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
|
|
7
|
+
import { resolveCapabilities } from "./capabilities.ts";
|
|
10
8
|
import { CliContext } from "./context.ts";
|
|
11
9
|
import { cliHelpRender } from "./help.ts";
|
|
12
|
-
import { parse, postParseValidate
|
|
10
|
+
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
11
|
+
import type { CliRouter } from "./types.ts";
|
|
12
|
+
import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
|
|
13
13
|
import { cliValidateProgram } from "./validate.ts";
|
|
14
14
|
|
|
15
15
|
function cliRootMergedWithBuiltins(program: CliProgram): CliRouter {
|
|
16
16
|
return cliParseRoot(program);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function cliRun(
|
|
19
|
+
export async function cliRun(
|
|
20
|
+
program: CliProgram,
|
|
21
|
+
argv: string[] = process.argv.slice(2),
|
|
22
|
+
): Promise<never> {
|
|
20
23
|
try {
|
|
21
24
|
cliValidateProgram(program);
|
|
22
25
|
} catch (err) {
|
|
23
26
|
if (err instanceof Error) {
|
|
24
|
-
process.stderr.write(err.message
|
|
27
|
+
process.stderr.write(`${err.message}\n`);
|
|
25
28
|
} else {
|
|
26
29
|
process.stderr.write("Invalid CLI definition.\n");
|
|
27
30
|
}
|
|
@@ -31,12 +34,16 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
|
|
|
31
34
|
const caps = resolveCapabilities(program);
|
|
32
35
|
|
|
33
36
|
if (argv.length >= 1 && argv[0] === "mcp" && !caps.mcp) {
|
|
34
|
-
process.stderr.write(
|
|
37
|
+
process.stderr.write(
|
|
38
|
+
"MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n",
|
|
39
|
+
);
|
|
35
40
|
process.exit(1);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
if (argv.length >= 1 && argv[0] === "install" && !caps.install) {
|
|
39
|
-
process.stderr.write(
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
"install is disabled. Remove install.enabled: false from the program root.\n",
|
|
46
|
+
);
|
|
40
47
|
process.exit(1);
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -75,13 +82,16 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
|
|
|
75
82
|
if (pr.kind === "error") {
|
|
76
83
|
const color = process.stderr.isTTY;
|
|
77
84
|
const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
|
|
78
|
-
process.stderr.write(msg
|
|
85
|
+
process.stderr.write(`${msg}\n`);
|
|
79
86
|
process.stderr.write(cliHelpRender(cliPresentationRoot(program), pr.errorHelpPath, true));
|
|
80
87
|
process.exit(1);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
if (pr.kind === ParseKind.Ok) {
|
|
84
|
-
await dispatchBuiltin(program, pr, {
|
|
91
|
+
await dispatchBuiltin(program, pr, {
|
|
92
|
+
isLeafCompletionIntercept,
|
|
93
|
+
parseRoot: completionParseRoot,
|
|
94
|
+
});
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
let current: CliNode = parseRoot;
|
|
@@ -109,7 +119,7 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
|
|
|
109
119
|
process.exit(0);
|
|
110
120
|
} catch (err) {
|
|
111
121
|
if (err instanceof Error) {
|
|
112
|
-
process.stderr.write(err.message
|
|
122
|
+
process.stderr.write(`${err.message}\n`);
|
|
113
123
|
}
|
|
114
124
|
process.exit(1);
|
|
115
125
|
}
|
|
@@ -118,7 +128,7 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
|
|
|
118
128
|
export function cliErrWithHelp(ctx: CliContext, msg: string): never {
|
|
119
129
|
const color = process.stderr.isTTY;
|
|
120
130
|
const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
|
|
121
|
-
process.stderr.write(line
|
|
131
|
+
process.stderr.write(`${line}\n`);
|
|
122
132
|
process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.program), ctx.commandPath, true));
|
|
123
133
|
process.exit(1);
|
|
124
134
|
}
|
package/src/schema.ts
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
This module serializes the CLI schema tree to JSON for machine-readable introspection.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type
|
|
6
|
-
import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
|
|
5
|
+
import { type CliSchemaExport, exportPresentationBuiltins } from "./builtins/export.ts";
|
|
7
6
|
import { cliResolveNotes } from "./help.ts";
|
|
8
7
|
import { visibleOptions } from "./hidden.ts";
|
|
8
|
+
import {
|
|
9
|
+
type CliNode,
|
|
10
|
+
type CliProgram,
|
|
11
|
+
isCliLeaf,
|
|
12
|
+
isCliRouter,
|
|
13
|
+
leafOutputSchema,
|
|
14
|
+
} from "./types.ts";
|
|
9
15
|
|
|
10
16
|
const RESERVED = new Set(["completion", "install", "docs", "mcp", "version"]);
|
|
11
17
|
|
|
@@ -60,8 +66,8 @@ function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport | null {
|
|
|
60
66
|
/** Resolves `{argsbarg:program}` in exported notes using the root program key. */
|
|
61
67
|
function resolveSchemaNotes(node: CliSchemaExport, appKey: string): CliSchemaExport {
|
|
62
68
|
const out: CliSchemaExport = { ...node };
|
|
63
|
-
if ((out.notes ?? "").length > 0) {
|
|
64
|
-
out.notes = cliResolveNotes(out.notes
|
|
69
|
+
if ((out.notes ?? "").length > 0 && out.notes !== undefined) {
|
|
70
|
+
out.notes = cliResolveNotes(out.notes, appKey);
|
|
65
71
|
}
|
|
66
72
|
if (out.commands) {
|
|
67
73
|
out.commands = out.commands.map((ch) => resolveSchemaNotes(ch, appKey));
|
|
@@ -83,7 +89,7 @@ export function cliSchemaExport(root: CliProgram): CliSchemaExport {
|
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
export function cliSchemaJson(root: CliProgram): string {
|
|
86
|
-
return JSON.stringify(cliSchemaExport(root), null, 2)
|
|
92
|
+
return `${JSON.stringify(cliSchemaExport(root), null, 2)}\n`;
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
export type { CliSchemaExport };
|
package/src/skill/generate.ts
CHANGED
|
@@ -3,9 +3,9 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { generateApiGuide } from "../docs/api-guide.ts";
|
|
6
|
+
import { collectMcpTools, type McpToolDef, sanitizeToolSegment } from "../mcp/tools.ts";
|
|
6
7
|
import { collectOptionDefs } from "../parse.ts";
|
|
7
|
-
import {
|
|
8
|
-
import { CliProgram, CliOptionKind } from "../types.ts";
|
|
8
|
+
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
9
9
|
|
|
10
10
|
export type SkillTarget = "cursor" | "claude";
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ export interface SkillBundle {
|
|
|
18
18
|
/** Truncates text to maxLen with ellipsis. */
|
|
19
19
|
function truncate(text: string, maxLen: number): string {
|
|
20
20
|
if (text.length <= maxLen) return text;
|
|
21
|
-
return text.slice(0, maxLen - 1)
|
|
21
|
+
return `${text.slice(0, maxLen - 1)}…`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Builds third-person skill description for YAML frontmatter. */
|
|
@@ -54,7 +54,7 @@ function formatCommandEntry(root: CliProgram, tool: McpToolDef): string {
|
|
|
54
54
|
}
|
|
55
55
|
const enums = opts.filter((o) => o.kind === CliOptionKind.Enum && o.choices?.length);
|
|
56
56
|
for (const e of enums) {
|
|
57
|
-
line += ` (\`--${e.name}\`: ${e.choices
|
|
57
|
+
line += ` (\`--${e.name}\`: ${e.choices?.join(" | ")})`;
|
|
58
58
|
}
|
|
59
59
|
const varargs = (tool.leaf.positionals ?? []).filter((p) => (p.argMax ?? 1) === 0);
|
|
60
60
|
if (varargs.length > 0) {
|
package/src/skill/install.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { CliProgram } from "../types.ts";
|
|
4
|
+
import type { CliProgram } from "../types.ts";
|
|
5
5
|
import { generateSkillBundle, type SkillTarget } from "./generate.ts";
|
|
6
6
|
import { applySkillInstallHints } from "./hint.ts";
|
|
7
7
|
|
|
@@ -25,7 +25,11 @@ function resolveSkillDir(target: SkillTarget, dirName: string, global: boolean):
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Writes SKILL.md and reference.md; returns changed file paths. */
|
|
28
|
-
export function cliSkillInstall(
|
|
28
|
+
export function cliSkillInstall(
|
|
29
|
+
root: CliProgram,
|
|
30
|
+
target: SkillTarget,
|
|
31
|
+
opts: SkillInstallOpts,
|
|
32
|
+
): string[] {
|
|
29
33
|
const bundle = generateSkillBundle(root, target);
|
|
30
34
|
const { skillMd, referenceMd } = applySkillInstallHints(root, bundle.skillMd, bundle.referenceMd);
|
|
31
35
|
const dir = resolveSkillDir(target, bundle.dirName, opts.global ?? false);
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,21 @@ export enum CliOptionKind {
|
|
|
25
25
|
Enum = "enum",
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Named validation/coercion for string options (`format` on `CliOption`).
|
|
30
|
+
* Positionals do not use `format`; varargs use space-separated CLI tokens and JSON arrays over MCP.
|
|
31
|
+
*/
|
|
32
|
+
export enum CliValueFormat {
|
|
33
|
+
/** Duration text such as `30s`, `20m`, `1h`, `2d` (default unit minutes when omitted). */
|
|
34
|
+
Duration = "duration",
|
|
35
|
+
/** Comma-separated list on a single option value (`--services a,b`). */
|
|
36
|
+
CommaList = "comma-list",
|
|
37
|
+
/** Calendar date `YYYY-MM-DD`. */
|
|
38
|
+
Date = "date",
|
|
39
|
+
/** RFC 3339 instant with `Z` or numeric offset. */
|
|
40
|
+
DateTime = "date-time",
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
/**
|
|
29
44
|
* When `fallbackCommand` is used for missing or unknown subcommand tokens at a routing node.
|
|
30
45
|
*/
|
|
@@ -65,6 +80,15 @@ export interface CliOption {
|
|
|
65
80
|
* Must be a non-empty array of distinct non-empty strings.
|
|
66
81
|
*/
|
|
67
82
|
choices?: string[];
|
|
83
|
+
/**
|
|
84
|
+
* Named string validation for `kind: String` options. Mutually exclusive with `pattern`.
|
|
85
|
+
* Not supported on positionals.
|
|
86
|
+
*/
|
|
87
|
+
format?: CliValueFormat;
|
|
88
|
+
/** Default value applied in post-parse when the option is omitted. */
|
|
89
|
+
default?: string;
|
|
90
|
+
/** Regex pattern for string options. Mutually exclusive with `format`. */
|
|
91
|
+
pattern?: string;
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
/**
|
package/src/validate.ts
CHANGED
|
@@ -3,17 +3,19 @@ This module validates CLI schemas before execution.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { reservedCommandNames, resolveCapabilities } from "./capabilities.ts";
|
|
6
|
+
import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
|
|
7
|
+
import { validateFormatValue } from "./formats.ts";
|
|
8
|
+
import { resolveMcpSchemaUri } from "./mcp/tools.ts";
|
|
6
9
|
import {
|
|
7
10
|
type CliLeaf,
|
|
8
11
|
type CliNode,
|
|
9
|
-
type CliProgram,
|
|
10
12
|
CliOptionKind,
|
|
13
|
+
type CliProgram,
|
|
11
14
|
CliSchemaValidationError,
|
|
15
|
+
CliValueFormat,
|
|
12
16
|
isCliLeaf,
|
|
13
17
|
isCliRouter,
|
|
14
18
|
} from "./types.ts";
|
|
15
|
-
import { resolveMcpSchemaUri } from "./mcp/tools.ts";
|
|
16
|
-
import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
|
|
17
19
|
|
|
18
20
|
/** Validates `docs` configuration on the program root. */
|
|
19
21
|
function validateDocsConfig(docs: import("./types.ts").CliDocsConfig): void {
|
|
@@ -93,17 +95,17 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
93
95
|
const rogue = node as CliProgram;
|
|
94
96
|
if (rogue.mcpServer !== undefined) {
|
|
95
97
|
throw new CliSchemaValidationError(
|
|
96
|
-
|
|
98
|
+
`mcpServer is only supported on the program root (not on ${node.key})`,
|
|
97
99
|
);
|
|
98
100
|
}
|
|
99
101
|
if (rogue.install !== undefined) {
|
|
100
102
|
throw new CliSchemaValidationError(
|
|
101
|
-
|
|
103
|
+
`install is only supported on the program root (not on ${node.key})`,
|
|
102
104
|
);
|
|
103
105
|
}
|
|
104
106
|
if (rogue.docs !== undefined) {
|
|
105
107
|
throw new CliSchemaValidationError(
|
|
106
|
-
|
|
108
|
+
`docs is only supported on the program root (not on ${node.key})`,
|
|
107
109
|
);
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -115,9 +117,7 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
115
117
|
const outputSchema = node.outputSchema;
|
|
116
118
|
const legacyOutputSchema = node.mcpTool?.outputSchema;
|
|
117
119
|
if (outputSchema !== undefined && legacyOutputSchema !== undefined) {
|
|
118
|
-
throw new CliSchemaValidationError(
|
|
119
|
-
"Set outputSchema on the leaf only, not under mcpTool",
|
|
120
|
-
);
|
|
120
|
+
throw new CliSchemaValidationError("Set outputSchema on the leaf only, not under mcpTool");
|
|
121
121
|
}
|
|
122
122
|
const resolved = outputSchema ?? legacyOutputSchema;
|
|
123
123
|
if (
|
|
@@ -132,7 +132,7 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
132
132
|
const rogue = node as unknown as CliLeaf;
|
|
133
133
|
if (rogue.mcpTool !== undefined) {
|
|
134
134
|
throw new CliSchemaValidationError(
|
|
135
|
-
|
|
135
|
+
`mcpTool is only supported on leaf commands (not on ${node.key})`,
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
@@ -160,9 +160,7 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
if (node.fallbackMode !== undefined && node.fallbackCommand === undefined) {
|
|
163
|
-
throw new CliSchemaValidationError(
|
|
164
|
-
`fallbackMode requires fallbackCommand on '${node.key}'`,
|
|
165
|
-
);
|
|
163
|
+
throw new CliSchemaValidationError(`fallbackMode requires fallbackCommand on '${node.key}'`);
|
|
166
164
|
}
|
|
167
165
|
|
|
168
166
|
if (node.fallbackCommand !== undefined) {
|
|
@@ -230,13 +228,70 @@ function validateOptions(scopeKey: string, options: import("./types.ts").CliOpti
|
|
|
230
228
|
`Option '${opt.name}' on '${scopeKey}': choices is only valid for Enum kind`,
|
|
231
229
|
);
|
|
232
230
|
}
|
|
231
|
+
|
|
232
|
+
if (opt.format !== undefined || opt.pattern !== undefined || opt.default !== undefined) {
|
|
233
|
+
validateOptionValueMetadata(scopeKey, opt);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function validateOptionValueMetadata(scopeKey: string, opt: import("./types.ts").CliOption): void {
|
|
239
|
+
const label = `${scopeKey}/${opt.name}`;
|
|
240
|
+
|
|
241
|
+
if (opt.default !== undefined) {
|
|
242
|
+
if (opt.kind === CliOptionKind.Presence) {
|
|
243
|
+
throw new CliSchemaValidationError(`default is not valid on presence option ${label}`);
|
|
244
|
+
}
|
|
245
|
+
if (opt.required) {
|
|
246
|
+
throw new CliSchemaValidationError(`default cannot be set on required option ${label}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (opt.format !== undefined && opt.pattern !== undefined) {
|
|
251
|
+
throw new CliSchemaValidationError(
|
|
252
|
+
`Option ${label}: format and pattern are mutually exclusive`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (opt.format !== undefined) {
|
|
257
|
+
if (opt.kind !== CliOptionKind.String) {
|
|
258
|
+
throw new CliSchemaValidationError(`Option ${label}: format is only valid on String kind`);
|
|
259
|
+
}
|
|
260
|
+
if (!Object.values(CliValueFormat).includes(opt.format)) {
|
|
261
|
+
throw new CliSchemaValidationError(`Option ${label}: unknown format '${opt.format}'`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (opt.pattern !== undefined) {
|
|
266
|
+
if (opt.kind !== CliOptionKind.String) {
|
|
267
|
+
throw new CliSchemaValidationError(`Option ${label}: pattern is only valid on String kind`);
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
new RegExp(opt.pattern);
|
|
271
|
+
} catch {
|
|
272
|
+
throw new CliSchemaValidationError(`Option ${label}: invalid pattern regex`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (opt.default !== undefined) {
|
|
277
|
+
try {
|
|
278
|
+
validateFormatValue(opt.default, opt.format, opt.pattern);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
281
|
+
throw new CliSchemaValidationError(`Option ${label}: invalid default: ${msg}`);
|
|
282
|
+
}
|
|
233
283
|
}
|
|
234
284
|
}
|
|
235
285
|
|
|
236
|
-
function validatePositionals(
|
|
286
|
+
function validatePositionals(
|
|
287
|
+
scopeKey: string,
|
|
288
|
+
positionals: import("./types.ts").CliPositional[],
|
|
289
|
+
): void {
|
|
237
290
|
for (const p of positionals) {
|
|
238
291
|
if (p.argMin !== undefined && p.argMin < 0) {
|
|
239
|
-
throw new CliSchemaValidationError(
|
|
292
|
+
throw new CliSchemaValidationError(
|
|
293
|
+
`argMin must be >= 0 for positional ${scopeKey}/${p.name}`,
|
|
294
|
+
);
|
|
240
295
|
}
|
|
241
296
|
if (p.argMax !== undefined && p.argMax < 0) {
|
|
242
297
|
throw new CliSchemaValidationError(
|
|
@@ -262,7 +317,11 @@ function validatePositionals(scopeKey: string, positionals: import("./types.ts")
|
|
|
262
317
|
}
|
|
263
318
|
|
|
264
319
|
for (let idx = 0; idx < positionals.length; idx++) {
|
|
265
|
-
const
|
|
320
|
+
const positional = positionals[idx];
|
|
321
|
+
if (!positional) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const { argMax = 1 } = positional;
|
|
266
325
|
if (argMax === 0 && idx + 1 < positionals.length) {
|
|
267
326
|
throw new CliSchemaValidationError(
|
|
268
327
|
`Unlimited positional (argMax == 0) must be last in scope ${scopeKey}`,
|