@maintainer-pro/ai-bridge 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/daemon.mjs +320 -144
package/package.json
CHANGED
package/src/daemon.mjs
CHANGED
|
@@ -1075,7 +1075,9 @@ function runLauncher(command, args, extra = {}) {
|
|
|
1075
1075
|
async function openInNewTerminal(opts) {
|
|
1076
1076
|
const { title, folder, command, env = {}, launchKey } = opts;
|
|
1077
1077
|
if (launchKey) {
|
|
1078
|
-
if (recentlyLaunched(launchKey))
|
|
1078
|
+
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
1079
|
+
return { ok: true, skipped: true };
|
|
1080
|
+
}
|
|
1079
1081
|
launchedAt.set(launchKey, Date.now());
|
|
1080
1082
|
}
|
|
1081
1083
|
if (!fs.existsSync(folder)) {
|
|
@@ -1276,188 +1278,350 @@ function sleep(ms) {
|
|
|
1276
1278
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1277
1279
|
}
|
|
1278
1280
|
|
|
1279
|
-
/** @type {Map<string, {
|
|
1281
|
+
/** @type {Map<string, { tunnels: Array<{ role: string, localUrl: string, publicUrl: string | null, logFile: string }> }>} */
|
|
1280
1282
|
const cloudflareTunnels = new Map();
|
|
1281
1283
|
|
|
1284
|
+
function stopAllCloudflare() {
|
|
1285
|
+
for (const id of [...cloudflareTunnels.keys()]) stopCloudflare(id);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function parseTryCloudflareUrl(text) {
|
|
1289
|
+
const match = String(text || "").match(
|
|
1290
|
+
/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i
|
|
1291
|
+
);
|
|
1292
|
+
return match ? match[0].replace(/\/$/, "") : null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function killProcessesByCommand(fragment) {
|
|
1296
|
+
if (!fragment) return Promise.resolve();
|
|
1297
|
+
return new Promise((resolve) => {
|
|
1298
|
+
const done = () => resolve();
|
|
1299
|
+
if (process.platform === "win32") {
|
|
1300
|
+
const escaped = String(fragment).replace(/'/g, "''");
|
|
1301
|
+
const child = spawn(
|
|
1302
|
+
"powershell.exe",
|
|
1303
|
+
[
|
|
1304
|
+
"-NoProfile",
|
|
1305
|
+
"-Command",
|
|
1306
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${escaped}*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`,
|
|
1307
|
+
],
|
|
1308
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
1309
|
+
);
|
|
1310
|
+
child.on("exit", done);
|
|
1311
|
+
child.on("error", done);
|
|
1312
|
+
setTimeout(done, 8000);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const child = spawn("pkill", ["-f", String(fragment)], { stdio: "ignore" });
|
|
1316
|
+
child.on("exit", done);
|
|
1317
|
+
child.on("error", done);
|
|
1318
|
+
setTimeout(done, 4000);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function killPort(port) {
|
|
1323
|
+
const n = Number(port);
|
|
1324
|
+
if (!n) return Promise.resolve();
|
|
1325
|
+
return new Promise((resolve) => {
|
|
1326
|
+
const done = () => resolve();
|
|
1327
|
+
if (process.platform === "win32") {
|
|
1328
|
+
const child = spawn(
|
|
1329
|
+
"powershell.exe",
|
|
1330
|
+
[
|
|
1331
|
+
"-NoProfile",
|
|
1332
|
+
"-Command",
|
|
1333
|
+
`$ErrorActionPreference='SilentlyContinue'; $pids=@(); $pids += Get-NetTCPConnection -LocalPort ${n} -State Listen | Select-Object -ExpandProperty OwningProcess; if (-not $pids) { netstat -ano | Select-String ':${n}\\s' | ForEach-Object { if ($_ -match 'LISTENING\\s+(\\d+)') { $pids += [int]$Matches[1] } } }; $pids | Sort-Object -Unique | Where-Object { $_ -gt 0 -and $_ -ne ${process.pid} } | ForEach-Object { Stop-Process -Id $_ -Force }`,
|
|
1334
|
+
],
|
|
1335
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
1336
|
+
);
|
|
1337
|
+
child.on("exit", done);
|
|
1338
|
+
child.on("error", done);
|
|
1339
|
+
setTimeout(done, 8000);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const child = spawn(
|
|
1343
|
+
"sh",
|
|
1344
|
+
["-c", `pids=$(lsof -ti tcp:${n} 2>/dev/null); [ -n "$pids" ] && kill $pids`],
|
|
1345
|
+
{ stdio: "ignore" }
|
|
1346
|
+
);
|
|
1347
|
+
child.on("exit", done);
|
|
1348
|
+
child.on("error", done);
|
|
1349
|
+
setTimeout(done, 4000);
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1282
1353
|
function stopCloudflare(sandboxId) {
|
|
1283
1354
|
const row = cloudflareTunnels.get(sandboxId);
|
|
1284
|
-
|
|
1355
|
+
const files = row?.tunnels?.map((t) => t.logFile).filter(Boolean) ?? [];
|
|
1356
|
+
cloudflareTunnels.delete(sandboxId);
|
|
1357
|
+
return Promise.all(files.map((file) => killProcessesByCommand(file)));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function forgetProcessLaunches(sandboxId) {
|
|
1361
|
+
for (const key of [...launchedAt.keys()]) {
|
|
1362
|
+
if (key === sandboxId || key.startsWith(`${sandboxId}:`)) {
|
|
1363
|
+
launchedAt.delete(key);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function stopWorkspaceApps(ws) {
|
|
1369
|
+
const folder = path.resolve(ws.folderPath || "");
|
|
1370
|
+
const jobs = planHostJobs(folder, null, ws.projectInfo);
|
|
1371
|
+
const ports = new Set();
|
|
1372
|
+
if (ws.port) ports.add(Number(ws.port));
|
|
1373
|
+
for (const job of jobs) {
|
|
1374
|
+
if (job.preferredPort) ports.add(Number(job.preferredPort));
|
|
1375
|
+
}
|
|
1376
|
+
if (ws.appUrl && isLocalAppUrl(ws.appUrl)) {
|
|
1285
1377
|
try {
|
|
1286
|
-
|
|
1378
|
+
const port = Number(new URL(ws.appUrl).port);
|
|
1379
|
+
if (port) ports.add(port);
|
|
1287
1380
|
} catch {
|
|
1288
1381
|
/* ignore */
|
|
1289
1382
|
}
|
|
1290
1383
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1384
|
+
log(`stopping local apps on ports ${[...ports].join(", ") || "(none)"}`);
|
|
1385
|
+
for (const port of ports) {
|
|
1386
|
+
await killPort(port);
|
|
1387
|
+
}
|
|
1388
|
+
forgetProcessLaunches(ws.sandboxId);
|
|
1389
|
+
await sleep(400);
|
|
1296
1390
|
}
|
|
1297
1391
|
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
|
|
1392
|
+
async function waitUntilReachable(url, timeoutMs, label) {
|
|
1393
|
+
const start = Date.now();
|
|
1394
|
+
while (Date.now() - start < timeoutMs) {
|
|
1395
|
+
if (await probeUrl(url)) return true;
|
|
1396
|
+
await sleep(600);
|
|
1397
|
+
}
|
|
1398
|
+
throw new Error(`${label} did not become reachable at ${url}`);
|
|
1301
1399
|
}
|
|
1302
1400
|
|
|
1303
|
-
function
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1401
|
+
async function waitForUrlInFile(file, timeoutMs = 90_000) {
|
|
1402
|
+
const start = Date.now();
|
|
1403
|
+
while (Date.now() - start < timeoutMs) {
|
|
1404
|
+
if (fs.existsSync(file)) {
|
|
1405
|
+
const url = parseTryCloudflareUrl(fs.readFileSync(file, "utf8"));
|
|
1406
|
+
if (url) return url;
|
|
1407
|
+
}
|
|
1408
|
+
await sleep(500);
|
|
1409
|
+
}
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`Cloudflare did not publish a URL in time (${path.basename(file)}). Check that terminal.`
|
|
1306
1412
|
);
|
|
1307
|
-
return match ? match[0].replace(/\/$/, "") : null;
|
|
1308
1413
|
}
|
|
1309
1414
|
|
|
1310
|
-
function
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
});
|
|
1319
|
-
const onError = (err) => reject(err);
|
|
1320
|
-
child.once("error", onError);
|
|
1321
|
-
child.once("spawn", () => {
|
|
1322
|
-
child.off("error", onError);
|
|
1323
|
-
resolve(child);
|
|
1324
|
-
});
|
|
1325
|
-
});
|
|
1415
|
+
function cloudflaredCommand(localUrl, logFile) {
|
|
1416
|
+
const run = `npx --yes cloudflared tunnel --url ${localUrl} --no-autoupdate`;
|
|
1417
|
+
if (process.platform === "win32") {
|
|
1418
|
+
const dest = String(logFile).replace(/'/g, "''");
|
|
1419
|
+
return `powershell -NoProfile -Command "${run} 2>&1 | Tee-Object -FilePath '${dest}'"`;
|
|
1420
|
+
}
|
|
1421
|
+
return `${run} 2>&1 | tee ${JSON.stringify(logFile)}`;
|
|
1422
|
+
}
|
|
1326
1423
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1424
|
+
async function startCloudflareTerminal(ws, role, localUrl) {
|
|
1425
|
+
const folder = path.resolve(ws.folderPath);
|
|
1426
|
+
const logDir = path.join(folder, ".maintainer-pro");
|
|
1427
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1428
|
+
const logFile = path.join(
|
|
1429
|
+
logDir,
|
|
1430
|
+
`cf-${String(ws.sandboxId).slice(0, 8)}-${role}.log`
|
|
1333
1431
|
);
|
|
1432
|
+
try {
|
|
1433
|
+
fs.unlinkSync(logFile);
|
|
1434
|
+
} catch {
|
|
1435
|
+
/* ignore */
|
|
1436
|
+
}
|
|
1437
|
+
const opened = await openInNewTerminal({
|
|
1438
|
+
title: `MP-cf-${role}`,
|
|
1439
|
+
folder,
|
|
1440
|
+
command: cloudflaredCommand(localUrl, logFile),
|
|
1441
|
+
launchKey: `${ws.sandboxId}:cf:${role}`,
|
|
1442
|
+
force: true,
|
|
1443
|
+
});
|
|
1444
|
+
if (!opened.ok) {
|
|
1445
|
+
throw new Error(opened.error || `Could not open a Cloudflare terminal for ${role}`);
|
|
1446
|
+
}
|
|
1447
|
+
const publicUrl = await waitForUrlInFile(logFile);
|
|
1448
|
+
return { role, localUrl, publicUrl, logFile };
|
|
1334
1449
|
}
|
|
1335
1450
|
|
|
1336
|
-
function
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1451
|
+
function uiPublicEnv(tunnels) {
|
|
1452
|
+
/** @type {Record<string, string>} */
|
|
1453
|
+
const env = {};
|
|
1454
|
+
if (tunnels.ai) {
|
|
1455
|
+
env.AI_SERVER_URL = tunnels.ai;
|
|
1456
|
+
env.NEXT_PUBLIC_AI_SERVER_URL = tunnels.ai;
|
|
1457
|
+
env.VITE_AI_SERVER_URL = tunnels.ai;
|
|
1458
|
+
}
|
|
1459
|
+
if (tunnels.backend) {
|
|
1460
|
+
env.API_URL = tunnels.backend;
|
|
1461
|
+
env.API_BASE_URL = tunnels.backend;
|
|
1462
|
+
env.VITE_API_URL = tunnels.backend;
|
|
1463
|
+
env.VITE_API_BASE_URL = tunnels.backend;
|
|
1464
|
+
env.NEXT_PUBLIC_API_URL = tunnels.backend;
|
|
1465
|
+
env.NEXT_PUBLIC_API_BASE_URL = tunnels.backend;
|
|
1466
|
+
env.BACKEND_URL = tunnels.backend;
|
|
1467
|
+
}
|
|
1468
|
+
if (tunnels.ui) {
|
|
1469
|
+
env.APP_URL = tunnels.ui;
|
|
1470
|
+
env.PUBLIC_URL = tunnels.ui;
|
|
1471
|
+
env.CORS_ORIGIN = tunnels.ui;
|
|
1472
|
+
} else if (tunnels.ai) {
|
|
1473
|
+
env.CORS_ORIGIN = tunnels.ai;
|
|
1474
|
+
}
|
|
1475
|
+
return env;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function writeTunnelEnv(ws, tunnels) {
|
|
1479
|
+
const folder = ws.folderPath ? path.resolve(ws.folderPath) : null;
|
|
1480
|
+
if (!folder || !fs.existsSync(folder)) return;
|
|
1481
|
+
const env = uiPublicEnv(tunnels);
|
|
1482
|
+
const lines = Object.entries(tunnels)
|
|
1483
|
+
.filter(([, url]) => url)
|
|
1484
|
+
.map(([role, url]) => `${role}=${url}`);
|
|
1485
|
+
fs.writeFileSync(
|
|
1486
|
+
path.join(folder, ".cloudflare-tunnel-url"),
|
|
1487
|
+
`${lines.join("\n")}\n`,
|
|
1488
|
+
"utf8"
|
|
1489
|
+
);
|
|
1490
|
+
const envPath = path.join(folder, ".env");
|
|
1491
|
+
if (Object.keys(env).length) mergeEnvFile(envPath, env);
|
|
1492
|
+
const localEnv = {};
|
|
1493
|
+
if (env.NEXT_PUBLIC_AI_SERVER_URL) {
|
|
1494
|
+
localEnv.NEXT_PUBLIC_AI_SERVER_URL = env.NEXT_PUBLIC_AI_SERVER_URL;
|
|
1495
|
+
}
|
|
1496
|
+
if (env.NEXT_PUBLIC_API_URL) {
|
|
1497
|
+
localEnv.NEXT_PUBLIC_API_URL = env.NEXT_PUBLIC_API_URL;
|
|
1498
|
+
}
|
|
1499
|
+
if (Object.keys(localEnv).length) {
|
|
1500
|
+
mergeEnvFile(path.join(folder, ".env.local"), localEnv);
|
|
1501
|
+
}
|
|
1369
1502
|
}
|
|
1370
1503
|
|
|
1371
1504
|
async function configureCloudflareForWorkspace(ws, cfg) {
|
|
1372
1505
|
const sandboxId = ws.sandboxId;
|
|
1373
1506
|
const label = ws.sandboxName || "this sandbox";
|
|
1374
|
-
const
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
appUrl: existing.url,
|
|
1381
|
-
origins: [existing.url],
|
|
1382
|
-
localUrl,
|
|
1383
|
-
cloudflare: true,
|
|
1384
|
-
reused: true,
|
|
1385
|
-
};
|
|
1507
|
+
const folder = path.resolve(ws.folderPath || "");
|
|
1508
|
+
const reserved = new Set();
|
|
1509
|
+
for (const other of cfg.workspaces || []) {
|
|
1510
|
+
if (other.sandboxId !== sandboxId && other.port) {
|
|
1511
|
+
reserved.add(Number(other.port));
|
|
1512
|
+
}
|
|
1386
1513
|
}
|
|
1387
|
-
if (existing) stopCloudflare(sandboxId);
|
|
1388
1514
|
|
|
1389
|
-
log(`
|
|
1390
|
-
let child;
|
|
1515
|
+
log(`Cloudflare setup for ${label}: stop apps, then tunnel ai-server/backend before UI`);
|
|
1391
1516
|
try {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
resolution:
|
|
1402
|
-
"Install cloudflared (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) or allow npx to download it, then try again.",
|
|
1403
|
-
actionCode: "configure_cloudflare",
|
|
1404
|
-
});
|
|
1405
|
-
throw new Error(
|
|
1406
|
-
`Could not start cloudflared. Install it or allow npx to download it. ${message}`
|
|
1517
|
+
await stopCloudflare(sandboxId);
|
|
1518
|
+
await stopWorkspaceApps(ws);
|
|
1519
|
+
|
|
1520
|
+
if (!cfg.noAiServer) {
|
|
1521
|
+
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
1522
|
+
await waitUntilReachable(
|
|
1523
|
+
`http://127.0.0.1:${ws.port}/embed-config.js`,
|
|
1524
|
+
45_000,
|
|
1525
|
+
"Chat script"
|
|
1407
1526
|
);
|
|
1408
1527
|
}
|
|
1409
1528
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
if (row?.child === child) cloudflareTunnels.delete(sandboxId);
|
|
1414
|
-
});
|
|
1529
|
+
const jobs = planHostJobs(folder, null, ws.projectInfo);
|
|
1530
|
+
const backendJob = jobs.find((job) => job.role === "backend");
|
|
1531
|
+
const uiJob = jobs.find((job) => job.role === "ui" || job.role === "app");
|
|
1415
1532
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1533
|
+
if (backendJob) {
|
|
1534
|
+
await ensureHostProcesses(ws, {
|
|
1535
|
+
reserved,
|
|
1536
|
+
cfg,
|
|
1537
|
+
onlyRoles: ["backend"],
|
|
1538
|
+
force: true,
|
|
1539
|
+
});
|
|
1540
|
+
const backendPort = Number(backendJob.preferredPort) || 4100;
|
|
1541
|
+
await waitUntilReachable(
|
|
1542
|
+
`http://127.0.0.1:${backendPort}`,
|
|
1543
|
+
45_000,
|
|
1544
|
+
"Backend"
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1419
1547
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1548
|
+
/** @type {Record<string, string>} */
|
|
1549
|
+
const tunnels = {};
|
|
1550
|
+
/** @type {Array<{ role: string, localUrl: string, publicUrl: string | null, logFile: string }>} */
|
|
1551
|
+
const started = [];
|
|
1552
|
+
|
|
1553
|
+
const aiLocal = `http://127.0.0.1:${Number(ws.port) || 3100}`;
|
|
1554
|
+
const aiTunnel = await startCloudflareTerminal(ws, "ai", aiLocal);
|
|
1555
|
+
tunnels.ai = aiTunnel.publicUrl;
|
|
1556
|
+
started.push(aiTunnel);
|
|
1557
|
+
log(`Cloudflare chat script: ${aiTunnel.publicUrl}`);
|
|
1558
|
+
|
|
1559
|
+
if (backendJob) {
|
|
1560
|
+
const backendLocal = `http://127.0.0.1:${Number(backendJob.preferredPort) || 4100}`;
|
|
1561
|
+
const backendTunnel = await startCloudflareTerminal(ws, "backend", backendLocal);
|
|
1562
|
+
tunnels.backend = backendTunnel.publicUrl;
|
|
1563
|
+
started.push(backendTunnel);
|
|
1564
|
+
log(`Cloudflare backend: ${backendTunnel.publicUrl}`);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
writeTunnelEnv(ws, tunnels);
|
|
1568
|
+
const uiEnv = uiPublicEnv(tunnels);
|
|
1569
|
+
|
|
1570
|
+
if (uiJob) {
|
|
1571
|
+
await ensureHostProcesses(ws, {
|
|
1572
|
+
reserved,
|
|
1573
|
+
cfg,
|
|
1574
|
+
onlyRoles: ["ui", "app"],
|
|
1575
|
+
extraEnv: uiEnv,
|
|
1576
|
+
force: true,
|
|
1577
|
+
});
|
|
1578
|
+
const uiPort = Number(uiJob.preferredPort) || 5173;
|
|
1579
|
+
await waitUntilReachable(`http://127.0.0.1:${uiPort}`, 60_000, "App UI");
|
|
1580
|
+
const uiTunnel = await startCloudflareTerminal(
|
|
1581
|
+
ws,
|
|
1582
|
+
"ui",
|
|
1583
|
+
`http://127.0.0.1:${uiPort}`
|
|
1428
1584
|
);
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
envValues.NEXT_PUBLIC_AI_SERVER_URL = publicUrl;
|
|
1438
|
-
}
|
|
1439
|
-
if (fs.existsSync(envPath)) mergeEnvFile(envPath, envValues);
|
|
1585
|
+
tunnels.ui = uiTunnel.publicUrl;
|
|
1586
|
+
started.push(uiTunnel);
|
|
1587
|
+
log(`Cloudflare UI: ${uiTunnel.publicUrl}`);
|
|
1588
|
+
writeTunnelEnv(ws, tunnels);
|
|
1589
|
+
launchedAt.delete(`${sandboxId}:${folder}:ai`);
|
|
1590
|
+
await killPort(ws.port);
|
|
1591
|
+
await sleep(1500);
|
|
1592
|
+
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
1440
1593
|
}
|
|
1594
|
+
|
|
1595
|
+
const appUrl = tunnels.ui || tunnels.ai;
|
|
1596
|
+
ws.cloudflareUrl = appUrl;
|
|
1597
|
+
ws.cloudflare = tunnels;
|
|
1598
|
+
ws.appUrl = appUrl;
|
|
1441
1599
|
persistWorkspaceEntry(cfg, ws);
|
|
1600
|
+
cloudflareTunnels.set(sandboxId, { tunnels: started });
|
|
1442
1601
|
clearProcessProblem(sandboxId, "cloudflare_launch", "tunnel");
|
|
1443
|
-
|
|
1602
|
+
|
|
1444
1603
|
return {
|
|
1445
1604
|
sandboxId,
|
|
1446
1605
|
folderPath: ws.folderPath,
|
|
1447
|
-
appUrl
|
|
1448
|
-
origins:
|
|
1449
|
-
|
|
1606
|
+
appUrl,
|
|
1607
|
+
origins: Object.values(tunnels).filter(Boolean),
|
|
1608
|
+
tunnels,
|
|
1450
1609
|
cloudflare: true,
|
|
1451
1610
|
reused: false,
|
|
1452
1611
|
};
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1614
|
+
recordProcessProblem({
|
|
1615
|
+
sandboxId,
|
|
1616
|
+
code: "cloudflare_launch",
|
|
1617
|
+
role: "tunnel",
|
|
1618
|
+
title: `Could not start Cloudflare (${label})`,
|
|
1619
|
+
message,
|
|
1620
|
+
resolution:
|
|
1621
|
+
"Install cloudflared or allow npx to download it, then try Share with Cloudflare again.",
|
|
1622
|
+
actionCode: "configure_cloudflare",
|
|
1623
|
+
});
|
|
1624
|
+
throw err;
|
|
1461
1625
|
}
|
|
1462
1626
|
}
|
|
1463
1627
|
|
|
@@ -1466,7 +1630,13 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1466
1630
|
const cfg = opts.cfg || null;
|
|
1467
1631
|
const folder = path.resolve(ws.folderPath);
|
|
1468
1632
|
const scripts = readPackageJson(folder)?.scripts || {};
|
|
1469
|
-
const jobs = planHostJobs(
|
|
1633
|
+
const jobs = planHostJobs(
|
|
1634
|
+
folder,
|
|
1635
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
1636
|
+
ws.projectInfo
|
|
1637
|
+
);
|
|
1638
|
+
const onlyRoles = Array.isArray(opts.onlyRoles) ? new Set(opts.onlyRoles) : null;
|
|
1639
|
+
const extraEnv = opts.extraEnv && typeof opts.extraEnv === "object" ? opts.extraEnv : {};
|
|
1470
1640
|
const started = [];
|
|
1471
1641
|
let persisted = false;
|
|
1472
1642
|
const label = ws.sandboxName || "this sandbox";
|
|
@@ -1484,6 +1654,7 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1484
1654
|
}
|
|
1485
1655
|
|
|
1486
1656
|
for (const job of jobs) {
|
|
1657
|
+
if (onlyRoles && !onlyRoles.has(job.role)) continue;
|
|
1487
1658
|
const preferred = Number(job.preferredPort) || 3000;
|
|
1488
1659
|
const probe = (job.probeUrl || `http://127.0.0.1:${preferred}`).replace(
|
|
1489
1660
|
"localhost",
|
|
@@ -1497,7 +1668,7 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1497
1668
|
continue;
|
|
1498
1669
|
}
|
|
1499
1670
|
const launchKey = `${ws.sandboxId}:${folder}:${job.script}`;
|
|
1500
|
-
if (recentlyLaunched(launchKey)) {
|
|
1671
|
+
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
1501
1672
|
reserved.add(preferred);
|
|
1502
1673
|
continue;
|
|
1503
1674
|
}
|
|
@@ -1531,8 +1702,9 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1531
1702
|
title: `MP-${job.role}-${port}`,
|
|
1532
1703
|
folder,
|
|
1533
1704
|
command,
|
|
1534
|
-
env: { PORT: String(port) },
|
|
1705
|
+
env: { PORT: String(port), ...extraEnv },
|
|
1535
1706
|
launchKey,
|
|
1707
|
+
force: Boolean(opts.force),
|
|
1536
1708
|
});
|
|
1537
1709
|
if (opened.skipped) continue;
|
|
1538
1710
|
if (!opened.ok) {
|
|
@@ -1563,7 +1735,11 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1563
1735
|
|
|
1564
1736
|
async function inspectHostJobs(ws) {
|
|
1565
1737
|
const folder = path.resolve(ws.folderPath || "");
|
|
1566
|
-
const jobs = planHostJobs(
|
|
1738
|
+
const jobs = planHostJobs(
|
|
1739
|
+
folder,
|
|
1740
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
1741
|
+
ws.projectInfo
|
|
1742
|
+
);
|
|
1567
1743
|
const label = ws.sandboxName || "this sandbox";
|
|
1568
1744
|
const hosts = [];
|
|
1569
1745
|
for (const job of jobs) {
|