@maintainer-pro/ai-bridge 0.1.16 → 0.1.19
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 +386 -74
- package/src/share-rewrite.mjs +529 -61
package/src/daemon.mjs
CHANGED
|
@@ -23,7 +23,13 @@ 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";
|
|
28
34
|
import {
|
|
29
35
|
canonicalActionCode,
|
|
@@ -80,6 +86,34 @@ function shortId(value) {
|
|
|
80
86
|
return text.length > 12 ? `${text.slice(0, 8)}…` : text;
|
|
81
87
|
}
|
|
82
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
|
+
|
|
83
117
|
function actionLabel(action) {
|
|
84
118
|
const sandbox = action.sandboxId || action.payload?.sandboxId;
|
|
85
119
|
const folder = action.payload?.folderPath;
|
|
@@ -364,7 +398,8 @@ async function detectCliProviders() {
|
|
|
364
398
|
}
|
|
365
399
|
|
|
366
400
|
async function resolveWorkspaceHostApps(ws, opts = {}) {
|
|
367
|
-
const
|
|
401
|
+
const folders = workspaceFolders(ws);
|
|
402
|
+
const folder = folders[0] || path.resolve(ws.folderPath || "");
|
|
368
403
|
const label = ws.sandboxName || shortId(ws.sandboxId) || "sandbox";
|
|
369
404
|
const desired = Array.isArray(opts.desired)
|
|
370
405
|
? opts.desired
|
|
@@ -379,10 +414,11 @@ async function resolveWorkspaceHostApps(ws, opts = {}) {
|
|
|
379
414
|
activity(
|
|
380
415
|
ws.sandboxId,
|
|
381
416
|
"info",
|
|
382
|
-
`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" : ""})`
|
|
383
418
|
);
|
|
384
419
|
const result = await cli.resolveHostApps({
|
|
385
420
|
workspaceDir: folder,
|
|
421
|
+
workspaceDirs: folders,
|
|
386
422
|
appName: ws.applicationName || ws.sandboxName,
|
|
387
423
|
preferredAiPort: Number(ws.port) || 3100,
|
|
388
424
|
desired: opts.ignoreDesired ? [] : desired,
|
|
@@ -391,6 +427,9 @@ async function resolveWorkspaceHostApps(ws, opts = {}) {
|
|
|
391
427
|
sandboxId: ws.sandboxId,
|
|
392
428
|
});
|
|
393
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
|
+
}
|
|
394
433
|
result.apps = result.apps.map((app) => {
|
|
395
434
|
const match = previous.find((row) => row && row.id === app.id);
|
|
396
435
|
return match ? { ...app, host: match.host === true || app.host === true } : app;
|
|
@@ -494,6 +533,14 @@ function projectFingerprint(folder) {
|
|
|
494
533
|
"next.config.js",
|
|
495
534
|
"next.config.mjs",
|
|
496
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",
|
|
497
544
|
]) {
|
|
498
545
|
const file = path.join(resolved, rel);
|
|
499
546
|
if (!fs.existsSync(file)) continue;
|
|
@@ -1440,28 +1487,71 @@ function proxySlugForApp(app) {
|
|
|
1440
1487
|
return id || "app";
|
|
1441
1488
|
}
|
|
1442
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
|
+
|
|
1443
1527
|
function applyAssignedProxy(ws, remote, cfg) {
|
|
1444
1528
|
const proxy =
|
|
1445
1529
|
remote?.proxy && typeof remote.proxy === "object" ? remote.proxy : null;
|
|
1446
1530
|
if (!proxy?.token) return;
|
|
1447
1531
|
const origin = String(proxy.origin || cfg?.adminUrl || "").replace(/\/$/, "");
|
|
1448
|
-
const
|
|
1532
|
+
const assigned =
|
|
1449
1533
|
proxy.slugs && typeof proxy.slugs === "object" ? proxy.slugs : {};
|
|
1534
|
+
const unique = uniqueProxySlugs(appsFrom(ws, remote));
|
|
1535
|
+
/** @type {Record<string, string>} */
|
|
1536
|
+
const slugs = {};
|
|
1450
1537
|
/** @type {Record<string, string>} */
|
|
1451
1538
|
const urls = {};
|
|
1452
|
-
const apps =
|
|
1453
|
-
? ws.hostApps
|
|
1454
|
-
: Array.isArray(remote.hostApps)
|
|
1455
|
-
? remote.hostApps
|
|
1456
|
-
: [];
|
|
1539
|
+
const apps = appsFrom(ws, remote);
|
|
1457
1540
|
for (const app of apps) {
|
|
1458
|
-
const slug =
|
|
1541
|
+
const slug = assigned[app.id] || unique[app.id] || proxySlugForApp(app);
|
|
1459
1542
|
if (!slug || !origin) continue;
|
|
1543
|
+
slugs[app.id] = slug;
|
|
1460
1544
|
urls[app.id] = `${origin}/p/${proxy.token}/${slug}`;
|
|
1461
1545
|
}
|
|
1462
1546
|
ws.proxy = { origin, token: String(proxy.token), slugs, urls };
|
|
1463
1547
|
}
|
|
1464
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
|
+
|
|
1465
1555
|
function proxyUrlForApp(ws, app) {
|
|
1466
1556
|
if (!usesBridgeProxy(app)) return "";
|
|
1467
1557
|
const urls =
|
|
@@ -1478,15 +1568,17 @@ function proxyUrlForApp(ws, app) {
|
|
|
1478
1568
|
return `${origin}/p/${token}/${slug}`;
|
|
1479
1569
|
}
|
|
1480
1570
|
|
|
1481
|
-
function jobsFromHostApps(ws) {
|
|
1571
|
+
function jobsFromHostApps(ws, filter = {}) {
|
|
1482
1572
|
const apps = Array.isArray(ws.hostApps) ? ws.hostApps : [];
|
|
1483
1573
|
const hostId = hostAppOf(ws)?.id;
|
|
1484
1574
|
return apps
|
|
1485
1575
|
.filter((app) => app && app.role !== "ai-server")
|
|
1576
|
+
.filter((app) => (filter.appId ? app.id === filter.appId : true))
|
|
1486
1577
|
.map((app) => {
|
|
1487
1578
|
const port = Number(app.port) || 3000;
|
|
1488
1579
|
const command = String(app.startCommand || "").trim();
|
|
1489
|
-
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);
|
|
1490
1582
|
return {
|
|
1491
1583
|
role: isBackendApp(app)
|
|
1492
1584
|
? "backend"
|
|
@@ -1500,6 +1592,7 @@ function jobsFromHostApps(ws) {
|
|
|
1500
1592
|
port,
|
|
1501
1593
|
probeUrl: `http://127.0.0.1:${port}`,
|
|
1502
1594
|
appId: app.id,
|
|
1595
|
+
folder,
|
|
1503
1596
|
};
|
|
1504
1597
|
});
|
|
1505
1598
|
}
|
|
@@ -2021,7 +2114,7 @@ const embeddedChat = new Map();
|
|
|
2021
2114
|
const embeddedChatStarting = new Map();
|
|
2022
2115
|
|
|
2023
2116
|
const PROXY_CHUNK_BYTES = 256 * 1024;
|
|
2024
|
-
/** @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>} */
|
|
2025
2118
|
const proxyHttpReqs = new Map();
|
|
2026
2119
|
/** Reuse sockets to the local app — Next/Vite fetch many files per page. */
|
|
2027
2120
|
const proxyKeepAliveAgent = new http.Agent({
|
|
@@ -2032,6 +2125,8 @@ const proxyKeepAliveAgent = new http.Agent({
|
|
|
2032
2125
|
});
|
|
2033
2126
|
/** @type {Map<string, WebSocket>} */
|
|
2034
2127
|
const proxyLocalSockets = new Map();
|
|
2128
|
+
/** @type {Map<string, Record<string, string>>} */
|
|
2129
|
+
const proxyWsPortUrls = new Map();
|
|
2035
2130
|
/** @type {Record<string, unknown> | null} */
|
|
2036
2131
|
let bridgeCfg = null;
|
|
2037
2132
|
|
|
@@ -2145,6 +2240,9 @@ function shareProxyContext(msg, ws, appId) {
|
|
|
2145
2240
|
).replace(/\/$/, "");
|
|
2146
2241
|
const acceptEncoding =
|
|
2147
2242
|
typeof msg.acceptEncoding === "string" ? msg.acceptEncoding : "";
|
|
2243
|
+
const fromWs = sharePortUrls(ws);
|
|
2244
|
+
const fromMsg =
|
|
2245
|
+
msg?.portUrls && typeof msg.portUrls === "object" ? msg.portUrls : {};
|
|
2148
2246
|
return {
|
|
2149
2247
|
path: safeProxyPath(msg.path),
|
|
2150
2248
|
slug,
|
|
@@ -2152,6 +2250,7 @@ function shareProxyContext(msg, ws, appId) {
|
|
|
2152
2250
|
aiPublicBase,
|
|
2153
2251
|
acceptEncoding,
|
|
2154
2252
|
port: localPortForProxy(ws, appId),
|
|
2253
|
+
portUrls: { ...fromWs, ...fromMsg },
|
|
2155
2254
|
};
|
|
2156
2255
|
}
|
|
2157
2256
|
|
|
@@ -2165,6 +2264,43 @@ function sendProcessedProxyHttp(id, ctx, status, headers, body) {
|
|
|
2165
2264
|
replyProxyHttp(id, processed.status, processed.headers, processed.body);
|
|
2166
2265
|
}
|
|
2167
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
|
+
|
|
2168
2304
|
function bridgeEmbedConfigJs(ws) {
|
|
2169
2305
|
const store = ws?.store && typeof ws.store === "object" ? ws.store : {};
|
|
2170
2306
|
const aiApp = { id: "ai-server", role: "ai-server" };
|
|
@@ -2191,19 +2327,25 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2191
2327
|
const reqPath = safeProxyPath(msg.path);
|
|
2192
2328
|
const pathname = reqPath.split("?")[0] || "/";
|
|
2193
2329
|
const ctx = shareProxyContext(msg, ws, "ai-server");
|
|
2330
|
+
if (replyShareInterceptor(id, ctx, reqPath)) return;
|
|
2194
2331
|
const prepared = prepareShareHttpRequest(
|
|
2195
2332
|
proxyReqHeaders(msg.headers),
|
|
2196
2333
|
ctx.publicBase,
|
|
2197
2334
|
ctx.acceptEncoding,
|
|
2198
|
-
reqPath
|
|
2335
|
+
reqPath,
|
|
2336
|
+
ctx.portUrls
|
|
2199
2337
|
);
|
|
2200
2338
|
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2201
2339
|
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
2202
2340
|
const headers = prepared.headers;
|
|
2203
|
-
const body =
|
|
2341
|
+
const body = rewriteShareRequestBody(
|
|
2204
2342
|
typeof msg.body === "string" && msg.body
|
|
2205
2343
|
? Buffer.from(msg.body, "base64")
|
|
2206
|
-
: Buffer.alloc(0)
|
|
2344
|
+
: Buffer.alloc(0),
|
|
2345
|
+
headers,
|
|
2346
|
+
ctx.portUrls,
|
|
2347
|
+
ctx.publicBase
|
|
2348
|
+
);
|
|
2207
2349
|
|
|
2208
2350
|
if (pathname === "/ai-ui.iife.js") {
|
|
2209
2351
|
const file = findIife();
|
|
@@ -2295,11 +2437,7 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2295
2437
|
}
|
|
2296
2438
|
const existing = proxyHttpReqs.get(id);
|
|
2297
2439
|
if (existing) {
|
|
2298
|
-
|
|
2299
|
-
existing.destroy();
|
|
2300
|
-
} catch {
|
|
2301
|
-
/* ignore */
|
|
2302
|
-
}
|
|
2440
|
+
destroyProxyHttpReq(existing);
|
|
2303
2441
|
proxyHttpReqs.delete(id);
|
|
2304
2442
|
}
|
|
2305
2443
|
const port = localPortForProxy(ws, appId);
|
|
@@ -2315,11 +2453,13 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2315
2453
|
const path = safeProxyPath(msg.path);
|
|
2316
2454
|
const ctx = shareProxyContext(msg, ws, appId);
|
|
2317
2455
|
ctx.port = port;
|
|
2456
|
+
if (replyShareInterceptor(id, ctx, path)) return;
|
|
2318
2457
|
const prepared = prepareShareHttpRequest(
|
|
2319
2458
|
proxyReqHeaders(msg.headers),
|
|
2320
2459
|
ctx.publicBase,
|
|
2321
2460
|
ctx.acceptEncoding,
|
|
2322
|
-
path
|
|
2461
|
+
path,
|
|
2462
|
+
ctx.portUrls
|
|
2323
2463
|
);
|
|
2324
2464
|
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2325
2465
|
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
@@ -2467,20 +2607,62 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2467
2607
|
error: err instanceof Error ? err.message : String(err),
|
|
2468
2608
|
});
|
|
2469
2609
|
});
|
|
2470
|
-
proxyHttpReqs.set(id,
|
|
2471
|
-
|
|
2472
|
-
|
|
2610
|
+
proxyHttpReqs.set(id, {
|
|
2611
|
+
req,
|
|
2612
|
+
rewrite:
|
|
2613
|
+
shouldRewriteBody(headers) &&
|
|
2614
|
+
ctx.portUrls &&
|
|
2615
|
+
Object.keys(ctx.portUrls).length > 0,
|
|
2616
|
+
chunks: [],
|
|
2617
|
+
headers,
|
|
2618
|
+
portUrls: ctx.portUrls,
|
|
2619
|
+
publicBase: ctx.publicBase,
|
|
2620
|
+
});
|
|
2621
|
+
const entry = proxyHttpReqs.get(id);
|
|
2622
|
+
if (entry.rewrite) {
|
|
2623
|
+
if (initialBody?.length) entry.chunks.push(initialBody);
|
|
2624
|
+
if (endAfter) {
|
|
2625
|
+
const body = rewriteShareRequestBody(
|
|
2626
|
+
Buffer.concat(entry.chunks),
|
|
2627
|
+
headers,
|
|
2628
|
+
ctx.portUrls,
|
|
2629
|
+
ctx.publicBase
|
|
2630
|
+
);
|
|
2631
|
+
if (body?.length) req.write(body);
|
|
2632
|
+
req.end();
|
|
2633
|
+
}
|
|
2634
|
+
} else {
|
|
2635
|
+
if (initialBody?.length) req.write(initialBody);
|
|
2636
|
+
if (endAfter) req.end();
|
|
2637
|
+
}
|
|
2473
2638
|
};
|
|
2474
2639
|
connect();
|
|
2475
2640
|
}
|
|
2476
2641
|
|
|
2477
2642
|
function handleProxyHttpBodyFromAdmin(msg) {
|
|
2478
2643
|
const id = typeof msg.id === "string" ? msg.id : "";
|
|
2479
|
-
const
|
|
2480
|
-
if (!
|
|
2481
|
-
|
|
2482
|
-
|
|
2644
|
+
const entry = proxyHttpReqs.get(id);
|
|
2645
|
+
if (!entry) return;
|
|
2646
|
+
const req = entry.req || entry;
|
|
2647
|
+
const chunk =
|
|
2648
|
+
typeof msg.data === "string" && msg.data
|
|
2649
|
+
? Buffer.from(msg.data, "base64")
|
|
2650
|
+
: null;
|
|
2651
|
+
if (entry.rewrite) {
|
|
2652
|
+
if (chunk?.length) entry.chunks.push(chunk);
|
|
2653
|
+
if (msg.eof === true) {
|
|
2654
|
+
const body = rewriteShareRequestBody(
|
|
2655
|
+
Buffer.concat(entry.chunks || []),
|
|
2656
|
+
entry.headers,
|
|
2657
|
+
entry.portUrls,
|
|
2658
|
+
entry.publicBase
|
|
2659
|
+
);
|
|
2660
|
+
if (body?.length) req.write(body);
|
|
2661
|
+
req.end();
|
|
2662
|
+
}
|
|
2663
|
+
return;
|
|
2483
2664
|
}
|
|
2665
|
+
if (chunk?.length) req.write(chunk);
|
|
2484
2666
|
if (msg.eof === true) req.end();
|
|
2485
2667
|
}
|
|
2486
2668
|
|
|
@@ -2509,10 +2691,14 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2509
2691
|
socket.addEventListener("message", (event) => {
|
|
2510
2692
|
try {
|
|
2511
2693
|
if (typeof event.data === "string") {
|
|
2694
|
+
const portUrls = proxyWsPortUrls.get(id);
|
|
2695
|
+
const data = portUrls
|
|
2696
|
+
? rewriteMappedLocalUrls(event.data, portUrls)
|
|
2697
|
+
: event.data;
|
|
2512
2698
|
bridgeSend({
|
|
2513
2699
|
type: "proxy.ws.frame",
|
|
2514
2700
|
id,
|
|
2515
|
-
data
|
|
2701
|
+
data,
|
|
2516
2702
|
binary: false,
|
|
2517
2703
|
});
|
|
2518
2704
|
return;
|
|
@@ -2532,6 +2718,7 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2532
2718
|
});
|
|
2533
2719
|
socket.addEventListener("close", (event) => {
|
|
2534
2720
|
proxyLocalSockets.delete(id);
|
|
2721
|
+
proxyWsPortUrls.delete(id);
|
|
2535
2722
|
bridgeSend({
|
|
2536
2723
|
type: "proxy.ws.close",
|
|
2537
2724
|
id,
|
|
@@ -2616,6 +2803,9 @@ function handleProxyWsOpenFromAdmin(msg) {
|
|
|
2616
2803
|
}
|
|
2617
2804
|
const path = safeProxyPath(msg.path);
|
|
2618
2805
|
const protocols = wsProtocolsFromMsg(msg);
|
|
2806
|
+
const fromMsg =
|
|
2807
|
+
msg?.portUrls && typeof msg.portUrls === "object" ? msg.portUrls : {};
|
|
2808
|
+
proxyWsPortUrls.set(id, { ...sharePortUrls(ws), ...fromMsg });
|
|
2619
2809
|
if (isAiServerAppId(appId, ws)) {
|
|
2620
2810
|
void (async () => {
|
|
2621
2811
|
const embedded = await ensureEmbeddedChat(ws);
|
|
@@ -2644,7 +2834,9 @@ function handleProxyWsFrameFromAdmin(msg) {
|
|
|
2644
2834
|
if (msg.binary === true) {
|
|
2645
2835
|
socket.send(Buffer.from(String(msg.data || ""), "base64"));
|
|
2646
2836
|
} else {
|
|
2647
|
-
|
|
2837
|
+
const portUrls = proxyWsPortUrls.get(id);
|
|
2838
|
+
const data = String(msg.data || "");
|
|
2839
|
+
socket.send(portUrls ? rewriteMappedLocalUrls(data, portUrls) : data);
|
|
2648
2840
|
}
|
|
2649
2841
|
} catch {
|
|
2650
2842
|
/* ignore */
|
|
@@ -2656,6 +2848,7 @@ function handleProxyWsCloseFromAdmin(msg) {
|
|
|
2656
2848
|
const socket = proxyLocalSockets.get(id);
|
|
2657
2849
|
if (!socket) return;
|
|
2658
2850
|
proxyLocalSockets.delete(id);
|
|
2851
|
+
proxyWsPortUrls.delete(id);
|
|
2659
2852
|
try {
|
|
2660
2853
|
socket.close(
|
|
2661
2854
|
typeof msg.code === "number" ? msg.code : 1000,
|
|
@@ -3089,6 +3282,10 @@ async function openInNewTerminal(opts) {
|
|
|
3089
3282
|
}
|
|
3090
3283
|
|
|
3091
3284
|
function commandWithPort(job, scripts, port) {
|
|
3285
|
+
const command = String(job.command || "");
|
|
3286
|
+
if (!/^(npm|pnpm|yarn|npx)\s/i.test(command)) {
|
|
3287
|
+
return command;
|
|
3288
|
+
}
|
|
3092
3289
|
const raw = String(scripts?.[job.script] || "");
|
|
3093
3290
|
if (
|
|
3094
3291
|
job.role === "ui" ||
|
|
@@ -3096,9 +3293,9 @@ function commandWithPort(job, scripts, port) {
|
|
|
3096
3293
|
isUiCommand(raw) ||
|
|
3097
3294
|
/--port\b/i.test(raw)
|
|
3098
3295
|
) {
|
|
3099
|
-
return `${
|
|
3296
|
+
return `${command} -- --port ${port}`;
|
|
3100
3297
|
}
|
|
3101
|
-
return
|
|
3298
|
+
return command;
|
|
3102
3299
|
}
|
|
3103
3300
|
|
|
3104
3301
|
async function stopEmbeddedChat(sandboxId) {
|
|
@@ -3551,9 +3748,8 @@ async function startSingleApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
3551
3748
|
await ensureHostProcesses(ws, {
|
|
3552
3749
|
reserved,
|
|
3553
3750
|
cfg,
|
|
3554
|
-
onlyRoles: [app.role === "custom" ? "app" : app.role],
|
|
3555
3751
|
force: true,
|
|
3556
|
-
plannedJobs: jobsFromHostApps(ws),
|
|
3752
|
+
plannedJobs: jobsFromHostApps(ws, { appId: app.id }),
|
|
3557
3753
|
});
|
|
3558
3754
|
}
|
|
3559
3755
|
const status = await reconcileWorkspacePresence(ws, cfg, {
|
|
@@ -3665,32 +3861,21 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3665
3861
|
const reserved = opts.reserved instanceof Set ? opts.reserved : new Set();
|
|
3666
3862
|
const cfg = opts.cfg || null;
|
|
3667
3863
|
const folder = path.resolve(ws.folderPath);
|
|
3668
|
-
const scripts = readPackageJson(folder)?.scripts || {};
|
|
3669
3864
|
const jobs = Array.isArray(opts.plannedJobs)
|
|
3670
3865
|
? opts.plannedJobs
|
|
3671
|
-
:
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3866
|
+
: jobsFromHostApps(ws).length
|
|
3867
|
+
? jobsFromHostApps(ws)
|
|
3868
|
+
: planHostJobs(
|
|
3869
|
+
folder,
|
|
3870
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
3871
|
+
ws.projectInfo
|
|
3872
|
+
);
|
|
3676
3873
|
const onlyRoles = Array.isArray(opts.onlyRoles) ? new Set(opts.onlyRoles) : null;
|
|
3677
3874
|
const extraEnv = opts.extraEnv && typeof opts.extraEnv === "object" ? opts.extraEnv : {};
|
|
3678
3875
|
const started = [];
|
|
3679
3876
|
let persisted = false;
|
|
3680
3877
|
const label = ws.sandboxName || "this sandbox";
|
|
3681
3878
|
|
|
3682
|
-
if (jobs.length > 0 && !fs.existsSync(folder)) {
|
|
3683
|
-
recordProcessProblem({
|
|
3684
|
-
sandboxId: ws.sandboxId,
|
|
3685
|
-
code: "host_process_launch",
|
|
3686
|
-
role: "app",
|
|
3687
|
-
title: `Could not start the app (${label})`,
|
|
3688
|
-
message: `The project folder is missing: ${folder}`,
|
|
3689
|
-
resolution: "Attach the folder again from Local setup.",
|
|
3690
|
-
});
|
|
3691
|
-
return started;
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
3879
|
log(
|
|
3695
3880
|
`start hosts ${label}: ${
|
|
3696
3881
|
jobs.length
|
|
@@ -3704,6 +3889,19 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3704
3889
|
|
|
3705
3890
|
for (const job of jobs) {
|
|
3706
3891
|
if (onlyRoles && !onlyRoles.has(job.role)) continue;
|
|
3892
|
+
const jobFolder = path.resolve(job.folder || folder);
|
|
3893
|
+
if (!fs.existsSync(jobFolder)) {
|
|
3894
|
+
recordProcessProblem({
|
|
3895
|
+
sandboxId: ws.sandboxId,
|
|
3896
|
+
code: "host_process_launch",
|
|
3897
|
+
role: job.role,
|
|
3898
|
+
title: `Could not start the ${processRoleLabel(job.role)} (${label})`,
|
|
3899
|
+
message: `The project folder is missing: ${jobFolder}`,
|
|
3900
|
+
resolution: "Attach the folder again from Local setup.",
|
|
3901
|
+
});
|
|
3902
|
+
continue;
|
|
3903
|
+
}
|
|
3904
|
+
const scripts = readPackageJson(jobFolder)?.scripts || {};
|
|
3707
3905
|
const preferred = Number(job.port || job.preferredPort) || 3000;
|
|
3708
3906
|
const probe = job.port
|
|
3709
3907
|
? `http://127.0.0.1:${job.port}`
|
|
@@ -3715,7 +3913,7 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3715
3913
|
log(`${job.role} already running at ${job.probeUrl || probe}`);
|
|
3716
3914
|
continue;
|
|
3717
3915
|
}
|
|
3718
|
-
const launchKey = `${ws.sandboxId}:${
|
|
3916
|
+
const launchKey = `${ws.sandboxId}:${jobFolder}:${job.command || job.script}:${preferred}`;
|
|
3719
3917
|
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
3720
3918
|
reserved.add(preferred);
|
|
3721
3919
|
log(`start ${job.role} skip ${label}: launched recently`);
|
|
@@ -3747,13 +3945,13 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3747
3945
|
}
|
|
3748
3946
|
|
|
3749
3947
|
const needsInstall =
|
|
3750
|
-
fs.existsSync(path.join(
|
|
3751
|
-
!fs.existsSync(path.join(
|
|
3948
|
+
fs.existsSync(path.join(jobFolder, "package.json")) &&
|
|
3949
|
+
!fs.existsSync(path.join(jobFolder, "node_modules"));
|
|
3752
3950
|
const run = commandWithPort(job, scripts, port);
|
|
3753
3951
|
const command = needsInstall ? `npm install && ${run}` : run;
|
|
3754
3952
|
const opened = await openInNewTerminal({
|
|
3755
3953
|
title: `MP-${job.role}-${port}`,
|
|
3756
|
-
folder,
|
|
3954
|
+
folder: jobFolder,
|
|
3757
3955
|
command,
|
|
3758
3956
|
env: { PORT: String(port), ...extraEnv },
|
|
3759
3957
|
launchKey,
|
|
@@ -3812,7 +4010,7 @@ async function inspectHostJobs(ws) {
|
|
|
3812
4010
|
const preferred = Number(job.preferredPort) || 3000;
|
|
3813
4011
|
const probe = job.probeUrl || `http://127.0.0.1:${preferred}`;
|
|
3814
4012
|
const up = await probeLoopbackUrl(probe);
|
|
3815
|
-
const launchKey = `${ws.sandboxId}:${folder}:${job.script}`;
|
|
4013
|
+
const launchKey = `${ws.sandboxId}:${job.folder || folder}:${job.command || job.script}:${preferred}`;
|
|
3816
4014
|
const starting = recentlyLaunched(launchKey);
|
|
3817
4015
|
hosts.push({
|
|
3818
4016
|
role: job.role,
|
|
@@ -3904,6 +4102,79 @@ async function setupWorkspace(cfg, action) {
|
|
|
3904
4102
|
config.sandbox?.name ||
|
|
3905
4103
|
"Maintainer Pro App";
|
|
3906
4104
|
|
|
4105
|
+
const partnerIgnorePaths = Array.isArray(config.aiIgnorePaths)
|
|
4106
|
+
? config.aiIgnorePaths
|
|
4107
|
+
: [];
|
|
4108
|
+
const addFolder = Boolean(action.payload?.addFolder);
|
|
4109
|
+
cfg.workspaces = cfg.workspaces || [];
|
|
4110
|
+
const existingIdx = cfg.workspaces.findIndex((w) => w.sandboxId === sandboxId);
|
|
4111
|
+
const existingWs = existingIdx >= 0 ? cfg.workspaces[existingIdx] : null;
|
|
4112
|
+
|
|
4113
|
+
if (addFolder && existingWs?.folderPath) {
|
|
4114
|
+
const extra = Array.isArray(existingWs.extraFolders)
|
|
4115
|
+
? [...existingWs.extraFolders]
|
|
4116
|
+
: [];
|
|
4117
|
+
if (
|
|
4118
|
+
!sameFolder(existingWs.folderPath, resolved) &&
|
|
4119
|
+
!extra.some((folder) => sameFolder(folder, resolved))
|
|
4120
|
+
) {
|
|
4121
|
+
extra.push(resolved);
|
|
4122
|
+
}
|
|
4123
|
+
existingWs.extraFolders = extra;
|
|
4124
|
+
const access = applyAccessPolicy(resolved, partnerIgnorePaths, sandboxId);
|
|
4125
|
+
persistWorkspaceEntry(cfg, existingWs);
|
|
4126
|
+
const ports = await resolveWorkspaceHostApps(existingWs, {
|
|
4127
|
+
cfg,
|
|
4128
|
+
allowAi: false,
|
|
4129
|
+
ignoreDesired: true,
|
|
4130
|
+
unionDetected: true,
|
|
4131
|
+
force: true,
|
|
4132
|
+
});
|
|
4133
|
+
const needsReview =
|
|
4134
|
+
Boolean(ports.confused) ||
|
|
4135
|
+
!(ports.apps || []).some((app) => app && app.role !== "ai-server");
|
|
4136
|
+
if (needsReview) {
|
|
4137
|
+
activity(
|
|
4138
|
+
sandboxId,
|
|
4139
|
+
"warn",
|
|
4140
|
+
"Review the suggested apps and ports in Maintainer Pro before Start Apps."
|
|
4141
|
+
);
|
|
4142
|
+
}
|
|
4143
|
+
await inspectHostJobs(existingWs);
|
|
4144
|
+
const host = workspaceHostReport(existingWs);
|
|
4145
|
+
const aiServerUp = await isChatServerOnPort(existingWs.port);
|
|
4146
|
+
log(`setup added folder ${resolved} sandbox=${shortId(sandboxId)}`);
|
|
4147
|
+
return {
|
|
4148
|
+
sandboxId,
|
|
4149
|
+
folderPath: existingWs.folderPath,
|
|
4150
|
+
extraFolders: extra,
|
|
4151
|
+
addFolder: true,
|
|
4152
|
+
addedFolder: resolved,
|
|
4153
|
+
port: existingWs.port,
|
|
4154
|
+
appUrl: host.appUrl || existingWs.appUrl || null,
|
|
4155
|
+
origins: host.origins,
|
|
4156
|
+
wroteEnv: false,
|
|
4157
|
+
clientKind: existingWs.clientKind || "skip",
|
|
4158
|
+
clientFiles: [],
|
|
4159
|
+
clientNotes: [`Added folder ${resolved}`],
|
|
4160
|
+
aiServerUp,
|
|
4161
|
+
startedHosts: [],
|
|
4162
|
+
openUrl: existingWs.appUrl || `http://localhost:${existingWs.port}`,
|
|
4163
|
+
processIssues: issuesForSandbox(sandboxId).map(
|
|
4164
|
+
({ role: _role, ...issue }) => issue
|
|
4165
|
+
),
|
|
4166
|
+
warning: needsReview
|
|
4167
|
+
? "Folder attached. Review the suggested apps and ports in Maintainer Pro, then Start Apps."
|
|
4168
|
+
: "Folder attached. Review apps if needed, then use Start Apps.",
|
|
4169
|
+
waitingForStart: !aiServerUp,
|
|
4170
|
+
needsReview,
|
|
4171
|
+
hostApps: ports.apps || existingWs.hostApps || [],
|
|
4172
|
+
reasons: ports.reasons || [],
|
|
4173
|
+
projectInfo: existingWs.projectInfo || null,
|
|
4174
|
+
ignorePaths: access.ignorePaths,
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
|
|
3907
4178
|
const client = configureClient({
|
|
3908
4179
|
dir: resolved,
|
|
3909
4180
|
port,
|
|
@@ -3916,23 +4187,22 @@ async function setupWorkspace(cfg, action) {
|
|
|
3916
4187
|
const corsOrigin = client.corsOrigin || aiOrigin;
|
|
3917
4188
|
const appUrl = client.appUrl || corsOrigin;
|
|
3918
4189
|
|
|
3919
|
-
const partnerIgnorePaths = Array.isArray(config.aiIgnorePaths)
|
|
3920
|
-
? config.aiIgnorePaths
|
|
3921
|
-
: [];
|
|
3922
4190
|
const access = applyAccessPolicy(resolved, partnerIgnorePaths, sandboxId);
|
|
3923
4191
|
|
|
3924
|
-
cfg.workspaces = cfg.workspaces || [];
|
|
3925
|
-
const existing = cfg.workspaces.findIndex((w) => w.sandboxId === sandboxId);
|
|
3926
4192
|
const entry = {
|
|
3927
4193
|
sandboxId,
|
|
3928
4194
|
folderPath: resolved,
|
|
4195
|
+
extraFolders: Array.isArray(existingWs?.extraFolders)
|
|
4196
|
+
? existingWs.extraFolders.filter((folder) => !sameFolder(folder, resolved))
|
|
4197
|
+
: [],
|
|
3929
4198
|
port,
|
|
3930
4199
|
sandboxName: config.sandbox?.name,
|
|
3931
4200
|
applicationName: config.sandbox?.applicationName,
|
|
3932
4201
|
clientKind: client.kind,
|
|
3933
4202
|
appUrl,
|
|
3934
4203
|
sameOrigin: Boolean(client.sameOrigin),
|
|
3935
|
-
appsRequested: false,
|
|
4204
|
+
appsRequested: existingWs?.appsRequested || false,
|
|
4205
|
+
hostApps: existingWs?.hostApps,
|
|
3936
4206
|
store: {
|
|
3937
4207
|
serverKey: String(config.env?.MAINTAINER_PRO_API_KEY || "").trim(),
|
|
3938
4208
|
clientKey: String(
|
|
@@ -3942,7 +4212,7 @@ async function setupWorkspace(cfg, action) {
|
|
|
3942
4212
|
).trim(),
|
|
3943
4213
|
},
|
|
3944
4214
|
};
|
|
3945
|
-
if (
|
|
4215
|
+
if (existingIdx >= 0) cfg.workspaces[existingIdx] = { ...existingWs, ...entry };
|
|
3946
4216
|
else cfg.workspaces.push(entry);
|
|
3947
4217
|
saveConfig(cfg);
|
|
3948
4218
|
|
|
@@ -3994,7 +4264,9 @@ async function setupWorkspace(cfg, action) {
|
|
|
3994
4264
|
const host = workspaceHostReport(entry);
|
|
3995
4265
|
return {
|
|
3996
4266
|
sandboxId,
|
|
3997
|
-
folderPath:
|
|
4267
|
+
folderPath: entry.folderPath,
|
|
4268
|
+
extraFolders: entry.extraFolders || [],
|
|
4269
|
+
addFolder: false,
|
|
3998
4270
|
port: entry.port,
|
|
3999
4271
|
appUrl: host.appUrl || entry.appUrl || appUrl,
|
|
4000
4272
|
origins: host.origins.length
|
|
@@ -4019,7 +4291,8 @@ async function setupWorkspace(cfg, action) {
|
|
|
4019
4291
|
}
|
|
4020
4292
|
|
|
4021
4293
|
async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
4022
|
-
const
|
|
4294
|
+
const folders = workspaceFolders(ws);
|
|
4295
|
+
const folder = folders[0] || path.resolve(ws.folderPath || "");
|
|
4023
4296
|
const label = ws.sandboxName || shortId(ws.sandboxId) || "sandbox";
|
|
4024
4297
|
activity(ws.sandboxId, "info", `Suggesting setup for ${label} from config files`);
|
|
4025
4298
|
try {
|
|
@@ -4031,6 +4304,7 @@ async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
|
4031
4304
|
}
|
|
4032
4305
|
const proposal = await cli.proposeHostAppsFromConfig({
|
|
4033
4306
|
workspaceDir: folder,
|
|
4307
|
+
workspaceDirs: folders,
|
|
4034
4308
|
appName: ws.applicationName || ws.sandboxName,
|
|
4035
4309
|
preferredAiPort: Number(ws.port) || 3100,
|
|
4036
4310
|
allowAi: opts.allowAi !== false,
|
|
@@ -4145,12 +4419,17 @@ async function runActions(cfg, actions) {
|
|
|
4145
4419
|
partnerIgnorePaths,
|
|
4146
4420
|
ws.sandboxId
|
|
4147
4421
|
);
|
|
4422
|
+
const extras = workspaceFolders(ws).slice(1).map((folder) =>
|
|
4423
|
+
applyAccessPolicy(folder, partnerIgnorePaths, ws.sandboxId)
|
|
4424
|
+
);
|
|
4148
4425
|
log(
|
|
4149
|
-
`access policy synced for ${ws.
|
|
4426
|
+
`access policy synced for ${workspaceFolders(ws).join(", ")} (${access.ignorePaths.length} ignore rules)`
|
|
4150
4427
|
);
|
|
4151
4428
|
result = {
|
|
4152
4429
|
folderPath: path.resolve(ws.folderPath),
|
|
4430
|
+
extraFolders: workspaceFolders(ws).slice(1),
|
|
4153
4431
|
ignorePaths: access.ignorePaths,
|
|
4432
|
+
extraIgnorePaths: extras.map((row) => row.ignorePaths),
|
|
4154
4433
|
syncedAt: new Date().toISOString(),
|
|
4155
4434
|
};
|
|
4156
4435
|
}
|
|
@@ -4386,13 +4665,46 @@ async function runActions(cfg, actions) {
|
|
|
4386
4665
|
const sandboxId = String(
|
|
4387
4666
|
action.sandboxId || action.payload?.sandboxId || ""
|
|
4388
4667
|
);
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
(
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4668
|
+
if (action.payload?.keepWorkspace) {
|
|
4669
|
+
const ws = (cfg.workspaces || []).find((w) => w.sandboxId === sandboxId);
|
|
4670
|
+
if (!ws) {
|
|
4671
|
+
ok = false;
|
|
4672
|
+
result = { error: "No folder is attached for this sandbox" };
|
|
4673
|
+
} else {
|
|
4674
|
+
const removed = String(action.payload.folderPath || "");
|
|
4675
|
+
const remaining = Array.isArray(action.payload.extraFolders)
|
|
4676
|
+
? action.payload.extraFolders
|
|
4677
|
+
: workspaceFolders(ws).filter((folder) => !sameFolder(folder, removed));
|
|
4678
|
+
const primary =
|
|
4679
|
+
String(action.payload.folderPathRemaining || remaining[0] || ws.folderPath);
|
|
4680
|
+
ws.folderPath = primary;
|
|
4681
|
+
ws.extraFolders = remaining.filter((folder) => !sameFolder(folder, primary));
|
|
4682
|
+
ws.hostApps = (Array.isArray(ws.hostApps) ? ws.hostApps : []).filter(
|
|
4683
|
+
(app) =>
|
|
4684
|
+
!app ||
|
|
4685
|
+
app.role === "ai-server" ||
|
|
4686
|
+
!app.folderPath ||
|
|
4687
|
+
!sameFolder(app.folderPath, removed)
|
|
4688
|
+
);
|
|
4689
|
+
persistWorkspaceEntry(cfg, ws);
|
|
4690
|
+
result = {
|
|
4691
|
+
sandboxId,
|
|
4692
|
+
folderPath: ws.folderPath,
|
|
4693
|
+
extraFolders: ws.extraFolders,
|
|
4694
|
+
port: ws.port,
|
|
4695
|
+
hostApps: ws.hostApps,
|
|
4696
|
+
};
|
|
4697
|
+
log(`${label} removed extra folder ${removed}`);
|
|
4698
|
+
}
|
|
4699
|
+
} else {
|
|
4700
|
+
await forgetLaunch(sandboxId);
|
|
4701
|
+
cfg.workspaces = (cfg.workspaces || []).filter(
|
|
4702
|
+
(w) => w.sandboxId !== sandboxId
|
|
4703
|
+
);
|
|
4704
|
+
saveConfig(cfg);
|
|
4705
|
+
result = { removedSandboxId: sandboxId };
|
|
4706
|
+
log(`${label} removed workspace`);
|
|
4707
|
+
}
|
|
4396
4708
|
} else {
|
|
4397
4709
|
ok = false;
|
|
4398
4710
|
result = { error: `Unknown action ${action.code}` };
|