dinorex 1.0.0 → 1.0.2

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/src/agent.groq.js DELETED
@@ -1,279 +0,0 @@
1
- /**
2
- * agent.groq.js — Free Groq/Llama3 version of the Dinorex AI agent.
3
- *
4
- * Get a free API key at: https://console.groq.com
5
- * Set it: export GROQ_API_KEY=gsk_your_key_here
6
- *
7
- * To use this instead of the Anthropic agent, change the import in cli.js and server.js:
8
- * import { analyzeWithAI, analyzeIncremental } from "./agent.groq.js";
9
- */
10
-
11
- const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
12
- const MODEL = "llama-3.3-70b-versatile";
13
- const MAX_CHARS = 12000; // safe limit per request (~3000 tokens of context)
14
-
15
- function buildContext(files) {
16
- return files.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n");
17
- }
18
-
19
- const SYSTEM_FULL = `You are an expert API analyst. Analyze source code (JavaScript OR TypeScript) and extract a complete API specification.
20
-
21
- Supported frameworks — recognize ALL of these:
22
- - Express / Fastify / Koa: router.get('/path', handler), app.post('/path', handler)
23
- - NestJS decorators: @Controller('base'), @Get(':id'), @Post(), @Put(), @Patch(), @Delete(), @Body(), @Param(), @Query(), @UseGuards()
24
- - TypeScript DTOs, interfaces, type aliases, class properties
25
- - Mongoose / Sequelize / TypeORM / Prisma schemas
26
- - Zod / Joi / Yup schemas: extract field names and types
27
- - class-validator decorators: @IsString(), @IsEmail(), etc.
28
-
29
- Rules:
30
- - Return ONLY valid JSON. No markdown, no explanation, no code fences.
31
- - Infer realistic example values from model/schema field names and types.
32
- - Group endpoints into logical collections (e.g. "Users", "Auth", "Products").
33
- - Detect auth guards: @UseGuards(), authMiddleware, isAuthenticated, verifyToken, requireAuth, JwtAuthGuard → requiresAuth: true.
34
- - For NestJS: combine @Controller('users') prefix with method paths (@Get(':id') → /users/:id).
35
- - TypeScript optional fields (field?: type) → required: false.
36
-
37
- Return ONLY this JSON structure, nothing else:
38
- {
39
- "projectName": "string",
40
- "baseUrl": "http://localhost:3000",
41
- "version": "1.0.0",
42
- "description": "string",
43
- "collections": [
44
- {
45
- "name": "string",
46
- "description": "string",
47
- "endpoints": [
48
- {
49
- "id": "unique-kebab-slug",
50
- "method": "GET|POST|PUT|PATCH|DELETE",
51
- "path": "/api/resource/:id",
52
- "summary": "Short title",
53
- "description": "Longer description",
54
- "requiresAuth": false,
55
- "pathParams": [{ "name": "id", "type": "string", "description": "...", "example": "abc123" }],
56
- "queryParams": [{ "name": "page", "type": "integer", "description": "...", "example": 1 }],
57
- "requestBody": {
58
- "contentType": "application/json",
59
- "schema": {
60
- "fieldName": { "type": "string", "example": "value", "required": true, "description": "..." }
61
- }
62
- },
63
- "responses": {
64
- "200": { "description": "Success", "example": {} },
65
- "400": { "description": "Bad Request" },
66
- "401": { "description": "Unauthorized" },
67
- "404": { "description": "Not Found" },
68
- "500": { "description": "Server Error" }
69
- }
70
- }
71
- ]
72
- }
73
- ]
74
- }`;
75
-
76
- const SYSTEM_INCREMENTAL = `You are an expert API analyst doing an INCREMENTAL update to an existing API spec.
77
-
78
- You understand JavaScript AND TypeScript including Express, NestJS decorators, DTOs, Zod schemas, Mongoose/TypeORM/Prisma models.
79
-
80
- You will receive:
81
- 1. The EXISTING spec (full JSON)
82
- 2. NEW or CHANGED source files to analyze
83
-
84
- Your job:
85
- - Extract endpoints from new/changed files
86
- - If endpoint already exists (same method + path): update it if code changed, keep it if unchanged
87
- - If it is NEW: add it to the correct collection (create collection if needed)
88
- - Remove endpoints whose source files are listed under REMOVED FILES
89
- - Keep all existing endpoints from unchanged files
90
-
91
- Return the COMPLETE updated spec JSON. No markdown, no explanation, ONLY JSON.`;
92
-
93
- async function callGroq(systemPrompt, userMessage) {
94
- const apiKey = process.env.GROQ_API_KEY;
95
- if (!apiKey) {
96
- throw new Error(
97
- "GROQ_API_KEY is not set.\nGet a free key at https://console.groq.com\nThen run: export GROQ_API_KEY=gsk_your_key_here"
98
- );
99
- }
100
-
101
- const response = await fetch(GROQ_API_URL, {
102
- method: "POST",
103
- headers: {
104
- "Content-Type": "application/json",
105
- "Authorization": `Bearer ${apiKey}`,
106
- },
107
- body: JSON.stringify({
108
- model: MODEL,
109
- temperature: 0.1, // low temp = more deterministic JSON output
110
- max_tokens: 8000,
111
- messages: [
112
- { role: "system", content: systemPrompt },
113
- { role: "user", content: userMessage },
114
- ],
115
- }),
116
- });
117
-
118
- if (!response.ok) {
119
- const err = await response.text();
120
- throw new Error(`Groq API error ${response.status}: ${err}`);
121
- }
122
-
123
- const data = await response.json();
124
- const raw = data.choices?.[0]?.message?.content || "";
125
- return raw;
126
- }
127
-
128
- function parseJSON(raw) {
129
- // Strip any accidental markdown fences Llama might add
130
- const cleaned = raw
131
- .trim()
132
- .replace(/^```json\s*/i, "")
133
- .replace(/^```\s*/i, "")
134
- .replace(/```\s*$/i, "")
135
- .trim();
136
-
137
- // Find the first { and last } to extract just the JSON object
138
- const start = cleaned.indexOf("{");
139
- const end = cleaned.lastIndexOf("}");
140
- if (start === -1 || end === -1) {
141
- throw new Error(`No JSON object found in response.\n\nSnippet: ${raw.slice(0, 300)}`);
142
- }
143
-
144
- const jsonStr = cleaned.slice(start, end + 1);
145
-
146
- try {
147
- return JSON.parse(jsonStr);
148
- } catch (err) {
149
- throw new Error(`Invalid JSON from Groq: ${err.message}\n\nSnippet: ${jsonStr.slice(0, 300)}`);
150
- }
151
- }
152
-
153
- /**
154
- * Split files into batches that fit within MAX_CHARS each.
155
- */
156
- function batchFiles(files) {
157
- const batches = [];
158
- let current = [];
159
- let size = 0;
160
-
161
- for (const f of files) {
162
- const len = f.content.length + f.path.length + 20;
163
- if (size + len > MAX_CHARS && current.length > 0) {
164
- batches.push(current);
165
- current = [];
166
- size = 0;
167
- }
168
- // If a single file is too large, truncate it
169
- const truncated = { ...f, content: f.content.slice(0, MAX_CHARS) };
170
- current.push(truncated);
171
- size += len;
172
- }
173
- if (current.length > 0) batches.push(current);
174
- return batches;
175
- }
176
-
177
- /**
178
- * Merge multiple partial specs into one, deduplicating endpoints by method+path.
179
- */
180
- function mergeSpecs(specs) {
181
- const base = specs[0];
182
- const collectionsMap = {};
183
-
184
- for (const spec of specs) {
185
- for (const col of spec.collections) {
186
- if (!collectionsMap[col.name]) {
187
- collectionsMap[col.name] = { ...col, endpoints: [] };
188
- }
189
- for (const ep of col.endpoints) {
190
- const key = `${ep.method}:${ep.path}`;
191
- const existing = collectionsMap[col.name].endpoints.find(
192
- e => `${e.method}:${e.path}` === key
193
- );
194
- if (!existing) collectionsMap[col.name].endpoints.push(ep);
195
- }
196
- }
197
- }
198
-
199
- return {
200
- ...base,
201
- collections: Object.values(collectionsMap),
202
- };
203
- }
204
-
205
- export async function analyzeWithAI(collected, projectName = "API") {
206
- // Priority order: routes+controllers first (most important), then services, then models
207
- const allFiles = [
208
- ...collected.routes.map(f => ({ ...f, kind: "ROUTE" })),
209
- ...collected.controllers.map(f => ({ ...f, kind: "CONTROLLER" })),
210
- ...collected.services.map(f => ({ ...f, kind: "SERVICE" })),
211
- ...collected.models.map(f => ({ ...f, kind: "MODEL" })),
212
- ];
213
-
214
- const batches = batchFiles(allFiles);
215
- console.log(`\n 📦 Sending ${batches.length} batch(es) to Groq (${allFiles.length} files total)...`);
216
-
217
- const partialSpecs = [];
218
-
219
- for (let i = 0; i < batches.length; i++) {
220
- const batch = batches[i];
221
- const context = batch
222
- .map(f => `### [${f.kind}] ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
223
- .join("\n\n");
224
-
225
- const userMessage = `Project name: "${projectName}" (batch ${i + 1} of ${batches.length})
226
-
227
- ${context}
228
-
229
- Extract all API endpoints found in these files and return ONLY the JSON spec.`;
230
-
231
- const raw = await callGroq(SYSTEM_FULL, userMessage);
232
- const partial = parseJSON(raw);
233
- partialSpecs.push(partial);
234
-
235
- // Small delay between batches to avoid rate limiting
236
- if (i < batches.length - 1) await sleep(1000);
237
- }
238
-
239
- return batches.length === 1 ? partialSpecs[0] : mergeSpecs(partialSpecs);
240
- }
241
-
242
- function sleep(ms) {
243
- return new Promise(resolve => setTimeout(resolve, ms));
244
- }
245
-
246
- export async function analyzeIncremental(existingSpec, diff) {
247
- const { newFiles, changedFiles, removedFiles } = diff;
248
-
249
- if (!newFiles.length && !changedFiles.length && !removedFiles.length) {
250
- return { spec: existingSpec, changed: false };
251
- }
252
-
253
- const filesToAnalyze = [...newFiles, ...changedFiles];
254
- const removedContext = removedFiles.length > 0
255
- ? `\n\nREMOVED FILES (delete their endpoints):\n${removedFiles.join("\n")}`
256
- : "";
257
-
258
- // Spec JSON itself can be large — truncate for incremental context
259
- const specStr = JSON.stringify(existingSpec, null, 2);
260
- const specTruncated = specStr.length > 6000
261
- ? specStr.slice(0, 6000) + "\n... [truncated]"
262
- : specStr;
263
-
264
- const changedContext = filesToAnalyze
265
- .map(f => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
266
- .join("\n\n");
267
-
268
- const userMessage = `EXISTING SPEC:
269
- ${specTruncated}
270
-
271
- NEW/CHANGED FILES:
272
- ${changedContext}${removedContext}
273
-
274
- Return the complete updated spec JSON only.`;
275
-
276
- const raw = await callGroq(SYSTEM_INCREMENTAL, userMessage);
277
- const updated = parseJSON(raw);
278
- return { spec: updated, changed: true };
279
- }
package/src/cli.js DELETED
@@ -1,198 +0,0 @@
1
- #!/usr/bin/env node
2
- import { program } from "commander";
3
- import chalk from "chalk";
4
- import ora from "ora";
5
- import { readFileSync, existsSync } from "fs";
6
- import { fileURLToPath } from "url";
7
- import path from "path";
8
- import { execSync } from "child_process";
9
-
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const pkgPath = path.join(__dirname, "../package.json");
12
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
13
-
14
- function banner() {
15
- console.log();
16
- console.log(chalk.green.bold(" 🦕 DINOREX") + chalk.dim(" v" + pkg.version));
17
- console.log(chalk.dim(" AI-powered API documentation — one command, full docs."));
18
- console.log();
19
- }
20
-
21
- function checkApiKey(options) {
22
- const isAnthropic = options.provider === "anthropic" || process.env.DINOREX_PROVIDER === "anthropic";
23
-
24
- if (isAnthropic) {
25
- if (options.apiKey) process.env.ANTHROPIC_API_KEY = options.apiKey;
26
- if (!process.env.ANTHROPIC_API_KEY) {
27
- console.log(chalk.red(" ✗ ANTHROPIC_API_KEY is not set.\n"));
28
- console.log(chalk.dim(" Option 1 — env var: export ANTHROPIC_API_KEY=sk-ant-..."));
29
- console.log(chalk.dim(" Option 2 — flag: dinorex scan --provider anthropic --api-key sk-ant-..."));
30
- console.log();
31
- process.exit(1);
32
- }
33
- } else {
34
- if (options.apiKey) process.env.GROQ_API_KEY = options.apiKey;
35
- if (!process.env.GROQ_API_KEY) {
36
- console.log(chalk.red(" ✗ GROQ_API_KEY is not set.\n"));
37
- console.log(chalk.dim(" Get a free key at: https://console.groq.com"));
38
- console.log(chalk.dim(" Then: export GROQ_API_KEY=gsk_your_key_here"));
39
- console.log(chalk.dim(" Or: dinorex scan --api-key gsk_..."));
40
- console.log(chalk.dim(" To use Anthropic instead: dinorex scan --provider anthropic --api-key sk-ant-..."));
41
- console.log();
42
- process.exit(1);
43
- }
44
- }
45
- }
46
-
47
- // ── dinorex scan [dir] ────────────────────────────────────────────────────
48
- program
49
- .name("dinorex")
50
- .version(pkg.version)
51
- .description("AI-powered API documentation generator");
52
-
53
- program
54
- .command("scan [directory]")
55
- .alias("init")
56
- .description("Scan a project and launch the interactive docs UI")
57
- .option("-p, --port <port>", "Port for the docs server", "4321")
58
- .option("--no-open", "Skip auto-opening browser")
59
- .option("--api-key <key>", "Anthropic API key")
60
- .option("--provider <name>", "AI provider: anthropic (default) or groq")
61
- .action(async (directory = ".", options) => {
62
- banner();
63
- checkApiKey(options);
64
-
65
- const targetDir = path.resolve(directory);
66
- const port = parseInt(options.port, 10);
67
-
68
- const { scanProject } = await import("./scanner.js");
69
- const agentFile = options.provider === "anthropic" || process.env.DINOREX_PROVIDER === "anthropic"
70
- ? "./agent.js"
71
- : "./agent.groq.js";
72
- const { analyzeWithAI, analyzeIncremental } = await import(agentFile);
73
- const { startServer } = await import("./server.js");
74
- const { loadStore, saveStore, diffScan } = await import("./store.js");
75
- const { createHash } = await import("crypto");
76
-
77
- console.log(chalk.dim(` 📂 ${targetDir}`));
78
- const providerLabel = (options.provider === "anthropic" || process.env.DINOREX_PROVIDER === "anthropic") ? "anthropic" : "groq (free)";
79
- console.log(chalk.dim(` 🤖 Provider: ${providerLabel}\n`));
80
-
81
- // ── 1. Scan ──
82
- const s1 = ora({ text: "Scanning project files…", color: "green" }).start();
83
- let scanResult;
84
- try { scanResult = await scanProject(targetDir); }
85
- catch (e) { s1.fail(chalk.red(e.message)); process.exit(1); }
86
-
87
- const { summary, collected } = scanResult;
88
- const total = Object.values(summary).reduce((a,b)=>a+b,0);
89
- if (!total) {
90
- s1.fail(chalk.red("No API files found. Make sure you're in an Express/Nest/Fastify project."));
91
- process.exit(1);
92
- }
93
- s1.succeed(chalk.green("Discovered: ") + chalk.white(
94
- `${summary.routes} routes ${summary.controllers} controllers ${summary.services} services ${summary.models} models`
95
- ));
96
-
97
- // ── 2. AI analysis (full or incremental) ──
98
- const stored = loadStore(targetDir);
99
- const allFiles = [...collected.routes, ...collected.controllers, ...collected.services, ...collected.models];
100
- let spec;
101
-
102
- if (stored?.spec && stored?.hashes) {
103
- const diff = diffScan(stored.hashes, allFiles);
104
- const hasChanges = diff.newFiles.length || diff.changedFiles.length || diff.removedFiles.length;
105
-
106
- if (hasChanges) {
107
- const s2 = ora({ text: `Incremental update — ${diff.newFiles.length} new, ${diff.changedFiles.length} changed files…`, color: "cyan" }).start();
108
- try {
109
- const result = await analyzeIncremental(stored.spec, diff);
110
- spec = result.spec;
111
- const hashes = {};
112
- for (const f of allFiles) hashes[f.path] = { hash: createHash("md5").update(f.content).digest("hex") };
113
- saveStore(targetDir, { spec, hashes, lastScan: new Date().toISOString() });
114
- s2.succeed(chalk.cyan("Incremental update complete."));
115
- } catch(e) { s2.fail(chalk.red(e.message)); process.exit(1); }
116
- } else {
117
- spec = stored.spec;
118
- console.log(chalk.dim(" ✓ No changes since last scan — using cached spec."));
119
- }
120
- } else {
121
- const s2 = ora({ text: "Running full AI analysis (~15s)…", color: "cyan" }).start();
122
- try {
123
- spec = await analyzeWithAI(collected, path.basename(targetDir));
124
- const hashes = {};
125
- for (const f of allFiles) hashes[f.path] = { hash: createHash("md5").update(f.content).digest("hex") };
126
- saveStore(targetDir, { spec, hashes, lastScan: new Date().toISOString() });
127
- s2.succeed(chalk.cyan("Analysis complete — ") + chalk.white(
128
- `${spec.collections.reduce((a,c)=>a+c.endpoints.length,0)} endpoints across ${spec.collections.length} collections`
129
- ));
130
- } catch(e) { s2.fail(chalk.red(e.message)); process.exit(1); }
131
- }
132
-
133
- // ── 3. Start server ──
134
- const s3 = ora({ text: `Starting docs server on :${port}…`, color: "yellow" }).start();
135
- let srv;
136
- try { srv = await startServer(targetDir, { port, _cachedSpec: spec }); }
137
- catch(e) { s3.fail(chalk.red(e.message)); process.exit(1); }
138
- s3.succeed(chalk.yellow("Docs server running!"));
139
-
140
- console.log();
141
- console.log(chalk.green.bold(` ✓ Dinorex docs → ${srv.url}`));
142
- console.log(chalk.dim(` Postman export → ${srv.url}/api/export/postman`));
143
- console.log(chalk.dim(` Swagger export → ${srv.url}/api/export/swagger`));
144
- console.log();
145
- console.log(chalk.dim(" Tip: run dinorex scan again anytime to pick up new endpoints."));
146
- console.log(chalk.dim(" Ctrl+C to stop.\n"));
147
-
148
- if (options.open !== false) {
149
- try {
150
- const cmd = process.platform==="darwin" ? `open ${srv.url}` : process.platform==="win32" ? `start ${srv.url}` : `xdg-open ${srv.url}`;
151
- execSync(cmd, { stdio: "ignore" });
152
- } catch {}
153
- }
154
-
155
- process.on("SIGINT", () => { console.log(chalk.dim("\n Dinorex stopped. 🦕\n")); process.exit(0); });
156
- });
157
-
158
- // ── dinorex generate [dir] — just files, no server ────────────────────────
159
- program
160
- .command("generate [directory]")
161
- .description("Generate Postman + Swagger files only (no server)")
162
- .option("--out <dir>", "Output directory", "./dinorex-output")
163
- .option("--api-key <key>", "Anthropic API key")
164
- .action(async (directory = ".", options) => {
165
- banner();
166
- checkApiKey(options);
167
-
168
- const targetDir = path.resolve(directory);
169
- const outputDir = path.resolve(options.out);
170
-
171
- const { scanProject } = await import("./scanner.js");
172
- const { analyzeWithAI } = await import("./agent.js");
173
- const { generatePostmanCollection } = await import("./generators/postman.js");
174
- const { generateSwaggerSpec } = await import("./generators/swagger.js");
175
- const { mkdirSync, writeFileSync } = await import("fs");
176
- const { createHash } = await import("crypto");
177
- const { loadStore, saveStore, diffScan } = await import("./store.js");
178
-
179
- const s1 = ora("Scanning…").start();
180
- const { collected } = await scanProject(targetDir);
181
- s1.succeed("Scanned");
182
-
183
- const s2 = ora("AI analysis…").start();
184
- const spec = await analyzeWithAI(collected, path.basename(targetDir));
185
- s2.succeed("Done");
186
-
187
- mkdirSync(outputDir, { recursive: true });
188
- const slug = spec.projectName.replace(/\s+/g, "-");
189
- const { writeFileSync: wf } = await import("fs");
190
- wf(path.join(outputDir, `${slug}-postman.json`), JSON.stringify(generatePostmanCollection(spec), null, 2));
191
- wf(path.join(outputDir, `${slug}-openapi.yaml`), generateSwaggerSpec(spec));
192
- wf(path.join(outputDir, `${slug}-spec.json`), JSON.stringify(spec, null, 2));
193
-
194
- console.log(chalk.green.bold(`\n ✓ Written to ${outputDir}/`));
195
- console.log(chalk.dim(` ${slug}-postman.json\n ${slug}-openapi.yaml\n ${slug}-spec.json\n`));
196
- });
197
-
198
- program.parse();
@@ -1,84 +0,0 @@
1
- export function generatePostmanCollection(spec) {
2
- const collection = {
3
- info: {
4
- name: spec.projectName,
5
- description: spec.description,
6
- schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7
- version: spec.version,
8
- },
9
- variable: [
10
- {
11
- key: "baseUrl",
12
- value: spec.baseUrl,
13
- type: "string",
14
- },
15
- ],
16
- item: [],
17
- };
18
-
19
- for (const col of spec.collections) {
20
- const folder = {
21
- name: col.name,
22
- description: col.description,
23
- item: [],
24
- };
25
-
26
- for (const ep of col.endpoints) {
27
- const url = {
28
- raw: `{{baseUrl}}${ep.path}`,
29
- host: ["{{baseUrl}}"],
30
- path: ep.path.split("/").filter(Boolean).map((p) => (p.startsWith(":") ? `{{${p.slice(1)}}}` : p)),
31
- };
32
-
33
- if (ep.queryParams && ep.queryParams.length > 0) {
34
- url.query = ep.queryParams.map((q) => ({
35
- key: q.name,
36
- value: String(q.example ?? ""),
37
- description: q.description,
38
- }));
39
- }
40
-
41
- if (ep.pathParams && ep.pathParams.length > 0) {
42
- url.variable = ep.pathParams.map((p) => ({
43
- key: p.name,
44
- value: String(p.example ?? ""),
45
- description: p.description,
46
- }));
47
- }
48
-
49
- const request = {
50
- method: ep.method,
51
- header: [
52
- { key: "Content-Type", value: "application/json" },
53
- ...(ep.requiresAuth
54
- ? [{ key: "Authorization", value: "Bearer {{token}}", description: "Auth token" }]
55
- : []),
56
- ],
57
- url,
58
- description: ep.description,
59
- };
60
-
61
- if (ep.requestBody && ["POST", "PUT", "PATCH"].includes(ep.method)) {
62
- const bodyExample = {};
63
- for (const [field, def] of Object.entries(ep.requestBody.schema || {})) {
64
- bodyExample[field] = def.example ?? "";
65
- }
66
- request.body = {
67
- mode: "raw",
68
- raw: JSON.stringify(bodyExample, null, 2),
69
- options: { raw: { language: "json" } },
70
- };
71
- }
72
-
73
- folder.item.push({
74
- name: ep.summary,
75
- request,
76
- response: [],
77
- });
78
- }
79
-
80
- collection.item.push(folder);
81
- }
82
-
83
- return collection;
84
- }
@@ -1,121 +0,0 @@
1
- import yaml from "js-yaml";
2
-
3
- export function generateSwaggerSpec(spec) {
4
- const openapi = {
5
- openapi: "3.0.3",
6
- info: {
7
- title: spec.projectName,
8
- description: spec.description,
9
- version: spec.version,
10
- },
11
- servers: [{ url: spec.baseUrl }],
12
- tags: spec.collections.map((c) => ({ name: c.name, description: c.description })),
13
- paths: {},
14
- components: {
15
- securitySchemes: {
16
- bearerAuth: {
17
- type: "http",
18
- scheme: "bearer",
19
- bearerFormat: "JWT",
20
- },
21
- },
22
- },
23
- };
24
-
25
- for (const col of spec.collections) {
26
- for (const ep of col.endpoints) {
27
- if (!openapi.paths[ep.path]) {
28
- openapi.paths[ep.path] = {};
29
- }
30
-
31
- const operation = {
32
- tags: [col.name],
33
- summary: ep.summary,
34
- description: ep.description,
35
- operationId: ep.id,
36
- parameters: [],
37
- responses: {},
38
- };
39
-
40
- if (ep.requiresAuth) {
41
- operation.security = [{ bearerAuth: [] }];
42
- }
43
-
44
- // Path params
45
- for (const p of ep.pathParams || []) {
46
- operation.parameters.push({
47
- name: p.name,
48
- in: "path",
49
- required: true,
50
- description: p.description,
51
- schema: { type: p.type || "string", example: p.example },
52
- });
53
- }
54
-
55
- // Query params
56
- for (const q of ep.queryParams || []) {
57
- operation.parameters.push({
58
- name: q.name,
59
- in: "query",
60
- required: false,
61
- description: q.description,
62
- schema: { type: q.type || "string", example: q.example },
63
- });
64
- }
65
-
66
- // Request body
67
- if (ep.requestBody && ["post", "put", "patch"].includes(ep.method.toLowerCase())) {
68
- const properties = {};
69
- const required = [];
70
-
71
- for (const [field, def] of Object.entries(ep.requestBody.schema || {})) {
72
- properties[field] = {
73
- type: def.type || "string",
74
- example: def.example,
75
- description: def.description,
76
- };
77
- if (def.required) required.push(field);
78
- }
79
-
80
- operation.requestBody = {
81
- required: true,
82
- content: {
83
- "application/json": {
84
- schema: {
85
- type: "object",
86
- properties,
87
- ...(required.length > 0 ? { required } : {}),
88
- },
89
- },
90
- },
91
- };
92
- }
93
-
94
- // Responses
95
- for (const [statusCode, res] of Object.entries(ep.responses || {})) {
96
- operation.responses[statusCode] = {
97
- description: res.description,
98
- ...(res.example
99
- ? {
100
- content: {
101
- "application/json": {
102
- schema: { type: "object", example: res.example },
103
- },
104
- },
105
- }
106
- : {}),
107
- };
108
- }
109
-
110
- if (Object.keys(operation.responses).length === 0) {
111
- operation.responses["200"] = { description: "Success" };
112
- }
113
-
114
- if (operation.parameters.length === 0) delete operation.parameters;
115
-
116
- openapi.paths[ep.path][ep.method.toLowerCase()] = operation;
117
- }
118
- }
119
-
120
- return yaml.dump(openapi, { noRefs: true, lineWidth: 120 });
121
- }