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,619 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import stripAnsi from "strip-ansi";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ProcessManager
|
|
9
|
+
*
|
|
10
|
+
* Gerencia ciclo de vida de processos (start, stop, restart)
|
|
11
|
+
* Monitora CPU, Memory, Uptime
|
|
12
|
+
* Captura logs em tempo real
|
|
13
|
+
*
|
|
14
|
+
* UNIFIED VERSION: Aceita AMBOS os formatos:
|
|
15
|
+
* 1. command/args direto (novo)
|
|
16
|
+
* 2. language/framework/buildTool (legado)
|
|
17
|
+
*/
|
|
18
|
+
export class ProcessManager extends EventEmitter {
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
this.processes = new Map(); // serviceId -> { process, startTime, logs, stats }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Verifica se um serviço está rodando
|
|
26
|
+
*/
|
|
27
|
+
isRunning(serviceId) {
|
|
28
|
+
const processData = this.processes.get(serviceId);
|
|
29
|
+
return processData && processData.process && !processData.process.killed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Inicia um serviço
|
|
34
|
+
*
|
|
35
|
+
* ACEITA DOIS FORMATOS:
|
|
36
|
+
* 1. { command, args, cwd, port, env } - Novo formato direto
|
|
37
|
+
* 2. { language, framework, buildTool, cwd, port, profile } - Formato legado
|
|
38
|
+
*/
|
|
39
|
+
async start(serviceId, config) {
|
|
40
|
+
const {
|
|
41
|
+
// Novo formato
|
|
42
|
+
command,
|
|
43
|
+
args,
|
|
44
|
+
// Formato legado
|
|
45
|
+
language,
|
|
46
|
+
framework,
|
|
47
|
+
buildTool,
|
|
48
|
+
// Comum
|
|
49
|
+
cwd,
|
|
50
|
+
port,
|
|
51
|
+
profile,
|
|
52
|
+
env,
|
|
53
|
+
envVars = {}
|
|
54
|
+
} = config;
|
|
55
|
+
|
|
56
|
+
// Se já estiver rodando, parar primeiro
|
|
57
|
+
if (this.processes.has(serviceId)) {
|
|
58
|
+
console.log(`⚠️ Service ${serviceId} already running, stopping first...`);
|
|
59
|
+
await this.stop(serviceId);
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Determinar comando a usar
|
|
64
|
+
let cmd, cmdArgs;
|
|
65
|
+
|
|
66
|
+
if (command && args) {
|
|
67
|
+
// NOVO FORMATO: command/args passados diretamente
|
|
68
|
+
cmd = command;
|
|
69
|
+
cmdArgs = args;
|
|
70
|
+
console.log(`🚀 Starting ${serviceId} (direct): ${cmd} ${cmdArgs.join(' ')}`);
|
|
71
|
+
} else if (language) {
|
|
72
|
+
// FORMATO LEGADO: derivar de language/framework/buildTool
|
|
73
|
+
const startCommand = await this.getStartCommand(language, framework, buildTool, cwd, profile || 'dev');
|
|
74
|
+
cmd = startCommand.cmd;
|
|
75
|
+
cmdArgs = startCommand.args;
|
|
76
|
+
console.log(`🚀 Starting ${serviceId} (derived): ${cmd} ${cmdArgs.join(' ')}`);
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error(`Either 'command/args' or 'language' must be provided to start ${serviceId}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`⚙️ Config: port=${port}, profile=${profile || 'none'}`);
|
|
82
|
+
console.log(`📁 Working directory: ${cwd}`);
|
|
83
|
+
|
|
84
|
+
// ============================================
|
|
85
|
+
// BUILD ENVIRONMENT
|
|
86
|
+
// ============================================
|
|
87
|
+
// Two strategies:
|
|
88
|
+
// DIRECT MODE (command/args provided): env is used AS-IS if provided.
|
|
89
|
+
// The caller (test-local/start) already builds a clean env without
|
|
90
|
+
// SPRING_* variables so the app uses only application.yml defaults.
|
|
91
|
+
// LEGACY MODE (language/framework): merge process.env with overrides.
|
|
92
|
+
// ============================================
|
|
93
|
+
let processEnv;
|
|
94
|
+
|
|
95
|
+
if (command && args && env) {
|
|
96
|
+
// ✅ DIRECT MODE: Use the caller's env as the base (it's already clean)
|
|
97
|
+
processEnv = {
|
|
98
|
+
...env,
|
|
99
|
+
...envVars
|
|
100
|
+
};
|
|
101
|
+
// Only set port vars if not already present
|
|
102
|
+
if (!processEnv.SERVER_PORT) processEnv.SERVER_PORT = String(port || 8080);
|
|
103
|
+
if (!processEnv.PORT) processEnv.PORT = String(port || 8080);
|
|
104
|
+
// DO NOT set SPRING_PROFILES_ACTIVE — let application.yml decide
|
|
105
|
+
console.log(` 📦 Using caller-provided env (clean, ${Object.keys(processEnv).length} vars)`);
|
|
106
|
+
} else {
|
|
107
|
+
// 🔄 LEGACY MODE: Merge with process.env and apply profile
|
|
108
|
+
processEnv = {
|
|
109
|
+
...process.env,
|
|
110
|
+
...(env || {}),
|
|
111
|
+
...envVars,
|
|
112
|
+
SERVER_PORT: String(port || 8080),
|
|
113
|
+
PORT: String(port || 8080),
|
|
114
|
+
NODE_ENV: profile === 'prod' ? 'production' : 'development',
|
|
115
|
+
FLASK_ENV: profile === 'prod' ? 'production' : 'development',
|
|
116
|
+
};
|
|
117
|
+
// Only set profile if explicitly provided
|
|
118
|
+
if (profile) {
|
|
119
|
+
processEnv.SPRING_PROFILES_ACTIVE = profile;
|
|
120
|
+
}
|
|
121
|
+
console.log(` 📦 Using merged env (legacy, profile=${profile || 'default'})`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const child = spawn(cmd, cmdArgs, {
|
|
125
|
+
cwd,
|
|
126
|
+
shell: true,
|
|
127
|
+
env: processEnv,
|
|
128
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!child.pid) {
|
|
132
|
+
throw new Error(`Failed to spawn process for ${serviceId}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`✅ Process spawned with PID: ${child.pid}`);
|
|
136
|
+
|
|
137
|
+
const processData = {
|
|
138
|
+
process: child,
|
|
139
|
+
pid: child.pid,
|
|
140
|
+
command: cmd,
|
|
141
|
+
args: cmdArgs,
|
|
142
|
+
cwd,
|
|
143
|
+
startTime: Date.now(),
|
|
144
|
+
logs: [],
|
|
145
|
+
port,
|
|
146
|
+
status: "starting",
|
|
147
|
+
cpu: 0,
|
|
148
|
+
memory: 0
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.processes.set(serviceId, processData);
|
|
152
|
+
|
|
153
|
+
// Capturar stdout
|
|
154
|
+
child.stdout.on("data", (data) => {
|
|
155
|
+
const text = data.toString();
|
|
156
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
157
|
+
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
const log = stripAnsi(line).trim();
|
|
160
|
+
if (!log) continue;
|
|
161
|
+
|
|
162
|
+
processData.logs.push({
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
type: "stdout",
|
|
165
|
+
message: log
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.emit("log", { serviceId, message: log, type: "stdout" });
|
|
169
|
+
|
|
170
|
+
// Detectar quando serviço está pronto
|
|
171
|
+
if (this.isServiceReady(log, language || 'java')) {
|
|
172
|
+
if (processData.status !== "running") {
|
|
173
|
+
processData.status = "running";
|
|
174
|
+
console.log(`✅ Service ${serviceId} is READY!`);
|
|
175
|
+
this.emit("started", { serviceId, port });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Capturar stderr (Spring Boot também loga aqui)
|
|
182
|
+
child.stderr.on("data", (data) => {
|
|
183
|
+
const text = data.toString();
|
|
184
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
185
|
+
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const log = stripAnsi(line).trim();
|
|
188
|
+
if (!log) continue;
|
|
189
|
+
|
|
190
|
+
processData.logs.push({
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
type: "stderr",
|
|
193
|
+
message: log
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.emit("log", { serviceId, message: log, type: "stderr" });
|
|
197
|
+
|
|
198
|
+
// Spring Boot pode logar "Started" no stderr também
|
|
199
|
+
if (this.isServiceReady(log, language || 'java')) {
|
|
200
|
+
if (processData.status !== "running") {
|
|
201
|
+
processData.status = "running";
|
|
202
|
+
console.log(`✅ Service ${serviceId} is READY!`);
|
|
203
|
+
this.emit("started", { serviceId, port });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Capturar erro de inicialização
|
|
210
|
+
child.on("error", (err) => {
|
|
211
|
+
console.error(`❌ Failed to start ${serviceId}:`, err.message);
|
|
212
|
+
processData.status = "failed";
|
|
213
|
+
this.emit("error", { serviceId, error: err.message });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Capturar encerramento
|
|
217
|
+
child.on("close", (code, signal) => {
|
|
218
|
+
console.log(`⏹️ Service ${serviceId} exited with code ${code}, signal ${signal}`);
|
|
219
|
+
this.processes.delete(serviceId);
|
|
220
|
+
this.emit("stopped", { serviceId, code, signal });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Iniciar monitoramento de recursos
|
|
224
|
+
this.startResourceMonitoring(serviceId);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
serviceId,
|
|
228
|
+
pid: child.pid,
|
|
229
|
+
status: "starting",
|
|
230
|
+
port,
|
|
231
|
+
command: `${cmd} ${cmdArgs.join(' ')}`,
|
|
232
|
+
startedAt: new Date().toISOString()
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Para um serviço
|
|
238
|
+
*/
|
|
239
|
+
async stop(serviceId) {
|
|
240
|
+
const processData = this.processes.get(serviceId);
|
|
241
|
+
if (!processData) {
|
|
242
|
+
console.log(`⚠️ Service ${serviceId} is not running (already stopped)`);
|
|
243
|
+
return {
|
|
244
|
+
serviceId,
|
|
245
|
+
status: 'stopped',
|
|
246
|
+
stoppedAt: new Date().toISOString(),
|
|
247
|
+
alreadyStopped: true
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(`⏹️ Stopping ${serviceId} (PID: ${processData.pid})...`);
|
|
252
|
+
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
const child = processData.process;
|
|
255
|
+
let resolved = false;
|
|
256
|
+
|
|
257
|
+
const onExit = (code) => {
|
|
258
|
+
if (!resolved) {
|
|
259
|
+
resolved = true;
|
|
260
|
+
this.processes.delete(serviceId);
|
|
261
|
+
console.log(`✅ Service ${serviceId} stopped (exit code: ${code})`);
|
|
262
|
+
this.emit("log", { serviceId, message: `Service stopped (exit code: ${code})`, type: "info" });
|
|
263
|
+
resolve({
|
|
264
|
+
serviceId,
|
|
265
|
+
status: 'stopped',
|
|
266
|
+
exitCode: code,
|
|
267
|
+
stoppedAt: new Date().toISOString()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
child.once('exit', onExit);
|
|
273
|
+
child.once('close', onExit);
|
|
274
|
+
|
|
275
|
+
// Tentar graceful shutdown
|
|
276
|
+
try {
|
|
277
|
+
child.kill('SIGTERM');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.log(`⚠️ SIGTERM failed for ${serviceId}: ${err.message}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Force kill após 5 segundos
|
|
283
|
+
setTimeout(() => {
|
|
284
|
+
if (!resolved && this.processes.has(serviceId)) {
|
|
285
|
+
console.log(`🔨 Force killing ${serviceId} (SIGKILL)`);
|
|
286
|
+
try {
|
|
287
|
+
child.kill('SIGKILL');
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.log(`⚠️ SIGKILL failed: ${err.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}, 5000);
|
|
293
|
+
|
|
294
|
+
// Timeout geral após 10 segundos
|
|
295
|
+
setTimeout(() => {
|
|
296
|
+
if (!resolved) {
|
|
297
|
+
resolved = true;
|
|
298
|
+
this.processes.delete(serviceId);
|
|
299
|
+
console.log(`⏰ Stop timeout for ${serviceId}, assuming stopped`);
|
|
300
|
+
resolve({
|
|
301
|
+
serviceId,
|
|
302
|
+
status: 'stopped',
|
|
303
|
+
timeout: true,
|
|
304
|
+
stoppedAt: new Date().toISOString()
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}, 10000);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Retorna status de um serviço
|
|
313
|
+
*/
|
|
314
|
+
getStatus(serviceId) {
|
|
315
|
+
const processData = this.processes.get(serviceId);
|
|
316
|
+
if (!processData) {
|
|
317
|
+
return { serviceId, status: "stopped" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const uptime = Math.floor((Date.now() - processData.startTime) / 1000);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
serviceId,
|
|
324
|
+
status: processData.status,
|
|
325
|
+
pid: processData.pid,
|
|
326
|
+
port: processData.port,
|
|
327
|
+
cpu: processData.cpu,
|
|
328
|
+
memory: processData.memory,
|
|
329
|
+
uptime,
|
|
330
|
+
logsCount: processData.logs.length,
|
|
331
|
+
startedAt: new Date(processData.startTime).toISOString()
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Retorna logs de um serviço
|
|
337
|
+
*/
|
|
338
|
+
getLogs(serviceId, limit = 100) {
|
|
339
|
+
const processData = this.processes.get(serviceId);
|
|
340
|
+
if (!processData) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
return processData.logs.slice(-limit);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Lista todos os serviços
|
|
348
|
+
*/
|
|
349
|
+
listServices() {
|
|
350
|
+
const services = [];
|
|
351
|
+
for (const [serviceId] of this.processes.entries()) {
|
|
352
|
+
services.push(this.getStatus(serviceId));
|
|
353
|
+
}
|
|
354
|
+
return services;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Alias para compatibilidade
|
|
359
|
+
*/
|
|
360
|
+
listAll() {
|
|
361
|
+
return this.listServices();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Determina comando de start baseado no framework (FORMATO LEGADO)
|
|
366
|
+
* Usado quando server.js não passa command/args diretamente
|
|
367
|
+
*/
|
|
368
|
+
async getStartCommand(language, framework, buildTool, cwd, profile = 'dev') {
|
|
369
|
+
console.log(`🔧 getStartCommand: language=${language}, framework=${framework}, buildTool=${buildTool}`);
|
|
370
|
+
|
|
371
|
+
// Java (qualquer framework ou sem framework)
|
|
372
|
+
if (language === "java") {
|
|
373
|
+
return await this.getJavaCommand(buildTool, cwd, profile);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Node.js
|
|
377
|
+
if (language === "node") {
|
|
378
|
+
return await this.getNodeCommand(framework, cwd);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Python
|
|
382
|
+
if (language === "python") {
|
|
383
|
+
return await this.getPythonCommand(framework, cwd);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// .NET
|
|
387
|
+
if (language === "dotnet" || language === ".net") {
|
|
388
|
+
return await this.getDotNetCommand(cwd);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Go
|
|
392
|
+
if (language === "go") {
|
|
393
|
+
return await this.getGoCommand(cwd);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Fallback genérico - tenta npm start
|
|
397
|
+
console.warn(`⚠️ Unknown language: ${language}, trying npm start`);
|
|
398
|
+
return { cmd: "npm", args: ["start"] };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Comando Java/Spring Boot
|
|
403
|
+
*/
|
|
404
|
+
async getJavaCommand(buildTool, cwd, profile = null) {
|
|
405
|
+
const targetDir = path.join(cwd, "target");
|
|
406
|
+
const buildDir = path.join(cwd, "build", "libs");
|
|
407
|
+
|
|
408
|
+
console.log(`🔍 Checking for compiled JARs in: ${cwd}`);
|
|
409
|
+
|
|
410
|
+
// ✅ Helper: Build args with optional profile
|
|
411
|
+
const buildJavaArgs = (jarPath) => {
|
|
412
|
+
const javaArgs = [];
|
|
413
|
+
// Only add profile if explicitly provided (not null/undefined/'default')
|
|
414
|
+
if (profile && profile !== 'default') {
|
|
415
|
+
javaArgs.push(`-Dspring.profiles.active=${profile}`);
|
|
416
|
+
console.log(` 📋 Using Spring profile: ${profile}`);
|
|
417
|
+
} else {
|
|
418
|
+
// ✅ Check if there's a local/dev profile available
|
|
419
|
+
const resourcesDir = path.join(cwd, 'src/main/resources');
|
|
420
|
+
const localProfile = ['local', 'dev'].find(p => {
|
|
421
|
+
return fs.existsSync(path.join(resourcesDir, `application-${p}.yml`)) ||
|
|
422
|
+
fs.existsSync(path.join(resourcesDir, `application-${p}.yaml`)) ||
|
|
423
|
+
fs.existsSync(path.join(resourcesDir, `application-${p}.properties`));
|
|
424
|
+
});
|
|
425
|
+
if (localProfile) {
|
|
426
|
+
javaArgs.push(`-Dspring.profiles.active=${localProfile}`);
|
|
427
|
+
console.log(` 📋 Auto-detected profile: ${localProfile}`);
|
|
428
|
+
} else {
|
|
429
|
+
console.log(` 📋 No profile set — using application.yml defaults`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
javaArgs.push("-jar", jarPath);
|
|
433
|
+
return javaArgs;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Maven: Procurar JAR em target/
|
|
437
|
+
if (buildTool === "maven") {
|
|
438
|
+
console.log(`📁 Target directory: ${targetDir} (exists: ${fs.existsSync(targetDir)})`);
|
|
439
|
+
|
|
440
|
+
if (fs.existsSync(targetDir)) {
|
|
441
|
+
try {
|
|
442
|
+
const allFiles = fs.readdirSync(targetDir);
|
|
443
|
+
console.log(`📦 Files in target/: ${allFiles.join(", ")}`);
|
|
444
|
+
|
|
445
|
+
const jars = allFiles
|
|
446
|
+
.filter(f => {
|
|
447
|
+
return f.endsWith(".jar") &&
|
|
448
|
+
!f.includes("original") &&
|
|
449
|
+
!f.includes("sources") &&
|
|
450
|
+
!f.includes("javadoc");
|
|
451
|
+
})
|
|
452
|
+
.sort((a, b) => {
|
|
453
|
+
const statsA = fs.statSync(path.join(targetDir, a));
|
|
454
|
+
const statsB = fs.statSync(path.join(targetDir, b));
|
|
455
|
+
return statsB.mtimeMs - statsA.mtimeMs;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
console.log(`🎯 Filtered JARs: ${jars.join(", ")}`);
|
|
459
|
+
|
|
460
|
+
if (jars.length > 0) {
|
|
461
|
+
const jarPath = path.join(targetDir, jars[0]);
|
|
462
|
+
console.log(`✅ Found compiled JAR: ${jars[0]}`);
|
|
463
|
+
return {
|
|
464
|
+
cmd: "java",
|
|
465
|
+
args: buildJavaArgs(jarPath)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.error(`❌ Error reading target: ${err.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Sem JAR - pedir para compilar
|
|
474
|
+
throw new Error(`No JAR found. Please compile first: mvn clean install -DskipTests`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Gradle: Procurar JAR em build/libs/
|
|
478
|
+
if (buildTool === "gradle") {
|
|
479
|
+
console.log(`📁 Build directory: ${buildDir} (exists: ${fs.existsSync(buildDir)})`);
|
|
480
|
+
|
|
481
|
+
if (fs.existsSync(buildDir)) {
|
|
482
|
+
try {
|
|
483
|
+
const allFiles = fs.readdirSync(buildDir);
|
|
484
|
+
const jars = allFiles
|
|
485
|
+
.filter(f => f.endsWith(".jar") && !f.includes("plain") && !f.includes("sources"))
|
|
486
|
+
.sort((a, b) => {
|
|
487
|
+
const statsA = fs.statSync(path.join(buildDir, a));
|
|
488
|
+
const statsB = fs.statSync(path.join(buildDir, b));
|
|
489
|
+
return statsB.mtimeMs - statsA.mtimeMs;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (jars.length > 0) {
|
|
493
|
+
const jarPath = path.join(buildDir, jars[0]);
|
|
494
|
+
console.log(`✅ Found compiled JAR: ${jars[0]}`);
|
|
495
|
+
return {
|
|
496
|
+
cmd: "java",
|
|
497
|
+
args: buildJavaArgs(jarPath)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.error(`❌ Error reading build/libs: ${err.message}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
throw new Error(`No JAR found. Please compile first: ./gradlew clean build -x test`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
throw new Error(`Unknown Java build tool: ${buildTool}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Comando Node.js
|
|
513
|
+
*/
|
|
514
|
+
async getNodeCommand(framework, cwd) {
|
|
515
|
+
const nodeModules = path.join(cwd, "node_modules");
|
|
516
|
+
const hasNodeModules = fs.existsSync(nodeModules);
|
|
517
|
+
|
|
518
|
+
if (!hasNodeModules) {
|
|
519
|
+
console.log("⚠️ node_modules not found, will install first");
|
|
520
|
+
if (framework === "next") {
|
|
521
|
+
return { cmd: "sh", args: ["-c", "npm install && npm run dev"] };
|
|
522
|
+
}
|
|
523
|
+
return { cmd: "sh", args: ["-c", "npm install && npm start"] };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (framework === "next") {
|
|
527
|
+
return { cmd: "npm", args: ["run", "dev"] };
|
|
528
|
+
}
|
|
529
|
+
return { cmd: "npm", args: ["start"] };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Comando Python
|
|
534
|
+
*/
|
|
535
|
+
async getPythonCommand(framework, cwd) {
|
|
536
|
+
if (framework === "django") {
|
|
537
|
+
return { cmd: "python", args: ["manage.py", "runserver"] };
|
|
538
|
+
}
|
|
539
|
+
if (framework === "flask") {
|
|
540
|
+
return { cmd: "flask", args: ["run"] };
|
|
541
|
+
}
|
|
542
|
+
if (framework === "fastapi") {
|
|
543
|
+
return { cmd: "uvicorn", args: ["main:app", "--reload"] };
|
|
544
|
+
}
|
|
545
|
+
return { cmd: "python", args: ["main.py"] };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Comando .NET
|
|
550
|
+
*/
|
|
551
|
+
async getDotNetCommand(cwd) {
|
|
552
|
+
return { cmd: "dotnet", args: ["run"] };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Comando Go
|
|
557
|
+
*/
|
|
558
|
+
async getGoCommand(cwd) {
|
|
559
|
+
return { cmd: "go", args: ["run", "."] };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Detecta quando serviço está pronto
|
|
564
|
+
*/
|
|
565
|
+
isServiceReady(log, language) {
|
|
566
|
+
if (!log) return false;
|
|
567
|
+
|
|
568
|
+
const patterns = [
|
|
569
|
+
// Spring Boot
|
|
570
|
+
/started.*in \d+(\.\d+)? seconds/i,
|
|
571
|
+
/tomcat started on port/i,
|
|
572
|
+
/started \w+application/i,
|
|
573
|
+
/jvm running for/i,
|
|
574
|
+
// Node.js
|
|
575
|
+
/listening on port/i,
|
|
576
|
+
/server running/i,
|
|
577
|
+
/ready on/i,
|
|
578
|
+
/started on/i,
|
|
579
|
+
// Python
|
|
580
|
+
/running on http/i,
|
|
581
|
+
/uvicorn running/i,
|
|
582
|
+
/development server/i,
|
|
583
|
+
// .NET
|
|
584
|
+
/now listening on/i,
|
|
585
|
+
// Generic
|
|
586
|
+
/server started/i,
|
|
587
|
+
/application started/i
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
return patterns.some(pattern => pattern.test(log));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Monitora recursos (CPU e Memory)
|
|
595
|
+
*/
|
|
596
|
+
startResourceMonitoring(serviceId) {
|
|
597
|
+
const interval = setInterval(async () => {
|
|
598
|
+
const processData = this.processes.get(serviceId);
|
|
599
|
+
if (!processData) {
|
|
600
|
+
clearInterval(interval);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
const pidusage = await import('pidusage').then(m => m.default).catch(() => null);
|
|
606
|
+
if (pidusage && processData.pid) {
|
|
607
|
+
const stats = await pidusage(processData.pid);
|
|
608
|
+
processData.cpu = parseFloat(stats.cpu.toFixed(1));
|
|
609
|
+
processData.memory = Math.floor(stats.memory / 1024 / 1024);
|
|
610
|
+
}
|
|
611
|
+
} catch {
|
|
612
|
+
processData.cpu = 0;
|
|
613
|
+
processData.memory = 0;
|
|
614
|
+
}
|
|
615
|
+
}, 5000);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export default ProcessManager;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export class WorkspaceScanner {
|
|
5
|
+
constructor(rootPath) {
|
|
6
|
+
this.rootPath = rootPath;
|
|
7
|
+
this.ignorePatterns = [
|
|
8
|
+
'node_modules', 'target', 'build', 'dist',
|
|
9
|
+
'.git', '.idea', '.vscode', '__pycache__',
|
|
10
|
+
'.next', 'out', 'coverage', '.gradle'
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async scan() {
|
|
15
|
+
const structure = {
|
|
16
|
+
root: this.rootPath,
|
|
17
|
+
files: [],
|
|
18
|
+
directories: [],
|
|
19
|
+
metadata: {
|
|
20
|
+
totalFiles: 0,
|
|
21
|
+
totalSize: 0,
|
|
22
|
+
languages: new Set(),
|
|
23
|
+
frameworks: new Set()
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await this.walkDirectory(this.rootPath, structure);
|
|
28
|
+
|
|
29
|
+
// Convert Set to Array for JSON serialization
|
|
30
|
+
structure.metadata.languages = Array.from(structure.metadata.languages);
|
|
31
|
+
structure.metadata.frameworks = Array.from(structure.metadata.frameworks);
|
|
32
|
+
|
|
33
|
+
return structure;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async walkDirectory(dir, structure, depth = 0) {
|
|
37
|
+
if (depth > 10) return; // Limite de profundidade
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (this.shouldIgnore(entry.name)) continue;
|
|
44
|
+
|
|
45
|
+
const fullPath = path.join(dir, entry.name);
|
|
46
|
+
const relativePath = path.relative(this.rootPath, fullPath);
|
|
47
|
+
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
structure.directories.push(relativePath);
|
|
50
|
+
await this.walkDirectory(fullPath, structure, depth + 1);
|
|
51
|
+
} else {
|
|
52
|
+
const stat = await fs.stat(fullPath);
|
|
53
|
+
structure.files.push({
|
|
54
|
+
path: relativePath,
|
|
55
|
+
name: entry.name,
|
|
56
|
+
extension: path.extname(entry.name),
|
|
57
|
+
size: stat.size,
|
|
58
|
+
modified: stat.mtime
|
|
59
|
+
});
|
|
60
|
+
structure.metadata.totalFiles++;
|
|
61
|
+
structure.metadata.totalSize += stat.size;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`Error reading directory ${dir}:`, err.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
shouldIgnore(name) {
|
|
70
|
+
return this.ignorePatterns.some(pattern => name.includes(pattern));
|
|
71
|
+
}
|
|
72
|
+
}
|