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.
- package/CHANGELOG.md +31 -1
- package/README.md +24 -8
- package/biome.json +29 -6
- package/bun.lock +22 -0
- package/docs/cli-program.md +75 -1
- package/docs/install.md +1 -1
- package/docs/mcp.md +45 -1
- package/docs/templates/cursor/rules/cli-program.mdc +15 -21
- package/index.d.ts +95 -50
- package/justfile +24 -6
- package/package.json +4 -2
- package/scripts/release.ts +26 -9
- package/src/builtins/builtins.test.ts +9 -4
- package/src/builtins/completion-bash.ts +74 -50
- package/src/builtins/completion-fish.ts +3 -8
- package/src/builtins/completion-group.ts +1 -1
- package/src/builtins/completion-zsh.ts +80 -42
- package/src/builtins/dispatch.ts +20 -16
- package/src/builtins/export.ts +19 -10
- package/src/builtins/index.ts +9 -4
- package/src/builtins/install.ts +10 -10
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/presentation.ts +8 -8
- package/src/builtins/scopes.ts +1 -1
- package/src/builtins/version.ts +1 -1
- package/src/completion.ts +4 -4
- package/src/context.ts +91 -15
- package/src/docs/api-guide.test.ts +2 -2
- package/src/docs/api-guide.ts +19 -5
- package/src/docs/builtin.ts +27 -8
- package/src/docs/docs.test.ts +18 -11
- package/src/docs/mcp-guide.ts +108 -25
- package/src/docs/resolve.ts +10 -3
- package/src/docs/save.ts +11 -3
- package/src/formats.test.ts +35 -0
- package/src/formats.ts +135 -0
- package/src/headless.test.ts +8 -16
- package/src/help.ts +73 -43
- package/src/hidden-mcpb.test.ts +7 -6
- package/src/hidden.ts +2 -2
- package/src/index.test.ts +120 -96
- package/src/index.ts +36 -24
- package/src/install/binary.ts +12 -5
- package/src/install/completions.ts +7 -3
- package/src/install/detect-installed.ts +29 -4
- package/src/install/gh-release-update.ts +31 -23
- package/src/install/index.ts +69 -19
- package/src/install/install.test.ts +31 -8
- package/src/install/mcp-codex.test.ts +57 -0
- package/src/install/mcp-codex.ts +125 -0
- package/src/install/mcp-config.ts +12 -5
- package/src/install/mcp-opencode.test.ts +98 -0
- package/src/install/mcp-opencode.ts +149 -0
- package/src/install/paths.ts +29 -3
- package/src/install/plan.ts +73 -6
- package/src/install/shell.ts +1 -4
- package/src/install/status.ts +12 -6
- package/src/install/uninstall.ts +38 -4
- package/src/install/update.test.ts +2 -2
- package/src/install/update.ts +3 -1
- package/src/invoke.ts +12 -9
- package/src/mcp/bundle.ts +36 -8
- package/src/mcp/env.ts +7 -13
- package/src/mcp/server.ts +12 -6
- package/src/mcp/tools.ts +83 -18
- package/src/mcp.ts +3 -3
- package/src/parse.ts +129 -27
- package/src/runtime.ts +22 -12
- package/src/schema.ts +11 -5
- package/src/skill/generate.ts +4 -4
- package/src/skill/install.ts +6 -2
- package/src/types.ts +24 -0
- 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 {
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
71
|
-
expect(pr.opts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
344
|
-
expect(pr.opts
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
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
|
|
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]
|
|
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]
|
|
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 {
|
|
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
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
{
|
|
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]
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1967
|
+
expect(argv).toEqual({ error: expect.stringContaining("JSON array") });
|
|
1944
1968
|
});
|
|
1945
1969
|
|
|
1946
|
-
test("mcpToolCallToArgv
|
|
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(
|
|
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
|
|
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(
|
|
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
|
+
});
|