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
@@ -0,0 +1,105 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export interface VectorDocument {
5
+ id: string;
6
+ content: string;
7
+ embedding: number[];
8
+ metadata: {
9
+ filePath: string;
10
+ language?: string;
11
+ type?: 'function' | 'class' | 'interface' | 'variable';
12
+ };
13
+ }
14
+
15
+ export interface SearchResult {
16
+ document: VectorDocument;
17
+ score: number;
18
+ }
19
+
20
+ export class VectorIndex {
21
+ private documents: Map<string, VectorDocument> = new Map();
22
+ private indexPath: string;
23
+
24
+ constructor(indexPath: string) {
25
+ this.indexPath = indexPath;
26
+ this.load();
27
+ }
28
+
29
+ private load(): void {
30
+ if (fs.existsSync(this.indexPath)) {
31
+ try {
32
+ const data = JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
33
+ this.documents = new Map(Object.entries(data));
34
+ } catch {
35
+ this.documents = new Map();
36
+ }
37
+ }
38
+ }
39
+
40
+ save(): void {
41
+ const data = Object.fromEntries(this.documents);
42
+ fs.writeFileSync(this.indexPath, JSON.stringify(data, null, 2));
43
+ }
44
+
45
+ addDocument(doc: VectorDocument): void {
46
+ this.documents.set(doc.id, doc);
47
+ }
48
+
49
+ search(query: string, topK: number = 5): SearchResult[] {
50
+ const queryEmbedding = this.simpleEmbedding(query);
51
+ const results: SearchResult[] = [];
52
+
53
+ for (const doc of this.documents.values()) {
54
+ const score = this.cosineSimilarity(queryEmbedding, doc.embedding);
55
+ results.push({ document: doc, score });
56
+ }
57
+
58
+ return results
59
+ .sort((a, b) => b.score - a.score)
60
+ .slice(0, topK);
61
+ }
62
+
63
+ private simpleEmbedding(text: string): number[] {
64
+ const words = text.toLowerCase().split(/\s+/);
65
+ const embedding: number[] = new Array(100).fill(0);
66
+
67
+ for (let i = 0; i < words.length && i < 100; i++) {
68
+ let hash = 0;
69
+ for (const char of words[i]) {
70
+ hash = ((hash << 5) - hash) + char.charCodeAt(0);
71
+ hash = hash & hash;
72
+ }
73
+ embedding[i] = Math.sin(hash) * 0.5 + 0.5;
74
+ }
75
+
76
+ return embedding;
77
+ }
78
+
79
+ private cosineSimilarity(a: number[], b: number[]): number {
80
+ let dotProduct = 0;
81
+ let normA = 0;
82
+ let normB = 0;
83
+
84
+ for (let i = 0; i < a.length; i++) {
85
+ dotProduct += a[i] * b[i];
86
+ normA += a[i] * a[i];
87
+ normB += b[i] * b[i];
88
+ }
89
+
90
+ if (normA === 0 || normB === 0) return 0;
91
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
92
+ }
93
+ }
94
+
95
+ export function createVectorIndex(indexPath: string): VectorIndex {
96
+ return new VectorIndex(indexPath);
97
+ }
98
+
99
+ export function semanticSearch(
100
+ index: VectorIndex,
101
+ query: string,
102
+ topK: number = 5
103
+ ): SearchResult[] {
104
+ return index.search(query, topK);
105
+ }
@@ -0,0 +1 @@
1
+ export { startMCPServer, startMCP } from './server.js';
@@ -0,0 +1,179 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+ import { runAIFirst } from '../commands/ai-first.js';
8
+ import { generateIndex } from '../core/indexer.js';
9
+ import { buildKnowledgeGraph } from '../core/knowledgeGraphBuilder.js';
10
+ import { analyzeArchitecture } from '../analyzers/architecture.js';
11
+
12
+ interface MCPServerOptions {
13
+ rootDir?: string;
14
+ aiDir?: string;
15
+ }
16
+
17
+ export function startMCPServer(options: MCPServerOptions = {}): void {
18
+ const rootDir = options.rootDir || process.cwd();
19
+ const aiDir = options.aiDir || `${rootDir}/ai-context`;
20
+
21
+ const server = new Server(
22
+ {
23
+ name: 'ai-first-cli',
24
+ version: '1.4.0',
25
+ },
26
+ {
27
+ capabilities: {
28
+ tools: {},
29
+ },
30
+ }
31
+ );
32
+
33
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
34
+ return {
35
+ tools: [
36
+ {
37
+ name: 'generate_context',
38
+ description: 'Generate AI context for the repository or a specific module',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ module: {
43
+ type: 'string',
44
+ description: 'Optional module path to generate context for (e.g., "src/auth")',
45
+ },
46
+ preset: {
47
+ type: 'string',
48
+ enum: ['full', 'quick', 'api', 'docs'],
49
+ description: 'Preset to use for context generation',
50
+ },
51
+ },
52
+ },
53
+ },
54
+ {
55
+ name: 'query_symbols',
56
+ description: 'Query symbols (functions, classes, interfaces) in the indexed repository',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ query: {
61
+ type: 'string',
62
+ description: 'Search query for symbols',
63
+ },
64
+ type: {
65
+ type: 'string',
66
+ enum: ['function', 'class', 'interface', 'variable', 'all'],
67
+ description: 'Type of symbol to search for',
68
+ },
69
+ },
70
+ required: ['query'],
71
+ },
72
+ },
73
+ {
74
+ name: 'get_architecture',
75
+ description: 'Get the architecture analysis of the project',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ format: {
80
+ type: 'string',
81
+ enum: ['summary', 'detailed'],
82
+ description: 'Level of detail for the architecture report',
83
+ },
84
+ },
85
+ },
86
+ },
87
+ ],
88
+ };
89
+ });
90
+
91
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
92
+ const { name, arguments: args } = request.params;
93
+
94
+ try {
95
+ switch (name) {
96
+ case 'generate_context': {
97
+ const result = await runAIFirst({
98
+ rootDir,
99
+ outputDir: aiDir,
100
+ });
101
+ return {
102
+ content: [
103
+ {
104
+ type: 'text',
105
+ text: JSON.stringify({
106
+ success: result.success,
107
+ filesCreated: result.filesCreated,
108
+ message: result.success
109
+ ? `Generated context in ${aiDir}`
110
+ : `Error: ${result.error}`,
111
+ }, null, 2),
112
+ },
113
+ ],
114
+ };
115
+ }
116
+
117
+ case 'query_symbols': {
118
+ const query = args?.query as string;
119
+ const symbolType = (args?.type as string) || 'all';
120
+
121
+ const index = generateIndex(rootDir, aiDir);
122
+
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: JSON.stringify({
128
+ query,
129
+ type: symbolType,
130
+ results: [],
131
+ message: `Symbol query executed for "${query}"`,
132
+ }, null, 2),
133
+ },
134
+ ],
135
+ };
136
+ }
137
+
138
+ case 'get_architecture': {
139
+ const format = (args?.format as string) || 'summary';
140
+
141
+ return {
142
+ content: [
143
+ {
144
+ type: 'text',
145
+ text: JSON.stringify({
146
+ format,
147
+ rootDir,
148
+ message: 'Architecture analysis available',
149
+ }, null, 2),
150
+ },
151
+ ],
152
+ };
153
+ }
154
+
155
+ default:
156
+ throw new Error(`Unknown tool: ${name}`);
157
+ }
158
+ } catch (error) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text',
163
+ text: JSON.stringify({
164
+ error: error instanceof Error ? error.message : String(error),
165
+ }, null, 2),
166
+ },
167
+ ],
168
+ isError: true,
169
+ };
170
+ }
171
+ });
172
+
173
+ const transport = new StdioServerTransport();
174
+ server.connect(transport);
175
+
176
+ console.error('AI-First MCP Server running on stdio');
177
+ }
178
+
179
+ export { startMCPServer as startMCP };
@@ -22,6 +22,11 @@ export const DEFAULT_EXCLUDE_PATTERNS = [
22
22
  "target",
23
23
  ".cache",
24
24
  ".DS_Store",
25
+ "test-projects",
26
+ ".ai-dev",
27
+ ".ai-dev-out",
28
+ "ai-context",
29
+ ".ai-first-ignore",
25
30
  ];
26
31
 
27
32
  /**
@@ -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
+ });