deepdebug-local-agent 0.3.1
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/.dockerignore +24 -0
- package/.idea/deepdebug-local-agent.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/Dockerfile +46 -0
- package/cloudbuild.yaml +42 -0
- package/index.js +42 -0
- package/mcp-server.js +533 -0
- package/package.json +22 -0
- package/src/ai-engine.js +861 -0
- package/src/analyzers/config-analyzer.js +446 -0
- package/src/analyzers/controller-analyzer.js +429 -0
- package/src/analyzers/dto-analyzer.js +455 -0
- package/src/detectors/build-tool-detector.js +0 -0
- package/src/detectors/framework-detector.js +91 -0
- package/src/detectors/language-detector.js +89 -0
- package/src/detectors/multi-project-detector.js +191 -0
- package/src/detectors/service-detector.js +244 -0
- package/src/detectors.js +30 -0
- package/src/exec-utils.js +215 -0
- package/src/fs-utils.js +34 -0
- package/src/git/base-git-provider.js +384 -0
- package/src/git/git-provider-registry.js +110 -0
- package/src/git/github-provider.js +502 -0
- package/src/mcp-http-server.js +313 -0
- package/src/patch/patch-engine.js +339 -0
- package/src/patch-manager.js +816 -0
- package/src/patch.js +607 -0
- package/src/patch_bkp.js +154 -0
- package/src/ports.js +69 -0
- package/src/routes/workspace.route.js +528 -0
- package/src/runtimes/base-runtime.js +290 -0
- package/src/runtimes/java/gradle-runtime.js +378 -0
- package/src/runtimes/java/java-integrations.js +339 -0
- package/src/runtimes/java/maven-runtime.js +418 -0
- package/src/runtimes/node/node-integrations.js +247 -0
- package/src/runtimes/node/npm-runtime.js +466 -0
- package/src/runtimes/node/yarn-runtime.js +354 -0
- package/src/runtimes/runtime-registry.js +256 -0
- package/src/server-local.js +576 -0
- package/src/server.js +4565 -0
- package/src/utils/environment-diagnostics.js +666 -0
- package/src/utils/exec-utils.js +264 -0
- package/src/utils/fs-utils.js +218 -0
- package/src/workspace/detect-port.js +176 -0
- package/src/workspace/file-reader.js +54 -0
- package/src/workspace/git-client.js +0 -0
- package/src/workspace/process-manager.js +619 -0
- package/src/workspace/scanner.js +72 -0
- package/src/workspace-manager.js +172 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { BaseRuntime } from '../base-runtime.js';
|
|
3
|
+
import { exists, readFile } from '../../utils/fs-utils.js';
|
|
4
|
+
import { run } from '../../utils/exec-utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MavenRuntime
|
|
8
|
+
*
|
|
9
|
+
* Runtime para projetos Java com Maven.
|
|
10
|
+
* Suporta Spring Boot, Quarkus, e projetos Maven genéricos.
|
|
11
|
+
*/
|
|
12
|
+
export class MavenRuntime extends BaseRuntime {
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.framework = null; // Detectado dinamicamente
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ==========================================
|
|
20
|
+
// IDENTIFICATION
|
|
21
|
+
// ==========================================
|
|
22
|
+
|
|
23
|
+
getId() {
|
|
24
|
+
return 'java-maven';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getName() {
|
|
28
|
+
return 'Java (Maven)';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getLanguage() {
|
|
32
|
+
return 'java';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getBuildTool() {
|
|
36
|
+
return 'maven';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ==========================================
|
|
40
|
+
// DETECTION
|
|
41
|
+
// ==========================================
|
|
42
|
+
|
|
43
|
+
getMarkerFiles() {
|
|
44
|
+
return ['pom.xml'];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async isCompatible(workspacePath) {
|
|
48
|
+
const pomPath = path.join(workspacePath, 'pom.xml');
|
|
49
|
+
return exists(pomPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async detectProject(workspacePath) {
|
|
53
|
+
const pomPath = path.join(workspacePath, 'pom.xml');
|
|
54
|
+
const pomContent = await readFile(pomPath, 'utf8');
|
|
55
|
+
|
|
56
|
+
// Parse básico do pom.xml
|
|
57
|
+
const name = this.extractXmlValue(pomContent, 'artifactId') ||
|
|
58
|
+
path.basename(workspacePath);
|
|
59
|
+
const version = this.extractXmlValue(pomContent, 'version') || '0.0.1';
|
|
60
|
+
|
|
61
|
+
// Detectar framework
|
|
62
|
+
const framework = this.detectFramework(pomContent);
|
|
63
|
+
this.framework = framework;
|
|
64
|
+
|
|
65
|
+
// Detectar porta
|
|
66
|
+
const port = await this.detectPort(workspacePath);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
version,
|
|
71
|
+
language: 'java',
|
|
72
|
+
framework,
|
|
73
|
+
buildTool: 'maven',
|
|
74
|
+
port
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
detectFramework(pomContent) {
|
|
79
|
+
if (pomContent.includes('spring-boot-starter') ||
|
|
80
|
+
pomContent.includes('org.springframework.boot')) {
|
|
81
|
+
return 'spring-boot';
|
|
82
|
+
}
|
|
83
|
+
if (pomContent.includes('quarkus')) {
|
|
84
|
+
return 'quarkus';
|
|
85
|
+
}
|
|
86
|
+
if (pomContent.includes('micronaut')) {
|
|
87
|
+
return 'micronaut';
|
|
88
|
+
}
|
|
89
|
+
if (pomContent.includes('jakarta.') || pomContent.includes('javax.')) {
|
|
90
|
+
return 'jakarta-ee';
|
|
91
|
+
}
|
|
92
|
+
return 'java-plain';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extrai valor XML ignorando o bloco <parent>
|
|
97
|
+
* Isso é necessário porque o <parent> tem seu próprio artifactId/version
|
|
98
|
+
*/
|
|
99
|
+
extractXmlValue(xml, tag) {
|
|
100
|
+
// Remove o bloco <parent>...</parent> para não pegar valores do parent
|
|
101
|
+
const xmlWithoutParent = xml.replace(/<parent>[\s\S]*?<\/parent>/gi, '');
|
|
102
|
+
|
|
103
|
+
const regex = new RegExp(`<${tag}>([^<]+)</${tag}>`);
|
|
104
|
+
const match = xmlWithoutParent.match(regex);
|
|
105
|
+
return match ? match[1].trim() : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ==========================================
|
|
109
|
+
// BUILD & RUN
|
|
110
|
+
// ==========================================
|
|
111
|
+
|
|
112
|
+
async install(workspacePath, options = {}) {
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
const result = await run('mvn', ['dependency:resolve', '-q'], workspacePath);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...result,
|
|
118
|
+
duration: Date.now() - startTime
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async build(workspacePath, options = {}) {
|
|
123
|
+
const startTime = Date.now();
|
|
124
|
+
const skipTests = options.skipTests ? '-DskipTests' : '';
|
|
125
|
+
const args = ['compile', '-q'];
|
|
126
|
+
if (skipTests) args.push(skipTests);
|
|
127
|
+
|
|
128
|
+
const result = await run('mvn', args, workspacePath);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
...result,
|
|
132
|
+
duration: Date.now() - startTime
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getStartCommand(options = {}) {
|
|
137
|
+
const framework = options.framework || this.framework || 'spring-boot';
|
|
138
|
+
const port = options.port; // Pode ser undefined se não precisa sobrescrever
|
|
139
|
+
const profile = options.profile || 'default';
|
|
140
|
+
|
|
141
|
+
if (framework === 'spring-boot') {
|
|
142
|
+
const args = [
|
|
143
|
+
'spring-boot:run',
|
|
144
|
+
'-Dspring-boot.run.fork=false' // Mantém Java no mesmo processo
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// Só adiciona porta se foi explicitamente fornecida
|
|
148
|
+
// Se o projeto já tem porta no application.yml, não precisa
|
|
149
|
+
if (port) {
|
|
150
|
+
args.push(`-Dspring-boot.run.arguments=--server.port=${port}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Perfil ativo
|
|
154
|
+
if (profile !== 'default') {
|
|
155
|
+
args.push(`-Dspring.profiles.active=${profile}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
cmd: 'mvn',
|
|
160
|
+
args,
|
|
161
|
+
env: port ? { SERVER_PORT: String(port) } : {}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (framework === 'quarkus') {
|
|
166
|
+
return {
|
|
167
|
+
cmd: 'mvn',
|
|
168
|
+
args: [
|
|
169
|
+
'quarkus:dev',
|
|
170
|
+
`-Dquarkus.http.port=${port || 8080}`,
|
|
171
|
+
'-q'
|
|
172
|
+
],
|
|
173
|
+
env: {
|
|
174
|
+
QUARKUS_HTTP_PORT: String(port || 8080)
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fallback genérico
|
|
180
|
+
return {
|
|
181
|
+
cmd: 'mvn',
|
|
182
|
+
args: ['exec:java', '-q'],
|
|
183
|
+
env: {}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getReadyPattern() {
|
|
188
|
+
if (this.framework === 'spring-boot') {
|
|
189
|
+
return /Started \w+ in \d+\.?\d* seconds/i;
|
|
190
|
+
}
|
|
191
|
+
if (this.framework === 'quarkus') {
|
|
192
|
+
return /Quarkus .* started in/i;
|
|
193
|
+
}
|
|
194
|
+
return /started|listening|ready/i;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
getDefaultPort() {
|
|
198
|
+
if (this.framework === 'quarkus') {
|
|
199
|
+
return 8080;
|
|
200
|
+
}
|
|
201
|
+
return 8080;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async detectPort(workspacePath) {
|
|
205
|
+
// Tentar application.properties
|
|
206
|
+
const propsPath = path.join(workspacePath, 'src/main/resources/application.properties');
|
|
207
|
+
if (await exists(propsPath)) {
|
|
208
|
+
const content = await readFile(propsPath, 'utf8');
|
|
209
|
+
const port = this.extractPortFromProperties(content);
|
|
210
|
+
if (port) return port;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Tentar application.yml
|
|
214
|
+
const ymlPath = path.join(workspacePath, 'src/main/resources/application.yml');
|
|
215
|
+
if (await exists(ymlPath)) {
|
|
216
|
+
const content = await readFile(ymlPath, 'utf8');
|
|
217
|
+
const port = this.extractPortFromYaml(content);
|
|
218
|
+
if (port) return port;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Tentar application.yaml
|
|
222
|
+
const yamlPath = path.join(workspacePath, 'src/main/resources/application.yaml');
|
|
223
|
+
if (await exists(yamlPath)) {
|
|
224
|
+
const content = await readFile(yamlPath, 'utf8');
|
|
225
|
+
const port = this.extractPortFromYaml(content);
|
|
226
|
+
if (port) return port;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return this.getDefaultPort();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Extrai porta de application.properties
|
|
234
|
+
* Suporta: server.port=8090 ou server.port=${PORT:8090}
|
|
235
|
+
*/
|
|
236
|
+
extractPortFromProperties(content) {
|
|
237
|
+
// Primeiro tenta: server.port=8090
|
|
238
|
+
let match = content.match(/server\.port\s*=\s*(\d+)/);
|
|
239
|
+
if (match) return parseInt(match[1]);
|
|
240
|
+
|
|
241
|
+
// Depois tenta: server.port=${PORT:8090} ou ${SERVER_PORT:8090}
|
|
242
|
+
match = content.match(/server\.port\s*=\s*\$\{[^:}]+:(\d+)\}/);
|
|
243
|
+
if (match) return parseInt(match[1]);
|
|
244
|
+
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extrai porta de application.yml/yaml
|
|
250
|
+
* Suporta: port: 8090 ou port: ${PORT:8090}
|
|
251
|
+
*/
|
|
252
|
+
extractPortFromYaml(content) {
|
|
253
|
+
// Primeiro tenta: port: 8090
|
|
254
|
+
let match = content.match(/port:\s*(\d+)/);
|
|
255
|
+
if (match) return parseInt(match[1]);
|
|
256
|
+
|
|
257
|
+
// Depois tenta: port: ${PORT:8090} ou port: ${SERVER_PORT:8090}
|
|
258
|
+
match = content.match(/port:\s*\$\{[^:}]+:(\d+)\}/);
|
|
259
|
+
if (match) return parseInt(match[1]);
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ==========================================
|
|
265
|
+
// TEST
|
|
266
|
+
// ==========================================
|
|
267
|
+
|
|
268
|
+
async test(workspacePath, options = {}) {
|
|
269
|
+
const startTime = Date.now();
|
|
270
|
+
|
|
271
|
+
// Clean primeiro
|
|
272
|
+
await run('mvn', ['clean', '-q'], workspacePath);
|
|
273
|
+
|
|
274
|
+
// Rodar testes
|
|
275
|
+
const result = await run('mvn', ['test', '-q'], workspacePath);
|
|
276
|
+
|
|
277
|
+
const testResult = this.parseTestOutput(result);
|
|
278
|
+
testResult.duration = Date.now() - startTime;
|
|
279
|
+
|
|
280
|
+
return testResult;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async testSingle(workspacePath, testFile) {
|
|
284
|
+
const startTime = Date.now();
|
|
285
|
+
|
|
286
|
+
// Extrair nome da classe do caminho
|
|
287
|
+
const className = path.basename(testFile, '.java');
|
|
288
|
+
|
|
289
|
+
const result = await run('mvn', [
|
|
290
|
+
'test',
|
|
291
|
+
`-Dtest=${className}`,
|
|
292
|
+
'-q'
|
|
293
|
+
], workspacePath);
|
|
294
|
+
|
|
295
|
+
const testResult = this.parseTestOutput(result);
|
|
296
|
+
testResult.duration = Date.now() - startTime;
|
|
297
|
+
|
|
298
|
+
return testResult;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
parseTestOutput(result) {
|
|
302
|
+
const output = result.stdout + result.stderr;
|
|
303
|
+
|
|
304
|
+
// Tentar parsear output do Surefire/JUnit
|
|
305
|
+
const testsRunMatch = output.match(/Tests run: (\d+)/);
|
|
306
|
+
const failuresMatch = output.match(/Failures: (\d+)/);
|
|
307
|
+
const errorsMatch = output.match(/Errors: (\d+)/);
|
|
308
|
+
const skippedMatch = output.match(/Skipped: (\d+)/);
|
|
309
|
+
|
|
310
|
+
const total = testsRunMatch ? parseInt(testsRunMatch[1]) : 0;
|
|
311
|
+
const failures = failuresMatch ? parseInt(failuresMatch[1]) : 0;
|
|
312
|
+
const errors = errorsMatch ? parseInt(errorsMatch[1]) : 0;
|
|
313
|
+
const skipped = skippedMatch ? parseInt(skippedMatch[1]) : 0;
|
|
314
|
+
const passed = total - failures - errors - skipped;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: result.code === 0 && failures === 0 && errors === 0,
|
|
318
|
+
passed,
|
|
319
|
+
failed: failures + errors,
|
|
320
|
+
skipped,
|
|
321
|
+
duration: 0,
|
|
322
|
+
output,
|
|
323
|
+
failures: this.extractFailures(output)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
extractFailures(output) {
|
|
328
|
+
const failures = [];
|
|
329
|
+
const failureRegex = /Failed tests?:\s*([\s\S]*?)(?=Tests run:|$)/gi;
|
|
330
|
+
const match = failureRegex.exec(output);
|
|
331
|
+
|
|
332
|
+
if (match) {
|
|
333
|
+
const lines = match[1].split('\n').filter(l => l.trim());
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
failures.push({
|
|
336
|
+
testName: line.trim(),
|
|
337
|
+
message: '',
|
|
338
|
+
stackTrace: ''
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return failures;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ==========================================
|
|
347
|
+
// CODE ANALYSIS
|
|
348
|
+
// ==========================================
|
|
349
|
+
|
|
350
|
+
getFileExtensions() {
|
|
351
|
+
return ['.java'];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async findIntegrations(workspacePath) {
|
|
355
|
+
const integrations = [];
|
|
356
|
+
const { JavaIntegrationAnalyzer } = await import('./java-integrations.js');
|
|
357
|
+
const analyzer = new JavaIntegrationAnalyzer(workspacePath);
|
|
358
|
+
|
|
359
|
+
return analyzer.findAll();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async analyzeImports(filePath, content) {
|
|
363
|
+
const imports = [];
|
|
364
|
+
const importRegex = /import\s+(static\s+)?([a-zA-Z0-9_.]+)(?:\.\*)?;/g;
|
|
365
|
+
|
|
366
|
+
let match;
|
|
367
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
368
|
+
const fullImport = match[2];
|
|
369
|
+
const isStatic = !!match[1];
|
|
370
|
+
|
|
371
|
+
imports.push({
|
|
372
|
+
name: fullImport,
|
|
373
|
+
type: isStatic ? 'static-import' : 'import',
|
|
374
|
+
isExternal: !fullImport.startsWith('com.') ||
|
|
375
|
+
fullImport.includes('springframework') ||
|
|
376
|
+
fullImport.includes('jakarta') ||
|
|
377
|
+
fullImport.includes('javax')
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return imports;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ==========================================
|
|
385
|
+
// PATCH
|
|
386
|
+
// ==========================================
|
|
387
|
+
|
|
388
|
+
async validatePatch(workspacePath, diff) {
|
|
389
|
+
// Validar se os arquivos existem
|
|
390
|
+
const errors = [];
|
|
391
|
+
|
|
392
|
+
// Extrair arquivos do diff
|
|
393
|
+
const fileRegex = /^[\+\-]{3}\s+[ab]\/(.+)$/gm;
|
|
394
|
+
let match;
|
|
395
|
+
|
|
396
|
+
while ((match = fileRegex.exec(diff)) !== null) {
|
|
397
|
+
const filePath = path.join(workspacePath, match[1]);
|
|
398
|
+
if (match[0].startsWith('---') && !(await exists(filePath))) {
|
|
399
|
+
// Arquivo sendo modificado não existe
|
|
400
|
+
if (!diff.includes('+++ /dev/null')) {
|
|
401
|
+
errors.push(`File not found: ${match[1]}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
valid: errors.length === 0,
|
|
408
|
+
errors
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async postPatchActions(workspacePath, modifiedFiles) {
|
|
413
|
+
// Opcional: rodar formatter após patch
|
|
414
|
+
// await run('mvn', ['formatter:format', '-q'], workspacePath);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export default MavenRuntime;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { readFile, readdir } from '../../utils/fs-utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NodeIntegrationAnalyzer
|
|
6
|
+
*
|
|
7
|
+
* Analisa código Node.js/TypeScript para encontrar integrações HTTP:
|
|
8
|
+
* - axios
|
|
9
|
+
* - fetch / node-fetch
|
|
10
|
+
* - got
|
|
11
|
+
* - HttpService (NestJS)
|
|
12
|
+
*/
|
|
13
|
+
export class NodeIntegrationAnalyzer {
|
|
14
|
+
|
|
15
|
+
constructor(workspacePath) {
|
|
16
|
+
this.workspacePath = workspacePath;
|
|
17
|
+
this.integrations = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async findAll() {
|
|
21
|
+
this.integrations = [];
|
|
22
|
+
|
|
23
|
+
const srcPath = path.join(this.workspacePath, 'src');
|
|
24
|
+
await this.scanDirectory(srcPath);
|
|
25
|
+
await this.scanDirectory(this.workspacePath, 1);
|
|
26
|
+
|
|
27
|
+
return this.integrations;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async scanDirectory(dir, maxDepth = 10, currentDepth = 0) {
|
|
31
|
+
if (currentDepth > maxDepth) return;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
await this.scanDirectory(fullPath, maxDepth, currentDepth + 1);
|
|
45
|
+
} else if (/\.(js|ts|jsx|tsx|mjs|cjs)$/.test(entry.name)) {
|
|
46
|
+
await this.analyzeFile(fullPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
// Directory not found
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async analyzeFile(filePath) {
|
|
55
|
+
try {
|
|
56
|
+
const content = await readFile(filePath, 'utf8');
|
|
57
|
+
const relativePath = path.relative(this.workspacePath, filePath);
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
|
|
60
|
+
this.findAxios(content, relativePath, lines);
|
|
61
|
+
this.findFetch(content, relativePath, lines);
|
|
62
|
+
this.findGot(content, relativePath, lines);
|
|
63
|
+
this.findNestHttpService(content, relativePath, lines);
|
|
64
|
+
this.findKafka(content, relativePath, lines);
|
|
65
|
+
this.findAmqp(content, relativePath, lines);
|
|
66
|
+
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Error reading file
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
findAxios(content, filePath, lines) {
|
|
73
|
+
const patterns = [
|
|
74
|
+
{ regex: /axios\.(get|post|put|patch|delete|head|options)\s*\(/gi, type: 'call' },
|
|
75
|
+
{ regex: /axios\s*\(\s*\{/g, type: 'config-call' },
|
|
76
|
+
{ regex: /axios\.create\s*\(/g, type: 'instance' }
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const pattern of patterns) {
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
82
|
+
const line = this.getLineNumber(content, match.index);
|
|
83
|
+
const url = this.extractUrl(content, match.index);
|
|
84
|
+
|
|
85
|
+
this.integrations.push({
|
|
86
|
+
type: 'axios',
|
|
87
|
+
subType: pattern.type,
|
|
88
|
+
file: filePath,
|
|
89
|
+
line,
|
|
90
|
+
targetUrl: url,
|
|
91
|
+
method: match[1]?.toUpperCase() || null,
|
|
92
|
+
snippet: lines[line - 1]?.trim() || match[0]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
findFetch(content, filePath, lines) {
|
|
99
|
+
const patterns = [
|
|
100
|
+
{ regex: /fetch\s*\(\s*['"`]/g, type: 'call' },
|
|
101
|
+
{ regex: /fetch\s*\(\s*\$\{/g, type: 'template-call' }
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const pattern of patterns) {
|
|
105
|
+
let match;
|
|
106
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
107
|
+
const line = this.getLineNumber(content, match.index);
|
|
108
|
+
const url = this.extractUrl(content, match.index);
|
|
109
|
+
const method = this.extractFetchMethod(content, match.index);
|
|
110
|
+
|
|
111
|
+
this.integrations.push({
|
|
112
|
+
type: 'fetch',
|
|
113
|
+
subType: pattern.type,
|
|
114
|
+
file: filePath,
|
|
115
|
+
line,
|
|
116
|
+
targetUrl: url,
|
|
117
|
+
method,
|
|
118
|
+
snippet: lines[line - 1]?.trim() || match[0]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
findGot(content, filePath, lines) {
|
|
125
|
+
const patterns = [
|
|
126
|
+
{ regex: /got\.(get|post|put|patch|delete|head)\s*\(/gi, type: 'call' },
|
|
127
|
+
{ regex: /got\s*\(\s*['"`]/g, type: 'call' }
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const pattern of patterns) {
|
|
131
|
+
let match;
|
|
132
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
133
|
+
const line = this.getLineNumber(content, match.index);
|
|
134
|
+
const url = this.extractUrl(content, match.index);
|
|
135
|
+
|
|
136
|
+
this.integrations.push({
|
|
137
|
+
type: 'got',
|
|
138
|
+
subType: pattern.type,
|
|
139
|
+
file: filePath,
|
|
140
|
+
line,
|
|
141
|
+
targetUrl: url,
|
|
142
|
+
method: match[1]?.toUpperCase() || 'GET',
|
|
143
|
+
snippet: lines[line - 1]?.trim() || match[0]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
findNestHttpService(content, filePath, lines) {
|
|
150
|
+
const patterns = [
|
|
151
|
+
{ regex: /this\.httpService\.(get|post|put|patch|delete|head)\s*\(/gi, type: 'call' }
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
for (const pattern of patterns) {
|
|
155
|
+
let match;
|
|
156
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
157
|
+
const line = this.getLineNumber(content, match.index);
|
|
158
|
+
const url = this.extractUrl(content, match.index);
|
|
159
|
+
|
|
160
|
+
this.integrations.push({
|
|
161
|
+
type: 'nest-http-service',
|
|
162
|
+
subType: pattern.type,
|
|
163
|
+
file: filePath,
|
|
164
|
+
line,
|
|
165
|
+
targetUrl: url,
|
|
166
|
+
method: match[1]?.toUpperCase() || null,
|
|
167
|
+
snippet: lines[line - 1]?.trim() || match[0]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
findKafka(content, filePath, lines) {
|
|
174
|
+
const patterns = [
|
|
175
|
+
{ regex: /\.send\s*\(\s*\{\s*topic\s*:\s*['"]([^'"]+)['"]/g, type: 'producer' },
|
|
176
|
+
{ regex: /\.subscribe\s*\(\s*\{\s*topic\s*:\s*['"]([^'"]+)['"]/g, type: 'consumer' }
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
for (const pattern of patterns) {
|
|
180
|
+
let match;
|
|
181
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
182
|
+
const line = this.getLineNumber(content, match.index);
|
|
183
|
+
|
|
184
|
+
this.integrations.push({
|
|
185
|
+
type: 'kafka',
|
|
186
|
+
subType: pattern.type,
|
|
187
|
+
file: filePath,
|
|
188
|
+
line,
|
|
189
|
+
topic: match[1] || null,
|
|
190
|
+
snippet: lines[line - 1]?.trim() || match[0]
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
findAmqp(content, filePath, lines) {
|
|
197
|
+
const patterns = [
|
|
198
|
+
{ regex: /\.sendToQueue\s*\(\s*['"]([^'"]+)['"]/g, type: 'producer' },
|
|
199
|
+
{ regex: /\.consume\s*\(\s*['"]([^'"]+)['"]/g, type: 'consumer' }
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
for (const pattern of patterns) {
|
|
203
|
+
let match;
|
|
204
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
205
|
+
const line = this.getLineNumber(content, match.index);
|
|
206
|
+
|
|
207
|
+
this.integrations.push({
|
|
208
|
+
type: 'amqp',
|
|
209
|
+
subType: pattern.type,
|
|
210
|
+
file: filePath,
|
|
211
|
+
line,
|
|
212
|
+
queue: match[1] || null,
|
|
213
|
+
snippet: lines[line - 1]?.trim() || match[0]
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getLineNumber(content, index) {
|
|
220
|
+
return content.substring(0, index).split('\n').length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
extractUrl(content, startIndex) {
|
|
224
|
+
const context = content.substring(startIndex, startIndex + 500);
|
|
225
|
+
|
|
226
|
+
const urlPatterns = [
|
|
227
|
+
/['"`](https?:\/\/[^'"`]+)['"`]/,
|
|
228
|
+
/['"`]([^'"`]*\/api\/[^'"`]+)['"`]/,
|
|
229
|
+
/['"`](\/[a-zA-Z][a-zA-Z0-9/\-_{}:]+)['"`]/
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
for (const pattern of urlPatterns) {
|
|
233
|
+
const match = context.match(pattern);
|
|
234
|
+
if (match) return match[1];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
extractFetchMethod(content, startIndex) {
|
|
241
|
+
const context = content.substring(startIndex, startIndex + 300);
|
|
242
|
+
const methodMatch = context.match(/method\s*:\s*['"](\w+)['"]/i);
|
|
243
|
+
return methodMatch ? methodMatch[1].toUpperCase() : 'GET';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default NodeIntegrationAnalyzer;
|