@maintainer-pro/ai-bridge 0.1.2 → 0.1.4
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 +517 -206
package/package.json
CHANGED
package/src/daemon.mjs
CHANGED
|
@@ -850,7 +850,65 @@ const launchedAt = new Map();
|
|
|
850
850
|
/** Last process problems to send on heartbeat. Key: sandboxId::code::role */
|
|
851
851
|
const processProblems = new Map();
|
|
852
852
|
|
|
853
|
-
|
|
853
|
+
/** @type {Map<string, Set<string>>} sandboxId -> CMD/terminal titles we opened */
|
|
854
|
+
const openedTerminalTitles = new Map();
|
|
855
|
+
|
|
856
|
+
function rememberTerminalTitle(sandboxId, title) {
|
|
857
|
+
const id = String(sandboxId || "").trim();
|
|
858
|
+
const name = String(title || "").trim();
|
|
859
|
+
if (!id || !name) return;
|
|
860
|
+
let titles = openedTerminalTitles.get(id);
|
|
861
|
+
if (!titles) {
|
|
862
|
+
titles = new Set();
|
|
863
|
+
openedTerminalTitles.set(id, titles);
|
|
864
|
+
}
|
|
865
|
+
titles.add(name);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function closeWindowsByTitle(title) {
|
|
869
|
+
const name = String(title || "").trim();
|
|
870
|
+
if (!name) return Promise.resolve();
|
|
871
|
+
return new Promise((resolve) => {
|
|
872
|
+
const done = () => resolve();
|
|
873
|
+
if (process.platform === "win32") {
|
|
874
|
+
const child = spawn(
|
|
875
|
+
"taskkill",
|
|
876
|
+
["/F", "/T", "/FI", `WINDOWTITLE eq ${name}*`],
|
|
877
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
878
|
+
);
|
|
879
|
+
child.on("exit", done);
|
|
880
|
+
child.on("error", done);
|
|
881
|
+
setTimeout(done, 4000);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (process.platform === "darwin") {
|
|
885
|
+
const child = spawn(
|
|
886
|
+
"osascript",
|
|
887
|
+
[
|
|
888
|
+
"-e",
|
|
889
|
+
`tell application "Terminal" to close (every window whose name contains ${JSON.stringify(name)})`,
|
|
890
|
+
],
|
|
891
|
+
{ stdio: "ignore" }
|
|
892
|
+
);
|
|
893
|
+
child.on("exit", done);
|
|
894
|
+
child.on("error", done);
|
|
895
|
+
setTimeout(done, 4000);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
done();
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function closeRememberedTerminals(sandboxId) {
|
|
903
|
+
const titles = openedTerminalTitles.get(sandboxId);
|
|
904
|
+
if (!titles) return;
|
|
905
|
+
for (const title of titles) {
|
|
906
|
+
await closeWindowsByTitle(title);
|
|
907
|
+
}
|
|
908
|
+
openedTerminalTitles.delete(sandboxId);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function forgetLaunch(sandboxId) {
|
|
854
912
|
for (const key of [...launchedAt.keys()]) {
|
|
855
913
|
if (key === sandboxId || key.startsWith(`${sandboxId}:`)) {
|
|
856
914
|
launchedAt.delete(key);
|
|
@@ -859,7 +917,8 @@ function forgetLaunch(sandboxId) {
|
|
|
859
917
|
for (const key of [...processProblems.keys()]) {
|
|
860
918
|
if (key.startsWith(`${sandboxId}::`)) processProblems.delete(key);
|
|
861
919
|
}
|
|
862
|
-
|
|
920
|
+
await closeRememberedTerminals(sandboxId);
|
|
921
|
+
await stopCloudflare(sandboxId);
|
|
863
922
|
}
|
|
864
923
|
|
|
865
924
|
function problemKey(sandboxId, code, role = "") {
|
|
@@ -1073,9 +1132,11 @@ function runLauncher(command, args, extra = {}) {
|
|
|
1073
1132
|
}
|
|
1074
1133
|
|
|
1075
1134
|
async function openInNewTerminal(opts) {
|
|
1076
|
-
const { title, folder, command, env = {}, launchKey } = opts;
|
|
1135
|
+
const { title, folder, command, env = {}, launchKey, sandboxId } = opts;
|
|
1077
1136
|
if (launchKey) {
|
|
1078
|
-
if (recentlyLaunched(launchKey))
|
|
1137
|
+
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
1138
|
+
return { ok: true, skipped: true };
|
|
1139
|
+
}
|
|
1079
1140
|
launchedAt.set(launchKey, Date.now());
|
|
1080
1141
|
}
|
|
1081
1142
|
if (!fs.existsSync(folder)) {
|
|
@@ -1084,6 +1145,13 @@ async function openInNewTerminal(opts) {
|
|
|
1084
1145
|
return { ok: false, error };
|
|
1085
1146
|
}
|
|
1086
1147
|
|
|
1148
|
+
await closeWindowsByTitle(title);
|
|
1149
|
+
await sleep(250);
|
|
1150
|
+
rememberTerminalTitle(
|
|
1151
|
+
sandboxId || String(launchKey || "").split(":")[0],
|
|
1152
|
+
title
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1087
1155
|
const envWin = Object.entries(env)
|
|
1088
1156
|
.map(([key, value]) => `set ${key}=${value}`)
|
|
1089
1157
|
.join("&& ");
|
|
@@ -1096,18 +1164,14 @@ async function openInNewTerminal(opts) {
|
|
|
1096
1164
|
try {
|
|
1097
1165
|
if (process.platform === "win32") {
|
|
1098
1166
|
const inner = `cd /d "${folder}" && ${envWin ? `${envWin}&& ` : ""}title ${title}&& ${command}`;
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
"
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
`Start-Process -FilePath $env:ComSpec -WorkingDirectory ${JSON.stringify(folder)} -ArgumentList @('/k', '${escaped}')`,
|
|
1108
|
-
],
|
|
1109
|
-
{ windowsHide: true }
|
|
1110
|
-
);
|
|
1167
|
+
const opened = await runLauncher(process.env.ComSpec || "cmd.exe", [
|
|
1168
|
+
"/c",
|
|
1169
|
+
"start",
|
|
1170
|
+
title,
|
|
1171
|
+
"cmd.exe",
|
|
1172
|
+
"/k",
|
|
1173
|
+
inner,
|
|
1174
|
+
]);
|
|
1111
1175
|
if (!opened.ok) {
|
|
1112
1176
|
return { ok: false, error: friendlyLaunchError(opened.error, title) };
|
|
1113
1177
|
}
|
|
@@ -1253,6 +1317,7 @@ async function startAiServerForWorkspace(ws, opts = {}) {
|
|
|
1253
1317
|
NEXT_PUBLIC_AI_SERVER_URL: `http://localhost:${port}`,
|
|
1254
1318
|
},
|
|
1255
1319
|
launchKey,
|
|
1320
|
+
sandboxId: ws.sandboxId,
|
|
1256
1321
|
});
|
|
1257
1322
|
if (opened.skipped) {
|
|
1258
1323
|
return { port, up: false, launched: false, starting: true };
|
|
@@ -1276,189 +1341,423 @@ function sleep(ms) {
|
|
|
1276
1341
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1277
1342
|
}
|
|
1278
1343
|
|
|
1279
|
-
/** @type {Map<string, {
|
|
1344
|
+
/** @type {Map<string, { tunnels: Array<{ role: string, localUrl: string, publicUrl: string | null, logFile: string }> }>} */
|
|
1280
1345
|
const cloudflareTunnels = new Map();
|
|
1281
1346
|
|
|
1347
|
+
function stopAllCloudflare() {
|
|
1348
|
+
for (const id of [...cloudflareTunnels.keys()]) stopCloudflare(id);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function parseTryCloudflareUrl(text) {
|
|
1352
|
+
const match = String(text || "").match(
|
|
1353
|
+
/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i
|
|
1354
|
+
);
|
|
1355
|
+
return match ? match[0].replace(/\/$/, "") : null;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function killProcessesByCommand(fragment) {
|
|
1359
|
+
if (!fragment) return Promise.resolve();
|
|
1360
|
+
return new Promise((resolve) => {
|
|
1361
|
+
const done = () => resolve();
|
|
1362
|
+
if (process.platform === "win32") {
|
|
1363
|
+
const escaped = String(fragment).replace(/'/g, "''");
|
|
1364
|
+
const child = spawn(
|
|
1365
|
+
"powershell.exe",
|
|
1366
|
+
[
|
|
1367
|
+
"-NoProfile",
|
|
1368
|
+
"-Command",
|
|
1369
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${escaped}*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`,
|
|
1370
|
+
],
|
|
1371
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
1372
|
+
);
|
|
1373
|
+
child.on("exit", done);
|
|
1374
|
+
child.on("error", done);
|
|
1375
|
+
setTimeout(done, 8000);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const child = spawn("pkill", ["-f", String(fragment)], { stdio: "ignore" });
|
|
1379
|
+
child.on("exit", done);
|
|
1380
|
+
child.on("error", done);
|
|
1381
|
+
setTimeout(done, 4000);
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function killPort(port) {
|
|
1386
|
+
const n = Number(port);
|
|
1387
|
+
if (!n) return Promise.resolve();
|
|
1388
|
+
return new Promise((resolve) => {
|
|
1389
|
+
const done = () => resolve();
|
|
1390
|
+
if (process.platform === "win32") {
|
|
1391
|
+
const child = spawn(
|
|
1392
|
+
"powershell.exe",
|
|
1393
|
+
[
|
|
1394
|
+
"-NoProfile",
|
|
1395
|
+
"-Command",
|
|
1396
|
+
`$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 }`,
|
|
1397
|
+
],
|
|
1398
|
+
{ windowsHide: true, stdio: "ignore" }
|
|
1399
|
+
);
|
|
1400
|
+
child.on("exit", done);
|
|
1401
|
+
child.on("error", done);
|
|
1402
|
+
setTimeout(done, 8000);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const child = spawn(
|
|
1406
|
+
"sh",
|
|
1407
|
+
["-c", `pids=$(lsof -ti tcp:${n} 2>/dev/null); [ -n "$pids" ] && kill $pids`],
|
|
1408
|
+
{ stdio: "ignore" }
|
|
1409
|
+
);
|
|
1410
|
+
child.on("exit", done);
|
|
1411
|
+
child.on("error", done);
|
|
1412
|
+
setTimeout(done, 4000);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1282
1416
|
function stopCloudflare(sandboxId) {
|
|
1283
1417
|
const row = cloudflareTunnels.get(sandboxId);
|
|
1284
|
-
|
|
1418
|
+
const files = row?.tunnels?.map((t) => t.logFile).filter(Boolean) ?? [];
|
|
1419
|
+
cloudflareTunnels.delete(sandboxId);
|
|
1420
|
+
return Promise.all(files.map((file) => killProcessesByCommand(file)));
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function forgetProcessLaunches(sandboxId) {
|
|
1424
|
+
for (const key of [...launchedAt.keys()]) {
|
|
1425
|
+
if (key === sandboxId || key.startsWith(`${sandboxId}:`)) {
|
|
1426
|
+
launchedAt.delete(key);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function stopWorkspaceApps(ws) {
|
|
1432
|
+
const folder = path.resolve(ws.folderPath || "");
|
|
1433
|
+
const jobs = planHostJobs(folder, null, ws.projectInfo);
|
|
1434
|
+
const ports = new Set();
|
|
1435
|
+
if (ws.port) ports.add(Number(ws.port));
|
|
1436
|
+
for (const job of jobs) {
|
|
1437
|
+
if (job.preferredPort) ports.add(Number(job.preferredPort));
|
|
1438
|
+
}
|
|
1439
|
+
if (ws.appUrl && isLocalAppUrl(ws.appUrl)) {
|
|
1285
1440
|
try {
|
|
1286
|
-
|
|
1441
|
+
const port = Number(new URL(ws.appUrl).port);
|
|
1442
|
+
if (port) ports.add(port);
|
|
1287
1443
|
} catch {
|
|
1288
1444
|
/* ignore */
|
|
1289
1445
|
}
|
|
1290
1446
|
}
|
|
1291
|
-
|
|
1447
|
+
log(`stopping local apps on ports ${[...ports].join(", ") || "(none)"}`);
|
|
1448
|
+
await closeRememberedTerminals(ws.sandboxId);
|
|
1449
|
+
for (const port of ports) {
|
|
1450
|
+
await killPort(port);
|
|
1451
|
+
}
|
|
1452
|
+
forgetProcessLaunches(ws.sandboxId);
|
|
1453
|
+
await sleep(400);
|
|
1292
1454
|
}
|
|
1293
1455
|
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1456
|
+
async function waitUntilReachable(url, timeoutMs, label) {
|
|
1457
|
+
const start = Date.now();
|
|
1458
|
+
while (Date.now() - start < timeoutMs) {
|
|
1459
|
+
if (await probeUrl(url)) return true;
|
|
1460
|
+
await sleep(600);
|
|
1461
|
+
}
|
|
1462
|
+
throw new Error(`${label} did not become reachable at ${url}`);
|
|
1296
1463
|
}
|
|
1297
1464
|
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
|
|
1465
|
+
async function waitForUrlInFile(file, timeoutMs = 90_000) {
|
|
1466
|
+
const start = Date.now();
|
|
1467
|
+
while (Date.now() - start < timeoutMs) {
|
|
1468
|
+
if (fs.existsSync(file)) {
|
|
1469
|
+
const url = parseTryCloudflareUrl(fs.readFileSync(file, "utf8"));
|
|
1470
|
+
if (url) return url;
|
|
1471
|
+
}
|
|
1472
|
+
await sleep(500);
|
|
1473
|
+
}
|
|
1474
|
+
throw new Error(
|
|
1475
|
+
`Cloudflare did not publish a URL in time (${path.basename(file)}). Check that terminal.`
|
|
1476
|
+
);
|
|
1301
1477
|
}
|
|
1302
1478
|
|
|
1303
|
-
function
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
);
|
|
1307
|
-
return
|
|
1479
|
+
function cloudflaredCommand(localUrl, logFile) {
|
|
1480
|
+
const logArg = JSON.stringify(logFile);
|
|
1481
|
+
const run = `npx --yes cloudflared tunnel --url ${localUrl} --no-autoupdate --logfile ${logArg}`;
|
|
1482
|
+
if (process.platform === "win32") return run;
|
|
1483
|
+
return `${run} 2>&1 | tee ${logArg}`;
|
|
1308
1484
|
}
|
|
1309
1485
|
|
|
1310
|
-
function
|
|
1311
|
-
const
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1486
|
+
async function startCloudflareTerminal(ws, role, localUrl) {
|
|
1487
|
+
const folder = path.resolve(ws.folderPath);
|
|
1488
|
+
const logDir = path.join(folder, ".maintainer-pro");
|
|
1489
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1490
|
+
const logFile = path.join(
|
|
1491
|
+
logDir,
|
|
1492
|
+
`cf-${String(ws.sandboxId).slice(0, 8)}-${role}.log`
|
|
1493
|
+
);
|
|
1494
|
+
try {
|
|
1495
|
+
fs.unlinkSync(logFile);
|
|
1496
|
+
} catch {
|
|
1497
|
+
/* ignore */
|
|
1498
|
+
}
|
|
1499
|
+
const opened = await openInNewTerminal({
|
|
1500
|
+
title: `MP-cf-${role}`,
|
|
1501
|
+
folder,
|
|
1502
|
+
command: cloudflaredCommand(localUrl, logFile),
|
|
1503
|
+
launchKey: `${ws.sandboxId}:cf:${role}`,
|
|
1504
|
+
sandboxId: ws.sandboxId,
|
|
1505
|
+
force: true,
|
|
1506
|
+
});
|
|
1507
|
+
if (!opened.ok) {
|
|
1508
|
+
throw new Error(opened.error || `Could not open a Cloudflare terminal for ${role}`);
|
|
1509
|
+
}
|
|
1510
|
+
const publicUrl = await waitForUrlInFile(logFile);
|
|
1511
|
+
return { role, localUrl, publicUrl, logFile };
|
|
1512
|
+
}
|
|
1326
1513
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1514
|
+
function uiPublicEnv(tunnels) {
|
|
1515
|
+
/** @type {Record<string, string>} */
|
|
1516
|
+
const env = {};
|
|
1517
|
+
if (tunnels.ai) {
|
|
1518
|
+
env.AI_SERVER_URL = tunnels.ai;
|
|
1519
|
+
env.NEXT_PUBLIC_AI_SERVER_URL = tunnels.ai;
|
|
1520
|
+
env.VITE_AI_SERVER_URL = tunnels.ai;
|
|
1521
|
+
}
|
|
1522
|
+
if (tunnels.backend) {
|
|
1523
|
+
env.API_URL = tunnels.backend;
|
|
1524
|
+
env.API_BASE_URL = tunnels.backend;
|
|
1525
|
+
env.VITE_API_URL = tunnels.backend;
|
|
1526
|
+
env.VITE_API_BASE_URL = tunnels.backend;
|
|
1527
|
+
env.NEXT_PUBLIC_API_URL = tunnels.backend;
|
|
1528
|
+
env.NEXT_PUBLIC_API_BASE_URL = tunnels.backend;
|
|
1529
|
+
env.BACKEND_URL = tunnels.backend;
|
|
1530
|
+
}
|
|
1531
|
+
if (tunnels.ui) {
|
|
1532
|
+
env.APP_URL = tunnels.ui;
|
|
1533
|
+
env.PUBLIC_URL = tunnels.ui;
|
|
1534
|
+
env.CORS_ORIGIN = tunnels.ui;
|
|
1535
|
+
} else if (tunnels.ai) {
|
|
1536
|
+
env.CORS_ORIGIN = tunnels.ai;
|
|
1537
|
+
}
|
|
1538
|
+
return env;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function writeTunnelEnv(ws, tunnels) {
|
|
1542
|
+
const folder = ws.folderPath ? path.resolve(ws.folderPath) : null;
|
|
1543
|
+
if (!folder || !fs.existsSync(folder)) return;
|
|
1544
|
+
const env = uiPublicEnv(tunnels);
|
|
1545
|
+
const lines = Object.entries(tunnels)
|
|
1546
|
+
.filter(([, url]) => url)
|
|
1547
|
+
.map(([role, url]) => `${role}=${url}`);
|
|
1548
|
+
fs.writeFileSync(
|
|
1549
|
+
path.join(folder, ".cloudflare-tunnel-url"),
|
|
1550
|
+
`${lines.join("\n")}\n`,
|
|
1551
|
+
"utf8"
|
|
1333
1552
|
);
|
|
1553
|
+
const envPath = path.join(folder, ".env");
|
|
1554
|
+
if (Object.keys(env).length) mergeEnvFile(envPath, env);
|
|
1555
|
+
const localEnv = {};
|
|
1556
|
+
if (env.NEXT_PUBLIC_AI_SERVER_URL) {
|
|
1557
|
+
localEnv.NEXT_PUBLIC_AI_SERVER_URL = env.NEXT_PUBLIC_AI_SERVER_URL;
|
|
1558
|
+
}
|
|
1559
|
+
if (env.NEXT_PUBLIC_API_URL) {
|
|
1560
|
+
localEnv.NEXT_PUBLIC_API_URL = env.NEXT_PUBLIC_API_URL;
|
|
1561
|
+
}
|
|
1562
|
+
if (Object.keys(localEnv).length) {
|
|
1563
|
+
mergeEnvFile(path.join(folder, ".env.local"), localEnv);
|
|
1564
|
+
}
|
|
1334
1565
|
}
|
|
1335
1566
|
|
|
1336
|
-
function
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
buffer += String(chunk);
|
|
1349
|
-
const url = parseTryCloudflareUrl(buffer);
|
|
1350
|
-
if (url) done(null, url);
|
|
1351
|
-
};
|
|
1352
|
-
child.stdout?.on("data", onData);
|
|
1353
|
-
child.stderr?.on("data", onData);
|
|
1354
|
-
child.once("exit", (code) => {
|
|
1355
|
-
done(
|
|
1356
|
-
new Error(
|
|
1357
|
-
`cloudflared exited ${code ?? "early"} before it published a URL`
|
|
1358
|
-
)
|
|
1359
|
-
);
|
|
1360
|
-
});
|
|
1361
|
-
const timer = setTimeout(() => {
|
|
1362
|
-
done(
|
|
1363
|
-
new Error(
|
|
1364
|
-
"Cloudflare did not publish a URL in time. Is cloudflared installed and online?"
|
|
1365
|
-
)
|
|
1366
|
-
);
|
|
1367
|
-
}, timeoutMs);
|
|
1368
|
-
});
|
|
1567
|
+
function reservedPortsFor(cfg, sandboxId) {
|
|
1568
|
+
const reserved = new Set();
|
|
1569
|
+
for (const other of cfg.workspaces || []) {
|
|
1570
|
+
if (other.sandboxId !== sandboxId && other.port) {
|
|
1571
|
+
reserved.add(Number(other.port));
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return reserved;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function appsWanted(ws) {
|
|
1578
|
+
return Boolean(ws?.appsRequested);
|
|
1369
1579
|
}
|
|
1370
1580
|
|
|
1371
1581
|
async function configureCloudflareForWorkspace(ws, cfg) {
|
|
1372
1582
|
const sandboxId = ws.sandboxId;
|
|
1373
1583
|
const label = ws.sandboxName || "this sandbox";
|
|
1374
|
-
const localUrl = localUrlForWorkspace(ws);
|
|
1375
|
-
const existing = cloudflareTunnels.get(sandboxId);
|
|
1376
|
-
if (existing?.url && existing.localUrl === localUrl && existing.child && !existing.child.killed) {
|
|
1377
|
-
return {
|
|
1378
|
-
sandboxId,
|
|
1379
|
-
folderPath: ws.folderPath,
|
|
1380
|
-
appUrl: existing.url,
|
|
1381
|
-
origins: [existing.url],
|
|
1382
|
-
localUrl,
|
|
1383
|
-
cloudflare: true,
|
|
1384
|
-
reused: true,
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
if (existing) stopCloudflare(sandboxId);
|
|
1388
1584
|
|
|
1389
|
-
log(`
|
|
1390
|
-
|
|
1585
|
+
log(`Cloudflare queued for ${label}: stop apps and wait for Start`);
|
|
1586
|
+
await stopCloudflare(sandboxId);
|
|
1587
|
+
await stopWorkspaceApps(ws);
|
|
1588
|
+
await forgetLaunch(sandboxId);
|
|
1589
|
+
ws.cloudflarePending = true;
|
|
1590
|
+
ws.appsRequested = false;
|
|
1591
|
+
persistWorkspaceEntry(cfg, ws);
|
|
1592
|
+
|
|
1593
|
+
return {
|
|
1594
|
+
sandboxId,
|
|
1595
|
+
folderPath: ws.folderPath,
|
|
1596
|
+
port: ws.port,
|
|
1597
|
+
pending: true,
|
|
1598
|
+
cloudflarePending: true,
|
|
1599
|
+
waitingForStart: true,
|
|
1600
|
+
warning:
|
|
1601
|
+
"Cloudflare is ready in Maintainer Pro. Use Start chat server when you want to launch the apps and create the public URLs.",
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
async function launchCloudflareTunnels(ws, cfg) {
|
|
1606
|
+
const sandboxId = ws.sandboxId;
|
|
1607
|
+
const label = ws.sandboxName || "this sandbox";
|
|
1608
|
+
const folder = path.resolve(ws.folderPath || "");
|
|
1609
|
+
const reserved = reservedPortsFor(cfg, sandboxId);
|
|
1610
|
+
|
|
1611
|
+
log(`Cloudflare start for ${label}: tunnel chat script/backend before UI`);
|
|
1391
1612
|
try {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
role: "tunnel",
|
|
1399
|
-
title: `Could not start Cloudflare (${label})`,
|
|
1400
|
-
message,
|
|
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}`
|
|
1613
|
+
if (!cfg.noAiServer) {
|
|
1614
|
+
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
1615
|
+
await waitUntilReachable(
|
|
1616
|
+
`http://127.0.0.1:${ws.port}/embed-config.js`,
|
|
1617
|
+
45_000,
|
|
1618
|
+
"Chat script"
|
|
1407
1619
|
);
|
|
1408
1620
|
}
|
|
1409
1621
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1622
|
+
const jobs = planHostJobs(folder, null, ws.projectInfo);
|
|
1623
|
+
const backendJob = jobs.find((job) => job.role === "backend");
|
|
1624
|
+
const uiJob = jobs.find((job) => job.role === "ui" || job.role === "app");
|
|
1625
|
+
|
|
1626
|
+
if (backendJob) {
|
|
1627
|
+
await ensureHostProcesses(ws, {
|
|
1628
|
+
reserved,
|
|
1629
|
+
cfg,
|
|
1630
|
+
onlyRoles: ["backend"],
|
|
1631
|
+
force: true,
|
|
1632
|
+
});
|
|
1633
|
+
const backendPort = Number(backendJob.preferredPort) || 4100;
|
|
1634
|
+
await waitUntilReachable(
|
|
1635
|
+
`http://127.0.0.1:${backendPort}`,
|
|
1636
|
+
45_000,
|
|
1637
|
+
"Backend"
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1415
1640
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1641
|
+
/** @type {Record<string, string>} */
|
|
1642
|
+
const tunnels = {};
|
|
1643
|
+
/** @type {Array<{ role: string, localUrl: string, publicUrl: string | null, logFile: string }>} */
|
|
1644
|
+
const started = [];
|
|
1419
1645
|
|
|
1420
|
-
ws.
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1646
|
+
const aiLocal = `http://127.0.0.1:${Number(ws.port) || 3100}`;
|
|
1647
|
+
const aiTunnel = await startCloudflareTerminal(ws, "ai", aiLocal);
|
|
1648
|
+
tunnels.ai = aiTunnel.publicUrl;
|
|
1649
|
+
started.push(aiTunnel);
|
|
1650
|
+
log(`Cloudflare chat script: ${aiTunnel.publicUrl}`);
|
|
1651
|
+
|
|
1652
|
+
if (backendJob) {
|
|
1653
|
+
const backendLocal = `http://127.0.0.1:${Number(backendJob.preferredPort) || 4100}`;
|
|
1654
|
+
const backendTunnel = await startCloudflareTerminal(ws, "backend", backendLocal);
|
|
1655
|
+
tunnels.backend = backendTunnel.publicUrl;
|
|
1656
|
+
started.push(backendTunnel);
|
|
1657
|
+
log(`Cloudflare backend: ${backendTunnel.publicUrl}`);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
writeTunnelEnv(ws, tunnels);
|
|
1661
|
+
const uiEnv = uiPublicEnv(tunnels);
|
|
1662
|
+
|
|
1663
|
+
if (uiJob) {
|
|
1664
|
+
await ensureHostProcesses(ws, {
|
|
1665
|
+
reserved,
|
|
1666
|
+
cfg,
|
|
1667
|
+
onlyRoles: ["ui", "app"],
|
|
1668
|
+
extraEnv: uiEnv,
|
|
1669
|
+
force: true,
|
|
1670
|
+
});
|
|
1671
|
+
const uiPort = Number(uiJob.preferredPort) || 5173;
|
|
1672
|
+
await waitUntilReachable(`http://127.0.0.1:${uiPort}`, 60_000, "App UI");
|
|
1673
|
+
const uiTunnel = await startCloudflareTerminal(
|
|
1674
|
+
ws,
|
|
1675
|
+
"ui",
|
|
1676
|
+
`http://127.0.0.1:${uiPort}`
|
|
1428
1677
|
);
|
|
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);
|
|
1678
|
+
tunnels.ui = uiTunnel.publicUrl;
|
|
1679
|
+
started.push(uiTunnel);
|
|
1680
|
+
log(`Cloudflare UI: ${uiTunnel.publicUrl}`);
|
|
1681
|
+
writeTunnelEnv(ws, tunnels);
|
|
1682
|
+
launchedAt.delete(`${sandboxId}:${folder}:ai`);
|
|
1683
|
+
await killPort(ws.port);
|
|
1684
|
+
await sleep(1500);
|
|
1685
|
+
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
1440
1686
|
}
|
|
1687
|
+
|
|
1688
|
+
const appUrl = tunnels.ui || tunnels.ai;
|
|
1689
|
+
ws.cloudflareUrl = appUrl;
|
|
1690
|
+
ws.cloudflare = tunnels;
|
|
1691
|
+
ws.appUrl = appUrl;
|
|
1692
|
+
ws.cloudflarePending = false;
|
|
1693
|
+
ws.appsRequested = true;
|
|
1441
1694
|
persistWorkspaceEntry(cfg, ws);
|
|
1695
|
+
cloudflareTunnels.set(sandboxId, { tunnels: started });
|
|
1442
1696
|
clearProcessProblem(sandboxId, "cloudflare_launch", "tunnel");
|
|
1443
|
-
|
|
1697
|
+
|
|
1444
1698
|
return {
|
|
1445
1699
|
sandboxId,
|
|
1446
1700
|
folderPath: ws.folderPath,
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1701
|
+
port: ws.port,
|
|
1702
|
+
appUrl,
|
|
1703
|
+
origins: Object.values(tunnels).filter(Boolean),
|
|
1704
|
+
tunnels,
|
|
1450
1705
|
cloudflare: true,
|
|
1451
1706
|
reused: false,
|
|
1452
1707
|
};
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1710
|
+
recordProcessProblem({
|
|
1711
|
+
sandboxId,
|
|
1712
|
+
code: "cloudflare_launch",
|
|
1713
|
+
role: "tunnel",
|
|
1714
|
+
title: `Could not start Cloudflare (${label})`,
|
|
1715
|
+
message,
|
|
1716
|
+
resolution:
|
|
1717
|
+
"Install cloudflared or allow npx to download it, then use Start chat server again.",
|
|
1718
|
+
actionCode: "start_ai_server",
|
|
1719
|
+
});
|
|
1720
|
+
throw err;
|
|
1721
|
+
}
|
|
1453
1722
|
}
|
|
1454
1723
|
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1724
|
+
async function startAppsForWorkspace(ws, cfg) {
|
|
1725
|
+
if (ws.cloudflarePending) {
|
|
1726
|
+
return launchCloudflareTunnels(ws, cfg);
|
|
1727
|
+
}
|
|
1728
|
+
ws.appsRequested = true;
|
|
1729
|
+
persistWorkspaceEntry(cfg, ws);
|
|
1730
|
+
const reserved = reservedPortsFor(cfg, ws.sandboxId);
|
|
1731
|
+
if (!cfg.noAiServer) {
|
|
1732
|
+
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
1733
|
+
await sleep(1500);
|
|
1734
|
+
}
|
|
1735
|
+
const startedHosts = await ensureHostProcesses(ws, {
|
|
1736
|
+
reserved,
|
|
1737
|
+
cfg,
|
|
1738
|
+
force: true,
|
|
1739
|
+
});
|
|
1740
|
+
await sleep(800);
|
|
1741
|
+
await inspectHostJobs(ws);
|
|
1742
|
+
const up = await probeUrl(`http://127.0.0.1:${ws.port}/embed-config.js`);
|
|
1743
|
+
if (up) {
|
|
1744
|
+
clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
|
|
1745
|
+
clearProcessProblem(ws.sandboxId, "apps_not_started");
|
|
1461
1746
|
}
|
|
1747
|
+
const processIssues = issuesForSandbox(ws.sandboxId).map(
|
|
1748
|
+
({ role: _role, ...issue }) => issue
|
|
1749
|
+
);
|
|
1750
|
+
const warning = processIssues[0]?.message || null;
|
|
1751
|
+
return {
|
|
1752
|
+
up,
|
|
1753
|
+
startedHosts,
|
|
1754
|
+
sandboxId: ws.sandboxId,
|
|
1755
|
+
folderPath: ws.folderPath,
|
|
1756
|
+
port: ws.port,
|
|
1757
|
+
appUrl: ws.appUrl,
|
|
1758
|
+
processIssues,
|
|
1759
|
+
warning,
|
|
1760
|
+
};
|
|
1462
1761
|
}
|
|
1463
1762
|
|
|
1464
1763
|
async function ensureHostProcesses(ws, opts = {}) {
|
|
@@ -1466,7 +1765,13 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1466
1765
|
const cfg = opts.cfg || null;
|
|
1467
1766
|
const folder = path.resolve(ws.folderPath);
|
|
1468
1767
|
const scripts = readPackageJson(folder)?.scripts || {};
|
|
1469
|
-
const jobs = planHostJobs(
|
|
1768
|
+
const jobs = planHostJobs(
|
|
1769
|
+
folder,
|
|
1770
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
1771
|
+
ws.projectInfo
|
|
1772
|
+
);
|
|
1773
|
+
const onlyRoles = Array.isArray(opts.onlyRoles) ? new Set(opts.onlyRoles) : null;
|
|
1774
|
+
const extraEnv = opts.extraEnv && typeof opts.extraEnv === "object" ? opts.extraEnv : {};
|
|
1470
1775
|
const started = [];
|
|
1471
1776
|
let persisted = false;
|
|
1472
1777
|
const label = ws.sandboxName || "this sandbox";
|
|
@@ -1484,6 +1789,7 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1484
1789
|
}
|
|
1485
1790
|
|
|
1486
1791
|
for (const job of jobs) {
|
|
1792
|
+
if (onlyRoles && !onlyRoles.has(job.role)) continue;
|
|
1487
1793
|
const preferred = Number(job.preferredPort) || 3000;
|
|
1488
1794
|
const probe = (job.probeUrl || `http://127.0.0.1:${preferred}`).replace(
|
|
1489
1795
|
"localhost",
|
|
@@ -1497,7 +1803,7 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1497
1803
|
continue;
|
|
1498
1804
|
}
|
|
1499
1805
|
const launchKey = `${ws.sandboxId}:${folder}:${job.script}`;
|
|
1500
|
-
if (recentlyLaunched(launchKey)) {
|
|
1806
|
+
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
1501
1807
|
reserved.add(preferred);
|
|
1502
1808
|
continue;
|
|
1503
1809
|
}
|
|
@@ -1531,8 +1837,10 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1531
1837
|
title: `MP-${job.role}-${port}`,
|
|
1532
1838
|
folder,
|
|
1533
1839
|
command,
|
|
1534
|
-
env: { PORT: String(port) },
|
|
1840
|
+
env: { PORT: String(port), ...extraEnv },
|
|
1535
1841
|
launchKey,
|
|
1842
|
+
sandboxId: ws.sandboxId,
|
|
1843
|
+
force: Boolean(opts.force),
|
|
1536
1844
|
});
|
|
1537
1845
|
if (opened.skipped) continue;
|
|
1538
1846
|
if (!opened.ok) {
|
|
@@ -1563,7 +1871,11 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
1563
1871
|
|
|
1564
1872
|
async function inspectHostJobs(ws) {
|
|
1565
1873
|
const folder = path.resolve(ws.folderPath || "");
|
|
1566
|
-
const jobs = planHostJobs(
|
|
1874
|
+
const jobs = planHostJobs(
|
|
1875
|
+
folder,
|
|
1876
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
1877
|
+
ws.projectInfo
|
|
1878
|
+
);
|
|
1567
1879
|
const label = ws.sandboxName || "this sandbox";
|
|
1568
1880
|
const hosts = [];
|
|
1569
1881
|
for (const job of jobs) {
|
|
@@ -1587,6 +1899,11 @@ async function inspectHostJobs(ws) {
|
|
|
1587
1899
|
clearProcessProblem(ws.sandboxId, "host_process_down", job.role);
|
|
1588
1900
|
continue;
|
|
1589
1901
|
}
|
|
1902
|
+
if (!appsWanted(ws)) {
|
|
1903
|
+
clearProcessProblem(ws.sandboxId, "host_process_launch", job.role);
|
|
1904
|
+
clearProcessProblem(ws.sandboxId, "host_process_down", job.role);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1590
1907
|
if (starting) continue;
|
|
1591
1908
|
const tried = launchedAt.has(launchKey);
|
|
1592
1909
|
recordProcessProblem({
|
|
@@ -1687,6 +2004,8 @@ async function setupWorkspace(cfg, action) {
|
|
|
1687
2004
|
clientKind: client.kind,
|
|
1688
2005
|
appUrl,
|
|
1689
2006
|
sameOrigin: Boolean(client.sameOrigin),
|
|
2007
|
+
appsRequested: false,
|
|
2008
|
+
cloudflarePending: false,
|
|
1690
2009
|
};
|
|
1691
2010
|
if (existing >= 0) cfg.workspaces[existing] = entry;
|
|
1692
2011
|
else cfg.workspaces.push(entry);
|
|
@@ -1705,12 +2024,6 @@ async function setupWorkspace(cfg, action) {
|
|
|
1705
2024
|
.join("\n"),
|
|
1706
2025
|
});
|
|
1707
2026
|
|
|
1708
|
-
if (!cfg.noAiServer) {
|
|
1709
|
-
await startAiServerForWorkspace(entry, { reserved, cfg });
|
|
1710
|
-
await sleep(1500);
|
|
1711
|
-
}
|
|
1712
|
-
const startedHosts = await ensureHostProcesses(entry, { reserved, cfg });
|
|
1713
|
-
await sleep(800);
|
|
1714
2027
|
await inspectHostJobs(entry);
|
|
1715
2028
|
|
|
1716
2029
|
const openUrl = client.sameOrigin
|
|
@@ -1719,23 +2032,22 @@ async function setupWorkspace(cfg, action) {
|
|
|
1719
2032
|
const aiServerUp = await probeUrl(`http://127.0.0.1:${entry.port}/embed-config.js`);
|
|
1720
2033
|
if (aiServerUp) {
|
|
1721
2034
|
clearProcessProblem(sandboxId, "ai_server_launch", "ai");
|
|
2035
|
+
clearProcessProblem(sandboxId, "apps_not_started");
|
|
1722
2036
|
}
|
|
1723
2037
|
|
|
1724
2038
|
const processIssues = issuesForSandbox(sandboxId).map(
|
|
1725
2039
|
({ role: _role, ...issue }) => issue
|
|
1726
2040
|
);
|
|
1727
|
-
const
|
|
2041
|
+
const waitingForStart = !aiServerUp;
|
|
2042
|
+
const warning = waitingForStart
|
|
2043
|
+
? "Folder is attached in Maintainer Pro. Use Start chat server when you want to launch the apps."
|
|
2044
|
+
: processIssues[0]?.message || null;
|
|
1728
2045
|
|
|
1729
2046
|
for (const note of client.notes) log(note);
|
|
1730
|
-
if (
|
|
1731
|
-
log(`
|
|
1732
|
-
}
|
|
1733
|
-
if (aiServerUp) {
|
|
1734
|
-
log(`ai-server up — open ${openUrl}`);
|
|
1735
|
-
} else if (warning) {
|
|
1736
|
-
log(`process issue: ${warning}`);
|
|
2047
|
+
if (waitingForStart) {
|
|
2048
|
+
log(`folder attached — waiting for Start (${openUrl})`);
|
|
1737
2049
|
} else {
|
|
1738
|
-
log(
|
|
2050
|
+
log(`ai-server already up — open ${openUrl}`);
|
|
1739
2051
|
}
|
|
1740
2052
|
|
|
1741
2053
|
return {
|
|
@@ -1749,10 +2061,11 @@ async function setupWorkspace(cfg, action) {
|
|
|
1749
2061
|
clientFiles: client.filesWritten,
|
|
1750
2062
|
clientNotes: client.notes,
|
|
1751
2063
|
aiServerUp,
|
|
1752
|
-
startedHosts,
|
|
2064
|
+
startedHosts: [],
|
|
1753
2065
|
openUrl,
|
|
1754
2066
|
processIssues,
|
|
1755
2067
|
warning,
|
|
2068
|
+
waitingForStart,
|
|
1756
2069
|
projectInfo,
|
|
1757
2070
|
};
|
|
1758
2071
|
}
|
|
@@ -1794,12 +2107,6 @@ async function runActions(cfg, actions) {
|
|
|
1794
2107
|
ok = false;
|
|
1795
2108
|
result = { error: "No workspace or --no-ai-server" };
|
|
1796
2109
|
} else {
|
|
1797
|
-
const reserved = new Set();
|
|
1798
|
-
for (const other of cfg.workspaces || []) {
|
|
1799
|
-
if (other.sandboxId !== ws.sandboxId && other.port) {
|
|
1800
|
-
reserved.add(Number(other.port));
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
2110
|
const problem = issuesForSandbox(ws.sandboxId)
|
|
1804
2111
|
.map((issue) => issue.message)
|
|
1805
2112
|
.join("\n");
|
|
@@ -1809,32 +2116,15 @@ async function runActions(cfg, actions) {
|
|
|
1809
2116
|
problem ||
|
|
1810
2117
|
"Local processes are not running or the project setup looks incomplete.",
|
|
1811
2118
|
});
|
|
1812
|
-
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
1813
|
-
await sleep(1500);
|
|
1814
|
-
const startedHosts = await ensureHostProcesses(ws, { reserved, cfg });
|
|
1815
|
-
await sleep(800);
|
|
1816
|
-
await inspectHostJobs(ws);
|
|
1817
|
-
const up = await probeUrl(
|
|
1818
|
-
`http://127.0.0.1:${ws.port}/embed-config.js`
|
|
1819
|
-
);
|
|
1820
|
-
if (up) clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
|
|
1821
|
-
const processIssues = issuesForSandbox(ws.sandboxId).map(
|
|
1822
|
-
({ role: _role, ...issue }) => issue
|
|
1823
|
-
);
|
|
1824
|
-
const warning = processIssues[0]?.message || null;
|
|
1825
2119
|
result = {
|
|
1826
|
-
|
|
1827
|
-
startedHosts,
|
|
1828
|
-
sandboxId: ws.sandboxId,
|
|
1829
|
-
folderPath: ws.folderPath,
|
|
1830
|
-
port: ws.port,
|
|
1831
|
-
appUrl: ws.appUrl,
|
|
1832
|
-
processIssues,
|
|
1833
|
-
warning,
|
|
2120
|
+
...(await startAppsForWorkspace(ws, cfg)),
|
|
1834
2121
|
projectInfo,
|
|
1835
2122
|
};
|
|
1836
|
-
if (
|
|
1837
|
-
result.
|
|
2123
|
+
if (
|
|
2124
|
+
Array.isArray(result.processIssues) &&
|
|
2125
|
+
result.processIssues.some((issue) => issue.code === "ai_server_launch")
|
|
2126
|
+
) {
|
|
2127
|
+
result.error = result.warning;
|
|
1838
2128
|
ok = false;
|
|
1839
2129
|
}
|
|
1840
2130
|
}
|
|
@@ -1875,7 +2165,7 @@ async function runActions(cfg, actions) {
|
|
|
1875
2165
|
const sandboxId = String(
|
|
1876
2166
|
action.sandboxId || action.payload?.sandboxId || ""
|
|
1877
2167
|
);
|
|
1878
|
-
forgetLaunch(sandboxId);
|
|
2168
|
+
await forgetLaunch(sandboxId);
|
|
1879
2169
|
cfg.workspaces = (cfg.workspaces || []).filter(
|
|
1880
2170
|
(w) => w.sandboxId !== sandboxId
|
|
1881
2171
|
);
|
|
@@ -1924,6 +2214,7 @@ async function collectWorkspaceStates(cfg) {
|
|
|
1924
2214
|
aiServerUp: up,
|
|
1925
2215
|
startingAi: recentlyLaunched(`${ws.sandboxId}:${folder}:ai`),
|
|
1926
2216
|
appUrl: ws.appUrl || null,
|
|
2217
|
+
appsRequested: appsWanted(ws),
|
|
1927
2218
|
});
|
|
1928
2219
|
}
|
|
1929
2220
|
return localStates;
|
|
@@ -1946,6 +2237,7 @@ async function sendHeartbeat(cfg, folders, localStates) {
|
|
|
1946
2237
|
aiServerUp: st.aiServerUp,
|
|
1947
2238
|
port: st.port,
|
|
1948
2239
|
appUrl: st.appUrl || undefined,
|
|
2240
|
+
appsRequested: Boolean(st.appsRequested),
|
|
1949
2241
|
})),
|
|
1950
2242
|
}
|
|
1951
2243
|
);
|
|
@@ -1969,6 +2261,19 @@ async function buildIssues(cfg, workspaceStates) {
|
|
|
1969
2261
|
}
|
|
1970
2262
|
for (const st of workspaceStates) {
|
|
1971
2263
|
if (st.aiServerUp || st.startingAi) continue;
|
|
2264
|
+
if (!st.appsRequested) {
|
|
2265
|
+
issues.push({
|
|
2266
|
+
code: "apps_not_started",
|
|
2267
|
+
severity: "info",
|
|
2268
|
+
title: `Apps are not running (${st.sandboxName || "sandbox"})`,
|
|
2269
|
+
message:
|
|
2270
|
+
"This folder is attached in Maintainer Pro. Use Start chat server when you want to launch the local apps.",
|
|
2271
|
+
resolution: "Use Start chat server.",
|
|
2272
|
+
actionCode: "start_ai_server",
|
|
2273
|
+
sandboxId: st.sandboxId,
|
|
2274
|
+
});
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
1972
2277
|
const launch = [...processProblems.values()].find(
|
|
1973
2278
|
(issue) =>
|
|
1974
2279
|
issue.sandboxId === st.sandboxId && issue.code === "ai_server_launch"
|
|
@@ -2105,13 +2410,18 @@ async function main() {
|
|
|
2105
2410
|
const up = await probeUrl(
|
|
2106
2411
|
`http://127.0.0.1:${ws.port}/embed-config.js`
|
|
2107
2412
|
);
|
|
2108
|
-
if (!cfg.noAiServer && !up) {
|
|
2413
|
+
if (appsWanted(ws) && !cfg.noAiServer && !up) {
|
|
2109
2414
|
await startAiServerForWorkspace(ws, { reserved, cfg });
|
|
2110
2415
|
} else if (ws.port) {
|
|
2111
2416
|
reserved.add(Number(ws.port));
|
|
2112
|
-
if (up)
|
|
2417
|
+
if (up) {
|
|
2418
|
+
clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
|
|
2419
|
+
clearProcessProblem(ws.sandboxId, "apps_not_started");
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (appsWanted(ws)) {
|
|
2423
|
+
await ensureHostProcesses(ws, { reserved, cfg });
|
|
2113
2424
|
}
|
|
2114
|
-
await ensureHostProcesses(ws, { reserved, cfg });
|
|
2115
2425
|
await inspectHostJobs(ws);
|
|
2116
2426
|
localStates.push({
|
|
2117
2427
|
sandboxId: ws.sandboxId,
|
|
@@ -2121,6 +2431,7 @@ async function main() {
|
|
|
2121
2431
|
aiServerUp: up,
|
|
2122
2432
|
startingAi: recentlyLaunched(`${ws.sandboxId}:${folder}:ai`),
|
|
2123
2433
|
appUrl: ws.appUrl || null,
|
|
2434
|
+
appsRequested: appsWanted(ws),
|
|
2124
2435
|
});
|
|
2125
2436
|
}
|
|
2126
2437
|
|