deepdebug-local-agent 0.3.8 → 0.3.10
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 +3 -2
- package/patch.js +607 -0
- package/ports.js +69 -0
- 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,429 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { readFile } from "../fs-utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ControllerAnalyzer - ENHANCED VERSION
|
|
6
|
+
*
|
|
7
|
+
* Analyzes Java Spring Boot controllers to extract:
|
|
8
|
+
* - REST endpoints (@GetMapping, @PostMapping, etc.)
|
|
9
|
+
* - Request/Response DTOs
|
|
10
|
+
* - Path variables and query parameters
|
|
11
|
+
* - HTTP methods and paths
|
|
12
|
+
* - Handles constants and complex path expressions
|
|
13
|
+
*/
|
|
14
|
+
export class ControllerAnalyzer {
|
|
15
|
+
constructor(workspaceRoot) {
|
|
16
|
+
this.workspaceRoot = workspaceRoot;
|
|
17
|
+
this.pathConstants = new Map(); // Store resolved path constants
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find all controller files in the workspace
|
|
22
|
+
*/
|
|
23
|
+
async findControllers(files) {
|
|
24
|
+
const controllerFiles = files.filter(file => {
|
|
25
|
+
const fileName = path.basename(file.path || file);
|
|
26
|
+
return fileName.endsWith("Controller.java") ||
|
|
27
|
+
fileName.endsWith("Resource.java") ||
|
|
28
|
+
fileName.endsWith("Api.java");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return controllerFiles.map(f => f.path || f);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Try to resolve path constants from PathAPI or similar classes
|
|
36
|
+
*/
|
|
37
|
+
async resolvePathConstants(files) {
|
|
38
|
+
// Look for PathAPI, ApiPaths, Routes, etc.
|
|
39
|
+
const pathFiles = files.filter(file => {
|
|
40
|
+
const filePath = file.path || file;
|
|
41
|
+
const fileName = path.basename(filePath);
|
|
42
|
+
return fileName.includes("Path") ||
|
|
43
|
+
fileName.includes("Route") ||
|
|
44
|
+
fileName.includes("Api") && fileName.endsWith(".java") &&
|
|
45
|
+
!fileName.includes("Controller");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const file of pathFiles) {
|
|
49
|
+
try {
|
|
50
|
+
const filePath = file.path || file;
|
|
51
|
+
const fullPath = path.join(this.workspaceRoot, filePath);
|
|
52
|
+
const content = await readFile(fullPath, "utf8");
|
|
53
|
+
|
|
54
|
+
// Extract constants like: public static final String CORE = "/v1";
|
|
55
|
+
const constantRegex = /(?:public\s+)?static\s+final\s+String\s+(\w+)\s*=\s*"([^"]+)"/g;
|
|
56
|
+
let match;
|
|
57
|
+
while ((match = constantRegex.exec(content)) !== null) {
|
|
58
|
+
this.pathConstants.set(match[1], match[2]);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Ignore errors reading path files
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Common defaults
|
|
66
|
+
if (!this.pathConstants.has("ID")) {
|
|
67
|
+
this.pathConstants.set("ID", "/{id}");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a path expression that may contain constants
|
|
73
|
+
*/
|
|
74
|
+
resolvePath(pathExpr) {
|
|
75
|
+
if (!pathExpr) return "";
|
|
76
|
+
|
|
77
|
+
// Handle string literals
|
|
78
|
+
if (pathExpr.startsWith('"') && pathExpr.endsWith('"')) {
|
|
79
|
+
return pathExpr.slice(1, -1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle constant references like PathAPI.CORE + PathAPI.AGREEMENT
|
|
83
|
+
let resolved = pathExpr;
|
|
84
|
+
|
|
85
|
+
// Replace constant references
|
|
86
|
+
for (const [name, value] of this.pathConstants) {
|
|
87
|
+
// Match patterns like PathAPI.NAME or ClassName.NAME
|
|
88
|
+
const pattern = new RegExp(`\\w+\\.${name}\\b`, 'g');
|
|
89
|
+
resolved = resolved.replace(pattern, `"${value}"`);
|
|
90
|
+
|
|
91
|
+
// Also match just the constant name
|
|
92
|
+
const simplePattern = new RegExp(`\\b${name}\\b`, 'g');
|
|
93
|
+
resolved = resolved.replace(simplePattern, `"${value}"`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Now concatenate string parts
|
|
97
|
+
// "a" + "b" + "c" -> "abc"
|
|
98
|
+
const stringParts = resolved.match(/"([^"]*)"/g);
|
|
99
|
+
if (stringParts) {
|
|
100
|
+
return stringParts.map(s => s.slice(1, -1)).join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// If we couldn't resolve, return as-is or empty
|
|
104
|
+
return pathExpr.includes('.') ? "" : pathExpr;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Analyze a single controller file
|
|
109
|
+
*/
|
|
110
|
+
async analyzeController(filePath) {
|
|
111
|
+
try {
|
|
112
|
+
const fullPath = path.join(this.workspaceRoot, filePath);
|
|
113
|
+
const content = await readFile(fullPath, "utf8");
|
|
114
|
+
|
|
115
|
+
const controller = {
|
|
116
|
+
file: filePath,
|
|
117
|
+
className: this.extractClassName(content),
|
|
118
|
+
basePath: this.extractBasePath(content),
|
|
119
|
+
endpoints: this.extractEndpoints(content),
|
|
120
|
+
imports: this.extractImports(content)
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return controller;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`Failed to analyze controller ${filePath}:`, err.message);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract class name from Java file
|
|
132
|
+
*/
|
|
133
|
+
extractClassName(content) {
|
|
134
|
+
const match = content.match(/public\s+class\s+(\w+)/);
|
|
135
|
+
return match ? match[1] : "Unknown";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract base path from @RequestMapping annotation on class
|
|
140
|
+
*/
|
|
141
|
+
extractBasePath(content) {
|
|
142
|
+
// Find @RequestMapping on class level (before the class declaration)
|
|
143
|
+
const classMatch = content.match(/(@RequestMapping[^)]*\))\s*(?:@\w+[^)]*\)\s*)*public\s+class/s);
|
|
144
|
+
|
|
145
|
+
if (classMatch) {
|
|
146
|
+
const annotation = classMatch[1];
|
|
147
|
+
|
|
148
|
+
// @RequestMapping("/api/v1/users")
|
|
149
|
+
let match = annotation.match(/@RequestMapping\s*\(\s*"([^"]+)"\s*\)/);
|
|
150
|
+
if (match) return match[1];
|
|
151
|
+
|
|
152
|
+
// @RequestMapping(value = "/api/v1/users")
|
|
153
|
+
match = annotation.match(/@RequestMapping\s*\([^)]*value\s*=\s*"([^"]+)"/);
|
|
154
|
+
if (match) return match[1];
|
|
155
|
+
|
|
156
|
+
// @RequestMapping(path = "/api/v1/users")
|
|
157
|
+
match = annotation.match(/@RequestMapping\s*\([^)]*path\s*=\s*"([^"]+)"/);
|
|
158
|
+
if (match) return match[1];
|
|
159
|
+
|
|
160
|
+
// @RequestMapping(PathAPI.CORE + PathAPI.USERS)
|
|
161
|
+
match = annotation.match(/@RequestMapping\s*\(([^)]+)\)/);
|
|
162
|
+
if (match) {
|
|
163
|
+
const pathExpr = match[1].trim();
|
|
164
|
+
if (!pathExpr.includes("=") || pathExpr.includes("value")) {
|
|
165
|
+
// Extract the value part if present
|
|
166
|
+
const valueMatch = pathExpr.match(/value\s*=\s*([^,)]+)/);
|
|
167
|
+
const resolved = this.resolvePath(valueMatch ? valueMatch[1].trim() : pathExpr);
|
|
168
|
+
if (resolved) return resolved;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract all endpoint mappings from controller
|
|
178
|
+
*/
|
|
179
|
+
extractEndpoints(content) {
|
|
180
|
+
const endpoints = [];
|
|
181
|
+
|
|
182
|
+
// Find all methods with mapping annotations
|
|
183
|
+
const methodPatterns = [
|
|
184
|
+
{ regex: /@GetMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "GET" },
|
|
185
|
+
{ regex: /@PostMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "POST" },
|
|
186
|
+
{ regex: /@PutMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "PUT" },
|
|
187
|
+
{ regex: /@DeleteMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "DELETE" },
|
|
188
|
+
{ regex: /@PatchMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "PATCH" }
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (const { regex, method } of methodPatterns) {
|
|
192
|
+
let match;
|
|
193
|
+
const regexCopy = new RegExp(regex.source, 'g');
|
|
194
|
+
|
|
195
|
+
while ((match = regexCopy.exec(content)) !== null) {
|
|
196
|
+
const annotationEnd = match.index + match[0].length;
|
|
197
|
+
|
|
198
|
+
// Get the method block that follows this annotation
|
|
199
|
+
const methodBlock = content.substring(match.index, Math.min(annotationEnd + 500, content.length));
|
|
200
|
+
|
|
201
|
+
// Extract path from annotation
|
|
202
|
+
let endpointPath = "";
|
|
203
|
+
const annotationContent = match[1] || "";
|
|
204
|
+
|
|
205
|
+
if (annotationContent) {
|
|
206
|
+
// Handle: @GetMapping("/{id}")
|
|
207
|
+
const stringMatch = annotationContent.match(/^"([^"]*)"$/);
|
|
208
|
+
if (stringMatch) {
|
|
209
|
+
endpointPath = stringMatch[1];
|
|
210
|
+
} else {
|
|
211
|
+
// Handle: @GetMapping(value = "/{id}")
|
|
212
|
+
const valueMatch = annotationContent.match(/value\s*=\s*"([^"]+)"/);
|
|
213
|
+
if (valueMatch) {
|
|
214
|
+
endpointPath = valueMatch[1];
|
|
215
|
+
} else {
|
|
216
|
+
// Handle: @GetMapping(PathAPI.ID)
|
|
217
|
+
endpointPath = this.resolvePath(annotationContent.trim());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const endpoint = this.parseMethodBlock(methodBlock, method, endpointPath);
|
|
223
|
+
if (endpoint) {
|
|
224
|
+
endpoints.push(endpoint);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Also check for @RequestMapping with method attribute
|
|
230
|
+
const requestMappingRegex = /@RequestMapping\s*\([^)]*method\s*=\s*(?:RequestMethod\.)?(\w+)[^)]*\)/g;
|
|
231
|
+
let match;
|
|
232
|
+
while ((match = requestMappingRegex.exec(content)) !== null) {
|
|
233
|
+
const httpMethod = match[1];
|
|
234
|
+
const annotationEnd = match.index + match[0].length;
|
|
235
|
+
const methodBlock = content.substring(match.index, Math.min(annotationEnd + 500, content.length));
|
|
236
|
+
|
|
237
|
+
// Extract path
|
|
238
|
+
const pathMatch = match[0].match(/value\s*=\s*"([^"]+)"|path\s*=\s*"([^"]+)"/);
|
|
239
|
+
const endpointPath = pathMatch ? (pathMatch[1] || pathMatch[2]) : "";
|
|
240
|
+
|
|
241
|
+
const endpoint = this.parseMethodBlock(methodBlock, httpMethod, endpointPath);
|
|
242
|
+
if (endpoint) {
|
|
243
|
+
endpoints.push(endpoint);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return endpoints;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parse a method block to extract endpoint information
|
|
252
|
+
*/
|
|
253
|
+
parseMethodBlock(methodBlock, httpMethod, endpointPath) {
|
|
254
|
+
// Find the method signature
|
|
255
|
+
// Pattern: returnType methodName(params)
|
|
256
|
+
const methodMatch = methodBlock.match(/(?:public|private|protected)\s+([\w<>,\s?]+?)\s+(\w+)\s*\(([^)]*)\)/);
|
|
257
|
+
|
|
258
|
+
if (!methodMatch) return null;
|
|
259
|
+
|
|
260
|
+
const returnType = methodMatch[1].trim();
|
|
261
|
+
const methodName = methodMatch[2];
|
|
262
|
+
const paramsString = methodMatch[3];
|
|
263
|
+
|
|
264
|
+
// Parse parameters
|
|
265
|
+
const requestBody = this.extractRequestBody(paramsString);
|
|
266
|
+
const pathVariables = this.extractPathVariables(paramsString);
|
|
267
|
+
const queryParams = this.extractQueryParams(paramsString);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
method: httpMethod,
|
|
271
|
+
path: endpointPath,
|
|
272
|
+
methodName: methodName,
|
|
273
|
+
returnType: this.cleanReturnType(returnType),
|
|
274
|
+
requestBody: requestBody,
|
|
275
|
+
pathVariables: pathVariables,
|
|
276
|
+
queryParams: queryParams
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Clean up return type (remove Mono/Flux wrappers, etc.)
|
|
282
|
+
*/
|
|
283
|
+
cleanReturnType(returnType) {
|
|
284
|
+
let clean = returnType
|
|
285
|
+
.replace(/Mono<(.+)>/, "$1")
|
|
286
|
+
.replace(/Flux<(.+)>/, "List<$1>")
|
|
287
|
+
.replace(/ResponseEntity<(.+)>/, "$1")
|
|
288
|
+
.replace(/CompletableFuture<(.+)>/, "$1")
|
|
289
|
+
.trim();
|
|
290
|
+
return clean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Extract @RequestBody parameter
|
|
295
|
+
*/
|
|
296
|
+
extractRequestBody(paramsString) {
|
|
297
|
+
// Match @RequestBody Type name or @RequestBody @Valid Type name
|
|
298
|
+
const match = paramsString.match(/@RequestBody\s+(?:@\w+\s+)*(\w+)\s+(\w+)/);
|
|
299
|
+
if (match) {
|
|
300
|
+
return {
|
|
301
|
+
type: match[1],
|
|
302
|
+
name: match[2]
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Extract @PathVariable parameters
|
|
310
|
+
*/
|
|
311
|
+
extractPathVariables(paramsString) {
|
|
312
|
+
const pathVars = [];
|
|
313
|
+
|
|
314
|
+
// @PathVariable("id") Long id
|
|
315
|
+
// @PathVariable Long id
|
|
316
|
+
// @PathVariable(value = "id") Long id
|
|
317
|
+
const regex = /@PathVariable(?:\s*\(\s*(?:value\s*=\s*)?["']?(\w+)["']?\s*\))?\s+(\w+)\s+(\w+)/g;
|
|
318
|
+
let match;
|
|
319
|
+
while ((match = regex.exec(paramsString)) !== null) {
|
|
320
|
+
pathVars.push({
|
|
321
|
+
name: match[1] || match[3],
|
|
322
|
+
type: match[2],
|
|
323
|
+
paramName: match[3]
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return pathVars;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Extract @RequestParam parameters
|
|
331
|
+
*/
|
|
332
|
+
extractQueryParams(paramsString) {
|
|
333
|
+
const queryParams = [];
|
|
334
|
+
|
|
335
|
+
const regex = /@RequestParam(?:\s*\([^)]*\))?\s+(\w+)\s+(\w+)/g;
|
|
336
|
+
let match;
|
|
337
|
+
while ((match = regex.exec(paramsString)) !== null) {
|
|
338
|
+
queryParams.push({
|
|
339
|
+
name: match[2],
|
|
340
|
+
type: match[1],
|
|
341
|
+
paramName: match[2],
|
|
342
|
+
required: !paramsString.includes("required = false")
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return queryParams;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Extract imports to understand DTOs
|
|
350
|
+
*/
|
|
351
|
+
extractImports(content) {
|
|
352
|
+
const imports = [];
|
|
353
|
+
const regex = /import\s+([\w.]+);/g;
|
|
354
|
+
let match;
|
|
355
|
+
while ((match = regex.exec(content)) !== null) {
|
|
356
|
+
imports.push(match[1]);
|
|
357
|
+
}
|
|
358
|
+
return imports;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Analyze all controllers in workspace
|
|
363
|
+
*/
|
|
364
|
+
async analyzeAll(files) {
|
|
365
|
+
// First, try to resolve path constants
|
|
366
|
+
await this.resolvePathConstants(files);
|
|
367
|
+
|
|
368
|
+
const controllerPaths = await this.findControllers(files);
|
|
369
|
+
const controllers = [];
|
|
370
|
+
|
|
371
|
+
for (const controllerPath of controllerPaths) {
|
|
372
|
+
const controller = await this.analyzeController(controllerPath);
|
|
373
|
+
if (controller) {
|
|
374
|
+
controllers.push(controller);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return controllers;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Generate full API documentation
|
|
383
|
+
*/
|
|
384
|
+
async generateApiDocs(files) {
|
|
385
|
+
const controllers = await this.analyzeAll(files);
|
|
386
|
+
|
|
387
|
+
const endpoints = [];
|
|
388
|
+
for (const controller of controllers) {
|
|
389
|
+
const basePath = controller.basePath;
|
|
390
|
+
|
|
391
|
+
for (const endpoint of controller.endpoints) {
|
|
392
|
+
const fullPath = this.combinePaths(basePath, endpoint.path);
|
|
393
|
+
endpoints.push({
|
|
394
|
+
controller: controller.className,
|
|
395
|
+
file: controller.file,
|
|
396
|
+
method: endpoint.method,
|
|
397
|
+
path: fullPath,
|
|
398
|
+
methodName: endpoint.methodName,
|
|
399
|
+
returnType: endpoint.returnType,
|
|
400
|
+
requestBody: endpoint.requestBody,
|
|
401
|
+
pathVariables: endpoint.pathVariables,
|
|
402
|
+
queryParams: endpoint.queryParams
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
totalControllers: controllers.length,
|
|
409
|
+
totalEndpoints: endpoints.length,
|
|
410
|
+
controllers: controllers,
|
|
411
|
+
endpoints: endpoints
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Combine base path and endpoint path
|
|
417
|
+
*/
|
|
418
|
+
combinePaths(basePath, endpointPath) {
|
|
419
|
+
if (!basePath && !endpointPath) return "/";
|
|
420
|
+
if (!basePath) return endpointPath.startsWith("/") ? endpointPath : "/" + endpointPath;
|
|
421
|
+
if (!endpointPath) return basePath.startsWith("/") ? basePath : "/" + basePath;
|
|
422
|
+
|
|
423
|
+
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
424
|
+
const endpoint = endpointPath.startsWith("/") ? endpointPath : "/" + endpointPath;
|
|
425
|
+
return base + endpoint;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export default ControllerAnalyzer;
|