@wrongstack/webui 0.273.1 → 0.274.0
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/dist/assets/index-CwMm5VgW.js +140 -0
- package/dist/assets/index-DqD59GFW.css +2 -0
- package/dist/assets/{vendor-P9eRrO6V.js → vendor-CzID01pz.js} +254 -254
- package/dist/index.html +3 -3
- package/dist/index.js +2752 -2278
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +404 -78
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +85 -15
- package/dist/server/index.js +326 -73
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +14 -1
- package/package.json +7 -7
- package/dist/assets/index-BGzM4-Zu.css +0 -2
- package/dist/assets/index-D0dNaLPf.js +0 -140
package/dist/server/index.js
CHANGED
|
@@ -1166,7 +1166,7 @@ function isTrustedLoopbackOrigin(origin) {
|
|
|
1166
1166
|
try {
|
|
1167
1167
|
const url = new URL(origin);
|
|
1168
1168
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1169
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1169
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
1170
1170
|
} catch {
|
|
1171
1171
|
return false;
|
|
1172
1172
|
}
|
|
@@ -1177,6 +1177,14 @@ function isLoopbackBind(wsHost) {
|
|
|
1177
1177
|
function isWildcardBind(wsHost) {
|
|
1178
1178
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
1179
1179
|
}
|
|
1180
|
+
function normalizeHostname(hostname) {
|
|
1181
|
+
const h = hostname.trim().toLowerCase();
|
|
1182
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
1183
|
+
}
|
|
1184
|
+
function allowedHostname(hostname, allowedHostnames) {
|
|
1185
|
+
const normalized = normalizeHostname(hostname);
|
|
1186
|
+
return (allowedHostnames ?? []).some((candidate) => normalizeHostname(candidate) === normalized);
|
|
1187
|
+
}
|
|
1180
1188
|
function tokenMatches(provided, expected) {
|
|
1181
1189
|
if (!provided) return false;
|
|
1182
1190
|
const a = Buffer.from(provided);
|
|
@@ -1215,28 +1223,37 @@ function hostHeaderOk(input) {
|
|
|
1215
1223
|
} catch {
|
|
1216
1224
|
return false;
|
|
1217
1225
|
}
|
|
1218
|
-
return isLoopbackHostname(hostname);
|
|
1226
|
+
return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
|
|
1219
1227
|
}
|
|
1220
1228
|
function verifyClient(input) {
|
|
1221
|
-
const {
|
|
1229
|
+
const {
|
|
1230
|
+
origin,
|
|
1231
|
+
url,
|
|
1232
|
+
hostHeader,
|
|
1233
|
+
remoteAddress,
|
|
1234
|
+
cookieHeader,
|
|
1235
|
+
wsHost,
|
|
1236
|
+
expectedToken,
|
|
1237
|
+
requireToken,
|
|
1238
|
+
allowedHostnames,
|
|
1239
|
+
allowBrowserUrlToken
|
|
1240
|
+
} = input;
|
|
1222
1241
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1223
1242
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
1224
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1243
|
+
if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
|
|
1225
1244
|
if (!origin) {
|
|
1226
1245
|
const remoteIp = remoteAddress ?? "";
|
|
1227
1246
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1228
1247
|
if (!isRemoteLoopback && isWildcardBind(wsHost)) return false;
|
|
1229
|
-
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost);
|
|
1248
|
+
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
|
|
1230
1249
|
}
|
|
1231
1250
|
try {
|
|
1232
|
-
const { hostname } = new URL(origin);
|
|
1233
|
-
if (isLoopbackHostname(
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
return true;
|
|
1251
|
+
const { hostname: originHostname } = new URL(origin);
|
|
1252
|
+
if (isLoopbackHostname(originHostname)) {
|
|
1253
|
+
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
1254
|
+
return isTrustedLoopbackOrigin(origin);
|
|
1238
1255
|
}
|
|
1239
|
-
return cookieTokenOk;
|
|
1256
|
+
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
1240
1257
|
} catch {
|
|
1241
1258
|
return false;
|
|
1242
1259
|
}
|
|
@@ -1262,8 +1279,69 @@ function injectWsPort(html, wsPort) {
|
|
|
1262
1279
|
return `${tag}
|
|
1263
1280
|
${html}`;
|
|
1264
1281
|
}
|
|
1265
|
-
function
|
|
1266
|
-
return
|
|
1282
|
+
function escapeHtmlAttr(value) {
|
|
1283
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1284
|
+
}
|
|
1285
|
+
function injectWsConfig(html, opts) {
|
|
1286
|
+
let out = injectWsPort(html, opts.wsPort);
|
|
1287
|
+
if (!opts.publicWsUrl || out.includes('name="wrongstack-ws-url"')) return out;
|
|
1288
|
+
const tag = `<meta name="wrongstack-ws-url" content="${escapeHtmlAttr(opts.publicWsUrl)}" />`;
|
|
1289
|
+
if (out.includes("</head>")) {
|
|
1290
|
+
return out.replace("</head>", ` ${tag}
|
|
1291
|
+
</head>`);
|
|
1292
|
+
}
|
|
1293
|
+
return `${tag}
|
|
1294
|
+
${out}`;
|
|
1295
|
+
}
|
|
1296
|
+
function firstHeader(value) {
|
|
1297
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1298
|
+
}
|
|
1299
|
+
function wsTokenCookie(token) {
|
|
1300
|
+
return `ws_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`;
|
|
1301
|
+
}
|
|
1302
|
+
function requestToken(req, url) {
|
|
1303
|
+
return url.searchParams.get("token") ?? firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
1304
|
+
}
|
|
1305
|
+
function requestHostForCsp(hostHeader) {
|
|
1306
|
+
const raw = firstHeader(hostHeader)?.trim();
|
|
1307
|
+
if (!raw) return void 0;
|
|
1308
|
+
try {
|
|
1309
|
+
return new URL(`http://${raw}`).hostname;
|
|
1310
|
+
} catch {
|
|
1311
|
+
return void 0;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function formatCspHostname(hostname) {
|
|
1315
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
1316
|
+
}
|
|
1317
|
+
function cspSourceFromUrl(rawUrl) {
|
|
1318
|
+
try {
|
|
1319
|
+
const url = new URL(rawUrl);
|
|
1320
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return void 0;
|
|
1321
|
+
return `${url.protocol}//${formatCspHostname(url.hostname)}${url.port ? `:${url.port}` : ""}`;
|
|
1322
|
+
} catch {
|
|
1323
|
+
return void 0;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
var ALLOWED_INLINE_SCRIPT_HASHES = [
|
|
1327
|
+
"'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
|
|
1328
|
+
"'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
|
|
1329
|
+
];
|
|
1330
|
+
function buildCspHeader(wsPort, requestHost, publicWsUrl) {
|
|
1331
|
+
const connect = /* @__PURE__ */ new Set([
|
|
1332
|
+
"'self'",
|
|
1333
|
+
`ws://127.0.0.1:${wsPort}`,
|
|
1334
|
+
`wss://127.0.0.1:${wsPort}`
|
|
1335
|
+
]);
|
|
1336
|
+
if (requestHost && requestHost !== "127.0.0.1") {
|
|
1337
|
+
const host = formatCspHostname(requestHost);
|
|
1338
|
+
connect.add(`ws://${host}:${wsPort}`);
|
|
1339
|
+
connect.add(`wss://${host}:${wsPort}`);
|
|
1340
|
+
}
|
|
1341
|
+
const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
|
|
1342
|
+
if (publicWsSource) connect.add(publicWsSource);
|
|
1343
|
+
const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
|
|
1344
|
+
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
1267
1345
|
}
|
|
1268
1346
|
function isInsideDist(candidate, distDir) {
|
|
1269
1347
|
const root = path.resolve(distDir);
|
|
@@ -1281,12 +1359,15 @@ function createHttpServer(opts) {
|
|
|
1281
1359
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1282
1360
|
const distDir = path.resolve(opts.distDir);
|
|
1283
1361
|
const wsPort = opts.wsPort;
|
|
1284
|
-
const
|
|
1362
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
1285
1363
|
return http.createServer(async (req, res) => {
|
|
1286
1364
|
try {
|
|
1287
1365
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
1366
|
+
const providedAccessToken = requestToken(req, url);
|
|
1367
|
+
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
1368
|
+
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
1288
1369
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
1289
|
-
const provided = url
|
|
1370
|
+
const provided = requestToken(req, url);
|
|
1290
1371
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
1291
1372
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
1292
1373
|
res.end("Unauthorized");
|
|
@@ -1294,7 +1375,7 @@ function createHttpServer(opts) {
|
|
|
1294
1375
|
}
|
|
1295
1376
|
res.writeHead(200, {
|
|
1296
1377
|
"Content-Type": "text/plain",
|
|
1297
|
-
"Set-Cookie":
|
|
1378
|
+
"Set-Cookie": wsTokenCookie(opts.apiToken),
|
|
1298
1379
|
// Belt-and-braces: tell any caches the cookie response itself
|
|
1299
1380
|
// is sensitive.
|
|
1300
1381
|
"Cache-Control": "no-store"
|
|
@@ -1302,10 +1383,20 @@ function createHttpServer(opts) {
|
|
|
1302
1383
|
res.end("ok");
|
|
1303
1384
|
return;
|
|
1304
1385
|
}
|
|
1386
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1387
|
+
res.writeHead(401, {
|
|
1388
|
+
"Content-Type": "text/plain",
|
|
1389
|
+
"Cache-Control": "no-store"
|
|
1390
|
+
});
|
|
1391
|
+
res.end("Unauthorized");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (shouldSetAuthCookie && opts.apiToken) {
|
|
1395
|
+
res.setHeader("Set-Cookie", wsTokenCookie(opts.apiToken));
|
|
1396
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1397
|
+
}
|
|
1305
1398
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
1306
|
-
|
|
1307
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1308
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1399
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1309
1400
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1310
1401
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1311
1402
|
return;
|
|
@@ -1319,9 +1410,7 @@ function createHttpServer(opts) {
|
|
|
1319
1410
|
return;
|
|
1320
1411
|
}
|
|
1321
1412
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
1322
|
-
|
|
1323
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1324
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1413
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1325
1414
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1326
1415
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1327
1416
|
return;
|
|
@@ -1331,9 +1420,7 @@ function createHttpServer(opts) {
|
|
|
1331
1420
|
}
|
|
1332
1421
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
1333
1422
|
if (agentsMatch && req.method === "GET") {
|
|
1334
|
-
|
|
1335
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1336
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1423
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1337
1424
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1338
1425
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1339
1426
|
return;
|
|
@@ -1343,9 +1430,7 @@ function createHttpServer(opts) {
|
|
|
1343
1430
|
}
|
|
1344
1431
|
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
1345
1432
|
if (eventsMatch && req.method === "GET") {
|
|
1346
|
-
|
|
1347
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1348
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1433
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1349
1434
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1350
1435
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1351
1436
|
return;
|
|
@@ -1357,9 +1442,7 @@ function createHttpServer(opts) {
|
|
|
1357
1442
|
}
|
|
1358
1443
|
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
1359
1444
|
if (msgMatch && req.method === "POST") {
|
|
1360
|
-
|
|
1361
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1362
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1445
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1363
1446
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1364
1447
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1365
1448
|
return;
|
|
@@ -1369,9 +1452,7 @@ function createHttpServer(opts) {
|
|
|
1369
1452
|
}
|
|
1370
1453
|
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
1371
1454
|
if (mailboxMatch && req.method === "GET") {
|
|
1372
|
-
|
|
1373
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1374
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1455
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1375
1456
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1376
1457
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1377
1458
|
return;
|
|
@@ -1381,9 +1462,7 @@ function createHttpServer(opts) {
|
|
|
1381
1462
|
}
|
|
1382
1463
|
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
1383
1464
|
if (interruptMatch && req.method === "POST") {
|
|
1384
|
-
|
|
1385
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1386
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1465
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1387
1466
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1388
1467
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1389
1468
|
return;
|
|
@@ -1397,9 +1476,7 @@ function createHttpServer(opts) {
|
|
|
1397
1476
|
return;
|
|
1398
1477
|
}
|
|
1399
1478
|
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
1400
|
-
|
|
1401
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1402
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1479
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1403
1480
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1404
1481
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1405
1482
|
return;
|
|
@@ -1446,11 +1523,14 @@ function createHttpServer(opts) {
|
|
|
1446
1523
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1447
1524
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1448
1525
|
if (ext === ".html") {
|
|
1449
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1450
|
-
res.setHeader(
|
|
1526
|
+
if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
|
|
1527
|
+
res.setHeader(
|
|
1528
|
+
"Content-Security-Policy",
|
|
1529
|
+
buildCspHeader(wsPort, requestHostForCsp(req.headers.host), opts.publicWsUrl)
|
|
1530
|
+
);
|
|
1451
1531
|
const html = await fs.readFile(resolvedPath, "utf8");
|
|
1452
1532
|
res.writeHead(200);
|
|
1453
|
-
res.end(
|
|
1533
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1454
1534
|
return;
|
|
1455
1535
|
}
|
|
1456
1536
|
const fileContent = await fs.readFile(resolvedPath);
|
|
@@ -1465,9 +1545,13 @@ function createHttpServer(opts) {
|
|
|
1465
1545
|
"X-Content-Type-Options": "nosniff",
|
|
1466
1546
|
"X-Frame-Options": "DENY",
|
|
1467
1547
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1468
|
-
"Content-Security-Policy": buildCspHeader(
|
|
1548
|
+
"Content-Security-Policy": buildCspHeader(
|
|
1549
|
+
wsPort,
|
|
1550
|
+
requestHostForCsp(req.headers.host),
|
|
1551
|
+
opts.publicWsUrl
|
|
1552
|
+
)
|
|
1469
1553
|
});
|
|
1470
|
-
res.end(
|
|
1554
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1471
1555
|
} catch {
|
|
1472
1556
|
res.writeHead(404);
|
|
1473
1557
|
res.end("Not found");
|
|
@@ -1699,6 +1783,37 @@ function errMessage(err) {
|
|
|
1699
1783
|
function generateAuthToken() {
|
|
1700
1784
|
return randomBytes(16).toString("hex");
|
|
1701
1785
|
}
|
|
1786
|
+
function resolveAuthToken(explicit) {
|
|
1787
|
+
const configured = explicit?.trim() || process.env["WEBUI_TOKEN"]?.trim() || process.env["WEBUI_AUTH_TOKEN"]?.trim();
|
|
1788
|
+
return configured || generateAuthToken();
|
|
1789
|
+
}
|
|
1790
|
+
function hostForBrowserUrl(bindHost) {
|
|
1791
|
+
if (bindHost === "0.0.0.0") return "127.0.0.1";
|
|
1792
|
+
if (bindHost === "::" || bindHost === "[::]") return "[::1]";
|
|
1793
|
+
if (bindHost.includes(":") && !bindHost.startsWith("[")) return `[${bindHost}]`;
|
|
1794
|
+
return bindHost;
|
|
1795
|
+
}
|
|
1796
|
+
function buildWebUIAccessUrl(opts) {
|
|
1797
|
+
const protocol = opts.protocol ?? "http";
|
|
1798
|
+
const base = opts.publicUrl?.trim() || `${protocol}://${hostForBrowserUrl(opts.host)}:${opts.port}`;
|
|
1799
|
+
if (!opts.token) return base;
|
|
1800
|
+
try {
|
|
1801
|
+
const url = new URL(base);
|
|
1802
|
+
url.searchParams.set("token", opts.token);
|
|
1803
|
+
const rendered = url.toString();
|
|
1804
|
+
const afterOrigin = base.slice(url.origin.length);
|
|
1805
|
+
if (url.pathname === "/" && !afterOrigin.startsWith("/")) {
|
|
1806
|
+
return `${url.origin}${url.search}${url.hash}`;
|
|
1807
|
+
}
|
|
1808
|
+
return rendered;
|
|
1809
|
+
} catch {
|
|
1810
|
+
return `${base}${base.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.token)}`;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function envFlag(name2) {
|
|
1814
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
1815
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1816
|
+
}
|
|
1702
1817
|
|
|
1703
1818
|
// src/server/file-handlers.ts
|
|
1704
1819
|
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
@@ -3030,6 +3145,13 @@ import {
|
|
|
3030
3145
|
PhaseStore,
|
|
3031
3146
|
WorktreeManager
|
|
3032
3147
|
} from "@wrongstack/core";
|
|
3148
|
+
function deriveTitle(goal) {
|
|
3149
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3150
|
+
if (!firstLine) return "AutoPhase";
|
|
3151
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3152
|
+
const trimmed = sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3153
|
+
return trimmed || "AutoPhase";
|
|
3154
|
+
}
|
|
3033
3155
|
function isGitRepo(cwd) {
|
|
3034
3156
|
try {
|
|
3035
3157
|
const r = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -3038,6 +3160,19 @@ function isGitRepo(cwd) {
|
|
|
3038
3160
|
return false;
|
|
3039
3161
|
}
|
|
3040
3162
|
}
|
|
3163
|
+
function commitsSince(cwd, baseSha, branch) {
|
|
3164
|
+
try {
|
|
3165
|
+
const r = spawnSync("git", ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`], {
|
|
3166
|
+
cwd,
|
|
3167
|
+
encoding: "utf8",
|
|
3168
|
+
windowsHide: true
|
|
3169
|
+
});
|
|
3170
|
+
if (r.status !== 0) return [];
|
|
3171
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3172
|
+
} catch {
|
|
3173
|
+
return [];
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3041
3176
|
var AutoPhaseWebSocketHandler = class {
|
|
3042
3177
|
constructor(agent, context, logger, storeDir, events, projectRoot) {
|
|
3043
3178
|
this.agent = agent;
|
|
@@ -3057,10 +3192,17 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3057
3192
|
store;
|
|
3058
3193
|
clients = /* @__PURE__ */ new Set();
|
|
3059
3194
|
broadcastInterval = null;
|
|
3060
|
-
/** Aborts in-flight task agents when the run is stopped. */
|
|
3195
|
+
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
3061
3196
|
abort = null;
|
|
3197
|
+
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
3198
|
+
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
3199
|
+
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
3200
|
+
stopping = false;
|
|
3062
3201
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
3063
3202
|
worktrees = null;
|
|
3203
|
+
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
3204
|
+
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
3205
|
+
runBase = null;
|
|
3064
3206
|
/** Per-run worker identities so the board can show "who is on what". */
|
|
3065
3207
|
usedNicknames = /* @__PURE__ */ new Set();
|
|
3066
3208
|
addClient(ws) {
|
|
@@ -3084,11 +3226,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3084
3226
|
this.broadcast({ type: "autophase.resumed", payload: {} });
|
|
3085
3227
|
break;
|
|
3086
3228
|
case "autophase.stop":
|
|
3087
|
-
this.
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3229
|
+
await this.handleStop();
|
|
3230
|
+
break;
|
|
3231
|
+
case "autophase.clear":
|
|
3232
|
+
await this.handleClear();
|
|
3233
|
+
break;
|
|
3234
|
+
case "autophase.revert":
|
|
3235
|
+
await this.handleRevert();
|
|
3092
3236
|
break;
|
|
3093
3237
|
case "autophase.status":
|
|
3094
3238
|
this.broadcastState();
|
|
@@ -3165,17 +3309,27 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3165
3309
|
}
|
|
3166
3310
|
}
|
|
3167
3311
|
async handleStart(payload) {
|
|
3168
|
-
const
|
|
3312
|
+
const goal = payload?.goal || payload?.title || "Untitled Project";
|
|
3313
|
+
const title = deriveTitle(goal);
|
|
3169
3314
|
const autonomous = payload?.autonomous ?? true;
|
|
3170
|
-
|
|
3315
|
+
this.abort = new AbortController();
|
|
3316
|
+
this.stopping = false;
|
|
3317
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
|
|
3318
|
+
if (this.stopping || this.abort.signal.aborted) {
|
|
3319
|
+
this.broadcast({ type: "autophase.stopped", payload: { title } });
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3171
3322
|
this.logger.info(`[AutoPhase] Starting: ${title}`);
|
|
3172
|
-
const graph = await new PhaseGraphBuilder({ title, phases, autonomous }).build();
|
|
3323
|
+
const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous }).build();
|
|
3173
3324
|
this.graph = graph;
|
|
3174
|
-
this.abort = new AbortController();
|
|
3175
3325
|
await this.store.save(graph);
|
|
3176
|
-
|
|
3326
|
+
const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_AUTOPHASE_WORKTREES"] !== "0";
|
|
3327
|
+
if (!this.worktrees && this.events && this.projectRoot && useWorktrees && isGitRepo(this.projectRoot)) {
|
|
3177
3328
|
this.worktrees = new WorktreeManager({ projectRoot: this.projectRoot, events: this.events });
|
|
3178
3329
|
}
|
|
3330
|
+
if (this.worktrees) {
|
|
3331
|
+
this.runBase = await this.worktrees.currentBase();
|
|
3332
|
+
}
|
|
3179
3333
|
this.orchestrator = new PhaseOrchestrator({
|
|
3180
3334
|
graph,
|
|
3181
3335
|
ctx: {
|
|
@@ -3222,6 +3376,62 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3222
3376
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
3223
3377
|
});
|
|
3224
3378
|
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
3381
|
+
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
3382
|
+
* tick, and ends the live broadcast. The board is kept for review; use
|
|
3383
|
+
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
3384
|
+
*/
|
|
3385
|
+
async handleStop() {
|
|
3386
|
+
this.stopping = true;
|
|
3387
|
+
this.abort?.abort();
|
|
3388
|
+
this.orchestrator?.stop();
|
|
3389
|
+
this.stopBroadcast();
|
|
3390
|
+
if (this.graph) await this.store.save(this.graph).catch(() => void 0);
|
|
3391
|
+
this.broadcast({ type: "autophase.stopped", payload: { title: this.graph?.title } });
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
3395
|
+
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
3396
|
+
* on the base branch — that is `autophase.revert`.
|
|
3397
|
+
*/
|
|
3398
|
+
async handleClear() {
|
|
3399
|
+
await this.handleStop();
|
|
3400
|
+
if (this.worktrees) await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3401
|
+
this.orchestrator = null;
|
|
3402
|
+
this.graph = null;
|
|
3403
|
+
this.runBase = null;
|
|
3404
|
+
this.usedNicknames.clear();
|
|
3405
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3406
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3407
|
+
}
|
|
3408
|
+
/**
|
|
3409
|
+
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
3410
|
+
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
3411
|
+
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
3412
|
+
* conflicting revert rather than leaving the tree half-reverted.
|
|
3413
|
+
*/
|
|
3414
|
+
async handleRevert() {
|
|
3415
|
+
await this.handleStop();
|
|
3416
|
+
if (!this.worktrees || !this.runBase || !this.projectRoot) {
|
|
3417
|
+
this.broadcast({
|
|
3418
|
+
type: "autophase.reverted",
|
|
3419
|
+
payload: { ok: false, reverted: 0, reason: "no git baseline was captured for this run" }
|
|
3420
|
+
});
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3424
|
+
const shas = commitsSince(this.projectRoot, this.runBase.sha, this.runBase.branch);
|
|
3425
|
+
const res = await this.worktrees.revertCommits(this.runBase.branch, shas);
|
|
3426
|
+
this.broadcast({ type: "autophase.reverted", payload: res });
|
|
3427
|
+
if (res.ok) {
|
|
3428
|
+
this.orchestrator = null;
|
|
3429
|
+
this.graph = null;
|
|
3430
|
+
this.runBase = null;
|
|
3431
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3432
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3225
3435
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
3226
3436
|
defaultPhases() {
|
|
3227
3437
|
return [
|
|
@@ -3232,13 +3442,18 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3232
3442
|
{ name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
|
|
3233
3443
|
];
|
|
3234
3444
|
}
|
|
3235
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3236
|
-
|
|
3445
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3446
|
+
* The caller passes the run's abort signal so a stop during planning cancels
|
|
3447
|
+
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
3448
|
+
* uninterruptible). */
|
|
3449
|
+
async planPhases(goal, signal) {
|
|
3237
3450
|
try {
|
|
3238
3451
|
const planner = new AutoPhasePlanner({
|
|
3239
3452
|
goal,
|
|
3240
3453
|
runOnce: async (prompt) => {
|
|
3241
|
-
const result = await this.agent.run(prompt, {
|
|
3454
|
+
const result = await this.agent.run(prompt, {
|
|
3455
|
+
signal: signal ?? new AbortController().signal
|
|
3456
|
+
});
|
|
3242
3457
|
return result.status === "done" ? result.finalText ?? "" : "";
|
|
3243
3458
|
}
|
|
3244
3459
|
});
|
|
@@ -3373,6 +3588,10 @@ Type: ${task.type}`;
|
|
|
3373
3588
|
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3374
3589
|
return {
|
|
3375
3590
|
title: this.graph.title,
|
|
3591
|
+
// Full operator prompt, shown verbatim in a dedicated goal block (the
|
|
3592
|
+
// title is only a short derived heading). Fall back to the title for
|
|
3593
|
+
// legacy boards saved before the title/goal split.
|
|
3594
|
+
goal: this.graph.description || this.graph.title,
|
|
3376
3595
|
phases: phaseItems,
|
|
3377
3596
|
tasks: taskItems,
|
|
3378
3597
|
activePhaseId: currentActiveId,
|
|
@@ -3683,6 +3902,12 @@ var SddBoardWebSocketHandler = class {
|
|
|
3683
3902
|
};
|
|
3684
3903
|
|
|
3685
3904
|
// src/server/sdd-wizard-ws-handler.ts
|
|
3905
|
+
function deriveTitle2(goal) {
|
|
3906
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3907
|
+
if (!firstLine) return "New SDD Project";
|
|
3908
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3909
|
+
return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3910
|
+
}
|
|
3686
3911
|
var SddWizardWebSocketHandler = class {
|
|
3687
3912
|
constructor(deps2) {
|
|
3688
3913
|
this.deps = deps2;
|
|
@@ -3721,7 +3946,8 @@ var SddWizardWebSocketHandler = class {
|
|
|
3721
3946
|
parallelSlots: msg.payload?.parallelSlots,
|
|
3722
3947
|
defaultModel: msg.payload?.model,
|
|
3723
3948
|
defaultProvider: msg.payload?.provider,
|
|
3724
|
-
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
3949
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0,
|
|
3950
|
+
worktrees: typeof msg.payload?.worktrees === "boolean" ? msg.payload.worktrees : void 0
|
|
3725
3951
|
});
|
|
3726
3952
|
break;
|
|
3727
3953
|
}
|
|
@@ -3741,7 +3967,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3741
3967
|
}
|
|
3742
3968
|
if (this.busy) return;
|
|
3743
3969
|
this.driver = this.deps.makeDriver();
|
|
3744
|
-
const prompt = this.driver.start(goal);
|
|
3970
|
+
const prompt = this.driver.start(deriveTitle2(goal), goal);
|
|
3745
3971
|
await this.runTurn(prompt);
|
|
3746
3972
|
}
|
|
3747
3973
|
async onMessage(text) {
|
|
@@ -3826,6 +4052,7 @@ import {
|
|
|
3826
4052
|
TaskGraphStore as TaskGraphStore2,
|
|
3827
4053
|
WorktreeManager as WorktreeManager2
|
|
3828
4054
|
} from "@wrongstack/core";
|
|
4055
|
+
var PLANNING_ONLY_GUARD = "SYSTEM: You are running a PLANNING-ONLY specification interview. Do NOT write, create, or edit any files, and do NOT run shell/terminal commands or use any code-editing tools \u2014 they are disabled here and any attempt will fail and waste the turn. Respond with TEXT ONLY: ask your question, or emit the requested spec / plan / task JSON. All code is written later, automatically, once the plan is approved and the multi-agent run starts.\n\n---\n\n";
|
|
3829
4056
|
function buildSddWizardDeps(opts) {
|
|
3830
4057
|
const registry = new SddRunRegistry();
|
|
3831
4058
|
let isolatedSeq = 0;
|
|
@@ -3834,11 +4061,11 @@ function buildSddWizardDeps(opts) {
|
|
|
3834
4061
|
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3835
4062
|
role: "executor",
|
|
3836
4063
|
name: name2,
|
|
3837
|
-
disabledTools: ["delegate"],
|
|
4064
|
+
disabledTools: ["delegate", "write", "edit", "patch", "bash", "exec"],
|
|
3838
4065
|
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3839
4066
|
});
|
|
3840
4067
|
try {
|
|
3841
|
-
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
4068
|
+
const res = await result.agent.run([{ type: "text", text: PLANNING_ONLY_GUARD + prompt }]);
|
|
3842
4069
|
return res.finalText ?? "";
|
|
3843
4070
|
} finally {
|
|
3844
4071
|
await result.dispose?.();
|
|
@@ -3851,14 +4078,15 @@ function buildSddWizardDeps(opts) {
|
|
|
3851
4078
|
sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3852
4079
|
}),
|
|
3853
4080
|
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3854
|
-
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
4081
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
|
|
3855
4082
|
const graph = driver.getGraph();
|
|
3856
4083
|
const tracker = driver.getTracker();
|
|
3857
4084
|
if (!graph || !tracker) {
|
|
3858
4085
|
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3859
4086
|
}
|
|
4087
|
+
const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
|
|
3860
4088
|
let worktrees;
|
|
3861
|
-
if (
|
|
4089
|
+
if (worktreesEnabled) {
|
|
3862
4090
|
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3863
4091
|
cwd: opts.projectRoot,
|
|
3864
4092
|
encoding: "utf8",
|
|
@@ -7775,8 +8003,11 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7775
8003
|
async function startWebUI(opts = {}) {
|
|
7776
8004
|
ensureSessionShell();
|
|
7777
8005
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7778
|
-
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7779
|
-
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8006
|
+
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
8007
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
|
|
8008
|
+
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
8009
|
+
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
8010
|
+
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
7780
8011
|
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
7781
8012
|
let wsPort = requestedWsPort;
|
|
7782
8013
|
let httpPort = requestedHttpPort;
|
|
@@ -8571,8 +8802,16 @@ async function startWebUI(opts = {}) {
|
|
|
8571
8802
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID2)
|
|
8572
8803
|
};
|
|
8573
8804
|
}
|
|
8574
|
-
const wsToken =
|
|
8575
|
-
console.log("[WebUI] WS auth token
|
|
8805
|
+
const wsToken = resolveAuthToken(opts.accessToken);
|
|
8806
|
+
console.log("[WebUI] WS auth token ready");
|
|
8807
|
+
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
8808
|
+
if (!value) return void 0;
|
|
8809
|
+
try {
|
|
8810
|
+
return new URL(value).hostname;
|
|
8811
|
+
} catch {
|
|
8812
|
+
return void 0;
|
|
8813
|
+
}
|
|
8814
|
+
}).filter((value) => Boolean(value));
|
|
8576
8815
|
const verifyClient2 = (info) => verifyClient({
|
|
8577
8816
|
origin: info.origin,
|
|
8578
8817
|
url: info.req.url ?? "",
|
|
@@ -8584,7 +8823,10 @@ async function startWebUI(opts = {}) {
|
|
|
8584
8823
|
// exposure class.
|
|
8585
8824
|
cookieHeader: info.req.headers.cookie,
|
|
8586
8825
|
wsHost,
|
|
8587
|
-
expectedToken: wsToken
|
|
8826
|
+
expectedToken: wsToken,
|
|
8827
|
+
requireToken,
|
|
8828
|
+
allowedHostnames: publicHostnames,
|
|
8829
|
+
allowBrowserUrlToken: Boolean(publicWsUrl)
|
|
8588
8830
|
});
|
|
8589
8831
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
8590
8832
|
const wssPrimary = new WebSocketServer({
|
|
@@ -9524,8 +9766,10 @@ async function startWebUI(opts = {}) {
|
|
|
9524
9766
|
host: wsHost,
|
|
9525
9767
|
distDir: path16.resolve(import.meta.dirname, "../../dist"),
|
|
9526
9768
|
wsPort,
|
|
9769
|
+
publicWsUrl,
|
|
9527
9770
|
globalRoot: wpaths.globalRoot,
|
|
9528
9771
|
apiToken: wsToken,
|
|
9772
|
+
requireToken,
|
|
9529
9773
|
watcherMetrics,
|
|
9530
9774
|
onFleetPing: () => {
|
|
9531
9775
|
void fleetBroadcast?.();
|
|
@@ -9533,7 +9777,12 @@ async function startWebUI(opts = {}) {
|
|
|
9533
9777
|
});
|
|
9534
9778
|
const registryBaseDir = path16.dirname(globalConfigPath);
|
|
9535
9779
|
httpServer.listen(httpPort, wsHost, () => {
|
|
9536
|
-
const openUrl =
|
|
9780
|
+
const openUrl = buildWebUIAccessUrl({
|
|
9781
|
+
host: wsHost,
|
|
9782
|
+
port: httpPort,
|
|
9783
|
+
token: wsToken,
|
|
9784
|
+
publicUrl
|
|
9785
|
+
});
|
|
9537
9786
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
9538
9787
|
if (opts.open) openBrowser(openUrl);
|
|
9539
9788
|
void registerInstance(
|
|
@@ -9545,7 +9794,7 @@ async function startWebUI(opts = {}) {
|
|
|
9545
9794
|
projectRoot,
|
|
9546
9795
|
projectName: path16.basename(projectRoot) || projectRoot,
|
|
9547
9796
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9548
|
-
url:
|
|
9797
|
+
url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
|
|
9549
9798
|
},
|
|
9550
9799
|
registryBaseDir
|
|
9551
9800
|
).catch((err) => console.warn(JSON.stringify({
|
|
@@ -9595,6 +9844,7 @@ export {
|
|
|
9595
9844
|
browserOpenCommand,
|
|
9596
9845
|
buildCspHeader,
|
|
9597
9846
|
buildSddWizardDeps,
|
|
9847
|
+
buildWebUIAccessUrl,
|
|
9598
9848
|
createCustomModeStore,
|
|
9599
9849
|
createEternalSubscription,
|
|
9600
9850
|
createHttpServer,
|
|
@@ -9602,6 +9852,7 @@ export {
|
|
|
9602
9852
|
createToolLspCompletionSource,
|
|
9603
9853
|
defaultBaseDir,
|
|
9604
9854
|
deleteKey,
|
|
9855
|
+
envFlag,
|
|
9605
9856
|
errMessage,
|
|
9606
9857
|
estimateTokens,
|
|
9607
9858
|
extractToken,
|
|
@@ -9637,6 +9888,7 @@ export {
|
|
|
9637
9888
|
handleSkillsInstall,
|
|
9638
9889
|
handleSkillsUninstall,
|
|
9639
9890
|
handleSkillsUpdate,
|
|
9891
|
+
hostForBrowserUrl,
|
|
9640
9892
|
hostHeaderOk,
|
|
9641
9893
|
injectWsPort,
|
|
9642
9894
|
isLoopbackBind,
|
|
@@ -9652,6 +9904,7 @@ export {
|
|
|
9652
9904
|
registerInstance,
|
|
9653
9905
|
registryPath,
|
|
9654
9906
|
removeProvider,
|
|
9907
|
+
resolveAuthToken,
|
|
9655
9908
|
saveProviders,
|
|
9656
9909
|
send,
|
|
9657
9910
|
sendResult2 as sendResult,
|