@adhd/apigen-plugin-openapi 0.1.2 → 0.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 ADDED
@@ -0,0 +1,57 @@
1
+ ## 0.1.4 (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
29
+
30
+ ## 0.1.3 (2026-07-23)
31
+
32
+
33
+ ### 🚀 Features
34
+
35
+ - session batch — dispatch tool-exec + provider routing, publish hygiene, agent/env/apigen fixes
36
+
37
+ - **release:** nx release independent versioning + verify-dist-load publish gate (Agent 2)
38
+
39
+
40
+ ### 🩹 Fixes
41
+
42
+ - resolve build errors from workspace-cleanup merge — unterminated strings, path mappings, lint
43
+
44
+ - **apigen:** update remaining stale @adhd/apigen-errors, core, logical refs
45
+
46
+ - **environment:** correct agent-mcp plugins at: classification (build->runtime)
47
+
48
+ - **apigen:** complete FEAT-APIGEN-022 test coverage — primitive→GET + complex→POST
49
+
50
+ - **apigen:** externalize real npm deps in vite builds — 10 packages shipped broken dist bundles (__filename/timeOrigin crash)
51
+
52
+ - **apigen-plugin-openapi:** sync @adhd/apigen-codegen-openapi range to installed 0.1.2
53
+
54
+
55
+ ### ❤️ Thank You
56
+
57
+ - 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
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"}});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;
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(P).join("")}function g(e){return e.words.join("_")}function w(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 P(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=w(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=T(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 T(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 k(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 k(e,t)}}}};exports.default=h;exports.openapiPlugin=h;
package/index.mjs CHANGED
@@ -43,43 +43,43 @@ function d(e) {
43
43
  return e.words.join("-");
44
44
  }
45
45
  function y(e) {
46
- return e.words.map(w).join("");
46
+ return e.words.map(q).join("");
47
47
  }
48
48
  function h(e) {
49
49
  return e.words.join("_");
50
50
  }
51
- function q(e, t = {}) {
51
+ function w(e, t = {}) {
52
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));
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((p) => d(p)).join("/"), a = n.map((p) => h(p)).join("_"), f = n.map((p) => h(p)).slice(0, -1).join("."), u = y(n[n.length - 2] ?? n[0]), s = y(n[n.length - 1]), g = n.map((p) => d(p));
54
54
  return {
55
55
  http: { verb: l, route: c },
56
56
  mcp: { name: a },
57
- grpc: { package: f, service: u, method: p },
57
+ grpc: { package: f, service: u, method: s },
58
58
  cli: { path: g }
59
59
  };
60
60
  }
61
- function w(e) {
61
+ function q(e) {
62
62
  return e.length === 0 ? e : e[0].toUpperCase() + e.slice(1);
63
63
  }
64
64
  function P(e, t = {}) {
65
65
  const o = t.title ?? "API", r = t.version ?? "0.0.0", n = {};
66
66
  for (const i of e) {
67
- const l = q(i), { verb: c, route: a } = l.http, f = c.toLowerCase(), u = {
67
+ const l = w(i), { verb: c, route: a } = l.http, f = c.toLowerCase(), u = {
68
68
  operationId: i.id,
69
69
  summary: i.id,
70
70
  responses: {
71
- 200: k(i.output)
71
+ 200: S(i.output)
72
72
  }
73
73
  };
74
74
  if (c === "GET")
75
- u.parameters = S(i.input);
75
+ u.parameters = k(i.input);
76
76
  else {
77
- const p = i.input;
78
- typeof p == "object" && p !== null && Object.keys(p).length > 0 && (u.requestBody = {
77
+ const s = i.input;
78
+ typeof s == "object" && s !== null && Object.keys(s).length > 0 && (u.requestBody = {
79
79
  required: !0,
80
80
  content: {
81
81
  "application/json": {
82
- schema: p
82
+ schema: s
83
83
  }
84
84
  }
85
85
  });
@@ -92,7 +92,7 @@ function P(e, t = {}) {
92
92
  paths: n
93
93
  };
94
94
  }
95
- function k(e) {
95
+ function S(e) {
96
96
  return typeof e == "object" && e !== null && Object.keys(e).length > 0 ? {
97
97
  description: "Success",
98
98
  content: {
@@ -100,7 +100,7 @@ function k(e) {
100
100
  }
101
101
  } : { description: "Success" };
102
102
  }
103
- function S(e) {
103
+ function k(e) {
104
104
  if (typeof e != "object" || e === null || typeof e.properties != "object" || e.properties === null)
105
105
  return [];
106
106
  const t = Array.isArray(e.required) ? e.required : [], o = e.properties;
@@ -117,7 +117,7 @@ function b(e) {
117
117
  words: e.split(/(?=[A-Z])|[-_]/).map((t) => t.toLowerCase()).filter(Boolean)
118
118
  };
119
119
  }
120
- function I(e, t = {}) {
120
+ function E(e, t = {}) {
121
121
  return [
122
122
  {
123
123
  // Canonical id — the `_meta` prefix is a convention for meta-endpoints.
@@ -162,7 +162,7 @@ const x = {
162
162
  capabilities: {
163
163
  mount: {
164
164
  operations(e, t) {
165
- return I(
165
+ return E(
166
166
  e,
167
167
  t
168
168
  );
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@adhd/apigen-plugin-openapi",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "main": "./index.js",
5
5
  "dependencies": {
6
- "@adhd/apigen-core-client": "^0.1.0",
7
- "@adhd/apigen-codegen-openapi": "^0.1.2"
6
+ "@adhd/apigen-core-client": "^0.1.2",
7
+ "@adhd/apigen-codegen-openapi": "^0.1.4"
8
8
  },
9
9
  "publishConfig": {
10
10
  "access": "public"