leglas 0.7.3 → 0.8.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 +9 -1
- package/dist/args.d.ts +4 -0
- package/dist/bin.js +550 -66
- package/dist/index.js +473 -47
- package/dist/run-log.d.ts +20 -0
- package/dist/shell/assets/index-CpYCCElH.css +1 -0
- package/dist/shell/assets/index-Ct9pp-D-.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-DT33xxfg.css +0 -1
- package/dist/shell/assets/index-DbONqSbP.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
|
}));
|
|
@@ -2745,6 +2921,141 @@ async function startAppProcess(options) {
|
|
|
2745
2921
|
throw new Error(`${options.label} did not start within ${Math.round(readyTimeoutMs / 1e3)}s. Check that its dev command serves the port it is given.`);
|
|
2746
2922
|
}
|
|
2747
2923
|
|
|
2924
|
+
// ../server/dist/branches.js
|
|
2925
|
+
var BRANCH_IDLE_MS = 10 * 60 * 1e3;
|
|
2926
|
+
var BRANCH_SWEEP_MS = 3e4;
|
|
2927
|
+
function publicBranchState(state) {
|
|
2928
|
+
if (state.status === "ready")
|
|
2929
|
+
return { status: "ready" };
|
|
2930
|
+
return state;
|
|
2931
|
+
}
|
|
2932
|
+
function createBranchRegistry(options) {
|
|
2933
|
+
const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
|
|
2934
|
+
const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
|
|
2935
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
2936
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
2937
|
+
const lastActivity = /* @__PURE__ */ new Map();
|
|
2938
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
2939
|
+
const boot = options.startWorktree ?? startWorktree;
|
|
2940
|
+
const proxy = options.startProxy ?? startProxyServer;
|
|
2941
|
+
let closed = false;
|
|
2942
|
+
let stopPromise = null;
|
|
2943
|
+
const transition = (title, state) => {
|
|
2944
|
+
const previous = states.get(title);
|
|
2945
|
+
if (previous?.status === "starting" && state.status === "starting" && previous.phase === state.phase) {
|
|
2946
|
+
return previous;
|
|
2947
|
+
}
|
|
2948
|
+
states.set(title, state);
|
|
2949
|
+
options.onChange?.(title, state);
|
|
2950
|
+
return state;
|
|
2951
|
+
};
|
|
2952
|
+
const stopReady = (title, state, toIdle) => {
|
|
2953
|
+
const current = stopping.get(title);
|
|
2954
|
+
if (current !== void 0)
|
|
2955
|
+
return current;
|
|
2956
|
+
const pending = (async () => {
|
|
2957
|
+
await proxies.get(title)?.close().catch(() => {
|
|
2958
|
+
});
|
|
2959
|
+
await state.worktree.stop().catch(() => {
|
|
2960
|
+
});
|
|
2961
|
+
proxies.delete(title);
|
|
2962
|
+
lastActivity.delete(title);
|
|
2963
|
+
if (toIdle && !closed && states.get(title) === state) {
|
|
2964
|
+
transition(title, { status: "idle" });
|
|
2965
|
+
}
|
|
2966
|
+
})().finally(() => {
|
|
2967
|
+
stopping.delete(title);
|
|
2968
|
+
});
|
|
2969
|
+
stopping.set(title, pending);
|
|
2970
|
+
return pending;
|
|
2971
|
+
};
|
|
2972
|
+
const sweep = () => {
|
|
2973
|
+
const now = Date.now();
|
|
2974
|
+
for (const [title, state] of states) {
|
|
2975
|
+
const branchProxy = proxies.get(title);
|
|
2976
|
+
if (state.status !== "ready" || branchProxy === void 0 || stopping.has(title))
|
|
2977
|
+
continue;
|
|
2978
|
+
if (branchProxy.active()) {
|
|
2979
|
+
lastActivity.set(title, now);
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
const seen = lastActivity.get(title) ?? now;
|
|
2983
|
+
if (now - seen >= BRANCH_IDLE_MS)
|
|
2984
|
+
void stopReady(title, state, true);
|
|
2985
|
+
}
|
|
2986
|
+
};
|
|
2987
|
+
const sweepTimer = setInterval(sweep, BRANCH_SWEEP_MS);
|
|
2988
|
+
sweepTimer.unref();
|
|
2989
|
+
const begin = (title) => {
|
|
2990
|
+
const preview = previews.get(title);
|
|
2991
|
+
const current = states.get(title);
|
|
2992
|
+
if (preview === void 0 || current === void 0 || closed)
|
|
2993
|
+
return void 0;
|
|
2994
|
+
if (current.status === "starting")
|
|
2995
|
+
return inflight.get(title);
|
|
2996
|
+
if (current.status === "ready")
|
|
2997
|
+
return Promise.resolve(current);
|
|
2998
|
+
transition(title, { status: "starting", phase: "checking out" });
|
|
2999
|
+
let checkout;
|
|
3000
|
+
try {
|
|
3001
|
+
checkout = Promise.resolve(boot({
|
|
3002
|
+
cwd: options.cwd,
|
|
3003
|
+
branch: preview.branch,
|
|
3004
|
+
installCommand: options.installCommand,
|
|
3005
|
+
devCommand: options.devCommand ?? "",
|
|
3006
|
+
onLog: (line) => {
|
|
3007
|
+
transition(title, {
|
|
3008
|
+
status: "starting",
|
|
3009
|
+
phase: line.startsWith("installing ") ? "installing" : "starting"
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}));
|
|
3013
|
+
} catch (error) {
|
|
3014
|
+
checkout = Promise.reject(error);
|
|
3015
|
+
}
|
|
3016
|
+
const starting = checkout.then(async (worktree) => {
|
|
3017
|
+
transition(title, { status: "starting", phase: "starting" });
|
|
3018
|
+
try {
|
|
3019
|
+
const branchProxy = await proxy({
|
|
3020
|
+
target: worktree.url,
|
|
3021
|
+
onActivity: () => lastActivity.set(title, Date.now())
|
|
3022
|
+
});
|
|
3023
|
+
proxies.set(title, branchProxy);
|
|
3024
|
+
lastActivity.set(title, Date.now());
|
|
3025
|
+
return transition(title, { status: "ready", worktree });
|
|
3026
|
+
} catch (error) {
|
|
3027
|
+
await worktree.stop().catch(() => {
|
|
3028
|
+
});
|
|
3029
|
+
throw error;
|
|
3030
|
+
}
|
|
3031
|
+
}).catch((error) => transition(title, {
|
|
3032
|
+
status: "failed",
|
|
3033
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3034
|
+
})).finally(() => {
|
|
3035
|
+
inflight.delete(title);
|
|
3036
|
+
});
|
|
3037
|
+
inflight.set(title, starting);
|
|
3038
|
+
return starting;
|
|
3039
|
+
};
|
|
3040
|
+
return {
|
|
3041
|
+
state: (title) => states.get(title),
|
|
3042
|
+
url: (title) => proxies.get(title)?.url,
|
|
3043
|
+
start: begin,
|
|
3044
|
+
stop: () => {
|
|
3045
|
+
if (stopPromise !== null)
|
|
3046
|
+
return stopPromise;
|
|
3047
|
+
closed = true;
|
|
3048
|
+
clearInterval(sweepTimer);
|
|
3049
|
+
stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
|
|
3050
|
+
await Promise.allSettled([...stopping.values()]);
|
|
3051
|
+
const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
|
|
3052
|
+
await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
|
|
3053
|
+
});
|
|
3054
|
+
return stopPromise;
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2748
3059
|
// ../server/dist/failure.js
|
|
2749
3060
|
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
2750
3061
|
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
@@ -4835,6 +5146,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
4835
5146
|
|
|
4836
5147
|
// ../server/dist/server.js
|
|
4837
5148
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5149
|
+
import { createHash as createHash2 } from "crypto";
|
|
4838
5150
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
4839
5151
|
import http2 from "http";
|
|
4840
5152
|
import net3 from "net";
|
|
@@ -4913,6 +5225,30 @@ function sendJson(res, status, body) {
|
|
|
4913
5225
|
});
|
|
4914
5226
|
res.end(payload);
|
|
4915
5227
|
}
|
|
5228
|
+
function etagMatches(value, etag) {
|
|
5229
|
+
if (value === void 0)
|
|
5230
|
+
return false;
|
|
5231
|
+
const values = Array.isArray(value) ? value : [value];
|
|
5232
|
+
return values.some((header) => header.split(",").some((candidate) => {
|
|
5233
|
+
const tag = candidate.trim();
|
|
5234
|
+
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
5235
|
+
}));
|
|
5236
|
+
}
|
|
5237
|
+
function sendConditionalJson(req, res, body) {
|
|
5238
|
+
const payload = JSON.stringify(body);
|
|
5239
|
+
const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
|
|
5240
|
+
if (etagMatches(req.headers["if-none-match"], etag)) {
|
|
5241
|
+
res.writeHead(304, { etag, "cache-control": "private, no-cache" });
|
|
5242
|
+
res.end();
|
|
5243
|
+
return;
|
|
5244
|
+
}
|
|
5245
|
+
res.writeHead(200, {
|
|
5246
|
+
"content-type": "application/json; charset=utf-8",
|
|
5247
|
+
"cache-control": "private, no-cache",
|
|
5248
|
+
etag
|
|
5249
|
+
});
|
|
5250
|
+
res.end(payload);
|
|
5251
|
+
}
|
|
4916
5252
|
var CAPTURE_DEADLINE_MS = 15e3;
|
|
4917
5253
|
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
4918
5254
|
function captureSlug(title) {
|
|
@@ -5292,6 +5628,33 @@ async function startServer(options) {
|
|
|
5292
5628
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5293
5629
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5294
5630
|
const live = options.live ?? createLiveHub();
|
|
5631
|
+
const branches = createBranchRegistry({
|
|
5632
|
+
cwd,
|
|
5633
|
+
previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
|
|
5634
|
+
installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
|
|
5635
|
+
devCommand: config?.devCommand,
|
|
5636
|
+
onChange: () => live.nudge("config"),
|
|
5637
|
+
...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
|
|
5638
|
+
});
|
|
5639
|
+
const previewForConfig = (preview) => {
|
|
5640
|
+
if (preview.branch === void 0)
|
|
5641
|
+
return preview;
|
|
5642
|
+
const state = branches.state(preview.title) ?? { status: "idle" };
|
|
5643
|
+
const { url: route, ...withoutUrl } = preview;
|
|
5644
|
+
const branchUrl = branches.url(preview.title);
|
|
5645
|
+
return state.status === "ready" ? {
|
|
5646
|
+
...withoutUrl,
|
|
5647
|
+
url: `${branchUrl ?? state.worktree.url}${route}`,
|
|
5648
|
+
state: publicBranchState(state)
|
|
5649
|
+
} : { ...withoutUrl, state: publicBranchState(state) };
|
|
5650
|
+
};
|
|
5651
|
+
const previewsForConfig = (previews) => previews.map(previewForConfig);
|
|
5652
|
+
const readyPreview = (preview) => {
|
|
5653
|
+
if (preview.branch === void 0)
|
|
5654
|
+
return preview;
|
|
5655
|
+
const state = branches.state(preview.title);
|
|
5656
|
+
return state?.status === "ready" ? { ...preview, url: `${branches.url(preview.title) ?? state.worktree.url}${preview.url}` } : null;
|
|
5657
|
+
};
|
|
5295
5658
|
if (options.pool === void 0) {
|
|
5296
5659
|
void reapOrphanedBrowsers().catch(() => {
|
|
5297
5660
|
});
|
|
@@ -5325,7 +5688,7 @@ async function startServer(options) {
|
|
|
5325
5688
|
}
|
|
5326
5689
|
return Promise.resolve(agentsCache.agents);
|
|
5327
5690
|
};
|
|
5328
|
-
const
|
|
5691
|
+
const livePreviewDefinitions = async () => {
|
|
5329
5692
|
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
5330
5693
|
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
5331
5694
|
const localTitles = new Set(local.map((entry) => entry.title));
|
|
@@ -5335,6 +5698,7 @@ async function startServer(options) {
|
|
|
5335
5698
|
const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
|
|
5336
5699
|
return [...boot, ...fresh];
|
|
5337
5700
|
};
|
|
5701
|
+
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
5338
5702
|
void probeAgents().catch(() => {
|
|
5339
5703
|
});
|
|
5340
5704
|
const server = http2.createServer((req, res) => {
|
|
@@ -5352,11 +5716,11 @@ async function startServer(options) {
|
|
|
5352
5716
|
errors.push(notice);
|
|
5353
5717
|
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
5354
5718
|
if (localErrors.length > 0) {
|
|
5355
|
-
return
|
|
5719
|
+
return sendConditionalJson(req, res, {
|
|
5356
5720
|
project,
|
|
5357
5721
|
devServer: target,
|
|
5358
5722
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5359
|
-
previews: boot,
|
|
5723
|
+
previews: previewsForConfig(boot),
|
|
5360
5724
|
errors,
|
|
5361
5725
|
warnings: configWarnings
|
|
5362
5726
|
});
|
|
@@ -5365,23 +5729,58 @@ async function startServer(options) {
|
|
|
5365
5729
|
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
5366
5730
|
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
5367
5731
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
5368
|
-
|
|
5732
|
+
sendConditionalJson(req, res, {
|
|
5369
5733
|
project,
|
|
5370
5734
|
devServer: target,
|
|
5371
5735
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5372
|
-
previews: [...currentBoot, ...fresh],
|
|
5736
|
+
previews: previewsForConfig([...currentBoot, ...fresh]),
|
|
5373
5737
|
errors,
|
|
5374
5738
|
warnings: configWarnings
|
|
5375
5739
|
});
|
|
5376
|
-
}).catch(() =>
|
|
5740
|
+
}).catch(() => sendConditionalJson(req, res, {
|
|
5377
5741
|
project,
|
|
5378
5742
|
devServer: target,
|
|
5379
5743
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5380
|
-
previews: boot,
|
|
5744
|
+
previews: previewsForConfig(boot),
|
|
5381
5745
|
errors,
|
|
5382
5746
|
warnings: configWarnings
|
|
5383
5747
|
}));
|
|
5384
5748
|
}
|
|
5749
|
+
if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
|
|
5750
|
+
let body = "";
|
|
5751
|
+
req.on("data", (chunk) => body += chunk);
|
|
5752
|
+
return void req.on("end", async () => {
|
|
5753
|
+
const parsed2 = jsonBody(body);
|
|
5754
|
+
if (parsed2 === null) {
|
|
5755
|
+
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5756
|
+
}
|
|
5757
|
+
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
5758
|
+
return sendJson(res, 400, { ok: false, error: "Body needs a direction title." });
|
|
5759
|
+
}
|
|
5760
|
+
const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed2.title);
|
|
5761
|
+
if (preview === void 0) {
|
|
5762
|
+
return sendJson(res, 404, { ok: false, error: "No such direction." });
|
|
5763
|
+
}
|
|
5764
|
+
if (preview.branch === void 0) {
|
|
5765
|
+
return sendJson(res, 400, {
|
|
5766
|
+
ok: false,
|
|
5767
|
+
error: `"${preview.title}" is not a branch preview.`
|
|
5768
|
+
});
|
|
5769
|
+
}
|
|
5770
|
+
if (config?.devCommand === void 0) {
|
|
5771
|
+
return sendJson(res, 400, {
|
|
5772
|
+
ok: false,
|
|
5773
|
+
error: `"${preview.title}" cannot start because the config sets no devCommand.`
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
void branches.start(preview.title);
|
|
5777
|
+
const state = branches.state(preview.title);
|
|
5778
|
+
if (state === void 0) {
|
|
5779
|
+
return sendJson(res, 404, { ok: false, error: "No such branch preview." });
|
|
5780
|
+
}
|
|
5781
|
+
return sendJson(res, 200, { ok: true, state: publicBranchState(state) });
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5385
5784
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
5386
5785
|
let body = "";
|
|
5387
5786
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -5773,7 +6172,7 @@ async function startServer(options) {
|
|
|
5773
6172
|
waiting: null,
|
|
5774
6173
|
failedIds: []
|
|
5775
6174
|
};
|
|
5776
|
-
return void readRequests(cwd).then((requests) =>
|
|
6175
|
+
return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
|
|
5777
6176
|
requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
|
|
5778
6177
|
id,
|
|
5779
6178
|
title,
|
|
@@ -5876,7 +6275,7 @@ async function startServer(options) {
|
|
|
5876
6275
|
});
|
|
5877
6276
|
}
|
|
5878
6277
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
5879
|
-
return void readAnnotations(cwd).then((annotations) =>
|
|
6278
|
+
return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
|
|
5880
6279
|
}
|
|
5881
6280
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
5882
6281
|
if (!hasJsonBody(req)) {
|
|
@@ -6002,7 +6401,7 @@ async function startServer(options) {
|
|
|
6002
6401
|
});
|
|
6003
6402
|
}
|
|
6004
6403
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
6005
|
-
return void probe(target).then((reachable) =>
|
|
6404
|
+
return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
|
|
6006
6405
|
}
|
|
6007
6406
|
if (path.startsWith(`${FILES_PREFIX}/`)) {
|
|
6008
6407
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
@@ -6076,7 +6475,12 @@ async function startServer(options) {
|
|
|
6076
6475
|
return closePromise;
|
|
6077
6476
|
liveFiles.close();
|
|
6078
6477
|
liveHealth.close();
|
|
6079
|
-
closePromise = Promise.all([
|
|
6478
|
+
closePromise = Promise.all([
|
|
6479
|
+
branches.stop(),
|
|
6480
|
+
runner.stop(),
|
|
6481
|
+
browserPool.close(),
|
|
6482
|
+
live.close()
|
|
6483
|
+
]).then(() => new Promise((done) => {
|
|
6080
6484
|
for (const socket of sockets)
|
|
6081
6485
|
socket.destroy();
|
|
6082
6486
|
sockets.clear();
|
|
@@ -6449,6 +6853,12 @@ out of the ignored directory, deletes the rest of the exploration, and drops
|
|
|
6449
6853
|
them from the rail. Then change their component to use the kept component
|
|
6450
6854
|
instead of the switcher.
|
|
6451
6855
|
|
|
6856
|
+
Keeping also writes what the exploration was into \`design-log/\`, which is
|
|
6857
|
+
committed. Before exploring a surface, read \`npx leglas log --json\` and any
|
|
6858
|
+
entry for that surface: it says what was already tried there, in the user's own
|
|
6859
|
+
words, and which direction won. Proposing something that was already rejected
|
|
6860
|
+
wastes their time, and the record is there so you do not have to ask.
|
|
6861
|
+
|
|
6452
6862
|
Useful to know:
|
|
6453
6863
|
|
|
6454
6864
|
- \`.leglas/\` is gitignored. Exploration is disposable and nothing in there
|
|
@@ -6548,7 +6958,7 @@ async function runInit(options, deps) {
|
|
|
6548
6958
|
|
|
6549
6959
|
// src/run-keep.ts
|
|
6550
6960
|
import { existsSync as existsSync4 } from "fs";
|
|
6551
|
-
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6961
|
+
import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6552
6962
|
import { dirname as dirname9, join as join15 } from "path";
|
|
6553
6963
|
|
|
6554
6964
|
// src/keep.ts
|
|
@@ -6631,6 +7041,29 @@ function renameExport(source, to) {
|
|
|
6631
7041
|
to
|
|
6632
7042
|
);
|
|
6633
7043
|
}
|
|
7044
|
+
async function writeLogEntry(options) {
|
|
7045
|
+
const entry = composeEntry({
|
|
7046
|
+
surface: options.surface,
|
|
7047
|
+
won: options.won,
|
|
7048
|
+
previews: options.previews,
|
|
7049
|
+
requests: await readRequests(options.cwd),
|
|
7050
|
+
annotations: await readAnnotations(options.cwd),
|
|
7051
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
7052
|
+
});
|
|
7053
|
+
const dir = join15(options.cwd, options.logDir);
|
|
7054
|
+
await mkdir9(dir, { recursive: true });
|
|
7055
|
+
const file = join15(dir, `${entry.slug}.md`);
|
|
7056
|
+
await writeFile10(file, entry.markdown, "utf8");
|
|
7057
|
+
if (entry.pictures.length > 0) {
|
|
7058
|
+
const pictureDir = join15(dir, entry.slug);
|
|
7059
|
+
await mkdir9(pictureDir, { recursive: true });
|
|
7060
|
+
for (const picture of entry.pictures) {
|
|
7061
|
+
await copyFile2(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
|
|
7062
|
+
});
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
7065
|
+
return `${options.logDir}/${entry.slug}.md`;
|
|
7066
|
+
}
|
|
6634
7067
|
async function runKeep(options, deps) {
|
|
6635
7068
|
const loaded = await loadConfig(options.cwd);
|
|
6636
7069
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -6659,6 +7092,20 @@ async function runKeep(options, deps) {
|
|
|
6659
7092
|
const source = await readFile13(from, "utf8");
|
|
6660
7093
|
await mkdir9(dirname9(to), { recursive: true });
|
|
6661
7094
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
7095
|
+
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
7096
|
+
let logged = null;
|
|
7097
|
+
let logError = null;
|
|
7098
|
+
try {
|
|
7099
|
+
logged = await writeLogEntry({
|
|
7100
|
+
cwd: options.cwd,
|
|
7101
|
+
logDir: loaded.config?.logDir ?? DEFAULT_LOG_DIR,
|
|
7102
|
+
surface,
|
|
7103
|
+
won: { title: resolved.title, to: plan.move.to },
|
|
7104
|
+
previews: previews.filter((preview) => plan.dropTitles.includes(preview.title))
|
|
7105
|
+
});
|
|
7106
|
+
} catch (error) {
|
|
7107
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
7108
|
+
}
|
|
6662
7109
|
await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6663
7110
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
6664
7111
|
if (options.json) {
|
|
@@ -6670,6 +7117,8 @@ async function runKeep(options, deps) {
|
|
|
6670
7117
|
exportName: plan.exportName,
|
|
6671
7118
|
removed: plan.removeDir,
|
|
6672
7119
|
droppedPreviews: dropped,
|
|
7120
|
+
logged,
|
|
7121
|
+
logError,
|
|
6673
7122
|
instructions: plan.instructions
|
|
6674
7123
|
})
|
|
6675
7124
|
);
|
|
@@ -6677,6 +7126,8 @@ async function runKeep(options, deps) {
|
|
|
6677
7126
|
}
|
|
6678
7127
|
deps.log(` kept ${plan.move.to}`);
|
|
6679
7128
|
deps.log(` removed ${plan.removeDir}`);
|
|
7129
|
+
if (logged !== null) deps.log(` logged ${logged}`);
|
|
7130
|
+
if (logError !== null) deps.error(` The decision log could not be written: ${logError}`);
|
|
6680
7131
|
if (dropped > 0) {
|
|
6681
7132
|
deps.log(` dropped ${dropped} direction${dropped === 1 ? "" : "s"} from the rail`);
|
|
6682
7133
|
}
|
|
@@ -6773,17 +7224,67 @@ async function runNew(options, deps) {
|
|
|
6773
7224
|
return { exitCode: 0, written };
|
|
6774
7225
|
}
|
|
6775
7226
|
|
|
6776
|
-
// src/run-
|
|
6777
|
-
import { readFile as readFile15,
|
|
7227
|
+
// src/run-log.ts
|
|
7228
|
+
import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
|
|
6778
7229
|
import { join as join17 } from "path";
|
|
7230
|
+
function headline(markdown) {
|
|
7231
|
+
const first = markdown.split("\n", 1)[0] ?? "";
|
|
7232
|
+
return first.replace(/^#\s*/, "").trim();
|
|
7233
|
+
}
|
|
7234
|
+
async function runLog(options, deps) {
|
|
7235
|
+
const loaded = await loadConfig(options.cwd);
|
|
7236
|
+
const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
|
|
7237
|
+
let names;
|
|
7238
|
+
try {
|
|
7239
|
+
names = (await readdir4(join17(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
|
|
7240
|
+
} catch {
|
|
7241
|
+
names = [];
|
|
7242
|
+
}
|
|
7243
|
+
if (options.entry !== null) {
|
|
7244
|
+
const wanted = options.entry.replace(/\.md$/, "");
|
|
7245
|
+
const found = names.find((name) => name === `${wanted}.md`);
|
|
7246
|
+
if (found === void 0) {
|
|
7247
|
+
const error = `No entry called ${JSON.stringify(options.entry)} in ${dir}.`;
|
|
7248
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
7249
|
+
else deps.error(error);
|
|
7250
|
+
return { exitCode: 1 };
|
|
7251
|
+
}
|
|
7252
|
+
const markdown = await readFile15(join17(options.cwd, dir, found), "utf8");
|
|
7253
|
+
if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
|
|
7254
|
+
else deps.log(markdown.trimEnd());
|
|
7255
|
+
return { exitCode: 0 };
|
|
7256
|
+
}
|
|
7257
|
+
const entries = await Promise.all(
|
|
7258
|
+
names.map(async (name) => ({
|
|
7259
|
+
entry: name.replace(/\.md$/, ""),
|
|
7260
|
+
title: headline(await readFile15(join17(options.cwd, dir, name), "utf8")),
|
|
7261
|
+
file: `${dir}/${name}`
|
|
7262
|
+
}))
|
|
7263
|
+
);
|
|
7264
|
+
if (options.json) {
|
|
7265
|
+
deps.log(JSON.stringify({ ok: true, dir, entries }));
|
|
7266
|
+
return { exitCode: 0 };
|
|
7267
|
+
}
|
|
7268
|
+
if (entries.length === 0) {
|
|
7269
|
+
deps.log(` No decisions recorded yet. One is written each time you run leglas keep.`);
|
|
7270
|
+
return { exitCode: 0 };
|
|
7271
|
+
}
|
|
7272
|
+
const width = Math.max(...entries.map((entry) => entry.entry.length));
|
|
7273
|
+
for (const entry of entries) deps.log(` ${entry.entry.padEnd(width)} ${entry.title}`);
|
|
7274
|
+
return { exitCode: 0 };
|
|
7275
|
+
}
|
|
7276
|
+
|
|
7277
|
+
// src/run-previews.ts
|
|
7278
|
+
import { readFile as readFile16, writeFile as writeFile12 } from "fs/promises";
|
|
7279
|
+
import { join as join18 } from "path";
|
|
6779
7280
|
function envelope(deps, ok, body) {
|
|
6780
7281
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
6781
7282
|
}
|
|
6782
7283
|
async function ensureIgnored(cwd) {
|
|
6783
|
-
const path =
|
|
7284
|
+
const path = join18(cwd, ".gitignore");
|
|
6784
7285
|
let current = null;
|
|
6785
7286
|
try {
|
|
6786
|
-
current = await
|
|
7287
|
+
current = await readFile16(path, "utf8");
|
|
6787
7288
|
} catch {
|
|
6788
7289
|
current = null;
|
|
6789
7290
|
}
|
|
@@ -7110,15 +7611,15 @@ async function runShow(options, deps) {
|
|
|
7110
7611
|
|
|
7111
7612
|
// src/run-watch.ts
|
|
7112
7613
|
import { spawn as spawn3 } from "child_process";
|
|
7113
|
-
import { mkdir as mkdir11, readFile as
|
|
7114
|
-
import { dirname as dirname11, join as
|
|
7614
|
+
import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile13 } from "fs/promises";
|
|
7615
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
7115
7616
|
var POLL_MS2 = 2e3;
|
|
7116
7617
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
7117
7618
|
async function saveTemplate(cwd, run4) {
|
|
7118
|
-
const path =
|
|
7619
|
+
const path = join19(cwd, WATCH_PATH);
|
|
7119
7620
|
let config = {};
|
|
7120
7621
|
try {
|
|
7121
|
-
const parsed2 = JSON.parse(await
|
|
7622
|
+
const parsed2 = JSON.parse(await readFile17(path, "utf8"));
|
|
7122
7623
|
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
7123
7624
|
config = parsed2;
|
|
7124
7625
|
}
|
|
@@ -7267,7 +7768,7 @@ async function runWatch(options, deps) {
|
|
|
7267
7768
|
import { existsSync as existsSync6 } from "fs";
|
|
7268
7769
|
import { realpath as realpath4 } from "fs/promises";
|
|
7269
7770
|
import { createRequire } from "module";
|
|
7270
|
-
import { basename as basename6, dirname as dirname12, join as
|
|
7771
|
+
import { basename as basename6, dirname as dirname12, join as join20, relative as relative5, resolve as resolve4 } from "path";
|
|
7271
7772
|
import { fileURLToPath } from "url";
|
|
7272
7773
|
|
|
7273
7774
|
// src/dev-server-owner.ts
|
|
@@ -7353,8 +7854,8 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
7353
7854
|
|
|
7354
7855
|
// src/run.ts
|
|
7355
7856
|
function findShellDir() {
|
|
7356
|
-
const bundled =
|
|
7357
|
-
if (existsSync6(
|
|
7857
|
+
const bundled = join20(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
7858
|
+
if (existsSync6(join20(bundled, "index.html"))) return bundled;
|
|
7358
7859
|
try {
|
|
7359
7860
|
const require2 = createRequire(import.meta.url);
|
|
7360
7861
|
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
@@ -7368,7 +7869,7 @@ function shellWord(value) {
|
|
|
7368
7869
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7369
7870
|
}
|
|
7370
7871
|
function embeddedLeglasCommand() {
|
|
7371
|
-
const entry =
|
|
7872
|
+
const entry = join20(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
7372
7873
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
7373
7874
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
7374
7875
|
}
|
|
@@ -7377,8 +7878,7 @@ async function run3(options, deps) {
|
|
|
7377
7878
|
const local = await readLocalPreviews(options.cwd);
|
|
7378
7879
|
let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
|
|
7379
7880
|
const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
|
|
7380
|
-
const
|
|
7381
|
-
const worktreeErrors = [];
|
|
7881
|
+
const previewErrors = [];
|
|
7382
7882
|
const previews = [];
|
|
7383
7883
|
let app = null;
|
|
7384
7884
|
const needsApp = (merged?.previews ?? []).some(
|
|
@@ -7395,15 +7895,15 @@ async function run3(options, deps) {
|
|
|
7395
7895
|
devServer = app.url;
|
|
7396
7896
|
merged.devServer = app.url;
|
|
7397
7897
|
} catch (error) {
|
|
7398
|
-
|
|
7898
|
+
previewErrors.push(error instanceof Error ? error.message : String(error));
|
|
7399
7899
|
}
|
|
7400
7900
|
}
|
|
7401
7901
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
7402
7902
|
for (const preview of merged?.previews ?? []) {
|
|
7403
7903
|
if (preview.file !== void 0) {
|
|
7404
|
-
const absolute =
|
|
7904
|
+
const absolute = join20(options.cwd, preview.file);
|
|
7405
7905
|
if (!existsSync6(absolute)) {
|
|
7406
|
-
|
|
7906
|
+
previewErrors.push(
|
|
7407
7907
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
7408
7908
|
);
|
|
7409
7909
|
continue;
|
|
@@ -7419,29 +7919,7 @@ async function run3(options, deps) {
|
|
|
7419
7919
|
});
|
|
7420
7920
|
continue;
|
|
7421
7921
|
}
|
|
7422
|
-
|
|
7423
|
-
previews.push(preview);
|
|
7424
|
-
continue;
|
|
7425
|
-
}
|
|
7426
|
-
if (merged?.devCommand === void 0) {
|
|
7427
|
-
worktreeErrors.push(
|
|
7428
|
-
`"${preview.title}" names branch ${preview.branch}, but the config sets no devCommand, so Leglas cannot start that checkout. Add devCommand (with {port}) to the config.`
|
|
7429
|
-
);
|
|
7430
|
-
continue;
|
|
7431
|
-
}
|
|
7432
|
-
if (!options.json) deps.log(` starting ${preview.branch}\u2026`);
|
|
7433
|
-
try {
|
|
7434
|
-
const worktree = await startWorktree({
|
|
7435
|
-
cwd: options.cwd,
|
|
7436
|
-
branch: preview.branch,
|
|
7437
|
-
installCommand: merged.installCommand,
|
|
7438
|
-
devCommand: merged.devCommand
|
|
7439
|
-
});
|
|
7440
|
-
worktrees.push(worktree);
|
|
7441
|
-
previews.push({ ...preview, url: `${worktree.url}${preview.url}` });
|
|
7442
|
-
} catch (error) {
|
|
7443
|
-
worktreeErrors.push(error instanceof Error ? error.message : String(error));
|
|
7444
|
-
}
|
|
7922
|
+
previews.push(preview);
|
|
7445
7923
|
}
|
|
7446
7924
|
const config = merged === null ? null : { ...merged, previews };
|
|
7447
7925
|
const configWarnings = [];
|
|
@@ -7451,7 +7929,7 @@ async function run3(options, deps) {
|
|
|
7451
7929
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7452
7930
|
const serverPromise = startServer({
|
|
7453
7931
|
config,
|
|
7454
|
-
configErrors: [...loaded.errors, ...local.errors, ...
|
|
7932
|
+
configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
|
|
7455
7933
|
configWarnings,
|
|
7456
7934
|
fileMounts,
|
|
7457
7935
|
shellDir: findShellDir(),
|
|
@@ -7490,9 +7968,9 @@ async function run3(options, deps) {
|
|
|
7490
7968
|
);
|
|
7491
7969
|
deps.log(`config ${configLabel}`);
|
|
7492
7970
|
deps.log(` ${previewCount} preview${previewCount === 1 ? "" : "s"}`);
|
|
7493
|
-
if (loaded.errors.length +
|
|
7971
|
+
if (loaded.errors.length + previewErrors.length > 0) {
|
|
7494
7972
|
deps.log("");
|
|
7495
|
-
for (const error of [...loaded.errors, ...
|
|
7973
|
+
for (const error of [...loaded.errors, ...previewErrors]) deps.log(` ! ${error}`);
|
|
7496
7974
|
deps.log(" Fix the config and reload; Leglas will pick it up on restart.");
|
|
7497
7975
|
}
|
|
7498
7976
|
if (configWarnings.length > 0) {
|
|
@@ -7515,8 +7993,6 @@ async function run3(options, deps) {
|
|
|
7515
7993
|
devServer,
|
|
7516
7994
|
previewCount,
|
|
7517
7995
|
stop: async () => {
|
|
7518
|
-
await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
|
|
7519
|
-
})));
|
|
7520
7996
|
await app?.stop().catch(() => {
|
|
7521
7997
|
});
|
|
7522
7998
|
await server.close();
|
|
@@ -7547,6 +8023,7 @@ Usage
|
|
|
7547
8023
|
leglas classify Decide where a direction should live
|
|
7548
8024
|
leglas add --title T --url U Register a preview on this machine
|
|
7549
8025
|
leglas list Show every preview, shared and local
|
|
8026
|
+
leglas log [entry] What past explorations decided
|
|
7550
8027
|
leglas show <title> Everything Leglas knows about one direction
|
|
7551
8028
|
leglas requests Show change requests made from the interface
|
|
7552
8029
|
leglas watch --run "<cmd>" Hand each request to your agent as it arrives
|
|
@@ -7684,6 +8161,13 @@ if (parsed.kind === "watch") {
|
|
|
7684
8161
|
);
|
|
7685
8162
|
process.exit(outcome.exitCode);
|
|
7686
8163
|
}
|
|
8164
|
+
if (parsed.kind === "log") {
|
|
8165
|
+
const outcome = await runLog(
|
|
8166
|
+
{ entry: parsed.entry, json: parsed.json, cwd: process.cwd() },
|
|
8167
|
+
previewDeps
|
|
8168
|
+
);
|
|
8169
|
+
process.exit(outcome.exitCode);
|
|
8170
|
+
}
|
|
7687
8171
|
if (parsed.kind === "list") {
|
|
7688
8172
|
const outcome = await runList({ json: parsed.json, cwd: process.cwd() }, previewDeps);
|
|
7689
8173
|
process.exit(outcome.exitCode);
|