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