leglas 0.7.0 → 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/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
  }
@@ -4226,6 +4226,10 @@ function startRunner(options) {
4226
4226
  stopping: false,
4227
4227
  waiting: null
4228
4228
  };
4229
+ const setState = (next) => {
4230
+ state = typeof next === "function" ? next(state) : next;
4231
+ options.onChange?.();
4232
+ };
4229
4233
  let stopped = false;
4230
4234
  let ticking = null;
4231
4235
  let pendingNudges = 0;
@@ -4277,7 +4281,7 @@ function startRunner(options) {
4277
4281
  return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
4278
4282
  };
4279
4283
  const idle = () => {
4280
- state = {
4284
+ setState({
4281
4285
  running: false,
4282
4286
  requestId: null,
4283
4287
  agent: null,
@@ -4285,7 +4289,7 @@ function startRunner(options) {
4285
4289
  startedAt: null,
4286
4290
  stopping: false,
4287
4291
  waiting: null
4288
- };
4292
+ });
4289
4293
  };
4290
4294
  const rememberLine = (lines2, line) => {
4291
4295
  lines2.push(line);
@@ -4393,14 +4397,15 @@ function startRunner(options) {
4393
4397
  if (retry !== null) {
4394
4398
  observed.retry = retry;
4395
4399
  if (active === current)
4396
- state = { ...state, waiting: retry };
4400
+ setState((value) => ({ ...value, waiting: retry }));
4397
4401
  }
4398
4402
  const activity = activityFrom(resolved.agent, line, options.cwd);
4399
4403
  if (activity !== null) {
4400
4404
  if (activity.startsWith("editing"))
4401
4405
  observed.edited = true;
4402
- if (active === current)
4403
- state = { ...state, activity, waiting: null };
4406
+ if (active === current) {
4407
+ setState((value) => ({ ...value, activity, waiting: null }));
4408
+ }
4404
4409
  }
4405
4410
  });
4406
4411
  const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
@@ -4447,7 +4452,7 @@ function startRunner(options) {
4447
4452
  await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
4448
4453
  return;
4449
4454
  }
4450
- state = {
4455
+ setState({
4451
4456
  running: true,
4452
4457
  requestId: request.id,
4453
4458
  agent: resolved.name,
@@ -4455,7 +4460,7 @@ function startRunner(options) {
4455
4460
  startedAt: Date.now(),
4456
4461
  stopping: false,
4457
4462
  waiting: null
4458
- };
4463
+ });
4459
4464
  const observed = {
4460
4465
  sessionId: null,
4461
4466
  edited: false,
@@ -4486,7 +4491,7 @@ function startRunner(options) {
4486
4491
  resolved = cold;
4487
4492
  observed.sessionId = null;
4488
4493
  observed.retry = null;
4489
- state = { ...state, activity: null, waiting: null };
4494
+ setState((value) => ({ ...value, activity: null, waiting: null }));
4490
4495
  outcome = await runChild(request, resolved, lines2, observed);
4491
4496
  failure = verdict();
4492
4497
  }
@@ -4560,7 +4565,7 @@ function startRunner(options) {
4560
4565
  const current = active;
4561
4566
  current.cancelled = true;
4562
4567
  failed.add(current.requestId);
4563
- state = { ...state, stopping: true, waiting: null };
4568
+ setState((value) => ({ ...value, stopping: true, waiting: null }));
4564
4569
  current.controller.abort();
4565
4570
  try {
4566
4571
  current.child?.kill("SIGTERM");
@@ -4603,6 +4608,193 @@ function startRunner(options) {
4603
4608
  };
4604
4609
  }
4605
4610
 
4611
+ // ../server/dist/live.js
4612
+ import { createHash } from "crypto";
4613
+ var LIVE_PATH = "/leglas/api/live";
4614
+ var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4615
+ function encodeFrame(opcode, payload) {
4616
+ const body = typeof payload === "string" ? Buffer.from(payload) : payload;
4617
+ let header;
4618
+ if (body.length < 126) {
4619
+ header = Buffer.allocUnsafe(2);
4620
+ header[1] = body.length;
4621
+ } else if (body.length <= 65535) {
4622
+ header = Buffer.allocUnsafe(4);
4623
+ header[1] = 126;
4624
+ header.writeUInt16BE(body.length, 2);
4625
+ } else {
4626
+ header = Buffer.allocUnsafe(10);
4627
+ header[1] = 127;
4628
+ header.writeBigUInt64BE(BigInt(body.length), 2);
4629
+ }
4630
+ header[0] = 128 | opcode & 15;
4631
+ return Buffer.concat([header, body], header.length + body.length);
4632
+ }
4633
+ var LIVE_DEBOUNCE_MS = 50;
4634
+ function createCoalescer(emit, options = {}) {
4635
+ const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
4636
+ const setLater = options.setTimeout ?? ((callback, ms) => {
4637
+ const timer = setTimeout(callback, ms);
4638
+ timer.unref?.();
4639
+ return timer;
4640
+ });
4641
+ const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
4642
+ const pending = /* @__PURE__ */ new Map();
4643
+ let closed = false;
4644
+ return {
4645
+ schedule(change) {
4646
+ if (closed)
4647
+ return;
4648
+ const waiting = pending.get(change);
4649
+ if (waiting !== void 0)
4650
+ clearLater(waiting);
4651
+ pending.set(change, setLater(() => {
4652
+ pending.delete(change);
4653
+ if (!closed)
4654
+ emit(change);
4655
+ }, windowMs));
4656
+ },
4657
+ close() {
4658
+ closed = true;
4659
+ for (const handle of pending.values())
4660
+ clearLater(handle);
4661
+ pending.clear();
4662
+ }
4663
+ };
4664
+ }
4665
+ function createLiveHub(_options = {}) {
4666
+ const listeners = /* @__PURE__ */ new Set();
4667
+ const drop = (listener) => {
4668
+ listeners.delete(listener);
4669
+ };
4670
+ const write2 = (listener, opcode, payload) => {
4671
+ if (listener.socket.destroyed || !listener.socket.writable) {
4672
+ drop(listener);
4673
+ return false;
4674
+ }
4675
+ try {
4676
+ listener.socket.write(encodeFrame(opcode, payload));
4677
+ return true;
4678
+ } catch {
4679
+ drop(listener);
4680
+ listener.socket.destroy();
4681
+ return false;
4682
+ }
4683
+ };
4684
+ const read = (listener, chunk) => {
4685
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4686
+ listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
4687
+ while (listener.buffered.length >= 2) {
4688
+ const first = listener.buffered[0] ?? 0;
4689
+ const second = listener.buffered[1] ?? 0;
4690
+ let length = second & 127;
4691
+ let offset = 2;
4692
+ if (length === 126) {
4693
+ if (listener.buffered.length < 4)
4694
+ return;
4695
+ length = listener.buffered.readUInt16BE(2);
4696
+ offset = 4;
4697
+ } else if (length === 127) {
4698
+ if (listener.buffered.length < 10)
4699
+ return;
4700
+ const wide = listener.buffered.readBigUInt64BE(2);
4701
+ if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
4702
+ drop(listener);
4703
+ listener.socket.destroy();
4704
+ return;
4705
+ }
4706
+ length = Number(wide);
4707
+ offset = 10;
4708
+ }
4709
+ const masked = (second & 128) !== 0;
4710
+ if (!masked) {
4711
+ drop(listener);
4712
+ listener.socket.destroy();
4713
+ return;
4714
+ }
4715
+ if (listener.buffered.length < offset + 4 + length)
4716
+ return;
4717
+ const mask = listener.buffered.subarray(offset, offset + 4);
4718
+ offset += 4;
4719
+ const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
4720
+ for (let index = 0; index < payload.length; index += 1) {
4721
+ payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
4722
+ }
4723
+ listener.buffered = listener.buffered.subarray(offset + length);
4724
+ const opcode = first & 15;
4725
+ if (opcode === 8) {
4726
+ write2(listener, 8, payload);
4727
+ drop(listener);
4728
+ listener.socket.destroy();
4729
+ return;
4730
+ }
4731
+ if (opcode === 9)
4732
+ write2(listener, 10, payload);
4733
+ }
4734
+ };
4735
+ return {
4736
+ nudge: (change) => {
4737
+ if (listeners.size === 0)
4738
+ return;
4739
+ const frame = encodeFrame(1, JSON.stringify({ changed: change }));
4740
+ for (const listener of [...listeners]) {
4741
+ if (listener.socket.destroyed || !listener.socket.writable) {
4742
+ drop(listener);
4743
+ continue;
4744
+ }
4745
+ try {
4746
+ listener.socket.write(frame);
4747
+ } catch {
4748
+ drop(listener);
4749
+ listener.socket.destroy();
4750
+ }
4751
+ }
4752
+ },
4753
+ upgrade: (req, socket, head) => {
4754
+ const path = (req.url ?? "/").split("?")[0] ?? "/";
4755
+ if (req.method !== "GET" || path !== LIVE_PATH)
4756
+ return false;
4757
+ const upgrade = req.headers.upgrade;
4758
+ const key = req.headers["sec-websocket-key"];
4759
+ if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
4760
+ socket.destroy();
4761
+ return false;
4762
+ }
4763
+ const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
4764
+ try {
4765
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
4766
+ Upgrade: websocket\r
4767
+ Connection: Upgrade\r
4768
+ Sec-WebSocket-Accept: ${accept}\r
4769
+ \r
4770
+ `);
4771
+ } catch {
4772
+ socket.destroy();
4773
+ return false;
4774
+ }
4775
+ const listener = { socket, buffered: Buffer.alloc(0) };
4776
+ listeners.add(listener);
4777
+ socket.on("data", (chunk) => read(listener, chunk));
4778
+ socket.once("error", () => drop(listener));
4779
+ socket.once("end", () => drop(listener));
4780
+ socket.once("close", () => drop(listener));
4781
+ if (head.length > 0)
4782
+ read(listener, head);
4783
+ return true;
4784
+ },
4785
+ close: async () => {
4786
+ for (const listener of [...listeners]) {
4787
+ write2(listener, 8, Buffer.alloc(0));
4788
+ drop(listener);
4789
+ listener.socket.destroy();
4790
+ }
4791
+ },
4792
+ get listening() {
4793
+ return listeners.size;
4794
+ }
4795
+ };
4796
+ }
4797
+
4606
4798
  // ../server/dist/renames.js
4607
4799
  import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
4608
4800
  import { dirname as dirname6, join as join10 } from "path";
@@ -4636,11 +4828,11 @@ function resolveTitle(input, titles, renames) {
4636
4828
  }
4637
4829
 
4638
4830
  // ../server/dist/server.js
4639
- import { createReadStream, existsSync as existsSync3, statSync } from "fs";
4831
+ import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
4640
4832
  import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
4641
4833
  import http2 from "http";
4642
4834
  import net3 from "net";
4643
- import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
4835
+ import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
4644
4836
 
4645
4837
  // ../server/dist/server-info.js
4646
4838
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
@@ -4825,6 +5017,204 @@ function serveShellFile(res, shellDir, urlPath) {
4825
5017
  const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
4826
5018
  return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
4827
5019
  }
5020
+ var HEALTH_PROBE_MS = 3e3;
5021
+ function fileStamp(path) {
5022
+ try {
5023
+ const stat5 = statSync(path);
5024
+ return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
5025
+ } catch {
5026
+ return null;
5027
+ }
5028
+ }
5029
+ function watchLiveFiles(cwd, configPath, live) {
5030
+ const leglasDir = join12(cwd, ".leglas");
5031
+ const targets = [
5032
+ { path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
5033
+ { path: join12(cwd, REQUESTS_PATH), change: "requests" },
5034
+ { path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
5035
+ ];
5036
+ const byName = new Map(targets.map((target) => [basename3(target.path), target]));
5037
+ const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
5038
+ const coalescer = createCoalescer((change) => live.nudge(change));
5039
+ const watchers = /* @__PURE__ */ new Set();
5040
+ const fallback = /* @__PURE__ */ new Map();
5041
+ let leglasWatcher = null;
5042
+ let retry = null;
5043
+ let closed = false;
5044
+ const nudgeSoon = (change) => coalescer.schedule(change);
5045
+ const scanLeglas = (notify) => {
5046
+ for (const target of targets) {
5047
+ const next = fileStamp(target.path);
5048
+ if (next === known.get(target.path))
5049
+ continue;
5050
+ known.set(target.path, next);
5051
+ if (notify)
5052
+ nudgeSoon(target.change);
5053
+ }
5054
+ };
5055
+ const fallbackWatch = (target) => {
5056
+ if (closed || fallback.has(target.path))
5057
+ return;
5058
+ const listener = () => {
5059
+ const next = fileStamp(target.path);
5060
+ if (next === known.get(target.path))
5061
+ return;
5062
+ known.set(target.path, next);
5063
+ nudgeSoon(target.change);
5064
+ };
5065
+ fallback.set(target.path, listener);
5066
+ watchFile(target.path, { persistent: false, interval: 250 }, listener);
5067
+ };
5068
+ const fallbackLeglas = () => {
5069
+ for (const target of targets)
5070
+ fallbackWatch(target);
5071
+ };
5072
+ const retryLeglas = () => {
5073
+ if (closed || retry !== null)
5074
+ return;
5075
+ retry = setTimeout(() => {
5076
+ retry = null;
5077
+ armLeglas(true);
5078
+ }, 250);
5079
+ retry.unref?.();
5080
+ };
5081
+ const armLeglas = (notify) => {
5082
+ if (closed)
5083
+ return;
5084
+ scanLeglas(notify);
5085
+ let directory = false;
5086
+ try {
5087
+ directory = statSync(leglasDir).isDirectory();
5088
+ } catch {
5089
+ directory = false;
5090
+ }
5091
+ if (!directory) {
5092
+ if (leglasWatcher !== null) {
5093
+ watchers.delete(leglasWatcher);
5094
+ leglasWatcher.close();
5095
+ leglasWatcher = null;
5096
+ }
5097
+ retryLeglas();
5098
+ return;
5099
+ }
5100
+ if (leglasWatcher !== null)
5101
+ return;
5102
+ try {
5103
+ const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
5104
+ if (filename === null) {
5105
+ scanLeglas(true);
5106
+ return;
5107
+ }
5108
+ const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
5109
+ const target = byName.get(name);
5110
+ if (target === void 0)
5111
+ return;
5112
+ known.set(target.path, fileStamp(target.path));
5113
+ nudgeSoon(target.change);
5114
+ });
5115
+ watcher.on("error", () => {
5116
+ if (leglasWatcher !== watcher)
5117
+ return;
5118
+ watchers.delete(watcher);
5119
+ watcher.close();
5120
+ leglasWatcher = null;
5121
+ fallbackLeglas();
5122
+ retryLeglas();
5123
+ });
5124
+ leglasWatcher = watcher;
5125
+ watchers.add(watcher);
5126
+ } catch {
5127
+ fallbackLeglas();
5128
+ retryLeglas();
5129
+ }
5130
+ };
5131
+ try {
5132
+ const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
5133
+ const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5134
+ if (name === null || name === ".leglas")
5135
+ armLeglas(true);
5136
+ });
5137
+ watcher.on("error", () => {
5138
+ watchers.delete(watcher);
5139
+ watcher.close();
5140
+ fallbackLeglas();
5141
+ });
5142
+ watchers.add(watcher);
5143
+ } catch {
5144
+ fallbackLeglas();
5145
+ }
5146
+ if (configPath !== null) {
5147
+ try {
5148
+ const directory = dirname8(configPath);
5149
+ const name = basename3(configPath);
5150
+ const target = { path: configPath, change: "config" };
5151
+ known.set(configPath, fileStamp(configPath));
5152
+ const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
5153
+ const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
5154
+ if (changed === null || changed === name)
5155
+ nudgeSoon("config");
5156
+ });
5157
+ watcher.on("error", () => {
5158
+ watchers.delete(watcher);
5159
+ watcher.close();
5160
+ fallbackWatch(target);
5161
+ });
5162
+ watchers.add(watcher);
5163
+ } catch {
5164
+ fallbackWatch({ path: configPath, change: "config" });
5165
+ }
5166
+ }
5167
+ armLeglas(false);
5168
+ return {
5169
+ close: () => {
5170
+ if (closed)
5171
+ return;
5172
+ closed = true;
5173
+ if (retry !== null)
5174
+ clearTimeout(retry);
5175
+ coalescer.close();
5176
+ for (const watcher of watchers)
5177
+ watcher.close();
5178
+ watchers.clear();
5179
+ for (const [path, listener] of fallback)
5180
+ unwatchFile(path, listener);
5181
+ fallback.clear();
5182
+ leglasWatcher = null;
5183
+ }
5184
+ };
5185
+ }
5186
+ function watchHealth(target, live) {
5187
+ let previous = null;
5188
+ let probing = false;
5189
+ let closed = false;
5190
+ const timer = setInterval(() => {
5191
+ if (live.listening === 0) {
5192
+ previous = null;
5193
+ return;
5194
+ }
5195
+ if (probing)
5196
+ return;
5197
+ probing = true;
5198
+ void probe(target).then((reachable) => {
5199
+ if (closed || live.listening === 0) {
5200
+ previous = null;
5201
+ return;
5202
+ }
5203
+ if (previous !== null && previous !== reachable)
5204
+ live.nudge("health");
5205
+ previous = reachable;
5206
+ }).finally(() => {
5207
+ probing = false;
5208
+ });
5209
+ }, HEALTH_PROBE_MS);
5210
+ timer.unref?.();
5211
+ return {
5212
+ close: () => {
5213
+ closed = true;
5214
+ clearInterval(timer);
5215
+ }
5216
+ };
5217
+ }
4828
5218
  function snapshotConfig(cwd) {
4829
5219
  const path = findConfigFile(cwd);
4830
5220
  if (path === null)
@@ -4895,12 +5285,14 @@ async function bind(server, requested) {
4895
5285
  async function startServer(options) {
4896
5286
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
4897
5287
  const browserPool = options.pool ?? createBrowserPool();
5288
+ const live = options.live ?? createLiveHub();
4898
5289
  if (options.pool === void 0) {
4899
5290
  void reapOrphanedBrowsers().catch(() => {
4900
5291
  });
4901
5292
  }
4902
5293
  const target = config?.devServer ?? "http://localhost:3000";
4903
5294
  const proxy = createProxyHandler({ target });
5295
+ const bootConfigPath = findConfigFile(cwd);
4904
5296
  const bootConfigSnapshot = snapshotConfig(cwd);
4905
5297
  let lastSeen = null;
4906
5298
  const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
@@ -5125,14 +5517,14 @@ async function startServer(options) {
5125
5517
  return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
5126
5518
  }
5127
5519
  const intent = (parsed2.intent ?? "").trim();
5128
- const live = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5520
+ const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
5129
5521
  const sameNotes = (entry) => {
5130
5522
  const before = [...entry.notes ?? []].sort().join(",");
5131
5523
  return before === notes.map((note) => note.id).sort().join(",");
5132
5524
  };
5133
5525
  const compare = typeof parsed2.compare === "string" && parsed2.compare !== preview.title ? previews.find((entry) => entry.title === parsed2.compare) ?? null : null;
5134
5526
  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:
5527
+ if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
5136
5528
  // one forks the direction and the other rewrites it. Only a
5137
5529
  // genuine repeat is refused.
5138
5530
  (entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
@@ -5192,10 +5584,8 @@ async function startServer(options) {
5192
5584
  let body = "";
5193
5585
  req.on("data", (chunk) => body += chunk);
5194
5586
  return void req.on("end", async () => {
5195
- let parsed2;
5196
- try {
5197
- parsed2 = JSON.parse(body || "{}");
5198
- } catch {
5587
+ const parsed2 = jsonBody(body);
5588
+ if (parsed2 === null) {
5199
5589
  return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
5200
5590
  }
5201
5591
  if (typeof parsed2.title !== "string" || parsed2.title === "") {
@@ -5519,16 +5909,10 @@ async function startServer(options) {
5519
5909
  let body = "";
5520
5910
  req.on("data", (chunk) => body += chunk);
5521
5911
  return void req.on("end", async () => {
5522
- let read;
5523
- try {
5524
- read = JSON.parse(body || "{}");
5525
- } catch {
5912
+ const parsed2 = jsonBody(body);
5913
+ if (parsed2 === null) {
5526
5914
  return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
5527
5915
  }
5528
- if (typeof read !== "object" || read === null || Array.isArray(read)) {
5529
- return sendJson(res, 400, { ok: false, error: "Body must be a JSON object." });
5530
- }
5531
- const parsed2 = read;
5532
5916
  if (typeof parsed2.id !== "string" || parsed2.id === "") {
5533
5917
  return sendJson(res, 400, { ok: false, error: "Body needs the note to reword." });
5534
5918
  }
@@ -5651,12 +6035,16 @@ async function startServer(options) {
5651
6035
  socket.once("close", () => sockets.delete(socket));
5652
6036
  });
5653
6037
  server.on("upgrade", (req, socket, head) => {
6038
+ if (live.upgrade(req, socket, head))
6039
+ return;
5654
6040
  const path = (req.url ?? "/").split("?")[0] ?? "/";
5655
6041
  if (path.startsWith(`${LEGLAS_PREFIX}/`))
5656
6042
  return socket.destroy();
5657
6043
  proxy.upgrade(req, socket, head);
5658
6044
  });
5659
6045
  const port = await bind(server, options.port ?? DEFAULT_PORT);
6046
+ const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
6047
+ const liveHealth = watchHealth(target, live);
5660
6048
  await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
5661
6049
  });
5662
6050
  await writeServerInfo(cwd, {
@@ -5668,6 +6056,7 @@ async function startServer(options) {
5668
6056
  runner = startRunner({
5669
6057
  cwd,
5670
6058
  externallyAttached,
6059
+ onChange: () => live.nudge("requests"),
5671
6060
  leglasCommand,
5672
6061
  ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
5673
6062
  ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
@@ -5679,7 +6068,9 @@ async function startServer(options) {
5679
6068
  close: () => {
5680
6069
  if (closePromise !== null)
5681
6070
  return closePromise;
5682
- closePromise = Promise.all([runner.stop(), browserPool.close()]).then(() => new Promise((done) => {
6071
+ liveFiles.close();
6072
+ liveHealth.close();
6073
+ closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
5683
6074
  for (const socket of sockets)
5684
6075
  socket.destroy();
5685
6076
  sockets.clear();
@@ -6152,10 +6543,10 @@ async function runInit(options, deps) {
6152
6543
  // src/run-keep.ts
6153
6544
  import { existsSync as existsSync4 } from "fs";
6154
6545
  import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
6155
- import { dirname as dirname8, join as join15 } from "path";
6546
+ import { dirname as dirname9, join as join15 } from "path";
6156
6547
 
6157
6548
  // src/keep.ts
6158
- import { basename as basename3, extname as extname4, normalize as normalize2 } from "path";
6549
+ import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
6159
6550
  function surfaceOf(url) {
6160
6551
  if (!url.startsWith("/") || !url.includes("?")) return null;
6161
6552
  for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
@@ -6165,7 +6556,7 @@ function surfaceOf(url) {
6165
6556
  return null;
6166
6557
  }
6167
6558
  function exportNameFor(to) {
6168
- const stem = basename3(to, extname4(to));
6559
+ const stem = basename4(to, extname4(to));
6169
6560
  return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
6170
6561
  }
6171
6562
  function planKeep(options) {
@@ -6260,7 +6651,7 @@ async function runKeep(options, deps) {
6260
6651
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
6261
6652
  }
6262
6653
  const source = await readFile13(from, "utf8");
6263
- await mkdir9(dirname8(to), { recursive: true });
6654
+ await mkdir9(dirname9(to), { recursive: true });
6264
6655
  await writeFile10(to, renameExport(source, plan.exportName), "utf8");
6265
6656
  await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
6266
6657
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
@@ -6298,7 +6689,7 @@ async function runKeep(options, deps) {
6298
6689
  // src/run-new.ts
6299
6690
  import { existsSync as existsSync5 } from "fs";
6300
6691
  import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
6301
- import { dirname as dirname9, join as join16 } from "path";
6692
+ import { dirname as dirname10, join as join16 } from "path";
6302
6693
  async function readIfPresent2(path) {
6303
6694
  try {
6304
6695
  return await readFile14(path, "utf8");
@@ -6351,7 +6742,7 @@ async function runNew(options, deps) {
6351
6742
  const written = [];
6352
6743
  for (const write2 of plan.writes) {
6353
6744
  const target = join16(options.cwd, write2.path);
6354
- await mkdir10(dirname9(target), { recursive: true });
6745
+ await mkdir10(dirname10(target), { recursive: true });
6355
6746
  await writeFile11(target, write2.contents, "utf8");
6356
6747
  written.push(write2.path);
6357
6748
  }
@@ -6714,7 +7105,7 @@ async function runShow(options, deps) {
6714
7105
  // src/run-watch.ts
6715
7106
  import { spawn as spawn3 } from "child_process";
6716
7107
  import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
6717
- import { dirname as dirname10, join as join18 } from "path";
7108
+ import { dirname as dirname11, join as join18 } from "path";
6718
7109
  var POLL_MS2 = 2e3;
6719
7110
  var HEARTBEAT_TIMEOUT_MS = 1e3;
6720
7111
  async function saveTemplate(cwd, run4) {
@@ -6728,7 +7119,7 @@ async function saveTemplate(cwd, run4) {
6728
7119
  } catch {
6729
7120
  }
6730
7121
  config.run = run4;
6731
- await mkdir11(dirname10(path), { recursive: true });
7122
+ await mkdir11(dirname11(path), { recursive: true });
6732
7123
  await writeFile13(path, `${JSON.stringify(config, null, 2)}
6733
7124
  `, "utf8");
6734
7125
  }
@@ -6870,13 +7261,13 @@ async function runWatch(options, deps) {
6870
7261
  import { existsSync as existsSync6 } from "fs";
6871
7262
  import { realpath as realpath4 } from "fs/promises";
6872
7263
  import { createRequire } from "module";
6873
- import { basename as basename5, dirname as dirname11, join as join19, relative as relative5, resolve as resolve4 } from "path";
7264
+ import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
6874
7265
  import { fileURLToPath } from "url";
6875
7266
 
6876
7267
  // src/dev-server-owner.ts
6877
7268
  import { execFile as execFile2 } from "child_process";
6878
7269
  import { realpath as realpath3 } from "fs/promises";
6879
- import { basename as basename4, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
7270
+ import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
6880
7271
  import { promisify as promisify2 } from "util";
6881
7272
  var run2 = promisify2(execFile2);
6882
7273
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
@@ -6949,18 +7340,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
6949
7340
  const port = localDevServerPort(origin);
6950
7341
  if (port === null || owners.length === 0) return null;
6951
7342
  if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
6952
- const owner = basename4(resolve3(owners[0]?.cwd ?? "")) || "another project";
6953
- const project = basename4(resolve3(projectRoot)) || "this project";
7343
+ const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
7344
+ const project = basename5(resolve3(projectRoot)) || "this project";
6954
7345
  return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
6955
7346
  }
6956
7347
 
6957
7348
  // src/run.ts
6958
7349
  function findShellDir() {
6959
- const bundled = join19(dirname11(fileURLToPath(import.meta.url)), "shell");
7350
+ const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
6960
7351
  if (existsSync6(join19(bundled, "index.html"))) return bundled;
6961
7352
  try {
6962
7353
  const require2 = createRequire(import.meta.url);
6963
- return dirname11(require2.resolve("@leglas/shell/dist/index.html"));
7354
+ return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
6964
7355
  } catch {
6965
7356
  return null;
6966
7357
  }
@@ -6971,7 +7362,7 @@ function shellWord(value) {
6971
7362
  return `'${value.replaceAll("'", `'\\''`)}'`;
6972
7363
  }
6973
7364
  function embeddedLeglasCommand() {
6974
- const entry = join19(dirname11(fileURLToPath(import.meta.url)), "bin.js");
7365
+ const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
6975
7366
  if (!existsSync6(entry)) return "npx -y leglas";
6976
7367
  return [process.execPath, entry].map(shellWord).join(" ");
6977
7368
  }
@@ -7015,10 +7406,10 @@ async function run3(options, deps) {
7015
7406
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
7016
7407
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
7017
7408
  }
7018
- fileMounts.set(slug, dirname11(absolute));
7409
+ fileMounts.set(slug, dirname12(absolute));
7019
7410
  previews.push({
7020
7411
  ...preview,
7021
- url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename5(absolute))}`
7412
+ url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
7022
7413
  });
7023
7414
  continue;
7024
7415
  }
@@ -7048,8 +7439,8 @@ async function run3(options, deps) {
7048
7439
  }
7049
7440
  const config = merged === null ? null : { ...merged, previews };
7050
7441
  const configWarnings = [];
7051
- const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname11(loaded.path)).catch(
7052
- () => resolve4(loaded.path === null ? options.cwd : dirname11(loaded.path))
7442
+ const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
7443
+ () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
7053
7444
  );
7054
7445
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7055
7446
  const serverPromise = startServer({