codesight 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 +213 -0
- package/dist/detectors/components.d.ts +2 -0
- package/dist/detectors/components.js +237 -0
- package/dist/detectors/config.d.ts +2 -0
- package/dist/detectors/config.js +142 -0
- package/dist/detectors/contracts.d.ts +6 -0
- package/dist/detectors/contracts.js +118 -0
- package/dist/detectors/graph.d.ts +2 -0
- package/dist/detectors/graph.js +113 -0
- package/dist/detectors/libs.d.ts +2 -0
- package/dist/detectors/libs.js +206 -0
- package/dist/detectors/middleware.d.ts +2 -0
- package/dist/detectors/middleware.js +116 -0
- package/dist/detectors/routes.d.ts +2 -0
- package/dist/detectors/routes.js +356 -0
- package/dist/detectors/schema.d.ts +2 -0
- package/dist/detectors/schema.js +283 -0
- package/dist/detectors/tokens.d.ts +6 -0
- package/dist/detectors/tokens.js +48 -0
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +268 -0
- package/dist/generators/ai-config.d.ts +2 -0
- package/dist/generators/ai-config.js +137 -0
- package/dist/generators/html-report.d.ts +2 -0
- package/dist/generators/html-report.js +200 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +304 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +171 -0
- package/dist/scanner.d.ts +4 -0
- package/dist/scanner.js +329 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.js +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { relative, basename } from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scanner.js";
|
|
3
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
4
|
+
const TAG_PATTERNS = [
|
|
5
|
+
["auth", [/auth/i, /jwt/i, /token/i, /session/i, /bearer/i, /passport/i, /clerk/i, /betterAuth/i, /better-auth/i]],
|
|
6
|
+
["db", [/prisma/i, /drizzle/i, /typeorm/i, /sequelize/i, /mongoose/i, /knex/i, /sql/i, /\.query\(/i, /\.execute\(/i, /\.findMany\(/i, /\.findFirst\(/i, /\.insert\(/i, /\.update\(/i, /\.delete\(/i]],
|
|
7
|
+
["cache", [/redis/i, /cache/i, /memcache/i, /\.setex\(/i, /\.getex\(/i]],
|
|
8
|
+
["queue", [/bullmq/i, /bull\b/i, /\.add\(\s*['"`]/i, /queue/i]],
|
|
9
|
+
["email", [/resend/i, /sendgrid/i, /nodemailer/i, /\.send\(\s*\{[\s\S]*?to:/i]],
|
|
10
|
+
["payment", [/stripe/i, /polar/i, /paddle/i, /lemon/i, /checkout/i, /webhook/i]],
|
|
11
|
+
["upload", [/multer/i, /formidable/i, /busboy/i, /upload/i, /multipart/i]],
|
|
12
|
+
["ai", [/openai/i, /anthropic/i, /claude/i, /\.chat\.completions/i, /\.messages\.create/i]],
|
|
13
|
+
];
|
|
14
|
+
function detectTags(content) {
|
|
15
|
+
const tags = [];
|
|
16
|
+
for (const [tag, patterns] of TAG_PATTERNS) {
|
|
17
|
+
if (patterns.some((p) => p.test(content))) {
|
|
18
|
+
tags.push(tag);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return tags;
|
|
22
|
+
}
|
|
23
|
+
export async function detectRoutes(files, project) {
|
|
24
|
+
const routes = [];
|
|
25
|
+
for (const fw of project.frameworks) {
|
|
26
|
+
switch (fw) {
|
|
27
|
+
case "next-app":
|
|
28
|
+
routes.push(...(await detectNextAppRoutes(files, project)));
|
|
29
|
+
break;
|
|
30
|
+
case "next-pages":
|
|
31
|
+
routes.push(...(await detectNextPagesApi(files, project)));
|
|
32
|
+
break;
|
|
33
|
+
case "hono":
|
|
34
|
+
routes.push(...(await detectHonoRoutes(files, project)));
|
|
35
|
+
break;
|
|
36
|
+
case "express":
|
|
37
|
+
routes.push(...(await detectExpressRoutes(files, project)));
|
|
38
|
+
break;
|
|
39
|
+
case "fastify":
|
|
40
|
+
routes.push(...(await detectFastifyRoutes(files, project)));
|
|
41
|
+
break;
|
|
42
|
+
case "koa":
|
|
43
|
+
routes.push(...(await detectKoaRoutes(files, project)));
|
|
44
|
+
break;
|
|
45
|
+
case "fastapi":
|
|
46
|
+
routes.push(...(await detectFastAPIRoutes(files, project)));
|
|
47
|
+
break;
|
|
48
|
+
case "flask":
|
|
49
|
+
routes.push(...(await detectFlaskRoutes(files, project)));
|
|
50
|
+
break;
|
|
51
|
+
case "django":
|
|
52
|
+
routes.push(...(await detectDjangoRoutes(files, project)));
|
|
53
|
+
break;
|
|
54
|
+
case "gin":
|
|
55
|
+
case "go-net-http":
|
|
56
|
+
case "fiber":
|
|
57
|
+
routes.push(...(await detectGoRoutes(files, project, fw)));
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return routes;
|
|
62
|
+
}
|
|
63
|
+
// --- Next.js App Router ---
|
|
64
|
+
async function detectNextAppRoutes(files, project) {
|
|
65
|
+
const routeFiles = files.filter((f) => f.match(/\/app\/.*\/route\.(ts|js|tsx|jsx)$/) || f.match(/\/app\/route\.(ts|js|tsx|jsx)$/));
|
|
66
|
+
const routes = [];
|
|
67
|
+
for (const file of routeFiles) {
|
|
68
|
+
const content = await readFileSafe(file);
|
|
69
|
+
const rel = relative(project.root, file);
|
|
70
|
+
// Extract API path from file path
|
|
71
|
+
const pathMatch = rel.match(/(?:src\/)?app(.*)\/route\./);
|
|
72
|
+
const apiPath = pathMatch ? pathMatch[1] || "/" : "/";
|
|
73
|
+
for (const method of HTTP_METHODS) {
|
|
74
|
+
const pattern = new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\b`);
|
|
75
|
+
if (pattern.test(content)) {
|
|
76
|
+
routes.push({
|
|
77
|
+
method,
|
|
78
|
+
path: apiPath,
|
|
79
|
+
file: rel,
|
|
80
|
+
tags: detectTags(content),
|
|
81
|
+
framework: "next-app",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return routes;
|
|
87
|
+
}
|
|
88
|
+
// --- Next.js Pages API ---
|
|
89
|
+
async function detectNextPagesApi(files, project) {
|
|
90
|
+
const apiFiles = files.filter((f) => f.match(/\/pages\/api\/.*\.(ts|js|tsx|jsx)$/));
|
|
91
|
+
const routes = [];
|
|
92
|
+
for (const file of apiFiles) {
|
|
93
|
+
const content = await readFileSafe(file);
|
|
94
|
+
const rel = relative(project.root, file);
|
|
95
|
+
const pathMatch = rel.match(/(?:src\/)?pages(\/api\/.*)\.(?:ts|js|tsx|jsx)$/);
|
|
96
|
+
let apiPath = pathMatch ? pathMatch[1] : "/api";
|
|
97
|
+
apiPath = apiPath.replace(/\/index$/, "").replace(/\[([^\]]+)\]/g, ":$1");
|
|
98
|
+
// Detect methods from handler
|
|
99
|
+
const methods = [];
|
|
100
|
+
for (const method of HTTP_METHODS) {
|
|
101
|
+
if (content.includes(`req.method === '${method}'`) || content.includes(`req.method === "${method}"`)) {
|
|
102
|
+
methods.push(method);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (methods.length === 0)
|
|
106
|
+
methods.push("ALL");
|
|
107
|
+
for (const method of methods) {
|
|
108
|
+
routes.push({
|
|
109
|
+
method,
|
|
110
|
+
path: apiPath,
|
|
111
|
+
file: rel,
|
|
112
|
+
tags: detectTags(content),
|
|
113
|
+
framework: "next-pages",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return routes;
|
|
118
|
+
}
|
|
119
|
+
// --- Hono ---
|
|
120
|
+
async function detectHonoRoutes(files, project) {
|
|
121
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js|tsx|jsx|mjs)$/));
|
|
122
|
+
const routes = [];
|
|
123
|
+
for (const file of tsFiles) {
|
|
124
|
+
const content = await readFileSafe(file);
|
|
125
|
+
if (!content.includes("hono") && !content.includes("Hono"))
|
|
126
|
+
continue;
|
|
127
|
+
const rel = relative(project.root, file);
|
|
128
|
+
// Match: app.get("/path", ...), router.post("/path", ...), .route("/base", ...)
|
|
129
|
+
const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
132
|
+
const path = match[2];
|
|
133
|
+
// Skip non-path strings (middleware keys like "user", "userId", etc.)
|
|
134
|
+
if (!path.startsWith("/") && !path.startsWith(":"))
|
|
135
|
+
continue;
|
|
136
|
+
routes.push({
|
|
137
|
+
method: match[1].toUpperCase(),
|
|
138
|
+
path,
|
|
139
|
+
file: rel,
|
|
140
|
+
tags: detectTags(content),
|
|
141
|
+
framework: "hono",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return routes;
|
|
146
|
+
}
|
|
147
|
+
// --- Express ---
|
|
148
|
+
async function detectExpressRoutes(files, project) {
|
|
149
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
|
|
150
|
+
const routes = [];
|
|
151
|
+
for (const file of tsFiles) {
|
|
152
|
+
const content = await readFileSafe(file);
|
|
153
|
+
if (!content.includes("express") && !content.includes("Router"))
|
|
154
|
+
continue;
|
|
155
|
+
const rel = relative(project.root, file);
|
|
156
|
+
// Match: app.get("/path", ...), router.post("/path", ...)
|
|
157
|
+
const routePattern = /(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
160
|
+
routes.push({
|
|
161
|
+
method: match[1].toUpperCase(),
|
|
162
|
+
path: match[2],
|
|
163
|
+
file: rel,
|
|
164
|
+
tags: detectTags(content),
|
|
165
|
+
framework: "express",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return routes;
|
|
170
|
+
}
|
|
171
|
+
// --- Fastify ---
|
|
172
|
+
async function detectFastifyRoutes(files, project) {
|
|
173
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
|
|
174
|
+
const routes = [];
|
|
175
|
+
for (const file of tsFiles) {
|
|
176
|
+
const content = await readFileSafe(file);
|
|
177
|
+
if (!content.includes("fastify"))
|
|
178
|
+
continue;
|
|
179
|
+
const rel = relative(project.root, file);
|
|
180
|
+
// Match: fastify.get("/path", ...) or server.route({ method: 'GET', url: '/path' })
|
|
181
|
+
const routePattern = /(?:fastify|server|app)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
182
|
+
let match;
|
|
183
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
184
|
+
routes.push({
|
|
185
|
+
method: match[1].toUpperCase(),
|
|
186
|
+
path: match[2],
|
|
187
|
+
file: rel,
|
|
188
|
+
tags: detectTags(content),
|
|
189
|
+
framework: "fastify",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Object-style route registration
|
|
193
|
+
const objPattern = /\.route\s*\(\s*\{[\s\S]*?method:\s*['"`](\w+)['"`][\s\S]*?url:\s*['"`]([^'"`]+)['"`]/gi;
|
|
194
|
+
while ((match = objPattern.exec(content)) !== null) {
|
|
195
|
+
routes.push({
|
|
196
|
+
method: match[1].toUpperCase(),
|
|
197
|
+
path: match[2],
|
|
198
|
+
file: rel,
|
|
199
|
+
tags: detectTags(content),
|
|
200
|
+
framework: "fastify",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return routes;
|
|
205
|
+
}
|
|
206
|
+
// --- Koa ---
|
|
207
|
+
async function detectKoaRoutes(files, project) {
|
|
208
|
+
const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
|
|
209
|
+
const routes = [];
|
|
210
|
+
for (const file of tsFiles) {
|
|
211
|
+
const content = await readFileSafe(file);
|
|
212
|
+
if (!content.includes("koa") && !content.includes("Router"))
|
|
213
|
+
continue;
|
|
214
|
+
const rel = relative(project.root, file);
|
|
215
|
+
const routePattern = /router\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
218
|
+
routes.push({
|
|
219
|
+
method: match[1].toUpperCase(),
|
|
220
|
+
path: match[2],
|
|
221
|
+
file: rel,
|
|
222
|
+
tags: detectTags(content),
|
|
223
|
+
framework: "koa",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return routes;
|
|
228
|
+
}
|
|
229
|
+
// --- FastAPI ---
|
|
230
|
+
async function detectFastAPIRoutes(files, project) {
|
|
231
|
+
const pyFiles = files.filter((f) => f.endsWith(".py"));
|
|
232
|
+
const routes = [];
|
|
233
|
+
for (const file of pyFiles) {
|
|
234
|
+
const content = await readFileSafe(file);
|
|
235
|
+
if (!content.includes("fastapi") && !content.includes("FastAPI") && !content.includes("APIRouter"))
|
|
236
|
+
continue;
|
|
237
|
+
const rel = relative(project.root, file);
|
|
238
|
+
// Match: @app.get("/path") or @router.post("/path") or @api_router.get("/path")
|
|
239
|
+
const routePattern = /@\w+\s*\.\s*(get|post|put|patch|delete|options)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
240
|
+
let match;
|
|
241
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
242
|
+
routes.push({
|
|
243
|
+
method: match[1].toUpperCase(),
|
|
244
|
+
path: match[2],
|
|
245
|
+
file: rel,
|
|
246
|
+
tags: detectTags(content),
|
|
247
|
+
framework: "fastapi",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return routes;
|
|
252
|
+
}
|
|
253
|
+
// --- Flask ---
|
|
254
|
+
async function detectFlaskRoutes(files, project) {
|
|
255
|
+
const pyFiles = files.filter((f) => f.endsWith(".py"));
|
|
256
|
+
const routes = [];
|
|
257
|
+
for (const file of pyFiles) {
|
|
258
|
+
const content = await readFileSafe(file);
|
|
259
|
+
if (!content.includes("flask") && !content.includes("Flask") && !content.includes("Blueprint"))
|
|
260
|
+
continue;
|
|
261
|
+
const rel = relative(project.root, file);
|
|
262
|
+
// Match: @app.route("/path", methods=["GET", "POST"])
|
|
263
|
+
const routePattern = /@(?:app|bp|blueprint)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
|
|
264
|
+
let match;
|
|
265
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
266
|
+
const path = match[1];
|
|
267
|
+
const methods = match[2]
|
|
268
|
+
? match[2].match(/['"](\w+)['"]/g)?.map((m) => m.replace(/['"]/g, "").toUpperCase()) || ["GET"]
|
|
269
|
+
: ["GET"];
|
|
270
|
+
for (const method of methods) {
|
|
271
|
+
routes.push({
|
|
272
|
+
method,
|
|
273
|
+
path,
|
|
274
|
+
file: rel,
|
|
275
|
+
tags: detectTags(content),
|
|
276
|
+
framework: "flask",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return routes;
|
|
282
|
+
}
|
|
283
|
+
// --- Django ---
|
|
284
|
+
async function detectDjangoRoutes(files, project) {
|
|
285
|
+
const pyFiles = files.filter((f) => f.endsWith(".py") && (basename(f) === "urls.py" || basename(f) === "views.py"));
|
|
286
|
+
const routes = [];
|
|
287
|
+
for (const file of pyFiles) {
|
|
288
|
+
const content = await readFileSafe(file);
|
|
289
|
+
const rel = relative(project.root, file);
|
|
290
|
+
// Match: path("api/v1/users/", views.UserView.as_view())
|
|
291
|
+
const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]\s*,/g;
|
|
292
|
+
let match;
|
|
293
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
294
|
+
routes.push({
|
|
295
|
+
method: "ALL",
|
|
296
|
+
path: "/" + match[1],
|
|
297
|
+
file: rel,
|
|
298
|
+
tags: detectTags(content),
|
|
299
|
+
framework: "django",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return routes;
|
|
304
|
+
}
|
|
305
|
+
// --- Go ---
|
|
306
|
+
async function detectGoRoutes(files, project, fw) {
|
|
307
|
+
const goFiles = files.filter((f) => f.endsWith(".go"));
|
|
308
|
+
const routes = [];
|
|
309
|
+
for (const file of goFiles) {
|
|
310
|
+
const content = await readFileSafe(file);
|
|
311
|
+
const rel = relative(project.root, file);
|
|
312
|
+
if (fw === "gin") {
|
|
313
|
+
// Match: r.GET("/path", handler)
|
|
314
|
+
const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
|
|
315
|
+
let match;
|
|
316
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
317
|
+
routes.push({
|
|
318
|
+
method: match[1],
|
|
319
|
+
path: match[2],
|
|
320
|
+
file: rel,
|
|
321
|
+
tags: detectTags(content),
|
|
322
|
+
framework: fw,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else if (fw === "fiber") {
|
|
327
|
+
// Match: app.Get("/path", handler)
|
|
328
|
+
const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
|
|
329
|
+
let match;
|
|
330
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
331
|
+
routes.push({
|
|
332
|
+
method: match[1].toUpperCase(),
|
|
333
|
+
path: match[2],
|
|
334
|
+
file: rel,
|
|
335
|
+
tags: detectTags(content),
|
|
336
|
+
framework: fw,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// net/http: http.HandleFunc("/path", handler) or mux.HandleFunc("/path", handler)
|
|
342
|
+
const pattern = /(?:HandleFunc|Handle)\s*\(\s*["']([^"']+)["']/g;
|
|
343
|
+
let match;
|
|
344
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
345
|
+
routes.push({
|
|
346
|
+
method: "ALL",
|
|
347
|
+
path: match[1],
|
|
348
|
+
file: rel,
|
|
349
|
+
tags: detectTags(content),
|
|
350
|
+
framework: fw,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return routes;
|
|
356
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scanner.js";
|
|
3
|
+
const AUDIT_FIELDS = new Set([
|
|
4
|
+
"createdAt",
|
|
5
|
+
"updatedAt",
|
|
6
|
+
"deletedAt",
|
|
7
|
+
"created_at",
|
|
8
|
+
"updated_at",
|
|
9
|
+
"deleted_at",
|
|
10
|
+
]);
|
|
11
|
+
export async function detectSchemas(files, project) {
|
|
12
|
+
const models = [];
|
|
13
|
+
for (const orm of project.orms) {
|
|
14
|
+
switch (orm) {
|
|
15
|
+
case "drizzle":
|
|
16
|
+
models.push(...(await detectDrizzleSchemas(files, project)));
|
|
17
|
+
break;
|
|
18
|
+
case "prisma":
|
|
19
|
+
models.push(...(await detectPrismaSchemas(project)));
|
|
20
|
+
break;
|
|
21
|
+
case "typeorm":
|
|
22
|
+
models.push(...(await detectTypeORMSchemas(files, project)));
|
|
23
|
+
break;
|
|
24
|
+
case "sqlalchemy":
|
|
25
|
+
models.push(...(await detectSQLAlchemySchemas(files, project)));
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return models;
|
|
30
|
+
}
|
|
31
|
+
// --- Drizzle ORM ---
|
|
32
|
+
async function detectDrizzleSchemas(files, project) {
|
|
33
|
+
const schemaFiles = files.filter((f) => f.match(/schema\.(ts|js)$/) ||
|
|
34
|
+
f.match(/\/schema\/.*\.(ts|js)$/) ||
|
|
35
|
+
f.match(/\.schema\.(ts|js)$/) ||
|
|
36
|
+
f.match(/\/db\/.*\.(ts|js)$/));
|
|
37
|
+
const models = [];
|
|
38
|
+
for (const file of schemaFiles) {
|
|
39
|
+
const content = await readFileSafe(file);
|
|
40
|
+
if (!content.includes("pgTable") && !content.includes("mysqlTable") && !content.includes("sqliteTable"))
|
|
41
|
+
continue;
|
|
42
|
+
// Match: export const users = pgTable("users", { ... })
|
|
43
|
+
const tablePattern = /(?:export\s+)?const\s+(\w+)\s*=\s*(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"`](\w+)['"`]\s*,\s*(?:\(\s*\)\s*=>\s*\(?\s*)?\{([\s\S]*?)\}\s*\)?\s*(?:,|\))/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = tablePattern.exec(content)) !== null) {
|
|
46
|
+
const varName = match[1];
|
|
47
|
+
const tableName = match[2];
|
|
48
|
+
const body = match[3];
|
|
49
|
+
const fields = [];
|
|
50
|
+
const relations = [];
|
|
51
|
+
// Parse fields: fieldName: dataType("col").flags()
|
|
52
|
+
const fieldPattern = /(\w+)\s*:\s*([\w.]+)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)([^,\n]*)/g;
|
|
53
|
+
let fieldMatch;
|
|
54
|
+
while ((fieldMatch = fieldPattern.exec(body)) !== null) {
|
|
55
|
+
const name = fieldMatch[1];
|
|
56
|
+
if (AUDIT_FIELDS.has(name))
|
|
57
|
+
continue;
|
|
58
|
+
const type = fieldMatch[2].replace(/.*\./, ""); // remove prefix like t.
|
|
59
|
+
const chain = fieldMatch[4] || "";
|
|
60
|
+
const flags = [];
|
|
61
|
+
if (chain.includes("primaryKey"))
|
|
62
|
+
flags.push("pk");
|
|
63
|
+
if (chain.includes("unique"))
|
|
64
|
+
flags.push("unique");
|
|
65
|
+
if (chain.includes("notNull"))
|
|
66
|
+
flags.push("required");
|
|
67
|
+
if (chain.includes("default"))
|
|
68
|
+
flags.push("default");
|
|
69
|
+
if (chain.includes("references")) {
|
|
70
|
+
flags.push("fk");
|
|
71
|
+
const refMatch = chain.match(/references\s*\(\s*\(\s*\)\s*=>\s*(\w+)\.(\w+)/);
|
|
72
|
+
if (refMatch)
|
|
73
|
+
relations.push(`${name} -> ${refMatch[1]}.${refMatch[2]}`);
|
|
74
|
+
}
|
|
75
|
+
if (name.endsWith("Id") || name.endsWith("_id")) {
|
|
76
|
+
if (!flags.includes("fk"))
|
|
77
|
+
flags.push("fk");
|
|
78
|
+
}
|
|
79
|
+
fields.push({ name, type, flags });
|
|
80
|
+
}
|
|
81
|
+
if (fields.length > 0) {
|
|
82
|
+
models.push({
|
|
83
|
+
name: tableName,
|
|
84
|
+
fields,
|
|
85
|
+
relations,
|
|
86
|
+
orm: "drizzle",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Also detect Drizzle relations
|
|
91
|
+
const relPattern = /relations\s*\(\s*(\w+)\s*,\s*\(\s*\{([^}]+)\}\s*\)\s*=>\s*\(?\s*\{([\s\S]*?)\}\s*\)?\s*\)/g;
|
|
92
|
+
let relMatch;
|
|
93
|
+
while ((relMatch = relPattern.exec(content)) !== null) {
|
|
94
|
+
const tableName = relMatch[1];
|
|
95
|
+
const relBody = relMatch[3];
|
|
96
|
+
const model = models.find((m) => m.name === tableName);
|
|
97
|
+
if (!model)
|
|
98
|
+
continue;
|
|
99
|
+
const relEntries = /(\w+)\s*:\s*(one|many)\s*\(\s*(\w+)/g;
|
|
100
|
+
let entry;
|
|
101
|
+
while ((entry = relEntries.exec(relBody)) !== null) {
|
|
102
|
+
model.relations.push(`${entry[1]}: ${entry[2]}(${entry[3]})`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return models;
|
|
107
|
+
}
|
|
108
|
+
// --- Prisma ---
|
|
109
|
+
async function detectPrismaSchemas(project) {
|
|
110
|
+
const possiblePaths = [
|
|
111
|
+
join(project.root, "prisma/schema.prisma"),
|
|
112
|
+
join(project.root, "schema.prisma"),
|
|
113
|
+
join(project.root, "prisma/schema"),
|
|
114
|
+
];
|
|
115
|
+
let content = "";
|
|
116
|
+
for (const p of possiblePaths) {
|
|
117
|
+
content = await readFileSafe(p);
|
|
118
|
+
if (content)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!content)
|
|
122
|
+
return [];
|
|
123
|
+
const models = [];
|
|
124
|
+
const modelPattern = /model\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = modelPattern.exec(content)) !== null) {
|
|
127
|
+
const name = match[1];
|
|
128
|
+
const body = match[2];
|
|
129
|
+
const fields = [];
|
|
130
|
+
const relations = [];
|
|
131
|
+
for (const line of body.split("\n")) {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("@@"))
|
|
134
|
+
continue;
|
|
135
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\?|\[\])?\s*(.*)/);
|
|
136
|
+
if (!fieldMatch)
|
|
137
|
+
continue;
|
|
138
|
+
const [, fieldName, fieldType, modifier, rest] = fieldMatch;
|
|
139
|
+
if (AUDIT_FIELDS.has(fieldName))
|
|
140
|
+
continue;
|
|
141
|
+
// Skip relation fields (type is another model)
|
|
142
|
+
if (rest.includes("@relation")) {
|
|
143
|
+
relations.push(`${fieldName}: ${fieldType}${modifier || ""}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (modifier === "[]") {
|
|
147
|
+
relations.push(`${fieldName}: ${fieldType}[]`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const flags = [];
|
|
151
|
+
if (rest.includes("@id"))
|
|
152
|
+
flags.push("pk");
|
|
153
|
+
if (rest.includes("@unique"))
|
|
154
|
+
flags.push("unique");
|
|
155
|
+
if (rest.includes("@default"))
|
|
156
|
+
flags.push("default");
|
|
157
|
+
if (modifier === "?")
|
|
158
|
+
flags.push("nullable");
|
|
159
|
+
if (fieldName.endsWith("Id") || fieldName.endsWith("_id"))
|
|
160
|
+
flags.push("fk");
|
|
161
|
+
fields.push({ name: fieldName, type: fieldType, flags });
|
|
162
|
+
}
|
|
163
|
+
if (fields.length > 0) {
|
|
164
|
+
models.push({ name, fields, relations, orm: "prisma" });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Also detect enums
|
|
168
|
+
const enumPattern = /enum\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
169
|
+
while ((match = enumPattern.exec(content)) !== null) {
|
|
170
|
+
const name = match[1];
|
|
171
|
+
const values = match[2]
|
|
172
|
+
.split("\n")
|
|
173
|
+
.map((l) => l.trim())
|
|
174
|
+
.filter((l) => l && !l.startsWith("//"));
|
|
175
|
+
models.push({
|
|
176
|
+
name: `enum:${name}`,
|
|
177
|
+
fields: values.map((v) => ({ name: v, type: "enum", flags: [] })),
|
|
178
|
+
relations: [],
|
|
179
|
+
orm: "prisma",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return models;
|
|
183
|
+
}
|
|
184
|
+
// --- TypeORM ---
|
|
185
|
+
async function detectTypeORMSchemas(files, project) {
|
|
186
|
+
const entityFiles = files.filter((f) => f.match(/\.entity\.(ts|js)$/) || f.match(/entities\/.*\.(ts|js)$/));
|
|
187
|
+
const models = [];
|
|
188
|
+
for (const file of entityFiles) {
|
|
189
|
+
const content = await readFileSafe(file);
|
|
190
|
+
if (!content.includes("@Entity") && !content.includes("@Column"))
|
|
191
|
+
continue;
|
|
192
|
+
// Extract entity name
|
|
193
|
+
const entityMatch = content.match(/@Entity\s*\(\s*(?:['"`](\w+)['"`])?\s*\)/);
|
|
194
|
+
const classMatch = content.match(/class\s+(\w+)/);
|
|
195
|
+
const name = entityMatch?.[1] || classMatch?.[1] || "Unknown";
|
|
196
|
+
const fields = [];
|
|
197
|
+
const relations = [];
|
|
198
|
+
// Match columns
|
|
199
|
+
const colPattern = /@(?:PrimaryGeneratedColumn|PrimaryColumn|Column|CreateDateColumn|UpdateDateColumn)\s*\(([^)]*)\)\s*\n\s*(\w+)\s*[!?]?\s*:\s*(\w+)/g;
|
|
200
|
+
let match;
|
|
201
|
+
while ((match = colPattern.exec(content)) !== null) {
|
|
202
|
+
const decorator = match[0];
|
|
203
|
+
const fieldName = match[2];
|
|
204
|
+
const fieldType = match[3];
|
|
205
|
+
if (AUDIT_FIELDS.has(fieldName))
|
|
206
|
+
continue;
|
|
207
|
+
const flags = [];
|
|
208
|
+
if (decorator.includes("PrimaryGeneratedColumn") || decorator.includes("PrimaryColumn"))
|
|
209
|
+
flags.push("pk");
|
|
210
|
+
if (decorator.includes("unique: true"))
|
|
211
|
+
flags.push("unique");
|
|
212
|
+
if (decorator.includes("nullable: true"))
|
|
213
|
+
flags.push("nullable");
|
|
214
|
+
if (decorator.includes("default:"))
|
|
215
|
+
flags.push("default");
|
|
216
|
+
fields.push({ name: fieldName, type: fieldType, flags });
|
|
217
|
+
}
|
|
218
|
+
// Match relations
|
|
219
|
+
const relPattern = /@(?:OneToMany|ManyToOne|OneToOne|ManyToMany)\s*\([^)]*\)\s*\n\s*(\w+)\s*[!?]?\s*:\s*(\w+)/g;
|
|
220
|
+
while ((match = relPattern.exec(content)) !== null) {
|
|
221
|
+
relations.push(`${match[1]}: ${match[2]}`);
|
|
222
|
+
}
|
|
223
|
+
if (fields.length > 0) {
|
|
224
|
+
models.push({ name, fields, relations, orm: "typeorm" });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return models;
|
|
228
|
+
}
|
|
229
|
+
// --- SQLAlchemy ---
|
|
230
|
+
async function detectSQLAlchemySchemas(files, project) {
|
|
231
|
+
const pyFiles = files.filter((f) => f.endsWith(".py"));
|
|
232
|
+
const models = [];
|
|
233
|
+
for (const file of pyFiles) {
|
|
234
|
+
const content = await readFileSafe(file);
|
|
235
|
+
if (!content.includes("Column") && !content.includes("mapped_column"))
|
|
236
|
+
continue;
|
|
237
|
+
if (!content.includes("Base") && !content.includes("DeclarativeBase") && !content.includes("Model"))
|
|
238
|
+
continue;
|
|
239
|
+
// Match class definitions
|
|
240
|
+
const classPattern = /class\s+(\w+)\s*\([^)]*(?:Base|Model|DeclarativeBase)[^)]*\)\s*:([\s\S]*?)(?=\nclass\s|\n[^\s]|\Z)/g;
|
|
241
|
+
let match;
|
|
242
|
+
while ((match = classPattern.exec(content)) !== null) {
|
|
243
|
+
const name = match[1];
|
|
244
|
+
const body = match[2];
|
|
245
|
+
const fields = [];
|
|
246
|
+
const relations = [];
|
|
247
|
+
// Match Column definitions
|
|
248
|
+
const colPattern = /(\w+)\s*(?::\s*Mapped\[([^\]]+)\]\s*=\s*mapped_column|=\s*(?:db\.)?Column)\s*\(([^)]*)\)/g;
|
|
249
|
+
let colMatch;
|
|
250
|
+
while ((colMatch = colPattern.exec(body)) !== null) {
|
|
251
|
+
const fieldName = colMatch[1];
|
|
252
|
+
if (AUDIT_FIELDS.has(fieldName))
|
|
253
|
+
continue;
|
|
254
|
+
const mappedType = colMatch[2] || "";
|
|
255
|
+
const args = colMatch[3];
|
|
256
|
+
const flags = [];
|
|
257
|
+
if (args.includes("primary_key=True"))
|
|
258
|
+
flags.push("pk");
|
|
259
|
+
if (args.includes("unique=True"))
|
|
260
|
+
flags.push("unique");
|
|
261
|
+
if (args.includes("nullable=True"))
|
|
262
|
+
flags.push("nullable");
|
|
263
|
+
if (args.includes("ForeignKey"))
|
|
264
|
+
flags.push("fk");
|
|
265
|
+
if (args.includes("default="))
|
|
266
|
+
flags.push("default");
|
|
267
|
+
const typeMatch = args.match(/(?:String|Integer|Boolean|Float|Text|DateTime|JSON|UUID)/);
|
|
268
|
+
const type = mappedType || typeMatch?.[0] || "unknown";
|
|
269
|
+
fields.push({ name: fieldName, type, flags });
|
|
270
|
+
}
|
|
271
|
+
// Match relationship
|
|
272
|
+
const relPattern = /(\w+)\s*=\s*relationship\s*\(\s*['"](\w+)['"]/g;
|
|
273
|
+
let relMatch;
|
|
274
|
+
while ((relMatch = relPattern.exec(body)) !== null) {
|
|
275
|
+
relations.push(`${relMatch[1]}: ${relMatch[2]}`);
|
|
276
|
+
}
|
|
277
|
+
if (fields.length > 0) {
|
|
278
|
+
models.push({ name, fields, relations, orm: "sqlalchemy" });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return models;
|
|
283
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ScanResult, TokenStats } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Calculates token stats: how many tokens the output uses
|
|
4
|
+
* vs. how many an AI would spend exploring the same info manually
|
|
5
|
+
*/
|
|
6
|
+
export declare function calculateTokenStats(result: ScanResult, outputContent: string, fileCount: number): TokenStats;
|