argsbarg 1.3.0 → 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.
@@ -1 +1 @@
1
- - [ ] --schema feature for ai agents
1
+ - [x] --schema feature for ai agents
package/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ 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
+
10
17
  ## [1.3.0] - 2026-06-18
11
18
 
12
19
  ### Added
@@ -77,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
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`).
78
85
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
79
86
 
80
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.3.0...HEAD
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
81
89
  [1.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.3.0
82
90
  [1.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.1
83
91
  [1.2.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "//just": "echo this app uses justfile for development tasks"
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
- o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " ]]; then\n";
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
- o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " ]]; then\n";
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 a synthetic `--help, -h` row. */
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,6 +8,7 @@ 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";
13
14
  import { cliSchemaJson } from "./schema.ts";
@@ -501,6 +502,14 @@ test("--schema exports JSON for leaf roots", async () => {
501
502
  expect(schema.key).toBe("minimal.ts");
502
503
  expect(schema.positionals[0].name).toBe("name");
503
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.");
504
513
  });
505
514
 
506
515
  test("parse recognizes --schema at the program root", () => {
@@ -570,4 +579,64 @@ test("reserved option name schema is rejected", () => {
570
579
  ],
571
580
  };
572
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.'");
573
642
  });
package/src/runtime.ts CHANGED
@@ -7,7 +7,7 @@ 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
13
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
@@ -22,9 +22,7 @@ function cliRootMergedWithBuiltins(root: CliCommand): CliCommand {
22
22
  if (root.handler) {
23
23
  return root;
24
24
  }
25
- const merged = { ...root } as any;
26
- merged.commands = [...(root.commands ?? []), cliBuiltinCompletionGroup(root.key)];
27
- return merged as CliCommand;
25
+ return cliPresentationRoot(root);
28
26
  }
29
27
 
30
28
  /**
@@ -65,7 +63,7 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
65
63
  pr = postParseValidate(parseRoot, pr);
66
64
 
67
65
  if (pr.kind === ParseKind.Help) {
68
- process.stdout.write(cliHelpRender(parseRoot, pr.helpPath, false));
66
+ process.stdout.write(cliHelpRender(cliPresentationRoot(root), pr.helpPath, false));
69
67
  process.exit(pr.helpExplicit ? 0 : 1);
70
68
  }
71
69
 
@@ -78,7 +76,7 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
78
76
  const color = process.stderr.isTTY;
79
77
  const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
80
78
  process.stderr.write(msg + "\n");
81
- process.stderr.write(cliHelpRender(parseRoot, pr.errorHelpPath, true));
79
+ process.stderr.write(cliHelpRender(cliPresentationRoot(root), pr.errorHelpPath, true));
82
80
  process.exit(1);
83
81
  }
84
82
 
@@ -133,6 +131,6 @@ export function cliErrWithHelp(ctx: CliContext, msg: string): never {
133
131
  const color = process.stderr.isTTY;
134
132
  const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
135
133
  process.stderr.write(line + "\n");
136
- process.stderr.write(cliHelpRender(ctx.schema, ctx.commandPath, true));
134
+ process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.schema), ctx.commandPath, true));
137
135
  process.exit(1);
138
136
  }
package/src/schema.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  CliOption,
14
14
  CliPositional,
15
15
  } from "./types.ts";
16
+ import { cliBuiltinCompletionGroup } from "./completion.ts";
16
17
 
17
18
  /** JSON-safe command node (no handlers). */
18
19
  export interface CliSchemaExport {
@@ -34,6 +35,20 @@ export interface CliSchemaExport {
34
35
  positionals?: CliPositional[];
35
36
  }
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
+
37
52
  /** Converts one `CliCommand` node into a JSON-safe export (handlers omitted). */
38
53
  function exportCommand(cmd: CliCommand): CliSchemaExport {
39
54
  const out: CliSchemaExport = {
@@ -53,6 +68,7 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
53
68
  if ((cmd.positionals ?? []).length > 0) {
54
69
  out.positionals = cmd.positionals;
55
70
  }
71
+ out.commands = [exportBuiltinCompletionGroup(cmd.key)];
56
72
  return out;
57
73
  }
58
74