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
package/src/mcp/tools.ts CHANGED
@@ -3,11 +3,8 @@ This module maps CliProgram leaf nodes to MCP tool definitions and converts
3
3
  flat JSON tool arguments into argv for Cli.invoke.
4
4
  */
5
5
 
6
- import { docsMcpResources } from "../docs/mcp-resources.ts";
7
- import { cliResolveNotes } from "../help.ts";
8
- import { visibleOptions } from "../hidden.ts";
9
- import { collectOptionDefs } from "../parse.ts";
10
- import { cliSchemaJson } from "../schema.ts";
6
+ import { collectOptionDefs } from "~/core/parse.ts";
7
+ import { cliSchemaJson } from "~/core/schema.ts";
11
8
  import {
12
9
  type CliLeaf,
13
10
  type CliNode,
@@ -17,8 +14,12 @@ import {
17
14
  type CliProgram,
18
15
  CliValueFormat,
19
16
  isCliLeaf,
17
+ isJsonLeaf,
20
18
  leafOutputSchema,
21
- } from "../types.ts";
19
+ } from "~/core/types.ts";
20
+ import { docsMcpResources } from "~/docs/mcp-resources.ts";
21
+ import { cliResolveNotes } from "~/help.ts";
22
+ import { isMcpHidden, visibleOptions } from "~/runtime/exposure.ts";
22
23
 
23
24
  const DURATION_PATTERN = "^\\d+[hdms]?$";
24
25
 
@@ -27,7 +28,7 @@ export function defaultMcpSchemaUri(mcpId: string): string {
27
28
  return `${mcpId}://schema`;
28
29
  }
29
30
 
30
- export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "../docs/mcp-resources.ts";
31
+ export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "~/docs/mcp-resources.ts";
31
32
 
32
33
  /** Sanitizes a command key segment for MCP tool names and server identity. */
33
34
  export function sanitizeToolSegment(key: string): string {
@@ -43,8 +44,6 @@ export function mcpServerId(root: CliProgram): string {
43
44
  export interface McpToolDef {
44
45
  /** MCP tool name (underscore-separated, sanitized segments). */
45
46
  name: string;
46
- /** HTTP API tool name (hyphen-separated path; preserves command key spelling). */
47
- apiName: string;
48
47
  /** Tool description from the leaf command. */
49
48
  description: string;
50
49
  /** Command path segments from the program root. */
@@ -71,14 +70,6 @@ export function mcpToolName(root: CliProgram, path: string[]): string {
71
70
  return path.map(sanitizeToolSegment).join("_");
72
71
  }
73
72
 
74
- /** Builds the HTTP API tool name for a leaf at the given path (hyphen-joined, unsanitized). */
75
- export function apiToolName(root: CliProgram, path: string[]): string {
76
- if (path.length === 0) {
77
- return root.key;
78
- }
79
- return path.join("-");
80
- }
81
-
82
73
  /** JSON Schema property for one option. */
83
74
  function optionProperty(opt: CliOption): Record<string, unknown> {
84
75
  const base: Record<string, unknown> = { description: opt.description };
@@ -121,7 +112,7 @@ function optionProperty(opt: CliOption): Record<string, unknown> {
121
112
  }
122
113
  }
123
114
 
124
- function formatMcpOptionValue(opt: CliOption, val: unknown): string | { error: string } {
115
+ export function formatMcpOptionValue(opt: CliOption, val: unknown): string | { error: string } {
125
116
  if (opt.format === CliValueFormat.CommaList) {
126
117
  if (Array.isArray(val)) {
127
118
  const items = val.map(String).filter(Boolean);
@@ -150,6 +141,10 @@ function positionalProperty(p: CliPositional): Record<string, unknown> {
150
141
 
151
142
  /** Builds inputSchema for a leaf command. */
152
143
  function buildInputSchema(root: CliProgram, path: string[], leaf: CliLeaf): Record<string, unknown> {
144
+ if (isJsonLeaf(leaf) && leaf.inputSchema !== undefined) {
145
+ return leaf.inputSchema;
146
+ }
147
+
153
148
  const properties: Record<string, unknown> = {};
154
149
  const required: string[] = [];
155
150
 
@@ -233,13 +228,12 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
233
228
  if (cmd.key === "completion" || cmd.key === "configure" || cmd.key === "mcp" || cmd.key === "version") {
234
229
  return;
235
230
  }
236
- if (cmd.hidden || cmd.mcpTool?.enabled === false) {
231
+ if (isMcpHidden(cmd)) {
237
232
  return;
238
233
  }
239
234
  const outputSchema = leafOutputSchema(cmd);
240
235
  out.push({
241
236
  name: mcpToolName(root, path),
242
- apiName: apiToolName(root, path),
243
237
  description: resolveToolDescription(root, path, cmd),
244
238
  path,
245
239
  leaf: cmd,
@@ -286,6 +280,10 @@ export function mcpToolCallToArgv(
286
280
  tool: McpToolDef,
287
281
  args: Record<string, unknown>,
288
282
  ): string[] | { error: string } {
283
+ if (isJsonLeaf(tool.leaf)) {
284
+ return [...tool.path];
285
+ }
286
+
289
287
  const argv = [...tool.path];
290
288
 
291
289
  for (const opt of collectOptionDefs(root, tool.path)) {
@@ -3,12 +3,12 @@ Internal capability resolver — decides which platform builtins are active for
3
3
  Not exported from the public package barrel.
4
4
  */
5
5
 
6
- import { configCommandsEnabled } from "./config/entry.ts";
7
- import type { CliProgram } from "./types.ts";
6
+ import { configCommandsEnabled } from "~/config/entry.ts";
7
+ import type { CliProgram } from "~/core/types.ts";
8
8
 
9
9
  /** Platform builtins derived from program config and runtime. */
10
10
  export interface CliCapabilities {
11
- api: boolean;
11
+ http: boolean;
12
12
  completion: boolean;
13
13
  mcp: boolean;
14
14
  configure: boolean;
@@ -20,11 +20,11 @@ export interface CliCapabilities {
20
20
  export function resolveCapabilities(program: CliProgram): CliCapabilities {
21
21
  const configure = program.configure?.enabled !== false;
22
22
  return {
23
- api: program.apiServer?.enabled === true,
23
+ http: program.httpServer?.enabled === true,
24
24
  completion: program.completion?.enabled !== false,
25
25
  mcp: program.mcpServer?.enabled === true,
26
26
  configure,
27
- docs: program.docs?.enabled === true,
27
+ docs: program.docs?.enabled !== false,
28
28
  configCommands: configCommandsEnabled(program),
29
29
  };
30
30
  }
@@ -44,8 +44,8 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
44
44
  if (caps.mcp) {
45
45
  names.push("mcp");
46
46
  }
47
- if (caps.api) {
48
- names.push("api");
47
+ if (caps.http) {
48
+ names.push("http");
49
49
  }
50
50
  return names;
51
51
  }
@@ -65,14 +65,14 @@ export function skipsRequiredAppConfigExit(path: string[], caps: CliCapabilities
65
65
  return false;
66
66
  }
67
67
 
68
- export type CapabilityFeature = "api" | "mcp" | "configure" | "docs" | "completion";
68
+ export type CapabilityFeature = "http" | "mcp" | "configure" | "docs" | "completion";
69
69
 
70
70
  /** Stderr message when a disabled built-in is invoked from the CLI. */
71
71
  export function capabilityDeniedMessage(feature: CapabilityFeature): string {
72
72
  switch (feature) {
73
73
  case "completion":
74
74
  return "Shell completion is not available for this app.\n";
75
- case "api":
75
+ case "http":
76
76
  return "HTTP API is not available for this app.\n";
77
77
  case "mcp":
78
78
  return "MCP is not available for this app.\n";
@@ -97,8 +97,8 @@ export function assertBuiltinAllowed(argv: string[], caps: CliCapabilities): voi
97
97
  process.stderr.write(capabilityDeniedMessage("mcp"));
98
98
  process.exit(1);
99
99
  }
100
- if (first === "api" && !caps.api) {
101
- process.stderr.write(capabilityDeniedMessage("api"));
100
+ if (first === "http" && !caps.http) {
101
+ process.stderr.write(capabilityDeniedMessage("http"));
102
102
  process.exit(1);
103
103
  }
104
104
  if (first === "configure" && !caps.configure) {
@@ -2,12 +2,12 @@
2
2
  Handler error helper with contextual help.
3
3
  */
4
4
 
5
- import { cliPresentationRoot } from "./builtins/presentation.ts";
6
- import type { CliContext } from "./context.ts";
7
- import { cliHelpRender } from "./help.ts";
5
+ import { cliPresentationRoot } from "~/builtins/presentation.ts";
6
+ import type { CliContext } from "~/core/context.ts";
7
+ import { cliHelpRender } from "~/help.ts";
8
8
 
9
9
  export function cliErrWithHelp(ctx: CliContext, msg: string): never {
10
- if (ctx.invocation === "api" || ctx.invocation === "mcp") {
10
+ if (ctx.invocation === "http" || ctx.invocation === "mcp") {
11
11
  throw new Error(msg);
12
12
  }
13
13
  const color = process.stderr.isTTY;
@@ -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
 
@@ -127,7 +147,7 @@ export class Cli {
127
147
 
128
148
  let preloadedJson: Record<string, unknown> = {};
129
149
  try {
130
- preloadedJson = await preloadPipableJson(this.program, pr.path, pr.opts, "cli");
150
+ preloadedJson = await preloadPipableJson(this.program, pr.path, pr.opts, "cli", pr.args);
131
151
  } catch (err) {
132
152
  if (err instanceof LeafInputError) {
133
153
  this.exitLeafInputError(err, pr.path);
@@ -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
+ }