argsbarg 1.2.1 → 1.3.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/.private/scratch.md +1 -0
- package/CHANGELOG.md +8 -1
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/index.test.ts +96 -0
- package/src/parse.ts +27 -0
- package/src/runtime.ts +8 -2
- package/src/schema.ts +77 -0
- package/src/validate.ts +6 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- [ ] --schema feature for ai agents
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-06-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--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.
|
|
15
|
+
|
|
10
16
|
## [1.2.1] - 2026-06-18
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -71,7 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
71
77
|
- 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
78
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
73
79
|
|
|
74
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.
|
|
80
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.3.0...HEAD
|
|
81
|
+
[1.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.3.0
|
|
75
82
|
[1.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.1
|
|
76
83
|
[1.2.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.0
|
|
77
84
|
[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/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ shell output regressions.
|
|
|
10
10
|
import { completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
11
|
import { CliCommand, CliFallbackMode, CliOptionKind } from "./index.ts";
|
|
12
12
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
13
|
+
import { cliSchemaJson } from "./schema.ts";
|
|
13
14
|
import { cliValidateRoot } from "./validate.ts";
|
|
14
15
|
import { expect, test } from "bun:test";
|
|
15
16
|
import { $ } from "bun";
|
|
@@ -474,4 +475,99 @@ test("leaf completion help prints correctly", async () => {
|
|
|
474
475
|
expect(out).toContain("Show help for this command.");
|
|
475
476
|
expect(out).toContain("Output is the whole script.");
|
|
476
477
|
expect(stderr.toString()).toBe("");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("--schema exports JSON for nested CLIs", async () => {
|
|
481
|
+
const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts --schema`.nothrow().quiet();
|
|
482
|
+
expect(exitCode).toBe(0);
|
|
483
|
+
expect(stderr.toString()).toBe("");
|
|
484
|
+
|
|
485
|
+
const schema = JSON.parse(stdout.toString());
|
|
486
|
+
expect(schema.key).toBe("nested.ts");
|
|
487
|
+
expect(schema.fallbackCommand).toBe("read");
|
|
488
|
+
expect(schema.commands.map((c: { key: string }) => c.key)).toEqual(["stat", "read"]);
|
|
489
|
+
expect(schema.commands).not.toContainEqual(expect.objectContaining({ key: "completion" }));
|
|
490
|
+
|
|
491
|
+
const lookup = schema.commands[0].commands[0].commands[0];
|
|
492
|
+
expect(lookup.key).toBe("lookup");
|
|
493
|
+
expect(lookup.positionals[0].name).toBe("path");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("--schema exports JSON for leaf roots", async () => {
|
|
497
|
+
const { stdout, exitCode } = await $`bun run examples/minimal.ts --schema`.nothrow().quiet();
|
|
498
|
+
expect(exitCode).toBe(0);
|
|
499
|
+
|
|
500
|
+
const schema = JSON.parse(stdout.toString());
|
|
501
|
+
expect(schema.key).toBe("minimal.ts");
|
|
502
|
+
expect(schema.positionals[0].name).toBe("name");
|
|
503
|
+
expect(schema.options[0].name).toBe("verbose");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("parse recognizes --schema at the program root", () => {
|
|
507
|
+
const root: CliCommand = {
|
|
508
|
+
key: "app",
|
|
509
|
+
description: "demo",
|
|
510
|
+
commands: [
|
|
511
|
+
{
|
|
512
|
+
key: "x",
|
|
513
|
+
description: "cmd",
|
|
514
|
+
handler: () => {},
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
};
|
|
518
|
+
cliValidateRoot(root);
|
|
519
|
+
const pr = parse(root, ["--schema"]);
|
|
520
|
+
expect(pr.kind).toBe(ParseKind.Schema);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("cliSchemaJson omits handlers and completion built-ins", () => {
|
|
524
|
+
const root: CliCommand = {
|
|
525
|
+
key: "app",
|
|
526
|
+
description: "demo",
|
|
527
|
+
commands: [
|
|
528
|
+
{
|
|
529
|
+
key: "x",
|
|
530
|
+
description: "cmd",
|
|
531
|
+
handler: () => {},
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
key: "completion",
|
|
535
|
+
description: "should not appear",
|
|
536
|
+
commands: [
|
|
537
|
+
{
|
|
538
|
+
key: "bash",
|
|
539
|
+
description: "",
|
|
540
|
+
handler: () => {},
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const schema = JSON.parse(cliSchemaJson(root));
|
|
548
|
+
expect(schema.commands).toHaveLength(1);
|
|
549
|
+
expect(schema.commands[0].key).toBe("x");
|
|
550
|
+
expect(schema).not.toHaveProperty("handler");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("reserved option name schema is rejected", () => {
|
|
554
|
+
const root: CliCommand = {
|
|
555
|
+
key: "app",
|
|
556
|
+
description: "",
|
|
557
|
+
commands: [
|
|
558
|
+
{
|
|
559
|
+
key: "x",
|
|
560
|
+
description: "cmd",
|
|
561
|
+
options: [
|
|
562
|
+
{
|
|
563
|
+
name: "schema",
|
|
564
|
+
description: "",
|
|
565
|
+
kind: CliOptionKind.String,
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
handler: () => {},
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
};
|
|
572
|
+
expect(() => cliValidateRoot(root)).toThrow(/reserved for --schema/);
|
|
477
573
|
});
|
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
|
@@ -10,7 +10,8 @@ the runtime responsibilities remain easy to reason about.
|
|
|
10
10
|
import { cliBuiltinCompletionGroup, 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
|
|
|
@@ -63,11 +64,16 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
63
64
|
let pr = parse(parseRoot, argv);
|
|
64
65
|
pr = postParseValidate(parseRoot, pr);
|
|
65
66
|
|
|
66
|
-
if (pr.kind ===
|
|
67
|
+
if (pr.kind === ParseKind.Help) {
|
|
67
68
|
process.stdout.write(cliHelpRender(parseRoot, pr.helpPath, false));
|
|
68
69
|
process.exit(pr.helpExplicit ? 0 : 1);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
if (pr.kind === ParseKind.Schema) {
|
|
73
|
+
process.stdout.write(cliSchemaJson(root));
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
if (pr.kind === "error") {
|
|
72
78
|
const color = process.stderr.isTTY;
|
|
73
79
|
const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
|
|
17
|
+
/** JSON-safe command node (no handlers). */
|
|
18
|
+
export interface CliSchemaExport {
|
|
19
|
+
/** Program or command key. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Short description shown in help. */
|
|
22
|
+
description: string;
|
|
23
|
+
/** Additional notes shown in help (supports {app} placeholder). */
|
|
24
|
+
notes?: string;
|
|
25
|
+
/** Global or command-level flags/options. */
|
|
26
|
+
options?: CliOption[];
|
|
27
|
+
/** Default top-level subcommand (program root only). */
|
|
28
|
+
fallbackCommand?: string;
|
|
29
|
+
/** How fallbackCommand is applied (program root only). */
|
|
30
|
+
fallbackMode?: CliFallbackMode;
|
|
31
|
+
/** Nested subcommands (routing nodes only). */
|
|
32
|
+
commands?: CliSchemaExport[];
|
|
33
|
+
/** Positional argument definitions (leaf nodes only). */
|
|
34
|
+
positionals?: CliPositional[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Converts one `CliCommand` node into a JSON-safe export (handlers omitted). */
|
|
38
|
+
function exportCommand(cmd: CliCommand): CliSchemaExport {
|
|
39
|
+
const out: CliSchemaExport = {
|
|
40
|
+
key: cmd.key,
|
|
41
|
+
description: cmd.description,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if ((cmd.notes ?? "").length > 0) {
|
|
45
|
+
out.notes = cmd.notes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ((cmd.options ?? []).length > 0) {
|
|
49
|
+
out.options = cmd.options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ("handler" in cmd && cmd.handler) {
|
|
53
|
+
if ((cmd.positionals ?? []).length > 0) {
|
|
54
|
+
out.positionals = cmd.positionals;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (cmd.fallbackCommand !== undefined) {
|
|
60
|
+
out.fallbackCommand = cmd.fallbackCommand;
|
|
61
|
+
}
|
|
62
|
+
if (cmd.fallbackMode !== undefined) {
|
|
63
|
+
out.fallbackMode = cmd.fallbackMode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const children = (cmd.commands ?? []).filter((ch) => ch.key !== "completion");
|
|
67
|
+
if (children.length > 0) {
|
|
68
|
+
out.commands = children.map(exportCommand);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Returns pretty-printed JSON for the full program schema (trailing newline). */
|
|
75
|
+
export function cliSchemaJson(root: CliCommand): string {
|
|
76
|
+
return JSON.stringify(exportCommand(root), null, 2) + "\n";
|
|
77
|
+
}
|
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(
|