leglas 1.1.2 → 1.2.1

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
@@ -1,4 +1,5 @@
1
1
  // src/args.ts
2
+ var SHARE_TUNNELS = ["cloudflared", "ngrok", "none"];
2
3
  var VALUE_FLAGS = /* @__PURE__ */ new Set(["--port", "--user-port", "--config"]);
3
4
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--no-open", "--json"]);
4
5
  var MIN_SHOW_WIDTH = 320;
@@ -11,7 +12,7 @@ function parsePort(flag, raw) {
11
12
  if (port < 1 || port > 65535) {
12
13
  return { error: `${flag} must be between 1 and 65535, received ${port}.` };
13
14
  }
14
- return port;
15
+ return { port };
15
16
  }
16
17
  function parseNew(rest) {
17
18
  let surface;
@@ -35,7 +36,6 @@ function parseNew(rest) {
35
36
  json = true;
36
37
  continue;
37
38
  }
38
- if (argument === "--help" || argument === "-h") return { kind: "help" };
39
39
  if (argument.startsWith("-")) {
40
40
  return { kind: "error", message: `leglas new does not take ${argument}.` };
41
41
  }
@@ -71,7 +71,6 @@ function parseAdd(rest) {
71
71
  json = true;
72
72
  continue;
73
73
  }
74
- if (argument === "--help" || argument === "-h") return { kind: "help" };
75
74
  const equals = argument.indexOf("=");
76
75
  const flag = equals === -1 ? argument : argument.slice(0, equals);
77
76
  let value;
@@ -141,7 +140,6 @@ function parseClassify(rest) {
141
140
  json = true;
142
141
  continue;
143
142
  }
144
- if (argument === "--help" || argument === "-h") return { kind: "help" };
145
143
  const equals = argument.indexOf("=");
146
144
  const flag = equals === -1 ? argument : argument.slice(0, equals);
147
145
  if (flag !== "--change" && flag !== "--rewrite") {
@@ -166,7 +164,6 @@ function parseWatch(rest) {
166
164
  let port;
167
165
  for (let index = 0; index < rest.length; index += 1) {
168
166
  const argument = rest[index];
169
- if (argument === "--help" || argument === "-h") return { kind: "help" };
170
167
  const equals = argument.indexOf("=");
171
168
  const flag = equals === -1 ? argument : argument.slice(0, equals);
172
169
  if (flag !== "--run" && flag !== "--port") {
@@ -184,12 +181,87 @@ function parseWatch(rest) {
184
181
  continue;
185
182
  }
186
183
  const parsed = parsePort(flag, value);
187
- if (typeof parsed !== "number") return { kind: "error", message: parsed.error };
188
- port = parsed;
184
+ if ("error" in parsed) return { kind: "error", message: parsed.error };
185
+ port = parsed.port;
189
186
  }
190
187
  return { kind: "watch", run: run4, port };
191
188
  }
189
+ function parseShare(rest) {
190
+ const titles = [];
191
+ let reach = "open";
192
+ let reachGiven = false;
193
+ let tunnel = null;
194
+ let stop = false;
195
+ let port = null;
196
+ let json = false;
197
+ for (let index = 0; index < rest.length; index += 1) {
198
+ const argument = rest[index];
199
+ if (argument === "--json") {
200
+ json = true;
201
+ continue;
202
+ }
203
+ if (argument === "--stop") {
204
+ stop = true;
205
+ continue;
206
+ }
207
+ const equals = argument.indexOf("=");
208
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
209
+ if (flag === "--reach" || flag === "--tunnel" || flag === "--port") {
210
+ const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
211
+ if (raw === void 0 || raw === "") {
212
+ return { kind: "error", message: `${flag} needs a value.` };
213
+ }
214
+ if (flag === "--port") {
215
+ const parsed = parsePort(flag, raw);
216
+ if ("error" in parsed) return { kind: "error", message: parsed.error };
217
+ port = parsed.port;
218
+ continue;
219
+ }
220
+ if (flag === "--reach") {
221
+ if (raw !== "open" && raw !== "listed") {
222
+ return {
223
+ kind: "error",
224
+ message: `--reach is open or listed, received ${JSON.stringify(raw)}.`
225
+ };
226
+ }
227
+ reach = raw;
228
+ reachGiven = true;
229
+ continue;
230
+ }
231
+ const chosen = SHARE_TUNNELS.find((candidate) => candidate === raw);
232
+ if (chosen === void 0) {
233
+ return {
234
+ kind: "error",
235
+ message: `--tunnel is cloudflared, ngrok or none, received ${JSON.stringify(raw)}.`
236
+ };
237
+ }
238
+ tunnel = chosen;
239
+ continue;
240
+ }
241
+ if (argument.startsWith("-")) {
242
+ return { kind: "error", message: `leglas share does not take ${argument}.` };
243
+ }
244
+ titles.push(argument);
245
+ }
246
+ if (stop && (titles.length > 0 || reachGiven || tunnel !== null)) {
247
+ return {
248
+ kind: "error",
249
+ message: "leglas share --stop ends the share; it takes no directions, reach or tunnel."
250
+ };
251
+ }
252
+ if (titles.length > 2) {
253
+ return {
254
+ kind: "error",
255
+ message: "leglas share takes one direction to share alone, or two to compare. Name none to share the rail."
256
+ };
257
+ }
258
+ return { kind: "share", titles, reach, tunnel, stop, port, json };
259
+ }
192
260
  function parseArgs(argv) {
261
+ const [command] = argv;
262
+ if (command !== void 0 && !command.startsWith("-") && argv.slice(1).some((argument) => argument === "--help" || argument === "-h")) {
263
+ return { kind: "help" };
264
+ }
193
265
  if (argv[0] === "new") return parseNew(argv.slice(1));
194
266
  if (argv[0] === "watch") return parseWatch(argv.slice(1));
195
267
  if (argv[0] === "add") return parseAdd(argv.slice(1));
@@ -347,8 +419,8 @@ function parseArgs(argv) {
347
419
  }
348
420
  if (flag === "--port") {
349
421
  const parsed = parsePort(flag, raw);
350
- if (typeof parsed !== "number") return { kind: "error", message: parsed.error };
351
- port = parsed;
422
+ if ("error" in parsed) return { kind: "error", message: parsed.error };
423
+ port = parsed.port;
352
424
  continue;
353
425
  }
354
426
  if (!/^\d+$/.test(raw)) {
@@ -388,6 +460,7 @@ function parseArgs(argv) {
388
460
  }
389
461
  return { kind: "show", title, json, screenshot, width, port };
390
462
  }
463
+ if (argv[0] === "share") return parseShare(argv.slice(1));
391
464
  const options = {
392
465
  port: void 0,
393
466
  userPort: void 0,
@@ -427,9 +500,9 @@ function parseArgs(argv) {
427
500
  continue;
428
501
  }
429
502
  const port = parsePort(flag, value);
430
- if (typeof port !== "number") return { kind: "error", message: port.error };
431
- if (flag === "--port") options.port = port;
432
- else options.userPort = port;
503
+ if ("error" in port) return { kind: "error", message: port.error };
504
+ if (flag === "--port") options.port = port.port;
505
+ else options.userPort = port.port;
433
506
  }
434
507
  return { kind: "run", options };
435
508
  }
@@ -525,8 +598,10 @@ type SearchParams = Record<string, string | string[] | undefined>;
525
598
  /**
526
599
  * Production always renders the fallback, whatever the URL says, so a branch
527
600
  * point that reaches a deployed build cannot expose an unreleased direction.
601
+ * It is not exported because a file whose exports are all components hot-swaps
602
+ * in place, while an extra export makes every edit reload the page.
528
603
  */
529
- export function resolve${titleCase(slug).replace(/\s/g, "")}(searchParams: SearchParams): Direction {
604
+ function resolve${titleCase(slug).replace(/\s/g, "")}(searchParams: SearchParams): Direction {
530
605
  if (process.env.NODE_ENV === "production") return FALLBACK;
531
606
  const raw = searchParams["v-${slug}"];
532
607
  const value = Array.isArray(raw) ? raw[0] : raw;
@@ -572,8 +647,10 @@ function isProduction(): boolean {
572
647
  /**
573
648
  * Production always renders the fallback, whatever the URL says, so a branch
574
649
  * point that reaches a deployed build cannot expose an unreleased direction.
650
+ * It is not exported because a file whose exports are all components hot-swaps
651
+ * in place, while an extra export makes every edit reload the page.
575
652
  */
576
- export function resolve${name}(): Direction {
653
+ function resolve${name}(): Direction {
577
654
  if (isProduction()) return FALLBACK;
578
655
  const value = new URLSearchParams(window.location.search).get("v-${slug}");
579
656
  return value !== null && value in DIRECTIONS ? (value as Direction) : FALLBACK;
@@ -855,7 +932,7 @@ function frameFor(title, requests) {
855
932
  return found;
856
933
  }
857
934
  function askedOf(title, requests) {
858
- return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
935
+ return requests.flatMap((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "" ? [request.intent.trim()] : []);
859
936
  }
860
937
  function composeEntry(input) {
861
938
  const slug = `${input.date}-${slugify(input.surface)}`;
@@ -915,13 +992,27 @@ function composeEntry(input) {
915
992
  `, pictures };
916
993
  }
917
994
 
995
+ // ../server/dist/json.js
996
+ function parseJson(text2) {
997
+ return JSON.parse(text2);
998
+ }
999
+ function isJsonRecord(value) {
1000
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1001
+ }
1002
+ function isString(value) {
1003
+ return typeof value === "string";
1004
+ }
1005
+ function isNumber(value) {
1006
+ return typeof value === "number";
1007
+ }
1008
+ function isBoolean(value) {
1009
+ return typeof value === "boolean";
1010
+ }
1011
+
918
1012
  // ../server/dist/config/config.js
919
1013
  var DEFAULT_DEV_SERVER = "http://localhost:3000";
920
1014
  var DEFAULT_INSTALL_COMMAND = "npm install";
921
1015
  var IMPLICIT_PREVIEW = { title: "App", url: "/" };
922
- function isRecord(value) {
923
- return typeof value === "object" && value !== null && !Array.isArray(value);
924
- }
925
1016
  function isValidOrigin(value) {
926
1017
  try {
927
1018
  const url = new URL(value);
@@ -951,11 +1042,11 @@ function normalizeConfig(raw, options = {}) {
951
1042
  const requireDevCommand = options.requireDevCommand ?? true;
952
1043
  const errors = [];
953
1044
  const source = raw === void 0 || raw === null ? {} : raw;
954
- if (!isRecord(source)) {
1045
+ if (!isJsonRecord(source)) {
955
1046
  return { config: null, errors: ["Config must export an object."] };
956
1047
  }
957
1048
  const devServer = source["devServer"] ?? DEFAULT_DEV_SERVER;
958
- if (typeof devServer !== "string" || !isValidOrigin(devServer)) {
1049
+ if (!isString(devServer) || !isValidOrigin(devServer)) {
959
1050
  errors.push(`devServer must be an http(s) URL, received ${JSON.stringify(devServer)}.`);
960
1051
  }
961
1052
  const rawPreviews = source["previews"] ?? [IMPLICIT_PREVIEW];
@@ -967,14 +1058,14 @@ function normalizeConfig(raw, options = {}) {
967
1058
  const seenTitles = /* @__PURE__ */ new Set();
968
1059
  rawPreviews.forEach((entry, index) => {
969
1060
  const at = `previews[${index}]`;
970
- if (!isRecord(entry)) {
1061
+ if (!isJsonRecord(entry)) {
971
1062
  errors.push(`${at} must be an object.`);
972
1063
  return;
973
1064
  }
974
1065
  const title = entry["title"];
975
1066
  const url = entry["url"];
976
1067
  const file = entry["file"];
977
- if (typeof title !== "string" || title.trim() === "") {
1068
+ if (!isString(title) || title.trim() === "") {
978
1069
  errors.push(`${at} needs a title; the rail has nothing to show without one.`);
979
1070
  } else if (seenTitles.has(title)) {
980
1071
  errors.push(`${at} repeats the title ${JSON.stringify(title)}; titles must be unique.`);
@@ -982,22 +1073,22 @@ function normalizeConfig(raw, options = {}) {
982
1073
  seenTitles.add(title);
983
1074
  }
984
1075
  if (file !== void 0) {
985
- if (typeof file !== "string" || !isSafePreviewFile(file)) {
1076
+ if (!isString(file) || !isSafePreviewFile(file)) {
986
1077
  errors.push(`${at} has an unusable file ${JSON.stringify(file)}; use a path inside the project, like "directions/hero.html".`);
987
1078
  }
988
1079
  if (url !== void 0) {
989
1080
  errors.push(`${at} names a file and a url; a file preview's url is assigned by Leglas.`);
990
1081
  }
991
- } else if (typeof url !== "string" || url.trim() === "") {
1082
+ } else if (!isString(url) || url.trim() === "") {
992
1083
  errors.push(`${at} needs a url.`);
993
1084
  } else if (!isValidPreviewUrl(url)) {
994
1085
  errors.push(`${at} has url ${JSON.stringify(url)}; use a root-relative path ("/pricing") or a full URL.`);
995
1086
  }
996
1087
  const branch = entry["branch"];
997
1088
  if (branch !== void 0) {
998
- if (typeof branch !== "string" || !isSafeBranch(branch)) {
1089
+ if (!isString(branch) || !isSafeBranch(branch)) {
999
1090
  errors.push(`${at} has an unusable branch ${JSON.stringify(branch)}; use a plain git branch name.`);
1000
- } else if (typeof url === "string" && !url.startsWith("/")) {
1091
+ } else if (isString(url) && !url.startsWith("/")) {
1001
1092
  errors.push(`${at} names a branch and an absolute url; a branch preview is served by Leglas, so its url must be a path.`);
1002
1093
  }
1003
1094
  if (file !== void 0) {
@@ -1005,54 +1096,59 @@ function normalizeConfig(raw, options = {}) {
1005
1096
  }
1006
1097
  }
1007
1098
  const basedOn = entry["basedOn"];
1008
- if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
1099
+ if (basedOn !== void 0 && (!isString(basedOn) || basedOn.trim() === "")) {
1009
1100
  errors.push(`${at} has a basedOn that is not a direction title.`);
1010
1101
  }
1011
1102
  const askedFor = entry["askedFor"];
1012
- if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
1103
+ if (askedFor !== void 0 && (!isString(askedFor) || askedFor.trim() === "")) {
1013
1104
  errors.push(`${at} has an askedFor that is not a change request.`);
1014
1105
  }
1015
1106
  const tags = entry["tags"];
1016
- previews.push({
1017
- title: typeof title === "string" ? title : "",
1018
- url: typeof url === "string" ? url : "",
1019
- note: typeof entry["note"] === "string" ? entry["note"] : void 0,
1020
- tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
1021
- ...typeof branch === "string" ? { branch } : {},
1022
- ...typeof file === "string" ? { file } : {},
1023
- ...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
1024
- ...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
1025
- });
1107
+ const preview = {
1108
+ title: isString(title) ? title : "",
1109
+ url: isString(url) ? url : "",
1110
+ note: isString(entry["note"]) ? entry["note"] : void 0,
1111
+ tags: Array.isArray(tags) ? tags.filter((tag) => isString(tag)) : []
1112
+ };
1113
+ if (isString(branch))
1114
+ preview.branch = branch;
1115
+ if (isString(file))
1116
+ preview.file = file;
1117
+ if (isString(basedOn) && basedOn.trim() !== "")
1118
+ preview.basedOn = basedOn;
1119
+ if (isString(askedFor) && askedFor.trim() !== "")
1120
+ preview.askedFor = askedFor;
1121
+ previews.push(preview);
1026
1122
  });
1027
1123
  const devCommand = source["devCommand"];
1028
- if (devCommand !== void 0 && typeof devCommand !== "string") {
1124
+ if (devCommand !== void 0 && !isString(devCommand)) {
1029
1125
  errors.push("devCommand must be a string.");
1030
- } else if (typeof devCommand === "string" && !devCommand.includes("{port}")) {
1126
+ } else if (isString(devCommand) && !devCommand.includes("{port}")) {
1031
1127
  errors.push(`devCommand must include {port}, so Leglas can start each checkout on a free port. Received ${JSON.stringify(devCommand)}.`);
1032
1128
  }
1033
1129
  if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
1034
1130
  errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
1035
1131
  }
1036
1132
  const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
1037
- if (typeof logDir !== "string" || logDir.trim() === "") {
1133
+ if (!isString(logDir) || logDir.trim() === "") {
1038
1134
  errors.push("logDir must be a non-empty string.");
1039
1135
  }
1040
1136
  const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
1041
- if (typeof installCommand !== "string" || installCommand.trim() === "") {
1137
+ if (!isString(installCommand) || installCommand.trim() === "") {
1042
1138
  errors.push("installCommand must be a non-empty string.");
1043
1139
  }
1044
1140
  const scanPreviews = source["scanPreviews"] ?? true;
1045
- if (typeof scanPreviews !== "boolean") {
1141
+ if (!isBoolean(scanPreviews)) {
1046
1142
  errors.push("scanPreviews must be a boolean.");
1047
1143
  }
1048
- if (errors.length > 0)
1144
+ if (errors.length > 0 || !isString(devServer) || !isBoolean(scanPreviews) || !isString(installCommand) || !isString(logDir))
1049
1145
  return { config: null, errors };
1050
1146
  return {
1051
1147
  config: {
1052
1148
  devServer,
1053
1149
  previews,
1054
1150
  scanPreviews,
1055
- devCommand: typeof devCommand === "string" ? devCommand : void 0,
1151
+ devCommand: isString(devCommand) ? devCommand : void 0,
1056
1152
  installCommand,
1057
1153
  logDir
1058
1154
  },
@@ -1068,17 +1164,17 @@ function tokenize(template) {
1068
1164
  const tokens = [];
1069
1165
  let current = "";
1070
1166
  let started = false;
1071
- let quote = null;
1167
+ let quote2 = null;
1072
1168
  for (const character of template) {
1073
- if (quote !== null) {
1074
- if (character === quote)
1075
- quote = null;
1169
+ if (quote2 !== null) {
1170
+ if (character === quote2)
1171
+ quote2 = null;
1076
1172
  else
1077
1173
  current += character;
1078
1174
  continue;
1079
1175
  }
1080
1176
  if (character === '"' || character === "'") {
1081
- quote = character;
1177
+ quote2 = character;
1082
1178
  started = true;
1083
1179
  continue;
1084
1180
  }
@@ -1092,8 +1188,8 @@ function tokenize(template) {
1092
1188
  current += character;
1093
1189
  started = true;
1094
1190
  }
1095
- if (quote !== null) {
1096
- return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
1191
+ if (quote2 !== null) {
1192
+ return { ok: false, error: `The agent command has an unclosed ${quote2} quote.` };
1097
1193
  }
1098
1194
  if (started)
1099
1195
  tokens.push(current);
@@ -1190,7 +1286,7 @@ var KNOWN_AGENTS = {
1190
1286
  ],
1191
1287
  activityVerified: true,
1192
1288
  // Every stream-json event names its session.
1193
- sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
1289
+ sessionFrom: (event) => isString(event.session_id) && event.session_id !== "" ? event.session_id : null,
1194
1290
  authArgs: ["auth", "status"],
1195
1291
  // `claude auth status` prints JSON with a loggedIn boolean. Only that
1196
1292
  // field decides; any other shape stays unknown.
@@ -1253,7 +1349,7 @@ var KNOWN_AGENTS = {
1253
1349
  ...images.flatMap((image) => ["-i", image]),
1254
1350
  prompt
1255
1351
  ],
1256
- sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
1352
+ sessionFrom: (event) => event.type === "thread.started" && isString(event.thread_id) ? event.thread_id : null,
1257
1353
  activityVerified: true,
1258
1354
  authArgs: ["login", "status"],
1259
1355
  // `codex login status` exits 0 when logged in and nonzero when not.
@@ -1291,7 +1387,7 @@ var KNOWN_AGENTS = {
1291
1387
  "stream-json",
1292
1388
  "--trust"
1293
1389
  ],
1294
- sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
1390
+ sessionFrom: (event) => isString(event.session_id) && event.session_id !== "" ? event.session_id : null,
1295
1391
  // Read against cursor-agent 2026.09.02: every event carries the id, a
1296
1392
  // resume in this argument order answers under the same id and remembers
1297
1393
  // the earlier turn.
@@ -1383,7 +1479,7 @@ function agentSearchPath(env = process.env, platform = process.platform) {
1383
1479
  platform === "darwin" ? "/usr/local/bin" : void 0,
1384
1480
  platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
1385
1481
  platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
1386
- ].filter((entry) => typeof entry === "string" && entry !== "");
1482
+ ].filter((entry) => isString(entry) && entry !== "");
1387
1483
  return [...new Set(candidates)].join(delimiter);
1388
1484
  }
1389
1485
  function agentEnvironment(env = process.env) {
@@ -1427,23 +1523,23 @@ async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
1427
1523
  }));
1428
1524
  }
1429
1525
  function record(value) {
1430
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1526
+ return isJsonRecord(value) ? value : null;
1431
1527
  }
1432
1528
  function shownPath(value, cwd) {
1433
- if (typeof value !== "string" || value === "")
1529
+ if (!isString(value) || value === "")
1434
1530
  return null;
1435
1531
  if (!isAbsolute(value))
1436
1532
  return value;
1437
1533
  return relative(cwd, value) || ".";
1438
1534
  }
1439
1535
  function shownCommand(value) {
1440
- let command = Array.isArray(value) ? value.filter((part) => typeof part === "string").join(" ") : typeof value === "string" ? value : "";
1536
+ let command = Array.isArray(value) ? value.filter((part) => isString(part)).join(" ") : isString(value) ? value : "";
1441
1537
  command = command.trim();
1442
1538
  const wrapped = /^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+([\s\S]*)$/.exec(command);
1443
1539
  if (wrapped?.[1] !== void 0) {
1444
1540
  command = wrapped[1].trim();
1445
- const quote = command[0];
1446
- if ((quote === "'" || quote === '"') && command.endsWith(quote) && command.length > 1) {
1541
+ const quote2 = command[0];
1542
+ if ((quote2 === "'" || quote2 === '"') && command.endsWith(quote2) && command.length > 1) {
1447
1543
  command = command.slice(1, -1);
1448
1544
  }
1449
1545
  }
@@ -1460,7 +1556,7 @@ function claudeActivity(event, cwd) {
1460
1556
  return null;
1461
1557
  for (const rawBlock of message2.content) {
1462
1558
  const block = record(rawBlock);
1463
- if (block?.type !== "tool_use" || typeof block.name !== "string")
1559
+ if (block?.type !== "tool_use" || !isString(block.name))
1464
1560
  continue;
1465
1561
  const input = record(block.input);
1466
1562
  if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name)) {
@@ -1526,7 +1622,7 @@ function cursorActivity(event, cwd) {
1526
1622
  function activityFrom(agent, line, cwd = process.cwd()) {
1527
1623
  let event;
1528
1624
  try {
1529
- event = record(JSON.parse(line));
1625
+ event = record(parseJson(line));
1530
1626
  } catch {
1531
1627
  return null;
1532
1628
  }
@@ -1545,7 +1641,7 @@ function sessionFrom(agent, line) {
1545
1641
  return null;
1546
1642
  let event;
1547
1643
  try {
1548
- event = record(JSON.parse(line));
1644
+ event = record(parseJson(line));
1549
1645
  } catch {
1550
1646
  return null;
1551
1647
  }
@@ -1558,29 +1654,29 @@ function retryFrom(agent, line) {
1558
1654
  return null;
1559
1655
  let event;
1560
1656
  try {
1561
- event = record(JSON.parse(line));
1657
+ event = record(parseJson(line));
1562
1658
  } catch {
1563
1659
  return null;
1564
1660
  }
1565
1661
  if (event === null || event.type !== "system" || event.subtype !== "api_retry")
1566
1662
  return null;
1567
- const attempt = typeof event.attempt === "number" ? event.attempt : 1;
1663
+ const attempt = isNumber(event.attempt) ? event.attempt : 1;
1568
1664
  return {
1569
1665
  attempt,
1570
- max: typeof event.max_retries === "number" ? event.max_retries : null,
1571
- status: typeof event.error_status === "number" ? event.error_status : null,
1572
- reason: typeof event.error === "string" && event.error !== "" ? event.error.toLowerCase() : null
1666
+ max: isNumber(event.max_retries) ? event.max_retries : null,
1667
+ status: isNumber(event.error_status) ? event.error_status : null,
1668
+ reason: isString(event.error) && event.error !== "" ? event.error.toLowerCase() : null
1573
1669
  };
1574
1670
  }
1575
1671
  function isAgentChoice(value) {
1576
- return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
1672
+ return value === "custom" || isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
1577
1673
  }
1578
1674
  function isAgentEffort(value) {
1579
- return typeof value === "string" && AGENT_EFFORTS.includes(value);
1675
+ return isString(value) && AGENT_EFFORTS.some((effort) => effort === value);
1580
1676
  }
1581
1677
  async function readWatchConfig(cwd) {
1582
1678
  try {
1583
- const parsed = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
1679
+ const parsed = parseJson(await readFile(join(cwd, WATCH_PATH), "utf8"));
1584
1680
  return record(parsed) ?? {};
1585
1681
  } catch {
1586
1682
  return {};
@@ -1593,7 +1689,7 @@ async function readAgentChoice(cwd) {
1593
1689
  return {
1594
1690
  agent,
1595
1691
  effort: agent !== null && agent !== "custom" && isAgentEffort(efforts?.[agent]) ? efforts[agent] : null,
1596
- run: typeof config.run === "string" && config.run !== "" ? config.run : null
1692
+ run: isString(config.run) && config.run !== "" ? config.run : null
1597
1693
  };
1598
1694
  }
1599
1695
  async function saveAgentChoice(cwd, choice) {
@@ -1739,7 +1835,7 @@ async function loadConfig(cwd) {
1739
1835
  const message2 = error instanceof Error ? error.message : String(error);
1740
1836
  return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
1741
1837
  }
1742
- const result = normalizeConfig(exported);
1838
+ const result = isJsonRecord(exported) || exported === void 0 || exported === null ? normalizeConfig(exported ?? void 0) : { config: null, errors: ["Config must export an object."] };
1743
1839
  return {
1744
1840
  config: result.config,
1745
1841
  errors: result.errors.map((error) => `${label}: ${error}`),
@@ -1757,7 +1853,7 @@ async function readLocalPreviews(cwd) {
1757
1853
  try {
1758
1854
  raw = await readFile3(path, "utf8");
1759
1855
  } catch (error) {
1760
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
1856
+ const code = error instanceof Error && "code" in error && isString(error.code) ? error.code : null;
1761
1857
  if (code === "ENOENT") {
1762
1858
  return { previews: [], errors: [] };
1763
1859
  }
@@ -1770,7 +1866,7 @@ async function readLocalPreviews(cwd) {
1770
1866
  }
1771
1867
  let parsed;
1772
1868
  try {
1773
- parsed = JSON.parse(raw);
1869
+ parsed = parseJson(raw);
1774
1870
  } catch (error) {
1775
1871
  return {
1776
1872
  previews: [],
@@ -1803,16 +1899,21 @@ async function addLocalPreview(cwd, input, shared) {
1803
1899
  error: `A preview called ${JSON.stringify(input.title)} already exists. Titles identify previews, so they have to be unique.`
1804
1900
  };
1805
1901
  }
1806
- const candidate = {
1807
- title: input.title,
1808
- ...input.url === void 0 ? {} : { url: input.url },
1809
- ...input.note === void 0 ? {} : { note: input.note },
1810
- ...input.tags === void 0 ? {} : { tags: input.tags },
1811
- ...input.branch === void 0 ? {} : { branch: input.branch },
1812
- ...input.file === void 0 ? {} : { file: input.file },
1813
- ...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
1814
- ...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
1815
- };
1902
+ const candidate = { title: input.title };
1903
+ if (input.url !== void 0)
1904
+ candidate.url = input.url;
1905
+ if (input.note !== void 0)
1906
+ candidate.note = input.note;
1907
+ if (input.tags !== void 0)
1908
+ candidate.tags = [...input.tags];
1909
+ if (input.branch !== void 0)
1910
+ candidate.branch = input.branch;
1911
+ if (input.file !== void 0)
1912
+ candidate.file = input.file;
1913
+ if (input.basedOn !== void 0)
1914
+ candidate.basedOn = input.basedOn;
1915
+ if (input.askedFor !== void 0)
1916
+ candidate.askedFor = input.askedFor;
1816
1917
  const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
1817
1918
  if (check.config === null) {
1818
1919
  return { ok: false, error: check.errors.join(" ") };
@@ -1892,7 +1993,7 @@ function createProxyHandler(options) {
1892
1993
  res.once("close", close);
1893
1994
  const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
1894
1995
  const headers = { ...upstreamRes.headers };
1895
- const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin2);
1996
+ const location = rewriteLocation(isString(headers.location) ? headers.location : void 0, publicOrigin2);
1896
1997
  if (location !== void 0)
1897
1998
  headers.location = location;
1898
1999
  res.writeHead(upstreamRes.statusCode ?? 502, headers);
@@ -1961,7 +2062,7 @@ function startProxyServer(options) {
1961
2062
  });
1962
2063
  const server = http.createServer((req, res) => {
1963
2064
  const address = server.address();
1964
- const port = typeof address === "object" && address !== null ? address.port : 0;
2065
+ const port = address !== null && !isString(address) ? address.port : 0;
1965
2066
  handler.request(req, res, `http://127.0.0.1:${port}`);
1966
2067
  });
1967
2068
  const sockets = /* @__PURE__ */ new Set();
@@ -1977,7 +2078,7 @@ function startProxyServer(options) {
1977
2078
  const onListening = () => {
1978
2079
  server.removeListener("error", onError);
1979
2080
  const address = server.address();
1980
- const port = typeof address === "object" && address !== null ? address.port : 0;
2081
+ const port = address !== null && !isString(address) ? address.port : 0;
1981
2082
  let closed = null;
1982
2083
  resolve5({
1983
2084
  active: () => open > 0,
@@ -2109,7 +2210,7 @@ function findBrowser(search = {}) {
2109
2210
  const readdir4 = search.readdir ?? readableDirectories;
2110
2211
  const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
2111
2212
  for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
2112
- if (typeof candidate === "string" && candidate !== "" && exists(candidate))
2213
+ if (isString(candidate) && candidate !== "" && exists(candidate))
2113
2214
  return candidate;
2114
2215
  }
2115
2216
  const caches = cacheRoots(platform, home, readdir4);
@@ -2135,7 +2236,7 @@ function findBrowser(search = {}) {
2135
2236
  return installed;
2136
2237
  }
2137
2238
  if (platform === "win32") {
2138
- const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => typeof entry === "string" && entry !== "");
2239
+ const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => isString(entry) && entry !== "");
2139
2240
  const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) => join4(root, browser))));
2140
2241
  if (installed !== null)
2141
2242
  return installed;
@@ -2218,7 +2319,7 @@ function endpoint(process2, timeoutMs) {
2218
2319
  return;
2219
2320
  settled = true;
2220
2321
  clearTimeout(timer);
2221
- if (typeof value === "string")
2322
+ if (isString(value))
2222
2323
  resolve5(value);
2223
2324
  else
2224
2325
  reject(value);
@@ -2247,12 +2348,15 @@ var OWNER_FILE = "leglas-owner.json";
2247
2348
  var PROFILE_PREFIX = "leglas-browser-";
2248
2349
  var RECORD_GRACE_MS = 6e4;
2249
2350
  var REAP_CLOSE_MS = 2e3;
2351
+ function permissionDenied(cause) {
2352
+ return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EPERM";
2353
+ }
2250
2354
  function livePid(pid) {
2251
2355
  try {
2252
2356
  nodeProcess.kill(pid, 0);
2253
2357
  return true;
2254
2358
  } catch (error) {
2255
- return typeof error === "object" && error !== null && error.code === "EPERM";
2359
+ return permissionDenied(error);
2256
2360
  }
2257
2361
  }
2258
2362
  async function closeOrphan(url, connect) {
@@ -2282,7 +2386,7 @@ async function closeOrphan(url, connect) {
2282
2386
  }
2283
2387
  async function reapOrphanedBrowsers(deps = {}) {
2284
2388
  const root = deps.tmpdir ?? osTmpdir();
2285
- const clock = deps.now ?? (() => Date.now());
2389
+ const clock2 = deps.now ?? (() => Date.now());
2286
2390
  const list = deps.list ?? (async (path) => (await readdir(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
2287
2391
  const read = deps.read ?? ((path) => readFile4(path, "utf8"));
2288
2392
  const alive = deps.alive ?? livePid;
@@ -2315,15 +2419,16 @@ async function reapOrphanedBrowsers(deps = {}) {
2315
2419
  let record3 = null;
2316
2420
  try {
2317
2421
  const raw = await read(join4(directory, OWNER_FILE));
2318
- record3 = raw === null ? null : JSON.parse(raw);
2422
+ const parsed = raw === null ? null : parseJson(raw);
2423
+ record3 = isJsonRecord(parsed) ? parsed : null;
2319
2424
  } catch {
2320
2425
  }
2321
- const owner = typeof record3?.owner === "number" ? record3.owner : null;
2426
+ const owner = isNumber(record3?.owner) ? record3.owner : null;
2322
2427
  if (owner !== null && alive(owner))
2323
2428
  continue;
2324
- if (owner === null && clock() - details.createdAt < RECORD_GRACE_MS)
2429
+ if (owner === null && clock2() - details.createdAt < RECORD_GRACE_MS)
2325
2430
  continue;
2326
- const endpoint2 = typeof record3?.ws === "string" && record3.ws !== "" ? record3.ws : null;
2431
+ const endpoint2 = isString(record3?.ws) && record3.ws !== "" ? record3.ws : null;
2327
2432
  if (endpoint2 !== null && await closeOrphan(endpoint2, connect))
2328
2433
  reaped += 1;
2329
2434
  await remove(directory).catch(() => {
@@ -2332,7 +2437,7 @@ async function reapOrphanedBrowsers(deps = {}) {
2332
2437
  return reaped;
2333
2438
  }
2334
2439
  async function launchBrowser(executable, options = {}) {
2335
- const spawn4 = options.spawn ?? nodeSpawn;
2440
+ const spawn5 = options.spawn ?? nodeSpawn;
2336
2441
  const connect = options.connect ?? connectWebSocket;
2337
2442
  const commandTimeoutMs = options.commandTimeoutMs ?? 3e4;
2338
2443
  const userDataDir = join4(options.tmpdir ?? osTmpdir(), `leglas-browser-${randomBytes(8).toString("hex")}`);
@@ -2345,7 +2450,7 @@ async function launchBrowser(executable, options = {}) {
2345
2450
  }
2346
2451
  };
2347
2452
  owned({});
2348
- const process2 = spawn4(executable, [
2453
+ const process2 = spawn5(executable, [
2349
2454
  "--headless=new",
2350
2455
  "--remote-debugging-port=0",
2351
2456
  `--user-data-dir=${userDataDir}`,
@@ -2423,11 +2528,14 @@ async function launchBrowser(executable, options = {}) {
2423
2528
  socket.onMessage((text2) => {
2424
2529
  let message2;
2425
2530
  try {
2426
- message2 = JSON.parse(text2);
2531
+ const parsed = parseJson(text2);
2532
+ if (!isJsonRecord(parsed))
2533
+ return;
2534
+ message2 = parsed;
2427
2535
  } catch {
2428
2536
  return;
2429
2537
  }
2430
- if (typeof message2.id === "number") {
2538
+ if (isNumber(message2.id)) {
2431
2539
  const command = pending.get(message2.id);
2432
2540
  if (command === void 0)
2433
2541
  return;
@@ -2435,15 +2543,15 @@ async function launchBrowser(executable, options = {}) {
2435
2543
  clearTimeout(command.timer);
2436
2544
  const error = message2.error;
2437
2545
  if (error !== void 0) {
2438
- command.reject(new Error(typeof error.message === "string" ? error.message : "CDP command failed."));
2546
+ command.reject(new Error(isJsonRecord(error) && isString(error.message) ? error.message : "CDP command failed."));
2439
2547
  } else {
2440
2548
  command.resolve(message2.result);
2441
2549
  }
2442
2550
  return;
2443
2551
  }
2444
- if (typeof message2.method !== "string")
2552
+ if (!isString(message2.method))
2445
2553
  return;
2446
- const sessionId = typeof message2.sessionId === "string" ? message2.sessionId : "";
2554
+ const sessionId = isString(message2.sessionId) ? message2.sessionId : "";
2447
2555
  const key = `${sessionId}\0${message2.method}`;
2448
2556
  for (const listener of listeners.get(key) ?? [])
2449
2557
  listener(message2.params ?? {});
@@ -2466,17 +2574,16 @@ async function launchBrowser(executable, options = {}) {
2466
2574
  }, commandTimeoutMs);
2467
2575
  timer.unref?.();
2468
2576
  pending.set(id, {
2577
+ // SAFETY: The pending command id pairs this CDP result with the method and result type requested by its caller.
2469
2578
  resolve: (value) => resolve5(value),
2470
2579
  reject,
2471
2580
  timer
2472
2581
  });
2473
2582
  try {
2474
- socket.send(JSON.stringify({
2475
- id,
2476
- method,
2477
- params,
2478
- ...sessionId === void 0 ? {} : { sessionId }
2479
- }));
2583
+ const message2 = { id, method, params };
2584
+ if (sessionId !== void 0)
2585
+ message2.sessionId = sessionId;
2586
+ socket.send(JSON.stringify(message2));
2480
2587
  } catch {
2481
2588
  clearTimeout(timer);
2482
2589
  pending.delete(id);
@@ -2561,36 +2668,37 @@ function createBrowserPool(options = {}) {
2561
2668
  const launch = options.launch ?? launchBrowser;
2562
2669
  const idleMs = options.idleMs ?? 6e4;
2563
2670
  const setLater = options.setTimeout ?? ((callback, milliseconds) => {
2564
- const timer2 = setTimeout(callback, milliseconds);
2565
- timer2.unref?.();
2566
- return timer2;
2671
+ const timer = setTimeout(callback, milliseconds);
2672
+ timer.unref?.();
2673
+ return timer;
2567
2674
  });
2568
2675
  const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
2569
2676
  let browser = null;
2570
2677
  let exposed = null;
2571
2678
  let working = 0;
2572
2679
  let launching = null;
2573
- let timer = null;
2680
+ let cancelTimer = null;
2574
2681
  let lastReason = null;
2575
2682
  let closed = false;
2576
2683
  const clearIdle = () => {
2577
- if (timer === null)
2684
+ if (cancelTimer === null)
2578
2685
  return;
2579
- clearLater(timer);
2580
- timer = null;
2686
+ cancelTimer();
2687
+ cancelTimer = null;
2581
2688
  };
2582
2689
  const scheduleIdle = () => {
2583
2690
  clearIdle();
2584
2691
  if (browser === null || closed || working > 0)
2585
2692
  return;
2586
- timer = setLater(() => {
2587
- timer = null;
2693
+ const handle = setLater(() => {
2694
+ cancelTimer = null;
2588
2695
  const retiring = browser;
2589
2696
  browser = null;
2590
2697
  exposed = null;
2591
2698
  void retiring?.close().catch(() => {
2592
2699
  });
2593
2700
  }, idleMs);
2701
+ cancelTimer = () => clearLater(handle);
2594
2702
  };
2595
2703
  const wrap = (launched) => ({
2596
2704
  withPage: async (work) => {
@@ -2640,8 +2748,8 @@ function createBrowserPool(options = {}) {
2640
2748
  lastReason = null;
2641
2749
  scheduleIdle();
2642
2750
  return exposed;
2643
- }).catch((error) => {
2644
- lastReason = error instanceof Error ? error.message : String(error);
2751
+ }).catch((cause) => {
2752
+ lastReason = cause instanceof Error ? cause.message : String(cause);
2645
2753
  return null;
2646
2754
  }).finally(() => {
2647
2755
  if (launching === attempt)
@@ -2715,6 +2823,12 @@ var LOCATOR = `(function (selector, text, tag) {
2715
2823
  if (r.width === 0 && r.height === 0) return null;
2716
2824
  return { x: r.left + window.scrollX, y: r.top + window.scrollY, width: r.width, height: r.height };
2717
2825
  })`;
2826
+ var DRAWN_WITH = /* @__PURE__ */ new Set(["Script", "Stylesheet", "Font", "Image"]);
2827
+ var LAID_OUT = `(async () => {
2828
+ await new Promise((next) => requestAnimationFrame(() => requestAnimationFrame(next)));
2829
+ if (document.fonts) await document.fonts.ready;
2830
+ return true;
2831
+ })()`;
2718
2832
  function clamp(value, low, high) {
2719
2833
  return Math.min(high, Math.max(low, value));
2720
2834
  }
@@ -2747,9 +2861,9 @@ function bounded(work, milliseconds, fallback) {
2747
2861
  void work.then((value) => {
2748
2862
  clearTimeout(timer);
2749
2863
  resolve5(value);
2750
- }, (error) => {
2864
+ }, (cause) => {
2751
2865
  clearTimeout(timer);
2752
- reject(error);
2866
+ reject(cause);
2753
2867
  });
2754
2868
  });
2755
2869
  }
@@ -2764,9 +2878,9 @@ function abortable(work, signal) {
2764
2878
  void work.then((value) => {
2765
2879
  signal.removeEventListener("abort", abort);
2766
2880
  resolve5(value);
2767
- }, (error) => {
2881
+ }, (cause) => {
2768
2882
  signal.removeEventListener("abort", abort);
2769
- reject(error);
2883
+ reject(cause);
2770
2884
  });
2771
2885
  });
2772
2886
  }
@@ -2775,10 +2889,9 @@ function resultValue(response) {
2775
2889
  return result !== void 0 && "value" in result ? result.value : null;
2776
2890
  }
2777
2891
  function validBox(value) {
2778
- if (typeof value !== "object" || value === null)
2892
+ if (!isJsonRecord(value))
2779
2893
  return false;
2780
- const box = value;
2781
- return [box.x, box.y, box.width, box.height].every((entry) => typeof entry === "number" && Number.isFinite(entry));
2894
+ return [value.x, value.y, value.width, value.height].every((entry) => isNumber(entry) && Number.isFinite(entry));
2782
2895
  }
2783
2896
  function locatorExpression(focus) {
2784
2897
  return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
@@ -2797,14 +2910,49 @@ async function render(page, input) {
2797
2910
  errors.push(message2);
2798
2911
  };
2799
2912
  let documentStatus = null;
2913
+ const pending = /* @__PURE__ */ new Set();
2914
+ const afterLoadScripts = /* @__PURE__ */ new Set();
2915
+ let loadFired = false;
2916
+ let scriptLanded = null;
2917
+ let asked = 0;
2918
+ let emptied = () => {
2919
+ };
2920
+ const received = (id) => {
2921
+ if (afterLoadScripts.delete(id))
2922
+ scriptLanded = Date.now();
2923
+ if (pending.delete(id) && pending.size === 0)
2924
+ emptied();
2925
+ };
2926
+ const landed = () => pending.size === 0 ? Promise.resolve() : new Promise((resolve5) => {
2927
+ emptied = resolve5;
2928
+ });
2800
2929
  const unlisten = [
2801
2930
  page.on("Network.responseReceived", (params) => {
2802
2931
  if (documentStatus !== null || params?.type !== "Document")
2803
2932
  return;
2804
2933
  const status = params?.response?.status;
2805
- if (typeof status === "number")
2934
+ if (isNumber(status))
2806
2935
  documentStatus = status;
2807
2936
  }),
2937
+ page.on("Network.requestWillBeSent", (params) => {
2938
+ const id = params?.requestId;
2939
+ if (!DRAWN_WITH.has(params?.type) || !isString(id))
2940
+ return;
2941
+ if (!pending.has(id)) {
2942
+ asked += 1;
2943
+ if (loadFired && params?.type === "Script")
2944
+ afterLoadScripts.add(id);
2945
+ }
2946
+ pending.add(id);
2947
+ }),
2948
+ page.on("Network.loadingFinished", (params) => {
2949
+ if (isString(params?.requestId))
2950
+ received(params.requestId);
2951
+ }),
2952
+ page.on("Network.loadingFailed", (params) => {
2953
+ if (isString(params?.requestId))
2954
+ received(params.requestId);
2955
+ }),
2808
2956
  page.on("Runtime.exceptionThrown", (params) => remember(params?.exceptionDetails?.exception?.description ?? params?.exceptionDetails?.text)),
2809
2957
  page.on("Runtime.consoleAPICalled", (params) => {
2810
2958
  if (params?.type !== "error")
@@ -2831,10 +2979,13 @@ async function render(page, input) {
2831
2979
  const load = new Promise((resolve5) => {
2832
2980
  loaded = resolve5;
2833
2981
  });
2834
- const stopLoad = page.on("Page.loadEventFired", () => loaded());
2982
+ const stopLoad = page.on("Page.loadEventFired", () => {
2983
+ loadFired = true;
2984
+ loaded();
2985
+ });
2835
2986
  unlisten.push(stopLoad);
2836
2987
  const navigation = await page.send("Page.navigate", { url: input.url });
2837
- if (typeof navigation.errorText === "string" && navigation.errorText !== "") {
2988
+ if (isString(navigation.errorText) && navigation.errorText !== "") {
2838
2989
  throw new Error(`The page did not load: ${navigation.errorText}`);
2839
2990
  }
2840
2991
  await bounded(load, input.timeoutMs ?? 15e3, void 0);
@@ -2842,12 +2993,28 @@ async function render(page, input) {
2842
2993
  if (documentStatus !== null && documentStatus >= 500) {
2843
2994
  throw new Error(`The page did not load: the app answered HTTP ${documentStatus}.`);
2844
2995
  }
2845
- await bounded(page.send("Runtime.evaluate", {
2846
- expression: "document.fonts ? document.fonts.ready.then(() => true) : true",
2847
- awaitPromise: true,
2848
- returnByValue: true
2849
- }), 2e3, void 0).catch(() => {
2850
- });
2996
+ const until = Date.now() + 2e3;
2997
+ let waitedForScript = null;
2998
+ while (Date.now() < until) {
2999
+ await bounded(landed(), until - Date.now(), void 0);
3000
+ const before = asked;
3001
+ const answered = await bounded(page.send("Runtime.evaluate", {
3002
+ expression: LAID_OUT,
3003
+ awaitPromise: true,
3004
+ returnByValue: true
3005
+ }), Math.max(0, until - Date.now()), void 0).then(() => true, () => false);
3006
+ if (!answered)
3007
+ break;
3008
+ if (pending.size !== 0 || asked !== before)
3009
+ continue;
3010
+ const latestScript = scriptLanded;
3011
+ if (latestScript === null || latestScript === waitedForScript)
3012
+ break;
3013
+ const remaining = Math.min(latestScript + 300, until) - Date.now();
3014
+ if (remaining > 0)
3015
+ await new Promise((resolve5) => setTimeout(resolve5, remaining));
3016
+ waitedForScript = latestScript;
3017
+ }
2851
3018
  await bounded(page.send("Runtime.evaluate", {
2852
3019
  expression: `(async () => {
2853
3020
  // The page stops itself before the caller's deadline can. Without
@@ -3071,13 +3238,15 @@ async function moveReference(cwd, id, destinationDir, index) {
3071
3238
  };
3072
3239
  }
3073
3240
  function focusOf(note) {
3074
- return {
3241
+ const focus = {
3075
3242
  selector: note.anchor.selector,
3076
3243
  text: note.anchor.text,
3077
3244
  tag: note.anchor.tag,
3078
- ...note.anchor.region === void 0 ? {} : { region: note.anchor.region },
3079
3245
  rect: note.anchor.rect
3080
3246
  };
3247
+ if (note.anchor.region !== void 0)
3248
+ focus.region = note.anchor.region;
3249
+ return focus;
3081
3250
  }
3082
3251
  async function attachRequest(cwd, requestId, input, deps) {
3083
3252
  const capture = deps.capture ?? capturePage;
@@ -3104,8 +3273,8 @@ async function attachRequest(cwd, requestId, input, deps) {
3104
3273
  finishDeadline();
3105
3274
  }, deadlineMs);
3106
3275
  timer.unref?.();
3107
- for (let index = 0; index < input.references.length; index += 1) {
3108
- const reference = await moveReference(cwd, input.references[index], destination, index + 1).catch(() => null);
3276
+ for (const [index, referenceId] of input.references.entries()) {
3277
+ const reference = await moveReference(cwd, referenceId, destination, index + 1).catch(() => null);
3109
3278
  if (reference !== null)
3110
3279
  references.push(reference);
3111
3280
  }
@@ -3286,7 +3455,7 @@ async function freePort() {
3286
3455
  probe2.once("error", reject);
3287
3456
  probe2.listen(0, "127.0.0.1", () => {
3288
3457
  const address = probe2.address();
3289
- const port = typeof address === "object" && address !== null ? address.port : 0;
3458
+ const port = address !== null && !isString(address) ? address.port : 0;
3290
3459
  probe2.close(() => resolve5(port));
3291
3460
  });
3292
3461
  });
@@ -3519,9 +3688,9 @@ function createBranchRegistry(options) {
3519
3688
  });
3520
3689
  throw error;
3521
3690
  }
3522
- }).catch((error) => transition(title, {
3691
+ }).catch((cause) => transition(title, {
3523
3692
  status: "failed",
3524
- reason: error instanceof Error ? error.message : String(error)
3693
+ reason: cause instanceof Error ? cause.message : String(cause)
3525
3694
  })).finally(() => {
3526
3695
  inflight.delete(title);
3527
3696
  });
@@ -3537,8 +3706,8 @@ function createBranchRegistry(options) {
3537
3706
  return stopPromise;
3538
3707
  closed = true;
3539
3708
  clearInterval(sweepTimer);
3540
- stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
3541
- await Promise.allSettled([...stopping.values()]);
3709
+ stopPromise = Promise.allSettled(inflight.values()).then(async () => {
3710
+ await Promise.allSettled(stopping.values());
3542
3711
  const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
3543
3712
  await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
3544
3713
  });
@@ -3547,6 +3716,10 @@ function createBranchRegistry(options) {
3547
3716
  };
3548
3717
  }
3549
3718
 
3719
+ // ../server/dist/agents/silence.js
3720
+ var QUIET_NOTICE_MS = 3 * 6e4;
3721
+ var SILENCE_CEILING_MS = 30 * 6e4;
3722
+
3550
3723
  // ../server/dist/agents/failure.js
3551
3724
  var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
3552
3725
  var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
@@ -3600,6 +3773,8 @@ function message(code, input) {
3600
3773
  return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
3601
3774
  case "not-registered":
3602
3775
  return `${agent} finished without registering the new direction, so nothing reached the rail. Its last output is in the Leglas terminal.`;
3776
+ case "agent-silent":
3777
+ return `${agent} sent nothing for ${SILENCE_CEILING_MS / 6e4} minutes, so Leglas stopped it. It may have been waiting on a question nothing here can answer. Its last output is in the Leglas terminal.`;
3603
3778
  case "agent-error":
3604
3779
  return input.exitCode === null || input.exitCode === void 0 ? `${agent} stopped without finishing. Its last output is in the Leglas terminal.` : `${agent} exited with code ${input.exitCode}. Its last output is in the Leglas terminal.`;
3605
3780
  }
@@ -3607,10 +3782,10 @@ function message(code, input) {
3607
3782
  function classifyFailure(input) {
3608
3783
  const lines2 = input.lines ?? [];
3609
3784
  const error = input.error ?? null;
3610
- const code = error === "cancelled" ? "cancelled" : error === "not-registered" ? "not-registered" : error !== null && /^stopped by /.test(error) ? "stopped" : error !== null && MISSING_BINARY.test(error) ? "missing-agent" : (error !== null ? fromLines([error]) : null) ?? fromStatus(input.retry?.status ?? null, input.retry?.reason ?? null) ?? fromLines(lines2) ?? "agent-error";
3785
+ const code = error === "cancelled" ? "cancelled" : error === "not-registered" ? "not-registered" : error === "silent" ? "agent-silent" : error !== null && error.startsWith("stopped by ") ? "stopped" : error !== null && MISSING_BINARY.test(error) ? "missing-agent" : (error !== null ? fromLines([error]) : null) ?? fromStatus(input.retry?.status ?? null, input.retry?.reason ?? null) ?? fromLines(lines2) ?? "agent-error";
3611
3786
  return { code, message: message(code, input) };
3612
3787
  }
3613
- function sessionShaped(code) {
3788
+ function conversationFailure(code) {
3614
3789
  return code === "agent-error";
3615
3790
  }
3616
3791
 
@@ -3626,43 +3801,38 @@ var TAG_CAP = 40;
3626
3801
  var CLASS_CAP = 8;
3627
3802
  var CLASS_LENGTH_CAP = 60;
3628
3803
  var COVERS_CAP = 8;
3629
- function isRecord2(value) {
3630
- return typeof value === "object" && value !== null && !Array.isArray(value);
3631
- }
3632
3804
  function text(value, cap) {
3633
- return typeof value === "string" ? value.trim().slice(0, cap) : "";
3805
+ return isString(value) ? value.trim().slice(0, cap) : "";
3634
3806
  }
3635
3807
  function fraction(value) {
3636
- if (typeof value !== "number" || !Number.isFinite(value))
3808
+ if (!isNumber(value) || !Number.isFinite(value))
3637
3809
  return 0.5;
3638
3810
  return Math.min(1, Math.max(0, value));
3639
3811
  }
3640
3812
  function size(value) {
3641
- return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
3813
+ return isNumber(value) && Number.isFinite(value) ? Math.round(value) : 0;
3642
3814
  }
3643
3815
  function anchorFrom(value) {
3644
- if (!isRecord2(value))
3816
+ if (!isJsonRecord(value))
3645
3817
  return null;
3646
3818
  const selector = text(value["selector"], SELECTOR_CAP);
3647
3819
  if (selector === "")
3648
3820
  return null;
3649
- const rect = isRecord2(value["rect"]) ? value["rect"] : {};
3650
- const classes = Array.isArray(value["classes"]) ? value["classes"].filter((entry) => typeof entry === "string").slice(0, CLASS_CAP).map((entry) => entry.slice(0, CLASS_LENGTH_CAP)) : [];
3651
- const rawRegion = isRecord2(value["region"]) ? value["region"] : null;
3821
+ const rect = isJsonRecord(value["rect"]) ? value["rect"] : {};
3822
+ const classes = Array.isArray(value["classes"]) ? value["classes"].filter((entry) => isString(entry)).slice(0, CLASS_CAP).map((entry) => entry.slice(0, CLASS_LENGTH_CAP)) : [];
3823
+ const rawRegion = isJsonRecord(value["region"]) ? value["region"] : null;
3652
3824
  const region = rawRegion === null ? null : {
3653
3825
  height: fraction(rawRegion["height"]),
3654
3826
  width: fraction(rawRegion["width"]),
3655
3827
  x: fraction(rawRegion["x"]),
3656
3828
  y: fraction(rawRegion["y"])
3657
3829
  };
3658
- const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
3830
+ const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isJsonRecord).slice(0, COVERS_CAP).map((entry) => ({
3659
3831
  tag: text(entry["tag"], TAG_CAP) || "element",
3660
3832
  text: text(entry["text"], TEXT_CAP)
3661
3833
  })) : [];
3662
- return {
3834
+ const anchor = {
3663
3835
  classes,
3664
- ...covers.length === 0 ? {} : { covers },
3665
- ...region === null ? {} : { region },
3666
3836
  rect: {
3667
3837
  height: size(rect["height"]),
3668
3838
  width: size(rect["width"]),
@@ -3671,22 +3841,27 @@ function anchorFrom(value) {
3671
3841
  },
3672
3842
  selector,
3673
3843
  spot: {
3674
- x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
3675
- y: fraction(isRecord2(value["spot"]) ? value["spot"]["y"] : void 0)
3844
+ x: fraction(isJsonRecord(value["spot"]) ? value["spot"]["x"] : void 0),
3845
+ y: fraction(isJsonRecord(value["spot"]) ? value["spot"]["y"] : void 0)
3676
3846
  },
3677
3847
  tag: text(value["tag"], TAG_CAP) || "element",
3678
3848
  text: text(value["text"], TEXT_CAP),
3679
3849
  viewport: size(value["viewport"])
3680
3850
  };
3851
+ if (covers.length > 0)
3852
+ anchor.covers = covers;
3853
+ if (region !== null)
3854
+ anchor.region = region;
3855
+ return anchor;
3681
3856
  }
3682
3857
  async function readAnnotations(cwd) {
3683
3858
  try {
3684
3859
  const raw = await readFile6(join7(cwd, ANNOTATIONS_PATH), "utf8");
3685
- const parsed = JSON.parse(raw);
3686
- if (!Array.isArray(parsed.annotations))
3860
+ const parsed = parseJson(raw);
3861
+ if (!isJsonRecord(parsed) || !Array.isArray(parsed.annotations))
3687
3862
  return [];
3688
3863
  return parsed.annotations.flatMap((entry, index) => {
3689
- if (!isRecord2(entry))
3864
+ if (!isJsonRecord(entry))
3690
3865
  return [];
3691
3866
  const anchor = anchorFrom(entry["anchor"]);
3692
3867
  const title = text(entry["title"], TAG_CAP * 4);
@@ -3695,7 +3870,7 @@ async function readAnnotations(cwd) {
3695
3870
  return [
3696
3871
  {
3697
3872
  anchor,
3698
- id: typeof entry["id"] === "string" ? entry["id"] : String(index),
3873
+ id: isString(entry["id"]) ? entry["id"] : String(index),
3699
3874
  note: text(entry["note"], NOTE_CAP),
3700
3875
  title
3701
3876
  }
@@ -3811,7 +3986,7 @@ function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx
3811
3986
  const target = preview.file ?? targetFor(preview.url);
3812
3987
  const cleaned = intent.trim();
3813
3988
  const asked = changeBlock(cleaned, notes);
3814
- const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
3989
+ const recorded = cleaned === "" ? notes.flatMap((entry) => entry.note !== "" ? [entry.note] : []).join("; ") : cleaned;
3815
3990
  const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand, captured) : replacePrompt(preview, asked, target, leglasCommand, captured);
3816
3991
  return { prompt, target, mode };
3817
3992
  }
@@ -3934,52 +4109,62 @@ var TERMINAL = ["failed", "cancelled"];
3934
4109
  function isTerminal(status) {
3935
4110
  return TERMINAL.includes(status);
3936
4111
  }
3937
- var FAILURE_CODES = [
3938
- "cancelled",
3939
- "stopped",
3940
- "missing-agent",
3941
- "not-signed-in",
3942
- "provider-overloaded",
3943
- "provider-limit",
3944
- "needs-trust",
3945
- "not-registered",
3946
- "agent-error"
3947
- ];
3948
- function failureOf(value) {
3949
- if (typeof value !== "object" || value === null)
4112
+ var FAILURE_CODES = {
4113
+ cancelled: true,
4114
+ stopped: true,
4115
+ "missing-agent": true,
4116
+ "not-signed-in": true,
4117
+ "provider-overloaded": true,
4118
+ "provider-limit": true,
4119
+ "needs-trust": true,
4120
+ "not-registered": true,
4121
+ "agent-error": true,
4122
+ "agent-silent": true
4123
+ };
4124
+ function isFailureCode(value) {
4125
+ return isString(value) && Object.hasOwn(FAILURE_CODES, value);
4126
+ }
4127
+ function failureOf(entry) {
4128
+ if (!isJsonRecord(entry))
3950
4129
  return null;
3951
- const entry = value;
3952
- if (typeof entry.message !== "string" || entry.message === "")
4130
+ if (!isString(entry.message) || entry.message === "")
3953
4131
  return null;
3954
- if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
4132
+ if (!isFailureCode(entry.code))
3955
4133
  return null;
3956
4134
  return { code: entry.code, message: entry.message };
3957
4135
  }
3958
4136
  async function readRequests(cwd) {
3959
4137
  try {
3960
4138
  const raw = await readFile7(join8(cwd, REQUESTS_PATH), "utf8");
3961
- const parsed = JSON.parse(raw);
3962
- if (!Array.isArray(parsed.requests))
4139
+ const parsed = parseJson(raw);
4140
+ if (!isJsonRecord(parsed) || !Array.isArray(parsed.requests))
3963
4141
  return [];
3964
4142
  return parsed.requests.map((request, index) => {
3965
- const source = typeof request === "object" && request !== null ? request : {};
4143
+ const source = isJsonRecord(request) ? request : Array.isArray(request) ? Object.fromEntries(Object.entries(request)) : {};
3966
4144
  const { failure: rawFailure, attachments: rawAttachments, captureNote: rawCaptureNote, compare: rawCompare, references: rawReferences, ...entry } = source;
3967
4145
  const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
3968
4146
  const failure = isTerminal(status) ? failureOf(rawFailure) : null;
3969
- const id = typeof entry.id === "string" && REQUEST_ID.test(entry.id) ? entry.id : String(index);
4147
+ const id = isString(entry.id) && REQUEST_ID.test(entry.id) ? entry.id : String(index);
3970
4148
  const ownFile = new RegExp(`^\\.leglas/captures/${id}/[A-Za-z0-9][A-Za-z0-9_.-]*$`);
3971
- const attachments = Array.isArray(rawAttachments) ? rawAttachments.filter((attachment) => typeof attachment === "object" && attachment !== null && !Array.isArray(attachment) && typeof attachment.file === "string" && ownFile.test(attachment.file) && !attachment.file.includes("..") && ["frame", "note", "compare", "reference"].includes(String(attachment.kind))) : null;
3972
- return {
4149
+ const attachments = Array.isArray(rawAttachments) ? rawAttachments.filter((attachment) => isJsonRecord(attachment) && isString(attachment.file) && ownFile.test(attachment.file) && !attachment.file.includes("..") && ["frame", "note", "compare", "reference"].includes(String(attachment.kind))) : null;
4150
+ const loaded = {
3973
4151
  ...entry,
3974
4152
  id,
3975
4153
  status,
3976
- mode: entry.mode === "variant" ? "variant" : "replace",
3977
- ...failure === null ? {} : { failure },
3978
- ...attachments === null || attachments.length === 0 ? {} : { attachments },
3979
- ...typeof rawCaptureNote === "string" ? { captureNote: rawCaptureNote } : {},
3980
- ...typeof rawCompare === "string" ? { compare: rawCompare } : {},
3981
- ...Array.isArray(rawReferences) && rawReferences.every((id2) => typeof id2 === "string") ? { references: rawReferences } : {}
4154
+ mode: entry.mode === "variant" ? "variant" : "replace"
3982
4155
  };
4156
+ const optional = {};
4157
+ if (failure !== null)
4158
+ optional.failure = failure;
4159
+ if (attachments !== null && attachments.length > 0)
4160
+ optional.attachments = attachments;
4161
+ if (isString(rawCaptureNote))
4162
+ optional.captureNote = rawCaptureNote;
4163
+ if (isString(rawCompare))
4164
+ optional.compare = rawCompare;
4165
+ if (Array.isArray(rawReferences) && rawReferences.every(isString))
4166
+ optional.references = rawReferences;
4167
+ return { ...loaded, ...optional };
3983
4168
  });
3984
4169
  } catch {
3985
4170
  return [];
@@ -4047,7 +4232,7 @@ async function clearRequests(cwd) {
4047
4232
  // ../server/dist/agents/runner.js
4048
4233
  import { spawn as nodeSpawn3 } from "child_process";
4049
4234
  import { readFile as readFile9 } from "fs/promises";
4050
- import { join as join9, resolve as resolve2, sep as sep2 } from "path";
4235
+ import { join as join9, resolve as resolve2 } from "path";
4051
4236
 
4052
4237
  // ../server/dist/agents/claude-agent-session.js
4053
4238
  import { EventEmitter } from "events";
@@ -4071,7 +4256,9 @@ function imageMediaType(path) {
4071
4256
  async function claudeContent(prompt, images) {
4072
4257
  if (images.length === 0)
4073
4258
  return prompt;
4074
- const blocks = [{ type: "text", text: prompt }];
4259
+ const blocks = [
4260
+ { type: "text", text: prompt }
4261
+ ];
4075
4262
  for (const path of images) {
4076
4263
  const mediaType = imageMediaType(path);
4077
4264
  if (mediaType === null)
@@ -4103,9 +4290,9 @@ function waitForAbort(work, signal) {
4103
4290
  void work.then((value) => {
4104
4291
  signal.removeEventListener("abort", onAbort);
4105
4292
  resolve5(value);
4106
- }, (error) => {
4293
+ }, (cause) => {
4107
4294
  signal.removeEventListener("abort", onAbort);
4108
- reject(error);
4295
+ reject(cause);
4109
4296
  });
4110
4297
  });
4111
4298
  }
@@ -4238,27 +4425,28 @@ var PersistentClaudeSession = class {
4238
4425
  this.warmedFor = sessionId;
4239
4426
  const controller = new AbortController();
4240
4427
  this.processAbort = controller;
4241
- const warming = this.startup({
4242
- options: {
4243
- abortController: controller,
4244
- cwd: this.cwd,
4245
- env: agentEnvironment(),
4246
- permissionMode: "acceptEdits",
4247
- settingSources: ["user", "project", "local"],
4248
- persistSession: true,
4249
- ...this.allowedCommands.length === 0 ? {} : { allowedTools: this.allowedCommands.map((command) => `Bash(${command} *)`) },
4250
- ...sessionId === null ? {} : { resume: sessionId }
4251
- },
4252
- initializeTimeoutMs: INITIALIZE_TIMEOUT_MS
4253
- }).then((warmQuery) => {
4428
+ const options = {
4429
+ abortController: controller,
4430
+ cwd: this.cwd,
4431
+ env: agentEnvironment(),
4432
+ permissionMode: "acceptEdits",
4433
+ settingSources: ["user", "project", "local"],
4434
+ persistSession: true
4435
+ };
4436
+ if (this.allowedCommands.length > 0) {
4437
+ options.allowedTools = this.allowedCommands.map((command) => `Bash(${command} *)`);
4438
+ }
4439
+ if (sessionId !== null)
4440
+ options.resume = sessionId;
4441
+ const warming = this.startup({ options, initializeTimeoutMs: INITIALIZE_TIMEOUT_MS }).then((warmQuery) => {
4254
4442
  if (this.closed || controller.signal.aborted || this.processAbort !== controller)
4255
4443
  warmQuery.close();
4256
4444
  else
4257
4445
  this.warmQuery = warmQuery;
4258
- }).catch((error) => {
4446
+ }).catch((cause) => {
4259
4447
  if (this.processAbort === controller)
4260
4448
  this.processAbort = null;
4261
- throw error;
4449
+ throw cause;
4262
4450
  }).finally(() => {
4263
4451
  if (this.warming === warming)
4264
4452
  this.warming = null;
@@ -4365,7 +4553,7 @@ var PersistentClaudeSession = class {
4365
4553
  let endedNormally = false;
4366
4554
  try {
4367
4555
  for await (const message2 of query) {
4368
- if (typeof message2.session_id === "string" && message2.session_id !== "") {
4556
+ if (isString(message2.session_id) && message2.session_id !== "") {
4369
4557
  this.loadedSessionId = message2.session_id;
4370
4558
  }
4371
4559
  const child = this.active;
@@ -4466,10 +4654,10 @@ import { EventEmitter as EventEmitter2 } from "events";
4466
4654
  import { PassThrough as PassThrough2 } from "stream";
4467
4655
  var REQUEST_TIMEOUT_MS = 3e4;
4468
4656
  function record2(value) {
4469
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
4657
+ return isJsonRecord(value) ? value : null;
4470
4658
  }
4471
4659
  function string(value) {
4472
- return typeof value === "string" && value !== "" ? value : null;
4660
+ return isString(value) && value !== "" ? value : null;
4473
4661
  }
4474
4662
  var CodexTurnChild = class {
4475
4663
  interrupt;
@@ -4529,9 +4717,9 @@ var PersistentCodexAppServer = class {
4529
4717
  stderr = [];
4530
4718
  closed = false;
4531
4719
  resetting = null;
4532
- constructor(cwd, spawn4, requestTimeoutMs) {
4720
+ constructor(cwd, spawn5, requestTimeoutMs) {
4533
4721
  this.cwd = cwd;
4534
- this.spawn = spawn4;
4722
+ this.spawn = spawn5;
4535
4723
  this.requestTimeoutMs = requestTimeoutMs;
4536
4724
  }
4537
4725
  /**
@@ -4585,9 +4773,9 @@ var PersistentCodexAppServer = class {
4585
4773
  capabilities: null
4586
4774
  }).then(() => {
4587
4775
  this.notify("initialized", {});
4588
- }).catch(async (error) => {
4589
- await this.resetProcess(process2, error instanceof Error ? error : new Error(String(error)));
4590
- throw error;
4776
+ }).catch(async (cause) => {
4777
+ await this.resetProcess(process2, cause instanceof Error ? cause : new Error(String(cause)));
4778
+ throw cause;
4591
4779
  });
4592
4780
  return this.ready;
4593
4781
  }
@@ -4619,7 +4807,7 @@ var PersistentCodexAppServer = class {
4619
4807
  this.active = { child, threadId, turnId: null };
4620
4808
  child.line({ type: "thread.started", thread_id: threadId });
4621
4809
  turnSubmitted = true;
4622
- const response = record2(await this.requestRaw("turn/start", {
4810
+ const params = {
4623
4811
  threadId,
4624
4812
  input: [
4625
4813
  { type: "text", text: input.prompt },
@@ -4633,9 +4821,11 @@ var PersistentCodexAppServer = class {
4633
4821
  networkAccess: true,
4634
4822
  excludeTmpdirEnvVar: false,
4635
4823
  excludeSlashTmp: false
4636
- },
4637
- ...input.effort === null ? {} : { effort: input.effort }
4638
- }));
4824
+ }
4825
+ };
4826
+ if (input.effort !== null)
4827
+ params.effort = input.effort;
4828
+ const response = record2(await this.requestRaw("turn/start", params));
4639
4829
  const turn = record2(response?.turn);
4640
4830
  const turnId = string(turn?.id);
4641
4831
  if (turnId === null)
@@ -4730,13 +4920,13 @@ var PersistentCodexAppServer = class {
4730
4920
  return;
4731
4921
  let message2;
4732
4922
  try {
4733
- message2 = record2(JSON.parse(line));
4923
+ message2 = record2(parseJson(line));
4734
4924
  } catch {
4735
4925
  message2 = null;
4736
4926
  }
4737
4927
  if (message2 === null)
4738
4928
  return;
4739
- const id = typeof message2.id === "number" ? message2.id : null;
4929
+ const id = isNumber(message2.id) ? message2.id : null;
4740
4930
  const method = string(message2.method);
4741
4931
  if (id !== null && method === null) {
4742
4932
  const pending = this.pending.get(id);
@@ -4805,7 +4995,7 @@ var PersistentCodexAppServer = class {
4805
4995
  }
4806
4996
  if (item.type === "reasoning")
4807
4997
  return { type: "reasoning" };
4808
- return { type: typeof item.type === "string" ? item.type : "unknown" };
4998
+ return { type: isString(item.type) ? item.type : "unknown" };
4809
4999
  }
4810
5000
  answerServerRequest(id, method) {
4811
5001
  if (method === "item/commandExecution/requestApproval") {
@@ -4928,8 +5118,39 @@ var defaultSpawn = (command, args, options) => nodeSpawn2(command, args, {
4928
5118
  ...options,
4929
5119
  env: agentEnvironment()
4930
5120
  });
4931
- function createCodexAppServer(cwd, spawn4 = defaultSpawn, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
4932
- return new PersistentCodexAppServer(cwd, spawn4, requestTimeoutMs);
5121
+ function createCodexAppServer(cwd, spawn5 = defaultSpawn, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
5122
+ return new PersistentCodexAppServer(cwd, spawn5, requestTimeoutMs);
5123
+ }
5124
+
5125
+ // ../server/dist/agents/process-tree.js
5126
+ import { spawn as spawn3 } from "child_process";
5127
+ function ownGroup(platform = process.platform) {
5128
+ return { detached: platform !== "win32" };
5129
+ }
5130
+ var system = {
5131
+ platform: process.platform,
5132
+ kill: (pid, signal) => process.kill(pid, signal),
5133
+ taskkill: (pid) => spawn3("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", shell: false })
5134
+ };
5135
+ function signalTree(child, signal, deps = system) {
5136
+ const { pid } = child;
5137
+ if (pid === void 0) {
5138
+ child.kill(signal);
5139
+ return;
5140
+ }
5141
+ if (deps.platform === "win32") {
5142
+ try {
5143
+ deps.taskkill(pid).once("error", () => child.kill(signal));
5144
+ } catch {
5145
+ child.kill(signal);
5146
+ }
5147
+ return;
5148
+ }
5149
+ try {
5150
+ deps.kill(-pid, signal);
5151
+ } catch {
5152
+ child.kill(signal);
5153
+ }
4933
5154
  }
4934
5155
 
4935
5156
  // ../server/dist/agents/runner.js
@@ -4937,6 +5158,9 @@ var POLL_MS = 2e3;
4937
5158
  var OUTPUT_LINES = 20;
4938
5159
  var IDLE_RELEASE_MS = 5 * 6e4;
4939
5160
  var CANCEL_GRACE_MS = 5e3;
5161
+ function isStateUpdater(value) {
5162
+ return typeof value === "function";
5163
+ }
4940
5164
  var SESSION_TURNS_CAP = 8;
4941
5165
  function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images = []) {
4942
5166
  if (choice.agent === null)
@@ -5007,7 +5231,7 @@ function defaultSpawn2(command, args, options) {
5007
5231
  return nodeSpawn3(command, args, { ...options, env: agentEnvironment() });
5008
5232
  }
5009
5233
  function startRunner(options) {
5010
- const spawn4 = options.spawn ?? defaultSpawn2;
5234
+ const spawn5 = options.spawn ?? defaultSpawn2;
5011
5235
  const codexAppServer = options.codexAppServer === void 0 ? options.spawn === void 0 ? createCodexAppServer(options.cwd) : null : options.codexAppServer;
5012
5236
  const claudeAgentSession = options.claudeAgentSession === void 0 ? options.spawn === void 0 ? createClaudeAgentSession(options.cwd, options.leglasCommand === void 0 ? [] : [`${options.leglasCommand} show`, registrationCommand(options.leglasCommand)]) : null : options.claudeAgentSession;
5013
5237
  const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
@@ -5016,6 +5240,7 @@ function startRunner(options) {
5016
5240
  const setLater = options.setTimeout ?? ((callback, milliseconds) => {
5017
5241
  setTimeout(callback, milliseconds).unref?.();
5018
5242
  });
5243
+ const now = options.now ?? (() => Date.now());
5019
5244
  let state = {
5020
5245
  running: false,
5021
5246
  requestId: null,
@@ -5023,10 +5248,11 @@ function startRunner(options) {
5023
5248
  activity: null,
5024
5249
  startedAt: null,
5025
5250
  stopping: false,
5026
- waiting: null
5251
+ waiting: null,
5252
+ quietSince: null
5027
5253
  };
5028
5254
  const setState = (next) => {
5029
- state = typeof next === "function" ? next(state) : next;
5255
+ state = isStateUpdater(next) ? next(state) : next;
5030
5256
  options.onChange?.();
5031
5257
  };
5032
5258
  let stopped = false;
@@ -5087,7 +5313,8 @@ function startRunner(options) {
5087
5313
  activity: null,
5088
5314
  startedAt: null,
5089
5315
  stopping: false,
5090
- waiting: null
5316
+ waiting: null,
5317
+ quietSince: null
5091
5318
  });
5092
5319
  };
5093
5320
  const rememberLine = (lines2, line) => {
@@ -5116,11 +5343,21 @@ function startRunner(options) {
5116
5343
  cancelled: false,
5117
5344
  controller,
5118
5345
  abandon: () => {
5119
- }
5346
+ },
5347
+ heardAt: now(),
5348
+ silenced: false,
5349
+ spawned: false
5120
5350
  };
5121
5351
  active = current;
5122
5352
  activeAgent = resolved.agent;
5123
5353
  const cancelled = () => ({ ok: false, error: "cancelled" });
5354
+ const silent = () => ({ ok: false, error: "silent" });
5355
+ const heard = () => {
5356
+ current.heardAt = now();
5357
+ if (active === current && state.quietSince !== null) {
5358
+ setState((value) => ({ ...value, quietSince: null }));
5359
+ }
5360
+ };
5124
5361
  const startPersistent = async () => {
5125
5362
  if (persistent === null)
5126
5363
  throw new Error("No persistent transport.");
@@ -5145,27 +5382,31 @@ function startRunner(options) {
5145
5382
  return;
5146
5383
  }
5147
5384
  resolve5(child);
5148
- }, (error) => reject(error));
5385
+ }, (cause) => reject(cause));
5149
5386
  });
5150
5387
  };
5388
+ const spawnAgent2 = () => {
5389
+ const child = spawn5(resolved.command, resolved.args, {
5390
+ cwd: options.cwd,
5391
+ shell: false,
5392
+ stdio: ["ignore", "pipe", "pipe"],
5393
+ ...ownGroup()
5394
+ });
5395
+ current.spawned = true;
5396
+ return child;
5397
+ };
5151
5398
  try {
5152
5399
  let child;
5153
5400
  try {
5154
- child = persistent !== null ? await startPersistent() : spawn4(resolved.command, resolved.args, {
5155
- cwd: options.cwd,
5156
- shell: false,
5157
- stdio: ["ignore", "pipe", "pipe"]
5158
- });
5401
+ child = persistent !== null ? await startPersistent() : spawnAgent2();
5159
5402
  } catch (error) {
5403
+ if (current.silenced)
5404
+ return silent();
5160
5405
  if (current.cancelled || stopped || controller.signal.aborted)
5161
5406
  return cancelled();
5162
5407
  if (persistent !== null) {
5163
5408
  try {
5164
- child = spawn4(resolved.command, resolved.args, {
5165
- cwd: options.cwd,
5166
- shell: false,
5167
- stdio: ["ignore", "pipe", "pipe"]
5168
- });
5409
+ child = spawnAgent2();
5169
5410
  } catch (fallbackError) {
5170
5411
  return {
5171
5412
  ok: false,
@@ -5182,12 +5423,13 @@ function startRunner(options) {
5182
5423
  current.child = child;
5183
5424
  if (current.cancelled || stopped || controller.signal.aborted) {
5184
5425
  try {
5185
- child.kill("SIGTERM");
5426
+ signalChild(current, "SIGTERM");
5186
5427
  } catch {
5187
5428
  }
5188
- return cancelled();
5429
+ return current.silenced ? silent() : cancelled();
5189
5430
  }
5190
5431
  const stdoutFlush = lineReader(child.stdout, (line) => {
5432
+ heard();
5191
5433
  rememberLine(lines2, line);
5192
5434
  const sessionId = sessionFrom(resolved.agent, line);
5193
5435
  if (sessionId !== null)
@@ -5207,7 +5449,10 @@ function startRunner(options) {
5207
5449
  }
5208
5450
  }
5209
5451
  });
5210
- const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
5452
+ const stderrFlush = lineReader(child.stderr, (line) => {
5453
+ heard();
5454
+ rememberLine(lines2, line);
5455
+ });
5211
5456
  return await new Promise((resolve5) => {
5212
5457
  let settled = false;
5213
5458
  const settle = (outcome) => {
@@ -5218,11 +5463,19 @@ function startRunner(options) {
5218
5463
  stderrFlush();
5219
5464
  resolve5(outcome);
5220
5465
  };
5221
- current.abandon = () => settle(cancelled());
5222
- child.once("error", (error) => settle({ ok: false, error: error.message }));
5466
+ current.abandon = () => settle(current.silenced ? silent() : cancelled());
5467
+ child.once("error", (error) => {
5468
+ if (current.cancelled)
5469
+ return settle(cancelled());
5470
+ if (current.silenced)
5471
+ return settle(silent());
5472
+ settle({ ok: false, error: error.message });
5473
+ });
5223
5474
  child.once("close", (code, signal) => {
5224
5475
  if (current.cancelled)
5225
5476
  return settle(cancelled());
5477
+ if (current.silenced)
5478
+ return settle(silent());
5226
5479
  if (signal !== null)
5227
5480
  return settle({ ok: false, error: `stopped by ${signal}` });
5228
5481
  settle({ ok: true, code: code ?? 0 });
@@ -5258,7 +5511,8 @@ function startRunner(options) {
5258
5511
  activity: null,
5259
5512
  startedAt: Date.now(),
5260
5513
  stopping: false,
5261
- waiting: null
5514
+ waiting: null,
5515
+ quietSince: null
5262
5516
  });
5263
5517
  const observed = {
5264
5518
  sessionId: null,
@@ -5280,7 +5534,7 @@ function startRunner(options) {
5280
5534
  // real output. Where it has not, "did not edit" means "was not seen
5281
5535
  // to edit", and rerunning on that is how a half-applied change gets
5282
5536
  // applied twice. Such a vendor keeps the failure card and its Retry.
5283
- activityVerified(resolved.agent) && sessionShaped(failure.code) && // Not redundant with the verdict: a stop that lands between the first
5537
+ activityVerified(resolved.agent) && conversationFailure(failure.code) && // Not redundant with the verdict: a stop that lands between the first
5284
5538
  // child settling and the retry starting finds no child to cancel, so
5285
5539
  // nothing says "cancelled". Stopped still means stopped.
5286
5540
  !stopped) {
@@ -5354,31 +5608,61 @@ function startRunner(options) {
5354
5608
  }
5355
5609
  });
5356
5610
  };
5357
- const timer = setEvery(() => schedule(), POLL_MS);
5358
- schedule();
5359
- const cancel = (id) => {
5360
- if (active === null || active.cancelled)
5361
- return false;
5362
- if (id !== void 0 && active.requestId !== id)
5363
- return false;
5364
- const current = active;
5365
- current.cancelled = true;
5366
- failed.add(current.requestId);
5367
- setState((value) => ({ ...value, stopping: true, waiting: null }));
5611
+ const signalChild = (current, value) => {
5612
+ const { child } = current;
5613
+ if (child === null)
5614
+ return;
5615
+ if (current.spawned)
5616
+ signalTree(child, value);
5617
+ else
5618
+ child.kill(value);
5619
+ };
5620
+ const end = (current) => {
5368
5621
  current.controller.abort();
5369
5622
  try {
5370
- current.child?.kill("SIGTERM");
5623
+ signalChild(current, "SIGTERM");
5371
5624
  } catch {
5372
5625
  }
5373
5626
  setLater(() => {
5374
5627
  if (active !== current)
5375
5628
  return;
5376
5629
  try {
5377
- current.child?.kill("SIGKILL");
5630
+ signalChild(current, "SIGKILL");
5378
5631
  } catch {
5379
5632
  }
5380
5633
  current.abandon();
5381
5634
  }, CANCEL_GRACE_MS);
5635
+ };
5636
+ const listen2 = () => {
5637
+ const current = active;
5638
+ if (current === null || current.cancelled || current.silenced)
5639
+ return;
5640
+ const quiet = now() - current.heardAt;
5641
+ if (quiet >= SILENCE_CEILING_MS) {
5642
+ current.silenced = true;
5643
+ setState((value) => ({ ...value, stopping: true, waiting: null, quietSince: null }));
5644
+ end(current);
5645
+ return;
5646
+ }
5647
+ if (quiet >= QUIET_NOTICE_MS && state.quietSince === null) {
5648
+ setState((value) => ({ ...value, quietSince: current.heardAt }));
5649
+ }
5650
+ };
5651
+ const timer = setEvery(() => {
5652
+ listen2();
5653
+ schedule();
5654
+ }, POLL_MS);
5655
+ schedule();
5656
+ const cancel = (id) => {
5657
+ if (active === null || active.cancelled || active.silenced)
5658
+ return false;
5659
+ if (id !== void 0 && active.requestId !== id)
5660
+ return false;
5661
+ const current = active;
5662
+ current.cancelled = true;
5663
+ failed.add(current.requestId);
5664
+ setState((value) => ({ ...value, stopping: true, waiting: null, quietSince: null }));
5665
+ end(current);
5382
5666
  return true;
5383
5667
  };
5384
5668
  const stop = () => {
@@ -5409,7 +5693,7 @@ import { createHash } from "crypto";
5409
5693
  var LIVE_PATH = "/leglas/api/live";
5410
5694
  var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
5411
5695
  function encodeFrame(opcode, payload) {
5412
- const body = typeof payload === "string" ? Buffer.from(payload) : payload;
5696
+ const body = isString(payload) ? Buffer.from(payload) : payload;
5413
5697
  let header;
5414
5698
  if (body.length < 126) {
5415
5699
  header = Buffer.allocUnsafe(2);
@@ -5443,17 +5727,18 @@ function createCoalescer(emit, options = {}) {
5443
5727
  return;
5444
5728
  const waiting = pending.get(change);
5445
5729
  if (waiting !== void 0)
5446
- clearLater(waiting);
5447
- pending.set(change, setLater(() => {
5730
+ waiting();
5731
+ const handle = setLater(() => {
5448
5732
  pending.delete(change);
5449
5733
  if (!closed)
5450
5734
  emit(change);
5451
- }, windowMs));
5735
+ }, windowMs);
5736
+ pending.set(change, () => clearLater(handle));
5452
5737
  },
5453
5738
  close() {
5454
5739
  closed = true;
5455
- for (const handle of pending.values())
5456
- clearLater(handle);
5740
+ for (const cancel of pending.values())
5741
+ cancel();
5457
5742
  pending.clear();
5458
5743
  }
5459
5744
  };
@@ -5537,7 +5822,7 @@ function createLiveHub(options = {}) {
5537
5822
  if (listeners.size === 0)
5538
5823
  return;
5539
5824
  const frame = encodeFrame(1, JSON.stringify({ changed: change }));
5540
- for (const listener of [...listeners]) {
5825
+ for (const listener of Array.from(listeners)) {
5541
5826
  if (listener.socket.destroyed || !listener.socket.writable) {
5542
5827
  drop(listener);
5543
5828
  continue;
@@ -5556,7 +5841,7 @@ function createLiveHub(options = {}) {
5556
5841
  return false;
5557
5842
  const upgrade = req.headers.upgrade;
5558
5843
  const key = req.headers["sec-websocket-key"];
5559
- if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
5844
+ if (!isString(upgrade) || upgrade.toLowerCase() !== "websocket" || !isString(key) || key.trim() === "") {
5560
5845
  socket.destroy();
5561
5846
  return false;
5562
5847
  }
@@ -5591,7 +5876,7 @@ Sec-WebSocket-Accept: ${accept}\r
5591
5876
  return true;
5592
5877
  },
5593
5878
  close: async () => {
5594
- for (const listener of [...listeners]) {
5879
+ for (const listener of Array.from(listeners)) {
5595
5880
  write2(listener, 8, Buffer.alloc(0));
5596
5881
  drop(listener);
5597
5882
  listener.socket.destroy();
@@ -5645,15 +5930,17 @@ async function askLink(resolver, url, entryPath) {
5645
5930
  return false;
5646
5931
  const secure = target.protocol === "https:";
5647
5932
  return new Promise((resolve5) => {
5648
- const request = (secure ? https : http2).request({
5933
+ const requestOptions = {
5649
5934
  host: address,
5650
5935
  port: Number(target.port || (secure ? 443 : 80)),
5651
5936
  path: entryPath,
5652
5937
  method: "GET",
5653
5938
  headers: { host: target.host },
5654
- ...secure ? { servername: target.hostname } : {},
5655
5939
  timeout: PROBE_TIMEOUT_MS2
5656
- }, (response) => {
5940
+ };
5941
+ if (secure)
5942
+ requestOptions.servername = target.hostname;
5943
+ const request = (secure ? https : http2).request(requestOptions, (response) => {
5657
5944
  response.resume();
5658
5945
  const status = response.statusCode ?? 0;
5659
5946
  resolve5(status >= 200 && status < 400);
@@ -5678,12 +5965,14 @@ function withOutput(sentence, output) {
5678
5965
  return `${stem} (${output.slice(0, 160)}).`;
5679
5966
  }
5680
5967
  function startTunnel(options, deps = {}) {
5681
- const spawn4 = deps.spawn ?? spawnChild;
5968
+ const spawn5 = deps.spawn ?? spawnChild;
5682
5969
  const now = deps.now ?? Date.now;
5683
5970
  const urlDeadlineMs = deps.urlDeadlineMs ?? URL_DEADLINE_MS;
5684
5971
  const probeDeadlineMs = deps.probeDeadlineMs ?? PROBE_DEADLINE_MS;
5685
5972
  const resolver = deps.probe === void 0 ? new Resolver() : null;
5686
- const probe2 = deps.probe ?? ((url2) => askLink(resolver, url2, options.entryPath));
5973
+ const probe2 = deps.probe ?? ((url2) => {
5974
+ return askLink(resolver, url2, options.entryPath);
5975
+ });
5687
5976
  const timers = /* @__PURE__ */ new Set();
5688
5977
  const later = (callback, ms) => {
5689
5978
  const timer = setTimeout(() => {
@@ -5724,18 +6013,16 @@ function startTunnel(options, deps = {}) {
5724
6013
  return;
5725
6014
  terminal = true;
5726
6015
  clearTimers();
5727
- report({
5728
- status: "failed",
5729
- provider: options.provider,
5730
- reason,
5731
- ...url === null ? {} : { url }
5732
- });
6016
+ const failure = { status: "failed", provider: options.provider, reason };
6017
+ if (url !== null)
6018
+ failure.url = url;
6019
+ report(failure);
5733
6020
  };
5734
6021
  report(state);
5735
6022
  const args = options.provider === "cloudflared" ? ["tunnel", "--url", `http://127.0.0.1:${options.port}`, "--no-autoupdate"] : ["http", String(options.port), "--log", "stdout", "--log-format", "json"];
5736
6023
  let child;
5737
6024
  try {
5738
- child = spawn4(options.provider, args, {
6025
+ child = spawn5(options.provider, args, {
5739
6026
  env: agentEnvironment(),
5740
6027
  shell: false,
5741
6028
  stdio: ["ignore", "pipe", "pipe"]
@@ -5805,8 +6092,8 @@ function startTunnel(options, deps = {}) {
5805
6092
  if (stream !== "stdout")
5806
6093
  return;
5807
6094
  try {
5808
- const event = JSON.parse(trimmed);
5809
- const candidate = typeof event.url === "string" && event.url.startsWith("https://") ? event.url : null;
6095
+ const event = parseJson(trimmed);
6096
+ const candidate = isJsonRecord(event) && isString(event.url) && event.url.startsWith("https://") ? event.url : null;
5810
6097
  if (candidate !== null)
5811
6098
  beginProbe(candidate);
5812
6099
  } catch {
@@ -6027,19 +6314,16 @@ function isHiddenPath(path) {
6027
6314
  }
6028
6315
  var FILES_PREFIX_PATH = "/leglas/files/";
6029
6316
  var DETECT_TTL_MS = 1e4;
6030
- function isRecord3(value) {
6031
- return typeof value === "object" && value !== null && !Array.isArray(value);
6032
- }
6033
6317
  function stringArray(value) {
6034
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
6318
+ return Array.isArray(value) && value.every((entry) => isString(entry));
6035
6319
  }
6036
6320
  function stringRecord(value) {
6037
- return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
6321
+ return isJsonRecord(value) && Object.values(value).every((entry) => isString(entry));
6038
6322
  }
6039
6323
  function layoutFrom(value) {
6040
- if (!isRecord3(value))
6324
+ if (!isJsonRecord(value))
6041
6325
  return null;
6042
- if (!stringArray(value.order) || !stringRecord(value.renames) || !stringArray(value.collapsedFamilies) || value.compare !== null && typeof value.compare !== "string" || value.viewport !== null && (typeof value.viewport !== "number" || !Number.isFinite(value.viewport))) {
6326
+ if (!stringArray(value.order) || !stringRecord(value.renames) || !stringArray(value.collapsedFamilies) || value.compare !== null && !isString(value.compare) || value.viewport !== null && (!isNumber(value.viewport) || !Number.isFinite(value.viewport))) {
6043
6327
  return null;
6044
6328
  }
6045
6329
  return {
@@ -6051,7 +6335,7 @@ function layoutFrom(value) {
6051
6335
  };
6052
6336
  }
6053
6337
  function manifestFrom(value, previews) {
6054
- if (!isRecord3(value)) {
6338
+ if (!isJsonRecord(value)) {
6055
6339
  return { ok: false, error: "Share details must be a JSON object." };
6056
6340
  }
6057
6341
  const scope2 = value.scope;
@@ -6216,7 +6500,7 @@ function bind(server) {
6216
6500
  const onListening = () => {
6217
6501
  server.removeListener("error", onError);
6218
6502
  const address = server.address();
6219
- resolve5(typeof address === "object" && address !== null ? address.port : 0);
6503
+ resolve5(address !== null && !isString(address) ? address.port : 0);
6220
6504
  };
6221
6505
  server.once("error", onError);
6222
6506
  server.once("listening", onListening);
@@ -6256,10 +6540,7 @@ function createShareManager(options) {
6256
6540
  }
6257
6541
  return detected;
6258
6542
  };
6259
- const status = () => {
6260
- const share = active;
6261
- if (share === null)
6262
- return null;
6543
+ const snapshot = (share) => {
6263
6544
  const tunnelUrl = "url" in share.tunnel ? share.tunnel.url : void 0;
6264
6545
  const origin = tunnelUrl === void 0 ? null : tunnelUrl.replace(/\/$/, "");
6265
6546
  return {
@@ -6268,7 +6549,7 @@ function createShareManager(options) {
6268
6549
  titles: [...share.titles],
6269
6550
  layout: cloneLayout(share.layout),
6270
6551
  sharePort: share.port,
6271
- grants: [...share.grants.values()].toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
6552
+ grants: Array.from(share.grants.values()).toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
6272
6553
  const entryPath = `${ENTRY_PREFIX}${grant.token}`;
6273
6554
  return {
6274
6555
  id: grant.id,
@@ -6287,6 +6568,7 @@ function createShareManager(options) {
6287
6568
  startedAt: share.startedAt
6288
6569
  };
6289
6570
  };
6571
+ const status = () => active === null ? null : snapshot(active);
6290
6572
  const endGrant = (share, grant, why) => {
6291
6573
  if (!share.grants.delete(grant.id))
6292
6574
  return;
@@ -6304,7 +6586,7 @@ function createShareManager(options) {
6304
6586
  held.req.destroy();
6305
6587
  }
6306
6588
  share.grantRequests.delete(grant.id);
6307
- for (const held of [...share.waiting.get(grant.id) ?? []]) {
6589
+ for (const held of Array.from(share.waiting.get(grant.id) ?? [])) {
6308
6590
  if (held.drop())
6309
6591
  refuse(held.req, held.res, why);
6310
6592
  }
@@ -6324,13 +6606,13 @@ function createShareManager(options) {
6324
6606
  const at = now();
6325
6607
  const mono = nowMono();
6326
6608
  let ended = false;
6327
- for (const grant of [...share.grants.values()]) {
6609
+ for (const grant of Array.from(share.grants.values())) {
6328
6610
  if (!expired(grant, at, mono))
6329
6611
  continue;
6330
6612
  endGrant(share, grant, "expiry");
6331
6613
  ended = true;
6332
6614
  }
6333
- const next = [...share.grants.values()].reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
6615
+ const next = Array.from(share.grants.values()).reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
6334
6616
  if (next !== null) {
6335
6617
  share.expiryTimer = setTimeout(sweepExpiry, Math.max(1, next - at));
6336
6618
  share.expiryTimer.unref?.();
@@ -6619,7 +6901,7 @@ function createShareManager(options) {
6619
6901
  if (!parsed.ok)
6620
6902
  return { ok: false, status: 400, error: parsed.error };
6621
6903
  const providers = await tunnels();
6622
- const requested = isRecord3(input) ? input.tunnel : void 0;
6904
+ const requested = isJsonRecord(input) ? input.tunnel : void 0;
6623
6905
  if (requested !== void 0 && requested !== "none" && requested !== "cloudflared" && requested !== "ngrok") {
6624
6906
  return { ok: false, status: 400, error: "That tunnel provider is not supported." };
6625
6907
  }
@@ -6693,7 +6975,7 @@ function createShareManager(options) {
6693
6975
  port,
6694
6976
  // Whichever link exists when the tunnel starts: the probe only
6695
6977
  // needs a path the listener answers, and a share always has one.
6696
- entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
6978
+ entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
6697
6979
  onState: (next) => {
6698
6980
  if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
6699
6981
  return;
@@ -6704,7 +6986,7 @@ function createShareManager(options) {
6704
6986
  });
6705
6987
  share.launch.unref?.();
6706
6988
  }
6707
- return { ok: true, share: status() };
6989
+ return { ok: true, share: snapshot(share) };
6708
6990
  } finally {
6709
6991
  creating = false;
6710
6992
  }
@@ -6713,7 +6995,7 @@ function createShareManager(options) {
6713
6995
  const share = active;
6714
6996
  if (share === null)
6715
6997
  return { ok: false, status: 404, error: "Nothing is being shared." };
6716
- const name = isRecord3(input) && typeof input.name === "string" ? input.name.trim() : "";
6998
+ const name = isJsonRecord(input) && isString(input.name) ? input.name.trim() : "";
6717
6999
  if (name.length > 60) {
6718
7000
  return { ok: false, status: 400, error: "That name is too long for a link." };
6719
7001
  }
@@ -6728,26 +7010,26 @@ function createShareManager(options) {
6728
7010
  mintGrant(share, name);
6729
7011
  sweepExpiry();
6730
7012
  options.live.nudge("share");
6731
- return { ok: true, share: status() };
7013
+ return { ok: true, share: snapshot(share) };
6732
7014
  };
6733
7015
  const revokeGrant = (input) => {
6734
7016
  const share = active;
6735
7017
  if (share === null)
6736
7018
  return { ok: false, status: 404, error: "Nothing is being shared." };
6737
- const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
7019
+ const id = isJsonRecord(input) && isString(input.id) ? input.id : "";
6738
7020
  const grant = share.grants.get(id);
6739
7021
  if (grant === void 0)
6740
7022
  return { ok: false, status: 404, error: "No such link." };
6741
7023
  endGrant(share, grant, "revoke");
6742
7024
  sweepExpiry();
6743
7025
  options.live.nudge("share");
6744
- return { ok: true, share: status() };
7026
+ return { ok: true, share: snapshot(share) };
6745
7027
  };
6746
7028
  const extendGrant = (input) => {
6747
7029
  const share = active;
6748
7030
  if (share === null)
6749
7031
  return { ok: false, status: 404, error: "Nothing is being shared." };
6750
- const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
7032
+ const id = isJsonRecord(input) && isString(input.id) ? input.id : "";
6751
7033
  sweepExpiry();
6752
7034
  const grant = share.grants.get(id);
6753
7035
  if (grant === void 0) {
@@ -6758,13 +7040,13 @@ function createShareManager(options) {
6758
7040
  grant.expiresAtMono = nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n;
6759
7041
  sweepExpiry();
6760
7042
  options.live.nudge("share");
6761
- return { ok: true, share: status() };
7043
+ return { ok: true, share: snapshot(share) };
6762
7044
  };
6763
7045
  const rotate = async () => {
6764
7046
  const share = active;
6765
7047
  if (share === null)
6766
7048
  return { ok: false, status: 404, error: "Nothing is being shared." };
6767
- for (const grant of [...share.grants.values()])
7049
+ for (const grant of Array.from(share.grants.values()))
6768
7050
  endGrant(share, grant, "revoke");
6769
7051
  const provider = "provider" in share.tunnel ? share.tunnel.provider : null;
6770
7052
  await share.runningTunnel?.stop().catch(() => {
@@ -6781,7 +7063,7 @@ function createShareManager(options) {
6781
7063
  share.runningTunnel = runTunnel({
6782
7064
  provider,
6783
7065
  port: share.port,
6784
- entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
7066
+ entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
6785
7067
  onState: (next) => {
6786
7068
  if (active !== share || share.tunnelGeneration !== generation)
6787
7069
  return;
@@ -6793,17 +7075,17 @@ function createShareManager(options) {
6793
7075
  });
6794
7076
  }
6795
7077
  options.live.nudge("share");
6796
- return { ok: true, share: status() };
7078
+ return { ok: true, share: snapshot(share) };
6797
7079
  };
6798
7080
  const allowRoute = (input) => {
6799
7081
  const share = active;
6800
7082
  if (share === null)
6801
7083
  return { ok: false, status: 404, error: "Nothing is being shared." };
6802
- const given = isRecord3(input) && typeof input.path === "string" ? input.path.trim() : "";
7084
+ const given = isJsonRecord(input) && isString(input.path) ? input.path.trim() : "";
6803
7085
  if (!given.startsWith("/")) {
6804
7086
  return { ok: false, status: 400, error: "A route is a path beginning with a slash." };
6805
7087
  }
6806
- const subtree = isRecord3(input) && input.subtree === true;
7088
+ const subtree = isJsonRecord(input) && input.subtree === true;
6807
7089
  if (subtree && given.replace(/\/+$/, "") === "") {
6808
7090
  return { ok: false, status: 400, error: "The root is a page, not a folder." };
6809
7091
  }
@@ -6815,7 +7097,7 @@ function createShareManager(options) {
6815
7097
  share.routes.push(asked);
6816
7098
  share.refused = share.refused.filter((path) => !routeAllowed([asked], path));
6817
7099
  options.live.nudge("share");
6818
- return { ok: true, share: status() };
7100
+ return { ok: true, share: snapshot(share) };
6819
7101
  };
6820
7102
  const update = async (input) => {
6821
7103
  const share = active;
@@ -6833,7 +7115,7 @@ function createShareManager(options) {
6833
7115
  share.layout = parsed.manifest.layout;
6834
7116
  options.live.nudge("share");
6835
7117
  options.live.nudge("config");
6836
- return { ok: true, share: status() };
7118
+ return { ok: true, share: snapshot(share) };
6837
7119
  };
6838
7120
  const viewerConfig = async (grantId) => {
6839
7121
  const share = active;
@@ -6923,8 +7205,8 @@ var RENAMES_PATH = ".leglas/renames.json";
6923
7205
  async function readRenames(cwd) {
6924
7206
  try {
6925
7207
  const raw = await readFile10(join10(cwd, RENAMES_PATH), "utf8");
6926
- const parsed = JSON.parse(raw);
6927
- if (parsed.renames === null || typeof parsed.renames !== "object")
7208
+ const parsed = parseJson(raw);
7209
+ if (!isJsonRecord(parsed) || !isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames))
6928
7210
  return {};
6929
7211
  return Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
6930
7212
  } catch {
@@ -6956,6 +7238,115 @@ import http4 from "http";
6956
7238
  import net3 from "net";
6957
7239
  import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
6958
7240
 
7241
+ // ../server/dist/frame-policy.js
7242
+ var FRAMING_TIMEOUT_MS = 4e3;
7243
+ var FRAME_ANCESTORS = "frame-ancestors";
7244
+ var QUOTE_CAP = 160;
7245
+ function quote(value) {
7246
+ return value.length <= QUOTE_CAP ? value : `${value.slice(0, QUOTE_CAP - 1)}\u2026`;
7247
+ }
7248
+ function defaultPort(protocol) {
7249
+ return protocol === "https:" || protocol === "wss:" ? "443" : "80";
7250
+ }
7251
+ function portOf(url) {
7252
+ return url.port === "" ? defaultPort(url.protocol) : url.port;
7253
+ }
7254
+ function schemeAdmits(scheme, url) {
7255
+ return url.protocol === scheme || scheme === "http:" && url.protocol === "https:";
7256
+ }
7257
+ var HOST_SOURCE = /^(?:([a-z][a-z0-9+.-]*):\/\/)?(\*|\*\.[^:/]+|\[[0-9a-f:.]+\]|[^:/*[\]]+)(?::(\*|\d+))?(\/.*)?$/i;
7258
+ function sourceAdmits(source, embedder, target) {
7259
+ const lower = source.toLowerCase();
7260
+ if (lower === "'self'")
7261
+ return embedder.origin === target.origin;
7262
+ if (lower === "*")
7263
+ return embedder.protocol === "http:" || embedder.protocol === "https:";
7264
+ if (/^[a-z][a-z0-9+.-]*:$/.test(lower))
7265
+ return schemeAdmits(lower, embedder);
7266
+ if (lower.startsWith("'"))
7267
+ return false;
7268
+ const match = HOST_SOURCE.exec(lower);
7269
+ if (match === null)
7270
+ return false;
7271
+ const [, scheme, host = "", port, path] = match;
7272
+ const schemeOk = scheme === void 0 ? (
7273
+ // No scheme borrows the protected page's own, with the upgrade allowed.
7274
+ schemeAdmits(target.protocol, embedder)
7275
+ ) : schemeAdmits(`${scheme}:`, embedder);
7276
+ const hostname = embedder.hostname.toLowerCase();
7277
+ const hostOk = host === "*" ? true : host.startsWith("*.") ? hostname.endsWith(host.slice(1)) : hostname === host;
7278
+ const portOk = port === "*" ? true : port === void 0 ? portOf(embedder) === defaultPort(embedder.protocol) : portOf(embedder) === port;
7279
+ const pathOk = path === void 0 || path === "/" ? true : path.endsWith("/") ? embedder.pathname.startsWith(path) : embedder.pathname === path;
7280
+ return schemeOk && hostOk && portOk && pathOk;
7281
+ }
7282
+ function listAdmits(list, embedder, target) {
7283
+ const sources = list.split(/\s+/).filter((source) => source !== "");
7284
+ return sources.some((source) => sourceAdmits(source, embedder, target));
7285
+ }
7286
+ function ancestorLists(csp) {
7287
+ if (csp === null)
7288
+ return [];
7289
+ const lists = [];
7290
+ for (const policy of csp.split(",")) {
7291
+ for (const directive of policy.split(";")) {
7292
+ const trimmed = directive.trim();
7293
+ const name = trimmed.split(/\s+/, 1)[0]?.toLowerCase();
7294
+ if (name === FRAME_ANCESTORS)
7295
+ lists.push(trimmed.slice(FRAME_ANCESTORS.length).trim());
7296
+ }
7297
+ }
7298
+ return lists;
7299
+ }
7300
+ function framingFor(headers, target, embedder) {
7301
+ const targetUrl = new URL(target);
7302
+ const embedderUrl = new URL(embedder);
7303
+ const lists = ancestorLists(headers.get("content-security-policy"));
7304
+ if (lists.length > 0) {
7305
+ const refusing = lists.find((list) => !listAdmits(list, embedderUrl, targetUrl));
7306
+ return refusing === void 0 ? { framable: true } : {
7307
+ framable: false,
7308
+ refusal: {
7309
+ header: "content-security-policy",
7310
+ value: quote(`${FRAME_ANCESTORS} ${refusing}`.trim())
7311
+ }
7312
+ };
7313
+ }
7314
+ const raw = headers.get("x-frame-options");
7315
+ if (raw === null)
7316
+ return { framable: true };
7317
+ const values = new Set(raw.split(",").flatMap((value) => {
7318
+ const normal = value.trim().toLowerCase();
7319
+ return normal === "" ? [] : [normal];
7320
+ }));
7321
+ const refused = {
7322
+ framable: false,
7323
+ refusal: { header: "x-frame-options", value: quote(raw.trim()) }
7324
+ };
7325
+ if (values.size > 1) {
7326
+ return values.has("deny") || values.has("sameorigin") || values.has("allowall") ? refused : { framable: true };
7327
+ }
7328
+ if (values.has("deny"))
7329
+ return refused;
7330
+ if (values.has("sameorigin")) {
7331
+ return embedderUrl.origin === targetUrl.origin ? { framable: true } : refused;
7332
+ }
7333
+ return { framable: true };
7334
+ }
7335
+ async function checkFraming(target, embedder, fetcher = fetch) {
7336
+ try {
7337
+ const response = await fetcher(target, {
7338
+ headers: { accept: "text/html" },
7339
+ redirect: "follow",
7340
+ signal: AbortSignal.timeout(FRAMING_TIMEOUT_MS)
7341
+ });
7342
+ void response.body?.cancel().catch(() => {
7343
+ });
7344
+ return framingFor(response.headers, response.url === "" ? target : response.url, embedder);
7345
+ } catch {
7346
+ return { framable: null };
7347
+ }
7348
+ }
7349
+
6959
7350
  // ../server/dist/server-info.js
6960
7351
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
6961
7352
  import { dirname as dirname7, join as join11 } from "path";
@@ -6972,16 +7363,16 @@ async function writeServerInfo(cwd, info) {
6972
7363
  const temporary = `${path}.${process.pid}.tmp`;
6973
7364
  await writeFile7(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
6974
7365
  `, "utf8");
6975
- await rename2(temporary, path).catch(async (error) => {
7366
+ await rename2(temporary, path).catch(async (cause) => {
6976
7367
  await rm4(temporary, { force: true }).catch(() => {
6977
7368
  });
6978
- throw error;
7369
+ throw cause;
6979
7370
  });
6980
7371
  }
6981
7372
  async function readServerInfo(cwd) {
6982
7373
  try {
6983
- const value = JSON.parse(await readFile11(join11(cwd, SERVER_INFO_PATH), "utf8"));
6984
- if (typeof value.port !== "number" || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535 || typeof value.url !== "string" || value.url === "" || typeof value.pid !== "number" || !Number.isInteger(value.pid))
7374
+ const value = parseJson(await readFile11(join11(cwd, SERVER_INFO_PATH), "utf8"));
7375
+ if (!isJsonRecord(value) || !isNumber(value.port) || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535 || !isString(value.url) || value.url === "" || !isNumber(value.pid) || !Number.isInteger(value.pid))
6985
7376
  return null;
6986
7377
  return { port: value.port, url: value.url, pid: value.pid };
6987
7378
  } catch {
@@ -7020,6 +7411,9 @@ var CONTENT_TYPES = {
7020
7411
  ".woff": "font/woff",
7021
7412
  ".woff2": "font/woff2"
7022
7413
  };
7414
+ function isContentExtension(extension) {
7415
+ return Object.hasOwn(CONTENT_TYPES, extension);
7416
+ }
7023
7417
  var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
7024
7418
  function sendJson2(res, status, body) {
7025
7419
  const payload = JSON.stringify(body);
@@ -7066,8 +7460,11 @@ function referenceName(value) {
7066
7460
  }).join("").trim().slice(0, 80);
7067
7461
  return safe || "image";
7068
7462
  }
7463
+ function isAddressInUse(cause) {
7464
+ return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EADDRINUSE";
7465
+ }
7069
7466
  function isKnownAgent(value) {
7070
- return typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
7467
+ return isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
7071
7468
  }
7072
7469
  function isAllowedMutationHost(hostname) {
7073
7470
  const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
@@ -7088,7 +7485,7 @@ function isLoopbackAddress(address) {
7088
7485
  function isTrustedMutation(req) {
7089
7486
  if (!isLoopbackAddress(req.socket.remoteAddress))
7090
7487
  return false;
7091
- if (typeof req.headers.host !== "string")
7488
+ if (!isString(req.headers.host))
7092
7489
  return false;
7093
7490
  let host;
7094
7491
  try {
@@ -7112,17 +7509,21 @@ function isEnded(request, failedIds) {
7112
7509
  return isTerminal(request.status) || failedIds.includes(request.id);
7113
7510
  }
7114
7511
  function jsonBody(body) {
7115
- let parsed;
7116
7512
  try {
7117
- parsed = JSON.parse(body || "{}");
7513
+ const parsed = parseJson(body || "{}");
7514
+ return isJsonRecord(parsed) ? parsed : null;
7118
7515
  } catch {
7119
7516
  return null;
7120
7517
  }
7121
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
7518
+ }
7519
+ function requestIntent(value) {
7520
+ if (value === null || value === void 0)
7521
+ return "";
7522
+ return isString(value) ? value.trim() : null;
7122
7523
  }
7123
7524
  function hasJsonBody(req) {
7124
7525
  const contentType = req.headers["content-type"];
7125
- return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
7526
+ return isString(contentType) && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
7126
7527
  }
7127
7528
  function probe(target, timeoutMs = 1e3) {
7128
7529
  return new Promise((resolve5) => {
@@ -7151,8 +7552,9 @@ function serveFrom(res, dir, relativePath) {
7151
7552
  return false;
7152
7553
  if (!existsSync3(candidate) || !statSync(candidate).isFile())
7153
7554
  return false;
7555
+ const extension = extname3(candidate);
7154
7556
  res.writeHead(200, {
7155
- "content-type": CONTENT_TYPES[extname3(candidate)] ?? "application/octet-stream",
7557
+ "content-type": isContentExtension(extension) ? CONTENT_TYPES[extension] : "application/octet-stream",
7156
7558
  "cache-control": "no-store"
7157
7559
  });
7158
7560
  createReadStream(candidate).pipe(res);
@@ -7409,7 +7811,7 @@ function listen(server, port) {
7409
7811
  const onListening = () => {
7410
7812
  server.removeListener("error", onError);
7411
7813
  const address = server.address();
7412
- resolve5(typeof address === "object" && address !== null ? address.port : port);
7814
+ resolve5(address !== null && !isString(address) ? address.port : port);
7413
7815
  };
7414
7816
  server.once("error", onError);
7415
7817
  server.once("listening", onListening);
@@ -7423,7 +7825,7 @@ async function bind2(server, requested) {
7423
7825
  try {
7424
7826
  return await listen(server, requested + attempt);
7425
7827
  } catch (error) {
7426
- if (error.code !== "EADDRINUSE")
7828
+ if (!isAddressInUse(error))
7427
7829
  throw error;
7428
7830
  }
7429
7831
  }
@@ -7435,14 +7837,16 @@ async function startServer(options) {
7435
7837
  let shares = null;
7436
7838
  let liveHealth = null;
7437
7839
  const live = options.live ?? createLiveHub();
7438
- const branches = createBranchRegistry({
7840
+ const branchOptions = {
7439
7841
  cwd,
7440
7842
  previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
7441
7843
  installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
7442
7844
  devCommand: config?.devCommand,
7443
- onChange: () => live.nudge("config"),
7444
- ...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
7445
- });
7845
+ onChange: () => live.nudge("config")
7846
+ };
7847
+ if (options.startWorktree !== void 0)
7848
+ branchOptions.startWorktree = options.startWorktree;
7849
+ const branches = createBranchRegistry(branchOptions);
7446
7850
  const previewForConfig = (preview) => {
7447
7851
  if (preview.branch === void 0)
7448
7852
  return preview;
@@ -7556,9 +7960,9 @@ async function startServer(options) {
7556
7960
  return void updates.check({ force: true }).then((status) => sendJson2(res, 200, status));
7557
7961
  }
7558
7962
  if (path.endsWith("/install")) {
7559
- return void updates.update().then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 409, {
7963
+ return void updates.update().then((status) => sendJson2(res, 200, status), (cause) => sendJson2(res, 409, {
7560
7964
  ok: false,
7561
- error: error instanceof Error ? error.message : String(error)
7965
+ error: cause instanceof Error ? cause.message : String(cause)
7562
7966
  }));
7563
7967
  }
7564
7968
  if (!hasJsonBody(req))
@@ -7569,12 +7973,12 @@ async function startServer(options) {
7569
7973
  const parsed = jsonBody(body);
7570
7974
  if (parsed === null)
7571
7975
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7572
- if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
7976
+ if (!isString(parsed.version) || parsed.version.trim() === "") {
7573
7977
  return sendJson2(res, 400, { ok: false, error: "Body needs a version." });
7574
7978
  }
7575
- void updates.skip(parsed.version).then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 400, {
7979
+ void updates.skip(parsed.version).then((status) => sendJson2(res, 200, status), (cause) => sendJson2(res, 400, {
7576
7980
  ok: false,
7577
- error: error instanceof Error ? error.message : String(error)
7981
+ error: cause instanceof Error ? cause.message : String(cause)
7578
7982
  }));
7579
7983
  });
7580
7984
  }
@@ -7592,10 +7996,10 @@ async function startServer(options) {
7592
7996
  if (parsed === null) {
7593
7997
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7594
7998
  }
7595
- const result = await shares?.create(parsed).catch((error) => ({
7999
+ const result = await shares?.create(parsed).catch((cause) => ({
7596
8000
  ok: false,
7597
8001
  status: 500,
7598
- error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
8002
+ error: `Leglas could not start the share (${cause instanceof Error ? cause.message : String(cause)}).`
7599
8003
  }));
7600
8004
  if (result === void 0) {
7601
8005
  return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
@@ -7677,6 +8081,24 @@ async function startServer(options) {
7677
8081
  warnings: configWarnings
7678
8082
  }));
7679
8083
  }
8084
+ if (path === `${LEGLAS_PREFIX}/api/previews/framing` && req.method === "GET") {
8085
+ const site = req.headers["sec-fetch-site"];
8086
+ if (site !== void 0 && site !== "same-origin" && site !== "none") {
8087
+ return sendJson2(res, 403, { ok: false, error: "Only the interface can ask this." });
8088
+ }
8089
+ const title = query.get("title");
8090
+ return void livePreviewDefinitions().then(async (previews) => {
8091
+ const preview = previews.find((entry) => entry.title === title);
8092
+ if (preview === void 0) {
8093
+ return sendJson2(res, 404, { ok: false, error: "There is no direction by that name." });
8094
+ }
8095
+ if (preview.file !== void 0 || preview.branch !== void 0 || !/^https?:\/\//i.test(preview.url)) {
8096
+ return sendJson2(res, 200, { framable: true });
8097
+ }
8098
+ const embedder = `http://${req.headers.host ?? "localhost"}${LEGLAS_PREFIX}/`;
8099
+ sendJson2(res, 200, await checkFraming(preview.url, embedder));
8100
+ });
8101
+ }
7680
8102
  if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
7681
8103
  let body = "";
7682
8104
  req.on("data", (chunk) => body += chunk);
@@ -7685,7 +8107,7 @@ async function startServer(options) {
7685
8107
  if (parsed === null) {
7686
8108
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7687
8109
  }
7688
- if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
8110
+ if (!isString(parsed.title) || parsed.title.trim() === "") {
7689
8111
  return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
7690
8112
  }
7691
8113
  const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
@@ -7721,7 +8143,7 @@ async function startServer(options) {
7721
8143
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7722
8144
  }
7723
8145
  const titles = parsed.titles;
7724
- if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
8146
+ if (!Array.isArray(titles) || titles.length === 0 || !titles.every((title) => isString(title) && title.trim() !== "")) {
7725
8147
  return sendJson2(res, 400, {
7726
8148
  ok: false,
7727
8149
  error: "Body needs a non-empty array of direction titles."
@@ -7753,7 +8175,7 @@ async function startServer(options) {
7753
8175
  }
7754
8176
  if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
7755
8177
  const declaredLength = req.headers["content-length"];
7756
- if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
8178
+ if (isString(declaredLength) && Number(declaredLength) > REFERENCE_MAX_BYTES) {
7757
8179
  return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
7758
8180
  }
7759
8181
  const chunks = [];
@@ -7828,7 +8250,7 @@ async function startServer(options) {
7828
8250
  });
7829
8251
  }
7830
8252
  const mode = parsed.mode === "replace" ? "replace" : "variant";
7831
- if (parsed.references !== void 0 && (!Array.isArray(parsed.references) || parsed.references.some((reference) => typeof reference !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
8253
+ if (parsed.references !== void 0 && (!Array.isArray(parsed.references) || !parsed.references.every((reference) => isString(reference) && /^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
7832
8254
  return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
7833
8255
  }
7834
8256
  const references = parsed.references ?? [];
@@ -7842,23 +8264,26 @@ async function startServer(options) {
7842
8264
  });
7843
8265
  }
7844
8266
  }
7845
- const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
8267
+ const width = isNumber(parsed.width) && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
7846
8268
  const previews = await livePreviews();
7847
8269
  const preview = previews.find((entry) => entry.title === parsed.title);
7848
8270
  if (!preview) {
7849
8271
  return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7850
8272
  }
7851
8273
  const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
7852
- if (!parsed.intent?.trim() && notes.length === 0) {
8274
+ const intent = requestIntent(parsed.intent);
8275
+ if (intent === null) {
8276
+ return sendJson2(res, 400, { ok: false, error: "A request's words must be text." });
8277
+ }
8278
+ if (!intent && notes.length === 0) {
7853
8279
  return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7854
8280
  }
7855
- const intent = (parsed.intent ?? "").trim();
7856
8281
  const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
7857
8282
  const sameNotes = (entry) => {
7858
8283
  const before = [...entry.notes ?? []].sort().join(",");
7859
8284
  return before === notes.map((note) => note.id).sort().join(",");
7860
8285
  };
7861
- const compare = typeof parsed.compare === "string" && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
8286
+ const compare = isString(parsed.compare) && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
7862
8287
  const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
7863
8288
  if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
7864
8289
  // one forks the direction and the other rewrites it. Only a
@@ -7871,7 +8296,7 @@ async function startServer(options) {
7871
8296
  });
7872
8297
  }
7873
8298
  const address = server.address();
7874
- const requestPort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
8299
+ const requestPort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
7875
8300
  const id = newRequestId();
7876
8301
  const captured = await attachRequest(cwd, id, {
7877
8302
  origin: `http://127.0.0.1:${requestPort}`,
@@ -7883,20 +8308,26 @@ async function startServer(options) {
7883
8308
  }, { pool: browserPool });
7884
8309
  const composed = composeRequest(preview, intent, mode, notes, leglasCommand, captured);
7885
8310
  try {
7886
- await appendRequest(cwd, {
8311
+ const queued = {
7887
8312
  title: preview.title,
7888
8313
  url: preview.url,
7889
8314
  intent,
7890
8315
  // The ids travel with the request so a change made in place can
7891
8316
  // forget the notes it answered. A fork leaves them where they are:
7892
8317
  // the direction they point at was not touched.
7893
- ...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
7894
- ...captured.attachments.length === 0 ? {} : { attachments: captured.attachments },
7895
- ...captured.skipped === null ? {} : { captureNote: captured.skipped },
7896
- ...compare === null ? {} : { compare: compare.title },
7897
- ...references.length === 0 ? {} : { references },
7898
8318
  ...composed
7899
- }, id);
8319
+ };
8320
+ if (notes.length > 0)
8321
+ queued.notes = notes.map((entry) => entry.id);
8322
+ if (captured.attachments.length > 0)
8323
+ queued.attachments = captured.attachments;
8324
+ if (captured.skipped !== null)
8325
+ queued.captureNote = captured.skipped;
8326
+ if (compare !== null)
8327
+ queued.compare = compare.title;
8328
+ if (references.length > 0)
8329
+ queued.references = references;
8330
+ await appendRequest(cwd, queued, id);
7900
8331
  runner?.nudge();
7901
8332
  return sendJson2(res, 200, {
7902
8333
  ok: true,
@@ -7924,17 +8355,17 @@ async function startServer(options) {
7924
8355
  if (parsed === null) {
7925
8356
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7926
8357
  }
7927
- if (typeof parsed.title !== "string" || parsed.title === "") {
8358
+ if (!isString(parsed.title) || parsed.title === "") {
7928
8359
  return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
7929
8360
  }
7930
- if (parsed.note !== void 0 && typeof parsed.note !== "string") {
8361
+ if (parsed.note !== void 0 && !isString(parsed.note)) {
7931
8362
  return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
7932
8363
  }
7933
8364
  const preview = (await livePreviews()).find((entry) => entry.title === parsed.title);
7934
8365
  if (preview === void 0) {
7935
8366
  return sendJson2(res, 404, { ok: false, error: "No such direction." });
7936
8367
  }
7937
- const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
8368
+ const width = isNumber(parsed.width) && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
7938
8369
  const browser = await browserPool.acquire();
7939
8370
  if (browser === null) {
7940
8371
  return sendJson2(res, 503, {
@@ -7942,9 +8373,9 @@ async function startServer(options) {
7942
8373
  error: browserPool.reason() ?? NO_BROWSER
7943
8374
  });
7944
8375
  }
7945
- const annotations = typeof parsed.note === "string" ? (await readAnnotations(cwd).catch(() => [])).filter((entry) => entry.id === parsed.note && entry.title === preview.title) : [];
8376
+ const annotations = isString(parsed.note) ? (await readAnnotations(cwd).catch(() => [])).filter((entry) => entry.id === parsed.note && entry.title === preview.title) : [];
7946
8377
  const address = server.address();
7947
- const capturePort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
8378
+ const capturePort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
7948
8379
  const controller = new AbortController();
7949
8380
  const timeoutMarker = /* @__PURE__ */ Symbol("capture timeout");
7950
8381
  let timedOut;
@@ -7957,21 +8388,26 @@ async function startServer(options) {
7957
8388
  }, CAPTURE_DEADLINE_MS);
7958
8389
  timer.unref?.();
7959
8390
  try {
7960
- const work = capturePage(browser, {
8391
+ const captureInput = {
7961
8392
  url: previewUrl(`http://127.0.0.1:${capturePort}`, preview),
7962
8393
  width,
7963
- ...annotations.length === 0 ? {} : {
7964
- focuses: annotations.map((entry) => ({
8394
+ timeoutMs: CAPTURE_LOAD_MS,
8395
+ signal: controller.signal
8396
+ };
8397
+ if (annotations.length > 0) {
8398
+ captureInput.focuses = annotations.map((entry) => {
8399
+ const focus = {
7965
8400
  selector: entry.anchor.selector,
7966
8401
  text: entry.anchor.text,
7967
8402
  tag: entry.anchor.tag,
7968
- ...entry.anchor.region === void 0 ? {} : { region: entry.anchor.region },
7969
8403
  rect: entry.anchor.rect
7970
- }))
7971
- },
7972
- timeoutMs: CAPTURE_LOAD_MS,
7973
- signal: controller.signal
7974
- });
8404
+ };
8405
+ if (entry.anchor.region !== void 0)
8406
+ focus.region = entry.anchor.region;
8407
+ return focus;
8408
+ });
8409
+ }
8410
+ const work = capturePage(browser, captureInput);
7975
8411
  const result = await Promise.race([work, timeout]);
7976
8412
  if (result === timeoutMarker) {
7977
8413
  return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
@@ -7979,7 +8415,7 @@ async function startServer(options) {
7979
8415
  clearTimeout(timer);
7980
8416
  const crop = annotations.length > 0 ? result.crops[0] : null;
7981
8417
  const shot = crop?.shot ?? result.frame;
7982
- const noteSuffix = typeof parsed.note === "string" ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
8418
+ const noteSuffix = isString(parsed.note) ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
7983
8419
  const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
7984
8420
  const relativeFile = `${CAPTURES_DIR}/show/${name}`;
7985
8421
  await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
@@ -8041,7 +8477,7 @@ async function startServer(options) {
8041
8477
  if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
8042
8478
  return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
8043
8479
  }
8044
- if (parsed.run !== void 0 && typeof parsed.run !== "string") {
8480
+ if (parsed.run !== void 0 && !isString(parsed.run)) {
8045
8481
  return sendJson2(res, 400, {
8046
8482
  ok: false,
8047
8483
  error: "The custom run command must be a string."
@@ -8061,7 +8497,7 @@ async function startServer(options) {
8061
8497
  error: "Custom agents manage effort in their own command."
8062
8498
  });
8063
8499
  }
8064
- if (typeof parsed.run !== "string") {
8500
+ if (!isString(parsed.run)) {
8065
8501
  return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
8066
8502
  }
8067
8503
  const template = parseTemplate(parsed.run);
@@ -8069,17 +8505,18 @@ async function startServer(options) {
8069
8505
  return sendJson2(res, 400, { ok: false, error: template.error });
8070
8506
  return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
8071
8507
  }
8072
- if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.includes(effort)) {
8508
+ if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.some((supported) => supported === effort)) {
8073
8509
  return sendJson2(res, 400, {
8074
8510
  ok: false,
8075
8511
  error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
8076
8512
  });
8077
8513
  }
8078
- return void saveAgentChoice(cwd, {
8079
- agent: parsed.agent,
8080
- ...effort === void 0 ? {} : { effort }
8081
- }).then(() => {
8082
- runner?.prepare(parsed.agent);
8514
+ const agent = parsed.agent;
8515
+ const choice = { agent };
8516
+ if (effort !== void 0)
8517
+ choice.effort = effort;
8518
+ return void saveAgentChoice(cwd, choice).then(() => {
8519
+ runner?.prepare(agent);
8083
8520
  sendJson2(res, 200, { ok: true });
8084
8521
  }, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
8085
8522
  });
@@ -8092,7 +8529,7 @@ async function startServer(options) {
8092
8529
  if (parsed === null) {
8093
8530
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8094
8531
  }
8095
- if (typeof parsed.watching !== "boolean") {
8532
+ if (!isBoolean(parsed.watching)) {
8096
8533
  return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
8097
8534
  }
8098
8535
  lastSeen = parsed.watching ? Date.now() : null;
@@ -8108,6 +8545,7 @@ async function startServer(options) {
8108
8545
  startedAt: null,
8109
8546
  stopping: false,
8110
8547
  waiting: null,
8548
+ quietSince: null,
8111
8549
  failedIds: []
8112
8550
  };
8113
8551
  return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
@@ -8139,7 +8577,10 @@ async function startServer(options) {
8139
8577
  // says so rather than going on describing a live run.
8140
8578
  stopping: snapshot.running && snapshot.stopping,
8141
8579
  // Why a run that looks stalled is stalled, while it is stalled.
8142
- waiting: snapshot.running ? snapshot.waiting : null
8580
+ waiting: snapshot.running ? snapshot.waiting : null,
8581
+ // When a run that has gone quiet last said anything. A run on its
8582
+ // way out is not quiet, it is stopping.
8583
+ quietSince: snapshot.running && !snapshot.stopping ? snapshot.quietSince : null
8143
8584
  }
8144
8585
  }));
8145
8586
  }
@@ -8154,7 +8595,7 @@ async function startServer(options) {
8154
8595
  if (parsed === null) {
8155
8596
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8156
8597
  }
8157
- if (parsed.id !== void 0 && typeof parsed.id !== "string") {
8598
+ if (parsed.id !== void 0 && !isString(parsed.id)) {
8158
8599
  return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
8159
8600
  }
8160
8601
  return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
@@ -8171,7 +8612,7 @@ async function startServer(options) {
8171
8612
  if (parsed === null) {
8172
8613
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8173
8614
  }
8174
- if (typeof parsed.id !== "string") {
8615
+ if (!isString(parsed.id)) {
8175
8616
  return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
8176
8617
  }
8177
8618
  const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
@@ -8190,7 +8631,7 @@ async function startServer(options) {
8190
8631
  if (!await removeRequest(cwd, request.id)) {
8191
8632
  return sendJson2(res, 404, { ok: false, error: "No such request." });
8192
8633
  }
8193
- await appendRequest(cwd, {
8634
+ const retry = {
8194
8635
  title: request.title,
8195
8636
  url: request.url,
8196
8637
  intent: request.intent,
@@ -8198,16 +8639,23 @@ async function startServer(options) {
8198
8639
  // The prompt names the captures by path, and the embedded pipes
8199
8640
  // are not its only readers: watch, a custom command and
8200
8641
  // `requests --json` all hand the text over as it stands.
8201
- prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId),
8642
+ prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId)
8202
8643
  // The stored prompt already carries the mode's instructions;
8203
8644
  // its notes and visual context travel with the retry too.
8204
- ...request.mode === void 0 ? {} : { mode: request.mode },
8205
- ...request.notes === void 0 ? {} : { notes: request.notes },
8206
- ...attachments.length === 0 ? {} : { attachments },
8207
- ...request.captureNote === void 0 ? {} : { captureNote: request.captureNote },
8208
- ...request.compare === void 0 ? {} : { compare: request.compare },
8209
- ...request.references === void 0 ? {} : { references: request.references }
8210
- }, retryId);
8645
+ };
8646
+ if (request.mode !== void 0)
8647
+ retry.mode = request.mode;
8648
+ if (request.notes !== void 0)
8649
+ retry.notes = request.notes;
8650
+ if (attachments.length > 0)
8651
+ retry.attachments = attachments;
8652
+ if (request.captureNote !== void 0)
8653
+ retry.captureNote = request.captureNote;
8654
+ if (request.compare !== void 0)
8655
+ retry.compare = request.compare;
8656
+ if (request.references !== void 0)
8657
+ retry.references = request.references;
8658
+ await appendRequest(cwd, retry, retryId);
8211
8659
  runner?.nudge();
8212
8660
  return sendJson2(res, 200, { ok: true });
8213
8661
  } catch {
@@ -8229,7 +8677,7 @@ async function startServer(options) {
8229
8677
  if (parsed === null) {
8230
8678
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8231
8679
  }
8232
- if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
8680
+ if (!isString(parsed.title) || parsed.title.trim() === "") {
8233
8681
  return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
8234
8682
  }
8235
8683
  const anchor = anchorFrom(parsed.anchor);
@@ -8239,7 +8687,7 @@ async function startServer(options) {
8239
8687
  try {
8240
8688
  const annotation = await addAnnotation(cwd, {
8241
8689
  anchor,
8242
- note: typeof parsed.note === "string" ? parsed.note.trim() : "",
8690
+ note: isString(parsed.note) ? parsed.note.trim() : "",
8243
8691
  title: parsed.title
8244
8692
  });
8245
8693
  return sendJson2(res, 200, { ok: true, annotation });
@@ -8259,10 +8707,10 @@ async function startServer(options) {
8259
8707
  if (parsed === null) {
8260
8708
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8261
8709
  }
8262
- if (typeof parsed.id !== "string" || parsed.id === "") {
8710
+ if (!isString(parsed.id) || parsed.id === "") {
8263
8711
  return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
8264
8712
  }
8265
- if (typeof parsed.note !== "string") {
8713
+ if (!isString(parsed.note)) {
8266
8714
  return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
8267
8715
  }
8268
8716
  try {
@@ -8287,7 +8735,7 @@ async function startServer(options) {
8287
8735
  if (parsed === null) {
8288
8736
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8289
8737
  }
8290
- const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
8738
+ const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => isString(entry)) : [];
8291
8739
  if (ids.length === 0) {
8292
8740
  return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
8293
8741
  }
@@ -8309,7 +8757,7 @@ async function startServer(options) {
8309
8757
  if (parsed === null) {
8310
8758
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8311
8759
  }
8312
- if (typeof parsed.id !== "string") {
8760
+ if (!isString(parsed.id)) {
8313
8761
  return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
8314
8762
  }
8315
8763
  const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
@@ -8337,7 +8785,7 @@ async function startServer(options) {
8337
8785
  if (parsed === null) {
8338
8786
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8339
8787
  }
8340
- if (parsed.renames === null || typeof parsed.renames !== "object") {
8788
+ if (!isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames)) {
8341
8789
  return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
8342
8790
  }
8343
8791
  const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
@@ -8426,7 +8874,7 @@ async function startServer(options) {
8426
8874
  server.on("upgrade", (req, socket, head) => {
8427
8875
  handleUpgrade(req, socket, head, { remote: false });
8428
8876
  });
8429
- shares = createShareManager({
8877
+ const shareOptions = {
8430
8878
  live,
8431
8879
  previews: livePreviewDefinitions,
8432
8880
  previewsForConfig,
@@ -8440,10 +8888,13 @@ async function startServer(options) {
8440
8888
  publicOrigin: context.publicOrigin,
8441
8889
  grantId: context.grantId
8442
8890
  }),
8443
- upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
8444
- ...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
8445
- ...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
8446
- });
8891
+ upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true })
8892
+ };
8893
+ if (options.detectTunnels !== void 0)
8894
+ shareOptions.detectTunnels = options.detectTunnels;
8895
+ if (options.startTunnel !== void 0)
8896
+ shareOptions.startTunnel = options.startTunnel;
8897
+ shares = createShareManager(shareOptions);
8447
8898
  port = await bind2(server, options.port ?? DEFAULT_PORT);
8448
8899
  options.updates?.setPort(port);
8449
8900
  const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
@@ -8456,14 +8907,17 @@ async function startServer(options) {
8456
8907
  pid: process.pid
8457
8908
  }).catch(() => {
8458
8909
  });
8459
- runner = startRunner({
8910
+ const runnerOptions = {
8460
8911
  cwd,
8461
8912
  externallyAttached,
8462
8913
  onChange: () => live.nudge("requests"),
8463
- leglasCommand,
8464
- ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
8465
- ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
8466
- });
8914
+ leglasCommand
8915
+ };
8916
+ if (options.codexAppServer !== void 0)
8917
+ runnerOptions.codexAppServer = options.codexAppServer;
8918
+ if (options.claudeAgentSession !== void 0)
8919
+ runnerOptions.claudeAgentSession = options.claudeAgentSession;
8920
+ runner = startRunner(runnerOptions);
8467
8921
  options.updates?.onBusy(() => runner?.snapshot().running ?? false);
8468
8922
  options.updates?.onChange(() => live.nudge("update"));
8469
8923
  let closePromise = null;
@@ -8882,17 +9336,16 @@ async function runAdd(options, deps) {
8882
9336
  await ensureIgnored(options.cwd);
8883
9337
  const needsDevCommand = options.preview.branch !== void 0 && loaded.config?.devCommand === void 0;
8884
9338
  if (options.json) {
8885
- envelope(deps, true, {
8886
- added: options.preview.title,
8887
- ...options.preview.url === void 0 ? {} : { url: options.preview.url },
8888
- local: true,
8889
- ...options.preview.branch === void 0 ? {} : { branch: options.preview.branch },
8890
- ...options.preview.file === void 0 ? {} : { file: options.preview.file },
8891
- note: options.preview.branch === void 0 && options.preview.file === void 0 ? "A running interface picks this up within seconds." : "Restart Leglas to see this preview: branch checkouts and file mounts are built when Leglas starts.",
8892
- ...needsDevCommand ? {
8893
- warning: "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config."
8894
- } : {}
8895
- });
9339
+ const added = { added: options.preview.title };
9340
+ if (options.preview.url !== void 0) added.url = options.preview.url;
9341
+ added.local = true;
9342
+ if (options.preview.branch !== void 0) added.branch = options.preview.branch;
9343
+ if (options.preview.file !== void 0) added.file = options.preview.file;
9344
+ added.note = options.preview.branch === void 0 && options.preview.file === void 0 ? "A running interface picks this up within seconds." : "Restart Leglas to see this preview: branch checkouts and file mounts are built when Leglas starts.";
9345
+ if (needsDevCommand) {
9346
+ added.warning = "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config.";
9347
+ }
9348
+ envelope(deps, true, added);
8896
9349
  } else {
8897
9350
  deps.log(
8898
9351
  ` added ${options.preview.title} ${options.preview.url ?? options.preview.file ?? ""}`
@@ -9031,10 +9484,29 @@ function planShow({ title, previews, requests }) {
9031
9484
  };
9032
9485
  }
9033
9486
 
9034
- // src/run-show.ts
9487
+ // src/running.ts
9035
9488
  import { realpath as realpath2 } from "fs/promises";
9489
+
9490
+ // src/json.ts
9491
+ function isJsonObject(value) {
9492
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9493
+ }
9494
+ function isString2(value) {
9495
+ return typeof value === "string";
9496
+ }
9497
+ function isNumber2(value) {
9498
+ return typeof value === "number";
9499
+ }
9500
+ async function bodyOf(response) {
9501
+ try {
9502
+ return JSON.parse(await response.text());
9503
+ } catch {
9504
+ return null;
9505
+ }
9506
+ }
9507
+
9508
+ // src/running.ts
9036
9509
  var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
9037
- var CAPTURE_WAIT_MS = 3e4;
9038
9510
  async function sameDirectory(left, right) {
9039
9511
  const [a, b] = await Promise.all([
9040
9512
  realpath2(left).catch(() => left),
@@ -9042,6 +9514,42 @@ async function sameDirectory(left, right) {
9042
9514
  ]);
9043
9515
  return a === b;
9044
9516
  }
9517
+ async function findLeglas(cwd, port, request) {
9518
+ const server = port === null ? await readServerInfo(cwd) : null;
9519
+ const chosen = port ?? server?.port ?? DEFAULT_PORT;
9520
+ try {
9521
+ const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
9522
+ signal: AbortSignal.timeout(2e3)
9523
+ });
9524
+ if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
9525
+ const answered = await health.text().then((text2) => JSON.parse(text2)).catch(() => ({}));
9526
+ if (answered === null) return { ok: false, error: NOT_RUNNING };
9527
+ if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
9528
+ return {
9529
+ ok: false,
9530
+ error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
9531
+ };
9532
+ }
9533
+ return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
9534
+ } catch {
9535
+ return { ok: false, error: NOT_RUNNING };
9536
+ }
9537
+ }
9538
+
9539
+ // src/run-show.ts
9540
+ function isString3(value) {
9541
+ return typeof value === "string";
9542
+ }
9543
+ function hasCaptureError(value) {
9544
+ return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string";
9545
+ }
9546
+ function hasCaptureSize(value) {
9547
+ return typeof value === "object" && value !== null && "file" in value && typeof value.file === "string" && "width" in value && typeof value.width === "number" && "height" in value && typeof value.height === "number" && "viewport" in value && typeof value.viewport === "number";
9548
+ }
9549
+ function isHydration(value) {
9550
+ return typeof value === "object" && value !== null && "framework" in value && typeof value.framework === "string" && "message" in value && typeof value.message === "string";
9551
+ }
9552
+ var CAPTURE_WAIT_MS = 3e4;
9045
9553
  async function runShow(options, deps) {
9046
9554
  const loaded = await loadConfig(options.cwd);
9047
9555
  const local = await readLocalPreviews(options.cwd);
@@ -9074,28 +9582,15 @@ async function runShow(options, deps) {
9074
9582
  requests: plan.requests
9075
9583
  };
9076
9584
  if (options.screenshot) {
9077
- const server = options.port === null ? await readServerInfo(options.cwd) : null;
9078
- const port = options.port ?? server?.port ?? DEFAULT_PORT;
9079
9585
  const fail = (error) => {
9080
9586
  if (options.json) deps.log(JSON.stringify({ ok: false, error }));
9081
9587
  else deps.error(error);
9082
9588
  return { exitCode: 1 };
9083
9589
  };
9084
9590
  const request = deps.fetch ?? fetch;
9085
- try {
9086
- const health = await request(`http://127.0.0.1:${port}/leglas/api/health`, {
9087
- signal: AbortSignal.timeout(2e3)
9088
- });
9089
- if (health.status !== 200) return fail(NOT_RUNNING);
9090
- const answered = await health.json().catch(() => ({}));
9091
- if (typeof answered.cwd === "string" && !await sameDirectory(answered.cwd, options.cwd)) {
9092
- return fail(
9093
- `The Leglas on port ${port} serves another project. Start one here with npx leglas, or name the right one with --port.`
9094
- );
9095
- }
9096
- } catch {
9097
- return fail(NOT_RUNNING);
9098
- }
9591
+ const found = await findLeglas(options.cwd, options.port, request);
9592
+ if (!found.ok) return fail(found.error);
9593
+ const port = found.port;
9099
9594
  let response;
9100
9595
  try {
9101
9596
  response = await request(`http://127.0.0.1:${port}/leglas/api/capture`, {
@@ -9112,18 +9607,17 @@ async function runShow(options, deps) {
9112
9607
  const captured = await response.json().catch(() => ({}));
9113
9608
  if (!response.ok) {
9114
9609
  return fail(
9115
- typeof captured.error === "string" ? captured.error : "The direction could not be captured."
9610
+ hasCaptureError(captured) ? captured.error : "The direction could not be captured."
9116
9611
  );
9117
9612
  }
9118
- if (typeof captured.file !== "string" || typeof captured.width !== "number" || typeof captured.height !== "number" || typeof captured.viewport !== "number")
9119
- return fail("The direction could not be captured.");
9613
+ if (!hasCaptureSize(captured)) return fail("The direction could not be captured.");
9120
9614
  envelope2.screenshot = {
9121
9615
  file: captured.file,
9122
9616
  width: captured.width,
9123
9617
  height: captured.height,
9124
9618
  viewport: captured.viewport,
9125
- errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
9126
- hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
9619
+ errors: Array.isArray(captured.errors) ? captured.errors.filter(isString3) : [],
9620
+ hydration: isHydration(captured.hydration) ? {
9127
9621
  framework: captured.hydration.framework,
9128
9622
  message: captured.hydration.message
9129
9623
  } : null,
@@ -9176,8 +9670,290 @@ async function runShow(options, deps) {
9176
9670
  return { exitCode: 0 };
9177
9671
  }
9178
9672
 
9673
+ // src/run-share.ts
9674
+ var TUNNEL_WAIT_MS = 6e4;
9675
+ var TUNNEL_POLL_MS = 500;
9676
+ function stringsOf(value) {
9677
+ return Array.isArray(value) && value.every(isString2) ? value : null;
9678
+ }
9679
+ function tunnelFrom(value) {
9680
+ if (value === void 0 || !isJsonObject(value)) return null;
9681
+ const provider = isString2(value.provider) ? value.provider : "The tunnel";
9682
+ switch (value.status) {
9683
+ case "none":
9684
+ return { status: "none" };
9685
+ case "starting":
9686
+ return { status: "starting", provider };
9687
+ case "ready":
9688
+ return isString2(value.url) ? { status: "ready", provider, url: value.url } : null;
9689
+ case "failed":
9690
+ return {
9691
+ status: "failed",
9692
+ provider,
9693
+ reason: isString2(value.reason) ? value.reason : "it stopped"
9694
+ };
9695
+ default:
9696
+ return null;
9697
+ }
9698
+ }
9699
+ function linkFrom(value) {
9700
+ if (!isJsonObject(value) || !isString2(value.localUrl) || !isNumber2(value.expiresAt)) return null;
9701
+ return {
9702
+ name: isString2(value.name) ? value.name : "",
9703
+ url: isString2(value.url) ? value.url : null,
9704
+ localUrl: value.localUrl,
9705
+ expiresAt: value.expiresAt
9706
+ };
9707
+ }
9708
+ function shareFrom(value) {
9709
+ if (value === void 0 || !isJsonObject(value)) return null;
9710
+ const titles = stringsOf(value.titles);
9711
+ const tunnel = tunnelFrom(value.tunnel);
9712
+ if (!isString2(value.scope) || titles === null || tunnel === null || !Array.isArray(value.grants)) {
9713
+ return null;
9714
+ }
9715
+ return {
9716
+ scope: value.scope,
9717
+ titles,
9718
+ reach: isString2(value.reach) ? value.reach : "open",
9719
+ tunnel,
9720
+ links: value.grants.flatMap((grant) => {
9721
+ const link = linkFrom(grant);
9722
+ return link === null ? [] : [link];
9723
+ })
9724
+ };
9725
+ }
9726
+ async function readShare(base, request) {
9727
+ try {
9728
+ const response = await request(`${base}/leglas/api/share`);
9729
+ if (!response.ok) {
9730
+ return {
9731
+ ok: false,
9732
+ error: `Leglas answered ${response.status} when asked what it is sharing.`,
9733
+ answered: true
9734
+ };
9735
+ }
9736
+ const payload = await bodyOf(response);
9737
+ if (!isJsonObject(payload)) {
9738
+ return { ok: false, error: "Leglas did not say what it is sharing.", answered: true };
9739
+ }
9740
+ const installed = stringsOf(payload.tunnels) ?? [];
9741
+ if (payload.share === null) return { ok: true, value: { share: null, installed } };
9742
+ const share = shareFrom(payload.share);
9743
+ return share === null ? {
9744
+ ok: false,
9745
+ error: "Leglas is sharing something this version of the command cannot read.",
9746
+ answered: true
9747
+ } : { ok: true, value: { share, installed } };
9748
+ } catch {
9749
+ return { ok: false, error: NOT_RUNNING, answered: false };
9750
+ }
9751
+ }
9752
+ async function post(base, path, body, request, fallback) {
9753
+ try {
9754
+ const response = await request(`${base}${path}`, {
9755
+ method: "POST",
9756
+ headers: { "content-type": "application/json" },
9757
+ body: JSON.stringify(body)
9758
+ });
9759
+ const payload = await bodyOf(response);
9760
+ if (!response.ok) {
9761
+ return {
9762
+ ok: false,
9763
+ error: isJsonObject(payload) && isString2(payload.error) ? payload.error : fallback,
9764
+ answered: true
9765
+ };
9766
+ }
9767
+ return { ok: true, value: payload };
9768
+ } catch {
9769
+ return { ok: false, error: NOT_RUNNING, answered: false };
9770
+ }
9771
+ }
9772
+ function describe2(share) {
9773
+ const [first, second] = share.titles;
9774
+ if (share.scope === "compare" && first !== void 0 && second !== void 0) {
9775
+ return `${first} and ${second}, side by side`;
9776
+ }
9777
+ if (share.scope === "direction" && first !== void 0) return first;
9778
+ const count = share.titles.length;
9779
+ return `the rail, ${count} ${count === 1 ? "direction" : "directions"}`;
9780
+ }
9781
+ function clock(at) {
9782
+ return new Date(at).toLocaleString([], { weekday: "short", hour: "numeric", minute: "2-digit" });
9783
+ }
9784
+ function tunnelLine(tunnel, installed) {
9785
+ switch (tunnel.status) {
9786
+ case "ready":
9787
+ return null;
9788
+ case "none":
9789
+ return installed.length > 0 ? "none, as asked; the link works on this machine, or through a tunnel you run yourself" : "no cloudflared or ngrok on this machine; the link works here, or through a tunnel you run yourself";
9790
+ case "starting":
9791
+ return `${tunnel.provider} is still starting; run npx leglas share again for the public link`;
9792
+ case "failed":
9793
+ return `${tunnel.provider} failed: ${tunnel.reason}`;
9794
+ }
9795
+ }
9796
+ async function runShare(options, deps) {
9797
+ const request = deps.fetch ?? fetch;
9798
+ const sleep = deps.sleep ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
9799
+ const fail = (error) => {
9800
+ if (options.json) deps.log(JSON.stringify({ ok: false, error }));
9801
+ else deps.error(error);
9802
+ return { exitCode: 1 };
9803
+ };
9804
+ const found = await findLeglas(options.cwd, options.port, request);
9805
+ if (!found.ok) return fail(found.error);
9806
+ if (options.stop) {
9807
+ const stopped = await post(
9808
+ found.base,
9809
+ "/leglas/api/share/stop",
9810
+ {},
9811
+ request,
9812
+ "Leglas could not stop sharing."
9813
+ );
9814
+ if (!stopped.ok) return fail(stopped.error);
9815
+ if (options.json) deps.log(JSON.stringify({ ok: true, stopped: true }));
9816
+ else deps.log(" stopped nothing is shared any more");
9817
+ return { exitCode: 0 };
9818
+ }
9819
+ const running = await readShare(found.base, request);
9820
+ if (!running.ok) return fail(running.error);
9821
+ const { installed } = running.value;
9822
+ const report = (share2, already, leftOut2) => {
9823
+ if (options.json) {
9824
+ deps.log(
9825
+ JSON.stringify({
9826
+ ok: true,
9827
+ alreadySharing: already,
9828
+ share: {
9829
+ scope: share2.scope,
9830
+ titles: share2.titles,
9831
+ reach: share2.reach,
9832
+ tunnel: share2.tunnel,
9833
+ links: share2.links
9834
+ },
9835
+ leftOut: leftOut2
9836
+ })
9837
+ );
9838
+ return { exitCode: 0 };
9839
+ }
9840
+ deps.log(` sharing ${describe2(share2)}${already ? " (already running)" : ""}`);
9841
+ if (share2.reach === "listed") {
9842
+ deps.log(" reach only the paths the share lists; allow more from the share panel");
9843
+ }
9844
+ for (const link of share2.links) {
9845
+ const label = link.name === "" ? "" : `${link.name} `;
9846
+ if (link.url !== null) deps.log(` link ${label}${link.url}`);
9847
+ else deps.log(` local ${label}${link.localUrl}`);
9848
+ }
9849
+ const tunnel = tunnelLine(share2.tunnel, installed);
9850
+ if (tunnel !== null) deps.log(` tunnel ${tunnel}`);
9851
+ const ends = share2.links.map((link) => link.expiresAt);
9852
+ if (ends.length > 0) deps.log(` until ${clock(Math.min(...ends))}`);
9853
+ for (const title of leftOut2) {
9854
+ deps.log(` left out ${title}: branch directions can't be shared yet`);
9855
+ }
9856
+ deps.log(" stop npx leglas share --stop");
9857
+ return { exitCode: 0 };
9858
+ };
9859
+ const loaded = await loadConfig(options.cwd);
9860
+ const local = await readLocalPreviews(options.cwd);
9861
+ const renames = await readRenames(options.cwd);
9862
+ const previews = [...loaded.config?.previews ?? [], ...local.previews];
9863
+ const known = [...new Set(previews.map((preview) => preview.title))];
9864
+ const titles = [];
9865
+ for (const asked of options.titles) {
9866
+ const resolved = resolveOrExplain(asked, known, renames);
9867
+ if (!resolved.ok) return fail(resolved.error);
9868
+ titles.push(resolved.title);
9869
+ }
9870
+ const [first, second] = titles;
9871
+ if (first !== void 0 && first === second) {
9872
+ return fail(`Both names are ${first}. Name two different directions to compare them.`);
9873
+ }
9874
+ const scope2 = titles.length === 0 ? "rail" : titles.length === 1 ? "direction" : "compare";
9875
+ const current = running.value.share;
9876
+ if (current !== null) {
9877
+ const same = titles.length === 0 || current.scope === scope2 && titles.length === current.titles.length && titles.every((title, index) => current.titles[index] === title);
9878
+ if (!same) {
9879
+ return fail(
9880
+ `Leglas is already sharing ${describe2(current)}. Stop it with npx leglas share --stop, then share again.`
9881
+ );
9882
+ }
9883
+ return report(current, true, []);
9884
+ }
9885
+ const branches = new Set(
9886
+ previews.flatMap((preview) => preview.branch === void 0 ? [] : [preview.title])
9887
+ );
9888
+ const shared = titles.length > 0 ? titles : known.filter((title) => !branches.has(title));
9889
+ const leftOut = titles.length > 0 ? [] : known.filter((title) => branches.has(title));
9890
+ if (shared.length === 0) {
9891
+ return fail("Nothing on the rail can be shared: branch directions can't be shared yet.");
9892
+ }
9893
+ const [, right] = shared;
9894
+ const body = {
9895
+ scope: scope2,
9896
+ titles: shared,
9897
+ layout: {
9898
+ order: shared,
9899
+ renames: Object.fromEntries(
9900
+ Object.entries(renames).filter(([title]) => shared.includes(title))
9901
+ ),
9902
+ collapsedFamilies: [],
9903
+ compare: scope2 === "compare" && right !== void 0 ? right : null,
9904
+ viewport: null
9905
+ },
9906
+ reach: options.reach,
9907
+ routes: []
9908
+ };
9909
+ if (options.tunnel !== null) body.tunnel = options.tunnel;
9910
+ const giveUp = async (error, known2 = true) => {
9911
+ const stopped = await post(found.base, "/leglas/api/share/stop", {}, request, "");
9912
+ const what = known2 ? "The share it started" : "A share";
9913
+ return fail(
9914
+ stopped.ok ? `${error} ${known2 ? "The share it started is stopped." : "Anything it started is stopped."}` : `${error} ${what} may still be running; stop it with npx leglas share --stop.`
9915
+ );
9916
+ };
9917
+ const created = await post(
9918
+ found.base,
9919
+ "/leglas/api/share",
9920
+ body,
9921
+ request,
9922
+ "Leglas could not start sharing."
9923
+ );
9924
+ if (!created.ok) {
9925
+ return created.answered ? fail(created.error) : giveUp("Leglas stopped answering while the share was starting.", false);
9926
+ }
9927
+ let share = isJsonObject(created.value) ? shareFrom(created.value.share) : null;
9928
+ if (share === null) {
9929
+ return giveUp("Leglas started a share this version of the command cannot read.");
9930
+ }
9931
+ for (let waited = 0; share.tunnel.status === "starting" && waited < TUNNEL_WAIT_MS; waited += TUNNEL_POLL_MS) {
9932
+ await sleep(TUNNEL_POLL_MS);
9933
+ const read = await readShare(found.base, request);
9934
+ if (!read.ok) return giveUp(read.error);
9935
+ if (read.value.share === null) {
9936
+ return fail("The share was stopped while its tunnel was starting.");
9937
+ }
9938
+ share = read.value.share;
9939
+ }
9940
+ return report(share, false, leftOut);
9941
+ }
9942
+
9943
+ // src/shutdown.ts
9944
+ var SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
9945
+ function installShutdown(stop, target = process) {
9946
+ let stopping = null;
9947
+ const shutdown = () => {
9948
+ stopping ??= stop();
9949
+ return stopping;
9950
+ };
9951
+ for (const signal of SHUTDOWN_SIGNALS) target.on(signal, () => void shutdown());
9952
+ return shutdown;
9953
+ }
9954
+
9179
9955
  // src/run-watch.ts
9180
- import { spawn as spawn3 } from "child_process";
9956
+ import { spawn as spawn4 } from "child_process";
9181
9957
  import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile14 } from "fs/promises";
9182
9958
  import { dirname as dirname12, join as join18 } from "path";
9183
9959
  var POLL_MS2 = 2e3;
@@ -9187,9 +9963,7 @@ async function saveTemplate(cwd, run4) {
9187
9963
  let config = {};
9188
9964
  try {
9189
9965
  const parsed = JSON.parse(await readFile16(path, "utf8"));
9190
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
9191
- config = parsed;
9192
- }
9966
+ if (isJsonObject(parsed)) config = parsed;
9193
9967
  } catch {
9194
9968
  }
9195
9969
  config.run = run4;
@@ -9205,7 +9979,7 @@ function spawnAgent(command, args, cwd) {
9205
9979
  settled = true;
9206
9980
  resolve5(outcome);
9207
9981
  };
9208
- const child = spawn3(command, args, { cwd, stdio: "inherit" });
9982
+ const child = spawn4(command, args, { cwd, stdio: "inherit" });
9209
9983
  child.on("error", (error) => settle({ ok: false, error: error.message }));
9210
9984
  child.on(
9211
9985
  "close",
@@ -9480,8 +10254,11 @@ function skipStartupCheck(env) {
9480
10254
  return env.CI !== void 0 && env.CI !== "" && env.CI !== "false" || !off(env.LEGLAS_NO_UPDATE_CHECK);
9481
10255
  }
9482
10256
  async function run3(options, deps) {
9483
- const loaded = await loadConfig(options.cwd);
9484
- const local = await readLocalPreviews(options.cwd);
10257
+ return runWithServices(options, deps);
10258
+ }
10259
+ async function runWithServices(options, deps, services = {}) {
10260
+ const loaded = await (services.loadConfig ?? loadConfig)(options.cwd);
10261
+ const local = await (services.readLocalPreviews ?? readLocalPreviews)(options.cwd);
9485
10262
  let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
9486
10263
  const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
9487
10264
  const previewErrors = [];
@@ -9532,8 +10309,8 @@ async function run3(options, deps) {
9532
10309
  const projectRoot = await realpath4(
9533
10310
  loaded.path === null ? options.cwd : dirname13(loaded.path)
9534
10311
  ).catch(() => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path)));
9535
- const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
9536
- const serverPromise = startServer({
10312
+ const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
10313
+ const serverOptions = {
9537
10314
  config,
9538
10315
  configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
9539
10316
  configWarnings,
@@ -9543,10 +10320,11 @@ async function run3(options, deps) {
9543
10320
  // directory does. Either way saved layout survives a port change.
9544
10321
  project: loaded.path ?? options.cwd,
9545
10322
  cwd: options.cwd,
9546
- leglasCommand: embeddedLeglasCommand(),
9547
- ...deps.updates === void 0 ? {} : { updates: deps.updates },
9548
- ...options.port === void 0 ? {} : { port: options.port }
9549
- });
10323
+ leglasCommand: embeddedLeglasCommand()
10324
+ };
10325
+ if (deps.updates !== void 0) serverOptions.updates = deps.updates;
10326
+ if (options.port !== void 0) serverOptions.port = options.port;
10327
+ const serverPromise = (services.startServer ?? startServer)(serverOptions);
9550
10328
  const [server, warning] = await Promise.all([serverPromise, ownerWarning]);
9551
10329
  if (warning !== null) configWarnings.push(warning);
9552
10330
  const url = `${server.url}${LEGLAS_PREFIX}`;
@@ -9635,6 +10413,7 @@ export {
9635
10413
  commandFor,
9636
10414
  detectFramework,
9637
10415
  findBrowser,
10416
+ installShutdown,
9638
10417
  isOwnCapture,
9639
10418
  nextRequest,
9640
10419
  parseArgs,
@@ -9655,6 +10434,7 @@ export {
9655
10434
  runList,
9656
10435
  runNew,
9657
10436
  runRequests,
10437
+ runShare,
9658
10438
  runShow,
9659
10439
  runWatch,
9660
10440
  surfaceSlug