deepdebug-local-agent 1.0.19 → 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 +34 -16
- package/Dockerfile.full +63 -0
- package/cloudbuild-agent-qa.yaml +43 -0
- package/docker-compose.yml +2 -2
- 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 +106 -18
- package/src/local-agent-source.tar.gz +0 -0
- package/src/runtimes/java/maven-runtime.js +4 -3
- package/src/server.js +889 -54
- package/src/utils/exec-utils.js +8 -4
- package/src/vercel-proxy.js +226 -0
- package/tunnel-manager.js +70 -0
package/Dockerfile
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ║ DeepDebug Local Agent - Enterprise Docker ║
|
|
3
3
|
# ║ ║
|
|
4
4
|
# ║ Security-hardened container for enterprise deployments ║
|
|
5
|
-
# ║ Supports:
|
|
5
|
+
# ║ Supports: Cloud Run with NFS (GCP Filestore) ║
|
|
6
6
|
# ╚══════════════════════════════════════════════════════════════╝
|
|
7
7
|
|
|
8
8
|
# ===========================================
|
|
@@ -12,14 +12,9 @@ FROM node:20-alpine AS builder
|
|
|
12
12
|
|
|
13
13
|
WORKDIR /build
|
|
14
14
|
|
|
15
|
-
# Copy package files first (better cache)
|
|
16
15
|
COPY package*.json ./
|
|
17
|
-
|
|
18
|
-
# Install dependencies (production only)
|
|
19
|
-
# Using npm install instead of npm ci for repos without package-lock.json
|
|
20
16
|
RUN npm install --omit=dev && npm cache clean --force
|
|
21
17
|
|
|
22
|
-
# Copy source code
|
|
23
18
|
COPY src/ ./src/
|
|
24
19
|
|
|
25
20
|
# ===========================================
|
|
@@ -27,48 +22,71 @@ COPY src/ ./src/
|
|
|
27
22
|
# ===========================================
|
|
28
23
|
FROM node:20-alpine AS production
|
|
29
24
|
|
|
30
|
-
# Security: Add labels for compliance
|
|
31
25
|
LABEL org.opencontainers.image.title="DeepDebug Local Agent"
|
|
32
26
|
LABEL org.opencontainers.image.description="Enterprise debugging agent for code analysis"
|
|
33
27
|
LABEL org.opencontainers.image.vendor="InspTech AI"
|
|
34
|
-
LABEL org.opencontainers.image.version="1.
|
|
28
|
+
LABEL org.opencontainers.image.version="1.2.0"
|
|
35
29
|
LABEL org.opencontainers.image.licenses="Proprietary"
|
|
36
30
|
LABEL security.scan.required="true"
|
|
37
31
|
|
|
38
|
-
#
|
|
32
|
+
# Create non-root user
|
|
39
33
|
RUN addgroup -g 1001 -S deepdebug && \
|
|
40
34
|
adduser -u 1001 -S deepdebug -G deepdebug
|
|
41
35
|
|
|
42
|
-
#
|
|
36
|
+
# Install dependencies including Java 17 for Maven wrapper support
|
|
43
37
|
RUN apk update && \
|
|
44
38
|
apk upgrade --no-cache && \
|
|
45
39
|
apk add --no-cache \
|
|
46
40
|
dumb-init \
|
|
47
41
|
git \
|
|
48
42
|
curl \
|
|
43
|
+
nfs-utils \
|
|
44
|
+
openjdk17-jdk \
|
|
45
|
+
maven \
|
|
46
|
+
python3 \
|
|
49
47
|
&& rm -rf /var/cache/apk/*
|
|
50
48
|
|
|
49
|
+
# Set JAVA_HOME so ./mvnw can find Java
|
|
50
|
+
# Alpine stores JVM under java-17-openjdk with arch suffix - use readlink to resolve
|
|
51
|
+
RUN ln -sf $(dirname $(dirname $(readlink -f $(which java)))) /usr/local/java-home
|
|
52
|
+
ENV JAVA_HOME=/usr/local/java-home
|
|
53
|
+
ENV PATH="$JAVA_HOME/bin:$PATH"
|
|
54
|
+
|
|
55
|
+
# Create NFS mount point with correct ownership
|
|
56
|
+
RUN mkdir -p /mnt/workspaces && \
|
|
57
|
+
chown deepdebug:deepdebug /mnt/workspaces
|
|
58
|
+
|
|
59
|
+
# Install cloudflared for tunnel support (Vibe preview)
|
|
60
|
+
RUN wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
|
|
61
|
+
-O /usr/local/bin/cloudflared && \
|
|
62
|
+
chmod +x /usr/local/bin/cloudflared && \
|
|
63
|
+
cloudflared --version
|
|
64
|
+
|
|
65
|
+
# Install GitHub CLI (gh) for PR creation
|
|
66
|
+
RUN wget -q https://github.com/cli/cli/releases/download/v2.63.2/gh_2.63.2_linux_amd64.tar.gz \
|
|
67
|
+
-O /tmp/gh.tar.gz && \
|
|
68
|
+
tar -xzf /tmp/gh.tar.gz -C /tmp && \
|
|
69
|
+
mv /tmp/gh_2.63.2_linux_amd64/bin/gh /usr/local/bin/gh && \
|
|
70
|
+
chmod +x /usr/local/bin/gh && \
|
|
71
|
+
rm -rf /tmp/gh* && \
|
|
72
|
+
gh --version
|
|
73
|
+
|
|
51
74
|
WORKDIR /app
|
|
52
75
|
|
|
53
|
-
# Copy from builder with correct ownership
|
|
54
76
|
COPY --from=builder --chown=deepdebug:deepdebug /build/node_modules ./node_modules
|
|
55
77
|
COPY --from=builder --chown=deepdebug:deepdebug /build/src ./src
|
|
56
78
|
COPY --chown=deepdebug:deepdebug package*.json ./
|
|
57
79
|
|
|
58
|
-
# Security: Switch to non-root user
|
|
59
80
|
USER deepdebug
|
|
60
81
|
|
|
61
|
-
# Health check
|
|
62
82
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
63
83
|
CMD curl -f http://localhost:5055/health || exit 1
|
|
64
84
|
|
|
65
|
-
# Environment
|
|
66
85
|
ENV NODE_ENV=production
|
|
67
86
|
ENV PORT=5055
|
|
87
|
+
ENV WORKSPACES_MOUNT=/mnt/workspaces
|
|
68
88
|
|
|
69
|
-
# Expose port
|
|
70
89
|
EXPOSE 5055
|
|
71
90
|
|
|
72
|
-
# Security: Use dumb-init to handle signals properly
|
|
73
91
|
ENTRYPOINT ["dumb-init", "--"]
|
|
74
92
|
CMD ["node", "src/server.js"]
|
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"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
steps:
|
|
2
|
+
# Step 1: Build Docker image
|
|
3
|
+
- name: 'gcr.io/cloud-builders/docker'
|
|
4
|
+
args:
|
|
5
|
+
- 'build'
|
|
6
|
+
- '--platform=linux/amd64'
|
|
7
|
+
- '-t'
|
|
8
|
+
- 'us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-qa:latest'
|
|
9
|
+
- '.'
|
|
10
|
+
timeout: '600s'
|
|
11
|
+
|
|
12
|
+
# Step 2: Push to Artifact Registry
|
|
13
|
+
- name: 'gcr.io/cloud-builders/docker'
|
|
14
|
+
args:
|
|
15
|
+
- 'push'
|
|
16
|
+
- 'us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-qa:latest'
|
|
17
|
+
|
|
18
|
+
# Step 3: Deploy to Cloud Run QA
|
|
19
|
+
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
|
20
|
+
entrypoint: 'gcloud'
|
|
21
|
+
args:
|
|
22
|
+
- 'run'
|
|
23
|
+
- 'deploy'
|
|
24
|
+
- 'deepdebug-local-agent-qa'
|
|
25
|
+
- '--image=us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-qa:latest'
|
|
26
|
+
- '--region=us-central1'
|
|
27
|
+
- '--platform=managed'
|
|
28
|
+
- '--allow-unauthenticated'
|
|
29
|
+
- '--memory=1Gi'
|
|
30
|
+
- '--cpu=1'
|
|
31
|
+
- '--min-instances=1'
|
|
32
|
+
- '--max-instances=3'
|
|
33
|
+
- '--port=5055'
|
|
34
|
+
- '--timeout=300'
|
|
35
|
+
- '--update-env-vars=NODE_ENV=qa'
|
|
36
|
+
|
|
37
|
+
images:
|
|
38
|
+
- 'us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-qa:latest'
|
|
39
|
+
|
|
40
|
+
options:
|
|
41
|
+
logging: CLOUD_LOGGING_ONLY
|
|
42
|
+
|
|
43
|
+
timeout: '1200s'
|
package/docker-compose.yml
CHANGED
|
@@ -52,8 +52,8 @@ services:
|
|
|
52
52
|
# Volumes
|
|
53
53
|
# ─────────────────────────────────────────
|
|
54
54
|
volumes:
|
|
55
|
-
# Project source code
|
|
56
|
-
-
|
|
55
|
+
# Project source code - writable so agent can apply patches
|
|
56
|
+
- /Users/macintosh/IdeaProjects:/workspace
|
|
57
57
|
# Temp directory for container writes
|
|
58
58
|
- agent-tmp:/tmp
|
|
59
59
|
|
|
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) {
|