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
@@ -2,17 +2,17 @@
2
2
  This module generates Agent Skills content (SKILL.md + reference.md) from a CLI schema.
3
3
  */
4
4
 
5
- import { defaultConfigEntryTitle } from "../config/entry.ts";
6
- import { generateApiGuide } from "../docs/api-guide.ts";
5
+ import { defaultConfigEntryTitle } from "~/config/entry.ts";
6
+ import { collectOptionDefs } from "~/core/parse.ts";
7
+ import { CliOptionKind, type CliProgram } from "~/core/types.ts";
8
+ import { generateCliGuide } from "~/docs/cli-guide.ts";
7
9
  import {
8
10
  collectMcpTools,
9
11
  type McpToolDef,
10
12
  mcpServerId,
11
13
  resolveMcpSchemaUri,
12
14
  sanitizeToolSegment,
13
- } from "../mcp/tools.ts";
14
- import { collectOptionDefs } from "../parse.ts";
15
- import { CliOptionKind, type CliProgram } from "../types.ts";
15
+ } from "~/mcp/tools.ts";
16
16
  import type { SkillTarget } from "./naming.ts";
17
17
  import { skillDirNameForTarget, skillFrontmatterName } from "./naming.ts";
18
18
 
@@ -149,7 +149,7 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
149
149
  "",
150
150
  "## Reference",
151
151
  "",
152
- `For full detail, open \`reference.md\` in this skill directory (same as \`${root.key} docs api\`).`,
152
+ `For full detail, open \`reference.md\` in this skill directory (same as \`${root.key} docs cli\`).`,
153
153
  "",
154
154
  );
155
155
 
@@ -190,9 +190,9 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
190
190
  return lines.join("\n");
191
191
  }
192
192
 
193
- /** Builds reference.md with the full `docs api` markdown guide. */
193
+ /** Builds reference.md with the compact `docs cli` markdown guide. */
194
194
  function buildReferenceMd(root: CliProgram): string {
195
- return generateApiGuide(root);
195
+ return generateCliGuide(root, { compact: true });
196
196
  }
197
197
 
198
198
  /** Builds MCP routing SKILL.md for Claude Code plugin zips. */
package/src/skill/hint.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CliProgram } from "../types.ts";
1
+ import type { CliProgram } from "~/core/types.ts";
2
2
 
3
3
  /** YAML frontmatter block at the start of SKILL.md. */
4
4
  export const MARKDOWN_FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n/;
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { userHome } from "../paths/host.ts";
4
- import type { CliProgram } from "../types.ts";
3
+ import type { CliProgram } from "~/core/types.ts";
4
+ import { userHome } from "~/paths/host.ts";
5
5
  import { generateSkillBundle, type SkillTarget } from "./generate.ts";
6
6
  import { applySkillInstallHints } from "./hint.ts";
7
7
 
@@ -1,7 +1,7 @@
1
1
  /** Agent skill install targets. */
2
2
  export type SkillTarget = "cursor" | "claude" | "codex" | "opencode" | "openclaw";
3
3
 
4
- import { sanitizeToolSegment } from "../mcp/tools.ts";
4
+ import { sanitizeToolSegment } from "~/mcp/tools.ts";
5
5
 
6
6
  /** Kebab-case skill folder + frontmatter name for agents that require hyphen slugs. */
7
7
  export function skillSlug(programKey: string): string {
@@ -1,5 +1,5 @@
1
- import type { CliProgram } from "./types.ts";
2
- import { CliFallbackMode, CliOptionKind } from "./types.ts";
1
+ import type { CliProgram } from "~/core/types.ts";
2
+ import { CliFallbackMode, CliOptionKind } from "~/core/types.ts";
3
3
 
4
4
  export function testProgram(prog: Record<string, unknown> & { key: string; description: string }): CliProgram {
5
5
  return { version: "0.0.0", ...prog } as CliProgram;
@@ -166,6 +166,7 @@ export function nestedDocsFallbackFixture(): CliProgram {
166
166
  return testProgram({
167
167
  key: "app",
168
168
  description: "",
169
+ docs: { enabled: false },
169
170
  commands: [
170
171
  {
171
172
  key: "docs",
@@ -6,9 +6,9 @@ import { expect, test } from "bun:test";
6
6
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { dirname, join } from "node:path";
9
- import { bootstrapAppConfig } from "./config/bootstrap.ts";
10
- import { resolveAppConfigPath } from "./config/file.ts";
11
- import { mcpRequest, testProgram } from "./test-fixtures.ts";
9
+ import { bootstrapAppConfig } from "~/config/bootstrap.ts";
10
+ import { resolveAppConfigPath } from "~/config/file.ts";
11
+ import { mcpRequest, testProgram } from "~/test/fixtures.ts";
12
12
 
13
13
  /** Tests that bootstrapAppConfig prefers host env over config file. */
14
14
  test("bootstrapAppConfig prefers host env over config file", () => {
@@ -124,13 +124,13 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
124
124
  rmSync(dir, { recursive: true, force: true });
125
125
  });
126
126
 
127
- /** Cli.run docs api skips required appConfig exit. */
128
- test("Cli.run docs api skips required appConfig exit", async () => {
127
+ /** Cli.run docs cli skips required appConfig exit. */
128
+ test("Cli.run docs cli skips required appConfig exit", async () => {
129
129
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-docs-skip-"));
130
130
  const configFile = join(dir, ".local", "lib", "docs_skip_test", "config");
131
131
  mkdirSync(dirname(configFile), { recursive: true });
132
132
  writeFileSync(configFile, "{}\n");
133
- const entry = join(import.meta.dir, "index.ts");
133
+ const entry = join(import.meta.dir, "..", "..", "index.ts");
134
134
  const mainPath = join(dir, "run-docs.ts");
135
135
  writeFileSync(
136
136
  mainPath,
@@ -139,7 +139,7 @@ const program = {
139
139
  key: "docs-skip-test",
140
140
  version: "1.0.0",
141
141
  description: "test",
142
- docs: { enabled: true, topics: { readme: { text: "# readme\\n" } } },
142
+ docs: { topics: { readme: { text: "# readme\\n" } } },
143
143
  appConfig: {
144
144
  entries: { token: { description: "Token.", env: "DOCS_SKIP_RUN_TOKEN" } },
145
145
  },
@@ -151,7 +151,7 @@ await new Cli(program).run(process.argv.slice(2));
151
151
  const env = { ...process.env, HOME: dir } as Record<string, string | undefined>;
152
152
  delete env.DOCS_SKIP_RUN_TOKEN;
153
153
  try {
154
- const proc = Bun.spawn(["bun", "run", mainPath, "docs", "api"], {
154
+ const proc = Bun.spawn(["bun", "run", mainPath, "docs", "cli"], {
155
155
  stdout: "pipe",
156
156
  stderr: "pipe",
157
157
  env,
@@ -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
  });