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.
Files changed (73) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +24 -8
  3. package/biome.json +29 -6
  4. package/bun.lock +22 -0
  5. package/docs/cli-program.md +75 -1
  6. package/docs/install.md +1 -1
  7. package/docs/mcp.md +45 -1
  8. package/docs/templates/cursor/rules/cli-program.mdc +15 -21
  9. package/index.d.ts +95 -50
  10. package/justfile +24 -6
  11. package/package.json +4 -2
  12. package/scripts/release.ts +26 -9
  13. package/src/builtins/builtins.test.ts +9 -4
  14. package/src/builtins/completion-bash.ts +74 -50
  15. package/src/builtins/completion-fish.ts +3 -8
  16. package/src/builtins/completion-group.ts +1 -1
  17. package/src/builtins/completion-zsh.ts +80 -42
  18. package/src/builtins/dispatch.ts +20 -16
  19. package/src/builtins/export.ts +19 -10
  20. package/src/builtins/index.ts +9 -4
  21. package/src/builtins/install.ts +10 -10
  22. package/src/builtins/mcp.ts +1 -1
  23. package/src/builtins/presentation.ts +8 -8
  24. package/src/builtins/scopes.ts +1 -1
  25. package/src/builtins/version.ts +1 -1
  26. package/src/completion.ts +4 -4
  27. package/src/context.ts +91 -15
  28. package/src/docs/api-guide.test.ts +2 -2
  29. package/src/docs/api-guide.ts +19 -5
  30. package/src/docs/builtin.ts +27 -8
  31. package/src/docs/docs.test.ts +18 -11
  32. package/src/docs/mcp-guide.ts +108 -25
  33. package/src/docs/resolve.ts +10 -3
  34. package/src/docs/save.ts +11 -3
  35. package/src/formats.test.ts +35 -0
  36. package/src/formats.ts +135 -0
  37. package/src/headless.test.ts +8 -16
  38. package/src/help.ts +73 -43
  39. package/src/hidden-mcpb.test.ts +7 -6
  40. package/src/hidden.ts +2 -2
  41. package/src/index.test.ts +120 -96
  42. package/src/index.ts +36 -24
  43. package/src/install/binary.ts +12 -5
  44. package/src/install/completions.ts +7 -3
  45. package/src/install/detect-installed.ts +29 -4
  46. package/src/install/gh-release-update.ts +31 -23
  47. package/src/install/index.ts +69 -19
  48. package/src/install/install.test.ts +31 -8
  49. package/src/install/mcp-codex.test.ts +57 -0
  50. package/src/install/mcp-codex.ts +125 -0
  51. package/src/install/mcp-config.ts +12 -5
  52. package/src/install/mcp-opencode.test.ts +98 -0
  53. package/src/install/mcp-opencode.ts +149 -0
  54. package/src/install/paths.ts +29 -3
  55. package/src/install/plan.ts +73 -6
  56. package/src/install/shell.ts +1 -4
  57. package/src/install/status.ts +12 -6
  58. package/src/install/uninstall.ts +38 -4
  59. package/src/install/update.test.ts +2 -2
  60. package/src/install/update.ts +3 -1
  61. package/src/invoke.ts +12 -9
  62. package/src/mcp/bundle.ts +36 -8
  63. package/src/mcp/env.ts +7 -13
  64. package/src/mcp/server.ts +12 -6
  65. package/src/mcp/tools.ts +83 -18
  66. package/src/mcp.ts +3 -3
  67. package/src/parse.ts +129 -27
  68. package/src/runtime.ts +22 -12
  69. package/src/schema.ts +11 -5
  70. package/src/skill/generate.ts +4 -4
  71. package/src/skill/install.ts +6 -2
  72. package/src/types.ts +24 -0
  73. 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 { CliContext } from "./context.ts";
10
+ import { formatValidationError, validateFormatValue } from "./formats.ts";
11
11
  import {
12
- type CliLeaf,
13
- CliNode,
14
12
  CliFallbackMode,
15
- CliOption,
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 { report: { err: null, stoppedOnUnknown: false, sawDoubleDash: true }, nextIndex: idx };
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 === "") return { report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false }, nextIndex: idx };
200
- if (err) return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
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 === "") return { report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false }, nextIndex: idx };
204
- if (err) return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
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
- let defs = [...(root.options ?? [])];
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 { kind: ParseKind.Ok, path, opts, args, helpExplicit: false, helpPath: [], errorMsg: "", errorHelpPath: [] };
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 { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
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
- root.fallbackCommand !== undefined &&
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 = root.fallbackCommand!;
469
+ cmdName = fallbackCommand;
438
470
  node = findChild(root.commands, cmdName);
439
471
  if (!node) {
440
- return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
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 ? `Expected subcommand but got positional: ${cmdName}` : `Unknown command: ${cmdName}`,
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
- let current = node!;
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(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
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 ? `Expected subcommand but got positional: ${tok}` : `Unknown subcommand: ${tok}`,
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(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
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
- let defs = [...(root.options ?? [])];
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.required && !(d.name in pr.opts)) {
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(pr.opts)) {
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 type { CliRouter } from "./types.ts";
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, ParseKind } from "./parse.ts";
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(program: CliProgram, argv: string[] = process.argv.slice(2)): Promise<never> {
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 + "\n");
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("MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n");
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("install is disabled. Remove install.enabled: false from the program root.\n");
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 + "\n");
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, { isLeafCompletionIntercept, parseRoot: completionParseRoot });
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 + "\n");
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 + "\n");
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 CliNode, type CliProgram, isCliLeaf, isCliRouter, leafOutputSchema } from "./types.ts";
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!, appKey);
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) + "\n";
92
+ return `${JSON.stringify(cliSchemaExport(root), null, 2)}\n`;
87
93
  }
88
94
 
89
95
  export type { CliSchemaExport };
@@ -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 { collectMcpTools, sanitizeToolSegment, type McpToolDef } from "../mcp/tools.ts";
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!.join(" | ")})`;
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) {
@@ -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(root: CliProgram, target: SkillTarget, opts: SkillInstallOpts): string[] {
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
- "mcpServer is only supported on the program root (not on " + node.key + ")",
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
- "install is only supported on the program root (not on " + node.key + ")",
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
- "docs is only supported on the program root (not on " + node.key + ")",
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
- "mcpTool is only supported on leaf commands (not on " + node.key + ")",
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(scopeKey: string, positionals: import("./types.ts").CliPositional[]): void {
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(`argMin must be >= 0 for positional ${scopeKey}/${p.name}`);
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 { argMax = 1 } = positionals[idx]!;
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}`,