leglas 0.7.1 → 0.7.2

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
  }
@@ -4602,6 +4602,10 @@ function startRunner(options) {
4602
4602
  stopping: false,
4603
4603
  waiting: null
4604
4604
  };
4605
+ const setState = (next) => {
4606
+ state = typeof next === "function" ? next(state) : next;
4607
+ options.onChange?.();
4608
+ };
4605
4609
  let stopped = false;
4606
4610
  let ticking = null;
4607
4611
  let pendingNudges = 0;
@@ -4653,7 +4657,7 @@ function startRunner(options) {
4653
4657
  return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
4654
4658
  };
4655
4659
  const idle = () => {
4656
- state = {
4660
+ setState({
4657
4661
  running: false,
4658
4662
  requestId: null,
4659
4663
  agent: null,
@@ -4661,7 +4665,7 @@ function startRunner(options) {
4661
4665
  startedAt: null,
4662
4666
  stopping: false,
4663
4667
  waiting: null
4664
- };
4668
+ });
4665
4669
  };
4666
4670
  const rememberLine = (lines2, line) => {
4667
4671
  lines2.push(line);
@@ -4769,14 +4773,15 @@ function startRunner(options) {
4769
4773
  if (retry !== null) {
4770
4774
  observed.retry = retry;
4771
4775
  if (active === current)
4772
- state = { ...state, waiting: retry };
4776
+ setState((value) => ({ ...value, waiting: retry }));
4773
4777
  }
4774
4778
  const activity = activityFrom(resolved.agent, line, options.cwd);
4775
4779
  if (activity !== null) {
4776
4780
  if (activity.startsWith("editing"))
4777
4781
  observed.edited = true;
4778
- if (active === current)
4779
- state = { ...state, activity, waiting: null };
4782
+ if (active === current) {
4783
+ setState((value) => ({ ...value, activity, waiting: null }));
4784
+ }
4780
4785
  }
4781
4786
  });
4782
4787
  const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
@@ -4823,7 +4828,7 @@ function startRunner(options) {
4823
4828
  await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
4824
4829
  return;
4825
4830
  }
4826
- state = {
4831
+ setState({
4827
4832
  running: true,
4828
4833
  requestId: request.id,
4829
4834
  agent: resolved.name,
@@ -4831,7 +4836,7 @@ function startRunner(options) {
4831
4836
  startedAt: Date.now(),
4832
4837
  stopping: false,
4833
4838
  waiting: null
4834
- };
4839
+ });
4835
4840
  const observed = {
4836
4841
  sessionId: null,
4837
4842
  edited: false,
@@ -4862,7 +4867,7 @@ function startRunner(options) {
4862
4867
  resolved = cold;
4863
4868
  observed.sessionId = null;
4864
4869
  observed.retry = null;
4865
- state = { ...state, activity: null, waiting: null };
4870
+ setState((value) => ({ ...value, activity: null, waiting: null }));
4866
4871
  outcome = await runChild(request, resolved, lines2, observed);
4867
4872
  failure = verdict();
4868
4873
  }
@@ -4936,7 +4941,7 @@ function startRunner(options) {
4936
4941
  const current = active;
4937
4942
  current.cancelled = true;
4938
4943
  failed.add(current.requestId);
4939
- state = { ...state, stopping: true, waiting: null };
4944
+ setState((value) => ({ ...value, stopping: true, waiting: null }));
4940
4945
  current.controller.abort();
4941
4946
  try {
4942
4947
  current.child?.kill("SIGTERM");
@@ -4979,6 +4984,193 @@ function startRunner(options) {
4979
4984
  };
4980
4985
  }
4981
4986
 
4987
+ // ../server/dist/live.js
4988
+ import { createHash } from "crypto";
4989
+ var LIVE_PATH = "/leglas/api/live";
4990
+ var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4991
+ function encodeFrame(opcode, payload) {
4992
+ const body = typeof payload === "string" ? Buffer.from(payload) : payload;
4993
+ let header;
4994
+ if (body.length < 126) {
4995
+ header = Buffer.allocUnsafe(2);
4996
+ header[1] = body.length;
4997
+ } else if (body.length <= 65535) {
4998
+ header = Buffer.allocUnsafe(4);
4999
+ header[1] = 126;
5000
+ header.writeUInt16BE(body.length, 2);
5001
+ } else {
5002
+ header = Buffer.allocUnsafe(10);
5003
+ header[1] = 127;
5004
+ header.writeBigUInt64BE(BigInt(body.length), 2);
5005
+ }
5006
+ header[0] = 128 | opcode & 15;
5007
+ return Buffer.concat([header, body], header.length + body.length);
5008
+ }
5009
+ var LIVE_DEBOUNCE_MS = 50;
5010
+ function createCoalescer(emit, options = {}) {
5011
+ const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
5012
+ const setLater = options.setTimeout ?? ((callback, ms) => {
5013
+ const timer = setTimeout(callback, ms);
5014
+ timer.unref?.();
5015
+ return timer;
5016
+ });
5017
+ const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
5018
+ const pending = /* @__PURE__ */ new Map();
5019
+ let closed = false;
5020
+ return {
5021
+ schedule(change) {
5022
+ if (closed)
5023
+ return;
5024
+ const waiting = pending.get(change);
5025
+ if (waiting !== void 0)
5026
+ clearLater(waiting);
5027
+ pending.set(change, setLater(() => {
5028
+ pending.delete(change);
5029
+ if (!closed)
5030
+ emit(change);
5031
+ }, windowMs));
5032
+ },
5033
+ close() {
5034
+ closed = true;
5035
+ for (const handle of pending.values())
5036
+ clearLater(handle);
5037
+ pending.clear();
5038
+ }
5039
+ };
5040
+ }
5041
+ function createLiveHub(_options = {}) {
5042
+ const listeners = /* @__PURE__ */ new Set();
5043
+ const drop = (listener) => {
5044
+ listeners.delete(listener);
5045
+ };
5046
+ const write2 = (listener, opcode, payload) => {
5047
+ if (listener.socket.destroyed || !listener.socket.writable) {
5048
+ drop(listener);
5049
+ return false;
5050
+ }
5051
+ try {
5052
+ listener.socket.write(encodeFrame(opcode, payload));
5053
+ return true;
5054
+ } catch {
5055
+ drop(listener);
5056
+ listener.socket.destroy();
5057
+ return false;
5058
+ }
5059
+ };
5060
+ const read = (listener, chunk) => {
5061
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5062
+ listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
5063
+ while (listener.buffered.length >= 2) {
5064
+ const first = listener.buffered[0] ?? 0;
5065
+ const second = listener.buffered[1] ?? 0;
5066
+ let length = second & 127;
5067
+ let offset = 2;
5068
+ if (length === 126) {
5069
+ if (listener.buffered.length < 4)
5070
+ return;
5071
+ length = listener.buffered.readUInt16BE(2);
5072
+ offset = 4;
5073
+ } else if (length === 127) {
5074
+ if (listener.buffered.length < 10)
5075
+ return;
5076
+ const wide = listener.buffered.readBigUInt64BE(2);
5077
+ if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
5078
+ drop(listener);
5079
+ listener.socket.destroy();
5080
+ return;
5081
+ }
5082
+ length = Number(wide);
5083
+ offset = 10;
5084
+ }
5085
+ const masked = (second & 128) !== 0;
5086
+ if (!masked) {
5087
+ drop(listener);
5088
+ listener.socket.destroy();
5089
+ return;
5090
+ }
5091
+ if (listener.buffered.length < offset + 4 + length)
5092
+ return;
5093
+ const mask = listener.buffered.subarray(offset, offset + 4);
5094
+ offset += 4;
5095
+ const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
5096
+ for (let index = 0; index < payload.length; index += 1) {
5097
+ payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
5098
+ }
5099
+ listener.buffered = listener.buffered.subarray(offset + length);
5100
+ const opcode = first & 15;
5101
+ if (opcode === 8) {
5102
+ write2(listener, 8, payload);
5103
+ drop(listener);
5104
+ listener.socket.destroy();
5105
+ return;
5106
+ }
5107
+ if (opcode === 9)
5108
+ write2(listener, 10, payload);
5109
+ }
5110
+ };
5111
+ return {
5112
+ nudge: (change) => {
5113
+ if (listeners.size === 0)
5114
+ return;
5115
+ const frame = encodeFrame(1, JSON.stringify({ changed: change }));
5116
+ for (const listener of [...listeners]) {
5117
+ if (listener.socket.destroyed || !listener.socket.writable) {
5118
+ drop(listener);
5119
+ continue;
5120
+ }
5121
+ try {
5122
+ listener.socket.write(frame);
5123
+ } catch {
5124
+ drop(listener);
5125
+ listener.socket.destroy();
5126
+ }
5127
+ }
5128
+ },
5129
+ upgrade: (req, socket, head) => {
5130
+ const path = (req.url ?? "/").split("?")[0] ?? "/";
5131
+ if (req.method !== "GET" || path !== LIVE_PATH)
5132
+ return false;
5133
+ const upgrade = req.headers.upgrade;
5134
+ const key = req.headers["sec-websocket-key"];
5135
+ if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
5136
+ socket.destroy();
5137
+ return false;
5138
+ }
5139
+ const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
5140
+ try {
5141
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
5142
+ Upgrade: websocket\r
5143
+ Connection: Upgrade\r
5144
+ Sec-WebSocket-Accept: ${accept}\r
5145
+ \r
5146
+ `);
5147
+ } catch {
5148
+ socket.destroy();
5149
+ return false;
5150
+ }
5151
+ const listener = { socket, buffered: Buffer.alloc(0) };
5152
+ listeners.add(listener);
5153
+ socket.on("data", (chunk) => read(listener, chunk));
5154
+ socket.once("error", () => drop(listener));
5155
+ socket.once("end", () => drop(listener));
5156
+ socket.once("close", () => drop(listener));
5157
+ if (head.length > 0)
5158
+ read(listener, head);
5159
+ return true;
5160
+ },
5161
+ close: async () => {
5162
+ for (const listener of [...listeners]) {
5163
+ write2(listener, 8, Buffer.alloc(0));
5164
+ drop(listener);
5165
+ listener.socket.destroy();
5166
+ }
5167
+ },
5168
+ get listening() {
5169
+ return listeners.size;
5170
+ }
5171
+ };
5172
+ }
5173
+
4982
5174
  // ../server/dist/renames.js
4983
5175
  import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
4984
5176
  import { dirname as dirname6, join as join10 } from "path";
@@ -5012,11 +5204,11 @@ function resolveTitle(input, titles, renames) {
5012
5204
  }
5013
5205
 
5014
5206
  // ../server/dist/server.js
5015
- import { createReadStream, existsSync as existsSync3, statSync } from "fs";
5207
+ import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
5016
5208
  import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
5017
5209
  import http2 from "http";
5018
5210
  import net3 from "net";
5019
- import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
5211
+ import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
5020
5212
 
5021
5213
  // ../server/dist/server-info.js
5022
5214
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
@@ -5201,6 +5393,204 @@ function serveShellFile(res, shellDir, urlPath) {
5201
5393
  const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
5202
5394
  return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
5203
5395
  }
5396
+ var HEALTH_PROBE_MS = 3e3;
5397
+ function fileStamp(path) {
5398
+ try {
5399
+ const stat5 = statSync(path);
5400
+ return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
5401
+ } catch {
5402
+ return null;
5403
+ }
5404
+ }
5405
+ function watchLiveFiles(cwd, configPath, live) {
5406
+ const leglasDir = join12(cwd, ".leglas");
5407
+ const targets = [
5408
+ { path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
5409
+ { path: join12(cwd, REQUESTS_PATH), change: "requests" },
5410
+ { path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
5411
+ ];
5412
+ const byName = new Map(targets.map((target) => [basename3(target.path), target]));
5413
+ const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
5414
+ const coalescer = createCoalescer((change) => live.nudge(change));
5415
+ const watchers = /* @__PURE__ */ new Set();
5416
+ const fallback = /* @__PURE__ */ new Map();
5417
+ let leglasWatcher = null;
5418
+ let retry = null;
5419
+ let closed = false;
5420
+ const nudgeSoon = (change) => coalescer.schedule(change);
5421
+ const scanLeglas = (notify) => {
5422
+ for (const target of targets) {
5423
+ const next = fileStamp(target.path);
5424
+ if (next === known.get(target.path))
5425
+ continue;
5426
+ known.set(target.path, next);
5427
+ if (notify)
5428
+ nudgeSoon(target.change);
5429
+ }
5430
+ };
5431
+ const fallbackWatch = (target) => {
5432
+ if (closed || fallback.has(target.path))
5433
+ return;
5434
+ const listener = () => {
5435
+ const next = fileStamp(target.path);
5436
+ if (next === known.get(target.path))
5437
+ return;
5438
+ known.set(target.path, next);
5439
+ nudgeSoon(target.change);
5440
+ };
5441
+ fallback.set(target.path, listener);
5442
+ watchFile(target.path, { persistent: false, interval: 250 }, listener);
5443
+ };
5444
+ const fallbackLeglas = () => {
5445
+ for (const target of targets)
5446
+ fallbackWatch(target);
5447
+ };
5448
+ const retryLeglas = () => {
5449
+ if (closed || retry !== null)
5450
+ return;
5451
+ retry = setTimeout(() => {
5452
+ retry = null;
5453
+ armLeglas(true);
5454
+ }, 250);
5455
+ retry.unref?.();
5456
+ };
5457
+ const armLeglas = (notify) => {
5458
+ if (closed)
5459
+ return;
5460
+ scanLeglas(notify);
5461
+ let directory = false;
5462
+ try {
5463
+ directory = statSync(leglasDir).isDirectory();
5464
+ } catch {
5465
+ directory = false;
5466
+ }
5467
+ if (!directory) {
5468
+ if (leglasWatcher !== null) {
5469
+ watchers.delete(leglasWatcher);
5470
+ leglasWatcher.close();
5471
+ leglasWatcher = null;
5472
+ }
5473
+ retryLeglas();
5474
+ return;
5475
+ }
5476
+ if (leglasWatcher !== null)
5477
+ return;
5478
+ try {
5479
+ const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
5480
+ if (filename === null) {
5481
+ scanLeglas(true);
5482
+ return;
5483
+ }
5484
+ const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
5485
+ const target = byName.get(name);
5486
+ if (target === void 0)
5487
+ return;
5488
+ known.set(target.path, fileStamp(target.path));
5489
+ nudgeSoon(target.change);
5490
+ });
5491
+ watcher.on("error", () => {
5492
+ if (leglasWatcher !== watcher)
5493
+ return;
5494
+ watchers.delete(watcher);
5495
+ watcher.close();
5496
+ leglasWatcher = null;
5497
+ fallbackLeglas();
5498
+ retryLeglas();
5499
+ });
5500
+ leglasWatcher = watcher;
5501
+ watchers.add(watcher);
5502
+ } catch {
5503
+ fallbackLeglas();
5504
+ retryLeglas();
5505
+ }
5506
+ };
5507
+ try {
5508
+ const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
5509
+ const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5510
+ if (name === null || name === ".leglas")
5511
+ armLeglas(true);
5512
+ });
5513
+ watcher.on("error", () => {
5514
+ watchers.delete(watcher);
5515
+ watcher.close();
5516
+ fallbackLeglas();
5517
+ });
5518
+ watchers.add(watcher);
5519
+ } catch {
5520
+ fallbackLeglas();
5521
+ }
5522
+ if (configPath !== null) {
5523
+ try {
5524
+ const directory = dirname8(configPath);
5525
+ const name = basename3(configPath);
5526
+ const target = { path: configPath, change: "config" };
5527
+ known.set(configPath, fileStamp(configPath));
5528
+ const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
5529
+ const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5530
+ if (changed === null || changed === name)
5531
+ nudgeSoon("config");
5532
+ });
5533
+ watcher.on("error", () => {
5534
+ watchers.delete(watcher);
5535
+ watcher.close();
5536
+ fallbackWatch(target);
5537
+ });
5538
+ watchers.add(watcher);
5539
+ } catch {
5540
+ fallbackWatch({ path: configPath, change: "config" });
5541
+ }
5542
+ }
5543
+ armLeglas(false);
5544
+ return {
5545
+ close: () => {
5546
+ if (closed)
5547
+ return;
5548
+ closed = true;
5549
+ if (retry !== null)
5550
+ clearTimeout(retry);
5551
+ coalescer.close();
5552
+ for (const watcher of watchers)
5553
+ watcher.close();
5554
+ watchers.clear();
5555
+ for (const [path, listener] of fallback)
5556
+ unwatchFile(path, listener);
5557
+ fallback.clear();
5558
+ leglasWatcher = null;
5559
+ }
5560
+ };
5561
+ }
5562
+ function watchHealth(target, live) {
5563
+ let previous = null;
5564
+ let probing = false;
5565
+ let closed = false;
5566
+ const timer = setInterval(() => {
5567
+ if (live.listening === 0) {
5568
+ previous = null;
5569
+ return;
5570
+ }
5571
+ if (probing)
5572
+ return;
5573
+ probing = true;
5574
+ void probe(target).then((reachable) => {
5575
+ if (closed || live.listening === 0) {
5576
+ previous = null;
5577
+ return;
5578
+ }
5579
+ if (previous !== null && previous !== reachable)
5580
+ live.nudge("health");
5581
+ previous = reachable;
5582
+ }).finally(() => {
5583
+ probing = false;
5584
+ });
5585
+ }, HEALTH_PROBE_MS);
5586
+ timer.unref?.();
5587
+ return {
5588
+ close: () => {
5589
+ closed = true;
5590
+ clearInterval(timer);
5591
+ }
5592
+ };
5593
+ }
5204
5594
  function snapshotConfig(cwd) {
5205
5595
  const path = findConfigFile(cwd);
5206
5596
  if (path === null)
@@ -5271,12 +5661,14 @@ async function bind(server, requested) {
5271
5661
  async function startServer(options) {
5272
5662
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
5273
5663
  const browserPool = options.pool ?? createBrowserPool();
5664
+ const live = options.live ?? createLiveHub();
5274
5665
  if (options.pool === void 0) {
5275
5666
  void reapOrphanedBrowsers().catch(() => {
5276
5667
  });
5277
5668
  }
5278
5669
  const target = config?.devServer ?? "http://localhost:3000";
5279
5670
  const proxy = createProxyHandler({ target });
5671
+ const bootConfigPath = findConfigFile(cwd);
5280
5672
  const bootConfigSnapshot = snapshotConfig(cwd);
5281
5673
  let lastSeen = null;
5282
5674
  const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
@@ -5501,14 +5893,14 @@ async function startServer(options) {
5501
5893
  return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
5502
5894
  }
5503
5895
  const intent = (parsed.intent ?? "").trim();
5504
- const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5896
+ const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5505
5897
  const sameNotes = (entry) => {
5506
5898
  const before = [...entry.notes ?? []].sort().join(",");
5507
5899
  return before === notes.map((note) => note.id).sort().join(",");
5508
5900
  };
5509
5901
  const compare = typeof parsed.compare === "string" && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
5510
5902
  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:
5903
+ if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
5512
5904
  // one forks the direction and the other rewrites it. Only a
5513
5905
  // genuine repeat is refused.
5514
5906
  (entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
@@ -6019,12 +6411,16 @@ async function startServer(options) {
6019
6411
  socket.once("close", () => sockets.delete(socket));
6020
6412
  });
6021
6413
  server.on("upgrade", (req, socket, head) => {
6414
+ if (live.upgrade(req, socket, head))
6415
+ return;
6022
6416
  const path = (req.url ?? "/").split("?")[0] ?? "/";
6023
6417
  if (path.startsWith(`${LEGLAS_PREFIX}/`))
6024
6418
  return socket.destroy();
6025
6419
  proxy.upgrade(req, socket, head);
6026
6420
  });
6027
6421
  const port = await bind(server, options.port ?? DEFAULT_PORT);
6422
+ const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
6423
+ const liveHealth = watchHealth(target, live);
6028
6424
  await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
6029
6425
  });
6030
6426
  await writeServerInfo(cwd, {
@@ -6036,6 +6432,7 @@ async function startServer(options) {
6036
6432
  runner = startRunner({
6037
6433
  cwd,
6038
6434
  externallyAttached,
6435
+ onChange: () => live.nudge("requests"),
6039
6436
  leglasCommand,
6040
6437
  ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
6041
6438
  ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
@@ -6047,7 +6444,9 @@ async function startServer(options) {
6047
6444
  close: () => {
6048
6445
  if (closePromise !== null)
6049
6446
  return closePromise;
6050
- closePromise = Promise.all([runner.stop(), browserPool.close()]).then(() => new Promise((done) => {
6447
+ liveFiles.close();
6448
+ liveHealth.close();
6449
+ closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
6051
6450
  for (const socket of sockets)
6052
6451
  socket.destroy();
6053
6452
  sockets.clear();
@@ -6070,7 +6469,7 @@ function surfaceOf(url) {
6070
6469
  return null;
6071
6470
  }
6072
6471
  function exportNameFor(to) {
6073
- const stem = basename3(to, extname4(to));
6472
+ const stem = basename4(to, extname4(to));
6074
6473
  return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
6075
6474
  }
6076
6475
  function planKeep(options) {
@@ -6159,7 +6558,7 @@ async function runInit(options, deps) {
6159
6558
  // src/run-keep.ts
6160
6559
  import { existsSync as existsSync4 } from "fs";
6161
6560
  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";
6561
+ import { dirname as dirname9, join as join14 } from "path";
6163
6562
 
6164
6563
  // src/resolve-title.ts
6165
6564
  function resolveOrExplain(input, titles, renames) {
@@ -6212,7 +6611,7 @@ async function runKeep(options, deps) {
6212
6611
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
6213
6612
  }
6214
6613
  const source = await readFile13(from, "utf8");
6215
- await mkdir9(dirname8(to), { recursive: true });
6614
+ await mkdir9(dirname9(to), { recursive: true });
6216
6615
  await writeFile10(to, renameExport(source, plan.exportName), "utf8");
6217
6616
  await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
6218
6617
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
@@ -6250,7 +6649,7 @@ async function runKeep(options, deps) {
6250
6649
  // src/run-new.ts
6251
6650
  import { existsSync as existsSync5 } from "fs";
6252
6651
  import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
6253
- import { dirname as dirname9, join as join15 } from "path";
6652
+ import { dirname as dirname10, join as join15 } from "path";
6254
6653
  async function readIfPresent2(path) {
6255
6654
  try {
6256
6655
  return await readFile14(path, "utf8");
@@ -6303,7 +6702,7 @@ async function runNew(options, deps) {
6303
6702
  const written = [];
6304
6703
  for (const write2 of plan.writes) {
6305
6704
  const target = join15(options.cwd, write2.path);
6306
- await mkdir10(dirname9(target), { recursive: true });
6705
+ await mkdir10(dirname10(target), { recursive: true });
6307
6706
  await writeFile11(target, write2.contents, "utf8");
6308
6707
  written.push(write2.path);
6309
6708
  }
@@ -6664,7 +7063,7 @@ async function runShow(options, deps) {
6664
7063
  // src/run-watch.ts
6665
7064
  import { spawn as spawn3 } from "child_process";
6666
7065
  import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
6667
- import { dirname as dirname10, join as join17 } from "path";
7066
+ import { dirname as dirname11, join as join17 } from "path";
6668
7067
  var POLL_MS2 = 2e3;
6669
7068
  var HEARTBEAT_TIMEOUT_MS = 1e3;
6670
7069
  async function saveTemplate(cwd, run4) {
@@ -6678,7 +7077,7 @@ async function saveTemplate(cwd, run4) {
6678
7077
  } catch {
6679
7078
  }
6680
7079
  config.run = run4;
6681
- await mkdir11(dirname10(path), { recursive: true });
7080
+ await mkdir11(dirname11(path), { recursive: true });
6682
7081
  await writeFile13(path, `${JSON.stringify(config, null, 2)}
6683
7082
  `, "utf8");
6684
7083
  }
@@ -6852,13 +7251,13 @@ async function runClassify(options, deps) {
6852
7251
  import { existsSync as existsSync6 } from "fs";
6853
7252
  import { realpath as realpath4 } from "fs/promises";
6854
7253
  import { createRequire } from "module";
6855
- import { basename as basename5, dirname as dirname11, join as join19, relative as relative5, resolve as resolve4 } from "path";
7254
+ import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
6856
7255
  import { fileURLToPath } from "url";
6857
7256
 
6858
7257
  // src/dev-server-owner.ts
6859
7258
  import { execFile as execFile2 } from "child_process";
6860
7259
  import { realpath as realpath3 } from "fs/promises";
6861
- import { basename as basename4, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
7260
+ import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
6862
7261
  import { promisify as promisify2 } from "util";
6863
7262
  var run2 = promisify2(execFile2);
6864
7263
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
@@ -6931,18 +7330,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
6931
7330
  const port = localDevServerPort(origin);
6932
7331
  if (port === null || owners.length === 0) return null;
6933
7332
  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";
7333
+ const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
7334
+ const project = basename5(resolve3(projectRoot)) || "this project";
6936
7335
  return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
6937
7336
  }
6938
7337
 
6939
7338
  // src/run.ts
6940
7339
  function findShellDir() {
6941
- const bundled = join19(dirname11(fileURLToPath(import.meta.url)), "shell");
7340
+ const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
6942
7341
  if (existsSync6(join19(bundled, "index.html"))) return bundled;
6943
7342
  try {
6944
7343
  const require2 = createRequire(import.meta.url);
6945
- return dirname11(require2.resolve("@leglas/shell/dist/index.html"));
7344
+ return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
6946
7345
  } catch {
6947
7346
  return null;
6948
7347
  }
@@ -6953,7 +7352,7 @@ function shellWord(value) {
6953
7352
  return `'${value.replaceAll("'", `'\\''`)}'`;
6954
7353
  }
6955
7354
  function embeddedLeglasCommand() {
6956
- const entry = join19(dirname11(fileURLToPath(import.meta.url)), "bin.js");
7355
+ const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
6957
7356
  if (!existsSync6(entry)) return "npx -y leglas";
6958
7357
  return [process.execPath, entry].map(shellWord).join(" ");
6959
7358
  }
@@ -6997,10 +7396,10 @@ async function run3(options, deps) {
6997
7396
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
6998
7397
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
6999
7398
  }
7000
- fileMounts.set(slug, dirname11(absolute));
7399
+ fileMounts.set(slug, dirname12(absolute));
7001
7400
  previews.push({
7002
7401
  ...preview,
7003
- url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename5(absolute))}`
7402
+ url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
7004
7403
  });
7005
7404
  continue;
7006
7405
  }
@@ -7030,8 +7429,8 @@ async function run3(options, deps) {
7030
7429
  }
7031
7430
  const config = merged === null ? null : { ...merged, previews };
7032
7431
  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))
7432
+ const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
7433
+ () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
7035
7434
  );
7036
7435
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7037
7436
  const serverPromise = startServer({