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/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: Kubernetes, OpenShift, Docker Compose ║
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.0.0"
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
- # Security: Create non-root user
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
- # Security: Install security updates
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"]
@@ -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'
@@ -52,8 +52,8 @@ services:
52
52
  # Volumes
53
53
  # ─────────────────────────────────────────
54
54
  volumes:
55
- # Project source code (read-only for security)
56
- - ${PROJECT_PATH:-/path/to/projects}:/workspace:ro
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.19",
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) {