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/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
@@ -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 clean = await run('mvn', ['clean', '-q'], cwd);
154
- steps.push({ name: 'clean', ...clean });
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', '-q'], cwd);
160
+ const test = await run('mvn', ['test', '-e'], cwd);
157
161
  steps.push({ name: 'test', ...test });
158
162
 
159
163
  return {