argsbarg 6.1.3 → 6.1.4
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 +10 -1
- package/docs/http-server.md +3 -3
- 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 +3 -3
- package/examples/full-example/docs/openapi.json +21 -0
- package/package.json +1 -1
- package/src/builtins/http.ts +1 -1
- package/src/docs/http-guide.ts +3 -3
- package/src/http/openapi.ts +113 -5
- package/src/http/result.ts +6 -6
- package/src/http/server.ts +1 -1
- package/src/test/integration/http.test.ts +112 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [6.1.4] - 2026-07-24
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`GET /swagger`** — Swagger UI API reference (replaces `GET /openapi-browser` and Scalar).
|
|
15
|
+
- **`GET /openapi.json`** — documents `/health`, `/health/live`, and `/health/ready` probe endpoints under a **Health** tag.
|
|
16
|
+
- **OpenAPI tags** — user `/api/*` routes are grouped by top-level command key (router `description` becomes the tag description).
|
|
17
|
+
|
|
10
18
|
## [6.1.3] - 2026-07-24
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -833,7 +841,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
833
841
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
834
842
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
835
843
|
|
|
836
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.
|
|
844
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.4...HEAD
|
|
845
|
+
[6.1.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.4
|
|
837
846
|
[6.1.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.3
|
|
838
847
|
[6.1.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.2
|
|
839
848
|
[6.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.1
|
package/docs/http-server.md
CHANGED
|
@@ -73,8 +73,8 @@ Per-surface exposure: `http.enabled: false` removes a leaf from the route table;
|
|
|
73
73
|
| --- | --- | --- |
|
|
74
74
|
| `GET` | `/health` or `/health/live` | Liveness — 200 when server is listening |
|
|
75
75
|
| `GET` | `/health/ready` | Readiness — config + optional `program.readiness` |
|
|
76
|
-
| `GET` | `/openapi.json` | OpenAPI 3.1 REST paths |
|
|
77
|
-
| `GET` | `/
|
|
76
|
+
| `GET` | `/openapi.json` | OpenAPI 3.1 REST paths (includes `/health/*` and `/api/*`) |
|
|
77
|
+
| `GET` | `/swagger` | Interactive Swagger UI API reference (CDN) |
|
|
78
78
|
| `*` | `/api/...` | Invoke user commands (method per route) |
|
|
79
79
|
| `OPTIONS` | `*` | CORS preflight (`GET, POST, PUT, PATCH, DELETE`) |
|
|
80
80
|
|
|
@@ -86,7 +86,7 @@ Per-surface exposure: `http.enabled: false` removes a leaf from the route table;
|
|
|
86
86
|
curl -s http://127.0.0.1:3000/health
|
|
87
87
|
curl -s http://127.0.0.1:3000/health/ready
|
|
88
88
|
curl -s http://127.0.0.1:3000/openapi.json
|
|
89
|
-
open http://127.0.0.1:3000/
|
|
89
|
+
open http://127.0.0.1:3000/swagger
|
|
90
90
|
curl -s http://127.0.0.1:3000/api/workspaces
|
|
91
91
|
curl -s -X POST http://127.0.0.1:3000/api/workspaces \
|
|
92
92
|
-H 'content-type: application/json' \
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
{
|
|
192
192
|
"key": "http",
|
|
193
193
|
"description": "HTTP API server for tools.",
|
|
194
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
194
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
195
195
|
"options": [
|
|
196
196
|
{
|
|
197
197
|
"name": "host",
|
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
{
|
|
425
425
|
"key": "http",
|
|
426
426
|
"description": "HTTP API server for tools.",
|
|
427
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
427
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
428
428
|
"options": [
|
|
429
429
|
{
|
|
430
430
|
"name": "host",
|
|
@@ -679,7 +679,7 @@
|
|
|
679
679
|
{
|
|
680
680
|
"key": "http",
|
|
681
681
|
"description": "HTTP API server for tools.",
|
|
682
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
682
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
683
683
|
"options": [
|
|
684
684
|
{
|
|
685
685
|
"name": "host",
|
|
@@ -916,7 +916,7 @@
|
|
|
916
916
|
{
|
|
917
917
|
"key": "http",
|
|
918
918
|
"description": "HTTP API server for tools.",
|
|
919
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
919
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
920
920
|
"options": [
|
|
921
921
|
{
|
|
922
922
|
"name": "host",
|
|
@@ -1149,7 +1149,7 @@
|
|
|
1149
1149
|
{
|
|
1150
1150
|
"key": "http",
|
|
1151
1151
|
"description": "HTTP API server for tools.",
|
|
1152
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1152
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
1153
1153
|
"options": [
|
|
1154
1154
|
{
|
|
1155
1155
|
"name": "host",
|
|
@@ -1386,7 +1386,7 @@
|
|
|
1386
1386
|
{
|
|
1387
1387
|
"key": "http",
|
|
1388
1388
|
"description": "HTTP API server for tools.",
|
|
1389
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1389
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
1390
1390
|
"options": [
|
|
1391
1391
|
{
|
|
1392
1392
|
"name": "host",
|
|
@@ -1619,7 +1619,7 @@
|
|
|
1619
1619
|
{
|
|
1620
1620
|
"key": "http",
|
|
1621
1621
|
"description": "HTTP API server for tools.",
|
|
1622
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1622
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
1623
1623
|
"options": [
|
|
1624
1624
|
{
|
|
1625
1625
|
"name": "host",
|
|
@@ -1852,7 +1852,7 @@
|
|
|
1852
1852
|
{
|
|
1853
1853
|
"key": "http",
|
|
1854
1854
|
"description": "HTTP API server for tools.",
|
|
1855
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1855
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
1856
1856
|
"options": [
|
|
1857
1857
|
{
|
|
1858
1858
|
"name": "host",
|
|
@@ -2085,7 +2085,7 @@
|
|
|
2085
2085
|
{
|
|
2086
2086
|
"key": "http",
|
|
2087
2087
|
"description": "HTTP API server for tools.",
|
|
2088
|
-
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
2088
|
+
"notes": "HTTP tool server on http://127.0.0.1:3000.\n\nEndpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*\n\nConfigure app settings:\n\n full-example configure\n\nFull setup guide: full-example docs http",
|
|
2089
2089
|
"options": [
|
|
2090
2090
|
{
|
|
2091
2091
|
"name": "host",
|
|
@@ -213,7 +213,7 @@ HTTP API server for tools.
|
|
|
213
213
|
|
|
214
214
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
215
215
|
>
|
|
216
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
216
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
217
217
|
>
|
|
218
218
|
> Configure app settings:
|
|
219
219
|
>
|
|
@@ -426,7 +426,7 @@ HTTP API server for tools.
|
|
|
426
426
|
|
|
427
427
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
428
428
|
>
|
|
429
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
429
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
430
430
|
>
|
|
431
431
|
> Configure app settings:
|
|
432
432
|
>
|
|
@@ -667,7 +667,7 @@ HTTP API server for tools.
|
|
|
667
667
|
|
|
668
668
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
669
669
|
>
|
|
670
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
670
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
671
671
|
>
|
|
672
672
|
> Configure app settings:
|
|
673
673
|
>
|
|
@@ -890,7 +890,7 @@ HTTP API server for tools.
|
|
|
890
890
|
|
|
891
891
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
892
892
|
>
|
|
893
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
893
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
894
894
|
>
|
|
895
895
|
> Configure app settings:
|
|
896
896
|
>
|
|
@@ -1103,7 +1103,7 @@ HTTP API server for tools.
|
|
|
1103
1103
|
|
|
1104
1104
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
1105
1105
|
>
|
|
1106
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1106
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
1107
1107
|
>
|
|
1108
1108
|
> Configure app settings:
|
|
1109
1109
|
>
|
|
@@ -1327,7 +1327,7 @@ HTTP API server for tools.
|
|
|
1327
1327
|
|
|
1328
1328
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
1329
1329
|
>
|
|
1330
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1330
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
1331
1331
|
>
|
|
1332
1332
|
> Configure app settings:
|
|
1333
1333
|
>
|
|
@@ -1540,7 +1540,7 @@ HTTP API server for tools.
|
|
|
1540
1540
|
|
|
1541
1541
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
1542
1542
|
>
|
|
1543
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1543
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
1544
1544
|
>
|
|
1545
1545
|
> Configure app settings:
|
|
1546
1546
|
>
|
|
@@ -1753,7 +1753,7 @@ HTTP API server for tools.
|
|
|
1753
1753
|
|
|
1754
1754
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
1755
1755
|
>
|
|
1756
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1756
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
1757
1757
|
>
|
|
1758
1758
|
> Configure app settings:
|
|
1759
1759
|
>
|
|
@@ -1966,7 +1966,7 @@ HTTP API server for tools.
|
|
|
1966
1966
|
|
|
1967
1967
|
> HTTP tool server on http://127.0.0.1:3000.
|
|
1968
1968
|
>
|
|
1969
|
-
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
1969
|
+
> Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*
|
|
1970
1970
|
>
|
|
1971
1971
|
> Configure app settings:
|
|
1972
1972
|
>
|
|
@@ -19,7 +19,7 @@ Bind is localhost-only in v0 — use a reverse proxy for remote access.
|
|
|
19
19
|
| `GET` | `/health` or `/health/live` | Liveness check |
|
|
20
20
|
| `GET` | `/health/ready` | Readiness (config + `program.readiness`) |
|
|
21
21
|
| `GET` | `/openapi.json` | OpenAPI 3.1 REST paths |
|
|
22
|
-
| `GET` | `/
|
|
22
|
+
| `GET` | `/swagger` | Interactive Swagger UI API reference |
|
|
23
23
|
| `*` | `/api/...` | Invoke user commands (method per route) |
|
|
24
24
|
| `OPTIONS` | `*` | CORS preflight |
|
|
25
25
|
|
|
@@ -61,7 +61,7 @@ Errors use `{ "error": "..." }` with `400`, `404`, `503`, or `500`.
|
|
|
61
61
|
|
|
62
62
|
POST/PUT/PATCH bodies are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).
|
|
63
63
|
|
|
64
|
-
For HTTP clients, use **`GET /openapi.json`** (or **`GET /
|
|
64
|
+
For HTTP clients, use **`GET /openapi.json`** (or **`GET /swagger`**) for per-route request shapes.
|
|
65
65
|
|
|
66
66
|
Varargs positionals accept a JSON array of strings (not a comma-separated string).
|
|
67
67
|
Options with `format: comma-list` accept a comma-separated string or JSON array.
|
|
@@ -73,7 +73,7 @@ Shell invocation reference: `full-example docs cli`. Full CLI tree JSON: `full-e
|
|
|
73
73
|
|
|
74
74
|
The HTTP API is described in OpenAPI 3.1.
|
|
75
75
|
|
|
76
|
-
- **Browse** — [http://127.0.0.1:3000/
|
|
76
|
+
- **Browse** — [http://127.0.0.1:3000/swagger](http://127.0.0.1:3000/swagger) (Swagger UI; loads `/openapi.json`)
|
|
77
77
|
- **Fetch** — `curl -s http://127.0.0.1:3000/openapi.json`
|
|
78
78
|
- **Save offline** — `full-example docs openapi --save` → `./docs/openapi.json` (or `just docgen` in app repos)
|
|
79
79
|
|
|
@@ -202,6 +202,27 @@
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
},
|
|
206
|
+
"requestBody": {
|
|
207
|
+
"required": true,
|
|
208
|
+
"content": {
|
|
209
|
+
"application/json; charset=utf-8": {
|
|
210
|
+
"schema": {
|
|
211
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
212
|
+
"type": "object",
|
|
213
|
+
"properties": {
|
|
214
|
+
"message": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"description": "Message to echo back."
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"required": [
|
|
220
|
+
"message"
|
|
221
|
+
],
|
|
222
|
+
"additionalProperties": false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
205
226
|
}
|
|
206
227
|
}
|
|
207
228
|
},
|
package/package.json
CHANGED
package/src/builtins/http.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function cliBuiltinHttpCommand(program: CliProgram): CliRouter {
|
|
|
37
37
|
const lines = [
|
|
38
38
|
`HTTP tool server on http://${hostname}:${port}.`,
|
|
39
39
|
"",
|
|
40
|
-
"Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /
|
|
40
|
+
"Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*",
|
|
41
41
|
"",
|
|
42
42
|
];
|
|
43
43
|
if (caps.configure) {
|
package/src/docs/http-guide.ts
CHANGED
|
@@ -50,7 +50,7 @@ export function generateHttpGuide(root: CliProgram): string {
|
|
|
50
50
|
"| `GET` | `/health` or `/health/live` | Liveness check |",
|
|
51
51
|
"| `GET` | `/health/ready` | Readiness (config + `program.readiness`) |",
|
|
52
52
|
"| `GET` | `/openapi.json` | OpenAPI 3.1 REST paths |",
|
|
53
|
-
"| `GET` | `/
|
|
53
|
+
"| `GET` | `/swagger` | Interactive Swagger UI API reference |",
|
|
54
54
|
"| `*` | `/api/...` | Invoke user commands (method per route) |",
|
|
55
55
|
"| `OPTIONS` | `*` | CORS preflight |",
|
|
56
56
|
"",
|
|
@@ -111,7 +111,7 @@ export function generateHttpGuide(root: CliProgram): string {
|
|
|
111
111
|
"",
|
|
112
112
|
"POST/PUT/PATCH bodies are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
|
|
113
113
|
"",
|
|
114
|
-
`For HTTP clients, use **\`GET /openapi.json\`** (or **\`GET /
|
|
114
|
+
`For HTTP clients, use **\`GET /openapi.json\`** (or **\`GET /swagger\`**) for per-route request shapes.`,
|
|
115
115
|
"",
|
|
116
116
|
"Varargs positionals accept a JSON array of strings (not a comma-separated string).",
|
|
117
117
|
"Options with `format: comma-list` accept a comma-separated string or JSON array.",
|
|
@@ -123,7 +123,7 @@ export function generateHttpGuide(root: CliProgram): string {
|
|
|
123
123
|
"",
|
|
124
124
|
"The HTTP API is described in OpenAPI 3.1.",
|
|
125
125
|
"",
|
|
126
|
-
`- **Browse** — [${baseUrl}/
|
|
126
|
+
`- **Browse** — [${baseUrl}/swagger](${baseUrl}/swagger) (Swagger UI; loads \`/openapi.json\`)`,
|
|
127
127
|
`- **Fetch** — \`curl -s ${baseUrl}/openapi.json\``,
|
|
128
128
|
`- **Save offline** — \`${root.key} docs openapi --save\` → \`./docs/openapi.json\` (or \`just docgen\` in app repos)`,
|
|
129
129
|
"",
|
package/src/http/openapi.ts
CHANGED
|
@@ -3,8 +3,8 @@ Hand-built OpenAPI 3.1 document from exposed HTTP REST routes.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { collectOptionDefs } from "~/core/parse.ts";
|
|
6
|
-
import type { CliHttpMethod, CliProgram } from "~/core/types.ts";
|
|
7
|
-
import { CliOptionKind, isJsonLeaf } from "~/core/types.ts";
|
|
6
|
+
import type { CliHttpMethod, CliNode, CliProgram } from "~/core/types.ts";
|
|
7
|
+
import { CliOptionKind, isCliLeaf, isJsonLeaf } from "~/core/types.ts";
|
|
8
8
|
import { collectHttpRoutes, defaultSuccessStatus } from "./routes.ts";
|
|
9
9
|
import { dereferenceJsonSchema } from "./schema-deref.ts";
|
|
10
10
|
|
|
@@ -106,15 +106,120 @@ function methodLower(method: CliHttpMethod): string {
|
|
|
106
106
|
return method.toLowerCase();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
const HEALTH_TAG = "health";
|
|
110
|
+
|
|
111
|
+
const livenessResponseSchema = {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: { ok: { type: "boolean", const: true } },
|
|
114
|
+
required: ["ok"],
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
const readinessCheckSchema = {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
ok: { type: "boolean" },
|
|
121
|
+
error: { type: "string" },
|
|
122
|
+
missing: { type: "array", items: { type: "string" } },
|
|
123
|
+
},
|
|
124
|
+
required: ["ok"],
|
|
125
|
+
} as const;
|
|
126
|
+
|
|
127
|
+
const readinessResponseSchema = {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
ok: { type: "boolean" },
|
|
131
|
+
checks: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
config_file: readinessCheckSchema,
|
|
135
|
+
config_required: readinessCheckSchema,
|
|
136
|
+
custom: readinessCheckSchema,
|
|
137
|
+
},
|
|
138
|
+
required: ["config_file", "config_required", "custom"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["ok", "checks"],
|
|
142
|
+
} as const;
|
|
143
|
+
|
|
144
|
+
function jsonResponseEntry(description: string, schema: Record<string, unknown>): Record<string, unknown> {
|
|
145
|
+
return {
|
|
146
|
+
description,
|
|
147
|
+
content: {
|
|
148
|
+
[JSON_CONTENT_TYPE]: { schema },
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function livenessGetOp(operationId: string, summary: string): Record<string, unknown> {
|
|
154
|
+
return {
|
|
155
|
+
tags: [HEALTH_TAG],
|
|
156
|
+
operationId,
|
|
157
|
+
summary,
|
|
158
|
+
responses: {
|
|
159
|
+
"200": jsonResponseEntry("Server is listening", livenessResponseSchema),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Framework health probe paths served alongside `/api/*` routes. */
|
|
165
|
+
function buildHealthPaths(): Record<string, unknown> {
|
|
166
|
+
return {
|
|
167
|
+
"/health": {
|
|
168
|
+
get: livenessGetOp("health", "Liveness probe (alias of /health/live)"),
|
|
169
|
+
},
|
|
170
|
+
"/health/live": {
|
|
171
|
+
get: livenessGetOp("health_live", "Liveness probe"),
|
|
172
|
+
},
|
|
173
|
+
"/health/ready": {
|
|
174
|
+
get: {
|
|
175
|
+
tags: [HEALTH_TAG],
|
|
176
|
+
operationId: "health_ready",
|
|
177
|
+
summary: "Readiness probe",
|
|
178
|
+
description: "Config file, required app config, and optional program.readiness checks.",
|
|
179
|
+
responses: {
|
|
180
|
+
"200": jsonResponseEntry("Ready to serve traffic", readinessResponseSchema),
|
|
181
|
+
"503": jsonResponseEntry("Not ready", readinessResponseSchema),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type HttpRoute = ReturnType<typeof collectHttpRoutes>[number];
|
|
189
|
+
|
|
190
|
+
/** Top-level command key for OpenAPI grouping (first non-`:param` segment). */
|
|
191
|
+
function topLevelCommandKey(route: HttpRoute, program: CliProgram): string {
|
|
192
|
+
const key = route.commandPath.find((k) => !k.startsWith(":"));
|
|
193
|
+
return key ?? program.key;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findTopLevelCommand(program: CliProgram, key: string): CliNode | undefined {
|
|
197
|
+
if (isCliLeaf(program)) {
|
|
198
|
+
return program.key === key ? program : undefined;
|
|
199
|
+
}
|
|
200
|
+
return program.commands.find((c) => c.key === key);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** OpenAPI tags for user `/api/*` routes, one per top-level command. */
|
|
204
|
+
function collectCommandTags(program: CliProgram, routes: HttpRoute[]): { name: string; description?: string }[] {
|
|
205
|
+
const names = [...new Set(routes.map((route) => topLevelCommandKey(route, program)))].sort();
|
|
206
|
+
return names.map((name) => {
|
|
207
|
+
const node = findTopLevelCommand(program, name);
|
|
208
|
+
return node?.description ? { name, description: node.description } : { name };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
109
212
|
/** Generates an OpenAPI 3.1 document for the program's HTTP routes. */
|
|
110
213
|
export function generateOpenApi(program: CliProgram): Record<string, unknown> {
|
|
111
214
|
const routes = collectHttpRoutes(program);
|
|
112
|
-
const paths: Record<string, unknown> = {};
|
|
215
|
+
const paths: Record<string, unknown> = program.httpServer?.enabled ? buildHealthPaths() : {};
|
|
216
|
+
const commandTags = collectCommandTags(program, routes);
|
|
113
217
|
|
|
114
218
|
for (const route of routes) {
|
|
115
219
|
const pathKey = route.openApiPath;
|
|
116
220
|
const existing = (paths[pathKey] as Record<string, unknown> | undefined) ?? {};
|
|
117
221
|
const op: Record<string, unknown> = {
|
|
222
|
+
tags: [topLevelCommandKey(route, program)],
|
|
118
223
|
operationId: route.openApiPath.replace(/\//g, "_").replace(/[{}]/g, ""),
|
|
119
224
|
summary: route.leaf.description ?? route.leaf.key,
|
|
120
225
|
responses: {
|
|
@@ -150,9 +255,9 @@ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
|
|
|
150
255
|
description: opt.description,
|
|
151
256
|
})),
|
|
152
257
|
];
|
|
153
|
-
} else
|
|
258
|
+
} else {
|
|
154
259
|
op.requestBody = {
|
|
155
|
-
required:
|
|
260
|
+
required: isJsonLeaf(route.leaf),
|
|
156
261
|
content: {
|
|
157
262
|
[JSON_CONTENT_TYPE]: {
|
|
158
263
|
schema: dereferenceJsonSchema(buildInputSchema(program, route)),
|
|
@@ -172,6 +277,9 @@ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
|
|
|
172
277
|
version: program.version,
|
|
173
278
|
description: program.description,
|
|
174
279
|
},
|
|
280
|
+
...(program.httpServer?.enabled
|
|
281
|
+
? { tags: [{ name: HEALTH_TAG, description: "Server health probes" }, ...commandTags] }
|
|
282
|
+
: {}),
|
|
175
283
|
paths,
|
|
176
284
|
};
|
|
177
285
|
}
|
package/src/http/result.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function apiErrorResponse(status: number, body: ApiToolCallErrorBody): Re
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/**
|
|
101
|
+
/** Swagger UI HTML served at GET /swagger. */
|
|
102
102
|
export function apiDocsHtml(): string {
|
|
103
103
|
return `<!doctype html>
|
|
104
104
|
<html lang="en">
|
|
@@ -106,15 +106,15 @@ export function apiDocsHtml(): string {
|
|
|
106
106
|
<meta charset="utf-8" />
|
|
107
107
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
108
108
|
<title>API Reference</title>
|
|
109
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" />
|
|
109
110
|
</head>
|
|
110
111
|
<body>
|
|
111
|
-
<div id="
|
|
112
|
-
<script src="https://cdn.jsdelivr.net/npm
|
|
112
|
+
<div id="swagger-ui"></div>
|
|
113
|
+
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
|
|
113
114
|
<script>
|
|
114
|
-
|
|
115
|
+
SwaggerUIBundle({
|
|
115
116
|
url: "/openapi.json",
|
|
116
|
-
|
|
117
|
-
orderRequiredPropertiesFirst: false,
|
|
117
|
+
dom_id: "#swagger-ui",
|
|
118
118
|
});
|
|
119
119
|
</script>
|
|
120
120
|
</body>
|
package/src/http/server.ts
CHANGED
|
@@ -129,7 +129,7 @@ export async function handleApiRequest(
|
|
|
129
129
|
return finish(jsonResponse(200, generateOpenApi(root)));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
if (request.method === "GET" && path === "/
|
|
132
|
+
if (request.method === "GET" && path === "/swagger") {
|
|
133
133
|
return finish(
|
|
134
134
|
new Response(apiDocsHtml(), {
|
|
135
135
|
status: 200,
|
|
@@ -432,19 +432,80 @@ describe("HTTP API routes", () => {
|
|
|
432
432
|
const doc = (await res.json()) as { openapi: string; paths: Record<string, unknown> };
|
|
433
433
|
expect(doc.openapi).toBe("3.1.0");
|
|
434
434
|
expect(doc.paths["/api/stat/owner/lookup"]).toBeDefined();
|
|
435
|
+
expect(doc.paths["/health"]).toBeDefined();
|
|
436
|
+
expect(doc.paths["/health/live"]).toBeDefined();
|
|
437
|
+
expect(doc.paths["/health/ready"]).toBeDefined();
|
|
435
438
|
});
|
|
436
439
|
|
|
437
|
-
test("GET /
|
|
438
|
-
const res = await apiRequest(program, new Request("http://127.0.0.1/
|
|
440
|
+
test("GET /swagger returns Swagger UI HTML", async () => {
|
|
441
|
+
const res = await apiRequest(program, new Request("http://127.0.0.1/swagger"));
|
|
439
442
|
expect(res.status).toBe(200);
|
|
440
443
|
expect(res.headers.get("content-type")).toContain("text/html");
|
|
441
444
|
const html = await res.text();
|
|
442
|
-
expect(html).toContain("
|
|
443
|
-
expect(html).toContain('
|
|
444
|
-
expect(html).toContain(
|
|
445
|
+
expect(html).toContain("swagger-ui-dist");
|
|
446
|
+
expect(html).toContain('url: "/openapi.json"');
|
|
447
|
+
expect(html).toContain('dom_id: "#swagger-ui"');
|
|
445
448
|
});
|
|
446
449
|
});
|
|
447
450
|
|
|
451
|
+
test("generateOpenApi includes health probe paths", () => {
|
|
452
|
+
const program = testProgram({
|
|
453
|
+
key: "app",
|
|
454
|
+
description: "Test app",
|
|
455
|
+
httpServer: { enabled: true },
|
|
456
|
+
handler: () => ({ ok: true }),
|
|
457
|
+
});
|
|
458
|
+
cliValidateProgram(program);
|
|
459
|
+
const doc = generateOpenApi(program) as {
|
|
460
|
+
tags: { name: string }[];
|
|
461
|
+
paths: Record<
|
|
462
|
+
string,
|
|
463
|
+
{
|
|
464
|
+
get: {
|
|
465
|
+
tags: string[];
|
|
466
|
+
responses: Record<string, { content: Record<string, { schema: Record<string, unknown> }> }>;
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
>;
|
|
470
|
+
};
|
|
471
|
+
expect(doc.tags.some((t) => t.name === "health")).toBe(true);
|
|
472
|
+
expect(doc.paths["/health"]?.get.tags).toContain("health");
|
|
473
|
+
expect(doc.paths["/health/live"]?.get.responses["200"]).toBeDefined();
|
|
474
|
+
expect(doc.paths["/health/ready"]?.get.responses["200"]).toBeDefined();
|
|
475
|
+
expect(doc.paths["/health/ready"]?.get.responses["503"]).toBeDefined();
|
|
476
|
+
const readySchema = doc.paths["/health/ready"]?.get.responses["200"].content["application/json; charset=utf-8"]
|
|
477
|
+
.schema as { properties?: { checks?: unknown } };
|
|
478
|
+
expect(readySchema.properties?.checks).toBeDefined();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("generateOpenApi omits health paths when httpServer disabled", () => {
|
|
482
|
+
const program = testProgram({
|
|
483
|
+
key: "app",
|
|
484
|
+
description: "Test app",
|
|
485
|
+
handler: () => ({ ok: true }),
|
|
486
|
+
});
|
|
487
|
+
cliValidateProgram(program);
|
|
488
|
+
const doc = generateOpenApi(program) as { paths: Record<string, unknown> };
|
|
489
|
+
expect(doc.paths["/health"]).toBeUndefined();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("generateOpenApi groups routes by top-level command tag", () => {
|
|
493
|
+
const program = nestedApiFixture();
|
|
494
|
+
cliValidateProgram(program);
|
|
495
|
+
const doc = generateOpenApi(program) as {
|
|
496
|
+
tags: { name: string; description?: string }[];
|
|
497
|
+
paths: Record<string, { post?: { tags: string[] }; get?: { tags: string[] } }>;
|
|
498
|
+
};
|
|
499
|
+
const tagNames = doc.tags.map((t) => t.name);
|
|
500
|
+
expect(tagNames).toContain("health");
|
|
501
|
+
expect(tagNames).toContain("stat");
|
|
502
|
+
expect(tagNames).toContain("pdf");
|
|
503
|
+
expect(doc.tags.find((t) => t.name === "stat")?.description).toBe("File metadata.");
|
|
504
|
+
expect(doc.paths["/api/stat/owner/lookup"]?.post?.tags).toEqual(["stat"]);
|
|
505
|
+
expect(doc.paths["/api/pdf"]?.post?.tags).toEqual(["pdf"]);
|
|
506
|
+
expect(doc.paths["/api/read"]?.post?.tags).toEqual(["read"]);
|
|
507
|
+
});
|
|
508
|
+
|
|
448
509
|
test("generateOpenApi maps binary content types", () => {
|
|
449
510
|
const program = nestedApiFixture();
|
|
450
511
|
const doc = generateOpenApi(program) as {
|
|
@@ -505,6 +566,52 @@ test("generateOpenApi dereferences nested inputSchema definitions", () => {
|
|
|
505
566
|
});
|
|
506
567
|
});
|
|
507
568
|
|
|
569
|
+
test("generateOpenApi generates requestBody for kind: json leaves", () => {
|
|
570
|
+
const program = testProgram({
|
|
571
|
+
key: "app",
|
|
572
|
+
description: "Test app",
|
|
573
|
+
httpServer: { enabled: true },
|
|
574
|
+
commands: [
|
|
575
|
+
{
|
|
576
|
+
key: "render-invoice",
|
|
577
|
+
description: "Render an invoice.",
|
|
578
|
+
kind: "json",
|
|
579
|
+
inputSchema: {
|
|
580
|
+
type: "object",
|
|
581
|
+
properties: {
|
|
582
|
+
id: { type: "string" },
|
|
583
|
+
},
|
|
584
|
+
required: ["id"],
|
|
585
|
+
},
|
|
586
|
+
handler: () => ({ ok: true }),
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
});
|
|
590
|
+
cliValidateProgram(program);
|
|
591
|
+
const doc = generateOpenApi(program) as {
|
|
592
|
+
paths: Record<
|
|
593
|
+
string,
|
|
594
|
+
{
|
|
595
|
+
post: {
|
|
596
|
+
requestBody: {
|
|
597
|
+
required: boolean;
|
|
598
|
+
content: Record<string, { schema: Record<string, unknown> }>;
|
|
599
|
+
};
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
>;
|
|
603
|
+
};
|
|
604
|
+
const op = doc.paths["/api/render-invoice"]?.post;
|
|
605
|
+
expect(op).toBeDefined();
|
|
606
|
+
expect(op.requestBody).toBeDefined();
|
|
607
|
+
expect(op.requestBody.required).toBe(true);
|
|
608
|
+
expect(op.requestBody.content["application/json; charset=utf-8"].schema).toEqual({
|
|
609
|
+
type: "object",
|
|
610
|
+
properties: { id: { type: "string" } },
|
|
611
|
+
required: ["id"],
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
508
615
|
test("ctx.respond throws when called twice", () => {
|
|
509
616
|
const program = testProgram({
|
|
510
617
|
key: "app",
|