leglas 1.3.0 → 1.4.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 +14 -0
- package/dist/baseline.d.ts +2 -8
- package/dist/bin.js +1656 -475
- package/dist/dev-server-owner.d.ts +3 -4
- package/dist/explore.d.ts +5 -13
- package/dist/help.d.ts +2 -0
- package/dist/ignore.d.ts +3 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1665 -502
- package/dist/keep.d.ts +3 -10
- package/dist/new.d.ts +3 -4
- package/dist/resolve-title.d.ts +4 -8
- package/dist/rules.d.ts +41 -0
- package/dist/run-classify.d.ts +3 -5
- package/dist/run-explore-build.d.ts +3 -6
- package/dist/run-explore.d.ts +3 -6
- package/dist/run-init.d.ts +3 -4
- package/dist/run-link.d.ts +18 -0
- package/dist/run-log.d.ts +3 -7
- package/dist/run-new.d.ts +4 -7
- package/dist/run-previews.d.ts +4 -4
- package/dist/run-remove.d.ts +17 -0
- package/dist/run-share.d.ts +10 -10
- package/dist/run-show.d.ts +3 -5
- package/dist/run-watch.d.ts +3 -9
- package/dist/run.d.ts +3 -6
- package/dist/running.d.ts +24 -7
- package/dist/shell/assets/index-CXz1BSV1.js +14 -0
- package/dist/shell/index.html +1 -1
- package/dist/show.d.ts +7 -16
- package/dist/shutdown.d.ts +7 -22
- package/dist/test-helpers.d.ts +2 -0
- package/package.json +1 -1
- package/dist/shell/assets/index-CcHf_UOb.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,17 +1,82 @@
|
|
|
1
|
+
// src/rules.ts
|
|
2
|
+
var MIN_PORT = 1;
|
|
3
|
+
var MAX_PORT = 65535;
|
|
4
|
+
var MIN_SHOW_WIDTH = 320;
|
|
5
|
+
var MAX_SHOW_WIDTH = 3840;
|
|
6
|
+
var DEFAULT_EXPLORE_COUNT = 3;
|
|
7
|
+
var DEFAULT_SHARE_REACH = "open";
|
|
8
|
+
function portRefusal(flag, port) {
|
|
9
|
+
return port < MIN_PORT || port > MAX_PORT ? `${flag} must be between ${MIN_PORT} and ${MAX_PORT}, received ${port}.` : null;
|
|
10
|
+
}
|
|
11
|
+
function valueRefusal(flag) {
|
|
12
|
+
return `${flag} needs a value.`;
|
|
13
|
+
}
|
|
14
|
+
function addRefusal(preview) {
|
|
15
|
+
const texts = [
|
|
16
|
+
["--title", preview.title],
|
|
17
|
+
["--url", preview.url],
|
|
18
|
+
["--note", preview.note],
|
|
19
|
+
["--branch", preview.branch],
|
|
20
|
+
["--file", preview.file],
|
|
21
|
+
["--based-on", preview.basedOn],
|
|
22
|
+
["--asked-for", preview.askedFor],
|
|
23
|
+
...(preview.tags ?? []).map((tag) => ["--tag", tag])
|
|
24
|
+
];
|
|
25
|
+
const empty = texts.find(([, value]) => value === "");
|
|
26
|
+
if (empty !== void 0) return valueRefusal(empty[0]);
|
|
27
|
+
if (preview.url === void 0 && preview.file === void 0) {
|
|
28
|
+
return "leglas add needs --url (for example --url '/?v-hero=aurora') or --file for a page Leglas serves itself.";
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function shareRefusal(options) {
|
|
33
|
+
const acts = [
|
|
34
|
+
options.stop && "--stop ends the share",
|
|
35
|
+
options.rotate === true && "--rotate replaces every link",
|
|
36
|
+
options.revoke !== void 0 && options.revoke !== null && "--revoke ends one link"
|
|
37
|
+
].filter((act2) => act2 !== false);
|
|
38
|
+
if (acts.length > 1) {
|
|
39
|
+
return "leglas share takes one of --stop, --rotate or --revoke at a time.";
|
|
40
|
+
}
|
|
41
|
+
const [act] = acts;
|
|
42
|
+
if (act !== void 0 && (options.titles.length > 0 || options.reach !== void 0 || options.tunnel !== null)) {
|
|
43
|
+
return `leglas share ${act}; it takes no directions, reach or tunnel.`;
|
|
44
|
+
}
|
|
45
|
+
if (options.titles.length > 2) {
|
|
46
|
+
return "leglas share takes one direction to share alone, or two to compare. Name none to share the rail.";
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function linkRefusal(titles) {
|
|
51
|
+
return titles.length > 2 ? "leglas link takes one direction, or two to put side by side. Name none for the rail." : null;
|
|
52
|
+
}
|
|
53
|
+
function widthRefusal(width) {
|
|
54
|
+
return width < MIN_SHOW_WIDTH || width > MAX_SHOW_WIDTH ? `--width must be between ${MIN_SHOW_WIDTH} and ${MAX_SHOW_WIDTH}, received ${width}.` : null;
|
|
55
|
+
}
|
|
56
|
+
function showRefusal(options) {
|
|
57
|
+
if (options.width !== null && !options.screenshot) {
|
|
58
|
+
return "leglas show --width needs --screenshot.";
|
|
59
|
+
}
|
|
60
|
+
if (options.port !== null && !options.screenshot) {
|
|
61
|
+
return "leglas show --port needs --screenshot.";
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function fromRefusal(from) {
|
|
66
|
+
return from === "" ? "--from needs a path, for example --from src/Hero.tsx" : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
1
69
|
// src/args.ts
|
|
2
70
|
var SHARE_TUNNELS = ["cloudflared", "ngrok", "none"];
|
|
3
71
|
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--port", "--user-port", "--config"]);
|
|
4
72
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--no-open", "--json"]);
|
|
5
|
-
var MIN_SHOW_WIDTH = 320;
|
|
6
|
-
var MAX_SHOW_WIDTH = 3840;
|
|
7
73
|
function parsePort(flag, raw) {
|
|
8
74
|
if (!/^\d+$/.test(raw)) {
|
|
9
75
|
return { error: `${flag} needs a number, received ${JSON.stringify(raw)}.` };
|
|
10
76
|
}
|
|
11
77
|
const port = Number(raw);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
78
|
+
const refusal = portRefusal(flag, port);
|
|
79
|
+
if (refusal !== null) return { error: refusal };
|
|
15
80
|
return { port };
|
|
16
81
|
}
|
|
17
82
|
function parseNew(rest) {
|
|
@@ -23,9 +88,8 @@ function parseNew(rest) {
|
|
|
23
88
|
const argument = rest[index];
|
|
24
89
|
if (argument === "--from" || argument.startsWith("--from=")) {
|
|
25
90
|
from = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
91
|
+
const refusal = fromRefusal(from ?? "");
|
|
92
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
29
93
|
continue;
|
|
30
94
|
}
|
|
31
95
|
if (argument === "--print") {
|
|
@@ -93,7 +157,7 @@ function parseAdd(rest) {
|
|
|
93
157
|
return { kind: "error", message: `leglas add does not take ${flag}.` };
|
|
94
158
|
}
|
|
95
159
|
if (value === void 0 || value === "") {
|
|
96
|
-
return { kind: "error", message:
|
|
160
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
97
161
|
}
|
|
98
162
|
if (flag === "--title") title = value;
|
|
99
163
|
else if (flag === "--url") url = value;
|
|
@@ -110,26 +174,19 @@ function parseAdd(rest) {
|
|
|
110
174
|
message: "leglas add needs --title, which is how the preview is identified."
|
|
111
175
|
};
|
|
112
176
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
title,
|
|
123
|
-
url,
|
|
124
|
-
note,
|
|
125
|
-
tags: tags.length > 0 ? tags : void 0,
|
|
126
|
-
branch,
|
|
127
|
-
file,
|
|
128
|
-
basedOn,
|
|
129
|
-
askedFor
|
|
130
|
-
},
|
|
131
|
-
json
|
|
177
|
+
const preview = {
|
|
178
|
+
title,
|
|
179
|
+
url,
|
|
180
|
+
note,
|
|
181
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
182
|
+
branch,
|
|
183
|
+
file,
|
|
184
|
+
basedOn,
|
|
185
|
+
askedFor
|
|
132
186
|
};
|
|
187
|
+
const refusal = addRefusal(preview);
|
|
188
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
189
|
+
return { kind: "add", preview, json };
|
|
133
190
|
}
|
|
134
191
|
function parseClassify(rest) {
|
|
135
192
|
const changes = [];
|
|
@@ -178,7 +235,7 @@ function parseWatch(rest) {
|
|
|
178
235
|
if (value === void 0 || value === "") {
|
|
179
236
|
return {
|
|
180
237
|
kind: "error",
|
|
181
|
-
message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' :
|
|
238
|
+
message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' : valueRefusal(flag)
|
|
182
239
|
};
|
|
183
240
|
}
|
|
184
241
|
if (flag === "--run") {
|
|
@@ -193,10 +250,12 @@ function parseWatch(rest) {
|
|
|
193
250
|
}
|
|
194
251
|
function parseShare(rest) {
|
|
195
252
|
const titles = [];
|
|
196
|
-
let reach =
|
|
253
|
+
let reach = DEFAULT_SHARE_REACH;
|
|
197
254
|
let reachGiven = false;
|
|
198
255
|
let tunnel = null;
|
|
199
256
|
let stop = false;
|
|
257
|
+
let rotate = false;
|
|
258
|
+
let revoke = null;
|
|
200
259
|
let port = null;
|
|
201
260
|
let json = false;
|
|
202
261
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -209,12 +268,22 @@ function parseShare(rest) {
|
|
|
209
268
|
stop = true;
|
|
210
269
|
continue;
|
|
211
270
|
}
|
|
271
|
+
if (argument === "--rotate") {
|
|
272
|
+
rotate = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
212
275
|
const equals = argument.indexOf("=");
|
|
213
276
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
277
|
+
if (flag === "--revoke") {
|
|
278
|
+
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
279
|
+
if (raw === void 0 || raw === "") return { kind: "error", message: valueRefusal(flag) };
|
|
280
|
+
revoke = raw;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
214
283
|
if (flag === "--reach" || flag === "--tunnel" || flag === "--port") {
|
|
215
284
|
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
216
285
|
if (raw === void 0 || raw === "") {
|
|
217
|
-
return { kind: "error", message:
|
|
286
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
218
287
|
}
|
|
219
288
|
if (flag === "--port") {
|
|
220
289
|
const parsed = parsePort(flag, raw);
|
|
@@ -248,19 +317,45 @@ function parseShare(rest) {
|
|
|
248
317
|
}
|
|
249
318
|
titles.push(argument);
|
|
250
319
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
320
|
+
const refusal = shareRefusal({
|
|
321
|
+
titles,
|
|
322
|
+
reach: reachGiven ? reach : void 0,
|
|
323
|
+
tunnel,
|
|
324
|
+
stop,
|
|
325
|
+
rotate,
|
|
326
|
+
revoke
|
|
327
|
+
});
|
|
328
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
329
|
+
return { kind: "share", titles, reach, tunnel, stop, rotate, revoke, port, json };
|
|
330
|
+
}
|
|
331
|
+
function parseLink(rest) {
|
|
332
|
+
const titles = [];
|
|
333
|
+
let port = null;
|
|
334
|
+
let json = false;
|
|
335
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
336
|
+
const argument = rest[index];
|
|
337
|
+
if (argument === "--json") {
|
|
338
|
+
json = true;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const equals = argument.indexOf("=");
|
|
342
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
343
|
+
if (flag === "--port") {
|
|
344
|
+
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
345
|
+
if (raw === void 0 || raw === "") return { kind: "error", message: valueRefusal(flag) };
|
|
346
|
+
const parsed = parsePort(flag, raw);
|
|
347
|
+
if ("error" in parsed) return { kind: "error", message: parsed.error };
|
|
348
|
+
port = parsed.port;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (argument.startsWith("-")) {
|
|
352
|
+
return { kind: "error", message: `leglas link does not take ${argument}.` };
|
|
353
|
+
}
|
|
354
|
+
titles.push(argument);
|
|
262
355
|
}
|
|
263
|
-
|
|
356
|
+
const refusal = linkRefusal(titles);
|
|
357
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
358
|
+
return { kind: "link", titles, port, json };
|
|
264
359
|
}
|
|
265
360
|
function parseArgs(argv) {
|
|
266
361
|
const [command] = argv;
|
|
@@ -325,7 +420,7 @@ function parseArgs(argv) {
|
|
|
325
420
|
if (argv[0] === "explore") {
|
|
326
421
|
const rest = argv.slice(1);
|
|
327
422
|
let surface;
|
|
328
|
-
let count =
|
|
423
|
+
let count = DEFAULT_EXPLORE_COUNT;
|
|
329
424
|
let basedOn = null;
|
|
330
425
|
let json = false;
|
|
331
426
|
let build = false;
|
|
@@ -458,7 +553,7 @@ function parseArgs(argv) {
|
|
|
458
553
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
459
554
|
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
460
555
|
if (raw === void 0 || raw === "") {
|
|
461
|
-
return { kind: "error", message:
|
|
556
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
462
557
|
}
|
|
463
558
|
if (flag === "--port") {
|
|
464
559
|
const parsed = parsePort(flag, raw);
|
|
@@ -473,12 +568,8 @@ function parseArgs(argv) {
|
|
|
473
568
|
};
|
|
474
569
|
}
|
|
475
570
|
width = Number(raw);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
kind: "error",
|
|
479
|
-
message: `--width must be between ${MIN_SHOW_WIDTH} and ${MAX_SHOW_WIDTH}, received ${width}.`
|
|
480
|
-
};
|
|
481
|
-
}
|
|
571
|
+
const refusal2 = widthRefusal(width);
|
|
572
|
+
if (refusal2 !== null) return { kind: "error", message: refusal2 };
|
|
482
573
|
continue;
|
|
483
574
|
}
|
|
484
575
|
if (argument.startsWith("-")) {
|
|
@@ -495,15 +586,27 @@ function parseArgs(argv) {
|
|
|
495
586
|
message: 'leglas show needs a direction title, for example: npx leglas show "Aurora" --json'
|
|
496
587
|
};
|
|
497
588
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
if (port !== null && !screenshot) {
|
|
502
|
-
return { kind: "error", message: "leglas show --port needs --screenshot." };
|
|
503
|
-
}
|
|
589
|
+
const refusal = showRefusal({ screenshot, width, port });
|
|
590
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
504
591
|
return { kind: "show", title, json, screenshot, width, port };
|
|
505
592
|
}
|
|
506
593
|
if (argv[0] === "share") return parseShare(argv.slice(1));
|
|
594
|
+
if (argv[0] === "link") return parseLink(argv.slice(1));
|
|
595
|
+
if (argv[0] === "remove") {
|
|
596
|
+
const rest = argv.slice(1);
|
|
597
|
+
const unknown = rest.find((argument) => argument.startsWith("-") && argument !== "--json");
|
|
598
|
+
if (unknown !== void 0) {
|
|
599
|
+
return { kind: "error", message: `leglas remove does not take ${unknown}.` };
|
|
600
|
+
}
|
|
601
|
+
const titles = rest.filter((argument) => argument !== "--json");
|
|
602
|
+
if (titles.length === 0) {
|
|
603
|
+
return {
|
|
604
|
+
kind: "error",
|
|
605
|
+
message: 'leglas remove needs a direction title, for example: npx leglas remove "Aurora"'
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return { kind: "remove", titles, json: rest.includes("--json") };
|
|
609
|
+
}
|
|
507
610
|
const options = {
|
|
508
611
|
port: void 0,
|
|
509
612
|
userPort: void 0,
|
|
@@ -536,7 +639,7 @@ function parseArgs(argv) {
|
|
|
536
639
|
value = argument.slice(equals + 1);
|
|
537
640
|
}
|
|
538
641
|
if (value === void 0 || value === "" || value.startsWith("--")) {
|
|
539
|
-
return { kind: "error", message:
|
|
642
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
540
643
|
}
|
|
541
644
|
if (flag === "--config") {
|
|
542
645
|
options.configPath = value;
|
|
@@ -909,6 +1012,12 @@ Useful to know:
|
|
|
909
1012
|
- Titles identify previews and must be unique. The user may rename one in the
|
|
910
1013
|
rail, which renames it on their machine only; the commands answer to either
|
|
911
1014
|
name, so use whichever they said.
|
|
1015
|
+
- \`npx leglas link "<title>"\` prints a link that opens the interface on that
|
|
1016
|
+
direction, and \`npx leglas link "<new>" "<old>"\` one that puts two side by
|
|
1017
|
+
side. Give it to the user when you finish a direction they asked for, rather
|
|
1018
|
+
than telling them where to click.
|
|
1019
|
+
- \`npx leglas remove "<title>"\` takes a direction registered on this machine
|
|
1020
|
+
off the rail when the user asks. One the config lists is theirs to edit.
|
|
912
1021
|
- \`npx leglas list\` shows every direction, shared and local.
|
|
913
1022
|
- \`npx leglas show "<title>" --json\` answers for one of them: the file behind it,
|
|
914
1023
|
the variants based on it, what it is being compared against and anything
|
|
@@ -956,7 +1065,7 @@ ${AGENTS_SECTION}`
|
|
|
956
1065
|
}
|
|
957
1066
|
|
|
958
1067
|
// src/keep.ts
|
|
959
|
-
import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, normalize as
|
|
1068
|
+
import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, normalize as normalize5 } from "path";
|
|
960
1069
|
|
|
961
1070
|
// ../server/dist/log.js
|
|
962
1071
|
var DEFAULT_LOG_DIR = "design-log";
|
|
@@ -1185,13 +1294,18 @@ function normalizeConfig(raw, options = {}) {
|
|
|
1185
1294
|
if (!isBoolean(scanPreviews)) {
|
|
1186
1295
|
errors.push("scanPreviews must be a boolean.");
|
|
1187
1296
|
}
|
|
1188
|
-
|
|
1297
|
+
const recordSets = source["recordSets"] ?? true;
|
|
1298
|
+
if (!isBoolean(recordSets)) {
|
|
1299
|
+
errors.push("recordSets must be a boolean.");
|
|
1300
|
+
}
|
|
1301
|
+
if (errors.length > 0 || !isString(devServer) || !isBoolean(scanPreviews) || !isBoolean(recordSets) || !isString(installCommand) || !isString(logDir))
|
|
1189
1302
|
return { config: null, errors };
|
|
1190
1303
|
return {
|
|
1191
1304
|
config: {
|
|
1192
1305
|
devServer,
|
|
1193
1306
|
previews,
|
|
1194
1307
|
scanPreviews,
|
|
1308
|
+
recordSets,
|
|
1195
1309
|
devCommand: isString(devCommand) ? devCommand : void 0,
|
|
1196
1310
|
installCommand,
|
|
1197
1311
|
logDir
|
|
@@ -1284,9 +1398,9 @@ function nextRequest(requests, failed) {
|
|
|
1284
1398
|
|
|
1285
1399
|
// ../server/dist/agents/agents.js
|
|
1286
1400
|
import { spawn } from "child_process";
|
|
1287
|
-
import { constants, readdirSync } from "fs";
|
|
1401
|
+
import { constants, readdirSync, realpathSync } from "fs";
|
|
1288
1402
|
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
1289
|
-
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
1403
|
+
import { delimiter, dirname, isAbsolute, join, normalize, relative } from "path";
|
|
1290
1404
|
var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
1291
1405
|
var effortFlag = (effort) => effort === null ? [] : ["--effort", effort];
|
|
1292
1406
|
var codexEffortConfig = (effort) => effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`];
|
|
@@ -1437,13 +1551,15 @@ var KNOWN_AGENTS = {
|
|
|
1437
1551
|
// the earlier turn.
|
|
1438
1552
|
activityVerified: true,
|
|
1439
1553
|
authArgs: ["status"],
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1554
|
+
// Both answers read from cursor-agent 2026.09.02, and both exit 0: "✓ Logged
|
|
1555
|
+
// in as <email>" and "Not logged in". The second contains the first's
|
|
1556
|
+
// words, so a "not logged in" or "not signed in" is read first.
|
|
1443
1557
|
authVerdict: (result) => {
|
|
1558
|
+
if (/not (logged|signed) in/i.test(result.stdout))
|
|
1559
|
+
return "signed-out";
|
|
1444
1560
|
if (/logged in|signed in/i.test(result.stdout))
|
|
1445
1561
|
return "ok";
|
|
1446
|
-
if (result.code !== 0 || /
|
|
1562
|
+
if (result.code !== 0 || /log in|sign in/i.test(result.stdout))
|
|
1447
1563
|
return "signed-out";
|
|
1448
1564
|
return "unknown";
|
|
1449
1565
|
}
|
|
@@ -1663,6 +1779,45 @@ function cursorActivity(event, cwd) {
|
|
|
1663
1779
|
const path = shownPath(args?.path, cwd);
|
|
1664
1780
|
return path === null ? `using ${tool}` : `using ${tool} on ${path}`;
|
|
1665
1781
|
}
|
|
1782
|
+
function editedFiles(agent, line, cwd) {
|
|
1783
|
+
let event;
|
|
1784
|
+
try {
|
|
1785
|
+
event = record(parseJson(line));
|
|
1786
|
+
} catch {
|
|
1787
|
+
return [];
|
|
1788
|
+
}
|
|
1789
|
+
const paths = [];
|
|
1790
|
+
if (agent === "claude" && event?.type === "assistant") {
|
|
1791
|
+
const content = record(event.message)?.content;
|
|
1792
|
+
for (const rawBlock of Array.isArray(content) ? content : []) {
|
|
1793
|
+
const block = record(rawBlock);
|
|
1794
|
+
const input = record(block?.input);
|
|
1795
|
+
if (block?.type === "tool_use" && isString(block.name) && ["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name))
|
|
1796
|
+
paths.push(input?.file_path ?? input?.notebook_path ?? null);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (agent === "codex" && (event?.type === "item.started" || event?.type === "item.completed") && record(event.item)?.type === "file_change") {
|
|
1800
|
+
const item = record(event.item);
|
|
1801
|
+
for (const change of Array.isArray(item?.changes) ? item.changes : [])
|
|
1802
|
+
paths.push(record(change)?.path ?? null);
|
|
1803
|
+
paths.push(item?.path ?? null);
|
|
1804
|
+
}
|
|
1805
|
+
if (paths.length === 0)
|
|
1806
|
+
return [];
|
|
1807
|
+
let root = cwd;
|
|
1808
|
+
try {
|
|
1809
|
+
root = realpathSync(cwd);
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
return paths.flatMap((path) => {
|
|
1813
|
+
if (!isString(path) || path === "")
|
|
1814
|
+
return [];
|
|
1815
|
+
if (!isAbsolute(path))
|
|
1816
|
+
return [normalize(path)];
|
|
1817
|
+
const shown = [relative(cwd, path), relative(root, path)].find((form) => !form.startsWith(".."));
|
|
1818
|
+
return [normalize(shown ?? path)];
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1666
1821
|
function activityFrom(agent, line, cwd = process.cwd()) {
|
|
1667
1822
|
let event;
|
|
1668
1823
|
try {
|
|
@@ -1888,9 +2043,21 @@ async function loadConfig(cwd) {
|
|
|
1888
2043
|
}
|
|
1889
2044
|
|
|
1890
2045
|
// ../server/dist/config/local-previews.js
|
|
1891
|
-
import {
|
|
2046
|
+
import { randomUUID } from "crypto";
|
|
2047
|
+
import { mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1892
2048
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1893
2049
|
var LOCAL_PREVIEWS_PATH = ".leglas/previews.json";
|
|
2050
|
+
async function writeRegistry(path, stored) {
|
|
2051
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
2052
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
2053
|
+
await writeFile2(temporary, `${JSON.stringify({ previews: stored }, null, 2)}
|
|
2054
|
+
`, "utf8");
|
|
2055
|
+
await rename(temporary, path).catch(async (cause) => {
|
|
2056
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
2057
|
+
});
|
|
2058
|
+
throw cause;
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
1894
2061
|
async function readLocalPreviews(cwd) {
|
|
1895
2062
|
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
1896
2063
|
let raw;
|
|
@@ -1962,10 +2129,10 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1962
2129
|
if (check.config === null) {
|
|
1963
2130
|
return { ok: false, error: check.errors.join(" ") };
|
|
1964
2131
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2132
|
+
await writeRegistry(join3(cwd, LOCAL_PREVIEWS_PATH), [
|
|
2133
|
+
...existing.previews.map(toStored),
|
|
2134
|
+
candidate
|
|
2135
|
+
]);
|
|
1969
2136
|
return { ok: true };
|
|
1970
2137
|
}
|
|
1971
2138
|
function toStored(preview) {
|
|
@@ -1977,13 +2144,127 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
1977
2144
|
const keep = existing.previews.filter((preview) => !titles.includes(preview.title));
|
|
1978
2145
|
if (keep.length === existing.previews.length)
|
|
1979
2146
|
return 0;
|
|
1980
|
-
|
|
1981
|
-
await mkdir2(dirname3(path), { recursive: true });
|
|
1982
|
-
await writeFile2(path, `${JSON.stringify({ previews: keep.map(toStored) }, null, 2)}
|
|
1983
|
-
`, "utf8");
|
|
2147
|
+
await writeRegistry(join3(cwd, LOCAL_PREVIEWS_PATH), keep.map(toStored));
|
|
1984
2148
|
return existing.previews.length - keep.length;
|
|
1985
2149
|
}
|
|
1986
2150
|
|
|
2151
|
+
// ../server/dist/generation/record.js
|
|
2152
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2153
|
+
import { appendFile, mkdir as mkdir3, readdir, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
2154
|
+
import { join as join4 } from "path";
|
|
2155
|
+
var RECORDS_DIR = ".leglas/generations";
|
|
2156
|
+
var KEPT_RECORDS = 100;
|
|
2157
|
+
async function recordIds(cwd) {
|
|
2158
|
+
const names = await readdir(join4(cwd, RECORDS_DIR)).catch(() => []);
|
|
2159
|
+
const started = (id) => Number.parseInt(id.slice(4), 36);
|
|
2160
|
+
return names.filter((name) => /^gen-[a-z0-9]+$/.test(name)).sort((a, b) => started(b) - started(a));
|
|
2161
|
+
}
|
|
2162
|
+
async function directionsOf(cwd, id) {
|
|
2163
|
+
let parsed;
|
|
2164
|
+
try {
|
|
2165
|
+
parsed = parseJson(await readFile4(join4(cwd, RECORDS_DIR, id, "set.json"), "utf8"));
|
|
2166
|
+
} catch {
|
|
2167
|
+
return [];
|
|
2168
|
+
}
|
|
2169
|
+
if (!isJsonRecord(parsed) || !Array.isArray(parsed.directions))
|
|
2170
|
+
return [];
|
|
2171
|
+
return parsed.directions.flatMap((direction) => {
|
|
2172
|
+
if (!isJsonRecord(direction) || !isString(direction.key))
|
|
2173
|
+
return [];
|
|
2174
|
+
const attempts2 = Array.isArray(direction.attempts) ? direction.attempts : [];
|
|
2175
|
+
const last = attempts2.at(-1);
|
|
2176
|
+
return [
|
|
2177
|
+
{
|
|
2178
|
+
key: direction.key,
|
|
2179
|
+
title: isJsonRecord(last) && isString(last.title) ? last.title : null
|
|
2180
|
+
}
|
|
2181
|
+
];
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
async function appendEvent(cwd, id, event) {
|
|
2185
|
+
const dir = join4(cwd, RECORDS_DIR, id);
|
|
2186
|
+
await mkdir3(dir, { recursive: true });
|
|
2187
|
+
await appendFile(join4(dir, "events.jsonl"), `${JSON.stringify(event)}
|
|
2188
|
+
`, "utf8");
|
|
2189
|
+
}
|
|
2190
|
+
async function setsOfTitles(cwd, titles) {
|
|
2191
|
+
const found = /* @__PURE__ */ new Map();
|
|
2192
|
+
for (const id of await recordIds(cwd)) {
|
|
2193
|
+
if (found.size === titles.length)
|
|
2194
|
+
break;
|
|
2195
|
+
for (const direction of await directionsOf(cwd, id)) {
|
|
2196
|
+
const title = direction.title;
|
|
2197
|
+
if (title !== null && titles.includes(title) && !found.has(title))
|
|
2198
|
+
found.set(title, { id, key: direction.key });
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return found;
|
|
2202
|
+
}
|
|
2203
|
+
async function setOfKey(cwd, key) {
|
|
2204
|
+
for (const id of await recordIds(cwd)) {
|
|
2205
|
+
if ((await directionsOf(cwd, id)).some((each) => each.key === key))
|
|
2206
|
+
return id;
|
|
2207
|
+
}
|
|
2208
|
+
return null;
|
|
2209
|
+
}
|
|
2210
|
+
async function noteDirections(cwd, titles, kind, detail) {
|
|
2211
|
+
const unique = [...new Set(titles)];
|
|
2212
|
+
const found = await setsOfTitles(cwd, unique).catch(() => /* @__PURE__ */ new Map());
|
|
2213
|
+
for (const title of unique) {
|
|
2214
|
+
const set = found.get(title);
|
|
2215
|
+
if (set === void 0)
|
|
2216
|
+
continue;
|
|
2217
|
+
const event = { at: Date.now(), kind, direction: set.key };
|
|
2218
|
+
if (detail !== void 0)
|
|
2219
|
+
event.detail = detail;
|
|
2220
|
+
await appendEvent(cwd, set.id, event).catch(() => {
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function createRecords(cwd) {
|
|
2225
|
+
let queue = Promise.resolve();
|
|
2226
|
+
const enqueue = (task) => {
|
|
2227
|
+
queue = queue.then(task).catch(() => {
|
|
2228
|
+
});
|
|
2229
|
+
};
|
|
2230
|
+
return {
|
|
2231
|
+
async prune() {
|
|
2232
|
+
const ids = await recordIds(cwd);
|
|
2233
|
+
for (const id of ids.slice(KEPT_RECORDS - 1)) {
|
|
2234
|
+
await rm2(join4(cwd, RECORDS_DIR, id), { recursive: true, force: true }).catch(() => {
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
2238
|
+
save(record3) {
|
|
2239
|
+
const body = `${JSON.stringify(record3, null, 2)}
|
|
2240
|
+
`;
|
|
2241
|
+
enqueue(async () => {
|
|
2242
|
+
const dir = join4(cwd, RECORDS_DIR, record3.id);
|
|
2243
|
+
const path = join4(dir, "set.json");
|
|
2244
|
+
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
2245
|
+
await mkdir3(dir, { recursive: true });
|
|
2246
|
+
await writeFile3(temporary, body, "utf8");
|
|
2247
|
+
await rename2(temporary, path).catch(async (cause) => {
|
|
2248
|
+
await rm2(temporary, { force: true }).catch(() => {
|
|
2249
|
+
});
|
|
2250
|
+
throw cause;
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2253
|
+
},
|
|
2254
|
+
picture(id, name, image) {
|
|
2255
|
+
enqueue(async () => {
|
|
2256
|
+
const dir = join4(cwd, RECORDS_DIR, id);
|
|
2257
|
+
await mkdir3(dir, { recursive: true });
|
|
2258
|
+
await writeFile3(join4(dir, name), image);
|
|
2259
|
+
});
|
|
2260
|
+
},
|
|
2261
|
+
event(id, event) {
|
|
2262
|
+
enqueue(() => appendEvent(cwd, id, event));
|
|
2263
|
+
},
|
|
2264
|
+
flush: () => queue
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
|
|
1987
2268
|
// ../server/dist/proxy.js
|
|
1988
2269
|
import http, {} from "http";
|
|
1989
2270
|
import net from "net";
|
|
@@ -2152,9 +2433,9 @@ import { randomBytes } from "crypto";
|
|
|
2152
2433
|
import nodeProcess from "process";
|
|
2153
2434
|
import { spawn as nodeSpawn } from "child_process";
|
|
2154
2435
|
import { accessSync, constants as constants2, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, writeFileSync } from "fs";
|
|
2155
|
-
import { readFile as
|
|
2436
|
+
import { readFile as readFile5, readdir as readdir2, rm as rm3, stat } from "fs/promises";
|
|
2156
2437
|
import { homedir, tmpdir as osTmpdir } from "os";
|
|
2157
|
-
import { delimiter as delimiter2, join as
|
|
2438
|
+
import { delimiter as delimiter2, join as join5 } from "path";
|
|
2158
2439
|
var NO_BROWSER = "No Chrome, Chromium, Brave or Edge was found on this machine, so Leglas could not take a screenshot. On a machine with no browser at all, `npx playwright install chromium` puts one where Leglas will find it. Set LEGLAS_BROWSER to point at a specific binary.";
|
|
2159
2440
|
var DARWIN_APPS = [
|
|
2160
2441
|
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -2187,7 +2468,7 @@ function firstOnPath(name, env) {
|
|
|
2187
2468
|
for (const directory of (env.PATH ?? "").split(delimiter2)) {
|
|
2188
2469
|
if (directory === "")
|
|
2189
2470
|
continue;
|
|
2190
|
-
const candidate =
|
|
2471
|
+
const candidate = join5(directory, name);
|
|
2191
2472
|
try {
|
|
2192
2473
|
accessSync(candidate, constants2.X_OK);
|
|
2193
2474
|
return candidate;
|
|
@@ -2225,17 +2506,17 @@ function buildNumber(entry) {
|
|
|
2225
2506
|
const digits = /(\d+)\s*$/.exec(entry)?.[1];
|
|
2226
2507
|
return digits === void 0 ? 0 : Number(digits);
|
|
2227
2508
|
}
|
|
2228
|
-
function cacheRoots(platform, home,
|
|
2229
|
-
const playwright = platform === "darwin" ?
|
|
2230
|
-
const puppeteer =
|
|
2509
|
+
function cacheRoots(platform, home, readdir7 = readableDirectories) {
|
|
2510
|
+
const playwright = platform === "darwin" ? join5(home, "Library", "Caches", "ms-playwright") : join5(home, ".cache", "ms-playwright");
|
|
2511
|
+
const puppeteer = join5(home, ".cache", "puppeteer");
|
|
2231
2512
|
const newestFirst = (dir, keep = () => true) => ({
|
|
2232
2513
|
root: dir,
|
|
2233
|
-
entries:
|
|
2514
|
+
entries: readdir7(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
|
|
2234
2515
|
});
|
|
2235
2516
|
return [
|
|
2236
2517
|
newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
|
|
2237
|
-
newestFirst(
|
|
2238
|
-
newestFirst(
|
|
2518
|
+
newestFirst(join5(puppeteer, "chrome-headless-shell")),
|
|
2519
|
+
newestFirst(join5(puppeteer, "chrome"))
|
|
2239
2520
|
];
|
|
2240
2521
|
}
|
|
2241
2522
|
function readableDirectories(dir) {
|
|
@@ -2251,18 +2532,18 @@ function findBrowser(search = {}) {
|
|
|
2251
2532
|
const home = search.home ?? homedir();
|
|
2252
2533
|
const exists = search.exists ?? existsSync2;
|
|
2253
2534
|
const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
|
|
2254
|
-
const
|
|
2535
|
+
const readdir7 = search.readdir ?? readableDirectories;
|
|
2255
2536
|
const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
|
|
2256
2537
|
for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
|
|
2257
2538
|
if (isString(candidate) && candidate !== "" && exists(candidate))
|
|
2258
2539
|
return candidate;
|
|
2259
2540
|
}
|
|
2260
|
-
const caches = cacheRoots(platform, home,
|
|
2261
|
-
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) =>
|
|
2541
|
+
const caches = cacheRoots(platform, home, readdir7);
|
|
2542
|
+
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join5(root, entry, ...rest)))));
|
|
2262
2543
|
if (shell !== null)
|
|
2263
2544
|
return shell;
|
|
2264
2545
|
if (platform === "darwin") {
|
|
2265
|
-
const installed = firstExisting(["/Applications",
|
|
2546
|
+
const installed = firstExisting(["/Applications", join5(home, "Applications")].flatMap((root) => DARWIN_APPS.map((app) => join5(root, app))));
|
|
2266
2547
|
if (installed !== null)
|
|
2267
2548
|
return installed;
|
|
2268
2549
|
}
|
|
@@ -2273,7 +2554,7 @@ function findBrowser(search = {}) {
|
|
|
2273
2554
|
return found;
|
|
2274
2555
|
}
|
|
2275
2556
|
const installed = firstExisting([
|
|
2276
|
-
...LINUX_NAMES.flatMap((name) => [
|
|
2557
|
+
...LINUX_NAMES.flatMap((name) => [join5("/usr/bin", name), join5("/snap/bin", name)]),
|
|
2277
2558
|
"/opt/google/chrome/chrome"
|
|
2278
2559
|
]);
|
|
2279
2560
|
if (installed !== null)
|
|
@@ -2281,32 +2562,32 @@ function findBrowser(search = {}) {
|
|
|
2281
2562
|
}
|
|
2282
2563
|
if (platform === "win32") {
|
|
2283
2564
|
const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => isString(entry) && entry !== "");
|
|
2284
|
-
const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) =>
|
|
2565
|
+
const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) => join5(root, browser))));
|
|
2285
2566
|
if (installed !== null)
|
|
2286
2567
|
return installed;
|
|
2287
2568
|
}
|
|
2288
2569
|
if (platform === "darwin" || platform === "linux") {
|
|
2289
|
-
const playwrightRoot = platform === "darwin" ?
|
|
2290
|
-
const playwright =
|
|
2291
|
-
const root =
|
|
2570
|
+
const playwrightRoot = platform === "darwin" ? join5(home, "Library", "Caches", "ms-playwright") : join5(home, ".cache", "ms-playwright");
|
|
2571
|
+
const playwright = readdir7(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2572
|
+
const root = join5(playwrightRoot, entry);
|
|
2292
2573
|
return [
|
|
2293
|
-
...FOR_TESTING.map((rest) =>
|
|
2294
|
-
...HEADLESS_SHELL.map((rest) =>
|
|
2574
|
+
...FOR_TESTING.map((rest) => join5(root, ...rest)),
|
|
2575
|
+
...HEADLESS_SHELL.map((rest) => join5(root, ...rest)),
|
|
2295
2576
|
// Older builds shipped a plain Chromium app.
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2577
|
+
join5(root, "chrome-mac-arm64", "Chromium.app", "Contents", "MacOS", "Chromium"),
|
|
2578
|
+
join5(root, "chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium"),
|
|
2579
|
+
join5(root, "chrome-linux", "chrome")
|
|
2299
2580
|
];
|
|
2300
2581
|
});
|
|
2301
2582
|
const playwrightBrowser = firstExisting(playwright);
|
|
2302
2583
|
if (playwrightBrowser !== null)
|
|
2303
2584
|
return playwrightBrowser;
|
|
2304
|
-
const puppeteerCache =
|
|
2585
|
+
const puppeteerCache = join5(home, ".cache", "puppeteer");
|
|
2305
2586
|
for (const kind of ["chrome", "chrome-headless-shell"]) {
|
|
2306
|
-
const kindRoot =
|
|
2307
|
-
const found = firstExisting(
|
|
2308
|
-
const root =
|
|
2309
|
-
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) =>
|
|
2587
|
+
const kindRoot = join5(puppeteerCache, kind);
|
|
2588
|
+
const found = firstExisting(readdir7(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2589
|
+
const root = join5(kindRoot, entry);
|
|
2590
|
+
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join5(root, ...rest));
|
|
2310
2591
|
}));
|
|
2311
2592
|
if (found !== null)
|
|
2312
2593
|
return found;
|
|
@@ -2431,8 +2712,8 @@ async function closeOrphan(url, connect) {
|
|
|
2431
2712
|
async function reapOrphanedBrowsers(deps = {}) {
|
|
2432
2713
|
const root = deps.tmpdir ?? osTmpdir();
|
|
2433
2714
|
const clock2 = deps.now ?? (() => Date.now());
|
|
2434
|
-
const list = deps.list ?? (async (path) => (await
|
|
2435
|
-
const read = deps.read ?? ((path) =>
|
|
2715
|
+
const list = deps.list ?? (async (path) => (await readdir2(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
|
|
2716
|
+
const read = deps.read ?? ((path) => readFile5(path, "utf8"));
|
|
2436
2717
|
const alive = deps.alive ?? livePid;
|
|
2437
2718
|
const profileOf = deps.profile ?? (async (path) => {
|
|
2438
2719
|
const entry = await stat(path);
|
|
@@ -2441,7 +2722,7 @@ async function reapOrphanedBrowsers(deps = {}) {
|
|
|
2441
2722
|
const currentUid = deps.uid ?? (() => nodeProcess.getuid?.() ?? null);
|
|
2442
2723
|
const connect = deps.connect ?? connectWebSocket;
|
|
2443
2724
|
const remove = deps.remove ?? (async (path) => {
|
|
2444
|
-
await
|
|
2725
|
+
await rm3(path, { recursive: true, force: true });
|
|
2445
2726
|
});
|
|
2446
2727
|
let names;
|
|
2447
2728
|
try {
|
|
@@ -2454,7 +2735,7 @@ async function reapOrphanedBrowsers(deps = {}) {
|
|
|
2454
2735
|
for (const name of names) {
|
|
2455
2736
|
if (!name.startsWith(PROFILE_PREFIX))
|
|
2456
2737
|
continue;
|
|
2457
|
-
const directory =
|
|
2738
|
+
const directory = join5(root, name);
|
|
2458
2739
|
const details = await profileOf(directory).catch(() => null);
|
|
2459
2740
|
if (details === null)
|
|
2460
2741
|
continue;
|
|
@@ -2462,7 +2743,7 @@ async function reapOrphanedBrowsers(deps = {}) {
|
|
|
2462
2743
|
continue;
|
|
2463
2744
|
let record3 = null;
|
|
2464
2745
|
try {
|
|
2465
|
-
const raw = await read(
|
|
2746
|
+
const raw = await read(join5(directory, OWNER_FILE));
|
|
2466
2747
|
const parsed = raw === null ? null : parseJson(raw);
|
|
2467
2748
|
record3 = isJsonRecord(parsed) ? parsed : null;
|
|
2468
2749
|
} catch {
|
|
@@ -2484,11 +2765,11 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2484
2765
|
const spawn5 = options.spawn ?? nodeSpawn;
|
|
2485
2766
|
const connect = options.connect ?? connectWebSocket;
|
|
2486
2767
|
const commandTimeoutMs = options.commandTimeoutMs ?? 3e4;
|
|
2487
|
-
const userDataDir =
|
|
2768
|
+
const userDataDir = join5(options.tmpdir ?? osTmpdir(), `leglas-browser-${randomBytes(8).toString("hex")}`);
|
|
2488
2769
|
const owned = (fields) => {
|
|
2489
2770
|
try {
|
|
2490
2771
|
mkdirSync(userDataDir, { recursive: true, mode: 448 });
|
|
2491
|
-
writeFileSync(
|
|
2772
|
+
writeFileSync(join5(userDataDir, OWNER_FILE), `${JSON.stringify({ owner: nodeProcess.pid, ...fields })}
|
|
2492
2773
|
`, { encoding: "utf8", mode: 384 });
|
|
2493
2774
|
} catch {
|
|
2494
2775
|
}
|
|
@@ -2508,11 +2789,10 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2508
2789
|
"--mute-audio",
|
|
2509
2790
|
"--force-color-profile=srgb",
|
|
2510
2791
|
"--window-size=1440,900",
|
|
2511
|
-
//
|
|
2512
|
-
//
|
|
2513
|
-
//
|
|
2514
|
-
//
|
|
2515
|
-
// otherwise reaches for the login keychain before it will start.
|
|
2792
|
+
// Desktop startup work a screenshot doesn't need. Measured here: about
|
|
2793
|
+
// 300MB and half a second off a Chrome launch, most of the wait off a
|
|
2794
|
+
// headless shell. `--use-mock-keychain` matters most on macOS, where
|
|
2795
|
+
// Chrome otherwise waits on the login keychain.
|
|
2516
2796
|
"--disable-dev-shm-usage",
|
|
2517
2797
|
"--disable-breakpad",
|
|
2518
2798
|
"--disable-client-side-phishing-detection",
|
|
@@ -2529,7 +2809,7 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2529
2809
|
try {
|
|
2530
2810
|
websocketUrl = await endpoint(process2, options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
2531
2811
|
} catch (error) {
|
|
2532
|
-
await
|
|
2812
|
+
await rm3(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
2533
2813
|
});
|
|
2534
2814
|
throw error;
|
|
2535
2815
|
}
|
|
@@ -2539,7 +2819,7 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2539
2819
|
socket = await connect(websocketUrl);
|
|
2540
2820
|
} catch (error) {
|
|
2541
2821
|
process2.kill("SIGKILL");
|
|
2542
|
-
await
|
|
2822
|
+
await rm3(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
2543
2823
|
});
|
|
2544
2824
|
throw error;
|
|
2545
2825
|
}
|
|
@@ -2692,7 +2972,7 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2692
2972
|
]);
|
|
2693
2973
|
}
|
|
2694
2974
|
socket.close();
|
|
2695
|
-
await
|
|
2975
|
+
await rm3(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
2696
2976
|
});
|
|
2697
2977
|
})();
|
|
2698
2978
|
return closing;
|
|
@@ -2700,8 +2980,8 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2700
2980
|
return {
|
|
2701
2981
|
withPage,
|
|
2702
2982
|
close,
|
|
2703
|
-
// A socket
|
|
2704
|
-
//
|
|
2983
|
+
// A socket gone with the process alive is the same to a caller: the pool
|
|
2984
|
+
// must not hand it out.
|
|
2705
2985
|
get closed() {
|
|
2706
2986
|
return processClosed || socketClosed;
|
|
2707
2987
|
}
|
|
@@ -2845,6 +3125,367 @@ function hydrationEvidence(messages) {
|
|
|
2845
3125
|
return null;
|
|
2846
3126
|
}
|
|
2847
3127
|
|
|
3128
|
+
// ../server/dist/capture/layout.js
|
|
3129
|
+
function isEdge(value) {
|
|
3130
|
+
return value === "left" || value === "right" || value === "top" || value === "bottom";
|
|
3131
|
+
}
|
|
3132
|
+
var LAYOUT_PROBE = `(() => {
|
|
3133
|
+
const OVERLAP_SHARE = 0.05;
|
|
3134
|
+
const OVERLAP_MIN = 1;
|
|
3135
|
+
const CUT_MIN = 3;
|
|
3136
|
+
const CUT_SHARE = 0.08;
|
|
3137
|
+
const LAYERED = 4;
|
|
3138
|
+
const TILT = 6;
|
|
3139
|
+
// Only a page gone wrong everywhere reaches this. The app's own findings are
|
|
3140
|
+
// subtracted after, so a low cap could crowd out a direction's.
|
|
3141
|
+
const LIMIT = 100;
|
|
3142
|
+
const width = window.innerWidth;
|
|
3143
|
+
const states = new Map();
|
|
3144
|
+
const inks = new Map();
|
|
3145
|
+
const context = document.createElement("canvas").getContext("2d");
|
|
3146
|
+
|
|
3147
|
+
// rgba(r, g, b, a), or a / before the alpha in the newer functions
|
|
3148
|
+
// (oklab, oklch, color), which computed colours keep.
|
|
3149
|
+
const alpha = (colour) => {
|
|
3150
|
+
if (colour === "transparent") return 0;
|
|
3151
|
+
const slash = /\\/\\s*([\\d.]+)(%?)\\s*\\)$/.exec(colour || "");
|
|
3152
|
+
if (slash) return parseFloat(slash[1]) / (slash[2] ? 100 : 1);
|
|
3153
|
+
const match = /rgba\\(([^)]+)\\)/.exec(colour || "");
|
|
3154
|
+
const parts = match ? match[1].split(/[\\s,]+/).filter(Boolean) : [];
|
|
3155
|
+
return parts.length > 3 ? parseFloat(parts[3]) : 1;
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3158
|
+
// Degrees this element turns its content by, or Infinity for a skew or a
|
|
3159
|
+
// 3D transform, whose boxes say nothing useful.
|
|
3160
|
+
const angleOf = (a, b, c, d) =>
|
|
3161
|
+
Math.abs(a * c + b * d) > 0.01 ? Infinity : (Math.atan2(b, a) * 180) / Math.PI;
|
|
3162
|
+
|
|
3163
|
+
const turnOf = (element, style) => {
|
|
3164
|
+
let turn = 0;
|
|
3165
|
+
if (style.rotate && style.rotate !== "none") turn += parseFloat(style.rotate) || 0;
|
|
3166
|
+
const transform = style.transform;
|
|
3167
|
+
if (transform && transform !== "none") {
|
|
3168
|
+
if (transform.startsWith("matrix3d")) return Infinity;
|
|
3169
|
+
const [a, b, c, d] = transform.slice(7, -1).split(",").map(Number);
|
|
3170
|
+
turn += angleOf(a, b, c, d);
|
|
3171
|
+
}
|
|
3172
|
+
return turn;
|
|
3173
|
+
};
|
|
3174
|
+
|
|
3175
|
+
const looping = new Set();
|
|
3176
|
+
try {
|
|
3177
|
+
for (const animation of document.getAnimations()) {
|
|
3178
|
+
const timing = animation.effect && animation.effect.getComputedTiming();
|
|
3179
|
+
const target = animation.effect && animation.effect.target;
|
|
3180
|
+
if (timing && timing.iterations === Infinity && target) looping.add(target);
|
|
3181
|
+
}
|
|
3182
|
+
} catch (error) {
|
|
3183
|
+
// An engine without getAnimations; nothing is excluded for it.
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const stateOf = (element) => {
|
|
3187
|
+
if (states.has(element)) return states.get(element);
|
|
3188
|
+
const style = getComputedStyle(element);
|
|
3189
|
+
const parent = element.parentElement ? stateOf(element.parentElement) : null;
|
|
3190
|
+
const clipsText = /text/.test(style.backgroundClip || "") || /text/.test(style.webkitBackgroundClip || "");
|
|
3191
|
+
const state = {
|
|
3192
|
+
style,
|
|
3193
|
+
opacity: (parent ? parent.opacity : 1) * parseFloat(style.opacity || "1"),
|
|
3194
|
+
turn: element instanceof SVGGraphicsElement && element.getCTM
|
|
3195
|
+
? (() => {
|
|
3196
|
+
const matrix = element.getCTM();
|
|
3197
|
+
return matrix ? angleOf(matrix.a, matrix.b, matrix.c, matrix.d) + (parent ? parent.turnOutsideSvg : 0) : 0;
|
|
3198
|
+
})()
|
|
3199
|
+
: (parent ? parent.turn : 0) + turnOf(element, style),
|
|
3200
|
+
// An <svg>'s own CSS transform is outside every getCTM beneath it.
|
|
3201
|
+
turnOutsideSvg: element.namespaceURI === "http://www.w3.org/2000/svg" && element.ownerSVGElement
|
|
3202
|
+
? (parent ? parent.turnOutsideSvg : 0)
|
|
3203
|
+
: (parent ? parent.turn : 0) + turnOf(element, style),
|
|
3204
|
+
masked:
|
|
3205
|
+
(parent ? parent.masked : false) ||
|
|
3206
|
+
(style.clipPath && style.clipPath !== "none") ||
|
|
3207
|
+
(style.maskImage && style.maskImage !== "none") ||
|
|
3208
|
+
(style.webkitMaskImage && style.webkitMaskImage !== "none"),
|
|
3209
|
+
truncates:
|
|
3210
|
+
(parent ? parent.truncates : false) ||
|
|
3211
|
+
style.textOverflow === "ellipsis" ||
|
|
3212
|
+
(style.webkitLineClamp && style.webkitLineClamp !== "none"),
|
|
3213
|
+
moving: (parent ? parent.moving : false) || looping.has(element),
|
|
3214
|
+
paintsText: (parent ? parent.paintsText : false) || clipsText,
|
|
3215
|
+
};
|
|
3216
|
+
states.set(element, state);
|
|
3217
|
+
return state;
|
|
3218
|
+
};
|
|
3219
|
+
|
|
3220
|
+
const blockOf = (element) => {
|
|
3221
|
+
if (element.namespaceURI === "http://www.w3.org/2000/svg") return element.closest("text") || element;
|
|
3222
|
+
for (let node = element; node; node = node.parentElement) {
|
|
3223
|
+
const display = stateOf(node).style.display;
|
|
3224
|
+
if (display !== "inline" && display !== "contents") return node;
|
|
3225
|
+
}
|
|
3226
|
+
return document.body;
|
|
3227
|
+
};
|
|
3228
|
+
|
|
3229
|
+
const set = (value) => Boolean(value) && value !== "none" && value !== "normal";
|
|
3230
|
+
|
|
3231
|
+
const establishes = (style) =>
|
|
3232
|
+
set(style.transform) ||
|
|
3233
|
+
set(style.scale) ||
|
|
3234
|
+
set(style.rotate) ||
|
|
3235
|
+
set(style.translate) ||
|
|
3236
|
+
set(style.perspective) ||
|
|
3237
|
+
set(style.filter) ||
|
|
3238
|
+
set(style.backdropFilter) ||
|
|
3239
|
+
set(style.containerType) ||
|
|
3240
|
+
style.contentVisibility === "auto" ||
|
|
3241
|
+
/transform|perspective|filter|scale|rotate|translate/.test(style.willChange || "") ||
|
|
3242
|
+
/paint|layout|strict|content/.test(style.contain || "");
|
|
3243
|
+
|
|
3244
|
+
const clipRect = (element, style) => {
|
|
3245
|
+
const match = /rect\\(([^)]+)\\)/.exec(style.clip || "");
|
|
3246
|
+
if (!match || (style.position !== "absolute" && style.position !== "fixed")) return null;
|
|
3247
|
+
const outer = element.getBoundingClientRect();
|
|
3248
|
+
const [top, right, bottom, left] = match[1].split(/[\\s,]+/).filter(Boolean);
|
|
3249
|
+
const at = (value, fallback, origin) => (value === "auto" ? fallback : origin + parseFloat(value));
|
|
3250
|
+
return {
|
|
3251
|
+
left: at(left, outer.left, outer.left),
|
|
3252
|
+
right: at(right, outer.right, outer.left),
|
|
3253
|
+
top: at(top, outer.top, outer.top),
|
|
3254
|
+
bottom: at(bottom, outer.bottom, outer.top),
|
|
3255
|
+
page: false,
|
|
3256
|
+
cutsX: true,
|
|
3257
|
+
cutsY: true,
|
|
3258
|
+
};
|
|
3259
|
+
};
|
|
3260
|
+
|
|
3261
|
+
// With the root's overflow visible, the body's overflow is the viewport's,
|
|
3262
|
+
// which the page clip already is, and the body's own box clips nothing.
|
|
3263
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
3264
|
+
const bodyIsViewport = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
|
|
3265
|
+
|
|
3266
|
+
// Every box that clips the text: its own element first. A positioned box
|
|
3267
|
+
// escapes every clipping box between it and its containing block.
|
|
3268
|
+
const clippers = (element) => {
|
|
3269
|
+
const found = [{ left: 0, right: width, top: 0, bottom: Infinity, page: true, cutsX: true, cutsY: true }];
|
|
3270
|
+
let escaping = null;
|
|
3271
|
+
for (let node = element; node && node !== document.documentElement; node = node.parentElement) {
|
|
3272
|
+
const style = stateOf(node).style;
|
|
3273
|
+
if (node !== element) {
|
|
3274
|
+
if (escaping === "absolute" && (style.position !== "static" || establishes(style))) escaping = null;
|
|
3275
|
+
if (escaping === "fixed" && establishes(style)) escaping = null;
|
|
3276
|
+
}
|
|
3277
|
+
if (escaping === null) {
|
|
3278
|
+
// A scrolling box hides what is scrolled away but cuts nothing off: the
|
|
3279
|
+
// rest is a scroll away. The document's own scroller hides nothing.
|
|
3280
|
+
const root = node === document.body;
|
|
3281
|
+
const hides = (value) => !(root && bodyIsViewport) && (value === "hidden" || value === "clip");
|
|
3282
|
+
const scrolls = (value) => !root && (value === "auto" || value === "scroll");
|
|
3283
|
+
const x = hides(style.overflowX) || scrolls(style.overflowX);
|
|
3284
|
+
const y = hides(style.overflowY) || scrolls(style.overflowY);
|
|
3285
|
+
if (x || y) {
|
|
3286
|
+
// The client box is measured before transforms, the bounding box after.
|
|
3287
|
+
const outer = node.getBoundingClientRect();
|
|
3288
|
+
const wide = typeof node.offsetWidth === "number" ? node.offsetWidth : node.clientWidth + 2 * node.clientLeft;
|
|
3289
|
+
const tall = typeof node.offsetHeight === "number" ? node.offsetHeight : node.clientHeight + 2 * node.clientTop;
|
|
3290
|
+
const across = wide > 0 ? outer.width / wide : 1;
|
|
3291
|
+
const down = tall > 0 ? outer.height / tall : 1;
|
|
3292
|
+
const left = outer.left + node.clientLeft * across;
|
|
3293
|
+
const top = outer.top + node.clientTop * down;
|
|
3294
|
+
found.push({
|
|
3295
|
+
left: x ? left : -Infinity,
|
|
3296
|
+
right: x ? left + node.clientWidth * across : Infinity,
|
|
3297
|
+
top: y ? top : -Infinity,
|
|
3298
|
+
bottom: y ? top + node.clientHeight * down : Infinity,
|
|
3299
|
+
page: false,
|
|
3300
|
+
cutsX: hides(style.overflowX),
|
|
3301
|
+
cutsY: hides(style.overflowY),
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
const clipped = clipRect(node, style);
|
|
3305
|
+
if (clipped) found.push(clipped);
|
|
3306
|
+
}
|
|
3307
|
+
if (style.position === "absolute" || style.position === "fixed") escaping = style.position;
|
|
3308
|
+
}
|
|
3309
|
+
return found;
|
|
3310
|
+
};
|
|
3311
|
+
|
|
3312
|
+
// The box around the letters themselves: a font's line box reaches past its
|
|
3313
|
+
// ink, and display type set tight overlaps line to line without touching.
|
|
3314
|
+
const inkOf = (words, style) => {
|
|
3315
|
+
const shown = style.textTransform === "uppercase" ? words.toUpperCase() : words;
|
|
3316
|
+
const font = [style.fontStyle, style.fontWeight, style.fontSize, style.fontFamily].join(" ");
|
|
3317
|
+
const key = font + "\\u0000" + style.letterSpacing + "\\u0000" + shown;
|
|
3318
|
+
if (inks.has(key)) return inks.get(key);
|
|
3319
|
+
let ink = null;
|
|
3320
|
+
if (context) {
|
|
3321
|
+
context.font = font;
|
|
3322
|
+
if ("letterSpacing" in context) context.letterSpacing = style.letterSpacing === "normal" ? "0px" : style.letterSpacing;
|
|
3323
|
+
const measured = context.measureText(shown);
|
|
3324
|
+
if (typeof measured.fontBoundingBoxAscent === "number") {
|
|
3325
|
+
ink = {
|
|
3326
|
+
top: Math.max(0, measured.fontBoundingBoxAscent - measured.actualBoundingBoxAscent),
|
|
3327
|
+
bottom: Math.max(0, measured.fontBoundingBoxDescent - measured.actualBoundingBoxDescent),
|
|
3328
|
+
left: -measured.actualBoundingBoxLeft,
|
|
3329
|
+
right: measured.actualBoundingBoxRight,
|
|
3330
|
+
height: measured.fontBoundingBoxAscent + measured.fontBoundingBoxDescent,
|
|
3331
|
+
advance: measured.width,
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
inks.set(key, ink);
|
|
3336
|
+
return ink;
|
|
3337
|
+
};
|
|
3338
|
+
|
|
3339
|
+
const area = (box) => Math.max(0, box.right - box.left) * Math.max(0, box.bottom - box.top);
|
|
3340
|
+
|
|
3341
|
+
const texts = [];
|
|
3342
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
3343
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
3344
|
+
const words = node.nodeValue.replace(/\\s+/g, " ").trim();
|
|
3345
|
+
const element = node.parentElement;
|
|
3346
|
+
if (words === "" || !element || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(element.tagName)) continue;
|
|
3347
|
+
const state = stateOf(element);
|
|
3348
|
+
// A slight tilt keeps its box close to its letters; more and the box lies.
|
|
3349
|
+
const tilt = Math.abs((((state.turn % 360) + 540) % 360) - 180);
|
|
3350
|
+
if (state.style.visibility !== "visible" || state.opacity < 0.05 || state.masked || !(tilt <= TILT)) continue;
|
|
3351
|
+
if (alpha(state.style.color) < 0.05 && alpha(state.style.webkitTextFillColor) < 0.05 && !state.paintsText) continue;
|
|
3352
|
+
const range = document.createRange();
|
|
3353
|
+
range.selectNodeContents(node);
|
|
3354
|
+
const ink = inkOf(words, state.style);
|
|
3355
|
+
const clips = clippers(element);
|
|
3356
|
+
const boxes = [];
|
|
3357
|
+
const lines = [...range.getClientRects()].filter((raw) => raw.width >= 2 && raw.height >= 2);
|
|
3358
|
+
const rtl = state.style.direction === "rtl";
|
|
3359
|
+
for (const raw of lines) {
|
|
3360
|
+
// Scaled type (an SVG viewBox, a scale transform) draws at another size
|
|
3361
|
+
// than its computed font-size; the box's own height says by how much.
|
|
3362
|
+
const scale = ink && ink.height > 0 ? raw.height / ink.height : 1;
|
|
3363
|
+
const top = ink ? ink.top * scale : 0;
|
|
3364
|
+
const bottom = ink ? ink.bottom * scale : 0;
|
|
3365
|
+
const box = raw.height - top - bottom >= 1
|
|
3366
|
+
? { left: raw.left, right: raw.right, top: raw.top + top, bottom: raw.bottom - bottom }
|
|
3367
|
+
: { left: raw.left, right: raw.right, top: raw.top, bottom: raw.bottom };
|
|
3368
|
+
// Across, the ink is known only when the whole string is on this one line,
|
|
3369
|
+
// and a line can be stretched one way only (scaleX).
|
|
3370
|
+
const wide = ink && ink.advance > 0 ? raw.width / ink.advance : scale;
|
|
3371
|
+
if (ink && lines.length === 1 && !rtl && (ink.right - ink.left) * wide >= 1) {
|
|
3372
|
+
box.left = Math.max(box.left, raw.left + ink.left * wide);
|
|
3373
|
+
box.right = Math.min(box.right, raw.left + ink.right * wide);
|
|
3374
|
+
}
|
|
3375
|
+
const seen = { ...box };
|
|
3376
|
+
for (const clip of clips) {
|
|
3377
|
+
seen.left = Math.max(seen.left, clip.left);
|
|
3378
|
+
seen.right = Math.min(seen.right, clip.right);
|
|
3379
|
+
seen.top = Math.max(seen.top, clip.top);
|
|
3380
|
+
seen.bottom = Math.min(seen.bottom, clip.bottom);
|
|
3381
|
+
}
|
|
3382
|
+
const shows = seen.right - seen.left >= 2 && seen.bottom - seen.top >= 2;
|
|
3383
|
+
boxes.push({ box, seen, shows, share: area(box) === 0 ? 0 : area(seen) / area(box), clips });
|
|
3384
|
+
}
|
|
3385
|
+
if (boxes.length === 0) continue;
|
|
3386
|
+
const drawn = ink && ink.height > 0 ? lines[0].height / ink.height : 1;
|
|
3387
|
+
const size = (parseFloat(state.style.fontSize) || 16) * drawn;
|
|
3388
|
+
texts.push({ words: words.slice(0, 80), boxes, block: blockOf(element), state, size });
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
const findings = [];
|
|
3392
|
+
const seen = new Set();
|
|
3393
|
+
|
|
3394
|
+
// Collisions between what shows of each box, swept by top edge.
|
|
3395
|
+
const shown = texts.flatMap((text) =>
|
|
3396
|
+
text.boxes.filter((entry) => entry.share >= 0.5).map((entry) => ({ box: entry.seen, text })),
|
|
3397
|
+
);
|
|
3398
|
+
shown.sort((a, b) => a.box.top - b.box.top);
|
|
3399
|
+
for (let i = 0; i < shown.length && findings.length < LIMIT; i += 1) {
|
|
3400
|
+
const a = shown[i];
|
|
3401
|
+
for (let j = i + 1; j < shown.length && shown[j].box.top < a.box.bottom; j += 1) {
|
|
3402
|
+
const b = shown[j];
|
|
3403
|
+
if (a.text.block === b.text.block || a.text === b.text) continue;
|
|
3404
|
+
if (a.text.state.moving || b.text.state.moving) continue;
|
|
3405
|
+
const across = Math.min(a.box.right, b.box.right) - Math.max(a.box.left, b.box.left);
|
|
3406
|
+
const down = Math.min(a.box.bottom, b.box.bottom) - Math.max(a.box.top, b.box.top);
|
|
3407
|
+
if (across < OVERLAP_MIN || down < OVERLAP_MIN) continue;
|
|
3408
|
+
if ((across * down) / Math.min(area(a.box), area(b.box)) < OVERLAP_SHARE) continue;
|
|
3409
|
+
// Words laid over giant display type are a composition, not a collision,
|
|
3410
|
+
// and a variable font's width is past what the canvas can measure.
|
|
3411
|
+
const larger = Math.max(a.text.size, b.text.size);
|
|
3412
|
+
const smaller = Math.min(a.text.size, b.text.size);
|
|
3413
|
+
if (larger >= LAYERED * smaller) continue;
|
|
3414
|
+
const pair = [a.text.words, b.text.words].sort().join("\\u0000");
|
|
3415
|
+
if (seen.has(pair)) continue;
|
|
3416
|
+
seen.add(pair);
|
|
3417
|
+
findings.push({ kind: "overlap", text: a.text.words, other: b.text.words });
|
|
3418
|
+
if (findings.length >= LIMIT) break;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// Cut off: partly shown. Text hidden whole is a choice (screen reader
|
|
3423
|
+
// text, a rolling digit, a carousel), not a mistake.
|
|
3424
|
+
for (const text of texts) {
|
|
3425
|
+
if (findings.length >= LIMIT) break;
|
|
3426
|
+
if (text.state.truncates || text.state.moving) continue;
|
|
3427
|
+
let cut = null;
|
|
3428
|
+
for (const entry of text.boxes) {
|
|
3429
|
+
if (!entry.shows || entry.share > 0.97) continue;
|
|
3430
|
+
const { box } = entry;
|
|
3431
|
+
// Nearest clip first, the page last. Past a scroll box, a clip further
|
|
3432
|
+
// out sees only what the scroll box shows.
|
|
3433
|
+
const inner = { ...box };
|
|
3434
|
+
for (const clip of [...entry.clips.slice(1), entry.clips[0]]) {
|
|
3435
|
+
const sides = [
|
|
3436
|
+
...(clip.cutsX ? [["left", clip.left - inner.left, box.right - box.left], ["right", inner.right - clip.right, box.right - box.left]] : []),
|
|
3437
|
+
...(clip.cutsY ? [["top", clip.top - inner.top, box.bottom - box.top], ["bottom", inner.bottom - clip.bottom, box.bottom - box.top]] : []),
|
|
3438
|
+
];
|
|
3439
|
+
for (const [edge, by, extent] of sides) {
|
|
3440
|
+
// A box flush with the page's edge cuts where the page does; say the page.
|
|
3441
|
+
const deeper = cut === null || by > cut.by || (clip.page && by === cut.by);
|
|
3442
|
+
if (by > Math.max(CUT_MIN, extent * CUT_SHARE) && deeper) {
|
|
3443
|
+
cut = { edge, by, within: clip.page ? "page" : "container" };
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
if (!clip.cutsX) {
|
|
3447
|
+
inner.left = Math.max(inner.left, clip.left);
|
|
3448
|
+
inner.right = Math.min(inner.right, clip.right);
|
|
3449
|
+
}
|
|
3450
|
+
if (!clip.cutsY) {
|
|
3451
|
+
inner.top = Math.max(inner.top, clip.top);
|
|
3452
|
+
inner.bottom = Math.min(inner.bottom, clip.bottom);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (cut) findings.push({ kind: "cut-off", text: text.words, edge: cut.edge, by: Math.round(cut.by), within: cut.within });
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
return findings;
|
|
3460
|
+
})()`;
|
|
3461
|
+
function layoutFindings(value) {
|
|
3462
|
+
if (!Array.isArray(value))
|
|
3463
|
+
return [];
|
|
3464
|
+
const findings = [];
|
|
3465
|
+
for (const entry of value) {
|
|
3466
|
+
if (!isJsonRecord(entry) || !isString(entry.text))
|
|
3467
|
+
continue;
|
|
3468
|
+
if (entry.kind === "overlap" && isString(entry.other)) {
|
|
3469
|
+
findings.push({ kind: "overlap", text: entry.text, other: entry.other });
|
|
3470
|
+
} else if (entry.kind === "cut-off" && isString(entry.edge) && isEdge(entry.edge) && isNumber(entry.by) && (entry.within === "page" || entry.within === "container")) {
|
|
3471
|
+
findings.push({
|
|
3472
|
+
kind: "cut-off",
|
|
3473
|
+
text: entry.text,
|
|
3474
|
+
edge: entry.edge,
|
|
3475
|
+
by: entry.by,
|
|
3476
|
+
within: entry.within
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
return findings;
|
|
3481
|
+
}
|
|
3482
|
+
function describeFinding(finding) {
|
|
3483
|
+
if (finding.kind === "overlap")
|
|
3484
|
+
return `"${finding.text}" runs into "${finding.other}"`;
|
|
3485
|
+
const where = finding.within === "page" ? "the page" : "the box around it";
|
|
3486
|
+
return `"${finding.text}" is cut off ${finding.by}px past the ${finding.edge} edge of ${where}`;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
2848
3489
|
// ../server/dist/capture/capture.js
|
|
2849
3490
|
var FRAME_MAX_HEIGHT = 4e3;
|
|
2850
3491
|
var MIN_WIDTH = 320;
|
|
@@ -2950,6 +3591,13 @@ function resourced(text3, url) {
|
|
|
2950
3591
|
function locatorExpression(focus) {
|
|
2951
3592
|
return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
|
|
2952
3593
|
}
|
|
3594
|
+
async function inspectLayout(page) {
|
|
3595
|
+
const response = await bounded(page.send("Runtime.evaluate", {
|
|
3596
|
+
expression: LAYOUT_PROBE,
|
|
3597
|
+
returnByValue: true
|
|
3598
|
+
}), 2e3, null).catch(() => null);
|
|
3599
|
+
return layoutFindings(resultValue(response));
|
|
3600
|
+
}
|
|
2953
3601
|
async function render(page, input) {
|
|
2954
3602
|
const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
|
|
2955
3603
|
const errors = [];
|
|
@@ -3109,22 +3757,30 @@ async function render(page, input) {
|
|
|
3109
3757
|
returnByValue: true
|
|
3110
3758
|
}), 2e3, void 0).catch(() => {
|
|
3111
3759
|
});
|
|
3760
|
+
const layout = input.inspect === true ? await inspectLayout(page) : [];
|
|
3112
3761
|
const metrics = await page.send("Page.getLayoutMetrics");
|
|
3113
3762
|
const size2 = metrics.cssContentSize ?? metrics.contentSize ?? { width, height: 900 };
|
|
3114
3763
|
const pageWidth = Math.max(width, Math.ceil(size2.width));
|
|
3115
3764
|
const contentHeight = Math.max(1, Math.ceil(size2.height));
|
|
3116
3765
|
const pageHeight = Math.min(FRAME_MAX_HEIGHT, contentHeight);
|
|
3117
3766
|
const cut = size2.height > FRAME_MAX_HEIGHT;
|
|
3767
|
+
const clip2 = { x: 0, y: 0, width, height: pageHeight, scale: 1 };
|
|
3118
3768
|
const frameResponse = await page.send("Page.captureScreenshot", {
|
|
3119
3769
|
format: "png",
|
|
3120
3770
|
captureBeyondViewport: true,
|
|
3121
|
-
clip:
|
|
3771
|
+
clip: clip2
|
|
3122
3772
|
});
|
|
3123
3773
|
const frame = {
|
|
3124
3774
|
png: Buffer.from(frameResponse.data, "base64"),
|
|
3125
3775
|
width,
|
|
3126
3776
|
height: pageHeight
|
|
3127
3777
|
};
|
|
3778
|
+
const jpeg = input.jpeg === true ? await page.send("Page.captureScreenshot", {
|
|
3779
|
+
format: "jpeg",
|
|
3780
|
+
quality: 80,
|
|
3781
|
+
captureBeyondViewport: true,
|
|
3782
|
+
clip: clip2
|
|
3783
|
+
}).then((shot) => Buffer.from(shot.data, "base64"), () => null) : null;
|
|
3128
3784
|
const crops = [];
|
|
3129
3785
|
for (const focus of input.focuses ?? []) {
|
|
3130
3786
|
const located = await page.send("Runtime.evaluate", {
|
|
@@ -3160,7 +3816,7 @@ async function render(page, input) {
|
|
|
3160
3816
|
resolved
|
|
3161
3817
|
});
|
|
3162
3818
|
}
|
|
3163
|
-
return { frame, crops, errors, hydration, cut };
|
|
3819
|
+
return { frame, crops, errors, hydration, cut, layout, jpeg };
|
|
3164
3820
|
} finally {
|
|
3165
3821
|
for (const stop of unlisten)
|
|
3166
3822
|
stop();
|
|
@@ -3172,8 +3828,8 @@ async function capturePage(browser, input) {
|
|
|
3172
3828
|
}
|
|
3173
3829
|
|
|
3174
3830
|
// ../server/dist/requests/attachments.js
|
|
3175
|
-
import { copyFile, lstat, mkdir as
|
|
3176
|
-
import { basename as basename2, extname, join as
|
|
3831
|
+
import { copyFile, lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile6, realpath, rename as rename3, rm as rm4, stat as stat2, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
3832
|
+
import { basename as basename2, extname, join as join6, resolve, sep } from "path";
|
|
3177
3833
|
var CAPTURES_DIR = ".leglas/captures";
|
|
3178
3834
|
var REFERENCES_DIR = ".leglas/references";
|
|
3179
3835
|
var LOAD_SHARE = 0.6;
|
|
@@ -3264,7 +3920,7 @@ function sniffImage(bytes) {
|
|
|
3264
3920
|
async function moveReference(cwd, id, destinationDir, index) {
|
|
3265
3921
|
let names;
|
|
3266
3922
|
try {
|
|
3267
|
-
names = await
|
|
3923
|
+
names = await readdir3(join6(cwd, REFERENCES_DIR));
|
|
3268
3924
|
} catch {
|
|
3269
3925
|
return null;
|
|
3270
3926
|
}
|
|
@@ -3273,17 +3929,17 @@ async function moveReference(cwd, id, destinationDir, index) {
|
|
|
3273
3929
|
return null;
|
|
3274
3930
|
const extension = extname(sourceName).toLowerCase();
|
|
3275
3931
|
const destinationName = `reference-${index}${extension}`;
|
|
3276
|
-
const source =
|
|
3277
|
-
const destination =
|
|
3278
|
-
await
|
|
3932
|
+
const source = join6(cwd, REFERENCES_DIR, sourceName);
|
|
3933
|
+
const destination = join6(destinationDir, destinationName);
|
|
3934
|
+
await mkdir4(destinationDir, { recursive: true });
|
|
3279
3935
|
try {
|
|
3280
|
-
await
|
|
3936
|
+
await rename3(source, destination);
|
|
3281
3937
|
} catch {
|
|
3282
3938
|
await copyFile(source, destination);
|
|
3283
3939
|
await unlink(source).catch(() => {
|
|
3284
3940
|
});
|
|
3285
3941
|
}
|
|
3286
|
-
const sniffed = sniffImage(await
|
|
3942
|
+
const sniffed = sniffImage(await readFile6(destination));
|
|
3287
3943
|
return {
|
|
3288
3944
|
kind: "reference",
|
|
3289
3945
|
file: `${CAPTURES_DIR}/${basename2(destinationDir)}/${destinationName}`,
|
|
@@ -3305,7 +3961,7 @@ function focusOf(note) {
|
|
|
3305
3961
|
async function attachRequest(cwd, requestId, input, deps) {
|
|
3306
3962
|
const capture = deps.capture ?? capturePage;
|
|
3307
3963
|
const deadlineMs = deps.deadlineMs ?? 12e3;
|
|
3308
|
-
const destination =
|
|
3964
|
+
const destination = join6(cwd, CAPTURES_DIR, requestId);
|
|
3309
3965
|
const captured = {
|
|
3310
3966
|
attachments: [],
|
|
3311
3967
|
errors: [],
|
|
@@ -3351,8 +4007,8 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3351
4007
|
const direction = await capture(browser, directionInput);
|
|
3352
4008
|
if (expired2)
|
|
3353
4009
|
return;
|
|
3354
|
-
await
|
|
3355
|
-
await
|
|
4010
|
+
await mkdir4(destination, { recursive: true });
|
|
4011
|
+
await writeFile4(join6(destination, "frame.png"), direction.frame.png);
|
|
3356
4012
|
captured.attachments.push({
|
|
3357
4013
|
kind: "frame",
|
|
3358
4014
|
file: `${CAPTURES_DIR}/${requestId}/frame.png`,
|
|
@@ -3370,7 +4026,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3370
4026
|
if (crop === null || crop === void 0 || note === void 0 || expired2)
|
|
3371
4027
|
continue;
|
|
3372
4028
|
const name = `note-${index + 1}.png`;
|
|
3373
|
-
await
|
|
4029
|
+
await writeFile4(join6(destination, name), crop.shot.png);
|
|
3374
4030
|
captured.attachments.push({
|
|
3375
4031
|
kind: "note",
|
|
3376
4032
|
file: `${CAPTURES_DIR}/${requestId}/${name}`,
|
|
@@ -3391,7 +4047,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3391
4047
|
const comparison = await capture(browser, compareInput);
|
|
3392
4048
|
if (expired2)
|
|
3393
4049
|
return;
|
|
3394
|
-
await
|
|
4050
|
+
await writeFile4(join6(destination, "compare.png"), comparison.frame.png);
|
|
3395
4051
|
captured.attachments.push({
|
|
3396
4052
|
kind: "compare",
|
|
3397
4053
|
file: `${CAPTURES_DIR}/${requestId}/compare.png`,
|
|
@@ -3415,13 +4071,13 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3415
4071
|
return captured;
|
|
3416
4072
|
}
|
|
3417
4073
|
function captureDirectory(cwd, requestId) {
|
|
3418
|
-
return /^[A-Za-z0-9_-]{1,32}$/.test(requestId) ?
|
|
4074
|
+
return /^[A-Za-z0-9_-]{1,32}$/.test(requestId) ? join6(cwd, CAPTURES_DIR, requestId) : null;
|
|
3419
4075
|
}
|
|
3420
4076
|
async function removeCaptures(cwd, requestId) {
|
|
3421
4077
|
const directory = captureDirectory(cwd, requestId);
|
|
3422
4078
|
if (directory === null)
|
|
3423
4079
|
return;
|
|
3424
|
-
await
|
|
4080
|
+
await rm4(directory, { recursive: true, force: true });
|
|
3425
4081
|
}
|
|
3426
4082
|
async function rehomeCaptures(cwd, from, to, attachments) {
|
|
3427
4083
|
if (attachments.length === 0)
|
|
@@ -3430,8 +4086,8 @@ async function rehomeCaptures(cwd, from, to, attachments) {
|
|
|
3430
4086
|
const destination = captureDirectory(cwd, to);
|
|
3431
4087
|
if (source === null || destination === null)
|
|
3432
4088
|
return [...attachments];
|
|
3433
|
-
await
|
|
3434
|
-
await
|
|
4089
|
+
await mkdir4(join6(cwd, CAPTURES_DIR), { recursive: true });
|
|
4090
|
+
await rename3(source, destination);
|
|
3435
4091
|
const prefix = `${CAPTURES_DIR}/${from}/`;
|
|
3436
4092
|
return attachments.map((attachment) => ({
|
|
3437
4093
|
...attachment,
|
|
@@ -3442,7 +4098,7 @@ function rehomeText(text3, from, to) {
|
|
|
3442
4098
|
return text3.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
|
|
3443
4099
|
}
|
|
3444
4100
|
async function ownCapturesRoot(cwd) {
|
|
3445
|
-
const root =
|
|
4101
|
+
const root = join6(cwd, CAPTURES_DIR);
|
|
3446
4102
|
try {
|
|
3447
4103
|
const entry = await lstat(root);
|
|
3448
4104
|
return entry.isDirectory() ? root : null;
|
|
@@ -3451,7 +4107,7 @@ async function ownCapturesRoot(cwd) {
|
|
|
3451
4107
|
}
|
|
3452
4108
|
}
|
|
3453
4109
|
async function isOwnCapture(cwd, file) {
|
|
3454
|
-
const root = await realpath(
|
|
4110
|
+
const root = await realpath(join6(cwd, CAPTURES_DIR)).catch(() => null);
|
|
3455
4111
|
if (root === null)
|
|
3456
4112
|
return false;
|
|
3457
4113
|
const real = await realpath(resolve(cwd, file)).catch(() => null);
|
|
@@ -3465,21 +4121,21 @@ async function pruneCaptures(cwd, keep) {
|
|
|
3465
4121
|
if (root === null)
|
|
3466
4122
|
return void await pruneReferences(cwd);
|
|
3467
4123
|
try {
|
|
3468
|
-
const entries = await
|
|
3469
|
-
await Promise.all(entries.filter((entry) => entry.isDirectory() && !kept.has(entry.name)).map((entry) =>
|
|
4124
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
4125
|
+
await Promise.all(entries.filter((entry) => entry.isDirectory() && !kept.has(entry.name)).map((entry) => rm4(join6(root, entry.name), { recursive: true, force: true })));
|
|
3470
4126
|
} catch {
|
|
3471
4127
|
}
|
|
3472
4128
|
await pruneReferences(cwd);
|
|
3473
4129
|
}
|
|
3474
4130
|
async function pruneReferences(cwd) {
|
|
3475
|
-
const references =
|
|
4131
|
+
const references = join6(cwd, REFERENCES_DIR);
|
|
3476
4132
|
try {
|
|
3477
4133
|
if (!(await lstat(references)).isDirectory())
|
|
3478
4134
|
return;
|
|
3479
|
-
const entries = await
|
|
4135
|
+
const entries = await readdir3(references, { withFileTypes: true });
|
|
3480
4136
|
const old = Date.now() - 60 * 60 * 1e3;
|
|
3481
4137
|
await Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
|
|
3482
|
-
const file =
|
|
4138
|
+
const file = join6(references, entry.name);
|
|
3483
4139
|
if ((await stat2(file)).mtimeMs < old)
|
|
3484
4140
|
await unlink(file).catch(() => {
|
|
3485
4141
|
});
|
|
@@ -3490,9 +4146,9 @@ async function pruneReferences(cwd) {
|
|
|
3490
4146
|
|
|
3491
4147
|
// ../server/dist/branches/worktree.js
|
|
3492
4148
|
import { execFile, spawn as spawn2 } from "child_process";
|
|
3493
|
-
import { rm as
|
|
4149
|
+
import { rm as rm5 } from "fs/promises";
|
|
3494
4150
|
import net2 from "net";
|
|
3495
|
-
import { join as
|
|
4151
|
+
import { join as join7 } from "path";
|
|
3496
4152
|
import { promisify } from "util";
|
|
3497
4153
|
var run = promisify(execFile);
|
|
3498
4154
|
var WORKTREES_DIR = ".leglas/worktrees";
|
|
@@ -3534,10 +4190,10 @@ async function answeringHost(port) {
|
|
|
3534
4190
|
}
|
|
3535
4191
|
async function startWorktree(options) {
|
|
3536
4192
|
const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
|
|
3537
|
-
const path =
|
|
4193
|
+
const path = join7(options.cwd, WORKTREES_DIR, worktreeSlug(options.branch));
|
|
3538
4194
|
const log = options.onLog ?? (() => {
|
|
3539
4195
|
});
|
|
3540
|
-
await
|
|
4196
|
+
await rm5(path, { recursive: true, force: true });
|
|
3541
4197
|
await run("git", ["worktree", "prune"], { cwd: options.cwd }).catch(() => {
|
|
3542
4198
|
});
|
|
3543
4199
|
try {
|
|
@@ -3549,7 +4205,7 @@ async function startWorktree(options) {
|
|
|
3549
4205
|
const cleanup = async () => {
|
|
3550
4206
|
await run("git", ["worktree", "remove", "--force", path], { cwd: options.cwd }).catch(() => {
|
|
3551
4207
|
});
|
|
3552
|
-
await
|
|
4208
|
+
await rm5(path, { recursive: true, force: true }).catch(() => {
|
|
3553
4209
|
});
|
|
3554
4210
|
};
|
|
3555
4211
|
try {
|
|
@@ -3598,8 +4254,8 @@ async function startAppProcess(options) {
|
|
|
3598
4254
|
child = spawn2(substitutePort(options.devCommand, port), {
|
|
3599
4255
|
cwd: options.cwd,
|
|
3600
4256
|
shell: true,
|
|
3601
|
-
// Own process group, so stopping kills the shell and
|
|
3602
|
-
//
|
|
4257
|
+
// Own process group, so stopping kills the shell and everything it
|
|
4258
|
+
// spawned instead of orphaning a dev server on the port.
|
|
3603
4259
|
detached: true,
|
|
3604
4260
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3605
4261
|
});
|
|
@@ -3845,8 +4501,8 @@ function conversationFailure(code) {
|
|
|
3845
4501
|
|
|
3846
4502
|
// ../server/dist/requests/annotations.js
|
|
3847
4503
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3848
|
-
import { mkdir as
|
|
3849
|
-
import { dirname as dirname4, join as
|
|
4504
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
4505
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
3850
4506
|
var ANNOTATIONS_PATH = ".leglas/annotations.json";
|
|
3851
4507
|
var NOTE_CAP = 500;
|
|
3852
4508
|
var SELECTOR_CAP = 300;
|
|
@@ -3910,7 +4566,7 @@ function anchorFrom(value) {
|
|
|
3910
4566
|
}
|
|
3911
4567
|
async function readAnnotations(cwd) {
|
|
3912
4568
|
try {
|
|
3913
|
-
const raw = await
|
|
4569
|
+
const raw = await readFile7(join8(cwd, ANNOTATIONS_PATH), "utf8");
|
|
3914
4570
|
const parsed = parseJson(raw);
|
|
3915
4571
|
if (!isJsonRecord(parsed) || !Array.isArray(parsed.annotations))
|
|
3916
4572
|
return [];
|
|
@@ -3935,9 +4591,9 @@ async function readAnnotations(cwd) {
|
|
|
3935
4591
|
}
|
|
3936
4592
|
}
|
|
3937
4593
|
async function write(cwd, annotations) {
|
|
3938
|
-
const path =
|
|
3939
|
-
await
|
|
3940
|
-
await
|
|
4594
|
+
const path = join8(cwd, ANNOTATIONS_PATH);
|
|
4595
|
+
await mkdir5(dirname4(path), { recursive: true });
|
|
4596
|
+
await writeFile5(path, `${JSON.stringify({ annotations }, null, 2)}
|
|
3941
4597
|
`, "utf8");
|
|
3942
4598
|
}
|
|
3943
4599
|
var writing = Promise.resolve();
|
|
@@ -4008,9 +4664,9 @@ function describeAnnotations(annotations) {
|
|
|
4008
4664
|
}
|
|
4009
4665
|
|
|
4010
4666
|
// ../server/dist/requests/requests.js
|
|
4011
|
-
import { mkdir as
|
|
4667
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
4012
4668
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
4013
|
-
import { dirname as dirname5, join as
|
|
4669
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
4014
4670
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
4015
4671
|
function variantSlot(url) {
|
|
4016
4672
|
if (!url.startsWith("/"))
|
|
@@ -4189,7 +4845,7 @@ function failureOf(entry) {
|
|
|
4189
4845
|
}
|
|
4190
4846
|
async function readRequests(cwd) {
|
|
4191
4847
|
try {
|
|
4192
|
-
const raw = await
|
|
4848
|
+
const raw = await readFile8(join9(cwd, REQUESTS_PATH), "utf8");
|
|
4193
4849
|
const parsed = parseJson(raw);
|
|
4194
4850
|
if (!isJsonRecord(parsed) || !Array.isArray(parsed.requests))
|
|
4195
4851
|
return [];
|
|
@@ -4225,9 +4881,9 @@ async function readRequests(cwd) {
|
|
|
4225
4881
|
}
|
|
4226
4882
|
}
|
|
4227
4883
|
async function writeQueue(cwd, requests) {
|
|
4228
|
-
const path =
|
|
4229
|
-
await
|
|
4230
|
-
await
|
|
4884
|
+
const path = join9(cwd, REQUESTS_PATH);
|
|
4885
|
+
await mkdir6(dirname5(path), { recursive: true });
|
|
4886
|
+
await writeFile6(path, `${JSON.stringify({ requests }, null, 2)}
|
|
4231
4887
|
`, "utf8");
|
|
4232
4888
|
}
|
|
4233
4889
|
async function appendRequest(cwd, request, id = newRequestId()) {
|
|
@@ -4285,12 +4941,12 @@ async function clearRequests(cwd) {
|
|
|
4285
4941
|
|
|
4286
4942
|
// ../server/dist/agents/runner.js
|
|
4287
4943
|
import { spawn as nodeSpawn3 } from "child_process";
|
|
4288
|
-
import { readFile as
|
|
4289
|
-
import { join as
|
|
4944
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4945
|
+
import { join as join10, resolve as resolve2 } from "path";
|
|
4290
4946
|
|
|
4291
4947
|
// ../server/dist/agents/claude-agent-session.js
|
|
4292
4948
|
import { EventEmitter } from "events";
|
|
4293
|
-
import { readFile as
|
|
4949
|
+
import { readFile as readFile9, stat as stat3 } from "fs/promises";
|
|
4294
4950
|
import { extname as extname2 } from "path";
|
|
4295
4951
|
import { PassThrough } from "stream";
|
|
4296
4952
|
var INITIALIZE_TIMEOUT_MS = 3e4;
|
|
@@ -4320,7 +4976,7 @@ async function claudeContent(prompt, images2) {
|
|
|
4320
4976
|
try {
|
|
4321
4977
|
if ((await stat3(path)).size > IMAGE_MAX_BYTES)
|
|
4322
4978
|
continue;
|
|
4323
|
-
const data = await
|
|
4979
|
+
const data = await readFile9(path);
|
|
4324
4980
|
blocks.push({
|
|
4325
4981
|
type: "image",
|
|
4326
4982
|
source: { type: "base64", media_type: mediaType, data: data.toString("base64") }
|
|
@@ -4453,11 +5109,10 @@ var PersistentClaudeSession = class {
|
|
|
4453
5109
|
warmedFor = null;
|
|
4454
5110
|
closed = false;
|
|
4455
5111
|
/**
|
|
4456
|
-
* Counts every ask for a warm process.
|
|
4457
|
-
*
|
|
4458
|
-
*
|
|
4459
|
-
*
|
|
4460
|
-
* release captured at entry says which side of that it is on.
|
|
5112
|
+
* Counts every ask for a warm process. warm() and release() aren't awaited
|
|
5113
|
+
* and race both ways: a release must outlast a warm that started while it
|
|
5114
|
+
* settled, and stand down for a warm asked for after it began. The generation
|
|
5115
|
+
* it captured says which.
|
|
4461
5116
|
*/
|
|
4462
5117
|
generation = 0;
|
|
4463
5118
|
constructor(cwd, allowedCommands, startup) {
|
|
@@ -4567,13 +5222,10 @@ var PersistentClaudeSession = class {
|
|
|
4567
5222
|
}
|
|
4568
5223
|
}
|
|
4569
5224
|
/**
|
|
4570
|
-
*
|
|
4571
|
-
*
|
|
4572
|
-
*
|
|
4573
|
-
*
|
|
4574
|
-
* may never come. The runner calls this once nothing has asked for Claude
|
|
4575
|
-
* in a while, and the next warm() starts it again. A session id the runner
|
|
4576
|
-
* still holds is served by the `claude --resume` fallback in the meantime.
|
|
5225
|
+
* Ends the native process but keeps the transport. A warm session is Claude
|
|
5226
|
+
* Code plus every MCP server the user configured, held for a request that may
|
|
5227
|
+
* not come. Called after an idle spell; the next warm() starts it again, and
|
|
5228
|
+
* a held session id goes through `claude --resume` meanwhile.
|
|
4577
5229
|
*/
|
|
4578
5230
|
async release() {
|
|
4579
5231
|
const asOf = this.generation;
|
|
@@ -4777,12 +5429,10 @@ var PersistentCodexAppServer = class {
|
|
|
4777
5429
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
4778
5430
|
}
|
|
4779
5431
|
/**
|
|
4780
|
-
* Counts every ask for a warm process.
|
|
4781
|
-
*
|
|
4782
|
-
*
|
|
4783
|
-
*
|
|
4784
|
-
* reset restarts through start() rather than warm(), so waiting its turn
|
|
4785
|
-
* does not read as a fresh ask.
|
|
5432
|
+
* Counts every ask for a warm process. warm() and release() aren't awaited
|
|
5433
|
+
* and race both ways: a release must outlast a warm queued behind its reset,
|
|
5434
|
+
* and stand down for a warm asked for after it began. A queued warm restarts
|
|
5435
|
+
* through start(), so waiting doesn't count as a new ask.
|
|
4786
5436
|
*/
|
|
4787
5437
|
generation = 0;
|
|
4788
5438
|
warm(_sessionId = null) {
|
|
@@ -4903,11 +5553,9 @@ var PersistentCodexAppServer = class {
|
|
|
4903
5553
|
}
|
|
4904
5554
|
}
|
|
4905
5555
|
/**
|
|
4906
|
-
*
|
|
4907
|
-
*
|
|
4908
|
-
*
|
|
4909
|
-
* next warm() spawns and handshakes again. A thread id the runner still
|
|
4910
|
-
* holds is resumed by the new process through `thread/resume`.
|
|
5556
|
+
* Ends the app-server process but keeps the transport. Called after an idle
|
|
5557
|
+
* spell; the next warm() spawns again and resumes a held thread through
|
|
5558
|
+
* `thread/resume`.
|
|
4911
5559
|
*/
|
|
4912
5560
|
async release() {
|
|
4913
5561
|
const asOf = this.generation;
|
|
@@ -5540,7 +6188,7 @@ function startRunner(options) {
|
|
|
5540
6188
|
active = null;
|
|
5541
6189
|
}
|
|
5542
6190
|
};
|
|
5543
|
-
const registered = () =>
|
|
6191
|
+
const registered = () => readFile10(join10(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
5544
6192
|
const handle = async (request, choice) => {
|
|
5545
6193
|
const allowedCommands = options.leglasCommand === void 0 ? [] : [
|
|
5546
6194
|
`${options.leglasCommand} show`,
|
|
@@ -5584,13 +6232,11 @@ function startRunner(options) {
|
|
|
5584
6232
|
retry: observed.retry
|
|
5585
6233
|
});
|
|
5586
6234
|
let failure = verdict();
|
|
5587
|
-
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && // The edited flag is only
|
|
5588
|
-
// real output
|
|
5589
|
-
//
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
// child settling and the retry starting finds no child to cancel, so
|
|
5593
|
-
// nothing says "cancelled". Stopped still means stopped.
|
|
6235
|
+
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && // The edited flag is evidence only where Leglas has read the vendor's
|
|
6236
|
+
// real output; elsewhere "didn't edit" means "wasn't seen to edit".
|
|
6237
|
+
// Those vendors keep the failure card and its Retry.
|
|
6238
|
+
activityVerified(resolved.agent) && conversationFailure(failure.code) && // Not redundant: a stop between the first child settling and the retry
|
|
6239
|
+
// starting finds no child to cancel.
|
|
5594
6240
|
!stopped) {
|
|
5595
6241
|
sessions.delete(resolved.agent);
|
|
5596
6242
|
const cold = resolveCommand(choice, request.prompt, null, allowedCommands, images2);
|
|
@@ -5735,9 +6381,8 @@ function startRunner(options) {
|
|
|
5735
6381
|
snapshot: () => ({ ...state, failedIds: [...failed] }),
|
|
5736
6382
|
cancel,
|
|
5737
6383
|
prepare,
|
|
5738
|
-
// A nudge during a tick is latched
|
|
5739
|
-
//
|
|
5740
|
-
// the active agent.
|
|
6384
|
+
// A nudge during a tick is latched until the tick settles; between ticks it
|
|
6385
|
+
// starts one at once. Neither overlaps the active agent.
|
|
5741
6386
|
nudge: () => schedule(true)
|
|
5742
6387
|
};
|
|
5743
6388
|
}
|
|
@@ -5946,7 +6591,7 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5946
6591
|
}
|
|
5947
6592
|
|
|
5948
6593
|
// ../server/dist/share/share.js
|
|
5949
|
-
import { randomBytes as randomBytes4, randomUUID, timingSafeEqual } from "crypto";
|
|
6594
|
+
import { randomBytes as randomBytes4, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
5950
6595
|
import http3 from "http";
|
|
5951
6596
|
import { posix } from "path";
|
|
5952
6597
|
|
|
@@ -6267,10 +6912,10 @@ var DEV_CONTROL_ROUTES = [
|
|
|
6267
6912
|
/** read, browser-sync 3.0.4: its client surface and server metadata. */
|
|
6268
6913
|
"/__browser_sync__",
|
|
6269
6914
|
/**
|
|
6270
|
-
* read, webpack-dev-server 6.0.0
|
|
6271
|
-
*
|
|
6272
|
-
*
|
|
6273
|
-
*
|
|
6915
|
+
* read, webpack-dev-server 6.0.0: the file listing,
|
|
6916
|
+
* `/webpack-dev-server/invalidate` (forces a rebuild) and
|
|
6917
|
+
* `/webpack-dev-server/open-editor` (launch-editor again). The subtree match
|
|
6918
|
+
* covers all three.
|
|
6274
6919
|
*/
|
|
6275
6920
|
"/webpack-dev-server",
|
|
6276
6921
|
/** reported: Rails Web Console, an interactive server-side REPL. */
|
|
@@ -6295,17 +6940,12 @@ var DEV_CONTROL_ROUTES = [
|
|
|
6295
6940
|
var DEV_CONTROL_PREFIXES = [
|
|
6296
6941
|
/** read, Next 16.3.1. Its app assets sit at `/_next/` and stay allowed. */
|
|
6297
6942
|
"/__nextjs_",
|
|
6298
|
-
/**
|
|
6299
|
-
* read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's own bundle is at `/_nuxt/`,
|
|
6300
|
-
* one underscore and a different prefix, so the app is untouched.
|
|
6301
|
-
*/
|
|
6943
|
+
/** read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's bundle is at `/_nuxt/`, a different prefix. */
|
|
6302
6944
|
"/__nuxt_devtools__",
|
|
6303
6945
|
/**
|
|
6304
|
-
* read, Parcel 2.16.4: `__parcel_launch_editor`
|
|
6305
|
-
*
|
|
6306
|
-
*
|
|
6307
|
-
* which serve source, and the HMR and health routes, which a viewer has no
|
|
6308
|
-
* use for: their live-reload socket is already refused.
|
|
6946
|
+
* read, Parcel 2.16.4: `__parcel_launch_editor` takes a `file` and calls
|
|
6947
|
+
* launch-editor. Its source-map, source-root, code-frame, HMR and health
|
|
6948
|
+
* routes serve source or are useless to a viewer.
|
|
6309
6949
|
*/
|
|
6310
6950
|
"/__parcel_"
|
|
6311
6951
|
];
|
|
@@ -6681,7 +7321,7 @@ function createShareManager(options) {
|
|
|
6681
7321
|
token = randomBytes4(24).toString("base64url");
|
|
6682
7322
|
const at = now();
|
|
6683
7323
|
const grant = {
|
|
6684
|
-
id:
|
|
7324
|
+
id: randomUUID3(),
|
|
6685
7325
|
name,
|
|
6686
7326
|
token,
|
|
6687
7327
|
createdAt: at,
|
|
@@ -6996,7 +7636,7 @@ function createShareManager(options) {
|
|
|
6996
7636
|
}
|
|
6997
7637
|
const share = {
|
|
6998
7638
|
...parsed.manifest,
|
|
6999
|
-
id:
|
|
7639
|
+
id: randomUUID3(),
|
|
7000
7640
|
grants: /* @__PURE__ */ new Map(),
|
|
7001
7641
|
tombstones: [],
|
|
7002
7642
|
port,
|
|
@@ -7027,8 +7667,8 @@ function createShareManager(options) {
|
|
|
7027
7667
|
share.runningTunnel = runTunnel({
|
|
7028
7668
|
provider,
|
|
7029
7669
|
port,
|
|
7030
|
-
// Whichever link exists when the tunnel starts
|
|
7031
|
-
//
|
|
7670
|
+
// Whichever link exists when the tunnel starts; a share always has
|
|
7671
|
+
// one, and the probe only needs a path the listener answers.
|
|
7032
7672
|
entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
|
|
7033
7673
|
onState: (next) => {
|
|
7034
7674
|
if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
@@ -7181,11 +7821,9 @@ function createShareManager(options) {
|
|
|
7181
7821
|
return null;
|
|
7182
7822
|
return {
|
|
7183
7823
|
...options.viewerConfig,
|
|
7184
|
-
// The project id is the config's absolute path
|
|
7185
|
-
//
|
|
7186
|
-
// server
|
|
7187
|
-
// never dials the dev server, so the share's own id does the job and
|
|
7188
|
-
// the address is left blank.
|
|
7824
|
+
// The project id is the config's absolute path and the dev server address
|
|
7825
|
+
// names the sharer's network. Viewers keep no layout and never dial the
|
|
7826
|
+
// dev server, so they get the share's id and a blank address.
|
|
7189
7827
|
project: `share:${share.id}`,
|
|
7190
7828
|
devServer: "",
|
|
7191
7829
|
previews: options.previewsForConfig(previews),
|
|
@@ -7253,12 +7891,12 @@ function createShareManager(options) {
|
|
|
7253
7891
|
}
|
|
7254
7892
|
|
|
7255
7893
|
// ../server/dist/config/renames.js
|
|
7256
|
-
import { mkdir as
|
|
7257
|
-
import { dirname as dirname6, join as
|
|
7894
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
|
|
7895
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
7258
7896
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
7259
7897
|
async function readRenames(cwd) {
|
|
7260
7898
|
try {
|
|
7261
|
-
const raw = await
|
|
7899
|
+
const raw = await readFile11(join11(cwd, RENAMES_PATH), "utf8");
|
|
7262
7900
|
const parsed = parseJson(raw);
|
|
7263
7901
|
if (!isJsonRecord(parsed) || !isJsonRecord(parsed.renames) && !Array.isArray(parsed.renames))
|
|
7264
7902
|
return {};
|
|
@@ -7268,9 +7906,9 @@ async function readRenames(cwd) {
|
|
|
7268
7906
|
}
|
|
7269
7907
|
}
|
|
7270
7908
|
async function writeRenames(cwd, renames) {
|
|
7271
|
-
const path =
|
|
7272
|
-
await
|
|
7273
|
-
await
|
|
7909
|
+
const path = join11(cwd, RENAMES_PATH);
|
|
7910
|
+
await mkdir7(dirname6(path), { recursive: true });
|
|
7911
|
+
await writeFile7(path, `${JSON.stringify({ renames }, null, 2)}
|
|
7274
7912
|
`, "utf8");
|
|
7275
7913
|
}
|
|
7276
7914
|
function resolveTitle(input, titles, renames) {
|
|
@@ -7287,10 +7925,10 @@ function resolveTitle(input, titles, renames) {
|
|
|
7287
7925
|
// ../server/dist/server.js
|
|
7288
7926
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
7289
7927
|
import { createHash as createHash2 } from "crypto";
|
|
7290
|
-
import { mkdir as
|
|
7928
|
+
import { mkdir as mkdir9, readdir as readdir6, writeFile as writeFile10 } from "fs/promises";
|
|
7291
7929
|
import http4 from "http";
|
|
7292
7930
|
import net3 from "net";
|
|
7293
|
-
import { basename as basename3, dirname as dirname10, extname as extname3, join as
|
|
7931
|
+
import { basename as basename3, dirname as dirname10, extname as extname3, join as join17, normalize as normalize4, relative as relative4 } from "path";
|
|
7294
7932
|
|
|
7295
7933
|
// ../server/dist/frame-policy.js
|
|
7296
7934
|
var FRAMING_TIMEOUT_MS = 4e3;
|
|
@@ -7403,8 +8041,8 @@ async function checkFraming(target, embedder, fetcher = fetch) {
|
|
|
7403
8041
|
|
|
7404
8042
|
// ../server/dist/generation/generation.js
|
|
7405
8043
|
import { spawn as nodeSpawn4 } from "child_process";
|
|
7406
|
-
import { readFile as
|
|
7407
|
-
import { dirname as dirname8, join as
|
|
8044
|
+
import { readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
8045
|
+
import { dirname as dirname8, join as join15, normalize as normalize3 } from "path";
|
|
7408
8046
|
|
|
7409
8047
|
// ../server/dist/generation/claude.js
|
|
7410
8048
|
var GENERATION_EFFORT = "medium";
|
|
@@ -7447,9 +8085,9 @@ function resultText(lines2) {
|
|
|
7447
8085
|
}
|
|
7448
8086
|
|
|
7449
8087
|
// ../server/dist/generation/codex.js
|
|
7450
|
-
import { readFile as
|
|
8088
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
7451
8089
|
import { homedir as homedir2 } from "os";
|
|
7452
|
-
import { join as
|
|
8090
|
+
import { join as join12 } from "path";
|
|
7453
8091
|
var RESTRICTED2 = [
|
|
7454
8092
|
"exec",
|
|
7455
8093
|
"--json",
|
|
@@ -7463,8 +8101,8 @@ var RESTRICTED2 = [
|
|
|
7463
8101
|
"skills.include_instructions=false",
|
|
7464
8102
|
"-c",
|
|
7465
8103
|
"skills.bundled.enabled=false",
|
|
7466
|
-
// Through the config, not `--disable`:
|
|
7467
|
-
//
|
|
8104
|
+
// Through the config, not `--disable`: an unknown feature is ignored here,
|
|
8105
|
+
// while `--disable` refuses to start.
|
|
7468
8106
|
...[
|
|
7469
8107
|
"multi_agent",
|
|
7470
8108
|
"apps",
|
|
@@ -7480,7 +8118,7 @@ var BARE = /^[A-Za-z0-9_-]+$/;
|
|
|
7480
8118
|
async function codexServers(home = defaultCodexHome()) {
|
|
7481
8119
|
let config;
|
|
7482
8120
|
try {
|
|
7483
|
-
config = await
|
|
8121
|
+
config = await readFile12(join12(home, "config.toml"), "utf8");
|
|
7484
8122
|
} catch {
|
|
7485
8123
|
return [];
|
|
7486
8124
|
}
|
|
@@ -7493,7 +8131,7 @@ async function codexServers(home = defaultCodexHome()) {
|
|
|
7493
8131
|
return [...names];
|
|
7494
8132
|
}
|
|
7495
8133
|
function defaultCodexHome(env = process.env) {
|
|
7496
|
-
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME :
|
|
8134
|
+
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME : join12(homedir2(), ".codex");
|
|
7497
8135
|
}
|
|
7498
8136
|
var serversOff = (servers) => servers.flatMap((name) => ["-c", `mcp_servers.${name}.enabled=false`]);
|
|
7499
8137
|
function codexPlanArgs(prompt, servers) {
|
|
@@ -7519,12 +8157,12 @@ function codexResultText(lines2) {
|
|
|
7519
8157
|
}
|
|
7520
8158
|
|
|
7521
8159
|
// ../server/dist/generation/facts.js
|
|
7522
|
-
import { readFile as
|
|
7523
|
-
import { dirname as dirname7, join as
|
|
8160
|
+
import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
|
|
8161
|
+
import { dirname as dirname7, join as join14, normalize as normalize2 } from "path";
|
|
7524
8162
|
|
|
7525
8163
|
// ../server/dist/generation/switch-file.js
|
|
7526
|
-
import { readFile as
|
|
7527
|
-
import { join as
|
|
8164
|
+
import { readFile as readFile13, readdir as readdir4, stat as stat4 } from "fs/promises";
|
|
8165
|
+
import { join as join13, relative as relative3 } from "path";
|
|
7528
8166
|
var MAP = /const DIRECTIONS = \{([\s\S]*?)\} as const;/;
|
|
7529
8167
|
var SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
7530
8168
|
var SCAN_LIMIT = 3e3;
|
|
@@ -7537,8 +8175,8 @@ async function isFile(path) {
|
|
|
7537
8175
|
}
|
|
7538
8176
|
async function findSwitch(cwd, slug) {
|
|
7539
8177
|
for (const name of ["switch.tsx", "switch.jsx"]) {
|
|
7540
|
-
const path =
|
|
7541
|
-
if (await isFile(
|
|
8178
|
+
const path = join13(".leglas", "variants", slug, name);
|
|
8179
|
+
if (await isFile(join13(cwd, path)))
|
|
7542
8180
|
return path;
|
|
7543
8181
|
}
|
|
7544
8182
|
const wanted = `"v-${slug}"`;
|
|
@@ -7546,14 +8184,14 @@ async function findSwitch(cwd, slug) {
|
|
|
7546
8184
|
const walk = async (folder) => {
|
|
7547
8185
|
let items;
|
|
7548
8186
|
try {
|
|
7549
|
-
items = await
|
|
8187
|
+
items = await readdir4(join13(cwd, folder), { withFileTypes: true });
|
|
7550
8188
|
} catch {
|
|
7551
8189
|
return null;
|
|
7552
8190
|
}
|
|
7553
8191
|
for (const item of items) {
|
|
7554
8192
|
if (seen >= SCAN_LIMIT || item.name.startsWith(".") || item.name === "node_modules")
|
|
7555
8193
|
continue;
|
|
7556
|
-
const path =
|
|
8194
|
+
const path = join13(folder, item.name);
|
|
7557
8195
|
if (item.isDirectory()) {
|
|
7558
8196
|
const found = await walk(path);
|
|
7559
8197
|
if (found !== null)
|
|
@@ -7563,7 +8201,7 @@ async function findSwitch(cwd, slug) {
|
|
|
7563
8201
|
if (!SOURCE.test(item.name))
|
|
7564
8202
|
continue;
|
|
7565
8203
|
seen += 1;
|
|
7566
|
-
const source = await
|
|
8204
|
+
const source = await readFile13(join13(cwd, path), "utf8").catch(() => "");
|
|
7567
8205
|
if (source.includes(wanted) && MAP.test(source))
|
|
7568
8206
|
return path;
|
|
7569
8207
|
}
|
|
@@ -7572,7 +8210,7 @@ async function findSwitch(cwd, slug) {
|
|
|
7572
8210
|
for (const root of ["src", "app"]) {
|
|
7573
8211
|
const found = await walk(root);
|
|
7574
8212
|
if (found !== null)
|
|
7575
|
-
return relative3(cwd,
|
|
8213
|
+
return relative3(cwd, join13(cwd, found));
|
|
7576
8214
|
}
|
|
7577
8215
|
return null;
|
|
7578
8216
|
}
|
|
@@ -7640,21 +8278,21 @@ function clip(source, limit) {
|
|
|
7640
8278
|
var BASELINE_LINES = 15;
|
|
7641
8279
|
var STUB = /Re-export what your|A first direction for/;
|
|
7642
8280
|
async function text2(path) {
|
|
7643
|
-
return
|
|
8281
|
+
return readFile14(path, "utf8").catch(() => null);
|
|
7644
8282
|
}
|
|
7645
8283
|
async function firstFile(cwd, candidates) {
|
|
7646
8284
|
for (const candidate of candidates) {
|
|
7647
|
-
if (await text2(
|
|
8285
|
+
if (await text2(join14(cwd, candidate)) !== null)
|
|
7648
8286
|
return candidate;
|
|
7649
8287
|
}
|
|
7650
8288
|
return null;
|
|
7651
8289
|
}
|
|
7652
8290
|
function cssImports(source, from) {
|
|
7653
|
-
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) =>
|
|
8291
|
+
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) => normalize2(join14(dirname7(from), match[1] ?? "")));
|
|
7654
8292
|
}
|
|
7655
8293
|
async function globalStylesheet(cwd) {
|
|
7656
8294
|
const entries = [];
|
|
7657
|
-
const html = await text2(
|
|
8295
|
+
const html = await text2(join14(cwd, "index.html"));
|
|
7658
8296
|
const script = html === null ? null : /<script[^>]+type="module"[^>]+src="\/?([^"]+)"/.exec(html);
|
|
7659
8297
|
if (script?.[1] !== void 0)
|
|
7660
8298
|
entries.push(script[1]);
|
|
@@ -7664,12 +8302,12 @@ async function globalStylesheet(cwd) {
|
|
|
7664
8302
|
}
|
|
7665
8303
|
const sheets = [];
|
|
7666
8304
|
for (const entry of entries) {
|
|
7667
|
-
const source = await text2(
|
|
8305
|
+
const source = await text2(join14(cwd, entry));
|
|
7668
8306
|
if (source !== null)
|
|
7669
8307
|
sheets.push(...cssImports(source, entry));
|
|
7670
8308
|
}
|
|
7671
8309
|
for (const sheet of sheets) {
|
|
7672
|
-
const source = await text2(
|
|
8310
|
+
const source = await text2(join14(cwd, sheet));
|
|
7673
8311
|
if (source !== null && /:root\s*\{|@font-face/.test(source))
|
|
7674
8312
|
return sheet;
|
|
7675
8313
|
}
|
|
@@ -7680,7 +8318,7 @@ async function images(cwd) {
|
|
|
7680
8318
|
const walk = async (folder) => {
|
|
7681
8319
|
let items;
|
|
7682
8320
|
try {
|
|
7683
|
-
items = await
|
|
8321
|
+
items = await readdir5(join14(cwd, "public", folder), { withFileTypes: true });
|
|
7684
8322
|
} catch {
|
|
7685
8323
|
return;
|
|
7686
8324
|
}
|
|
@@ -7698,14 +8336,14 @@ async function images(cwd) {
|
|
|
7698
8336
|
return found;
|
|
7699
8337
|
}
|
|
7700
8338
|
async function componentFile(cwd, from, specifier) {
|
|
7701
|
-
const base =
|
|
8339
|
+
const base = normalize2(join14(dirname7(from), specifier));
|
|
7702
8340
|
return firstFile(cwd, [
|
|
7703
8341
|
base,
|
|
7704
8342
|
...["tsx", "jsx", "ts", "js"].map((extension) => `${base}.${extension}`)
|
|
7705
8343
|
]);
|
|
7706
8344
|
}
|
|
7707
8345
|
async function example(cwd, switchPath, prefer) {
|
|
7708
|
-
const source = await text2(
|
|
8346
|
+
const source = await text2(join14(cwd, switchPath));
|
|
7709
8347
|
if (source === null)
|
|
7710
8348
|
return null;
|
|
7711
8349
|
const directions = readDirections(source);
|
|
@@ -7714,13 +8352,13 @@ async function example(cwd, switchPath, prefer) {
|
|
|
7714
8352
|
for (const direction of ordered) {
|
|
7715
8353
|
const limit = direction.key === prefer ? VARIED_LINES : EXAMPLE_LINES;
|
|
7716
8354
|
const path = await componentFile(cwd, switchPath, direction.from);
|
|
7717
|
-
const body = path === null ? null : await text2(
|
|
8355
|
+
const body = path === null ? null : await text2(join14(cwd, path));
|
|
7718
8356
|
if (path === null || body === null || body === placeholderSource(direction.name) || STUB.test(body))
|
|
7719
8357
|
continue;
|
|
7720
8358
|
if (body.split("\n").length < BASELINE_LINES) {
|
|
7721
8359
|
const target = /from\s+["'](\.[^"']+)["']/.exec(body)?.[1];
|
|
7722
8360
|
const followed = target === void 0 ? null : await componentFile(cwd, path, target);
|
|
7723
|
-
const real = followed === null ? null : await text2(
|
|
8361
|
+
const real = followed === null ? null : await text2(join14(cwd, followed));
|
|
7724
8362
|
if (followed !== null && real !== null) {
|
|
7725
8363
|
return {
|
|
7726
8364
|
key: direction.key,
|
|
@@ -7736,7 +8374,7 @@ async function example(cwd, switchPath, prefer) {
|
|
|
7736
8374
|
}
|
|
7737
8375
|
async function dependencyNames(cwd) {
|
|
7738
8376
|
try {
|
|
7739
|
-
const pkg = parseJson(await text2(
|
|
8377
|
+
const pkg = parseJson(await text2(join14(cwd, "package.json")) ?? "");
|
|
7740
8378
|
return isJsonRecord(pkg) && isJsonRecord(pkg.dependencies) ? Object.keys(pkg.dependencies) : [];
|
|
7741
8379
|
} catch {
|
|
7742
8380
|
return [];
|
|
@@ -7745,7 +8383,7 @@ async function dependencyNames(cwd) {
|
|
|
7745
8383
|
async function readProjectFacts(cwd, switchPath, prefer = null) {
|
|
7746
8384
|
const dependencies = await dependencyNames(cwd);
|
|
7747
8385
|
const sheetPath = await globalStylesheet(cwd);
|
|
7748
|
-
const css = sheetPath === null ? null : await text2(
|
|
8386
|
+
const css = sheetPath === null ? null : await text2(join14(cwd, sheetPath));
|
|
7749
8387
|
return {
|
|
7750
8388
|
dependencies,
|
|
7751
8389
|
stylesheet: sheetPath === null || css === null ? null : {
|
|
@@ -7822,7 +8460,13 @@ ${input.facts}
|
|
|
7822
8460
|
This is a first draft for someone comparing directions side by side, not a finished page. Aim for about 250 lines and write it in one pass: no second drafts, and no review, lint, tests or screenshots. Leglas renders and captures it for you. Your task ends when the file is written.`;
|
|
7823
8461
|
}
|
|
7824
8462
|
function fixPrompt(input) {
|
|
7825
|
-
|
|
8463
|
+
const layout = input.layout ?? [];
|
|
8464
|
+
const reported = `Leglas rendered ${input.file} and the page reported: ${input.errors.slice(0, 3).join(" | ")}.`;
|
|
8465
|
+
if (layout.length === 0) {
|
|
8466
|
+
return `${reported} Fix what your file causes, in as few edits as you can, and change nothing else. If nothing in your file causes it, change nothing. Do not look around the project.`;
|
|
8467
|
+
}
|
|
8468
|
+
const opening = input.errors.length === 0 ? `Leglas rendered ${input.file}.` : reported;
|
|
8469
|
+
return `${opening} At 1440 by 900, text a person would see runs into other text or is cut off: ${layout.slice(0, 5).join("; ")}. Fix it where it happens: move, wrap or re-set that text, or nudge what crowds it. Keep the design's scale and composition, and never shrink the design or its type to make room. A crop or an overlap of shapes and pictures can stay. If nothing in your file causes it, change nothing. Do not look around the project.`;
|
|
7826
8470
|
}
|
|
7827
8471
|
function kebab(value) {
|
|
7828
8472
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -7873,6 +8517,16 @@ var OWN_FAILURES = /* @__PURE__ */ new Set([
|
|
|
7873
8517
|
]);
|
|
7874
8518
|
var BUSY = "A set of directions is already being built. Wait for it, or stop it first.";
|
|
7875
8519
|
var KEPT_JOBS = 5;
|
|
8520
|
+
var guardKey = (slot, attempt) => `${slot.key}#${attempt}`;
|
|
8521
|
+
function listed(items) {
|
|
8522
|
+
return items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
8523
|
+
}
|
|
8524
|
+
function strayMessage(paths, restored, named) {
|
|
8525
|
+
const which = paths.length === 1 ? "which is not its own file" : "which are not its own files";
|
|
8526
|
+
const back = restored.length === 0 ? "" : restored.length === paths.length ? ` and put ${paths.length === 1 ? "the file" : "them"} back` : ` and put ${listed(restored)} back`;
|
|
8527
|
+
const check = named.length === 0 ? "." : `. Check ${listed(named)} before trying again.`;
|
|
8528
|
+
return `The build edited ${listed(paths)}, ${which}, so Leglas stopped it${back}${check}`;
|
|
8529
|
+
}
|
|
7876
8530
|
var CLAUDE = {
|
|
7877
8531
|
id: "claude",
|
|
7878
8532
|
name: "Claude",
|
|
@@ -7889,6 +8543,10 @@ function codex(servers) {
|
|
|
7889
8543
|
result: codexResultText
|
|
7890
8544
|
};
|
|
7891
8545
|
}
|
|
8546
|
+
function argsOf(agent) {
|
|
8547
|
+
const mark = "\0prompt";
|
|
8548
|
+
return agent.build(mark).map((arg) => arg === mark ? "<prompt>" : arg);
|
|
8549
|
+
}
|
|
7892
8550
|
function wait(milliseconds) {
|
|
7893
8551
|
return new Promise((resolve5) => {
|
|
7894
8552
|
setTimeout(resolve5, milliseconds).unref?.();
|
|
@@ -7909,7 +8567,7 @@ function baseOf(preview, surface) {
|
|
|
7909
8567
|
async function stackOf(cwd, switchPath) {
|
|
7910
8568
|
let deps = {};
|
|
7911
8569
|
try {
|
|
7912
|
-
const pkg = parseJson(await
|
|
8570
|
+
const pkg = parseJson(await readFile15(join15(cwd, "package.json"), "utf8"));
|
|
7913
8571
|
if (isJsonRecord(pkg)) {
|
|
7914
8572
|
for (const group of [pkg.devDependencies, pkg.dependencies]) {
|
|
7915
8573
|
if (!isJsonRecord(group))
|
|
@@ -7957,6 +8615,81 @@ function createGenerations(deps) {
|
|
|
7957
8615
|
let closed = false;
|
|
7958
8616
|
const busy = () => lives.some((live) => live.job.state === "planning" || live.job.state === "building");
|
|
7959
8617
|
const changed = () => deps.onChange();
|
|
8618
|
+
const records = createRecords(deps.cwd);
|
|
8619
|
+
const runRecord = (prompt) => ({
|
|
8620
|
+
prompt,
|
|
8621
|
+
startedAt: now(),
|
|
8622
|
+
endedAt: null,
|
|
8623
|
+
exitCode: null,
|
|
8624
|
+
timedOut: false,
|
|
8625
|
+
error: null,
|
|
8626
|
+
result: null,
|
|
8627
|
+
tail: []
|
|
8628
|
+
});
|
|
8629
|
+
const ended = (agent, record3, outcome) => {
|
|
8630
|
+
const failed = outcome.timedOut || outcome.error !== null || outcome.code !== 0;
|
|
8631
|
+
record3.endedAt = now();
|
|
8632
|
+
record3.exitCode = outcome.code;
|
|
8633
|
+
record3.timedOut = outcome.timedOut;
|
|
8634
|
+
record3.error = outcome.error;
|
|
8635
|
+
record3.result = agent.result(outcome.lines);
|
|
8636
|
+
record3.tail = failed ? outcome.lines.slice(-40) : [];
|
|
8637
|
+
};
|
|
8638
|
+
const attemptRecord = (live, key, attempt) => live.record?.directions.find((direction) => direction.key === key)?.attempts.find((each) => each.attempt === attempt);
|
|
8639
|
+
const opened = (live, slot, attempt, cause) => {
|
|
8640
|
+
live.record?.directions.find((direction) => direction.key === slot.key)?.attempts.push({
|
|
8641
|
+
attempt,
|
|
8642
|
+
cause,
|
|
8643
|
+
title: slot.title,
|
|
8644
|
+
idea: slot.idea,
|
|
8645
|
+
ideaRun: null,
|
|
8646
|
+
build: null,
|
|
8647
|
+
renders: [],
|
|
8648
|
+
fix: null,
|
|
8649
|
+
state: slot.state,
|
|
8650
|
+
failure: null,
|
|
8651
|
+
fixed: false,
|
|
8652
|
+
layout: [],
|
|
8653
|
+
picture: null
|
|
8654
|
+
});
|
|
8655
|
+
};
|
|
8656
|
+
const saveRecord = (live) => {
|
|
8657
|
+
const { job, record: record3 } = live;
|
|
8658
|
+
if (record3 === null)
|
|
8659
|
+
return;
|
|
8660
|
+
record3.state = job.state;
|
|
8661
|
+
record3.error = job.error;
|
|
8662
|
+
record3.plannedAt = job.plannedAt;
|
|
8663
|
+
record3.endedAt = job.endedAt;
|
|
8664
|
+
for (const slot of job.slots) {
|
|
8665
|
+
const latest = record3.directions.find((each) => each.key === slot.key)?.attempts.at(-1);
|
|
8666
|
+
if (latest === void 0)
|
|
8667
|
+
continue;
|
|
8668
|
+
latest.title = slot.title;
|
|
8669
|
+
latest.idea = slot.idea;
|
|
8670
|
+
latest.state = slot.state;
|
|
8671
|
+
latest.failure = slot.failure === null ? null : { ...slot.failure };
|
|
8672
|
+
latest.fixed = slot.fixed;
|
|
8673
|
+
latest.layout = [...slot.layout];
|
|
8674
|
+
}
|
|
8675
|
+
records.save(record3);
|
|
8676
|
+
};
|
|
8677
|
+
const noted = (live, kind, direction) => {
|
|
8678
|
+
if (live.record === null)
|
|
8679
|
+
return;
|
|
8680
|
+
records.event(live.record.id, { at: now(), kind, direction });
|
|
8681
|
+
saveRecord(live);
|
|
8682
|
+
};
|
|
8683
|
+
const rendered = (live, slot, attempt, report) => {
|
|
8684
|
+
const record3 = attemptRecord(live, slot.key, attempt);
|
|
8685
|
+
if (live.record === null || record3 === void 0)
|
|
8686
|
+
return;
|
|
8687
|
+
record3.renders.push({ at: now(), errors: [...report.errors], layout: [...report.layout] });
|
|
8688
|
+
if (report.frame === null)
|
|
8689
|
+
return;
|
|
8690
|
+
record3.picture = `${slot.key}-${attempt}.jpg`;
|
|
8691
|
+
records.picture(live.record.id, record3.picture, report.frame);
|
|
8692
|
+
};
|
|
7960
8693
|
let registry = Promise.resolve();
|
|
7961
8694
|
const serially = (task) => {
|
|
7962
8695
|
const next = registry.then(task, task);
|
|
@@ -8017,8 +8750,10 @@ function createGenerations(deps) {
|
|
|
8017
8750
|
});
|
|
8018
8751
|
child.once("close", (code) => {
|
|
8019
8752
|
clearTimeout(deadline);
|
|
8020
|
-
if (buffered !== "")
|
|
8753
|
+
if (buffered !== "") {
|
|
8021
8754
|
lines2.push(buffered);
|
|
8755
|
+
onLine?.(buffered);
|
|
8756
|
+
}
|
|
8022
8757
|
resolve5({ code, lines: lines2, error: null, timedOut });
|
|
8023
8758
|
});
|
|
8024
8759
|
});
|
|
@@ -8061,7 +8796,7 @@ function createGenerations(deps) {
|
|
|
8061
8796
|
job.endedAt = now();
|
|
8062
8797
|
};
|
|
8063
8798
|
const restore = async (slot) => {
|
|
8064
|
-
await
|
|
8799
|
+
await writeFile8(join15(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), "utf8").catch(() => {
|
|
8065
8800
|
});
|
|
8066
8801
|
};
|
|
8067
8802
|
const begin = (live, slot) => {
|
|
@@ -8118,8 +8853,85 @@ function createGenerations(deps) {
|
|
|
8118
8853
|
fail(live, slot, { code: "unexpected", message: message2 });
|
|
8119
8854
|
settle(live);
|
|
8120
8855
|
changed();
|
|
8856
|
+
saveRecord(live);
|
|
8857
|
+
};
|
|
8858
|
+
const strayed = async (live, slot, attempt) => {
|
|
8859
|
+
const guard = live.guards.get(guardKey(slot, attempt));
|
|
8860
|
+
if (guard === void 0 || guard.strays.size === 0)
|
|
8861
|
+
return null;
|
|
8862
|
+
const paths = [...guard.strays];
|
|
8863
|
+
const restored = [];
|
|
8864
|
+
const named = [];
|
|
8865
|
+
const building = new Set(lives.flatMap((other) => other.job.slots.flatMap((each) => each.state === "building" || each.state === "checking" ? [normalize3(each.file)] : [])));
|
|
8866
|
+
for (const path of paths) {
|
|
8867
|
+
const kept = guard.kept.get(path);
|
|
8868
|
+
if (kept === void 0 || building.has(path)) {
|
|
8869
|
+
named.push(path);
|
|
8870
|
+
continue;
|
|
8871
|
+
}
|
|
8872
|
+
const back = await writeFile8(join15(deps.cwd, path), kept, "utf8").then(() => true, () => false);
|
|
8873
|
+
(back ? restored : named).push(path);
|
|
8874
|
+
}
|
|
8875
|
+
guard.strays.clear();
|
|
8876
|
+
return {
|
|
8877
|
+
failure: { code: "outside-file", message: strayMessage(paths, restored, named) },
|
|
8878
|
+
putBack: named.length === 0
|
|
8879
|
+
};
|
|
8880
|
+
};
|
|
8881
|
+
const pendingStrays = (except) => {
|
|
8882
|
+
const pending = /* @__PURE__ */ new Map();
|
|
8883
|
+
for (const [key, guard] of lives.flatMap((other) => [...other.guards.entries()])) {
|
|
8884
|
+
if (except !== void 0 && key.startsWith(`${except.key}#`))
|
|
8885
|
+
continue;
|
|
8886
|
+
for (const path of guard.strays) {
|
|
8887
|
+
const kept = guard.kept.get(path);
|
|
8888
|
+
if (kept !== void 0)
|
|
8889
|
+
pending.set(path, kept);
|
|
8890
|
+
}
|
|
8891
|
+
}
|
|
8892
|
+
return pending;
|
|
8893
|
+
};
|
|
8894
|
+
const keptFiles = async (live, own = new Set(live.job.slots.map((slot) => normalize3(slot.file)))) => {
|
|
8895
|
+
const switchPath = live.switchPath;
|
|
8896
|
+
const pending = pendingStrays();
|
|
8897
|
+
const read = async (path) => pending.get(normalize3(path)) ?? await readFile15(join15(deps.cwd, path), "utf8").catch(() => null);
|
|
8898
|
+
const source = await read(switchPath);
|
|
8899
|
+
const kept = /* @__PURE__ */ new Map();
|
|
8900
|
+
if (source === null)
|
|
8901
|
+
return kept;
|
|
8902
|
+
kept.set(normalize3(switchPath), source);
|
|
8903
|
+
const keep = async (path) => {
|
|
8904
|
+
if (path === null || own.has(normalize3(path)) || kept.has(normalize3(path)))
|
|
8905
|
+
return null;
|
|
8906
|
+
const body = await read(path);
|
|
8907
|
+
if (body !== null)
|
|
8908
|
+
kept.set(normalize3(path), body);
|
|
8909
|
+
return body;
|
|
8910
|
+
};
|
|
8911
|
+
for (const direction of readDirections(source)) {
|
|
8912
|
+
const path = await componentFile(deps.cwd, switchPath, direction.from);
|
|
8913
|
+
const body = await keep(path);
|
|
8914
|
+
if (path === null || body === null)
|
|
8915
|
+
continue;
|
|
8916
|
+
for (const match of body.matchAll(/(?:from\s+|import\s*\(?\s*)["'](\.{1,2}\/[^"']+)["']/g)) {
|
|
8917
|
+
await keep(await componentFile(deps.cwd, path, match[1] ?? ""));
|
|
8918
|
+
}
|
|
8919
|
+
}
|
|
8920
|
+
return kept;
|
|
8121
8921
|
};
|
|
8122
8922
|
const follow = (live, slot, attempt) => (line) => {
|
|
8923
|
+
const guard = live.guards.get(guardKey(slot, attempt));
|
|
8924
|
+
for (const path of editedFiles(live.agent.id, line, deps.cwd)) {
|
|
8925
|
+
if (guard === void 0 || path === normalize3(slot.file) || guard.strays.has(path))
|
|
8926
|
+
continue;
|
|
8927
|
+
guard.strays.add(path);
|
|
8928
|
+
if (guard.strays.size > 1 || !owns(live, slot, attempt))
|
|
8929
|
+
continue;
|
|
8930
|
+
const running = live.runs.get(slot.key);
|
|
8931
|
+
if (running !== void 0)
|
|
8932
|
+
detach(live, running.stop(), () => {
|
|
8933
|
+
});
|
|
8934
|
+
}
|
|
8123
8935
|
const activity = activityFrom(live.agent.id, line, deps.cwd);
|
|
8124
8936
|
if (activity === null || activity === slot.activity || !owns(live, slot, attempt))
|
|
8125
8937
|
return;
|
|
@@ -8136,8 +8948,21 @@ function createGenerations(deps) {
|
|
|
8136
8948
|
slot.endedAt = null;
|
|
8137
8949
|
slot.failure = null;
|
|
8138
8950
|
slot.fixed = false;
|
|
8951
|
+
slot.layout = [];
|
|
8139
8952
|
slot.activity = null;
|
|
8140
8953
|
changed();
|
|
8954
|
+
const key = guardKey(slot, attempt);
|
|
8955
|
+
if (!live.guards.has(key))
|
|
8956
|
+
live.guards.set(key, { kept: await keptFiles(live), strays: /* @__PURE__ */ new Set() });
|
|
8957
|
+
try {
|
|
8958
|
+
if (!owns(live, slot, attempt))
|
|
8959
|
+
return;
|
|
8960
|
+
await buildOwned(live, slot, attempt, concept);
|
|
8961
|
+
} finally {
|
|
8962
|
+
live.guards.delete(key);
|
|
8963
|
+
}
|
|
8964
|
+
};
|
|
8965
|
+
const buildOwned = async (live, slot, attempt, concept) => {
|
|
8141
8966
|
const others = [...live.concepts.values()].filter((other) => other !== concept);
|
|
8142
8967
|
const prompt = buildPrompt({
|
|
8143
8968
|
surface: live.job.surface,
|
|
@@ -8150,17 +8975,26 @@ function createGenerations(deps) {
|
|
|
8150
8975
|
facts: live.facts ?? "",
|
|
8151
8976
|
base: live.base
|
|
8152
8977
|
});
|
|
8978
|
+
const buildRun = runRecord(prompt);
|
|
8979
|
+
const own = attemptRecord(live, slot.key, attempt);
|
|
8980
|
+
if (own !== void 0)
|
|
8981
|
+
own.build = buildRun;
|
|
8153
8982
|
const building = run4(live.agent, live.agent.build(prompt), BUILD_DEADLINE_MS, follow(live, slot, attempt));
|
|
8154
8983
|
live.runs.set(slot.key, building);
|
|
8155
8984
|
const outcome = await building.done;
|
|
8985
|
+
ended(live.agent, buildRun, outcome);
|
|
8986
|
+
saveRecord(live);
|
|
8987
|
+
const outside = await strayed(live, slot, attempt);
|
|
8156
8988
|
if (!owns(live, slot, attempt))
|
|
8157
8989
|
return;
|
|
8158
8990
|
if (live.runs.get(slot.key) === building)
|
|
8159
8991
|
live.runs.delete(slot.key);
|
|
8160
|
-
const written = await
|
|
8992
|
+
const written = await readFile15(join15(deps.cwd, slot.file), "utf8").catch(() => "");
|
|
8161
8993
|
if (!owns(live, slot, attempt))
|
|
8162
8994
|
return;
|
|
8163
|
-
if (
|
|
8995
|
+
if (outside !== null) {
|
|
8996
|
+
fail(live, slot, outside.failure);
|
|
8997
|
+
} else if (outcome.timedOut) {
|
|
8164
8998
|
fail(live, slot, {
|
|
8165
8999
|
code: "too-slow",
|
|
8166
9000
|
message: `The build took longer than ${BUILD_DEADLINE_MS / 6e4} minutes, so Leglas stopped it.`
|
|
@@ -8180,19 +9014,25 @@ function createGenerations(deps) {
|
|
|
8180
9014
|
}
|
|
8181
9015
|
settle(live);
|
|
8182
9016
|
changed();
|
|
9017
|
+
saveRecord(live);
|
|
8183
9018
|
};
|
|
8184
|
-
const
|
|
9019
|
+
const ownReport = async (live, slot, attempt) => {
|
|
8185
9020
|
const others = live.job.slots.filter((other) => other !== slot);
|
|
8186
9021
|
const until = now() + BUILD_DEADLINE_MS + FIX_DEADLINE_MS;
|
|
8187
9022
|
for (; ; ) {
|
|
8188
|
-
const report = await deps.render(slot.title);
|
|
9023
|
+
const report = await deps.render(slot.title, { picture: live.record !== null });
|
|
8189
9024
|
if (!owns(live, slot, attempt))
|
|
8190
9025
|
return null;
|
|
8191
9026
|
const errors = report?.errors ?? [];
|
|
8192
9027
|
const names = (file) => errors.some((error) => error.includes(file));
|
|
8193
|
-
const
|
|
9028
|
+
const pending = [...pendingStrays(slot).keys()];
|
|
9029
|
+
const waiting = !names(slot.file) && (pending.some(names) || others.some((other) => (other.state === "building" || other.state === "checking") && names(other.file)));
|
|
8194
9030
|
if (!waiting || now() >= until) {
|
|
8195
|
-
return
|
|
9031
|
+
return {
|
|
9032
|
+
errors: errors.filter((error) => !others.some((other) => error.includes(other.file))),
|
|
9033
|
+
layout: [...report?.layout ?? []],
|
|
9034
|
+
frame: report?.frame ?? null
|
|
9035
|
+
};
|
|
8196
9036
|
}
|
|
8197
9037
|
await wait(RECHECK_MS);
|
|
8198
9038
|
if (!owns(live, slot, attempt))
|
|
@@ -8200,35 +9040,67 @@ function createGenerations(deps) {
|
|
|
8200
9040
|
}
|
|
8201
9041
|
};
|
|
8202
9042
|
const check = async (live, slot, attempt) => {
|
|
8203
|
-
let
|
|
8204
|
-
if (
|
|
9043
|
+
let report = await ownReport(live, slot, attempt);
|
|
9044
|
+
if (report === null)
|
|
8205
9045
|
return;
|
|
8206
|
-
|
|
8207
|
-
|
|
9046
|
+
rendered(live, slot, attempt, report);
|
|
9047
|
+
if (report.errors.length > 0 || report.layout.length > 0) {
|
|
9048
|
+
const repairs = report.errors.length > 0;
|
|
9049
|
+
const found = report.layout;
|
|
9050
|
+
const working = repairs ? null : await readFile15(join15(deps.cwd, slot.file), "utf8").catch(() => null);
|
|
9051
|
+
if (!owns(live, slot, attempt))
|
|
9052
|
+
return;
|
|
9053
|
+
const prompt = fixPrompt({ file: slot.file, errors: report.errors, layout: report.layout });
|
|
9054
|
+
const fixRun = runRecord(prompt);
|
|
9055
|
+
const own = attemptRecord(live, slot.key, attempt);
|
|
9056
|
+
if (own !== void 0)
|
|
9057
|
+
own.fix = fixRun;
|
|
9058
|
+
const fixing = run4(live.agent, live.agent.build(prompt), FIX_DEADLINE_MS, follow(live, slot, attempt));
|
|
8208
9059
|
live.runs.set(slot.key, fixing);
|
|
8209
9060
|
const outcome = await fixing.done;
|
|
9061
|
+
ended(live.agent, fixRun, outcome);
|
|
9062
|
+
const outside = await strayed(live, slot, attempt);
|
|
8210
9063
|
if (!owns(live, slot, attempt))
|
|
8211
9064
|
return;
|
|
8212
9065
|
slot.activity = null;
|
|
8213
9066
|
changed();
|
|
8214
9067
|
if (live.runs.get(slot.key) === fixing)
|
|
8215
9068
|
live.runs.delete(slot.key);
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
9069
|
+
let failure = outside?.failure ?? (outcome.timedOut ? { code: "too-slow", message: "The fix took too long, so Leglas stopped it." } : outcome.error !== null || outcome.code !== 0 ? failureOf2(live.agent, outcome) : null);
|
|
9070
|
+
if (failure === null) {
|
|
9071
|
+
slot.fixed = repairs;
|
|
9072
|
+
report = await ownReport(live, slot, attempt);
|
|
9073
|
+
if (report === null)
|
|
9074
|
+
return;
|
|
9075
|
+
rendered(live, slot, attempt, report);
|
|
9076
|
+
if (report.errors.length > 0) {
|
|
9077
|
+
failure = {
|
|
9078
|
+
code: "broken",
|
|
9079
|
+
message: `The page still reports: ${report.errors[0] ?? ""}`
|
|
9080
|
+
};
|
|
9081
|
+
}
|
|
8219
9082
|
}
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
9083
|
+
const worse = failure === null && !repairs && report.layout.length > found.length;
|
|
9084
|
+
if (failure !== null || worse) {
|
|
9085
|
+
if (working !== null && outside?.putBack !== false) {
|
|
9086
|
+
const restored = await writeFile8(join15(deps.cwd, slot.file), working, "utf8").then(() => true, () => false);
|
|
9087
|
+
if (!owns(live, slot, attempt))
|
|
9088
|
+
return;
|
|
9089
|
+
if (restored) {
|
|
9090
|
+
slot.fixed = false;
|
|
9091
|
+
slot.layout = found;
|
|
9092
|
+
slot.state = "ready";
|
|
9093
|
+
slot.endedAt = now();
|
|
9094
|
+
return;
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
if (failure !== null) {
|
|
9098
|
+
fail(live, slot, failure);
|
|
9099
|
+
return;
|
|
9100
|
+
}
|
|
8230
9101
|
}
|
|
8231
9102
|
}
|
|
9103
|
+
slot.layout = report.layout;
|
|
8232
9104
|
slot.state = "ready";
|
|
8233
9105
|
slot.endedAt = now();
|
|
8234
9106
|
};
|
|
@@ -8241,16 +9113,22 @@ function createGenerations(deps) {
|
|
|
8241
9113
|
};
|
|
8242
9114
|
const plan = async (live, existing) => {
|
|
8243
9115
|
const { job } = live;
|
|
8244
|
-
const
|
|
9116
|
+
const prompt = planPrompt({
|
|
8245
9117
|
surface: job.surface,
|
|
8246
9118
|
brief: job.brief,
|
|
8247
9119
|
count: job.count,
|
|
8248
9120
|
existing,
|
|
8249
9121
|
base: live.base
|
|
8250
|
-
})
|
|
9122
|
+
});
|
|
9123
|
+
const planRun = { ...runRecord(prompt), concepts: [] };
|
|
9124
|
+
if (live.record !== null)
|
|
9125
|
+
live.record.plan = planRun;
|
|
9126
|
+
const planning = run4(live.agent, live.agent.plan(prompt), PLAN_DEADLINE_MS);
|
|
8251
9127
|
live.plan = planning;
|
|
8252
9128
|
const outcome = await planning.done;
|
|
8253
9129
|
live.plan = null;
|
|
9130
|
+
ended(live.agent, planRun, outcome);
|
|
9131
|
+
saveRecord(live);
|
|
8254
9132
|
if (job.state !== "planning")
|
|
8255
9133
|
return;
|
|
8256
9134
|
if (outcome.timedOut || outcome.error !== null || outcome.code !== 0) {
|
|
@@ -8258,6 +9136,7 @@ function createGenerations(deps) {
|
|
|
8258
9136
|
job.error = outcome.timedOut ? "Planning took too long, so Leglas stopped it." : failureOf2(live.agent, outcome).message;
|
|
8259
9137
|
job.endedAt = now();
|
|
8260
9138
|
changed();
|
|
9139
|
+
saveRecord(live);
|
|
8261
9140
|
return;
|
|
8262
9141
|
}
|
|
8263
9142
|
let concepts;
|
|
@@ -8268,8 +9147,10 @@ function createGenerations(deps) {
|
|
|
8268
9147
|
job.error = error instanceof Error ? error.message : String(error);
|
|
8269
9148
|
job.endedAt = now();
|
|
8270
9149
|
changed();
|
|
9150
|
+
saveRecord(live);
|
|
8271
9151
|
return;
|
|
8272
9152
|
}
|
|
9153
|
+
planRun.concepts = concepts;
|
|
8273
9154
|
live.facts = factsBlock(await readProjectFacts(deps.cwd, live.switchPath, live.base?.key ?? null), job.surface, live.base);
|
|
8274
9155
|
const slug = surfaceSlug2(job.surface);
|
|
8275
9156
|
const keys = new Set(existing);
|
|
@@ -8277,7 +9158,7 @@ function createGenerations(deps) {
|
|
|
8277
9158
|
const folder = dirname8(live.switchPath);
|
|
8278
9159
|
const slots = [];
|
|
8279
9160
|
const extension = live.switchPath.endsWith(".jsx") ? "jsx" : "tsx";
|
|
8280
|
-
const taken = async (key) => keys.has(key) || await isFile(
|
|
9161
|
+
const taken = async (key) => keys.has(key) || await isFile(join15(deps.cwd, folder, `${key}.${extension}`));
|
|
8281
9162
|
for (const concept of concepts) {
|
|
8282
9163
|
let key = `${slug}-${concept.key}`;
|
|
8283
9164
|
for (let suffix = 2; await taken(key); suffix += 1)
|
|
@@ -8289,23 +9170,25 @@ function createGenerations(deps) {
|
|
|
8289
9170
|
key,
|
|
8290
9171
|
title,
|
|
8291
9172
|
idea: concept.idea,
|
|
8292
|
-
file:
|
|
9173
|
+
file: join15(folder, `${key}.${extension}`),
|
|
8293
9174
|
state: "building",
|
|
8294
9175
|
startedAt: null,
|
|
8295
9176
|
endedAt: null,
|
|
8296
9177
|
failure: null,
|
|
8297
9178
|
fixed: false,
|
|
9179
|
+
layout: [],
|
|
8298
9180
|
activity: null
|
|
8299
9181
|
});
|
|
8300
9182
|
}
|
|
8301
|
-
const source = await
|
|
9183
|
+
const source = await readFile15(join15(deps.cwd, live.switchPath), "utf8");
|
|
8302
9184
|
for (const slot of slots) {
|
|
8303
|
-
await
|
|
9185
|
+
await writeFile8(join15(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), {
|
|
8304
9186
|
encoding: "utf8",
|
|
8305
9187
|
flag: "wx"
|
|
8306
9188
|
});
|
|
8307
9189
|
}
|
|
8308
|
-
await
|
|
9190
|
+
await writeFile8(join15(deps.cwd, live.switchPath), addSlots(source, slots.map((slot) => ({ key: slot.key, name: pascal(slot.key) }))), "utf8");
|
|
9191
|
+
const kept = await keptFiles(live, new Set(slots.map((slot) => normalize3(slot.file))));
|
|
8309
9192
|
for (const slot of slots) {
|
|
8310
9193
|
await serially(() => deps.register({
|
|
8311
9194
|
title: slot.title,
|
|
@@ -8315,21 +9198,33 @@ function createGenerations(deps) {
|
|
|
8315
9198
|
}));
|
|
8316
9199
|
}
|
|
8317
9200
|
job.slots = slots;
|
|
9201
|
+
if (live.record !== null) {
|
|
9202
|
+
live.record.directions = slots.map((slot) => ({
|
|
9203
|
+
key: slot.key,
|
|
9204
|
+
file: slot.file,
|
|
9205
|
+
attempts: []
|
|
9206
|
+
}));
|
|
9207
|
+
}
|
|
8318
9208
|
if (job.state !== "planning") {
|
|
8319
9209
|
for (const slot of slots) {
|
|
8320
9210
|
slot.state = "stopped";
|
|
8321
9211
|
slot.endedAt = now();
|
|
8322
9212
|
}
|
|
8323
9213
|
changed();
|
|
9214
|
+
saveRecord(live);
|
|
8324
9215
|
return;
|
|
8325
9216
|
}
|
|
8326
9217
|
job.state = "building";
|
|
8327
9218
|
job.plannedAt = now();
|
|
8328
9219
|
changed();
|
|
8329
|
-
|
|
9220
|
+
const builds = job.slots.map((slot) => {
|
|
8330
9221
|
const attempt = begin(live, slot);
|
|
9222
|
+
live.guards.set(guardKey(slot, attempt), { kept, strays: /* @__PURE__ */ new Set() });
|
|
9223
|
+
opened(live, slot, attempt, "plan");
|
|
8331
9224
|
return guarded(build(live, slot, attempt), (message2) => lost(live, slot, attempt, message2));
|
|
8332
|
-
})
|
|
9225
|
+
});
|
|
9226
|
+
saveRecord(live);
|
|
9227
|
+
await Promise.all(builds);
|
|
8333
9228
|
};
|
|
8334
9229
|
const find = (id) => lives.find((live) => live.job.id === id);
|
|
8335
9230
|
const stop = async (id, key) => {
|
|
@@ -8341,6 +9236,7 @@ function createGenerations(deps) {
|
|
|
8341
9236
|
job.state = "stopped";
|
|
8342
9237
|
job.endedAt = now();
|
|
8343
9238
|
changed();
|
|
9239
|
+
noted(live, "stop", null);
|
|
8344
9240
|
await live.plan?.stop();
|
|
8345
9241
|
await drain(live);
|
|
8346
9242
|
return true;
|
|
@@ -8353,6 +9249,8 @@ function createGenerations(deps) {
|
|
|
8353
9249
|
}
|
|
8354
9250
|
settle(live);
|
|
8355
9251
|
changed();
|
|
9252
|
+
if (targets.length > 0)
|
|
9253
|
+
noted(live, "stop", key ?? null);
|
|
8356
9254
|
await Promise.all(targets.map((slot) => release(live, slot)));
|
|
8357
9255
|
return targets.length > 0;
|
|
8358
9256
|
};
|
|
@@ -8387,7 +9285,7 @@ function createGenerations(deps) {
|
|
|
8387
9285
|
error: `The ${slug} has no switch file yet. Run \`leglas new ${slug} --from <your component>\` and render ${pascal(slug)}Switch where the ${slug} is, then build directions into it.`
|
|
8388
9286
|
};
|
|
8389
9287
|
}
|
|
8390
|
-
const existing = directionKeys(await
|
|
9288
|
+
const existing = directionKeys(await readFile15(join15(deps.cwd, switchPath), "utf8"));
|
|
8391
9289
|
if (base !== null && !existing.includes(base.key)) {
|
|
8392
9290
|
return {
|
|
8393
9291
|
ok: false,
|
|
@@ -8426,19 +9324,54 @@ function createGenerations(deps) {
|
|
|
8426
9324
|
attempts: /* @__PURE__ */ new Map(),
|
|
8427
9325
|
base,
|
|
8428
9326
|
agent: agent === "codex" ? codex(await codexServers(deps.codexHome)) : CLAUDE,
|
|
8429
|
-
|
|
9327
|
+
guards: /* @__PURE__ */ new Map(),
|
|
9328
|
+
pending: /* @__PURE__ */ new Set(),
|
|
9329
|
+
record: null
|
|
8430
9330
|
};
|
|
9331
|
+
const recording = deps.record?.() !== false;
|
|
9332
|
+
if (recording)
|
|
9333
|
+
await records.prune();
|
|
9334
|
+
const baseSet = base === null || !recording ? null : await setOfKey(deps.cwd, base.key).catch(() => null);
|
|
8431
9335
|
if (busy())
|
|
8432
9336
|
return { ok: false, error: BUSY };
|
|
8433
9337
|
lives.push(live);
|
|
8434
9338
|
while (lives.length > KEPT_JOBS)
|
|
8435
9339
|
lives.shift();
|
|
8436
9340
|
changed();
|
|
9341
|
+
if (recording) {
|
|
9342
|
+
live.record = {
|
|
9343
|
+
version: 1,
|
|
9344
|
+
id: job.id,
|
|
9345
|
+
surface: slug,
|
|
9346
|
+
brief,
|
|
9347
|
+
count,
|
|
9348
|
+
agent,
|
|
9349
|
+
args: argsOf(live.agent),
|
|
9350
|
+
basedOn: base === null ? null : { title: base.title, key: base.key, set: baseSet },
|
|
9351
|
+
state: job.state,
|
|
9352
|
+
error: null,
|
|
9353
|
+
startedAt: job.startedAt,
|
|
9354
|
+
plannedAt: null,
|
|
9355
|
+
endedAt: null,
|
|
9356
|
+
plan: null,
|
|
9357
|
+
directions: []
|
|
9358
|
+
};
|
|
9359
|
+
saveRecord(live);
|
|
9360
|
+
if (base !== null && baseSet !== null) {
|
|
9361
|
+
records.event(baseSet, {
|
|
9362
|
+
at: now(),
|
|
9363
|
+
kind: "more-like",
|
|
9364
|
+
direction: base.key,
|
|
9365
|
+
detail: job.id
|
|
9366
|
+
});
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
8437
9369
|
detach(live, plan(live, existing), (message2) => {
|
|
8438
9370
|
job.state = "failed";
|
|
8439
9371
|
job.error = message2;
|
|
8440
9372
|
job.endedAt = now();
|
|
8441
9373
|
changed();
|
|
9374
|
+
saveRecord(live);
|
|
8442
9375
|
});
|
|
8443
9376
|
return { ok: true, job: copy(job) };
|
|
8444
9377
|
},
|
|
@@ -8453,10 +9386,12 @@ function createGenerations(deps) {
|
|
|
8453
9386
|
slot.state = "building";
|
|
8454
9387
|
slot.startedAt = now();
|
|
8455
9388
|
slot.failure = null;
|
|
9389
|
+
opened(live, slot, attempt, "retry");
|
|
8456
9390
|
detach(live, restore(slot).then(() => build(live, slot, attempt)), (message2) => lost(live, slot, attempt, message2));
|
|
8457
9391
|
live.job.state = "building";
|
|
8458
9392
|
live.job.endedAt = null;
|
|
8459
9393
|
changed();
|
|
9394
|
+
noted(live, "retry", slot.key);
|
|
8460
9395
|
return true;
|
|
8461
9396
|
},
|
|
8462
9397
|
replace(id, key) {
|
|
@@ -8468,18 +9403,27 @@ function createGenerations(deps) {
|
|
|
8468
9403
|
slot.state = "building";
|
|
8469
9404
|
slot.startedAt = now();
|
|
8470
9405
|
slot.failure = null;
|
|
9406
|
+
opened(live, slot, attempt, "replace");
|
|
8471
9407
|
live.job.state = "building";
|
|
8472
9408
|
live.job.endedAt = null;
|
|
8473
9409
|
changed();
|
|
9410
|
+
noted(live, "replace", slot.key);
|
|
8474
9411
|
const replacing = async () => {
|
|
8475
|
-
const
|
|
9412
|
+
const prompt = replacePrompt2({
|
|
8476
9413
|
surface: live.job.surface,
|
|
8477
9414
|
brief: live.job.brief,
|
|
8478
9415
|
avoid: [...live.concepts.values()],
|
|
8479
9416
|
base: live.base
|
|
8480
|
-
})
|
|
9417
|
+
});
|
|
9418
|
+
const ideaRun = runRecord(prompt);
|
|
9419
|
+
const own = attemptRecord(live, slot.key, attempt);
|
|
9420
|
+
if (own !== void 0)
|
|
9421
|
+
own.ideaRun = ideaRun;
|
|
9422
|
+
const asking = run4(live.agent, live.agent.plan(prompt), PLAN_DEADLINE_MS);
|
|
8481
9423
|
live.runs.set(slot.key, asking);
|
|
8482
9424
|
const outcome = await asking.done;
|
|
9425
|
+
ended(live.agent, ideaRun, outcome);
|
|
9426
|
+
saveRecord(live);
|
|
8483
9427
|
if (!owns(live, slot, attempt))
|
|
8484
9428
|
return;
|
|
8485
9429
|
if (live.runs.get(slot.key) === asking)
|
|
@@ -8496,6 +9440,7 @@ function createGenerations(deps) {
|
|
|
8496
9440
|
});
|
|
8497
9441
|
settle(live);
|
|
8498
9442
|
changed();
|
|
9443
|
+
saveRecord(live);
|
|
8499
9444
|
return;
|
|
8500
9445
|
}
|
|
8501
9446
|
const titles = new Set(await deps.titles());
|
|
@@ -8515,6 +9460,7 @@ function createGenerations(deps) {
|
|
|
8515
9460
|
live.concepts.set(slot.key, { ...concept, title });
|
|
8516
9461
|
slot.title = title;
|
|
8517
9462
|
slot.idea = concept.idea;
|
|
9463
|
+
saveRecord(live);
|
|
8518
9464
|
if (!owns(live, slot, attempt))
|
|
8519
9465
|
return;
|
|
8520
9466
|
await restore(slot);
|
|
@@ -8532,35 +9478,36 @@ function createGenerations(deps) {
|
|
|
8532
9478
|
})),
|
|
8533
9479
|
wait(KILL_GRACE_MS * 2 + 1e3)
|
|
8534
9480
|
]);
|
|
9481
|
+
await Promise.race([records.flush(), wait(KILL_GRACE_MS)]);
|
|
8535
9482
|
}
|
|
8536
9483
|
};
|
|
8537
9484
|
}
|
|
8538
9485
|
|
|
8539
9486
|
// ../server/dist/server-info.js
|
|
8540
|
-
import { lstat as lstat2, mkdir as
|
|
8541
|
-
import { dirname as dirname9, join as
|
|
9487
|
+
import { lstat as lstat2, mkdir as mkdir8, readFile as readFile16, rename as rename4, rm as rm6, writeFile as writeFile9 } from "fs/promises";
|
|
9488
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
8542
9489
|
var SERVER_INFO_PATH = ".leglas/server.json";
|
|
8543
9490
|
async function writeServerInfo(cwd, info) {
|
|
8544
|
-
const path =
|
|
9491
|
+
const path = join16(cwd, SERVER_INFO_PATH);
|
|
8545
9492
|
const directory = dirname9(path);
|
|
8546
|
-
await
|
|
9493
|
+
await mkdir8(directory, { recursive: true });
|
|
8547
9494
|
if (!(await lstat2(directory)).isDirectory())
|
|
8548
9495
|
return;
|
|
8549
9496
|
const existing = await lstat2(path).catch(() => null);
|
|
8550
9497
|
if (existing !== null && !existing.isFile())
|
|
8551
9498
|
return;
|
|
8552
9499
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
8553
|
-
await
|
|
9500
|
+
await writeFile9(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
8554
9501
|
`, "utf8");
|
|
8555
|
-
await
|
|
8556
|
-
await
|
|
9502
|
+
await rename4(temporary, path).catch(async (cause) => {
|
|
9503
|
+
await rm6(temporary, { force: true }).catch(() => {
|
|
8557
9504
|
});
|
|
8558
9505
|
throw cause;
|
|
8559
9506
|
});
|
|
8560
9507
|
}
|
|
8561
9508
|
async function readServerInfo(cwd) {
|
|
8562
9509
|
try {
|
|
8563
|
-
const value = parseJson(await
|
|
9510
|
+
const value = parseJson(await readFile16(join16(cwd, SERVER_INFO_PATH), "utf8"));
|
|
8564
9511
|
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))
|
|
8565
9512
|
return null;
|
|
8566
9513
|
return { port: value.port, url: value.url, pid: value.pid };
|
|
@@ -8574,7 +9521,7 @@ async function removeServerInfo(cwd, expected) {
|
|
|
8574
9521
|
if (current !== null && (current.port !== expected.port || current.pid !== expected.pid))
|
|
8575
9522
|
return;
|
|
8576
9523
|
}
|
|
8577
|
-
await
|
|
9524
|
+
await rm6(join16(cwd, SERVER_INFO_PATH), { force: true });
|
|
8578
9525
|
}
|
|
8579
9526
|
|
|
8580
9527
|
// ../server/dist/server.js
|
|
@@ -8723,7 +9670,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
8723
9670
|
return resolve5(false);
|
|
8724
9671
|
}
|
|
8725
9672
|
const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
8726
|
-
const socket = net3.connect(port, url.hostname);
|
|
9673
|
+
const socket = net3.connect(port, url.hostname.replace(/^\[|\]$/g, ""));
|
|
8727
9674
|
const settle = (reachable) => {
|
|
8728
9675
|
socket.destroy();
|
|
8729
9676
|
resolve5(reachable);
|
|
@@ -8735,8 +9682,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
8735
9682
|
});
|
|
8736
9683
|
}
|
|
8737
9684
|
function serveFrom(res, dir, relativePath) {
|
|
8738
|
-
const
|
|
8739
|
-
const candidate =
|
|
9685
|
+
const relative9 = normalize4(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
9686
|
+
const candidate = join17(dir, relative9);
|
|
8740
9687
|
if (!candidate.startsWith(dir))
|
|
8741
9688
|
return false;
|
|
8742
9689
|
if (!existsSync3(candidate) || !statSync(candidate).isFile())
|
|
@@ -8750,9 +9697,9 @@ function serveFrom(res, dir, relativePath) {
|
|
|
8750
9697
|
return true;
|
|
8751
9698
|
}
|
|
8752
9699
|
function serveShellFile(res, shellDir, urlPath) {
|
|
8753
|
-
const
|
|
8754
|
-
const isRoot =
|
|
8755
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
9700
|
+
const relative9 = normalize4(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
9701
|
+
const isRoot = relative9 === "" || relative9 === "." || relative9 === "/";
|
|
9702
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative9);
|
|
8756
9703
|
}
|
|
8757
9704
|
var HEALTH_PROBE_MS = 3e3;
|
|
8758
9705
|
function fileStamp(path) {
|
|
@@ -8764,11 +9711,11 @@ function fileStamp(path) {
|
|
|
8764
9711
|
}
|
|
8765
9712
|
}
|
|
8766
9713
|
function watchLiveFiles(cwd, configPath, live) {
|
|
8767
|
-
const leglasDir =
|
|
9714
|
+
const leglasDir = join17(cwd, ".leglas");
|
|
8768
9715
|
const targets = [
|
|
8769
|
-
{ path:
|
|
8770
|
-
{ path:
|
|
8771
|
-
{ path:
|
|
9716
|
+
{ path: join17(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
9717
|
+
{ path: join17(cwd, REQUESTS_PATH), change: "requests" },
|
|
9718
|
+
{ path: join17(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
8772
9719
|
];
|
|
8773
9720
|
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
8774
9721
|
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
@@ -9421,6 +10368,7 @@ async function startServer(options) {
|
|
|
9421
10368
|
});
|
|
9422
10369
|
}
|
|
9423
10370
|
const deleted = await dropLocalPreviews(cwd, unique);
|
|
10371
|
+
await noteDirections(cwd, unique, "remove");
|
|
9424
10372
|
return sendJson2(res, 200, { ok: true, deleted });
|
|
9425
10373
|
} catch {
|
|
9426
10374
|
return sendJson2(res, 500, {
|
|
@@ -9469,8 +10417,8 @@ async function startServer(options) {
|
|
|
9469
10417
|
const id = newRequestId();
|
|
9470
10418
|
const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
|
|
9471
10419
|
try {
|
|
9472
|
-
await
|
|
9473
|
-
await
|
|
10420
|
+
await mkdir9(join17(cwd, REFERENCES_DIR), { recursive: true });
|
|
10421
|
+
await writeFile10(join17(cwd, file), body);
|
|
9474
10422
|
void pruneReferences(cwd).catch(() => {
|
|
9475
10423
|
});
|
|
9476
10424
|
return sendJson2(res, 200, {
|
|
@@ -9512,7 +10460,7 @@ async function startServer(options) {
|
|
|
9512
10460
|
}
|
|
9513
10461
|
const references = parsed.references ?? [];
|
|
9514
10462
|
if (references.length > 0) {
|
|
9515
|
-
const present = new Set((await
|
|
10463
|
+
const present = new Set((await readdir6(join17(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
9516
10464
|
const gone = references.filter((id2) => !present.has(id2));
|
|
9517
10465
|
if (gone.length > 0) {
|
|
9518
10466
|
return sendJson2(res, 410, {
|
|
@@ -9542,9 +10490,8 @@ async function startServer(options) {
|
|
|
9542
10490
|
};
|
|
9543
10491
|
const compare = isString(parsed.compare) && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
|
|
9544
10492
|
const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
|
|
9545
|
-
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode
|
|
9546
|
-
//
|
|
9547
|
-
// genuine repeat is refused.
|
|
10493
|
+
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode aren't the same request: one
|
|
10494
|
+
// forks, one rewrites.
|
|
9548
10495
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
9549
10496
|
return sendJson2(res, 409, {
|
|
9550
10497
|
ok: false,
|
|
@@ -9569,9 +10516,8 @@ async function startServer(options) {
|
|
|
9569
10516
|
title: preview.title,
|
|
9570
10517
|
url: preview.url,
|
|
9571
10518
|
intent,
|
|
9572
|
-
// The ids travel with the request so a change
|
|
9573
|
-
//
|
|
9574
|
-
// the direction they point at was not touched.
|
|
10519
|
+
// The ids travel with the request so a change in place can forget
|
|
10520
|
+
// the notes it answered. A fork leaves them.
|
|
9575
10521
|
...composed
|
|
9576
10522
|
};
|
|
9577
10523
|
if (notes.length > 0)
|
|
@@ -9675,8 +10621,8 @@ async function startServer(options) {
|
|
|
9675
10621
|
const noteSuffix = isString(parsed.note) ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
|
|
9676
10622
|
const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
|
|
9677
10623
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
9678
|
-
await
|
|
9679
|
-
await
|
|
10624
|
+
await mkdir9(join17(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
10625
|
+
await writeFile10(join17(cwd, relativeFile), shot.png);
|
|
9680
10626
|
return sendJson2(res, 200, {
|
|
9681
10627
|
ok: true,
|
|
9682
10628
|
file: relativeFile,
|
|
@@ -9811,16 +10757,14 @@ async function startServer(options) {
|
|
|
9811
10757
|
title,
|
|
9812
10758
|
intent,
|
|
9813
10759
|
// A fork leaves its parent's document alone; the interface keeps
|
|
9814
|
-
// the parent's duplicate verdict
|
|
10760
|
+
// the parent's duplicate verdict because of this.
|
|
9815
10761
|
mode,
|
|
9816
|
-
// Which pins this change speaks for
|
|
9817
|
-
//
|
|
9818
|
-
// like one nobody has read.
|
|
10762
|
+
// Which pins this change speaks for, so the interface can show a
|
|
10763
|
+
// note already in an agent's prompt as read.
|
|
9819
10764
|
notes: notes ?? [],
|
|
9820
|
-
// The
|
|
9821
|
-
//
|
|
9822
|
-
//
|
|
9823
|
-
// could not be written.
|
|
10765
|
+
// The file can't know the run in flight. Otherwise it's the record,
|
|
10766
|
+
// across restarts; the in-memory failed set only covers a verdict
|
|
10767
|
+
// that couldn't be written.
|
|
9824
10768
|
status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
|
|
9825
10769
|
failure: failure ?? null
|
|
9826
10770
|
})),
|
|
@@ -9830,13 +10774,13 @@ async function startServer(options) {
|
|
|
9830
10774
|
name: snapshot.running ? snapshot.agent : null,
|
|
9831
10775
|
activity: snapshot.running ? snapshot.activity : null,
|
|
9832
10776
|
startedAt: snapshot.running ? snapshot.startedAt : null,
|
|
9833
|
-
// A stop
|
|
9834
|
-
//
|
|
10777
|
+
// A stop asked for but not yet obeyed, so the card stops describing
|
|
10778
|
+
// a live run.
|
|
9835
10779
|
stopping: snapshot.running && snapshot.stopping,
|
|
9836
10780
|
// Why a run that looks stalled is stalled, while it is stalled.
|
|
9837
10781
|
waiting: snapshot.running ? snapshot.waiting : null,
|
|
9838
|
-
// When a run
|
|
9839
|
-
//
|
|
10782
|
+
// When a quiet run last said anything. A run on its way out is
|
|
10783
|
+
// stopping, not quiet.
|
|
9840
10784
|
quietSince: snapshot.running && !snapshot.stopping ? snapshot.quietSince : null
|
|
9841
10785
|
}
|
|
9842
10786
|
}));
|
|
@@ -9893,12 +10837,11 @@ async function startServer(options) {
|
|
|
9893
10837
|
url: request.url,
|
|
9894
10838
|
intent: request.intent,
|
|
9895
10839
|
target: request.target,
|
|
9896
|
-
// The prompt names
|
|
9897
|
-
//
|
|
9898
|
-
// `requests --json` all hand the text over as it stands.
|
|
10840
|
+
// The prompt names captures by path, and watch, a custom command
|
|
10841
|
+
// and `requests --json` hand the text over as is.
|
|
9899
10842
|
prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId)
|
|
9900
|
-
// The stored prompt already carries the mode's instructions;
|
|
9901
|
-
//
|
|
10843
|
+
// The stored prompt already carries the mode's instructions; its
|
|
10844
|
+
// notes and visual context travel with the retry.
|
|
9902
10845
|
};
|
|
9903
10846
|
if (request.mode !== void 0)
|
|
9904
10847
|
retry.mode = request.mode;
|
|
@@ -10056,15 +10999,15 @@ async function startServer(options) {
|
|
|
10056
10999
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
10057
11000
|
const slash = rest.indexOf("/");
|
|
10058
11001
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
10059
|
-
let
|
|
11002
|
+
let relative9 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
10060
11003
|
try {
|
|
10061
|
-
|
|
11004
|
+
relative9 = decodeURIComponent(relative9);
|
|
10062
11005
|
} catch {
|
|
10063
|
-
|
|
11006
|
+
relative9 = "";
|
|
10064
11007
|
}
|
|
10065
11008
|
const dir = fileMounts.get(slug);
|
|
10066
11009
|
const serveMount = () => {
|
|
10067
|
-
if (dir !== void 0 &&
|
|
11010
|
+
if (dir !== void 0 && relative9 !== "" && serveFrom(res, dir, relative9))
|
|
10068
11011
|
return;
|
|
10069
11012
|
res.writeHead(404, {
|
|
10070
11013
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -10074,7 +11017,7 @@ async function startServer(options) {
|
|
|
10074
11017
|
};
|
|
10075
11018
|
if (!context.remote)
|
|
10076
11019
|
return serveMount();
|
|
10077
|
-
if (
|
|
11020
|
+
if (relative9.split("/").some((segment) => segment.startsWith("."))) {
|
|
10078
11021
|
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
10079
11022
|
}
|
|
10080
11023
|
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
@@ -10175,7 +11118,7 @@ async function startServer(options) {
|
|
|
10175
11118
|
if (options.claudeAgentSession !== void 0)
|
|
10176
11119
|
runnerOptions.claudeAgentSession = options.claudeAgentSession;
|
|
10177
11120
|
runner = startRunner(runnerOptions);
|
|
10178
|
-
const renderDirection = async (title) => {
|
|
11121
|
+
const renderDirection = async (title, { picture = false } = {}) => {
|
|
10179
11122
|
const preview = (await livePreviews()).find((entry) => entry.title === title);
|
|
10180
11123
|
if (preview === void 0)
|
|
10181
11124
|
return null;
|
|
@@ -10184,15 +11127,29 @@ async function startServer(options) {
|
|
|
10184
11127
|
return null;
|
|
10185
11128
|
const address = server.address();
|
|
10186
11129
|
const renderPort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
|
|
11130
|
+
const url = previewUrl(`http://127.0.0.1:${renderPort}`, preview);
|
|
11131
|
+
const settings = { width: 1440, timeoutMs: CAPTURE_LOAD_MS, inspect: true };
|
|
10187
11132
|
try {
|
|
10188
11133
|
const captured = await capturePage(browser, {
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
11134
|
+
...settings,
|
|
11135
|
+
url,
|
|
11136
|
+
jpeg: picture
|
|
10192
11137
|
});
|
|
10193
|
-
|
|
11138
|
+
const layout = captured.layout.map(describeFinding);
|
|
11139
|
+
const frame = captured.jpeg;
|
|
11140
|
+
if (layout.length === 0)
|
|
11141
|
+
return { errors: captured.errors, layout, frame };
|
|
11142
|
+
const plain = new URL(url);
|
|
11143
|
+
plain.search = new URLSearchParams([...plain.searchParams].filter(([key]) => !key.startsWith("v-"))).toString();
|
|
11144
|
+
const before = await capturePage(browser, { ...settings, url: plain.href }).catch(() => null);
|
|
11145
|
+
const known = new Set(before?.layout.map(describeFinding) ?? []);
|
|
11146
|
+
return {
|
|
11147
|
+
errors: captured.errors,
|
|
11148
|
+
layout: layout.filter((line) => !known.has(line)),
|
|
11149
|
+
frame
|
|
11150
|
+
};
|
|
10194
11151
|
} catch (error) {
|
|
10195
|
-
return { errors: [error instanceof Error ? error.message : String(error)] };
|
|
11152
|
+
return { errors: [error instanceof Error ? error.message : String(error)], layout: [] };
|
|
10196
11153
|
}
|
|
10197
11154
|
};
|
|
10198
11155
|
const generationDeps = {
|
|
@@ -10206,7 +11163,8 @@ async function startServer(options) {
|
|
|
10206
11163
|
const local = await readLocalPreviews(cwd).catch(() => null);
|
|
10207
11164
|
return new Set([...config?.previews ?? [], ...local?.previews ?? []].map((entry) => entry.title));
|
|
10208
11165
|
},
|
|
10209
|
-
onChange: () => live.nudge("generation")
|
|
11166
|
+
onChange: () => live.nudge("generation"),
|
|
11167
|
+
record: () => config?.recordSets !== false
|
|
10210
11168
|
};
|
|
10211
11169
|
if (options.generationSpawn !== void 0)
|
|
10212
11170
|
generationDeps.spawn = options.generationSpawn;
|
|
@@ -10249,10 +11207,10 @@ async function startServer(options) {
|
|
|
10249
11207
|
|
|
10250
11208
|
// ../server/dist/update.js
|
|
10251
11209
|
import { spawn as spawnChild2 } from "child_process";
|
|
10252
|
-
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
10253
|
-
import { lstat as lstat3, mkdir as
|
|
11210
|
+
import { existsSync as existsSync4, readFileSync, realpathSync as realpathSync2 } from "fs";
|
|
11211
|
+
import { lstat as lstat3, mkdir as mkdir10, mkdtemp, rename as rename5, rm as rm7, writeFile as writeFile11 } from "fs/promises";
|
|
10254
11212
|
import { homedir as homedir3 } from "os";
|
|
10255
|
-
import { dirname as dirname11, join as
|
|
11213
|
+
import { dirname as dirname11, join as join18, posix as posix2, win32 } from "path";
|
|
10256
11214
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
10257
11215
|
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
10258
11216
|
|
|
@@ -10285,7 +11243,7 @@ function planKeep(options) {
|
|
|
10285
11243
|
error: `Cannot tell which file produces ${JSON.stringify(options.title)}. Its URL (${winner.url}) was not generated by leglas new, so there is nothing safe to move.`
|
|
10286
11244
|
};
|
|
10287
11245
|
}
|
|
10288
|
-
const to =
|
|
11246
|
+
const to = normalize5(options.to);
|
|
10289
11247
|
if (to.startsWith("..") || isAbsolute2(to)) {
|
|
10290
11248
|
return { ok: false, error: "The destination has to be inside the project." };
|
|
10291
11249
|
}
|
|
@@ -10311,11 +11269,11 @@ function planKeep(options) {
|
|
|
10311
11269
|
}
|
|
10312
11270
|
|
|
10313
11271
|
// src/run-init.ts
|
|
10314
|
-
import { readFile as
|
|
10315
|
-
import { join as
|
|
11272
|
+
import { readFile as readFile17, writeFile as writeFile12 } from "fs/promises";
|
|
11273
|
+
import { join as join19 } from "path";
|
|
10316
11274
|
async function readIfPresent(path) {
|
|
10317
11275
|
try {
|
|
10318
|
-
return await
|
|
11276
|
+
return await readFile17(path, "utf8");
|
|
10319
11277
|
} catch {
|
|
10320
11278
|
return null;
|
|
10321
11279
|
}
|
|
@@ -10323,18 +11281,18 @@ async function readIfPresent(path) {
|
|
|
10323
11281
|
async function runInit(options, deps) {
|
|
10324
11282
|
const existingConfig = findConfigFile(options.cwd);
|
|
10325
11283
|
const plan = planInit({
|
|
10326
|
-
agents: await readIfPresent(
|
|
11284
|
+
agents: await readIfPresent(join19(options.cwd, "AGENTS.md")),
|
|
10327
11285
|
config: existingConfig === null ? null : "present",
|
|
10328
|
-
gitignore: await readIfPresent(
|
|
11286
|
+
gitignore: await readIfPresent(join19(options.cwd, ".gitignore")),
|
|
10329
11287
|
force: options.force
|
|
10330
11288
|
});
|
|
10331
11289
|
const touched = [];
|
|
10332
11290
|
for (const write2 of plan.writes) {
|
|
10333
|
-
await
|
|
11291
|
+
await writeFile12(join19(options.cwd, write2.path), write2.contents, "utf8");
|
|
10334
11292
|
touched.push(write2.path);
|
|
10335
11293
|
}
|
|
10336
11294
|
if (plan.gitignore !== null) {
|
|
10337
|
-
await
|
|
11295
|
+
await writeFile12(join19(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
10338
11296
|
touched.push(".gitignore");
|
|
10339
11297
|
}
|
|
10340
11298
|
if (options.json) {
|
|
@@ -10354,8 +11312,8 @@ async function runInit(options, deps) {
|
|
|
10354
11312
|
|
|
10355
11313
|
// src/run-keep.ts
|
|
10356
11314
|
import { existsSync as existsSync5 } from "fs";
|
|
10357
|
-
import { copyFile as copyFile2, mkdir as
|
|
10358
|
-
import { dirname as dirname12, isAbsolute as isAbsolute3, join as
|
|
11315
|
+
import { copyFile as copyFile2, mkdir as mkdir11, readFile as readFile18, rm as rm8, writeFile as writeFile13 } from "fs/promises";
|
|
11316
|
+
import { dirname as dirname12, isAbsolute as isAbsolute3, join as join20, relative as relative5 } from "path";
|
|
10359
11317
|
|
|
10360
11318
|
// src/resolve-title.ts
|
|
10361
11319
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -10388,15 +11346,15 @@ async function writeLogEntry(options) {
|
|
|
10388
11346
|
annotations: await readAnnotations(options.cwd),
|
|
10389
11347
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
10390
11348
|
});
|
|
10391
|
-
const dir =
|
|
10392
|
-
await
|
|
10393
|
-
const file =
|
|
10394
|
-
await
|
|
11349
|
+
const dir = join20(options.cwd, options.logDir);
|
|
11350
|
+
await mkdir11(dir, { recursive: true });
|
|
11351
|
+
const file = join20(dir, `${entry.slug}.md`);
|
|
11352
|
+
await writeFile13(file, entry.markdown, "utf8");
|
|
10395
11353
|
if (entry.pictures.length > 0) {
|
|
10396
|
-
const pictureDir =
|
|
10397
|
-
await
|
|
11354
|
+
const pictureDir = join20(dir, entry.slug);
|
|
11355
|
+
await mkdir11(pictureDir, { recursive: true });
|
|
10398
11356
|
for (const picture of entry.pictures) {
|
|
10399
|
-
await copyFile2(
|
|
11357
|
+
await copyFile2(join20(options.cwd, picture.from), join20(pictureDir, picture.to)).catch(() => {
|
|
10400
11358
|
});
|
|
10401
11359
|
}
|
|
10402
11360
|
}
|
|
@@ -10420,17 +11378,17 @@ async function runKeep(options, deps) {
|
|
|
10420
11378
|
const destination = isAbsolute3(options.to) ? relative5(options.cwd, options.to) : options.to;
|
|
10421
11379
|
const plan = planKeep({ title: resolved.title, previews, to: destination });
|
|
10422
11380
|
if (!plan.ok) return fail(plan.error);
|
|
10423
|
-
const from =
|
|
10424
|
-
const to =
|
|
11381
|
+
const from = join20(options.cwd, plan.move.from);
|
|
11382
|
+
const to = join20(options.cwd, plan.move.to);
|
|
10425
11383
|
if (!existsSync5(from)) {
|
|
10426
11384
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
10427
11385
|
}
|
|
10428
11386
|
if (existsSync5(to)) {
|
|
10429
11387
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
10430
11388
|
}
|
|
10431
|
-
const source = await
|
|
10432
|
-
await
|
|
10433
|
-
await
|
|
11389
|
+
const source = await readFile18(from, "utf8");
|
|
11390
|
+
await mkdir11(dirname12(to), { recursive: true });
|
|
11391
|
+
await writeFile13(to, renameExport(source, plan.exportName), "utf8");
|
|
10434
11392
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
10435
11393
|
let logged = null;
|
|
10436
11394
|
let logError = null;
|
|
@@ -10445,8 +11403,9 @@ async function runKeep(options, deps) {
|
|
|
10445
11403
|
} catch (error) {
|
|
10446
11404
|
logError = error instanceof Error ? error.message : String(error);
|
|
10447
11405
|
}
|
|
10448
|
-
await
|
|
11406
|
+
await rm8(join20(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
10449
11407
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
11408
|
+
await noteDirections(options.cwd, [resolved.title], "keep", plan.move.to);
|
|
10450
11409
|
if (options.json) {
|
|
10451
11410
|
deps.log(
|
|
10452
11411
|
JSON.stringify({
|
|
@@ -10484,11 +11443,11 @@ async function runKeep(options, deps) {
|
|
|
10484
11443
|
|
|
10485
11444
|
// src/run-new.ts
|
|
10486
11445
|
import { existsSync as existsSync6 } from "fs";
|
|
10487
|
-
import { mkdir as
|
|
10488
|
-
import { dirname as dirname13, join as
|
|
11446
|
+
import { mkdir as mkdir12, readFile as readFile19, writeFile as writeFile14 } from "fs/promises";
|
|
11447
|
+
import { dirname as dirname13, join as join21 } from "path";
|
|
10489
11448
|
async function readIfPresent2(path) {
|
|
10490
11449
|
try {
|
|
10491
|
-
return await
|
|
11450
|
+
return await readFile19(path, "utf8");
|
|
10492
11451
|
} catch {
|
|
10493
11452
|
return null;
|
|
10494
11453
|
}
|
|
@@ -10496,7 +11455,7 @@ async function readIfPresent2(path) {
|
|
|
10496
11455
|
async function runNew(options, deps) {
|
|
10497
11456
|
let from;
|
|
10498
11457
|
if (options.from !== void 0) {
|
|
10499
|
-
const contents = await readIfPresent2(
|
|
11458
|
+
const contents = await readIfPresent2(join21(options.cwd, options.from));
|
|
10500
11459
|
if (contents === null) {
|
|
10501
11460
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
10502
11461
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -10507,8 +11466,8 @@ async function runNew(options, deps) {
|
|
|
10507
11466
|
}
|
|
10508
11467
|
const plan = planNew({
|
|
10509
11468
|
surface: options.surface,
|
|
10510
|
-
packageJson: await readIfPresent2(
|
|
10511
|
-
gitignore: await readIfPresent2(
|
|
11469
|
+
packageJson: await readIfPresent2(join21(options.cwd, "package.json")),
|
|
11470
|
+
gitignore: await readIfPresent2(join21(options.cwd, ".gitignore")),
|
|
10512
11471
|
from
|
|
10513
11472
|
});
|
|
10514
11473
|
const fail = (error) => {
|
|
@@ -10538,7 +11497,7 @@ async function runNew(options, deps) {
|
|
|
10538
11497
|
deps.log(plan.instructions);
|
|
10539
11498
|
return { exitCode: 0, written: [] };
|
|
10540
11499
|
}
|
|
10541
|
-
const existing = plan.writes.filter((write2) => existsSync6(
|
|
11500
|
+
const existing = plan.writes.filter((write2) => existsSync6(join21(options.cwd, write2.path)));
|
|
10542
11501
|
if (existing.length > 0) {
|
|
10543
11502
|
return fail(
|
|
10544
11503
|
`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
|
|
@@ -10546,13 +11505,13 @@ async function runNew(options, deps) {
|
|
|
10546
11505
|
}
|
|
10547
11506
|
const written = [];
|
|
10548
11507
|
for (const write2 of plan.writes) {
|
|
10549
|
-
const target =
|
|
10550
|
-
await
|
|
10551
|
-
await
|
|
11508
|
+
const target = join21(options.cwd, write2.path);
|
|
11509
|
+
await mkdir12(dirname13(target), { recursive: true });
|
|
11510
|
+
await writeFile14(target, write2.contents, "utf8");
|
|
10552
11511
|
written.push(write2.path);
|
|
10553
11512
|
}
|
|
10554
11513
|
if (plan.gitignore !== null) {
|
|
10555
|
-
await
|
|
11514
|
+
await writeFile14(join21(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
10556
11515
|
written.push(".gitignore");
|
|
10557
11516
|
}
|
|
10558
11517
|
if (options.json) {
|
|
@@ -10580,21 +11539,107 @@ async function runNew(options, deps) {
|
|
|
10580
11539
|
}
|
|
10581
11540
|
|
|
10582
11541
|
// src/run-previews.ts
|
|
10583
|
-
import { readFile as
|
|
10584
|
-
import { join as
|
|
11542
|
+
import { readFile as readFile20, writeFile as writeFile15 } from "fs/promises";
|
|
11543
|
+
import { join as join22 } from "path";
|
|
11544
|
+
|
|
11545
|
+
// src/running.ts
|
|
11546
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
11547
|
+
|
|
11548
|
+
// src/json.ts
|
|
11549
|
+
function isJsonObject(value) {
|
|
11550
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11551
|
+
}
|
|
11552
|
+
function isString2(value) {
|
|
11553
|
+
return typeof value === "string";
|
|
11554
|
+
}
|
|
11555
|
+
function isNumber2(value) {
|
|
11556
|
+
return typeof value === "number";
|
|
11557
|
+
}
|
|
11558
|
+
async function bodyOf(response) {
|
|
11559
|
+
try {
|
|
11560
|
+
return JSON.parse(await response.text());
|
|
11561
|
+
} catch {
|
|
11562
|
+
return null;
|
|
11563
|
+
}
|
|
11564
|
+
}
|
|
11565
|
+
|
|
11566
|
+
// src/running.ts
|
|
11567
|
+
var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
|
|
11568
|
+
async function sameDirectory(left, right) {
|
|
11569
|
+
const [a, b] = await Promise.all([
|
|
11570
|
+
realpath2(left).catch(() => left),
|
|
11571
|
+
realpath2(right).catch(() => right)
|
|
11572
|
+
]);
|
|
11573
|
+
return a === b;
|
|
11574
|
+
}
|
|
11575
|
+
async function findLeglas(cwd, port, request) {
|
|
11576
|
+
const server = port === null ? await readServerInfo(cwd) : null;
|
|
11577
|
+
const chosen = port ?? server?.port ?? DEFAULT_PORT;
|
|
11578
|
+
try {
|
|
11579
|
+
const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
|
|
11580
|
+
signal: AbortSignal.timeout(2e3)
|
|
11581
|
+
});
|
|
11582
|
+
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
11583
|
+
const answered = await health.text().then((text3) => JSON.parse(text3)).catch(() => ({}));
|
|
11584
|
+
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
11585
|
+
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
11586
|
+
return {
|
|
11587
|
+
ok: false,
|
|
11588
|
+
error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
|
|
11589
|
+
};
|
|
11590
|
+
}
|
|
11591
|
+
return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
|
|
11592
|
+
} catch {
|
|
11593
|
+
return { ok: false, error: NOT_RUNNING };
|
|
11594
|
+
}
|
|
11595
|
+
}
|
|
11596
|
+
function interfaceUrl(port, titles) {
|
|
11597
|
+
const url = new URL(`http://localhost:${port}${LEGLAS_PREFIX}`);
|
|
11598
|
+
const [direction, compare] = titles;
|
|
11599
|
+
if (direction !== void 0) url.searchParams.set("direction", direction);
|
|
11600
|
+
if (compare !== void 0) url.searchParams.set("compare", compare);
|
|
11601
|
+
return url.href;
|
|
11602
|
+
}
|
|
11603
|
+
async function railTitles(port, request) {
|
|
11604
|
+
try {
|
|
11605
|
+
const response = await request(`http://127.0.0.1:${port}${LEGLAS_PREFIX}/api/config`, {
|
|
11606
|
+
signal: AbortSignal.timeout(2e3)
|
|
11607
|
+
});
|
|
11608
|
+
if (!response.ok) return null;
|
|
11609
|
+
const payload = await bodyOf(response);
|
|
11610
|
+
if (!isJsonObject(payload) || !Array.isArray(payload.previews)) return null;
|
|
11611
|
+
return new Set(
|
|
11612
|
+
payload.previews.flatMap(
|
|
11613
|
+
(preview) => isJsonObject(preview) && isString2(preview.title) ? [preview.title] : []
|
|
11614
|
+
)
|
|
11615
|
+
);
|
|
11616
|
+
} catch {
|
|
11617
|
+
return null;
|
|
11618
|
+
}
|
|
11619
|
+
}
|
|
11620
|
+
async function recordedRail(cwd, request) {
|
|
11621
|
+
const record3 = await readServerInfo(cwd);
|
|
11622
|
+
if (record3 === null) return null;
|
|
11623
|
+
const found = await findLeglas(cwd, record3.port, request);
|
|
11624
|
+
if (!found.ok) return null;
|
|
11625
|
+
const titles = await railTitles(found.port, request);
|
|
11626
|
+
return titles === null ? null : { port: found.port, titles };
|
|
11627
|
+
}
|
|
11628
|
+
|
|
11629
|
+
// src/run-previews.ts
|
|
10585
11630
|
function envelope(deps, ok, body) {
|
|
10586
11631
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
10587
11632
|
}
|
|
10588
11633
|
async function ensureIgnored(cwd) {
|
|
10589
|
-
const path =
|
|
11634
|
+
const path = join22(cwd, ".gitignore");
|
|
10590
11635
|
let current = null;
|
|
10591
11636
|
try {
|
|
10592
|
-
current = await
|
|
11637
|
+
current = await readFile20(path, "utf8");
|
|
10593
11638
|
} catch {
|
|
10594
11639
|
current = null;
|
|
10595
11640
|
}
|
|
10596
11641
|
const next = ignoreEntry(current);
|
|
10597
|
-
if (next !== null) await
|
|
11642
|
+
if (next !== null) await writeFile15(path, next, "utf8");
|
|
10598
11643
|
}
|
|
10599
11644
|
async function runAdd(options, deps) {
|
|
10600
11645
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -10630,6 +11675,8 @@ async function runAdd(options, deps) {
|
|
|
10630
11675
|
}
|
|
10631
11676
|
await ensureIgnored(options.cwd);
|
|
10632
11677
|
const needsDevCommand = options.preview.branch !== void 0 && loaded.config?.devCommand === void 0;
|
|
11678
|
+
const rail = await recordedRail(options.cwd, deps.fetch ?? fetch);
|
|
11679
|
+
const opens = rail !== null && rail.titles.has(options.preview.title) ? interfaceUrl(rail.port, [options.preview.title]) : null;
|
|
10633
11680
|
if (options.json) {
|
|
10634
11681
|
const added = { added: options.preview.title };
|
|
10635
11682
|
if (options.preview.url !== void 0) added.url = options.preview.url;
|
|
@@ -10640,11 +11687,13 @@ async function runAdd(options, deps) {
|
|
|
10640
11687
|
if (needsDevCommand) {
|
|
10641
11688
|
added.warning = "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config.";
|
|
10642
11689
|
}
|
|
11690
|
+
if (opens !== null) added.interfaceUrl = opens;
|
|
10643
11691
|
envelope(deps, true, added);
|
|
10644
11692
|
} else {
|
|
10645
11693
|
deps.log(
|
|
10646
11694
|
` added ${options.preview.title} ${options.preview.url ?? options.preview.file ?? ""}`
|
|
10647
11695
|
);
|
|
11696
|
+
if (opens !== null) deps.log(` open ${opens}`);
|
|
10648
11697
|
deps.log("");
|
|
10649
11698
|
if (needsDevCommand) {
|
|
10650
11699
|
deps.log(" ! The config sets no devCommand, so Leglas cannot start this branch yet.");
|
|
@@ -10668,12 +11717,10 @@ async function runList(options, deps) {
|
|
|
10668
11717
|
...local.previews
|
|
10669
11718
|
];
|
|
10670
11719
|
if (options.json) {
|
|
11720
|
+
const rail = await recordedRail(options.cwd, deps.fetch ?? fetch);
|
|
10671
11721
|
envelope(deps, errors.length === 0, {
|
|
10672
|
-
// The whole record
|
|
10673
|
-
//
|
|
10674
|
-
// exactly what tells an agent why these are being compared, and leaving
|
|
10675
|
-
// it out made the listing thinner than the reference block that points
|
|
10676
|
-
// at it.
|
|
11722
|
+
// The whole record: a preview's note, tags and base direction are what
|
|
11723
|
+
// tell an agent why these are being compared.
|
|
10677
11724
|
previews: previews.map((preview) => ({
|
|
10678
11725
|
title: preview.title,
|
|
10679
11726
|
url: preview.url,
|
|
@@ -10683,7 +11730,8 @@ async function runList(options, deps) {
|
|
|
10683
11730
|
askedFor: preview.askedFor ?? null,
|
|
10684
11731
|
local: preview.local,
|
|
10685
11732
|
branch: preview.branch ?? null,
|
|
10686
|
-
file: preview.file ?? null
|
|
11733
|
+
file: preview.file ?? null,
|
|
11734
|
+
interfaceUrl: rail !== null && rail.titles.has(preview.title) ? interfaceUrl(rail.port, [preview.title]) : null
|
|
10687
11735
|
})),
|
|
10688
11736
|
errors
|
|
10689
11737
|
});
|
|
@@ -10746,9 +11794,9 @@ function describe(preview) {
|
|
|
10746
11794
|
branch: preview.branch ?? null,
|
|
10747
11795
|
file: preview.file ?? null,
|
|
10748
11796
|
local: preview.local === true,
|
|
10749
|
-
// A file preview names its own source
|
|
11797
|
+
// A file preview names its own source; anything else is decoded from the
|
|
10750
11798
|
// URL, and a URL outside the convention yields nothing rather than a path
|
|
10751
|
-
// that
|
|
11799
|
+
// that isn't there.
|
|
10752
11800
|
target: preview.file ?? targetFor(preview.url)
|
|
10753
11801
|
};
|
|
10754
11802
|
}
|
|
@@ -10766,8 +11814,7 @@ function planShow({ title, previews, requests }) {
|
|
|
10766
11814
|
ok: true,
|
|
10767
11815
|
direction: describe(found),
|
|
10768
11816
|
variants,
|
|
10769
|
-
// Its
|
|
10770
|
-
// here; this is the rest of the comparison.
|
|
11817
|
+
// Its variants are listed above, so this is the rest of the comparison.
|
|
10771
11818
|
comparedWith: previews.map((preview) => preview.title).filter((other) => other !== title && !variantTitles.has(other)),
|
|
10772
11819
|
requests: requests.filter((request) => request.title === title).map((request) => ({
|
|
10773
11820
|
id: request.id,
|
|
@@ -10779,58 +11826,6 @@ function planShow({ title, previews, requests }) {
|
|
|
10779
11826
|
};
|
|
10780
11827
|
}
|
|
10781
11828
|
|
|
10782
|
-
// src/running.ts
|
|
10783
|
-
import { realpath as realpath2 } from "fs/promises";
|
|
10784
|
-
|
|
10785
|
-
// src/json.ts
|
|
10786
|
-
function isJsonObject(value) {
|
|
10787
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10788
|
-
}
|
|
10789
|
-
function isString2(value) {
|
|
10790
|
-
return typeof value === "string";
|
|
10791
|
-
}
|
|
10792
|
-
function isNumber2(value) {
|
|
10793
|
-
return typeof value === "number";
|
|
10794
|
-
}
|
|
10795
|
-
async function bodyOf(response) {
|
|
10796
|
-
try {
|
|
10797
|
-
return JSON.parse(await response.text());
|
|
10798
|
-
} catch {
|
|
10799
|
-
return null;
|
|
10800
|
-
}
|
|
10801
|
-
}
|
|
10802
|
-
|
|
10803
|
-
// src/running.ts
|
|
10804
|
-
var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
|
|
10805
|
-
async function sameDirectory(left, right) {
|
|
10806
|
-
const [a, b] = await Promise.all([
|
|
10807
|
-
realpath2(left).catch(() => left),
|
|
10808
|
-
realpath2(right).catch(() => right)
|
|
10809
|
-
]);
|
|
10810
|
-
return a === b;
|
|
10811
|
-
}
|
|
10812
|
-
async function findLeglas(cwd, port, request) {
|
|
10813
|
-
const server = port === null ? await readServerInfo(cwd) : null;
|
|
10814
|
-
const chosen = port ?? server?.port ?? DEFAULT_PORT;
|
|
10815
|
-
try {
|
|
10816
|
-
const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
|
|
10817
|
-
signal: AbortSignal.timeout(2e3)
|
|
10818
|
-
});
|
|
10819
|
-
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
10820
|
-
const answered = await health.text().then((text3) => JSON.parse(text3)).catch(() => ({}));
|
|
10821
|
-
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
10822
|
-
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
10823
|
-
return {
|
|
10824
|
-
ok: false,
|
|
10825
|
-
error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
|
|
10826
|
-
};
|
|
10827
|
-
}
|
|
10828
|
-
return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
|
|
10829
|
-
} catch {
|
|
10830
|
-
return { ok: false, error: NOT_RUNNING };
|
|
10831
|
-
}
|
|
10832
|
-
}
|
|
10833
|
-
|
|
10834
11829
|
// src/run-show.ts
|
|
10835
11830
|
function isString3(value) {
|
|
10836
11831
|
return typeof value === "string";
|
|
@@ -10871,21 +11866,23 @@ async function runShow(options, deps) {
|
|
|
10871
11866
|
}
|
|
10872
11867
|
const envelope2 = {
|
|
10873
11868
|
ok: true,
|
|
10874
|
-
direction: plan.direction,
|
|
11869
|
+
direction: { ...plan.direction, interfaceUrl: null },
|
|
10875
11870
|
variants: plan.variants,
|
|
10876
11871
|
comparedWith: plan.comparedWith,
|
|
10877
11872
|
requests: plan.requests
|
|
10878
11873
|
};
|
|
11874
|
+
const request = deps.fetch ?? fetch;
|
|
11875
|
+
let running = null;
|
|
10879
11876
|
if (options.screenshot) {
|
|
10880
11877
|
const fail = (error) => {
|
|
10881
11878
|
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
10882
11879
|
else deps.error(error);
|
|
10883
11880
|
return { exitCode: 1 };
|
|
10884
11881
|
};
|
|
10885
|
-
const request = deps.fetch ?? fetch;
|
|
10886
11882
|
const found = await findLeglas(options.cwd, options.port, request);
|
|
10887
11883
|
if (!found.ok) return fail(found.error);
|
|
10888
11884
|
const port = found.port;
|
|
11885
|
+
running = port;
|
|
10889
11886
|
let response;
|
|
10890
11887
|
try {
|
|
10891
11888
|
response = await request(`http://127.0.0.1:${port}/leglas/api/capture`, {
|
|
@@ -10919,17 +11916,22 @@ async function runShow(options, deps) {
|
|
|
10919
11916
|
cut: captured.cut === true
|
|
10920
11917
|
};
|
|
10921
11918
|
}
|
|
11919
|
+
const rail = running === null ? await recordedRail(options.cwd, request) : { port: running, titles: await railTitles(running, request) ?? /* @__PURE__ */ new Set() };
|
|
11920
|
+
if (rail !== null && rail.titles.has(plan.direction.title)) {
|
|
11921
|
+
envelope2.direction.interfaceUrl = interfaceUrl(rail.port, [plan.direction.title]);
|
|
11922
|
+
}
|
|
10922
11923
|
if (options.json) {
|
|
10923
11924
|
deps.log(JSON.stringify(envelope2));
|
|
10924
11925
|
return { exitCode: 0 };
|
|
10925
11926
|
}
|
|
10926
|
-
const { direction } =
|
|
11927
|
+
const { direction } = envelope2;
|
|
10927
11928
|
deps.log(` ${direction.title}${direction.local ? " (local)" : ""}`);
|
|
10928
11929
|
if (direction.note !== null) deps.log(` ${direction.note}`);
|
|
10929
11930
|
deps.log("");
|
|
10930
11931
|
if (direction.target !== null) deps.log(` file ${direction.target}`);
|
|
10931
11932
|
if (direction.branch !== null) deps.log(` branch ${direction.branch}`);
|
|
10932
11933
|
deps.log(` url ${direction.url}`);
|
|
11934
|
+
if (direction.interfaceUrl !== null) deps.log(` open ${direction.interfaceUrl}`);
|
|
10933
11935
|
if (direction.tags.length > 0) deps.log(` tags ${direction.tags.join(", ")}`);
|
|
10934
11936
|
if (direction.basedOn !== null) deps.log(` variant of ${direction.basedOn}`);
|
|
10935
11937
|
if (plan.variants.length > 0) {
|
|
@@ -10958,7 +11960,7 @@ async function runShow(options, deps) {
|
|
|
10958
11960
|
if (plan.requests.length > 0) {
|
|
10959
11961
|
deps.log("");
|
|
10960
11962
|
deps.log(` Pending, not yet done (${plan.requests.length}):`);
|
|
10961
|
-
for (const
|
|
11963
|
+
for (const request2 of plan.requests) deps.log(` ${request2.status} ${request2.intent}`);
|
|
10962
11964
|
deps.log("");
|
|
10963
11965
|
deps.log(" Run npx leglas requests --json for the full prompts.");
|
|
10964
11966
|
}
|
|
@@ -10992,8 +11994,11 @@ function tunnelFrom(value) {
|
|
|
10992
11994
|
}
|
|
10993
11995
|
}
|
|
10994
11996
|
function linkFrom(value) {
|
|
10995
|
-
if (!isJsonObject(value) || !isString2(value.localUrl) || !isNumber2(value.expiresAt))
|
|
11997
|
+
if (!isJsonObject(value) || !isString2(value.id) || !isString2(value.localUrl) || !isNumber2(value.expiresAt)) {
|
|
11998
|
+
return null;
|
|
11999
|
+
}
|
|
10996
12000
|
return {
|
|
12001
|
+
id: value.id,
|
|
10997
12002
|
name: isString2(value.name) ? value.name : "",
|
|
10998
12003
|
url: isString2(value.url) ? value.url : null,
|
|
10999
12004
|
localUrl: value.localUrl,
|
|
@@ -11114,11 +12119,12 @@ async function runShare(options, deps) {
|
|
|
11114
12119
|
const running = await readShare(found.base, request);
|
|
11115
12120
|
if (!running.ok) return fail(running.error);
|
|
11116
12121
|
const { installed } = running.value;
|
|
11117
|
-
const report = (share2, already, leftOut2) => {
|
|
12122
|
+
const report = (share2, already, leftOut2, done = null) => {
|
|
11118
12123
|
if (options.json) {
|
|
11119
12124
|
deps.log(
|
|
11120
12125
|
JSON.stringify({
|
|
11121
12126
|
ok: true,
|
|
12127
|
+
...done?.json,
|
|
11122
12128
|
alreadySharing: already,
|
|
11123
12129
|
share: {
|
|
11124
12130
|
scope: share2.scope,
|
|
@@ -11132,6 +12138,7 @@ async function runShare(options, deps) {
|
|
|
11132
12138
|
);
|
|
11133
12139
|
return { exitCode: 0 };
|
|
11134
12140
|
}
|
|
12141
|
+
if (done !== null) deps.log(done.line);
|
|
11135
12142
|
deps.log(` sharing ${describe2(share2)}${already ? " (already running)" : ""}`);
|
|
11136
12143
|
if (share2.reach === "listed") {
|
|
11137
12144
|
deps.log(" reach only the paths the share lists; allow more from the share panel");
|
|
@@ -11141,6 +12148,9 @@ async function runShare(options, deps) {
|
|
|
11141
12148
|
if (link.url !== null) deps.log(` link ${label}${link.url}`);
|
|
11142
12149
|
else deps.log(` local ${label}${link.localUrl}`);
|
|
11143
12150
|
}
|
|
12151
|
+
if (share2.links.length === 0) {
|
|
12152
|
+
deps.log(" links no link is live; npx leglas share --rotate starts one");
|
|
12153
|
+
}
|
|
11144
12154
|
const tunnel = tunnelLine(share2.tunnel, installed);
|
|
11145
12155
|
if (tunnel !== null) deps.log(` tunnel ${tunnel}`);
|
|
11146
12156
|
const ends = share2.links.map((link) => link.expiresAt);
|
|
@@ -11151,6 +12161,66 @@ async function runShare(options, deps) {
|
|
|
11151
12161
|
deps.log(" stop npx leglas share --stop");
|
|
11152
12162
|
return { exitCode: 0 };
|
|
11153
12163
|
};
|
|
12164
|
+
const settle = async (share2) => {
|
|
12165
|
+
let settled2 = share2;
|
|
12166
|
+
for (let waited = 0; settled2.tunnel.status === "starting" && waited < TUNNEL_WAIT_MS; waited += TUNNEL_POLL_MS) {
|
|
12167
|
+
await sleep(TUNNEL_POLL_MS);
|
|
12168
|
+
const read = await readShare(found.base, request);
|
|
12169
|
+
if (!read.ok) return read;
|
|
12170
|
+
if (read.value.share === null) return { ok: true, value: null };
|
|
12171
|
+
settled2 = read.value.share;
|
|
12172
|
+
}
|
|
12173
|
+
return { ok: true, value: settled2 };
|
|
12174
|
+
};
|
|
12175
|
+
const revoke = options.revoke ?? null;
|
|
12176
|
+
if (options.rotate === true || revoke !== null) {
|
|
12177
|
+
const current2 = running.value.share;
|
|
12178
|
+
if (current2 === null) {
|
|
12179
|
+
return fail("Nothing is being shared. Start a share with npx leglas share.");
|
|
12180
|
+
}
|
|
12181
|
+
if (revoke !== null) {
|
|
12182
|
+
const named = revoke;
|
|
12183
|
+
const link = current2.links.find(
|
|
12184
|
+
(candidate) => candidate.id === named || candidate.url === named || candidate.localUrl === named
|
|
12185
|
+
);
|
|
12186
|
+
if (link === void 0) {
|
|
12187
|
+
return fail(`No live link is ${JSON.stringify(named)}. npx leglas share lists them.`);
|
|
12188
|
+
}
|
|
12189
|
+
const revoked = await post(
|
|
12190
|
+
found.base,
|
|
12191
|
+
"/leglas/api/share/grants/revoke",
|
|
12192
|
+
{ id: link.id },
|
|
12193
|
+
request,
|
|
12194
|
+
"Leglas could not end that link."
|
|
12195
|
+
);
|
|
12196
|
+
if (!revoked.ok) return fail(revoked.error);
|
|
12197
|
+
const after = isJsonObject(revoked.value) ? shareFrom(revoked.value.share) : null;
|
|
12198
|
+
if (after === null) return fail("Leglas ended the link but did not say what is left.");
|
|
12199
|
+
return report(after, true, [], {
|
|
12200
|
+
json: { revoked: link },
|
|
12201
|
+
line: ` revoked ${link.name === "" ? "" : `${link.name} `}${link.url ?? link.localUrl}`
|
|
12202
|
+
});
|
|
12203
|
+
}
|
|
12204
|
+
const rotated = await post(
|
|
12205
|
+
found.base,
|
|
12206
|
+
"/leglas/api/share/rotate",
|
|
12207
|
+
{},
|
|
12208
|
+
request,
|
|
12209
|
+
"Leglas could not replace the links."
|
|
12210
|
+
);
|
|
12211
|
+
if (!rotated.ok) return fail(rotated.error);
|
|
12212
|
+
const replaced = isJsonObject(rotated.value) ? shareFrom(rotated.value.share) : null;
|
|
12213
|
+
if (replaced === null) return fail("Leglas replaced the links but did not say with what.");
|
|
12214
|
+
const settled2 = await settle(replaced);
|
|
12215
|
+
if (!settled2.ok) return fail(settled2.error);
|
|
12216
|
+
if (settled2.value === null) {
|
|
12217
|
+
return fail("The share was stopped while its new tunnel was starting.");
|
|
12218
|
+
}
|
|
12219
|
+
return report(settled2.value, true, [], {
|
|
12220
|
+
json: { rotated: true },
|
|
12221
|
+
line: " rotated every earlier link has ended"
|
|
12222
|
+
});
|
|
12223
|
+
}
|
|
11154
12224
|
const loaded = await loadConfig(options.cwd);
|
|
11155
12225
|
const local = await readLocalPreviews(options.cwd);
|
|
11156
12226
|
const renames = await readRenames(options.cwd);
|
|
@@ -11219,20 +12289,102 @@ async function runShare(options, deps) {
|
|
|
11219
12289
|
if (!created.ok) {
|
|
11220
12290
|
return created.answered ? fail(created.error) : giveUp("Leglas stopped answering while the share was starting.", false);
|
|
11221
12291
|
}
|
|
11222
|
-
|
|
12292
|
+
const share = isJsonObject(created.value) ? shareFrom(created.value.share) : null;
|
|
11223
12293
|
if (share === null) {
|
|
11224
12294
|
return giveUp("Leglas started a share this version of the command cannot read.");
|
|
11225
12295
|
}
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
12296
|
+
const settled = await settle(share);
|
|
12297
|
+
if (!settled.ok) return giveUp(settled.error);
|
|
12298
|
+
if (settled.value === null) {
|
|
12299
|
+
return fail("The share was stopped while its tunnel was starting.");
|
|
12300
|
+
}
|
|
12301
|
+
return report(settled.value, false, leftOut);
|
|
12302
|
+
}
|
|
12303
|
+
|
|
12304
|
+
// src/run-link.ts
|
|
12305
|
+
async function runLink(options, deps) {
|
|
12306
|
+
const fail = (error) => {
|
|
12307
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
12308
|
+
else deps.error(error);
|
|
12309
|
+
return { exitCode: 1 };
|
|
12310
|
+
};
|
|
12311
|
+
const loaded = await loadConfig(options.cwd);
|
|
12312
|
+
const local = await readLocalPreviews(options.cwd);
|
|
12313
|
+
const renames = await readRenames(options.cwd);
|
|
12314
|
+
const known = [
|
|
12315
|
+
...new Set(
|
|
12316
|
+
[...loaded.config?.previews ?? [], ...local.previews].map((preview) => preview.title)
|
|
12317
|
+
)
|
|
12318
|
+
];
|
|
12319
|
+
const titles = [];
|
|
12320
|
+
for (const asked of options.titles) {
|
|
12321
|
+
const resolved = resolveOrExplain(asked, known, renames);
|
|
12322
|
+
if (!resolved.ok) return fail(resolved.error);
|
|
12323
|
+
titles.push(resolved.title);
|
|
12324
|
+
}
|
|
12325
|
+
const [first, second] = titles;
|
|
12326
|
+
if (first !== void 0 && first === second) {
|
|
12327
|
+
return fail(`Both names are ${first}. Name two different directions to put side by side.`);
|
|
12328
|
+
}
|
|
12329
|
+
const request = deps.fetch ?? fetch;
|
|
12330
|
+
const found = await findLeglas(options.cwd, options.port, request);
|
|
12331
|
+
if (!found.ok) return fail(found.error);
|
|
12332
|
+
const rail = await railTitles(found.port, request);
|
|
12333
|
+
if (rail === null) return fail("Leglas did not say which directions are on its rail.");
|
|
12334
|
+
const absent = titles.find((title) => !rail.has(title));
|
|
12335
|
+
if (absent !== void 0) {
|
|
12336
|
+
return fail(
|
|
12337
|
+
`${absent} isn't on the running rail yet. A branch or file direction joins it when Leglas restarts.`
|
|
12338
|
+
);
|
|
12339
|
+
}
|
|
12340
|
+
const url = interfaceUrl(found.port, titles);
|
|
12341
|
+
if (options.json) deps.log(JSON.stringify({ ok: true, url }));
|
|
12342
|
+
else deps.log(url);
|
|
12343
|
+
return { exitCode: 0 };
|
|
12344
|
+
}
|
|
12345
|
+
|
|
12346
|
+
// src/run-remove.ts
|
|
12347
|
+
import { relative as relative6 } from "path";
|
|
12348
|
+
async function runRemove(options, deps) {
|
|
12349
|
+
const fail = (error) => {
|
|
12350
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
12351
|
+
else deps.error(error);
|
|
12352
|
+
return { exitCode: 1 };
|
|
12353
|
+
};
|
|
12354
|
+
const loaded = await loadConfig(options.cwd);
|
|
12355
|
+
const local = await readLocalPreviews(options.cwd);
|
|
12356
|
+
if (local.errors.length > 0) {
|
|
12357
|
+
return fail(`${LOCAL_PREVIEWS_PATH} has entries Leglas can't read: ${local.errors.join(" ")}`);
|
|
11234
12358
|
}
|
|
11235
|
-
|
|
12359
|
+
const known = [
|
|
12360
|
+
...new Set(
|
|
12361
|
+
[...loaded.config?.previews ?? [], ...local.previews].map((preview) => preview.title)
|
|
12362
|
+
)
|
|
12363
|
+
];
|
|
12364
|
+
const renames = await readRenames(options.cwd);
|
|
12365
|
+
const titles = [];
|
|
12366
|
+
for (const asked of options.titles) {
|
|
12367
|
+
const resolved = resolveOrExplain(asked, known, renames);
|
|
12368
|
+
if (!resolved.ok) return fail(resolved.error);
|
|
12369
|
+
if (!titles.includes(resolved.title)) titles.push(resolved.title);
|
|
12370
|
+
}
|
|
12371
|
+
const registered = new Set(local.previews.map((preview) => preview.title));
|
|
12372
|
+
const listed2 = new Set((loaded.config?.previews ?? []).map((preview) => preview.title));
|
|
12373
|
+
const shared = titles.find((title) => listed2.has(title) || !registered.has(title));
|
|
12374
|
+
if (shared !== void 0) {
|
|
12375
|
+
const where = loaded.path === null ? "the config" : relative6(options.cwd, loaded.path);
|
|
12376
|
+
return fail(`${shared} is in ${where}, which the project shares. Remove it there.`);
|
|
12377
|
+
}
|
|
12378
|
+
await dropLocalPreviews(options.cwd, titles);
|
|
12379
|
+
await noteDirections(options.cwd, titles, "remove");
|
|
12380
|
+
if (options.json) {
|
|
12381
|
+
deps.log(JSON.stringify({ ok: true, removed: titles }));
|
|
12382
|
+
} else {
|
|
12383
|
+
for (const title of titles) deps.log(` removed ${title}`);
|
|
12384
|
+
deps.log("");
|
|
12385
|
+
deps.log(`The files behind ${titles.length === 1 ? "it" : "them"} stay where they are.`);
|
|
12386
|
+
}
|
|
12387
|
+
return { exitCode: 0 };
|
|
11236
12388
|
}
|
|
11237
12389
|
|
|
11238
12390
|
// src/shutdown.ts
|
|
@@ -11249,21 +12401,21 @@ function installShutdown(stop, target = process) {
|
|
|
11249
12401
|
|
|
11250
12402
|
// src/run-watch.ts
|
|
11251
12403
|
import { spawn as spawn4 } from "child_process";
|
|
11252
|
-
import { mkdir as
|
|
11253
|
-
import { dirname as dirname14, join as
|
|
12404
|
+
import { mkdir as mkdir13, readFile as readFile21, writeFile as writeFile16 } from "fs/promises";
|
|
12405
|
+
import { dirname as dirname14, join as join23 } from "path";
|
|
11254
12406
|
var POLL_MS2 = 2e3;
|
|
11255
12407
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
11256
12408
|
async function saveTemplate(cwd, run4) {
|
|
11257
|
-
const path =
|
|
12409
|
+
const path = join23(cwd, WATCH_PATH);
|
|
11258
12410
|
let config = {};
|
|
11259
12411
|
try {
|
|
11260
|
-
const parsed = JSON.parse(await
|
|
12412
|
+
const parsed = JSON.parse(await readFile21(path, "utf8"));
|
|
11261
12413
|
if (isJsonObject(parsed)) config = parsed;
|
|
11262
12414
|
} catch {
|
|
11263
12415
|
}
|
|
11264
12416
|
config.run = run4;
|
|
11265
|
-
await
|
|
11266
|
-
await
|
|
12417
|
+
await mkdir13(dirname14(path), { recursive: true });
|
|
12418
|
+
await writeFile16(path, `${JSON.stringify(config, null, 2)}
|
|
11267
12419
|
`, "utf8");
|
|
11268
12420
|
}
|
|
11269
12421
|
function spawnAgent(command, args, cwd, json) {
|
|
@@ -11427,12 +12579,12 @@ async function runWatch(options, deps) {
|
|
|
11427
12579
|
|
|
11428
12580
|
// src/run-classify.ts
|
|
11429
12581
|
import { stat as stat5 } from "fs/promises";
|
|
11430
|
-
import { join as
|
|
12582
|
+
import { join as join24 } from "path";
|
|
11431
12583
|
async function runClassify(options, deps) {
|
|
11432
12584
|
const declared = await Promise.all(
|
|
11433
12585
|
options.changes.map(async (change) => ({
|
|
11434
12586
|
...change,
|
|
11435
|
-
exists: await stat5(
|
|
12587
|
+
exists: await stat5(join24(options.cwd, change.path)).then(
|
|
11436
12588
|
() => true,
|
|
11437
12589
|
() => false
|
|
11438
12590
|
)
|
|
@@ -11461,13 +12613,13 @@ async function runClassify(options, deps) {
|
|
|
11461
12613
|
import { existsSync as existsSync7 } from "fs";
|
|
11462
12614
|
import { realpath as realpath4 } from "fs/promises";
|
|
11463
12615
|
import { createRequire } from "module";
|
|
11464
|
-
import { basename as basename6, dirname as dirname15, join as
|
|
12616
|
+
import { basename as basename6, dirname as dirname15, join as join25, relative as relative8, resolve as resolve4 } from "path";
|
|
11465
12617
|
import { fileURLToPath } from "url";
|
|
11466
12618
|
|
|
11467
12619
|
// src/dev-server-owner.ts
|
|
11468
12620
|
import { execFile as execFile2 } from "child_process";
|
|
11469
12621
|
import { realpath as realpath3 } from "fs/promises";
|
|
11470
|
-
import { basename as basename5, isAbsolute as isAbsolute4, relative as
|
|
12622
|
+
import { basename as basename5, isAbsolute as isAbsolute4, relative as relative7, resolve as resolve3 } from "path";
|
|
11471
12623
|
import { promisify as promisify2 } from "util";
|
|
11472
12624
|
var run2 = promisify2(execFile2);
|
|
11473
12625
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -11536,7 +12688,7 @@ async function inspectLocalDevServer(origin) {
|
|
|
11536
12688
|
}
|
|
11537
12689
|
}
|
|
11538
12690
|
function isInside(parent, child) {
|
|
11539
|
-
const path =
|
|
12691
|
+
const path = relative7(resolve3(parent), resolve3(child));
|
|
11540
12692
|
return path === "" || !path.startsWith("..") && !isAbsolute4(path);
|
|
11541
12693
|
}
|
|
11542
12694
|
function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
@@ -11550,8 +12702,8 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
11550
12702
|
|
|
11551
12703
|
// src/run.ts
|
|
11552
12704
|
function findShellDir() {
|
|
11553
|
-
const bundled =
|
|
11554
|
-
if (existsSync7(
|
|
12705
|
+
const bundled = join25(dirname15(fileURLToPath(import.meta.url)), "shell");
|
|
12706
|
+
if (existsSync7(join25(bundled, "index.html"))) return bundled;
|
|
11555
12707
|
try {
|
|
11556
12708
|
const require2 = createRequire(import.meta.url);
|
|
11557
12709
|
return dirname15(require2.resolve("@leglas/shell/dist/index.html"));
|
|
@@ -11565,7 +12717,7 @@ function shellWord(value) {
|
|
|
11565
12717
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
11566
12718
|
}
|
|
11567
12719
|
function embeddedLeglasCommand() {
|
|
11568
|
-
const entry =
|
|
12720
|
+
const entry = join25(dirname15(fileURLToPath(import.meta.url)), "bin.js");
|
|
11569
12721
|
if (!existsSync7(entry)) return "npx -y leglas";
|
|
11570
12722
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
11571
12723
|
}
|
|
@@ -11604,7 +12756,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11604
12756
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
11605
12757
|
for (const preview of merged?.previews ?? []) {
|
|
11606
12758
|
if (preview.file !== void 0) {
|
|
11607
|
-
const absolute =
|
|
12759
|
+
const absolute = join25(options.cwd, preview.file);
|
|
11608
12760
|
if (!existsSync7(absolute)) {
|
|
11609
12761
|
previewErrors.push(
|
|
11610
12762
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -11636,8 +12788,8 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11636
12788
|
configWarnings,
|
|
11637
12789
|
fileMounts,
|
|
11638
12790
|
shellDir: findShellDir(),
|
|
11639
|
-
// The config file identifies the project
|
|
11640
|
-
//
|
|
12791
|
+
// The config file identifies the project, else the directory, so saved
|
|
12792
|
+
// layout survives a port change.
|
|
11641
12793
|
project: loaded.path ?? options.cwd,
|
|
11642
12794
|
cwd: options.cwd,
|
|
11643
12795
|
leglasCommand: embeddedLeglasCommand()
|
|
@@ -11666,7 +12818,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11666
12818
|
})
|
|
11667
12819
|
);
|
|
11668
12820
|
} else {
|
|
11669
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
12821
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative8(options.cwd, loaded.path) || loaded.path;
|
|
11670
12822
|
deps.log(`Leglas ${url}`);
|
|
11671
12823
|
deps.log(
|
|
11672
12824
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|
|
@@ -11722,17 +12874,24 @@ export {
|
|
|
11722
12874
|
AGENTS_MARKER_END,
|
|
11723
12875
|
AGENTS_MARKER_START,
|
|
11724
12876
|
CAPTURES_DIR,
|
|
12877
|
+
DEFAULT_EXPLORE_COUNT,
|
|
11725
12878
|
DEFAULT_PORT,
|
|
12879
|
+
DEFAULT_SHARE_REACH,
|
|
11726
12880
|
LEGLAS_PREFIX,
|
|
12881
|
+
MAX_PORT,
|
|
12882
|
+
MAX_SHOW_WIDTH,
|
|
12883
|
+
MIN_SHOW_WIDTH,
|
|
11727
12884
|
NO_BROWSER,
|
|
11728
12885
|
PROMPT_TOKEN,
|
|
11729
12886
|
REFERENCES_DIR,
|
|
11730
12887
|
SERVER_INFO_PATH,
|
|
11731
12888
|
WATCH_PATH,
|
|
12889
|
+
addRefusal,
|
|
11732
12890
|
baselineFrom,
|
|
11733
12891
|
commandFor,
|
|
11734
12892
|
detectFramework,
|
|
11735
12893
|
findBrowser,
|
|
12894
|
+
fromRefusal,
|
|
11736
12895
|
installShutdown,
|
|
11737
12896
|
isOwnCapture,
|
|
11738
12897
|
nextRequest,
|
|
@@ -11751,11 +12910,15 @@ export {
|
|
|
11751
12910
|
runExplore,
|
|
11752
12911
|
runInit,
|
|
11753
12912
|
runKeep,
|
|
12913
|
+
runLink,
|
|
11754
12914
|
runList,
|
|
11755
12915
|
runNew,
|
|
12916
|
+
runRemove,
|
|
11756
12917
|
runRequests,
|
|
11757
12918
|
runShare,
|
|
11758
12919
|
runShow,
|
|
11759
12920
|
runWatch,
|
|
12921
|
+
shareRefusal,
|
|
12922
|
+
showRefusal,
|
|
11760
12923
|
surfaceSlug
|
|
11761
12924
|
};
|