deepdebug-local-agent 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +24 -0
- package/.idea/deepdebug-local-agent.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/Dockerfile +46 -0
- package/cloudbuild.yaml +42 -0
- package/index.js +42 -0
- package/mcp-server.js +533 -0
- package/package.json +22 -0
- package/src/ai-engine.js +861 -0
- package/src/analyzers/config-analyzer.js +446 -0
- package/src/analyzers/controller-analyzer.js +429 -0
- package/src/analyzers/dto-analyzer.js +455 -0
- package/src/detectors/build-tool-detector.js +0 -0
- package/src/detectors/framework-detector.js +91 -0
- package/src/detectors/language-detector.js +89 -0
- package/src/detectors/multi-project-detector.js +191 -0
- package/src/detectors/service-detector.js +244 -0
- package/src/detectors.js +30 -0
- package/src/exec-utils.js +215 -0
- package/src/fs-utils.js +34 -0
- package/src/git/base-git-provider.js +384 -0
- package/src/git/git-provider-registry.js +110 -0
- package/src/git/github-provider.js +502 -0
- package/src/mcp-http-server.js +313 -0
- package/src/patch/patch-engine.js +339 -0
- package/src/patch-manager.js +816 -0
- package/src/patch.js +607 -0
- package/src/patch_bkp.js +154 -0
- package/src/ports.js +69 -0
- package/src/routes/workspace.route.js +528 -0
- package/src/runtimes/base-runtime.js +290 -0
- package/src/runtimes/java/gradle-runtime.js +378 -0
- package/src/runtimes/java/java-integrations.js +339 -0
- package/src/runtimes/java/maven-runtime.js +418 -0
- package/src/runtimes/node/node-integrations.js +247 -0
- package/src/runtimes/node/npm-runtime.js +466 -0
- package/src/runtimes/node/yarn-runtime.js +354 -0
- package/src/runtimes/runtime-registry.js +256 -0
- package/src/server-local.js +576 -0
- package/src/server.js +4565 -0
- package/src/utils/environment-diagnostics.js +666 -0
- package/src/utils/exec-utils.js +264 -0
- package/src/utils/fs-utils.js +218 -0
- package/src/workspace/detect-port.js +176 -0
- package/src/workspace/file-reader.js +54 -0
- package/src/workspace/git-client.js +0 -0
- package/src/workspace/process-manager.js +619 -0
- package/src/workspace/scanner.js +72 -0
- package/src/workspace-manager.js +172 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { spawn, exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import stripAnsi from 'strip-ansi';
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Executa um comando e retorna o resultado
|
|
9
|
+
*
|
|
10
|
+
* @param {string} cmd - Comando a executar
|
|
11
|
+
* @param {string[]} args - Argumentos
|
|
12
|
+
* @param {string} cwd - Diretório de trabalho
|
|
13
|
+
* @param {number} timeoutMs - Timeout em ms (default: 10 min)
|
|
14
|
+
* @returns {Promise<CommandResult>}
|
|
15
|
+
*/
|
|
16
|
+
export function run(cmd, args = [], cwd = process.cwd(), timeoutMs = 10 * 60 * 1000) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
|
|
20
|
+
const child = spawn(cmd, args, {
|
|
21
|
+
cwd,
|
|
22
|
+
shell: true,
|
|
23
|
+
env: { ...process.env }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let stdout = '';
|
|
27
|
+
let stderr = '';
|
|
28
|
+
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
try {
|
|
31
|
+
child.kill('SIGKILL');
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Process already terminated
|
|
34
|
+
}
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
|
|
37
|
+
child.stdout.on('data', (data) => {
|
|
38
|
+
stdout += data.toString();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
child.stderr.on('data', (data) => {
|
|
42
|
+
stderr += data.toString();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on('error', (err) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve({
|
|
48
|
+
code: 1,
|
|
49
|
+
stdout: stripAnsi(stdout),
|
|
50
|
+
stderr: stripAnsi(err.message),
|
|
51
|
+
duration: Date.now() - startTime
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
child.on('close', (code) => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve({
|
|
58
|
+
code: code || 0,
|
|
59
|
+
stdout: stripAnsi(stdout),
|
|
60
|
+
stderr: stripAnsi(stderr),
|
|
61
|
+
duration: Date.now() - startTime
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Executa comando com streaming de output
|
|
69
|
+
*
|
|
70
|
+
* @param {string} cmd - Comando
|
|
71
|
+
* @param {string[]} args - Argumentos
|
|
72
|
+
* @param {string} cwd - Diretório de trabalho
|
|
73
|
+
* @param {object} callbacks - { onStdout, onStderr, onClose }
|
|
74
|
+
* @returns {ChildProcess}
|
|
75
|
+
*/
|
|
76
|
+
export function runStreaming(cmd, args = [], cwd = process.cwd(), callbacks = {}) {
|
|
77
|
+
const { onStdout, onStderr, onClose, onError } = callbacks;
|
|
78
|
+
|
|
79
|
+
const child = spawn(cmd, args, {
|
|
80
|
+
cwd,
|
|
81
|
+
shell: true,
|
|
82
|
+
env: { ...process.env }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (onStdout) {
|
|
86
|
+
child.stdout.on('data', (data) => {
|
|
87
|
+
onStdout(stripAnsi(data.toString()));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (onStderr) {
|
|
92
|
+
child.stderr.on('data', (data) => {
|
|
93
|
+
onStderr(stripAnsi(data.toString()));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (onError) {
|
|
98
|
+
child.on('error', onError);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (onClose) {
|
|
102
|
+
child.on('close', onClose);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return child;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Executa comando simples e retorna output
|
|
110
|
+
*
|
|
111
|
+
* @param {string} command - Comando completo
|
|
112
|
+
* @param {string} cwd - Diretório de trabalho
|
|
113
|
+
* @returns {Promise<string>}
|
|
114
|
+
*/
|
|
115
|
+
export async function execSimple(command, cwd = process.cwd()) {
|
|
116
|
+
try {
|
|
117
|
+
const { stdout } = await execAsync(command, { cwd });
|
|
118
|
+
return stripAnsi(stdout.trim());
|
|
119
|
+
} catch (e) {
|
|
120
|
+
throw new Error(e.stderr || e.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Verifica se um comando está disponível no sistema
|
|
126
|
+
*
|
|
127
|
+
* @param {string} cmd - Nome do comando
|
|
128
|
+
* @returns {Promise<boolean>}
|
|
129
|
+
*/
|
|
130
|
+
export async function commandExists(cmd) {
|
|
131
|
+
try {
|
|
132
|
+
const checkCmd = process.platform === 'win32'
|
|
133
|
+
? `where ${cmd}`
|
|
134
|
+
: `which ${cmd}`;
|
|
135
|
+
await execAsync(checkCmd);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Executa compile e test para um projeto
|
|
144
|
+
*
|
|
145
|
+
* @param {object} options - { language, buildTool, cwd }
|
|
146
|
+
* @returns {Promise<CompileTestResult>}
|
|
147
|
+
*/
|
|
148
|
+
export async function compileAndTest({ language, buildTool, cwd }) {
|
|
149
|
+
const steps = [];
|
|
150
|
+
|
|
151
|
+
// Java + Maven
|
|
152
|
+
if (language === 'java' && buildTool === 'maven') {
|
|
153
|
+
const clean = await run('mvn', ['clean', '-q'], cwd);
|
|
154
|
+
steps.push({ name: 'clean', ...clean });
|
|
155
|
+
|
|
156
|
+
const test = await run('mvn', ['test', '-q'], cwd);
|
|
157
|
+
steps.push({ name: 'test', ...test });
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
success: test.code === 0,
|
|
161
|
+
steps
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Java + Gradle
|
|
166
|
+
if (language === 'java' && buildTool === 'gradle') {
|
|
167
|
+
const cmd = await commandExists('./gradlew') ? './gradlew' : 'gradle';
|
|
168
|
+
|
|
169
|
+
const clean = await run(cmd, ['clean'], cwd);
|
|
170
|
+
steps.push({ name: 'clean', ...clean });
|
|
171
|
+
|
|
172
|
+
const test = await run(cmd, ['test'], cwd);
|
|
173
|
+
steps.push({ name: 'test', ...test });
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
success: test.code === 0,
|
|
177
|
+
steps
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Node.js
|
|
182
|
+
if (language === 'node') {
|
|
183
|
+
const install = await run('npm', ['install', '--silent'], cwd);
|
|
184
|
+
steps.push({ name: 'install', ...install });
|
|
185
|
+
|
|
186
|
+
const test = await run('npm', ['test', '--', '--passWithNoTests'], cwd);
|
|
187
|
+
steps.push({ name: 'test', ...test });
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
success: test.code === 0,
|
|
191
|
+
steps
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Python
|
|
196
|
+
if (language === 'python') {
|
|
197
|
+
const test = await run('pytest', [], cwd);
|
|
198
|
+
steps.push({ name: 'test', ...test });
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: test.code === 0,
|
|
202
|
+
steps
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Go
|
|
207
|
+
if (language === 'go') {
|
|
208
|
+
const test = await run('go', ['test', './...'], cwd);
|
|
209
|
+
steps.push({ name: 'test', ...test });
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: test.code === 0,
|
|
213
|
+
steps
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// .NET
|
|
218
|
+
if (language === 'dotnet' || language === '.net') {
|
|
219
|
+
const build = await run('dotnet', ['build'], cwd);
|
|
220
|
+
steps.push({ name: 'build', ...build });
|
|
221
|
+
|
|
222
|
+
const test = await run('dotnet', ['test'], cwd);
|
|
223
|
+
steps.push({ name: 'test', ...test });
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
success: test.code === 0,
|
|
227
|
+
steps
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Unsupported
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
steps: [{
|
|
235
|
+
name: 'error',
|
|
236
|
+
code: 1,
|
|
237
|
+
stdout: '',
|
|
238
|
+
stderr: `Unsupported language/build tool: ${language}/${buildTool}`,
|
|
239
|
+
duration: 0
|
|
240
|
+
}]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @typedef {object} CommandResult
|
|
246
|
+
* @property {number} code - Exit code
|
|
247
|
+
* @property {string} stdout - Standard output
|
|
248
|
+
* @property {string} stderr - Standard error
|
|
249
|
+
* @property {number} duration - Duration in ms
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @typedef {object} CompileTestResult
|
|
254
|
+
* @property {boolean} success - Whether all steps passed
|
|
255
|
+
* @property {CommandResult[]} steps - Results of each step
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
export default {
|
|
259
|
+
run,
|
|
260
|
+
runStreaming,
|
|
261
|
+
execSimple,
|
|
262
|
+
commandExists,
|
|
263
|
+
compileAndTest
|
|
264
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
|
|
5
|
+
// Promisified functions
|
|
6
|
+
export const stat = promisify(fs.stat);
|
|
7
|
+
export const readdir = promisify(fs.readdir);
|
|
8
|
+
export const readFile = promisify(fs.readFile);
|
|
9
|
+
export const writeFile = promisify(fs.writeFile);
|
|
10
|
+
export const mkdir = promisify(fs.mkdir);
|
|
11
|
+
export const unlink = promisify(fs.unlink);
|
|
12
|
+
export const rmdir = promisify(fs.rmdir);
|
|
13
|
+
export const copyFile = promisify(fs.copyFile);
|
|
14
|
+
export const access = promisify(fs.access);
|
|
15
|
+
export const rename = promisify(fs.rename);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Verifica se um arquivo/diretório existe
|
|
19
|
+
*/
|
|
20
|
+
export async function exists(p) {
|
|
21
|
+
try {
|
|
22
|
+
await access(p, fs.constants.F_OK);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Cria diretório recursivamente se não existir
|
|
31
|
+
*/
|
|
32
|
+
export async function ensureDir(dir) {
|
|
33
|
+
if (!(await exists(dir))) {
|
|
34
|
+
await mkdir(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lista arquivos recursivamente
|
|
40
|
+
*/
|
|
41
|
+
export async function listRecursive(root, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
maxFiles = 5000,
|
|
44
|
+
includeHidden = false,
|
|
45
|
+
extensions = null,
|
|
46
|
+
ignore = ['node_modules', '.git', 'target', 'build', 'dist', '__pycache__', '.idea', '.vscode']
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const results = [];
|
|
50
|
+
|
|
51
|
+
async function walk(dir) {
|
|
52
|
+
if (results.length >= maxFiles) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (results.length >= maxFiles) break;
|
|
59
|
+
|
|
60
|
+
// Skip hidden files
|
|
61
|
+
if (!includeHidden && entry.name.startsWith('.')) continue;
|
|
62
|
+
|
|
63
|
+
// Skip ignored directories
|
|
64
|
+
if (ignore.includes(entry.name)) continue;
|
|
65
|
+
|
|
66
|
+
const fullPath = path.join(dir, entry.name);
|
|
67
|
+
const relativePath = path.relative(root, fullPath);
|
|
68
|
+
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
results.push({ type: 'dir', path: relativePath });
|
|
71
|
+
await walk(fullPath);
|
|
72
|
+
} else if (entry.isFile()) {
|
|
73
|
+
// Filter by extension if specified
|
|
74
|
+
if (extensions) {
|
|
75
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
76
|
+
if (!extensions.includes(ext)) continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
results.push({ type: 'file', path: relativePath });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Permission denied or other error
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await walk(root);
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Copia diretório recursivamente
|
|
93
|
+
*/
|
|
94
|
+
export async function copyDir(src, dest) {
|
|
95
|
+
await ensureDir(dest);
|
|
96
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const srcPath = path.join(src, entry.name);
|
|
100
|
+
const destPath = path.join(dest, entry.name);
|
|
101
|
+
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
await copyDir(srcPath, destPath);
|
|
104
|
+
} else {
|
|
105
|
+
await copyFile(srcPath, destPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove diretório recursivamente
|
|
112
|
+
*/
|
|
113
|
+
export async function removeDir(dir) {
|
|
114
|
+
if (!(await exists(dir))) return;
|
|
115
|
+
|
|
116
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
117
|
+
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const fullPath = path.join(dir, entry.name);
|
|
120
|
+
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
await removeDir(fullPath);
|
|
123
|
+
} else {
|
|
124
|
+
await unlink(fullPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await rmdir(dir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Calcula tamanho total de um diretório
|
|
133
|
+
*/
|
|
134
|
+
export async function dirSize(dir) {
|
|
135
|
+
let totalSize = 0;
|
|
136
|
+
|
|
137
|
+
async function walk(d) {
|
|
138
|
+
try {
|
|
139
|
+
const entries = await readdir(d, { withFileTypes: true });
|
|
140
|
+
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const fullPath = path.join(d, entry.name);
|
|
143
|
+
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
await walk(fullPath);
|
|
146
|
+
} else {
|
|
147
|
+
const stats = await stat(fullPath);
|
|
148
|
+
totalSize += stats.size;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// Skip inaccessible directories
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await walk(dir);
|
|
157
|
+
return totalSize;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Lê arquivo JSON
|
|
162
|
+
*/
|
|
163
|
+
export async function readJson(filePath) {
|
|
164
|
+
const content = await readFile(filePath, 'utf8');
|
|
165
|
+
return JSON.parse(content);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Escreve arquivo JSON
|
|
170
|
+
*/
|
|
171
|
+
export async function writeJson(filePath, data, pretty = true) {
|
|
172
|
+
const content = pretty
|
|
173
|
+
? JSON.stringify(data, null, 2)
|
|
174
|
+
: JSON.stringify(data);
|
|
175
|
+
await writeFile(filePath, content, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Obtém extensão do arquivo normalizada
|
|
180
|
+
*/
|
|
181
|
+
export function getExtension(filePath) {
|
|
182
|
+
return path.extname(filePath).toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Verifica se é arquivo de código
|
|
187
|
+
*/
|
|
188
|
+
export function isCodeFile(filePath) {
|
|
189
|
+
const codeExtensions = [
|
|
190
|
+
'.java', '.js', '.ts', '.jsx', '.tsx', '.py', '.cs', '.go',
|
|
191
|
+
'.rb', '.php', '.swift', '.kt', '.scala', '.rs', '.cpp', '.c',
|
|
192
|
+
'.h', '.hpp', '.vue', '.svelte'
|
|
193
|
+
];
|
|
194
|
+
return codeExtensions.includes(getExtension(filePath));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default {
|
|
198
|
+
stat,
|
|
199
|
+
readdir,
|
|
200
|
+
readFile,
|
|
201
|
+
writeFile,
|
|
202
|
+
mkdir,
|
|
203
|
+
unlink,
|
|
204
|
+
rmdir,
|
|
205
|
+
copyFile,
|
|
206
|
+
access,
|
|
207
|
+
rename,
|
|
208
|
+
exists,
|
|
209
|
+
ensureDir,
|
|
210
|
+
listRecursive,
|
|
211
|
+
copyDir,
|
|
212
|
+
removeDir,
|
|
213
|
+
dirSize,
|
|
214
|
+
readJson,
|
|
215
|
+
writeJson,
|
|
216
|
+
getExtension,
|
|
217
|
+
isCodeFile
|
|
218
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Adicionar no server.js do Local Agent
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /workspace/detect-port
|
|
5
|
+
*
|
|
6
|
+
* Detecta porta de um serviço específico lendo arquivos de configuração
|
|
7
|
+
*
|
|
8
|
+
* Query params:
|
|
9
|
+
* - servicePath: caminho relativo do serviço
|
|
10
|
+
* - language: linguagem do serviço
|
|
11
|
+
* - framework: framework do serviço
|
|
12
|
+
*/
|
|
13
|
+
app.get("/workspace/detect-port", async (req, res) => {
|
|
14
|
+
if (!WORKSPACE_ROOT) {
|
|
15
|
+
return res.status(400).json({
|
|
16
|
+
error: "workspace not set",
|
|
17
|
+
hint: "call POST /workspace/open first"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { servicePath, language, framework } = req.query;
|
|
22
|
+
|
|
23
|
+
if (!servicePath) {
|
|
24
|
+
return res.status(400).json({ error: "servicePath is required" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const fullPath = path.join(WORKSPACE_ROOT, servicePath);
|
|
29
|
+
|
|
30
|
+
console.log(`🔍 Detecting port for service at: ${fullPath}`);
|
|
31
|
+
|
|
32
|
+
let port = null;
|
|
33
|
+
|
|
34
|
+
// Detectar porta baseado na linguagem/framework
|
|
35
|
+
if (language === 'java' && framework === 'spring-boot') {
|
|
36
|
+
port = await detectSpringBootPort(fullPath);
|
|
37
|
+
} else if (language === 'node') {
|
|
38
|
+
port = await detectNodePort(fullPath);
|
|
39
|
+
} else if (language === 'python') {
|
|
40
|
+
port = await detectPythonPort(fullPath);
|
|
41
|
+
} else if (language === 'dotnet') {
|
|
42
|
+
port = await detectDotNetPort(fullPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`✅ Detected port: ${port || 'default'}`);
|
|
46
|
+
|
|
47
|
+
res.json({ port });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('❌ Error detecting port:', err);
|
|
50
|
+
res.status(500).json({ error: err.message });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detecta porta do Spring Boot
|
|
56
|
+
*/
|
|
57
|
+
async function detectSpringBootPort(servicePath) {
|
|
58
|
+
const candidates = [
|
|
59
|
+
path.join(servicePath, 'src/main/resources/application.yml'),
|
|
60
|
+
path.join(servicePath, 'src/main/resources/application.yaml'),
|
|
61
|
+
path.join(servicePath, 'src/main/resources/application.properties'),
|
|
62
|
+
path.join(servicePath, 'application.yml'),
|
|
63
|
+
path.join(servicePath, 'application.yaml'),
|
|
64
|
+
path.join(servicePath, 'application.properties')
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
for (const filePath of candidates) {
|
|
68
|
+
if (await exists(filePath)) {
|
|
69
|
+
console.log(`📄 Reading config: ${filePath}`);
|
|
70
|
+
const content = await readFile(filePath, 'utf8');
|
|
71
|
+
|
|
72
|
+
// YAML
|
|
73
|
+
if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
|
|
74
|
+
const match = content.match(/server:\s+port:\s*(\d+)/);
|
|
75
|
+
if (match) {
|
|
76
|
+
console.log(`✓ Found port in YAML: ${match[1]}`);
|
|
77
|
+
return parseInt(match[1]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Properties
|
|
82
|
+
if (filePath.endsWith('.properties')) {
|
|
83
|
+
const match = content.match(/server\.port\s*=\s*(\d+)/);
|
|
84
|
+
if (match) {
|
|
85
|
+
console.log(`✓ Found port in Properties: ${match[1]}`);
|
|
86
|
+
return parseInt(match[1]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('⚠️ No port config found, using default: 8080');
|
|
93
|
+
return 8080; // Default Spring Boot
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detecta porta do Node.js
|
|
98
|
+
*/
|
|
99
|
+
async function detectNodePort(servicePath) {
|
|
100
|
+
// Tentar .env
|
|
101
|
+
const envPath = path.join(servicePath, '.env');
|
|
102
|
+
if (await exists(envPath)) {
|
|
103
|
+
const content = await readFile(envPath, 'utf8');
|
|
104
|
+
const match = content.match(/PORT\s*=\s*(\d+)/i);
|
|
105
|
+
if (match) {
|
|
106
|
+
console.log(`✓ Found port in .env: ${match[1]}`);
|
|
107
|
+
return parseInt(match[1]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Tentar package.json scripts
|
|
112
|
+
const pkgPath = path.join(servicePath, 'package.json');
|
|
113
|
+
if (await exists(pkgPath)) {
|
|
114
|
+
const content = await readFile(pkgPath, 'utf8');
|
|
115
|
+
const pkg = JSON.parse(content);
|
|
116
|
+
|
|
117
|
+
// Procurar por --port ou PORT= nos scripts
|
|
118
|
+
const scripts = JSON.stringify(pkg.scripts || {});
|
|
119
|
+
const match = scripts.match(/--port[=\s]+(\d+)|PORT[=\s]+(\d+)/i);
|
|
120
|
+
if (match) {
|
|
121
|
+
const port = parseInt(match[1] || match[2]);
|
|
122
|
+
console.log(`✓ Found port in package.json: ${port}`);
|
|
123
|
+
return port;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('⚠️ No port config found, using default: 3000');
|
|
128
|
+
return 3000; // Default Node
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detecta porta do Python
|
|
133
|
+
*/
|
|
134
|
+
async function detectPythonPort(servicePath) {
|
|
135
|
+
// Tentar .env
|
|
136
|
+
const envPath = path.join(servicePath, '.env');
|
|
137
|
+
if (await exists(envPath)) {
|
|
138
|
+
const content = await readFile(envPath, 'utf8');
|
|
139
|
+
const match = content.match(/PORT\s*=\s*(\d+)/i);
|
|
140
|
+
if (match) {
|
|
141
|
+
console.log(`✓ Found port in .env: ${match[1]}`);
|
|
142
|
+
return parseInt(match[1]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Defaults por framework
|
|
147
|
+
console.log('⚠️ No port config found, using framework default');
|
|
148
|
+
return 8000; // Default Django/FastAPI
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Detecta porta do .NET
|
|
153
|
+
*/
|
|
154
|
+
async function detectDotNetPort(servicePath) {
|
|
155
|
+
const launchSettings = path.join(servicePath, 'Properties/launchSettings.json');
|
|
156
|
+
|
|
157
|
+
if (await exists(launchSettings)) {
|
|
158
|
+
const content = await readFile(launchSettings, 'utf8');
|
|
159
|
+
const settings = JSON.parse(content);
|
|
160
|
+
|
|
161
|
+
// Procurar em profiles
|
|
162
|
+
const profiles = settings.profiles || {};
|
|
163
|
+
for (const profile of Object.values(profiles)) {
|
|
164
|
+
if (profile.applicationUrl) {
|
|
165
|
+
const match = profile.applicationUrl.match(/:(\d+)/);
|
|
166
|
+
if (match) {
|
|
167
|
+
console.log(`✓ Found port in launchSettings: ${match[1]}`);
|
|
168
|
+
return parseInt(match[1]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('⚠️ No port config found, using default: 5000');
|
|
175
|
+
return 5000; // Default .NET
|
|
176
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export class FileReader {
|
|
5
|
+
constructor(rootPath) {
|
|
6
|
+
this.rootPath = rootPath;
|
|
7
|
+
this.ignorePatterns = ['node_modules', 'target', '.git', 'build', 'dist'];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async read(relativePath) {
|
|
11
|
+
const fullPath = path.join(this.rootPath, relativePath);
|
|
12
|
+
|
|
13
|
+
// Validação de segurança - previne path traversal
|
|
14
|
+
if (!fullPath.startsWith(this.rootPath)) {
|
|
15
|
+
throw new Error('Path traversal detected');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
19
|
+
return {
|
|
20
|
+
path: relativePath,
|
|
21
|
+
content,
|
|
22
|
+
lines: content.split('\n').length,
|
|
23
|
+
size: Buffer.byteLength(content, 'utf8')
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async readMultiple(paths) {
|
|
28
|
+
return Promise.all(paths.map(p => this.read(p)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async findByPattern(pattern) {
|
|
32
|
+
const allFiles = await this.getAllFiles();
|
|
33
|
+
return allFiles.filter(f => pattern.test(f));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getAllFiles(dir = this.rootPath) {
|
|
37
|
+
const files = [];
|
|
38
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
39
|
+
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory() && !this.shouldIgnore(entry.name)) {
|
|
43
|
+
files.push(...await this.getAllFiles(fullPath));
|
|
44
|
+
} else if (entry.isFile()) {
|
|
45
|
+
files.push(path.relative(this.rootPath, fullPath));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
shouldIgnore(name) {
|
|
52
|
+
return this.ignorePatterns.includes(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
File without changes
|