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/README.md +0 -15
- package/dist/agent.d.ts +8 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.groq.d.ts +14 -0
- package/dist/agent.groq.d.ts.map +1 -0
- package/dist/agent.groq.js +213 -0
- package/dist/agent.groq.js.map +1 -0
- package/{src → dist}/agent.js +75 -71
- package/dist/agent.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +179 -0
- package/dist/cli.js.map +1 -0
- package/dist/scanner.d.ts +20 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +108 -0
- package/dist/scanner.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +116 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +75 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +72 -0
- package/dist/store.js.map +1 -0
- package/package.json +22 -8
- package/src/public/index.html +356 -396
- package/src/agent.groq.js +0 -279
- package/src/cli.js +0 -198
- package/src/generators/postman.js +0 -84
- package/src/generators/swagger.js +0 -121
- package/src/scanner.js +0 -119
- package/src/server.js +0 -136
- package/src/store.js +0 -80
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
|
-
}
|