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