argsbarg 3.4.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +24 -8
  3. package/biome.json +29 -6
  4. package/bun.lock +22 -0
  5. package/docs/cli-program.md +75 -1
  6. package/docs/install.md +1 -1
  7. package/docs/mcp.md +45 -1
  8. package/docs/templates/cursor/rules/cli-program.mdc +15 -21
  9. package/index.d.ts +95 -50
  10. package/justfile +24 -6
  11. package/package.json +4 -2
  12. package/scripts/release.ts +26 -9
  13. package/src/builtins/builtins.test.ts +9 -4
  14. package/src/builtins/completion-bash.ts +74 -50
  15. package/src/builtins/completion-fish.ts +3 -8
  16. package/src/builtins/completion-group.ts +1 -1
  17. package/src/builtins/completion-zsh.ts +80 -42
  18. package/src/builtins/dispatch.ts +20 -16
  19. package/src/builtins/export.ts +19 -10
  20. package/src/builtins/index.ts +9 -4
  21. package/src/builtins/install.ts +10 -10
  22. package/src/builtins/mcp.ts +1 -1
  23. package/src/builtins/presentation.ts +8 -8
  24. package/src/builtins/scopes.ts +1 -1
  25. package/src/builtins/version.ts +1 -1
  26. package/src/completion.ts +4 -4
  27. package/src/context.ts +91 -15
  28. package/src/docs/api-guide.test.ts +2 -2
  29. package/src/docs/api-guide.ts +19 -5
  30. package/src/docs/builtin.ts +27 -8
  31. package/src/docs/docs.test.ts +18 -11
  32. package/src/docs/mcp-guide.ts +108 -25
  33. package/src/docs/resolve.ts +10 -3
  34. package/src/docs/save.ts +11 -3
  35. package/src/formats.test.ts +35 -0
  36. package/src/formats.ts +135 -0
  37. package/src/headless.test.ts +8 -16
  38. package/src/help.ts +73 -43
  39. package/src/hidden-mcpb.test.ts +7 -6
  40. package/src/hidden.ts +2 -2
  41. package/src/index.test.ts +120 -96
  42. package/src/index.ts +36 -24
  43. package/src/install/binary.ts +12 -5
  44. package/src/install/completions.ts +7 -3
  45. package/src/install/detect-installed.ts +29 -4
  46. package/src/install/gh-release-update.ts +31 -23
  47. package/src/install/index.ts +69 -19
  48. package/src/install/install.test.ts +31 -8
  49. package/src/install/mcp-codex.test.ts +57 -0
  50. package/src/install/mcp-codex.ts +125 -0
  51. package/src/install/mcp-config.ts +12 -5
  52. package/src/install/mcp-opencode.test.ts +98 -0
  53. package/src/install/mcp-opencode.ts +149 -0
  54. package/src/install/paths.ts +29 -3
  55. package/src/install/plan.ts +73 -6
  56. package/src/install/shell.ts +1 -4
  57. package/src/install/status.ts +12 -6
  58. package/src/install/uninstall.ts +38 -4
  59. package/src/install/update.test.ts +2 -2
  60. package/src/install/update.ts +3 -1
  61. package/src/invoke.ts +12 -9
  62. package/src/mcp/bundle.ts +36 -8
  63. package/src/mcp/env.ts +7 -13
  64. package/src/mcp/server.ts +12 -6
  65. package/src/mcp/tools.ts +83 -18
  66. package/src/mcp.ts +3 -3
  67. package/src/parse.ts +129 -27
  68. package/src/runtime.ts +22 -12
  69. package/src/schema.ts +11 -5
  70. package/src/skill/generate.ts +4 -4
  71. package/src/skill/install.ts +6 -2
  72. package/src/types.ts +24 -0
  73. package/src/validate.ts +75 -16
package/src/index.test.ts CHANGED
@@ -7,12 +7,19 @@ It keeps the CLI contract stable by catching routing, option handling, and gener
7
7
  shell output regressions.
8
8
  */
9
9
 
10
+ import { expect, test } from "bun:test";
10
11
  import { cliPresentationRoot } from "./builtins/presentation.ts";
11
12
  import { completionBashScript, completionZshScript } from "./completion.ts";
12
13
  import { cliHelpRender } from "./help.ts";
13
- import { CliProgram, CliFallbackMode, CliOptionKind, cliInvoke, CliContext } from "./index.ts";
14
- import type { CliLeaf } from "./types.ts";
15
- import { isCliRouter } from "./types.ts";
14
+ import {
15
+ type CliContext,
16
+ CliFallbackMode,
17
+ CliOptionKind,
18
+ type CliProgram,
19
+ cliInvoke,
20
+ } from "./index.ts";
21
+ import { applyShellEnv, loadEnvFile } from "./mcp/env.ts";
22
+ import { buildToolCallSuccess } from "./mcp/result.ts";
16
23
  import {
17
24
  allMcpResources,
18
25
  collectMcpTools,
@@ -21,25 +28,27 @@ import {
21
28
  resolveMcpSchemaUri,
22
29
  sanitizeToolSegment,
23
30
  } from "./mcp/tools.ts";
24
- import { applyShellEnv, loadEnvFile } from "./mcp/env.ts";
25
- import { buildToolCallSuccess } from "./mcp/result.ts";
26
- import { generateSkillBundle } from "./skill/generate.ts";
27
- import { cliSkillInstall } from "./skill/install.ts";
28
31
  import { ParseKind, parse, postParseValidate } from "./parse.ts";
29
32
  import { cliSchemaExport, cliSchemaJson } from "./schema.ts";
33
+ import { generateSkillBundle } from "./skill/generate.ts";
34
+ import { cliSkillInstall } from "./skill/install.ts";
35
+ import type { CliLeaf } from "./types.ts";
36
+ import { isCliRouter } from "./types.ts";
30
37
  import { cliValidateProgram } from "./validate.ts";
31
- import { expect, test } from "bun:test";
32
38
 
33
- function testProgram(prog: Record<string, unknown> & { key: string; description: string }): CliProgram {
39
+ function testProgram(
40
+ prog: Record<string, unknown> & { key: string; description: string },
41
+ ): CliProgram {
34
42
  return { version: "0.0.0", ...prog } as CliProgram;
35
43
  }
36
- import { $ } from "bun";
44
+
37
45
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
38
46
  import { tmpdir } from "node:os";
39
47
  import { join } from "node:path";
48
+ import { $ } from "bun";
40
49
 
41
50
  test("bundled short presence flags", () => {
42
- const root= testProgram({
51
+ const root = testProgram({
43
52
  key: "app",
44
53
  description: "",
45
54
  commands: [
@@ -67,12 +76,12 @@ test("bundled short presence flags", () => {
67
76
  cliValidateProgram(root);
68
77
  const pr = postParseValidate(root, parse(root, ["x", "-ab"]));
69
78
  expect(pr.kind).toBe(ParseKind.Ok);
70
- expect(pr.opts["a"]).toBe("1");
71
- expect(pr.opts["b"]).toBe("1");
79
+ expect(pr.opts.a).toBe("1");
80
+ expect(pr.opts.b).toBe("1");
72
81
  });
73
82
 
74
83
  test("long option equals", () => {
75
- const root= testProgram({
84
+ const root = testProgram({
76
85
  key: "app",
77
86
  description: "",
78
87
  commands: [
@@ -93,11 +102,11 @@ test("long option equals", () => {
93
102
  cliValidateProgram(root);
94
103
  const pr = postParseValidate(root, parse(root, ["x", "--name=pat"]));
95
104
  expect(pr.kind).toBe(ParseKind.Ok);
96
- expect(pr.opts["name"]).toBe("pat");
105
+ expect(pr.opts.name).toBe("pat");
97
106
  });
98
107
 
99
108
  test("fallback missing or unknown root flags", () => {
100
- const root= testProgram({
109
+ const root = testProgram({
101
110
  key: "app",
102
111
  description: "",
103
112
  commands: [
@@ -121,11 +130,11 @@ test("fallback missing or unknown root flags", () => {
121
130
  const pr = postParseValidate(root, parse(root, ["--name", "bob"]));
122
131
  expect(pr.kind).toBe(ParseKind.Ok);
123
132
  expect(pr.path).toEqual(["hello"]);
124
- expect(pr.opts["name"]).toBe("bob");
133
+ expect(pr.opts.name).toBe("bob");
125
134
  });
126
135
 
127
136
  test("unknown command", () => {
128
- const root= testProgram({
137
+ const root = testProgram({
129
138
  key: "app",
130
139
  description: "",
131
140
  commands: [{ key: "hello", description: "", handler: () => {} }],
@@ -137,7 +146,7 @@ test("unknown command", () => {
137
146
  });
138
147
 
139
148
  test("implicit help empty", () => {
140
- const root= testProgram({
149
+ const root = testProgram({
141
150
  key: "app",
142
151
  description: "",
143
152
  commands: [{ key: "x", description: "", handler: () => {} }],
@@ -149,7 +158,7 @@ test("implicit help empty", () => {
149
158
  });
150
159
 
151
160
  test("invalid number post validate", () => {
152
- const root= testProgram({
161
+ const root = testProgram({
153
162
  key: "app",
154
163
  description: "",
155
164
  commands: [
@@ -175,7 +184,7 @@ test("invalid number post validate", () => {
175
184
  });
176
185
 
177
186
  test("supports scientific notation in numbers", () => {
178
- const root= testProgram({
187
+ const root = testProgram({
179
188
  key: "app",
180
189
  description: "",
181
190
  commands: [
@@ -197,13 +206,11 @@ test("supports scientific notation in numbers", () => {
197
206
  let pr = parse(root, ["x", "--n", "1.23e4"]);
198
207
  pr = postParseValidate(root, pr);
199
208
  expect(pr.kind).toBe(ParseKind.Ok);
200
- expect(Number(pr.opts["n"])).toBe(12300);
209
+ expect(Number(pr.opts.n)).toBe(12300);
201
210
  });
202
211
 
203
-
204
-
205
212
  test("completion scripts contain app name", () => {
206
- const root= testProgram({
213
+ const root = testProgram({
207
214
  key: "myapp",
208
215
  description: "Test",
209
216
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
@@ -220,7 +227,7 @@ test("completion scripts contain app name", () => {
220
227
  });
221
228
 
222
229
  test("completion scripts do not emit invalid bash substitutions", () => {
223
- const root= testProgram({
230
+ const root = testProgram({
224
231
  key: "app",
225
232
  description: "Test",
226
233
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
@@ -231,7 +238,7 @@ test("completion scripts do not emit invalid bash substitutions", () => {
231
238
  });
232
239
 
233
240
  test("completion scripts escape shell-sensitive command text in zsh", () => {
234
- const root= testProgram({
241
+ const root = testProgram({
235
242
  key: "app",
236
243
  description: "Test",
237
244
  commands: [
@@ -248,7 +255,7 @@ test("completion scripts escape shell-sensitive command text in zsh", () => {
248
255
  });
249
256
 
250
257
  test("completion scripts keep dotted app names in registration names", () => {
251
- const root= testProgram({
258
+ const root = testProgram({
252
259
  key: "minimal.ts",
253
260
  description: "Test",
254
261
  commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
@@ -263,7 +270,7 @@ test("completion scripts keep dotted app names in registration names", () => {
263
270
  });
264
271
 
265
272
  test("trailing options after bounded positionals", () => {
266
- const root= testProgram({
273
+ const root = testProgram({
267
274
  key: "app",
268
275
  description: "",
269
276
  commands: [
@@ -292,11 +299,11 @@ test("trailing options after bounded positionals", () => {
292
299
  const pr = postParseValidate(root, parse(root, ["x", "./file", "--verbose"]));
293
300
  expect(pr.kind).toBe(ParseKind.Ok);
294
301
  expect(pr.args).toEqual(["./file"]);
295
- expect(pr.opts["verbose"]).toBe("1");
302
+ expect(pr.opts.verbose).toBe("1");
296
303
  });
297
304
 
298
305
  test("trailing options include parent-scoped flags", () => {
299
- const root= testProgram({
306
+ const root = testProgram({
300
307
  key: "app",
301
308
  description: "",
302
309
  commands: [
@@ -336,16 +343,19 @@ test("trailing options include parent-scoped flags", () => {
336
343
  ],
337
344
  });
338
345
  cliValidateProgram(root);
339
- const pr = postParseValidate(root, parse(root, ["group", "leaf", "-u", "alice", "./file", "--json"]));
346
+ const pr = postParseValidate(
347
+ root,
348
+ parse(root, ["group", "leaf", "-u", "alice", "./file", "--json"]),
349
+ );
340
350
  expect(pr.kind).toBe(ParseKind.Ok);
341
351
  expect(pr.path).toEqual(["group", "leaf"]);
342
352
  expect(pr.args).toEqual(["./file"]);
343
- expect(pr.opts["user"]).toBe("alice");
344
- expect(pr.opts["json"]).toBe("1");
353
+ expect(pr.opts.user).toBe("alice");
354
+ expect(pr.opts.json).toBe("1");
345
355
  });
346
356
 
347
357
  test("varargs tail parses trailing options", () => {
348
- const root= testProgram({
358
+ const root = testProgram({
349
359
  key: "app",
350
360
  description: "",
351
361
  commands: [
@@ -376,11 +386,11 @@ test("varargs tail parses trailing options", () => {
376
386
  const pr = postParseValidate(root, parse(root, ["x", "./file", "--json"]));
377
387
  expect(pr.kind).toBe(ParseKind.Ok);
378
388
  expect(pr.args).toEqual(["./file"]);
379
- expect(pr.opts["json"]).toBe("1");
389
+ expect(pr.opts.json).toBe("1");
380
390
  });
381
391
 
382
392
  test("stops parsing options at --", () => {
383
- const root= testProgram({
393
+ const root = testProgram({
384
394
  key: "app",
385
395
  description: "",
386
396
  commands: [
@@ -408,14 +418,17 @@ test("stops parsing options at --", () => {
408
418
  ],
409
419
  });
410
420
  cliValidateProgram(root);
411
- const pr = postParseValidate(root, parse(root, ["x", "--name", "pat", "--", "--name", "bob", "-x"]));
421
+ const pr = postParseValidate(
422
+ root,
423
+ parse(root, ["x", "--name", "pat", "--", "--name", "bob", "-x"]),
424
+ );
412
425
  expect(pr.kind).toBe(ParseKind.Ok);
413
- expect(pr.opts["name"]).toBe("pat");
426
+ expect(pr.opts.name).toBe("pat");
414
427
  expect(pr.args).toEqual(["--name", "bob", "-x"]);
415
428
  });
416
429
 
417
430
  test("missing required option returns error", () => {
418
- const root= testProgram({
431
+ const root = testProgram({
419
432
  key: "app",
420
433
  description: "",
421
434
  options: [
@@ -441,7 +454,7 @@ test("missing required option returns error", () => {
441
454
  });
442
455
 
443
456
  test("provided required option parses ok", () => {
444
- const root= testProgram({
457
+ const root = testProgram({
445
458
  key: "app",
446
459
  description: "",
447
460
  commands: [
@@ -463,11 +476,11 @@ test("provided required option parses ok", () => {
463
476
  cliValidateProgram(root);
464
477
  const pr = postParseValidate(root, parse(root, ["x", "--req", "val"]));
465
478
  expect(pr.kind).toBe(ParseKind.Ok);
466
- expect(pr.opts["req"]).toBe("val");
479
+ expect(pr.opts.req).toBe("val");
467
480
  });
468
481
 
469
482
  test("presence option cannot be required", () => {
470
- const root= testProgram({
483
+ const root = testProgram({
471
484
  key: "app",
472
485
  description: "",
473
486
  options: [
@@ -492,7 +505,9 @@ test("presence option cannot be required", () => {
492
505
  test("leaf completion help prints correctly", async () => {
493
506
  // Test the fix where `completion zsh -h` on a leaf root was incorrectly ignored.
494
507
  // We run this as a subprocess so we don't accidentally exit the test runner.
495
- const { stdout, stderr, exitCode } = await $`bun run examples/minimal.ts completion zsh -h`.nothrow().quiet();
508
+ const { stdout, stderr, exitCode } = await $`bun run examples/minimal.ts completion zsh -h`
509
+ .nothrow()
510
+ .quiet();
496
511
  const out = stdout.toString();
497
512
  expect(exitCode).toBe(0);
498
513
  expect(out).toContain("Show help for this command.");
@@ -501,7 +516,9 @@ test("leaf completion help prints correctly", async () => {
501
516
  });
502
517
 
503
518
  test("docs schema exports JSON for nested CLIs", async () => {
504
- const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts docs schema`.nothrow().quiet();
519
+ const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts docs schema`
520
+ .nothrow()
521
+ .quiet();
505
522
  expect(exitCode).toBe(0);
506
523
  expect(stderr.toString()).toBe("");
507
524
 
@@ -568,7 +585,7 @@ test("root --schema is no longer a flag", () => {
568
585
  });
569
586
 
570
587
  test("cliSchemaJson omits handlers and completion built-ins", () => {
571
- const root= testProgram({
588
+ const root = testProgram({
572
589
  key: "app",
573
590
  description: "demo",
574
591
  commands: [
@@ -805,7 +822,7 @@ async function mcpRequest(
805
822
  env: opts?.env ? { ...process.env, ...opts.env } : process.env,
806
823
  });
807
824
 
808
- const input = requests.map((r) => JSON.stringify(r) + "\n").join("");
825
+ const input = requests.map((r) => `${JSON.stringify(r)}\n`).join("");
809
826
  proc.stdin.write(input);
810
827
  proc.stdin.end();
811
828
 
@@ -869,7 +886,7 @@ test("collectMcpTools appends leaf notes to MCP tool description", () => {
869
886
  ],
870
887
  });
871
888
  const tools = collectMcpTools(root);
872
- expect(tools[0]!.description).toBe("run — Run.\n\nUse `--json` for structured output.");
889
+ expect(tools[0]?.description).toBe("run — Run.\n\nUse `--json` for structured output.");
873
890
  });
874
891
 
875
892
  test("collectMcpTools appends notes after mcpTool.description override", () => {
@@ -889,7 +906,7 @@ test("collectMcpTools appends notes after mcpTool.description override", () => {
889
906
  ],
890
907
  });
891
908
  const tools = collectMcpTools(root);
892
- expect(tools[0]!.description).toBe("Custom MCP text.\n\nOperational hint.");
909
+ expect(tools[0]?.description).toBe("Custom MCP text.\n\nOperational hint.");
893
910
  });
894
911
 
895
912
  test("collectMcpTools resolves {argsbarg:program} in appended notes", () => {
@@ -908,7 +925,7 @@ test("collectMcpTools resolves {argsbarg:program} in appended notes", () => {
908
925
  ],
909
926
  });
910
927
  const tools = collectMcpTools(root);
911
- expect(tools[0]!.description).toContain("See `myapp docs api`.");
928
+ expect(tools[0]?.description).toContain("See `myapp docs api`.");
912
929
  });
913
930
 
914
931
  test("cliSchemaExport includes leaf outputSchema", () => {
@@ -930,7 +947,7 @@ test("cliSchemaExport includes leaf outputSchema", () => {
930
947
  ],
931
948
  });
932
949
  const schema = cliSchemaExport(root);
933
- expect(schema.commands![0]!.outputSchema).toEqual({
950
+ expect(schema.commands?.[0]?.outputSchema).toEqual({
934
951
  type: "object",
935
952
  properties: { ok: { type: "boolean" } },
936
953
  });
@@ -952,7 +969,7 @@ test("cliSchemaExport accepts legacy mcpTool.outputSchema", () => {
952
969
  },
953
970
  ],
954
971
  });
955
- expect(cliSchemaExport(root).commands![0]!.outputSchema).toEqual({
972
+ expect(cliSchemaExport(root).commands?.[0]?.outputSchema).toEqual({
956
973
  type: "object",
957
974
  properties: { id: { type: "string" } },
958
975
  });
@@ -1022,7 +1039,7 @@ test("collectMcpTools includes outputSchema when set on leaf", () => {
1022
1039
  });
1023
1040
  const tools = collectMcpTools(root);
1024
1041
  expect(tools).toHaveLength(1);
1025
- expect(tools[0]!.outputSchema).toEqual({
1042
+ expect(tools[0]?.outputSchema).toEqual({
1026
1043
  type: "object",
1027
1044
  properties: { ok: { type: "boolean" } },
1028
1045
  required: ["ok"],
@@ -1054,7 +1071,7 @@ test("mcpToolCallToArgv expands varargs positionals", () => {
1054
1071
  });
1055
1072
 
1056
1073
  test("reserved command name install is rejected", () => {
1057
- const root= testProgram({
1074
+ const root = testProgram({
1058
1075
  key: "app",
1059
1076
  description: "",
1060
1077
  commands: [
@@ -1069,7 +1086,7 @@ test("reserved command name install is rejected", () => {
1069
1086
  });
1070
1087
 
1071
1088
  test("top-level command name mcp is allowed without mcpServer", () => {
1072
- const root= testProgram({
1089
+ const root = testProgram({
1073
1090
  key: "app",
1074
1091
  description: "",
1075
1092
  commands: [
@@ -1117,7 +1134,7 @@ test("mcpServer on non-root node is rejected", () => {
1117
1134
  });
1118
1135
 
1119
1136
  test("mcpTool on root is rejected", () => {
1120
- const root= testProgram({
1137
+ const root = testProgram({
1121
1138
  key: "app",
1122
1139
  description: "",
1123
1140
  mcpTool: { enabled: false },
@@ -1127,7 +1144,7 @@ test("mcpTool on root is rejected", () => {
1127
1144
  });
1128
1145
 
1129
1146
  test("mcpTool on routing node is rejected", () => {
1130
- const root= testProgram({
1147
+ const root = testProgram({
1131
1148
  key: "app",
1132
1149
  description: "",
1133
1150
  commands: [
@@ -1175,7 +1192,7 @@ test("buildToolCallSuccess stderr-only still includes stdout slot", () => {
1175
1192
  test("buildToolCallSuccess parses JSON structuredContent", () => {
1176
1193
  const result = buildToolCallSuccess('{"a":1}\n', "");
1177
1194
  expect(result.structuredContent).toEqual({ a: 1 });
1178
- expect(result.content[0]!.text).toBe('{"a":1}\n');
1195
+ expect(result.content[0]?.text).toBe('{"a":1}\n');
1179
1196
  });
1180
1197
 
1181
1198
  test("buildToolCallSuccess skips structuredContent for plain text", () => {
@@ -1197,10 +1214,12 @@ test("MCP initialize returns tools and resources capabilities", async () => {
1197
1214
 
1198
1215
  test("MCP tools/list includes stat_owner_lookup", async () => {
1199
1216
  const responses = await mcpRequest([{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }]);
1200
- const res = responses.get(2) as { result: { tools: { name: string; inputSchema: { required?: string[] } }[] } };
1217
+ const res = responses.get(2) as {
1218
+ result: { tools: { name: string; inputSchema: { required?: string[] } }[] };
1219
+ };
1201
1220
  const lookup = res.result.tools.find((t) => t.name === "stat_owner_lookup");
1202
1221
  expect(lookup).toBeDefined();
1203
- expect(lookup!.inputSchema.required).toContain("path");
1222
+ expect(lookup?.inputSchema.required).toContain("path");
1204
1223
  });
1205
1224
 
1206
1225
  test("MCP resources/read returns schema JSON", async () => {
@@ -1208,7 +1227,7 @@ test("MCP resources/read returns schema JSON", async () => {
1208
1227
  { jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "nested_ts://schema" } },
1209
1228
  ]);
1210
1229
  const res = responses.get(3) as { result: { contents: { text: string }[] } };
1211
- const schema = JSON.parse(res.result.contents[0]!.text);
1230
+ const schema = JSON.parse(res.result.contents[0]?.text);
1212
1231
  expect(schema.key).toBe("nested.ts");
1213
1232
  });
1214
1233
 
@@ -1227,7 +1246,7 @@ test("MCP tools/call runs stat_owner_lookup", async () => {
1227
1246
  ]);
1228
1247
  const res = responses.get(4) as { result: { content: { text: string }[]; isError: boolean } };
1229
1248
  expect(res.result.isError).toBe(false);
1230
- expect(res.result.content[0]!.text).toContain("lookup user=test");
1249
+ expect(res.result.content[0]?.text).toContain("lookup user=test");
1231
1250
  });
1232
1251
 
1233
1252
  test("MCP tools/call returns structuredContent for JSON stdout", async () => {
@@ -1252,7 +1271,7 @@ test("MCP tools/call returns structuredContent for JSON stdout", async () => {
1252
1271
  };
1253
1272
  expect(res.result.isError).toBe(false);
1254
1273
  expect(res.result.structuredContent).toEqual({ user: "test", path: readme });
1255
- expect(JSON.parse(res.result.content[0]!.text.trim())).toEqual({ user: "test", path: readme });
1274
+ expect(JSON.parse(res.result.content[0]?.text.trim())).toEqual({ user: "test", path: readme });
1256
1275
  });
1257
1276
 
1258
1277
  test("MCP tools/call errors on missing required positional", async () => {
@@ -1266,7 +1285,7 @@ test("MCP tools/call errors on missing required positional", async () => {
1266
1285
  ]);
1267
1286
  const res = responses.get(5) as { result: { isError: boolean; content: { text: string }[] } };
1268
1287
  expect(res.result.isError).toBe(true);
1269
- expect(res.result.content[0]!.text).toContain("Missing argument: path");
1288
+ expect(res.result.content[0]?.text).toContain("Missing argument: path");
1270
1289
  });
1271
1290
 
1272
1291
  test("MCP ping returns empty result", async () => {
@@ -1293,7 +1312,7 @@ await cliRun(cli, []);
1293
1312
 
1294
1313
  test("ctx.invocation is mcp via cliInvoke", async () => {
1295
1314
  let seen = "";
1296
- const root= testProgram({
1315
+ const root = testProgram({
1297
1316
  key: "app",
1298
1317
  description: "",
1299
1318
  handler: (ctx: CliContext) => {
@@ -1336,7 +1355,7 @@ test("Enum option inputSchema includes enum array", () => {
1336
1355
  });
1337
1356
 
1338
1357
  test("cliInvoke rejects invalid Enum value", async () => {
1339
- const root= testProgram({
1358
+ const root = testProgram({
1340
1359
  key: "app",
1341
1360
  description: "",
1342
1361
  handler: () => {},
@@ -1357,7 +1376,7 @@ test("cliInvoke rejects invalid Enum value", async () => {
1357
1376
  });
1358
1377
 
1359
1378
  test("cliInvoke accepts valid Enum value", async () => {
1360
- const root= testProgram({
1379
+ const root = testProgram({
1361
1380
  key: "app",
1362
1381
  description: "",
1363
1382
  handler: (ctx: CliContext) => {
@@ -1380,7 +1399,7 @@ test("cliInvoke accepts valid Enum value", async () => {
1380
1399
  });
1381
1400
 
1382
1401
  test("cliValidateProgram rejects Enum with no choices", () => {
1383
- const root= testProgram({
1402
+ const root = testProgram({
1384
1403
  key: "app",
1385
1404
  description: "",
1386
1405
  handler: () => {},
@@ -1390,7 +1409,7 @@ test("cliValidateProgram rejects Enum with no choices", () => {
1390
1409
  });
1391
1410
 
1392
1411
  test("cliValidateProgram rejects Enum with duplicate choices", () => {
1393
- const root= testProgram({
1412
+ const root = testProgram({
1394
1413
  key: "app",
1395
1414
  description: "",
1396
1415
  handler: () => {},
@@ -1400,7 +1419,7 @@ test("cliValidateProgram rejects Enum with duplicate choices", () => {
1400
1419
  });
1401
1420
 
1402
1421
  test("mcpTool.description override wins without requiresEnv suffix", () => {
1403
- const root= testProgram({
1422
+ const root = testProgram({
1404
1423
  key: "app",
1405
1424
  description: "",
1406
1425
  mcpServer: { enabled: true },
@@ -1414,11 +1433,11 @@ test("mcpTool.description override wins without requiresEnv suffix", () => {
1414
1433
  ],
1415
1434
  });
1416
1435
  const tools = collectMcpTools(root);
1417
- expect(tools[0]!.description).toBe("custom");
1436
+ expect(tools[0]?.description).toBe("custom");
1418
1437
  });
1419
1438
 
1420
1439
  test("mcpTool.requiresEnv appended to auto description", () => {
1421
- const root= testProgram({
1440
+ const root = testProgram({
1422
1441
  key: "app",
1423
1442
  description: "",
1424
1443
  mcpServer: { enabled: true },
@@ -1432,7 +1451,7 @@ test("mcpTool.requiresEnv appended to auto description", () => {
1432
1451
  ],
1433
1452
  });
1434
1453
  const tools = collectMcpTools(root);
1435
- expect(tools[0]!.description).toContain("[requires env: TOKEN]");
1454
+ expect(tools[0]?.description).toContain("[requires env: TOKEN]");
1436
1455
  });
1437
1456
 
1438
1457
  test("cliValidateProgram rejects duplicate mcpResources URIs", () => {
@@ -1553,7 +1572,7 @@ test("loadEnvFile overwrites existing keys", () => {
1553
1572
  });
1554
1573
 
1555
1574
  test("Enum completions list choices in bash script", () => {
1556
- const root= testProgram({
1575
+ const root = testProgram({
1557
1576
  key: "app",
1558
1577
  description: "",
1559
1578
  commands: [
@@ -1590,7 +1609,7 @@ test("MCP resources/read returns custom resource body", async () => {
1590
1609
  { script: "examples/mcp-test.ts" },
1591
1610
  );
1592
1611
  const res = responses.get(11) as { result: { contents: { text: string }[] } };
1593
- expect(res.result.contents[0]!.text).toBe("hello resource");
1612
+ expect(res.result.contents[0]?.text).toBe("hello resource");
1594
1613
  });
1595
1614
 
1596
1615
  test("MCP resources/read unknown URI returns error", async () => {
@@ -1616,7 +1635,7 @@ test("MCP requiresEnv fails when env missing", async () => {
1616
1635
  );
1617
1636
  const res = responses.get(13) as { result: { isError: boolean; content: { text: string }[] } };
1618
1637
  expect(res.result.isError).toBe(true);
1619
- expect(res.result.content[0]!.text).toContain("ARGS_TEST_SECRET");
1638
+ expect(res.result.content[0]?.text).toContain("ARGS_TEST_SECRET");
1620
1639
  });
1621
1640
 
1622
1641
  test("MCP requiresEnv succeeds when env present", async () => {
@@ -1633,7 +1652,7 @@ test("MCP requiresEnv succeeds when env present", async () => {
1633
1652
  );
1634
1653
  const res = responses.get(14) as { result: { isError: boolean; content: { text: string }[] } };
1635
1654
  expect(res.result.isError).toBe(false);
1636
- expect(res.result.content[0]!.text.trim()).toBe("sekrit");
1655
+ expect(res.result.content[0]?.text.trim()).toBe("sekrit");
1637
1656
  });
1638
1657
 
1639
1658
  test("MCP envFile loads vars for tool handlers", async () => {
@@ -1649,11 +1668,14 @@ test("MCP envFile loads vars for tool handlers", async () => {
1649
1668
  params: { name: "echo_env", arguments: { name: "ARGS_FILE_TOKEN" } },
1650
1669
  },
1651
1670
  ],
1652
- { script: "examples/mcp-test.ts", env: { ARGS_TEST_ENV_FILE: envFile, ARGS_TEST_SECRET: "present" } },
1671
+ {
1672
+ script: "examples/mcp-test.ts",
1673
+ env: { ARGS_TEST_ENV_FILE: envFile, ARGS_TEST_SECRET: "present" },
1674
+ },
1653
1675
  );
1654
1676
  const res = responses.get(15) as { result: { isError: boolean; content: { text: string }[] } };
1655
1677
  expect(res.result.isError).toBe(false);
1656
- expect(res.result.content[0]!.text.trim()).toBe("file-value");
1678
+ expect(res.result.content[0]?.text.trim()).toBe("file-value");
1657
1679
  });
1658
1680
 
1659
1681
  // ── v1.3 parser ergonomics ────────────────────────────────────────────────────
@@ -1724,7 +1746,7 @@ test("nested fallback routes to default when argv exhausted at router", () => {
1724
1746
  });
1725
1747
 
1726
1748
  test("nested fallback MissingOrUnknown routes unknown token to default", () => {
1727
- const root= testProgram({
1749
+ const root = testProgram({
1728
1750
  key: "app",
1729
1751
  description: "",
1730
1752
  commands: [
@@ -1773,7 +1795,7 @@ test("nested fallback MissingOnly errors on unknown subcommand", () => {
1773
1795
  });
1774
1796
 
1775
1797
  test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
1776
- const root= testProgram({
1798
+ const root = testProgram({
1777
1799
  key: "app",
1778
1800
  description: "",
1779
1801
  commands: [
@@ -1791,7 +1813,9 @@ test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
1791
1813
  },
1792
1814
  ],
1793
1815
  });
1794
- expect(() => cliValidateProgram(root)).toThrow(/fallbackCommand 'missing' is not a child of 'docs'/);
1816
+ expect(() => cliValidateProgram(root)).toThrow(
1817
+ /fallbackCommand 'missing' is not a child of 'docs'/,
1818
+ );
1795
1819
  });
1796
1820
 
1797
1821
  test("cliValidateProgram accepts nested fallbackCommand when child exists", () => {
@@ -1817,7 +1841,7 @@ test("varargs trailing option after positionals via cliInvoke", async () => {
1817
1841
  const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--json"]));
1818
1842
  expect(pr.kind).toBe(ParseKind.Ok);
1819
1843
  expect(pr.args).toEqual(["file.txt"]);
1820
- expect(pr.opts["json"]).toBe("1");
1844
+ expect(pr.opts.json).toBe("1");
1821
1845
  });
1822
1846
 
1823
1847
  test("varargs option before positionals", () => {
@@ -1826,7 +1850,7 @@ test("varargs option before positionals", () => {
1826
1850
  const pr = postParseValidate(root, parse(root, ["read", "--json", "file.txt"]));
1827
1851
  expect(pr.kind).toBe(ParseKind.Ok);
1828
1852
  expect(pr.args).toEqual(["file.txt"]);
1829
- expect(pr.opts["json"]).toBe("1");
1853
+ expect(pr.opts.json).toBe("1");
1830
1854
  });
1831
1855
 
1832
1856
  test("varargs multiple files then trailing option", () => {
@@ -1835,7 +1859,7 @@ test("varargs multiple files then trailing option", () => {
1835
1859
  const pr = postParseValidate(root, parse(root, ["read", "a.txt", "b.txt", "--json"]));
1836
1860
  expect(pr.kind).toBe(ParseKind.Ok);
1837
1861
  expect(pr.args).toEqual(["a.txt", "b.txt"]);
1838
- expect(pr.opts["json"]).toBe("1");
1862
+ expect(pr.opts.json).toBe("1");
1839
1863
  });
1840
1864
 
1841
1865
  test("varargs double dash forces positional", () => {
@@ -1844,7 +1868,7 @@ test("varargs double dash forces positional", () => {
1844
1868
  const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--", "--json"]));
1845
1869
  expect(pr.kind).toBe(ParseKind.Ok);
1846
1870
  expect(pr.args).toEqual(["file.txt", "--json"]);
1847
- expect(pr.opts["json"]).toBeUndefined();
1871
+ expect(pr.opts.json).toBeUndefined();
1848
1872
  });
1849
1873
 
1850
1874
  test("varargs unknown flag errors", async () => {
@@ -1865,7 +1889,7 @@ test("varargs scoped help in tail", () => {
1865
1889
  });
1866
1890
 
1867
1891
  test("ctx.positional returns single slot value", async () => {
1868
- const root= testProgram({
1892
+ const root = testProgram({
1869
1893
  key: "app",
1870
1894
  description: "",
1871
1895
  commands: [
@@ -1899,7 +1923,7 @@ test("ctx.positional returns varargs array", async () => {
1899
1923
  });
1900
1924
 
1901
1925
  test("ctx.positional returns undefined for absent optional slot", async () => {
1902
- const root= testProgram({
1926
+ const root = testProgram({
1903
1927
  key: "app",
1904
1928
  description: "",
1905
1929
  commands: [
@@ -1936,18 +1960,18 @@ test("ctx.positional varargs matches ctx.args", async () => {
1936
1960
  expect(positional).toEqual(args);
1937
1961
  });
1938
1962
 
1939
- test("mcpToolCallToArgv coerces comma-separated string for varargs", () => {
1963
+ test("mcpToolCallToArgv rejects comma-separated string for varargs", () => {
1940
1964
  const tools = collectMcpTools(nestedMcpFixture);
1941
1965
  const read = tools.find((t) => t.name === "read")!;
1942
1966
  const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: "a,b" });
1943
- expect(argv).toEqual(["read", "a", "b"]);
1967
+ expect(argv).toEqual({ error: expect.stringContaining("JSON array") });
1944
1968
  });
1945
1969
 
1946
- test("mcpToolCallToArgv coerces single string for varargs", () => {
1970
+ test("mcpToolCallToArgv rejects bare string for varargs", () => {
1947
1971
  const tools = collectMcpTools(nestedMcpFixture);
1948
1972
  const read = tools.find((t) => t.name === "read")!;
1949
1973
  const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: "a" });
1950
- expect(argv).toEqual(["read", "a"]);
1974
+ expect(argv).toEqual({ error: expect.stringContaining("JSON array") });
1951
1975
  });
1952
1976
 
1953
1977
  test("mcpToolCallToArgv array varargs unchanged", () => {
@@ -1957,11 +1981,11 @@ test("mcpToolCallToArgv array varargs unchanged", () => {
1957
1981
  expect(argv).toEqual(["read", "a", "b"]);
1958
1982
  });
1959
1983
 
1960
- test("mcpToolCallToArgv empty string varargs appends nothing", () => {
1984
+ test("mcpToolCallToArgv empty array varargs errors when required", () => {
1961
1985
  const tools = collectMcpTools(nestedMcpFixture);
1962
1986
  const read = tools.find((t) => t.name === "read")!;
1963
- const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: "" });
1964
- expect(argv).toEqual(["read"]);
1987
+ const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: [] });
1988
+ expect(argv).toEqual({ error: "Missing argument: files" });
1965
1989
  });
1966
1990
 
1967
1991
  // ── Skills ────────────────────────────────────────────────────────────────────
@@ -2077,4 +2101,4 @@ test("cliSkillInstall claude target uses .claude/skills", () => {
2077
2101
  process.chdir(prev);
2078
2102
  rmSync(cwd, { recursive: true, force: true });
2079
2103
  }
2080
- });
2104
+ });