dinorex 1.0.0

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/cli.js ADDED
@@ -0,0 +1,198 @@
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();
@@ -0,0 +1,84 @@
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
+ }
@@ -0,0 +1,121 @@
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
+ }