leglas 0.9.0 → 1.1.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/README.md +297 -379
- package/dist/bin.js +4337 -2094
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1788 -222
- package/dist/restart.d.ts +13 -0
- package/dist/run.d.ts +5 -0
- package/dist/shell/assets/index-BzaBzGEv.css +1 -0
- package/dist/shell/assets/index-CKmxj5gW.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-BMpAHiSA.js +0 -14
- package/dist/shell/assets/index-pfmBGQQ0.css +0 -1
package/dist/index.js
CHANGED
|
@@ -1247,13 +1247,21 @@ var KNOWN_AGENTS = {
|
|
|
1247
1247
|
name: "Cursor",
|
|
1248
1248
|
binary: "cursor-agent",
|
|
1249
1249
|
efforts: [],
|
|
1250
|
+
// `--trust` on every invocation. Without it, print mode stops at a
|
|
1251
|
+
// "Workspace Trust Required" prompt nothing can answer and exits 1 with
|
|
1252
|
+
// no events, in any directory Cursor has not been trusted for by hand:
|
|
1253
|
+
// measured against cursor-agent 2026.09.02, one second, zero output. The
|
|
1254
|
+
// project is the one the user pointed Leglas at, which is the trust the
|
|
1255
|
+
// flag grants. It is the only permission the run needs: with it alone,
|
|
1256
|
+
// the edit and the shell command in the same run both executed.
|
|
1250
1257
|
args: (prompt, _effort = null, _images = []) => [
|
|
1251
1258
|
"-p",
|
|
1252
1259
|
prompt,
|
|
1253
1260
|
"--output-format",
|
|
1254
|
-
"stream-json"
|
|
1261
|
+
"stream-json",
|
|
1262
|
+
"--trust"
|
|
1255
1263
|
],
|
|
1256
|
-
terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt],
|
|
1264
|
+
terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt, "--trust"],
|
|
1257
1265
|
// `--resume [chatId]` is documented alongside `--continue` in the CLI
|
|
1258
1266
|
// parameter reference, and every stream-json event carries the
|
|
1259
1267
|
// `session_id` to feed it. Cursor exposes no persistent transport the way
|
|
@@ -1264,11 +1272,16 @@ var KNOWN_AGENTS = {
|
|
|
1264
1272
|
// Images are accepted and ignored, like its other argument builders:
|
|
1265
1273
|
// `cursor-agent` documents no flag for them, and the capture paths reach
|
|
1266
1274
|
// it as text in the prompt either way.
|
|
1267
|
-
resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json"],
|
|
1275
|
+
resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json", "--trust"],
|
|
1268
1276
|
sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
|
|
1277
|
+
// Read against cursor-agent 2026.09.02: every event carries the id, a
|
|
1278
|
+
// resume in this argument order answers under the same id and remembers
|
|
1279
|
+
// the earlier turn.
|
|
1280
|
+
activityVerified: true,
|
|
1269
1281
|
authArgs: ["status"],
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1282
|
+
// The signed-in form was read from the CLI: "✓ Logged in as <email>",
|
|
1283
|
+
// exit 0. The signed-out form has not been, so that side of the reading
|
|
1284
|
+
// stays loose, and anything ambiguous stays unknown.
|
|
1272
1285
|
authVerdict: (result) => {
|
|
1273
1286
|
if (/logged in|signed in/i.test(result.stdout))
|
|
1274
1287
|
return "ok";
|
|
@@ -1358,9 +1371,9 @@ function agentSearchPath(env = process.env, platform = process.platform) {
|
|
|
1358
1371
|
function agentEnvironment(env = process.env) {
|
|
1359
1372
|
return { ...env, PATH: agentSearchPath(env) };
|
|
1360
1373
|
}
|
|
1361
|
-
async function pathLookup(binary) {
|
|
1362
|
-
const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
|
|
1363
|
-
const extensions =
|
|
1374
|
+
async function pathLookup(binary, env = process.env, platform = process.platform) {
|
|
1375
|
+
const entries = agentSearchPath(env, platform).split(delimiter).filter((entry) => entry !== "");
|
|
1376
|
+
const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
1364
1377
|
for (const entry of entries) {
|
|
1365
1378
|
for (const extension of extensions) {
|
|
1366
1379
|
try {
|
|
@@ -1372,10 +1385,10 @@ async function pathLookup(binary) {
|
|
|
1372
1385
|
}
|
|
1373
1386
|
return false;
|
|
1374
1387
|
}
|
|
1375
|
-
async function detectAgents(
|
|
1388
|
+
async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
|
|
1376
1389
|
const entries = Object.entries(KNOWN_AGENTS);
|
|
1377
1390
|
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
1378
|
-
const available = await
|
|
1391
|
+
const available = await lookup2(adapter.binary).catch(() => false);
|
|
1379
1392
|
if (!available) {
|
|
1380
1393
|
return {
|
|
1381
1394
|
id,
|
|
@@ -1472,15 +1485,15 @@ function cursorActivity(event, cwd) {
|
|
|
1472
1485
|
const wrapper = record(event.tool_call);
|
|
1473
1486
|
if (wrapper === null)
|
|
1474
1487
|
return null;
|
|
1475
|
-
const key = Object.keys(wrapper)
|
|
1488
|
+
const key = Object.keys(wrapper).find((name) => name.endsWith("ToolCall"));
|
|
1476
1489
|
if (key === void 0)
|
|
1477
1490
|
return null;
|
|
1478
1491
|
const call = record(wrapper[key]);
|
|
1479
1492
|
const args = record(call?.args);
|
|
1480
1493
|
const tool = key.replace(/ToolCall$/, "");
|
|
1481
|
-
if (tool === "write") {
|
|
1494
|
+
if (tool === "edit" || tool === "write") {
|
|
1482
1495
|
const path2 = shownPath(args?.path, cwd);
|
|
1483
|
-
return path2 === null ? "
|
|
1496
|
+
return path2 === null ? "editing a file" : `editing ${path2}`;
|
|
1484
1497
|
}
|
|
1485
1498
|
if (tool === "read") {
|
|
1486
1499
|
const path2 = shownPath(args?.path, cwd);
|
|
@@ -1804,6 +1817,13 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
1804
1817
|
// ../server/dist/proxy.js
|
|
1805
1818
|
import http, {} from "http";
|
|
1806
1819
|
import net from "net";
|
|
1820
|
+
var SHARE_COOKIE = "leglas-share";
|
|
1821
|
+
function withoutShareCookie(cookie) {
|
|
1822
|
+
if (cookie === void 0)
|
|
1823
|
+
return void 0;
|
|
1824
|
+
const kept = (Array.isArray(cookie) ? cookie.join("; ") : cookie).split(";").map((entry) => entry.trim()).filter((entry) => entry !== "" && !entry.startsWith(`${SHARE_COOKIE}=`));
|
|
1825
|
+
return kept.length === 0 ? void 0 : kept.join("; ");
|
|
1826
|
+
}
|
|
1807
1827
|
function createProxyHandler(options) {
|
|
1808
1828
|
const target = new URL(options.target);
|
|
1809
1829
|
const host = target.hostname;
|
|
@@ -1811,19 +1831,25 @@ function createProxyHandler(options) {
|
|
|
1811
1831
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
1812
1832
|
const authority = target.port ? `${host}:${target.port}` : host;
|
|
1813
1833
|
function upstreamHeaders(req) {
|
|
1814
|
-
|
|
1834
|
+
const headers = { ...req.headers, host: authority };
|
|
1835
|
+
const cookie = withoutShareCookie(headers.cookie);
|
|
1836
|
+
if (cookie === void 0)
|
|
1837
|
+
delete headers.cookie;
|
|
1838
|
+
else
|
|
1839
|
+
headers.cookie = cookie;
|
|
1840
|
+
return headers;
|
|
1815
1841
|
}
|
|
1816
|
-
function rewriteLocation(location,
|
|
1842
|
+
function rewriteLocation(location, publicOrigin2) {
|
|
1817
1843
|
if (location === void 0)
|
|
1818
1844
|
return void 0;
|
|
1819
1845
|
for (const origin of [`${target.protocol}//${authority}`, `${target.protocol}//localhost:${port}`]) {
|
|
1820
1846
|
if (location.startsWith(origin))
|
|
1821
|
-
return
|
|
1847
|
+
return publicOrigin2 + location.slice(origin.length);
|
|
1822
1848
|
}
|
|
1823
1849
|
return location;
|
|
1824
1850
|
}
|
|
1825
1851
|
return {
|
|
1826
|
-
request(req, res,
|
|
1852
|
+
request(req, res, publicOrigin2) {
|
|
1827
1853
|
options.onActivity?.();
|
|
1828
1854
|
options.onOpen?.();
|
|
1829
1855
|
let open = true;
|
|
@@ -1838,7 +1864,7 @@ function createProxyHandler(options) {
|
|
|
1838
1864
|
res.once("close", close);
|
|
1839
1865
|
const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
|
|
1840
1866
|
const headers = { ...upstreamRes.headers };
|
|
1841
|
-
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0,
|
|
1867
|
+
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin2);
|
|
1842
1868
|
if (location !== void 0)
|
|
1843
1869
|
headers.location = location;
|
|
1844
1870
|
res.writeHead(upstreamRes.statusCode ?? 502, headers);
|
|
@@ -3027,13 +3053,13 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3027
3053
|
const references = [];
|
|
3028
3054
|
requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
|
|
3029
3055
|
const controller = new AbortController();
|
|
3030
|
-
let
|
|
3056
|
+
let expired2 = false;
|
|
3031
3057
|
let finishDeadline;
|
|
3032
3058
|
const deadline = new Promise((resolve5) => {
|
|
3033
3059
|
finishDeadline = resolve5;
|
|
3034
3060
|
});
|
|
3035
3061
|
const timer = setTimeout(() => {
|
|
3036
|
-
|
|
3062
|
+
expired2 = true;
|
|
3037
3063
|
controller.abort();
|
|
3038
3064
|
finishDeadline();
|
|
3039
3065
|
}, deadlineMs);
|
|
@@ -3046,7 +3072,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3046
3072
|
const work = (async () => {
|
|
3047
3073
|
try {
|
|
3048
3074
|
const browser = await deps.pool.acquire();
|
|
3049
|
-
if (
|
|
3075
|
+
if (expired2)
|
|
3050
3076
|
return;
|
|
3051
3077
|
if (browser === null) {
|
|
3052
3078
|
captured.skipped = deps.pool.reason() ?? NO_BROWSER;
|
|
@@ -3060,7 +3086,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3060
3086
|
signal: controller.signal
|
|
3061
3087
|
};
|
|
3062
3088
|
const direction = await capture(browser, directionInput);
|
|
3063
|
-
if (
|
|
3089
|
+
if (expired2)
|
|
3064
3090
|
return;
|
|
3065
3091
|
await mkdir3(destination, { recursive: true });
|
|
3066
3092
|
await writeFile3(join5(destination, "frame.png"), direction.frame.png);
|
|
@@ -3078,7 +3104,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3078
3104
|
for (let index = 0; index < direction.crops.length; index += 1) {
|
|
3079
3105
|
const crop = direction.crops[index];
|
|
3080
3106
|
const note = input.notes[index];
|
|
3081
|
-
if (crop === null || crop === void 0 || note === void 0 ||
|
|
3107
|
+
if (crop === null || crop === void 0 || note === void 0 || expired2)
|
|
3082
3108
|
continue;
|
|
3083
3109
|
const name = `note-${index + 1}.png`;
|
|
3084
3110
|
await writeFile3(join5(destination, name), crop.shot.png);
|
|
@@ -3092,7 +3118,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3092
3118
|
viewport: direction.frame.width
|
|
3093
3119
|
});
|
|
3094
3120
|
}
|
|
3095
|
-
if (input.compare !== null && !
|
|
3121
|
+
if (input.compare !== null && !expired2) {
|
|
3096
3122
|
const compareInput = {
|
|
3097
3123
|
url: previewUrl(input.origin, input.compare),
|
|
3098
3124
|
width: input.width,
|
|
@@ -3100,7 +3126,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3100
3126
|
signal: controller.signal
|
|
3101
3127
|
};
|
|
3102
3128
|
const comparison = await capture(browser, compareInput);
|
|
3103
|
-
if (
|
|
3129
|
+
if (expired2)
|
|
3104
3130
|
return;
|
|
3105
3131
|
await writeFile3(join5(destination, "compare.png"), comparison.frame.png);
|
|
3106
3132
|
captured.attachments.push({
|
|
@@ -3113,14 +3139,14 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3113
3139
|
});
|
|
3114
3140
|
}
|
|
3115
3141
|
} catch (error) {
|
|
3116
|
-
if (!
|
|
3142
|
+
if (!expired2) {
|
|
3117
3143
|
captured.skipped = error instanceof Error ? error.message : `The page did not load: ${String(error)}`;
|
|
3118
3144
|
}
|
|
3119
3145
|
}
|
|
3120
3146
|
})();
|
|
3121
3147
|
await Promise.race([work, deadline]);
|
|
3122
3148
|
clearTimeout(timer);
|
|
3123
|
-
if (
|
|
3149
|
+
if (expired2)
|
|
3124
3150
|
captured.skipped = "The design could not be captured in time.";
|
|
3125
3151
|
captured.attachments.push(...references);
|
|
3126
3152
|
return captured;
|
|
@@ -5407,10 +5433,14 @@ function createCoalescer(emit, options = {}) {
|
|
|
5407
5433
|
}
|
|
5408
5434
|
};
|
|
5409
5435
|
}
|
|
5410
|
-
function createLiveHub(
|
|
5436
|
+
function createLiveHub(options = {}) {
|
|
5411
5437
|
const listeners = /* @__PURE__ */ new Set();
|
|
5438
|
+
let viewers = 0;
|
|
5412
5439
|
const drop = (listener) => {
|
|
5413
|
-
listeners.delete(listener)
|
|
5440
|
+
if (!listeners.delete(listener) || !listener.viewer)
|
|
5441
|
+
return;
|
|
5442
|
+
viewers = Math.max(0, viewers - 1);
|
|
5443
|
+
options.onViewers?.(viewers);
|
|
5414
5444
|
};
|
|
5415
5445
|
const write2 = (listener, opcode, payload) => {
|
|
5416
5446
|
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
@@ -5495,7 +5525,7 @@ function createLiveHub(_options = {}) {
|
|
|
5495
5525
|
}
|
|
5496
5526
|
}
|
|
5497
5527
|
},
|
|
5498
|
-
upgrade: (req, socket, head) => {
|
|
5528
|
+
upgrade: (req, socket, head, upgradeOptions = {}) => {
|
|
5499
5529
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5500
5530
|
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
5501
5531
|
return false;
|
|
@@ -5517,8 +5547,16 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5517
5547
|
socket.destroy();
|
|
5518
5548
|
return false;
|
|
5519
5549
|
}
|
|
5520
|
-
const listener = {
|
|
5550
|
+
const listener = {
|
|
5551
|
+
socket,
|
|
5552
|
+
buffered: Buffer.alloc(0),
|
|
5553
|
+
viewer: upgradeOptions.viewer === true
|
|
5554
|
+
};
|
|
5521
5555
|
listeners.add(listener);
|
|
5556
|
+
if (listener.viewer) {
|
|
5557
|
+
viewers += 1;
|
|
5558
|
+
options.onViewers?.(viewers);
|
|
5559
|
+
}
|
|
5522
5560
|
socket.on("data", (chunk) => read(listener, chunk));
|
|
5523
5561
|
socket.once("error", () => drop(listener));
|
|
5524
5562
|
socket.once("end", () => drop(listener));
|
|
@@ -5536,10 +5574,1330 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5536
5574
|
},
|
|
5537
5575
|
get listening() {
|
|
5538
5576
|
return listeners.size;
|
|
5577
|
+
},
|
|
5578
|
+
get viewers() {
|
|
5579
|
+
return viewers;
|
|
5580
|
+
}
|
|
5581
|
+
};
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5584
|
+
// ../server/dist/share.js
|
|
5585
|
+
import { randomBytes as randomBytes4, randomUUID, timingSafeEqual } from "crypto";
|
|
5586
|
+
import http3 from "http";
|
|
5587
|
+
import { posix } from "path";
|
|
5588
|
+
|
|
5589
|
+
// ../server/dist/tunnel.js
|
|
5590
|
+
import { spawn as spawnChild } from "child_process";
|
|
5591
|
+
import { Resolver, lookup } from "dns/promises";
|
|
5592
|
+
import http2 from "http";
|
|
5593
|
+
import https from "https";
|
|
5594
|
+
var URL_DEADLINE_MS = 3e4;
|
|
5595
|
+
var PROBE_DEADLINE_MS = 3e4;
|
|
5596
|
+
var PROBE_INTERVAL_MS = 1500;
|
|
5597
|
+
var SLOW_PROBE_INTERVAL_MS = 4e3;
|
|
5598
|
+
var SLOW_PROBE_CAP_MS = 3e4;
|
|
5599
|
+
var STOP_GRACE_MS = 3e3;
|
|
5600
|
+
var STOP_LIMIT_MS = STOP_GRACE_MS + 2e3;
|
|
5601
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
5602
|
+
async function askLink(resolver, url, entryPath) {
|
|
5603
|
+
let target;
|
|
5604
|
+
try {
|
|
5605
|
+
target = new URL(url);
|
|
5606
|
+
} catch {
|
|
5607
|
+
return false;
|
|
5608
|
+
}
|
|
5609
|
+
let address;
|
|
5610
|
+
try {
|
|
5611
|
+
[address] = await resolver.resolve4(target.hostname);
|
|
5612
|
+
} catch {
|
|
5613
|
+
try {
|
|
5614
|
+
address = (await lookup(target.hostname, { family: 4 })).address;
|
|
5615
|
+
} catch {
|
|
5616
|
+
return false;
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
if (address === void 0)
|
|
5620
|
+
return false;
|
|
5621
|
+
const secure = target.protocol === "https:";
|
|
5622
|
+
return new Promise((resolve5) => {
|
|
5623
|
+
const request = (secure ? https : http2).request({
|
|
5624
|
+
host: address,
|
|
5625
|
+
port: Number(target.port || (secure ? 443 : 80)),
|
|
5626
|
+
path: entryPath,
|
|
5627
|
+
method: "GET",
|
|
5628
|
+
headers: { host: target.host },
|
|
5629
|
+
...secure ? { servername: target.hostname } : {},
|
|
5630
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
5631
|
+
}, (response) => {
|
|
5632
|
+
response.resume();
|
|
5633
|
+
const status = response.statusCode ?? 0;
|
|
5634
|
+
resolve5(status >= 200 && status < 400);
|
|
5635
|
+
});
|
|
5636
|
+
request.once("timeout", () => request.destroy());
|
|
5637
|
+
request.once("error", () => resolve5(false));
|
|
5638
|
+
request.end();
|
|
5639
|
+
});
|
|
5640
|
+
}
|
|
5641
|
+
async function detectTunnels(env = process.env) {
|
|
5642
|
+
const providers = ["cloudflared", "ngrok"];
|
|
5643
|
+
const found = await Promise.all(providers.map((provider) => pathLookup(provider, env).catch(() => false)));
|
|
5644
|
+
return providers.filter((_provider, index) => found[index] === true);
|
|
5645
|
+
}
|
|
5646
|
+
function duration(ms) {
|
|
5647
|
+
return ms % 1e3 === 0 ? `${ms / 1e3}s` : `${ms}ms`;
|
|
5648
|
+
}
|
|
5649
|
+
function withOutput(sentence, output) {
|
|
5650
|
+
if (output === "")
|
|
5651
|
+
return sentence;
|
|
5652
|
+
const stem = sentence.endsWith(".") ? sentence.slice(0, -1) : sentence;
|
|
5653
|
+
return `${stem} (${output.slice(0, 160)}).`;
|
|
5654
|
+
}
|
|
5655
|
+
function startTunnel(options, deps = {}) {
|
|
5656
|
+
const spawn4 = deps.spawn ?? spawnChild;
|
|
5657
|
+
const now = deps.now ?? Date.now;
|
|
5658
|
+
const urlDeadlineMs = deps.urlDeadlineMs ?? URL_DEADLINE_MS;
|
|
5659
|
+
const probeDeadlineMs = deps.probeDeadlineMs ?? PROBE_DEADLINE_MS;
|
|
5660
|
+
const resolver = deps.probe === void 0 ? new Resolver() : null;
|
|
5661
|
+
const probe2 = deps.probe ?? ((url2) => askLink(resolver, url2, options.entryPath));
|
|
5662
|
+
const timers = /* @__PURE__ */ new Set();
|
|
5663
|
+
const later = (callback, ms) => {
|
|
5664
|
+
const timer = setTimeout(() => {
|
|
5665
|
+
timers.delete(timer);
|
|
5666
|
+
callback();
|
|
5667
|
+
}, ms);
|
|
5668
|
+
timer.unref?.();
|
|
5669
|
+
timers.add(timer);
|
|
5670
|
+
return timer;
|
|
5671
|
+
};
|
|
5672
|
+
const clearTimers = () => {
|
|
5673
|
+
for (const timer of timers)
|
|
5674
|
+
clearTimeout(timer);
|
|
5675
|
+
timers.clear();
|
|
5676
|
+
};
|
|
5677
|
+
let lastState = "";
|
|
5678
|
+
let state = { status: "starting", provider: options.provider };
|
|
5679
|
+
let terminal = false;
|
|
5680
|
+
let stopping = false;
|
|
5681
|
+
let exited = false;
|
|
5682
|
+
let stopPromise = null;
|
|
5683
|
+
let settleStop = null;
|
|
5684
|
+
let url = null;
|
|
5685
|
+
let urlAt = 0;
|
|
5686
|
+
let urlTimer = null;
|
|
5687
|
+
let lastLine = "";
|
|
5688
|
+
const partial = { stdout: "", stderr: "" };
|
|
5689
|
+
const report = (next) => {
|
|
5690
|
+
const serialized = JSON.stringify(next);
|
|
5691
|
+
if (serialized === lastState)
|
|
5692
|
+
return;
|
|
5693
|
+
lastState = serialized;
|
|
5694
|
+
state = next;
|
|
5695
|
+
options.onState(next);
|
|
5696
|
+
};
|
|
5697
|
+
const fail = (reason) => {
|
|
5698
|
+
if (terminal)
|
|
5699
|
+
return;
|
|
5700
|
+
terminal = true;
|
|
5701
|
+
clearTimers();
|
|
5702
|
+
report({
|
|
5703
|
+
status: "failed",
|
|
5704
|
+
provider: options.provider,
|
|
5705
|
+
reason,
|
|
5706
|
+
...url === null ? {} : { url }
|
|
5707
|
+
});
|
|
5708
|
+
};
|
|
5709
|
+
report(state);
|
|
5710
|
+
const args = options.provider === "cloudflared" ? [
|
|
5711
|
+
"tunnel",
|
|
5712
|
+
"--url",
|
|
5713
|
+
`http://127.0.0.1:${options.port}`,
|
|
5714
|
+
"--no-autoupdate"
|
|
5715
|
+
] : ["http", String(options.port), "--log", "stdout", "--log-format", "json"];
|
|
5716
|
+
let child;
|
|
5717
|
+
try {
|
|
5718
|
+
child = spawn4(options.provider, args, {
|
|
5719
|
+
env: agentEnvironment(),
|
|
5720
|
+
shell: false,
|
|
5721
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5722
|
+
});
|
|
5723
|
+
} catch {
|
|
5724
|
+
fail(`${options.provider} exited before the tunnel came up.`);
|
|
5725
|
+
return { settle: () => {
|
|
5726
|
+
}, stop: async () => {
|
|
5727
|
+
} };
|
|
5728
|
+
}
|
|
5729
|
+
const beginProbe = (found) => {
|
|
5730
|
+
if (url !== null || terminal || stopping)
|
|
5731
|
+
return;
|
|
5732
|
+
url = found;
|
|
5733
|
+
urlAt = now();
|
|
5734
|
+
if (urlTimer !== null) {
|
|
5735
|
+
clearTimeout(urlTimer);
|
|
5736
|
+
timers.delete(urlTimer);
|
|
5737
|
+
urlTimer = null;
|
|
5738
|
+
}
|
|
5739
|
+
report({ status: "starting", provider: options.provider, url });
|
|
5740
|
+
later(() => {
|
|
5741
|
+
if (terminal || stopping || url === null)
|
|
5742
|
+
return;
|
|
5743
|
+
report({ status: "starting", provider: options.provider, url, slow: true });
|
|
5744
|
+
}, probeDeadlineMs);
|
|
5745
|
+
let slowWait = SLOW_PROBE_INTERVAL_MS;
|
|
5746
|
+
const again = () => {
|
|
5747
|
+
if (terminal || stopping || url === null)
|
|
5748
|
+
return;
|
|
5749
|
+
if (now() - urlAt < probeDeadlineMs) {
|
|
5750
|
+
later(poll, PROBE_INTERVAL_MS);
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5753
|
+
later(poll, slowWait);
|
|
5754
|
+
slowWait = Math.min(SLOW_PROBE_CAP_MS, slowWait * 2);
|
|
5755
|
+
};
|
|
5756
|
+
const poll = () => {
|
|
5757
|
+
if (terminal || stopping || url === null)
|
|
5758
|
+
return;
|
|
5759
|
+
void probe2(url).then((reachable) => {
|
|
5760
|
+
if (terminal || stopping || url === null)
|
|
5761
|
+
return;
|
|
5762
|
+
if (reachable) {
|
|
5763
|
+
terminal = true;
|
|
5764
|
+
clearTimers();
|
|
5765
|
+
report({ status: "ready", provider: options.provider, url });
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
again();
|
|
5769
|
+
}, again);
|
|
5770
|
+
};
|
|
5771
|
+
poll();
|
|
5772
|
+
};
|
|
5773
|
+
const inspect = (stream, line) => {
|
|
5774
|
+
const trimmed = line.trim();
|
|
5775
|
+
if (trimmed !== "")
|
|
5776
|
+
lastLine = trimmed;
|
|
5777
|
+
if (url !== null || trimmed === "")
|
|
5778
|
+
return;
|
|
5779
|
+
if (options.provider === "cloudflared") {
|
|
5780
|
+
const found = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/.exec(trimmed)?.[0];
|
|
5781
|
+
if (found !== void 0)
|
|
5782
|
+
beginProbe(found);
|
|
5783
|
+
return;
|
|
5784
|
+
}
|
|
5785
|
+
if (stream !== "stdout")
|
|
5786
|
+
return;
|
|
5787
|
+
try {
|
|
5788
|
+
const event = JSON.parse(trimmed);
|
|
5789
|
+
const candidate = typeof event.url === "string" && event.url.startsWith("https://") ? event.url : null;
|
|
5790
|
+
if (candidate !== null)
|
|
5791
|
+
beginProbe(candidate);
|
|
5792
|
+
} catch {
|
|
5793
|
+
}
|
|
5794
|
+
};
|
|
5795
|
+
const read = (stream, chunk) => {
|
|
5796
|
+
const combined = partial[stream] + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
|
|
5797
|
+
const lines2 = combined.split(/\r?\n/);
|
|
5798
|
+
partial[stream] = lines2.pop() ?? "";
|
|
5799
|
+
for (const line of lines2)
|
|
5800
|
+
inspect(stream, line);
|
|
5801
|
+
if (partial[stream] !== "")
|
|
5802
|
+
inspect(stream, partial[stream]);
|
|
5803
|
+
};
|
|
5804
|
+
child.stdout?.on("data", (chunk) => read("stdout", chunk));
|
|
5805
|
+
child.stderr?.on("data", (chunk) => read("stderr", chunk));
|
|
5806
|
+
const onExit = () => {
|
|
5807
|
+
if (exited)
|
|
5808
|
+
return;
|
|
5809
|
+
exited = true;
|
|
5810
|
+
clearTimers();
|
|
5811
|
+
settleStop?.();
|
|
5812
|
+
settleStop = null;
|
|
5813
|
+
if (stopping)
|
|
5814
|
+
return;
|
|
5815
|
+
if (state.status === "ready") {
|
|
5816
|
+
terminal = true;
|
|
5817
|
+
report({
|
|
5818
|
+
status: "failed",
|
|
5819
|
+
provider: options.provider,
|
|
5820
|
+
url: state.url,
|
|
5821
|
+
reason: "The tunnel process exited."
|
|
5822
|
+
});
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
fail(withOutput(`${options.provider} exited before the tunnel came up.`, lastLine));
|
|
5826
|
+
};
|
|
5827
|
+
child.once("error", onExit);
|
|
5828
|
+
child.once("exit", onExit);
|
|
5829
|
+
urlTimer = later(() => fail(withOutput(`${options.provider} did not report a URL within ${duration(urlDeadlineMs)}.`, lastLine)), urlDeadlineMs);
|
|
5830
|
+
return {
|
|
5831
|
+
settle() {
|
|
5832
|
+
if (terminal || stopping || url === null)
|
|
5833
|
+
return;
|
|
5834
|
+
terminal = true;
|
|
5835
|
+
clearTimers();
|
|
5836
|
+
report({ status: "ready", provider: options.provider, url });
|
|
5837
|
+
},
|
|
5838
|
+
stop() {
|
|
5839
|
+
if (stopPromise !== null)
|
|
5840
|
+
return stopPromise;
|
|
5841
|
+
stopping = true;
|
|
5842
|
+
clearTimers();
|
|
5843
|
+
if (exited)
|
|
5844
|
+
return Promise.resolve();
|
|
5845
|
+
stopPromise = new Promise((resolve5) => {
|
|
5846
|
+
let settled = false;
|
|
5847
|
+
const done = () => {
|
|
5848
|
+
if (settled)
|
|
5849
|
+
return;
|
|
5850
|
+
settled = true;
|
|
5851
|
+
clearTimers();
|
|
5852
|
+
settleStop = null;
|
|
5853
|
+
resolve5();
|
|
5854
|
+
};
|
|
5855
|
+
settleStop = done;
|
|
5856
|
+
try {
|
|
5857
|
+
child.kill("SIGTERM");
|
|
5858
|
+
} catch {
|
|
5859
|
+
done();
|
|
5860
|
+
return;
|
|
5861
|
+
}
|
|
5862
|
+
later(() => {
|
|
5863
|
+
if (exited)
|
|
5864
|
+
return done();
|
|
5865
|
+
try {
|
|
5866
|
+
child.kill("SIGKILL");
|
|
5867
|
+
} catch {
|
|
5868
|
+
}
|
|
5869
|
+
}, STOP_GRACE_MS);
|
|
5870
|
+
later(done, STOP_LIMIT_MS);
|
|
5871
|
+
});
|
|
5872
|
+
return stopPromise;
|
|
5539
5873
|
}
|
|
5540
5874
|
};
|
|
5541
5875
|
}
|
|
5542
5876
|
|
|
5877
|
+
// ../server/dist/share.js
|
|
5878
|
+
var MAX_REFUSED = 40;
|
|
5879
|
+
function routeAllowed(routes, url) {
|
|
5880
|
+
const [rawPath = "/"] = url.split("?", 2);
|
|
5881
|
+
const path = canonical(rawPath);
|
|
5882
|
+
return routes.some((route) => {
|
|
5883
|
+
const prefix = route.endsWith("/") && route !== "/";
|
|
5884
|
+
return prefix ? path.startsWith(route) || `${path}/` === route : path === route || path === `${route}/`;
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
5887
|
+
var OWN_PREFIX = "/leglas";
|
|
5888
|
+
var ENTRY_PREFIX = `${OWN_PREFIX}/s/`;
|
|
5889
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5890
|
+
var MAX_GRANTS = 16;
|
|
5891
|
+
var MAX_TOMBSTONES = 32;
|
|
5892
|
+
var VIEWER_CONCURRENCY = 12;
|
|
5893
|
+
var VIEWER_QUEUE = 128;
|
|
5894
|
+
var VIEWER_DEADLINE_MS = 3e4;
|
|
5895
|
+
var DEV_CONTROL_ROUTES = [
|
|
5896
|
+
/** read, Vite 8.2.2: opens `?file=` in the machine's editor. Rsbuild 2.2.3 too. */
|
|
5897
|
+
"/__open-in-editor",
|
|
5898
|
+
/** read, react-dev-utils 12.0.1: the same, older name. */
|
|
5899
|
+
"/__open-stack-frame-in-editor",
|
|
5900
|
+
/** read, react-dev-utils 12.0.1: serves a module's source through the overlay. */
|
|
5901
|
+
"/__get-internal-source",
|
|
5902
|
+
/** read, vite-plugin-inspect 12.0.2: the module graph and every transformed source. */
|
|
5903
|
+
"/__inspect",
|
|
5904
|
+
/** read, vite-plugin-vue-devtools 8.2.1: its whole interface and RPC surface. */
|
|
5905
|
+
"/__devtools__",
|
|
5906
|
+
/** read, browser-sync 3.0.4: its client surface and server metadata. */
|
|
5907
|
+
"/__browser_sync__",
|
|
5908
|
+
/**
|
|
5909
|
+
* read, webpack-dev-server 6.0.0, which mounts its own surface here: the
|
|
5910
|
+
* file listing, `/webpack-dev-server/invalidate`, which forces a rebuild,
|
|
5911
|
+
* and `/webpack-dev-server/open-editor`, which calls the same launch-editor
|
|
5912
|
+
* package Vite does. The subtree match below takes all three.
|
|
5913
|
+
*/
|
|
5914
|
+
"/webpack-dev-server",
|
|
5915
|
+
/** reported: Rails Web Console, an interactive server-side REPL. */
|
|
5916
|
+
"/__web_console",
|
|
5917
|
+
/** reported: the Better Errors gem, likewise. */
|
|
5918
|
+
"/__better_errors",
|
|
5919
|
+
/** reported: Laravel Ignition, whose solutions endpoint runs code. */
|
|
5920
|
+
"/_ignition",
|
|
5921
|
+
/** reported: Symfony's profiler, which serves traces, config and source. */
|
|
5922
|
+
"/_profiler",
|
|
5923
|
+
/** reported: Symfony's web debug toolbar. */
|
|
5924
|
+
"/_wdt",
|
|
5925
|
+
/** reported: Django Debug Toolbar, which serves settings, SQL and templates. */
|
|
5926
|
+
"/__debug__",
|
|
5927
|
+
/** reported: Go's pprof, where some GETs start expensive profiling. */
|
|
5928
|
+
"/debug/pprof",
|
|
5929
|
+
/** reported: Spring Boot Actuator, which can serve env, beans and heap dumps. */
|
|
5930
|
+
"/actuator",
|
|
5931
|
+
/** reported: Gatsby's development GraphQL surface, schema and content. */
|
|
5932
|
+
"/___graphql"
|
|
5933
|
+
];
|
|
5934
|
+
var DEV_CONTROL_PREFIXES = [
|
|
5935
|
+
/** read, Next 16.3.1. Its app assets sit at `/_next/` and stay allowed. */
|
|
5936
|
+
"/__nextjs_",
|
|
5937
|
+
/**
|
|
5938
|
+
* read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's own bundle is at `/_nuxt/`,
|
|
5939
|
+
* one underscore and a different prefix, so the app is untouched.
|
|
5940
|
+
*/
|
|
5941
|
+
"/__nuxt_devtools__",
|
|
5942
|
+
/**
|
|
5943
|
+
* read, Parcel 2.16.4: `__parcel_launch_editor` reads a `file` parameter
|
|
5944
|
+
* and calls the same launch-editor code Vite does. Beside it sit
|
|
5945
|
+
* `__parcel_source_map`, `__parcel_source_root` and `__parcel_code_frame`,
|
|
5946
|
+
* which serve source, and the HMR and health routes, which a viewer has no
|
|
5947
|
+
* use for: their live-reload socket is already refused.
|
|
5948
|
+
*/
|
|
5949
|
+
"/__parcel_"
|
|
5950
|
+
];
|
|
5951
|
+
var DEV_CONTROL_QUERY_KEYS = ["__debugger__"];
|
|
5952
|
+
function canonical(path) {
|
|
5953
|
+
let form = path;
|
|
5954
|
+
try {
|
|
5955
|
+
form = decodeURIComponent(path);
|
|
5956
|
+
} catch {
|
|
5957
|
+
}
|
|
5958
|
+
return posix.normalize(form.replaceAll("\\", "/").replace(/\/{2,}/g, "/"));
|
|
5959
|
+
}
|
|
5960
|
+
function spellings(path) {
|
|
5961
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5962
|
+
const add = (value) => {
|
|
5963
|
+
seen.add(value);
|
|
5964
|
+
seen.add(value.toLowerCase());
|
|
5965
|
+
};
|
|
5966
|
+
add(path);
|
|
5967
|
+
const forms = [path];
|
|
5968
|
+
try {
|
|
5969
|
+
forms.push(decodeURIComponent(path));
|
|
5970
|
+
} catch {
|
|
5971
|
+
}
|
|
5972
|
+
for (const value of forms) {
|
|
5973
|
+
add(value);
|
|
5974
|
+
for (const slashed of [value, value.replaceAll("\\", "/")]) {
|
|
5975
|
+
const collapsed = slashed.replace(/\/{2,}/g, "/");
|
|
5976
|
+
add(collapsed);
|
|
5977
|
+
add(posix.normalize(collapsed));
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
return [...seen];
|
|
5981
|
+
}
|
|
5982
|
+
function isDevControlRequest(url) {
|
|
5983
|
+
const [rawPath = "/", query] = url.split("?", 2);
|
|
5984
|
+
for (const path of spellings(rawPath)) {
|
|
5985
|
+
if (DEV_CONTROL_PREFIXES.some((prefix) => path.startsWith(prefix)))
|
|
5986
|
+
return true;
|
|
5987
|
+
if (DEV_CONTROL_ROUTES.some((route) => path === route || path.startsWith(`${route}/`))) {
|
|
5988
|
+
return true;
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
if (query === void 0)
|
|
5992
|
+
return false;
|
|
5993
|
+
const keys = new URLSearchParams(query);
|
|
5994
|
+
return DEV_CONTROL_QUERY_KEYS.some((key) => keys.has(key) || keys.has(key.toUpperCase()));
|
|
5995
|
+
}
|
|
5996
|
+
function isHiddenPath(path) {
|
|
5997
|
+
return spellings(path).some((form) => {
|
|
5998
|
+
const segments = form.split("/");
|
|
5999
|
+
const modules = segments.indexOf("node_modules");
|
|
6000
|
+
return segments.some((segment, at) => {
|
|
6001
|
+
if (!segment.startsWith(".") || segment === "." || segment === "..")
|
|
6002
|
+
return false;
|
|
6003
|
+
const last = at === segments.length - 1;
|
|
6004
|
+
return last || !(modules >= 0 && at > modules);
|
|
6005
|
+
});
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
var FILES_PREFIX_PATH = "/leglas/files/";
|
|
6009
|
+
var DETECT_TTL_MS = 1e4;
|
|
6010
|
+
function isRecord3(value) {
|
|
6011
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6012
|
+
}
|
|
6013
|
+
function stringArray(value) {
|
|
6014
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
6015
|
+
}
|
|
6016
|
+
function stringRecord(value) {
|
|
6017
|
+
return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
6018
|
+
}
|
|
6019
|
+
function layoutFrom(value) {
|
|
6020
|
+
if (!isRecord3(value))
|
|
6021
|
+
return null;
|
|
6022
|
+
if (!stringArray(value.order) || !stringRecord(value.renames) || !stringArray(value.collapsedFamilies) || value.compare !== null && typeof value.compare !== "string" || value.viewport !== null && (typeof value.viewport !== "number" || !Number.isFinite(value.viewport))) {
|
|
6023
|
+
return null;
|
|
6024
|
+
}
|
|
6025
|
+
return {
|
|
6026
|
+
order: [...value.order],
|
|
6027
|
+
renames: { ...value.renames },
|
|
6028
|
+
collapsedFamilies: [...value.collapsedFamilies],
|
|
6029
|
+
compare: value.compare,
|
|
6030
|
+
viewport: value.viewport
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
function manifestFrom(value, previews) {
|
|
6034
|
+
if (!isRecord3(value)) {
|
|
6035
|
+
return { ok: false, error: "Share details must be a JSON object." };
|
|
6036
|
+
}
|
|
6037
|
+
const scope2 = value.scope;
|
|
6038
|
+
const layout = layoutFrom(value.layout);
|
|
6039
|
+
if (scope2 !== "direction" && scope2 !== "compare" && scope2 !== "rail" || !stringArray(value.titles) || layout === null) {
|
|
6040
|
+
return { ok: false, error: "Share details need a scope, directions and a complete layout." };
|
|
6041
|
+
}
|
|
6042
|
+
const titles = [...value.titles];
|
|
6043
|
+
if (titles.length === 0) {
|
|
6044
|
+
return { ok: false, error: "Choose at least one direction to share." };
|
|
6045
|
+
}
|
|
6046
|
+
const byTitle = new Map(previews.map((preview) => [preview.title, preview]));
|
|
6047
|
+
const unknown = [...new Set(titles.filter((title) => !byTitle.has(title)))];
|
|
6048
|
+
if (unknown.length > 0) {
|
|
6049
|
+
return {
|
|
6050
|
+
ok: false,
|
|
6051
|
+
error: `Directions are not available to share: ${unknown.join(", ")}.`
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
const branches = [
|
|
6055
|
+
...new Set(titles.filter((title) => byTitle.get(title)?.branch !== void 0))
|
|
6056
|
+
];
|
|
6057
|
+
if (branches.length > 0) {
|
|
6058
|
+
return {
|
|
6059
|
+
ok: false,
|
|
6060
|
+
error: `Branch directions can't be shared yet: ${branches.join(", ")}.`
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
if (scope2 === "direction" && titles.length > 1) {
|
|
6064
|
+
return { ok: false, error: "A direction share can contain only one direction." };
|
|
6065
|
+
}
|
|
6066
|
+
if (scope2 === "compare" && (titles.length !== 2 || new Set(titles).size !== 2 || layout.compare === null || !titles.includes(layout.compare))) {
|
|
6067
|
+
return {
|
|
6068
|
+
ok: false,
|
|
6069
|
+
error: "A comparison share needs exactly two directions and one of them on the right."
|
|
6070
|
+
};
|
|
6071
|
+
}
|
|
6072
|
+
if (scope2 !== "compare" && layout.compare !== null) {
|
|
6073
|
+
return { ok: false, error: "Only a comparison share can name a right pane." };
|
|
6074
|
+
}
|
|
6075
|
+
const reach = value.reach === "listed" ? "listed" : "open";
|
|
6076
|
+
if (value.reach !== void 0 && value.reach !== "open" && value.reach !== "listed") {
|
|
6077
|
+
return { ok: false, error: "Reach is either open or listed." };
|
|
6078
|
+
}
|
|
6079
|
+
if (value.routes !== void 0 && !stringArray(value.routes)) {
|
|
6080
|
+
return { ok: false, error: "The route list must be an array of paths." };
|
|
6081
|
+
}
|
|
6082
|
+
const routes = [...new Set((value.routes ?? []).map((route) => route.split("?", 1)[0] ?? ""))].filter((route) => route !== "").slice(0, 400);
|
|
6083
|
+
if (routes.some((route) => !route.startsWith("/"))) {
|
|
6084
|
+
return { ok: false, error: "Every route must be a path beginning with a slash." };
|
|
6085
|
+
}
|
|
6086
|
+
const own = titles.flatMap((title) => {
|
|
6087
|
+
const url = byTitle.get(title)?.url;
|
|
6088
|
+
return url === void 0 ? [] : [url.split("?", 1)[0] ?? ""];
|
|
6089
|
+
});
|
|
6090
|
+
return {
|
|
6091
|
+
ok: true,
|
|
6092
|
+
manifest: { scope: scope2, titles, layout, reach, routes: [.../* @__PURE__ */ new Set([...routes, ...own])] }
|
|
6093
|
+
};
|
|
6094
|
+
}
|
|
6095
|
+
function matchOne(candidate, grants) {
|
|
6096
|
+
const received = Buffer.from(candidate, "utf8");
|
|
6097
|
+
let found = null;
|
|
6098
|
+
for (const grant of grants) {
|
|
6099
|
+
const expected = Buffer.from(grant.token, "utf8");
|
|
6100
|
+
if (expected.length !== received.length)
|
|
6101
|
+
continue;
|
|
6102
|
+
if (timingSafeEqual(expected, received))
|
|
6103
|
+
found = grant;
|
|
6104
|
+
}
|
|
6105
|
+
return found;
|
|
6106
|
+
}
|
|
6107
|
+
function grantFor(share, candidate) {
|
|
6108
|
+
return matchOne(candidate, share.grants.values());
|
|
6109
|
+
}
|
|
6110
|
+
function endedGrantFor(share, candidate) {
|
|
6111
|
+
return matchOne(candidate, share.tombstones);
|
|
6112
|
+
}
|
|
6113
|
+
function expired(grant, now, nowMono) {
|
|
6114
|
+
return now >= grant.expiresAt || nowMono >= grant.expiresAtMono;
|
|
6115
|
+
}
|
|
6116
|
+
function cookieToken(req) {
|
|
6117
|
+
const raw = req.headers.cookie;
|
|
6118
|
+
const cookies = (Array.isArray(raw) ? raw.join(";") : raw ?? "").split(";");
|
|
6119
|
+
for (const cookie of cookies) {
|
|
6120
|
+
const separator = cookie.indexOf("=");
|
|
6121
|
+
if (separator === -1)
|
|
6122
|
+
continue;
|
|
6123
|
+
if (cookie.slice(0, separator).trim() !== SHARE_COOKIE)
|
|
6124
|
+
continue;
|
|
6125
|
+
return cookie.slice(separator + 1).trim();
|
|
6126
|
+
}
|
|
6127
|
+
return null;
|
|
6128
|
+
}
|
|
6129
|
+
function forwardedProto(req) {
|
|
6130
|
+
const forwarded = req.headers["x-forwarded-proto"];
|
|
6131
|
+
const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
6132
|
+
return first?.split(",", 1)[0]?.trim() || "http";
|
|
6133
|
+
}
|
|
6134
|
+
function publicOrigin(req) {
|
|
6135
|
+
return `${forwardedProto(req)}://${req.headers.host ?? "127.0.0.1"}`;
|
|
6136
|
+
}
|
|
6137
|
+
function throughTunnel(req) {
|
|
6138
|
+
return req.headers["x-forwarded-for"] !== void 0 || req.headers["cf-connecting-ip"] !== void 0 || req.headers["x-forwarded-proto"] !== void 0;
|
|
6139
|
+
}
|
|
6140
|
+
function cloneLayout(layout) {
|
|
6141
|
+
return {
|
|
6142
|
+
...layout,
|
|
6143
|
+
order: [...layout.order],
|
|
6144
|
+
renames: { ...layout.renames },
|
|
6145
|
+
collapsedFamilies: [...layout.collapsedFamilies]
|
|
6146
|
+
};
|
|
6147
|
+
}
|
|
6148
|
+
function sendJson(res, status, body) {
|
|
6149
|
+
res.writeHead(status, {
|
|
6150
|
+
"content-type": "application/json; charset=utf-8",
|
|
6151
|
+
"cache-control": "no-store"
|
|
6152
|
+
});
|
|
6153
|
+
res.end(JSON.stringify(body));
|
|
6154
|
+
}
|
|
6155
|
+
var REFUSALS = {
|
|
6156
|
+
inactive: {
|
|
6157
|
+
status: 403,
|
|
6158
|
+
sentence: "This link isn't active.",
|
|
6159
|
+
title: "This Leglas link isn't active"
|
|
6160
|
+
},
|
|
6161
|
+
expiry: {
|
|
6162
|
+
status: 410,
|
|
6163
|
+
sentence: "This link expired. The person sharing it can send a new one.",
|
|
6164
|
+
title: "This Leglas link expired"
|
|
6165
|
+
},
|
|
6166
|
+
revoke: {
|
|
6167
|
+
status: 410,
|
|
6168
|
+
sentence: "This link was turned off.",
|
|
6169
|
+
title: "This Leglas link was turned off"
|
|
6170
|
+
}
|
|
6171
|
+
};
|
|
6172
|
+
function refuse(req, res, cause = "inactive") {
|
|
6173
|
+
const refusal = REFUSALS[cause];
|
|
6174
|
+
const accept = req.headers.accept;
|
|
6175
|
+
const html = (Array.isArray(accept) ? accept.join(",") : accept ?? "").includes("text/html");
|
|
6176
|
+
if (!html) {
|
|
6177
|
+
return sendJson(res, refusal.status, { ok: false, error: refusal.sentence });
|
|
6178
|
+
}
|
|
6179
|
+
res.writeHead(refusal.status, {
|
|
6180
|
+
"content-type": "text/html; charset=utf-8",
|
|
6181
|
+
"cache-control": "no-store"
|
|
6182
|
+
});
|
|
6183
|
+
res.end(`<!doctype html>
|
|
6184
|
+
<meta charset="utf-8">
|
|
6185
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6186
|
+
<title>${refusal.title}</title>
|
|
6187
|
+
<body style="margin:0;background:#1C1C20;color:#f5f4f1;font:16px/1.5 ui-sans-serif,system-ui;display:grid;min-height:100vh;place-items:center">
|
|
6188
|
+
<main style="max-width:34rem;padding:2rem"><h1 style="font-size:1.25rem">${refusal.title}</h1>
|
|
6189
|
+
<p>${refusal.sentence}</p></main>
|
|
6190
|
+
</body>`);
|
|
6191
|
+
}
|
|
6192
|
+
function bind(server) {
|
|
6193
|
+
return new Promise((resolve5, reject) => {
|
|
6194
|
+
const onError = (error) => {
|
|
6195
|
+
server.removeListener("listening", onListening);
|
|
6196
|
+
reject(error);
|
|
6197
|
+
};
|
|
6198
|
+
const onListening = () => {
|
|
6199
|
+
server.removeListener("error", onError);
|
|
6200
|
+
const address = server.address();
|
|
6201
|
+
resolve5(typeof address === "object" && address !== null ? address.port : 0);
|
|
6202
|
+
};
|
|
6203
|
+
server.once("error", onError);
|
|
6204
|
+
server.once("listening", onListening);
|
|
6205
|
+
server.listen(0, "127.0.0.1");
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
function closeListener(share) {
|
|
6209
|
+
return new Promise((resolve5) => {
|
|
6210
|
+
if (share.expiryTimer !== null) {
|
|
6211
|
+
clearTimeout(share.expiryTimer);
|
|
6212
|
+
share.expiryTimer = null;
|
|
6213
|
+
}
|
|
6214
|
+
for (const socket of share.sockets)
|
|
6215
|
+
socket.destroy();
|
|
6216
|
+
share.sockets.clear();
|
|
6217
|
+
share.server.closeAllConnections();
|
|
6218
|
+
share.server.close(() => resolve5());
|
|
6219
|
+
});
|
|
6220
|
+
}
|
|
6221
|
+
function createShareManager(options) {
|
|
6222
|
+
const detect = options.detectTunnels ?? detectTunnels;
|
|
6223
|
+
const runTunnel = options.startTunnel ?? startTunnel;
|
|
6224
|
+
const now = options.now ?? Date.now;
|
|
6225
|
+
const nowMono = options.nowMono ?? process.hrtime.bigint;
|
|
6226
|
+
const deadlineMs = options.viewerDeadlineMs ?? VIEWER_DEADLINE_MS;
|
|
6227
|
+
let detected = null;
|
|
6228
|
+
let active = null;
|
|
6229
|
+
let creating = false;
|
|
6230
|
+
let stops = 0;
|
|
6231
|
+
let closed = false;
|
|
6232
|
+
let stopPromise = null;
|
|
6233
|
+
let detectedAt = 0;
|
|
6234
|
+
const tunnels = () => {
|
|
6235
|
+
if (detected === null || Date.now() - detectedAt > DETECT_TTL_MS) {
|
|
6236
|
+
detectedAt = Date.now();
|
|
6237
|
+
detected = detect().catch(() => []);
|
|
6238
|
+
}
|
|
6239
|
+
return detected;
|
|
6240
|
+
};
|
|
6241
|
+
const status = () => {
|
|
6242
|
+
const share = active;
|
|
6243
|
+
if (share === null)
|
|
6244
|
+
return null;
|
|
6245
|
+
const tunnelUrl = "url" in share.tunnel ? share.tunnel.url : void 0;
|
|
6246
|
+
const origin = tunnelUrl === void 0 ? null : tunnelUrl.replace(/\/$/, "");
|
|
6247
|
+
return {
|
|
6248
|
+
id: share.id,
|
|
6249
|
+
scope: share.scope,
|
|
6250
|
+
titles: [...share.titles],
|
|
6251
|
+
layout: cloneLayout(share.layout),
|
|
6252
|
+
sharePort: share.port,
|
|
6253
|
+
grants: [...share.grants.values()].toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
|
|
6254
|
+
const entryPath = `${ENTRY_PREFIX}${grant.token}`;
|
|
6255
|
+
return {
|
|
6256
|
+
id: grant.id,
|
|
6257
|
+
name: grant.name,
|
|
6258
|
+
url: origin === null ? null : `${origin}${entryPath}`,
|
|
6259
|
+
localUrl: `http://127.0.0.1:${share.port}${entryPath}`,
|
|
6260
|
+
viewers: grant.viewers,
|
|
6261
|
+
createdAt: grant.createdAt,
|
|
6262
|
+
expiresAt: grant.expiresAt
|
|
6263
|
+
};
|
|
6264
|
+
}),
|
|
6265
|
+
reach: share.reach,
|
|
6266
|
+
routes: [...share.routes],
|
|
6267
|
+
refused: [...share.refused],
|
|
6268
|
+
tunnel: { ...share.tunnel },
|
|
6269
|
+
startedAt: share.startedAt
|
|
6270
|
+
};
|
|
6271
|
+
};
|
|
6272
|
+
const endGrant = (share, grant, why) => {
|
|
6273
|
+
if (!share.grants.delete(grant.id))
|
|
6274
|
+
return;
|
|
6275
|
+
grant.endedAt = now();
|
|
6276
|
+
grant.endedBy = why;
|
|
6277
|
+
grant.viewers = 0;
|
|
6278
|
+
share.tombstones.push(grant);
|
|
6279
|
+
while (share.tombstones.length > MAX_TOMBSTONES)
|
|
6280
|
+
share.tombstones.shift();
|
|
6281
|
+
for (const socket of share.grantSockets.get(grant.id) ?? [])
|
|
6282
|
+
socket.destroy();
|
|
6283
|
+
share.grantSockets.delete(grant.id);
|
|
6284
|
+
for (const held of share.grantRequests.get(grant.id) ?? []) {
|
|
6285
|
+
held.res.destroy();
|
|
6286
|
+
held.req.destroy();
|
|
6287
|
+
}
|
|
6288
|
+
share.grantRequests.delete(grant.id);
|
|
6289
|
+
for (const held of [...share.waiting.get(grant.id) ?? []]) {
|
|
6290
|
+
if (held.drop())
|
|
6291
|
+
refuse(held.req, held.res, why);
|
|
6292
|
+
}
|
|
6293
|
+
share.waiting.delete(grant.id);
|
|
6294
|
+
const turn = share.rota.indexOf(grant.id);
|
|
6295
|
+
if (turn >= 0)
|
|
6296
|
+
share.rota.splice(turn, 1);
|
|
6297
|
+
};
|
|
6298
|
+
const sweepExpiry = () => {
|
|
6299
|
+
const share = active;
|
|
6300
|
+
if (share === null)
|
|
6301
|
+
return;
|
|
6302
|
+
if (share.expiryTimer !== null) {
|
|
6303
|
+
clearTimeout(share.expiryTimer);
|
|
6304
|
+
share.expiryTimer = null;
|
|
6305
|
+
}
|
|
6306
|
+
const at = now();
|
|
6307
|
+
const mono = nowMono();
|
|
6308
|
+
let ended = false;
|
|
6309
|
+
for (const grant of [...share.grants.values()]) {
|
|
6310
|
+
if (!expired(grant, at, mono))
|
|
6311
|
+
continue;
|
|
6312
|
+
endGrant(share, grant, "expiry");
|
|
6313
|
+
ended = true;
|
|
6314
|
+
}
|
|
6315
|
+
const next = [...share.grants.values()].reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
|
|
6316
|
+
if (next !== null) {
|
|
6317
|
+
share.expiryTimer = setTimeout(sweepExpiry, Math.max(1, next - at));
|
|
6318
|
+
share.expiryTimer.unref?.();
|
|
6319
|
+
}
|
|
6320
|
+
if (ended)
|
|
6321
|
+
options.live.nudge("share");
|
|
6322
|
+
};
|
|
6323
|
+
const mintGrant = (share, name) => {
|
|
6324
|
+
let token = randomBytes4(24).toString("base64url");
|
|
6325
|
+
const taken = new Set([...share.grants.values(), ...share.tombstones].map((g) => g.token));
|
|
6326
|
+
while (taken.has(token))
|
|
6327
|
+
token = randomBytes4(24).toString("base64url");
|
|
6328
|
+
const at = now();
|
|
6329
|
+
const grant = {
|
|
6330
|
+
id: randomUUID(),
|
|
6331
|
+
name,
|
|
6332
|
+
token,
|
|
6333
|
+
createdAt: at,
|
|
6334
|
+
expiresAt: at + DEFAULT_TTL_MS,
|
|
6335
|
+
expiresAtMono: nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n,
|
|
6336
|
+
endedAt: null,
|
|
6337
|
+
endedBy: null,
|
|
6338
|
+
viewers: 0
|
|
6339
|
+
};
|
|
6340
|
+
share.grants.set(grant.id, grant);
|
|
6341
|
+
return grant;
|
|
6342
|
+
};
|
|
6343
|
+
const resolve5 = (share, candidate) => {
|
|
6344
|
+
const grant = grantFor(share, candidate);
|
|
6345
|
+
if (grant !== null) {
|
|
6346
|
+
if (!expired(grant, now(), nowMono()))
|
|
6347
|
+
return { grant };
|
|
6348
|
+
endGrant(share, grant, "expiry");
|
|
6349
|
+
options.live.nudge("share");
|
|
6350
|
+
return { refusal: "expiry" };
|
|
6351
|
+
}
|
|
6352
|
+
const ended = endedGrantFor(share, candidate);
|
|
6353
|
+
if (ended !== null)
|
|
6354
|
+
return { refusal: ended.endedBy === "revoke" ? "revoke" : "expiry" };
|
|
6355
|
+
return { refusal: "inactive" };
|
|
6356
|
+
};
|
|
6357
|
+
const pump = (share) => {
|
|
6358
|
+
while (share.running < VIEWER_CONCURRENCY && share.rota.length > 0) {
|
|
6359
|
+
const grantId = share.rota[0];
|
|
6360
|
+
if (grantId === void 0)
|
|
6361
|
+
return;
|
|
6362
|
+
const next = share.waiting.get(grantId)?.[0];
|
|
6363
|
+
if (next === void 0) {
|
|
6364
|
+
share.rota.shift();
|
|
6365
|
+
share.waiting.delete(grantId);
|
|
6366
|
+
continue;
|
|
6367
|
+
}
|
|
6368
|
+
next.drop();
|
|
6369
|
+
const turn = share.rota.indexOf(grantId);
|
|
6370
|
+
if (turn >= 0) {
|
|
6371
|
+
share.rota.splice(turn, 1);
|
|
6372
|
+
share.rota.push(grantId);
|
|
6373
|
+
}
|
|
6374
|
+
if (Date.now() >= next.spentAt) {
|
|
6375
|
+
next.shed();
|
|
6376
|
+
continue;
|
|
6377
|
+
}
|
|
6378
|
+
if (!share.grants.has(grantId)) {
|
|
6379
|
+
refuse(next.req, next.res, "revoke");
|
|
6380
|
+
continue;
|
|
6381
|
+
}
|
|
6382
|
+
next.start();
|
|
6383
|
+
}
|
|
6384
|
+
};
|
|
6385
|
+
const admit = (share, grant, req, res, run4) => {
|
|
6386
|
+
const queue = share.waiting.get(grant.id) ?? [];
|
|
6387
|
+
if (queue.length >= VIEWER_QUEUE) {
|
|
6388
|
+
return sendJson(res, 503, { ok: false, error: "Too much at once. Try again." });
|
|
6389
|
+
}
|
|
6390
|
+
let queued = true;
|
|
6391
|
+
let running = false;
|
|
6392
|
+
let over = false;
|
|
6393
|
+
const spentAt = Date.now() + deadlineMs;
|
|
6394
|
+
const shed = () => {
|
|
6395
|
+
if (over)
|
|
6396
|
+
return;
|
|
6397
|
+
over = true;
|
|
6398
|
+
sendJson(res, 503, { ok: false, error: "The dev server is busy. Try again." });
|
|
6399
|
+
};
|
|
6400
|
+
const deadline = setTimeout(() => {
|
|
6401
|
+
if (over)
|
|
6402
|
+
return;
|
|
6403
|
+
const waited = held.drop();
|
|
6404
|
+
if (running) {
|
|
6405
|
+
over = true;
|
|
6406
|
+
res.destroy();
|
|
6407
|
+
release();
|
|
6408
|
+
return;
|
|
6409
|
+
}
|
|
6410
|
+
if (waited)
|
|
6411
|
+
shed();
|
|
6412
|
+
}, deadlineMs);
|
|
6413
|
+
deadline.unref?.();
|
|
6414
|
+
const release = () => {
|
|
6415
|
+
if (!running)
|
|
6416
|
+
return;
|
|
6417
|
+
running = false;
|
|
6418
|
+
over = true;
|
|
6419
|
+
clearTimeout(deadline);
|
|
6420
|
+
share.running = Math.max(0, share.running - 1);
|
|
6421
|
+
pump(share);
|
|
6422
|
+
};
|
|
6423
|
+
const start = () => {
|
|
6424
|
+
running = true;
|
|
6425
|
+
share.running += 1;
|
|
6426
|
+
const writeHead = res.writeHead.bind(res);
|
|
6427
|
+
res.writeHead = ((...args) => {
|
|
6428
|
+
release();
|
|
6429
|
+
return writeHead(...args);
|
|
6430
|
+
});
|
|
6431
|
+
res.once("finish", release);
|
|
6432
|
+
res.once("close", release);
|
|
6433
|
+
run4();
|
|
6434
|
+
};
|
|
6435
|
+
const held = {
|
|
6436
|
+
req,
|
|
6437
|
+
res,
|
|
6438
|
+
grantId: grant.id,
|
|
6439
|
+
spentAt,
|
|
6440
|
+
start,
|
|
6441
|
+
shed,
|
|
6442
|
+
drop: () => {
|
|
6443
|
+
if (!queued)
|
|
6444
|
+
return false;
|
|
6445
|
+
queued = false;
|
|
6446
|
+
const rest = share.waiting.get(grant.id);
|
|
6447
|
+
const at = rest?.indexOf(held) ?? -1;
|
|
6448
|
+
if (rest !== void 0 && at >= 0)
|
|
6449
|
+
rest.splice(at, 1);
|
|
6450
|
+
if (rest !== void 0 && rest.length === 0) {
|
|
6451
|
+
share.waiting.delete(grant.id);
|
|
6452
|
+
const turn = share.rota.indexOf(grant.id);
|
|
6453
|
+
if (turn >= 0)
|
|
6454
|
+
share.rota.splice(turn, 1);
|
|
6455
|
+
}
|
|
6456
|
+
return true;
|
|
6457
|
+
}
|
|
6458
|
+
};
|
|
6459
|
+
queue.push(held);
|
|
6460
|
+
share.waiting.set(grant.id, queue);
|
|
6461
|
+
if (!share.rota.includes(grant.id))
|
|
6462
|
+
share.rota.push(grant.id);
|
|
6463
|
+
pump(share);
|
|
6464
|
+
if (!queued)
|
|
6465
|
+
return;
|
|
6466
|
+
res.once("close", () => {
|
|
6467
|
+
held.drop();
|
|
6468
|
+
if (running)
|
|
6469
|
+
return;
|
|
6470
|
+
over = true;
|
|
6471
|
+
clearTimeout(deadline);
|
|
6472
|
+
});
|
|
6473
|
+
};
|
|
6474
|
+
const request = (req, res) => {
|
|
6475
|
+
const share = active;
|
|
6476
|
+
if (share === null)
|
|
6477
|
+
return refuse(req, res);
|
|
6478
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6479
|
+
if (path.startsWith(ENTRY_PREFIX)) {
|
|
6480
|
+
const candidate = path.slice(ENTRY_PREFIX.length);
|
|
6481
|
+
if (req.method !== "GET" && req.method !== "HEAD" || candidate.includes("/")) {
|
|
6482
|
+
return refuse(req, res);
|
|
6483
|
+
}
|
|
6484
|
+
const found2 = resolve5(share, candidate);
|
|
6485
|
+
if ("refusal" in found2)
|
|
6486
|
+
return refuse(req, res, found2.refusal);
|
|
6487
|
+
const secure = forwardedProto(req) === "https" ? "; Secure" : "";
|
|
6488
|
+
res.writeHead(302, {
|
|
6489
|
+
location: "/leglas/",
|
|
6490
|
+
"set-cookie": `${SHARE_COOKIE}=${found2.grant.token}; Path=/; HttpOnly; SameSite=Lax${secure}`,
|
|
6491
|
+
"cache-control": "no-store"
|
|
6492
|
+
});
|
|
6493
|
+
res.end();
|
|
6494
|
+
return;
|
|
6495
|
+
}
|
|
6496
|
+
const cookie = cookieToken(req);
|
|
6497
|
+
if (cookie === null)
|
|
6498
|
+
return refuse(req, res);
|
|
6499
|
+
const found = resolve5(share, cookie);
|
|
6500
|
+
if ("refusal" in found)
|
|
6501
|
+
return refuse(req, res, found.refusal);
|
|
6502
|
+
const grant = found.grant;
|
|
6503
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
6504
|
+
return sendJson(res, 403, {
|
|
6505
|
+
ok: false,
|
|
6506
|
+
error: "Viewers can look, not change what runs."
|
|
6507
|
+
});
|
|
6508
|
+
}
|
|
6509
|
+
if (isDevControlRequest(req.url ?? "/")) {
|
|
6510
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6511
|
+
}
|
|
6512
|
+
if (isHiddenPath(path)) {
|
|
6513
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6514
|
+
}
|
|
6515
|
+
if (req.headers["sec-fetch-dest"] === "serviceworker") {
|
|
6516
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6517
|
+
}
|
|
6518
|
+
const url = req.url ?? "/";
|
|
6519
|
+
const interfaceOwn = spellings(path).every((form) => form === OWN_PREFIX || form.startsWith(`${OWN_PREFIX}/`));
|
|
6520
|
+
if (share.reach === "listed" && !interfaceOwn && !routeAllowed(share.routes, url)) {
|
|
6521
|
+
const asked = canonical(url.split("?", 1)[0] ?? "/");
|
|
6522
|
+
if (!share.refused.includes(asked)) {
|
|
6523
|
+
share.refused.push(asked);
|
|
6524
|
+
while (share.refused.length > MAX_REFUSED)
|
|
6525
|
+
share.refused.shift();
|
|
6526
|
+
options.live.nudge("share");
|
|
6527
|
+
}
|
|
6528
|
+
return sendJson(res, 403, { ok: false, error: "Not shared." });
|
|
6529
|
+
}
|
|
6530
|
+
const run4 = () => {
|
|
6531
|
+
const held = { req, res };
|
|
6532
|
+
const inFlight = share.grantRequests.get(grant.id) ?? /* @__PURE__ */ new Set();
|
|
6533
|
+
inFlight.add(held);
|
|
6534
|
+
share.grantRequests.set(grant.id, inFlight);
|
|
6535
|
+
let released = false;
|
|
6536
|
+
const release = () => {
|
|
6537
|
+
if (released)
|
|
6538
|
+
return;
|
|
6539
|
+
released = true;
|
|
6540
|
+
share.grantRequests.get(grant.id)?.delete(held);
|
|
6541
|
+
};
|
|
6542
|
+
res.once("finish", release);
|
|
6543
|
+
res.once("close", release);
|
|
6544
|
+
options.request(req, res, { publicOrigin: publicOrigin(req), grantId: grant.id });
|
|
6545
|
+
};
|
|
6546
|
+
if (interfaceOwn)
|
|
6547
|
+
return run4();
|
|
6548
|
+
admit(share, grant, req, res, run4);
|
|
6549
|
+
};
|
|
6550
|
+
const upgrade = (req, socket, head) => {
|
|
6551
|
+
const share = active;
|
|
6552
|
+
const cookie = cookieToken(req);
|
|
6553
|
+
if (share === null || cookie === null) {
|
|
6554
|
+
socket.destroy();
|
|
6555
|
+
return;
|
|
6556
|
+
}
|
|
6557
|
+
const found = resolve5(share, cookie);
|
|
6558
|
+
if ("refusal" in found) {
|
|
6559
|
+
socket.destroy();
|
|
6560
|
+
return;
|
|
6561
|
+
}
|
|
6562
|
+
const grant = found.grant;
|
|
6563
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6564
|
+
if (path !== LIVE_PATH) {
|
|
6565
|
+
socket.destroy();
|
|
6566
|
+
return;
|
|
6567
|
+
}
|
|
6568
|
+
if (throughTunnel(req))
|
|
6569
|
+
share.runningTunnel?.settle();
|
|
6570
|
+
if (!options.upgrade(req, socket, head))
|
|
6571
|
+
return;
|
|
6572
|
+
const held = share.grantSockets.get(grant.id) ?? /* @__PURE__ */ new Set();
|
|
6573
|
+
held.add(socket);
|
|
6574
|
+
share.grantSockets.set(grant.id, held);
|
|
6575
|
+
grant.viewers += 1;
|
|
6576
|
+
options.live.nudge("share");
|
|
6577
|
+
let gone = false;
|
|
6578
|
+
const letGo = () => {
|
|
6579
|
+
if (gone)
|
|
6580
|
+
return;
|
|
6581
|
+
gone = true;
|
|
6582
|
+
share.grantSockets.get(grant.id)?.delete(socket);
|
|
6583
|
+
grant.viewers = Math.max(0, grant.viewers - 1);
|
|
6584
|
+
options.live.nudge("share");
|
|
6585
|
+
};
|
|
6586
|
+
socket.once("close", letGo);
|
|
6587
|
+
socket.once("end", letGo);
|
|
6588
|
+
socket.once("error", letGo);
|
|
6589
|
+
};
|
|
6590
|
+
const create = async (input) => {
|
|
6591
|
+
if (closed)
|
|
6592
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6593
|
+
if (active !== null || creating) {
|
|
6594
|
+
return { ok: false, status: 409, error: "Stop the current share first." };
|
|
6595
|
+
}
|
|
6596
|
+
creating = true;
|
|
6597
|
+
const stopsAtStart = stops;
|
|
6598
|
+
try {
|
|
6599
|
+
const previews = await options.previews();
|
|
6600
|
+
const parsed = manifestFrom(input, previews);
|
|
6601
|
+
if (!parsed.ok)
|
|
6602
|
+
return { ok: false, status: 400, error: parsed.error };
|
|
6603
|
+
const providers = await tunnels();
|
|
6604
|
+
const requested = isRecord3(input) ? input.tunnel : void 0;
|
|
6605
|
+
if (requested !== void 0 && requested !== "none" && requested !== "cloudflared" && requested !== "ngrok") {
|
|
6606
|
+
return { ok: false, status: 400, error: "That tunnel provider is not supported." };
|
|
6607
|
+
}
|
|
6608
|
+
if (requested !== void 0 && requested !== "none" && !providers.includes(requested)) {
|
|
6609
|
+
return {
|
|
6610
|
+
ok: false,
|
|
6611
|
+
status: 400,
|
|
6612
|
+
error: `${requested} is not available on this machine.`
|
|
6613
|
+
};
|
|
6614
|
+
}
|
|
6615
|
+
const provider = requested ?? providers[0] ?? "none";
|
|
6616
|
+
if (closed)
|
|
6617
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6618
|
+
const server = http3.createServer(request);
|
|
6619
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
6620
|
+
server.on("connection", (socket) => {
|
|
6621
|
+
sockets.add(socket);
|
|
6622
|
+
socket.once("close", () => sockets.delete(socket));
|
|
6623
|
+
});
|
|
6624
|
+
server.on("upgrade", upgrade);
|
|
6625
|
+
let port;
|
|
6626
|
+
try {
|
|
6627
|
+
port = await bind(server);
|
|
6628
|
+
} catch (error) {
|
|
6629
|
+
return {
|
|
6630
|
+
ok: false,
|
|
6631
|
+
status: 500,
|
|
6632
|
+
error: `Leglas could not open a listener for the share (${error instanceof Error ? error.message : String(error)}).`
|
|
6633
|
+
};
|
|
6634
|
+
}
|
|
6635
|
+
if (closed) {
|
|
6636
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
6637
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6638
|
+
}
|
|
6639
|
+
if (stops !== stopsAtStart) {
|
|
6640
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
6641
|
+
return { ok: false, status: 409, error: "Sharing was stopped while it was starting." };
|
|
6642
|
+
}
|
|
6643
|
+
const share = {
|
|
6644
|
+
...parsed.manifest,
|
|
6645
|
+
id: randomUUID(),
|
|
6646
|
+
grants: /* @__PURE__ */ new Map(),
|
|
6647
|
+
tombstones: [],
|
|
6648
|
+
port,
|
|
6649
|
+
startedAt: now(),
|
|
6650
|
+
tunnel: provider === "none" ? { status: "none" } : { status: "starting", provider },
|
|
6651
|
+
runningTunnel: null,
|
|
6652
|
+
tunnelGeneration: 0,
|
|
6653
|
+
server,
|
|
6654
|
+
sockets,
|
|
6655
|
+
grantSockets: /* @__PURE__ */ new Map(),
|
|
6656
|
+
grantRequests: /* @__PURE__ */ new Map(),
|
|
6657
|
+
launch: null,
|
|
6658
|
+
expiryTimer: null,
|
|
6659
|
+
refused: [],
|
|
6660
|
+
running: 0,
|
|
6661
|
+
waiting: /* @__PURE__ */ new Map(),
|
|
6662
|
+
rota: []
|
|
6663
|
+
};
|
|
6664
|
+
active = share;
|
|
6665
|
+
mintGrant(share, "");
|
|
6666
|
+
sweepExpiry();
|
|
6667
|
+
options.live.nudge("share");
|
|
6668
|
+
if (provider !== "none") {
|
|
6669
|
+
share.launch = setImmediate(() => {
|
|
6670
|
+
share.launch = null;
|
|
6671
|
+
if (active !== share)
|
|
6672
|
+
return;
|
|
6673
|
+
share.runningTunnel = runTunnel({
|
|
6674
|
+
provider,
|
|
6675
|
+
port,
|
|
6676
|
+
// Whichever link exists when the tunnel starts: the probe only
|
|
6677
|
+
// needs a path the listener answers, and a share always has one.
|
|
6678
|
+
entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
|
|
6679
|
+
onState: (next) => {
|
|
6680
|
+
if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
6681
|
+
return;
|
|
6682
|
+
share.tunnel = next;
|
|
6683
|
+
options.live.nudge("share");
|
|
6684
|
+
}
|
|
6685
|
+
});
|
|
6686
|
+
});
|
|
6687
|
+
share.launch.unref?.();
|
|
6688
|
+
}
|
|
6689
|
+
return { ok: true, share: status() };
|
|
6690
|
+
} finally {
|
|
6691
|
+
creating = false;
|
|
6692
|
+
}
|
|
6693
|
+
};
|
|
6694
|
+
const createGrant = (input) => {
|
|
6695
|
+
const share = active;
|
|
6696
|
+
if (share === null)
|
|
6697
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6698
|
+
const name = isRecord3(input) && typeof input.name === "string" ? input.name.trim() : "";
|
|
6699
|
+
if (name.length > 60) {
|
|
6700
|
+
return { ok: false, status: 400, error: "That name is too long for a link." };
|
|
6701
|
+
}
|
|
6702
|
+
sweepExpiry();
|
|
6703
|
+
if (share.grants.size >= MAX_GRANTS) {
|
|
6704
|
+
return {
|
|
6705
|
+
ok: false,
|
|
6706
|
+
status: 409,
|
|
6707
|
+
error: `A share can hold ${MAX_GRANTS} links. Revoke one to make another.`
|
|
6708
|
+
};
|
|
6709
|
+
}
|
|
6710
|
+
mintGrant(share, name);
|
|
6711
|
+
sweepExpiry();
|
|
6712
|
+
options.live.nudge("share");
|
|
6713
|
+
return { ok: true, share: status() };
|
|
6714
|
+
};
|
|
6715
|
+
const revokeGrant = (input) => {
|
|
6716
|
+
const share = active;
|
|
6717
|
+
if (share === null)
|
|
6718
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6719
|
+
const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
|
|
6720
|
+
const grant = share.grants.get(id);
|
|
6721
|
+
if (grant === void 0)
|
|
6722
|
+
return { ok: false, status: 404, error: "No such link." };
|
|
6723
|
+
endGrant(share, grant, "revoke");
|
|
6724
|
+
sweepExpiry();
|
|
6725
|
+
options.live.nudge("share");
|
|
6726
|
+
return { ok: true, share: status() };
|
|
6727
|
+
};
|
|
6728
|
+
const extendGrant = (input) => {
|
|
6729
|
+
const share = active;
|
|
6730
|
+
if (share === null)
|
|
6731
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6732
|
+
const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
|
|
6733
|
+
sweepExpiry();
|
|
6734
|
+
const grant = share.grants.get(id);
|
|
6735
|
+
if (grant === void 0) {
|
|
6736
|
+
return { ok: false, status: 404, error: "That link has ended. Make a new one." };
|
|
6737
|
+
}
|
|
6738
|
+
const at = now();
|
|
6739
|
+
grant.expiresAt = at + DEFAULT_TTL_MS;
|
|
6740
|
+
grant.expiresAtMono = nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n;
|
|
6741
|
+
sweepExpiry();
|
|
6742
|
+
options.live.nudge("share");
|
|
6743
|
+
return { ok: true, share: status() };
|
|
6744
|
+
};
|
|
6745
|
+
const rotate = async () => {
|
|
6746
|
+
const share = active;
|
|
6747
|
+
if (share === null)
|
|
6748
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6749
|
+
for (const grant of [...share.grants.values()])
|
|
6750
|
+
endGrant(share, grant, "revoke");
|
|
6751
|
+
const provider = "provider" in share.tunnel ? share.tunnel.provider : null;
|
|
6752
|
+
await share.runningTunnel?.stop().catch(() => {
|
|
6753
|
+
});
|
|
6754
|
+
share.runningTunnel = null;
|
|
6755
|
+
if (active !== share)
|
|
6756
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6757
|
+
mintGrant(share, "");
|
|
6758
|
+
sweepExpiry();
|
|
6759
|
+
if (provider !== null) {
|
|
6760
|
+
share.tunnelGeneration += 1;
|
|
6761
|
+
const generation = share.tunnelGeneration;
|
|
6762
|
+
share.tunnel = { status: "starting", provider };
|
|
6763
|
+
share.runningTunnel = runTunnel({
|
|
6764
|
+
provider,
|
|
6765
|
+
port: share.port,
|
|
6766
|
+
entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
|
|
6767
|
+
onState: (next) => {
|
|
6768
|
+
if (active !== share || share.tunnelGeneration !== generation)
|
|
6769
|
+
return;
|
|
6770
|
+
if (JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
6771
|
+
return;
|
|
6772
|
+
share.tunnel = next;
|
|
6773
|
+
options.live.nudge("share");
|
|
6774
|
+
}
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
6777
|
+
options.live.nudge("share");
|
|
6778
|
+
return { ok: true, share: status() };
|
|
6779
|
+
};
|
|
6780
|
+
const allowRoute = (input) => {
|
|
6781
|
+
const share = active;
|
|
6782
|
+
if (share === null)
|
|
6783
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6784
|
+
const given = isRecord3(input) && typeof input.path === "string" ? input.path.trim() : "";
|
|
6785
|
+
if (!given.startsWith("/")) {
|
|
6786
|
+
return { ok: false, status: 400, error: "A route is a path beginning with a slash." };
|
|
6787
|
+
}
|
|
6788
|
+
const subtree = isRecord3(input) && input.subtree === true;
|
|
6789
|
+
if (subtree && given.replace(/\/+$/, "") === "") {
|
|
6790
|
+
return { ok: false, status: 400, error: "The root is a page, not a folder." };
|
|
6791
|
+
}
|
|
6792
|
+
const asked = subtree ? `${given.replace(/\/+$/, "")}/` : given === "/" ? given : given.replace(/\/+$/, "");
|
|
6793
|
+
if (share.routes.length >= 400) {
|
|
6794
|
+
return { ok: false, status: 409, error: "That share is holding as many routes as it can." };
|
|
6795
|
+
}
|
|
6796
|
+
if (!share.routes.includes(asked))
|
|
6797
|
+
share.routes.push(asked);
|
|
6798
|
+
share.refused = share.refused.filter((path) => !routeAllowed([asked], path));
|
|
6799
|
+
options.live.nudge("share");
|
|
6800
|
+
return { ok: true, share: status() };
|
|
6801
|
+
};
|
|
6802
|
+
const update = async (input) => {
|
|
6803
|
+
const share = active;
|
|
6804
|
+
if (share === null) {
|
|
6805
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6806
|
+
}
|
|
6807
|
+
const parsed = manifestFrom(input, await options.previews());
|
|
6808
|
+
if (!parsed.ok)
|
|
6809
|
+
return { ok: false, status: 400, error: parsed.error };
|
|
6810
|
+
if (active !== share) {
|
|
6811
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6812
|
+
}
|
|
6813
|
+
share.scope = parsed.manifest.scope;
|
|
6814
|
+
share.titles = parsed.manifest.titles;
|
|
6815
|
+
share.layout = parsed.manifest.layout;
|
|
6816
|
+
options.live.nudge("share");
|
|
6817
|
+
options.live.nudge("config");
|
|
6818
|
+
return { ok: true, share: status() };
|
|
6819
|
+
};
|
|
6820
|
+
const viewerConfig = async (grantId) => {
|
|
6821
|
+
const share = active;
|
|
6822
|
+
if (share === null || !share.grants.has(grantId))
|
|
6823
|
+
return null;
|
|
6824
|
+
const titles = new Set(share.titles);
|
|
6825
|
+
const previews = (await options.previews()).filter((preview) => titles.has(preview.title));
|
|
6826
|
+
if (active !== share)
|
|
6827
|
+
return null;
|
|
6828
|
+
return {
|
|
6829
|
+
...options.viewerConfig,
|
|
6830
|
+
// The project id is the config's absolute path, which keys the sharer's
|
|
6831
|
+
// saved layout and names their machine's directories, and the dev
|
|
6832
|
+
// server's address names their network. A viewer keeps no layout and
|
|
6833
|
+
// never dials the dev server, so the share's own id does the job and
|
|
6834
|
+
// the address is left blank.
|
|
6835
|
+
project: `share:${share.id}`,
|
|
6836
|
+
devServer: "",
|
|
6837
|
+
previews: options.previewsForConfig(previews),
|
|
6838
|
+
errors: [],
|
|
6839
|
+
warnings: [],
|
|
6840
|
+
viewer: { scope: share.scope, layout: cloneLayout(share.layout) }
|
|
6841
|
+
};
|
|
6842
|
+
};
|
|
6843
|
+
const fileSlugAllowed = async (slug, grantId) => {
|
|
6844
|
+
const share = active;
|
|
6845
|
+
if (share === null || !share.grants.has(grantId))
|
|
6846
|
+
return false;
|
|
6847
|
+
const titles = new Set(share.titles);
|
|
6848
|
+
const previews = await options.previews();
|
|
6849
|
+
return previews.some((preview) => {
|
|
6850
|
+
if (!titles.has(preview.title) || preview.file === void 0)
|
|
6851
|
+
return false;
|
|
6852
|
+
const rest = preview.url.startsWith(FILES_PREFIX_PATH) ? preview.url.slice(FILES_PREFIX_PATH.length) : "";
|
|
6853
|
+
const slash = rest.indexOf("/");
|
|
6854
|
+
return (slash === -1 ? rest : rest.slice(0, slash)) === slug;
|
|
6855
|
+
});
|
|
6856
|
+
};
|
|
6857
|
+
const stop = () => {
|
|
6858
|
+
stops += 1;
|
|
6859
|
+
if (stopPromise !== null)
|
|
6860
|
+
return stopPromise;
|
|
6861
|
+
const share = active;
|
|
6862
|
+
if (share === null)
|
|
6863
|
+
return Promise.resolve();
|
|
6864
|
+
if (share.launch !== null) {
|
|
6865
|
+
clearImmediate(share.launch);
|
|
6866
|
+
share.launch = null;
|
|
6867
|
+
}
|
|
6868
|
+
stopPromise = (async () => {
|
|
6869
|
+
await share.runningTunnel?.stop().catch(() => {
|
|
6870
|
+
});
|
|
6871
|
+
await closeListener(share);
|
|
6872
|
+
if (active === share)
|
|
6873
|
+
active = null;
|
|
6874
|
+
options.live.nudge("share");
|
|
6875
|
+
})().finally(() => {
|
|
6876
|
+
stopPromise = null;
|
|
6877
|
+
});
|
|
6878
|
+
return stopPromise;
|
|
6879
|
+
};
|
|
6880
|
+
const close = async () => {
|
|
6881
|
+
closed = true;
|
|
6882
|
+
await stop();
|
|
6883
|
+
};
|
|
6884
|
+
return {
|
|
6885
|
+
tunnels,
|
|
6886
|
+
status,
|
|
6887
|
+
fileSlugAllowed,
|
|
6888
|
+
allowRoute,
|
|
6889
|
+
create,
|
|
6890
|
+
createGrant,
|
|
6891
|
+
revokeGrant,
|
|
6892
|
+
extendGrant,
|
|
6893
|
+
rotate,
|
|
6894
|
+
update,
|
|
6895
|
+
viewerConfig,
|
|
6896
|
+
stop,
|
|
6897
|
+
close
|
|
6898
|
+
};
|
|
6899
|
+
}
|
|
6900
|
+
|
|
5543
6901
|
// ../server/dist/renames.js
|
|
5544
6902
|
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
5545
6903
|
import { dirname as dirname6, join as join10 } from "path";
|
|
@@ -5576,7 +6934,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
5576
6934
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5577
6935
|
import { createHash as createHash2 } from "crypto";
|
|
5578
6936
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5579
|
-
import
|
|
6937
|
+
import http4 from "http";
|
|
5580
6938
|
import net3 from "net";
|
|
5581
6939
|
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5582
6940
|
|
|
@@ -5645,7 +7003,7 @@ var CONTENT_TYPES = {
|
|
|
5645
7003
|
".woff2": "font/woff2"
|
|
5646
7004
|
};
|
|
5647
7005
|
var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
|
|
5648
|
-
function
|
|
7006
|
+
function sendJson2(res, status, body) {
|
|
5649
7007
|
const payload = JSON.stringify(body);
|
|
5650
7008
|
res.writeHead(status, {
|
|
5651
7009
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -5982,7 +7340,8 @@ function watchHealth(target, live) {
|
|
|
5982
7340
|
close: () => {
|
|
5983
7341
|
closed = true;
|
|
5984
7342
|
clearInterval(timer);
|
|
5985
|
-
}
|
|
7343
|
+
},
|
|
7344
|
+
reachable: () => previous
|
|
5986
7345
|
};
|
|
5987
7346
|
}
|
|
5988
7347
|
function snapshotConfig(cwd) {
|
|
@@ -6039,7 +7398,7 @@ function listen(server, port) {
|
|
|
6039
7398
|
server.listen(port, "127.0.0.1");
|
|
6040
7399
|
});
|
|
6041
7400
|
}
|
|
6042
|
-
async function
|
|
7401
|
+
async function bind2(server, requested) {
|
|
6043
7402
|
if (requested === 0)
|
|
6044
7403
|
return listen(server, 0);
|
|
6045
7404
|
for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt += 1) {
|
|
@@ -6055,6 +7414,8 @@ async function bind(server, requested) {
|
|
|
6055
7414
|
async function startServer(options) {
|
|
6056
7415
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
6057
7416
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7417
|
+
let shares = null;
|
|
7418
|
+
let liveHealth = null;
|
|
6058
7419
|
const live = options.live ?? createLiveHub();
|
|
6059
7420
|
const branches = createBranchRegistry({
|
|
6060
7421
|
cwd,
|
|
@@ -6129,12 +7490,136 @@ async function startServer(options) {
|
|
|
6129
7490
|
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
6130
7491
|
void probeAgents().catch(() => {
|
|
6131
7492
|
});
|
|
6132
|
-
const
|
|
7493
|
+
const readShareBody = (req, res, run4) => {
|
|
7494
|
+
let body = "";
|
|
7495
|
+
req.on("data", (chunk) => body += chunk);
|
|
7496
|
+
req.on("end", () => {
|
|
7497
|
+
const parsed = jsonBody(body);
|
|
7498
|
+
if (parsed === null)
|
|
7499
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7500
|
+
void Promise.resolve(run4(parsed)).then((result) => {
|
|
7501
|
+
if (result === void 0) {
|
|
7502
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7503
|
+
}
|
|
7504
|
+
return result.ok ? sendJson2(res, 200, result) : sendJson2(res, result.status, { ok: false, error: result.error });
|
|
7505
|
+
});
|
|
7506
|
+
});
|
|
7507
|
+
};
|
|
7508
|
+
const handleRequest = (req, res, context) => {
|
|
6133
7509
|
const url = req.url ?? "/";
|
|
6134
7510
|
const path = url.split("?")[0] ?? "/";
|
|
6135
7511
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
6136
|
-
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
6137
|
-
return
|
|
7512
|
+
if (!context.remote && req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7513
|
+
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7514
|
+
}
|
|
7515
|
+
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
7516
|
+
return void shares?.viewerConfig(context.grantId ?? "").then((payload) => {
|
|
7517
|
+
if (payload === null) {
|
|
7518
|
+
return sendJson2(res, 403, { ok: false, error: "This link isn't active." });
|
|
7519
|
+
}
|
|
7520
|
+
sendConditionalJson(req, res, payload);
|
|
7521
|
+
});
|
|
7522
|
+
}
|
|
7523
|
+
if (context.remote && path === `${LEGLAS_PREFIX}/api/health`) {
|
|
7524
|
+
const known = liveHealth?.reachable() ?? null;
|
|
7525
|
+
return void (known === null ? probe(target) : Promise.resolve(known)).then((reachable) => sendConditionalJson(req, res, { reachable }));
|
|
7526
|
+
}
|
|
7527
|
+
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7528
|
+
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7529
|
+
}
|
|
7530
|
+
if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
|
|
7531
|
+
const updates = options.updates;
|
|
7532
|
+
if (updates === void 0) {
|
|
7533
|
+
return sendJson2(res, 404, { ok: false, error: "Updates are not available here." });
|
|
7534
|
+
}
|
|
7535
|
+
if (req.method === "GET")
|
|
7536
|
+
return sendJson2(res, 200, updates.status());
|
|
7537
|
+
if (path.endsWith("/check")) {
|
|
7538
|
+
return void updates.check({ force: true }).then((status) => sendJson2(res, 200, status));
|
|
7539
|
+
}
|
|
7540
|
+
if (path.endsWith("/install")) {
|
|
7541
|
+
return void updates.update().then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 409, {
|
|
7542
|
+
ok: false,
|
|
7543
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7544
|
+
}));
|
|
7545
|
+
}
|
|
7546
|
+
if (!hasJsonBody(req))
|
|
7547
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7548
|
+
let body = "";
|
|
7549
|
+
req.on("data", (chunk) => body += chunk);
|
|
7550
|
+
return void req.on("end", () => {
|
|
7551
|
+
const parsed = jsonBody(body);
|
|
7552
|
+
if (parsed === null)
|
|
7553
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7554
|
+
if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
|
|
7555
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a version." });
|
|
7556
|
+
}
|
|
7557
|
+
void updates.skip(parsed.version).then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 400, {
|
|
7558
|
+
ok: false,
|
|
7559
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7560
|
+
}));
|
|
7561
|
+
});
|
|
7562
|
+
}
|
|
7563
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "GET") {
|
|
7564
|
+
return void shares?.tunnels().then((tunnels) => sendJson2(res, 200, { share: shares?.status() ?? null, tunnels }));
|
|
7565
|
+
}
|
|
7566
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "POST") {
|
|
7567
|
+
if (!hasJsonBody(req)) {
|
|
7568
|
+
return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
|
|
7569
|
+
}
|
|
7570
|
+
let body = "";
|
|
7571
|
+
req.on("data", (chunk) => body += chunk);
|
|
7572
|
+
return void req.on("end", async () => {
|
|
7573
|
+
const parsed = jsonBody(body);
|
|
7574
|
+
if (parsed === null) {
|
|
7575
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7576
|
+
}
|
|
7577
|
+
const result = await shares?.create(parsed).catch((error) => ({
|
|
7578
|
+
ok: false,
|
|
7579
|
+
status: 500,
|
|
7580
|
+
error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
|
|
7581
|
+
}));
|
|
7582
|
+
if (result === void 0) {
|
|
7583
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7584
|
+
}
|
|
7585
|
+
return result.ok ? sendJson2(res, 200, result) : sendJson2(res, result.status, { ok: false, error: result.error });
|
|
7586
|
+
});
|
|
7587
|
+
}
|
|
7588
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/update` && req.method === "POST") {
|
|
7589
|
+
if (!hasJsonBody(req)) {
|
|
7590
|
+
return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
|
|
7591
|
+
}
|
|
7592
|
+
let body = "";
|
|
7593
|
+
req.on("data", (chunk) => body += chunk);
|
|
7594
|
+
return void req.on("end", async () => {
|
|
7595
|
+
const parsed = jsonBody(body);
|
|
7596
|
+
if (parsed === null) {
|
|
7597
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7598
|
+
}
|
|
7599
|
+
const result = await shares?.update(parsed);
|
|
7600
|
+
if (result === void 0) {
|
|
7601
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7602
|
+
}
|
|
7603
|
+
return result.ok ? sendJson2(res, 200, result) : sendJson2(res, result.status, { ok: false, error: result.error });
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants` && req.method === "POST") {
|
|
7607
|
+
return void readShareBody(req, res, (body) => shares?.createGrant(body));
|
|
7608
|
+
}
|
|
7609
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/revoke` && req.method === "POST") {
|
|
7610
|
+
return void readShareBody(req, res, (body) => shares?.revokeGrant(body));
|
|
7611
|
+
}
|
|
7612
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/extend` && req.method === "POST") {
|
|
7613
|
+
return void readShareBody(req, res, (body) => shares?.extendGrant(body));
|
|
7614
|
+
}
|
|
7615
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/allow` && req.method === "POST") {
|
|
7616
|
+
return void readShareBody(req, res, (body) => shares?.allowRoute(body));
|
|
7617
|
+
}
|
|
7618
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/rotate` && req.method === "POST") {
|
|
7619
|
+
return void readShareBody(req, res, () => shares?.rotate());
|
|
7620
|
+
}
|
|
7621
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/stop` && req.method === "POST") {
|
|
7622
|
+
return void (shares?.stop() ?? Promise.resolve()).then(() => sendJson2(res, 200, { ok: true }));
|
|
6138
7623
|
}
|
|
6139
7624
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
6140
7625
|
const boot = config?.previews ?? [];
|
|
@@ -6180,23 +7665,23 @@ async function startServer(options) {
|
|
|
6180
7665
|
return void req.on("end", async () => {
|
|
6181
7666
|
const parsed = jsonBody(body);
|
|
6182
7667
|
if (parsed === null) {
|
|
6183
|
-
return
|
|
7668
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6184
7669
|
}
|
|
6185
7670
|
if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
|
|
6186
|
-
return
|
|
7671
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
|
|
6187
7672
|
}
|
|
6188
7673
|
const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
|
|
6189
7674
|
if (preview === void 0) {
|
|
6190
|
-
return
|
|
7675
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
6191
7676
|
}
|
|
6192
7677
|
if (preview.branch === void 0) {
|
|
6193
|
-
return
|
|
7678
|
+
return sendJson2(res, 400, {
|
|
6194
7679
|
ok: false,
|
|
6195
7680
|
error: `"${preview.title}" is not a branch preview.`
|
|
6196
7681
|
});
|
|
6197
7682
|
}
|
|
6198
7683
|
if (config?.devCommand === void 0) {
|
|
6199
|
-
return
|
|
7684
|
+
return sendJson2(res, 400, {
|
|
6200
7685
|
ok: false,
|
|
6201
7686
|
error: `"${preview.title}" cannot start because the config sets no devCommand.`
|
|
6202
7687
|
});
|
|
@@ -6204,9 +7689,9 @@ async function startServer(options) {
|
|
|
6204
7689
|
void branches.start(preview.title);
|
|
6205
7690
|
const state = branches.state(preview.title);
|
|
6206
7691
|
if (state === void 0) {
|
|
6207
|
-
return
|
|
7692
|
+
return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
|
|
6208
7693
|
}
|
|
6209
|
-
return
|
|
7694
|
+
return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
|
|
6210
7695
|
});
|
|
6211
7696
|
}
|
|
6212
7697
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
@@ -6215,11 +7700,11 @@ async function startServer(options) {
|
|
|
6215
7700
|
return void req.on("end", async () => {
|
|
6216
7701
|
const parsed = jsonBody(body);
|
|
6217
7702
|
if (parsed === null) {
|
|
6218
|
-
return
|
|
7703
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6219
7704
|
}
|
|
6220
7705
|
const titles = parsed.titles;
|
|
6221
7706
|
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
6222
|
-
return
|
|
7707
|
+
return sendJson2(res, 400, {
|
|
6223
7708
|
ok: false,
|
|
6224
7709
|
error: "Body needs a non-empty array of direction titles."
|
|
6225
7710
|
});
|
|
@@ -6228,20 +7713,20 @@ async function startServer(options) {
|
|
|
6228
7713
|
try {
|
|
6229
7714
|
const local = await readLocalPreviews(cwd);
|
|
6230
7715
|
if (local.errors.length > 0) {
|
|
6231
|
-
return
|
|
7716
|
+
return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
6232
7717
|
}
|
|
6233
7718
|
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
6234
7719
|
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
6235
7720
|
if (unknown.length > 0) {
|
|
6236
|
-
return
|
|
7721
|
+
return sendJson2(res, 400, {
|
|
6237
7722
|
ok: false,
|
|
6238
7723
|
error: "Only machine-local directions can be deleted from the registry."
|
|
6239
7724
|
});
|
|
6240
7725
|
}
|
|
6241
7726
|
const deleted = await dropLocalPreviews(cwd, unique);
|
|
6242
|
-
return
|
|
7727
|
+
return sendJson2(res, 200, { ok: true, deleted });
|
|
6243
7728
|
} catch {
|
|
6244
|
-
return
|
|
7729
|
+
return sendJson2(res, 500, {
|
|
6245
7730
|
ok: false,
|
|
6246
7731
|
error: "The directions could not be deleted from Leglas."
|
|
6247
7732
|
});
|
|
@@ -6251,7 +7736,7 @@ async function startServer(options) {
|
|
|
6251
7736
|
if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
|
|
6252
7737
|
const declaredLength = req.headers["content-length"];
|
|
6253
7738
|
if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
|
|
6254
|
-
return
|
|
7739
|
+
return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
6255
7740
|
}
|
|
6256
7741
|
const chunks = [];
|
|
6257
7742
|
let bytes = 0;
|
|
@@ -6265,7 +7750,7 @@ async function startServer(options) {
|
|
|
6265
7750
|
refused = true;
|
|
6266
7751
|
req.pause();
|
|
6267
7752
|
res.once("finish", () => req.socket.destroy());
|
|
6268
|
-
|
|
7753
|
+
sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
6269
7754
|
return;
|
|
6270
7755
|
}
|
|
6271
7756
|
chunks.push(buffer);
|
|
@@ -6274,12 +7759,12 @@ async function startServer(options) {
|
|
|
6274
7759
|
if (refused)
|
|
6275
7760
|
return;
|
|
6276
7761
|
if (bytes === 0) {
|
|
6277
|
-
return
|
|
7762
|
+
return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
|
|
6278
7763
|
}
|
|
6279
7764
|
const body = Buffer.concat(chunks, bytes);
|
|
6280
7765
|
const image = sniffImage(body);
|
|
6281
7766
|
if (image === null) {
|
|
6282
|
-
return
|
|
7767
|
+
return sendJson2(res, 415, {
|
|
6283
7768
|
ok: false,
|
|
6284
7769
|
error: "Only PNG, JPEG, WebP and GIF images can be attached."
|
|
6285
7770
|
});
|
|
@@ -6291,7 +7776,7 @@ async function startServer(options) {
|
|
|
6291
7776
|
await writeFile8(join12(cwd, file), body);
|
|
6292
7777
|
void pruneReferences(cwd).catch(() => {
|
|
6293
7778
|
});
|
|
6294
|
-
return
|
|
7779
|
+
return sendJson2(res, 200, {
|
|
6295
7780
|
ok: true,
|
|
6296
7781
|
reference: {
|
|
6297
7782
|
id,
|
|
@@ -6303,7 +7788,7 @@ async function startServer(options) {
|
|
|
6303
7788
|
}
|
|
6304
7789
|
});
|
|
6305
7790
|
} catch {
|
|
6306
|
-
return
|
|
7791
|
+
return sendJson2(res, 500, {
|
|
6307
7792
|
ok: false,
|
|
6308
7793
|
error: "The image could not be attached."
|
|
6309
7794
|
});
|
|
@@ -6316,24 +7801,24 @@ async function startServer(options) {
|
|
|
6316
7801
|
return void req.on("end", async () => {
|
|
6317
7802
|
const parsed = jsonBody(body);
|
|
6318
7803
|
if (parsed === null) {
|
|
6319
|
-
return
|
|
7804
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6320
7805
|
}
|
|
6321
7806
|
if (parsed.mode !== void 0 && parsed.mode !== "variant" && parsed.mode !== "replace") {
|
|
6322
|
-
return
|
|
7807
|
+
return sendJson2(res, 400, {
|
|
6323
7808
|
ok: false,
|
|
6324
7809
|
error: 'mode must be "variant" or "replace".'
|
|
6325
7810
|
});
|
|
6326
7811
|
}
|
|
6327
7812
|
const mode = parsed.mode === "replace" ? "replace" : "variant";
|
|
6328
7813
|
if (parsed.references !== void 0 && (!Array.isArray(parsed.references) || parsed.references.some((reference) => typeof reference !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
|
|
6329
|
-
return
|
|
7814
|
+
return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
|
|
6330
7815
|
}
|
|
6331
7816
|
const references = parsed.references ?? [];
|
|
6332
7817
|
if (references.length > 0) {
|
|
6333
7818
|
const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
6334
7819
|
const gone = references.filter((id2) => !present.has(id2));
|
|
6335
7820
|
if (gone.length > 0) {
|
|
6336
|
-
return
|
|
7821
|
+
return sendJson2(res, 410, {
|
|
6337
7822
|
ok: false,
|
|
6338
7823
|
error: gone.length === 1 ? "An attached image is gone: it was pasted over an hour ago and never sent. Attach it again." : "Some attached images are gone: they were pasted over an hour ago and never sent. Attach them again."
|
|
6339
7824
|
});
|
|
@@ -6343,11 +7828,11 @@ async function startServer(options) {
|
|
|
6343
7828
|
const previews = await livePreviews();
|
|
6344
7829
|
const preview = previews.find((entry) => entry.title === parsed.title);
|
|
6345
7830
|
if (!preview) {
|
|
6346
|
-
return
|
|
7831
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
6347
7832
|
}
|
|
6348
7833
|
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
6349
7834
|
if (!parsed.intent?.trim() && notes.length === 0) {
|
|
6350
|
-
return
|
|
7835
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
6351
7836
|
}
|
|
6352
7837
|
const intent = (parsed.intent ?? "").trim();
|
|
6353
7838
|
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
@@ -6361,7 +7846,7 @@ async function startServer(options) {
|
|
|
6361
7846
|
// one forks the direction and the other rewrites it. Only a
|
|
6362
7847
|
// genuine repeat is refused.
|
|
6363
7848
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
6364
|
-
return
|
|
7849
|
+
return sendJson2(res, 409, {
|
|
6365
7850
|
ok: false,
|
|
6366
7851
|
duplicate: true,
|
|
6367
7852
|
error: `That exact change to ${preview.title} is already waiting.`
|
|
@@ -6395,13 +7880,13 @@ async function startServer(options) {
|
|
|
6395
7880
|
...composed
|
|
6396
7881
|
}, id);
|
|
6397
7882
|
runner?.nudge();
|
|
6398
|
-
return
|
|
7883
|
+
return sendJson2(res, 200, {
|
|
6399
7884
|
ok: true,
|
|
6400
7885
|
...composed,
|
|
6401
7886
|
attachments: captured.attachments
|
|
6402
7887
|
});
|
|
6403
7888
|
} catch {
|
|
6404
|
-
return
|
|
7889
|
+
return sendJson2(res, 200, {
|
|
6405
7890
|
ok: true,
|
|
6406
7891
|
...composed,
|
|
6407
7892
|
attachments: captured.attachments,
|
|
@@ -6412,29 +7897,29 @@ async function startServer(options) {
|
|
|
6412
7897
|
}
|
|
6413
7898
|
if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
|
|
6414
7899
|
if (!hasJsonBody(req)) {
|
|
6415
|
-
return
|
|
7900
|
+
return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
|
|
6416
7901
|
}
|
|
6417
7902
|
let body = "";
|
|
6418
7903
|
req.on("data", (chunk) => body += chunk);
|
|
6419
7904
|
return void req.on("end", async () => {
|
|
6420
7905
|
const parsed = jsonBody(body);
|
|
6421
7906
|
if (parsed === null) {
|
|
6422
|
-
return
|
|
7907
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6423
7908
|
}
|
|
6424
7909
|
if (typeof parsed.title !== "string" || parsed.title === "") {
|
|
6425
|
-
return
|
|
7910
|
+
return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
|
|
6426
7911
|
}
|
|
6427
7912
|
if (parsed.note !== void 0 && typeof parsed.note !== "string") {
|
|
6428
|
-
return
|
|
7913
|
+
return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
|
|
6429
7914
|
}
|
|
6430
7915
|
const preview = (await livePreviews()).find((entry) => entry.title === parsed.title);
|
|
6431
7916
|
if (preview === void 0) {
|
|
6432
|
-
return
|
|
7917
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
6433
7918
|
}
|
|
6434
7919
|
const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
|
|
6435
7920
|
const browser = await browserPool.acquire();
|
|
6436
7921
|
if (browser === null) {
|
|
6437
|
-
return
|
|
7922
|
+
return sendJson2(res, 503, {
|
|
6438
7923
|
ok: false,
|
|
6439
7924
|
error: browserPool.reason() ?? NO_BROWSER
|
|
6440
7925
|
});
|
|
@@ -6471,7 +7956,7 @@ async function startServer(options) {
|
|
|
6471
7956
|
});
|
|
6472
7957
|
const result = await Promise.race([work, timeout]);
|
|
6473
7958
|
if (result === timeoutMarker) {
|
|
6474
|
-
return
|
|
7959
|
+
return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
|
|
6475
7960
|
}
|
|
6476
7961
|
clearTimeout(timer);
|
|
6477
7962
|
const crop = annotations.length > 0 ? result.crops[0] : null;
|
|
@@ -6481,7 +7966,7 @@ async function startServer(options) {
|
|
|
6481
7966
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
6482
7967
|
await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
6483
7968
|
await writeFile8(join12(cwd, relativeFile), shot.png);
|
|
6484
|
-
return
|
|
7969
|
+
return sendJson2(res, 200, {
|
|
6485
7970
|
ok: true,
|
|
6486
7971
|
file: relativeFile,
|
|
6487
7972
|
width: shot.width,
|
|
@@ -6493,7 +7978,7 @@ async function startServer(options) {
|
|
|
6493
7978
|
});
|
|
6494
7979
|
} catch (error) {
|
|
6495
7980
|
clearTimeout(timer);
|
|
6496
|
-
return
|
|
7981
|
+
return sendJson2(res, 502, {
|
|
6497
7982
|
ok: false,
|
|
6498
7983
|
error: error instanceof Error ? error.message : String(error)
|
|
6499
7984
|
});
|
|
@@ -6504,14 +7989,14 @@ async function startServer(options) {
|
|
|
6504
7989
|
return void readAgentChoice(cwd).then((choice) => {
|
|
6505
7990
|
if (choice.agent !== null)
|
|
6506
7991
|
runner?.prepare(choice.agent);
|
|
6507
|
-
|
|
6508
|
-
}, () =>
|
|
7992
|
+
sendJson2(res, 200, { ok: true });
|
|
7993
|
+
}, () => sendJson2(res, 200, { ok: true }));
|
|
6509
7994
|
}
|
|
6510
7995
|
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
6511
7996
|
return void Promise.all([
|
|
6512
7997
|
currentAgents(query.get("refresh") === "1"),
|
|
6513
7998
|
readAgentChoice(cwd)
|
|
6514
|
-
]).then(([agents, choice]) =>
|
|
7999
|
+
]).then(([agents, choice]) => sendJson2(res, 200, {
|
|
6515
8000
|
agents,
|
|
6516
8001
|
choice: choice.agent,
|
|
6517
8002
|
customRun: choice.run,
|
|
@@ -6520,48 +8005,48 @@ async function startServer(options) {
|
|
|
6520
8005
|
}
|
|
6521
8006
|
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
6522
8007
|
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
6523
|
-
return
|
|
8008
|
+
return sendJson2(res, 403, {
|
|
6524
8009
|
ok: false,
|
|
6525
8010
|
error: "The agent choice can only be made from the machine running Leglas."
|
|
6526
8011
|
});
|
|
6527
8012
|
}
|
|
6528
8013
|
if (!hasJsonBody(req)) {
|
|
6529
|
-
return
|
|
8014
|
+
return sendJson2(res, 400, { ok: false, error: "Agent choice must be JSON." });
|
|
6530
8015
|
}
|
|
6531
8016
|
let body = "";
|
|
6532
8017
|
req.on("data", (chunk) => body += chunk);
|
|
6533
8018
|
return void req.on("end", () => {
|
|
6534
8019
|
const parsed = jsonBody(body);
|
|
6535
8020
|
if (parsed === null) {
|
|
6536
|
-
return
|
|
8021
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6537
8022
|
}
|
|
6538
8023
|
if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
|
|
6539
|
-
return
|
|
8024
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
|
|
6540
8025
|
}
|
|
6541
8026
|
if (parsed.run !== void 0 && typeof parsed.run !== "string") {
|
|
6542
|
-
return
|
|
8027
|
+
return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
6543
8028
|
}
|
|
6544
8029
|
const effort = parsed.effort === null || isAgentEffort(parsed.effort) ? parsed.effort : void 0;
|
|
6545
8030
|
if (parsed.effort !== void 0 && effort === void 0) {
|
|
6546
|
-
return
|
|
8031
|
+
return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
|
|
6547
8032
|
}
|
|
6548
8033
|
if (parsed.agent === "custom") {
|
|
6549
8034
|
if (effort !== void 0) {
|
|
6550
|
-
return
|
|
8035
|
+
return sendJson2(res, 400, {
|
|
6551
8036
|
ok: false,
|
|
6552
8037
|
error: "Custom agents manage effort in their own command."
|
|
6553
8038
|
});
|
|
6554
8039
|
}
|
|
6555
8040
|
if (typeof parsed.run !== "string") {
|
|
6556
|
-
return
|
|
8041
|
+
return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
6557
8042
|
}
|
|
6558
8043
|
const template = parseTemplate(parsed.run);
|
|
6559
8044
|
if (!template.ok)
|
|
6560
|
-
return
|
|
6561
|
-
return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() =>
|
|
8045
|
+
return sendJson2(res, 400, { ok: false, error: template.error });
|
|
8046
|
+
return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6562
8047
|
}
|
|
6563
8048
|
if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.includes(effort)) {
|
|
6564
|
-
return
|
|
8049
|
+
return sendJson2(res, 400, {
|
|
6565
8050
|
ok: false,
|
|
6566
8051
|
error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
|
|
6567
8052
|
});
|
|
@@ -6571,8 +8056,8 @@ async function startServer(options) {
|
|
|
6571
8056
|
...effort === void 0 ? {} : { effort }
|
|
6572
8057
|
}).then(() => {
|
|
6573
8058
|
runner?.prepare(parsed.agent);
|
|
6574
|
-
|
|
6575
|
-
}, () =>
|
|
8059
|
+
sendJson2(res, 200, { ok: true });
|
|
8060
|
+
}, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6576
8061
|
});
|
|
6577
8062
|
}
|
|
6578
8063
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -6581,13 +8066,13 @@ async function startServer(options) {
|
|
|
6581
8066
|
return void req.on("end", () => {
|
|
6582
8067
|
const parsed = jsonBody(body);
|
|
6583
8068
|
if (parsed === null) {
|
|
6584
|
-
return
|
|
8069
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6585
8070
|
}
|
|
6586
8071
|
if (typeof parsed.watching !== "boolean") {
|
|
6587
|
-
return
|
|
8072
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
|
|
6588
8073
|
}
|
|
6589
8074
|
lastSeen = parsed.watching ? Date.now() : null;
|
|
6590
|
-
|
|
8075
|
+
sendJson2(res, 200, { ok: true });
|
|
6591
8076
|
});
|
|
6592
8077
|
}
|
|
6593
8078
|
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
@@ -6636,47 +8121,47 @@ async function startServer(options) {
|
|
|
6636
8121
|
}
|
|
6637
8122
|
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
6638
8123
|
if (!hasJsonBody(req)) {
|
|
6639
|
-
return
|
|
8124
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
|
|
6640
8125
|
}
|
|
6641
8126
|
let body = "";
|
|
6642
8127
|
req.on("data", (chunk) => body += chunk);
|
|
6643
8128
|
return void req.on("end", () => {
|
|
6644
8129
|
const parsed = jsonBody(body);
|
|
6645
8130
|
if (parsed === null) {
|
|
6646
|
-
return
|
|
8131
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6647
8132
|
}
|
|
6648
8133
|
if (parsed.id !== void 0 && typeof parsed.id !== "string") {
|
|
6649
|
-
return
|
|
8134
|
+
return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
|
|
6650
8135
|
}
|
|
6651
|
-
return
|
|
8136
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
|
|
6652
8137
|
});
|
|
6653
8138
|
}
|
|
6654
8139
|
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
6655
8140
|
if (!hasJsonBody(req)) {
|
|
6656
|
-
return
|
|
8141
|
+
return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
|
|
6657
8142
|
}
|
|
6658
8143
|
let body = "";
|
|
6659
8144
|
req.on("data", (chunk) => body += chunk);
|
|
6660
8145
|
return void req.on("end", async () => {
|
|
6661
8146
|
const parsed = jsonBody(body);
|
|
6662
8147
|
if (parsed === null) {
|
|
6663
|
-
return
|
|
8148
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6664
8149
|
}
|
|
6665
8150
|
if (typeof parsed.id !== "string") {
|
|
6666
|
-
return
|
|
8151
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6667
8152
|
}
|
|
6668
8153
|
const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
6669
8154
|
if (request === void 0) {
|
|
6670
|
-
return
|
|
8155
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6671
8156
|
}
|
|
6672
8157
|
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
6673
|
-
return
|
|
8158
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
6674
8159
|
}
|
|
6675
8160
|
try {
|
|
6676
8161
|
const retryId = newRequestId();
|
|
6677
8162
|
const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
|
|
6678
8163
|
if (!await removeRequest(cwd, request.id)) {
|
|
6679
|
-
return
|
|
8164
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6680
8165
|
}
|
|
6681
8166
|
await appendRequest(cwd, {
|
|
6682
8167
|
title: request.title,
|
|
@@ -6697,9 +8182,9 @@ async function startServer(options) {
|
|
|
6697
8182
|
...request.references === void 0 ? {} : { references: request.references }
|
|
6698
8183
|
}, retryId);
|
|
6699
8184
|
runner?.nudge();
|
|
6700
|
-
return
|
|
8185
|
+
return sendJson2(res, 200, { ok: true });
|
|
6701
8186
|
} catch {
|
|
6702
|
-
return
|
|
8187
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be retried." });
|
|
6703
8188
|
}
|
|
6704
8189
|
});
|
|
6705
8190
|
}
|
|
@@ -6708,21 +8193,21 @@ async function startServer(options) {
|
|
|
6708
8193
|
}
|
|
6709
8194
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6710
8195
|
if (!hasJsonBody(req)) {
|
|
6711
|
-
return
|
|
8196
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6712
8197
|
}
|
|
6713
8198
|
let body = "";
|
|
6714
8199
|
req.on("data", (chunk) => body += chunk);
|
|
6715
8200
|
return void req.on("end", async () => {
|
|
6716
8201
|
const parsed = jsonBody(body);
|
|
6717
8202
|
if (parsed === null) {
|
|
6718
|
-
return
|
|
8203
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6719
8204
|
}
|
|
6720
8205
|
if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
|
|
6721
|
-
return
|
|
8206
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
|
|
6722
8207
|
}
|
|
6723
8208
|
const anchor = anchorFrom(parsed.anchor);
|
|
6724
8209
|
if (anchor === null) {
|
|
6725
|
-
return
|
|
8210
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
6726
8211
|
}
|
|
6727
8212
|
try {
|
|
6728
8213
|
const annotation = await addAnnotation(cwd, {
|
|
@@ -6730,87 +8215,87 @@ async function startServer(options) {
|
|
|
6730
8215
|
note: typeof parsed.note === "string" ? parsed.note.trim() : "",
|
|
6731
8216
|
title: parsed.title
|
|
6732
8217
|
});
|
|
6733
|
-
return
|
|
8218
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6734
8219
|
} catch {
|
|
6735
|
-
return
|
|
8220
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be kept." });
|
|
6736
8221
|
}
|
|
6737
8222
|
});
|
|
6738
8223
|
}
|
|
6739
8224
|
if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
|
|
6740
8225
|
if (!hasJsonBody(req)) {
|
|
6741
|
-
return
|
|
8226
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6742
8227
|
}
|
|
6743
8228
|
let body = "";
|
|
6744
8229
|
req.on("data", (chunk) => body += chunk);
|
|
6745
8230
|
return void req.on("end", async () => {
|
|
6746
8231
|
const parsed = jsonBody(body);
|
|
6747
8232
|
if (parsed === null) {
|
|
6748
|
-
return
|
|
8233
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6749
8234
|
}
|
|
6750
8235
|
if (typeof parsed.id !== "string" || parsed.id === "") {
|
|
6751
|
-
return
|
|
8236
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
|
|
6752
8237
|
}
|
|
6753
8238
|
if (typeof parsed.note !== "string") {
|
|
6754
|
-
return
|
|
8239
|
+
return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
|
|
6755
8240
|
}
|
|
6756
8241
|
try {
|
|
6757
8242
|
const annotation = await updateAnnotation(cwd, parsed.id, parsed.note);
|
|
6758
8243
|
if (annotation === null) {
|
|
6759
|
-
return
|
|
8244
|
+
return sendJson2(res, 404, { ok: false, error: "That note has gone." });
|
|
6760
8245
|
}
|
|
6761
|
-
return
|
|
8246
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6762
8247
|
} catch {
|
|
6763
|
-
return
|
|
8248
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be reworded." });
|
|
6764
8249
|
}
|
|
6765
8250
|
});
|
|
6766
8251
|
}
|
|
6767
8252
|
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
6768
8253
|
if (!hasJsonBody(req)) {
|
|
6769
|
-
return
|
|
8254
|
+
return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
6770
8255
|
}
|
|
6771
8256
|
let body = "";
|
|
6772
8257
|
req.on("data", (chunk) => body += chunk);
|
|
6773
8258
|
return void req.on("end", async () => {
|
|
6774
8259
|
const parsed = jsonBody(body);
|
|
6775
8260
|
if (parsed === null) {
|
|
6776
|
-
return
|
|
8261
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6777
8262
|
}
|
|
6778
8263
|
const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
|
|
6779
8264
|
if (ids.length === 0) {
|
|
6780
|
-
return
|
|
8265
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
6781
8266
|
}
|
|
6782
8267
|
try {
|
|
6783
|
-
return
|
|
8268
|
+
return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
6784
8269
|
} catch {
|
|
6785
|
-
return
|
|
8270
|
+
return sendJson2(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
6786
8271
|
}
|
|
6787
8272
|
});
|
|
6788
8273
|
}
|
|
6789
8274
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
6790
8275
|
if (!hasJsonBody(req)) {
|
|
6791
|
-
return
|
|
8276
|
+
return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
6792
8277
|
}
|
|
6793
8278
|
let body = "";
|
|
6794
8279
|
req.on("data", (chunk) => body += chunk);
|
|
6795
8280
|
return void req.on("end", async () => {
|
|
6796
8281
|
const parsed = jsonBody(body);
|
|
6797
8282
|
if (parsed === null) {
|
|
6798
|
-
return
|
|
8283
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6799
8284
|
}
|
|
6800
8285
|
if (typeof parsed.id !== "string") {
|
|
6801
|
-
return
|
|
8286
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6802
8287
|
}
|
|
6803
8288
|
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
6804
8289
|
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
6805
|
-
return
|
|
8290
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
6806
8291
|
}
|
|
6807
8292
|
try {
|
|
6808
8293
|
if (!await removeRequest(cwd, parsed.id)) {
|
|
6809
|
-
return
|
|
8294
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6810
8295
|
}
|
|
6811
|
-
return
|
|
8296
|
+
return sendJson2(res, 200, { ok: true });
|
|
6812
8297
|
} catch {
|
|
6813
|
-
return
|
|
8298
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be dismissed." });
|
|
6814
8299
|
}
|
|
6815
8300
|
});
|
|
6816
8301
|
}
|
|
@@ -6820,13 +8305,13 @@ async function startServer(options) {
|
|
|
6820
8305
|
return void req.on("end", () => {
|
|
6821
8306
|
const parsed = jsonBody(body);
|
|
6822
8307
|
if (parsed === null) {
|
|
6823
|
-
return
|
|
8308
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6824
8309
|
}
|
|
6825
8310
|
if (parsed.renames === null || typeof parsed.renames !== "object") {
|
|
6826
|
-
return
|
|
8311
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
|
|
6827
8312
|
}
|
|
6828
8313
|
const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
6829
|
-
void writeRenames(cwd, renames).then(() =>
|
|
8314
|
+
void writeRenames(cwd, renames).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 200, { ok: false }));
|
|
6830
8315
|
});
|
|
6831
8316
|
}
|
|
6832
8317
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
@@ -6843,42 +8328,87 @@ async function startServer(options) {
|
|
|
6843
8328
|
relative6 = "";
|
|
6844
8329
|
}
|
|
6845
8330
|
const dir = fileMounts.get(slug);
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
8331
|
+
const serveMount = () => {
|
|
8332
|
+
if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
|
|
8333
|
+
return;
|
|
8334
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
8335
|
+
res.end("Leglas: no such preview file.");
|
|
8336
|
+
};
|
|
8337
|
+
if (!context.remote)
|
|
8338
|
+
return serveMount();
|
|
8339
|
+
if (relative6.split("/").some((segment) => segment.startsWith("."))) {
|
|
8340
|
+
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8341
|
+
}
|
|
8342
|
+
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
8343
|
+
if (!allowed)
|
|
8344
|
+
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8345
|
+
serveMount();
|
|
8346
|
+
});
|
|
6850
8347
|
}
|
|
6851
8348
|
if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
6852
|
-
return
|
|
8349
|
+
return sendJson2(res, 404, { error: "No such Leglas API path." });
|
|
6853
8350
|
}
|
|
6854
8351
|
if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
6855
8352
|
if (shellDir !== null && serveShellFile(res, shellDir, path))
|
|
6856
8353
|
return;
|
|
6857
8354
|
if (shellDir !== null) {
|
|
6858
8355
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
6859
|
-
|
|
8356
|
+
res.end("Leglas: no such path.");
|
|
8357
|
+
return;
|
|
6860
8358
|
}
|
|
6861
8359
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
6862
|
-
|
|
8360
|
+
res.end(PLACEHOLDER);
|
|
8361
|
+
return;
|
|
6863
8362
|
}
|
|
6864
|
-
return proxy.request(req, res,
|
|
6865
|
-
}
|
|
8363
|
+
return proxy.request(req, res, context.publicOrigin);
|
|
8364
|
+
};
|
|
8365
|
+
let port = 0;
|
|
8366
|
+
const server = http4.createServer((req, res) => handleRequest(req, res, {
|
|
8367
|
+
remote: false,
|
|
8368
|
+
publicOrigin: `http://localhost:${port}`
|
|
8369
|
+
}));
|
|
6866
8370
|
const sockets = /* @__PURE__ */ new Set();
|
|
6867
8371
|
server.on("connection", (socket) => {
|
|
6868
8372
|
sockets.add(socket);
|
|
6869
8373
|
socket.once("close", () => sockets.delete(socket));
|
|
6870
8374
|
});
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
8375
|
+
const handleUpgrade = (req, socket, head, context) => {
|
|
8376
|
+
const liveUpgrade = context.remote ? live.upgrade(req, socket, head, { viewer: true }) : live.upgrade(req, socket, head);
|
|
8377
|
+
if (liveUpgrade)
|
|
8378
|
+
return true;
|
|
6874
8379
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6875
|
-
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6876
|
-
|
|
8380
|
+
if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
8381
|
+
socket.destroy();
|
|
8382
|
+
return false;
|
|
8383
|
+
}
|
|
6877
8384
|
proxy.upgrade(req, socket, head);
|
|
8385
|
+
return false;
|
|
8386
|
+
};
|
|
8387
|
+
server.on("upgrade", (req, socket, head) => {
|
|
8388
|
+
handleUpgrade(req, socket, head, { remote: false });
|
|
8389
|
+
});
|
|
8390
|
+
shares = createShareManager({
|
|
8391
|
+
live,
|
|
8392
|
+
previews: livePreviewDefinitions,
|
|
8393
|
+
previewsForConfig,
|
|
8394
|
+
viewerConfig: {
|
|
8395
|
+
project,
|
|
8396
|
+
devServer: target,
|
|
8397
|
+
scanPreviews: config?.scanPreviews ?? true
|
|
8398
|
+
},
|
|
8399
|
+
request: (req, res, context) => handleRequest(req, res, {
|
|
8400
|
+
remote: true,
|
|
8401
|
+
publicOrigin: context.publicOrigin,
|
|
8402
|
+
grantId: context.grantId
|
|
8403
|
+
}),
|
|
8404
|
+
upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
|
|
8405
|
+
...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
|
|
8406
|
+
...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
|
|
6878
8407
|
});
|
|
6879
|
-
|
|
8408
|
+
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
8409
|
+
options.updates?.setPort(port);
|
|
6880
8410
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6881
|
-
|
|
8411
|
+
liveHealth = watchHealth(target, live);
|
|
6882
8412
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6883
8413
|
});
|
|
6884
8414
|
await writeServerInfo(cwd, {
|
|
@@ -6895,6 +8425,8 @@ async function startServer(options) {
|
|
|
6895
8425
|
...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
|
|
6896
8426
|
...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
|
|
6897
8427
|
});
|
|
8428
|
+
options.updates?.onBusy(() => runner?.snapshot().running ?? false);
|
|
8429
|
+
options.updates?.onChange(() => live.nudge("update"));
|
|
6898
8430
|
let closePromise = null;
|
|
6899
8431
|
return {
|
|
6900
8432
|
port,
|
|
@@ -6902,26 +8434,41 @@ async function startServer(options) {
|
|
|
6902
8434
|
close: () => {
|
|
6903
8435
|
if (closePromise !== null)
|
|
6904
8436
|
return closePromise;
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
8437
|
+
closePromise = (async () => {
|
|
8438
|
+
liveFiles.close();
|
|
8439
|
+
liveHealth?.close();
|
|
8440
|
+
await options.updates?.close();
|
|
8441
|
+
await Promise.all([
|
|
8442
|
+
shares?.close() ?? Promise.resolve(),
|
|
8443
|
+
branches.stop(),
|
|
8444
|
+
runner.stop(),
|
|
8445
|
+
browserPool.close(),
|
|
8446
|
+
live.close()
|
|
8447
|
+
]);
|
|
8448
|
+
await new Promise((done) => {
|
|
8449
|
+
for (const socket of sockets)
|
|
8450
|
+
socket.destroy();
|
|
8451
|
+
sockets.clear();
|
|
8452
|
+
server.closeAllConnections();
|
|
8453
|
+
server.close(() => done());
|
|
8454
|
+
});
|
|
8455
|
+
await removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
|
|
8456
|
+
});
|
|
8457
|
+
})();
|
|
6920
8458
|
return closePromise;
|
|
6921
8459
|
}
|
|
6922
8460
|
};
|
|
6923
8461
|
}
|
|
6924
8462
|
|
|
8463
|
+
// ../server/dist/update.js
|
|
8464
|
+
import { spawn as spawnChild2 } from "child_process";
|
|
8465
|
+
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
8466
|
+
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as writeFile9 } from "fs/promises";
|
|
8467
|
+
import { homedir as homedir2 } from "os";
|
|
8468
|
+
import { dirname as dirname9, join as join13, posix as posix2, win32 } from "path";
|
|
8469
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8470
|
+
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
8471
|
+
|
|
6925
8472
|
// src/keep.ts
|
|
6926
8473
|
function surfaceOf(url) {
|
|
6927
8474
|
if (!url.startsWith("/") || !url.includes("?")) return null;
|
|
@@ -6977,8 +8524,8 @@ function planKeep(options) {
|
|
|
6977
8524
|
}
|
|
6978
8525
|
|
|
6979
8526
|
// src/run-init.ts
|
|
6980
|
-
import { readFile as readFile12, writeFile as
|
|
6981
|
-
import { join as
|
|
8527
|
+
import { readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
|
|
8528
|
+
import { join as join14 } from "path";
|
|
6982
8529
|
async function readIfPresent(path) {
|
|
6983
8530
|
try {
|
|
6984
8531
|
return await readFile12(path, "utf8");
|
|
@@ -6989,18 +8536,18 @@ async function readIfPresent(path) {
|
|
|
6989
8536
|
async function runInit(options, deps) {
|
|
6990
8537
|
const existingConfig = findConfigFile(options.cwd);
|
|
6991
8538
|
const plan = planInit({
|
|
6992
|
-
agents: await readIfPresent(
|
|
8539
|
+
agents: await readIfPresent(join14(options.cwd, "AGENTS.md")),
|
|
6993
8540
|
config: existingConfig === null ? null : "present",
|
|
6994
|
-
gitignore: await readIfPresent(
|
|
8541
|
+
gitignore: await readIfPresent(join14(options.cwd, ".gitignore")),
|
|
6995
8542
|
force: options.force
|
|
6996
8543
|
});
|
|
6997
8544
|
const touched = [];
|
|
6998
8545
|
for (const write2 of plan.writes) {
|
|
6999
|
-
await
|
|
8546
|
+
await writeFile10(join14(options.cwd, write2.path), write2.contents, "utf8");
|
|
7000
8547
|
touched.push(write2.path);
|
|
7001
8548
|
}
|
|
7002
8549
|
if (plan.gitignore !== null) {
|
|
7003
|
-
await
|
|
8550
|
+
await writeFile10(join14(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
7004
8551
|
touched.push(".gitignore");
|
|
7005
8552
|
}
|
|
7006
8553
|
if (options.json) {
|
|
@@ -7019,9 +8566,9 @@ async function runInit(options, deps) {
|
|
|
7019
8566
|
}
|
|
7020
8567
|
|
|
7021
8568
|
// src/run-keep.ts
|
|
7022
|
-
import { existsSync as
|
|
7023
|
-
import { copyFile as copyFile2, mkdir as
|
|
7024
|
-
import { dirname as
|
|
8569
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8570
|
+
import { copyFile as copyFile2, mkdir as mkdir10, readFile as readFile13, rm as rm6, writeFile as writeFile11 } from "fs/promises";
|
|
8571
|
+
import { dirname as dirname10, join as join15 } from "path";
|
|
7025
8572
|
|
|
7026
8573
|
// src/resolve-title.ts
|
|
7027
8574
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -7057,15 +8604,15 @@ async function writeLogEntry(options) {
|
|
|
7057
8604
|
annotations: await readAnnotations(options.cwd),
|
|
7058
8605
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
7059
8606
|
});
|
|
7060
|
-
const dir =
|
|
7061
|
-
await
|
|
7062
|
-
const file =
|
|
7063
|
-
await
|
|
8607
|
+
const dir = join15(options.cwd, options.logDir);
|
|
8608
|
+
await mkdir10(dir, { recursive: true });
|
|
8609
|
+
const file = join15(dir, `${entry.slug}.md`);
|
|
8610
|
+
await writeFile11(file, entry.markdown, "utf8");
|
|
7064
8611
|
if (entry.pictures.length > 0) {
|
|
7065
|
-
const pictureDir =
|
|
7066
|
-
await
|
|
8612
|
+
const pictureDir = join15(dir, entry.slug);
|
|
8613
|
+
await mkdir10(pictureDir, { recursive: true });
|
|
7067
8614
|
for (const picture of entry.pictures) {
|
|
7068
|
-
await copyFile2(
|
|
8615
|
+
await copyFile2(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
|
|
7069
8616
|
});
|
|
7070
8617
|
}
|
|
7071
8618
|
}
|
|
@@ -7088,17 +8635,17 @@ async function runKeep(options, deps) {
|
|
|
7088
8635
|
if (!resolved.ok) return fail(resolved.error);
|
|
7089
8636
|
const plan = planKeep({ title: resolved.title, previews, to: options.to });
|
|
7090
8637
|
if (!plan.ok) return fail(plan.error);
|
|
7091
|
-
const from =
|
|
7092
|
-
const to =
|
|
7093
|
-
if (!
|
|
8638
|
+
const from = join15(options.cwd, plan.move.from);
|
|
8639
|
+
const to = join15(options.cwd, plan.move.to);
|
|
8640
|
+
if (!existsSync5(from)) {
|
|
7094
8641
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
7095
8642
|
}
|
|
7096
|
-
if (
|
|
8643
|
+
if (existsSync5(to)) {
|
|
7097
8644
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
7098
8645
|
}
|
|
7099
8646
|
const source = await readFile13(from, "utf8");
|
|
7100
|
-
await
|
|
7101
|
-
await
|
|
8647
|
+
await mkdir10(dirname10(to), { recursive: true });
|
|
8648
|
+
await writeFile11(to, renameExport(source, plan.exportName), "utf8");
|
|
7102
8649
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
7103
8650
|
let logged = null;
|
|
7104
8651
|
let logError = null;
|
|
@@ -7113,7 +8660,7 @@ async function runKeep(options, deps) {
|
|
|
7113
8660
|
} catch (error) {
|
|
7114
8661
|
logError = error instanceof Error ? error.message : String(error);
|
|
7115
8662
|
}
|
|
7116
|
-
await
|
|
8663
|
+
await rm6(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
7117
8664
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
7118
8665
|
if (options.json) {
|
|
7119
8666
|
deps.log(
|
|
@@ -7151,9 +8698,9 @@ async function runKeep(options, deps) {
|
|
|
7151
8698
|
}
|
|
7152
8699
|
|
|
7153
8700
|
// src/run-new.ts
|
|
7154
|
-
import { existsSync as
|
|
7155
|
-
import { mkdir as
|
|
7156
|
-
import { dirname as
|
|
8701
|
+
import { existsSync as existsSync6 } from "fs";
|
|
8702
|
+
import { mkdir as mkdir11, readFile as readFile14, writeFile as writeFile12 } from "fs/promises";
|
|
8703
|
+
import { dirname as dirname11, join as join16 } from "path";
|
|
7157
8704
|
async function readIfPresent2(path) {
|
|
7158
8705
|
try {
|
|
7159
8706
|
return await readFile14(path, "utf8");
|
|
@@ -7164,7 +8711,7 @@ async function readIfPresent2(path) {
|
|
|
7164
8711
|
async function runNew(options, deps) {
|
|
7165
8712
|
let from;
|
|
7166
8713
|
if (options.from !== void 0) {
|
|
7167
|
-
const contents = await readIfPresent2(
|
|
8714
|
+
const contents = await readIfPresent2(join16(options.cwd, options.from));
|
|
7168
8715
|
if (contents === null) {
|
|
7169
8716
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
7170
8717
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -7175,8 +8722,8 @@ async function runNew(options, deps) {
|
|
|
7175
8722
|
}
|
|
7176
8723
|
const plan = planNew({
|
|
7177
8724
|
surface: options.surface,
|
|
7178
|
-
packageJson: await readIfPresent2(
|
|
7179
|
-
gitignore: await readIfPresent2(
|
|
8725
|
+
packageJson: await readIfPresent2(join16(options.cwd, "package.json")),
|
|
8726
|
+
gitignore: await readIfPresent2(join16(options.cwd, ".gitignore")),
|
|
7180
8727
|
from
|
|
7181
8728
|
});
|
|
7182
8729
|
const fail = (error) => {
|
|
@@ -7199,19 +8746,19 @@ async function runNew(options, deps) {
|
|
|
7199
8746
|
deps.log(plan.instructions);
|
|
7200
8747
|
return { exitCode: 0, written: [] };
|
|
7201
8748
|
}
|
|
7202
|
-
const existing = plan.writes.filter((write2) =>
|
|
8749
|
+
const existing = plan.writes.filter((write2) => existsSync6(join16(options.cwd, write2.path)));
|
|
7203
8750
|
if (existing.length > 0) {
|
|
7204
8751
|
return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
|
|
7205
8752
|
}
|
|
7206
8753
|
const written = [];
|
|
7207
8754
|
for (const write2 of plan.writes) {
|
|
7208
|
-
const target =
|
|
7209
|
-
await
|
|
7210
|
-
await
|
|
8755
|
+
const target = join16(options.cwd, write2.path);
|
|
8756
|
+
await mkdir11(dirname11(target), { recursive: true });
|
|
8757
|
+
await writeFile12(target, write2.contents, "utf8");
|
|
7211
8758
|
written.push(write2.path);
|
|
7212
8759
|
}
|
|
7213
8760
|
if (plan.gitignore !== null) {
|
|
7214
|
-
await
|
|
8761
|
+
await writeFile12(join16(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
7215
8762
|
written.push(".gitignore");
|
|
7216
8763
|
}
|
|
7217
8764
|
if (options.json) {
|
|
@@ -7232,13 +8779,13 @@ async function runNew(options, deps) {
|
|
|
7232
8779
|
}
|
|
7233
8780
|
|
|
7234
8781
|
// src/run-previews.ts
|
|
7235
|
-
import { readFile as readFile15, writeFile as
|
|
7236
|
-
import { join as
|
|
8782
|
+
import { readFile as readFile15, writeFile as writeFile13 } from "fs/promises";
|
|
8783
|
+
import { join as join17 } from "path";
|
|
7237
8784
|
function envelope(deps, ok, body) {
|
|
7238
8785
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
7239
8786
|
}
|
|
7240
8787
|
async function ensureIgnored(cwd) {
|
|
7241
|
-
const path =
|
|
8788
|
+
const path = join17(cwd, ".gitignore");
|
|
7242
8789
|
let current = null;
|
|
7243
8790
|
try {
|
|
7244
8791
|
current = await readFile15(path, "utf8");
|
|
@@ -7246,7 +8793,7 @@ async function ensureIgnored(cwd) {
|
|
|
7246
8793
|
current = null;
|
|
7247
8794
|
}
|
|
7248
8795
|
const next = ignoreEntry(current);
|
|
7249
|
-
if (next !== null) await
|
|
8796
|
+
if (next !== null) await writeFile13(path, next, "utf8");
|
|
7250
8797
|
}
|
|
7251
8798
|
async function runAdd(options, deps) {
|
|
7252
8799
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -7576,12 +9123,12 @@ async function runShow(options, deps) {
|
|
|
7576
9123
|
|
|
7577
9124
|
// src/run-watch.ts
|
|
7578
9125
|
import { spawn as spawn3 } from "child_process";
|
|
7579
|
-
import { mkdir as
|
|
7580
|
-
import { dirname as
|
|
9126
|
+
import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile14 } from "fs/promises";
|
|
9127
|
+
import { dirname as dirname12, join as join18 } from "path";
|
|
7581
9128
|
var POLL_MS2 = 2e3;
|
|
7582
9129
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
7583
9130
|
async function saveTemplate(cwd, run4) {
|
|
7584
|
-
const path =
|
|
9131
|
+
const path = join18(cwd, WATCH_PATH);
|
|
7585
9132
|
let config = {};
|
|
7586
9133
|
try {
|
|
7587
9134
|
const parsed = JSON.parse(await readFile16(path, "utf8"));
|
|
@@ -7591,8 +9138,8 @@ async function saveTemplate(cwd, run4) {
|
|
|
7591
9138
|
} catch {
|
|
7592
9139
|
}
|
|
7593
9140
|
config.run = run4;
|
|
7594
|
-
await
|
|
7595
|
-
await
|
|
9141
|
+
await mkdir12(dirname12(path), { recursive: true });
|
|
9142
|
+
await writeFile14(path, `${JSON.stringify(config, null, 2)}
|
|
7596
9143
|
`, "utf8");
|
|
7597
9144
|
}
|
|
7598
9145
|
function spawnAgent(command, args, cwd) {
|
|
@@ -7731,12 +9278,12 @@ async function runWatch(options, deps) {
|
|
|
7731
9278
|
|
|
7732
9279
|
// src/run-classify.ts
|
|
7733
9280
|
import { stat as stat4 } from "fs/promises";
|
|
7734
|
-
import { join as
|
|
9281
|
+
import { join as join19 } from "path";
|
|
7735
9282
|
async function runClassify(options, deps) {
|
|
7736
9283
|
const declared = await Promise.all(
|
|
7737
9284
|
options.changes.map(async (change) => ({
|
|
7738
9285
|
...change,
|
|
7739
|
-
exists: await stat4(
|
|
9286
|
+
exists: await stat4(join19(options.cwd, change.path)).then(
|
|
7740
9287
|
() => true,
|
|
7741
9288
|
() => false
|
|
7742
9289
|
)
|
|
@@ -7762,10 +9309,10 @@ async function runClassify(options, deps) {
|
|
|
7762
9309
|
}
|
|
7763
9310
|
|
|
7764
9311
|
// src/run.ts
|
|
7765
|
-
import { existsSync as
|
|
9312
|
+
import { existsSync as existsSync7 } from "fs";
|
|
7766
9313
|
import { realpath as realpath4 } from "fs/promises";
|
|
7767
9314
|
import { createRequire } from "module";
|
|
7768
|
-
import { basename as basename6, dirname as
|
|
9315
|
+
import { basename as basename6, dirname as dirname13, join as join20, relative as relative5, resolve as resolve4 } from "path";
|
|
7769
9316
|
import { fileURLToPath } from "url";
|
|
7770
9317
|
|
|
7771
9318
|
// src/dev-server-owner.ts
|
|
@@ -7851,11 +9398,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
7851
9398
|
|
|
7852
9399
|
// src/run.ts
|
|
7853
9400
|
function findShellDir() {
|
|
7854
|
-
const bundled =
|
|
7855
|
-
if (
|
|
9401
|
+
const bundled = join20(dirname13(fileURLToPath(import.meta.url)), "shell");
|
|
9402
|
+
if (existsSync7(join20(bundled, "index.html"))) return bundled;
|
|
7856
9403
|
try {
|
|
7857
9404
|
const require2 = createRequire(import.meta.url);
|
|
7858
|
-
return
|
|
9405
|
+
return dirname13(require2.resolve("@leglas/shell/dist/index.html"));
|
|
7859
9406
|
} catch {
|
|
7860
9407
|
return null;
|
|
7861
9408
|
}
|
|
@@ -7866,10 +9413,14 @@ function shellWord(value) {
|
|
|
7866
9413
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7867
9414
|
}
|
|
7868
9415
|
function embeddedLeglasCommand() {
|
|
7869
|
-
const entry =
|
|
7870
|
-
if (!
|
|
9416
|
+
const entry = join20(dirname13(fileURLToPath(import.meta.url)), "bin.js");
|
|
9417
|
+
if (!existsSync7(entry)) return "npx -y leglas";
|
|
7871
9418
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
7872
9419
|
}
|
|
9420
|
+
function skipStartupCheck(env) {
|
|
9421
|
+
const off = (value) => value === void 0 || value === "" || value === "0" || value === "false";
|
|
9422
|
+
return env.CI !== void 0 && env.CI !== "" && env.CI !== "false" || !off(env.LEGLAS_NO_UPDATE_CHECK);
|
|
9423
|
+
}
|
|
7873
9424
|
async function run3(options, deps) {
|
|
7874
9425
|
const loaded = await loadConfig(options.cwd);
|
|
7875
9426
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -7898,8 +9449,8 @@ async function run3(options, deps) {
|
|
|
7898
9449
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
7899
9450
|
for (const preview of merged?.previews ?? []) {
|
|
7900
9451
|
if (preview.file !== void 0) {
|
|
7901
|
-
const absolute =
|
|
7902
|
-
if (!
|
|
9452
|
+
const absolute = join20(options.cwd, preview.file);
|
|
9453
|
+
if (!existsSync7(absolute)) {
|
|
7903
9454
|
previewErrors.push(
|
|
7904
9455
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
7905
9456
|
);
|
|
@@ -7909,7 +9460,7 @@ async function run3(options, deps) {
|
|
|
7909
9460
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
7910
9461
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
7911
9462
|
}
|
|
7912
|
-
fileMounts.set(slug,
|
|
9463
|
+
fileMounts.set(slug, dirname13(absolute));
|
|
7913
9464
|
previews.push({
|
|
7914
9465
|
...preview,
|
|
7915
9466
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
@@ -7920,8 +9471,8 @@ async function run3(options, deps) {
|
|
|
7920
9471
|
}
|
|
7921
9472
|
const config = merged === null ? null : { ...merged, previews };
|
|
7922
9473
|
const configWarnings = [];
|
|
7923
|
-
const projectRoot = await realpath4(loaded.path === null ? options.cwd :
|
|
7924
|
-
() => resolve4(loaded.path === null ? options.cwd :
|
|
9474
|
+
const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname13(loaded.path)).catch(
|
|
9475
|
+
() => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path))
|
|
7925
9476
|
);
|
|
7926
9477
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7927
9478
|
const serverPromise = startServer({
|
|
@@ -7935,6 +9486,7 @@ async function run3(options, deps) {
|
|
|
7935
9486
|
project: loaded.path ?? options.cwd,
|
|
7936
9487
|
cwd: options.cwd,
|
|
7937
9488
|
leglasCommand: embeddedLeglasCommand(),
|
|
9489
|
+
...deps.updates === void 0 ? {} : { updates: deps.updates },
|
|
7938
9490
|
...options.port === void 0 ? {} : { port: options.port }
|
|
7939
9491
|
});
|
|
7940
9492
|
const [server, warning] = await Promise.all([serverPromise, ownerWarning]);
|
|
@@ -7984,12 +9536,26 @@ async function run3(options, deps) {
|
|
|
7984
9536
|
}
|
|
7985
9537
|
}
|
|
7986
9538
|
if (options.open) await deps.open(url);
|
|
9539
|
+
let stopped = false;
|
|
9540
|
+
let updateTimer = null;
|
|
9541
|
+
if (!options.json && deps.updates !== void 0 && !skipStartupCheck(process.env)) {
|
|
9542
|
+
void deps.updates.check().then(() => {
|
|
9543
|
+
if (stopped) return;
|
|
9544
|
+
const line = deps.updates?.notice();
|
|
9545
|
+
if (line !== null && line !== void 0) deps.log(line);
|
|
9546
|
+
});
|
|
9547
|
+
const updates = deps.updates;
|
|
9548
|
+
updateTimer = setInterval(() => void updates.check(), 60 * 6e4);
|
|
9549
|
+
updateTimer.unref?.();
|
|
9550
|
+
}
|
|
7987
9551
|
return {
|
|
7988
9552
|
exitCode: 0,
|
|
7989
9553
|
url,
|
|
7990
9554
|
devServer,
|
|
7991
9555
|
previewCount,
|
|
7992
9556
|
stop: async () => {
|
|
9557
|
+
stopped = true;
|
|
9558
|
+
if (updateTimer !== null) clearInterval(updateTimer);
|
|
7993
9559
|
await app?.stop().catch(() => {
|
|
7994
9560
|
});
|
|
7995
9561
|
await server.close();
|