leglas 1.1.2 → 1.2.0

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
  }
@@ -855,7 +928,7 @@ function frameFor(title, requests) {
855
928
  return found;
856
929
  }
857
930
  function askedOf(title, requests) {
858
- return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
931
+ return requests.flatMap((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "" ? [request.intent.trim()] : []);
859
932
  }
860
933
  function composeEntry(input) {
861
934
  const slug = `${input.date}-${slugify(input.surface)}`;
@@ -915,13 +988,27 @@ function composeEntry(input) {
915
988
  `, pictures };
916
989
  }
917
990
 
991
+ // ../server/dist/json.js
992
+ function parseJson(text2) {
993
+ return JSON.parse(text2);
994
+ }
995
+ function isJsonRecord(value) {
996
+ return typeof value === "object" && value !== null && !Array.isArray(value);
997
+ }
998
+ function isString(value) {
999
+ return typeof value === "string";
1000
+ }
1001
+ function isNumber(value) {
1002
+ return typeof value === "number";
1003
+ }
1004
+ function isBoolean(value) {
1005
+ return typeof value === "boolean";
1006
+ }
1007
+
918
1008
  // ../server/dist/config/config.js
919
1009
  var DEFAULT_DEV_SERVER = "http://localhost:3000";
920
1010
  var DEFAULT_INSTALL_COMMAND = "npm install";
921
1011
  var IMPLICIT_PREVIEW = { title: "App", url: "/" };
922
- function isRecord(value) {
923
- return typeof value === "object" && value !== null && !Array.isArray(value);
924
- }
925
1012
  function isValidOrigin(value) {
926
1013
  try {
927
1014
  const url = new URL(value);
@@ -951,11 +1038,11 @@ function normalizeConfig(raw, options = {}) {
951
1038
  const requireDevCommand = options.requireDevCommand ?? true;
952
1039
  const errors = [];
953
1040
  const source = raw === void 0 || raw === null ? {} : raw;
954
- if (!isRecord(source)) {
1041
+ if (!isJsonRecord(source)) {
955
1042
  return { config: null, errors: ["Config must export an object."] };
956
1043
  }
957
1044
  const devServer = source["devServer"] ?? DEFAULT_DEV_SERVER;
958
- if (typeof devServer !== "string" || !isValidOrigin(devServer)) {
1045
+ if (!isString(devServer) || !isValidOrigin(devServer)) {
959
1046
  errors.push(`devServer must be an http(s) URL, received ${JSON.stringify(devServer)}.`);
960
1047
  }
961
1048
  const rawPreviews = source["previews"] ?? [IMPLICIT_PREVIEW];
@@ -967,14 +1054,14 @@ function normalizeConfig(raw, options = {}) {
967
1054
  const seenTitles = /* @__PURE__ */ new Set();
968
1055
  rawPreviews.forEach((entry, index) => {
969
1056
  const at = `previews[${index}]`;
970
- if (!isRecord(entry)) {
1057
+ if (!isJsonRecord(entry)) {
971
1058
  errors.push(`${at} must be an object.`);
972
1059
  return;
973
1060
  }
974
1061
  const title = entry["title"];
975
1062
  const url = entry["url"];
976
1063
  const file = entry["file"];
977
- if (typeof title !== "string" || title.trim() === "") {
1064
+ if (!isString(title) || title.trim() === "") {
978
1065
  errors.push(`${at} needs a title; the rail has nothing to show without one.`);
979
1066
  } else if (seenTitles.has(title)) {
980
1067
  errors.push(`${at} repeats the title ${JSON.stringify(title)}; titles must be unique.`);
@@ -982,22 +1069,22 @@ function normalizeConfig(raw, options = {}) {
982
1069
  seenTitles.add(title);
983
1070
  }
984
1071
  if (file !== void 0) {
985
- if (typeof file !== "string" || !isSafePreviewFile(file)) {
1072
+ if (!isString(file) || !isSafePreviewFile(file)) {
986
1073
  errors.push(`${at} has an unusable file ${JSON.stringify(file)}; use a path inside the project, like "directions/hero.html".`);
987
1074
  }
988
1075
  if (url !== void 0) {
989
1076
  errors.push(`${at} names a file and a url; a file preview's url is assigned by Leglas.`);
990
1077
  }
991
- } else if (typeof url !== "string" || url.trim() === "") {
1078
+ } else if (!isString(url) || url.trim() === "") {
992
1079
  errors.push(`${at} needs a url.`);
993
1080
  } else if (!isValidPreviewUrl(url)) {
994
1081
  errors.push(`${at} has url ${JSON.stringify(url)}; use a root-relative path ("/pricing") or a full URL.`);
995
1082
  }
996
1083
  const branch = entry["branch"];
997
1084
  if (branch !== void 0) {
998
- if (typeof branch !== "string" || !isSafeBranch(branch)) {
1085
+ if (!isString(branch) || !isSafeBranch(branch)) {
999
1086
  errors.push(`${at} has an unusable branch ${JSON.stringify(branch)}; use a plain git branch name.`);
1000
- } else if (typeof url === "string" && !url.startsWith("/")) {
1087
+ } else if (isString(url) && !url.startsWith("/")) {
1001
1088
  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
1089
  }
1003
1090
  if (file !== void 0) {
@@ -1005,54 +1092,59 @@ function normalizeConfig(raw, options = {}) {
1005
1092
  }
1006
1093
  }
1007
1094
  const basedOn = entry["basedOn"];
1008
- if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
1095
+ if (basedOn !== void 0 && (!isString(basedOn) || basedOn.trim() === "")) {
1009
1096
  errors.push(`${at} has a basedOn that is not a direction title.`);
1010
1097
  }
1011
1098
  const askedFor = entry["askedFor"];
1012
- if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
1099
+ if (askedFor !== void 0 && (!isString(askedFor) || askedFor.trim() === "")) {
1013
1100
  errors.push(`${at} has an askedFor that is not a change request.`);
1014
1101
  }
1015
1102
  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
- });
1103
+ const preview = {
1104
+ title: isString(title) ? title : "",
1105
+ url: isString(url) ? url : "",
1106
+ note: isString(entry["note"]) ? entry["note"] : void 0,
1107
+ tags: Array.isArray(tags) ? tags.filter((tag) => isString(tag)) : []
1108
+ };
1109
+ if (isString(branch))
1110
+ preview.branch = branch;
1111
+ if (isString(file))
1112
+ preview.file = file;
1113
+ if (isString(basedOn) && basedOn.trim() !== "")
1114
+ preview.basedOn = basedOn;
1115
+ if (isString(askedFor) && askedFor.trim() !== "")
1116
+ preview.askedFor = askedFor;
1117
+ previews.push(preview);
1026
1118
  });
1027
1119
  const devCommand = source["devCommand"];
1028
- if (devCommand !== void 0 && typeof devCommand !== "string") {
1120
+ if (devCommand !== void 0 && !isString(devCommand)) {
1029
1121
  errors.push("devCommand must be a string.");
1030
- } else if (typeof devCommand === "string" && !devCommand.includes("{port}")) {
1122
+ } else if (isString(devCommand) && !devCommand.includes("{port}")) {
1031
1123
  errors.push(`devCommand must include {port}, so Leglas can start each checkout on a free port. Received ${JSON.stringify(devCommand)}.`);
1032
1124
  }
1033
1125
  if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
1034
1126
  errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
1035
1127
  }
1036
1128
  const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
1037
- if (typeof logDir !== "string" || logDir.trim() === "") {
1129
+ if (!isString(logDir) || logDir.trim() === "") {
1038
1130
  errors.push("logDir must be a non-empty string.");
1039
1131
  }
1040
1132
  const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
1041
- if (typeof installCommand !== "string" || installCommand.trim() === "") {
1133
+ if (!isString(installCommand) || installCommand.trim() === "") {
1042
1134
  errors.push("installCommand must be a non-empty string.");
1043
1135
  }
1044
1136
  const scanPreviews = source["scanPreviews"] ?? true;
1045
- if (typeof scanPreviews !== "boolean") {
1137
+ if (!isBoolean(scanPreviews)) {
1046
1138
  errors.push("scanPreviews must be a boolean.");
1047
1139
  }
1048
- if (errors.length > 0)
1140
+ if (errors.length > 0 || !isString(devServer) || !isBoolean(scanPreviews) || !isString(installCommand) || !isString(logDir))
1049
1141
  return { config: null, errors };
1050
1142
  return {
1051
1143
  config: {
1052
1144
  devServer,
1053
1145
  previews,
1054
1146
  scanPreviews,
1055
- devCommand: typeof devCommand === "string" ? devCommand : void 0,
1147
+ devCommand: isString(devCommand) ? devCommand : void 0,
1056
1148
  installCommand,
1057
1149
  logDir
1058
1150
  },
@@ -1068,17 +1160,17 @@ function tokenize(template) {
1068
1160
  const tokens = [];
1069
1161
  let current = "";
1070
1162
  let started = false;
1071
- let quote = null;
1163
+ let quote2 = null;
1072
1164
  for (const character of template) {
1073
- if (quote !== null) {
1074
- if (character === quote)
1075
- quote = null;
1165
+ if (quote2 !== null) {
1166
+ if (character === quote2)
1167
+ quote2 = null;
1076
1168
  else
1077
1169
  current += character;
1078
1170
  continue;
1079
1171
  }
1080
1172
  if (character === '"' || character === "'") {
1081
- quote = character;
1173
+ quote2 = character;
1082
1174
  started = true;
1083
1175
  continue;
1084
1176
  }
@@ -1092,8 +1184,8 @@ function tokenize(template) {
1092
1184
  current += character;
1093
1185
  started = true;
1094
1186
  }
1095
- if (quote !== null) {
1096
- return { ok: false, error: `The agent command has an unclosed ${quote} quote.` };
1187
+ if (quote2 !== null) {
1188
+ return { ok: false, error: `The agent command has an unclosed ${quote2} quote.` };
1097
1189
  }
1098
1190
  if (started)
1099
1191
  tokens.push(current);
@@ -1190,7 +1282,7 @@ var KNOWN_AGENTS = {
1190
1282
  ],
1191
1283
  activityVerified: true,
1192
1284
  // Every stream-json event names its session.
1193
- sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
1285
+ sessionFrom: (event) => isString(event.session_id) && event.session_id !== "" ? event.session_id : null,
1194
1286
  authArgs: ["auth", "status"],
1195
1287
  // `claude auth status` prints JSON with a loggedIn boolean. Only that
1196
1288
  // field decides; any other shape stays unknown.
@@ -1253,7 +1345,7 @@ var KNOWN_AGENTS = {
1253
1345
  ...images.flatMap((image) => ["-i", image]),
1254
1346
  prompt
1255
1347
  ],
1256
- sessionFrom: (event) => event.type === "thread.started" && typeof event.thread_id === "string" ? event.thread_id : null,
1348
+ sessionFrom: (event) => event.type === "thread.started" && isString(event.thread_id) ? event.thread_id : null,
1257
1349
  activityVerified: true,
1258
1350
  authArgs: ["login", "status"],
1259
1351
  // `codex login status` exits 0 when logged in and nonzero when not.
@@ -1291,7 +1383,7 @@ var KNOWN_AGENTS = {
1291
1383
  "stream-json",
1292
1384
  "--trust"
1293
1385
  ],
1294
- sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
1386
+ sessionFrom: (event) => isString(event.session_id) && event.session_id !== "" ? event.session_id : null,
1295
1387
  // Read against cursor-agent 2026.09.02: every event carries the id, a
1296
1388
  // resume in this argument order answers under the same id and remembers
1297
1389
  // the earlier turn.
@@ -1383,7 +1475,7 @@ function agentSearchPath(env = process.env, platform = process.platform) {
1383
1475
  platform === "darwin" ? "/usr/local/bin" : void 0,
1384
1476
  platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
1385
1477
  platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
1386
- ].filter((entry) => typeof entry === "string" && entry !== "");
1478
+ ].filter((entry) => isString(entry) && entry !== "");
1387
1479
  return [...new Set(candidates)].join(delimiter);
1388
1480
  }
1389
1481
  function agentEnvironment(env = process.env) {
@@ -1427,23 +1519,23 @@ async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
1427
1519
  }));
1428
1520
  }
1429
1521
  function record(value) {
1430
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1522
+ return isJsonRecord(value) ? value : null;
1431
1523
  }
1432
1524
  function shownPath(value, cwd) {
1433
- if (typeof value !== "string" || value === "")
1525
+ if (!isString(value) || value === "")
1434
1526
  return null;
1435
1527
  if (!isAbsolute(value))
1436
1528
  return value;
1437
1529
  return relative(cwd, value) || ".";
1438
1530
  }
1439
1531
  function shownCommand(value) {
1440
- let command = Array.isArray(value) ? value.filter((part) => typeof part === "string").join(" ") : typeof value === "string" ? value : "";
1532
+ let command = Array.isArray(value) ? value.filter((part) => isString(part)).join(" ") : isString(value) ? value : "";
1441
1533
  command = command.trim();
1442
1534
  const wrapped = /^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+([\s\S]*)$/.exec(command);
1443
1535
  if (wrapped?.[1] !== void 0) {
1444
1536
  command = wrapped[1].trim();
1445
- const quote = command[0];
1446
- if ((quote === "'" || quote === '"') && command.endsWith(quote) && command.length > 1) {
1537
+ const quote2 = command[0];
1538
+ if ((quote2 === "'" || quote2 === '"') && command.endsWith(quote2) && command.length > 1) {
1447
1539
  command = command.slice(1, -1);
1448
1540
  }
1449
1541
  }
@@ -1460,7 +1552,7 @@ function claudeActivity(event, cwd) {
1460
1552
  return null;
1461
1553
  for (const rawBlock of message2.content) {
1462
1554
  const block = record(rawBlock);
1463
- if (block?.type !== "tool_use" || typeof block.name !== "string")
1555
+ if (block?.type !== "tool_use" || !isString(block.name))
1464
1556
  continue;
1465
1557
  const input = record(block.input);
1466
1558
  if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name)) {
@@ -1526,7 +1618,7 @@ function cursorActivity(event, cwd) {
1526
1618
  function activityFrom(agent, line, cwd = process.cwd()) {
1527
1619
  let event;
1528
1620
  try {
1529
- event = record(JSON.parse(line));
1621
+ event = record(parseJson(line));
1530
1622
  } catch {
1531
1623
  return null;
1532
1624
  }
@@ -1545,7 +1637,7 @@ function sessionFrom(agent, line) {
1545
1637
  return null;
1546
1638
  let event;
1547
1639
  try {
1548
- event = record(JSON.parse(line));
1640
+ event = record(parseJson(line));
1549
1641
  } catch {
1550
1642
  return null;
1551
1643
  }
@@ -1558,29 +1650,29 @@ function retryFrom(agent, line) {
1558
1650
  return null;
1559
1651
  let event;
1560
1652
  try {
1561
- event = record(JSON.parse(line));
1653
+ event = record(parseJson(line));
1562
1654
  } catch {
1563
1655
  return null;
1564
1656
  }
1565
1657
  if (event === null || event.type !== "system" || event.subtype !== "api_retry")
1566
1658
  return null;
1567
- const attempt = typeof event.attempt === "number" ? event.attempt : 1;
1659
+ const attempt = isNumber(event.attempt) ? event.attempt : 1;
1568
1660
  return {
1569
1661
  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
1662
+ max: isNumber(event.max_retries) ? event.max_retries : null,
1663
+ status: isNumber(event.error_status) ? event.error_status : null,
1664
+ reason: isString(event.error) && event.error !== "" ? event.error.toLowerCase() : null
1573
1665
  };
1574
1666
  }
1575
1667
  function isAgentChoice(value) {
1576
- return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
1668
+ return value === "custom" || isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
1577
1669
  }
1578
1670
  function isAgentEffort(value) {
1579
- return typeof value === "string" && AGENT_EFFORTS.includes(value);
1671
+ return isString(value) && AGENT_EFFORTS.some((effort) => effort === value);
1580
1672
  }
1581
1673
  async function readWatchConfig(cwd) {
1582
1674
  try {
1583
- const parsed = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
1675
+ const parsed = parseJson(await readFile(join(cwd, WATCH_PATH), "utf8"));
1584
1676
  return record(parsed) ?? {};
1585
1677
  } catch {
1586
1678
  return {};
@@ -1593,7 +1685,7 @@ async function readAgentChoice(cwd) {
1593
1685
  return {
1594
1686
  agent,
1595
1687
  effort: agent !== null && agent !== "custom" && isAgentEffort(efforts?.[agent]) ? efforts[agent] : null,
1596
- run: typeof config.run === "string" && config.run !== "" ? config.run : null
1688
+ run: isString(config.run) && config.run !== "" ? config.run : null
1597
1689
  };
1598
1690
  }
1599
1691
  async function saveAgentChoice(cwd, choice) {
@@ -1739,7 +1831,7 @@ async function loadConfig(cwd) {
1739
1831
  const message2 = error instanceof Error ? error.message : String(error);
1740
1832
  return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
1741
1833
  }
1742
- const result = normalizeConfig(exported);
1834
+ const result = isJsonRecord(exported) || exported === void 0 || exported === null ? normalizeConfig(exported ?? void 0) : { config: null, errors: ["Config must export an object."] };
1743
1835
  return {
1744
1836
  config: result.config,
1745
1837
  errors: result.errors.map((error) => `${label}: ${error}`),
@@ -1757,7 +1849,7 @@ async function readLocalPreviews(cwd) {
1757
1849
  try {
1758
1850
  raw = await readFile3(path, "utf8");
1759
1851
  } catch (error) {
1760
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
1852
+ const code = error instanceof Error && "code" in error && isString(error.code) ? error.code : null;
1761
1853
  if (code === "ENOENT") {
1762
1854
  return { previews: [], errors: [] };
1763
1855
  }
@@ -1770,7 +1862,7 @@ async function readLocalPreviews(cwd) {
1770
1862
  }
1771
1863
  let parsed;
1772
1864
  try {
1773
- parsed = JSON.parse(raw);
1865
+ parsed = parseJson(raw);
1774
1866
  } catch (error) {
1775
1867
  return {
1776
1868
  previews: [],
@@ -1803,16 +1895,21 @@ async function addLocalPreview(cwd, input, shared) {
1803
1895
  error: `A preview called ${JSON.stringify(input.title)} already exists. Titles identify previews, so they have to be unique.`
1804
1896
  };
1805
1897
  }
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
- };
1898
+ const candidate = { title: input.title };
1899
+ if (input.url !== void 0)
1900
+ candidate.url = input.url;
1901
+ if (input.note !== void 0)
1902
+ candidate.note = input.note;
1903
+ if (input.tags !== void 0)
1904
+ candidate.tags = [...input.tags];
1905
+ if (input.branch !== void 0)
1906
+ candidate.branch = input.branch;
1907
+ if (input.file !== void 0)
1908
+ candidate.file = input.file;
1909
+ if (input.basedOn !== void 0)
1910
+ candidate.basedOn = input.basedOn;
1911
+ if (input.askedFor !== void 0)
1912
+ candidate.askedFor = input.askedFor;
1816
1913
  const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
1817
1914
  if (check.config === null) {
1818
1915
  return { ok: false, error: check.errors.join(" ") };
@@ -1892,7 +1989,7 @@ function createProxyHandler(options) {
1892
1989
  res.once("close", close);
1893
1990
  const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
1894
1991
  const headers = { ...upstreamRes.headers };
1895
- const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin2);
1992
+ const location = rewriteLocation(isString(headers.location) ? headers.location : void 0, publicOrigin2);
1896
1993
  if (location !== void 0)
1897
1994
  headers.location = location;
1898
1995
  res.writeHead(upstreamRes.statusCode ?? 502, headers);
@@ -1961,7 +2058,7 @@ function startProxyServer(options) {
1961
2058
  });
1962
2059
  const server = http.createServer((req, res) => {
1963
2060
  const address = server.address();
1964
- const port = typeof address === "object" && address !== null ? address.port : 0;
2061
+ const port = address !== null && !isString(address) ? address.port : 0;
1965
2062
  handler.request(req, res, `http://127.0.0.1:${port}`);
1966
2063
  });
1967
2064
  const sockets = /* @__PURE__ */ new Set();
@@ -1977,7 +2074,7 @@ function startProxyServer(options) {
1977
2074
  const onListening = () => {
1978
2075
  server.removeListener("error", onError);
1979
2076
  const address = server.address();
1980
- const port = typeof address === "object" && address !== null ? address.port : 0;
2077
+ const port = address !== null && !isString(address) ? address.port : 0;
1981
2078
  let closed = null;
1982
2079
  resolve5({
1983
2080
  active: () => open > 0,
@@ -2109,7 +2206,7 @@ function findBrowser(search = {}) {
2109
2206
  const readdir4 = search.readdir ?? readableDirectories;
2110
2207
  const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
2111
2208
  for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
2112
- if (typeof candidate === "string" && candidate !== "" && exists(candidate))
2209
+ if (isString(candidate) && candidate !== "" && exists(candidate))
2113
2210
  return candidate;
2114
2211
  }
2115
2212
  const caches = cacheRoots(platform, home, readdir4);
@@ -2135,7 +2232,7 @@ function findBrowser(search = {}) {
2135
2232
  return installed;
2136
2233
  }
2137
2234
  if (platform === "win32") {
2138
- const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => typeof entry === "string" && entry !== "");
2235
+ const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => isString(entry) && entry !== "");
2139
2236
  const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) => join4(root, browser))));
2140
2237
  if (installed !== null)
2141
2238
  return installed;
@@ -2218,7 +2315,7 @@ function endpoint(process2, timeoutMs) {
2218
2315
  return;
2219
2316
  settled = true;
2220
2317
  clearTimeout(timer);
2221
- if (typeof value === "string")
2318
+ if (isString(value))
2222
2319
  resolve5(value);
2223
2320
  else
2224
2321
  reject(value);
@@ -2247,12 +2344,15 @@ var OWNER_FILE = "leglas-owner.json";
2247
2344
  var PROFILE_PREFIX = "leglas-browser-";
2248
2345
  var RECORD_GRACE_MS = 6e4;
2249
2346
  var REAP_CLOSE_MS = 2e3;
2347
+ function permissionDenied(cause) {
2348
+ return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EPERM";
2349
+ }
2250
2350
  function livePid(pid) {
2251
2351
  try {
2252
2352
  nodeProcess.kill(pid, 0);
2253
2353
  return true;
2254
2354
  } catch (error) {
2255
- return typeof error === "object" && error !== null && error.code === "EPERM";
2355
+ return permissionDenied(error);
2256
2356
  }
2257
2357
  }
2258
2358
  async function closeOrphan(url, connect) {
@@ -2282,7 +2382,7 @@ async function closeOrphan(url, connect) {
2282
2382
  }
2283
2383
  async function reapOrphanedBrowsers(deps = {}) {
2284
2384
  const root = deps.tmpdir ?? osTmpdir();
2285
- const clock = deps.now ?? (() => Date.now());
2385
+ const clock2 = deps.now ?? (() => Date.now());
2286
2386
  const list = deps.list ?? (async (path) => (await readdir(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
2287
2387
  const read = deps.read ?? ((path) => readFile4(path, "utf8"));
2288
2388
  const alive = deps.alive ?? livePid;
@@ -2315,15 +2415,16 @@ async function reapOrphanedBrowsers(deps = {}) {
2315
2415
  let record3 = null;
2316
2416
  try {
2317
2417
  const raw = await read(join4(directory, OWNER_FILE));
2318
- record3 = raw === null ? null : JSON.parse(raw);
2418
+ const parsed = raw === null ? null : parseJson(raw);
2419
+ record3 = isJsonRecord(parsed) ? parsed : null;
2319
2420
  } catch {
2320
2421
  }
2321
- const owner = typeof record3?.owner === "number" ? record3.owner : null;
2422
+ const owner = isNumber(record3?.owner) ? record3.owner : null;
2322
2423
  if (owner !== null && alive(owner))
2323
2424
  continue;
2324
- if (owner === null && clock() - details.createdAt < RECORD_GRACE_MS)
2425
+ if (owner === null && clock2() - details.createdAt < RECORD_GRACE_MS)
2325
2426
  continue;
2326
- const endpoint2 = typeof record3?.ws === "string" && record3.ws !== "" ? record3.ws : null;
2427
+ const endpoint2 = isString(record3?.ws) && record3.ws !== "" ? record3.ws : null;
2327
2428
  if (endpoint2 !== null && await closeOrphan(endpoint2, connect))
2328
2429
  reaped += 1;
2329
2430
  await remove(directory).catch(() => {
@@ -2332,7 +2433,7 @@ async function reapOrphanedBrowsers(deps = {}) {
2332
2433
  return reaped;
2333
2434
  }
2334
2435
  async function launchBrowser(executable, options = {}) {
2335
- const spawn4 = options.spawn ?? nodeSpawn;
2436
+ const spawn5 = options.spawn ?? nodeSpawn;
2336
2437
  const connect = options.connect ?? connectWebSocket;
2337
2438
  const commandTimeoutMs = options.commandTimeoutMs ?? 3e4;
2338
2439
  const userDataDir = join4(options.tmpdir ?? osTmpdir(), `leglas-browser-${randomBytes(8).toString("hex")}`);
@@ -2345,7 +2446,7 @@ async function launchBrowser(executable, options = {}) {
2345
2446
  }
2346
2447
  };
2347
2448
  owned({});
2348
- const process2 = spawn4(executable, [
2449
+ const process2 = spawn5(executable, [
2349
2450
  "--headless=new",
2350
2451
  "--remote-debugging-port=0",
2351
2452
  `--user-data-dir=${userDataDir}`,
@@ -2423,11 +2524,14 @@ async function launchBrowser(executable, options = {}) {
2423
2524
  socket.onMessage((text2) => {
2424
2525
  let message2;
2425
2526
  try {
2426
- message2 = JSON.parse(text2);
2527
+ const parsed = parseJson(text2);
2528
+ if (!isJsonRecord(parsed))
2529
+ return;
2530
+ message2 = parsed;
2427
2531
  } catch {
2428
2532
  return;
2429
2533
  }
2430
- if (typeof message2.id === "number") {
2534
+ if (isNumber(message2.id)) {
2431
2535
  const command = pending.get(message2.id);
2432
2536
  if (command === void 0)
2433
2537
  return;
@@ -2435,15 +2539,15 @@ async function launchBrowser(executable, options = {}) {
2435
2539
  clearTimeout(command.timer);
2436
2540
  const error = message2.error;
2437
2541
  if (error !== void 0) {
2438
- command.reject(new Error(typeof error.message === "string" ? error.message : "CDP command failed."));
2542
+ command.reject(new Error(isJsonRecord(error) && isString(error.message) ? error.message : "CDP command failed."));
2439
2543
  } else {
2440
2544
  command.resolve(message2.result);
2441
2545
  }
2442
2546
  return;
2443
2547
  }
2444
- if (typeof message2.method !== "string")
2548
+ if (!isString(message2.method))
2445
2549
  return;
2446
- const sessionId = typeof message2.sessionId === "string" ? message2.sessionId : "";
2550
+ const sessionId = isString(message2.sessionId) ? message2.sessionId : "";
2447
2551
  const key = `${sessionId}\0${message2.method}`;
2448
2552
  for (const listener of listeners.get(key) ?? [])
2449
2553
  listener(message2.params ?? {});
@@ -2466,17 +2570,16 @@ async function launchBrowser(executable, options = {}) {
2466
2570
  }, commandTimeoutMs);
2467
2571
  timer.unref?.();
2468
2572
  pending.set(id, {
2573
+ // SAFETY: The pending command id pairs this CDP result with the method and result type requested by its caller.
2469
2574
  resolve: (value) => resolve5(value),
2470
2575
  reject,
2471
2576
  timer
2472
2577
  });
2473
2578
  try {
2474
- socket.send(JSON.stringify({
2475
- id,
2476
- method,
2477
- params,
2478
- ...sessionId === void 0 ? {} : { sessionId }
2479
- }));
2579
+ const message2 = { id, method, params };
2580
+ if (sessionId !== void 0)
2581
+ message2.sessionId = sessionId;
2582
+ socket.send(JSON.stringify(message2));
2480
2583
  } catch {
2481
2584
  clearTimeout(timer);
2482
2585
  pending.delete(id);
@@ -2561,36 +2664,37 @@ function createBrowserPool(options = {}) {
2561
2664
  const launch = options.launch ?? launchBrowser;
2562
2665
  const idleMs = options.idleMs ?? 6e4;
2563
2666
  const setLater = options.setTimeout ?? ((callback, milliseconds) => {
2564
- const timer2 = setTimeout(callback, milliseconds);
2565
- timer2.unref?.();
2566
- return timer2;
2667
+ const timer = setTimeout(callback, milliseconds);
2668
+ timer.unref?.();
2669
+ return timer;
2567
2670
  });
2568
2671
  const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
2569
2672
  let browser = null;
2570
2673
  let exposed = null;
2571
2674
  let working = 0;
2572
2675
  let launching = null;
2573
- let timer = null;
2676
+ let cancelTimer = null;
2574
2677
  let lastReason = null;
2575
2678
  let closed = false;
2576
2679
  const clearIdle = () => {
2577
- if (timer === null)
2680
+ if (cancelTimer === null)
2578
2681
  return;
2579
- clearLater(timer);
2580
- timer = null;
2682
+ cancelTimer();
2683
+ cancelTimer = null;
2581
2684
  };
2582
2685
  const scheduleIdle = () => {
2583
2686
  clearIdle();
2584
2687
  if (browser === null || closed || working > 0)
2585
2688
  return;
2586
- timer = setLater(() => {
2587
- timer = null;
2689
+ const handle = setLater(() => {
2690
+ cancelTimer = null;
2588
2691
  const retiring = browser;
2589
2692
  browser = null;
2590
2693
  exposed = null;
2591
2694
  void retiring?.close().catch(() => {
2592
2695
  });
2593
2696
  }, idleMs);
2697
+ cancelTimer = () => clearLater(handle);
2594
2698
  };
2595
2699
  const wrap = (launched) => ({
2596
2700
  withPage: async (work) => {
@@ -2640,8 +2744,8 @@ function createBrowserPool(options = {}) {
2640
2744
  lastReason = null;
2641
2745
  scheduleIdle();
2642
2746
  return exposed;
2643
- }).catch((error) => {
2644
- lastReason = error instanceof Error ? error.message : String(error);
2747
+ }).catch((cause) => {
2748
+ lastReason = cause instanceof Error ? cause.message : String(cause);
2645
2749
  return null;
2646
2750
  }).finally(() => {
2647
2751
  if (launching === attempt)
@@ -2747,9 +2851,9 @@ function bounded(work, milliseconds, fallback) {
2747
2851
  void work.then((value) => {
2748
2852
  clearTimeout(timer);
2749
2853
  resolve5(value);
2750
- }, (error) => {
2854
+ }, (cause) => {
2751
2855
  clearTimeout(timer);
2752
- reject(error);
2856
+ reject(cause);
2753
2857
  });
2754
2858
  });
2755
2859
  }
@@ -2764,9 +2868,9 @@ function abortable(work, signal) {
2764
2868
  void work.then((value) => {
2765
2869
  signal.removeEventListener("abort", abort);
2766
2870
  resolve5(value);
2767
- }, (error) => {
2871
+ }, (cause) => {
2768
2872
  signal.removeEventListener("abort", abort);
2769
- reject(error);
2873
+ reject(cause);
2770
2874
  });
2771
2875
  });
2772
2876
  }
@@ -2775,10 +2879,9 @@ function resultValue(response) {
2775
2879
  return result !== void 0 && "value" in result ? result.value : null;
2776
2880
  }
2777
2881
  function validBox(value) {
2778
- if (typeof value !== "object" || value === null)
2882
+ if (!isJsonRecord(value))
2779
2883
  return false;
2780
- const box = value;
2781
- return [box.x, box.y, box.width, box.height].every((entry) => typeof entry === "number" && Number.isFinite(entry));
2884
+ return [value.x, value.y, value.width, value.height].every((entry) => isNumber(entry) && Number.isFinite(entry));
2782
2885
  }
2783
2886
  function locatorExpression(focus) {
2784
2887
  return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
@@ -2802,7 +2905,7 @@ async function render(page, input) {
2802
2905
  if (documentStatus !== null || params?.type !== "Document")
2803
2906
  return;
2804
2907
  const status = params?.response?.status;
2805
- if (typeof status === "number")
2908
+ if (isNumber(status))
2806
2909
  documentStatus = status;
2807
2910
  }),
2808
2911
  page.on("Runtime.exceptionThrown", (params) => remember(params?.exceptionDetails?.exception?.description ?? params?.exceptionDetails?.text)),
@@ -2834,7 +2937,7 @@ async function render(page, input) {
2834
2937
  const stopLoad = page.on("Page.loadEventFired", () => loaded());
2835
2938
  unlisten.push(stopLoad);
2836
2939
  const navigation = await page.send("Page.navigate", { url: input.url });
2837
- if (typeof navigation.errorText === "string" && navigation.errorText !== "") {
2940
+ if (isString(navigation.errorText) && navigation.errorText !== "") {
2838
2941
  throw new Error(`The page did not load: ${navigation.errorText}`);
2839
2942
  }
2840
2943
  await bounded(load, input.timeoutMs ?? 15e3, void 0);
@@ -3071,13 +3174,15 @@ async function moveReference(cwd, id, destinationDir, index) {
3071
3174
  };
3072
3175
  }
3073
3176
  function focusOf(note) {
3074
- return {
3177
+ const focus = {
3075
3178
  selector: note.anchor.selector,
3076
3179
  text: note.anchor.text,
3077
3180
  tag: note.anchor.tag,
3078
- ...note.anchor.region === void 0 ? {} : { region: note.anchor.region },
3079
3181
  rect: note.anchor.rect
3080
3182
  };
3183
+ if (note.anchor.region !== void 0)
3184
+ focus.region = note.anchor.region;
3185
+ return focus;
3081
3186
  }
3082
3187
  async function attachRequest(cwd, requestId, input, deps) {
3083
3188
  const capture = deps.capture ?? capturePage;
@@ -3104,8 +3209,8 @@ async function attachRequest(cwd, requestId, input, deps) {
3104
3209
  finishDeadline();
3105
3210
  }, deadlineMs);
3106
3211
  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);
3212
+ for (const [index, referenceId] of input.references.entries()) {
3213
+ const reference = await moveReference(cwd, referenceId, destination, index + 1).catch(() => null);
3109
3214
  if (reference !== null)
3110
3215
  references.push(reference);
3111
3216
  }
@@ -3286,7 +3391,7 @@ async function freePort() {
3286
3391
  probe2.once("error", reject);
3287
3392
  probe2.listen(0, "127.0.0.1", () => {
3288
3393
  const address = probe2.address();
3289
- const port = typeof address === "object" && address !== null ? address.port : 0;
3394
+ const port = address !== null && !isString(address) ? address.port : 0;
3290
3395
  probe2.close(() => resolve5(port));
3291
3396
  });
3292
3397
  });
@@ -3519,9 +3624,9 @@ function createBranchRegistry(options) {
3519
3624
  });
3520
3625
  throw error;
3521
3626
  }
3522
- }).catch((error) => transition(title, {
3627
+ }).catch((cause) => transition(title, {
3523
3628
  status: "failed",
3524
- reason: error instanceof Error ? error.message : String(error)
3629
+ reason: cause instanceof Error ? cause.message : String(cause)
3525
3630
  })).finally(() => {
3526
3631
  inflight.delete(title);
3527
3632
  });
@@ -3537,8 +3642,8 @@ function createBranchRegistry(options) {
3537
3642
  return stopPromise;
3538
3643
  closed = true;
3539
3644
  clearInterval(sweepTimer);
3540
- stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
3541
- await Promise.allSettled([...stopping.values()]);
3645
+ stopPromise = Promise.allSettled(inflight.values()).then(async () => {
3646
+ await Promise.allSettled(stopping.values());
3542
3647
  const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
3543
3648
  await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
3544
3649
  });
@@ -3547,6 +3652,10 @@ function createBranchRegistry(options) {
3547
3652
  };
3548
3653
  }
3549
3654
 
3655
+ // ../server/dist/agents/silence.js
3656
+ var QUIET_NOTICE_MS = 3 * 6e4;
3657
+ var SILENCE_CEILING_MS = 30 * 6e4;
3658
+
3550
3659
  // ../server/dist/agents/failure.js
3551
3660
  var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
3552
3661
  var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
@@ -3600,6 +3709,8 @@ function message(code, input) {
3600
3709
  return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
3601
3710
  case "not-registered":
3602
3711
  return `${agent} finished without registering the new direction, so nothing reached the rail. Its last output is in the Leglas terminal.`;
3712
+ case "agent-silent":
3713
+ 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
3714
  case "agent-error":
3604
3715
  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
3716
  }
@@ -3607,10 +3718,10 @@ function message(code, input) {
3607
3718
  function classifyFailure(input) {
3608
3719
  const lines2 = input.lines ?? [];
3609
3720
  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";
3721
+ 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
3722
  return { code, message: message(code, input) };
3612
3723
  }
3613
- function sessionShaped(code) {
3724
+ function conversationFailure(code) {
3614
3725
  return code === "agent-error";
3615
3726
  }
3616
3727
 
@@ -3626,43 +3737,38 @@ var TAG_CAP = 40;
3626
3737
  var CLASS_CAP = 8;
3627
3738
  var CLASS_LENGTH_CAP = 60;
3628
3739
  var COVERS_CAP = 8;
3629
- function isRecord2(value) {
3630
- return typeof value === "object" && value !== null && !Array.isArray(value);
3631
- }
3632
3740
  function text(value, cap) {
3633
- return typeof value === "string" ? value.trim().slice(0, cap) : "";
3741
+ return isString(value) ? value.trim().slice(0, cap) : "";
3634
3742
  }
3635
3743
  function fraction(value) {
3636
- if (typeof value !== "number" || !Number.isFinite(value))
3744
+ if (!isNumber(value) || !Number.isFinite(value))
3637
3745
  return 0.5;
3638
3746
  return Math.min(1, Math.max(0, value));
3639
3747
  }
3640
3748
  function size(value) {
3641
- return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
3749
+ return isNumber(value) && Number.isFinite(value) ? Math.round(value) : 0;
3642
3750
  }
3643
3751
  function anchorFrom(value) {
3644
- if (!isRecord2(value))
3752
+ if (!isJsonRecord(value))
3645
3753
  return null;
3646
3754
  const selector = text(value["selector"], SELECTOR_CAP);
3647
3755
  if (selector === "")
3648
3756
  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;
3757
+ const rect = isJsonRecord(value["rect"]) ? value["rect"] : {};
3758
+ const classes = Array.isArray(value["classes"]) ? value["classes"].filter((entry) => isString(entry)).slice(0, CLASS_CAP).map((entry) => entry.slice(0, CLASS_LENGTH_CAP)) : [];
3759
+ const rawRegion = isJsonRecord(value["region"]) ? value["region"] : null;
3652
3760
  const region = rawRegion === null ? null : {
3653
3761
  height: fraction(rawRegion["height"]),
3654
3762
  width: fraction(rawRegion["width"]),
3655
3763
  x: fraction(rawRegion["x"]),
3656
3764
  y: fraction(rawRegion["y"])
3657
3765
  };
3658
- const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isRecord2).slice(0, COVERS_CAP).map((entry) => ({
3766
+ const covers = Array.isArray(value["covers"]) ? value["covers"].filter(isJsonRecord).slice(0, COVERS_CAP).map((entry) => ({
3659
3767
  tag: text(entry["tag"], TAG_CAP) || "element",
3660
3768
  text: text(entry["text"], TEXT_CAP)
3661
3769
  })) : [];
3662
- return {
3770
+ const anchor = {
3663
3771
  classes,
3664
- ...covers.length === 0 ? {} : { covers },
3665
- ...region === null ? {} : { region },
3666
3772
  rect: {
3667
3773
  height: size(rect["height"]),
3668
3774
  width: size(rect["width"]),
@@ -3671,22 +3777,27 @@ function anchorFrom(value) {
3671
3777
  },
3672
3778
  selector,
3673
3779
  spot: {
3674
- x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
3675
- y: fraction(isRecord2(value["spot"]) ? value["spot"]["y"] : void 0)
3780
+ x: fraction(isJsonRecord(value["spot"]) ? value["spot"]["x"] : void 0),
3781
+ y: fraction(isJsonRecord(value["spot"]) ? value["spot"]["y"] : void 0)
3676
3782
  },
3677
3783
  tag: text(value["tag"], TAG_CAP) || "element",
3678
3784
  text: text(value["text"], TEXT_CAP),
3679
3785
  viewport: size(value["viewport"])
3680
3786
  };
3787
+ if (covers.length > 0)
3788
+ anchor.covers = covers;
3789
+ if (region !== null)
3790
+ anchor.region = region;
3791
+ return anchor;
3681
3792
  }
3682
3793
  async function readAnnotations(cwd) {
3683
3794
  try {
3684
3795
  const raw = await readFile6(join7(cwd, ANNOTATIONS_PATH), "utf8");
3685
- const parsed = JSON.parse(raw);
3686
- if (!Array.isArray(parsed.annotations))
3796
+ const parsed = parseJson(raw);
3797
+ if (!isJsonRecord(parsed) || !Array.isArray(parsed.annotations))
3687
3798
  return [];
3688
3799
  return parsed.annotations.flatMap((entry, index) => {
3689
- if (!isRecord2(entry))
3800
+ if (!isJsonRecord(entry))
3690
3801
  return [];
3691
3802
  const anchor = anchorFrom(entry["anchor"]);
3692
3803
  const title = text(entry["title"], TAG_CAP * 4);
@@ -3695,7 +3806,7 @@ async function readAnnotations(cwd) {
3695
3806
  return [
3696
3807
  {
3697
3808
  anchor,
3698
- id: typeof entry["id"] === "string" ? entry["id"] : String(index),
3809
+ id: isString(entry["id"]) ? entry["id"] : String(index),
3699
3810
  note: text(entry["note"], NOTE_CAP),
3700
3811
  title
3701
3812
  }
@@ -3811,7 +3922,7 @@ function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx
3811
3922
  const target = preview.file ?? targetFor(preview.url);
3812
3923
  const cleaned = intent.trim();
3813
3924
  const asked = changeBlock(cleaned, notes);
3814
- const recorded = cleaned === "" ? notes.map((entry) => entry.note).filter((entry) => entry !== "").join("; ") : cleaned;
3925
+ const recorded = cleaned === "" ? notes.flatMap((entry) => entry.note !== "" ? [entry.note] : []).join("; ") : cleaned;
3815
3926
  const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand, captured) : replacePrompt(preview, asked, target, leglasCommand, captured);
3816
3927
  return { prompt, target, mode };
3817
3928
  }
@@ -3934,52 +4045,62 @@ var TERMINAL = ["failed", "cancelled"];
3934
4045
  function isTerminal(status) {
3935
4046
  return TERMINAL.includes(status);
3936
4047
  }
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)
4048
+ var FAILURE_CODES = {
4049
+ cancelled: true,
4050
+ stopped: true,
4051
+ "missing-agent": true,
4052
+ "not-signed-in": true,
4053
+ "provider-overloaded": true,
4054
+ "provider-limit": true,
4055
+ "needs-trust": true,
4056
+ "not-registered": true,
4057
+ "agent-error": true,
4058
+ "agent-silent": true
4059
+ };
4060
+ function isFailureCode(value) {
4061
+ return isString(value) && Object.hasOwn(FAILURE_CODES, value);
4062
+ }
4063
+ function failureOf(entry) {
4064
+ if (!isJsonRecord(entry))
3950
4065
  return null;
3951
- const entry = value;
3952
- if (typeof entry.message !== "string" || entry.message === "")
4066
+ if (!isString(entry.message) || entry.message === "")
3953
4067
  return null;
3954
- if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
4068
+ if (!isFailureCode(entry.code))
3955
4069
  return null;
3956
4070
  return { code: entry.code, message: entry.message };
3957
4071
  }
3958
4072
  async function readRequests(cwd) {
3959
4073
  try {
3960
4074
  const raw = await readFile7(join8(cwd, REQUESTS_PATH), "utf8");
3961
- const parsed = JSON.parse(raw);
3962
- if (!Array.isArray(parsed.requests))
4075
+ const parsed = parseJson(raw);
4076
+ if (!isJsonRecord(parsed) || !Array.isArray(parsed.requests))
3963
4077
  return [];
3964
4078
  return parsed.requests.map((request, index) => {
3965
- const source = typeof request === "object" && request !== null ? request : {};
4079
+ const source = isJsonRecord(request) ? request : Array.isArray(request) ? Object.fromEntries(Object.entries(request)) : {};
3966
4080
  const { failure: rawFailure, attachments: rawAttachments, captureNote: rawCaptureNote, compare: rawCompare, references: rawReferences, ...entry } = source;
3967
4081
  const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
3968
4082
  const failure = isTerminal(status) ? failureOf(rawFailure) : null;
3969
- const id = typeof entry.id === "string" && REQUEST_ID.test(entry.id) ? entry.id : String(index);
4083
+ const id = isString(entry.id) && REQUEST_ID.test(entry.id) ? entry.id : String(index);
3970
4084
  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 {
4085
+ 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;
4086
+ const loaded = {
3973
4087
  ...entry,
3974
4088
  id,
3975
4089
  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 } : {}
4090
+ mode: entry.mode === "variant" ? "variant" : "replace"
3982
4091
  };
4092
+ const optional = {};
4093
+ if (failure !== null)
4094
+ optional.failure = failure;
4095
+ if (attachments !== null && attachments.length > 0)
4096
+ optional.attachments = attachments;
4097
+ if (isString(rawCaptureNote))
4098
+ optional.captureNote = rawCaptureNote;
4099
+ if (isString(rawCompare))
4100
+ optional.compare = rawCompare;
4101
+ if (Array.isArray(rawReferences) && rawReferences.every(isString))
4102
+ optional.references = rawReferences;
4103
+ return { ...loaded, ...optional };
3983
4104
  });
3984
4105
  } catch {
3985
4106
  return [];
@@ -4047,7 +4168,7 @@ async function clearRequests(cwd) {
4047
4168
  // ../server/dist/agents/runner.js
4048
4169
  import { spawn as nodeSpawn3 } from "child_process";
4049
4170
  import { readFile as readFile9 } from "fs/promises";
4050
- import { join as join9, resolve as resolve2, sep as sep2 } from "path";
4171
+ import { join as join9, resolve as resolve2 } from "path";
4051
4172
 
4052
4173
  // ../server/dist/agents/claude-agent-session.js
4053
4174
  import { EventEmitter } from "events";
@@ -4071,7 +4192,9 @@ function imageMediaType(path) {
4071
4192
  async function claudeContent(prompt, images) {
4072
4193
  if (images.length === 0)
4073
4194
  return prompt;
4074
- const blocks = [{ type: "text", text: prompt }];
4195
+ const blocks = [
4196
+ { type: "text", text: prompt }
4197
+ ];
4075
4198
  for (const path of images) {
4076
4199
  const mediaType = imageMediaType(path);
4077
4200
  if (mediaType === null)
@@ -4103,9 +4226,9 @@ function waitForAbort(work, signal) {
4103
4226
  void work.then((value) => {
4104
4227
  signal.removeEventListener("abort", onAbort);
4105
4228
  resolve5(value);
4106
- }, (error) => {
4229
+ }, (cause) => {
4107
4230
  signal.removeEventListener("abort", onAbort);
4108
- reject(error);
4231
+ reject(cause);
4109
4232
  });
4110
4233
  });
4111
4234
  }
@@ -4238,27 +4361,28 @@ var PersistentClaudeSession = class {
4238
4361
  this.warmedFor = sessionId;
4239
4362
  const controller = new AbortController();
4240
4363
  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) => {
4364
+ const options = {
4365
+ abortController: controller,
4366
+ cwd: this.cwd,
4367
+ env: agentEnvironment(),
4368
+ permissionMode: "acceptEdits",
4369
+ settingSources: ["user", "project", "local"],
4370
+ persistSession: true
4371
+ };
4372
+ if (this.allowedCommands.length > 0) {
4373
+ options.allowedTools = this.allowedCommands.map((command) => `Bash(${command} *)`);
4374
+ }
4375
+ if (sessionId !== null)
4376
+ options.resume = sessionId;
4377
+ const warming = this.startup({ options, initializeTimeoutMs: INITIALIZE_TIMEOUT_MS }).then((warmQuery) => {
4254
4378
  if (this.closed || controller.signal.aborted || this.processAbort !== controller)
4255
4379
  warmQuery.close();
4256
4380
  else
4257
4381
  this.warmQuery = warmQuery;
4258
- }).catch((error) => {
4382
+ }).catch((cause) => {
4259
4383
  if (this.processAbort === controller)
4260
4384
  this.processAbort = null;
4261
- throw error;
4385
+ throw cause;
4262
4386
  }).finally(() => {
4263
4387
  if (this.warming === warming)
4264
4388
  this.warming = null;
@@ -4365,7 +4489,7 @@ var PersistentClaudeSession = class {
4365
4489
  let endedNormally = false;
4366
4490
  try {
4367
4491
  for await (const message2 of query) {
4368
- if (typeof message2.session_id === "string" && message2.session_id !== "") {
4492
+ if (isString(message2.session_id) && message2.session_id !== "") {
4369
4493
  this.loadedSessionId = message2.session_id;
4370
4494
  }
4371
4495
  const child = this.active;
@@ -4466,10 +4590,10 @@ import { EventEmitter as EventEmitter2 } from "events";
4466
4590
  import { PassThrough as PassThrough2 } from "stream";
4467
4591
  var REQUEST_TIMEOUT_MS = 3e4;
4468
4592
  function record2(value) {
4469
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
4593
+ return isJsonRecord(value) ? value : null;
4470
4594
  }
4471
4595
  function string(value) {
4472
- return typeof value === "string" && value !== "" ? value : null;
4596
+ return isString(value) && value !== "" ? value : null;
4473
4597
  }
4474
4598
  var CodexTurnChild = class {
4475
4599
  interrupt;
@@ -4529,9 +4653,9 @@ var PersistentCodexAppServer = class {
4529
4653
  stderr = [];
4530
4654
  closed = false;
4531
4655
  resetting = null;
4532
- constructor(cwd, spawn4, requestTimeoutMs) {
4656
+ constructor(cwd, spawn5, requestTimeoutMs) {
4533
4657
  this.cwd = cwd;
4534
- this.spawn = spawn4;
4658
+ this.spawn = spawn5;
4535
4659
  this.requestTimeoutMs = requestTimeoutMs;
4536
4660
  }
4537
4661
  /**
@@ -4585,9 +4709,9 @@ var PersistentCodexAppServer = class {
4585
4709
  capabilities: null
4586
4710
  }).then(() => {
4587
4711
  this.notify("initialized", {});
4588
- }).catch(async (error) => {
4589
- await this.resetProcess(process2, error instanceof Error ? error : new Error(String(error)));
4590
- throw error;
4712
+ }).catch(async (cause) => {
4713
+ await this.resetProcess(process2, cause instanceof Error ? cause : new Error(String(cause)));
4714
+ throw cause;
4591
4715
  });
4592
4716
  return this.ready;
4593
4717
  }
@@ -4619,7 +4743,7 @@ var PersistentCodexAppServer = class {
4619
4743
  this.active = { child, threadId, turnId: null };
4620
4744
  child.line({ type: "thread.started", thread_id: threadId });
4621
4745
  turnSubmitted = true;
4622
- const response = record2(await this.requestRaw("turn/start", {
4746
+ const params = {
4623
4747
  threadId,
4624
4748
  input: [
4625
4749
  { type: "text", text: input.prompt },
@@ -4633,9 +4757,11 @@ var PersistentCodexAppServer = class {
4633
4757
  networkAccess: true,
4634
4758
  excludeTmpdirEnvVar: false,
4635
4759
  excludeSlashTmp: false
4636
- },
4637
- ...input.effort === null ? {} : { effort: input.effort }
4638
- }));
4760
+ }
4761
+ };
4762
+ if (input.effort !== null)
4763
+ params.effort = input.effort;
4764
+ const response = record2(await this.requestRaw("turn/start", params));
4639
4765
  const turn = record2(response?.turn);
4640
4766
  const turnId = string(turn?.id);
4641
4767
  if (turnId === null)
@@ -4730,13 +4856,13 @@ var PersistentCodexAppServer = class {
4730
4856
  return;
4731
4857
  let message2;
4732
4858
  try {
4733
- message2 = record2(JSON.parse(line));
4859
+ message2 = record2(parseJson(line));
4734
4860
  } catch {
4735
4861
  message2 = null;
4736
4862
  }
4737
4863
  if (message2 === null)
4738
4864
  return;
4739
- const id = typeof message2.id === "number" ? message2.id : null;
4865
+ const id = isNumber(message2.id) ? message2.id : null;
4740
4866
  const method = string(message2.method);
4741
4867
  if (id !== null && method === null) {
4742
4868
  const pending = this.pending.get(id);
@@ -4805,7 +4931,7 @@ var PersistentCodexAppServer = class {
4805
4931
  }
4806
4932
  if (item.type === "reasoning")
4807
4933
  return { type: "reasoning" };
4808
- return { type: typeof item.type === "string" ? item.type : "unknown" };
4934
+ return { type: isString(item.type) ? item.type : "unknown" };
4809
4935
  }
4810
4936
  answerServerRequest(id, method) {
4811
4937
  if (method === "item/commandExecution/requestApproval") {
@@ -4928,8 +5054,39 @@ var defaultSpawn = (command, args, options) => nodeSpawn2(command, args, {
4928
5054
  ...options,
4929
5055
  env: agentEnvironment()
4930
5056
  });
4931
- function createCodexAppServer(cwd, spawn4 = defaultSpawn, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
4932
- return new PersistentCodexAppServer(cwd, spawn4, requestTimeoutMs);
5057
+ function createCodexAppServer(cwd, spawn5 = defaultSpawn, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
5058
+ return new PersistentCodexAppServer(cwd, spawn5, requestTimeoutMs);
5059
+ }
5060
+
5061
+ // ../server/dist/agents/process-tree.js
5062
+ import { spawn as spawn3 } from "child_process";
5063
+ function ownGroup(platform = process.platform) {
5064
+ return { detached: platform !== "win32" };
5065
+ }
5066
+ var system = {
5067
+ platform: process.platform,
5068
+ kill: (pid, signal) => process.kill(pid, signal),
5069
+ taskkill: (pid) => spawn3("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", shell: false })
5070
+ };
5071
+ function signalTree(child, signal, deps = system) {
5072
+ const { pid } = child;
5073
+ if (pid === void 0) {
5074
+ child.kill(signal);
5075
+ return;
5076
+ }
5077
+ if (deps.platform === "win32") {
5078
+ try {
5079
+ deps.taskkill(pid).once("error", () => child.kill(signal));
5080
+ } catch {
5081
+ child.kill(signal);
5082
+ }
5083
+ return;
5084
+ }
5085
+ try {
5086
+ deps.kill(-pid, signal);
5087
+ } catch {
5088
+ child.kill(signal);
5089
+ }
4933
5090
  }
4934
5091
 
4935
5092
  // ../server/dist/agents/runner.js
@@ -4937,6 +5094,9 @@ var POLL_MS = 2e3;
4937
5094
  var OUTPUT_LINES = 20;
4938
5095
  var IDLE_RELEASE_MS = 5 * 6e4;
4939
5096
  var CANCEL_GRACE_MS = 5e3;
5097
+ function isStateUpdater(value) {
5098
+ return typeof value === "function";
5099
+ }
4940
5100
  var SESSION_TURNS_CAP = 8;
4941
5101
  function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images = []) {
4942
5102
  if (choice.agent === null)
@@ -5007,7 +5167,7 @@ function defaultSpawn2(command, args, options) {
5007
5167
  return nodeSpawn3(command, args, { ...options, env: agentEnvironment() });
5008
5168
  }
5009
5169
  function startRunner(options) {
5010
- const spawn4 = options.spawn ?? defaultSpawn2;
5170
+ const spawn5 = options.spawn ?? defaultSpawn2;
5011
5171
  const codexAppServer = options.codexAppServer === void 0 ? options.spawn === void 0 ? createCodexAppServer(options.cwd) : null : options.codexAppServer;
5012
5172
  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
5173
  const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
@@ -5016,6 +5176,7 @@ function startRunner(options) {
5016
5176
  const setLater = options.setTimeout ?? ((callback, milliseconds) => {
5017
5177
  setTimeout(callback, milliseconds).unref?.();
5018
5178
  });
5179
+ const now = options.now ?? (() => Date.now());
5019
5180
  let state = {
5020
5181
  running: false,
5021
5182
  requestId: null,
@@ -5023,10 +5184,11 @@ function startRunner(options) {
5023
5184
  activity: null,
5024
5185
  startedAt: null,
5025
5186
  stopping: false,
5026
- waiting: null
5187
+ waiting: null,
5188
+ quietSince: null
5027
5189
  };
5028
5190
  const setState = (next) => {
5029
- state = typeof next === "function" ? next(state) : next;
5191
+ state = isStateUpdater(next) ? next(state) : next;
5030
5192
  options.onChange?.();
5031
5193
  };
5032
5194
  let stopped = false;
@@ -5087,7 +5249,8 @@ function startRunner(options) {
5087
5249
  activity: null,
5088
5250
  startedAt: null,
5089
5251
  stopping: false,
5090
- waiting: null
5252
+ waiting: null,
5253
+ quietSince: null
5091
5254
  });
5092
5255
  };
5093
5256
  const rememberLine = (lines2, line) => {
@@ -5116,11 +5279,21 @@ function startRunner(options) {
5116
5279
  cancelled: false,
5117
5280
  controller,
5118
5281
  abandon: () => {
5119
- }
5282
+ },
5283
+ heardAt: now(),
5284
+ silenced: false,
5285
+ spawned: false
5120
5286
  };
5121
5287
  active = current;
5122
5288
  activeAgent = resolved.agent;
5123
5289
  const cancelled = () => ({ ok: false, error: "cancelled" });
5290
+ const silent = () => ({ ok: false, error: "silent" });
5291
+ const heard = () => {
5292
+ current.heardAt = now();
5293
+ if (active === current && state.quietSince !== null) {
5294
+ setState((value) => ({ ...value, quietSince: null }));
5295
+ }
5296
+ };
5124
5297
  const startPersistent = async () => {
5125
5298
  if (persistent === null)
5126
5299
  throw new Error("No persistent transport.");
@@ -5145,27 +5318,31 @@ function startRunner(options) {
5145
5318
  return;
5146
5319
  }
5147
5320
  resolve5(child);
5148
- }, (error) => reject(error));
5321
+ }, (cause) => reject(cause));
5149
5322
  });
5150
5323
  };
5324
+ const spawnAgent2 = () => {
5325
+ const child = spawn5(resolved.command, resolved.args, {
5326
+ cwd: options.cwd,
5327
+ shell: false,
5328
+ stdio: ["ignore", "pipe", "pipe"],
5329
+ ...ownGroup()
5330
+ });
5331
+ current.spawned = true;
5332
+ return child;
5333
+ };
5151
5334
  try {
5152
5335
  let child;
5153
5336
  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
- });
5337
+ child = persistent !== null ? await startPersistent() : spawnAgent2();
5159
5338
  } catch (error) {
5339
+ if (current.silenced)
5340
+ return silent();
5160
5341
  if (current.cancelled || stopped || controller.signal.aborted)
5161
5342
  return cancelled();
5162
5343
  if (persistent !== null) {
5163
5344
  try {
5164
- child = spawn4(resolved.command, resolved.args, {
5165
- cwd: options.cwd,
5166
- shell: false,
5167
- stdio: ["ignore", "pipe", "pipe"]
5168
- });
5345
+ child = spawnAgent2();
5169
5346
  } catch (fallbackError) {
5170
5347
  return {
5171
5348
  ok: false,
@@ -5182,12 +5359,13 @@ function startRunner(options) {
5182
5359
  current.child = child;
5183
5360
  if (current.cancelled || stopped || controller.signal.aborted) {
5184
5361
  try {
5185
- child.kill("SIGTERM");
5362
+ signalChild(current, "SIGTERM");
5186
5363
  } catch {
5187
5364
  }
5188
- return cancelled();
5365
+ return current.silenced ? silent() : cancelled();
5189
5366
  }
5190
5367
  const stdoutFlush = lineReader(child.stdout, (line) => {
5368
+ heard();
5191
5369
  rememberLine(lines2, line);
5192
5370
  const sessionId = sessionFrom(resolved.agent, line);
5193
5371
  if (sessionId !== null)
@@ -5207,7 +5385,10 @@ function startRunner(options) {
5207
5385
  }
5208
5386
  }
5209
5387
  });
5210
- const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
5388
+ const stderrFlush = lineReader(child.stderr, (line) => {
5389
+ heard();
5390
+ rememberLine(lines2, line);
5391
+ });
5211
5392
  return await new Promise((resolve5) => {
5212
5393
  let settled = false;
5213
5394
  const settle = (outcome) => {
@@ -5218,11 +5399,19 @@ function startRunner(options) {
5218
5399
  stderrFlush();
5219
5400
  resolve5(outcome);
5220
5401
  };
5221
- current.abandon = () => settle(cancelled());
5222
- child.once("error", (error) => settle({ ok: false, error: error.message }));
5402
+ current.abandon = () => settle(current.silenced ? silent() : cancelled());
5403
+ child.once("error", (error) => {
5404
+ if (current.cancelled)
5405
+ return settle(cancelled());
5406
+ if (current.silenced)
5407
+ return settle(silent());
5408
+ settle({ ok: false, error: error.message });
5409
+ });
5223
5410
  child.once("close", (code, signal) => {
5224
5411
  if (current.cancelled)
5225
5412
  return settle(cancelled());
5413
+ if (current.silenced)
5414
+ return settle(silent());
5226
5415
  if (signal !== null)
5227
5416
  return settle({ ok: false, error: `stopped by ${signal}` });
5228
5417
  settle({ ok: true, code: code ?? 0 });
@@ -5258,7 +5447,8 @@ function startRunner(options) {
5258
5447
  activity: null,
5259
5448
  startedAt: Date.now(),
5260
5449
  stopping: false,
5261
- waiting: null
5450
+ waiting: null,
5451
+ quietSince: null
5262
5452
  });
5263
5453
  const observed = {
5264
5454
  sessionId: null,
@@ -5280,7 +5470,7 @@ function startRunner(options) {
5280
5470
  // real output. Where it has not, "did not edit" means "was not seen
5281
5471
  // to edit", and rerunning on that is how a half-applied change gets
5282
5472
  // 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
5473
+ activityVerified(resolved.agent) && conversationFailure(failure.code) && // Not redundant with the verdict: a stop that lands between the first
5284
5474
  // child settling and the retry starting finds no child to cancel, so
5285
5475
  // nothing says "cancelled". Stopped still means stopped.
5286
5476
  !stopped) {
@@ -5354,31 +5544,61 @@ function startRunner(options) {
5354
5544
  }
5355
5545
  });
5356
5546
  };
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 }));
5547
+ const signalChild = (current, value) => {
5548
+ const { child } = current;
5549
+ if (child === null)
5550
+ return;
5551
+ if (current.spawned)
5552
+ signalTree(child, value);
5553
+ else
5554
+ child.kill(value);
5555
+ };
5556
+ const end = (current) => {
5368
5557
  current.controller.abort();
5369
5558
  try {
5370
- current.child?.kill("SIGTERM");
5559
+ signalChild(current, "SIGTERM");
5371
5560
  } catch {
5372
5561
  }
5373
5562
  setLater(() => {
5374
5563
  if (active !== current)
5375
5564
  return;
5376
5565
  try {
5377
- current.child?.kill("SIGKILL");
5566
+ signalChild(current, "SIGKILL");
5378
5567
  } catch {
5379
5568
  }
5380
5569
  current.abandon();
5381
5570
  }, CANCEL_GRACE_MS);
5571
+ };
5572
+ const listen2 = () => {
5573
+ const current = active;
5574
+ if (current === null || current.cancelled || current.silenced)
5575
+ return;
5576
+ const quiet = now() - current.heardAt;
5577
+ if (quiet >= SILENCE_CEILING_MS) {
5578
+ current.silenced = true;
5579
+ setState((value) => ({ ...value, stopping: true, waiting: null, quietSince: null }));
5580
+ end(current);
5581
+ return;
5582
+ }
5583
+ if (quiet >= QUIET_NOTICE_MS && state.quietSince === null) {
5584
+ setState((value) => ({ ...value, quietSince: current.heardAt }));
5585
+ }
5586
+ };
5587
+ const timer = setEvery(() => {
5588
+ listen2();
5589
+ schedule();
5590
+ }, POLL_MS);
5591
+ schedule();
5592
+ const cancel = (id) => {
5593
+ if (active === null || active.cancelled || active.silenced)
5594
+ return false;
5595
+ if (id !== void 0 && active.requestId !== id)
5596
+ return false;
5597
+ const current = active;
5598
+ current.cancelled = true;
5599
+ failed.add(current.requestId);
5600
+ setState((value) => ({ ...value, stopping: true, waiting: null, quietSince: null }));
5601
+ end(current);
5382
5602
  return true;
5383
5603
  };
5384
5604
  const stop = () => {
@@ -5409,7 +5629,7 @@ import { createHash } from "crypto";
5409
5629
  var LIVE_PATH = "/leglas/api/live";
5410
5630
  var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
5411
5631
  function encodeFrame(opcode, payload) {
5412
- const body = typeof payload === "string" ? Buffer.from(payload) : payload;
5632
+ const body = isString(payload) ? Buffer.from(payload) : payload;
5413
5633
  let header;
5414
5634
  if (body.length < 126) {
5415
5635
  header = Buffer.allocUnsafe(2);
@@ -5443,17 +5663,18 @@ function createCoalescer(emit, options = {}) {
5443
5663
  return;
5444
5664
  const waiting = pending.get(change);
5445
5665
  if (waiting !== void 0)
5446
- clearLater(waiting);
5447
- pending.set(change, setLater(() => {
5666
+ waiting();
5667
+ const handle = setLater(() => {
5448
5668
  pending.delete(change);
5449
5669
  if (!closed)
5450
5670
  emit(change);
5451
- }, windowMs));
5671
+ }, windowMs);
5672
+ pending.set(change, () => clearLater(handle));
5452
5673
  },
5453
5674
  close() {
5454
5675
  closed = true;
5455
- for (const handle of pending.values())
5456
- clearLater(handle);
5676
+ for (const cancel of pending.values())
5677
+ cancel();
5457
5678
  pending.clear();
5458
5679
  }
5459
5680
  };
@@ -5537,7 +5758,7 @@ function createLiveHub(options = {}) {
5537
5758
  if (listeners.size === 0)
5538
5759
  return;
5539
5760
  const frame = encodeFrame(1, JSON.stringify({ changed: change }));
5540
- for (const listener of [...listeners]) {
5761
+ for (const listener of Array.from(listeners)) {
5541
5762
  if (listener.socket.destroyed || !listener.socket.writable) {
5542
5763
  drop(listener);
5543
5764
  continue;
@@ -5556,7 +5777,7 @@ function createLiveHub(options = {}) {
5556
5777
  return false;
5557
5778
  const upgrade = req.headers.upgrade;
5558
5779
  const key = req.headers["sec-websocket-key"];
5559
- if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
5780
+ if (!isString(upgrade) || upgrade.toLowerCase() !== "websocket" || !isString(key) || key.trim() === "") {
5560
5781
  socket.destroy();
5561
5782
  return false;
5562
5783
  }
@@ -5591,7 +5812,7 @@ Sec-WebSocket-Accept: ${accept}\r
5591
5812
  return true;
5592
5813
  },
5593
5814
  close: async () => {
5594
- for (const listener of [...listeners]) {
5815
+ for (const listener of Array.from(listeners)) {
5595
5816
  write2(listener, 8, Buffer.alloc(0));
5596
5817
  drop(listener);
5597
5818
  listener.socket.destroy();
@@ -5645,15 +5866,17 @@ async function askLink(resolver, url, entryPath) {
5645
5866
  return false;
5646
5867
  const secure = target.protocol === "https:";
5647
5868
  return new Promise((resolve5) => {
5648
- const request = (secure ? https : http2).request({
5869
+ const requestOptions = {
5649
5870
  host: address,
5650
5871
  port: Number(target.port || (secure ? 443 : 80)),
5651
5872
  path: entryPath,
5652
5873
  method: "GET",
5653
5874
  headers: { host: target.host },
5654
- ...secure ? { servername: target.hostname } : {},
5655
5875
  timeout: PROBE_TIMEOUT_MS2
5656
- }, (response) => {
5876
+ };
5877
+ if (secure)
5878
+ requestOptions.servername = target.hostname;
5879
+ const request = (secure ? https : http2).request(requestOptions, (response) => {
5657
5880
  response.resume();
5658
5881
  const status = response.statusCode ?? 0;
5659
5882
  resolve5(status >= 200 && status < 400);
@@ -5678,12 +5901,14 @@ function withOutput(sentence, output) {
5678
5901
  return `${stem} (${output.slice(0, 160)}).`;
5679
5902
  }
5680
5903
  function startTunnel(options, deps = {}) {
5681
- const spawn4 = deps.spawn ?? spawnChild;
5904
+ const spawn5 = deps.spawn ?? spawnChild;
5682
5905
  const now = deps.now ?? Date.now;
5683
5906
  const urlDeadlineMs = deps.urlDeadlineMs ?? URL_DEADLINE_MS;
5684
5907
  const probeDeadlineMs = deps.probeDeadlineMs ?? PROBE_DEADLINE_MS;
5685
5908
  const resolver = deps.probe === void 0 ? new Resolver() : null;
5686
- const probe2 = deps.probe ?? ((url2) => askLink(resolver, url2, options.entryPath));
5909
+ const probe2 = deps.probe ?? ((url2) => {
5910
+ return askLink(resolver, url2, options.entryPath);
5911
+ });
5687
5912
  const timers = /* @__PURE__ */ new Set();
5688
5913
  const later = (callback, ms) => {
5689
5914
  const timer = setTimeout(() => {
@@ -5724,18 +5949,16 @@ function startTunnel(options, deps = {}) {
5724
5949
  return;
5725
5950
  terminal = true;
5726
5951
  clearTimers();
5727
- report({
5728
- status: "failed",
5729
- provider: options.provider,
5730
- reason,
5731
- ...url === null ? {} : { url }
5732
- });
5952
+ const failure = { status: "failed", provider: options.provider, reason };
5953
+ if (url !== null)
5954
+ failure.url = url;
5955
+ report(failure);
5733
5956
  };
5734
5957
  report(state);
5735
5958
  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
5959
  let child;
5737
5960
  try {
5738
- child = spawn4(options.provider, args, {
5961
+ child = spawn5(options.provider, args, {
5739
5962
  env: agentEnvironment(),
5740
5963
  shell: false,
5741
5964
  stdio: ["ignore", "pipe", "pipe"]
@@ -5805,8 +6028,8 @@ function startTunnel(options, deps = {}) {
5805
6028
  if (stream !== "stdout")
5806
6029
  return;
5807
6030
  try {
5808
- const event = JSON.parse(trimmed);
5809
- const candidate = typeof event.url === "string" && event.url.startsWith("https://") ? event.url : null;
6031
+ const event = parseJson(trimmed);
6032
+ const candidate = isJsonRecord(event) && isString(event.url) && event.url.startsWith("https://") ? event.url : null;
5810
6033
  if (candidate !== null)
5811
6034
  beginProbe(candidate);
5812
6035
  } catch {
@@ -6027,19 +6250,16 @@ function isHiddenPath(path) {
6027
6250
  }
6028
6251
  var FILES_PREFIX_PATH = "/leglas/files/";
6029
6252
  var DETECT_TTL_MS = 1e4;
6030
- function isRecord3(value) {
6031
- return typeof value === "object" && value !== null && !Array.isArray(value);
6032
- }
6033
6253
  function stringArray(value) {
6034
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
6254
+ return Array.isArray(value) && value.every((entry) => isString(entry));
6035
6255
  }
6036
6256
  function stringRecord(value) {
6037
- return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
6257
+ return isJsonRecord(value) && Object.values(value).every((entry) => isString(entry));
6038
6258
  }
6039
6259
  function layoutFrom(value) {
6040
- if (!isRecord3(value))
6260
+ if (!isJsonRecord(value))
6041
6261
  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))) {
6262
+ 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
6263
  return null;
6044
6264
  }
6045
6265
  return {
@@ -6051,7 +6271,7 @@ function layoutFrom(value) {
6051
6271
  };
6052
6272
  }
6053
6273
  function manifestFrom(value, previews) {
6054
- if (!isRecord3(value)) {
6274
+ if (!isJsonRecord(value)) {
6055
6275
  return { ok: false, error: "Share details must be a JSON object." };
6056
6276
  }
6057
6277
  const scope2 = value.scope;
@@ -6216,7 +6436,7 @@ function bind(server) {
6216
6436
  const onListening = () => {
6217
6437
  server.removeListener("error", onError);
6218
6438
  const address = server.address();
6219
- resolve5(typeof address === "object" && address !== null ? address.port : 0);
6439
+ resolve5(address !== null && !isString(address) ? address.port : 0);
6220
6440
  };
6221
6441
  server.once("error", onError);
6222
6442
  server.once("listening", onListening);
@@ -6256,10 +6476,7 @@ function createShareManager(options) {
6256
6476
  }
6257
6477
  return detected;
6258
6478
  };
6259
- const status = () => {
6260
- const share = active;
6261
- if (share === null)
6262
- return null;
6479
+ const snapshot = (share) => {
6263
6480
  const tunnelUrl = "url" in share.tunnel ? share.tunnel.url : void 0;
6264
6481
  const origin = tunnelUrl === void 0 ? null : tunnelUrl.replace(/\/$/, "");
6265
6482
  return {
@@ -6268,7 +6485,7 @@ function createShareManager(options) {
6268
6485
  titles: [...share.titles],
6269
6486
  layout: cloneLayout(share.layout),
6270
6487
  sharePort: share.port,
6271
- grants: [...share.grants.values()].toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
6488
+ grants: Array.from(share.grants.values()).toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
6272
6489
  const entryPath = `${ENTRY_PREFIX}${grant.token}`;
6273
6490
  return {
6274
6491
  id: grant.id,
@@ -6287,6 +6504,7 @@ function createShareManager(options) {
6287
6504
  startedAt: share.startedAt
6288
6505
  };
6289
6506
  };
6507
+ const status = () => active === null ? null : snapshot(active);
6290
6508
  const endGrant = (share, grant, why) => {
6291
6509
  if (!share.grants.delete(grant.id))
6292
6510
  return;
@@ -6304,7 +6522,7 @@ function createShareManager(options) {
6304
6522
  held.req.destroy();
6305
6523
  }
6306
6524
  share.grantRequests.delete(grant.id);
6307
- for (const held of [...share.waiting.get(grant.id) ?? []]) {
6525
+ for (const held of Array.from(share.waiting.get(grant.id) ?? [])) {
6308
6526
  if (held.drop())
6309
6527
  refuse(held.req, held.res, why);
6310
6528
  }
@@ -6324,13 +6542,13 @@ function createShareManager(options) {
6324
6542
  const at = now();
6325
6543
  const mono = nowMono();
6326
6544
  let ended = false;
6327
- for (const grant of [...share.grants.values()]) {
6545
+ for (const grant of Array.from(share.grants.values())) {
6328
6546
  if (!expired(grant, at, mono))
6329
6547
  continue;
6330
6548
  endGrant(share, grant, "expiry");
6331
6549
  ended = true;
6332
6550
  }
6333
- const next = [...share.grants.values()].reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
6551
+ const next = Array.from(share.grants.values()).reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
6334
6552
  if (next !== null) {
6335
6553
  share.expiryTimer = setTimeout(sweepExpiry, Math.max(1, next - at));
6336
6554
  share.expiryTimer.unref?.();
@@ -6619,7 +6837,7 @@ function createShareManager(options) {
6619
6837
  if (!parsed.ok)
6620
6838
  return { ok: false, status: 400, error: parsed.error };
6621
6839
  const providers = await tunnels();
6622
- const requested = isRecord3(input) ? input.tunnel : void 0;
6840
+ const requested = isJsonRecord(input) ? input.tunnel : void 0;
6623
6841
  if (requested !== void 0 && requested !== "none" && requested !== "cloudflared" && requested !== "ngrok") {
6624
6842
  return { ok: false, status: 400, error: "That tunnel provider is not supported." };
6625
6843
  }
@@ -6693,7 +6911,7 @@ function createShareManager(options) {
6693
6911
  port,
6694
6912
  // Whichever link exists when the tunnel starts: the probe only
6695
6913
  // needs a path the listener answers, and a share always has one.
6696
- entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
6914
+ entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
6697
6915
  onState: (next) => {
6698
6916
  if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
6699
6917
  return;
@@ -6704,7 +6922,7 @@ function createShareManager(options) {
6704
6922
  });
6705
6923
  share.launch.unref?.();
6706
6924
  }
6707
- return { ok: true, share: status() };
6925
+ return { ok: true, share: snapshot(share) };
6708
6926
  } finally {
6709
6927
  creating = false;
6710
6928
  }
@@ -6713,7 +6931,7 @@ function createShareManager(options) {
6713
6931
  const share = active;
6714
6932
  if (share === null)
6715
6933
  return { ok: false, status: 404, error: "Nothing is being shared." };
6716
- const name = isRecord3(input) && typeof input.name === "string" ? input.name.trim() : "";
6934
+ const name = isJsonRecord(input) && isString(input.name) ? input.name.trim() : "";
6717
6935
  if (name.length > 60) {
6718
6936
  return { ok: false, status: 400, error: "That name is too long for a link." };
6719
6937
  }
@@ -6728,26 +6946,26 @@ function createShareManager(options) {
6728
6946
  mintGrant(share, name);
6729
6947
  sweepExpiry();
6730
6948
  options.live.nudge("share");
6731
- return { ok: true, share: status() };
6949
+ return { ok: true, share: snapshot(share) };
6732
6950
  };
6733
6951
  const revokeGrant = (input) => {
6734
6952
  const share = active;
6735
6953
  if (share === null)
6736
6954
  return { ok: false, status: 404, error: "Nothing is being shared." };
6737
- const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
6955
+ const id = isJsonRecord(input) && isString(input.id) ? input.id : "";
6738
6956
  const grant = share.grants.get(id);
6739
6957
  if (grant === void 0)
6740
6958
  return { ok: false, status: 404, error: "No such link." };
6741
6959
  endGrant(share, grant, "revoke");
6742
6960
  sweepExpiry();
6743
6961
  options.live.nudge("share");
6744
- return { ok: true, share: status() };
6962
+ return { ok: true, share: snapshot(share) };
6745
6963
  };
6746
6964
  const extendGrant = (input) => {
6747
6965
  const share = active;
6748
6966
  if (share === null)
6749
6967
  return { ok: false, status: 404, error: "Nothing is being shared." };
6750
- const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
6968
+ const id = isJsonRecord(input) && isString(input.id) ? input.id : "";
6751
6969
  sweepExpiry();
6752
6970
  const grant = share.grants.get(id);
6753
6971
  if (grant === void 0) {
@@ -6758,13 +6976,13 @@ function createShareManager(options) {
6758
6976
  grant.expiresAtMono = nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n;
6759
6977
  sweepExpiry();
6760
6978
  options.live.nudge("share");
6761
- return { ok: true, share: status() };
6979
+ return { ok: true, share: snapshot(share) };
6762
6980
  };
6763
6981
  const rotate = async () => {
6764
6982
  const share = active;
6765
6983
  if (share === null)
6766
6984
  return { ok: false, status: 404, error: "Nothing is being shared." };
6767
- for (const grant of [...share.grants.values()])
6985
+ for (const grant of Array.from(share.grants.values()))
6768
6986
  endGrant(share, grant, "revoke");
6769
6987
  const provider = "provider" in share.tunnel ? share.tunnel.provider : null;
6770
6988
  await share.runningTunnel?.stop().catch(() => {
@@ -6781,7 +6999,7 @@ function createShareManager(options) {
6781
6999
  share.runningTunnel = runTunnel({
6782
7000
  provider,
6783
7001
  port: share.port,
6784
- entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
7002
+ entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
6785
7003
  onState: (next) => {
6786
7004
  if (active !== share || share.tunnelGeneration !== generation)
6787
7005
  return;
@@ -6793,17 +7011,17 @@ function createShareManager(options) {
6793
7011
  });
6794
7012
  }
6795
7013
  options.live.nudge("share");
6796
- return { ok: true, share: status() };
7014
+ return { ok: true, share: snapshot(share) };
6797
7015
  };
6798
7016
  const allowRoute = (input) => {
6799
7017
  const share = active;
6800
7018
  if (share === null)
6801
7019
  return { ok: false, status: 404, error: "Nothing is being shared." };
6802
- const given = isRecord3(input) && typeof input.path === "string" ? input.path.trim() : "";
7020
+ const given = isJsonRecord(input) && isString(input.path) ? input.path.trim() : "";
6803
7021
  if (!given.startsWith("/")) {
6804
7022
  return { ok: false, status: 400, error: "A route is a path beginning with a slash." };
6805
7023
  }
6806
- const subtree = isRecord3(input) && input.subtree === true;
7024
+ const subtree = isJsonRecord(input) && input.subtree === true;
6807
7025
  if (subtree && given.replace(/\/+$/, "") === "") {
6808
7026
  return { ok: false, status: 400, error: "The root is a page, not a folder." };
6809
7027
  }
@@ -6815,7 +7033,7 @@ function createShareManager(options) {
6815
7033
  share.routes.push(asked);
6816
7034
  share.refused = share.refused.filter((path) => !routeAllowed([asked], path));
6817
7035
  options.live.nudge("share");
6818
- return { ok: true, share: status() };
7036
+ return { ok: true, share: snapshot(share) };
6819
7037
  };
6820
7038
  const update = async (input) => {
6821
7039
  const share = active;
@@ -6833,7 +7051,7 @@ function createShareManager(options) {
6833
7051
  share.layout = parsed.manifest.layout;
6834
7052
  options.live.nudge("share");
6835
7053
  options.live.nudge("config");
6836
- return { ok: true, share: status() };
7054
+ return { ok: true, share: snapshot(share) };
6837
7055
  };
6838
7056
  const viewerConfig = async (grantId) => {
6839
7057
  const share = active;
@@ -6923,8 +7141,8 @@ var RENAMES_PATH = ".leglas/renames.json";
6923
7141
  async function readRenames(cwd) {
6924
7142
  try {
6925
7143
  const raw = await readFile10(join10(cwd, RENAMES_PATH), "utf8");
6926
- const parsed = JSON.parse(raw);
6927
- if (parsed.renames === null || typeof parsed.renames !== "object")
7144
+ const parsed = parseJson(raw);
7145
+ if (!isJsonRecord(parsed) || !isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames))
6928
7146
  return {};
6929
7147
  return Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
6930
7148
  } catch {
@@ -6956,6 +7174,115 @@ import http4 from "http";
6956
7174
  import net3 from "net";
6957
7175
  import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
6958
7176
 
7177
+ // ../server/dist/frame-policy.js
7178
+ var FRAMING_TIMEOUT_MS = 4e3;
7179
+ var FRAME_ANCESTORS = "frame-ancestors";
7180
+ var QUOTE_CAP = 160;
7181
+ function quote(value) {
7182
+ return value.length <= QUOTE_CAP ? value : `${value.slice(0, QUOTE_CAP - 1)}\u2026`;
7183
+ }
7184
+ function defaultPort(protocol) {
7185
+ return protocol === "https:" || protocol === "wss:" ? "443" : "80";
7186
+ }
7187
+ function portOf(url) {
7188
+ return url.port === "" ? defaultPort(url.protocol) : url.port;
7189
+ }
7190
+ function schemeAdmits(scheme, url) {
7191
+ return url.protocol === scheme || scheme === "http:" && url.protocol === "https:";
7192
+ }
7193
+ var HOST_SOURCE = /^(?:([a-z][a-z0-9+.-]*):\/\/)?(\*|\*\.[^:/]+|\[[0-9a-f:.]+\]|[^:/*[\]]+)(?::(\*|\d+))?(\/.*)?$/i;
7194
+ function sourceAdmits(source, embedder, target) {
7195
+ const lower = source.toLowerCase();
7196
+ if (lower === "'self'")
7197
+ return embedder.origin === target.origin;
7198
+ if (lower === "*")
7199
+ return embedder.protocol === "http:" || embedder.protocol === "https:";
7200
+ if (/^[a-z][a-z0-9+.-]*:$/.test(lower))
7201
+ return schemeAdmits(lower, embedder);
7202
+ if (lower.startsWith("'"))
7203
+ return false;
7204
+ const match = HOST_SOURCE.exec(lower);
7205
+ if (match === null)
7206
+ return false;
7207
+ const [, scheme, host = "", port, path] = match;
7208
+ const schemeOk = scheme === void 0 ? (
7209
+ // No scheme borrows the protected page's own, with the upgrade allowed.
7210
+ schemeAdmits(target.protocol, embedder)
7211
+ ) : schemeAdmits(`${scheme}:`, embedder);
7212
+ const hostname = embedder.hostname.toLowerCase();
7213
+ const hostOk = host === "*" ? true : host.startsWith("*.") ? hostname.endsWith(host.slice(1)) : hostname === host;
7214
+ const portOk = port === "*" ? true : port === void 0 ? portOf(embedder) === defaultPort(embedder.protocol) : portOf(embedder) === port;
7215
+ const pathOk = path === void 0 || path === "/" ? true : path.endsWith("/") ? embedder.pathname.startsWith(path) : embedder.pathname === path;
7216
+ return schemeOk && hostOk && portOk && pathOk;
7217
+ }
7218
+ function listAdmits(list, embedder, target) {
7219
+ const sources = list.split(/\s+/).filter((source) => source !== "");
7220
+ return sources.some((source) => sourceAdmits(source, embedder, target));
7221
+ }
7222
+ function ancestorLists(csp) {
7223
+ if (csp === null)
7224
+ return [];
7225
+ const lists = [];
7226
+ for (const policy of csp.split(",")) {
7227
+ for (const directive of policy.split(";")) {
7228
+ const trimmed = directive.trim();
7229
+ const name = trimmed.split(/\s+/, 1)[0]?.toLowerCase();
7230
+ if (name === FRAME_ANCESTORS)
7231
+ lists.push(trimmed.slice(FRAME_ANCESTORS.length).trim());
7232
+ }
7233
+ }
7234
+ return lists;
7235
+ }
7236
+ function framingFor(headers, target, embedder) {
7237
+ const targetUrl = new URL(target);
7238
+ const embedderUrl = new URL(embedder);
7239
+ const lists = ancestorLists(headers.get("content-security-policy"));
7240
+ if (lists.length > 0) {
7241
+ const refusing = lists.find((list) => !listAdmits(list, embedderUrl, targetUrl));
7242
+ return refusing === void 0 ? { framable: true } : {
7243
+ framable: false,
7244
+ refusal: {
7245
+ header: "content-security-policy",
7246
+ value: quote(`${FRAME_ANCESTORS} ${refusing}`.trim())
7247
+ }
7248
+ };
7249
+ }
7250
+ const raw = headers.get("x-frame-options");
7251
+ if (raw === null)
7252
+ return { framable: true };
7253
+ const values = new Set(raw.split(",").flatMap((value) => {
7254
+ const normal = value.trim().toLowerCase();
7255
+ return normal === "" ? [] : [normal];
7256
+ }));
7257
+ const refused = {
7258
+ framable: false,
7259
+ refusal: { header: "x-frame-options", value: quote(raw.trim()) }
7260
+ };
7261
+ if (values.size > 1) {
7262
+ return values.has("deny") || values.has("sameorigin") || values.has("allowall") ? refused : { framable: true };
7263
+ }
7264
+ if (values.has("deny"))
7265
+ return refused;
7266
+ if (values.has("sameorigin")) {
7267
+ return embedderUrl.origin === targetUrl.origin ? { framable: true } : refused;
7268
+ }
7269
+ return { framable: true };
7270
+ }
7271
+ async function checkFraming(target, embedder, fetcher = fetch) {
7272
+ try {
7273
+ const response = await fetcher(target, {
7274
+ headers: { accept: "text/html" },
7275
+ redirect: "follow",
7276
+ signal: AbortSignal.timeout(FRAMING_TIMEOUT_MS)
7277
+ });
7278
+ void response.body?.cancel().catch(() => {
7279
+ });
7280
+ return framingFor(response.headers, response.url === "" ? target : response.url, embedder);
7281
+ } catch {
7282
+ return { framable: null };
7283
+ }
7284
+ }
7285
+
6959
7286
  // ../server/dist/server-info.js
6960
7287
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
6961
7288
  import { dirname as dirname7, join as join11 } from "path";
@@ -6972,16 +7299,16 @@ async function writeServerInfo(cwd, info) {
6972
7299
  const temporary = `${path}.${process.pid}.tmp`;
6973
7300
  await writeFile7(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
6974
7301
  `, "utf8");
6975
- await rename2(temporary, path).catch(async (error) => {
7302
+ await rename2(temporary, path).catch(async (cause) => {
6976
7303
  await rm4(temporary, { force: true }).catch(() => {
6977
7304
  });
6978
- throw error;
7305
+ throw cause;
6979
7306
  });
6980
7307
  }
6981
7308
  async function readServerInfo(cwd) {
6982
7309
  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))
7310
+ const value = parseJson(await readFile11(join11(cwd, SERVER_INFO_PATH), "utf8"));
7311
+ 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
7312
  return null;
6986
7313
  return { port: value.port, url: value.url, pid: value.pid };
6987
7314
  } catch {
@@ -7020,6 +7347,9 @@ var CONTENT_TYPES = {
7020
7347
  ".woff": "font/woff",
7021
7348
  ".woff2": "font/woff2"
7022
7349
  };
7350
+ function isContentExtension(extension) {
7351
+ return Object.hasOwn(CONTENT_TYPES, extension);
7352
+ }
7023
7353
  var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
7024
7354
  function sendJson2(res, status, body) {
7025
7355
  const payload = JSON.stringify(body);
@@ -7066,8 +7396,11 @@ function referenceName(value) {
7066
7396
  }).join("").trim().slice(0, 80);
7067
7397
  return safe || "image";
7068
7398
  }
7399
+ function isAddressInUse(cause) {
7400
+ return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EADDRINUSE";
7401
+ }
7069
7402
  function isKnownAgent(value) {
7070
- return typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
7403
+ return isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
7071
7404
  }
7072
7405
  function isAllowedMutationHost(hostname) {
7073
7406
  const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
@@ -7088,7 +7421,7 @@ function isLoopbackAddress(address) {
7088
7421
  function isTrustedMutation(req) {
7089
7422
  if (!isLoopbackAddress(req.socket.remoteAddress))
7090
7423
  return false;
7091
- if (typeof req.headers.host !== "string")
7424
+ if (!isString(req.headers.host))
7092
7425
  return false;
7093
7426
  let host;
7094
7427
  try {
@@ -7112,17 +7445,21 @@ function isEnded(request, failedIds) {
7112
7445
  return isTerminal(request.status) || failedIds.includes(request.id);
7113
7446
  }
7114
7447
  function jsonBody(body) {
7115
- let parsed;
7116
7448
  try {
7117
- parsed = JSON.parse(body || "{}");
7449
+ const parsed = parseJson(body || "{}");
7450
+ return isJsonRecord(parsed) ? parsed : null;
7118
7451
  } catch {
7119
7452
  return null;
7120
7453
  }
7121
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
7454
+ }
7455
+ function requestIntent(value) {
7456
+ if (value === null || value === void 0)
7457
+ return "";
7458
+ return isString(value) ? value.trim() : null;
7122
7459
  }
7123
7460
  function hasJsonBody(req) {
7124
7461
  const contentType = req.headers["content-type"];
7125
- return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
7462
+ return isString(contentType) && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
7126
7463
  }
7127
7464
  function probe(target, timeoutMs = 1e3) {
7128
7465
  return new Promise((resolve5) => {
@@ -7151,8 +7488,9 @@ function serveFrom(res, dir, relativePath) {
7151
7488
  return false;
7152
7489
  if (!existsSync3(candidate) || !statSync(candidate).isFile())
7153
7490
  return false;
7491
+ const extension = extname3(candidate);
7154
7492
  res.writeHead(200, {
7155
- "content-type": CONTENT_TYPES[extname3(candidate)] ?? "application/octet-stream",
7493
+ "content-type": isContentExtension(extension) ? CONTENT_TYPES[extension] : "application/octet-stream",
7156
7494
  "cache-control": "no-store"
7157
7495
  });
7158
7496
  createReadStream(candidate).pipe(res);
@@ -7409,7 +7747,7 @@ function listen(server, port) {
7409
7747
  const onListening = () => {
7410
7748
  server.removeListener("error", onError);
7411
7749
  const address = server.address();
7412
- resolve5(typeof address === "object" && address !== null ? address.port : port);
7750
+ resolve5(address !== null && !isString(address) ? address.port : port);
7413
7751
  };
7414
7752
  server.once("error", onError);
7415
7753
  server.once("listening", onListening);
@@ -7423,7 +7761,7 @@ async function bind2(server, requested) {
7423
7761
  try {
7424
7762
  return await listen(server, requested + attempt);
7425
7763
  } catch (error) {
7426
- if (error.code !== "EADDRINUSE")
7764
+ if (!isAddressInUse(error))
7427
7765
  throw error;
7428
7766
  }
7429
7767
  }
@@ -7435,14 +7773,16 @@ async function startServer(options) {
7435
7773
  let shares = null;
7436
7774
  let liveHealth = null;
7437
7775
  const live = options.live ?? createLiveHub();
7438
- const branches = createBranchRegistry({
7776
+ const branchOptions = {
7439
7777
  cwd,
7440
7778
  previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
7441
7779
  installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
7442
7780
  devCommand: config?.devCommand,
7443
- onChange: () => live.nudge("config"),
7444
- ...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
7445
- });
7781
+ onChange: () => live.nudge("config")
7782
+ };
7783
+ if (options.startWorktree !== void 0)
7784
+ branchOptions.startWorktree = options.startWorktree;
7785
+ const branches = createBranchRegistry(branchOptions);
7446
7786
  const previewForConfig = (preview) => {
7447
7787
  if (preview.branch === void 0)
7448
7788
  return preview;
@@ -7556,9 +7896,9 @@ async function startServer(options) {
7556
7896
  return void updates.check({ force: true }).then((status) => sendJson2(res, 200, status));
7557
7897
  }
7558
7898
  if (path.endsWith("/install")) {
7559
- return void updates.update().then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 409, {
7899
+ return void updates.update().then((status) => sendJson2(res, 200, status), (cause) => sendJson2(res, 409, {
7560
7900
  ok: false,
7561
- error: error instanceof Error ? error.message : String(error)
7901
+ error: cause instanceof Error ? cause.message : String(cause)
7562
7902
  }));
7563
7903
  }
7564
7904
  if (!hasJsonBody(req))
@@ -7569,12 +7909,12 @@ async function startServer(options) {
7569
7909
  const parsed = jsonBody(body);
7570
7910
  if (parsed === null)
7571
7911
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7572
- if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
7912
+ if (!isString(parsed.version) || parsed.version.trim() === "") {
7573
7913
  return sendJson2(res, 400, { ok: false, error: "Body needs a version." });
7574
7914
  }
7575
- void updates.skip(parsed.version).then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 400, {
7915
+ void updates.skip(parsed.version).then((status) => sendJson2(res, 200, status), (cause) => sendJson2(res, 400, {
7576
7916
  ok: false,
7577
- error: error instanceof Error ? error.message : String(error)
7917
+ error: cause instanceof Error ? cause.message : String(cause)
7578
7918
  }));
7579
7919
  });
7580
7920
  }
@@ -7592,10 +7932,10 @@ async function startServer(options) {
7592
7932
  if (parsed === null) {
7593
7933
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7594
7934
  }
7595
- const result = await shares?.create(parsed).catch((error) => ({
7935
+ const result = await shares?.create(parsed).catch((cause) => ({
7596
7936
  ok: false,
7597
7937
  status: 500,
7598
- error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
7938
+ error: `Leglas could not start the share (${cause instanceof Error ? cause.message : String(cause)}).`
7599
7939
  }));
7600
7940
  if (result === void 0) {
7601
7941
  return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
@@ -7677,6 +8017,24 @@ async function startServer(options) {
7677
8017
  warnings: configWarnings
7678
8018
  }));
7679
8019
  }
8020
+ if (path === `${LEGLAS_PREFIX}/api/previews/framing` && req.method === "GET") {
8021
+ const site = req.headers["sec-fetch-site"];
8022
+ if (site !== void 0 && site !== "same-origin" && site !== "none") {
8023
+ return sendJson2(res, 403, { ok: false, error: "Only the interface can ask this." });
8024
+ }
8025
+ const title = query.get("title");
8026
+ return void livePreviewDefinitions().then(async (previews) => {
8027
+ const preview = previews.find((entry) => entry.title === title);
8028
+ if (preview === void 0) {
8029
+ return sendJson2(res, 404, { ok: false, error: "There is no direction by that name." });
8030
+ }
8031
+ if (preview.file !== void 0 || preview.branch !== void 0 || !/^https?:\/\//i.test(preview.url)) {
8032
+ return sendJson2(res, 200, { framable: true });
8033
+ }
8034
+ const embedder = `http://${req.headers.host ?? "localhost"}${LEGLAS_PREFIX}/`;
8035
+ sendJson2(res, 200, await checkFraming(preview.url, embedder));
8036
+ });
8037
+ }
7680
8038
  if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
7681
8039
  let body = "";
7682
8040
  req.on("data", (chunk) => body += chunk);
@@ -7685,7 +8043,7 @@ async function startServer(options) {
7685
8043
  if (parsed === null) {
7686
8044
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7687
8045
  }
7688
- if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
8046
+ if (!isString(parsed.title) || parsed.title.trim() === "") {
7689
8047
  return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
7690
8048
  }
7691
8049
  const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
@@ -7721,7 +8079,7 @@ async function startServer(options) {
7721
8079
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7722
8080
  }
7723
8081
  const titles = parsed.titles;
7724
- if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
8082
+ if (!Array.isArray(titles) || titles.length === 0 || !titles.every((title) => isString(title) && title.trim() !== "")) {
7725
8083
  return sendJson2(res, 400, {
7726
8084
  ok: false,
7727
8085
  error: "Body needs a non-empty array of direction titles."
@@ -7753,7 +8111,7 @@ async function startServer(options) {
7753
8111
  }
7754
8112
  if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
7755
8113
  const declaredLength = req.headers["content-length"];
7756
- if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
8114
+ if (isString(declaredLength) && Number(declaredLength) > REFERENCE_MAX_BYTES) {
7757
8115
  return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
7758
8116
  }
7759
8117
  const chunks = [];
@@ -7828,7 +8186,7 @@ async function startServer(options) {
7828
8186
  });
7829
8187
  }
7830
8188
  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)))) {
8189
+ if (parsed.references !== void 0 && (!Array.isArray(parsed.references) || !parsed.references.every((reference) => isString(reference) && /^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
7832
8190
  return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
7833
8191
  }
7834
8192
  const references = parsed.references ?? [];
@@ -7842,23 +8200,26 @@ async function startServer(options) {
7842
8200
  });
7843
8201
  }
7844
8202
  }
7845
- const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
8203
+ const width = isNumber(parsed.width) && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
7846
8204
  const previews = await livePreviews();
7847
8205
  const preview = previews.find((entry) => entry.title === parsed.title);
7848
8206
  if (!preview) {
7849
8207
  return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7850
8208
  }
7851
8209
  const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
7852
- if (!parsed.intent?.trim() && notes.length === 0) {
8210
+ const intent = requestIntent(parsed.intent);
8211
+ if (intent === null) {
8212
+ return sendJson2(res, 400, { ok: false, error: "A request's words must be text." });
8213
+ }
8214
+ if (!intent && notes.length === 0) {
7853
8215
  return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7854
8216
  }
7855
- const intent = (parsed.intent ?? "").trim();
7856
8217
  const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
7857
8218
  const sameNotes = (entry) => {
7858
8219
  const before = [...entry.notes ?? []].sort().join(",");
7859
8220
  return before === notes.map((note) => note.id).sort().join(",");
7860
8221
  };
7861
- const compare = typeof parsed.compare === "string" && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
8222
+ const compare = isString(parsed.compare) && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
7862
8223
  const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
7863
8224
  if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
7864
8225
  // one forks the direction and the other rewrites it. Only a
@@ -7871,7 +8232,7 @@ async function startServer(options) {
7871
8232
  });
7872
8233
  }
7873
8234
  const address = server.address();
7874
- const requestPort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
8235
+ const requestPort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
7875
8236
  const id = newRequestId();
7876
8237
  const captured = await attachRequest(cwd, id, {
7877
8238
  origin: `http://127.0.0.1:${requestPort}`,
@@ -7883,20 +8244,26 @@ async function startServer(options) {
7883
8244
  }, { pool: browserPool });
7884
8245
  const composed = composeRequest(preview, intent, mode, notes, leglasCommand, captured);
7885
8246
  try {
7886
- await appendRequest(cwd, {
8247
+ const queued = {
7887
8248
  title: preview.title,
7888
8249
  url: preview.url,
7889
8250
  intent,
7890
8251
  // The ids travel with the request so a change made in place can
7891
8252
  // forget the notes it answered. A fork leaves them where they are:
7892
8253
  // 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
8254
  ...composed
7899
- }, id);
8255
+ };
8256
+ if (notes.length > 0)
8257
+ queued.notes = notes.map((entry) => entry.id);
8258
+ if (captured.attachments.length > 0)
8259
+ queued.attachments = captured.attachments;
8260
+ if (captured.skipped !== null)
8261
+ queued.captureNote = captured.skipped;
8262
+ if (compare !== null)
8263
+ queued.compare = compare.title;
8264
+ if (references.length > 0)
8265
+ queued.references = references;
8266
+ await appendRequest(cwd, queued, id);
7900
8267
  runner?.nudge();
7901
8268
  return sendJson2(res, 200, {
7902
8269
  ok: true,
@@ -7924,17 +8291,17 @@ async function startServer(options) {
7924
8291
  if (parsed === null) {
7925
8292
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7926
8293
  }
7927
- if (typeof parsed.title !== "string" || parsed.title === "") {
8294
+ if (!isString(parsed.title) || parsed.title === "") {
7928
8295
  return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
7929
8296
  }
7930
- if (parsed.note !== void 0 && typeof parsed.note !== "string") {
8297
+ if (parsed.note !== void 0 && !isString(parsed.note)) {
7931
8298
  return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
7932
8299
  }
7933
8300
  const preview = (await livePreviews()).find((entry) => entry.title === parsed.title);
7934
8301
  if (preview === void 0) {
7935
8302
  return sendJson2(res, 404, { ok: false, error: "No such direction." });
7936
8303
  }
7937
- const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
8304
+ const width = isNumber(parsed.width) && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
7938
8305
  const browser = await browserPool.acquire();
7939
8306
  if (browser === null) {
7940
8307
  return sendJson2(res, 503, {
@@ -7942,9 +8309,9 @@ async function startServer(options) {
7942
8309
  error: browserPool.reason() ?? NO_BROWSER
7943
8310
  });
7944
8311
  }
7945
- const annotations = typeof parsed.note === "string" ? (await readAnnotations(cwd).catch(() => [])).filter((entry) => entry.id === parsed.note && entry.title === preview.title) : [];
8312
+ const annotations = isString(parsed.note) ? (await readAnnotations(cwd).catch(() => [])).filter((entry) => entry.id === parsed.note && entry.title === preview.title) : [];
7946
8313
  const address = server.address();
7947
- const capturePort = typeof address === "object" && address !== null ? address.port : options.port ?? DEFAULT_PORT;
8314
+ const capturePort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
7948
8315
  const controller = new AbortController();
7949
8316
  const timeoutMarker = /* @__PURE__ */ Symbol("capture timeout");
7950
8317
  let timedOut;
@@ -7957,21 +8324,26 @@ async function startServer(options) {
7957
8324
  }, CAPTURE_DEADLINE_MS);
7958
8325
  timer.unref?.();
7959
8326
  try {
7960
- const work = capturePage(browser, {
8327
+ const captureInput = {
7961
8328
  url: previewUrl(`http://127.0.0.1:${capturePort}`, preview),
7962
8329
  width,
7963
- ...annotations.length === 0 ? {} : {
7964
- focuses: annotations.map((entry) => ({
8330
+ timeoutMs: CAPTURE_LOAD_MS,
8331
+ signal: controller.signal
8332
+ };
8333
+ if (annotations.length > 0) {
8334
+ captureInput.focuses = annotations.map((entry) => {
8335
+ const focus = {
7965
8336
  selector: entry.anchor.selector,
7966
8337
  text: entry.anchor.text,
7967
8338
  tag: entry.anchor.tag,
7968
- ...entry.anchor.region === void 0 ? {} : { region: entry.anchor.region },
7969
8339
  rect: entry.anchor.rect
7970
- }))
7971
- },
7972
- timeoutMs: CAPTURE_LOAD_MS,
7973
- signal: controller.signal
7974
- });
8340
+ };
8341
+ if (entry.anchor.region !== void 0)
8342
+ focus.region = entry.anchor.region;
8343
+ return focus;
8344
+ });
8345
+ }
8346
+ const work = capturePage(browser, captureInput);
7975
8347
  const result = await Promise.race([work, timeout]);
7976
8348
  if (result === timeoutMarker) {
7977
8349
  return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
@@ -7979,7 +8351,7 @@ async function startServer(options) {
7979
8351
  clearTimeout(timer);
7980
8352
  const crop = annotations.length > 0 ? result.crops[0] : null;
7981
8353
  const shot = crop?.shot ?? result.frame;
7982
- const noteSuffix = typeof parsed.note === "string" ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
8354
+ const noteSuffix = isString(parsed.note) ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
7983
8355
  const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
7984
8356
  const relativeFile = `${CAPTURES_DIR}/show/${name}`;
7985
8357
  await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
@@ -8041,7 +8413,7 @@ async function startServer(options) {
8041
8413
  if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
8042
8414
  return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
8043
8415
  }
8044
- if (parsed.run !== void 0 && typeof parsed.run !== "string") {
8416
+ if (parsed.run !== void 0 && !isString(parsed.run)) {
8045
8417
  return sendJson2(res, 400, {
8046
8418
  ok: false,
8047
8419
  error: "The custom run command must be a string."
@@ -8061,7 +8433,7 @@ async function startServer(options) {
8061
8433
  error: "Custom agents manage effort in their own command."
8062
8434
  });
8063
8435
  }
8064
- if (typeof parsed.run !== "string") {
8436
+ if (!isString(parsed.run)) {
8065
8437
  return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
8066
8438
  }
8067
8439
  const template = parseTemplate(parsed.run);
@@ -8069,17 +8441,18 @@ async function startServer(options) {
8069
8441
  return sendJson2(res, 400, { ok: false, error: template.error });
8070
8442
  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
8443
  }
8072
- if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.includes(effort)) {
8444
+ if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.some((supported) => supported === effort)) {
8073
8445
  return sendJson2(res, 400, {
8074
8446
  ok: false,
8075
8447
  error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
8076
8448
  });
8077
8449
  }
8078
- return void saveAgentChoice(cwd, {
8079
- agent: parsed.agent,
8080
- ...effort === void 0 ? {} : { effort }
8081
- }).then(() => {
8082
- runner?.prepare(parsed.agent);
8450
+ const agent = parsed.agent;
8451
+ const choice = { agent };
8452
+ if (effort !== void 0)
8453
+ choice.effort = effort;
8454
+ return void saveAgentChoice(cwd, choice).then(() => {
8455
+ runner?.prepare(agent);
8083
8456
  sendJson2(res, 200, { ok: true });
8084
8457
  }, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
8085
8458
  });
@@ -8092,7 +8465,7 @@ async function startServer(options) {
8092
8465
  if (parsed === null) {
8093
8466
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8094
8467
  }
8095
- if (typeof parsed.watching !== "boolean") {
8468
+ if (!isBoolean(parsed.watching)) {
8096
8469
  return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
8097
8470
  }
8098
8471
  lastSeen = parsed.watching ? Date.now() : null;
@@ -8108,6 +8481,7 @@ async function startServer(options) {
8108
8481
  startedAt: null,
8109
8482
  stopping: false,
8110
8483
  waiting: null,
8484
+ quietSince: null,
8111
8485
  failedIds: []
8112
8486
  };
8113
8487
  return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
@@ -8139,7 +8513,10 @@ async function startServer(options) {
8139
8513
  // says so rather than going on describing a live run.
8140
8514
  stopping: snapshot.running && snapshot.stopping,
8141
8515
  // Why a run that looks stalled is stalled, while it is stalled.
8142
- waiting: snapshot.running ? snapshot.waiting : null
8516
+ waiting: snapshot.running ? snapshot.waiting : null,
8517
+ // When a run that has gone quiet last said anything. A run on its
8518
+ // way out is not quiet, it is stopping.
8519
+ quietSince: snapshot.running && !snapshot.stopping ? snapshot.quietSince : null
8143
8520
  }
8144
8521
  }));
8145
8522
  }
@@ -8154,7 +8531,7 @@ async function startServer(options) {
8154
8531
  if (parsed === null) {
8155
8532
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8156
8533
  }
8157
- if (parsed.id !== void 0 && typeof parsed.id !== "string") {
8534
+ if (parsed.id !== void 0 && !isString(parsed.id)) {
8158
8535
  return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
8159
8536
  }
8160
8537
  return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
@@ -8171,7 +8548,7 @@ async function startServer(options) {
8171
8548
  if (parsed === null) {
8172
8549
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8173
8550
  }
8174
- if (typeof parsed.id !== "string") {
8551
+ if (!isString(parsed.id)) {
8175
8552
  return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
8176
8553
  }
8177
8554
  const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
@@ -8190,7 +8567,7 @@ async function startServer(options) {
8190
8567
  if (!await removeRequest(cwd, request.id)) {
8191
8568
  return sendJson2(res, 404, { ok: false, error: "No such request." });
8192
8569
  }
8193
- await appendRequest(cwd, {
8570
+ const retry = {
8194
8571
  title: request.title,
8195
8572
  url: request.url,
8196
8573
  intent: request.intent,
@@ -8198,16 +8575,23 @@ async function startServer(options) {
8198
8575
  // The prompt names the captures by path, and the embedded pipes
8199
8576
  // are not its only readers: watch, a custom command and
8200
8577
  // `requests --json` all hand the text over as it stands.
8201
- prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId),
8578
+ prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId)
8202
8579
  // The stored prompt already carries the mode's instructions;
8203
8580
  // 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);
8581
+ };
8582
+ if (request.mode !== void 0)
8583
+ retry.mode = request.mode;
8584
+ if (request.notes !== void 0)
8585
+ retry.notes = request.notes;
8586
+ if (attachments.length > 0)
8587
+ retry.attachments = attachments;
8588
+ if (request.captureNote !== void 0)
8589
+ retry.captureNote = request.captureNote;
8590
+ if (request.compare !== void 0)
8591
+ retry.compare = request.compare;
8592
+ if (request.references !== void 0)
8593
+ retry.references = request.references;
8594
+ await appendRequest(cwd, retry, retryId);
8211
8595
  runner?.nudge();
8212
8596
  return sendJson2(res, 200, { ok: true });
8213
8597
  } catch {
@@ -8229,7 +8613,7 @@ async function startServer(options) {
8229
8613
  if (parsed === null) {
8230
8614
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8231
8615
  }
8232
- if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
8616
+ if (!isString(parsed.title) || parsed.title.trim() === "") {
8233
8617
  return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
8234
8618
  }
8235
8619
  const anchor = anchorFrom(parsed.anchor);
@@ -8239,7 +8623,7 @@ async function startServer(options) {
8239
8623
  try {
8240
8624
  const annotation = await addAnnotation(cwd, {
8241
8625
  anchor,
8242
- note: typeof parsed.note === "string" ? parsed.note.trim() : "",
8626
+ note: isString(parsed.note) ? parsed.note.trim() : "",
8243
8627
  title: parsed.title
8244
8628
  });
8245
8629
  return sendJson2(res, 200, { ok: true, annotation });
@@ -8259,10 +8643,10 @@ async function startServer(options) {
8259
8643
  if (parsed === null) {
8260
8644
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8261
8645
  }
8262
- if (typeof parsed.id !== "string" || parsed.id === "") {
8646
+ if (!isString(parsed.id) || parsed.id === "") {
8263
8647
  return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
8264
8648
  }
8265
- if (typeof parsed.note !== "string") {
8649
+ if (!isString(parsed.note)) {
8266
8650
  return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
8267
8651
  }
8268
8652
  try {
@@ -8287,7 +8671,7 @@ async function startServer(options) {
8287
8671
  if (parsed === null) {
8288
8672
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8289
8673
  }
8290
- const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
8674
+ const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => isString(entry)) : [];
8291
8675
  if (ids.length === 0) {
8292
8676
  return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
8293
8677
  }
@@ -8309,7 +8693,7 @@ async function startServer(options) {
8309
8693
  if (parsed === null) {
8310
8694
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8311
8695
  }
8312
- if (typeof parsed.id !== "string") {
8696
+ if (!isString(parsed.id)) {
8313
8697
  return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
8314
8698
  }
8315
8699
  const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
@@ -8337,7 +8721,7 @@ async function startServer(options) {
8337
8721
  if (parsed === null) {
8338
8722
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8339
8723
  }
8340
- if (parsed.renames === null || typeof parsed.renames !== "object") {
8724
+ if (!isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames)) {
8341
8725
  return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
8342
8726
  }
8343
8727
  const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
@@ -8426,7 +8810,7 @@ async function startServer(options) {
8426
8810
  server.on("upgrade", (req, socket, head) => {
8427
8811
  handleUpgrade(req, socket, head, { remote: false });
8428
8812
  });
8429
- shares = createShareManager({
8813
+ const shareOptions = {
8430
8814
  live,
8431
8815
  previews: livePreviewDefinitions,
8432
8816
  previewsForConfig,
@@ -8440,10 +8824,13 @@ async function startServer(options) {
8440
8824
  publicOrigin: context.publicOrigin,
8441
8825
  grantId: context.grantId
8442
8826
  }),
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
- });
8827
+ upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true })
8828
+ };
8829
+ if (options.detectTunnels !== void 0)
8830
+ shareOptions.detectTunnels = options.detectTunnels;
8831
+ if (options.startTunnel !== void 0)
8832
+ shareOptions.startTunnel = options.startTunnel;
8833
+ shares = createShareManager(shareOptions);
8447
8834
  port = await bind2(server, options.port ?? DEFAULT_PORT);
8448
8835
  options.updates?.setPort(port);
8449
8836
  const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
@@ -8456,14 +8843,17 @@ async function startServer(options) {
8456
8843
  pid: process.pid
8457
8844
  }).catch(() => {
8458
8845
  });
8459
- runner = startRunner({
8846
+ const runnerOptions = {
8460
8847
  cwd,
8461
8848
  externallyAttached,
8462
8849
  onChange: () => live.nudge("requests"),
8463
- leglasCommand,
8464
- ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
8465
- ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
8466
- });
8850
+ leglasCommand
8851
+ };
8852
+ if (options.codexAppServer !== void 0)
8853
+ runnerOptions.codexAppServer = options.codexAppServer;
8854
+ if (options.claudeAgentSession !== void 0)
8855
+ runnerOptions.claudeAgentSession = options.claudeAgentSession;
8856
+ runner = startRunner(runnerOptions);
8467
8857
  options.updates?.onBusy(() => runner?.snapshot().running ?? false);
8468
8858
  options.updates?.onChange(() => live.nudge("update"));
8469
8859
  let closePromise = null;
@@ -8882,17 +9272,16 @@ async function runAdd(options, deps) {
8882
9272
  await ensureIgnored(options.cwd);
8883
9273
  const needsDevCommand = options.preview.branch !== void 0 && loaded.config?.devCommand === void 0;
8884
9274
  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
- });
9275
+ const added = { added: options.preview.title };
9276
+ if (options.preview.url !== void 0) added.url = options.preview.url;
9277
+ added.local = true;
9278
+ if (options.preview.branch !== void 0) added.branch = options.preview.branch;
9279
+ if (options.preview.file !== void 0) added.file = options.preview.file;
9280
+ 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.";
9281
+ if (needsDevCommand) {
9282
+ added.warning = "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config.";
9283
+ }
9284
+ envelope(deps, true, added);
8896
9285
  } else {
8897
9286
  deps.log(
8898
9287
  ` added ${options.preview.title} ${options.preview.url ?? options.preview.file ?? ""}`
@@ -9031,10 +9420,29 @@ function planShow({ title, previews, requests }) {
9031
9420
  };
9032
9421
  }
9033
9422
 
9034
- // src/run-show.ts
9423
+ // src/running.ts
9035
9424
  import { realpath as realpath2 } from "fs/promises";
9425
+
9426
+ // src/json.ts
9427
+ function isJsonObject(value) {
9428
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9429
+ }
9430
+ function isString2(value) {
9431
+ return typeof value === "string";
9432
+ }
9433
+ function isNumber2(value) {
9434
+ return typeof value === "number";
9435
+ }
9436
+ async function bodyOf(response) {
9437
+ try {
9438
+ return JSON.parse(await response.text());
9439
+ } catch {
9440
+ return null;
9441
+ }
9442
+ }
9443
+
9444
+ // src/running.ts
9036
9445
  var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
9037
- var CAPTURE_WAIT_MS = 3e4;
9038
9446
  async function sameDirectory(left, right) {
9039
9447
  const [a, b] = await Promise.all([
9040
9448
  realpath2(left).catch(() => left),
@@ -9042,6 +9450,42 @@ async function sameDirectory(left, right) {
9042
9450
  ]);
9043
9451
  return a === b;
9044
9452
  }
9453
+ async function findLeglas(cwd, port, request) {
9454
+ const server = port === null ? await readServerInfo(cwd) : null;
9455
+ const chosen = port ?? server?.port ?? DEFAULT_PORT;
9456
+ try {
9457
+ const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
9458
+ signal: AbortSignal.timeout(2e3)
9459
+ });
9460
+ if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
9461
+ const answered = await health.text().then((text2) => JSON.parse(text2)).catch(() => ({}));
9462
+ if (answered === null) return { ok: false, error: NOT_RUNNING };
9463
+ if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
9464
+ return {
9465
+ ok: false,
9466
+ error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
9467
+ };
9468
+ }
9469
+ return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
9470
+ } catch {
9471
+ return { ok: false, error: NOT_RUNNING };
9472
+ }
9473
+ }
9474
+
9475
+ // src/run-show.ts
9476
+ function isString3(value) {
9477
+ return typeof value === "string";
9478
+ }
9479
+ function hasCaptureError(value) {
9480
+ return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string";
9481
+ }
9482
+ function hasCaptureSize(value) {
9483
+ 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";
9484
+ }
9485
+ function isHydration(value) {
9486
+ return typeof value === "object" && value !== null && "framework" in value && typeof value.framework === "string" && "message" in value && typeof value.message === "string";
9487
+ }
9488
+ var CAPTURE_WAIT_MS = 3e4;
9045
9489
  async function runShow(options, deps) {
9046
9490
  const loaded = await loadConfig(options.cwd);
9047
9491
  const local = await readLocalPreviews(options.cwd);
@@ -9074,28 +9518,15 @@ async function runShow(options, deps) {
9074
9518
  requests: plan.requests
9075
9519
  };
9076
9520
  if (options.screenshot) {
9077
- const server = options.port === null ? await readServerInfo(options.cwd) : null;
9078
- const port = options.port ?? server?.port ?? DEFAULT_PORT;
9079
9521
  const fail = (error) => {
9080
9522
  if (options.json) deps.log(JSON.stringify({ ok: false, error }));
9081
9523
  else deps.error(error);
9082
9524
  return { exitCode: 1 };
9083
9525
  };
9084
9526
  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
- }
9527
+ const found = await findLeglas(options.cwd, options.port, request);
9528
+ if (!found.ok) return fail(found.error);
9529
+ const port = found.port;
9099
9530
  let response;
9100
9531
  try {
9101
9532
  response = await request(`http://127.0.0.1:${port}/leglas/api/capture`, {
@@ -9112,18 +9543,17 @@ async function runShow(options, deps) {
9112
9543
  const captured = await response.json().catch(() => ({}));
9113
9544
  if (!response.ok) {
9114
9545
  return fail(
9115
- typeof captured.error === "string" ? captured.error : "The direction could not be captured."
9546
+ hasCaptureError(captured) ? captured.error : "The direction could not be captured."
9116
9547
  );
9117
9548
  }
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.");
9549
+ if (!hasCaptureSize(captured)) return fail("The direction could not be captured.");
9120
9550
  envelope2.screenshot = {
9121
9551
  file: captured.file,
9122
9552
  width: captured.width,
9123
9553
  height: captured.height,
9124
9554
  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" ? {
9555
+ errors: Array.isArray(captured.errors) ? captured.errors.filter(isString3) : [],
9556
+ hydration: isHydration(captured.hydration) ? {
9127
9557
  framework: captured.hydration.framework,
9128
9558
  message: captured.hydration.message
9129
9559
  } : null,
@@ -9176,8 +9606,290 @@ async function runShow(options, deps) {
9176
9606
  return { exitCode: 0 };
9177
9607
  }
9178
9608
 
9609
+ // src/run-share.ts
9610
+ var TUNNEL_WAIT_MS = 6e4;
9611
+ var TUNNEL_POLL_MS = 500;
9612
+ function stringsOf(value) {
9613
+ return Array.isArray(value) && value.every(isString2) ? value : null;
9614
+ }
9615
+ function tunnelFrom(value) {
9616
+ if (value === void 0 || !isJsonObject(value)) return null;
9617
+ const provider = isString2(value.provider) ? value.provider : "The tunnel";
9618
+ switch (value.status) {
9619
+ case "none":
9620
+ return { status: "none" };
9621
+ case "starting":
9622
+ return { status: "starting", provider };
9623
+ case "ready":
9624
+ return isString2(value.url) ? { status: "ready", provider, url: value.url } : null;
9625
+ case "failed":
9626
+ return {
9627
+ status: "failed",
9628
+ provider,
9629
+ reason: isString2(value.reason) ? value.reason : "it stopped"
9630
+ };
9631
+ default:
9632
+ return null;
9633
+ }
9634
+ }
9635
+ function linkFrom(value) {
9636
+ if (!isJsonObject(value) || !isString2(value.localUrl) || !isNumber2(value.expiresAt)) return null;
9637
+ return {
9638
+ name: isString2(value.name) ? value.name : "",
9639
+ url: isString2(value.url) ? value.url : null,
9640
+ localUrl: value.localUrl,
9641
+ expiresAt: value.expiresAt
9642
+ };
9643
+ }
9644
+ function shareFrom(value) {
9645
+ if (value === void 0 || !isJsonObject(value)) return null;
9646
+ const titles = stringsOf(value.titles);
9647
+ const tunnel = tunnelFrom(value.tunnel);
9648
+ if (!isString2(value.scope) || titles === null || tunnel === null || !Array.isArray(value.grants)) {
9649
+ return null;
9650
+ }
9651
+ return {
9652
+ scope: value.scope,
9653
+ titles,
9654
+ reach: isString2(value.reach) ? value.reach : "open",
9655
+ tunnel,
9656
+ links: value.grants.flatMap((grant) => {
9657
+ const link = linkFrom(grant);
9658
+ return link === null ? [] : [link];
9659
+ })
9660
+ };
9661
+ }
9662
+ async function readShare(base, request) {
9663
+ try {
9664
+ const response = await request(`${base}/leglas/api/share`);
9665
+ if (!response.ok) {
9666
+ return {
9667
+ ok: false,
9668
+ error: `Leglas answered ${response.status} when asked what it is sharing.`,
9669
+ answered: true
9670
+ };
9671
+ }
9672
+ const payload = await bodyOf(response);
9673
+ if (!isJsonObject(payload)) {
9674
+ return { ok: false, error: "Leglas did not say what it is sharing.", answered: true };
9675
+ }
9676
+ const installed = stringsOf(payload.tunnels) ?? [];
9677
+ if (payload.share === null) return { ok: true, value: { share: null, installed } };
9678
+ const share = shareFrom(payload.share);
9679
+ return share === null ? {
9680
+ ok: false,
9681
+ error: "Leglas is sharing something this version of the command cannot read.",
9682
+ answered: true
9683
+ } : { ok: true, value: { share, installed } };
9684
+ } catch {
9685
+ return { ok: false, error: NOT_RUNNING, answered: false };
9686
+ }
9687
+ }
9688
+ async function post(base, path, body, request, fallback) {
9689
+ try {
9690
+ const response = await request(`${base}${path}`, {
9691
+ method: "POST",
9692
+ headers: { "content-type": "application/json" },
9693
+ body: JSON.stringify(body)
9694
+ });
9695
+ const payload = await bodyOf(response);
9696
+ if (!response.ok) {
9697
+ return {
9698
+ ok: false,
9699
+ error: isJsonObject(payload) && isString2(payload.error) ? payload.error : fallback,
9700
+ answered: true
9701
+ };
9702
+ }
9703
+ return { ok: true, value: payload };
9704
+ } catch {
9705
+ return { ok: false, error: NOT_RUNNING, answered: false };
9706
+ }
9707
+ }
9708
+ function describe2(share) {
9709
+ const [first, second] = share.titles;
9710
+ if (share.scope === "compare" && first !== void 0 && second !== void 0) {
9711
+ return `${first} and ${second}, side by side`;
9712
+ }
9713
+ if (share.scope === "direction" && first !== void 0) return first;
9714
+ const count = share.titles.length;
9715
+ return `the rail, ${count} ${count === 1 ? "direction" : "directions"}`;
9716
+ }
9717
+ function clock(at) {
9718
+ return new Date(at).toLocaleString([], { weekday: "short", hour: "numeric", minute: "2-digit" });
9719
+ }
9720
+ function tunnelLine(tunnel, installed) {
9721
+ switch (tunnel.status) {
9722
+ case "ready":
9723
+ return null;
9724
+ case "none":
9725
+ 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";
9726
+ case "starting":
9727
+ return `${tunnel.provider} is still starting; run npx leglas share again for the public link`;
9728
+ case "failed":
9729
+ return `${tunnel.provider} failed: ${tunnel.reason}`;
9730
+ }
9731
+ }
9732
+ async function runShare(options, deps) {
9733
+ const request = deps.fetch ?? fetch;
9734
+ const sleep = deps.sleep ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
9735
+ const fail = (error) => {
9736
+ if (options.json) deps.log(JSON.stringify({ ok: false, error }));
9737
+ else deps.error(error);
9738
+ return { exitCode: 1 };
9739
+ };
9740
+ const found = await findLeglas(options.cwd, options.port, request);
9741
+ if (!found.ok) return fail(found.error);
9742
+ if (options.stop) {
9743
+ const stopped = await post(
9744
+ found.base,
9745
+ "/leglas/api/share/stop",
9746
+ {},
9747
+ request,
9748
+ "Leglas could not stop sharing."
9749
+ );
9750
+ if (!stopped.ok) return fail(stopped.error);
9751
+ if (options.json) deps.log(JSON.stringify({ ok: true, stopped: true }));
9752
+ else deps.log(" stopped nothing is shared any more");
9753
+ return { exitCode: 0 };
9754
+ }
9755
+ const running = await readShare(found.base, request);
9756
+ if (!running.ok) return fail(running.error);
9757
+ const { installed } = running.value;
9758
+ const report = (share2, already, leftOut2) => {
9759
+ if (options.json) {
9760
+ deps.log(
9761
+ JSON.stringify({
9762
+ ok: true,
9763
+ alreadySharing: already,
9764
+ share: {
9765
+ scope: share2.scope,
9766
+ titles: share2.titles,
9767
+ reach: share2.reach,
9768
+ tunnel: share2.tunnel,
9769
+ links: share2.links
9770
+ },
9771
+ leftOut: leftOut2
9772
+ })
9773
+ );
9774
+ return { exitCode: 0 };
9775
+ }
9776
+ deps.log(` sharing ${describe2(share2)}${already ? " (already running)" : ""}`);
9777
+ if (share2.reach === "listed") {
9778
+ deps.log(" reach only the paths the share lists; allow more from the share panel");
9779
+ }
9780
+ for (const link of share2.links) {
9781
+ const label = link.name === "" ? "" : `${link.name} `;
9782
+ if (link.url !== null) deps.log(` link ${label}${link.url}`);
9783
+ else deps.log(` local ${label}${link.localUrl}`);
9784
+ }
9785
+ const tunnel = tunnelLine(share2.tunnel, installed);
9786
+ if (tunnel !== null) deps.log(` tunnel ${tunnel}`);
9787
+ const ends = share2.links.map((link) => link.expiresAt);
9788
+ if (ends.length > 0) deps.log(` until ${clock(Math.min(...ends))}`);
9789
+ for (const title of leftOut2) {
9790
+ deps.log(` left out ${title}: branch directions can't be shared yet`);
9791
+ }
9792
+ deps.log(" stop npx leglas share --stop");
9793
+ return { exitCode: 0 };
9794
+ };
9795
+ const loaded = await loadConfig(options.cwd);
9796
+ const local = await readLocalPreviews(options.cwd);
9797
+ const renames = await readRenames(options.cwd);
9798
+ const previews = [...loaded.config?.previews ?? [], ...local.previews];
9799
+ const known = [...new Set(previews.map((preview) => preview.title))];
9800
+ const titles = [];
9801
+ for (const asked of options.titles) {
9802
+ const resolved = resolveOrExplain(asked, known, renames);
9803
+ if (!resolved.ok) return fail(resolved.error);
9804
+ titles.push(resolved.title);
9805
+ }
9806
+ const [first, second] = titles;
9807
+ if (first !== void 0 && first === second) {
9808
+ return fail(`Both names are ${first}. Name two different directions to compare them.`);
9809
+ }
9810
+ const scope2 = titles.length === 0 ? "rail" : titles.length === 1 ? "direction" : "compare";
9811
+ const current = running.value.share;
9812
+ if (current !== null) {
9813
+ const same = titles.length === 0 || current.scope === scope2 && titles.length === current.titles.length && titles.every((title, index) => current.titles[index] === title);
9814
+ if (!same) {
9815
+ return fail(
9816
+ `Leglas is already sharing ${describe2(current)}. Stop it with npx leglas share --stop, then share again.`
9817
+ );
9818
+ }
9819
+ return report(current, true, []);
9820
+ }
9821
+ const branches = new Set(
9822
+ previews.flatMap((preview) => preview.branch === void 0 ? [] : [preview.title])
9823
+ );
9824
+ const shared = titles.length > 0 ? titles : known.filter((title) => !branches.has(title));
9825
+ const leftOut = titles.length > 0 ? [] : known.filter((title) => branches.has(title));
9826
+ if (shared.length === 0) {
9827
+ return fail("Nothing on the rail can be shared: branch directions can't be shared yet.");
9828
+ }
9829
+ const [, right] = shared;
9830
+ const body = {
9831
+ scope: scope2,
9832
+ titles: shared,
9833
+ layout: {
9834
+ order: shared,
9835
+ renames: Object.fromEntries(
9836
+ Object.entries(renames).filter(([title]) => shared.includes(title))
9837
+ ),
9838
+ collapsedFamilies: [],
9839
+ compare: scope2 === "compare" && right !== void 0 ? right : null,
9840
+ viewport: null
9841
+ },
9842
+ reach: options.reach,
9843
+ routes: []
9844
+ };
9845
+ if (options.tunnel !== null) body.tunnel = options.tunnel;
9846
+ const giveUp = async (error, known2 = true) => {
9847
+ const stopped = await post(found.base, "/leglas/api/share/stop", {}, request, "");
9848
+ const what = known2 ? "The share it started" : "A share";
9849
+ return fail(
9850
+ 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.`
9851
+ );
9852
+ };
9853
+ const created = await post(
9854
+ found.base,
9855
+ "/leglas/api/share",
9856
+ body,
9857
+ request,
9858
+ "Leglas could not start sharing."
9859
+ );
9860
+ if (!created.ok) {
9861
+ return created.answered ? fail(created.error) : giveUp("Leglas stopped answering while the share was starting.", false);
9862
+ }
9863
+ let share = isJsonObject(created.value) ? shareFrom(created.value.share) : null;
9864
+ if (share === null) {
9865
+ return giveUp("Leglas started a share this version of the command cannot read.");
9866
+ }
9867
+ for (let waited = 0; share.tunnel.status === "starting" && waited < TUNNEL_WAIT_MS; waited += TUNNEL_POLL_MS) {
9868
+ await sleep(TUNNEL_POLL_MS);
9869
+ const read = await readShare(found.base, request);
9870
+ if (!read.ok) return giveUp(read.error);
9871
+ if (read.value.share === null) {
9872
+ return fail("The share was stopped while its tunnel was starting.");
9873
+ }
9874
+ share = read.value.share;
9875
+ }
9876
+ return report(share, false, leftOut);
9877
+ }
9878
+
9879
+ // src/shutdown.ts
9880
+ var SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
9881
+ function installShutdown(stop, target = process) {
9882
+ let stopping = null;
9883
+ const shutdown = () => {
9884
+ stopping ??= stop();
9885
+ return stopping;
9886
+ };
9887
+ for (const signal of SHUTDOWN_SIGNALS) target.on(signal, () => void shutdown());
9888
+ return shutdown;
9889
+ }
9890
+
9179
9891
  // src/run-watch.ts
9180
- import { spawn as spawn3 } from "child_process";
9892
+ import { spawn as spawn4 } from "child_process";
9181
9893
  import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile14 } from "fs/promises";
9182
9894
  import { dirname as dirname12, join as join18 } from "path";
9183
9895
  var POLL_MS2 = 2e3;
@@ -9187,9 +9899,7 @@ async function saveTemplate(cwd, run4) {
9187
9899
  let config = {};
9188
9900
  try {
9189
9901
  const parsed = JSON.parse(await readFile16(path, "utf8"));
9190
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
9191
- config = parsed;
9192
- }
9902
+ if (isJsonObject(parsed)) config = parsed;
9193
9903
  } catch {
9194
9904
  }
9195
9905
  config.run = run4;
@@ -9205,7 +9915,7 @@ function spawnAgent(command, args, cwd) {
9205
9915
  settled = true;
9206
9916
  resolve5(outcome);
9207
9917
  };
9208
- const child = spawn3(command, args, { cwd, stdio: "inherit" });
9918
+ const child = spawn4(command, args, { cwd, stdio: "inherit" });
9209
9919
  child.on("error", (error) => settle({ ok: false, error: error.message }));
9210
9920
  child.on(
9211
9921
  "close",
@@ -9480,8 +10190,11 @@ function skipStartupCheck(env) {
9480
10190
  return env.CI !== void 0 && env.CI !== "" && env.CI !== "false" || !off(env.LEGLAS_NO_UPDATE_CHECK);
9481
10191
  }
9482
10192
  async function run3(options, deps) {
9483
- const loaded = await loadConfig(options.cwd);
9484
- const local = await readLocalPreviews(options.cwd);
10193
+ return runWithServices(options, deps);
10194
+ }
10195
+ async function runWithServices(options, deps, services = {}) {
10196
+ const loaded = await (services.loadConfig ?? loadConfig)(options.cwd);
10197
+ const local = await (services.readLocalPreviews ?? readLocalPreviews)(options.cwd);
9485
10198
  let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
9486
10199
  const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
9487
10200
  const previewErrors = [];
@@ -9532,8 +10245,8 @@ async function run3(options, deps) {
9532
10245
  const projectRoot = await realpath4(
9533
10246
  loaded.path === null ? options.cwd : dirname13(loaded.path)
9534
10247
  ).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({
10248
+ const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
10249
+ const serverOptions = {
9537
10250
  config,
9538
10251
  configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
9539
10252
  configWarnings,
@@ -9543,10 +10256,11 @@ async function run3(options, deps) {
9543
10256
  // directory does. Either way saved layout survives a port change.
9544
10257
  project: loaded.path ?? options.cwd,
9545
10258
  cwd: options.cwd,
9546
- leglasCommand: embeddedLeglasCommand(),
9547
- ...deps.updates === void 0 ? {} : { updates: deps.updates },
9548
- ...options.port === void 0 ? {} : { port: options.port }
9549
- });
10259
+ leglasCommand: embeddedLeglasCommand()
10260
+ };
10261
+ if (deps.updates !== void 0) serverOptions.updates = deps.updates;
10262
+ if (options.port !== void 0) serverOptions.port = options.port;
10263
+ const serverPromise = (services.startServer ?? startServer)(serverOptions);
9550
10264
  const [server, warning] = await Promise.all([serverPromise, ownerWarning]);
9551
10265
  if (warning !== null) configWarnings.push(warning);
9552
10266
  const url = `${server.url}${LEGLAS_PREFIX}`;
@@ -9635,6 +10349,7 @@ export {
9635
10349
  commandFor,
9636
10350
  detectFramework,
9637
10351
  findBrowser,
10352
+ installShutdown,
9638
10353
  isOwnCapture,
9639
10354
  nextRequest,
9640
10355
  parseArgs,
@@ -9655,6 +10370,7 @@ export {
9655
10370
  runList,
9656
10371
  runNew,
9657
10372
  runRequests,
10373
+ runShare,
9658
10374
  runShow,
9659
10375
  runWatch,
9660
10376
  surfaceSlug