argsbarg 6.1.4 → 6.1.5
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.
- package/CHANGELOG.md +11 -2
- package/README.md +1 -1
- package/docs/http-server.md +20 -15
- package/examples/full-example/docs/cli-schema.json +9 -9
- package/examples/full-example/docs/cli.md +9 -9
- package/examples/full-example/docs/http.md +17 -17
- package/examples/full-example/docs/openapi.json +313 -14
- package/examples/servers.ts +2 -2
- package/index.d.ts +6 -1
- package/package.json +1 -1
- package/src/builtins/http.ts +3 -1
- package/src/core/types.ts +6 -1
- package/src/core/validate.ts +43 -0
- package/src/docs/http-guide.ts +12 -8
- package/src/http/openapi.ts +7 -7
- package/src/http/paths.ts +39 -0
- package/src/http/readiness.ts +1 -1
- package/src/http/routes.ts +22 -9
- package/src/http/server.ts +7 -11
- package/src/test/integration/http.test.ts +63 -33
|
@@ -5,10 +5,285 @@
|
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"description": "Argsbarg full example reference app"
|
|
7
7
|
},
|
|
8
|
+
"tags": [
|
|
9
|
+
{
|
|
10
|
+
"name": "health",
|
|
11
|
+
"description": "Server health probes"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "echo",
|
|
15
|
+
"description": "Echo a message (MCP-friendly leaf)."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "render-json",
|
|
19
|
+
"description": "Echo a JSON message (schema-first JSON leaf demo)."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "status",
|
|
23
|
+
"description": "Show app version."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "workspaces",
|
|
27
|
+
"description": "Workspace collection and CRUD."
|
|
28
|
+
}
|
|
29
|
+
],
|
|
8
30
|
"paths": {
|
|
9
|
-
"/
|
|
31
|
+
"/health": {
|
|
32
|
+
"get": {
|
|
33
|
+
"tags": [
|
|
34
|
+
"health"
|
|
35
|
+
],
|
|
36
|
+
"operationId": "health",
|
|
37
|
+
"summary": "Liveness probe (alias of /health/liveness)",
|
|
38
|
+
"responses": {
|
|
39
|
+
"200": {
|
|
40
|
+
"description": "Server is listening",
|
|
41
|
+
"content": {
|
|
42
|
+
"application/json; charset=utf-8": {
|
|
43
|
+
"schema": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"ok": {
|
|
47
|
+
"type": "boolean",
|
|
48
|
+
"const": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"required": [
|
|
52
|
+
"ok"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"/health/liveness": {
|
|
62
|
+
"get": {
|
|
63
|
+
"tags": [
|
|
64
|
+
"health"
|
|
65
|
+
],
|
|
66
|
+
"operationId": "health_liveness",
|
|
67
|
+
"summary": "Liveness probe",
|
|
68
|
+
"responses": {
|
|
69
|
+
"200": {
|
|
70
|
+
"description": "Server is listening",
|
|
71
|
+
"content": {
|
|
72
|
+
"application/json; charset=utf-8": {
|
|
73
|
+
"schema": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"ok": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"const": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"required": [
|
|
82
|
+
"ok"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"/health/readiness": {
|
|
92
|
+
"get": {
|
|
93
|
+
"tags": [
|
|
94
|
+
"health"
|
|
95
|
+
],
|
|
96
|
+
"operationId": "health_readiness",
|
|
97
|
+
"summary": "Readiness probe",
|
|
98
|
+
"description": "Config file, required app config, and optional program.readiness checks.",
|
|
99
|
+
"responses": {
|
|
100
|
+
"200": {
|
|
101
|
+
"description": "Ready to serve traffic",
|
|
102
|
+
"content": {
|
|
103
|
+
"application/json; charset=utf-8": {
|
|
104
|
+
"schema": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"ok": {
|
|
108
|
+
"type": "boolean"
|
|
109
|
+
},
|
|
110
|
+
"checks": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"config_file": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": {
|
|
116
|
+
"ok": {
|
|
117
|
+
"type": "boolean"
|
|
118
|
+
},
|
|
119
|
+
"error": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"missing": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"items": {
|
|
125
|
+
"type": "string"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"required": [
|
|
130
|
+
"ok"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"config_required": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"ok": {
|
|
137
|
+
"type": "boolean"
|
|
138
|
+
},
|
|
139
|
+
"error": {
|
|
140
|
+
"type": "string"
|
|
141
|
+
},
|
|
142
|
+
"missing": {
|
|
143
|
+
"type": "array",
|
|
144
|
+
"items": {
|
|
145
|
+
"type": "string"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"required": [
|
|
150
|
+
"ok"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"custom": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"ok": {
|
|
157
|
+
"type": "boolean"
|
|
158
|
+
},
|
|
159
|
+
"error": {
|
|
160
|
+
"type": "string"
|
|
161
|
+
},
|
|
162
|
+
"missing": {
|
|
163
|
+
"type": "array",
|
|
164
|
+
"items": {
|
|
165
|
+
"type": "string"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"required": [
|
|
170
|
+
"ok"
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"required": [
|
|
175
|
+
"config_file",
|
|
176
|
+
"config_required",
|
|
177
|
+
"custom"
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"required": [
|
|
182
|
+
"ok",
|
|
183
|
+
"checks"
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"503": {
|
|
190
|
+
"description": "Not ready",
|
|
191
|
+
"content": {
|
|
192
|
+
"application/json; charset=utf-8": {
|
|
193
|
+
"schema": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"properties": {
|
|
196
|
+
"ok": {
|
|
197
|
+
"type": "boolean"
|
|
198
|
+
},
|
|
199
|
+
"checks": {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"config_file": {
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"ok": {
|
|
206
|
+
"type": "boolean"
|
|
207
|
+
},
|
|
208
|
+
"error": {
|
|
209
|
+
"type": "string"
|
|
210
|
+
},
|
|
211
|
+
"missing": {
|
|
212
|
+
"type": "array",
|
|
213
|
+
"items": {
|
|
214
|
+
"type": "string"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"required": [
|
|
219
|
+
"ok"
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
"config_required": {
|
|
223
|
+
"type": "object",
|
|
224
|
+
"properties": {
|
|
225
|
+
"ok": {
|
|
226
|
+
"type": "boolean"
|
|
227
|
+
},
|
|
228
|
+
"error": {
|
|
229
|
+
"type": "string"
|
|
230
|
+
},
|
|
231
|
+
"missing": {
|
|
232
|
+
"type": "array",
|
|
233
|
+
"items": {
|
|
234
|
+
"type": "string"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"required": [
|
|
239
|
+
"ok"
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
"custom": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"properties": {
|
|
245
|
+
"ok": {
|
|
246
|
+
"type": "boolean"
|
|
247
|
+
},
|
|
248
|
+
"error": {
|
|
249
|
+
"type": "string"
|
|
250
|
+
},
|
|
251
|
+
"missing": {
|
|
252
|
+
"type": "array",
|
|
253
|
+
"items": {
|
|
254
|
+
"type": "string"
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"required": [
|
|
259
|
+
"ok"
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
"required": [
|
|
264
|
+
"config_file",
|
|
265
|
+
"config_required",
|
|
266
|
+
"custom"
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"required": [
|
|
271
|
+
"ok",
|
|
272
|
+
"checks"
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
"/echo": {
|
|
10
282
|
"post": {
|
|
11
|
-
"
|
|
283
|
+
"tags": [
|
|
284
|
+
"echo"
|
|
285
|
+
],
|
|
286
|
+
"operationId": "_echo",
|
|
12
287
|
"summary": "Echo a message (MCP-friendly leaf).",
|
|
13
288
|
"responses": {
|
|
14
289
|
"201": {
|
|
@@ -115,9 +390,12 @@
|
|
|
115
390
|
}
|
|
116
391
|
}
|
|
117
392
|
},
|
|
118
|
-
"/
|
|
393
|
+
"/render-json": {
|
|
119
394
|
"post": {
|
|
120
|
-
"
|
|
395
|
+
"tags": [
|
|
396
|
+
"render-json"
|
|
397
|
+
],
|
|
398
|
+
"operationId": "_render-json",
|
|
121
399
|
"summary": "Echo a JSON message (schema-first JSON leaf demo).",
|
|
122
400
|
"responses": {
|
|
123
401
|
"201": {
|
|
@@ -226,9 +504,12 @@
|
|
|
226
504
|
}
|
|
227
505
|
}
|
|
228
506
|
},
|
|
229
|
-
"/
|
|
507
|
+
"/status": {
|
|
230
508
|
"post": {
|
|
231
|
-
"
|
|
509
|
+
"tags": [
|
|
510
|
+
"status"
|
|
511
|
+
],
|
|
512
|
+
"operationId": "_status",
|
|
232
513
|
"summary": "Show app version.",
|
|
233
514
|
"responses": {
|
|
234
515
|
"201": {
|
|
@@ -343,9 +624,12 @@
|
|
|
343
624
|
}
|
|
344
625
|
}
|
|
345
626
|
},
|
|
346
|
-
"/
|
|
627
|
+
"/workspaces": {
|
|
347
628
|
"get": {
|
|
348
|
-
"
|
|
629
|
+
"tags": [
|
|
630
|
+
"workspaces"
|
|
631
|
+
],
|
|
632
|
+
"operationId": "_workspaces",
|
|
349
633
|
"summary": "List workspaces.",
|
|
350
634
|
"responses": {
|
|
351
635
|
"200": {
|
|
@@ -434,7 +718,10 @@
|
|
|
434
718
|
"parameters": []
|
|
435
719
|
},
|
|
436
720
|
"post": {
|
|
437
|
-
"
|
|
721
|
+
"tags": [
|
|
722
|
+
"workspaces"
|
|
723
|
+
],
|
|
724
|
+
"operationId": "_workspaces",
|
|
438
725
|
"summary": "Create a workspace.",
|
|
439
726
|
"responses": {
|
|
440
727
|
"201": {
|
|
@@ -543,9 +830,12 @@
|
|
|
543
830
|
}
|
|
544
831
|
}
|
|
545
832
|
},
|
|
546
|
-
"/
|
|
833
|
+
"/workspaces/{id}": {
|
|
547
834
|
"get": {
|
|
548
|
-
"
|
|
835
|
+
"tags": [
|
|
836
|
+
"workspaces"
|
|
837
|
+
],
|
|
838
|
+
"operationId": "_workspaces_id",
|
|
549
839
|
"summary": "Get one workspace.",
|
|
550
840
|
"responses": {
|
|
551
841
|
"200": {
|
|
@@ -643,7 +933,10 @@
|
|
|
643
933
|
]
|
|
644
934
|
},
|
|
645
935
|
"put": {
|
|
646
|
-
"
|
|
936
|
+
"tags": [
|
|
937
|
+
"workspaces"
|
|
938
|
+
],
|
|
939
|
+
"operationId": "_workspaces_id",
|
|
647
940
|
"summary": "Replace a workspace.",
|
|
648
941
|
"responses": {
|
|
649
942
|
"200": {
|
|
@@ -762,7 +1055,10 @@
|
|
|
762
1055
|
}
|
|
763
1056
|
},
|
|
764
1057
|
"patch": {
|
|
765
|
-
"
|
|
1058
|
+
"tags": [
|
|
1059
|
+
"workspaces"
|
|
1060
|
+
],
|
|
1061
|
+
"operationId": "_workspaces_id",
|
|
766
1062
|
"summary": "Patch a workspace name.",
|
|
767
1063
|
"responses": {
|
|
768
1064
|
"200": {
|
|
@@ -881,7 +1177,10 @@
|
|
|
881
1177
|
}
|
|
882
1178
|
},
|
|
883
1179
|
"delete": {
|
|
884
|
-
"
|
|
1180
|
+
"tags": [
|
|
1181
|
+
"workspaces"
|
|
1182
|
+
],
|
|
1183
|
+
"operationId": "_workspaces_id",
|
|
885
1184
|
"summary": "Delete a workspace.",
|
|
886
1185
|
"responses": {
|
|
887
1186
|
"204": {
|
package/examples/servers.ts
CHANGED
|
@@ -4,10 +4,10 @@ This example shows the smallest end-to-end CLI+MCP+API setup.
|
|
|
4
4
|
It includes one command, a couple of options, and a direct call to the runtime so
|
|
5
5
|
readers can copy the pattern into their own scripts quickly.
|
|
6
6
|
|
|
7
|
-
Demonstrates: `servers.ts hello`, MCP tool `hello`, and `POST /
|
|
7
|
+
Demonstrates: `servers.ts hello`, MCP tool `hello`, and `POST /hello`.
|
|
8
8
|
|
|
9
9
|
Ex API Call:
|
|
10
|
-
curl -s -X POST http://127.0.0.1:3000/
|
|
10
|
+
curl -s -X POST http://127.0.0.1:3000/hello \
|
|
11
11
|
-H 'content-type: application/json' \
|
|
12
12
|
-d '{"name":"alice"}'
|
|
13
13
|
Ex API Response:
|
package/index.d.ts
CHANGED
|
@@ -353,6 +353,11 @@ export interface CliHttpServerConfig {
|
|
|
353
353
|
host?: string;
|
|
354
354
|
/** Listen port (default: `3000`). */
|
|
355
355
|
port?: number;
|
|
356
|
+
/**
|
|
357
|
+
* URL prefix for user command routes (default: `""` — routes at server root, e.g. `/workspaces`).
|
|
358
|
+
* Set to `"/api"` for `/api/workspaces`-style paths.
|
|
359
|
+
*/
|
|
360
|
+
pathPrefix?: string;
|
|
356
361
|
/** Honor `X-Forwarded-For` for client IP in hooks and logs. */
|
|
357
362
|
trustProxy?: boolean;
|
|
358
363
|
/** HTTP error response defaults. */
|
|
@@ -744,7 +749,7 @@ export type CliProgram = CliNode & {
|
|
|
744
749
|
docs?: CliDocsConfig;
|
|
745
750
|
/** Invoke and error hooks for user commands on CLI, HTTP, and MCP. */
|
|
746
751
|
hooks?: CliProgramHooks;
|
|
747
|
-
/** Optional readiness probe for HTTP/MCP `GET /health/
|
|
752
|
+
/** Optional readiness probe for HTTP/MCP `GET /health/readiness` only. */
|
|
748
753
|
readiness?: (ctx: ReadinessContext) => boolean | Promise<boolean>;
|
|
749
754
|
/** Framework logging (stderr + optional file). */
|
|
750
755
|
log?: CliLogConfig;
|
package/package.json
CHANGED
package/src/builtins/http.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type CliRouter,
|
|
8
8
|
} from "~/core/types.ts";
|
|
9
9
|
import { docsEnabled } from "~/docs/resolve.ts";
|
|
10
|
+
import { httpUserPathGlob, resolveHttpPathPrefix } from "~/http/paths.ts";
|
|
10
11
|
import { resolveHttpListenAddress } from "~/http/server.ts";
|
|
11
12
|
import { resolveCapabilities } from "~/runtime/capabilities.ts";
|
|
12
13
|
|
|
@@ -34,10 +35,11 @@ const HTTP_SERVE_OPTIONS: CliOption[] = [
|
|
|
34
35
|
export function cliBuiltinHttpCommand(program: CliProgram): CliRouter {
|
|
35
36
|
const caps = resolveCapabilities(program);
|
|
36
37
|
const { hostname, port } = resolveHttpListenAddress(program);
|
|
38
|
+
const userGlob = httpUserPathGlob(resolveHttpPathPrefix(program));
|
|
37
39
|
const lines = [
|
|
38
40
|
`HTTP tool server on http://${hostname}:${port}.`,
|
|
39
41
|
"",
|
|
40
|
-
|
|
42
|
+
`Endpoints: GET /health, GET /health/readiness, GET /openapi.json, GET /swagger, ${userGlob}`,
|
|
41
43
|
"",
|
|
42
44
|
];
|
|
43
45
|
if (caps.configure) {
|
package/src/core/types.ts
CHANGED
|
@@ -246,6 +246,11 @@ export interface CliHttpServerConfig {
|
|
|
246
246
|
host?: string;
|
|
247
247
|
/** Listen port (default: `3000`). */
|
|
248
248
|
port?: number;
|
|
249
|
+
/**
|
|
250
|
+
* URL prefix for user command routes (default: `""` — routes at server root, e.g. `/workspaces`).
|
|
251
|
+
* Set to `"/api"` for `/api/workspaces`-style paths.
|
|
252
|
+
*/
|
|
253
|
+
pathPrefix?: string;
|
|
249
254
|
/** Honor `X-Forwarded-For` for client IP in hooks and logs. */
|
|
250
255
|
trustProxy?: boolean;
|
|
251
256
|
/** HTTP error response defaults. */
|
|
@@ -669,7 +674,7 @@ export type CliProgram = CliNode & {
|
|
|
669
674
|
docs?: CliDocsConfig;
|
|
670
675
|
/** Invoke and error hooks for user commands on CLI, HTTP, and MCP. */
|
|
671
676
|
hooks?: CliProgramHooks;
|
|
672
|
-
/** Optional readiness probe for HTTP/MCP `GET /health/
|
|
677
|
+
/** Optional readiness probe for HTTP/MCP `GET /health/readiness` only. */
|
|
673
678
|
readiness?: (ctx: ReadinessContext) => boolean | Promise<boolean>;
|
|
674
679
|
/** Framework logging (stderr + optional file). */
|
|
675
680
|
log?: CliLogConfig;
|
package/src/core/validate.ts
CHANGED
|
@@ -5,6 +5,7 @@ This module validates CLI schemas before execution.
|
|
|
5
5
|
import { AGENT_PAIRS, MCP_KEYS, mcpServerRequiredForArtifact } from "~/configure/artifacts/target-registry.ts";
|
|
6
6
|
import { reservedDocsTopicResourceUris } from "~/docs/mcp-resources.ts";
|
|
7
7
|
import { DOCS_BUILTIN_TOPIC_KEYS, docsEnabled } from "~/docs/resolve.ts";
|
|
8
|
+
import { HTTP_RESERVED_TOP_LEVEL_SEGMENTS } from "~/http/paths.ts";
|
|
8
9
|
import { resolveMcpSchemaUri } from "~/mcp/tools.ts";
|
|
9
10
|
import { reservedCommandNames, resolveCapabilities } from "~/runtime/capabilities.ts";
|
|
10
11
|
import { validateFormatValue } from "./formats.ts";
|
|
@@ -180,6 +181,8 @@ export function cliValidateProgram(program: CliProgram): void {
|
|
|
180
181
|
throw new CliSchemaValidationError("httpServer requires enabled: true; omit httpServer to disable HTTP API");
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
validateHttpPathPrefix(program);
|
|
185
|
+
|
|
183
186
|
if (docsEnabled(program) && program.docs?.topics !== undefined) {
|
|
184
187
|
validateDocsConfig(program.docs);
|
|
185
188
|
}
|
|
@@ -212,6 +215,46 @@ function isParamRouterKey(key: string): boolean {
|
|
|
212
215
|
return key.startsWith(":");
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
/** Validates `httpServer.pathPrefix` and reserved top-level command keys. */
|
|
219
|
+
function validateHttpPathPrefix(program: CliProgram): void {
|
|
220
|
+
if (!program.httpServer?.enabled) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const raw = program.httpServer.pathPrefix;
|
|
224
|
+
if (raw !== undefined && raw !== "") {
|
|
225
|
+
if (!raw.startsWith("/")) {
|
|
226
|
+
throw new CliSchemaValidationError(`httpServer.pathPrefix must start with / (got ${JSON.stringify(raw)})`);
|
|
227
|
+
}
|
|
228
|
+
if (raw.length > 1 && raw.endsWith("/")) {
|
|
229
|
+
throw new CliSchemaValidationError(`httpServer.pathPrefix must not end with / (got ${JSON.stringify(raw)})`);
|
|
230
|
+
}
|
|
231
|
+
if (raw.includes("//")) {
|
|
232
|
+
throw new CliSchemaValidationError("httpServer.pathPrefix must not contain //");
|
|
233
|
+
}
|
|
234
|
+
if (raw === "/health" || raw === "/swagger" || raw === "/openapi.json" || raw === "/tools") {
|
|
235
|
+
throw new CliSchemaValidationError(
|
|
236
|
+
`httpServer.pathPrefix must not be a framework path (got ${JSON.stringify(raw)})`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!isCliRouter(program)) {
|
|
242
|
+
if (isCliLeaf(program) && HTTP_RESERVED_TOP_LEVEL_SEGMENTS.has(program.key)) {
|
|
243
|
+
throw new CliSchemaValidationError(
|
|
244
|
+
`Reserved HTTP program key when httpServer.pathPrefix is empty: ${program.key}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const child of program.commands) {
|
|
250
|
+
if (HTTP_RESERVED_TOP_LEVEL_SEGMENTS.has(child.key)) {
|
|
251
|
+
throw new CliSchemaValidationError(
|
|
252
|
+
`Reserved HTTP command name when httpServer.pathPrefix is empty: ${child.key} (set httpServer.pathPrefix or rename)`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
215
258
|
function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
216
259
|
if (!isRoot) {
|
|
217
260
|
const rogue = node as CliProgram;
|
package/src/docs/http-guide.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defaultConfigEntryTitle } from "~/config/entry.ts";
|
|
|
2
2
|
import { displayAppConfigPath } from "~/config/file.ts";
|
|
3
3
|
import { collectOptionDefs } from "~/core/parse.ts";
|
|
4
4
|
import { CliOptionKind, type CliProgram } from "~/core/types.ts";
|
|
5
|
+
import { httpUserPathGlob, resolveHttpPathPrefix } from "~/http/paths.ts";
|
|
5
6
|
import { collectHttpRoutes } from "~/http/routes.ts";
|
|
6
7
|
import { resolveHttpListenAddress } from "~/http/server.ts";
|
|
7
8
|
|
|
@@ -27,6 +28,9 @@ export function generateHttpGuide(root: CliProgram): string {
|
|
|
27
28
|
const routes = collectHttpRoutes(root);
|
|
28
29
|
const { hostname, port } = resolveHttpListenAddress(root);
|
|
29
30
|
const baseUrl = `http://${hostname}:${port}`;
|
|
31
|
+
const pathPrefix = resolveHttpPathPrefix(root);
|
|
32
|
+
const userGlob = httpUserPathGlob(pathPrefix);
|
|
33
|
+
const sampleWorkspaces = `${pathPrefix}/workspaces`;
|
|
30
34
|
|
|
31
35
|
const lines: string[] = [
|
|
32
36
|
`# HTTP API (${root.key})`,
|
|
@@ -47,23 +51,23 @@ export function generateHttpGuide(root: CliProgram): string {
|
|
|
47
51
|
"",
|
|
48
52
|
"| Method | Path | Purpose |",
|
|
49
53
|
"| --- | --- | --- |",
|
|
50
|
-
"| `GET` | `/health` or `/health/
|
|
51
|
-
"| `GET` | `/health/
|
|
54
|
+
"| `GET` | `/health` or `/health/liveness` | Liveness check |",
|
|
55
|
+
"| `GET` | `/health/readiness` | Readiness (config + `program.readiness`) |",
|
|
52
56
|
"| `GET` | `/openapi.json` | OpenAPI 3.1 REST paths |",
|
|
53
57
|
"| `GET` | `/swagger` | Interactive Swagger UI API reference |",
|
|
54
|
-
|
|
58
|
+
`| * | \`${userGlob}\` | Invoke user commands (method per route) |`,
|
|
55
59
|
"| `OPTIONS` | `*` | CORS preflight |",
|
|
56
60
|
"",
|
|
57
|
-
|
|
61
|
+
`Discover paths from \`openapi.json\` (\`${userGlob}\`). Query binds options; POST/PUT/PATCH body binds options and \`inputSchema\` fields.`,
|
|
58
62
|
"",
|
|
59
63
|
"## Examples",
|
|
60
64
|
"",
|
|
61
65
|
"```bash",
|
|
62
66
|
`curl -s ${baseUrl}/health`,
|
|
63
|
-
`curl -s ${baseUrl}/health/
|
|
67
|
+
`curl -s ${baseUrl}/health/readiness`,
|
|
64
68
|
`curl -s ${baseUrl}/openapi.json`,
|
|
65
|
-
`curl -s ${baseUrl}
|
|
66
|
-
`curl -s -X POST ${baseUrl}
|
|
69
|
+
`curl -s ${baseUrl}${sampleWorkspaces}`,
|
|
70
|
+
`curl -s -X POST ${baseUrl}${sampleWorkspaces} \\`,
|
|
67
71
|
' -H "content-type: application/json" \\',
|
|
68
72
|
` -d '{"name":"qa2"}'`,
|
|
69
73
|
"```",
|
|
@@ -127,7 +131,7 @@ export function generateHttpGuide(root: CliProgram): string {
|
|
|
127
131
|
`- **Fetch** — \`curl -s ${baseUrl}/openapi.json\``,
|
|
128
132
|
`- **Save offline** — \`${root.key} docs openapi --save\` → \`./docs/openapi.json\` (or \`just docgen\` in app repos)`,
|
|
129
133
|
"",
|
|
130
|
-
|
|
134
|
+
`Use the spec to discover REST paths and request/response shapes before calling \`${userGlob}\`.`,
|
|
131
135
|
"",
|
|
132
136
|
);
|
|
133
137
|
|
package/src/http/openapi.ts
CHANGED
|
@@ -161,19 +161,19 @@ function livenessGetOp(operationId: string, summary: string): Record<string, unk
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** Framework health probe paths served alongside
|
|
164
|
+
/** Framework health probe paths served alongside user command routes. */
|
|
165
165
|
function buildHealthPaths(): Record<string, unknown> {
|
|
166
166
|
return {
|
|
167
167
|
"/health": {
|
|
168
|
-
get: livenessGetOp("health", "Liveness probe (alias of /health/
|
|
168
|
+
get: livenessGetOp("health", "Liveness probe (alias of /health/liveness)"),
|
|
169
169
|
},
|
|
170
|
-
"/health/
|
|
171
|
-
get: livenessGetOp("
|
|
170
|
+
"/health/liveness": {
|
|
171
|
+
get: livenessGetOp("health_liveness", "Liveness probe"),
|
|
172
172
|
},
|
|
173
|
-
"/health/
|
|
173
|
+
"/health/readiness": {
|
|
174
174
|
get: {
|
|
175
175
|
tags: [HEALTH_TAG],
|
|
176
|
-
operationId: "
|
|
176
|
+
operationId: "health_readiness",
|
|
177
177
|
summary: "Readiness probe",
|
|
178
178
|
description: "Config file, required app config, and optional program.readiness checks.",
|
|
179
179
|
responses: {
|
|
@@ -200,7 +200,7 @@ function findTopLevelCommand(program: CliProgram, key: string): CliNode | undefi
|
|
|
200
200
|
return program.commands.find((c) => c.key === key);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
/** OpenAPI tags for user
|
|
203
|
+
/** OpenAPI tags for user command routes, one per top-level command. */
|
|
204
204
|
function collectCommandTags(program: CliProgram, routes: HttpRoute[]): { name: string; description?: string }[] {
|
|
205
205
|
const names = [...new Set(routes.map((route) => topLevelCommandKey(route, program)))].sort();
|
|
206
206
|
return names.map((name) => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
HTTP path prefix helpers and framework route guards.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CliProgram } from "~/core/types.ts";
|
|
6
|
+
|
|
7
|
+
/** Top-level segments reserved for framework routes when `pathPrefix` is empty. */
|
|
8
|
+
export const HTTP_RESERVED_TOP_LEVEL_SEGMENTS = new Set(["health", "openapi.json", "swagger", "tools"]);
|
|
9
|
+
|
|
10
|
+
/** Resolved path prefix for user HTTP routes (`""` by default, or e.g. `"/api"`). */
|
|
11
|
+
export function resolveHttpPathPrefix(program: CliProgram): string {
|
|
12
|
+
const raw = program.httpServer?.pathPrefix;
|
|
13
|
+
if (raw === undefined || raw === "") {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** OpenAPI / route path for a user command from URL segments. */
|
|
20
|
+
export function buildHttpUserPath(prefix: string, urlSegments: string[]): string {
|
|
21
|
+
const tail = urlSegments.map((s) => (s.startsWith(":") ? `{${s.slice(1)}}` : s)).join("/");
|
|
22
|
+
if (!prefix) {
|
|
23
|
+
return tail ? `/${tail}` : "/";
|
|
24
|
+
}
|
|
25
|
+
return tail ? `${prefix}/${tail}` : prefix;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Regex-safe path prefix for route matching. */
|
|
29
|
+
export function httpUserPathRegexPrefix(prefix: string): string {
|
|
30
|
+
if (!prefix) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wildcard label for docs (e.g. `/api/*` or `/*`). */
|
|
37
|
+
export function httpUserPathGlob(prefix: string): string {
|
|
38
|
+
return prefix ? `${prefix}/*` : "/*";
|
|
39
|
+
}
|