codesight 1.1.1 → 1.3.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,105 @@
1
+ /**
2
+ * Dynamic TypeScript compiler loader.
3
+ * Loads the TypeScript compiler from the scanned project's node_modules.
4
+ * Zero new dependencies — borrows TS from the project being analyzed.
5
+ * Falls back gracefully when TypeScript is not available.
6
+ */
7
+ import { createRequire } from "node:module";
8
+ import { readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ let cached = undefined; // undefined = not tried, null = tried and failed
11
+ let cachedRoot = "";
12
+ export function loadTypeScript(projectRoot) {
13
+ if (cached !== undefined && cachedRoot === projectRoot)
14
+ return cached;
15
+ cachedRoot = projectRoot;
16
+ // Strategy 1: createRequire from project root (works for npm/yarn)
17
+ try {
18
+ const req = createRequire(join(projectRoot, "package.json"));
19
+ cached = req("typescript");
20
+ return cached;
21
+ }
22
+ catch { }
23
+ // Strategy 2: Direct path (works for pnpm with public-hoist-pattern)
24
+ try {
25
+ const directPath = join(projectRoot, "node_modules", "typescript");
26
+ const req = createRequire(join(directPath, "package.json"));
27
+ cached = req(directPath);
28
+ return cached;
29
+ }
30
+ catch { }
31
+ // Strategy 3: Find in pnpm .pnpm store (strict mode fallback)
32
+ try {
33
+ const pnpmDir = join(projectRoot, "node_modules", ".pnpm");
34
+ const entries = readdirSync(pnpmDir);
35
+ const tsDir = entries.find((e) => e.startsWith("typescript@"));
36
+ if (tsDir) {
37
+ const tsPath = join(pnpmDir, tsDir, "node_modules", "typescript");
38
+ const req = createRequire(join(tsPath, "package.json"));
39
+ cached = req(tsPath);
40
+ return cached;
41
+ }
42
+ }
43
+ catch { }
44
+ // TypeScript not available — fall back to regex
45
+ cached = null;
46
+ return null;
47
+ }
48
+ export function resetCache() {
49
+ cached = undefined;
50
+ }
51
+ export function parseSourceFile(ts, fileName, content) {
52
+ return ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true // setParentNodes — needed for walking up the tree
53
+ );
54
+ }
55
+ /**
56
+ * Get decorators from a node, handling both TS 4.x (node.decorators)
57
+ * and TS 5.x (node.modifiers with SyntaxKind.Decorator).
58
+ */
59
+ export function getDecorators(ts, node) {
60
+ if (node.decorators)
61
+ return Array.from(node.decorators);
62
+ if (node.modifiers) {
63
+ return node.modifiers.filter((m) => m.kind === ts.SyntaxKind.Decorator);
64
+ }
65
+ return [];
66
+ }
67
+ /**
68
+ * Extract the name and first string argument from a decorator.
69
+ * @returns { name: string, arg: string | null }
70
+ */
71
+ export function parseDecorator(ts, sf, decorator) {
72
+ const SK = ts.SyntaxKind;
73
+ const expr = decorator.expression;
74
+ if (!expr)
75
+ return { name: "", arg: null };
76
+ // @Get() or @Get('path') — CallExpression
77
+ if (expr.kind === SK.CallExpression) {
78
+ const callee = expr.expression;
79
+ const name = callee.kind === SK.Identifier ? callee.getText(sf) : "";
80
+ let arg = null;
81
+ if (expr.arguments?.length > 0) {
82
+ const first = expr.arguments[0];
83
+ if (first.kind === SK.StringLiteral || first.kind === SK.NoSubstitutionTemplateLiteral) {
84
+ arg = first.text;
85
+ }
86
+ }
87
+ return { name, arg };
88
+ }
89
+ // @Controller (without parens) — Identifier
90
+ if (expr.kind === SK.Identifier) {
91
+ return { name: expr.getText(sf), arg: null };
92
+ }
93
+ return { name: "", arg: null };
94
+ }
95
+ /**
96
+ * Get text from a node safely.
97
+ */
98
+ export function getText(sf, node) {
99
+ try {
100
+ return node.getText(sf);
101
+ }
102
+ catch {
103
+ return node.escapedText || node.text || "";
104
+ }
105
+ }
@@ -0,0 +1,11 @@
1
+ import type { ScanResult, BlastRadiusResult } from "../types.js";
2
+ /**
3
+ * Blast radius analysis: given a file, find all transitively affected
4
+ * files, routes, models, and middleware using BFS through the import graph.
5
+ */
6
+ export declare function analyzeBlastRadius(filePath: string, result: ScanResult, maxDepth?: number): BlastRadiusResult;
7
+ /**
8
+ * Multi-file blast radius: given a list of changed files (e.g., from git diff),
9
+ * find the combined blast radius.
10
+ */
11
+ export declare function analyzeMultiFileBlastRadius(files: string[], result: ScanResult, maxDepth?: number): BlastRadiusResult;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Blast radius analysis: given a file, find all transitively affected
3
+ * files, routes, models, and middleware using BFS through the import graph.
4
+ */
5
+ export function analyzeBlastRadius(filePath, result, maxDepth = 3) {
6
+ const { graph, routes, schemas, middleware } = result;
7
+ // Build reverse adjacency map: file -> files that import it
8
+ const importedBy = new Map();
9
+ // Build forward adjacency map: file -> files it imports
10
+ const imports = new Map();
11
+ for (const edge of graph.edges) {
12
+ if (!importedBy.has(edge.to))
13
+ importedBy.set(edge.to, new Set());
14
+ importedBy.get(edge.to).add(edge.from);
15
+ if (!imports.has(edge.from))
16
+ imports.set(edge.from, new Set());
17
+ imports.get(edge.from).add(edge.to);
18
+ }
19
+ // BFS: find all files affected by changing this file
20
+ // "affected" = files that directly or transitively import this file
21
+ const affected = new Set();
22
+ const queue = [{ file: filePath, depth: 0 }];
23
+ while (queue.length > 0) {
24
+ const { file, depth } = queue.shift();
25
+ if (depth > maxDepth)
26
+ continue;
27
+ const dependents = importedBy.get(file);
28
+ if (!dependents)
29
+ continue;
30
+ for (const dep of dependents) {
31
+ if (!affected.has(dep)) {
32
+ affected.add(dep);
33
+ queue.push({ file: dep, depth: depth + 1 });
34
+ }
35
+ }
36
+ }
37
+ // Also include the file itself
38
+ affected.add(filePath);
39
+ // Find affected routes (routes whose handler file is in the affected set)
40
+ const affectedRoutes = routes.filter((r) => affected.has(r.file));
41
+ // Find affected models (schemas referenced in affected files)
42
+ const affectedModels = [];
43
+ for (const schema of schemas) {
44
+ for (const file of affected) {
45
+ // Check if any route/lib in this file touches this model
46
+ const routesInFile = routes.filter((r) => r.file === file);
47
+ if (routesInFile.some((r) => r.tags.includes("db"))) {
48
+ if (!affectedModels.includes(schema.name)) {
49
+ affectedModels.push(schema.name);
50
+ }
51
+ break;
52
+ }
53
+ }
54
+ }
55
+ // Find affected middleware
56
+ const affectedMiddleware = middleware
57
+ .filter((m) => affected.has(m.file))
58
+ .map((m) => m.name);
59
+ return {
60
+ file: filePath,
61
+ affectedFiles: Array.from(affected).filter((f) => f !== filePath),
62
+ affectedRoutes,
63
+ affectedModels,
64
+ affectedMiddleware,
65
+ depth: maxDepth,
66
+ };
67
+ }
68
+ /**
69
+ * Multi-file blast radius: given a list of changed files (e.g., from git diff),
70
+ * find the combined blast radius.
71
+ */
72
+ export function analyzeMultiFileBlastRadius(files, result, maxDepth = 3) {
73
+ const combined = new Set();
74
+ const combinedRoutes = [];
75
+ const combinedModels = new Set();
76
+ const combinedMiddleware = new Set();
77
+ for (const file of files) {
78
+ const br = analyzeBlastRadius(file, result, maxDepth);
79
+ for (const f of br.affectedFiles)
80
+ combined.add(f);
81
+ for (const r of br.affectedRoutes) {
82
+ if (!combinedRoutes.some((cr) => cr.path === r.path && cr.method === r.method)) {
83
+ combinedRoutes.push(r);
84
+ }
85
+ }
86
+ for (const m of br.affectedModels)
87
+ combinedModels.add(m);
88
+ for (const mw of br.affectedMiddleware)
89
+ combinedMiddleware.add(mw);
90
+ }
91
+ // Remove the input files from affected
92
+ for (const file of files)
93
+ combined.delete(file);
94
+ return {
95
+ file: files.join(", "),
96
+ affectedFiles: Array.from(combined),
97
+ affectedRoutes: combinedRoutes,
98
+ affectedModels: Array.from(combinedModels),
99
+ affectedMiddleware: Array.from(combinedMiddleware),
100
+ depth: maxDepth,
101
+ };
102
+ }
@@ -1,5 +1,7 @@
1
1
  import { relative, basename, extname } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
+ import { loadTypeScript } from "../ast/loader.js";
4
+ import { extractReactComponentsAST } from "../ast/extract-components.js";
3
5
  // shadcn/ui + radix primitives to filter out
4
6
  const UI_PRIMITIVES = new Set([
5
7
  "accordion",
@@ -84,6 +86,7 @@ async function detectReactComponents(files, project) {
84
86
  !f.endsWith(".stories.tsx") &&
85
87
  !f.endsWith(".stories.jsx"));
86
88
  const components = [];
89
+ const ts = loadTypeScript(project.root);
87
90
  for (const file of componentFiles) {
88
91
  if (isUIPrimitive(file))
89
92
  continue;
@@ -91,6 +94,14 @@ async function detectReactComponents(files, project) {
91
94
  if (!content)
92
95
  continue;
93
96
  const rel = relative(project.root, file);
97
+ // Try AST first — extracts prop types, handles forwardRef/memo
98
+ if (ts) {
99
+ const astComponents = extractReactComponentsAST(ts, file, content, rel);
100
+ if (astComponents.length > 0) {
101
+ components.push(...astComponents);
102
+ continue;
103
+ }
104
+ }
94
105
  // Detect component name from export
95
106
  let name = "";
96
107
  // export default function ComponentName
@@ -1,5 +1,7 @@
1
1
  import { relative, basename } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
+ import { loadTypeScript } from "../ast/loader.js";
4
+ import { extractRoutesAST } from "../ast/extract-routes.js";
3
5
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
4
6
  const TAG_PATTERNS = [
5
7
  ["auth", [/auth/i, /jwt/i, /token/i, /session/i, /bearer/i, /passport/i, /clerk/i, /betterAuth/i, /better-auth/i]],
@@ -157,11 +159,22 @@ async function detectNextPagesApi(files, project) {
157
159
  async function detectHonoRoutes(files, project) {
158
160
  const tsFiles = files.filter((f) => f.match(/\.(ts|js|tsx|jsx|mjs)$/));
159
161
  const routes = [];
162
+ const ts = loadTypeScript(project.root);
160
163
  for (const file of tsFiles) {
161
164
  const content = await readFileSafe(file);
162
165
  if (!content.includes("hono") && !content.includes("Hono"))
163
166
  continue;
164
167
  const rel = relative(project.root, file);
168
+ const tags = detectTags(content);
169
+ // Try AST first
170
+ if (ts) {
171
+ const astRoutes = extractRoutesAST(ts, rel, content, "hono", tags);
172
+ if (astRoutes.length > 0) {
173
+ routes.push(...astRoutes);
174
+ continue;
175
+ }
176
+ }
177
+ // Regex fallback
165
178
  const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
166
179
  let match;
167
180
  while ((match = routePattern.exec(content)) !== null) {
@@ -172,8 +185,9 @@ async function detectHonoRoutes(files, project) {
172
185
  method: match[1].toUpperCase(),
173
186
  path,
174
187
  file: rel,
175
- tags: detectTags(content),
188
+ tags,
176
189
  framework: "hono",
190
+ confidence: "regex",
177
191
  });
178
192
  }
179
193
  }
@@ -183,11 +197,22 @@ async function detectHonoRoutes(files, project) {
183
197
  async function detectExpressRoutes(files, project) {
184
198
  const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
185
199
  const routes = [];
200
+ const ts = loadTypeScript(project.root);
186
201
  for (const file of tsFiles) {
187
202
  const content = await readFileSafe(file);
188
203
  if (!content.includes("express") && !content.includes("Router"))
189
204
  continue;
190
205
  const rel = relative(project.root, file);
206
+ const tags = detectTags(content);
207
+ // Try AST first
208
+ if (ts) {
209
+ const astRoutes = extractRoutesAST(ts, rel, content, "express", tags);
210
+ if (astRoutes.length > 0) {
211
+ routes.push(...astRoutes);
212
+ continue;
213
+ }
214
+ }
215
+ // Regex fallback
191
216
  const routePattern = /(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
192
217
  let match;
193
218
  while ((match = routePattern.exec(content)) !== null) {
@@ -195,8 +220,9 @@ async function detectExpressRoutes(files, project) {
195
220
  method: match[1].toUpperCase(),
196
221
  path: match[2],
197
222
  file: rel,
198
- tags: detectTags(content),
223
+ tags,
199
224
  framework: "express",
225
+ confidence: "regex",
200
226
  });
201
227
  }
202
228
  }
@@ -206,11 +232,22 @@ async function detectExpressRoutes(files, project) {
206
232
  async function detectFastifyRoutes(files, project) {
207
233
  const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
208
234
  const routes = [];
235
+ const ts = loadTypeScript(project.root);
209
236
  for (const file of tsFiles) {
210
237
  const content = await readFileSafe(file);
211
238
  if (!content.includes("fastify"))
212
239
  continue;
213
240
  const rel = relative(project.root, file);
241
+ const tags = detectTags(content);
242
+ // Try AST first
243
+ if (ts) {
244
+ const astRoutes = extractRoutesAST(ts, rel, content, "fastify", tags);
245
+ if (astRoutes.length > 0) {
246
+ routes.push(...astRoutes);
247
+ continue;
248
+ }
249
+ }
250
+ // Regex fallback
214
251
  const routePattern = /(?:fastify|server|app)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
215
252
  let match;
216
253
  while ((match = routePattern.exec(content)) !== null) {
@@ -218,8 +255,9 @@ async function detectFastifyRoutes(files, project) {
218
255
  method: match[1].toUpperCase(),
219
256
  path: match[2],
220
257
  file: rel,
221
- tags: detectTags(content),
258
+ tags,
222
259
  framework: "fastify",
260
+ confidence: "regex",
223
261
  });
224
262
  }
225
263
  // Object-style route registration
@@ -229,8 +267,9 @@ async function detectFastifyRoutes(files, project) {
229
267
  method: match[1].toUpperCase(),
230
268
  path: match[2],
231
269
  file: rel,
232
- tags: detectTags(content),
270
+ tags,
233
271
  framework: "fastify",
272
+ confidence: "regex",
234
273
  });
235
274
  }
236
275
  }
@@ -240,11 +279,20 @@ async function detectFastifyRoutes(files, project) {
240
279
  async function detectKoaRoutes(files, project) {
241
280
  const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
242
281
  const routes = [];
282
+ const ts = loadTypeScript(project.root);
243
283
  for (const file of tsFiles) {
244
284
  const content = await readFileSafe(file);
245
285
  if (!content.includes("koa") && !content.includes("Router"))
246
286
  continue;
247
287
  const rel = relative(project.root, file);
288
+ const tags = detectTags(content);
289
+ if (ts) {
290
+ const astRoutes = extractRoutesAST(ts, rel, content, "koa", tags);
291
+ if (astRoutes.length > 0) {
292
+ routes.push(...astRoutes);
293
+ continue;
294
+ }
295
+ }
248
296
  const routePattern = /router\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
249
297
  let match;
250
298
  while ((match = routePattern.exec(content)) !== null) {
@@ -252,8 +300,9 @@ async function detectKoaRoutes(files, project) {
252
300
  method: match[1].toUpperCase(),
253
301
  path: match[2],
254
302
  file: rel,
255
- tags: detectTags(content),
303
+ tags,
256
304
  framework: "koa",
305
+ confidence: "regex",
257
306
  });
258
307
  }
259
308
  }
@@ -263,15 +312,24 @@ async function detectKoaRoutes(files, project) {
263
312
  async function detectNestJSRoutes(files, project) {
264
313
  const tsFiles = files.filter((f) => f.match(/\.(ts|js)$/));
265
314
  const routes = [];
315
+ const ts = loadTypeScript(project.root);
266
316
  for (const file of tsFiles) {
267
317
  const content = await readFileSafe(file);
268
318
  if (!content.includes("@Controller") && !content.includes("@Get") && !content.includes("@Post"))
269
319
  continue;
270
320
  const rel = relative(project.root, file);
271
- // Extract controller base path: @Controller('users') or @Controller('/users')
321
+ const tags = detectTags(content);
322
+ // Try AST — NestJS benefits most from AST (decorator + controller prefix combining)
323
+ if (ts) {
324
+ const astRoutes = extractRoutesAST(ts, rel, content, "nestjs", tags);
325
+ if (astRoutes.length > 0) {
326
+ routes.push(...astRoutes);
327
+ continue;
328
+ }
329
+ }
330
+ // Regex fallback
272
331
  const controllerMatch = content.match(/@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/);
273
332
  const basePath = controllerMatch ? "/" + controllerMatch[1].replace(/^\//, "") : "";
274
- // Match method decorators: @Get(), @Post('/create'), @Put(':id'), etc.
275
333
  const decoratorPattern = /@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/gi;
276
334
  let match;
277
335
  while ((match = decoratorPattern.exec(content)) !== null) {
@@ -282,8 +340,9 @@ async function detectNestJSRoutes(files, project) {
282
340
  method,
283
341
  path: fullPath,
284
342
  file: rel,
285
- tags: detectTags(content),
343
+ tags,
286
344
  framework: "nestjs",
345
+ confidence: "regex",
287
346
  });
288
347
  }
289
348
  }
@@ -293,11 +352,20 @@ async function detectNestJSRoutes(files, project) {
293
352
  async function detectElysiaRoutes(files, project) {
294
353
  const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs)$/));
295
354
  const routes = [];
355
+ const ts = loadTypeScript(project.root);
296
356
  for (const file of tsFiles) {
297
357
  const content = await readFileSafe(file);
298
358
  if (!content.includes("elysia") && !content.includes("Elysia"))
299
359
  continue;
300
360
  const rel = relative(project.root, file);
361
+ const tags = detectTags(content);
362
+ if (ts) {
363
+ const astRoutes = extractRoutesAST(ts, rel, content, "elysia", tags);
364
+ if (astRoutes.length > 0) {
365
+ routes.push(...astRoutes);
366
+ continue;
367
+ }
368
+ }
301
369
  const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
302
370
  let match;
303
371
  while ((match = routePattern.exec(content)) !== null) {
@@ -308,8 +376,9 @@ async function detectElysiaRoutes(files, project) {
308
376
  method: match[1].toUpperCase(),
309
377
  path,
310
378
  file: rel,
311
- tags: detectTags(content),
379
+ tags,
312
380
  framework: "elysia",
381
+ confidence: "regex",
313
382
  });
314
383
  }
315
384
  }
@@ -341,6 +410,7 @@ async function detectAdonisRoutes(files, project) {
341
410
  async function detectTRPCRoutes(files, project) {
342
411
  const tsFiles = files.filter((f) => f.match(/\.(ts|js)$/));
343
412
  const routes = [];
413
+ const ts = loadTypeScript(project.root);
344
414
  for (const file of tsFiles) {
345
415
  const content = await readFileSafe(file);
346
416
  if (!content.includes("Procedure") && !content.includes("procedure") && !content.includes("router"))
@@ -348,11 +418,16 @@ async function detectTRPCRoutes(files, project) {
348
418
  if (!content.includes("trpc") && !content.includes("TRPC") && !content.includes("createTRPCRouter") && !content.includes("publicProcedure") && !content.includes("protectedProcedure"))
349
419
  continue;
350
420
  const rel = relative(project.root, file);
351
- // Match tRPC procedure definitions like:
352
- // list: publicProcedure.query(...)
353
- // create: publicProcedure.input(schema).mutation(...)
354
- // getById: t.procedure.input(z.object({...})).query(...)
355
- // Strategy: find lines with "query(" or "mutation(" and extract the procedure name
421
+ const tags = detectTags(content);
422
+ // AST handles tRPC much better — properly parses router nesting and procedure chains
423
+ if (ts) {
424
+ const astRoutes = extractRoutesAST(ts, rel, content, "trpc", tags);
425
+ if (astRoutes.length > 0) {
426
+ routes.push(...astRoutes);
427
+ continue;
428
+ }
429
+ }
430
+ // Regex fallback
356
431
  const lines = content.split("\n");
357
432
  for (const line of lines) {
358
433
  const queryMatch = line.match(/^\s*(\w+)\s*:\s*.*\.(query)\s*\(/);
@@ -366,8 +441,9 @@ async function detectTRPCRoutes(files, project) {
366
441
  method: isQuery ? "QUERY" : "MUTATION",
367
442
  path: procName,
368
443
  file: rel,
369
- tags: detectTags(content),
444
+ tags,
370
445
  framework: "trpc",
446
+ confidence: "regex",
371
447
  });
372
448
  }
373
449
  }
@@ -1,5 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
+ import { loadTypeScript } from "../ast/loader.js";
4
+ import { extractDrizzleSchemaAST, extractTypeORMSchemaAST } from "../ast/extract-schema.js";
3
5
  const AUDIT_FIELDS = new Set([
4
6
  "createdAt",
5
7
  "updatedAt",
@@ -35,10 +37,19 @@ async function detectDrizzleSchemas(files, project) {
35
37
  f.match(/\.schema\.(ts|js)$/) ||
36
38
  f.match(/\/db\/.*\.(ts|js)$/));
37
39
  const models = [];
40
+ const ts = loadTypeScript(project.root);
38
41
  for (const file of schemaFiles) {
39
42
  const content = await readFileSafe(file);
40
43
  if (!content.includes("pgTable") && !content.includes("mysqlTable") && !content.includes("sqliteTable"))
41
44
  continue;
45
+ // Try AST first — much more accurate for Drizzle field chains
46
+ if (ts) {
47
+ const astModels = extractDrizzleSchemaAST(ts, file, content);
48
+ if (astModels.length > 0) {
49
+ models.push(...astModels);
50
+ continue;
51
+ }
52
+ }
42
53
  // Match: export const users = pgTable("users", { ... })
43
54
  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
55
  let match;
@@ -185,10 +196,19 @@ async function detectPrismaSchemas(project) {
185
196
  async function detectTypeORMSchemas(files, project) {
186
197
  const entityFiles = files.filter((f) => f.match(/\.entity\.(ts|js)$/) || f.match(/entities\/.*\.(ts|js)$/));
187
198
  const models = [];
199
+ const ts = loadTypeScript(project.root);
188
200
  for (const file of entityFiles) {
189
201
  const content = await readFileSafe(file);
190
202
  if (!content.includes("@Entity") && !content.includes("@Column"))
191
203
  continue;
204
+ // Try AST first — handles TypeORM decorators accurately
205
+ if (ts) {
206
+ const astModels = extractTypeORMSchemaAST(ts, file, content);
207
+ if (astModels.length > 0) {
208
+ models.push(...astModels);
209
+ continue;
210
+ }
211
+ }
192
212
  // Extract entity name
193
213
  const entityMatch = content.match(/@Entity\s*\(\s*(?:['"`](\w+)['"`])?\s*\)/);
194
214
  const classMatch = content.match(/class\s+(\w+)/);
@@ -1,2 +1,7 @@
1
1
  import type { ScanResult } from "../types.js";
2
2
  export declare function generateAIConfigs(result: ScanResult, root: string): Promise<string[]>;
3
+ /**
4
+ * Generate a profile-specific config file optimized for a particular AI tool.
5
+ * Includes tool-specific instructions on how to use codesight outputs.
6
+ */
7
+ export declare function generateProfileConfig(result: ScanResult, root: string, profile: string): Promise<string>;
@@ -135,3 +135,100 @@ ${context}
135
135
  }
136
136
  return generated;
137
137
  }
138
+ /**
139
+ * Generate a profile-specific config file optimized for a particular AI tool.
140
+ * Includes tool-specific instructions on how to use codesight outputs.
141
+ */
142
+ export async function generateProfileConfig(result, root, profile) {
143
+ const { project, routes, schemas, graph, config } = result;
144
+ // Build a compact always-load summary (~1-2k tokens)
145
+ const summaryLines = [];
146
+ summaryLines.push(`# ${project.name} — Project Context\n`);
147
+ summaryLines.push(`**Stack:** ${project.frameworks.join(", ") || "generic"} | ${project.orms.join(", ") || "none"} | ${project.language}`);
148
+ if (project.isMonorepo) {
149
+ summaryLines.push(`**Monorepo:** ${project.workspaces.map((w) => w.name).join(", ")}`);
150
+ }
151
+ summaryLines.push(`\n${routes.length} routes | ${schemas.length} models | ${config.envVars.length} env vars | ${graph.edges.length} import links\n`);
152
+ // Top entry points
153
+ if (routes.length > 0) {
154
+ const areas = [...new Set(routes.map((r) => r.path.split("/").slice(0, 3).join("/")))].slice(0, 10);
155
+ summaryLines.push(`**API areas:** ${areas.join(", ")}`);
156
+ }
157
+ // High-impact files
158
+ if (graph.hotFiles.length > 0) {
159
+ summaryLines.push(`\n**High-impact files** (change carefully):`);
160
+ for (const hf of graph.hotFiles.slice(0, 5)) {
161
+ summaryLines.push(`- ${hf.file} (imported by ${hf.importedBy} files)`);
162
+ }
163
+ }
164
+ // Required env
165
+ const required = config.envVars.filter((e) => !e.hasDefault);
166
+ if (required.length > 0) {
167
+ summaryLines.push(`\n**Required env vars:** ${required.map((e) => e.name).join(", ")}`);
168
+ }
169
+ summaryLines.push(`\n---\n`);
170
+ // Profile-specific instructions
171
+ switch (profile) {
172
+ case "claude-code": {
173
+ summaryLines.push(`## Instructions for Claude Code\n`);
174
+ summaryLines.push(`Before exploring the repo, read these files in order:`);
175
+ summaryLines.push(`1. \`.codesight/CODESIGHT.md\` — full context map (routes, schema, components, deps)`);
176
+ summaryLines.push(`2. Use the codesight MCP server for targeted queries:\n`);
177
+ summaryLines.push(` - \`codesight_get_summary\` — quick project overview`);
178
+ summaryLines.push(` - \`codesight_get_routes --prefix /api/users\` — filtered routes`);
179
+ summaryLines.push(` - \`codesight_get_blast_radius --file src/lib/db.ts\` — impact analysis before changes`);
180
+ summaryLines.push(` - \`codesight_get_schema --model users\` — specific model details`);
181
+ summaryLines.push(`\nOnly open specific files after consulting codesight context. This saves ~${result.tokenStats.saved.toLocaleString()} tokens per conversation.`);
182
+ const outPath = join(root, "CLAUDE.md");
183
+ const existing = await fileExists(outPath) ? await readFile(outPath, "utf-8") : "";
184
+ if (existing && existing.includes("codesight")) {
185
+ await writeFile(outPath, existing.replace(/# AI Context.*?(?=\n#|\n$|$)/s, summaryLines.join("\n")));
186
+ }
187
+ else if (existing) {
188
+ await writeFile(outPath, existing + "\n\n" + summaryLines.join("\n"));
189
+ }
190
+ else {
191
+ await writeFile(outPath, summaryLines.join("\n"));
192
+ }
193
+ return "CLAUDE.md";
194
+ }
195
+ case "cursor": {
196
+ summaryLines.push(`## Instructions for Cursor\n`);
197
+ summaryLines.push(`When answering questions about this project:`);
198
+ summaryLines.push(`1. Read \`.codesight/CODESIGHT.md\` first for full project structure`);
199
+ summaryLines.push(`2. Use \`.codesight/routes.md\` to find relevant API handlers`);
200
+ summaryLines.push(`3. Use \`.codesight/schema.md\` to understand data models`);
201
+ summaryLines.push(`4. Use \`.codesight/graph.md\` to check blast radius before changes`);
202
+ summaryLines.push(`\nDo not crawl the file tree — the codesight context map already contains the full project structure.`);
203
+ await writeFile(join(root, ".cursorrules"), summaryLines.join("\n"));
204
+ return ".cursorrules";
205
+ }
206
+ case "codex": {
207
+ summaryLines.push(`## Instructions for OpenAI Codex\n`);
208
+ summaryLines.push(`This project uses codesight for structured context. Read .codesight/CODESIGHT.md before exploring files.`);
209
+ summaryLines.push(`Routes: .codesight/routes.md | Schema: .codesight/schema.md | Dependencies: .codesight/graph.md`);
210
+ await writeFile(join(root, "codex.md"), summaryLines.join("\n"));
211
+ return "codex.md";
212
+ }
213
+ case "copilot": {
214
+ summaryLines.push(`## Instructions for GitHub Copilot\n`);
215
+ summaryLines.push(`Project context is pre-generated in .codesight/. Consult CODESIGHT.md for full architecture.`);
216
+ const ghDir = join(root, ".github");
217
+ await fileExists(ghDir) || await (await import("node:fs/promises")).mkdir(ghDir, { recursive: true });
218
+ await writeFile(join(ghDir, "copilot-instructions.md"), summaryLines.join("\n"));
219
+ return ".github/copilot-instructions.md";
220
+ }
221
+ case "windsurf": {
222
+ summaryLines.push(`## Instructions for Windsurf\n`);
223
+ summaryLines.push(`Read .codesight/CODESIGHT.md for full project map before exploring files.`);
224
+ summaryLines.push(`Use .codesight/routes.md for API structure and .codesight/graph.md for dependency analysis.`);
225
+ await writeFile(join(root, ".windsurfrules"), summaryLines.join("\n"));
226
+ return ".windsurfrules";
227
+ }
228
+ default: {
229
+ // Generic profile — write all configs
230
+ const generated = await generateAIConfigs(result, root);
231
+ return generated.join(", ") || "all configs already exist";
232
+ }
233
+ }
234
+ }