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
@@ -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
+ }
package/src/index.ts CHANGED
@@ -7,40 +7,21 @@ It gives consumers one stable import path without forcing them to know the inter
7
7
  module layout.
8
8
  */
9
9
 
10
- export { generateOpenApi, openApiJson } from "./api/openapi.ts";
11
- export { Cli, type CliInvokeKind, type CliInvokeResult } from "./cli.ts";
12
- export { cliErrWithHelp } from "./cli-errors.ts";
13
10
  export { displayAppConfigPath, resolveAppConfigPath } from "./config/file.ts";
14
- export type { CliLeafInputs } from "./context.ts";
15
- export { CliContext } from "./context.ts";
11
+ export type { CliLeafInputs } from "./core/context.ts";
12
+ export { CliContext } from "./core/context.ts";
16
13
  export {
17
14
  parseCommaList,
18
15
  parseDate,
19
16
  parseDateTime,
20
17
  parseDurationMs,
21
- } from "./formats.ts";
22
- export type { HeadlessContext } from "./headless.ts";
23
- export {
24
- formatDryRunMessage,
25
- requireYesInNonTty,
26
- shouldRunHeadless,
27
- shouldRunHeadlessWithPositionals,
28
- shouldRunHeadlessWithYes,
29
- wantsExplicitJson,
30
- } from "./headless.ts";
18
+ } from "./core/formats.ts";
31
19
  export {
32
20
  LeafInputError,
33
- loadLeafInputs,
34
21
  preloadPipableJson,
35
22
  readJsonOptionValue,
36
- readLeafInputs,
37
- readLeafInputsAsync,
38
- } from "./leaf-inputs.ts";
39
- export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
40
- export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
23
+ } from "./core/leaf-inputs.ts";
41
24
  export type {
42
- CliApiResponseConfig,
43
- CliApiServerConfig,
44
25
  CliAppConfig,
45
26
  CliAppConfigEntry,
46
27
  CliAppConfigResolveContext,
@@ -49,27 +30,57 @@ export type {
49
30
  CliConfigureTargets,
50
31
  CliDocsConfig,
51
32
  CliDocsTopic,
33
+ ClientErrorOverride,
52
34
  CliHandler,
35
+ CliHttpServerConfig,
36
+ CliHttpWireContext,
37
+ CliHttpWireHooks,
53
38
  CliInvocation,
39
+ CliInvokeHookResult,
54
40
  CliLeafKind,
41
+ CliLocals,
42
+ CliLogConfig,
55
43
  CliMcpBundleConfig,
56
44
  CliMcpResource,
57
45
  CliMcpServerConfig,
58
46
  CliMcpToolConfig,
47
+ CliMcpWireContext,
48
+ CliMcpWireHooks,
59
49
  CliOption,
60
50
  CliPositional,
61
51
  CliProgram,
52
+ CliProgramHooks,
62
53
  CliRespondBody,
63
54
  CliRespondOptions,
55
+ ErrorHookContext,
64
56
  InstallAgentIntegration,
65
57
  InstallTargetSpec,
58
+ InvokeFailureKind,
59
+ InvokeHookContext,
60
+ ReadinessContext,
66
61
  ResolvedInstallTarget,
67
- } from "./types.ts";
62
+ ServerRuntime,
63
+ ServerState,
64
+ } from "./core/types.ts";
68
65
  export {
69
66
  CliFallbackMode,
70
67
  CliOptionKind,
71
68
  CliSchemaValidationError,
72
69
  CliValueFormat,
73
70
  isJsonLeaf,
74
- } from "./types.ts";
71
+ } from "./core/types.ts";
72
+ export type { HeadlessContext } from "./headless/routing.ts";
73
+ export {
74
+ formatDryRunMessage,
75
+ requireYesInNonTty,
76
+ shouldRunHeadless,
77
+ shouldRunHeadlessWithPositionals,
78
+ shouldRunHeadlessWithYes,
79
+ wantsExplicitJson,
80
+ } from "./headless/routing.ts";
81
+ export { generateOpenApi, openApiJson } from "./http/openapi.ts";
82
+ export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
83
+ export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
84
+ export { Cli, type CliInvokeKind, type CliInvokeResult } from "./runtime/cli.ts";
85
+ export { cliErrWithHelp } from "./runtime/cli-errors.ts";
75
86
  export { isInteractiveTty } from "./utils.ts";
@@ -0,0 +1,43 @@
1
+ /*
2
+ Unit tests for ECS log line formatting.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { formatEcsLine } from "./ecs.ts";
7
+
8
+ describe("formatEcsLine", () => {
9
+ test("includes service fields and event action", () => {
10
+ const line = formatEcsLine(
11
+ { name: "myapp", version: "7.0.0" },
12
+ {
13
+ level: "info",
14
+ message: "HTTP API listening",
15
+ action: "http.server.start",
16
+ },
17
+ );
18
+ const parsed = JSON.parse(line) as Record<string, unknown>;
19
+ expect(parsed["service.name"]).toBe("myapp");
20
+ expect(parsed["service.version"]).toBe("7.0.0");
21
+ expect(parsed["event.action"]).toBe("http.server.start");
22
+ expect(parsed.message).toBe("HTTP API listening");
23
+ expect(parsed["log.level"]).toBe("info");
24
+ expect(typeof parsed["@timestamp"]).toBe("string");
25
+ });
26
+
27
+ test("includes error stack fields", () => {
28
+ const err = new Error("boom");
29
+ const line = formatEcsLine(
30
+ { name: "app", version: "1.0.0" },
31
+ {
32
+ level: "error",
33
+ message: "invoke failed",
34
+ action: "invoke.error",
35
+ error: err,
36
+ },
37
+ );
38
+ const parsed = JSON.parse(line) as Record<string, unknown>;
39
+ expect(parsed["error.message"]).toBe("boom");
40
+ expect(parsed["error.type"]).toBe("Error");
41
+ expect(String(parsed["error.stack_trace"])).toContain("boom");
42
+ });
43
+ });