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.
@@ -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.2.1...HEAD
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "//just": "echo this app uses justfile for development tasks"
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 === "help") {
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(