@maintainer-pro/ai-bridge 0.1.15 → 0.1.18
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 +2 -2
- package/src/daemon.mjs +685 -838
- package/src/discarded-tunnels.mjs +32 -0
- package/src/share-rewrite.mjs +404 -40
package/src/daemon.mjs
CHANGED
|
@@ -23,8 +23,20 @@ import {
|
|
|
23
23
|
lookupShareResponse,
|
|
24
24
|
prepareShareHttpRequest,
|
|
25
25
|
processShareHttpResponse,
|
|
26
|
+
rewriteMappedLocalUrls,
|
|
27
|
+
rewriteShareRequestBody,
|
|
28
|
+
shareServiceWorkerScript,
|
|
29
|
+
shareShimScript,
|
|
30
|
+
shareTokenRoot,
|
|
26
31
|
shouldProcessShareResponse,
|
|
32
|
+
shouldRewriteBody,
|
|
27
33
|
} from "./share-rewrite.mjs";
|
|
34
|
+
import {
|
|
35
|
+
canonicalActionCode,
|
|
36
|
+
isDiscardedTunnelUrl,
|
|
37
|
+
leftoverStateKeys,
|
|
38
|
+
LEGACY_TUNNEL_FILE,
|
|
39
|
+
} from "./discarded-tunnels.mjs";
|
|
28
40
|
|
|
29
41
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
42
|
const PACKAGE_VERSION = readPackageVersion();
|
|
@@ -74,6 +86,34 @@ function shortId(value) {
|
|
|
74
86
|
return text.length > 12 ? `${text.slice(0, 8)}…` : text;
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
function sameFolder(a, b) {
|
|
90
|
+
const na = path.resolve(String(a || ""));
|
|
91
|
+
const nb = path.resolve(String(b || ""));
|
|
92
|
+
if (process.platform === "win32") return na.toLowerCase() === nb.toLowerCase();
|
|
93
|
+
return na === nb;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function workspaceFolders(ws) {
|
|
97
|
+
const primary = String(ws?.folderPath || "").trim();
|
|
98
|
+
const extra = Array.isArray(ws?.extraFolders) ? ws.extraFolders : [];
|
|
99
|
+
const out = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const raw of [primary, ...extra]) {
|
|
102
|
+
if (!raw || typeof raw !== "string") continue;
|
|
103
|
+
const resolved = path.resolve(raw);
|
|
104
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
105
|
+
if (seen.has(key)) continue;
|
|
106
|
+
seen.add(key);
|
|
107
|
+
out.push(resolved);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function folderForApp(ws, app) {
|
|
113
|
+
if (app?.folderPath) return path.resolve(String(app.folderPath));
|
|
114
|
+
return path.resolve(ws?.folderPath || "");
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
function actionLabel(action) {
|
|
78
118
|
const sandbox = action.sandboxId || action.payload?.sandboxId;
|
|
79
119
|
const folder = action.payload?.folderPath;
|
|
@@ -358,7 +398,8 @@ async function detectCliProviders() {
|
|
|
358
398
|
}
|
|
359
399
|
|
|
360
400
|
async function resolveWorkspaceHostApps(ws, opts = {}) {
|
|
361
|
-
const
|
|
401
|
+
const folders = workspaceFolders(ws);
|
|
402
|
+
const folder = folders[0] || path.resolve(ws.folderPath || "");
|
|
362
403
|
const label = ws.sandboxName || shortId(ws.sandboxId) || "sandbox";
|
|
363
404
|
const desired = Array.isArray(opts.desired)
|
|
364
405
|
? opts.desired
|
|
@@ -373,10 +414,11 @@ async function resolveWorkspaceHostApps(ws, opts = {}) {
|
|
|
373
414
|
activity(
|
|
374
415
|
ws.sandboxId,
|
|
375
416
|
"info",
|
|
376
|
-
`detecting ports for ${label} in ${folder} (env first${opts.force ? ", redetect" : ""})`
|
|
417
|
+
`detecting ports for ${label} in ${folders.join(", ") || folder} (env first${opts.force ? ", redetect" : ""})`
|
|
377
418
|
);
|
|
378
419
|
const result = await cli.resolveHostApps({
|
|
379
420
|
workspaceDir: folder,
|
|
421
|
+
workspaceDirs: folders,
|
|
380
422
|
appName: ws.applicationName || ws.sandboxName,
|
|
381
423
|
preferredAiPort: Number(ws.port) || 3100,
|
|
382
424
|
desired: opts.ignoreDesired ? [] : desired,
|
|
@@ -385,10 +427,12 @@ async function resolveWorkspaceHostApps(ws, opts = {}) {
|
|
|
385
427
|
sandboxId: ws.sandboxId,
|
|
386
428
|
});
|
|
387
429
|
const previous = Array.isArray(ws.hostApps) ? ws.hostApps : [];
|
|
430
|
+
if (opts.unionDetected && previous.length && typeof cli.unionHostApps === "function") {
|
|
431
|
+
result.apps = cli.unionHostApps(previous, result.apps);
|
|
432
|
+
}
|
|
388
433
|
result.apps = result.apps.map((app) => {
|
|
389
434
|
const match = previous.find((row) => row && row.id === app.id);
|
|
390
|
-
|
|
391
|
-
return { ...app, envMaps: match.envMaps || [] };
|
|
435
|
+
return match ? { ...app, host: match.host === true || app.host === true } : app;
|
|
392
436
|
});
|
|
393
437
|
ws.hostApps = result.apps;
|
|
394
438
|
if (opts.cfg) persistWorkspaceEntry(opts.cfg, ws);
|
|
@@ -489,6 +533,14 @@ function projectFingerprint(folder) {
|
|
|
489
533
|
"next.config.js",
|
|
490
534
|
"next.config.mjs",
|
|
491
535
|
"next.config.ts",
|
|
536
|
+
"pom.xml",
|
|
537
|
+
"build.gradle",
|
|
538
|
+
"build.gradle.kts",
|
|
539
|
+
"build.sbt",
|
|
540
|
+
"src/main/resources/application.yml",
|
|
541
|
+
"src/main/resources/application.properties",
|
|
542
|
+
"src/main/resources/application.conf",
|
|
543
|
+
"conf/application.conf",
|
|
492
544
|
]) {
|
|
493
545
|
const file = path.join(resolved, rel);
|
|
494
546
|
if (!fs.existsSync(file)) continue;
|
|
@@ -819,6 +871,67 @@ function probeUrl(url, timeoutMs = 2500) {
|
|
|
819
871
|
});
|
|
820
872
|
}
|
|
821
873
|
|
|
874
|
+
/** Some Windows apps bind `localhost` (::1) but refuse `127.0.0.1`. */
|
|
875
|
+
const LOOPBACK_HOSTS = ["127.0.0.1", "localhost"];
|
|
876
|
+
/** @type {Map<number, string>} */
|
|
877
|
+
const loopbackHostByPort = new Map();
|
|
878
|
+
|
|
879
|
+
function isLoopbackConnError(err) {
|
|
880
|
+
const code = err && typeof err === "object" ? String(err.code || "") : "";
|
|
881
|
+
return (
|
|
882
|
+
code === "ECONNREFUSED" ||
|
|
883
|
+
code === "EHOSTUNREACH" ||
|
|
884
|
+
code === "EADDRNOTAVAIL" ||
|
|
885
|
+
code === "ENOTFOUND" ||
|
|
886
|
+
code === "ETIMEDOUT"
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function loopbackHostsForPort(port) {
|
|
891
|
+
const cached = loopbackHostByPort.get(Number(port));
|
|
892
|
+
if (cached === "localhost") return ["localhost", "127.0.0.1"];
|
|
893
|
+
return LOOPBACK_HOSTS;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function rememberLoopbackHost(port, host) {
|
|
897
|
+
const n = Number(port);
|
|
898
|
+
const name = String(host || "").trim();
|
|
899
|
+
if (n && (name === "127.0.0.1" || name === "localhost")) {
|
|
900
|
+
loopbackHostByPort.set(n, name);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function loopbackUrlVariants(url) {
|
|
905
|
+
try {
|
|
906
|
+
const parsed = new URL(String(url));
|
|
907
|
+
const host = String(parsed.hostname || "").toLowerCase();
|
|
908
|
+
if (host !== "localhost" && host !== "127.0.0.1") return [String(url)];
|
|
909
|
+
const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
910
|
+
return loopbackHostsForPort(port).map((name) => {
|
|
911
|
+
const next = new URL(parsed);
|
|
912
|
+
next.hostname = name;
|
|
913
|
+
return next.toString();
|
|
914
|
+
});
|
|
915
|
+
} catch {
|
|
916
|
+
return [String(url)];
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function probeLoopbackUrl(url, timeoutMs = 2500) {
|
|
921
|
+
for (const candidate of loopbackUrlVariants(url)) {
|
|
922
|
+
if (await probeUrl(candidate, timeoutMs)) {
|
|
923
|
+
try {
|
|
924
|
+
const parsed = new URL(candidate);
|
|
925
|
+
rememberLoopbackHost(parsed.port, parsed.hostname);
|
|
926
|
+
} catch {
|
|
927
|
+
/* ignore */
|
|
928
|
+
}
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
|
|
822
935
|
function adminListenPort(cfg = bridgeCfg) {
|
|
823
936
|
try {
|
|
824
937
|
const u = new URL(String(cfg?.adminUrl || "http://localhost:4100"));
|
|
@@ -1027,9 +1140,8 @@ function mergeEnvFile(file, values, opts = {}) {
|
|
|
1027
1140
|
}
|
|
1028
1141
|
|
|
1029
1142
|
/**
|
|
1030
|
-
* Do not write Maintainer Pro keys into the app.
|
|
1031
|
-
*
|
|
1032
|
-
* Only strip leftover trycloudflare values we previously wrote.
|
|
1143
|
+
* Do not write Maintainer Pro keys or share URLs into the app.
|
|
1144
|
+
* Only strip leftover tunnel URLs we previously wrote.
|
|
1033
1145
|
*/
|
|
1034
1146
|
function writeProjectEnv(folder, _values, opts = {}) {
|
|
1035
1147
|
const remove = (opts.remove || []).filter(Boolean);
|
|
@@ -1061,37 +1173,9 @@ function readProjectEnvValues(folder) {
|
|
|
1061
1173
|
return map;
|
|
1062
1174
|
}
|
|
1063
1175
|
|
|
1064
|
-
function
|
|
1065
|
-
try {
|
|
1066
|
-
return new URL(String(value || "").trim()).hostname.endsWith(
|
|
1067
|
-
".trycloudflare.com"
|
|
1068
|
-
);
|
|
1069
|
-
} catch {
|
|
1070
|
-
return false;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function readCloudflareTunnelFile(folder) {
|
|
1075
|
-
if (!folder) return null;
|
|
1076
|
-
const file = path.join(path.resolve(folder), ".cloudflare-tunnel-url");
|
|
1077
|
-
if (!fs.existsSync(file)) return null;
|
|
1078
|
-
/** @type {Record<string, string>} */
|
|
1079
|
-
const tunnels = {};
|
|
1080
|
-
for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
1081
|
-
const line = raw.trim();
|
|
1082
|
-
if (!line || line.startsWith("#")) continue;
|
|
1083
|
-
const eq = line.indexOf("=");
|
|
1084
|
-
if (eq < 1) continue;
|
|
1085
|
-
const role = line.slice(0, eq).trim();
|
|
1086
|
-
const url = line.slice(eq + 1).trim().replace(/\/$/, "");
|
|
1087
|
-
if (role && isTryCloudflareUrl(url)) tunnels[role] = url;
|
|
1088
|
-
}
|
|
1089
|
-
return Object.keys(tunnels).length ? tunnels : null;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function clearCloudflareTunnelFile(folder) {
|
|
1176
|
+
function clearLegacyTunnelFile(folder) {
|
|
1093
1177
|
if (!folder) return;
|
|
1094
|
-
const file = path.join(path.resolve(folder),
|
|
1178
|
+
const file = path.join(path.resolve(folder), LEGACY_TUNNEL_FILE);
|
|
1095
1179
|
try {
|
|
1096
1180
|
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
1097
1181
|
} catch {
|
|
@@ -1099,8 +1183,7 @@ function clearCloudflareTunnelFile(folder) {
|
|
|
1099
1183
|
}
|
|
1100
1184
|
}
|
|
1101
1185
|
|
|
1102
|
-
|
|
1103
|
-
function archiveStaleCloudflareLogs(folder, sandboxId) {
|
|
1186
|
+
function archiveLegacyTunnelLogs(folder, sandboxId) {
|
|
1104
1187
|
if (!folder) return;
|
|
1105
1188
|
const logDir = dataDirFor(folder, sandboxId);
|
|
1106
1189
|
if (!fs.existsSync(logDir)) return;
|
|
@@ -1123,185 +1206,25 @@ function archiveStaleCloudflareLogs(folder, sandboxId) {
|
|
|
1123
1206
|
}
|
|
1124
1207
|
}
|
|
1125
1208
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
"PUBLIC_URL",
|
|
1129
|
-
"CORS_ORIGIN",
|
|
1130
|
-
"CORS_ORIGINS",
|
|
1131
|
-
"NEXT_PUBLIC_APP_URL",
|
|
1132
|
-
"VITE_APP_URL",
|
|
1133
|
-
"REACT_APP_APP_URL",
|
|
1134
|
-
"AI_SERVER_URL",
|
|
1135
|
-
"NEXT_PUBLIC_AI_SERVER_URL",
|
|
1136
|
-
"VITE_AI_SERVER_URL",
|
|
1137
|
-
"REACT_APP_AI_SERVER_URL",
|
|
1138
|
-
"API_URL",
|
|
1139
|
-
"API_BASE_URL",
|
|
1140
|
-
"VITE_API_URL",
|
|
1141
|
-
"VITE_API_BASE_URL",
|
|
1142
|
-
"NEXT_PUBLIC_API_URL",
|
|
1143
|
-
"NEXT_PUBLIC_API_BASE_URL",
|
|
1144
|
-
"BACKEND_URL",
|
|
1145
|
-
];
|
|
1146
|
-
|
|
1147
|
-
/**
|
|
1148
|
-
* Remove dead trycloudflare URLs from workspace state, tunnel file, env, and
|
|
1149
|
-
* archived logs so heartbeats stop re-probing them.
|
|
1150
|
-
*/
|
|
1151
|
-
function purgeUnreachableCloudflare(ws, cfg, opts = {}) {
|
|
1209
|
+
/** Drop leftover tunnel state so heartbeats never revive old public URLs. */
|
|
1210
|
+
function stripDiscardedTunnelState(ws, cfg) {
|
|
1152
1211
|
const folder = ws.folderPath ? path.resolve(ws.folderPath) : null;
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1212
|
+
const leftoverKeys = leftoverStateKeys(ws);
|
|
1213
|
+
const had = leftoverKeys.length > 0 || isDiscardedTunnelUrl(ws.appUrl);
|
|
1214
|
+
for (const key of leftoverKeys) delete ws[key];
|
|
1215
|
+
if (isDiscardedTunnelUrl(ws.appUrl)) ws.appUrl = null;
|
|
1216
|
+
|
|
1217
|
+
if (folder && fs.existsSync(folder)) {
|
|
1218
|
+
archiveLegacyTunnelLogs(folder, ws.sandboxId);
|
|
1219
|
+
clearLegacyTunnelFile(folder);
|
|
1220
|
+
const current = readProjectEnvValues(folder);
|
|
1221
|
+
const remove = Object.keys(current).filter((key) =>
|
|
1222
|
+
isDiscardedTunnelUrl(current[key])
|
|
1162
1223
|
);
|
|
1163
|
-
|
|
1164
|
-
log(`clearing stale Cloudflare for ${label}`);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
ws.cloudflareUrl = null;
|
|
1168
|
-
ws.cloudflare = keep && Object.keys(keep).length ? { ...keep } : null;
|
|
1169
|
-
if (keep?.ui || keep?.ai) {
|
|
1170
|
-
ws.cloudflareUrl = keep.ui || keep.ai;
|
|
1171
|
-
ws.appUrl = keep.ui || keep.ai;
|
|
1172
|
-
} else if (ws.appUrl && !isLocalAppUrl(ws.appUrl)) {
|
|
1173
|
-
ws.appUrl = null;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
if (ws.sandboxId && !(keep && Object.keys(keep).length)) {
|
|
1177
|
-
cloudflareTunnels.delete(ws.sandboxId);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
if (!folder || !fs.existsSync(folder)) {
|
|
1181
|
-
persistWorkspaceEntry(cfg, ws);
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// Always archive cf-*.log so dead trycloudflare hosts are not rediscovered.
|
|
1186
|
-
archiveStaleCloudflareLogs(folder, ws.sandboxId);
|
|
1187
|
-
if (keep && Object.keys(keep).length) {
|
|
1188
|
-
writeTunnelEnv(ws, keep);
|
|
1189
|
-
} else {
|
|
1190
|
-
clearCloudflareTunnelFile(folder);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
const current = readProjectEnvValues(folder);
|
|
1194
|
-
/** @type {string[]} */
|
|
1195
|
-
const remove = [];
|
|
1196
|
-
for (const key of CLOUDFLARE_ENV_KEYS) {
|
|
1197
|
-
const value = current[key];
|
|
1198
|
-
if (!isTryCloudflareUrl(value)) continue;
|
|
1199
|
-
const normalized = String(value).replace(/\/$/, "");
|
|
1200
|
-
if (keep && Object.values(keep).some((u) => String(u).replace(/\/$/, "") === normalized)) {
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
remove.push(key);
|
|
1204
|
-
}
|
|
1205
|
-
if (remove.length) {
|
|
1206
|
-
writeProjectEnv(folder, {}, { remove });
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
persistWorkspaceEntry(cfg, ws);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
function parseAllTryCloudflareUrls(text) {
|
|
1213
|
-
const matches = [
|
|
1214
|
-
...String(text || "").matchAll(
|
|
1215
|
-
/https:\/\/[a-z0-9-]+\.trycloudflare\.com/gi
|
|
1216
|
-
),
|
|
1217
|
-
];
|
|
1218
|
-
return matches.map((m) => m[0].replace(/\/$/, ""));
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
function lastTryCloudflareUrl(text) {
|
|
1222
|
-
const all = parseAllTryCloudflareUrls(text);
|
|
1223
|
-
return all.length ? all[all.length - 1] : null;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
/**
|
|
1227
|
-
* Discover Cloudflare tunnel URLs from bridge state, tunnel file, project env,
|
|
1228
|
-
* and cloudflared log files — even if this bridge process did not start them.
|
|
1229
|
-
* @returns {Record<string, string> | null}
|
|
1230
|
-
*/
|
|
1231
|
-
function discoverCloudflareTunnels(ws) {
|
|
1232
|
-
if (!ws?.folderPath) return null;
|
|
1233
|
-
const folder = path.resolve(ws.folderPath);
|
|
1234
|
-
/** @type {Record<string, string>} */
|
|
1235
|
-
const tunnels = {};
|
|
1236
|
-
const setRole = (role, value) => {
|
|
1237
|
-
if (!role || tunnels[role] || !isTryCloudflareUrl(value)) return;
|
|
1238
|
-
tunnels[role] = String(value).replace(/\/$/, "");
|
|
1239
|
-
};
|
|
1240
|
-
|
|
1241
|
-
if (ws.cloudflare && typeof ws.cloudflare === "object") {
|
|
1242
|
-
for (const [role, value] of Object.entries(ws.cloudflare)) {
|
|
1243
|
-
setRole(role, value);
|
|
1244
|
-
}
|
|
1224
|
+
if (remove.length) writeProjectEnv(folder, {}, { remove });
|
|
1245
1225
|
}
|
|
1246
|
-
if (ws.cloudflareUrl) setRole("ui", ws.cloudflareUrl);
|
|
1247
|
-
|
|
1248
|
-
const fromFile = readCloudflareTunnelFile(folder);
|
|
1249
|
-
if (fromFile) {
|
|
1250
|
-
for (const [role, value] of Object.entries(fromFile)) setRole(role, value);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
const env = readProjectEnvValues(folder);
|
|
1254
|
-
const envRoles = [
|
|
1255
|
-
["ai", env.NEXT_PUBLIC_AI_SERVER_URL],
|
|
1256
|
-
["ai", env.VITE_AI_SERVER_URL],
|
|
1257
|
-
["ai", env.AI_SERVER_URL],
|
|
1258
|
-
["ai", env.REACT_APP_AI_SERVER_URL],
|
|
1259
|
-
["ui", env.NEXT_PUBLIC_APP_URL],
|
|
1260
|
-
["ui", env.VITE_APP_URL],
|
|
1261
|
-
["ui", env.APP_URL],
|
|
1262
|
-
["ui", env.PUBLIC_URL],
|
|
1263
|
-
["backend", env.NEXT_PUBLIC_API_URL],
|
|
1264
|
-
["backend", env.VITE_API_URL],
|
|
1265
|
-
["backend", env.API_URL],
|
|
1266
|
-
];
|
|
1267
|
-
for (const [role, value] of envRoles) setRole(role, value);
|
|
1268
1226
|
|
|
1269
|
-
|
|
1270
|
-
if (fs.existsSync(logDir)) {
|
|
1271
|
-
const sandboxPrefix = `cf-${String(ws.sandboxId || "").slice(0, 8)}-`;
|
|
1272
|
-
let names = [];
|
|
1273
|
-
try {
|
|
1274
|
-
names = fs.readdirSync(logDir);
|
|
1275
|
-
} catch {
|
|
1276
|
-
names = [];
|
|
1277
|
-
}
|
|
1278
|
-
// Prefer sandbox-scoped logs, then any cf-*-role.log in the project.
|
|
1279
|
-
const ranked = names
|
|
1280
|
-
.filter((name) => /^cf-.*\.log$/i.test(name) && !name.endsWith(".stale"))
|
|
1281
|
-
.sort((a, b) => {
|
|
1282
|
-
const aScore = a.startsWith(sandboxPrefix) ? 0 : 1;
|
|
1283
|
-
const bScore = b.startsWith(sandboxPrefix) ? 0 : 1;
|
|
1284
|
-
return aScore - bScore || a.localeCompare(b);
|
|
1285
|
-
});
|
|
1286
|
-
for (const name of ranked) {
|
|
1287
|
-
const roleMatch = name.match(
|
|
1288
|
-
/cf-(?:[a-f0-9]{6,}-)?(ai|ui|backend|app)\.log$/i
|
|
1289
|
-
);
|
|
1290
|
-
if (!roleMatch) continue;
|
|
1291
|
-
let role = roleMatch[1].toLowerCase();
|
|
1292
|
-
if (role === "app") role = "ui";
|
|
1293
|
-
if (tunnels[role]) continue;
|
|
1294
|
-
try {
|
|
1295
|
-
const text = fs.readFileSync(path.join(logDir, name), "utf8");
|
|
1296
|
-
const url = lastTryCloudflareUrl(text);
|
|
1297
|
-
if (url) setRole(role, url);
|
|
1298
|
-
} catch {
|
|
1299
|
-
/* ignore unreadable logs */
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
return Object.keys(tunnels).length ? tunnels : null;
|
|
1227
|
+
if (had) persistWorkspaceEntry(cfg, ws);
|
|
1305
1228
|
}
|
|
1306
1229
|
|
|
1307
1230
|
function workspaceHostReport(ws) {
|
|
@@ -1326,7 +1249,7 @@ function workspaceHostReport(ws) {
|
|
|
1326
1249
|
env.NEXT_PUBLIC_AI_SERVER_URL,
|
|
1327
1250
|
ws.appUrl,
|
|
1328
1251
|
]) {
|
|
1329
|
-
if (!value ||
|
|
1252
|
+
if (!value || isDiscardedTunnelUrl(value)) continue;
|
|
1330
1253
|
add(value);
|
|
1331
1254
|
}
|
|
1332
1255
|
if (chatPort) {
|
|
@@ -1360,7 +1283,7 @@ function workspaceHostReport(ws) {
|
|
|
1360
1283
|
/**
|
|
1361
1284
|
* Status-only reconcile for a workspace:
|
|
1362
1285
|
* 1) probe chat / ui / backend
|
|
1363
|
-
* 2) drop leftover
|
|
1286
|
+
* 2) drop leftover tunnel URLs
|
|
1364
1287
|
* 3) compute host appUrl + CORS origins for Maintainer Pro
|
|
1365
1288
|
*
|
|
1366
1289
|
* Does not start apps. Does not write the app's .env files.
|
|
@@ -1375,19 +1298,8 @@ async function reconcileWorkspacePresence(ws, cfg, opts = {}) {
|
|
|
1375
1298
|
ws.port = probe.chatPort;
|
|
1376
1299
|
}
|
|
1377
1300
|
|
|
1378
|
-
// 2. Remove leftover
|
|
1379
|
-
|
|
1380
|
-
if (leftover || ws.cloudflareUrl || ws.cloudflare) {
|
|
1381
|
-
purgeUnreachableCloudflare(ws, cfg, {
|
|
1382
|
-
deadUrls: leftover ? Object.values(leftover) : [],
|
|
1383
|
-
keep: null,
|
|
1384
|
-
});
|
|
1385
|
-
try {
|
|
1386
|
-
await stopCloudflare(ws.sandboxId, ws.folderPath);
|
|
1387
|
-
} catch {
|
|
1388
|
-
/* leftover cloudflared */
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1301
|
+
// 2. Remove leftover tunnel URLs / env — share URLs replace them.
|
|
1302
|
+
stripDiscardedTunnelState(ws, cfg);
|
|
1391
1303
|
|
|
1392
1304
|
// 3. Host + CORS origins for Maintainer Pro
|
|
1393
1305
|
const host = workspaceHostReport(ws);
|
|
@@ -1445,8 +1357,6 @@ async function reconcileWorkspacePresence(ws, cfg, opts = {}) {
|
|
|
1445
1357
|
|
|
1446
1358
|
return {
|
|
1447
1359
|
probe,
|
|
1448
|
-
usingCloudflare: false,
|
|
1449
|
-
cloudflare: null,
|
|
1450
1360
|
host,
|
|
1451
1361
|
appsRunning: probe.running,
|
|
1452
1362
|
aiServerUp: probe.chatUp,
|
|
@@ -1467,18 +1377,28 @@ async function probeHttpPaths(port, paths, timeoutMs = 2500) {
|
|
|
1467
1377
|
function probePortOpen(port, timeoutMs = 800) {
|
|
1468
1378
|
const n = Number(port);
|
|
1469
1379
|
if (!n) return Promise.resolve(false);
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1380
|
+
const tryHost = (host) =>
|
|
1381
|
+
new Promise((resolve) => {
|
|
1382
|
+
const socket = net.connect({ host, port: n });
|
|
1383
|
+
const done = (ok) => {
|
|
1384
|
+
socket.removeAllListeners();
|
|
1385
|
+
socket.destroy();
|
|
1386
|
+
resolve(ok);
|
|
1387
|
+
};
|
|
1388
|
+
socket.setTimeout(timeoutMs);
|
|
1389
|
+
socket.once("connect", () => done(true));
|
|
1390
|
+
socket.once("timeout", () => done(false));
|
|
1391
|
+
socket.once("error", () => done(false));
|
|
1392
|
+
});
|
|
1393
|
+
return (async () => {
|
|
1394
|
+
for (const host of loopbackHostsForPort(n)) {
|
|
1395
|
+
if (await tryHost(host)) {
|
|
1396
|
+
rememberLoopbackHost(n, host);
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return false;
|
|
1401
|
+
})();
|
|
1482
1402
|
}
|
|
1483
1403
|
|
|
1484
1404
|
async function portIsLive(port, timeoutMs = 1200) {
|
|
@@ -1567,28 +1487,71 @@ function proxySlugForApp(app) {
|
|
|
1567
1487
|
return id || "app";
|
|
1568
1488
|
}
|
|
1569
1489
|
|
|
1490
|
+
function uniqueProxySlugs(apps) {
|
|
1491
|
+
const used = new Set();
|
|
1492
|
+
/** @type {Record<string, string>} */
|
|
1493
|
+
const slugs = {};
|
|
1494
|
+
for (const app of apps || []) {
|
|
1495
|
+
let slug = proxySlugForApp(app);
|
|
1496
|
+
if (!slug || !app?.id) continue;
|
|
1497
|
+
if (used.has(slug)) {
|
|
1498
|
+
const fromId = String(app.id)
|
|
1499
|
+
.trim()
|
|
1500
|
+
.toLowerCase()
|
|
1501
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
1502
|
+
.replace(/^-+|-+$/g, "");
|
|
1503
|
+
slug =
|
|
1504
|
+
fromId && !used.has(fromId)
|
|
1505
|
+
? fromId
|
|
1506
|
+
: `${slug}-${Number(app.port) || used.size}`;
|
|
1507
|
+
}
|
|
1508
|
+
used.add(slug);
|
|
1509
|
+
slugs[app.id] = slug;
|
|
1510
|
+
}
|
|
1511
|
+
return slugs;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function sharePortUrls(ws) {
|
|
1515
|
+
const apps = Array.isArray(ws?.hostApps) ? ws.hostApps : [];
|
|
1516
|
+
/** @type {Record<string, string>} */
|
|
1517
|
+
const out = {};
|
|
1518
|
+
for (const app of apps) {
|
|
1519
|
+
const port = Number(app.port);
|
|
1520
|
+
if (!port) continue;
|
|
1521
|
+
const url = proxyUrlForApp(ws, app);
|
|
1522
|
+
if (url) out[String(port)] = url.replace(/\/$/, "");
|
|
1523
|
+
}
|
|
1524
|
+
return out;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1570
1527
|
function applyAssignedProxy(ws, remote, cfg) {
|
|
1571
1528
|
const proxy =
|
|
1572
1529
|
remote?.proxy && typeof remote.proxy === "object" ? remote.proxy : null;
|
|
1573
1530
|
if (!proxy?.token) return;
|
|
1574
1531
|
const origin = String(proxy.origin || cfg?.adminUrl || "").replace(/\/$/, "");
|
|
1575
|
-
const
|
|
1532
|
+
const assigned =
|
|
1576
1533
|
proxy.slugs && typeof proxy.slugs === "object" ? proxy.slugs : {};
|
|
1534
|
+
const unique = uniqueProxySlugs(appsFrom(ws, remote));
|
|
1535
|
+
/** @type {Record<string, string>} */
|
|
1536
|
+
const slugs = {};
|
|
1577
1537
|
/** @type {Record<string, string>} */
|
|
1578
1538
|
const urls = {};
|
|
1579
|
-
const apps =
|
|
1580
|
-
? ws.hostApps
|
|
1581
|
-
: Array.isArray(remote.hostApps)
|
|
1582
|
-
? remote.hostApps
|
|
1583
|
-
: [];
|
|
1539
|
+
const apps = appsFrom(ws, remote);
|
|
1584
1540
|
for (const app of apps) {
|
|
1585
|
-
const slug =
|
|
1541
|
+
const slug = assigned[app.id] || unique[app.id] || proxySlugForApp(app);
|
|
1586
1542
|
if (!slug || !origin) continue;
|
|
1543
|
+
slugs[app.id] = slug;
|
|
1587
1544
|
urls[app.id] = `${origin}/p/${proxy.token}/${slug}`;
|
|
1588
1545
|
}
|
|
1589
1546
|
ws.proxy = { origin, token: String(proxy.token), slugs, urls };
|
|
1590
1547
|
}
|
|
1591
1548
|
|
|
1549
|
+
function appsFrom(ws, remote) {
|
|
1550
|
+
if (Array.isArray(ws?.hostApps) && ws.hostApps.length) return ws.hostApps;
|
|
1551
|
+
if (Array.isArray(remote?.hostApps)) return remote.hostApps;
|
|
1552
|
+
return [];
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1592
1555
|
function proxyUrlForApp(ws, app) {
|
|
1593
1556
|
if (!usesBridgeProxy(app)) return "";
|
|
1594
1557
|
const urls =
|
|
@@ -1605,24 +1568,17 @@ function proxyUrlForApp(ws, app) {
|
|
|
1605
1568
|
return `${origin}/p/${token}/${slug}`;
|
|
1606
1569
|
}
|
|
1607
1570
|
|
|
1608
|
-
function
|
|
1609
|
-
if (!app) return "app";
|
|
1610
|
-
if (app.role === "ai-server") return "ai";
|
|
1611
|
-
if (app.host === true || app.role === "ui" || app.role === "app") return "ui";
|
|
1612
|
-
if (isBackendApp(app)) return "backend";
|
|
1613
|
-
if (app.role === "custom") return "app";
|
|
1614
|
-
return app.role;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
function jobsFromHostApps(ws) {
|
|
1571
|
+
function jobsFromHostApps(ws, filter = {}) {
|
|
1618
1572
|
const apps = Array.isArray(ws.hostApps) ? ws.hostApps : [];
|
|
1619
1573
|
const hostId = hostAppOf(ws)?.id;
|
|
1620
1574
|
return apps
|
|
1621
1575
|
.filter((app) => app && app.role !== "ai-server")
|
|
1576
|
+
.filter((app) => (filter.appId ? app.id === filter.appId : true))
|
|
1622
1577
|
.map((app) => {
|
|
1623
1578
|
const port = Number(app.port) || 3000;
|
|
1624
1579
|
const command = String(app.startCommand || "").trim();
|
|
1625
|
-
const script = command.replace(/^npm\s+run\s+/, "") || "dev";
|
|
1580
|
+
const script = command.replace(/^npm\s+run\s+/, "") || command || "dev";
|
|
1581
|
+
const folder = folderForApp(ws, app);
|
|
1626
1582
|
return {
|
|
1627
1583
|
role: isBackendApp(app)
|
|
1628
1584
|
? "backend"
|
|
@@ -1636,6 +1592,7 @@ function jobsFromHostApps(ws) {
|
|
|
1636
1592
|
port,
|
|
1637
1593
|
probeUrl: `http://127.0.0.1:${port}`,
|
|
1638
1594
|
appId: app.id,
|
|
1595
|
+
folder,
|
|
1639
1596
|
};
|
|
1640
1597
|
});
|
|
1641
1598
|
}
|
|
@@ -1678,7 +1635,6 @@ async function probeRunningApps(ws, timeoutMs = 2500) {
|
|
|
1678
1635
|
...app,
|
|
1679
1636
|
port: port || app.port,
|
|
1680
1637
|
running: Boolean(up),
|
|
1681
|
-
cloudflareUrl: null,
|
|
1682
1638
|
publicUrl: proxyUrlForApp(ws, app) || null,
|
|
1683
1639
|
lastCheckedAt: new Date().toISOString(),
|
|
1684
1640
|
};
|
|
@@ -1722,66 +1678,20 @@ async function restoreHostsAfterReconnect(cfg) {
|
|
|
1722
1678
|
}
|
|
1723
1679
|
}
|
|
1724
1680
|
|
|
1725
|
-
function
|
|
1681
|
+
function chatLaunchEnv(ws, cfg) {
|
|
1726
1682
|
const aiPort = Number(ws.port) || 3100;
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
)
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
const env = {
|
|
1683
|
+
const admin = originFromUrl(cfg?.adminUrl);
|
|
1684
|
+
const origins = [];
|
|
1685
|
+
if (admin) origins.push(admin);
|
|
1686
|
+
origins.push(`http://localhost:${aiPort}`);
|
|
1687
|
+
origins.push(`http://127.0.0.1:${aiPort}`);
|
|
1688
|
+
return {
|
|
1734
1689
|
AI_SERVER_PORT: String(aiPort),
|
|
1735
|
-
AI_SERVER_URL: ai,
|
|
1736
|
-
NEXT_PUBLIC_AI_SERVER_URL: ai,
|
|
1737
|
-
VITE_AI_SERVER_URL: ai,
|
|
1738
|
-
REACT_APP_AI_SERVER_URL: ai,
|
|
1739
1690
|
MAINTAINER_PRO_DATA_DIR: dataDirFor(ws.folderPath, ws.sandboxId),
|
|
1740
1691
|
MAINTAINER_PRO_SANDBOX_ID: String(ws.sandboxId || ""),
|
|
1692
|
+
CORS_ORIGIN: origins.join(","),
|
|
1693
|
+
CORS_ORIGINS: origins.join(","),
|
|
1741
1694
|
};
|
|
1742
|
-
const ui =
|
|
1743
|
-
jobs.find((job) => job.host) ||
|
|
1744
|
-
jobs.find((job) => job.role === "ui" || job.role === "app");
|
|
1745
|
-
const uiApp =
|
|
1746
|
-
hostAppOf(ws) ||
|
|
1747
|
-
(Array.isArray(ws.hostApps) ? ws.hostApps : []).find(
|
|
1748
|
-
(app) => app && (app.host || app.role === "ui" || app.role === "app")
|
|
1749
|
-
) ||
|
|
1750
|
-
{ id: "ui", role: "ui", host: true };
|
|
1751
|
-
const publicUi = proxyUrlForApp(ws, uiApp) || "";
|
|
1752
|
-
if (publicUi) {
|
|
1753
|
-
env.PORT = ui?.port ? String(ui.port) : env.PORT;
|
|
1754
|
-
env.APP_URL = publicUi;
|
|
1755
|
-
env.CORS_ORIGIN = publicUi;
|
|
1756
|
-
env.PUBLIC_URL = publicUi;
|
|
1757
|
-
env.NEXT_PUBLIC_APP_URL = publicUi;
|
|
1758
|
-
env.VITE_APP_URL = publicUi;
|
|
1759
|
-
} else if (ui?.port) {
|
|
1760
|
-
const app = `http://localhost:${ui.port}`;
|
|
1761
|
-
env.PORT = String(ui.port);
|
|
1762
|
-
env.APP_URL = app;
|
|
1763
|
-
env.CORS_ORIGIN = app;
|
|
1764
|
-
env.NEXT_PUBLIC_APP_URL = app;
|
|
1765
|
-
env.VITE_APP_URL = app;
|
|
1766
|
-
}
|
|
1767
|
-
const backend = jobs.find((job) => job.role === "backend");
|
|
1768
|
-
if (backend?.port) {
|
|
1769
|
-
const backendApp = (Array.isArray(ws.hostApps) ? ws.hostApps : []).find(
|
|
1770
|
-
(app) => isBackendApp(app)
|
|
1771
|
-
) || { id: "backend", role: "backend", port: backend.port };
|
|
1772
|
-
const publicBackend = proxyUrlForApp(ws, backendApp) || "";
|
|
1773
|
-
const api = publicBackend || `http://localhost:${backend.port}`;
|
|
1774
|
-
env.API_URL = api;
|
|
1775
|
-
env.API_BASE_URL = api;
|
|
1776
|
-
env.API_PORT = String(backend.port);
|
|
1777
|
-
env.BACKEND_URL = api;
|
|
1778
|
-
env.VITE_API_URL = api;
|
|
1779
|
-
env.VITE_API_BASE_URL = api;
|
|
1780
|
-
env.NEXT_PUBLIC_API_URL = api;
|
|
1781
|
-
env.NEXT_PUBLIC_API_BASE_URL = api;
|
|
1782
|
-
env.REACT_APP_API_URL = api;
|
|
1783
|
-
}
|
|
1784
|
-
return env;
|
|
1785
1695
|
}
|
|
1786
1696
|
|
|
1787
1697
|
async function prepareWorkspaceLaunch(ws, cfg, reserved) {
|
|
@@ -1790,7 +1700,7 @@ async function prepareWorkspaceLaunch(ws, cfg, reserved) {
|
|
|
1790
1700
|
if (!folder || !fs.existsSync(folder)) {
|
|
1791
1701
|
const aiPort = Number(ws.port) || 3100;
|
|
1792
1702
|
log(`ports skip ${label}: folder missing (${folder || "none"})`);
|
|
1793
|
-
return { aiPort, jobs: []
|
|
1703
|
+
return { aiPort, jobs: [] };
|
|
1794
1704
|
}
|
|
1795
1705
|
log(`ports pick ${label} in ${folder}`);
|
|
1796
1706
|
const listedAi = Array.isArray(ws.hostApps)
|
|
@@ -1826,11 +1736,8 @@ async function prepareWorkspaceLaunch(ws, cfg, reserved) {
|
|
|
1826
1736
|
const planned = [];
|
|
1827
1737
|
for (const job of jobs) {
|
|
1828
1738
|
const preferred = Number(job.preferredPort) || 3000;
|
|
1829
|
-
const probe =
|
|
1830
|
-
|
|
1831
|
-
"127.0.0.1"
|
|
1832
|
-
);
|
|
1833
|
-
const up = await probeUrl(probe);
|
|
1739
|
+
const probe = job.probeUrl || `http://127.0.0.1:${preferred}`;
|
|
1740
|
+
const up = await probeLoopbackUrl(probe);
|
|
1834
1741
|
const port = up
|
|
1835
1742
|
? (reserved.add(portFromText(probe, preferred)), portFromText(probe, preferred))
|
|
1836
1743
|
: await findFreePort(preferred, reserved);
|
|
@@ -1850,10 +1757,6 @@ async function prepareWorkspaceLaunch(ws, cfg, reserved) {
|
|
|
1850
1757
|
}
|
|
1851
1758
|
}
|
|
1852
1759
|
|
|
1853
|
-
const env = envForWorkspacePorts(ws, planned);
|
|
1854
|
-
log(
|
|
1855
|
-
`env inject ${label} (process only): ${Object.keys(env).join(" ") || "(none)"}`
|
|
1856
|
-
);
|
|
1857
1760
|
if (planned.length) {
|
|
1858
1761
|
ws.projectInfo = {
|
|
1859
1762
|
...(ws.projectInfo || {}),
|
|
@@ -1869,7 +1772,7 @@ async function prepareWorkspaceLaunch(ws, cfg, reserved) {
|
|
|
1869
1772
|
.map((job) => ` ${job.role}=${job.port}${job.up ? "(up)" : ""}`)
|
|
1870
1773
|
.join("")}`
|
|
1871
1774
|
);
|
|
1872
|
-
return { aiPort, jobs: planned
|
|
1775
|
+
return { aiPort, jobs: planned };
|
|
1873
1776
|
}
|
|
1874
1777
|
|
|
1875
1778
|
const DEFAULT_AI_IGNORE_PATHS = [
|
|
@@ -1934,7 +1837,7 @@ const IGNORE_NAMES = new Set([
|
|
|
1934
1837
|
".maintainer-pro",
|
|
1935
1838
|
".collaborater",
|
|
1936
1839
|
".maintainer-pro-bridge.json",
|
|
1937
|
-
|
|
1840
|
+
LEGACY_TUNNEL_FILE,
|
|
1938
1841
|
]);
|
|
1939
1842
|
|
|
1940
1843
|
function isIgnorableEntry(name) {
|
|
@@ -2204,8 +2107,6 @@ function listLaunchRoots(allowed) {
|
|
|
2204
2107
|
|
|
2205
2108
|
/** Prevents opening a new window on every heartbeat while a process is starting. */
|
|
2206
2109
|
const launchedAt = new Map();
|
|
2207
|
-
/** @type {Map<string, { tunnels: Array<{ role: string, localUrl: string, publicUrl: string | null, logFile: string }> }>} */
|
|
2208
|
-
const cloudflareTunnels = new Map();
|
|
2209
2110
|
/** In-process chat servers, one per sandbox/project. */
|
|
2210
2111
|
/** @type {Map<string, { port: number, workspaceDir: string, close: () => Promise<void>, runChat: (body: Record<string, unknown>) => Promise<{ ok: boolean, status: number, data: Record<string, unknown> }>, handleHttp?: Function }>} */
|
|
2211
2112
|
const embeddedChat = new Map();
|
|
@@ -2213,7 +2114,7 @@ const embeddedChat = new Map();
|
|
|
2213
2114
|
const embeddedChatStarting = new Map();
|
|
2214
2115
|
|
|
2215
2116
|
const PROXY_CHUNK_BYTES = 256 * 1024;
|
|
2216
|
-
/** @type {Map<string, import("node:http").ClientRequest>} */
|
|
2117
|
+
/** @type {Map<string, { req: import("node:http").ClientRequest, rewrite?: boolean, chunks?: Buffer[], headers?: Record<string, string>, portUrls?: Record<string, string> } | import("node:http").ClientRequest>} */
|
|
2217
2118
|
const proxyHttpReqs = new Map();
|
|
2218
2119
|
/** Reuse sockets to the local app — Next/Vite fetch many files per page. */
|
|
2219
2120
|
const proxyKeepAliveAgent = new http.Agent({
|
|
@@ -2224,6 +2125,8 @@ const proxyKeepAliveAgent = new http.Agent({
|
|
|
2224
2125
|
});
|
|
2225
2126
|
/** @type {Map<string, WebSocket>} */
|
|
2226
2127
|
const proxyLocalSockets = new Map();
|
|
2128
|
+
/** @type {Map<string, Record<string, string>>} */
|
|
2129
|
+
const proxyWsPortUrls = new Map();
|
|
2227
2130
|
/** @type {Record<string, unknown> | null} */
|
|
2228
2131
|
let bridgeCfg = null;
|
|
2229
2132
|
|
|
@@ -2337,6 +2240,9 @@ function shareProxyContext(msg, ws, appId) {
|
|
|
2337
2240
|
).replace(/\/$/, "");
|
|
2338
2241
|
const acceptEncoding =
|
|
2339
2242
|
typeof msg.acceptEncoding === "string" ? msg.acceptEncoding : "";
|
|
2243
|
+
const fromWs = sharePortUrls(ws);
|
|
2244
|
+
const fromMsg =
|
|
2245
|
+
msg?.portUrls && typeof msg.portUrls === "object" ? msg.portUrls : {};
|
|
2340
2246
|
return {
|
|
2341
2247
|
path: safeProxyPath(msg.path),
|
|
2342
2248
|
slug,
|
|
@@ -2344,6 +2250,7 @@ function shareProxyContext(msg, ws, appId) {
|
|
|
2344
2250
|
aiPublicBase,
|
|
2345
2251
|
acceptEncoding,
|
|
2346
2252
|
port: localPortForProxy(ws, appId),
|
|
2253
|
+
portUrls: { ...fromWs, ...fromMsg },
|
|
2347
2254
|
};
|
|
2348
2255
|
}
|
|
2349
2256
|
|
|
@@ -2357,6 +2264,43 @@ function sendProcessedProxyHttp(id, ctx, status, headers, body) {
|
|
|
2357
2264
|
replyProxyHttp(id, processed.status, processed.headers, processed.body);
|
|
2358
2265
|
}
|
|
2359
2266
|
|
|
2267
|
+
function publicPathPrefix(publicBase) {
|
|
2268
|
+
try {
|
|
2269
|
+
return new URL(String(publicBase || "")).pathname.replace(/\/$/, "") || "";
|
|
2270
|
+
} catch {
|
|
2271
|
+
return "";
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function replyShareInterceptor(id, ctx, path) {
|
|
2276
|
+
const pathname = String(path || "").split("?")[0] || "";
|
|
2277
|
+
if (pathname !== "/__mp/shim.js" && pathname !== "/__mp/sw.js") return false;
|
|
2278
|
+
const tokenRoot = shareTokenRoot(ctx.publicBase) || "/";
|
|
2279
|
+
/** @type {Record<string, string>} */
|
|
2280
|
+
const headers = {
|
|
2281
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
2282
|
+
"cache-control": "private, no-store",
|
|
2283
|
+
};
|
|
2284
|
+
let body = "";
|
|
2285
|
+
if (pathname === "/__mp/sw.js") {
|
|
2286
|
+
headers["service-worker-allowed"] = tokenRoot;
|
|
2287
|
+
body = shareServiceWorkerScript(ctx.portUrls, tokenRoot);
|
|
2288
|
+
} else {
|
|
2289
|
+
body = shareShimScript(publicPathPrefix(ctx.publicBase), ctx.portUrls);
|
|
2290
|
+
}
|
|
2291
|
+
replyProxyHttp(id, 200, headers, body);
|
|
2292
|
+
return true;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function destroyProxyHttpReq(entry) {
|
|
2296
|
+
const req = entry?.req || entry;
|
|
2297
|
+
try {
|
|
2298
|
+
req?.destroy?.();
|
|
2299
|
+
} catch {
|
|
2300
|
+
/* ignore */
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2360
2304
|
function bridgeEmbedConfigJs(ws) {
|
|
2361
2305
|
const store = ws?.store && typeof ws.store === "object" ? ws.store : {};
|
|
2362
2306
|
const aiApp = { id: "ai-server", role: "ai-server" };
|
|
@@ -2383,19 +2327,24 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2383
2327
|
const reqPath = safeProxyPath(msg.path);
|
|
2384
2328
|
const pathname = reqPath.split("?")[0] || "/";
|
|
2385
2329
|
const ctx = shareProxyContext(msg, ws, "ai-server");
|
|
2330
|
+
if (replyShareInterceptor(id, ctx, reqPath)) return;
|
|
2386
2331
|
const prepared = prepareShareHttpRequest(
|
|
2387
2332
|
proxyReqHeaders(msg.headers),
|
|
2388
2333
|
ctx.publicBase,
|
|
2389
2334
|
ctx.acceptEncoding,
|
|
2390
|
-
reqPath
|
|
2335
|
+
reqPath,
|
|
2336
|
+
ctx.portUrls
|
|
2391
2337
|
);
|
|
2392
2338
|
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2393
2339
|
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
2394
2340
|
const headers = prepared.headers;
|
|
2395
|
-
const body =
|
|
2341
|
+
const body = rewriteShareRequestBody(
|
|
2396
2342
|
typeof msg.body === "string" && msg.body
|
|
2397
2343
|
? Buffer.from(msg.body, "base64")
|
|
2398
|
-
: Buffer.alloc(0)
|
|
2344
|
+
: Buffer.alloc(0),
|
|
2345
|
+
headers,
|
|
2346
|
+
ctx.portUrls
|
|
2347
|
+
);
|
|
2399
2348
|
|
|
2400
2349
|
if (pathname === "/ai-ui.iife.js") {
|
|
2401
2350
|
const file = findIife();
|
|
@@ -2487,11 +2436,7 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2487
2436
|
}
|
|
2488
2437
|
const existing = proxyHttpReqs.get(id);
|
|
2489
2438
|
if (existing) {
|
|
2490
|
-
|
|
2491
|
-
existing.destroy();
|
|
2492
|
-
} catch {
|
|
2493
|
-
/* ignore */
|
|
2494
|
-
}
|
|
2439
|
+
destroyProxyHttpReq(existing);
|
|
2495
2440
|
proxyHttpReqs.delete(id);
|
|
2496
2441
|
}
|
|
2497
2442
|
const port = localPortForProxy(ws, appId);
|
|
@@ -2507,11 +2452,13 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2507
2452
|
const path = safeProxyPath(msg.path);
|
|
2508
2453
|
const ctx = shareProxyContext(msg, ws, appId);
|
|
2509
2454
|
ctx.port = port;
|
|
2455
|
+
if (replyShareInterceptor(id, ctx, path)) return;
|
|
2510
2456
|
const prepared = prepareShareHttpRequest(
|
|
2511
2457
|
proxyReqHeaders(msg.headers),
|
|
2512
2458
|
ctx.publicBase,
|
|
2513
2459
|
ctx.acceptEncoding,
|
|
2514
|
-
path
|
|
2460
|
+
path,
|
|
2461
|
+
ctx.portUrls
|
|
2515
2462
|
);
|
|
2516
2463
|
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2517
2464
|
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
@@ -2528,7 +2475,6 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2528
2475
|
}
|
|
2529
2476
|
}
|
|
2530
2477
|
const headers = prepared.headers;
|
|
2531
|
-
headers.host = `127.0.0.1:${port}`;
|
|
2532
2478
|
// Next.js dev 403s `/_next` when Origin/sec-fetch look cross-site.
|
|
2533
2479
|
// This hop is server-to-server; drop those so chunks always load.
|
|
2534
2480
|
for (const key of Object.keys(headers)) {
|
|
@@ -2545,100 +2491,174 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2545
2491
|
}
|
|
2546
2492
|
}
|
|
2547
2493
|
const stream = randomBytes(8).toString("hex");
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2494
|
+
const initialBody =
|
|
2495
|
+
typeof msg.body === "string" && msg.body
|
|
2496
|
+
? Buffer.from(msg.body, "base64")
|
|
2497
|
+
: null;
|
|
2498
|
+
const endAfter = msg.bodyEof !== false;
|
|
2499
|
+
const hosts = loopbackHostsForPort(port);
|
|
2500
|
+
let hostIndex = 0;
|
|
2501
|
+
|
|
2502
|
+
const connect = () => {
|
|
2503
|
+
const host = hosts[hostIndex];
|
|
2504
|
+
const reqHeaders = { ...headers, host: `${host}:${port}` };
|
|
2505
|
+
let req;
|
|
2506
|
+
try {
|
|
2507
|
+
req = http.request(
|
|
2508
|
+
{
|
|
2509
|
+
hostname: host,
|
|
2510
|
+
port,
|
|
2511
|
+
path,
|
|
2512
|
+
method,
|
|
2513
|
+
headers: reqHeaders,
|
|
2514
|
+
agent: proxyKeepAliveAgent,
|
|
2515
|
+
},
|
|
2516
|
+
(res) => {
|
|
2517
|
+
rememberLoopbackHost(port, host);
|
|
2518
|
+
/** @type {Record<string, string>} */
|
|
2519
|
+
const outHeaders = {};
|
|
2520
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
2521
|
+
if (value == null) continue;
|
|
2522
|
+
outHeaders[key] = Array.isArray(value)
|
|
2523
|
+
? value.join(", ")
|
|
2524
|
+
: String(value);
|
|
2525
|
+
}
|
|
2526
|
+
const status = res.statusCode || 502;
|
|
2527
|
+
applyShareCacheHeaders(outHeaders, path);
|
|
2528
|
+
if (shouldProcessShareResponse(outHeaders)) {
|
|
2529
|
+
/** @type {Buffer[]} */
|
|
2530
|
+
const chunks = [];
|
|
2531
|
+
res.on("data", (chunk) => {
|
|
2532
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2533
|
+
});
|
|
2534
|
+
res.on("end", () => {
|
|
2535
|
+
proxyHttpReqs.delete(id);
|
|
2536
|
+
sendProcessedProxyHttp(
|
|
2537
|
+
id,
|
|
2538
|
+
ctx,
|
|
2539
|
+
status,
|
|
2540
|
+
outHeaders,
|
|
2541
|
+
Buffer.concat(chunks)
|
|
2542
|
+
);
|
|
2543
|
+
});
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
bridgeSend({
|
|
2547
|
+
type: "proxy.http.start",
|
|
2548
|
+
id,
|
|
2549
|
+
stream,
|
|
2550
|
+
status,
|
|
2551
|
+
headers: outHeaders,
|
|
2552
|
+
});
|
|
2571
2553
|
res.on("data", (chunk) => {
|
|
2572
|
-
|
|
2554
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2555
|
+
for (
|
|
2556
|
+
let offset = 0;
|
|
2557
|
+
offset < buf.length;
|
|
2558
|
+
offset += PROXY_CHUNK_BYTES
|
|
2559
|
+
) {
|
|
2560
|
+
const end = Math.min(offset + PROXY_CHUNK_BYTES, buf.length);
|
|
2561
|
+
bridgeSend({
|
|
2562
|
+
type: "proxy.http.chunk",
|
|
2563
|
+
id,
|
|
2564
|
+
stream,
|
|
2565
|
+
data: Buffer.from(buf.subarray(offset, end)).toString("base64"),
|
|
2566
|
+
eof: false,
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2573
2569
|
});
|
|
2574
2570
|
res.on("end", () => {
|
|
2575
2571
|
proxyHttpReqs.delete(id);
|
|
2576
|
-
sendProcessedProxyHttp(
|
|
2577
|
-
id,
|
|
2578
|
-
ctx,
|
|
2579
|
-
status,
|
|
2580
|
-
outHeaders,
|
|
2581
|
-
Buffer.concat(chunks)
|
|
2582
|
-
);
|
|
2583
|
-
});
|
|
2584
|
-
return;
|
|
2585
|
-
}
|
|
2586
|
-
bridgeSend({
|
|
2587
|
-
type: "proxy.http.start",
|
|
2588
|
-
id,
|
|
2589
|
-
stream,
|
|
2590
|
-
status,
|
|
2591
|
-
headers: outHeaders,
|
|
2592
|
-
});
|
|
2593
|
-
res.on("data", (chunk) => {
|
|
2594
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2595
|
-
for (let offset = 0; offset < buf.length; offset += PROXY_CHUNK_BYTES) {
|
|
2596
|
-
const end = Math.min(offset + PROXY_CHUNK_BYTES, buf.length);
|
|
2597
2572
|
bridgeSend({
|
|
2598
2573
|
type: "proxy.http.chunk",
|
|
2599
2574
|
id,
|
|
2600
2575
|
stream,
|
|
2601
|
-
data:
|
|
2602
|
-
eof:
|
|
2576
|
+
data: "",
|
|
2577
|
+
eof: true,
|
|
2603
2578
|
});
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
);
|
|
2582
|
+
} catch (err) {
|
|
2583
|
+
if (hostIndex + 1 < hosts.length) {
|
|
2584
|
+
hostIndex += 1;
|
|
2585
|
+
connect();
|
|
2586
|
+
return;
|
|
2610
2587
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2588
|
+
bridgeSend({
|
|
2589
|
+
type: "proxy.http.error",
|
|
2590
|
+
id,
|
|
2591
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2592
|
+
});
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
req.on("error", (err) => {
|
|
2596
|
+
if (isLoopbackConnError(err) && hostIndex + 1 < hosts.length) {
|
|
2597
|
+
proxyHttpReqs.delete(id);
|
|
2598
|
+
hostIndex += 1;
|
|
2599
|
+
connect();
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
proxyHttpReqs.delete(id);
|
|
2603
|
+
bridgeSend({
|
|
2604
|
+
type: "proxy.http.error",
|
|
2605
|
+
id,
|
|
2606
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2607
|
+
});
|
|
2617
2608
|
});
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2609
|
+
proxyHttpReqs.set(id, {
|
|
2610
|
+
req,
|
|
2611
|
+
rewrite:
|
|
2612
|
+
shouldRewriteBody(headers) &&
|
|
2613
|
+
ctx.portUrls &&
|
|
2614
|
+
Object.keys(ctx.portUrls).length > 0,
|
|
2615
|
+
chunks: [],
|
|
2616
|
+
headers,
|
|
2617
|
+
portUrls: ctx.portUrls,
|
|
2626
2618
|
});
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2619
|
+
const entry = proxyHttpReqs.get(id);
|
|
2620
|
+
if (entry.rewrite) {
|
|
2621
|
+
if (initialBody?.length) entry.chunks.push(initialBody);
|
|
2622
|
+
if (endAfter) {
|
|
2623
|
+
const body = rewriteShareRequestBody(
|
|
2624
|
+
Buffer.concat(entry.chunks),
|
|
2625
|
+
headers,
|
|
2626
|
+
ctx.portUrls
|
|
2627
|
+
);
|
|
2628
|
+
if (body?.length) req.write(body);
|
|
2629
|
+
req.end();
|
|
2630
|
+
}
|
|
2631
|
+
} else {
|
|
2632
|
+
if (initialBody?.length) req.write(initialBody);
|
|
2633
|
+
if (endAfter) req.end();
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
connect();
|
|
2633
2637
|
}
|
|
2634
2638
|
|
|
2635
2639
|
function handleProxyHttpBodyFromAdmin(msg) {
|
|
2636
2640
|
const id = typeof msg.id === "string" ? msg.id : "";
|
|
2637
|
-
const
|
|
2638
|
-
if (!
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
+
const entry = proxyHttpReqs.get(id);
|
|
2642
|
+
if (!entry) return;
|
|
2643
|
+
const req = entry.req || entry;
|
|
2644
|
+
const chunk =
|
|
2645
|
+
typeof msg.data === "string" && msg.data
|
|
2646
|
+
? Buffer.from(msg.data, "base64")
|
|
2647
|
+
: null;
|
|
2648
|
+
if (entry.rewrite) {
|
|
2649
|
+
if (chunk?.length) entry.chunks.push(chunk);
|
|
2650
|
+
if (msg.eof === true) {
|
|
2651
|
+
const body = rewriteShareRequestBody(
|
|
2652
|
+
Buffer.concat(entry.chunks || []),
|
|
2653
|
+
entry.headers,
|
|
2654
|
+
entry.portUrls
|
|
2655
|
+
);
|
|
2656
|
+
if (body?.length) req.write(body);
|
|
2657
|
+
req.end();
|
|
2658
|
+
}
|
|
2659
|
+
return;
|
|
2641
2660
|
}
|
|
2661
|
+
if (chunk?.length) req.write(chunk);
|
|
2642
2662
|
if (msg.eof === true) req.end();
|
|
2643
2663
|
}
|
|
2644
2664
|
|
|
@@ -2659,16 +2679,22 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2659
2679
|
/* WHATWG WebSocket in Node 22 */
|
|
2660
2680
|
}
|
|
2661
2681
|
proxyLocalSockets.set(id, socket);
|
|
2662
|
-
|
|
2682
|
+
const announceOpen = () => {
|
|
2663
2683
|
bridgeSend({ type: "proxy.ws.opened", id });
|
|
2664
|
-
}
|
|
2684
|
+
};
|
|
2685
|
+
if (socket.readyState === 1) announceOpen();
|
|
2686
|
+
else socket.addEventListener("open", announceOpen);
|
|
2665
2687
|
socket.addEventListener("message", (event) => {
|
|
2666
2688
|
try {
|
|
2667
2689
|
if (typeof event.data === "string") {
|
|
2690
|
+
const portUrls = proxyWsPortUrls.get(id);
|
|
2691
|
+
const data = portUrls
|
|
2692
|
+
? rewriteMappedLocalUrls(event.data, portUrls)
|
|
2693
|
+
: event.data;
|
|
2668
2694
|
bridgeSend({
|
|
2669
2695
|
type: "proxy.ws.frame",
|
|
2670
2696
|
id,
|
|
2671
|
-
data
|
|
2697
|
+
data,
|
|
2672
2698
|
binary: false,
|
|
2673
2699
|
});
|
|
2674
2700
|
return;
|
|
@@ -2688,6 +2714,7 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2688
2714
|
});
|
|
2689
2715
|
socket.addEventListener("close", (event) => {
|
|
2690
2716
|
proxyLocalSockets.delete(id);
|
|
2717
|
+
proxyWsPortUrls.delete(id);
|
|
2691
2718
|
bridgeSend({
|
|
2692
2719
|
type: "proxy.ws.close",
|
|
2693
2720
|
id,
|
|
@@ -2713,23 +2740,51 @@ function wsProtocolsFromMsg(msg) {
|
|
|
2713
2740
|
}
|
|
2714
2741
|
|
|
2715
2742
|
function openProxyLocalWs(id, port, path, protocols) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
socket
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2743
|
+
const proto = Array.isArray(protocols)
|
|
2744
|
+
? protocols.map((p) => String(p || "").trim()).filter(Boolean)
|
|
2745
|
+
: [];
|
|
2746
|
+
const hosts = loopbackHostsForPort(port);
|
|
2747
|
+
const tryHost = (index) => {
|
|
2748
|
+
const host = hosts[index];
|
|
2749
|
+
const url = `ws://${host}:${port}${path}`;
|
|
2750
|
+
let socket;
|
|
2751
|
+
try {
|
|
2752
|
+
// Vite HMR only accepts upgrades with subprotocol `vite-hmr` / `vite-ping`.
|
|
2753
|
+
socket = proto.length ? new WebSocket(url, proto) : new WebSocket(url);
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
if (index + 1 < hosts.length) {
|
|
2756
|
+
tryHost(index + 1);
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
bridgeSend({
|
|
2760
|
+
type: "proxy.ws.error",
|
|
2761
|
+
id,
|
|
2762
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2763
|
+
});
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
let settled = false;
|
|
2767
|
+
socket.addEventListener("open", () => {
|
|
2768
|
+
if (settled) return;
|
|
2769
|
+
settled = true;
|
|
2770
|
+
rememberLoopbackHost(port, host);
|
|
2771
|
+
attachProxyLocalWs(id, socket);
|
|
2729
2772
|
});
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2773
|
+
socket.addEventListener("close", () => {
|
|
2774
|
+
if (settled) return;
|
|
2775
|
+
settled = true;
|
|
2776
|
+
if (index + 1 < hosts.length) {
|
|
2777
|
+
tryHost(index + 1);
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
bridgeSend({
|
|
2781
|
+
type: "proxy.ws.error",
|
|
2782
|
+
id,
|
|
2783
|
+
error: `Cannot reach ${url}`,
|
|
2784
|
+
});
|
|
2785
|
+
});
|
|
2786
|
+
};
|
|
2787
|
+
tryHost(0);
|
|
2733
2788
|
}
|
|
2734
2789
|
|
|
2735
2790
|
function handleProxyWsOpenFromAdmin(msg) {
|
|
@@ -2744,6 +2799,9 @@ function handleProxyWsOpenFromAdmin(msg) {
|
|
|
2744
2799
|
}
|
|
2745
2800
|
const path = safeProxyPath(msg.path);
|
|
2746
2801
|
const protocols = wsProtocolsFromMsg(msg);
|
|
2802
|
+
const fromMsg =
|
|
2803
|
+
msg?.portUrls && typeof msg.portUrls === "object" ? msg.portUrls : {};
|
|
2804
|
+
proxyWsPortUrls.set(id, { ...sharePortUrls(ws), ...fromMsg });
|
|
2747
2805
|
if (isAiServerAppId(appId, ws)) {
|
|
2748
2806
|
void (async () => {
|
|
2749
2807
|
const embedded = await ensureEmbeddedChat(ws);
|
|
@@ -2772,7 +2830,9 @@ function handleProxyWsFrameFromAdmin(msg) {
|
|
|
2772
2830
|
if (msg.binary === true) {
|
|
2773
2831
|
socket.send(Buffer.from(String(msg.data || ""), "base64"));
|
|
2774
2832
|
} else {
|
|
2775
|
-
|
|
2833
|
+
const portUrls = proxyWsPortUrls.get(id);
|
|
2834
|
+
const data = String(msg.data || "");
|
|
2835
|
+
socket.send(portUrls ? rewriteMappedLocalUrls(data, portUrls) : data);
|
|
2776
2836
|
}
|
|
2777
2837
|
} catch {
|
|
2778
2838
|
/* ignore */
|
|
@@ -2784,6 +2844,7 @@ function handleProxyWsCloseFromAdmin(msg) {
|
|
|
2784
2844
|
const socket = proxyLocalSockets.get(id);
|
|
2785
2845
|
if (!socket) return;
|
|
2786
2846
|
proxyLocalSockets.delete(id);
|
|
2847
|
+
proxyWsPortUrls.delete(id);
|
|
2787
2848
|
try {
|
|
2788
2849
|
socket.close(
|
|
2789
2850
|
typeof msg.code === "number" ? msg.code : 1000,
|
|
@@ -2865,7 +2926,6 @@ async function forgetLaunch(sandboxId) {
|
|
|
2865
2926
|
if (key.startsWith(`${sandboxId}::`)) processProblems.delete(key);
|
|
2866
2927
|
}
|
|
2867
2928
|
await closeRememberedTerminals(sandboxId);
|
|
2868
|
-
await stopCloudflare(sandboxId);
|
|
2869
2929
|
}
|
|
2870
2930
|
|
|
2871
2931
|
function problemKey(sandboxId, code, role = "") {
|
|
@@ -3218,6 +3278,10 @@ async function openInNewTerminal(opts) {
|
|
|
3218
3278
|
}
|
|
3219
3279
|
|
|
3220
3280
|
function commandWithPort(job, scripts, port) {
|
|
3281
|
+
const command = String(job.command || "");
|
|
3282
|
+
if (!/^(npm|pnpm|yarn|npx)\s/i.test(command)) {
|
|
3283
|
+
return command;
|
|
3284
|
+
}
|
|
3221
3285
|
const raw = String(scripts?.[job.script] || "");
|
|
3222
3286
|
if (
|
|
3223
3287
|
job.role === "ui" ||
|
|
@@ -3225,9 +3289,9 @@ function commandWithPort(job, scripts, port) {
|
|
|
3225
3289
|
isUiCommand(raw) ||
|
|
3226
3290
|
/--port\b/i.test(raw)
|
|
3227
3291
|
) {
|
|
3228
|
-
return `${
|
|
3292
|
+
return `${command} -- --port ${port}`;
|
|
3229
3293
|
}
|
|
3230
|
-
return
|
|
3294
|
+
return command;
|
|
3231
3295
|
}
|
|
3232
3296
|
|
|
3233
3297
|
async function stopEmbeddedChat(sandboxId) {
|
|
@@ -3254,15 +3318,10 @@ async function ensureEmbeddedChat(ws) {
|
|
|
3254
3318
|
if (inflight) return inflight;
|
|
3255
3319
|
const task = (async () => {
|
|
3256
3320
|
const cfg = bridgeCfg || loadConfig();
|
|
3257
|
-
const jobs = jobsFromHostApps(ws);
|
|
3258
|
-
const shareEnv = {
|
|
3259
|
-
...envForWorkspacePorts(ws, jobs),
|
|
3260
|
-
...uiPublicEnv({}, ws),
|
|
3261
|
-
};
|
|
3262
3321
|
log(`lazy-start in-process chat for ${ws.sandboxName || shortId(ws.sandboxId)}`);
|
|
3263
3322
|
const result = await startAiServerForWorkspace(ws, {
|
|
3264
3323
|
cfg,
|
|
3265
|
-
env:
|
|
3324
|
+
env: chatLaunchEnv(ws, cfg),
|
|
3266
3325
|
forceEmbed: true,
|
|
3267
3326
|
});
|
|
3268
3327
|
if (!result.up) return null;
|
|
@@ -3343,14 +3402,6 @@ async function startAiServerForWorkspace(ws, opts = {}) {
|
|
|
3343
3402
|
const localAi = `http://localhost:${port}`;
|
|
3344
3403
|
const overrideEnv =
|
|
3345
3404
|
opts.env && typeof opts.env === "object" ? opts.env : {};
|
|
3346
|
-
const publicAi =
|
|
3347
|
-
(typeof overrideEnv.AI_SERVER_URL === "string" &&
|
|
3348
|
-
overrideEnv.AI_SERVER_URL.trim()) ||
|
|
3349
|
-
(typeof overrideEnv.NEXT_PUBLIC_AI_SERVER_URL === "string" &&
|
|
3350
|
-
overrideEnv.NEXT_PUBLIC_AI_SERVER_URL.trim()) ||
|
|
3351
|
-
(typeof ws.cloudflare?.ai === "string" && ws.cloudflare.ai.trim()) ||
|
|
3352
|
-
"";
|
|
3353
|
-
const aiUrl = publicAi || localAi;
|
|
3354
3405
|
persistWorkspaceEntry(cfg, ws);
|
|
3355
3406
|
|
|
3356
3407
|
const storeEnv = await loadSandboxStoreEnv(ws, cfg);
|
|
@@ -3360,7 +3411,7 @@ async function startAiServerForWorkspace(ws, opts = {}) {
|
|
|
3360
3411
|
...folderEnv,
|
|
3361
3412
|
...overrideEnv,
|
|
3362
3413
|
...storeEnv,
|
|
3363
|
-
AI_SERVER_URL:
|
|
3414
|
+
AI_SERVER_URL: localAi,
|
|
3364
3415
|
CORS_ORIGIN: overrideEnv.CORS_ORIGIN || folderEnv.CORS_ORIGIN || "",
|
|
3365
3416
|
CORS_ORIGINS: overrideEnv.CORS_ORIGINS || folderEnv.CORS_ORIGINS || "",
|
|
3366
3417
|
MAINTAINER_PRO_DATA_DIR: dataDir,
|
|
@@ -3403,37 +3454,6 @@ function sleep(ms) {
|
|
|
3403
3454
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3404
3455
|
}
|
|
3405
3456
|
|
|
3406
|
-
function stopAllCloudflare() {
|
|
3407
|
-
for (const id of [...cloudflareTunnels.keys()]) stopCloudflare(id);
|
|
3408
|
-
}
|
|
3409
|
-
|
|
3410
|
-
function killProcessesByCommand(fragment) {
|
|
3411
|
-
if (!fragment) return Promise.resolve();
|
|
3412
|
-
return new Promise((resolve) => {
|
|
3413
|
-
const done = () => resolve();
|
|
3414
|
-
if (process.platform === "win32") {
|
|
3415
|
-
const escaped = String(fragment).replace(/'/g, "''");
|
|
3416
|
-
const child = spawn(
|
|
3417
|
-
"powershell.exe",
|
|
3418
|
-
[
|
|
3419
|
-
"-NoProfile",
|
|
3420
|
-
"-Command",
|
|
3421
|
-
`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${escaped}*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`,
|
|
3422
|
-
],
|
|
3423
|
-
{ windowsHide: true, stdio: "ignore" }
|
|
3424
|
-
);
|
|
3425
|
-
child.on("exit", done);
|
|
3426
|
-
child.on("error", done);
|
|
3427
|
-
setTimeout(done, 8000);
|
|
3428
|
-
return;
|
|
3429
|
-
}
|
|
3430
|
-
const child = spawn("pkill", ["-f", String(fragment)], { stdio: "ignore" });
|
|
3431
|
-
child.on("exit", done);
|
|
3432
|
-
child.on("error", done);
|
|
3433
|
-
setTimeout(done, 4000);
|
|
3434
|
-
});
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
3457
|
function killPort(port) {
|
|
3438
3458
|
const n = Number(port);
|
|
3439
3459
|
if (!n) return Promise.resolve();
|
|
@@ -3465,29 +3485,6 @@ function killPort(port) {
|
|
|
3465
3485
|
});
|
|
3466
3486
|
}
|
|
3467
3487
|
|
|
3468
|
-
function stopCloudflare(sandboxId, folder) {
|
|
3469
|
-
const row = cloudflareTunnels.get(sandboxId);
|
|
3470
|
-
const files = new Set(
|
|
3471
|
-
(row?.tunnels?.map((t) => t.logFile).filter(Boolean) ?? [])
|
|
3472
|
-
);
|
|
3473
|
-
cloudflareTunnels.delete(sandboxId);
|
|
3474
|
-
const logDir = folder
|
|
3475
|
-
? dataDirFor(folder, sandboxId)
|
|
3476
|
-
: null;
|
|
3477
|
-
if (logDir && fs.existsSync(logDir)) {
|
|
3478
|
-
try {
|
|
3479
|
-
for (const name of fs.readdirSync(logDir)) {
|
|
3480
|
-
if (/^cf-.*\.log$/i.test(name) && !name.endsWith(".stale")) {
|
|
3481
|
-
files.add(path.join(logDir, name));
|
|
3482
|
-
}
|
|
3483
|
-
}
|
|
3484
|
-
} catch {
|
|
3485
|
-
/* ignore */
|
|
3486
|
-
}
|
|
3487
|
-
}
|
|
3488
|
-
return Promise.all([...files].map((file) => killProcessesByCommand(file)));
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
3488
|
function forgetProcessLaunches(sandboxId) {
|
|
3492
3489
|
for (const key of [...launchedAt.keys()]) {
|
|
3493
3490
|
if (key === sandboxId || key.startsWith(`${sandboxId}:`)) {
|
|
@@ -3545,231 +3542,6 @@ async function waitUntilReachable(url, timeoutMs, label, onWait) {
|
|
|
3545
3542
|
throw new Error(`${label} did not become reachable at ${url}`);
|
|
3546
3543
|
}
|
|
3547
3544
|
|
|
3548
|
-
function applyBackendPublicEnv(env, url) {
|
|
3549
|
-
const backend = String(url || "").replace(/\/$/, "");
|
|
3550
|
-
if (!backend) return env;
|
|
3551
|
-
env.API_URL = backend;
|
|
3552
|
-
env.API_BASE_URL = backend;
|
|
3553
|
-
env.BACKEND_URL = backend;
|
|
3554
|
-
env.VITE_API_URL = backend;
|
|
3555
|
-
env.VITE_API_BASE_URL = backend;
|
|
3556
|
-
env.NEXT_PUBLIC_API_URL = backend;
|
|
3557
|
-
env.NEXT_PUBLIC_API_BASE_URL = backend;
|
|
3558
|
-
env.REACT_APP_API_URL = backend;
|
|
3559
|
-
return env;
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
|
-
function uiPublicEnv(tunnels, ws) {
|
|
3563
|
-
/** @type {Record<string, string>} */
|
|
3564
|
-
const env = {};
|
|
3565
|
-
const aiApp = (Array.isArray(ws?.hostApps) ? ws.hostApps : []).find(
|
|
3566
|
-
(app) => app.role === "ai-server" || app.id === "ai-server"
|
|
3567
|
-
) || { id: "ai-server", role: "ai-server" };
|
|
3568
|
-
const ai =
|
|
3569
|
-
proxyUrlForApp(ws, aiApp) ||
|
|
3570
|
-
(tunnels.ai ? String(tunnels.ai).replace(/\/$/, "") : "");
|
|
3571
|
-
if (ai) {
|
|
3572
|
-
env.AI_SERVER_URL = ai;
|
|
3573
|
-
env.NEXT_PUBLIC_AI_SERVER_URL = ai;
|
|
3574
|
-
env.VITE_AI_SERVER_URL = ai;
|
|
3575
|
-
env.REACT_APP_AI_SERVER_URL = ai;
|
|
3576
|
-
}
|
|
3577
|
-
const backendApp = (Array.isArray(ws?.hostApps) ? ws.hostApps : []).find(
|
|
3578
|
-
(app) => isBackendApp(app)
|
|
3579
|
-
);
|
|
3580
|
-
const backendUrl =
|
|
3581
|
-
(backendApp ? proxyUrlForApp(ws, backendApp) : "") ||
|
|
3582
|
-
(typeof tunnels.backend === "string" && tunnels.backend) ||
|
|
3583
|
-
(backendApp ? publicUrlForApp(ws, backendApp) : "") ||
|
|
3584
|
-
"";
|
|
3585
|
-
if (backendUrl && backendApp) {
|
|
3586
|
-
applyBackendPublicEnv(env, backendUrl);
|
|
3587
|
-
}
|
|
3588
|
-
if (tunnels.ui) {
|
|
3589
|
-
const ui = String(tunnels.ui).replace(/\/$/, "");
|
|
3590
|
-
env.APP_URL = ui;
|
|
3591
|
-
env.PUBLIC_URL = ui;
|
|
3592
|
-
env.CORS_ORIGIN = ui;
|
|
3593
|
-
env.NEXT_PUBLIC_APP_URL = ui;
|
|
3594
|
-
env.VITE_APP_URL = ui;
|
|
3595
|
-
env.REACT_APP_APP_URL = ui;
|
|
3596
|
-
} else if (tunnels.app) {
|
|
3597
|
-
const app = String(tunnels.app).replace(/\/$/, "");
|
|
3598
|
-
env.APP_URL = app;
|
|
3599
|
-
env.PUBLIC_URL = app;
|
|
3600
|
-
env.CORS_ORIGIN = app;
|
|
3601
|
-
env.NEXT_PUBLIC_APP_URL = app;
|
|
3602
|
-
env.VITE_APP_URL = app;
|
|
3603
|
-
env.REACT_APP_APP_URL = app;
|
|
3604
|
-
} else if (ai) {
|
|
3605
|
-
const hostUi = (Array.isArray(ws?.hostApps) ? ws.hostApps : []).some(
|
|
3606
|
-
(app) => app.host || app.role === "ui" || app.role === "app"
|
|
3607
|
-
);
|
|
3608
|
-
if (!hostUi) {
|
|
3609
|
-
env.CORS_ORIGIN = ai;
|
|
3610
|
-
env.APP_URL = ai;
|
|
3611
|
-
env.PUBLIC_URL = ai;
|
|
3612
|
-
env.NEXT_PUBLIC_APP_URL = ai;
|
|
3613
|
-
env.VITE_APP_URL = ai;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
return env;
|
|
3617
|
-
}
|
|
3618
|
-
|
|
3619
|
-
function publicUrlForApp(ws, app) {
|
|
3620
|
-
if (!app) return "";
|
|
3621
|
-
const proxied = proxyUrlForApp(ws, app);
|
|
3622
|
-
if (proxied) return proxied;
|
|
3623
|
-
const port = Number(app.port) || 0;
|
|
3624
|
-
return port ? `http://localhost:${port}` : "";
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
function envFromConfiguredMaps(ws) {
|
|
3628
|
-
/** @type {Record<string, string>} */
|
|
3629
|
-
const env = {};
|
|
3630
|
-
const apps = Array.isArray(ws.hostApps) ? ws.hostApps : [];
|
|
3631
|
-
for (const app of apps) {
|
|
3632
|
-
const maps = Array.isArray(app.envMaps) ? app.envMaps : [];
|
|
3633
|
-
for (const row of maps) {
|
|
3634
|
-
const key = String(row?.key || "").trim();
|
|
3635
|
-
const sourceId = String(row?.sourceAppId || "").trim();
|
|
3636
|
-
if (!key || !sourceId) continue;
|
|
3637
|
-
const source =
|
|
3638
|
-
sourceId === "self"
|
|
3639
|
-
? app
|
|
3640
|
-
: apps.find((item) => item.id === sourceId) || null;
|
|
3641
|
-
const url = publicUrlForApp(ws, source);
|
|
3642
|
-
if (url) env[key] = url;
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
return env;
|
|
3646
|
-
}
|
|
3647
|
-
|
|
3648
|
-
function cloudflarePublicEnv(ws) {
|
|
3649
|
-
const tunnels =
|
|
3650
|
-
ws.cloudflare && typeof ws.cloudflare === "object" ? ws.cloudflare : {};
|
|
3651
|
-
const jobs = jobsFromHostApps(ws);
|
|
3652
|
-
const env = {
|
|
3653
|
-
...envForWorkspacePorts(ws, jobs),
|
|
3654
|
-
...uiPublicEnv(tunnels, ws),
|
|
3655
|
-
};
|
|
3656
|
-
const origins = [];
|
|
3657
|
-
const add = (value) => {
|
|
3658
|
-
const origin = originFromUrl(value);
|
|
3659
|
-
if (origin && !origins.includes(origin)) origins.push(origin);
|
|
3660
|
-
};
|
|
3661
|
-
add(tunnels.ui);
|
|
3662
|
-
add(tunnels.app);
|
|
3663
|
-
add(tunnels.backend);
|
|
3664
|
-
add(tunnels.ai);
|
|
3665
|
-
add(env.APP_URL);
|
|
3666
|
-
add(env.CORS_ORIGIN);
|
|
3667
|
-
for (const job of jobs) {
|
|
3668
|
-
if (job.port) add(`http://localhost:${job.port}`);
|
|
3669
|
-
}
|
|
3670
|
-
add(`http://localhost:${Number(ws.port) || 3100}`);
|
|
3671
|
-
if (origins.length) {
|
|
3672
|
-
env.CORS_ORIGIN = origins.join(",");
|
|
3673
|
-
env.CORS_ORIGINS = origins.join(",");
|
|
3674
|
-
}
|
|
3675
|
-
Object.assign(env, envFromConfiguredMaps(ws));
|
|
3676
|
-
return env;
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
async function applyPublicUrlsToRunningApps(ws, cfg, opts = {}) {
|
|
3680
|
-
const folder = ws.folderPath ? path.resolve(ws.folderPath) : "";
|
|
3681
|
-
const progress =
|
|
3682
|
-
typeof opts.onProgress === "function" ? opts.onProgress : async () => {};
|
|
3683
|
-
const tunnels =
|
|
3684
|
-
ws.cloudflare && typeof ws.cloudflare === "object" ? ws.cloudflare : {};
|
|
3685
|
-
const hasProxy = Boolean(ws.proxy?.token);
|
|
3686
|
-
if (
|
|
3687
|
-
!folder ||
|
|
3688
|
-
!fs.existsSync(folder) ||
|
|
3689
|
-
(!Object.keys(tunnels).length && !hasProxy)
|
|
3690
|
-
) {
|
|
3691
|
-
return { restarted: [], rewritten: [] };
|
|
3692
|
-
}
|
|
3693
|
-
const env = {
|
|
3694
|
-
...cloudflarePublicEnv(ws),
|
|
3695
|
-
};
|
|
3696
|
-
if (Object.keys(tunnels).length) writeTunnelEnv(ws, tunnels);
|
|
3697
|
-
const mappedKeys = Object.keys(envFromConfiguredMaps(ws));
|
|
3698
|
-
if (mappedKeys.length) {
|
|
3699
|
-
activity(
|
|
3700
|
-
ws.sandboxId,
|
|
3701
|
-
"info",
|
|
3702
|
-
`Injecting public URLs into process env for ${mappedKeys.join(", ")}`
|
|
3703
|
-
);
|
|
3704
|
-
await progress(`Passing ${mappedKeys.join(", ")} via process env`);
|
|
3705
|
-
}
|
|
3706
|
-
|
|
3707
|
-
const probe = await probeRunningApps(ws, 1200);
|
|
3708
|
-
const runningRoles = new Set(
|
|
3709
|
-
(probe.hostApps || [])
|
|
3710
|
-
.filter((app) => app.running)
|
|
3711
|
-
.map((app) =>
|
|
3712
|
-
app.role === "ai-server" ? "ai-server" : app.role === "custom" ? "app" : app.role
|
|
3713
|
-
)
|
|
3714
|
-
);
|
|
3715
|
-
const reserved = reservedPortsFor(cfg, ws.sandboxId);
|
|
3716
|
-
const restarted = [];
|
|
3717
|
-
|
|
3718
|
-
if (probe.chatUp || runningRoles.has("ai-server") || embeddedChat.has(ws.sandboxId)) {
|
|
3719
|
-
await progress("Restarting in-process chat with public URLs and CORS…");
|
|
3720
|
-
launchedAt.delete(`${ws.sandboxId}:${folder}:ai`);
|
|
3721
|
-
await stopEmbeddedChat(ws.sandboxId);
|
|
3722
|
-
await startAiServerForWorkspace(ws, {
|
|
3723
|
-
reserved,
|
|
3724
|
-
cfg,
|
|
3725
|
-
port: ws.port,
|
|
3726
|
-
env,
|
|
3727
|
-
});
|
|
3728
|
-
restarted.push("ai-server");
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
|
-
const jobs = jobsFromHostApps(ws).filter((job) =>
|
|
3732
|
-
runningRoles.has(job.role === "custom" ? "app" : job.role)
|
|
3733
|
-
);
|
|
3734
|
-
if (jobs.length) {
|
|
3735
|
-
const roles = [...new Set(jobs.map((job) => job.role))];
|
|
3736
|
-
await progress(
|
|
3737
|
-
`Restarting ${roles.join(", ")} so they load the public URLs…`
|
|
3738
|
-
);
|
|
3739
|
-
for (const job of jobs) {
|
|
3740
|
-
launchedAt.delete(`${ws.sandboxId}:${folder}:${job.script}`);
|
|
3741
|
-
await killPort(job.port);
|
|
3742
|
-
}
|
|
3743
|
-
await sleep(1500);
|
|
3744
|
-
await ensureHostProcesses(ws, {
|
|
3745
|
-
reserved,
|
|
3746
|
-
cfg,
|
|
3747
|
-
onlyRoles: roles,
|
|
3748
|
-
extraEnv: env,
|
|
3749
|
-
force: true,
|
|
3750
|
-
plannedJobs: jobsFromHostApps(ws),
|
|
3751
|
-
});
|
|
3752
|
-
restarted.push(...roles);
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
if (restarted.length) {
|
|
3756
|
-
activity(
|
|
3757
|
-
ws.sandboxId,
|
|
3758
|
-
"info",
|
|
3759
|
-
`Restarted ${[...new Set(restarted)].join(", ")} with public URLs`
|
|
3760
|
-
);
|
|
3761
|
-
}
|
|
3762
|
-
return { restarted: [...new Set(restarted)], rewritten: [], env };
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
function writeTunnelEnv(ws, tunnels) {
|
|
3766
|
-
const folder = ws.folderPath ? path.resolve(ws.folderPath) : null;
|
|
3767
|
-
if (!folder || !fs.existsSync(folder)) return { ok: false, env: {} };
|
|
3768
|
-
clearCloudflareTunnelFile(folder);
|
|
3769
|
-
const env = uiPublicEnv(tunnels, ws);
|
|
3770
|
-
return { ok: true, env };
|
|
3771
|
-
}
|
|
3772
|
-
|
|
3773
3545
|
function reservedPortsFor(cfg, sandboxId) {
|
|
3774
3546
|
const reserved = new Set();
|
|
3775
3547
|
for (const other of cfg.workspaces || []) {
|
|
@@ -3784,17 +3556,12 @@ function appsWanted(ws) {
|
|
|
3784
3556
|
return Boolean(ws?.appsRequested);
|
|
3785
3557
|
}
|
|
3786
3558
|
|
|
3787
|
-
async function
|
|
3559
|
+
async function configureShareForWorkspace(ws, cfg, opts = {}) {
|
|
3788
3560
|
const sandboxId = ws.sandboxId;
|
|
3789
3561
|
const progress = (message) =>
|
|
3790
3562
|
reportActionProgress(cfg, opts.actionId, message);
|
|
3791
|
-
await progress("Using Maintainer Pro share URLs
|
|
3792
|
-
|
|
3793
|
-
await stopCloudflare(sandboxId, ws.folderPath);
|
|
3794
|
-
} catch {
|
|
3795
|
-
/* leftover tunnels */
|
|
3796
|
-
}
|
|
3797
|
-
ws.cloudflarePending = false;
|
|
3563
|
+
await progress("Using Maintainer Pro share URLs.");
|
|
3564
|
+
stripDiscardedTunnelState(ws, cfg);
|
|
3798
3565
|
persistWorkspaceEntry(cfg, ws);
|
|
3799
3566
|
const status = await reconcileWorkspacePresence(ws, cfg, {
|
|
3800
3567
|
timeoutMs: 2500,
|
|
@@ -3808,12 +3575,10 @@ async function configureCloudflareForWorkspace(ws, cfg, opts = {}) {
|
|
|
3808
3575
|
folderPath: ws.folderPath,
|
|
3809
3576
|
port: ws.port,
|
|
3810
3577
|
pending: false,
|
|
3811
|
-
cloudflarePending: false,
|
|
3812
3578
|
waitingForStart: false,
|
|
3813
3579
|
appUrl: status.host.appUrl || share,
|
|
3814
3580
|
origins: status.host.origins,
|
|
3815
3581
|
publicUrl: share || null,
|
|
3816
|
-
cloudflare: false,
|
|
3817
3582
|
};
|
|
3818
3583
|
}
|
|
3819
3584
|
|
|
@@ -3825,12 +3590,7 @@ async function startAppsForWorkspace(ws, cfg, opts = {}) {
|
|
|
3825
3590
|
`Checking running apps for ${label}…`
|
|
3826
3591
|
);
|
|
3827
3592
|
|
|
3828
|
-
|
|
3829
|
-
await stopCloudflare(ws.sandboxId, ws.folderPath);
|
|
3830
|
-
} catch {
|
|
3831
|
-
/* leftover cloudflared */
|
|
3832
|
-
}
|
|
3833
|
-
ws.cloudflarePending = false;
|
|
3593
|
+
stripDiscardedTunnelState(ws, cfg);
|
|
3834
3594
|
|
|
3835
3595
|
let status = await reconcileWorkspacePresence(ws, cfg, {
|
|
3836
3596
|
timeoutMs: 2500,
|
|
@@ -3858,11 +3618,7 @@ async function startAppsForWorkspace(ws, cfg, opts = {}) {
|
|
|
3858
3618
|
timeoutMs: 800,
|
|
3859
3619
|
});
|
|
3860
3620
|
|
|
3861
|
-
const
|
|
3862
|
-
...envForWorkspacePorts(ws, plan.jobs),
|
|
3863
|
-
...uiPublicEnv({}, ws),
|
|
3864
|
-
...envFromConfiguredMaps(ws),
|
|
3865
|
-
};
|
|
3621
|
+
const chatEnv = chatLaunchEnv(ws, cfg);
|
|
3866
3622
|
|
|
3867
3623
|
if (!cfg.noAiServer) {
|
|
3868
3624
|
if (status.probe.chatUp) {
|
|
@@ -3875,7 +3631,7 @@ async function startAppsForWorkspace(ws, cfg, opts = {}) {
|
|
|
3875
3631
|
reserved,
|
|
3876
3632
|
cfg,
|
|
3877
3633
|
port: plan.aiPort,
|
|
3878
|
-
env:
|
|
3634
|
+
env: chatEnv,
|
|
3879
3635
|
});
|
|
3880
3636
|
await sleep(1500);
|
|
3881
3637
|
}
|
|
@@ -3890,14 +3646,9 @@ async function startAppsForWorkspace(ws, cfg, opts = {}) {
|
|
|
3890
3646
|
const probed = status.probe.hosts.find((h) => h.role === job.role);
|
|
3891
3647
|
return probed?.up ? { ...job, up: true, port: probed.port } : job;
|
|
3892
3648
|
}),
|
|
3893
|
-
extraEnv: shareEnv,
|
|
3894
3649
|
});
|
|
3895
3650
|
await sleep(800);
|
|
3896
3651
|
|
|
3897
|
-
await applyPublicUrlsToRunningApps(ws, cfg, {
|
|
3898
|
-
onProgress: (message) => reportActionProgress(cfg, opts.actionId, message),
|
|
3899
|
-
});
|
|
3900
|
-
|
|
3901
3652
|
status = await reconcileWorkspacePresence(ws, cfg, {
|
|
3902
3653
|
timeoutMs: 2500,
|
|
3903
3654
|
});
|
|
@@ -3929,7 +3680,6 @@ async function startAppsForWorkspace(ws, cfg, opts = {}) {
|
|
|
3929
3680
|
ws,
|
|
3930
3681
|
hostAppOf(ws) || { id: "ui", role: "ui", host: true }
|
|
3931
3682
|
) || null,
|
|
3932
|
-
cloudflare: false,
|
|
3933
3683
|
hostApps: status.probe.hostApps || ws.hostApps || [],
|
|
3934
3684
|
processIssues,
|
|
3935
3685
|
warning,
|
|
@@ -3975,9 +3725,6 @@ async function startSingleApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
3975
3725
|
await progress(`Starting ${app.name} on port ${app.port}…`);
|
|
3976
3726
|
ws.appsRequested = true;
|
|
3977
3727
|
persistWorkspaceEntry(cfg, ws);
|
|
3978
|
-
const publicEnv = {
|
|
3979
|
-
...cloudflarePublicEnv(ws),
|
|
3980
|
-
};
|
|
3981
3728
|
if (app.role === "ai-server") {
|
|
3982
3729
|
const reserved = reservedPortsFor(cfg, ws.sandboxId);
|
|
3983
3730
|
const chat = await discoverChatPort(ws, 1200);
|
|
@@ -3989,7 +3736,7 @@ async function startSingleApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
3989
3736
|
reserved,
|
|
3990
3737
|
cfg,
|
|
3991
3738
|
port: Number(app.port) || ws.port || 3100,
|
|
3992
|
-
env:
|
|
3739
|
+
env: chatLaunchEnv(ws, cfg),
|
|
3993
3740
|
});
|
|
3994
3741
|
}
|
|
3995
3742
|
} else {
|
|
@@ -3997,10 +3744,8 @@ async function startSingleApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
3997
3744
|
await ensureHostProcesses(ws, {
|
|
3998
3745
|
reserved,
|
|
3999
3746
|
cfg,
|
|
4000
|
-
onlyRoles: [app.role === "custom" ? "app" : app.role],
|
|
4001
|
-
extraEnv: publicEnv,
|
|
4002
3747
|
force: true,
|
|
4003
|
-
plannedJobs: jobsFromHostApps(ws),
|
|
3748
|
+
plannedJobs: jobsFromHostApps(ws, { appId: app.id }),
|
|
4004
3749
|
});
|
|
4005
3750
|
}
|
|
4006
3751
|
const status = await reconcileWorkspacePresence(ws, cfg, {
|
|
@@ -4068,7 +3813,7 @@ async function restartSingleApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
4068
3813
|
return startSingleApp(ws, cfg, payload, opts);
|
|
4069
3814
|
}
|
|
4070
3815
|
|
|
4071
|
-
async function
|
|
3816
|
+
async function startShareForApp(ws, cfg, payload = {}, opts = {}) {
|
|
4072
3817
|
const app = findHostApp(ws, payload);
|
|
4073
3818
|
if (!app) {
|
|
4074
3819
|
activity(ws.sandboxId, "error", "Share URL failed: unknown app/port");
|
|
@@ -4086,9 +3831,6 @@ async function startCloudflareForApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
4086
3831
|
await progress(`Using the Maintainer Pro share URL for ${app.name}…`);
|
|
4087
3832
|
const started = await startSingleApp(ws, cfg, { appId: app.id }, opts);
|
|
4088
3833
|
if (started.error) return started;
|
|
4089
|
-
const applied = await applyPublicUrlsToRunningApps(ws, cfg, {
|
|
4090
|
-
onProgress: progress,
|
|
4091
|
-
});
|
|
4092
3834
|
const status = await reconcileWorkspacePresence(ws, cfg, {
|
|
4093
3835
|
timeoutMs: 2500,
|
|
4094
3836
|
});
|
|
@@ -4098,12 +3840,9 @@ async function startCloudflareForApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
4098
3840
|
running: Boolean(started.running),
|
|
4099
3841
|
port: started.port,
|
|
4100
3842
|
publicUrl: shareUrl,
|
|
4101
|
-
cloudflareUrl: null,
|
|
4102
3843
|
appUrl: status.host.appUrl,
|
|
4103
3844
|
origins: status.host.origins,
|
|
4104
3845
|
hostApps: status.probe.hostApps || [],
|
|
4105
|
-
rewritten: applied.rewritten,
|
|
4106
|
-
restarted: applied.restarted,
|
|
4107
3846
|
log: activityLogFor(ws.sandboxId),
|
|
4108
3847
|
};
|
|
4109
3848
|
}
|
|
@@ -4118,32 +3857,21 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
4118
3857
|
const reserved = opts.reserved instanceof Set ? opts.reserved : new Set();
|
|
4119
3858
|
const cfg = opts.cfg || null;
|
|
4120
3859
|
const folder = path.resolve(ws.folderPath);
|
|
4121
|
-
const scripts = readPackageJson(folder)?.scripts || {};
|
|
4122
3860
|
const jobs = Array.isArray(opts.plannedJobs)
|
|
4123
3861
|
? opts.plannedJobs
|
|
4124
|
-
:
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
3862
|
+
: jobsFromHostApps(ws).length
|
|
3863
|
+
? jobsFromHostApps(ws)
|
|
3864
|
+
: planHostJobs(
|
|
3865
|
+
folder,
|
|
3866
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
3867
|
+
ws.projectInfo
|
|
3868
|
+
);
|
|
4129
3869
|
const onlyRoles = Array.isArray(opts.onlyRoles) ? new Set(opts.onlyRoles) : null;
|
|
4130
3870
|
const extraEnv = opts.extraEnv && typeof opts.extraEnv === "object" ? opts.extraEnv : {};
|
|
4131
3871
|
const started = [];
|
|
4132
3872
|
let persisted = false;
|
|
4133
3873
|
const label = ws.sandboxName || "this sandbox";
|
|
4134
3874
|
|
|
4135
|
-
if (jobs.length > 0 && !fs.existsSync(folder)) {
|
|
4136
|
-
recordProcessProblem({
|
|
4137
|
-
sandboxId: ws.sandboxId,
|
|
4138
|
-
code: "host_process_launch",
|
|
4139
|
-
role: "app",
|
|
4140
|
-
title: `Could not start the app (${label})`,
|
|
4141
|
-
message: `The project folder is missing: ${folder}`,
|
|
4142
|
-
resolution: "Attach the folder again from Local setup.",
|
|
4143
|
-
});
|
|
4144
|
-
return started;
|
|
4145
|
-
}
|
|
4146
|
-
|
|
4147
3875
|
log(
|
|
4148
3876
|
`start hosts ${label}: ${
|
|
4149
3877
|
jobs.length
|
|
@@ -4157,20 +3885,31 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
4157
3885
|
|
|
4158
3886
|
for (const job of jobs) {
|
|
4159
3887
|
if (onlyRoles && !onlyRoles.has(job.role)) continue;
|
|
3888
|
+
const jobFolder = path.resolve(job.folder || folder);
|
|
3889
|
+
if (!fs.existsSync(jobFolder)) {
|
|
3890
|
+
recordProcessProblem({
|
|
3891
|
+
sandboxId: ws.sandboxId,
|
|
3892
|
+
code: "host_process_launch",
|
|
3893
|
+
role: job.role,
|
|
3894
|
+
title: `Could not start the ${processRoleLabel(job.role)} (${label})`,
|
|
3895
|
+
message: `The project folder is missing: ${jobFolder}`,
|
|
3896
|
+
resolution: "Attach the folder again from Local setup.",
|
|
3897
|
+
});
|
|
3898
|
+
continue;
|
|
3899
|
+
}
|
|
3900
|
+
const scripts = readPackageJson(jobFolder)?.scripts || {};
|
|
4160
3901
|
const preferred = Number(job.port || job.preferredPort) || 3000;
|
|
4161
|
-
const probe =
|
|
4162
|
-
job.port
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
).replace("localhost", "127.0.0.1");
|
|
4166
|
-
if (job.up || (await probeUrl(probe))) {
|
|
3902
|
+
const probe = job.port
|
|
3903
|
+
? `http://127.0.0.1:${job.port}`
|
|
3904
|
+
: job.probeUrl || `http://127.0.0.1:${preferred}`;
|
|
3905
|
+
if (job.up || (await probeLoopbackUrl(probe))) {
|
|
4167
3906
|
reserved.add(portFromText(probe, preferred));
|
|
4168
3907
|
clearProcessProblem(ws.sandboxId, "host_process_launch", job.role);
|
|
4169
3908
|
clearProcessProblem(ws.sandboxId, "host_process_down", job.role);
|
|
4170
3909
|
log(`${job.role} already running at ${job.probeUrl || probe}`);
|
|
4171
3910
|
continue;
|
|
4172
3911
|
}
|
|
4173
|
-
const launchKey = `${ws.sandboxId}:${
|
|
3912
|
+
const launchKey = `${ws.sandboxId}:${jobFolder}:${job.command || job.script}:${preferred}`;
|
|
4174
3913
|
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
4175
3914
|
reserved.add(preferred);
|
|
4176
3915
|
log(`start ${job.role} skip ${label}: launched recently`);
|
|
@@ -4202,13 +3941,13 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
4202
3941
|
}
|
|
4203
3942
|
|
|
4204
3943
|
const needsInstall =
|
|
4205
|
-
fs.existsSync(path.join(
|
|
4206
|
-
!fs.existsSync(path.join(
|
|
3944
|
+
fs.existsSync(path.join(jobFolder, "package.json")) &&
|
|
3945
|
+
!fs.existsSync(path.join(jobFolder, "node_modules"));
|
|
4207
3946
|
const run = commandWithPort(job, scripts, port);
|
|
4208
3947
|
const command = needsInstall ? `npm install && ${run}` : run;
|
|
4209
3948
|
const opened = await openInNewTerminal({
|
|
4210
3949
|
title: `MP-${job.role}-${port}`,
|
|
4211
|
-
folder,
|
|
3950
|
+
folder: jobFolder,
|
|
4212
3951
|
command,
|
|
4213
3952
|
env: { PORT: String(port), ...extraEnv },
|
|
4214
3953
|
launchKey,
|
|
@@ -4265,12 +4004,9 @@ async function inspectHostJobs(ws) {
|
|
|
4265
4004
|
const hosts = [];
|
|
4266
4005
|
for (const job of jobs) {
|
|
4267
4006
|
const preferred = Number(job.preferredPort) || 3000;
|
|
4268
|
-
const probe =
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
);
|
|
4272
|
-
const up = await probeUrl(probe);
|
|
4273
|
-
const launchKey = `${ws.sandboxId}:${folder}:${job.script}`;
|
|
4007
|
+
const probe = job.probeUrl || `http://127.0.0.1:${preferred}`;
|
|
4008
|
+
const up = await probeLoopbackUrl(probe);
|
|
4009
|
+
const launchKey = `${ws.sandboxId}:${job.folder || folder}:${job.command || job.script}:${preferred}`;
|
|
4274
4010
|
const starting = recentlyLaunched(launchKey);
|
|
4275
4011
|
hosts.push({
|
|
4276
4012
|
role: job.role,
|
|
@@ -4362,6 +4098,79 @@ async function setupWorkspace(cfg, action) {
|
|
|
4362
4098
|
config.sandbox?.name ||
|
|
4363
4099
|
"Maintainer Pro App";
|
|
4364
4100
|
|
|
4101
|
+
const partnerIgnorePaths = Array.isArray(config.aiIgnorePaths)
|
|
4102
|
+
? config.aiIgnorePaths
|
|
4103
|
+
: [];
|
|
4104
|
+
const addFolder = Boolean(action.payload?.addFolder);
|
|
4105
|
+
cfg.workspaces = cfg.workspaces || [];
|
|
4106
|
+
const existingIdx = cfg.workspaces.findIndex((w) => w.sandboxId === sandboxId);
|
|
4107
|
+
const existingWs = existingIdx >= 0 ? cfg.workspaces[existingIdx] : null;
|
|
4108
|
+
|
|
4109
|
+
if (addFolder && existingWs?.folderPath) {
|
|
4110
|
+
const extra = Array.isArray(existingWs.extraFolders)
|
|
4111
|
+
? [...existingWs.extraFolders]
|
|
4112
|
+
: [];
|
|
4113
|
+
if (
|
|
4114
|
+
!sameFolder(existingWs.folderPath, resolved) &&
|
|
4115
|
+
!extra.some((folder) => sameFolder(folder, resolved))
|
|
4116
|
+
) {
|
|
4117
|
+
extra.push(resolved);
|
|
4118
|
+
}
|
|
4119
|
+
existingWs.extraFolders = extra;
|
|
4120
|
+
const access = applyAccessPolicy(resolved, partnerIgnorePaths, sandboxId);
|
|
4121
|
+
persistWorkspaceEntry(cfg, existingWs);
|
|
4122
|
+
const ports = await resolveWorkspaceHostApps(existingWs, {
|
|
4123
|
+
cfg,
|
|
4124
|
+
allowAi: false,
|
|
4125
|
+
ignoreDesired: true,
|
|
4126
|
+
unionDetected: true,
|
|
4127
|
+
force: true,
|
|
4128
|
+
});
|
|
4129
|
+
const needsReview =
|
|
4130
|
+
Boolean(ports.confused) ||
|
|
4131
|
+
!(ports.apps || []).some((app) => app && app.role !== "ai-server");
|
|
4132
|
+
if (needsReview) {
|
|
4133
|
+
activity(
|
|
4134
|
+
sandboxId,
|
|
4135
|
+
"warn",
|
|
4136
|
+
"Review the suggested apps and ports in Maintainer Pro before Start Apps."
|
|
4137
|
+
);
|
|
4138
|
+
}
|
|
4139
|
+
await inspectHostJobs(existingWs);
|
|
4140
|
+
const host = workspaceHostReport(existingWs);
|
|
4141
|
+
const aiServerUp = await isChatServerOnPort(existingWs.port);
|
|
4142
|
+
log(`setup added folder ${resolved} sandbox=${shortId(sandboxId)}`);
|
|
4143
|
+
return {
|
|
4144
|
+
sandboxId,
|
|
4145
|
+
folderPath: existingWs.folderPath,
|
|
4146
|
+
extraFolders: extra,
|
|
4147
|
+
addFolder: true,
|
|
4148
|
+
addedFolder: resolved,
|
|
4149
|
+
port: existingWs.port,
|
|
4150
|
+
appUrl: host.appUrl || existingWs.appUrl || null,
|
|
4151
|
+
origins: host.origins,
|
|
4152
|
+
wroteEnv: false,
|
|
4153
|
+
clientKind: existingWs.clientKind || "skip",
|
|
4154
|
+
clientFiles: [],
|
|
4155
|
+
clientNotes: [`Added folder ${resolved}`],
|
|
4156
|
+
aiServerUp,
|
|
4157
|
+
startedHosts: [],
|
|
4158
|
+
openUrl: existingWs.appUrl || `http://localhost:${existingWs.port}`,
|
|
4159
|
+
processIssues: issuesForSandbox(sandboxId).map(
|
|
4160
|
+
({ role: _role, ...issue }) => issue
|
|
4161
|
+
),
|
|
4162
|
+
warning: needsReview
|
|
4163
|
+
? "Folder attached. Review the suggested apps and ports in Maintainer Pro, then Start Apps."
|
|
4164
|
+
: "Folder attached. Review apps if needed, then use Start Apps.",
|
|
4165
|
+
waitingForStart: !aiServerUp,
|
|
4166
|
+
needsReview,
|
|
4167
|
+
hostApps: ports.apps || existingWs.hostApps || [],
|
|
4168
|
+
reasons: ports.reasons || [],
|
|
4169
|
+
projectInfo: existingWs.projectInfo || null,
|
|
4170
|
+
ignorePaths: access.ignorePaths,
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4365
4174
|
const client = configureClient({
|
|
4366
4175
|
dir: resolved,
|
|
4367
4176
|
port,
|
|
@@ -4374,24 +4183,22 @@ async function setupWorkspace(cfg, action) {
|
|
|
4374
4183
|
const corsOrigin = client.corsOrigin || aiOrigin;
|
|
4375
4184
|
const appUrl = client.appUrl || corsOrigin;
|
|
4376
4185
|
|
|
4377
|
-
const partnerIgnorePaths = Array.isArray(config.aiIgnorePaths)
|
|
4378
|
-
? config.aiIgnorePaths
|
|
4379
|
-
: [];
|
|
4380
4186
|
const access = applyAccessPolicy(resolved, partnerIgnorePaths, sandboxId);
|
|
4381
4187
|
|
|
4382
|
-
cfg.workspaces = cfg.workspaces || [];
|
|
4383
|
-
const existing = cfg.workspaces.findIndex((w) => w.sandboxId === sandboxId);
|
|
4384
4188
|
const entry = {
|
|
4385
4189
|
sandboxId,
|
|
4386
4190
|
folderPath: resolved,
|
|
4191
|
+
extraFolders: Array.isArray(existingWs?.extraFolders)
|
|
4192
|
+
? existingWs.extraFolders.filter((folder) => !sameFolder(folder, resolved))
|
|
4193
|
+
: [],
|
|
4387
4194
|
port,
|
|
4388
4195
|
sandboxName: config.sandbox?.name,
|
|
4389
4196
|
applicationName: config.sandbox?.applicationName,
|
|
4390
4197
|
clientKind: client.kind,
|
|
4391
4198
|
appUrl,
|
|
4392
4199
|
sameOrigin: Boolean(client.sameOrigin),
|
|
4393
|
-
appsRequested: false,
|
|
4394
|
-
|
|
4200
|
+
appsRequested: existingWs?.appsRequested || false,
|
|
4201
|
+
hostApps: existingWs?.hostApps,
|
|
4395
4202
|
store: {
|
|
4396
4203
|
serverKey: String(config.env?.MAINTAINER_PRO_API_KEY || "").trim(),
|
|
4397
4204
|
clientKey: String(
|
|
@@ -4401,7 +4208,7 @@ async function setupWorkspace(cfg, action) {
|
|
|
4401
4208
|
).trim(),
|
|
4402
4209
|
},
|
|
4403
4210
|
};
|
|
4404
|
-
if (
|
|
4211
|
+
if (existingIdx >= 0) cfg.workspaces[existingIdx] = { ...existingWs, ...entry };
|
|
4405
4212
|
else cfg.workspaces.push(entry);
|
|
4406
4213
|
saveConfig(cfg);
|
|
4407
4214
|
|
|
@@ -4453,7 +4260,9 @@ async function setupWorkspace(cfg, action) {
|
|
|
4453
4260
|
const host = workspaceHostReport(entry);
|
|
4454
4261
|
return {
|
|
4455
4262
|
sandboxId,
|
|
4456
|
-
folderPath:
|
|
4263
|
+
folderPath: entry.folderPath,
|
|
4264
|
+
extraFolders: entry.extraFolders || [],
|
|
4265
|
+
addFolder: false,
|
|
4457
4266
|
port: entry.port,
|
|
4458
4267
|
appUrl: host.appUrl || entry.appUrl || appUrl,
|
|
4459
4268
|
origins: host.origins.length
|
|
@@ -4478,7 +4287,8 @@ async function setupWorkspace(cfg, action) {
|
|
|
4478
4287
|
}
|
|
4479
4288
|
|
|
4480
4289
|
async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
4481
|
-
const
|
|
4290
|
+
const folders = workspaceFolders(ws);
|
|
4291
|
+
const folder = folders[0] || path.resolve(ws.folderPath || "");
|
|
4482
4292
|
const label = ws.sandboxName || shortId(ws.sandboxId) || "sandbox";
|
|
4483
4293
|
activity(ws.sandboxId, "info", `Suggesting setup for ${label} from config files`);
|
|
4484
4294
|
try {
|
|
@@ -4490,6 +4300,7 @@ async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
|
4490
4300
|
}
|
|
4491
4301
|
const proposal = await cli.proposeHostAppsFromConfig({
|
|
4492
4302
|
workspaceDir: folder,
|
|
4303
|
+
workspaceDirs: folders,
|
|
4493
4304
|
appName: ws.applicationName || ws.sandboxName,
|
|
4494
4305
|
preferredAiPort: Number(ws.port) || 3100,
|
|
4495
4306
|
allowAi: opts.allowAi !== false,
|
|
@@ -4497,8 +4308,7 @@ async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
|
4497
4308
|
const previous = Array.isArray(ws.hostApps) ? ws.hostApps : [];
|
|
4498
4309
|
const apps = (proposal.apps || []).map((app) => {
|
|
4499
4310
|
const match = previous.find((row) => row && row.id === app.id);
|
|
4500
|
-
|
|
4501
|
-
return { ...app, envMaps: match.envMaps || [] };
|
|
4311
|
+
return match ? { ...app, host: match.host === true || app.host === true } : app;
|
|
4502
4312
|
});
|
|
4503
4313
|
// Keep last known apps on the workspace; do not auto-apply proposal.
|
|
4504
4314
|
if (!Array.isArray(ws.hostApps) || !ws.hostApps.length) {
|
|
@@ -4565,6 +4375,7 @@ async function runActions(cfg, actions) {
|
|
|
4565
4375
|
if (!actions.length) return;
|
|
4566
4376
|
log(`actions received ${actions.length}: ${actions.map((a) => a.code).join(", ")}`);
|
|
4567
4377
|
for (const action of actions) {
|
|
4378
|
+
action.code = canonicalActionCode(action.code);
|
|
4568
4379
|
const startedAt = Date.now();
|
|
4569
4380
|
const label = actionLabel(action);
|
|
4570
4381
|
log(`${label} start`);
|
|
@@ -4604,12 +4415,17 @@ async function runActions(cfg, actions) {
|
|
|
4604
4415
|
partnerIgnorePaths,
|
|
4605
4416
|
ws.sandboxId
|
|
4606
4417
|
);
|
|
4418
|
+
const extras = workspaceFolders(ws).slice(1).map((folder) =>
|
|
4419
|
+
applyAccessPolicy(folder, partnerIgnorePaths, ws.sandboxId)
|
|
4420
|
+
);
|
|
4607
4421
|
log(
|
|
4608
|
-
`access policy synced for ${ws.
|
|
4422
|
+
`access policy synced for ${workspaceFolders(ws).join(", ")} (${access.ignorePaths.length} ignore rules)`
|
|
4609
4423
|
);
|
|
4610
4424
|
result = {
|
|
4611
4425
|
folderPath: path.resolve(ws.folderPath),
|
|
4426
|
+
extraFolders: workspaceFolders(ws).slice(1),
|
|
4612
4427
|
ignorePaths: access.ignorePaths,
|
|
4428
|
+
extraIgnorePaths: extras.map((row) => row.ignorePaths),
|
|
4613
4429
|
syncedAt: new Date().toISOString(),
|
|
4614
4430
|
};
|
|
4615
4431
|
}
|
|
@@ -4693,14 +4509,14 @@ async function runActions(cfg, actions) {
|
|
|
4693
4509
|
});
|
|
4694
4510
|
if (result.error) ok = false;
|
|
4695
4511
|
}
|
|
4696
|
-
} else if (action.code === "
|
|
4512
|
+
} else if (action.code === "start_share_app") {
|
|
4697
4513
|
const sandboxId = action.sandboxId || action.payload?.sandboxId;
|
|
4698
4514
|
const ws = (cfg.workspaces || []).find((w) => w.sandboxId === sandboxId);
|
|
4699
4515
|
if (!ws) {
|
|
4700
4516
|
ok = false;
|
|
4701
4517
|
result = { error: "No folder is attached for this sandbox" };
|
|
4702
4518
|
} else {
|
|
4703
|
-
result = await
|
|
4519
|
+
result = await startShareForApp(ws, cfg, action.payload || {}, {
|
|
4704
4520
|
actionId: action.id,
|
|
4705
4521
|
});
|
|
4706
4522
|
if (result.error) ok = false;
|
|
@@ -4805,7 +4621,6 @@ async function runActions(cfg, actions) {
|
|
|
4805
4621
|
sandboxId: sandboxId || ws?.sandboxId || null,
|
|
4806
4622
|
appUrl:
|
|
4807
4623
|
host.appUrl ||
|
|
4808
|
-
ws?.cloudflareUrl ||
|
|
4809
4624
|
ws?.appUrl ||
|
|
4810
4625
|
process.env.APP_URL ||
|
|
4811
4626
|
process.env.PUBLIC_URL ||
|
|
@@ -4817,7 +4632,7 @@ async function runActions(cfg, actions) {
|
|
|
4817
4632
|
host.origins.join(",") || "none"
|
|
4818
4633
|
}`
|
|
4819
4634
|
);
|
|
4820
|
-
} else if (action.code === "
|
|
4635
|
+
} else if (action.code === "configure_share") {
|
|
4821
4636
|
const sandboxId = action.sandboxId || action.payload?.sandboxId;
|
|
4822
4637
|
const ws =
|
|
4823
4638
|
(cfg.workspaces || []).find((w) => w.sandboxId === sandboxId) ||
|
|
@@ -4838,7 +4653,7 @@ async function runActions(cfg, actions) {
|
|
|
4838
4653
|
result = { error: "No folder is attached for this sandbox" };
|
|
4839
4654
|
warn(`${label} skipped: ${result.error}`);
|
|
4840
4655
|
} else {
|
|
4841
|
-
result = await
|
|
4656
|
+
result = await configureShareForWorkspace(ws, cfg, {
|
|
4842
4657
|
actionId: action.id,
|
|
4843
4658
|
});
|
|
4844
4659
|
}
|
|
@@ -4846,13 +4661,46 @@ async function runActions(cfg, actions) {
|
|
|
4846
4661
|
const sandboxId = String(
|
|
4847
4662
|
action.sandboxId || action.payload?.sandboxId || ""
|
|
4848
4663
|
);
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
(
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4664
|
+
if (action.payload?.keepWorkspace) {
|
|
4665
|
+
const ws = (cfg.workspaces || []).find((w) => w.sandboxId === sandboxId);
|
|
4666
|
+
if (!ws) {
|
|
4667
|
+
ok = false;
|
|
4668
|
+
result = { error: "No folder is attached for this sandbox" };
|
|
4669
|
+
} else {
|
|
4670
|
+
const removed = String(action.payload.folderPath || "");
|
|
4671
|
+
const remaining = Array.isArray(action.payload.extraFolders)
|
|
4672
|
+
? action.payload.extraFolders
|
|
4673
|
+
: workspaceFolders(ws).filter((folder) => !sameFolder(folder, removed));
|
|
4674
|
+
const primary =
|
|
4675
|
+
String(action.payload.folderPathRemaining || remaining[0] || ws.folderPath);
|
|
4676
|
+
ws.folderPath = primary;
|
|
4677
|
+
ws.extraFolders = remaining.filter((folder) => !sameFolder(folder, primary));
|
|
4678
|
+
ws.hostApps = (Array.isArray(ws.hostApps) ? ws.hostApps : []).filter(
|
|
4679
|
+
(app) =>
|
|
4680
|
+
!app ||
|
|
4681
|
+
app.role === "ai-server" ||
|
|
4682
|
+
!app.folderPath ||
|
|
4683
|
+
!sameFolder(app.folderPath, removed)
|
|
4684
|
+
);
|
|
4685
|
+
persistWorkspaceEntry(cfg, ws);
|
|
4686
|
+
result = {
|
|
4687
|
+
sandboxId,
|
|
4688
|
+
folderPath: ws.folderPath,
|
|
4689
|
+
extraFolders: ws.extraFolders,
|
|
4690
|
+
port: ws.port,
|
|
4691
|
+
hostApps: ws.hostApps,
|
|
4692
|
+
};
|
|
4693
|
+
log(`${label} removed extra folder ${removed}`);
|
|
4694
|
+
}
|
|
4695
|
+
} else {
|
|
4696
|
+
await forgetLaunch(sandboxId);
|
|
4697
|
+
cfg.workspaces = (cfg.workspaces || []).filter(
|
|
4698
|
+
(w) => w.sandboxId !== sandboxId
|
|
4699
|
+
);
|
|
4700
|
+
saveConfig(cfg);
|
|
4701
|
+
result = { removedSandboxId: sandboxId };
|
|
4702
|
+
log(`${label} removed workspace`);
|
|
4703
|
+
}
|
|
4856
4704
|
} else {
|
|
4857
4705
|
ok = false;
|
|
4858
4706
|
result = { error: `Unknown action ${action.code}` };
|
|
@@ -5643,7 +5491,6 @@ async function main() {
|
|
|
5643
5491
|
clearInterval(watchdogTimer);
|
|
5644
5492
|
watchdogTimer = null;
|
|
5645
5493
|
}
|
|
5646
|
-
stopAllCloudflare();
|
|
5647
5494
|
dropSocket(socket);
|
|
5648
5495
|
socket = null;
|
|
5649
5496
|
log("shutting down (other terminals stay open)");
|