argsbarg 1.5.0 → 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 (49) hide show
  1. package/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
  2. package/CHANGELOG.md +24 -1
  3. package/README.md +12 -8
  4. package/docs/install.md +2 -2
  5. package/docs/mcp.md +3 -3
  6. package/examples/mcp-test.ts +3 -3
  7. package/examples/minimal.ts +3 -3
  8. package/examples/nested.ts +3 -3
  9. package/examples/option-required.ts +3 -3
  10. package/index.d.ts +40 -37
  11. package/package.json +1 -1
  12. package/src/builtins/builtins.test.ts +3 -3
  13. package/src/builtins/completion-bash.ts +3 -3
  14. package/src/builtins/completion-fish.ts +2 -2
  15. package/src/builtins/completion-group.ts +2 -2
  16. package/src/builtins/completion-zsh.ts +3 -3
  17. package/src/builtins/dispatch.ts +41 -26
  18. package/src/builtins/export.ts +15 -8
  19. package/src/builtins/install.ts +3 -3
  20. package/src/builtins/mcp.ts +2 -2
  21. package/src/builtins/presentation.ts +34 -23
  22. package/src/builtins/scopes.ts +9 -8
  23. package/src/capabilities.ts +32 -0
  24. package/src/context.ts +21 -6
  25. package/src/help.ts +21 -9
  26. package/src/index.test.ts +71 -64
  27. package/src/index.ts +1 -1
  28. package/src/install/binary.ts +3 -3
  29. package/src/install/completions.ts +2 -2
  30. package/src/install/detect-installed.ts +1 -1
  31. package/src/install/index.ts +4 -4
  32. package/src/install/install.test.ts +2 -2
  33. package/src/install/mcp-config.ts +2 -2
  34. package/src/install/paths.ts +3 -3
  35. package/src/install/plan.ts +4 -4
  36. package/src/install/status.ts +2 -2
  37. package/src/install/uninstall.ts +2 -2
  38. package/src/invoke.ts +14 -5
  39. package/src/mcp/server.ts +3 -3
  40. package/src/mcp/tools.ts +16 -16
  41. package/src/mcp.ts +2 -2
  42. package/src/parse.ts +55 -27
  43. package/src/runtime.ts +33 -24
  44. package/src/schema.ts +6 -6
  45. package/src/skill/generate.ts +6 -6
  46. package/src/skill/install.ts +2 -2
  47. package/src/types.test.ts +40 -0
  48. package/src/types.ts +54 -44
  49. package/src/validate.ts +89 -71
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" },
@@ -811,7 +814,7 @@ test("mcpToolCallToArgv expands varargs positionals", () => {
811
814
  });
812
815
 
813
816
  test("reserved command name install is rejected", () => {
814
- const root: CliCommand = {
817
+ const root: CliProgram = {
815
818
  key: "app",
816
819
  description: "",
817
820
  commands: [
@@ -826,7 +829,7 @@ test("reserved command name install is rejected", () => {
826
829
  });
827
830
 
828
831
  test("top-level command name mcp is allowed without mcpServer", () => {
829
- const root: CliCommand = {
832
+ const root: CliProgram = {
830
833
  key: "app",
831
834
  description: "",
832
835
  commands: [
@@ -841,7 +844,7 @@ test("top-level command name mcp is allowed without mcpServer", () => {
841
844
  });
842
845
 
843
846
  test("top-level command name mcp is rejected when mcpServer is set", () => {
844
- const root: CliCommand = {
847
+ const root: CliProgram = {
845
848
  key: "app",
846
849
  description: "",
847
850
  mcpServer: {},
@@ -857,7 +860,7 @@ test("top-level command name mcp is rejected when mcpServer is set", () => {
857
860
  });
858
861
 
859
862
  test("mcpServer on non-root node is rejected", () => {
860
- const root: CliCommand = {
863
+ const root = {
861
864
  key: "app",
862
865
  description: "",
863
866
  commands: [
@@ -868,12 +871,12 @@ test("mcpServer on non-root node is rejected", () => {
868
871
  handler: () => {},
869
872
  },
870
873
  ],
871
- };
874
+ } as unknown as CliProgram;
872
875
  expect(() => cliValidateRoot(root)).toThrow(/mcpServer is only supported on the program root/);
873
876
  });
874
877
 
875
878
  test("mcpTool on root is rejected", () => {
876
- const root: CliCommand = {
879
+ const root: CliProgram = {
877
880
  key: "app",
878
881
  description: "",
879
882
  mcpTool: { enabled: false },
@@ -883,7 +886,7 @@ test("mcpTool on root is rejected", () => {
883
886
  });
884
887
 
885
888
  test("mcpTool on routing node is rejected", () => {
886
- const root: CliCommand = {
889
+ const root: CliProgram = {
887
890
  key: "app",
888
891
  description: "",
889
892
  commands: [
@@ -1040,7 +1043,7 @@ test("minimal.ts mcp without opt-in fails", async () => {
1040
1043
  test("ctx.invocation is cli via cliRun", async () => {
1041
1044
  const indexPath = join(import.meta.dir, "index.ts");
1042
1045
  const { stdout } = await $`bun -e ${`
1043
- import { cliRun, CliCommand } from ${JSON.stringify(indexPath)};
1046
+ import { cliRun, CliProgram } from ${JSON.stringify(indexPath)};
1044
1047
  const cli = { key: "t", description: "d", handler: (ctx) => console.log(ctx.invocation) };
1045
1048
  await cliRun(cli, []);
1046
1049
  `}`.quiet();
@@ -1049,7 +1052,7 @@ await cliRun(cli, []);
1049
1052
 
1050
1053
  test("ctx.invocation is mcp via cliInvoke", async () => {
1051
1054
  let seen = "";
1052
- const root: CliCommand = {
1055
+ const root: CliProgram = {
1053
1056
  key: "app",
1054
1057
  description: "",
1055
1058
  handler: (ctx) => {
@@ -1062,7 +1065,7 @@ test("ctx.invocation is mcp via cliInvoke", async () => {
1062
1065
  expect(seen).toBe("mcp");
1063
1066
  });
1064
1067
 
1065
- const enumMcpFixture: CliCommand = {
1068
+ const enumMcpFixture: CliProgram = {
1066
1069
  key: "app",
1067
1070
  description: "",
1068
1071
  mcpServer: {},
@@ -1092,7 +1095,7 @@ test("Enum option inputSchema includes enum array", () => {
1092
1095
  });
1093
1096
 
1094
1097
  test("cliInvoke rejects invalid Enum value", async () => {
1095
- const root: CliCommand = {
1098
+ const root: CliProgram = {
1096
1099
  key: "app",
1097
1100
  description: "",
1098
1101
  handler: () => {},
@@ -1113,7 +1116,7 @@ test("cliInvoke rejects invalid Enum value", async () => {
1113
1116
  });
1114
1117
 
1115
1118
  test("cliInvoke accepts valid Enum value", async () => {
1116
- const root: CliCommand = {
1119
+ const root: CliProgram = {
1117
1120
  key: "app",
1118
1121
  description: "",
1119
1122
  handler: (ctx) => {
@@ -1136,7 +1139,7 @@ test("cliInvoke accepts valid Enum value", async () => {
1136
1139
  });
1137
1140
 
1138
1141
  test("cliValidateRoot rejects Enum with no choices", () => {
1139
- const root: CliCommand = {
1142
+ const root: CliProgram = {
1140
1143
  key: "app",
1141
1144
  description: "",
1142
1145
  handler: () => {},
@@ -1146,7 +1149,7 @@ test("cliValidateRoot rejects Enum with no choices", () => {
1146
1149
  });
1147
1150
 
1148
1151
  test("cliValidateRoot rejects Enum with duplicate choices", () => {
1149
- const root: CliCommand = {
1152
+ const root: CliProgram = {
1150
1153
  key: "app",
1151
1154
  description: "",
1152
1155
  handler: () => {},
@@ -1156,7 +1159,7 @@ test("cliValidateRoot rejects Enum with duplicate choices", () => {
1156
1159
  });
1157
1160
 
1158
1161
  test("mcpTool.description override wins without requiresEnv suffix", () => {
1159
- const root: CliCommand = {
1162
+ const root: CliProgram = {
1160
1163
  key: "app",
1161
1164
  description: "",
1162
1165
  mcpServer: {},
@@ -1174,7 +1177,7 @@ test("mcpTool.description override wins without requiresEnv suffix", () => {
1174
1177
  });
1175
1178
 
1176
1179
  test("mcpTool.requiresEnv appended to auto description", () => {
1177
- const root: CliCommand = {
1180
+ const root: CliProgram = {
1178
1181
  key: "app",
1179
1182
  description: "",
1180
1183
  mcpServer: {},
@@ -1192,7 +1195,7 @@ test("mcpTool.requiresEnv appended to auto description", () => {
1192
1195
  });
1193
1196
 
1194
1197
  test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
1195
- const root: CliCommand = {
1198
+ const root: CliProgram = {
1196
1199
  key: "app",
1197
1200
  description: "",
1198
1201
  mcpServer: {
@@ -1207,7 +1210,7 @@ test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
1207
1210
  });
1208
1211
 
1209
1212
  test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
1210
- const root: CliCommand = {
1213
+ const root: CliProgram = {
1211
1214
  key: "app",
1212
1215
  description: "",
1213
1216
  mcpServer: {
@@ -1220,7 +1223,7 @@ test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
1220
1223
  });
1221
1224
 
1222
1225
  test("allMcpResources includes custom resources", () => {
1223
- const root: CliCommand = {
1226
+ const root: CliProgram = {
1224
1227
  key: "app",
1225
1228
  description: "",
1226
1229
  mcpServer: {
@@ -1263,7 +1266,7 @@ test("loadEnvFile overwrites existing keys", () => {
1263
1266
  });
1264
1267
 
1265
1268
  test("Enum completions list choices in bash script", () => {
1266
- const root: CliCommand = {
1269
+ const root: CliProgram = {
1267
1270
  key: "app",
1268
1271
  description: "",
1269
1272
  commands: [
@@ -1368,7 +1371,7 @@ test("MCP envFile loads vars for tool handlers", async () => {
1368
1371
 
1369
1372
  // ── v1.3 parser ergonomics ────────────────────────────────────────────────────
1370
1373
 
1371
- function varargsReadFixture(): CliCommand {
1374
+ function varargsReadFixture(): CliProgram {
1372
1375
  return {
1373
1376
  key: "app",
1374
1377
  description: "",
@@ -1398,7 +1401,7 @@ function varargsReadFixture(): CliCommand {
1398
1401
  };
1399
1402
  }
1400
1403
 
1401
- function nestedDocsFallbackFixture(): CliCommand {
1404
+ function nestedDocsFallbackFixture(): CliProgram {
1402
1405
  return {
1403
1406
  key: "app",
1404
1407
  description: "",
@@ -1434,7 +1437,7 @@ test("nested fallback routes to default when argv exhausted at router", () => {
1434
1437
  });
1435
1438
 
1436
1439
  test("nested fallback MissingOrUnknown routes unknown token to default", () => {
1437
- const root: CliCommand = {
1440
+ const root: CliProgram = {
1438
1441
  key: "app",
1439
1442
  description: "",
1440
1443
  commands: [
@@ -1483,7 +1486,7 @@ test("nested fallback MissingOnly errors on unknown subcommand", () => {
1483
1486
  });
1484
1487
 
1485
1488
  test("cliValidateRoot rejects invalid nested fallbackCommand", () => {
1486
- const root: CliCommand = {
1489
+ const root: CliProgram = {
1487
1490
  key: "app",
1488
1491
  description: "",
1489
1492
  commands: [
@@ -1516,7 +1519,7 @@ test("nested router scoped help does not route to fallback", () => {
1516
1519
  expect(pr.kind).toBe(ParseKind.Help);
1517
1520
  expect(pr.helpPath).toEqual(["docs"]);
1518
1521
  expect(pr.helpExplicit).toBe(true);
1519
- const help = cliHelpRender(root, pr.helpPath, false);
1522
+ const help = cliHelpRender(cliPresentationRoot(root), pr.helpPath, false);
1520
1523
  expect(help).toContain("api");
1521
1524
  expect(help).toContain("guide");
1522
1525
  });
@@ -1575,7 +1578,7 @@ test("varargs scoped help in tail", () => {
1575
1578
  });
1576
1579
 
1577
1580
  test("ctx.positional returns single slot value", async () => {
1578
- const root: CliCommand = {
1581
+ const root: CliProgram = {
1579
1582
  key: "app",
1580
1583
  description: "",
1581
1584
  commands: [
@@ -1598,16 +1601,18 @@ test("ctx.positional returns single slot value", async () => {
1598
1601
  test("ctx.positional returns varargs array", async () => {
1599
1602
  const root = varargsReadFixture();
1600
1603
  let captured: string | string[] | undefined;
1601
- root.commands![0]!.handler = (ctx) => {
1602
- captured = ctx.positional("files");
1603
- };
1604
+ if (isCliRouter(root)) {
1605
+ (root.commands[0] as CliLeaf).handler = (ctx) => {
1606
+ captured = ctx.positional("files");
1607
+ };
1608
+ }
1604
1609
  cliValidateRoot(root);
1605
1610
  await cliInvoke(root, ["read", "a.txt", "b.txt"]);
1606
1611
  expect(captured).toEqual(["a.txt", "b.txt"]);
1607
1612
  });
1608
1613
 
1609
1614
  test("ctx.positional returns undefined for absent optional slot", async () => {
1610
- const root: CliCommand = {
1615
+ const root: CliProgram = {
1611
1616
  key: "app",
1612
1617
  description: "",
1613
1618
  commands: [
@@ -1633,10 +1638,12 @@ test("ctx.positional varargs matches ctx.args", async () => {
1633
1638
  const root = varargsReadFixture();
1634
1639
  let positional: string | string[] | undefined;
1635
1640
  let args: string[] = [];
1636
- root.commands![0]!.handler = (ctx) => {
1637
- positional = ctx.positional("files");
1638
- args = ctx.args;
1639
- };
1641
+ if (isCliRouter(root)) {
1642
+ (root.commands[0] as CliLeaf).handler = (ctx) => {
1643
+ positional = ctx.positional("files");
1644
+ args = ctx.args;
1645
+ };
1646
+ }
1640
1647
  cliValidateRoot(root);
1641
1648
  await cliInvoke(root, ["read", "a.txt", "b.txt"]);
1642
1649
  expect(positional).toEqual(args);
@@ -1673,7 +1680,7 @@ test("mcpToolCallToArgv empty string varargs appends nothing", () => {
1673
1680
  // ── Skills ────────────────────────────────────────────────────────────────────
1674
1681
 
1675
1682
  test("install config on non-root node is rejected", () => {
1676
- const root: CliCommand = {
1683
+ const root = {
1677
1684
  key: "app",
1678
1685
  description: "",
1679
1686
  commands: [
@@ -1684,7 +1691,7 @@ test("install config on non-root node is rejected", () => {
1684
1691
  handler: () => {},
1685
1692
  },
1686
1693
  ],
1687
- };
1694
+ } as unknown as CliProgram;
1688
1695
  expect(() => cliValidateRoot(root)).toThrow(/install is only supported on the program root/);
1689
1696
  });
1690
1697
 
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ export { CliContext } from "./context.ts";
13
13
  export { cliErrWithHelp, cliRun } from "./runtime";
14
14
  export { CliFallbackMode, CliOptionKind, CliSchemaValidationError } from "./types.ts";
15
15
  export type {
16
- CliCommand,
16
+ CliProgram,
17
17
  CliHandler,
18
18
  CliInvocation,
19
19
  CliMcpResource,
@@ -1,6 +1,6 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { CliCommand } from "../types.ts";
3
+ import { CliProgram } from "../types.ts";
4
4
  import { InstallPaths } from "./paths.ts";
5
5
  import {
6
6
  buildPathRcBlock,
@@ -17,7 +17,7 @@ export interface BinaryInstallResult {
17
17
  }
18
18
 
19
19
  /** Copies the running binary to the install path and patches rc files when shells are detected. */
20
- export function installBinary(root: CliCommand, paths: InstallPaths, dry: boolean): BinaryInstallResult {
20
+ export function installBinary(root: CliProgram, paths: InstallPaths, dry: boolean): BinaryInstallResult {
21
21
  const changed: string[] = [];
22
22
  const shells = detectShells();
23
23
  let patchedBashRc = false;
@@ -58,7 +58,7 @@ export function installBinary(root: CliCommand, paths: InstallPaths, dry: boolea
58
58
  }
59
59
 
60
60
  /** Removes binary and rc marker blocks. */
61
- export function uninstallBinary(root: CliCommand, paths: InstallPaths, dry: boolean): string[] {
61
+ export function uninstallBinary(root: CliProgram, paths: InstallPaths, dry: boolean): string[] {
62
62
  const changed: string[] = [];
63
63
  if (existsSync(paths.binaryPath)) {
64
64
  if (!dry) unlinkSync(paths.binaryPath);
@@ -1,13 +1,13 @@
1
1
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { CliCommand } from "../types.ts";
3
+ import { CliProgram } from "../types.ts";
4
4
  import { completionBashScript, completionFishScript, completionZshScript } from "../builtins/index.ts";
5
5
  import { cliPresentationRoot } from "../builtins/presentation.ts";
6
6
  import { InstallPaths } from "./paths.ts";
7
7
  import { detectShells } from "./shell.ts";
8
8
 
9
9
  /** Writes shell completion scripts for detected shells. */
10
- export function installCompletions(root: CliCommand, paths: InstallPaths, dry: boolean): string[] {
10
+ export function installCompletions(root: CliProgram, paths: InstallPaths, dry: boolean): string[] {
11
11
  const changed: string[] = [];
12
12
  const shells = detectShells();
13
13
  const schema = cliPresentationRoot(root);
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { CliCommand } from "../types.ts";
2
+ import { CliProgram } from "../types.ts";
3
3
  import { InstallPaths } from "./paths.ts";
4
4
 
5
5
  export interface InstalledArtifacts {
@@ -1,5 +1,5 @@
1
1
  import { readSync } from "node:fs";
2
- import { CliCommand } from "../types.ts";
2
+ import { CliProgram } from "../types.ts";
3
3
  import { cliSkillInstall } from "../skill/install.ts";
4
4
  import { checkMcpConflict, expectedMcpEntry } from "./mcp-config.ts";
5
5
  import {
@@ -74,7 +74,7 @@ function promptConfirm(): boolean {
74
74
  }
75
75
 
76
76
  function runSkillAction(
77
- root: CliCommand,
77
+ root: CliProgram,
78
78
  kind: "cursor-skill" | "claude-skill",
79
79
  opts: InstallOpts,
80
80
  ): string[] {
@@ -87,7 +87,7 @@ function runSkillAction(
87
87
  }
88
88
 
89
89
  function executePlan(
90
- root: CliCommand,
90
+ root: CliProgram,
91
91
  actions: Array<InstallAction | UninstallAction>,
92
92
  opts: InstallOpts,
93
93
  ): string[] {
@@ -114,7 +114,7 @@ function executePlan(
114
114
  }
115
115
 
116
116
  /** Main install command orchestrator. */
117
- export async function cliInstall(root: CliCommand, rawOpts: Record<string, string>): Promise<never> {
117
+ export async function cliInstall(root: CliProgram, rawOpts: Record<string, string>): Promise<never> {
118
118
  const opts = parseInstallOpts(rawOpts);
119
119
  const err = validateOpts(opts);
120
120
  if (err) {
@@ -2,14 +2,14 @@ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
- import { CliCommand } from "../types.ts";
5
+ import { CliProgram } from "../types.ts";
6
6
  import { detectInstalledArtifacts } from "./detect-installed.ts";
7
7
  import { resolveInstallPaths } from "./paths.ts";
8
8
  import { buildInstallPlan } from "./plan.ts";
9
9
  import { printInstallStatus } from "./status.ts";
10
10
  import { parseInstallOpts } from "./index.ts";
11
11
 
12
- const fixture: CliCommand = {
12
+ const fixture: CliProgram = {
13
13
  key: "testapp",
14
14
  description: "Test",
15
15
  mcpServer: { name: "testapp" },
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { CliCommand } from "../types.ts";
3
+ import { CliProgram } from "../types.ts";
4
4
  import { InstallPaths } from "./paths.ts";
5
5
 
6
6
  export interface McpServerEntry {
@@ -8,7 +8,7 @@ export interface McpServerEntry {
8
8
  args: string[];
9
9
  }
10
10
 
11
- export function expectedMcpEntry(root: CliCommand): McpServerEntry {
11
+ export function expectedMcpEntry(root: CliProgram): McpServerEntry {
12
12
  return { command: root.key, args: ["mcp"] };
13
13
  }
14
14
 
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { CliCommand } from "../types.ts";
3
+ import { CliProgram } from "../types.ts";
4
4
  import { sanitizeToolSegment } from "../mcp/tools.ts";
5
5
 
6
6
  export interface InstallPaths {
@@ -35,7 +35,7 @@ function expandTilde(path: string): string {
35
35
  }
36
36
 
37
37
  /** Resolves the binary install directory from CLI flag, env, or config. */
38
- export function resolveBindir(root: CliCommand, prefix?: string): string {
38
+ export function resolveBindir(root: CliProgram, prefix?: string): string {
39
39
  const raw = prefix ?? process.env.INSTALL_PREFIX ?? root.install?.prefix;
40
40
  if (raw) {
41
41
  return expandTilde(raw);
@@ -44,7 +44,7 @@ export function resolveBindir(root: CliCommand, prefix?: string): string {
44
44
  }
45
45
 
46
46
  /** Resolves all install artifact paths for a program root. */
47
- export function resolveInstallPaths(root: CliCommand, opts: { prefix?: string }): InstallPaths {
47
+ export function resolveInstallPaths(root: CliProgram, opts: { prefix?: string }): InstallPaths {
48
48
  const home = userHome();
49
49
  const bindir = resolveBindir(root, opts.prefix);
50
50
  const key = root.key;