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/README.md +126 -0
- package/package.json +40 -0
- package/src/agent.groq.js +279 -0
- package/src/agent.js +155 -0
- package/src/cli.js +198 -0
- package/src/generators/postman.js +84 -0
- package/src/generators/swagger.js +121 -0
- package/src/public/index.html +654 -0
- package/src/scanner.js +119 -0
- package/src/server.js +136 -0
- package/src/store.js +80 -0
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
|
+
}
|