argsbarg 6.1.2 → 6.1.4

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 (230) hide show
  1. package/CHANGELOG.md +74 -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 +30 -31
  23. package/examples/full-example/docs/mcp.md +8 -22
  24. package/examples/full-example/docs/openapi.json +798 -44
  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 +39 -36
  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 +290 -0
  173. package/src/http/readiness.ts +78 -0
  174. package/src/{api → http}/result.ts +22 -11
  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/test/integration/http.test.ts +651 -0
  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/api.integration.test.ts +0 -441
  212. package/src/builtins/api.ts +0 -38
  213. package/src/hidden.ts +0 -30
  214. package/src/install/plan.ts +0 -53
  215. /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
  216. /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
  217. /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
  218. /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
  219. /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
  220. /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
  221. /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
  222. /package/src/{install → configure/artifacts}/normalize.ts +0 -0
  223. /package/src/{install → configure/artifacts}/opts.ts +0 -0
  224. /package/src/{install → configure/artifacts}/shell.ts +0 -0
  225. /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
  226. /package/src/{formats.ts → core/formats.ts} +0 -0
  227. /package/src/{respond.ts → core/respond.ts} +0 -0
  228. /package/src/{types.test.ts → core/types.test.ts} +0 -0
  229. /package/src/{api → http}/schema-deref.test.ts +0 -0
  230. /package/src/{api → http}/schema-deref.ts +0 -0
@@ -1,117 +0,0 @@
1
- /*
2
- Hand-built OpenAPI 3.1 document from exposed MCP tools.
3
- */
4
-
5
- import { collectMcpTools } from "../mcp/tools.ts";
6
- import type { CliProgram } from "../types.ts";
7
- import { dereferenceJsonSchema } from "./schema-deref.ts";
8
-
9
- const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
10
-
11
- /** Resolves the effective API content type for OpenAPI response mapping. */
12
- function effectiveApiContentType(tool: ReturnType<typeof collectMcpTools>[number]): string {
13
- return tool.leaf.apiResponse?.contentType ?? "application/json";
14
- }
15
-
16
- /** Builds an OpenAPI 3.1 response schema for a tool's success payload. */
17
- function buildSuccessResponse(tool: ReturnType<typeof collectMcpTools>[number]): Record<string, unknown> {
18
- const contentType = effectiveApiContentType(tool);
19
- const media: Record<string, unknown> = {};
20
-
21
- if (contentType.includes("application/json")) {
22
- const outputSchema = tool.outputSchema ?? { type: "object" };
23
- media[contentType] = {
24
- schema: dereferenceJsonSchema(outputSchema),
25
- };
26
- } else if (contentType.includes("text/html")) {
27
- media[contentType] = { schema: { type: "string" } };
28
- } else {
29
- media[contentType] = { schema: { type: "string", format: "binary" } };
30
- }
31
-
32
- return {
33
- description: "Successful tool invocation",
34
- content: media,
35
- };
36
- }
37
-
38
- /** Generates an OpenAPI 3.1 document for the program's exposed tools. */
39
- export function generateOpenApi(program: CliProgram): Record<string, unknown> {
40
- const tools = collectMcpTools(program);
41
- const paths: Record<string, unknown> = {};
42
-
43
- for (const tool of tools) {
44
- const pathKey = `/tools/${tool.apiName}`;
45
- paths[pathKey] = {
46
- post: {
47
- operationId: tool.apiName,
48
- summary: tool.description,
49
- requestBody: {
50
- required: false,
51
- content: {
52
- [JSON_CONTENT_TYPE]: {
53
- schema: dereferenceJsonSchema(tool.inputSchema),
54
- },
55
- },
56
- },
57
- responses: {
58
- "200": buildSuccessResponse(tool),
59
- "400": {
60
- description: "Invalid arguments or help requested",
61
- content: {
62
- [JSON_CONTENT_TYPE]: {
63
- schema: {
64
- type: "object",
65
- properties: {
66
- error: { type: "string" },
67
- exitCode: { type: "integer" },
68
- },
69
- required: ["error"],
70
- },
71
- },
72
- },
73
- },
74
- "404": {
75
- description: "Unknown tool",
76
- content: {
77
- [JSON_CONTENT_TYPE]: {
78
- schema: {
79
- type: "object",
80
- properties: { error: { type: "string" } },
81
- required: ["error"],
82
- },
83
- },
84
- },
85
- },
86
- "500": {
87
- description: "Handler error",
88
- content: {
89
- [JSON_CONTENT_TYPE]: {
90
- schema: {
91
- type: "object",
92
- properties: { error: { type: "string" } },
93
- required: ["error"],
94
- },
95
- },
96
- },
97
- },
98
- },
99
- },
100
- };
101
- }
102
-
103
- return {
104
- openapi: "3.1.0",
105
- info: {
106
- title: program.key,
107
- version: program.version,
108
- description: program.description,
109
- },
110
- paths,
111
- };
112
- }
113
-
114
- /** Pretty-printed OpenAPI JSON (same document as `GET /openapi.json`). */
115
- export function openApiJson(program: CliProgram): string {
116
- return `${JSON.stringify(generateOpenApi(program), null, 2)}\n`;
117
- }
package/src/api/server.ts DELETED
@@ -1,120 +0,0 @@
1
- /*
2
- HTTP tool server for ArgsBarg programs: health, OpenAPI, and tool invocation.
3
- */
4
-
5
- import type { Cli } from "../cli.ts";
6
- import {
7
- executeHeadlessToolCall,
8
- headlessFailureToHttpResponse,
9
- headlessSuccessToHttpResponse,
10
- lookupHeadlessTool,
11
- } from "../headless/tool-call.ts";
12
- import type { CliProgram } from "../types.ts";
13
- import { generateOpenApi } from "./openapi.ts";
14
- import { API_CORS_HEADERS, apiDocsHtml, apiErrorResponse, apiOptionsResponse } from "./result.ts";
15
-
16
- const DEFAULT_HOST = "127.0.0.1";
17
- const DEFAULT_PORT = 3000;
18
-
19
- /** Resolved listen address for the HTTP API server. */
20
- export function resolveApiListenAddress(program: CliProgram): { hostname: string; port: number } {
21
- const config = program.apiServer;
22
- return {
23
- hostname: config?.host ?? DEFAULT_HOST,
24
- port: config?.port ?? DEFAULT_PORT,
25
- };
26
- }
27
-
28
- /** Writes a JSON HTTP response with CORS headers. */
29
- function jsonResponse(status: number, body: unknown): Response {
30
- return new Response(JSON.stringify(body), {
31
- status,
32
- headers: {
33
- ...API_CORS_HEADERS,
34
- "content-type": "application/json; charset=utf-8",
35
- },
36
- });
37
- }
38
-
39
- /** Handles one HTTP request for the API server. */
40
- export async function handleApiRequest(cli: Cli, request: Request): Promise<Response> {
41
- if (request.method === "OPTIONS") {
42
- return apiOptionsResponse();
43
- }
44
-
45
- const root = cli.program;
46
- const url = new URL(request.url);
47
- const path = url.pathname;
48
-
49
- if (request.method === "GET" && path === "/health") {
50
- return jsonResponse(200, { ok: true });
51
- }
52
-
53
- if (request.method === "GET" && path === "/openapi.json") {
54
- return jsonResponse(200, generateOpenApi(root));
55
- }
56
-
57
- if (request.method === "GET" && path === "/openapi-browser") {
58
- return new Response(apiDocsHtml(), {
59
- status: 200,
60
- headers: {
61
- ...API_CORS_HEADERS,
62
- "content-type": "text/html; charset=utf-8",
63
- },
64
- });
65
- }
66
-
67
- const toolPathMatch = /^\/tools\/([^/]+)$/.exec(path);
68
- if (request.method === "POST" && toolPathMatch) {
69
- const toolName = decodeURIComponent(toolPathMatch[1] ?? "");
70
- let body: unknown = {};
71
- const rawBody = await request.text();
72
- if (rawBody.trim().length > 0) {
73
- try {
74
- body = JSON.parse(rawBody);
75
- } catch {
76
- return apiErrorResponse(400, { error: "Invalid JSON body" });
77
- }
78
- }
79
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
80
- return apiErrorResponse(400, { error: "Request body must be a JSON object" });
81
- }
82
- return invokeApiTool(cli, toolName, body as Record<string, unknown>);
83
- }
84
-
85
- if (path === "/tools" || path.startsWith("/tools/")) {
86
- return apiErrorResponse(405, { error: "Method not allowed" });
87
- }
88
-
89
- return apiErrorResponse(404, { error: "Not found" });
90
- }
91
-
92
- /** Resolves a tool and runs it through the shared headless invoke path. */
93
- async function invokeApiTool(cli: Cli, toolName: string, args: Record<string, unknown>): Promise<Response> {
94
- const lookup = lookupHeadlessTool(cli.program, toolName, "api");
95
- if (!lookup.ok) {
96
- if (lookup.kind === "unknown") {
97
- return apiErrorResponse(404, { error: lookup.message });
98
- }
99
- return apiErrorResponse(503, { error: lookup.message });
100
- }
101
-
102
- const result = await executeHeadlessToolCall(cli, lookup.tool, args, "api");
103
- if (result.ok) {
104
- return headlessSuccessToHttpResponse(result, lookup.tool.leaf.apiResponse);
105
- }
106
- return headlessFailureToHttpResponse(result);
107
- }
108
-
109
- /** Runs the HTTP API server until the process is interrupted. */
110
- export async function apiServeHttp(cli: Cli): Promise<never> {
111
- const { hostname, port } = resolveApiListenAddress(cli.program);
112
- const server = Bun.serve({
113
- hostname,
114
- port,
115
- fetch: (request) => handleApiRequest(cli, request),
116
- });
117
- process.stderr.write(`HTTP API listening on http://${server.hostname}:${server.port}\n`);
118
- await new Promise<never>(() => {});
119
- throw new Error("HTTP API server stopped unexpectedly");
120
- }
@@ -1,441 +0,0 @@
1
- /*
2
- HTTP API integration tests: routes, tool invocation, CORS, OpenAPI, and validation.
3
- */
4
-
5
- import { describe, expect, test } from "bun:test";
6
- import { join } from "node:path";
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";
14
-
15
- /** Program with HTTP API enabled and handlers that return values. */
16
- function nestedApiFixture() {
17
- return testProgram({
18
- ...nestedMcpFixture,
19
- apiServer: { enabled: true },
20
- commands: [
21
- {
22
- key: "stat",
23
- description: "File metadata.",
24
- options: [
25
- {
26
- name: "json",
27
- description: "Emit handler output as JSON.",
28
- kind: CliOptionKind.Presence,
29
- },
30
- ],
31
- commands: [
32
- {
33
- key: "owner",
34
- description: "Ownership helpers.",
35
- commands: [
36
- {
37
- key: "lookup",
38
- description: "Resolve owner info.",
39
- options: [
40
- {
41
- name: "user-name",
42
- description: "User to look up.",
43
- kind: CliOptionKind.String,
44
- shortName: "u",
45
- },
46
- ],
47
- positionals: [
48
- {
49
- name: "path",
50
- description: "File or directory.",
51
- kind: CliOptionKind.String,
52
- },
53
- ],
54
- handler: (ctx: CliContextType) => {
55
- const user = ctx.stringOpt("user-name") ?? "unknown";
56
- const path = ctx.positional("path") ?? "";
57
- if (ctx.hasFlag("json")) {
58
- return { user, path };
59
- }
60
- return `lookup user=${user} path=${path}`;
61
- },
62
- },
63
- ],
64
- },
65
- ],
66
- },
67
- {
68
- key: "read",
69
- description: "Print the first line of each file.",
70
- positionals: [
71
- {
72
- name: "files",
73
- description: "Paths to read.",
74
- kind: CliOptionKind.String,
75
- argMax: 0,
76
- },
77
- ],
78
- handler: () => ({ lines: [] }),
79
- },
80
- {
81
- key: "pdf",
82
- description: "Return a minimal PDF.",
83
- apiResponse: { contentType: "application/pdf" },
84
- handler: (ctx: CliContextType) => {
85
- ctx.respond({
86
- body: new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34]),
87
- contentType: "application/pdf",
88
- });
89
- },
90
- },
91
- {
92
- key: "html",
93
- description: "Return HTML.",
94
- apiResponse: { contentType: "text/html; charset=utf-8" },
95
- handler: (ctx: CliContextType) => {
96
- ctx.respond({
97
- body: "<!DOCTYPE html><html><body>hi</body></html>",
98
- contentType: "text/html; charset=utf-8",
99
- });
100
- },
101
- },
102
- {
103
- key: "silent",
104
- description: "Returns nothing.",
105
- handler: () => {},
106
- },
107
- ],
108
- });
109
- }
110
-
111
- /** Sends one HTTP request through the in-process API handler. */
112
- async function apiRequest(program: ReturnType<typeof nestedApiFixture>, request: Request) {
113
- const cli = new Cli(program);
114
- return handleApiRequest(cli, request);
115
- }
116
-
117
- describe("apiServer validation", () => {
118
- test("rejects empty apiServer", () => {
119
- const root = testProgram({
120
- key: "app",
121
- description: "",
122
- apiServer: {} as { enabled: boolean },
123
- handler: () => {},
124
- });
125
- expect(() => cliValidateProgram(root)).toThrow(/apiServer requires enabled: true/);
126
- });
127
-
128
- test("rejects top-level command name api when apiServer enabled", () => {
129
- const root = testProgram({
130
- key: "app",
131
- description: "",
132
- apiServer: { enabled: true },
133
- commands: [{ key: "api", description: "user", handler: () => {} }],
134
- });
135
- expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: api/);
136
- });
137
-
138
- test("allows top-level command name api without apiServer", () => {
139
- const root = testProgram({
140
- key: "app",
141
- description: "",
142
- commands: [{ key: "api", description: "user", handler: () => {} }],
143
- });
144
- expect(() => cliValidateProgram(root)).not.toThrow();
145
- });
146
-
147
- test("rejects apiServer on non-root node", () => {
148
- const root = {
149
- key: "app",
150
- version: "0.0.0",
151
- description: "",
152
- commands: [
153
- {
154
- key: "x",
155
- description: "cmd",
156
- apiServer: { enabled: true },
157
- handler: () => {},
158
- },
159
- ],
160
- } as unknown as import("./types.ts").CliProgram;
161
- expect(() => cliValidateProgram(root)).toThrow(/apiServer is only supported on the program root/);
162
- });
163
- });
164
-
165
- describe("HTTP API routes", () => {
166
- const program = nestedApiFixture();
167
- cliValidateProgram(program);
168
-
169
- test("GET /health includes CORS headers", async () => {
170
- const res = await apiRequest(program, new Request("http://127.0.0.1/health"));
171
- expect(res.status).toBe(200);
172
- expect(res.headers.get("access-control-allow-origin")).toBe("*");
173
- expect(await res.json()).toEqual({ ok: true });
174
- });
175
-
176
- 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
- );
181
- expect(res.status).toBe(204);
182
- expect(res.headers.get("access-control-allow-origin")).toBe("*");
183
- expect(res.headers.get("access-control-allow-methods")).toContain("POST");
184
- });
185
-
186
- test("POST /tools/:name returns raw JSON body", async () => {
187
- const readme = join(import.meta.dir, "..", "README.md");
188
- const res = await apiRequest(
189
- program,
190
- new Request("http://127.0.0.1/tools/stat-owner-lookup", {
191
- method: "POST",
192
- headers: { "content-type": "application/json" },
193
- body: JSON.stringify({ "user-name": "alice", path: readme, json: true }),
194
- }),
195
- );
196
- expect(res.status).toBe(200);
197
- expect(res.headers.get("content-type")).toContain("application/json");
198
- expect(await res.json()).toEqual({ user: "alice", path: readme });
199
- });
200
-
201
- test("POST /tools/:name returns raw text body", async () => {
202
- const readme = join(import.meta.dir, "..", "README.md");
203
- const res = await apiRequest(
204
- program,
205
- new Request("http://127.0.0.1/tools/stat-owner-lookup", {
206
- method: "POST",
207
- headers: { "content-type": "application/json" },
208
- body: JSON.stringify({ "user-name": "alice", path: readme }),
209
- }),
210
- );
211
- expect(res.status).toBe(200);
212
- const text = await res.text();
213
- expect(text).toContain("lookup user=alice");
214
- });
215
-
216
- test("POST /tools returns 405", async () => {
217
- const res = await apiRequest(
218
- program,
219
- new Request("http://127.0.0.1/tools", {
220
- method: "POST",
221
- headers: { "content-type": "application/json" },
222
- body: "{}",
223
- }),
224
- );
225
- expect(res.status).toBe(405);
226
- });
227
-
228
- test("POST /tools/:name returns PDF bytes", async () => {
229
- const res = await apiRequest(
230
- program,
231
- new Request("http://127.0.0.1/tools/pdf", {
232
- method: "POST",
233
- headers: { "content-type": "application/json" },
234
- body: "{}",
235
- }),
236
- );
237
- expect(res.status).toBe(200);
238
- expect(res.headers.get("content-type")).toBe("application/pdf");
239
- const bytes = new Uint8Array(await res.arrayBuffer());
240
- expect(String.fromCharCode(...bytes.slice(0, 4))).toBe("%PDF");
241
- });
242
-
243
- test("POST /tools/:name returns HTML", async () => {
244
- const res = await apiRequest(
245
- program,
246
- new Request("http://127.0.0.1/tools/html", {
247
- method: "POST",
248
- body: "{}",
249
- }),
250
- );
251
- expect(res.status).toBe(200);
252
- expect(res.headers.get("content-type")).toContain("text/html");
253
- expect(await res.text()).toContain("<!DOCTYPE html>");
254
- });
255
-
256
- test("POST /tools/:name returns 500 when handler has no response", async () => {
257
- const res = await apiRequest(
258
- program,
259
- new Request("http://127.0.0.1/tools/silent", {
260
- method: "POST",
261
- body: "{}",
262
- }),
263
- );
264
- expect(res.status).toBe(500);
265
- const body = (await res.json()) as { error: string };
266
- expect(body.error).toContain("ctx.respond()");
267
- });
268
-
269
- test("POST /tools returns 404 for unknown tool", async () => {
270
- const res = await apiRequest(
271
- program,
272
- new Request("http://127.0.0.1/tools/missing_tool", {
273
- method: "POST",
274
- headers: { "content-type": "application/json" },
275
- body: "{}",
276
- }),
277
- );
278
- expect(res.status).toBe(404);
279
- });
280
-
281
- test("POST /tools returns 400 for bad args", async () => {
282
- const res = await apiRequest(
283
- program,
284
- new Request("http://127.0.0.1/tools/stat-owner-lookup", {
285
- method: "POST",
286
- headers: { "content-type": "application/json" },
287
- body: JSON.stringify({ "user-name": "alice" }),
288
- }),
289
- );
290
- expect(res.status).toBe(400);
291
- const body = (await res.json()) as { error: string };
292
- expect(body.error).toContain("Missing argument: path");
293
- expect(body).not.toHaveProperty("stderr");
294
- expect(body.error).not.toContain("\u001B[");
295
- });
296
-
297
- test("POST /tools/:name returns plain JSON validation errors", async () => {
298
- const failProgram = testProgram({
299
- key: "app",
300
- description: "Test app",
301
- apiServer: { enabled: true },
302
- commands: [
303
- {
304
- key: "fail",
305
- description: "Fails with cliErrWithHelp.",
306
- handler: (ctx: CliContextType) => {
307
- cliErrWithHelp(ctx, "bad input");
308
- },
309
- },
310
- ],
311
- });
312
- cliValidateProgram(failProgram);
313
- const res = await apiRequest(
314
- failProgram,
315
- new Request("http://127.0.0.1/tools/fail", {
316
- method: "POST",
317
- headers: { "content-type": "application/json" },
318
- body: "{}",
319
- }),
320
- );
321
- expect(res.status).toBe(400);
322
- const body = (await res.json()) as Record<string, unknown>;
323
- expect(body).toEqual({ error: "bad input" });
324
- });
325
-
326
- test("GET /openapi.json lists tool paths", async () => {
327
- const res = await apiRequest(program, new Request("http://127.0.0.1/openapi.json"));
328
- expect(res.status).toBe(200);
329
- const doc = (await res.json()) as { openapi: string; paths: Record<string, unknown> };
330
- expect(doc.openapi).toBe("3.1.0");
331
- expect(doc.paths["/tools/stat-owner-lookup"]).toBeDefined();
332
- });
333
-
334
- test("GET /openapi-browser returns Scalar HTML", async () => {
335
- const res = await apiRequest(program, new Request("http://127.0.0.1/openapi-browser"));
336
- expect(res.status).toBe(200);
337
- expect(res.headers.get("content-type")).toContain("text/html");
338
- const html = await res.text();
339
- expect(html).toContain("@scalar/api-reference");
340
- expect(html).toContain('orderSchemaPropertiesBy: "preserve"');
341
- expect(html).toContain("orderRequiredPropertiesFirst: false");
342
- });
343
- });
344
-
345
- test("generateOpenApi maps binary content types", () => {
346
- const program = nestedApiFixture();
347
- const doc = generateOpenApi(program) as {
348
- paths: Record<string, { post: { responses: { "200": { content: Record<string, unknown> } } } }>;
349
- };
350
- const pdf = doc.paths["/tools/pdf"]?.post.responses["200"].content["application/pdf"] as {
351
- schema: { format: string };
352
- };
353
- expect(pdf.schema.format).toBe("binary");
354
- });
355
-
356
- test("generateOpenApi dereferences nested inputSchema definitions", () => {
357
- const program = testProgram({
358
- key: "app",
359
- description: "Test app",
360
- apiServer: { enabled: true },
361
- commands: [
362
- {
363
- key: "render",
364
- description: "Render a document.",
365
- inputSchema: {
366
- type: "object",
367
- properties: {
368
- invoice: { $ref: "#/definitions/InvoiceData" },
369
- },
370
- definitions: {
371
- InvoiceData: {
372
- type: "object",
373
- properties: {
374
- id: { type: "string" },
375
- },
376
- required: ["id"],
377
- },
378
- },
379
- },
380
- handler: () => ({ ok: true }),
381
- },
382
- ],
383
- });
384
- cliValidateProgram(program);
385
- const doc = generateOpenApi(program) as {
386
- paths: Record<
387
- string,
388
- {
389
- post: {
390
- requestBody: {
391
- content: Record<string, { schema: { properties: { invoice: Record<string, unknown> } } }>;
392
- };
393
- };
394
- }
395
- >;
396
- };
397
- const schema = doc.paths["/tools/render"]?.post.requestBody.content["application/json; charset=utf-8"].schema;
398
- expect(schema.properties.invoice).toEqual({
399
- type: "object",
400
- properties: { id: { type: "string" } },
401
- required: ["id"],
402
- });
403
- });
404
-
405
- test("ctx.respond throws when called twice", () => {
406
- const program = testProgram({
407
- key: "app",
408
- description: "",
409
- handler: () => {},
410
- });
411
- const context = new CliContext("app", [], [], {}, program, "api");
412
- context.respond({ body: { ok: true } });
413
- expect(() => context.respond({ body: { ok: true } })).toThrow(/already called/);
414
- });
415
-
416
- test("API_CORS_HEADERS are wide open", () => {
417
- expect(API_CORS_HEADERS["access-control-allow-origin"]).toBe("*");
418
- });
419
-
420
- test("ctx.invocation is api via Cli.invoke", async () => {
421
- let seen = "";
422
- const root = testProgram({
423
- key: "app",
424
- description: "",
425
- handler: (ctx: CliContextType) => {
426
- seen = ctx.invocation;
427
- return { invocation: ctx.invocation };
428
- },
429
- });
430
- cliValidateProgram(root);
431
- const result = await new Cli(root).invoke([], { invocation: "api" });
432
- expect(result.kind).toBe("ok");
433
- expect(seen).toBe("api");
434
- expect(result.response?.body).toEqual({ invocation: "api" });
435
- });
436
-
437
- test("minimal.ts api without opt-in fails", async () => {
438
- const { stderr, exitCode } = await $`bun run examples/minimal.ts api`.nothrow().quiet();
439
- expect(exitCode).toBe(1);
440
- expect(stderr.toString()).toContain("HTTP API is not available");
441
- });