ai-first-cli 1.3.5 → 1.3.8

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.
Files changed (126) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.es.md +68 -0
  3. package/README.md +53 -15
  4. package/ai/graph/knowledge-graph.json +1 -1
  5. package/ai-context/index-state.json +86 -2
  6. package/dist/analyzers/architecture.d.ts.map +1 -1
  7. package/dist/analyzers/architecture.js +72 -5
  8. package/dist/analyzers/architecture.js.map +1 -1
  9. package/dist/analyzers/entrypoints.d.ts.map +1 -1
  10. package/dist/analyzers/entrypoints.js +253 -0
  11. package/dist/analyzers/entrypoints.js.map +1 -1
  12. package/dist/analyzers/symbols.d.ts.map +1 -1
  13. package/dist/analyzers/symbols.js +47 -2
  14. package/dist/analyzers/symbols.js.map +1 -1
  15. package/dist/analyzers/techStack.d.ts.map +1 -1
  16. package/dist/analyzers/techStack.js +86 -0
  17. package/dist/analyzers/techStack.js.map +1 -1
  18. package/dist/commands/ai-first.d.ts.map +1 -1
  19. package/dist/commands/ai-first.js +78 -4
  20. package/dist/commands/ai-first.js.map +1 -1
  21. package/dist/config/configLoader.d.ts +6 -0
  22. package/dist/config/configLoader.d.ts.map +1 -0
  23. package/dist/config/configLoader.js +232 -0
  24. package/dist/config/configLoader.js.map +1 -0
  25. package/dist/config/index.d.ts +3 -0
  26. package/dist/config/index.d.ts.map +1 -0
  27. package/dist/config/index.js +2 -0
  28. package/dist/config/index.js.map +1 -0
  29. package/dist/config/types.d.ts +101 -0
  30. package/dist/config/types.d.ts.map +1 -0
  31. package/dist/config/types.js +2 -0
  32. package/dist/config/types.js.map +1 -0
  33. package/dist/core/content/contentProcessor.d.ts +4 -0
  34. package/dist/core/content/contentProcessor.d.ts.map +1 -0
  35. package/dist/core/content/contentProcessor.js +235 -0
  36. package/dist/core/content/contentProcessor.js.map +1 -0
  37. package/dist/core/content/index.d.ts +3 -0
  38. package/dist/core/content/index.d.ts.map +1 -0
  39. package/dist/core/content/index.js +2 -0
  40. package/dist/core/content/index.js.map +1 -0
  41. package/dist/core/content/types.d.ts +32 -0
  42. package/dist/core/content/types.d.ts.map +1 -0
  43. package/dist/core/content/types.js +2 -0
  44. package/dist/core/content/types.js.map +1 -0
  45. package/dist/core/gitAnalyzer.d.ts +14 -0
  46. package/dist/core/gitAnalyzer.d.ts.map +1 -1
  47. package/dist/core/gitAnalyzer.js +98 -0
  48. package/dist/core/gitAnalyzer.js.map +1 -1
  49. package/dist/core/multiRepo/index.d.ts +3 -0
  50. package/dist/core/multiRepo/index.d.ts.map +1 -0
  51. package/dist/core/multiRepo/index.js +2 -0
  52. package/dist/core/multiRepo/index.js.map +1 -0
  53. package/dist/core/multiRepo/multiRepoScanner.d.ts +18 -0
  54. package/dist/core/multiRepo/multiRepoScanner.d.ts.map +1 -0
  55. package/dist/core/multiRepo/multiRepoScanner.js +131 -0
  56. package/dist/core/multiRepo/multiRepoScanner.js.map +1 -0
  57. package/dist/core/rag/index.d.ts +3 -0
  58. package/dist/core/rag/index.d.ts.map +1 -0
  59. package/dist/core/rag/index.js +2 -0
  60. package/dist/core/rag/index.js.map +1 -0
  61. package/dist/core/rag/vectorIndex.d.ts +28 -0
  62. package/dist/core/rag/vectorIndex.d.ts.map +1 -0
  63. package/dist/core/rag/vectorIndex.js +71 -0
  64. package/dist/core/rag/vectorIndex.js.map +1 -0
  65. package/dist/mcp/index.d.ts +2 -0
  66. package/dist/mcp/index.d.ts.map +1 -0
  67. package/dist/mcp/index.js +2 -0
  68. package/dist/mcp/index.js.map +1 -0
  69. package/dist/mcp/server.d.ts +7 -0
  70. package/dist/mcp/server.d.ts.map +1 -0
  71. package/dist/mcp/server.js +154 -0
  72. package/dist/mcp/server.js.map +1 -0
  73. package/dist/utils/fileUtils.d.ts.map +1 -1
  74. package/dist/utils/fileUtils.js +5 -0
  75. package/dist/utils/fileUtils.js.map +1 -1
  76. package/docs/planning/evaluator-v1.0.0/README.md +112 -0
  77. package/docs/planning/evaluator-v1.0.0/improvements_plan_2026-03-28.md +237 -0
  78. package/package.json +13 -3
  79. package/src/analyzers/architecture.ts +75 -6
  80. package/src/analyzers/entrypoints.ts +285 -0
  81. package/src/analyzers/symbols.ts +52 -2
  82. package/src/analyzers/techStack.ts +90 -0
  83. package/src/commands/ai-first.ts +83 -4
  84. package/src/config/configLoader.ts +274 -0
  85. package/src/config/index.ts +27 -0
  86. package/src/config/types.ts +117 -0
  87. package/src/core/content/contentProcessor.ts +292 -0
  88. package/src/core/content/index.ts +9 -0
  89. package/src/core/content/types.ts +35 -0
  90. package/src/core/gitAnalyzer.ts +130 -0
  91. package/src/core/multiRepo/index.ts +2 -0
  92. package/src/core/multiRepo/multiRepoScanner.ts +177 -0
  93. package/src/core/rag/index.ts +2 -0
  94. package/src/core/rag/vectorIndex.ts +105 -0
  95. package/src/mcp/index.ts +1 -0
  96. package/src/mcp/server.ts +179 -0
  97. package/src/utils/fileUtils.ts +5 -0
  98. package/tests/entrypoints-languages.test.ts +373 -0
  99. package/tests/framework-detection.test.ts +296 -0
  100. package/tests/v1.3.8-integration.test.ts +361 -0
  101. package/BETA_EVALUATION_REPORT.md +0 -151
  102. package/ai-context/context/flows/App.json +0 -17
  103. package/ai-context/context/flows/DashboardPage.json +0 -14
  104. package/ai-context/context/flows/LoginPage.json +0 -14
  105. package/ai-context/context/flows/admin.json +0 -10
  106. package/ai-context/context/flows/androidresources.json +0 -11
  107. package/ai-context/context/flows/authController.json +0 -14
  108. package/ai-context/context/flows/entrypoints.json +0 -9
  109. package/ai-context/context/flows/fastapiAdapter.json +0 -14
  110. package/ai-context/context/flows/fastapiadapter.json +0 -11
  111. package/ai-context/context/flows/index.json +0 -19
  112. package/ai-context/context/flows/indexer.json +0 -9
  113. package/ai-context/context/flows/indexstate.json +0 -9
  114. package/ai-context/context/flows/init.json +0 -22
  115. package/ai-context/context/flows/main.json +0 -18
  116. package/ai-context/context/flows/mainactivity.json +0 -9
  117. package/ai-context/context/flows/models.json +0 -15
  118. package/ai-context/context/flows/posts.json +0 -15
  119. package/ai-context/context/flows/repoMapper.json +0 -20
  120. package/ai-context/context/flows/repomapper.json +0 -11
  121. package/ai-context/context/flows/serializers.json +0 -10
  122. package/ai-context-evaluation-report-1774223059505.md +0 -206
  123. package/dist/scripts/ai-context-evaluator.js +0 -367
  124. package/quick-evaluation-report-1774396002305.md +0 -64
  125. package/quick-evaluator.ts +0 -200
  126. package/scripts/ai-context-evaluator.ts +0 -440
@@ -27,7 +27,7 @@ export function analyzeArchitecture(
27
27
  const extensions = Array.from(groupByExtension(files).keys());
28
28
 
29
29
  // Detect patterns based on directory structure
30
- const patterns = detectPatterns(directories, extensions);
30
+ const patterns = detectPatterns(directories, extensions, files);
31
31
 
32
32
  // Identify layers
33
33
  const layers = detectLayers(directories);
@@ -49,7 +49,7 @@ export function analyzeArchitecture(
49
49
  /**
50
50
  * Detect architecture patterns from directory structure
51
51
  */
52
- function detectPatterns(directories: string[], extensions: string[]): string[] {
52
+ function detectPatterns(directories: string[], extensions: string[], files: FileInfo[]): string[] {
53
53
  const patterns: string[] = [];
54
54
  const dirs = directories.filter(d => d && d !== "root");
55
55
 
@@ -86,9 +86,38 @@ function detectPatterns(directories: string[], extensions: string[]): string[] {
86
86
  patterns.push("Monorepo");
87
87
  }
88
88
 
89
- // Microservices
90
- if (dirs.some(d => d.includes("services") || d.includes("api")) && dirs.length > 3) {
89
+ // Microservices - check for multiple service subdirectories
90
+ const serviceSubdirs = new Set<string>();
91
+ const apiSubdirs = new Set<string>();
92
+ const versionPattern = /^v?\d+$/i;
93
+
94
+ for (const file of files) {
95
+ const parts = file.relativePath.split("/");
96
+ // Check for services/*/ pattern (must be an actual subdirectory, not a file)
97
+ const servicesIndex = parts.indexOf("services");
98
+ if (servicesIndex >= 0 && servicesIndex < parts.length - 2) {
99
+ // parts.length - 2 ensures there's at least one directory after 'services' before the file
100
+ const subdir = parts[servicesIndex + 1];
101
+ if (!versionPattern.test(subdir)) {
102
+ serviceSubdirs.add(subdir);
103
+ }
104
+ }
105
+ // Check for api/*/ pattern at root level (must be an actual subdirectory)
106
+ if (parts[0] === "api" && parts.length > 2) {
107
+ // parts.length > 2 ensures there's at least one directory after 'api' before the file
108
+ const subdir = parts[1];
109
+ if (!versionPattern.test(subdir)) {
110
+ apiSubdirs.add(subdir);
111
+ }
112
+ }
113
+ }
114
+
115
+ if (serviceSubdirs.size >= 2 || apiSubdirs.size >= 2) {
91
116
  patterns.push("Microservices");
117
+ } else if (dirs.some(d => d === "services" || d === "api") ||
118
+ files.some(f => f.relativePath.includes("/services/") || f.relativePath.includes("/api/")) ||
119
+ serviceSubdirs.size === 1 || apiSubdirs.size === 1) {
120
+ patterns.push("API Server");
92
121
  }
93
122
 
94
123
  // Serverless / Functions
@@ -217,8 +246,48 @@ function inferModuleResponsibility(dir: string, extensions: string[]): string {
217
246
  if (name.includes("auth") || name.includes("security") || name.includes("login")) {
218
247
  return "Authentication and authorization";
219
248
  }
220
-
221
- return `Contains ${extensions.length} files`;
249
+
250
+ // Infer from file types
251
+ const uniqueExts = [...new Set(extensions)];
252
+ if (uniqueExts.includes("ts") || uniqueExts.includes("tsx") || uniqueExts.includes("js") || uniqueExts.includes("jsx")) {
253
+ if (uniqueExts.includes("css") || uniqueExts.includes("scss") || uniqueExts.includes("less")) {
254
+ return "Frontend components and styling";
255
+ }
256
+ return "JavaScript/TypeScript implementation";
257
+ }
258
+ if (uniqueExts.includes("py")) {
259
+ return "Python implementation";
260
+ }
261
+ if (uniqueExts.includes("java")) {
262
+ return "Java implementation";
263
+ }
264
+ if (uniqueExts.includes("go")) {
265
+ return "Go implementation";
266
+ }
267
+ if (uniqueExts.includes("rs")) {
268
+ return "Rust implementation";
269
+ }
270
+ if (uniqueExts.includes("php")) {
271
+ return "PHP implementation";
272
+ }
273
+ if (uniqueExts.includes("rb")) {
274
+ return "Ruby implementation";
275
+ }
276
+ if (uniqueExts.includes("swift")) {
277
+ return "Swift implementation";
278
+ }
279
+ if (uniqueExts.includes("kt")) {
280
+ return "Kotlin implementation";
281
+ }
282
+ if (uniqueExts.includes("cls") || uniqueExts.includes("trigger")) {
283
+ return "Apex/Salesforce implementation";
284
+ }
285
+
286
+ // Fallback based on file count
287
+ if (extensions.length === 1) {
288
+ return "Single file module";
289
+ }
290
+ return `Module with ${extensions.length} source files`;
222
291
  }
223
292
 
224
293
  /**
@@ -96,6 +96,33 @@ export function discoverEntrypoints(files: FileInfo[], rootDir: string): Entrypo
96
96
  } catch {}
97
97
  }
98
98
 
99
+ // Detect Go entrypoints
100
+ const goFiles = files.filter(f => f.extension === "go");
101
+ if (goFiles.length > 0) {
102
+ try {
103
+ const goEntrypoints = discoverGoEntrypoints(goFiles, rootDir);
104
+ entrypoints.push(...goEntrypoints);
105
+ } catch {}
106
+ }
107
+
108
+ // Detect Rust entrypoints
109
+ const rustFiles = files.filter(f => f.extension === "rs");
110
+ if (rustFiles.length > 0) {
111
+ try {
112
+ const rustEntrypoints = discoverRustEntrypoints(rustFiles, rootDir);
113
+ entrypoints.push(...rustEntrypoints);
114
+ } catch {}
115
+ }
116
+
117
+ // Detect PHP entrypoints
118
+ const phpFiles = files.filter(f => f.extension === "php");
119
+ if (phpFiles.length > 0) {
120
+ try {
121
+ const phpEntrypoints = discoverPHPEntrypoints(phpFiles, rootDir);
122
+ entrypoints.push(...phpEntrypoints);
123
+ } catch {}
124
+ }
125
+
99
126
  return entrypoints;
100
127
  }
101
128
 
@@ -276,6 +303,264 @@ function getScriptType(name: string): Entrypoint["type"] | null {
276
303
  return null;
277
304
  }
278
305
 
306
+ function discoverGoEntrypoints(goFiles: FileInfo[], rootDir: string): Entrypoint[] {
307
+ const entrypoints: Entrypoint[] = [];
308
+
309
+ for (const file of goFiles) {
310
+ try {
311
+ const content = readFile(path.join(rootDir, file.relativePath));
312
+ const fileName = file.name;
313
+
314
+ if (fileName === "main.go") {
315
+ const hasMain = content.match(/func\s+main\s*\(\s*\)/);
316
+ const packageMatch = content.match(/package\s+(\w+)/);
317
+ const packageName = packageMatch ? packageMatch[1] : "main";
318
+
319
+ const handlers: string[] = [];
320
+ const handlerMatches = content.matchAll(/http\.HandleFunc\s*\(\s*["']([^"']+)["']/g);
321
+ for (const match of handlerMatches) {
322
+ handlers.push(match[1]);
323
+ }
324
+
325
+ const portMatches = content.matchAll(/:\s*(\d{2,5})/g);
326
+ const ports: string[] = [];
327
+ for (const match of portMatches) {
328
+ ports.push(match[1]);
329
+ }
330
+
331
+ let description = `Go main package (${packageName})`;
332
+ if (handlers.length > 0) {
333
+ description += ` with HTTP handlers: ${handlers.join(", ")}`;
334
+ }
335
+ if (ports.length > 0) {
336
+ description += ` on port${ports.length > 1 ? "s" : ""} :${ports.join(", :")}`;
337
+ }
338
+
339
+ entrypoints.push({
340
+ name: "main.go",
341
+ path: file.relativePath,
342
+ type: hasMain ? "server" : "library",
343
+ description,
344
+ });
345
+ } else {
346
+ const structMatches = content.matchAll(/type\s+(\w+)\s+struct/g);
347
+ const structs: string[] = [];
348
+ for (const match of structMatches) {
349
+ structs.push(match[1]);
350
+ }
351
+
352
+ const methodMatches = content.matchAll(/func\s*\(?\s*\*?\s*(\w+)\s*\)?\s*(\w+)\s*\(/g);
353
+ const methods: string[] = [];
354
+ for (const match of methodMatches) {
355
+ methods.push(match[2]);
356
+ }
357
+
358
+ if (structs.length > 0 || methods.length > 0) {
359
+ let description = "Go module";
360
+ if (structs.length > 0) {
361
+ description += ` with structs: ${structs.slice(0, 3).join(", ")}`;
362
+ }
363
+ if (methods.length > 0) {
364
+ description += `, methods: ${methods.slice(0, 3).join(", ")}`;
365
+ }
366
+
367
+ entrypoints.push({
368
+ name: fileName,
369
+ path: file.relativePath,
370
+ type: "library",
371
+ description,
372
+ });
373
+ }
374
+ }
375
+ } catch {}
376
+ }
377
+
378
+ const goModPath = path.join(rootDir, "go.mod");
379
+ try {
380
+ const goMod = readFile(goModPath);
381
+ const moduleMatch = goMod.match(/module\s+(\S+)/);
382
+ if (moduleMatch) {
383
+ entrypoints.push({
384
+ name: "go.mod",
385
+ path: "go.mod",
386
+ type: "config",
387
+ description: `Go module: ${moduleMatch[1]}`,
388
+ });
389
+ }
390
+ } catch {}
391
+
392
+ return entrypoints;
393
+ }
394
+
395
+ function discoverRustEntrypoints(rustFiles: FileInfo[], rootDir: string): Entrypoint[] {
396
+ const entrypoints: Entrypoint[] = [];
397
+
398
+ for (const file of rustFiles) {
399
+ try {
400
+ const content = readFile(path.join(rootDir, file.relativePath));
401
+ const fileName = file.name;
402
+
403
+ if (fileName === "main.rs") {
404
+ const hasMain = content.match(/fn\s+main\s*\(\s*\)/);
405
+
406
+ const structMatches = content.matchAll(/struct\s+(\w+)/g);
407
+ const structs: string[] = [];
408
+ for (const match of structMatches) {
409
+ structs.push(match[1]);
410
+ }
411
+
412
+ const implMatches = content.matchAll(/impl\s+(?:\w+\s+for\s+)?(\w+)/g);
413
+ const implementations: string[] = [];
414
+ for (const match of implMatches) {
415
+ implementations.push(match[1]);
416
+ }
417
+
418
+ let description = "Rust main";
419
+ if (structs.length > 0) {
420
+ description += ` with structs: ${structs.slice(0, 3).join(", ")}`;
421
+ }
422
+ if (implementations.length > 0) {
423
+ description += `, implementations: ${implementations.slice(0, 3).join(", ")}`;
424
+ }
425
+
426
+ entrypoints.push({
427
+ name: "main.rs",
428
+ path: file.relativePath,
429
+ type: hasMain ? "cli" : "library",
430
+ description,
431
+ });
432
+ } else if (fileName === "lib.rs") {
433
+ const pubFnMatches = content.matchAll(/pub\s+fn\s+(\w+)/g);
434
+ const publicFns: string[] = [];
435
+ for (const match of pubFnMatches) {
436
+ publicFns.push(match[1]);
437
+ }
438
+
439
+ let description = "Rust library";
440
+ if (publicFns.length > 0) {
441
+ description += ` with public functions: ${publicFns.slice(0, 3).join(", ")}`;
442
+ }
443
+
444
+ entrypoints.push({
445
+ name: "lib.rs",
446
+ path: file.relativePath,
447
+ type: "library",
448
+ description,
449
+ });
450
+ }
451
+ } catch {}
452
+ }
453
+
454
+ const cargoPath = path.join(rootDir, "Cargo.toml");
455
+ try {
456
+ const cargoContent = readFile(cargoPath);
457
+ const nameMatch = cargoContent.match(/name\s*=\s*"([^"]+)"/);
458
+ const versionMatch = cargoContent.match(/version\s*=\s*"([^"]+)"/);
459
+
460
+ let description = "Rust project";
461
+ if (nameMatch) {
462
+ description += `: ${nameMatch[1]}`;
463
+ }
464
+ if (versionMatch) {
465
+ description += ` v${versionMatch[1]}`;
466
+ }
467
+
468
+ const binMatch = cargoContent.match(/\[\[bin\]\]/);
469
+ if (binMatch) {
470
+ description += " (has binaries)";
471
+ }
472
+
473
+ entrypoints.push({
474
+ name: "Cargo.toml",
475
+ path: "Cargo.toml",
476
+ type: "config",
477
+ description,
478
+ });
479
+ } catch {}
480
+
481
+ return entrypoints;
482
+ }
483
+
484
+ function discoverPHPEntrypoints(phpFiles: FileInfo[], rootDir: string): Entrypoint[] {
485
+ const entrypoints: Entrypoint[] = [];
486
+
487
+ const hasIndexPhp = phpFiles.some(f => f.name === "index.php");
488
+ const hasPublicIndex = phpFiles.some(f => f.relativePath.includes("public/index.php"));
489
+
490
+ if (hasIndexPhp) {
491
+ const indexFile = phpFiles.find(f => f.name === "index.php")!;
492
+ try {
493
+ const content = readFile(path.join(rootDir, indexFile.relativePath));
494
+
495
+ const classMatches = content.matchAll(/class\s+(\w+)/g);
496
+ const classes: string[] = [];
497
+ for (const match of classMatches) {
498
+ classes.push(match[1]);
499
+ }
500
+
501
+ const routes: string[] = [];
502
+
503
+ // Match $router->get('/path') or $app->post('/path') patterns
504
+ const httpMethodMatches = content.matchAll(/->\s*(?:get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']/g);
505
+ for (const match of httpMethodMatches) {
506
+ routes.push(match[1]);
507
+ }
508
+
509
+ // Match $router->add('METHOD', '/path') patterns (second argument is the path)
510
+ const addMethodMatches = content.matchAll(/->\s*add\s*\(\s*["'][^"']+["']\s*,\s*["']([^"']+)["']/g);
511
+ for (const match of addMethodMatches) {
512
+ routes.push(match[1]);
513
+ }
514
+
515
+ let description = "PHP entry point";
516
+ if (classes.length > 0) {
517
+ description += ` with classes: ${classes.slice(0, 3).join(", ")}`;
518
+ }
519
+ if (routes.length > 0) {
520
+ description += `, routes: ${routes.slice(0, 3).join(", ")}`;
521
+ }
522
+
523
+ entrypoints.push({
524
+ name: "index.php",
525
+ path: indexFile.relativePath,
526
+ type: hasPublicIndex ? "server" : "api",
527
+ description,
528
+ });
529
+ } catch {}
530
+ }
531
+
532
+ const composerPath = path.join(rootDir, "composer.json");
533
+ try {
534
+ const composer = readJsonFile(composerPath) as { name?: string; description?: string; require?: Record<string, string> };
535
+
536
+ let description = "PHP project";
537
+ if (composer.name) {
538
+ description += `: ${composer.name}`;
539
+ }
540
+ if (composer.description) {
541
+ description += ` - ${composer.description}`;
542
+ }
543
+
544
+ const hasLaravel = composer.require && (composer.require["laravel/framework"] || composer.require["illuminate/support"]);
545
+ const hasSymfony = composer.require && (composer.require["symfony/framework-bundle"] || composer.require["symfony/symfony"]);
546
+
547
+ if (hasLaravel) {
548
+ description += " (Laravel)";
549
+ } else if (hasSymfony) {
550
+ description += " (Symfony)";
551
+ }
552
+
553
+ entrypoints.push({
554
+ name: "composer.json",
555
+ path: "composer.json",
556
+ type: "config",
557
+ description,
558
+ });
559
+ } catch {}
560
+
561
+ return entrypoints;
562
+ }
563
+
279
564
  export function generateEntrypointsFile(entrypoints: Entrypoint[]): string {
280
565
  const grouped = new Map<string, Entrypoint[]>();
281
566
  for (const ep of entrypoints) {
@@ -533,8 +533,24 @@ function parseSwift(file: FileInfo, content: string, lines: string[], symbols: S
533
533
  * Parse Apex (Salesforce) files
534
534
  */
535
535
  function parseApex(file: FileInfo, content: string, lines: string[], symbols: Symbol[]): void {
536
+ // Track annotations across lines
537
+ let pendingAnnotations: string[] = [];
538
+
536
539
  for (let i = 0; i < lines.length; i++) {
537
540
  const line = lines[i].trim();
541
+
542
+ // Skip empty lines
543
+ if (!line) {
544
+ continue;
545
+ }
546
+
547
+ // Collect annotations (@AuraEnabled, @IsTest, etc.) - handles both single-line and multi-line
548
+ // Match patterns like @AuraEnabled, @AuraEnabled(cacheable=true), @IsTest
549
+ const annotationMatch = line.match(/^@(\w+)(?:\s*\([^)]*\))?\s*$/);
550
+ if (annotationMatch) {
551
+ pendingAnnotations.push(annotationMatch[1]);
552
+ continue;
553
+ }
538
554
 
539
555
  // Classes: public with sharing class ClassName, public class ClassName, etc.
540
556
  const classMatch = line.match(/^(?:\s*(?:public|private|global)(?:\s+(?:with|without|inherited)\s+sharing)?\s+)?class\s+(\w+)/);
@@ -547,6 +563,8 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
547
563
  line: i + 1,
548
564
  export: true,
549
565
  });
566
+ pendingAnnotations = [];
567
+ continue;
550
568
  }
551
569
 
552
570
  // Interfaces
@@ -560,11 +578,16 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
560
578
  line: i + 1,
561
579
  export: true,
562
580
  });
581
+ pendingAnnotations = [];
582
+ continue;
563
583
  }
564
584
 
565
585
  // Methods: public static ReturnType methodName(
566
586
  // Also handles @AuraEnabled public static ReturnType methodName(
567
- const methodMatch = line.match(/^(?:@\w+\s+)?(?:public|private|protected|global)\s+(?:static\s+)?(?:\w+)\s+(\w+)\s*\(/);
587
+ // Also handles @AuraEnabled(cacheable=true) on separate line
588
+ // Handles generic return types like List<Account>, Map<String, Object>
589
+ // Also handles webservice methods
590
+ const methodMatch = line.match(/^(?:@\w+(?:\s*\([^)]*\))?\s+)?(?:public|private|protected|global|webservice)\s+(?:static\s+)?(?:[\w<>,\s]+?)\s+(\w+)\s*\(/);
568
591
  if (methodMatch && !["if", "for", "while", "switch"].includes(methodMatch[1])) {
569
592
  symbols.push({
570
593
  id: generateSymbolId(file.relativePath, methodMatch[1]),
@@ -572,8 +595,28 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
572
595
  type: "function",
573
596
  file: file.relativePath,
574
597
  line: i + 1,
575
- export: line.includes("public") || line.includes("global"),
598
+ export: line.includes("public") || line.includes("global") || line.includes("webservice"),
576
599
  });
600
+ pendingAnnotations = [];
601
+ continue;
602
+ }
603
+
604
+ // Alternative: Method with annotations on previous lines
605
+ // Check if we have pending annotations and current line looks like a method
606
+ if (pendingAnnotations.length > 0) {
607
+ const methodWithAnnotationMatch = line.match(/^(?:public|private|protected|global|webservice)\s+(?:static\s+)?(?:[\w<>,\s]+?)\s+(\w+)\s*\(/);
608
+ if (methodWithAnnotationMatch && !["if", "for", "while", "switch"].includes(methodWithAnnotationMatch[1])) {
609
+ symbols.push({
610
+ id: generateSymbolId(file.relativePath, methodWithAnnotationMatch[1]),
611
+ name: methodWithAnnotationMatch[1],
612
+ type: "function",
613
+ file: file.relativePath,
614
+ line: i + 1,
615
+ export: line.includes("public") || line.includes("global") || line.includes("webservice"),
616
+ });
617
+ pendingAnnotations = [];
618
+ continue;
619
+ }
577
620
  }
578
621
 
579
622
  // Triggers: trigger TriggerName on ObjectName
@@ -587,6 +630,13 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
587
630
  line: i + 1,
588
631
  export: true,
589
632
  });
633
+ pendingAnnotations = [];
634
+ continue;
635
+ }
636
+
637
+ // Reset pending annotations if we encounter non-annotation, non-method line
638
+ if (!line.startsWith("@")) {
639
+ pendingAnnotations = [];
590
640
  }
591
641
  }
592
642
  }
@@ -134,6 +134,18 @@ function detectFrameworks(files: FileInfo[], fileNames: Set<string>, rootDir: st
134
134
  frameworks.push(...names);
135
135
  }
136
136
  }
137
+
138
+ // Check for @nestjs/* packages (NestJS framework)
139
+ const hasNestJs = Object.keys(deps).some(dep => dep.startsWith("@nestjs/"));
140
+ if (hasNestJs && !frameworks.includes("NestJS")) {
141
+ frameworks.push("NestJS");
142
+ }
143
+
144
+ // Check for Spring Boot in dependencies
145
+ const hasSpringBoot = Object.keys(deps).some(dep => dep.startsWith("@spring.io/") || dep.includes("spring-boot"));
146
+ if (hasSpringBoot && !frameworks.includes("Spring Boot")) {
147
+ frameworks.push("Spring Boot");
148
+ }
137
149
  } catch {}
138
150
 
139
151
  const frameworkIndicators: Record<string, string> = {
@@ -165,6 +177,84 @@ function detectFrameworks(files: FileInfo[], fileNames: Set<string>, rootDir: st
165
177
  }
166
178
  }
167
179
 
180
+ // Detect Spring Boot from pom.xml
181
+ try {
182
+ const pomPath = path.join(rootDir, "pom.xml");
183
+ if (fs.existsSync(pomPath)) {
184
+ const pomContent = readFile(pomPath);
185
+ if (pomContent.includes("spring-boot") && !frameworks.includes("Spring Boot")) {
186
+ frameworks.push("Spring Boot");
187
+ }
188
+ if (pomContent.includes("spring-boot-starter-parent") && !frameworks.includes("Spring Boot")) {
189
+ frameworks.push("Spring Boot");
190
+ }
191
+ }
192
+ } catch {}
193
+
194
+ // Detect Spring Boot from build.gradle
195
+ try {
196
+ const gradlePath = path.join(rootDir, "build.gradle");
197
+ const gradleKtsPath = path.join(rootDir, "build.gradle.kts");
198
+ let gradleContent = "";
199
+ if (fs.existsSync(gradlePath)) {
200
+ gradleContent = readFile(gradlePath);
201
+ } else if (fs.existsSync(gradleKtsPath)) {
202
+ gradleContent = readFile(gradleKtsPath);
203
+ }
204
+ if (gradleContent.includes("spring-boot") && !frameworks.includes("Spring Boot")) {
205
+ frameworks.push("Spring Boot");
206
+ }
207
+ if (gradleContent.includes("org.springframework.boot") && !frameworks.includes("Spring Boot")) {
208
+ frameworks.push("Spring Boot");
209
+ }
210
+ } catch {}
211
+
212
+ // Detect Python frameworks from requirements.txt, Pipfile, pyproject.toml
213
+ try {
214
+ const pythonFrameworkMap: Record<string, string[]> = {
215
+ "django": ["Django"],
216
+ "flask": ["Flask"],
217
+ "fastapi": ["FastAPI"],
218
+ "tornado": ["Tornado"],
219
+ "pyramid": ["Pyramid"],
220
+ "bottle": ["Bottle"],
221
+ "cherrypy": ["CherryPy"],
222
+ };
223
+
224
+ // Check requirements.txt
225
+ const reqPath = path.join(rootDir, "requirements.txt");
226
+ if (fs.existsSync(reqPath)) {
227
+ const reqContent = readFile(reqPath);
228
+ for (const [dep, names] of Object.entries(pythonFrameworkMap)) {
229
+ if (reqContent.toLowerCase().includes(dep.toLowerCase()) && !frameworks.some(f => names.includes(f))) {
230
+ frameworks.push(...names);
231
+ }
232
+ }
233
+ }
234
+
235
+ // Check Pipfile
236
+ const pipfilePath = path.join(rootDir, "Pipfile");
237
+ if (fs.existsSync(pipfilePath)) {
238
+ const pipfileContent = readFile(pipfilePath);
239
+ for (const [dep, names] of Object.entries(pythonFrameworkMap)) {
240
+ if (pipfileContent.toLowerCase().includes(dep.toLowerCase()) && !frameworks.some(f => names.includes(f))) {
241
+ frameworks.push(...names);
242
+ }
243
+ }
244
+ }
245
+
246
+ // Check pyproject.toml
247
+ const pyprojectPath = path.join(rootDir, "pyproject.toml");
248
+ if (fs.existsSync(pyprojectPath)) {
249
+ const pyprojectContent = readFile(pyprojectPath);
250
+ for (const [dep, names] of Object.entries(pythonFrameworkMap)) {
251
+ if (pyprojectContent.toLowerCase().includes(dep.toLowerCase()) && !frameworks.some(f => names.includes(f))) {
252
+ frameworks.push(...names);
253
+ }
254
+ }
255
+ }
256
+ } catch {}
257
+
168
258
  // Detect SwiftUI from Swift files
169
259
  const swiftFiles = files.filter(f => f.extension === "swift");
170
260
  for (const swiftFile of swiftFiles) {