argsbarg 6.1.2 → 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 +65 -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 +52 -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 +431 -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/{json-leaf.test.ts → core/json-leaf.test.ts} +4 -4
  144. package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
  145. package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +16 -12
  146. package/src/{parse.test.ts → core/parse.test.ts} +97 -109
  147. package/src/{parse.ts → core/parse.ts} +129 -31
  148. package/src/{schema.ts → core/schema.ts} +25 -13
  149. package/src/{types.ts → core/types.ts} +225 -35
  150. package/src/{validate.ts → core/validate.ts} +39 -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 +3 -3
  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 +36 -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 +9 -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} +159 -49
  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,
@@ -17,7 +16,8 @@ import {
17
16
  isCliLeaf,
18
17
  isCliRouter,
19
18
  isJsonLeaf,
20
- } from "./types.ts";
19
+ } from "./core/types.ts";
20
+ import { visibleOptions, visibleSubcommands } from "./runtime/exposure.ts";
21
21
 
22
22
  // ── ANSI Style Helpers ────────────────────────────────────────────────────────
23
23
 
@@ -197,7 +197,7 @@ export function cliOptionLabel(o: CliOption, color: boolean): string {
197
197
  return `${style.aquaBold(left)} ${style.greenBright(right)}`;
198
198
  }
199
199
 
200
- /** 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). */
201
201
  export const CLI_NOTES_PROGRAM = "{argsbarg:program}";
202
202
 
203
203
  /** Replaces `{argsbarg:program}` in notes/help text with the program key. */
@@ -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
+ }
@@ -0,0 +1,78 @@
1
+ /*
2
+ HTTP/MCP readiness checks for GET /health/ready (orchestrator probes only).
3
+ */
4
+
5
+ import type { AnyAppConfigSnapshot } from "~/config/context.ts";
6
+ import { missingRequiredConfig } from "~/config/resolve.ts";
7
+ import type { CliProgram, ReadinessContext, ServerRuntime } from "~/core/types.ts";
8
+
9
+ const READINESS_CACHE_MS = 3000;
10
+
11
+ export interface ReadinessCheck {
12
+ ok: boolean;
13
+ error?: string;
14
+ missing?: string[];
15
+ }
16
+
17
+ export interface ReadinessResult {
18
+ ok: boolean;
19
+ checks: Record<string, ReadinessCheck>;
20
+ }
21
+
22
+ function configFileCheck(runtime: ServerRuntime): ReadinessCheck {
23
+ const err = runtime.state.configFileError;
24
+ if (typeof err === "string" && err.length > 0) {
25
+ return { ok: false, error: err };
26
+ }
27
+ return { ok: true };
28
+ }
29
+
30
+ function configRequiredCheck(program: CliProgram, appConfig: AnyAppConfigSnapshot): ReadinessCheck {
31
+ if (!program.appConfig) {
32
+ return { ok: true };
33
+ }
34
+ const missing = missingRequiredConfig(program, appConfig.read());
35
+ if (missing.length > 0) {
36
+ return { ok: false, missing };
37
+ }
38
+ return { ok: true };
39
+ }
40
+
41
+ async function customReadinessCheck(ctx: ReadinessContext): Promise<ReadinessCheck> {
42
+ const fn = ctx.program.readiness;
43
+ if (!fn) {
44
+ return { ok: true };
45
+ }
46
+ try {
47
+ const ok = await Promise.resolve(fn(ctx));
48
+ return ok ? { ok: true } : { ok: false, error: "readiness check returned false" };
49
+ } catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ return { ok: false, error: message };
52
+ }
53
+ }
54
+
55
+ /** Runs built-in + custom readiness checks (short TTL cache in runtime.state). */
56
+ export async function evaluateReadiness(
57
+ program: CliProgram,
58
+ surface: "http" | "mcp",
59
+ runtime: ServerRuntime,
60
+ appConfig: AnyAppConfigSnapshot,
61
+ ): Promise<ReadinessResult> {
62
+ const cached = runtime.state.readinessCache;
63
+ if (cached && Date.now() - cached.at < READINESS_CACHE_MS) {
64
+ return cached.result;
65
+ }
66
+
67
+ const ctx: ReadinessContext = { program, surface, appConfig, runtime };
68
+ const checks: Record<string, ReadinessCheck> = {
69
+ config_file: configFileCheck(runtime),
70
+ config_required: configRequiredCheck(program, appConfig),
71
+ custom: await customReadinessCheck(ctx),
72
+ };
73
+ const ok = Object.values(checks).every((c) => c.ok);
74
+ const result: ReadinessResult = { ok, checks };
75
+ runtime.state.readinessCache = { at: Date.now(), result };
76
+ runtime.state.readiness = result;
77
+ return result;
78
+ }
@@ -2,7 +2,7 @@
2
2
  Maps headless respond payloads to native HTTP Response objects.
3
3
  */
4
4
 
5
- import type { CliApiResponseConfig, CliRespondOptions } from "../types.ts";
5
+ import type { CliHttpResponseConfig, CliRespondOptions } from "~/core/types.ts";
6
6
 
7
7
  /** JSON body for a failed HTTP tool invocation. */
8
8
  export interface ApiToolCallErrorBody {
@@ -30,7 +30,7 @@ export function firstErrorLine(text: string): string {
30
30
  /** Wide-open CORS headers applied to all API responses. */
31
31
  export const API_CORS_HEADERS: Readonly<Record<string, string>> = {
32
32
  "access-control-allow-origin": "*",
33
- "access-control-allow-methods": "GET, POST, OPTIONS",
33
+ "access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
34
34
  "access-control-allow-headers": "Content-Type, Authorization",
35
35
  "access-control-max-age": "86400",
36
36
  };
@@ -41,7 +41,10 @@ export function apiOptionsResponse(): Response {
41
41
  }
42
42
 
43
43
  /** Resolves effective Content-Type for a respond payload. */
44
- export function resolveRespondContentType(response: CliRespondOptions, leafApiResponse?: CliApiResponseConfig): string {
44
+ export function resolveRespondContentType(
45
+ response: CliRespondOptions,
46
+ leafApiResponse?: CliHttpResponseConfig,
47
+ ): string {
45
48
  return (
46
49
  response.contentType ??
47
50
  leafApiResponse?.contentType ??
@@ -50,7 +53,11 @@ export function resolveRespondContentType(response: CliRespondOptions, leafApiRe
50
53
  }
51
54
 
52
55
  /** Builds a native HTTP Response from a successful headless respond payload. */
53
- export function apiSuccessResponse(response: CliRespondOptions, leafApiResponse?: CliApiResponseConfig): Response {
56
+ export function apiSuccessResponse(
57
+ response: CliRespondOptions,
58
+ leafApiResponse?: CliHttpResponseConfig,
59
+ defaultStatus?: number,
60
+ ): Response {
54
61
  const contentType = resolveRespondContentType(response, leafApiResponse);
55
62
  const headers: Record<string, string> = {
56
63
  ...API_CORS_HEADERS,
@@ -61,9 +68,13 @@ export function apiSuccessResponse(response: CliRespondOptions, leafApiResponse?
61
68
  headers["content-disposition"] = leafApiResponse.contentDisposition;
62
69
  }
63
70
 
64
- const status = response.status ?? 200;
71
+ const status = response.status ?? defaultStatus ?? 200;
65
72
  const { body } = response;
66
73
 
74
+ if (status === 204) {
75
+ return new Response(null, { status: 204, headers });
76
+ }
77
+
67
78
  if (body instanceof Uint8Array) {
68
79
  return new Response(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer, {
69
80
  status,