@adhd/apigen-plugin-openapi 0.1.1 → 0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ ## 0.1.3 (2026-07-23)
2
+
3
+
4
+ ### 🚀 Features
5
+
6
+ - session batch — dispatch tool-exec + provider routing, publish hygiene, agent/env/apigen fixes
7
+
8
+ - **release:** nx release independent versioning + verify-dist-load publish gate (Agent 2)
9
+
10
+
11
+ ### 🩹 Fixes
12
+
13
+ - resolve build errors from workspace-cleanup merge — unterminated strings, path mappings, lint
14
+
15
+ - **apigen:** update remaining stale @adhd/apigen-errors, core, logical refs
16
+
17
+ - **environment:** correct agent-mcp plugins at: classification (build->runtime)
18
+
19
+ - **apigen:** complete FEAT-APIGEN-022 test coverage — primitive→GET + complex→POST
20
+
21
+ - **apigen:** externalize real npm deps in vite builds — 10 packages shipped broken dist bundles (__filename/timeOrigin crash)
22
+
23
+ - **apigen-plugin-openapi:** sync @adhd/apigen-codegen-openapi range to installed 0.1.2
24
+
25
+
26
+ ### ❤️ Thank You
27
+
28
+ - pseudosky
package/README.md CHANGED
@@ -1,14 +1,141 @@
1
1
  # @adhd/apigen-plugin-openapi
2
2
 
3
- apigen plugin — emits an **OpenAPI** document for a source file's exports, built on
4
- [`@adhd/apigen-codegen-openapi`](../../codegen/openapi).
3
+ **Mount plugin** — serves a **live OpenAPI 3.1 document** at `GET /_meta/openapi`. The document is derived from the extracted operations at request time, so it always reflects the current service definition.
5
4
 
6
- Part of [apigen](../../README.md). Driven via [`@adhd/apigen-cli`](../../cli).
5
+ Part of [apigen](../README.md). Driven via [`@adhd/apigen-cli`](../../../entrypoint/apigen-cli/README.md).
6
+
7
+ ---
8
+
9
+ ## What it does
10
+
11
+ When loaded via `--use` on any HTTP target plugin (`api-fastify`, `api-express`), the openapi plugin contributes a synthetic `_meta/openapi` operation. Hitting `GET /_meta/openapi` returns a full OpenAPI 3.1 spec derived from your operations at request time — every endpoint, parameter type, and response shape, all inferred from your TypeScript types.
12
+
13
+ **No code generation, no build step.** The spec is always current with your running service.
7
14
 
8
15
  ```bash
9
- npx @adhd/apigen-cli generate --source ./api.ts --type openapi --out-dir ./out
16
+ curl http://localhost:3000/_meta/openapi
17
+ # → {
18
+ # "openapi": "3.1.0",
19
+ # "info": { "title": "My API", "version": "1.0.0" },
20
+ # "paths": {
21
+ # "/api/getUser": {
22
+ # "get": {
23
+ # "parameters": [{ "name": "id", "in": "query", "schema": { "type": "string" } }],
24
+ # "responses": { "200": { "description": "success" } }
25
+ # }
26
+ # }
27
+ # }
28
+ # }
29
+ ```
30
+
31
+ ---
32
+
33
+ ## CLI usage
34
+
35
+ ```bash
36
+ # Fastify with OpenAPI doc
37
+ npx @adhd/apigen-cli run --source api.ts --type api-fastify --opt port=3000 --use openapi
38
+
39
+ # With custom title and version
40
+ npx @adhd/apigen-cli run --source api.ts --type api-fastify --use openapi \
41
+ --opt 'useOptions={"openapi":{"title":"User API","version":"2.0.0"}}'
42
+
43
+ # Combined with other plugins
44
+ npx @adhd/apigen-cli run --source api.ts --type api-fastify --use openapi --use health --use logger
10
45
  ```
11
46
 
47
+ ---
48
+
49
+ ## Programmatic usage
50
+
51
+ ```ts
52
+ import { extract, composeSchemas } from '@adhd/apigen-core-client';
53
+ import { apiFastifyPlugin } from '@adhd/apigen-plugin-api-fastify';
54
+ import { openapiPlugin } from '@adhd/apigen-plugin-openapi';
55
+
56
+ const ops = await extract({ sourceFile: './api.ts', namespace: 'api' });
57
+ const schemas = composeSchemas(
58
+ {
59
+ metadata: { namespace: 'api', phase: '' },
60
+ schemas: Object.fromEntries(
61
+ ops.filter(o => o.kind === 'action').map(op => [
62
+ op.path[op.path.length - 1].raw,
63
+ { input: op.input, output: op.output, hasCtx: op.hasCtx, 'x-apigen-safe': op.safe },
64
+ ])
65
+ ),
66
+ },
67
+ []
68
+ );
69
+ const mod = await import('./api.ts');
70
+
71
+ const abort = new AbortController();
72
+ process.on('SIGINT', () => abort.abort());
73
+
74
+ await apiFastifyPlugin.run({
75
+ packages: [{ id: 'api', schemas, importPath: './api.ts', fns: mod, createClient: async () => ({}) }],
76
+ outputDir: '',
77
+ options: {
78
+ port: 3000,
79
+ usePlugins: [
80
+ openapiPlugin, // ← mounts GET /_meta/openapi
81
+ ],
82
+ // Custom title and version:
83
+ // useOptions: { openapi: { title: 'User API', version: '2.0.0' } },
84
+ },
85
+ signal: abort.signal,
86
+ operations: ops,
87
+ });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Options
93
+
94
+ ```ts
95
+ import { type OpenapiOptions } from '@adhd/apigen-plugin-openapi';
96
+ ```
97
+
98
+ | Option | Type | Default | Description |
99
+ |--------|------|---------|-------------|
100
+ | `title` | `string` | `'API'` | `info.title` in the generated OpenAPI document |
101
+ | `version` | `string` | `'0.0.0'` | `info.version` in the generated OpenAPI document |
102
+
103
+ ---
104
+
105
+ ## What gets documented
106
+
107
+ Every operation extracted from your source file appears in the `paths` section:
108
+
109
+ - **Safe operations** (`x-apigen-safe: true`) → listed as `GET` with query parameters
110
+ - **Unsafe operations** → listed as `POST` with `requestBody`
111
+
112
+ Rich types (Date, Decimal, bigint, UUID) use their canonical JSON Schema representations with `x-apigen-*` hints so codegen tools can reconstruct the correct types.
113
+
114
+ ---
115
+
116
+ ## How it works
117
+
118
+ The openapi plugin implements the **mount capability** (SPEC §7.2b). At compose time, it registers a synthetic `_meta/openapi` operation. At request time, the handler calls `toOpenApi()` from `@adhd/apigen-codegen-openapi` with the full `Operation[]` descriptor — the same one used to serve requests — and returns the live document. No caching, no staleness.
119
+
120
+ This is the same function you can use independently:
121
+
122
+ ```ts
123
+ import { toOpenApi } from '@adhd/apigen-codegen-openapi';
124
+ import type { Operation } from '@adhd/apigen-core-client';
125
+
126
+ const doc = toOpenApi(operations, { title: 'My API', version: '1.0.0' });
127
+ // doc satisfies the OpenAPI 3.1 schema
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Exports
133
+
134
+ | Export | Kind |
135
+ |--------|------|
136
+ | `openapiPlugin` | v2 `Plugin<OpenapiOptions>` — mount capability only |
137
+ | `OpenapiOptions` | TypeScript interface |
138
+
12
139
  ```ts
13
- import { openapiPlugin, type OpenapiOptions } from '@adhd/apigen-plugin-openapi'
140
+ import { openapiPlugin, type OpenapiOptions } from '@adhd/apigen-plugin-openapi';
14
141
  ```
package/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});function m(e){return e.words.join("-")}function g(e){return e.words.map(P).join("")}function b(e){return e.words.join("_")}function v(e,t={}){var d,h;const n=[e.namespace,...e.path],s=e.safe?"GET":"POST",r=((h=(d=t.http)==null?void 0:d.verb)==null?void 0:h[e.id])??s,o="/"+n.map(i=>m(i)).join("/"),l=n.map(i=>b(i)).join("_"),p=n.map(i=>b(i)).slice(0,-1).join("."),u=g(n[n.length-2]??n[0]),c=g(n[n.length-1]),a=n.map(i=>m(i));return{http:{verb:r,route:o},mcp:{name:l},grpc:{package:p,service:u,method:c},cli:{path:a}}}function P(e){return e.length===0?e:e[0].toUpperCase()+e.slice(1)}function O(e,t={}){const n=t.title??"API",s=t.version??"0.0.0",r={};for(const o of e){const l=v(o),{verb:f,route:p}=l.http,u=f.toLowerCase(),c={operationId:o.id,summary:o.id,responses:{200:S(o.output)}};if(o.safe)c.parameters=q(o.input);else{const a=o.input;typeof a=="object"&&a!==null&&Object.keys(a).length>0&&(c.requestBody={required:!0,content:{"application/json":{schema:a}}})}r[p]===void 0&&(r[p]={}),r[p][u]=c}return{openapi:"3.1.0",info:{title:n,version:s},paths:r}}function S(e){return typeof e=="object"&&e!==null&&Object.keys(e).length>0?{description:"Success",content:{"application/json":{schema:e}}}:{description:"Success"}}function q(e){if(typeof e!="object"||e===null||typeof e.properties!="object"||e.properties===null)return[];const t=Array.isArray(e.required)?e.required:[],n=e.properties;return Object.entries(n).map(([s,r])=>({name:s,in:"query",required:t.includes(s),schema:r}))}function y(e){return{raw:e,words:e.split(/(?=[A-Z])|[-_]/).map(t=>t.toLowerCase()).filter(Boolean)}}function A(e,t={}){return[{id:"_meta/openapi",host:e.host,namespace:y("meta"),path:[y("openapi")],kind:"query",async:!1,streaming:!1,safe:!0,input:{},output:{type:"object"},envelope:{},typeText:null,transports:["http"],handler:n=>O(e.operations,{title:t.title,version:t.version})}]}const j={id:"openapi",description:"Mount plugin: serves the OpenAPI 3.1 doc at GET /meta/openapi",optionsSchema:{type:"object",properties:{title:{type:"string",description:"OpenAPI info.title"},version:{type:"string",description:"OpenAPI info.version"}},additionalProperties:!1},capabilities:{mount:{operations(e,t){return A(e,t)}}}};exports.default=j;exports.openapiPlugin=j;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});require("ts-morph");require("node:fs");require("node:path");require("ts-json-schema-generator");const d=new Set(["string","number","boolean","integer"]);function v(e){if(typeof e!="object"||e===null)return!1;const t=e;if(t.$ref!==void 0||t.oneOf||t.anyOf||t.allOf)return!1;const{type:r}=t;return typeof r=="string"?d.has(r):Array.isArray(r)?r.every(o=>d.has(o)||o==="null"):Array.isArray(t.enum)&&t.enum.length>0?t.enum.every(o=>["string","number","boolean"].includes(typeof o)):!1}function O(e){if(!e||typeof e!="object")return!1;const t=e.properties;return t===void 0||typeof t!="object"||t===null?!1:Object.values(t).every(v)}const q={"decimal.js":"Decimal"};function A(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}Object.entries(q).map(([e,t])=>({pattern:new RegExp(`import\\(["'][^"']*${A(e)}[^"']*["']\\)\\.\\w+`,"g"),key:t}));function m(e){return e.words.join("-")}function y(e){return e.words.map(w).join("")}function g(e){return e.words.join("_")}function P(e,t={}){var r,o;const n=[e.namespace,...e.path],i=e.safe||O(e.input)?"GET":"POST",c=((o=(r=t.http)==null?void 0:r.verb)==null?void 0:o[e.id])??i,l="/"+n.map(s=>m(s)).join("/"),a=n.map(s=>g(s)).join("_"),f=n.map(s=>g(s)).slice(0,-1).join("."),u=y(n[n.length-2]??n[0]),p=y(n[n.length-1]),j=n.map(s=>m(s));return{http:{verb:c,route:l},mcp:{name:a},grpc:{package:f,service:u,method:p},cli:{path:j}}}function w(e){return e.length===0?e:e[0].toUpperCase()+e.slice(1)}function S(e,t={}){const r=t.title??"API",o=t.version??"0.0.0",n={};for(const i of e){const c=P(i),{verb:l,route:a}=c.http,f=l.toLowerCase(),u={operationId:i.id,summary:i.id,responses:{200:_(i.output)}};if(l==="GET")u.parameters=k(i.input);else{const p=i.input;typeof p=="object"&&p!==null&&Object.keys(p).length>0&&(u.requestBody={required:!0,content:{"application/json":{schema:p}}})}n[a]===void 0&&(n[a]={}),n[a][f]=u}return{openapi:"3.1.0",info:{title:r,version:o},paths:n}}function _(e){return typeof e=="object"&&e!==null&&Object.keys(e).length>0?{description:"Success",content:{"application/json":{schema:e}}}:{description:"Success"}}function k(e){if(typeof e!="object"||e===null||typeof e.properties!="object"||e.properties===null)return[];const t=Array.isArray(e.required)?e.required:[],r=e.properties;return Object.entries(r).map(([o,n])=>({name:o,in:"query",required:t.includes(o),schema:n}))}function b(e){return{raw:e,words:e.split(/(?=[A-Z])|[-_]/).map(t=>t.toLowerCase()).filter(Boolean)}}function T(e,t={}){return[{id:"_meta/openapi",host:e.host,namespace:b("meta"),path:[b("openapi")],kind:"query",async:!1,streaming:!1,safe:!0,input:{},output:{type:"object"},envelope:{},typeText:null,transports:["http"],handler:r=>S(e.operations,{title:t.title,version:t.version})}]}const h={id:"openapi",description:"Mount plugin: serves the OpenAPI 3.1 doc at GET /meta/openapi",language:"ts",optionsSchema:{type:"object",properties:{title:{type:"string",description:"OpenAPI info.title"},version:{type:"string",description:"OpenAPI info.version"}},additionalProperties:!1},capabilities:{mount:{operations(e,t){return T(e,t)}}}};exports.default=h;exports.openapiPlugin=h;
package/index.mjs CHANGED
@@ -1,40 +1,81 @@
1
- function m(e) {
1
+ import "ts-morph";
2
+ import "node:fs";
3
+ import "node:path";
4
+ import "ts-json-schema-generator";
5
+ const m = /* @__PURE__ */ new Set(["string", "number", "boolean", "integer"]);
6
+ function j(e) {
7
+ if (typeof e != "object" || e === null)
8
+ return !1;
9
+ const t = e;
10
+ if (t.$ref !== void 0 || t.oneOf || t.anyOf || t.allOf)
11
+ return !1;
12
+ const { type: o } = t;
13
+ return typeof o == "string" ? m.has(o) : Array.isArray(o) ? o.every((r) => m.has(r) || r === "null") : Array.isArray(t.enum) && t.enum.length > 0 ? t.enum.every((r) => ["string", "number", "boolean"].includes(typeof r)) : !1;
14
+ }
15
+ function v(e) {
16
+ if (!e || typeof e != "object")
17
+ return !1;
18
+ const t = e.properties;
19
+ return t === void 0 || typeof t != "object" || t === null ? !1 : Object.values(t).every(
20
+ j
21
+ );
22
+ }
23
+ const O = {
24
+ "decimal.js": "Decimal"
25
+ };
26
+ function A(e) {
27
+ return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+ Object.entries(O).map(([e, t]) => ({
30
+ // Matches: import("...{modSpec}...").<anyMemberName> (single or double quotes)
31
+ // The module specifier appears inside an absolute path, so we allow any
32
+ // characters between the quote and the specifier, and between the specifier
33
+ // and the closing quote. The trailing member accessor (`.default`, `.Decimal`,
34
+ // …) is matched generically (`\w+`) rather than anchored to one spelling —
35
+ // see the doc comment above for why.
36
+ pattern: new RegExp(
37
+ `import\\(["'][^"']*${A(e)}[^"']*["']\\)\\.\\w+`,
38
+ "g"
39
+ ),
40
+ key: t
41
+ }));
42
+ function d(e) {
2
43
  return e.words.join("-");
3
44
  }
4
45
  function y(e) {
5
- return e.words.map(v).join("");
46
+ return e.words.map(w).join("");
6
47
  }
7
- function b(e) {
48
+ function h(e) {
8
49
  return e.words.join("_");
9
50
  }
10
- function j(e, t = {}) {
11
- var f, h;
12
- const n = [e.namespace, ...e.path], s = e.safe ? "GET" : "POST", r = ((h = (f = t.http) == null ? void 0 : f.verb) == null ? void 0 : h[e.id]) ?? s, o = "/" + n.map((i) => m(i)).join("/"), l = n.map((i) => b(i)).join("_"), a = n.map((i) => b(i)).slice(0, -1).join("."), u = y(n[n.length - 2] ?? n[0]), c = y(n[n.length - 1]), p = n.map((i) => m(i));
51
+ function q(e, t = {}) {
52
+ var o, r;
53
+ const n = [e.namespace, ...e.path], i = e.safe || v(e.input) ? "GET" : "POST", l = ((r = (o = t.http) == null ? void 0 : o.verb) == null ? void 0 : r[e.id]) ?? i, c = "/" + n.map((s) => d(s)).join("/"), a = n.map((s) => h(s)).join("_"), f = n.map((s) => h(s)).slice(0, -1).join("."), u = y(n[n.length - 2] ?? n[0]), p = y(n[n.length - 1]), g = n.map((s) => d(s));
13
54
  return {
14
- http: { verb: r, route: o },
15
- mcp: { name: l },
16
- grpc: { package: a, service: u, method: c },
17
- cli: { path: p }
55
+ http: { verb: l, route: c },
56
+ mcp: { name: a },
57
+ grpc: { package: f, service: u, method: p },
58
+ cli: { path: g }
18
59
  };
19
60
  }
20
- function v(e) {
61
+ function w(e) {
21
62
  return e.length === 0 ? e : e[0].toUpperCase() + e.slice(1);
22
63
  }
23
64
  function P(e, t = {}) {
24
- const n = t.title ?? "API", s = t.version ?? "0.0.0", r = {};
25
- for (const o of e) {
26
- const l = j(o), { verb: d, route: a } = l.http, u = d.toLowerCase(), c = {
27
- operationId: o.id,
28
- summary: o.id,
65
+ const o = t.title ?? "API", r = t.version ?? "0.0.0", n = {};
66
+ for (const i of e) {
67
+ const l = q(i), { verb: c, route: a } = l.http, f = c.toLowerCase(), u = {
68
+ operationId: i.id,
69
+ summary: i.id,
29
70
  responses: {
30
- 200: O(o.output)
71
+ 200: k(i.output)
31
72
  }
32
73
  };
33
- if (o.safe)
34
- c.parameters = q(o.input);
74
+ if (c === "GET")
75
+ u.parameters = S(i.input);
35
76
  else {
36
- const p = o.input;
37
- typeof p == "object" && p !== null && Object.keys(p).length > 0 && (c.requestBody = {
77
+ const p = i.input;
78
+ typeof p == "object" && p !== null && Object.keys(p).length > 0 && (u.requestBody = {
38
79
  required: !0,
39
80
  content: {
40
81
  "application/json": {
@@ -43,15 +84,15 @@ function P(e, t = {}) {
43
84
  }
44
85
  });
45
86
  }
46
- r[a] === void 0 && (r[a] = {}), r[a][u] = c;
87
+ n[a] === void 0 && (n[a] = {}), n[a][f] = u;
47
88
  }
48
89
  return {
49
90
  openapi: "3.1.0",
50
- info: { title: n, version: s },
51
- paths: r
91
+ info: { title: o, version: r },
92
+ paths: n
52
93
  };
53
94
  }
54
- function O(e) {
95
+ function k(e) {
55
96
  return typeof e == "object" && e !== null && Object.keys(e).length > 0 ? {
56
97
  description: "Success",
57
98
  content: {
@@ -59,28 +100,31 @@ function O(e) {
59
100
  }
60
101
  } : { description: "Success" };
61
102
  }
62
- function q(e) {
103
+ function S(e) {
63
104
  if (typeof e != "object" || e === null || typeof e.properties != "object" || e.properties === null)
64
105
  return [];
65
- const t = Array.isArray(e.required) ? e.required : [], n = e.properties;
66
- return Object.entries(n).map(([s, r]) => ({
67
- name: s,
106
+ const t = Array.isArray(e.required) ? e.required : [], o = e.properties;
107
+ return Object.entries(o).map(([r, n]) => ({
108
+ name: r,
68
109
  in: "query",
69
- required: t.includes(s),
70
- schema: r
110
+ required: t.includes(r),
111
+ schema: n
71
112
  }));
72
113
  }
73
- function g(e) {
74
- return { raw: e, words: e.split(/(?=[A-Z])|[-_]/).map((t) => t.toLowerCase()).filter(Boolean) };
114
+ function b(e) {
115
+ return {
116
+ raw: e,
117
+ words: e.split(/(?=[A-Z])|[-_]/).map((t) => t.toLowerCase()).filter(Boolean)
118
+ };
75
119
  }
76
- function A(e, t = {}) {
120
+ function I(e, t = {}) {
77
121
  return [
78
122
  {
79
123
  // Canonical id — the `_meta` prefix is a convention for meta-endpoints.
80
124
  id: "_meta/openapi",
81
125
  host: e.host,
82
- namespace: g("meta"),
83
- path: [g("openapi")],
126
+ namespace: b("meta"),
127
+ path: [b("openapi")],
84
128
  // query + safe → GET /meta/openapi (SPEC §5)
85
129
  kind: "query",
86
130
  async: !1,
@@ -96,16 +140,17 @@ function A(e, t = {}) {
96
140
  // Expose only on HTTP — the doc is a tooling artefact (SPEC §7.2b).
97
141
  transports: ["http"],
98
142
  // Handler: derive the doc from the descriptor at request time.
99
- handler: (n) => P(e.operations, {
143
+ handler: (o) => P(e.operations, {
100
144
  title: t.title,
101
145
  version: t.version
102
146
  })
103
147
  }
104
148
  ];
105
149
  }
106
- const S = {
150
+ const x = {
107
151
  id: "openapi",
108
152
  description: "Mount plugin: serves the OpenAPI 3.1 doc at GET /meta/openapi",
153
+ language: "ts",
109
154
  optionsSchema: {
110
155
  type: "object",
111
156
  properties: {
@@ -117,12 +162,15 @@ const S = {
117
162
  capabilities: {
118
163
  mount: {
119
164
  operations(e, t) {
120
- return A(e, t);
165
+ return I(
166
+ e,
167
+ t
168
+ );
121
169
  }
122
170
  }
123
171
  }
124
172
  };
125
173
  export {
126
- S as default,
127
- S as openapiPlugin
174
+ x as default,
175
+ x as openapiPlugin
128
176
  };
package/lib/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Plugin } from '@adhd/apigen-core';
1
+ import { Plugin } from '@adhd/apigen-core-client';
2
2
 
3
3
  /** Options accepted by the openapi mount plugin. */
4
4
  export interface OpenapiOptions {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@adhd/apigen-plugin-openapi",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "main": "./index.js",
5
5
  "dependencies": {
6
- "@adhd/apigen-core": "^0.1.0",
7
- "@adhd/apigen-codegen-openapi": "^0.1.0"
6
+ "@adhd/apigen-core-client": "^0.1.1",
7
+ "@adhd/apigen-codegen-openapi": "^0.1.3"
8
8
  },
9
9
  "publishConfig": {
10
10
  "access": "public"