argsbarg 6.1.1 → 6.1.3

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 (229) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +17 -19
  3. package/bin/argsbarg +10 -0
  4. package/docs/README.md +4 -3
  5. package/docs/ai-skills.md +4 -2
  6. package/docs/bundled-docs.md +50 -25
  7. package/docs/cli-program.md +76 -10
  8. package/docs/config-schema.md +10 -11
  9. package/docs/configure.md +2 -0
  10. package/docs/decisions.md +40 -0
  11. package/docs/developing.md +43 -5
  12. package/docs/http-server.md +171 -0
  13. package/docs/json-schema-subset.md +51 -0
  14. package/docs/mcp.md +4 -2
  15. package/docs/output-schema.md +55 -62
  16. package/examples/formats.ts +6 -6
  17. package/examples/full-example/Formula/full-example.rb +35 -0
  18. package/examples/full-example/README.md +20 -21
  19. package/examples/full-example/docs/README.md +1 -1
  20. package/examples/full-example/docs/cli-schema.json +1790 -98
  21. package/examples/full-example/docs/cli.md +1990 -0
  22. package/examples/full-example/docs/http.md +28 -29
  23. package/examples/full-example/docs/mcp.md +8 -22
  24. package/examples/full-example/docs/openapi.json +783 -50
  25. package/examples/full-example/docs/skill.md +10 -10
  26. package/examples/full-example/justfile +11 -1
  27. package/examples/full-example/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
  28. package/examples/full-example/src/commands/render-json/__generated__/index.ts +5 -0
  29. package/examples/full-example/src/commands/render-json/command.test.ts +46 -0
  30. package/examples/full-example/src/commands/render-json/command.ts +30 -0
  31. package/examples/full-example/src/commands/render-json/types.ts +9 -0
  32. package/examples/full-example/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
  33. package/examples/full-example/src/commands/status/__generated__/index.ts +2 -2
  34. package/examples/full-example/src/commands/status/command.ts +5 -13
  35. package/examples/full-example/src/commands/status/types.ts +1 -14
  36. package/examples/full-example/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
  37. package/examples/full-example/src/commands/workspaces/__generated__/index.ts +5 -0
  38. package/examples/full-example/src/commands/workspaces/command.test.ts +58 -0
  39. package/examples/full-example/src/commands/workspaces/command.ts +94 -0
  40. package/examples/full-example/src/commands/workspaces/types.ts +6 -0
  41. package/examples/full-example/src/db/index.test.ts +86 -0
  42. package/examples/full-example/src/db/index.ts +101 -0
  43. package/examples/full-example/src/db/migrate.test.ts +35 -0
  44. package/examples/full-example/src/db/migrate.ts +69 -0
  45. package/examples/full-example/src/db/migrations/001_workspaces.sql +6 -0
  46. package/examples/full-example/src/db/tables/workspaces.ts +66 -0
  47. package/examples/full-example/src/program.ts +11 -36
  48. package/examples/full-example/src/types/argsbarg.d.ts +11 -0
  49. package/examples/full-example/src/types/md.d.ts +4 -0
  50. package/examples/full-example/tsconfig.json +5 -2
  51. package/examples/mcp-test.ts +1 -2
  52. package/examples/minimal.ts +1 -7
  53. package/examples/nested.ts +1 -2
  54. package/examples/option-required.ts +1 -1
  55. package/examples/servers.ts +4 -5
  56. package/index.d.ts +440 -136
  57. package/package.json +19 -2
  58. package/src/builtins/builtins.test.ts +7 -7
  59. package/src/builtins/completion-bash.ts +1 -1
  60. package/src/builtins/completion-fish.ts +1 -1
  61. package/src/builtins/completion-group.ts +4 -4
  62. package/src/builtins/completion-simulate-shared.ts +9 -0
  63. package/src/builtins/completion-zsh.ts +1 -1
  64. package/src/builtins/config.test.ts +3 -3
  65. package/src/builtins/config.ts +9 -9
  66. package/src/builtins/configure-copy.ts +2 -2
  67. package/src/builtins/configure.ts +4 -4
  68. package/src/builtins/dispatch.ts +19 -18
  69. package/src/builtins/export.ts +7 -5
  70. package/src/builtins/http.ts +68 -0
  71. package/src/builtins/mcp.ts +28 -4
  72. package/src/builtins/presentation.ts +6 -6
  73. package/src/builtins/registry.ts +6 -6
  74. package/src/builtins/scopes.ts +2 -2
  75. package/src/builtins/version.ts +1 -1
  76. package/src/cli-tool/full-example-capabilities.test.ts +10 -15
  77. package/src/cli-tool/main.ts +1 -1
  78. package/src/cli-tool/program.ts +3 -2
  79. package/src/cli-tool/prompt.ts +1 -1
  80. package/src/cli-tool/run-schemagen.ts +1 -3
  81. package/src/cli-tool/schemagen/cleanup.ts +6 -7
  82. package/src/cli-tool/schemagen/discover-schema-roots.ts +66 -120
  83. package/src/cli-tool/schemagen/index.ts +2 -2
  84. package/src/cli-tool/schemagen/names.ts +8 -13
  85. package/src/cli-tool/schemagen/run.ts +21 -28
  86. package/src/cli-tool/schemagen/schemagen.test.ts +136 -46
  87. package/src/config/bindings.test.ts +1 -1
  88. package/src/config/bindings.ts +1 -1
  89. package/src/config/bootstrap.test.ts +1 -1
  90. package/src/config/bootstrap.ts +36 -4
  91. package/src/config/context.test.ts +1 -1
  92. package/src/config/context.ts +1 -1
  93. package/src/config/entry.ts +1 -1
  94. package/src/config/file.test.ts +1 -1
  95. package/src/config/file.ts +3 -3
  96. package/src/config/manifest.ts +1 -1
  97. package/src/config/resolve.test.ts +1 -1
  98. package/src/config/resolve.ts +1 -1
  99. package/src/config/schema.ts +1 -1
  100. package/src/config/validate.ts +1 -1
  101. package/src/{install → configure/artifacts}/binary-placement.test.ts +1 -1
  102. package/src/{install → configure/artifacts}/binary-placement.ts +1 -1
  103. package/src/{install → configure/artifacts}/gh-release-update.ts +1 -1
  104. package/src/{install → configure/artifacts}/install-validate.test.ts +3 -3
  105. package/src/{install → configure/artifacts}/mcp-config.ts +1 -1
  106. package/src/{install → configure/artifacts}/mcp-opencode.test.ts +1 -1
  107. package/src/{install → configure/artifacts}/mcp-opencode.ts +1 -1
  108. package/src/{install → configure/artifacts}/paths.ts +5 -5
  109. package/src/configure/artifacts/plan.ts +24 -0
  110. package/src/{install → configure/artifacts}/status.test.ts +1 -1
  111. package/src/{install → configure/artifacts}/status.ts +2 -2
  112. package/src/{install → configure/artifacts}/target-base.ts +1 -1
  113. package/src/{install → configure/artifacts}/target-detect.ts +1 -1
  114. package/src/{install → configure/artifacts}/target-effective.ts +3 -9
  115. package/src/{install → configure/artifacts}/target-mcp-cli.ts +1 -1
  116. package/src/{install → configure/artifacts}/target-mcp-json.ts +1 -1
  117. package/src/{install → configure/artifacts}/target-plan-build.ts +2 -2
  118. package/src/{install → configure/artifacts}/target-registry.ts +2 -2
  119. package/src/{install → configure/artifacts}/target-scope.ts +3 -3
  120. package/src/{install → configure/artifacts}/target-skill.ts +1 -1
  121. package/src/{install → configure/artifacts}/target-types.ts +2 -2
  122. package/src/{install → configure/artifacts}/targets/app.ts +5 -5
  123. package/src/{install → configure/artifacts}/targets/chatgpt-mcp.ts +2 -2
  124. package/src/{install → configure/artifacts}/targets/claude-code-mcp.ts +2 -2
  125. package/src/{install → configure/artifacts}/targets/claude-desktop-mcp.ts +2 -2
  126. package/src/{install → configure/artifacts}/targets/claude-skill.ts +2 -2
  127. package/src/{install → configure/artifacts}/targets/codex-mcp.ts +2 -2
  128. package/src/{install → configure/artifacts}/targets/codex-skill.ts +2 -2
  129. package/src/{install → configure/artifacts}/targets/configure.ts +5 -5
  130. package/src/{install → configure/artifacts}/targets/cursor-mcp.ts +2 -2
  131. package/src/{install → configure/artifacts}/targets/cursor-skill.ts +2 -2
  132. package/src/{install → configure/artifacts}/targets/index.ts +1 -1
  133. package/src/{install → configure/artifacts}/targets/openclaw-mcp.ts +2 -2
  134. package/src/{install → configure/artifacts}/targets/openclaw-skill.ts +3 -3
  135. package/src/{install → configure/artifacts}/targets/opencode-mcp.ts +5 -5
  136. package/src/{install → configure/artifacts}/targets/opencode-skill.ts +3 -3
  137. package/src/{install → configure/artifacts}/targets.test.ts +1 -1
  138. package/src/{install → configure/artifacts}/uninstall.ts +1 -1
  139. package/src/configure/configure.test.ts +11 -11
  140. package/src/configure/index.ts +14 -14
  141. package/src/configure/prompt.ts +2 -2
  142. package/src/{context.ts → core/context.ts} +26 -20
  143. package/src/core/json-leaf.test.ts +156 -0
  144. package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
  145. package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +76 -16
  146. package/src/{parse.test.ts → core/parse.test.ts} +97 -109
  147. package/src/{parse.ts → core/parse.ts} +173 -25
  148. package/src/{schema.ts → core/schema.ts} +25 -13
  149. package/src/{types.ts → core/types.ts} +238 -35
  150. package/src/{validate.ts → core/validate.ts} +51 -29
  151. package/src/docs/builtin.ts +8 -19
  152. package/src/docs/{api-guide.test.ts → cli-guide.test.ts} +21 -21
  153. package/src/docs/{api-guide.ts → cli-guide.ts} +45 -16
  154. package/src/docs/docs.test.ts +76 -41
  155. package/src/docs/http-guide.ts +37 -34
  156. package/src/docs/mcp-guide.ts +12 -14
  157. package/src/docs/mcp-resources.test.ts +2 -3
  158. package/src/docs/mcp-resources.ts +6 -11
  159. package/src/docs/resolve.ts +22 -30
  160. package/src/docs/save.ts +3 -3
  161. package/src/exports/cli.ts +47 -0
  162. package/src/exports/headless.ts +13 -0
  163. package/src/exports/http.ts +6 -0
  164. package/src/exports/mcp.ts +6 -0
  165. package/src/{headless.test.ts → headless/routing.test.ts} +3 -3
  166. package/src/{headless.ts → headless/routing.ts} +3 -3
  167. package/src/headless/tool-call.ts +114 -46
  168. package/src/help.test.ts +152 -0
  169. package/src/help.ts +54 -18
  170. package/src/hooks/builtin.ts +20 -0
  171. package/src/hooks/run.ts +142 -0
  172. package/src/http/openapi.ts +182 -0
  173. package/src/http/readiness.ts +78 -0
  174. package/src/{api → http}/result.ts +16 -5
  175. package/src/http/routes.ts +329 -0
  176. package/src/http/server.ts +225 -0
  177. package/src/index.ts +38 -25
  178. package/src/log/ecs.test.ts +43 -0
  179. package/src/log/ecs.ts +59 -0
  180. package/src/log/emitter.ts +166 -0
  181. package/src/mcp/bundle.ts +2 -2
  182. package/src/mcp/claude.test.ts +1 -1
  183. package/src/mcp/claude.ts +4 -4
  184. package/src/{hidden-mcpb.test.ts → mcp/hidden-mcpb.test.ts} +10 -9
  185. package/src/mcp/result.ts +2 -2
  186. package/src/mcp/server.ts +54 -6
  187. package/src/mcp/tools.ts +18 -20
  188. package/src/{capabilities.ts → runtime/capabilities.ts} +11 -11
  189. package/src/{cli-errors.ts → runtime/cli-errors.ts} +4 -4
  190. package/src/{cli.ts → runtime/cli.ts} +160 -50
  191. package/src/runtime/exposure.ts +102 -0
  192. package/src/{invoke.test.ts → runtime/invoke.test.ts} +31 -7
  193. package/src/server/context.ts +25 -0
  194. package/src/server/overrides.ts +112 -0
  195. package/src/skill/generate.ts +8 -8
  196. package/src/skill/hint.ts +1 -1
  197. package/src/skill/install.ts +2 -2
  198. package/src/skill/naming.ts +1 -1
  199. package/src/{test-fixtures.ts → test/fixtures.ts} +3 -2
  200. package/src/{config.integration.test.ts → test/integration/config.test.ts} +8 -8
  201. package/src/{api.integration.test.ts → test/integration/http.test.ts} +170 -67
  202. package/src/{mcp.integration.test.ts → test/integration/mcp.test.ts} +11 -57
  203. package/docs/api-server.md +0 -141
  204. package/examples/full-example/docs/api.md +0 -511
  205. package/examples/full-example/src/commands/status/__generated__/outputSchema.json +0 -28
  206. package/examples/full-example/src/config/__generated__/configSchema.json +0 -40
  207. package/examples/full-example/src/config/__generated__/index.ts +0 -5
  208. package/examples/full-example/src/config/types.ts +0 -24
  209. package/src/api/openapi.ts +0 -117
  210. package/src/api/server.ts +0 -120
  211. package/src/builtins/api.ts +0 -38
  212. package/src/hidden.ts +0 -30
  213. package/src/install/plan.ts +0 -53
  214. /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
  215. /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
  216. /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
  217. /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
  218. /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
  219. /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
  220. /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
  221. /package/src/{install → configure/artifacts}/normalize.ts +0 -0
  222. /package/src/{install → configure/artifacts}/opts.ts +0 -0
  223. /package/src/{install → configure/artifacts}/shell.ts +0 -0
  224. /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
  225. /package/src/{formats.ts → core/formats.ts} +0 -0
  226. /package/src/{respond.ts → core/respond.ts} +0 -0
  227. /package/src/{types.test.ts → core/types.test.ts} +0 -0
  228. /package/src/{api → http}/schema-deref.test.ts +0 -0
  229. /package/src/{api → http}/schema-deref.ts +0 -0
@@ -0,0 +1,152 @@
1
+ /*
2
+ Help rendering and label formatting tests.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { cliPresentationRoot } from "./builtins/presentation.ts";
7
+ import { type CliOption, CliOptionKind, type CliPositional } from "./core/types.ts";
8
+ import { CLI_NOTES_PROGRAM, cliHelpRender, cliOptionLabel, cliPositionalLabel, cliResolveNotes } from "./help.ts";
9
+ import { testProgram } from "./test/fixtures.ts";
10
+
11
+ describe("cliOptionLabel", () => {
12
+ test.each([
13
+ {
14
+ name: "string option",
15
+ option: { name: "out", description: "Output path.", kind: CliOptionKind.String },
16
+ expected: "--out <string>",
17
+ },
18
+ {
19
+ name: "required enum",
20
+ option: {
21
+ name: "format",
22
+ description: "Format.",
23
+ kind: CliOptionKind.Enum,
24
+ choices: ["pdf", "html"],
25
+ required: true,
26
+ },
27
+ expected: "--format <pdf|html>",
28
+ },
29
+ {
30
+ name: "short name",
31
+ option: {
32
+ name: "verbose",
33
+ description: "Verbose.",
34
+ kind: CliOptionKind.Presence,
35
+ shortName: "v",
36
+ },
37
+ expected: "--verbose, -v",
38
+ },
39
+ {
40
+ name: "json option",
41
+ option: { name: "body", description: "JSON body.", kind: CliOptionKind.Json },
42
+ expected: "--body <json>",
43
+ },
44
+ ])("$name", ({ option, expected }) => {
45
+ expect(cliOptionLabel(option as CliOption, false)).toBe(expected);
46
+ });
47
+ });
48
+
49
+ describe("cliPositionalLabel", () => {
50
+ test.each([
51
+ { positional: { name: "file", description: "File." }, expected: "<file>" },
52
+ { positional: { name: "file", description: "File.", argMin: 0 }, expected: "[file]" },
53
+ { positional: { name: "paths", description: "Paths.", argMax: 0 }, expected: "<paths...>" },
54
+ { positional: { name: "paths", description: "Paths.", argMin: 0, argMax: 0 }, expected: "[paths...]" },
55
+ ])("$expected", ({ positional, expected }) => {
56
+ expect(cliPositionalLabel(positional as CliPositional, false)).toBe(expected);
57
+ });
58
+ });
59
+
60
+ describe("cliResolveNotes", () => {
61
+ test("replaces program placeholder", () => {
62
+ expect(cliResolveNotes(`Run \`${CLI_NOTES_PROGRAM} docs readme\`.`, "myapp")).toBe("Run `myapp docs readme`.");
63
+ });
64
+ });
65
+
66
+ describe("cliHelpRender", () => {
67
+ test("docs help lists schema, cli, and skill subcommands", () => {
68
+ const root = testProgram({
69
+ key: "app",
70
+ version: "1.0.0",
71
+ description: "demo",
72
+ docs: {
73
+ topics: { readme: { text: "# readme\n" } },
74
+ },
75
+ commands: [
76
+ {
77
+ key: "x",
78
+ description: "cmd",
79
+ handler: () => {},
80
+ },
81
+ ],
82
+ });
83
+ const help = cliHelpRender(cliPresentationRoot(root), ["docs"], false);
84
+ expect(help).toContain("cli-schema");
85
+ expect(help).toContain("Print the full CLI command tree as JSON.");
86
+ expect(help).toContain("cli");
87
+ expect(help).toContain("markdown");
88
+ expect(help).toContain("skill");
89
+ expect(help).toContain("reference agent SKILL");
90
+ });
91
+
92
+ test("root help omits legacy --schema flag", () => {
93
+ const root = testProgram({
94
+ key: "app",
95
+ version: "1.0.0",
96
+ description: "demo",
97
+ commands: [
98
+ {
99
+ key: "x",
100
+ description: "cmd",
101
+ handler: () => {},
102
+ },
103
+ ],
104
+ });
105
+ const help = cliHelpRender(cliPresentationRoot(root), [], false);
106
+ expect(help).not.toContain("--schema");
107
+ });
108
+
109
+ test("root help shows agent docs hint when docs enabled", () => {
110
+ const root = testProgram({
111
+ key: "myapp",
112
+ version: "1.0.0",
113
+ description: "demo",
114
+ docs: {
115
+ topics: { readme: { text: "# readme\n" } },
116
+ },
117
+ commands: [{ key: "run", description: "Run.", handler: () => {} }],
118
+ });
119
+ const help = cliHelpRender(cliPresentationRoot(root), [], false);
120
+ expect(help).toContain("For AI agents: `myapp docs skill`.");
121
+ expect(help).not.toContain("install --skill");
122
+ });
123
+
124
+ test("root help omits agent hint when docs disabled", () => {
125
+ const root = testProgram({
126
+ key: "myapp",
127
+ version: "1.0.0",
128
+ description: "demo",
129
+ docs: { enabled: false },
130
+ commands: [{ key: "run", description: "Run.", handler: () => {} }],
131
+ });
132
+ const help = cliHelpRender(cliPresentationRoot(root), [], false);
133
+ expect(help).not.toContain("Agents:");
134
+ expect(help).not.toContain("docs skill");
135
+ });
136
+
137
+ test("root help includes program notes and agent hint", () => {
138
+ const root = testProgram({
139
+ key: "myapp",
140
+ version: "1.0.0",
141
+ description: "demo",
142
+ notes: "See `{argsbarg:program} docs readme` for the user guide.",
143
+ docs: {
144
+ topics: { readme: { text: "# readme\n" } },
145
+ },
146
+ commands: [{ key: "run", description: "Run.", handler: () => {} }],
147
+ });
148
+ const help = cliHelpRender(cliPresentationRoot(root), [], false);
149
+ expect(help).toContain("See `myapp docs readme` for the user guide.");
150
+ expect(help).toContain("myapp docs skill");
151
+ });
152
+ });
package/src/help.ts CHANGED
@@ -7,7 +7,6 @@ It keeps help formatting shared across help and error paths so users see one con
7
7
  style no matter how help is reached.
8
8
  */
9
9
 
10
- import { visibleOptions, visibleSubcommands } from "./hidden.ts";
11
10
  import {
12
11
  type CliNode,
13
12
  type CliOption,
@@ -16,7 +15,9 @@ import {
16
15
  type CliRouter,
17
16
  isCliLeaf,
18
17
  isCliRouter,
19
- } from "./types.ts";
18
+ isJsonLeaf,
19
+ } from "./core/types.ts";
20
+ import { visibleOptions, visibleSubcommands } from "./runtime/exposure.ts";
20
21
 
21
22
  // ── ANSI Style Helpers ────────────────────────────────────────────────────────
22
23
 
@@ -196,7 +197,7 @@ export function cliOptionLabel(o: CliOption, color: boolean): string {
196
197
  return `${style.aquaBold(left)} ${style.greenBright(right)}`;
197
198
  }
198
199
 
199
- /** Placeholder in `notes` for the root program key (resolved in help, schema, and docs api). */
200
+ /** Placeholder in `notes` for the root program key (resolved in help, schema, and docs cli). */
200
201
  export const CLI_NOTES_PROGRAM = "{argsbarg:program}";
201
202
 
202
203
  /** Replaces `{argsbarg:program}` in notes/help text with the program key. */
@@ -328,6 +329,7 @@ function usageLines(
328
329
  helpPath: string[],
329
330
  hasCommands: boolean,
330
331
  hasArgs: boolean,
332
+ jsonLeaf: boolean,
331
333
  color: boolean,
332
334
  ): string[] {
333
335
  let fullPath = appName;
@@ -337,6 +339,7 @@ function usageLines(
337
339
  const usageOpts = color ? style.aquaBold("[OPTIONS]") : "[OPTIONS]";
338
340
  const usageCmd = color ? style.aquaBold("COMMAND") : "COMMAND";
339
341
  const usageArgs = color ? style.aquaBold("[ARGS]...") : "[ARGS]...";
342
+ const usageJson = color ? style.aquaBold("[JSON]") : "[JSON]";
340
343
 
341
344
  const out: string[] = [];
342
345
  if (helpPath.length === 0) {
@@ -347,6 +350,10 @@ function usageLines(
347
350
  }
348
351
  return out;
349
352
  }
353
+ if (jsonLeaf) {
354
+ out.push(`${fullPath} ${usageJson}`);
355
+ return out;
356
+ }
350
357
  out.push(`${fullPath} ${usageOpts}${hasArgs ? ` ${usageArgs}` : ""}`);
351
358
  if (hasCommands) {
352
359
  out.push(`${fullPath} ${usageCmd} ${usageArgs}`);
@@ -354,6 +361,25 @@ function usageLines(
354
361
  return out;
355
362
  }
356
363
 
364
+ /** Table rows for `kind: "json"` leaf input (schema properties + stdin hint). */
365
+ function rowsForJsonInput(inputSchema: Record<string, unknown> | undefined): HelpRow[] {
366
+ const hint = "Pass a JSON document as an argument or pipe to stdin.";
367
+ const rows: HelpRow[] = [{ label: "JSON", description: hint }];
368
+ const props = inputSchema?.properties;
369
+ if (!props || typeof props !== "object" || Array.isArray(props)) {
370
+ return rows;
371
+ }
372
+ const required = new Set(Array.isArray(inputSchema?.required) ? inputSchema.required.map((k) => String(k)) : []);
373
+ for (const [name, prop] of Object.entries(props as Record<string, { description?: string }>)) {
374
+ const desc = prop.description ?? "";
375
+ rows.push({
376
+ label: name,
377
+ description: required.has(name) ? `(required) ${desc}` : desc,
378
+ });
379
+ }
380
+ return rows;
381
+ }
382
+
357
383
  /** Table rows for named options, including synthetic built-in rows. */
358
384
  function rowsForOptions(defs: CliOption[], color: boolean): HelpRow[] {
359
385
  const rows: HelpRow[] = [];
@@ -407,7 +433,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
407
433
  lines.push(
408
434
  renderTextBox(
409
435
  "Usage",
410
- usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, color),
436
+ usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, false, color),
411
437
  hw,
412
438
  color,
413
439
  ).join("\n"),
@@ -446,6 +472,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
446
472
  lines.push(color ? style.white(node.description) : node.description);
447
473
  lines.push("");
448
474
  }
475
+ const nodeIsJsonLeaf = isCliLeaf(node) && isJsonLeaf(node);
449
476
  lines.push(
450
477
  renderTextBox(
451
478
  "Usage",
@@ -454,6 +481,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
454
481
  helpPath,
455
482
  isCliRouter(node) && node.commands.length > 0,
456
483
  isCliLeaf(node) && (node.positionals ?? []).length > 0,
484
+ nodeIsJsonLeaf,
457
485
  color,
458
486
  ),
459
487
  hw,
@@ -461,21 +489,29 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
461
489
  ).join("\n"),
462
490
  );
463
491
 
464
- const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
465
- if (optBox.length > 0) {
466
- lines.push("");
467
- lines.push(optBox.join("\n"));
468
- }
492
+ if (nodeIsJsonLeaf && isCliLeaf(node)) {
493
+ const inputBox = renderTableBox("Input", rowsForJsonInput(node.inputSchema), hw, color);
494
+ if (inputBox.length > 0) {
495
+ lines.push("");
496
+ lines.push(inputBox.join("\n"));
497
+ }
498
+ } else {
499
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
500
+ if (optBox.length > 0) {
501
+ lines.push("");
502
+ lines.push(optBox.join("\n"));
503
+ }
469
504
 
470
- const posBox = renderTableBox(
471
- "Arguments",
472
- rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color),
473
- hw,
474
- color,
475
- );
476
- if (posBox.length > 0) {
477
- lines.push("");
478
- lines.push(posBox.join("\n"));
505
+ const posBox = renderTableBox(
506
+ "Arguments",
507
+ rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color),
508
+ hw,
509
+ color,
510
+ );
511
+ if (posBox.length > 0) {
512
+ lines.push("");
513
+ lines.push(posBox.join("\n"));
514
+ }
479
515
  }
480
516
 
481
517
  const subcmds = isCliRouter(node) ? node.commands : [];
@@ -0,0 +1,20 @@
1
+ /*
2
+ Detects built-in command paths so invoke hooks are skipped for framework commands.
3
+ */
4
+
5
+ const BUILTIN_ROOTS = new Set(["completion", "version", "http", "mcp", "configure", "docs"]);
6
+
7
+ /** True when `path` routes to a framework built-in (hooks are skipped). */
8
+ export function isBuiltinInvokePath(path: string[]): boolean {
9
+ const root = path[0];
10
+ if (!root || !BUILTIN_ROOTS.has(root)) {
11
+ return false;
12
+ }
13
+ if (root === "http") {
14
+ return path.length <= 1 || path[1] === "serve";
15
+ }
16
+ if (root === "mcp") {
17
+ return path.length <= 1 || path[1] === "serve" || path[1] === "bundle";
18
+ }
19
+ return true;
20
+ }
@@ -0,0 +1,142 @@
1
+ /*
2
+ Safe async hook runner, failure classification, and invoke error pipeline.
3
+ */
4
+
5
+ import type { CliContext } from "~/core/context.ts";
6
+ import { LeafInputError } from "~/core/leaf-inputs.ts";
7
+ import type {
8
+ ClientErrorOverride,
9
+ ErrorHookContext,
10
+ InvokeFailureKind,
11
+ InvokeHookContext,
12
+ ServerRuntime,
13
+ } from "~/core/types.ts";
14
+ import { firstErrorLine } from "~/http/result.ts";
15
+ import { type LogEmitter, obscureUnexpectedClientMessage } from "~/log/emitter.ts";
16
+
17
+ /** Runs a hook without letting hook throws escape uncaught. */
18
+ export async function runHook<T>(hook: (() => T | Promise<T>) | undefined, label: string): Promise<T | undefined> {
19
+ if (!hook) {
20
+ return undefined;
21
+ }
22
+ try {
23
+ return await Promise.resolve(hook());
24
+ } catch (err) {
25
+ const message = err instanceof Error ? err.message : String(err);
26
+ throw new Error(`${label} hook failed: ${message}`, { cause: err });
27
+ }
28
+ }
29
+
30
+ /** Classifies an invoke failure for status mapping and logging. */
31
+ export function classifyFailureKind(
32
+ err: unknown,
33
+ opts: { parseError?: boolean; help?: boolean; missingConfig?: boolean; notReady?: boolean },
34
+ ): InvokeFailureKind {
35
+ if (opts.help) {
36
+ return "help";
37
+ }
38
+ if (opts.missingConfig) {
39
+ return "missing_config";
40
+ }
41
+ if (opts.notReady) {
42
+ return "not_ready";
43
+ }
44
+ if (opts.parseError || err instanceof LeafInputError) {
45
+ return "validation";
46
+ }
47
+ if (err instanceof Error) {
48
+ return "validation";
49
+ }
50
+ return "unexpected";
51
+ }
52
+
53
+ /** HTTP status for a classified failure kind. */
54
+ export function failureKindHttpStatus(kind: InvokeFailureKind): number {
55
+ switch (kind) {
56
+ case "validation":
57
+ case "help":
58
+ return 400;
59
+ case "unknown_route":
60
+ return 404;
61
+ case "missing_config":
62
+ case "not_ready":
63
+ return 503;
64
+ case "unexpected":
65
+ return 500;
66
+ }
67
+ }
68
+
69
+ /** Builds {@link InvokeHookContext} from a live {@link CliContext}. */
70
+ export function buildInvokeHookContext(
71
+ ctx: CliContext,
72
+ extras: {
73
+ path: string[];
74
+ runtime?: ServerRuntime;
75
+ http?: InvokeHookContext["http"];
76
+ mcp?: InvokeHookContext["mcp"];
77
+ },
78
+ ): InvokeHookContext {
79
+ return {
80
+ invocation: ctx.invocation,
81
+ path: extras.path,
82
+ pathParams: { ...ctx.pathParams },
83
+ opts: ctx.opts,
84
+ locals: ctx.locals,
85
+ runtime: extras.runtime,
86
+ appConfig: ctx.appConfig,
87
+ http: extras.http,
88
+ mcp: extras.mcp,
89
+ };
90
+ }
91
+
92
+ function defaultClientError(err: unknown, _failureKind: InvokeFailureKind): ClientErrorOverride {
93
+ const message =
94
+ err instanceof Error ? err.message : typeof err === "string" ? err : firstErrorLine(String(err)) || "Error";
95
+ return { message, exitCode: 1 };
96
+ }
97
+
98
+ export interface ErrorPipelineResult {
99
+ failureKind: InvokeFailureKind;
100
+ clientError: ClientErrorOverride;
101
+ errorMsg: string;
102
+ }
103
+
104
+ /** Runs formatError → onError → ECS emit for one invoke failure. */
105
+ export async function runErrorPipeline(
106
+ hookCtx: InvokeHookContext,
107
+ err: unknown,
108
+ failureKind: InvokeFailureKind,
109
+ hooks: import("~/core/types.ts").CliProgramHooks | undefined,
110
+ emitter: LogEmitter | undefined,
111
+ obscureUnexpected: boolean,
112
+ ): Promise<ErrorPipelineResult> {
113
+ let clientError = defaultClientError(err, failureKind);
114
+ if (failureKind === "unexpected" && obscureUnexpected) {
115
+ clientError = { message: obscureUnexpectedClientMessage(), exitCode: 1 };
116
+ }
117
+
118
+ const errorCtx: ErrorHookContext = {
119
+ ...hookCtx,
120
+ failureKind,
121
+ error: err,
122
+ clientError: { ...clientError },
123
+ };
124
+
125
+ const formatted = await runHook(() => hooks?.formatError?.(errorCtx), "formatError");
126
+ if (formatted) {
127
+ clientError = { ...clientError, ...formatted };
128
+ errorCtx.clientError = { ...clientError };
129
+ }
130
+
131
+ await runHook(() => hooks?.onError?.(errorCtx), "onError");
132
+
133
+ const displayMessage =
134
+ failureKind === "unexpected" && obscureUnexpected ? obscureUnexpectedClientMessage() : clientError.message;
135
+
136
+ emitter?.emitInvokeError(failureKind, err, displayMessage, {
137
+ invocation: hookCtx.invocation,
138
+ path: hookCtx.path.join(" "),
139
+ });
140
+
141
+ return { failureKind, clientError, errorMsg: displayMessage };
142
+ }
@@ -0,0 +1,182 @@
1
+ /*
2
+ Hand-built OpenAPI 3.1 document from exposed HTTP REST routes.
3
+ */
4
+
5
+ import { collectOptionDefs } from "~/core/parse.ts";
6
+ import type { CliHttpMethod, CliProgram } from "~/core/types.ts";
7
+ import { CliOptionKind, isJsonLeaf } from "~/core/types.ts";
8
+ import { collectHttpRoutes, defaultSuccessStatus } from "./routes.ts";
9
+ import { dereferenceJsonSchema } from "./schema-deref.ts";
10
+
11
+ const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
12
+
13
+ function defaultErrorSchema(): Record<string, unknown> {
14
+ return {
15
+ type: "object",
16
+ properties: { error: { type: "string" } },
17
+ required: ["error"],
18
+ };
19
+ }
20
+
21
+ function errorResponseSchema(program: CliProgram): Record<string, unknown> {
22
+ const custom = program.httpServer?.errors?.errorSchema;
23
+ return custom ? dereferenceJsonSchema(custom) : defaultErrorSchema();
24
+ }
25
+
26
+ function errorResponseEntry(program: CliProgram, description: string): Record<string, unknown> {
27
+ return {
28
+ description,
29
+ content: {
30
+ [JSON_CONTENT_TYPE]: {
31
+ schema: errorResponseSchema(program),
32
+ },
33
+ },
34
+ };
35
+ }
36
+
37
+ function buildInputSchema(
38
+ program: CliProgram,
39
+ route: ReturnType<typeof collectHttpRoutes>[number],
40
+ ): Record<string, unknown> {
41
+ const leaf = route.leaf;
42
+ if (leaf.inputSchema) {
43
+ return leaf.inputSchema;
44
+ }
45
+ const properties: Record<string, unknown> = {};
46
+ const required: string[] = [];
47
+ for (const p of route.paramNames) {
48
+ properties[p] = { type: "string" };
49
+ required.push(p);
50
+ }
51
+ const argv = route.commandPath.filter((k) => !k.startsWith(":"));
52
+ for (const opt of collectOptionDefs(program, argv)) {
53
+ if (opt.kind === CliOptionKind.Json) {
54
+ continue;
55
+ }
56
+ properties[opt.name] = { type: "string", description: opt.description };
57
+ if (opt.required) {
58
+ required.push(opt.name);
59
+ }
60
+ }
61
+ for (const p of leaf.positionals ?? []) {
62
+ properties[p.name] = { type: "string", description: p.description };
63
+ if ((p.argMin ?? 1) >= 1) {
64
+ required.push(p.name);
65
+ }
66
+ }
67
+ return {
68
+ type: "object",
69
+ properties,
70
+ ...(required.length > 0 ? { required } : {}),
71
+ };
72
+ }
73
+
74
+ /** Builds success response entries for OpenAPI (status → response object). */
75
+ function buildSuccessResponses(route: ReturnType<typeof collectHttpRoutes>[number]): Record<string, unknown> {
76
+ const contentType = route.leaf.http?.successContentType ?? "application/json";
77
+ const media: Record<string, unknown> = {};
78
+ const method = route.method;
79
+
80
+ if (contentType.includes("application/json")) {
81
+ const outputSchema = route.leaf.outputSchema ?? { type: "object" };
82
+ media[contentType] = {
83
+ schema: dereferenceJsonSchema(outputSchema),
84
+ };
85
+ } else if (contentType.includes("text/html")) {
86
+ media[contentType] = { schema: { type: "string" } };
87
+ } else {
88
+ media[contentType] = { schema: { type: "string", format: "binary" } };
89
+ }
90
+
91
+ const status = String(route.leaf.http?.successStatus ?? defaultSuccessStatus(method, method !== "DELETE"));
92
+ if (method === "DELETE" && status === "204") {
93
+ return {
94
+ "204": { description: "Successful invocation" },
95
+ };
96
+ }
97
+ return {
98
+ [status]: {
99
+ description: "Successful invocation",
100
+ content: media,
101
+ },
102
+ };
103
+ }
104
+
105
+ function methodLower(method: CliHttpMethod): string {
106
+ return method.toLowerCase();
107
+ }
108
+
109
+ /** Generates an OpenAPI 3.1 document for the program's HTTP routes. */
110
+ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
111
+ const routes = collectHttpRoutes(program);
112
+ const paths: Record<string, unknown> = {};
113
+
114
+ for (const route of routes) {
115
+ const pathKey = route.openApiPath;
116
+ const existing = (paths[pathKey] as Record<string, unknown> | undefined) ?? {};
117
+ const op: Record<string, unknown> = {
118
+ operationId: route.openApiPath.replace(/\//g, "_").replace(/[{}]/g, ""),
119
+ summary: route.leaf.description ?? route.leaf.key,
120
+ responses: {
121
+ ...buildSuccessResponses(route),
122
+ "400": errorResponseEntry(program, "Invalid arguments or help requested"),
123
+ "404": errorResponseEntry(program, "Not found"),
124
+ "500": errorResponseEntry(program, "Handler error"),
125
+ "503": errorResponseEntry(program, "Not ready or missing required config"),
126
+ },
127
+ };
128
+
129
+ if (route.paramNames.length > 0) {
130
+ op.parameters = route.paramNames.map((name) => ({
131
+ name,
132
+ in: "path",
133
+ required: true,
134
+ schema: { type: "string" },
135
+ }));
136
+ }
137
+
138
+ const method = methodLower(route.method);
139
+ if (method === "get" || method === "delete") {
140
+ op.parameters = [
141
+ ...((op.parameters as unknown[]) ?? []),
142
+ ...collectOptionDefs(
143
+ program,
144
+ route.commandPath.filter((k) => !k.startsWith(":")),
145
+ ).map((opt) => ({
146
+ name: opt.name,
147
+ in: "query",
148
+ required: opt.required ?? false,
149
+ schema: { type: "string" },
150
+ description: opt.description,
151
+ })),
152
+ ];
153
+ } else if (!isJsonLeaf(route.leaf)) {
154
+ op.requestBody = {
155
+ required: false,
156
+ content: {
157
+ [JSON_CONTENT_TYPE]: {
158
+ schema: dereferenceJsonSchema(buildInputSchema(program, route)),
159
+ },
160
+ },
161
+ };
162
+ }
163
+
164
+ existing[method] = op;
165
+ paths[pathKey] = existing;
166
+ }
167
+
168
+ return {
169
+ openapi: "3.1.0",
170
+ info: {
171
+ title: program.key,
172
+ version: program.version,
173
+ description: program.description,
174
+ },
175
+ paths,
176
+ };
177
+ }
178
+
179
+ /** Pretty-printed OpenAPI JSON (same document as `GET /openapi.json`). */
180
+ export function openApiJson(program: CliProgram): string {
181
+ return `${JSON.stringify(generateOpenApi(program), null, 2)}\n`;
182
+ }