argsbarg 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/README.md +1 -1
- package/index.d.ts +2 -4
- package/package.json +1 -1
- package/src/context.ts +4 -9
- package/src/index.test.ts +57 -57
- package/src/runtime.ts +1 -1
- package/src/validate.ts +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.1.0] - 2026-06-20
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [2.0.1] - 2026-06-20
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- **`CliContext.schema`** — use `ctx.program` (removed alias; `program` is the only field).
|
|
18
|
+
|
|
10
19
|
## [2.0.0] - 2026-06-20
|
|
11
20
|
|
|
12
21
|
### Changed (breaking)
|
|
@@ -171,7 +180,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
171
180
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
172
181
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
173
182
|
|
|
174
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v2.
|
|
183
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v2.1.0...HEAD
|
|
184
|
+
[2.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v2.1.0
|
|
185
|
+
[2.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v2.0.1
|
|
175
186
|
[2.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v2.0.0
|
|
176
187
|
[1.5.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.5.0
|
|
177
188
|
[1.4.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.4.3
|
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ Add `CliPositional` entries to the command’s `positionals` list (separate from
|
|
|
181
181
|
- `ctx.typedOpt<T>("custom", parseFn)` — pass a custom parsing function for type-safe option resolution.
|
|
182
182
|
- `ctx.args` — positional words in order as `string[]`.
|
|
183
183
|
- `ctx.positional("name")` — named positional lookup; varargs slots return `string[]`, single slots return `string | undefined`.
|
|
184
|
-
- `ctx.
|
|
184
|
+
- `ctx.program` — program root (`CliProgram`) for contextual help.
|
|
185
185
|
|
|
186
186
|
### Capabilities (built-ins)
|
|
187
187
|
|
package/index.d.ts
CHANGED
|
@@ -7,13 +7,11 @@ export declare class CliContext {
|
|
|
7
7
|
readonly appName: string;
|
|
8
8
|
readonly commandPath: string[];
|
|
9
9
|
readonly args: string[];
|
|
10
|
-
readonly
|
|
10
|
+
readonly program: CliProgram;
|
|
11
11
|
readonly opts: Record<string, string>;
|
|
12
12
|
readonly invocation: CliInvocation;
|
|
13
|
-
/** Program root schema (same as {@link schema}). */
|
|
14
|
-
get program(): CliProgram;
|
|
15
13
|
/** Captures the program root, routed path, positional words, and option map for a leaf handler. */
|
|
16
|
-
constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>,
|
|
14
|
+
constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation);
|
|
17
15
|
/** Returns whether a presence flag was set (including implicit "1" for boolean options). */
|
|
18
16
|
hasFlag(name: string): boolean;
|
|
19
17
|
/** Returns the string value for a string-valued option, if present. */
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -18,29 +18,24 @@ export class CliContext {
|
|
|
18
18
|
readonly appName: string;
|
|
19
19
|
readonly commandPath: string[];
|
|
20
20
|
readonly args: string[];
|
|
21
|
-
readonly
|
|
21
|
+
readonly program: CliProgram;
|
|
22
22
|
readonly opts: Record<string, string>;
|
|
23
23
|
readonly invocation: CliInvocation;
|
|
24
24
|
|
|
25
|
-
/** Program root schema (same as {@link schema}). */
|
|
26
|
-
get program(): CliProgram {
|
|
27
|
-
return this.schema;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
25
|
/** Captures the program root, routed path, positional words, and option map for a leaf handler. */
|
|
31
26
|
constructor(
|
|
32
27
|
appName: string,
|
|
33
28
|
commandPath: string[],
|
|
34
29
|
args: string[],
|
|
35
30
|
opts: Record<string, string>,
|
|
36
|
-
|
|
31
|
+
program: CliProgram,
|
|
37
32
|
invocation: CliInvocation = "cli",
|
|
38
33
|
) {
|
|
39
34
|
this.appName = appName;
|
|
40
35
|
this.commandPath = commandPath;
|
|
41
36
|
this.args = args;
|
|
42
37
|
this.opts = opts;
|
|
43
|
-
this.
|
|
38
|
+
this.program = program;
|
|
44
39
|
this.invocation = invocation;
|
|
45
40
|
}
|
|
46
41
|
|
|
@@ -85,7 +80,7 @@ export class CliContext {
|
|
|
85
80
|
private _positionalMap(): Record<string, string | string[]> {
|
|
86
81
|
if (this._posMap) return this._posMap;
|
|
87
82
|
|
|
88
|
-
let node: CliNode = this.
|
|
83
|
+
let node: CliNode = this.program;
|
|
89
84
|
for (const seg of this.commandPath) {
|
|
90
85
|
if (!isCliRouter(node)) {
|
|
91
86
|
this._posMap = {};
|
package/src/index.test.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { generateSkillBundle } from "./skill/generate.ts";
|
|
|
26
26
|
import { cliSkillInstall } from "./skill/install.ts";
|
|
27
27
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
28
28
|
import { cliSchemaJson } from "./schema.ts";
|
|
29
|
-
import {
|
|
29
|
+
import { cliValidateProgram } from "./validate.ts";
|
|
30
30
|
import { expect, test } from "bun:test";
|
|
31
31
|
import { $ } from "bun";
|
|
32
32
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -59,7 +59,7 @@ test("bundled short presence flags", () => {
|
|
|
59
59
|
},
|
|
60
60
|
],
|
|
61
61
|
};
|
|
62
|
-
|
|
62
|
+
cliValidateProgram(root);
|
|
63
63
|
const pr = postParseValidate(root, parse(root, ["x", "-ab"]));
|
|
64
64
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
65
65
|
expect(pr.opts["a"]).toBe("1");
|
|
@@ -85,7 +85,7 @@ test("long option equals", () => {
|
|
|
85
85
|
},
|
|
86
86
|
],
|
|
87
87
|
};
|
|
88
|
-
|
|
88
|
+
cliValidateProgram(root);
|
|
89
89
|
const pr = postParseValidate(root, parse(root, ["x", "--name=pat"]));
|
|
90
90
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
91
91
|
expect(pr.opts["name"]).toBe("pat");
|
|
@@ -112,7 +112,7 @@ test("fallback missing or unknown root flags", () => {
|
|
|
112
112
|
fallbackCommand: "hello",
|
|
113
113
|
fallbackMode: CliFallbackMode.MissingOrUnknown,
|
|
114
114
|
};
|
|
115
|
-
|
|
115
|
+
cliValidateProgram(root);
|
|
116
116
|
const pr = postParseValidate(root, parse(root, ["--name", "bob"]));
|
|
117
117
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
118
118
|
expect(pr.path).toEqual(["hello"]);
|
|
@@ -125,7 +125,7 @@ test("unknown command", () => {
|
|
|
125
125
|
description: "",
|
|
126
126
|
commands: [{ key: "hello", description: "", handler: () => {} }],
|
|
127
127
|
};
|
|
128
|
-
|
|
128
|
+
cliValidateProgram(root);
|
|
129
129
|
const pr = parse(root, ["nope"]);
|
|
130
130
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
131
131
|
expect(pr.errorMsg).toContain("Unknown command");
|
|
@@ -137,7 +137,7 @@ test("implicit help empty", () => {
|
|
|
137
137
|
description: "",
|
|
138
138
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
139
139
|
};
|
|
140
|
-
|
|
140
|
+
cliValidateProgram(root);
|
|
141
141
|
const pr = parse(root, []);
|
|
142
142
|
expect(pr.kind).toBe(ParseKind.Help);
|
|
143
143
|
expect(pr.helpExplicit).toBe(false);
|
|
@@ -162,7 +162,7 @@ test("invalid number post validate", () => {
|
|
|
162
162
|
},
|
|
163
163
|
],
|
|
164
164
|
};
|
|
165
|
-
|
|
165
|
+
cliValidateProgram(root);
|
|
166
166
|
let pr = parse(root, ["x", "--n", "notnum"]);
|
|
167
167
|
pr = postParseValidate(root, pr);
|
|
168
168
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
@@ -188,7 +188,7 @@ test("supports scientific notation in numbers", () => {
|
|
|
188
188
|
},
|
|
189
189
|
],
|
|
190
190
|
};
|
|
191
|
-
|
|
191
|
+
cliValidateProgram(root);
|
|
192
192
|
let pr = parse(root, ["x", "--n", "1.23e4"]);
|
|
193
193
|
pr = postParseValidate(root, pr);
|
|
194
194
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
@@ -203,7 +203,7 @@ test("completion scripts contain app name", () => {
|
|
|
203
203
|
description: "Test",
|
|
204
204
|
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
205
205
|
};
|
|
206
|
-
|
|
206
|
+
cliValidateProgram(root);
|
|
207
207
|
const bash = completionBashScript(root);
|
|
208
208
|
expect(bash).toContain("bash completion for myapp");
|
|
209
209
|
expect(bash).toContain("complete -F _myapp myapp");
|
|
@@ -220,7 +220,7 @@ test("completion scripts do not emit invalid bash substitutions", () => {
|
|
|
220
220
|
description: "Test",
|
|
221
221
|
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
222
222
|
};
|
|
223
|
-
|
|
223
|
+
cliValidateProgram(root);
|
|
224
224
|
const bash = completionBashScript(root);
|
|
225
225
|
expect(bash).not.toContain("${${");
|
|
226
226
|
});
|
|
@@ -237,7 +237,7 @@ test("completion scripts escape shell-sensitive command text in zsh", () => {
|
|
|
237
237
|
},
|
|
238
238
|
],
|
|
239
239
|
};
|
|
240
|
-
|
|
240
|
+
cliValidateProgram(root);
|
|
241
241
|
const zsh = completionZshScript(root);
|
|
242
242
|
expect(zsh).toContain("quote'\\''cmd:Say '\\''hello'\\'' and keep going.");
|
|
243
243
|
});
|
|
@@ -248,7 +248,7 @@ test("completion scripts keep dotted app names in registration names", () => {
|
|
|
248
248
|
description: "Test",
|
|
249
249
|
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
250
250
|
};
|
|
251
|
-
|
|
251
|
+
cliValidateProgram(root);
|
|
252
252
|
|
|
253
253
|
const bash = completionBashScript(root);
|
|
254
254
|
expect(bash).toContain("complete -F _minimal_ts minimal.ts");
|
|
@@ -283,7 +283,7 @@ test("trailing options after bounded positionals", () => {
|
|
|
283
283
|
},
|
|
284
284
|
],
|
|
285
285
|
};
|
|
286
|
-
|
|
286
|
+
cliValidateProgram(root);
|
|
287
287
|
const pr = postParseValidate(root, parse(root, ["x", "./file", "--verbose"]));
|
|
288
288
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
289
289
|
expect(pr.args).toEqual(["./file"]);
|
|
@@ -330,7 +330,7 @@ test("trailing options include parent-scoped flags", () => {
|
|
|
330
330
|
},
|
|
331
331
|
],
|
|
332
332
|
};
|
|
333
|
-
|
|
333
|
+
cliValidateProgram(root);
|
|
334
334
|
const pr = postParseValidate(root, parse(root, ["group", "leaf", "-u", "alice", "./file", "--json"]));
|
|
335
335
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
336
336
|
expect(pr.path).toEqual(["group", "leaf"]);
|
|
@@ -367,7 +367,7 @@ test("varargs tail parses trailing options", () => {
|
|
|
367
367
|
},
|
|
368
368
|
],
|
|
369
369
|
};
|
|
370
|
-
|
|
370
|
+
cliValidateProgram(root);
|
|
371
371
|
const pr = postParseValidate(root, parse(root, ["x", "./file", "--json"]));
|
|
372
372
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
373
373
|
expect(pr.args).toEqual(["./file"]);
|
|
@@ -402,7 +402,7 @@ test("stops parsing options at --", () => {
|
|
|
402
402
|
},
|
|
403
403
|
],
|
|
404
404
|
};
|
|
405
|
-
|
|
405
|
+
cliValidateProgram(root);
|
|
406
406
|
const pr = postParseValidate(root, parse(root, ["x", "--name", "pat", "--", "--name", "bob", "-x"]));
|
|
407
407
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
408
408
|
expect(pr.opts["name"]).toBe("pat");
|
|
@@ -429,7 +429,7 @@ test("missing required option returns error", () => {
|
|
|
429
429
|
},
|
|
430
430
|
],
|
|
431
431
|
};
|
|
432
|
-
|
|
432
|
+
cliValidateProgram(root);
|
|
433
433
|
const pr = postParseValidate(root, parse(root, ["x"]));
|
|
434
434
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
435
435
|
expect(pr.errorMsg).toContain("Missing required option: --req");
|
|
@@ -455,7 +455,7 @@ test("provided required option parses ok", () => {
|
|
|
455
455
|
},
|
|
456
456
|
],
|
|
457
457
|
};
|
|
458
|
-
|
|
458
|
+
cliValidateProgram(root);
|
|
459
459
|
const pr = postParseValidate(root, parse(root, ["x", "--req", "val"]));
|
|
460
460
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
461
461
|
expect(pr.opts["req"]).toBe("val");
|
|
@@ -481,7 +481,7 @@ test("presence option cannot be required", () => {
|
|
|
481
481
|
},
|
|
482
482
|
],
|
|
483
483
|
};
|
|
484
|
-
expect(() =>
|
|
484
|
+
expect(() => cliValidateProgram(root)).toThrow(/Presence option cannot be required/);
|
|
485
485
|
});
|
|
486
486
|
|
|
487
487
|
test("leaf completion help prints correctly", async () => {
|
|
@@ -541,7 +541,7 @@ test("parse recognizes --schema at the program root", () => {
|
|
|
541
541
|
},
|
|
542
542
|
],
|
|
543
543
|
};
|
|
544
|
-
|
|
544
|
+
cliValidateProgram(root);
|
|
545
545
|
const pr = parse(root, ["--schema"]);
|
|
546
546
|
expect(pr.kind).toBe(ParseKind.Schema);
|
|
547
547
|
});
|
|
@@ -595,7 +595,7 @@ test("reserved option name schema is rejected", () => {
|
|
|
595
595
|
},
|
|
596
596
|
],
|
|
597
597
|
};
|
|
598
|
-
expect(() =>
|
|
598
|
+
expect(() => cliValidateProgram(root)).toThrow(/reserved for --schema/);
|
|
599
599
|
});
|
|
600
600
|
|
|
601
601
|
test("root help lists --schema built-in", () => {
|
|
@@ -825,7 +825,7 @@ test("reserved command name install is rejected", () => {
|
|
|
825
825
|
},
|
|
826
826
|
],
|
|
827
827
|
};
|
|
828
|
-
expect(() =>
|
|
828
|
+
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: install/);
|
|
829
829
|
});
|
|
830
830
|
|
|
831
831
|
test("top-level command name mcp is allowed without mcpServer", () => {
|
|
@@ -840,7 +840,7 @@ test("top-level command name mcp is allowed without mcpServer", () => {
|
|
|
840
840
|
},
|
|
841
841
|
],
|
|
842
842
|
};
|
|
843
|
-
expect(() =>
|
|
843
|
+
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
844
844
|
});
|
|
845
845
|
|
|
846
846
|
test("top-level command name mcp is rejected when mcpServer is set", () => {
|
|
@@ -856,7 +856,7 @@ test("top-level command name mcp is rejected when mcpServer is set", () => {
|
|
|
856
856
|
},
|
|
857
857
|
],
|
|
858
858
|
};
|
|
859
|
-
expect(() =>
|
|
859
|
+
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: mcp/);
|
|
860
860
|
});
|
|
861
861
|
|
|
862
862
|
test("mcpServer on non-root node is rejected", () => {
|
|
@@ -872,7 +872,7 @@ test("mcpServer on non-root node is rejected", () => {
|
|
|
872
872
|
},
|
|
873
873
|
],
|
|
874
874
|
} as unknown as CliProgram;
|
|
875
|
-
expect(() =>
|
|
875
|
+
expect(() => cliValidateProgram(root)).toThrow(/mcpServer is only supported on the program root/);
|
|
876
876
|
});
|
|
877
877
|
|
|
878
878
|
test("mcpTool on root is rejected", () => {
|
|
@@ -882,7 +882,7 @@ test("mcpTool on root is rejected", () => {
|
|
|
882
882
|
mcpTool: { enabled: false },
|
|
883
883
|
handler: () => {},
|
|
884
884
|
};
|
|
885
|
-
expect(() =>
|
|
885
|
+
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
886
886
|
});
|
|
887
887
|
|
|
888
888
|
test("mcpTool on routing node is rejected", () => {
|
|
@@ -904,7 +904,7 @@ test("mcpTool on routing node is rejected", () => {
|
|
|
904
904
|
},
|
|
905
905
|
],
|
|
906
906
|
};
|
|
907
|
-
expect(() =>
|
|
907
|
+
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
908
908
|
});
|
|
909
909
|
|
|
910
910
|
test("buildToolCallSuccess returns stdout only", () => {
|
|
@@ -1059,7 +1059,7 @@ test("ctx.invocation is mcp via cliInvoke", async () => {
|
|
|
1059
1059
|
seen = ctx.invocation;
|
|
1060
1060
|
},
|
|
1061
1061
|
};
|
|
1062
|
-
|
|
1062
|
+
cliValidateProgram(root);
|
|
1063
1063
|
const result = await cliInvoke(root, []);
|
|
1064
1064
|
expect(result.kind).toBe("ok");
|
|
1065
1065
|
expect(seen).toBe("mcp");
|
|
@@ -1109,7 +1109,7 @@ test("cliInvoke rejects invalid Enum value", async () => {
|
|
|
1109
1109
|
},
|
|
1110
1110
|
],
|
|
1111
1111
|
};
|
|
1112
|
-
|
|
1112
|
+
cliValidateProgram(root);
|
|
1113
1113
|
const result = await cliInvoke(root, ["--mode", "staging"]);
|
|
1114
1114
|
expect(result.kind).toBe("error");
|
|
1115
1115
|
expect(result.errorMsg).toContain("not one of");
|
|
@@ -1132,30 +1132,30 @@ test("cliInvoke accepts valid Enum value", async () => {
|
|
|
1132
1132
|
},
|
|
1133
1133
|
],
|
|
1134
1134
|
};
|
|
1135
|
-
|
|
1135
|
+
cliValidateProgram(root);
|
|
1136
1136
|
const result = await cliInvoke(root, ["--mode", "dev"]);
|
|
1137
1137
|
expect(result.kind).toBe("ok");
|
|
1138
1138
|
expect(result.stdout.trim()).toBe("dev");
|
|
1139
1139
|
});
|
|
1140
1140
|
|
|
1141
|
-
test("
|
|
1141
|
+
test("cliValidateProgram rejects Enum with no choices", () => {
|
|
1142
1142
|
const root: CliProgram = {
|
|
1143
1143
|
key: "app",
|
|
1144
1144
|
description: "",
|
|
1145
1145
|
handler: () => {},
|
|
1146
1146
|
options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: [] }],
|
|
1147
1147
|
};
|
|
1148
|
-
expect(() =>
|
|
1148
|
+
expect(() => cliValidateProgram(root)).toThrow(/requires non-empty choices/);
|
|
1149
1149
|
});
|
|
1150
1150
|
|
|
1151
|
-
test("
|
|
1151
|
+
test("cliValidateProgram rejects Enum with duplicate choices", () => {
|
|
1152
1152
|
const root: CliProgram = {
|
|
1153
1153
|
key: "app",
|
|
1154
1154
|
description: "",
|
|
1155
1155
|
handler: () => {},
|
|
1156
1156
|
options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: ["a", "a"] }],
|
|
1157
1157
|
};
|
|
1158
|
-
expect(() =>
|
|
1158
|
+
expect(() => cliValidateProgram(root)).toThrow(/choices must be distinct/);
|
|
1159
1159
|
});
|
|
1160
1160
|
|
|
1161
1161
|
test("mcpTool.description override wins without requiresEnv suffix", () => {
|
|
@@ -1194,7 +1194,7 @@ test("mcpTool.requiresEnv appended to auto description", () => {
|
|
|
1194
1194
|
expect(tools[0]!.description).toContain("[requires env: TOKEN]");
|
|
1195
1195
|
});
|
|
1196
1196
|
|
|
1197
|
-
test("
|
|
1197
|
+
test("cliValidateProgram rejects duplicate mcpResources URIs", () => {
|
|
1198
1198
|
const root: CliProgram = {
|
|
1199
1199
|
key: "app",
|
|
1200
1200
|
description: "",
|
|
@@ -1206,10 +1206,10 @@ test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
|
|
|
1206
1206
|
},
|
|
1207
1207
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
1208
1208
|
};
|
|
1209
|
-
expect(() =>
|
|
1209
|
+
expect(() => cliValidateProgram(root)).toThrow(/URIs must be unique/);
|
|
1210
1210
|
});
|
|
1211
1211
|
|
|
1212
|
-
test("
|
|
1212
|
+
test("cliValidateProgram rejects resource URI matching schemaResourceUri", () => {
|
|
1213
1213
|
const root: CliProgram = {
|
|
1214
1214
|
key: "app",
|
|
1215
1215
|
description: "",
|
|
@@ -1219,7 +1219,7 @@ test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
|
|
|
1219
1219
|
},
|
|
1220
1220
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
1221
1221
|
};
|
|
1222
|
-
expect(() =>
|
|
1222
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with the built-in schema resource/);
|
|
1223
1223
|
});
|
|
1224
1224
|
|
|
1225
1225
|
test("allMcpResources includes custom resources", () => {
|
|
@@ -1430,7 +1430,7 @@ function nestedDocsFallbackFixture(): CliProgram {
|
|
|
1430
1430
|
|
|
1431
1431
|
test("nested fallback routes to default when argv exhausted at router", () => {
|
|
1432
1432
|
const root = nestedDocsFallbackFixture();
|
|
1433
|
-
|
|
1433
|
+
cliValidateProgram(root);
|
|
1434
1434
|
const pr = postParseValidate(root, parse(root, ["docs"]));
|
|
1435
1435
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1436
1436
|
expect(pr.path).toEqual(["docs", "guide"]);
|
|
@@ -1470,7 +1470,7 @@ test("nested fallback MissingOrUnknown routes unknown token to default", () => {
|
|
|
1470
1470
|
},
|
|
1471
1471
|
],
|
|
1472
1472
|
};
|
|
1473
|
-
|
|
1473
|
+
cliValidateProgram(root);
|
|
1474
1474
|
const pr = postParseValidate(root, parse(root, ["docs", "extra-topic"]));
|
|
1475
1475
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1476
1476
|
expect(pr.path).toEqual(["docs", "guide"]);
|
|
@@ -1479,13 +1479,13 @@ test("nested fallback MissingOrUnknown routes unknown token to default", () => {
|
|
|
1479
1479
|
|
|
1480
1480
|
test("nested fallback MissingOnly errors on unknown subcommand", () => {
|
|
1481
1481
|
const root = nestedDocsFallbackFixture();
|
|
1482
|
-
|
|
1482
|
+
cliValidateProgram(root);
|
|
1483
1483
|
const pr = parse(root, ["docs", "nope"]);
|
|
1484
1484
|
expect(pr.kind).toBe(ParseKind.Error);
|
|
1485
1485
|
expect(pr.errorMsg).toContain("Unknown subcommand");
|
|
1486
1486
|
});
|
|
1487
1487
|
|
|
1488
|
-
test("
|
|
1488
|
+
test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
|
|
1489
1489
|
const root: CliProgram = {
|
|
1490
1490
|
key: "app",
|
|
1491
1491
|
description: "",
|
|
@@ -1504,17 +1504,17 @@ test("cliValidateRoot rejects invalid nested fallbackCommand", () => {
|
|
|
1504
1504
|
},
|
|
1505
1505
|
],
|
|
1506
1506
|
};
|
|
1507
|
-
expect(() =>
|
|
1507
|
+
expect(() => cliValidateProgram(root)).toThrow(/fallbackCommand 'missing' is not a child of 'docs'/);
|
|
1508
1508
|
});
|
|
1509
1509
|
|
|
1510
|
-
test("
|
|
1510
|
+
test("cliValidateProgram accepts nested fallbackCommand when child exists", () => {
|
|
1511
1511
|
const root = nestedDocsFallbackFixture();
|
|
1512
|
-
expect(() =>
|
|
1512
|
+
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
1513
1513
|
});
|
|
1514
1514
|
|
|
1515
1515
|
test("nested router scoped help does not route to fallback", () => {
|
|
1516
1516
|
const root = nestedDocsFallbackFixture();
|
|
1517
|
-
|
|
1517
|
+
cliValidateProgram(root);
|
|
1518
1518
|
const pr = parse(root, ["docs", "--help"]);
|
|
1519
1519
|
expect(pr.kind).toBe(ParseKind.Help);
|
|
1520
1520
|
expect(pr.helpPath).toEqual(["docs"]);
|
|
@@ -1526,7 +1526,7 @@ test("nested router scoped help does not route to fallback", () => {
|
|
|
1526
1526
|
|
|
1527
1527
|
test("varargs trailing option after positionals via cliInvoke", async () => {
|
|
1528
1528
|
const root = varargsReadFixture();
|
|
1529
|
-
|
|
1529
|
+
cliValidateProgram(root);
|
|
1530
1530
|
const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--json"]));
|
|
1531
1531
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1532
1532
|
expect(pr.args).toEqual(["file.txt"]);
|
|
@@ -1535,7 +1535,7 @@ test("varargs trailing option after positionals via cliInvoke", async () => {
|
|
|
1535
1535
|
|
|
1536
1536
|
test("varargs option before positionals", () => {
|
|
1537
1537
|
const root = varargsReadFixture();
|
|
1538
|
-
|
|
1538
|
+
cliValidateProgram(root);
|
|
1539
1539
|
const pr = postParseValidate(root, parse(root, ["read", "--json", "file.txt"]));
|
|
1540
1540
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1541
1541
|
expect(pr.args).toEqual(["file.txt"]);
|
|
@@ -1544,7 +1544,7 @@ test("varargs option before positionals", () => {
|
|
|
1544
1544
|
|
|
1545
1545
|
test("varargs multiple files then trailing option", () => {
|
|
1546
1546
|
const root = varargsReadFixture();
|
|
1547
|
-
|
|
1547
|
+
cliValidateProgram(root);
|
|
1548
1548
|
const pr = postParseValidate(root, parse(root, ["read", "a.txt", "b.txt", "--json"]));
|
|
1549
1549
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1550
1550
|
expect(pr.args).toEqual(["a.txt", "b.txt"]);
|
|
@@ -1553,7 +1553,7 @@ test("varargs multiple files then trailing option", () => {
|
|
|
1553
1553
|
|
|
1554
1554
|
test("varargs double dash forces positional", () => {
|
|
1555
1555
|
const root = varargsReadFixture();
|
|
1556
|
-
|
|
1556
|
+
cliValidateProgram(root);
|
|
1557
1557
|
const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--", "--json"]));
|
|
1558
1558
|
expect(pr.kind).toBe(ParseKind.Ok);
|
|
1559
1559
|
expect(pr.args).toEqual(["file.txt", "--json"]);
|
|
@@ -1562,7 +1562,7 @@ test("varargs double dash forces positional", () => {
|
|
|
1562
1562
|
|
|
1563
1563
|
test("varargs unknown flag errors", async () => {
|
|
1564
1564
|
const root = varargsReadFixture();
|
|
1565
|
-
|
|
1565
|
+
cliValidateProgram(root);
|
|
1566
1566
|
const result = await cliInvoke(root, ["read", "--unknown"]);
|
|
1567
1567
|
expect(result.kind).toBe("error");
|
|
1568
1568
|
expect(result.stderr).toContain("--unknown");
|
|
@@ -1570,7 +1570,7 @@ test("varargs unknown flag errors", async () => {
|
|
|
1570
1570
|
|
|
1571
1571
|
test("varargs scoped help in tail", () => {
|
|
1572
1572
|
const root = varargsReadFixture();
|
|
1573
|
-
|
|
1573
|
+
cliValidateProgram(root);
|
|
1574
1574
|
const pr = parse(root, ["read", "file.txt", "--help"]);
|
|
1575
1575
|
expect(pr.kind).toBe(ParseKind.Help);
|
|
1576
1576
|
expect(pr.helpPath).toContain("read");
|
|
@@ -1593,7 +1593,7 @@ test("ctx.positional returns single slot value", async () => {
|
|
|
1593
1593
|
],
|
|
1594
1594
|
};
|
|
1595
1595
|
let captured: string | string[] | undefined;
|
|
1596
|
-
|
|
1596
|
+
cliValidateProgram(root);
|
|
1597
1597
|
await cliInvoke(root, ["x", "./file"]);
|
|
1598
1598
|
expect(captured).toBe("./file");
|
|
1599
1599
|
});
|
|
@@ -1606,7 +1606,7 @@ test("ctx.positional returns varargs array", async () => {
|
|
|
1606
1606
|
captured = ctx.positional("files");
|
|
1607
1607
|
};
|
|
1608
1608
|
}
|
|
1609
|
-
|
|
1609
|
+
cliValidateProgram(root);
|
|
1610
1610
|
await cliInvoke(root, ["read", "a.txt", "b.txt"]);
|
|
1611
1611
|
expect(captured).toEqual(["a.txt", "b.txt"]);
|
|
1612
1612
|
});
|
|
@@ -1629,7 +1629,7 @@ test("ctx.positional returns undefined for absent optional slot", async () => {
|
|
|
1629
1629
|
],
|
|
1630
1630
|
};
|
|
1631
1631
|
let captured: string | string[] | undefined;
|
|
1632
|
-
|
|
1632
|
+
cliValidateProgram(root);
|
|
1633
1633
|
await cliInvoke(root, ["x"]);
|
|
1634
1634
|
expect(captured).toBeUndefined();
|
|
1635
1635
|
});
|
|
@@ -1644,7 +1644,7 @@ test("ctx.positional varargs matches ctx.args", async () => {
|
|
|
1644
1644
|
args = ctx.args;
|
|
1645
1645
|
};
|
|
1646
1646
|
}
|
|
1647
|
-
|
|
1647
|
+
cliValidateProgram(root);
|
|
1648
1648
|
await cliInvoke(root, ["read", "a.txt", "b.txt"]);
|
|
1649
1649
|
expect(positional).toEqual(args);
|
|
1650
1650
|
});
|
|
@@ -1692,7 +1692,7 @@ test("install config on non-root node is rejected", () => {
|
|
|
1692
1692
|
},
|
|
1693
1693
|
],
|
|
1694
1694
|
} as unknown as CliProgram;
|
|
1695
|
-
expect(() =>
|
|
1695
|
+
expect(() => cliValidateProgram(root)).toThrow(/install is only supported on the program root/);
|
|
1696
1696
|
});
|
|
1697
1697
|
|
|
1698
1698
|
test("generateSkillBundle includes frontmatter and command catalog", () => {
|
package/src/runtime.ts
CHANGED
|
@@ -121,6 +121,6 @@ export function cliErrWithHelp(ctx: CliContext, msg: string): never {
|
|
|
121
121
|
const color = process.stderr.isTTY;
|
|
122
122
|
const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
|
|
123
123
|
process.stderr.write(line + "\n");
|
|
124
|
-
process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.
|
|
124
|
+
process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.program), ctx.commandPath, true));
|
|
125
125
|
process.exit(1);
|
|
126
126
|
}
|
package/src/validate.ts
CHANGED
|
@@ -30,9 +30,6 @@ export function cliValidateProgram(program: CliProgram): void {
|
|
|
30
30
|
walkNode(program, program, true);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** @deprecated Internal alias — use cliValidateProgram */
|
|
34
|
-
export const cliValidateRoot = cliValidateProgram;
|
|
35
|
-
|
|
36
33
|
function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
37
34
|
if (!isRoot) {
|
|
38
35
|
const rogue = node as CliProgram;
|