deepdebug-local-agent 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +24 -0
- package/.idea/deepdebug-local-agent.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/Dockerfile +46 -0
- package/cloudbuild.yaml +42 -0
- package/index.js +42 -0
- package/mcp-server.js +533 -0
- package/package.json +22 -0
- package/src/ai-engine.js +861 -0
- package/src/analyzers/config-analyzer.js +446 -0
- package/src/analyzers/controller-analyzer.js +429 -0
- package/src/analyzers/dto-analyzer.js +455 -0
- package/src/detectors/build-tool-detector.js +0 -0
- package/src/detectors/framework-detector.js +91 -0
- package/src/detectors/language-detector.js +89 -0
- package/src/detectors/multi-project-detector.js +191 -0
- package/src/detectors/service-detector.js +244 -0
- package/src/detectors.js +30 -0
- package/src/exec-utils.js +215 -0
- package/src/fs-utils.js +34 -0
- package/src/git/base-git-provider.js +384 -0
- package/src/git/git-provider-registry.js +110 -0
- package/src/git/github-provider.js +502 -0
- package/src/mcp-http-server.js +313 -0
- package/src/patch/patch-engine.js +339 -0
- package/src/patch-manager.js +816 -0
- package/src/patch.js +607 -0
- package/src/patch_bkp.js +154 -0
- package/src/ports.js +69 -0
- package/src/routes/workspace.route.js +528 -0
- package/src/runtimes/base-runtime.js +290 -0
- package/src/runtimes/java/gradle-runtime.js +378 -0
- package/src/runtimes/java/java-integrations.js +339 -0
- package/src/runtimes/java/maven-runtime.js +418 -0
- package/src/runtimes/node/node-integrations.js +247 -0
- package/src/runtimes/node/npm-runtime.js +466 -0
- package/src/runtimes/node/yarn-runtime.js +354 -0
- package/src/runtimes/runtime-registry.js +256 -0
- package/src/server-local.js +576 -0
- package/src/server.js +4565 -0
- package/src/utils/environment-diagnostics.js +666 -0
- package/src/utils/exec-utils.js +264 -0
- package/src/utils/fs-utils.js +218 -0
- package/src/workspace/detect-port.js +176 -0
- package/src/workspace/file-reader.js +54 -0
- package/src/workspace/git-client.js +0 -0
- package/src/workspace/process-manager.js +619 -0
- package/src/workspace/scanner.js +72 -0
- package/src/workspace-manager.js +172 -0
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
|
+
}
|