@maintainer-pro/ai-bridge 0.1.16 → 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 +382 -74
- package/src/share-rewrite.mjs +404 -40
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,24 @@ 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
|
+
);
|
|
2207
2348
|
|
|
2208
2349
|
if (pathname === "/ai-ui.iife.js") {
|
|
2209
2350
|
const file = findIife();
|
|
@@ -2295,11 +2436,7 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2295
2436
|
}
|
|
2296
2437
|
const existing = proxyHttpReqs.get(id);
|
|
2297
2438
|
if (existing) {
|
|
2298
|
-
|
|
2299
|
-
existing.destroy();
|
|
2300
|
-
} catch {
|
|
2301
|
-
/* ignore */
|
|
2302
|
-
}
|
|
2439
|
+
destroyProxyHttpReq(existing);
|
|
2303
2440
|
proxyHttpReqs.delete(id);
|
|
2304
2441
|
}
|
|
2305
2442
|
const port = localPortForProxy(ws, appId);
|
|
@@ -2315,11 +2452,13 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2315
2452
|
const path = safeProxyPath(msg.path);
|
|
2316
2453
|
const ctx = shareProxyContext(msg, ws, appId);
|
|
2317
2454
|
ctx.port = port;
|
|
2455
|
+
if (replyShareInterceptor(id, ctx, path)) return;
|
|
2318
2456
|
const prepared = prepareShareHttpRequest(
|
|
2319
2457
|
proxyReqHeaders(msg.headers),
|
|
2320
2458
|
ctx.publicBase,
|
|
2321
2459
|
ctx.acceptEncoding,
|
|
2322
|
-
path
|
|
2460
|
+
path,
|
|
2461
|
+
ctx.portUrls
|
|
2323
2462
|
);
|
|
2324
2463
|
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2325
2464
|
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
@@ -2467,20 +2606,59 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2467
2606
|
error: err instanceof Error ? err.message : String(err),
|
|
2468
2607
|
});
|
|
2469
2608
|
});
|
|
2470
|
-
proxyHttpReqs.set(id,
|
|
2471
|
-
|
|
2472
|
-
|
|
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,
|
|
2618
|
+
});
|
|
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
|
+
}
|
|
2473
2635
|
};
|
|
2474
2636
|
connect();
|
|
2475
2637
|
}
|
|
2476
2638
|
|
|
2477
2639
|
function handleProxyHttpBodyFromAdmin(msg) {
|
|
2478
2640
|
const id = typeof msg.id === "string" ? msg.id : "";
|
|
2479
|
-
const
|
|
2480
|
-
if (!
|
|
2481
|
-
|
|
2482
|
-
|
|
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;
|
|
2483
2660
|
}
|
|
2661
|
+
if (chunk?.length) req.write(chunk);
|
|
2484
2662
|
if (msg.eof === true) req.end();
|
|
2485
2663
|
}
|
|
2486
2664
|
|
|
@@ -2509,10 +2687,14 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2509
2687
|
socket.addEventListener("message", (event) => {
|
|
2510
2688
|
try {
|
|
2511
2689
|
if (typeof event.data === "string") {
|
|
2690
|
+
const portUrls = proxyWsPortUrls.get(id);
|
|
2691
|
+
const data = portUrls
|
|
2692
|
+
? rewriteMappedLocalUrls(event.data, portUrls)
|
|
2693
|
+
: event.data;
|
|
2512
2694
|
bridgeSend({
|
|
2513
2695
|
type: "proxy.ws.frame",
|
|
2514
2696
|
id,
|
|
2515
|
-
data
|
|
2697
|
+
data,
|
|
2516
2698
|
binary: false,
|
|
2517
2699
|
});
|
|
2518
2700
|
return;
|
|
@@ -2532,6 +2714,7 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2532
2714
|
});
|
|
2533
2715
|
socket.addEventListener("close", (event) => {
|
|
2534
2716
|
proxyLocalSockets.delete(id);
|
|
2717
|
+
proxyWsPortUrls.delete(id);
|
|
2535
2718
|
bridgeSend({
|
|
2536
2719
|
type: "proxy.ws.close",
|
|
2537
2720
|
id,
|
|
@@ -2616,6 +2799,9 @@ function handleProxyWsOpenFromAdmin(msg) {
|
|
|
2616
2799
|
}
|
|
2617
2800
|
const path = safeProxyPath(msg.path);
|
|
2618
2801
|
const protocols = wsProtocolsFromMsg(msg);
|
|
2802
|
+
const fromMsg =
|
|
2803
|
+
msg?.portUrls && typeof msg.portUrls === "object" ? msg.portUrls : {};
|
|
2804
|
+
proxyWsPortUrls.set(id, { ...sharePortUrls(ws), ...fromMsg });
|
|
2619
2805
|
if (isAiServerAppId(appId, ws)) {
|
|
2620
2806
|
void (async () => {
|
|
2621
2807
|
const embedded = await ensureEmbeddedChat(ws);
|
|
@@ -2644,7 +2830,9 @@ function handleProxyWsFrameFromAdmin(msg) {
|
|
|
2644
2830
|
if (msg.binary === true) {
|
|
2645
2831
|
socket.send(Buffer.from(String(msg.data || ""), "base64"));
|
|
2646
2832
|
} else {
|
|
2647
|
-
|
|
2833
|
+
const portUrls = proxyWsPortUrls.get(id);
|
|
2834
|
+
const data = String(msg.data || "");
|
|
2835
|
+
socket.send(portUrls ? rewriteMappedLocalUrls(data, portUrls) : data);
|
|
2648
2836
|
}
|
|
2649
2837
|
} catch {
|
|
2650
2838
|
/* ignore */
|
|
@@ -2656,6 +2844,7 @@ function handleProxyWsCloseFromAdmin(msg) {
|
|
|
2656
2844
|
const socket = proxyLocalSockets.get(id);
|
|
2657
2845
|
if (!socket) return;
|
|
2658
2846
|
proxyLocalSockets.delete(id);
|
|
2847
|
+
proxyWsPortUrls.delete(id);
|
|
2659
2848
|
try {
|
|
2660
2849
|
socket.close(
|
|
2661
2850
|
typeof msg.code === "number" ? msg.code : 1000,
|
|
@@ -3089,6 +3278,10 @@ async function openInNewTerminal(opts) {
|
|
|
3089
3278
|
}
|
|
3090
3279
|
|
|
3091
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
|
+
}
|
|
3092
3285
|
const raw = String(scripts?.[job.script] || "");
|
|
3093
3286
|
if (
|
|
3094
3287
|
job.role === "ui" ||
|
|
@@ -3096,9 +3289,9 @@ function commandWithPort(job, scripts, port) {
|
|
|
3096
3289
|
isUiCommand(raw) ||
|
|
3097
3290
|
/--port\b/i.test(raw)
|
|
3098
3291
|
) {
|
|
3099
|
-
return `${
|
|
3292
|
+
return `${command} -- --port ${port}`;
|
|
3100
3293
|
}
|
|
3101
|
-
return
|
|
3294
|
+
return command;
|
|
3102
3295
|
}
|
|
3103
3296
|
|
|
3104
3297
|
async function stopEmbeddedChat(sandboxId) {
|
|
@@ -3551,9 +3744,8 @@ async function startSingleApp(ws, cfg, payload = {}, opts = {}) {
|
|
|
3551
3744
|
await ensureHostProcesses(ws, {
|
|
3552
3745
|
reserved,
|
|
3553
3746
|
cfg,
|
|
3554
|
-
onlyRoles: [app.role === "custom" ? "app" : app.role],
|
|
3555
3747
|
force: true,
|
|
3556
|
-
plannedJobs: jobsFromHostApps(ws),
|
|
3748
|
+
plannedJobs: jobsFromHostApps(ws, { appId: app.id }),
|
|
3557
3749
|
});
|
|
3558
3750
|
}
|
|
3559
3751
|
const status = await reconcileWorkspacePresence(ws, cfg, {
|
|
@@ -3665,32 +3857,21 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3665
3857
|
const reserved = opts.reserved instanceof Set ? opts.reserved : new Set();
|
|
3666
3858
|
const cfg = opts.cfg || null;
|
|
3667
3859
|
const folder = path.resolve(ws.folderPath);
|
|
3668
|
-
const scripts = readPackageJson(folder)?.scripts || {};
|
|
3669
3860
|
const jobs = Array.isArray(opts.plannedJobs)
|
|
3670
3861
|
? opts.plannedJobs
|
|
3671
|
-
:
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3862
|
+
: jobsFromHostApps(ws).length
|
|
3863
|
+
? jobsFromHostApps(ws)
|
|
3864
|
+
: planHostJobs(
|
|
3865
|
+
folder,
|
|
3866
|
+
ws.appUrl && isLocalAppUrl(ws.appUrl) ? ws.appUrl : null,
|
|
3867
|
+
ws.projectInfo
|
|
3868
|
+
);
|
|
3676
3869
|
const onlyRoles = Array.isArray(opts.onlyRoles) ? new Set(opts.onlyRoles) : null;
|
|
3677
3870
|
const extraEnv = opts.extraEnv && typeof opts.extraEnv === "object" ? opts.extraEnv : {};
|
|
3678
3871
|
const started = [];
|
|
3679
3872
|
let persisted = false;
|
|
3680
3873
|
const label = ws.sandboxName || "this sandbox";
|
|
3681
3874
|
|
|
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
3875
|
log(
|
|
3695
3876
|
`start hosts ${label}: ${
|
|
3696
3877
|
jobs.length
|
|
@@ -3704,6 +3885,19 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3704
3885
|
|
|
3705
3886
|
for (const job of jobs) {
|
|
3706
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 || {};
|
|
3707
3901
|
const preferred = Number(job.port || job.preferredPort) || 3000;
|
|
3708
3902
|
const probe = job.port
|
|
3709
3903
|
? `http://127.0.0.1:${job.port}`
|
|
@@ -3715,7 +3909,7 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3715
3909
|
log(`${job.role} already running at ${job.probeUrl || probe}`);
|
|
3716
3910
|
continue;
|
|
3717
3911
|
}
|
|
3718
|
-
const launchKey = `${ws.sandboxId}:${
|
|
3912
|
+
const launchKey = `${ws.sandboxId}:${jobFolder}:${job.command || job.script}:${preferred}`;
|
|
3719
3913
|
if (!opts.force && recentlyLaunched(launchKey)) {
|
|
3720
3914
|
reserved.add(preferred);
|
|
3721
3915
|
log(`start ${job.role} skip ${label}: launched recently`);
|
|
@@ -3747,13 +3941,13 @@ async function ensureHostProcesses(ws, opts = {}) {
|
|
|
3747
3941
|
}
|
|
3748
3942
|
|
|
3749
3943
|
const needsInstall =
|
|
3750
|
-
fs.existsSync(path.join(
|
|
3751
|
-
!fs.existsSync(path.join(
|
|
3944
|
+
fs.existsSync(path.join(jobFolder, "package.json")) &&
|
|
3945
|
+
!fs.existsSync(path.join(jobFolder, "node_modules"));
|
|
3752
3946
|
const run = commandWithPort(job, scripts, port);
|
|
3753
3947
|
const command = needsInstall ? `npm install && ${run}` : run;
|
|
3754
3948
|
const opened = await openInNewTerminal({
|
|
3755
3949
|
title: `MP-${job.role}-${port}`,
|
|
3756
|
-
folder,
|
|
3950
|
+
folder: jobFolder,
|
|
3757
3951
|
command,
|
|
3758
3952
|
env: { PORT: String(port), ...extraEnv },
|
|
3759
3953
|
launchKey,
|
|
@@ -3812,7 +4006,7 @@ async function inspectHostJobs(ws) {
|
|
|
3812
4006
|
const preferred = Number(job.preferredPort) || 3000;
|
|
3813
4007
|
const probe = job.probeUrl || `http://127.0.0.1:${preferred}`;
|
|
3814
4008
|
const up = await probeLoopbackUrl(probe);
|
|
3815
|
-
const launchKey = `${ws.sandboxId}:${folder}:${job.script}`;
|
|
4009
|
+
const launchKey = `${ws.sandboxId}:${job.folder || folder}:${job.command || job.script}:${preferred}`;
|
|
3816
4010
|
const starting = recentlyLaunched(launchKey);
|
|
3817
4011
|
hosts.push({
|
|
3818
4012
|
role: job.role,
|
|
@@ -3904,6 +4098,79 @@ async function setupWorkspace(cfg, action) {
|
|
|
3904
4098
|
config.sandbox?.name ||
|
|
3905
4099
|
"Maintainer Pro App";
|
|
3906
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
|
+
|
|
3907
4174
|
const client = configureClient({
|
|
3908
4175
|
dir: resolved,
|
|
3909
4176
|
port,
|
|
@@ -3916,23 +4183,22 @@ async function setupWorkspace(cfg, action) {
|
|
|
3916
4183
|
const corsOrigin = client.corsOrigin || aiOrigin;
|
|
3917
4184
|
const appUrl = client.appUrl || corsOrigin;
|
|
3918
4185
|
|
|
3919
|
-
const partnerIgnorePaths = Array.isArray(config.aiIgnorePaths)
|
|
3920
|
-
? config.aiIgnorePaths
|
|
3921
|
-
: [];
|
|
3922
4186
|
const access = applyAccessPolicy(resolved, partnerIgnorePaths, sandboxId);
|
|
3923
4187
|
|
|
3924
|
-
cfg.workspaces = cfg.workspaces || [];
|
|
3925
|
-
const existing = cfg.workspaces.findIndex((w) => w.sandboxId === sandboxId);
|
|
3926
4188
|
const entry = {
|
|
3927
4189
|
sandboxId,
|
|
3928
4190
|
folderPath: resolved,
|
|
4191
|
+
extraFolders: Array.isArray(existingWs?.extraFolders)
|
|
4192
|
+
? existingWs.extraFolders.filter((folder) => !sameFolder(folder, resolved))
|
|
4193
|
+
: [],
|
|
3929
4194
|
port,
|
|
3930
4195
|
sandboxName: config.sandbox?.name,
|
|
3931
4196
|
applicationName: config.sandbox?.applicationName,
|
|
3932
4197
|
clientKind: client.kind,
|
|
3933
4198
|
appUrl,
|
|
3934
4199
|
sameOrigin: Boolean(client.sameOrigin),
|
|
3935
|
-
appsRequested: false,
|
|
4200
|
+
appsRequested: existingWs?.appsRequested || false,
|
|
4201
|
+
hostApps: existingWs?.hostApps,
|
|
3936
4202
|
store: {
|
|
3937
4203
|
serverKey: String(config.env?.MAINTAINER_PRO_API_KEY || "").trim(),
|
|
3938
4204
|
clientKey: String(
|
|
@@ -3942,7 +4208,7 @@ async function setupWorkspace(cfg, action) {
|
|
|
3942
4208
|
).trim(),
|
|
3943
4209
|
},
|
|
3944
4210
|
};
|
|
3945
|
-
if (
|
|
4211
|
+
if (existingIdx >= 0) cfg.workspaces[existingIdx] = { ...existingWs, ...entry };
|
|
3946
4212
|
else cfg.workspaces.push(entry);
|
|
3947
4213
|
saveConfig(cfg);
|
|
3948
4214
|
|
|
@@ -3994,7 +4260,9 @@ async function setupWorkspace(cfg, action) {
|
|
|
3994
4260
|
const host = workspaceHostReport(entry);
|
|
3995
4261
|
return {
|
|
3996
4262
|
sandboxId,
|
|
3997
|
-
folderPath:
|
|
4263
|
+
folderPath: entry.folderPath,
|
|
4264
|
+
extraFolders: entry.extraFolders || [],
|
|
4265
|
+
addFolder: false,
|
|
3998
4266
|
port: entry.port,
|
|
3999
4267
|
appUrl: host.appUrl || entry.appUrl || appUrl,
|
|
4000
4268
|
origins: host.origins.length
|
|
@@ -4019,7 +4287,8 @@ async function setupWorkspace(cfg, action) {
|
|
|
4019
4287
|
}
|
|
4020
4288
|
|
|
4021
4289
|
async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
4022
|
-
const
|
|
4290
|
+
const folders = workspaceFolders(ws);
|
|
4291
|
+
const folder = folders[0] || path.resolve(ws.folderPath || "");
|
|
4023
4292
|
const label = ws.sandboxName || shortId(ws.sandboxId) || "sandbox";
|
|
4024
4293
|
activity(ws.sandboxId, "info", `Suggesting setup for ${label} from config files`);
|
|
4025
4294
|
try {
|
|
@@ -4031,6 +4300,7 @@ async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
|
|
|
4031
4300
|
}
|
|
4032
4301
|
const proposal = await cli.proposeHostAppsFromConfig({
|
|
4033
4302
|
workspaceDir: folder,
|
|
4303
|
+
workspaceDirs: folders,
|
|
4034
4304
|
appName: ws.applicationName || ws.sandboxName,
|
|
4035
4305
|
preferredAiPort: Number(ws.port) || 3100,
|
|
4036
4306
|
allowAi: opts.allowAi !== false,
|
|
@@ -4145,12 +4415,17 @@ async function runActions(cfg, actions) {
|
|
|
4145
4415
|
partnerIgnorePaths,
|
|
4146
4416
|
ws.sandboxId
|
|
4147
4417
|
);
|
|
4418
|
+
const extras = workspaceFolders(ws).slice(1).map((folder) =>
|
|
4419
|
+
applyAccessPolicy(folder, partnerIgnorePaths, ws.sandboxId)
|
|
4420
|
+
);
|
|
4148
4421
|
log(
|
|
4149
|
-
`access policy synced for ${ws.
|
|
4422
|
+
`access policy synced for ${workspaceFolders(ws).join(", ")} (${access.ignorePaths.length} ignore rules)`
|
|
4150
4423
|
);
|
|
4151
4424
|
result = {
|
|
4152
4425
|
folderPath: path.resolve(ws.folderPath),
|
|
4426
|
+
extraFolders: workspaceFolders(ws).slice(1),
|
|
4153
4427
|
ignorePaths: access.ignorePaths,
|
|
4428
|
+
extraIgnorePaths: extras.map((row) => row.ignorePaths),
|
|
4154
4429
|
syncedAt: new Date().toISOString(),
|
|
4155
4430
|
};
|
|
4156
4431
|
}
|
|
@@ -4386,13 +4661,46 @@ async function runActions(cfg, actions) {
|
|
|
4386
4661
|
const sandboxId = String(
|
|
4387
4662
|
action.sandboxId || action.payload?.sandboxId || ""
|
|
4388
4663
|
);
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
(
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
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
|
+
}
|
|
4396
4704
|
} else {
|
|
4397
4705
|
ok = false;
|
|
4398
4706
|
result = { error: `Unknown action ${action.code}` };
|