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/bin.js
CHANGED
|
@@ -5,20 +5,85 @@ import { spawn as spawn6 } from "child_process";
|
|
|
5
5
|
import { createRequire as createRequire2 } from "module";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
|
+
// src/rules.ts
|
|
9
|
+
var MIN_PORT = 1;
|
|
10
|
+
var MAX_PORT = 65535;
|
|
11
|
+
var MIN_SHOW_WIDTH = 320;
|
|
12
|
+
var MAX_SHOW_WIDTH = 3840;
|
|
13
|
+
var DEFAULT_EXPLORE_COUNT = 3;
|
|
14
|
+
var DEFAULT_SHARE_REACH = "open";
|
|
15
|
+
function portRefusal(flag, port) {
|
|
16
|
+
return port < MIN_PORT || port > MAX_PORT ? `${flag} must be between ${MIN_PORT} and ${MAX_PORT}, received ${port}.` : null;
|
|
17
|
+
}
|
|
18
|
+
function valueRefusal(flag) {
|
|
19
|
+
return `${flag} needs a value.`;
|
|
20
|
+
}
|
|
21
|
+
function addRefusal(preview) {
|
|
22
|
+
const texts = [
|
|
23
|
+
["--title", preview.title],
|
|
24
|
+
["--url", preview.url],
|
|
25
|
+
["--note", preview.note],
|
|
26
|
+
["--branch", preview.branch],
|
|
27
|
+
["--file", preview.file],
|
|
28
|
+
["--based-on", preview.basedOn],
|
|
29
|
+
["--asked-for", preview.askedFor],
|
|
30
|
+
...(preview.tags ?? []).map((tag) => ["--tag", tag])
|
|
31
|
+
];
|
|
32
|
+
const empty = texts.find(([, value]) => value === "");
|
|
33
|
+
if (empty !== void 0) return valueRefusal(empty[0]);
|
|
34
|
+
if (preview.url === void 0 && preview.file === void 0) {
|
|
35
|
+
return "leglas add needs --url (for example --url '/?v-hero=aurora') or --file for a page Leglas serves itself.";
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function shareRefusal(options) {
|
|
40
|
+
const acts = [
|
|
41
|
+
options.stop && "--stop ends the share",
|
|
42
|
+
options.rotate === true && "--rotate replaces every link",
|
|
43
|
+
options.revoke !== void 0 && options.revoke !== null && "--revoke ends one link"
|
|
44
|
+
].filter((act2) => act2 !== false);
|
|
45
|
+
if (acts.length > 1) {
|
|
46
|
+
return "leglas share takes one of --stop, --rotate or --revoke at a time.";
|
|
47
|
+
}
|
|
48
|
+
const [act] = acts;
|
|
49
|
+
if (act !== void 0 && (options.titles.length > 0 || options.reach !== void 0 || options.tunnel !== null)) {
|
|
50
|
+
return `leglas share ${act}; it takes no directions, reach or tunnel.`;
|
|
51
|
+
}
|
|
52
|
+
if (options.titles.length > 2) {
|
|
53
|
+
return "leglas share takes one direction to share alone, or two to compare. Name none to share the rail.";
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function linkRefusal(titles) {
|
|
58
|
+
return titles.length > 2 ? "leglas link takes one direction, or two to put side by side. Name none for the rail." : null;
|
|
59
|
+
}
|
|
60
|
+
function widthRefusal(width) {
|
|
61
|
+
return width < MIN_SHOW_WIDTH || width > MAX_SHOW_WIDTH ? `--width must be between ${MIN_SHOW_WIDTH} and ${MAX_SHOW_WIDTH}, received ${width}.` : null;
|
|
62
|
+
}
|
|
63
|
+
function showRefusal(options) {
|
|
64
|
+
if (options.width !== null && !options.screenshot) {
|
|
65
|
+
return "leglas show --width needs --screenshot.";
|
|
66
|
+
}
|
|
67
|
+
if (options.port !== null && !options.screenshot) {
|
|
68
|
+
return "leglas show --port needs --screenshot.";
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function fromRefusal(from) {
|
|
73
|
+
return from === "" ? "--from needs a path, for example --from src/Hero.tsx" : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
8
76
|
// src/args.ts
|
|
9
77
|
var SHARE_TUNNELS = ["cloudflared", "ngrok", "none"];
|
|
10
78
|
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--port", "--user-port", "--config"]);
|
|
11
79
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--no-open", "--json"]);
|
|
12
|
-
var MIN_SHOW_WIDTH = 320;
|
|
13
|
-
var MAX_SHOW_WIDTH = 3840;
|
|
14
80
|
function parsePort(flag, raw) {
|
|
15
81
|
if (!/^\d+$/.test(raw)) {
|
|
16
82
|
return { error: `${flag} needs a number, received ${JSON.stringify(raw)}.` };
|
|
17
83
|
}
|
|
18
84
|
const port = Number(raw);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
85
|
+
const refusal = portRefusal(flag, port);
|
|
86
|
+
if (refusal !== null) return { error: refusal };
|
|
22
87
|
return { port };
|
|
23
88
|
}
|
|
24
89
|
function parseNew(rest) {
|
|
@@ -30,9 +95,8 @@ function parseNew(rest) {
|
|
|
30
95
|
const argument = rest[index];
|
|
31
96
|
if (argument === "--from" || argument.startsWith("--from=")) {
|
|
32
97
|
from = argument.includes("=") ? argument.split("=").slice(1).join("=") : rest[index += 1];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
98
|
+
const refusal = fromRefusal(from ?? "");
|
|
99
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
36
100
|
continue;
|
|
37
101
|
}
|
|
38
102
|
if (argument === "--print") {
|
|
@@ -100,7 +164,7 @@ function parseAdd(rest) {
|
|
|
100
164
|
return { kind: "error", message: `leglas add does not take ${flag}.` };
|
|
101
165
|
}
|
|
102
166
|
if (value === void 0 || value === "") {
|
|
103
|
-
return { kind: "error", message:
|
|
167
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
104
168
|
}
|
|
105
169
|
if (flag === "--title") title = value;
|
|
106
170
|
else if (flag === "--url") url = value;
|
|
@@ -117,26 +181,19 @@ function parseAdd(rest) {
|
|
|
117
181
|
message: "leglas add needs --title, which is how the preview is identified."
|
|
118
182
|
};
|
|
119
183
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
title,
|
|
130
|
-
url,
|
|
131
|
-
note,
|
|
132
|
-
tags: tags.length > 0 ? tags : void 0,
|
|
133
|
-
branch,
|
|
134
|
-
file,
|
|
135
|
-
basedOn,
|
|
136
|
-
askedFor
|
|
137
|
-
},
|
|
138
|
-
json
|
|
184
|
+
const preview = {
|
|
185
|
+
title,
|
|
186
|
+
url,
|
|
187
|
+
note,
|
|
188
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
189
|
+
branch,
|
|
190
|
+
file,
|
|
191
|
+
basedOn,
|
|
192
|
+
askedFor
|
|
139
193
|
};
|
|
194
|
+
const refusal = addRefusal(preview);
|
|
195
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
196
|
+
return { kind: "add", preview, json };
|
|
140
197
|
}
|
|
141
198
|
function parseClassify(rest) {
|
|
142
199
|
const changes = [];
|
|
@@ -185,7 +242,7 @@ function parseWatch(rest) {
|
|
|
185
242
|
if (value === void 0 || value === "") {
|
|
186
243
|
return {
|
|
187
244
|
kind: "error",
|
|
188
|
-
message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' :
|
|
245
|
+
message: flag === "--run" ? '--run needs an agent command, for example --run "claude -p {prompt}"' : valueRefusal(flag)
|
|
189
246
|
};
|
|
190
247
|
}
|
|
191
248
|
if (flag === "--run") {
|
|
@@ -200,10 +257,12 @@ function parseWatch(rest) {
|
|
|
200
257
|
}
|
|
201
258
|
function parseShare(rest) {
|
|
202
259
|
const titles = [];
|
|
203
|
-
let reach =
|
|
260
|
+
let reach = DEFAULT_SHARE_REACH;
|
|
204
261
|
let reachGiven = false;
|
|
205
262
|
let tunnel = null;
|
|
206
263
|
let stop = false;
|
|
264
|
+
let rotate = false;
|
|
265
|
+
let revoke = null;
|
|
207
266
|
let port = null;
|
|
208
267
|
let json = false;
|
|
209
268
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -216,12 +275,22 @@ function parseShare(rest) {
|
|
|
216
275
|
stop = true;
|
|
217
276
|
continue;
|
|
218
277
|
}
|
|
278
|
+
if (argument === "--rotate") {
|
|
279
|
+
rotate = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
219
282
|
const equals = argument.indexOf("=");
|
|
220
283
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
284
|
+
if (flag === "--revoke") {
|
|
285
|
+
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
286
|
+
if (raw === void 0 || raw === "") return { kind: "error", message: valueRefusal(flag) };
|
|
287
|
+
revoke = raw;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
221
290
|
if (flag === "--reach" || flag === "--tunnel" || flag === "--port") {
|
|
222
291
|
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
223
292
|
if (raw === void 0 || raw === "") {
|
|
224
|
-
return { kind: "error", message:
|
|
293
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
225
294
|
}
|
|
226
295
|
if (flag === "--port") {
|
|
227
296
|
const parsed2 = parsePort(flag, raw);
|
|
@@ -255,19 +324,45 @@ function parseShare(rest) {
|
|
|
255
324
|
}
|
|
256
325
|
titles.push(argument);
|
|
257
326
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
327
|
+
const refusal = shareRefusal({
|
|
328
|
+
titles,
|
|
329
|
+
reach: reachGiven ? reach : void 0,
|
|
330
|
+
tunnel,
|
|
331
|
+
stop,
|
|
332
|
+
rotate,
|
|
333
|
+
revoke
|
|
334
|
+
});
|
|
335
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
336
|
+
return { kind: "share", titles, reach, tunnel, stop, rotate, revoke, port, json };
|
|
337
|
+
}
|
|
338
|
+
function parseLink(rest) {
|
|
339
|
+
const titles = [];
|
|
340
|
+
let port = null;
|
|
341
|
+
let json = false;
|
|
342
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
343
|
+
const argument = rest[index];
|
|
344
|
+
if (argument === "--json") {
|
|
345
|
+
json = true;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const equals = argument.indexOf("=");
|
|
349
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
350
|
+
if (flag === "--port") {
|
|
351
|
+
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
352
|
+
if (raw === void 0 || raw === "") return { kind: "error", message: valueRefusal(flag) };
|
|
353
|
+
const parsed2 = parsePort(flag, raw);
|
|
354
|
+
if ("error" in parsed2) return { kind: "error", message: parsed2.error };
|
|
355
|
+
port = parsed2.port;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (argument.startsWith("-")) {
|
|
359
|
+
return { kind: "error", message: `leglas link does not take ${argument}.` };
|
|
360
|
+
}
|
|
361
|
+
titles.push(argument);
|
|
269
362
|
}
|
|
270
|
-
|
|
363
|
+
const refusal = linkRefusal(titles);
|
|
364
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
365
|
+
return { kind: "link", titles, port, json };
|
|
271
366
|
}
|
|
272
367
|
function parseArgs(argv) {
|
|
273
368
|
const [command] = argv;
|
|
@@ -332,7 +427,7 @@ function parseArgs(argv) {
|
|
|
332
427
|
if (argv[0] === "explore") {
|
|
333
428
|
const rest = argv.slice(1);
|
|
334
429
|
let surface;
|
|
335
|
-
let count =
|
|
430
|
+
let count = DEFAULT_EXPLORE_COUNT;
|
|
336
431
|
let basedOn = null;
|
|
337
432
|
let json = false;
|
|
338
433
|
let build = false;
|
|
@@ -465,7 +560,7 @@ function parseArgs(argv) {
|
|
|
465
560
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
466
561
|
const raw = equals === -1 ? rest[index += 1] : argument.slice(equals + 1);
|
|
467
562
|
if (raw === void 0 || raw === "") {
|
|
468
|
-
return { kind: "error", message:
|
|
563
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
469
564
|
}
|
|
470
565
|
if (flag === "--port") {
|
|
471
566
|
const parsed2 = parsePort(flag, raw);
|
|
@@ -480,12 +575,8 @@ function parseArgs(argv) {
|
|
|
480
575
|
};
|
|
481
576
|
}
|
|
482
577
|
width = Number(raw);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
kind: "error",
|
|
486
|
-
message: `--width must be between ${MIN_SHOW_WIDTH} and ${MAX_SHOW_WIDTH}, received ${width}.`
|
|
487
|
-
};
|
|
488
|
-
}
|
|
578
|
+
const refusal2 = widthRefusal(width);
|
|
579
|
+
if (refusal2 !== null) return { kind: "error", message: refusal2 };
|
|
489
580
|
continue;
|
|
490
581
|
}
|
|
491
582
|
if (argument.startsWith("-")) {
|
|
@@ -502,15 +593,27 @@ function parseArgs(argv) {
|
|
|
502
593
|
message: 'leglas show needs a direction title, for example: npx leglas show "Aurora" --json'
|
|
503
594
|
};
|
|
504
595
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
if (port !== null && !screenshot) {
|
|
509
|
-
return { kind: "error", message: "leglas show --port needs --screenshot." };
|
|
510
|
-
}
|
|
596
|
+
const refusal = showRefusal({ screenshot, width, port });
|
|
597
|
+
if (refusal !== null) return { kind: "error", message: refusal };
|
|
511
598
|
return { kind: "show", title, json, screenshot, width, port };
|
|
512
599
|
}
|
|
513
600
|
if (argv[0] === "share") return parseShare(argv.slice(1));
|
|
601
|
+
if (argv[0] === "link") return parseLink(argv.slice(1));
|
|
602
|
+
if (argv[0] === "remove") {
|
|
603
|
+
const rest = argv.slice(1);
|
|
604
|
+
const unknown = rest.find((argument) => argument.startsWith("-") && argument !== "--json");
|
|
605
|
+
if (unknown !== void 0) {
|
|
606
|
+
return { kind: "error", message: `leglas remove does not take ${unknown}.` };
|
|
607
|
+
}
|
|
608
|
+
const titles = rest.filter((argument) => argument !== "--json");
|
|
609
|
+
if (titles.length === 0) {
|
|
610
|
+
return {
|
|
611
|
+
kind: "error",
|
|
612
|
+
message: 'leglas remove needs a direction title, for example: npx leglas remove "Aurora"'
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return { kind: "remove", titles, json: rest.includes("--json") };
|
|
616
|
+
}
|
|
514
617
|
const options = {
|
|
515
618
|
port: void 0,
|
|
516
619
|
userPort: void 0,
|
|
@@ -543,7 +646,7 @@ function parseArgs(argv) {
|
|
|
543
646
|
value = argument.slice(equals + 1);
|
|
544
647
|
}
|
|
545
648
|
if (value === void 0 || value === "" || value.startsWith("--")) {
|
|
546
|
-
return { kind: "error", message:
|
|
649
|
+
return { kind: "error", message: valueRefusal(flag) };
|
|
547
650
|
}
|
|
548
651
|
if (flag === "--config") {
|
|
549
652
|
options.configPath = value;
|
|
@@ -559,7 +662,7 @@ function parseArgs(argv) {
|
|
|
559
662
|
|
|
560
663
|
// src/run-classify.ts
|
|
561
664
|
import { stat as stat5 } from "fs/promises";
|
|
562
|
-
import { join as
|
|
665
|
+
import { join as join19 } from "path";
|
|
563
666
|
|
|
564
667
|
// ../server/dist/log.js
|
|
565
668
|
var DEFAULT_LOG_DIR = "design-log";
|
|
@@ -788,13 +891,18 @@ function normalizeConfig(raw, options = {}) {
|
|
|
788
891
|
if (!isBoolean(scanPreviews)) {
|
|
789
892
|
errors.push("scanPreviews must be a boolean.");
|
|
790
893
|
}
|
|
791
|
-
|
|
894
|
+
const recordSets = source["recordSets"] ?? true;
|
|
895
|
+
if (!isBoolean(recordSets)) {
|
|
896
|
+
errors.push("recordSets must be a boolean.");
|
|
897
|
+
}
|
|
898
|
+
if (errors.length > 0 || !isString(devServer) || !isBoolean(scanPreviews) || !isBoolean(recordSets) || !isString(installCommand) || !isString(logDir))
|
|
792
899
|
return { config: null, errors };
|
|
793
900
|
return {
|
|
794
901
|
config: {
|
|
795
902
|
devServer,
|
|
796
903
|
previews,
|
|
797
904
|
scanPreviews,
|
|
905
|
+
recordSets,
|
|
798
906
|
devCommand: isString(devCommand) ? devCommand : void 0,
|
|
799
907
|
installCommand,
|
|
800
908
|
logDir
|
|
@@ -887,9 +995,9 @@ function nextRequest(requests, failed) {
|
|
|
887
995
|
|
|
888
996
|
// ../server/dist/agents/agents.js
|
|
889
997
|
import { spawn } from "child_process";
|
|
890
|
-
import { constants, readdirSync } from "fs";
|
|
998
|
+
import { constants, readdirSync, realpathSync } from "fs";
|
|
891
999
|
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
892
|
-
import { delimiter, dirname, isAbsolute, join, relative } from "path";
|
|
1000
|
+
import { delimiter, dirname, isAbsolute, join, normalize, relative } from "path";
|
|
893
1001
|
var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
894
1002
|
var effortFlag = (effort) => effort === null ? [] : ["--effort", effort];
|
|
895
1003
|
var codexEffortConfig = (effort) => effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`];
|
|
@@ -1040,13 +1148,15 @@ var KNOWN_AGENTS = {
|
|
|
1040
1148
|
// the earlier turn.
|
|
1041
1149
|
activityVerified: true,
|
|
1042
1150
|
authArgs: ["status"],
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1151
|
+
// Both answers read from cursor-agent 2026.09.02, and both exit 0: "✓ Logged
|
|
1152
|
+
// in as <email>" and "Not logged in". The second contains the first's
|
|
1153
|
+
// words, so a "not logged in" or "not signed in" is read first.
|
|
1046
1154
|
authVerdict: (result) => {
|
|
1155
|
+
if (/not (logged|signed) in/i.test(result.stdout))
|
|
1156
|
+
return "signed-out";
|
|
1047
1157
|
if (/logged in|signed in/i.test(result.stdout))
|
|
1048
1158
|
return "ok";
|
|
1049
|
-
if (result.code !== 0 || /
|
|
1159
|
+
if (result.code !== 0 || /log in|sign in/i.test(result.stdout))
|
|
1050
1160
|
return "signed-out";
|
|
1051
1161
|
return "unknown";
|
|
1052
1162
|
}
|
|
@@ -1266,6 +1376,45 @@ function cursorActivity(event, cwd) {
|
|
|
1266
1376
|
const path = shownPath(args?.path, cwd);
|
|
1267
1377
|
return path === null ? `using ${tool}` : `using ${tool} on ${path}`;
|
|
1268
1378
|
}
|
|
1379
|
+
function editedFiles(agent, line, cwd) {
|
|
1380
|
+
let event;
|
|
1381
|
+
try {
|
|
1382
|
+
event = record(parseJson(line));
|
|
1383
|
+
} catch {
|
|
1384
|
+
return [];
|
|
1385
|
+
}
|
|
1386
|
+
const paths = [];
|
|
1387
|
+
if (agent === "claude" && event?.type === "assistant") {
|
|
1388
|
+
const content = record(event.message)?.content;
|
|
1389
|
+
for (const rawBlock of Array.isArray(content) ? content : []) {
|
|
1390
|
+
const block = record(rawBlock);
|
|
1391
|
+
const input = record(block?.input);
|
|
1392
|
+
if (block?.type === "tool_use" && isString(block.name) && ["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(block.name))
|
|
1393
|
+
paths.push(input?.file_path ?? input?.notebook_path ?? null);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (agent === "codex" && (event?.type === "item.started" || event?.type === "item.completed") && record(event.item)?.type === "file_change") {
|
|
1397
|
+
const item = record(event.item);
|
|
1398
|
+
for (const change of Array.isArray(item?.changes) ? item.changes : [])
|
|
1399
|
+
paths.push(record(change)?.path ?? null);
|
|
1400
|
+
paths.push(item?.path ?? null);
|
|
1401
|
+
}
|
|
1402
|
+
if (paths.length === 0)
|
|
1403
|
+
return [];
|
|
1404
|
+
let root = cwd;
|
|
1405
|
+
try {
|
|
1406
|
+
root = realpathSync(cwd);
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
return paths.flatMap((path) => {
|
|
1410
|
+
if (!isString(path) || path === "")
|
|
1411
|
+
return [];
|
|
1412
|
+
if (!isAbsolute(path))
|
|
1413
|
+
return [normalize(path)];
|
|
1414
|
+
const shown = [relative(cwd, path), relative(root, path)].find((form) => !form.startsWith(".."));
|
|
1415
|
+
return [normalize(shown ?? path)];
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1269
1418
|
function activityFrom(agent, line, cwd = process.cwd()) {
|
|
1270
1419
|
let event;
|
|
1271
1420
|
try {
|
|
@@ -1491,9 +1640,21 @@ async function loadConfig(cwd) {
|
|
|
1491
1640
|
}
|
|
1492
1641
|
|
|
1493
1642
|
// ../server/dist/config/local-previews.js
|
|
1494
|
-
import {
|
|
1643
|
+
import { randomUUID } from "crypto";
|
|
1644
|
+
import { mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1495
1645
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1496
1646
|
var LOCAL_PREVIEWS_PATH = ".leglas/previews.json";
|
|
1647
|
+
async function writeRegistry(path, stored) {
|
|
1648
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
1649
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1650
|
+
await writeFile2(temporary, `${JSON.stringify({ previews: stored }, null, 2)}
|
|
1651
|
+
`, "utf8");
|
|
1652
|
+
await rename(temporary, path).catch(async (cause) => {
|
|
1653
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
1654
|
+
});
|
|
1655
|
+
throw cause;
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1497
1658
|
async function readLocalPreviews(cwd) {
|
|
1498
1659
|
const path = join3(cwd, LOCAL_PREVIEWS_PATH);
|
|
1499
1660
|
let raw;
|
|
@@ -1565,10 +1726,10 @@ async function addLocalPreview(cwd, input, shared) {
|
|
|
1565
1726
|
if (check.config === null) {
|
|
1566
1727
|
return { ok: false, error: check.errors.join(" ") };
|
|
1567
1728
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1729
|
+
await writeRegistry(join3(cwd, LOCAL_PREVIEWS_PATH), [
|
|
1730
|
+
...existing.previews.map(toStored),
|
|
1731
|
+
candidate
|
|
1732
|
+
]);
|
|
1572
1733
|
return { ok: true };
|
|
1573
1734
|
}
|
|
1574
1735
|
function toStored(preview) {
|
|
@@ -1580,13 +1741,127 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
1580
1741
|
const keep = existing.previews.filter((preview) => !titles.includes(preview.title));
|
|
1581
1742
|
if (keep.length === existing.previews.length)
|
|
1582
1743
|
return 0;
|
|
1583
|
-
|
|
1584
|
-
await mkdir2(dirname3(path), { recursive: true });
|
|
1585
|
-
await writeFile2(path, `${JSON.stringify({ previews: keep.map(toStored) }, null, 2)}
|
|
1586
|
-
`, "utf8");
|
|
1744
|
+
await writeRegistry(join3(cwd, LOCAL_PREVIEWS_PATH), keep.map(toStored));
|
|
1587
1745
|
return existing.previews.length - keep.length;
|
|
1588
1746
|
}
|
|
1589
1747
|
|
|
1748
|
+
// ../server/dist/generation/record.js
|
|
1749
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1750
|
+
import { appendFile, mkdir as mkdir3, readdir, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
1751
|
+
import { join as join4 } from "path";
|
|
1752
|
+
var RECORDS_DIR = ".leglas/generations";
|
|
1753
|
+
var KEPT_RECORDS = 100;
|
|
1754
|
+
async function recordIds(cwd) {
|
|
1755
|
+
const names = await readdir(join4(cwd, RECORDS_DIR)).catch(() => []);
|
|
1756
|
+
const started = (id) => Number.parseInt(id.slice(4), 36);
|
|
1757
|
+
return names.filter((name) => /^gen-[a-z0-9]+$/.test(name)).sort((a, b) => started(b) - started(a));
|
|
1758
|
+
}
|
|
1759
|
+
async function directionsOf(cwd, id) {
|
|
1760
|
+
let parsed2;
|
|
1761
|
+
try {
|
|
1762
|
+
parsed2 = parseJson(await readFile4(join4(cwd, RECORDS_DIR, id, "set.json"), "utf8"));
|
|
1763
|
+
} catch {
|
|
1764
|
+
return [];
|
|
1765
|
+
}
|
|
1766
|
+
if (!isJsonRecord(parsed2) || !Array.isArray(parsed2.directions))
|
|
1767
|
+
return [];
|
|
1768
|
+
return parsed2.directions.flatMap((direction) => {
|
|
1769
|
+
if (!isJsonRecord(direction) || !isString(direction.key))
|
|
1770
|
+
return [];
|
|
1771
|
+
const attempts2 = Array.isArray(direction.attempts) ? direction.attempts : [];
|
|
1772
|
+
const last = attempts2.at(-1);
|
|
1773
|
+
return [
|
|
1774
|
+
{
|
|
1775
|
+
key: direction.key,
|
|
1776
|
+
title: isJsonRecord(last) && isString(last.title) ? last.title : null
|
|
1777
|
+
}
|
|
1778
|
+
];
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
async function appendEvent(cwd, id, event) {
|
|
1782
|
+
const dir = join4(cwd, RECORDS_DIR, id);
|
|
1783
|
+
await mkdir3(dir, { recursive: true });
|
|
1784
|
+
await appendFile(join4(dir, "events.jsonl"), `${JSON.stringify(event)}
|
|
1785
|
+
`, "utf8");
|
|
1786
|
+
}
|
|
1787
|
+
async function setsOfTitles(cwd, titles) {
|
|
1788
|
+
const found = /* @__PURE__ */ new Map();
|
|
1789
|
+
for (const id of await recordIds(cwd)) {
|
|
1790
|
+
if (found.size === titles.length)
|
|
1791
|
+
break;
|
|
1792
|
+
for (const direction of await directionsOf(cwd, id)) {
|
|
1793
|
+
const title = direction.title;
|
|
1794
|
+
if (title !== null && titles.includes(title) && !found.has(title))
|
|
1795
|
+
found.set(title, { id, key: direction.key });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return found;
|
|
1799
|
+
}
|
|
1800
|
+
async function setOfKey(cwd, key) {
|
|
1801
|
+
for (const id of await recordIds(cwd)) {
|
|
1802
|
+
if ((await directionsOf(cwd, id)).some((each) => each.key === key))
|
|
1803
|
+
return id;
|
|
1804
|
+
}
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
async function noteDirections(cwd, titles, kind, detail) {
|
|
1808
|
+
const unique = [...new Set(titles)];
|
|
1809
|
+
const found = await setsOfTitles(cwd, unique).catch(() => /* @__PURE__ */ new Map());
|
|
1810
|
+
for (const title of unique) {
|
|
1811
|
+
const set = found.get(title);
|
|
1812
|
+
if (set === void 0)
|
|
1813
|
+
continue;
|
|
1814
|
+
const event = { at: Date.now(), kind, direction: set.key };
|
|
1815
|
+
if (detail !== void 0)
|
|
1816
|
+
event.detail = detail;
|
|
1817
|
+
await appendEvent(cwd, set.id, event).catch(() => {
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function createRecords(cwd) {
|
|
1822
|
+
let queue = Promise.resolve();
|
|
1823
|
+
const enqueue = (task) => {
|
|
1824
|
+
queue = queue.then(task).catch(() => {
|
|
1825
|
+
});
|
|
1826
|
+
};
|
|
1827
|
+
return {
|
|
1828
|
+
async prune() {
|
|
1829
|
+
const ids = await recordIds(cwd);
|
|
1830
|
+
for (const id of ids.slice(KEPT_RECORDS - 1)) {
|
|
1831
|
+
await rm2(join4(cwd, RECORDS_DIR, id), { recursive: true, force: true }).catch(() => {
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
},
|
|
1835
|
+
save(record3) {
|
|
1836
|
+
const body = `${JSON.stringify(record3, null, 2)}
|
|
1837
|
+
`;
|
|
1838
|
+
enqueue(async () => {
|
|
1839
|
+
const dir = join4(cwd, RECORDS_DIR, record3.id);
|
|
1840
|
+
const path = join4(dir, "set.json");
|
|
1841
|
+
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
1842
|
+
await mkdir3(dir, { recursive: true });
|
|
1843
|
+
await writeFile3(temporary, body, "utf8");
|
|
1844
|
+
await rename2(temporary, path).catch(async (cause) => {
|
|
1845
|
+
await rm2(temporary, { force: true }).catch(() => {
|
|
1846
|
+
});
|
|
1847
|
+
throw cause;
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
},
|
|
1851
|
+
picture(id, name, image) {
|
|
1852
|
+
enqueue(async () => {
|
|
1853
|
+
const dir = join4(cwd, RECORDS_DIR, id);
|
|
1854
|
+
await mkdir3(dir, { recursive: true });
|
|
1855
|
+
await writeFile3(join4(dir, name), image);
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
event(id, event) {
|
|
1859
|
+
enqueue(() => appendEvent(cwd, id, event));
|
|
1860
|
+
},
|
|
1861
|
+
flush: () => queue
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1590
1865
|
// ../server/dist/proxy.js
|
|
1591
1866
|
import http, {} from "http";
|
|
1592
1867
|
import net from "net";
|
|
@@ -1755,9 +2030,9 @@ import { randomBytes } from "crypto";
|
|
|
1755
2030
|
import nodeProcess from "process";
|
|
1756
2031
|
import { spawn as nodeSpawn } from "child_process";
|
|
1757
2032
|
import { accessSync, constants as constants2, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, writeFileSync } from "fs";
|
|
1758
|
-
import { readFile as
|
|
2033
|
+
import { readFile as readFile5, readdir as readdir2, rm as rm3, stat } from "fs/promises";
|
|
1759
2034
|
import { homedir, tmpdir as osTmpdir } from "os";
|
|
1760
|
-
import { delimiter as delimiter2, join as
|
|
2035
|
+
import { delimiter as delimiter2, join as join5 } from "path";
|
|
1761
2036
|
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.";
|
|
1762
2037
|
var DARWIN_APPS = [
|
|
1763
2038
|
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -1790,7 +2065,7 @@ function firstOnPath(name, env) {
|
|
|
1790
2065
|
for (const directory of (env.PATH ?? "").split(delimiter2)) {
|
|
1791
2066
|
if (directory === "")
|
|
1792
2067
|
continue;
|
|
1793
|
-
const candidate =
|
|
2068
|
+
const candidate = join5(directory, name);
|
|
1794
2069
|
try {
|
|
1795
2070
|
accessSync(candidate, constants2.X_OK);
|
|
1796
2071
|
return candidate;
|
|
@@ -1828,17 +2103,17 @@ function buildNumber(entry) {
|
|
|
1828
2103
|
const digits = /(\d+)\s*$/.exec(entry)?.[1];
|
|
1829
2104
|
return digits === void 0 ? 0 : Number(digits);
|
|
1830
2105
|
}
|
|
1831
|
-
function cacheRoots(platform, home,
|
|
1832
|
-
const playwright = platform === "darwin" ?
|
|
1833
|
-
const puppeteer =
|
|
2106
|
+
function cacheRoots(platform, home, readdir8 = readableDirectories) {
|
|
2107
|
+
const playwright = platform === "darwin" ? join5(home, "Library", "Caches", "ms-playwright") : join5(home, ".cache", "ms-playwright");
|
|
2108
|
+
const puppeteer = join5(home, ".cache", "puppeteer");
|
|
1834
2109
|
const newestFirst = (dir, keep = () => true) => ({
|
|
1835
2110
|
root: dir,
|
|
1836
|
-
entries:
|
|
2111
|
+
entries: readdir8(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
|
|
1837
2112
|
});
|
|
1838
2113
|
return [
|
|
1839
2114
|
newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
|
|
1840
|
-
newestFirst(
|
|
1841
|
-
newestFirst(
|
|
2115
|
+
newestFirst(join5(puppeteer, "chrome-headless-shell")),
|
|
2116
|
+
newestFirst(join5(puppeteer, "chrome"))
|
|
1842
2117
|
];
|
|
1843
2118
|
}
|
|
1844
2119
|
function readableDirectories(dir) {
|
|
@@ -1854,18 +2129,18 @@ function findBrowser(search = {}) {
|
|
|
1854
2129
|
const home = search.home ?? homedir();
|
|
1855
2130
|
const exists = search.exists ?? existsSync2;
|
|
1856
2131
|
const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
|
|
1857
|
-
const
|
|
2132
|
+
const readdir8 = search.readdir ?? readableDirectories;
|
|
1858
2133
|
const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
|
|
1859
2134
|
for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
|
|
1860
2135
|
if (isString(candidate) && candidate !== "" && exists(candidate))
|
|
1861
2136
|
return candidate;
|
|
1862
2137
|
}
|
|
1863
|
-
const caches = cacheRoots(platform, home,
|
|
1864
|
-
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) =>
|
|
2138
|
+
const caches = cacheRoots(platform, home, readdir8);
|
|
2139
|
+
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join5(root, entry, ...rest)))));
|
|
1865
2140
|
if (shell !== null)
|
|
1866
2141
|
return shell;
|
|
1867
2142
|
if (platform === "darwin") {
|
|
1868
|
-
const installed = firstExisting(["/Applications",
|
|
2143
|
+
const installed = firstExisting(["/Applications", join5(home, "Applications")].flatMap((root) => DARWIN_APPS.map((app) => join5(root, app))));
|
|
1869
2144
|
if (installed !== null)
|
|
1870
2145
|
return installed;
|
|
1871
2146
|
}
|
|
@@ -1876,7 +2151,7 @@ function findBrowser(search = {}) {
|
|
|
1876
2151
|
return found;
|
|
1877
2152
|
}
|
|
1878
2153
|
const installed = firstExisting([
|
|
1879
|
-
...LINUX_NAMES.flatMap((name) => [
|
|
2154
|
+
...LINUX_NAMES.flatMap((name) => [join5("/usr/bin", name), join5("/snap/bin", name)]),
|
|
1880
2155
|
"/opt/google/chrome/chrome"
|
|
1881
2156
|
]);
|
|
1882
2157
|
if (installed !== null)
|
|
@@ -1884,32 +2159,32 @@ function findBrowser(search = {}) {
|
|
|
1884
2159
|
}
|
|
1885
2160
|
if (platform === "win32") {
|
|
1886
2161
|
const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter((entry) => isString(entry) && entry !== "");
|
|
1887
|
-
const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) =>
|
|
2162
|
+
const installed = firstExisting(roots.flatMap((root) => WINDOWS_BROWSERS.map((browser) => join5(root, browser))));
|
|
1888
2163
|
if (installed !== null)
|
|
1889
2164
|
return installed;
|
|
1890
2165
|
}
|
|
1891
2166
|
if (platform === "darwin" || platform === "linux") {
|
|
1892
|
-
const playwrightRoot = platform === "darwin" ?
|
|
1893
|
-
const playwright =
|
|
1894
|
-
const root =
|
|
2167
|
+
const playwrightRoot = platform === "darwin" ? join5(home, "Library", "Caches", "ms-playwright") : join5(home, ".cache", "ms-playwright");
|
|
2168
|
+
const playwright = readdir8(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2169
|
+
const root = join5(playwrightRoot, entry);
|
|
1895
2170
|
return [
|
|
1896
|
-
...FOR_TESTING.map((rest) =>
|
|
1897
|
-
...HEADLESS_SHELL.map((rest) =>
|
|
2171
|
+
...FOR_TESTING.map((rest) => join5(root, ...rest)),
|
|
2172
|
+
...HEADLESS_SHELL.map((rest) => join5(root, ...rest)),
|
|
1898
2173
|
// Older builds shipped a plain Chromium app.
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2174
|
+
join5(root, "chrome-mac-arm64", "Chromium.app", "Contents", "MacOS", "Chromium"),
|
|
2175
|
+
join5(root, "chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium"),
|
|
2176
|
+
join5(root, "chrome-linux", "chrome")
|
|
1902
2177
|
];
|
|
1903
2178
|
});
|
|
1904
2179
|
const playwrightBrowser = firstExisting(playwright);
|
|
1905
2180
|
if (playwrightBrowser !== null)
|
|
1906
2181
|
return playwrightBrowser;
|
|
1907
|
-
const puppeteerCache =
|
|
2182
|
+
const puppeteerCache = join5(home, ".cache", "puppeteer");
|
|
1908
2183
|
for (const kind of ["chrome", "chrome-headless-shell"]) {
|
|
1909
|
-
const kindRoot =
|
|
1910
|
-
const found = firstExisting(
|
|
1911
|
-
const root =
|
|
1912
|
-
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) =>
|
|
2184
|
+
const kindRoot = join5(puppeteerCache, kind);
|
|
2185
|
+
const found = firstExisting(readdir8(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2186
|
+
const root = join5(kindRoot, entry);
|
|
2187
|
+
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join5(root, ...rest));
|
|
1913
2188
|
}));
|
|
1914
2189
|
if (found !== null)
|
|
1915
2190
|
return found;
|
|
@@ -2034,8 +2309,8 @@ async function closeOrphan(url, connect) {
|
|
|
2034
2309
|
async function reapOrphanedBrowsers(deps = {}) {
|
|
2035
2310
|
const root = deps.tmpdir ?? osTmpdir();
|
|
2036
2311
|
const clock2 = deps.now ?? (() => Date.now());
|
|
2037
|
-
const list = deps.list ?? (async (path) => (await
|
|
2038
|
-
const read = deps.read ?? ((path) =>
|
|
2312
|
+
const list = deps.list ?? (async (path) => (await readdir2(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
|
|
2313
|
+
const read = deps.read ?? ((path) => readFile5(path, "utf8"));
|
|
2039
2314
|
const alive = deps.alive ?? livePid;
|
|
2040
2315
|
const profileOf = deps.profile ?? (async (path) => {
|
|
2041
2316
|
const entry = await stat(path);
|
|
@@ -2044,7 +2319,7 @@ async function reapOrphanedBrowsers(deps = {}) {
|
|
|
2044
2319
|
const currentUid = deps.uid ?? (() => nodeProcess.getuid?.() ?? null);
|
|
2045
2320
|
const connect = deps.connect ?? connectWebSocket;
|
|
2046
2321
|
const remove = deps.remove ?? (async (path) => {
|
|
2047
|
-
await
|
|
2322
|
+
await rm3(path, { recursive: true, force: true });
|
|
2048
2323
|
});
|
|
2049
2324
|
let names;
|
|
2050
2325
|
try {
|
|
@@ -2057,7 +2332,7 @@ async function reapOrphanedBrowsers(deps = {}) {
|
|
|
2057
2332
|
for (const name of names) {
|
|
2058
2333
|
if (!name.startsWith(PROFILE_PREFIX))
|
|
2059
2334
|
continue;
|
|
2060
|
-
const directory =
|
|
2335
|
+
const directory = join5(root, name);
|
|
2061
2336
|
const details = await profileOf(directory).catch(() => null);
|
|
2062
2337
|
if (details === null)
|
|
2063
2338
|
continue;
|
|
@@ -2065,7 +2340,7 @@ async function reapOrphanedBrowsers(deps = {}) {
|
|
|
2065
2340
|
continue;
|
|
2066
2341
|
let record3 = null;
|
|
2067
2342
|
try {
|
|
2068
|
-
const raw = await read(
|
|
2343
|
+
const raw = await read(join5(directory, OWNER_FILE));
|
|
2069
2344
|
const parsed2 = raw === null ? null : parseJson(raw);
|
|
2070
2345
|
record3 = isJsonRecord(parsed2) ? parsed2 : null;
|
|
2071
2346
|
} catch {
|
|
@@ -2087,11 +2362,11 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2087
2362
|
const spawn7 = options.spawn ?? nodeSpawn;
|
|
2088
2363
|
const connect = options.connect ?? connectWebSocket;
|
|
2089
2364
|
const commandTimeoutMs = options.commandTimeoutMs ?? 3e4;
|
|
2090
|
-
const userDataDir =
|
|
2365
|
+
const userDataDir = join5(options.tmpdir ?? osTmpdir(), `leglas-browser-${randomBytes(8).toString("hex")}`);
|
|
2091
2366
|
const owned = (fields) => {
|
|
2092
2367
|
try {
|
|
2093
2368
|
mkdirSync(userDataDir, { recursive: true, mode: 448 });
|
|
2094
|
-
writeFileSync(
|
|
2369
|
+
writeFileSync(join5(userDataDir, OWNER_FILE), `${JSON.stringify({ owner: nodeProcess.pid, ...fields })}
|
|
2095
2370
|
`, { encoding: "utf8", mode: 384 });
|
|
2096
2371
|
} catch {
|
|
2097
2372
|
}
|
|
@@ -2111,11 +2386,10 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2111
2386
|
"--mute-audio",
|
|
2112
2387
|
"--force-color-profile=srgb",
|
|
2113
2388
|
"--window-size=1440,900",
|
|
2114
|
-
//
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
//
|
|
2118
|
-
// otherwise reaches for the login keychain before it will start.
|
|
2389
|
+
// Desktop startup work a screenshot doesn't need. Measured here: about
|
|
2390
|
+
// 300MB and half a second off a Chrome launch, most of the wait off a
|
|
2391
|
+
// headless shell. `--use-mock-keychain` matters most on macOS, where
|
|
2392
|
+
// Chrome otherwise waits on the login keychain.
|
|
2119
2393
|
"--disable-dev-shm-usage",
|
|
2120
2394
|
"--disable-breakpad",
|
|
2121
2395
|
"--disable-client-side-phishing-detection",
|
|
@@ -2132,7 +2406,7 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2132
2406
|
try {
|
|
2133
2407
|
websocketUrl = await endpoint(process2, options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
2134
2408
|
} catch (error) {
|
|
2135
|
-
await
|
|
2409
|
+
await rm3(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
2136
2410
|
});
|
|
2137
2411
|
throw error;
|
|
2138
2412
|
}
|
|
@@ -2142,7 +2416,7 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2142
2416
|
socket = await connect(websocketUrl);
|
|
2143
2417
|
} catch (error) {
|
|
2144
2418
|
process2.kill("SIGKILL");
|
|
2145
|
-
await
|
|
2419
|
+
await rm3(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
2146
2420
|
});
|
|
2147
2421
|
throw error;
|
|
2148
2422
|
}
|
|
@@ -2295,7 +2569,7 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2295
2569
|
]);
|
|
2296
2570
|
}
|
|
2297
2571
|
socket.close();
|
|
2298
|
-
await
|
|
2572
|
+
await rm3(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
2299
2573
|
});
|
|
2300
2574
|
})();
|
|
2301
2575
|
return closing;
|
|
@@ -2303,8 +2577,8 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2303
2577
|
return {
|
|
2304
2578
|
withPage,
|
|
2305
2579
|
close,
|
|
2306
|
-
// A socket
|
|
2307
|
-
//
|
|
2580
|
+
// A socket gone with the process alive is the same to a caller: the pool
|
|
2581
|
+
// must not hand it out.
|
|
2308
2582
|
get closed() {
|
|
2309
2583
|
return processClosed || socketClosed;
|
|
2310
2584
|
}
|
|
@@ -2448,6 +2722,367 @@ function hydrationEvidence(messages) {
|
|
|
2448
2722
|
return null;
|
|
2449
2723
|
}
|
|
2450
2724
|
|
|
2725
|
+
// ../server/dist/capture/layout.js
|
|
2726
|
+
function isEdge(value) {
|
|
2727
|
+
return value === "left" || value === "right" || value === "top" || value === "bottom";
|
|
2728
|
+
}
|
|
2729
|
+
var LAYOUT_PROBE = `(() => {
|
|
2730
|
+
const OVERLAP_SHARE = 0.05;
|
|
2731
|
+
const OVERLAP_MIN = 1;
|
|
2732
|
+
const CUT_MIN = 3;
|
|
2733
|
+
const CUT_SHARE = 0.08;
|
|
2734
|
+
const LAYERED = 4;
|
|
2735
|
+
const TILT = 6;
|
|
2736
|
+
// Only a page gone wrong everywhere reaches this. The app's own findings are
|
|
2737
|
+
// subtracted after, so a low cap could crowd out a direction's.
|
|
2738
|
+
const LIMIT = 100;
|
|
2739
|
+
const width = window.innerWidth;
|
|
2740
|
+
const states = new Map();
|
|
2741
|
+
const inks = new Map();
|
|
2742
|
+
const context = document.createElement("canvas").getContext("2d");
|
|
2743
|
+
|
|
2744
|
+
// rgba(r, g, b, a), or a / before the alpha in the newer functions
|
|
2745
|
+
// (oklab, oklch, color), which computed colours keep.
|
|
2746
|
+
const alpha = (colour) => {
|
|
2747
|
+
if (colour === "transparent") return 0;
|
|
2748
|
+
const slash = /\\/\\s*([\\d.]+)(%?)\\s*\\)$/.exec(colour || "");
|
|
2749
|
+
if (slash) return parseFloat(slash[1]) / (slash[2] ? 100 : 1);
|
|
2750
|
+
const match = /rgba\\(([^)]+)\\)/.exec(colour || "");
|
|
2751
|
+
const parts = match ? match[1].split(/[\\s,]+/).filter(Boolean) : [];
|
|
2752
|
+
return parts.length > 3 ? parseFloat(parts[3]) : 1;
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
// Degrees this element turns its content by, or Infinity for a skew or a
|
|
2756
|
+
// 3D transform, whose boxes say nothing useful.
|
|
2757
|
+
const angleOf = (a, b, c, d) =>
|
|
2758
|
+
Math.abs(a * c + b * d) > 0.01 ? Infinity : (Math.atan2(b, a) * 180) / Math.PI;
|
|
2759
|
+
|
|
2760
|
+
const turnOf = (element, style) => {
|
|
2761
|
+
let turn = 0;
|
|
2762
|
+
if (style.rotate && style.rotate !== "none") turn += parseFloat(style.rotate) || 0;
|
|
2763
|
+
const transform = style.transform;
|
|
2764
|
+
if (transform && transform !== "none") {
|
|
2765
|
+
if (transform.startsWith("matrix3d")) return Infinity;
|
|
2766
|
+
const [a, b, c, d] = transform.slice(7, -1).split(",").map(Number);
|
|
2767
|
+
turn += angleOf(a, b, c, d);
|
|
2768
|
+
}
|
|
2769
|
+
return turn;
|
|
2770
|
+
};
|
|
2771
|
+
|
|
2772
|
+
const looping = new Set();
|
|
2773
|
+
try {
|
|
2774
|
+
for (const animation of document.getAnimations()) {
|
|
2775
|
+
const timing = animation.effect && animation.effect.getComputedTiming();
|
|
2776
|
+
const target = animation.effect && animation.effect.target;
|
|
2777
|
+
if (timing && timing.iterations === Infinity && target) looping.add(target);
|
|
2778
|
+
}
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
// An engine without getAnimations; nothing is excluded for it.
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
const stateOf = (element) => {
|
|
2784
|
+
if (states.has(element)) return states.get(element);
|
|
2785
|
+
const style = getComputedStyle(element);
|
|
2786
|
+
const parent = element.parentElement ? stateOf(element.parentElement) : null;
|
|
2787
|
+
const clipsText = /text/.test(style.backgroundClip || "") || /text/.test(style.webkitBackgroundClip || "");
|
|
2788
|
+
const state = {
|
|
2789
|
+
style,
|
|
2790
|
+
opacity: (parent ? parent.opacity : 1) * parseFloat(style.opacity || "1"),
|
|
2791
|
+
turn: element instanceof SVGGraphicsElement && element.getCTM
|
|
2792
|
+
? (() => {
|
|
2793
|
+
const matrix = element.getCTM();
|
|
2794
|
+
return matrix ? angleOf(matrix.a, matrix.b, matrix.c, matrix.d) + (parent ? parent.turnOutsideSvg : 0) : 0;
|
|
2795
|
+
})()
|
|
2796
|
+
: (parent ? parent.turn : 0) + turnOf(element, style),
|
|
2797
|
+
// An <svg>'s own CSS transform is outside every getCTM beneath it.
|
|
2798
|
+
turnOutsideSvg: element.namespaceURI === "http://www.w3.org/2000/svg" && element.ownerSVGElement
|
|
2799
|
+
? (parent ? parent.turnOutsideSvg : 0)
|
|
2800
|
+
: (parent ? parent.turn : 0) + turnOf(element, style),
|
|
2801
|
+
masked:
|
|
2802
|
+
(parent ? parent.masked : false) ||
|
|
2803
|
+
(style.clipPath && style.clipPath !== "none") ||
|
|
2804
|
+
(style.maskImage && style.maskImage !== "none") ||
|
|
2805
|
+
(style.webkitMaskImage && style.webkitMaskImage !== "none"),
|
|
2806
|
+
truncates:
|
|
2807
|
+
(parent ? parent.truncates : false) ||
|
|
2808
|
+
style.textOverflow === "ellipsis" ||
|
|
2809
|
+
(style.webkitLineClamp && style.webkitLineClamp !== "none"),
|
|
2810
|
+
moving: (parent ? parent.moving : false) || looping.has(element),
|
|
2811
|
+
paintsText: (parent ? parent.paintsText : false) || clipsText,
|
|
2812
|
+
};
|
|
2813
|
+
states.set(element, state);
|
|
2814
|
+
return state;
|
|
2815
|
+
};
|
|
2816
|
+
|
|
2817
|
+
const blockOf = (element) => {
|
|
2818
|
+
if (element.namespaceURI === "http://www.w3.org/2000/svg") return element.closest("text") || element;
|
|
2819
|
+
for (let node = element; node; node = node.parentElement) {
|
|
2820
|
+
const display = stateOf(node).style.display;
|
|
2821
|
+
if (display !== "inline" && display !== "contents") return node;
|
|
2822
|
+
}
|
|
2823
|
+
return document.body;
|
|
2824
|
+
};
|
|
2825
|
+
|
|
2826
|
+
const set = (value) => Boolean(value) && value !== "none" && value !== "normal";
|
|
2827
|
+
|
|
2828
|
+
const establishes = (style) =>
|
|
2829
|
+
set(style.transform) ||
|
|
2830
|
+
set(style.scale) ||
|
|
2831
|
+
set(style.rotate) ||
|
|
2832
|
+
set(style.translate) ||
|
|
2833
|
+
set(style.perspective) ||
|
|
2834
|
+
set(style.filter) ||
|
|
2835
|
+
set(style.backdropFilter) ||
|
|
2836
|
+
set(style.containerType) ||
|
|
2837
|
+
style.contentVisibility === "auto" ||
|
|
2838
|
+
/transform|perspective|filter|scale|rotate|translate/.test(style.willChange || "") ||
|
|
2839
|
+
/paint|layout|strict|content/.test(style.contain || "");
|
|
2840
|
+
|
|
2841
|
+
const clipRect = (element, style) => {
|
|
2842
|
+
const match = /rect\\(([^)]+)\\)/.exec(style.clip || "");
|
|
2843
|
+
if (!match || (style.position !== "absolute" && style.position !== "fixed")) return null;
|
|
2844
|
+
const outer = element.getBoundingClientRect();
|
|
2845
|
+
const [top, right, bottom, left] = match[1].split(/[\\s,]+/).filter(Boolean);
|
|
2846
|
+
const at = (value, fallback, origin) => (value === "auto" ? fallback : origin + parseFloat(value));
|
|
2847
|
+
return {
|
|
2848
|
+
left: at(left, outer.left, outer.left),
|
|
2849
|
+
right: at(right, outer.right, outer.left),
|
|
2850
|
+
top: at(top, outer.top, outer.top),
|
|
2851
|
+
bottom: at(bottom, outer.bottom, outer.top),
|
|
2852
|
+
page: false,
|
|
2853
|
+
cutsX: true,
|
|
2854
|
+
cutsY: true,
|
|
2855
|
+
};
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
// With the root's overflow visible, the body's overflow is the viewport's,
|
|
2859
|
+
// which the page clip already is, and the body's own box clips nothing.
|
|
2860
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
2861
|
+
const bodyIsViewport = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
|
|
2862
|
+
|
|
2863
|
+
// Every box that clips the text: its own element first. A positioned box
|
|
2864
|
+
// escapes every clipping box between it and its containing block.
|
|
2865
|
+
const clippers = (element) => {
|
|
2866
|
+
const found = [{ left: 0, right: width, top: 0, bottom: Infinity, page: true, cutsX: true, cutsY: true }];
|
|
2867
|
+
let escaping = null;
|
|
2868
|
+
for (let node = element; node && node !== document.documentElement; node = node.parentElement) {
|
|
2869
|
+
const style = stateOf(node).style;
|
|
2870
|
+
if (node !== element) {
|
|
2871
|
+
if (escaping === "absolute" && (style.position !== "static" || establishes(style))) escaping = null;
|
|
2872
|
+
if (escaping === "fixed" && establishes(style)) escaping = null;
|
|
2873
|
+
}
|
|
2874
|
+
if (escaping === null) {
|
|
2875
|
+
// A scrolling box hides what is scrolled away but cuts nothing off: the
|
|
2876
|
+
// rest is a scroll away. The document's own scroller hides nothing.
|
|
2877
|
+
const root = node === document.body;
|
|
2878
|
+
const hides = (value) => !(root && bodyIsViewport) && (value === "hidden" || value === "clip");
|
|
2879
|
+
const scrolls = (value) => !root && (value === "auto" || value === "scroll");
|
|
2880
|
+
const x = hides(style.overflowX) || scrolls(style.overflowX);
|
|
2881
|
+
const y = hides(style.overflowY) || scrolls(style.overflowY);
|
|
2882
|
+
if (x || y) {
|
|
2883
|
+
// The client box is measured before transforms, the bounding box after.
|
|
2884
|
+
const outer = node.getBoundingClientRect();
|
|
2885
|
+
const wide = typeof node.offsetWidth === "number" ? node.offsetWidth : node.clientWidth + 2 * node.clientLeft;
|
|
2886
|
+
const tall = typeof node.offsetHeight === "number" ? node.offsetHeight : node.clientHeight + 2 * node.clientTop;
|
|
2887
|
+
const across = wide > 0 ? outer.width / wide : 1;
|
|
2888
|
+
const down = tall > 0 ? outer.height / tall : 1;
|
|
2889
|
+
const left = outer.left + node.clientLeft * across;
|
|
2890
|
+
const top = outer.top + node.clientTop * down;
|
|
2891
|
+
found.push({
|
|
2892
|
+
left: x ? left : -Infinity,
|
|
2893
|
+
right: x ? left + node.clientWidth * across : Infinity,
|
|
2894
|
+
top: y ? top : -Infinity,
|
|
2895
|
+
bottom: y ? top + node.clientHeight * down : Infinity,
|
|
2896
|
+
page: false,
|
|
2897
|
+
cutsX: hides(style.overflowX),
|
|
2898
|
+
cutsY: hides(style.overflowY),
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
const clipped = clipRect(node, style);
|
|
2902
|
+
if (clipped) found.push(clipped);
|
|
2903
|
+
}
|
|
2904
|
+
if (style.position === "absolute" || style.position === "fixed") escaping = style.position;
|
|
2905
|
+
}
|
|
2906
|
+
return found;
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// The box around the letters themselves: a font's line box reaches past its
|
|
2910
|
+
// ink, and display type set tight overlaps line to line without touching.
|
|
2911
|
+
const inkOf = (words, style) => {
|
|
2912
|
+
const shown = style.textTransform === "uppercase" ? words.toUpperCase() : words;
|
|
2913
|
+
const font = [style.fontStyle, style.fontWeight, style.fontSize, style.fontFamily].join(" ");
|
|
2914
|
+
const key = font + "\\u0000" + style.letterSpacing + "\\u0000" + shown;
|
|
2915
|
+
if (inks.has(key)) return inks.get(key);
|
|
2916
|
+
let ink = null;
|
|
2917
|
+
if (context) {
|
|
2918
|
+
context.font = font;
|
|
2919
|
+
if ("letterSpacing" in context) context.letterSpacing = style.letterSpacing === "normal" ? "0px" : style.letterSpacing;
|
|
2920
|
+
const measured = context.measureText(shown);
|
|
2921
|
+
if (typeof measured.fontBoundingBoxAscent === "number") {
|
|
2922
|
+
ink = {
|
|
2923
|
+
top: Math.max(0, measured.fontBoundingBoxAscent - measured.actualBoundingBoxAscent),
|
|
2924
|
+
bottom: Math.max(0, measured.fontBoundingBoxDescent - measured.actualBoundingBoxDescent),
|
|
2925
|
+
left: -measured.actualBoundingBoxLeft,
|
|
2926
|
+
right: measured.actualBoundingBoxRight,
|
|
2927
|
+
height: measured.fontBoundingBoxAscent + measured.fontBoundingBoxDescent,
|
|
2928
|
+
advance: measured.width,
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
inks.set(key, ink);
|
|
2933
|
+
return ink;
|
|
2934
|
+
};
|
|
2935
|
+
|
|
2936
|
+
const area = (box) => Math.max(0, box.right - box.left) * Math.max(0, box.bottom - box.top);
|
|
2937
|
+
|
|
2938
|
+
const texts = [];
|
|
2939
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
2940
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
2941
|
+
const words = node.nodeValue.replace(/\\s+/g, " ").trim();
|
|
2942
|
+
const element = node.parentElement;
|
|
2943
|
+
if (words === "" || !element || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(element.tagName)) continue;
|
|
2944
|
+
const state = stateOf(element);
|
|
2945
|
+
// A slight tilt keeps its box close to its letters; more and the box lies.
|
|
2946
|
+
const tilt = Math.abs((((state.turn % 360) + 540) % 360) - 180);
|
|
2947
|
+
if (state.style.visibility !== "visible" || state.opacity < 0.05 || state.masked || !(tilt <= TILT)) continue;
|
|
2948
|
+
if (alpha(state.style.color) < 0.05 && alpha(state.style.webkitTextFillColor) < 0.05 && !state.paintsText) continue;
|
|
2949
|
+
const range = document.createRange();
|
|
2950
|
+
range.selectNodeContents(node);
|
|
2951
|
+
const ink = inkOf(words, state.style);
|
|
2952
|
+
const clips = clippers(element);
|
|
2953
|
+
const boxes = [];
|
|
2954
|
+
const lines = [...range.getClientRects()].filter((raw) => raw.width >= 2 && raw.height >= 2);
|
|
2955
|
+
const rtl = state.style.direction === "rtl";
|
|
2956
|
+
for (const raw of lines) {
|
|
2957
|
+
// Scaled type (an SVG viewBox, a scale transform) draws at another size
|
|
2958
|
+
// than its computed font-size; the box's own height says by how much.
|
|
2959
|
+
const scale = ink && ink.height > 0 ? raw.height / ink.height : 1;
|
|
2960
|
+
const top = ink ? ink.top * scale : 0;
|
|
2961
|
+
const bottom = ink ? ink.bottom * scale : 0;
|
|
2962
|
+
const box = raw.height - top - bottom >= 1
|
|
2963
|
+
? { left: raw.left, right: raw.right, top: raw.top + top, bottom: raw.bottom - bottom }
|
|
2964
|
+
: { left: raw.left, right: raw.right, top: raw.top, bottom: raw.bottom };
|
|
2965
|
+
// Across, the ink is known only when the whole string is on this one line,
|
|
2966
|
+
// and a line can be stretched one way only (scaleX).
|
|
2967
|
+
const wide = ink && ink.advance > 0 ? raw.width / ink.advance : scale;
|
|
2968
|
+
if (ink && lines.length === 1 && !rtl && (ink.right - ink.left) * wide >= 1) {
|
|
2969
|
+
box.left = Math.max(box.left, raw.left + ink.left * wide);
|
|
2970
|
+
box.right = Math.min(box.right, raw.left + ink.right * wide);
|
|
2971
|
+
}
|
|
2972
|
+
const seen = { ...box };
|
|
2973
|
+
for (const clip of clips) {
|
|
2974
|
+
seen.left = Math.max(seen.left, clip.left);
|
|
2975
|
+
seen.right = Math.min(seen.right, clip.right);
|
|
2976
|
+
seen.top = Math.max(seen.top, clip.top);
|
|
2977
|
+
seen.bottom = Math.min(seen.bottom, clip.bottom);
|
|
2978
|
+
}
|
|
2979
|
+
const shows = seen.right - seen.left >= 2 && seen.bottom - seen.top >= 2;
|
|
2980
|
+
boxes.push({ box, seen, shows, share: area(box) === 0 ? 0 : area(seen) / area(box), clips });
|
|
2981
|
+
}
|
|
2982
|
+
if (boxes.length === 0) continue;
|
|
2983
|
+
const drawn = ink && ink.height > 0 ? lines[0].height / ink.height : 1;
|
|
2984
|
+
const size = (parseFloat(state.style.fontSize) || 16) * drawn;
|
|
2985
|
+
texts.push({ words: words.slice(0, 80), boxes, block: blockOf(element), state, size });
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
const findings = [];
|
|
2989
|
+
const seen = new Set();
|
|
2990
|
+
|
|
2991
|
+
// Collisions between what shows of each box, swept by top edge.
|
|
2992
|
+
const shown = texts.flatMap((text) =>
|
|
2993
|
+
text.boxes.filter((entry) => entry.share >= 0.5).map((entry) => ({ box: entry.seen, text })),
|
|
2994
|
+
);
|
|
2995
|
+
shown.sort((a, b) => a.box.top - b.box.top);
|
|
2996
|
+
for (let i = 0; i < shown.length && findings.length < LIMIT; i += 1) {
|
|
2997
|
+
const a = shown[i];
|
|
2998
|
+
for (let j = i + 1; j < shown.length && shown[j].box.top < a.box.bottom; j += 1) {
|
|
2999
|
+
const b = shown[j];
|
|
3000
|
+
if (a.text.block === b.text.block || a.text === b.text) continue;
|
|
3001
|
+
if (a.text.state.moving || b.text.state.moving) continue;
|
|
3002
|
+
const across = Math.min(a.box.right, b.box.right) - Math.max(a.box.left, b.box.left);
|
|
3003
|
+
const down = Math.min(a.box.bottom, b.box.bottom) - Math.max(a.box.top, b.box.top);
|
|
3004
|
+
if (across < OVERLAP_MIN || down < OVERLAP_MIN) continue;
|
|
3005
|
+
if ((across * down) / Math.min(area(a.box), area(b.box)) < OVERLAP_SHARE) continue;
|
|
3006
|
+
// Words laid over giant display type are a composition, not a collision,
|
|
3007
|
+
// and a variable font's width is past what the canvas can measure.
|
|
3008
|
+
const larger = Math.max(a.text.size, b.text.size);
|
|
3009
|
+
const smaller = Math.min(a.text.size, b.text.size);
|
|
3010
|
+
if (larger >= LAYERED * smaller) continue;
|
|
3011
|
+
const pair = [a.text.words, b.text.words].sort().join("\\u0000");
|
|
3012
|
+
if (seen.has(pair)) continue;
|
|
3013
|
+
seen.add(pair);
|
|
3014
|
+
findings.push({ kind: "overlap", text: a.text.words, other: b.text.words });
|
|
3015
|
+
if (findings.length >= LIMIT) break;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// Cut off: partly shown. Text hidden whole is a choice (screen reader
|
|
3020
|
+
// text, a rolling digit, a carousel), not a mistake.
|
|
3021
|
+
for (const text of texts) {
|
|
3022
|
+
if (findings.length >= LIMIT) break;
|
|
3023
|
+
if (text.state.truncates || text.state.moving) continue;
|
|
3024
|
+
let cut = null;
|
|
3025
|
+
for (const entry of text.boxes) {
|
|
3026
|
+
if (!entry.shows || entry.share > 0.97) continue;
|
|
3027
|
+
const { box } = entry;
|
|
3028
|
+
// Nearest clip first, the page last. Past a scroll box, a clip further
|
|
3029
|
+
// out sees only what the scroll box shows.
|
|
3030
|
+
const inner = { ...box };
|
|
3031
|
+
for (const clip of [...entry.clips.slice(1), entry.clips[0]]) {
|
|
3032
|
+
const sides = [
|
|
3033
|
+
...(clip.cutsX ? [["left", clip.left - inner.left, box.right - box.left], ["right", inner.right - clip.right, box.right - box.left]] : []),
|
|
3034
|
+
...(clip.cutsY ? [["top", clip.top - inner.top, box.bottom - box.top], ["bottom", inner.bottom - clip.bottom, box.bottom - box.top]] : []),
|
|
3035
|
+
];
|
|
3036
|
+
for (const [edge, by, extent] of sides) {
|
|
3037
|
+
// A box flush with the page's edge cuts where the page does; say the page.
|
|
3038
|
+
const deeper = cut === null || by > cut.by || (clip.page && by === cut.by);
|
|
3039
|
+
if (by > Math.max(CUT_MIN, extent * CUT_SHARE) && deeper) {
|
|
3040
|
+
cut = { edge, by, within: clip.page ? "page" : "container" };
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (!clip.cutsX) {
|
|
3044
|
+
inner.left = Math.max(inner.left, clip.left);
|
|
3045
|
+
inner.right = Math.min(inner.right, clip.right);
|
|
3046
|
+
}
|
|
3047
|
+
if (!clip.cutsY) {
|
|
3048
|
+
inner.top = Math.max(inner.top, clip.top);
|
|
3049
|
+
inner.bottom = Math.min(inner.bottom, clip.bottom);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
if (cut) findings.push({ kind: "cut-off", text: text.words, edge: cut.edge, by: Math.round(cut.by), within: cut.within });
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
return findings;
|
|
3057
|
+
})()`;
|
|
3058
|
+
function layoutFindings(value) {
|
|
3059
|
+
if (!Array.isArray(value))
|
|
3060
|
+
return [];
|
|
3061
|
+
const findings = [];
|
|
3062
|
+
for (const entry of value) {
|
|
3063
|
+
if (!isJsonRecord(entry) || !isString(entry.text))
|
|
3064
|
+
continue;
|
|
3065
|
+
if (entry.kind === "overlap" && isString(entry.other)) {
|
|
3066
|
+
findings.push({ kind: "overlap", text: entry.text, other: entry.other });
|
|
3067
|
+
} else if (entry.kind === "cut-off" && isString(entry.edge) && isEdge(entry.edge) && isNumber(entry.by) && (entry.within === "page" || entry.within === "container")) {
|
|
3068
|
+
findings.push({
|
|
3069
|
+
kind: "cut-off",
|
|
3070
|
+
text: entry.text,
|
|
3071
|
+
edge: entry.edge,
|
|
3072
|
+
by: entry.by,
|
|
3073
|
+
within: entry.within
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
return findings;
|
|
3078
|
+
}
|
|
3079
|
+
function describeFinding(finding) {
|
|
3080
|
+
if (finding.kind === "overlap")
|
|
3081
|
+
return `"${finding.text}" runs into "${finding.other}"`;
|
|
3082
|
+
const where = finding.within === "page" ? "the page" : "the box around it";
|
|
3083
|
+
return `"${finding.text}" is cut off ${finding.by}px past the ${finding.edge} edge of ${where}`;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
2451
3086
|
// ../server/dist/capture/capture.js
|
|
2452
3087
|
var FRAME_MAX_HEIGHT = 4e3;
|
|
2453
3088
|
var MIN_WIDTH = 320;
|
|
@@ -2553,6 +3188,13 @@ function resourced(text3, url) {
|
|
|
2553
3188
|
function locatorExpression(focus) {
|
|
2554
3189
|
return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
|
|
2555
3190
|
}
|
|
3191
|
+
async function inspectLayout(page) {
|
|
3192
|
+
const response = await bounded(page.send("Runtime.evaluate", {
|
|
3193
|
+
expression: LAYOUT_PROBE,
|
|
3194
|
+
returnByValue: true
|
|
3195
|
+
}), 2e3, null).catch(() => null);
|
|
3196
|
+
return layoutFindings(resultValue(response));
|
|
3197
|
+
}
|
|
2556
3198
|
async function render(page, input) {
|
|
2557
3199
|
const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
|
|
2558
3200
|
const errors = [];
|
|
@@ -2712,22 +3354,30 @@ async function render(page, input) {
|
|
|
2712
3354
|
returnByValue: true
|
|
2713
3355
|
}), 2e3, void 0).catch(() => {
|
|
2714
3356
|
});
|
|
3357
|
+
const layout = input.inspect === true ? await inspectLayout(page) : [];
|
|
2715
3358
|
const metrics = await page.send("Page.getLayoutMetrics");
|
|
2716
3359
|
const size2 = metrics.cssContentSize ?? metrics.contentSize ?? { width, height: 900 };
|
|
2717
3360
|
const pageWidth = Math.max(width, Math.ceil(size2.width));
|
|
2718
3361
|
const contentHeight = Math.max(1, Math.ceil(size2.height));
|
|
2719
3362
|
const pageHeight = Math.min(FRAME_MAX_HEIGHT, contentHeight);
|
|
2720
3363
|
const cut = size2.height > FRAME_MAX_HEIGHT;
|
|
3364
|
+
const clip2 = { x: 0, y: 0, width, height: pageHeight, scale: 1 };
|
|
2721
3365
|
const frameResponse = await page.send("Page.captureScreenshot", {
|
|
2722
3366
|
format: "png",
|
|
2723
3367
|
captureBeyondViewport: true,
|
|
2724
|
-
clip:
|
|
3368
|
+
clip: clip2
|
|
2725
3369
|
});
|
|
2726
3370
|
const frame = {
|
|
2727
3371
|
png: Buffer.from(frameResponse.data, "base64"),
|
|
2728
3372
|
width,
|
|
2729
3373
|
height: pageHeight
|
|
2730
3374
|
};
|
|
3375
|
+
const jpeg = input.jpeg === true ? await page.send("Page.captureScreenshot", {
|
|
3376
|
+
format: "jpeg",
|
|
3377
|
+
quality: 80,
|
|
3378
|
+
captureBeyondViewport: true,
|
|
3379
|
+
clip: clip2
|
|
3380
|
+
}).then((shot) => Buffer.from(shot.data, "base64"), () => null) : null;
|
|
2731
3381
|
const crops = [];
|
|
2732
3382
|
for (const focus of input.focuses ?? []) {
|
|
2733
3383
|
const located = await page.send("Runtime.evaluate", {
|
|
@@ -2763,7 +3413,7 @@ async function render(page, input) {
|
|
|
2763
3413
|
resolved
|
|
2764
3414
|
});
|
|
2765
3415
|
}
|
|
2766
|
-
return { frame, crops, errors, hydration, cut };
|
|
3416
|
+
return { frame, crops, errors, hydration, cut, layout, jpeg };
|
|
2767
3417
|
} finally {
|
|
2768
3418
|
for (const stop of unlisten)
|
|
2769
3419
|
stop();
|
|
@@ -2775,8 +3425,8 @@ async function capturePage(browser, input) {
|
|
|
2775
3425
|
}
|
|
2776
3426
|
|
|
2777
3427
|
// ../server/dist/requests/attachments.js
|
|
2778
|
-
import { copyFile, lstat, mkdir as
|
|
2779
|
-
import { basename as basename2, extname, join as
|
|
3428
|
+
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";
|
|
3429
|
+
import { basename as basename2, extname, join as join6, resolve, sep } from "path";
|
|
2780
3430
|
var CAPTURES_DIR = ".leglas/captures";
|
|
2781
3431
|
var REFERENCES_DIR = ".leglas/references";
|
|
2782
3432
|
var LOAD_SHARE = 0.6;
|
|
@@ -2867,7 +3517,7 @@ function sniffImage(bytes) {
|
|
|
2867
3517
|
async function moveReference(cwd, id, destinationDir, index) {
|
|
2868
3518
|
let names;
|
|
2869
3519
|
try {
|
|
2870
|
-
names = await
|
|
3520
|
+
names = await readdir3(join6(cwd, REFERENCES_DIR));
|
|
2871
3521
|
} catch {
|
|
2872
3522
|
return null;
|
|
2873
3523
|
}
|
|
@@ -2876,17 +3526,17 @@ async function moveReference(cwd, id, destinationDir, index) {
|
|
|
2876
3526
|
return null;
|
|
2877
3527
|
const extension = extname(sourceName).toLowerCase();
|
|
2878
3528
|
const destinationName = `reference-${index}${extension}`;
|
|
2879
|
-
const source =
|
|
2880
|
-
const destination =
|
|
2881
|
-
await
|
|
3529
|
+
const source = join6(cwd, REFERENCES_DIR, sourceName);
|
|
3530
|
+
const destination = join6(destinationDir, destinationName);
|
|
3531
|
+
await mkdir4(destinationDir, { recursive: true });
|
|
2882
3532
|
try {
|
|
2883
|
-
await
|
|
3533
|
+
await rename3(source, destination);
|
|
2884
3534
|
} catch {
|
|
2885
3535
|
await copyFile(source, destination);
|
|
2886
3536
|
await unlink(source).catch(() => {
|
|
2887
3537
|
});
|
|
2888
3538
|
}
|
|
2889
|
-
const sniffed = sniffImage(await
|
|
3539
|
+
const sniffed = sniffImage(await readFile6(destination));
|
|
2890
3540
|
return {
|
|
2891
3541
|
kind: "reference",
|
|
2892
3542
|
file: `${CAPTURES_DIR}/${basename2(destinationDir)}/${destinationName}`,
|
|
@@ -2908,7 +3558,7 @@ function focusOf(note) {
|
|
|
2908
3558
|
async function attachRequest(cwd, requestId, input, deps) {
|
|
2909
3559
|
const capture = deps.capture ?? capturePage;
|
|
2910
3560
|
const deadlineMs = deps.deadlineMs ?? 12e3;
|
|
2911
|
-
const destination =
|
|
3561
|
+
const destination = join6(cwd, CAPTURES_DIR, requestId);
|
|
2912
3562
|
const captured = {
|
|
2913
3563
|
attachments: [],
|
|
2914
3564
|
errors: [],
|
|
@@ -2954,8 +3604,8 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2954
3604
|
const direction = await capture(browser, directionInput);
|
|
2955
3605
|
if (expired2)
|
|
2956
3606
|
return;
|
|
2957
|
-
await
|
|
2958
|
-
await
|
|
3607
|
+
await mkdir4(destination, { recursive: true });
|
|
3608
|
+
await writeFile4(join6(destination, "frame.png"), direction.frame.png);
|
|
2959
3609
|
captured.attachments.push({
|
|
2960
3610
|
kind: "frame",
|
|
2961
3611
|
file: `${CAPTURES_DIR}/${requestId}/frame.png`,
|
|
@@ -2973,7 +3623,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2973
3623
|
if (crop === null || crop === void 0 || note === void 0 || expired2)
|
|
2974
3624
|
continue;
|
|
2975
3625
|
const name = `note-${index + 1}.png`;
|
|
2976
|
-
await
|
|
3626
|
+
await writeFile4(join6(destination, name), crop.shot.png);
|
|
2977
3627
|
captured.attachments.push({
|
|
2978
3628
|
kind: "note",
|
|
2979
3629
|
file: `${CAPTURES_DIR}/${requestId}/${name}`,
|
|
@@ -2994,7 +3644,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2994
3644
|
const comparison = await capture(browser, compareInput);
|
|
2995
3645
|
if (expired2)
|
|
2996
3646
|
return;
|
|
2997
|
-
await
|
|
3647
|
+
await writeFile4(join6(destination, "compare.png"), comparison.frame.png);
|
|
2998
3648
|
captured.attachments.push({
|
|
2999
3649
|
kind: "compare",
|
|
3000
3650
|
file: `${CAPTURES_DIR}/${requestId}/compare.png`,
|
|
@@ -3018,13 +3668,13 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3018
3668
|
return captured;
|
|
3019
3669
|
}
|
|
3020
3670
|
function captureDirectory(cwd, requestId) {
|
|
3021
|
-
return /^[A-Za-z0-9_-]{1,32}$/.test(requestId) ?
|
|
3671
|
+
return /^[A-Za-z0-9_-]{1,32}$/.test(requestId) ? join6(cwd, CAPTURES_DIR, requestId) : null;
|
|
3022
3672
|
}
|
|
3023
3673
|
async function removeCaptures(cwd, requestId) {
|
|
3024
3674
|
const directory = captureDirectory(cwd, requestId);
|
|
3025
3675
|
if (directory === null)
|
|
3026
3676
|
return;
|
|
3027
|
-
await
|
|
3677
|
+
await rm4(directory, { recursive: true, force: true });
|
|
3028
3678
|
}
|
|
3029
3679
|
async function rehomeCaptures(cwd, from, to, attachments) {
|
|
3030
3680
|
if (attachments.length === 0)
|
|
@@ -3033,8 +3683,8 @@ async function rehomeCaptures(cwd, from, to, attachments) {
|
|
|
3033
3683
|
const destination = captureDirectory(cwd, to);
|
|
3034
3684
|
if (source === null || destination === null)
|
|
3035
3685
|
return [...attachments];
|
|
3036
|
-
await
|
|
3037
|
-
await
|
|
3686
|
+
await mkdir4(join6(cwd, CAPTURES_DIR), { recursive: true });
|
|
3687
|
+
await rename3(source, destination);
|
|
3038
3688
|
const prefix = `${CAPTURES_DIR}/${from}/`;
|
|
3039
3689
|
return attachments.map((attachment) => ({
|
|
3040
3690
|
...attachment,
|
|
@@ -3045,7 +3695,7 @@ function rehomeText(text3, from, to) {
|
|
|
3045
3695
|
return text3.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
|
|
3046
3696
|
}
|
|
3047
3697
|
async function ownCapturesRoot(cwd) {
|
|
3048
|
-
const root =
|
|
3698
|
+
const root = join6(cwd, CAPTURES_DIR);
|
|
3049
3699
|
try {
|
|
3050
3700
|
const entry = await lstat(root);
|
|
3051
3701
|
return entry.isDirectory() ? root : null;
|
|
@@ -3054,7 +3704,7 @@ async function ownCapturesRoot(cwd) {
|
|
|
3054
3704
|
}
|
|
3055
3705
|
}
|
|
3056
3706
|
async function isOwnCapture(cwd, file) {
|
|
3057
|
-
const root = await realpath(
|
|
3707
|
+
const root = await realpath(join6(cwd, CAPTURES_DIR)).catch(() => null);
|
|
3058
3708
|
if (root === null)
|
|
3059
3709
|
return false;
|
|
3060
3710
|
const real = await realpath(resolve(cwd, file)).catch(() => null);
|
|
@@ -3068,21 +3718,21 @@ async function pruneCaptures(cwd, keep) {
|
|
|
3068
3718
|
if (root === null)
|
|
3069
3719
|
return void await pruneReferences(cwd);
|
|
3070
3720
|
try {
|
|
3071
|
-
const entries = await
|
|
3072
|
-
await Promise.all(entries.filter((entry) => entry.isDirectory() && !kept.has(entry.name)).map((entry) =>
|
|
3721
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
3722
|
+
await Promise.all(entries.filter((entry) => entry.isDirectory() && !kept.has(entry.name)).map((entry) => rm4(join6(root, entry.name), { recursive: true, force: true })));
|
|
3073
3723
|
} catch {
|
|
3074
3724
|
}
|
|
3075
3725
|
await pruneReferences(cwd);
|
|
3076
3726
|
}
|
|
3077
3727
|
async function pruneReferences(cwd) {
|
|
3078
|
-
const references =
|
|
3728
|
+
const references = join6(cwd, REFERENCES_DIR);
|
|
3079
3729
|
try {
|
|
3080
3730
|
if (!(await lstat(references)).isDirectory())
|
|
3081
3731
|
return;
|
|
3082
|
-
const entries = await
|
|
3732
|
+
const entries = await readdir3(references, { withFileTypes: true });
|
|
3083
3733
|
const old = Date.now() - 60 * 60 * 1e3;
|
|
3084
3734
|
await Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
|
|
3085
|
-
const file =
|
|
3735
|
+
const file = join6(references, entry.name);
|
|
3086
3736
|
if ((await stat2(file)).mtimeMs < old)
|
|
3087
3737
|
await unlink(file).catch(() => {
|
|
3088
3738
|
});
|
|
@@ -3093,9 +3743,9 @@ async function pruneReferences(cwd) {
|
|
|
3093
3743
|
|
|
3094
3744
|
// ../server/dist/branches/worktree.js
|
|
3095
3745
|
import { execFile, spawn as spawn2 } from "child_process";
|
|
3096
|
-
import { rm as
|
|
3746
|
+
import { rm as rm5 } from "fs/promises";
|
|
3097
3747
|
import net2 from "net";
|
|
3098
|
-
import { join as
|
|
3748
|
+
import { join as join7 } from "path";
|
|
3099
3749
|
import { promisify } from "util";
|
|
3100
3750
|
var run = promisify(execFile);
|
|
3101
3751
|
var WORKTREES_DIR = ".leglas/worktrees";
|
|
@@ -3137,10 +3787,10 @@ async function answeringHost(port) {
|
|
|
3137
3787
|
}
|
|
3138
3788
|
async function startWorktree(options) {
|
|
3139
3789
|
const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
|
|
3140
|
-
const path =
|
|
3790
|
+
const path = join7(options.cwd, WORKTREES_DIR, worktreeSlug(options.branch));
|
|
3141
3791
|
const log = options.onLog ?? (() => {
|
|
3142
3792
|
});
|
|
3143
|
-
await
|
|
3793
|
+
await rm5(path, { recursive: true, force: true });
|
|
3144
3794
|
await run("git", ["worktree", "prune"], { cwd: options.cwd }).catch(() => {
|
|
3145
3795
|
});
|
|
3146
3796
|
try {
|
|
@@ -3152,7 +3802,7 @@ async function startWorktree(options) {
|
|
|
3152
3802
|
const cleanup = async () => {
|
|
3153
3803
|
await run("git", ["worktree", "remove", "--force", path], { cwd: options.cwd }).catch(() => {
|
|
3154
3804
|
});
|
|
3155
|
-
await
|
|
3805
|
+
await rm5(path, { recursive: true, force: true }).catch(() => {
|
|
3156
3806
|
});
|
|
3157
3807
|
};
|
|
3158
3808
|
try {
|
|
@@ -3201,8 +3851,8 @@ async function startAppProcess(options) {
|
|
|
3201
3851
|
child = spawn2(substitutePort(options.devCommand, port), {
|
|
3202
3852
|
cwd: options.cwd,
|
|
3203
3853
|
shell: true,
|
|
3204
|
-
// Own process group, so stopping kills the shell and
|
|
3205
|
-
//
|
|
3854
|
+
// Own process group, so stopping kills the shell and everything it
|
|
3855
|
+
// spawned instead of orphaning a dev server on the port.
|
|
3206
3856
|
detached: true,
|
|
3207
3857
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3208
3858
|
});
|
|
@@ -3448,8 +4098,8 @@ function conversationFailure(code) {
|
|
|
3448
4098
|
|
|
3449
4099
|
// ../server/dist/requests/annotations.js
|
|
3450
4100
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3451
|
-
import { mkdir as
|
|
3452
|
-
import { dirname as dirname4, join as
|
|
4101
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
4102
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
3453
4103
|
var ANNOTATIONS_PATH = ".leglas/annotations.json";
|
|
3454
4104
|
var NOTE_CAP = 500;
|
|
3455
4105
|
var SELECTOR_CAP = 300;
|
|
@@ -3513,7 +4163,7 @@ function anchorFrom(value) {
|
|
|
3513
4163
|
}
|
|
3514
4164
|
async function readAnnotations(cwd) {
|
|
3515
4165
|
try {
|
|
3516
|
-
const raw = await
|
|
4166
|
+
const raw = await readFile7(join8(cwd, ANNOTATIONS_PATH), "utf8");
|
|
3517
4167
|
const parsed2 = parseJson(raw);
|
|
3518
4168
|
if (!isJsonRecord(parsed2) || !Array.isArray(parsed2.annotations))
|
|
3519
4169
|
return [];
|
|
@@ -3538,9 +4188,9 @@ async function readAnnotations(cwd) {
|
|
|
3538
4188
|
}
|
|
3539
4189
|
}
|
|
3540
4190
|
async function write(cwd, annotations) {
|
|
3541
|
-
const path =
|
|
3542
|
-
await
|
|
3543
|
-
await
|
|
4191
|
+
const path = join8(cwd, ANNOTATIONS_PATH);
|
|
4192
|
+
await mkdir5(dirname4(path), { recursive: true });
|
|
4193
|
+
await writeFile5(path, `${JSON.stringify({ annotations }, null, 2)}
|
|
3544
4194
|
`, "utf8");
|
|
3545
4195
|
}
|
|
3546
4196
|
var writing = Promise.resolve();
|
|
@@ -3611,9 +4261,9 @@ function describeAnnotations(annotations) {
|
|
|
3611
4261
|
}
|
|
3612
4262
|
|
|
3613
4263
|
// ../server/dist/requests/requests.js
|
|
3614
|
-
import { mkdir as
|
|
4264
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
3615
4265
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
3616
|
-
import { dirname as dirname5, join as
|
|
4266
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
3617
4267
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;
|
|
3618
4268
|
function variantSlot(url) {
|
|
3619
4269
|
if (!url.startsWith("/"))
|
|
@@ -3792,7 +4442,7 @@ function failureOf(entry) {
|
|
|
3792
4442
|
}
|
|
3793
4443
|
async function readRequests(cwd) {
|
|
3794
4444
|
try {
|
|
3795
|
-
const raw = await
|
|
4445
|
+
const raw = await readFile8(join9(cwd, REQUESTS_PATH), "utf8");
|
|
3796
4446
|
const parsed2 = parseJson(raw);
|
|
3797
4447
|
if (!isJsonRecord(parsed2) || !Array.isArray(parsed2.requests))
|
|
3798
4448
|
return [];
|
|
@@ -3828,9 +4478,9 @@ async function readRequests(cwd) {
|
|
|
3828
4478
|
}
|
|
3829
4479
|
}
|
|
3830
4480
|
async function writeQueue(cwd, requests) {
|
|
3831
|
-
const path =
|
|
3832
|
-
await
|
|
3833
|
-
await
|
|
4481
|
+
const path = join9(cwd, REQUESTS_PATH);
|
|
4482
|
+
await mkdir6(dirname5(path), { recursive: true });
|
|
4483
|
+
await writeFile6(path, `${JSON.stringify({ requests }, null, 2)}
|
|
3834
4484
|
`, "utf8");
|
|
3835
4485
|
}
|
|
3836
4486
|
async function appendRequest(cwd, request, id = newRequestId()) {
|
|
@@ -3888,12 +4538,12 @@ async function clearRequests(cwd) {
|
|
|
3888
4538
|
|
|
3889
4539
|
// ../server/dist/agents/runner.js
|
|
3890
4540
|
import { spawn as nodeSpawn3 } from "child_process";
|
|
3891
|
-
import { readFile as
|
|
3892
|
-
import { join as
|
|
4541
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4542
|
+
import { join as join10, resolve as resolve2 } from "path";
|
|
3893
4543
|
|
|
3894
4544
|
// ../server/dist/agents/claude-agent-session.js
|
|
3895
4545
|
import { EventEmitter } from "events";
|
|
3896
|
-
import { readFile as
|
|
4546
|
+
import { readFile as readFile9, stat as stat3 } from "fs/promises";
|
|
3897
4547
|
import { extname as extname2 } from "path";
|
|
3898
4548
|
import { PassThrough } from "stream";
|
|
3899
4549
|
var INITIALIZE_TIMEOUT_MS = 3e4;
|
|
@@ -3923,7 +4573,7 @@ async function claudeContent(prompt, images2) {
|
|
|
3923
4573
|
try {
|
|
3924
4574
|
if ((await stat3(path)).size > IMAGE_MAX_BYTES)
|
|
3925
4575
|
continue;
|
|
3926
|
-
const data = await
|
|
4576
|
+
const data = await readFile9(path);
|
|
3927
4577
|
blocks.push({
|
|
3928
4578
|
type: "image",
|
|
3929
4579
|
source: { type: "base64", media_type: mediaType, data: data.toString("base64") }
|
|
@@ -4056,11 +4706,10 @@ var PersistentClaudeSession = class {
|
|
|
4056
4706
|
warmedFor = null;
|
|
4057
4707
|
closed = false;
|
|
4058
4708
|
/**
|
|
4059
|
-
* Counts every ask for a warm process.
|
|
4060
|
-
*
|
|
4061
|
-
*
|
|
4062
|
-
*
|
|
4063
|
-
* release captured at entry says which side of that it is on.
|
|
4709
|
+
* Counts every ask for a warm process. warm() and release() aren't awaited
|
|
4710
|
+
* and race both ways: a release must outlast a warm that started while it
|
|
4711
|
+
* settled, and stand down for a warm asked for after it began. The generation
|
|
4712
|
+
* it captured says which.
|
|
4064
4713
|
*/
|
|
4065
4714
|
generation = 0;
|
|
4066
4715
|
constructor(cwd, allowedCommands, startup) {
|
|
@@ -4170,13 +4819,10 @@ var PersistentClaudeSession = class {
|
|
|
4170
4819
|
}
|
|
4171
4820
|
}
|
|
4172
4821
|
/**
|
|
4173
|
-
*
|
|
4174
|
-
*
|
|
4175
|
-
*
|
|
4176
|
-
*
|
|
4177
|
-
* may never come. The runner calls this once nothing has asked for Claude
|
|
4178
|
-
* in a while, and the next warm() starts it again. A session id the runner
|
|
4179
|
-
* still holds is served by the `claude --resume` fallback in the meantime.
|
|
4822
|
+
* Ends the native process but keeps the transport. A warm session is Claude
|
|
4823
|
+
* Code plus every MCP server the user configured, held for a request that may
|
|
4824
|
+
* not come. Called after an idle spell; the next warm() starts it again, and
|
|
4825
|
+
* a held session id goes through `claude --resume` meanwhile.
|
|
4180
4826
|
*/
|
|
4181
4827
|
async release() {
|
|
4182
4828
|
const asOf = this.generation;
|
|
@@ -4380,12 +5026,10 @@ var PersistentCodexAppServer = class {
|
|
|
4380
5026
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
4381
5027
|
}
|
|
4382
5028
|
/**
|
|
4383
|
-
* Counts every ask for a warm process.
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4387
|
-
* reset restarts through start() rather than warm(), so waiting its turn
|
|
4388
|
-
* does not read as a fresh ask.
|
|
5029
|
+
* Counts every ask for a warm process. warm() and release() aren't awaited
|
|
5030
|
+
* and race both ways: a release must outlast a warm queued behind its reset,
|
|
5031
|
+
* and stand down for a warm asked for after it began. A queued warm restarts
|
|
5032
|
+
* through start(), so waiting doesn't count as a new ask.
|
|
4389
5033
|
*/
|
|
4390
5034
|
generation = 0;
|
|
4391
5035
|
warm(_sessionId = null) {
|
|
@@ -4506,11 +5150,9 @@ var PersistentCodexAppServer = class {
|
|
|
4506
5150
|
}
|
|
4507
5151
|
}
|
|
4508
5152
|
/**
|
|
4509
|
-
*
|
|
4510
|
-
*
|
|
4511
|
-
*
|
|
4512
|
-
* next warm() spawns and handshakes again. A thread id the runner still
|
|
4513
|
-
* holds is resumed by the new process through `thread/resume`.
|
|
5153
|
+
* Ends the app-server process but keeps the transport. Called after an idle
|
|
5154
|
+
* spell; the next warm() spawns again and resumes a held thread through
|
|
5155
|
+
* `thread/resume`.
|
|
4514
5156
|
*/
|
|
4515
5157
|
async release() {
|
|
4516
5158
|
const asOf = this.generation;
|
|
@@ -5143,7 +5785,7 @@ function startRunner(options) {
|
|
|
5143
5785
|
active = null;
|
|
5144
5786
|
}
|
|
5145
5787
|
};
|
|
5146
|
-
const registered = () =>
|
|
5788
|
+
const registered = () => readFile10(join10(options.cwd, LOCAL_PREVIEWS_PATH), "utf8").catch(() => null);
|
|
5147
5789
|
const handle = async (request, choice) => {
|
|
5148
5790
|
const allowedCommands = options.leglasCommand === void 0 ? [] : [
|
|
5149
5791
|
`${options.leglasCommand} show`,
|
|
@@ -5187,13 +5829,11 @@ function startRunner(options) {
|
|
|
5187
5829
|
retry: observed.retry
|
|
5188
5830
|
});
|
|
5189
5831
|
let failure = verdict();
|
|
5190
|
-
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && // The edited flag is only
|
|
5191
|
-
// real output
|
|
5192
|
-
//
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
// child settling and the retry starting finds no child to cancel, so
|
|
5196
|
-
// nothing says "cancelled". Stopped still means stopped.
|
|
5832
|
+
if (!(outcome.ok && outcome.code === 0) && resolved.resumed && !observed.edited && // The edited flag is evidence only where Leglas has read the vendor's
|
|
5833
|
+
// real output; elsewhere "didn't edit" means "wasn't seen to edit".
|
|
5834
|
+
// Those vendors keep the failure card and its Retry.
|
|
5835
|
+
activityVerified(resolved.agent) && conversationFailure(failure.code) && // Not redundant: a stop between the first child settling and the retry
|
|
5836
|
+
// starting finds no child to cancel.
|
|
5197
5837
|
!stopped) {
|
|
5198
5838
|
sessions.delete(resolved.agent);
|
|
5199
5839
|
const cold = resolveCommand(choice, request.prompt, null, allowedCommands, images2);
|
|
@@ -5338,9 +5978,8 @@ function startRunner(options) {
|
|
|
5338
5978
|
snapshot: () => ({ ...state, failedIds: [...failed] }),
|
|
5339
5979
|
cancel,
|
|
5340
5980
|
prepare,
|
|
5341
|
-
// A nudge during a tick is latched
|
|
5342
|
-
//
|
|
5343
|
-
// the active agent.
|
|
5981
|
+
// A nudge during a tick is latched until the tick settles; between ticks it
|
|
5982
|
+
// starts one at once. Neither overlaps the active agent.
|
|
5344
5983
|
nudge: () => schedule(true)
|
|
5345
5984
|
};
|
|
5346
5985
|
}
|
|
@@ -5549,7 +6188,7 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5549
6188
|
}
|
|
5550
6189
|
|
|
5551
6190
|
// ../server/dist/share/share.js
|
|
5552
|
-
import { randomBytes as randomBytes4, randomUUID, timingSafeEqual } from "crypto";
|
|
6191
|
+
import { randomBytes as randomBytes4, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
5553
6192
|
import http3 from "http";
|
|
5554
6193
|
import { posix } from "path";
|
|
5555
6194
|
|
|
@@ -5870,10 +6509,10 @@ var DEV_CONTROL_ROUTES = [
|
|
|
5870
6509
|
/** read, browser-sync 3.0.4: its client surface and server metadata. */
|
|
5871
6510
|
"/__browser_sync__",
|
|
5872
6511
|
/**
|
|
5873
|
-
* read, webpack-dev-server 6.0.0
|
|
5874
|
-
*
|
|
5875
|
-
*
|
|
5876
|
-
*
|
|
6512
|
+
* read, webpack-dev-server 6.0.0: the file listing,
|
|
6513
|
+
* `/webpack-dev-server/invalidate` (forces a rebuild) and
|
|
6514
|
+
* `/webpack-dev-server/open-editor` (launch-editor again). The subtree match
|
|
6515
|
+
* covers all three.
|
|
5877
6516
|
*/
|
|
5878
6517
|
"/webpack-dev-server",
|
|
5879
6518
|
/** reported: Rails Web Console, an interactive server-side REPL. */
|
|
@@ -5898,17 +6537,12 @@ var DEV_CONTROL_ROUTES = [
|
|
|
5898
6537
|
var DEV_CONTROL_PREFIXES = [
|
|
5899
6538
|
/** read, Next 16.3.1. Its app assets sit at `/_next/` and stay allowed. */
|
|
5900
6539
|
"/__nextjs_",
|
|
5901
|
-
/**
|
|
5902
|
-
* read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's own bundle is at `/_nuxt/`,
|
|
5903
|
-
* one underscore and a different prefix, so the app is untouched.
|
|
5904
|
-
*/
|
|
6540
|
+
/** read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's bundle is at `/_nuxt/`, a different prefix. */
|
|
5905
6541
|
"/__nuxt_devtools__",
|
|
5906
6542
|
/**
|
|
5907
|
-
* read, Parcel 2.16.4: `__parcel_launch_editor`
|
|
5908
|
-
*
|
|
5909
|
-
*
|
|
5910
|
-
* which serve source, and the HMR and health routes, which a viewer has no
|
|
5911
|
-
* use for: their live-reload socket is already refused.
|
|
6543
|
+
* read, Parcel 2.16.4: `__parcel_launch_editor` takes a `file` and calls
|
|
6544
|
+
* launch-editor. Its source-map, source-root, code-frame, HMR and health
|
|
6545
|
+
* routes serve source or are useless to a viewer.
|
|
5912
6546
|
*/
|
|
5913
6547
|
"/__parcel_"
|
|
5914
6548
|
];
|
|
@@ -6284,7 +6918,7 @@ function createShareManager(options) {
|
|
|
6284
6918
|
token = randomBytes4(24).toString("base64url");
|
|
6285
6919
|
const at = now();
|
|
6286
6920
|
const grant = {
|
|
6287
|
-
id:
|
|
6921
|
+
id: randomUUID3(),
|
|
6288
6922
|
name,
|
|
6289
6923
|
token,
|
|
6290
6924
|
createdAt: at,
|
|
@@ -6599,7 +7233,7 @@ function createShareManager(options) {
|
|
|
6599
7233
|
}
|
|
6600
7234
|
const share = {
|
|
6601
7235
|
...parsed2.manifest,
|
|
6602
|
-
id:
|
|
7236
|
+
id: randomUUID3(),
|
|
6603
7237
|
grants: /* @__PURE__ */ new Map(),
|
|
6604
7238
|
tombstones: [],
|
|
6605
7239
|
port,
|
|
@@ -6630,8 +7264,8 @@ function createShareManager(options) {
|
|
|
6630
7264
|
share.runningTunnel = runTunnel({
|
|
6631
7265
|
provider,
|
|
6632
7266
|
port,
|
|
6633
|
-
// Whichever link exists when the tunnel starts
|
|
6634
|
-
//
|
|
7267
|
+
// Whichever link exists when the tunnel starts; a share always has
|
|
7268
|
+
// one, and the probe only needs a path the listener answers.
|
|
6635
7269
|
entryPath: `${ENTRY_PREFIX}${Array.from(share.grants.values())[0]?.token ?? ""}`,
|
|
6636
7270
|
onState: (next) => {
|
|
6637
7271
|
if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
@@ -6784,11 +7418,9 @@ function createShareManager(options) {
|
|
|
6784
7418
|
return null;
|
|
6785
7419
|
return {
|
|
6786
7420
|
...options.viewerConfig,
|
|
6787
|
-
// The project id is the config's absolute path
|
|
6788
|
-
//
|
|
6789
|
-
// server
|
|
6790
|
-
// never dials the dev server, so the share's own id does the job and
|
|
6791
|
-
// the address is left blank.
|
|
7421
|
+
// The project id is the config's absolute path and the dev server address
|
|
7422
|
+
// names the sharer's network. Viewers keep no layout and never dial the
|
|
7423
|
+
// dev server, so they get the share's id and a blank address.
|
|
6792
7424
|
project: `share:${share.id}`,
|
|
6793
7425
|
devServer: "",
|
|
6794
7426
|
previews: options.previewsForConfig(previews),
|
|
@@ -6856,12 +7488,12 @@ function createShareManager(options) {
|
|
|
6856
7488
|
}
|
|
6857
7489
|
|
|
6858
7490
|
// ../server/dist/config/renames.js
|
|
6859
|
-
import { mkdir as
|
|
6860
|
-
import { dirname as dirname6, join as
|
|
7491
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
|
|
7492
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
6861
7493
|
var RENAMES_PATH = ".leglas/renames.json";
|
|
6862
7494
|
async function readRenames(cwd) {
|
|
6863
7495
|
try {
|
|
6864
|
-
const raw = await
|
|
7496
|
+
const raw = await readFile11(join11(cwd, RENAMES_PATH), "utf8");
|
|
6865
7497
|
const parsed2 = parseJson(raw);
|
|
6866
7498
|
if (!isJsonRecord(parsed2) || !isJsonRecord(parsed2.renames) && !Array.isArray(parsed2.renames))
|
|
6867
7499
|
return {};
|
|
@@ -6871,9 +7503,9 @@ async function readRenames(cwd) {
|
|
|
6871
7503
|
}
|
|
6872
7504
|
}
|
|
6873
7505
|
async function writeRenames(cwd, renames) {
|
|
6874
|
-
const path =
|
|
6875
|
-
await
|
|
6876
|
-
await
|
|
7506
|
+
const path = join11(cwd, RENAMES_PATH);
|
|
7507
|
+
await mkdir7(dirname6(path), { recursive: true });
|
|
7508
|
+
await writeFile7(path, `${JSON.stringify({ renames }, null, 2)}
|
|
6877
7509
|
`, "utf8");
|
|
6878
7510
|
}
|
|
6879
7511
|
function resolveTitle(input, titles, renames) {
|
|
@@ -6890,10 +7522,10 @@ function resolveTitle(input, titles, renames) {
|
|
|
6890
7522
|
// ../server/dist/server.js
|
|
6891
7523
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
6892
7524
|
import { createHash as createHash2 } from "crypto";
|
|
6893
|
-
import { mkdir as
|
|
7525
|
+
import { mkdir as mkdir9, readdir as readdir6, writeFile as writeFile10 } from "fs/promises";
|
|
6894
7526
|
import http4 from "http";
|
|
6895
7527
|
import net3 from "net";
|
|
6896
|
-
import { basename as basename3, dirname as dirname10, extname as extname3, join as
|
|
7528
|
+
import { basename as basename3, dirname as dirname10, extname as extname3, join as join17, normalize as normalize4, relative as relative4 } from "path";
|
|
6897
7529
|
|
|
6898
7530
|
// ../server/dist/frame-policy.js
|
|
6899
7531
|
var FRAMING_TIMEOUT_MS = 4e3;
|
|
@@ -7006,8 +7638,8 @@ async function checkFraming(target, embedder, fetcher = fetch) {
|
|
|
7006
7638
|
|
|
7007
7639
|
// ../server/dist/generation/generation.js
|
|
7008
7640
|
import { spawn as nodeSpawn4 } from "child_process";
|
|
7009
|
-
import { readFile as
|
|
7010
|
-
import { dirname as dirname8, join as
|
|
7641
|
+
import { readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
7642
|
+
import { dirname as dirname8, join as join15, normalize as normalize3 } from "path";
|
|
7011
7643
|
|
|
7012
7644
|
// ../server/dist/generation/claude.js
|
|
7013
7645
|
var GENERATION_EFFORT = "medium";
|
|
@@ -7050,9 +7682,9 @@ function resultText(lines2) {
|
|
|
7050
7682
|
}
|
|
7051
7683
|
|
|
7052
7684
|
// ../server/dist/generation/codex.js
|
|
7053
|
-
import { readFile as
|
|
7685
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
7054
7686
|
import { homedir as homedir2 } from "os";
|
|
7055
|
-
import { join as
|
|
7687
|
+
import { join as join12 } from "path";
|
|
7056
7688
|
var RESTRICTED2 = [
|
|
7057
7689
|
"exec",
|
|
7058
7690
|
"--json",
|
|
@@ -7066,8 +7698,8 @@ var RESTRICTED2 = [
|
|
|
7066
7698
|
"skills.include_instructions=false",
|
|
7067
7699
|
"-c",
|
|
7068
7700
|
"skills.bundled.enabled=false",
|
|
7069
|
-
// Through the config, not `--disable`:
|
|
7070
|
-
//
|
|
7701
|
+
// Through the config, not `--disable`: an unknown feature is ignored here,
|
|
7702
|
+
// while `--disable` refuses to start.
|
|
7071
7703
|
...[
|
|
7072
7704
|
"multi_agent",
|
|
7073
7705
|
"apps",
|
|
@@ -7083,7 +7715,7 @@ var BARE = /^[A-Za-z0-9_-]+$/;
|
|
|
7083
7715
|
async function codexServers(home = defaultCodexHome()) {
|
|
7084
7716
|
let config;
|
|
7085
7717
|
try {
|
|
7086
|
-
config = await
|
|
7718
|
+
config = await readFile12(join12(home, "config.toml"), "utf8");
|
|
7087
7719
|
} catch {
|
|
7088
7720
|
return [];
|
|
7089
7721
|
}
|
|
@@ -7096,7 +7728,7 @@ async function codexServers(home = defaultCodexHome()) {
|
|
|
7096
7728
|
return [...names];
|
|
7097
7729
|
}
|
|
7098
7730
|
function defaultCodexHome(env = process.env) {
|
|
7099
|
-
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME :
|
|
7731
|
+
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME : join12(homedir2(), ".codex");
|
|
7100
7732
|
}
|
|
7101
7733
|
var serversOff = (servers) => servers.flatMap((name) => ["-c", `mcp_servers.${name}.enabled=false`]);
|
|
7102
7734
|
function codexPlanArgs(prompt, servers) {
|
|
@@ -7122,12 +7754,12 @@ function codexResultText(lines2) {
|
|
|
7122
7754
|
}
|
|
7123
7755
|
|
|
7124
7756
|
// ../server/dist/generation/facts.js
|
|
7125
|
-
import { readFile as
|
|
7126
|
-
import { dirname as dirname7, join as
|
|
7757
|
+
import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
|
|
7758
|
+
import { dirname as dirname7, join as join14, normalize as normalize2 } from "path";
|
|
7127
7759
|
|
|
7128
7760
|
// ../server/dist/generation/switch-file.js
|
|
7129
|
-
import { readFile as
|
|
7130
|
-
import { join as
|
|
7761
|
+
import { readFile as readFile13, readdir as readdir4, stat as stat4 } from "fs/promises";
|
|
7762
|
+
import { join as join13, relative as relative3 } from "path";
|
|
7131
7763
|
var MAP = /const DIRECTIONS = \{([\s\S]*?)\} as const;/;
|
|
7132
7764
|
var SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
7133
7765
|
var SCAN_LIMIT = 3e3;
|
|
@@ -7140,8 +7772,8 @@ async function isFile(path) {
|
|
|
7140
7772
|
}
|
|
7141
7773
|
async function findSwitch(cwd, slug) {
|
|
7142
7774
|
for (const name of ["switch.tsx", "switch.jsx"]) {
|
|
7143
|
-
const path =
|
|
7144
|
-
if (await isFile(
|
|
7775
|
+
const path = join13(".leglas", "variants", slug, name);
|
|
7776
|
+
if (await isFile(join13(cwd, path)))
|
|
7145
7777
|
return path;
|
|
7146
7778
|
}
|
|
7147
7779
|
const wanted = `"v-${slug}"`;
|
|
@@ -7149,14 +7781,14 @@ async function findSwitch(cwd, slug) {
|
|
|
7149
7781
|
const walk = async (folder) => {
|
|
7150
7782
|
let items;
|
|
7151
7783
|
try {
|
|
7152
|
-
items = await
|
|
7784
|
+
items = await readdir4(join13(cwd, folder), { withFileTypes: true });
|
|
7153
7785
|
} catch {
|
|
7154
7786
|
return null;
|
|
7155
7787
|
}
|
|
7156
7788
|
for (const item of items) {
|
|
7157
7789
|
if (seen >= SCAN_LIMIT || item.name.startsWith(".") || item.name === "node_modules")
|
|
7158
7790
|
continue;
|
|
7159
|
-
const path =
|
|
7791
|
+
const path = join13(folder, item.name);
|
|
7160
7792
|
if (item.isDirectory()) {
|
|
7161
7793
|
const found = await walk(path);
|
|
7162
7794
|
if (found !== null)
|
|
@@ -7166,7 +7798,7 @@ async function findSwitch(cwd, slug) {
|
|
|
7166
7798
|
if (!SOURCE.test(item.name))
|
|
7167
7799
|
continue;
|
|
7168
7800
|
seen += 1;
|
|
7169
|
-
const source = await
|
|
7801
|
+
const source = await readFile13(join13(cwd, path), "utf8").catch(() => "");
|
|
7170
7802
|
if (source.includes(wanted) && MAP.test(source))
|
|
7171
7803
|
return path;
|
|
7172
7804
|
}
|
|
@@ -7175,7 +7807,7 @@ async function findSwitch(cwd, slug) {
|
|
|
7175
7807
|
for (const root of ["src", "app"]) {
|
|
7176
7808
|
const found = await walk(root);
|
|
7177
7809
|
if (found !== null)
|
|
7178
|
-
return relative3(cwd,
|
|
7810
|
+
return relative3(cwd, join13(cwd, found));
|
|
7179
7811
|
}
|
|
7180
7812
|
return null;
|
|
7181
7813
|
}
|
|
@@ -7243,21 +7875,21 @@ function clip(source, limit) {
|
|
|
7243
7875
|
var BASELINE_LINES = 15;
|
|
7244
7876
|
var STUB = /Re-export what your|A first direction for/;
|
|
7245
7877
|
async function text2(path) {
|
|
7246
|
-
return
|
|
7878
|
+
return readFile14(path, "utf8").catch(() => null);
|
|
7247
7879
|
}
|
|
7248
7880
|
async function firstFile(cwd, candidates) {
|
|
7249
7881
|
for (const candidate of candidates) {
|
|
7250
|
-
if (await text2(
|
|
7882
|
+
if (await text2(join14(cwd, candidate)) !== null)
|
|
7251
7883
|
return candidate;
|
|
7252
7884
|
}
|
|
7253
7885
|
return null;
|
|
7254
7886
|
}
|
|
7255
7887
|
function cssImports(source, from) {
|
|
7256
|
-
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) =>
|
|
7888
|
+
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) => normalize2(join14(dirname7(from), match[1] ?? "")));
|
|
7257
7889
|
}
|
|
7258
7890
|
async function globalStylesheet(cwd) {
|
|
7259
7891
|
const entries = [];
|
|
7260
|
-
const html = await text2(
|
|
7892
|
+
const html = await text2(join14(cwd, "index.html"));
|
|
7261
7893
|
const script = html === null ? null : /<script[^>]+type="module"[^>]+src="\/?([^"]+)"/.exec(html);
|
|
7262
7894
|
if (script?.[1] !== void 0)
|
|
7263
7895
|
entries.push(script[1]);
|
|
@@ -7267,12 +7899,12 @@ async function globalStylesheet(cwd) {
|
|
|
7267
7899
|
}
|
|
7268
7900
|
const sheets = [];
|
|
7269
7901
|
for (const entry of entries) {
|
|
7270
|
-
const source = await text2(
|
|
7902
|
+
const source = await text2(join14(cwd, entry));
|
|
7271
7903
|
if (source !== null)
|
|
7272
7904
|
sheets.push(...cssImports(source, entry));
|
|
7273
7905
|
}
|
|
7274
7906
|
for (const sheet of sheets) {
|
|
7275
|
-
const source = await text2(
|
|
7907
|
+
const source = await text2(join14(cwd, sheet));
|
|
7276
7908
|
if (source !== null && /:root\s*\{|@font-face/.test(source))
|
|
7277
7909
|
return sheet;
|
|
7278
7910
|
}
|
|
@@ -7283,7 +7915,7 @@ async function images(cwd) {
|
|
|
7283
7915
|
const walk = async (folder) => {
|
|
7284
7916
|
let items;
|
|
7285
7917
|
try {
|
|
7286
|
-
items = await
|
|
7918
|
+
items = await readdir5(join14(cwd, "public", folder), { withFileTypes: true });
|
|
7287
7919
|
} catch {
|
|
7288
7920
|
return;
|
|
7289
7921
|
}
|
|
@@ -7301,14 +7933,14 @@ async function images(cwd) {
|
|
|
7301
7933
|
return found;
|
|
7302
7934
|
}
|
|
7303
7935
|
async function componentFile(cwd, from, specifier) {
|
|
7304
|
-
const base =
|
|
7936
|
+
const base = normalize2(join14(dirname7(from), specifier));
|
|
7305
7937
|
return firstFile(cwd, [
|
|
7306
7938
|
base,
|
|
7307
7939
|
...["tsx", "jsx", "ts", "js"].map((extension) => `${base}.${extension}`)
|
|
7308
7940
|
]);
|
|
7309
7941
|
}
|
|
7310
7942
|
async function example(cwd, switchPath, prefer) {
|
|
7311
|
-
const source = await text2(
|
|
7943
|
+
const source = await text2(join14(cwd, switchPath));
|
|
7312
7944
|
if (source === null)
|
|
7313
7945
|
return null;
|
|
7314
7946
|
const directions = readDirections(source);
|
|
@@ -7317,13 +7949,13 @@ async function example(cwd, switchPath, prefer) {
|
|
|
7317
7949
|
for (const direction of ordered) {
|
|
7318
7950
|
const limit = direction.key === prefer ? VARIED_LINES : EXAMPLE_LINES;
|
|
7319
7951
|
const path = await componentFile(cwd, switchPath, direction.from);
|
|
7320
|
-
const body = path === null ? null : await text2(
|
|
7952
|
+
const body = path === null ? null : await text2(join14(cwd, path));
|
|
7321
7953
|
if (path === null || body === null || body === placeholderSource(direction.name) || STUB.test(body))
|
|
7322
7954
|
continue;
|
|
7323
7955
|
if (body.split("\n").length < BASELINE_LINES) {
|
|
7324
7956
|
const target = /from\s+["'](\.[^"']+)["']/.exec(body)?.[1];
|
|
7325
7957
|
const followed = target === void 0 ? null : await componentFile(cwd, path, target);
|
|
7326
|
-
const real = followed === null ? null : await text2(
|
|
7958
|
+
const real = followed === null ? null : await text2(join14(cwd, followed));
|
|
7327
7959
|
if (followed !== null && real !== null) {
|
|
7328
7960
|
return {
|
|
7329
7961
|
key: direction.key,
|
|
@@ -7339,7 +7971,7 @@ async function example(cwd, switchPath, prefer) {
|
|
|
7339
7971
|
}
|
|
7340
7972
|
async function dependencyNames(cwd) {
|
|
7341
7973
|
try {
|
|
7342
|
-
const pkg = parseJson(await text2(
|
|
7974
|
+
const pkg = parseJson(await text2(join14(cwd, "package.json")) ?? "");
|
|
7343
7975
|
return isJsonRecord(pkg) && isJsonRecord(pkg.dependencies) ? Object.keys(pkg.dependencies) : [];
|
|
7344
7976
|
} catch {
|
|
7345
7977
|
return [];
|
|
@@ -7348,7 +7980,7 @@ async function dependencyNames(cwd) {
|
|
|
7348
7980
|
async function readProjectFacts(cwd, switchPath, prefer = null) {
|
|
7349
7981
|
const dependencies = await dependencyNames(cwd);
|
|
7350
7982
|
const sheetPath = await globalStylesheet(cwd);
|
|
7351
|
-
const css = sheetPath === null ? null : await text2(
|
|
7983
|
+
const css = sheetPath === null ? null : await text2(join14(cwd, sheetPath));
|
|
7352
7984
|
return {
|
|
7353
7985
|
dependencies,
|
|
7354
7986
|
stylesheet: sheetPath === null || css === null ? null : {
|
|
@@ -7425,7 +8057,13 @@ ${input.facts}
|
|
|
7425
8057
|
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.`;
|
|
7426
8058
|
}
|
|
7427
8059
|
function fixPrompt(input) {
|
|
7428
|
-
|
|
8060
|
+
const layout = input.layout ?? [];
|
|
8061
|
+
const reported = `Leglas rendered ${input.file} and the page reported: ${input.errors.slice(0, 3).join(" | ")}.`;
|
|
8062
|
+
if (layout.length === 0) {
|
|
8063
|
+
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.`;
|
|
8064
|
+
}
|
|
8065
|
+
const opening = input.errors.length === 0 ? `Leglas rendered ${input.file}.` : reported;
|
|
8066
|
+
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.`;
|
|
7429
8067
|
}
|
|
7430
8068
|
function kebab(value) {
|
|
7431
8069
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -7476,6 +8114,16 @@ var OWN_FAILURES = /* @__PURE__ */ new Set([
|
|
|
7476
8114
|
]);
|
|
7477
8115
|
var BUSY = "A set of directions is already being built. Wait for it, or stop it first.";
|
|
7478
8116
|
var KEPT_JOBS = 5;
|
|
8117
|
+
var guardKey = (slot, attempt) => `${slot.key}#${attempt}`;
|
|
8118
|
+
function listed(items) {
|
|
8119
|
+
return items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
8120
|
+
}
|
|
8121
|
+
function strayMessage(paths, restored, named) {
|
|
8122
|
+
const which = paths.length === 1 ? "which is not its own file" : "which are not its own files";
|
|
8123
|
+
const back = restored.length === 0 ? "" : restored.length === paths.length ? ` and put ${paths.length === 1 ? "the file" : "them"} back` : ` and put ${listed(restored)} back`;
|
|
8124
|
+
const check = named.length === 0 ? "." : `. Check ${listed(named)} before trying again.`;
|
|
8125
|
+
return `The build edited ${listed(paths)}, ${which}, so Leglas stopped it${back}${check}`;
|
|
8126
|
+
}
|
|
7479
8127
|
var CLAUDE = {
|
|
7480
8128
|
id: "claude",
|
|
7481
8129
|
name: "Claude",
|
|
@@ -7492,6 +8140,10 @@ function codex(servers) {
|
|
|
7492
8140
|
result: codexResultText
|
|
7493
8141
|
};
|
|
7494
8142
|
}
|
|
8143
|
+
function argsOf(agent) {
|
|
8144
|
+
const mark = "\0prompt";
|
|
8145
|
+
return agent.build(mark).map((arg) => arg === mark ? "<prompt>" : arg);
|
|
8146
|
+
}
|
|
7495
8147
|
function wait(milliseconds) {
|
|
7496
8148
|
return new Promise((resolve5) => {
|
|
7497
8149
|
setTimeout(resolve5, milliseconds).unref?.();
|
|
@@ -7512,7 +8164,7 @@ function baseOf(preview, surface) {
|
|
|
7512
8164
|
async function stackOf(cwd, switchPath) {
|
|
7513
8165
|
let deps = {};
|
|
7514
8166
|
try {
|
|
7515
|
-
const pkg = parseJson(await
|
|
8167
|
+
const pkg = parseJson(await readFile15(join15(cwd, "package.json"), "utf8"));
|
|
7516
8168
|
if (isJsonRecord(pkg)) {
|
|
7517
8169
|
for (const group of [pkg.devDependencies, pkg.dependencies]) {
|
|
7518
8170
|
if (!isJsonRecord(group))
|
|
@@ -7560,6 +8212,81 @@ function createGenerations(deps) {
|
|
|
7560
8212
|
let closed = false;
|
|
7561
8213
|
const busy = () => lives.some((live) => live.job.state === "planning" || live.job.state === "building");
|
|
7562
8214
|
const changed = () => deps.onChange();
|
|
8215
|
+
const records = createRecords(deps.cwd);
|
|
8216
|
+
const runRecord = (prompt) => ({
|
|
8217
|
+
prompt,
|
|
8218
|
+
startedAt: now(),
|
|
8219
|
+
endedAt: null,
|
|
8220
|
+
exitCode: null,
|
|
8221
|
+
timedOut: false,
|
|
8222
|
+
error: null,
|
|
8223
|
+
result: null,
|
|
8224
|
+
tail: []
|
|
8225
|
+
});
|
|
8226
|
+
const ended = (agent, record3, outcome) => {
|
|
8227
|
+
const failed = outcome.timedOut || outcome.error !== null || outcome.code !== 0;
|
|
8228
|
+
record3.endedAt = now();
|
|
8229
|
+
record3.exitCode = outcome.code;
|
|
8230
|
+
record3.timedOut = outcome.timedOut;
|
|
8231
|
+
record3.error = outcome.error;
|
|
8232
|
+
record3.result = agent.result(outcome.lines);
|
|
8233
|
+
record3.tail = failed ? outcome.lines.slice(-40) : [];
|
|
8234
|
+
};
|
|
8235
|
+
const attemptRecord = (live, key, attempt) => live.record?.directions.find((direction) => direction.key === key)?.attempts.find((each) => each.attempt === attempt);
|
|
8236
|
+
const opened = (live, slot, attempt, cause) => {
|
|
8237
|
+
live.record?.directions.find((direction) => direction.key === slot.key)?.attempts.push({
|
|
8238
|
+
attempt,
|
|
8239
|
+
cause,
|
|
8240
|
+
title: slot.title,
|
|
8241
|
+
idea: slot.idea,
|
|
8242
|
+
ideaRun: null,
|
|
8243
|
+
build: null,
|
|
8244
|
+
renders: [],
|
|
8245
|
+
fix: null,
|
|
8246
|
+
state: slot.state,
|
|
8247
|
+
failure: null,
|
|
8248
|
+
fixed: false,
|
|
8249
|
+
layout: [],
|
|
8250
|
+
picture: null
|
|
8251
|
+
});
|
|
8252
|
+
};
|
|
8253
|
+
const saveRecord = (live) => {
|
|
8254
|
+
const { job, record: record3 } = live;
|
|
8255
|
+
if (record3 === null)
|
|
8256
|
+
return;
|
|
8257
|
+
record3.state = job.state;
|
|
8258
|
+
record3.error = job.error;
|
|
8259
|
+
record3.plannedAt = job.plannedAt;
|
|
8260
|
+
record3.endedAt = job.endedAt;
|
|
8261
|
+
for (const slot of job.slots) {
|
|
8262
|
+
const latest = record3.directions.find((each) => each.key === slot.key)?.attempts.at(-1);
|
|
8263
|
+
if (latest === void 0)
|
|
8264
|
+
continue;
|
|
8265
|
+
latest.title = slot.title;
|
|
8266
|
+
latest.idea = slot.idea;
|
|
8267
|
+
latest.state = slot.state;
|
|
8268
|
+
latest.failure = slot.failure === null ? null : { ...slot.failure };
|
|
8269
|
+
latest.fixed = slot.fixed;
|
|
8270
|
+
latest.layout = [...slot.layout];
|
|
8271
|
+
}
|
|
8272
|
+
records.save(record3);
|
|
8273
|
+
};
|
|
8274
|
+
const noted = (live, kind, direction) => {
|
|
8275
|
+
if (live.record === null)
|
|
8276
|
+
return;
|
|
8277
|
+
records.event(live.record.id, { at: now(), kind, direction });
|
|
8278
|
+
saveRecord(live);
|
|
8279
|
+
};
|
|
8280
|
+
const rendered = (live, slot, attempt, report) => {
|
|
8281
|
+
const record3 = attemptRecord(live, slot.key, attempt);
|
|
8282
|
+
if (live.record === null || record3 === void 0)
|
|
8283
|
+
return;
|
|
8284
|
+
record3.renders.push({ at: now(), errors: [...report.errors], layout: [...report.layout] });
|
|
8285
|
+
if (report.frame === null)
|
|
8286
|
+
return;
|
|
8287
|
+
record3.picture = `${slot.key}-${attempt}.jpg`;
|
|
8288
|
+
records.picture(live.record.id, record3.picture, report.frame);
|
|
8289
|
+
};
|
|
7563
8290
|
let registry = Promise.resolve();
|
|
7564
8291
|
const serially = (task) => {
|
|
7565
8292
|
const next = registry.then(task, task);
|
|
@@ -7620,8 +8347,10 @@ function createGenerations(deps) {
|
|
|
7620
8347
|
});
|
|
7621
8348
|
child.once("close", (code) => {
|
|
7622
8349
|
clearTimeout(deadline);
|
|
7623
|
-
if (buffered !== "")
|
|
8350
|
+
if (buffered !== "") {
|
|
7624
8351
|
lines2.push(buffered);
|
|
8352
|
+
onLine?.(buffered);
|
|
8353
|
+
}
|
|
7625
8354
|
resolve5({ code, lines: lines2, error: null, timedOut: timedOut2 });
|
|
7626
8355
|
});
|
|
7627
8356
|
});
|
|
@@ -7664,7 +8393,7 @@ function createGenerations(deps) {
|
|
|
7664
8393
|
job.endedAt = now();
|
|
7665
8394
|
};
|
|
7666
8395
|
const restore = async (slot) => {
|
|
7667
|
-
await
|
|
8396
|
+
await writeFile8(join15(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), "utf8").catch(() => {
|
|
7668
8397
|
});
|
|
7669
8398
|
};
|
|
7670
8399
|
const begin = (live, slot) => {
|
|
@@ -7721,8 +8450,85 @@ function createGenerations(deps) {
|
|
|
7721
8450
|
fail(live, slot, { code: "unexpected", message: message2 });
|
|
7722
8451
|
settle(live);
|
|
7723
8452
|
changed();
|
|
8453
|
+
saveRecord(live);
|
|
8454
|
+
};
|
|
8455
|
+
const strayed = async (live, slot, attempt) => {
|
|
8456
|
+
const guard = live.guards.get(guardKey(slot, attempt));
|
|
8457
|
+
if (guard === void 0 || guard.strays.size === 0)
|
|
8458
|
+
return null;
|
|
8459
|
+
const paths = [...guard.strays];
|
|
8460
|
+
const restored = [];
|
|
8461
|
+
const named = [];
|
|
8462
|
+
const building = new Set(lives.flatMap((other) => other.job.slots.flatMap((each) => each.state === "building" || each.state === "checking" ? [normalize3(each.file)] : [])));
|
|
8463
|
+
for (const path of paths) {
|
|
8464
|
+
const kept = guard.kept.get(path);
|
|
8465
|
+
if (kept === void 0 || building.has(path)) {
|
|
8466
|
+
named.push(path);
|
|
8467
|
+
continue;
|
|
8468
|
+
}
|
|
8469
|
+
const back = await writeFile8(join15(deps.cwd, path), kept, "utf8").then(() => true, () => false);
|
|
8470
|
+
(back ? restored : named).push(path);
|
|
8471
|
+
}
|
|
8472
|
+
guard.strays.clear();
|
|
8473
|
+
return {
|
|
8474
|
+
failure: { code: "outside-file", message: strayMessage(paths, restored, named) },
|
|
8475
|
+
putBack: named.length === 0
|
|
8476
|
+
};
|
|
8477
|
+
};
|
|
8478
|
+
const pendingStrays = (except) => {
|
|
8479
|
+
const pending = /* @__PURE__ */ new Map();
|
|
8480
|
+
for (const [key, guard] of lives.flatMap((other) => [...other.guards.entries()])) {
|
|
8481
|
+
if (except !== void 0 && key.startsWith(`${except.key}#`))
|
|
8482
|
+
continue;
|
|
8483
|
+
for (const path of guard.strays) {
|
|
8484
|
+
const kept = guard.kept.get(path);
|
|
8485
|
+
if (kept !== void 0)
|
|
8486
|
+
pending.set(path, kept);
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
return pending;
|
|
8490
|
+
};
|
|
8491
|
+
const keptFiles = async (live, own = new Set(live.job.slots.map((slot) => normalize3(slot.file)))) => {
|
|
8492
|
+
const switchPath = live.switchPath;
|
|
8493
|
+
const pending = pendingStrays();
|
|
8494
|
+
const read = async (path) => pending.get(normalize3(path)) ?? await readFile15(join15(deps.cwd, path), "utf8").catch(() => null);
|
|
8495
|
+
const source = await read(switchPath);
|
|
8496
|
+
const kept = /* @__PURE__ */ new Map();
|
|
8497
|
+
if (source === null)
|
|
8498
|
+
return kept;
|
|
8499
|
+
kept.set(normalize3(switchPath), source);
|
|
8500
|
+
const keep = async (path) => {
|
|
8501
|
+
if (path === null || own.has(normalize3(path)) || kept.has(normalize3(path)))
|
|
8502
|
+
return null;
|
|
8503
|
+
const body = await read(path);
|
|
8504
|
+
if (body !== null)
|
|
8505
|
+
kept.set(normalize3(path), body);
|
|
8506
|
+
return body;
|
|
8507
|
+
};
|
|
8508
|
+
for (const direction of readDirections(source)) {
|
|
8509
|
+
const path = await componentFile(deps.cwd, switchPath, direction.from);
|
|
8510
|
+
const body = await keep(path);
|
|
8511
|
+
if (path === null || body === null)
|
|
8512
|
+
continue;
|
|
8513
|
+
for (const match of body.matchAll(/(?:from\s+|import\s*\(?\s*)["'](\.{1,2}\/[^"']+)["']/g)) {
|
|
8514
|
+
await keep(await componentFile(deps.cwd, path, match[1] ?? ""));
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
return kept;
|
|
7724
8518
|
};
|
|
7725
8519
|
const follow = (live, slot, attempt) => (line) => {
|
|
8520
|
+
const guard = live.guards.get(guardKey(slot, attempt));
|
|
8521
|
+
for (const path of editedFiles(live.agent.id, line, deps.cwd)) {
|
|
8522
|
+
if (guard === void 0 || path === normalize3(slot.file) || guard.strays.has(path))
|
|
8523
|
+
continue;
|
|
8524
|
+
guard.strays.add(path);
|
|
8525
|
+
if (guard.strays.size > 1 || !owns(live, slot, attempt))
|
|
8526
|
+
continue;
|
|
8527
|
+
const running = live.runs.get(slot.key);
|
|
8528
|
+
if (running !== void 0)
|
|
8529
|
+
detach(live, running.stop(), () => {
|
|
8530
|
+
});
|
|
8531
|
+
}
|
|
7726
8532
|
const activity = activityFrom(live.agent.id, line, deps.cwd);
|
|
7727
8533
|
if (activity === null || activity === slot.activity || !owns(live, slot, attempt))
|
|
7728
8534
|
return;
|
|
@@ -7739,8 +8545,21 @@ function createGenerations(deps) {
|
|
|
7739
8545
|
slot.endedAt = null;
|
|
7740
8546
|
slot.failure = null;
|
|
7741
8547
|
slot.fixed = false;
|
|
8548
|
+
slot.layout = [];
|
|
7742
8549
|
slot.activity = null;
|
|
7743
8550
|
changed();
|
|
8551
|
+
const key = guardKey(slot, attempt);
|
|
8552
|
+
if (!live.guards.has(key))
|
|
8553
|
+
live.guards.set(key, { kept: await keptFiles(live), strays: /* @__PURE__ */ new Set() });
|
|
8554
|
+
try {
|
|
8555
|
+
if (!owns(live, slot, attempt))
|
|
8556
|
+
return;
|
|
8557
|
+
await buildOwned(live, slot, attempt, concept);
|
|
8558
|
+
} finally {
|
|
8559
|
+
live.guards.delete(key);
|
|
8560
|
+
}
|
|
8561
|
+
};
|
|
8562
|
+
const buildOwned = async (live, slot, attempt, concept) => {
|
|
7744
8563
|
const others = [...live.concepts.values()].filter((other) => other !== concept);
|
|
7745
8564
|
const prompt = buildPrompt({
|
|
7746
8565
|
surface: live.job.surface,
|
|
@@ -7753,17 +8572,26 @@ function createGenerations(deps) {
|
|
|
7753
8572
|
facts: live.facts ?? "",
|
|
7754
8573
|
base: live.base
|
|
7755
8574
|
});
|
|
8575
|
+
const buildRun = runRecord(prompt);
|
|
8576
|
+
const own = attemptRecord(live, slot.key, attempt);
|
|
8577
|
+
if (own !== void 0)
|
|
8578
|
+
own.build = buildRun;
|
|
7756
8579
|
const building = run4(live.agent, live.agent.build(prompt), BUILD_DEADLINE_MS, follow(live, slot, attempt));
|
|
7757
8580
|
live.runs.set(slot.key, building);
|
|
7758
8581
|
const outcome = await building.done;
|
|
8582
|
+
ended(live.agent, buildRun, outcome);
|
|
8583
|
+
saveRecord(live);
|
|
8584
|
+
const outside = await strayed(live, slot, attempt);
|
|
7759
8585
|
if (!owns(live, slot, attempt))
|
|
7760
8586
|
return;
|
|
7761
8587
|
if (live.runs.get(slot.key) === building)
|
|
7762
8588
|
live.runs.delete(slot.key);
|
|
7763
|
-
const written = await
|
|
8589
|
+
const written = await readFile15(join15(deps.cwd, slot.file), "utf8").catch(() => "");
|
|
7764
8590
|
if (!owns(live, slot, attempt))
|
|
7765
8591
|
return;
|
|
7766
|
-
if (
|
|
8592
|
+
if (outside !== null) {
|
|
8593
|
+
fail(live, slot, outside.failure);
|
|
8594
|
+
} else if (outcome.timedOut) {
|
|
7767
8595
|
fail(live, slot, {
|
|
7768
8596
|
code: "too-slow",
|
|
7769
8597
|
message: `The build took longer than ${BUILD_DEADLINE_MS / 6e4} minutes, so Leglas stopped it.`
|
|
@@ -7783,19 +8611,25 @@ function createGenerations(deps) {
|
|
|
7783
8611
|
}
|
|
7784
8612
|
settle(live);
|
|
7785
8613
|
changed();
|
|
8614
|
+
saveRecord(live);
|
|
7786
8615
|
};
|
|
7787
|
-
const
|
|
8616
|
+
const ownReport = async (live, slot, attempt) => {
|
|
7788
8617
|
const others = live.job.slots.filter((other) => other !== slot);
|
|
7789
8618
|
const until = now() + BUILD_DEADLINE_MS + FIX_DEADLINE_MS;
|
|
7790
8619
|
for (; ; ) {
|
|
7791
|
-
const report = await deps.render(slot.title);
|
|
8620
|
+
const report = await deps.render(slot.title, { picture: live.record !== null });
|
|
7792
8621
|
if (!owns(live, slot, attempt))
|
|
7793
8622
|
return null;
|
|
7794
8623
|
const errors = report?.errors ?? [];
|
|
7795
8624
|
const names = (file) => errors.some((error) => error.includes(file));
|
|
7796
|
-
const
|
|
8625
|
+
const pending = [...pendingStrays(slot).keys()];
|
|
8626
|
+
const waiting = !names(slot.file) && (pending.some(names) || others.some((other) => (other.state === "building" || other.state === "checking") && names(other.file)));
|
|
7797
8627
|
if (!waiting || now() >= until) {
|
|
7798
|
-
return
|
|
8628
|
+
return {
|
|
8629
|
+
errors: errors.filter((error) => !others.some((other) => error.includes(other.file))),
|
|
8630
|
+
layout: [...report?.layout ?? []],
|
|
8631
|
+
frame: report?.frame ?? null
|
|
8632
|
+
};
|
|
7799
8633
|
}
|
|
7800
8634
|
await wait(RECHECK_MS);
|
|
7801
8635
|
if (!owns(live, slot, attempt))
|
|
@@ -7803,35 +8637,67 @@ function createGenerations(deps) {
|
|
|
7803
8637
|
}
|
|
7804
8638
|
};
|
|
7805
8639
|
const check = async (live, slot, attempt) => {
|
|
7806
|
-
let
|
|
7807
|
-
if (
|
|
8640
|
+
let report = await ownReport(live, slot, attempt);
|
|
8641
|
+
if (report === null)
|
|
7808
8642
|
return;
|
|
7809
|
-
|
|
7810
|
-
|
|
8643
|
+
rendered(live, slot, attempt, report);
|
|
8644
|
+
if (report.errors.length > 0 || report.layout.length > 0) {
|
|
8645
|
+
const repairs = report.errors.length > 0;
|
|
8646
|
+
const found = report.layout;
|
|
8647
|
+
const working = repairs ? null : await readFile15(join15(deps.cwd, slot.file), "utf8").catch(() => null);
|
|
8648
|
+
if (!owns(live, slot, attempt))
|
|
8649
|
+
return;
|
|
8650
|
+
const prompt = fixPrompt({ file: slot.file, errors: report.errors, layout: report.layout });
|
|
8651
|
+
const fixRun = runRecord(prompt);
|
|
8652
|
+
const own = attemptRecord(live, slot.key, attempt);
|
|
8653
|
+
if (own !== void 0)
|
|
8654
|
+
own.fix = fixRun;
|
|
8655
|
+
const fixing = run4(live.agent, live.agent.build(prompt), FIX_DEADLINE_MS, follow(live, slot, attempt));
|
|
7811
8656
|
live.runs.set(slot.key, fixing);
|
|
7812
8657
|
const outcome = await fixing.done;
|
|
8658
|
+
ended(live.agent, fixRun, outcome);
|
|
8659
|
+
const outside = await strayed(live, slot, attempt);
|
|
7813
8660
|
if (!owns(live, slot, attempt))
|
|
7814
8661
|
return;
|
|
7815
8662
|
slot.activity = null;
|
|
7816
8663
|
changed();
|
|
7817
8664
|
if (live.runs.get(slot.key) === fixing)
|
|
7818
8665
|
live.runs.delete(slot.key);
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
8666
|
+
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);
|
|
8667
|
+
if (failure === null) {
|
|
8668
|
+
slot.fixed = repairs;
|
|
8669
|
+
report = await ownReport(live, slot, attempt);
|
|
8670
|
+
if (report === null)
|
|
8671
|
+
return;
|
|
8672
|
+
rendered(live, slot, attempt, report);
|
|
8673
|
+
if (report.errors.length > 0) {
|
|
8674
|
+
failure = {
|
|
8675
|
+
code: "broken",
|
|
8676
|
+
message: `The page still reports: ${report.errors[0] ?? ""}`
|
|
8677
|
+
};
|
|
8678
|
+
}
|
|
7822
8679
|
}
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
8680
|
+
const worse = failure === null && !repairs && report.layout.length > found.length;
|
|
8681
|
+
if (failure !== null || worse) {
|
|
8682
|
+
if (working !== null && outside?.putBack !== false) {
|
|
8683
|
+
const restored = await writeFile8(join15(deps.cwd, slot.file), working, "utf8").then(() => true, () => false);
|
|
8684
|
+
if (!owns(live, slot, attempt))
|
|
8685
|
+
return;
|
|
8686
|
+
if (restored) {
|
|
8687
|
+
slot.fixed = false;
|
|
8688
|
+
slot.layout = found;
|
|
8689
|
+
slot.state = "ready";
|
|
8690
|
+
slot.endedAt = now();
|
|
8691
|
+
return;
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
if (failure !== null) {
|
|
8695
|
+
fail(live, slot, failure);
|
|
8696
|
+
return;
|
|
8697
|
+
}
|
|
7833
8698
|
}
|
|
7834
8699
|
}
|
|
8700
|
+
slot.layout = report.layout;
|
|
7835
8701
|
slot.state = "ready";
|
|
7836
8702
|
slot.endedAt = now();
|
|
7837
8703
|
};
|
|
@@ -7844,16 +8710,22 @@ function createGenerations(deps) {
|
|
|
7844
8710
|
};
|
|
7845
8711
|
const plan = async (live, existing) => {
|
|
7846
8712
|
const { job } = live;
|
|
7847
|
-
const
|
|
8713
|
+
const prompt = planPrompt({
|
|
7848
8714
|
surface: job.surface,
|
|
7849
8715
|
brief: job.brief,
|
|
7850
8716
|
count: job.count,
|
|
7851
8717
|
existing,
|
|
7852
8718
|
base: live.base
|
|
7853
|
-
})
|
|
8719
|
+
});
|
|
8720
|
+
const planRun = { ...runRecord(prompt), concepts: [] };
|
|
8721
|
+
if (live.record !== null)
|
|
8722
|
+
live.record.plan = planRun;
|
|
8723
|
+
const planning = run4(live.agent, live.agent.plan(prompt), PLAN_DEADLINE_MS);
|
|
7854
8724
|
live.plan = planning;
|
|
7855
8725
|
const outcome = await planning.done;
|
|
7856
8726
|
live.plan = null;
|
|
8727
|
+
ended(live.agent, planRun, outcome);
|
|
8728
|
+
saveRecord(live);
|
|
7857
8729
|
if (job.state !== "planning")
|
|
7858
8730
|
return;
|
|
7859
8731
|
if (outcome.timedOut || outcome.error !== null || outcome.code !== 0) {
|
|
@@ -7861,6 +8733,7 @@ function createGenerations(deps) {
|
|
|
7861
8733
|
job.error = outcome.timedOut ? "Planning took too long, so Leglas stopped it." : failureOf2(live.agent, outcome).message;
|
|
7862
8734
|
job.endedAt = now();
|
|
7863
8735
|
changed();
|
|
8736
|
+
saveRecord(live);
|
|
7864
8737
|
return;
|
|
7865
8738
|
}
|
|
7866
8739
|
let concepts;
|
|
@@ -7871,8 +8744,10 @@ function createGenerations(deps) {
|
|
|
7871
8744
|
job.error = error instanceof Error ? error.message : String(error);
|
|
7872
8745
|
job.endedAt = now();
|
|
7873
8746
|
changed();
|
|
8747
|
+
saveRecord(live);
|
|
7874
8748
|
return;
|
|
7875
8749
|
}
|
|
8750
|
+
planRun.concepts = concepts;
|
|
7876
8751
|
live.facts = factsBlock(await readProjectFacts(deps.cwd, live.switchPath, live.base?.key ?? null), job.surface, live.base);
|
|
7877
8752
|
const slug = surfaceSlug(job.surface);
|
|
7878
8753
|
const keys = new Set(existing);
|
|
@@ -7880,7 +8755,7 @@ function createGenerations(deps) {
|
|
|
7880
8755
|
const folder = dirname8(live.switchPath);
|
|
7881
8756
|
const slots = [];
|
|
7882
8757
|
const extension = live.switchPath.endsWith(".jsx") ? "jsx" : "tsx";
|
|
7883
|
-
const taken = async (key) => keys.has(key) || await isFile(
|
|
8758
|
+
const taken = async (key) => keys.has(key) || await isFile(join15(deps.cwd, folder, `${key}.${extension}`));
|
|
7884
8759
|
for (const concept of concepts) {
|
|
7885
8760
|
let key = `${slug}-${concept.key}`;
|
|
7886
8761
|
for (let suffix = 2; await taken(key); suffix += 1)
|
|
@@ -7892,23 +8767,25 @@ function createGenerations(deps) {
|
|
|
7892
8767
|
key,
|
|
7893
8768
|
title,
|
|
7894
8769
|
idea: concept.idea,
|
|
7895
|
-
file:
|
|
8770
|
+
file: join15(folder, `${key}.${extension}`),
|
|
7896
8771
|
state: "building",
|
|
7897
8772
|
startedAt: null,
|
|
7898
8773
|
endedAt: null,
|
|
7899
8774
|
failure: null,
|
|
7900
8775
|
fixed: false,
|
|
8776
|
+
layout: [],
|
|
7901
8777
|
activity: null
|
|
7902
8778
|
});
|
|
7903
8779
|
}
|
|
7904
|
-
const source = await
|
|
8780
|
+
const source = await readFile15(join15(deps.cwd, live.switchPath), "utf8");
|
|
7905
8781
|
for (const slot of slots) {
|
|
7906
|
-
await
|
|
8782
|
+
await writeFile8(join15(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), {
|
|
7907
8783
|
encoding: "utf8",
|
|
7908
8784
|
flag: "wx"
|
|
7909
8785
|
});
|
|
7910
8786
|
}
|
|
7911
|
-
await
|
|
8787
|
+
await writeFile8(join15(deps.cwd, live.switchPath), addSlots(source, slots.map((slot) => ({ key: slot.key, name: pascal(slot.key) }))), "utf8");
|
|
8788
|
+
const kept = await keptFiles(live, new Set(slots.map((slot) => normalize3(slot.file))));
|
|
7912
8789
|
for (const slot of slots) {
|
|
7913
8790
|
await serially(() => deps.register({
|
|
7914
8791
|
title: slot.title,
|
|
@@ -7918,21 +8795,33 @@ function createGenerations(deps) {
|
|
|
7918
8795
|
}));
|
|
7919
8796
|
}
|
|
7920
8797
|
job.slots = slots;
|
|
8798
|
+
if (live.record !== null) {
|
|
8799
|
+
live.record.directions = slots.map((slot) => ({
|
|
8800
|
+
key: slot.key,
|
|
8801
|
+
file: slot.file,
|
|
8802
|
+
attempts: []
|
|
8803
|
+
}));
|
|
8804
|
+
}
|
|
7921
8805
|
if (job.state !== "planning") {
|
|
7922
8806
|
for (const slot of slots) {
|
|
7923
8807
|
slot.state = "stopped";
|
|
7924
8808
|
slot.endedAt = now();
|
|
7925
8809
|
}
|
|
7926
8810
|
changed();
|
|
8811
|
+
saveRecord(live);
|
|
7927
8812
|
return;
|
|
7928
8813
|
}
|
|
7929
8814
|
job.state = "building";
|
|
7930
8815
|
job.plannedAt = now();
|
|
7931
8816
|
changed();
|
|
7932
|
-
|
|
8817
|
+
const builds = job.slots.map((slot) => {
|
|
7933
8818
|
const attempt = begin(live, slot);
|
|
8819
|
+
live.guards.set(guardKey(slot, attempt), { kept, strays: /* @__PURE__ */ new Set() });
|
|
8820
|
+
opened(live, slot, attempt, "plan");
|
|
7934
8821
|
return guarded(build(live, slot, attempt), (message2) => lost(live, slot, attempt, message2));
|
|
7935
|
-
})
|
|
8822
|
+
});
|
|
8823
|
+
saveRecord(live);
|
|
8824
|
+
await Promise.all(builds);
|
|
7936
8825
|
};
|
|
7937
8826
|
const find = (id) => lives.find((live) => live.job.id === id);
|
|
7938
8827
|
const stop = async (id, key) => {
|
|
@@ -7944,6 +8833,7 @@ function createGenerations(deps) {
|
|
|
7944
8833
|
job.state = "stopped";
|
|
7945
8834
|
job.endedAt = now();
|
|
7946
8835
|
changed();
|
|
8836
|
+
noted(live, "stop", null);
|
|
7947
8837
|
await live.plan?.stop();
|
|
7948
8838
|
await drain(live);
|
|
7949
8839
|
return true;
|
|
@@ -7956,6 +8846,8 @@ function createGenerations(deps) {
|
|
|
7956
8846
|
}
|
|
7957
8847
|
settle(live);
|
|
7958
8848
|
changed();
|
|
8849
|
+
if (targets.length > 0)
|
|
8850
|
+
noted(live, "stop", key ?? null);
|
|
7959
8851
|
await Promise.all(targets.map((slot) => release(live, slot)));
|
|
7960
8852
|
return targets.length > 0;
|
|
7961
8853
|
};
|
|
@@ -7990,7 +8882,7 @@ function createGenerations(deps) {
|
|
|
7990
8882
|
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.`
|
|
7991
8883
|
};
|
|
7992
8884
|
}
|
|
7993
|
-
const existing = directionKeys(await
|
|
8885
|
+
const existing = directionKeys(await readFile15(join15(deps.cwd, switchPath), "utf8"));
|
|
7994
8886
|
if (base !== null && !existing.includes(base.key)) {
|
|
7995
8887
|
return {
|
|
7996
8888
|
ok: false,
|
|
@@ -8029,19 +8921,54 @@ function createGenerations(deps) {
|
|
|
8029
8921
|
attempts: /* @__PURE__ */ new Map(),
|
|
8030
8922
|
base,
|
|
8031
8923
|
agent: agent === "codex" ? codex(await codexServers(deps.codexHome)) : CLAUDE,
|
|
8032
|
-
|
|
8924
|
+
guards: /* @__PURE__ */ new Map(),
|
|
8925
|
+
pending: /* @__PURE__ */ new Set(),
|
|
8926
|
+
record: null
|
|
8033
8927
|
};
|
|
8928
|
+
const recording = deps.record?.() !== false;
|
|
8929
|
+
if (recording)
|
|
8930
|
+
await records.prune();
|
|
8931
|
+
const baseSet = base === null || !recording ? null : await setOfKey(deps.cwd, base.key).catch(() => null);
|
|
8034
8932
|
if (busy())
|
|
8035
8933
|
return { ok: false, error: BUSY };
|
|
8036
8934
|
lives.push(live);
|
|
8037
8935
|
while (lives.length > KEPT_JOBS)
|
|
8038
8936
|
lives.shift();
|
|
8039
8937
|
changed();
|
|
8938
|
+
if (recording) {
|
|
8939
|
+
live.record = {
|
|
8940
|
+
version: 1,
|
|
8941
|
+
id: job.id,
|
|
8942
|
+
surface: slug,
|
|
8943
|
+
brief,
|
|
8944
|
+
count,
|
|
8945
|
+
agent,
|
|
8946
|
+
args: argsOf(live.agent),
|
|
8947
|
+
basedOn: base === null ? null : { title: base.title, key: base.key, set: baseSet },
|
|
8948
|
+
state: job.state,
|
|
8949
|
+
error: null,
|
|
8950
|
+
startedAt: job.startedAt,
|
|
8951
|
+
plannedAt: null,
|
|
8952
|
+
endedAt: null,
|
|
8953
|
+
plan: null,
|
|
8954
|
+
directions: []
|
|
8955
|
+
};
|
|
8956
|
+
saveRecord(live);
|
|
8957
|
+
if (base !== null && baseSet !== null) {
|
|
8958
|
+
records.event(baseSet, {
|
|
8959
|
+
at: now(),
|
|
8960
|
+
kind: "more-like",
|
|
8961
|
+
direction: base.key,
|
|
8962
|
+
detail: job.id
|
|
8963
|
+
});
|
|
8964
|
+
}
|
|
8965
|
+
}
|
|
8040
8966
|
detach(live, plan(live, existing), (message2) => {
|
|
8041
8967
|
job.state = "failed";
|
|
8042
8968
|
job.error = message2;
|
|
8043
8969
|
job.endedAt = now();
|
|
8044
8970
|
changed();
|
|
8971
|
+
saveRecord(live);
|
|
8045
8972
|
});
|
|
8046
8973
|
return { ok: true, job: copy(job) };
|
|
8047
8974
|
},
|
|
@@ -8056,10 +8983,12 @@ function createGenerations(deps) {
|
|
|
8056
8983
|
slot.state = "building";
|
|
8057
8984
|
slot.startedAt = now();
|
|
8058
8985
|
slot.failure = null;
|
|
8986
|
+
opened(live, slot, attempt, "retry");
|
|
8059
8987
|
detach(live, restore(slot).then(() => build(live, slot, attempt)), (message2) => lost(live, slot, attempt, message2));
|
|
8060
8988
|
live.job.state = "building";
|
|
8061
8989
|
live.job.endedAt = null;
|
|
8062
8990
|
changed();
|
|
8991
|
+
noted(live, "retry", slot.key);
|
|
8063
8992
|
return true;
|
|
8064
8993
|
},
|
|
8065
8994
|
replace(id, key) {
|
|
@@ -8071,18 +9000,27 @@ function createGenerations(deps) {
|
|
|
8071
9000
|
slot.state = "building";
|
|
8072
9001
|
slot.startedAt = now();
|
|
8073
9002
|
slot.failure = null;
|
|
9003
|
+
opened(live, slot, attempt, "replace");
|
|
8074
9004
|
live.job.state = "building";
|
|
8075
9005
|
live.job.endedAt = null;
|
|
8076
9006
|
changed();
|
|
9007
|
+
noted(live, "replace", slot.key);
|
|
8077
9008
|
const replacing = async () => {
|
|
8078
|
-
const
|
|
9009
|
+
const prompt = replacePrompt2({
|
|
8079
9010
|
surface: live.job.surface,
|
|
8080
9011
|
brief: live.job.brief,
|
|
8081
9012
|
avoid: [...live.concepts.values()],
|
|
8082
9013
|
base: live.base
|
|
8083
|
-
})
|
|
9014
|
+
});
|
|
9015
|
+
const ideaRun = runRecord(prompt);
|
|
9016
|
+
const own = attemptRecord(live, slot.key, attempt);
|
|
9017
|
+
if (own !== void 0)
|
|
9018
|
+
own.ideaRun = ideaRun;
|
|
9019
|
+
const asking = run4(live.agent, live.agent.plan(prompt), PLAN_DEADLINE_MS);
|
|
8084
9020
|
live.runs.set(slot.key, asking);
|
|
8085
9021
|
const outcome = await asking.done;
|
|
9022
|
+
ended(live.agent, ideaRun, outcome);
|
|
9023
|
+
saveRecord(live);
|
|
8086
9024
|
if (!owns(live, slot, attempt))
|
|
8087
9025
|
return;
|
|
8088
9026
|
if (live.runs.get(slot.key) === asking)
|
|
@@ -8099,6 +9037,7 @@ function createGenerations(deps) {
|
|
|
8099
9037
|
});
|
|
8100
9038
|
settle(live);
|
|
8101
9039
|
changed();
|
|
9040
|
+
saveRecord(live);
|
|
8102
9041
|
return;
|
|
8103
9042
|
}
|
|
8104
9043
|
const titles = new Set(await deps.titles());
|
|
@@ -8118,6 +9057,7 @@ function createGenerations(deps) {
|
|
|
8118
9057
|
live.concepts.set(slot.key, { ...concept, title });
|
|
8119
9058
|
slot.title = title;
|
|
8120
9059
|
slot.idea = concept.idea;
|
|
9060
|
+
saveRecord(live);
|
|
8121
9061
|
if (!owns(live, slot, attempt))
|
|
8122
9062
|
return;
|
|
8123
9063
|
await restore(slot);
|
|
@@ -8135,35 +9075,36 @@ function createGenerations(deps) {
|
|
|
8135
9075
|
})),
|
|
8136
9076
|
wait(KILL_GRACE_MS * 2 + 1e3)
|
|
8137
9077
|
]);
|
|
9078
|
+
await Promise.race([records.flush(), wait(KILL_GRACE_MS)]);
|
|
8138
9079
|
}
|
|
8139
9080
|
};
|
|
8140
9081
|
}
|
|
8141
9082
|
|
|
8142
9083
|
// ../server/dist/server-info.js
|
|
8143
|
-
import { lstat as lstat2, mkdir as
|
|
8144
|
-
import { dirname as dirname9, join as
|
|
9084
|
+
import { lstat as lstat2, mkdir as mkdir8, readFile as readFile16, rename as rename4, rm as rm6, writeFile as writeFile9 } from "fs/promises";
|
|
9085
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
8145
9086
|
var SERVER_INFO_PATH = ".leglas/server.json";
|
|
8146
9087
|
async function writeServerInfo(cwd, info) {
|
|
8147
|
-
const path =
|
|
9088
|
+
const path = join16(cwd, SERVER_INFO_PATH);
|
|
8148
9089
|
const directory = dirname9(path);
|
|
8149
|
-
await
|
|
9090
|
+
await mkdir8(directory, { recursive: true });
|
|
8150
9091
|
if (!(await lstat2(directory)).isDirectory())
|
|
8151
9092
|
return;
|
|
8152
9093
|
const existing = await lstat2(path).catch(() => null);
|
|
8153
9094
|
if (existing !== null && !existing.isFile())
|
|
8154
9095
|
return;
|
|
8155
9096
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
8156
|
-
await
|
|
9097
|
+
await writeFile9(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
8157
9098
|
`, "utf8");
|
|
8158
|
-
await
|
|
8159
|
-
await
|
|
9099
|
+
await rename4(temporary, path).catch(async (cause) => {
|
|
9100
|
+
await rm6(temporary, { force: true }).catch(() => {
|
|
8160
9101
|
});
|
|
8161
9102
|
throw cause;
|
|
8162
9103
|
});
|
|
8163
9104
|
}
|
|
8164
9105
|
async function readServerInfo(cwd) {
|
|
8165
9106
|
try {
|
|
8166
|
-
const value = parseJson(await
|
|
9107
|
+
const value = parseJson(await readFile16(join16(cwd, SERVER_INFO_PATH), "utf8"));
|
|
8167
9108
|
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))
|
|
8168
9109
|
return null;
|
|
8169
9110
|
return { port: value.port, url: value.url, pid: value.pid };
|
|
@@ -8177,7 +9118,7 @@ async function removeServerInfo(cwd, expected) {
|
|
|
8177
9118
|
if (current !== null && (current.port !== expected.port || current.pid !== expected.pid))
|
|
8178
9119
|
return;
|
|
8179
9120
|
}
|
|
8180
|
-
await
|
|
9121
|
+
await rm6(join16(cwd, SERVER_INFO_PATH), { force: true });
|
|
8181
9122
|
}
|
|
8182
9123
|
|
|
8183
9124
|
// ../server/dist/server.js
|
|
@@ -8326,7 +9267,7 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
8326
9267
|
return resolve5(false);
|
|
8327
9268
|
}
|
|
8328
9269
|
const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
8329
|
-
const socket = net3.connect(port, url.hostname);
|
|
9270
|
+
const socket = net3.connect(port, url.hostname.replace(/^\[|\]$/g, ""));
|
|
8330
9271
|
const settle = (reachable) => {
|
|
8331
9272
|
socket.destroy();
|
|
8332
9273
|
resolve5(reachable);
|
|
@@ -8338,8 +9279,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
8338
9279
|
});
|
|
8339
9280
|
}
|
|
8340
9281
|
function serveFrom(res, dir, relativePath) {
|
|
8341
|
-
const
|
|
8342
|
-
const candidate =
|
|
9282
|
+
const relative9 = normalize4(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
9283
|
+
const candidate = join17(dir, relative9);
|
|
8343
9284
|
if (!candidate.startsWith(dir))
|
|
8344
9285
|
return false;
|
|
8345
9286
|
if (!existsSync3(candidate) || !statSync(candidate).isFile())
|
|
@@ -8353,9 +9294,9 @@ function serveFrom(res, dir, relativePath) {
|
|
|
8353
9294
|
return true;
|
|
8354
9295
|
}
|
|
8355
9296
|
function serveShellFile(res, shellDir, urlPath) {
|
|
8356
|
-
const
|
|
8357
|
-
const isRoot =
|
|
8358
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
9297
|
+
const relative9 = normalize4(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
9298
|
+
const isRoot = relative9 === "" || relative9 === "." || relative9 === "/";
|
|
9299
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative9);
|
|
8359
9300
|
}
|
|
8360
9301
|
var HEALTH_PROBE_MS = 3e3;
|
|
8361
9302
|
function fileStamp(path) {
|
|
@@ -8367,11 +9308,11 @@ function fileStamp(path) {
|
|
|
8367
9308
|
}
|
|
8368
9309
|
}
|
|
8369
9310
|
function watchLiveFiles(cwd, configPath, live) {
|
|
8370
|
-
const leglasDir =
|
|
9311
|
+
const leglasDir = join17(cwd, ".leglas");
|
|
8371
9312
|
const targets = [
|
|
8372
|
-
{ path:
|
|
8373
|
-
{ path:
|
|
8374
|
-
{ path:
|
|
9313
|
+
{ path: join17(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
9314
|
+
{ path: join17(cwd, REQUESTS_PATH), change: "requests" },
|
|
9315
|
+
{ path: join17(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
8375
9316
|
];
|
|
8376
9317
|
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
8377
9318
|
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
@@ -9024,6 +9965,7 @@ async function startServer(options) {
|
|
|
9024
9965
|
});
|
|
9025
9966
|
}
|
|
9026
9967
|
const deleted = await dropLocalPreviews(cwd, unique);
|
|
9968
|
+
await noteDirections(cwd, unique, "remove");
|
|
9027
9969
|
return sendJson2(res, 200, { ok: true, deleted });
|
|
9028
9970
|
} catch {
|
|
9029
9971
|
return sendJson2(res, 500, {
|
|
@@ -9072,8 +10014,8 @@ async function startServer(options) {
|
|
|
9072
10014
|
const id = newRequestId();
|
|
9073
10015
|
const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
|
|
9074
10016
|
try {
|
|
9075
|
-
await
|
|
9076
|
-
await
|
|
10017
|
+
await mkdir9(join17(cwd, REFERENCES_DIR), { recursive: true });
|
|
10018
|
+
await writeFile10(join17(cwd, file), body);
|
|
9077
10019
|
void pruneReferences(cwd).catch(() => {
|
|
9078
10020
|
});
|
|
9079
10021
|
return sendJson2(res, 200, {
|
|
@@ -9115,7 +10057,7 @@ async function startServer(options) {
|
|
|
9115
10057
|
}
|
|
9116
10058
|
const references = parsed2.references ?? [];
|
|
9117
10059
|
if (references.length > 0) {
|
|
9118
|
-
const present = new Set((await
|
|
10060
|
+
const present = new Set((await readdir6(join17(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
9119
10061
|
const gone = references.filter((id2) => !present.has(id2));
|
|
9120
10062
|
if (gone.length > 0) {
|
|
9121
10063
|
return sendJson2(res, 410, {
|
|
@@ -9145,9 +10087,8 @@ async function startServer(options) {
|
|
|
9145
10087
|
};
|
|
9146
10088
|
const compare = isString(parsed2.compare) && parsed2.compare !== preview.title ? previews.find((entry) => entry.title === parsed2.compare) ?? null : null;
|
|
9147
10089
|
const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
|
|
9148
|
-
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode
|
|
9149
|
-
//
|
|
9150
|
-
// genuine repeat is refused.
|
|
10090
|
+
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode aren't the same request: one
|
|
10091
|
+
// forks, one rewrites.
|
|
9151
10092
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
9152
10093
|
return sendJson2(res, 409, {
|
|
9153
10094
|
ok: false,
|
|
@@ -9172,9 +10113,8 @@ async function startServer(options) {
|
|
|
9172
10113
|
title: preview.title,
|
|
9173
10114
|
url: preview.url,
|
|
9174
10115
|
intent,
|
|
9175
|
-
// The ids travel with the request so a change
|
|
9176
|
-
//
|
|
9177
|
-
// the direction they point at was not touched.
|
|
10116
|
+
// The ids travel with the request so a change in place can forget
|
|
10117
|
+
// the notes it answered. A fork leaves them.
|
|
9178
10118
|
...composed
|
|
9179
10119
|
};
|
|
9180
10120
|
if (notes.length > 0)
|
|
@@ -9278,8 +10218,8 @@ async function startServer(options) {
|
|
|
9278
10218
|
const noteSuffix = isString(parsed2.note) ? `-${parsed2.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
|
|
9279
10219
|
const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
|
|
9280
10220
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
9281
|
-
await
|
|
9282
|
-
await
|
|
10221
|
+
await mkdir9(join17(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
10222
|
+
await writeFile10(join17(cwd, relativeFile), shot.png);
|
|
9283
10223
|
return sendJson2(res, 200, {
|
|
9284
10224
|
ok: true,
|
|
9285
10225
|
file: relativeFile,
|
|
@@ -9414,16 +10354,14 @@ async function startServer(options) {
|
|
|
9414
10354
|
title,
|
|
9415
10355
|
intent,
|
|
9416
10356
|
// A fork leaves its parent's document alone; the interface keeps
|
|
9417
|
-
// the parent's duplicate verdict
|
|
10357
|
+
// the parent's duplicate verdict because of this.
|
|
9418
10358
|
mode,
|
|
9419
|
-
// Which pins this change speaks for
|
|
9420
|
-
//
|
|
9421
|
-
// like one nobody has read.
|
|
10359
|
+
// Which pins this change speaks for, so the interface can show a
|
|
10360
|
+
// note already in an agent's prompt as read.
|
|
9422
10361
|
notes: notes ?? [],
|
|
9423
|
-
// The
|
|
9424
|
-
//
|
|
9425
|
-
//
|
|
9426
|
-
// could not be written.
|
|
10362
|
+
// The file can't know the run in flight. Otherwise it's the record,
|
|
10363
|
+
// across restarts; the in-memory failed set only covers a verdict
|
|
10364
|
+
// that couldn't be written.
|
|
9427
10365
|
status: snapshot.running && snapshot.requestId === id ? "running" : status === "queued" && snapshot.failedIds.includes(id) ? "failed" : status,
|
|
9428
10366
|
failure: failure ?? null
|
|
9429
10367
|
})),
|
|
@@ -9433,13 +10371,13 @@ async function startServer(options) {
|
|
|
9433
10371
|
name: snapshot.running ? snapshot.agent : null,
|
|
9434
10372
|
activity: snapshot.running ? snapshot.activity : null,
|
|
9435
10373
|
startedAt: snapshot.running ? snapshot.startedAt : null,
|
|
9436
|
-
// A stop
|
|
9437
|
-
//
|
|
10374
|
+
// A stop asked for but not yet obeyed, so the card stops describing
|
|
10375
|
+
// a live run.
|
|
9438
10376
|
stopping: snapshot.running && snapshot.stopping,
|
|
9439
10377
|
// Why a run that looks stalled is stalled, while it is stalled.
|
|
9440
10378
|
waiting: snapshot.running ? snapshot.waiting : null,
|
|
9441
|
-
// When a run
|
|
9442
|
-
//
|
|
10379
|
+
// When a quiet run last said anything. A run on its way out is
|
|
10380
|
+
// stopping, not quiet.
|
|
9443
10381
|
quietSince: snapshot.running && !snapshot.stopping ? snapshot.quietSince : null
|
|
9444
10382
|
}
|
|
9445
10383
|
}));
|
|
@@ -9496,12 +10434,11 @@ async function startServer(options) {
|
|
|
9496
10434
|
url: request.url,
|
|
9497
10435
|
intent: request.intent,
|
|
9498
10436
|
target: request.target,
|
|
9499
|
-
// The prompt names
|
|
9500
|
-
//
|
|
9501
|
-
// `requests --json` all hand the text over as it stands.
|
|
10437
|
+
// The prompt names captures by path, and watch, a custom command
|
|
10438
|
+
// and `requests --json` hand the text over as is.
|
|
9502
10439
|
prompt: attachments.length === 0 ? request.prompt : rehomeText(request.prompt, request.id, retryId)
|
|
9503
|
-
// The stored prompt already carries the mode's instructions;
|
|
9504
|
-
//
|
|
10440
|
+
// The stored prompt already carries the mode's instructions; its
|
|
10441
|
+
// notes and visual context travel with the retry.
|
|
9505
10442
|
};
|
|
9506
10443
|
if (request.mode !== void 0)
|
|
9507
10444
|
retry.mode = request.mode;
|
|
@@ -9659,15 +10596,15 @@ async function startServer(options) {
|
|
|
9659
10596
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
9660
10597
|
const slash = rest.indexOf("/");
|
|
9661
10598
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
9662
|
-
let
|
|
10599
|
+
let relative9 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
9663
10600
|
try {
|
|
9664
|
-
|
|
10601
|
+
relative9 = decodeURIComponent(relative9);
|
|
9665
10602
|
} catch {
|
|
9666
|
-
|
|
10603
|
+
relative9 = "";
|
|
9667
10604
|
}
|
|
9668
10605
|
const dir = fileMounts.get(slug);
|
|
9669
10606
|
const serveMount = () => {
|
|
9670
|
-
if (dir !== void 0 &&
|
|
10607
|
+
if (dir !== void 0 && relative9 !== "" && serveFrom(res, dir, relative9))
|
|
9671
10608
|
return;
|
|
9672
10609
|
res.writeHead(404, {
|
|
9673
10610
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -9677,7 +10614,7 @@ async function startServer(options) {
|
|
|
9677
10614
|
};
|
|
9678
10615
|
if (!context.remote)
|
|
9679
10616
|
return serveMount();
|
|
9680
|
-
if (
|
|
10617
|
+
if (relative9.split("/").some((segment) => segment.startsWith("."))) {
|
|
9681
10618
|
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
9682
10619
|
}
|
|
9683
10620
|
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
@@ -9778,7 +10715,7 @@ async function startServer(options) {
|
|
|
9778
10715
|
if (options.claudeAgentSession !== void 0)
|
|
9779
10716
|
runnerOptions.claudeAgentSession = options.claudeAgentSession;
|
|
9780
10717
|
runner = startRunner(runnerOptions);
|
|
9781
|
-
const renderDirection = async (title) => {
|
|
10718
|
+
const renderDirection = async (title, { picture = false } = {}) => {
|
|
9782
10719
|
const preview = (await livePreviews()).find((entry) => entry.title === title);
|
|
9783
10720
|
if (preview === void 0)
|
|
9784
10721
|
return null;
|
|
@@ -9787,15 +10724,29 @@ async function startServer(options) {
|
|
|
9787
10724
|
return null;
|
|
9788
10725
|
const address = server.address();
|
|
9789
10726
|
const renderPort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
|
|
10727
|
+
const url = previewUrl(`http://127.0.0.1:${renderPort}`, preview);
|
|
10728
|
+
const settings = { width: 1440, timeoutMs: CAPTURE_LOAD_MS, inspect: true };
|
|
9790
10729
|
try {
|
|
9791
10730
|
const captured = await capturePage(browser, {
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
10731
|
+
...settings,
|
|
10732
|
+
url,
|
|
10733
|
+
jpeg: picture
|
|
9795
10734
|
});
|
|
9796
|
-
|
|
10735
|
+
const layout = captured.layout.map(describeFinding);
|
|
10736
|
+
const frame = captured.jpeg;
|
|
10737
|
+
if (layout.length === 0)
|
|
10738
|
+
return { errors: captured.errors, layout, frame };
|
|
10739
|
+
const plain = new URL(url);
|
|
10740
|
+
plain.search = new URLSearchParams([...plain.searchParams].filter(([key]) => !key.startsWith("v-"))).toString();
|
|
10741
|
+
const before = await capturePage(browser, { ...settings, url: plain.href }).catch(() => null);
|
|
10742
|
+
const known = new Set(before?.layout.map(describeFinding) ?? []);
|
|
10743
|
+
return {
|
|
10744
|
+
errors: captured.errors,
|
|
10745
|
+
layout: layout.filter((line) => !known.has(line)),
|
|
10746
|
+
frame
|
|
10747
|
+
};
|
|
9797
10748
|
} catch (error) {
|
|
9798
|
-
return { errors: [error instanceof Error ? error.message : String(error)] };
|
|
10749
|
+
return { errors: [error instanceof Error ? error.message : String(error)], layout: [] };
|
|
9799
10750
|
}
|
|
9800
10751
|
};
|
|
9801
10752
|
const generationDeps = {
|
|
@@ -9809,7 +10760,8 @@ async function startServer(options) {
|
|
|
9809
10760
|
const local = await readLocalPreviews(cwd).catch(() => null);
|
|
9810
10761
|
return new Set([...config?.previews ?? [], ...local?.previews ?? []].map((entry) => entry.title));
|
|
9811
10762
|
},
|
|
9812
|
-
onChange: () => live.nudge("generation")
|
|
10763
|
+
onChange: () => live.nudge("generation"),
|
|
10764
|
+
record: () => config?.recordSets !== false
|
|
9813
10765
|
};
|
|
9814
10766
|
if (options.generationSpawn !== void 0)
|
|
9815
10767
|
generationDeps.spawn = options.generationSpawn;
|
|
@@ -9852,10 +10804,10 @@ async function startServer(options) {
|
|
|
9852
10804
|
|
|
9853
10805
|
// ../server/dist/update.js
|
|
9854
10806
|
import { spawn as spawnChild2 } from "child_process";
|
|
9855
|
-
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
9856
|
-
import { lstat as lstat3, mkdir as
|
|
10807
|
+
import { existsSync as existsSync4, readFileSync, realpathSync as realpathSync2 } from "fs";
|
|
10808
|
+
import { lstat as lstat3, mkdir as mkdir10, mkdtemp, rename as rename5, rm as rm7, writeFile as writeFile11 } from "fs/promises";
|
|
9857
10809
|
import { homedir as homedir3 } from "os";
|
|
9858
|
-
import { dirname as dirname11, join as
|
|
10810
|
+
import { dirname as dirname11, join as join18, posix as posix2, win32 } from "path";
|
|
9859
10811
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
9860
10812
|
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
9861
10813
|
var REGISTRY = "https://registry.npmjs.org";
|
|
@@ -9867,7 +10819,7 @@ function normalized(path) {
|
|
|
9867
10819
|
}
|
|
9868
10820
|
function realPath(path) {
|
|
9869
10821
|
try {
|
|
9870
|
-
return
|
|
10822
|
+
return realpathSync2(path);
|
|
9871
10823
|
} catch {
|
|
9872
10824
|
return null;
|
|
9873
10825
|
}
|
|
@@ -10055,7 +11007,7 @@ function restartCommand(install, argv, latest, port, options) {
|
|
|
10055
11007
|
return spawnCommand(["leglas", ...rest], options.platform);
|
|
10056
11008
|
}
|
|
10057
11009
|
if (install.kind === "project" && install.root !== void 0) {
|
|
10058
|
-
const shim = (windows ? win32.join :
|
|
11010
|
+
const shim = (windows ? win32.join : join18)(install.root, "node_modules", ".bin", windows ? "leglas.cmd" : "leglas");
|
|
10059
11011
|
return spawnCommand(options.exists(shim) ? [shim, ...rest] : ["yarn", "leglas", ...rest], options.platform);
|
|
10060
11012
|
}
|
|
10061
11013
|
throw new Error(CHECKOUT_NOTICE);
|
|
@@ -10086,7 +11038,7 @@ function readState(path) {
|
|
|
10086
11038
|
}
|
|
10087
11039
|
function defaultStatePath(home) {
|
|
10088
11040
|
try {
|
|
10089
|
-
return
|
|
11041
|
+
return join18(home(), ".leglas", "update.json");
|
|
10090
11042
|
} catch {
|
|
10091
11043
|
return null;
|
|
10092
11044
|
}
|
|
@@ -10105,21 +11057,21 @@ async function writeState(path, state) {
|
|
|
10105
11057
|
let temporary = null;
|
|
10106
11058
|
try {
|
|
10107
11059
|
const directory = dirname11(path);
|
|
10108
|
-
await
|
|
11060
|
+
await mkdir10(directory, { recursive: true });
|
|
10109
11061
|
if (!(await lstat3(directory)).isDirectory())
|
|
10110
11062
|
return;
|
|
10111
11063
|
const existing = await lstat3(path).catch(() => null);
|
|
10112
11064
|
if (existing !== null && !existing.isFile())
|
|
10113
11065
|
return;
|
|
10114
|
-
temporary = await mkdtemp(
|
|
10115
|
-
const file =
|
|
10116
|
-
await
|
|
11066
|
+
temporary = await mkdtemp(join18(directory, ".update-"));
|
|
11067
|
+
const file = join18(temporary, "update.json");
|
|
11068
|
+
await writeFile11(file, `${JSON.stringify(state, null, 2)}
|
|
10117
11069
|
`, "utf8");
|
|
10118
|
-
await
|
|
11070
|
+
await rename5(file, path);
|
|
10119
11071
|
} catch {
|
|
10120
11072
|
} finally {
|
|
10121
11073
|
if (temporary !== null)
|
|
10122
|
-
await
|
|
11074
|
+
await rm7(temporary, { recursive: true, force: true }).catch(() => {
|
|
10123
11075
|
});
|
|
10124
11076
|
}
|
|
10125
11077
|
}
|
|
@@ -10497,7 +11449,7 @@ async function runClassify(options, deps) {
|
|
|
10497
11449
|
const declared = await Promise.all(
|
|
10498
11450
|
options.changes.map(async (change) => ({
|
|
10499
11451
|
...change,
|
|
10500
|
-
exists: await stat5(
|
|
11452
|
+
exists: await stat5(join19(options.cwd, change.path)).then(
|
|
10501
11453
|
() => true,
|
|
10502
11454
|
() => false
|
|
10503
11455
|
)
|
|
@@ -10819,6 +11771,38 @@ async function findLeglas(cwd, port, request) {
|
|
|
10819
11771
|
return { ok: false, error: NOT_RUNNING };
|
|
10820
11772
|
}
|
|
10821
11773
|
}
|
|
11774
|
+
function interfaceUrl(port, titles) {
|
|
11775
|
+
const url = new URL(`http://localhost:${port}${LEGLAS_PREFIX}`);
|
|
11776
|
+
const [direction, compare] = titles;
|
|
11777
|
+
if (direction !== void 0) url.searchParams.set("direction", direction);
|
|
11778
|
+
if (compare !== void 0) url.searchParams.set("compare", compare);
|
|
11779
|
+
return url.href;
|
|
11780
|
+
}
|
|
11781
|
+
async function railTitles(port, request) {
|
|
11782
|
+
try {
|
|
11783
|
+
const response = await request(`http://127.0.0.1:${port}${LEGLAS_PREFIX}/api/config`, {
|
|
11784
|
+
signal: AbortSignal.timeout(2e3)
|
|
11785
|
+
});
|
|
11786
|
+
if (!response.ok) return null;
|
|
11787
|
+
const payload = await bodyOf(response);
|
|
11788
|
+
if (!isJsonObject(payload) || !Array.isArray(payload.previews)) return null;
|
|
11789
|
+
return new Set(
|
|
11790
|
+
payload.previews.flatMap(
|
|
11791
|
+
(preview) => isJsonObject(preview) && isString2(preview.title) ? [preview.title] : []
|
|
11792
|
+
)
|
|
11793
|
+
);
|
|
11794
|
+
} catch {
|
|
11795
|
+
return null;
|
|
11796
|
+
}
|
|
11797
|
+
}
|
|
11798
|
+
async function recordedRail(cwd, request) {
|
|
11799
|
+
const record3 = await readServerInfo(cwd);
|
|
11800
|
+
if (record3 === null) return null;
|
|
11801
|
+
const found = await findLeglas(cwd, record3.port, request);
|
|
11802
|
+
if (!found.ok) return null;
|
|
11803
|
+
const titles = await railTitles(found.port, request);
|
|
11804
|
+
return titles === null ? null : { port: found.port, titles };
|
|
11805
|
+
}
|
|
10822
11806
|
|
|
10823
11807
|
// src/run-explore-build.ts
|
|
10824
11808
|
var POLL_MS2 = 1e3;
|
|
@@ -10894,13 +11878,13 @@ async function runExploreBuild(options, deps) {
|
|
|
10894
11878
|
const deadline = Date.now() + WAIT_MS;
|
|
10895
11879
|
while (Date.now() < deadline) {
|
|
10896
11880
|
await sleep(POLL_MS2);
|
|
10897
|
-
let
|
|
11881
|
+
let listed2;
|
|
10898
11882
|
try {
|
|
10899
|
-
|
|
11883
|
+
listed2 = await bodyOf(await request(base));
|
|
10900
11884
|
} catch {
|
|
10901
11885
|
return fail("Leglas stopped answering while the directions were being built.");
|
|
10902
11886
|
}
|
|
10903
|
-
const jobs = isJsonObject(
|
|
11887
|
+
const jobs = isJsonObject(listed2) ? listed2.jobs : void 0;
|
|
10904
11888
|
const latest = Array.isArray(jobs) ? jobs.map(readJob).find((candidate) => candidate?.id === first.id) : void 0;
|
|
10905
11889
|
if (latest === void 0 || latest === null) continue;
|
|
10906
11890
|
job = latest;
|
|
@@ -10938,8 +11922,8 @@ async function runExploreBuild(options, deps) {
|
|
|
10938
11922
|
}
|
|
10939
11923
|
|
|
10940
11924
|
// src/run-init.ts
|
|
10941
|
-
import { readFile as
|
|
10942
|
-
import { join as
|
|
11925
|
+
import { readFile as readFile17, writeFile as writeFile12 } from "fs/promises";
|
|
11926
|
+
import { join as join20 } from "path";
|
|
10943
11927
|
|
|
10944
11928
|
// src/init.ts
|
|
10945
11929
|
var AGENTS_MARKER_START = "<!-- leglas:start -->";
|
|
@@ -11052,6 +12036,12 @@ Useful to know:
|
|
|
11052
12036
|
- Titles identify previews and must be unique. The user may rename one in the
|
|
11053
12037
|
rail, which renames it on their machine only; the commands answer to either
|
|
11054
12038
|
name, so use whichever they said.
|
|
12039
|
+
- \`npx leglas link "<title>"\` prints a link that opens the interface on that
|
|
12040
|
+
direction, and \`npx leglas link "<new>" "<old>"\` one that puts two side by
|
|
12041
|
+
side. Give it to the user when you finish a direction they asked for, rather
|
|
12042
|
+
than telling them where to click.
|
|
12043
|
+
- \`npx leglas remove "<title>"\` takes a direction registered on this machine
|
|
12044
|
+
off the rail when the user asks. One the config lists is theirs to edit.
|
|
11055
12045
|
- \`npx leglas list\` shows every direction, shared and local.
|
|
11056
12046
|
- \`npx leglas show "<title>" --json\` answers for one of them: the file behind it,
|
|
11057
12047
|
the variants based on it, what it is being compared against and anything
|
|
@@ -11101,7 +12091,7 @@ ${AGENTS_SECTION}`
|
|
|
11101
12091
|
// src/run-init.ts
|
|
11102
12092
|
async function readIfPresent(path) {
|
|
11103
12093
|
try {
|
|
11104
|
-
return await
|
|
12094
|
+
return await readFile17(path, "utf8");
|
|
11105
12095
|
} catch {
|
|
11106
12096
|
return null;
|
|
11107
12097
|
}
|
|
@@ -11109,18 +12099,18 @@ async function readIfPresent(path) {
|
|
|
11109
12099
|
async function runInit(options, deps) {
|
|
11110
12100
|
const existingConfig = findConfigFile(options.cwd);
|
|
11111
12101
|
const plan = planInit({
|
|
11112
|
-
agents: await readIfPresent(
|
|
12102
|
+
agents: await readIfPresent(join20(options.cwd, "AGENTS.md")),
|
|
11113
12103
|
config: existingConfig === null ? null : "present",
|
|
11114
|
-
gitignore: await readIfPresent(
|
|
12104
|
+
gitignore: await readIfPresent(join20(options.cwd, ".gitignore")),
|
|
11115
12105
|
force: options.force
|
|
11116
12106
|
});
|
|
11117
12107
|
const touched = [];
|
|
11118
12108
|
for (const write2 of plan.writes) {
|
|
11119
|
-
await
|
|
12109
|
+
await writeFile12(join20(options.cwd, write2.path), write2.contents, "utf8");
|
|
11120
12110
|
touched.push(write2.path);
|
|
11121
12111
|
}
|
|
11122
12112
|
if (plan.gitignore !== null) {
|
|
11123
|
-
await
|
|
12113
|
+
await writeFile12(join20(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
11124
12114
|
touched.push(".gitignore");
|
|
11125
12115
|
}
|
|
11126
12116
|
if (options.json) {
|
|
@@ -11140,11 +12130,11 @@ async function runInit(options, deps) {
|
|
|
11140
12130
|
|
|
11141
12131
|
// src/run-keep.ts
|
|
11142
12132
|
import { existsSync as existsSync5 } from "fs";
|
|
11143
|
-
import { copyFile as copyFile2, mkdir as
|
|
11144
|
-
import { dirname as dirname12, isAbsolute as isAbsolute3, join as
|
|
12133
|
+
import { copyFile as copyFile2, mkdir as mkdir11, readFile as readFile18, rm as rm8, writeFile as writeFile13 } from "fs/promises";
|
|
12134
|
+
import { dirname as dirname12, isAbsolute as isAbsolute3, join as join21, relative as relative5 } from "path";
|
|
11145
12135
|
|
|
11146
12136
|
// src/keep.ts
|
|
11147
|
-
import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, normalize as
|
|
12137
|
+
import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, normalize as normalize5 } from "path";
|
|
11148
12138
|
function surfaceOf(url) {
|
|
11149
12139
|
if (!url.startsWith("/") || !url.includes("?")) return null;
|
|
11150
12140
|
for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
|
|
@@ -11173,7 +12163,7 @@ function planKeep(options) {
|
|
|
11173
12163
|
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.`
|
|
11174
12164
|
};
|
|
11175
12165
|
}
|
|
11176
|
-
const to =
|
|
12166
|
+
const to = normalize5(options.to);
|
|
11177
12167
|
if (to.startsWith("..") || isAbsolute2(to)) {
|
|
11178
12168
|
return { ok: false, error: "The destination has to be inside the project." };
|
|
11179
12169
|
}
|
|
@@ -11229,15 +12219,15 @@ async function writeLogEntry(options) {
|
|
|
11229
12219
|
annotations: await readAnnotations(options.cwd),
|
|
11230
12220
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
11231
12221
|
});
|
|
11232
|
-
const dir =
|
|
11233
|
-
await
|
|
11234
|
-
const file =
|
|
11235
|
-
await
|
|
12222
|
+
const dir = join21(options.cwd, options.logDir);
|
|
12223
|
+
await mkdir11(dir, { recursive: true });
|
|
12224
|
+
const file = join21(dir, `${entry.slug}.md`);
|
|
12225
|
+
await writeFile13(file, entry.markdown, "utf8");
|
|
11236
12226
|
if (entry.pictures.length > 0) {
|
|
11237
|
-
const pictureDir =
|
|
11238
|
-
await
|
|
12227
|
+
const pictureDir = join21(dir, entry.slug);
|
|
12228
|
+
await mkdir11(pictureDir, { recursive: true });
|
|
11239
12229
|
for (const picture of entry.pictures) {
|
|
11240
|
-
await copyFile2(
|
|
12230
|
+
await copyFile2(join21(options.cwd, picture.from), join21(pictureDir, picture.to)).catch(() => {
|
|
11241
12231
|
});
|
|
11242
12232
|
}
|
|
11243
12233
|
}
|
|
@@ -11261,17 +12251,17 @@ async function runKeep(options, deps) {
|
|
|
11261
12251
|
const destination = isAbsolute3(options.to) ? relative5(options.cwd, options.to) : options.to;
|
|
11262
12252
|
const plan = planKeep({ title: resolved.title, previews, to: destination });
|
|
11263
12253
|
if (!plan.ok) return fail(plan.error);
|
|
11264
|
-
const from =
|
|
11265
|
-
const to =
|
|
12254
|
+
const from = join21(options.cwd, plan.move.from);
|
|
12255
|
+
const to = join21(options.cwd, plan.move.to);
|
|
11266
12256
|
if (!existsSync5(from)) {
|
|
11267
12257
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
11268
12258
|
}
|
|
11269
12259
|
if (existsSync5(to)) {
|
|
11270
12260
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
11271
12261
|
}
|
|
11272
|
-
const source = await
|
|
11273
|
-
await
|
|
11274
|
-
await
|
|
12262
|
+
const source = await readFile18(from, "utf8");
|
|
12263
|
+
await mkdir11(dirname12(to), { recursive: true });
|
|
12264
|
+
await writeFile13(to, renameExport(source, plan.exportName), "utf8");
|
|
11275
12265
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
11276
12266
|
let logged = null;
|
|
11277
12267
|
let logError = null;
|
|
@@ -11286,8 +12276,9 @@ async function runKeep(options, deps) {
|
|
|
11286
12276
|
} catch (error) {
|
|
11287
12277
|
logError = error instanceof Error ? error.message : String(error);
|
|
11288
12278
|
}
|
|
11289
|
-
await
|
|
12279
|
+
await rm8(join21(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
11290
12280
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
12281
|
+
await noteDirections(options.cwd, [resolved.title], "keep", plan.move.to);
|
|
11291
12282
|
if (options.json) {
|
|
11292
12283
|
deps.log(
|
|
11293
12284
|
JSON.stringify({
|
|
@@ -11323,13 +12314,99 @@ async function runKeep(options, deps) {
|
|
|
11323
12314
|
return { exitCode: 0 };
|
|
11324
12315
|
}
|
|
11325
12316
|
|
|
12317
|
+
// src/run-link.ts
|
|
12318
|
+
async function runLink(options, deps) {
|
|
12319
|
+
const fail = (error) => {
|
|
12320
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
12321
|
+
else deps.error(error);
|
|
12322
|
+
return { exitCode: 1 };
|
|
12323
|
+
};
|
|
12324
|
+
const loaded = await loadConfig(options.cwd);
|
|
12325
|
+
const local = await readLocalPreviews(options.cwd);
|
|
12326
|
+
const renames = await readRenames(options.cwd);
|
|
12327
|
+
const known = [
|
|
12328
|
+
...new Set(
|
|
12329
|
+
[...loaded.config?.previews ?? [], ...local.previews].map((preview) => preview.title)
|
|
12330
|
+
)
|
|
12331
|
+
];
|
|
12332
|
+
const titles = [];
|
|
12333
|
+
for (const asked of options.titles) {
|
|
12334
|
+
const resolved = resolveOrExplain(asked, known, renames);
|
|
12335
|
+
if (!resolved.ok) return fail(resolved.error);
|
|
12336
|
+
titles.push(resolved.title);
|
|
12337
|
+
}
|
|
12338
|
+
const [first, second] = titles;
|
|
12339
|
+
if (first !== void 0 && first === second) {
|
|
12340
|
+
return fail(`Both names are ${first}. Name two different directions to put side by side.`);
|
|
12341
|
+
}
|
|
12342
|
+
const request = deps.fetch ?? fetch;
|
|
12343
|
+
const found = await findLeglas(options.cwd, options.port, request);
|
|
12344
|
+
if (!found.ok) return fail(found.error);
|
|
12345
|
+
const rail = await railTitles(found.port, request);
|
|
12346
|
+
if (rail === null) return fail("Leglas did not say which directions are on its rail.");
|
|
12347
|
+
const absent = titles.find((title) => !rail.has(title));
|
|
12348
|
+
if (absent !== void 0) {
|
|
12349
|
+
return fail(
|
|
12350
|
+
`${absent} isn't on the running rail yet. A branch or file direction joins it when Leglas restarts.`
|
|
12351
|
+
);
|
|
12352
|
+
}
|
|
12353
|
+
const url = interfaceUrl(found.port, titles);
|
|
12354
|
+
if (options.json) deps.log(JSON.stringify({ ok: true, url }));
|
|
12355
|
+
else deps.log(url);
|
|
12356
|
+
return { exitCode: 0 };
|
|
12357
|
+
}
|
|
12358
|
+
|
|
12359
|
+
// src/run-remove.ts
|
|
12360
|
+
import { relative as relative6 } from "path";
|
|
12361
|
+
async function runRemove(options, deps) {
|
|
12362
|
+
const fail = (error) => {
|
|
12363
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
12364
|
+
else deps.error(error);
|
|
12365
|
+
return { exitCode: 1 };
|
|
12366
|
+
};
|
|
12367
|
+
const loaded = await loadConfig(options.cwd);
|
|
12368
|
+
const local = await readLocalPreviews(options.cwd);
|
|
12369
|
+
if (local.errors.length > 0) {
|
|
12370
|
+
return fail(`${LOCAL_PREVIEWS_PATH} has entries Leglas can't read: ${local.errors.join(" ")}`);
|
|
12371
|
+
}
|
|
12372
|
+
const known = [
|
|
12373
|
+
...new Set(
|
|
12374
|
+
[...loaded.config?.previews ?? [], ...local.previews].map((preview) => preview.title)
|
|
12375
|
+
)
|
|
12376
|
+
];
|
|
12377
|
+
const renames = await readRenames(options.cwd);
|
|
12378
|
+
const titles = [];
|
|
12379
|
+
for (const asked of options.titles) {
|
|
12380
|
+
const resolved = resolveOrExplain(asked, known, renames);
|
|
12381
|
+
if (!resolved.ok) return fail(resolved.error);
|
|
12382
|
+
if (!titles.includes(resolved.title)) titles.push(resolved.title);
|
|
12383
|
+
}
|
|
12384
|
+
const registered = new Set(local.previews.map((preview) => preview.title));
|
|
12385
|
+
const listed2 = new Set((loaded.config?.previews ?? []).map((preview) => preview.title));
|
|
12386
|
+
const shared = titles.find((title) => listed2.has(title) || !registered.has(title));
|
|
12387
|
+
if (shared !== void 0) {
|
|
12388
|
+
const where = loaded.path === null ? "the config" : relative6(options.cwd, loaded.path);
|
|
12389
|
+
return fail(`${shared} is in ${where}, which the project shares. Remove it there.`);
|
|
12390
|
+
}
|
|
12391
|
+
await dropLocalPreviews(options.cwd, titles);
|
|
12392
|
+
await noteDirections(options.cwd, titles, "remove");
|
|
12393
|
+
if (options.json) {
|
|
12394
|
+
deps.log(JSON.stringify({ ok: true, removed: titles }));
|
|
12395
|
+
} else {
|
|
12396
|
+
for (const title of titles) deps.log(` removed ${title}`);
|
|
12397
|
+
deps.log("");
|
|
12398
|
+
deps.log(`The files behind ${titles.length === 1 ? "it" : "them"} stay where they are.`);
|
|
12399
|
+
}
|
|
12400
|
+
return { exitCode: 0 };
|
|
12401
|
+
}
|
|
12402
|
+
|
|
11326
12403
|
// src/run-new.ts
|
|
11327
12404
|
import { existsSync as existsSync6 } from "fs";
|
|
11328
|
-
import { mkdir as
|
|
11329
|
-
import { dirname as dirname13, join as
|
|
12405
|
+
import { mkdir as mkdir12, readFile as readFile19, writeFile as writeFile14 } from "fs/promises";
|
|
12406
|
+
import { dirname as dirname13, join as join22 } from "path";
|
|
11330
12407
|
async function readIfPresent2(path) {
|
|
11331
12408
|
try {
|
|
11332
|
-
return await
|
|
12409
|
+
return await readFile19(path, "utf8");
|
|
11333
12410
|
} catch {
|
|
11334
12411
|
return null;
|
|
11335
12412
|
}
|
|
@@ -11337,7 +12414,7 @@ async function readIfPresent2(path) {
|
|
|
11337
12414
|
async function runNew(options, deps) {
|
|
11338
12415
|
let from;
|
|
11339
12416
|
if (options.from !== void 0) {
|
|
11340
|
-
const contents = await readIfPresent2(
|
|
12417
|
+
const contents = await readIfPresent2(join22(options.cwd, options.from));
|
|
11341
12418
|
if (contents === null) {
|
|
11342
12419
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
11343
12420
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -11348,8 +12425,8 @@ async function runNew(options, deps) {
|
|
|
11348
12425
|
}
|
|
11349
12426
|
const plan = planNew({
|
|
11350
12427
|
surface: options.surface,
|
|
11351
|
-
packageJson: await readIfPresent2(
|
|
11352
|
-
gitignore: await readIfPresent2(
|
|
12428
|
+
packageJson: await readIfPresent2(join22(options.cwd, "package.json")),
|
|
12429
|
+
gitignore: await readIfPresent2(join22(options.cwd, ".gitignore")),
|
|
11353
12430
|
from
|
|
11354
12431
|
});
|
|
11355
12432
|
const fail = (error) => {
|
|
@@ -11379,7 +12456,7 @@ async function runNew(options, deps) {
|
|
|
11379
12456
|
deps.log(plan.instructions);
|
|
11380
12457
|
return { exitCode: 0, written: [] };
|
|
11381
12458
|
}
|
|
11382
|
-
const existing = plan.writes.filter((write2) => existsSync6(
|
|
12459
|
+
const existing = plan.writes.filter((write2) => existsSync6(join22(options.cwd, write2.path)));
|
|
11383
12460
|
if (existing.length > 0) {
|
|
11384
12461
|
return fail(
|
|
11385
12462
|
`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
|
|
@@ -11387,13 +12464,13 @@ async function runNew(options, deps) {
|
|
|
11387
12464
|
}
|
|
11388
12465
|
const written = [];
|
|
11389
12466
|
for (const write2 of plan.writes) {
|
|
11390
|
-
const target =
|
|
11391
|
-
await
|
|
11392
|
-
await
|
|
12467
|
+
const target = join22(options.cwd, write2.path);
|
|
12468
|
+
await mkdir12(dirname13(target), { recursive: true });
|
|
12469
|
+
await writeFile14(target, write2.contents, "utf8");
|
|
11393
12470
|
written.push(write2.path);
|
|
11394
12471
|
}
|
|
11395
12472
|
if (plan.gitignore !== null) {
|
|
11396
|
-
await
|
|
12473
|
+
await writeFile14(join22(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
11397
12474
|
written.push(".gitignore");
|
|
11398
12475
|
}
|
|
11399
12476
|
if (options.json) {
|
|
@@ -11421,8 +12498,8 @@ async function runNew(options, deps) {
|
|
|
11421
12498
|
}
|
|
11422
12499
|
|
|
11423
12500
|
// src/run-log.ts
|
|
11424
|
-
import { readFile as
|
|
11425
|
-
import { join as
|
|
12501
|
+
import { readFile as readFile20, readdir as readdir7 } from "fs/promises";
|
|
12502
|
+
import { join as join23 } from "path";
|
|
11426
12503
|
function headline(markdown) {
|
|
11427
12504
|
const first = markdown.split("\n", 1)[0] ?? "";
|
|
11428
12505
|
return first.replace(/^#\s*/, "").trim();
|
|
@@ -11432,7 +12509,7 @@ async function runLog(options, deps) {
|
|
|
11432
12509
|
const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
|
|
11433
12510
|
let names;
|
|
11434
12511
|
try {
|
|
11435
|
-
names = (await
|
|
12512
|
+
names = (await readdir7(join23(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
|
|
11436
12513
|
} catch {
|
|
11437
12514
|
names = [];
|
|
11438
12515
|
}
|
|
@@ -11445,7 +12522,7 @@ async function runLog(options, deps) {
|
|
|
11445
12522
|
else deps.error(error);
|
|
11446
12523
|
return { exitCode: 1 };
|
|
11447
12524
|
}
|
|
11448
|
-
const markdown = await
|
|
12525
|
+
const markdown = await readFile20(join23(options.cwd, dir, found), "utf8");
|
|
11449
12526
|
if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
|
|
11450
12527
|
else deps.log(markdown.trimEnd());
|
|
11451
12528
|
return { exitCode: 0 };
|
|
@@ -11453,7 +12530,7 @@ async function runLog(options, deps) {
|
|
|
11453
12530
|
const entries = await Promise.all(
|
|
11454
12531
|
names.map(async (name) => ({
|
|
11455
12532
|
entry: name.replace(/\.md$/, ""),
|
|
11456
|
-
title: headline(await
|
|
12533
|
+
title: headline(await readFile20(join23(options.cwd, dir, name), "utf8")),
|
|
11457
12534
|
file: `${dir}/${name}`
|
|
11458
12535
|
}))
|
|
11459
12536
|
);
|
|
@@ -11471,21 +12548,21 @@ async function runLog(options, deps) {
|
|
|
11471
12548
|
}
|
|
11472
12549
|
|
|
11473
12550
|
// src/run-previews.ts
|
|
11474
|
-
import { readFile as
|
|
11475
|
-
import { join as
|
|
12551
|
+
import { readFile as readFile21, writeFile as writeFile15 } from "fs/promises";
|
|
12552
|
+
import { join as join24 } from "path";
|
|
11476
12553
|
function envelope(deps, ok, body) {
|
|
11477
12554
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
11478
12555
|
}
|
|
11479
12556
|
async function ensureIgnored(cwd) {
|
|
11480
|
-
const path =
|
|
12557
|
+
const path = join24(cwd, ".gitignore");
|
|
11481
12558
|
let current = null;
|
|
11482
12559
|
try {
|
|
11483
|
-
current = await
|
|
12560
|
+
current = await readFile21(path, "utf8");
|
|
11484
12561
|
} catch {
|
|
11485
12562
|
current = null;
|
|
11486
12563
|
}
|
|
11487
12564
|
const next = ignoreEntry(current);
|
|
11488
|
-
if (next !== null) await
|
|
12565
|
+
if (next !== null) await writeFile15(path, next, "utf8");
|
|
11489
12566
|
}
|
|
11490
12567
|
async function runAdd(options, deps) {
|
|
11491
12568
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -11521,6 +12598,8 @@ async function runAdd(options, deps) {
|
|
|
11521
12598
|
}
|
|
11522
12599
|
await ensureIgnored(options.cwd);
|
|
11523
12600
|
const needsDevCommand = options.preview.branch !== void 0 && loaded.config?.devCommand === void 0;
|
|
12601
|
+
const rail = await recordedRail(options.cwd, deps.fetch ?? fetch);
|
|
12602
|
+
const opens = rail !== null && rail.titles.has(options.preview.title) ? interfaceUrl(rail.port, [options.preview.title]) : null;
|
|
11524
12603
|
if (options.json) {
|
|
11525
12604
|
const added = { added: options.preview.title };
|
|
11526
12605
|
if (options.preview.url !== void 0) added.url = options.preview.url;
|
|
@@ -11531,11 +12610,13 @@ async function runAdd(options, deps) {
|
|
|
11531
12610
|
if (needsDevCommand) {
|
|
11532
12611
|
added.warning = "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config.";
|
|
11533
12612
|
}
|
|
12613
|
+
if (opens !== null) added.interfaceUrl = opens;
|
|
11534
12614
|
envelope(deps, true, added);
|
|
11535
12615
|
} else {
|
|
11536
12616
|
deps.log(
|
|
11537
12617
|
` added ${options.preview.title} ${options.preview.url ?? options.preview.file ?? ""}`
|
|
11538
12618
|
);
|
|
12619
|
+
if (opens !== null) deps.log(` open ${opens}`);
|
|
11539
12620
|
deps.log("");
|
|
11540
12621
|
if (needsDevCommand) {
|
|
11541
12622
|
deps.log(" ! The config sets no devCommand, so Leglas cannot start this branch yet.");
|
|
@@ -11559,12 +12640,10 @@ async function runList(options, deps) {
|
|
|
11559
12640
|
...local.previews
|
|
11560
12641
|
];
|
|
11561
12642
|
if (options.json) {
|
|
12643
|
+
const rail = await recordedRail(options.cwd, deps.fetch ?? fetch);
|
|
11562
12644
|
envelope(deps, errors.length === 0, {
|
|
11563
|
-
// The whole record
|
|
11564
|
-
//
|
|
11565
|
-
// exactly what tells an agent why these are being compared, and leaving
|
|
11566
|
-
// it out made the listing thinner than the reference block that points
|
|
11567
|
-
// at it.
|
|
12645
|
+
// The whole record: a preview's note, tags and base direction are what
|
|
12646
|
+
// tell an agent why these are being compared.
|
|
11568
12647
|
previews: previews.map((preview) => ({
|
|
11569
12648
|
title: preview.title,
|
|
11570
12649
|
url: preview.url,
|
|
@@ -11574,7 +12653,8 @@ async function runList(options, deps) {
|
|
|
11574
12653
|
askedFor: preview.askedFor ?? null,
|
|
11575
12654
|
local: preview.local,
|
|
11576
12655
|
branch: preview.branch ?? null,
|
|
11577
|
-
file: preview.file ?? null
|
|
12656
|
+
file: preview.file ?? null,
|
|
12657
|
+
interfaceUrl: rail !== null && rail.titles.has(preview.title) ? interfaceUrl(rail.port, [preview.title]) : null
|
|
11578
12658
|
})),
|
|
11579
12659
|
errors
|
|
11580
12660
|
});
|
|
@@ -11653,8 +12733,11 @@ function tunnelFrom(value) {
|
|
|
11653
12733
|
}
|
|
11654
12734
|
}
|
|
11655
12735
|
function linkFrom(value) {
|
|
11656
|
-
if (!isJsonObject(value) || !isString2(value.localUrl) || !isNumber2(value.expiresAt))
|
|
12736
|
+
if (!isJsonObject(value) || !isString2(value.id) || !isString2(value.localUrl) || !isNumber2(value.expiresAt)) {
|
|
12737
|
+
return null;
|
|
12738
|
+
}
|
|
11657
12739
|
return {
|
|
12740
|
+
id: value.id,
|
|
11658
12741
|
name: isString2(value.name) ? value.name : "",
|
|
11659
12742
|
url: isString2(value.url) ? value.url : null,
|
|
11660
12743
|
localUrl: value.localUrl,
|
|
@@ -11775,11 +12858,12 @@ async function runShare(options, deps) {
|
|
|
11775
12858
|
const running = await readShare(found.base, request);
|
|
11776
12859
|
if (!running.ok) return fail(running.error);
|
|
11777
12860
|
const { installed } = running.value;
|
|
11778
|
-
const report = (share2, already, leftOut2) => {
|
|
12861
|
+
const report = (share2, already, leftOut2, done = null) => {
|
|
11779
12862
|
if (options.json) {
|
|
11780
12863
|
deps.log(
|
|
11781
12864
|
JSON.stringify({
|
|
11782
12865
|
ok: true,
|
|
12866
|
+
...done?.json,
|
|
11783
12867
|
alreadySharing: already,
|
|
11784
12868
|
share: {
|
|
11785
12869
|
scope: share2.scope,
|
|
@@ -11793,6 +12877,7 @@ async function runShare(options, deps) {
|
|
|
11793
12877
|
);
|
|
11794
12878
|
return { exitCode: 0 };
|
|
11795
12879
|
}
|
|
12880
|
+
if (done !== null) deps.log(done.line);
|
|
11796
12881
|
deps.log(` sharing ${describe(share2)}${already ? " (already running)" : ""}`);
|
|
11797
12882
|
if (share2.reach === "listed") {
|
|
11798
12883
|
deps.log(" reach only the paths the share lists; allow more from the share panel");
|
|
@@ -11802,6 +12887,9 @@ async function runShare(options, deps) {
|
|
|
11802
12887
|
if (link.url !== null) deps.log(` link ${label}${link.url}`);
|
|
11803
12888
|
else deps.log(` local ${label}${link.localUrl}`);
|
|
11804
12889
|
}
|
|
12890
|
+
if (share2.links.length === 0) {
|
|
12891
|
+
deps.log(" links no link is live; npx leglas share --rotate starts one");
|
|
12892
|
+
}
|
|
11805
12893
|
const tunnel = tunnelLine(share2.tunnel, installed);
|
|
11806
12894
|
if (tunnel !== null) deps.log(` tunnel ${tunnel}`);
|
|
11807
12895
|
const ends = share2.links.map((link) => link.expiresAt);
|
|
@@ -11812,6 +12900,66 @@ async function runShare(options, deps) {
|
|
|
11812
12900
|
deps.log(" stop npx leglas share --stop");
|
|
11813
12901
|
return { exitCode: 0 };
|
|
11814
12902
|
};
|
|
12903
|
+
const settle = async (share2) => {
|
|
12904
|
+
let settled2 = share2;
|
|
12905
|
+
for (let waited = 0; settled2.tunnel.status === "starting" && waited < TUNNEL_WAIT_MS; waited += TUNNEL_POLL_MS) {
|
|
12906
|
+
await sleep(TUNNEL_POLL_MS);
|
|
12907
|
+
const read = await readShare(found.base, request);
|
|
12908
|
+
if (!read.ok) return read;
|
|
12909
|
+
if (read.value.share === null) return { ok: true, value: null };
|
|
12910
|
+
settled2 = read.value.share;
|
|
12911
|
+
}
|
|
12912
|
+
return { ok: true, value: settled2 };
|
|
12913
|
+
};
|
|
12914
|
+
const revoke = options.revoke ?? null;
|
|
12915
|
+
if (options.rotate === true || revoke !== null) {
|
|
12916
|
+
const current2 = running.value.share;
|
|
12917
|
+
if (current2 === null) {
|
|
12918
|
+
return fail("Nothing is being shared. Start a share with npx leglas share.");
|
|
12919
|
+
}
|
|
12920
|
+
if (revoke !== null) {
|
|
12921
|
+
const named = revoke;
|
|
12922
|
+
const link = current2.links.find(
|
|
12923
|
+
(candidate) => candidate.id === named || candidate.url === named || candidate.localUrl === named
|
|
12924
|
+
);
|
|
12925
|
+
if (link === void 0) {
|
|
12926
|
+
return fail(`No live link is ${JSON.stringify(named)}. npx leglas share lists them.`);
|
|
12927
|
+
}
|
|
12928
|
+
const revoked = await post(
|
|
12929
|
+
found.base,
|
|
12930
|
+
"/leglas/api/share/grants/revoke",
|
|
12931
|
+
{ id: link.id },
|
|
12932
|
+
request,
|
|
12933
|
+
"Leglas could not end that link."
|
|
12934
|
+
);
|
|
12935
|
+
if (!revoked.ok) return fail(revoked.error);
|
|
12936
|
+
const after = isJsonObject(revoked.value) ? shareFrom(revoked.value.share) : null;
|
|
12937
|
+
if (after === null) return fail("Leglas ended the link but did not say what is left.");
|
|
12938
|
+
return report(after, true, [], {
|
|
12939
|
+
json: { revoked: link },
|
|
12940
|
+
line: ` revoked ${link.name === "" ? "" : `${link.name} `}${link.url ?? link.localUrl}`
|
|
12941
|
+
});
|
|
12942
|
+
}
|
|
12943
|
+
const rotated = await post(
|
|
12944
|
+
found.base,
|
|
12945
|
+
"/leglas/api/share/rotate",
|
|
12946
|
+
{},
|
|
12947
|
+
request,
|
|
12948
|
+
"Leglas could not replace the links."
|
|
12949
|
+
);
|
|
12950
|
+
if (!rotated.ok) return fail(rotated.error);
|
|
12951
|
+
const replaced = isJsonObject(rotated.value) ? shareFrom(rotated.value.share) : null;
|
|
12952
|
+
if (replaced === null) return fail("Leglas replaced the links but did not say with what.");
|
|
12953
|
+
const settled2 = await settle(replaced);
|
|
12954
|
+
if (!settled2.ok) return fail(settled2.error);
|
|
12955
|
+
if (settled2.value === null) {
|
|
12956
|
+
return fail("The share was stopped while its new tunnel was starting.");
|
|
12957
|
+
}
|
|
12958
|
+
return report(settled2.value, true, [], {
|
|
12959
|
+
json: { rotated: true },
|
|
12960
|
+
line: " rotated every earlier link has ended"
|
|
12961
|
+
});
|
|
12962
|
+
}
|
|
11815
12963
|
const loaded = await loadConfig(options.cwd);
|
|
11816
12964
|
const local = await readLocalPreviews(options.cwd);
|
|
11817
12965
|
const renames = await readRenames(options.cwd);
|
|
@@ -11880,20 +13028,16 @@ async function runShare(options, deps) {
|
|
|
11880
13028
|
if (!created.ok) {
|
|
11881
13029
|
return created.answered ? fail(created.error) : giveUp("Leglas stopped answering while the share was starting.", false);
|
|
11882
13030
|
}
|
|
11883
|
-
|
|
13031
|
+
const share = isJsonObject(created.value) ? shareFrom(created.value.share) : null;
|
|
11884
13032
|
if (share === null) {
|
|
11885
13033
|
return giveUp("Leglas started a share this version of the command cannot read.");
|
|
11886
13034
|
}
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
if (read.value.share === null) {
|
|
11892
|
-
return fail("The share was stopped while its tunnel was starting.");
|
|
11893
|
-
}
|
|
11894
|
-
share = read.value.share;
|
|
13035
|
+
const settled = await settle(share);
|
|
13036
|
+
if (!settled.ok) return giveUp(settled.error);
|
|
13037
|
+
if (settled.value === null) {
|
|
13038
|
+
return fail("The share was stopped while its tunnel was starting.");
|
|
11895
13039
|
}
|
|
11896
|
-
return report(
|
|
13040
|
+
return report(settled.value, false, leftOut);
|
|
11897
13041
|
}
|
|
11898
13042
|
|
|
11899
13043
|
// src/show.ts
|
|
@@ -11907,9 +13051,9 @@ function describe2(preview) {
|
|
|
11907
13051
|
branch: preview.branch ?? null,
|
|
11908
13052
|
file: preview.file ?? null,
|
|
11909
13053
|
local: preview.local === true,
|
|
11910
|
-
// A file preview names its own source
|
|
13054
|
+
// A file preview names its own source; anything else is decoded from the
|
|
11911
13055
|
// URL, and a URL outside the convention yields nothing rather than a path
|
|
11912
|
-
// that
|
|
13056
|
+
// that isn't there.
|
|
11913
13057
|
target: preview.file ?? targetFor(preview.url)
|
|
11914
13058
|
};
|
|
11915
13059
|
}
|
|
@@ -11927,8 +13071,7 @@ function planShow({ title, previews, requests }) {
|
|
|
11927
13071
|
ok: true,
|
|
11928
13072
|
direction: describe2(found),
|
|
11929
13073
|
variants,
|
|
11930
|
-
// Its
|
|
11931
|
-
// here; this is the rest of the comparison.
|
|
13074
|
+
// Its variants are listed above, so this is the rest of the comparison.
|
|
11932
13075
|
comparedWith: previews.map((preview) => preview.title).filter((other) => other !== title && !variantTitles.has(other)),
|
|
11933
13076
|
requests: requests.filter((request) => request.title === title).map((request) => ({
|
|
11934
13077
|
id: request.id,
|
|
@@ -11980,21 +13123,23 @@ async function runShow(options, deps) {
|
|
|
11980
13123
|
}
|
|
11981
13124
|
const envelope2 = {
|
|
11982
13125
|
ok: true,
|
|
11983
|
-
direction: plan.direction,
|
|
13126
|
+
direction: { ...plan.direction, interfaceUrl: null },
|
|
11984
13127
|
variants: plan.variants,
|
|
11985
13128
|
comparedWith: plan.comparedWith,
|
|
11986
13129
|
requests: plan.requests
|
|
11987
13130
|
};
|
|
13131
|
+
const request = deps.fetch ?? fetch;
|
|
13132
|
+
let running = null;
|
|
11988
13133
|
if (options.screenshot) {
|
|
11989
13134
|
const fail = (error) => {
|
|
11990
13135
|
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
11991
13136
|
else deps.error(error);
|
|
11992
13137
|
return { exitCode: 1 };
|
|
11993
13138
|
};
|
|
11994
|
-
const request = deps.fetch ?? fetch;
|
|
11995
13139
|
const found = await findLeglas(options.cwd, options.port, request);
|
|
11996
13140
|
if (!found.ok) return fail(found.error);
|
|
11997
13141
|
const port = found.port;
|
|
13142
|
+
running = port;
|
|
11998
13143
|
let response;
|
|
11999
13144
|
try {
|
|
12000
13145
|
response = await request(`http://127.0.0.1:${port}/leglas/api/capture`, {
|
|
@@ -12028,17 +13173,22 @@ async function runShow(options, deps) {
|
|
|
12028
13173
|
cut: captured.cut === true
|
|
12029
13174
|
};
|
|
12030
13175
|
}
|
|
13176
|
+
const rail = running === null ? await recordedRail(options.cwd, request) : { port: running, titles: await railTitles(running, request) ?? /* @__PURE__ */ new Set() };
|
|
13177
|
+
if (rail !== null && rail.titles.has(plan.direction.title)) {
|
|
13178
|
+
envelope2.direction.interfaceUrl = interfaceUrl(rail.port, [plan.direction.title]);
|
|
13179
|
+
}
|
|
12031
13180
|
if (options.json) {
|
|
12032
13181
|
deps.log(JSON.stringify(envelope2));
|
|
12033
13182
|
return { exitCode: 0 };
|
|
12034
13183
|
}
|
|
12035
|
-
const { direction } =
|
|
13184
|
+
const { direction } = envelope2;
|
|
12036
13185
|
deps.log(` ${direction.title}${direction.local ? " (local)" : ""}`);
|
|
12037
13186
|
if (direction.note !== null) deps.log(` ${direction.note}`);
|
|
12038
13187
|
deps.log("");
|
|
12039
13188
|
if (direction.target !== null) deps.log(` file ${direction.target}`);
|
|
12040
13189
|
if (direction.branch !== null) deps.log(` branch ${direction.branch}`);
|
|
12041
13190
|
deps.log(` url ${direction.url}`);
|
|
13191
|
+
if (direction.interfaceUrl !== null) deps.log(` open ${direction.interfaceUrl}`);
|
|
12042
13192
|
if (direction.tags.length > 0) deps.log(` tags ${direction.tags.join(", ")}`);
|
|
12043
13193
|
if (direction.basedOn !== null) deps.log(` variant of ${direction.basedOn}`);
|
|
12044
13194
|
if (plan.variants.length > 0) {
|
|
@@ -12067,7 +13217,7 @@ async function runShow(options, deps) {
|
|
|
12067
13217
|
if (plan.requests.length > 0) {
|
|
12068
13218
|
deps.log("");
|
|
12069
13219
|
deps.log(` Pending, not yet done (${plan.requests.length}):`);
|
|
12070
|
-
for (const
|
|
13220
|
+
for (const request2 of plan.requests) deps.log(` ${request2.status} ${request2.intent}`);
|
|
12071
13221
|
deps.log("");
|
|
12072
13222
|
deps.log(" Run npx leglas requests --json for the full prompts.");
|
|
12073
13223
|
}
|
|
@@ -12076,21 +13226,21 @@ async function runShow(options, deps) {
|
|
|
12076
13226
|
|
|
12077
13227
|
// src/run-watch.ts
|
|
12078
13228
|
import { spawn as spawn4 } from "child_process";
|
|
12079
|
-
import { mkdir as
|
|
12080
|
-
import { dirname as dirname14, join as
|
|
13229
|
+
import { mkdir as mkdir13, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
|
|
13230
|
+
import { dirname as dirname14, join as join25 } from "path";
|
|
12081
13231
|
var POLL_MS3 = 2e3;
|
|
12082
13232
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
12083
13233
|
async function saveTemplate(cwd, run4) {
|
|
12084
|
-
const path =
|
|
13234
|
+
const path = join25(cwd, WATCH_PATH);
|
|
12085
13235
|
let config = {};
|
|
12086
13236
|
try {
|
|
12087
|
-
const parsed2 = JSON.parse(await
|
|
13237
|
+
const parsed2 = JSON.parse(await readFile22(path, "utf8"));
|
|
12088
13238
|
if (isJsonObject(parsed2)) config = parsed2;
|
|
12089
13239
|
} catch {
|
|
12090
13240
|
}
|
|
12091
13241
|
config.run = run4;
|
|
12092
|
-
await
|
|
12093
|
-
await
|
|
13242
|
+
await mkdir13(dirname14(path), { recursive: true });
|
|
13243
|
+
await writeFile16(path, `${JSON.stringify(config, null, 2)}
|
|
12094
13244
|
`, "utf8");
|
|
12095
13245
|
}
|
|
12096
13246
|
function spawnAgent(command, args, cwd, json) {
|
|
@@ -12254,7 +13404,7 @@ async function runWatch(options, deps) {
|
|
|
12254
13404
|
|
|
12255
13405
|
// src/bin-start.ts
|
|
12256
13406
|
import { spawn as spawn5 } from "child_process";
|
|
12257
|
-
import { realpathSync as
|
|
13407
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
12258
13408
|
|
|
12259
13409
|
// src/shutdown.ts
|
|
12260
13410
|
var SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
@@ -12332,13 +13482,13 @@ function createHandoff() {
|
|
|
12332
13482
|
import { existsSync as existsSync7 } from "fs";
|
|
12333
13483
|
import { realpath as realpath4 } from "fs/promises";
|
|
12334
13484
|
import { createRequire } from "module";
|
|
12335
|
-
import { basename as basename6, dirname as dirname15, join as
|
|
13485
|
+
import { basename as basename6, dirname as dirname15, join as join26, relative as relative8, resolve as resolve4 } from "path";
|
|
12336
13486
|
import { fileURLToPath } from "url";
|
|
12337
13487
|
|
|
12338
13488
|
// src/dev-server-owner.ts
|
|
12339
13489
|
import { execFile as execFile2 } from "child_process";
|
|
12340
13490
|
import { realpath as realpath3 } from "fs/promises";
|
|
12341
|
-
import { basename as basename5, isAbsolute as isAbsolute4, relative as
|
|
13491
|
+
import { basename as basename5, isAbsolute as isAbsolute4, relative as relative7, resolve as resolve3 } from "path";
|
|
12342
13492
|
import { promisify as promisify2 } from "util";
|
|
12343
13493
|
var run2 = promisify2(execFile2);
|
|
12344
13494
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -12407,7 +13557,7 @@ async function inspectLocalDevServer(origin) {
|
|
|
12407
13557
|
}
|
|
12408
13558
|
}
|
|
12409
13559
|
function isInside(parent, child) {
|
|
12410
|
-
const path =
|
|
13560
|
+
const path = relative7(resolve3(parent), resolve3(child));
|
|
12411
13561
|
return path === "" || !path.startsWith("..") && !isAbsolute4(path);
|
|
12412
13562
|
}
|
|
12413
13563
|
function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
@@ -12421,8 +13571,8 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
12421
13571
|
|
|
12422
13572
|
// src/run.ts
|
|
12423
13573
|
function findShellDir() {
|
|
12424
|
-
const bundled =
|
|
12425
|
-
if (existsSync7(
|
|
13574
|
+
const bundled = join26(dirname15(fileURLToPath(import.meta.url)), "shell");
|
|
13575
|
+
if (existsSync7(join26(bundled, "index.html"))) return bundled;
|
|
12426
13576
|
try {
|
|
12427
13577
|
const require2 = createRequire(import.meta.url);
|
|
12428
13578
|
return dirname15(require2.resolve("@leglas/shell/dist/index.html"));
|
|
@@ -12436,7 +13586,7 @@ function shellWord(value) {
|
|
|
12436
13586
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
12437
13587
|
}
|
|
12438
13588
|
function embeddedLeglasCommand() {
|
|
12439
|
-
const entry =
|
|
13589
|
+
const entry = join26(dirname15(fileURLToPath(import.meta.url)), "bin.js");
|
|
12440
13590
|
if (!existsSync7(entry)) return "npx -y leglas";
|
|
12441
13591
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
12442
13592
|
}
|
|
@@ -12475,7 +13625,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
12475
13625
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
12476
13626
|
for (const preview of merged?.previews ?? []) {
|
|
12477
13627
|
if (preview.file !== void 0) {
|
|
12478
|
-
const absolute =
|
|
13628
|
+
const absolute = join26(options.cwd, preview.file);
|
|
12479
13629
|
if (!existsSync7(absolute)) {
|
|
12480
13630
|
previewErrors.push(
|
|
12481
13631
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -12507,8 +13657,8 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
12507
13657
|
configWarnings,
|
|
12508
13658
|
fileMounts,
|
|
12509
13659
|
shellDir: findShellDir(),
|
|
12510
|
-
// The config file identifies the project
|
|
12511
|
-
//
|
|
13660
|
+
// The config file identifies the project, else the directory, so saved
|
|
13661
|
+
// layout survives a port change.
|
|
12512
13662
|
project: loaded.path ?? options.cwd,
|
|
12513
13663
|
cwd: options.cwd,
|
|
12514
13664
|
leglasCommand: embeddedLeglasCommand()
|
|
@@ -12537,7 +13687,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
12537
13687
|
})
|
|
12538
13688
|
);
|
|
12539
13689
|
} else {
|
|
12540
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
13690
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative8(options.cwd, loaded.path) || loaded.path;
|
|
12541
13691
|
deps.log(`Leglas ${url}`);
|
|
12542
13692
|
deps.log(
|
|
12543
13693
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|
|
@@ -12594,7 +13744,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
12594
13744
|
async function startViewer(options, deps) {
|
|
12595
13745
|
let entry = deps.entry;
|
|
12596
13746
|
try {
|
|
12597
|
-
entry = (deps.realpath ??
|
|
13747
|
+
entry = (deps.realpath ?? realpathSync3)(entry);
|
|
12598
13748
|
} catch {
|
|
12599
13749
|
}
|
|
12600
13750
|
const updates = (deps.createUpdateService ?? createUpdateService)({
|
|
@@ -12627,7 +13777,7 @@ async function startViewer(options, deps) {
|
|
|
12627
13777
|
}, handoff.target);
|
|
12628
13778
|
}
|
|
12629
13779
|
|
|
12630
|
-
// src/
|
|
13780
|
+
// src/help.ts
|
|
12631
13781
|
var HELP = `leglas - compare design directions inside your own running app
|
|
12632
13782
|
|
|
12633
13783
|
Usage
|
|
@@ -12641,25 +13791,30 @@ Usage
|
|
|
12641
13791
|
leglas log [entry] What past explorations decided
|
|
12642
13792
|
leglas show <title> Everything Leglas knows about one direction
|
|
12643
13793
|
leglas share [title] [title] Share the rail, one direction or a pair
|
|
13794
|
+
leglas link [title] [title] A link that opens the interface on them
|
|
13795
|
+
leglas remove <title>... Take directions this machine registered off the rail
|
|
12644
13796
|
leglas requests Show change requests made from the interface
|
|
12645
13797
|
leglas watch --run "<cmd>" Hand each request to your agent as it arrives
|
|
12646
13798
|
leglas keep <title> --to <path> Keep a winner and end the exploration
|
|
12647
13799
|
|
|
12648
13800
|
Options
|
|
12649
13801
|
--user-port <port> Port your dev server is on (default: from config, or 3000)
|
|
12650
|
-
--port <port> Port for Leglas itself (default:
|
|
13802
|
+
--port <port> Port for Leglas itself (default: ${DEFAULT_PORT}, next free if taken)
|
|
12651
13803
|
--config <path> Config file to use instead of searching upward
|
|
12652
13804
|
--no-open Do not open the browser
|
|
12653
13805
|
--json Print a single machine-readable envelope
|
|
12654
13806
|
-h, --help Show this
|
|
12655
13807
|
-v, --version Show the version
|
|
12656
13808
|
|
|
13809
|
+
Options for init
|
|
13810
|
+
--force Rewrite the AGENTS.md section if it is already there
|
|
13811
|
+
|
|
12657
13812
|
Options for new
|
|
12658
13813
|
--print Print the scaffold instead of writing it
|
|
12659
13814
|
--from <path> Use an existing component as the baseline
|
|
12660
13815
|
|
|
12661
13816
|
Options for explore
|
|
12662
|
-
--count <n> How many directions (default
|
|
13817
|
+
--count <n> How many directions (default ${DEFAULT_EXPLORE_COUNT}; up to ${MAX_DIRECTIONS} with --build)
|
|
12663
13818
|
--based-on <title> Variants of an existing direction instead of new ones
|
|
12664
13819
|
--build Build the set with your agent (Claude or Codex) in the
|
|
12665
13820
|
running Leglas, instead of printing a brief for an agent
|
|
@@ -12667,10 +13822,13 @@ Options for explore
|
|
|
12667
13822
|
with --based-on)
|
|
12668
13823
|
--port <port> Running Leglas port (needs --build)
|
|
12669
13824
|
|
|
13825
|
+
Options for requests
|
|
13826
|
+
--clear Acknowledge the requests once they are done
|
|
13827
|
+
|
|
12670
13828
|
Options for watch
|
|
12671
13829
|
--run <command> Your agent, with {prompt} where the request goes, for
|
|
12672
13830
|
example "claude -p {prompt}". Remembered after first use
|
|
12673
|
-
--port <port> Port Leglas itself is on (default:
|
|
13831
|
+
--port <port> Port Leglas itself is on (default: ${DEFAULT_PORT})
|
|
12674
13832
|
--json One JSON line per event; the agent's output goes to stderr
|
|
12675
13833
|
|
|
12676
13834
|
Options for classify
|
|
@@ -12687,15 +13845,22 @@ Options for add
|
|
|
12687
13845
|
|
|
12688
13846
|
Options for show
|
|
12689
13847
|
--screenshot Render the direction and write a PNG
|
|
12690
|
-
--width <n> Capture width from
|
|
13848
|
+
--width <n> Capture width from ${MIN_SHOW_WIDTH} to ${MAX_SHOW_WIDTH} (needs --screenshot)
|
|
12691
13849
|
--port <port> Running Leglas port (needs --screenshot)
|
|
12692
13850
|
|
|
12693
13851
|
Options for share
|
|
12694
|
-
--reach <open|listed> How far viewers reach into the app (default
|
|
13852
|
+
--reach <open|listed> How far viewers reach into the app (default ${DEFAULT_SHARE_REACH})
|
|
12695
13853
|
--tunnel <name> cloudflared, ngrok or none (default: the first found)
|
|
12696
13854
|
--stop End the share, and every link to it
|
|
13855
|
+
--rotate End every link and start a new one, through a new tunnel
|
|
13856
|
+
--revoke <link> End one link, named by its address or its id
|
|
13857
|
+
--port <port> Running Leglas port
|
|
13858
|
+
|
|
13859
|
+
Options for link
|
|
12697
13860
|
--port <port> Running Leglas port
|
|
12698
13861
|
`;
|
|
13862
|
+
|
|
13863
|
+
// src/bin.ts
|
|
12699
13864
|
function version() {
|
|
12700
13865
|
const require2 = createRequire2(import.meta.url);
|
|
12701
13866
|
const pkg = require2("../package.json");
|
|
@@ -12827,6 +13992,8 @@ if (parsed.kind === "share") {
|
|
|
12827
13992
|
reach: parsed.reach,
|
|
12828
13993
|
tunnel: parsed.tunnel,
|
|
12829
13994
|
stop: parsed.stop,
|
|
13995
|
+
rotate: parsed.rotate,
|
|
13996
|
+
revoke: parsed.revoke,
|
|
12830
13997
|
port: parsed.port,
|
|
12831
13998
|
json: parsed.json,
|
|
12832
13999
|
cwd: process.cwd()
|
|
@@ -12835,6 +14002,20 @@ if (parsed.kind === "share") {
|
|
|
12835
14002
|
);
|
|
12836
14003
|
process.exit(outcome.exitCode);
|
|
12837
14004
|
}
|
|
14005
|
+
if (parsed.kind === "remove") {
|
|
14006
|
+
const outcome = await runRemove(
|
|
14007
|
+
{ titles: parsed.titles, json: parsed.json, cwd: process.cwd() },
|
|
14008
|
+
previewDeps
|
|
14009
|
+
);
|
|
14010
|
+
process.exit(outcome.exitCode);
|
|
14011
|
+
}
|
|
14012
|
+
if (parsed.kind === "link") {
|
|
14013
|
+
const outcome = await runLink(
|
|
14014
|
+
{ titles: parsed.titles, port: parsed.port, json: parsed.json, cwd: process.cwd() },
|
|
14015
|
+
previewDeps
|
|
14016
|
+
);
|
|
14017
|
+
process.exit(outcome.exitCode);
|
|
14018
|
+
}
|
|
12838
14019
|
if (parsed.kind === "show") {
|
|
12839
14020
|
const outcome = await runShow(
|
|
12840
14021
|
{
|