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.
Files changed (50) hide show
  1. package/.dockerignore +24 -0
  2. package/.idea/deepdebug-local-agent.iml +12 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/Dockerfile +46 -0
  6. package/cloudbuild.yaml +42 -0
  7. package/index.js +42 -0
  8. package/mcp-server.js +533 -0
  9. package/package.json +22 -0
  10. package/src/ai-engine.js +861 -0
  11. package/src/analyzers/config-analyzer.js +446 -0
  12. package/src/analyzers/controller-analyzer.js +429 -0
  13. package/src/analyzers/dto-analyzer.js +455 -0
  14. package/src/detectors/build-tool-detector.js +0 -0
  15. package/src/detectors/framework-detector.js +91 -0
  16. package/src/detectors/language-detector.js +89 -0
  17. package/src/detectors/multi-project-detector.js +191 -0
  18. package/src/detectors/service-detector.js +244 -0
  19. package/src/detectors.js +30 -0
  20. package/src/exec-utils.js +215 -0
  21. package/src/fs-utils.js +34 -0
  22. package/src/git/base-git-provider.js +384 -0
  23. package/src/git/git-provider-registry.js +110 -0
  24. package/src/git/github-provider.js +502 -0
  25. package/src/mcp-http-server.js +313 -0
  26. package/src/patch/patch-engine.js +339 -0
  27. package/src/patch-manager.js +816 -0
  28. package/src/patch.js +607 -0
  29. package/src/patch_bkp.js +154 -0
  30. package/src/ports.js +69 -0
  31. package/src/routes/workspace.route.js +528 -0
  32. package/src/runtimes/base-runtime.js +290 -0
  33. package/src/runtimes/java/gradle-runtime.js +378 -0
  34. package/src/runtimes/java/java-integrations.js +339 -0
  35. package/src/runtimes/java/maven-runtime.js +418 -0
  36. package/src/runtimes/node/node-integrations.js +247 -0
  37. package/src/runtimes/node/npm-runtime.js +466 -0
  38. package/src/runtimes/node/yarn-runtime.js +354 -0
  39. package/src/runtimes/runtime-registry.js +256 -0
  40. package/src/server-local.js +576 -0
  41. package/src/server.js +4565 -0
  42. package/src/utils/environment-diagnostics.js +666 -0
  43. package/src/utils/exec-utils.js +264 -0
  44. package/src/utils/fs-utils.js +218 -0
  45. package/src/workspace/detect-port.js +176 -0
  46. package/src/workspace/file-reader.js +54 -0
  47. package/src/workspace/git-client.js +0 -0
  48. package/src/workspace/process-manager.js +619 -0
  49. package/src/workspace/scanner.js +72 -0
  50. package/src/workspace-manager.js +172 -0
package/src/server.js ADDED
@@ -0,0 +1,4565 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import bodyParser from "body-parser";
4
+ import path from "path";
5
+ import os from "os";
6
+ import fs from "fs";
7
+ const fsPromises = fs.promises;
8
+ import { exec } from "child_process";
9
+ import { promisify } from "util";
10
+ import { EventEmitter } from "events";
11
+ import { exists, listRecursive, readFile, writeFile } from "./fs-utils.js";
12
+ import { detectProject, readText } from "./detectors.js";
13
+ import { detectPort } from "./ports.js";
14
+ import { compileAndTest, run } from "./exec-utils.js";
15
+ import { applyUnifiedDiff } from "./patch.js";
16
+ import { WorkspaceScanner } from "./workspace/scanner.js";
17
+ import { FileReader } from "./workspace/file-reader.js";
18
+ import { LanguageDetector } from "./detectors/language-detector.js";
19
+ import { FrameworkDetector } from "./detectors/framework-detector.js";
20
+ import { ServiceDetector } from "./detectors/service-detector.js";
21
+ import { ProcessManager } from "./workspace/process-manager.js";
22
+ import { ControllerAnalyzer } from "./analyzers/controller-analyzer.js";
23
+ import { DTOAnalyzer } from "./analyzers/dto-analyzer.js";
24
+ import { ConfigAnalyzer } from "./analyzers/config-analyzer.js";
25
+ import { WorkspaceManager } from "./workspace-manager.js";
26
+ import { startMCPHttpServer } from "./mcp-http-server.js";
27
+
28
+ const execAsync = promisify(exec);
29
+
30
+ // ============================================
31
+ // 🧠 AI VIBE CODING ENGINE
32
+ // Sistema universal de auto-healing que usa AI
33
+ // para resolver QUALQUER erro automaticamente
34
+ // ============================================
35
+
36
+ class AIVibeCodingEngine extends EventEmitter {
37
+ constructor(processManager, getWorkspaceRoot) {
38
+ super();
39
+ this.processManager = processManager;
40
+ this.getWorkspaceRoot = getWorkspaceRoot;
41
+ this.gatewayUrl = process.env.GATEWAY_URL || 'http://localhost:8085';
42
+ this.maxRetries = 3;
43
+ this.isActive = true;
44
+
45
+ this.errorHistory = [];
46
+ this.fixHistory = [];
47
+ this.pendingFixes = [];
48
+ this.currentSession = null;
49
+ this.lastSuccessfulConfig = null;
50
+
51
+ console.log('🧠 [AI-Engine] Vibe Coding Engine initialized');
52
+ this.setupErrorMonitoring();
53
+ }
54
+
55
+ setupErrorMonitoring() {
56
+ this.processManager.on('log', async ({ serviceId, message, type }) => {
57
+ if (this.isActive && message && this.isError(message)) {
58
+ await this.handleRuntimeError(serviceId, message, type);
59
+ }
60
+ });
61
+
62
+ this.processManager.on('stopped', async ({ serviceId, code, signal }) => {
63
+ if (this.isActive && code !== 0 && code !== null) {
64
+ console.log(`🧠 [AI-Engine] Process ${serviceId} crashed (code: ${code})`);
65
+ if (this.pendingFixes.length > 0) {
66
+ console.log(`💡 [AI-Engine] ${this.pendingFixes.length} fixes pending`);
67
+ }
68
+ }
69
+ });
70
+ }
71
+
72
+ isError(message) {
73
+ if (!message || typeof message !== 'string') return false;
74
+ const errorPatterns = [
75
+ /exception/i,
76
+ /error.*failed/i,
77
+ /failed.*to.*start/i,
78
+ /connection.*refused/i,
79
+ /unable.*to.*connect/i,
80
+ /port.*in.*use/i,
81
+ /address.*already.*in.*use/i,
82
+ /compilation.*failed/i,
83
+ /syntax.*error/i,
84
+ /null.*pointer/i,
85
+ /class.*not.*found/i,
86
+ /no.*such.*file/i,
87
+ /permission.*denied/i,
88
+ /module.*not.*found/i,
89
+ /cannot.*find.*module/i,
90
+ /application.*run.*failed/i,
91
+ /bean.*creation.*exception/i,
92
+ /hikaripool.*exception/i, // Só exceções, não "HikariPool-1 - Starting"
93
+ /jdbc.*exception/i,
94
+ /datasource.*failed/i
95
+ ];
96
+
97
+ // Excluir falsos positivos (linhas normais que contêm palavras de erro)
98
+ const falsePositives = [
99
+ /hikaripool.*start/i, // "HikariPool-1 - Starting..." é normal
100
+ /hikaripool.*completed/i, // "HikariPool-1 - Start completed" é normal
101
+ /no active profile/i, // "No active profile set" é normal
102
+ /exposing.*endpoint/i, // Linha normal de startup
103
+ /started.*application/i, // "Started PurePilatesCoreApplication" é sucesso!
104
+ /tomcat started/i // "Tomcat started on port" é sucesso!
105
+ ];
106
+
107
+ // Se match com falso positivo, não é erro
108
+ if (falsePositives.some(p => p.test(message))) {
109
+ return false;
110
+ }
111
+
112
+ return errorPatterns.some(p => p.test(message));
113
+ }
114
+
115
+ classifyError(message) {
116
+ if (!message) return { type: 'unknown', severity: 'low', autoFixable: false };
117
+
118
+ const classifications = {
119
+ 'database_connection': {
120
+ patterns: [/connection.*refused.*\d{4}/i, /jdbc/i, /hikari/i, /sql.*server/i, /mysql/i, /postgres/i],
121
+ severity: 'high', autoFixable: true
122
+ },
123
+ 'port_conflict': {
124
+ patterns: [/port.*in.*use/i, /address.*already/i, /eaddrinuse/i],
125
+ severity: 'medium', autoFixable: true
126
+ },
127
+ 'compilation': {
128
+ patterns: [/compilation.*failed/i, /syntax.*error/i, /cannot.*resolve/i],
129
+ severity: 'high', autoFixable: true
130
+ },
131
+ 'dependency_missing': {
132
+ patterns: [/class.*not.*found/i, /module.*not.*found/i],
133
+ severity: 'high', autoFixable: true
134
+ },
135
+ 'configuration': {
136
+ patterns: [/no.*qualifying.*bean/i, /could.*not.*autowire/i],
137
+ severity: 'medium', autoFixable: true
138
+ }
139
+ };
140
+
141
+ for (const [type, config] of Object.entries(classifications)) {
142
+ if (config.patterns.some(p => p.test(message))) {
143
+ return { type, ...config };
144
+ }
145
+ }
146
+ return { type: 'unknown', severity: 'low', autoFixable: false };
147
+ }
148
+
149
+ async handleRuntimeError(serviceId, errorMessage, type) {
150
+ const classification = this.classifyError(errorMessage);
151
+
152
+ const recent = this.errorHistory.find(e =>
153
+ e.classification.type === classification.type &&
154
+ Date.now() - e.timestamp < 5000
155
+ );
156
+ if (recent) return;
157
+
158
+ const entry = {
159
+ id: `err_${Date.now()}`,
160
+ serviceId, message: errorMessage, classification,
161
+ timestamp: Date.now()
162
+ };
163
+
164
+ this.errorHistory.push(entry);
165
+ if (this.errorHistory.length > 200) this.errorHistory = this.errorHistory.slice(-200);
166
+
167
+ console.log(`🧠 [AI-Engine] Error: ${classification.type} (${classification.severity})`);
168
+
169
+ if (classification.autoFixable) {
170
+ const fix = this.getQuickFix(classification.type, errorMessage);
171
+ if (fix) {
172
+ console.log(`💡 [AI-Engine] Quick fix: ${fix.description}`);
173
+ this.pendingFixes.push({ errorId: entry.id, fix, timestamp: Date.now() });
174
+ }
175
+ }
176
+ }
177
+
178
+ getQuickFix(errorType, message) {
179
+ const fixes = {
180
+ 'database_connection': {
181
+ description: 'Switch to test/local profile with embedded DB',
182
+ action: 'change_profile',
183
+ profiles: ['test', 'local', 'h2']
184
+ },
185
+ 'port_conflict': {
186
+ description: 'Use different port',
187
+ action: 'change_port',
188
+ portIncrement: 1
189
+ },
190
+ 'compilation': {
191
+ description: 'Recompile project',
192
+ action: 'recompile'
193
+ },
194
+ 'dependency_missing': {
195
+ description: 'Reinstall dependencies',
196
+ action: 'reinstall_dependencies'
197
+ },
198
+ 'configuration': {
199
+ description: 'Use default config',
200
+ action: 'use_default_config'
201
+ }
202
+ };
203
+ return fixes[errorType] || null;
204
+ }
205
+
206
+ async startWithAutoHealing(config) {
207
+ console.log('🧠 [AI-Engine] Starting with auto-healing...');
208
+
209
+ this.currentSession = {
210
+ startTime: Date.now(),
211
+ attempts: [],
212
+ originalConfig: { ...config }
213
+ };
214
+
215
+ let currentConfig = { ...config };
216
+ let attempts = 0;
217
+ let lastError = null;
218
+
219
+ while (attempts < this.maxRetries) {
220
+ attempts++;
221
+ console.log(`\n🔄 [AI-Engine] Attempt ${attempts}/${this.maxRetries}`);
222
+
223
+ if (attempts > 1 && this.pendingFixes.length > 0) {
224
+ const fix = this.pendingFixes.shift();
225
+ console.log(`💡 Applying: ${fix.fix.description}`);
226
+ currentConfig = this.applyFix(currentConfig, fix.fix);
227
+ }
228
+
229
+ if (currentConfig.recompile) {
230
+ console.log('🔨 Recompiling...');
231
+ await this.recompile();
232
+ delete currentConfig.recompile;
233
+ }
234
+
235
+ const result = await this.attemptStart(currentConfig);
236
+
237
+ this.currentSession.attempts.push({
238
+ attempt: attempts,
239
+ config: { ...currentConfig },
240
+ result: result.success ? 'success' : 'failed',
241
+ error: result.error
242
+ });
243
+
244
+ if (result.success) {
245
+ console.log(`\n✅ [AI-Engine] Success after ${attempts} attempt(s)`);
246
+ this.lastSuccessfulConfig = { ...currentConfig };
247
+
248
+ if (attempts > 1) {
249
+ this.fixHistory.push({
250
+ original: this.currentSession.originalConfig,
251
+ fixed: currentConfig,
252
+ timestamp: Date.now()
253
+ });
254
+ }
255
+
256
+ return { ok: true, attempts, config: currentConfig, autoHealed: attempts > 1 };
257
+ }
258
+
259
+ lastError = result.error;
260
+ console.log(`❌ Attempt ${attempts} failed: ${lastError?.substring(0, 100)}...`);
261
+
262
+ const fix = this.getFix(result.error, currentConfig);
263
+ if (fix) {
264
+ console.log(`💡 Fix: ${fix.description}`);
265
+ currentConfig = this.applyFix(currentConfig, fix);
266
+ } else {
267
+ const ai = await this.analyzeWithAI('startup', lastError, currentConfig);
268
+ if (ai?.newConfig) {
269
+ console.log(`🤖 AI: ${ai.suggestion}`);
270
+ currentConfig = ai.newConfig;
271
+ } else {
272
+ break;
273
+ }
274
+ }
275
+ }
276
+
277
+ console.log(`\n❌ [AI-Engine] Failed after ${attempts} attempts`);
278
+ return { ok: false, attempts, error: lastError, config: currentConfig };
279
+ }
280
+
281
+ async attemptStart(config) {
282
+ return new Promise(async (resolve) => {
283
+ const logs = [];
284
+ let startupError = null;
285
+ let resolved = false;
286
+
287
+ const cleanup = () => {
288
+ this.processManager.off('log', logListener);
289
+ this.processManager.off('started', startedListener);
290
+ this.processManager.off('stopped', stoppedListener);
291
+ clearTimeout(timeout);
292
+ };
293
+
294
+ // Detectar sucesso do Spring Boot nos logs
295
+ const isStartupSuccess = (message) => {
296
+ if (!message) return false;
297
+ return /started.*in.*seconds/i.test(message) ||
298
+ /tomcat started on port/i.test(message) ||
299
+ /started.*application/i.test(message);
300
+ };
301
+
302
+ const logListener = ({ serviceId, message }) => {
303
+ if (serviceId === 'test-local' && message) {
304
+ logs.push(message);
305
+
306
+ // Detectar sucesso nos logs do Spring Boot
307
+ if (isStartupSuccess(message) && !resolved) {
308
+ console.log(`✅ [AI-Engine] Detected startup success: ${message.substring(0, 80)}...`);
309
+ resolved = true;
310
+ cleanup();
311
+ resolve({ success: true, logs });
312
+ return;
313
+ }
314
+
315
+ if (this.isError(message) && !startupError) {
316
+ startupError = message;
317
+ }
318
+ }
319
+ };
320
+
321
+ const startedListener = ({ serviceId }) => {
322
+ if (serviceId === 'test-local' && !resolved) {
323
+ resolved = true; cleanup();
324
+ resolve({ success: true, logs });
325
+ }
326
+ };
327
+
328
+ const stoppedListener = ({ serviceId, code }) => {
329
+ if (serviceId === 'test-local' && code !== 0 && !resolved) {
330
+ resolved = true; cleanup();
331
+ resolve({ success: false, error: startupError || `Exit code ${code}`, logs });
332
+ }
333
+ };
334
+
335
+ const timeout = setTimeout(() => {
336
+ if (!resolved) {
337
+ resolved = true; cleanup();
338
+ resolve(startupError
339
+ ? { success: false, error: startupError, logs }
340
+ : { success: true, logs, partial: true }
341
+ );
342
+ }
343
+ }, 90000);
344
+
345
+ this.processManager.on('log', logListener);
346
+ this.processManager.on('started', startedListener);
347
+ this.processManager.on('stopped', stoppedListener);
348
+
349
+ try {
350
+ await this.processManager.start('test-local', config);
351
+ } catch (err) {
352
+ if (!resolved) {
353
+ resolved = true; cleanup();
354
+ resolve({ success: false, error: err.message, logs });
355
+ }
356
+ }
357
+ });
358
+ }
359
+
360
+ getFix(error, config) {
361
+ const classification = this.classifyError(error);
362
+ return this.getQuickFix(classification.type, error);
363
+ }
364
+
365
+ applyFix(config, fix) {
366
+ const newConfig = { ...config };
367
+
368
+ switch (fix.action) {
369
+ case 'change_profile':
370
+ const profiles = fix.profiles || ['test', 'local'];
371
+ const next = profiles.find(p => p !== config.profile) || profiles[0];
372
+ newConfig.profile = next;
373
+ newConfig.args = this.updateArgs(config.args, 'profile', next);
374
+ newConfig.env = { ...config.env, SPRING_PROFILES_ACTIVE: next };
375
+ console.log(` → Profile: ${config.profile} → ${next}`);
376
+ break;
377
+
378
+ case 'change_port':
379
+ const newPort = (config.port || 8080) + 1;
380
+ newConfig.port = newPort;
381
+ newConfig.args = this.updateArgs(config.args, 'port', newPort);
382
+ newConfig.env = { ...config.env, SERVER_PORT: String(newPort), PORT: String(newPort) };
383
+ console.log(` → Port: ${config.port} → ${newPort}`);
384
+ break;
385
+
386
+ case 'recompile':
387
+ newConfig.recompile = true;
388
+ break;
389
+
390
+ case 'reinstall_dependencies':
391
+ newConfig.recompile = true;
392
+ break;
393
+
394
+ case 'use_default_config':
395
+ newConfig.profile = null;
396
+ newConfig.args = this.removeArg(config.args, 'profile');
397
+ delete newConfig.env?.SPRING_PROFILES_ACTIVE;
398
+ break;
399
+ }
400
+
401
+ return newConfig;
402
+ }
403
+
404
+ async analyzeWithAI(errorType, error, config) {
405
+ try {
406
+ const workspaceRoot = this.getWorkspaceRoot();
407
+ const configFiles = this.collectConfigFiles(workspaceRoot);
408
+
409
+ const response = await fetch(`${this.gatewayUrl}/api/test-local/analyze-startup-error`, {
410
+ method: 'POST',
411
+ headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': 'default' },
412
+ body: JSON.stringify({
413
+ context: { workspace: workspaceRoot, error, configFiles },
414
+ currentConfig: config
415
+ }),
416
+ signal: AbortSignal.timeout(30000)
417
+ });
418
+
419
+ if (response.ok) return await response.json();
420
+ } catch (err) {
421
+ console.log(`⚠️ [AI-Engine] AI unavailable: ${err.message}`);
422
+ }
423
+
424
+ return this.localFallback(errorType, error, config);
425
+ }
426
+
427
+ localFallback(errorType, error, config) {
428
+ const tried = this.currentSession?.attempts?.map(a => a.config?.profile).filter(Boolean) || [];
429
+ const all = ['test', 'local', 'h2', 'dev'];
430
+ const next = all.find(p => !tried.includes(p));
431
+
432
+ if (next) {
433
+ return {
434
+ suggestion: `Trying ${next} profile`,
435
+ newConfig: {
436
+ ...config,
437
+ profile: next,
438
+ args: this.updateArgs(config.args, 'profile', next),
439
+ env: { ...config.env, SPRING_PROFILES_ACTIVE: next }
440
+ }
441
+ };
442
+ }
443
+ return null;
444
+ }
445
+
446
+ async recompile() {
447
+ const workspaceRoot = this.getWorkspaceRoot();
448
+ if (!workspaceRoot) return { success: false };
449
+
450
+ try {
451
+ const meta = await detectProject(workspaceRoot);
452
+ let cmd = meta.buildTool === 'maven' ? 'mvn clean install -DskipTests' :
453
+ meta.buildTool === 'gradle' ? './gradlew clean build -x test' : null;
454
+
455
+ if (cmd) {
456
+ await execAsync(cmd, { cwd: workspaceRoot, timeout: 300000 });
457
+ }
458
+ return { success: true };
459
+ } catch (err) {
460
+ return { success: false, error: err.message };
461
+ }
462
+ }
463
+
464
+ collectConfigFiles(workspaceRoot) {
465
+ if (!workspaceRoot) return {};
466
+ const configs = {};
467
+ const paths = [
468
+ 'src/main/resources/application.yml',
469
+ 'src/main/resources/application-dev.yml',
470
+ 'src/main/resources/application-local.yml',
471
+ 'src/main/resources/application-test.yml',
472
+ 'pom.xml', 'package.json', '.env'
473
+ ];
474
+
475
+ for (const p of paths) {
476
+ const fullPath = path.join(workspaceRoot, p);
477
+ if (fs.existsSync(fullPath)) {
478
+ try {
479
+ configs[p] = fs.readFileSync(fullPath, 'utf8').substring(0, 5000);
480
+ } catch (e) {}
481
+ }
482
+ }
483
+ return configs;
484
+ }
485
+
486
+ updateArgs(args, type, value) {
487
+ if (!args) args = [];
488
+ let newArgs = [...args];
489
+
490
+ if (type === 'profile') {
491
+ newArgs = newArgs.filter(a => !a.includes('spring.profiles.active'));
492
+ const jarIdx = newArgs.findIndex(a => a === '-jar');
493
+ if (jarIdx >= 0) {
494
+ newArgs.splice(jarIdx, 0, `-Dspring.profiles.active=${value}`);
495
+ } else {
496
+ newArgs.unshift(`-Dspring.profiles.active=${value}`);
497
+ }
498
+ }
499
+
500
+ if (type === 'port') {
501
+ newArgs = newArgs.filter(a => !a.includes('server.port'));
502
+ newArgs.push(`--server.port=${value}`);
503
+ }
504
+
505
+ return newArgs;
506
+ }
507
+
508
+ removeArg(args, type) {
509
+ if (!args) return [];
510
+ if (type === 'profile') {
511
+ return args.filter(a => !a.includes('spring.profiles.active'));
512
+ }
513
+ return args;
514
+ }
515
+
516
+ getStatus() {
517
+ return {
518
+ active: this.isActive,
519
+ gatewayUrl: this.gatewayUrl,
520
+ maxRetries: this.maxRetries,
521
+ errors: this.errorHistory.length,
522
+ fixes: this.fixHistory.length,
523
+ pending: this.pendingFixes.length,
524
+ lastConfig: this.lastSuccessfulConfig
525
+ };
526
+ }
527
+
528
+ setActive(active) {
529
+ this.isActive = active;
530
+ console.log(`🧠 [AI-Engine] ${active ? 'ENABLED' : 'DISABLED'}`);
531
+ }
532
+
533
+ clearHistory() {
534
+ this.errorHistory = [];
535
+ this.fixHistory = [];
536
+ this.pendingFixes = [];
537
+ }
538
+ }
539
+
540
+ // Instância global do AI Engine
541
+ let aiEngine = null;
542
+
543
+ const app = express();
544
+
545
+ // ✅ FIXED: Support Cloud Run PORT environment variable (GCP uses PORT)
546
+ const PORT = process.env.PORT || process.env.LOCAL_AGENT_PORT || 5055;
547
+
548
+ // ✅ FIXED: Allow CORS from Cloud Run and local development
549
+ app.use(cors({
550
+ origin: [
551
+ "http://localhost:3010",
552
+ "http://localhost:3000",
553
+ "http://localhost:8085",
554
+ "http://127.0.0.1:3010",
555
+ "http://127.0.0.1:3000",
556
+ "http://127.0.0.1:8085",
557
+ // Cloud Run URLs (regex patterns)
558
+ /https:\/\/.*\.run\.app$/,
559
+ /https:\/\/.*\.web\.app$/
560
+ ],
561
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
562
+ allowedHeaders: ["Content-Type", "Authorization", "X-Tenant-ID"],
563
+ credentials: true
564
+ }));
565
+
566
+ // ✅ Handle preflight requests explicitly
567
+ app.options('*', cors());
568
+
569
+ app.use(bodyParser.json({ limit: "50mb" }));
570
+
571
+ // 🔧 DEFAULT WORKSPACE - Define o workspace padrão
572
+ // Pode ser sobrescrito via variável de ambiente ou POST /workspace/open
573
+ const DEFAULT_WORKSPACE = process.env.DEFAULT_WORKSPACE || '/Users/macintosh/IdeaProjects/pure-core-ms';
574
+
575
+ let WORKSPACE_ROOT = fs.existsSync(DEFAULT_WORKSPACE) ? DEFAULT_WORKSPACE : null;
576
+ if (WORKSPACE_ROOT) {
577
+ console.log(`📁 Default workspace: ${WORKSPACE_ROOT}`);
578
+ }
579
+
580
+ let DETECTED_SERVICES = [];
581
+ const processManager = new ProcessManager();
582
+ const wsManager = new WorkspaceManager();
583
+ const MCP_PORT = process.env.MCP_PORT || 5056;
584
+
585
+ // 🧠 Inicializar AI Vibe Coding Engine
586
+ aiEngine = new AIVibeCodingEngine(processManager, () => WORKSPACE_ROOT);
587
+
588
+ // ============================================
589
+ // 🆕 BACKUP STORAGE (Sprint 1.3)
590
+ // In-memory backup storage with configurable max size
591
+ // ============================================
592
+ const BACKUPS = new Map();
593
+ const MAX_BACKUPS = 50;
594
+ const BACKUP_INDEX_PATH = path.join(os.tmpdir(), 'deepdebug-backups-index.json');
595
+
596
+ /**
597
+ * Persist backup index to disk so diffs survive server restarts.
598
+ * Only saves the index (backupId → files paths), not the file contents.
599
+ * File contents are read from the backup directory on disk.
600
+ */
601
+ function saveBackupIndex() {
602
+ try {
603
+ const index = {};
604
+ for (const [id, backup] of BACKUPS.entries()) {
605
+ index[id] = {
606
+ timestamp: backup.timestamp,
607
+ incidentId: backup.incidentId,
608
+ files: backup.files.map(f => ({
609
+ path: f.path,
610
+ backupPath: f.backupPath || null // path to backup copy on disk
611
+ }))
612
+ };
613
+ }
614
+ fs.writeFileSync(BACKUP_INDEX_PATH, JSON.stringify(index, null, 2));
615
+ } catch (e) {
616
+ console.warn('Could not save backup index:', e.message);
617
+ }
618
+ }
619
+
620
+ function loadBackupIndex() {
621
+ try {
622
+ if (fs.existsSync(BACKUP_INDEX_PATH)) {
623
+ const index = JSON.parse(fs.readFileSync(BACKUP_INDEX_PATH, 'utf8'));
624
+ for (const [id, backup] of Object.entries(index)) {
625
+ // Restore backup with file contents read from disk
626
+ const files = [];
627
+ for (const file of backup.files) {
628
+ if (file.backupPath && fs.existsSync(file.backupPath)) {
629
+ files.push({
630
+ path: file.path,
631
+ content: fs.readFileSync(file.backupPath, 'utf8'),
632
+ backupPath: file.backupPath
633
+ });
634
+ }
635
+ }
636
+ if (files.length > 0) {
637
+ BACKUPS.set(id, {
638
+ timestamp: backup.timestamp,
639
+ incidentId: backup.incidentId,
640
+ files
641
+ });
642
+ }
643
+ }
644
+ console.log(`📦 Restored ${BACKUPS.size} backups from disk`);
645
+ }
646
+ } catch (e) {
647
+ console.warn('Could not load backup index:', e.message);
648
+ }
649
+ }
650
+
651
+ // Load backups on startup
652
+ loadBackupIndex();
653
+
654
+ // Event listeners do ProcessManager
655
+ processManager.on("started", ({ serviceId }) => {
656
+ console.log(`✅ Service ${serviceId} started successfully`);
657
+ updateServiceStatus(serviceId, "running");
658
+ addServerLog("info", `Service ${serviceId} started successfully`);
659
+ });
660
+
661
+ processManager.on("stopped", ({ serviceId }) => {
662
+ console.log(`ℹ️ Service ${serviceId} stopped`);
663
+ updateServiceStatus(serviceId, "stopped");
664
+ addServerLog("info", `Service ${serviceId} stopped`);
665
+ });
666
+
667
+ processManager.on("error", ({ serviceId, error }) => {
668
+ console.error(`❌ Service ${serviceId} error: ${error}`);
669
+ updateServiceStatus(serviceId, "failed");
670
+ addServerLog("error", `Service ${serviceId} error: ${error}`);
671
+ });
672
+
673
+ processManager.on("log", ({ serviceId, message, type }) => {
674
+ // Capture stdout/stderr from service
675
+ console.log(`[${serviceId}] ${message}`);
676
+ addServerLog(type || "stdout", message);
677
+ });
678
+
679
+ function addServerLog(type, line) {
680
+ const log = {
681
+ type: type,
682
+ line: line,
683
+ timestamp: Date.now()
684
+ };
685
+
686
+ TEST_LOCAL_STATE.serverLogs.push(log);
687
+
688
+ // Keep only last 1000 logs (circular buffer)
689
+ if (TEST_LOCAL_STATE.serverLogs.length > 1000) {
690
+ TEST_LOCAL_STATE.serverLogs.shift();
691
+ }
692
+ }
693
+
694
+ function updateServiceStatus(serviceId, status) {
695
+ const service = DETECTED_SERVICES.find(s => s.id === serviceId);
696
+ if (service) {
697
+ service.status = status;
698
+ }
699
+ }
700
+
701
+ /** Health */
702
+ app.get("/health", (_req, res) => {
703
+ res.json({
704
+ status: "ok",
705
+ workspace: WORKSPACE_ROOT || null,
706
+ services: DETECTED_SERVICES.length,
707
+ mcpPort: MCP_PORT,
708
+ openWorkspaces: wsManager.count,
709
+ workspaces: wsManager.list().map(w => ({
710
+ id: w.id, root: w.root, language: w.projectInfo?.language
711
+ }))
712
+ });
713
+ });
714
+
715
+ /** Define/abre o workspace local */
716
+ app.post("/workspace/open", async (req, res) => {
717
+ const { root, workspaceId } = req.body || {};
718
+ if (!root) return res.status(400).json({ error: "root is required" });
719
+ const abs = path.resolve(root);
720
+ if (!(await exists(abs))) return res.status(404).json({ error: "path not found" });
721
+
722
+ WORKSPACE_ROOT = abs;
723
+
724
+ // Registar no WorkspaceManager (multi-workspace support)
725
+ const wsId = workspaceId || "default";
726
+ try {
727
+ await wsManager.open(wsId, abs);
728
+ } catch (err) {
729
+ console.warn(`⚠️ WorkspaceManager open failed (non-fatal): ${err.message}`);
730
+ }
731
+
732
+ const meta = await detectProject(WORKSPACE_ROOT);
733
+ const port = await detectPort(WORKSPACE_ROOT);
734
+ res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, meta, port });
735
+ });
736
+
737
+ /** Info do workspace */
738
+ app.get("/workspace/info", async (_req, res) => {
739
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
740
+ const meta = await detectProject(WORKSPACE_ROOT);
741
+ const port = await detectPort(WORKSPACE_ROOT);
742
+ res.json({ root: WORKSPACE_ROOT, meta, port });
743
+ });
744
+
745
+ /** Scan completo do workspace */
746
+ app.get("/workspace/scan", async (_req, res) => {
747
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
748
+
749
+ try {
750
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
751
+ const structure = await scanner.scan();
752
+ res.json(structure);
753
+ } catch (err) {
754
+ res.status(500).json({ error: err.message });
755
+ }
756
+ });
757
+
758
+ /** Análise completa: language + framework */
759
+ app.get("/workspace/analyze", async (_req, res) => {
760
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
761
+
762
+ try {
763
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
764
+ const structure = await scanner.scan();
765
+
766
+ const languageDetector = new LanguageDetector(structure.files);
767
+ const languageInfo = languageDetector.detect();
768
+
769
+ const fileReader = new FileReader(WORKSPACE_ROOT);
770
+ const frameworkDetector = new FrameworkDetector(
771
+ languageInfo.primary,
772
+ structure.files,
773
+ fileReader
774
+ );
775
+ const frameworkInfo = await frameworkDetector.detect();
776
+
777
+ res.json({
778
+ workspace: WORKSPACE_ROOT,
779
+ language: languageInfo,
780
+ framework: frameworkInfo,
781
+ stats: structure.metadata,
782
+ analyzedAt: new Date().toISOString()
783
+ });
784
+ } catch (err) {
785
+ res.status(500).json({ error: err.message });
786
+ }
787
+ });
788
+
789
+ /** Lê conteúdo de arquivo específico */
790
+ app.get("/workspace/file-content", async (req, res) => {
791
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
792
+
793
+ const { path: relativePath } = req.query;
794
+ if (!relativePath) return res.status(400).json({ error: "path query param required" });
795
+
796
+ try {
797
+ const reader = new FileReader(WORKSPACE_ROOT);
798
+ const file = await reader.read(relativePath);
799
+ res.json(file);
800
+ } catch (err) {
801
+ res.status(404).json({ error: "file not found", details: err.message });
802
+ }
803
+ });
804
+
805
+ /** Lê múltiplos arquivos */
806
+ app.post("/workspace/batch-read", async (req, res) => {
807
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
808
+
809
+ const { paths } = req.body || {};
810
+ if (!paths || !Array.isArray(paths)) {
811
+ return res.status(400).json({ error: "paths array required" });
812
+ }
813
+
814
+ try {
815
+ const reader = new FileReader(WORKSPACE_ROOT);
816
+ const files = await reader.readMultiple(paths);
817
+ res.json(files);
818
+ } catch (err) {
819
+ res.status(500).json({ error: err.message });
820
+ }
821
+ });
822
+
823
+ // ============================================
824
+ // 🆕 FILE VALIDATION ENDPOINTS (Enhanced Analysis)
825
+ // ============================================
826
+
827
+ /**
828
+ * GET /workspace/file-exists
829
+ * Checks if a file exists in the workspace
830
+ */
831
+ app.get("/workspace/file-exists", async (req, res) => {
832
+ if (!WORKSPACE_ROOT) {
833
+ return res.status(400).json({ error: "workspace not set" });
834
+ }
835
+
836
+ const { path: relativePath } = req.query;
837
+ if (!relativePath) {
838
+ return res.status(400).json({ error: "path query param required" });
839
+ }
840
+
841
+ try {
842
+ const fullPath = path.join(WORKSPACE_ROOT, relativePath);
843
+ const fileExists = await exists(fullPath);
844
+
845
+ console.log(`🔍 [file-exists] ${relativePath} -> ${fileExists ? 'EXISTS' : 'NOT FOUND'}`);
846
+
847
+ res.json({
848
+ ok: true,
849
+ exists: fileExists,
850
+ path: relativePath,
851
+ fullPath: fullPath
852
+ });
853
+ } catch (err) {
854
+ console.error(`❌ [file-exists] Error:`, err.message);
855
+ res.status(500).json({ ok: false, error: err.message });
856
+ }
857
+ });
858
+
859
+ /**
860
+ * POST /workspace/validate-paths
861
+ * Validates multiple file paths at once
862
+ */
863
+ app.post("/workspace/validate-paths", async (req, res) => {
864
+ if (!WORKSPACE_ROOT) {
865
+ return res.status(400).json({ error: "workspace not set" });
866
+ }
867
+
868
+ const { paths: pathList } = req.body || {};
869
+ if (!pathList || !Array.isArray(pathList)) {
870
+ return res.status(400).json({ error: "paths array required in body" });
871
+ }
872
+
873
+ try {
874
+ const results = await Promise.all(
875
+ pathList.map(async (relativePath) => {
876
+ const fullPath = path.join(WORKSPACE_ROOT, relativePath);
877
+ const fileExists = await exists(fullPath);
878
+ return { path: relativePath, exists: fileExists };
879
+ })
880
+ );
881
+
882
+ const allExist = results.every(r => r.exists);
883
+ const missingPaths = results.filter(r => !r.exists).map(r => r.path);
884
+
885
+ console.log(`🔍 [validate-paths] Checked ${pathList.length} paths, ${missingPaths.length} missing`);
886
+
887
+ res.json({
888
+ ok: true,
889
+ results,
890
+ allExist,
891
+ missingPaths,
892
+ totalChecked: pathList.length
893
+ });
894
+ } catch (err) {
895
+ console.error(`❌ [validate-paths] Error:`, err.message);
896
+ res.status(500).json({ ok: false, error: err.message });
897
+ }
898
+ });
899
+
900
+ /**
901
+ * POST /workspace/search-file
902
+ * Searches for a file by name in the workspace
903
+ *
904
+ * FIXED: Handle both string arrays and object arrays from listRecursive
905
+ */
906
+ app.post("/workspace/search-file", async (req, res) => {
907
+ if (!WORKSPACE_ROOT) {
908
+ return res.status(400).json({ error: "workspace not set" });
909
+ }
910
+
911
+ const { fileName } = req.body || {};
912
+ if (!fileName) {
913
+ return res.status(400).json({ error: "fileName required in body" });
914
+ }
915
+
916
+ try {
917
+ console.log(`🔍 [search-file] Searching for: ${fileName}`);
918
+
919
+ const rawFiles = await listRecursive(WORKSPACE_ROOT, {
920
+ maxDepth: 15,
921
+ includeHidden: false,
922
+ extensions: null
923
+ });
924
+
925
+ // 🆕 FIXED: Normalize files to string paths
926
+ // listRecursive may return strings OR objects like {path: 'xxx', name: 'yyy'}
927
+ const allFiles = rawFiles.map(f => {
928
+ if (typeof f === 'string') return f;
929
+ if (f && typeof f === 'object' && f.path) return f.path;
930
+ if (f && typeof f === 'object' && f.name) return f.name;
931
+ return String(f);
932
+ }).filter(f => f && typeof f === 'string');
933
+
934
+ console.log(`📁 [search-file] Scanning ${allFiles.length} files`);
935
+
936
+ // Strategy 1: Exact path match
937
+ let foundPath = allFiles.find(f => f.endsWith('/' + fileName) || f === fileName);
938
+
939
+ // Strategy 2: Case-insensitive basename match
940
+ if (!foundPath) {
941
+ const fileNameLower = fileName.toLowerCase();
942
+ foundPath = allFiles.find(f => {
943
+ const basename = path.basename(f);
944
+ return basename.toLowerCase() === fileNameLower;
945
+ });
946
+ }
947
+
948
+ // Strategy 3: Partial name match (without extension)
949
+ if (!foundPath) {
950
+ const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, '').toLowerCase();
951
+ foundPath = allFiles.find(f => {
952
+ const basename = path.basename(f).toLowerCase();
953
+ return basename.includes(fileNameWithoutExt);
954
+ });
955
+ }
956
+
957
+ // Strategy 4: Package-based search (for Java files like com.pure.core.SomeClass)
958
+ if (!foundPath && fileName.includes('.')) {
959
+ const packagePath = fileName.replace(/\./g, '/');
960
+ foundPath = allFiles.find(f => f.includes(packagePath));
961
+ }
962
+
963
+ if (foundPath) {
964
+ console.log(`✅ [search-file] Found: ${foundPath}`);
965
+ res.json({ ok: true, found: true, path: foundPath, fileName });
966
+ } else {
967
+ console.log(`⚠️ [search-file] Not found: ${fileName}`);
968
+ res.json({ ok: true, found: false, fileName, searchedFiles: allFiles.length });
969
+ }
970
+ } catch (err) {
971
+ console.error(`❌ [search-file] Error:`, err.message);
972
+ res.status(500).json({ ok: false, error: err.message });
973
+ }
974
+ });
975
+
976
+ /**
977
+ * POST /workspace/search-by-content
978
+ * Searches for files containing specific terms
979
+ *
980
+ * FIXED: Handle both string arrays and object arrays from listRecursive
981
+ */
982
+ app.post("/workspace/search-by-content", async (req, res) => {
983
+ if (!WORKSPACE_ROOT) {
984
+ return res.status(400).json({ error: "workspace not set" });
985
+ }
986
+
987
+ const { terms, extensions, maxResults = 10 } = req.body || {};
988
+
989
+ if (!terms || !Array.isArray(terms) || terms.length === 0) {
990
+ return res.status(400).json({ error: "terms array required" });
991
+ }
992
+
993
+ try {
994
+ console.log(`🔍 [search-by-content] Searching for terms: ${terms.join(', ')}`);
995
+
996
+ const rawFiles = await listRecursive(WORKSPACE_ROOT, {
997
+ maxDepth: 15,
998
+ includeHidden: false
999
+ });
1000
+
1001
+ // 🆕 FIXED: Normalize files to string paths
1002
+ const allFiles = rawFiles.map(f => {
1003
+ if (typeof f === 'string') return f;
1004
+ if (f && typeof f === 'object' && f.path) return f.path;
1005
+ if (f && typeof f === 'object' && f.name) return f.name;
1006
+ return String(f);
1007
+ }).filter(f => f && typeof f === 'string');
1008
+
1009
+ const filteredFiles = allFiles.filter(filePath => {
1010
+ if (!extensions || extensions.length === 0) return true;
1011
+ return extensions.some(ext => filePath.endsWith(ext));
1012
+ });
1013
+
1014
+ console.log(`📁 [search-by-content] Scanning ${filteredFiles.length} files`);
1015
+
1016
+ const results = [];
1017
+
1018
+ for (const filePath of filteredFiles) {
1019
+ if (results.length >= maxResults * 2) break;
1020
+
1021
+ try {
1022
+ const fullPath = path.join(WORKSPACE_ROOT, filePath);
1023
+ const content = await readFile(fullPath, 'utf8');
1024
+ const lines = content.split('\n');
1025
+
1026
+ for (const term of terms) {
1027
+ for (let i = 0; i < lines.length; i++) {
1028
+ if (lines[i].includes(term)) {
1029
+ results.push({
1030
+ path: filePath,
1031
+ matchedTerm: term,
1032
+ lineNumber: i + 1,
1033
+ lineContent: lines[i].trim().substring(0, 100)
1034
+ });
1035
+ break;
1036
+ }
1037
+ }
1038
+ }
1039
+ } catch (readErr) {
1040
+ continue;
1041
+ }
1042
+ }
1043
+
1044
+ results.sort((a, b) => {
1045
+ const aIsDto = a.path.toLowerCase().includes('request') || a.path.toLowerCase().includes('dto');
1046
+ const bIsDto = b.path.toLowerCase().includes('request') || b.path.toLowerCase().includes('dto');
1047
+ if (aIsDto && !bIsDto) return -1;
1048
+ if (!aIsDto && bIsDto) return 1;
1049
+ return 0;
1050
+ });
1051
+
1052
+ const seen = new Set();
1053
+ const dedupedResults = results.filter(r => {
1054
+ if (seen.has(r.path)) return false;
1055
+ seen.add(r.path);
1056
+ return true;
1057
+ }).slice(0, maxResults);
1058
+
1059
+ console.log(`✅ [search-by-content] Found ${dedupedResults.length} matching files`);
1060
+
1061
+ res.json({
1062
+ ok: true,
1063
+ results: dedupedResults,
1064
+ totalSearched: filteredFiles.length,
1065
+ termsSearched: terms
1066
+ });
1067
+ } catch (err) {
1068
+ console.error(`❌ [search-by-content] Error:`, err.message);
1069
+ res.status(500).json({ ok: false, error: err.message });
1070
+ }
1071
+ });
1072
+
1073
+ /**
1074
+ * POST /workspace/find-field-definition
1075
+ * Finds where a specific field is defined in the codebase
1076
+ *
1077
+ * FIXED: Handle both string arrays and object arrays from listRecursive
1078
+ */
1079
+ app.post("/workspace/find-field-definition", async (req, res) => {
1080
+ if (!WORKSPACE_ROOT) {
1081
+ return res.status(400).json({ error: "workspace not set" });
1082
+ }
1083
+
1084
+ const { fieldName, fileType = "java" } = req.body || {};
1085
+
1086
+ if (!fieldName) {
1087
+ return res.status(400).json({ error: "fieldName required" });
1088
+ }
1089
+
1090
+ try {
1091
+ console.log(`🔍 [find-field] Searching for field: ${fieldName}`);
1092
+
1093
+ const rawFiles = await listRecursive(WORKSPACE_ROOT, {
1094
+ maxDepth: 15,
1095
+ includeHidden: false
1096
+ });
1097
+
1098
+ // 🆕 FIXED: Normalize files to string paths
1099
+ const allFiles = rawFiles.map(f => {
1100
+ if (typeof f === 'string') return f;
1101
+ if (f && typeof f === 'object' && f.path) return f.path;
1102
+ if (f && typeof f === 'object' && f.name) return f.name;
1103
+ return String(f);
1104
+ }).filter(f => f && typeof f === 'string');
1105
+
1106
+ const targetFiles = allFiles.filter(filePath => filePath.endsWith(`.${fileType}`));
1107
+ const definitions = [];
1108
+
1109
+ const fieldPatterns = [
1110
+ new RegExp(`(private|protected|public)\\s+\\w+\\s+${fieldName}\\s*[;=]`, 'i'),
1111
+ new RegExp(`(private|protected|public)\\s+\\w+<[^>]+>\\s+${fieldName}\\s*[;=]`, 'i'),
1112
+ new RegExp(`^\\s*\\w+\\s+${fieldName}\\s*;`, 'i')
1113
+ ];
1114
+
1115
+ for (const filePath of targetFiles) {
1116
+ try {
1117
+ const fullPath = path.join(WORKSPACE_ROOT, filePath);
1118
+ const content = await readFile(fullPath, 'utf8');
1119
+ const lines = content.split('\n');
1120
+
1121
+ for (let i = 0; i < lines.length; i++) {
1122
+ const line = lines[i];
1123
+
1124
+ for (const pattern of fieldPatterns) {
1125
+ if (pattern.test(line)) {
1126
+ const annotations = [];
1127
+ for (let j = i - 1; j >= Math.max(0, i - 5); j--) {
1128
+ const prevLine = lines[j].trim();
1129
+ if (prevLine.startsWith('@')) {
1130
+ const match = prevLine.match(/@(\w+)/);
1131
+ if (match) annotations.unshift(match[0]);
1132
+ } else if (prevLine && !prevLine.startsWith('//') && !prevLine.startsWith('*')) {
1133
+ break;
1134
+ }
1135
+ }
1136
+
1137
+ let className = null;
1138
+ for (const l of lines) {
1139
+ const classMatch = l.match(/class\s+(\w+)/);
1140
+ if (classMatch) {
1141
+ className = classMatch[1];
1142
+ break;
1143
+ }
1144
+ }
1145
+
1146
+ definitions.push({
1147
+ path: filePath,
1148
+ lineNumber: i + 1,
1149
+ lineContent: line.trim(),
1150
+ annotations,
1151
+ className
1152
+ });
1153
+ break;
1154
+ }
1155
+ }
1156
+ }
1157
+ } catch (readErr) {
1158
+ continue;
1159
+ }
1160
+ }
1161
+
1162
+ definitions.sort((a, b) => {
1163
+ const aIsDto = a.path.toLowerCase().includes('request') || a.path.toLowerCase().includes('dto');
1164
+ const bIsDto = b.path.toLowerCase().includes('request') || b.path.toLowerCase().includes('dto');
1165
+ if (aIsDto && !bIsDto) return -1;
1166
+ if (!aIsDto && bIsDto) return 1;
1167
+ return 0;
1168
+ });
1169
+
1170
+ console.log(`✅ [find-field] Found ${definitions.length} definitions for '${fieldName}'`);
1171
+
1172
+ res.json({
1173
+ ok: true,
1174
+ fieldName,
1175
+ definitions,
1176
+ totalSearched: targetFiles.length
1177
+ });
1178
+ } catch (err) {
1179
+ console.error(`❌ [find-field] Error:`, err.message);
1180
+ res.status(500).json({ ok: false, error: err.message });
1181
+ }
1182
+ });
1183
+
1184
+
1185
+ // ============================================
1186
+ // 🆕 RUNTIME MANAGEMENT ENDPOINTS
1187
+ // ============================================
1188
+
1189
+ /** Detecta serviços no workspace */
1190
+ app.get("/workspace/services/detect", async (_req, res) => {
1191
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1192
+
1193
+ try {
1194
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
1195
+ const structure = await scanner.scan();
1196
+
1197
+ const languageDetector = new LanguageDetector(structure.files);
1198
+ const languageInfo = languageDetector.detect();
1199
+
1200
+ const fileReader = new FileReader(WORKSPACE_ROOT);
1201
+ const frameworkDetector = new FrameworkDetector(
1202
+ languageInfo.primary,
1203
+ structure.files,
1204
+ fileReader
1205
+ );
1206
+ const frameworkInfo = await frameworkDetector.detect();
1207
+
1208
+ const serviceDetector = new ServiceDetector(
1209
+ WORKSPACE_ROOT,
1210
+ languageInfo,
1211
+ frameworkInfo
1212
+ );
1213
+
1214
+ DETECTED_SERVICES = await serviceDetector.detect();
1215
+
1216
+ res.json({
1217
+ services: DETECTED_SERVICES,
1218
+ detectedAt: new Date().toISOString()
1219
+ });
1220
+ } catch (err) {
1221
+ res.status(500).json({ error: err.message });
1222
+ }
1223
+ });
1224
+
1225
+ /** Lista todos os serviços */
1226
+ app.get("/workspace/services", (_req, res) => {
1227
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1228
+
1229
+ // Atualizar status dos serviços com info do ProcessManager
1230
+ const servicesWithStatus = DETECTED_SERVICES.map(service => {
1231
+ const status = processManager.getStatus(service.id);
1232
+ return {
1233
+ ...service,
1234
+ ...status
1235
+ };
1236
+ });
1237
+
1238
+ res.json({ services: servicesWithStatus });
1239
+ });
1240
+
1241
+ /** Inicia um serviço */
1242
+ app.post("/workspace/services/:serviceId/start", async (req, res) => {
1243
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1244
+
1245
+ const { serviceId } = req.params;
1246
+ const service = DETECTED_SERVICES.find(s => s.id === serviceId);
1247
+
1248
+ if (!service) {
1249
+ return res.status(404).json({ error: "service not found" });
1250
+ }
1251
+
1252
+ try {
1253
+ const result = await processManager.start(serviceId, {
1254
+ language: service.language,
1255
+ framework: service.framework,
1256
+ buildTool: service.buildTool,
1257
+ cwd: service.path,
1258
+ port: service.port
1259
+ });
1260
+
1261
+ res.json(result);
1262
+ } catch (err) {
1263
+ res.status(500).json({ error: err.message });
1264
+ }
1265
+ });
1266
+
1267
+ /** Para um serviço */
1268
+ app.post("/workspace/services/:serviceId/stop", async (req, res) => {
1269
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1270
+
1271
+ const { serviceId } = req.params;
1272
+
1273
+ try {
1274
+ const result = await processManager.stop(serviceId);
1275
+ res.json(result);
1276
+ } catch (err) {
1277
+ res.status(500).json({ error: err.message });
1278
+ }
1279
+ });
1280
+
1281
+ /** Retorna status de um serviço */
1282
+ app.get("/workspace/services/:serviceId/status", (req, res) => {
1283
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1284
+
1285
+ const { serviceId } = req.params;
1286
+ const status = processManager.getStatus(serviceId);
1287
+
1288
+ res.json({ serviceId, ...status });
1289
+ });
1290
+
1291
+ /** Retorna logs de um serviço */
1292
+ app.get("/workspace/services/:serviceId/logs", (req, res) => {
1293
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1294
+
1295
+ const { serviceId } = req.params;
1296
+ const { limit = 100 } = req.query;
1297
+
1298
+ try {
1299
+ const logs = processManager.getLogs(serviceId, parseInt(limit));
1300
+ res.json({ serviceId, logs });
1301
+ } catch (err) {
1302
+ res.status(500).json({ error: err.message });
1303
+ }
1304
+ });
1305
+
1306
+ /** Streaming de logs em tempo real (SSE) */
1307
+ app.get("/workspace/services/:serviceId/logs/stream", (req, res) => {
1308
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1309
+
1310
+ const { serviceId } = req.params;
1311
+
1312
+ // Setup SSE
1313
+ res.setHeader("Content-Type", "text/event-stream");
1314
+ res.setHeader("Cache-Control", "no-cache");
1315
+ res.setHeader("Connection", "keep-alive");
1316
+
1317
+ // Enviar logs existentes
1318
+ const existingLogs = processManager.getLogs(serviceId);
1319
+ existingLogs.forEach(log => {
1320
+ res.write(`data: ${JSON.stringify(log)}\n\n`);
1321
+ });
1322
+
1323
+ // Listener para novos logs
1324
+ const logHandler = ({ serviceId: sid, log, type }) => {
1325
+ if (sid === serviceId) {
1326
+ res.write(`data: ${JSON.stringify({ timestamp: new Date().toISOString(), type, message: log })}\n\n`);
1327
+ }
1328
+ };
1329
+
1330
+ processManager.on("log", logHandler);
1331
+
1332
+ // Cleanup ao fechar conexão
1333
+ req.on("close", () => {
1334
+ processManager.off("log", logHandler);
1335
+ });
1336
+ });
1337
+
1338
+ // ============================================
1339
+ // ENDPOINTS LEGADOS (manter compatibilidade)
1340
+ // ============================================
1341
+
1342
+ app.get("/workspace/files", async (req, res) => {
1343
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1344
+ const { max = 5000 } = req.query;
1345
+ const tree = await listRecursive(WORKSPACE_ROOT, { maxFiles: Number(max) });
1346
+ res.json({ root: WORKSPACE_ROOT, count: tree.length, tree });
1347
+ });
1348
+
1349
+ app.get("/workspace/file", async (req, res) => {
1350
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1351
+ const rel = req.query.path;
1352
+ if (!rel) return res.status(400).json({ error: "path is required" });
1353
+ try {
1354
+ const content = await readText(WORKSPACE_ROOT, rel);
1355
+ res.json({ path: rel, content });
1356
+ } catch (e) {
1357
+ res.status(404).json({ error: "file not found", details: String(e) });
1358
+ }
1359
+ });
1360
+
1361
+ app.post("/workspace/write", async (req, res) => {
1362
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1363
+ const { path: rel, content } = req.body || {};
1364
+ if (!rel) return res.status(400).json({ error: "path is required" });
1365
+ try {
1366
+ await writeFile(path.join(WORKSPACE_ROOT, rel), content ?? "", "utf8");
1367
+ res.json({ ok: true, path: rel, bytes: Buffer.byteLength(content ?? "", "utf8") });
1368
+ } catch (e) {
1369
+ res.status(400).json({ error: "write failed", details: String(e) });
1370
+ }
1371
+ });
1372
+
1373
+ // ============================================
1374
+ // ✅ CORRECTED: /workspace/patch endpoint
1375
+ // ============================================
1376
+ app.post("/workspace/patch", async (req, res) => {
1377
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1378
+ const { diff, incidentId } = req.body || {};
1379
+ if (!diff) return res.status(400).json({ error: "diff is required" });
1380
+
1381
+ try {
1382
+ console.log(`📝 Applying patch for incident: ${incidentId || 'unknown'}`);
1383
+ const out = await applyUnifiedDiff(WORKSPACE_ROOT, diff);
1384
+
1385
+ // ✅ CRITICAL FIX: Format response as expected by Gateway
1386
+ const response = {
1387
+ ok: true,
1388
+ filesModified: 1,
1389
+ patchedFiles: [out.target],
1390
+ bytesWritten: out.bytes,
1391
+ target: out.target,
1392
+ bytes: out.bytes,
1393
+ message: `Patch applied successfully to ${out.target}`,
1394
+ incidentId: incidentId
1395
+ };
1396
+
1397
+ console.log(`✅ Patch applied successfully:`, {
1398
+ target: out.target,
1399
+ bytes: out.bytes,
1400
+ incident: incidentId
1401
+ });
1402
+
1403
+ res.json(response);
1404
+ } catch (e) {
1405
+ console.error(`❌ Patch failed:`, e.message);
1406
+ res.status(400).json({
1407
+ ok: false,
1408
+ error: "patch failed",
1409
+ details: String(e),
1410
+ filesModified: 0,
1411
+ patchedFiles: []
1412
+ });
1413
+ }
1414
+ });
1415
+
1416
+ app.post("/workspace/test", async (_req, res) => {
1417
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1418
+ const meta = await detectProject(WORKSPACE_ROOT);
1419
+ const result = await compileAndTest({ language: meta.language, buildTool: meta.buildTool, cwd: WORKSPACE_ROOT });
1420
+ res.json({ root: WORKSPACE_ROOT, meta, result });
1421
+ });
1422
+
1423
+ app.post("/workspace/run", async (req, res) => {
1424
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1425
+ const { cmd, args = [] } = req.body || {};
1426
+ if (!cmd) return res.status(400).json({ error: "cmd is required" });
1427
+ const out = await run(cmd, args, WORKSPACE_ROOT, 5 * 60 * 1000);
1428
+ res.json(out);
1429
+ });
1430
+
1431
+ // ============================================
1432
+ // 🆕 TEST LOCAL ENDPOINTS
1433
+ // ============================================
1434
+
1435
+ /** Store test local state */
1436
+ let TEST_LOCAL_STATE = {
1437
+ status: "idle", // idle, compiling, compiled, starting, running, stopped, error
1438
+ compilationResult: null,
1439
+ serverProcess: null,
1440
+ endpoints: [],
1441
+ config: null,
1442
+ testResults: [],
1443
+ serverLogs: [] // Buffer circular de logs do servidor (últimos 1000)
1444
+ };
1445
+
1446
+ // ============================================
1447
+ // 🆕 TEST LOCAL STATE ENDPOINTS (ADDED)
1448
+ // ============================================
1449
+
1450
+ /**
1451
+ * GET /workspace/test-local/state
1452
+ * Returns current test local state with auto-detected port
1453
+ */
1454
+ app.get("/workspace/test-local/state", async (req, res) => {
1455
+ console.log("📊 [TEST-LOCAL] Getting state:", TEST_LOCAL_STATE.status);
1456
+
1457
+ // Auto-detect port if not set in config
1458
+ let port = TEST_LOCAL_STATE.config?.server?.port;
1459
+ if (!port && WORKSPACE_ROOT) {
1460
+ try {
1461
+ port = await detectPort(WORKSPACE_ROOT);
1462
+ } catch (e) {
1463
+ port = 8080;
1464
+ }
1465
+ }
1466
+
1467
+ res.json({
1468
+ ok: true,
1469
+ status: TEST_LOCAL_STATE.status,
1470
+ compilationResult: TEST_LOCAL_STATE.compilationResult,
1471
+ endpoints: TEST_LOCAL_STATE.endpoints?.length || 0,
1472
+ config: {
1473
+ port: port || 8080,
1474
+ contextPath: TEST_LOCAL_STATE.config?.server?.contextPath || "",
1475
+ profiles: TEST_LOCAL_STATE.config?.profiles || []
1476
+ },
1477
+ testResultsCount: TEST_LOCAL_STATE.testResults?.length || 0,
1478
+ serverLogsCount: TEST_LOCAL_STATE.serverLogs?.length || 0,
1479
+ hasRunningServer: processManager.isRunning ? processManager.isRunning('test-local') : false,
1480
+ workspace: WORKSPACE_ROOT,
1481
+ timestamp: Date.now()
1482
+ });
1483
+ });
1484
+
1485
+ /**
1486
+ * POST /workspace/test-local/compile
1487
+ * Compiles the project without starting server
1488
+ */
1489
+ app.post("/workspace/test-local/compile", async (req, res) => {
1490
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1491
+
1492
+ try {
1493
+ console.log("🔨 [TEST-LOCAL] Starting compilation...");
1494
+ TEST_LOCAL_STATE.status = "compiling";
1495
+
1496
+ const meta = await detectProject(WORKSPACE_ROOT);
1497
+ const compileResult = await compileAndTest({
1498
+ language: meta.language,
1499
+ buildTool: meta.buildTool,
1500
+ cwd: WORKSPACE_ROOT,
1501
+ skipTests: true
1502
+ });
1503
+
1504
+ if (compileResult.code !== 0) {
1505
+ TEST_LOCAL_STATE.status = "error";
1506
+ TEST_LOCAL_STATE.compilationResult = {
1507
+ success: false,
1508
+ error: compileResult.stderr,
1509
+ duration: compileResult.duration
1510
+ };
1511
+
1512
+ return res.json({
1513
+ ok: false,
1514
+ error: compileResult.stderr,
1515
+ stdout: compileResult.stdout,
1516
+ duration: compileResult.duration
1517
+ });
1518
+ }
1519
+
1520
+ TEST_LOCAL_STATE.status = "compiled";
1521
+ TEST_LOCAL_STATE.compilationResult = {
1522
+ success: true,
1523
+ language: meta.language,
1524
+ buildTool: meta.buildTool,
1525
+ duration: compileResult.duration
1526
+ };
1527
+
1528
+ console.log("✅ [TEST-LOCAL] Compilation successful");
1529
+
1530
+ res.json({
1531
+ ok: true,
1532
+ language: meta.language,
1533
+ buildTool: meta.buildTool,
1534
+ duration: compileResult.duration,
1535
+ stdout: compileResult.stdout
1536
+ });
1537
+ } catch (err) {
1538
+ console.error("❌ [TEST-LOCAL] Compilation failed:", err.message);
1539
+ TEST_LOCAL_STATE.status = "error";
1540
+ res.status(500).json({ ok: false, error: err.message });
1541
+ }
1542
+ });
1543
+
1544
+ /**
1545
+ * POST /workspace/test-local/start
1546
+ * Starts the local server with AUTO-HEALING
1547
+ *
1548
+ * 🧠 UPDATED: Now uses AI Vibe Coding Engine for auto-healing
1549
+ */
1550
+ app.post("/workspace/test-local/start", async (req, res) => {
1551
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1552
+
1553
+ const { port } = req.body || {};
1554
+
1555
+ try {
1556
+ console.log(`\n🚀 [TEST-LOCAL] Starting server (SIMPLE MODE)...`);
1557
+ TEST_LOCAL_STATE.status = "starting";
1558
+
1559
+ const meta = await detectProject(WORKSPACE_ROOT);
1560
+
1561
+ // Detect port if not provided
1562
+ let serverPort = port || 8080;
1563
+
1564
+ // Para Java, encontrar o JAR e correr directamente
1565
+ if (meta.language === 'java') {
1566
+ const targetDir = path.join(WORKSPACE_ROOT, 'target');
1567
+
1568
+ if (!fs.existsSync(targetDir)) {
1569
+ throw new Error('target/ directory not found. Run mvn clean install first.');
1570
+ }
1571
+
1572
+ const files = fs.readdirSync(targetDir);
1573
+ const jarFiles = files.filter(f =>
1574
+ f.endsWith('.jar') &&
1575
+ !f.endsWith('.original') &&
1576
+ !f.includes('-sources') &&
1577
+ !f.includes('-javadoc')
1578
+ );
1579
+
1580
+ if (jarFiles.length === 0) {
1581
+ throw new Error('No JAR found in target/. Run mvn clean install first.');
1582
+ }
1583
+
1584
+ const jarPath = path.join(targetDir, jarFiles[0]);
1585
+
1586
+ // Comando EXACTO como no terminal (sem profiles!)
1587
+ const command = 'java';
1588
+ const args = ['-jar', jarPath, `--server.port=${serverPort}`];
1589
+
1590
+ console.log(`🚀 Command: ${command} ${args.join(' ')}`);
1591
+
1592
+ // Env LIMPO - remover TODAS as variáveis Spring que podem interferir
1593
+ const cleanEnv = { ...process.env };
1594
+ delete cleanEnv.SPRING_PROFILES_ACTIVE;
1595
+ delete cleanEnv.SPRING_DATASOURCE_URL;
1596
+ delete cleanEnv.SPRING_DATASOURCE_USERNAME;
1597
+ delete cleanEnv.SPRING_DATASOURCE_PASSWORD;
1598
+ // Remover qualquer variável que comece com SPRING_
1599
+ Object.keys(cleanEnv).forEach(key => {
1600
+ if (key.startsWith('SPRING_')) {
1601
+ delete cleanEnv[key];
1602
+ }
1603
+ });
1604
+ cleanEnv.SERVER_PORT = String(serverPort);
1605
+ cleanEnv.PORT = String(serverPort);
1606
+
1607
+ const startConfig = {
1608
+ command,
1609
+ args,
1610
+ cwd: WORKSPACE_ROOT,
1611
+ port: serverPort,
1612
+ env: cleanEnv
1613
+ };
1614
+
1615
+ await processManager.start('test-local', startConfig);
1616
+
1617
+ TEST_LOCAL_STATE.status = "running";
1618
+ TEST_LOCAL_STATE.config = { port: serverPort };
1619
+
1620
+ console.log(`✅ [TEST-LOCAL] Server started on port ${serverPort}`);
1621
+
1622
+ res.json({
1623
+ ok: true,
1624
+ port: serverPort,
1625
+ language: meta.language,
1626
+ command: `${command} ${args.join(' ')}`,
1627
+ status: "running"
1628
+ });
1629
+
1630
+ } else {
1631
+ // Outras linguagens - manter comportamento original
1632
+ let command, args;
1633
+
1634
+ if (meta.language === 'node') {
1635
+ command = meta.buildTool === 'yarn' ? 'yarn' : 'npm';
1636
+ args = ['start'];
1637
+ } else if (meta.language === 'python') {
1638
+ command = 'python';
1639
+ args = ['main.py'];
1640
+ } else if (meta.language === 'go') {
1641
+ command = 'go';
1642
+ args = ['run', '.'];
1643
+ } else if (meta.language === 'dotnet' || meta.language === '.net') {
1644
+ command = 'dotnet';
1645
+ args = ['run'];
1646
+ } else {
1647
+ throw new Error(`Unsupported language: ${meta.language}`);
1648
+ }
1649
+
1650
+ await processManager.start('test-local', {
1651
+ command,
1652
+ args,
1653
+ cwd: WORKSPACE_ROOT,
1654
+ port: serverPort,
1655
+ env: { ...process.env, PORT: String(serverPort) }
1656
+ });
1657
+
1658
+ TEST_LOCAL_STATE.status = "running";
1659
+ TEST_LOCAL_STATE.config = { port: serverPort };
1660
+
1661
+ res.json({
1662
+ ok: true,
1663
+ port: serverPort,
1664
+ language: meta.language,
1665
+ command: `${command} ${args.join(' ')}`,
1666
+ status: "running"
1667
+ });
1668
+ }
1669
+ } catch (err) {
1670
+ console.error("❌ [TEST-LOCAL] Server start failed:", err.message);
1671
+ TEST_LOCAL_STATE.status = "error";
1672
+ res.status(500).json({ ok: false, error: err.message });
1673
+ }
1674
+ });
1675
+
1676
+ /**
1677
+ * POST /workspace/test-local/stop
1678
+ * Stops the local server
1679
+ */
1680
+ app.post("/workspace/test-local/stop", async (req, res) => {
1681
+ try {
1682
+ console.log("🛑 [TEST-LOCAL] Stopping server...");
1683
+
1684
+ await processManager.stop('test-local');
1685
+ TEST_LOCAL_STATE.status = "stopped";
1686
+
1687
+ console.log("✅ [TEST-LOCAL] Server stopped");
1688
+
1689
+ res.json({
1690
+ ok: true,
1691
+ status: "stopped"
1692
+ });
1693
+ } catch (err) {
1694
+ console.error("❌ [TEST-LOCAL] Server stop failed:", err.message);
1695
+ res.status(500).json({ ok: false, error: err.message });
1696
+ }
1697
+ });
1698
+
1699
+ /**
1700
+ * GET /workspace/test-local/endpoints
1701
+ * Returns discovered endpoints
1702
+ */
1703
+ app.get("/workspace/test-local/endpoints", async (req, res) => {
1704
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1705
+
1706
+ try {
1707
+ console.log("📋 [TEST-LOCAL] Getting endpoints...");
1708
+
1709
+ // If endpoints are cached, return them
1710
+ if (TEST_LOCAL_STATE.endpoints && TEST_LOCAL_STATE.endpoints.length > 0) {
1711
+ return res.json({
1712
+ ok: true,
1713
+ endpoints: TEST_LOCAL_STATE.endpoints,
1714
+ cached: true
1715
+ });
1716
+ }
1717
+
1718
+ // Otherwise discover them
1719
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
1720
+ const structure = await scanner.scan();
1721
+
1722
+ const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
1723
+ const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
1724
+
1725
+ const dtoAnalyzer = new DTOAnalyzer(WORKSPACE_ROOT);
1726
+ const payloadDocs = await dtoAnalyzer.generatePayloadDocs(structure.files, apiDocs.endpoints);
1727
+
1728
+ TEST_LOCAL_STATE.endpoints = payloadDocs.endpoints;
1729
+
1730
+ console.log(`✅ [TEST-LOCAL] Discovered ${payloadDocs.endpoints.length} endpoints`);
1731
+
1732
+ res.json({
1733
+ ok: true,
1734
+ endpoints: payloadDocs.endpoints,
1735
+ cached: false
1736
+ });
1737
+ } catch (err) {
1738
+ console.error("❌ [TEST-LOCAL] Failed to get endpoints:", err.message);
1739
+ res.status(500).json({ ok: false, error: err.message });
1740
+ }
1741
+ });
1742
+
1743
+ /**
1744
+ * POST /workspace/test-local/execute
1745
+ * Executes a test request against the running server
1746
+ */
1747
+ app.post("/workspace/test-local/execute", async (req, res) => {
1748
+ const { method, path: reqPath, body, headers, port } = req.body || {};
1749
+
1750
+ if (!method || !reqPath) {
1751
+ return res.status(400).json({ error: "method and path are required" });
1752
+ }
1753
+
1754
+ try {
1755
+ const serverPort = port || TEST_LOCAL_STATE.config?.server?.port || 8080;
1756
+ const url = `http://localhost:${serverPort}${reqPath}`;
1757
+
1758
+ console.log(`🧪 [TEST-LOCAL] Executing: ${method} ${url}`);
1759
+
1760
+ const startTime = Date.now();
1761
+
1762
+ const fetchOptions = {
1763
+ method: method.toUpperCase(),
1764
+ headers: {
1765
+ 'Content-Type': 'application/json',
1766
+ ...headers
1767
+ }
1768
+ };
1769
+
1770
+ if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
1771
+ fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
1772
+ }
1773
+
1774
+ const response = await fetch(url, fetchOptions);
1775
+ const duration = Date.now() - startTime;
1776
+
1777
+ let responseBody;
1778
+ const contentType = response.headers.get('content-type');
1779
+
1780
+ if (contentType?.includes('application/json')) {
1781
+ responseBody = await response.json();
1782
+ } else {
1783
+ responseBody = await response.text();
1784
+ }
1785
+
1786
+ const result = {
1787
+ statusCode: response.status,
1788
+ statusText: response.statusText,
1789
+ headers: Object.fromEntries(response.headers.entries()),
1790
+ body: responseBody,
1791
+ duration,
1792
+ url
1793
+ };
1794
+
1795
+ // Store result
1796
+ TEST_LOCAL_STATE.testResults.push({
1797
+ timestamp: Date.now(),
1798
+ request: { method, path: reqPath, body },
1799
+ result
1800
+ });
1801
+
1802
+ // Keep only last 100 results
1803
+ if (TEST_LOCAL_STATE.testResults.length > 100) {
1804
+ TEST_LOCAL_STATE.testResults.shift();
1805
+ }
1806
+
1807
+ console.log(`✅ [TEST-LOCAL] Test result: ${response.status} (${duration}ms)`);
1808
+
1809
+ res.json({
1810
+ ok: true,
1811
+ result
1812
+ });
1813
+ } catch (err) {
1814
+ console.error("❌ [TEST-LOCAL] Test execution failed:", err.message);
1815
+ res.json({
1816
+ ok: false,
1817
+ error: err.message,
1818
+ result: {
1819
+ statusCode: 0,
1820
+ statusText: 'Connection Failed',
1821
+ body: err.message,
1822
+ duration: 0
1823
+ }
1824
+ });
1825
+ }
1826
+ });
1827
+
1828
+ /**
1829
+ * GET /workspace/test-local/results
1830
+ * Returns stored test results
1831
+ */
1832
+ app.get("/workspace/test-local/results", (req, res) => {
1833
+ console.log("📊 [TEST-LOCAL] Getting test results...");
1834
+
1835
+ res.json({
1836
+ ok: true,
1837
+ results: TEST_LOCAL_STATE.testResults,
1838
+ total: TEST_LOCAL_STATE.testResults.length
1839
+ });
1840
+ });
1841
+
1842
+ /**
1843
+ * POST /workspace/test-local/clear-results
1844
+ * Clears stored test results
1845
+ */
1846
+ app.post("/workspace/test-local/clear-results", (req, res) => {
1847
+ console.log("🗑️ [TEST-LOCAL] Clearing test results...");
1848
+
1849
+ TEST_LOCAL_STATE.testResults = [];
1850
+
1851
+ res.json({
1852
+ ok: true,
1853
+ message: "Test results cleared"
1854
+ });
1855
+ });
1856
+
1857
+ /**
1858
+ * GET /workspace/test-local/logs
1859
+ * Returns server logs (non-streaming)
1860
+ */
1861
+ app.get("/workspace/test-local/logs", (req, res) => {
1862
+ const limit = parseInt(req.query.limit) || 500;
1863
+
1864
+ console.log(`📋 [TEST-LOCAL] Getting logs (limit: ${limit})`);
1865
+
1866
+ const logs = TEST_LOCAL_STATE.serverLogs.slice(-limit);
1867
+
1868
+ res.json({
1869
+ ok: true,
1870
+ logs,
1871
+ total: TEST_LOCAL_STATE.serverLogs.length,
1872
+ returned: logs.length
1873
+ });
1874
+ });
1875
+
1876
+
1877
+ /** Discover controllers and endpoints */
1878
+ app.get("/workspace/controllers", async (_req, res) => {
1879
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1880
+
1881
+ try {
1882
+ console.log("🔍 [CONTROLLERS] Discovering controllers...");
1883
+
1884
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
1885
+ const structure = await scanner.scan();
1886
+
1887
+ const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
1888
+ const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
1889
+
1890
+ console.log(`✅ [CONTROLLERS] Found ${apiDocs.totalEndpoints} endpoints in ${apiDocs.totalControllers} controllers`);
1891
+
1892
+ res.json({
1893
+ ok: true,
1894
+ ...apiDocs
1895
+ });
1896
+ } catch (err) {
1897
+ console.error("❌ [CONTROLLERS] Failed:", err.message);
1898
+ res.status(500).json({ ok: false, error: err.message });
1899
+ }
1900
+ });
1901
+
1902
+ /** Analyze DTOs and payloads */
1903
+ app.get("/workspace/dtos", async (_req, res) => {
1904
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1905
+
1906
+ try {
1907
+ console.log("📦 [DTOS] Analyzing DTOs...");
1908
+
1909
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
1910
+ const structure = await scanner.scan();
1911
+
1912
+ const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
1913
+ const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
1914
+
1915
+ const dtoAnalyzer = new DTOAnalyzer(WORKSPACE_ROOT);
1916
+ const payloadDocs = await dtoAnalyzer.generatePayloadDocs(structure.files, apiDocs.endpoints);
1917
+
1918
+ console.log(`✅ [DTOS] Found ${payloadDocs.totalDtos} DTOs`);
1919
+
1920
+ res.json({
1921
+ ok: true,
1922
+ ...payloadDocs
1923
+ });
1924
+ } catch (err) {
1925
+ console.error("❌ [DTOS] Failed:", err.message);
1926
+ res.status(500).json({ ok: false, error: err.message });
1927
+ }
1928
+ });
1929
+
1930
+ /** Get server configuration */
1931
+ app.get("/workspace/config", async (_req, res) => {
1932
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1933
+
1934
+ try {
1935
+ console.log("⚙️ [CONFIG] Analyzing configuration...");
1936
+
1937
+ const configAnalyzer = new ConfigAnalyzer(WORKSPACE_ROOT);
1938
+ const config = await configAnalyzer.analyze();
1939
+
1940
+ console.log(`✅ [CONFIG] Server port: ${config.server.port}`);
1941
+
1942
+ res.json({
1943
+ ok: true,
1944
+ ...config
1945
+ });
1946
+ } catch (err) {
1947
+ console.error("❌ [CONFIG] Failed:", err.message);
1948
+ res.status(500).json({ ok: false, error: err.message });
1949
+ }
1950
+ });
1951
+
1952
+ // ============================================
1953
+ // 🆕 BACKUP & ROLLBACK ENDPOINTS (Sprint 1.3)
1954
+ // Added without modifying existing endpoints
1955
+ // ============================================
1956
+
1957
+ /**
1958
+ * Helper: Validate diff format
1959
+ */
1960
+ function validateDiff(diff) {
1961
+ const errors = [];
1962
+
1963
+ if (!diff || typeof diff !== 'string') {
1964
+ return { valid: false, errors: ['Diff text is empty or invalid'] };
1965
+ }
1966
+
1967
+ const lines = diff.split('\n');
1968
+ const hasOldFileHeader = lines.some(l => l.startsWith('--- '));
1969
+ const hasNewFileHeader = lines.some(l => l.startsWith('+++ '));
1970
+ const hasHunkHeader = lines.some(l => /^@@ -\d+,?\d* \+\d+,?\d* @@/.test(l));
1971
+
1972
+ if (!hasOldFileHeader) errors.push('Missing old file header (--- a/path/to/file)');
1973
+ if (!hasNewFileHeader) errors.push('Missing new file header (+++ b/path/to/file)');
1974
+ if (!hasHunkHeader) errors.push('Missing hunk header (@@ -line,count +line,count @@)');
1975
+
1976
+ return { valid: errors.length === 0, errors };
1977
+ }
1978
+
1979
+ /**
1980
+ * Helper: Extract target files from diff
1981
+ */
1982
+ function extractTargetFiles(diff) {
1983
+ const files = [];
1984
+ const lines = diff.split('\n');
1985
+
1986
+ for (const line of lines) {
1987
+ if (line.startsWith('+++ ')) {
1988
+ let filePath = line.substring(4).trim();
1989
+ // Remove prefixes like b/ or ./
1990
+ filePath = filePath.replace(/^[ab]\//, '').replace(/^\.\//,'');
1991
+ if (filePath && filePath !== '/dev/null') {
1992
+ files.push(filePath);
1993
+ }
1994
+ }
1995
+ }
1996
+
1997
+ return files;
1998
+ }
1999
+
2000
+ /**
2001
+ * POST /workspace/safe-patch
2002
+ * Applies patch with automatic backup and rollback on failure
2003
+ */
2004
+ app.post("/workspace/safe-patch", async (req, res) => {
2005
+ if (!WORKSPACE_ROOT) {
2006
+ return res.status(400).json({
2007
+ error: "workspace not set",
2008
+ hint: "call POST /workspace/open first"
2009
+ });
2010
+ }
2011
+
2012
+ const { diff, incidentId } = req.body || {};
2013
+ if (!diff) return res.status(400).json({ error: "diff is required" });
2014
+
2015
+ console.log(`🔧 Safe patch requested for incident: ${incidentId || 'unknown'}`);
2016
+
2017
+ try {
2018
+ // 1. Validate diff
2019
+ const validation = validateDiff(diff);
2020
+ if (!validation.valid) {
2021
+ return res.status(400).json({
2022
+ ok: false,
2023
+ error: "Invalid diff format",
2024
+ details: validation.errors
2025
+ });
2026
+ }
2027
+
2028
+ // 2. Extract target files
2029
+ const targetFiles = extractTargetFiles(diff);
2030
+ console.log(`📂 Target files: ${targetFiles.join(', ')}`);
2031
+
2032
+ // 3. Create backup
2033
+ const backupId = `backup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2034
+ const backupFiles = [];
2035
+
2036
+ for (const relPath of targetFiles) {
2037
+ const fullPath = path.join(WORKSPACE_ROOT, relPath);
2038
+ if (await exists(fullPath)) {
2039
+ const content = await readFile(fullPath, 'utf8');
2040
+ backupFiles.push({ path: relPath, content });
2041
+ }
2042
+ }
2043
+
2044
+ BACKUPS.set(backupId, {
2045
+ files: backupFiles,
2046
+ timestamp: new Date().toISOString(),
2047
+ incidentId: incidentId || null
2048
+ });
2049
+
2050
+ // Save backup files to disk for persistence across restarts
2051
+ const backupDir = path.join(os.tmpdir(), 'deepdebug-backups', backupId);
2052
+ try {
2053
+ await fsPromises.mkdir(backupDir, { recursive: true });
2054
+ for (const file of backupFiles) {
2055
+ const backupFilePath = path.join(backupDir, file.path.replace(/\//g, '__'));
2056
+ await fsPromises.writeFile(backupFilePath, file.content, 'utf8');
2057
+ file.backupPath = backupFilePath;
2058
+ }
2059
+ saveBackupIndex();
2060
+ } catch (e) {
2061
+ console.warn(`⚠️ Could not persist backup to disk: ${e.message}`);
2062
+ }
2063
+
2064
+ // Cleanup old backups if exceeded max
2065
+ if (BACKUPS.size > MAX_BACKUPS) {
2066
+ const oldest = Array.from(BACKUPS.keys())[0];
2067
+ BACKUPS.delete(oldest);
2068
+ console.log(`🗑️ Removed old backup: ${oldest}`);
2069
+ saveBackupIndex();
2070
+ }
2071
+
2072
+ console.log(`💾 Backup created: ${backupId} (${backupFiles.length} files)`);
2073
+
2074
+ // 4. Apply patch
2075
+ try {
2076
+ const result = await applyUnifiedDiff(WORKSPACE_ROOT, diff);
2077
+ console.log(`✅ Patch applied successfully: ${result.target}`);
2078
+
2079
+ res.json({
2080
+ ok: true,
2081
+ backupId,
2082
+ patchedFiles: [result.target],
2083
+ bytesWritten: result.bytes,
2084
+ message: "Patch applied successfully"
2085
+ });
2086
+ } catch (patchError) {
2087
+ // 5. Rollback on failure
2088
+ console.error(`❌ Patch failed, rolling back: ${patchError.message}`);
2089
+
2090
+ for (const file of backupFiles) {
2091
+ const fullPath = path.join(WORKSPACE_ROOT, file.path);
2092
+ await writeFile(fullPath, file.content, 'utf8');
2093
+ }
2094
+
2095
+ BACKUPS.delete(backupId);
2096
+
2097
+ res.status(400).json({
2098
+ ok: false,
2099
+ error: "Patch failed and was rolled back",
2100
+ details: patchError.message,
2101
+ rolledBack: true
2102
+ });
2103
+ }
2104
+ } catch (err) {
2105
+ console.error(`❌ Safe patch error: ${err.message}`);
2106
+ res.status(500).json({ ok: false, error: err.message });
2107
+ }
2108
+ });
2109
+
2110
+ /**
2111
+ * POST /workspace/patch/dry-run
2112
+ * Validates and simulates patch application WITHOUT modifying files
2113
+ */
2114
+ app.post("/workspace/patch/dry-run", async (req, res) => {
2115
+ if (!WORKSPACE_ROOT) {
2116
+ return res.status(400).json({
2117
+ error: "workspace not set",
2118
+ hint: "call POST /workspace/open first"
2119
+ });
2120
+ }
2121
+
2122
+ const { diff } = req.body || {};
2123
+ if (!diff) return res.status(400).json({ error: "diff is required" });
2124
+
2125
+ console.log(`🔍 Dry-run patch validation requested`);
2126
+
2127
+ try {
2128
+ // 1. Validate diff format
2129
+ const validation = validateDiff(diff);
2130
+ if (!validation.valid) {
2131
+ return res.json({
2132
+ ok: true,
2133
+ valid: false,
2134
+ canApply: false,
2135
+ errors: validation.errors,
2136
+ wouldModify: [],
2137
+ hunksCount: 0
2138
+ });
2139
+ }
2140
+
2141
+ // 2. Extract target files
2142
+ const targetFiles = extractTargetFiles(diff);
2143
+
2144
+ // 3. Check if files exist
2145
+ const fileChecks = await Promise.all(
2146
+ targetFiles.map(async (relPath) => {
2147
+ const fullPath = path.join(WORKSPACE_ROOT, relPath);
2148
+ const fileExists = await exists(fullPath);
2149
+ let currentContent = null;
2150
+ let lineCount = 0;
2151
+
2152
+ if (fileExists) {
2153
+ try {
2154
+ currentContent = await readFile(fullPath, 'utf8');
2155
+ lineCount = currentContent.split('\n').length;
2156
+ } catch (e) {
2157
+ // File exists but can't be read
2158
+ }
2159
+ }
2160
+
2161
+ return {
2162
+ path: relPath,
2163
+ exists: fileExists,
2164
+ lineCount
2165
+ };
2166
+ })
2167
+ );
2168
+
2169
+ // 4. Count hunks
2170
+ const hunkCount = (diff.match(/@@ -\d+,?\d* \+\d+,?\d* @@/g) || []).length;
2171
+
2172
+ // 5. Check if all files exist (for modification patches)
2173
+ const allFilesExist = fileChecks.every(f => f.exists || diff.includes('--- /dev/null'));
2174
+ const missingFiles = fileChecks.filter(f => !f.exists && !diff.includes('--- /dev/null'));
2175
+
2176
+ console.log(`✅ Dry-run complete: ${targetFiles.length} files, ${hunkCount} hunks`);
2177
+
2178
+ res.json({
2179
+ ok: true,
2180
+ valid: true,
2181
+ canApply: allFilesExist,
2182
+ wouldModify: targetFiles,
2183
+ hunksCount: hunkCount,
2184
+ fileChecks,
2185
+ missingFiles: missingFiles.map(f => f.path),
2186
+ warnings: missingFiles.length > 0 ? [`${missingFiles.length} file(s) not found`] : []
2187
+ });
2188
+ } catch (err) {
2189
+ console.error(`❌ Dry-run error: ${err.message}`);
2190
+ res.status(500).json({ ok: false, error: err.message });
2191
+ }
2192
+ });
2193
+
2194
+ /**
2195
+ * POST /workspace/validate-diff
2196
+ * Validates diff format without checking files
2197
+ */
2198
+ app.post("/workspace/validate-diff", async (req, res) => {
2199
+ const { diff } = req.body || {};
2200
+ if (!diff) return res.status(400).json({ valid: false, errors: ["diff is required"] });
2201
+
2202
+ const validation = validateDiff(diff);
2203
+ const targetFiles = validation.valid ? extractTargetFiles(diff) : [];
2204
+ const hunkCount = validation.valid ? (diff.match(/@@ -\d+,?\d* \+\d+,?\d* @@/g) || []).length : 0;
2205
+
2206
+ res.json({
2207
+ ...validation,
2208
+ targetFiles,
2209
+ hunksCount: hunkCount
2210
+ });
2211
+ });
2212
+
2213
+ /**
2214
+ * POST /workspace/backup
2215
+ * Creates manual backup of specific files
2216
+ */
2217
+ app.post("/workspace/backup", async (req, res) => {
2218
+ if (!WORKSPACE_ROOT) {
2219
+ return res.status(400).json({
2220
+ error: "workspace not set",
2221
+ hint: "call POST /workspace/open first"
2222
+ });
2223
+ }
2224
+
2225
+ const { files, incidentId } = req.body || {};
2226
+ if (!files || !Array.isArray(files)) {
2227
+ return res.status(400).json({ error: "files array is required" });
2228
+ }
2229
+
2230
+ try {
2231
+ const backupId = `backup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2232
+ const backupFiles = [];
2233
+
2234
+ for (const relPath of files) {
2235
+ const fullPath = path.join(WORKSPACE_ROOT, relPath);
2236
+ if (await exists(fullPath)) {
2237
+ const content = await readFile(fullPath, 'utf8');
2238
+ backupFiles.push({ path: relPath, content });
2239
+ }
2240
+ }
2241
+
2242
+ if (backupFiles.length === 0) {
2243
+ return res.status(400).json({
2244
+ ok: false,
2245
+ error: "No valid files to backup",
2246
+ requestedFiles: files
2247
+ });
2248
+ }
2249
+
2250
+ BACKUPS.set(backupId, {
2251
+ files: backupFiles,
2252
+ timestamp: new Date().toISOString(),
2253
+ incidentId: incidentId || null
2254
+ });
2255
+
2256
+ console.log(`💾 Manual backup created: ${backupId}`);
2257
+
2258
+ res.json({
2259
+ ok: true,
2260
+ backupId,
2261
+ files: backupFiles.map(f => f.path),
2262
+ timestamp: new Date().toISOString()
2263
+ });
2264
+ } catch (err) {
2265
+ console.error(`❌ Backup error: ${err.message}`);
2266
+ res.status(500).json({ ok: false, error: err.message });
2267
+ }
2268
+ });
2269
+
2270
+ /**
2271
+ * POST /workspace/rollback
2272
+ * Restores files from a backup
2273
+ */
2274
+ app.post("/workspace/rollback", async (req, res) => {
2275
+ if (!WORKSPACE_ROOT) {
2276
+ return res.status(400).json({
2277
+ error: "workspace not set",
2278
+ hint: "call POST /workspace/open first"
2279
+ });
2280
+ }
2281
+
2282
+ const { backupId } = req.body || {};
2283
+ if (!backupId) {
2284
+ return res.status(400).json({ error: "backupId is required" });
2285
+ }
2286
+
2287
+ const backup = BACKUPS.get(backupId);
2288
+ if (!backup) {
2289
+ return res.status(404).json({
2290
+ error: "backup not found",
2291
+ availableBackups: Array.from(BACKUPS.keys())
2292
+ });
2293
+ }
2294
+
2295
+ try {
2296
+ console.log(`♻️ Rolling back to backup: ${backupId}`);
2297
+
2298
+ for (const file of backup.files) {
2299
+ const fullPath = path.join(WORKSPACE_ROOT, file.path);
2300
+ await writeFile(fullPath, file.content, 'utf8');
2301
+ }
2302
+
2303
+ console.log(`✅ Rollback completed: ${backup.files.length} files restored`);
2304
+
2305
+ res.json({
2306
+ ok: true,
2307
+ backupId,
2308
+ restoredFiles: backup.files.map(f => f.path),
2309
+ timestamp: backup.timestamp
2310
+ });
2311
+ } catch (err) {
2312
+ console.error(`❌ Rollback error: ${err.message}`);
2313
+ res.status(500).json({ ok: false, error: err.message });
2314
+ }
2315
+ });
2316
+
2317
+ /**
2318
+ * GET /workspace/backups
2319
+ * Lists all available backups
2320
+ */
2321
+ app.get("/workspace/backups", (_req, res) => {
2322
+ const backupList = Array.from(BACKUPS.entries()).map(([id, data]) => ({
2323
+ backupId: id,
2324
+ timestamp: data.timestamp,
2325
+ incidentId: data.incidentId,
2326
+ fileCount: data.files.length,
2327
+ files: data.files.map(f => f.path)
2328
+ }));
2329
+
2330
+ // Sort by timestamp (newest first)
2331
+ backupList.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
2332
+
2333
+ res.json({
2334
+ backups: backupList,
2335
+ total: backupList.length,
2336
+ maxBackups: MAX_BACKUPS
2337
+ });
2338
+ });
2339
+
2340
+ /**
2341
+ * DELETE /workspace/backups/:backupId
2342
+ * Deletes a specific backup
2343
+ */
2344
+ app.delete("/workspace/backups/:backupId", (req, res) => {
2345
+ const { backupId } = req.params;
2346
+
2347
+ if (!BACKUPS.has(backupId)) {
2348
+ return res.status(404).json({ error: "backup not found" });
2349
+ }
2350
+
2351
+ BACKUPS.delete(backupId);
2352
+ console.log(`🗑️ Backup deleted: ${backupId}`);
2353
+
2354
+ res.json({
2355
+ ok: true,
2356
+ deleted: backupId
2357
+ });
2358
+ });
2359
+
2360
+ // ============================================
2361
+ // 📊 DIFF VIEWER ENDPOINT
2362
+ // Returns before/after content for files modified by a patch
2363
+ // Used by the frontend diff viewer
2364
+ // ============================================
2365
+
2366
+ /**
2367
+ * GET /workspace/diff/by-incident/:incidentId
2368
+ *
2369
+ * Find the most recent backup for an incident and return its diff.
2370
+ * This is used when the frontend doesn't have the exact backupId.
2371
+ */
2372
+ app.get("/workspace/diff/by-incident/:incidentId", async (req, res) => {
2373
+ const { incidentId } = req.params;
2374
+
2375
+ if (!WORKSPACE_ROOT) {
2376
+ return res.status(400).json({ error: "workspace not set" });
2377
+ }
2378
+
2379
+ // Find the most recent backup for this incident
2380
+ let matchedBackupId = null;
2381
+ let matchedBackup = null;
2382
+ let latestTimestamp = 0;
2383
+
2384
+ for (const [id, backup] of BACKUPS.entries()) {
2385
+ if (backup.incidentId === incidentId ||
2386
+ backup.incidentId === `autofix-${incidentId}` ||
2387
+ id.includes(incidentId)) {
2388
+ const ts = backup.timestamp ? new Date(backup.timestamp).getTime() : 0;
2389
+ if (ts > latestTimestamp || !matchedBackupId) {
2390
+ matchedBackupId = id;
2391
+ matchedBackup = backup;
2392
+ latestTimestamp = ts;
2393
+ }
2394
+ }
2395
+ }
2396
+
2397
+ // Also search by partial match on the last few chars
2398
+ if (!matchedBackup) {
2399
+ const shortId = incidentId.length > 8 ? incidentId.substring(incidentId.length - 8) : incidentId;
2400
+ for (const [id, backup] of BACKUPS.entries()) {
2401
+ if (id.includes(shortId) || (backup.incidentId && backup.incidentId.includes(shortId))) {
2402
+ matchedBackupId = id;
2403
+ matchedBackup = backup;
2404
+ break;
2405
+ }
2406
+ }
2407
+ }
2408
+
2409
+ if (!matchedBackup) {
2410
+ console.log(`⚠️ No backup found for incident ${incidentId}. Available backups:`, Array.from(BACKUPS.keys()));
2411
+ return res.status(404).json({
2412
+ ok: false,
2413
+ error: "No backup found for this incident",
2414
+ incidentId,
2415
+ availableBackups: Array.from(BACKUPS.keys())
2416
+ });
2417
+ }
2418
+
2419
+ console.log(`📊 Found backup ${matchedBackupId} for incident ${incidentId}`);
2420
+
2421
+ // Reuse the diff logic
2422
+ try {
2423
+ const diffs = [];
2424
+ for (const file of matchedBackup.files) {
2425
+ const fullPath = path.join(WORKSPACE_ROOT, file.path);
2426
+ let currentContent = '';
2427
+ try {
2428
+ currentContent = await readFile(fullPath, 'utf8');
2429
+ } catch (err) {
2430
+ currentContent = '// File was deleted or moved after patching';
2431
+ }
2432
+ const hasChanges = file.content !== currentContent;
2433
+ if (hasChanges) {
2434
+ diffs.push({
2435
+ filePath: file.path,
2436
+ fileName: path.basename(file.path),
2437
+ before: file.content,
2438
+ after: currentContent,
2439
+ hasChanges: true,
2440
+ beforeLines: file.content.split('\n').length,
2441
+ afterLines: currentContent.split('\n').length
2442
+ });
2443
+ }
2444
+ }
2445
+
2446
+ res.json({
2447
+ ok: true,
2448
+ backupId: matchedBackupId,
2449
+ incidentId,
2450
+ timestamp: matchedBackup.timestamp,
2451
+ totalFiles: matchedBackup.files.length,
2452
+ changedFiles: diffs.length,
2453
+ files: diffs
2454
+ });
2455
+ } catch (err) {
2456
+ console.error(`❌ Diff error: ${err.message}`);
2457
+ res.status(500).json({ ok: false, error: err.message });
2458
+ }
2459
+ });
2460
+
2461
+ /**
2462
+ * GET /workspace/diff/:backupId
2463
+ *
2464
+ * Returns before/after content for files modified by a patch.
2465
+ * The BACKUPS map stores original file content (before patch).
2466
+ * Current file content is read from disk (after patch).
2467
+ *
2468
+ * Response:
2469
+ * {
2470
+ * "ok": true,
2471
+ * "backupId": "backup-xxx",
2472
+ * "files": [
2473
+ * {
2474
+ * "filePath": "src/main/java/.../Service.java",
2475
+ * "fileName": "Service.java",
2476
+ * "before": "original content...",
2477
+ * "after": "current content...",
2478
+ * "hasChanges": true
2479
+ * }
2480
+ * ]
2481
+ * }
2482
+ */
2483
+ app.get("/workspace/diff/:backupId", async (req, res) => {
2484
+ const { backupId } = req.params;
2485
+
2486
+ if (!WORKSPACE_ROOT) {
2487
+ return res.status(400).json({ error: "workspace not set" });
2488
+ }
2489
+
2490
+ const backup = BACKUPS.get(backupId);
2491
+ if (!backup) {
2492
+ return res.status(404).json({
2493
+ ok: false,
2494
+ error: "Backup not found. It may have expired or the server was restarted.",
2495
+ availableBackups: Array.from(BACKUPS.keys())
2496
+ });
2497
+ }
2498
+
2499
+ try {
2500
+ const diffs = [];
2501
+
2502
+ for (const file of backup.files) {
2503
+ const fullPath = path.join(WORKSPACE_ROOT, file.path);
2504
+ let currentContent = '';
2505
+
2506
+ try {
2507
+ currentContent = await readFile(fullPath, 'utf8');
2508
+ } catch (err) {
2509
+ currentContent = '// File was deleted or moved after patching';
2510
+ }
2511
+
2512
+ // Only include files that actually changed
2513
+ const hasChanges = file.content !== currentContent;
2514
+ if (hasChanges) {
2515
+ diffs.push({
2516
+ filePath: file.path,
2517
+ fileName: path.basename(file.path),
2518
+ before: file.content,
2519
+ after: currentContent,
2520
+ hasChanges: true,
2521
+ beforeLines: file.content.split('\n').length,
2522
+ afterLines: currentContent.split('\n').length
2523
+ });
2524
+ }
2525
+ }
2526
+
2527
+ console.log(`📊 Diff for ${backupId}: ${diffs.length} file(s) changed`);
2528
+
2529
+ res.json({
2530
+ ok: true,
2531
+ backupId,
2532
+ timestamp: backup.timestamp,
2533
+ incidentId: backup.incidentId,
2534
+ totalFiles: backup.files.length,
2535
+ changedFiles: diffs.length,
2536
+ files: diffs
2537
+ });
2538
+ } catch (err) {
2539
+ console.error(`❌ Diff error: ${err.message}`);
2540
+ res.status(500).json({ ok: false, error: err.message });
2541
+ }
2542
+ });
2543
+
2544
+ // ============================================
2545
+ // 🆕 DETECT PORT ENDPOINT (Sprint 1.2)
2546
+ // Multi-language port detection
2547
+ // ============================================
2548
+
2549
+ /**
2550
+ * GET /workspace/detect-port
2551
+ * Detects port from service configuration files
2552
+ * Supports: Java (Spring Boot), Node.js, Python, .NET
2553
+ *
2554
+ * Query params:
2555
+ * - servicePath: relative path to service (optional, defaults to workspace root)
2556
+ * - language: java, node, python, dotnet (optional, auto-detect if not provided)
2557
+ * - framework: spring-boot, express, flask, etc (optional)
2558
+ */
2559
+ app.get("/workspace/detect-port", async (req, res) => {
2560
+ if (!WORKSPACE_ROOT) {
2561
+ return res.status(400).json({
2562
+ error: "workspace not set",
2563
+ hint: "call POST /workspace/open first"
2564
+ });
2565
+ }
2566
+
2567
+ const { servicePath = '', language, framework } = req.query;
2568
+
2569
+ try {
2570
+ const fullPath = path.join(WORKSPACE_ROOT, servicePath);
2571
+ console.log(`🔍 Detecting port for service at: ${fullPath}`);
2572
+
2573
+ let port = null;
2574
+ let detectionMethod = null;
2575
+
2576
+ // Auto-detect language if not provided
2577
+ let detectedLanguage = language;
2578
+ if (!detectedLanguage) {
2579
+ const meta = await detectProject(fullPath);
2580
+ detectedLanguage = meta.language;
2581
+ }
2582
+
2583
+ // Detect port based on language/framework
2584
+ if (detectedLanguage === 'java') {
2585
+ const result = await detectSpringBootPort(fullPath);
2586
+ port = result.port;
2587
+ detectionMethod = result.method;
2588
+ } else if (detectedLanguage === 'node' || detectedLanguage === 'javascript' || detectedLanguage === 'typescript') {
2589
+ const result = await detectNodePort(fullPath);
2590
+ port = result.port;
2591
+ detectionMethod = result.method;
2592
+ } else if (detectedLanguage === 'python') {
2593
+ const result = await detectPythonPort(fullPath);
2594
+ port = result.port;
2595
+ detectionMethod = result.method;
2596
+ } else if (detectedLanguage === 'dotnet' || detectedLanguage === 'csharp') {
2597
+ const result = await detectDotNetPort(fullPath);
2598
+ port = result.port;
2599
+ detectionMethod = result.method;
2600
+ } else {
2601
+ // Try global detection
2602
+ port = await detectPort(fullPath);
2603
+ detectionMethod = 'global-detection';
2604
+ }
2605
+
2606
+ console.log(`✅ Detected port: ${port || 'default'} via ${detectionMethod}`);
2607
+
2608
+ res.json({
2609
+ ok: true,
2610
+ port,
2611
+ language: detectedLanguage,
2612
+ framework: framework || null,
2613
+ detectionMethod,
2614
+ servicePath: servicePath || '/'
2615
+ });
2616
+ } catch (err) {
2617
+ console.error('❌ Error detecting port:', err.message);
2618
+ res.status(500).json({ ok: false, error: err.message });
2619
+ }
2620
+ });
2621
+
2622
+ /**
2623
+ * Detects Spring Boot port from configuration files
2624
+ */
2625
+ async function detectSpringBootPort(servicePath) {
2626
+ const candidates = [
2627
+ path.join(servicePath, 'src/main/resources/application.yml'),
2628
+ path.join(servicePath, 'src/main/resources/application.yaml'),
2629
+ path.join(servicePath, 'src/main/resources/application.properties'),
2630
+ path.join(servicePath, 'src/main/resources/application-local.yml'),
2631
+ path.join(servicePath, 'src/main/resources/application-local.yaml'),
2632
+ path.join(servicePath, 'src/main/resources/application-local.properties'),
2633
+ path.join(servicePath, 'application.yml'),
2634
+ path.join(servicePath, 'application.yaml'),
2635
+ path.join(servicePath, 'application.properties')
2636
+ ];
2637
+
2638
+ for (const filePath of candidates) {
2639
+ if (await exists(filePath)) {
2640
+ try {
2641
+ const content = await readFile(filePath, 'utf8');
2642
+
2643
+ // YAML format
2644
+ if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
2645
+ // Match patterns like: server:\n port: 8090 or server.port: 8090
2646
+ const simpleMatch = content.match(/server:\s*\n\s+port:\s*(\d+)/);
2647
+ if (simpleMatch) {
2648
+ return { port: parseInt(simpleMatch[1]), method: `yaml:${path.basename(filePath)}` };
2649
+ }
2650
+
2651
+ // Also check for ${PORT:8080} or ${SERVER_PORT:8080} patterns
2652
+ const envMatch = content.match(/port:\s*\$\{[A-Z_]+:(\d+)\}/);
2653
+ if (envMatch) {
2654
+ return { port: parseInt(envMatch[1]), method: `yaml-env-default:${path.basename(filePath)}` };
2655
+ }
2656
+ }
2657
+
2658
+ // Properties format
2659
+ if (filePath.endsWith('.properties')) {
2660
+ const match = content.match(/server\.port\s*=\s*(\d+)/);
2661
+ if (match) {
2662
+ return { port: parseInt(match[1]), method: `properties:${path.basename(filePath)}` };
2663
+ }
2664
+
2665
+ // Check for ${PORT:8080} pattern
2666
+ const envMatch = content.match(/server\.port\s*=\s*\$\{[A-Z_]+:(\d+)\}/);
2667
+ if (envMatch) {
2668
+ return { port: parseInt(envMatch[1]), method: `properties-env-default:${path.basename(filePath)}` };
2669
+ }
2670
+ }
2671
+ } catch (e) {
2672
+ continue;
2673
+ }
2674
+ }
2675
+ }
2676
+
2677
+ return { port: 8080, method: 'spring-boot-default' };
2678
+ }
2679
+
2680
+ /**
2681
+ * Detects Node.js port from configuration files
2682
+ */
2683
+ async function detectNodePort(servicePath) {
2684
+ // Try .env file
2685
+ const envPath = path.join(servicePath, '.env');
2686
+ if (await exists(envPath)) {
2687
+ try {
2688
+ const content = await readFile(envPath, 'utf8');
2689
+ const match = content.match(/PORT\s*=\s*(\d+)/i);
2690
+ if (match) {
2691
+ return { port: parseInt(match[1]), method: 'dotenv' };
2692
+ }
2693
+ } catch (e) {
2694
+ // Continue to next method
2695
+ }
2696
+ }
2697
+
2698
+ // Try .env.local
2699
+ const envLocalPath = path.join(servicePath, '.env.local');
2700
+ if (await exists(envLocalPath)) {
2701
+ try {
2702
+ const content = await readFile(envLocalPath, 'utf8');
2703
+ const match = content.match(/PORT\s*=\s*(\d+)/i);
2704
+ if (match) {
2705
+ return { port: parseInt(match[1]), method: 'dotenv-local' };
2706
+ }
2707
+ } catch (e) {
2708
+ // Continue to next method
2709
+ }
2710
+ }
2711
+
2712
+ // Try package.json scripts
2713
+ const pkgPath = path.join(servicePath, 'package.json');
2714
+ if (await exists(pkgPath)) {
2715
+ try {
2716
+ const content = await readFile(pkgPath, 'utf8');
2717
+ const pkg = JSON.parse(content);
2718
+ const scripts = JSON.stringify(pkg.scripts || {});
2719
+
2720
+ // Look for --port or PORT= patterns
2721
+ const match = scripts.match(/--port[=\s]+(\d+)|PORT[=\s]+(\d+)|-p\s+(\d+)/i);
2722
+ if (match) {
2723
+ const port = parseInt(match[1] || match[2] || match[3]);
2724
+ return { port, method: 'package-json-scripts' };
2725
+ }
2726
+ } catch (e) {
2727
+ // Continue to default
2728
+ }
2729
+ }
2730
+
2731
+ return { port: 3000, method: 'node-default' };
2732
+ }
2733
+
2734
+ /**
2735
+ * Detects Python port from configuration files
2736
+ */
2737
+ async function detectPythonPort(servicePath) {
2738
+ // Try .env file
2739
+ const envPath = path.join(servicePath, '.env');
2740
+ if (await exists(envPath)) {
2741
+ try {
2742
+ const content = await readFile(envPath, 'utf8');
2743
+ const match = content.match(/PORT\s*=\s*(\d+)/i);
2744
+ if (match) {
2745
+ return { port: parseInt(match[1]), method: 'dotenv' };
2746
+ }
2747
+ } catch (e) {
2748
+ // Continue to next method
2749
+ }
2750
+ }
2751
+
2752
+ // Try config.py or settings.py
2753
+ const configFiles = ['config.py', 'settings.py', 'app/config.py', 'src/config.py'];
2754
+ for (const configFile of configFiles) {
2755
+ const configPath = path.join(servicePath, configFile);
2756
+ if (await exists(configPath)) {
2757
+ try {
2758
+ const content = await readFile(configPath, 'utf8');
2759
+ const match = content.match(/PORT\s*=\s*(\d+)/i);
2760
+ if (match) {
2761
+ return { port: parseInt(match[1]), method: `python-config:${configFile}` };
2762
+ }
2763
+ } catch (e) {
2764
+ continue;
2765
+ }
2766
+ }
2767
+ }
2768
+
2769
+ return { port: 8000, method: 'python-default' };
2770
+ }
2771
+
2772
+ /**
2773
+ * Detects .NET port from launchSettings.json
2774
+ */
2775
+ async function detectDotNetPort(servicePath) {
2776
+ const launchSettings = path.join(servicePath, 'Properties/launchSettings.json');
2777
+
2778
+ if (await exists(launchSettings)) {
2779
+ try {
2780
+ const content = await readFile(launchSettings, 'utf8');
2781
+ const settings = JSON.parse(content);
2782
+
2783
+ const profiles = settings.profiles || {};
2784
+ for (const [profileName, profile] of Object.entries(profiles)) {
2785
+ if (profile.applicationUrl) {
2786
+ const match = profile.applicationUrl.match(/:(\d+)/);
2787
+ if (match) {
2788
+ return { port: parseInt(match[1]), method: `launchSettings:${profileName}` };
2789
+ }
2790
+ }
2791
+ }
2792
+ } catch (e) {
2793
+ // Continue to default
2794
+ }
2795
+ }
2796
+
2797
+ // Try appsettings.json
2798
+ const appSettings = path.join(servicePath, 'appsettings.json');
2799
+ if (await exists(appSettings)) {
2800
+ try {
2801
+ const content = await readFile(appSettings, 'utf8');
2802
+ const settings = JSON.parse(content);
2803
+
2804
+ if (settings.Kestrel?.Endpoints?.Http?.Url) {
2805
+ const match = settings.Kestrel.Endpoints.Http.Url.match(/:(\d+)/);
2806
+ if (match) {
2807
+ return { port: parseInt(match[1]), method: 'appsettings-kestrel' };
2808
+ }
2809
+ }
2810
+ } catch (e) {
2811
+ // Continue to default
2812
+ }
2813
+ }
2814
+
2815
+ return { port: 5000, method: 'dotnet-default' };
2816
+ }
2817
+
2818
+ // ============================================
2819
+ // TEST LOCAL ENDPOINTS (existing)
2820
+ // ============================================
2821
+
2822
+ /** Prepare for testing: compile + discover endpoints */
2823
+ app.post("/workspace/test-local/prepare", async (req, res) => {
2824
+ if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
2825
+
2826
+ try {
2827
+ console.log("🔧 [TEST-LOCAL] Preparing test environment...");
2828
+ TEST_LOCAL_STATE.status = "compiling";
2829
+
2830
+ // Step 1: Compile
2831
+ const meta = await detectProject(WORKSPACE_ROOT);
2832
+ const compileResult = await compileAndTest({
2833
+ language: meta.language,
2834
+ buildTool: meta.buildTool,
2835
+ cwd: WORKSPACE_ROOT,
2836
+ skipTests: true
2837
+ });
2838
+
2839
+ if (compileResult.code !== 0) {
2840
+ TEST_LOCAL_STATE.status = "error";
2841
+ return res.status(500).json({
2842
+ ok: false,
2843
+ step: "compile",
2844
+ error: compileResult.stderr
2845
+ });
2846
+ }
2847
+
2848
+ TEST_LOCAL_STATE.status = "compiled";
2849
+
2850
+ // Step 2: Discover endpoints
2851
+ const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
2852
+ const structure = await scanner.scan();
2853
+
2854
+ const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
2855
+ const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
2856
+
2857
+ const dtoAnalyzer = new DTOAnalyzer(WORKSPACE_ROOT);
2858
+ const payloadDocs = await dtoAnalyzer.generatePayloadDocs(structure.files, apiDocs.endpoints);
2859
+
2860
+ TEST_LOCAL_STATE.endpoints = payloadDocs.endpoints;
2861
+
2862
+ // Step 3: Get config
2863
+ const configAnalyzer = new ConfigAnalyzer(WORKSPACE_ROOT);
2864
+ const config = await configAnalyzer.analyze();
2865
+ TEST_LOCAL_STATE.config = config;
2866
+
2867
+ console.log(`✅ [TEST-LOCAL] Prepared: ${payloadDocs.endpoints.length} endpoints discovered`);
2868
+
2869
+ res.json({
2870
+ ok: true,
2871
+ compilation: {
2872
+ success: true,
2873
+ language: meta.language,
2874
+ buildTool: meta.buildTool,
2875
+ duration: compileResult.duration
2876
+ },
2877
+ discovery: {
2878
+ endpoints: payloadDocs.endpoints.length,
2879
+ dtos: payloadDocs.totalDtos,
2880
+ controllers: apiDocs.totalControllers
2881
+ },
2882
+ config: {
2883
+ port: config.server.port,
2884
+ contextPath: config.server.contextPath,
2885
+ profiles: configAnalyzer.detectProfiles(config.files)
2886
+ },
2887
+ endpoints: payloadDocs.endpoints
2888
+ });
2889
+ } catch (err) {
2890
+ console.error("❌ [TEST-LOCAL] Prepare failed:", err.message);
2891
+ TEST_LOCAL_STATE.status = "error";
2892
+ res.status(500).json({ ok: false, error: err.message });
2893
+ }
2894
+ });
2895
+
2896
+ /**
2897
+ * GET /workspace/test-local/compile/stream
2898
+ * SSE endpoint for real-time compilation logs
2899
+ */
2900
+ app.get("/workspace/test-local/compile/stream", async (req, res) => {
2901
+ if (!WORKSPACE_ROOT) {
2902
+ res.status(400).json({ error: "workspace not set" });
2903
+ return;
2904
+ }
2905
+
2906
+ console.log("[INFO] Starting compilation stream...");
2907
+
2908
+ // Set headers for SSE
2909
+ res.setHeader("Content-Type", "text/event-stream");
2910
+ res.setHeader("Cache-Control", "no-cache");
2911
+ res.setHeader("Connection", "keep-alive");
2912
+ res.flushHeaders();
2913
+
2914
+ try {
2915
+ const meta = await detectProject(WORKSPACE_ROOT);
2916
+
2917
+ // Send initial event
2918
+ res.write(`event: log\n`);
2919
+ res.write(`data: ${JSON.stringify({ type: "info", line: `[INFO] Starting build (compile + package + install)...`, timestamp: Date.now() })}\n\n`);
2920
+
2921
+ res.write(`event: log\n`);
2922
+ res.write(`data: ${JSON.stringify({ type: "info", line: `[INFO] Build tool: ${meta.buildTool}`, timestamp: Date.now() })}\n\n`);
2923
+
2924
+ // Import spawn
2925
+ const { spawn } = await import("child_process");
2926
+
2927
+ // Determine command based on build tool
2928
+ let command, args;
2929
+ switch (meta.buildTool) {
2930
+ case "maven":
2931
+ command = "mvn";
2932
+ args = ["clean", "install", "-DskipTests", "-B"];
2933
+ break;
2934
+ case "gradle":
2935
+ command = "./gradlew";
2936
+ args = ["clean", "build", "-x", "test"];
2937
+ break;
2938
+ case "npm":
2939
+ command = "npm";
2940
+ args = ["run", "build"];
2941
+ break;
2942
+ case "yarn":
2943
+ command = "yarn";
2944
+ args = ["build"];
2945
+ break;
2946
+ default:
2947
+ res.write(`event: error\n`);
2948
+ res.write(`data: ${JSON.stringify({ error: `Unsupported build tool: ${meta.buildTool}` })}\n\n`);
2949
+ res.end();
2950
+ return;
2951
+ }
2952
+
2953
+ console.log(`[INFO] Running: ${command} ${args.join(" ")}`);
2954
+
2955
+ const startTime = Date.now();
2956
+ const proc = spawn(command, args, {
2957
+ cwd: WORKSPACE_ROOT,
2958
+ shell: true,
2959
+ env: { ...process.env, MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.defaultLogLevel=info" }
2960
+ });
2961
+
2962
+ // Stream stdout
2963
+ proc.stdout.on("data", (data) => {
2964
+ const lines = data.toString().split("\n").filter(l => l.trim());
2965
+ lines.forEach(line => {
2966
+ console.log(`[STDOUT] ${line}`);
2967
+ res.write(`event: log\n`);
2968
+ res.write(`data: ${JSON.stringify({ type: "stdout", line, timestamp: Date.now() })}\n\n`);
2969
+ });
2970
+ });
2971
+
2972
+ // Stream stderr
2973
+ proc.stderr.on("data", (data) => {
2974
+ const lines = data.toString().split("\n").filter(l => l.trim());
2975
+ lines.forEach(line => {
2976
+ console.log(`[STDERR] ${line}`);
2977
+ res.write(`event: log\n`);
2978
+ res.write(`data: ${JSON.stringify({ type: "stderr", line, timestamp: Date.now() })}\n\n`);
2979
+ });
2980
+ });
2981
+
2982
+ // Handle completion
2983
+ proc.on("close", (code) => {
2984
+ const duration = Date.now() - startTime;
2985
+ const success = code === 0;
2986
+
2987
+ console.log(`[INFO] Compilation ${success ? "successful" : "failed"} (${duration}ms)`);
2988
+
2989
+ res.write(`event: complete\n`);
2990
+ res.write(`data: ${JSON.stringify({
2991
+ success,
2992
+ exitCode: code,
2993
+ duration,
2994
+ message: success ? "Compilation successful" : "Compilation failed"
2995
+ })}\n\n`);
2996
+
2997
+ res.end();
2998
+ });
2999
+
3000
+ // Handle errors
3001
+ proc.on("error", (error) => {
3002
+ console.error(`[ERROR] Compilation process error: ${error.message}`);
3003
+ res.write(`event: error\n`);
3004
+ res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
3005
+ res.end();
3006
+ });
3007
+
3008
+ // Handle client disconnect
3009
+ req.on("close", () => {
3010
+ console.log("[INFO] Client disconnected, killing compilation process");
3011
+ proc.kill();
3012
+ });
3013
+
3014
+ } catch (error) {
3015
+ console.error(`[ERROR] Compilation stream error: ${error.message}`);
3016
+ res.write(`event: error\n`);
3017
+ res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
3018
+ res.end();
3019
+ }
3020
+ });
3021
+
3022
+ /**
3023
+ * GET /workspace/test-local/logs/stream
3024
+ * SSE endpoint for real-time server logs
3025
+ */
3026
+ app.get("/workspace/test-local/logs/stream", async (req, res) => {
3027
+ console.log("[INFO] Client connected to logs stream");
3028
+
3029
+ // Set headers for SSE
3030
+ res.setHeader("Content-Type", "text/event-stream");
3031
+ res.setHeader("Cache-Control", "no-cache");
3032
+ res.setHeader("Connection", "keep-alive");
3033
+ res.flushHeaders();
3034
+
3035
+ try {
3036
+ // Send existing logs first
3037
+ if (TEST_LOCAL_STATE.serverLogs.length > 0) {
3038
+ TEST_LOCAL_STATE.serverLogs.forEach(log => {
3039
+ res.write(`event: log\n`);
3040
+ res.write(`data: ${JSON.stringify(log)}\n\n`);
3041
+ });
3042
+ }
3043
+
3044
+ // Send heartbeat to keep connection alive
3045
+ const heartbeatInterval = setInterval(() => {
3046
+ res.write(`event: heartbeat\n`);
3047
+ res.write(`data: ${JSON.stringify({ timestamp: Date.now() })}\n\n`);
3048
+ }, 30000);
3049
+
3050
+ // Monitor for new logs
3051
+ const logCheckInterval = setInterval(() => {
3052
+ // This will be updated by ProcessManager events
3053
+ // For now, just keep connection alive
3054
+ }, 1000);
3055
+
3056
+ // Handle client disconnect
3057
+ req.on("close", () => {
3058
+ console.log("[INFO] Client disconnected from logs stream");
3059
+ clearInterval(heartbeatInterval);
3060
+ clearInterval(logCheckInterval);
3061
+ });
3062
+
3063
+ } catch (error) {
3064
+ console.error(`[ERROR] Logs stream error: ${error.message}`);
3065
+ res.write(`event: error\n`);
3066
+ res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
3067
+ res.end();
3068
+ }
3069
+ });
3070
+
3071
+ // ============================================
3072
+ // 🆕 AUTO-TRAINING ENDPOINTS
3073
+ // Escanear e ler arquivos para treinamento AI
3074
+ // ============================================
3075
+
3076
+ /**
3077
+ * POST /workspace/scan-files
3078
+ * Escaneia todos os arquivos do workspace para auto-training
3079
+ */
3080
+ app.post("/workspace/scan-files", async (req, res) => {
3081
+ const { root, excludePatterns = [], includeExtensions = [], maxDepth = 10, maxFiles = 1000 } = req.body;
3082
+
3083
+ const workspaceRoot = root || WORKSPACE_ROOT;
3084
+ if (!workspaceRoot) {
3085
+ return res.status(400).json({
3086
+ error: "workspace not set",
3087
+ hint: "provide 'root' in body or call POST /workspace/open first"
3088
+ });
3089
+ }
3090
+
3091
+ console.log(`📂 [SCAN-FILES] Scanning: ${workspaceRoot}`);
3092
+ console.log(` Extensions: ${includeExtensions.join(", ")}`);
3093
+ console.log(` Exclude: ${excludePatterns.join(", ")}`);
3094
+
3095
+ try {
3096
+ const files = [];
3097
+ const fs = await import("fs/promises");
3098
+ const pathModule = await import("path");
3099
+
3100
+ // Padrões padrão para excluir
3101
+ const defaultExcludes = [
3102
+ "node_modules", "target", "build", "dist", ".git", ".idea",
3103
+ ".vscode", "__pycache__", ".gradle", "bin", "obj",
3104
+ "*.min.js", "*.min.css", "package-lock.json", "yarn.lock"
3105
+ ];
3106
+ const allExcludes = [...defaultExcludes, ...excludePatterns];
3107
+
3108
+ // Função recursiva para escanear
3109
+ async function scanDir(dir, depth = 0) {
3110
+ if (depth > maxDepth || files.length >= maxFiles) return;
3111
+
3112
+ try {
3113
+ const entries = await fs.readdir(dir, { withFileTypes: true });
3114
+
3115
+ for (const entry of entries) {
3116
+ if (files.length >= maxFiles) break;
3117
+
3118
+ const fullPath = pathModule.join(dir, entry.name);
3119
+ const relativePath = pathModule.relative(workspaceRoot, fullPath);
3120
+
3121
+ // Verificar se deve excluir
3122
+ const shouldExclude = allExcludes.some(pattern => {
3123
+ if (pattern.includes("*")) {
3124
+ const regex = new RegExp(pattern.replace(/\*/g, ".*"));
3125
+ return regex.test(entry.name) || regex.test(relativePath);
3126
+ }
3127
+ return entry.name === pattern || relativePath.includes(pattern);
3128
+ });
3129
+
3130
+ if (shouldExclude) continue;
3131
+
3132
+ if (entry.isDirectory()) {
3133
+ await scanDir(fullPath, depth + 1);
3134
+ } else if (entry.isFile()) {
3135
+ // Verificar extensão
3136
+ const ext = pathModule.extname(entry.name).toLowerCase();
3137
+ if (includeExtensions.length === 0 || includeExtensions.includes(ext)) {
3138
+ files.push(relativePath);
3139
+ }
3140
+ }
3141
+ }
3142
+ } catch (error) {
3143
+ console.warn(`⚠️ Cannot read directory ${dir}: ${error.message}`);
3144
+ }
3145
+ }
3146
+
3147
+ await scanDir(workspaceRoot);
3148
+
3149
+ console.log(`✅ [SCAN-FILES] Found ${files.length} files`);
3150
+
3151
+ res.json({
3152
+ success: true,
3153
+ files,
3154
+ totalFiles: files.length,
3155
+ workspace: workspaceRoot
3156
+ });
3157
+
3158
+ } catch (error) {
3159
+ console.error(`❌ [SCAN-FILES] Error: ${error.message}`);
3160
+ res.status(500).json({
3161
+ success: false,
3162
+ error: error.message,
3163
+ files: []
3164
+ });
3165
+ }
3166
+ });
3167
+
3168
+ /**
3169
+ * POST /workspace/read-file
3170
+ * Lê conteúdo de um arquivo específico
3171
+ */
3172
+ app.post("/workspace/read-file", async (req, res) => {
3173
+ const { root, filePath } = req.body;
3174
+
3175
+ const workspaceRoot = root || WORKSPACE_ROOT;
3176
+ if (!workspaceRoot) {
3177
+ return res.status(400).json({
3178
+ error: "workspace not set"
3179
+ });
3180
+ }
3181
+
3182
+ if (!filePath) {
3183
+ return res.status(400).json({
3184
+ error: "filePath is required"
3185
+ });
3186
+ }
3187
+
3188
+ console.log(`📄 [READ-FILE] Reading: ${filePath}`);
3189
+
3190
+ try {
3191
+ const fs = await import("fs/promises");
3192
+ const pathModule = await import("path");
3193
+
3194
+ const fullPath = pathModule.join(workspaceRoot, filePath);
3195
+
3196
+ // Verificar se arquivo existe
3197
+ const stats = await fs.stat(fullPath);
3198
+
3199
+ // Limitar tamanho (100KB)
3200
+ if (stats.size > 100 * 1024) {
3201
+ return res.json({
3202
+ success: false,
3203
+ error: "File too large (max 100KB)",
3204
+ filePath,
3205
+ sizeBytes: stats.size
3206
+ });
3207
+ }
3208
+
3209
+ const content = await fs.readFile(fullPath, "utf-8");
3210
+
3211
+ res.json({
3212
+ success: true,
3213
+ filePath,
3214
+ content,
3215
+ sizeBytes: content.length
3216
+ });
3217
+
3218
+ } catch (error) {
3219
+ console.error(`❌ [READ-FILE] Error: ${error.message}`);
3220
+ res.status(404).json({
3221
+ success: false,
3222
+ error: error.message,
3223
+ filePath
3224
+ });
3225
+ }
3226
+ });
3227
+
3228
+ /**
3229
+ * POST /workspace/read-files
3230
+ * Lê múltiplos arquivos de uma vez (batch)
3231
+ */
3232
+ app.post("/workspace/read-files", async (req, res) => {
3233
+ const { root, filePaths } = req.body;
3234
+
3235
+ const workspaceRoot = root || WORKSPACE_ROOT;
3236
+ if (!workspaceRoot) {
3237
+ return res.status(400).json({
3238
+ error: "workspace not set"
3239
+ });
3240
+ }
3241
+
3242
+ if (!filePaths || !Array.isArray(filePaths)) {
3243
+ return res.status(400).json({
3244
+ error: "filePaths array is required"
3245
+ });
3246
+ }
3247
+
3248
+ console.log(`📚 [READ-FILES] Reading ${filePaths.length} files`);
3249
+
3250
+ try {
3251
+ const fs = await import("fs/promises");
3252
+ const pathModule = await import("path");
3253
+
3254
+ const files = {};
3255
+ const errors = [];
3256
+
3257
+ for (const filePath of filePaths) {
3258
+ try {
3259
+ const fullPath = pathModule.join(workspaceRoot, filePath);
3260
+ const stats = await fs.stat(fullPath);
3261
+
3262
+ // Pular arquivos muito grandes
3263
+ if (stats.size > 100 * 1024) {
3264
+ errors.push({ filePath, error: "File too large" });
3265
+ continue;
3266
+ }
3267
+
3268
+ const content = await fs.readFile(fullPath, "utf-8");
3269
+ files[filePath] = content;
3270
+ } catch (error) {
3271
+ errors.push({ filePath, error: error.message });
3272
+ }
3273
+ }
3274
+
3275
+ console.log(`✅ [READ-FILES] Read ${Object.keys(files).length} files, ${errors.length} errors`);
3276
+
3277
+ res.json({
3278
+ success: true,
3279
+ files,
3280
+ totalRead: Object.keys(files).length,
3281
+ errors: errors.length > 0 ? errors : undefined
3282
+ });
3283
+
3284
+ } catch (error) {
3285
+ console.error(`❌ [READ-FILES] Error: ${error.message}`);
3286
+ res.status(500).json({
3287
+ success: false,
3288
+ error: error.message
3289
+ });
3290
+ }
3291
+ });
3292
+
3293
+ // ============================================
3294
+ // 🆕 SYSTEM FOLDER PICKER ENDPOINT
3295
+ // Abre file picker nativo do SO (Windows/Mac/Linux)
3296
+ // ============================================
3297
+
3298
+ /**
3299
+ * GET /system/folder-picker
3300
+ * Abre o file picker nativo do sistema operacional
3301
+ */
3302
+ app.get("/system/folder-picker", async (req, res) => {
3303
+ console.log(`📂 [FOLDER-PICKER] Opening native folder picker...`);
3304
+
3305
+ try {
3306
+ const platform = process.platform;
3307
+ let selectedPath = null;
3308
+
3309
+ if (platform === "darwin") {
3310
+ // macOS - usar osascript (AppleScript)
3311
+ const script = `osascript -e 'POSIX path of (choose folder with prompt "Select your project folder")'`;
3312
+ try {
3313
+ const { stdout } = await execAsync(script);
3314
+ selectedPath = stdout.trim();
3315
+ } catch (error) {
3316
+ // Usuário cancelou
3317
+ if (error.code === 1) {
3318
+ return res.json({
3319
+ success: true,
3320
+ path: null,
3321
+ canceled: true
3322
+ });
3323
+ }
3324
+ throw error;
3325
+ }
3326
+
3327
+ } else if (platform === "win32") {
3328
+ // Windows - usar PowerShell
3329
+ const script = `powershell -command "Add-Type -AssemblyName System.Windows.Forms; $folder = New-Object System.Windows.Forms.FolderBrowserDialog; $folder.Description = 'Select your project folder'; $folder.ShowNewFolderButton = $true; if ($folder.ShowDialog() -eq 'OK') { $folder.SelectedPath } else { '' }"`;
3330
+ try {
3331
+ const { stdout } = await execAsync(script);
3332
+ selectedPath = stdout.trim();
3333
+ if (!selectedPath) {
3334
+ return res.json({
3335
+ success: true,
3336
+ path: null,
3337
+ canceled: true
3338
+ });
3339
+ }
3340
+ } catch (error) {
3341
+ throw error;
3342
+ }
3343
+
3344
+ } else if (platform === "linux") {
3345
+ // Linux - tentar zenity, kdialog ou yad
3346
+ const commands = [
3347
+ `zenity --file-selection --directory --title="Select your project folder"`,
3348
+ `kdialog --getexistingdirectory ~ --title "Select your project folder"`,
3349
+ `yad --file --directory --title="Select your project folder"`
3350
+ ];
3351
+
3352
+ let success = false;
3353
+ for (const cmd of commands) {
3354
+ try {
3355
+ const { stdout } = await execAsync(cmd);
3356
+ selectedPath = stdout.trim();
3357
+ if (selectedPath) {
3358
+ success = true;
3359
+ break;
3360
+ }
3361
+ } catch (error) {
3362
+ continue;
3363
+ }
3364
+ }
3365
+
3366
+ if (!success) {
3367
+ return res.status(500).json({
3368
+ success: false,
3369
+ error: "No file picker available. Install zenity, kdialog, or yad.",
3370
+ hint: "sudo apt install zenity"
3371
+ });
3372
+ }
3373
+
3374
+ } else {
3375
+ return res.status(500).json({
3376
+ success: false,
3377
+ error: `Unsupported platform: ${platform}`
3378
+ });
3379
+ }
3380
+
3381
+ // Remover trailing slash se houver
3382
+ if (selectedPath && selectedPath.endsWith("/")) {
3383
+ selectedPath = selectedPath.slice(0, -1);
3384
+ }
3385
+
3386
+ console.log(`✅ [FOLDER-PICKER] Selected: ${selectedPath}`);
3387
+
3388
+ res.json({
3389
+ success: true,
3390
+ path: selectedPath,
3391
+ canceled: false
3392
+ });
3393
+
3394
+ } catch (error) {
3395
+ console.error(`❌ [FOLDER-PICKER] Error:`, error.message);
3396
+ res.status(500).json({
3397
+ success: false,
3398
+ error: error.message,
3399
+ path: null,
3400
+ canceled: false
3401
+ });
3402
+ }
3403
+ });
3404
+
3405
+ /**
3406
+ * GET /system/info
3407
+ * Retorna informações do sistema
3408
+ */
3409
+ app.get("/system/info", (req, res) => {
3410
+ res.json({
3411
+ platform: process.platform,
3412
+ arch: process.arch,
3413
+ nodeVersion: process.version,
3414
+ homedir: process.env.HOME || process.env.USERPROFILE,
3415
+ cwd: process.cwd()
3416
+ });
3417
+ });
3418
+
3419
+ // ============================================
3420
+ // 🆕 API DOCS ENDPOINT
3421
+ // Retorna endpoints detectados das controllers
3422
+ // ============================================
3423
+
3424
+ /**
3425
+ * GET /workspace/api-docs
3426
+ * Analisa controllers e retorna todos os endpoints da API
3427
+ */
3428
+ app.get("/workspace/api-docs", async (req, res) => {
3429
+ if (!WORKSPACE_ROOT) {
3430
+ return res.status(400).json({
3431
+ error: "workspace not set",
3432
+ hint: "call POST /workspace/open first"
3433
+ });
3434
+ }
3435
+
3436
+ console.log(`📚 [API-DOCS] Analyzing controllers in ${WORKSPACE_ROOT}`);
3437
+
3438
+ try {
3439
+ // Primeiro, escanear arquivos do workspace
3440
+ const fs = await import("fs/promises");
3441
+ const pathModule = await import("path");
3442
+
3443
+ const files = [];
3444
+
3445
+ async function scanDir(dir, depth = 0) {
3446
+ if (depth > 10) return;
3447
+
3448
+ try {
3449
+ const entries = await fs.readdir(dir, { withFileTypes: true });
3450
+
3451
+ for (const entry of entries) {
3452
+ const fullPath = pathModule.join(dir, entry.name);
3453
+ const relativePath = pathModule.relative(WORKSPACE_ROOT, fullPath);
3454
+
3455
+ // Skip common ignored directories
3456
+ if (entry.name === 'node_modules' || entry.name === 'target' ||
3457
+ entry.name === 'build' || entry.name === '.git' ||
3458
+ entry.name === '.idea' || entry.name === 'dist') {
3459
+ continue;
3460
+ }
3461
+
3462
+ if (entry.isDirectory()) {
3463
+ await scanDir(fullPath, depth + 1);
3464
+ } else if (entry.isFile() && entry.name.endsWith('.java')) {
3465
+ files.push({ path: relativePath });
3466
+ }
3467
+ }
3468
+ } catch (err) {
3469
+ // Ignore permission errors
3470
+ }
3471
+ }
3472
+
3473
+ await scanDir(WORKSPACE_ROOT);
3474
+
3475
+ console.log(`📁 [API-DOCS] Found ${files.length} Java files`);
3476
+
3477
+ // Agora analisar os controllers
3478
+ const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
3479
+ const apiDocs = await controllerAnalyzer.generateApiDocs(files);
3480
+
3481
+ console.log(`✅ [API-DOCS] Found ${apiDocs.totalEndpoints} endpoints in ${apiDocs.totalControllers} controllers`);
3482
+
3483
+ res.json({
3484
+ success: true,
3485
+ endpoints: apiDocs.endpoints,
3486
+ controllers: apiDocs.controllers.map(c => c.className),
3487
+ totalEndpoints: apiDocs.totalEndpoints,
3488
+ totalControllers: apiDocs.totalControllers,
3489
+ workspace: WORKSPACE_ROOT
3490
+ });
3491
+
3492
+ } catch (error) {
3493
+ console.error(`❌ [API-DOCS] Error:`, error.message);
3494
+
3495
+ res.json({
3496
+ success: false,
3497
+ error: error.message,
3498
+ endpoints: [],
3499
+ controllers: [],
3500
+ totalEndpoints: 0,
3501
+ totalControllers: 0
3502
+ });
3503
+ }
3504
+ });
3505
+
3506
+ /**
3507
+ * GET /workspace/api-docs/:controller
3508
+ * Retorna endpoints de uma controller específica
3509
+ */
3510
+ app.get("/workspace/api-docs/:controller", async (req, res) => {
3511
+ if (!WORKSPACE_ROOT) {
3512
+ return res.status(400).json({
3513
+ error: "workspace not set",
3514
+ hint: "call POST /workspace/open first"
3515
+ });
3516
+ }
3517
+
3518
+ const { controller } = req.params;
3519
+ console.log(`📚 [API-DOCS] Getting endpoints for controller: ${controller}`);
3520
+
3521
+ try {
3522
+ // Primeiro, escanear arquivos do workspace
3523
+ const fs = await import("fs/promises");
3524
+ const pathModule = await import("path");
3525
+
3526
+ const files = [];
3527
+
3528
+ async function scanDir(dir, depth = 0) {
3529
+ if (depth > 10) return;
3530
+
3531
+ try {
3532
+ const entries = await fs.readdir(dir, { withFileTypes: true });
3533
+
3534
+ for (const entry of entries) {
3535
+ const fullPath = pathModule.join(dir, entry.name);
3536
+ const relativePath = pathModule.relative(WORKSPACE_ROOT, fullPath);
3537
+
3538
+ if (entry.name === 'node_modules' || entry.name === 'target' ||
3539
+ entry.name === 'build' || entry.name === '.git' ||
3540
+ entry.name === '.idea' || entry.name === 'dist') {
3541
+ continue;
3542
+ }
3543
+
3544
+ if (entry.isDirectory()) {
3545
+ await scanDir(fullPath, depth + 1);
3546
+ } else if (entry.isFile() && entry.name.endsWith('.java')) {
3547
+ files.push({ path: relativePath });
3548
+ }
3549
+ }
3550
+ } catch (err) {
3551
+ // Ignore permission errors
3552
+ }
3553
+ }
3554
+
3555
+ await scanDir(WORKSPACE_ROOT);
3556
+
3557
+ // Analisar os controllers
3558
+ const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
3559
+ const apiDocs = await controllerAnalyzer.generateApiDocs(files);
3560
+
3561
+ const found = apiDocs.controllers?.find(c =>
3562
+ c.className.toLowerCase() === controller.toLowerCase() ||
3563
+ c.className.toLowerCase().includes(controller.toLowerCase())
3564
+ );
3565
+
3566
+ if (!found) {
3567
+ return res.status(404).json({
3568
+ success: false,
3569
+ error: `Controller '${controller}' not found`,
3570
+ availableControllers: apiDocs.controllers?.map(c => c.className) || []
3571
+ });
3572
+ }
3573
+
3574
+ const endpoints = (found.endpoints || []).map(endpoint => ({
3575
+ method: endpoint.method || "GET",
3576
+ path: endpoint.path || "",
3577
+ methodName: endpoint.methodName || "",
3578
+ returnType: endpoint.returnType || "void",
3579
+ requestBody: endpoint.requestBody || null,
3580
+ pathVariables: endpoint.pathVariables || [],
3581
+ queryParams: endpoint.queryParams || []
3582
+ }));
3583
+
3584
+ res.json({
3585
+ success: true,
3586
+ controller: found.className,
3587
+ file: found.file,
3588
+ basePath: found.basePath,
3589
+ endpoints,
3590
+ totalEndpoints: endpoints.length
3591
+ });
3592
+
3593
+ } catch (error) {
3594
+ console.error(`❌ [API-DOCS] Error:`, error.message);
3595
+ res.status(500).json({
3596
+ success: false,
3597
+ error: error.message
3598
+ });
3599
+ }
3600
+ });
3601
+
3602
+ // ============================================
3603
+ // 🧠 AI ENGINE ENDPOINTS
3604
+ // ============================================
3605
+
3606
+ /**
3607
+ * GET /ai-engine/status
3608
+ * Retorna estado do AI Vibe Coding Engine
3609
+ */
3610
+ app.get("/ai-engine/status", (req, res) => {
3611
+ res.json(aiEngine ? aiEngine.getStatus() : { active: false, error: 'AI Engine not initialized' });
3612
+ });
3613
+
3614
+ /**
3615
+ * POST /ai-engine/toggle
3616
+ * Liga/desliga o auto-healing
3617
+ */
3618
+ app.post("/ai-engine/toggle", (req, res) => {
3619
+ const { active } = req.body || {};
3620
+ if (aiEngine) {
3621
+ aiEngine.setActive(active !== false);
3622
+ res.json({ ok: true, active: aiEngine.isActive });
3623
+ } else {
3624
+ res.status(500).json({ ok: false, error: 'AI Engine not initialized' });
3625
+ }
3626
+ });
3627
+
3628
+ /**
3629
+ * POST /ai-engine/clear-history
3630
+ * Limpa histórico de erros e fixes
3631
+ */
3632
+ app.post("/ai-engine/clear-history", (req, res) => {
3633
+ if (aiEngine) {
3634
+ aiEngine.clearHistory();
3635
+ res.json({ ok: true });
3636
+ } else {
3637
+ res.status(500).json({ ok: false, error: 'AI Engine not initialized' });
3638
+ }
3639
+ });
3640
+
3641
+ /**
3642
+ * GET /ai-engine/errors
3643
+ * Retorna histórico de erros
3644
+ */
3645
+ app.get("/ai-engine/errors", (req, res) => {
3646
+ const { limit = 50 } = req.query;
3647
+ if (aiEngine) {
3648
+ res.json({
3649
+ ok: true,
3650
+ errors: aiEngine.errorHistory.slice(-parseInt(limit)),
3651
+ total: aiEngine.errorHistory.length
3652
+ });
3653
+ } else {
3654
+ res.json({ ok: true, errors: [], total: 0 });
3655
+ }
3656
+ });
3657
+
3658
+ /**
3659
+ * GET /ai-engine/fixes
3660
+ * Retorna histórico de fixes
3661
+ */
3662
+ app.get("/ai-engine/fixes", (req, res) => {
3663
+ if (aiEngine) {
3664
+ res.json({
3665
+ ok: true,
3666
+ fixes: aiEngine.fixHistory,
3667
+ pending: aiEngine.pendingFixes
3668
+ });
3669
+ } else {
3670
+ res.json({ ok: true, fixes: [], pending: [] });
3671
+ }
3672
+ });
3673
+
3674
+ /**
3675
+ * GET /workspace/smart-config
3676
+ * Recolhe ficheiros de configuração para análise AI
3677
+ */
3678
+ app.get("/workspace/smart-config", async (req, res) => {
3679
+ if (!WORKSPACE_ROOT) {
3680
+ return res.status(400).json({ error: "workspace not set" });
3681
+ }
3682
+
3683
+ console.log("🧠 [SMART-CONFIG] Collecting configuration files...");
3684
+
3685
+ try {
3686
+ const configPatterns = [
3687
+ 'pom.xml', 'build.gradle', 'build.gradle.kts',
3688
+ 'src/main/resources/application.yml',
3689
+ 'src/main/resources/application.yaml',
3690
+ 'src/main/resources/application.properties',
3691
+ 'src/main/resources/application-dev.yml',
3692
+ 'src/main/resources/application-local.yml',
3693
+ 'src/main/resources/application-test.yml',
3694
+ 'src/main/resources/application-h2.yml',
3695
+ 'src/main/resources/application-prod.yml',
3696
+ 'src/main/resources/bootstrap.yml',
3697
+ 'package.json', '.env', '.env.local',
3698
+ 'docker-compose.yml', 'Dockerfile'
3699
+ ];
3700
+
3701
+ const collectedFiles = [];
3702
+ const meta = await detectProject(WORKSPACE_ROOT);
3703
+
3704
+ for (const pattern of configPatterns) {
3705
+ const filePath = path.join(WORKSPACE_ROOT, pattern);
3706
+ if (fs.existsSync(filePath)) {
3707
+ try {
3708
+ const content = fs.readFileSync(filePath, 'utf8');
3709
+ const maxSize = 15000;
3710
+ collectedFiles.push({
3711
+ path: pattern,
3712
+ content: content.length > maxSize
3713
+ ? content.substring(0, maxSize) + '\n...[truncated]'
3714
+ : content,
3715
+ size: content.length
3716
+ });
3717
+ console.log(`📄 Found: ${pattern}`);
3718
+ } catch (err) {
3719
+ console.error(`❌ Error reading ${pattern}: ${err.message}`);
3720
+ }
3721
+ }
3722
+ }
3723
+
3724
+ // Detectar profiles disponíveis
3725
+ const availableProfiles = [];
3726
+ const resourcesDir = path.join(WORKSPACE_ROOT, 'src/main/resources');
3727
+ if (fs.existsSync(resourcesDir)) {
3728
+ const files = fs.readdirSync(resourcesDir);
3729
+ files.forEach(file => {
3730
+ const match = file.match(/application-(\w+)\.(yml|yaml|properties)/);
3731
+ if (match) availableProfiles.push(match[1]);
3732
+ });
3733
+ }
3734
+
3735
+ console.log(`✅ Collected ${collectedFiles.length} files, profiles: ${availableProfiles.join(', ')}`);
3736
+
3737
+ res.json({
3738
+ ok: true,
3739
+ workspace: WORKSPACE_ROOT,
3740
+ language: meta.language,
3741
+ buildTool: meta.buildTool,
3742
+ framework: meta.framework || 'unknown',
3743
+ files: collectedFiles,
3744
+ availableProfiles,
3745
+ collectedAt: new Date().toISOString()
3746
+ });
3747
+
3748
+ } catch (err) {
3749
+ console.error("❌ [SMART-CONFIG] Error:", err.message);
3750
+ res.status(500).json({ ok: false, error: err.message });
3751
+ }
3752
+ });
3753
+
3754
+ /**
3755
+ * POST /workspace/test-local/restart
3756
+ * Restarts the server with auto-healing using last successful config
3757
+ */
3758
+ app.post("/workspace/test-local/restart", async (req, res) => {
3759
+ try {
3760
+ console.log("🔄 [TEST-LOCAL] Restarting server...");
3761
+
3762
+ // Stop
3763
+ await processManager.stop('test-local');
3764
+
3765
+ // Esperar um pouco
3766
+ await new Promise(r => setTimeout(r, 2000));
3767
+
3768
+ // Usar última config que funcionou
3769
+ const config = (aiEngine && aiEngine.lastSuccessfulConfig) || TEST_LOCAL_STATE.config;
3770
+
3771
+ if (!config) {
3772
+ return res.status(400).json({ error: "No previous config found" });
3773
+ }
3774
+
3775
+ // Start com auto-healing
3776
+ let result;
3777
+ if (aiEngine && aiEngine.isActive) {
3778
+ result = await aiEngine.startWithAutoHealing(config);
3779
+ } else {
3780
+ await processManager.start('test-local', config);
3781
+ result = { ok: true, config };
3782
+ }
3783
+
3784
+ if (result.ok) {
3785
+ TEST_LOCAL_STATE.status = "running";
3786
+ TEST_LOCAL_STATE.config = result.config;
3787
+
3788
+ res.json({
3789
+ ok: true,
3790
+ status: "running",
3791
+ config: result.config,
3792
+ attempts: result.attempts || 1,
3793
+ autoHealed: result.autoHealed || false
3794
+ });
3795
+ } else {
3796
+ TEST_LOCAL_STATE.status = "error";
3797
+ res.status(500).json({
3798
+ ok: false,
3799
+ error: result.error,
3800
+ attempts: result.attempts
3801
+ });
3802
+ }
3803
+
3804
+ } catch (err) {
3805
+ console.error("❌ [TEST-LOCAL] Restart failed:", err.message);
3806
+ res.status(500).json({ ok: false, error: err.message });
3807
+ }
3808
+ });
3809
+
3810
+ // ============================================
3811
+ // 🤖 AGENTIC TOOLS ENDPOINTS
3812
+ // Used by the agentic Claude loop for autonomous debugging
3813
+ // ============================================
3814
+
3815
+ /**
3816
+ * POST /workspace/search
3817
+ *
3818
+ * Grep-based search across all files in the workspace.
3819
+ * Used by the agentic Claude to find code patterns quickly.
3820
+ *
3821
+ * Body: { "pattern": "validateToken", "fileFilter": "*.java" }
3822
+ * Response: { "ok": true, "results": "file:line: matching text\n...", "count": 15 }
3823
+ */
3824
+ app.post("/workspace/search", async (req, res) => {
3825
+ if (!WORKSPACE_ROOT) {
3826
+ return res.status(400).json({
3827
+ ok: false,
3828
+ error: "workspace not set",
3829
+ hint: "call POST /workspace/open first"
3830
+ });
3831
+ }
3832
+
3833
+ const { pattern, fileFilter } = req.body || {};
3834
+ if (!pattern) {
3835
+ return res.status(400).json({ ok: false, error: "pattern is required" });
3836
+ }
3837
+
3838
+ console.log(`🔍 [workspace/search] pattern="${pattern}" filter="${fileFilter || '*'}"`);
3839
+
3840
+ try {
3841
+ const { execSync } = await import('child_process');
3842
+
3843
+ // Build grep command with safety limits
3844
+ let cmd = `grep -rn `;
3845
+
3846
+ // Apply file filter if provided
3847
+ if (fileFilter && fileFilter.trim()) {
3848
+ cmd += `--include="${fileFilter}" `;
3849
+ }
3850
+
3851
+ // Exclude common non-source directories
3852
+ cmd += `--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=target `;
3853
+ cmd += `--exclude-dir=build --exclude-dir=dist --exclude-dir=.idea `;
3854
+ cmd += `--exclude-dir=__pycache__ --exclude-dir=.next --exclude-dir=vendor `;
3855
+ cmd += `--exclude-dir=bin --exclude-dir=obj --exclude-dir=.gradle `;
3856
+ cmd += `--exclude-dir=.mvn --exclude-dir=.venv --exclude-dir=coverage `;
3857
+
3858
+ // Escape pattern for shell safety and limit results
3859
+ const safePattern = pattern.replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
3860
+ cmd += `"${safePattern}" "${WORKSPACE_ROOT}" | head -100`;
3861
+
3862
+ let stdout = '';
3863
+ try {
3864
+ stdout = execSync(cmd, {
3865
+ encoding: 'utf8',
3866
+ timeout: 15000,
3867
+ maxBuffer: 1024 * 1024
3868
+ });
3869
+ } catch (grepErr) {
3870
+ // grep returns exit code 1 when no matches found
3871
+ if (grepErr.status === 1) {
3872
+ return res.json({
3873
+ ok: true,
3874
+ results: `No matches found for: ${pattern}`,
3875
+ count: 0
3876
+ });
3877
+ }
3878
+ // Exit code 2 = error in grep
3879
+ if (grepErr.status === 2) {
3880
+ return res.status(400).json({
3881
+ ok: false,
3882
+ error: "Invalid search pattern: " + grepErr.message
3883
+ });
3884
+ }
3885
+ throw grepErr;
3886
+ }
3887
+
3888
+ // Make paths relative to workspace root for readability
3889
+ const results = stdout
3890
+ .split('\n')
3891
+ .filter(line => line.trim())
3892
+ .map(line => line.replace(WORKSPACE_ROOT + '/', ''))
3893
+ .join('\n');
3894
+
3895
+ const count = results.split('\n').filter(l => l.trim()).length;
3896
+ console.log(` ✅ Found ${count} matches`);
3897
+
3898
+ res.json({
3899
+ ok: true,
3900
+ results: results,
3901
+ count: count
3902
+ });
3903
+ } catch (err) {
3904
+ console.error(`❌ [workspace/search] Error:`, err.message);
3905
+ res.status(500).json({ ok: false, error: err.message });
3906
+ }
3907
+ });
3908
+
3909
+ /**
3910
+ * POST /workspace/exec
3911
+ *
3912
+ * Execute a shell command in the workspace directory.
3913
+ * Used by the agentic Claude for build steps, dependency checks, etc.
3914
+ *
3915
+ * Body: { "command": "mvn dependency:tree" }
3916
+ * Response: { "ok": true, "stdout": "...", "stderr": "...", "exitCode": 0 }
3917
+ *
3918
+ * SECURITY: Commands run inside the workspace directory only.
3919
+ * Dangerous commands are blocked.
3920
+ */
3921
+ app.post("/workspace/exec", async (req, res) => {
3922
+ if (!WORKSPACE_ROOT) {
3923
+ return res.status(400).json({
3924
+ ok: false,
3925
+ error: "workspace not set",
3926
+ hint: "call POST /workspace/open first"
3927
+ });
3928
+ }
3929
+
3930
+ const { command } = req.body || {};
3931
+ if (!command) {
3932
+ return res.status(400).json({ ok: false, error: "command is required" });
3933
+ }
3934
+
3935
+ // Security: block dangerous commands
3936
+ const dangerous = [
3937
+ 'rm -rf /', 'rm -rf /*', 'mkfs', 'dd if=', 'shutdown', 'reboot',
3938
+ 'init 0', '> /dev/', 'chmod -R 777 /', 'curl | sh', 'wget | sh',
3939
+ 'curl | bash', 'wget | bash', 'format c:', 'del /f /s /q'
3940
+ ];
3941
+ const lowerCmd = command.toLowerCase().trim();
3942
+ if (dangerous.some(d => lowerCmd.includes(d))) {
3943
+ console.warn(`⚠️ [workspace/exec] BLOCKED dangerous command: ${command}`);
3944
+ return res.status(403).json({
3945
+ ok: false,
3946
+ error: "Command blocked for security reasons"
3947
+ });
3948
+ }
3949
+
3950
+ console.log(`⚡ [workspace/exec] Running: ${command.substring(0, 120)}...`);
3951
+
3952
+ try {
3953
+ const { exec: execCb } = await import('child_process');
3954
+ const { promisify } = await import('util');
3955
+ const execAsync = promisify(execCb);
3956
+
3957
+ const result = await execAsync(command, {
3958
+ cwd: WORKSPACE_ROOT,
3959
+ timeout: 60000,
3960
+ maxBuffer: 5 * 1024 * 1024,
3961
+ env: { ...process.env, FORCE_COLOR: '0' }
3962
+ });
3963
+
3964
+ console.log(` ✅ Command completed (stdout: ${result.stdout.length} chars)`);
3965
+
3966
+ res.json({
3967
+ ok: true,
3968
+ stdout: result.stdout,
3969
+ stderr: result.stderr,
3970
+ exitCode: 0
3971
+ });
3972
+ } catch (err) {
3973
+ console.error(` ❌ Command failed (exit: ${err.code}):`, err.message?.substring(0, 200));
3974
+ res.json({
3975
+ ok: false,
3976
+ stdout: err.stdout || '',
3977
+ stderr: err.stderr || err.message,
3978
+ exitCode: err.code || 1
3979
+ });
3980
+ }
3981
+ });
3982
+
3983
+ // ============================================
3984
+ // 🧪 ENDPOINT TESTING
3985
+ // Execute curl-like requests and capture full request/response
3986
+ // ============================================
3987
+
3988
+ /**
3989
+ * POST /workspace/test-endpoint
3990
+ * Makes an HTTP request and captures the full request/response for evidence.
3991
+ *
3992
+ * Body: { method, url, headers?, body?, testName, expectedStatus? }
3993
+ */
3994
+ app.post("/workspace/test-endpoint", async (req, res) => {
3995
+ const { method = 'GET', url, headers = {}, body: reqBody, testName, expectedStatus } = req.body || {};
3996
+
3997
+ if (!url) {
3998
+ return res.status(400).json({ error: "url is required" });
3999
+ }
4000
+
4001
+ // Security: only localhost
4002
+ if (!url.startsWith('http://localhost') && !url.startsWith('http://127.0.0.1')) {
4003
+ return res.status(403).json({ error: "Only localhost URLs allowed for security" });
4004
+ }
4005
+
4006
+ console.log(`🧪 Testing: ${method} ${url} (${testName || 'unnamed'})`);
4007
+
4008
+ const startTime = Date.now();
4009
+
4010
+ try {
4011
+ const fetchOptions = {
4012
+ method: method.toUpperCase(),
4013
+ headers: {
4014
+ 'Content-Type': 'application/json',
4015
+ ...headers
4016
+ },
4017
+ signal: AbortSignal.timeout(15000) // 15s timeout
4018
+ };
4019
+
4020
+ if (reqBody && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
4021
+ fetchOptions.body = reqBody;
4022
+ }
4023
+
4024
+ const response = await fetch(url, fetchOptions);
4025
+ const durationMs = Date.now() - startTime;
4026
+
4027
+ // Capture response
4028
+ let responseBody = '';
4029
+ const contentType = response.headers.get('content-type') || '';
4030
+ if (contentType.includes('json') || contentType.includes('text')) {
4031
+ responseBody = await response.text();
4032
+ } else {
4033
+ responseBody = `[Binary content: ${contentType}, ${response.headers.get('content-length') || '?'} bytes]`;
4034
+ }
4035
+
4036
+ // Capture response headers (key ones)
4037
+ const responseHeaders = {};
4038
+ for (const key of ['content-type', 'content-length', 'x-request-id', 'authorization', 'set-cookie']) {
4039
+ const val = response.headers.get(key);
4040
+ if (val) responseHeaders[key] = val;
4041
+ }
4042
+
4043
+ const result = {
4044
+ ok: true,
4045
+ testName,
4046
+ status: response.status,
4047
+ statusText: response.statusText,
4048
+ body: responseBody.substring(0, 5000), // Cap at 5KB
4049
+ responseHeaders,
4050
+ durationMs,
4051
+ passed: expectedStatus
4052
+ ? response.status === expectedStatus
4053
+ : response.status >= 200 && response.status < 400
4054
+ };
4055
+
4056
+ console.log(` → ${response.status} ${response.statusText} (${durationMs}ms) ${result.passed ? '✅' : '❌'}`);
4057
+ res.json(result);
4058
+
4059
+ } catch (err) {
4060
+ const durationMs = Date.now() - startTime;
4061
+ console.error(` → ❌ Failed: ${err.message} (${durationMs}ms)`);
4062
+
4063
+ res.json({
4064
+ ok: false,
4065
+ testName,
4066
+ status: 0,
4067
+ statusText: 'Connection Failed',
4068
+ body: err.message,
4069
+ responseHeaders: {},
4070
+ durationMs,
4071
+ passed: false,
4072
+ error: err.message
4073
+ });
4074
+ }
4075
+ });
4076
+
4077
+ // ============================================
4078
+ // 🔀 GIT INTEGRATION
4079
+ // Create branch, commit, push for auto-fix PRs
4080
+ // ============================================
4081
+
4082
+ /**
4083
+ * POST /workspace/git/create-fix-branch
4084
+ * Creates a branch, commits changes, and pushes to remote.
4085
+ *
4086
+ * Body: { branchName, commitMessage, files? }
4087
+ */
4088
+ app.post("/workspace/git/create-fix-branch", async (req, res) => {
4089
+ if (!WORKSPACE_ROOT) {
4090
+ return res.status(400).json({ error: "workspace not set" });
4091
+ }
4092
+
4093
+ const { branchName, commitMessage, files, gitToken, repoUrl } = req.body || {};
4094
+ if (!branchName || !commitMessage) {
4095
+ return res.status(400).json({ error: "branchName and commitMessage required" });
4096
+ }
4097
+
4098
+ try {
4099
+ const { execSync } = await import('child_process');
4100
+ const opts = {
4101
+ cwd: WORKSPACE_ROOT,
4102
+ encoding: 'utf8',
4103
+ timeout: 30000,
4104
+ env: {
4105
+ ...process.env,
4106
+ GIT_TERMINAL_PROMPT: '0', // Never prompt for credentials
4107
+ GIT_ASKPASS: 'echo', // Return empty if asked
4108
+ GIT_SSH_COMMAND: 'ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5'
4109
+ }
4110
+ };
4111
+
4112
+ // 1. Check if git repo
4113
+ try {
4114
+ execSync('git rev-parse --is-inside-work-tree', opts);
4115
+ } catch {
4116
+ return res.status(400).json({
4117
+ ok: false,
4118
+ error: "Not a git repository",
4119
+ hint: "The workspace must be a git repository to create branches"
4120
+ });
4121
+ }
4122
+
4123
+ // 2. Get current branch
4124
+ const currentBranch = execSync('git branch --show-current', opts).trim();
4125
+
4126
+ // 3. Stash any uncommitted changes first (they might conflict with checkout)
4127
+ let hadStash = false;
4128
+ try {
4129
+ const stashResult = execSync('git stash --include-untracked 2>&1', opts).trim();
4130
+ hadStash = !stashResult.includes('No local changes');
4131
+ if (hadStash) console.log('📦 Stashed uncommitted changes');
4132
+ } catch {}
4133
+
4134
+ // 4. Create and checkout new branch (with unique name if exists)
4135
+ let finalBranchName = branchName;
4136
+ try {
4137
+ execSync(`git checkout -b ${finalBranchName}`, opts);
4138
+ } catch (e) {
4139
+ // Branch already exists — add timestamp suffix
4140
+ finalBranchName = `${branchName}-${Date.now() % 100000}`;
4141
+ try {
4142
+ execSync(`git checkout -b ${finalBranchName}`, opts);
4143
+ } catch (e2) {
4144
+ // Restore stash and bail
4145
+ if (hadStash) try { execSync('git stash pop', opts); } catch {}
4146
+ return res.status(400).json({ ok: false, error: `Could not create branch: ${e2.message}` });
4147
+ }
4148
+ }
4149
+
4150
+ // 5. Pop the stash back (apply the changes on the new branch)
4151
+ if (hadStash) {
4152
+ try {
4153
+ execSync('git stash pop', opts);
4154
+ } catch (e) {
4155
+ console.warn('⚠️ Stash pop had conflicts, trying apply:', e.message);
4156
+ try { execSync('git stash apply', opts); } catch {}
4157
+ }
4158
+ }
4159
+
4160
+ // 6. Stage changes
4161
+ if (files && files.length > 0) {
4162
+ for (const file of files) {
4163
+ execSync(`git add "${file}"`, opts);
4164
+ }
4165
+ } else {
4166
+ execSync('git add -A', opts);
4167
+ }
4168
+
4169
+ // 7. Check for changes
4170
+ const status = execSync('git status --porcelain', opts).trim();
4171
+ if (!status) {
4172
+ try { execSync(`git checkout ${currentBranch}`, opts); } catch {}
4173
+ return res.json({ ok: false, error: "No changes to commit" });
4174
+ }
4175
+
4176
+ // 8. Commit
4177
+ const safeMsg = commitMessage.replace(/"/g, '\\"').replace(/\n/g, '\\n');
4178
+ execSync(`git commit -m "${safeMsg}"`, opts);
4179
+
4180
+ // 9. Push (use token if available for authentication)
4181
+ let pushResult = '';
4182
+ let pushed = false;
4183
+
4184
+ console.log(`🔑 Git push — gitToken provided: ${!!gitToken}, repoUrl provided: ${!!repoUrl}`);
4185
+
4186
+ // If gitToken provided, set up authenticated remote URL for push
4187
+ if (gitToken) {
4188
+ try {
4189
+ let remoteUrlRaw = '';
4190
+ try {
4191
+ remoteUrlRaw = execSync('git remote get-url origin 2>/dev/null', opts).trim();
4192
+ } catch {
4193
+ // No remote configured — use repoUrl from integration if available
4194
+ if (repoUrl) {
4195
+ remoteUrlRaw = repoUrl;
4196
+ console.log(`📍 No git remote, using repoUrl from integration: ${repoUrl}`);
4197
+ }
4198
+ }
4199
+
4200
+ console.log(`📍 Remote URL: ${remoteUrlRaw.replace(gitToken, '***')}`);
4201
+ let authenticatedUrl = '';
4202
+
4203
+ if (remoteUrlRaw.includes('github.com')) {
4204
+ const repoPath = remoteUrlRaw
4205
+ .replace(/^https?:\/\/(.*@)?github\.com\//, '')
4206
+ .replace(/^git@github\.com:/, '')
4207
+ .replace(/\.git$/, '');
4208
+ authenticatedUrl = `https://x-access-token:${gitToken}@github.com/${repoPath}.git`;
4209
+ console.log(`📍 GitHub auth URL: https://x-access-token:***@github.com/${repoPath}.git`);
4210
+ } else if (remoteUrlRaw.includes('gitlab.com') || remoteUrlRaw.includes('gitlab')) {
4211
+ const repoPath = remoteUrlRaw
4212
+ .replace(/^https?:\/\/(.*@)?[^\/]+\//, '')
4213
+ .replace(/^git@[^:]+:/, '')
4214
+ .replace(/\.git$/, '');
4215
+ const host = remoteUrlRaw.match(/https?:\/\/([^\/]+)/)?.[1] || 'gitlab.com';
4216
+ authenticatedUrl = `https://oauth2:${gitToken}@${host}/${repoPath}.git`;
4217
+ console.log(`📍 GitLab auth URL: https://oauth2:***@${host}/${repoPath}.git`);
4218
+ } else if (remoteUrlRaw.includes('bitbucket.org') || remoteUrlRaw.includes('bitbucket')) {
4219
+ const repoPath = remoteUrlRaw
4220
+ .replace(/^https?:\/\/(.*@)?bitbucket\.org\//, '')
4221
+ .replace(/^git@bitbucket\.org:/, '')
4222
+ .replace(/\.git$/, '');
4223
+ authenticatedUrl = `https://x-token-auth:${gitToken}@bitbucket.org/${repoPath}.git`;
4224
+ } else if (remoteUrlRaw) {
4225
+ // Unknown provider — try generic https with token
4226
+ const urlObj = new URL(remoteUrlRaw.replace(/^git@([^:]+):/, 'https://$1/'));
4227
+ authenticatedUrl = `https://oauth2:${gitToken}@${urlObj.host}${urlObj.pathname}`;
4228
+ if (!authenticatedUrl.endsWith('.git')) authenticatedUrl += '.git';
4229
+ console.log(`📍 Generic auth URL for ${urlObj.host}`);
4230
+ }
4231
+
4232
+ if (authenticatedUrl) {
4233
+ console.log(`🚀 Pushing ${finalBranchName} with token auth...`);
4234
+ try {
4235
+ // Don't use 2>&1 — let execSync capture stderr separately
4236
+ pushResult = execSync(`git push ${authenticatedUrl} ${finalBranchName}`, {
4237
+ ...opts,
4238
+ timeout: 60000, // 60s for slow connections
4239
+ stdio: ['pipe', 'pipe', 'pipe'] // capture stdin, stdout, stderr separately
4240
+ }).toString();
4241
+ pushed = true;
4242
+ console.log(`✅ Pushed with token authentication`);
4243
+ } catch (innerErr) {
4244
+ // Mask token in error messages before logging
4245
+ const maskToken = (str) => str ? str.replace(gitToken, '***TOKEN***') : '';
4246
+ const errMsg = maskToken(innerErr.stderr?.toString() || innerErr.stdout?.toString() || innerErr.message || '');
4247
+ console.warn(`⚠️ Authenticated push failed: ${errMsg}`);
4248
+ pushResult = errMsg;
4249
+ }
4250
+ } else {
4251
+ console.warn(`⚠️ Could not construct authenticated URL from: ${remoteUrlRaw}`);
4252
+ }
4253
+ } catch (pushErr) {
4254
+ const maskToken = (str) => str ? str.replace(gitToken, '***TOKEN***') : '';
4255
+ console.warn(`⚠️ Git auth setup failed: ${maskToken(pushErr.message)}`);
4256
+ pushResult = maskToken(pushErr.message) || 'Push setup failed';
4257
+ }
4258
+ }
4259
+
4260
+ // Fallback: try push without token (uses local git credentials)
4261
+ if (!pushed) {
4262
+ try {
4263
+ pushResult = execSync(`git push -u origin ${finalBranchName} 2>&1`, opts).toString();
4264
+ pushed = true;
4265
+ } catch (pushErr) {
4266
+ try {
4267
+ pushResult = execSync(`git push origin ${finalBranchName} 2>&1`, opts).toString();
4268
+ pushed = true;
4269
+ } catch (pushErr2) {
4270
+ console.warn(`⚠️ Git push failed: ${pushErr2.message}`);
4271
+ pushResult = pushErr2.message || 'Push failed — no remote configured or auth required';
4272
+ }
4273
+ }
4274
+ }
4275
+
4276
+ // 8. Get commit SHA
4277
+ const commitSha = execSync('git rev-parse HEAD', opts).trim();
4278
+
4279
+ // 9. Get remote URL and generate MR/PR link
4280
+ let remoteUrl = '';
4281
+ let mrUrl = '';
4282
+ let targetBranch = 'develop'; // default target — always prefer develop
4283
+ try {
4284
+ remoteUrl = execSync('git remote get-url origin 2>/dev/null', opts).trim();
4285
+ // Always target develop if it exists, regardless of repo default branch
4286
+ try {
4287
+ execSync('git rev-parse --verify origin/develop 2>/dev/null', opts);
4288
+ targetBranch = 'develop';
4289
+ } catch {
4290
+ // develop doesn't exist remotely, check locally
4291
+ try {
4292
+ execSync('git rev-parse --verify develop 2>/dev/null', opts);
4293
+ targetBranch = 'develop';
4294
+ } catch {
4295
+ // No develop branch at all — use main as last resort
4296
+ targetBranch = 'main';
4297
+ }
4298
+ }
4299
+
4300
+ // Generate MR/PR URL based on provider
4301
+ const cleanUrl = remoteUrl
4302
+ .replace(/\.git$/, '')
4303
+ .replace(/^git@github\.com:/, 'https://github.com/')
4304
+ .replace(/^git@gitlab\.com:/, 'https://gitlab.com/')
4305
+ .replace(/^git@bitbucket\.org:/, 'https://bitbucket.org/');
4306
+
4307
+ if (cleanUrl.includes('github.com')) {
4308
+ mrUrl = `${cleanUrl}/compare/${targetBranch}...${finalBranchName}?expand=1`;
4309
+ } else if (cleanUrl.includes('gitlab.com') || cleanUrl.includes('gitlab')) {
4310
+ mrUrl = `${cleanUrl}/-/merge_requests/new?merge_request[source_branch]=${finalBranchName}&merge_request[target_branch]=${targetBranch}`;
4311
+ } else if (cleanUrl.includes('bitbucket.org') || cleanUrl.includes('bitbucket')) {
4312
+ mrUrl = `${cleanUrl}/pull-requests/new?source=${finalBranchName}&dest=${targetBranch}`;
4313
+ } else {
4314
+ // Generic — try GitHub-style URL
4315
+ mrUrl = `${cleanUrl}/compare/${targetBranch}...${finalBranchName}?expand=1`;
4316
+ }
4317
+ } catch (e) {
4318
+ console.warn('Could not detect remote URL:', e.message);
4319
+ }
4320
+
4321
+ console.log(`✅ Fix branch created: ${finalBranchName} (${commitSha.substring(0, 8)}) pushed=${pushed}`);
4322
+ if (mrUrl) console.log(`🔗 MR URL: ${mrUrl}`);
4323
+
4324
+ // 10. Switch back to original branch and merge fix to keep changes on disk
4325
+ try {
4326
+ execSync(`git checkout ${currentBranch}`, opts);
4327
+ // Merge the fix branch so the working directory has the patched files
4328
+ // This keeps the diff viewer working (disk has patched version)
4329
+ try {
4330
+ execSync(`git merge ${finalBranchName} --no-edit`, opts);
4331
+ console.log(`✅ Merged ${finalBranchName} into ${currentBranch}`);
4332
+ } catch (mergeErr) {
4333
+ // If merge fails (conflicts), just cherry-pick the changes without committing
4334
+ try {
4335
+ execSync(`git merge --abort`, opts);
4336
+ } catch {}
4337
+ // Alternative: checkout the patched files from the fix branch
4338
+ try {
4339
+ const changedFiles = status.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean);
4340
+ for (const f of changedFiles) {
4341
+ try {
4342
+ execSync(`git checkout ${finalBranchName} -- ${f}`, opts);
4343
+ } catch {}
4344
+ }
4345
+ console.log(`✅ Cherry-picked ${changedFiles.length} file(s) from ${finalBranchName}`);
4346
+ } catch {}
4347
+ }
4348
+ } catch {}
4349
+
4350
+ // 11. Auto-create Pull Request if pushed and token available
4351
+ let prUrl = '';
4352
+ if (pushed && gitToken && remoteUrl && remoteUrl.includes('github.com')) {
4353
+ try {
4354
+ const repoPath = remoteUrl
4355
+ .replace(/^https?:\/\/(.*@)?github\.com\//, '')
4356
+ .replace(/^git@github\.com:/, '')
4357
+ .replace(/\.git$/, '');
4358
+
4359
+ const prBody = {
4360
+ title: commitMessage,
4361
+ head: finalBranchName,
4362
+ base: targetBranch,
4363
+ body: `## 🤖 Auto-fix by DeepDebug AI\n\n` +
4364
+ `**Branch:** \`${finalBranchName}\`\n` +
4365
+ `**Files changed:** ${status.split('\n').length}\n` +
4366
+ `**Commit:** ${commitSha.substring(0, 8)}\n\n` +
4367
+ `This pull request was automatically created by DeepDebug AI after detecting and fixing a production error.\n\n` +
4368
+ `### Changes\n` +
4369
+ status.split('\n').map(l => `- \`${l.trim()}\``).join('\n') + '\n\n' +
4370
+ `---\n*Generated by [DeepDebug AI](https://deepdebug.ai) — Autonomous Debugging Platform*`
4371
+ };
4372
+
4373
+ const response = await fetch(`https://api.github.com/repos/${repoPath}/pulls`, {
4374
+ method: 'POST',
4375
+ headers: {
4376
+ 'Authorization': `Bearer ${gitToken}`,
4377
+ 'Accept': 'application/vnd.github+json',
4378
+ 'Content-Type': 'application/json',
4379
+ 'X-GitHub-Api-Version': '2022-11-28'
4380
+ },
4381
+ body: JSON.stringify(prBody)
4382
+ });
4383
+
4384
+ if (response.ok) {
4385
+ const prData = await response.json();
4386
+ prUrl = prData.html_url;
4387
+ console.log(`✅ Pull Request created: ${prUrl}`);
4388
+ } else {
4389
+ const errText = await response.text();
4390
+ console.warn(`⚠️ PR creation failed (${response.status}): ${errText.substring(0, 200)}`);
4391
+ // Still use the compare URL as fallback
4392
+ prUrl = mrUrl;
4393
+ }
4394
+ } catch (prErr) {
4395
+ console.warn(`⚠️ PR creation error: ${prErr.message}`);
4396
+ prUrl = mrUrl;
4397
+ }
4398
+ }
4399
+
4400
+ res.json({
4401
+ ok: true,
4402
+ branch: finalBranchName,
4403
+ previousBranch: currentBranch,
4404
+ targetBranch,
4405
+ commitSha,
4406
+ commitMessage,
4407
+ pushed,
4408
+ pushResult: pushResult.toString().trim(),
4409
+ remoteUrl,
4410
+ mrUrl: prUrl || (pushed ? mrUrl : ''),
4411
+ prCreated: !!prUrl && prUrl !== mrUrl,
4412
+ filesChanged: status.split('\n').length,
4413
+ changedFiles: status.split('\n').map(l => l.trim().split(/\s+/).pop())
4414
+ });
4415
+
4416
+ } catch (err) {
4417
+ console.error(`❌ Git branch creation failed: ${err.message}`);
4418
+ res.status(500).json({ ok: false, error: err.message });
4419
+ }
4420
+ });
4421
+
4422
+ /**
4423
+ * GET /workspace/git/status
4424
+ * Returns current git status (branch, changes, remote)
4425
+ */
4426
+ app.get("/workspace/git/status", async (req, res) => {
4427
+ if (!WORKSPACE_ROOT) {
4428
+ return res.status(400).json({ error: "workspace not set" });
4429
+ }
4430
+
4431
+ try {
4432
+ const { execSync } = await import('child_process');
4433
+ const opts = { cwd: WORKSPACE_ROOT, encoding: 'utf8', timeout: 10000 };
4434
+
4435
+ let branch = '', remoteUrl = '', status = '';
4436
+ let isGitRepo = false;
4437
+
4438
+ try {
4439
+ execSync('git rev-parse --is-inside-work-tree', opts);
4440
+ isGitRepo = true;
4441
+ branch = execSync('git branch --show-current', opts).trim();
4442
+ status = execSync('git status --porcelain', opts).trim();
4443
+ try { remoteUrl = execSync('git remote get-url origin', opts).trim(); } catch {}
4444
+ } catch {}
4445
+
4446
+ res.json({
4447
+ ok: true,
4448
+ isGitRepo,
4449
+ branch,
4450
+ hasChanges: status.length > 0,
4451
+ changedFiles: status ? status.split('\n').filter(Boolean).length : 0,
4452
+ remoteUrl
4453
+ });
4454
+ } catch (err) {
4455
+ res.json({ ok: false, isGitRepo: false, error: err.message });
4456
+ }
4457
+ });
4458
+
4459
+ // ============================================
4460
+ // 📂 MULTI-WORKSPACE ENDPOINTS
4461
+ // ============================================
4462
+
4463
+ /** Lista todos os workspaces abertos */
4464
+ app.get("/workspaces", (_req, res) => {
4465
+ res.json({
4466
+ workspaces: wsManager.list(),
4467
+ total: wsManager.count,
4468
+ defaultWorkspaceId: wsManager.defaultWorkspaceId
4469
+ });
4470
+ });
4471
+
4472
+ /** Fecha um workspace */
4473
+ app.post("/workspace/close", (req, res) => {
4474
+ const { workspaceId } = req.body || {};
4475
+ if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
4476
+ const closed = wsManager.close(workspaceId);
4477
+ if (closed && wsManager.defaultRoot) WORKSPACE_ROOT = wsManager.defaultRoot;
4478
+ res.json({ ok: closed, remainingWorkspaces: wsManager.count });
4479
+ });
4480
+
4481
+ /** Abre workspace por ID */
4482
+ app.post("/workspace/:workspaceId/open", async (req, res) => {
4483
+ const { workspaceId } = req.params;
4484
+ const { root } = req.body || {};
4485
+ if (!root) return res.status(400).json({ error: "root is required" });
4486
+ const abs = path.resolve(root);
4487
+ if (!(await exists(abs))) return res.status(404).json({ error: "path not found" });
4488
+ try {
4489
+ const ws = await wsManager.open(workspaceId, abs);
4490
+ if (wsManager.count === 1) WORKSPACE_ROOT = abs;
4491
+ res.json({ ok: true, workspace: ws });
4492
+ } catch (err) {
4493
+ res.status(400).json({ error: err.message });
4494
+ }
4495
+ });
4496
+
4497
+ /** Aplica patch num workspace específico */
4498
+ app.post("/workspace/:workspaceId/patch", async (req, res) => {
4499
+ const { workspaceId } = req.params;
4500
+ const { diff } = req.body || {};
4501
+ if (!diff) return res.status(400).json({ error: "diff is required" });
4502
+ try {
4503
+ const root = wsManager.resolveRoot(workspaceId);
4504
+ const out = await applyUnifiedDiff(root, diff);
4505
+ res.json({ ok: true, workspaceId, ...out });
4506
+ } catch (e) {
4507
+ res.status(400).json({ error: "patch failed", details: String(e) });
4508
+ }
4509
+ });
4510
+
4511
+ /** Compila e testa num workspace específico */
4512
+ app.post("/workspace/:workspaceId/test", async (req, res) => {
4513
+ const { workspaceId } = req.params;
4514
+ try {
4515
+ const root = wsManager.resolveRoot(workspaceId);
4516
+ const meta = await detectProject(root);
4517
+ const result = await compileAndTest({ language: meta.language, buildTool: meta.buildTool, cwd: root });
4518
+ res.json({ root, workspaceId, meta, result });
4519
+ } catch (e) {
4520
+ res.status(400).json({ error: String(e) });
4521
+ }
4522
+ });
4523
+
4524
+ /** Executa comando num workspace específico */
4525
+ app.post("/workspace/:workspaceId/run", async (req, res) => {
4526
+ const { workspaceId } = req.params;
4527
+ const { cmd, args = [] } = req.body || {};
4528
+ if (!cmd) return res.status(400).json({ error: "cmd is required" });
4529
+ try {
4530
+ const root = wsManager.resolveRoot(workspaceId);
4531
+ const out = await run(cmd, args, root, 5 * 60 * 1000);
4532
+ res.json({ workspaceId, ...out });
4533
+ } catch (e) {
4534
+ res.status(400).json({ error: String(e) });
4535
+ }
4536
+ });
4537
+
4538
+ // ============================================
4539
+ // START SERVER
4540
+ // ============================================
4541
+ app.listen(PORT, '0.0.0.0', () => {
4542
+ console.log(`\n🔌 DeepDebug Local Agent listening on port ${PORT}`);
4543
+ console.log(`📦 Environment: ${process.env.NODE_ENV || 'development'}`);
4544
+ console.log(`📦 Process Manager initialized`);
4545
+ console.log(`💾 Backup system ready (max: ${MAX_BACKUPS} backups)`);
4546
+ console.log(`🧠 AI Vibe Coding Engine: ${aiEngine?.isActive ? 'ACTIVE' : 'DISABLED'}`);
4547
+ if (aiEngine) {
4548
+ console.log(` Gateway URL: ${aiEngine.gatewayUrl}`);
4549
+ console.log(` Max Retries: ${aiEngine.maxRetries}`);
4550
+ }
4551
+ console.log(`\n🚀 Ready to receive requests!\n`);
4552
+ });
4553
+
4554
+ // Start MCP HTTP Bridge em paralelo (port 5056)
4555
+ try {
4556
+ startMCPHttpServer(wsManager, parseInt(MCP_PORT));
4557
+ } catch (err) {
4558
+ console.warn(`⚠️ MCP HTTP Server failed to start: ${err.message}`);
4559
+ console.warn(` (MCP features disabled, REST API continues normally)`);
4560
+ }
4561
+
4562
+ // Auto-register default workspace in WorkspaceManager
4563
+ if (WORKSPACE_ROOT) {
4564
+ wsManager.open("default", WORKSPACE_ROOT).catch(() => {});
4565
+ }