leglas 0.7.4 → 0.9.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/README.md +23 -6
- package/dist/args.d.ts +4 -0
- package/dist/bin.js +462 -41
- package/dist/index.js +385 -22
- package/dist/run-log.d.ts +20 -0
- package/dist/shell/assets/index-BMpAHiSA.js +14 -0
- package/dist/shell/assets/index-pfmBGQQ0.css +1 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-CpYCCElH.css +0 -1
- package/dist/shell/assets/index-Ct9pp-D-.js +0 -14
package/dist/bin.js
CHANGED
|
@@ -287,6 +287,19 @@ function parseArgs(argv) {
|
|
|
287
287
|
}
|
|
288
288
|
return { kind: "requests", json: rest.includes("--json"), clear: rest.includes("--clear") };
|
|
289
289
|
}
|
|
290
|
+
if (argv[0] === "log") {
|
|
291
|
+
const rest = argv.slice(1);
|
|
292
|
+
const flags = rest.filter((argument) => argument.startsWith("--"));
|
|
293
|
+
const unknown = flags.find((flag) => flag !== "--json");
|
|
294
|
+
if (unknown !== void 0) {
|
|
295
|
+
return { kind: "error", message: `leglas log does not take ${unknown}.` };
|
|
296
|
+
}
|
|
297
|
+
const names = rest.filter((argument) => !argument.startsWith("--"));
|
|
298
|
+
if (names.length > 1) {
|
|
299
|
+
return { kind: "error", message: "leglas log takes one entry at most." };
|
|
300
|
+
}
|
|
301
|
+
return { kind: "log", entry: names[0] ?? null, json: flags.includes("--json") };
|
|
302
|
+
}
|
|
290
303
|
if (argv[0] === "list") {
|
|
291
304
|
const rest = argv.slice(1);
|
|
292
305
|
const unknown = rest.find((argument) => argument !== "--json");
|
|
@@ -409,6 +422,84 @@ function parseArgs(argv) {
|
|
|
409
422
|
import { stat as stat4 } from "fs/promises";
|
|
410
423
|
import { join as join13 } from "path";
|
|
411
424
|
|
|
425
|
+
// ../server/dist/log.js
|
|
426
|
+
var DEFAULT_LOG_DIR = "design-log";
|
|
427
|
+
function slugify(value) {
|
|
428
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
|
|
429
|
+
}
|
|
430
|
+
function frameFor(title, requests) {
|
|
431
|
+
let found = null;
|
|
432
|
+
for (const request of requests) {
|
|
433
|
+
if (request.title !== title)
|
|
434
|
+
continue;
|
|
435
|
+
for (const attachment of request.attachments ?? []) {
|
|
436
|
+
if (attachment.kind === "frame")
|
|
437
|
+
found = attachment;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return found;
|
|
441
|
+
}
|
|
442
|
+
function askedOf(title, requests) {
|
|
443
|
+
return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
|
|
444
|
+
}
|
|
445
|
+
function composeEntry(input) {
|
|
446
|
+
const slug = `${input.date}-${slugify(input.surface)}`;
|
|
447
|
+
const pictures = [];
|
|
448
|
+
const lines2 = [];
|
|
449
|
+
lines2.push(`# ${input.surface}, ${input.date}`);
|
|
450
|
+
lines2.push("");
|
|
451
|
+
lines2.push(`**${input.won.title}** won and became \`${input.won.to}\`. ${input.previews.length === 1 ? "It was the only direction." : `${input.previews.length} directions were compared.`}`);
|
|
452
|
+
lines2.push("");
|
|
453
|
+
for (const preview of input.previews) {
|
|
454
|
+
const won = preview.title === input.won.title;
|
|
455
|
+
lines2.push(`## ${preview.title}${won ? " \u2014 kept" : ""}`);
|
|
456
|
+
lines2.push("");
|
|
457
|
+
if (preview.note !== void 0 && preview.note.trim() !== "") {
|
|
458
|
+
lines2.push(preview.note.trim());
|
|
459
|
+
lines2.push("");
|
|
460
|
+
}
|
|
461
|
+
const frame = frameFor(preview.title, input.requests);
|
|
462
|
+
if (frame !== null) {
|
|
463
|
+
const name = `${slugify(preview.title)}.png`;
|
|
464
|
+
pictures.push({ from: frame.file, to: name });
|
|
465
|
+
lines2.push(``);
|
|
466
|
+
lines2.push("");
|
|
467
|
+
}
|
|
468
|
+
if (preview.basedOn !== void 0) {
|
|
469
|
+
lines2.push(`A variant of ${preview.basedOn}.`);
|
|
470
|
+
lines2.push("");
|
|
471
|
+
}
|
|
472
|
+
const asked = askedOf(preview.title, input.requests);
|
|
473
|
+
if (asked.length > 0) {
|
|
474
|
+
lines2.push("Asked for:");
|
|
475
|
+
lines2.push("");
|
|
476
|
+
for (const words of asked)
|
|
477
|
+
lines2.push(`- ${words}`);
|
|
478
|
+
lines2.push("");
|
|
479
|
+
}
|
|
480
|
+
const notes = input.annotations.filter((note) => note.title === preview.title);
|
|
481
|
+
if (notes.length > 0) {
|
|
482
|
+
lines2.push("Marked on the design:");
|
|
483
|
+
lines2.push("");
|
|
484
|
+
for (const note of notes)
|
|
485
|
+
lines2.push(`- ${note.note}`);
|
|
486
|
+
lines2.push("");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const failed = input.requests.filter((request) => request.status === "failed");
|
|
490
|
+
if (failed.length > 0) {
|
|
491
|
+
lines2.push("## Changes that did not land");
|
|
492
|
+
lines2.push("");
|
|
493
|
+
for (const request of failed) {
|
|
494
|
+
const why = request.failure?.message;
|
|
495
|
+
lines2.push(`- ${request.title}: ${request.intent}${why === void 0 ? "" : ` (${why})`}`);
|
|
496
|
+
}
|
|
497
|
+
lines2.push("");
|
|
498
|
+
}
|
|
499
|
+
return { slug, markdown: `${lines2.join("\n").trimEnd()}
|
|
500
|
+
`, pictures };
|
|
501
|
+
}
|
|
502
|
+
|
|
412
503
|
// ../server/dist/config.js
|
|
413
504
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
414
505
|
var DEFAULT_INSTALL_COMMAND = "npm install";
|
|
@@ -527,6 +618,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
527
618
|
if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
|
|
528
619
|
errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
|
|
529
620
|
}
|
|
621
|
+
const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
|
|
622
|
+
if (typeof logDir !== "string" || logDir.trim() === "") {
|
|
623
|
+
errors.push("logDir must be a non-empty string.");
|
|
624
|
+
}
|
|
530
625
|
const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
|
|
531
626
|
if (typeof installCommand !== "string" || installCommand.trim() === "") {
|
|
532
627
|
errors.push("installCommand must be a non-empty string.");
|
|
@@ -543,7 +638,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
543
638
|
previews,
|
|
544
639
|
scanPreviews,
|
|
545
640
|
devCommand: typeof devCommand === "string" ? devCommand : void 0,
|
|
546
|
-
installCommand
|
|
641
|
+
installCommand,
|
|
642
|
+
logDir
|
|
547
643
|
},
|
|
548
644
|
errors: []
|
|
549
645
|
};
|
|
@@ -1321,6 +1417,7 @@ import net from "net";
|
|
|
1321
1417
|
function createProxyHandler(options) {
|
|
1322
1418
|
const target = new URL(options.target);
|
|
1323
1419
|
const host = target.hostname;
|
|
1420
|
+
const dialHost = host.replace(/^\[|\]$/g, "");
|
|
1324
1421
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
1325
1422
|
const authority = target.port ? `${host}:${target.port}` : host;
|
|
1326
1423
|
function upstreamHeaders(req) {
|
|
@@ -1337,7 +1434,19 @@ function createProxyHandler(options) {
|
|
|
1337
1434
|
}
|
|
1338
1435
|
return {
|
|
1339
1436
|
request(req, res, publicOrigin) {
|
|
1340
|
-
|
|
1437
|
+
options.onActivity?.();
|
|
1438
|
+
options.onOpen?.();
|
|
1439
|
+
let open = true;
|
|
1440
|
+
const close = () => {
|
|
1441
|
+
if (!open)
|
|
1442
|
+
return;
|
|
1443
|
+
open = false;
|
|
1444
|
+
options.onActivity?.();
|
|
1445
|
+
options.onClose?.();
|
|
1446
|
+
};
|
|
1447
|
+
res.once("finish", close);
|
|
1448
|
+
res.once("close", close);
|
|
1449
|
+
const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
|
|
1341
1450
|
const headers = { ...upstreamRes.headers };
|
|
1342
1451
|
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin);
|
|
1343
1452
|
if (location !== void 0)
|
|
@@ -1359,7 +1468,17 @@ Start it, or point Leglas somewhere else with --user-port.`);
|
|
|
1359
1468
|
req.pipe(upstream);
|
|
1360
1469
|
},
|
|
1361
1470
|
upgrade(req, socket, head) {
|
|
1362
|
-
|
|
1471
|
+
options.onActivity?.();
|
|
1472
|
+
options.onOpen?.();
|
|
1473
|
+
let open = true;
|
|
1474
|
+
const closeActivity = () => {
|
|
1475
|
+
if (!open)
|
|
1476
|
+
return;
|
|
1477
|
+
open = false;
|
|
1478
|
+
options.onActivity?.();
|
|
1479
|
+
options.onClose?.();
|
|
1480
|
+
};
|
|
1481
|
+
const upstream = net.connect(port, dialHost, () => {
|
|
1363
1482
|
const headers = Object.entries(upstreamHeaders(req)).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value}\r
|
|
1364
1483
|
`).join("");
|
|
1365
1484
|
upstream.write(`${req.method} ${req.url} HTTP/1.1\r
|
|
@@ -1371,6 +1490,7 @@ ${headers}\r
|
|
|
1371
1490
|
socket.pipe(upstream);
|
|
1372
1491
|
});
|
|
1373
1492
|
const shutdown = () => {
|
|
1493
|
+
closeActivity();
|
|
1374
1494
|
upstream.destroy();
|
|
1375
1495
|
socket.destroy();
|
|
1376
1496
|
};
|
|
@@ -1381,6 +1501,62 @@ ${headers}\r
|
|
|
1381
1501
|
}
|
|
1382
1502
|
};
|
|
1383
1503
|
}
|
|
1504
|
+
function startProxyServer(options) {
|
|
1505
|
+
return new Promise((resolve5, reject) => {
|
|
1506
|
+
let open = 0;
|
|
1507
|
+
const handler = createProxyHandler({
|
|
1508
|
+
...options,
|
|
1509
|
+
onOpen: () => {
|
|
1510
|
+
open += 1;
|
|
1511
|
+
options.onOpen?.();
|
|
1512
|
+
},
|
|
1513
|
+
onClose: () => {
|
|
1514
|
+
open = Math.max(0, open - 1);
|
|
1515
|
+
options.onClose?.();
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
const server = http.createServer((req, res) => {
|
|
1519
|
+
const address = server.address();
|
|
1520
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1521
|
+
handler.request(req, res, `http://127.0.0.1:${port}`);
|
|
1522
|
+
});
|
|
1523
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
1524
|
+
server.on("connection", (socket) => {
|
|
1525
|
+
sockets.add(socket);
|
|
1526
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1527
|
+
});
|
|
1528
|
+
server.on("upgrade", (req, socket, head) => handler.upgrade(req, socket, head));
|
|
1529
|
+
const onError = (error) => {
|
|
1530
|
+
server.removeListener("listening", onListening);
|
|
1531
|
+
reject(error);
|
|
1532
|
+
};
|
|
1533
|
+
const onListening = () => {
|
|
1534
|
+
server.removeListener("error", onError);
|
|
1535
|
+
const address = server.address();
|
|
1536
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1537
|
+
let closed = null;
|
|
1538
|
+
resolve5({
|
|
1539
|
+
active: () => open > 0,
|
|
1540
|
+
close: () => {
|
|
1541
|
+
if (closed !== null)
|
|
1542
|
+
return closed;
|
|
1543
|
+
closed = new Promise((done) => {
|
|
1544
|
+
for (const socket of sockets)
|
|
1545
|
+
socket.destroy();
|
|
1546
|
+
sockets.clear();
|
|
1547
|
+
server.closeAllConnections();
|
|
1548
|
+
server.close(() => done());
|
|
1549
|
+
});
|
|
1550
|
+
return closed;
|
|
1551
|
+
},
|
|
1552
|
+
url: `http://127.0.0.1:${port}`
|
|
1553
|
+
});
|
|
1554
|
+
};
|
|
1555
|
+
server.once("error", onError);
|
|
1556
|
+
server.once("listening", onListening);
|
|
1557
|
+
server.listen(0, "127.0.0.1");
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1384
1560
|
|
|
1385
1561
|
// ../server/dist/browser.js
|
|
1386
1562
|
import { randomBytes } from "crypto";
|
|
@@ -1448,12 +1624,12 @@ function buildNumber(entry) {
|
|
|
1448
1624
|
const digits = /(\d+)\s*$/.exec(entry)?.[1];
|
|
1449
1625
|
return digits === void 0 ? 0 : Number(digits);
|
|
1450
1626
|
}
|
|
1451
|
-
function cacheRoots(platform, home,
|
|
1627
|
+
function cacheRoots(platform, home, readdir5 = readableDirectories) {
|
|
1452
1628
|
const playwright = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
1453
1629
|
const puppeteer = join4(home, ".cache", "puppeteer");
|
|
1454
1630
|
const newestFirst = (dir, keep = () => true) => ({
|
|
1455
1631
|
root: dir,
|
|
1456
|
-
entries:
|
|
1632
|
+
entries: readdir5(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
|
|
1457
1633
|
});
|
|
1458
1634
|
return [
|
|
1459
1635
|
newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
|
|
@@ -1474,13 +1650,13 @@ function findBrowser(search = {}) {
|
|
|
1474
1650
|
const home = search.home ?? homedir();
|
|
1475
1651
|
const exists = search.exists ?? existsSync2;
|
|
1476
1652
|
const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
|
|
1477
|
-
const
|
|
1653
|
+
const readdir5 = search.readdir ?? readableDirectories;
|
|
1478
1654
|
const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
|
|
1479
1655
|
for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
|
|
1480
1656
|
if (typeof candidate === "string" && candidate !== "" && exists(candidate))
|
|
1481
1657
|
return candidate;
|
|
1482
1658
|
}
|
|
1483
|
-
const caches = cacheRoots(platform, home,
|
|
1659
|
+
const caches = cacheRoots(platform, home, readdir5);
|
|
1484
1660
|
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join4(root, entry, ...rest)))));
|
|
1485
1661
|
if (shell !== null)
|
|
1486
1662
|
return shell;
|
|
@@ -1510,7 +1686,7 @@ function findBrowser(search = {}) {
|
|
|
1510
1686
|
}
|
|
1511
1687
|
if (platform === "darwin" || platform === "linux") {
|
|
1512
1688
|
const playwrightRoot = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
1513
|
-
const playwright =
|
|
1689
|
+
const playwright = readdir5(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
1514
1690
|
const root = join4(playwrightRoot, entry);
|
|
1515
1691
|
return [
|
|
1516
1692
|
...FOR_TESTING.map((rest) => join4(root, ...rest)),
|
|
@@ -1527,7 +1703,7 @@ function findBrowser(search = {}) {
|
|
|
1527
1703
|
const puppeteerCache = join4(home, ".cache", "puppeteer");
|
|
1528
1704
|
for (const kind of ["chrome", "chrome-headless-shell"]) {
|
|
1529
1705
|
const kindRoot = join4(puppeteerCache, kind);
|
|
1530
|
-
const found = firstExisting(
|
|
1706
|
+
const found = firstExisting(readdir5(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
1531
1707
|
const root = join4(kindRoot, entry);
|
|
1532
1708
|
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join4(root, ...rest));
|
|
1533
1709
|
}));
|
|
@@ -2035,6 +2211,32 @@ function createBrowserPool(options = {}) {
|
|
|
2035
2211
|
};
|
|
2036
2212
|
}
|
|
2037
2213
|
|
|
2214
|
+
// ../server/dist/hydration.js
|
|
2215
|
+
function hydrationEvidence(messages) {
|
|
2216
|
+
for (const raw of messages) {
|
|
2217
|
+
const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
|
|
2218
|
+
if (/Minified React error #(418|419|422|423|425)\b/.test(message2)) {
|
|
2219
|
+
return { framework: "React", message: message2 };
|
|
2220
|
+
}
|
|
2221
|
+
if (/Hydration failed because/.test(message2) || /error while hydrating/i.test(message2) || /Text content (did not|does not) match/i.test(message2) || /Expected server HTML to contain/i.test(message2) || /did not match\. Server:/.test(message2)) {
|
|
2222
|
+
return { framework: "React", message: message2 };
|
|
2223
|
+
}
|
|
2224
|
+
if (/Hydration (node|text|children|class|style|attribute) mismatch/i.test(message2) || /Hydration completed but contains mismatches/i.test(message2)) {
|
|
2225
|
+
return { framework: "Vue", message: message2 };
|
|
2226
|
+
}
|
|
2227
|
+
if (/hydration_mismatch/.test(message2)) {
|
|
2228
|
+
return { framework: "Svelte", message: message2 };
|
|
2229
|
+
}
|
|
2230
|
+
if (/Hydration Mismatch\. Unable to find DOM nodes/.test(message2)) {
|
|
2231
|
+
return { framework: "Solid", message: message2 };
|
|
2232
|
+
}
|
|
2233
|
+
if (/hydrat/i.test(message2) && (/expected .+ but found/i.test(message2) || /mismatch/i.test(message2) && /(node|element|markup|dom|tag|text|attribute|server|client)/i.test(message2))) {
|
|
2234
|
+
return { framework: "the app", message: message2 };
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2038
2240
|
// ../server/dist/capture.js
|
|
2039
2241
|
var FRAME_MAX_HEIGHT = 4e3;
|
|
2040
2242
|
var MIN_WIDTH = 320;
|
|
@@ -2128,10 +2330,12 @@ function locatorExpression(focus) {
|
|
|
2128
2330
|
async function render(page, input) {
|
|
2129
2331
|
const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
|
|
2130
2332
|
const errors = [];
|
|
2333
|
+
let hydration = null;
|
|
2131
2334
|
const remember = (value) => {
|
|
2335
|
+
const message2 = String(value ?? "").trim().slice(0, 240);
|
|
2336
|
+
hydration ??= hydrationEvidence([message2]);
|
|
2132
2337
|
if (errors.length >= 10)
|
|
2133
2338
|
return;
|
|
2134
|
-
const message2 = String(value ?? "").slice(0, 240);
|
|
2135
2339
|
if (message2 === "" || /favicon/i.test(message2))
|
|
2136
2340
|
return;
|
|
2137
2341
|
errors.push(message2);
|
|
@@ -2279,7 +2483,7 @@ async function render(page, input) {
|
|
|
2279
2483
|
resolved
|
|
2280
2484
|
});
|
|
2281
2485
|
}
|
|
2282
|
-
return { frame, crops, errors, cut };
|
|
2486
|
+
return { frame, crops, errors, hydration, cut };
|
|
2283
2487
|
} finally {
|
|
2284
2488
|
for (const stop of unlisten)
|
|
2285
2489
|
stop();
|
|
@@ -2423,7 +2627,13 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2423
2627
|
const capture = deps.capture ?? capturePage;
|
|
2424
2628
|
const deadlineMs = deps.deadlineMs ?? 12e3;
|
|
2425
2629
|
const destination = join5(cwd, CAPTURES_DIR, requestId);
|
|
2426
|
-
const captured = {
|
|
2630
|
+
const captured = {
|
|
2631
|
+
attachments: [],
|
|
2632
|
+
errors: [],
|
|
2633
|
+
hydration: null,
|
|
2634
|
+
cut: false,
|
|
2635
|
+
skipped: null
|
|
2636
|
+
};
|
|
2427
2637
|
const references = [];
|
|
2428
2638
|
requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
|
|
2429
2639
|
const controller = new AbortController();
|
|
@@ -2473,6 +2683,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2473
2683
|
viewport: direction.frame.width
|
|
2474
2684
|
});
|
|
2475
2685
|
captured.errors = direction.errors;
|
|
2686
|
+
captured.hydration = direction.hydration;
|
|
2476
2687
|
captured.cut = direction.cut;
|
|
2477
2688
|
for (let index = 0; index < direction.crops.length; index += 1) {
|
|
2478
2689
|
const crop = direction.crops[index];
|
|
@@ -2746,6 +2957,8 @@ async function startAppProcess(options) {
|
|
|
2746
2957
|
}
|
|
2747
2958
|
|
|
2748
2959
|
// ../server/dist/branches.js
|
|
2960
|
+
var BRANCH_IDLE_MS = 10 * 60 * 1e3;
|
|
2961
|
+
var BRANCH_SWEEP_MS = 3e4;
|
|
2749
2962
|
function publicBranchState(state) {
|
|
2750
2963
|
if (state.status === "ready")
|
|
2751
2964
|
return { status: "ready" };
|
|
@@ -2755,7 +2968,11 @@ function createBranchRegistry(options) {
|
|
|
2755
2968
|
const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
|
|
2756
2969
|
const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
|
|
2757
2970
|
const inflight = /* @__PURE__ */ new Map();
|
|
2971
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
2972
|
+
const lastActivity = /* @__PURE__ */ new Map();
|
|
2973
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
2758
2974
|
const boot = options.startWorktree ?? startWorktree;
|
|
2975
|
+
const proxy = options.startProxy ?? startProxyServer;
|
|
2759
2976
|
let closed = false;
|
|
2760
2977
|
let stopPromise = null;
|
|
2761
2978
|
const transition = (title, state) => {
|
|
@@ -2767,6 +2984,43 @@ function createBranchRegistry(options) {
|
|
|
2767
2984
|
options.onChange?.(title, state);
|
|
2768
2985
|
return state;
|
|
2769
2986
|
};
|
|
2987
|
+
const stopReady = (title, state, toIdle) => {
|
|
2988
|
+
const current = stopping.get(title);
|
|
2989
|
+
if (current !== void 0)
|
|
2990
|
+
return current;
|
|
2991
|
+
const pending = (async () => {
|
|
2992
|
+
await proxies.get(title)?.close().catch(() => {
|
|
2993
|
+
});
|
|
2994
|
+
await state.worktree.stop().catch(() => {
|
|
2995
|
+
});
|
|
2996
|
+
proxies.delete(title);
|
|
2997
|
+
lastActivity.delete(title);
|
|
2998
|
+
if (toIdle && !closed && states.get(title) === state) {
|
|
2999
|
+
transition(title, { status: "idle" });
|
|
3000
|
+
}
|
|
3001
|
+
})().finally(() => {
|
|
3002
|
+
stopping.delete(title);
|
|
3003
|
+
});
|
|
3004
|
+
stopping.set(title, pending);
|
|
3005
|
+
return pending;
|
|
3006
|
+
};
|
|
3007
|
+
const sweep = () => {
|
|
3008
|
+
const now = Date.now();
|
|
3009
|
+
for (const [title, state] of states) {
|
|
3010
|
+
const branchProxy = proxies.get(title);
|
|
3011
|
+
if (state.status !== "ready" || branchProxy === void 0 || stopping.has(title))
|
|
3012
|
+
continue;
|
|
3013
|
+
if (branchProxy.active()) {
|
|
3014
|
+
lastActivity.set(title, now);
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
const seen = lastActivity.get(title) ?? now;
|
|
3018
|
+
if (now - seen >= BRANCH_IDLE_MS)
|
|
3019
|
+
void stopReady(title, state, true);
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
const sweepTimer = setInterval(sweep, BRANCH_SWEEP_MS);
|
|
3023
|
+
sweepTimer.unref();
|
|
2770
3024
|
const begin = (title) => {
|
|
2771
3025
|
const preview = previews.get(title);
|
|
2772
3026
|
const current = states.get(title);
|
|
@@ -2794,9 +3048,21 @@ function createBranchRegistry(options) {
|
|
|
2794
3048
|
} catch (error) {
|
|
2795
3049
|
checkout = Promise.reject(error);
|
|
2796
3050
|
}
|
|
2797
|
-
const starting = checkout.then((worktree) => {
|
|
3051
|
+
const starting = checkout.then(async (worktree) => {
|
|
2798
3052
|
transition(title, { status: "starting", phase: "starting" });
|
|
2799
|
-
|
|
3053
|
+
try {
|
|
3054
|
+
const branchProxy = await proxy({
|
|
3055
|
+
target: worktree.url,
|
|
3056
|
+
onActivity: () => lastActivity.set(title, Date.now())
|
|
3057
|
+
});
|
|
3058
|
+
proxies.set(title, branchProxy);
|
|
3059
|
+
lastActivity.set(title, Date.now());
|
|
3060
|
+
return transition(title, { status: "ready", worktree });
|
|
3061
|
+
} catch (error) {
|
|
3062
|
+
await worktree.stop().catch(() => {
|
|
3063
|
+
});
|
|
3064
|
+
throw error;
|
|
3065
|
+
}
|
|
2800
3066
|
}).catch((error) => transition(title, {
|
|
2801
3067
|
status: "failed",
|
|
2802
3068
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -2808,15 +3074,17 @@ function createBranchRegistry(options) {
|
|
|
2808
3074
|
};
|
|
2809
3075
|
return {
|
|
2810
3076
|
state: (title) => states.get(title),
|
|
3077
|
+
url: (title) => proxies.get(title)?.url,
|
|
2811
3078
|
start: begin,
|
|
2812
3079
|
stop: () => {
|
|
2813
3080
|
if (stopPromise !== null)
|
|
2814
3081
|
return stopPromise;
|
|
2815
3082
|
closed = true;
|
|
3083
|
+
clearInterval(sweepTimer);
|
|
2816
3084
|
stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3085
|
+
await Promise.allSettled([...stopping.values()]);
|
|
3086
|
+
const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
|
|
3087
|
+
await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
|
|
2820
3088
|
});
|
|
2821
3089
|
return stopPromise;
|
|
2822
3090
|
}
|
|
@@ -3111,10 +3379,10 @@ function scope(leglasCommand, quotedTitle) {
|
|
|
3111
3379
|
|
|
3112
3380
|
This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
|
|
3113
3381
|
|
|
3114
|
-
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on.`;
|
|
3382
|
+
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on. A shared script may gain one small per-direction override, at the point it reads what it renders, that defaults to what it renders today; every other direction then renders exactly as before, so that counts as additive.`;
|
|
3115
3383
|
}
|
|
3116
3384
|
function capturedBlock(captured) {
|
|
3117
|
-
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.skipped === null)
|
|
3385
|
+
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.hydration === null && captured.skipped === null)
|
|
3118
3386
|
return "";
|
|
3119
3387
|
const lines2 = [];
|
|
3120
3388
|
const frames = captured.attachments.filter((attachment) => attachment.kind === "frame" || attachment.kind === "note");
|
|
@@ -3146,6 +3414,9 @@ function capturedBlock(captured) {
|
|
|
3146
3414
|
for (const error of captured.errors)
|
|
3147
3415
|
lines2.push(` - ${error}`);
|
|
3148
3416
|
}
|
|
3417
|
+
if (captured.hydration !== null) {
|
|
3418
|
+
lines2.push(`After load, ${captured.hydration.framework} rebuilt this page in the browser from the app's own JavaScript and data (${captured.hydration.message}). Markup edited in the served HTML shows for a moment and is then replaced, so make the change where that JavaScript gets what it renders: the data or source it reads, or a per-direction override that a shared script reads with the original as its default. Look at the result a few seconds after load, not at first paint.`);
|
|
3419
|
+
}
|
|
3149
3420
|
if (captured.skipped !== null) {
|
|
3150
3421
|
lines2.push(`(${captured.skipped} Use the live preview instead.)`);
|
|
3151
3422
|
}
|
|
@@ -4913,6 +5184,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
4913
5184
|
|
|
4914
5185
|
// ../server/dist/server.js
|
|
4915
5186
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5187
|
+
import { createHash as createHash2 } from "crypto";
|
|
4916
5188
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
4917
5189
|
import http2 from "http";
|
|
4918
5190
|
import net3 from "net";
|
|
@@ -4991,6 +5263,30 @@ function sendJson(res, status, body) {
|
|
|
4991
5263
|
});
|
|
4992
5264
|
res.end(payload);
|
|
4993
5265
|
}
|
|
5266
|
+
function etagMatches(value, etag) {
|
|
5267
|
+
if (value === void 0)
|
|
5268
|
+
return false;
|
|
5269
|
+
const values = Array.isArray(value) ? value : [value];
|
|
5270
|
+
return values.some((header) => header.split(",").some((candidate) => {
|
|
5271
|
+
const tag = candidate.trim();
|
|
5272
|
+
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
5273
|
+
}));
|
|
5274
|
+
}
|
|
5275
|
+
function sendConditionalJson(req, res, body) {
|
|
5276
|
+
const payload = JSON.stringify(body);
|
|
5277
|
+
const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
|
|
5278
|
+
if (etagMatches(req.headers["if-none-match"], etag)) {
|
|
5279
|
+
res.writeHead(304, { etag, "cache-control": "private, no-cache" });
|
|
5280
|
+
res.end();
|
|
5281
|
+
return;
|
|
5282
|
+
}
|
|
5283
|
+
res.writeHead(200, {
|
|
5284
|
+
"content-type": "application/json; charset=utf-8",
|
|
5285
|
+
"cache-control": "private, no-cache",
|
|
5286
|
+
etag
|
|
5287
|
+
});
|
|
5288
|
+
res.end(payload);
|
|
5289
|
+
}
|
|
4994
5290
|
var CAPTURE_DEADLINE_MS = 15e3;
|
|
4995
5291
|
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
4996
5292
|
function captureSlug(title) {
|
|
@@ -5383,9 +5679,10 @@ async function startServer(options) {
|
|
|
5383
5679
|
return preview;
|
|
5384
5680
|
const state = branches.state(preview.title) ?? { status: "idle" };
|
|
5385
5681
|
const { url: route, ...withoutUrl } = preview;
|
|
5682
|
+
const branchUrl = branches.url(preview.title);
|
|
5386
5683
|
return state.status === "ready" ? {
|
|
5387
5684
|
...withoutUrl,
|
|
5388
|
-
url: `${state.worktree.url}${route}`,
|
|
5685
|
+
url: `${branchUrl ?? state.worktree.url}${route}`,
|
|
5389
5686
|
state: publicBranchState(state)
|
|
5390
5687
|
} : { ...withoutUrl, state: publicBranchState(state) };
|
|
5391
5688
|
};
|
|
@@ -5394,7 +5691,7 @@ async function startServer(options) {
|
|
|
5394
5691
|
if (preview.branch === void 0)
|
|
5395
5692
|
return preview;
|
|
5396
5693
|
const state = branches.state(preview.title);
|
|
5397
|
-
return state?.status === "ready" ? { ...preview, url: `${state.worktree.url}${preview.url}` } : null;
|
|
5694
|
+
return state?.status === "ready" ? { ...preview, url: `${branches.url(preview.title) ?? state.worktree.url}${preview.url}` } : null;
|
|
5398
5695
|
};
|
|
5399
5696
|
if (options.pool === void 0) {
|
|
5400
5697
|
void reapOrphanedBrowsers().catch(() => {
|
|
@@ -5457,7 +5754,7 @@ async function startServer(options) {
|
|
|
5457
5754
|
errors.push(notice);
|
|
5458
5755
|
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
5459
5756
|
if (localErrors.length > 0) {
|
|
5460
|
-
return
|
|
5757
|
+
return sendConditionalJson(req, res, {
|
|
5461
5758
|
project,
|
|
5462
5759
|
devServer: target,
|
|
5463
5760
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5470,7 +5767,7 @@ async function startServer(options) {
|
|
|
5470
5767
|
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
5471
5768
|
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
5472
5769
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
5473
|
-
|
|
5770
|
+
sendConditionalJson(req, res, {
|
|
5474
5771
|
project,
|
|
5475
5772
|
devServer: target,
|
|
5476
5773
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5478,7 +5775,7 @@ async function startServer(options) {
|
|
|
5478
5775
|
errors,
|
|
5479
5776
|
warnings: configWarnings
|
|
5480
5777
|
});
|
|
5481
|
-
}).catch(() =>
|
|
5778
|
+
}).catch(() => sendConditionalJson(req, res, {
|
|
5482
5779
|
project,
|
|
5483
5780
|
devServer: target,
|
|
5484
5781
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5801,6 +6098,7 @@ async function startServer(options) {
|
|
|
5801
6098
|
height: shot.height,
|
|
5802
6099
|
viewport: result2.frame.width,
|
|
5803
6100
|
errors: result2.errors,
|
|
6101
|
+
hydration: result2.hydration,
|
|
5804
6102
|
cut: result2.cut
|
|
5805
6103
|
});
|
|
5806
6104
|
} catch (error) {
|
|
@@ -5913,7 +6211,7 @@ async function startServer(options) {
|
|
|
5913
6211
|
waiting: null,
|
|
5914
6212
|
failedIds: []
|
|
5915
6213
|
};
|
|
5916
|
-
return void readRequests(cwd).then((requests) =>
|
|
6214
|
+
return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
|
|
5917
6215
|
requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
|
|
5918
6216
|
id,
|
|
5919
6217
|
title,
|
|
@@ -6016,7 +6314,7 @@ async function startServer(options) {
|
|
|
6016
6314
|
});
|
|
6017
6315
|
}
|
|
6018
6316
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
6019
|
-
return void readAnnotations(cwd).then((annotations) =>
|
|
6317
|
+
return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
|
|
6020
6318
|
}
|
|
6021
6319
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6022
6320
|
if (!hasJsonBody(req)) {
|
|
@@ -6142,7 +6440,7 @@ async function startServer(options) {
|
|
|
6142
6440
|
});
|
|
6143
6441
|
}
|
|
6144
6442
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
6145
|
-
return void probe(target).then((reachable) =>
|
|
6443
|
+
return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
|
|
6146
6444
|
}
|
|
6147
6445
|
if (path.startsWith(`${FILES_PREFIX}/`)) {
|
|
6148
6446
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
@@ -6544,6 +6842,14 @@ When asked for design variations, alternatives, or "a few options":
|
|
|
6544
6842
|
and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
6545
6843
|
(the config needs \`devCommand\` with \`{port}\`). Everything below is the
|
|
6546
6844
|
ordinary, in-app path.
|
|
6845
|
+
A page the app rebuilds in the browser after load (anything that hydrates:
|
|
6846
|
+
Next, Nuxt, SvelteKit, a captured production site) is not its served HTML.
|
|
6847
|
+
Markup edited there shows for a moment and is then replaced from the app's
|
|
6848
|
+
own JavaScript and data, so make the change where that JavaScript gets what
|
|
6849
|
+
it renders. When that is a script other directions share, give it a
|
|
6850
|
+
per-direction override that defaults to what it renders today: every other
|
|
6851
|
+
direction renders exactly as before, which is adding beside, not rewriting.
|
|
6852
|
+
\`npx leglas show\` says when a page was rebuilt after load.
|
|
6547
6853
|
2. Run \`npx leglas explore <surface> --count <n>\` first, adding
|
|
6548
6854
|
\`--based-on "<title>"\` when the user wants variations of a direction they
|
|
6549
6855
|
already like. It prints what the set needs and how to register it. In
|
|
@@ -6594,6 +6900,12 @@ out of the ignored directory, deletes the rest of the exploration, and drops
|
|
|
6594
6900
|
them from the rail. Then change their component to use the kept component
|
|
6595
6901
|
instead of the switcher.
|
|
6596
6902
|
|
|
6903
|
+
Keeping also writes what the exploration was into \`design-log/\`, which is
|
|
6904
|
+
committed. Before exploring a surface, read \`npx leglas log --json\` and any
|
|
6905
|
+
entry for that surface: it says what was already tried there, in the user's own
|
|
6906
|
+
words, and which direction won. Proposing something that was already rejected
|
|
6907
|
+
wastes their time, and the record is there so you do not have to ask.
|
|
6908
|
+
|
|
6597
6909
|
Useful to know:
|
|
6598
6910
|
|
|
6599
6911
|
- \`.leglas/\` is gitignored. Exploration is disposable and nothing in there
|
|
@@ -6693,7 +7005,7 @@ async function runInit(options, deps) {
|
|
|
6693
7005
|
|
|
6694
7006
|
// src/run-keep.ts
|
|
6695
7007
|
import { existsSync as existsSync4 } from "fs";
|
|
6696
|
-
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
7008
|
+
import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6697
7009
|
import { dirname as dirname9, join as join15 } from "path";
|
|
6698
7010
|
|
|
6699
7011
|
// src/keep.ts
|
|
@@ -6776,6 +7088,29 @@ function renameExport(source, to) {
|
|
|
6776
7088
|
to
|
|
6777
7089
|
);
|
|
6778
7090
|
}
|
|
7091
|
+
async function writeLogEntry(options) {
|
|
7092
|
+
const entry = composeEntry({
|
|
7093
|
+
surface: options.surface,
|
|
7094
|
+
won: options.won,
|
|
7095
|
+
previews: options.previews,
|
|
7096
|
+
requests: await readRequests(options.cwd),
|
|
7097
|
+
annotations: await readAnnotations(options.cwd),
|
|
7098
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
7099
|
+
});
|
|
7100
|
+
const dir = join15(options.cwd, options.logDir);
|
|
7101
|
+
await mkdir9(dir, { recursive: true });
|
|
7102
|
+
const file = join15(dir, `${entry.slug}.md`);
|
|
7103
|
+
await writeFile10(file, entry.markdown, "utf8");
|
|
7104
|
+
if (entry.pictures.length > 0) {
|
|
7105
|
+
const pictureDir = join15(dir, entry.slug);
|
|
7106
|
+
await mkdir9(pictureDir, { recursive: true });
|
|
7107
|
+
for (const picture of entry.pictures) {
|
|
7108
|
+
await copyFile2(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
|
|
7109
|
+
});
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
7112
|
+
return `${options.logDir}/${entry.slug}.md`;
|
|
7113
|
+
}
|
|
6779
7114
|
async function runKeep(options, deps) {
|
|
6780
7115
|
const loaded = await loadConfig(options.cwd);
|
|
6781
7116
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -6804,6 +7139,20 @@ async function runKeep(options, deps) {
|
|
|
6804
7139
|
const source = await readFile13(from, "utf8");
|
|
6805
7140
|
await mkdir9(dirname9(to), { recursive: true });
|
|
6806
7141
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
7142
|
+
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
7143
|
+
let logged = null;
|
|
7144
|
+
let logError = null;
|
|
7145
|
+
try {
|
|
7146
|
+
logged = await writeLogEntry({
|
|
7147
|
+
cwd: options.cwd,
|
|
7148
|
+
logDir: loaded.config?.logDir ?? DEFAULT_LOG_DIR,
|
|
7149
|
+
surface,
|
|
7150
|
+
won: { title: resolved.title, to: plan.move.to },
|
|
7151
|
+
previews: previews.filter((preview) => plan.dropTitles.includes(preview.title))
|
|
7152
|
+
});
|
|
7153
|
+
} catch (error) {
|
|
7154
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
7155
|
+
}
|
|
6807
7156
|
await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6808
7157
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
6809
7158
|
if (options.json) {
|
|
@@ -6815,6 +7164,8 @@ async function runKeep(options, deps) {
|
|
|
6815
7164
|
exportName: plan.exportName,
|
|
6816
7165
|
removed: plan.removeDir,
|
|
6817
7166
|
droppedPreviews: dropped,
|
|
7167
|
+
logged,
|
|
7168
|
+
logError,
|
|
6818
7169
|
instructions: plan.instructions
|
|
6819
7170
|
})
|
|
6820
7171
|
);
|
|
@@ -6822,6 +7173,8 @@ async function runKeep(options, deps) {
|
|
|
6822
7173
|
}
|
|
6823
7174
|
deps.log(` kept ${plan.move.to}`);
|
|
6824
7175
|
deps.log(` removed ${plan.removeDir}`);
|
|
7176
|
+
if (logged !== null) deps.log(` logged ${logged}`);
|
|
7177
|
+
if (logError !== null) deps.error(` The decision log could not be written: ${logError}`);
|
|
6825
7178
|
if (dropped > 0) {
|
|
6826
7179
|
deps.log(` dropped ${dropped} direction${dropped === 1 ? "" : "s"} from the rail`);
|
|
6827
7180
|
}
|
|
@@ -6918,17 +7271,67 @@ async function runNew(options, deps) {
|
|
|
6918
7271
|
return { exitCode: 0, written };
|
|
6919
7272
|
}
|
|
6920
7273
|
|
|
6921
|
-
// src/run-
|
|
6922
|
-
import { readFile as readFile15,
|
|
7274
|
+
// src/run-log.ts
|
|
7275
|
+
import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
|
|
6923
7276
|
import { join as join17 } from "path";
|
|
7277
|
+
function headline(markdown) {
|
|
7278
|
+
const first = markdown.split("\n", 1)[0] ?? "";
|
|
7279
|
+
return first.replace(/^#\s*/, "").trim();
|
|
7280
|
+
}
|
|
7281
|
+
async function runLog(options, deps) {
|
|
7282
|
+
const loaded = await loadConfig(options.cwd);
|
|
7283
|
+
const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
|
|
7284
|
+
let names;
|
|
7285
|
+
try {
|
|
7286
|
+
names = (await readdir4(join17(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
|
|
7287
|
+
} catch {
|
|
7288
|
+
names = [];
|
|
7289
|
+
}
|
|
7290
|
+
if (options.entry !== null) {
|
|
7291
|
+
const wanted = options.entry.replace(/\.md$/, "");
|
|
7292
|
+
const found = names.find((name) => name === `${wanted}.md`);
|
|
7293
|
+
if (found === void 0) {
|
|
7294
|
+
const error = `No entry called ${JSON.stringify(options.entry)} in ${dir}.`;
|
|
7295
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
7296
|
+
else deps.error(error);
|
|
7297
|
+
return { exitCode: 1 };
|
|
7298
|
+
}
|
|
7299
|
+
const markdown = await readFile15(join17(options.cwd, dir, found), "utf8");
|
|
7300
|
+
if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
|
|
7301
|
+
else deps.log(markdown.trimEnd());
|
|
7302
|
+
return { exitCode: 0 };
|
|
7303
|
+
}
|
|
7304
|
+
const entries = await Promise.all(
|
|
7305
|
+
names.map(async (name) => ({
|
|
7306
|
+
entry: name.replace(/\.md$/, ""),
|
|
7307
|
+
title: headline(await readFile15(join17(options.cwd, dir, name), "utf8")),
|
|
7308
|
+
file: `${dir}/${name}`
|
|
7309
|
+
}))
|
|
7310
|
+
);
|
|
7311
|
+
if (options.json) {
|
|
7312
|
+
deps.log(JSON.stringify({ ok: true, dir, entries }));
|
|
7313
|
+
return { exitCode: 0 };
|
|
7314
|
+
}
|
|
7315
|
+
if (entries.length === 0) {
|
|
7316
|
+
deps.log(` No decisions recorded yet. One is written each time you run leglas keep.`);
|
|
7317
|
+
return { exitCode: 0 };
|
|
7318
|
+
}
|
|
7319
|
+
const width = Math.max(...entries.map((entry) => entry.entry.length));
|
|
7320
|
+
for (const entry of entries) deps.log(` ${entry.entry.padEnd(width)} ${entry.title}`);
|
|
7321
|
+
return { exitCode: 0 };
|
|
7322
|
+
}
|
|
7323
|
+
|
|
7324
|
+
// src/run-previews.ts
|
|
7325
|
+
import { readFile as readFile16, writeFile as writeFile12 } from "fs/promises";
|
|
7326
|
+
import { join as join18 } from "path";
|
|
6924
7327
|
function envelope(deps, ok, body) {
|
|
6925
7328
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
6926
7329
|
}
|
|
6927
7330
|
async function ensureIgnored(cwd) {
|
|
6928
|
-
const path =
|
|
7331
|
+
const path = join18(cwd, ".gitignore");
|
|
6929
7332
|
let current = null;
|
|
6930
7333
|
try {
|
|
6931
|
-
current = await
|
|
7334
|
+
current = await readFile16(path, "utf8");
|
|
6932
7335
|
} catch {
|
|
6933
7336
|
current = null;
|
|
6934
7337
|
}
|
|
@@ -7210,6 +7613,10 @@ async function runShow(options, deps) {
|
|
|
7210
7613
|
height: captured.height,
|
|
7211
7614
|
viewport: captured.viewport,
|
|
7212
7615
|
errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
|
|
7616
|
+
hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
|
|
7617
|
+
framework: captured.hydration.framework,
|
|
7618
|
+
message: captured.hydration.message
|
|
7619
|
+
} : null,
|
|
7213
7620
|
cut: captured.cut === true
|
|
7214
7621
|
};
|
|
7215
7622
|
}
|
|
@@ -7237,6 +7644,12 @@ async function runShow(options, deps) {
|
|
|
7237
7644
|
if (envelope2.screenshot.cut) {
|
|
7238
7645
|
deps.log(" the top of the page only; it is taller than one capture");
|
|
7239
7646
|
}
|
|
7647
|
+
if (envelope2.screenshot.hydration !== null) {
|
|
7648
|
+
deps.log(
|
|
7649
|
+
` hydration ${envelope2.screenshot.hydration.framework} rebuilt the page in the browser after load; the served markup is not what is on screen`
|
|
7650
|
+
);
|
|
7651
|
+
deps.log(` ${envelope2.screenshot.hydration.message}`);
|
|
7652
|
+
}
|
|
7240
7653
|
if (envelope2.screenshot.errors.length > 0) {
|
|
7241
7654
|
const count = envelope2.screenshot.errors.length;
|
|
7242
7655
|
deps.log(` console ${count} ${count === 1 ? "error" : "errors"} on load`);
|
|
@@ -7255,15 +7668,15 @@ async function runShow(options, deps) {
|
|
|
7255
7668
|
|
|
7256
7669
|
// src/run-watch.ts
|
|
7257
7670
|
import { spawn as spawn3 } from "child_process";
|
|
7258
|
-
import { mkdir as mkdir11, readFile as
|
|
7259
|
-
import { dirname as dirname11, join as
|
|
7671
|
+
import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile13 } from "fs/promises";
|
|
7672
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
7260
7673
|
var POLL_MS2 = 2e3;
|
|
7261
7674
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
7262
7675
|
async function saveTemplate(cwd, run4) {
|
|
7263
|
-
const path =
|
|
7676
|
+
const path = join19(cwd, WATCH_PATH);
|
|
7264
7677
|
let config = {};
|
|
7265
7678
|
try {
|
|
7266
|
-
const parsed2 = JSON.parse(await
|
|
7679
|
+
const parsed2 = JSON.parse(await readFile17(path, "utf8"));
|
|
7267
7680
|
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
7268
7681
|
config = parsed2;
|
|
7269
7682
|
}
|
|
@@ -7412,7 +7825,7 @@ async function runWatch(options, deps) {
|
|
|
7412
7825
|
import { existsSync as existsSync6 } from "fs";
|
|
7413
7826
|
import { realpath as realpath4 } from "fs/promises";
|
|
7414
7827
|
import { createRequire } from "module";
|
|
7415
|
-
import { basename as basename6, dirname as dirname12, join as
|
|
7828
|
+
import { basename as basename6, dirname as dirname12, join as join20, relative as relative5, resolve as resolve4 } from "path";
|
|
7416
7829
|
import { fileURLToPath } from "url";
|
|
7417
7830
|
|
|
7418
7831
|
// src/dev-server-owner.ts
|
|
@@ -7498,8 +7911,8 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
7498
7911
|
|
|
7499
7912
|
// src/run.ts
|
|
7500
7913
|
function findShellDir() {
|
|
7501
|
-
const bundled =
|
|
7502
|
-
if (existsSync6(
|
|
7914
|
+
const bundled = join20(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
7915
|
+
if (existsSync6(join20(bundled, "index.html"))) return bundled;
|
|
7503
7916
|
try {
|
|
7504
7917
|
const require2 = createRequire(import.meta.url);
|
|
7505
7918
|
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
@@ -7513,7 +7926,7 @@ function shellWord(value) {
|
|
|
7513
7926
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7514
7927
|
}
|
|
7515
7928
|
function embeddedLeglasCommand() {
|
|
7516
|
-
const entry =
|
|
7929
|
+
const entry = join20(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
7517
7930
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
7518
7931
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
7519
7932
|
}
|
|
@@ -7545,7 +7958,7 @@ async function run3(options, deps) {
|
|
|
7545
7958
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
7546
7959
|
for (const preview of merged?.previews ?? []) {
|
|
7547
7960
|
if (preview.file !== void 0) {
|
|
7548
|
-
const absolute =
|
|
7961
|
+
const absolute = join20(options.cwd, preview.file);
|
|
7549
7962
|
if (!existsSync6(absolute)) {
|
|
7550
7963
|
previewErrors.push(
|
|
7551
7964
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -7667,6 +8080,7 @@ Usage
|
|
|
7667
8080
|
leglas classify Decide where a direction should live
|
|
7668
8081
|
leglas add --title T --url U Register a preview on this machine
|
|
7669
8082
|
leglas list Show every preview, shared and local
|
|
8083
|
+
leglas log [entry] What past explorations decided
|
|
7670
8084
|
leglas show <title> Everything Leglas knows about one direction
|
|
7671
8085
|
leglas requests Show change requests made from the interface
|
|
7672
8086
|
leglas watch --run "<cmd>" Hand each request to your agent as it arrives
|
|
@@ -7804,6 +8218,13 @@ if (parsed.kind === "watch") {
|
|
|
7804
8218
|
);
|
|
7805
8219
|
process.exit(outcome.exitCode);
|
|
7806
8220
|
}
|
|
8221
|
+
if (parsed.kind === "log") {
|
|
8222
|
+
const outcome = await runLog(
|
|
8223
|
+
{ entry: parsed.entry, json: parsed.json, cwd: process.cwd() },
|
|
8224
|
+
previewDeps
|
|
8225
|
+
);
|
|
8226
|
+
process.exit(outcome.exitCode);
|
|
8227
|
+
}
|
|
7807
8228
|
if (parsed.kind === "list") {
|
|
7808
8229
|
const outcome = await runList({ json: parsed.json, cwd: process.cwd() }, previewDeps);
|
|
7809
8230
|
process.exit(outcome.exitCode);
|