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/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
- // UNVERIFIED: cursor-agent was not available on the build machine. The
1271
- // reading is deliberately loose, and anything ambiguous stays unknown.
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 = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
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(lookup = pathLookup, probe2 = execProbe) {
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 lookup(adapter.binary).catch(() => false);
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)[0];
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 ? "using write" : `editing ${path2}`;
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
- return { ...req.headers, host: authority };
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, publicOrigin) {
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 publicOrigin + location.slice(origin.length);
1847
+ return publicOrigin2 + location.slice(origin.length);
1822
1848
  }
1823
1849
  return location;
1824
1850
  }
1825
1851
  return {
1826
- request(req, res, publicOrigin) {
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, publicOrigin);
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 expired = false;
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
- expired = true;
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 (expired)
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 (expired)
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 || expired)
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 && !expired) {
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 (expired)
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 (!expired) {
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 (expired)
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(_options = {}) {
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 = { socket, buffered: Buffer.alloc(0) };
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 http2 from "http";
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 sendJson(res, status, body) {
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 bind(server, requested) {
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 server = http2.createServer((req, res) => {
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 sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body needs a direction title." });
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 sendJson(res, 404, { ok: false, error: "No such direction." });
7675
+ return sendJson2(res, 404, { ok: false, error: "No such direction." });
6191
7676
  }
6192
7677
  if (preview.branch === void 0) {
6193
- return sendJson(res, 400, {
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 sendJson(res, 400, {
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 sendJson(res, 404, { ok: false, error: "No such branch preview." });
7692
+ return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
6208
7693
  }
6209
- return sendJson(res, 200, { ok: true, state: publicBranchState(state) });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, {
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 sendJson(res, 409, { ok: false, error: local.errors.join(" ") });
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 sendJson(res, 400, {
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 sendJson(res, 200, { ok: true, deleted });
7727
+ return sendJson2(res, 200, { ok: true, deleted });
6243
7728
  } catch {
6244
- return sendJson(res, 500, {
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 sendJson(res, 413, { ok: false, error: "That image is over 10MB." });
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
- sendJson(res, 413, { ok: false, error: "That image is over 10MB." });
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 sendJson(res, 400, { ok: false, error: "The upload was empty." });
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 sendJson(res, 415, {
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 sendJson(res, 200, {
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 sendJson(res, 500, {
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, {
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 sendJson(res, 400, { ok: false, error: "references must be uploaded image ids." });
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 sendJson(res, 410, {
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 sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
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 sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
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 sendJson(res, 409, {
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 sendJson(res, 200, {
7883
+ return sendJson2(res, 200, {
6399
7884
  ok: true,
6400
7885
  ...composed,
6401
7886
  attachments: captured.attachments
6402
7887
  });
6403
7888
  } catch {
6404
- return sendJson(res, 200, {
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 sendJson(res, 400, { ok: false, error: "Capture must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Capture needs a direction title." });
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 sendJson(res, 400, { ok: false, error: "The note id must be a string." });
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 sendJson(res, 404, { ok: false, error: "No such direction." });
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 sendJson(res, 503, {
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 sendJson(res, 504, { ok: false, error: "The page did not load in time." });
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 sendJson(res, 200, {
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 sendJson(res, 502, {
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
- sendJson(res, 200, { ok: true });
6508
- }, () => sendJson(res, 200, { ok: true }));
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]) => sendJson(res, 200, {
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 sendJson(res, 403, {
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 sendJson(res, 400, { ok: false, error: "Agent choice must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body needs a known agent." });
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 sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
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 sendJson(res, 400, { ok: false, error: "Effort must be a supported level or null." });
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 sendJson(res, 400, {
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 sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
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 sendJson(res, 400, { ok: false, error: template.error });
6561
- return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
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 sendJson(res, 400, {
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
- sendJson(res, 200, { ok: true });
6575
- }, () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8069
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6585
8070
  }
6586
8071
  if (typeof parsed.watching !== "boolean") {
6587
- return sendJson(res, 400, { ok: false, error: "Body needs a watching boolean." });
8072
+ return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
6588
8073
  }
6589
8074
  lastSeen = parsed.watching ? Date.now() : null;
6590
- sendJson(res, 200, { ok: true });
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 sendJson(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "The request id must be a string." });
8134
+ return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
6650
8135
  }
6651
- return sendJson(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
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 sendJson(res, 400, { ok: false, error: "Retry must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8148
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6664
8149
  }
6665
8150
  if (typeof parsed.id !== "string") {
6666
- return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
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 sendJson(res, 404, { ok: false, error: "No such request." });
8155
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
6671
8156
  }
6672
8157
  if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
6673
- return sendJson(res, 400, { ok: false, error: "Only an ended request can be run again." });
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 sendJson(res, 404, { ok: false, error: "No such request." });
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 sendJson(res, 200, { ok: true });
8185
+ return sendJson2(res, 200, { ok: true });
6701
8186
  } catch {
6702
- return sendJson(res, 500, { ok: false, error: "The request could not be retried." });
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 sendJson(res, 400, { ok: false, error: "A note must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "A note needs a direction." });
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 sendJson(res, 400, { ok: false, error: "A note needs something to point at." });
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 sendJson(res, 200, { ok: true, annotation });
8218
+ return sendJson2(res, 200, { ok: true, annotation });
6734
8219
  } catch {
6735
- return sendJson(res, 500, { ok: false, error: "The note could not be kept." });
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 sendJson(res, 400, { ok: false, error: "A note must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body needs the note to reword." });
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 sendJson(res, 400, { ok: false, error: "A reworded note needs its words." });
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 sendJson(res, 404, { ok: false, error: "That note has gone." });
8244
+ return sendJson2(res, 404, { ok: false, error: "That note has gone." });
6760
8245
  }
6761
- return sendJson(res, 200, { ok: true, annotation });
8246
+ return sendJson2(res, 200, { ok: true, annotation });
6762
8247
  } catch {
6763
- return sendJson(res, 500, { ok: false, error: "The note could not be reworded." });
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 sendJson(res, 400, { ok: false, error: "Delete must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body needs the notes to forget." });
8265
+ return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
6781
8266
  }
6782
8267
  try {
6783
- return sendJson(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
8268
+ return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
6784
8269
  } catch {
6785
- return sendJson(res, 500, { ok: false, error: "The notes could not be forgotten." });
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 sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8283
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6799
8284
  }
6800
8285
  if (typeof parsed.id !== "string") {
6801
- return sendJson(res, 400, { ok: false, error: "Body needs a request id." });
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 sendJson(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
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 sendJson(res, 404, { ok: false, error: "No such request." });
8294
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
6810
8295
  }
6811
- return sendJson(res, 200, { ok: true });
8296
+ return sendJson2(res, 200, { ok: true });
6812
8297
  } catch {
6813
- return sendJson(res, 500, { ok: false, error: "The request could not be dismissed." });
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 sendJson(res, 400, { ok: false, error: "Body must be JSON." });
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 sendJson(res, 400, { ok: false, error: "Body needs a renames object." });
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(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 200, { ok: false }));
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
- if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
6847
- return;
6848
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
6849
- return res.end("Leglas: no such preview file.");
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 sendJson(res, 404, { error: "No such Leglas API path." });
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
- return res.end("Leglas: no such path.");
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
- return res.end(PLACEHOLDER);
8360
+ res.end(PLACEHOLDER);
8361
+ return;
6863
8362
  }
6864
- return proxy.request(req, res, `http://localhost:${port}`);
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
- server.on("upgrade", (req, socket, head) => {
6872
- if (live.upgrade(req, socket, head))
6873
- return;
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
- return socket.destroy();
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
- const port = await bind(server, options.port ?? DEFAULT_PORT);
8408
+ port = await bind2(server, options.port ?? DEFAULT_PORT);
8409
+ options.updates?.setPort(port);
6880
8410
  const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
6881
- const liveHealth = watchHealth(target, live);
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
- liveFiles.close();
6906
- liveHealth.close();
6907
- closePromise = Promise.all([
6908
- branches.stop(),
6909
- runner.stop(),
6910
- browserPool.close(),
6911
- live.close()
6912
- ]).then(() => new Promise((done) => {
6913
- for (const socket of sockets)
6914
- socket.destroy();
6915
- sockets.clear();
6916
- server.closeAllConnections();
6917
- server.close(() => done());
6918
- })).then(() => removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
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 writeFile9 } from "fs/promises";
6981
- import { join as join13 } from "path";
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(join13(options.cwd, "AGENTS.md")),
8539
+ agents: await readIfPresent(join14(options.cwd, "AGENTS.md")),
6993
8540
  config: existingConfig === null ? null : "present",
6994
- gitignore: await readIfPresent(join13(options.cwd, ".gitignore")),
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 writeFile9(join13(options.cwd, write2.path), write2.contents, "utf8");
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 writeFile9(join13(options.cwd, ".gitignore"), plan.gitignore, "utf8");
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 existsSync4 } from "fs";
7023
- import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
7024
- import { dirname as dirname9, join as join14 } from "path";
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 = join14(options.cwd, options.logDir);
7061
- await mkdir9(dir, { recursive: true });
7062
- const file = join14(dir, `${entry.slug}.md`);
7063
- await writeFile10(file, entry.markdown, "utf8");
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 = join14(dir, entry.slug);
7066
- await mkdir9(pictureDir, { recursive: true });
8612
+ const pictureDir = join15(dir, entry.slug);
8613
+ await mkdir10(pictureDir, { recursive: true });
7067
8614
  for (const picture of entry.pictures) {
7068
- await copyFile2(join14(options.cwd, picture.from), join14(pictureDir, picture.to)).catch(() => {
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 = join14(options.cwd, plan.move.from);
7092
- const to = join14(options.cwd, plan.move.to);
7093
- if (!existsSync4(from)) {
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 (existsSync4(to)) {
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 mkdir9(dirname9(to), { recursive: true });
7101
- await writeFile10(to, renameExport(source, plan.exportName), "utf8");
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 rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
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 existsSync5 } from "fs";
7155
- import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
7156
- import { dirname as dirname10, join as join15 } from "path";
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(join15(options.cwd, options.from));
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(join15(options.cwd, "package.json")),
7179
- gitignore: await readIfPresent2(join15(options.cwd, ".gitignore")),
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) => existsSync5(join15(options.cwd, write2.path)));
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 = join15(options.cwd, write2.path);
7209
- await mkdir10(dirname10(target), { recursive: true });
7210
- await writeFile11(target, write2.contents, "utf8");
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 writeFile11(join15(options.cwd, ".gitignore"), plan.gitignore, "utf8");
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 writeFile12 } from "fs/promises";
7236
- import { join as join16 } from "path";
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 = join16(cwd, ".gitignore");
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 writeFile12(path, next, "utf8");
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 mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
7580
- import { dirname as dirname11, join as join17 } from "path";
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 = join17(cwd, WATCH_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 mkdir11(dirname11(path), { recursive: true });
7595
- await writeFile13(path, `${JSON.stringify(config, null, 2)}
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 join18 } from "path";
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(join18(options.cwd, change.path)).then(
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 existsSync6 } from "fs";
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 dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
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 = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
7855
- if (existsSync6(join19(bundled, "index.html"))) return bundled;
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 dirname12(require2.resolve("@leglas/shell/dist/index.html"));
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 = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
7870
- if (!existsSync6(entry)) return "npx -y leglas";
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 = join19(options.cwd, preview.file);
7902
- if (!existsSync6(absolute)) {
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, dirname12(absolute));
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 : dirname12(loaded.path)).catch(
7924
- () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
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();