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/scanner.js ADDED
@@ -0,0 +1,119 @@
1
+ import { glob } from "glob";
2
+ import { readFileSync } from "fs";
3
+ import path from "path";
4
+
5
+ const ROUTE_PATTERNS = [
6
+ "**/routes/**/*.{js,ts,tsx}",
7
+ "**/route/**/*.{js,ts,tsx}",
8
+ "**/router/**/*.{js,ts,tsx}",
9
+ "**/*route*.{js,ts,tsx}",
10
+ "**/*router*.{js,ts,tsx}",
11
+ "**/*Route*.{js,ts,tsx}",
12
+ "**/*Router*.{js,ts,tsx}",
13
+ ];
14
+
15
+ const CONTROLLER_PATTERNS = [
16
+ "**/controllers/**/*.{js,ts,tsx}",
17
+ "**/controller/**/*.{js,ts,tsx}",
18
+ "**/*controller*.{js,ts,tsx}",
19
+ "**/*Controller*.{js,ts,tsx}",
20
+ ];
21
+
22
+ const SERVICE_PATTERNS = [
23
+ "**/services/**/*.{js,ts,tsx}",
24
+ "**/service/**/*.{js,ts,tsx}",
25
+ "**/*service*.{js,ts,tsx}",
26
+ "**/*Service*.{js,ts,tsx}",
27
+ ];
28
+
29
+ const MODEL_PATTERNS = [
30
+ "**/models/**/*.{js,ts,tsx}",
31
+ "**/model/**/*.{js,ts,tsx}",
32
+ "**/schemas/**/*.{js,ts,tsx}",
33
+ "**/schema/**/*.{js,ts,tsx}",
34
+ "**/entities/**/*.{js,ts,tsx}",
35
+ "**/dto/**/*.{js,ts,tsx}",
36
+ "**/dtos/**/*.{js,ts,tsx}",
37
+ "**/*model*.{js,ts,tsx}",
38
+ "**/*Model*.{js,ts,tsx}",
39
+ "**/*schema*.{js,ts,tsx}",
40
+ "**/*Schema*.{js,ts,tsx}",
41
+ "**/*entity*.{js,ts,tsx}",
42
+ "**/*Entity*.{js,ts,tsx}",
43
+ "**/*.dto.{ts,tsx}",
44
+ ];
45
+
46
+ const IGNORE_DIRS = [
47
+ "**/node_modules/**",
48
+ "**/.git/**",
49
+ "**/dist/**",
50
+ "**/build/**",
51
+ "**/.next/**",
52
+ "**/coverage/**",
53
+ "**/*.test.*",
54
+ "**/*.spec.*",
55
+ "**/__tests__/**",
56
+ ];
57
+
58
+ async function findFiles(patterns, cwd) {
59
+ const results = new Set();
60
+ for (const pattern of patterns) {
61
+ const files = await glob(pattern, { cwd, ignore: IGNORE_DIRS, absolute: true });
62
+ files.forEach((f) => results.add(f));
63
+ }
64
+ return [...results];
65
+ }
66
+
67
+ function readFile(filePath) {
68
+ try {
69
+ return readFileSync(filePath, "utf-8");
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function truncate(content, maxChars = 8000) {
76
+ if (content.length <= maxChars) return content;
77
+ return content.slice(0, maxChars) + "\n\n... [truncated for length]";
78
+ }
79
+
80
+ export async function scanProject(targetDir) {
81
+ const cwd = path.resolve(targetDir);
82
+
83
+ console.log(`\nšŸ¦• Dinorex scanning: ${cwd}\n`);
84
+
85
+ const [routeFiles, controllerFiles, serviceFiles, modelFiles] = await Promise.all([
86
+ findFiles(ROUTE_PATTERNS, cwd),
87
+ findFiles(CONTROLLER_PATTERNS, cwd),
88
+ findFiles(SERVICE_PATTERNS, cwd),
89
+ findFiles(MODEL_PATTERNS, cwd),
90
+ ]);
91
+
92
+ const summary = {
93
+ routes: routeFiles.length,
94
+ controllers: controllerFiles.length,
95
+ services: serviceFiles.length,
96
+ models: modelFiles.length,
97
+ };
98
+
99
+ const collected = {
100
+ routes: routeFiles.map((f) => ({
101
+ path: path.relative(cwd, f),
102
+ content: truncate(readFile(f) || ""),
103
+ })),
104
+ controllers: controllerFiles.map((f) => ({
105
+ path: path.relative(cwd, f),
106
+ content: truncate(readFile(f) || ""),
107
+ })),
108
+ services: serviceFiles.map((f) => ({
109
+ path: path.relative(cwd, f),
110
+ content: truncate(readFile(f) || ""),
111
+ })),
112
+ models: modelFiles.map((f) => ({
113
+ path: path.relative(cwd, f),
114
+ content: truncate(readFile(f) || ""),
115
+ })),
116
+ };
117
+
118
+ return { cwd, summary, collected };
119
+ }
package/src/server.js ADDED
@@ -0,0 +1,136 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { scanProject } from "./scanner.js";
6
+ import { analyzeWithAI, analyzeIncremental } from "./agent.js";
7
+ import { generatePostmanCollection } from "./generators/postman.js";
8
+ import { generateSwaggerSpec } from "./generators/swagger.js";
9
+ import { loadStore, saveStore, diffScan } from "./store.js";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ export async function startServer(targetDir, options = {}) {
14
+ const { port = 4321, _cachedSpec = null } = options;
15
+
16
+ const app = express();
17
+ app.use(cors());
18
+ app.use(express.json());
19
+ app.use(express.static(path.join(__dirname, "public")));
20
+
21
+ let specCache = _cachedSpec;
22
+ let isAnalyzing = false;
23
+ let analysisStatus = _cachedSpec ? { state: "ready" } : { state: "pending", message: "Starting analysis..." };
24
+
25
+ async function runAnalysis(forceRescan = false) {
26
+ if (isAnalyzing) return;
27
+ isAnalyzing = true;
28
+
29
+ try {
30
+ const { collected, summary } = await scanProject(targetDir);
31
+ const allFiles = [
32
+ ...collected.routes,
33
+ ...collected.controllers,
34
+ ...collected.services,
35
+ ...collected.models,
36
+ ];
37
+
38
+ const stored = loadStore(targetDir);
39
+
40
+ if (!forceRescan && stored && stored.spec && stored.hashes) {
41
+ // Incremental mode
42
+ analysisStatus = { state: "analyzing", message: "Checking for new/changed endpoints..." };
43
+ const diff = diffScan(stored.hashes, allFiles);
44
+
45
+ const { spec, changed } = await analyzeIncremental(stored.spec, diff);
46
+ specCache = spec;
47
+
48
+ if (changed) {
49
+ saveStore(targetDir, { spec, hashes: diff.newHashes, lastScan: new Date().toISOString() });
50
+ analysisStatus = { state: "ready", message: "Spec updated with new endpoints." };
51
+ } else {
52
+ analysisStatus = { state: "ready", message: "No changes detected." };
53
+ }
54
+ } else {
55
+ // Full scan
56
+ analysisStatus = { state: "analyzing", message: "Running full AI analysis..." };
57
+ const projectName = path.basename(targetDir);
58
+ specCache = await analyzeWithAI(collected, projectName);
59
+
60
+ const hashes = {};
61
+ for (const f of allFiles) {
62
+ const { createHash } = await import("crypto");
63
+ hashes[f.path] = { hash: createHash("md5").update(f.content).digest("hex") };
64
+ }
65
+ saveStore(targetDir, { spec: specCache, hashes, lastScan: new Date().toISOString() });
66
+ analysisStatus = { state: "ready", message: "Full analysis complete." };
67
+ }
68
+ } catch (err) {
69
+ analysisStatus = { state: "error", message: err.message };
70
+ } finally {
71
+ isAnalyzing = false;
72
+ }
73
+ }
74
+
75
+ // On startup: use cache if no cached spec passed in
76
+ if (!specCache) {
77
+ const stored = loadStore(targetDir);
78
+ if (stored?.spec) {
79
+ specCache = stored.spec;
80
+ analysisStatus = { state: "ready", message: "Loaded from cache." };
81
+ // Still run incremental check in background
82
+ runAnalysis(false);
83
+ } else {
84
+ runAnalysis(true);
85
+ }
86
+ } else {
87
+ analysisStatus = { state: "ready" };
88
+ }
89
+
90
+ // ── Routes ──────────────────────────────────────────────────────────────
91
+
92
+ app.get("/api/status", (req, res) => res.json(analysisStatus));
93
+
94
+ app.get("/api/spec", (req, res) => {
95
+ if (!specCache) return res.json({ _loading: true, status: analysisStatus });
96
+ res.json(specCache);
97
+ });
98
+
99
+ app.post("/api/rescan", async (req, res) => {
100
+ if (isAnalyzing) return res.json({ message: "Already scanning..." });
101
+ runAnalysis(false); // incremental
102
+ res.json({ message: "Rescan started" });
103
+ });
104
+
105
+ app.post("/api/rescan/full", async (req, res) => {
106
+ if (isAnalyzing) return res.json({ message: "Already scanning..." });
107
+ runAnalysis(true); // force full
108
+ res.json({ message: "Full rescan started" });
109
+ });
110
+
111
+ app.get("/api/export/postman", (req, res) => {
112
+ if (!specCache) return res.status(503).json({ error: "Still analyzing" });
113
+ const collection = generatePostmanCollection(specCache);
114
+ res.setHeader("Content-Type", "application/json");
115
+ res.setHeader("Content-Disposition", `attachment; filename="${specCache.projectName.replace(/\s+/g, "-")}-postman.json"`);
116
+ res.json(collection);
117
+ });
118
+
119
+ app.get("/api/export/swagger", (req, res) => {
120
+ if (!specCache) return res.status(503).json({ error: "Still analyzing" });
121
+ const yamlContent = generateSwaggerSpec(specCache);
122
+ res.setHeader("Content-Type", "application/x-yaml");
123
+ res.setHeader("Content-Disposition", `attachment; filename="${specCache.projectName.replace(/\s+/g, "-")}-openapi.yaml"`);
124
+ res.send(yamlContent);
125
+ });
126
+
127
+ app.get("*", (req, res) => {
128
+ res.sendFile(path.join(__dirname, "public", "index.html"));
129
+ });
130
+
131
+ return new Promise((resolve) => {
132
+ const server = app.listen(port, () => {
133
+ resolve({ server, port, url: `http://localhost:${port}` });
134
+ });
135
+ });
136
+ }
package/src/store.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Persistent spec store — saves to .dinorex/spec.json in the user's project.
3
+ * Tracks which files have been seen so we can detect new/changed routes.
4
+ */
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
6
+ import path from "path";
7
+ import crypto from "crypto";
8
+
9
+ function storePath(projectDir) {
10
+ return path.join(projectDir, ".dinorex");
11
+ }
12
+
13
+ function specFile(projectDir) {
14
+ return path.join(storePath(projectDir), "spec.json");
15
+ }
16
+
17
+ function hashContent(content) {
18
+ return crypto.createHash("md5").update(content).digest("hex");
19
+ }
20
+
21
+ export function loadStore(projectDir) {
22
+ const file = specFile(projectDir);
23
+ if (!existsSync(file)) return null;
24
+ try {
25
+ return JSON.parse(readFileSync(file, "utf-8"));
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export function saveStore(projectDir, data) {
32
+ const dir = storePath(projectDir);
33
+ mkdirSync(dir, { recursive: true });
34
+
35
+ // write a .gitignore inside .dinorex so spec isn't committed
36
+ const gi = path.join(dir, ".gitignore");
37
+ if (!existsSync(gi)) writeFileSync(gi, "*\n");
38
+
39
+ writeFileSync(specFile(projectDir), JSON.stringify(data, null, 2));
40
+ }
41
+
42
+ /**
43
+ * Compare new scan against stored file hashes.
44
+ * Returns:
45
+ * - newFiles : files that didn't exist before
46
+ * - changedFiles: files whose content changed
47
+ * - removedFiles: files that no longer exist
48
+ * - unchanged : everything else
49
+ */
50
+ export function diffScan(storedHashes = {}, currentFiles) {
51
+ const currentHashes = {};
52
+ for (const f of currentFiles) {
53
+ currentHashes[f.path] = { hash: hashContent(f.content), content: f.content };
54
+ }
55
+
56
+ const newFiles = [];
57
+ const changedFiles = [];
58
+ const unchanged = [];
59
+
60
+ for (const f of currentFiles) {
61
+ const prev = storedHashes[f.path];
62
+ if (!prev) {
63
+ newFiles.push(f);
64
+ } else if (prev.hash !== currentHashes[f.path].hash) {
65
+ changedFiles.push(f);
66
+ } else {
67
+ unchanged.push(f);
68
+ }
69
+ }
70
+
71
+ const currentPaths = new Set(currentFiles.map(f => f.path));
72
+ const removedFiles = Object.keys(storedHashes).filter(p => !currentPaths.has(p));
73
+
74
+ const newHashes = {};
75
+ for (const f of currentFiles) {
76
+ newHashes[f.path] = { hash: currentHashes[f.path].hash };
77
+ }
78
+
79
+ return { newFiles, changedFiles, removedFiles, unchanged, newHashes };
80
+ }