argsbarg 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.private/scratch.md +1 -0
- package/CHANGELOG.md +16 -1
- package/README.md +2 -0
- package/justfile +4 -4
- package/package.json +1 -1
- package/src/completion.ts +43 -2
- package/src/help.ts +8 -4
- package/src/index.test.ts +165 -0
- package/src/parse.ts +27 -0
- package/src/runtime.ts +13 -9
- package/src/schema.ts +93 -0
- package/src/validate.ts +6 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- [x] --schema feature for ai agents
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.1] - 2026-06-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`--schema` discoverability** — list the flag in root help and offer it in shell completions at the program root (same pattern as `--help`).
|
|
15
|
+
- **Leaf root help** — show the reserved `completion` command in root help and `--schema` output (routing CLIs already did).
|
|
16
|
+
|
|
17
|
+
## [1.3.0] - 2026-06-18
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`--schema`** — prints the full CLI tree as JSON to stdout (exit 0). Handlers are omitted; the injected `completion` subtree is excluded. Option name `schema` is reserved.
|
|
22
|
+
|
|
10
23
|
## [1.2.1] - 2026-06-18
|
|
11
24
|
|
|
12
25
|
### Changed
|
|
@@ -71,7 +84,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
71
84
|
- 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`).
|
|
72
85
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
73
86
|
|
|
74
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.
|
|
87
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.3.1...HEAD
|
|
88
|
+
[1.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.3.1
|
|
89
|
+
[1.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.3.0
|
|
75
90
|
[1.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.1
|
|
76
91
|
[1.2.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.0
|
|
77
92
|
[1.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.1.1
|
package/README.md
CHANGED
|
@@ -88,9 +88,11 @@ Everything you need for a first-class CLI:
|
|
|
88
88
|
Every app gets:
|
|
89
89
|
|
|
90
90
|
- `-h` / `--help` at any routing depth (scoped help).
|
|
91
|
+
- **`--schema`** at the program root — print the full command tree as JSON (for tooling and agents).
|
|
91
92
|
- **`completion bash` / `completion zsh`** — print shell completion scripts to stdout (injected by `cliRun`).
|
|
92
93
|
|
|
93
94
|
Do not declare a top-level command named **`completion`** — it is reserved for this built-in.
|
|
95
|
+
Do not declare an option named **`schema`** — it is reserved for `--schema`.
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
### Shell completions
|
package/justfile
CHANGED
|
@@ -8,12 +8,12 @@ check-types:
|
|
|
8
8
|
bun x tsc
|
|
9
9
|
|
|
10
10
|
# run the minimal example
|
|
11
|
-
example:
|
|
12
|
-
bun ./examples/minimal.ts
|
|
11
|
+
example *ARGS:
|
|
12
|
+
bun ./examples/minimal.ts {{ARGS}}
|
|
13
13
|
|
|
14
14
|
# run the minimal example and watch for changes
|
|
15
|
-
example-watch:
|
|
16
|
-
bun --watch ./examples/minimal.ts
|
|
15
|
+
example-watch *ARGS:
|
|
16
|
+
bun --watch ./examples/minimal.ts {{ARGS}}
|
|
17
17
|
|
|
18
18
|
# format the codebase
|
|
19
19
|
format:
|
package/package.json
CHANGED
package/src/completion.ts
CHANGED
|
@@ -77,6 +77,8 @@ function mainName(schemaName: string): string {
|
|
|
77
77
|
|
|
78
78
|
const kHelpLong = "--help";
|
|
79
79
|
const kHelpShort = "-h";
|
|
80
|
+
const kSchemaLong = "--schema";
|
|
81
|
+
const kSchemaDesc = "Print the full command tree as JSON.";
|
|
80
82
|
|
|
81
83
|
// ── Bash Completion ────────────────────────────────────────────────────────────
|
|
82
84
|
|
|
@@ -89,6 +91,9 @@ function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
|
89
91
|
o += " " + i + ")\n";
|
|
90
92
|
o += " case $w in\n";
|
|
91
93
|
o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
94
|
+
if (sc.path === "") {
|
|
95
|
+
o += " " + kSchemaLong + ") echo 1 ;;\n";
|
|
96
|
+
}
|
|
92
97
|
for (const op of sc.opts) {
|
|
93
98
|
const base = "--" + op.name;
|
|
94
99
|
if (op.kind === "presence") {
|
|
@@ -179,7 +184,7 @@ function emitSimulate(ident: string): string {
|
|
|
179
184
|
o += " local i=1 sid=0 w steps next\n";
|
|
180
185
|
o += " while (( i < COMP_CWORD )); do\n";
|
|
181
186
|
o += " w=\"${COMP_WORDS[i]}\"\n";
|
|
182
|
-
|
|
187
|
+
o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
|
|
183
188
|
o += " ((i++)); continue\n";
|
|
184
189
|
o += " fi\n";
|
|
185
190
|
o += " if [[ $w == --* ]]; then\n";
|
|
@@ -260,6 +265,9 @@ export function completionBashScript(schema: CliCommand): string {
|
|
|
260
265
|
for (const [i, sc] of scopes.entries()) {
|
|
261
266
|
out += "A_" + ident + "_" + i + "_opts=()\n";
|
|
262
267
|
out += "A_" + ident + "_" + i + "_opts+=('" + kHelpLong + "' '" + kHelpShort + "')\n";
|
|
268
|
+
if (sc.path === "") {
|
|
269
|
+
out += "A_" + ident + "_" + i + "_opts+=('" + kSchemaLong + "')\n";
|
|
270
|
+
}
|
|
263
271
|
for (const o of sc.opts) {
|
|
264
272
|
out += "A_" + ident + "_" + i + "_opts+=('--" + o.name + "')\n";
|
|
265
273
|
if (o.shortName) {
|
|
@@ -295,6 +303,14 @@ function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
295
303
|
out += "typeset -g A_" + ident + "_" + i + "_opts\n";
|
|
296
304
|
out += "A_" + ident + "_" + i + "_opts=(";
|
|
297
305
|
out += "'" + escShellSingleQuoted(kHelpLong) + ":" + escShellSingleQuoted("Show help for this command.") + "' '" + escShellSingleQuoted(kHelpShort) + ":" + escShellSingleQuoted("Show help for this command.") + "'";
|
|
306
|
+
if (sc.path === "") {
|
|
307
|
+
out +=
|
|
308
|
+
" '" +
|
|
309
|
+
escShellSingleQuoted(kSchemaLong) +
|
|
310
|
+
":" +
|
|
311
|
+
escShellSingleQuoted(kSchemaDesc) +
|
|
312
|
+
"'";
|
|
313
|
+
}
|
|
298
314
|
for (const o of sc.opts) {
|
|
299
315
|
out += " '" + escShellSingleQuoted("--" + o.name) + ":" + escShellSingleQuoted(o.description) + "'";
|
|
300
316
|
if (o.shortName) {
|
|
@@ -329,6 +345,9 @@ function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
329
345
|
o += " " + i + ")\n";
|
|
330
346
|
o += " case $w in\n";
|
|
331
347
|
o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
348
|
+
if (sc.path === "") {
|
|
349
|
+
o += " " + kSchemaLong + ") echo 1 ;;\n";
|
|
350
|
+
}
|
|
332
351
|
for (const op of sc.opts) {
|
|
333
352
|
const base = "--" + op.name;
|
|
334
353
|
if (op.kind === "presence") {
|
|
@@ -419,7 +438,7 @@ function emitSimulateZsh(ident: string): string {
|
|
|
419
438
|
o += " local i=2 sid=0 w steps next\n";
|
|
420
439
|
o += " while (( i < CURRENT )); do\n";
|
|
421
440
|
o += " w=$words[i]\n";
|
|
422
|
-
|
|
441
|
+
o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
|
|
423
442
|
o += " ((i++)); continue\n";
|
|
424
443
|
o += " fi\n";
|
|
425
444
|
o += " if [[ $w == --* ]]; then\n";
|
|
@@ -502,6 +521,28 @@ export function completionZshScript(schema: CliCommand): string {
|
|
|
502
521
|
return out;
|
|
503
522
|
}
|
|
504
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Returns a schema suitable for help display, including the reserved `completion` subtree.
|
|
526
|
+
* Routing roots get `completion` merged; leaf roots are wrapped as a tiny router.
|
|
527
|
+
*/
|
|
528
|
+
export function cliPresentationRoot(root: CliCommand): CliCommand {
|
|
529
|
+
if ((root.commands ?? []).some((c) => c.key === "completion")) {
|
|
530
|
+
return root;
|
|
531
|
+
}
|
|
532
|
+
if ("handler" in root && root.handler) {
|
|
533
|
+
return {
|
|
534
|
+
key: root.key,
|
|
535
|
+
description: root.description,
|
|
536
|
+
options: root.options,
|
|
537
|
+
commands: [cliBuiltinCompletionGroup(root.key)],
|
|
538
|
+
} as CliCommand;
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
...root,
|
|
542
|
+
commands: [...(root.commands ?? []), cliBuiltinCompletionGroup(root.key)],
|
|
543
|
+
} as CliCommand;
|
|
544
|
+
}
|
|
545
|
+
|
|
505
546
|
/**
|
|
506
547
|
* Builds the static `completion` / `bash` / `zsh` command subtree (merged into the program root at runtime).
|
|
507
548
|
*/
|
package/src/help.ts
CHANGED
|
@@ -335,13 +335,17 @@ function usageLines(
|
|
|
335
335
|
return out;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
/** Table rows for named options, including
|
|
339
|
-
function rowsForOptions(defs: CliOption[], color: boolean): HelpRow[] {
|
|
338
|
+
/** Table rows for named options, including synthetic built-in rows. */
|
|
339
|
+
function rowsForOptions(defs: CliOption[], color: boolean, isRoot: boolean): HelpRow[] {
|
|
340
340
|
const rows: HelpRow[] = [];
|
|
341
341
|
const helpLabel = color
|
|
342
342
|
? style.aquaBold("--help, ") + style.greenBright("-h")
|
|
343
343
|
: "--help, -h";
|
|
344
344
|
rows.push({ label: helpLabel, description: "Show help for this command." });
|
|
345
|
+
if (isRoot) {
|
|
346
|
+
const schemaLabel = color ? style.aquaBold("--schema") : "--schema";
|
|
347
|
+
rows.push({ label: schemaLabel, description: "Print the full command tree as JSON." });
|
|
348
|
+
}
|
|
345
349
|
for (const o of defs) {
|
|
346
350
|
const desc = o.required ? "(required) " + o.description : o.description;
|
|
347
351
|
rows.push({ label: cliOptionLabel(o, color), description: desc });
|
|
@@ -387,7 +391,7 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
387
391
|
).join("\n"),
|
|
388
392
|
);
|
|
389
393
|
|
|
390
|
-
const optBox = renderTableBox("Options", rowsForOptions(schema.options ?? [], color), hw, color);
|
|
394
|
+
const optBox = renderTableBox("Options", rowsForOptions(schema.options ?? [], color, true), hw, color);
|
|
391
395
|
if (optBox.length > 0) {
|
|
392
396
|
lines.push("");
|
|
393
397
|
lines.push(optBox.join("\n"));
|
|
@@ -430,7 +434,7 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
430
434
|
).join("\n"),
|
|
431
435
|
);
|
|
432
436
|
|
|
433
|
-
const optBox = renderTableBox("Options", rowsForOptions(node.options ?? [], color), hw, color);
|
|
437
|
+
const optBox = renderTableBox("Options", rowsForOptions(node.options ?? [], color, false), hw, color);
|
|
434
438
|
if (optBox.length > 0) {
|
|
435
439
|
lines.push("");
|
|
436
440
|
lines.push(optBox.join("\n"));
|
package/src/index.test.ts
CHANGED
|
@@ -8,8 +8,10 @@ shell output regressions.
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
|
+
import { cliHelpRender } from "./help.ts";
|
|
11
12
|
import { CliCommand, CliFallbackMode, CliOptionKind } from "./index.ts";
|
|
12
13
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
14
|
+
import { cliSchemaJson } from "./schema.ts";
|
|
13
15
|
import { cliValidateRoot } from "./validate.ts";
|
|
14
16
|
import { expect, test } from "bun:test";
|
|
15
17
|
import { $ } from "bun";
|
|
@@ -474,4 +476,167 @@ test("leaf completion help prints correctly", async () => {
|
|
|
474
476
|
expect(out).toContain("Show help for this command.");
|
|
475
477
|
expect(out).toContain("Output is the whole script.");
|
|
476
478
|
expect(stderr.toString()).toBe("");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("--schema exports JSON for nested CLIs", async () => {
|
|
482
|
+
const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts --schema`.nothrow().quiet();
|
|
483
|
+
expect(exitCode).toBe(0);
|
|
484
|
+
expect(stderr.toString()).toBe("");
|
|
485
|
+
|
|
486
|
+
const schema = JSON.parse(stdout.toString());
|
|
487
|
+
expect(schema.key).toBe("nested.ts");
|
|
488
|
+
expect(schema.fallbackCommand).toBe("read");
|
|
489
|
+
expect(schema.commands.map((c: { key: string }) => c.key)).toEqual(["stat", "read"]);
|
|
490
|
+
expect(schema.commands).not.toContainEqual(expect.objectContaining({ key: "completion" }));
|
|
491
|
+
|
|
492
|
+
const lookup = schema.commands[0].commands[0].commands[0];
|
|
493
|
+
expect(lookup.key).toBe("lookup");
|
|
494
|
+
expect(lookup.positionals[0].name).toBe("path");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("--schema exports JSON for leaf roots", async () => {
|
|
498
|
+
const { stdout, exitCode } = await $`bun run examples/minimal.ts --schema`.nothrow().quiet();
|
|
499
|
+
expect(exitCode).toBe(0);
|
|
500
|
+
|
|
501
|
+
const schema = JSON.parse(stdout.toString());
|
|
502
|
+
expect(schema.key).toBe("minimal.ts");
|
|
503
|
+
expect(schema.positionals[0].name).toBe("name");
|
|
504
|
+
expect(schema.options[0].name).toBe("verbose");
|
|
505
|
+
expect(schema.commands.map((c: { key: string }) => c.key)).toEqual(["completion"]);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("leaf root help lists completion built-in", async () => {
|
|
509
|
+
const { stdout, exitCode } = await $`bun run examples/minimal.ts -h`.nothrow().quiet();
|
|
510
|
+
expect(exitCode).toBe(0);
|
|
511
|
+
expect(stdout.toString()).toContain("completion");
|
|
512
|
+
expect(stdout.toString()).toContain("Generate the autocompletion script for shells.");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("parse recognizes --schema at the program root", () => {
|
|
516
|
+
const root: CliCommand = {
|
|
517
|
+
key: "app",
|
|
518
|
+
description: "demo",
|
|
519
|
+
commands: [
|
|
520
|
+
{
|
|
521
|
+
key: "x",
|
|
522
|
+
description: "cmd",
|
|
523
|
+
handler: () => {},
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
cliValidateRoot(root);
|
|
528
|
+
const pr = parse(root, ["--schema"]);
|
|
529
|
+
expect(pr.kind).toBe(ParseKind.Schema);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("cliSchemaJson omits handlers and completion built-ins", () => {
|
|
533
|
+
const root: CliCommand = {
|
|
534
|
+
key: "app",
|
|
535
|
+
description: "demo",
|
|
536
|
+
commands: [
|
|
537
|
+
{
|
|
538
|
+
key: "x",
|
|
539
|
+
description: "cmd",
|
|
540
|
+
handler: () => {},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
key: "completion",
|
|
544
|
+
description: "should not appear",
|
|
545
|
+
commands: [
|
|
546
|
+
{
|
|
547
|
+
key: "bash",
|
|
548
|
+
description: "",
|
|
549
|
+
handler: () => {},
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const schema = JSON.parse(cliSchemaJson(root));
|
|
557
|
+
expect(schema.commands).toHaveLength(1);
|
|
558
|
+
expect(schema.commands[0].key).toBe("x");
|
|
559
|
+
expect(schema).not.toHaveProperty("handler");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("reserved option name schema is rejected", () => {
|
|
563
|
+
const root: CliCommand = {
|
|
564
|
+
key: "app",
|
|
565
|
+
description: "",
|
|
566
|
+
commands: [
|
|
567
|
+
{
|
|
568
|
+
key: "x",
|
|
569
|
+
description: "cmd",
|
|
570
|
+
options: [
|
|
571
|
+
{
|
|
572
|
+
name: "schema",
|
|
573
|
+
description: "",
|
|
574
|
+
kind: CliOptionKind.String,
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
handler: () => {},
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
};
|
|
581
|
+
expect(() => cliValidateRoot(root)).toThrow(/reserved for --schema/);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("root help lists --schema built-in", () => {
|
|
585
|
+
const root: CliCommand = {
|
|
586
|
+
key: "app",
|
|
587
|
+
description: "demo",
|
|
588
|
+
commands: [
|
|
589
|
+
{
|
|
590
|
+
key: "x",
|
|
591
|
+
description: "cmd",
|
|
592
|
+
handler: () => {},
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
};
|
|
596
|
+
const help = cliHelpRender(root, [], false);
|
|
597
|
+
expect(help).toContain("--schema");
|
|
598
|
+
expect(help).toContain("Print the full command tree as JSON.");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("nested help omits --schema built-in", () => {
|
|
602
|
+
const root: CliCommand = {
|
|
603
|
+
key: "app",
|
|
604
|
+
description: "demo",
|
|
605
|
+
commands: [
|
|
606
|
+
{
|
|
607
|
+
key: "x",
|
|
608
|
+
description: "cmd",
|
|
609
|
+
handler: () => {},
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
};
|
|
613
|
+
const help = cliHelpRender(root, ["x"], false);
|
|
614
|
+
expect(help).not.toContain("--schema");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("completion scripts offer --schema at the program root only", () => {
|
|
618
|
+
const root: CliCommand = {
|
|
619
|
+
key: "myapp",
|
|
620
|
+
description: "",
|
|
621
|
+
commands: [
|
|
622
|
+
{
|
|
623
|
+
key: "stat",
|
|
624
|
+
description: "stats",
|
|
625
|
+
commands: [
|
|
626
|
+
{
|
|
627
|
+
key: "show",
|
|
628
|
+
description: "show",
|
|
629
|
+
handler: () => {},
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const bash = completionBashScript(root);
|
|
637
|
+
expect(bash).toContain("A_myapp_0_opts+=('--schema')");
|
|
638
|
+
expect(bash).not.toContain("A_myapp_1_opts+=('--schema')");
|
|
639
|
+
|
|
640
|
+
const zsh = completionZshScript(root);
|
|
641
|
+
expect(zsh).toContain("'--schema:Print the full command tree as JSON.'");
|
|
477
642
|
});
|
package/src/parse.ts
CHANGED
|
@@ -26,6 +26,8 @@ export enum ParseKind {
|
|
|
26
26
|
Ok = "ok",
|
|
27
27
|
/** User requested help (explicit or implicit). */
|
|
28
28
|
Help = "help",
|
|
29
|
+
/** User requested machine-readable schema export (`--schema`). */
|
|
30
|
+
Schema = "schema",
|
|
29
31
|
/** User error (unknown command, bad option, etc.). */
|
|
30
32
|
Error = "error",
|
|
31
33
|
}
|
|
@@ -54,12 +56,18 @@ export interface ParseResult {
|
|
|
54
56
|
|
|
55
57
|
const helpShort = "-h";
|
|
56
58
|
const helpLong = "--help";
|
|
59
|
+
const schemaLong = "--schema";
|
|
57
60
|
|
|
58
61
|
/** Returns true if the argv token is `-h` or `--help`. */
|
|
59
62
|
function isHelpTok(tok: string): boolean {
|
|
60
63
|
return tok === helpShort || tok === helpLong;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
/** Returns true if the argv token is `--schema`. */
|
|
67
|
+
function isSchemaTok(tok: string): boolean {
|
|
68
|
+
return tok === schemaLong;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
/** Looks up a subcommand or routing node by `key`. */
|
|
64
72
|
function findChild(cmds: CliCommand[], name: string): CliCommand | undefined {
|
|
65
73
|
return cmds.find((c) => c.key === name);
|
|
@@ -184,6 +192,7 @@ function consumeOptions(
|
|
|
184
192
|
const tok = argv[idx];
|
|
185
193
|
|
|
186
194
|
if (isHelpTok(tok)) break;
|
|
195
|
+
if (isSchemaTok(tok)) break;
|
|
187
196
|
if (!tok.startsWith("-")) break;
|
|
188
197
|
|
|
189
198
|
if (tok === "--") {
|
|
@@ -334,6 +343,20 @@ function helpResult(p: string[], explicit: boolean): ParseResult {
|
|
|
334
343
|
};
|
|
335
344
|
}
|
|
336
345
|
|
|
346
|
+
/** Builds a schema-export result for the program root. */
|
|
347
|
+
function schemaResult(): ParseResult {
|
|
348
|
+
return {
|
|
349
|
+
kind: ParseKind.Schema,
|
|
350
|
+
path: [],
|
|
351
|
+
opts: {},
|
|
352
|
+
args: [],
|
|
353
|
+
helpExplicit: false,
|
|
354
|
+
helpPath: [],
|
|
355
|
+
errorMsg: "",
|
|
356
|
+
errorHelpPath: [],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
337
360
|
/**
|
|
338
361
|
* Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
|
|
339
362
|
*/
|
|
@@ -367,6 +390,10 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
367
390
|
return helpResult([], true);
|
|
368
391
|
}
|
|
369
392
|
|
|
393
|
+
if (i < argv.length && !forcePositionals && isSchemaTok(argv[i])) {
|
|
394
|
+
return schemaResult();
|
|
395
|
+
}
|
|
396
|
+
|
|
370
397
|
// Determine which subcommand to route to
|
|
371
398
|
let cmdName: string;
|
|
372
399
|
let node: CliCommand | undefined;
|
package/src/runtime.ts
CHANGED
|
@@ -7,10 +7,11 @@ It keeps execution flow out of the public barrel so the exported API stays small
|
|
|
7
7
|
the runtime responsibilities remain easy to reason about.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliBuiltinCompletionGroup, completionBashScript, completionZshScript } from "./completion.ts";
|
|
10
|
+
import { cliBuiltinCompletionGroup, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
11
|
import { CliContext } from "./context.ts";
|
|
12
12
|
import { cliHelpRender } from "./help.ts";
|
|
13
|
-
import { parse, postParseValidate } from "./parse.ts";
|
|
13
|
+
import { parse, postParseValidate, ParseKind } from "./parse.ts";
|
|
14
|
+
import { cliSchemaJson } from "./schema.ts";
|
|
14
15
|
import { CliCommand } from "./types.ts";
|
|
15
16
|
import { cliValidateRoot } from "./validate.ts";
|
|
16
17
|
|
|
@@ -21,9 +22,7 @@ function cliRootMergedWithBuiltins(root: CliCommand): CliCommand {
|
|
|
21
22
|
if (root.handler) {
|
|
22
23
|
return root;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
merged.commands = [...(root.commands ?? []), cliBuiltinCompletionGroup(root.key)];
|
|
26
|
-
return merged as CliCommand;
|
|
25
|
+
return cliPresentationRoot(root);
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
/**
|
|
@@ -63,16 +62,21 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
63
62
|
let pr = parse(parseRoot, argv);
|
|
64
63
|
pr = postParseValidate(parseRoot, pr);
|
|
65
64
|
|
|
66
|
-
if (pr.kind ===
|
|
67
|
-
process.stdout.write(cliHelpRender(
|
|
65
|
+
if (pr.kind === ParseKind.Help) {
|
|
66
|
+
process.stdout.write(cliHelpRender(cliPresentationRoot(root), pr.helpPath, false));
|
|
68
67
|
process.exit(pr.helpExplicit ? 0 : 1);
|
|
69
68
|
}
|
|
70
69
|
|
|
70
|
+
if (pr.kind === ParseKind.Schema) {
|
|
71
|
+
process.stdout.write(cliSchemaJson(root));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
if (pr.kind === "error") {
|
|
72
76
|
const color = process.stderr.isTTY;
|
|
73
77
|
const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
|
|
74
78
|
process.stderr.write(msg + "\n");
|
|
75
|
-
process.stderr.write(cliHelpRender(
|
|
79
|
+
process.stderr.write(cliHelpRender(cliPresentationRoot(root), pr.errorHelpPath, true));
|
|
76
80
|
process.exit(1);
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -127,6 +131,6 @@ export function cliErrWithHelp(ctx: CliContext, msg: string): never {
|
|
|
127
131
|
const color = process.stderr.isTTY;
|
|
128
132
|
const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
|
|
129
133
|
process.stderr.write(line + "\n");
|
|
130
|
-
process.stderr.write(cliHelpRender(ctx.schema, ctx.commandPath, true));
|
|
134
|
+
process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.schema), ctx.commandPath, true));
|
|
131
135
|
process.exit(1);
|
|
132
136
|
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module serializes the CLI schema tree to JSON for machine-readable introspection.
|
|
3
|
+
It strips handlers and runtime-only nodes so agents can discover commands, options,
|
|
4
|
+
and positionals in one shot.
|
|
5
|
+
|
|
6
|
+
It keeps schema export aligned with the declarative CliCommand model that drives help
|
|
7
|
+
and completion.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CliCommand,
|
|
12
|
+
CliFallbackMode,
|
|
13
|
+
CliOption,
|
|
14
|
+
CliPositional,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
import { cliBuiltinCompletionGroup } from "./completion.ts";
|
|
17
|
+
|
|
18
|
+
/** JSON-safe command node (no handlers). */
|
|
19
|
+
export interface CliSchemaExport {
|
|
20
|
+
/** Program or command key. */
|
|
21
|
+
key: string;
|
|
22
|
+
/** Short description shown in help. */
|
|
23
|
+
description: string;
|
|
24
|
+
/** Additional notes shown in help (supports {app} placeholder). */
|
|
25
|
+
notes?: string;
|
|
26
|
+
/** Global or command-level flags/options. */
|
|
27
|
+
options?: CliOption[];
|
|
28
|
+
/** Default top-level subcommand (program root only). */
|
|
29
|
+
fallbackCommand?: string;
|
|
30
|
+
/** How fallbackCommand is applied (program root only). */
|
|
31
|
+
fallbackMode?: CliFallbackMode;
|
|
32
|
+
/** Nested subcommands (routing nodes only). */
|
|
33
|
+
commands?: CliSchemaExport[];
|
|
34
|
+
/** Positional argument definitions (leaf nodes only). */
|
|
35
|
+
positionals?: CliPositional[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** JSON-safe export of the reserved `completion` subtree (no handler recursion). */
|
|
39
|
+
function exportBuiltinCompletionGroup(appName: string): CliSchemaExport {
|
|
40
|
+
const group = cliBuiltinCompletionGroup(appName);
|
|
41
|
+
return {
|
|
42
|
+
key: group.key,
|
|
43
|
+
description: group.description,
|
|
44
|
+
commands: (group.commands ?? []).map((ch) => ({
|
|
45
|
+
key: ch.key,
|
|
46
|
+
description: ch.description,
|
|
47
|
+
...((ch.notes ?? "").length > 0 ? { notes: ch.notes } : {}),
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Converts one `CliCommand` node into a JSON-safe export (handlers omitted). */
|
|
53
|
+
function exportCommand(cmd: CliCommand): CliSchemaExport {
|
|
54
|
+
const out: CliSchemaExport = {
|
|
55
|
+
key: cmd.key,
|
|
56
|
+
description: cmd.description,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if ((cmd.notes ?? "").length > 0) {
|
|
60
|
+
out.notes = cmd.notes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ((cmd.options ?? []).length > 0) {
|
|
64
|
+
out.options = cmd.options;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if ("handler" in cmd && cmd.handler) {
|
|
68
|
+
if ((cmd.positionals ?? []).length > 0) {
|
|
69
|
+
out.positionals = cmd.positionals;
|
|
70
|
+
}
|
|
71
|
+
out.commands = [exportBuiltinCompletionGroup(cmd.key)];
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (cmd.fallbackCommand !== undefined) {
|
|
76
|
+
out.fallbackCommand = cmd.fallbackCommand;
|
|
77
|
+
}
|
|
78
|
+
if (cmd.fallbackMode !== undefined) {
|
|
79
|
+
out.fallbackMode = cmd.fallbackMode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const children = (cmd.commands ?? []).filter((ch) => ch.key !== "completion");
|
|
83
|
+
if (children.length > 0) {
|
|
84
|
+
out.commands = children.map(exportCommand);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Returns pretty-printed JSON for the full program schema (trailing newline). */
|
|
91
|
+
export function cliSchemaJson(root: CliCommand): string {
|
|
92
|
+
return JSON.stringify(exportCommand(root), null, 2) + "\n";
|
|
93
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -69,6 +69,12 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
if (opt.name === "schema") {
|
|
73
|
+
throw new CliSchemaValidationError(
|
|
74
|
+
`Option name "schema" is reserved for --schema: ${cmd.key}/${opt.name}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
if (opt.shortName !== undefined) {
|
|
73
79
|
if (opt.shortName === "h") {
|
|
74
80
|
throw new CliSchemaValidationError(
|