deepdebug-local-agent 1.0.20 → 1.0.21
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/CLAUDE_CODE_AGENT_INFINITE.md +623 -0
- package/Dockerfile +2 -0
- package/Dockerfile.full +63 -0
- package/local-agent-source.tar.gz +0 -0
- package/package.json +4 -3
- package/src/detectors/language-detector.js +29 -12
- package/src/detectors.js +291 -8
- package/src/exec-utils.js +19 -16
- package/src/local-agent-source.tar.gz +0 -0
- package/src/runtimes/java/maven-runtime.js +4 -3
- package/src/server.js +456 -0
- package/src/utils/exec-utils.js +8 -4
package/src/server.js
CHANGED
|
@@ -1096,6 +1096,383 @@ app.get("/workspace/file-content", async (req, res) => {
|
|
|
1096
1096
|
}
|
|
1097
1097
|
});
|
|
1098
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* POST /workspace/build-push
|
|
1101
|
+
* Compress workspace, upload to GCS, submit Cloud Build.
|
|
1102
|
+
* No Docker daemon needed — uses Cloud Build API + GCS.
|
|
1103
|
+
* Body: { workspaceId, imageName, gcpProject, saKeyJson }
|
|
1104
|
+
*/
|
|
1105
|
+
app.post("/workspace/build-push", async (req, res) => {
|
|
1106
|
+
const { workspaceId, imageName, gcpProject, saKeyJson } = req.body || {};
|
|
1107
|
+
if (!workspaceId || !imageName || !gcpProject || !saKeyJson) {
|
|
1108
|
+
return res.status(400).json({ error: "workspaceId, imageName, gcpProject and saKeyJson required" });
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1112
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1113
|
+
res.setHeader("Connection", "keep-alive");
|
|
1114
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1115
|
+
|
|
1116
|
+
const emit = (event, msg) => {
|
|
1117
|
+
const data = typeof msg === "object" ? JSON.stringify(msg) : JSON.stringify({ message: msg });
|
|
1118
|
+
res.write(`event:${event}\ndata:${data}\n\n`);
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
try {
|
|
1122
|
+
const nfsBase = process.env.WORKSPACES_MOUNT || "/mnt/workspaces";
|
|
1123
|
+
const wsPath = path.join(nfsBase, workspaceId);
|
|
1124
|
+
|
|
1125
|
+
if (!fs.existsSync(wsPath)) {
|
|
1126
|
+
emit("error", { error: "Workspace not found: " + wsPath });
|
|
1127
|
+
return res.end();
|
|
1128
|
+
}
|
|
1129
|
+
if (!fs.existsSync(path.join(wsPath, "Dockerfile"))) {
|
|
1130
|
+
emit("error", { error: "No Dockerfile in workspace. Run detect+generate first." });
|
|
1131
|
+
return res.end();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Step 1: Get GCP access token from SA key
|
|
1135
|
+
emit("progress", "Authenticating with GCP...");
|
|
1136
|
+
let accessToken;
|
|
1137
|
+
try {
|
|
1138
|
+
const { createSign } = await import("crypto");
|
|
1139
|
+
const sa = JSON.parse(saKeyJson);
|
|
1140
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1141
|
+
const h = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
|
|
1142
|
+
const c = Buffer.from(JSON.stringify({
|
|
1143
|
+
iss: sa.client_email,
|
|
1144
|
+
scope: "https://www.googleapis.com/auth/cloud-platform",
|
|
1145
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
1146
|
+
iat: now, exp: now + 3600
|
|
1147
|
+
})).toString("base64url");
|
|
1148
|
+
const sign = createSign("SHA256");
|
|
1149
|
+
sign.update(`${h}.${c}`);
|
|
1150
|
+
const sig = sign.sign(sa.private_key, "base64url");
|
|
1151
|
+
const jwt = `${h}.${c}.${sig}`;
|
|
1152
|
+
|
|
1153
|
+
const tokenResp = await fetch("https://oauth2.googleapis.com/token", {
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1156
|
+
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`
|
|
1157
|
+
});
|
|
1158
|
+
const tokenData = await tokenResp.json();
|
|
1159
|
+
accessToken = tokenData.access_token;
|
|
1160
|
+
if (!accessToken) throw new Error("No access_token: " + JSON.stringify(tokenData));
|
|
1161
|
+
emit("progress", "GCP authentication successful");
|
|
1162
|
+
} catch (e) {
|
|
1163
|
+
emit("error", { error: "GCP auth failed: " + e.message });
|
|
1164
|
+
return res.end();
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Step 2: Compress workspace to tar.gz
|
|
1168
|
+
emit("progress", "Compressing source code...");
|
|
1169
|
+
const { execSync } = await import("child_process");
|
|
1170
|
+
const tarPath = `/tmp/source-${workspaceId}-${Date.now()}.tar.gz`;
|
|
1171
|
+
try {
|
|
1172
|
+
execSync(
|
|
1173
|
+
`tar -czf "${tarPath}" --exclude=".git" --exclude="node_modules" ` +
|
|
1174
|
+
`--exclude="target" --exclude="*.class" --exclude=".gradle" ` +
|
|
1175
|
+
`-C "${wsPath}" . 2>&1`,
|
|
1176
|
+
{ stdio: "pipe", timeout: 120000 }
|
|
1177
|
+
);
|
|
1178
|
+
const sizeKB = Math.round(fs.statSync(tarPath).size / 1024);
|
|
1179
|
+
emit("progress", `Source compressed: ${sizeKB}KB`);
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
emit("error", { error: "Compression failed: " + (e.message || "").substring(0, 200) });
|
|
1182
|
+
return res.end();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Step 3: Upload to GCS
|
|
1186
|
+
const bucketName = `${gcpProject}_cloudbuild`;
|
|
1187
|
+
const objectName = `source/${workspaceId}-${Date.now()}.tar.gz`;
|
|
1188
|
+
emit("progress", `Uploading source to GCS...`);
|
|
1189
|
+
try {
|
|
1190
|
+
// Create bucket (ignore if exists)
|
|
1191
|
+
await fetch(`https://storage.googleapis.com/storage/v1/b?project=${gcpProject}`, {
|
|
1192
|
+
method: "POST",
|
|
1193
|
+
headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
1194
|
+
body: JSON.stringify({ name: bucketName, location: "US" })
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
const fileBuffer = fs.readFileSync(tarPath);
|
|
1198
|
+
const uploadResp = await fetch(
|
|
1199
|
+
`https://storage.googleapis.com/upload/storage/v1/b/${encodeURIComponent(bucketName)}/o?uploadType=media&name=${encodeURIComponent(objectName)}`,
|
|
1200
|
+
{
|
|
1201
|
+
method: "POST",
|
|
1202
|
+
headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/gzip" },
|
|
1203
|
+
body: fileBuffer
|
|
1204
|
+
}
|
|
1205
|
+
);
|
|
1206
|
+
if (!uploadResp.ok) {
|
|
1207
|
+
const errText = await uploadResp.text();
|
|
1208
|
+
throw new Error(`GCS upload ${uploadResp.status}: ${errText.substring(0, 200)}`);
|
|
1209
|
+
}
|
|
1210
|
+
emit("progress", "Source uploaded to GCS");
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
emit("error", { error: "GCS upload failed: " + e.message });
|
|
1213
|
+
return res.end();
|
|
1214
|
+
} finally {
|
|
1215
|
+
try { fs.unlinkSync(tarPath); } catch {}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Step 4: Submit Cloud Build
|
|
1219
|
+
emit("progress", "Submitting Cloud Build...");
|
|
1220
|
+
let buildId = "unknown";
|
|
1221
|
+
try {
|
|
1222
|
+
const buildReq = {
|
|
1223
|
+
source: { storageSource: { bucket: bucketName, object: objectName } },
|
|
1224
|
+
steps: [
|
|
1225
|
+
{ name: "gcr.io/cloud-builders/docker", args: ["build", "-t", imageName, "."], id: "build" },
|
|
1226
|
+
{ name: "gcr.io/cloud-builders/docker", args: ["push", imageName], id: "push", waitFor: ["build"] }
|
|
1227
|
+
],
|
|
1228
|
+
images: [imageName],
|
|
1229
|
+
timeout: "1200s"
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const buildResp = await fetch(
|
|
1233
|
+
`https://cloudbuild.googleapis.com/v1/projects/${gcpProject}/builds`,
|
|
1234
|
+
{
|
|
1235
|
+
method: "POST",
|
|
1236
|
+
headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
1237
|
+
body: JSON.stringify(buildReq)
|
|
1238
|
+
}
|
|
1239
|
+
);
|
|
1240
|
+
if (!buildResp.ok) {
|
|
1241
|
+
const errText = await buildResp.text();
|
|
1242
|
+
throw new Error(`Cloud Build ${buildResp.status}: ${errText.substring(0, 300)}`);
|
|
1243
|
+
}
|
|
1244
|
+
const buildData = await buildResp.json();
|
|
1245
|
+
buildId = buildData.metadata?.build?.id || buildData.name?.split("/").pop() || "unknown";
|
|
1246
|
+
emit("progress", `Cloud Build started: ${buildId}`);
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
emit("error", { error: "Cloud Build submit failed: " + e.message });
|
|
1249
|
+
return res.end();
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Step 5: Poll build status
|
|
1253
|
+
emit("progress", "Building Docker image (3-5 minutes)...");
|
|
1254
|
+
let status = "WORKING";
|
|
1255
|
+
let attempts = 0;
|
|
1256
|
+
while (!["SUCCESS", "FAILURE", "CANCELLED", "TIMEOUT"].includes(status) && attempts < 40) {
|
|
1257
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
1258
|
+
attempts++;
|
|
1259
|
+
try {
|
|
1260
|
+
const statusResp = await fetch(
|
|
1261
|
+
`https://cloudbuild.googleapis.com/v1/projects/${gcpProject}/builds/${buildId}`,
|
|
1262
|
+
{ headers: { "Authorization": `Bearer ${accessToken}` } }
|
|
1263
|
+
);
|
|
1264
|
+
const statusData = await statusResp.json();
|
|
1265
|
+
status = statusData.status || "WORKING";
|
|
1266
|
+
emit("progress", `Build status: ${status} (${attempts}/40)`);
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
emit("progress", `Status check error: ${e.message}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (status === "SUCCESS") {
|
|
1273
|
+
emit("progress", "Docker image built and pushed successfully!");
|
|
1274
|
+
emit("complete", { success: true, imageName, buildId });
|
|
1275
|
+
} else {
|
|
1276
|
+
emit("error", { error: `Cloud Build ${status}. Check GCP Console.` });
|
|
1277
|
+
}
|
|
1278
|
+
res.end();
|
|
1279
|
+
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
console.error("[build-push] Fatal:", err.message);
|
|
1282
|
+
emit("error", { error: err.message });
|
|
1283
|
+
res.end();
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* POST /workspace/setup
|
|
1289
|
+
* AI Cloud Engineering Agent - autonomous setup:
|
|
1290
|
+
* 1. Clone repo 2. Detect stack 3. Generate Dockerfile 4. Register workspace
|
|
1291
|
+
* Returns SSE stream with real-time progress.
|
|
1292
|
+
*/
|
|
1293
|
+
app.post("/workspace/setup", async (req, res) => {
|
|
1294
|
+
const { workspaceId, repoUrl, gitToken, provider = "deepdebug_cloud",
|
|
1295
|
+
cloudProjectId, cloudToken } = req.body || {};
|
|
1296
|
+
|
|
1297
|
+
if (!workspaceId || !repoUrl) {
|
|
1298
|
+
return res.status(400).json({ error: "workspaceId and repoUrl required" });
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1302
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1303
|
+
res.setHeader("Connection", "keep-alive");
|
|
1304
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1305
|
+
|
|
1306
|
+
const emit = (event, msg) => {
|
|
1307
|
+
const data = typeof msg === "object" ? JSON.stringify(msg) : JSON.stringify({ message: msg });
|
|
1308
|
+
res.write(`event:${event}\ndata:${data}\n\n`);
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
try {
|
|
1312
|
+
// Use /mnt/workspaces as base (not WORKSPACE_ROOT which is a specific workspace)
|
|
1313
|
+
const nfsBase = process.env.WORKSPACES_MOUNT || "/mnt/workspaces";
|
|
1314
|
+
const wsPath = path.join(nfsBase, workspaceId);
|
|
1315
|
+
|
|
1316
|
+
emit("progress", `Starting setup for workspace: ${workspaceId}`);
|
|
1317
|
+
emit("progress", `Workspace path: ${wsPath}`);
|
|
1318
|
+
|
|
1319
|
+
// STEP 1 - Clone
|
|
1320
|
+
emit("progress", "Cloning repository...");
|
|
1321
|
+
let authUrl = repoUrl;
|
|
1322
|
+
if (gitToken && gitToken.length > 5) {
|
|
1323
|
+
// Build authenticated URL
|
|
1324
|
+
try {
|
|
1325
|
+
const urlObj = new URL(repoUrl);
|
|
1326
|
+
urlObj.username = "x-access-token";
|
|
1327
|
+
urlObj.password = gitToken;
|
|
1328
|
+
authUrl = urlObj.toString();
|
|
1329
|
+
emit("progress", `Using authenticated clone for ${urlObj.hostname}`);
|
|
1330
|
+
} catch {
|
|
1331
|
+
authUrl = repoUrl.replace("https://", `https://x-access-token:${gitToken}@`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const { execSync } = await import("child_process");
|
|
1336
|
+
const cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0", HOME: process.env.HOME || "/tmp" };
|
|
1337
|
+
const execOpts = { stdio: "pipe", timeout: 300000, encoding: "utf8", env: cloneEnv };
|
|
1338
|
+
let cloneBranch = "main";
|
|
1339
|
+
|
|
1340
|
+
// Configure safe directory
|
|
1341
|
+
try { execSync(`git config --global --add safe.directory "${wsPath}"`, execOpts); } catch {}
|
|
1342
|
+
|
|
1343
|
+
if (fs.existsSync(path.join(wsPath, ".git"))) {
|
|
1344
|
+
emit("progress", "Workspace exists, updating...");
|
|
1345
|
+
try {
|
|
1346
|
+
if (gitToken && gitToken.length > 5) {
|
|
1347
|
+
execSync(`git remote set-url origin "${authUrl}"`, { ...execOpts, cwd: wsPath });
|
|
1348
|
+
}
|
|
1349
|
+
execSync("git fetch origin", { ...execOpts, cwd: wsPath });
|
|
1350
|
+
cloneBranch = execSync("git branch --show-current", { ...execOpts, cwd: wsPath }).trim() || "main";
|
|
1351
|
+
execSync(`git reset --hard origin/${cloneBranch}`, { ...execOpts, cwd: wsPath });
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
const safeMsg = gitToken ? e.message.replace(gitToken, "***") : e.message;
|
|
1354
|
+
emit("progress", `Update failed (${safeMsg.substring(0, 100)}), re-cloning...`);
|
|
1355
|
+
fs.rmSync(wsPath, { recursive: true, force: true });
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (!fs.existsSync(path.join(wsPath, ".git"))) {
|
|
1360
|
+
// Remove existing empty dir before clone (git clone needs non-existent or empty target)
|
|
1361
|
+
if (fs.existsSync(wsPath)) {
|
|
1362
|
+
const contents = fs.readdirSync(wsPath);
|
|
1363
|
+
if (contents.length === 0) fs.rmSync(wsPath, { recursive: true });
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
for (const b of ["main", "master", "develop"]) {
|
|
1367
|
+
try {
|
|
1368
|
+
emit("progress", `Trying branch: ${b}...`);
|
|
1369
|
+
execSync(`git clone --depth=1 --branch ${b} "${authUrl}" "${wsPath}"`, execOpts);
|
|
1370
|
+
cloneBranch = b;
|
|
1371
|
+
emit("progress", `Cloned on branch: ${b}`);
|
|
1372
|
+
break;
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
const stderr = (e.stderr || e.stdout || e.message || "").toString();
|
|
1375
|
+
const safeMsg = gitToken ? stderr.replace(new RegExp(gitToken, 'g'), "***") : stderr;
|
|
1376
|
+
console.warn(`[Clone] Branch ${b} failed: ${safeMsg.substring(0, 500)}`);
|
|
1377
|
+
emit("progress", `Branch ${b} failed: ${safeMsg.substring(0, 200)}`);
|
|
1378
|
+
// Clean up partial clone
|
|
1379
|
+
if (fs.existsSync(wsPath)) fs.rmSync(wsPath, { recursive: true, force: true });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Fallback: clone without specifying branch
|
|
1384
|
+
if (!fs.existsSync(path.join(wsPath, ".git"))) {
|
|
1385
|
+
try {
|
|
1386
|
+
emit("progress", "Trying default branch...");
|
|
1387
|
+
execSync(`git clone --depth=1 "${authUrl}" "${wsPath}" 2>&1`, execOpts);
|
|
1388
|
+
cloneBranch = execSync("git branch --show-current", { ...execOpts, cwd: wsPath }).trim() || "main";
|
|
1389
|
+
emit("progress", `Cloned on default branch: ${cloneBranch}`);
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
const stderr = (e.stderr || e.stdout || e.message || "").toString();
|
|
1392
|
+
const safeMsg = gitToken ? stderr.replace(new RegExp(gitToken, 'g'), "***") : stderr;
|
|
1393
|
+
console.error(`[Clone] Default branch failed: ${safeMsg.substring(0, 500)}`);
|
|
1394
|
+
emit("progress", `Default branch failed: ${safeMsg.substring(0, 200)}`);
|
|
1395
|
+
if (fs.existsSync(wsPath)) fs.rmSync(wsPath, { recursive: true, force: true });
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (!fs.existsSync(path.join(wsPath, ".git"))) {
|
|
1401
|
+
emit("error", { error: "Clone failed for all branches. Check repo URL and permissions." });
|
|
1402
|
+
return res.end();
|
|
1403
|
+
}
|
|
1404
|
+
emit("progress", `Repository cloned on branch: ${cloneBranch}`);
|
|
1405
|
+
|
|
1406
|
+
// STEP 2 - Detect stack
|
|
1407
|
+
emit("progress", "AI analysing project structure...");
|
|
1408
|
+
const meta = await detectProject(wsPath);
|
|
1409
|
+
emit("progress", `Stack: ${meta.language} / ${meta.buildTool} / ${meta.framework || "unknown"}`);
|
|
1410
|
+
|
|
1411
|
+
// STEP 3 - Generate Dockerfile if missing
|
|
1412
|
+
const dockerfilePath = path.join(wsPath, "Dockerfile");
|
|
1413
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
1414
|
+
emit("progress", `Generating Dockerfile for ${meta.language}...`);
|
|
1415
|
+
const content = generateDockerfileContent(meta.language, meta.buildTool);
|
|
1416
|
+
fs.writeFileSync(dockerfilePath, content, "utf8");
|
|
1417
|
+
emit("progress", "Dockerfile generated");
|
|
1418
|
+
} else {
|
|
1419
|
+
emit("progress", "Dockerfile already exists");
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// STEP 4 - Register
|
|
1423
|
+
emit("progress", `Provider: ${provider}. Workspace ready.`);
|
|
1424
|
+
|
|
1425
|
+
emit("progress", "AI Cloud Engineering setup complete!");
|
|
1426
|
+
emit("complete", {
|
|
1427
|
+
success: true, workspaceId, wsPath, provider, branch: cloneBranch,
|
|
1428
|
+
stack: { language: meta.language, buildTool: meta.buildTool, framework: meta.framework },
|
|
1429
|
+
message: "Setup complete. Monitoring is now active."
|
|
1430
|
+
});
|
|
1431
|
+
res.end();
|
|
1432
|
+
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
console.error("[Setup] Error:", err.message);
|
|
1435
|
+
emit("error", { error: err.message });
|
|
1436
|
+
res.end();
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
function generateDockerfileContent(language, buildTool) {
|
|
1441
|
+
if (language === "java" && (buildTool === "maven" || buildTool === "mvn"))
|
|
1442
|
+
return `FROM maven:3.9-eclipse-temurin-17 AS build\nWORKDIR /app\nCOPY pom.xml .\nRUN mvn dependency:go-offline -B\nCOPY src ./src\nRUN mvn clean package -DskipTests -B\nFROM eclipse-temurin:17-jre-jammy\nWORKDIR /app\nCOPY --from=build /app/target/*.jar app.jar\nEXPOSE 8080\nENTRYPOINT ["java", "-jar", "app.jar"]\n`;
|
|
1443
|
+
if (language === "java")
|
|
1444
|
+
return `FROM gradle:8-jdk17 AS build\nWORKDIR /app\nCOPY . .\nRUN gradle build -x test\nFROM eclipse-temurin:17-jre-jammy\nWORKDIR /app\nCOPY --from=build /app/build/libs/*.jar app.jar\nEXPOSE 8080\nENTRYPOINT ["java", "-jar", "app.jar"]\n`;
|
|
1445
|
+
if (language === "node" || language === "javascript" || language === "typescript")
|
|
1446
|
+
return `FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --only=production\nCOPY . .\nEXPOSE 8080\nENV PORT=8080\nCMD ["node", "src/index.js"]\n`;
|
|
1447
|
+
if (language === "python")
|
|
1448
|
+
return `FROM python:3.11-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nEXPOSE 8080\nCMD ["python", "app.py"]\n`;
|
|
1449
|
+
if (language === "go")
|
|
1450
|
+
return `FROM golang:1.21-alpine AS build\nWORKDIR /app\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\nRUN CGO_ENABLED=0 go build -o main .\nFROM alpine:3.18\nCOPY --from=build /app/main /app/main\nEXPOSE 8080\nCMD ["/app/main"]\n`;
|
|
1451
|
+
return `FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .\nEXPOSE 8080\nCMD ["./start.sh"]\n`;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/** Detect project stack from files (pom.xml, package.json, etc.) */
|
|
1455
|
+
app.post("/workspace/detect", async (req, res) => {
|
|
1456
|
+
const wsRoot = resolveWorkspaceRoot(req);
|
|
1457
|
+
if (!wsRoot) return res.status(400).json({ ok: false, error: "workspace not set" });
|
|
1458
|
+
try {
|
|
1459
|
+
const meta = await detectProject(wsRoot);
|
|
1460
|
+
const fs = await import("fs");
|
|
1461
|
+
const path = await import("path");
|
|
1462
|
+
res.json({
|
|
1463
|
+
ok: true,
|
|
1464
|
+
language: meta.language || "unknown",
|
|
1465
|
+
buildTool: meta.buildTool || "unknown",
|
|
1466
|
+
framework: meta.framework || "unknown",
|
|
1467
|
+
hasDockerfile: fs.existsSync(path.join(wsRoot, "Dockerfile")),
|
|
1468
|
+
path: wsRoot
|
|
1469
|
+
});
|
|
1470
|
+
} catch (e) {
|
|
1471
|
+
res.json({ ok: false, language: "unknown", buildTool: "unknown",
|
|
1472
|
+
framework: "unknown", error: e.message });
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1099
1476
|
/** Escreve/salva conteudo de arquivo no workspace */
|
|
1100
1477
|
app.post("/workspace/write-file", async (req, res) => {
|
|
1101
1478
|
const workspaceRoot = resolveWorkspaceRoot(req);
|
|
@@ -1826,6 +2203,85 @@ app.get("/workspace/test-local/state", async (req, res) => {
|
|
|
1826
2203
|
});
|
|
1827
2204
|
});
|
|
1828
2205
|
|
|
2206
|
+
/**
|
|
2207
|
+
* POST /workspace/git/prepare
|
|
2208
|
+
* Prepares a clean workspace for a new fix:
|
|
2209
|
+
* 1. Detects main branch (main, master, develop)
|
|
2210
|
+
* 2. git fetch origin
|
|
2211
|
+
* 3. git checkout main branch
|
|
2212
|
+
* 4. git reset --hard origin/main branch
|
|
2213
|
+
*/
|
|
2214
|
+
app.post("/workspace/git/prepare", async (req, res) => {
|
|
2215
|
+
const wsRoot = resolveWorkspaceRoot(req);
|
|
2216
|
+
if (!wsRoot) {
|
|
2217
|
+
return res.status(400).json({ ok: false, error: "workspace not set" });
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const { gitToken, repoUrl } = req.body || {};
|
|
2221
|
+
console.log("[git-prepare] Starting workspace preparation...");
|
|
2222
|
+
|
|
2223
|
+
try {
|
|
2224
|
+
const { execSync } = await import("child_process");
|
|
2225
|
+
const execGit = (cmd) => {
|
|
2226
|
+
try {
|
|
2227
|
+
return { ok: true, output: execSync(cmd, {
|
|
2228
|
+
cwd: wsRoot, encoding: "utf8", timeout: 60000,
|
|
2229
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
2230
|
+
}).trim() };
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
return { ok: false, output: (err.stderr || err.stdout || err.message || "unknown").toString().trim() };
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
|
|
2236
|
+
execGit(`git config --global --add safe.directory "${wsRoot}"`);
|
|
2237
|
+
|
|
2238
|
+
if (gitToken && repoUrl) {
|
|
2239
|
+
const urlWithToken = repoUrl.replace("https://", `https://x-access-token:${gitToken}@`);
|
|
2240
|
+
execGit(`git remote set-url origin "${urlWithToken}"`);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
console.log("[git-prepare] Fetching origin...");
|
|
2244
|
+
execGit("git fetch origin --prune");
|
|
2245
|
+
|
|
2246
|
+
let mainBranch = null;
|
|
2247
|
+
for (const branch of ["main", "master", "develop", "dev"]) {
|
|
2248
|
+
if (execGit(`git rev-parse --verify origin/${branch}`).ok) {
|
|
2249
|
+
mainBranch = branch;
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (!mainBranch) {
|
|
2254
|
+
const cur = execGit("git rev-parse --abbrev-ref HEAD");
|
|
2255
|
+
mainBranch = cur.ok ? cur.output : "main";
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
const prevBranch = (execGit("git rev-parse --abbrev-ref HEAD").output || "unknown");
|
|
2259
|
+
const prevCommit = (execGit("git rev-parse --short HEAD").output || "unknown");
|
|
2260
|
+
|
|
2261
|
+
console.log(`[git-prepare] ${prevBranch}@${prevCommit} -> ${mainBranch}`);
|
|
2262
|
+
|
|
2263
|
+
let checkout = execGit(`git checkout ${mainBranch}`);
|
|
2264
|
+
if (!checkout.ok) checkout = execGit(`git checkout -f ${mainBranch}`);
|
|
2265
|
+
if (!checkout.ok) {
|
|
2266
|
+
return res.json({ ok: false, error: `Could not checkout ${mainBranch}: ${checkout.output}`,
|
|
2267
|
+
mainBranch, previousBranch: prevBranch, previousCommit: prevCommit });
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
execGit(`git reset --hard origin/${mainBranch}`);
|
|
2271
|
+
execGit('git clean -fd --exclude="*.env" --exclude=".env*"');
|
|
2272
|
+
|
|
2273
|
+
const newCommit = (execGit("git rev-parse --short HEAD").output || "unknown");
|
|
2274
|
+
console.log(`[git-prepare] Ready: ${mainBranch}@${newCommit}`);
|
|
2275
|
+
|
|
2276
|
+
res.json({ ok: true, mainBranch, currentCommit: newCommit,
|
|
2277
|
+
previousBranch: prevBranch, previousCommit: prevCommit,
|
|
2278
|
+
message: `Workspace prepared: on ${mainBranch} at ${newCommit}` });
|
|
2279
|
+
} catch (err) {
|
|
2280
|
+
console.error("[git-prepare] Error:", err.message);
|
|
2281
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
2282
|
+
}
|
|
2283
|
+
});
|
|
2284
|
+
|
|
1829
2285
|
/**
|
|
1830
2286
|
* POST /workspace/test-local/compile
|
|
1831
2287
|
* Compiles the project without starting server
|
package/src/utils/exec-utils.js
CHANGED
|
@@ -148,12 +148,16 @@ export async function commandExists(cmd) {
|
|
|
148
148
|
export async function compileAndTest({ language, buildTool, cwd }) {
|
|
149
149
|
const steps = [];
|
|
150
150
|
|
|
151
|
-
// Java + Maven
|
|
151
|
+
// Java + Maven — no -q flag so compilation errors are visible to AI
|
|
152
152
|
if (language === 'java' && buildTool === 'maven') {
|
|
153
|
-
const
|
|
154
|
-
steps.push({ name: '
|
|
153
|
+
const compile = await run('mvn', ['compile', '-DskipTests', '-e'], cwd);
|
|
154
|
+
steps.push({ name: 'compile', ...compile });
|
|
155
|
+
|
|
156
|
+
if (compile.code !== 0) {
|
|
157
|
+
return { success: false, steps };
|
|
158
|
+
}
|
|
155
159
|
|
|
156
|
-
const test = await run('mvn', ['test', '-
|
|
160
|
+
const test = await run('mvn', ['test', '-e'], cwd);
|
|
157
161
|
steps.push({ name: 'test', ...test });
|
|
158
162
|
|
|
159
163
|
return {
|