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.
@@ -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,2 @@
1
+ import type { SchemaModel, ProjectInfo } from "../types.js";
2
+ export declare function detectSchemas(files: string[], project: ProjectInfo): Promise<SchemaModel[]>;
@@ -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;