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
@@ -0,0 +1,78 @@
1
+ /*
2
+ HTTP/MCP readiness checks for GET /health/ready (orchestrator probes only).
3
+ */
4
+
5
+ import type { AnyAppConfigSnapshot } from "~/config/context.ts";
6
+ import { missingRequiredConfig } from "~/config/resolve.ts";
7
+ import type { CliProgram, ReadinessContext, ServerRuntime } from "~/core/types.ts";
8
+
9
+ const READINESS_CACHE_MS = 3000;
10
+
11
+ export interface ReadinessCheck {
12
+ ok: boolean;
13
+ error?: string;
14
+ missing?: string[];
15
+ }
16
+
17
+ export interface ReadinessResult {
18
+ ok: boolean;
19
+ checks: Record<string, ReadinessCheck>;
20
+ }
21
+
22
+ function configFileCheck(runtime: ServerRuntime): ReadinessCheck {
23
+ const err = runtime.state.configFileError;
24
+ if (typeof err === "string" && err.length > 0) {
25
+ return { ok: false, error: err };
26
+ }
27
+ return { ok: true };
28
+ }
29
+
30
+ function configRequiredCheck(program: CliProgram, appConfig: AnyAppConfigSnapshot): ReadinessCheck {
31
+ if (!program.appConfig) {
32
+ return { ok: true };
33
+ }
34
+ const missing = missingRequiredConfig(program, appConfig.read());
35
+ if (missing.length > 0) {
36
+ return { ok: false, missing };
37
+ }
38
+ return { ok: true };
39
+ }
40
+
41
+ async function customReadinessCheck(ctx: ReadinessContext): Promise<ReadinessCheck> {
42
+ const fn = ctx.program.readiness;
43
+ if (!fn) {
44
+ return { ok: true };
45
+ }
46
+ try {
47
+ const ok = await Promise.resolve(fn(ctx));
48
+ return ok ? { ok: true } : { ok: false, error: "readiness check returned false" };
49
+ } catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ return { ok: false, error: message };
52
+ }
53
+ }
54
+
55
+ /** Runs built-in + custom readiness checks (short TTL cache in runtime.state). */
56
+ export async function evaluateReadiness(
57
+ program: CliProgram,
58
+ surface: "http" | "mcp",
59
+ runtime: ServerRuntime,
60
+ appConfig: AnyAppConfigSnapshot,
61
+ ): Promise<ReadinessResult> {
62
+ const cached = runtime.state.readinessCache;
63
+ if (cached && Date.now() - cached.at < READINESS_CACHE_MS) {
64
+ return cached.result;
65
+ }
66
+
67
+ const ctx: ReadinessContext = { program, surface, appConfig, runtime };
68
+ const checks: Record<string, ReadinessCheck> = {
69
+ config_file: configFileCheck(runtime),
70
+ config_required: configRequiredCheck(program, appConfig),
71
+ custom: await customReadinessCheck(ctx),
72
+ };
73
+ const ok = Object.values(checks).every((c) => c.ok);
74
+ const result: ReadinessResult = { ok, checks };
75
+ runtime.state.readinessCache = { at: Date.now(), result };
76
+ runtime.state.readiness = result;
77
+ return result;
78
+ }
@@ -2,7 +2,7 @@
2
2
  Maps headless respond payloads to native HTTP Response objects.
3
3
  */
4
4
 
5
- import type { CliApiResponseConfig, CliRespondOptions } from "../types.ts";
5
+ import type { CliHttpResponseConfig, CliRespondOptions } from "~/core/types.ts";
6
6
 
7
7
  /** JSON body for a failed HTTP tool invocation. */
8
8
  export interface ApiToolCallErrorBody {
@@ -30,7 +30,7 @@ export function firstErrorLine(text: string): string {
30
30
  /** Wide-open CORS headers applied to all API responses. */
31
31
  export const API_CORS_HEADERS: Readonly<Record<string, string>> = {
32
32
  "access-control-allow-origin": "*",
33
- "access-control-allow-methods": "GET, POST, OPTIONS",
33
+ "access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
34
34
  "access-control-allow-headers": "Content-Type, Authorization",
35
35
  "access-control-max-age": "86400",
36
36
  };
@@ -41,7 +41,10 @@ export function apiOptionsResponse(): Response {
41
41
  }
42
42
 
43
43
  /** Resolves effective Content-Type for a respond payload. */
44
- export function resolveRespondContentType(response: CliRespondOptions, leafApiResponse?: CliApiResponseConfig): string {
44
+ export function resolveRespondContentType(
45
+ response: CliRespondOptions,
46
+ leafApiResponse?: CliHttpResponseConfig,
47
+ ): string {
45
48
  return (
46
49
  response.contentType ??
47
50
  leafApiResponse?.contentType ??
@@ -50,7 +53,11 @@ export function resolveRespondContentType(response: CliRespondOptions, leafApiRe
50
53
  }
51
54
 
52
55
  /** Builds a native HTTP Response from a successful headless respond payload. */
53
- export function apiSuccessResponse(response: CliRespondOptions, leafApiResponse?: CliApiResponseConfig): Response {
56
+ export function apiSuccessResponse(
57
+ response: CliRespondOptions,
58
+ leafApiResponse?: CliHttpResponseConfig,
59
+ defaultStatus?: number,
60
+ ): Response {
54
61
  const contentType = resolveRespondContentType(response, leafApiResponse);
55
62
  const headers: Record<string, string> = {
56
63
  ...API_CORS_HEADERS,
@@ -61,9 +68,13 @@ export function apiSuccessResponse(response: CliRespondOptions, leafApiResponse?
61
68
  headers["content-disposition"] = leafApiResponse.contentDisposition;
62
69
  }
63
70
 
64
- const status = response.status ?? 200;
71
+ const status = response.status ?? defaultStatus ?? 200;
65
72
  const { body } = response;
66
73
 
74
+ if (status === 204) {
75
+ return new Response(null, { status: 204, headers });
76
+ }
77
+
67
78
  if (body instanceof Uint8Array) {
68
79
  return new Response(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer, {
69
80
  status,
@@ -87,7 +98,7 @@ export function apiErrorResponse(status: number, body: ApiToolCallErrorBody): Re
87
98
  });
88
99
  }
89
100
 
90
- /** Scalar API reference HTML served at GET /openapi-browser. */
101
+ /** Swagger UI HTML served at GET /swagger. */
91
102
  export function apiDocsHtml(): string {
92
103
  return `<!doctype html>
93
104
  <html lang="en">
@@ -95,15 +106,15 @@ export function apiDocsHtml(): string {
95
106
  <meta charset="utf-8" />
96
107
  <meta name="viewport" content="width=device-width, initial-scale=1" />
97
108
  <title>API Reference</title>
109
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" />
98
110
  </head>
99
111
  <body>
100
- <div id="api-reference"></div>
101
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
112
+ <div id="swagger-ui"></div>
113
+ <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
102
114
  <script>
103
- Scalar.createApiReference("#api-reference", {
115
+ SwaggerUIBundle({
104
116
  url: "/openapi.json",
105
- orderSchemaPropertiesBy: "preserve",
106
- orderRequiredPropertiesFirst: false,
117
+ dom_id: "#swagger-ui",
107
118
  });
108
119
  </script>
109
120
  </body>
@@ -0,0 +1,329 @@
1
+ /*
2
+ HTTP REST route collection and request matching from the CLI command tree.
3
+ */
4
+
5
+ import { collectOptionDefs } from "~/core/parse.ts";
6
+ import {
7
+ type CliHttpMethod,
8
+ type CliLeaf,
9
+ type CliNode,
10
+ type CliProgram,
11
+ isCliLeaf,
12
+ isJsonLeaf,
13
+ CliOptionKind as OptKind,
14
+ } from "~/core/types.ts";
15
+ import { formatMcpOptionValue } from "~/mcp/tools.ts";
16
+ import { isHttpDisabled, isHttpHidden } from "~/runtime/exposure.ts";
17
+
18
+ const VERB_KEYS = new Set(["get", "post", "put", "patch", "delete"]);
19
+
20
+ /** One HTTP route derived from a user leaf command. */
21
+ export interface HttpRouteDef {
22
+ method: CliHttpMethod;
23
+ /** OpenAPI-style path e.g. `/api/workspaces/{id}`. */
24
+ openApiPath: string;
25
+ /** Regex matching pathname (no query). */
26
+ pathPattern: RegExp;
27
+ /** Argv command path; `:id` tokens replaced at request time. */
28
+ commandPath: string[];
29
+ /** Param names in URL order (without `:`). */
30
+ paramNames: string[];
31
+ leaf: CliLeaf;
32
+ }
33
+
34
+ function isParamRouterKey(key: string): boolean {
35
+ return key.startsWith(":");
36
+ }
37
+
38
+ function inferHttpMethod(leaf: CliLeaf): CliHttpMethod {
39
+ if (leaf.http?.method) {
40
+ return leaf.http.method;
41
+ }
42
+ const lower = leaf.key.toLowerCase();
43
+ if (VERB_KEYS.has(lower)) {
44
+ return lower.toUpperCase() as CliHttpMethod;
45
+ }
46
+ return "POST";
47
+ }
48
+
49
+ function isVerbLeaf(leaf: CliLeaf): boolean {
50
+ return VERB_KEYS.has(leaf.key.toLowerCase()) && leaf.http?.method === undefined;
51
+ }
52
+
53
+ function segmentForNode(node: CliNode): string {
54
+ return node.http?.segment ?? node.key;
55
+ }
56
+
57
+ function leafHttpExposed(leaf: CliLeaf): boolean {
58
+ if (isHttpDisabled(leaf) || isHttpHidden(leaf)) {
59
+ return false;
60
+ }
61
+ return true;
62
+ }
63
+
64
+ type WalkState = {
65
+ urlSegments: string[];
66
+ commandPath: string[];
67
+ paramNames: string[];
68
+ };
69
+
70
+ function pushRoute(routes: HttpRouteDef[], leaf: CliLeaf, state: WalkState): void {
71
+ const urlSegments = [...state.urlSegments];
72
+ const commandPath = [...state.commandPath];
73
+ if (!isVerbLeaf(leaf)) {
74
+ const seg = segmentForNode(leaf);
75
+ if (urlSegments[urlSegments.length - 1] !== seg) {
76
+ urlSegments.push(seg);
77
+ }
78
+ if (commandPath[commandPath.length - 1] !== leaf.key) {
79
+ commandPath.push(leaf.key);
80
+ }
81
+ }
82
+ const openApiPath = `/api/${urlSegments.map((s) => (s.startsWith(":") ? `{${s.slice(1)}}` : s)).join("/")}`;
83
+ const patternParts = urlSegments.map((s) => (s.startsWith(":") ? "([^/]+)" : escapeRegex(s)));
84
+ const pathPattern = new RegExp(`^/api/${patternParts.join("/")}/?$`);
85
+ routes.push({
86
+ method: inferHttpMethod(leaf),
87
+ openApiPath,
88
+ pathPattern,
89
+ commandPath,
90
+ paramNames: [...state.paramNames],
91
+ leaf,
92
+ });
93
+ }
94
+
95
+ function walk(node: CliNode, state: WalkState, routes: HttpRouteDef[]): void {
96
+ if (isHttpDisabled(node) || isHttpHidden(node)) {
97
+ return;
98
+ }
99
+
100
+ if (isCliLeaf(node)) {
101
+ if (leafHttpExposed(node)) {
102
+ pushRoute(routes, node, state);
103
+ }
104
+ return;
105
+ }
106
+
107
+ for (const child of node.commands) {
108
+ if (isParamRouterKey(child.key)) {
109
+ const paramName = child.key.slice(1);
110
+ walk(
111
+ child,
112
+ {
113
+ urlSegments: [...state.urlSegments, child.key],
114
+ commandPath: [...state.commandPath, child.key],
115
+ paramNames: [...state.paramNames, paramName],
116
+ },
117
+ routes,
118
+ );
119
+ continue;
120
+ }
121
+ if (isCliLeaf(child)) {
122
+ walk(
123
+ child,
124
+ {
125
+ urlSegments: state.urlSegments,
126
+ commandPath: [...state.commandPath, child.key],
127
+ paramNames: state.paramNames,
128
+ },
129
+ routes,
130
+ );
131
+ continue;
132
+ }
133
+ const seg = segmentForNode(child);
134
+ walk(
135
+ child,
136
+ {
137
+ urlSegments: [...state.urlSegments, seg],
138
+ commandPath: [...state.commandPath, child.key],
139
+ paramNames: state.paramNames,
140
+ },
141
+ routes,
142
+ );
143
+ }
144
+ }
145
+
146
+ function escapeRegex(s: string): string {
147
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
148
+ }
149
+
150
+ /** Collects all HTTP routes for exposed user leaves. */
151
+ export function collectHttpRoutes(program: CliProgram): HttpRouteDef[] {
152
+ const routes: HttpRouteDef[] = [];
153
+ if (!program.httpServer?.enabled) {
154
+ return routes;
155
+ }
156
+
157
+ if (isCliLeaf(program)) {
158
+ walk(program, { urlSegments: [], commandPath: [], paramNames: [] }, routes);
159
+ return routes;
160
+ }
161
+
162
+ for (const child of program.commands) {
163
+ if (
164
+ child.key === "completion" ||
165
+ child.key === "configure" ||
166
+ child.key === "docs" ||
167
+ child.key === "mcp" ||
168
+ child.key === "version" ||
169
+ child.key === "http"
170
+ ) {
171
+ continue;
172
+ }
173
+ if (isCliLeaf(child)) {
174
+ walk(child, { urlSegments: [], commandPath: [child.key], paramNames: [] }, routes);
175
+ } else {
176
+ walk(child, { urlSegments: [segmentForNode(child)], commandPath: [child.key], paramNames: [] }, routes);
177
+ }
178
+ }
179
+
180
+ return routes;
181
+ }
182
+
183
+ /** Match result for an incoming HTTP request. */
184
+ export type HttpRouteMatch = { ok: true; route: HttpRouteDef; pathParams: Record<string, string> } | { ok: false };
185
+
186
+ /** Finds the best matching route for method + pathname. */
187
+ export function matchHttpRoute(program: CliProgram, method: string, pathname: string): HttpRouteMatch {
188
+ const routes = collectHttpRoutes(program);
189
+ const upper = method.toUpperCase();
190
+ let best: { route: HttpRouteDef; pathParams: Record<string, string>; score: number } | undefined;
191
+
192
+ for (const route of routes) {
193
+ if (route.method !== upper) {
194
+ continue;
195
+ }
196
+ const m = route.pathPattern.exec(pathname);
197
+ if (!m) {
198
+ continue;
199
+ }
200
+ const pathParams: Record<string, string> = {};
201
+ for (let i = 0; i < route.paramNames.length; i++) {
202
+ const name = route.paramNames[i];
203
+ const val = m[i + 1];
204
+ if (name && val !== undefined) {
205
+ pathParams[name] = decodeURIComponent(val);
206
+ }
207
+ }
208
+ const score = route.openApiPath.length;
209
+ if (!best || score > best.score) {
210
+ best = { route, pathParams, score };
211
+ }
212
+ }
213
+
214
+ if (!best) {
215
+ return { ok: false };
216
+ }
217
+ return { ok: true, route: best.route, pathParams: best.pathParams };
218
+ }
219
+
220
+ /** Builds argv from an HTTP route match, query string, and optional JSON body. */
221
+ export function httpRequestToArgv(
222
+ program: CliProgram,
223
+ route: HttpRouteDef,
224
+ pathParams: Record<string, string>,
225
+ query: Record<string, string>,
226
+ body: Record<string, unknown>,
227
+ ): string[] | { error: string } {
228
+ const argv: string[] = [];
229
+ for (const key of route.commandPath) {
230
+ if (isParamRouterKey(key)) {
231
+ const name = key.slice(1);
232
+ const val = pathParams[name];
233
+ if (val === undefined || val.length === 0) {
234
+ return { error: `Missing path parameter: ${name}` };
235
+ }
236
+ argv.push(val);
237
+ } else {
238
+ argv.push(key);
239
+ }
240
+ }
241
+
242
+ const leaf = route.leaf;
243
+ if (isJsonLeaf(leaf)) {
244
+ return argv;
245
+ }
246
+
247
+ const merged: Record<string, unknown> = { ...body, ...query };
248
+ for (const [k, v] of Object.entries(query)) {
249
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
250
+ try {
251
+ merged[k] = JSON.parse(v);
252
+ } catch {
253
+ merged[k] = v;
254
+ }
255
+ }
256
+ }
257
+
258
+ for (const opt of collectOptionDefs(program, argv)) {
259
+ if (opt.kind === OptKind.Json) {
260
+ continue;
261
+ }
262
+ const val = merged[opt.name];
263
+ if (val === undefined) {
264
+ continue;
265
+ }
266
+ if (opt.kind === OptKind.Presence) {
267
+ if (val === true || val === "true" || val === "1") {
268
+ argv.push(`--${opt.name}`);
269
+ }
270
+ continue;
271
+ }
272
+ const formatted = formatMcpOptionValue(opt, val);
273
+ if (typeof formatted !== "string") {
274
+ return formatted;
275
+ }
276
+ argv.push(`--${opt.name}`, formatted);
277
+ }
278
+
279
+ for (const p of leaf.positionals ?? []) {
280
+ const val = merged[p.name] ?? pathParams[p.name];
281
+ const { argMin = 1, argMax = 1 } = p;
282
+
283
+ if (argMax === 0) {
284
+ const raw = merged[p.name];
285
+ if (raw === undefined) {
286
+ if (argMin >= 1) {
287
+ return { error: `Missing argument: ${p.name} (use a JSON array)` };
288
+ }
289
+ continue;
290
+ }
291
+ if (!Array.isArray(raw)) {
292
+ return { error: `Argument ${p.name} must be a JSON array of strings` };
293
+ }
294
+ const items = raw.map(String).filter(Boolean);
295
+ if (items.length === 0 && argMin >= 1) {
296
+ return { error: `Missing argument: ${p.name}` };
297
+ }
298
+ argv.push(...items);
299
+ continue;
300
+ }
301
+
302
+ if (val === undefined || val === "") {
303
+ if (argMin >= 1) {
304
+ return { error: `Missing argument: ${p.name}` };
305
+ }
306
+ continue;
307
+ }
308
+ argv.push(String(val));
309
+ }
310
+
311
+ return argv;
312
+ }
313
+
314
+ /** Default success HTTP status for a route method when handler omits status. */
315
+ export function defaultSuccessStatus(method: CliHttpMethod, hasBody: boolean): number {
316
+ switch (method) {
317
+ case "GET":
318
+ return 200;
319
+ case "POST":
320
+ return 201;
321
+ case "PUT":
322
+ case "PATCH":
323
+ return 200;
324
+ case "DELETE":
325
+ return hasBody ? 200 : 204;
326
+ default:
327
+ return 200;
328
+ }
329
+ }