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,576 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// 🆕 ADICIONAR ESTES ENDPOINTS AO server-local.js
|
|
3
|
+
// Colocar ANTES da seção "START SERVER"
|
|
4
|
+
// NÃO modificar endpoints existentes
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// 🆕 DRY-RUN ENDPOINT (Sprint 1.3)
|
|
9
|
+
// Valida e simula patch SEM modificar arquivos
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* POST /workspace/patch/dry-run
|
|
14
|
+
* Validates and simulates patch application WITHOUT modifying files
|
|
15
|
+
* Useful for preview before actually applying
|
|
16
|
+
*
|
|
17
|
+
* Body: { "diff": "unified diff text" }
|
|
18
|
+
*
|
|
19
|
+
* Response:
|
|
20
|
+
* {
|
|
21
|
+
* "ok": true,
|
|
22
|
+
* "valid": true,
|
|
23
|
+
* "canApply": true,
|
|
24
|
+
* "wouldModify": ["src/Main.java"],
|
|
25
|
+
* "hunksCount": 3,
|
|
26
|
+
* "fileChecks": [{ "path": "...", "exists": true, "lineCount": 100 }],
|
|
27
|
+
* "warnings": []
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
app.post("/workspace/patch/dry-run", async (req, res) => {
|
|
31
|
+
if (!WORKSPACE_ROOT) {
|
|
32
|
+
return res.status(400).json({
|
|
33
|
+
error: "workspace not set",
|
|
34
|
+
hint: "call POST /workspace/open first"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { diff } = req.body || {};
|
|
39
|
+
if (!diff) return res.status(400).json({ error: "diff is required" });
|
|
40
|
+
|
|
41
|
+
console.log(`🔍 Dry-run patch validation requested`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// 1. Validate diff format
|
|
45
|
+
const validation = validateDiff(diff);
|
|
46
|
+
if (!validation.valid) {
|
|
47
|
+
return res.json({
|
|
48
|
+
ok: true,
|
|
49
|
+
valid: false,
|
|
50
|
+
canApply: false,
|
|
51
|
+
errors: validation.errors,
|
|
52
|
+
wouldModify: [],
|
|
53
|
+
hunksCount: 0
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Extract target files
|
|
58
|
+
const targetFiles = extractTargetFiles(diff);
|
|
59
|
+
|
|
60
|
+
// 3. Check if files exist and get line counts
|
|
61
|
+
const fileChecks = await Promise.all(
|
|
62
|
+
targetFiles.map(async (relPath) => {
|
|
63
|
+
const fullPath = path.join(WORKSPACE_ROOT, relPath);
|
|
64
|
+
const fileExists = await exists(fullPath);
|
|
65
|
+
let lineCount = 0;
|
|
66
|
+
let size = 0;
|
|
67
|
+
|
|
68
|
+
if (fileExists) {
|
|
69
|
+
try {
|
|
70
|
+
const content = await readFile(fullPath, 'utf8');
|
|
71
|
+
lineCount = content.split('\n').length;
|
|
72
|
+
size = Buffer.byteLength(content, 'utf8');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// File exists but can't be read
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
path: relPath,
|
|
80
|
+
exists: fileExists,
|
|
81
|
+
lineCount,
|
|
82
|
+
size
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 4. Count hunks
|
|
88
|
+
const hunkMatches = diff.match(/@@ -\d+,?\d* \+\d+,?\d* @@/g) || [];
|
|
89
|
+
const hunkCount = hunkMatches.length;
|
|
90
|
+
|
|
91
|
+
// 5. Analyze changes
|
|
92
|
+
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
93
|
+
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
94
|
+
|
|
95
|
+
// 6. Check if all files exist (for modification patches)
|
|
96
|
+
const isNewFile = diff.includes('--- /dev/null');
|
|
97
|
+
const allFilesExist = fileChecks.every(f => f.exists) || isNewFile;
|
|
98
|
+
const missingFiles = fileChecks.filter(f => !f.exists && !isNewFile);
|
|
99
|
+
|
|
100
|
+
console.log(`✅ Dry-run complete: ${targetFiles.length} files, ${hunkCount} hunks, +${addedLines}/-${removedLines} lines`);
|
|
101
|
+
|
|
102
|
+
res.json({
|
|
103
|
+
ok: true,
|
|
104
|
+
valid: true,
|
|
105
|
+
canApply: allFilesExist,
|
|
106
|
+
wouldModify: targetFiles,
|
|
107
|
+
hunksCount: hunkCount,
|
|
108
|
+
changes: {
|
|
109
|
+
addedLines,
|
|
110
|
+
removedLines,
|
|
111
|
+
netChange: addedLines - removedLines
|
|
112
|
+
},
|
|
113
|
+
fileChecks,
|
|
114
|
+
missingFiles: missingFiles.map(f => f.path),
|
|
115
|
+
isNewFile,
|
|
116
|
+
warnings: missingFiles.length > 0 ? [`${missingFiles.length} file(s) not found in workspace`] : []
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`❌ Dry-run error: ${err.message}`);
|
|
120
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* DELETE /workspace/backups/:backupId
|
|
126
|
+
* Deletes a specific backup
|
|
127
|
+
*/
|
|
128
|
+
app.delete("/workspace/backups/:backupId", (req, res) => {
|
|
129
|
+
const { backupId } = req.params;
|
|
130
|
+
|
|
131
|
+
if (!BACKUPS.has(backupId)) {
|
|
132
|
+
return res.status(404).json({
|
|
133
|
+
error: "backup not found",
|
|
134
|
+
availableBackups: Array.from(BACKUPS.keys())
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const backup = BACKUPS.get(backupId);
|
|
139
|
+
BACKUPS.delete(backupId);
|
|
140
|
+
|
|
141
|
+
console.log(`🗑️ Backup deleted: ${backupId}`);
|
|
142
|
+
|
|
143
|
+
res.json({
|
|
144
|
+
ok: true,
|
|
145
|
+
deleted: backupId,
|
|
146
|
+
deletedFiles: backup.files.map(f => f.path)
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ============================================
|
|
151
|
+
// 🆕 DETECT PORT ENDPOINT (Sprint 1.2)
|
|
152
|
+
// Multi-language, multi-framework port detection
|
|
153
|
+
// ============================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* GET /workspace/detect-port
|
|
157
|
+
* Detects port from service configuration files
|
|
158
|
+
* Supports: Java (Spring Boot), Node.js, Python, .NET, Go, Ruby
|
|
159
|
+
*
|
|
160
|
+
* Query params:
|
|
161
|
+
* - servicePath: relative path to service (optional, defaults to workspace root)
|
|
162
|
+
* - language: java, node, python, dotnet, go, ruby (optional, auto-detect)
|
|
163
|
+
* - framework: spring-boot, express, flask, fastapi, etc (optional)
|
|
164
|
+
*/
|
|
165
|
+
app.get("/workspace/detect-port", async (req, res) => {
|
|
166
|
+
if (!WORKSPACE_ROOT) {
|
|
167
|
+
return res.status(400).json({
|
|
168
|
+
error: "workspace not set",
|
|
169
|
+
hint: "call POST /workspace/open first"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { servicePath = '', language, framework } = req.query;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const fullPath = servicePath ? path.join(WORKSPACE_ROOT, servicePath) : WORKSPACE_ROOT;
|
|
177
|
+
console.log(`🔍 Detecting port for service at: ${fullPath}`);
|
|
178
|
+
|
|
179
|
+
let port = null;
|
|
180
|
+
let detectionMethod = null;
|
|
181
|
+
let detectedLanguage = language;
|
|
182
|
+
|
|
183
|
+
// Auto-detect language if not provided
|
|
184
|
+
if (!detectedLanguage) {
|
|
185
|
+
const meta = await detectProject(fullPath);
|
|
186
|
+
detectedLanguage = meta.language;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Detect port based on language/framework
|
|
190
|
+
if (detectedLanguage === 'java') {
|
|
191
|
+
const result = await detectSpringBootPortLocal(fullPath);
|
|
192
|
+
port = result.port;
|
|
193
|
+
detectionMethod = result.method;
|
|
194
|
+
} else if (['node', 'javascript', 'typescript'].includes(detectedLanguage)) {
|
|
195
|
+
const result = await detectNodePortLocal(fullPath);
|
|
196
|
+
port = result.port;
|
|
197
|
+
detectionMethod = result.method;
|
|
198
|
+
} else if (detectedLanguage === 'python') {
|
|
199
|
+
const result = await detectPythonPortLocal(fullPath);
|
|
200
|
+
port = result.port;
|
|
201
|
+
detectionMethod = result.method;
|
|
202
|
+
} else if (['dotnet', 'csharp'].includes(detectedLanguage)) {
|
|
203
|
+
const result = await detectDotNetPortLocal(fullPath);
|
|
204
|
+
port = result.port;
|
|
205
|
+
detectionMethod = result.method;
|
|
206
|
+
} else if (detectedLanguage === 'go') {
|
|
207
|
+
const result = await detectGoPortLocal(fullPath);
|
|
208
|
+
port = result.port;
|
|
209
|
+
detectionMethod = result.method;
|
|
210
|
+
} else if (detectedLanguage === 'ruby') {
|
|
211
|
+
const result = await detectRubyPortLocal(fullPath);
|
|
212
|
+
port = result.port;
|
|
213
|
+
detectionMethod = result.method;
|
|
214
|
+
} else {
|
|
215
|
+
// Fallback to global detection
|
|
216
|
+
port = await detectPort(fullPath);
|
|
217
|
+
detectionMethod = 'global-detection';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`✅ Detected port: ${port || 'default'} via ${detectionMethod}`);
|
|
221
|
+
|
|
222
|
+
res.json({
|
|
223
|
+
ok: true,
|
|
224
|
+
port,
|
|
225
|
+
language: detectedLanguage,
|
|
226
|
+
framework: framework || null,
|
|
227
|
+
detectionMethod,
|
|
228
|
+
servicePath: servicePath || '/',
|
|
229
|
+
workspaceRoot: WORKSPACE_ROOT
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error('❌ Error detecting port:', err.message);
|
|
233
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ============================================
|
|
238
|
+
// PORT DETECTION HELPER FUNCTIONS
|
|
239
|
+
// ============================================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Detects Spring Boot port from configuration files
|
|
243
|
+
*/
|
|
244
|
+
async function detectSpringBootPortLocal(servicePath) {
|
|
245
|
+
const candidates = [
|
|
246
|
+
path.join(servicePath, 'src/main/resources/application.yml'),
|
|
247
|
+
path.join(servicePath, 'src/main/resources/application.yaml'),
|
|
248
|
+
path.join(servicePath, 'src/main/resources/application.properties'),
|
|
249
|
+
path.join(servicePath, 'src/main/resources/application-local.yml'),
|
|
250
|
+
path.join(servicePath, 'src/main/resources/application-local.yaml'),
|
|
251
|
+
path.join(servicePath, 'src/main/resources/application-local.properties'),
|
|
252
|
+
path.join(servicePath, 'src/main/resources/application-dev.yml'),
|
|
253
|
+
path.join(servicePath, 'src/main/resources/application-dev.properties'),
|
|
254
|
+
path.join(servicePath, 'application.yml'),
|
|
255
|
+
path.join(servicePath, 'application.yaml'),
|
|
256
|
+
path.join(servicePath, 'application.properties')
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
for (const filePath of candidates) {
|
|
260
|
+
if (await exists(filePath)) {
|
|
261
|
+
try {
|
|
262
|
+
const content = await readFile(filePath, 'utf8');
|
|
263
|
+
|
|
264
|
+
// YAML format - multiple patterns
|
|
265
|
+
if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
|
|
266
|
+
// Pattern: server:\n port: 8090
|
|
267
|
+
const simpleMatch = content.match(/server:\s*\n\s+port:\s*(\d+)/);
|
|
268
|
+
if (simpleMatch) {
|
|
269
|
+
return { port: parseInt(simpleMatch[1]), method: `yaml:${path.basename(filePath)}` };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Pattern: ${PORT:8080} or ${SERVER_PORT:8080}
|
|
273
|
+
const envMatch = content.match(/port:\s*\$\{[A-Z_]+:(\d+)\}/);
|
|
274
|
+
if (envMatch) {
|
|
275
|
+
return { port: parseInt(envMatch[1]), method: `yaml-env-default:${path.basename(filePath)}` };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Pattern: port: 8090 (simple)
|
|
279
|
+
const directMatch = content.match(/^\s*port:\s*(\d+)/m);
|
|
280
|
+
if (directMatch) {
|
|
281
|
+
return { port: parseInt(directMatch[1]), method: `yaml-direct:${path.basename(filePath)}` };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Properties format
|
|
286
|
+
if (filePath.endsWith('.properties')) {
|
|
287
|
+
const match = content.match(/server\.port\s*=\s*(\d+)/);
|
|
288
|
+
if (match) {
|
|
289
|
+
return { port: parseInt(match[1]), method: `properties:${path.basename(filePath)}` };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ${PORT:8080} pattern
|
|
293
|
+
const envMatch = content.match(/server\.port\s*=\s*\$\{[A-Z_]+:(\d+)\}/);
|
|
294
|
+
if (envMatch) {
|
|
295
|
+
return { port: parseInt(envMatch[1]), method: `properties-env-default:${path.basename(filePath)}` };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (e) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { port: 8080, method: 'spring-boot-default' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Detects Node.js port from configuration files
|
|
309
|
+
*/
|
|
310
|
+
async function detectNodePortLocal(servicePath) {
|
|
311
|
+
// Priority 1: .env files
|
|
312
|
+
const envFiles = ['.env.local', '.env.development', '.env'];
|
|
313
|
+
for (const envFile of envFiles) {
|
|
314
|
+
const envPath = path.join(servicePath, envFile);
|
|
315
|
+
if (await exists(envPath)) {
|
|
316
|
+
try {
|
|
317
|
+
const content = await readFile(envPath, 'utf8');
|
|
318
|
+
const match = content.match(/^PORT\s*=\s*(\d+)/mi);
|
|
319
|
+
if (match) {
|
|
320
|
+
return { port: parseInt(match[1]), method: `dotenv:${envFile}` };
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Priority 2: package.json scripts
|
|
329
|
+
const pkgPath = path.join(servicePath, 'package.json');
|
|
330
|
+
if (await exists(pkgPath)) {
|
|
331
|
+
try {
|
|
332
|
+
const content = await readFile(pkgPath, 'utf8');
|
|
333
|
+
const pkg = JSON.parse(content);
|
|
334
|
+
const scripts = JSON.stringify(pkg.scripts || {});
|
|
335
|
+
|
|
336
|
+
// Patterns: --port 3000, --port=3000, PORT=3000, -p 3000
|
|
337
|
+
const match = scripts.match(/--port[=\s]+(\d+)|PORT[=\s]+(\d+)|-p\s+(\d+)/i);
|
|
338
|
+
if (match) {
|
|
339
|
+
const port = parseInt(match[1] || match[2] || match[3]);
|
|
340
|
+
return { port, method: 'package-json-scripts' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for port in config
|
|
344
|
+
if (pkg.config?.port) {
|
|
345
|
+
return { port: parseInt(pkg.config.port), method: 'package-json-config' };
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {
|
|
348
|
+
// Continue to default
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Priority 3: Check for common config files
|
|
353
|
+
const configFiles = ['config.json', 'config/default.json', '.env.example'];
|
|
354
|
+
for (const configFile of configFiles) {
|
|
355
|
+
const configPath = path.join(servicePath, configFile);
|
|
356
|
+
if (await exists(configPath)) {
|
|
357
|
+
try {
|
|
358
|
+
const content = await readFile(configPath, 'utf8');
|
|
359
|
+
const match = content.match(/["']?port["']?\s*[=:]\s*(\d+)/i);
|
|
360
|
+
if (match) {
|
|
361
|
+
return { port: parseInt(match[1]), method: `config:${configFile}` };
|
|
362
|
+
}
|
|
363
|
+
} catch (e) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { port: 3000, method: 'node-default' };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Detects Python port from configuration files
|
|
374
|
+
*/
|
|
375
|
+
async function detectPythonPortLocal(servicePath) {
|
|
376
|
+
// Priority 1: .env files
|
|
377
|
+
const envFiles = ['.env', '.env.local', '.env.development'];
|
|
378
|
+
for (const envFile of envFiles) {
|
|
379
|
+
const envPath = path.join(servicePath, envFile);
|
|
380
|
+
if (await exists(envPath)) {
|
|
381
|
+
try {
|
|
382
|
+
const content = await readFile(envPath, 'utf8');
|
|
383
|
+
const match = content.match(/^PORT\s*=\s*(\d+)/mi);
|
|
384
|
+
if (match) {
|
|
385
|
+
return { port: parseInt(match[1]), method: `dotenv:${envFile}` };
|
|
386
|
+
}
|
|
387
|
+
} catch (e) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Priority 2: Python config files
|
|
394
|
+
const configFiles = [
|
|
395
|
+
'config.py', 'settings.py', 'app/config.py', 'src/config.py',
|
|
396
|
+
'config/settings.py', 'core/settings.py'
|
|
397
|
+
];
|
|
398
|
+
for (const configFile of configFiles) {
|
|
399
|
+
const configPath = path.join(servicePath, configFile);
|
|
400
|
+
if (await exists(configPath)) {
|
|
401
|
+
try {
|
|
402
|
+
const content = await readFile(configPath, 'utf8');
|
|
403
|
+
const match = content.match(/PORT\s*=\s*(\d+)/i);
|
|
404
|
+
if (match) {
|
|
405
|
+
return { port: parseInt(match[1]), method: `python-config:${configFile}` };
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Priority 3: Check for Dockerfile with EXPOSE
|
|
414
|
+
const dockerfilePath = path.join(servicePath, 'Dockerfile');
|
|
415
|
+
if (await exists(dockerfilePath)) {
|
|
416
|
+
try {
|
|
417
|
+
const content = await readFile(dockerfilePath, 'utf8');
|
|
418
|
+
const match = content.match(/EXPOSE\s+(\d+)/);
|
|
419
|
+
if (match) {
|
|
420
|
+
return { port: parseInt(match[1]), method: 'dockerfile-expose' };
|
|
421
|
+
}
|
|
422
|
+
} catch (e) {
|
|
423
|
+
// Continue to default
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { port: 8000, method: 'python-default' };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Detects .NET port from launchSettings.json
|
|
432
|
+
*/
|
|
433
|
+
async function detectDotNetPortLocal(servicePath) {
|
|
434
|
+
// Priority 1: launchSettings.json
|
|
435
|
+
const launchSettings = path.join(servicePath, 'Properties/launchSettings.json');
|
|
436
|
+
if (await exists(launchSettings)) {
|
|
437
|
+
try {
|
|
438
|
+
const content = await readFile(launchSettings, 'utf8');
|
|
439
|
+
const settings = JSON.parse(content);
|
|
440
|
+
|
|
441
|
+
const profiles = settings.profiles || {};
|
|
442
|
+
for (const [profileName, profile] of Object.entries(profiles)) {
|
|
443
|
+
if (profile.applicationUrl) {
|
|
444
|
+
const match = profile.applicationUrl.match(/:(\d+)/);
|
|
445
|
+
if (match) {
|
|
446
|
+
return { port: parseInt(match[1]), method: `launchSettings:${profileName}` };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
// Continue to next method
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Priority 2: appsettings.json
|
|
456
|
+
const appSettingsFiles = ['appsettings.Development.json', 'appsettings.json'];
|
|
457
|
+
for (const settingsFile of appSettingsFiles) {
|
|
458
|
+
const appSettings = path.join(servicePath, settingsFile);
|
|
459
|
+
if (await exists(appSettings)) {
|
|
460
|
+
try {
|
|
461
|
+
const content = await readFile(appSettings, 'utf8');
|
|
462
|
+
const settings = JSON.parse(content);
|
|
463
|
+
|
|
464
|
+
if (settings.Kestrel?.Endpoints?.Http?.Url) {
|
|
465
|
+
const match = settings.Kestrel.Endpoints.Http.Url.match(/:(\d+)/);
|
|
466
|
+
if (match) {
|
|
467
|
+
return { port: parseInt(match[1]), method: `appsettings:${settingsFile}` };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { port: 5000, method: 'dotnet-default' };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Detects Go port from configuration
|
|
481
|
+
*/
|
|
482
|
+
async function detectGoPortLocal(servicePath) {
|
|
483
|
+
// Priority 1: .env files
|
|
484
|
+
const envPath = path.join(servicePath, '.env');
|
|
485
|
+
if (await exists(envPath)) {
|
|
486
|
+
try {
|
|
487
|
+
const content = await readFile(envPath, 'utf8');
|
|
488
|
+
const match = content.match(/^PORT\s*=\s*(\d+)/mi);
|
|
489
|
+
if (match) {
|
|
490
|
+
return { port: parseInt(match[1]), method: 'dotenv' };
|
|
491
|
+
}
|
|
492
|
+
} catch (e) {
|
|
493
|
+
// Continue
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Priority 2: config.yaml/config.json
|
|
498
|
+
const configFiles = ['config.yaml', 'config.yml', 'config.json', 'configs/config.yaml'];
|
|
499
|
+
for (const configFile of configFiles) {
|
|
500
|
+
const configPath = path.join(servicePath, configFile);
|
|
501
|
+
if (await exists(configPath)) {
|
|
502
|
+
try {
|
|
503
|
+
const content = await readFile(configPath, 'utf8');
|
|
504
|
+
const match = content.match(/port['"]*\s*[=:]\s*['"]*(\d+)/i);
|
|
505
|
+
if (match) {
|
|
506
|
+
return { port: parseInt(match[1]), method: `config:${configFile}` };
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { port: 8080, method: 'go-default' };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Detects Ruby/Rails port from configuration
|
|
519
|
+
*/
|
|
520
|
+
async function detectRubyPortLocal(servicePath) {
|
|
521
|
+
// Priority 1: .env
|
|
522
|
+
const envPath = path.join(servicePath, '.env');
|
|
523
|
+
if (await exists(envPath)) {
|
|
524
|
+
try {
|
|
525
|
+
const content = await readFile(envPath, 'utf8');
|
|
526
|
+
const match = content.match(/^PORT\s*=\s*(\d+)/mi);
|
|
527
|
+
if (match) {
|
|
528
|
+
return { port: parseInt(match[1]), method: 'dotenv' };
|
|
529
|
+
}
|
|
530
|
+
} catch (e) {
|
|
531
|
+
// Continue
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Priority 2: Puma config
|
|
536
|
+
const pumaConfig = path.join(servicePath, 'config/puma.rb');
|
|
537
|
+
if (await exists(pumaConfig)) {
|
|
538
|
+
try {
|
|
539
|
+
const content = await readFile(pumaConfig, 'utf8');
|
|
540
|
+
const match = content.match(/port\s+(\d+)/i);
|
|
541
|
+
if (match) {
|
|
542
|
+
return { port: parseInt(match[1]), method: 'puma-config' };
|
|
543
|
+
}
|
|
544
|
+
} catch (e) {
|
|
545
|
+
// Continue
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return { port: 3000, method: 'ruby-default' };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============================================
|
|
553
|
+
// HELPER FUNCTIONS (if not already present)
|
|
554
|
+
// ============================================
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Helper: Extract target files from diff
|
|
558
|
+
* (Add only if not already present in server-local.js)
|
|
559
|
+
*/
|
|
560
|
+
function extractTargetFiles(diff) {
|
|
561
|
+
const files = [];
|
|
562
|
+
const lines = diff.split('\n');
|
|
563
|
+
|
|
564
|
+
for (const line of lines) {
|
|
565
|
+
if (line.startsWith('+++ ')) {
|
|
566
|
+
let filePath = line.substring(4).trim();
|
|
567
|
+
// Remove prefixes like b/ or ./
|
|
568
|
+
filePath = filePath.replace(/^[ab]\//, '').replace(/^\.\//,'');
|
|
569
|
+
if (filePath && filePath !== '/dev/null') {
|
|
570
|
+
files.push(filePath);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return files;
|
|
576
|
+
}
|