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