deepdebug-local-agent 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/analyzers/config-analyzer.js +446 -0
- package/analyzers/controller-analyzer.js +429 -0
- package/analyzers/dto-analyzer.js +455 -0
- package/detectors/build-tool-detector.js +0 -0
- package/detectors/framework-detector.js +91 -0
- package/detectors/language-detector.js +89 -0
- package/detectors/multi-project-detector.js +191 -0
- package/detectors/service-detector.js +244 -0
- package/detectors.js +30 -0
- package/exec-utils.js +215 -0
- package/fs-utils.js +34 -0
- package/mcp-http-server.js +313 -0
- package/package.json +1 -1
- package/patch.js +607 -0
- package/ports.js +69 -0
- package/server.js +1 -138
- package/workspace/detect-port.js +176 -0
- package/workspace/file-reader.js +54 -0
- package/workspace/git-client.js +0 -0
- package/workspace/process-manager.js +619 -0
- package/workspace/scanner.js +72 -0
- package/workspace-manager.js +172 -0
package/server.js
CHANGED
|
@@ -557,7 +557,6 @@ app.use(cors({
|
|
|
557
557
|
// Cloud Run URLs (regex patterns)
|
|
558
558
|
/https:\/\/.*\.run\.app$/,
|
|
559
559
|
/https:\/\/.*\.web\.app$/,
|
|
560
|
-
// Production frontend
|
|
561
560
|
"https://deepdebug.ai",
|
|
562
561
|
"https://www.deepdebug.ai"
|
|
563
562
|
],
|
|
@@ -5083,126 +5082,10 @@ app.post("/workspace/:workspaceId/run", async (req, res) => {
|
|
|
5083
5082
|
}
|
|
5084
5083
|
});
|
|
5085
5084
|
|
|
5086
|
-
|
|
5087
|
-
// ============================================
|
|
5088
|
-
// 🌐 CLOUDFLARE TUNNEL - Auto-expose Local Agent
|
|
5089
|
-
// ============================================
|
|
5090
|
-
import { execFile, spawn } from 'child_process';
|
|
5091
|
-
import { createWriteStream } from 'fs';
|
|
5092
|
-
import { pipeline } from 'stream/promises';
|
|
5093
|
-
|
|
5094
|
-
async function startCloudflaredTunnel(port, gatewayUrl, apiKey, workspaceId) {
|
|
5095
|
-
const platform = process.platform;
|
|
5096
|
-
const arch = process.arch;
|
|
5097
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
5098
|
-
const deepdebugDir = path.join(homeDir, '.deepdebug');
|
|
5099
|
-
const binaryName = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
5100
|
-
const binaryPath = path.join(deepdebugDir, binaryName);
|
|
5101
|
-
|
|
5102
|
-
// Download URL based on platform
|
|
5103
|
-
const downloadUrls = {
|
|
5104
|
-
'win32-x64': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe',
|
|
5105
|
-
'darwin-x64': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64',
|
|
5106
|
-
'darwin-arm64':'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64',
|
|
5107
|
-
'linux-x64': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
|
|
5108
|
-
'linux-arm64': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64',
|
|
5109
|
-
};
|
|
5110
|
-
|
|
5111
|
-
const key = `${platform}-${arch}`;
|
|
5112
|
-
const downloadUrl = downloadUrls[key];
|
|
5113
|
-
|
|
5114
|
-
if (!downloadUrl) {
|
|
5115
|
-
console.warn(`⚠️ Cloudflared not available for platform: ${key}`);
|
|
5116
|
-
return null;
|
|
5117
|
-
}
|
|
5118
|
-
|
|
5119
|
-
// Download cloudflared if not present
|
|
5120
|
-
if (!fs.existsSync(binaryPath)) {
|
|
5121
|
-
console.log(`⬇️ Downloading cloudflared for ${key}...`);
|
|
5122
|
-
try {
|
|
5123
|
-
const res = await fetch(downloadUrl);
|
|
5124
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
5125
|
-
await pipeline(res.body, createWriteStream(binaryPath));
|
|
5126
|
-
// Make executable on unix
|
|
5127
|
-
if (platform !== 'win32') {
|
|
5128
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
5129
|
-
}
|
|
5130
|
-
console.log(`✅ cloudflared downloaded to ${binaryPath}`);
|
|
5131
|
-
} catch (err) {
|
|
5132
|
-
console.warn(`⚠️ Failed to download cloudflared: ${err.message}`);
|
|
5133
|
-
return null;
|
|
5134
|
-
}
|
|
5135
|
-
}
|
|
5136
|
-
|
|
5137
|
-
// Start tunnel
|
|
5138
|
-
return new Promise((resolve) => {
|
|
5139
|
-
console.log(`🌐 Starting Cloudflare tunnel on port ${port}...`);
|
|
5140
|
-
|
|
5141
|
-
const child = spawn(binaryPath, [
|
|
5142
|
-
'tunnel', '--url', `http://localhost:${port}`,
|
|
5143
|
-
'--no-autoupdate'
|
|
5144
|
-
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
5145
|
-
|
|
5146
|
-
let tunnelUrl = null;
|
|
5147
|
-
const urlPattern = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
5148
|
-
|
|
5149
|
-
const handleOutput = async (data) => {
|
|
5150
|
-
const text = data.toString();
|
|
5151
|
-
const match = text.match(urlPattern);
|
|
5152
|
-
if (match && !tunnelUrl) {
|
|
5153
|
-
tunnelUrl = match[0];
|
|
5154
|
-
console.log(`\n🌐 ====================================`);
|
|
5155
|
-
console.log(`🌐 Tunnel URL: ${tunnelUrl}`);
|
|
5156
|
-
console.log(`🌐 ====================================\n`);
|
|
5157
|
-
|
|
5158
|
-
// Register tunnel URL with Gateway
|
|
5159
|
-
if (gatewayUrl && apiKey && workspaceId) {
|
|
5160
|
-
try {
|
|
5161
|
-
const registerRes = await fetch(`${gatewayUrl}/api/v1/workspaces/${workspaceId}/agent-url`, {
|
|
5162
|
-
method: 'PUT',
|
|
5163
|
-
headers: {
|
|
5164
|
-
'Content-Type': 'application/json',
|
|
5165
|
-
'Authorization': `Bearer ${apiKey}`
|
|
5166
|
-
},
|
|
5167
|
-
body: JSON.stringify({ agentUrl: tunnelUrl })
|
|
5168
|
-
});
|
|
5169
|
-
if (registerRes.ok) {
|
|
5170
|
-
console.log(`✅ Agent URL registered with Gateway`);
|
|
5171
|
-
} else {
|
|
5172
|
-
console.warn(`⚠️ Failed to register agent URL: ${registerRes.status}`);
|
|
5173
|
-
}
|
|
5174
|
-
} catch (err) {
|
|
5175
|
-
console.warn(`⚠️ Could not register agent URL: ${err.message}`);
|
|
5176
|
-
}
|
|
5177
|
-
}
|
|
5178
|
-
resolve(tunnelUrl);
|
|
5179
|
-
}
|
|
5180
|
-
};
|
|
5181
|
-
|
|
5182
|
-
child.stdout.on('data', handleOutput);
|
|
5183
|
-
child.stderr.on('data', handleOutput);
|
|
5184
|
-
|
|
5185
|
-
child.on('exit', (code) => {
|
|
5186
|
-
if (!tunnelUrl) {
|
|
5187
|
-
console.warn(`⚠️ cloudflared exited with code ${code} before tunnel was established`);
|
|
5188
|
-
resolve(null);
|
|
5189
|
-
}
|
|
5190
|
-
});
|
|
5191
|
-
|
|
5192
|
-
// Timeout after 30 seconds
|
|
5193
|
-
setTimeout(() => {
|
|
5194
|
-
if (!tunnelUrl) {
|
|
5195
|
-
console.warn('⚠️ Cloudflare tunnel timeout - continuing without tunnel');
|
|
5196
|
-
resolve(null);
|
|
5197
|
-
}
|
|
5198
|
-
}, 30000);
|
|
5199
|
-
});
|
|
5200
|
-
}
|
|
5201
|
-
|
|
5202
5085
|
// ============================================
|
|
5203
5086
|
// START SERVER
|
|
5204
5087
|
// ============================================
|
|
5205
|
-
app.listen(PORT, '0.0.0.0',
|
|
5088
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
5206
5089
|
console.log(`\n🔌 DeepDebug Local Agent listening on port ${PORT}`);
|
|
5207
5090
|
console.log(`📦 Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
5208
5091
|
console.log(`📦 Process Manager initialized`);
|
|
@@ -5213,26 +5096,6 @@ app.listen(PORT, '0.0.0.0', async () => {
|
|
|
5213
5096
|
console.log(` Max Retries: ${aiEngine.maxRetries}`);
|
|
5214
5097
|
}
|
|
5215
5098
|
console.log(`\n🚀 Ready to receive requests!\n`);
|
|
5216
|
-
|
|
5217
|
-
// 🌐 Auto-start Cloudflare Tunnel
|
|
5218
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
5219
|
-
const configPath = path.join(homeDir, '.deepdebug', 'config.json');
|
|
5220
|
-
let tunnelConfig = {};
|
|
5221
|
-
try {
|
|
5222
|
-
if (fs.existsSync(configPath)) {
|
|
5223
|
-
tunnelConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
5224
|
-
}
|
|
5225
|
-
} catch (e) {}
|
|
5226
|
-
|
|
5227
|
-
const gatewayUrl = tunnelConfig.gatewayUrl || process.env.GATEWAY_URL;
|
|
5228
|
-
const apiKey = tunnelConfig.apiKey || process.env.DEEPDEBUG_API_KEY;
|
|
5229
|
-
const workspaceId = tunnelConfig.workspaceId || process.env.WORKSPACE_ID;
|
|
5230
|
-
|
|
5231
|
-
if (process.env.DISABLE_TUNNEL !== 'true') {
|
|
5232
|
-
startCloudflaredTunnel(PORT, gatewayUrl, apiKey, workspaceId).catch(err => {
|
|
5233
|
-
console.warn(`⚠️ Tunnel error: ${err.message}`);
|
|
5234
|
-
});
|
|
5235
|
-
}
|
|
5236
5099
|
});
|
|
5237
5100
|
|
|
5238
5101
|
// Start MCP HTTP Bridge em paralelo (port 5056)
|
|
@@ -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
|