leglas 0.7.1 → 0.7.3

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
@@ -783,7 +783,7 @@ ${AGENTS_SECTION}`
783
783
  }
784
784
 
785
785
  // src/keep.ts
786
- import { basename as basename3, extname as extname4, normalize as normalize2 } from "path";
786
+ import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
787
787
 
788
788
  // ../server/dist/config.js
789
789
  var DEFAULT_DEV_SERVER = "http://localhost:3000";
@@ -1555,8 +1555,8 @@ function findConfigFile(startDir) {
1555
1555
  const { root } = parse(startDir);
1556
1556
  let dir = startDir;
1557
1557
  for (; ; ) {
1558
- for (const basename6 of CONFIG_BASENAMES) {
1559
- const candidate = join2(dir, basename6);
1558
+ for (const basename7 of CONFIG_BASENAMES) {
1559
+ const candidate = join2(dir, basename7);
1560
1560
  if (existsSync(candidate))
1561
1561
  return candidate;
1562
1562
  }
@@ -3000,18 +3000,23 @@ async function freePort() {
3000
3000
  });
3001
3001
  });
3002
3002
  }
3003
- function answers(port) {
3004
- return new Promise((resolve5) => {
3005
- const socket = net2.connect(port, "127.0.0.1");
3003
+ var LOOPBACK = ["127.0.0.1", "::1"];
3004
+ function forUrl(host) {
3005
+ return host.includes(":") ? `[${host}]` : host;
3006
+ }
3007
+ async function answeringHost(port) {
3008
+ const reached = await Promise.all(LOOPBACK.map((host) => new Promise((resolve5) => {
3009
+ const socket = net2.connect({ port, host });
3006
3010
  const settle = (value) => {
3007
3011
  socket.destroy();
3008
3012
  resolve5(value);
3009
3013
  };
3010
3014
  socket.setTimeout(400);
3011
- socket.once("connect", () => settle(true));
3012
- socket.once("timeout", () => settle(false));
3013
- socket.once("error", () => settle(false));
3014
- });
3015
+ socket.once("connect", () => settle(host));
3016
+ socket.once("timeout", () => settle(null));
3017
+ socket.once("error", () => settle(null));
3018
+ })));
3019
+ return reached.find((host) => host !== null) ?? null;
3015
3020
  }
3016
3021
  async function startWorktree(options) {
3017
3022
  const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
@@ -3106,8 +3111,9 @@ async function startAppProcess(options) {
3106
3111
  if (exited !== null) {
3107
3112
  throw new Error(`${options.label} did not start: its dev command exited with code ${exited}.`);
3108
3113
  }
3109
- if (await answers(port)) {
3110
- return { port, url: `http://127.0.0.1:${port}`, stop };
3114
+ const host = await answeringHost(port);
3115
+ if (host !== null) {
3116
+ return { port, url: `http://${forUrl(host)}:${port}`, stop };
3111
3117
  }
3112
3118
  await new Promise((resolve5) => setTimeout(resolve5, 250));
3113
3119
  }
@@ -4602,6 +4608,10 @@ function startRunner(options) {
4602
4608
  stopping: false,
4603
4609
  waiting: null
4604
4610
  };
4611
+ const setState = (next) => {
4612
+ state = typeof next === "function" ? next(state) : next;
4613
+ options.onChange?.();
4614
+ };
4605
4615
  let stopped = false;
4606
4616
  let ticking = null;
4607
4617
  let pendingNudges = 0;
@@ -4653,7 +4663,7 @@ function startRunner(options) {
4653
4663
  return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
4654
4664
  };
4655
4665
  const idle = () => {
4656
- state = {
4666
+ setState({
4657
4667
  running: false,
4658
4668
  requestId: null,
4659
4669
  agent: null,
@@ -4661,7 +4671,7 @@ function startRunner(options) {
4661
4671
  startedAt: null,
4662
4672
  stopping: false,
4663
4673
  waiting: null
4664
- };
4674
+ });
4665
4675
  };
4666
4676
  const rememberLine = (lines2, line) => {
4667
4677
  lines2.push(line);
@@ -4769,14 +4779,15 @@ function startRunner(options) {
4769
4779
  if (retry !== null) {
4770
4780
  observed.retry = retry;
4771
4781
  if (active === current)
4772
- state = { ...state, waiting: retry };
4782
+ setState((value) => ({ ...value, waiting: retry }));
4773
4783
  }
4774
4784
  const activity = activityFrom(resolved.agent, line, options.cwd);
4775
4785
  if (activity !== null) {
4776
4786
  if (activity.startsWith("editing"))
4777
4787
  observed.edited = true;
4778
- if (active === current)
4779
- state = { ...state, activity, waiting: null };
4788
+ if (active === current) {
4789
+ setState((value) => ({ ...value, activity, waiting: null }));
4790
+ }
4780
4791
  }
4781
4792
  });
4782
4793
  const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
@@ -4823,7 +4834,7 @@ function startRunner(options) {
4823
4834
  await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
4824
4835
  return;
4825
4836
  }
4826
- state = {
4837
+ setState({
4827
4838
  running: true,
4828
4839
  requestId: request.id,
4829
4840
  agent: resolved.name,
@@ -4831,7 +4842,7 @@ function startRunner(options) {
4831
4842
  startedAt: Date.now(),
4832
4843
  stopping: false,
4833
4844
  waiting: null
4834
- };
4845
+ });
4835
4846
  const observed = {
4836
4847
  sessionId: null,
4837
4848
  edited: false,
@@ -4862,7 +4873,7 @@ function startRunner(options) {
4862
4873
  resolved = cold;
4863
4874
  observed.sessionId = null;
4864
4875
  observed.retry = null;
4865
- state = { ...state, activity: null, waiting: null };
4876
+ setState((value) => ({ ...value, activity: null, waiting: null }));
4866
4877
  outcome = await runChild(request, resolved, lines2, observed);
4867
4878
  failure = verdict();
4868
4879
  }
@@ -4936,7 +4947,7 @@ function startRunner(options) {
4936
4947
  const current = active;
4937
4948
  current.cancelled = true;
4938
4949
  failed.add(current.requestId);
4939
- state = { ...state, stopping: true, waiting: null };
4950
+ setState((value) => ({ ...value, stopping: true, waiting: null }));
4940
4951
  current.controller.abort();
4941
4952
  try {
4942
4953
  current.child?.kill("SIGTERM");
@@ -4979,6 +4990,193 @@ function startRunner(options) {
4979
4990
  };
4980
4991
  }
4981
4992
 
4993
+ // ../server/dist/live.js
4994
+ import { createHash } from "crypto";
4995
+ var LIVE_PATH = "/leglas/api/live";
4996
+ var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4997
+ function encodeFrame(opcode, payload) {
4998
+ const body = typeof payload === "string" ? Buffer.from(payload) : payload;
4999
+ let header;
5000
+ if (body.length < 126) {
5001
+ header = Buffer.allocUnsafe(2);
5002
+ header[1] = body.length;
5003
+ } else if (body.length <= 65535) {
5004
+ header = Buffer.allocUnsafe(4);
5005
+ header[1] = 126;
5006
+ header.writeUInt16BE(body.length, 2);
5007
+ } else {
5008
+ header = Buffer.allocUnsafe(10);
5009
+ header[1] = 127;
5010
+ header.writeBigUInt64BE(BigInt(body.length), 2);
5011
+ }
5012
+ header[0] = 128 | opcode & 15;
5013
+ return Buffer.concat([header, body], header.length + body.length);
5014
+ }
5015
+ var LIVE_DEBOUNCE_MS = 50;
5016
+ function createCoalescer(emit, options = {}) {
5017
+ const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
5018
+ const setLater = options.setTimeout ?? ((callback, ms) => {
5019
+ const timer = setTimeout(callback, ms);
5020
+ timer.unref?.();
5021
+ return timer;
5022
+ });
5023
+ const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
5024
+ const pending = /* @__PURE__ */ new Map();
5025
+ let closed = false;
5026
+ return {
5027
+ schedule(change) {
5028
+ if (closed)
5029
+ return;
5030
+ const waiting = pending.get(change);
5031
+ if (waiting !== void 0)
5032
+ clearLater(waiting);
5033
+ pending.set(change, setLater(() => {
5034
+ pending.delete(change);
5035
+ if (!closed)
5036
+ emit(change);
5037
+ }, windowMs));
5038
+ },
5039
+ close() {
5040
+ closed = true;
5041
+ for (const handle of pending.values())
5042
+ clearLater(handle);
5043
+ pending.clear();
5044
+ }
5045
+ };
5046
+ }
5047
+ function createLiveHub(_options = {}) {
5048
+ const listeners = /* @__PURE__ */ new Set();
5049
+ const drop = (listener) => {
5050
+ listeners.delete(listener);
5051
+ };
5052
+ const write2 = (listener, opcode, payload) => {
5053
+ if (listener.socket.destroyed || !listener.socket.writable) {
5054
+ drop(listener);
5055
+ return false;
5056
+ }
5057
+ try {
5058
+ listener.socket.write(encodeFrame(opcode, payload));
5059
+ return true;
5060
+ } catch {
5061
+ drop(listener);
5062
+ listener.socket.destroy();
5063
+ return false;
5064
+ }
5065
+ };
5066
+ const read = (listener, chunk) => {
5067
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5068
+ listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
5069
+ while (listener.buffered.length >= 2) {
5070
+ const first = listener.buffered[0] ?? 0;
5071
+ const second = listener.buffered[1] ?? 0;
5072
+ let length = second & 127;
5073
+ let offset = 2;
5074
+ if (length === 126) {
5075
+ if (listener.buffered.length < 4)
5076
+ return;
5077
+ length = listener.buffered.readUInt16BE(2);
5078
+ offset = 4;
5079
+ } else if (length === 127) {
5080
+ if (listener.buffered.length < 10)
5081
+ return;
5082
+ const wide = listener.buffered.readBigUInt64BE(2);
5083
+ if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
5084
+ drop(listener);
5085
+ listener.socket.destroy();
5086
+ return;
5087
+ }
5088
+ length = Number(wide);
5089
+ offset = 10;
5090
+ }
5091
+ const masked = (second & 128) !== 0;
5092
+ if (!masked) {
5093
+ drop(listener);
5094
+ listener.socket.destroy();
5095
+ return;
5096
+ }
5097
+ if (listener.buffered.length < offset + 4 + length)
5098
+ return;
5099
+ const mask = listener.buffered.subarray(offset, offset + 4);
5100
+ offset += 4;
5101
+ const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
5102
+ for (let index = 0; index < payload.length; index += 1) {
5103
+ payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
5104
+ }
5105
+ listener.buffered = listener.buffered.subarray(offset + length);
5106
+ const opcode = first & 15;
5107
+ if (opcode === 8) {
5108
+ write2(listener, 8, payload);
5109
+ drop(listener);
5110
+ listener.socket.destroy();
5111
+ return;
5112
+ }
5113
+ if (opcode === 9)
5114
+ write2(listener, 10, payload);
5115
+ }
5116
+ };
5117
+ return {
5118
+ nudge: (change) => {
5119
+ if (listeners.size === 0)
5120
+ return;
5121
+ const frame = encodeFrame(1, JSON.stringify({ changed: change }));
5122
+ for (const listener of [...listeners]) {
5123
+ if (listener.socket.destroyed || !listener.socket.writable) {
5124
+ drop(listener);
5125
+ continue;
5126
+ }
5127
+ try {
5128
+ listener.socket.write(frame);
5129
+ } catch {
5130
+ drop(listener);
5131
+ listener.socket.destroy();
5132
+ }
5133
+ }
5134
+ },
5135
+ upgrade: (req, socket, head) => {
5136
+ const path = (req.url ?? "/").split("?")[0] ?? "/";
5137
+ if (req.method !== "GET" || path !== LIVE_PATH)
5138
+ return false;
5139
+ const upgrade = req.headers.upgrade;
5140
+ const key = req.headers["sec-websocket-key"];
5141
+ if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
5142
+ socket.destroy();
5143
+ return false;
5144
+ }
5145
+ const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
5146
+ try {
5147
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
5148
+ Upgrade: websocket\r
5149
+ Connection: Upgrade\r
5150
+ Sec-WebSocket-Accept: ${accept}\r
5151
+ \r
5152
+ `);
5153
+ } catch {
5154
+ socket.destroy();
5155
+ return false;
5156
+ }
5157
+ const listener = { socket, buffered: Buffer.alloc(0) };
5158
+ listeners.add(listener);
5159
+ socket.on("data", (chunk) => read(listener, chunk));
5160
+ socket.once("error", () => drop(listener));
5161
+ socket.once("end", () => drop(listener));
5162
+ socket.once("close", () => drop(listener));
5163
+ if (head.length > 0)
5164
+ read(listener, head);
5165
+ return true;
5166
+ },
5167
+ close: async () => {
5168
+ for (const listener of [...listeners]) {
5169
+ write2(listener, 8, Buffer.alloc(0));
5170
+ drop(listener);
5171
+ listener.socket.destroy();
5172
+ }
5173
+ },
5174
+ get listening() {
5175
+ return listeners.size;
5176
+ }
5177
+ };
5178
+ }
5179
+
4982
5180
  // ../server/dist/renames.js
4983
5181
  import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
4984
5182
  import { dirname as dirname6, join as join10 } from "path";
@@ -5012,11 +5210,11 @@ function resolveTitle(input, titles, renames) {
5012
5210
  }
5013
5211
 
5014
5212
  // ../server/dist/server.js
5015
- import { createReadStream, existsSync as existsSync3, statSync } from "fs";
5213
+ import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
5016
5214
  import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
5017
5215
  import http2 from "http";
5018
5216
  import net3 from "net";
5019
- import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
5217
+ import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
5020
5218
 
5021
5219
  // ../server/dist/server-info.js
5022
5220
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
@@ -5201,6 +5399,204 @@ function serveShellFile(res, shellDir, urlPath) {
5201
5399
  const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
5202
5400
  return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
5203
5401
  }
5402
+ var HEALTH_PROBE_MS = 3e3;
5403
+ function fileStamp(path) {
5404
+ try {
5405
+ const stat5 = statSync(path);
5406
+ return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
5407
+ } catch {
5408
+ return null;
5409
+ }
5410
+ }
5411
+ function watchLiveFiles(cwd, configPath, live) {
5412
+ const leglasDir = join12(cwd, ".leglas");
5413
+ const targets = [
5414
+ { path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
5415
+ { path: join12(cwd, REQUESTS_PATH), change: "requests" },
5416
+ { path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
5417
+ ];
5418
+ const byName = new Map(targets.map((target) => [basename3(target.path), target]));
5419
+ const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
5420
+ const coalescer = createCoalescer((change) => live.nudge(change));
5421
+ const watchers = /* @__PURE__ */ new Set();
5422
+ const fallback = /* @__PURE__ */ new Map();
5423
+ let leglasWatcher = null;
5424
+ let retry = null;
5425
+ let closed = false;
5426
+ const nudgeSoon = (change) => coalescer.schedule(change);
5427
+ const scanLeglas = (notify) => {
5428
+ for (const target of targets) {
5429
+ const next = fileStamp(target.path);
5430
+ if (next === known.get(target.path))
5431
+ continue;
5432
+ known.set(target.path, next);
5433
+ if (notify)
5434
+ nudgeSoon(target.change);
5435
+ }
5436
+ };
5437
+ const fallbackWatch = (target) => {
5438
+ if (closed || fallback.has(target.path))
5439
+ return;
5440
+ const listener = () => {
5441
+ const next = fileStamp(target.path);
5442
+ if (next === known.get(target.path))
5443
+ return;
5444
+ known.set(target.path, next);
5445
+ nudgeSoon(target.change);
5446
+ };
5447
+ fallback.set(target.path, listener);
5448
+ watchFile(target.path, { persistent: false, interval: 250 }, listener);
5449
+ };
5450
+ const fallbackLeglas = () => {
5451
+ for (const target of targets)
5452
+ fallbackWatch(target);
5453
+ };
5454
+ const retryLeglas = () => {
5455
+ if (closed || retry !== null)
5456
+ return;
5457
+ retry = setTimeout(() => {
5458
+ retry = null;
5459
+ armLeglas(true);
5460
+ }, 250);
5461
+ retry.unref?.();
5462
+ };
5463
+ const armLeglas = (notify) => {
5464
+ if (closed)
5465
+ return;
5466
+ scanLeglas(notify);
5467
+ let directory = false;
5468
+ try {
5469
+ directory = statSync(leglasDir).isDirectory();
5470
+ } catch {
5471
+ directory = false;
5472
+ }
5473
+ if (!directory) {
5474
+ if (leglasWatcher !== null) {
5475
+ watchers.delete(leglasWatcher);
5476
+ leglasWatcher.close();
5477
+ leglasWatcher = null;
5478
+ }
5479
+ retryLeglas();
5480
+ return;
5481
+ }
5482
+ if (leglasWatcher !== null)
5483
+ return;
5484
+ try {
5485
+ const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
5486
+ if (filename === null) {
5487
+ scanLeglas(true);
5488
+ return;
5489
+ }
5490
+ const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
5491
+ const target = byName.get(name);
5492
+ if (target === void 0)
5493
+ return;
5494
+ known.set(target.path, fileStamp(target.path));
5495
+ nudgeSoon(target.change);
5496
+ });
5497
+ watcher.on("error", () => {
5498
+ if (leglasWatcher !== watcher)
5499
+ return;
5500
+ watchers.delete(watcher);
5501
+ watcher.close();
5502
+ leglasWatcher = null;
5503
+ fallbackLeglas();
5504
+ retryLeglas();
5505
+ });
5506
+ leglasWatcher = watcher;
5507
+ watchers.add(watcher);
5508
+ } catch {
5509
+ fallbackLeglas();
5510
+ retryLeglas();
5511
+ }
5512
+ };
5513
+ try {
5514
+ const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
5515
+ const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5516
+ if (name === null || name === ".leglas")
5517
+ armLeglas(true);
5518
+ });
5519
+ watcher.on("error", () => {
5520
+ watchers.delete(watcher);
5521
+ watcher.close();
5522
+ fallbackLeglas();
5523
+ });
5524
+ watchers.add(watcher);
5525
+ } catch {
5526
+ fallbackLeglas();
5527
+ }
5528
+ if (configPath !== null) {
5529
+ try {
5530
+ const directory = dirname8(configPath);
5531
+ const name = basename3(configPath);
5532
+ const target = { path: configPath, change: "config" };
5533
+ known.set(configPath, fileStamp(configPath));
5534
+ const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
5535
+ const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5536
+ if (changed === null || changed === name)
5537
+ nudgeSoon("config");
5538
+ });
5539
+ watcher.on("error", () => {
5540
+ watchers.delete(watcher);
5541
+ watcher.close();
5542
+ fallbackWatch(target);
5543
+ });
5544
+ watchers.add(watcher);
5545
+ } catch {
5546
+ fallbackWatch({ path: configPath, change: "config" });
5547
+ }
5548
+ }
5549
+ armLeglas(false);
5550
+ return {
5551
+ close: () => {
5552
+ if (closed)
5553
+ return;
5554
+ closed = true;
5555
+ if (retry !== null)
5556
+ clearTimeout(retry);
5557
+ coalescer.close();
5558
+ for (const watcher of watchers)
5559
+ watcher.close();
5560
+ watchers.clear();
5561
+ for (const [path, listener] of fallback)
5562
+ unwatchFile(path, listener);
5563
+ fallback.clear();
5564
+ leglasWatcher = null;
5565
+ }
5566
+ };
5567
+ }
5568
+ function watchHealth(target, live) {
5569
+ let previous = null;
5570
+ let probing = false;
5571
+ let closed = false;
5572
+ const timer = setInterval(() => {
5573
+ if (live.listening === 0) {
5574
+ previous = null;
5575
+ return;
5576
+ }
5577
+ if (probing)
5578
+ return;
5579
+ probing = true;
5580
+ void probe(target).then((reachable) => {
5581
+ if (closed || live.listening === 0) {
5582
+ previous = null;
5583
+ return;
5584
+ }
5585
+ if (previous !== null && previous !== reachable)
5586
+ live.nudge("health");
5587
+ previous = reachable;
5588
+ }).finally(() => {
5589
+ probing = false;
5590
+ });
5591
+ }, HEALTH_PROBE_MS);
5592
+ timer.unref?.();
5593
+ return {
5594
+ close: () => {
5595
+ closed = true;
5596
+ clearInterval(timer);
5597
+ }
5598
+ };
5599
+ }
5204
5600
  function snapshotConfig(cwd) {
5205
5601
  const path = findConfigFile(cwd);
5206
5602
  if (path === null)
@@ -5271,12 +5667,14 @@ async function bind(server, requested) {
5271
5667
  async function startServer(options) {
5272
5668
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
5273
5669
  const browserPool = options.pool ?? createBrowserPool();
5670
+ const live = options.live ?? createLiveHub();
5274
5671
  if (options.pool === void 0) {
5275
5672
  void reapOrphanedBrowsers().catch(() => {
5276
5673
  });
5277
5674
  }
5278
5675
  const target = config?.devServer ?? "http://localhost:3000";
5279
5676
  const proxy = createProxyHandler({ target });
5677
+ const bootConfigPath = findConfigFile(cwd);
5280
5678
  const bootConfigSnapshot = snapshotConfig(cwd);
5281
5679
  let lastSeen = null;
5282
5680
  const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
@@ -5501,14 +5899,14 @@ async function startServer(options) {
5501
5899
  return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
5502
5900
  }
5503
5901
  const intent = (parsed.intent ?? "").trim();
5504
- const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5902
+ const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5505
5903
  const sameNotes = (entry) => {
5506
5904
  const before = [...entry.notes ?? []].sort().join(",");
5507
5905
  return before === notes.map((note) => note.id).sort().join(",");
5508
5906
  };
5509
5907
  const compare = typeof parsed.compare === "string" && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
5510
5908
  const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
5511
- if (live.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
5909
+ if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
5512
5910
  // one forks the direction and the other rewrites it. Only a
5513
5911
  // genuine repeat is refused.
5514
5912
  (entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
@@ -6019,12 +6417,16 @@ async function startServer(options) {
6019
6417
  socket.once("close", () => sockets.delete(socket));
6020
6418
  });
6021
6419
  server.on("upgrade", (req, socket, head) => {
6420
+ if (live.upgrade(req, socket, head))
6421
+ return;
6022
6422
  const path = (req.url ?? "/").split("?")[0] ?? "/";
6023
6423
  if (path.startsWith(`${LEGLAS_PREFIX}/`))
6024
6424
  return socket.destroy();
6025
6425
  proxy.upgrade(req, socket, head);
6026
6426
  });
6027
6427
  const port = await bind(server, options.port ?? DEFAULT_PORT);
6428
+ const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
6429
+ const liveHealth = watchHealth(target, live);
6028
6430
  await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
6029
6431
  });
6030
6432
  await writeServerInfo(cwd, {
@@ -6036,6 +6438,7 @@ async function startServer(options) {
6036
6438
  runner = startRunner({
6037
6439
  cwd,
6038
6440
  externallyAttached,
6441
+ onChange: () => live.nudge("requests"),
6039
6442
  leglasCommand,
6040
6443
  ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
6041
6444
  ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
@@ -6047,7 +6450,9 @@ async function startServer(options) {
6047
6450
  close: () => {
6048
6451
  if (closePromise !== null)
6049
6452
  return closePromise;
6050
- closePromise = Promise.all([runner.stop(), browserPool.close()]).then(() => new Promise((done) => {
6453
+ liveFiles.close();
6454
+ liveHealth.close();
6455
+ closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
6051
6456
  for (const socket of sockets)
6052
6457
  socket.destroy();
6053
6458
  sockets.clear();
@@ -6070,7 +6475,7 @@ function surfaceOf(url) {
6070
6475
  return null;
6071
6476
  }
6072
6477
  function exportNameFor(to) {
6073
- const stem = basename3(to, extname4(to));
6478
+ const stem = basename4(to, extname4(to));
6074
6479
  return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
6075
6480
  }
6076
6481
  function planKeep(options) {
@@ -6159,7 +6564,7 @@ async function runInit(options, deps) {
6159
6564
  // src/run-keep.ts
6160
6565
  import { existsSync as existsSync4 } from "fs";
6161
6566
  import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
6162
- import { dirname as dirname8, join as join14 } from "path";
6567
+ import { dirname as dirname9, join as join14 } from "path";
6163
6568
 
6164
6569
  // src/resolve-title.ts
6165
6570
  function resolveOrExplain(input, titles, renames) {
@@ -6212,7 +6617,7 @@ async function runKeep(options, deps) {
6212
6617
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
6213
6618
  }
6214
6619
  const source = await readFile13(from, "utf8");
6215
- await mkdir9(dirname8(to), { recursive: true });
6620
+ await mkdir9(dirname9(to), { recursive: true });
6216
6621
  await writeFile10(to, renameExport(source, plan.exportName), "utf8");
6217
6622
  await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
6218
6623
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
@@ -6250,7 +6655,7 @@ async function runKeep(options, deps) {
6250
6655
  // src/run-new.ts
6251
6656
  import { existsSync as existsSync5 } from "fs";
6252
6657
  import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
6253
- import { dirname as dirname9, join as join15 } from "path";
6658
+ import { dirname as dirname10, join as join15 } from "path";
6254
6659
  async function readIfPresent2(path) {
6255
6660
  try {
6256
6661
  return await readFile14(path, "utf8");
@@ -6303,7 +6708,7 @@ async function runNew(options, deps) {
6303
6708
  const written = [];
6304
6709
  for (const write2 of plan.writes) {
6305
6710
  const target = join15(options.cwd, write2.path);
6306
- await mkdir10(dirname9(target), { recursive: true });
6711
+ await mkdir10(dirname10(target), { recursive: true });
6307
6712
  await writeFile11(target, write2.contents, "utf8");
6308
6713
  written.push(write2.path);
6309
6714
  }
@@ -6664,7 +7069,7 @@ async function runShow(options, deps) {
6664
7069
  // src/run-watch.ts
6665
7070
  import { spawn as spawn3 } from "child_process";
6666
7071
  import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
6667
- import { dirname as dirname10, join as join17 } from "path";
7072
+ import { dirname as dirname11, join as join17 } from "path";
6668
7073
  var POLL_MS2 = 2e3;
6669
7074
  var HEARTBEAT_TIMEOUT_MS = 1e3;
6670
7075
  async function saveTemplate(cwd, run4) {
@@ -6678,7 +7083,7 @@ async function saveTemplate(cwd, run4) {
6678
7083
  } catch {
6679
7084
  }
6680
7085
  config.run = run4;
6681
- await mkdir11(dirname10(path), { recursive: true });
7086
+ await mkdir11(dirname11(path), { recursive: true });
6682
7087
  await writeFile13(path, `${JSON.stringify(config, null, 2)}
6683
7088
  `, "utf8");
6684
7089
  }
@@ -6852,13 +7257,13 @@ async function runClassify(options, deps) {
6852
7257
  import { existsSync as existsSync6 } from "fs";
6853
7258
  import { realpath as realpath4 } from "fs/promises";
6854
7259
  import { createRequire } from "module";
6855
- import { basename as basename5, dirname as dirname11, join as join19, relative as relative5, resolve as resolve4 } from "path";
7260
+ import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
6856
7261
  import { fileURLToPath } from "url";
6857
7262
 
6858
7263
  // src/dev-server-owner.ts
6859
7264
  import { execFile as execFile2 } from "child_process";
6860
7265
  import { realpath as realpath3 } from "fs/promises";
6861
- import { basename as basename4, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
7266
+ import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
6862
7267
  import { promisify as promisify2 } from "util";
6863
7268
  var run2 = promisify2(execFile2);
6864
7269
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
@@ -6931,18 +7336,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
6931
7336
  const port = localDevServerPort(origin);
6932
7337
  if (port === null || owners.length === 0) return null;
6933
7338
  if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
6934
- const owner = basename4(resolve3(owners[0]?.cwd ?? "")) || "another project";
6935
- const project = basename4(resolve3(projectRoot)) || "this project";
7339
+ const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
7340
+ const project = basename5(resolve3(projectRoot)) || "this project";
6936
7341
  return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
6937
7342
  }
6938
7343
 
6939
7344
  // src/run.ts
6940
7345
  function findShellDir() {
6941
- const bundled = join19(dirname11(fileURLToPath(import.meta.url)), "shell");
7346
+ const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
6942
7347
  if (existsSync6(join19(bundled, "index.html"))) return bundled;
6943
7348
  try {
6944
7349
  const require2 = createRequire(import.meta.url);
6945
- return dirname11(require2.resolve("@leglas/shell/dist/index.html"));
7350
+ return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
6946
7351
  } catch {
6947
7352
  return null;
6948
7353
  }
@@ -6953,7 +7358,7 @@ function shellWord(value) {
6953
7358
  return `'${value.replaceAll("'", `'\\''`)}'`;
6954
7359
  }
6955
7360
  function embeddedLeglasCommand() {
6956
- const entry = join19(dirname11(fileURLToPath(import.meta.url)), "bin.js");
7361
+ const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
6957
7362
  if (!existsSync6(entry)) return "npx -y leglas";
6958
7363
  return [process.execPath, entry].map(shellWord).join(" ");
6959
7364
  }
@@ -6997,10 +7402,10 @@ async function run3(options, deps) {
6997
7402
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
6998
7403
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
6999
7404
  }
7000
- fileMounts.set(slug, dirname11(absolute));
7405
+ fileMounts.set(slug, dirname12(absolute));
7001
7406
  previews.push({
7002
7407
  ...preview,
7003
- url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename5(absolute))}`
7408
+ url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
7004
7409
  });
7005
7410
  continue;
7006
7411
  }
@@ -7030,8 +7435,8 @@ async function run3(options, deps) {
7030
7435
  }
7031
7436
  const config = merged === null ? null : { ...merged, previews };
7032
7437
  const configWarnings = [];
7033
- const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname11(loaded.path)).catch(
7034
- () => resolve4(loaded.path === null ? options.cwd : dirname11(loaded.path))
7438
+ const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
7439
+ () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
7035
7440
  );
7036
7441
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7037
7442
  const serverPromise = startServer({