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.
- package/CHANGELOG.md +186 -0
- package/README.es.md +68 -0
- package/README.md +53 -15
- package/ai/graph/knowledge-graph.json +1 -1
- package/ai-context/index-state.json +86 -2
- package/dist/analyzers/architecture.d.ts.map +1 -1
- package/dist/analyzers/architecture.js +72 -5
- package/dist/analyzers/architecture.js.map +1 -1
- package/dist/analyzers/entrypoints.d.ts.map +1 -1
- package/dist/analyzers/entrypoints.js +253 -0
- package/dist/analyzers/entrypoints.js.map +1 -1
- package/dist/analyzers/symbols.d.ts.map +1 -1
- package/dist/analyzers/symbols.js +47 -2
- package/dist/analyzers/symbols.js.map +1 -1
- package/dist/analyzers/techStack.d.ts.map +1 -1
- package/dist/analyzers/techStack.js +86 -0
- package/dist/analyzers/techStack.js.map +1 -1
- package/dist/commands/ai-first.d.ts.map +1 -1
- package/dist/commands/ai-first.js +78 -4
- package/dist/commands/ai-first.js.map +1 -1
- package/dist/config/configLoader.d.ts +6 -0
- package/dist/config/configLoader.d.ts.map +1 -0
- package/dist/config/configLoader.js +232 -0
- package/dist/config/configLoader.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +101 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/core/content/contentProcessor.d.ts +4 -0
- package/dist/core/content/contentProcessor.d.ts.map +1 -0
- package/dist/core/content/contentProcessor.js +235 -0
- package/dist/core/content/contentProcessor.js.map +1 -0
- package/dist/core/content/index.d.ts +3 -0
- package/dist/core/content/index.d.ts.map +1 -0
- package/dist/core/content/index.js +2 -0
- package/dist/core/content/index.js.map +1 -0
- package/dist/core/content/types.d.ts +32 -0
- package/dist/core/content/types.d.ts.map +1 -0
- package/dist/core/content/types.js +2 -0
- package/dist/core/content/types.js.map +1 -0
- package/dist/core/gitAnalyzer.d.ts +14 -0
- package/dist/core/gitAnalyzer.d.ts.map +1 -1
- package/dist/core/gitAnalyzer.js +98 -0
- package/dist/core/gitAnalyzer.js.map +1 -1
- package/dist/core/multiRepo/index.d.ts +3 -0
- package/dist/core/multiRepo/index.d.ts.map +1 -0
- package/dist/core/multiRepo/index.js +2 -0
- package/dist/core/multiRepo/index.js.map +1 -0
- package/dist/core/multiRepo/multiRepoScanner.d.ts +18 -0
- package/dist/core/multiRepo/multiRepoScanner.d.ts.map +1 -0
- package/dist/core/multiRepo/multiRepoScanner.js +131 -0
- package/dist/core/multiRepo/multiRepoScanner.js.map +1 -0
- package/dist/core/rag/index.d.ts +3 -0
- package/dist/core/rag/index.d.ts.map +1 -0
- package/dist/core/rag/index.js +2 -0
- package/dist/core/rag/index.js.map +1 -0
- package/dist/core/rag/vectorIndex.d.ts +28 -0
- package/dist/core/rag/vectorIndex.d.ts.map +1 -0
- package/dist/core/rag/vectorIndex.js +71 -0
- package/dist/core/rag/vectorIndex.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +154 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/utils/fileUtils.d.ts.map +1 -1
- package/dist/utils/fileUtils.js +5 -0
- package/dist/utils/fileUtils.js.map +1 -1
- package/docs/planning/evaluator-v1.0.0/README.md +112 -0
- package/docs/planning/evaluator-v1.0.0/improvements_plan_2026-03-28.md +237 -0
- package/package.json +13 -3
- package/src/analyzers/architecture.ts +75 -6
- package/src/analyzers/entrypoints.ts +285 -0
- package/src/analyzers/symbols.ts +52 -2
- package/src/analyzers/techStack.ts +90 -0
- package/src/commands/ai-first.ts +83 -4
- package/src/config/configLoader.ts +274 -0
- package/src/config/index.ts +27 -0
- package/src/config/types.ts +117 -0
- package/src/core/content/contentProcessor.ts +292 -0
- package/src/core/content/index.ts +9 -0
- package/src/core/content/types.ts +35 -0
- package/src/core/gitAnalyzer.ts +130 -0
- package/src/core/multiRepo/index.ts +2 -0
- package/src/core/multiRepo/multiRepoScanner.ts +177 -0
- package/src/core/rag/index.ts +2 -0
- package/src/core/rag/vectorIndex.ts +105 -0
- package/src/mcp/index.ts +1 -0
- package/src/mcp/server.ts +179 -0
- package/src/utils/fileUtils.ts +5 -0
- package/tests/entrypoints-languages.test.ts +373 -0
- package/tests/framework-detection.test.ts +296 -0
- package/tests/v1.3.8-integration.test.ts +361 -0
- package/BETA_EVALUATION_REPORT.md +0 -151
- package/ai-context/context/flows/App.json +0 -17
- package/ai-context/context/flows/DashboardPage.json +0 -14
- package/ai-context/context/flows/LoginPage.json +0 -14
- package/ai-context/context/flows/admin.json +0 -10
- package/ai-context/context/flows/androidresources.json +0 -11
- package/ai-context/context/flows/authController.json +0 -14
- package/ai-context/context/flows/entrypoints.json +0 -9
- package/ai-context/context/flows/fastapiAdapter.json +0 -14
- package/ai-context/context/flows/fastapiadapter.json +0 -11
- package/ai-context/context/flows/index.json +0 -19
- package/ai-context/context/flows/indexer.json +0 -9
- package/ai-context/context/flows/indexstate.json +0 -9
- package/ai-context/context/flows/init.json +0 -22
- package/ai-context/context/flows/main.json +0 -18
- package/ai-context/context/flows/mainactivity.json +0 -9
- package/ai-context/context/flows/models.json +0 -15
- package/ai-context/context/flows/posts.json +0 -15
- package/ai-context/context/flows/repoMapper.json +0 -20
- package/ai-context/context/flows/repomapper.json +0 -11
- package/ai-context/context/flows/serializers.json +0 -10
- package/ai-context-evaluation-report-1774223059505.md +0 -206
- package/dist/scripts/ai-context-evaluator.js +0 -367
- package/quick-evaluation-report-1774396002305.md +0 -64
- package/quick-evaluator.ts +0 -200
- package/scripts/ai-context-evaluator.ts +0 -440
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { ContentProcessorOptions, ProcessedContent, DetailLevel, InclusionLevel } from './types.js';
|
|
2
|
+
|
|
3
|
+
export function processContent(
|
|
4
|
+
content: string,
|
|
5
|
+
options: ContentProcessorOptions = {}
|
|
6
|
+
): ProcessedContent {
|
|
7
|
+
const {
|
|
8
|
+
inclusionLevel = 'full',
|
|
9
|
+
detailLevel = 'full',
|
|
10
|
+
language = detectLanguage(content),
|
|
11
|
+
preserveComments = true,
|
|
12
|
+
preserveImports = true
|
|
13
|
+
} = options;
|
|
14
|
+
|
|
15
|
+
const originalLength = content.length;
|
|
16
|
+
let processedContent = content;
|
|
17
|
+
|
|
18
|
+
if (inclusionLevel === 'directory') {
|
|
19
|
+
return {
|
|
20
|
+
originalLength,
|
|
21
|
+
processedLength: 0,
|
|
22
|
+
compressionRatio: 100,
|
|
23
|
+
content: '',
|
|
24
|
+
tokens: 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (inclusionLevel === 'exclude') {
|
|
29
|
+
return {
|
|
30
|
+
originalLength,
|
|
31
|
+
processedLength: 0,
|
|
32
|
+
compressionRatio: 100,
|
|
33
|
+
content: '',
|
|
34
|
+
tokens: 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (inclusionLevel === 'compress' || detailLevel !== 'full') {
|
|
39
|
+
processedContent = compressContent(content, detailLevel, language, preserveComments, preserveImports);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const processedLength = processedContent.length;
|
|
43
|
+
const compressionRatio = originalLength > 0
|
|
44
|
+
? ((originalLength - processedLength) / originalLength) * 100
|
|
45
|
+
: 0;
|
|
46
|
+
const tokens = estimateTokens(processedContent);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
originalLength,
|
|
50
|
+
processedLength,
|
|
51
|
+
compressionRatio,
|
|
52
|
+
content: processedContent,
|
|
53
|
+
tokens
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function compressContent(
|
|
58
|
+
content: string,
|
|
59
|
+
detailLevel: DetailLevel,
|
|
60
|
+
language: string,
|
|
61
|
+
preserveComments: boolean,
|
|
62
|
+
preserveImports: boolean
|
|
63
|
+
): string {
|
|
64
|
+
const lines = content.split('\n');
|
|
65
|
+
const result: string[] = [];
|
|
66
|
+
let inFunction = false;
|
|
67
|
+
let braceCount = 0;
|
|
68
|
+
let functionSignature: string | null = null;
|
|
69
|
+
let inComment = false;
|
|
70
|
+
let commentBlock: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
const line = lines[i];
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
|
|
76
|
+
if (trimmed.startsWith('/*')) {
|
|
77
|
+
inComment = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (inComment) {
|
|
81
|
+
if (preserveComments && detailLevel !== 'skeleton') {
|
|
82
|
+
commentBlock.push(line);
|
|
83
|
+
}
|
|
84
|
+
if (trimmed.includes('*/')) {
|
|
85
|
+
inComment = false;
|
|
86
|
+
if (commentBlock.length > 0) {
|
|
87
|
+
result.push(...commentBlock);
|
|
88
|
+
commentBlock = [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
|
|
95
|
+
if (preserveComments && detailLevel !== 'skeleton') {
|
|
96
|
+
result.push(line);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (preserveImports && isImportLine(trimmed, language)) {
|
|
102
|
+
result.push(line);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isExportLine(trimmed, language)) {
|
|
107
|
+
if (detailLevel === 'skeleton') {
|
|
108
|
+
const simplified = simplifyExport(line, language);
|
|
109
|
+
if (simplified) result.push(simplified);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isFunctionSignature(line, language)) {
|
|
115
|
+
if (inFunction) {
|
|
116
|
+
braceCount = 0;
|
|
117
|
+
}
|
|
118
|
+
inFunction = true;
|
|
119
|
+
functionSignature = line;
|
|
120
|
+
braceCount += countBraces(line);
|
|
121
|
+
|
|
122
|
+
if (detailLevel === 'signatures' || detailLevel === 'skeleton') {
|
|
123
|
+
const signature = extractSignature(line, language);
|
|
124
|
+
if (signature) result.push(signature);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (inFunction) {
|
|
130
|
+
braceCount += countBraces(line);
|
|
131
|
+
if (braceCount <= 0) {
|
|
132
|
+
inFunction = false;
|
|
133
|
+
functionSignature = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (detailLevel === 'full') {
|
|
137
|
+
result.push(line);
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (detailLevel === 'full') {
|
|
143
|
+
result.push(line);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isImportLine(line: string, language: string): boolean {
|
|
151
|
+
const importPatterns = [
|
|
152
|
+
/^import\s+/,
|
|
153
|
+
/^from\s+\w+\s+import/,
|
|
154
|
+
/^require\s*\(/,
|
|
155
|
+
/^using\s+/,
|
|
156
|
+
/^#include/,
|
|
157
|
+
/^#import/,
|
|
158
|
+
/^const\s+\w+\s+=\s+require\s*\(/,
|
|
159
|
+
];
|
|
160
|
+
return importPatterns.some(pattern => pattern.test(line));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isExportLine(line: string, language: string): boolean {
|
|
164
|
+
const exportPatterns = [
|
|
165
|
+
/^export\s+/,
|
|
166
|
+
/^module\.exports\s*=/,
|
|
167
|
+
/^exports\./,
|
|
168
|
+
/^public\s+/,
|
|
169
|
+
];
|
|
170
|
+
return exportPatterns.some(pattern => pattern.test(line));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function simplifyExport(line: string, language: string): string | null {
|
|
174
|
+
const simplified = line.replace(/\{[\s\S]*\}/, '{ ... }');
|
|
175
|
+
return simplified;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isFunctionSignature(line: string, language: string): boolean {
|
|
179
|
+
const patterns = [
|
|
180
|
+
/^(export\s+)?(async\s+)?function\s+\w+\s*\(/,
|
|
181
|
+
/^(export\s+)?(async\s+)?function\s*\(/,
|
|
182
|
+
/^(export\s+)?const\s+\w+\s*=\s*(async\s*)?\(/,
|
|
183
|
+
/^(export\s+)?(async\s+)?\w+\s*\([^)]*\)\s*{/,
|
|
184
|
+
/^(export\s+)?(async\s+)?\w+\s*\([^)]*\)\s*:\s*\w+\s*{/,
|
|
185
|
+
/^(export\s+)?class\s+\w+/,
|
|
186
|
+
/^(export\s+)?interface\s+\w+/,
|
|
187
|
+
/^(export\s+)?type\s+\w+\s*=/,
|
|
188
|
+
/^(public|private|protected)\s+(async\s+)?\w+\s*\(/,
|
|
189
|
+
/^\s*(async\s+)?def\s+\w+\s*\(/,
|
|
190
|
+
/^func\s+\w+\s*\(/,
|
|
191
|
+
];
|
|
192
|
+
return patterns.some(pattern => pattern.test(line));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractSignature(line: string, language: string): string | null {
|
|
196
|
+
const signatureMatch = line.match(/^(.*\{)/);
|
|
197
|
+
if (signatureMatch) {
|
|
198
|
+
return signatureMatch[1] + ' ... }';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const arrowMatch = line.match(/^(.*=>).*$/);
|
|
202
|
+
if (arrowMatch) {
|
|
203
|
+
return arrowMatch[1] + ' ...;';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const classMatch = line.match(/^(export\s+)?class\s+\w+([^{]*)(\{)?/);
|
|
207
|
+
if (classMatch) {
|
|
208
|
+
return classMatch[1] || '' + 'class ' + line.match(/class\s+(\w+)/)?.[1] + ' { ... }';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const interfaceMatch = line.match(/^(export\s+)?interface\s+\w+/);
|
|
212
|
+
if (interfaceMatch) {
|
|
213
|
+
return line + ' { ... }';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return line + ';';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function countBraces(line: string): number {
|
|
220
|
+
let count = 0;
|
|
221
|
+
for (const char of line) {
|
|
222
|
+
if (char === '{') count++;
|
|
223
|
+
if (char === '}') count--;
|
|
224
|
+
}
|
|
225
|
+
return count;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function detectLanguage(content: string): string {
|
|
229
|
+
if (content.includes('interface ') || content.includes(': string') || content.includes(': number')) {
|
|
230
|
+
return 'typescript';
|
|
231
|
+
}
|
|
232
|
+
if (content.includes('def ') || content.includes('import ') && content.includes(':')) {
|
|
233
|
+
return 'python';
|
|
234
|
+
}
|
|
235
|
+
if (content.includes('func ') || content.includes('package main')) {
|
|
236
|
+
return 'go';
|
|
237
|
+
}
|
|
238
|
+
if (content.includes('fn ') || content.includes('let mut ')) {
|
|
239
|
+
return 'rust';
|
|
240
|
+
}
|
|
241
|
+
return 'javascript';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function estimateTokens(content: string): number {
|
|
245
|
+
return Math.ceil(content.length / 4);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function classifyFile(
|
|
249
|
+
filePath: string,
|
|
250
|
+
includePatterns: string[],
|
|
251
|
+
compressPatterns: string[],
|
|
252
|
+
directoryPatterns: string[],
|
|
253
|
+
excludePatterns: string[]
|
|
254
|
+
): InclusionLevel {
|
|
255
|
+
if (matchesPatterns(filePath, excludePatterns)) {
|
|
256
|
+
return 'exclude';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (matchesPatterns(filePath, directoryPatterns)) {
|
|
260
|
+
return 'directory';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (matchesPatterns(filePath, compressPatterns)) {
|
|
264
|
+
return 'compress';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (includePatterns.length === 0 || matchesPatterns(filePath, includePatterns)) {
|
|
268
|
+
return 'full';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return 'exclude';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function matchesPatterns(filePath: string, patterns: string[]): boolean {
|
|
275
|
+
if (patterns.length === 0) return false;
|
|
276
|
+
|
|
277
|
+
return patterns.some(pattern => {
|
|
278
|
+
const regex = globToRegex(pattern);
|
|
279
|
+
return regex.test(filePath);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function globToRegex(pattern: string): RegExp {
|
|
284
|
+
let regex = pattern
|
|
285
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
286
|
+
.replace(/\*/g, '[^/]*')
|
|
287
|
+
.replace(/\?/g, '.')
|
|
288
|
+
.replace(/\./g, '\\.')
|
|
289
|
+
.replace('{{GLOBSTAR}}', '.*');
|
|
290
|
+
|
|
291
|
+
return new RegExp(`^${regex}$`);
|
|
292
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type InclusionLevel = 'full' | 'compress' | 'directory' | 'exclude';
|
|
2
|
+
export type DetailLevel = 'full' | 'signatures' | 'skeleton';
|
|
3
|
+
|
|
4
|
+
export interface FileClassification {
|
|
5
|
+
path: string;
|
|
6
|
+
inclusionLevel: InclusionLevel;
|
|
7
|
+
detailLevel: DetailLevel;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ContentProcessorOptions {
|
|
11
|
+
inclusionLevel?: InclusionLevel;
|
|
12
|
+
detailLevel?: DetailLevel;
|
|
13
|
+
language?: string;
|
|
14
|
+
preserveComments?: boolean;
|
|
15
|
+
preserveImports?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProcessedContent {
|
|
19
|
+
originalLength: number;
|
|
20
|
+
processedLength: number;
|
|
21
|
+
compressionRatio: number;
|
|
22
|
+
content: string;
|
|
23
|
+
tokens: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CompressionStats {
|
|
27
|
+
totalFiles: number;
|
|
28
|
+
fullFiles: number;
|
|
29
|
+
compressedFiles: number;
|
|
30
|
+
directoryOnlyFiles: number;
|
|
31
|
+
excludedFiles: number;
|
|
32
|
+
originalTokens: number;
|
|
33
|
+
processedTokens: number;
|
|
34
|
+
savingsPercentage: number;
|
|
35
|
+
}
|
package/src/core/gitAnalyzer.ts
CHANGED
|
@@ -389,3 +389,133 @@ export function generateGitContext(rootDir: string, aiDir?: string): {
|
|
|
389
389
|
activity
|
|
390
390
|
};
|
|
391
391
|
}
|
|
392
|
+
|
|
393
|
+
export interface GitBlameLine {
|
|
394
|
+
line: number;
|
|
395
|
+
content: string;
|
|
396
|
+
author: string;
|
|
397
|
+
date: string;
|
|
398
|
+
hash: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface GitBlameResult {
|
|
402
|
+
filePath: string;
|
|
403
|
+
lines: GitBlameLine[];
|
|
404
|
+
authors: Map<string, number>;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function getGitBlame(rootDir: string, filePath: string): GitBlameResult {
|
|
408
|
+
const fullPath = path.join(rootDir, filePath);
|
|
409
|
+
|
|
410
|
+
if (!fs.existsSync(fullPath)) {
|
|
411
|
+
return {
|
|
412
|
+
filePath,
|
|
413
|
+
lines: [],
|
|
414
|
+
authors: new Map()
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!detectGitRepository(rootDir)) {
|
|
419
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
420
|
+
const lines = content.split('\n');
|
|
421
|
+
return {
|
|
422
|
+
filePath,
|
|
423
|
+
lines: lines.map((content, idx) => ({
|
|
424
|
+
line: idx + 1,
|
|
425
|
+
content,
|
|
426
|
+
author: 'unknown',
|
|
427
|
+
date: '',
|
|
428
|
+
hash: ''
|
|
429
|
+
})),
|
|
430
|
+
authors: new Map([['unknown', lines.length]])
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const blameOutput = gitExec(rootDir, `git blame --line-porcelain "${filePath}"`);
|
|
435
|
+
|
|
436
|
+
if (!blameOutput) {
|
|
437
|
+
return {
|
|
438
|
+
filePath,
|
|
439
|
+
lines: [],
|
|
440
|
+
authors: new Map()
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const lines: GitBlameLine[] = [];
|
|
445
|
+
const authors = new Map<string, number>();
|
|
446
|
+
const lineData: { hash?: string; author?: string; date?: string; content?: string } = {};
|
|
447
|
+
let lineNumber = 0;
|
|
448
|
+
|
|
449
|
+
const blameLines = blameOutput.split('\n');
|
|
450
|
+
|
|
451
|
+
for (const blameLine of blameLines) {
|
|
452
|
+
if (blameLine.startsWith('\t')) {
|
|
453
|
+
lineData.content = blameLine.slice(1);
|
|
454
|
+
lineNumber++;
|
|
455
|
+
|
|
456
|
+
const author = lineData.author || 'unknown';
|
|
457
|
+
const date = lineData.date || '';
|
|
458
|
+
const hash = lineData.hash || '';
|
|
459
|
+
|
|
460
|
+
lines.push({
|
|
461
|
+
line: lineNumber,
|
|
462
|
+
content: lineData.content,
|
|
463
|
+
author,
|
|
464
|
+
date,
|
|
465
|
+
hash
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
authors.set(author, (authors.get(author) || 0) + 1);
|
|
469
|
+
|
|
470
|
+
lineData.hash = undefined;
|
|
471
|
+
lineData.author = undefined;
|
|
472
|
+
lineData.date = undefined;
|
|
473
|
+
lineData.content = undefined;
|
|
474
|
+
} else if (blameLine.startsWith('author ')) {
|
|
475
|
+
lineData.author = blameLine.slice(7);
|
|
476
|
+
} else if (blameLine.startsWith('author-time ')) {
|
|
477
|
+
const timestamp = parseInt(blameLine.slice(12), 10);
|
|
478
|
+
lineData.date = new Date(timestamp * 1000).toISOString().split('T')[0];
|
|
479
|
+
} else if (!blameLine.startsWith(' ') && blameLine.length >= 40) {
|
|
480
|
+
lineData.hash = blameLine.split(' ')[0];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
filePath,
|
|
486
|
+
lines,
|
|
487
|
+
authors
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function formatGitBlame(
|
|
492
|
+
blameResult: GitBlameResult,
|
|
493
|
+
format: 'inline' | 'block' = 'inline'
|
|
494
|
+
): string {
|
|
495
|
+
if (format === 'block') {
|
|
496
|
+
const sections: string[] = [];
|
|
497
|
+
let currentAuthor = '';
|
|
498
|
+
let currentSection: string[] = [];
|
|
499
|
+
|
|
500
|
+
for (const line of blameResult.lines) {
|
|
501
|
+
if (line.author !== currentAuthor) {
|
|
502
|
+
if (currentSection.length > 0) {
|
|
503
|
+
sections.push(`// ${currentAuthor}\n${currentSection.join('\n')}`);
|
|
504
|
+
}
|
|
505
|
+
currentAuthor = line.author;
|
|
506
|
+
currentSection = [];
|
|
507
|
+
}
|
|
508
|
+
currentSection.push(line.content);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (currentSection.length > 0) {
|
|
512
|
+
sections.push(`// ${currentAuthor}\n${currentSection.join('\n')}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return sections.join('\n\n');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return blameResult.lines
|
|
519
|
+
.map(line => `[${line.author} ${line.date}] ${line.content}`)
|
|
520
|
+
.join('\n');
|
|
521
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { scanRepo, FileInfo } from '../repoScanner.js';
|
|
4
|
+
|
|
5
|
+
export interface Repository {
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
files: FileInfo[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MultiRepoContext {
|
|
12
|
+
repositories: Repository[];
|
|
13
|
+
totalFiles: number;
|
|
14
|
+
crossRepoDependencies: Map<string, string[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MultiRepoOptions {
|
|
18
|
+
repositories: string[];
|
|
19
|
+
includeSubmodules?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function scanMultiRepo(options: MultiRepoOptions): MultiRepoContext {
|
|
23
|
+
const repositories: Repository[] = [];
|
|
24
|
+
const crossRepoDependencies = new Map<string, string[]>();
|
|
25
|
+
let totalFiles = 0;
|
|
26
|
+
|
|
27
|
+
for (const repoPath of options.repositories) {
|
|
28
|
+
const resolvedPath = path.resolve(repoPath);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
31
|
+
console.warn(`Repository path does not exist: ${resolvedPath}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const repoName = path.basename(resolvedPath);
|
|
36
|
+
const scanResult = scanRepo(resolvedPath);
|
|
37
|
+
|
|
38
|
+
repositories.push({
|
|
39
|
+
name: repoName,
|
|
40
|
+
path: resolvedPath,
|
|
41
|
+
files: scanResult.files
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
totalFiles += scanResult.totalFiles;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options.includeSubmodules) {
|
|
48
|
+
for (const repo of repositories) {
|
|
49
|
+
const submodules = detectSubmodules(repo.path);
|
|
50
|
+
for (const submodule of submodules) {
|
|
51
|
+
const submoduleName = path.basename(submodule);
|
|
52
|
+
const scanResult = scanRepo(submodule);
|
|
53
|
+
|
|
54
|
+
repositories.push({
|
|
55
|
+
name: `${repo.name}/${submoduleName}`,
|
|
56
|
+
path: submodule,
|
|
57
|
+
files: scanResult.files
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
totalFiles += scanResult.totalFiles;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
detectCrossRepoDependencies(repositories, crossRepoDependencies);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
repositories,
|
|
69
|
+
totalFiles,
|
|
70
|
+
crossRepoDependencies
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectSubmodules(repoPath: string): string[] {
|
|
75
|
+
const submodules: string[] = [];
|
|
76
|
+
const gitmodulesPath = path.join(repoPath, '.gitmodules');
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(gitmodulesPath)) {
|
|
79
|
+
return submodules;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(gitmodulesPath, 'utf-8');
|
|
84
|
+
const matches = content.match(/path\s*=\s*(.+)/g);
|
|
85
|
+
|
|
86
|
+
if (matches) {
|
|
87
|
+
for (const match of matches) {
|
|
88
|
+
const submodulePath = match.split('=')[1].trim();
|
|
89
|
+
const fullPath = path.join(repoPath, submodulePath);
|
|
90
|
+
if (fs.existsSync(fullPath)) {
|
|
91
|
+
submodules.push(fullPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore errors reading .gitmodules
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return submodules;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function detectCrossRepoDependencies(
|
|
103
|
+
repositories: Repository[],
|
|
104
|
+
dependencies: Map<string, string[]>
|
|
105
|
+
): void {
|
|
106
|
+
for (const repo of repositories) {
|
|
107
|
+
const deps: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const otherRepo of repositories) {
|
|
110
|
+
if (repo.name === otherRepo.name) continue;
|
|
111
|
+
|
|
112
|
+
const hasDependency = checkDependency(repo, otherRepo);
|
|
113
|
+
if (hasDependency) {
|
|
114
|
+
deps.push(otherRepo.name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (deps.length > 0) {
|
|
119
|
+
dependencies.set(repo.name, deps);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkDependency(repoA: Repository, repoB: Repository): boolean {
|
|
125
|
+
const packageJsonA = path.join(repoA.path, 'package.json');
|
|
126
|
+
|
|
127
|
+
if (fs.existsSync(packageJsonA)) {
|
|
128
|
+
try {
|
|
129
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonA, 'utf-8'));
|
|
130
|
+
const deps = {
|
|
131
|
+
...pkg.dependencies,
|
|
132
|
+
...pkg.devDependencies,
|
|
133
|
+
...pkg.peerDependencies
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
for (const dep of Object.keys(deps)) {
|
|
137
|
+
if (dep.includes(repoB.name.toLowerCase())) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore errors reading package.json
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function generateMultiRepoReport(context: MultiRepoContext): string {
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
|
|
152
|
+
lines.push('# Multi-Repository Context\n');
|
|
153
|
+
lines.push(`## Summary`);
|
|
154
|
+
lines.push(`- **Total Repositories:** ${context.repositories.length}`);
|
|
155
|
+
lines.push(`- **Total Files:** ${context.totalFiles}\n`);
|
|
156
|
+
|
|
157
|
+
lines.push(`## Repositories`);
|
|
158
|
+
for (const repo of context.repositories) {
|
|
159
|
+
lines.push(`\n### ${repo.name}`);
|
|
160
|
+
lines.push(`- **Path:** ${repo.path}`);
|
|
161
|
+
lines.push(`- **Files:** ${repo.files.length}`);
|
|
162
|
+
|
|
163
|
+
const deps = context.crossRepoDependencies.get(repo.name);
|
|
164
|
+
if (deps && deps.length > 0) {
|
|
165
|
+
lines.push(`- **Dependencies:** ${deps.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (context.crossRepoDependencies.size > 0) {
|
|
170
|
+
lines.push(`\n## Cross-Repository Dependencies`);
|
|
171
|
+
for (const [repo, deps] of context.crossRepoDependencies) {
|
|
172
|
+
lines.push(`- **${repo}** depends on: ${deps.join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|