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