deepdebug-local-agent 1.0.21 → 1.0.23
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/README.md +81 -0
- package/cloudbuild-agent-staging.yaml +40 -0
- package/docs/legacy-connectors.md +268 -0
- package/package.json +4 -1
- package/scripts/sync-fixtures.mjs +246 -0
- package/src/adapters/as400/as400-parser.js +12 -0
- package/src/adapters/as400/index.js +134 -0
- package/src/adapters/cobol/cobol-parser.js +37 -0
- package/src/adapters/cobol/index.js +151 -0
- package/src/adapters/ibmmq/ibmmq-parser.js +23 -0
- package/src/adapters/ibmmq/index.js +137 -0
- package/src/adapters/index.js +79 -0
- package/src/adapters/oracle-forms/index.js +145 -0
- package/src/adapters/oracle-forms/oracle-forms-parser.js +11 -0
- package/src/adapters/sap/index.js +146 -0
- package/src/adapters/sap/sap-parser.js +41 -0
- 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 +329 -16
- package/test/adapters/as400/as400.test.js +121 -0
- package/test/adapters/cobol/cobol.test.js +217 -0
- package/test/adapters/ibmmq/ibmmq.test.js +122 -0
- package/test/adapters/oracle-forms/oracle-forms.test.js +180 -0
- package/test/adapters/registry.test.js +101 -0
- package/test/adapters/sap/sap.test.js +142 -0
- package/src/local-agent-source.tar.gz +0 -0
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
|
@@ -25,6 +25,7 @@ import { ConfigAnalyzer } from "./analyzers/config-analyzer.js";
|
|
|
25
25
|
import { WorkspaceManager } from "./workspace-manager.js";
|
|
26
26
|
import { startMCPHttpServer } from "./mcp-http-server.js";
|
|
27
27
|
import { registerVercelRoutes } from "./vercel-proxy.js";
|
|
28
|
+
import { loadAdapters, stopAll } from "./adapters/index.js";
|
|
28
29
|
|
|
29
30
|
const execAsync = promisify(exec);
|
|
30
31
|
|
|
@@ -541,6 +542,10 @@ class AIVibeCodingEngine extends EventEmitter {
|
|
|
541
542
|
// Instncia global do AI Engine
|
|
542
543
|
let aiEngine = null;
|
|
543
544
|
|
|
545
|
+
// Phase 2 — Legacy connector adapters (lifecycle owned by SIGTERM/SIGINT handlers below).
|
|
546
|
+
// Populated at boot from LEGACY_STACKS env var; empty when env var unset (zero-regression).
|
|
547
|
+
let _legacyAdapters = [];
|
|
548
|
+
|
|
544
549
|
const app = express();
|
|
545
550
|
|
|
546
551
|
// FIXED: Support Cloud Run PORT environment variable (GCP uses PORT)
|
|
@@ -644,6 +649,31 @@ const MCP_PORT = process.env.MCP_PORT || 5056;
|
|
|
644
649
|
// Inicializar AI Vibe Coding Engine
|
|
645
650
|
aiEngine = new AIVibeCodingEngine(processManager, () => WORKSPACE_ROOT);
|
|
646
651
|
|
|
652
|
+
// Phase 2 — bootstrap legacy connectors from LEGACY_STACKS env var.
|
|
653
|
+
// Adapters are loaded UNSTARTED by the registry (AD-1); we start them inline so that
|
|
654
|
+
// the existing local-agent flow is byte-for-byte unchanged when LEGACY_STACKS is unset.
|
|
655
|
+
// Errors during start surface via the adapter's own 'error' event; the loop never throws.
|
|
656
|
+
try {
|
|
657
|
+
const stacks = (process.env.LEGACY_STACKS || '')
|
|
658
|
+
.split(',')
|
|
659
|
+
.map((s) => s.trim())
|
|
660
|
+
.filter(Boolean);
|
|
661
|
+
if (stacks.length > 0) {
|
|
662
|
+
_legacyAdapters = loadAdapters({ legacyStacks: stacks });
|
|
663
|
+
for (const a of _legacyAdapters) {
|
|
664
|
+
// Start failures are surfaced via the adapter's 'error' event; do not crash boot.
|
|
665
|
+
Promise.resolve()
|
|
666
|
+
.then(() => a.start())
|
|
667
|
+
.catch((err) => {
|
|
668
|
+
process.stderr.write(`legacy-connectors: ${a.constructor.name}.start() failed: ${err.message}\n`);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (err) {
|
|
673
|
+
// Bad LEGACY_STACKS = programmer/operator error; emit to stderr without crashing boot.
|
|
674
|
+
process.stderr.write(`legacy-connectors: ${err.message}\n`);
|
|
675
|
+
}
|
|
676
|
+
|
|
647
677
|
// ============================================
|
|
648
678
|
// BACKUP STORAGE (Sprint 1.3)
|
|
649
679
|
// In-memory backup storage with configurable max size
|
|
@@ -867,7 +897,7 @@ app.post("/workspace/open", async (req, res) => {
|
|
|
867
897
|
|
|
868
898
|
return res.json({
|
|
869
899
|
ok: true,
|
|
870
|
-
root:
|
|
900
|
+
root: clonePath,
|
|
871
901
|
workspaceId: wsId,
|
|
872
902
|
mode: "cloud-clone",
|
|
873
903
|
meta,
|
|
@@ -988,13 +1018,16 @@ app.post("/workspace/clone", async (req, res) => {
|
|
|
988
1018
|
const safeLog = authenticatedUrl.replace(/x-token-auth:[^@]+@/g, 'x-token-auth:***@').replace(/oauth2:[^@]+@/g, 'oauth2:***@').replace(/x-access-token:[^@]+@/g, 'x-access-token:***@');
|
|
989
1019
|
const cloneCmd = branch ? `git clone -b ${branch} "${authenticatedUrl}" "${absTarget}"` : `git clone "${authenticatedUrl}" "${absTarget}"`;
|
|
990
1020
|
console.log(`Running: git clone${branch ? ' -b ' + branch : ''} "${safeLog}" "${absTarget}"`);
|
|
991
|
-
|
|
1021
|
+
const cloneEnvWorkspace = { ...process.env, GIT_TERMINAL_PROMPT: "0", HOME: process.env.HOME || "/tmp" };
|
|
1022
|
+
await execAsync(cloneCmd, { timeout: 300000, env: cloneEnvWorkspace });
|
|
992
1023
|
console.log(`Clone complete: ${absTarget}`);
|
|
993
1024
|
}
|
|
994
1025
|
|
|
995
1026
|
// Open the cloned workspace
|
|
996
|
-
WORKSPACE_ROOT = absTarget;
|
|
997
1027
|
const wsId = workspaceId || "default";
|
|
1028
|
+
if (wsId === "default") {
|
|
1029
|
+
WORKSPACE_ROOT = absTarget;
|
|
1030
|
+
}
|
|
998
1031
|
try {
|
|
999
1032
|
await wsManager.open(wsId, absTarget);
|
|
1000
1033
|
} catch (err) {
|
|
@@ -1293,6 +1326,14 @@ app.post("/workspace/build-push", async (req, res) => {
|
|
|
1293
1326
|
app.post("/workspace/setup", async (req, res) => {
|
|
1294
1327
|
const { workspaceId, repoUrl, gitToken, provider = "deepdebug_cloud",
|
|
1295
1328
|
cloudProjectId, cloudToken } = req.body || {};
|
|
1329
|
+
// gitProvider is passed by the gateway (OnboardingSetupController) and used below
|
|
1330
|
+
// to pick the correct authenticated-clone username. Auto-detect from the URL if
|
|
1331
|
+
// the gateway didn't send it, so older callers keep working.
|
|
1332
|
+
const gitProvider = (req.body && req.body.gitProvider
|
|
1333
|
+
? String(req.body.gitProvider)
|
|
1334
|
+
: (repoUrl && repoUrl.includes("bitbucket.org") ? "bitbucket"
|
|
1335
|
+
: repoUrl && repoUrl.includes("gitlab.com") ? "gitlab"
|
|
1336
|
+
: "github")).toLowerCase();
|
|
1296
1337
|
|
|
1297
1338
|
if (!workspaceId || !repoUrl) {
|
|
1298
1339
|
return res.status(400).json({ error: "workspaceId and repoUrl required" });
|
|
@@ -1320,15 +1361,22 @@ app.post("/workspace/setup", async (req, res) => {
|
|
|
1320
1361
|
emit("progress", "Cloning repository...");
|
|
1321
1362
|
let authUrl = repoUrl;
|
|
1322
1363
|
if (gitToken && gitToken.length > 5) {
|
|
1323
|
-
//
|
|
1364
|
+
// Provider-specific auth username:
|
|
1365
|
+
// bitbucket -> "x-token-auth" (required by Bitbucket Cloud)
|
|
1366
|
+
// gitlab -> "oauth2"
|
|
1367
|
+
// github/* -> "x-access-token" (GitHub's recommended)
|
|
1368
|
+
const authUser = gitProvider === "bitbucket" ? "x-token-auth"
|
|
1369
|
+
: gitProvider === "gitlab" ? "oauth2"
|
|
1370
|
+
: "x-access-token";
|
|
1324
1371
|
try {
|
|
1325
1372
|
const urlObj = new URL(repoUrl);
|
|
1326
|
-
urlObj.username =
|
|
1373
|
+
urlObj.username = authUser;
|
|
1327
1374
|
urlObj.password = gitToken;
|
|
1328
1375
|
authUrl = urlObj.toString();
|
|
1329
|
-
emit("progress", `Using authenticated clone for ${urlObj.hostname}`);
|
|
1376
|
+
emit("progress", `Using authenticated clone for ${urlObj.hostname} (auth=${authUser})`);
|
|
1330
1377
|
} catch {
|
|
1331
|
-
authUrl = repoUrl.replace("https://", `https
|
|
1378
|
+
authUrl = repoUrl.replace("https://", `https://${authUser}:${gitToken}@`);
|
|
1379
|
+
emit("progress", `Using authenticated clone (fallback, auth=${authUser})`);
|
|
1332
1380
|
}
|
|
1333
1381
|
}
|
|
1334
1382
|
|
|
@@ -2217,16 +2265,16 @@ app.post("/workspace/git/prepare", async (req, res) => {
|
|
|
2217
2265
|
return res.status(400).json({ ok: false, error: "workspace not set" });
|
|
2218
2266
|
}
|
|
2219
2267
|
|
|
2220
|
-
const { gitToken, repoUrl } = req.body || {};
|
|
2221
|
-
console.log(
|
|
2268
|
+
const { gitToken, repoUrl, gitProvider, branch: reqBranch, workspaceId } = req.body || {};
|
|
2269
|
+
console.log(`[git-prepare] Starting workspace preparation: wsRoot=${wsRoot} repoUrl=${repoUrl || 'none'}`);
|
|
2222
2270
|
|
|
2223
2271
|
try {
|
|
2224
2272
|
const { execSync } = await import("child_process");
|
|
2273
|
+
const execOpts = { encoding: "utf8", timeout: 120000, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } };
|
|
2225
2274
|
const execGit = (cmd) => {
|
|
2226
2275
|
try {
|
|
2227
2276
|
return { ok: true, output: execSync(cmd, {
|
|
2228
|
-
cwd: wsRoot,
|
|
2229
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
2277
|
+
cwd: wsRoot, ...execOpts
|
|
2230
2278
|
}).trim() };
|
|
2231
2279
|
} catch (err) {
|
|
2232
2280
|
return { ok: false, output: (err.stderr || err.stdout || err.message || "unknown").toString().trim() };
|
|
@@ -2235,9 +2283,80 @@ app.post("/workspace/git/prepare", async (req, res) => {
|
|
|
2235
2283
|
|
|
2236
2284
|
execGit(`git config --global --add safe.directory "${wsRoot}"`);
|
|
2237
2285
|
|
|
2286
|
+
// If workspace is empty (no .git), clone first before attempting fetch/reset
|
|
2287
|
+
const gitDir = path.join(wsRoot, ".git");
|
|
2288
|
+
if (!fs.existsSync(gitDir)) {
|
|
2289
|
+
if (!repoUrl) {
|
|
2290
|
+
return res.json({ ok: false, error: "Workspace is empty and no repoUrl provided for clone" });
|
|
2291
|
+
}
|
|
2292
|
+
console.log(`[git-prepare] No .git found at ${wsRoot}, cloning from ${repoUrl}`);
|
|
2293
|
+
fs.mkdirSync(wsRoot, { recursive: true });
|
|
2294
|
+
|
|
2295
|
+
// Build authenticated URL
|
|
2296
|
+
let authUrl = repoUrl;
|
|
2297
|
+
if (gitToken) {
|
|
2298
|
+
const prov = (gitProvider || "").toLowerCase();
|
|
2299
|
+
if (repoUrl.includes("bitbucket.org") || prov === "bitbucket") {
|
|
2300
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-token-auth:${gitToken}@`);
|
|
2301
|
+
} else if (repoUrl.includes("gitlab.com") || prov === "gitlab") {
|
|
2302
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://oauth2:${gitToken}@`);
|
|
2303
|
+
} else {
|
|
2304
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-access-token:${gitToken}@`);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (!authUrl.endsWith(".git")) authUrl += ".git";
|
|
2308
|
+
|
|
2309
|
+
const branchesToTry = [reqBranch || "main", "main", "master"].filter((b, i, a) => a.indexOf(b) === i);
|
|
2310
|
+
let cloned = false;
|
|
2311
|
+
let clonedBranch = "main";
|
|
2312
|
+
for (const b of branchesToTry) {
|
|
2313
|
+
try {
|
|
2314
|
+
fs.rmSync(wsRoot, { recursive: true, force: true });
|
|
2315
|
+
fs.mkdirSync(wsRoot, { recursive: true });
|
|
2316
|
+
execSync(`git clone --depth 50 --branch "${b}" "${authUrl}" "${wsRoot}"`, execOpts);
|
|
2317
|
+
clonedBranch = b;
|
|
2318
|
+
cloned = true;
|
|
2319
|
+
console.log(`[git-prepare] Cloned branch ${b} successfully`);
|
|
2320
|
+
break;
|
|
2321
|
+
} catch (cloneErr) {
|
|
2322
|
+
console.warn(`[git-prepare] Clone branch ${b} failed: ${(cloneErr.message || "").slice(0, 100)}`);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (!cloned) {
|
|
2326
|
+
try {
|
|
2327
|
+
fs.rmSync(wsRoot, { recursive: true, force: true });
|
|
2328
|
+
fs.mkdirSync(wsRoot, { recursive: true });
|
|
2329
|
+
execSync(`git clone --depth 50 "${authUrl}" "${wsRoot}"`, execOpts);
|
|
2330
|
+
cloned = true;
|
|
2331
|
+
clonedBranch = "default";
|
|
2332
|
+
} catch (e) {
|
|
2333
|
+
return res.json({ ok: false, error: "Clone failed: " + (e.message || "").slice(0, 200) });
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// Update WORKSPACE_ROOT so subsequent tool calls find the files
|
|
2338
|
+
WORKSPACE_ROOT = wsRoot;
|
|
2339
|
+
try { await wsManager.open(workspaceId || path.basename(wsRoot), wsRoot); } catch (_) {}
|
|
2340
|
+
|
|
2341
|
+
const commit = execGit("git rev-parse --short HEAD");
|
|
2342
|
+
let fileCount = 0;
|
|
2343
|
+
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 (_) {}
|
|
2344
|
+
return res.json({ ok: true, cloned: true, mainBranch: clonedBranch,
|
|
2345
|
+
currentCommit: commit.ok ? commit.output : "unknown", fileCount,
|
|
2346
|
+
message: `Repository cloned: ${clonedBranch} (${fileCount} files)` });
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2238
2349
|
if (gitToken && repoUrl) {
|
|
2239
|
-
|
|
2240
|
-
|
|
2350
|
+
let authUrl = repoUrl;
|
|
2351
|
+
const prov = (gitProvider || "").toLowerCase();
|
|
2352
|
+
if (repoUrl.includes("bitbucket.org") || prov === "bitbucket") {
|
|
2353
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-token-auth:${gitToken}@`);
|
|
2354
|
+
} else if (repoUrl.includes("gitlab.com") || prov === "gitlab") {
|
|
2355
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://oauth2:${gitToken}@`);
|
|
2356
|
+
} else {
|
|
2357
|
+
authUrl = repoUrl.replace(/https:\/\/([^@]*@)?/, `https://x-access-token:${gitToken}@`);
|
|
2358
|
+
}
|
|
2359
|
+
execGit(`git remote set-url origin "${authUrl}"`);
|
|
2241
2360
|
}
|
|
2242
2361
|
|
|
2243
2362
|
console.log("[git-prepare] Fetching origin...");
|
|
@@ -6161,8 +6280,22 @@ async function startWebSocketTunnel(port, gatewayUrl, apiKey, tenantId) {
|
|
|
6161
6280
|
}
|
|
6162
6281
|
}
|
|
6163
6282
|
|
|
6164
|
-
process.on('SIGINT', () => {
|
|
6165
|
-
|
|
6283
|
+
process.on('SIGINT', async () => {
|
|
6284
|
+
isShuttingDown = true;
|
|
6285
|
+
if (ws) ws.close();
|
|
6286
|
+
// Phase 2: graceful shutdown of legacy connectors (AC-11; bounded to 5 s).
|
|
6287
|
+
try { await stopAll(_legacyAdapters, 5000); } catch (_) { /* never block exit */ }
|
|
6288
|
+
// Preserve existing process model: signal trap does NOT call process.exit();
|
|
6289
|
+
// Node exits naturally once the event loop drains.
|
|
6290
|
+
});
|
|
6291
|
+
process.on('SIGTERM', async () => {
|
|
6292
|
+
isShuttingDown = true;
|
|
6293
|
+
if (ws) ws.close();
|
|
6294
|
+
// Phase 2: graceful shutdown of legacy connectors (AC-11; bounded to 5 s).
|
|
6295
|
+
try { await stopAll(_legacyAdapters, 5000); } catch (_) { /* never block exit */ }
|
|
6296
|
+
// Preserve existing process model: signal trap does NOT call process.exit();
|
|
6297
|
+
// Node exits naturally once the event loop drains.
|
|
6298
|
+
});
|
|
6166
6299
|
connect();
|
|
6167
6300
|
}
|
|
6168
6301
|
|
|
@@ -6766,6 +6899,186 @@ app.delete("/workspace/vibe/tunnel/:sessionId", async (req, res) => {
|
|
|
6766
6899
|
// ============================================
|
|
6767
6900
|
registerVercelRoutes(app);
|
|
6768
6901
|
|
|
6902
|
+
// ============================================
|
|
6903
|
+
// LIVE PREVIEW — iOS / Android / Web
|
|
6904
|
+
// ============================================
|
|
6905
|
+
|
|
6906
|
+
app.get('/workspace/preview', async (req, res) => {
|
|
6907
|
+
const wsRoot = req.headers['x-workspace-root'] || WORKSPACE_ROOT;
|
|
6908
|
+
if (!wsRoot) {
|
|
6909
|
+
return res.json({ type: 'none', message: 'Workspace not set' });
|
|
6910
|
+
}
|
|
6911
|
+
|
|
6912
|
+
try {
|
|
6913
|
+
const meta = await detectProject(wsRoot);
|
|
6914
|
+
const lang = (meta.language || '').toLowerCase();
|
|
6915
|
+
const fw = (meta.framework || '').toLowerCase();
|
|
6916
|
+
|
|
6917
|
+
if (lang === 'ios' || lang === 'swift' || fw.includes('swiftui') || fw.includes('uikit')) {
|
|
6918
|
+
const html = renderSwiftUIPreview(wsRoot);
|
|
6919
|
+
return res.json({ type: 'ios', html, framework: fw, message: 'SwiftUI static render' });
|
|
6920
|
+
}
|
|
6921
|
+
|
|
6922
|
+
if (lang === 'android' || lang === 'kotlin' || fw.includes('compose') || fw.includes('android')) {
|
|
6923
|
+
const html = renderAndroidPreview(wsRoot);
|
|
6924
|
+
return res.json({ type: 'android', html, framework: fw, message: 'Android static render' });
|
|
6925
|
+
}
|
|
6926
|
+
|
|
6927
|
+
const webFw = ['react','next','vue','nuxt','angular','svelte','vite'];
|
|
6928
|
+
if (webFw.some(f => fw.includes(f))) {
|
|
6929
|
+
let cmd = 'npm run dev';
|
|
6930
|
+
if (fw.includes('angular')) cmd = 'npx ng serve --port 4200 --host 0.0.0.0';
|
|
6931
|
+
return res.json({ type: 'web', framework: fw, startCommand: cmd, message: 'Start dev server for live preview' });
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
return res.json({ type: 'none', language: lang, framework: fw, message: 'No preview for ' + lang + '/' + fw });
|
|
6935
|
+
} catch (err) {
|
|
6936
|
+
console.error('[preview]', err.message);
|
|
6937
|
+
res.json({ type: 'none', message: err.message });
|
|
6938
|
+
}
|
|
6939
|
+
});
|
|
6940
|
+
|
|
6941
|
+
function renderSwiftUIPreview(wsRoot) {
|
|
6942
|
+
const swiftFiles = [];
|
|
6943
|
+
function walk(dir) {
|
|
6944
|
+
try {
|
|
6945
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6946
|
+
const full = path.join(dir, entry.name);
|
|
6947
|
+
if (entry.isDirectory() && !['node_modules','.git','build','DerivedData','.build'].includes(entry.name)) {
|
|
6948
|
+
walk(full);
|
|
6949
|
+
} else if (entry.name.endsWith('.swift')) {
|
|
6950
|
+
swiftFiles.push(full);
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
} catch (_) {}
|
|
6954
|
+
}
|
|
6955
|
+
walk(wsRoot);
|
|
6956
|
+
if (swiftFiles.length === 0) return buildPhoneFrame('<p style="color:#999;padding:20px">No Swift files found</p>', 'iOS');
|
|
6957
|
+
|
|
6958
|
+
const priority = ['ContentView.swift','TaskListView.swift','HomeView.swift','MainView.swift'];
|
|
6959
|
+
swiftFiles.sort((a, b) => {
|
|
6960
|
+
const ai = priority.findIndex(p => a.endsWith(p));
|
|
6961
|
+
const bi = priority.findIndex(p => b.endsWith(p));
|
|
6962
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
6963
|
+
});
|
|
6964
|
+
|
|
6965
|
+
const bodyParts = [];
|
|
6966
|
+
for (const f of swiftFiles.slice(0, 5)) {
|
|
6967
|
+
try { bodyParts.push(parseSwiftUI(path.basename(f), fs.readFileSync(f, 'utf8'))); } catch (_) {}
|
|
6968
|
+
}
|
|
6969
|
+
return buildPhoneFrame(bodyParts.join('\n'), 'iOS');
|
|
6970
|
+
}
|
|
6971
|
+
|
|
6972
|
+
function parseSwiftUI(name, src) {
|
|
6973
|
+
let h = '<div class="vb"><div class="vn">' + name + '</div>';
|
|
6974
|
+
if (src.includes('TabView')) {
|
|
6975
|
+
const tabs = [...src.matchAll(/\.tabItem\s*\{[^}]*Label\s*\(\s*"([^"]+)"/g)].map(m => m[1]);
|
|
6976
|
+
if (tabs.length) h += '<div class="tb">' + tabs.map(t => '<div class="ti">' + t + '</div>').join('') + '</div>';
|
|
6977
|
+
}
|
|
6978
|
+
if (src.includes('NavigationStack') || src.includes('NavigationView')) {
|
|
6979
|
+
const m = src.match(/\.navigationTitle\s*\(\s*"([^"]+)"/);
|
|
6980
|
+
h += '<div class="nb"><span>' + (m ? m[1] : 'Navigation') + '</span></div>';
|
|
6981
|
+
}
|
|
6982
|
+
if (src.includes('List') || src.includes('ForEach')) {
|
|
6983
|
+
h += '<div class="lc">';
|
|
6984
|
+
const rows = [...src.matchAll(/Text\s*\(\s*"([^"]+)"\s*\)/g)].slice(0, 6);
|
|
6985
|
+
(rows.length ? rows : [{1:'Task item'},{1:'Task item'},{1:'Task item'}]).forEach(r => {
|
|
6986
|
+
h += '<div class="lr">' + (r[1] || 'Item') + '</div>';
|
|
6987
|
+
});
|
|
6988
|
+
h += '</div>';
|
|
6989
|
+
}
|
|
6990
|
+
const btns = [...src.matchAll(/Button\s*\(?\s*"([^"]+)"/g)].slice(0, 3);
|
|
6991
|
+
if (btns.length) { h += '<div class="bg">'; btns.forEach(b => { h += '<button class="ib">' + b[1] + '</button>'; }); h += '</div>'; }
|
|
6992
|
+
if (src.includes('import Charts') || src.includes('Chart {')) {
|
|
6993
|
+
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>';
|
|
6994
|
+
}
|
|
6995
|
+
const texts = [...src.matchAll(/Text\s*\(\s*"([^"]+)"\s*\)/g)].map(m => m[1]).filter(t => t.length > 2 && t.length < 60).slice(0, 4);
|
|
6996
|
+
if (texts.length && !src.includes('List')) { h += '<div class="tg">'; texts.forEach(t => { h += '<p class="it">' + t + '</p>'; }); h += '</div>'; }
|
|
6997
|
+
return h + '</div>';
|
|
6998
|
+
}
|
|
6999
|
+
|
|
7000
|
+
function renderAndroidPreview(wsRoot) {
|
|
7001
|
+
const ktFiles = [];
|
|
7002
|
+
function walk(dir) {
|
|
7003
|
+
try {
|
|
7004
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
7005
|
+
const full = path.join(dir, entry.name);
|
|
7006
|
+
if (entry.isDirectory() && !['node_modules','.git','build','.gradle'].includes(entry.name)) {
|
|
7007
|
+
walk(full);
|
|
7008
|
+
} else if (entry.name.endsWith('.kt')) {
|
|
7009
|
+
ktFiles.push(full);
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
} catch (_) {}
|
|
7013
|
+
}
|
|
7014
|
+
walk(wsRoot);
|
|
7015
|
+
if (ktFiles.length === 0) return buildPhoneFrame('<p style="color:#999;padding:20px">No Kotlin files found</p>', 'Android');
|
|
7016
|
+
|
|
7017
|
+
const priority = ['MainActivity.kt','MainScreen.kt','HomeScreen.kt'];
|
|
7018
|
+
ktFiles.sort((a, b) => {
|
|
7019
|
+
const ai = priority.findIndex(p => a.endsWith(p));
|
|
7020
|
+
const bi = priority.findIndex(p => b.endsWith(p));
|
|
7021
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
7022
|
+
});
|
|
7023
|
+
|
|
7024
|
+
const bodyParts = [];
|
|
7025
|
+
for (const f of ktFiles.slice(0, 5)) {
|
|
7026
|
+
try { bodyParts.push(parseKotlin(path.basename(f), fs.readFileSync(f, 'utf8'))); } catch (_) {}
|
|
7027
|
+
}
|
|
7028
|
+
return buildPhoneFrame(bodyParts.join('\n'), 'Android');
|
|
7029
|
+
}
|
|
7030
|
+
|
|
7031
|
+
function parseKotlin(name, src) {
|
|
7032
|
+
let h = '<div class="vb"><div class="vn">' + name + '</div>';
|
|
7033
|
+
const tm = src.match(/title\s*=\s*\{\s*Text\s*\(\s*"([^"]+)"/);
|
|
7034
|
+
if (tm) h += '<div class="nb mat"><span>' + tm[1] + '</span></div>';
|
|
7035
|
+
if (src.includes('LazyColumn') || src.includes('items(')) {
|
|
7036
|
+
h += '<div class="lc mat">';
|
|
7037
|
+
const rows = [...src.matchAll(/Text\s*\(\s*(?:text\s*=\s*)?"([^"]+)"/g)].slice(0, 5);
|
|
7038
|
+
(rows.length ? rows : [{1:'List item'},{1:'List item'},{1:'List item'}]).forEach(r => {
|
|
7039
|
+
h += '<div class="lr mat">' + (r[1] || 'Item') + '</div>';
|
|
7040
|
+
});
|
|
7041
|
+
h += '</div>';
|
|
7042
|
+
}
|
|
7043
|
+
const btns = [...src.matchAll(/Button\s*\([^)]*onClick[^)]*\)\s*\{[^}]*Text\s*\(\s*"([^"]+)"/g)].slice(0, 3);
|
|
7044
|
+
if (btns.length) { h += '<div class="bg">'; btns.forEach(b => { h += '<button class="mb">' + b[1] + '</button>'; }); h += '</div>'; }
|
|
7045
|
+
return h + '</div>';
|
|
7046
|
+
}
|
|
7047
|
+
|
|
7048
|
+
function buildPhoneFrame(body, platform) {
|
|
7049
|
+
const iOS = platform === 'iOS';
|
|
7050
|
+
const bg = iOS ? '#f2f2f7' : '#fafafa';
|
|
7051
|
+
const accent = iOS ? '#007AFF' : '#6200EA';
|
|
7052
|
+
const font = iOS ? "-apple-system,BlinkMacSystemFont,'SF Pro Display',sans-serif" : "'Roboto','Google Sans',sans-serif";
|
|
7053
|
+
const sb = iOS
|
|
7054
|
+
? '<div class="sb ios"><span>9:41</span><span style="margin-left:auto">WiFi 100%</span></div>'
|
|
7055
|
+
: '<div class="sb and"><span>12:00</span><span style="margin-left:auto">WiFi</span></div>';
|
|
7056
|
+
return '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>'
|
|
7057
|
+
+ '*{box-sizing:border-box;margin:0;padding:0}'
|
|
7058
|
+
+ 'body{background:#1a1a2e;display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:' + font + '}'
|
|
7059
|
+
+ '.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}'
|
|
7060
|
+
+ '.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}'
|
|
7061
|
+
+ '.pc{flex:1;overflow-y:auto;padding-top:' + (iOS?'56':'48') + 'px;padding-bottom:34px}'
|
|
7062
|
+
+ '.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 + '}'
|
|
7063
|
+
+ '.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') + '}'
|
|
7064
|
+
+ '.nb.mat{background:' + accent + ';color:#fff;font-size:20px;font-weight:500;padding:16px;text-align:left}'
|
|
7065
|
+
+ '.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}'
|
|
7066
|
+
+ '.ti{flex:1;text-align:center;font-size:10px;color:' + accent + ';padding-top:24px}'
|
|
7067
|
+
+ '.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}'
|
|
7068
|
+
+ '.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}'
|
|
7069
|
+
+ '.bg{padding:16px;display:flex;flex-direction:column;gap:10px}'
|
|
7070
|
+
+ '.ib{background:' + accent + ';color:#fff;border:none;border-radius:12px;padding:14px;font-size:16px;font-weight:600;width:100%;cursor:pointer}'
|
|
7071
|
+
+ '.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)}'
|
|
7072
|
+
+ '.tg{padding:16px}.it{font-size:15px;color:#1c1c1e;margin:6px 0;line-height:1.4}'
|
|
7073
|
+
+ '.vb{margin-bottom:4px}.vn{font-size:10px;color:#8e8e93;padding:4px 16px;background:rgba(0,0,0,.03);font-family:monospace}'
|
|
7074
|
+
+ '.cp{margin:16px;background:#fff;border-radius:12px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}'
|
|
7075
|
+
+ '.cb{display:flex;align-items:flex-end;gap:8px;height:100px;padding:8px 0}'
|
|
7076
|
+
+ '.bar{flex:1;background:' + accent + ';border-radius:4px 4px 0 0;opacity:.8}'
|
|
7077
|
+
+ '.cl{text-align:center;font-size:11px;color:#8e8e93;margin-top:8px}'
|
|
7078
|
+
+ '.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}'
|
|
7079
|
+
+ '</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>';
|
|
7080
|
+
}
|
|
7081
|
+
|
|
6769
7082
|
// ============================================
|
|
6770
7083
|
// START SERVER
|
|
6771
7084
|
|
|
@@ -6814,4 +7127,4 @@ try {
|
|
|
6814
7127
|
// Auto-register default workspace in WorkspaceManager
|
|
6815
7128
|
if (WORKSPACE_ROOT) {
|
|
6816
7129
|
wsManager.open("default", WORKSPACE_ROOT).catch(() => {});
|
|
6817
|
-
}
|
|
7130
|
+
}
|