argsbarg 2.1.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.test.ts CHANGED
@@ -10,7 +10,7 @@ shell output regressions.
10
10
  import { cliPresentationRoot } from "./builtins/presentation.ts";
11
11
  import { completionBashScript, completionZshScript } from "./completion.ts";
12
12
  import { cliHelpRender } from "./help.ts";
13
- import { CliProgram, CliFallbackMode, CliOptionKind, cliInvoke } from "./index.ts";
13
+ import { CliProgram, CliFallbackMode, CliOptionKind, cliInvoke, CliContext } from "./index.ts";
14
14
  import type { CliLeaf } from "./types.ts";
15
15
  import { isCliRouter } from "./types.ts";
16
16
  import {
@@ -18,6 +18,7 @@ import {
18
18
  collectMcpTools,
19
19
  mcpToolCallToArgv,
20
20
  mcpToolDescription,
21
+ resolveMcpSchemaUri,
21
22
  sanitizeToolSegment,
22
23
  } from "./mcp/tools.ts";
23
24
  import { applyShellEnv, loadEnvFile } from "./mcp/env.ts";
@@ -28,13 +29,17 @@ import { ParseKind, parse, postParseValidate } from "./parse.ts";
28
29
  import { cliSchemaJson } from "./schema.ts";
29
30
  import { cliValidateProgram } from "./validate.ts";
30
31
  import { expect, test } from "bun:test";
32
+
33
+ function testProgram(prog: Record<string, unknown> & { key: string; description: string }): CliProgram {
34
+ return { version: "0.0.0", ...prog } as CliProgram;
35
+ }
31
36
  import { $ } from "bun";
32
37
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
38
  import { tmpdir } from "node:os";
34
39
  import { join } from "node:path";
35
40
 
36
41
  test("bundled short presence flags", () => {
37
- const root: CliProgram = {
42
+ const root= testProgram({
38
43
  key: "app",
39
44
  description: "",
40
45
  commands: [
@@ -58,7 +63,7 @@ test("bundled short presence flags", () => {
58
63
  handler: () => {},
59
64
  },
60
65
  ],
61
- };
66
+ });
62
67
  cliValidateProgram(root);
63
68
  const pr = postParseValidate(root, parse(root, ["x", "-ab"]));
64
69
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -67,7 +72,7 @@ test("bundled short presence flags", () => {
67
72
  });
68
73
 
69
74
  test("long option equals", () => {
70
- const root: CliProgram = {
75
+ const root= testProgram({
71
76
  key: "app",
72
77
  description: "",
73
78
  commands: [
@@ -84,7 +89,7 @@ test("long option equals", () => {
84
89
  handler: () => {},
85
90
  },
86
91
  ],
87
- };
92
+ });
88
93
  cliValidateProgram(root);
89
94
  const pr = postParseValidate(root, parse(root, ["x", "--name=pat"]));
90
95
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -92,7 +97,7 @@ test("long option equals", () => {
92
97
  });
93
98
 
94
99
  test("fallback missing or unknown root flags", () => {
95
- const root: CliProgram = {
100
+ const root= testProgram({
96
101
  key: "app",
97
102
  description: "",
98
103
  commands: [
@@ -111,7 +116,7 @@ test("fallback missing or unknown root flags", () => {
111
116
  ],
112
117
  fallbackCommand: "hello",
113
118
  fallbackMode: CliFallbackMode.MissingOrUnknown,
114
- };
119
+ });
115
120
  cliValidateProgram(root);
116
121
  const pr = postParseValidate(root, parse(root, ["--name", "bob"]));
117
122
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -120,11 +125,11 @@ test("fallback missing or unknown root flags", () => {
120
125
  });
121
126
 
122
127
  test("unknown command", () => {
123
- const root: CliProgram = {
128
+ const root= testProgram({
124
129
  key: "app",
125
130
  description: "",
126
131
  commands: [{ key: "hello", description: "", handler: () => {} }],
127
- };
132
+ });
128
133
  cliValidateProgram(root);
129
134
  const pr = parse(root, ["nope"]);
130
135
  expect(pr.kind).toBe(ParseKind.Error);
@@ -132,11 +137,11 @@ test("unknown command", () => {
132
137
  });
133
138
 
134
139
  test("implicit help empty", () => {
135
- const root: CliProgram = {
140
+ const root= testProgram({
136
141
  key: "app",
137
142
  description: "",
138
143
  commands: [{ key: "x", description: "", handler: () => {} }],
139
- };
144
+ });
140
145
  cliValidateProgram(root);
141
146
  const pr = parse(root, []);
142
147
  expect(pr.kind).toBe(ParseKind.Help);
@@ -144,7 +149,7 @@ test("implicit help empty", () => {
144
149
  });
145
150
 
146
151
  test("invalid number post validate", () => {
147
- const root: CliProgram = {
152
+ const root= testProgram({
148
153
  key: "app",
149
154
  description: "",
150
155
  commands: [
@@ -161,7 +166,7 @@ test("invalid number post validate", () => {
161
166
  handler: () => {},
162
167
  },
163
168
  ],
164
- };
169
+ });
165
170
  cliValidateProgram(root);
166
171
  let pr = parse(root, ["x", "--n", "notnum"]);
167
172
  pr = postParseValidate(root, pr);
@@ -170,7 +175,7 @@ test("invalid number post validate", () => {
170
175
  });
171
176
 
172
177
  test("supports scientific notation in numbers", () => {
173
- const root: CliProgram = {
178
+ const root= testProgram({
174
179
  key: "app",
175
180
  description: "",
176
181
  commands: [
@@ -187,7 +192,7 @@ test("supports scientific notation in numbers", () => {
187
192
  handler: () => {},
188
193
  },
189
194
  ],
190
- };
195
+ });
191
196
  cliValidateProgram(root);
192
197
  let pr = parse(root, ["x", "--n", "1.23e4"]);
193
198
  pr = postParseValidate(root, pr);
@@ -198,35 +203,35 @@ test("supports scientific notation in numbers", () => {
198
203
 
199
204
 
200
205
  test("completion scripts contain app name", () => {
201
- const root: CliProgram = {
206
+ const root= testProgram({
202
207
  key: "myapp",
203
208
  description: "Test",
204
209
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
205
- };
210
+ });
206
211
  cliValidateProgram(root);
207
- const bash = completionBashScript(root);
212
+ const bash = completionBashScript(cliPresentationRoot(root));
208
213
  expect(bash).toContain("bash completion for myapp");
209
214
  expect(bash).toContain("complete -F _myapp myapp");
210
215
 
211
- const zsh = completionZshScript(root);
216
+ const zsh = completionZshScript(cliPresentationRoot(root));
212
217
  expect(zsh).toContain("#compdef myapp");
213
218
  expect(zsh).toContain("compdef _myapp myapp");
214
219
  expect(zsh).toContain("hello:Say hello.");
215
220
  });
216
221
 
217
222
  test("completion scripts do not emit invalid bash substitutions", () => {
218
- const root: CliProgram = {
223
+ const root= testProgram({
219
224
  key: "app",
220
225
  description: "Test",
221
226
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
222
- };
227
+ });
223
228
  cliValidateProgram(root);
224
- const bash = completionBashScript(root);
229
+ const bash = completionBashScript(cliPresentationRoot(root));
225
230
  expect(bash).not.toContain("${${");
226
231
  });
227
232
 
228
233
  test("completion scripts escape shell-sensitive command text in zsh", () => {
229
- const root: CliProgram = {
234
+ const root= testProgram({
230
235
  key: "app",
231
236
  description: "Test",
232
237
  commands: [
@@ -236,29 +241,29 @@ test("completion scripts escape shell-sensitive command text in zsh", () => {
236
241
  handler: () => {},
237
242
  },
238
243
  ],
239
- };
244
+ });
240
245
  cliValidateProgram(root);
241
- const zsh = completionZshScript(root);
246
+ const zsh = completionZshScript(cliPresentationRoot(root));
242
247
  expect(zsh).toContain("quote'\\''cmd:Say '\\''hello'\\'' and keep going.");
243
248
  });
244
249
 
245
250
  test("completion scripts keep dotted app names in registration names", () => {
246
- const root: CliProgram = {
251
+ const root= testProgram({
247
252
  key: "minimal.ts",
248
253
  description: "Test",
249
254
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
250
- };
255
+ });
251
256
  cliValidateProgram(root);
252
257
 
253
- const bash = completionBashScript(root);
258
+ const bash = completionBashScript(cliPresentationRoot(root));
254
259
  expect(bash).toContain("complete -F _minimal_ts minimal.ts");
255
260
 
256
- const zsh = completionZshScript(root);
261
+ const zsh = completionZshScript(cliPresentationRoot(root));
257
262
  expect(zsh).toContain("compdef _minimal_ts minimal.ts");
258
263
  });
259
264
 
260
265
  test("trailing options after bounded positionals", () => {
261
- const root: CliProgram = {
266
+ const root= testProgram({
262
267
  key: "app",
263
268
  description: "",
264
269
  commands: [
@@ -282,7 +287,7 @@ test("trailing options after bounded positionals", () => {
282
287
  handler: () => {},
283
288
  },
284
289
  ],
285
- };
290
+ });
286
291
  cliValidateProgram(root);
287
292
  const pr = postParseValidate(root, parse(root, ["x", "./file", "--verbose"]));
288
293
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -291,7 +296,7 @@ test("trailing options after bounded positionals", () => {
291
296
  });
292
297
 
293
298
  test("trailing options include parent-scoped flags", () => {
294
- const root: CliProgram = {
299
+ const root= testProgram({
295
300
  key: "app",
296
301
  description: "",
297
302
  commands: [
@@ -329,7 +334,7 @@ test("trailing options include parent-scoped flags", () => {
329
334
  ],
330
335
  },
331
336
  ],
332
- };
337
+ });
333
338
  cliValidateProgram(root);
334
339
  const pr = postParseValidate(root, parse(root, ["group", "leaf", "-u", "alice", "./file", "--json"]));
335
340
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -340,7 +345,7 @@ test("trailing options include parent-scoped flags", () => {
340
345
  });
341
346
 
342
347
  test("varargs tail parses trailing options", () => {
343
- const root: CliProgram = {
348
+ const root= testProgram({
344
349
  key: "app",
345
350
  description: "",
346
351
  commands: [
@@ -366,7 +371,7 @@ test("varargs tail parses trailing options", () => {
366
371
  handler: () => {},
367
372
  },
368
373
  ],
369
- };
374
+ });
370
375
  cliValidateProgram(root);
371
376
  const pr = postParseValidate(root, parse(root, ["x", "./file", "--json"]));
372
377
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -375,7 +380,7 @@ test("varargs tail parses trailing options", () => {
375
380
  });
376
381
 
377
382
  test("stops parsing options at --", () => {
378
- const root: CliProgram = {
383
+ const root= testProgram({
379
384
  key: "app",
380
385
  description: "",
381
386
  commands: [
@@ -401,7 +406,7 @@ test("stops parsing options at --", () => {
401
406
  handler: () => {},
402
407
  },
403
408
  ],
404
- };
409
+ });
405
410
  cliValidateProgram(root);
406
411
  const pr = postParseValidate(root, parse(root, ["x", "--name", "pat", "--", "--name", "bob", "-x"]));
407
412
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -410,7 +415,7 @@ test("stops parsing options at --", () => {
410
415
  });
411
416
 
412
417
  test("missing required option returns error", () => {
413
- const root: CliProgram = {
418
+ const root= testProgram({
414
419
  key: "app",
415
420
  description: "",
416
421
  options: [
@@ -428,7 +433,7 @@ test("missing required option returns error", () => {
428
433
  handler: () => {},
429
434
  },
430
435
  ],
431
- };
436
+ });
432
437
  cliValidateProgram(root);
433
438
  const pr = postParseValidate(root, parse(root, ["x"]));
434
439
  expect(pr.kind).toBe(ParseKind.Error);
@@ -436,7 +441,7 @@ test("missing required option returns error", () => {
436
441
  });
437
442
 
438
443
  test("provided required option parses ok", () => {
439
- const root: CliProgram = {
444
+ const root= testProgram({
440
445
  key: "app",
441
446
  description: "",
442
447
  commands: [
@@ -454,7 +459,7 @@ test("provided required option parses ok", () => {
454
459
  handler: () => {},
455
460
  },
456
461
  ],
457
- };
462
+ });
458
463
  cliValidateProgram(root);
459
464
  const pr = postParseValidate(root, parse(root, ["x", "--req", "val"]));
460
465
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -462,7 +467,7 @@ test("provided required option parses ok", () => {
462
467
  });
463
468
 
464
469
  test("presence option cannot be required", () => {
465
- const root: CliProgram = {
470
+ const root= testProgram({
466
471
  key: "app",
467
472
  description: "",
468
473
  options: [
@@ -480,7 +485,7 @@ test("presence option cannot be required", () => {
480
485
  handler: () => {},
481
486
  },
482
487
  ],
483
- };
488
+ });
484
489
  expect(() => cliValidateProgram(root)).toThrow(/Presence option cannot be required/);
485
490
  });
486
491
 
@@ -519,7 +524,13 @@ test("--schema exports JSON for leaf roots", async () => {
519
524
  expect(schema.key).toBe("minimal.ts");
520
525
  expect(schema.positionals[0].name).toBe("name");
521
526
  expect(schema.options[0].name).toBe("verbose");
522
- expect(schema.commands.map((c: { key: string }) => c.key)).toEqual(["completion", "install"]);
527
+ expect(schema.commands.map((c: { key: string }) => c.key)).toEqual(["completion", "version", "install"]);
528
+ });
529
+
530
+ test("version builtin prints program version", async () => {
531
+ const { stdout, exitCode } = await $`bun run examples/nested.ts version`.nothrow().quiet();
532
+ expect(exitCode).toBe(0);
533
+ expect(stdout.toString().trim()).toMatch(/^\d+\.\d+\.\d+/);
523
534
  });
524
535
 
525
536
  test("leaf root help lists completion built-in", async () => {
@@ -530,7 +541,7 @@ test("leaf root help lists completion built-in", async () => {
530
541
  });
531
542
 
532
543
  test("parse recognizes --schema at the program root", () => {
533
- const root: CliProgram = {
544
+ const root= testProgram({
534
545
  key: "app",
535
546
  description: "demo",
536
547
  commands: [
@@ -540,14 +551,14 @@ test("parse recognizes --schema at the program root", () => {
540
551
  handler: () => {},
541
552
  },
542
553
  ],
543
- };
554
+ });
544
555
  cliValidateProgram(root);
545
556
  const pr = parse(root, ["--schema"]);
546
557
  expect(pr.kind).toBe(ParseKind.Schema);
547
558
  });
548
559
 
549
560
  test("cliSchemaJson omits handlers and completion built-ins", () => {
550
- const root: CliProgram = {
561
+ const root= testProgram({
551
562
  key: "app",
552
563
  description: "demo",
553
564
  commands: [
@@ -568,7 +579,7 @@ test("cliSchemaJson omits handlers and completion built-ins", () => {
568
579
  ],
569
580
  },
570
581
  ],
571
- };
582
+ });
572
583
 
573
584
  const schema = JSON.parse(cliSchemaJson(root));
574
585
  expect(schema.commands).toHaveLength(1);
@@ -577,7 +588,7 @@ test("cliSchemaJson omits handlers and completion built-ins", () => {
577
588
  });
578
589
 
579
590
  test("reserved option name schema is rejected", () => {
580
- const root: CliProgram = {
591
+ const root= testProgram({
581
592
  key: "app",
582
593
  description: "",
583
594
  commands: [
@@ -594,12 +605,12 @@ test("reserved option name schema is rejected", () => {
594
605
  handler: () => {},
595
606
  },
596
607
  ],
597
- };
608
+ });
598
609
  expect(() => cliValidateProgram(root)).toThrow(/reserved for --schema/);
599
610
  });
600
611
 
601
612
  test("root help lists --schema built-in", () => {
602
- const root: CliProgram = {
613
+ const root= testProgram({
603
614
  key: "app",
604
615
  description: "demo",
605
616
  commands: [
@@ -609,14 +620,14 @@ test("root help lists --schema built-in", () => {
609
620
  handler: () => {},
610
621
  },
611
622
  ],
612
- };
623
+ });
613
624
  const help = cliHelpRender(cliPresentationRoot(root), [], false);
614
625
  expect(help).toContain("--schema");
615
626
  expect(help).toContain("Print the full command tree as JSON.");
616
627
  });
617
628
 
618
629
  test("nested help omits --schema built-in", () => {
619
- const root: CliProgram = {
630
+ const root= testProgram({
620
631
  key: "app",
621
632
  description: "demo",
622
633
  commands: [
@@ -626,13 +637,13 @@ test("nested help omits --schema built-in", () => {
626
637
  handler: () => {},
627
638
  },
628
639
  ],
629
- };
640
+ });
630
641
  const help = cliHelpRender(cliPresentationRoot(root), ["x"], false);
631
642
  expect(help).not.toContain("--schema");
632
643
  });
633
644
 
634
645
  test("completion scripts offer --schema at the program root only", () => {
635
- const root: CliProgram = {
646
+ const root= testProgram({
636
647
  key: "myapp",
637
648
  description: "",
638
649
  commands: [
@@ -648,20 +659,21 @@ test("completion scripts offer --schema at the program root only", () => {
648
659
  ],
649
660
  },
650
661
  ],
651
- };
662
+ });
652
663
 
653
- const bash = completionBashScript(root);
664
+ const bash = completionBashScript(cliPresentationRoot(root));
654
665
  expect(bash).toContain("A_myapp_0_opts+=('--schema')");
655
666
  expect(bash).not.toContain("A_myapp_1_opts+=('--schema')");
656
667
 
657
- const zsh = completionZshScript(root);
668
+ const zsh = completionZshScript(cliPresentationRoot(root));
658
669
  expect(zsh).toContain("'--schema:Print the full command tree as JSON.'");
659
670
  });
660
671
 
661
- const nestedMcpFixture: CliProgram = {
672
+ const nestedMcpFixture = testProgram({
662
673
  key: "nested.ts",
663
674
  description: "Nested groups demo.",
664
- mcpServer: { name: "nested-demo", version: "1.0.0" },
675
+ version: "1.0.0",
676
+ mcpServer: { enabled: true },
665
677
  commands: [
666
678
  {
667
679
  key: "stat",
@@ -724,7 +736,7 @@ const nestedMcpFixture: CliProgram = {
724
736
  ],
725
737
  fallbackCommand: "read",
726
738
  fallbackMode: CliFallbackMode.MissingOrUnknown,
727
- };
739
+ });
728
740
 
729
741
  /** Sends NDJSON MCP requests to a subprocess and collects responses by id. */
730
742
  async function mcpRequest(
@@ -814,7 +826,7 @@ test("mcpToolCallToArgv expands varargs positionals", () => {
814
826
  });
815
827
 
816
828
  test("reserved command name install is rejected", () => {
817
- const root: CliProgram = {
829
+ const root= testProgram({
818
830
  key: "app",
819
831
  description: "",
820
832
  commands: [
@@ -824,12 +836,12 @@ test("reserved command name install is rejected", () => {
824
836
  handler: () => {},
825
837
  },
826
838
  ],
827
- };
839
+ });
828
840
  expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: install/);
829
841
  });
830
842
 
831
843
  test("top-level command name mcp is allowed without mcpServer", () => {
832
- const root: CliProgram = {
844
+ const root= testProgram({
833
845
  key: "app",
834
846
  description: "",
835
847
  commands: [
@@ -839,15 +851,15 @@ test("top-level command name mcp is allowed without mcpServer", () => {
839
851
  handler: () => {},
840
852
  },
841
853
  ],
842
- };
854
+ });
843
855
  expect(() => cliValidateProgram(root)).not.toThrow();
844
856
  });
845
857
 
846
- test("top-level command name mcp is rejected when mcpServer is set", () => {
847
- const root: CliProgram = {
858
+ test("top-level command name mcp is rejected when mcpServer is enabled", () => {
859
+ const root = testProgram({
848
860
  key: "app",
849
861
  description: "",
850
- mcpServer: {},
862
+ mcpServer: { enabled: true },
851
863
  commands: [
852
864
  {
853
865
  key: "mcp",
@@ -855,19 +867,20 @@ test("top-level command name mcp is rejected when mcpServer is set", () => {
855
867
  handler: () => {},
856
868
  },
857
869
  ],
858
- };
870
+ });
859
871
  expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: mcp/);
860
872
  });
861
873
 
862
874
  test("mcpServer on non-root node is rejected", () => {
863
875
  const root = {
864
876
  key: "app",
877
+ version: "0.0.0",
865
878
  description: "",
866
879
  commands: [
867
880
  {
868
881
  key: "x",
869
882
  description: "cmd",
870
- mcpServer: {},
883
+ mcpServer: { enabled: true },
871
884
  handler: () => {},
872
885
  },
873
886
  ],
@@ -876,17 +889,17 @@ test("mcpServer on non-root node is rejected", () => {
876
889
  });
877
890
 
878
891
  test("mcpTool on root is rejected", () => {
879
- const root: CliProgram = {
892
+ const root= testProgram({
880
893
  key: "app",
881
894
  description: "",
882
895
  mcpTool: { enabled: false },
883
896
  handler: () => {},
884
- };
897
+ });
885
898
  expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
886
899
  });
887
900
 
888
901
  test("mcpTool on routing node is rejected", () => {
889
- const root: CliProgram = {
902
+ const root= testProgram({
890
903
  key: "app",
891
904
  description: "",
892
905
  commands: [
@@ -903,7 +916,7 @@ test("mcpTool on routing node is rejected", () => {
903
916
  ],
904
917
  },
905
918
  ],
906
- };
919
+ });
907
920
  expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
908
921
  });
909
922
 
@@ -964,7 +977,7 @@ test("MCP tools/list includes stat_owner_lookup", async () => {
964
977
 
965
978
  test("MCP resources/read returns schema JSON", async () => {
966
979
  const responses = await mcpRequest([
967
- { jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "argsbarg://schema" } },
980
+ { jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "nested_ts://schema" } },
968
981
  ]);
969
982
  const res = responses.get(3) as { result: { contents: { text: string }[] } };
970
983
  const schema = JSON.parse(res.result.contents[0]!.text);
@@ -1044,7 +1057,7 @@ test("ctx.invocation is cli via cliRun", async () => {
1044
1057
  const indexPath = join(import.meta.dir, "index.ts");
1045
1058
  const { stdout } = await $`bun -e ${`
1046
1059
  import { cliRun, CliProgram } from ${JSON.stringify(indexPath)};
1047
- const cli = { key: "t", description: "d", handler: (ctx) => console.log(ctx.invocation) };
1060
+ const cli = { key: "t", description: "d", version: "0.0.0", handler: (ctx) => console.log(ctx.invocation) };
1048
1061
  await cliRun(cli, []);
1049
1062
  `}`.quiet();
1050
1063
  expect(stdout.toString().trim()).toBe("cli");
@@ -1052,23 +1065,23 @@ await cliRun(cli, []);
1052
1065
 
1053
1066
  test("ctx.invocation is mcp via cliInvoke", async () => {
1054
1067
  let seen = "";
1055
- const root: CliProgram = {
1068
+ const root= testProgram({
1056
1069
  key: "app",
1057
1070
  description: "",
1058
- handler: (ctx) => {
1071
+ handler: (ctx: CliContext) => {
1059
1072
  seen = ctx.invocation;
1060
1073
  },
1061
- };
1074
+ });
1062
1075
  cliValidateProgram(root);
1063
1076
  const result = await cliInvoke(root, []);
1064
1077
  expect(result.kind).toBe("ok");
1065
1078
  expect(seen).toBe("mcp");
1066
1079
  });
1067
1080
 
1068
- const enumMcpFixture: CliProgram = {
1081
+ const enumMcpFixture = testProgram({
1069
1082
  key: "app",
1070
1083
  description: "",
1071
- mcpServer: {},
1084
+ mcpServer: { enabled: true },
1072
1085
  commands: [
1073
1086
  {
1074
1087
  key: "run",
@@ -1085,7 +1098,7 @@ const enumMcpFixture: CliProgram = {
1085
1098
  handler: () => {},
1086
1099
  },
1087
1100
  ],
1088
- };
1101
+ });
1089
1102
 
1090
1103
  test("Enum option inputSchema includes enum array", () => {
1091
1104
  const tools = collectMcpTools(enumMcpFixture);
@@ -1095,7 +1108,7 @@ test("Enum option inputSchema includes enum array", () => {
1095
1108
  });
1096
1109
 
1097
1110
  test("cliInvoke rejects invalid Enum value", async () => {
1098
- const root: CliProgram = {
1111
+ const root= testProgram({
1099
1112
  key: "app",
1100
1113
  description: "",
1101
1114
  handler: () => {},
@@ -1108,7 +1121,7 @@ test("cliInvoke rejects invalid Enum value", async () => {
1108
1121
  required: true,
1109
1122
  },
1110
1123
  ],
1111
- };
1124
+ });
1112
1125
  cliValidateProgram(root);
1113
1126
  const result = await cliInvoke(root, ["--mode", "staging"]);
1114
1127
  expect(result.kind).toBe("error");
@@ -1116,10 +1129,10 @@ test("cliInvoke rejects invalid Enum value", async () => {
1116
1129
  });
1117
1130
 
1118
1131
  test("cliInvoke accepts valid Enum value", async () => {
1119
- const root: CliProgram = {
1132
+ const root= testProgram({
1120
1133
  key: "app",
1121
1134
  description: "",
1122
- handler: (ctx) => {
1135
+ handler: (ctx: CliContext) => {
1123
1136
  console.log(ctx.stringOpt("mode"));
1124
1137
  },
1125
1138
  options: [
@@ -1131,7 +1144,7 @@ test("cliInvoke accepts valid Enum value", async () => {
1131
1144
  required: true,
1132
1145
  },
1133
1146
  ],
1134
- };
1147
+ });
1135
1148
  cliValidateProgram(root);
1136
1149
  const result = await cliInvoke(root, ["--mode", "dev"]);
1137
1150
  expect(result.kind).toBe("ok");
@@ -1139,30 +1152,30 @@ test("cliInvoke accepts valid Enum value", async () => {
1139
1152
  });
1140
1153
 
1141
1154
  test("cliValidateProgram rejects Enum with no choices", () => {
1142
- const root: CliProgram = {
1155
+ const root= testProgram({
1143
1156
  key: "app",
1144
1157
  description: "",
1145
1158
  handler: () => {},
1146
1159
  options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: [] }],
1147
- };
1160
+ });
1148
1161
  expect(() => cliValidateProgram(root)).toThrow(/requires non-empty choices/);
1149
1162
  });
1150
1163
 
1151
1164
  test("cliValidateProgram rejects Enum with duplicate choices", () => {
1152
- const root: CliProgram = {
1165
+ const root= testProgram({
1153
1166
  key: "app",
1154
1167
  description: "",
1155
1168
  handler: () => {},
1156
1169
  options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: ["a", "a"] }],
1157
- };
1170
+ });
1158
1171
  expect(() => cliValidateProgram(root)).toThrow(/choices must be distinct/);
1159
1172
  });
1160
1173
 
1161
1174
  test("mcpTool.description override wins without requiresEnv suffix", () => {
1162
- const root: CliProgram = {
1175
+ const root= testProgram({
1163
1176
  key: "app",
1164
1177
  description: "",
1165
- mcpServer: {},
1178
+ mcpServer: { enabled: true },
1166
1179
  commands: [
1167
1180
  {
1168
1181
  key: "x",
@@ -1171,16 +1184,16 @@ test("mcpTool.description override wins without requiresEnv suffix", () => {
1171
1184
  handler: () => {},
1172
1185
  },
1173
1186
  ],
1174
- };
1187
+ });
1175
1188
  const tools = collectMcpTools(root);
1176
1189
  expect(tools[0]!.description).toBe("custom");
1177
1190
  });
1178
1191
 
1179
1192
  test("mcpTool.requiresEnv appended to auto description", () => {
1180
- const root: CliProgram = {
1193
+ const root= testProgram({
1181
1194
  key: "app",
1182
1195
  description: "",
1183
- mcpServer: {},
1196
+ mcpServer: { enabled: true },
1184
1197
  commands: [
1185
1198
  {
1186
1199
  key: "x",
@@ -1189,50 +1202,96 @@ test("mcpTool.requiresEnv appended to auto description", () => {
1189
1202
  handler: () => {},
1190
1203
  },
1191
1204
  ],
1192
- };
1205
+ });
1193
1206
  const tools = collectMcpTools(root);
1194
1207
  expect(tools[0]!.description).toContain("[requires env: TOKEN]");
1195
1208
  });
1196
1209
 
1197
1210
  test("cliValidateProgram rejects duplicate mcpResources URIs", () => {
1198
- const root: CliProgram = {
1211
+ const root = testProgram({
1199
1212
  key: "app",
1200
1213
  description: "",
1201
1214
  mcpServer: {
1215
+ enabled: true,
1202
1216
  resources: [
1203
1217
  { uri: "a://1", name: "a", load: () => "a" },
1204
1218
  { uri: "a://1", name: "b", load: () => "b" },
1205
1219
  ],
1206
1220
  },
1207
1221
  commands: [{ key: "x", description: "", handler: () => {} }],
1208
- };
1222
+ });
1209
1223
  expect(() => cliValidateProgram(root)).toThrow(/URIs must be unique/);
1210
1224
  });
1211
1225
 
1226
+ test("cliValidateProgram rejects empty mcpServer", () => {
1227
+ const root = testProgram({
1228
+ key: "app",
1229
+ description: "",
1230
+ mcpServer: {} as { enabled: boolean },
1231
+ handler: () => {},
1232
+ });
1233
+ expect(() => cliValidateProgram(root)).toThrow(/mcpServer requires enabled: true/);
1234
+ });
1235
+
1236
+ test("resolveMcpSchemaUri uses sanitized root key", () => {
1237
+ const root = testProgram({
1238
+ key: "nested.ts",
1239
+ description: "",
1240
+ mcpServer: { enabled: true },
1241
+ handler: () => {},
1242
+ });
1243
+ expect(resolveMcpSchemaUri(root)).toBe("nested_ts://schema");
1244
+ });
1245
+
1246
+ test("resolveMcpSchemaUri uses plain key when alphanumeric", () => {
1247
+ const root = testProgram({
1248
+ key: "qa",
1249
+ description: "",
1250
+ mcpServer: { enabled: true },
1251
+ handler: () => {},
1252
+ });
1253
+ expect(resolveMcpSchemaUri(root)).toBe("qa://schema");
1254
+ });
1255
+
1256
+ test("cliValidateProgram rejects resource URI matching default schema URI", () => {
1257
+ const root = testProgram({
1258
+ key: "app",
1259
+ description: "",
1260
+ mcpServer: {
1261
+ enabled: true,
1262
+ resources: [{ uri: "app://schema", name: "dup", load: () => "" }],
1263
+ },
1264
+ commands: [{ key: "x", description: "", handler: () => {} }],
1265
+ });
1266
+ expect(() => cliValidateProgram(root)).toThrow(/conflicts with the built-in schema resource/);
1267
+ });
1268
+
1212
1269
  test("cliValidateProgram rejects resource URI matching schemaResourceUri", () => {
1213
- const root: CliProgram = {
1270
+ const root = testProgram({
1214
1271
  key: "app",
1215
1272
  description: "",
1216
1273
  mcpServer: {
1274
+ enabled: true,
1217
1275
  schemaResourceUri: "custom://schema",
1218
1276
  resources: [{ uri: "custom://schema", name: "dup", load: () => "" }],
1219
1277
  },
1220
1278
  commands: [{ key: "x", description: "", handler: () => {} }],
1221
- };
1279
+ });
1222
1280
  expect(() => cliValidateProgram(root)).toThrow(/conflicts with the built-in schema resource/);
1223
1281
  });
1224
1282
 
1225
1283
  test("allMcpResources includes custom resources", () => {
1226
- const root: CliProgram = {
1284
+ const root = testProgram({
1227
1285
  key: "app",
1228
1286
  description: "",
1229
1287
  mcpServer: {
1288
+ enabled: true,
1230
1289
  resources: [{ uri: "test://x", name: "x", load: () => "body" }],
1231
1290
  },
1232
1291
  commands: [{ key: "leaf", description: "", handler: () => {} }],
1233
- };
1292
+ });
1234
1293
  const resources = allMcpResources(root);
1235
- expect(resources.map((r) => r.uri)).toContain("argsbarg://schema");
1294
+ expect(resources.map((r) => r.uri)).toContain("app://schema");
1236
1295
  expect(resources.map((r) => r.uri)).toContain("test://x");
1237
1296
  });
1238
1297
 
@@ -1266,7 +1325,7 @@ test("loadEnvFile overwrites existing keys", () => {
1266
1325
  });
1267
1326
 
1268
1327
  test("Enum completions list choices in bash script", () => {
1269
- const root: CliProgram = {
1328
+ const root= testProgram({
1270
1329
  key: "app",
1271
1330
  description: "",
1272
1331
  commands: [
@@ -1279,8 +1338,8 @@ test("Enum completions list choices in bash script", () => {
1279
1338
  handler: () => {},
1280
1339
  },
1281
1340
  ],
1282
- };
1283
- const bash = completionBashScript(root);
1341
+ });
1342
+ const bash = completionBashScript(cliPresentationRoot(root));
1284
1343
  expect(bash).toContain("--mode) COMPREPLY=");
1285
1344
  expect(bash).toContain("dev");
1286
1345
  expect(bash).toContain("prod");
@@ -1293,7 +1352,7 @@ test("MCP resources/list includes custom resource", async () => {
1293
1352
  );
1294
1353
  const res = responses.get(10) as { result: { resources: { uri: string }[] } };
1295
1354
  const uris = res.result.resources.map((r) => r.uri);
1296
- expect(uris).toContain("argsbarg://schema");
1355
+ expect(uris).toContain("mcp_test://schema");
1297
1356
  expect(uris).toContain("test://hello");
1298
1357
  });
1299
1358
 
@@ -1372,7 +1431,7 @@ test("MCP envFile loads vars for tool handlers", async () => {
1372
1431
  // ── v1.3 parser ergonomics ────────────────────────────────────────────────────
1373
1432
 
1374
1433
  function varargsReadFixture(): CliProgram {
1375
- return {
1434
+ return testProgram({
1376
1435
  key: "app",
1377
1436
  description: "",
1378
1437
  commands: [
@@ -1398,11 +1457,11 @@ function varargsReadFixture(): CliProgram {
1398
1457
  handler: () => {},
1399
1458
  },
1400
1459
  ],
1401
- };
1460
+ });
1402
1461
  }
1403
1462
 
1404
1463
  function nestedDocsFallbackFixture(): CliProgram {
1405
- return {
1464
+ return testProgram({
1406
1465
  key: "app",
1407
1466
  description: "",
1408
1467
  commands: [
@@ -1425,7 +1484,7 @@ function nestedDocsFallbackFixture(): CliProgram {
1425
1484
  ],
1426
1485
  },
1427
1486
  ],
1428
- };
1487
+ });
1429
1488
  }
1430
1489
 
1431
1490
  test("nested fallback routes to default when argv exhausted at router", () => {
@@ -1437,7 +1496,7 @@ test("nested fallback routes to default when argv exhausted at router", () => {
1437
1496
  });
1438
1497
 
1439
1498
  test("nested fallback MissingOrUnknown routes unknown token to default", () => {
1440
- const root: CliProgram = {
1499
+ const root= testProgram({
1441
1500
  key: "app",
1442
1501
  description: "",
1443
1502
  commands: [
@@ -1469,7 +1528,7 @@ test("nested fallback MissingOrUnknown routes unknown token to default", () => {
1469
1528
  ],
1470
1529
  },
1471
1530
  ],
1472
- };
1531
+ });
1473
1532
  cliValidateProgram(root);
1474
1533
  const pr = postParseValidate(root, parse(root, ["docs", "extra-topic"]));
1475
1534
  expect(pr.kind).toBe(ParseKind.Ok);
@@ -1486,7 +1545,7 @@ test("nested fallback MissingOnly errors on unknown subcommand", () => {
1486
1545
  });
1487
1546
 
1488
1547
  test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
1489
- const root: CliProgram = {
1548
+ const root= testProgram({
1490
1549
  key: "app",
1491
1550
  description: "",
1492
1551
  commands: [
@@ -1503,7 +1562,7 @@ test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
1503
1562
  ],
1504
1563
  },
1505
1564
  ],
1506
- };
1565
+ });
1507
1566
  expect(() => cliValidateProgram(root)).toThrow(/fallbackCommand 'missing' is not a child of 'docs'/);
1508
1567
  });
1509
1568
 
@@ -1578,7 +1637,7 @@ test("varargs scoped help in tail", () => {
1578
1637
  });
1579
1638
 
1580
1639
  test("ctx.positional returns single slot value", async () => {
1581
- const root: CliProgram = {
1640
+ const root= testProgram({
1582
1641
  key: "app",
1583
1642
  description: "",
1584
1643
  commands: [
@@ -1586,12 +1645,12 @@ test("ctx.positional returns single slot value", async () => {
1586
1645
  key: "x",
1587
1646
  description: "",
1588
1647
  positionals: [{ name: "path", description: "", kind: CliOptionKind.String }],
1589
- handler: (ctx) => {
1648
+ handler: (ctx: CliContext) => {
1590
1649
  captured = ctx.positional("path");
1591
1650
  },
1592
1651
  },
1593
1652
  ],
1594
- };
1653
+ });
1595
1654
  let captured: string | string[] | undefined;
1596
1655
  cliValidateProgram(root);
1597
1656
  await cliInvoke(root, ["x", "./file"]);
@@ -1612,7 +1671,7 @@ test("ctx.positional returns varargs array", async () => {
1612
1671
  });
1613
1672
 
1614
1673
  test("ctx.positional returns undefined for absent optional slot", async () => {
1615
- const root: CliProgram = {
1674
+ const root= testProgram({
1616
1675
  key: "app",
1617
1676
  description: "",
1618
1677
  commands: [
@@ -1622,12 +1681,12 @@ test("ctx.positional returns undefined for absent optional slot", async () => {
1622
1681
  positionals: [
1623
1682
  { name: "opt", description: "", kind: CliOptionKind.String, argMin: 0, argMax: 1 },
1624
1683
  ],
1625
- handler: (ctx) => {
1684
+ handler: (ctx: CliContext) => {
1626
1685
  captured = ctx.positional("opt");
1627
1686
  },
1628
1687
  },
1629
1688
  ],
1630
- };
1689
+ });
1631
1690
  let captured: string | string[] | undefined;
1632
1691
  cliValidateProgram(root);
1633
1692
  await cliInvoke(root, ["x"]);
@@ -1682,6 +1741,7 @@ test("mcpToolCallToArgv empty string varargs appends nothing", () => {
1682
1741
  test("install config on non-root node is rejected", () => {
1683
1742
  const root = {
1684
1743
  key: "app",
1744
+ version: "0.0.0",
1685
1745
  description: "",
1686
1746
  commands: [
1687
1747
  {
@@ -1700,7 +1760,10 @@ test("generateSkillBundle includes frontmatter and command catalog", () => {
1700
1760
  expect(bundle.dirName).toBe("nested_ts");
1701
1761
  expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
1702
1762
  expect(bundle.skillMd).toContain("stat owner lookup");
1703
- expect(bundle.skillMd).toContain("nested.ts mcp");
1763
+ expect(bundle.skillMd).toContain("Invoke via shell:");
1764
+ expect(bundle.skillMd).not.toContain("mcp.json");
1765
+ expect(bundle.skillMd).not.toContain("Prefer MCP");
1766
+ expect(bundle.skillMd).not.toContain("tools/call");
1704
1767
  expect(bundle.referenceMd).toContain("```json");
1705
1768
  expect(() => JSON.parse(bundle.referenceMd.match(/```json\n([\s\S]*?)\n```/)![1]!)).not.toThrow();
1706
1769
  });