argsbarg 3.1.0 → 3.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +4 -6
  3. package/docs/ai-skills.md +1 -1
  4. package/docs/bundled-docs.md +18 -4
  5. package/docs/install.md +51 -4
  6. package/docs/mcp.md +4 -6
  7. package/examples/minimal.ts +6 -0
  8. package/examples/nested.ts +3 -0
  9. package/index.d.ts +93 -1
  10. package/package.json +1 -1
  11. package/plan.md +10 -185
  12. package/src/builtins/completion-bash.ts +1 -8
  13. package/src/builtins/completion-fish.ts +0 -5
  14. package/src/builtins/completion-zsh.ts +1 -9
  15. package/src/builtins/dispatch.ts +27 -0
  16. package/src/builtins/export.ts +4 -0
  17. package/src/builtins/install.ts +9 -3
  18. package/src/builtins/presentation.ts +4 -0
  19. package/src/builtins/shell-helpers.ts +0 -2
  20. package/src/builtins/update.ts +14 -0
  21. package/src/capabilities.ts +7 -1
  22. package/src/docs/api-guide.test.ts +55 -0
  23. package/src/docs/api-guide.ts +129 -0
  24. package/src/docs/builtin.ts +3 -0
  25. package/src/docs/docs.test.ts +47 -1
  26. package/src/docs/mcp-guide.ts +3 -3
  27. package/src/docs/resolve.ts +32 -1
  28. package/src/headless.test.ts +86 -0
  29. package/src/headless.ts +86 -0
  30. package/src/help.ts +3 -7
  31. package/src/index.test.ts +36 -65
  32. package/src/index.ts +20 -0
  33. package/src/install/binary.ts +8 -3
  34. package/src/install/gh-release-update.test.ts +22 -0
  35. package/src/install/gh-release-update.ts +229 -0
  36. package/src/install/index.ts +55 -30
  37. package/src/install/plan.ts +5 -3
  38. package/src/install/update.test.ts +106 -0
  39. package/src/install/update.ts +55 -0
  40. package/src/invoke.ts +1 -11
  41. package/src/mcp/tools.ts +1 -1
  42. package/src/parse.ts +0 -27
  43. package/src/runtime.ts +7 -6
  44. package/src/schema.ts +7 -2
  45. package/src/skill/generate.ts +2 -2
  46. package/src/types.ts +17 -0
  47. package/src/validate.ts +11 -6
@@ -5,7 +5,6 @@ import {
5
5
  identToken,
6
6
  kHelpLong,
7
7
  kHelpShort,
8
- kSchemaLong,
9
8
  mainName,
10
9
  } from "./shell-helpers.ts";
11
10
 
@@ -17,9 +16,6 @@ function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
17
16
  o += " " + i + ")\n";
18
17
  o += " case $w in\n";
19
18
  o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
20
- if (sc.path === "") {
21
- o += " " + kSchemaLong + ") echo 1 ;;\n";
22
- }
23
19
  for (const op of sc.opts) {
24
20
  const base = "--" + op.name;
25
21
  if (op.kind === "presence") {
@@ -107,7 +103,7 @@ function emitSimulate(ident: string): string {
107
103
  o += " local i=1 sid=0 w steps next\n";
108
104
  o += " while (( i < COMP_CWORD )); do\n";
109
105
  o += " w=\"${COMP_WORDS[i]}\"\n";
110
- o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
106
+ o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " ]]; then\n";
111
107
  o += " ((i++)); continue\n";
112
108
  o += " fi\n";
113
109
  o += " if [[ $w == --* ]]; then\n";
@@ -209,9 +205,6 @@ export function completionBashScript(schema: CliRouter): string {
209
205
  for (const [i, sc] of scopes.entries()) {
210
206
  out += "A_" + ident + "_" + i + "_opts=()\n";
211
207
  out += "A_" + ident + "_" + i + "_opts+=('" + kHelpLong + "' '" + kHelpShort + "')\n";
212
- if (sc.path === "") {
213
- out += "A_" + ident + "_" + i + "_opts+=('" + kSchemaLong + "')\n";
214
- }
215
208
  for (const o of sc.opts) {
216
209
  out += "A_" + ident + "_" + i + "_opts+=('--" + o.name + "')\n";
217
210
  if (o.shortName) {
@@ -5,8 +5,6 @@ import {
5
5
  identToken,
6
6
  kHelpLong,
7
7
  kHelpShort,
8
- kSchemaDesc,
9
- kSchemaLong,
10
8
  } from "./shell-helpers.ts";
11
9
 
12
10
  function scopeCondition(ident: string, scopeIndex: number, path: string): string {
@@ -43,9 +41,6 @@ export function completionFishScript(schema: CliRouter): string {
43
41
  }
44
42
 
45
43
  out += `complete -c ${app} -n '${cond}' -s h -l help -d '${escFishSingleQuoted("Show help for this command.")}'\n`;
46
- if (sc.path === "") {
47
- out += `complete -c ${app} -n '${cond}' -l schema -d '${escFishSingleQuoted(kSchemaDesc)}'\n`;
48
- }
49
44
 
50
45
  for (const op of sc.opts) {
51
46
  if (op.kind === CliOptionKind.Presence) {
@@ -5,8 +5,6 @@ import {
5
5
  identToken,
6
6
  kHelpLong,
7
7
  kHelpShort,
8
- kSchemaDesc,
9
- kSchemaLong,
10
8
  mainName,
11
9
  } from "./shell-helpers.ts";
12
10
 
@@ -16,9 +14,6 @@ function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
16
14
  out += "typeset -g A_" + ident + "_" + i + "_opts\n";
17
15
  out += "A_" + ident + "_" + i + "_opts=(";
18
16
  out += "'" + escShellSingleQuoted(kHelpLong) + ":" + escShellSingleQuoted("Show help for this command.") + "' '" + escShellSingleQuoted(kHelpShort) + ":" + escShellSingleQuoted("Show help for this command.") + "'";
19
- if (sc.path === "") {
20
- out += " '" + escShellSingleQuoted(kSchemaLong) + ":" + escShellSingleQuoted(kSchemaDesc) + "'";
21
- }
22
17
  for (const o of sc.opts) {
23
18
  out += " '" + escShellSingleQuoted("--" + o.name) + ":" + escShellSingleQuoted(o.description) + "'";
24
19
  if (o.shortName) {
@@ -47,9 +42,6 @@ function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
47
42
  o += " " + i + ")\n";
48
43
  o += " case $w in\n";
49
44
  o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
50
- if (sc.path === "") {
51
- o += " " + kSchemaLong + ") echo 1 ;;\n";
52
- }
53
45
  for (const op of sc.opts) {
54
46
  const base = "--" + op.name;
55
47
  if (op.kind === "presence") {
@@ -137,7 +129,7 @@ function emitSimulateZsh(ident: string): string {
137
129
  o += " local i=2 sid=0 w steps next\n";
138
130
  o += " while (( i < CURRENT )); do\n";
139
131
  o += " w=$words[i]\n";
140
- o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
132
+ o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " ]]; then\n";
141
133
  o += " ((i++)); continue\n";
142
134
  o += " fi\n";
143
135
  o += " if [[ $w == --* ]]; then\n";
@@ -6,12 +6,14 @@ import { completionFishScript } from "./completion-fish.ts";
6
6
  import { completionZshScript } from "./completion-zsh.ts";
7
7
  import { cliBuiltinInstallCommand } from "./install.ts";
8
8
  import { cliBuiltinMcpCommand } from "./mcp.ts";
9
+ import { cliBuiltinUpdateCommand } from "./update.ts";
9
10
  import { cliBuiltinVersionCommand } from "./version.ts";
10
11
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
11
12
  import { cliPresentationRoot } from "./presentation.ts";
12
13
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
13
14
  import { cliMcpServeStdio } from "../mcp.ts";
14
15
  import { cliInstall } from "../install/index.ts";
16
+ import { cliUpdate } from "../install/update.ts";
15
17
  import type { ParseResult } from "../parse.ts";
16
18
  import { ParseKind } from "../parse.ts";
17
19
 
@@ -80,6 +82,20 @@ export async function dispatchBuiltin(
80
82
  process.exit(0);
81
83
  }
82
84
 
85
+ if (pr.path[0] === "update") {
86
+ if (!caps.update) {
87
+ process.stderr.write(
88
+ "update is not enabled. Set install.updateGetLatest on the program root.\n",
89
+ );
90
+ process.exit(1);
91
+ }
92
+ if (pr.path.length !== 1) {
93
+ process.stderr.write("Unknown subcommand: update " + pr.path.slice(1).join(" ") + "\n");
94
+ process.exit(1);
95
+ }
96
+ await cliUpdate(program);
97
+ }
98
+
83
99
  if (pr.path[0] === "install") {
84
100
  if (!caps.install) {
85
101
  process.stderr.write("install is disabled. Remove install.enabled: false from the program root.\n");
@@ -127,6 +143,17 @@ export function builtinInterceptRoot(
127
143
  };
128
144
  }
129
145
 
146
+ if (first === "update" && caps.update) {
147
+ return {
148
+ parseRoot: {
149
+ key: program.key,
150
+ description: program.description,
151
+ commands: [cliBuiltinUpdateCommand(program)],
152
+ },
153
+ isLeafCompletionIntercept: false,
154
+ };
155
+ }
156
+
130
157
  if (first === "mcp" && caps.mcp) {
131
158
  return {
132
159
  parseRoot: {
@@ -3,6 +3,7 @@ import type { CliFallbackMode, CliOption, CliPositional, CliProgram } from "../t
3
3
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
4
4
  import { cliBuiltinInstallCommand } from "./install.ts";
5
5
  import { cliBuiltinMcpCommand } from "./mcp.ts";
6
+ import { cliBuiltinUpdateCommand } from "./update.ts";
6
7
  import { cliBuiltinVersionCommand } from "./version.ts";
7
8
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
8
9
 
@@ -51,6 +52,9 @@ export function exportPresentationBuiltins(program: CliProgram, caps?: CliCapabi
51
52
  if (resolved.install) {
52
53
  builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(program)));
53
54
  }
55
+ if (resolved.update) {
56
+ builtins.push(exportBuiltinNode(cliBuiltinUpdateCommand(program)));
57
+ }
54
58
  const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
55
59
  if (docsGroup) {
56
60
  builtins.push(exportBuiltinNode(docsGroup));
@@ -26,10 +26,15 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
26
26
  kind: CliOptionKind.Presence,
27
27
  },
28
28
  {
29
- name: "update",
30
- description: "Update only artifacts already installed (always includes the binary).",
29
+ name: "reinstall",
30
+ description: "Reinstall artifacts already on disk (always includes the binary).",
31
31
  kind: CliOptionKind.Presence,
32
32
  },
33
+ {
34
+ name: "from",
35
+ description: "Binary to copy (default: running executable). Used with --reinstall.",
36
+ kind: CliOptionKind.String,
37
+ },
33
38
  {
34
39
  name: "status",
35
40
  description: "Print what is currently installed (read-only).",
@@ -87,7 +92,8 @@ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
87
92
  "First-time setup:\n" +
88
93
  ` {app} install --all --yes\n\n` +
89
94
  "Refresh after upgrading:\n" +
90
- ` {app} install --update\n\n` +
95
+ ` {app} install --reinstall\n` +
96
+ ` {app} update\n\n` +
91
97
  "See what is installed:\n" +
92
98
  ` {app} install --status\n\n` +
93
99
  "Remove everything:\n" +
@@ -5,6 +5,7 @@ import { isCliLeaf, isCliRouter } from "../types.ts";
5
5
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
6
6
  import { cliBuiltinInstallCommand } from "./install.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
+ import { cliBuiltinUpdateCommand } from "./update.ts";
8
9
  import { cliBuiltinVersionCommand } from "./version.ts";
9
10
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
10
11
 
@@ -17,6 +18,9 @@ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities)
17
18
  if (caps.install) {
18
19
  builtins.push(cliBuiltinInstallCommand(program));
19
20
  }
21
+ if (caps.update) {
22
+ builtins.push(cliBuiltinUpdateCommand(program));
23
+ }
20
24
  const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
21
25
  if (docsGroup) {
22
26
  builtins.push(docsGroup);
@@ -20,5 +20,3 @@ export function mainName(schemaName: string): string {
20
20
 
21
21
  export const kHelpLong = "--help";
22
22
  export const kHelpShort = "-h";
23
- export const kSchemaLong = "--schema";
24
- export const kSchemaDesc = "Print the full command tree as JSON.";
@@ -0,0 +1,14 @@
1
+ import type { CliLeaf, CliProgram } from "../types.ts";
2
+ import { cliUpdate } from "../install/update.ts";
3
+
4
+ /** Built-in `update` command (enabled when `install.updateGetLatest` is set). */
5
+ export function cliBuiltinUpdateCommand(program: CliProgram): CliLeaf {
6
+ return {
7
+ key: "update",
8
+ description: "Download and install the latest release.",
9
+ mcpTool: { enabled: false },
10
+ handler: async () => {
11
+ await cliUpdate(program);
12
+ },
13
+ };
14
+ }
@@ -11,15 +11,18 @@ export interface CliCapabilities {
11
11
  mcp: boolean;
12
12
  install: boolean;
13
13
  docs: boolean;
14
+ update: boolean;
14
15
  }
15
16
 
16
17
  /** Resolves which capabilities are enabled for a program. */
17
18
  export function resolveCapabilities(program: CliProgram): CliCapabilities {
19
+ const install = program.install?.enabled !== false;
18
20
  return {
19
21
  completion: true,
20
22
  mcp: program.mcpServer?.enabled === true,
21
- install: program.install?.enabled !== false,
23
+ install,
22
24
  docs: program.docs?.enabled === true,
25
+ update: install && typeof program.install?.updateGetLatest === "function",
23
26
  };
24
27
  }
25
28
 
@@ -29,6 +32,9 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
29
32
  if (caps.install) {
30
33
  names.push("install");
31
34
  }
35
+ if (caps.update) {
36
+ names.push("update");
37
+ }
32
38
  if (caps.docs) {
33
39
  names.push("docs");
34
40
  }
@@ -0,0 +1,55 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { CliProgram } from "../types.ts";
3
+ import { CliOptionKind } from "../types.ts";
4
+ import { generateApiGuide } from "./api-guide.ts";
5
+ import { cliSchemaExport } from "../schema.ts";
6
+
7
+ const nestedFixture: CliProgram = {
8
+ key: "nested.ts",
9
+ version: "1.0.0",
10
+ description: "Nested groups demo.",
11
+ docs: { enabled: true, topics: { readme: { text: "# readme\n" } } },
12
+ commands: [
13
+ {
14
+ key: "stat",
15
+ description: "File metadata.",
16
+ commands: [
17
+ {
18
+ key: "owner",
19
+ description: "Ownership helpers.",
20
+ commands: [
21
+ {
22
+ key: "lookup",
23
+ description: "Resolve owner info.",
24
+ options: [
25
+ {
26
+ name: "user-name",
27
+ description: "User to look up.",
28
+ kind: CliOptionKind.String,
29
+ shortName: "u",
30
+ },
31
+ ],
32
+ positionals: [
33
+ {
34
+ name: "path",
35
+ description: "File or directory.",
36
+ kind: CliOptionKind.String,
37
+ },
38
+ ],
39
+ handler: () => {},
40
+ },
41
+ ],
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ };
47
+
48
+ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
49
+ const md = generateApiGuide(nestedFixture);
50
+ const schema = cliSchemaExport(nestedFixture);
51
+ expect(md).toContain("`nested.ts stat owner lookup`");
52
+ expect(md).toContain("`--user-name` (`-u`)");
53
+ expect(md).toContain("`<path>`");
54
+ expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
55
+ });
@@ -0,0 +1,129 @@
1
+ import type { CliSchemaExport } from "../builtins/export.ts";
2
+ import { cliPositionalLabel } from "../help.ts";
3
+ import { cliSchemaExport } from "../schema.ts";
4
+ import type { CliOption, CliPositional, CliProgram } from "../types.ts";
5
+ import { CliFallbackMode, CliOptionKind } from "../types.ts";
6
+
7
+ /** CLI invocation path as a single string (`myapp stat owner lookup`). */
8
+ function commandPath(rootKey: string, path: string[]): string {
9
+ if (path.length === 0) {
10
+ return rootKey;
11
+ }
12
+ return [rootKey, ...path].join(" ");
13
+ }
14
+
15
+ /** Human-readable option type for API tables. */
16
+ function optionType(opt: CliOption): string {
17
+ if (opt.kind === CliOptionKind.Presence) {
18
+ return "flag";
19
+ }
20
+ if (opt.kind === CliOptionKind.Enum) {
21
+ return `enum (\`${(opt.choices ?? []).join("`, `")}\`)`;
22
+ }
23
+ return opt.kind;
24
+ }
25
+
26
+ /** Markdown table cell for one option flag. */
27
+ function optionLabel(opt: CliOption): string {
28
+ const long = `\`--${opt.name}\``;
29
+ const short = opt.shortName ? ` (\`-${opt.shortName}\`)` : "";
30
+ return `${long}${short}`;
31
+ }
32
+
33
+ /** One options table row. */
34
+ function formatOptionRow(opt: CliOption): string {
35
+ const req = opt.required ? "required" : "optional";
36
+ return `| ${optionLabel(opt)} | ${optionType(opt)} | ${req} | ${opt.description} |`;
37
+ }
38
+
39
+ /** One positionals table row. */
40
+ function formatPositionalRow(p: CliPositional): string {
41
+ const label = cliPositionalLabel(p, false);
42
+ const req = (p.argMin ?? 1) > 0 ? "required" : "optional";
43
+ return `| \`${label}\` | ${p.kind} | ${req} | ${p.description} |`;
44
+ }
45
+
46
+ /** Fallback routing note when present on a router node. */
47
+ function fallbackLine(node: CliSchemaExport): string | null {
48
+ if (node.fallbackCommand === undefined) {
49
+ return null;
50
+ }
51
+ const mode = node.fallbackMode ?? CliFallbackMode.MissingOnly;
52
+ return `**Default subcommand:** \`${node.fallbackCommand}\` (\`${mode}\`)`;
53
+ }
54
+
55
+ /** Renders one command node and recurses into subcommands. */
56
+ function renderCommandNode(
57
+ rootKey: string,
58
+ path: string[],
59
+ node: CliSchemaExport,
60
+ lines: string[],
61
+ ): void {
62
+ const level = Math.min(path.length + 2, 6);
63
+ const heading = "#".repeat(level);
64
+ const cmd = commandPath(rootKey, path);
65
+
66
+ lines.push(`${heading} \`${cmd}\``, "", node.description, "");
67
+
68
+ if (node.notes) {
69
+ lines.push(`> ${node.notes}`, "");
70
+ }
71
+
72
+ const fb = fallbackLine(node);
73
+ if (fb) {
74
+ lines.push(fb, "");
75
+ }
76
+
77
+ if ((node.options ?? []).length > 0) {
78
+ lines.push("#### Options", "");
79
+ lines.push("| Option | Type | Required | Description |");
80
+ lines.push("| --- | --- | --- | --- |");
81
+ for (const opt of node.options!) {
82
+ lines.push(formatOptionRow(opt));
83
+ }
84
+ lines.push("");
85
+ }
86
+
87
+ if ((node.positionals ?? []).length > 0) {
88
+ lines.push("#### Positionals", "");
89
+ lines.push("| Argument | Type | Required | Description |");
90
+ lines.push("| --- | --- | --- | --- |");
91
+ for (const p of node.positionals!) {
92
+ lines.push(formatPositionalRow(p));
93
+ }
94
+ lines.push("");
95
+ }
96
+
97
+ const children = node.commands ?? [];
98
+ if (children.length > 0) {
99
+ lines.push("#### Subcommands", "");
100
+ for (const child of children) {
101
+ lines.push(`- \`${child.key}\` — ${child.description}`);
102
+ }
103
+ lines.push("");
104
+ }
105
+
106
+ for (const child of children) {
107
+ renderCommandNode(rootKey, [...path, child.key], child, lines);
108
+ }
109
+ }
110
+
111
+ /** Generates markdown API reference from the same export as `docs schema`. */
112
+ export function generateApiGuide(program: CliProgram): string {
113
+ const schema = cliSchemaExport(program);
114
+ const lines: string[] = [
115
+ `# ${program.key} — CLI API reference`,
116
+ "",
117
+ schema.description,
118
+ "",
119
+ `Machine-readable export: \`${program.key} docs schema\``,
120
+ "",
121
+ ];
122
+
123
+ if (schema.notes) {
124
+ lines.push(`> ${schema.notes}`, "");
125
+ }
126
+
127
+ renderCommandNode(program.key, [], schema, lines);
128
+ return `${lines.join("\n").trimEnd()}\n`;
129
+ }
@@ -37,6 +37,9 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
37
37
  }
38
38
 
39
39
  leaves.push(
40
+ docsLeaf(program, "schema", "Print the full command tree as JSON."),
41
+ docsLeaf(program, "api", "Print the command tree as markdown."),
42
+ docsLeaf(program, "skill", "Print generated Cursor SKILL.md content."),
40
43
  docsLeaf(program, "all", "Print all bundled documentation combined."),
41
44
  );
42
45
 
@@ -46,7 +46,13 @@ test("docs reserved when enabled", () => {
46
46
 
47
47
  test("docs rejects reserved topic keys", () => {
48
48
  const root = docsFixture();
49
- root.docs!.topics.mcp = { text: "nope" };
49
+ root.docs!.topics.schema = { text: "nope" };
50
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
51
+ delete root.docs!.topics.schema;
52
+ root.docs!.topics.skill = { text: "nope" };
53
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
54
+ delete root.docs!.topics.skill;
55
+ root.docs!.topics.api = { text: "nope" };
50
56
  expect(() => cliValidateProgram(root)).toThrow(/reserved/);
51
57
  });
52
58
 
@@ -109,10 +115,50 @@ test("presentation includes docs subtree", () => {
109
115
  );
110
116
  });
111
117
 
118
+ test("docs schema prints JSON", async () => {
119
+ const result = await cliInvoke(docsFixture(), ["docs", "schema"]);
120
+ expect(result.exitCode).toBe(0);
121
+ const schema = JSON.parse(result.stdout);
122
+ expect(schema.key).toBe("myapp");
123
+ expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
124
+ });
125
+
126
+ test("docs api prints markdown reference", async () => {
127
+ const result = await cliInvoke(docsFixture(), ["docs", "api"]);
128
+ expect(result.exitCode).toBe(0);
129
+ expect(result.stdout).toContain("# myapp — CLI API reference");
130
+ expect(result.stdout).toContain("## `myapp run`");
131
+ expect(result.stdout).toContain("Run something.");
132
+ expect(result.stdout).toContain("myapp docs schema");
133
+ });
134
+
135
+ test("docs skill prints Cursor SKILL.md", async () => {
136
+ const result = await cliInvoke(docsFixture(), ["docs", "skill"]);
137
+ expect(result.exitCode).toBe(0);
138
+ expect(result.stdout).toContain("---");
139
+ expect(result.stdout).toContain("name: myapp");
140
+ expect(result.stdout).toContain("## Commands");
141
+ expect(result.stdout).not.toContain("mcp.json");
142
+ });
143
+
144
+ test("presentation includes docs schema and skill", () => {
145
+ const presentation = cliPresentationRoot(docsFixture());
146
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
147
+ expect(docsNode && "commands" in docsNode).toBe(true);
148
+ if (docsNode && "commands" in docsNode) {
149
+ expect(docsNode.commands.some((c) => c.key === "schema")).toBe(true);
150
+ expect(docsNode.commands.some((c) => c.key === "api")).toBe(true);
151
+ expect(docsNode.commands.some((c) => c.key === "skill")).toBe(true);
152
+ }
153
+ });
154
+
112
155
  test("completions offer docs subcommands", () => {
113
156
  const bash = completionBashScript(cliPresentationRoot(docsFixture()));
114
157
  expect(bash).toContain("docs) echo");
115
158
  expect(bash).toContain("readme) echo");
159
+ expect(bash).toContain("schema) echo");
160
+ expect(bash).toContain("api) echo");
161
+ expect(bash).toContain("skill) echo");
116
162
  });
117
163
 
118
164
  test("generateMcpGuide includes schema URI", () => {
@@ -84,7 +84,7 @@ export function generateMcpGuide(root: CliProgram): string {
84
84
  "|-----------|---------|",
85
85
  "| `tools/list` | Callable tools for exposed leaf commands |",
86
86
  "| `tools/call` | Runs handlers headlessly; JSON stdout becomes `structuredContent` when valid |",
87
- `| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} --schema\` |`,
87
+ `| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} docs schema\` |`,
88
88
  "",
89
89
  "## Exposed tools",
90
90
  "",
@@ -103,13 +103,13 @@ export function generateMcpGuide(root: CliProgram): string {
103
103
  "## Tool arguments",
104
104
  "",
105
105
  "Arguments are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
106
- `See \`${root.key} --schema\` or the schema resource for per-tool shapes.`,
106
+ `See \`${root.key} docs schema\` or the schema resource for per-tool shapes.`,
107
107
  "",
108
108
  "Varargs positionals accept a JSON array or a comma-separated string.",
109
109
  "",
110
110
  "## Protocol",
111
111
  "",
112
- "Stdio NDJSON JSON-RPC. Help and `--schema` are not available through tool calls.",
112
+ "Stdio NDJSON JSON-RPC. Help and `docs schema` are not available through tool calls.",
113
113
  `Run \`${root.key} docs\` for bundled user documentation.`,
114
114
  "",
115
115
  );
@@ -1,8 +1,11 @@
1
1
  import type { CliDocsConfig, CliProgram } from "../types.ts";
2
+ import { cliSchemaJson } from "../schema.ts";
3
+ import { generateSkillBundle } from "../skill/generate.ts";
4
+ import { generateApiGuide } from "./api-guide.ts";
2
5
  import { generateMcpGuide } from "./mcp-guide.ts";
3
6
 
4
7
  /** Built-in docs subcommand keys not allowed in `docs.topics`. */
5
- export const DOCS_BUILTIN_TOPIC_KEYS = ["mcp", "all"] as const;
8
+ export const DOCS_BUILTIN_TOPIC_KEYS = ["mcp", "all", "schema", "api", "skill"] as const;
6
9
 
7
10
  export type DocsBuiltinTopicKey = (typeof DOCS_BUILTIN_TOPIC_KEYS)[number];
8
11
 
@@ -81,8 +84,36 @@ export function combineAllDocs(program: CliProgram): string {
81
84
  .join("\n\n---\n\n");
82
85
  }
83
86
 
87
+ /** Writes CLI schema JSON to stdout (`docs schema`). */
88
+ export function printDocsSchema(program: CliProgram): void {
89
+ process.stdout.write(cliSchemaJson(program));
90
+ }
91
+
92
+ /** Writes markdown API reference to stdout (`docs api`). */
93
+ export function printDocsApi(program: CliProgram): void {
94
+ process.stdout.write(generateApiGuide(program));
95
+ }
96
+
97
+ /** Writes generated Cursor SKILL.md to stdout (`docs skill`). */
98
+ export function printDocsSkill(program: CliProgram): void {
99
+ const bundle = generateSkillBundle(program, "cursor");
100
+ process.stdout.write(`${bundle.skillMd}\n`);
101
+ }
102
+
84
103
  /** Writes one docs topic (or `all`) to stdout. */
85
104
  export function printDocsTopic(program: CliProgram, topic: string): void {
105
+ if (topic === "schema") {
106
+ printDocsSchema(program);
107
+ return;
108
+ }
109
+ if (topic === "api") {
110
+ printDocsApi(program);
111
+ return;
112
+ }
113
+ if (topic === "skill") {
114
+ printDocsSkill(program);
115
+ return;
116
+ }
86
117
  const content = topic === "all" ? combineAllDocs(program) : docsTopicText(program, topic);
87
118
  process.stdout.write(`${content}\n`);
88
119
  }