leglas 1.1.1 → 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,12 +36,14 @@ 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
  }
42
42
  if (surface !== void 0) {
43
- return { kind: "error", message: `leglas new takes one surface name, received ${JSON.stringify(argument)} as well.` };
43
+ return {
44
+ kind: "error",
45
+ message: `leglas new takes one surface name, received ${JSON.stringify(argument)} as well.`
46
+ };
44
47
  }
45
48
  surface = argument;
46
49
  }
@@ -68,7 +71,6 @@ function parseAdd(rest) {
68
71
  json = true;
69
72
  continue;
70
73
  }
71
- if (argument === "--help" || argument === "-h") return { kind: "help" };
72
74
  const equals = argument.indexOf("=");
73
75
  const flag = equals === -1 ? argument : argument.slice(0, equals);
74
76
  let value;
@@ -78,9 +80,16 @@ function parseAdd(rest) {
78
80
  } else {
79
81
  value = argument.slice(equals + 1);
80
82
  }
81
- if (!["--title", "--url", "--note", "--tag", "--branch", "--file", "--based-on", "--asked-for"].includes(
82
- flag
83
- )) {
83
+ if (![
84
+ "--title",
85
+ "--url",
86
+ "--note",
87
+ "--tag",
88
+ "--branch",
89
+ "--file",
90
+ "--based-on",
91
+ "--asked-for"
92
+ ].includes(flag)) {
84
93
  return { kind: "error", message: `leglas add does not take ${flag}.` };
85
94
  }
86
95
  if (value === void 0 || value === "") {
@@ -96,7 +105,10 @@ function parseAdd(rest) {
96
105
  else tags.push(value);
97
106
  }
98
107
  if (title === void 0) {
99
- return { kind: "error", message: "leglas add needs --title, which is how the preview is identified." };
108
+ return {
109
+ kind: "error",
110
+ message: "leglas add needs --title, which is how the preview is identified."
111
+ };
100
112
  }
101
113
  if (url === void 0 && file === void 0) {
102
114
  return {
@@ -128,7 +140,6 @@ function parseClassify(rest) {
128
140
  json = true;
129
141
  continue;
130
142
  }
131
- if (argument === "--help" || argument === "-h") return { kind: "help" };
132
143
  const equals = argument.indexOf("=");
133
144
  const flag = equals === -1 ? argument : argument.slice(0, equals);
134
145
  if (flag !== "--change" && flag !== "--rewrite") {
@@ -153,7 +164,6 @@ function parseWatch(rest) {
153
164
  let port;
154
165
  for (let index = 0; index < rest.length; index += 1) {
155
166
  const argument = rest[index];
156
- if (argument === "--help" || argument === "-h") return { kind: "help" };
157
167
  const equals = argument.indexOf("=");
158
168
  const flag = equals === -1 ? argument : argument.slice(0, equals);
159
169
  if (flag !== "--run" && flag !== "--port") {
@@ -171,12 +181,87 @@ function parseWatch(rest) {
171
181
  continue;
172
182
  }
173
183
  const parsed = parsePort(flag, value);
174
- if (typeof parsed !== "number") return { kind: "error", message: parsed.error };
175
- port = parsed;
184
+ if ("error" in parsed) return { kind: "error", message: parsed.error };
185
+ port = parsed.port;
176
186
  }
177
187
  return { kind: "watch", run: run4, port };
178
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
+ }
179
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
+ }
180
265
  if (argv[0] === "new") return parseNew(argv.slice(1));
181
266
  if (argv[0] === "watch") return parseWatch(argv.slice(1));
182
267
  if (argv[0] === "add") return parseAdd(argv.slice(1));
@@ -203,7 +288,10 @@ function parseArgs(argv) {
203
288
  if (argument === "--to" || argument.startsWith("--to=")) {
204
289
  to = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
205
290
  if (to === void 0 || to === "") {
206
- return { kind: "error", message: "--to needs a path, for example --to src/components/hero.tsx" };
291
+ return {
292
+ kind: "error",
293
+ message: "--to needs a path, for example --to src/components/hero.tsx"
294
+ };
207
295
  }
208
296
  continue;
209
297
  }
@@ -222,7 +310,10 @@ function parseArgs(argv) {
222
310
  };
223
311
  }
224
312
  if (to === void 0) {
225
- return { kind: "error", message: "leglas keep needs --to, the path the winner should live at." };
313
+ return {
314
+ kind: "error",
315
+ message: "leglas keep needs --to, the path the winner should live at."
316
+ };
226
317
  }
227
318
  return { kind: "keep", title, to, json };
228
319
  }
@@ -328,12 +419,15 @@ function parseArgs(argv) {
328
419
  }
329
420
  if (flag === "--port") {
330
421
  const parsed = parsePort(flag, raw);
331
- if (typeof parsed !== "number") return { kind: "error", message: parsed.error };
332
- port = parsed;
422
+ if ("error" in parsed) return { kind: "error", message: parsed.error };
423
+ port = parsed.port;
333
424
  continue;
334
425
  }
335
426
  if (!/^\d+$/.test(raw)) {
336
- return { kind: "error", message: `--width needs a number, received ${JSON.stringify(raw)}.` };
427
+ return {
428
+ kind: "error",
429
+ message: `--width needs a number, received ${JSON.stringify(raw)}.`
430
+ };
337
431
  }
338
432
  width = Number(raw);
339
433
  if (width < MIN_SHOW_WIDTH || width > MAX_SHOW_WIDTH) {
@@ -366,6 +460,7 @@ function parseArgs(argv) {
366
460
  }
367
461
  return { kind: "show", title, json, screenshot, width, port };
368
462
  }
463
+ if (argv[0] === "share") return parseShare(argv.slice(1));
369
464
  const options = {
370
465
  port: void 0,
371
466
  userPort: void 0,
@@ -405,9 +500,9 @@ function parseArgs(argv) {
405
500
  continue;
406
501
  }
407
502
  const port = parsePort(flag, value);
408
- if (typeof port !== "number") return { kind: "error", message: port.error };
409
- if (flag === "--port") options.port = port;
410
- 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;
411
506
  }
412
507
  return { kind: "run", options };
413
508
  }
@@ -592,7 +687,10 @@ function planNew(options) {
592
687
  },
593
688
  {
594
689
  path: `${dir}/${slug}-a.tsx`,
595
- contents: placeholder(`${name}A`, `A first direction for ${slug}. Change it and watch it reload.`)
690
+ contents: placeholder(
691
+ `${name}A`,
692
+ `A first direction for ${slug}. Change it and watch it reload.`
693
+ )
596
694
  }
597
695
  ];
598
696
  const gitignore = ignoreEntry(options.gitignore);
@@ -830,7 +928,7 @@ function frameFor(title, requests) {
830
928
  return found;
831
929
  }
832
930
  function askedOf(title, requests) {
833
- 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()] : []);
834
932
  }
835
933
  function composeEntry(input) {
836
934
  const slug = `${input.date}-${slugify(input.surface)}`;
@@ -890,13 +988,27 @@ function composeEntry(input) {
890
988
  `, pictures };
891
989
  }
892
990
 
893
- // ../server/dist/config.js
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
+
1008
+ // ../server/dist/config/config.js
894
1009
  var DEFAULT_DEV_SERVER = "http://localhost:3000";
895
1010
  var DEFAULT_INSTALL_COMMAND = "npm install";
896
1011
  var IMPLICIT_PREVIEW = { title: "App", url: "/" };
897
- function isRecord(value) {
898
- return typeof value === "object" && value !== null && !Array.isArray(value);
899
- }
900
1012
  function isValidOrigin(value) {
901
1013
  try {
902
1014
  const url = new URL(value);
@@ -926,11 +1038,11 @@ function normalizeConfig(raw, options = {}) {
926
1038
  const requireDevCommand = options.requireDevCommand ?? true;
927
1039
  const errors = [];
928
1040
  const source = raw === void 0 || raw === null ? {} : raw;
929
- if (!isRecord(source)) {
1041
+ if (!isJsonRecord(source)) {
930
1042
  return { config: null, errors: ["Config must export an object."] };
931
1043
  }
932
1044
  const devServer = source["devServer"] ?? DEFAULT_DEV_SERVER;
933
- if (typeof devServer !== "string" || !isValidOrigin(devServer)) {
1045
+ if (!isString(devServer) || !isValidOrigin(devServer)) {
934
1046
  errors.push(`devServer must be an http(s) URL, received ${JSON.stringify(devServer)}.`);
935
1047
  }
936
1048
  const rawPreviews = source["previews"] ?? [IMPLICIT_PREVIEW];
@@ -942,14 +1054,14 @@ function normalizeConfig(raw, options = {}) {
942
1054
  const seenTitles = /* @__PURE__ */ new Set();
943
1055
  rawPreviews.forEach((entry, index) => {
944
1056
  const at = `previews[${index}]`;
945
- if (!isRecord(entry)) {
1057
+ if (!isJsonRecord(entry)) {
946
1058
  errors.push(`${at} must be an object.`);
947
1059
  return;
948
1060
  }
949
1061
  const title = entry["title"];
950
1062
  const url = entry["url"];
951
1063
  const file = entry["file"];
952
- if (typeof title !== "string" || title.trim() === "") {
1064
+ if (!isString(title) || title.trim() === "") {
953
1065
  errors.push(`${at} needs a title; the rail has nothing to show without one.`);
954
1066
  } else if (seenTitles.has(title)) {
955
1067
  errors.push(`${at} repeats the title ${JSON.stringify(title)}; titles must be unique.`);
@@ -957,22 +1069,22 @@ function normalizeConfig(raw, options = {}) {
957
1069
  seenTitles.add(title);
958
1070
  }
959
1071
  if (file !== void 0) {
960
- if (typeof file !== "string" || !isSafePreviewFile(file)) {
1072
+ if (!isString(file) || !isSafePreviewFile(file)) {
961
1073
  errors.push(`${at} has an unusable file ${JSON.stringify(file)}; use a path inside the project, like "directions/hero.html".`);
962
1074
  }
963
1075
  if (url !== void 0) {
964
1076
  errors.push(`${at} names a file and a url; a file preview's url is assigned by Leglas.`);
965
1077
  }
966
- } else if (typeof url !== "string" || url.trim() === "") {
1078
+ } else if (!isString(url) || url.trim() === "") {
967
1079
  errors.push(`${at} needs a url.`);
968
1080
  } else if (!isValidPreviewUrl(url)) {
969
1081
  errors.push(`${at} has url ${JSON.stringify(url)}; use a root-relative path ("/pricing") or a full URL.`);
970
1082
  }
971
1083
  const branch = entry["branch"];
972
1084
  if (branch !== void 0) {
973
- if (typeof branch !== "string" || !isSafeBranch(branch)) {
1085
+ if (!isString(branch) || !isSafeBranch(branch)) {
974
1086
  errors.push(`${at} has an unusable branch ${JSON.stringify(branch)}; use a plain git branch name.`);
975
- } else if (typeof url === "string" && !url.startsWith("/")) {
1087
+ } else if (isString(url) && !url.startsWith("/")) {
976
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.`);
977
1089
  }
978
1090
  if (file !== void 0) {
@@ -980,54 +1092,59 @@ function normalizeConfig(raw, options = {}) {
980
1092
  }
981
1093
  }
982
1094
  const basedOn = entry["basedOn"];
983
- if (basedOn !== void 0 && (typeof basedOn !== "string" || basedOn.trim() === "")) {
1095
+ if (basedOn !== void 0 && (!isString(basedOn) || basedOn.trim() === "")) {
984
1096
  errors.push(`${at} has a basedOn that is not a direction title.`);
985
1097
  }
986
1098
  const askedFor = entry["askedFor"];
987
- if (askedFor !== void 0 && (typeof askedFor !== "string" || askedFor.trim() === "")) {
1099
+ if (askedFor !== void 0 && (!isString(askedFor) || askedFor.trim() === "")) {
988
1100
  errors.push(`${at} has an askedFor that is not a change request.`);
989
1101
  }
990
1102
  const tags = entry["tags"];
991
- previews.push({
992
- title: typeof title === "string" ? title : "",
993
- url: typeof url === "string" ? url : "",
994
- note: typeof entry["note"] === "string" ? entry["note"] : void 0,
995
- tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
996
- ...typeof branch === "string" ? { branch } : {},
997
- ...typeof file === "string" ? { file } : {},
998
- ...typeof basedOn === "string" && basedOn.trim() !== "" ? { basedOn } : {},
999
- ...typeof askedFor === "string" && askedFor.trim() !== "" ? { askedFor } : {}
1000
- });
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);
1001
1118
  });
1002
1119
  const devCommand = source["devCommand"];
1003
- if (devCommand !== void 0 && typeof devCommand !== "string") {
1120
+ if (devCommand !== void 0 && !isString(devCommand)) {
1004
1121
  errors.push("devCommand must be a string.");
1005
- } else if (typeof devCommand === "string" && !devCommand.includes("{port}")) {
1122
+ } else if (isString(devCommand) && !devCommand.includes("{port}")) {
1006
1123
  errors.push(`devCommand must include {port}, so Leglas can start each checkout on a free port. Received ${JSON.stringify(devCommand)}.`);
1007
1124
  }
1008
1125
  if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
1009
1126
  errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
1010
1127
  }
1011
1128
  const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
1012
- if (typeof logDir !== "string" || logDir.trim() === "") {
1129
+ if (!isString(logDir) || logDir.trim() === "") {
1013
1130
  errors.push("logDir must be a non-empty string.");
1014
1131
  }
1015
1132
  const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
1016
- if (typeof installCommand !== "string" || installCommand.trim() === "") {
1133
+ if (!isString(installCommand) || installCommand.trim() === "") {
1017
1134
  errors.push("installCommand must be a non-empty string.");
1018
1135
  }
1019
1136
  const scanPreviews = source["scanPreviews"] ?? true;
1020
- if (typeof scanPreviews !== "boolean") {
1137
+ if (!isBoolean(scanPreviews)) {
1021
1138
  errors.push("scanPreviews must be a boolean.");
1022
1139
  }
1023
- if (errors.length > 0)
1140
+ if (errors.length > 0 || !isString(devServer) || !isBoolean(scanPreviews) || !isString(installCommand) || !isString(logDir))
1024
1141
  return { config: null, errors };
1025
1142
  return {
1026
1143
  config: {
1027
1144
  devServer,
1028
1145
  previews,
1029
1146
  scanPreviews,
1030
- devCommand: typeof devCommand === "string" ? devCommand : void 0,
1147
+ devCommand: isString(devCommand) ? devCommand : void 0,
1031
1148
  installCommand,
1032
1149
  logDir
1033
1150
  },
@@ -1035,7 +1152,7 @@ function normalizeConfig(raw, options = {}) {
1035
1152
  };
1036
1153
  }
1037
1154
 
1038
- // ../server/dist/agent-command.js
1155
+ // ../server/dist/agents/agent-command.js
1039
1156
  var WATCH_PATH = ".leglas/watch.json";
1040
1157
  var PROMPT_TOKEN = "{prompt}";
1041
1158
  var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
@@ -1043,17 +1160,17 @@ function tokenize(template) {
1043
1160
  const tokens = [];
1044
1161
  let current = "";
1045
1162
  let started = false;
1046
- let quote = null;
1163
+ let quote2 = null;
1047
1164
  for (const character of template) {
1048
- if (quote !== null) {
1049
- if (character === quote)
1050
- quote = null;
1165
+ if (quote2 !== null) {
1166
+ if (character === quote2)
1167
+ quote2 = null;
1051
1168
  else
1052
1169
  current += character;
1053
1170
  continue;
1054
1171
  }
1055
1172
  if (character === '"' || character === "'") {
1056
- quote = character;
1173
+ quote2 = character;
1057
1174
  started = true;
1058
1175
  continue;
1059
1176
  }
@@ -1067,8 +1184,8 @@ function tokenize(template) {
1067
1184
  current += character;
1068
1185
  started = true;
1069
1186
  }
1070
- if (quote !== null) {
1071
- 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.` };
1072
1189
  }
1073
1190
  if (started)
1074
1191
  tokens.push(current);
@@ -1117,7 +1234,7 @@ function nextRequest(requests, failed) {
1117
1234
  return requests.find((request) => request.status === "queued" && !failed.has(request.id)) ?? null;
1118
1235
  }
1119
1236
 
1120
- // ../server/dist/agents.js
1237
+ // ../server/dist/agents/agents.js
1121
1238
  import { spawn } from "child_process";
1122
1239
  import { constants, readdirSync } from "fs";
1123
1240
  import { access, mkdir, readFile, writeFile } from "fs/promises";
@@ -1125,10 +1242,7 @@ import { delimiter, dirname, isAbsolute, join, relative } from "path";
1125
1242
  var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
1126
1243
  var effortFlag = (effort) => effort === null ? [] : ["--effort", effort];
1127
1244
  var codexEffortConfig = (effort) => effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`];
1128
- var CODEX_WORKSPACE_CONFIG = [
1129
- "-c",
1130
- "sandbox_workspace_write.network_access=true"
1131
- ];
1245
+ var CODEX_WORKSPACE_CONFIG = ["-c", "sandbox_workspace_write.network_access=true"];
1132
1246
  var KNOWN_AGENTS = {
1133
1247
  claude: {
1134
1248
  name: "Claude",
@@ -1144,13 +1258,7 @@ var KNOWN_AGENTS = {
1144
1258
  "acceptEdits",
1145
1259
  ...effortFlag(effort)
1146
1260
  ],
1147
- terminalArgs: (prompt, effort = null, _images = []) => [
1148
- "-p",
1149
- prompt,
1150
- "--permission-mode",
1151
- "acceptEdits",
1152
- ...effortFlag(effort)
1153
- ],
1261
+ terminalArgs: (prompt, effort = null, _images = []) => ["-p", prompt, "--permission-mode", "acceptEdits", ...effortFlag(effort)],
1154
1262
  resumeArgs: (sessionId, prompt, effort = null, _images = []) => [
1155
1263
  "-p",
1156
1264
  "--resume",
@@ -1174,7 +1282,7 @@ var KNOWN_AGENTS = {
1174
1282
  ],
1175
1283
  activityVerified: true,
1176
1284
  // Every stream-json event names its session.
1177
- 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,
1178
1286
  authArgs: ["auth", "status"],
1179
1287
  // `claude auth status` prints JSON with a loggedIn boolean. Only that
1180
1288
  // field decides; any other shape stays unknown.
@@ -1237,7 +1345,7 @@ var KNOWN_AGENTS = {
1237
1345
  ...images.flatMap((image) => ["-i", image]),
1238
1346
  prompt
1239
1347
  ],
1240
- 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,
1241
1349
  activityVerified: true,
1242
1350
  authArgs: ["login", "status"],
1243
1351
  // `codex login status` exits 0 when logged in and nonzero when not.
@@ -1254,13 +1362,7 @@ var KNOWN_AGENTS = {
1254
1362
  // project is the one the user pointed Leglas at, which is the trust the
1255
1363
  // flag grants. It is the only permission the run needs: with it alone,
1256
1364
  // the edit and the shell command in the same run both executed.
1257
- args: (prompt, _effort = null, _images = []) => [
1258
- "-p",
1259
- prompt,
1260
- "--output-format",
1261
- "stream-json",
1262
- "--trust"
1263
- ],
1365
+ args: (prompt, _effort = null, _images = []) => ["-p", prompt, "--output-format", "stream-json", "--trust"],
1264
1366
  terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt, "--trust"],
1265
1367
  // `--resume [chatId]` is documented alongside `--continue` in the CLI
1266
1368
  // parameter reference, and every stream-json event carries the
@@ -1272,8 +1374,16 @@ var KNOWN_AGENTS = {
1272
1374
  // Images are accepted and ignored, like its other argument builders:
1273
1375
  // `cursor-agent` documents no flag for them, and the capture paths reach
1274
1376
  // it as text in the prompt either way.
1275
- resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json", "--trust"],
1276
- sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
1377
+ resumeArgs: (sessionId, prompt, _effort = null, _images = []) => [
1378
+ "-p",
1379
+ "--resume",
1380
+ sessionId,
1381
+ prompt,
1382
+ "--output-format",
1383
+ "stream-json",
1384
+ "--trust"
1385
+ ],
1386
+ sessionFrom: (event) => isString(event.session_id) && event.session_id !== "" ? event.session_id : null,
1277
1387
  // Read against cursor-agent 2026.09.02: every event carries the id, a
1278
1388
  // resume in this argument order answers under the same id and remembers
1279
1389
  // the earlier turn.
@@ -1365,7 +1475,7 @@ function agentSearchPath(env = process.env, platform = process.platform) {
1365
1475
  platform === "darwin" ? "/usr/local/bin" : void 0,
1366
1476
  platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
1367
1477
  platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
1368
- ].filter((entry) => typeof entry === "string" && entry !== "");
1478
+ ].filter((entry) => isString(entry) && entry !== "");
1369
1479
  return [...new Set(candidates)].join(delimiter);
1370
1480
  }
1371
1481
  function agentEnvironment(env = process.env) {
@@ -1409,23 +1519,23 @@ async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
1409
1519
  }));
1410
1520
  }
1411
1521
  function record(value) {
1412
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1522
+ return isJsonRecord(value) ? value : null;
1413
1523
  }
1414
1524
  function shownPath(value, cwd) {
1415
- if (typeof value !== "string" || value === "")
1525
+ if (!isString(value) || value === "")
1416
1526
  return null;
1417
1527
  if (!isAbsolute(value))
1418
1528
  return value;
1419
1529
  return relative(cwd, value) || ".";
1420
1530
  }
1421
1531
  function shownCommand(value) {
1422
- 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 : "";
1423
1533
  command = command.trim();
1424
1534
  const wrapped = /^(?:\S*\/)?(?:bash|sh|zsh)\s+-l?c\s+([\s\S]*)$/.exec(command);
1425
1535
  if (wrapped?.[1] !== void 0) {
1426
1536
  command = wrapped[1].trim();
1427
- const quote = command[0];
1428
- if ((quote === "'" || quote === '"') && command.endsWith(quote) && command.length > 1) {
1537
+ const quote2 = command[0];
1538
+ if ((quote2 === "'" || quote2 === '"') && command.endsWith(quote2) && command.length > 1) {
1429
1539
  command = command.slice(1, -1);
1430
1540
  }
1431
1541
  }
@@ -1442,7 +1552,7 @@ function claudeActivity(event, cwd) {
1442
1552
  return null;
1443
1553
  for (const rawBlock of message2.content) {
1444
1554
  const block = record(rawBlock);
1445
- if (block?.type !== "tool_use" || typeof block.name !== "string")
1555
+ if (block?.type !== "tool_use" || !isString(block.name))
1446
1556
  continue;
1447
1557
  const input = record(block.input);
1448
1558
  if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name)) {
@@ -1508,7 +1618,7 @@ function cursorActivity(event, cwd) {
1508
1618
  function activityFrom(agent, line, cwd = process.cwd()) {
1509
1619
  let event;
1510
1620
  try {
1511
- event = record(JSON.parse(line));
1621
+ event = record(parseJson(line));
1512
1622
  } catch {
1513
1623
  return null;
1514
1624
  }
@@ -1527,7 +1637,7 @@ function sessionFrom(agent, line) {
1527
1637
  return null;
1528
1638
  let event;
1529
1639
  try {
1530
- event = record(JSON.parse(line));
1640
+ event = record(parseJson(line));
1531
1641
  } catch {
1532
1642
  return null;
1533
1643
  }
@@ -1540,29 +1650,29 @@ function retryFrom(agent, line) {
1540
1650
  return null;
1541
1651
  let event;
1542
1652
  try {
1543
- event = record(JSON.parse(line));
1653
+ event = record(parseJson(line));
1544
1654
  } catch {
1545
1655
  return null;
1546
1656
  }
1547
1657
  if (event === null || event.type !== "system" || event.subtype !== "api_retry")
1548
1658
  return null;
1549
- const attempt = typeof event.attempt === "number" ? event.attempt : 1;
1659
+ const attempt = isNumber(event.attempt) ? event.attempt : 1;
1550
1660
  return {
1551
1661
  attempt,
1552
- max: typeof event.max_retries === "number" ? event.max_retries : null,
1553
- status: typeof event.error_status === "number" ? event.error_status : null,
1554
- 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
1555
1665
  };
1556
1666
  }
1557
1667
  function isAgentChoice(value) {
1558
- return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
1668
+ return value === "custom" || isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
1559
1669
  }
1560
1670
  function isAgentEffort(value) {
1561
- return typeof value === "string" && AGENT_EFFORTS.includes(value);
1671
+ return isString(value) && AGENT_EFFORTS.some((effort) => effort === value);
1562
1672
  }
1563
1673
  async function readWatchConfig(cwd) {
1564
1674
  try {
1565
- const parsed = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
1675
+ const parsed = parseJson(await readFile(join(cwd, WATCH_PATH), "utf8"));
1566
1676
  return record(parsed) ?? {};
1567
1677
  } catch {
1568
1678
  return {};
@@ -1575,7 +1685,7 @@ async function readAgentChoice(cwd) {
1575
1685
  return {
1576
1686
  agent,
1577
1687
  effort: agent !== null && agent !== "custom" && isAgentEffort(efforts?.[agent]) ? efforts[agent] : null,
1578
- run: typeof config.run === "string" && config.run !== "" ? config.run : null
1688
+ run: isString(config.run) && config.run !== "" ? config.run : null
1579
1689
  };
1580
1690
  }
1581
1691
  async function saveAgentChoice(cwd, choice) {
@@ -1600,7 +1710,7 @@ async function saveAgentChoice(cwd, choice) {
1600
1710
  `, "utf8");
1601
1711
  }
1602
1712
 
1603
- // ../server/dist/classify.js
1713
+ // ../server/dist/branches/classify.js
1604
1714
  var MANIFESTS = /* @__PURE__ */ new Set([
1605
1715
  "package.json",
1606
1716
  "pnpm-lock.yaml",
@@ -1645,7 +1755,11 @@ var IN_APP_STEPS = [
1645
1755
  'Register it: npx leglas add --title "<title>" --url "/?v-<surface>=<direction>".'
1646
1756
  ];
1647
1757
  function classifyDirection(input) {
1648
- const checkout = (reason) => ({ level: "checkout", reason, steps: CHECKOUT_STEPS });
1758
+ const checkout = (reason) => ({
1759
+ level: "checkout",
1760
+ reason,
1761
+ steps: CHECKOUT_STEPS
1762
+ });
1649
1763
  const manifest = input.changes.find((change) => isManifest(change.path));
1650
1764
  if (manifest !== void 0) {
1651
1765
  return checkout(`${manifest.path} changes the dependency set, and one running server cannot hold two.`);
@@ -1665,7 +1779,7 @@ function classifyDirection(input) {
1665
1779
  };
1666
1780
  }
1667
1781
 
1668
- // ../server/dist/find-config.js
1782
+ // ../server/dist/config/find-config.js
1669
1783
  import { existsSync } from "fs";
1670
1784
  import { dirname as dirname2, join as join2, parse } from "path";
1671
1785
  var CONFIG_BASENAMES = [
@@ -1692,7 +1806,7 @@ function findConfigFile(startDir) {
1692
1806
  }
1693
1807
  }
1694
1808
 
1695
- // ../server/dist/load-config.js
1809
+ // ../server/dist/config/load-config.js
1696
1810
  import { readFile as readFile2 } from "fs/promises";
1697
1811
  import { relative as relative2 } from "path";
1698
1812
  import { pathToFileURL } from "url";
@@ -1717,7 +1831,7 @@ async function loadConfig(cwd) {
1717
1831
  const message2 = error instanceof Error ? error.message : String(error);
1718
1832
  return { config: null, errors: [`${label} could not be loaded: ${message2}`], path };
1719
1833
  }
1720
- 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."] };
1721
1835
  return {
1722
1836
  config: result.config,
1723
1837
  errors: result.errors.map((error) => `${label}: ${error}`),
@@ -1725,7 +1839,7 @@ async function loadConfig(cwd) {
1725
1839
  };
1726
1840
  }
1727
1841
 
1728
- // ../server/dist/local-previews.js
1842
+ // ../server/dist/config/local-previews.js
1729
1843
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1730
1844
  import { dirname as dirname3, join as join3 } from "path";
1731
1845
  var LOCAL_PREVIEWS_PATH = ".leglas/previews.json";
@@ -1735,7 +1849,7 @@ async function readLocalPreviews(cwd) {
1735
1849
  try {
1736
1850
  raw = await readFile3(path, "utf8");
1737
1851
  } catch (error) {
1738
- 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;
1739
1853
  if (code === "ENOENT") {
1740
1854
  return { previews: [], errors: [] };
1741
1855
  }
@@ -1748,7 +1862,7 @@ async function readLocalPreviews(cwd) {
1748
1862
  }
1749
1863
  let parsed;
1750
1864
  try {
1751
- parsed = JSON.parse(raw);
1865
+ parsed = parseJson(raw);
1752
1866
  } catch (error) {
1753
1867
  return {
1754
1868
  previews: [],
@@ -1759,7 +1873,10 @@ async function readLocalPreviews(cwd) {
1759
1873
  }
1760
1874
  const result = normalizeConfig(parsed, { requireDevCommand: false });
1761
1875
  if (result.config === null) {
1762
- return { previews: [], errors: result.errors.map((error) => `${LOCAL_PREVIEWS_PATH}: ${error}`) };
1876
+ return {
1877
+ previews: [],
1878
+ errors: result.errors.map((error) => `${LOCAL_PREVIEWS_PATH}: ${error}`)
1879
+ };
1763
1880
  }
1764
1881
  return {
1765
1882
  previews: result.config.previews.map((preview) => ({ ...preview, local: true })),
@@ -1778,16 +1895,21 @@ async function addLocalPreview(cwd, input, shared) {
1778
1895
  error: `A preview called ${JSON.stringify(input.title)} already exists. Titles identify previews, so they have to be unique.`
1779
1896
  };
1780
1897
  }
1781
- const candidate = {
1782
- title: input.title,
1783
- ...input.url === void 0 ? {} : { url: input.url },
1784
- ...input.note === void 0 ? {} : { note: input.note },
1785
- ...input.tags === void 0 ? {} : { tags: input.tags },
1786
- ...input.branch === void 0 ? {} : { branch: input.branch },
1787
- ...input.file === void 0 ? {} : { file: input.file },
1788
- ...input.basedOn === void 0 ? {} : { basedOn: input.basedOn },
1789
- ...input.askedFor === void 0 ? {} : { askedFor: input.askedFor }
1790
- };
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;
1791
1913
  const check = normalizeConfig({ previews: [candidate] }, { requireDevCommand: false });
1792
1914
  if (check.config === null) {
1793
1915
  return { ok: false, error: check.errors.join(" ") };
@@ -1842,7 +1964,10 @@ function createProxyHandler(options) {
1842
1964
  function rewriteLocation(location, publicOrigin2) {
1843
1965
  if (location === void 0)
1844
1966
  return void 0;
1845
- for (const origin of [`${target.protocol}//${authority}`, `${target.protocol}//localhost:${port}`]) {
1967
+ for (const origin of [
1968
+ `${target.protocol}//${authority}`,
1969
+ `${target.protocol}//localhost:${port}`
1970
+ ]) {
1846
1971
  if (location.startsWith(origin))
1847
1972
  return publicOrigin2 + location.slice(origin.length);
1848
1973
  }
@@ -1864,7 +1989,7 @@ function createProxyHandler(options) {
1864
1989
  res.once("close", close);
1865
1990
  const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
1866
1991
  const headers = { ...upstreamRes.headers };
1867
- const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin2);
1992
+ const location = rewriteLocation(isString(headers.location) ? headers.location : void 0, publicOrigin2);
1868
1993
  if (location !== void 0)
1869
1994
  headers.location = location;
1870
1995
  res.writeHead(upstreamRes.statusCode ?? 502, headers);
@@ -1933,7 +2058,7 @@ function startProxyServer(options) {
1933
2058
  });
1934
2059
  const server = http.createServer((req, res) => {
1935
2060
  const address = server.address();
1936
- const port = typeof address === "object" && address !== null ? address.port : 0;
2061
+ const port = address !== null && !isString(address) ? address.port : 0;
1937
2062
  handler.request(req, res, `http://127.0.0.1:${port}`);
1938
2063
  });
1939
2064
  const sockets = /* @__PURE__ */ new Set();
@@ -1949,7 +2074,7 @@ function startProxyServer(options) {
1949
2074
  const onListening = () => {
1950
2075
  server.removeListener("error", onError);
1951
2076
  const address = server.address();
1952
- const port = typeof address === "object" && address !== null ? address.port : 0;
2077
+ const port = address !== null && !isString(address) ? address.port : 0;
1953
2078
  let closed = null;
1954
2079
  resolve5({
1955
2080
  active: () => open > 0,
@@ -1974,7 +2099,7 @@ function startProxyServer(options) {
1974
2099
  });
1975
2100
  }
1976
2101
 
1977
- // ../server/dist/browser.js
2102
+ // ../server/dist/capture/browser.js
1978
2103
  import { randomBytes } from "crypto";
1979
2104
  import nodeProcess from "process";
1980
2105
  import { spawn as nodeSpawn } from "child_process";
@@ -2024,8 +2149,20 @@ function firstOnPath(name, env) {
2024
2149
  return null;
2025
2150
  }
2026
2151
  var FOR_TESTING = [
2027
- ["chrome-mac-arm64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"],
2028
- ["chrome-mac-x64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"],
2152
+ [
2153
+ "chrome-mac-arm64",
2154
+ "Google Chrome for Testing.app",
2155
+ "Contents",
2156
+ "MacOS",
2157
+ "Google Chrome for Testing"
2158
+ ],
2159
+ [
2160
+ "chrome-mac-x64",
2161
+ "Google Chrome for Testing.app",
2162
+ "Contents",
2163
+ "MacOS",
2164
+ "Google Chrome for Testing"
2165
+ ],
2029
2166
  ["chrome-linux64", "chrome"],
2030
2167
  ["chrome-linux", "chrome"],
2031
2168
  ["chrome-win64", "chrome.exe"]
@@ -2069,7 +2206,7 @@ function findBrowser(search = {}) {
2069
2206
  const readdir4 = search.readdir ?? readableDirectories;
2070
2207
  const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
2071
2208
  for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
2072
- if (typeof candidate === "string" && candidate !== "" && exists(candidate))
2209
+ if (isString(candidate) && candidate !== "" && exists(candidate))
2073
2210
  return candidate;
2074
2211
  }
2075
2212
  const caches = cacheRoots(platform, home, readdir4);
@@ -2095,7 +2232,7 @@ function findBrowser(search = {}) {
2095
2232
  return installed;
2096
2233
  }
2097
2234
  if (platform === "win32") {
2098
- 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 !== "");
2099
2236
  const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) => join4(root, browser))));
2100
2237
  if (installed !== null)
2101
2238
  return installed;
@@ -2178,7 +2315,7 @@ function endpoint(process2, timeoutMs) {
2178
2315
  return;
2179
2316
  settled = true;
2180
2317
  clearTimeout(timer);
2181
- if (typeof value === "string")
2318
+ if (isString(value))
2182
2319
  resolve5(value);
2183
2320
  else
2184
2321
  reject(value);
@@ -2207,12 +2344,15 @@ var OWNER_FILE = "leglas-owner.json";
2207
2344
  var PROFILE_PREFIX = "leglas-browser-";
2208
2345
  var RECORD_GRACE_MS = 6e4;
2209
2346
  var REAP_CLOSE_MS = 2e3;
2347
+ function permissionDenied(cause) {
2348
+ return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EPERM";
2349
+ }
2210
2350
  function livePid(pid) {
2211
2351
  try {
2212
2352
  nodeProcess.kill(pid, 0);
2213
2353
  return true;
2214
2354
  } catch (error) {
2215
- return typeof error === "object" && error !== null && error.code === "EPERM";
2355
+ return permissionDenied(error);
2216
2356
  }
2217
2357
  }
2218
2358
  async function closeOrphan(url, connect) {
@@ -2242,7 +2382,7 @@ async function closeOrphan(url, connect) {
2242
2382
  }
2243
2383
  async function reapOrphanedBrowsers(deps = {}) {
2244
2384
  const root = deps.tmpdir ?? osTmpdir();
2245
- const clock = deps.now ?? (() => Date.now());
2385
+ const clock2 = deps.now ?? (() => Date.now());
2246
2386
  const list = deps.list ?? (async (path) => (await readdir(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
2247
2387
  const read = deps.read ?? ((path) => readFile4(path, "utf8"));
2248
2388
  const alive = deps.alive ?? livePid;
@@ -2275,15 +2415,16 @@ async function reapOrphanedBrowsers(deps = {}) {
2275
2415
  let record3 = null;
2276
2416
  try {
2277
2417
  const raw = await read(join4(directory, OWNER_FILE));
2278
- record3 = raw === null ? null : JSON.parse(raw);
2418
+ const parsed = raw === null ? null : parseJson(raw);
2419
+ record3 = isJsonRecord(parsed) ? parsed : null;
2279
2420
  } catch {
2280
2421
  }
2281
- const owner = typeof record3?.owner === "number" ? record3.owner : null;
2422
+ const owner = isNumber(record3?.owner) ? record3.owner : null;
2282
2423
  if (owner !== null && alive(owner))
2283
2424
  continue;
2284
- if (owner === null && clock() - details.createdAt < RECORD_GRACE_MS)
2425
+ if (owner === null && clock2() - details.createdAt < RECORD_GRACE_MS)
2285
2426
  continue;
2286
- const endpoint2 = typeof record3?.ws === "string" && record3.ws !== "" ? record3.ws : null;
2427
+ const endpoint2 = isString(record3?.ws) && record3.ws !== "" ? record3.ws : null;
2287
2428
  if (endpoint2 !== null && await closeOrphan(endpoint2, connect))
2288
2429
  reaped += 1;
2289
2430
  await remove(directory).catch(() => {
@@ -2292,7 +2433,7 @@ async function reapOrphanedBrowsers(deps = {}) {
2292
2433
  return reaped;
2293
2434
  }
2294
2435
  async function launchBrowser(executable, options = {}) {
2295
- const spawn4 = options.spawn ?? nodeSpawn;
2436
+ const spawn5 = options.spawn ?? nodeSpawn;
2296
2437
  const connect = options.connect ?? connectWebSocket;
2297
2438
  const commandTimeoutMs = options.commandTimeoutMs ?? 3e4;
2298
2439
  const userDataDir = join4(options.tmpdir ?? osTmpdir(), `leglas-browser-${randomBytes(8).toString("hex")}`);
@@ -2305,7 +2446,7 @@ async function launchBrowser(executable, options = {}) {
2305
2446
  }
2306
2447
  };
2307
2448
  owned({});
2308
- const process2 = spawn4(executable, [
2449
+ const process2 = spawn5(executable, [
2309
2450
  "--headless=new",
2310
2451
  "--remote-debugging-port=0",
2311
2452
  `--user-data-dir=${userDataDir}`,
@@ -2383,11 +2524,14 @@ async function launchBrowser(executable, options = {}) {
2383
2524
  socket.onMessage((text2) => {
2384
2525
  let message2;
2385
2526
  try {
2386
- message2 = JSON.parse(text2);
2527
+ const parsed = parseJson(text2);
2528
+ if (!isJsonRecord(parsed))
2529
+ return;
2530
+ message2 = parsed;
2387
2531
  } catch {
2388
2532
  return;
2389
2533
  }
2390
- if (typeof message2.id === "number") {
2534
+ if (isNumber(message2.id)) {
2391
2535
  const command = pending.get(message2.id);
2392
2536
  if (command === void 0)
2393
2537
  return;
@@ -2395,15 +2539,15 @@ async function launchBrowser(executable, options = {}) {
2395
2539
  clearTimeout(command.timer);
2396
2540
  const error = message2.error;
2397
2541
  if (error !== void 0) {
2398
- 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."));
2399
2543
  } else {
2400
2544
  command.resolve(message2.result);
2401
2545
  }
2402
2546
  return;
2403
2547
  }
2404
- if (typeof message2.method !== "string")
2548
+ if (!isString(message2.method))
2405
2549
  return;
2406
- const sessionId = typeof message2.sessionId === "string" ? message2.sessionId : "";
2550
+ const sessionId = isString(message2.sessionId) ? message2.sessionId : "";
2407
2551
  const key = `${sessionId}\0${message2.method}`;
2408
2552
  for (const listener of listeners.get(key) ?? [])
2409
2553
  listener(message2.params ?? {});
@@ -2426,17 +2570,16 @@ async function launchBrowser(executable, options = {}) {
2426
2570
  }, commandTimeoutMs);
2427
2571
  timer.unref?.();
2428
2572
  pending.set(id, {
2573
+ // SAFETY: The pending command id pairs this CDP result with the method and result type requested by its caller.
2429
2574
  resolve: (value) => resolve5(value),
2430
2575
  reject,
2431
2576
  timer
2432
2577
  });
2433
2578
  try {
2434
- socket.send(JSON.stringify({
2435
- id,
2436
- method,
2437
- params,
2438
- ...sessionId === void 0 ? {} : { sessionId }
2439
- }));
2579
+ const message2 = { id, method, params };
2580
+ if (sessionId !== void 0)
2581
+ message2.sessionId = sessionId;
2582
+ socket.send(JSON.stringify(message2));
2440
2583
  } catch {
2441
2584
  clearTimeout(timer);
2442
2585
  pending.delete(id);
@@ -2521,36 +2664,37 @@ function createBrowserPool(options = {}) {
2521
2664
  const launch = options.launch ?? launchBrowser;
2522
2665
  const idleMs = options.idleMs ?? 6e4;
2523
2666
  const setLater = options.setTimeout ?? ((callback, milliseconds) => {
2524
- const timer2 = setTimeout(callback, milliseconds);
2525
- timer2.unref?.();
2526
- return timer2;
2667
+ const timer = setTimeout(callback, milliseconds);
2668
+ timer.unref?.();
2669
+ return timer;
2527
2670
  });
2528
2671
  const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
2529
2672
  let browser = null;
2530
2673
  let exposed = null;
2531
2674
  let working = 0;
2532
2675
  let launching = null;
2533
- let timer = null;
2676
+ let cancelTimer = null;
2534
2677
  let lastReason = null;
2535
2678
  let closed = false;
2536
2679
  const clearIdle = () => {
2537
- if (timer === null)
2680
+ if (cancelTimer === null)
2538
2681
  return;
2539
- clearLater(timer);
2540
- timer = null;
2682
+ cancelTimer();
2683
+ cancelTimer = null;
2541
2684
  };
2542
2685
  const scheduleIdle = () => {
2543
2686
  clearIdle();
2544
2687
  if (browser === null || closed || working > 0)
2545
2688
  return;
2546
- timer = setLater(() => {
2547
- timer = null;
2689
+ const handle = setLater(() => {
2690
+ cancelTimer = null;
2548
2691
  const retiring = browser;
2549
2692
  browser = null;
2550
2693
  exposed = null;
2551
2694
  void retiring?.close().catch(() => {
2552
2695
  });
2553
2696
  }, idleMs);
2697
+ cancelTimer = () => clearLater(handle);
2554
2698
  };
2555
2699
  const wrap = (launched) => ({
2556
2700
  withPage: async (work) => {
@@ -2600,8 +2744,8 @@ function createBrowserPool(options = {}) {
2600
2744
  lastReason = null;
2601
2745
  scheduleIdle();
2602
2746
  return exposed;
2603
- }).catch((error) => {
2604
- lastReason = error instanceof Error ? error.message : String(error);
2747
+ }).catch((cause) => {
2748
+ lastReason = cause instanceof Error ? cause.message : String(cause);
2605
2749
  return null;
2606
2750
  }).finally(() => {
2607
2751
  if (launching === attempt)
@@ -2627,7 +2771,7 @@ function createBrowserPool(options = {}) {
2627
2771
  };
2628
2772
  }
2629
2773
 
2630
- // ../server/dist/hydration.js
2774
+ // ../server/dist/capture/hydration.js
2631
2775
  function hydrationEvidence(messages) {
2632
2776
  for (const raw of messages) {
2633
2777
  const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
@@ -2653,7 +2797,7 @@ function hydrationEvidence(messages) {
2653
2797
  return null;
2654
2798
  }
2655
2799
 
2656
- // ../server/dist/capture.js
2800
+ // ../server/dist/capture/capture.js
2657
2801
  var FRAME_MAX_HEIGHT = 4e3;
2658
2802
  var MIN_WIDTH = 320;
2659
2803
  var MAX_WIDTH = 3840;
@@ -2707,9 +2851,9 @@ function bounded(work, milliseconds, fallback) {
2707
2851
  void work.then((value) => {
2708
2852
  clearTimeout(timer);
2709
2853
  resolve5(value);
2710
- }, (error) => {
2854
+ }, (cause) => {
2711
2855
  clearTimeout(timer);
2712
- reject(error);
2856
+ reject(cause);
2713
2857
  });
2714
2858
  });
2715
2859
  }
@@ -2724,9 +2868,9 @@ function abortable(work, signal) {
2724
2868
  void work.then((value) => {
2725
2869
  signal.removeEventListener("abort", abort);
2726
2870
  resolve5(value);
2727
- }, (error) => {
2871
+ }, (cause) => {
2728
2872
  signal.removeEventListener("abort", abort);
2729
- reject(error);
2873
+ reject(cause);
2730
2874
  });
2731
2875
  });
2732
2876
  }
@@ -2735,10 +2879,9 @@ function resultValue(response) {
2735
2879
  return result !== void 0 && "value" in result ? result.value : null;
2736
2880
  }
2737
2881
  function validBox(value) {
2738
- if (typeof value !== "object" || value === null)
2882
+ if (!isJsonRecord(value))
2739
2883
  return false;
2740
- const box = value;
2741
- 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));
2742
2885
  }
2743
2886
  function locatorExpression(focus) {
2744
2887
  return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
@@ -2762,7 +2905,7 @@ async function render(page, input) {
2762
2905
  if (documentStatus !== null || params?.type !== "Document")
2763
2906
  return;
2764
2907
  const status = params?.response?.status;
2765
- if (typeof status === "number")
2908
+ if (isNumber(status))
2766
2909
  documentStatus = status;
2767
2910
  }),
2768
2911
  page.on("Runtime.exceptionThrown", (params) => remember(params?.exceptionDetails?.exception?.description ?? params?.exceptionDetails?.text)),
@@ -2794,7 +2937,7 @@ async function render(page, input) {
2794
2937
  const stopLoad = page.on("Page.loadEventFired", () => loaded());
2795
2938
  unlisten.push(stopLoad);
2796
2939
  const navigation = await page.send("Page.navigate", { url: input.url });
2797
- if (typeof navigation.errorText === "string" && navigation.errorText !== "") {
2940
+ if (isString(navigation.errorText) && navigation.errorText !== "") {
2798
2941
  throw new Error(`The page did not load: ${navigation.errorText}`);
2799
2942
  }
2800
2943
  await bounded(load, input.timeoutMs ?? 15e3, void 0);
@@ -2910,7 +3053,7 @@ async function capturePage(browser, input) {
2910
3053
  return browser.withPage((page) => abortable(render(page, input), abortInput.signal));
2911
3054
  }
2912
3055
 
2913
- // ../server/dist/attachments.js
3056
+ // ../server/dist/requests/attachments.js
2914
3057
  import { copyFile, lstat, mkdir as mkdir3, readdir as readdir2, readFile as readFile5, realpath, rename, rm as rm2, stat as stat2, unlink, writeFile as writeFile3 } from "fs/promises";
2915
3058
  import { basename as basename2, extname, join as join5, resolve, sep } from "path";
2916
3059
  var CAPTURES_DIR = ".leglas/captures";
@@ -3031,13 +3174,15 @@ async function moveReference(cwd, id, destinationDir, index) {
3031
3174
  };
3032
3175
  }
3033
3176
  function focusOf(note) {
3034
- return {
3177
+ const focus = {
3035
3178
  selector: note.anchor.selector,
3036
3179
  text: note.anchor.text,
3037
3180
  tag: note.anchor.tag,
3038
- ...note.anchor.region === void 0 ? {} : { region: note.anchor.region },
3039
3181
  rect: note.anchor.rect
3040
3182
  };
3183
+ if (note.anchor.region !== void 0)
3184
+ focus.region = note.anchor.region;
3185
+ return focus;
3041
3186
  }
3042
3187
  async function attachRequest(cwd, requestId, input, deps) {
3043
3188
  const capture = deps.capture ?? capturePage;
@@ -3064,8 +3209,8 @@ async function attachRequest(cwd, requestId, input, deps) {
3064
3209
  finishDeadline();
3065
3210
  }, deadlineMs);
3066
3211
  timer.unref?.();
3067
- for (let index = 0; index < input.references.length; index += 1) {
3068
- 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);
3069
3214
  if (reference !== null)
3070
3215
  references.push(reference);
3071
3216
  }
@@ -3225,7 +3370,7 @@ async function pruneReferences(cwd) {
3225
3370
  }
3226
3371
  }
3227
3372
 
3228
- // ../server/dist/worktree.js
3373
+ // ../server/dist/branches/worktree.js
3229
3374
  import { execFile, spawn as spawn2 } from "child_process";
3230
3375
  import { rm as rm3 } from "fs/promises";
3231
3376
  import net2 from "net";
@@ -3246,7 +3391,7 @@ async function freePort() {
3246
3391
  probe2.once("error", reject);
3247
3392
  probe2.listen(0, "127.0.0.1", () => {
3248
3393
  const address = probe2.address();
3249
- const port = typeof address === "object" && address !== null ? address.port : 0;
3394
+ const port = address !== null && !isString(address) ? address.port : 0;
3250
3395
  probe2.close(() => resolve5(port));
3251
3396
  });
3252
3397
  });
@@ -3372,7 +3517,7 @@ async function startAppProcess(options) {
3372
3517
  throw new Error(`${options.label} did not start within ${Math.round(readyTimeoutMs / 1e3)}s. Check that its dev command serves the port it is given.`);
3373
3518
  }
3374
3519
 
3375
- // ../server/dist/branches.js
3520
+ // ../server/dist/branches/branches.js
3376
3521
  var BRANCH_IDLE_MS = 10 * 60 * 1e3;
3377
3522
  var BRANCH_SWEEP_MS = 3e4;
3378
3523
  function publicBranchState(state) {
@@ -3479,9 +3624,9 @@ function createBranchRegistry(options) {
3479
3624
  });
3480
3625
  throw error;
3481
3626
  }
3482
- }).catch((error) => transition(title, {
3627
+ }).catch((cause) => transition(title, {
3483
3628
  status: "failed",
3484
- reason: error instanceof Error ? error.message : String(error)
3629
+ reason: cause instanceof Error ? cause.message : String(cause)
3485
3630
  })).finally(() => {
3486
3631
  inflight.delete(title);
3487
3632
  });
@@ -3497,8 +3642,8 @@ function createBranchRegistry(options) {
3497
3642
  return stopPromise;
3498
3643
  closed = true;
3499
3644
  clearInterval(sweepTimer);
3500
- stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
3501
- await Promise.allSettled([...stopping.values()]);
3645
+ stopPromise = Promise.allSettled(inflight.values()).then(async () => {
3646
+ await Promise.allSettled(stopping.values());
3502
3647
  const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
3503
3648
  await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
3504
3649
  });
@@ -3507,7 +3652,11 @@ function createBranchRegistry(options) {
3507
3652
  };
3508
3653
  }
3509
3654
 
3510
- // ../server/dist/failure.js
3655
+ // ../server/dist/agents/silence.js
3656
+ var QUIET_NOTICE_MS = 3 * 6e4;
3657
+ var SILENCE_CEILING_MS = 30 * 6e4;
3658
+
3659
+ // ../server/dist/agents/failure.js
3511
3660
  var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
3512
3661
  var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
3513
3662
  var NOT_SIGNED_IN = /not logged in|not signed in|please (?:re-?)?(?:run|sign|log)\s*in|\/login\b|invalid api key|unauthorized|authentication_failed|\b401\b/i;
@@ -3560,6 +3709,8 @@ function message(code, input) {
3560
3709
  return `Codex refused this project: it is not a git repository and Codex has no trust on record for it.`;
3561
3710
  case "not-registered":
3562
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.`;
3563
3714
  case "agent-error":
3564
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.`;
3565
3716
  }
@@ -3567,14 +3718,14 @@ function message(code, input) {
3567
3718
  function classifyFailure(input) {
3568
3719
  const lines2 = input.lines ?? [];
3569
3720
  const error = input.error ?? null;
3570
- 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";
3571
3722
  return { code, message: message(code, input) };
3572
3723
  }
3573
- function sessionShaped(code) {
3724
+ function conversationFailure(code) {
3574
3725
  return code === "agent-error";
3575
3726
  }
3576
3727
 
3577
- // ../server/dist/annotations.js
3728
+ // ../server/dist/requests/annotations.js
3578
3729
  import { randomBytes as randomBytes2 } from "crypto";
3579
3730
  import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
3580
3731
  import { dirname as dirname4, join as join7 } from "path";
@@ -3586,43 +3737,38 @@ var TAG_CAP = 40;
3586
3737
  var CLASS_CAP = 8;
3587
3738
  var CLASS_LENGTH_CAP = 60;
3588
3739
  var COVERS_CAP = 8;
3589
- function isRecord2(value) {
3590
- return typeof value === "object" && value !== null && !Array.isArray(value);
3591
- }
3592
3740
  function text(value, cap) {
3593
- return typeof value === "string" ? value.trim().slice(0, cap) : "";
3741
+ return isString(value) ? value.trim().slice(0, cap) : "";
3594
3742
  }
3595
3743
  function fraction(value) {
3596
- if (typeof value !== "number" || !Number.isFinite(value))
3744
+ if (!isNumber(value) || !Number.isFinite(value))
3597
3745
  return 0.5;
3598
3746
  return Math.min(1, Math.max(0, value));
3599
3747
  }
3600
3748
  function size(value) {
3601
- return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
3749
+ return isNumber(value) && Number.isFinite(value) ? Math.round(value) : 0;
3602
3750
  }
3603
3751
  function anchorFrom(value) {
3604
- if (!isRecord2(value))
3752
+ if (!isJsonRecord(value))
3605
3753
  return null;
3606
3754
  const selector = text(value["selector"], SELECTOR_CAP);
3607
3755
  if (selector === "")
3608
3756
  return null;
3609
- const rect = isRecord2(value["rect"]) ? value["rect"] : {};
3610
- 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)) : [];
3611
- 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;
3612
3760
  const region = rawRegion === null ? null : {
3613
3761
  height: fraction(rawRegion["height"]),
3614
3762
  width: fraction(rawRegion["width"]),
3615
3763
  x: fraction(rawRegion["x"]),
3616
3764
  y: fraction(rawRegion["y"])
3617
3765
  };
3618
- 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) => ({
3619
3767
  tag: text(entry["tag"], TAG_CAP) || "element",
3620
3768
  text: text(entry["text"], TEXT_CAP)
3621
3769
  })) : [];
3622
- return {
3770
+ const anchor = {
3623
3771
  classes,
3624
- ...covers.length === 0 ? {} : { covers },
3625
- ...region === null ? {} : { region },
3626
3772
  rect: {
3627
3773
  height: size(rect["height"]),
3628
3774
  width: size(rect["width"]),
@@ -3631,22 +3777,27 @@ function anchorFrom(value) {
3631
3777
  },
3632
3778
  selector,
3633
3779
  spot: {
3634
- x: fraction(isRecord2(value["spot"]) ? value["spot"]["x"] : void 0),
3635
- 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)
3636
3782
  },
3637
3783
  tag: text(value["tag"], TAG_CAP) || "element",
3638
3784
  text: text(value["text"], TEXT_CAP),
3639
3785
  viewport: size(value["viewport"])
3640
3786
  };
3787
+ if (covers.length > 0)
3788
+ anchor.covers = covers;
3789
+ if (region !== null)
3790
+ anchor.region = region;
3791
+ return anchor;
3641
3792
  }
3642
3793
  async function readAnnotations(cwd) {
3643
3794
  try {
3644
3795
  const raw = await readFile6(join7(cwd, ANNOTATIONS_PATH), "utf8");
3645
- const parsed = JSON.parse(raw);
3646
- if (!Array.isArray(parsed.annotations))
3796
+ const parsed = parseJson(raw);
3797
+ if (!isJsonRecord(parsed) || !Array.isArray(parsed.annotations))
3647
3798
  return [];
3648
3799
  return parsed.annotations.flatMap((entry, index) => {
3649
- if (!isRecord2(entry))
3800
+ if (!isJsonRecord(entry))
3650
3801
  return [];
3651
3802
  const anchor = anchorFrom(entry["anchor"]);
3652
3803
  const title = text(entry["title"], TAG_CAP * 4);
@@ -3655,7 +3806,7 @@ async function readAnnotations(cwd) {
3655
3806
  return [
3656
3807
  {
3657
3808
  anchor,
3658
- id: typeof entry["id"] === "string" ? entry["id"] : String(index),
3809
+ id: isString(entry["id"]) ? entry["id"] : String(index),
3659
3810
  note: text(entry["note"], NOTE_CAP),
3660
3811
  title
3661
3812
  }
@@ -3738,7 +3889,7 @@ function describeAnnotations(annotations) {
3738
3889
  }).join("\n\n");
3739
3890
  }
3740
3891
 
3741
- // ../server/dist/requests.js
3892
+ // ../server/dist/requests/requests.js
3742
3893
  import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3743
3894
  import { randomBytes as randomBytes3 } from "crypto";
3744
3895
  import { dirname as dirname5, join as join8 } from "path";
@@ -3771,7 +3922,7 @@ function composeRequest(preview, intent, mode, notes = [], leglasCommand = "npx
3771
3922
  const target = preview.file ?? targetFor(preview.url);
3772
3923
  const cleaned = intent.trim();
3773
3924
  const asked = changeBlock(cleaned, notes);
3774
- 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;
3775
3926
  const prompt = mode === "variant" ? variantPrompt(preview, recorded, asked, target, leglasCommand, captured) : replacePrompt(preview, asked, target, leglasCommand, captured);
3776
3927
  return { prompt, target, mode };
3777
3928
  }
@@ -3894,52 +4045,62 @@ var TERMINAL = ["failed", "cancelled"];
3894
4045
  function isTerminal(status) {
3895
4046
  return TERMINAL.includes(status);
3896
4047
  }
3897
- var FAILURE_CODES = [
3898
- "cancelled",
3899
- "stopped",
3900
- "missing-agent",
3901
- "not-signed-in",
3902
- "provider-overloaded",
3903
- "provider-limit",
3904
- "needs-trust",
3905
- "not-registered",
3906
- "agent-error"
3907
- ];
3908
- function failureOf(value) {
3909
- 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))
3910
4065
  return null;
3911
- const entry = value;
3912
- if (typeof entry.message !== "string" || entry.message === "")
4066
+ if (!isString(entry.message) || entry.message === "")
3913
4067
  return null;
3914
- if (entry.code === void 0 || !FAILURE_CODES.includes(entry.code))
4068
+ if (!isFailureCode(entry.code))
3915
4069
  return null;
3916
4070
  return { code: entry.code, message: entry.message };
3917
4071
  }
3918
4072
  async function readRequests(cwd) {
3919
4073
  try {
3920
4074
  const raw = await readFile7(join8(cwd, REQUESTS_PATH), "utf8");
3921
- const parsed = JSON.parse(raw);
3922
- if (!Array.isArray(parsed.requests))
4075
+ const parsed = parseJson(raw);
4076
+ if (!isJsonRecord(parsed) || !Array.isArray(parsed.requests))
3923
4077
  return [];
3924
4078
  return parsed.requests.map((request, index) => {
3925
- const source = typeof request === "object" && request !== null ? request : {};
4079
+ const source = isJsonRecord(request) ? request : Array.isArray(request) ? Object.fromEntries(Object.entries(request)) : {};
3926
4080
  const { failure: rawFailure, attachments: rawAttachments, captureNote: rawCaptureNote, compare: rawCompare, references: rawReferences, ...entry } = source;
3927
4081
  const status = entry.status === "picked-up" || entry.status === "failed" || entry.status === "cancelled" ? entry.status : "queued";
3928
4082
  const failure = isTerminal(status) ? failureOf(rawFailure) : null;
3929
- 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);
3930
4084
  const ownFile = new RegExp(`^\\.leglas/captures/${id}/[A-Za-z0-9][A-Za-z0-9_.-]*$`);
3931
- 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;
3932
- 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 = {
3933
4087
  ...entry,
3934
4088
  id,
3935
4089
  status,
3936
- mode: entry.mode === "variant" ? "variant" : "replace",
3937
- ...failure === null ? {} : { failure },
3938
- ...attachments === null || attachments.length === 0 ? {} : { attachments },
3939
- ...typeof rawCaptureNote === "string" ? { captureNote: rawCaptureNote } : {},
3940
- ...typeof rawCompare === "string" ? { compare: rawCompare } : {},
3941
- ...Array.isArray(rawReferences) && rawReferences.every((id2) => typeof id2 === "string") ? { references: rawReferences } : {}
4090
+ mode: entry.mode === "variant" ? "variant" : "replace"
3942
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 };
3943
4104
  });
3944
4105
  } catch {
3945
4106
  return [];
@@ -3952,10 +4113,7 @@ async function writeQueue(cwd, requests) {
3952
4113
  `, "utf8");
3953
4114
  }
3954
4115
  async function appendRequest(cwd, request, id = newRequestId()) {
3955
- await writeQueue(cwd, [
3956
- ...await readRequests(cwd),
3957
- { ...request, id, status: "queued" }
3958
- ]);
4116
+ await writeQueue(cwd, [...await readRequests(cwd), { ...request, id, status: "queued" }]);
3959
4117
  }
3960
4118
  function newRequestId() {
3961
4119
  return randomBytes3(6).toString("base64url");
@@ -4007,12 +4165,12 @@ async function clearRequests(cwd) {
4007
4165
  return { cleared, pending: pending.length };
4008
4166
  }
4009
4167
 
4010
- // ../server/dist/runner.js
4168
+ // ../server/dist/agents/runner.js
4011
4169
  import { spawn as nodeSpawn3 } from "child_process";
4012
4170
  import { readFile as readFile9 } from "fs/promises";
4013
- import { join as join9, resolve as resolve2, sep as sep2 } from "path";
4171
+ import { join as join9, resolve as resolve2 } from "path";
4014
4172
 
4015
- // ../server/dist/claude-agent-session.js
4173
+ // ../server/dist/agents/claude-agent-session.js
4016
4174
  import { EventEmitter } from "events";
4017
4175
  import { readFile as readFile8, stat as stat3 } from "fs/promises";
4018
4176
  import { extname as extname2 } from "path";
@@ -4034,7 +4192,9 @@ function imageMediaType(path) {
4034
4192
  async function claudeContent(prompt, images) {
4035
4193
  if (images.length === 0)
4036
4194
  return prompt;
4037
- const blocks = [{ type: "text", text: prompt }];
4195
+ const blocks = [
4196
+ { type: "text", text: prompt }
4197
+ ];
4038
4198
  for (const path of images) {
4039
4199
  const mediaType = imageMediaType(path);
4040
4200
  if (mediaType === null)
@@ -4066,9 +4226,9 @@ function waitForAbort(work, signal) {
4066
4226
  void work.then((value) => {
4067
4227
  signal.removeEventListener("abort", onAbort);
4068
4228
  resolve5(value);
4069
- }, (error) => {
4229
+ }, (cause) => {
4070
4230
  signal.removeEventListener("abort", onAbort);
4071
- reject(error);
4231
+ reject(cause);
4072
4232
  });
4073
4233
  });
4074
4234
  }
@@ -4201,27 +4361,28 @@ var PersistentClaudeSession = class {
4201
4361
  this.warmedFor = sessionId;
4202
4362
  const controller = new AbortController();
4203
4363
  this.processAbort = controller;
4204
- const warming = this.startup({
4205
- options: {
4206
- abortController: controller,
4207
- cwd: this.cwd,
4208
- env: agentEnvironment(),
4209
- permissionMode: "acceptEdits",
4210
- settingSources: ["user", "project", "local"],
4211
- persistSession: true,
4212
- ...this.allowedCommands.length === 0 ? {} : { allowedTools: this.allowedCommands.map((command) => `Bash(${command} *)`) },
4213
- ...sessionId === null ? {} : { resume: sessionId }
4214
- },
4215
- initializeTimeoutMs: INITIALIZE_TIMEOUT_MS
4216
- }).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) => {
4217
4378
  if (this.closed || controller.signal.aborted || this.processAbort !== controller)
4218
4379
  warmQuery.close();
4219
4380
  else
4220
4381
  this.warmQuery = warmQuery;
4221
- }).catch((error) => {
4382
+ }).catch((cause) => {
4222
4383
  if (this.processAbort === controller)
4223
4384
  this.processAbort = null;
4224
- throw error;
4385
+ throw cause;
4225
4386
  }).finally(() => {
4226
4387
  if (this.warming === warming)
4227
4388
  this.warming = null;
@@ -4328,7 +4489,7 @@ var PersistentClaudeSession = class {
4328
4489
  let endedNormally = false;
4329
4490
  try {
4330
4491
  for await (const message2 of query) {
4331
- if (typeof message2.session_id === "string" && message2.session_id !== "") {
4492
+ if (isString(message2.session_id) && message2.session_id !== "") {
4332
4493
  this.loadedSessionId = message2.session_id;
4333
4494
  }
4334
4495
  const child = this.active;
@@ -4423,16 +4584,16 @@ function createClaudeAgentSession(cwd, allowedCommands = [], startup = defaultSt
4423
4584
  return new PersistentClaudeSession(cwd, allowedCommands, startup);
4424
4585
  }
4425
4586
 
4426
- // ../server/dist/codex-app-server.js
4587
+ // ../server/dist/agents/codex-app-server.js
4427
4588
  import { spawn as nodeSpawn2 } from "child_process";
4428
4589
  import { EventEmitter as EventEmitter2 } from "events";
4429
4590
  import { PassThrough as PassThrough2 } from "stream";
4430
4591
  var REQUEST_TIMEOUT_MS = 3e4;
4431
4592
  function record2(value) {
4432
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
4593
+ return isJsonRecord(value) ? value : null;
4433
4594
  }
4434
4595
  function string(value) {
4435
- return typeof value === "string" && value !== "" ? value : null;
4596
+ return isString(value) && value !== "" ? value : null;
4436
4597
  }
4437
4598
  var CodexTurnChild = class {
4438
4599
  interrupt;
@@ -4492,9 +4653,9 @@ var PersistentCodexAppServer = class {
4492
4653
  stderr = [];
4493
4654
  closed = false;
4494
4655
  resetting = null;
4495
- constructor(cwd, spawn4, requestTimeoutMs) {
4656
+ constructor(cwd, spawn5, requestTimeoutMs) {
4496
4657
  this.cwd = cwd;
4497
- this.spawn = spawn4;
4658
+ this.spawn = spawn5;
4498
4659
  this.requestTimeoutMs = requestTimeoutMs;
4499
4660
  }
4500
4661
  /**
@@ -4548,9 +4709,9 @@ var PersistentCodexAppServer = class {
4548
4709
  capabilities: null
4549
4710
  }).then(() => {
4550
4711
  this.notify("initialized", {});
4551
- }).catch(async (error) => {
4552
- await this.resetProcess(process2, error instanceof Error ? error : new Error(String(error)));
4553
- throw error;
4712
+ }).catch(async (cause) => {
4713
+ await this.resetProcess(process2, cause instanceof Error ? cause : new Error(String(cause)));
4714
+ throw cause;
4554
4715
  });
4555
4716
  return this.ready;
4556
4717
  }
@@ -4582,7 +4743,7 @@ var PersistentCodexAppServer = class {
4582
4743
  this.active = { child, threadId, turnId: null };
4583
4744
  child.line({ type: "thread.started", thread_id: threadId });
4584
4745
  turnSubmitted = true;
4585
- const response = record2(await this.requestRaw("turn/start", {
4746
+ const params = {
4586
4747
  threadId,
4587
4748
  input: [
4588
4749
  { type: "text", text: input.prompt },
@@ -4596,9 +4757,11 @@ var PersistentCodexAppServer = class {
4596
4757
  networkAccess: true,
4597
4758
  excludeTmpdirEnvVar: false,
4598
4759
  excludeSlashTmp: false
4599
- },
4600
- ...input.effort === null ? {} : { effort: input.effort }
4601
- }));
4760
+ }
4761
+ };
4762
+ if (input.effort !== null)
4763
+ params.effort = input.effort;
4764
+ const response = record2(await this.requestRaw("turn/start", params));
4602
4765
  const turn = record2(response?.turn);
4603
4766
  const turnId = string(turn?.id);
4604
4767
  if (turnId === null)
@@ -4693,13 +4856,13 @@ var PersistentCodexAppServer = class {
4693
4856
  return;
4694
4857
  let message2;
4695
4858
  try {
4696
- message2 = record2(JSON.parse(line));
4859
+ message2 = record2(parseJson(line));
4697
4860
  } catch {
4698
4861
  message2 = null;
4699
4862
  }
4700
4863
  if (message2 === null)
4701
4864
  return;
4702
- const id = typeof message2.id === "number" ? message2.id : null;
4865
+ const id = isNumber(message2.id) ? message2.id : null;
4703
4866
  const method = string(message2.method);
4704
4867
  if (id !== null && method === null) {
4705
4868
  const pending = this.pending.get(id);
@@ -4768,7 +4931,7 @@ var PersistentCodexAppServer = class {
4768
4931
  }
4769
4932
  if (item.type === "reasoning")
4770
4933
  return { type: "reasoning" };
4771
- return { type: typeof item.type === "string" ? item.type : "unknown" };
4934
+ return { type: isString(item.type) ? item.type : "unknown" };
4772
4935
  }
4773
4936
  answerServerRequest(id, method) {
4774
4937
  if (method === "item/commandExecution/requestApproval") {
@@ -4846,17 +5009,11 @@ var PersistentCodexAppServer = class {
4846
5009
  });
4847
5010
  });
4848
5011
  process2.kill("SIGTERM");
4849
- await Promise.race([
4850
- ended,
4851
- new Promise((resolve5) => setTimeout(resolve5, 1e3))
4852
- ]);
5012
+ await Promise.race([ended, new Promise((resolve5) => setTimeout(resolve5, 1e3))]);
4853
5013
  if (closed)
4854
5014
  return;
4855
5015
  process2.kill("SIGKILL");
4856
- await Promise.race([
4857
- ended,
4858
- new Promise((resolve5) => setTimeout(resolve5, 1e3))
4859
- ]);
5016
+ await Promise.race([ended, new Promise((resolve5) => setTimeout(resolve5, 1e3))]);
4860
5017
  }
4861
5018
  failProcess(process2, error) {
4862
5019
  if (this.process !== process2)
@@ -4897,15 +5054,49 @@ var defaultSpawn = (command, args, options) => nodeSpawn2(command, args, {
4897
5054
  ...options,
4898
5055
  env: agentEnvironment()
4899
5056
  });
4900
- function createCodexAppServer(cwd, spawn4 = defaultSpawn, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
4901
- return new PersistentCodexAppServer(cwd, spawn4, requestTimeoutMs);
5057
+ function createCodexAppServer(cwd, spawn5 = defaultSpawn, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
5058
+ return new PersistentCodexAppServer(cwd, spawn5, requestTimeoutMs);
4902
5059
  }
4903
5060
 
4904
- // ../server/dist/runner.js
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
+ }
5090
+ }
5091
+
5092
+ // ../server/dist/agents/runner.js
4905
5093
  var POLL_MS = 2e3;
4906
5094
  var OUTPUT_LINES = 20;
4907
5095
  var IDLE_RELEASE_MS = 5 * 6e4;
4908
5096
  var CANCEL_GRACE_MS = 5e3;
5097
+ function isStateUpdater(value) {
5098
+ return typeof value === "function";
5099
+ }
4909
5100
  var SESSION_TURNS_CAP = 8;
4910
5101
  function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images = []) {
4911
5102
  if (choice.agent === null)
@@ -4976,18 +5167,16 @@ function defaultSpawn2(command, args, options) {
4976
5167
  return nodeSpawn3(command, args, { ...options, env: agentEnvironment() });
4977
5168
  }
4978
5169
  function startRunner(options) {
4979
- const spawn4 = options.spawn ?? defaultSpawn2;
5170
+ const spawn5 = options.spawn ?? defaultSpawn2;
4980
5171
  const codexAppServer = options.codexAppServer === void 0 ? options.spawn === void 0 ? createCodexAppServer(options.cwd) : null : options.codexAppServer;
4981
- const claudeAgentSession = options.claudeAgentSession === void 0 ? options.spawn === void 0 ? createClaudeAgentSession(options.cwd, options.leglasCommand === void 0 ? [] : [
4982
- `${options.leglasCommand} show`,
4983
- registrationCommand(options.leglasCommand)
4984
- ]) : null : options.claudeAgentSession;
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;
4985
5173
  const setEvery = options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
4986
5174
  const clearEvery = options.clearInterval ?? ((handle2) => clearInterval(handle2));
4987
5175
  const failed = /* @__PURE__ */ new Set();
4988
5176
  const setLater = options.setTimeout ?? ((callback, milliseconds) => {
4989
5177
  setTimeout(callback, milliseconds).unref?.();
4990
5178
  });
5179
+ const now = options.now ?? (() => Date.now());
4991
5180
  let state = {
4992
5181
  running: false,
4993
5182
  requestId: null,
@@ -4995,10 +5184,11 @@ function startRunner(options) {
4995
5184
  activity: null,
4996
5185
  startedAt: null,
4997
5186
  stopping: false,
4998
- waiting: null
5187
+ waiting: null,
5188
+ quietSince: null
4999
5189
  };
5000
5190
  const setState = (next) => {
5001
- state = typeof next === "function" ? next(state) : next;
5191
+ state = isStateUpdater(next) ? next(state) : next;
5002
5192
  options.onChange?.();
5003
5193
  };
5004
5194
  let stopped = false;
@@ -5059,7 +5249,8 @@ function startRunner(options) {
5059
5249
  activity: null,
5060
5250
  startedAt: null,
5061
5251
  stopping: false,
5062
- waiting: null
5252
+ waiting: null,
5253
+ quietSince: null
5063
5254
  });
5064
5255
  };
5065
5256
  const rememberLine = (lines2, line) => {
@@ -5088,11 +5279,21 @@ function startRunner(options) {
5088
5279
  cancelled: false,
5089
5280
  controller,
5090
5281
  abandon: () => {
5091
- }
5282
+ },
5283
+ heardAt: now(),
5284
+ silenced: false,
5285
+ spawned: false
5092
5286
  };
5093
5287
  active = current;
5094
5288
  activeAgent = resolved.agent;
5095
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
+ };
5096
5297
  const startPersistent = async () => {
5097
5298
  if (persistent === null)
5098
5299
  throw new Error("No persistent transport.");
@@ -5117,27 +5318,31 @@ function startRunner(options) {
5117
5318
  return;
5118
5319
  }
5119
5320
  resolve5(child);
5120
- }, (error) => reject(error));
5321
+ }, (cause) => reject(cause));
5322
+ });
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()
5121
5330
  });
5331
+ current.spawned = true;
5332
+ return child;
5122
5333
  };
5123
5334
  try {
5124
5335
  let child;
5125
5336
  try {
5126
- child = persistent !== null ? await startPersistent() : spawn4(resolved.command, resolved.args, {
5127
- cwd: options.cwd,
5128
- shell: false,
5129
- stdio: ["ignore", "pipe", "pipe"]
5130
- });
5337
+ child = persistent !== null ? await startPersistent() : spawnAgent2();
5131
5338
  } catch (error) {
5339
+ if (current.silenced)
5340
+ return silent();
5132
5341
  if (current.cancelled || stopped || controller.signal.aborted)
5133
5342
  return cancelled();
5134
5343
  if (persistent !== null) {
5135
5344
  try {
5136
- child = spawn4(resolved.command, resolved.args, {
5137
- cwd: options.cwd,
5138
- shell: false,
5139
- stdio: ["ignore", "pipe", "pipe"]
5140
- });
5345
+ child = spawnAgent2();
5141
5346
  } catch (fallbackError) {
5142
5347
  return {
5143
5348
  ok: false,
@@ -5154,12 +5359,13 @@ function startRunner(options) {
5154
5359
  current.child = child;
5155
5360
  if (current.cancelled || stopped || controller.signal.aborted) {
5156
5361
  try {
5157
- child.kill("SIGTERM");
5362
+ signalChild(current, "SIGTERM");
5158
5363
  } catch {
5159
5364
  }
5160
- return cancelled();
5365
+ return current.silenced ? silent() : cancelled();
5161
5366
  }
5162
5367
  const stdoutFlush = lineReader(child.stdout, (line) => {
5368
+ heard();
5163
5369
  rememberLine(lines2, line);
5164
5370
  const sessionId = sessionFrom(resolved.agent, line);
5165
5371
  if (sessionId !== null)
@@ -5179,7 +5385,10 @@ function startRunner(options) {
5179
5385
  }
5180
5386
  }
5181
5387
  });
5182
- const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
5388
+ const stderrFlush = lineReader(child.stderr, (line) => {
5389
+ heard();
5390
+ rememberLine(lines2, line);
5391
+ });
5183
5392
  return await new Promise((resolve5) => {
5184
5393
  let settled = false;
5185
5394
  const settle = (outcome) => {
@@ -5190,11 +5399,19 @@ function startRunner(options) {
5190
5399
  stderrFlush();
5191
5400
  resolve5(outcome);
5192
5401
  };
5193
- current.abandon = () => settle(cancelled());
5194
- 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
+ });
5195
5410
  child.once("close", (code, signal) => {
5196
5411
  if (current.cancelled)
5197
5412
  return settle(cancelled());
5413
+ if (current.silenced)
5414
+ return settle(silent());
5198
5415
  if (signal !== null)
5199
5416
  return settle({ ok: false, error: `stopped by ${signal}` });
5200
5417
  settle({ ok: true, code: code ?? 0 });
@@ -5230,7 +5447,8 @@ function startRunner(options) {
5230
5447
  activity: null,
5231
5448
  startedAt: Date.now(),
5232
5449
  stopping: false,
5233
- waiting: null
5450
+ waiting: null,
5451
+ quietSince: null
5234
5452
  });
5235
5453
  const observed = {
5236
5454
  sessionId: null,
@@ -5252,7 +5470,7 @@ function startRunner(options) {
5252
5470
  // real output. Where it has not, "did not edit" means "was not seen
5253
5471
  // to edit", and rerunning on that is how a half-applied change gets
5254
5472
  // applied twice. Such a vendor keeps the failure card and its Retry.
5255
- 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
5256
5474
  // child settling and the retry starting finds no child to cancel, so
5257
5475
  // nothing says "cancelled". Stopped still means stopped.
5258
5476
  !stopped) {
@@ -5326,31 +5544,61 @@ function startRunner(options) {
5326
5544
  }
5327
5545
  });
5328
5546
  };
5329
- const timer = setEvery(() => schedule(), POLL_MS);
5330
- schedule();
5331
- const cancel = (id) => {
5332
- if (active === null || active.cancelled)
5333
- return false;
5334
- if (id !== void 0 && active.requestId !== id)
5335
- return false;
5336
- const current = active;
5337
- current.cancelled = true;
5338
- failed.add(current.requestId);
5339
- 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) => {
5340
5557
  current.controller.abort();
5341
5558
  try {
5342
- current.child?.kill("SIGTERM");
5559
+ signalChild(current, "SIGTERM");
5343
5560
  } catch {
5344
5561
  }
5345
5562
  setLater(() => {
5346
5563
  if (active !== current)
5347
5564
  return;
5348
5565
  try {
5349
- current.child?.kill("SIGKILL");
5566
+ signalChild(current, "SIGKILL");
5350
5567
  } catch {
5351
5568
  }
5352
5569
  current.abandon();
5353
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);
5354
5602
  return true;
5355
5603
  };
5356
5604
  const stop = () => {
@@ -5360,10 +5608,7 @@ function startRunner(options) {
5360
5608
  clearEvery(timer);
5361
5609
  cancel();
5362
5610
  stopPromise = Promise.resolve(ticking).catch(() => {
5363
- }).then(() => Promise.all([
5364
- codexAppServer?.close(),
5365
- claudeAgentSession?.close()
5366
- ])).then(() => {
5611
+ }).then(() => Promise.all([codexAppServer?.close(), claudeAgentSession?.close()])).then(() => {
5367
5612
  });
5368
5613
  return stopPromise;
5369
5614
  };
@@ -5384,7 +5629,7 @@ import { createHash } from "crypto";
5384
5629
  var LIVE_PATH = "/leglas/api/live";
5385
5630
  var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
5386
5631
  function encodeFrame(opcode, payload) {
5387
- const body = typeof payload === "string" ? Buffer.from(payload) : payload;
5632
+ const body = isString(payload) ? Buffer.from(payload) : payload;
5388
5633
  let header;
5389
5634
  if (body.length < 126) {
5390
5635
  header = Buffer.allocUnsafe(2);
@@ -5418,17 +5663,18 @@ function createCoalescer(emit, options = {}) {
5418
5663
  return;
5419
5664
  const waiting = pending.get(change);
5420
5665
  if (waiting !== void 0)
5421
- clearLater(waiting);
5422
- pending.set(change, setLater(() => {
5666
+ waiting();
5667
+ const handle = setLater(() => {
5423
5668
  pending.delete(change);
5424
5669
  if (!closed)
5425
5670
  emit(change);
5426
- }, windowMs));
5671
+ }, windowMs);
5672
+ pending.set(change, () => clearLater(handle));
5427
5673
  },
5428
5674
  close() {
5429
5675
  closed = true;
5430
- for (const handle of pending.values())
5431
- clearLater(handle);
5676
+ for (const cancel of pending.values())
5677
+ cancel();
5432
5678
  pending.clear();
5433
5679
  }
5434
5680
  };
@@ -5512,7 +5758,7 @@ function createLiveHub(options = {}) {
5512
5758
  if (listeners.size === 0)
5513
5759
  return;
5514
5760
  const frame = encodeFrame(1, JSON.stringify({ changed: change }));
5515
- for (const listener of [...listeners]) {
5761
+ for (const listener of Array.from(listeners)) {
5516
5762
  if (listener.socket.destroyed || !listener.socket.writable) {
5517
5763
  drop(listener);
5518
5764
  continue;
@@ -5531,7 +5777,7 @@ function createLiveHub(options = {}) {
5531
5777
  return false;
5532
5778
  const upgrade = req.headers.upgrade;
5533
5779
  const key = req.headers["sec-websocket-key"];
5534
- if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
5780
+ if (!isString(upgrade) || upgrade.toLowerCase() !== "websocket" || !isString(key) || key.trim() === "") {
5535
5781
  socket.destroy();
5536
5782
  return false;
5537
5783
  }
@@ -5566,7 +5812,7 @@ Sec-WebSocket-Accept: ${accept}\r
5566
5812
  return true;
5567
5813
  },
5568
5814
  close: async () => {
5569
- for (const listener of [...listeners]) {
5815
+ for (const listener of Array.from(listeners)) {
5570
5816
  write2(listener, 8, Buffer.alloc(0));
5571
5817
  drop(listener);
5572
5818
  listener.socket.destroy();
@@ -5581,12 +5827,12 @@ Sec-WebSocket-Accept: ${accept}\r
5581
5827
  };
5582
5828
  }
5583
5829
 
5584
- // ../server/dist/share.js
5830
+ // ../server/dist/share/share.js
5585
5831
  import { randomBytes as randomBytes4, randomUUID, timingSafeEqual } from "crypto";
5586
5832
  import http3 from "http";
5587
5833
  import { posix } from "path";
5588
5834
 
5589
- // ../server/dist/tunnel.js
5835
+ // ../server/dist/share/tunnel.js
5590
5836
  import { spawn as spawnChild } from "child_process";
5591
5837
  import { Resolver, lookup } from "dns/promises";
5592
5838
  import http2 from "http";
@@ -5620,15 +5866,17 @@ async function askLink(resolver, url, entryPath) {
5620
5866
  return false;
5621
5867
  const secure = target.protocol === "https:";
5622
5868
  return new Promise((resolve5) => {
5623
- const request = (secure ? https : http2).request({
5869
+ const requestOptions = {
5624
5870
  host: address,
5625
5871
  port: Number(target.port || (secure ? 443 : 80)),
5626
5872
  path: entryPath,
5627
5873
  method: "GET",
5628
5874
  headers: { host: target.host },
5629
- ...secure ? { servername: target.hostname } : {},
5630
5875
  timeout: PROBE_TIMEOUT_MS2
5631
- }, (response) => {
5876
+ };
5877
+ if (secure)
5878
+ requestOptions.servername = target.hostname;
5879
+ const request = (secure ? https : http2).request(requestOptions, (response) => {
5632
5880
  response.resume();
5633
5881
  const status = response.statusCode ?? 0;
5634
5882
  resolve5(status >= 200 && status < 400);
@@ -5653,12 +5901,14 @@ function withOutput(sentence, output) {
5653
5901
  return `${stem} (${output.slice(0, 160)}).`;
5654
5902
  }
5655
5903
  function startTunnel(options, deps = {}) {
5656
- const spawn4 = deps.spawn ?? spawnChild;
5904
+ const spawn5 = deps.spawn ?? spawnChild;
5657
5905
  const now = deps.now ?? Date.now;
5658
5906
  const urlDeadlineMs = deps.urlDeadlineMs ?? URL_DEADLINE_MS;
5659
5907
  const probeDeadlineMs = deps.probeDeadlineMs ?? PROBE_DEADLINE_MS;
5660
5908
  const resolver = deps.probe === void 0 ? new Resolver() : null;
5661
- const probe2 = deps.probe ?? ((url2) => askLink(resolver, url2, options.entryPath));
5909
+ const probe2 = deps.probe ?? ((url2) => {
5910
+ return askLink(resolver, url2, options.entryPath);
5911
+ });
5662
5912
  const timers = /* @__PURE__ */ new Set();
5663
5913
  const later = (callback, ms) => {
5664
5914
  const timer = setTimeout(() => {
@@ -5699,23 +5949,16 @@ function startTunnel(options, deps = {}) {
5699
5949
  return;
5700
5950
  terminal = true;
5701
5951
  clearTimers();
5702
- report({
5703
- status: "failed",
5704
- provider: options.provider,
5705
- reason,
5706
- ...url === null ? {} : { url }
5707
- });
5952
+ const failure = { status: "failed", provider: options.provider, reason };
5953
+ if (url !== null)
5954
+ failure.url = url;
5955
+ report(failure);
5708
5956
  };
5709
5957
  report(state);
5710
- const args = options.provider === "cloudflared" ? [
5711
- "tunnel",
5712
- "--url",
5713
- `http://127.0.0.1:${options.port}`,
5714
- "--no-autoupdate"
5715
- ] : ["http", String(options.port), "--log", "stdout", "--log-format", "json"];
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"];
5716
5959
  let child;
5717
5960
  try {
5718
- child = spawn4(options.provider, args, {
5961
+ child = spawn5(options.provider, args, {
5719
5962
  env: agentEnvironment(),
5720
5963
  shell: false,
5721
5964
  stdio: ["ignore", "pipe", "pipe"]
@@ -5785,8 +6028,8 @@ function startTunnel(options, deps = {}) {
5785
6028
  if (stream !== "stdout")
5786
6029
  return;
5787
6030
  try {
5788
- const event = JSON.parse(trimmed);
5789
- 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;
5790
6033
  if (candidate !== null)
5791
6034
  beginProbe(candidate);
5792
6035
  } catch {
@@ -5874,7 +6117,7 @@ function startTunnel(options, deps = {}) {
5874
6117
  };
5875
6118
  }
5876
6119
 
5877
- // ../server/dist/share.js
6120
+ // ../server/dist/share/share.js
5878
6121
  var MAX_REFUSED = 40;
5879
6122
  function routeAllowed(routes, url) {
5880
6123
  const [rawPath = "/"] = url.split("?", 2);
@@ -6007,19 +6250,16 @@ function isHiddenPath(path) {
6007
6250
  }
6008
6251
  var FILES_PREFIX_PATH = "/leglas/files/";
6009
6252
  var DETECT_TTL_MS = 1e4;
6010
- function isRecord3(value) {
6011
- return typeof value === "object" && value !== null && !Array.isArray(value);
6012
- }
6013
6253
  function stringArray(value) {
6014
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
6254
+ return Array.isArray(value) && value.every((entry) => isString(entry));
6015
6255
  }
6016
6256
  function stringRecord(value) {
6017
- return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
6257
+ return isJsonRecord(value) && Object.values(value).every((entry) => isString(entry));
6018
6258
  }
6019
6259
  function layoutFrom(value) {
6020
- if (!isRecord3(value))
6260
+ if (!isJsonRecord(value))
6021
6261
  return null;
6022
- 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))) {
6023
6263
  return null;
6024
6264
  }
6025
6265
  return {
@@ -6031,7 +6271,7 @@ function layoutFrom(value) {
6031
6271
  };
6032
6272
  }
6033
6273
  function manifestFrom(value, previews) {
6034
- if (!isRecord3(value)) {
6274
+ if (!isJsonRecord(value)) {
6035
6275
  return { ok: false, error: "Share details must be a JSON object." };
6036
6276
  }
6037
6277
  const scope2 = value.scope;
@@ -6051,9 +6291,7 @@ function manifestFrom(value, previews) {
6051
6291
  error: `Directions are not available to share: ${unknown.join(", ")}.`
6052
6292
  };
6053
6293
  }
6054
- const branches = [
6055
- ...new Set(titles.filter((title) => byTitle.get(title)?.branch !== void 0))
6056
- ];
6294
+ const branches = [...new Set(titles.filter((title) => byTitle.get(title)?.branch !== void 0))];
6057
6295
  if (branches.length > 0) {
6058
6296
  return {
6059
6297
  ok: false,
@@ -6198,7 +6436,7 @@ function bind(server) {
6198
6436
  const onListening = () => {
6199
6437
  server.removeListener("error", onError);
6200
6438
  const address = server.address();
6201
- resolve5(typeof address === "object" && address !== null ? address.port : 0);
6439
+ resolve5(address !== null && !isString(address) ? address.port : 0);
6202
6440
  };
6203
6441
  server.once("error", onError);
6204
6442
  server.once("listening", onListening);
@@ -6238,10 +6476,7 @@ function createShareManager(options) {
6238
6476
  }
6239
6477
  return detected;
6240
6478
  };
6241
- const status = () => {
6242
- const share = active;
6243
- if (share === null)
6244
- return null;
6479
+ const snapshot = (share) => {
6245
6480
  const tunnelUrl = "url" in share.tunnel ? share.tunnel.url : void 0;
6246
6481
  const origin = tunnelUrl === void 0 ? null : tunnelUrl.replace(/\/$/, "");
6247
6482
  return {
@@ -6250,7 +6485,7 @@ function createShareManager(options) {
6250
6485
  titles: [...share.titles],
6251
6486
  layout: cloneLayout(share.layout),
6252
6487
  sharePort: share.port,
6253
- 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) => {
6254
6489
  const entryPath = `${ENTRY_PREFIX}${grant.token}`;
6255
6490
  return {
6256
6491
  id: grant.id,
@@ -6269,6 +6504,7 @@ function createShareManager(options) {
6269
6504
  startedAt: share.startedAt
6270
6505
  };
6271
6506
  };
6507
+ const status = () => active === null ? null : snapshot(active);
6272
6508
  const endGrant = (share, grant, why) => {
6273
6509
  if (!share.grants.delete(grant.id))
6274
6510
  return;
@@ -6286,7 +6522,7 @@ function createShareManager(options) {
6286
6522
  held.req.destroy();
6287
6523
  }
6288
6524
  share.grantRequests.delete(grant.id);
6289
- for (const held of [...share.waiting.get(grant.id) ?? []]) {
6525
+ for (const held of Array.from(share.waiting.get(grant.id) ?? [])) {
6290
6526
  if (held.drop())
6291
6527
  refuse(held.req, held.res, why);
6292
6528
  }
@@ -6306,13 +6542,13 @@ function createShareManager(options) {
6306
6542
  const at = now();
6307
6543
  const mono = nowMono();
6308
6544
  let ended = false;
6309
- for (const grant of [...share.grants.values()]) {
6545
+ for (const grant of Array.from(share.grants.values())) {
6310
6546
  if (!expired(grant, at, mono))
6311
6547
  continue;
6312
6548
  endGrant(share, grant, "expiry");
6313
6549
  ended = true;
6314
6550
  }
6315
- 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);
6316
6552
  if (next !== null) {
6317
6553
  share.expiryTimer = setTimeout(sweepExpiry, Math.max(1, next - at));
6318
6554
  share.expiryTimer.unref?.();
@@ -6601,7 +6837,7 @@ function createShareManager(options) {
6601
6837
  if (!parsed.ok)
6602
6838
  return { ok: false, status: 400, error: parsed.error };
6603
6839
  const providers = await tunnels();
6604
- const requested = isRecord3(input) ? input.tunnel : void 0;
6840
+ const requested = isJsonRecord(input) ? input.tunnel : void 0;
6605
6841
  if (requested !== void 0 && requested !== "none" && requested !== "cloudflared" && requested !== "ngrok") {
6606
6842
  return { ok: false, status: 400, error: "That tunnel provider is not supported." };
6607
6843
  }
@@ -6675,7 +6911,7 @@ function createShareManager(options) {
6675
6911
  port,
6676
6912
  // Whichever link exists when the tunnel starts: the probe only
6677
6913
  // needs a path the listener answers, and a share always has one.
6678
- entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
6914
+ entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
6679
6915
  onState: (next) => {
6680
6916
  if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
6681
6917
  return;
@@ -6686,7 +6922,7 @@ function createShareManager(options) {
6686
6922
  });
6687
6923
  share.launch.unref?.();
6688
6924
  }
6689
- return { ok: true, share: status() };
6925
+ return { ok: true, share: snapshot(share) };
6690
6926
  } finally {
6691
6927
  creating = false;
6692
6928
  }
@@ -6695,7 +6931,7 @@ function createShareManager(options) {
6695
6931
  const share = active;
6696
6932
  if (share === null)
6697
6933
  return { ok: false, status: 404, error: "Nothing is being shared." };
6698
- const name = isRecord3(input) && typeof input.name === "string" ? input.name.trim() : "";
6934
+ const name = isJsonRecord(input) && isString(input.name) ? input.name.trim() : "";
6699
6935
  if (name.length > 60) {
6700
6936
  return { ok: false, status: 400, error: "That name is too long for a link." };
6701
6937
  }
@@ -6710,26 +6946,26 @@ function createShareManager(options) {
6710
6946
  mintGrant(share, name);
6711
6947
  sweepExpiry();
6712
6948
  options.live.nudge("share");
6713
- return { ok: true, share: status() };
6949
+ return { ok: true, share: snapshot(share) };
6714
6950
  };
6715
6951
  const revokeGrant = (input) => {
6716
6952
  const share = active;
6717
6953
  if (share === null)
6718
6954
  return { ok: false, status: 404, error: "Nothing is being shared." };
6719
- const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
6955
+ const id = isJsonRecord(input) && isString(input.id) ? input.id : "";
6720
6956
  const grant = share.grants.get(id);
6721
6957
  if (grant === void 0)
6722
6958
  return { ok: false, status: 404, error: "No such link." };
6723
6959
  endGrant(share, grant, "revoke");
6724
6960
  sweepExpiry();
6725
6961
  options.live.nudge("share");
6726
- return { ok: true, share: status() };
6962
+ return { ok: true, share: snapshot(share) };
6727
6963
  };
6728
6964
  const extendGrant = (input) => {
6729
6965
  const share = active;
6730
6966
  if (share === null)
6731
6967
  return { ok: false, status: 404, error: "Nothing is being shared." };
6732
- const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
6968
+ const id = isJsonRecord(input) && isString(input.id) ? input.id : "";
6733
6969
  sweepExpiry();
6734
6970
  const grant = share.grants.get(id);
6735
6971
  if (grant === void 0) {
@@ -6740,13 +6976,13 @@ function createShareManager(options) {
6740
6976
  grant.expiresAtMono = nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n;
6741
6977
  sweepExpiry();
6742
6978
  options.live.nudge("share");
6743
- return { ok: true, share: status() };
6979
+ return { ok: true, share: snapshot(share) };
6744
6980
  };
6745
6981
  const rotate = async () => {
6746
6982
  const share = active;
6747
6983
  if (share === null)
6748
6984
  return { ok: false, status: 404, error: "Nothing is being shared." };
6749
- for (const grant of [...share.grants.values()])
6985
+ for (const grant of Array.from(share.grants.values()))
6750
6986
  endGrant(share, grant, "revoke");
6751
6987
  const provider = "provider" in share.tunnel ? share.tunnel.provider : null;
6752
6988
  await share.runningTunnel?.stop().catch(() => {
@@ -6763,7 +6999,7 @@ function createShareManager(options) {
6763
6999
  share.runningTunnel = runTunnel({
6764
7000
  provider,
6765
7001
  port: share.port,
6766
- entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
7002
+ entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
6767
7003
  onState: (next) => {
6768
7004
  if (active !== share || share.tunnelGeneration !== generation)
6769
7005
  return;
@@ -6775,17 +7011,17 @@ function createShareManager(options) {
6775
7011
  });
6776
7012
  }
6777
7013
  options.live.nudge("share");
6778
- return { ok: true, share: status() };
7014
+ return { ok: true, share: snapshot(share) };
6779
7015
  };
6780
7016
  const allowRoute = (input) => {
6781
7017
  const share = active;
6782
7018
  if (share === null)
6783
7019
  return { ok: false, status: 404, error: "Nothing is being shared." };
6784
- const given = isRecord3(input) && typeof input.path === "string" ? input.path.trim() : "";
7020
+ const given = isJsonRecord(input) && isString(input.path) ? input.path.trim() : "";
6785
7021
  if (!given.startsWith("/")) {
6786
7022
  return { ok: false, status: 400, error: "A route is a path beginning with a slash." };
6787
7023
  }
6788
- const subtree = isRecord3(input) && input.subtree === true;
7024
+ const subtree = isJsonRecord(input) && input.subtree === true;
6789
7025
  if (subtree && given.replace(/\/+$/, "") === "") {
6790
7026
  return { ok: false, status: 400, error: "The root is a page, not a folder." };
6791
7027
  }
@@ -6797,7 +7033,7 @@ function createShareManager(options) {
6797
7033
  share.routes.push(asked);
6798
7034
  share.refused = share.refused.filter((path) => !routeAllowed([asked], path));
6799
7035
  options.live.nudge("share");
6800
- return { ok: true, share: status() };
7036
+ return { ok: true, share: snapshot(share) };
6801
7037
  };
6802
7038
  const update = async (input) => {
6803
7039
  const share = active;
@@ -6815,7 +7051,7 @@ function createShareManager(options) {
6815
7051
  share.layout = parsed.manifest.layout;
6816
7052
  options.live.nudge("share");
6817
7053
  options.live.nudge("config");
6818
- return { ok: true, share: status() };
7054
+ return { ok: true, share: snapshot(share) };
6819
7055
  };
6820
7056
  const viewerConfig = async (grantId) => {
6821
7057
  const share = active;
@@ -6898,15 +7134,15 @@ function createShareManager(options) {
6898
7134
  };
6899
7135
  }
6900
7136
 
6901
- // ../server/dist/renames.js
7137
+ // ../server/dist/config/renames.js
6902
7138
  import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
6903
7139
  import { dirname as dirname6, join as join10 } from "path";
6904
7140
  var RENAMES_PATH = ".leglas/renames.json";
6905
7141
  async function readRenames(cwd) {
6906
7142
  try {
6907
7143
  const raw = await readFile10(join10(cwd, RENAMES_PATH), "utf8");
6908
- const parsed = JSON.parse(raw);
6909
- if (parsed.renames === null || typeof parsed.renames !== "object")
7144
+ const parsed = parseJson(raw);
7145
+ if (!isJsonRecord(parsed) || !isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames))
6910
7146
  return {};
6911
7147
  return Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
6912
7148
  } catch {
@@ -6938,6 +7174,115 @@ import http4 from "http";
6938
7174
  import net3 from "net";
6939
7175
  import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
6940
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
+
6941
7286
  // ../server/dist/server-info.js
6942
7287
  import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
6943
7288
  import { dirname as dirname7, join as join11 } from "path";
@@ -6954,16 +7299,16 @@ async function writeServerInfo(cwd, info) {
6954
7299
  const temporary = `${path}.${process.pid}.tmp`;
6955
7300
  await writeFile7(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
6956
7301
  `, "utf8");
6957
- await rename2(temporary, path).catch(async (error) => {
7302
+ await rename2(temporary, path).catch(async (cause) => {
6958
7303
  await rm4(temporary, { force: true }).catch(() => {
6959
7304
  });
6960
- throw error;
7305
+ throw cause;
6961
7306
  });
6962
7307
  }
6963
7308
  async function readServerInfo(cwd) {
6964
7309
  try {
6965
- const value = JSON.parse(await readFile11(join11(cwd, SERVER_INFO_PATH), "utf8"));
6966
- 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))
6967
7312
  return null;
6968
7313
  return { port: value.port, url: value.url, pid: value.pid };
6969
7314
  } catch {
@@ -7002,6 +7347,9 @@ var CONTENT_TYPES = {
7002
7347
  ".woff": "font/woff",
7003
7348
  ".woff2": "font/woff2"
7004
7349
  };
7350
+ function isContentExtension(extension) {
7351
+ return Object.hasOwn(CONTENT_TYPES, extension);
7352
+ }
7005
7353
  var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
7006
7354
  function sendJson2(res, status, body) {
7007
7355
  const payload = JSON.stringify(body);
@@ -7048,8 +7396,11 @@ function referenceName(value) {
7048
7396
  }).join("").trim().slice(0, 80);
7049
7397
  return safe || "image";
7050
7398
  }
7399
+ function isAddressInUse(cause) {
7400
+ return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EADDRINUSE";
7401
+ }
7051
7402
  function isKnownAgent(value) {
7052
- return typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
7403
+ return isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
7053
7404
  }
7054
7405
  function isAllowedMutationHost(hostname) {
7055
7406
  const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
@@ -7070,7 +7421,7 @@ function isLoopbackAddress(address) {
7070
7421
  function isTrustedMutation(req) {
7071
7422
  if (!isLoopbackAddress(req.socket.remoteAddress))
7072
7423
  return false;
7073
- if (typeof req.headers.host !== "string")
7424
+ if (!isString(req.headers.host))
7074
7425
  return false;
7075
7426
  let host;
7076
7427
  try {
@@ -7094,17 +7445,21 @@ function isEnded(request, failedIds) {
7094
7445
  return isTerminal(request.status) || failedIds.includes(request.id);
7095
7446
  }
7096
7447
  function jsonBody(body) {
7097
- let parsed;
7098
7448
  try {
7099
- parsed = JSON.parse(body || "{}");
7449
+ const parsed = parseJson(body || "{}");
7450
+ return isJsonRecord(parsed) ? parsed : null;
7100
7451
  } catch {
7101
7452
  return null;
7102
7453
  }
7103
- 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;
7104
7459
  }
7105
7460
  function hasJsonBody(req) {
7106
7461
  const contentType = req.headers["content-type"];
7107
- return typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
7462
+ return isString(contentType) && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
7108
7463
  }
7109
7464
  function probe(target, timeoutMs = 1e3) {
7110
7465
  return new Promise((resolve5) => {
@@ -7133,8 +7488,9 @@ function serveFrom(res, dir, relativePath) {
7133
7488
  return false;
7134
7489
  if (!existsSync3(candidate) || !statSync(candidate).isFile())
7135
7490
  return false;
7491
+ const extension = extname3(candidate);
7136
7492
  res.writeHead(200, {
7137
- "content-type": CONTENT_TYPES[extname3(candidate)] ?? "application/octet-stream",
7493
+ "content-type": isContentExtension(extension) ? CONTENT_TYPES[extension] : "application/octet-stream",
7138
7494
  "cache-control": "no-store"
7139
7495
  });
7140
7496
  createReadStream(candidate).pipe(res);
@@ -7391,7 +7747,7 @@ function listen(server, port) {
7391
7747
  const onListening = () => {
7392
7748
  server.removeListener("error", onError);
7393
7749
  const address = server.address();
7394
- resolve5(typeof address === "object" && address !== null ? address.port : port);
7750
+ resolve5(address !== null && !isString(address) ? address.port : port);
7395
7751
  };
7396
7752
  server.once("error", onError);
7397
7753
  server.once("listening", onListening);
@@ -7405,7 +7761,7 @@ async function bind2(server, requested) {
7405
7761
  try {
7406
7762
  return await listen(server, requested + attempt);
7407
7763
  } catch (error) {
7408
- if (error.code !== "EADDRINUSE")
7764
+ if (!isAddressInUse(error))
7409
7765
  throw error;
7410
7766
  }
7411
7767
  }
@@ -7417,14 +7773,16 @@ async function startServer(options) {
7417
7773
  let shares = null;
7418
7774
  let liveHealth = null;
7419
7775
  const live = options.live ?? createLiveHub();
7420
- const branches = createBranchRegistry({
7776
+ const branchOptions = {
7421
7777
  cwd,
7422
7778
  previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
7423
7779
  installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
7424
7780
  devCommand: config?.devCommand,
7425
- onChange: () => live.nudge("config"),
7426
- ...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
7427
- });
7781
+ onChange: () => live.nudge("config")
7782
+ };
7783
+ if (options.startWorktree !== void 0)
7784
+ branchOptions.startWorktree = options.startWorktree;
7785
+ const branches = createBranchRegistry(branchOptions);
7428
7786
  const previewForConfig = (preview) => {
7429
7787
  if (preview.branch === void 0)
7430
7788
  return preview;
@@ -7538,9 +7896,9 @@ async function startServer(options) {
7538
7896
  return void updates.check({ force: true }).then((status) => sendJson2(res, 200, status));
7539
7897
  }
7540
7898
  if (path.endsWith("/install")) {
7541
- 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, {
7542
7900
  ok: false,
7543
- error: error instanceof Error ? error.message : String(error)
7901
+ error: cause instanceof Error ? cause.message : String(cause)
7544
7902
  }));
7545
7903
  }
7546
7904
  if (!hasJsonBody(req))
@@ -7551,12 +7909,12 @@ async function startServer(options) {
7551
7909
  const parsed = jsonBody(body);
7552
7910
  if (parsed === null)
7553
7911
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7554
- if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
7912
+ if (!isString(parsed.version) || parsed.version.trim() === "") {
7555
7913
  return sendJson2(res, 400, { ok: false, error: "Body needs a version." });
7556
7914
  }
7557
- 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, {
7558
7916
  ok: false,
7559
- error: error instanceof Error ? error.message : String(error)
7917
+ error: cause instanceof Error ? cause.message : String(cause)
7560
7918
  }));
7561
7919
  });
7562
7920
  }
@@ -7574,10 +7932,10 @@ async function startServer(options) {
7574
7932
  if (parsed === null) {
7575
7933
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7576
7934
  }
7577
- const result = await shares?.create(parsed).catch((error) => ({
7935
+ const result = await shares?.create(parsed).catch((cause) => ({
7578
7936
  ok: false,
7579
7937
  status: 500,
7580
- 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)}).`
7581
7939
  }));
7582
7940
  if (result === void 0) {
7583
7941
  return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
@@ -7659,6 +8017,24 @@ async function startServer(options) {
7659
8017
  warnings: configWarnings
7660
8018
  }));
7661
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
+ }
7662
8038
  if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
7663
8039
  let body = "";
7664
8040
  req.on("data", (chunk) => body += chunk);
@@ -7667,7 +8043,7 @@ async function startServer(options) {
7667
8043
  if (parsed === null) {
7668
8044
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7669
8045
  }
7670
- if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
8046
+ if (!isString(parsed.title) || parsed.title.trim() === "") {
7671
8047
  return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
7672
8048
  }
7673
8049
  const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
@@ -7703,7 +8079,7 @@ async function startServer(options) {
7703
8079
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7704
8080
  }
7705
8081
  const titles = parsed.titles;
7706
- 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() !== "")) {
7707
8083
  return sendJson2(res, 400, {
7708
8084
  ok: false,
7709
8085
  error: "Body needs a non-empty array of direction titles."
@@ -7735,7 +8111,7 @@ async function startServer(options) {
7735
8111
  }
7736
8112
  if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
7737
8113
  const declaredLength = req.headers["content-length"];
7738
- if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
8114
+ if (isString(declaredLength) && Number(declaredLength) > REFERENCE_MAX_BYTES) {
7739
8115
  return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
7740
8116
  }
7741
8117
  const chunks = [];
@@ -7810,7 +8186,7 @@ async function startServer(options) {
7810
8186
  });
7811
8187
  }
7812
8188
  const mode = parsed.mode === "replace" ? "replace" : "variant";
7813
- 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)))) {
7814
8190
  return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
7815
8191
  }
7816
8192
  const references = parsed.references ?? [];
@@ -7824,23 +8200,26 @@ async function startServer(options) {
7824
8200
  });
7825
8201
  }
7826
8202
  }
7827
- 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;
7828
8204
  const previews = await livePreviews();
7829
8205
  const preview = previews.find((entry) => entry.title === parsed.title);
7830
8206
  if (!preview) {
7831
8207
  return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7832
8208
  }
7833
8209
  const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
7834
- 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) {
7835
8215
  return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
7836
8216
  }
7837
- const intent = (parsed.intent ?? "").trim();
7838
8217
  const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
7839
8218
  const sameNotes = (entry) => {
7840
8219
  const before = [...entry.notes ?? []].sort().join(",");
7841
8220
  return before === notes.map((note) => note.id).sort().join(",");
7842
8221
  };
7843
- 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;
7844
8223
  const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
7845
8224
  if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
7846
8225
  // one forks the direction and the other rewrites it. Only a
@@ -7853,7 +8232,7 @@ async function startServer(options) {
7853
8232
  });
7854
8233
  }
7855
8234
  const address = server.address();
7856
- 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;
7857
8236
  const id = newRequestId();
7858
8237
  const captured = await attachRequest(cwd, id, {
7859
8238
  origin: `http://127.0.0.1:${requestPort}`,
@@ -7865,20 +8244,26 @@ async function startServer(options) {
7865
8244
  }, { pool: browserPool });
7866
8245
  const composed = composeRequest(preview, intent, mode, notes, leglasCommand, captured);
7867
8246
  try {
7868
- await appendRequest(cwd, {
8247
+ const queued = {
7869
8248
  title: preview.title,
7870
8249
  url: preview.url,
7871
8250
  intent,
7872
8251
  // The ids travel with the request so a change made in place can
7873
8252
  // forget the notes it answered. A fork leaves them where they are:
7874
8253
  // the direction they point at was not touched.
7875
- ...notes.length === 0 ? {} : { notes: notes.map((entry) => entry.id) },
7876
- ...captured.attachments.length === 0 ? {} : { attachments: captured.attachments },
7877
- ...captured.skipped === null ? {} : { captureNote: captured.skipped },
7878
- ...compare === null ? {} : { compare: compare.title },
7879
- ...references.length === 0 ? {} : { references },
7880
8254
  ...composed
7881
- }, 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);
7882
8267
  runner?.nudge();
7883
8268
  return sendJson2(res, 200, {
7884
8269
  ok: true,
@@ -7906,17 +8291,17 @@ async function startServer(options) {
7906
8291
  if (parsed === null) {
7907
8292
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7908
8293
  }
7909
- if (typeof parsed.title !== "string" || parsed.title === "") {
8294
+ if (!isString(parsed.title) || parsed.title === "") {
7910
8295
  return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
7911
8296
  }
7912
- if (parsed.note !== void 0 && typeof parsed.note !== "string") {
8297
+ if (parsed.note !== void 0 && !isString(parsed.note)) {
7913
8298
  return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
7914
8299
  }
7915
8300
  const preview = (await livePreviews()).find((entry) => entry.title === parsed.title);
7916
8301
  if (preview === void 0) {
7917
8302
  return sendJson2(res, 404, { ok: false, error: "No such direction." });
7918
8303
  }
7919
- 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;
7920
8305
  const browser = await browserPool.acquire();
7921
8306
  if (browser === null) {
7922
8307
  return sendJson2(res, 503, {
@@ -7924,9 +8309,9 @@ async function startServer(options) {
7924
8309
  error: browserPool.reason() ?? NO_BROWSER
7925
8310
  });
7926
8311
  }
7927
- 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) : [];
7928
8313
  const address = server.address();
7929
- 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;
7930
8315
  const controller = new AbortController();
7931
8316
  const timeoutMarker = /* @__PURE__ */ Symbol("capture timeout");
7932
8317
  let timedOut;
@@ -7939,21 +8324,26 @@ async function startServer(options) {
7939
8324
  }, CAPTURE_DEADLINE_MS);
7940
8325
  timer.unref?.();
7941
8326
  try {
7942
- const work = capturePage(browser, {
8327
+ const captureInput = {
7943
8328
  url: previewUrl(`http://127.0.0.1:${capturePort}`, preview),
7944
8329
  width,
7945
- ...annotations.length === 0 ? {} : {
7946
- 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 = {
7947
8336
  selector: entry.anchor.selector,
7948
8337
  text: entry.anchor.text,
7949
8338
  tag: entry.anchor.tag,
7950
- ...entry.anchor.region === void 0 ? {} : { region: entry.anchor.region },
7951
8339
  rect: entry.anchor.rect
7952
- }))
7953
- },
7954
- timeoutMs: CAPTURE_LOAD_MS,
7955
- signal: controller.signal
7956
- });
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);
7957
8347
  const result = await Promise.race([work, timeout]);
7958
8348
  if (result === timeoutMarker) {
7959
8349
  return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
@@ -7961,7 +8351,7 @@ async function startServer(options) {
7961
8351
  clearTimeout(timer);
7962
8352
  const crop = annotations.length > 0 ? result.crops[0] : null;
7963
8353
  const shot = crop?.shot ?? result.frame;
7964
- 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, "-")}` : "";
7965
8355
  const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
7966
8356
  const relativeFile = `${CAPTURES_DIR}/show/${name}`;
7967
8357
  await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
@@ -8023,12 +8413,18 @@ async function startServer(options) {
8023
8413
  if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
8024
8414
  return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
8025
8415
  }
8026
- if (parsed.run !== void 0 && typeof parsed.run !== "string") {
8027
- return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
8416
+ if (parsed.run !== void 0 && !isString(parsed.run)) {
8417
+ return sendJson2(res, 400, {
8418
+ ok: false,
8419
+ error: "The custom run command must be a string."
8420
+ });
8028
8421
  }
8029
8422
  const effort = parsed.effort === null || isAgentEffort(parsed.effort) ? parsed.effort : void 0;
8030
8423
  if (parsed.effort !== void 0 && effort === void 0) {
8031
- return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
8424
+ return sendJson2(res, 400, {
8425
+ ok: false,
8426
+ error: "Effort must be a supported level or null."
8427
+ });
8032
8428
  }
8033
8429
  if (parsed.agent === "custom") {
8034
8430
  if (effort !== void 0) {
@@ -8037,7 +8433,7 @@ async function startServer(options) {
8037
8433
  error: "Custom agents manage effort in their own command."
8038
8434
  });
8039
8435
  }
8040
- if (typeof parsed.run !== "string") {
8436
+ if (!isString(parsed.run)) {
8041
8437
  return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
8042
8438
  }
8043
8439
  const template = parseTemplate(parsed.run);
@@ -8045,17 +8441,18 @@ async function startServer(options) {
8045
8441
  return sendJson2(res, 400, { ok: false, error: template.error });
8046
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." }));
8047
8443
  }
8048
- 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)) {
8049
8445
  return sendJson2(res, 400, {
8050
8446
  ok: false,
8051
8447
  error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
8052
8448
  });
8053
8449
  }
8054
- return void saveAgentChoice(cwd, {
8055
- agent: parsed.agent,
8056
- ...effort === void 0 ? {} : { effort }
8057
- }).then(() => {
8058
- 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);
8059
8456
  sendJson2(res, 200, { ok: true });
8060
8457
  }, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
8061
8458
  });
@@ -8068,7 +8465,7 @@ async function startServer(options) {
8068
8465
  if (parsed === null) {
8069
8466
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8070
8467
  }
8071
- if (typeof parsed.watching !== "boolean") {
8468
+ if (!isBoolean(parsed.watching)) {
8072
8469
  return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
8073
8470
  }
8074
8471
  lastSeen = parsed.watching ? Date.now() : null;
@@ -8084,6 +8481,7 @@ async function startServer(options) {
8084
8481
  startedAt: null,
8085
8482
  stopping: false,
8086
8483
  waiting: null,
8484
+ quietSince: null,
8087
8485
  failedIds: []
8088
8486
  };
8089
8487
  return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
@@ -8115,7 +8513,10 @@ async function startServer(options) {
8115
8513
  // says so rather than going on describing a live run.
8116
8514
  stopping: snapshot.running && snapshot.stopping,
8117
8515
  // Why a run that looks stalled is stalled, while it is stalled.
8118
- 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
8119
8520
  }
8120
8521
  }));
8121
8522
  }
@@ -8130,7 +8531,7 @@ async function startServer(options) {
8130
8531
  if (parsed === null) {
8131
8532
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8132
8533
  }
8133
- if (parsed.id !== void 0 && typeof parsed.id !== "string") {
8534
+ if (parsed.id !== void 0 && !isString(parsed.id)) {
8134
8535
  return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
8135
8536
  }
8136
8537
  return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
@@ -8147,7 +8548,7 @@ async function startServer(options) {
8147
8548
  if (parsed === null) {
8148
8549
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8149
8550
  }
8150
- if (typeof parsed.id !== "string") {
8551
+ if (!isString(parsed.id)) {
8151
8552
  return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
8152
8553
  }
8153
8554
  const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
@@ -8155,7 +8556,10 @@ async function startServer(options) {
8155
8556
  return sendJson2(res, 404, { ok: false, error: "No such request." });
8156
8557
  }
8157
8558
  if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
8158
- return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
8559
+ return sendJson2(res, 400, {
8560
+ ok: false,
8561
+ error: "Only an ended request can be run again."
8562
+ });
8159
8563
  }
8160
8564
  try {
8161
8565
  const retryId = newRequestId();
@@ -8163,7 +8567,7 @@ async function startServer(options) {
8163
8567
  if (!await removeRequest(cwd, request.id)) {
8164
8568
  return sendJson2(res, 404, { ok: false, error: "No such request." });
8165
8569
  }
8166
- await appendRequest(cwd, {
8570
+ const retry = {
8167
8571
  title: request.title,
8168
8572
  url: request.url,
8169
8573
  intent: request.intent,
@@ -8171,16 +8575,23 @@ async function startServer(options) {
8171
8575
  // The prompt names the captures by path, and the embedded pipes
8172
8576
  // are not its only readers: watch, a custom command and
8173
8577
  // `requests --json` all hand the text over as it stands.
8174
- 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)
8175
8579
  // The stored prompt already carries the mode's instructions;
8176
8580
  // its notes and visual context travel with the retry too.
8177
- ...request.mode === void 0 ? {} : { mode: request.mode },
8178
- ...request.notes === void 0 ? {} : { notes: request.notes },
8179
- ...attachments.length === 0 ? {} : { attachments },
8180
- ...request.captureNote === void 0 ? {} : { captureNote: request.captureNote },
8181
- ...request.compare === void 0 ? {} : { compare: request.compare },
8182
- ...request.references === void 0 ? {} : { references: request.references }
8183
- }, 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);
8184
8595
  runner?.nudge();
8185
8596
  return sendJson2(res, 200, { ok: true });
8186
8597
  } catch {
@@ -8202,7 +8613,7 @@ async function startServer(options) {
8202
8613
  if (parsed === null) {
8203
8614
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8204
8615
  }
8205
- if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
8616
+ if (!isString(parsed.title) || parsed.title.trim() === "") {
8206
8617
  return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
8207
8618
  }
8208
8619
  const anchor = anchorFrom(parsed.anchor);
@@ -8212,7 +8623,7 @@ async function startServer(options) {
8212
8623
  try {
8213
8624
  const annotation = await addAnnotation(cwd, {
8214
8625
  anchor,
8215
- note: typeof parsed.note === "string" ? parsed.note.trim() : "",
8626
+ note: isString(parsed.note) ? parsed.note.trim() : "",
8216
8627
  title: parsed.title
8217
8628
  });
8218
8629
  return sendJson2(res, 200, { ok: true, annotation });
@@ -8232,10 +8643,10 @@ async function startServer(options) {
8232
8643
  if (parsed === null) {
8233
8644
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8234
8645
  }
8235
- if (typeof parsed.id !== "string" || parsed.id === "") {
8646
+ if (!isString(parsed.id) || parsed.id === "") {
8236
8647
  return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
8237
8648
  }
8238
- if (typeof parsed.note !== "string") {
8649
+ if (!isString(parsed.note)) {
8239
8650
  return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
8240
8651
  }
8241
8652
  try {
@@ -8260,7 +8671,7 @@ async function startServer(options) {
8260
8671
  if (parsed === null) {
8261
8672
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8262
8673
  }
8263
- 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)) : [];
8264
8675
  if (ids.length === 0) {
8265
8676
  return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
8266
8677
  }
@@ -8282,12 +8693,15 @@ async function startServer(options) {
8282
8693
  if (parsed === null) {
8283
8694
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8284
8695
  }
8285
- if (typeof parsed.id !== "string") {
8696
+ if (!isString(parsed.id)) {
8286
8697
  return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
8287
8698
  }
8288
8699
  const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
8289
8700
  if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
8290
- return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
8701
+ return sendJson2(res, 400, {
8702
+ ok: false,
8703
+ error: "Only an ended request can be dismissed."
8704
+ });
8291
8705
  }
8292
8706
  try {
8293
8707
  if (!await removeRequest(cwd, parsed.id)) {
@@ -8307,7 +8721,7 @@ async function startServer(options) {
8307
8721
  if (parsed === null) {
8308
8722
  return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
8309
8723
  }
8310
- if (parsed.renames === null || typeof parsed.renames !== "object") {
8724
+ if (!isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames)) {
8311
8725
  return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
8312
8726
  }
8313
8727
  const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
@@ -8331,7 +8745,10 @@ async function startServer(options) {
8331
8745
  const serveMount = () => {
8332
8746
  if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
8333
8747
  return;
8334
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
8748
+ res.writeHead(404, {
8749
+ "content-type": "text/plain; charset=utf-8",
8750
+ "cache-control": "no-store"
8751
+ });
8335
8752
  res.end("Leglas: no such preview file.");
8336
8753
  };
8337
8754
  if (!context.remote)
@@ -8352,11 +8769,17 @@ async function startServer(options) {
8352
8769
  if (shellDir !== null && serveShellFile(res, shellDir, path))
8353
8770
  return;
8354
8771
  if (shellDir !== null) {
8355
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
8772
+ res.writeHead(404, {
8773
+ "content-type": "text/plain; charset=utf-8",
8774
+ "cache-control": "no-store"
8775
+ });
8356
8776
  res.end("Leglas: no such path.");
8357
8777
  return;
8358
8778
  }
8359
- res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
8779
+ res.writeHead(200, {
8780
+ "content-type": "text/html; charset=utf-8",
8781
+ "cache-control": "no-store"
8782
+ });
8360
8783
  res.end(PLACEHOLDER);
8361
8784
  return;
8362
8785
  }
@@ -8387,7 +8810,7 @@ async function startServer(options) {
8387
8810
  server.on("upgrade", (req, socket, head) => {
8388
8811
  handleUpgrade(req, socket, head, { remote: false });
8389
8812
  });
8390
- shares = createShareManager({
8813
+ const shareOptions = {
8391
8814
  live,
8392
8815
  previews: livePreviewDefinitions,
8393
8816
  previewsForConfig,
@@ -8401,10 +8824,13 @@ async function startServer(options) {
8401
8824
  publicOrigin: context.publicOrigin,
8402
8825
  grantId: context.grantId
8403
8826
  }),
8404
- upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
8405
- ...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
8406
- ...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
8407
- });
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);
8408
8834
  port = await bind2(server, options.port ?? DEFAULT_PORT);
8409
8835
  options.updates?.setPort(port);
8410
8836
  const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
@@ -8417,14 +8843,17 @@ async function startServer(options) {
8417
8843
  pid: process.pid
8418
8844
  }).catch(() => {
8419
8845
  });
8420
- runner = startRunner({
8846
+ const runnerOptions = {
8421
8847
  cwd,
8422
8848
  externallyAttached,
8423
8849
  onChange: () => live.nudge("requests"),
8424
- leglasCommand,
8425
- ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
8426
- ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
8427
- });
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);
8428
8857
  options.updates?.onBusy(() => runner?.snapshot().running ?? false);
8429
8858
  options.updates?.onChange(() => live.nudge("update"));
8430
8859
  let closePromise = null;
@@ -8590,10 +9019,7 @@ function resolveOrExplain(input, titles, renames) {
8590
9019
  function renameExport(source, to) {
8591
9020
  const match = /export function ([A-Za-z0-9_]+)\s*\(/.exec(source);
8592
9021
  if (!match || match[1] === void 0) return source;
8593
- return source.replace(
8594
- new RegExp(`\\b${match[1]}\\b`, "g"),
8595
- to
8596
- );
9022
+ return source.replace(new RegExp(`\\b${match[1]}\\b`, "g"), to);
8597
9023
  }
8598
9024
  async function writeLogEntry(options) {
8599
9025
  const entry = composeEntry({
@@ -8736,7 +9162,14 @@ async function runNew(options, deps) {
8736
9162
  }
8737
9163
  if (options.print) {
8738
9164
  if (options.json) {
8739
- deps.log(JSON.stringify({ ok: true, files: plan.writes, instructions: plan.instructions, previews: plan.previews }));
9165
+ deps.log(
9166
+ JSON.stringify({
9167
+ ok: true,
9168
+ files: plan.writes,
9169
+ instructions: plan.instructions,
9170
+ previews: plan.previews
9171
+ })
9172
+ );
8740
9173
  return { exitCode: 0, written: [] };
8741
9174
  }
8742
9175
  for (const write2 of plan.writes) {
@@ -8748,7 +9181,9 @@ async function runNew(options, deps) {
8748
9181
  }
8749
9182
  const existing = plan.writes.filter((write2) => existsSync6(join16(options.cwd, write2.path)));
8750
9183
  if (existing.length > 0) {
8751
- return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
9184
+ return fail(
9185
+ `${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
9186
+ );
8752
9187
  }
8753
9188
  const written = [];
8754
9189
  for (const write2 of plan.writes) {
@@ -8763,7 +9198,12 @@ async function runNew(options, deps) {
8763
9198
  }
8764
9199
  if (options.json) {
8765
9200
  deps.log(
8766
- JSON.stringify({ ok: true, written, instructions: plan.instructions, previews: plan.previews })
9201
+ JSON.stringify({
9202
+ ok: true,
9203
+ written,
9204
+ instructions: plan.instructions,
9205
+ previews: plan.previews
9206
+ })
8767
9207
  );
8768
9208
  return { exitCode: 0, written };
8769
9209
  }
@@ -8773,7 +9213,9 @@ async function runNew(options, deps) {
8773
9213
  deps.log("Then register them so they appear in the interface:");
8774
9214
  deps.log("");
8775
9215
  for (const preview of plan.previews) {
8776
- deps.log(` npx leglas add --title ${JSON.stringify(preview.title)} --url ${JSON.stringify(preview.url)}`);
9216
+ deps.log(
9217
+ ` npx leglas add --title ${JSON.stringify(preview.title)} --url ${JSON.stringify(preview.url)}`
9218
+ );
8777
9219
  }
8778
9220
  return { exitCode: 0, written };
8779
9221
  }
@@ -8830,15 +9272,16 @@ async function runAdd(options, deps) {
8830
9272
  await ensureIgnored(options.cwd);
8831
9273
  const needsDevCommand = options.preview.branch !== void 0 && loaded.config?.devCommand === void 0;
8832
9274
  if (options.json) {
8833
- envelope(deps, true, {
8834
- added: options.preview.title,
8835
- ...options.preview.url === void 0 ? {} : { url: options.preview.url },
8836
- local: true,
8837
- ...options.preview.branch === void 0 ? {} : { branch: options.preview.branch },
8838
- ...options.preview.file === void 0 ? {} : { file: options.preview.file },
8839
- 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.",
8840
- ...needsDevCommand ? { warning: "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config." } : {}
8841
- });
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);
8842
9285
  } else {
8843
9286
  deps.log(
8844
9287
  ` added ${options.preview.title} ${options.preview.url ?? options.preview.file ?? ""}`
@@ -8977,10 +9420,29 @@ function planShow({ title, previews, requests }) {
8977
9420
  };
8978
9421
  }
8979
9422
 
8980
- // src/run-show.ts
9423
+ // src/running.ts
8981
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
8982
9445
  var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
8983
- var CAPTURE_WAIT_MS = 3e4;
8984
9446
  async function sameDirectory(left, right) {
8985
9447
  const [a, b] = await Promise.all([
8986
9448
  realpath2(left).catch(() => left),
@@ -8988,6 +9450,42 @@ async function sameDirectory(left, right) {
8988
9450
  ]);
8989
9451
  return a === b;
8990
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;
8991
9489
  async function runShow(options, deps) {
8992
9490
  const loaded = await loadConfig(options.cwd);
8993
9491
  const local = await readLocalPreviews(options.cwd);
@@ -9020,28 +9518,15 @@ async function runShow(options, deps) {
9020
9518
  requests: plan.requests
9021
9519
  };
9022
9520
  if (options.screenshot) {
9023
- const server = options.port === null ? await readServerInfo(options.cwd) : null;
9024
- const port = options.port ?? server?.port ?? DEFAULT_PORT;
9025
9521
  const fail = (error) => {
9026
9522
  if (options.json) deps.log(JSON.stringify({ ok: false, error }));
9027
9523
  else deps.error(error);
9028
9524
  return { exitCode: 1 };
9029
9525
  };
9030
9526
  const request = deps.fetch ?? fetch;
9031
- try {
9032
- const health = await request(`http://127.0.0.1:${port}/leglas/api/health`, {
9033
- signal: AbortSignal.timeout(2e3)
9034
- });
9035
- if (health.status !== 200) return fail(NOT_RUNNING);
9036
- const answered = await health.json().catch(() => ({}));
9037
- if (typeof answered.cwd === "string" && !await sameDirectory(answered.cwd, options.cwd)) {
9038
- return fail(
9039
- `The Leglas on port ${port} serves another project. Start one here with npx leglas, or name the right one with --port.`
9040
- );
9041
- }
9042
- } catch {
9043
- return fail(NOT_RUNNING);
9044
- }
9527
+ const found = await findLeglas(options.cwd, options.port, request);
9528
+ if (!found.ok) return fail(found.error);
9529
+ const port = found.port;
9045
9530
  let response;
9046
9531
  try {
9047
9532
  response = await request(`http://127.0.0.1:${port}/leglas/api/capture`, {
@@ -9058,17 +9543,17 @@ async function runShow(options, deps) {
9058
9543
  const captured = await response.json().catch(() => ({}));
9059
9544
  if (!response.ok) {
9060
9545
  return fail(
9061
- typeof captured.error === "string" ? captured.error : "The direction could not be captured."
9546
+ hasCaptureError(captured) ? captured.error : "The direction could not be captured."
9062
9547
  );
9063
9548
  }
9064
- if (typeof captured.file !== "string" || typeof captured.width !== "number" || typeof captured.height !== "number" || typeof captured.viewport !== "number") return fail("The direction could not be captured.");
9549
+ if (!hasCaptureSize(captured)) return fail("The direction could not be captured.");
9065
9550
  envelope2.screenshot = {
9066
9551
  file: captured.file,
9067
9552
  width: captured.width,
9068
9553
  height: captured.height,
9069
9554
  viewport: captured.viewport,
9070
- errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
9071
- 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) ? {
9072
9557
  framework: captured.hydration.framework,
9073
9558
  message: captured.hydration.message
9074
9559
  } : null,
@@ -9121,8 +9606,290 @@ async function runShow(options, deps) {
9121
9606
  return { exitCode: 0 };
9122
9607
  }
9123
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
+
9124
9891
  // src/run-watch.ts
9125
- import { spawn as spawn3 } from "child_process";
9892
+ import { spawn as spawn4 } from "child_process";
9126
9893
  import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile14 } from "fs/promises";
9127
9894
  import { dirname as dirname12, join as join18 } from "path";
9128
9895
  var POLL_MS2 = 2e3;
@@ -9132,9 +9899,7 @@ async function saveTemplate(cwd, run4) {
9132
9899
  let config = {};
9133
9900
  try {
9134
9901
  const parsed = JSON.parse(await readFile16(path, "utf8"));
9135
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
9136
- config = parsed;
9137
- }
9902
+ if (isJsonObject(parsed)) config = parsed;
9138
9903
  } catch {
9139
9904
  }
9140
9905
  config.run = run4;
@@ -9150,7 +9915,7 @@ function spawnAgent(command, args, cwd) {
9150
9915
  settled = true;
9151
9916
  resolve5(outcome);
9152
9917
  };
9153
- const child = spawn3(command, args, { cwd, stdio: "inherit" });
9918
+ const child = spawn4(command, args, { cwd, stdio: "inherit" });
9154
9919
  child.on("error", (error) => settle({ ok: false, error: error.message }));
9155
9920
  child.on(
9156
9921
  "close",
@@ -9334,10 +10099,14 @@ function localDevServerPort(origin) {
9334
10099
  }
9335
10100
  }
9336
10101
  function parseListeningPids(output) {
9337
- return [...new Set(output.split("\n").flatMap((line) => {
9338
- if (!/^p\d+$/.test(line)) return [];
9339
- return [Number(line.slice(1))];
9340
- }))];
10102
+ return [
10103
+ ...new Set(
10104
+ output.split("\n").flatMap((line) => {
10105
+ if (!/^p\d+$/.test(line)) return [];
10106
+ return [Number(line.slice(1))];
10107
+ })
10108
+ )
10109
+ ];
9341
10110
  }
9342
10111
  function parseOwnerCwds(output) {
9343
10112
  const owners = [];
@@ -9360,11 +10129,10 @@ async function inspectLocalDevServer(origin) {
9360
10129
  const port = localDevServerPort(origin);
9361
10130
  if (port === null || process.platform === "win32") return [];
9362
10131
  try {
9363
- const listeners = await run2(
9364
- "lsof",
9365
- ["-nP", "-a", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"],
9366
- { maxBuffer: 64 * 1024, timeout: INSPECTION_TIMEOUT_MS }
9367
- );
10132
+ const listeners = await run2("lsof", ["-nP", "-a", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
10133
+ maxBuffer: 64 * 1024,
10134
+ timeout: INSPECTION_TIMEOUT_MS
10135
+ });
9368
10136
  const pids = parseListeningPids(String(listeners.stdout));
9369
10137
  if (pids.length === 0) return [];
9370
10138
  const directories = await run2(
@@ -9422,8 +10190,11 @@ function skipStartupCheck(env) {
9422
10190
  return env.CI !== void 0 && env.CI !== "" && env.CI !== "false" || !off(env.LEGLAS_NO_UPDATE_CHECK);
9423
10191
  }
9424
10192
  async function run3(options, deps) {
9425
- const loaded = await loadConfig(options.cwd);
9426
- 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);
9427
10198
  let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
9428
10199
  const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
9429
10200
  const previewErrors = [];
@@ -9471,11 +10242,11 @@ async function run3(options, deps) {
9471
10242
  }
9472
10243
  const config = merged === null ? null : { ...merged, previews };
9473
10244
  const configWarnings = [];
9474
- const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname13(loaded.path)).catch(
9475
- () => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path))
9476
- );
9477
- const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
9478
- const serverPromise = startServer({
10245
+ const projectRoot = await realpath4(
10246
+ loaded.path === null ? options.cwd : dirname13(loaded.path)
10247
+ ).catch(() => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path)));
10248
+ const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
10249
+ const serverOptions = {
9479
10250
  config,
9480
10251
  configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
9481
10252
  configWarnings,
@@ -9485,10 +10256,11 @@ async function run3(options, deps) {
9485
10256
  // directory does. Either way saved layout survives a port change.
9486
10257
  project: loaded.path ?? options.cwd,
9487
10258
  cwd: options.cwd,
9488
- leglasCommand: embeddedLeglasCommand(),
9489
- ...deps.updates === void 0 ? {} : { updates: deps.updates },
9490
- ...options.port === void 0 ? {} : { port: options.port }
9491
- });
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);
9492
10264
  const [server, warning] = await Promise.all([serverPromise, ownerWarning]);
9493
10265
  if (warning !== null) configWarnings.push(warning);
9494
10266
  const url = `${server.url}${LEGLAS_PREFIX}`;
@@ -9577,6 +10349,7 @@ export {
9577
10349
  commandFor,
9578
10350
  detectFramework,
9579
10351
  findBrowser,
10352
+ installShutdown,
9580
10353
  isOwnCapture,
9581
10354
  nextRequest,
9582
10355
  parseArgs,
@@ -9597,6 +10370,7 @@ export {
9597
10370
  runList,
9598
10371
  runNew,
9599
10372
  runRequests,
10373
+ runShare,
9600
10374
  runShow,
9601
10375
  runWatch,
9602
10376
  surfaceSlug