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/bin.js CHANGED
@@ -1179,8 +1179,8 @@ function findConfigFile(startDir) {
1179
1179
  const { root } = parse(startDir);
1180
1180
  let dir = startDir;
1181
1181
  for (; ; ) {
1182
- for (const basename6 of CONFIG_BASENAMES) {
1183
- const candidate = join2(dir, basename6);
1182
+ for (const basename7 of CONFIG_BASENAMES) {
1183
+ const candidate = join2(dir, basename7);
1184
1184
  if (existsSync(candidate))
1185
1185
  return candidate;
1186
1186
  }
@@ -2624,18 +2624,23 @@ async function freePort() {
2624
2624
  });
2625
2625
  });
2626
2626
  }
2627
- function answers(port) {
2628
- return new Promise((resolve5) => {
2629
- const socket = net2.connect(port, "127.0.0.1");
2627
+ var LOOPBACK = ["127.0.0.1", "::1"];
2628
+ function forUrl(host) {
2629
+ return host.includes(":") ? `[${host}]` : host;
2630
+ }
2631
+ async function answeringHost(port) {
2632
+ const reached = await Promise.all(LOOPBACK.map((host) => new Promise((resolve5) => {
2633
+ const socket = net2.connect({ port, host });
2630
2634
  const settle = (value) => {
2631
2635
  socket.destroy();
2632
2636
  resolve5(value);
2633
2637
  };
2634
2638
  socket.setTimeout(400);
2635
- socket.once("connect", () => settle(true));
2636
- socket.once("timeout", () => settle(false));
2637
- socket.once("error", () => settle(false));
2638
- });
2639
+ socket.once("connect", () => settle(host));
2640
+ socket.once("timeout", () => settle(null));
2641
+ socket.once("error", () => settle(null));
2642
+ })));
2643
+ return reached.find((host) => host !== null) ?? null;
2639
2644
  }
2640
2645
  async function startWorktree(options) {
2641
2646
  const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
@@ -2730,8 +2735,9 @@ async function startAppProcess(options) {
2730
2735
  if (exited !== null) {
2731
2736
  throw new Error(`${options.label} did not start: its dev command exited with code ${exited}.`);
2732
2737
  }
2733
- if (await answers(port)) {
2734
- return { port, url: `http://127.0.0.1:${port}`, stop };
2738
+ const host = await answeringHost(port);
2739
+ if (host !== null) {
2740
+ return { port, url: `http://${forUrl(host)}:${port}`, stop };
2735
2741
  }
2736
2742
  await new Promise((resolve5) => setTimeout(resolve5, 250));
2737
2743
  }
@@ -4226,6 +4232,10 @@ function startRunner(options) {
4226
4232
  stopping: false,
4227
4233
  waiting: null
4228
4234
  };
4235
+ const setState = (next) => {
4236
+ state = typeof next === "function" ? next(state) : next;
4237
+ options.onChange?.();
4238
+ };
4229
4239
  let stopped = false;
4230
4240
  let ticking = null;
4231
4241
  let pendingNudges = 0;
@@ -4277,7 +4287,7 @@ function startRunner(options) {
4277
4287
  return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
4278
4288
  };
4279
4289
  const idle = () => {
4280
- state = {
4290
+ setState({
4281
4291
  running: false,
4282
4292
  requestId: null,
4283
4293
  agent: null,
@@ -4285,7 +4295,7 @@ function startRunner(options) {
4285
4295
  startedAt: null,
4286
4296
  stopping: false,
4287
4297
  waiting: null
4288
- };
4298
+ });
4289
4299
  };
4290
4300
  const rememberLine = (lines2, line) => {
4291
4301
  lines2.push(line);
@@ -4393,14 +4403,15 @@ function startRunner(options) {
4393
4403
  if (retry !== null) {
4394
4404
  observed.retry = retry;
4395
4405
  if (active === current)
4396
- state = { ...state, waiting: retry };
4406
+ setState((value) => ({ ...value, waiting: retry }));
4397
4407
  }
4398
4408
  const activity = activityFrom(resolved.agent, line, options.cwd);
4399
4409
  if (activity !== null) {
4400
4410
  if (activity.startsWith("editing"))
4401
4411
  observed.edited = true;
4402
- if (active === current)
4403
- state = { ...state, activity, waiting: null };
4412
+ if (active === current) {
4413
+ setState((value) => ({ ...value, activity, waiting: null }));
4414
+ }
4404
4415
  }
4405
4416
  });
4406
4417
  const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
@@ -4447,7 +4458,7 @@ function startRunner(options) {
4447
4458
  await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
4448
4459
  return;
4449
4460
  }
4450
- state = {
4461
+ setState({
4451
4462
  running: true,
4452
4463
  requestId: request.id,
4453
4464
  agent: resolved.name,
@@ -4455,7 +4466,7 @@ function startRunner(options) {
4455
4466
  startedAt: Date.now(),
4456
4467
  stopping: false,
4457
4468
  waiting: null
4458
- };
4469
+ });
4459
4470
  const observed = {
4460
4471
  sessionId: null,
4461
4472
  edited: false,
@@ -4486,7 +4497,7 @@ function startRunner(options) {
4486
4497
  resolved = cold;
4487
4498
  observed.sessionId = null;
4488
4499
  observed.retry = null;
4489
- state = { ...state, activity: null, waiting: null };
4500
+ setState((value) => ({ ...value, activity: null, waiting: null }));
4490
4501
  outcome = await runChild(request, resolved, lines2, observed);
4491
4502
  failure = verdict();
4492
4503
  }
@@ -4560,7 +4571,7 @@ function startRunner(options) {
4560
4571
  const current = active;
4561
4572
  current.cancelled = true;
4562
4573
  failed.add(current.requestId);
4563
- state = { ...state, stopping: true, waiting: null };
4574
+ setState((value) => ({ ...value, stopping: true, waiting: null }));
4564
4575
  current.controller.abort();
4565
4576
  try {
4566
4577
  current.child?.kill("SIGTERM");
@@ -4603,6 +4614,193 @@ function startRunner(options) {
4603
4614
  };
4604
4615
  }
4605
4616
 
4617
+ // ../server/dist/live.js
4618
+ import { createHash } from "crypto";
4619
+ var LIVE_PATH = "/leglas/api/live";
4620
+ var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4621
+ function encodeFrame(opcode, payload) {
4622
+ const body = typeof payload === "string" ? Buffer.from(payload) : payload;
4623
+ let header;
4624
+ if (body.length < 126) {
4625
+ header = Buffer.allocUnsafe(2);
4626
+ header[1] = body.length;
4627
+ } else if (body.length <= 65535) {
4628
+ header = Buffer.allocUnsafe(4);
4629
+ header[1] = 126;
4630
+ header.writeUInt16BE(body.length, 2);
4631
+ } else {
4632
+ header = Buffer.allocUnsafe(10);
4633
+ header[1] = 127;
4634
+ header.writeBigUInt64BE(BigInt(body.length), 2);
4635
+ }
4636
+ header[0] = 128 | opcode & 15;
4637
+ return Buffer.concat([header, body], header.length + body.length);
4638
+ }
4639
+ var LIVE_DEBOUNCE_MS = 50;
4640
+ function createCoalescer(emit, options = {}) {
4641
+ const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
4642
+ const setLater = options.setTimeout ?? ((callback, ms) => {
4643
+ const timer = setTimeout(callback, ms);
4644
+ timer.unref?.();
4645
+ return timer;
4646
+ });
4647
+ const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
4648
+ const pending = /* @__PURE__ */ new Map();
4649
+ let closed = false;
4650
+ return {
4651
+ schedule(change) {
4652
+ if (closed)
4653
+ return;
4654
+ const waiting = pending.get(change);
4655
+ if (waiting !== void 0)
4656
+ clearLater(waiting);
4657
+ pending.set(change, setLater(() => {
4658
+ pending.delete(change);
4659
+ if (!closed)
4660
+ emit(change);
4661
+ }, windowMs));
4662
+ },
4663
+ close() {
4664
+ closed = true;
4665
+ for (const handle of pending.values())
4666
+ clearLater(handle);
4667
+ pending.clear();
4668
+ }
4669
+ };
4670
+ }
4671
+ function createLiveHub(_options = {}) {
4672
+ const listeners = /* @__PURE__ */ new Set();
4673
+ const drop = (listener) => {
4674
+ listeners.delete(listener);
4675
+ };
4676
+ const write2 = (listener, opcode, payload) => {
4677
+ if (listener.socket.destroyed || !listener.socket.writable) {
4678
+ drop(listener);
4679
+ return false;
4680
+ }
4681
+ try {
4682
+ listener.socket.write(encodeFrame(opcode, payload));
4683
+ return true;
4684
+ } catch {
4685
+ drop(listener);
4686
+ listener.socket.destroy();
4687
+ return false;
4688
+ }
4689
+ };
4690
+ const read = (listener, chunk) => {
4691
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4692
+ listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
4693
+ while (listener.buffered.length >= 2) {
4694
+ const first = listener.buffered[0] ?? 0;
4695
+ const second = listener.buffered[1] ?? 0;
4696
+ let length = second & 127;
4697
+ let offset = 2;
4698
+ if (length === 126) {
4699
+ if (listener.buffered.length < 4)
4700
+ return;
4701
+ length = listener.buffered.readUInt16BE(2);
4702
+ offset = 4;
4703
+ } else if (length === 127) {
4704
+ if (listener.buffered.length < 10)
4705
+ return;
4706
+ const wide = listener.buffered.readBigUInt64BE(2);
4707
+ if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
4708
+ drop(listener);
4709
+ listener.socket.destroy();
4710
+ return;
4711
+ }
4712
+ length = Number(wide);
4713
+ offset = 10;
4714
+ }
4715
+ const masked = (second & 128) !== 0;
4716
+ if (!masked) {
4717
+ drop(listener);
4718
+ listener.socket.destroy();
4719
+ return;
4720
+ }
4721
+ if (listener.buffered.length < offset + 4 + length)
4722
+ return;
4723
+ const mask = listener.buffered.subarray(offset, offset + 4);
4724
+ offset += 4;
4725
+ const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
4726
+ for (let index = 0; index < payload.length; index += 1) {
4727
+ payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
4728
+ }
4729
+ listener.buffered = listener.buffered.subarray(offset + length);
4730
+ const opcode = first & 15;
4731
+ if (opcode === 8) {
4732
+ write2(listener, 8, payload);
4733
+ drop(listener);
4734
+ listener.socket.destroy();
4735
+ return;
4736
+ }
4737
+ if (opcode === 9)
4738
+ write2(listener, 10, payload);
4739
+ }
4740
+ };
4741
+ return {
4742
+ nudge: (change) => {
4743
+ if (listeners.size === 0)
4744
+ return;
4745
+ const frame = encodeFrame(1, JSON.stringify({ changed: change }));
4746
+ for (const listener of [...listeners]) {
4747
+ if (listener.socket.destroyed || !listener.socket.writable) {
4748
+ drop(listener);
4749
+ continue;
4750
+ }
4751
+ try {
4752
+ listener.socket.write(frame);
4753
+ } catch {
4754
+ drop(listener);
4755
+ listener.socket.destroy();
4756
+ }
4757
+ }
4758
+ },
4759
+ upgrade: (req, socket, head) => {
4760
+ const path = (req.url ?? "/").split("?")[0] ?? "/";
4761
+ if (req.method !== "GET" || path !== LIVE_PATH)
4762
+ return false;
4763
+ const upgrade = req.headers.upgrade;
4764
+ const key = req.headers["sec-websocket-key"];
4765
+ if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
4766
+ socket.destroy();
4767
+ return false;
4768
+ }
4769
+ const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
4770
+ try {
4771
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
4772
+ Upgrade: websocket\r
4773
+ Connection: Upgrade\r
4774
+ Sec-WebSocket-Accept: ${accept}\r
4775
+ \r
4776
+ `);
4777
+ } catch {
4778
+ socket.destroy();
4779
+ return false;
4780
+ }
4781
+ const listener = { socket, buffered: Buffer.alloc(0) };
4782
+ listeners.add(listener);
4783
+ socket.on("data", (chunk) => read(listener, chunk));
4784
+ socket.once("error", () => drop(listener));
4785
+ socket.once("end", () => drop(listener));
4786
+ socket.once("close", () => drop(listener));
4787
+ if (head.length > 0)
4788
+ read(listener, head);
4789
+ return true;
4790
+ },
4791
+ close: async () => {
4792
+ for (const listener of [...listeners]) {
4793
+ write2(listener, 8, Buffer.alloc(0));
4794
+ drop(listener);
4795
+ listener.socket.destroy();
4796
+ }
4797
+ },
4798
+ get listening() {
4799
+ return listeners.size;
4800
+ }
4801
+ };
4802
+ }
4803
+
4606
4804
  // ../server/dist/renames.js
4607
4805
  import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
4608
4806
  import { dirname as dirname6, join as join10 } from "path";
@@ -4636,11 +4834,11 @@ function resolveTitle(input, titles, renames) {
4636
4834
  }
4637
4835
 
4638
4836
  // ../server/dist/server.js
4639
- import { createReadStream, existsSync as existsSync3, statSync } from "fs";
4837
+ import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
4640
4838
  import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
4641
4839
  import http2 from "http";
4642
4840
  import net3 from "net";
4643
- import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
4841
+ import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
4644
4842
 
4645
4843
  // ../server/dist/server-info.js
4646
4844
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
@@ -4825,6 +5023,204 @@ function serveShellFile(res, shellDir, urlPath) {
4825
5023
  const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
4826
5024
  return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
4827
5025
  }
5026
+ var HEALTH_PROBE_MS = 3e3;
5027
+ function fileStamp(path) {
5028
+ try {
5029
+ const stat5 = statSync(path);
5030
+ return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
5031
+ } catch {
5032
+ return null;
5033
+ }
5034
+ }
5035
+ function watchLiveFiles(cwd, configPath, live) {
5036
+ const leglasDir = join12(cwd, ".leglas");
5037
+ const targets = [
5038
+ { path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
5039
+ { path: join12(cwd, REQUESTS_PATH), change: "requests" },
5040
+ { path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
5041
+ ];
5042
+ const byName = new Map(targets.map((target) => [basename3(target.path), target]));
5043
+ const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
5044
+ const coalescer = createCoalescer((change) => live.nudge(change));
5045
+ const watchers = /* @__PURE__ */ new Set();
5046
+ const fallback = /* @__PURE__ */ new Map();
5047
+ let leglasWatcher = null;
5048
+ let retry = null;
5049
+ let closed = false;
5050
+ const nudgeSoon = (change) => coalescer.schedule(change);
5051
+ const scanLeglas = (notify) => {
5052
+ for (const target of targets) {
5053
+ const next = fileStamp(target.path);
5054
+ if (next === known.get(target.path))
5055
+ continue;
5056
+ known.set(target.path, next);
5057
+ if (notify)
5058
+ nudgeSoon(target.change);
5059
+ }
5060
+ };
5061
+ const fallbackWatch = (target) => {
5062
+ if (closed || fallback.has(target.path))
5063
+ return;
5064
+ const listener = () => {
5065
+ const next = fileStamp(target.path);
5066
+ if (next === known.get(target.path))
5067
+ return;
5068
+ known.set(target.path, next);
5069
+ nudgeSoon(target.change);
5070
+ };
5071
+ fallback.set(target.path, listener);
5072
+ watchFile(target.path, { persistent: false, interval: 250 }, listener);
5073
+ };
5074
+ const fallbackLeglas = () => {
5075
+ for (const target of targets)
5076
+ fallbackWatch(target);
5077
+ };
5078
+ const retryLeglas = () => {
5079
+ if (closed || retry !== null)
5080
+ return;
5081
+ retry = setTimeout(() => {
5082
+ retry = null;
5083
+ armLeglas(true);
5084
+ }, 250);
5085
+ retry.unref?.();
5086
+ };
5087
+ const armLeglas = (notify) => {
5088
+ if (closed)
5089
+ return;
5090
+ scanLeglas(notify);
5091
+ let directory = false;
5092
+ try {
5093
+ directory = statSync(leglasDir).isDirectory();
5094
+ } catch {
5095
+ directory = false;
5096
+ }
5097
+ if (!directory) {
5098
+ if (leglasWatcher !== null) {
5099
+ watchers.delete(leglasWatcher);
5100
+ leglasWatcher.close();
5101
+ leglasWatcher = null;
5102
+ }
5103
+ retryLeglas();
5104
+ return;
5105
+ }
5106
+ if (leglasWatcher !== null)
5107
+ return;
5108
+ try {
5109
+ const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
5110
+ if (filename === null) {
5111
+ scanLeglas(true);
5112
+ return;
5113
+ }
5114
+ const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
5115
+ const target = byName.get(name);
5116
+ if (target === void 0)
5117
+ return;
5118
+ known.set(target.path, fileStamp(target.path));
5119
+ nudgeSoon(target.change);
5120
+ });
5121
+ watcher.on("error", () => {
5122
+ if (leglasWatcher !== watcher)
5123
+ return;
5124
+ watchers.delete(watcher);
5125
+ watcher.close();
5126
+ leglasWatcher = null;
5127
+ fallbackLeglas();
5128
+ retryLeglas();
5129
+ });
5130
+ leglasWatcher = watcher;
5131
+ watchers.add(watcher);
5132
+ } catch {
5133
+ fallbackLeglas();
5134
+ retryLeglas();
5135
+ }
5136
+ };
5137
+ try {
5138
+ const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
5139
+ const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5140
+ if (name === null || name === ".leglas")
5141
+ armLeglas(true);
5142
+ });
5143
+ watcher.on("error", () => {
5144
+ watchers.delete(watcher);
5145
+ watcher.close();
5146
+ fallbackLeglas();
5147
+ });
5148
+ watchers.add(watcher);
5149
+ } catch {
5150
+ fallbackLeglas();
5151
+ }
5152
+ if (configPath !== null) {
5153
+ try {
5154
+ const directory = dirname8(configPath);
5155
+ const name = basename3(configPath);
5156
+ const target = { path: configPath, change: "config" };
5157
+ known.set(configPath, fileStamp(configPath));
5158
+ const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
5159
+ const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5160
+ if (changed === null || changed === name)
5161
+ nudgeSoon("config");
5162
+ });
5163
+ watcher.on("error", () => {
5164
+ watchers.delete(watcher);
5165
+ watcher.close();
5166
+ fallbackWatch(target);
5167
+ });
5168
+ watchers.add(watcher);
5169
+ } catch {
5170
+ fallbackWatch({ path: configPath, change: "config" });
5171
+ }
5172
+ }
5173
+ armLeglas(false);
5174
+ return {
5175
+ close: () => {
5176
+ if (closed)
5177
+ return;
5178
+ closed = true;
5179
+ if (retry !== null)
5180
+ clearTimeout(retry);
5181
+ coalescer.close();
5182
+ for (const watcher of watchers)
5183
+ watcher.close();
5184
+ watchers.clear();
5185
+ for (const [path, listener] of fallback)
5186
+ unwatchFile(path, listener);
5187
+ fallback.clear();
5188
+ leglasWatcher = null;
5189
+ }
5190
+ };
5191
+ }
5192
+ function watchHealth(target, live) {
5193
+ let previous = null;
5194
+ let probing = false;
5195
+ let closed = false;
5196
+ const timer = setInterval(() => {
5197
+ if (live.listening === 0) {
5198
+ previous = null;
5199
+ return;
5200
+ }
5201
+ if (probing)
5202
+ return;
5203
+ probing = true;
5204
+ void probe(target).then((reachable) => {
5205
+ if (closed || live.listening === 0) {
5206
+ previous = null;
5207
+ return;
5208
+ }
5209
+ if (previous !== null && previous !== reachable)
5210
+ live.nudge("health");
5211
+ previous = reachable;
5212
+ }).finally(() => {
5213
+ probing = false;
5214
+ });
5215
+ }, HEALTH_PROBE_MS);
5216
+ timer.unref?.();
5217
+ return {
5218
+ close: () => {
5219
+ closed = true;
5220
+ clearInterval(timer);
5221
+ }
5222
+ };
5223
+ }
4828
5224
  function snapshotConfig(cwd) {
4829
5225
  const path = findConfigFile(cwd);
4830
5226
  if (path === null)
@@ -4895,12 +5291,14 @@ async function bind(server, requested) {
4895
5291
  async function startServer(options) {
4896
5292
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
4897
5293
  const browserPool = options.pool ?? createBrowserPool();
5294
+ const live = options.live ?? createLiveHub();
4898
5295
  if (options.pool === void 0) {
4899
5296
  void reapOrphanedBrowsers().catch(() => {
4900
5297
  });
4901
5298
  }
4902
5299
  const target = config?.devServer ?? "http://localhost:3000";
4903
5300
  const proxy = createProxyHandler({ target });
5301
+ const bootConfigPath = findConfigFile(cwd);
4904
5302
  const bootConfigSnapshot = snapshotConfig(cwd);
4905
5303
  let lastSeen = null;
4906
5304
  const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
@@ -5125,14 +5523,14 @@ async function startServer(options) {
5125
5523
  return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
5126
5524
  }
5127
5525
  const intent = (parsed2.intent ?? "").trim();
5128
- const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5526
+ const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5129
5527
  const sameNotes = (entry) => {
5130
5528
  const before = [...entry.notes ?? []].sort().join(",");
5131
5529
  return before === notes.map((note) => note.id).sort().join(",");
5132
5530
  };
5133
5531
  const compare = typeof parsed2.compare === "string" && parsed2.compare !== preview.title ? previews.find((entry) => entry.title === parsed2.compare) ?? null : null;
5134
5532
  const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
5135
- if (live.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
5533
+ if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
5136
5534
  // one forks the direction and the other rewrites it. Only a
5137
5535
  // genuine repeat is refused.
5138
5536
  (entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
@@ -5643,12 +6041,16 @@ async function startServer(options) {
5643
6041
  socket.once("close", () => sockets.delete(socket));
5644
6042
  });
5645
6043
  server.on("upgrade", (req, socket, head) => {
6044
+ if (live.upgrade(req, socket, head))
6045
+ return;
5646
6046
  const path = (req.url ?? "/").split("?")[0] ?? "/";
5647
6047
  if (path.startsWith(`${LEGLAS_PREFIX}/`))
5648
6048
  return socket.destroy();
5649
6049
  proxy.upgrade(req, socket, head);
5650
6050
  });
5651
6051
  const port = await bind(server, options.port ?? DEFAULT_PORT);
6052
+ const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
6053
+ const liveHealth = watchHealth(target, live);
5652
6054
  await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
5653
6055
  });
5654
6056
  await writeServerInfo(cwd, {
@@ -5660,6 +6062,7 @@ async function startServer(options) {
5660
6062
  runner = startRunner({
5661
6063
  cwd,
5662
6064
  externallyAttached,
6065
+ onChange: () => live.nudge("requests"),
5663
6066
  leglasCommand,
5664
6067
  ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
5665
6068
  ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
@@ -5671,7 +6074,9 @@ async function startServer(options) {
5671
6074
  close: () => {
5672
6075
  if (closePromise !== null)
5673
6076
  return closePromise;
5674
- closePromise = Promise.all([runner.stop(), browserPool.close()]).then(() => new Promise((done) => {
6077
+ liveFiles.close();
6078
+ liveHealth.close();
6079
+ closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
5675
6080
  for (const socket of sockets)
5676
6081
  socket.destroy();
5677
6082
  sockets.clear();
@@ -6144,10 +6549,10 @@ async function runInit(options, deps) {
6144
6549
  // src/run-keep.ts
6145
6550
  import { existsSync as existsSync4 } from "fs";
6146
6551
  import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
6147
- import { dirname as dirname8, join as join15 } from "path";
6552
+ import { dirname as dirname9, join as join15 } from "path";
6148
6553
 
6149
6554
  // src/keep.ts
6150
- import { basename as basename3, extname as extname4, normalize as normalize2 } from "path";
6555
+ import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
6151
6556
  function surfaceOf(url) {
6152
6557
  if (!url.startsWith("/") || !url.includes("?")) return null;
6153
6558
  for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
@@ -6157,7 +6562,7 @@ function surfaceOf(url) {
6157
6562
  return null;
6158
6563
  }
6159
6564
  function exportNameFor(to) {
6160
- const stem = basename3(to, extname4(to));
6565
+ const stem = basename4(to, extname4(to));
6161
6566
  return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
6162
6567
  }
6163
6568
  function planKeep(options) {
@@ -6252,7 +6657,7 @@ async function runKeep(options, deps) {
6252
6657
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
6253
6658
  }
6254
6659
  const source = await readFile13(from, "utf8");
6255
- await mkdir9(dirname8(to), { recursive: true });
6660
+ await mkdir9(dirname9(to), { recursive: true });
6256
6661
  await writeFile10(to, renameExport(source, plan.exportName), "utf8");
6257
6662
  await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
6258
6663
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
@@ -6290,7 +6695,7 @@ async function runKeep(options, deps) {
6290
6695
  // src/run-new.ts
6291
6696
  import { existsSync as existsSync5 } from "fs";
6292
6697
  import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
6293
- import { dirname as dirname9, join as join16 } from "path";
6698
+ import { dirname as dirname10, join as join16 } from "path";
6294
6699
  async function readIfPresent2(path) {
6295
6700
  try {
6296
6701
  return await readFile14(path, "utf8");
@@ -6343,7 +6748,7 @@ async function runNew(options, deps) {
6343
6748
  const written = [];
6344
6749
  for (const write2 of plan.writes) {
6345
6750
  const target = join16(options.cwd, write2.path);
6346
- await mkdir10(dirname9(target), { recursive: true });
6751
+ await mkdir10(dirname10(target), { recursive: true });
6347
6752
  await writeFile11(target, write2.contents, "utf8");
6348
6753
  written.push(write2.path);
6349
6754
  }
@@ -6706,7 +7111,7 @@ async function runShow(options, deps) {
6706
7111
  // src/run-watch.ts
6707
7112
  import { spawn as spawn3 } from "child_process";
6708
7113
  import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
6709
- import { dirname as dirname10, join as join18 } from "path";
7114
+ import { dirname as dirname11, join as join18 } from "path";
6710
7115
  var POLL_MS2 = 2e3;
6711
7116
  var HEARTBEAT_TIMEOUT_MS = 1e3;
6712
7117
  async function saveTemplate(cwd, run4) {
@@ -6720,7 +7125,7 @@ async function saveTemplate(cwd, run4) {
6720
7125
  } catch {
6721
7126
  }
6722
7127
  config.run = run4;
6723
- await mkdir11(dirname10(path), { recursive: true });
7128
+ await mkdir11(dirname11(path), { recursive: true });
6724
7129
  await writeFile13(path, `${JSON.stringify(config, null, 2)}
6725
7130
  `, "utf8");
6726
7131
  }
@@ -6862,13 +7267,13 @@ async function runWatch(options, deps) {
6862
7267
  import { existsSync as existsSync6 } from "fs";
6863
7268
  import { realpath as realpath4 } from "fs/promises";
6864
7269
  import { createRequire } from "module";
6865
- import { basename as basename5, dirname as dirname11, join as join19, relative as relative5, resolve as resolve4 } from "path";
7270
+ import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
6866
7271
  import { fileURLToPath } from "url";
6867
7272
 
6868
7273
  // src/dev-server-owner.ts
6869
7274
  import { execFile as execFile2 } from "child_process";
6870
7275
  import { realpath as realpath3 } from "fs/promises";
6871
- import { basename as basename4, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
7276
+ import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
6872
7277
  import { promisify as promisify2 } from "util";
6873
7278
  var run2 = promisify2(execFile2);
6874
7279
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
@@ -6941,18 +7346,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
6941
7346
  const port = localDevServerPort(origin);
6942
7347
  if (port === null || owners.length === 0) return null;
6943
7348
  if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
6944
- const owner = basename4(resolve3(owners[0]?.cwd ?? "")) || "another project";
6945
- const project = basename4(resolve3(projectRoot)) || "this project";
7349
+ const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
7350
+ const project = basename5(resolve3(projectRoot)) || "this project";
6946
7351
  return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
6947
7352
  }
6948
7353
 
6949
7354
  // src/run.ts
6950
7355
  function findShellDir() {
6951
- const bundled = join19(dirname11(fileURLToPath(import.meta.url)), "shell");
7356
+ const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
6952
7357
  if (existsSync6(join19(bundled, "index.html"))) return bundled;
6953
7358
  try {
6954
7359
  const require2 = createRequire(import.meta.url);
6955
- return dirname11(require2.resolve("@leglas/shell/dist/index.html"));
7360
+ return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
6956
7361
  } catch {
6957
7362
  return null;
6958
7363
  }
@@ -6963,7 +7368,7 @@ function shellWord(value) {
6963
7368
  return `'${value.replaceAll("'", `'\\''`)}'`;
6964
7369
  }
6965
7370
  function embeddedLeglasCommand() {
6966
- const entry = join19(dirname11(fileURLToPath(import.meta.url)), "bin.js");
7371
+ const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
6967
7372
  if (!existsSync6(entry)) return "npx -y leglas";
6968
7373
  return [process.execPath, entry].map(shellWord).join(" ");
6969
7374
  }
@@ -7007,10 +7412,10 @@ async function run3(options, deps) {
7007
7412
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
7008
7413
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
7009
7414
  }
7010
- fileMounts.set(slug, dirname11(absolute));
7415
+ fileMounts.set(slug, dirname12(absolute));
7011
7416
  previews.push({
7012
7417
  ...preview,
7013
- url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename5(absolute))}`
7418
+ url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
7014
7419
  });
7015
7420
  continue;
7016
7421
  }
@@ -7040,8 +7445,8 @@ async function run3(options, deps) {
7040
7445
  }
7041
7446
  const config = merged === null ? null : { ...merged, previews };
7042
7447
  const configWarnings = [];
7043
- const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname11(loaded.path)).catch(
7044
- () => resolve4(loaded.path === null ? options.cwd : dirname11(loaded.path))
7448
+ const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
7449
+ () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
7045
7450
  );
7046
7451
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7047
7452
  const serverPromise = startServer({