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.
@@ -0,0 +1,63 @@
1
+ # ================================================================
2
+ # DeepDebug Local Agent - Full Stack (with build tools)
3
+ # ================================================================
4
+ # This image includes Java 17, Maven, Node.js, Python, and Git
5
+ # for compilation and testing in Cloud Engineer provisioned
6
+ # environments. Used when the client has OAuth connected and
7
+ # we provision a compilation container in their cloud.
8
+ # ================================================================
9
+
10
+ # Stage 1: Build Node.js agent
11
+ FROM node:20-slim AS builder
12
+
13
+ WORKDIR /build
14
+ COPY package*.json ./
15
+ RUN npm install --omit=dev && npm cache clean --force
16
+ COPY src/ ./src/
17
+
18
+ # Stage 2: Full runtime with build tools
19
+ FROM node:20-slim
20
+
21
+ # Install Java 17 + Maven for Spring Boot projects
22
+ RUN apt-get update && \
23
+ apt-get install -y --no-install-recommends \
24
+ openjdk-17-jdk-headless \
25
+ maven \
26
+ git \
27
+ curl \
28
+ openssh-client \
29
+ python3 \
30
+ python3-pip \
31
+ && rm -rf /var/lib/apt/lists/*
32
+
33
+ ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
34
+ ENV PATH=$PATH:$JAVA_HOME/bin:/usr/share/maven/bin
35
+
36
+ # Verify all tools are installed
37
+ RUN java -version && mvn -version && node -v && git --version
38
+
39
+ # Git safe directory for NFS mounted workspaces
40
+ RUN git config --global --add safe.directory '*'
41
+
42
+ # Create non-root user
43
+ RUN groupadd -r deepdebug && useradd -r -g deepdebug -m deepdebug
44
+
45
+ WORKDIR /app
46
+
47
+ # Copy built Node.js agent
48
+ COPY --from=builder /build/node_modules ./node_modules
49
+ COPY --from=builder /build/src ./src
50
+ COPY --from=builder /build/package*.json ./
51
+
52
+ # Maven dependencies cache directory
53
+ RUN mkdir -p /home/deepdebug/.m2 && chown -R deepdebug:deepdebug /home/deepdebug
54
+
55
+ # Workspace mount point
56
+ RUN mkdir -p /mnt/workspaces && chown -R deepdebug:deepdebug /mnt/workspaces
57
+
58
+ EXPOSE 8080
59
+
60
+ ENV NODE_ENV=production
61
+ ENV PORT=8080
62
+
63
+ CMD ["node", "src/server.js"]
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepdebug-local-agent",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "DeepDebug Local Agent - AI-powered code debugging assistant",
5
5
  "private": false,
6
6
  "type": "module",
@@ -37,6 +37,7 @@
37
37
  "cors": "^2.8.5",
38
38
  "express": "^4.19.2",
39
39
  "js-yaml": "^4.1.0",
40
+ "pg": "^8.20.0",
40
41
  "pidusage": "^3.0.2",
41
42
  "properties-reader": "^2.3.0",
42
43
  "strip-ansi": "^7.1.0",
@@ -45,8 +46,8 @@
45
46
  },
46
47
  "devDependencies": {
47
48
  "esbuild": "^0.20.0",
48
- "pkg": "^5.8.1",
49
- "eslint": "^8.57.0"
49
+ "eslint": "^8.57.0",
50
+ "pkg": "^5.8.1"
50
51
  },
51
52
  "engines": {
52
53
  "node": ">=18.0.0"
@@ -17,12 +17,18 @@ export class LanguageDetector {
17
17
  findMarkers() {
18
18
  const markers = {
19
19
  java: this.files.some(f => f.name === 'pom.xml' || f.name === 'build.gradle'),
20
+ kotlin: this.files.some(f => f.name === 'build.gradle.kts'),
20
21
  node: this.files.some(f => f.name === 'package.json'),
21
22
  python: this.files.some(f => f.name === 'requirements.txt' || f.name === 'pyproject.toml'),
22
- dotnet: this.files.some(f => f.name.endsWith('.csproj') || f.name.endsWith('.sln')),
23
+ dotnet: this.files.some(f => f.name?.endsWith('.csproj') || f.name?.endsWith('.sln')),
23
24
  go: this.files.some(f => f.name === 'go.mod'),
24
25
  php: this.files.some(f => f.name === 'composer.json'),
25
- ruby: this.files.some(f => f.name === 'Gemfile')
26
+ ruby: this.files.some(f => f.name === 'Gemfile' || f.name === 'Rakefile'),
27
+ rust: this.files.some(f => f.name === 'Cargo.toml'),
28
+ dart: this.files.some(f => f.name === 'pubspec.yaml'),
29
+ elixir: this.files.some(f => f.name === 'mix.exs'),
30
+ docker: this.files.some(f => f.name === 'Dockerfile'),
31
+ static: this.files.some(f => f.name === 'index.html'),
26
32
  };
27
33
  return markers;
28
34
  }
@@ -37,20 +43,24 @@ export class LanguageDetector {
37
43
  }
38
44
 
39
45
  determinePrimaryLanguage(markers, extensions) {
40
- // Prioridade: markers > extensões
41
46
  const detected = Object.entries(markers)
42
47
  .filter(([_, exists]) => exists)
43
48
  .map(([lang]) => lang);
44
49
 
45
50
  if (detected.length === 1) return detected[0];
46
51
  if (detected.length > 1) {
47
- // Múltiplas linguagens, escolhe por extensões
48
- const sortedExts = Object.entries(extensions)
49
- .sort((a, b) => b[1] - a[1]);
50
- return this.mapExtToLang(sortedExts[0]?.[0]);
52
+ // Filter out meta-markers (docker, static) if real languages exist
53
+ const realLangs = detected.filter(l => l !== 'docker' && l !== 'static');
54
+ if (realLangs.length === 1) return realLangs[0];
55
+ if (realLangs.length > 1) {
56
+ const sortedExts = Object.entries(extensions)
57
+ .sort((a, b) => b[1] - a[1]);
58
+ return this.mapExtToLang(sortedExts[0]?.[0]) || realLangs[0];
59
+ }
60
+ return detected[0];
51
61
  }
52
62
 
53
- // Fallback: extensão mais comum
63
+ // Fallback: most common extension
54
64
  const sortedExts = Object.entries(extensions)
55
65
  .sort((a, b) => b[1] - a[1]);
56
66
  return this.mapExtToLang(sortedExts[0]?.[0]) || 'unknown';
@@ -59,6 +69,7 @@ export class LanguageDetector {
59
69
  mapExtToLang(ext) {
60
70
  const map = {
61
71
  '.java': 'java',
72
+ '.kt': 'kotlin',
62
73
  '.js': 'node',
63
74
  '.ts': 'node',
64
75
  '.jsx': 'node',
@@ -67,7 +78,13 @@ export class LanguageDetector {
67
78
  '.cs': 'dotnet',
68
79
  '.go': 'go',
69
80
  '.php': 'php',
70
- '.rb': 'ruby'
81
+ '.rb': 'ruby',
82
+ '.rs': 'rust',
83
+ '.dart': 'dart',
84
+ '.ex': 'elixir',
85
+ '.exs': 'elixir',
86
+ '.swift': 'swift',
87
+ '.scala': 'scala',
71
88
  };
72
89
  return map[ext] || null;
73
90
  }
@@ -77,13 +94,13 @@ export class LanguageDetector {
77
94
  return Object.keys(extensions)
78
95
  .map(ext => this.mapExtToLang(ext))
79
96
  .filter(lang => lang && lang !== primary)
80
- .filter((v, i, a) => a.indexOf(v) === i); // unique
97
+ .filter((v, i, a) => a.indexOf(v) === i);
81
98
  }
82
99
 
83
100
  calculateConfidence(markers) {
84
101
  const markerCount = Object.values(markers).filter(Boolean).length;
85
102
  if (markerCount === 0) return 0.3;
86
103
  if (markerCount === 1) return 0.9;
87
- return 0.7; // Múltiplos markers = projeto polyglot
104
+ return 0.7;
88
105
  }
89
- }
106
+ }
package/src/detectors.js CHANGED
@@ -2,27 +2,310 @@ import path from "path";
2
2
  import { exists, readFile } from "./fs-utils.js";
3
3
 
4
4
  export async function detectProject(root) {
5
+ const fs = (await import("fs")).promises;
6
+ const pathModule = await import("path");
7
+ const pathExt = pathModule.default || pathModule;
8
+
9
+ // Phase 1: Check well-known marker files
10
+ // Swift / iOS markers go FIRST so a Package.swift at the root beats a
11
+ // stray Java pom.xml sitting next to an xcworkspace (VisionHammer slice
12
+ // 5 hardening).
5
13
  const markers = [
14
+ { file: "Package.swift", lang: "swift", buildTool: "swiftpm" },
15
+ { file: "Podfile", lang: "swift", buildTool: "cocoapods" },
16
+ { file: "Podfile.lock", lang: "swift", buildTool: "cocoapods" },
6
17
  { file: "pom.xml", lang: "java", buildTool: "maven" },
7
18
  { file: "build.gradle", lang: "java", buildTool: "gradle" },
19
+ { file: "build.gradle.kts", lang: "kotlin", buildTool: "gradle" },
8
20
  { file: "package.json", lang: "node", buildTool: "npm" },
9
21
  { file: "pyproject.toml", lang: "python", buildTool: "poetry" },
10
22
  { file: "requirements.txt", lang: "python", buildTool: "pip" },
11
- { file: "go.mod", lang: "go", buildTool: "go" }
23
+ { file: "go.mod", lang: "go", buildTool: "go" },
24
+ { file: "Gemfile", lang: "ruby", buildTool: "bundler" },
25
+ { file: "Rakefile", lang: "ruby", buildTool: "rake" },
26
+ { file: "pubspec.yaml", lang: "dart", buildTool: "flutter" },
27
+ { file: "composer.json", lang: "php", buildTool: "composer" },
28
+ { file: "Cargo.toml", lang: "rust", buildTool: "cargo" },
29
+ { file: "mix.exs", lang: "elixir", buildTool: "mix" },
12
30
  ];
13
31
 
14
32
  for (const m of markers) {
15
- if (await exists(path.join(root, m.file))) {
16
- return { language: m.lang, buildTool: m.buildTool, marker: m.file };
33
+ const fullPath = path.join(root, m.file);
34
+ if (await exists(fullPath)) {
35
+ const result = { language: m.lang, buildTool: m.buildTool, marker: m.file };
36
+
37
+ // Refine: build.gradle.kts with kotlin plugin -> kotlin
38
+ if (m.file === "build.gradle" || m.file === "build.gradle.kts") {
39
+ try {
40
+ const content = await fs.readFile(fullPath, "utf8");
41
+ if (content.includes("org.jetbrains.kotlin") || content.includes("kotlin(")) {
42
+ result.language = "kotlin";
43
+ }
44
+ } catch (_) {}
45
+ }
46
+
47
+ // Refine: Gemfile -> detect Rails vs Sinatra
48
+ if (m.file === "Gemfile") {
49
+ try {
50
+ const content = await fs.readFile(fullPath, "utf8");
51
+ if (content.includes("rails")) result.framework = "rails";
52
+ else if (content.includes("sinatra")) result.framework = "sinatra";
53
+ } catch (_) {}
54
+ }
55
+
56
+ // Refine: package.json -> detect framework
57
+ if (m.file === "package.json") {
58
+ try {
59
+ const pkg = JSON.parse(await fs.readFile(fullPath, "utf8"));
60
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
61
+ if (deps["next"]) result.framework = "next";
62
+ else if (deps["nuxt"]) result.framework = "nuxt";
63
+ else if (deps["react"]) result.framework = "react";
64
+ else if (deps["vue"]) result.framework = "vue";
65
+ else if (deps["express"]) result.framework = "express";
66
+ else if (deps["fastify"]) result.framework = "fastify";
67
+ else if (deps["@angular/core"]) result.framework = "angular";
68
+ else if (deps["svelte"]) result.framework = "svelte";
69
+ } catch (_) {}
70
+ }
71
+
72
+ // Refine: pom.xml -> detect Spring Boot
73
+ if (m.file === "pom.xml") {
74
+ try {
75
+ const content = await fs.readFile(fullPath, "utf8");
76
+ if (content.includes("spring-boot")) result.framework = "spring-boot";
77
+ else if (content.includes("quarkus")) result.framework = "quarkus";
78
+ else if (content.includes("micronaut")) result.framework = "micronaut";
79
+ } catch (_) {}
80
+ }
81
+
82
+ // Refine: pubspec.yaml -> detect Flutter
83
+ if (m.file === "pubspec.yaml") {
84
+ try {
85
+ const content = await fs.readFile(fullPath, "utf8");
86
+ if (content.includes("flutter:")) result.framework = "flutter";
87
+ } catch (_) {}
88
+ }
89
+
90
+ return result;
17
91
  }
18
92
  }
19
93
 
20
- // .NET: procura primeiro nível
21
- const dotnet = (await (await import("fs")).promises.readdir(root))
22
- .find(f => f.endsWith(".csproj") || f.endsWith(".sln"));
23
- if (dotnet) return { language: ".net", buildTool: "dotnet", marker: dotnet };
94
+ // Phase 2: native mobile — iOS / Android
95
+ let rootFiles;
96
+ try {
97
+ rootFiles = await fs.readdir(root);
98
+ } catch (_) {
99
+ return { language: "unknown", buildTool: "unknown", marker: null };
100
+ }
101
+
102
+ const hasRootFile = name => rootFiles.includes(name);
103
+ const hasRootDirSuffix = suffix => rootFiles.some(f => f.endsWith(suffix));
104
+
105
+ if (hasRootFile("Podfile") || hasRootFile("Podfile.lock") || hasRootFile("Package.swift")
106
+ || hasRootDirSuffix(".xcodeproj") || hasRootDirSuffix(".xcworkspace")) {
107
+ const framework = await detectAppleFramework(root, fs, pathExt);
108
+ return {
109
+ language: "ios",
110
+ buildTool: "xcodebuild",
111
+ framework,
112
+ marker: hasRootFile("Podfile") ? "Podfile"
113
+ : hasRootFile("Package.swift") ? "Package.swift"
114
+ : rootFiles.find(f => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"))
115
+ };
116
+ }
117
+
118
+ // Phase 2b: recursive scan up to 2 levels for *.xcodeproj / *.xcworkspace
119
+ // so iOS apps living under ios/, apps/ios/, etc. are still detected.
120
+ try {
121
+ for (const entry of rootFiles) {
122
+ const level1 = path.join(root, entry);
123
+ let level1Stat;
124
+ try { level1Stat = await fs.stat(level1); } catch (_) { continue; }
125
+ if (!level1Stat.isDirectory()) continue;
126
+ let level1Entries;
127
+ try { level1Entries = await fs.readdir(level1); } catch (_) { continue; }
128
+ if (level1Entries.some(f => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"))) {
129
+ const framework = await detectAppleFramework(level1, fs, pathExt);
130
+ return {
131
+ language: "ios",
132
+ buildTool: "xcodebuild",
133
+ framework,
134
+ marker: level1Entries.find(f => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"))
135
+ };
136
+ }
137
+ for (const sub of level1Entries) {
138
+ const level2 = path.join(level1, sub);
139
+ let level2Stat;
140
+ try { level2Stat = await fs.stat(level2); } catch (_) { continue; }
141
+ if (!level2Stat.isDirectory()) continue;
142
+ let level2Entries;
143
+ try { level2Entries = await fs.readdir(level2); } catch (_) { continue; }
144
+ if (level2Entries.some(f => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"))) {
145
+ const framework = await detectAppleFramework(level2, fs, pathExt);
146
+ return {
147
+ language: "ios",
148
+ buildTool: "xcodebuild",
149
+ framework,
150
+ marker: level2Entries.find(f => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"))
151
+ };
152
+ }
153
+ }
154
+ }
155
+ } catch (_) {}
156
+
157
+ if (hasRootFile("gradlew") || hasRootFile("gradlew.bat") || hasRootFile("settings.gradle")
158
+ || hasRootFile("settings.gradle.kts") || hasRootFile("gradle.properties")
159
+ || await exists(path.join(root, "app", "src", "main", "AndroidManifest.xml"))
160
+ || await exists(path.join(root, "AndroidManifest.xml"))) {
161
+ const framework = await detectAndroidFramework(root, fs, pathExt);
162
+ return {
163
+ language: "android",
164
+ buildTool: "gradle",
165
+ framework,
166
+ marker: hasRootFile("gradlew") ? "gradlew"
167
+ : hasRootFile("settings.gradle") ? "settings.gradle"
168
+ : hasRootFile("settings.gradle.kts") ? "settings.gradle.kts"
169
+ : "AndroidManifest.xml"
170
+ };
171
+ }
172
+
173
+ // Phase 3: .NET — check for .csproj or .sln in root
174
+
175
+ const dotnetFile = rootFiles.find(f => f.endsWith(".csproj") || f.endsWith(".sln"));
176
+ if (dotnetFile) {
177
+ return { language: "dotnet", buildTool: "dotnet", marker: dotnetFile };
178
+ }
179
+
180
+ // Phase 4: Docker-only — Dockerfile without any other language marker
181
+ if (rootFiles.includes("Dockerfile") || rootFiles.includes("docker-compose.yml") || rootFiles.includes("docker-compose.yaml")) {
182
+ return { language: "docker", buildTool: "docker", marker: "Dockerfile" };
183
+ }
184
+
185
+ // Phase 5: Static site — index.html without backend markers
186
+ if (rootFiles.includes("index.html")) {
187
+ return { language: "static", buildTool: "none", marker: "index.html" };
188
+ }
189
+
190
+ // Phase 6: Infer from file extensions
191
+ const extCount = {};
192
+ for (const f of rootFiles) {
193
+ const ext = path.extname(f).toLowerCase();
194
+ if (ext) extCount[ext] = (extCount[ext] || 0) + 1;
195
+ }
196
+ // Also scan one level deep (src/ or lib/)
197
+ for (const dir of ["src", "lib", "app", "cmd"]) {
198
+ try {
199
+ const subFiles = await fs.readdir(path.join(root, dir));
200
+ for (const f of subFiles) {
201
+ const ext = path.extname(f).toLowerCase();
202
+ if (ext) extCount[ext] = (extCount[ext] || 0) + 1;
203
+ }
204
+ } catch (_) {}
205
+ }
206
+
207
+ const extMap = {
208
+ ".java": "java", ".kt": "kotlin", ".scala": "scala",
209
+ ".js": "node", ".ts": "node", ".jsx": "node", ".tsx": "node",
210
+ ".py": "python",
211
+ ".go": "go",
212
+ ".rb": "ruby",
213
+ ".rs": "rust",
214
+ ".cs": "dotnet",
215
+ ".php": "php",
216
+ ".dart": "dart",
217
+ ".ex": "elixir", ".exs": "elixir",
218
+ ".swift": "swift",
219
+ };
220
+
221
+ const langCount = {};
222
+ for (const [ext, count] of Object.entries(extCount)) {
223
+ const lang = extMap[ext];
224
+ if (lang) langCount[lang] = (langCount[lang] || 0) + count;
225
+ }
226
+
227
+ const sorted = Object.entries(langCount).sort((a, b) => b[1] - a[1]);
228
+ if (sorted.length > 0) {
229
+ const [lang, count] = sorted[0];
230
+ console.log(`[detectProject] Inferred ${lang} from ${count} files (extensions: ${JSON.stringify(extCount)})`);
231
+ return { language: lang, buildTool: "unknown", marker: `inferred from ${count} .${lang} files` };
232
+ }
233
+
234
+ // Phase 7: Unknown — log root files for diagnostics
235
+ console.warn(`[detectProject] Unknown stack in ${root}. Root files: ${rootFiles.join(", ")}`);
236
+ return { language: "unknown", buildTool: "unknown", marker: null, rootFiles };
237
+ }
238
+
239
+ async function detectAppleFramework(root, fs, pathExt) {
240
+ const swiftSources = await collectFiles(root, fs, pathExt, file => file.endsWith(".swift"), 4);
241
+ for (const file of swiftSources.slice(0, 12)) {
242
+ try {
243
+ const content = await fs.readFile(file, "utf8");
244
+ if (content.includes("import SwiftUI") || content.includes("SwiftUI")) return "swiftui";
245
+ if (content.includes("UIKit")) return "uikit";
246
+ } catch (_) {}
247
+ }
248
+ return "ios";
249
+ }
250
+
251
+ async function detectAndroidFramework(root, fs, pathExt) {
252
+ const candidateFiles = [
253
+ path.join(root, "app", "build.gradle"),
254
+ path.join(root, "app", "build.gradle.kts"),
255
+ path.join(root, "build.gradle"),
256
+ path.join(root, "build.gradle.kts"),
257
+ ];
258
+
259
+ for (const file of candidateFiles) {
260
+ try {
261
+ const content = await fs.readFile(file, "utf8");
262
+ if (content.includes("androidx.compose") || content.includes("compose = true")) {
263
+ return "jetpack-compose";
264
+ }
265
+ } catch (_) {}
266
+ }
267
+
268
+ const kotlinSources = await collectFiles(root, fs, pathExt, file => file.endsWith(".kt"), 4);
269
+ for (const file of kotlinSources.slice(0, 12)) {
270
+ try {
271
+ const content = await fs.readFile(file, "utf8");
272
+ if (content.includes("androidx.compose")) return "jetpack-compose";
273
+ } catch (_) {}
274
+ }
275
+
276
+ return "android";
277
+ }
278
+
279
+ async function collectFiles(root, fs, pathExt, predicate, maxDepth) {
280
+ const results = [];
281
+ const ignored = new Set([".git", "node_modules", "build", ".build", "DerivedData", ".gradle"]);
282
+
283
+ async function walk(dir, depth) {
284
+ if (depth > maxDepth) return;
285
+ let entries = [];
286
+ try {
287
+ entries = await fs.readdir(dir, { withFileTypes: true });
288
+ } catch (_) {
289
+ return;
290
+ }
291
+
292
+ for (const entry of entries) {
293
+ const fullPath = path.join(dir, entry.name);
294
+ if (entry.isDirectory()) {
295
+ if (!ignored.has(entry.name)) {
296
+ await walk(fullPath, depth + 1);
297
+ }
298
+ continue;
299
+ }
300
+
301
+ if (predicate(fullPath, entry)) {
302
+ results.push(fullPath);
303
+ }
304
+ }
305
+ }
24
306
 
25
- return { language: "unknown", buildTool: "unknown", marker: null };
307
+ await walk(root, 0);
308
+ return results;
26
309
  }
27
310
 
28
311
  export async function readText(root, relPath) {
package/src/exec-utils.js CHANGED
@@ -56,23 +56,25 @@ export async function compileAndTest({ language, buildTool, cwd, skipTests = fal
56
56
 
57
57
  try {
58
58
  if (language === "java" && buildTool === "maven") {
59
- // Prefer ./mvnw wrapper over system mvn
59
+ // Prefer system mvn (installed in Docker), fallback to ./mvnw
60
60
  const fs = await import("fs");
61
61
  const path = await import("path");
62
+ const checkMvn = await run("which", ["mvn"], cwd, 5000);
62
63
  const hasMvnw = fs.existsSync(path.join(cwd, "mvnw"));
63
- const mvnCmd = hasMvnw ? "./mvnw" : "mvn";
64
-
65
- // Verify build tool is available before attempting
66
- if (!hasMvnw) {
67
- const check = await run("which mvn", [], cwd);
68
- if (check.code !== 0) {
69
- return {
70
- code: 1,
71
- stdout: "",
72
- stderr: "Maven not found: neither ./mvnw nor mvn is available in this environment.",
73
- duration: Date.now() - start
74
- };
75
- }
64
+ let mvnCmd;
65
+ if (checkMvn.code === 0) {
66
+ mvnCmd = "mvn"; // system Maven (always works, no permission issues)
67
+ } else if (hasMvnw) {
68
+ // Fix permission on NFS-mounted mvnw
69
+ await run("chmod", ["+x", path.join(cwd, "mvnw")], cwd, 5000);
70
+ mvnCmd = "./mvnw";
71
+ } else {
72
+ return {
73
+ code: 1,
74
+ stdout: "",
75
+ stderr: "Maven not found: neither mvn nor ./mvnw is available.",
76
+ duration: Date.now() - start
77
+ };
76
78
  }
77
79
 
78
80
  // Fix for maven-compiler-plugin 3.8.1 + Java 17:
@@ -154,9 +156,10 @@ export async function compileAndTest({ language, buildTool, cwd, skipTests = fal
154
156
  }
155
157
  }
156
158
 
159
+ // -e flag shows full error stack traces from Maven
157
160
  const args = skipTests
158
- ? ["compile", "-DskipTests", ...javaCompatFlags]
159
- : ["test", ...javaCompatFlags];
161
+ ? ["compile", "-DskipTests", "-e", ...javaCompatFlags]
162
+ : ["test", "-e", ...javaCompatFlags];
160
163
 
161
164
  console.log(`[BUILD] [COMPILE] Running: ${mvnCmd} ${args.join(" ")}`);
162
165
  result = await run(mvnCmd, args, cwd, 10 * 60 * 1000, mavenEnv);
Binary file
@@ -121,9 +121,10 @@ export class MavenRuntime extends BaseRuntime {
121
121
 
122
122
  async build(workspacePath, options = {}) {
123
123
  const startTime = Date.now();
124
- const skipTests = options.skipTests ? '-DskipTests' : '';
125
- const args = ['compile', '-q'];
126
- if (skipTests) args.push(skipTests);
124
+ const skipTests = options.skipTests !== false;
125
+ // No -q flag: compilation errors must be visible to the AI agent
126
+ const args = ['compile', '-e'];
127
+ if (skipTests) args.push('-DskipTests');
127
128
 
128
129
  const result = await run('mvn', args, workspacePath);
129
130