argsbarg 6.1.2 → 6.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +17 -19
  3. package/bin/argsbarg +10 -0
  4. package/docs/README.md +4 -3
  5. package/docs/ai-skills.md +4 -2
  6. package/docs/bundled-docs.md +50 -25
  7. package/docs/cli-program.md +52 -10
  8. package/docs/config-schema.md +10 -11
  9. package/docs/configure.md +2 -0
  10. package/docs/decisions.md +40 -0
  11. package/docs/developing.md +43 -5
  12. package/docs/http-server.md +171 -0
  13. package/docs/json-schema-subset.md +51 -0
  14. package/docs/mcp.md +4 -2
  15. package/docs/output-schema.md +55 -62
  16. package/examples/formats.ts +6 -6
  17. package/examples/full-example/Formula/full-example.rb +35 -0
  18. package/examples/full-example/README.md +20 -21
  19. package/examples/full-example/docs/README.md +1 -1
  20. package/examples/full-example/docs/cli-schema.json +1790 -98
  21. package/examples/full-example/docs/cli.md +1990 -0
  22. package/examples/full-example/docs/http.md +28 -29
  23. package/examples/full-example/docs/mcp.md +8 -22
  24. package/examples/full-example/docs/openapi.json +783 -50
  25. package/examples/full-example/docs/skill.md +10 -10
  26. package/examples/full-example/justfile +11 -1
  27. package/examples/full-example/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
  28. package/examples/full-example/src/commands/render-json/__generated__/index.ts +5 -0
  29. package/examples/full-example/src/commands/render-json/command.test.ts +46 -0
  30. package/examples/full-example/src/commands/render-json/command.ts +30 -0
  31. package/examples/full-example/src/commands/render-json/types.ts +9 -0
  32. package/examples/full-example/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
  33. package/examples/full-example/src/commands/status/__generated__/index.ts +2 -2
  34. package/examples/full-example/src/commands/status/command.ts +5 -13
  35. package/examples/full-example/src/commands/status/types.ts +1 -14
  36. package/examples/full-example/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
  37. package/examples/full-example/src/commands/workspaces/__generated__/index.ts +5 -0
  38. package/examples/full-example/src/commands/workspaces/command.test.ts +58 -0
  39. package/examples/full-example/src/commands/workspaces/command.ts +94 -0
  40. package/examples/full-example/src/commands/workspaces/types.ts +6 -0
  41. package/examples/full-example/src/db/index.test.ts +86 -0
  42. package/examples/full-example/src/db/index.ts +101 -0
  43. package/examples/full-example/src/db/migrate.test.ts +35 -0
  44. package/examples/full-example/src/db/migrate.ts +69 -0
  45. package/examples/full-example/src/db/migrations/001_workspaces.sql +6 -0
  46. package/examples/full-example/src/db/tables/workspaces.ts +66 -0
  47. package/examples/full-example/src/program.ts +11 -36
  48. package/examples/full-example/src/types/argsbarg.d.ts +11 -0
  49. package/examples/full-example/src/types/md.d.ts +4 -0
  50. package/examples/full-example/tsconfig.json +5 -2
  51. package/examples/mcp-test.ts +1 -2
  52. package/examples/minimal.ts +1 -7
  53. package/examples/nested.ts +1 -2
  54. package/examples/option-required.ts +1 -1
  55. package/examples/servers.ts +4 -5
  56. package/index.d.ts +431 -136
  57. package/package.json +19 -2
  58. package/src/builtins/builtins.test.ts +7 -7
  59. package/src/builtins/completion-bash.ts +1 -1
  60. package/src/builtins/completion-fish.ts +1 -1
  61. package/src/builtins/completion-group.ts +4 -4
  62. package/src/builtins/completion-simulate-shared.ts +9 -0
  63. package/src/builtins/completion-zsh.ts +1 -1
  64. package/src/builtins/config.test.ts +3 -3
  65. package/src/builtins/config.ts +9 -9
  66. package/src/builtins/configure-copy.ts +2 -2
  67. package/src/builtins/configure.ts +4 -4
  68. package/src/builtins/dispatch.ts +19 -18
  69. package/src/builtins/export.ts +7 -5
  70. package/src/builtins/http.ts +68 -0
  71. package/src/builtins/mcp.ts +28 -4
  72. package/src/builtins/presentation.ts +6 -6
  73. package/src/builtins/registry.ts +6 -6
  74. package/src/builtins/scopes.ts +2 -2
  75. package/src/builtins/version.ts +1 -1
  76. package/src/cli-tool/full-example-capabilities.test.ts +10 -15
  77. package/src/cli-tool/main.ts +1 -1
  78. package/src/cli-tool/program.ts +3 -2
  79. package/src/cli-tool/prompt.ts +1 -1
  80. package/src/cli-tool/run-schemagen.ts +1 -3
  81. package/src/cli-tool/schemagen/cleanup.ts +6 -7
  82. package/src/cli-tool/schemagen/discover-schema-roots.ts +66 -120
  83. package/src/cli-tool/schemagen/index.ts +2 -2
  84. package/src/cli-tool/schemagen/names.ts +8 -13
  85. package/src/cli-tool/schemagen/run.ts +21 -28
  86. package/src/cli-tool/schemagen/schemagen.test.ts +136 -46
  87. package/src/config/bindings.test.ts +1 -1
  88. package/src/config/bindings.ts +1 -1
  89. package/src/config/bootstrap.test.ts +1 -1
  90. package/src/config/bootstrap.ts +36 -4
  91. package/src/config/context.test.ts +1 -1
  92. package/src/config/context.ts +1 -1
  93. package/src/config/entry.ts +1 -1
  94. package/src/config/file.test.ts +1 -1
  95. package/src/config/file.ts +3 -3
  96. package/src/config/manifest.ts +1 -1
  97. package/src/config/resolve.test.ts +1 -1
  98. package/src/config/resolve.ts +1 -1
  99. package/src/config/schema.ts +1 -1
  100. package/src/config/validate.ts +1 -1
  101. package/src/{install → configure/artifacts}/binary-placement.test.ts +1 -1
  102. package/src/{install → configure/artifacts}/binary-placement.ts +1 -1
  103. package/src/{install → configure/artifacts}/gh-release-update.ts +1 -1
  104. package/src/{install → configure/artifacts}/install-validate.test.ts +3 -3
  105. package/src/{install → configure/artifacts}/mcp-config.ts +1 -1
  106. package/src/{install → configure/artifacts}/mcp-opencode.test.ts +1 -1
  107. package/src/{install → configure/artifacts}/mcp-opencode.ts +1 -1
  108. package/src/{install → configure/artifacts}/paths.ts +5 -5
  109. package/src/configure/artifacts/plan.ts +24 -0
  110. package/src/{install → configure/artifacts}/status.test.ts +1 -1
  111. package/src/{install → configure/artifacts}/status.ts +2 -2
  112. package/src/{install → configure/artifacts}/target-base.ts +1 -1
  113. package/src/{install → configure/artifacts}/target-detect.ts +1 -1
  114. package/src/{install → configure/artifacts}/target-effective.ts +3 -9
  115. package/src/{install → configure/artifacts}/target-mcp-cli.ts +1 -1
  116. package/src/{install → configure/artifacts}/target-mcp-json.ts +1 -1
  117. package/src/{install → configure/artifacts}/target-plan-build.ts +2 -2
  118. package/src/{install → configure/artifacts}/target-registry.ts +2 -2
  119. package/src/{install → configure/artifacts}/target-scope.ts +3 -3
  120. package/src/{install → configure/artifacts}/target-skill.ts +1 -1
  121. package/src/{install → configure/artifacts}/target-types.ts +2 -2
  122. package/src/{install → configure/artifacts}/targets/app.ts +5 -5
  123. package/src/{install → configure/artifacts}/targets/chatgpt-mcp.ts +2 -2
  124. package/src/{install → configure/artifacts}/targets/claude-code-mcp.ts +2 -2
  125. package/src/{install → configure/artifacts}/targets/claude-desktop-mcp.ts +2 -2
  126. package/src/{install → configure/artifacts}/targets/claude-skill.ts +2 -2
  127. package/src/{install → configure/artifacts}/targets/codex-mcp.ts +2 -2
  128. package/src/{install → configure/artifacts}/targets/codex-skill.ts +2 -2
  129. package/src/{install → configure/artifacts}/targets/configure.ts +5 -5
  130. package/src/{install → configure/artifacts}/targets/cursor-mcp.ts +2 -2
  131. package/src/{install → configure/artifacts}/targets/cursor-skill.ts +2 -2
  132. package/src/{install → configure/artifacts}/targets/index.ts +1 -1
  133. package/src/{install → configure/artifacts}/targets/openclaw-mcp.ts +2 -2
  134. package/src/{install → configure/artifacts}/targets/openclaw-skill.ts +3 -3
  135. package/src/{install → configure/artifacts}/targets/opencode-mcp.ts +5 -5
  136. package/src/{install → configure/artifacts}/targets/opencode-skill.ts +3 -3
  137. package/src/{install → configure/artifacts}/targets.test.ts +1 -1
  138. package/src/{install → configure/artifacts}/uninstall.ts +1 -1
  139. package/src/configure/configure.test.ts +11 -11
  140. package/src/configure/index.ts +14 -14
  141. package/src/configure/prompt.ts +2 -2
  142. package/src/{context.ts → core/context.ts} +26 -20
  143. package/src/{json-leaf.test.ts → core/json-leaf.test.ts} +4 -4
  144. package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
  145. package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +16 -12
  146. package/src/{parse.test.ts → core/parse.test.ts} +97 -109
  147. package/src/{parse.ts → core/parse.ts} +129 -31
  148. package/src/{schema.ts → core/schema.ts} +25 -13
  149. package/src/{types.ts → core/types.ts} +225 -35
  150. package/src/{validate.ts → core/validate.ts} +39 -29
  151. package/src/docs/builtin.ts +8 -19
  152. package/src/docs/{api-guide.test.ts → cli-guide.test.ts} +21 -21
  153. package/src/docs/{api-guide.ts → cli-guide.ts} +45 -16
  154. package/src/docs/docs.test.ts +76 -41
  155. package/src/docs/http-guide.ts +37 -34
  156. package/src/docs/mcp-guide.ts +12 -14
  157. package/src/docs/mcp-resources.test.ts +2 -3
  158. package/src/docs/mcp-resources.ts +6 -11
  159. package/src/docs/resolve.ts +22 -30
  160. package/src/docs/save.ts +3 -3
  161. package/src/exports/cli.ts +47 -0
  162. package/src/exports/headless.ts +13 -0
  163. package/src/exports/http.ts +6 -0
  164. package/src/exports/mcp.ts +6 -0
  165. package/src/{headless.test.ts → headless/routing.test.ts} +3 -3
  166. package/src/{headless.ts → headless/routing.ts} +3 -3
  167. package/src/headless/tool-call.ts +114 -46
  168. package/src/help.test.ts +152 -0
  169. package/src/help.ts +3 -3
  170. package/src/hooks/builtin.ts +20 -0
  171. package/src/hooks/run.ts +142 -0
  172. package/src/http/openapi.ts +182 -0
  173. package/src/http/readiness.ts +78 -0
  174. package/src/{api → http}/result.ts +16 -5
  175. package/src/http/routes.ts +329 -0
  176. package/src/http/server.ts +225 -0
  177. package/src/index.ts +36 -25
  178. package/src/log/ecs.test.ts +43 -0
  179. package/src/log/ecs.ts +59 -0
  180. package/src/log/emitter.ts +166 -0
  181. package/src/mcp/bundle.ts +2 -2
  182. package/src/mcp/claude.test.ts +1 -1
  183. package/src/mcp/claude.ts +4 -4
  184. package/src/{hidden-mcpb.test.ts → mcp/hidden-mcpb.test.ts} +10 -9
  185. package/src/mcp/result.ts +2 -2
  186. package/src/mcp/server.ts +54 -6
  187. package/src/mcp/tools.ts +9 -20
  188. package/src/{capabilities.ts → runtime/capabilities.ts} +11 -11
  189. package/src/{cli-errors.ts → runtime/cli-errors.ts} +4 -4
  190. package/src/{cli.ts → runtime/cli.ts} +159 -49
  191. package/src/runtime/exposure.ts +102 -0
  192. package/src/{invoke.test.ts → runtime/invoke.test.ts} +31 -7
  193. package/src/server/context.ts +25 -0
  194. package/src/server/overrides.ts +112 -0
  195. package/src/skill/generate.ts +8 -8
  196. package/src/skill/hint.ts +1 -1
  197. package/src/skill/install.ts +2 -2
  198. package/src/skill/naming.ts +1 -1
  199. package/src/{test-fixtures.ts → test/fixtures.ts} +3 -2
  200. package/src/{config.integration.test.ts → test/integration/config.test.ts} +8 -8
  201. package/src/{api.integration.test.ts → test/integration/http.test.ts} +170 -67
  202. package/src/{mcp.integration.test.ts → test/integration/mcp.test.ts} +11 -57
  203. package/docs/api-server.md +0 -141
  204. package/examples/full-example/docs/api.md +0 -511
  205. package/examples/full-example/src/commands/status/__generated__/outputSchema.json +0 -28
  206. package/examples/full-example/src/config/__generated__/configSchema.json +0 -40
  207. package/examples/full-example/src/config/__generated__/index.ts +0 -5
  208. package/examples/full-example/src/config/types.ts +0 -24
  209. package/src/api/openapi.ts +0 -117
  210. package/src/api/server.ts +0 -120
  211. package/src/builtins/api.ts +0 -38
  212. package/src/hidden.ts +0 -30
  213. package/src/install/plan.ts +0 -53
  214. /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
  215. /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
  216. /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
  217. /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
  218. /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
  219. /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
  220. /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
  221. /package/src/{install → configure/artifacts}/normalize.ts +0 -0
  222. /package/src/{install → configure/artifacts}/opts.ts +0 -0
  223. /package/src/{install → configure/artifacts}/shell.ts +0 -0
  224. /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
  225. /package/src/{formats.ts → core/formats.ts} +0 -0
  226. /package/src/{respond.ts → core/respond.ts} +0 -0
  227. /package/src/{types.test.ts → core/types.test.ts} +0 -0
  228. /package/src/{api → http}/schema-deref.test.ts +0 -0
  229. /package/src/{api → http}/schema-deref.ts +0 -0
@@ -2,30 +2,46 @@
2
2
  Runtime entry point: validate program, cache derived state, run / invoke / MCP serve.
3
3
  */
4
4
 
5
+ import { randomUUID } from "node:crypto";
5
6
  import { format } from "node:util";
6
- import { apiServeHttp } from "./api/server.ts";
7
- import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
8
- import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
7
+ import { builtinInterceptRoot, dispatchBuiltin } from "~/builtins/dispatch.ts";
8
+ import { cliParseRoot, cliPresentationRoot } from "~/builtins/presentation.ts";
9
+ import { bootstrapAppConfig, type EnsureAppConfigOpts, ensureAppConfig } from "~/config/bootstrap.ts";
10
+ import { type AnyAppConfigSnapshot, createAppConfigSnapshot } from "~/config/context.ts";
11
+ import { readAppConfigFileRaw, resolveAppConfigPath } from "~/config/file.ts";
12
+ import { effectiveJsonSchema } from "~/config/schema.ts";
13
+ import { CliContext } from "~/core/context.ts";
14
+ import { LeafInputError, preloadPipableJson } from "~/core/leaf-inputs.ts";
15
+ import { ParseKind, type ParseResult, parse, postParseValidate } from "~/core/parse.ts";
16
+ import { type CliSchemaRootExport, cliSchemaExport } from "~/core/schema.ts";
17
+ import type {
18
+ CliHandler,
19
+ CliInvocation,
20
+ CliLeaf,
21
+ CliLocals,
22
+ CliNode,
23
+ CliProgram,
24
+ CliRespondOptions,
25
+ CliRouter,
26
+ InvokeFailureKind,
27
+ } from "~/core/types.ts";
28
+ import { isCliLeaf, isCliRouter } from "~/core/types.ts";
29
+ import { cliValidateProgram } from "~/core/validate.ts";
30
+ import { cliHelpRender } from "~/help.ts";
31
+ import { isBuiltinInvokePath } from "~/hooks/builtin.ts";
32
+ import { buildInvokeHookContext, classifyFailureKind, runErrorPipeline, runHook } from "~/hooks/run.ts";
33
+ import { httpServeHttp } from "~/http/server.ts";
34
+ import { LogEmitter } from "~/log/emitter.ts";
35
+ import { bootstrapMcpEnv } from "~/mcp/env.ts";
36
+ import { mcpServeStdioLoop } from "~/mcp/server.ts";
37
+ import { createServerRuntime, type ServerHandleContext } from "~/server/context.ts";
38
+ import { resolveHttpServeConfig, resolveMcpServeConfig, type ServeOverrides } from "~/server/overrides.ts";
9
39
  import {
10
40
  assertBuiltinAllowed,
11
41
  type CliCapabilities,
12
42
  resolveCapabilities,
13
43
  skipsRequiredAppConfigExit,
14
44
  } from "./capabilities.ts";
15
- import { bootstrapAppConfig, type EnsureAppConfigOpts, ensureAppConfig } from "./config/bootstrap.ts";
16
- import { type AnyAppConfigSnapshot, createAppConfigSnapshot } from "./config/context.ts";
17
- import { readAppConfigFileRaw, resolveAppConfigPath } from "./config/file.ts";
18
- import { effectiveJsonSchema } from "./config/schema.ts";
19
- import { CliContext } from "./context.ts";
20
- import { cliHelpRender } from "./help.ts";
21
- import { LeafInputError, preloadPipableJson } from "./leaf-inputs.ts";
22
- import { bootstrapMcpEnv } from "./mcp/env.ts";
23
- import { mcpServeStdioLoop } from "./mcp/server.ts";
24
- import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
25
- import { type CliSchemaExport, cliSchemaExport } from "./schema.ts";
26
- import type { CliHandler, CliInvocation, CliLeaf, CliNode, CliProgram, CliRespondOptions, CliRouter } from "./types.ts";
27
- import { isCliLeaf, isCliRouter } from "./types.ts";
28
- import { cliValidateProgram } from "./validate.ts";
29
45
 
30
46
  /** Outcome of a non-exiting CLI invocation. */
31
47
  export type CliInvokeKind = "ok" | "help" | "error";
@@ -37,6 +53,8 @@ export interface CliInvokeResult {
37
53
  stdout: string;
38
54
  stderr: string;
39
55
  errorMsg?: string;
56
+ /** Classified failure for HTTP/MCP status mapping. */
57
+ failureKind?: InvokeFailureKind;
40
58
  /** Headless response payload when invocation is `api` or `mcp` and the handler succeeded. */
41
59
  response?: CliRespondOptions;
42
60
  }
@@ -66,6 +84,8 @@ export class Cli {
66
84
  private readonly parseRootMerged: CliRouter;
67
85
  private readonly presentationRoot: CliRouter;
68
86
  private _appConfig?: AnyAppConfigSnapshot;
87
+ /** Active HTTP/MCP server handle (set during serve). */
88
+ server?: ServerHandleContext;
69
89
 
70
90
  constructor(program: CliProgram) {
71
91
  cliValidateProgram(program);
@@ -86,7 +106,7 @@ export class Cli {
86
106
  return this._appConfig;
87
107
  }
88
108
 
89
- exportCommandSchema(): CliSchemaExport {
109
+ exportCommandSchema(): CliSchemaRootExport {
90
110
  return cliSchemaExport(this.program);
91
111
  }
92
112
 
@@ -148,6 +168,8 @@ export class Cli {
148
168
  snapshot,
149
169
  undefined,
150
170
  preloadedJson,
171
+ pr.pathParams,
172
+ { requestId: randomUUID() } as CliLocals,
151
173
  );
152
174
  try {
153
175
  this.ensureValidatedLeafInputs(ctx, leaf);
@@ -169,7 +191,13 @@ export class Cli {
169
191
 
170
192
  async invoke(
171
193
  argv: string[],
172
- opts?: { invocation?: CliInvocation; toolArgs?: Record<string, unknown> },
194
+ opts?: {
195
+ invocation?: CliInvocation;
196
+ toolArgs?: Record<string, unknown>;
197
+ requestId?: string;
198
+ http?: { request: Request; clientIp: string; requestId: string };
199
+ mcp?: { rpcMethod: string; toolName?: string; requestId: string };
200
+ },
173
201
  ): Promise<CliInvokeResult> {
174
202
  const invocation = opts?.invocation ?? "mcp";
175
203
  const prep = this.prepareDispatch(argv, { presentationFallback: true });
@@ -181,6 +209,7 @@ export class Cli {
181
209
  stdout: "",
182
210
  stderr: "",
183
211
  errorMsg: "Help is not available via tool calls.",
212
+ failureKind: "help",
184
213
  };
185
214
  }
186
215
  return {
@@ -189,6 +218,7 @@ export class Cli {
189
218
  stdout: "",
190
219
  stderr: prep.error.errorMsg,
191
220
  errorMsg: prep.error.errorMsg,
221
+ failureKind: "validation",
192
222
  };
193
223
  }
194
224
 
@@ -198,6 +228,8 @@ export class Cli {
198
228
  exitOnMissing: false,
199
229
  });
200
230
 
231
+ const runtime = this.server?.runtime;
232
+ const requestId = opts?.requestId ?? opts?.http?.requestId ?? opts?.mcp?.requestId ?? randomUUID();
201
233
  const ctx = new CliContext(
202
234
  this.program.key,
203
235
  pr.path,
@@ -207,8 +239,30 @@ export class Cli {
207
239
  invocation,
208
240
  snapshot,
209
241
  opts?.toolArgs,
242
+ {},
243
+ pr.pathParams,
244
+ { requestId } as CliLocals,
245
+ runtime,
210
246
  );
211
247
 
248
+ const skipHooks = isBuiltinInvokePath(pr.path);
249
+ const hooks = this.program.hooks;
250
+ const obscureUnexpected =
251
+ invocation === "http"
252
+ ? (this.server?.http?.obscureUnexpected ?? this.program.httpServer?.errors?.obscureUnexpected ?? false)
253
+ : invocation === "mcp"
254
+ ? (this.server?.mcp?.obscureUnexpected ?? this.program.mcpServer?.errors?.obscureUnexpected ?? false)
255
+ : false;
256
+ const emitter = this.server?.emitter;
257
+
258
+ const hookCtx = () =>
259
+ buildInvokeHookContext(ctx, {
260
+ path: pr.path,
261
+ runtime,
262
+ http: opts?.http,
263
+ mcp: opts?.mcp,
264
+ });
265
+
212
266
  let stdout = "";
213
267
  let stderr = "";
214
268
  const origExit = process.exit;
@@ -252,6 +306,33 @@ export class Cli {
252
306
  stderr += `${format(...args)}\n`;
253
307
  };
254
308
 
309
+ const finishError = async (
310
+ err: unknown,
311
+ kindOpts: Parameters<typeof classifyFailureKind>[1],
312
+ ): Promise<CliInvokeResult> => {
313
+ const failureKind = classifyFailureKind(err, kindOpts);
314
+ if (!skipHooks) {
315
+ const piped = await runErrorPipeline(hookCtx(), err, failureKind, hooks, emitter, obscureUnexpected);
316
+ return {
317
+ kind: "error",
318
+ exitCode: piped.clientError.exitCode ?? 1,
319
+ stdout,
320
+ stderr: `${piped.errorMsg}\n`,
321
+ errorMsg: piped.errorMsg,
322
+ failureKind: piped.failureKind,
323
+ };
324
+ }
325
+ const message = err instanceof Error ? err.message : String(err);
326
+ return {
327
+ kind: "error",
328
+ exitCode: 1,
329
+ stdout,
330
+ stderr: `${message}\n`,
331
+ errorMsg: message,
332
+ failureKind,
333
+ };
334
+ };
335
+
255
336
  try {
256
337
  if (pr.kind === ParseKind.Ok) {
257
338
  await dispatchBuiltin(this.program, pr, {
@@ -260,6 +341,10 @@ export class Cli {
260
341
  });
261
342
  }
262
343
 
344
+ if (!skipHooks) {
345
+ await runHook(() => hooks?.beforeInvoke?.(hookCtx()), "beforeInvoke");
346
+ }
347
+
263
348
  this.ensureValidatedLeafInputs(ctx, leaf);
264
349
  const handlerResult = await Promise.resolve(leaf.handler(ctx));
265
350
  if (handlerResult !== undefined && ctx.getResponse() === undefined) {
@@ -267,53 +352,44 @@ export class Cli {
267
352
  }
268
353
 
269
354
  const response = ctx.getResponse();
270
- return {
355
+ const okResult: CliInvokeResult = {
271
356
  kind: "ok",
272
357
  exitCode: 0,
273
358
  stdout,
274
359
  stderr,
275
360
  ...(response ? { response } : {}),
276
361
  };
362
+
363
+ if (!skipHooks) {
364
+ await runHook(() => hooks?.afterInvoke?.({ ...hookCtx(), result: okResult }), "afterInvoke");
365
+ }
366
+
367
+ return okResult;
277
368
  } catch (err) {
278
369
  if (err instanceof CliInvokeExit) {
279
370
  if (err.code === 0) {
280
371
  const response = ctx.getResponse();
281
- return {
372
+ const okResult: CliInvokeResult = {
282
373
  kind: "ok",
283
374
  exitCode: 0,
284
375
  stdout,
285
376
  stderr,
286
377
  ...(response ? { response } : {}),
287
378
  };
379
+ if (!skipHooks) {
380
+ await runHook(() => hooks?.afterInvoke?.({ ...hookCtx(), result: okResult }), "afterInvoke");
381
+ }
382
+ return okResult;
288
383
  }
289
- const msg = stderr.trim() || `Exit code ${err.code}`;
290
- return { kind: "error", exitCode: err.code, stdout, stderr, errorMsg: msg };
384
+ return finishError(err, {});
291
385
  }
292
386
  if (err instanceof LeafInputError) {
293
- return {
294
- kind: "error",
295
- exitCode: 1,
296
- stdout,
297
- stderr: `${err.message}\n`,
298
- errorMsg: err.message,
299
- };
387
+ return finishError(err, { parseError: true });
300
388
  }
301
389
  if (err instanceof Error) {
302
- return {
303
- kind: "error",
304
- exitCode: 1,
305
- stdout,
306
- stderr: `${err.message}\n`,
307
- errorMsg: err.message,
308
- };
390
+ return finishError(err, {});
309
391
  }
310
- return {
311
- kind: "error",
312
- exitCode: 1,
313
- stdout,
314
- stderr: "Unknown error\n",
315
- errorMsg: "Unknown error",
316
- };
392
+ return finishError(err, {});
317
393
  } finally {
318
394
  process.exit = origExit;
319
395
  process.stdout.write = origStdoutWrite;
@@ -325,12 +401,28 @@ export class Cli {
325
401
  }
326
402
  }
327
403
 
328
- async serveMcp(): Promise<never> {
404
+ async serveMcp(overrides: ServeOverrides = {}): Promise<never> {
329
405
  try {
330
406
  if (this.program.mcpServer) {
331
407
  bootstrapMcpEnv(this.program.mcpServer);
332
408
  }
333
- bootstrapAppConfig(this.program, { validateFile: false });
409
+ const resolved = resolveMcpServeConfig(this.program, overrides);
410
+ const runtime = createServerRuntime(this.program, "mcp");
411
+ const emitter = new LogEmitter({ program: this.program, resolved: resolved.log });
412
+ this.server = {
413
+ runtime,
414
+ emitter,
415
+ mcp: resolved,
416
+ mcpHooks: this.program.mcpServer?.hooks,
417
+ };
418
+ bootstrapAppConfig(this.program, { validateFile: "soft", runtime, emitter });
419
+ const shutdown = () => {
420
+ emitter.emit({ level: "info", message: "server stopping", action: "server.stop" });
421
+ process.exit(0);
422
+ };
423
+ process.once("SIGINT", shutdown);
424
+ process.once("SIGTERM", shutdown);
425
+ emitter.emitLifecycle(`${this.program.key} ${this.program.version} — MCP ready (stdio)`, "mcp.server.ready");
334
426
  await mcpServeStdioLoop(this);
335
427
  process.exit(0);
336
428
  } catch (err) {
@@ -343,10 +435,25 @@ export class Cli {
343
435
  }
344
436
  }
345
437
 
346
- async serveApi(): Promise<never> {
438
+ async serveHttp(overrides: ServeOverrides = {}): Promise<never> {
347
439
  try {
348
- bootstrapAppConfig(this.program, { validateFile: false });
349
- await apiServeHttp(this);
440
+ const resolved = resolveHttpServeConfig(this.program, overrides);
441
+ const runtime = createServerRuntime(this.program, "http");
442
+ const emitter = new LogEmitter({ program: this.program, resolved: resolved.log });
443
+ this.server = {
444
+ runtime,
445
+ emitter,
446
+ http: resolved,
447
+ httpHooks: this.program.httpServer?.hooks,
448
+ };
449
+ bootstrapAppConfig(this.program, { validateFile: "soft", runtime, emitter });
450
+ const shutdown = () => {
451
+ emitter.emit({ level: "info", message: "server stopping", action: "server.stop" });
452
+ process.exit(0);
453
+ };
454
+ process.once("SIGINT", shutdown);
455
+ process.once("SIGTERM", shutdown);
456
+ await httpServeHttp(this, resolved);
350
457
  process.exit(0);
351
458
  } catch (err) {
352
459
  if (err instanceof Error) {
@@ -416,6 +523,7 @@ export class Cli {
416
523
  path: pr.path,
417
524
  args: pr.args,
418
525
  opts: pr.opts,
526
+ pathParams: pr.pathParams,
419
527
  helpExplicit: false,
420
528
  helpPath: [],
421
529
  errorMsg: msg,
@@ -432,6 +540,7 @@ export class Cli {
432
540
  path: pr.path,
433
541
  args: pr.args,
434
542
  opts: pr.opts,
543
+ pathParams: pr.pathParams,
435
544
  helpExplicit: false,
436
545
  helpPath: [],
437
546
  errorMsg: msg,
@@ -450,6 +559,7 @@ export class Cli {
450
559
  path: pr.path,
451
560
  args: pr.args,
452
561
  opts: pr.opts,
562
+ pathParams: pr.pathParams,
453
563
  helpExplicit: false,
454
564
  helpPath: [],
455
565
  errorMsg: msg,
@@ -0,0 +1,102 @@
1
+ /*
2
+ Per-surface exposure helpers (cli, http, mcpTool).
3
+ Parsing uses the full tree; presentation/schema/MCP/HTTP discovery use these filters.
4
+ */
5
+
6
+ import type { CliLeaf, CliNode, CliNodeBase, CliOption } from "~/core/types.ts";
7
+ import { isCliRouter } from "~/core/types.ts";
8
+
9
+ /** True when the node is omitted from CLI help, schema, and completions (still invocable). */
10
+ export function isCliHidden(node: CliNodeBase): boolean {
11
+ return node.cli?.hidden === true;
12
+ }
13
+
14
+ /** True when the option is omitted from CLI help, schema, and completions. */
15
+ export function isOptionCliHidden(opt: CliOption): boolean {
16
+ return opt.cli?.hidden === true;
17
+ }
18
+
19
+ /** True when the node is omitted from cli-schema export. */
20
+ export function isCliSchemaHidden(node: CliNodeBase): boolean {
21
+ if (node.cli?.schema?.enabled === false) {
22
+ return true;
23
+ }
24
+ if (node.cli?.schema?.hidden === true) {
25
+ return true;
26
+ }
27
+ return isCliHidden(node);
28
+ }
29
+
30
+ /** True when the node is omitted from shell completions. */
31
+ export function isCliCompletionsHidden(node: CliNodeBase): boolean {
32
+ if (node.cli?.completions?.enabled === false) {
33
+ return true;
34
+ }
35
+ if (node.cli?.completions?.hidden === true) {
36
+ return true;
37
+ }
38
+ return isCliHidden(node);
39
+ }
40
+
41
+ /** True when the leaf is omitted from MCP tools/list. */
42
+ export function isMcpHidden(leaf: CliLeaf): boolean {
43
+ if (leaf.mcpTool?.enabled === false) {
44
+ return true;
45
+ }
46
+ return leaf.mcpTool?.hidden === true;
47
+ }
48
+
49
+ /** True when the node is not callable via CLI (`cli.enabled: false`, cascades from parent). */
50
+ export function isCliCallable(node: CliNodeBase, parentEnabled = true): boolean {
51
+ if (!parentEnabled) {
52
+ return false;
53
+ }
54
+ if (node.cli?.enabled === false) {
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+
60
+ /** True when the leaf is omitted from HTTP route table / OpenAPI. */
61
+ export function isHttpHidden(node: CliNodeBase): boolean {
62
+ return node.http?.hidden === true;
63
+ }
64
+
65
+ /** True when the leaf is not exposed on HTTP (disabled or hidden). */
66
+ export function isHttpDisabled(node: CliNodeBase): boolean {
67
+ return node.http?.enabled === false;
68
+ }
69
+
70
+ /** Options visible in help, schema, completions, and MCP tool inputSchema. */
71
+ export function visibleOptions(options: CliOption[] | undefined): CliOption[] {
72
+ return (options ?? []).filter((o) => !isOptionCliHidden(o));
73
+ }
74
+
75
+ /** Strips CLI-hidden commands and options from one node for presentation export. */
76
+ export function presentationNode(node: CliNode): CliNode | null {
77
+ if (isCliHidden(node)) {
78
+ return null;
79
+ }
80
+ const options = visibleOptions(node.options);
81
+ if (isCliRouter(node)) {
82
+ const commands = node.commands.map((ch) => presentationNode(ch)).filter((ch): ch is CliNode => ch !== null);
83
+ return { ...node, options, commands };
84
+ }
85
+ return { ...node, options };
86
+ }
87
+
88
+ /** Subcommands visible in help listings. */
89
+ export function visibleSubcommands(cmds: CliNode[]): CliNode[] {
90
+ return cmds.filter((c) => !isCliHidden(c));
91
+ }
92
+
93
+ /** Default HTTP response metadata from a leaf `http` block. */
94
+ export function leafHttpResponseDefaults(leaf: CliLeaf): {
95
+ contentType?: string;
96
+ contentDisposition?: string;
97
+ } {
98
+ return {
99
+ contentType: leaf.http?.successContentType,
100
+ contentDisposition: leaf.http?.contentDisposition,
101
+ };
102
+ }
@@ -5,16 +5,16 @@ Domain-specific regression tests (split from index.test.ts).
5
5
  import { expect, test } from "bun:test";
6
6
  import { join } from "node:path";
7
7
  import { $ } from "bun";
8
- import { Cli, type CliContext, CliOptionKind } from "./index.ts";
9
- import { ParseKind, parse, postParseValidate } from "./parse.ts";
10
- import { testProgram, varargsReadFixture } from "./test-fixtures.ts";
11
- import type { CliLeaf } from "./types.ts";
12
- import { isCliRouter } from "./types.ts";
13
- import { cliValidateProgram } from "./validate.ts";
8
+ import { ParseKind, parse, postParseValidate } from "~/core/parse.ts";
9
+ import type { CliLeaf } from "~/core/types.ts";
10
+ import { isCliRouter } from "~/core/types.ts";
11
+ import { cliValidateProgram } from "~/core/validate.ts";
12
+ import { Cli, type CliContext, CliOptionKind } from "~/index";
13
+ import { testProgram, varargsReadFixture } from "~/test/fixtures.ts";
14
14
 
15
15
  /** Tests that ctx.invocation is cli via Cli.run. */
16
16
  test("ctx.invocation is cli via Cli.run", async () => {
17
- const indexPath = join(import.meta.dir, "index.ts");
17
+ const indexPath = join(import.meta.dir, "../index.ts");
18
18
  const { stdout } = await $`bun -e ${`
19
19
  import { Cli, CliProgram } from ${JSON.stringify(indexPath)};
20
20
  const program = {
@@ -45,6 +45,30 @@ test("ctx.invocation is mcp via Cli.invoke", async () => {
45
45
  expect(seen).toBe("mcp");
46
46
  });
47
47
 
48
+ /** Tests that ctx.locals.requestId is seeded before handler on invoke. */
49
+ test("Cli.invoke seeds ctx.locals.requestId", async () => {
50
+ let requestId = "";
51
+ const root = testProgram({
52
+ key: "app",
53
+ description: "",
54
+ hooks: {
55
+ beforeInvoke: (ctx: CliContext) => {
56
+ requestId = String(ctx.locals.requestId ?? "");
57
+ },
58
+ },
59
+ handler: () => {},
60
+ });
61
+ cliValidateProgram(root);
62
+ const wireId = "00000000-0000-4000-8000-000000000001";
63
+ const result = await new Cli(root).invoke([], {
64
+ invocation: "http",
65
+ requestId: wireId,
66
+ http: { request: new Request("http://localhost/"), clientIp: "127.0.0.1", requestId: wireId },
67
+ });
68
+ expect(result.kind).toBe("ok");
69
+ expect(requestId).toBe(wireId);
70
+ });
71
+
48
72
  /** Cli.invoke rejects invalid Enum value. */
49
73
  test("Cli.invoke rejects invalid Enum value", async () => {
50
74
  const root = testProgram({
@@ -0,0 +1,25 @@
1
+ /*
2
+ Per-server mutable handle context passed through HTTP/MCP dispatch.
3
+ */
4
+
5
+ import type { CliHttpWireHooks, CliMcpWireHooks, ServerRuntime, ServerState } from "~/core/types.ts";
6
+ import type { LogEmitter } from "~/log/emitter.ts";
7
+ import type { ResolvedHttpServeConfig, ResolvedMcpServeConfig } from "./overrides.ts";
8
+
9
+ /** Shared server state for one HTTP or MCP serve session. */
10
+ export interface ServerHandleContext {
11
+ runtime: ServerRuntime;
12
+ emitter: LogEmitter;
13
+ http?: ResolvedHttpServeConfig;
14
+ mcp?: ResolvedMcpServeConfig;
15
+ httpHooks?: CliHttpWireHooks;
16
+ mcpHooks?: CliMcpWireHooks;
17
+ }
18
+
19
+ /** Creates a fresh {@link ServerRuntime} for HTTP or MCP. */
20
+ export function createServerRuntime(
21
+ program: import("~/core/types.ts").CliProgram,
22
+ surface: "http" | "mcp",
23
+ ): ServerRuntime {
24
+ return { state: {} as ServerState, program, surface };
25
+ }
@@ -0,0 +1,112 @@
1
+ /*
2
+ CLI flag and programmatic overrides merged into HTTP/MCP server runtime config.
3
+ */
4
+
5
+ import { join } from "node:path";
6
+ import { resolveAppConfigDir } from "~/config/file.ts";
7
+ import type { CliProgram } from "~/core/types.ts";
8
+ import type { ResolvedLogConfig } from "~/log/emitter.ts";
9
+
10
+ /** Overrides from `myapp http` / `serveHttp()` flags and embedders. */
11
+ export interface ServeOverrides {
12
+ host?: string;
13
+ port?: number;
14
+ trustProxy?: boolean;
15
+ obscureErrors?: boolean;
16
+ logFormat?: "json" | "text";
17
+ logFile?: string;
18
+ noAccessLog?: boolean;
19
+ dev?: boolean;
20
+ }
21
+
22
+ /** Resolved HTTP listen and error options after merging schema + overrides. */
23
+ export interface ResolvedHttpServeConfig {
24
+ hostname: string;
25
+ port: number;
26
+ trustProxy: boolean;
27
+ obscureUnexpected: boolean;
28
+ log: ResolvedLogConfig;
29
+ }
30
+
31
+ /** Resolved MCP serve options after merging schema + overrides. */
32
+ export interface ResolvedMcpServeConfig {
33
+ obscureUnexpected: boolean;
34
+ log: ResolvedLogConfig;
35
+ }
36
+
37
+ function resolveLogFile(program: CliProgram, logFile: string | undefined): string | undefined {
38
+ if (!logFile) {
39
+ return undefined;
40
+ }
41
+ if (logFile.startsWith("/") || logFile.startsWith("~")) {
42
+ return logFile;
43
+ }
44
+ return join(resolveAppConfigDir(program), logFile);
45
+ }
46
+
47
+ /** Builds resolved HTTP server config (CLI flags > program schema). */
48
+ export function resolveHttpServeConfig(program: CliProgram, overrides: ServeOverrides = {}): ResolvedHttpServeConfig {
49
+ const http = program.httpServer;
50
+ const obscureUnexpected = overrides.obscureErrors ?? http?.errors?.obscureUnexpected ?? false;
51
+ return {
52
+ hostname: overrides.host ?? http?.host ?? "127.0.0.1",
53
+ port: overrides.port ?? http?.port ?? 3000,
54
+ trustProxy: overrides.trustProxy ?? http?.trustProxy ?? false,
55
+ obscureUnexpected,
56
+ log: {
57
+ format: overrides.logFormat ?? program.log?.format ?? "json",
58
+ file: resolveLogFile(program, overrides.logFile ?? program.log?.file),
59
+ access: overrides.noAccessLog ? false : (program.log?.access ?? true),
60
+ errors: program.log?.errors ?? true,
61
+ dev: overrides.dev ?? false,
62
+ },
63
+ };
64
+ }
65
+
66
+ /** Builds resolved MCP server config (CLI flags > program schema). */
67
+ export function resolveMcpServeConfig(program: CliProgram, overrides: ServeOverrides = {}): ResolvedMcpServeConfig {
68
+ const mcp = program.mcpServer;
69
+ return {
70
+ obscureUnexpected: overrides.obscureErrors ?? mcp?.errors?.obscureUnexpected ?? false,
71
+ log: {
72
+ format: overrides.logFormat ?? program.log?.format ?? "json",
73
+ file: resolveLogFile(program, overrides.logFile ?? program.log?.file),
74
+ access: program.log?.access ?? true,
75
+ errors: program.log?.errors ?? true,
76
+ dev: overrides.dev ?? false,
77
+ },
78
+ };
79
+ }
80
+
81
+ /** Parses `http` / `mcp serve` CLI opts into {@link ServeOverrides}. */
82
+ export function serveOverridesFromOpts(opts: Record<string, string>, surface: "http" | "mcp"): ServeOverrides {
83
+ const out: ServeOverrides = {};
84
+ if (opts.host) {
85
+ out.host = opts.host;
86
+ }
87
+ if (opts.port) {
88
+ const port = Number(opts.port);
89
+ if (!Number.isNaN(port)) {
90
+ out.port = port;
91
+ }
92
+ }
93
+ if (opts["trust-proxy"]) {
94
+ out.trustProxy = true;
95
+ }
96
+ if (opts["obscure-errors"]) {
97
+ out.obscureErrors = true;
98
+ }
99
+ if (opts["log-format"] === "json" || opts["log-format"] === "text") {
100
+ out.logFormat = opts["log-format"];
101
+ }
102
+ if (opts["log-file"]) {
103
+ out.logFile = opts["log-file"];
104
+ }
105
+ if (opts["no-access-log"] && surface === "http") {
106
+ out.noAccessLog = true;
107
+ }
108
+ if (opts.dev) {
109
+ out.dev = true;
110
+ }
111
+ return out;
112
+ }