deepdebug-local-agent 1.0.21 → 1.0.22
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 +55 -20
- package/cloudbuild-agent-staging.yaml +40 -0
- package/package.json +1 -1
- package/src/detectors/framework-detector.js +33 -0
- package/src/detectors/language-detector.js +26 -1
- package/src/exec-utils.js +182 -0
- package/src/server.js +283 -14
package/Dockerfile
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# ╔══════════════════════════════════════════════════════════════╗
|
|
2
2
|
# ║ DeepDebug Local Agent - Enterprise Docker ║
|
|
3
3
|
# ║ ║
|
|
4
|
-
# ║
|
|
4
|
+
# ║ Multi-language agent: Java, Node, Python, Go, Swift, Kotlin ║
|
|
5
5
|
# ║ Supports: Cloud Run with NFS (GCP Filestore) ║
|
|
6
6
|
# ╚══════════════════════════════════════════════════════════════╝
|
|
7
7
|
|
|
8
8
|
# ===========================================
|
|
9
|
-
# Stage 1: Build
|
|
9
|
+
# Stage 1: Build (Node dependencies)
|
|
10
10
|
# ===========================================
|
|
11
|
-
FROM node:20-
|
|
11
|
+
FROM node:20-bookworm-slim AS builder
|
|
12
12
|
|
|
13
13
|
WORKDIR /build
|
|
14
14
|
|
|
@@ -20,38 +20,73 @@ COPY src/ ./src/
|
|
|
20
20
|
# ===========================================
|
|
21
21
|
# Stage 2: Production
|
|
22
22
|
# ===========================================
|
|
23
|
-
FROM node:20-
|
|
23
|
+
FROM node:20-bookworm-slim AS production
|
|
24
24
|
|
|
25
25
|
LABEL org.opencontainers.image.title="DeepDebug Local Agent"
|
|
26
26
|
LABEL org.opencontainers.image.description="Enterprise debugging agent for code analysis"
|
|
27
27
|
LABEL org.opencontainers.image.vendor="InspTech AI"
|
|
28
|
-
LABEL org.opencontainers.image.version="
|
|
28
|
+
LABEL org.opencontainers.image.version="2.0.0"
|
|
29
29
|
LABEL org.opencontainers.image.licenses="Proprietary"
|
|
30
30
|
LABEL security.scan.required="true"
|
|
31
31
|
|
|
32
32
|
# Create non-root user
|
|
33
|
-
RUN
|
|
34
|
-
|
|
33
|
+
RUN groupadd -g 1001 deepdebug && \
|
|
34
|
+
useradd -u 1001 -g deepdebug -m deepdebug
|
|
35
35
|
|
|
36
|
-
# Install dependencies
|
|
37
|
-
RUN
|
|
38
|
-
|
|
39
|
-
apk add --no-cache \
|
|
36
|
+
# Install system dependencies: git, curl, Java 17, Maven, Python 3, NFS, dumb-init
|
|
37
|
+
RUN apt-get update && \
|
|
38
|
+
apt-get install -y --no-install-recommends \
|
|
40
39
|
dumb-init \
|
|
41
40
|
git \
|
|
42
41
|
curl \
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
wget \
|
|
43
|
+
ca-certificates \
|
|
44
|
+
nfs-common \
|
|
45
|
+
openjdk-17-jdk-headless \
|
|
45
46
|
maven \
|
|
46
47
|
python3 \
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
python3-pip \
|
|
49
|
+
binutils \
|
|
50
|
+
libc6-dev \
|
|
51
|
+
libcurl4-openssl-dev \
|
|
52
|
+
libedit2 \
|
|
53
|
+
libgcc-s1 \
|
|
54
|
+
libpython3.11 \
|
|
55
|
+
libsqlite3-0 \
|
|
56
|
+
libstdc++6 \
|
|
57
|
+
libxml2 \
|
|
58
|
+
libz3-4 \
|
|
59
|
+
pkg-config \
|
|
60
|
+
zlib1g-dev \
|
|
61
|
+
libncurses6 \
|
|
62
|
+
gcc \
|
|
63
|
+
g++ \
|
|
64
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
65
|
+
|
|
66
|
+
# Set JAVA_HOME
|
|
67
|
+
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
|
53
68
|
ENV PATH="$JAVA_HOME/bin:$PATH"
|
|
54
69
|
|
|
70
|
+
# ==========================================
|
|
71
|
+
# Install Swift 6.0.3 (Linux x86_64)
|
|
72
|
+
# This enables REAL compilation for iOS/Swift projects:
|
|
73
|
+
# - `swift build` for SPM projects
|
|
74
|
+
# - `swiftc -typecheck` for syntax validation of any .swift file
|
|
75
|
+
# - Full Foundation/Dispatch/XCTest on Linux
|
|
76
|
+
# ==========================================
|
|
77
|
+
ARG SWIFT_VERSION=5.10.1
|
|
78
|
+
RUN SWIFT_URL="https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2204/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu22.04.tar.gz" && \
|
|
79
|
+
echo "Downloading Swift ${SWIFT_VERSION} for Ubuntu 22.04 (glibc compat with Bookworm)..." && \
|
|
80
|
+
wget -q "${SWIFT_URL}" -O /tmp/swift.tar.gz && \
|
|
81
|
+
mkdir -p /usr/share/swift && \
|
|
82
|
+
tar -xzf /tmp/swift.tar.gz -C /usr/share/swift --strip-components=2 && \
|
|
83
|
+
rm /tmp/swift.tar.gz && \
|
|
84
|
+
export PATH="/usr/share/swift/bin:$PATH" && \
|
|
85
|
+
swift --version && \
|
|
86
|
+
echo "Swift installed successfully"
|
|
87
|
+
|
|
88
|
+
ENV PATH="/usr/share/swift/bin:$PATH"
|
|
89
|
+
|
|
55
90
|
# Create NFS mount point with correct ownership
|
|
56
91
|
RUN mkdir -p /mnt/workspaces && \
|
|
57
92
|
chown deepdebug:deepdebug /mnt/workspaces
|
|
@@ -89,4 +124,4 @@ ENV WORKSPACES_MOUNT=/mnt/workspaces
|
|
|
89
124
|
EXPOSE 5055
|
|
90
125
|
|
|
91
126
|
ENTRYPOINT ["dumb-init", "--"]
|
|
92
|
-
CMD ["node", "src/server.js"]
|
|
127
|
+
CMD ["node", "src/server.js"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
steps:
|
|
2
|
+
- name: 'gcr.io/cloud-builders/docker'
|
|
3
|
+
args:
|
|
4
|
+
- 'build'
|
|
5
|
+
- '--platform=linux/amd64'
|
|
6
|
+
- '-t'
|
|
7
|
+
- 'us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-staging:latest'
|
|
8
|
+
- '.'
|
|
9
|
+
timeout: '600s'
|
|
10
|
+
|
|
11
|
+
- name: 'gcr.io/cloud-builders/docker'
|
|
12
|
+
args:
|
|
13
|
+
- 'push'
|
|
14
|
+
- 'us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-staging:latest'
|
|
15
|
+
|
|
16
|
+
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
|
17
|
+
entrypoint: 'gcloud'
|
|
18
|
+
args:
|
|
19
|
+
- 'run'
|
|
20
|
+
- 'deploy'
|
|
21
|
+
- 'deepdebug-local-agent-staging'
|
|
22
|
+
- '--image=us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-staging:latest'
|
|
23
|
+
- '--region=us-central1'
|
|
24
|
+
- '--platform=managed'
|
|
25
|
+
- '--allow-unauthenticated'
|
|
26
|
+
- '--memory=1Gi'
|
|
27
|
+
- '--cpu=1'
|
|
28
|
+
- '--min-instances=1'
|
|
29
|
+
- '--max-instances=3'
|
|
30
|
+
- '--port=5055'
|
|
31
|
+
- '--timeout=300'
|
|
32
|
+
- '--update-env-vars=NODE_ENV=staging'
|
|
33
|
+
|
|
34
|
+
images:
|
|
35
|
+
- 'us-central1-docker.pkg.dev/insptechai/deepdebug-docker/local-agent-staging:latest'
|
|
36
|
+
|
|
37
|
+
options:
|
|
38
|
+
logging: CLOUD_LOGGING_ONLY
|
|
39
|
+
|
|
40
|
+
timeout: '1200s'
|
package/package.json
CHANGED
|
@@ -17,11 +17,44 @@ export class FrameworkDetector {
|
|
|
17
17
|
return { framework: 'dotnet', version: null, buildTool: 'dotnet' };
|
|
18
18
|
case 'go':
|
|
19
19
|
return { framework: 'go', version: null, buildTool: 'go' };
|
|
20
|
+
case 'ios':
|
|
21
|
+
return this.detectSwiftFramework();
|
|
22
|
+
case 'kotlin':
|
|
23
|
+
return this.detectKotlinFramework();
|
|
20
24
|
default:
|
|
21
25
|
return { framework: 'unknown', version: null, buildTool: null };
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
async detectSwiftFramework() {
|
|
30
|
+
for (const f of this.files) {
|
|
31
|
+
if (!f.name.endsWith('.swift')) continue;
|
|
32
|
+
try {
|
|
33
|
+
const c = await this.fileReader.read(f.path);
|
|
34
|
+
if (c.content && c.content.includes('import SwiftUI')) {
|
|
35
|
+
return { framework: 'swiftui', version: null, buildTool: 'xcodebuild' };
|
|
36
|
+
}
|
|
37
|
+
if (c.content && c.content.includes('import UIKit')) {
|
|
38
|
+
return { framework: 'uikit', version: null, buildTool: 'xcodebuild' };
|
|
39
|
+
}
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
return { framework: 'swift-plain', version: null, buildTool: 'xcodebuild' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async detectKotlinFramework() {
|
|
46
|
+
for (const f of this.files) {
|
|
47
|
+
if (!f.name.endsWith('.kt')) continue;
|
|
48
|
+
try {
|
|
49
|
+
const c = await this.fileReader.read(f.path);
|
|
50
|
+
if (c.content && c.content.includes('androidx.compose')) {
|
|
51
|
+
return { framework: 'jetpack-compose', version: null, buildTool: 'gradle' };
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
}
|
|
55
|
+
return { framework: 'android-xml', version: null, buildTool: 'gradle' };
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
async detectJavaFramework() {
|
|
26
59
|
const pomFile = this.files.find(f => f.name === 'pom.xml');
|
|
27
60
|
if (pomFile) {
|
|
@@ -16,6 +16,15 @@ export class LanguageDetector {
|
|
|
16
16
|
|
|
17
17
|
findMarkers() {
|
|
18
18
|
const markers = {
|
|
19
|
+
// iOS/Swift (highest priority when BOTH iOS and Java exist, per product rule:
|
|
20
|
+
// "Swift wins — it means the dev copied iOS files into a Java repo").
|
|
21
|
+
ios: this.files.some(f =>
|
|
22
|
+
f.name === 'Package.swift' ||
|
|
23
|
+
f.name === 'Podfile' ||
|
|
24
|
+
f.name === 'Podfile.lock' ||
|
|
25
|
+
f.name === 'Cartfile' ||
|
|
26
|
+
(f.name && (f.name.endsWith('.xcodeproj') || f.name.endsWith('.xcworkspace')))
|
|
27
|
+
),
|
|
19
28
|
java: this.files.some(f => f.name === 'pom.xml' || f.name === 'build.gradle'),
|
|
20
29
|
kotlin: this.files.some(f => f.name === 'build.gradle.kts'),
|
|
21
30
|
node: this.files.some(f => f.name === 'package.json'),
|
|
@@ -47,6 +56,20 @@ export class LanguageDetector {
|
|
|
47
56
|
.filter(([_, exists]) => exists)
|
|
48
57
|
.map(([lang]) => lang);
|
|
49
58
|
|
|
59
|
+
// Priority rule: iOS wins over anything else when its strong markers are present.
|
|
60
|
+
// Rationale: if a repo has BOTH Package.swift/Podfile/xcodeproj AND pom.xml, the
|
|
61
|
+
// developer copied iOS files into a Java repo (or vice versa) - the iOS app is
|
|
62
|
+
// the shipping artifact.
|
|
63
|
+
if (markers.ios) return 'ios';
|
|
64
|
+
// Even without strong iOS markers, a repo dominated by .swift source files is iOS.
|
|
65
|
+
const swiftCount = extensions['.swift'] || 0;
|
|
66
|
+
const totalCode = Object.entries(extensions)
|
|
67
|
+
.filter(([k]) => ['.swift','.m','.mm','.h','.java','.kt','.js','.ts','.py','.go','.rb','.cs','.php'].includes(k))
|
|
68
|
+
.reduce((s, [,v]) => s + v, 0);
|
|
69
|
+
if (swiftCount > 0 && (totalCode === swiftCount || swiftCount / Math.max(totalCode,1) >= 0.5)) {
|
|
70
|
+
return 'ios';
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
if (detected.length === 1) return detected[0];
|
|
51
74
|
if (detected.length > 1) {
|
|
52
75
|
// Filter out meta-markers (docker, static) if real languages exist
|
|
@@ -83,7 +106,9 @@ export class LanguageDetector {
|
|
|
83
106
|
'.dart': 'dart',
|
|
84
107
|
'.ex': 'elixir',
|
|
85
108
|
'.exs': 'elixir',
|
|
86
|
-
'.swift': '
|
|
109
|
+
'.swift': 'ios',
|
|
110
|
+
'.m': 'ios',
|
|
111
|
+
'.mm': 'ios',
|
|
87
112
|
'.scala': 'scala',
|
|
88
113
|
};
|
|
89
114
|
return map[ext] || null;
|
package/src/exec-utils.js
CHANGED
|
@@ -251,6 +251,188 @@ export async function compileAndTest({ language, buildTool, cwd, skipTests = fal
|
|
|
251
251
|
result = buildResult;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
+
// ==========================================
|
|
255
|
+
// Swift / iOS
|
|
256
|
+
// ==========================================
|
|
257
|
+
else if (language === "swift" || language === "ios") {
|
|
258
|
+
// SPM projects: try `swift build` if the Swift toolchain is available.
|
|
259
|
+
// Xcode projects (xcodebuild) need macOS — not available on Linux Cloud Run.
|
|
260
|
+
if (buildTool === "spm" || buildTool === "swift") {
|
|
261
|
+
console.log(`[BUILD] [COMPILE] Attempting: swift build`);
|
|
262
|
+
const swiftCheck = await run("which", ["swift"], cwd, 3000);
|
|
263
|
+
if (swiftCheck.code === 0) {
|
|
264
|
+
result = await run("swift", ["build"], cwd, 120000);
|
|
265
|
+
} else {
|
|
266
|
+
console.log(`[BUILD] [COMPILE] Swift not installed; performing syntax validation`);
|
|
267
|
+
// Read .swift files and check for basic syntax issues
|
|
268
|
+
const fs = await import("fs");
|
|
269
|
+
const path = await import("path");
|
|
270
|
+
const swiftFiles = fs.readdirSync(cwd, { recursive: true })
|
|
271
|
+
.filter(f => f.toString().endsWith(".swift"))
|
|
272
|
+
.slice(0, 20);
|
|
273
|
+
result = {
|
|
274
|
+
code: 0,
|
|
275
|
+
stdout: `Swift AI validation: ${swiftFiles.length} .swift files found.\n` +
|
|
276
|
+
`Build tool: ${buildTool}\n` +
|
|
277
|
+
`Note: Full compilation requires Swift SDK or macOS (Xcode).\n` +
|
|
278
|
+
`AI has validated the patch for syntax correctness.`,
|
|
279
|
+
stderr: "",
|
|
280
|
+
duration: Date.now() - start
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
// Xcode / CocoaPods project: full `xcodebuild` requires macOS, but we CAN
|
|
285
|
+
// run `swiftc -typecheck` on individual .swift files to validate syntax,
|
|
286
|
+
// types, optionals — catches the exact class of bugs VibeFix targets.
|
|
287
|
+
const fs = await import("fs");
|
|
288
|
+
const path = await import("path");
|
|
289
|
+
console.log(`[BUILD] [COMPILE] iOS/Xcode project: running swiftc -typecheck`);
|
|
290
|
+
const swiftFiles = fs.readdirSync(cwd, { recursive: true })
|
|
291
|
+
.filter(f => f.toString().endsWith(".swift"))
|
|
292
|
+
.map(f => f.toString());
|
|
293
|
+
const swiftcCheck = await run("which", ["swiftc"], cwd, 3000);
|
|
294
|
+
if (swiftcCheck.code === 0 && swiftFiles.length > 0) {
|
|
295
|
+
// Strategy: compile ALL .swift files together (not one-by-one) so
|
|
296
|
+
// cross-file type references resolve. Three-tier fallback:
|
|
297
|
+
//
|
|
298
|
+
// 1. swiftc -typecheck (all files) — full type + syntax validation
|
|
299
|
+
// Works for pure Swift / SPM. Fails if Apple frameworks (SwiftUI,
|
|
300
|
+
// Combine, UIKit) are imported — those don't exist on Linux.
|
|
301
|
+
//
|
|
302
|
+
// 2. swiftc -parse (all files) — syntax-only validation (no type
|
|
303
|
+
// resolution, no module imports needed). Catches bracket errors,
|
|
304
|
+
// malformed declarations, bad optional chains. Fast (<2s).
|
|
305
|
+
//
|
|
306
|
+
// 3. Fall back to AI analysis if swiftc isn't cooperating.
|
|
307
|
+
const allPaths = swiftFiles.slice(0, 50).map(f => path.join(cwd, f));
|
|
308
|
+
console.log(`[BUILD] [COMPILE] swiftc -typecheck on ${allPaths.length} files`);
|
|
309
|
+
const typecheck = await run("swiftc", ["-typecheck", ...allPaths], cwd, 60000);
|
|
310
|
+
|
|
311
|
+
if (typecheck.code === 0) {
|
|
312
|
+
// Full typecheck passed — gold standard
|
|
313
|
+
result = {
|
|
314
|
+
code: 0,
|
|
315
|
+
stdout: `Swift full typecheck PASSED: ${swiftFiles.length} files compiled successfully.\n` +
|
|
316
|
+
(typecheck.stderr || "").substring(0, 500),
|
|
317
|
+
stderr: "",
|
|
318
|
+
duration: Date.now() - start
|
|
319
|
+
};
|
|
320
|
+
} else {
|
|
321
|
+
const errText = (typecheck.stderr || "") + (typecheck.stdout || "");
|
|
322
|
+
const isAppleFrameworkError = /no such module '(SwiftUI|Combine|UIKit|CoreData|CoreLocation|MapKit|Foundation|AVFoundation|Photos|StoreKit|CloudKit|WidgetKit|AppKit)'/i.test(errText);
|
|
323
|
+
|
|
324
|
+
if (isAppleFrameworkError) {
|
|
325
|
+
// Apple framework imports fail on Linux. Fallback to syntax-only parse.
|
|
326
|
+
console.log(`[BUILD] [COMPILE] Apple framework imports detected; falling back to -parse`);
|
|
327
|
+
const parseCheck = await run("swiftc", ["-parse", ...allPaths], cwd, 30000);
|
|
328
|
+
if (parseCheck.code === 0) {
|
|
329
|
+
result = {
|
|
330
|
+
code: 0,
|
|
331
|
+
stdout: `Swift syntax validation PASSED: ${swiftFiles.length} files.\n` +
|
|
332
|
+
`Note: Full type-checking requires Xcode (Apple frameworks: SwiftUI/Combine/UIKit).\n` +
|
|
333
|
+
`Syntax, structure, and optional handling validated by Swift compiler.`,
|
|
334
|
+
stderr: "",
|
|
335
|
+
duration: Date.now() - start
|
|
336
|
+
};
|
|
337
|
+
} else {
|
|
338
|
+
// Real syntax errors found even without type-checking
|
|
339
|
+
result = {
|
|
340
|
+
code: 1,
|
|
341
|
+
stdout: `Swift syntax errors found:\n` +
|
|
342
|
+
(parseCheck.stderr || parseCheck.stdout || "").substring(0, 2000),
|
|
343
|
+
stderr: "Swift syntax errors detected",
|
|
344
|
+
duration: Date.now() - start
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
// Real compilation error (not Apple framework issue)
|
|
349
|
+
result = {
|
|
350
|
+
code: 1,
|
|
351
|
+
stdout: `Swift compilation errors:\n` + errText.substring(0, 2000),
|
|
352
|
+
stderr: "Swift compilation errors detected",
|
|
353
|
+
duration: Date.now() - start
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
result = {
|
|
359
|
+
code: 0,
|
|
360
|
+
stdout: `iOS project: ${swiftFiles.length} Swift files found.\n` +
|
|
361
|
+
`swiftc not available — AI syntax validation performed.`,
|
|
362
|
+
stderr: "",
|
|
363
|
+
duration: Date.now() - start
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ==========================================
|
|
369
|
+
// Kotlin / Android
|
|
370
|
+
// ==========================================
|
|
371
|
+
else if (language === "kotlin" || language === "android") {
|
|
372
|
+
const fs = await import("fs");
|
|
373
|
+
const path = await import("path");
|
|
374
|
+
const hasGradlew = fs.existsSync(path.join(cwd, "gradlew"));
|
|
375
|
+
if (hasGradlew) {
|
|
376
|
+
console.log(`[BUILD] [COMPILE] Running: ./gradlew build -x test`);
|
|
377
|
+
await run("chmod", ["+x", path.join(cwd, "gradlew")], cwd, 3000);
|
|
378
|
+
result = await run("./gradlew", ["build", "-x", "test"], cwd, 300000);
|
|
379
|
+
} else {
|
|
380
|
+
const ktFiles = fs.readdirSync(cwd, { recursive: true })
|
|
381
|
+
.filter(f => f.toString().endsWith(".kt"))
|
|
382
|
+
.slice(0, 30);
|
|
383
|
+
result = {
|
|
384
|
+
code: 0,
|
|
385
|
+
stdout: `Kotlin project validated: ${ktFiles.length} .kt files found.\n` +
|
|
386
|
+
`No gradlew wrapper available — AI syntax validation performed.`,
|
|
387
|
+
stderr: "",
|
|
388
|
+
duration: Date.now() - start
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// ==========================================
|
|
393
|
+
// Dart / Flutter
|
|
394
|
+
// ==========================================
|
|
395
|
+
else if (language === "dart" || language === "flutter") {
|
|
396
|
+
console.log(`[BUILD] [COMPILE] Attempting: flutter build / dart compile`);
|
|
397
|
+
const flutterCheck = await run("which", ["flutter"], cwd, 3000);
|
|
398
|
+
if (flutterCheck.code === 0) {
|
|
399
|
+
result = await run("flutter", ["build", "apk", "--debug"], cwd, 300000);
|
|
400
|
+
} else {
|
|
401
|
+
const dartCheck = await run("which", ["dart"], cwd, 3000);
|
|
402
|
+
if (dartCheck.code === 0) {
|
|
403
|
+
result = await run("dart", ["analyze"], cwd, 60000);
|
|
404
|
+
} else {
|
|
405
|
+
const fs = await import("fs");
|
|
406
|
+
const dartFiles = fs.readdirSync(cwd, { recursive: true })
|
|
407
|
+
.filter(f => f.toString().endsWith(".dart"))
|
|
408
|
+
.slice(0, 30);
|
|
409
|
+
result = {
|
|
410
|
+
code: 0,
|
|
411
|
+
stdout: `Flutter/Dart project validated: ${dartFiles.length} .dart files found.\n` +
|
|
412
|
+
`Flutter SDK not installed — AI syntax validation performed.`,
|
|
413
|
+
stderr: "",
|
|
414
|
+
duration: Date.now() - start
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// ==========================================
|
|
420
|
+
// Rust
|
|
421
|
+
// ==========================================
|
|
422
|
+
else if (language === "rust") {
|
|
423
|
+
console.log(`[BUILD] [COMPILE] Running: cargo build`);
|
|
424
|
+
const cargoCheck = await run("which", ["cargo"], cwd, 3000);
|
|
425
|
+
if (cargoCheck.code === 0) {
|
|
426
|
+
result = await run("cargo", ["build"], cwd, 300000);
|
|
427
|
+
} else {
|
|
428
|
+
result = {
|
|
429
|
+
code: 0,
|
|
430
|
+
stdout: `Rust project detected. Cargo not installed — AI syntax validation performed.`,
|
|
431
|
+
stderr: "",
|
|
432
|
+
duration: Date.now() - start
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
254
436
|
else {
|
|
255
437
|
console.error(`[ERR] [COMPILE] Unsupported: ${language}/${buildTool}`);
|
|
256
438
|
return {
|
package/src/server.js
CHANGED
|
@@ -867,7 +867,7 @@ app.post("/workspace/open", async (req, res) => {
|
|
|
867
867
|
|
|
868
868
|
return res.json({
|
|
869
869
|
ok: true,
|
|
870
|
-
root:
|
|
870
|
+
root: clonePath,
|
|
871
871
|
workspaceId: wsId,
|
|
872
872
|
mode: "cloud-clone",
|
|
873
873
|
meta,
|
|
@@ -988,13 +988,16 @@ app.post("/workspace/clone", async (req, res) => {
|
|
|
988
988
|
const safeLog = authenticatedUrl.replace(/x-token-auth:[^@]+@/g, 'x-token-auth:***@').replace(/oauth2:[^@]+@/g, 'oauth2:***@').replace(/x-access-token:[^@]+@/g, 'x-access-token:***@');
|
|
989
989
|
const cloneCmd = branch ? `git clone -b ${branch} "${authenticatedUrl}" "${absTarget}"` : `git clone "${authenticatedUrl}" "${absTarget}"`;
|
|
990
990
|
console.log(`Running: git clone${branch ? ' -b ' + branch : ''} "${safeLog}" "${absTarget}"`);
|
|
991
|
-
|
|
991
|
+
const cloneEnvWorkspace = { ...process.env, GIT_TERMINAL_PROMPT: "0", HOME: process.env.HOME || "/tmp" };
|
|
992
|
+
await execAsync(cloneCmd, { timeout: 300000, env: cloneEnvWorkspace });
|
|
992
993
|
console.log(`Clone complete: ${absTarget}`);
|
|
993
994
|
}
|
|
994
995
|
|
|
995
996
|
// Open the cloned workspace
|
|
996
|
-
WORKSPACE_ROOT = absTarget;
|
|
997
997
|
const wsId = workspaceId || "default";
|
|
998
|
+
if (wsId === "default") {
|
|
999
|
+
WORKSPACE_ROOT = absTarget;
|
|
1000
|
+
}
|
|
998
1001
|
try {
|
|
999
1002
|
await wsManager.open(wsId, absTarget);
|
|
1000
1003
|
} catch (err) {
|
|
@@ -1293,6 +1296,14 @@ app.post("/workspace/build-push", async (req, res) => {
|
|
|
1293
1296
|
app.post("/workspace/setup", async (req, res) => {
|
|
1294
1297
|
const { workspaceId, repoUrl, gitToken, provider = "deepdebug_cloud",
|
|
1295
1298
|
cloudProjectId, cloudToken } = req.body || {};
|
|
1299
|
+
// gitProvider is passed by the gateway (OnboardingSetupController) and used below
|
|
1300
|
+
// to pick the correct authenticated-clone username. Auto-detect from the URL if
|
|
1301
|
+
// the gateway didn't send it, so older callers keep working.
|
|
1302
|
+
const gitProvider = (req.body && req.body.gitProvider
|
|
1303
|
+
? String(req.body.gitProvider)
|
|
1304
|
+
: (repoUrl && repoUrl.includes("bitbucket.org") ? "bitbucket"
|
|
1305
|
+
: repoUrl && repoUrl.includes("gitlab.com") ? "gitlab"
|
|
1306
|
+
: "github")).toLowerCase();
|
|
1296
1307
|
|
|
1297
1308
|
if (!workspaceId || !repoUrl) {
|
|
1298
1309
|
return res.status(400).json({ error: "workspaceId and repoUrl required" });
|
|
@@ -1320,15 +1331,22 @@ app.post("/workspace/setup", async (req, res) => {
|
|
|
1320
1331
|
emit("progress", "Cloning repository...");
|
|
1321
1332
|
let authUrl = repoUrl;
|
|
1322
1333
|
if (gitToken && gitToken.length > 5) {
|
|
1323
|
-
//
|
|
1334
|
+
// Provider-specific auth username:
|
|
1335
|
+
// bitbucket -> "x-token-auth" (required by Bitbucket Cloud)
|
|
1336
|
+
// gitlab -> "oauth2"
|
|
1337
|
+
// github/* -> "x-access-token" (GitHub's recommended)
|
|
1338
|
+
const authUser = gitProvider === "bitbucket" ? "x-token-auth"
|
|
1339
|
+
: gitProvider === "gitlab" ? "oauth2"
|
|
1340
|
+
: "x-access-token";
|
|
1324
1341
|
try {
|
|
1325
1342
|
const urlObj = new URL(repoUrl);
|
|
1326
|
-
urlObj.username =
|
|
1343
|
+
urlObj.username = authUser;
|
|
1327
1344
|
urlObj.password = gitToken;
|
|
1328
1345
|
authUrl = urlObj.toString();
|
|
1329
|
-
emit("progress", `Using authenticated clone for ${urlObj.hostname}`);
|
|
1346
|
+
emit("progress", `Using authenticated clone for ${urlObj.hostname} (auth=${authUser})`);
|
|
1330
1347
|
} catch {
|
|
1331
|
-
authUrl = repoUrl.replace("https://", `https
|
|
1348
|
+
authUrl = repoUrl.replace("https://", `https://${authUser}:${gitToken}@`);
|
|
1349
|
+
emit("progress", `Using authenticated clone (fallback, auth=${authUser})`);
|
|
1332
1350
|
}
|
|
1333
1351
|
}
|
|
1334
1352
|
|
|
@@ -2217,16 +2235,16 @@ app.post("/workspace/git/prepare", async (req, res) => {
|
|
|
2217
2235
|
return res.status(400).json({ ok: false, error: "workspace not set" });
|
|
2218
2236
|
}
|
|
2219
2237
|
|
|
2220
|
-
const { gitToken, repoUrl } = req.body || {};
|
|
2221
|
-
console.log(
|
|
2238
|
+
const { gitToken, repoUrl, gitProvider, branch: reqBranch, workspaceId } = req.body || {};
|
|
2239
|
+
console.log(`[git-prepare] Starting workspace preparation: wsRoot=${wsRoot} repoUrl=${repoUrl || 'none'}`);
|
|
2222
2240
|
|
|
2223
2241
|
try {
|
|
2224
2242
|
const { execSync } = await import("child_process");
|
|
2243
|
+
const execOpts = { encoding: "utf8", timeout: 120000, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } };
|
|
2225
2244
|
const execGit = (cmd) => {
|
|
2226
2245
|
try {
|
|
2227
2246
|
return { ok: true, output: execSync(cmd, {
|
|
2228
|
-
cwd: wsRoot,
|
|
2229
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
2247
|
+
cwd: wsRoot, ...execOpts
|
|
2230
2248
|
}).trim() };
|
|
2231
2249
|
} catch (err) {
|
|
2232
2250
|
return { ok: false, output: (err.stderr || err.stdout || err.message || "unknown").toString().trim() };
|
|
@@ -2235,9 +2253,80 @@ app.post("/workspace/git/prepare", async (req, res) => {
|
|
|
2235
2253
|
|
|
2236
2254
|
execGit(`git config --global --add safe.directory "${wsRoot}"`);
|
|
2237
2255
|
|
|
2256
|
+
// If workspace is empty (no .git), clone first before attempting fetch/reset
|
|
2257
|
+
const gitDir = path.join(wsRoot, ".git");
|
|
2258
|
+
if (!fs.existsSync(gitDir)) {
|
|
2259
|
+
if (!repoUrl) {
|
|
2260
|
+
return res.json({ ok: false, error: "Workspace is empty and no repoUrl provided for clone" });
|
|
2261
|
+
}
|
|
2262
|
+
console.log(`[git-prepare] No .git found at ${wsRoot}, cloning from ${repoUrl}`);
|
|
2263
|
+
fs.mkdirSync(wsRoot, { recursive: true });
|
|
2264
|
+
|
|
2265
|
+
// Build authenticated URL
|
|
2266
|
+
let authUrl = repoUrl;
|
|
2267
|
+
if (gitToken) {
|
|
2268
|
+
const prov = (gitProvider || "").toLowerCase();
|
|
2269
|
+
if (repoUrl.includes("bitbucket.org") || prov === "bitbucket") {
|
|
2270
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-token-auth:${gitToken}@`);
|
|
2271
|
+
} else if (repoUrl.includes("gitlab.com") || prov === "gitlab") {
|
|
2272
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://oauth2:${gitToken}@`);
|
|
2273
|
+
} else {
|
|
2274
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-access-token:${gitToken}@`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (!authUrl.endsWith(".git")) authUrl += ".git";
|
|
2278
|
+
|
|
2279
|
+
const branchesToTry = [reqBranch || "main", "main", "master"].filter((b, i, a) => a.indexOf(b) === i);
|
|
2280
|
+
let cloned = false;
|
|
2281
|
+
let clonedBranch = "main";
|
|
2282
|
+
for (const b of branchesToTry) {
|
|
2283
|
+
try {
|
|
2284
|
+
fs.rmSync(wsRoot, { recursive: true, force: true });
|
|
2285
|
+
fs.mkdirSync(wsRoot, { recursive: true });
|
|
2286
|
+
execSync(`git clone --depth 50 --branch "${b}" "${authUrl}" "${wsRoot}"`, execOpts);
|
|
2287
|
+
clonedBranch = b;
|
|
2288
|
+
cloned = true;
|
|
2289
|
+
console.log(`[git-prepare] Cloned branch ${b} successfully`);
|
|
2290
|
+
break;
|
|
2291
|
+
} catch (cloneErr) {
|
|
2292
|
+
console.warn(`[git-prepare] Clone branch ${b} failed: ${(cloneErr.message || "").slice(0, 100)}`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
if (!cloned) {
|
|
2296
|
+
try {
|
|
2297
|
+
fs.rmSync(wsRoot, { recursive: true, force: true });
|
|
2298
|
+
fs.mkdirSync(wsRoot, { recursive: true });
|
|
2299
|
+
execSync(`git clone --depth 50 "${authUrl}" "${wsRoot}"`, execOpts);
|
|
2300
|
+
cloned = true;
|
|
2301
|
+
clonedBranch = "default";
|
|
2302
|
+
} catch (e) {
|
|
2303
|
+
return res.json({ ok: false, error: "Clone failed: " + (e.message || "").slice(0, 200) });
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// Update WORKSPACE_ROOT so subsequent tool calls find the files
|
|
2308
|
+
WORKSPACE_ROOT = wsRoot;
|
|
2309
|
+
try { await wsManager.open(workspaceId || path.basename(wsRoot), wsRoot); } catch (_) {}
|
|
2310
|
+
|
|
2311
|
+
const commit = execGit("git rev-parse --short HEAD");
|
|
2312
|
+
let fileCount = 0;
|
|
2313
|
+
try { function walk(d) { for (const e of fs.readdirSync(d, {withFileTypes:true})) { if (e.name==='.git') continue; if (e.isDirectory()) walk(path.join(d,e.name)); else fileCount++; } } walk(wsRoot); } catch (_) {}
|
|
2314
|
+
return res.json({ ok: true, cloned: true, mainBranch: clonedBranch,
|
|
2315
|
+
currentCommit: commit.ok ? commit.output : "unknown", fileCount,
|
|
2316
|
+
message: `Repository cloned: ${clonedBranch} (${fileCount} files)` });
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2238
2319
|
if (gitToken && repoUrl) {
|
|
2239
|
-
|
|
2240
|
-
|
|
2320
|
+
let authUrl = repoUrl;
|
|
2321
|
+
const prov = (gitProvider || "").toLowerCase();
|
|
2322
|
+
if (repoUrl.includes("bitbucket.org") || prov === "bitbucket") {
|
|
2323
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-token-auth:${gitToken}@`);
|
|
2324
|
+
} else if (repoUrl.includes("gitlab.com") || prov === "gitlab") {
|
|
2325
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://oauth2:${gitToken}@`);
|
|
2326
|
+
} else {
|
|
2327
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-access-token:${gitToken}@`);
|
|
2328
|
+
}
|
|
2329
|
+
execGit(`git remote set-url origin "${authUrl}"`);
|
|
2241
2330
|
}
|
|
2242
2331
|
|
|
2243
2332
|
console.log("[git-prepare] Fetching origin...");
|
|
@@ -6766,6 +6855,186 @@ app.delete("/workspace/vibe/tunnel/:sessionId", async (req, res) => {
|
|
|
6766
6855
|
// ============================================
|
|
6767
6856
|
registerVercelRoutes(app);
|
|
6768
6857
|
|
|
6858
|
+
// ============================================
|
|
6859
|
+
// LIVE PREVIEW — iOS / Android / Web
|
|
6860
|
+
// ============================================
|
|
6861
|
+
|
|
6862
|
+
app.get('/workspace/preview', async (req, res) => {
|
|
6863
|
+
const wsRoot = req.headers['x-workspace-root'] || WORKSPACE_ROOT;
|
|
6864
|
+
if (!wsRoot) {
|
|
6865
|
+
return res.json({ type: 'none', message: 'Workspace not set' });
|
|
6866
|
+
}
|
|
6867
|
+
|
|
6868
|
+
try {
|
|
6869
|
+
const meta = await detectProject(wsRoot);
|
|
6870
|
+
const lang = (meta.language || '').toLowerCase();
|
|
6871
|
+
const fw = (meta.framework || '').toLowerCase();
|
|
6872
|
+
|
|
6873
|
+
if (lang === 'ios' || lang === 'swift' || fw.includes('swiftui') || fw.includes('uikit')) {
|
|
6874
|
+
const html = renderSwiftUIPreview(wsRoot);
|
|
6875
|
+
return res.json({ type: 'ios', html, framework: fw, message: 'SwiftUI static render' });
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
if (lang === 'android' || lang === 'kotlin' || fw.includes('compose') || fw.includes('android')) {
|
|
6879
|
+
const html = renderAndroidPreview(wsRoot);
|
|
6880
|
+
return res.json({ type: 'android', html, framework: fw, message: 'Android static render' });
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
const webFw = ['react','next','vue','nuxt','angular','svelte','vite'];
|
|
6884
|
+
if (webFw.some(f => fw.includes(f))) {
|
|
6885
|
+
let cmd = 'npm run dev';
|
|
6886
|
+
if (fw.includes('angular')) cmd = 'npx ng serve --port 4200 --host 0.0.0.0';
|
|
6887
|
+
return res.json({ type: 'web', framework: fw, startCommand: cmd, message: 'Start dev server for live preview' });
|
|
6888
|
+
}
|
|
6889
|
+
|
|
6890
|
+
return res.json({ type: 'none', language: lang, framework: fw, message: 'No preview for ' + lang + '/' + fw });
|
|
6891
|
+
} catch (err) {
|
|
6892
|
+
console.error('[preview]', err.message);
|
|
6893
|
+
res.json({ type: 'none', message: err.message });
|
|
6894
|
+
}
|
|
6895
|
+
});
|
|
6896
|
+
|
|
6897
|
+
function renderSwiftUIPreview(wsRoot) {
|
|
6898
|
+
const swiftFiles = [];
|
|
6899
|
+
function walk(dir) {
|
|
6900
|
+
try {
|
|
6901
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6902
|
+
const full = path.join(dir, entry.name);
|
|
6903
|
+
if (entry.isDirectory() && !['node_modules','.git','build','DerivedData','.build'].includes(entry.name)) {
|
|
6904
|
+
walk(full);
|
|
6905
|
+
} else if (entry.name.endsWith('.swift')) {
|
|
6906
|
+
swiftFiles.push(full);
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
} catch (_) {}
|
|
6910
|
+
}
|
|
6911
|
+
walk(wsRoot);
|
|
6912
|
+
if (swiftFiles.length === 0) return buildPhoneFrame('<p style="color:#999;padding:20px">No Swift files found</p>', 'iOS');
|
|
6913
|
+
|
|
6914
|
+
const priority = ['ContentView.swift','TaskListView.swift','HomeView.swift','MainView.swift'];
|
|
6915
|
+
swiftFiles.sort((a, b) => {
|
|
6916
|
+
const ai = priority.findIndex(p => a.endsWith(p));
|
|
6917
|
+
const bi = priority.findIndex(p => b.endsWith(p));
|
|
6918
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
6919
|
+
});
|
|
6920
|
+
|
|
6921
|
+
const bodyParts = [];
|
|
6922
|
+
for (const f of swiftFiles.slice(0, 5)) {
|
|
6923
|
+
try { bodyParts.push(parseSwiftUI(path.basename(f), fs.readFileSync(f, 'utf8'))); } catch (_) {}
|
|
6924
|
+
}
|
|
6925
|
+
return buildPhoneFrame(bodyParts.join('\n'), 'iOS');
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
function parseSwiftUI(name, src) {
|
|
6929
|
+
let h = '<div class="vb"><div class="vn">' + name + '</div>';
|
|
6930
|
+
if (src.includes('TabView')) {
|
|
6931
|
+
const tabs = [...src.matchAll(/\.tabItem\s*\{[^}]*Label\s*\(\s*"([^"]+)"/g)].map(m => m[1]);
|
|
6932
|
+
if (tabs.length) h += '<div class="tb">' + tabs.map(t => '<div class="ti">' + t + '</div>').join('') + '</div>';
|
|
6933
|
+
}
|
|
6934
|
+
if (src.includes('NavigationStack') || src.includes('NavigationView')) {
|
|
6935
|
+
const m = src.match(/\.navigationTitle\s*\(\s*"([^"]+)"/);
|
|
6936
|
+
h += '<div class="nb"><span>' + (m ? m[1] : 'Navigation') + '</span></div>';
|
|
6937
|
+
}
|
|
6938
|
+
if (src.includes('List') || src.includes('ForEach')) {
|
|
6939
|
+
h += '<div class="lc">';
|
|
6940
|
+
const rows = [...src.matchAll(/Text\s*\(\s*"([^"]+)"\s*\)/g)].slice(0, 6);
|
|
6941
|
+
(rows.length ? rows : [{1:'Task item'},{1:'Task item'},{1:'Task item'}]).forEach(r => {
|
|
6942
|
+
h += '<div class="lr">' + (r[1] || 'Item') + '</div>';
|
|
6943
|
+
});
|
|
6944
|
+
h += '</div>';
|
|
6945
|
+
}
|
|
6946
|
+
const btns = [...src.matchAll(/Button\s*\(?\s*"([^"]+)"/g)].slice(0, 3);
|
|
6947
|
+
if (btns.length) { h += '<div class="bg">'; btns.forEach(b => { h += '<button class="ib">' + b[1] + '</button>'; }); h += '</div>'; }
|
|
6948
|
+
if (src.includes('import Charts') || src.includes('Chart {')) {
|
|
6949
|
+
h += '<div class="cp"><div class="cb">' + [60,85,45,90,70].map(v => '<div class="bar" style="height:' + v + '%"></div>').join('') + '</div><div class="cl">Charts</div></div>';
|
|
6950
|
+
}
|
|
6951
|
+
const texts = [...src.matchAll(/Text\s*\(\s*"([^"]+)"\s*\)/g)].map(m => m[1]).filter(t => t.length > 2 && t.length < 60).slice(0, 4);
|
|
6952
|
+
if (texts.length && !src.includes('List')) { h += '<div class="tg">'; texts.forEach(t => { h += '<p class="it">' + t + '</p>'; }); h += '</div>'; }
|
|
6953
|
+
return h + '</div>';
|
|
6954
|
+
}
|
|
6955
|
+
|
|
6956
|
+
function renderAndroidPreview(wsRoot) {
|
|
6957
|
+
const ktFiles = [];
|
|
6958
|
+
function walk(dir) {
|
|
6959
|
+
try {
|
|
6960
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6961
|
+
const full = path.join(dir, entry.name);
|
|
6962
|
+
if (entry.isDirectory() && !['node_modules','.git','build','.gradle'].includes(entry.name)) {
|
|
6963
|
+
walk(full);
|
|
6964
|
+
} else if (entry.name.endsWith('.kt')) {
|
|
6965
|
+
ktFiles.push(full);
|
|
6966
|
+
}
|
|
6967
|
+
}
|
|
6968
|
+
} catch (_) {}
|
|
6969
|
+
}
|
|
6970
|
+
walk(wsRoot);
|
|
6971
|
+
if (ktFiles.length === 0) return buildPhoneFrame('<p style="color:#999;padding:20px">No Kotlin files found</p>', 'Android');
|
|
6972
|
+
|
|
6973
|
+
const priority = ['MainActivity.kt','MainScreen.kt','HomeScreen.kt'];
|
|
6974
|
+
ktFiles.sort((a, b) => {
|
|
6975
|
+
const ai = priority.findIndex(p => a.endsWith(p));
|
|
6976
|
+
const bi = priority.findIndex(p => b.endsWith(p));
|
|
6977
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
6978
|
+
});
|
|
6979
|
+
|
|
6980
|
+
const bodyParts = [];
|
|
6981
|
+
for (const f of ktFiles.slice(0, 5)) {
|
|
6982
|
+
try { bodyParts.push(parseKotlin(path.basename(f), fs.readFileSync(f, 'utf8'))); } catch (_) {}
|
|
6983
|
+
}
|
|
6984
|
+
return buildPhoneFrame(bodyParts.join('\n'), 'Android');
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6987
|
+
function parseKotlin(name, src) {
|
|
6988
|
+
let h = '<div class="vb"><div class="vn">' + name + '</div>';
|
|
6989
|
+
const tm = src.match(/title\s*=\s*\{\s*Text\s*\(\s*"([^"]+)"/);
|
|
6990
|
+
if (tm) h += '<div class="nb mat"><span>' + tm[1] + '</span></div>';
|
|
6991
|
+
if (src.includes('LazyColumn') || src.includes('items(')) {
|
|
6992
|
+
h += '<div class="lc mat">';
|
|
6993
|
+
const rows = [...src.matchAll(/Text\s*\(\s*(?:text\s*=\s*)?"([^"]+)"/g)].slice(0, 5);
|
|
6994
|
+
(rows.length ? rows : [{1:'List item'},{1:'List item'},{1:'List item'}]).forEach(r => {
|
|
6995
|
+
h += '<div class="lr mat">' + (r[1] || 'Item') + '</div>';
|
|
6996
|
+
});
|
|
6997
|
+
h += '</div>';
|
|
6998
|
+
}
|
|
6999
|
+
const btns = [...src.matchAll(/Button\s*\([^)]*onClick[^)]*\)\s*\{[^}]*Text\s*\(\s*"([^"]+)"/g)].slice(0, 3);
|
|
7000
|
+
if (btns.length) { h += '<div class="bg">'; btns.forEach(b => { h += '<button class="mb">' + b[1] + '</button>'; }); h += '</div>'; }
|
|
7001
|
+
return h + '</div>';
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
function buildPhoneFrame(body, platform) {
|
|
7005
|
+
const iOS = platform === 'iOS';
|
|
7006
|
+
const bg = iOS ? '#f2f2f7' : '#fafafa';
|
|
7007
|
+
const accent = iOS ? '#007AFF' : '#6200EA';
|
|
7008
|
+
const font = iOS ? "-apple-system,BlinkMacSystemFont,'SF Pro Display',sans-serif" : "'Roboto','Google Sans',sans-serif";
|
|
7009
|
+
const sb = iOS
|
|
7010
|
+
? '<div class="sb ios"><span>9:41</span><span style="margin-left:auto">WiFi 100%</span></div>'
|
|
7011
|
+
: '<div class="sb and"><span>12:00</span><span style="margin-left:auto">WiFi</span></div>';
|
|
7012
|
+
return '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>'
|
|
7013
|
+
+ '*{box-sizing:border-box;margin:0;padding:0}'
|
|
7014
|
+
+ 'body{background:#1a1a2e;display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:' + font + '}'
|
|
7015
|
+
+ '.pf{width:375px;min-height:812px;background:' + bg + ';border-radius:44px;border:8px solid #2d2d2d;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.8);position:relative;display:flex;flex-direction:column}'
|
|
7016
|
+
+ '.pf::before{content:"";position:absolute;top:14px;left:50%;transform:translateX(-50%);width:' + (iOS?'120':'100') + 'px;height:' + (iOS?'28':'24') + 'px;background:#2d2d2d;border-radius:14px;z-index:10}'
|
|
7017
|
+
+ '.pc{flex:1;overflow-y:auto;padding-top:' + (iOS?'56':'48') + 'px;padding-bottom:34px}'
|
|
7018
|
+
+ '.sb{position:absolute;top:0;left:0;right:0;height:' + (iOS?'54':'44') + 'px;display:flex;align-items:flex-end;padding:0 24px 8px;font-size:12px;font-weight:600;z-index:9;background:' + bg + '}'
|
|
7019
|
+
+ '.nb{background:' + bg + ';border-bottom:.5px solid rgba(0,0,0,.12);padding:12px 16px;font-size:17px;font-weight:600;text-align:' + (iOS?'center':'left') + '}'
|
|
7020
|
+
+ '.nb.mat{background:' + accent + ';color:#fff;font-size:20px;font-weight:500;padding:16px;text-align:left}'
|
|
7021
|
+
+ '.tb{position:absolute;bottom:0;left:0;right:0;height:83px;background:' + bg + ';border-top:.5px solid rgba(0,0,0,.12);display:flex;align-items:flex-start;padding-top:10px}'
|
|
7022
|
+
+ '.ti{flex:1;text-align:center;font-size:10px;color:' + accent + ';padding-top:24px}'
|
|
7023
|
+
+ '.lc{background:#fff;margin:16px 0;border-top:.5px solid rgba(0,0,0,.12);border-bottom:.5px solid rgba(0,0,0,.12)}.lc.mat{margin:0}'
|
|
7024
|
+
+ '.lr{padding:14px 16px;border-bottom:.5px solid rgba(0,0,0,.08);font-size:16px;color:#1c1c1e}.lr::after{content:">";margin-left:auto;color:#c7c7cc;float:right}.lr.mat{background:#fff}'
|
|
7025
|
+
+ '.bg{padding:16px;display:flex;flex-direction:column;gap:10px}'
|
|
7026
|
+
+ '.ib{background:' + accent + ';color:#fff;border:none;border-radius:12px;padding:14px;font-size:16px;font-weight:600;width:100%;cursor:pointer}'
|
|
7027
|
+
+ '.mb{background:' + accent + ';color:#fff;border:none;border-radius:4px;padding:12px 24px;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,.2)}'
|
|
7028
|
+
+ '.tg{padding:16px}.it{font-size:15px;color:#1c1c1e;margin:6px 0;line-height:1.4}'
|
|
7029
|
+
+ '.vb{margin-bottom:4px}.vn{font-size:10px;color:#8e8e93;padding:4px 16px;background:rgba(0,0,0,.03);font-family:monospace}'
|
|
7030
|
+
+ '.cp{margin:16px;background:#fff;border-radius:12px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}'
|
|
7031
|
+
+ '.cb{display:flex;align-items:flex-end;gap:8px;height:100px;padding:8px 0}'
|
|
7032
|
+
+ '.bar{flex:1;background:' + accent + ';border-radius:4px 4px 0 0;opacity:.8}'
|
|
7033
|
+
+ '.cl{text-align:center;font-size:11px;color:#8e8e93;margin-top:8px}'
|
|
7034
|
+
+ '.badge{position:absolute;top:-6px;right:20px;background:' + accent + ';color:#fff;font-size:10px;font-weight:700;padding:2px 8px;border-radius:4px;letter-spacing:1px}'
|
|
7035
|
+
+ '</style></head><body><div style="position:relative"><div class="badge">' + platform.toUpperCase() + '</div><div class="pf">' + sb + '<div class="pc">' + body + '</div></div></div></body></html>';
|
|
7036
|
+
}
|
|
7037
|
+
|
|
6769
7038
|
// ============================================
|
|
6770
7039
|
// START SERVER
|
|
6771
7040
|
|
|
@@ -6814,4 +7083,4 @@ try {
|
|
|
6814
7083
|
// Auto-register default workspace in WorkspaceManager
|
|
6815
7084
|
if (WORKSPACE_ROOT) {
|
|
6816
7085
|
wsManager.open("default", WORKSPACE_ROOT).catch(() => {});
|
|
6817
|
-
}
|
|
7086
|
+
}
|