ai-first-cli 1.3.5 → 1.3.6

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 (51) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.es.md +55 -0
  3. package/README.md +40 -15
  4. package/ai/graph/knowledge-graph.json +1 -1
  5. package/dist/analyzers/architecture.d.ts.map +1 -1
  6. package/dist/analyzers/architecture.js +72 -5
  7. package/dist/analyzers/architecture.js.map +1 -1
  8. package/dist/analyzers/entrypoints.d.ts.map +1 -1
  9. package/dist/analyzers/entrypoints.js +253 -0
  10. package/dist/analyzers/entrypoints.js.map +1 -1
  11. package/dist/analyzers/symbols.d.ts.map +1 -1
  12. package/dist/analyzers/symbols.js +47 -2
  13. package/dist/analyzers/symbols.js.map +1 -1
  14. package/dist/analyzers/techStack.d.ts.map +1 -1
  15. package/dist/analyzers/techStack.js +43 -0
  16. package/dist/analyzers/techStack.js.map +1 -1
  17. package/dist/utils/fileUtils.d.ts.map +1 -1
  18. package/dist/utils/fileUtils.js +5 -0
  19. package/dist/utils/fileUtils.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/analyzers/architecture.ts +75 -6
  22. package/src/analyzers/entrypoints.ts +285 -0
  23. package/src/analyzers/symbols.ts +52 -2
  24. package/src/analyzers/techStack.ts +44 -0
  25. package/src/utils/fileUtils.ts +5 -0
  26. package/tests/entrypoints-languages.test.ts +373 -0
  27. package/tests/framework-detection.test.ts +296 -0
  28. package/BETA_EVALUATION_REPORT.md +0 -151
  29. package/ai-context/context/flows/App.json +0 -17
  30. package/ai-context/context/flows/DashboardPage.json +0 -14
  31. package/ai-context/context/flows/LoginPage.json +0 -14
  32. package/ai-context/context/flows/admin.json +0 -10
  33. package/ai-context/context/flows/androidresources.json +0 -11
  34. package/ai-context/context/flows/authController.json +0 -14
  35. package/ai-context/context/flows/entrypoints.json +0 -9
  36. package/ai-context/context/flows/fastapiAdapter.json +0 -14
  37. package/ai-context/context/flows/fastapiadapter.json +0 -11
  38. package/ai-context/context/flows/index.json +0 -19
  39. package/ai-context/context/flows/indexer.json +0 -9
  40. package/ai-context/context/flows/indexstate.json +0 -9
  41. package/ai-context/context/flows/init.json +0 -22
  42. package/ai-context/context/flows/main.json +0 -18
  43. package/ai-context/context/flows/mainactivity.json +0 -9
  44. package/ai-context/context/flows/models.json +0 -15
  45. package/ai-context/context/flows/posts.json +0 -15
  46. package/ai-context/context/flows/repoMapper.json +0 -20
  47. package/ai-context/context/flows/repomapper.json +0 -11
  48. package/ai-context/context/flows/serializers.json +0 -10
  49. package/dist/scripts/ai-context-evaluator.js +0 -367
  50. package/quick-evaluation-report-1774396002305.md +0 -64
  51. package/quick-evaluator.ts +0 -200
@@ -0,0 +1,373 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { discoverEntrypoints, Entrypoint } from "../src/analyzers/entrypoints.js";
3
+ import { FileInfo } from "../src/core/repoScanner.js";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+
8
+ function createTempProjectDir(files: Record<string, string>): string {
9
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ai-first-entrypoints-test-"));
10
+ for (const [filePath, content] of Object.entries(files)) {
11
+ const fullPath = path.join(tempDir, filePath);
12
+ const dir = path.dirname(fullPath);
13
+ if (!fs.existsSync(dir)) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+ fs.writeFileSync(fullPath, content);
17
+ }
18
+ return tempDir;
19
+ }
20
+
21
+ function createFileInfo(relativePath: string, name: string, extension: string): FileInfo {
22
+ return {
23
+ path: path.join("/tmp", relativePath),
24
+ relativePath,
25
+ name,
26
+ extension,
27
+ };
28
+ }
29
+
30
+ describe("Entrypoints - Go Language", () => {
31
+ it("should detect main.go with func main()", () => {
32
+ const tempDir = createTempProjectDir({
33
+ "main.go": `package main
34
+
35
+ import "net/http"
36
+
37
+ func main() {
38
+ http.ListenAndServe(":8080", nil)
39
+ }`,
40
+ });
41
+
42
+ const files: FileInfo[] = [createFileInfo("main.go", "main.go", "go")];
43
+ const entrypoints = discoverEntrypoints(files, tempDir);
44
+
45
+ const mainEntry = entrypoints.find(ep => ep.name === "main.go");
46
+ expect(mainEntry).toBeDefined();
47
+ expect(mainEntry?.type).toBe("server");
48
+ expect(mainEntry?.path).toBe("main.go");
49
+
50
+ fs.rmSync(tempDir, { recursive: true, force: true });
51
+ });
52
+
53
+ it("should detect HTTP handlers in Go", () => {
54
+ const tempDir = createTempProjectDir({
55
+ "main.go": `package main
56
+
57
+ import "net/http"
58
+
59
+ func main() {
60
+ http.HandleFunc("/users", handleUsers)
61
+ http.HandleFunc("/posts", handlePosts)
62
+ http.ListenAndServe(":3000", nil)
63
+ }
64
+
65
+ func handleUsers(w http.ResponseWriter, r *http.Request) {}
66
+ func handlePosts(w http.ResponseWriter, r *http.Request) {}`,
67
+ });
68
+
69
+ const files: FileInfo[] = [createFileInfo("main.go", "main.go", "go")];
70
+ const entrypoints = discoverEntrypoints(files, tempDir);
71
+
72
+ const mainEntry = entrypoints.find(ep => ep.name === "main.go");
73
+ expect(mainEntry).toBeDefined();
74
+ expect(mainEntry?.description).toContain("/users");
75
+ expect(mainEntry?.description).toContain("/posts");
76
+ expect(mainEntry?.description).toContain(":3000");
77
+
78
+ fs.rmSync(tempDir, { recursive: true, force: true });
79
+ });
80
+
81
+ it("should detect go.mod file", () => {
82
+ const tempDir = createTempProjectDir({
83
+ "main.go": "package main\n\nfunc main() {}",
84
+ "go.mod": "module github.com/example/app\n\ngo 1.21",
85
+ });
86
+
87
+ const files: FileInfo[] = [
88
+ createFileInfo("main.go", "main.go", "go"),
89
+ createFileInfo("go.mod", "go.mod", ""),
90
+ ];
91
+ const entrypoints = discoverEntrypoints(files, tempDir);
92
+
93
+ const modEntry = entrypoints.find(ep => ep.name === "go.mod");
94
+ expect(modEntry).toBeDefined();
95
+ expect(modEntry?.type).toBe("config");
96
+ expect(modEntry?.description).toContain("github.com/example/app");
97
+
98
+ fs.rmSync(tempDir, { recursive: true, force: true });
99
+ });
100
+
101
+ it("should detect structs in Go modules", () => {
102
+ const tempDir = createTempProjectDir({
103
+ "models.go": `package models
104
+
105
+ type User struct {
106
+ ID int
107
+ Name string
108
+ }
109
+
110
+ type Post struct {
111
+ Title string
112
+ Body string
113
+ }`,
114
+ });
115
+
116
+ const files: FileInfo[] = [createFileInfo("models.go", "models.go", "go")];
117
+ const entrypoints = discoverEntrypoints(files, tempDir);
118
+
119
+ const modelEntry = entrypoints.find(ep => ep.name === "models.go");
120
+ expect(modelEntry).toBeDefined();
121
+ expect(modelEntry?.type).toBe("library");
122
+ expect(modelEntry?.description).toContain("User");
123
+ expect(modelEntry?.description).toContain("Post");
124
+
125
+ fs.rmSync(tempDir, { recursive: true, force: true });
126
+ });
127
+ });
128
+
129
+ describe("Entrypoints - Rust Language", () => {
130
+ it("should detect main.rs with fn main()", () => {
131
+ const tempDir = createTempProjectDir({
132
+ "src/main.rs": `fn main() {
133
+ println!("Hello, world!");
134
+ }`,
135
+ });
136
+
137
+ const files: FileInfo[] = [createFileInfo("src/main.rs", "main.rs", "rs")];
138
+ const entrypoints = discoverEntrypoints(files, tempDir);
139
+
140
+ const mainEntry = entrypoints.find(ep => ep.name === "main.rs");
141
+ expect(mainEntry).toBeDefined();
142
+ expect(mainEntry?.type).toBe("cli");
143
+ expect(mainEntry?.path).toBe("src/main.rs");
144
+
145
+ fs.rmSync(tempDir, { recursive: true, force: true });
146
+ });
147
+
148
+ it("should detect structs and implementations in main.rs", () => {
149
+ const tempDir = createTempProjectDir({
150
+ "src/main.rs": `struct Config {
151
+ name: String,
152
+ }
153
+
154
+ impl Config {
155
+ fn new() -> Self {
156
+ Config { name: "app".to_string() }
157
+ }
158
+ }
159
+
160
+ fn main() {}`,
161
+ });
162
+
163
+ const files: FileInfo[] = [createFileInfo("src/main.rs", "main.rs", "rs")];
164
+ const entrypoints = discoverEntrypoints(files, tempDir);
165
+
166
+ const mainEntry = entrypoints.find(ep => ep.name === "main.rs");
167
+ expect(mainEntry).toBeDefined();
168
+ expect(mainEntry?.description).toContain("Config");
169
+ expect(mainEntry?.description).toContain("implementations");
170
+
171
+ fs.rmSync(tempDir, { recursive: true, force: true });
172
+ });
173
+
174
+ it("should detect lib.rs as library", () => {
175
+ const tempDir = createTempProjectDir({
176
+ "src/lib.rs": `pub fn public_function() {}
177
+
178
+ pub fn another_public_fn() {}`,
179
+ });
180
+
181
+ const files: FileInfo[] = [createFileInfo("src/lib.rs", "lib.rs", "rs")];
182
+ const entrypoints = discoverEntrypoints(files, tempDir);
183
+
184
+ const libEntry = entrypoints.find(ep => ep.name === "lib.rs");
185
+ expect(libEntry).toBeDefined();
186
+ expect(libEntry?.type).toBe("library");
187
+ expect(libEntry?.description).toContain("public_function");
188
+
189
+ fs.rmSync(tempDir, { recursive: true, force: true });
190
+ });
191
+
192
+ it("should detect Cargo.toml with project info", () => {
193
+ const tempDir = createTempProjectDir({
194
+ "src/main.rs": "fn main() {}",
195
+ "Cargo.toml": `[package]
196
+ name = "my-app"
197
+ version = "1.0.0"
198
+ edition = "2021"`,
199
+ });
200
+
201
+ const files: FileInfo[] = [
202
+ createFileInfo("src/main.rs", "main.rs", "rs"),
203
+ createFileInfo("Cargo.toml", "Cargo.toml", ""),
204
+ ];
205
+ const entrypoints = discoverEntrypoints(files, tempDir);
206
+
207
+ const cargoEntry = entrypoints.find(ep => ep.name === "Cargo.toml");
208
+ expect(cargoEntry).toBeDefined();
209
+ expect(cargoEntry?.type).toBe("config");
210
+ expect(cargoEntry?.description).toContain("my-app");
211
+ expect(cargoEntry?.description).toContain("v1.0.0");
212
+
213
+ fs.rmSync(tempDir, { recursive: true, force: true });
214
+ });
215
+ });
216
+
217
+ describe("Entrypoints - PHP Language", () => {
218
+ it("should detect index.php", () => {
219
+ const tempDir = createTempProjectDir({
220
+ "index.php": `<?php
221
+ echo "Hello, World!";`,
222
+ });
223
+
224
+ const files: FileInfo[] = [createFileInfo("index.php", "index.php", "php")];
225
+ const entrypoints = discoverEntrypoints(files, tempDir);
226
+
227
+ const indexEntry = entrypoints.find(ep => ep.name === "index.php");
228
+ expect(indexEntry).toBeDefined();
229
+ expect(indexEntry?.type).toBe("api");
230
+ expect(indexEntry?.path).toBe("index.php");
231
+
232
+ fs.rmSync(tempDir, { recursive: true, force: true });
233
+ });
234
+
235
+ it("should detect public/index.php as server", () => {
236
+ const tempDir = createTempProjectDir({
237
+ "public/index.php": `<?php
238
+ require_once __DIR__ . '/../vendor/autoload.php';`,
239
+ });
240
+
241
+ const files: FileInfo[] = [createFileInfo("public/index.php", "index.php", "php")];
242
+ const entrypoints = discoverEntrypoints(files, tempDir);
243
+
244
+ const indexEntry = entrypoints.find(ep => ep.name === "index.php");
245
+ expect(indexEntry).toBeDefined();
246
+ expect(indexEntry?.type).toBe("server");
247
+
248
+ fs.rmSync(tempDir, { recursive: true, force: true });
249
+ });
250
+
251
+ it("should detect PHP classes and routes", () => {
252
+ const tempDir = createTempProjectDir({
253
+ "index.php": `<?php
254
+
255
+ class UserController {
256
+ public function index() {}
257
+ }
258
+
259
+ class Router {
260
+ public function add() {}
261
+ }
262
+
263
+ $router = new Router();
264
+ $router->add('GET', '/users', function() {});`,
265
+ });
266
+
267
+ const files: FileInfo[] = [createFileInfo("index.php", "index.php", "php")];
268
+ const entrypoints = discoverEntrypoints(files, tempDir);
269
+
270
+ const indexEntry = entrypoints.find(ep => ep.name === "index.php");
271
+ expect(indexEntry).toBeDefined();
272
+ expect(indexEntry?.description).toContain("UserController");
273
+ expect(indexEntry?.description).toContain("Router");
274
+ expect(indexEntry?.description).toContain("/users");
275
+
276
+ fs.rmSync(tempDir, { recursive: true, force: true });
277
+ });
278
+
279
+ it("should detect composer.json with Laravel", () => {
280
+ const tempDir = createTempProjectDir({
281
+ "index.php": "<?php echo 'Hello';",
282
+ "composer.json": JSON.stringify({
283
+ name: "example/laravel-app",
284
+ description: "My Laravel application",
285
+ require: {
286
+ "laravel/framework": "^10.0",
287
+ },
288
+ }),
289
+ });
290
+
291
+ const files: FileInfo[] = [
292
+ createFileInfo("index.php", "index.php", "php"),
293
+ createFileInfo("composer.json", "composer.json", ""),
294
+ ];
295
+ const entrypoints = discoverEntrypoints(files, tempDir);
296
+
297
+ const composerEntry = entrypoints.find(ep => ep.name === "composer.json");
298
+ expect(composerEntry).toBeDefined();
299
+ expect(composerEntry?.type).toBe("config");
300
+ expect(composerEntry?.description).toContain("example/laravel-app");
301
+ expect(composerEntry?.description).toContain("Laravel");
302
+
303
+ fs.rmSync(tempDir, { recursive: true, force: true });
304
+ });
305
+
306
+ it("should detect composer.json with Symfony", () => {
307
+ const tempDir = createTempProjectDir({
308
+ "index.php": "<?php echo 'Hello';",
309
+ "composer.json": JSON.stringify({
310
+ name: "example/symfony-app",
311
+ require: {
312
+ "symfony/framework-bundle": "^6.0",
313
+ },
314
+ }),
315
+ });
316
+
317
+ const files: FileInfo[] = [
318
+ createFileInfo("index.php", "index.php", "php"),
319
+ createFileInfo("composer.json", "composer.json", ""),
320
+ ];
321
+ const entrypoints = discoverEntrypoints(files, tempDir);
322
+
323
+ const composerEntry = entrypoints.find(ep => ep.name === "composer.json");
324
+ expect(composerEntry).toBeDefined();
325
+ expect(composerEntry?.description).toContain("Symfony");
326
+
327
+ fs.rmSync(tempDir, { recursive: true, force: true });
328
+ });
329
+ });
330
+
331
+ describe("Entrypoints - Integration with Test Projects", () => {
332
+ it("should detect Go microservice entrypoints", () => {
333
+ const testProjectPath = path.join(process.cwd(), "test-projects/go-microservice");
334
+
335
+ const files: FileInfo[] = [
336
+ createFileInfo("main.go", "main.go", "go"),
337
+ ];
338
+
339
+ const entrypoints = discoverEntrypoints(files, testProjectPath);
340
+
341
+ const mainEntry = entrypoints.find(ep => ep.name === "main.go");
342
+ expect(mainEntry).toBeDefined();
343
+ expect(mainEntry?.type).toBe("server");
344
+ });
345
+
346
+ it("should detect Rust CLI entrypoints", () => {
347
+ const testProjectPath = path.join(process.cwd(), "test-projects/rust-cli");
348
+
349
+ const files: FileInfo[] = [
350
+ createFileInfo("src/main.rs", "main.rs", "rs"),
351
+ ];
352
+
353
+ const entrypoints = discoverEntrypoints(files, testProjectPath);
354
+
355
+ const mainEntry = entrypoints.find(ep => ep.name === "main.rs");
356
+ expect(mainEntry).toBeDefined();
357
+ expect(mainEntry?.type).toBe("cli");
358
+ });
359
+
360
+ it("should detect PHP vanilla entrypoints", () => {
361
+ const testProjectPath = path.join(process.cwd(), "test-projects/php-vanilla");
362
+
363
+ const files: FileInfo[] = [
364
+ createFileInfo("index.php", "index.php", "php"),
365
+ ];
366
+
367
+ const entrypoints = discoverEntrypoints(files, testProjectPath);
368
+
369
+ const indexEntry = entrypoints.find(ep => ep.name === "index.php");
370
+ expect(indexEntry).toBeDefined();
371
+ expect(indexEntry?.type).toBe("api");
372
+ });
373
+ });
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { detectTechStack } from "../src/analyzers/techStack.js";
3
+ import { analyzeArchitecture } from "../src/analyzers/architecture.js";
4
+ import { FileInfo } from "../src/core/repoScanner.js";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+
9
+ function createTempProjectDir(files: Record<string, string>): string {
10
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ai-first-framework-test-"));
11
+ for (const [filePath, content] of Object.entries(files)) {
12
+ const fullPath = path.join(tempDir, filePath);
13
+ const dir = path.dirname(fullPath);
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ fs.writeFileSync(fullPath, content);
18
+ }
19
+ return tempDir;
20
+ }
21
+
22
+ function createFileInfo(relativePath: string, name: string, extension: string): FileInfo {
23
+ return {
24
+ path: path.join("/tmp", relativePath),
25
+ relativePath,
26
+ name,
27
+ extension,
28
+ };
29
+ }
30
+
31
+ describe("Framework Detection - NestJS", () => {
32
+ it("should detect NestJS from @nestjs/common in dependencies", () => {
33
+ const tempDir = createTempProjectDir({
34
+ "package.json": JSON.stringify({
35
+ name: "nestjs-app",
36
+ dependencies: {
37
+ "@nestjs/common": "^10.0.0",
38
+ "@nestjs/core": "^10.0.0",
39
+ "@nestjs/platform-express": "^10.0.0",
40
+ },
41
+ }),
42
+ "src/main.ts": "// main file",
43
+ });
44
+
45
+ const files: FileInfo[] = [
46
+ createFileInfo("package.json", "package.json", ""),
47
+ createFileInfo("src/main.ts", "main.ts", "ts"),
48
+ ];
49
+
50
+ const techStack = detectTechStack(files, tempDir);
51
+
52
+ expect(techStack.frameworks).toContain("NestJS");
53
+
54
+ fs.rmSync(tempDir, { recursive: true, force: true });
55
+ });
56
+
57
+ it("should detect NestJS from @nestjs/* packages in devDependencies", () => {
58
+ const tempDir = createTempProjectDir({
59
+ "package.json": JSON.stringify({
60
+ name: "nestjs-app",
61
+ devDependencies: {
62
+ "@nestjs/testing": "^10.0.0",
63
+ "@nestjs/cli": "^10.0.0",
64
+ },
65
+ }),
66
+ });
67
+
68
+ const files: FileInfo[] = [createFileInfo("package.json", "package.json", "")];
69
+
70
+ const techStack = detectTechStack(files, tempDir);
71
+
72
+ expect(techStack.frameworks).toContain("NestJS");
73
+
74
+ fs.rmSync(tempDir, { recursive: true, force: true });
75
+ });
76
+
77
+ it("should not detect NestJS when only 'nest' is present without @nestjs/*", () => {
78
+ const tempDir = createTempProjectDir({
79
+ "package.json": JSON.stringify({
80
+ name: "other-app",
81
+ dependencies: {
82
+ "nestjs-stuff": "1.0.0",
83
+ },
84
+ }),
85
+ });
86
+
87
+ const files: FileInfo[] = [createFileInfo("package.json", "package.json", "")];
88
+
89
+ const techStack = detectTechStack(files, tempDir);
90
+
91
+ // Should not detect NestJS from "nestjs-stuff" package
92
+ expect(techStack.frameworks).not.toContain("NestJS");
93
+
94
+ fs.rmSync(tempDir, { recursive: true, force: true });
95
+ });
96
+ });
97
+
98
+ describe("Framework Detection - Spring Boot", () => {
99
+ it("should detect Spring Boot from pom.xml", () => {
100
+ const tempDir = createTempProjectDir({
101
+ "pom.xml": `<?xml version="1.0" encoding="UTF-8"?>
102
+ <project>
103
+ <parent>
104
+ <groupId>org.springframework.boot</groupId>
105
+ <artifactId>spring-boot-starter-parent</artifactId>
106
+ <version>3.0.0</version>
107
+ </parent>
108
+ <dependencies>
109
+ <dependency>
110
+ <groupId>org.springframework.boot</groupId>
111
+ <artifactId>spring-boot-starter-web</artifactId>
112
+ </dependency>
113
+ </dependencies>
114
+ </project>`,
115
+ "src/main/java/com/example/Application.java": "package com.example;",
116
+ });
117
+
118
+ const files: FileInfo[] = [
119
+ createFileInfo("pom.xml", "pom.xml", ""),
120
+ createFileInfo("src/main/java/com/example/Application.java", "Application.java", "java"),
121
+ ];
122
+
123
+ const techStack = detectTechStack(files, tempDir);
124
+
125
+ expect(techStack.frameworks).toContain("Spring Boot");
126
+
127
+ fs.rmSync(tempDir, { recursive: true, force: true });
128
+ });
129
+
130
+ it("should detect Spring Boot from build.gradle", () => {
131
+ const tempDir = createTempProjectDir({
132
+ "build.gradle": `plugins {
133
+ id 'java'
134
+ id 'org.springframework.boot' version '3.0.0'
135
+ }
136
+
137
+ dependencies {
138
+ implementation 'org.springframework.boot:spring-boot-starter-web'
139
+ }`,
140
+ });
141
+
142
+ const files: FileInfo[] = [
143
+ createFileInfo("build.gradle", "build.gradle", ""),
144
+ ];
145
+
146
+ const techStack = detectTechStack(files, tempDir);
147
+
148
+ expect(techStack.frameworks).toContain("Spring Boot");
149
+
150
+ fs.rmSync(tempDir, { recursive: true, force: true });
151
+ });
152
+
153
+ it("should detect Spring Boot from build.gradle.kts", () => {
154
+ const tempDir = createTempProjectDir({
155
+ "build.gradle.kts": `plugins {
156
+ java
157
+ id("org.springframework.boot") version "3.0.0"
158
+ }
159
+
160
+ dependencies {
161
+ implementation("org.springframework.boot:spring-boot-starter-web")
162
+ }`,
163
+ });
164
+
165
+ const files: FileInfo[] = [
166
+ createFileInfo("build.gradle.kts", "build.gradle.kts", ""),
167
+ ];
168
+
169
+ const techStack = detectTechStack(files, tempDir);
170
+
171
+ expect(techStack.frameworks).toContain("Spring Boot");
172
+
173
+ fs.rmSync(tempDir, { recursive: true, force: true });
174
+ });
175
+
176
+ it("should detect Spring Boot from spring-boot in pom.xml", () => {
177
+ const tempDir = createTempProjectDir({
178
+ "pom.xml": `<?xml version="1.0" encoding="UTF-8"?>
179
+ <project>
180
+ <dependencies>
181
+ <dependency>
182
+ <groupId>org.springframework.boot</groupId>
183
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
184
+ </dependency>
185
+ </dependencies>
186
+ </project>`,
187
+ });
188
+
189
+ const files: FileInfo[] = [createFileInfo("pom.xml", "pom.xml", "")];
190
+
191
+ const techStack = detectTechStack(files, tempDir);
192
+
193
+ expect(techStack.frameworks).toContain("Spring Boot");
194
+
195
+ fs.rmSync(tempDir, { recursive: true, force: true });
196
+ });
197
+ });
198
+
199
+ describe("Architecture Detection - Microservices vs API Server", () => {
200
+ it("should detect API Server for single services directory", () => {
201
+ const files: FileInfo[] = [
202
+ createFileInfo("src/controllers/user.js", "user.js", "js"),
203
+ createFileInfo("src/services/userService.js", "userService.js", "js"),
204
+ createFileInfo("package.json", "package.json", ""),
205
+ ];
206
+
207
+ const architecture = analyzeArchitecture(files, "/tmp");
208
+
209
+ // Should detect API Server, not Microservices
210
+ expect(architecture.description).not.toContain("Microservices");
211
+ expect(architecture.description).toContain("API Server");
212
+ });
213
+
214
+ it("should detect Microservices for multiple service directories", () => {
215
+ const files: FileInfo[] = [
216
+ createFileInfo("services/user/src/index.js", "index.js", "js"),
217
+ createFileInfo("services/order/src/index.js", "index.js", "js"),
218
+ createFileInfo("services/payment/src/index.js", "index.js", "js"),
219
+ createFileInfo("package.json", "package.json", ""),
220
+ ];
221
+
222
+ const architecture = analyzeArchitecture(files, "/tmp");
223
+
224
+ // Should detect Microservices when there are multiple service directories
225
+ expect(architecture.description).toContain("Microservices");
226
+ });
227
+
228
+ it("should detect API Server for single api directory", () => {
229
+ const files: FileInfo[] = [
230
+ createFileInfo("api/routes.js", "routes.js", "js"),
231
+ createFileInfo("api/controllers.js", "controllers.js", "js"),
232
+ ];
233
+
234
+ const architecture = analyzeArchitecture(files, "/tmp");
235
+
236
+ // Should detect API Server, not Microservices
237
+ expect(architecture.description).not.toContain("Microservices");
238
+ expect(architecture.description).toContain("API Server");
239
+ });
240
+
241
+ it("should detect API Server for nested api directories", () => {
242
+ const files: FileInfo[] = [
243
+ createFileInfo("api/v1/routes.js", "routes.js", "js"),
244
+ createFileInfo("api/v2/routes.js", "routes.js", "js"),
245
+ createFileInfo("api/v3/routes.js", "routes.js", "js"),
246
+ ];
247
+
248
+ const architecture = analyzeArchitecture(files, "/tmp");
249
+
250
+ // Should detect API Server for nested api directories, not Microservices
251
+ expect(architecture.description).not.toContain("Microservices");
252
+ expect(architecture.description).toContain("API Server");
253
+ });
254
+ });
255
+
256
+ describe("Framework Detection - Integration Tests", () => {
257
+ it("should detect NestJS in nestjs-backend test project", () => {
258
+ const testProjectPath = path.join(process.cwd(), "test-projects/nestjs-backend");
259
+
260
+ const files: FileInfo[] = [
261
+ createFileInfo("package.json", "package.json", ""),
262
+ createFileInfo("src/main.ts", "main.ts", "ts"),
263
+ ];
264
+
265
+ const techStack = detectTechStack(files, testProjectPath);
266
+
267
+ expect(techStack.frameworks).toContain("NestJS");
268
+ });
269
+
270
+ it("should detect Spring Boot in spring-boot-app test project", () => {
271
+ const testProjectPath = path.join(process.cwd(), "test-projects/spring-boot-app");
272
+
273
+ const files: FileInfo[] = [
274
+ createFileInfo("pom.xml", "pom.xml", ""),
275
+ ];
276
+
277
+ const techStack = detectTechStack(files, testProjectPath);
278
+
279
+ expect(techStack.frameworks).toContain("Spring Boot");
280
+ });
281
+
282
+ it("should detect API Server for express-api test project", () => {
283
+ const testProjectPath = path.join(process.cwd(), "test-projects/express-api");
284
+
285
+ const files: FileInfo[] = [
286
+ createFileInfo("src/routes/index.js", "index.js", "js"),
287
+ createFileInfo("src/services/dataService.js", "dataService.js", "js"),
288
+ ];
289
+
290
+ const architecture = analyzeArchitecture(files, testProjectPath);
291
+
292
+ // Should detect API Server, not Microservices
293
+ expect(architecture.description).not.toContain("Microservices");
294
+ expect(architecture.description).toContain("API Server");
295
+ });
296
+ });