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/index.test.ts CHANGED
@@ -7,9 +7,12 @@ It keeps the CLI contract stable by catching routing, option handling, and gener
7
7
  shell output regressions.
8
8
  */
9
9
 
10
+ import { cliPresentationRoot } from "./builtins/presentation.ts";
10
11
  import { completionBashScript, completionZshScript } from "./completion.ts";
11
12
  import { cliHelpRender } from "./help.ts";
12
- import { CliCommand, CliFallbackMode, CliOptionKind, cliInvoke } from "./index.ts";
13
+ import { CliProgram, CliFallbackMode, CliOptionKind, cliInvoke } from "./index.ts";
14
+ import type { CliLeaf } from "./types.ts";
15
+ import { isCliRouter } from "./types.ts";
13
16
  import {
14
17
  allMcpResources,
15
18
  collectMcpTools,
@@ -31,7 +34,7 @@ import { tmpdir } from "node:os";
31
34
  import { join } from "node:path";
32
35
 
33
36
  test("bundled short presence flags", () => {
34
- const root: CliCommand = {
37
+ const root: CliProgram = {
35
38
  key: "app",
36
39
  description: "",
37
40
  commands: [
@@ -64,7 +67,7 @@ test("bundled short presence flags", () => {
64
67
  });
65
68
 
66
69
  test("long option equals", () => {
67
- const root: CliCommand = {
70
+ const root: CliProgram = {
68
71
  key: "app",
69
72
  description: "",
70
73
  commands: [
@@ -89,7 +92,7 @@ test("long option equals", () => {
89
92
  });
90
93
 
91
94
  test("fallback missing or unknown root flags", () => {
92
- const root: CliCommand = {
95
+ const root: CliProgram = {
93
96
  key: "app",
94
97
  description: "",
95
98
  commands: [
@@ -117,7 +120,7 @@ test("fallback missing or unknown root flags", () => {
117
120
  });
118
121
 
119
122
  test("unknown command", () => {
120
- const root: CliCommand = {
123
+ const root: CliProgram = {
121
124
  key: "app",
122
125
  description: "",
123
126
  commands: [{ key: "hello", description: "", handler: () => {} }],
@@ -129,7 +132,7 @@ test("unknown command", () => {
129
132
  });
130
133
 
131
134
  test("implicit help empty", () => {
132
- const root: CliCommand = {
135
+ const root: CliProgram = {
133
136
  key: "app",
134
137
  description: "",
135
138
  commands: [{ key: "x", description: "", handler: () => {} }],
@@ -141,7 +144,7 @@ test("implicit help empty", () => {
141
144
  });
142
145
 
143
146
  test("invalid number post validate", () => {
144
- const root: CliCommand = {
147
+ const root: CliProgram = {
145
148
  key: "app",
146
149
  description: "",
147
150
  commands: [
@@ -167,7 +170,7 @@ test("invalid number post validate", () => {
167
170
  });
168
171
 
169
172
  test("supports scientific notation in numbers", () => {
170
- const root: CliCommand = {
173
+ const root: CliProgram = {
171
174
  key: "app",
172
175
  description: "",
173
176
  commands: [
@@ -195,7 +198,7 @@ test("supports scientific notation in numbers", () => {
195
198
 
196
199
 
197
200
  test("completion scripts contain app name", () => {
198
- const root: CliCommand = {
201
+ const root: CliProgram = {
199
202
  key: "myapp",
200
203
  description: "Test",
201
204
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
@@ -212,7 +215,7 @@ test("completion scripts contain app name", () => {
212
215
  });
213
216
 
214
217
  test("completion scripts do not emit invalid bash substitutions", () => {
215
- const root: CliCommand = {
218
+ const root: CliProgram = {
216
219
  key: "app",
217
220
  description: "Test",
218
221
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
@@ -223,7 +226,7 @@ test("completion scripts do not emit invalid bash substitutions", () => {
223
226
  });
224
227
 
225
228
  test("completion scripts escape shell-sensitive command text in zsh", () => {
226
- const root: CliCommand = {
229
+ const root: CliProgram = {
227
230
  key: "app",
228
231
  description: "Test",
229
232
  commands: [
@@ -240,7 +243,7 @@ test("completion scripts escape shell-sensitive command text in zsh", () => {
240
243
  });
241
244
 
242
245
  test("completion scripts keep dotted app names in registration names", () => {
243
- const root: CliCommand = {
246
+ const root: CliProgram = {
244
247
  key: "minimal.ts",
245
248
  description: "Test",
246
249
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
@@ -255,7 +258,7 @@ test("completion scripts keep dotted app names in registration names", () => {
255
258
  });
256
259
 
257
260
  test("trailing options after bounded positionals", () => {
258
- const root: CliCommand = {
261
+ const root: CliProgram = {
259
262
  key: "app",
260
263
  description: "",
261
264
  commands: [
@@ -288,7 +291,7 @@ test("trailing options after bounded positionals", () => {
288
291
  });
289
292
 
290
293
  test("trailing options include parent-scoped flags", () => {
291
- const root: CliCommand = {
294
+ const root: CliProgram = {
292
295
  key: "app",
293
296
  description: "",
294
297
  commands: [
@@ -337,7 +340,7 @@ test("trailing options include parent-scoped flags", () => {
337
340
  });
338
341
 
339
342
  test("varargs tail parses trailing options", () => {
340
- const root: CliCommand = {
343
+ const root: CliProgram = {
341
344
  key: "app",
342
345
  description: "",
343
346
  commands: [
@@ -372,7 +375,7 @@ test("varargs tail parses trailing options", () => {
372
375
  });
373
376
 
374
377
  test("stops parsing options at --", () => {
375
- const root: CliCommand = {
378
+ const root: CliProgram = {
376
379
  key: "app",
377
380
  description: "",
378
381
  commands: [
@@ -407,7 +410,7 @@ test("stops parsing options at --", () => {
407
410
  });
408
411
 
409
412
  test("missing required option returns error", () => {
410
- const root: CliCommand = {
413
+ const root: CliProgram = {
411
414
  key: "app",
412
415
  description: "",
413
416
  options: [
@@ -433,7 +436,7 @@ test("missing required option returns error", () => {
433
436
  });
434
437
 
435
438
  test("provided required option parses ok", () => {
436
- const root: CliCommand = {
439
+ const root: CliProgram = {
437
440
  key: "app",
438
441
  description: "",
439
442
  commands: [
@@ -459,7 +462,7 @@ test("provided required option parses ok", () => {
459
462
  });
460
463
 
461
464
  test("presence option cannot be required", () => {
462
- const root: CliCommand = {
465
+ const root: CliProgram = {
463
466
  key: "app",
464
467
  description: "",
465
468
  options: [
@@ -527,7 +530,7 @@ test("leaf root help lists completion built-in", async () => {
527
530
  });
528
531
 
529
532
  test("parse recognizes --schema at the program root", () => {
530
- const root: CliCommand = {
533
+ const root: CliProgram = {
531
534
  key: "app",
532
535
  description: "demo",
533
536
  commands: [
@@ -544,7 +547,7 @@ test("parse recognizes --schema at the program root", () => {
544
547
  });
545
548
 
546
549
  test("cliSchemaJson omits handlers and completion built-ins", () => {
547
- const root: CliCommand = {
550
+ const root: CliProgram = {
548
551
  key: "app",
549
552
  description: "demo",
550
553
  commands: [
@@ -574,7 +577,7 @@ test("cliSchemaJson omits handlers and completion built-ins", () => {
574
577
  });
575
578
 
576
579
  test("reserved option name schema is rejected", () => {
577
- const root: CliCommand = {
580
+ const root: CliProgram = {
578
581
  key: "app",
579
582
  description: "",
580
583
  commands: [
@@ -596,7 +599,7 @@ test("reserved option name schema is rejected", () => {
596
599
  });
597
600
 
598
601
  test("root help lists --schema built-in", () => {
599
- const root: CliCommand = {
602
+ const root: CliProgram = {
600
603
  key: "app",
601
604
  description: "demo",
602
605
  commands: [
@@ -607,13 +610,13 @@ test("root help lists --schema built-in", () => {
607
610
  },
608
611
  ],
609
612
  };
610
- const help = cliHelpRender(root, [], false);
613
+ const help = cliHelpRender(cliPresentationRoot(root), [], false);
611
614
  expect(help).toContain("--schema");
612
615
  expect(help).toContain("Print the full command tree as JSON.");
613
616
  });
614
617
 
615
618
  test("nested help omits --schema built-in", () => {
616
- const root: CliCommand = {
619
+ const root: CliProgram = {
617
620
  key: "app",
618
621
  description: "demo",
619
622
  commands: [
@@ -624,12 +627,12 @@ test("nested help omits --schema built-in", () => {
624
627
  },
625
628
  ],
626
629
  };
627
- const help = cliHelpRender(root, ["x"], false);
630
+ const help = cliHelpRender(cliPresentationRoot(root), ["x"], false);
628
631
  expect(help).not.toContain("--schema");
629
632
  });
630
633
 
631
634
  test("completion scripts offer --schema at the program root only", () => {
632
- const root: CliCommand = {
635
+ const root: CliProgram = {
633
636
  key: "myapp",
634
637
  description: "",
635
638
  commands: [
@@ -655,7 +658,7 @@ test("completion scripts offer --schema at the program root only", () => {
655
658
  expect(zsh).toContain("'--schema:Print the full command tree as JSON.'");
656
659
  });
657
660
 
658
- const nestedMcpFixture: CliCommand = {
661
+ const nestedMcpFixture: CliProgram = {
659
662
  key: "nested.ts",
660
663
  description: "Nested groups demo.",
661
664
  mcpServer: { name: "nested-demo", version: "1.0.0" },
@@ -729,7 +732,7 @@ async function mcpRequest(
729
732
  opts?: { script?: string; env?: Record<string, string> },
730
733
  ): Promise<Map<string | number, object>> {
731
734
  const script = opts?.script ?? "examples/nested.ts";
732
- const proc = Bun.spawn(["bun", "run", script, "ai", "mcp"], {
735
+ const proc = Bun.spawn(["bun", "run", script, "mcp"], {
733
736
  stdin: "pipe",
734
737
  stdout: "pipe",
735
738
  stderr: "pipe",
@@ -777,7 +780,8 @@ test("collectMcpTools lists user leaf commands only", () => {
777
780
  expect(names).toContain("stat_owner_lookup");
778
781
  expect(names).toContain("read");
779
782
  expect(names).not.toContain("hidden");
780
- expect(names).not.toContain("ai");
783
+ expect(names).not.toContain("install");
784
+ expect(names).not.toContain("mcp");
781
785
  expect(names).not.toContain("completion");
782
786
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
783
787
  expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
@@ -809,23 +813,23 @@ test("mcpToolCallToArgv expands varargs positionals", () => {
809
813
  expect(argv).toEqual(["read", "a", "b"]);
810
814
  });
811
815
 
812
- test("reserved command name ai is rejected", () => {
813
- const root: CliCommand = {
816
+ test("reserved command name install is rejected", () => {
817
+ const root: CliProgram = {
814
818
  key: "app",
815
819
  description: "",
816
820
  commands: [
817
821
  {
818
- key: "ai",
822
+ key: "install",
819
823
  description: "bad",
820
824
  handler: () => {},
821
825
  },
822
826
  ],
823
827
  };
824
- expect(() => cliValidateRoot(root)).toThrow(/Reserved command name: ai/);
828
+ expect(() => cliValidateRoot(root)).toThrow(/Reserved command name: install/);
825
829
  });
826
830
 
827
- test("top-level command name mcp is allowed", () => {
828
- const root: CliCommand = {
831
+ test("top-level command name mcp is allowed without mcpServer", () => {
832
+ const root: CliProgram = {
829
833
  key: "app",
830
834
  description: "",
831
835
  commands: [
@@ -839,8 +843,24 @@ test("top-level command name mcp is allowed", () => {
839
843
  expect(() => cliValidateRoot(root)).not.toThrow();
840
844
  });
841
845
 
846
+ test("top-level command name mcp is rejected when mcpServer is set", () => {
847
+ const root: CliProgram = {
848
+ key: "app",
849
+ description: "",
850
+ mcpServer: {},
851
+ commands: [
852
+ {
853
+ key: "mcp",
854
+ description: "user command",
855
+ handler: () => {},
856
+ },
857
+ ],
858
+ };
859
+ expect(() => cliValidateRoot(root)).toThrow(/Reserved command name: mcp/);
860
+ });
861
+
842
862
  test("mcpServer on non-root node is rejected", () => {
843
- const root: CliCommand = {
863
+ const root = {
844
864
  key: "app",
845
865
  description: "",
846
866
  commands: [
@@ -851,12 +871,12 @@ test("mcpServer on non-root node is rejected", () => {
851
871
  handler: () => {},
852
872
  },
853
873
  ],
854
- };
874
+ } as unknown as CliProgram;
855
875
  expect(() => cliValidateRoot(root)).toThrow(/mcpServer is only supported on the program root/);
856
876
  });
857
877
 
858
878
  test("mcpTool on root is rejected", () => {
859
- const root: CliCommand = {
879
+ const root: CliProgram = {
860
880
  key: "app",
861
881
  description: "",
862
882
  mcpTool: { enabled: false },
@@ -866,7 +886,7 @@ test("mcpTool on root is rejected", () => {
866
886
  });
867
887
 
868
888
  test("mcpTool on routing node is rejected", () => {
869
- const root: CliCommand = {
889
+ const root: CliProgram = {
870
890
  key: "app",
871
891
  description: "",
872
892
  commands: [
@@ -1014,8 +1034,8 @@ test("MCP ping returns empty result", async () => {
1014
1034
  expect(res.result).toEqual({});
1015
1035
  });
1016
1036
 
1017
- test("minimal.ts ai mcp without opt-in fails", async () => {
1018
- const { stderr, exitCode } = await $`bun run examples/minimal.ts ai mcp`.nothrow().quiet();
1037
+ test("minimal.ts mcp without opt-in fails", async () => {
1038
+ const { stderr, exitCode } = await $`bun run examples/minimal.ts mcp`.nothrow().quiet();
1019
1039
  expect(exitCode).toBe(1);
1020
1040
  expect(stderr.toString()).toContain("MCP is not enabled");
1021
1041
  });
@@ -1023,7 +1043,7 @@ test("minimal.ts ai mcp without opt-in fails", async () => {
1023
1043
  test("ctx.invocation is cli via cliRun", async () => {
1024
1044
  const indexPath = join(import.meta.dir, "index.ts");
1025
1045
  const { stdout } = await $`bun -e ${`
1026
- import { cliRun, CliCommand } from ${JSON.stringify(indexPath)};
1046
+ import { cliRun, CliProgram } from ${JSON.stringify(indexPath)};
1027
1047
  const cli = { key: "t", description: "d", handler: (ctx) => console.log(ctx.invocation) };
1028
1048
  await cliRun(cli, []);
1029
1049
  `}`.quiet();
@@ -1032,7 +1052,7 @@ await cliRun(cli, []);
1032
1052
 
1033
1053
  test("ctx.invocation is mcp via cliInvoke", async () => {
1034
1054
  let seen = "";
1035
- const root: CliCommand = {
1055
+ const root: CliProgram = {
1036
1056
  key: "app",
1037
1057
  description: "",
1038
1058
  handler: (ctx) => {
@@ -1045,7 +1065,7 @@ test("ctx.invocation is mcp via cliInvoke", async () => {
1045
1065
  expect(seen).toBe("mcp");
1046
1066
  });
1047
1067
 
1048
- const enumMcpFixture: CliCommand = {
1068
+ const enumMcpFixture: CliProgram = {
1049
1069
  key: "app",
1050
1070
  description: "",
1051
1071
  mcpServer: {},
@@ -1075,7 +1095,7 @@ test("Enum option inputSchema includes enum array", () => {
1075
1095
  });
1076
1096
 
1077
1097
  test("cliInvoke rejects invalid Enum value", async () => {
1078
- const root: CliCommand = {
1098
+ const root: CliProgram = {
1079
1099
  key: "app",
1080
1100
  description: "",
1081
1101
  handler: () => {},
@@ -1096,7 +1116,7 @@ test("cliInvoke rejects invalid Enum value", async () => {
1096
1116
  });
1097
1117
 
1098
1118
  test("cliInvoke accepts valid Enum value", async () => {
1099
- const root: CliCommand = {
1119
+ const root: CliProgram = {
1100
1120
  key: "app",
1101
1121
  description: "",
1102
1122
  handler: (ctx) => {
@@ -1119,7 +1139,7 @@ test("cliInvoke accepts valid Enum value", async () => {
1119
1139
  });
1120
1140
 
1121
1141
  test("cliValidateRoot rejects Enum with no choices", () => {
1122
- const root: CliCommand = {
1142
+ const root: CliProgram = {
1123
1143
  key: "app",
1124
1144
  description: "",
1125
1145
  handler: () => {},
@@ -1129,7 +1149,7 @@ test("cliValidateRoot rejects Enum with no choices", () => {
1129
1149
  });
1130
1150
 
1131
1151
  test("cliValidateRoot rejects Enum with duplicate choices", () => {
1132
- const root: CliCommand = {
1152
+ const root: CliProgram = {
1133
1153
  key: "app",
1134
1154
  description: "",
1135
1155
  handler: () => {},
@@ -1139,7 +1159,7 @@ test("cliValidateRoot rejects Enum with duplicate choices", () => {
1139
1159
  });
1140
1160
 
1141
1161
  test("mcpTool.description override wins without requiresEnv suffix", () => {
1142
- const root: CliCommand = {
1162
+ const root: CliProgram = {
1143
1163
  key: "app",
1144
1164
  description: "",
1145
1165
  mcpServer: {},
@@ -1157,7 +1177,7 @@ test("mcpTool.description override wins without requiresEnv suffix", () => {
1157
1177
  });
1158
1178
 
1159
1179
  test("mcpTool.requiresEnv appended to auto description", () => {
1160
- const root: CliCommand = {
1180
+ const root: CliProgram = {
1161
1181
  key: "app",
1162
1182
  description: "",
1163
1183
  mcpServer: {},
@@ -1175,7 +1195,7 @@ test("mcpTool.requiresEnv appended to auto description", () => {
1175
1195
  });
1176
1196
 
1177
1197
  test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
1178
- const root: CliCommand = {
1198
+ const root: CliProgram = {
1179
1199
  key: "app",
1180
1200
  description: "",
1181
1201
  mcpServer: {
@@ -1190,7 +1210,7 @@ test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
1190
1210
  });
1191
1211
 
1192
1212
  test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
1193
- const root: CliCommand = {
1213
+ const root: CliProgram = {
1194
1214
  key: "app",
1195
1215
  description: "",
1196
1216
  mcpServer: {
@@ -1203,7 +1223,7 @@ test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
1203
1223
  });
1204
1224
 
1205
1225
  test("allMcpResources includes custom resources", () => {
1206
- const root: CliCommand = {
1226
+ const root: CliProgram = {
1207
1227
  key: "app",
1208
1228
  description: "",
1209
1229
  mcpServer: {
@@ -1246,7 +1266,7 @@ test("loadEnvFile overwrites existing keys", () => {
1246
1266
  });
1247
1267
 
1248
1268
  test("Enum completions list choices in bash script", () => {
1249
- const root: CliCommand = {
1269
+ const root: CliProgram = {
1250
1270
  key: "app",
1251
1271
  description: "",
1252
1272
  commands: [
@@ -1351,7 +1371,7 @@ test("MCP envFile loads vars for tool handlers", async () => {
1351
1371
 
1352
1372
  // ── v1.3 parser ergonomics ────────────────────────────────────────────────────
1353
1373
 
1354
- function varargsReadFixture(): CliCommand {
1374
+ function varargsReadFixture(): CliProgram {
1355
1375
  return {
1356
1376
  key: "app",
1357
1377
  description: "",
@@ -1381,7 +1401,7 @@ function varargsReadFixture(): CliCommand {
1381
1401
  };
1382
1402
  }
1383
1403
 
1384
- function nestedDocsFallbackFixture(): CliCommand {
1404
+ function nestedDocsFallbackFixture(): CliProgram {
1385
1405
  return {
1386
1406
  key: "app",
1387
1407
  description: "",
@@ -1417,7 +1437,7 @@ test("nested fallback routes to default when argv exhausted at router", () => {
1417
1437
  });
1418
1438
 
1419
1439
  test("nested fallback MissingOrUnknown routes unknown token to default", () => {
1420
- const root: CliCommand = {
1440
+ const root: CliProgram = {
1421
1441
  key: "app",
1422
1442
  description: "",
1423
1443
  commands: [
@@ -1466,7 +1486,7 @@ test("nested fallback MissingOnly errors on unknown subcommand", () => {
1466
1486
  });
1467
1487
 
1468
1488
  test("cliValidateRoot rejects invalid nested fallbackCommand", () => {
1469
- const root: CliCommand = {
1489
+ const root: CliProgram = {
1470
1490
  key: "app",
1471
1491
  description: "",
1472
1492
  commands: [
@@ -1499,7 +1519,7 @@ test("nested router scoped help does not route to fallback", () => {
1499
1519
  expect(pr.kind).toBe(ParseKind.Help);
1500
1520
  expect(pr.helpPath).toEqual(["docs"]);
1501
1521
  expect(pr.helpExplicit).toBe(true);
1502
- const help = cliHelpRender(root, pr.helpPath, false);
1522
+ const help = cliHelpRender(cliPresentationRoot(root), pr.helpPath, false);
1503
1523
  expect(help).toContain("api");
1504
1524
  expect(help).toContain("guide");
1505
1525
  });
@@ -1558,7 +1578,7 @@ test("varargs scoped help in tail", () => {
1558
1578
  });
1559
1579
 
1560
1580
  test("ctx.positional returns single slot value", async () => {
1561
- const root: CliCommand = {
1581
+ const root: CliProgram = {
1562
1582
  key: "app",
1563
1583
  description: "",
1564
1584
  commands: [
@@ -1581,16 +1601,18 @@ test("ctx.positional returns single slot value", async () => {
1581
1601
  test("ctx.positional returns varargs array", async () => {
1582
1602
  const root = varargsReadFixture();
1583
1603
  let captured: string | string[] | undefined;
1584
- root.commands![0]!.handler = (ctx) => {
1585
- captured = ctx.positional("files");
1586
- };
1604
+ if (isCliRouter(root)) {
1605
+ (root.commands[0] as CliLeaf).handler = (ctx) => {
1606
+ captured = ctx.positional("files");
1607
+ };
1608
+ }
1587
1609
  cliValidateRoot(root);
1588
1610
  await cliInvoke(root, ["read", "a.txt", "b.txt"]);
1589
1611
  expect(captured).toEqual(["a.txt", "b.txt"]);
1590
1612
  });
1591
1613
 
1592
1614
  test("ctx.positional returns undefined for absent optional slot", async () => {
1593
- const root: CliCommand = {
1615
+ const root: CliProgram = {
1594
1616
  key: "app",
1595
1617
  description: "",
1596
1618
  commands: [
@@ -1616,10 +1638,12 @@ test("ctx.positional varargs matches ctx.args", async () => {
1616
1638
  const root = varargsReadFixture();
1617
1639
  let positional: string | string[] | undefined;
1618
1640
  let args: string[] = [];
1619
- root.commands![0]!.handler = (ctx) => {
1620
- positional = ctx.positional("files");
1621
- args = ctx.args;
1622
- };
1641
+ if (isCliRouter(root)) {
1642
+ (root.commands[0] as CliLeaf).handler = (ctx) => {
1643
+ positional = ctx.positional("files");
1644
+ args = ctx.args;
1645
+ };
1646
+ }
1623
1647
  cliValidateRoot(root);
1624
1648
  await cliInvoke(root, ["read", "a.txt", "b.txt"]);
1625
1649
  expect(positional).toEqual(args);
@@ -1653,22 +1677,22 @@ test("mcpToolCallToArgv empty string varargs appends nothing", () => {
1653
1677
  expect(argv).toEqual(["read"]);
1654
1678
  });
1655
1679
 
1656
- // ── AI builtins and skills ────────────────────────────────────────────────────
1680
+ // ── Skills ────────────────────────────────────────────────────────────────────
1657
1681
 
1658
- test("aiSkill on non-root node is rejected", () => {
1659
- const root: CliCommand = {
1682
+ test("install config on non-root node is rejected", () => {
1683
+ const root = {
1660
1684
  key: "app",
1661
1685
  description: "",
1662
1686
  commands: [
1663
1687
  {
1664
1688
  key: "x",
1665
1689
  description: "",
1666
- aiSkill: { enabled: false },
1690
+ install: { enabled: false },
1667
1691
  handler: () => {},
1668
1692
  },
1669
1693
  ],
1670
- };
1671
- expect(() => cliValidateRoot(root)).toThrow(/aiSkill is only supported on the program root/);
1694
+ } as unknown as CliProgram;
1695
+ expect(() => cliValidateRoot(root)).toThrow(/install is only supported on the program root/);
1672
1696
  });
1673
1697
 
1674
1698
  test("generateSkillBundle includes frontmatter and command catalog", () => {
@@ -1676,7 +1700,7 @@ test("generateSkillBundle includes frontmatter and command catalog", () => {
1676
1700
  expect(bundle.dirName).toBe("nested_ts");
1677
1701
  expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
1678
1702
  expect(bundle.skillMd).toContain("stat owner lookup");
1679
- expect(bundle.skillMd).toContain("ai mcp");
1703
+ expect(bundle.skillMd).toContain("nested.ts mcp");
1680
1704
  expect(bundle.referenceMd).toContain("```json");
1681
1705
  expect(() => JSON.parse(bundle.referenceMd.match(/```json\n([\s\S]*?)\n```/)![1]!)).not.toThrow();
1682
1706
  });
@@ -1686,8 +1710,8 @@ test("cliSkillInstall writes project Cursor skill files", () => {
1686
1710
  const prev = process.cwd();
1687
1711
  process.chdir(cwd);
1688
1712
  try {
1689
- const msg = cliSkillInstall(nestedMcpFixture, "cursor", { force: true });
1690
- expect(msg).toContain(".cursor/skills/nested_ts/");
1713
+ const files = cliSkillInstall(nestedMcpFixture, "cursor", { rimraf: true });
1714
+ expect(files.some((f) => f.includes(".cursor/skills/nested_ts/"))).toBe(true);
1691
1715
  const skillDir = join(cwd, ".cursor", "skills", "nested_ts");
1692
1716
  expect(existsSync(join(skillDir, "SKILL.md"))).toBe(true);
1693
1717
  expect(existsSync(join(skillDir, "reference.md"))).toBe(true);
@@ -1703,8 +1727,8 @@ test("cliSkillInstall global uses HOME skills directory", () => {
1703
1727
  const prevHome = process.env.HOME;
1704
1728
  process.env.HOME = home;
1705
1729
  try {
1706
- const msg = cliSkillInstall(nestedMcpFixture, "cursor", { global: true, force: true });
1707
- expect(msg).toContain(join(home, ".cursor", "skills", "nested_ts"));
1730
+ const files = cliSkillInstall(nestedMcpFixture, "cursor", { global: true, rimraf: true });
1731
+ expect(files.some((f) => f.includes(join(home, ".cursor", "skills", "nested_ts")))).toBe(true);
1708
1732
  expect(existsSync(join(home, ".cursor", "skills", "nested_ts", "SKILL.md"))).toBe(true);
1709
1733
  } finally {
1710
1734
  if (prevHome === undefined) {
@@ -1716,26 +1740,19 @@ test("cliSkillInstall global uses HOME skills directory", () => {
1716
1740
  }
1717
1741
  });
1718
1742
 
1719
- test("cliSkillInstall fails when directory exists without force", () => {
1743
+ test("cliSkillInstall rimraf overwrites existing directory", () => {
1720
1744
  const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-dup-"));
1721
1745
  const prev = process.cwd();
1722
1746
  process.chdir(cwd);
1723
- const prevExit = process.exit;
1724
- let exitCode = 0;
1725
- process.exit = ((code?: number) => {
1726
- exitCode = code ?? 0;
1727
- throw new Error("exit");
1728
- }) as typeof process.exit;
1729
1747
  try {
1730
- cliSkillInstall(nestedMcpFixture, "cursor", { force: true });
1731
- try {
1732
- cliSkillInstall(nestedMcpFixture, "cursor", {});
1733
- } catch {
1734
- // expected exit throw
1735
- }
1736
- expect(exitCode).toBe(1);
1748
+ cliSkillInstall(nestedMcpFixture, "cursor", { rimraf: true });
1749
+ writeFileSync(join(cwd, ".cursor", "skills", "nested_ts", "SKILL.md"), "stale", "utf8");
1750
+ const files = cliSkillInstall(nestedMcpFixture, "cursor", { rimraf: true });
1751
+ expect(files.length).toBeGreaterThan(0);
1752
+ expect(readFileSync(join(cwd, ".cursor", "skills", "nested_ts", "SKILL.md"), "utf8")).toContain(
1753
+ "stat owner lookup",
1754
+ );
1737
1755
  } finally {
1738
- process.exit = prevExit;
1739
1756
  process.chdir(prev);
1740
1757
  rmSync(cwd, { recursive: true, force: true });
1741
1758
  }
@@ -1746,8 +1763,8 @@ test("cliSkillInstall claude target uses .claude/skills", () => {
1746
1763
  const prev = process.cwd();
1747
1764
  process.chdir(cwd);
1748
1765
  try {
1749
- const msg = cliSkillInstall(nestedMcpFixture, "claude", { force: true });
1750
- expect(msg).toContain(".claude/skills/nested_ts/");
1766
+ const files = cliSkillInstall(nestedMcpFixture, "claude", { rimraf: true });
1767
+ expect(files.some((f) => f.includes(".claude/skills/nested_ts/"))).toBe(true);
1751
1768
  expect(readFileSync(join(cwd, ".claude", "skills", "nested_ts", "SKILL.md"), "utf8")).toContain(
1752
1769
  "Claude Code",
1753
1770
  );
@@ -1755,25 +1772,4 @@ test("cliSkillInstall claude target uses .claude/skills", () => {
1755
1772
  process.chdir(prev);
1756
1773
  rmSync(cwd, { recursive: true, force: true });
1757
1774
  }
1758
- });
1759
-
1760
- test("ai skill cursor fails when aiSkill disabled", async () => {
1761
- const dir = mkdtempSync(join(tmpdir(), "argsbarg-skill-off-"));
1762
- const script = join(dir, "skill-off.ts");
1763
- writeFileSync(
1764
- script,
1765
- `import { cliRun, CliCommand } from ${JSON.stringify(join(import.meta.dir, "index.ts"))};
1766
- const cli: CliCommand = {
1767
- key: "offapp",
1768
- description: "demo",
1769
- aiSkill: { enabled: false },
1770
- commands: [{ key: "x", description: "x", handler: () => {} }],
1771
- };
1772
- await cliRun(cli);
1773
- `,
1774
- "utf8",
1775
- );
1776
- const { stderr, exitCode } = await $`bun run ${script} ai skill cursor`.nothrow().quiet();
1777
- expect(exitCode).toBe(1);
1778
- expect(stderr.toString()).toContain("AI skills are disabled");
1779
1775
  });