argsbarg 6.1.1 → 6.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +17 -19
  3. package/bin/argsbarg +10 -0
  4. package/docs/README.md +4 -3
  5. package/docs/ai-skills.md +4 -2
  6. package/docs/bundled-docs.md +50 -25
  7. package/docs/cli-program.md +76 -10
  8. package/docs/config-schema.md +10 -11
  9. package/docs/configure.md +2 -0
  10. package/docs/decisions.md +40 -0
  11. package/docs/developing.md +43 -5
  12. package/docs/http-server.md +171 -0
  13. package/docs/json-schema-subset.md +51 -0
  14. package/docs/mcp.md +4 -2
  15. package/docs/output-schema.md +55 -62
  16. package/examples/formats.ts +6 -6
  17. package/examples/full-example/Formula/full-example.rb +35 -0
  18. package/examples/full-example/README.md +20 -21
  19. package/examples/full-example/docs/README.md +1 -1
  20. package/examples/full-example/docs/cli-schema.json +1790 -98
  21. package/examples/full-example/docs/cli.md +1990 -0
  22. package/examples/full-example/docs/http.md +28 -29
  23. package/examples/full-example/docs/mcp.md +8 -22
  24. package/examples/full-example/docs/openapi.json +783 -50
  25. package/examples/full-example/docs/skill.md +10 -10
  26. package/examples/full-example/justfile +11 -1
  27. package/examples/full-example/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
  28. package/examples/full-example/src/commands/render-json/__generated__/index.ts +5 -0
  29. package/examples/full-example/src/commands/render-json/command.test.ts +46 -0
  30. package/examples/full-example/src/commands/render-json/command.ts +30 -0
  31. package/examples/full-example/src/commands/render-json/types.ts +9 -0
  32. package/examples/full-example/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
  33. package/examples/full-example/src/commands/status/__generated__/index.ts +2 -2
  34. package/examples/full-example/src/commands/status/command.ts +5 -13
  35. package/examples/full-example/src/commands/status/types.ts +1 -14
  36. package/examples/full-example/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
  37. package/examples/full-example/src/commands/workspaces/__generated__/index.ts +5 -0
  38. package/examples/full-example/src/commands/workspaces/command.test.ts +58 -0
  39. package/examples/full-example/src/commands/workspaces/command.ts +94 -0
  40. package/examples/full-example/src/commands/workspaces/types.ts +6 -0
  41. package/examples/full-example/src/db/index.test.ts +86 -0
  42. package/examples/full-example/src/db/index.ts +101 -0
  43. package/examples/full-example/src/db/migrate.test.ts +35 -0
  44. package/examples/full-example/src/db/migrate.ts +69 -0
  45. package/examples/full-example/src/db/migrations/001_workspaces.sql +6 -0
  46. package/examples/full-example/src/db/tables/workspaces.ts +66 -0
  47. package/examples/full-example/src/program.ts +11 -36
  48. package/examples/full-example/src/types/argsbarg.d.ts +11 -0
  49. package/examples/full-example/src/types/md.d.ts +4 -0
  50. package/examples/full-example/tsconfig.json +5 -2
  51. package/examples/mcp-test.ts +1 -2
  52. package/examples/minimal.ts +1 -7
  53. package/examples/nested.ts +1 -2
  54. package/examples/option-required.ts +1 -1
  55. package/examples/servers.ts +4 -5
  56. package/index.d.ts +440 -136
  57. package/package.json +19 -2
  58. package/src/builtins/builtins.test.ts +7 -7
  59. package/src/builtins/completion-bash.ts +1 -1
  60. package/src/builtins/completion-fish.ts +1 -1
  61. package/src/builtins/completion-group.ts +4 -4
  62. package/src/builtins/completion-simulate-shared.ts +9 -0
  63. package/src/builtins/completion-zsh.ts +1 -1
  64. package/src/builtins/config.test.ts +3 -3
  65. package/src/builtins/config.ts +9 -9
  66. package/src/builtins/configure-copy.ts +2 -2
  67. package/src/builtins/configure.ts +4 -4
  68. package/src/builtins/dispatch.ts +19 -18
  69. package/src/builtins/export.ts +7 -5
  70. package/src/builtins/http.ts +68 -0
  71. package/src/builtins/mcp.ts +28 -4
  72. package/src/builtins/presentation.ts +6 -6
  73. package/src/builtins/registry.ts +6 -6
  74. package/src/builtins/scopes.ts +2 -2
  75. package/src/builtins/version.ts +1 -1
  76. package/src/cli-tool/full-example-capabilities.test.ts +10 -15
  77. package/src/cli-tool/main.ts +1 -1
  78. package/src/cli-tool/program.ts +3 -2
  79. package/src/cli-tool/prompt.ts +1 -1
  80. package/src/cli-tool/run-schemagen.ts +1 -3
  81. package/src/cli-tool/schemagen/cleanup.ts +6 -7
  82. package/src/cli-tool/schemagen/discover-schema-roots.ts +66 -120
  83. package/src/cli-tool/schemagen/index.ts +2 -2
  84. package/src/cli-tool/schemagen/names.ts +8 -13
  85. package/src/cli-tool/schemagen/run.ts +21 -28
  86. package/src/cli-tool/schemagen/schemagen.test.ts +136 -46
  87. package/src/config/bindings.test.ts +1 -1
  88. package/src/config/bindings.ts +1 -1
  89. package/src/config/bootstrap.test.ts +1 -1
  90. package/src/config/bootstrap.ts +36 -4
  91. package/src/config/context.test.ts +1 -1
  92. package/src/config/context.ts +1 -1
  93. package/src/config/entry.ts +1 -1
  94. package/src/config/file.test.ts +1 -1
  95. package/src/config/file.ts +3 -3
  96. package/src/config/manifest.ts +1 -1
  97. package/src/config/resolve.test.ts +1 -1
  98. package/src/config/resolve.ts +1 -1
  99. package/src/config/schema.ts +1 -1
  100. package/src/config/validate.ts +1 -1
  101. package/src/{install → configure/artifacts}/binary-placement.test.ts +1 -1
  102. package/src/{install → configure/artifacts}/binary-placement.ts +1 -1
  103. package/src/{install → configure/artifacts}/gh-release-update.ts +1 -1
  104. package/src/{install → configure/artifacts}/install-validate.test.ts +3 -3
  105. package/src/{install → configure/artifacts}/mcp-config.ts +1 -1
  106. package/src/{install → configure/artifacts}/mcp-opencode.test.ts +1 -1
  107. package/src/{install → configure/artifacts}/mcp-opencode.ts +1 -1
  108. package/src/{install → configure/artifacts}/paths.ts +5 -5
  109. package/src/configure/artifacts/plan.ts +24 -0
  110. package/src/{install → configure/artifacts}/status.test.ts +1 -1
  111. package/src/{install → configure/artifacts}/status.ts +2 -2
  112. package/src/{install → configure/artifacts}/target-base.ts +1 -1
  113. package/src/{install → configure/artifacts}/target-detect.ts +1 -1
  114. package/src/{install → configure/artifacts}/target-effective.ts +3 -9
  115. package/src/{install → configure/artifacts}/target-mcp-cli.ts +1 -1
  116. package/src/{install → configure/artifacts}/target-mcp-json.ts +1 -1
  117. package/src/{install → configure/artifacts}/target-plan-build.ts +2 -2
  118. package/src/{install → configure/artifacts}/target-registry.ts +2 -2
  119. package/src/{install → configure/artifacts}/target-scope.ts +3 -3
  120. package/src/{install → configure/artifacts}/target-skill.ts +1 -1
  121. package/src/{install → configure/artifacts}/target-types.ts +2 -2
  122. package/src/{install → configure/artifacts}/targets/app.ts +5 -5
  123. package/src/{install → configure/artifacts}/targets/chatgpt-mcp.ts +2 -2
  124. package/src/{install → configure/artifacts}/targets/claude-code-mcp.ts +2 -2
  125. package/src/{install → configure/artifacts}/targets/claude-desktop-mcp.ts +2 -2
  126. package/src/{install → configure/artifacts}/targets/claude-skill.ts +2 -2
  127. package/src/{install → configure/artifacts}/targets/codex-mcp.ts +2 -2
  128. package/src/{install → configure/artifacts}/targets/codex-skill.ts +2 -2
  129. package/src/{install → configure/artifacts}/targets/configure.ts +5 -5
  130. package/src/{install → configure/artifacts}/targets/cursor-mcp.ts +2 -2
  131. package/src/{install → configure/artifacts}/targets/cursor-skill.ts +2 -2
  132. package/src/{install → configure/artifacts}/targets/index.ts +1 -1
  133. package/src/{install → configure/artifacts}/targets/openclaw-mcp.ts +2 -2
  134. package/src/{install → configure/artifacts}/targets/openclaw-skill.ts +3 -3
  135. package/src/{install → configure/artifacts}/targets/opencode-mcp.ts +5 -5
  136. package/src/{install → configure/artifacts}/targets/opencode-skill.ts +3 -3
  137. package/src/{install → configure/artifacts}/targets.test.ts +1 -1
  138. package/src/{install → configure/artifacts}/uninstall.ts +1 -1
  139. package/src/configure/configure.test.ts +11 -11
  140. package/src/configure/index.ts +14 -14
  141. package/src/configure/prompt.ts +2 -2
  142. package/src/{context.ts → core/context.ts} +26 -20
  143. package/src/core/json-leaf.test.ts +156 -0
  144. package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
  145. package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +76 -16
  146. package/src/{parse.test.ts → core/parse.test.ts} +97 -109
  147. package/src/{parse.ts → core/parse.ts} +173 -25
  148. package/src/{schema.ts → core/schema.ts} +25 -13
  149. package/src/{types.ts → core/types.ts} +238 -35
  150. package/src/{validate.ts → core/validate.ts} +51 -29
  151. package/src/docs/builtin.ts +8 -19
  152. package/src/docs/{api-guide.test.ts → cli-guide.test.ts} +21 -21
  153. package/src/docs/{api-guide.ts → cli-guide.ts} +45 -16
  154. package/src/docs/docs.test.ts +76 -41
  155. package/src/docs/http-guide.ts +37 -34
  156. package/src/docs/mcp-guide.ts +12 -14
  157. package/src/docs/mcp-resources.test.ts +2 -3
  158. package/src/docs/mcp-resources.ts +6 -11
  159. package/src/docs/resolve.ts +22 -30
  160. package/src/docs/save.ts +3 -3
  161. package/src/exports/cli.ts +47 -0
  162. package/src/exports/headless.ts +13 -0
  163. package/src/exports/http.ts +6 -0
  164. package/src/exports/mcp.ts +6 -0
  165. package/src/{headless.test.ts → headless/routing.test.ts} +3 -3
  166. package/src/{headless.ts → headless/routing.ts} +3 -3
  167. package/src/headless/tool-call.ts +114 -46
  168. package/src/help.test.ts +152 -0
  169. package/src/help.ts +54 -18
  170. package/src/hooks/builtin.ts +20 -0
  171. package/src/hooks/run.ts +142 -0
  172. package/src/http/openapi.ts +182 -0
  173. package/src/http/readiness.ts +78 -0
  174. package/src/{api → http}/result.ts +16 -5
  175. package/src/http/routes.ts +329 -0
  176. package/src/http/server.ts +225 -0
  177. package/src/index.ts +38 -25
  178. package/src/log/ecs.test.ts +43 -0
  179. package/src/log/ecs.ts +59 -0
  180. package/src/log/emitter.ts +166 -0
  181. package/src/mcp/bundle.ts +2 -2
  182. package/src/mcp/claude.test.ts +1 -1
  183. package/src/mcp/claude.ts +4 -4
  184. package/src/{hidden-mcpb.test.ts → mcp/hidden-mcpb.test.ts} +10 -9
  185. package/src/mcp/result.ts +2 -2
  186. package/src/mcp/server.ts +54 -6
  187. package/src/mcp/tools.ts +18 -20
  188. package/src/{capabilities.ts → runtime/capabilities.ts} +11 -11
  189. package/src/{cli-errors.ts → runtime/cli-errors.ts} +4 -4
  190. package/src/{cli.ts → runtime/cli.ts} +160 -50
  191. package/src/runtime/exposure.ts +102 -0
  192. package/src/{invoke.test.ts → runtime/invoke.test.ts} +31 -7
  193. package/src/server/context.ts +25 -0
  194. package/src/server/overrides.ts +112 -0
  195. package/src/skill/generate.ts +8 -8
  196. package/src/skill/hint.ts +1 -1
  197. package/src/skill/install.ts +2 -2
  198. package/src/skill/naming.ts +1 -1
  199. package/src/{test-fixtures.ts → test/fixtures.ts} +3 -2
  200. package/src/{config.integration.test.ts → test/integration/config.test.ts} +8 -8
  201. package/src/{api.integration.test.ts → test/integration/http.test.ts} +170 -67
  202. package/src/{mcp.integration.test.ts → test/integration/mcp.test.ts} +11 -57
  203. package/docs/api-server.md +0 -141
  204. package/examples/full-example/docs/api.md +0 -511
  205. package/examples/full-example/src/commands/status/__generated__/outputSchema.json +0 -28
  206. package/examples/full-example/src/config/__generated__/configSchema.json +0 -40
  207. package/examples/full-example/src/config/__generated__/index.ts +0 -5
  208. package/examples/full-example/src/config/types.ts +0 -24
  209. package/src/api/openapi.ts +0 -117
  210. package/src/api/server.ts +0 -120
  211. package/src/builtins/api.ts +0 -38
  212. package/src/hidden.ts +0 -30
  213. package/src/install/plan.ts +0 -53
  214. /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
  215. /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
  216. /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
  217. /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
  218. /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
  219. /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
  220. /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
  221. /package/src/{install → configure/artifacts}/normalize.ts +0 -0
  222. /package/src/{install → configure/artifacts}/opts.ts +0 -0
  223. /package/src/{install → configure/artifacts}/shell.ts +0 -0
  224. /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
  225. /package/src/{formats.ts → core/formats.ts} +0 -0
  226. /package/src/{respond.ts → core/respond.ts} +0 -0
  227. /package/src/{types.test.ts → core/types.test.ts} +0 -0
  228. /package/src/{api → http}/schema-deref.test.ts +0 -0
  229. /package/src/{api → http}/schema-deref.ts +0 -0
@@ -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,
@@ -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
+ }
@@ -0,0 +1,225 @@
1
+ /*
2
+ HTTP tool server for ArgsBarg programs: health, OpenAPI, and REST API invocation.
3
+ */
4
+
5
+ import { randomUUID } from "node:crypto";
6
+ import type { CliHttpWireContext, CliProgram } from "~/core/types.ts";
7
+ import {
8
+ executeHttpRouteCall,
9
+ headlessFailureToHttpResponse,
10
+ headlessSuccessToHttpResponse,
11
+ } from "~/headless/tool-call.ts";
12
+ import type { Cli } from "~/runtime/cli.ts";
13
+ import { leafHttpResponseDefaults } from "~/runtime/exposure.ts";
14
+ import type { ResolvedHttpServeConfig } from "~/server/overrides.ts";
15
+ import { generateOpenApi } from "./openapi.ts";
16
+ import { evaluateReadiness } from "./readiness.ts";
17
+ import { API_CORS_HEADERS, apiDocsHtml, apiErrorResponse, apiOptionsResponse } from "./result.ts";
18
+ import { defaultSuccessStatus, matchHttpRoute } from "./routes.ts";
19
+
20
+ const DEFAULT_HOST = "127.0.0.1";
21
+ const DEFAULT_PORT = 3000;
22
+
23
+ /** Resolved listen address for the HTTP API server. */
24
+ export function resolveHttpListenAddress(program: CliProgram): { hostname: string; port: number } {
25
+ const config = program.httpServer;
26
+ return {
27
+ hostname: config?.host ?? DEFAULT_HOST,
28
+ port: config?.port ?? DEFAULT_PORT,
29
+ };
30
+ }
31
+
32
+ /** Resolves client IP, optionally honoring X-Forwarded-For. */
33
+ export function resolveClientIp(request: Request, trustProxy: boolean): string {
34
+ if (trustProxy) {
35
+ const xff = request.headers.get("x-forwarded-for");
36
+ if (xff) {
37
+ return xff.split(",")[0]?.trim() ?? "unknown";
38
+ }
39
+ }
40
+ return "unknown";
41
+ }
42
+
43
+ /** Writes a JSON HTTP response with CORS headers. */
44
+ function jsonResponse(status: number, body: unknown): Response {
45
+ return new Response(JSON.stringify(body), {
46
+ status,
47
+ headers: {
48
+ ...API_CORS_HEADERS,
49
+ "content-type": "application/json; charset=utf-8",
50
+ },
51
+ });
52
+ }
53
+
54
+ function parseQuery(url: URL): Record<string, string> {
55
+ const out: Record<string, string> = {};
56
+ for (const [k, v] of url.searchParams.entries()) {
57
+ out[k] = v;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /** Handles one HTTP request for the API server. */
63
+ export async function handleApiRequest(
64
+ cli: Cli,
65
+ request: Request,
66
+ resolved?: ResolvedHttpServeConfig,
67
+ ): Promise<Response> {
68
+ const httpConfig = resolved ?? cli.server?.http;
69
+ const trustProxy = httpConfig?.trustProxy ?? cli.program.httpServer?.trustProxy ?? false;
70
+ const requestId = randomUUID();
71
+ const url = new URL(request.url);
72
+ const clientIp = resolveClientIp(request, trustProxy);
73
+ const wireCtx: CliHttpWireContext = {
74
+ request,
75
+ requestId,
76
+ clientIp,
77
+ path: url.pathname,
78
+ method: request.method,
79
+ };
80
+ const hooks = cli.server?.httpHooks ?? cli.program.httpServer?.hooks;
81
+ const emitter = cli.server?.emitter;
82
+ const started = performance.now();
83
+
84
+ const finish = async (response: Response, failureKind?: string, error?: unknown): Promise<Response> => {
85
+ const durationMs = Math.round(performance.now() - started);
86
+ if (failureKind && error !== undefined) {
87
+ await hooks?.onError?.({
88
+ ...wireCtx,
89
+ failureKind: failureKind as import("~/core/types.ts").InvokeFailureKind,
90
+ error,
91
+ });
92
+ } else {
93
+ await hooks?.onResponse?.({ ...wireCtx, status: response.status, durationMs });
94
+ }
95
+ emitter?.emitAccess({
96
+ method: request.method,
97
+ path: url.pathname,
98
+ status: response.status,
99
+ durationMs,
100
+ requestId,
101
+ clientIp,
102
+ });
103
+ return response;
104
+ };
105
+
106
+ await hooks?.onRequest?.(wireCtx);
107
+
108
+ if (request.method === "OPTIONS") {
109
+ return finish(apiOptionsResponse());
110
+ }
111
+
112
+ const root = cli.program;
113
+ const path = url.pathname;
114
+
115
+ if (request.method === "GET" && (path === "/health" || path === "/health/live")) {
116
+ return finish(jsonResponse(200, { ok: true }));
117
+ }
118
+
119
+ if (request.method === "GET" && path === "/health/ready") {
120
+ const runtime = cli.server?.runtime;
121
+ if (!runtime) {
122
+ return finish(jsonResponse(200, { ok: true }));
123
+ }
124
+ const readiness = await evaluateReadiness(root, "http", runtime, cli.appConfig);
125
+ return finish(jsonResponse(readiness.ok ? 200 : 503, readiness));
126
+ }
127
+
128
+ if (request.method === "GET" && path === "/openapi.json") {
129
+ return finish(jsonResponse(200, generateOpenApi(root)));
130
+ }
131
+
132
+ if (request.method === "GET" && path === "/openapi-browser") {
133
+ return finish(
134
+ new Response(apiDocsHtml(), {
135
+ status: 200,
136
+ headers: {
137
+ ...API_CORS_HEADERS,
138
+ "content-type": "text/html; charset=utf-8",
139
+ },
140
+ }),
141
+ );
142
+ }
143
+
144
+ if (path.startsWith("/api")) {
145
+ const match = matchHttpRoute(root, request.method, path);
146
+ if (!match.ok) {
147
+ return finish(apiErrorResponse(404, { error: "Not found" }));
148
+ }
149
+
150
+ let body: Record<string, unknown> = {};
151
+ if (request.method === "POST" || request.method === "PUT" || request.method === "PATCH") {
152
+ const rawBody = await request.text();
153
+ if (rawBody.trim().length > 0) {
154
+ try {
155
+ const parsed = JSON.parse(rawBody);
156
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
157
+ return finish(apiErrorResponse(400, { error: "Request body must be a JSON object" }));
158
+ }
159
+ body = parsed as Record<string, unknown>;
160
+ } catch {
161
+ return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
162
+ }
163
+ }
164
+ }
165
+
166
+ const query = parseQuery(url);
167
+ const result = await executeHttpRouteCall(cli, match.route, match.pathParams, query, body, {
168
+ request,
169
+ clientIp,
170
+ requestId,
171
+ });
172
+ if (result.ok) {
173
+ const leafHttp = leafHttpResponseDefaults(match.route.leaf);
174
+ const hasBody = result.response.body !== undefined;
175
+ const methodDefault =
176
+ match.route.leaf.http?.successStatus ??
177
+ defaultSuccessStatus(match.route.method, hasBody && match.route.method !== "DELETE");
178
+ return finish(headlessSuccessToHttpResponse(result, leafHttp, methodDefault));
179
+ }
180
+ const obscure = httpConfig?.obscureUnexpected ?? false;
181
+ return finish(headlessFailureToHttpResponse(result, obscure), result.invokeResult?.failureKind, result.message);
182
+ }
183
+
184
+ if (path.startsWith("/tools")) {
185
+ return finish(apiErrorResponse(404, { error: "Not found" }));
186
+ }
187
+
188
+ return finish(apiErrorResponse(404, { error: "Not found" }));
189
+ }
190
+
191
+ /** Runs the HTTP API server until the process is interrupted. */
192
+ export async function httpServeHttp(cli: Cli, resolved?: ResolvedHttpServeConfig): Promise<never> {
193
+ const listen = resolved ?? {
194
+ hostname: resolveHttpListenAddress(cli.program).hostname,
195
+ port: resolveHttpListenAddress(cli.program).port,
196
+ trustProxy: cli.program.httpServer?.trustProxy ?? false,
197
+ obscureUnexpected: cli.program.httpServer?.errors?.obscureUnexpected ?? false,
198
+ log: { format: "json" as const, access: true, errors: true, dev: false },
199
+ };
200
+ const server = Bun.serve({
201
+ hostname: listen.hostname,
202
+ port: listen.port,
203
+ fetch: (request) => handleApiRequest(cli, request, listen),
204
+ });
205
+ const url = `http://${server.hostname}:${server.port}`;
206
+ const emitter = cli.server?.emitter;
207
+ if (emitter && listen.log.format === "text") {
208
+ emitter.emitLifecycle(
209
+ `${cli.program.key} ${cli.program.version} — HTTP API listening on ${url}`,
210
+ "http.server.start",
211
+ );
212
+ } else {
213
+ emitter?.emit({
214
+ level: "info",
215
+ message: `HTTP API listening on ${url}`,
216
+ action: "http.server.start",
217
+ labels: { url },
218
+ });
219
+ if (!emitter) {
220
+ process.stderr.write(`HTTP API listening on ${url}\n`);
221
+ }
222
+ }
223
+ await new Promise<never>(() => {});
224
+ throw new Error("HTTP API server stopped unexpectedly");
225
+ }