argsbarg 3.3.3 → 3.3.5

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 CHANGED
@@ -7,11 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.3.5] - 2026-06-21
11
+
12
+ ### Changed
13
+
14
+ - **`notes` placeholders** — use `{argsbarg:program}` for the root program key in consumer `notes`. Built-in copy (e.g. `install` notes) interpolates the program key directly.
15
+
16
+ ### Removed
17
+
18
+ - **`{app}` notes placeholder** — use `{argsbarg:program}` instead.
19
+
20
+ ### Fixed
21
+
22
+ - **`docs schema` / `docs api` / MCP schema resource** — `{argsbarg:program}` in `notes` is resolved to the program key (same as help). Schema export uses the root program key for built-in subtrees on nested leaves.
23
+
24
+ ## [3.3.4] - 2026-06-21
25
+
26
+
10
27
  ## [3.3.3] - 2026-06-21
11
28
 
12
29
  ### Added
13
30
 
14
- - **`docs --save`** — write bundled docs to `./docs/`; `docs all --save` writes one file per topic instead of combined stdout output. Argsbarg-generated markdown (`mcp`, `api`, `skill`) is prefixed with a `Generated by … docs … --save` HTML comment.
31
+ - **`docs --save`** — write one docs subcommand to `./docs/`; argsbarg-generated markdown (`mcp`, `api`, `skill`) is prefixed with a `Generated by … docs … --save` HTML comment.
32
+
33
+ ### Removed
34
+
35
+ - **`docs all`** — use individual subcommands (`docs readme`, `docs schema`, `docs api`, …) or `--save` per topic.
15
36
 
16
37
  ## [3.3.2] - 2026-06-21
17
38
 
@@ -255,7 +276,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
255
276
  - 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`).
256
277
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
257
278
 
258
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.3...HEAD
279
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.5...HEAD
280
+ [3.3.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.5
281
+ [3.3.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.4
259
282
  [3.3.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.3
260
283
  [3.3.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.2
261
284
  [3.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.1
@@ -30,10 +30,9 @@ myapp docs architecture
30
30
  myapp docs schema # full command tree as JSON
31
31
  myapp docs api # command tree as markdown
32
32
  myapp docs skill # generated Cursor SKILL.md
33
- myapp docs all # all user topics; includes auto mcp when MCP enabled
34
33
  myapp docs mcp # auto-generated when mcpServer.enabled
35
34
  myapp docs readme --save # write ./docs/readme.md
36
- myapp docs all --save # write ./docs/<topic>.md for each bundled topic
35
+ myapp docs schema --save # write ./docs/schema.json
37
36
  ```
38
37
 
39
38
  ## Configuration
@@ -45,7 +44,7 @@ myapp docs all --save # write ./docs/<topic>.md for each bundled topic
45
44
  | `defaultTopic` | first key in `topics` | `fallbackCommand` for bare `myapp docs` |
46
45
  | `topics` | *(required)* | Topic key → `{ text, description? }` |
47
46
 
48
- Reserved topic keys in `topics`: **`mcp`**, **`all`**, **`schema`**, **`api`**, **`skill`** (supplied by the built-in).
47
+ Reserved topic keys in `topics`: **`mcp`**, **`all`**, **`schema`**, **`api`**, **`skill`** (reserved use the matching `docs <name>` subcommand instead).
49
48
 
50
49
  When `description` is omitted on a topic, ArgsBarg generates leaf help (`readme` → "Print README (user guide).").
51
50
 
@@ -102,8 +101,5 @@ Pass **`--save`** on `docs` or any docs subcommand to write files under **`./doc
102
101
  | `docs schema --save` | `./docs/schema.json` |
103
102
  | `docs api --save` | `./docs/api.md` |
104
103
  | `docs skill --save` | `./docs/skill.md` |
105
- | `docs all --save` | one file per bundled topic (`readme.md`, `mcp.md`, …) — not a combined file |
106
104
 
107
- `docs all --save` uses the same topic set as stdout `docs all` (user topics plus auto `mcp` when enabled). It does not include `schema`, `api`, or `skill` unless you invoke those subcommands separately.
108
-
109
- Argsbarg-generated markdown (`mcp`, `api`, `skill`) is prefixed with an HTML comment: `<!-- Generated by myapp docs api --save; do not edit. -->`. Consumer topic files and `schema.json` are written as-is.
105
+ Argsbarg-generated markdown (`mcp`, `api`, `skill`) includes a `Generated by docs … --save` HTML comment (`skill` places it after YAML frontmatter so parsers still work). `schema.json` and argsbarg-generated markdown resolve `{argsbarg:program}` in `notes` to the program key. Consumer-authored topic files are written as-is.
@@ -79,7 +79,7 @@ const cli = {
79
79
  {
80
80
  key: "read",
81
81
  description: "Print the first line of each file.",
82
- notes: "Pass one or more file paths. {app} prints the first line of each.",
82
+ notes: "Pass one or more file paths. The program prints the first line of each.",
83
83
  positionals: [
84
84
  {
85
85
  name: "files",
package/index.d.ts CHANGED
@@ -224,7 +224,7 @@ export interface CliNodeBase {
224
224
  key: string;
225
225
  /** Short description shown in help. */
226
226
  description: string;
227
- /** Additional notes shown in help (supports {app} placeholder). */
227
+ /** Additional notes shown in help (`{argsbarg:program}` → program key). */
228
228
  notes?: string;
229
229
  /** Global or command-level flags/options. */
230
230
  options?: CliOption[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -85,19 +85,20 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
85
85
 
86
86
  /** Builds the `install` built-in command. */
87
87
  export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
88
+ const app = root.key;
88
89
  return {
89
90
  key: "install",
90
91
  description: "Install the binary, shell completions, agent skills, and MCP config to your user environment.",
91
92
  notes:
92
93
  "First-time setup:\n" +
93
- ` {app} install --all --yes\n\n` +
94
+ ` ${app} install --all --yes\n\n` +
94
95
  "Refresh after upgrading:\n" +
95
- ` {app} install --reinstall\n` +
96
- ` {app} update\n\n` +
96
+ ` ${app} install --reinstall\n` +
97
+ ` ${app} update\n\n` +
97
98
  "See what is installed:\n" +
98
- ` {app} install --status\n\n` +
99
+ ` ${app} install --status\n\n` +
99
100
  "Remove everything installed with --all:\n" +
100
- ` {app} install --uninstall --all --yes\n\n` +
101
+ ` ${app} install --uninstall --all --yes\n\n` +
101
102
  "Use --dry to preview changes, --json for machine-readable output.",
102
103
  options: installBuiltinOptions(root),
103
104
  handler: () => {},
@@ -53,3 +53,33 @@ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
53
53
  expect(md).toContain("`<path>`");
54
54
  expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
55
55
  });
56
+
57
+ test("generateApiGuide resolves program key in install notes", () => {
58
+ const fixture: CliProgram = {
59
+ key: "myapp",
60
+ version: "1.0.0",
61
+ description: "Demo app.",
62
+ commands: [{ key: "run", description: "Run.", handler: () => {} }],
63
+ };
64
+ const md = generateApiGuide(fixture);
65
+ expect(md).not.toContain("{argsbarg:program}");
66
+ expect(md).toContain("myapp install --all --yes");
67
+ });
68
+
69
+ test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
70
+ const fixture: CliProgram = {
71
+ key: "myapp",
72
+ version: "1.0.0",
73
+ description: "Demo app.",
74
+ commands: [
75
+ {
76
+ key: "run",
77
+ description: "Run.",
78
+ notes: "Invoke `{argsbarg:program} run`.",
79
+ handler: () => {},
80
+ },
81
+ ],
82
+ };
83
+ const md = generateApiGuide(fixture);
84
+ expect(md).toContain("Invoke `myapp run`.");
85
+ });
@@ -1,5 +1,5 @@
1
1
  import type { CliSchemaExport } from "../builtins/export.ts";
2
- import { cliPositionalLabel } from "../help.ts";
2
+ import { cliPositionalLabel, cliResolveNotes } from "../help.ts";
3
3
  import { cliSchemaExport } from "../schema.ts";
4
4
  import type { CliOption, CliPositional, CliProgram } from "../types.ts";
5
5
  import { CliFallbackMode, CliOptionKind } from "../types.ts";
@@ -43,6 +43,15 @@ function formatPositionalRow(p: CliPositional): string {
43
43
  return `| \`${label}\` | ${p.kind} | ${req} | ${p.description} |`;
44
44
  }
45
45
 
46
+ /** Markdown blockquote for command notes (`{argsbarg:program}` resolved to root key). */
47
+ function formatNotesBlockquote(notes: string, appKey: string): string {
48
+ const resolved = cliResolveNotes(notes, appKey);
49
+ return resolved
50
+ .split("\n")
51
+ .map((line) => `> ${line}`)
52
+ .join("\n");
53
+ }
54
+
46
55
  /** Fallback routing note when present on a router node. */
47
56
  function fallbackLine(node: CliSchemaExport): string | null {
48
57
  if (node.fallbackCommand === undefined) {
@@ -66,7 +75,7 @@ function renderCommandNode(
66
75
  lines.push(`${heading} \`${cmd}\``, "", node.description, "");
67
76
 
68
77
  if (node.notes) {
69
- lines.push(`> ${node.notes}`, "");
78
+ lines.push(formatNotesBlockquote(node.notes, rootKey), "");
70
79
  }
71
80
 
72
81
  const fb = fallbackLine(node);
@@ -121,7 +130,7 @@ export function generateApiGuide(program: CliProgram): string {
121
130
  ];
122
131
 
123
132
  if (schema.notes) {
124
- lines.push(`> ${schema.notes}`, "");
133
+ lines.push(formatNotesBlockquote(schema.notes, program.key), "");
125
134
  }
126
135
 
127
136
  renderCommandNode(program.key, [], schema, lines);
@@ -8,19 +8,17 @@ import {
8
8
  docsUserTopicKeys,
9
9
  printDocsTopic,
10
10
  } from "./resolve.ts";
11
- import { saveDocsTopics } from "./save.ts";
11
+ import { saveDocsTopic } from "./save.ts";
12
12
 
13
13
  const DOCS_SAVE_OPTION: CliOption = {
14
14
  name: "save",
15
- description: "Write documentation to ./docs/ (`docs all` writes one file per topic).",
15
+ description: "Write documentation to ./docs/.",
16
16
  kind: CliOptionKind.Presence,
17
17
  };
18
18
 
19
19
  function runDocsTopic(program: CliProgram, topic: string, ctx: { hasFlag(name: string): boolean }): void {
20
20
  if (ctx.hasFlag("save")) {
21
- for (const path of saveDocsTopics(program, topic)) {
22
- process.stdout.write(`${path}\n`);
23
- }
21
+ process.stdout.write(`${saveDocsTopic(program, topic)}\n`);
24
22
  return;
25
23
  }
26
24
  printDocsTopic(program, topic);
@@ -58,7 +56,6 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
58
56
  docsLeaf(program, "schema", "Print the full command tree as JSON."),
59
57
  docsLeaf(program, "api", "Print the command tree as markdown."),
60
58
  docsLeaf(program, "skill", "Print generated Cursor SKILL.md content."),
61
- docsLeaf(program, "all", "Print all bundled documentation combined."),
62
59
  );
63
60
 
64
61
  return {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test, beforeEach, afterEach } from "bun:test";
2
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { cliPresentationRoot } from "../builtins/presentation.ts";
@@ -7,9 +7,9 @@ import { completionBashScript } from "../completion.ts";
7
7
  import { cliInvoke } from "../index.ts";
8
8
  import type { CliProgram } from "../types.ts";
9
9
  import { cliValidateProgram } from "../validate.ts";
10
- import { combineAllDocs, docsEffectiveDefaultTopic } from "./resolve.ts";
10
+ import { docsEffectiveDefaultTopic } from "./resolve.ts";
11
11
  import { generateMcpGuide } from "./mcp-guide.ts";
12
- import { docsTopicsToSave, saveDocsTopics } from "./save.ts";
12
+ import { saveDocsTopic } from "./save.ts";
13
13
 
14
14
  let workDir: string;
15
15
  let prevCwd: string;
@@ -104,6 +104,11 @@ test("docs mcp when MCP enabled", async () => {
104
104
  expect(result.stdout).toContain("myapp mcp");
105
105
  });
106
106
 
107
+ test("docs rejects unknown subcommand", async () => {
108
+ const result = await cliInvoke(docsFixture(), ["docs", "all"]);
109
+ expect(result.exitCode).not.toBe(0);
110
+ });
111
+
107
112
  test("docs mcp absent from router when MCP disabled", async () => {
108
113
  const root = docsFixture(false);
109
114
  const presentation = cliPresentationRoot(root);
@@ -116,14 +121,6 @@ test("docs mcp absent from router when MCP disabled", async () => {
116
121
  expect(result.exitCode).not.toBe(0);
117
122
  });
118
123
 
119
- test("docs all concatenates user topics and mcp", () => {
120
- const program = docsFixture(true);
121
- const combined = combineAllDocs(program);
122
- expect(combined).toContain("Hello README");
123
- expect(combined).toContain("Architecture");
124
- expect(combined).toContain("MCP server (myapp)");
125
- });
126
-
127
124
  test("presentation includes docs subtree", () => {
128
125
  const presentation = cliPresentationRoot(docsFixture());
129
126
  const docsNode = presentation.commands.find((c) => c.key === "docs");
@@ -203,6 +200,16 @@ test("docs api --save prepends generated hint", async () => {
203
200
  expect(text).toContain("CLI API reference");
204
201
  });
205
202
 
203
+ test("docs skill --save keeps frontmatter first", async () => {
204
+ const result = await cliInvoke(docsFixture(), ["docs", "skill", "--save"]);
205
+ expect(result.exitCode).toBe(0);
206
+ const text = readFileSync(join(workDir, "docs/skill.md"), "utf8");
207
+ expect(text.startsWith("---\n")).toBe(true);
208
+ expect(text).toContain("name: myapp");
209
+ const hint = "<!-- Generated by myapp docs skill --save; do not edit. -->";
210
+ expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
211
+ });
212
+
206
213
  test("docs schema --save writes JSON file", async () => {
207
214
  const result = await cliInvoke(docsFixture(), ["docs", "schema", "--save"]);
208
215
  expect(result.exitCode).toBe(0);
@@ -213,21 +220,9 @@ test("docs schema --save writes JSON file", async () => {
213
220
  expect(schema.key).toBe("myapp");
214
221
  });
215
222
 
216
- test("docs all --save writes separate topic files", async () => {
217
- const program = docsFixture(true);
218
- const result = await cliInvoke(program, ["docs", "all", "--save"]);
219
- expect(result.exitCode).toBe(0);
220
- const lines = result.stdout.trim().split("\n");
221
- expect(lines).toEqual(docsTopicsToSave(program, "all").map((key) => `docs/${key}.md`));
222
- expect(readFileSync(join(workDir, "docs/readme.md"), "utf8")).not.toContain("Generated by");
223
- expect(readFileSync(join(workDir, "docs/mcp.md"), "utf8")).toContain(
224
- "<!-- Generated by myapp docs mcp --save; do not edit. -->",
225
- );
226
- });
227
-
228
- test("saveDocsTopics returns relative paths", () => {
229
- const paths = saveDocsTopics(docsFixture(), "api");
230
- expect(paths).toEqual(["docs/api.md"]);
223
+ test("saveDocsTopic returns relative path", () => {
224
+ const path = saveDocsTopic(docsFixture(), "api");
225
+ expect(path).toBe("docs/api.md");
231
226
  const text = readFileSync(join(workDir, "docs/api.md"), "utf8");
232
227
  expect(text).toContain("<!-- Generated by myapp docs api --save; do not edit. -->");
233
228
  expect(text).toContain("CLI API reference");
@@ -51,16 +51,6 @@ export function docsTopicDescription(key: string, custom?: string): string {
51
51
  return `Print ${label} documentation.`;
52
52
  }
53
53
 
54
- /** Ordered keys for `docs all` (user topics, then auto `mcp` when enabled). */
55
- export function docsPrintOrder(program: CliProgram): string[] {
56
- const docs = program.docs!;
57
- const order = docsUserTopicKeys(docs);
58
- if (docsIncludesMcpTopic(program)) {
59
- order.push("mcp");
60
- }
61
- return order;
62
- }
63
-
64
54
  /** Markdown body for one docs topic key. */
65
55
  export function docsTopicText(program: CliProgram, topic: string): string {
66
56
  const docs = program.docs!;
@@ -88,21 +78,11 @@ export function docsTopicContent(program: CliProgram, topic: string): string {
88
78
  if (topic === "skill") {
89
79
  return `${generateSkillBundle(program, "cursor").skillMd}\n`;
90
80
  }
91
- if (topic === "all") {
92
- return `${combineAllDocs(program)}\n`;
93
- }
94
81
  const text = docsTopicText(program, topic);
95
82
  return text.endsWith("\n") ? text : `${text}\n`;
96
83
  }
97
84
 
98
- /** All bundled docs concatenated with horizontal rules. */
99
- export function combineAllDocs(program: CliProgram): string {
100
- return docsPrintOrder(program)
101
- .map((key) => docsTopicText(program, key).trim())
102
- .join("\n\n---\n\n");
103
- }
104
-
105
- /** Writes one docs topic (or `all`) to stdout. */
85
+ /** Writes one docs topic to stdout. */
106
86
  export function printDocsTopic(program: CliProgram, topic: string): void {
107
87
  process.stdout.write(docsTopicContent(program, topic));
108
88
  }
package/src/docs/save.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { CliProgram } from "../types.ts";
4
- import { docsPrintOrder, docsTopicContent } from "./resolve.ts";
4
+ import { docsTopicContent } from "./resolve.ts";
5
5
 
6
6
  /** Relative output directory for `docs --save`. */
7
7
  export const DOCS_SAVE_DIR = "docs";
@@ -14,18 +14,31 @@ export function docsTopicIsGeneratedByArgsbarg(topic: string): boolean {
14
14
  return (DOCS_GENERATED_SAVE_TOPICS as readonly string[]).includes(topic);
15
15
  }
16
16
 
17
- /** HTML comment prepended to generated markdown saved with `--save`. */
17
+ /** HTML comment for generated markdown saved with `--save`. */
18
18
  export function docsSaveGeneratedHint(program: CliProgram, topic: string): string {
19
19
  return `<!-- Generated by ${program.key} docs ${topic} --save; do not edit. -->\n\n`;
20
20
  }
21
21
 
22
- /** File body for `--save` (hint on argsbarg-generated markdown only). */
23
- export function docsTopicContentForSave(program: CliProgram, topic: string): string {
24
- const content = docsTopicContent(program, topic);
22
+ const SKILL_FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n/;
23
+
24
+ /** Inserts save hint without breaking YAML frontmatter (`docs skill`). */
25
+ export function applySaveGeneratedHint(program: CliProgram, topic: string, content: string): string {
25
26
  if (!docsTopicIsGeneratedByArgsbarg(topic)) {
26
27
  return content;
27
28
  }
28
- return `${docsSaveGeneratedHint(program, topic)}${content}`;
29
+ const hint = docsSaveGeneratedHint(program, topic);
30
+ if (topic === "skill") {
31
+ const match = content.match(SKILL_FRONTMATTER_RE);
32
+ if (match) {
33
+ return `${match[0]}${hint}${content.slice(match[0].length)}`;
34
+ }
35
+ }
36
+ return `${hint}${content}`;
37
+ }
38
+
39
+ /** File body for `--save` (hint on argsbarg-generated markdown only). */
40
+ export function docsTopicContentForSave(program: CliProgram, topic: string): string {
41
+ return applySaveGeneratedHint(program, topic, docsTopicContent(program, topic));
29
42
  }
30
43
 
31
44
  /** Filename for a saved docs topic. */
@@ -36,25 +49,18 @@ export function docsSaveFilename(topic: string): string {
36
49
  return `${topic}.md`;
37
50
  }
38
51
 
39
- /** Topic keys to write for one `docs` invocation (`all` expands to bundled topics). */
40
- export function docsTopicsToSave(program: CliProgram, topic: string): string[] {
41
- if (topic === "all") {
42
- return docsPrintOrder(program);
43
- }
44
- return [topic];
52
+ /** Relative path under cwd for a saved docs topic. */
53
+ export function docsSaveRelativePath(topic: string): string {
54
+ return join(DOCS_SAVE_DIR, docsSaveFilename(topic));
45
55
  }
46
56
 
47
- /** Writes docs topic(s) under `./docs/`; returns relative paths written. */
48
- export function saveDocsTopics(program: CliProgram, topic: string): string[] {
57
+ /** Writes one docs topic under `./docs/`; returns relative path written. */
58
+ export function saveDocsTopic(program: CliProgram, topic: string): string {
49
59
  const dir = join(process.cwd(), DOCS_SAVE_DIR);
50
60
  mkdirSync(dir, { recursive: true });
51
61
 
52
- const saved: string[] = [];
53
- for (const key of docsTopicsToSave(program, topic)) {
54
- const rel = join(DOCS_SAVE_DIR, docsSaveFilename(key));
55
- const abs = join(process.cwd(), rel);
56
- writeFileSync(abs, docsTopicContentForSave(program, key), "utf8");
57
- saved.push(rel);
58
- }
59
- return saved;
62
+ const rel = docsSaveRelativePath(topic);
63
+ const abs = join(process.cwd(), rel);
64
+ writeFileSync(abs, docsTopicContentForSave(program, topic), "utf8");
65
+ return rel;
60
66
  }
package/src/help.ts CHANGED
@@ -185,6 +185,14 @@ export function cliOptionLabel(o: CliOption, color: boolean): string {
185
185
  return style.aquaBold(left) + " " + style.greenBright(right);
186
186
  }
187
187
 
188
+ /** Placeholder in `notes` for the root program key (resolved in help, schema, and docs api). */
189
+ export const CLI_NOTES_PROGRAM = "{argsbarg:program}";
190
+
191
+ /** Replaces `{argsbarg:program}` in notes/help text with the program key. */
192
+ export function cliResolveNotes(notes: string, appKey: string): string {
193
+ return notes.replaceAll(CLI_NOTES_PROGRAM, appKey);
194
+ }
195
+
188
196
  /** Formats a positional slot label (`<n>`, `[n]`, or varargs) for help. */
189
197
  export function cliPositionalLabel(p: CliPositional, color: boolean): string {
190
198
  const { argMin = 1, argMax = 1 } = p;
@@ -471,12 +479,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
471
479
  }
472
480
 
473
481
  if ((node.notes ?? "").length > 0) {
474
- let resolved = node.notes!;
475
- while (true) {
476
- const r = resolved.indexOf("{app}");
477
- if (r === -1) break;
478
- resolved = resolved.slice(0, r) + schema.key + resolved.slice(r + 5);
479
- }
482
+ const resolved = cliResolveNotes(node.notes!, schema.key);
480
483
  lines.push("");
481
484
  lines.push(
482
485
  renderTextBox("Notes", wrapText(resolved, hw - 4), hw, color).join("\n"),
package/src/index.test.ts CHANGED
@@ -597,6 +597,44 @@ test("cliSchemaJson omits handlers and completion built-ins", () => {
597
597
  expect(schema).not.toHaveProperty("handler");
598
598
  });
599
599
 
600
+ test("cliSchemaExport resolves program key in install notes", () => {
601
+ const root = testProgram({
602
+ key: "myapp",
603
+ version: "1.0.0",
604
+ description: "demo",
605
+ commands: [
606
+ {
607
+ key: "run",
608
+ description: "run",
609
+ handler: () => {},
610
+ },
611
+ ],
612
+ });
613
+
614
+ const json = cliSchemaJson(root);
615
+ expect(json).not.toContain("{argsbarg:program}");
616
+ expect(json).toContain("myapp install --all --yes");
617
+ });
618
+
619
+ test("cliSchemaExport resolves {argsbarg:program} in consumer notes", () => {
620
+ const root = testProgram({
621
+ key: "myapp",
622
+ version: "1.0.0",
623
+ description: "demo",
624
+ commands: [
625
+ {
626
+ key: "run",
627
+ description: "run",
628
+ notes: "Run `{argsbarg:program} run` to start.",
629
+ handler: () => {},
630
+ },
631
+ ],
632
+ });
633
+
634
+ const schema = JSON.parse(cliSchemaJson(root));
635
+ expect(schema.commands[0].notes).toBe("Run `myapp run` to start.");
636
+ });
637
+
600
638
  test("docs help lists schema, api, and skill subcommands", () => {
601
639
  const root = testProgram({
602
640
  key: "app",
package/src/schema.ts CHANGED
@@ -4,10 +4,11 @@ This module serializes the CLI schema tree to JSON for machine-readable introspe
4
4
 
5
5
  import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
6
6
  import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
7
+ import { cliResolveNotes } from "./help.ts";
7
8
 
8
9
  const RESERVED = new Set(["completion", "install", "docs", "mcp", "version", "update"]);
9
10
 
10
- function exportCommand(cmd: CliNode): CliSchemaExport {
11
+ function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport {
11
12
  const out: CliSchemaExport = {
12
13
  key: cmd.key,
13
14
  description: cmd.description,
@@ -25,7 +26,7 @@ function exportCommand(cmd: CliNode): CliSchemaExport {
25
26
  if ((cmd.positionals ?? []).length > 0) {
26
27
  out.positionals = cmd.positionals;
27
28
  }
28
- out.commands = exportPresentationBuiltins(cmd as CliProgram);
29
+ out.commands = exportPresentationBuiltins(root);
29
30
  return out;
30
31
  }
31
32
 
@@ -38,15 +39,27 @@ function exportCommand(cmd: CliNode): CliSchemaExport {
38
39
 
39
40
  const children = isCliRouter(cmd) ? cmd.commands.filter((ch) => !RESERVED.has(ch.key)) : [];
40
41
  if (children.length > 0) {
41
- out.commands = children.map(exportCommand);
42
+ out.commands = children.map((ch) => exportCommand(ch, root));
42
43
  }
43
44
 
44
45
  return out;
45
46
  }
46
47
 
48
+ /** Resolves `{argsbarg:program}` in exported notes using the root program key. */
49
+ function resolveSchemaNotes(node: CliSchemaExport, appKey: string): CliSchemaExport {
50
+ const out: CliSchemaExport = { ...node };
51
+ if ((out.notes ?? "").length > 0) {
52
+ out.notes = cliResolveNotes(out.notes!, appKey);
53
+ }
54
+ if (out.commands) {
55
+ out.commands = out.commands.map((ch) => resolveSchemaNotes(ch, appKey));
56
+ }
57
+ return out;
58
+ }
59
+
47
60
  /** Returns the JSON-safe command tree (handlers omitted). */
48
61
  export function cliSchemaExport(root: CliProgram): CliSchemaExport {
49
- return exportCommand(root);
62
+ return resolveSchemaNotes(exportCommand(root, root), root.key);
50
63
  }
51
64
 
52
65
  export function cliSchemaJson(root: CliProgram): string {
package/src/types.ts CHANGED
@@ -213,7 +213,7 @@ export interface CliNodeBase {
213
213
  key: string;
214
214
  /** Short description shown in help. */
215
215
  description: string;
216
- /** Additional notes shown in help (supports {app} placeholder). */
216
+ /** Additional notes shown in help (`{argsbarg:program}` → program key). */
217
217
  notes?: string;
218
218
  /** Global or command-level flags/options. */
219
219
  options?: CliOption[];