deepdebug-local-agent 0.3.7 → 0.3.9
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/analyzers/config-analyzer.js +446 -0
- package/analyzers/controller-analyzer.js +429 -0
- package/analyzers/dto-analyzer.js +455 -0
- package/detectors/build-tool-detector.js +0 -0
- package/detectors/framework-detector.js +91 -0
- package/detectors/language-detector.js +89 -0
- package/detectors/multi-project-detector.js +191 -0
- package/detectors/service-detector.js +244 -0
- package/detectors.js +30 -0
- package/exec-utils.js +215 -0
- package/fs-utils.js +34 -0
- package/mcp-http-server.js +313 -0
- package/package.json +1 -1
- package/patch.js +607 -0
- package/ports.js +69 -0
- package/server.js +1 -138
- package/workspace/detect-port.js +176 -0
- package/workspace/file-reader.js +54 -0
- package/workspace/git-client.js +0 -0
- package/workspace/process-manager.js +619 -0
- package/workspace/scanner.js +72 -0
- package/workspace-manager.js +172 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { readFile } from "../fs-utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DTOAnalyzer
|
|
6
|
+
*
|
|
7
|
+
* Analyzes Java DTO/Model classes to extract:
|
|
8
|
+
* - Fields and types
|
|
9
|
+
* - Validation annotations
|
|
10
|
+
* - Example payloads
|
|
11
|
+
*/
|
|
12
|
+
export class DTOAnalyzer {
|
|
13
|
+
constructor(workspaceRoot) {
|
|
14
|
+
this.workspaceRoot = workspaceRoot;
|
|
15
|
+
this.analyzedDtos = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find all DTO/Model files in the workspace
|
|
20
|
+
*/
|
|
21
|
+
async findDtoFiles(files) {
|
|
22
|
+
const dtoPatterns = [
|
|
23
|
+
/Dto\.java$/,
|
|
24
|
+
/DTO\.java$/,
|
|
25
|
+
/Request\.java$/,
|
|
26
|
+
/Response\.java$/,
|
|
27
|
+
/Model\.java$/,
|
|
28
|
+
/Entity\.java$/
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Also look in model/dto/request/response directories
|
|
32
|
+
const dtoDirectories = ["/model/", "/dto/", "/request/", "/response/", "/entity/"];
|
|
33
|
+
|
|
34
|
+
return files.filter(file => {
|
|
35
|
+
const filePath = file.path || file;
|
|
36
|
+
const fileName = path.basename(filePath);
|
|
37
|
+
|
|
38
|
+
// Check by name pattern
|
|
39
|
+
for (const pattern of dtoPatterns) {
|
|
40
|
+
if (pattern.test(fileName)) return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check by directory
|
|
44
|
+
for (const dir of dtoDirectories) {
|
|
45
|
+
if (filePath.includes(dir)) return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}).map(f => f.path || f);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Analyze a single DTO file
|
|
54
|
+
*/
|
|
55
|
+
async analyzeDto(filePath) {
|
|
56
|
+
// Check cache
|
|
57
|
+
if (this.analyzedDtos.has(filePath)) {
|
|
58
|
+
return this.analyzedDtos.get(filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const fullPath = path.join(this.workspaceRoot, filePath);
|
|
63
|
+
const content = await readFile(fullPath, "utf8");
|
|
64
|
+
|
|
65
|
+
const dto = {
|
|
66
|
+
file: filePath,
|
|
67
|
+
className: this.extractClassName(content),
|
|
68
|
+
packageName: this.extractPackageName(content),
|
|
69
|
+
fields: this.extractFields(content),
|
|
70
|
+
annotations: this.extractClassAnnotations(content),
|
|
71
|
+
isRecord: content.includes("public record"),
|
|
72
|
+
extends: this.extractExtends(content),
|
|
73
|
+
implements: this.extractImplements(content)
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Generate example payload
|
|
77
|
+
dto.examplePayload = this.generateExamplePayload(dto);
|
|
78
|
+
|
|
79
|
+
this.analyzedDtos.set(filePath, dto);
|
|
80
|
+
return dto;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`Failed to analyze DTO ${filePath}:`, err.message);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract package name
|
|
89
|
+
*/
|
|
90
|
+
extractPackageName(content) {
|
|
91
|
+
const match = content.match(/package\s+([\w.]+);/);
|
|
92
|
+
return match ? match[1] : "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract class name
|
|
97
|
+
*/
|
|
98
|
+
extractClassName(content) {
|
|
99
|
+
// Handle records
|
|
100
|
+
const recordMatch = content.match(/public\s+record\s+(\w+)/);
|
|
101
|
+
if (recordMatch) return recordMatch[1];
|
|
102
|
+
|
|
103
|
+
const classMatch = content.match(/public\s+class\s+(\w+)/);
|
|
104
|
+
return classMatch ? classMatch[1] : "Unknown";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract class-level annotations
|
|
109
|
+
*/
|
|
110
|
+
extractClassAnnotations(content) {
|
|
111
|
+
const annotations = [];
|
|
112
|
+
const beforeClass = content.split(/public\s+(?:class|record)/)[0];
|
|
113
|
+
const regex = /@(\w+)(?:\([^)]*\))?/g;
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = regex.exec(beforeClass)) !== null) {
|
|
116
|
+
annotations.push(match[1]);
|
|
117
|
+
}
|
|
118
|
+
return annotations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extract all fields from class
|
|
123
|
+
*/
|
|
124
|
+
extractFields(content) {
|
|
125
|
+
const fields = [];
|
|
126
|
+
|
|
127
|
+
// Check if it's a record
|
|
128
|
+
const recordMatch = content.match(/public\s+record\s+\w+\s*\(([^)]+)\)/);
|
|
129
|
+
if (recordMatch) {
|
|
130
|
+
return this.extractRecordFields(recordMatch[1]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Regular class fields
|
|
134
|
+
const fieldRegex = /(?:@[\w.]+(?:\([^)]*\))?\s+)*(?:private|protected|public)\s+([\w<>,\s?]+)\s+(\w+)\s*(?:=|;)/g;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = fieldRegex.exec(content)) !== null) {
|
|
137
|
+
const fieldBlock = content.substring(match.index - 200, match.index + match[0].length);
|
|
138
|
+
const annotations = this.extractFieldAnnotations(fieldBlock);
|
|
139
|
+
|
|
140
|
+
fields.push({
|
|
141
|
+
type: match[1].trim(),
|
|
142
|
+
name: match[2],
|
|
143
|
+
annotations: annotations,
|
|
144
|
+
required: this.isFieldRequired(annotations),
|
|
145
|
+
validation: this.extractValidationRules(annotations)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return fields;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract fields from record definition
|
|
154
|
+
*/
|
|
155
|
+
extractRecordFields(recordParams) {
|
|
156
|
+
const fields = [];
|
|
157
|
+
const params = this.splitParameters(recordParams);
|
|
158
|
+
|
|
159
|
+
for (const param of params) {
|
|
160
|
+
const parts = param.trim().split(/\s+/);
|
|
161
|
+
if (parts.length >= 2) {
|
|
162
|
+
const type = parts[0];
|
|
163
|
+
const name = parts[parts.length - 1];
|
|
164
|
+
fields.push({
|
|
165
|
+
type: type,
|
|
166
|
+
name: name,
|
|
167
|
+
annotations: [],
|
|
168
|
+
required: true,
|
|
169
|
+
validation: {}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return fields;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Split parameters handling generics
|
|
179
|
+
*/
|
|
180
|
+
splitParameters(paramsString) {
|
|
181
|
+
const params = [];
|
|
182
|
+
let current = "";
|
|
183
|
+
let depth = 0;
|
|
184
|
+
|
|
185
|
+
for (const char of paramsString) {
|
|
186
|
+
if (char === "<") depth++;
|
|
187
|
+
else if (char === ">") depth--;
|
|
188
|
+
else if (char === "," && depth === 0) {
|
|
189
|
+
params.push(current);
|
|
190
|
+
current = "";
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
current += char;
|
|
194
|
+
}
|
|
195
|
+
if (current.trim()) params.push(current);
|
|
196
|
+
return params;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract annotations for a field
|
|
201
|
+
*/
|
|
202
|
+
extractFieldAnnotations(fieldBlock) {
|
|
203
|
+
const annotations = [];
|
|
204
|
+
const regex = /@(\w+)(?:\(([^)]*)\))?/g;
|
|
205
|
+
let match;
|
|
206
|
+
while ((match = regex.exec(fieldBlock)) !== null) {
|
|
207
|
+
annotations.push({
|
|
208
|
+
name: match[1],
|
|
209
|
+
value: match[2] || null
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return annotations;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if field is required based on annotations
|
|
217
|
+
*/
|
|
218
|
+
isFieldRequired(annotations) {
|
|
219
|
+
const requiredAnnotations = ["NotNull", "NotBlank", "NotEmpty", "NonNull"];
|
|
220
|
+
return annotations.some(a => requiredAnnotations.includes(a.name));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Extract validation rules from annotations
|
|
225
|
+
*/
|
|
226
|
+
extractValidationRules(annotations) {
|
|
227
|
+
const rules = {};
|
|
228
|
+
|
|
229
|
+
for (const annotation of annotations) {
|
|
230
|
+
switch (annotation.name) {
|
|
231
|
+
case "Size":
|
|
232
|
+
const sizeMatch = annotation.value?.match(/min\s*=\s*(\d+).*max\s*=\s*(\d+)/);
|
|
233
|
+
if (sizeMatch) {
|
|
234
|
+
rules.minLength = parseInt(sizeMatch[1]);
|
|
235
|
+
rules.maxLength = parseInt(sizeMatch[2]);
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
case "Min":
|
|
239
|
+
rules.min = parseInt(annotation.value);
|
|
240
|
+
break;
|
|
241
|
+
case "Max":
|
|
242
|
+
rules.max = parseInt(annotation.value);
|
|
243
|
+
break;
|
|
244
|
+
case "Email":
|
|
245
|
+
rules.format = "email";
|
|
246
|
+
break;
|
|
247
|
+
case "Pattern":
|
|
248
|
+
const patternMatch = annotation.value?.match(/regexp\s*=\s*"([^"]+)"/);
|
|
249
|
+
if (patternMatch) rules.pattern = patternMatch[1];
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return rules;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract extends clause
|
|
259
|
+
*/
|
|
260
|
+
extractExtends(content) {
|
|
261
|
+
const match = content.match(/class\s+\w+\s+extends\s+([\w<>]+)/);
|
|
262
|
+
return match ? match[1] : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Extract implements clause
|
|
267
|
+
*/
|
|
268
|
+
extractImplements(content) {
|
|
269
|
+
const match = content.match(/class\s+\w+(?:\s+extends\s+[\w<>]+)?\s+implements\s+([\w<>,\s]+)/);
|
|
270
|
+
if (match) {
|
|
271
|
+
return match[1].split(",").map(s => s.trim());
|
|
272
|
+
}
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Generate example payload JSON
|
|
278
|
+
*/
|
|
279
|
+
generateExamplePayload(dto) {
|
|
280
|
+
const payload = {};
|
|
281
|
+
|
|
282
|
+
for (const field of dto.fields) {
|
|
283
|
+
payload[field.name] = this.generateExampleValue(field);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return payload;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Generate example value for a field based on type
|
|
291
|
+
*/
|
|
292
|
+
generateExampleValue(field) {
|
|
293
|
+
const type = field.type.replace(/<.*>/, ""); // Remove generics
|
|
294
|
+
|
|
295
|
+
// Check validation rules first
|
|
296
|
+
if (field.validation?.format === "email") {
|
|
297
|
+
return "user@example.com";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
switch (type) {
|
|
301
|
+
case "String":
|
|
302
|
+
if (field.name.toLowerCase().includes("email")) return "user@example.com";
|
|
303
|
+
if (field.name.toLowerCase().includes("name")) return "John Doe";
|
|
304
|
+
if (field.name.toLowerCase().includes("phone")) return "+1234567890";
|
|
305
|
+
if (field.name.toLowerCase().includes("password")) return "********";
|
|
306
|
+
if (field.name.toLowerCase().includes("id")) return "abc123";
|
|
307
|
+
if (field.name.toLowerCase().includes("url")) return "https://example.com";
|
|
308
|
+
return "example_" + field.name;
|
|
309
|
+
|
|
310
|
+
case "Integer":
|
|
311
|
+
case "int":
|
|
312
|
+
case "Long":
|
|
313
|
+
case "long":
|
|
314
|
+
if (field.name.toLowerCase().includes("id")) return 1;
|
|
315
|
+
if (field.name.toLowerCase().includes("age")) return 25;
|
|
316
|
+
if (field.name.toLowerCase().includes("count")) return 10;
|
|
317
|
+
return field.validation?.min || 1;
|
|
318
|
+
|
|
319
|
+
case "Double":
|
|
320
|
+
case "double":
|
|
321
|
+
case "Float":
|
|
322
|
+
case "float":
|
|
323
|
+
case "BigDecimal":
|
|
324
|
+
if (field.name.toLowerCase().includes("price")) return 99.99;
|
|
325
|
+
if (field.name.toLowerCase().includes("amount")) return 100.00;
|
|
326
|
+
return 0.0;
|
|
327
|
+
|
|
328
|
+
case "Boolean":
|
|
329
|
+
case "boolean":
|
|
330
|
+
return true;
|
|
331
|
+
|
|
332
|
+
case "List":
|
|
333
|
+
case "Set":
|
|
334
|
+
case "Collection":
|
|
335
|
+
return [];
|
|
336
|
+
|
|
337
|
+
case "Map":
|
|
338
|
+
return {};
|
|
339
|
+
|
|
340
|
+
case "Date":
|
|
341
|
+
case "LocalDate":
|
|
342
|
+
return "2024-01-15";
|
|
343
|
+
|
|
344
|
+
case "LocalDateTime":
|
|
345
|
+
case "Instant":
|
|
346
|
+
case "ZonedDateTime":
|
|
347
|
+
return "2024-01-15T10:30:00Z";
|
|
348
|
+
|
|
349
|
+
case "UUID":
|
|
350
|
+
return "550e8400-e29b-41d4-a716-446655440000";
|
|
351
|
+
|
|
352
|
+
default:
|
|
353
|
+
// Complex type - return empty object
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Find DTO by class name
|
|
360
|
+
*/
|
|
361
|
+
async findDtoByClassName(className, files) {
|
|
362
|
+
// Clean class name (remove generics)
|
|
363
|
+
const cleanName = className.replace(/<.*>/, "").replace("ResponseEntity", "").trim();
|
|
364
|
+
|
|
365
|
+
if (!cleanName || cleanName === "void" || cleanName === "Object") {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Look for file with this class name
|
|
370
|
+
for (const file of files) {
|
|
371
|
+
const filePath = file.path || file;
|
|
372
|
+
const fileName = path.basename(filePath, ".java");
|
|
373
|
+
|
|
374
|
+
if (fileName === cleanName) {
|
|
375
|
+
return this.analyzeDto(filePath);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Analyze all DTOs in workspace
|
|
384
|
+
*/
|
|
385
|
+
async analyzeAll(files) {
|
|
386
|
+
const dtoPaths = await this.findDtoFiles(files);
|
|
387
|
+
const dtos = [];
|
|
388
|
+
|
|
389
|
+
for (const dtoPath of dtoPaths) {
|
|
390
|
+
const dto = await this.analyzeDto(dtoPath);
|
|
391
|
+
if (dto) {
|
|
392
|
+
dtos.push(dto);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return dtos;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Generate complete API payload documentation
|
|
401
|
+
*/
|
|
402
|
+
async generatePayloadDocs(files, endpoints) {
|
|
403
|
+
const dtos = await this.analyzeAll(files);
|
|
404
|
+
const dtoMap = new Map(dtos.map(d => [d.className, d]));
|
|
405
|
+
|
|
406
|
+
// Enrich endpoints with payload info
|
|
407
|
+
const enrichedEndpoints = [];
|
|
408
|
+
|
|
409
|
+
for (const endpoint of endpoints) {
|
|
410
|
+
const enriched = { ...endpoint };
|
|
411
|
+
|
|
412
|
+
// Find request body DTO
|
|
413
|
+
if (endpoint.requestBody) {
|
|
414
|
+
const requestDto = dtoMap.get(endpoint.requestBody.type);
|
|
415
|
+
if (requestDto) {
|
|
416
|
+
enriched.requestPayload = {
|
|
417
|
+
dto: requestDto.className,
|
|
418
|
+
fields: requestDto.fields,
|
|
419
|
+
example: requestDto.examplePayload
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Find response DTO
|
|
425
|
+
if (endpoint.returnType) {
|
|
426
|
+
const returnTypeName = endpoint.returnType
|
|
427
|
+
.replace(/ResponseEntity</, "")
|
|
428
|
+
.replace(/Mono</, "")
|
|
429
|
+
.replace(/Flux</, "")
|
|
430
|
+
.replace(/<.*>/, "")
|
|
431
|
+
.replace(/>/, "")
|
|
432
|
+
.trim();
|
|
433
|
+
|
|
434
|
+
const responseDto = dtoMap.get(returnTypeName);
|
|
435
|
+
if (responseDto) {
|
|
436
|
+
enriched.responsePayload = {
|
|
437
|
+
dto: responseDto.className,
|
|
438
|
+
fields: responseDto.fields,
|
|
439
|
+
example: responseDto.examplePayload
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
enrichedEndpoints.push(enriched);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
totalDtos: dtos.length,
|
|
449
|
+
dtos: dtos,
|
|
450
|
+
endpoints: enrichedEndpoints
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export default DTOAnalyzer;
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export class FrameworkDetector {
|
|
2
|
+
constructor(language, files, fileReader) {
|
|
3
|
+
this.language = language;
|
|
4
|
+
this.files = files;
|
|
5
|
+
this.fileReader = fileReader;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async detect() {
|
|
9
|
+
switch (this.language) {
|
|
10
|
+
case 'java':
|
|
11
|
+
return this.detectJavaFramework();
|
|
12
|
+
case 'node':
|
|
13
|
+
return this.detectNodeFramework();
|
|
14
|
+
case 'python':
|
|
15
|
+
return this.detectPythonFramework();
|
|
16
|
+
case 'dotnet':
|
|
17
|
+
return { framework: 'dotnet', version: null, buildTool: 'dotnet' };
|
|
18
|
+
case 'go':
|
|
19
|
+
return { framework: 'go', version: null, buildTool: 'go' };
|
|
20
|
+
default:
|
|
21
|
+
return { framework: 'unknown', version: null, buildTool: null };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async detectJavaFramework() {
|
|
26
|
+
const pomFile = this.files.find(f => f.name === 'pom.xml');
|
|
27
|
+
if (pomFile) {
|
|
28
|
+
const content = await this.fileReader.read(pomFile.path);
|
|
29
|
+
if (content.content.includes('spring-boot')) {
|
|
30
|
+
const version = this.extractVersion(content.content, 'spring-boot-starter-parent');
|
|
31
|
+
return { framework: 'spring-boot', version, buildTool: 'maven' };
|
|
32
|
+
}
|
|
33
|
+
if (content.content.includes('quarkus')) {
|
|
34
|
+
return { framework: 'quarkus', version: null, buildTool: 'maven' };
|
|
35
|
+
}
|
|
36
|
+
return { framework: 'java-maven', version: null, buildTool: 'maven' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const gradleFile = this.files.find(f => f.name === 'build.gradle' || f.name === 'build.gradle.kts');
|
|
40
|
+
if (gradleFile) {
|
|
41
|
+
const content = await this.fileReader.read(gradleFile.path);
|
|
42
|
+
if (content.content.includes('spring-boot') || content.content.includes('org.springframework.boot')) {
|
|
43
|
+
return { framework: 'spring-boot', version: null, buildTool: 'gradle' };
|
|
44
|
+
}
|
|
45
|
+
return { framework: 'java-gradle', version: null, buildTool: 'gradle' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { framework: 'java-plain', version: null, buildTool: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async detectNodeFramework() {
|
|
52
|
+
const pkgFile = this.files.find(f => f.name === 'package.json');
|
|
53
|
+
if (!pkgFile) return { framework: 'unknown', version: null, buildTool: 'npm' };
|
|
54
|
+
|
|
55
|
+
const content = await this.fileReader.read(pkgFile.path);
|
|
56
|
+
const pkg = JSON.parse(content.content);
|
|
57
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
58
|
+
|
|
59
|
+
if (deps['next']) return { framework: 'next', version: deps['next'], buildTool: 'npm' };
|
|
60
|
+
if (deps['express']) return { framework: 'express', version: deps['express'], buildTool: 'npm' };
|
|
61
|
+
if (deps['react']) return { framework: 'react', version: deps['react'], buildTool: 'npm' };
|
|
62
|
+
if (deps['vue']) return { framework: 'vue', version: deps['vue'], buildTool: 'npm' };
|
|
63
|
+
if (deps['@nestjs/core']) return { framework: 'nestjs', version: deps['@nestjs/core'], buildTool: 'npm' };
|
|
64
|
+
if (deps['@angular/core']) return { framework: 'angular', version: deps['@angular/core'], buildTool: 'npm' };
|
|
65
|
+
|
|
66
|
+
return { framework: 'node-plain', version: null, buildTool: 'npm' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async detectPythonFramework() {
|
|
70
|
+
const reqFile = this.files.find(f => f.name === 'requirements.txt');
|
|
71
|
+
if (reqFile) {
|
|
72
|
+
const content = await this.fileReader.read(reqFile.path);
|
|
73
|
+
if (content.content.includes('django')) return { framework: 'django', version: null, buildTool: 'pip' };
|
|
74
|
+
if (content.content.includes('flask')) return { framework: 'flask', version: null, buildTool: 'pip' };
|
|
75
|
+
if (content.content.includes('fastapi')) return { framework: 'fastapi', version: null, buildTool: 'pip' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pyprojectFile = this.files.find(f => f.name === 'pyproject.toml');
|
|
79
|
+
if (pyprojectFile) {
|
|
80
|
+
return { framework: 'python-poetry', version: null, buildTool: 'poetry' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { framework: 'python-plain', version: null, buildTool: 'pip' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
extractVersion(xml, dependency) {
|
|
87
|
+
const regex = new RegExp(`<${dependency}>.*?<version>(.*?)</version>`, 's');
|
|
88
|
+
const match = xml.match(regex);
|
|
89
|
+
return match ? match[1] : null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export class LanguageDetector {
|
|
2
|
+
constructor(files) {
|
|
3
|
+
this.files = files;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
detect() {
|
|
7
|
+
const markers = this.findMarkers();
|
|
8
|
+
const extensions = this.analyzeExtensions();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
primary: this.determinePrimaryLanguage(markers, extensions),
|
|
12
|
+
secondary: this.findSecondaryLanguages(extensions),
|
|
13
|
+
confidence: this.calculateConfidence(markers)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
findMarkers() {
|
|
18
|
+
const markers = {
|
|
19
|
+
java: this.files.some(f => f.name === 'pom.xml' || f.name === 'build.gradle'),
|
|
20
|
+
node: this.files.some(f => f.name === 'package.json'),
|
|
21
|
+
python: this.files.some(f => f.name === 'requirements.txt' || f.name === 'pyproject.toml'),
|
|
22
|
+
dotnet: this.files.some(f => f.name.endsWith('.csproj') || f.name.endsWith('.sln')),
|
|
23
|
+
go: this.files.some(f => f.name === 'go.mod'),
|
|
24
|
+
php: this.files.some(f => f.name === 'composer.json'),
|
|
25
|
+
ruby: this.files.some(f => f.name === 'Gemfile')
|
|
26
|
+
};
|
|
27
|
+
return markers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
analyzeExtensions() {
|
|
31
|
+
const extCount = {};
|
|
32
|
+
this.files.forEach(f => {
|
|
33
|
+
const ext = f.extension?.toLowerCase();
|
|
34
|
+
if (ext) extCount[ext] = (extCount[ext] || 0) + 1;
|
|
35
|
+
});
|
|
36
|
+
return extCount;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
determinePrimaryLanguage(markers, extensions) {
|
|
40
|
+
// Prioridade: markers > extensões
|
|
41
|
+
const detected = Object.entries(markers)
|
|
42
|
+
.filter(([_, exists]) => exists)
|
|
43
|
+
.map(([lang]) => lang);
|
|
44
|
+
|
|
45
|
+
if (detected.length === 1) return detected[0];
|
|
46
|
+
if (detected.length > 1) {
|
|
47
|
+
// Múltiplas linguagens, escolhe por extensões
|
|
48
|
+
const sortedExts = Object.entries(extensions)
|
|
49
|
+
.sort((a, b) => b[1] - a[1]);
|
|
50
|
+
return this.mapExtToLang(sortedExts[0]?.[0]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback: extensão mais comum
|
|
54
|
+
const sortedExts = Object.entries(extensions)
|
|
55
|
+
.sort((a, b) => b[1] - a[1]);
|
|
56
|
+
return this.mapExtToLang(sortedExts[0]?.[0]) || 'unknown';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
mapExtToLang(ext) {
|
|
60
|
+
const map = {
|
|
61
|
+
'.java': 'java',
|
|
62
|
+
'.js': 'node',
|
|
63
|
+
'.ts': 'node',
|
|
64
|
+
'.jsx': 'node',
|
|
65
|
+
'.tsx': 'node',
|
|
66
|
+
'.py': 'python',
|
|
67
|
+
'.cs': 'dotnet',
|
|
68
|
+
'.go': 'go',
|
|
69
|
+
'.php': 'php',
|
|
70
|
+
'.rb': 'ruby'
|
|
71
|
+
};
|
|
72
|
+
return map[ext] || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
findSecondaryLanguages(extensions) {
|
|
76
|
+
const primary = this.determinePrimaryLanguage(this.findMarkers(), extensions);
|
|
77
|
+
return Object.keys(extensions)
|
|
78
|
+
.map(ext => this.mapExtToLang(ext))
|
|
79
|
+
.filter(lang => lang && lang !== primary)
|
|
80
|
+
.filter((v, i, a) => a.indexOf(v) === i); // unique
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
calculateConfidence(markers) {
|
|
84
|
+
const markerCount = Object.values(markers).filter(Boolean).length;
|
|
85
|
+
if (markerCount === 0) return 0.3;
|
|
86
|
+
if (markerCount === 1) return 0.9;
|
|
87
|
+
return 0.7; // Múltiplos markers = projeto polyglot
|
|
88
|
+
}
|
|
89
|
+
}
|