argsbarg 1.5.0 → 2.0.1
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/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
- package/CHANGELOG.md +31 -1
- package/README.md +12 -8
- package/docs/install.md +2 -2
- package/docs/mcp.md +3 -3
- package/examples/mcp-test.ts +3 -3
- package/examples/minimal.ts +3 -3
- package/examples/nested.ts +3 -3
- package/examples/option-required.ts +3 -3
- package/index.d.ts +38 -37
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +3 -3
- package/src/builtins/completion-bash.ts +3 -3
- package/src/builtins/completion-fish.ts +2 -2
- package/src/builtins/completion-group.ts +2 -2
- package/src/builtins/completion-zsh.ts +3 -3
- package/src/builtins/dispatch.ts +41 -26
- package/src/builtins/export.ts +15 -8
- package/src/builtins/install.ts +3 -3
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/presentation.ts +34 -23
- package/src/builtins/scopes.ts +9 -8
- package/src/capabilities.ts +32 -0
- package/src/context.ts +17 -7
- package/src/help.ts +21 -9
- package/src/index.test.ts +128 -121
- package/src/index.ts +1 -1
- package/src/install/binary.ts +3 -3
- package/src/install/completions.ts +2 -2
- package/src/install/detect-installed.ts +1 -1
- package/src/install/index.ts +4 -4
- package/src/install/install.test.ts +2 -2
- package/src/install/mcp-config.ts +2 -2
- package/src/install/paths.ts +3 -3
- package/src/install/plan.ts +4 -4
- package/src/install/status.ts +2 -2
- package/src/install/uninstall.ts +2 -2
- package/src/invoke.ts +14 -5
- package/src/mcp/server.ts +3 -3
- package/src/mcp/tools.ts +16 -16
- package/src/mcp.ts +2 -2
- package/src/parse.ts +55 -27
- package/src/runtime.ts +34 -25
- package/src/schema.ts +6 -6
- package/src/skill/generate.ts +6 -6
- package/src/skill/install.ts +2 -2
- package/src/types.test.ts +40 -0
- package/src/types.ts +54 -44
- package/src/validate.ts +87 -72
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 {
|
|
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,
|
|
@@ -23,7 +26,7 @@ import { generateSkillBundle } from "./skill/generate.ts";
|
|
|
23
26
|
import { cliSkillInstall } from "./skill/install.ts";
|
|
24
27
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
25
28
|
import { cliSchemaJson } from "./schema.ts";
|
|
26
|
-
import {
|
|
29
|
+
import { cliValidateProgram } from "./validate.ts";
|
|
27
30
|
import { expect, test } from "bun:test";
|
|
28
31
|
import { $ } from "bun";
|
|
29
32
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -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:
|
|
37
|
+
const root: CliProgram = {
|
|
35
38
|
key: "app",
|
|
36
39
|
description: "",
|
|
37
40
|
commands: [
|
|
@@ -56,7 +59,7 @@ test("bundled short presence flags", () => {
|
|
|
56
59
|
},
|
|
57
60
|
],
|
|
58
61
|
};
|
|
59
|
-
|
|
62
|
+
cliValidateProgram(root);
|
|
60
63
|
const pr = postParseValidate(root, parse(root, ["x", "-ab"]));
|
|
61
64
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
62
65
|
expect(pr.opts["a"]).toBe("1");
|
|
@@ -64,7 +67,7 @@ test("bundled short presence flags", () => {
|
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
test("long option equals", () => {
|
|
67
|
-
const root:
|
|
70
|
+
const root: CliProgram = {
|
|
68
71
|
key: "app",
|
|
69
72
|
description: "",
|
|
70
73
|
commands: [
|
|
@@ -82,14 +85,14 @@ test("long option equals", () => {
|
|
|
82
85
|
},
|
|
83
86
|
],
|
|
84
87
|
};
|
|
85
|
-
|
|
88
|
+
cliValidateProgram(root);
|
|
86
89
|
const pr = postParseValidate(root, parse(root, ["x", "--name=pat"]));
|
|
87
90
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
88
91
|
expect(pr.opts["name"]).toBe("pat");
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
test("fallback missing or unknown root flags", () => {
|
|
92
|
-
const root:
|
|
95
|
+
const root: CliProgram = {
|
|
93
96
|
key: "app",
|
|
94
97
|
description: "",
|
|
95
98
|
commands: [
|
|
@@ -109,7 +112,7 @@ test("fallback missing or unknown root flags", () => {
|
|
|
109
112
|
fallbackCommand: "hello",
|
|
110
113
|
fallbackMode: CliFallbackMode.MissingOrUnknown,
|
|
111
114
|
};
|
|
112
|
-
|
|
115
|
+
cliValidateProgram(root);
|
|
113
116
|
const pr = postParseValidate(root, parse(root, ["--name", "bob"]));
|
|
114
117
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
115
118
|
expect(pr.path).toEqual(["hello"]);
|
|
@@ -117,31 +120,31 @@ test("fallback missing or unknown root flags", () => {
|
|
|
117
120
|
});
|
|
118
121
|
|
|
119
122
|
test("unknown command", () => {
|
|
120
|
-
const root:
|
|
123
|
+
const root: CliProgram = {
|
|
121
124
|
key: "app",
|
|
122
125
|
description: "",
|
|
123
126
|
commands: [{ key: "hello", description: "", handler: () => {} }],
|
|
124
127
|
};
|
|
125
|
-
|
|
128
|
+
cliValidateProgram(root);
|
|
126
129
|
const pr = parse(root, ["nope"]);
|
|
127
130
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
128
131
|
expect(pr.errorMsg).toContain("Unknown command");
|
|
129
132
|
});
|
|
130
133
|
|
|
131
134
|
test("implicit help empty", () => {
|
|
132
|
-
const root:
|
|
135
|
+
const root: CliProgram = {
|
|
133
136
|
key: "app",
|
|
134
137
|
description: "",
|
|
135
138
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
136
139
|
};
|
|
137
|
-
|
|
140
|
+
cliValidateProgram(root);
|
|
138
141
|
const pr = parse(root, []);
|
|
139
142
|
expect(pr.kind).toBe(ParseKind.Help);
|
|
140
143
|
expect(pr.helpExplicit).toBe(false);
|
|
141
144
|
});
|
|
142
145
|
|
|
143
146
|
test("invalid number post validate", () => {
|
|
144
|
-
const root:
|
|
147
|
+
const root: CliProgram = {
|
|
145
148
|
key: "app",
|
|
146
149
|
description: "",
|
|
147
150
|
commands: [
|
|
@@ -159,7 +162,7 @@ test("invalid number post validate", () => {
|
|
|
159
162
|
},
|
|
160
163
|
],
|
|
161
164
|
};
|
|
162
|
-
|
|
165
|
+
cliValidateProgram(root);
|
|
163
166
|
let pr = parse(root, ["x", "--n", "notnum"]);
|
|
164
167
|
pr = postParseValidate(root, pr);
|
|
165
168
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
@@ -167,7 +170,7 @@ test("invalid number post validate", () => {
|
|
|
167
170
|
});
|
|
168
171
|
|
|
169
172
|
test("supports scientific notation in numbers", () => {
|
|
170
|
-
const root:
|
|
173
|
+
const root: CliProgram = {
|
|
171
174
|
key: "app",
|
|
172
175
|
description: "",
|
|
173
176
|
commands: [
|
|
@@ -185,7 +188,7 @@ test("supports scientific notation in numbers", () => {
|
|
|
185
188
|
},
|
|
186
189
|
],
|
|
187
190
|
};
|
|
188
|
-
|
|
191
|
+
cliValidateProgram(root);
|
|
189
192
|
let pr = parse(root, ["x", "--n", "1.23e4"]);
|
|
190
193
|
pr = postParseValidate(root, pr);
|
|
191
194
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
@@ -195,12 +198,12 @@ test("supports scientific notation in numbers", () => {
|
|
|
195
198
|
|
|
196
199
|
|
|
197
200
|
test("completion scripts contain app name", () => {
|
|
198
|
-
const root:
|
|
201
|
+
const root: CliProgram = {
|
|
199
202
|
key: "myapp",
|
|
200
203
|
description: "Test",
|
|
201
204
|
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
202
205
|
};
|
|
203
|
-
|
|
206
|
+
cliValidateProgram(root);
|
|
204
207
|
const bash = completionBashScript(root);
|
|
205
208
|
expect(bash).toContain("bash completion for myapp");
|
|
206
209
|
expect(bash).toContain("complete -F _myapp myapp");
|
|
@@ -212,18 +215,18 @@ test("completion scripts contain app name", () => {
|
|
|
212
215
|
});
|
|
213
216
|
|
|
214
217
|
test("completion scripts do not emit invalid bash substitutions", () => {
|
|
215
|
-
const root:
|
|
218
|
+
const root: CliProgram = {
|
|
216
219
|
key: "app",
|
|
217
220
|
description: "Test",
|
|
218
221
|
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
219
222
|
};
|
|
220
|
-
|
|
223
|
+
cliValidateProgram(root);
|
|
221
224
|
const bash = completionBashScript(root);
|
|
222
225
|
expect(bash).not.toContain("${${");
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
test("completion scripts escape shell-sensitive command text in zsh", () => {
|
|
226
|
-
const root:
|
|
229
|
+
const root: CliProgram = {
|
|
227
230
|
key: "app",
|
|
228
231
|
description: "Test",
|
|
229
232
|
commands: [
|
|
@@ -234,18 +237,18 @@ test("completion scripts escape shell-sensitive command text in zsh", () => {
|
|
|
234
237
|
},
|
|
235
238
|
],
|
|
236
239
|
};
|
|
237
|
-
|
|
240
|
+
cliValidateProgram(root);
|
|
238
241
|
const zsh = completionZshScript(root);
|
|
239
242
|
expect(zsh).toContain("quote'\\''cmd:Say '\\''hello'\\'' and keep going.");
|
|
240
243
|
});
|
|
241
244
|
|
|
242
245
|
test("completion scripts keep dotted app names in registration names", () => {
|
|
243
|
-
const root:
|
|
246
|
+
const root: CliProgram = {
|
|
244
247
|
key: "minimal.ts",
|
|
245
248
|
description: "Test",
|
|
246
249
|
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
247
250
|
};
|
|
248
|
-
|
|
251
|
+
cliValidateProgram(root);
|
|
249
252
|
|
|
250
253
|
const bash = completionBashScript(root);
|
|
251
254
|
expect(bash).toContain("complete -F _minimal_ts minimal.ts");
|
|
@@ -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:
|
|
261
|
+
const root: CliProgram = {
|
|
259
262
|
key: "app",
|
|
260
263
|
description: "",
|
|
261
264
|
commands: [
|
|
@@ -280,7 +283,7 @@ test("trailing options after bounded positionals", () => {
|
|
|
280
283
|
},
|
|
281
284
|
],
|
|
282
285
|
};
|
|
283
|
-
|
|
286
|
+
cliValidateProgram(root);
|
|
284
287
|
const pr = postParseValidate(root, parse(root, ["x", "./file", "--verbose"]));
|
|
285
288
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
286
289
|
expect(pr.args).toEqual(["./file"]);
|
|
@@ -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:
|
|
294
|
+
const root: CliProgram = {
|
|
292
295
|
key: "app",
|
|
293
296
|
description: "",
|
|
294
297
|
commands: [
|
|
@@ -327,7 +330,7 @@ test("trailing options include parent-scoped flags", () => {
|
|
|
327
330
|
},
|
|
328
331
|
],
|
|
329
332
|
};
|
|
330
|
-
|
|
333
|
+
cliValidateProgram(root);
|
|
331
334
|
const pr = postParseValidate(root, parse(root, ["group", "leaf", "-u", "alice", "./file", "--json"]));
|
|
332
335
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
333
336
|
expect(pr.path).toEqual(["group", "leaf"]);
|
|
@@ -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:
|
|
343
|
+
const root: CliProgram = {
|
|
341
344
|
key: "app",
|
|
342
345
|
description: "",
|
|
343
346
|
commands: [
|
|
@@ -364,7 +367,7 @@ test("varargs tail parses trailing options", () => {
|
|
|
364
367
|
},
|
|
365
368
|
],
|
|
366
369
|
};
|
|
367
|
-
|
|
370
|
+
cliValidateProgram(root);
|
|
368
371
|
const pr = postParseValidate(root, parse(root, ["x", "./file", "--json"]));
|
|
369
372
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
370
373
|
expect(pr.args).toEqual(["./file"]);
|
|
@@ -372,7 +375,7 @@ test("varargs tail parses trailing options", () => {
|
|
|
372
375
|
});
|
|
373
376
|
|
|
374
377
|
test("stops parsing options at --", () => {
|
|
375
|
-
const root:
|
|
378
|
+
const root: CliProgram = {
|
|
376
379
|
key: "app",
|
|
377
380
|
description: "",
|
|
378
381
|
commands: [
|
|
@@ -399,7 +402,7 @@ test("stops parsing options at --", () => {
|
|
|
399
402
|
},
|
|
400
403
|
],
|
|
401
404
|
};
|
|
402
|
-
|
|
405
|
+
cliValidateProgram(root);
|
|
403
406
|
const pr = postParseValidate(root, parse(root, ["x", "--name", "pat", "--", "--name", "bob", "-x"]));
|
|
404
407
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
405
408
|
expect(pr.opts["name"]).toBe("pat");
|
|
@@ -407,7 +410,7 @@ test("stops parsing options at --", () => {
|
|
|
407
410
|
});
|
|
408
411
|
|
|
409
412
|
test("missing required option returns error", () => {
|
|
410
|
-
const root:
|
|
413
|
+
const root: CliProgram = {
|
|
411
414
|
key: "app",
|
|
412
415
|
description: "",
|
|
413
416
|
options: [
|
|
@@ -426,14 +429,14 @@ test("missing required option returns error", () => {
|
|
|
426
429
|
},
|
|
427
430
|
],
|
|
428
431
|
};
|
|
429
|
-
|
|
432
|
+
cliValidateProgram(root);
|
|
430
433
|
const pr = postParseValidate(root, parse(root, ["x"]));
|
|
431
434
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
432
435
|
expect(pr.errorMsg).toContain("Missing required option: --req");
|
|
433
436
|
});
|
|
434
437
|
|
|
435
438
|
test("provided required option parses ok", () => {
|
|
436
|
-
const root:
|
|
439
|
+
const root: CliProgram = {
|
|
437
440
|
key: "app",
|
|
438
441
|
description: "",
|
|
439
442
|
commands: [
|
|
@@ -452,14 +455,14 @@ test("provided required option parses ok", () => {
|
|
|
452
455
|
},
|
|
453
456
|
],
|
|
454
457
|
};
|
|
455
|
-
|
|
458
|
+
cliValidateProgram(root);
|
|
456
459
|
const pr = postParseValidate(root, parse(root, ["x", "--req", "val"]));
|
|
457
460
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
458
461
|
expect(pr.opts["req"]).toBe("val");
|
|
459
462
|
});
|
|
460
463
|
|
|
461
464
|
test("presence option cannot be required", () => {
|
|
462
|
-
const root:
|
|
465
|
+
const root: CliProgram = {
|
|
463
466
|
key: "app",
|
|
464
467
|
description: "",
|
|
465
468
|
options: [
|
|
@@ -478,7 +481,7 @@ test("presence option cannot be required", () => {
|
|
|
478
481
|
},
|
|
479
482
|
],
|
|
480
483
|
};
|
|
481
|
-
expect(() =>
|
|
484
|
+
expect(() => cliValidateProgram(root)).toThrow(/Presence option cannot be required/);
|
|
482
485
|
});
|
|
483
486
|
|
|
484
487
|
test("leaf completion help prints correctly", async () => {
|
|
@@ -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:
|
|
533
|
+
const root: CliProgram = {
|
|
531
534
|
key: "app",
|
|
532
535
|
description: "demo",
|
|
533
536
|
commands: [
|
|
@@ -538,13 +541,13 @@ test("parse recognizes --schema at the program root", () => {
|
|
|
538
541
|
},
|
|
539
542
|
],
|
|
540
543
|
};
|
|
541
|
-
|
|
544
|
+
cliValidateProgram(root);
|
|
542
545
|
const pr = parse(root, ["--schema"]);
|
|
543
546
|
expect(pr.kind).toBe(ParseKind.Schema);
|
|
544
547
|
});
|
|
545
548
|
|
|
546
549
|
test("cliSchemaJson omits handlers and completion built-ins", () => {
|
|
547
|
-
const root:
|
|
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:
|
|
580
|
+
const root: CliProgram = {
|
|
578
581
|
key: "app",
|
|
579
582
|
description: "",
|
|
580
583
|
commands: [
|
|
@@ -592,11 +595,11 @@ test("reserved option name schema is rejected", () => {
|
|
|
592
595
|
},
|
|
593
596
|
],
|
|
594
597
|
};
|
|
595
|
-
expect(() =>
|
|
598
|
+
expect(() => cliValidateProgram(root)).toThrow(/reserved for --schema/);
|
|
596
599
|
});
|
|
597
600
|
|
|
598
601
|
test("root help lists --schema built-in", () => {
|
|
599
|
-
const root:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
817
|
+
const root: CliProgram = {
|
|
815
818
|
key: "app",
|
|
816
819
|
description: "",
|
|
817
820
|
commands: [
|
|
@@ -822,11 +825,11 @@ test("reserved command name install is rejected", () => {
|
|
|
822
825
|
},
|
|
823
826
|
],
|
|
824
827
|
};
|
|
825
|
-
expect(() =>
|
|
828
|
+
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: install/);
|
|
826
829
|
});
|
|
827
830
|
|
|
828
831
|
test("top-level command name mcp is allowed without mcpServer", () => {
|
|
829
|
-
const root:
|
|
832
|
+
const root: CliProgram = {
|
|
830
833
|
key: "app",
|
|
831
834
|
description: "",
|
|
832
835
|
commands: [
|
|
@@ -837,11 +840,11 @@ test("top-level command name mcp is allowed without mcpServer", () => {
|
|
|
837
840
|
},
|
|
838
841
|
],
|
|
839
842
|
};
|
|
840
|
-
expect(() =>
|
|
843
|
+
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
841
844
|
});
|
|
842
845
|
|
|
843
846
|
test("top-level command name mcp is rejected when mcpServer is set", () => {
|
|
844
|
-
const root:
|
|
847
|
+
const root: CliProgram = {
|
|
845
848
|
key: "app",
|
|
846
849
|
description: "",
|
|
847
850
|
mcpServer: {},
|
|
@@ -853,11 +856,11 @@ test("top-level command name mcp is rejected when mcpServer is set", () => {
|
|
|
853
856
|
},
|
|
854
857
|
],
|
|
855
858
|
};
|
|
856
|
-
expect(() =>
|
|
859
|
+
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: mcp/);
|
|
857
860
|
});
|
|
858
861
|
|
|
859
862
|
test("mcpServer on non-root node is rejected", () => {
|
|
860
|
-
const root
|
|
863
|
+
const root = {
|
|
861
864
|
key: "app",
|
|
862
865
|
description: "",
|
|
863
866
|
commands: [
|
|
@@ -868,22 +871,22 @@ test("mcpServer on non-root node is rejected", () => {
|
|
|
868
871
|
handler: () => {},
|
|
869
872
|
},
|
|
870
873
|
],
|
|
871
|
-
};
|
|
872
|
-
expect(() =>
|
|
874
|
+
} as unknown as CliProgram;
|
|
875
|
+
expect(() => cliValidateProgram(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:
|
|
879
|
+
const root: CliProgram = {
|
|
877
880
|
key: "app",
|
|
878
881
|
description: "",
|
|
879
882
|
mcpTool: { enabled: false },
|
|
880
883
|
handler: () => {},
|
|
881
884
|
};
|
|
882
|
-
expect(() =>
|
|
885
|
+
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
883
886
|
});
|
|
884
887
|
|
|
885
888
|
test("mcpTool on routing node is rejected", () => {
|
|
886
|
-
const root:
|
|
889
|
+
const root: CliProgram = {
|
|
887
890
|
key: "app",
|
|
888
891
|
description: "",
|
|
889
892
|
commands: [
|
|
@@ -901,7 +904,7 @@ test("mcpTool on routing node is rejected", () => {
|
|
|
901
904
|
},
|
|
902
905
|
],
|
|
903
906
|
};
|
|
904
|
-
expect(() =>
|
|
907
|
+
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
905
908
|
});
|
|
906
909
|
|
|
907
910
|
test("buildToolCallSuccess returns stdout only", () => {
|
|
@@ -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,
|
|
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,20 +1052,20 @@ await cliRun(cli, []);
|
|
|
1049
1052
|
|
|
1050
1053
|
test("ctx.invocation is mcp via cliInvoke", async () => {
|
|
1051
1054
|
let seen = "";
|
|
1052
|
-
const root:
|
|
1055
|
+
const root: CliProgram = {
|
|
1053
1056
|
key: "app",
|
|
1054
1057
|
description: "",
|
|
1055
1058
|
handler: (ctx) => {
|
|
1056
1059
|
seen = ctx.invocation;
|
|
1057
1060
|
},
|
|
1058
1061
|
};
|
|
1059
|
-
|
|
1062
|
+
cliValidateProgram(root);
|
|
1060
1063
|
const result = await cliInvoke(root, []);
|
|
1061
1064
|
expect(result.kind).toBe("ok");
|
|
1062
1065
|
expect(seen).toBe("mcp");
|
|
1063
1066
|
});
|
|
1064
1067
|
|
|
1065
|
-
const enumMcpFixture:
|
|
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:
|
|
1098
|
+
const root: CliProgram = {
|
|
1096
1099
|
key: "app",
|
|
1097
1100
|
description: "",
|
|
1098
1101
|
handler: () => {},
|
|
@@ -1106,14 +1109,14 @@ test("cliInvoke rejects invalid Enum value", async () => {
|
|
|
1106
1109
|
},
|
|
1107
1110
|
],
|
|
1108
1111
|
};
|
|
1109
|
-
|
|
1112
|
+
cliValidateProgram(root);
|
|
1110
1113
|
const result = await cliInvoke(root, ["--mode", "staging"]);
|
|
1111
1114
|
expect(result.kind).toBe("error");
|
|
1112
1115
|
expect(result.errorMsg).toContain("not one of");
|
|
1113
1116
|
});
|
|
1114
1117
|
|
|
1115
1118
|
test("cliInvoke accepts valid Enum value", async () => {
|
|
1116
|
-
const root:
|
|
1119
|
+
const root: CliProgram = {
|
|
1117
1120
|
key: "app",
|
|
1118
1121
|
description: "",
|
|
1119
1122
|
handler: (ctx) => {
|
|
@@ -1129,34 +1132,34 @@ test("cliInvoke accepts valid Enum value", async () => {
|
|
|
1129
1132
|
},
|
|
1130
1133
|
],
|
|
1131
1134
|
};
|
|
1132
|
-
|
|
1135
|
+
cliValidateProgram(root);
|
|
1133
1136
|
const result = await cliInvoke(root, ["--mode", "dev"]);
|
|
1134
1137
|
expect(result.kind).toBe("ok");
|
|
1135
1138
|
expect(result.stdout.trim()).toBe("dev");
|
|
1136
1139
|
});
|
|
1137
1140
|
|
|
1138
|
-
test("
|
|
1139
|
-
const root:
|
|
1141
|
+
test("cliValidateProgram rejects Enum with no choices", () => {
|
|
1142
|
+
const root: CliProgram = {
|
|
1140
1143
|
key: "app",
|
|
1141
1144
|
description: "",
|
|
1142
1145
|
handler: () => {},
|
|
1143
1146
|
options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: [] }],
|
|
1144
1147
|
};
|
|
1145
|
-
expect(() =>
|
|
1148
|
+
expect(() => cliValidateProgram(root)).toThrow(/requires non-empty choices/);
|
|
1146
1149
|
});
|
|
1147
1150
|
|
|
1148
|
-
test("
|
|
1149
|
-
const root:
|
|
1151
|
+
test("cliValidateProgram rejects Enum with duplicate choices", () => {
|
|
1152
|
+
const root: CliProgram = {
|
|
1150
1153
|
key: "app",
|
|
1151
1154
|
description: "",
|
|
1152
1155
|
handler: () => {},
|
|
1153
1156
|
options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: ["a", "a"] }],
|
|
1154
1157
|
};
|
|
1155
|
-
expect(() =>
|
|
1158
|
+
expect(() => cliValidateProgram(root)).toThrow(/choices must be distinct/);
|
|
1156
1159
|
});
|
|
1157
1160
|
|
|
1158
1161
|
test("mcpTool.description override wins without requiresEnv suffix", () => {
|
|
1159
|
-
const root:
|
|
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:
|
|
1180
|
+
const root: CliProgram = {
|
|
1178
1181
|
key: "app",
|
|
1179
1182
|
description: "",
|
|
1180
1183
|
mcpServer: {},
|
|
@@ -1191,8 +1194,8 @@ test("mcpTool.requiresEnv appended to auto description", () => {
|
|
|
1191
1194
|
expect(tools[0]!.description).toContain("[requires env: TOKEN]");
|
|
1192
1195
|
});
|
|
1193
1196
|
|
|
1194
|
-
test("
|
|
1195
|
-
const root:
|
|
1197
|
+
test("cliValidateProgram rejects duplicate mcpResources URIs", () => {
|
|
1198
|
+
const root: CliProgram = {
|
|
1196
1199
|
key: "app",
|
|
1197
1200
|
description: "",
|
|
1198
1201
|
mcpServer: {
|
|
@@ -1203,11 +1206,11 @@ test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
|
|
|
1203
1206
|
},
|
|
1204
1207
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
1205
1208
|
};
|
|
1206
|
-
expect(() =>
|
|
1209
|
+
expect(() => cliValidateProgram(root)).toThrow(/URIs must be unique/);
|
|
1207
1210
|
});
|
|
1208
1211
|
|
|
1209
|
-
test("
|
|
1210
|
-
const root:
|
|
1212
|
+
test("cliValidateProgram rejects resource URI matching schemaResourceUri", () => {
|
|
1213
|
+
const root: CliProgram = {
|
|
1211
1214
|
key: "app",
|
|
1212
1215
|
description: "",
|
|
1213
1216
|
mcpServer: {
|
|
@@ -1216,11 +1219,11 @@ test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
|
|
|
1216
1219
|
},
|
|
1217
1220
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
1218
1221
|
};
|
|
1219
|
-
expect(() =>
|
|
1222
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with the built-in schema resource/);
|
|
1220
1223
|
});
|
|
1221
1224
|
|
|
1222
1225
|
test("allMcpResources includes custom resources", () => {
|
|
1223
|
-
const root:
|
|
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:
|
|
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():
|
|
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():
|
|
1404
|
+
function nestedDocsFallbackFixture(): CliProgram {
|
|
1402
1405
|
return {
|
|
1403
1406
|
key: "app",
|
|
1404
1407
|
description: "",
|
|
@@ -1427,14 +1430,14 @@ function nestedDocsFallbackFixture(): CliCommand {
|
|
|
1427
1430
|
|
|
1428
1431
|
test("nested fallback routes to default when argv exhausted at router", () => {
|
|
1429
1432
|
const root = nestedDocsFallbackFixture();
|
|
1430
|
-
|
|
1433
|
+
cliValidateProgram(root);
|
|
1431
1434
|
const pr = postParseValidate(root, parse(root, ["docs"]));
|
|
1432
1435
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1433
1436
|
expect(pr.path).toEqual(["docs", "guide"]);
|
|
1434
1437
|
});
|
|
1435
1438
|
|
|
1436
1439
|
test("nested fallback MissingOrUnknown routes unknown token to default", () => {
|
|
1437
|
-
const root:
|
|
1440
|
+
const root: CliProgram = {
|
|
1438
1441
|
key: "app",
|
|
1439
1442
|
description: "",
|
|
1440
1443
|
commands: [
|
|
@@ -1467,7 +1470,7 @@ test("nested fallback MissingOrUnknown routes unknown token to default", () => {
|
|
|
1467
1470
|
},
|
|
1468
1471
|
],
|
|
1469
1472
|
};
|
|
1470
|
-
|
|
1473
|
+
cliValidateProgram(root);
|
|
1471
1474
|
const pr = postParseValidate(root, parse(root, ["docs", "extra-topic"]));
|
|
1472
1475
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1473
1476
|
expect(pr.path).toEqual(["docs", "guide"]);
|
|
@@ -1476,14 +1479,14 @@ test("nested fallback MissingOrUnknown routes unknown token to default", () => {
|
|
|
1476
1479
|
|
|
1477
1480
|
test("nested fallback MissingOnly errors on unknown subcommand", () => {
|
|
1478
1481
|
const root = nestedDocsFallbackFixture();
|
|
1479
|
-
|
|
1482
|
+
cliValidateProgram(root);
|
|
1480
1483
|
const pr = parse(root, ["docs", "nope"]);
|
|
1481
1484
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
1482
1485
|
expect(pr.errorMsg).toContain("Unknown subcommand");
|
|
1483
1486
|
});
|
|
1484
1487
|
|
|
1485
|
-
test("
|
|
1486
|
-
const root:
|
|
1488
|
+
test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
|
|
1489
|
+
const root: CliProgram = {
|
|
1487
1490
|
key: "app",
|
|
1488
1491
|
description: "",
|
|
1489
1492
|
commands: [
|
|
@@ -1501,29 +1504,29 @@ test("cliValidateRoot rejects invalid nested fallbackCommand", () => {
|
|
|
1501
1504
|
},
|
|
1502
1505
|
],
|
|
1503
1506
|
};
|
|
1504
|
-
expect(() =>
|
|
1507
|
+
expect(() => cliValidateProgram(root)).toThrow(/fallbackCommand 'missing' is not a child of 'docs'/);
|
|
1505
1508
|
});
|
|
1506
1509
|
|
|
1507
|
-
test("
|
|
1510
|
+
test("cliValidateProgram accepts nested fallbackCommand when child exists", () => {
|
|
1508
1511
|
const root = nestedDocsFallbackFixture();
|
|
1509
|
-
expect(() =>
|
|
1512
|
+
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
1510
1513
|
});
|
|
1511
1514
|
|
|
1512
1515
|
test("nested router scoped help does not route to fallback", () => {
|
|
1513
1516
|
const root = nestedDocsFallbackFixture();
|
|
1514
|
-
|
|
1517
|
+
cliValidateProgram(root);
|
|
1515
1518
|
const pr = parse(root, ["docs", "--help"]);
|
|
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
|
});
|
|
1523
1526
|
|
|
1524
1527
|
test("varargs trailing option after positionals via cliInvoke", async () => {
|
|
1525
1528
|
const root = varargsReadFixture();
|
|
1526
|
-
|
|
1529
|
+
cliValidateProgram(root);
|
|
1527
1530
|
const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--json"]));
|
|
1528
1531
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1529
1532
|
expect(pr.args).toEqual(["file.txt"]);
|
|
@@ -1532,7 +1535,7 @@ test("varargs trailing option after positionals via cliInvoke", async () => {
|
|
|
1532
1535
|
|
|
1533
1536
|
test("varargs option before positionals", () => {
|
|
1534
1537
|
const root = varargsReadFixture();
|
|
1535
|
-
|
|
1538
|
+
cliValidateProgram(root);
|
|
1536
1539
|
const pr = postParseValidate(root, parse(root, ["read", "--json", "file.txt"]));
|
|
1537
1540
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1538
1541
|
expect(pr.args).toEqual(["file.txt"]);
|
|
@@ -1541,7 +1544,7 @@ test("varargs option before positionals", () => {
|
|
|
1541
1544
|
|
|
1542
1545
|
test("varargs multiple files then trailing option", () => {
|
|
1543
1546
|
const root = varargsReadFixture();
|
|
1544
|
-
|
|
1547
|
+
cliValidateProgram(root);
|
|
1545
1548
|
const pr = postParseValidate(root, parse(root, ["read", "a.txt", "b.txt", "--json"]));
|
|
1546
1549
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1547
1550
|
expect(pr.args).toEqual(["a.txt", "b.txt"]);
|
|
@@ -1550,7 +1553,7 @@ test("varargs multiple files then trailing option", () => {
|
|
|
1550
1553
|
|
|
1551
1554
|
test("varargs double dash forces positional", () => {
|
|
1552
1555
|
const root = varargsReadFixture();
|
|
1553
|
-
|
|
1556
|
+
cliValidateProgram(root);
|
|
1554
1557
|
const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--", "--json"]));
|
|
1555
1558
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1556
1559
|
expect(pr.args).toEqual(["file.txt", "--json"]);
|
|
@@ -1559,7 +1562,7 @@ test("varargs double dash forces positional", () => {
|
|
|
1559
1562
|
|
|
1560
1563
|
test("varargs unknown flag errors", async () => {
|
|
1561
1564
|
const root = varargsReadFixture();
|
|
1562
|
-
|
|
1565
|
+
cliValidateProgram(root);
|
|
1563
1566
|
const result = await cliInvoke(root, ["read", "--unknown"]);
|
|
1564
1567
|
expect(result.kind).toBe("error");
|
|
1565
1568
|
expect(result.stderr).toContain("--unknown");
|
|
@@ -1567,7 +1570,7 @@ test("varargs unknown flag errors", async () => {
|
|
|
1567
1570
|
|
|
1568
1571
|
test("varargs scoped help in tail", () => {
|
|
1569
1572
|
const root = varargsReadFixture();
|
|
1570
|
-
|
|
1573
|
+
cliValidateProgram(root);
|
|
1571
1574
|
const pr = parse(root, ["read", "file.txt", "--help"]);
|
|
1572
1575
|
expect(pr.kind).toBe(ParseKind.Help);
|
|
1573
1576
|
expect(pr.helpPath).toContain("read");
|
|
@@ -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:
|
|
1581
|
+
const root: CliProgram = {
|
|
1579
1582
|
key: "app",
|
|
1580
1583
|
description: "",
|
|
1581
1584
|
commands: [
|
|
@@ -1590,7 +1593,7 @@ test("ctx.positional returns single slot value", async () => {
|
|
|
1590
1593
|
],
|
|
1591
1594
|
};
|
|
1592
1595
|
let captured: string | string[] | undefined;
|
|
1593
|
-
|
|
1596
|
+
cliValidateProgram(root);
|
|
1594
1597
|
await cliInvoke(root, ["x", "./file"]);
|
|
1595
1598
|
expect(captured).toBe("./file");
|
|
1596
1599
|
});
|
|
@@ -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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1604
|
+
if (isCliRouter(root)) {
|
|
1605
|
+
(root.commands[0] as CliLeaf).handler = (ctx) => {
|
|
1606
|
+
captured = ctx.positional("files");
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
cliValidateProgram(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:
|
|
1615
|
+
const root: CliProgram = {
|
|
1611
1616
|
key: "app",
|
|
1612
1617
|
description: "",
|
|
1613
1618
|
commands: [
|
|
@@ -1624,7 +1629,7 @@ test("ctx.positional returns undefined for absent optional slot", async () => {
|
|
|
1624
1629
|
],
|
|
1625
1630
|
};
|
|
1626
1631
|
let captured: string | string[] | undefined;
|
|
1627
|
-
|
|
1632
|
+
cliValidateProgram(root);
|
|
1628
1633
|
await cliInvoke(root, ["x"]);
|
|
1629
1634
|
expect(captured).toBeUndefined();
|
|
1630
1635
|
});
|
|
@@ -1633,11 +1638,13 @@ 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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
+
if (isCliRouter(root)) {
|
|
1642
|
+
(root.commands[0] as CliLeaf).handler = (ctx) => {
|
|
1643
|
+
positional = ctx.positional("files");
|
|
1644
|
+
args = ctx.args;
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
cliValidateProgram(root);
|
|
1641
1648
|
await cliInvoke(root, ["read", "a.txt", "b.txt"]);
|
|
1642
1649
|
expect(positional).toEqual(args);
|
|
1643
1650
|
});
|
|
@@ -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
|
|
1683
|
+
const root = {
|
|
1677
1684
|
key: "app",
|
|
1678
1685
|
description: "",
|
|
1679
1686
|
commands: [
|
|
@@ -1684,8 +1691,8 @@ test("install config on non-root node is rejected", () => {
|
|
|
1684
1691
|
handler: () => {},
|
|
1685
1692
|
},
|
|
1686
1693
|
],
|
|
1687
|
-
};
|
|
1688
|
-
expect(() =>
|
|
1694
|
+
} as unknown as CliProgram;
|
|
1695
|
+
expect(() => cliValidateProgram(root)).toThrow(/install is only supported on the program root/);
|
|
1689
1696
|
});
|
|
1690
1697
|
|
|
1691
1698
|
test("generateSkillBundle includes frontmatter and command catalog", () => {
|