argsbarg 1.4.3 → 2.0.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 (57) hide show
  1. package/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
  2. package/.private/scratch.md +1 -1
  3. package/CHANGELOG.md +39 -1
  4. package/README.md +29 -21
  5. package/docs/ai-skills.md +24 -52
  6. package/docs/install.md +84 -0
  7. package/docs/mcp.md +8 -8
  8. package/examples/mcp-test.ts +3 -3
  9. package/examples/minimal.ts +3 -3
  10. package/examples/nested.ts +3 -3
  11. package/examples/option-required.ts +3 -3
  12. package/index.d.ts +44 -50
  13. package/package.json +1 -1
  14. package/src/builtins/builtins.test.ts +101 -0
  15. package/src/builtins/completion-bash.ts +240 -0
  16. package/src/builtins/completion-fish.ts +73 -0
  17. package/src/builtins/completion-group.ts +50 -0
  18. package/src/builtins/completion-zsh.ts +244 -0
  19. package/src/builtins/dispatch.ts +138 -0
  20. package/src/builtins/export.ts +53 -0
  21. package/src/builtins/index.ts +10 -0
  22. package/src/builtins/install.ts +99 -0
  23. package/src/builtins/mcp.ts +13 -0
  24. package/src/builtins/presentation.ts +50 -0
  25. package/src/builtins/scopes.ts +46 -0
  26. package/src/builtins/shell-helpers.ts +24 -0
  27. package/src/capabilities.ts +32 -0
  28. package/src/completion.ts +10 -693
  29. package/src/context.ts +21 -6
  30. package/src/help.ts +21 -9
  31. package/src/index.test.ts +114 -118
  32. package/src/index.ts +2 -1
  33. package/src/install/binary.ts +82 -0
  34. package/src/install/compiled.ts +15 -0
  35. package/src/install/completions.ts +52 -0
  36. package/src/install/detect-installed.ts +67 -0
  37. package/src/install/index.ts +196 -0
  38. package/src/install/install.test.ts +124 -0
  39. package/src/install/mcp-config.ts +70 -0
  40. package/src/install/paths.ts +69 -0
  41. package/src/install/plan.ts +183 -0
  42. package/src/install/shell.ts +56 -0
  43. package/src/install/status.ts +63 -0
  44. package/src/install/uninstall.ts +111 -0
  45. package/src/invoke.ts +14 -5
  46. package/src/mcp/server.ts +3 -3
  47. package/src/mcp/tools.ts +17 -17
  48. package/src/mcp.ts +2 -2
  49. package/src/parse.ts +55 -27
  50. package/src/runtime.ts +47 -100
  51. package/src/schema.ts +10 -52
  52. package/src/skill/generate.ts +10 -10
  53. package/src/skill/install.ts +21 -19
  54. package/src/types.test.ts +40 -0
  55. package/src/types.ts +59 -49
  56. package/src/validate.ts +89 -83
  57. package/src/ai.ts +0 -7
package/src/parse.ts CHANGED
@@ -9,10 +9,13 @@ across every entry path.
9
9
 
10
10
  import { CliContext } from "./context.ts";
11
11
  import {
12
- CliCommand,
12
+ type CliLeaf,
13
+ CliNode,
13
14
  CliFallbackMode,
14
15
  CliOption,
15
16
  CliOptionKind,
17
+ isCliLeaf,
18
+ isCliRouter,
16
19
  } from "./types.ts";
17
20
  import { fullStringIsDouble } from "./utils.ts";
18
21
 
@@ -69,7 +72,7 @@ function isSchemaTok(tok: string): boolean {
69
72
  }
70
73
 
71
74
  /** Looks up a subcommand or routing node by `key`. */
72
- function findChild(cmds: CliCommand[], name: string): CliCommand | undefined {
75
+ function findChild(cmds: CliNode[], name: string): CliNode | undefined {
73
76
  return cmds.find((c) => c.key === name);
74
77
  }
75
78
 
@@ -217,15 +220,16 @@ function consumeOptions(
217
220
  // ── Positional Collection ─────────────────────────────────────────────────────
218
221
 
219
222
  /** Merges option defs from the program root along the routed command path. */
220
- export function collectOptionDefs(root: CliCommand, path: string[]): CliOption[] {
223
+ export function collectOptionDefs(root: CliNode, path: string[]): CliOption[] {
221
224
  let defs = [...(root.options ?? [])];
222
- let cmds = root.commands ?? [];
225
+ let node: CliNode = root;
223
226
 
224
227
  for (const seg of path) {
225
- const ch = findChild(cmds, seg);
228
+ if (!isCliRouter(node)) break;
229
+ const ch = findChild(node.commands, seg);
226
230
  if (!ch) break;
227
231
  defs.push(...(ch.options ?? []));
228
- cmds = ch.commands ?? [];
232
+ node = ch;
229
233
  }
230
234
 
231
235
  return defs;
@@ -233,7 +237,7 @@ export function collectOptionDefs(root: CliCommand, path: string[]): CliOption[]
233
237
 
234
238
  /** Fills `args` for a leaf from `startIdx` according to `node.positionals`. */
235
239
  function finishLeaf(
236
- node: CliCommand,
240
+ node: CliLeaf,
237
241
  startIdx: number,
238
242
  argv: string[],
239
243
  path: string[],
@@ -384,14 +388,16 @@ function schemaResult(): ParseResult {
384
388
  /**
385
389
  * Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
386
390
  */
387
- export function parse(root: CliCommand, argv: string[]): ParseResult {
391
+ export function parse(root: CliNode, argv: string[]): ParseResult {
388
392
  let i = 0;
389
393
  const path: string[] = [];
390
394
  const opts: Record<string, string> = {};
391
395
 
392
396
  const rootLenient =
397
+ isCliRouter(root) &&
393
398
  root.fallbackCommand !== undefined &&
394
- ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown || (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.UnknownOnly);
399
+ ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown ||
400
+ (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.UnknownOnly);
395
401
 
396
402
  // Consume root-level options first
397
403
  const rootRep = consumeOptions(root.options ?? [], rootLenient, argv, i, opts);
@@ -420,16 +426,20 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
420
426
 
421
427
  // Determine which subcommand to route to
422
428
  let cmdName: string;
423
- let node: CliCommand | undefined;
429
+ let node: CliNode | undefined;
424
430
 
425
- if (root.handler) {
426
- return finishLeaf(root as CliCommand, i, argv, path, opts, root.options ?? [], forcePositionals);
431
+ if (isCliLeaf(root)) {
432
+ return finishLeaf(root, i, argv, path, opts, root.options ?? [], forcePositionals);
427
433
  }
428
434
 
429
435
  if (i >= argv.length) {
430
- if (root.fallbackCommand !== undefined && ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOnly || (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown)) {
436
+ if (
437
+ root.fallbackCommand !== undefined &&
438
+ ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOnly ||
439
+ (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown)
440
+ ) {
431
441
  cmdName = root.fallbackCommand;
432
- node = findChild(root.commands ?? [], cmdName);
442
+ node = findChild(root.commands, cmdName);
433
443
  if (!node) {
434
444
  return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
435
445
  }
@@ -438,7 +448,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
438
448
  }
439
449
  } else {
440
450
  const peek = argv[i];
441
- const childPick = !forcePositionals ? findChild(root.commands ?? [], peek) : undefined;
451
+ const childPick = !forcePositionals ? findChild(root.commands, peek) : undefined;
442
452
 
443
453
  if (childPick !== undefined) {
444
454
  cmdName = peek;
@@ -452,14 +462,14 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
452
462
 
453
463
  if (canRouteUnknown) {
454
464
  cmdName = root.fallbackCommand!;
455
- node = findChild(root.commands ?? [], cmdName);
465
+ node = findChild(root.commands, cmdName);
456
466
  if (!node) {
457
467
  return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
458
468
  }
459
469
  } else {
460
470
  cmdName = peek;
461
471
  if (!forcePositionals) i += 1;
462
- node = findChild(root.commands ?? [], cmdName);
472
+ node = findChild(root.commands, cmdName);
463
473
  if (!node) {
464
474
  return {
465
475
  kind: ParseKind.Error,
@@ -506,14 +516,14 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
506
516
  }
507
517
 
508
518
  if (i >= argv.length) {
509
- if ((current.commands ?? []).length > 0) {
519
+ if (isCliRouter(current) && current.commands.length > 0) {
510
520
  const fb = current.fallbackCommand;
511
521
  const fm = current.fallbackMode ?? CliFallbackMode.MissingOnly;
512
522
  if (
513
523
  fb !== undefined &&
514
524
  (fm === CliFallbackMode.MissingOnly || fm === CliFallbackMode.MissingOrUnknown)
515
525
  ) {
516
- const fbNode = findChild(current.commands ?? [], fb);
526
+ const fbNode = findChild(current.commands, fb);
517
527
  if (fbNode) {
518
528
  path.push(fb);
519
529
  current = fbNode;
@@ -522,6 +532,9 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
522
532
  }
523
533
  return helpResult(path, false);
524
534
  }
535
+ if (!isCliLeaf(current)) {
536
+ return helpResult(path, false);
537
+ }
525
538
  return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
526
539
  }
527
540
 
@@ -539,8 +552,8 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
539
552
  };
540
553
  }
541
554
 
542
- if (!forcePositionals) {
543
- const childOpt = findChild(current.commands ?? [], tok);
555
+ if (!forcePositionals && isCliRouter(current)) {
556
+ const childOpt = findChild(current.commands, tok);
544
557
  if (childOpt !== undefined) {
545
558
  i += 1;
546
559
  path.push(tok);
@@ -549,7 +562,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
549
562
  }
550
563
  }
551
564
 
552
- if ((current.commands ?? []).length > 0) {
565
+ if (isCliRouter(current) && current.commands.length > 0) {
553
566
  const fb = current.fallbackCommand;
554
567
  const fm = current.fallbackMode ?? CliFallbackMode.MissingOnly;
555
568
  const canRouteUnknown =
@@ -557,7 +570,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
557
570
  (fm === CliFallbackMode.MissingOrUnknown || fm === CliFallbackMode.UnknownOnly);
558
571
 
559
572
  if (canRouteUnknown) {
560
- const fbNode = findChild(current.commands ?? [], fb!);
573
+ const fbNode = findChild(current.commands, fb!);
561
574
  if (fbNode) {
562
575
  path.push(fb!);
563
576
  current = fbNode;
@@ -577,6 +590,9 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
577
590
  };
578
591
  }
579
592
 
593
+ if (!isCliLeaf(current)) {
594
+ return helpResult(path, false);
595
+ }
580
596
  return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
581
597
  }
582
598
  }
@@ -586,14 +602,26 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
586
602
  /**
587
603
  * Validates option keys and numeric values for an Ok parse, merging in-scope options along `pr.path`.
588
604
  */
589
- export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResult {
605
+ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
590
606
  if (pr.kind !== ParseKind.Ok) return pr;
591
607
 
592
608
  let defs = [...(root.options ?? [])];
593
- let cmds = root.commands ?? [];
609
+ let node: CliNode = root;
594
610
 
595
611
  for (const seg of pr.path) {
596
- const ch = findChild(cmds, seg);
612
+ if (!isCliRouter(node)) {
613
+ return {
614
+ kind: ParseKind.Error,
615
+ path: pr.path,
616
+ opts: {},
617
+ args: [],
618
+ helpExplicit: false,
619
+ helpPath: [],
620
+ errorMsg: "Internal path error",
621
+ errorHelpPath: pr.path,
622
+ };
623
+ }
624
+ const ch = findChild(node.commands, seg);
597
625
  if (!ch) {
598
626
  return {
599
627
  kind: ParseKind.Error,
@@ -607,7 +635,7 @@ export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResul
607
635
  };
608
636
  }
609
637
  defs.push(...(ch.options ?? []));
610
- cmds = ch.commands ?? [];
638
+ node = ch;
611
639
  }
612
640
 
613
641
  for (const d of defs) {
package/src/runtime.ts CHANGED
@@ -1,41 +1,26 @@
1
1
  /*
2
2
  This module runs parsed commands, help, errors, completion, and leaf handlers.
3
- It owns the top-level control flow after parsing, including validation failures,
4
- shell completion dispatch, and leaf handler invocation.
5
-
6
- It keeps execution flow out of the public barrel so the exported API stays small and
7
- the runtime responsibilities remain easy to reason about.
8
3
  */
9
4
 
10
- import { cliBuiltinAiGroup, cliBuiltinCompletionGroup, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
5
+ import { resolveCapabilities } from "./capabilities.ts";
6
+ import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
7
+ import { cliPresentationRoot } from "./builtins/presentation.ts";
8
+ import type { CliRouter } from "./types.ts";
9
+ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
10
+ import { isCompiledExecutable } from "./install/compiled.ts";
11
11
  import { CliContext } from "./context.ts";
12
12
  import { cliHelpRender } from "./help.ts";
13
- import { cliSkillInstall } from "./skill/install.ts";
14
- import { cliMcpServeStdio } from "./mcp.ts";
15
13
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
16
14
  import { cliSchemaJson } from "./schema.ts";
17
- import { CliCommand } from "./types.ts";
18
- import { cliValidateRoot } from "./validate.ts";
19
-
20
- /**
21
- * Merges the caller's program root with the reserved `completion` subtree.
22
- */
23
- function cliRootMergedWithBuiltins(root: CliCommand): CliCommand {
24
- if (root.handler) {
25
- return root;
26
- }
27
- return cliPresentationRoot(root);
15
+ import { cliValidateProgram } from "./validate.ts";
16
+
17
+ function cliRootMergedWithBuiltins(program: CliProgram): CliRouter {
18
+ return cliPresentationRoot(program);
28
19
  }
29
20
 
30
- /**
31
- * Validates the schema, parses argv, prints help or errors, runs completion or the leaf handler, then exits.
32
- *
33
- * @param root The root CliCommand.
34
- * @param argv Override the default argv (process.argv.slice(2)).
35
- */
36
- export async function cliRun(root: CliCommand, argv: string[] = process.argv.slice(2)): Promise<never> {
21
+ export async function cliRun(program: CliProgram, argv: string[] = process.argv.slice(2)): Promise<never> {
37
22
  try {
38
- cliValidateRoot(root);
23
+ cliValidateProgram(program);
39
24
  } catch (err) {
40
25
  if (err instanceof Error) {
41
26
  process.stderr.write(err.message + "\n");
@@ -45,41 +30,47 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
45
30
  process.exit(1);
46
31
  }
47
32
 
48
- if (argv.length >= 2 && argv[0] === "ai" && argv[1] === "mcp" && !root.mcpServer) {
33
+ const caps = resolveCapabilities(program);
34
+
35
+ if (argv.length >= 1 && argv[0] === "mcp" && !caps.mcp) {
49
36
  process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
50
37
  process.exit(1);
51
38
  }
52
39
 
53
- let parseRoot = root;
40
+ if (argv.length >= 1 && argv[0] === "install" && !isCompiledExecutable()) {
41
+ process.stderr.write("install is only available in compiled binaries (bun build --compile).\n");
42
+ process.exit(1);
43
+ }
44
+
45
+ let parseRoot: CliNode;
46
+ let completionParseRoot: CliRouter = cliRootMergedWithBuiltins(program);
54
47
  let isLeafCompletionIntercept = false;
55
48
 
56
- if (root.handler && argv.length >= 1 && argv[0] === "ai") {
57
- parseRoot = {
58
- key: root.key,
59
- description: root.description,
60
- commands: [cliBuiltinAiGroup(root)],
61
- } as CliCommand;
62
- } else if (root.handler && argv.length >= 1 && argv[0] === "completion") {
63
- isLeafCompletionIntercept = true;
64
- parseRoot = {
65
- key: root.key,
66
- description: root.description,
67
- commands: [cliBuiltinCompletionGroup(root.key)],
68
- } as any;
49
+ if (isCliLeaf(program)) {
50
+ const intercept = builtinInterceptRoot(program, argv);
51
+ if (intercept.isLeafCompletionIntercept || intercept.parseRoot !== program) {
52
+ parseRoot = intercept.parseRoot;
53
+ completionParseRoot = isCliRouter(intercept.parseRoot)
54
+ ? intercept.parseRoot
55
+ : cliRootMergedWithBuiltins(program);
56
+ isLeafCompletionIntercept = intercept.isLeafCompletionIntercept;
57
+ } else {
58
+ parseRoot = program;
59
+ }
69
60
  } else {
70
- parseRoot = cliRootMergedWithBuiltins(root);
61
+ parseRoot = cliRootMergedWithBuiltins(program);
71
62
  }
72
63
 
73
64
  let pr = parse(parseRoot, argv);
74
65
  pr = postParseValidate(parseRoot, pr);
75
66
 
76
67
  if (pr.kind === ParseKind.Help) {
77
- process.stdout.write(cliHelpRender(cliPresentationRoot(root), pr.helpPath, false));
68
+ process.stdout.write(cliHelpRender(cliPresentationRoot(program), pr.helpPath, false));
78
69
  process.exit(pr.helpExplicit ? 0 : 1);
79
70
  }
80
71
 
81
72
  if (pr.kind === ParseKind.Schema) {
82
- process.stdout.write(cliSchemaJson(root));
73
+ process.stdout.write(cliSchemaJson(program));
83
74
  process.exit(0);
84
75
  }
85
76
 
@@ -87,62 +78,21 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
87
78
  const color = process.stderr.isTTY;
88
79
  const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
89
80
  process.stderr.write(msg + "\n");
90
- process.stderr.write(cliHelpRender(cliPresentationRoot(root), pr.errorHelpPath, true));
81
+ process.stderr.write(cliHelpRender(cliPresentationRoot(program), pr.errorHelpPath, true));
91
82
  process.exit(1);
92
83
  }
93
84
 
94
- // Leaf roots have an empty path; that's normal.
95
-
96
- if (pr.path[0] === "completion") {
97
- // If we intercepted a leaf, we MUST pass the original `root` to generate completions
98
- // because `parseRoot` is just a dummy router!
99
- const schemaForCompletion = isLeafCompletionIntercept ? root : parseRoot;
100
-
101
- if (pr.path[1] === "bash") {
102
- process.stdout.write(completionBashScript(schemaForCompletion));
103
- process.exit(0);
104
- }
105
- if (pr.path[1] === "zsh") {
106
- process.stdout.write(completionZshScript(schemaForCompletion));
107
- process.exit(0);
108
- }
85
+ if (pr.kind === ParseKind.Ok) {
86
+ await dispatchBuiltin(program, pr, { isLeafCompletionIntercept, parseRoot: completionParseRoot });
109
87
  }
110
88
 
111
- if (pr.path[0] === "ai") {
112
- if (pr.path[1] === "mcp") {
113
- if (!root.mcpServer) {
114
- process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
115
- process.exit(1);
116
- }
117
- if (pr.path.length !== 2) {
118
- process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
119
- process.exit(1);
120
- }
121
- await cliMcpServeStdio(root);
122
- } else if (pr.path[1] === "skill" && (pr.path[2] === "cursor" || pr.path[2] === "claude")) {
123
- if (root.aiSkill?.enabled === false) {
124
- process.stderr.write("AI skills are disabled. Remove aiSkill.enabled: false from the program root.\n");
125
- process.exit(1);
126
- }
127
- if (pr.path.length !== 3) {
128
- process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
129
- process.exit(1);
130
- }
131
- const msg = cliSkillInstall(root, pr.path[2], {
132
- global: pr.opts.global === "1",
133
- force: pr.opts.force === "1",
134
- });
135
- process.stderr.write(msg + "\n");
136
- process.exit(0);
137
- } else {
138
- process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
89
+ let current: CliNode = parseRoot;
90
+ for (const seg of pr.path) {
91
+ if (!isCliRouter(current)) {
92
+ process.stderr.write("Internal error: missing handler for path.\n");
139
93
  process.exit(1);
140
94
  }
141
- }
142
-
143
- let current = parseRoot;
144
- for (const seg of pr.path) {
145
- const ch = (current.commands ?? []).find((candidate: CliCommand) => candidate.key === seg);
95
+ const ch = current.commands.find((candidate) => candidate.key === seg);
146
96
  if (!ch) {
147
97
  process.stderr.write("Internal error: missing handler for path.\n");
148
98
  process.exit(1);
@@ -150,12 +100,12 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
150
100
  current = ch;
151
101
  }
152
102
 
153
- if (!current.handler) {
103
+ if (!isCliLeaf(current) || !current.handler) {
154
104
  process.stderr.write("Internal error: missing handler for path.\n");
155
105
  process.exit(1);
156
106
  }
157
107
 
158
- const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot, "cli");
108
+ const ctx = new CliContext(program.key, pr.path, pr.args, pr.opts, program, "cli");
159
109
  try {
160
110
  await Promise.resolve(current.handler(ctx));
161
111
  process.exit(0);
@@ -167,13 +117,10 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
167
117
  }
168
118
  }
169
119
 
170
- /**
171
- * Prints a red error line and contextual help on stderr, then exits with status 1.
172
- */
173
120
  export function cliErrWithHelp(ctx: CliContext, msg: string): never {
174
121
  const color = process.stderr.isTTY;
175
122
  const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
176
123
  process.stderr.write(line + "\n");
177
124
  process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.schema), ctx.commandPath, true));
178
125
  process.exit(1);
179
- }
126
+ }
package/src/schema.ts CHANGED
@@ -1,56 +1,13 @@
1
1
  /*
2
2
  This module serializes the CLI schema tree to JSON for machine-readable introspection.
3
- It strips handlers and runtime-only nodes so agents can discover commands, options,
4
- and positionals in one shot.
5
-
6
- It keeps schema export aligned with the declarative CliCommand model that drives help
7
- and completion.
8
3
  */
9
4
 
10
- import {
11
- CliCommand,
12
- CliFallbackMode,
13
- CliOption,
14
- CliPositional,
15
- } from "./types.ts";
16
- import { cliBuiltinCompletionGroup } from "./completion.ts";
17
-
18
- /** JSON-safe command node (no handlers). */
19
- export interface CliSchemaExport {
20
- /** Program or command key. */
21
- key: string;
22
- /** Short description shown in help. */
23
- description: string;
24
- /** Additional notes shown in help (supports {app} placeholder). */
25
- notes?: string;
26
- /** Global or command-level flags/options. */
27
- options?: CliOption[];
28
- /** Default top-level subcommand (program root only). */
29
- fallbackCommand?: string;
30
- /** How fallbackCommand is applied (program root only). */
31
- fallbackMode?: CliFallbackMode;
32
- /** Nested subcommands (routing nodes only). */
33
- commands?: CliSchemaExport[];
34
- /** Positional argument definitions (leaf nodes only). */
35
- positionals?: CliPositional[];
36
- }
5
+ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
6
+ import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
37
7
 
38
- /** JSON-safe export of the reserved `completion` subtree (no handler recursion). */
39
- function exportBuiltinCompletionGroup(appName: string): CliSchemaExport {
40
- const group = cliBuiltinCompletionGroup(appName);
41
- return {
42
- key: group.key,
43
- description: group.description,
44
- commands: (group.commands ?? []).map((ch) => ({
45
- key: ch.key,
46
- description: ch.description,
47
- ...((ch.notes ?? "").length > 0 ? { notes: ch.notes } : {}),
48
- })),
49
- };
50
- }
8
+ const RESERVED = new Set(["completion", "install", "mcp"]);
51
9
 
52
- /** Converts one `CliCommand` node into a JSON-safe export (handlers omitted). */
53
- function exportCommand(cmd: CliCommand): CliSchemaExport {
10
+ function exportCommand(cmd: CliNode): CliSchemaExport {
54
11
  const out: CliSchemaExport = {
55
12
  key: cmd.key,
56
13
  description: cmd.description,
@@ -64,11 +21,11 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
64
21
  out.options = cmd.options;
65
22
  }
66
23
 
67
- if ("handler" in cmd && cmd.handler) {
24
+ if (isCliLeaf(cmd)) {
68
25
  if ((cmd.positionals ?? []).length > 0) {
69
26
  out.positionals = cmd.positionals;
70
27
  }
71
- out.commands = [exportBuiltinCompletionGroup(cmd.key)];
28
+ out.commands = exportPresentationBuiltins(cmd as CliProgram);
72
29
  return out;
73
30
  }
74
31
 
@@ -79,7 +36,7 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
79
36
  out.fallbackMode = cmd.fallbackMode;
80
37
  }
81
38
 
82
- const children = (cmd.commands ?? []).filter((ch) => ch.key !== "completion");
39
+ const children = isCliRouter(cmd) ? cmd.commands.filter((ch) => !RESERVED.has(ch.key)) : [];
83
40
  if (children.length > 0) {
84
41
  out.commands = children.map(exportCommand);
85
42
  }
@@ -87,7 +44,8 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
87
44
  return out;
88
45
  }
89
46
 
90
- /** Returns pretty-printed JSON for the full program schema (trailing newline). */
91
- export function cliSchemaJson(root: CliCommand): string {
47
+ export function cliSchemaJson(root: CliProgram): string {
92
48
  return JSON.stringify(exportCommand(root), null, 2) + "\n";
93
49
  }
50
+
51
+ export type { CliSchemaExport };
@@ -5,7 +5,7 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
5
5
  import { collectOptionDefs } from "../parse.ts";
6
6
  import { cliSchemaJson } from "../schema.ts";
7
7
  import { collectMcpTools, sanitizeToolSegment } from "../mcp/tools.ts";
8
- import { CliCommand, CliOptionKind } from "../types.ts";
8
+ import { CliProgram, CliOptionKind } from "../types.ts";
9
9
 
10
10
  export type SkillTarget = "cursor" | "claude";
11
11
 
@@ -22,7 +22,7 @@ function truncate(text: string, maxLen: number): string {
22
22
  }
23
23
 
24
24
  /** Builds third-person skill description for YAML frontmatter. */
25
- function skillDescription(root: CliCommand): string {
25
+ function skillDescription(root: CliProgram): string {
26
26
  const tools = collectMcpTools(root);
27
27
  const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
28
28
  const sample = paths.slice(0, 5).join(", ");
@@ -32,7 +32,7 @@ function skillDescription(root: CliCommand): string {
32
32
  }
33
33
 
34
34
  /** Formats one command line for the catalog section. */
35
- function formatCommandEntry(root: CliCommand, tool: ReturnType<typeof collectMcpTools>[number]): string {
35
+ function formatCommandEntry(root: CliProgram, tool: ReturnType<typeof collectMcpTools>[number]): string {
36
36
  const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
37
37
  let line = `- **\`${cliPath}\`** — ${tool.description}`;
38
38
  const opts = collectOptionDefs(root, tool.path);
@@ -52,8 +52,8 @@ function formatCommandEntry(root: CliCommand, tool: ReturnType<typeof collectMcp
52
52
  }
53
53
 
54
54
  /** Builds SKILL.md body for the given target. */
55
- function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): string {
56
- const name = root.aiSkill?.name ?? sanitizeToolSegment(root.key);
55
+ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): string {
56
+ const name = sanitizeToolSegment(root.key);
57
57
  const description = skillDescription(root);
58
58
  const tools = collectMcpTools(root);
59
59
 
@@ -80,7 +80,7 @@ function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): s
80
80
  "**Prefer MCP** when a host has the server connected:",
81
81
  "",
82
82
  "```bash",
83
- `${root.key} ai mcp`,
83
+ `${root.key} mcp`,
84
84
  "```",
85
85
  "",
86
86
  "Example Cursor `mcp.json` entry:",
@@ -91,7 +91,7 @@ function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): s
91
91
  mcpServers: {
92
92
  [root.mcpServer.name ?? root.key]: {
93
93
  command: root.key,
94
- args: ["ai", "mcp"],
94
+ args: ["mcp"],
95
95
  },
96
96
  },
97
97
  },
@@ -159,7 +159,7 @@ function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): s
159
159
  }
160
160
 
161
161
  /** Builds reference.md with pretty-printed schema JSON. */
162
- function buildReferenceMd(root: CliCommand): string {
162
+ function buildReferenceMd(root: CliProgram): string {
163
163
  return [
164
164
  `# ${root.key} — CLI reference`,
165
165
  "",
@@ -173,8 +173,8 @@ function buildReferenceMd(root: CliCommand): string {
173
173
  }
174
174
 
175
175
  /** Generates SKILL.md and reference.md for Cursor or Claude Code. */
176
- export function generateSkillBundle(root: CliCommand, target: SkillTarget): SkillBundle {
177
- const dirName = root.aiSkill?.name ?? sanitizeToolSegment(root.key);
176
+ export function generateSkillBundle(root: CliProgram, target: SkillTarget): SkillBundle {
177
+ const dirName = sanitizeToolSegment(root.key);
178
178
  return {
179
179
  dirName,
180
180
  skillMd: buildSkillMd(root, target, dirName),