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
@@ -5,18 +5,21 @@ HTTP API integration tests: routes, tool invocation, CORS, OpenAPI, and validati
5
5
  import { describe, expect, test } from "bun:test";
6
6
  import { join } from "node:path";
7
7
  import { $ } from "bun";
8
- import { generateOpenApi } from "./api/openapi.ts";
9
- import { API_CORS_HEADERS } from "./api/result.ts";
10
- import { handleApiRequest } from "./api/server.ts";
11
- import { Cli, CliContext, type CliContext as CliContextType, CliOptionKind, cliErrWithHelp } from "./index.ts";
12
- import { nestedMcpFixture, testProgram } from "./test-fixtures.ts";
13
- import { cliValidateProgram } from "./validate.ts";
8
+ import { cliValidateProgram } from "~/core/validate.ts";
9
+ import { generateOpenApi } from "~/http/openapi.ts";
10
+ import { API_CORS_HEADERS } from "~/http/result.ts";
11
+ import { handleApiRequest } from "~/http/server.ts";
12
+ import { Cli, CliContext, type CliContext as CliContextType, CliOptionKind, cliErrWithHelp } from "~/index";
13
+ import { LogEmitter } from "~/log/emitter.ts";
14
+ import { createServerRuntime } from "~/server/context.ts";
15
+ import { resolveHttpServeConfig } from "~/server/overrides.ts";
16
+ import { nestedMcpFixture, testProgram } from "~/test/fixtures.ts";
14
17
 
15
18
  /** Program with HTTP API enabled and handlers that return values. */
16
19
  function nestedApiFixture() {
17
20
  return testProgram({
18
21
  ...nestedMcpFixture,
19
- apiServer: { enabled: true },
22
+ httpServer: { enabled: true },
20
23
  commands: [
21
24
  {
22
25
  key: "stat",
@@ -80,7 +83,7 @@ function nestedApiFixture() {
80
83
  {
81
84
  key: "pdf",
82
85
  description: "Return a minimal PDF.",
83
- apiResponse: { contentType: "application/pdf" },
86
+ http: { successContentType: "application/pdf" },
84
87
  handler: (ctx: CliContextType) => {
85
88
  ctx.respond({
86
89
  body: new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34]),
@@ -91,7 +94,7 @@ function nestedApiFixture() {
91
94
  {
92
95
  key: "html",
93
96
  description: "Return HTML.",
94
- apiResponse: { contentType: "text/html; charset=utf-8" },
97
+ http: { successContentType: "text/html; charset=utf-8" },
95
98
  handler: (ctx: CliContextType) => {
96
99
  ctx.respond({
97
100
  body: "<!DOCTYPE html><html><body>hi</body></html>",
@@ -109,42 +112,57 @@ function nestedApiFixture() {
109
112
  }
110
113
 
111
114
  /** Sends one HTTP request through the in-process API handler. */
112
- async function apiRequest(program: ReturnType<typeof nestedApiFixture>, request: Request) {
115
+ async function apiRequest(
116
+ program: ReturnType<typeof nestedApiFixture>,
117
+ request: Request,
118
+ opts?: { withServer?: boolean },
119
+ ) {
113
120
  const cli = new Cli(program);
114
- return handleApiRequest(cli, request);
121
+ if (opts?.withServer) {
122
+ const resolved = resolveHttpServeConfig(program);
123
+ cli.server = {
124
+ runtime: createServerRuntime(program, "http"),
125
+ emitter: new LogEmitter({
126
+ program,
127
+ resolved: { ...resolved.log, access: false },
128
+ }),
129
+ http: resolved,
130
+ };
131
+ }
132
+ return handleApiRequest(cli, request, cli.server?.http);
115
133
  }
116
134
 
117
- describe("apiServer validation", () => {
118
- test("rejects empty apiServer", () => {
135
+ describe("httpServer validation", () => {
136
+ test("rejects empty httpServer", () => {
119
137
  const root = testProgram({
120
138
  key: "app",
121
139
  description: "",
122
- apiServer: {} as { enabled: boolean },
140
+ httpServer: {} as { enabled: boolean },
123
141
  handler: () => {},
124
142
  });
125
- expect(() => cliValidateProgram(root)).toThrow(/apiServer requires enabled: true/);
143
+ expect(() => cliValidateProgram(root)).toThrow(/httpServer requires enabled: true/);
126
144
  });
127
145
 
128
- test("rejects top-level command name api when apiServer enabled", () => {
146
+ test("rejects top-level command name http when httpServer enabled", () => {
129
147
  const root = testProgram({
130
148
  key: "app",
131
149
  description: "",
132
- apiServer: { enabled: true },
133
- commands: [{ key: "api", description: "user", handler: () => {} }],
150
+ httpServer: { enabled: true },
151
+ commands: [{ key: "http", description: "user", handler: () => {} }],
134
152
  });
135
- expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: api/);
153
+ expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: http/);
136
154
  });
137
155
 
138
- test("allows top-level command name api without apiServer", () => {
156
+ test("allows top-level command name http without httpServer", () => {
139
157
  const root = testProgram({
140
158
  key: "app",
141
159
  description: "",
142
- commands: [{ key: "api", description: "user", handler: () => {} }],
160
+ commands: [{ key: "http", description: "user", handler: () => {} }],
143
161
  });
144
162
  expect(() => cliValidateProgram(root)).not.toThrow();
145
163
  });
146
164
 
147
- test("rejects apiServer on non-root node", () => {
165
+ test("rejects httpServer on non-root node", () => {
148
166
  const root = {
149
167
  key: "app",
150
168
  version: "0.0.0",
@@ -153,12 +171,12 @@ describe("apiServer validation", () => {
153
171
  {
154
172
  key: "x",
155
173
  description: "cmd",
156
- apiServer: { enabled: true },
174
+ httpServer: { enabled: true },
157
175
  handler: () => {},
158
176
  },
159
177
  ],
160
- } as unknown as import("./types.ts").CliProgram;
161
- expect(() => cliValidateProgram(root)).toThrow(/apiServer is only supported on the program root/);
178
+ } as unknown as import("~/core/types.ts").CliProgram;
179
+ expect(() => cliValidateProgram(root)).toThrow(/httpServer is only supported on the program root/);
162
180
  });
163
181
  });
164
182
 
@@ -166,6 +184,94 @@ describe("HTTP API routes", () => {
166
184
  const program = nestedApiFixture();
167
185
  cliValidateProgram(program);
168
186
 
187
+ test("GET /health/live returns ok", async () => {
188
+ const res = await apiRequest(program, new Request("http://127.0.0.1/health/live"));
189
+ expect(res.status).toBe(200);
190
+ expect(await res.json()).toEqual({ ok: true });
191
+ });
192
+
193
+ test("GET /health/ready returns ok when healthy", async () => {
194
+ const res = await apiRequest(program, new Request("http://127.0.0.1/health/ready"), { withServer: true });
195
+ expect(res.status).toBe(200);
196
+ const body = (await res.json()) as { ok: boolean; checks: Record<string, { ok: boolean }> };
197
+ expect(body.ok).toBe(true);
198
+ expect(body.checks.config_file.ok).toBe(true);
199
+ expect(body.checks.config_required.ok).toBe(true);
200
+ });
201
+
202
+ test("GET /health/ready returns 503 when custom readiness fails", async () => {
203
+ const failProgram = testProgram({
204
+ key: "app",
205
+ description: "Test",
206
+ version: "1.0.0",
207
+ httpServer: { enabled: true },
208
+ readiness: () => false,
209
+ handler: () => ({ ok: true }),
210
+ });
211
+ cliValidateProgram(failProgram);
212
+ const res = await apiRequest(failProgram, new Request("http://127.0.0.1/health/ready"), { withServer: true });
213
+ expect(res.status).toBe(503);
214
+ const body = (await res.json()) as { ok: boolean; checks: { custom: { ok: boolean } } };
215
+ expect(body.ok).toBe(false);
216
+ expect(body.checks.custom.ok).toBe(false);
217
+ });
218
+
219
+ test("POST /api returns 500 for non-Error throw", async () => {
220
+ const throwProgram = testProgram({
221
+ key: "app",
222
+ description: "Test",
223
+ httpServer: { enabled: true },
224
+ commands: [
225
+ {
226
+ key: "boom",
227
+ description: "Throws non-Error",
228
+ handler: () => {
229
+ throw "unexpected";
230
+ },
231
+ },
232
+ ],
233
+ });
234
+ cliValidateProgram(throwProgram);
235
+ const res = await apiRequest(
236
+ throwProgram,
237
+ new Request("http://127.0.0.1/api/boom", { method: "POST", body: "{}" }),
238
+ );
239
+ expect(res.status).toBe(500);
240
+ });
241
+
242
+ test("POST /api obscures unexpected errors when configured", async () => {
243
+ const throwProgram = testProgram({
244
+ key: "app",
245
+ description: "Test",
246
+ httpServer: { enabled: true, errors: { obscureUnexpected: true } },
247
+ commands: [
248
+ {
249
+ key: "boom",
250
+ description: "Throws non-Error",
251
+ handler: () => {
252
+ throw "secret";
253
+ },
254
+ },
255
+ ],
256
+ });
257
+ cliValidateProgram(throwProgram);
258
+ const cli = new Cli(throwProgram);
259
+ const resolved = resolveHttpServeConfig(throwProgram);
260
+ cli.server = {
261
+ runtime: createServerRuntime(throwProgram, "http"),
262
+ emitter: new LogEmitter({ program: throwProgram, resolved: { ...resolved.log, access: false } }),
263
+ http: resolved,
264
+ };
265
+ const res = await handleApiRequest(
266
+ cli,
267
+ new Request("http://127.0.0.1/api/boom", { method: "POST", body: "{}" }),
268
+ resolved,
269
+ );
270
+ expect(res.status).toBe(500);
271
+ const body = (await res.json()) as { error: string };
272
+ expect(body.error).toBe("An unexpected error occurred.");
273
+ });
274
+
169
275
  test("GET /health includes CORS headers", async () => {
170
276
  const res = await apiRequest(program, new Request("http://127.0.0.1/health"));
171
277
  expect(res.status).toBe(200);
@@ -174,46 +280,43 @@ describe("HTTP API routes", () => {
174
280
  });
175
281
 
176
282
  test("OPTIONS returns 204 with CORS headers", async () => {
177
- const res = await apiRequest(
178
- program,
179
- new Request("http://127.0.0.1/tools/stat-owner-lookup", { method: "OPTIONS" }),
180
- );
283
+ const res = await apiRequest(program, new Request("http://127.0.0.1/api/stat/owner/lookup", { method: "OPTIONS" }));
181
284
  expect(res.status).toBe(204);
182
285
  expect(res.headers.get("access-control-allow-origin")).toBe("*");
183
286
  expect(res.headers.get("access-control-allow-methods")).toContain("POST");
184
287
  });
185
288
 
186
- test("POST /tools/:name returns raw JSON body", async () => {
187
- const readme = join(import.meta.dir, "..", "README.md");
289
+ test("POST /api/... returns raw JSON body with 201", async () => {
290
+ const readme = join(import.meta.dir, "..", "..", "..", "README.md");
188
291
  const res = await apiRequest(
189
292
  program,
190
- new Request("http://127.0.0.1/tools/stat-owner-lookup", {
293
+ new Request("http://127.0.0.1/api/stat/owner/lookup", {
191
294
  method: "POST",
192
295
  headers: { "content-type": "application/json" },
193
296
  body: JSON.stringify({ "user-name": "alice", path: readme, json: true }),
194
297
  }),
195
298
  );
196
- expect(res.status).toBe(200);
299
+ expect(res.status).toBe(201);
197
300
  expect(res.headers.get("content-type")).toContain("application/json");
198
301
  expect(await res.json()).toEqual({ user: "alice", path: readme });
199
302
  });
200
303
 
201
- test("POST /tools/:name returns raw text body", async () => {
202
- const readme = join(import.meta.dir, "..", "README.md");
304
+ test("POST /api/... returns raw text body with 201", async () => {
305
+ const readme = join(import.meta.dir, "..", "..", "..", "README.md");
203
306
  const res = await apiRequest(
204
307
  program,
205
- new Request("http://127.0.0.1/tools/stat-owner-lookup", {
308
+ new Request("http://127.0.0.1/api/stat/owner/lookup", {
206
309
  method: "POST",
207
310
  headers: { "content-type": "application/json" },
208
311
  body: JSON.stringify({ "user-name": "alice", path: readme }),
209
312
  }),
210
313
  );
211
- expect(res.status).toBe(200);
314
+ expect(res.status).toBe(201);
212
315
  const text = await res.text();
213
316
  expect(text).toContain("lookup user=alice");
214
317
  });
215
318
 
216
- test("POST /tools returns 405", async () => {
319
+ test("POST /tools returns 404 (legacy path removed)", async () => {
217
320
  const res = await apiRequest(
218
321
  program,
219
322
  new Request("http://127.0.0.1/tools", {
@@ -222,41 +325,41 @@ describe("HTTP API routes", () => {
222
325
  body: "{}",
223
326
  }),
224
327
  );
225
- expect(res.status).toBe(405);
328
+ expect(res.status).toBe(404);
226
329
  });
227
330
 
228
- test("POST /tools/:name returns PDF bytes", async () => {
331
+ test("POST /api/pdf returns PDF bytes with 201", async () => {
229
332
  const res = await apiRequest(
230
333
  program,
231
- new Request("http://127.0.0.1/tools/pdf", {
334
+ new Request("http://127.0.0.1/api/pdf", {
232
335
  method: "POST",
233
336
  headers: { "content-type": "application/json" },
234
337
  body: "{}",
235
338
  }),
236
339
  );
237
- expect(res.status).toBe(200);
340
+ expect(res.status).toBe(201);
238
341
  expect(res.headers.get("content-type")).toBe("application/pdf");
239
342
  const bytes = new Uint8Array(await res.arrayBuffer());
240
343
  expect(String.fromCharCode(...bytes.slice(0, 4))).toBe("%PDF");
241
344
  });
242
345
 
243
- test("POST /tools/:name returns HTML", async () => {
346
+ test("POST /api/html returns HTML with 201", async () => {
244
347
  const res = await apiRequest(
245
348
  program,
246
- new Request("http://127.0.0.1/tools/html", {
349
+ new Request("http://127.0.0.1/api/html", {
247
350
  method: "POST",
248
351
  body: "{}",
249
352
  }),
250
353
  );
251
- expect(res.status).toBe(200);
354
+ expect(res.status).toBe(201);
252
355
  expect(res.headers.get("content-type")).toContain("text/html");
253
356
  expect(await res.text()).toContain("<!DOCTYPE html>");
254
357
  });
255
358
 
256
- test("POST /tools/:name returns 500 when handler has no response", async () => {
359
+ test("POST /api/silent returns 500 when handler has no response", async () => {
257
360
  const res = await apiRequest(
258
361
  program,
259
- new Request("http://127.0.0.1/tools/silent", {
362
+ new Request("http://127.0.0.1/api/silent", {
260
363
  method: "POST",
261
364
  body: "{}",
262
365
  }),
@@ -266,10 +369,10 @@ describe("HTTP API routes", () => {
266
369
  expect(body.error).toContain("ctx.respond()");
267
370
  });
268
371
 
269
- test("POST /tools returns 404 for unknown tool", async () => {
372
+ test("POST /api returns 404 for unknown route", async () => {
270
373
  const res = await apiRequest(
271
374
  program,
272
- new Request("http://127.0.0.1/tools/missing_tool", {
375
+ new Request("http://127.0.0.1/api/missing_tool", {
273
376
  method: "POST",
274
377
  headers: { "content-type": "application/json" },
275
378
  body: "{}",
@@ -278,10 +381,10 @@ describe("HTTP API routes", () => {
278
381
  expect(res.status).toBe(404);
279
382
  });
280
383
 
281
- test("POST /tools returns 400 for bad args", async () => {
384
+ test("POST /api returns 400 for bad args", async () => {
282
385
  const res = await apiRequest(
283
386
  program,
284
- new Request("http://127.0.0.1/tools/stat-owner-lookup", {
387
+ new Request("http://127.0.0.1/api/stat/owner/lookup", {
285
388
  method: "POST",
286
389
  headers: { "content-type": "application/json" },
287
390
  body: JSON.stringify({ "user-name": "alice" }),
@@ -294,11 +397,11 @@ describe("HTTP API routes", () => {
294
397
  expect(body.error).not.toContain("\u001B[");
295
398
  });
296
399
 
297
- test("POST /tools/:name returns plain JSON validation errors", async () => {
400
+ test("POST /api/... returns plain JSON validation errors", async () => {
298
401
  const failProgram = testProgram({
299
402
  key: "app",
300
403
  description: "Test app",
301
- apiServer: { enabled: true },
404
+ httpServer: { enabled: true },
302
405
  commands: [
303
406
  {
304
407
  key: "fail",
@@ -312,7 +415,7 @@ describe("HTTP API routes", () => {
312
415
  cliValidateProgram(failProgram);
313
416
  const res = await apiRequest(
314
417
  failProgram,
315
- new Request("http://127.0.0.1/tools/fail", {
418
+ new Request("http://127.0.0.1/api/fail", {
316
419
  method: "POST",
317
420
  headers: { "content-type": "application/json" },
318
421
  body: "{}",
@@ -323,12 +426,12 @@ describe("HTTP API routes", () => {
323
426
  expect(body).toEqual({ error: "bad input" });
324
427
  });
325
428
 
326
- test("GET /openapi.json lists tool paths", async () => {
429
+ test("GET /openapi.json lists REST paths", async () => {
327
430
  const res = await apiRequest(program, new Request("http://127.0.0.1/openapi.json"));
328
431
  expect(res.status).toBe(200);
329
432
  const doc = (await res.json()) as { openapi: string; paths: Record<string, unknown> };
330
433
  expect(doc.openapi).toBe("3.1.0");
331
- expect(doc.paths["/tools/stat-owner-lookup"]).toBeDefined();
434
+ expect(doc.paths["/api/stat/owner/lookup"]).toBeDefined();
332
435
  });
333
436
 
334
437
  test("GET /openapi-browser returns Scalar HTML", async () => {
@@ -345,9 +448,9 @@ describe("HTTP API routes", () => {
345
448
  test("generateOpenApi maps binary content types", () => {
346
449
  const program = nestedApiFixture();
347
450
  const doc = generateOpenApi(program) as {
348
- paths: Record<string, { post: { responses: { "200": { content: Record<string, unknown> } } } }>;
451
+ paths: Record<string, { post: { responses: { "201": { content: Record<string, unknown> } } } }>;
349
452
  };
350
- const pdf = doc.paths["/tools/pdf"]?.post.responses["200"].content["application/pdf"] as {
453
+ const pdf = doc.paths["/api/pdf"]?.post.responses["201"].content["application/pdf"] as {
351
454
  schema: { format: string };
352
455
  };
353
456
  expect(pdf.schema.format).toBe("binary");
@@ -357,7 +460,7 @@ test("generateOpenApi dereferences nested inputSchema definitions", () => {
357
460
  const program = testProgram({
358
461
  key: "app",
359
462
  description: "Test app",
360
- apiServer: { enabled: true },
463
+ httpServer: { enabled: true },
361
464
  commands: [
362
465
  {
363
466
  key: "render",
@@ -394,7 +497,7 @@ test("generateOpenApi dereferences nested inputSchema definitions", () => {
394
497
  }
395
498
  >;
396
499
  };
397
- const schema = doc.paths["/tools/render"]?.post.requestBody.content["application/json; charset=utf-8"].schema;
500
+ const schema = doc.paths["/api/render"]?.post.requestBody.content["application/json; charset=utf-8"].schema;
398
501
  expect(schema.properties.invoice).toEqual({
399
502
  type: "object",
400
503
  properties: { id: { type: "string" } },
@@ -408,7 +511,7 @@ test("ctx.respond throws when called twice", () => {
408
511
  description: "",
409
512
  handler: () => {},
410
513
  });
411
- const context = new CliContext("app", [], [], {}, program, "api");
514
+ const context = new CliContext("app", [], [], {}, program, "http");
412
515
  context.respond({ body: { ok: true } });
413
516
  expect(() => context.respond({ body: { ok: true } })).toThrow(/already called/);
414
517
  });
@@ -417,7 +520,7 @@ test("API_CORS_HEADERS are wide open", () => {
417
520
  expect(API_CORS_HEADERS["access-control-allow-origin"]).toBe("*");
418
521
  });
419
522
 
420
- test("ctx.invocation is api via Cli.invoke", async () => {
523
+ test("ctx.invocation is http via Cli.invoke", async () => {
421
524
  let seen = "";
422
525
  const root = testProgram({
423
526
  key: "app",
@@ -428,14 +531,14 @@ test("ctx.invocation is api via Cli.invoke", async () => {
428
531
  },
429
532
  });
430
533
  cliValidateProgram(root);
431
- const result = await new Cli(root).invoke([], { invocation: "api" });
534
+ const result = await new Cli(root).invoke([], { invocation: "http" });
432
535
  expect(result.kind).toBe("ok");
433
- expect(seen).toBe("api");
434
- expect(result.response?.body).toEqual({ invocation: "api" });
536
+ expect(seen).toBe("http");
537
+ expect(result.response?.body).toEqual({ invocation: "http" });
435
538
  });
436
539
 
437
- test("minimal.ts api without opt-in fails", async () => {
438
- const { stderr, exitCode } = await $`bun run examples/minimal.ts api`.nothrow().quiet();
540
+ test("minimal.ts http without opt-in fails", async () => {
541
+ const { stderr, exitCode } = await $`bun run examples/minimal.ts http`.nothrow().quiet();
439
542
  expect(exitCode).toBe(1);
440
543
  expect(stderr.toString()).toContain("HTTP API is not available");
441
544
  });
@@ -5,19 +5,18 @@ Domain-specific regression tests (split from index.test.ts).
5
5
  import { expect, test } from "bun:test";
6
6
  import { join } from "node:path";
7
7
  import { $ } from "bun";
8
- import type { CliProgram } from "./index.ts";
9
- import { buildToolCallSuccessFromResponse } from "./mcp/result.ts";
8
+ import { cliSchemaExport } from "~/core/schema.ts";
9
+ import { cliValidateProgram } from "~/core/validate.ts";
10
+ import type { CliProgram } from "~/index";
11
+ import { buildToolCallSuccessFromResponse } from "~/mcp/result.ts";
10
12
  import {
11
- apiToolName,
12
13
  collectMcpTools,
13
14
  mcpToolCallToArgv,
14
15
  mcpToolDescription,
15
16
  mcpToolName,
16
17
  sanitizeToolSegment,
17
- } from "./mcp/tools.ts";
18
- import { cliSchemaExport } from "./schema.ts";
19
- import { mcpRequest, nestedMcpFixture, testProgram } from "./test-fixtures.ts";
20
- import { cliValidateProgram } from "./validate.ts";
18
+ } from "~/mcp/tools.ts";
19
+ import { mcpRequest, nestedMcpFixture, testProgram } from "~/test/fixtures.ts";
21
20
 
22
21
  test("sanitizeToolSegment normalizes dotted app keys", () => {
23
22
  expect(sanitizeToolSegment("minimal.ts")).toBe("minimal_ts");
@@ -31,10 +30,8 @@ test("mcpToolDescription formats CLI path and root-leaf prefix", () => {
31
30
  expect(mcpToolDescription([], "helloapp", "Tiny demo.")).toBe("helloapp — Tiny demo.");
32
31
  });
33
32
 
34
- test("apiToolName hyphen-joins path; mcpToolName sanitizes to underscores", () => {
35
- expect(apiToolName(nestedMcpFixture, ["stat", "owner", "lookup"])).toBe("stat-owner-lookup");
33
+ test("mcpToolName sanitizes path segments to underscores", () => {
36
34
  expect(mcpToolName(nestedMcpFixture, ["stat", "owner", "lookup"])).toBe("stat_owner_lookup");
37
- expect(apiToolName(nestedMcpFixture, ["render-invoice"])).toBe("render-invoice");
38
35
  expect(mcpToolName(nestedMcpFixture, ["render-invoice"])).toBe("render_invoice");
39
36
  });
40
37
 
@@ -43,7 +40,6 @@ test("collectMcpTools lists user leaf commands only", () => {
43
40
  const names = tools.map((t) => t.name);
44
41
  expect(names).toContain("stat_owner_lookup");
45
42
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
46
- expect(lookup.apiName).toBe("stat-owner-lookup");
47
43
  expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
48
44
  expect(names).toContain("read");
49
45
  expect(names).not.toContain("hidden");
@@ -104,13 +100,13 @@ test("collectMcpTools resolves {argsbarg:program} in appended notes", () => {
104
100
  {
105
101
  key: "run",
106
102
  description: "Run.",
107
- notes: "See `{argsbarg:program} docs api`.",
103
+ notes: "See `{argsbarg:program} docs cli`.",
108
104
  handler: () => {},
109
105
  },
110
106
  ],
111
107
  });
112
108
  const tools = collectMcpTools(root);
113
- expect(tools[0]?.description).toContain("See `myapp docs api`.");
109
+ expect(tools[0]?.description).toContain("See `myapp docs cli`.");
114
110
  });
115
111
 
116
112
  /** CliSchemaExport includes leaf outputSchema. */
@@ -139,29 +135,6 @@ test("cliSchemaExport includes leaf outputSchema", () => {
139
135
  });
140
136
  });
141
137
 
142
- /** CliSchemaExport accepts legacy mcpTool.outputSchema. */
143
- test("cliSchemaExport accepts legacy mcpTool.outputSchema", () => {
144
- const root = testProgram({
145
- key: "app",
146
- version: "1.0.0",
147
- description: "Schema export demo.",
148
- commands: [
149
- {
150
- key: "run",
151
- description: "Run.",
152
- mcpTool: {
153
- outputSchema: { type: "object", properties: { id: { type: "string" } } },
154
- },
155
- handler: () => {},
156
- },
157
- ],
158
- });
159
- expect(cliSchemaExport(root).commands?.[0]?.outputSchema).toEqual({
160
- type: "object",
161
- properties: { id: { type: "string" } },
162
- });
163
- });
164
-
165
138
  /** Tests that outputSchema must be a JSON Schema object. */
166
139
  test("outputSchema must be a JSON Schema object", () => {
167
140
  const root = testProgram({
@@ -180,25 +153,6 @@ test("outputSchema must be a JSON Schema object", () => {
180
153
  expect(() => cliValidateProgram(root)).toThrow(/outputSchema must be a JSON Schema object/);
181
154
  });
182
155
 
183
- /** Tests that outputSchema cannot be set on both leaf and mcpTool. */
184
- test("outputSchema cannot be set on both leaf and mcpTool", () => {
185
- const root = testProgram({
186
- key: "app",
187
- version: "1.0.0",
188
- description: "Duplicate output schema.",
189
- commands: [
190
- {
191
- key: "run",
192
- description: "Run.",
193
- outputSchema: { type: "object" },
194
- mcpTool: { outputSchema: { type: "object" } },
195
- handler: () => {},
196
- },
197
- ],
198
- });
199
- expect(() => cliValidateProgram(root)).toThrow(/Set outputSchema on the leaf only/);
200
- });
201
-
202
156
  test("collectMcpTools merges parent options into inputSchema", () => {
203
157
  const tools = collectMcpTools(nestedMcpFixture);
204
158
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
@@ -419,7 +373,7 @@ test("MCP resources/read returns schema JSON", async () => {
419
373
 
420
374
  /** MCP tools/call runs stat_owner_lookup. */
421
375
  test("MCP tools/call runs stat_owner_lookup", async () => {
422
- const readme = join(import.meta.dir, "..", "README.md");
376
+ const readme = join(import.meta.dir, "..", "..", "..", "README.md");
423
377
  const responses = await mcpRequest([
424
378
  {
425
379
  jsonrpc: "2.0",
@@ -440,7 +394,7 @@ test("MCP tools/call runs stat_owner_lookup", async () => {
440
394
 
441
395
  /** MCP tools/call returns structuredContent for JSON stdout. */
442
396
  test("MCP tools/call returns structuredContent for JSON stdout", async () => {
443
- const readme = join(import.meta.dir, "..", "README.md");
397
+ const readme = join(import.meta.dir, "..", "..", "..", "README.md");
444
398
  const responses = await mcpRequest([
445
399
  {
446
400
  jsonrpc: "2.0",