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/Dockerfile.full
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
49
|
-
"
|
|
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
|
|
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
|
-
//
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
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);
|
|
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;
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|