leglas 0.7.4 → 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 +400 -36
- package/dist/index.js +323 -17
- package/dist/run-log.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,7 +196,14 @@ edit the hero. The supporting commands:
|
|
|
196
196
|
a direction from the rail hands over a block that ends in this command, so
|
|
197
197
|
an agent given the block can go and get the rest.
|
|
198
198
|
- `npx leglas keep "Aurora" --to src/components/hero.tsx` moves the winner
|
|
199
|
-
into real source and ends the exploration.
|
|
199
|
+
into real source and ends the exploration. It also writes down what the
|
|
200
|
+
exploration was, into `design-log/`: every direction with its note, the words
|
|
201
|
+
you typed at each of them, the captures the agent was sent, and which one
|
|
202
|
+
won. Plain markdown and PNGs, committed, so a pull request can link it and
|
|
203
|
+
somebody can read it in three months without this tool. Exploring is
|
|
204
|
+
episodic, and the archive is what makes coming back to a surface cheaper than
|
|
205
|
+
starting over. `npx leglas log` lists what is there. Set `logDir` if you want
|
|
206
|
+
it somewhere else.
|
|
200
207
|
|
|
201
208
|
Asking for a change works from the interface too. Type what you want
|
|
202
209
|
changed into the field under the rail (or press `R`) and Leglas composes a
|
|
@@ -388,6 +395,7 @@ Usage
|
|
|
388
395
|
leglas classify Decide where a direction should live
|
|
389
396
|
leglas add --title T --url U Register a preview on this machine
|
|
390
397
|
leglas list Show every preview, shared and local
|
|
398
|
+
leglas log [entry] What past explorations decided
|
|
391
399
|
leglas show <title> Everything Leglas knows about one direction
|
|
392
400
|
leglas requests Show change requests made from the interface
|
|
393
401
|
leglas watch --run "<cmd>" Hand each request to your agent as it arrives
|
package/dist/args.d.ts
CHANGED
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
|
}));
|
|
@@ -2746,6 +2922,8 @@ async function startAppProcess(options) {
|
|
|
2746
2922
|
}
|
|
2747
2923
|
|
|
2748
2924
|
// ../server/dist/branches.js
|
|
2925
|
+
var BRANCH_IDLE_MS = 10 * 60 * 1e3;
|
|
2926
|
+
var BRANCH_SWEEP_MS = 3e4;
|
|
2749
2927
|
function publicBranchState(state) {
|
|
2750
2928
|
if (state.status === "ready")
|
|
2751
2929
|
return { status: "ready" };
|
|
@@ -2755,7 +2933,11 @@ function createBranchRegistry(options) {
|
|
|
2755
2933
|
const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
|
|
2756
2934
|
const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
|
|
2757
2935
|
const inflight = /* @__PURE__ */ new Map();
|
|
2936
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
2937
|
+
const lastActivity = /* @__PURE__ */ new Map();
|
|
2938
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
2758
2939
|
const boot = options.startWorktree ?? startWorktree;
|
|
2940
|
+
const proxy = options.startProxy ?? startProxyServer;
|
|
2759
2941
|
let closed = false;
|
|
2760
2942
|
let stopPromise = null;
|
|
2761
2943
|
const transition = (title, state) => {
|
|
@@ -2767,6 +2949,43 @@ function createBranchRegistry(options) {
|
|
|
2767
2949
|
options.onChange?.(title, state);
|
|
2768
2950
|
return state;
|
|
2769
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();
|
|
2770
2989
|
const begin = (title) => {
|
|
2771
2990
|
const preview = previews.get(title);
|
|
2772
2991
|
const current = states.get(title);
|
|
@@ -2794,9 +3013,21 @@ function createBranchRegistry(options) {
|
|
|
2794
3013
|
} catch (error) {
|
|
2795
3014
|
checkout = Promise.reject(error);
|
|
2796
3015
|
}
|
|
2797
|
-
const starting = checkout.then((worktree) => {
|
|
3016
|
+
const starting = checkout.then(async (worktree) => {
|
|
2798
3017
|
transition(title, { status: "starting", phase: "starting" });
|
|
2799
|
-
|
|
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
|
+
}
|
|
2800
3031
|
}).catch((error) => transition(title, {
|
|
2801
3032
|
status: "failed",
|
|
2802
3033
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -2808,15 +3039,17 @@ function createBranchRegistry(options) {
|
|
|
2808
3039
|
};
|
|
2809
3040
|
return {
|
|
2810
3041
|
state: (title) => states.get(title),
|
|
3042
|
+
url: (title) => proxies.get(title)?.url,
|
|
2811
3043
|
start: begin,
|
|
2812
3044
|
stop: () => {
|
|
2813
3045
|
if (stopPromise !== null)
|
|
2814
3046
|
return stopPromise;
|
|
2815
3047
|
closed = true;
|
|
3048
|
+
clearInterval(sweepTimer);
|
|
2816
3049
|
stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
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)));
|
|
2820
3053
|
});
|
|
2821
3054
|
return stopPromise;
|
|
2822
3055
|
}
|
|
@@ -4913,6 +5146,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
4913
5146
|
|
|
4914
5147
|
// ../server/dist/server.js
|
|
4915
5148
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5149
|
+
import { createHash as createHash2 } from "crypto";
|
|
4916
5150
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
4917
5151
|
import http2 from "http";
|
|
4918
5152
|
import net3 from "net";
|
|
@@ -4991,6 +5225,30 @@ function sendJson(res, status, body) {
|
|
|
4991
5225
|
});
|
|
4992
5226
|
res.end(payload);
|
|
4993
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
|
+
}
|
|
4994
5252
|
var CAPTURE_DEADLINE_MS = 15e3;
|
|
4995
5253
|
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
4996
5254
|
function captureSlug(title) {
|
|
@@ -5383,9 +5641,10 @@ async function startServer(options) {
|
|
|
5383
5641
|
return preview;
|
|
5384
5642
|
const state = branches.state(preview.title) ?? { status: "idle" };
|
|
5385
5643
|
const { url: route, ...withoutUrl } = preview;
|
|
5644
|
+
const branchUrl = branches.url(preview.title);
|
|
5386
5645
|
return state.status === "ready" ? {
|
|
5387
5646
|
...withoutUrl,
|
|
5388
|
-
url: `${state.worktree.url}${route}`,
|
|
5647
|
+
url: `${branchUrl ?? state.worktree.url}${route}`,
|
|
5389
5648
|
state: publicBranchState(state)
|
|
5390
5649
|
} : { ...withoutUrl, state: publicBranchState(state) };
|
|
5391
5650
|
};
|
|
@@ -5394,7 +5653,7 @@ async function startServer(options) {
|
|
|
5394
5653
|
if (preview.branch === void 0)
|
|
5395
5654
|
return preview;
|
|
5396
5655
|
const state = branches.state(preview.title);
|
|
5397
|
-
return state?.status === "ready" ? { ...preview, url: `${state.worktree.url}${preview.url}` } : null;
|
|
5656
|
+
return state?.status === "ready" ? { ...preview, url: `${branches.url(preview.title) ?? state.worktree.url}${preview.url}` } : null;
|
|
5398
5657
|
};
|
|
5399
5658
|
if (options.pool === void 0) {
|
|
5400
5659
|
void reapOrphanedBrowsers().catch(() => {
|
|
@@ -5457,7 +5716,7 @@ async function startServer(options) {
|
|
|
5457
5716
|
errors.push(notice);
|
|
5458
5717
|
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
5459
5718
|
if (localErrors.length > 0) {
|
|
5460
|
-
return
|
|
5719
|
+
return sendConditionalJson(req, res, {
|
|
5461
5720
|
project,
|
|
5462
5721
|
devServer: target,
|
|
5463
5722
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5470,7 +5729,7 @@ async function startServer(options) {
|
|
|
5470
5729
|
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
5471
5730
|
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
5472
5731
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
5473
|
-
|
|
5732
|
+
sendConditionalJson(req, res, {
|
|
5474
5733
|
project,
|
|
5475
5734
|
devServer: target,
|
|
5476
5735
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5478,7 +5737,7 @@ async function startServer(options) {
|
|
|
5478
5737
|
errors,
|
|
5479
5738
|
warnings: configWarnings
|
|
5480
5739
|
});
|
|
5481
|
-
}).catch(() =>
|
|
5740
|
+
}).catch(() => sendConditionalJson(req, res, {
|
|
5482
5741
|
project,
|
|
5483
5742
|
devServer: target,
|
|
5484
5743
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5913,7 +6172,7 @@ async function startServer(options) {
|
|
|
5913
6172
|
waiting: null,
|
|
5914
6173
|
failedIds: []
|
|
5915
6174
|
};
|
|
5916
|
-
return void readRequests(cwd).then((requests) =>
|
|
6175
|
+
return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
|
|
5917
6176
|
requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
|
|
5918
6177
|
id,
|
|
5919
6178
|
title,
|
|
@@ -6016,7 +6275,7 @@ async function startServer(options) {
|
|
|
6016
6275
|
});
|
|
6017
6276
|
}
|
|
6018
6277
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
6019
|
-
return void readAnnotations(cwd).then((annotations) =>
|
|
6278
|
+
return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
|
|
6020
6279
|
}
|
|
6021
6280
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6022
6281
|
if (!hasJsonBody(req)) {
|
|
@@ -6142,7 +6401,7 @@ async function startServer(options) {
|
|
|
6142
6401
|
});
|
|
6143
6402
|
}
|
|
6144
6403
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
6145
|
-
return void probe(target).then((reachable) =>
|
|
6404
|
+
return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
|
|
6146
6405
|
}
|
|
6147
6406
|
if (path.startsWith(`${FILES_PREFIX}/`)) {
|
|
6148
6407
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
@@ -6594,6 +6853,12 @@ out of the ignored directory, deletes the rest of the exploration, and drops
|
|
|
6594
6853
|
them from the rail. Then change their component to use the kept component
|
|
6595
6854
|
instead of the switcher.
|
|
6596
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
|
+
|
|
6597
6862
|
Useful to know:
|
|
6598
6863
|
|
|
6599
6864
|
- \`.leglas/\` is gitignored. Exploration is disposable and nothing in there
|
|
@@ -6693,7 +6958,7 @@ async function runInit(options, deps) {
|
|
|
6693
6958
|
|
|
6694
6959
|
// src/run-keep.ts
|
|
6695
6960
|
import { existsSync as existsSync4 } from "fs";
|
|
6696
|
-
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";
|
|
6697
6962
|
import { dirname as dirname9, join as join15 } from "path";
|
|
6698
6963
|
|
|
6699
6964
|
// src/keep.ts
|
|
@@ -6776,6 +7041,29 @@ function renameExport(source, to) {
|
|
|
6776
7041
|
to
|
|
6777
7042
|
);
|
|
6778
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
|
+
}
|
|
6779
7067
|
async function runKeep(options, deps) {
|
|
6780
7068
|
const loaded = await loadConfig(options.cwd);
|
|
6781
7069
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -6804,6 +7092,20 @@ async function runKeep(options, deps) {
|
|
|
6804
7092
|
const source = await readFile13(from, "utf8");
|
|
6805
7093
|
await mkdir9(dirname9(to), { recursive: true });
|
|
6806
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
|
+
}
|
|
6807
7109
|
await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6808
7110
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
6809
7111
|
if (options.json) {
|
|
@@ -6815,6 +7117,8 @@ async function runKeep(options, deps) {
|
|
|
6815
7117
|
exportName: plan.exportName,
|
|
6816
7118
|
removed: plan.removeDir,
|
|
6817
7119
|
droppedPreviews: dropped,
|
|
7120
|
+
logged,
|
|
7121
|
+
logError,
|
|
6818
7122
|
instructions: plan.instructions
|
|
6819
7123
|
})
|
|
6820
7124
|
);
|
|
@@ -6822,6 +7126,8 @@ async function runKeep(options, deps) {
|
|
|
6822
7126
|
}
|
|
6823
7127
|
deps.log(` kept ${plan.move.to}`);
|
|
6824
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}`);
|
|
6825
7131
|
if (dropped > 0) {
|
|
6826
7132
|
deps.log(` dropped ${dropped} direction${dropped === 1 ? "" : "s"} from the rail`);
|
|
6827
7133
|
}
|
|
@@ -6918,17 +7224,67 @@ async function runNew(options, deps) {
|
|
|
6918
7224
|
return { exitCode: 0, written };
|
|
6919
7225
|
}
|
|
6920
7226
|
|
|
6921
|
-
// src/run-
|
|
6922
|
-
import { readFile as readFile15,
|
|
7227
|
+
// src/run-log.ts
|
|
7228
|
+
import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
|
|
6923
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";
|
|
6924
7280
|
function envelope(deps, ok, body) {
|
|
6925
7281
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
6926
7282
|
}
|
|
6927
7283
|
async function ensureIgnored(cwd) {
|
|
6928
|
-
const path =
|
|
7284
|
+
const path = join18(cwd, ".gitignore");
|
|
6929
7285
|
let current = null;
|
|
6930
7286
|
try {
|
|
6931
|
-
current = await
|
|
7287
|
+
current = await readFile16(path, "utf8");
|
|
6932
7288
|
} catch {
|
|
6933
7289
|
current = null;
|
|
6934
7290
|
}
|
|
@@ -7255,15 +7611,15 @@ async function runShow(options, deps) {
|
|
|
7255
7611
|
|
|
7256
7612
|
// src/run-watch.ts
|
|
7257
7613
|
import { spawn as spawn3 } from "child_process";
|
|
7258
|
-
import { mkdir as mkdir11, readFile as
|
|
7259
|
-
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";
|
|
7260
7616
|
var POLL_MS2 = 2e3;
|
|
7261
7617
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
7262
7618
|
async function saveTemplate(cwd, run4) {
|
|
7263
|
-
const path =
|
|
7619
|
+
const path = join19(cwd, WATCH_PATH);
|
|
7264
7620
|
let config = {};
|
|
7265
7621
|
try {
|
|
7266
|
-
const parsed2 = JSON.parse(await
|
|
7622
|
+
const parsed2 = JSON.parse(await readFile17(path, "utf8"));
|
|
7267
7623
|
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
7268
7624
|
config = parsed2;
|
|
7269
7625
|
}
|
|
@@ -7412,7 +7768,7 @@ async function runWatch(options, deps) {
|
|
|
7412
7768
|
import { existsSync as existsSync6 } from "fs";
|
|
7413
7769
|
import { realpath as realpath4 } from "fs/promises";
|
|
7414
7770
|
import { createRequire } from "module";
|
|
7415
|
-
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";
|
|
7416
7772
|
import { fileURLToPath } from "url";
|
|
7417
7773
|
|
|
7418
7774
|
// src/dev-server-owner.ts
|
|
@@ -7498,8 +7854,8 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
7498
7854
|
|
|
7499
7855
|
// src/run.ts
|
|
7500
7856
|
function findShellDir() {
|
|
7501
|
-
const bundled =
|
|
7502
|
-
if (existsSync6(
|
|
7857
|
+
const bundled = join20(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
7858
|
+
if (existsSync6(join20(bundled, "index.html"))) return bundled;
|
|
7503
7859
|
try {
|
|
7504
7860
|
const require2 = createRequire(import.meta.url);
|
|
7505
7861
|
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
@@ -7513,7 +7869,7 @@ function shellWord(value) {
|
|
|
7513
7869
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7514
7870
|
}
|
|
7515
7871
|
function embeddedLeglasCommand() {
|
|
7516
|
-
const entry =
|
|
7872
|
+
const entry = join20(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
7517
7873
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
7518
7874
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
7519
7875
|
}
|
|
@@ -7545,7 +7901,7 @@ async function run3(options, deps) {
|
|
|
7545
7901
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
7546
7902
|
for (const preview of merged?.previews ?? []) {
|
|
7547
7903
|
if (preview.file !== void 0) {
|
|
7548
|
-
const absolute =
|
|
7904
|
+
const absolute = join20(options.cwd, preview.file);
|
|
7549
7905
|
if (!existsSync6(absolute)) {
|
|
7550
7906
|
previewErrors.push(
|
|
7551
7907
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -7667,6 +8023,7 @@ Usage
|
|
|
7667
8023
|
leglas classify Decide where a direction should live
|
|
7668
8024
|
leglas add --title T --url U Register a preview on this machine
|
|
7669
8025
|
leglas list Show every preview, shared and local
|
|
8026
|
+
leglas log [entry] What past explorations decided
|
|
7670
8027
|
leglas show <title> Everything Leglas knows about one direction
|
|
7671
8028
|
leglas requests Show change requests made from the interface
|
|
7672
8029
|
leglas watch --run "<cmd>" Hand each request to your agent as it arrives
|
|
@@ -7804,6 +8161,13 @@ if (parsed.kind === "watch") {
|
|
|
7804
8161
|
);
|
|
7805
8162
|
process.exit(outcome.exitCode);
|
|
7806
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
|
+
}
|
|
7807
8171
|
if (parsed.kind === "list") {
|
|
7808
8172
|
const outcome = await runList({ json: parsed.json, cwd: process.cwd() }, previewDeps);
|
|
7809
8173
|
process.exit(outcome.exitCode);
|
package/dist/index.js
CHANGED
|
@@ -281,6 +281,19 @@ function parseArgs(argv) {
|
|
|
281
281
|
}
|
|
282
282
|
return { kind: "requests", json: rest.includes("--json"), clear: rest.includes("--clear") };
|
|
283
283
|
}
|
|
284
|
+
if (argv[0] === "log") {
|
|
285
|
+
const rest = argv.slice(1);
|
|
286
|
+
const flags = rest.filter((argument) => argument.startsWith("--"));
|
|
287
|
+
const unknown = flags.find((flag) => flag !== "--json");
|
|
288
|
+
if (unknown !== void 0) {
|
|
289
|
+
return { kind: "error", message: `leglas log does not take ${unknown}.` };
|
|
290
|
+
}
|
|
291
|
+
const names = rest.filter((argument) => !argument.startsWith("--"));
|
|
292
|
+
if (names.length > 1) {
|
|
293
|
+
return { kind: "error", message: "leglas log takes one entry at most." };
|
|
294
|
+
}
|
|
295
|
+
return { kind: "log", entry: names[0] ?? null, json: flags.includes("--json") };
|
|
296
|
+
}
|
|
284
297
|
if (argv[0] === "list") {
|
|
285
298
|
const rest = argv.slice(1);
|
|
286
299
|
const unknown = rest.find((argument) => argument !== "--json");
|
|
@@ -725,6 +738,12 @@ out of the ignored directory, deletes the rest of the exploration, and drops
|
|
|
725
738
|
them from the rail. Then change their component to use the kept component
|
|
726
739
|
instead of the switcher.
|
|
727
740
|
|
|
741
|
+
Keeping also writes what the exploration was into \`design-log/\`, which is
|
|
742
|
+
committed. Before exploring a surface, read \`npx leglas log --json\` and any
|
|
743
|
+
entry for that surface: it says what was already tried there, in the user's own
|
|
744
|
+
words, and which direction won. Proposing something that was already rejected
|
|
745
|
+
wastes their time, and the record is there so you do not have to ask.
|
|
746
|
+
|
|
728
747
|
Useful to know:
|
|
729
748
|
|
|
730
749
|
- \`.leglas/\` is gitignored. Exploration is disposable and nothing in there
|
|
@@ -785,6 +804,84 @@ ${AGENTS_SECTION}`
|
|
|
785
804
|
// src/keep.ts
|
|
786
805
|
import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
|
|
787
806
|
|
|
807
|
+
// ../server/dist/log.js
|
|
808
|
+
var DEFAULT_LOG_DIR = "design-log";
|
|
809
|
+
function slugify(value) {
|
|
810
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
|
|
811
|
+
}
|
|
812
|
+
function frameFor(title, requests) {
|
|
813
|
+
let found = null;
|
|
814
|
+
for (const request of requests) {
|
|
815
|
+
if (request.title !== title)
|
|
816
|
+
continue;
|
|
817
|
+
for (const attachment of request.attachments ?? []) {
|
|
818
|
+
if (attachment.kind === "frame")
|
|
819
|
+
found = attachment;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return found;
|
|
823
|
+
}
|
|
824
|
+
function askedOf(title, requests) {
|
|
825
|
+
return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
|
|
826
|
+
}
|
|
827
|
+
function composeEntry(input) {
|
|
828
|
+
const slug = `${input.date}-${slugify(input.surface)}`;
|
|
829
|
+
const pictures = [];
|
|
830
|
+
const lines2 = [];
|
|
831
|
+
lines2.push(`# ${input.surface}, ${input.date}`);
|
|
832
|
+
lines2.push("");
|
|
833
|
+
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.`}`);
|
|
834
|
+
lines2.push("");
|
|
835
|
+
for (const preview of input.previews) {
|
|
836
|
+
const won = preview.title === input.won.title;
|
|
837
|
+
lines2.push(`## ${preview.title}${won ? " \u2014 kept" : ""}`);
|
|
838
|
+
lines2.push("");
|
|
839
|
+
if (preview.note !== void 0 && preview.note.trim() !== "") {
|
|
840
|
+
lines2.push(preview.note.trim());
|
|
841
|
+
lines2.push("");
|
|
842
|
+
}
|
|
843
|
+
const frame = frameFor(preview.title, input.requests);
|
|
844
|
+
if (frame !== null) {
|
|
845
|
+
const name = `${slugify(preview.title)}.png`;
|
|
846
|
+
pictures.push({ from: frame.file, to: name });
|
|
847
|
+
lines2.push(``);
|
|
848
|
+
lines2.push("");
|
|
849
|
+
}
|
|
850
|
+
if (preview.basedOn !== void 0) {
|
|
851
|
+
lines2.push(`A variant of ${preview.basedOn}.`);
|
|
852
|
+
lines2.push("");
|
|
853
|
+
}
|
|
854
|
+
const asked = askedOf(preview.title, input.requests);
|
|
855
|
+
if (asked.length > 0) {
|
|
856
|
+
lines2.push("Asked for:");
|
|
857
|
+
lines2.push("");
|
|
858
|
+
for (const words of asked)
|
|
859
|
+
lines2.push(`- ${words}`);
|
|
860
|
+
lines2.push("");
|
|
861
|
+
}
|
|
862
|
+
const notes = input.annotations.filter((note) => note.title === preview.title);
|
|
863
|
+
if (notes.length > 0) {
|
|
864
|
+
lines2.push("Marked on the design:");
|
|
865
|
+
lines2.push("");
|
|
866
|
+
for (const note of notes)
|
|
867
|
+
lines2.push(`- ${note.note}`);
|
|
868
|
+
lines2.push("");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const failed = input.requests.filter((request) => request.status === "failed");
|
|
872
|
+
if (failed.length > 0) {
|
|
873
|
+
lines2.push("## Changes that did not land");
|
|
874
|
+
lines2.push("");
|
|
875
|
+
for (const request of failed) {
|
|
876
|
+
const why = request.failure?.message;
|
|
877
|
+
lines2.push(`- ${request.title}: ${request.intent}${why === void 0 ? "" : ` (${why})`}`);
|
|
878
|
+
}
|
|
879
|
+
lines2.push("");
|
|
880
|
+
}
|
|
881
|
+
return { slug, markdown: `${lines2.join("\n").trimEnd()}
|
|
882
|
+
`, pictures };
|
|
883
|
+
}
|
|
884
|
+
|
|
788
885
|
// ../server/dist/config.js
|
|
789
886
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
790
887
|
var DEFAULT_INSTALL_COMMAND = "npm install";
|
|
@@ -903,6 +1000,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
903
1000
|
if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
|
|
904
1001
|
errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
|
|
905
1002
|
}
|
|
1003
|
+
const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
|
|
1004
|
+
if (typeof logDir !== "string" || logDir.trim() === "") {
|
|
1005
|
+
errors.push("logDir must be a non-empty string.");
|
|
1006
|
+
}
|
|
906
1007
|
const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
|
|
907
1008
|
if (typeof installCommand !== "string" || installCommand.trim() === "") {
|
|
908
1009
|
errors.push("installCommand must be a non-empty string.");
|
|
@@ -919,7 +1020,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
919
1020
|
previews,
|
|
920
1021
|
scanPreviews,
|
|
921
1022
|
devCommand: typeof devCommand === "string" ? devCommand : void 0,
|
|
922
|
-
installCommand
|
|
1023
|
+
installCommand,
|
|
1024
|
+
logDir
|
|
923
1025
|
},
|
|
924
1026
|
errors: []
|
|
925
1027
|
};
|
|
@@ -1697,6 +1799,7 @@ import net from "net";
|
|
|
1697
1799
|
function createProxyHandler(options) {
|
|
1698
1800
|
const target = new URL(options.target);
|
|
1699
1801
|
const host = target.hostname;
|
|
1802
|
+
const dialHost = host.replace(/^\[|\]$/g, "");
|
|
1700
1803
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
1701
1804
|
const authority = target.port ? `${host}:${target.port}` : host;
|
|
1702
1805
|
function upstreamHeaders(req) {
|
|
@@ -1713,7 +1816,19 @@ function createProxyHandler(options) {
|
|
|
1713
1816
|
}
|
|
1714
1817
|
return {
|
|
1715
1818
|
request(req, res, publicOrigin) {
|
|
1716
|
-
|
|
1819
|
+
options.onActivity?.();
|
|
1820
|
+
options.onOpen?.();
|
|
1821
|
+
let open = true;
|
|
1822
|
+
const close = () => {
|
|
1823
|
+
if (!open)
|
|
1824
|
+
return;
|
|
1825
|
+
open = false;
|
|
1826
|
+
options.onActivity?.();
|
|
1827
|
+
options.onClose?.();
|
|
1828
|
+
};
|
|
1829
|
+
res.once("finish", close);
|
|
1830
|
+
res.once("close", close);
|
|
1831
|
+
const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
|
|
1717
1832
|
const headers = { ...upstreamRes.headers };
|
|
1718
1833
|
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin);
|
|
1719
1834
|
if (location !== void 0)
|
|
@@ -1735,7 +1850,17 @@ Start it, or point Leglas somewhere else with --user-port.`);
|
|
|
1735
1850
|
req.pipe(upstream);
|
|
1736
1851
|
},
|
|
1737
1852
|
upgrade(req, socket, head) {
|
|
1738
|
-
|
|
1853
|
+
options.onActivity?.();
|
|
1854
|
+
options.onOpen?.();
|
|
1855
|
+
let open = true;
|
|
1856
|
+
const closeActivity = () => {
|
|
1857
|
+
if (!open)
|
|
1858
|
+
return;
|
|
1859
|
+
open = false;
|
|
1860
|
+
options.onActivity?.();
|
|
1861
|
+
options.onClose?.();
|
|
1862
|
+
};
|
|
1863
|
+
const upstream = net.connect(port, dialHost, () => {
|
|
1739
1864
|
const headers = Object.entries(upstreamHeaders(req)).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value}\r
|
|
1740
1865
|
`).join("");
|
|
1741
1866
|
upstream.write(`${req.method} ${req.url} HTTP/1.1\r
|
|
@@ -1747,6 +1872,7 @@ ${headers}\r
|
|
|
1747
1872
|
socket.pipe(upstream);
|
|
1748
1873
|
});
|
|
1749
1874
|
const shutdown = () => {
|
|
1875
|
+
closeActivity();
|
|
1750
1876
|
upstream.destroy();
|
|
1751
1877
|
socket.destroy();
|
|
1752
1878
|
};
|
|
@@ -1757,6 +1883,62 @@ ${headers}\r
|
|
|
1757
1883
|
}
|
|
1758
1884
|
};
|
|
1759
1885
|
}
|
|
1886
|
+
function startProxyServer(options) {
|
|
1887
|
+
return new Promise((resolve5, reject) => {
|
|
1888
|
+
let open = 0;
|
|
1889
|
+
const handler = createProxyHandler({
|
|
1890
|
+
...options,
|
|
1891
|
+
onOpen: () => {
|
|
1892
|
+
open += 1;
|
|
1893
|
+
options.onOpen?.();
|
|
1894
|
+
},
|
|
1895
|
+
onClose: () => {
|
|
1896
|
+
open = Math.max(0, open - 1);
|
|
1897
|
+
options.onClose?.();
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
const server = http.createServer((req, res) => {
|
|
1901
|
+
const address = server.address();
|
|
1902
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1903
|
+
handler.request(req, res, `http://127.0.0.1:${port}`);
|
|
1904
|
+
});
|
|
1905
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
1906
|
+
server.on("connection", (socket) => {
|
|
1907
|
+
sockets.add(socket);
|
|
1908
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1909
|
+
});
|
|
1910
|
+
server.on("upgrade", (req, socket, head) => handler.upgrade(req, socket, head));
|
|
1911
|
+
const onError = (error) => {
|
|
1912
|
+
server.removeListener("listening", onListening);
|
|
1913
|
+
reject(error);
|
|
1914
|
+
};
|
|
1915
|
+
const onListening = () => {
|
|
1916
|
+
server.removeListener("error", onError);
|
|
1917
|
+
const address = server.address();
|
|
1918
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1919
|
+
let closed = null;
|
|
1920
|
+
resolve5({
|
|
1921
|
+
active: () => open > 0,
|
|
1922
|
+
close: () => {
|
|
1923
|
+
if (closed !== null)
|
|
1924
|
+
return closed;
|
|
1925
|
+
closed = new Promise((done) => {
|
|
1926
|
+
for (const socket of sockets)
|
|
1927
|
+
socket.destroy();
|
|
1928
|
+
sockets.clear();
|
|
1929
|
+
server.closeAllConnections();
|
|
1930
|
+
server.close(() => done());
|
|
1931
|
+
});
|
|
1932
|
+
return closed;
|
|
1933
|
+
},
|
|
1934
|
+
url: `http://127.0.0.1:${port}`
|
|
1935
|
+
});
|
|
1936
|
+
};
|
|
1937
|
+
server.once("error", onError);
|
|
1938
|
+
server.once("listening", onListening);
|
|
1939
|
+
server.listen(0, "127.0.0.1");
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1760
1942
|
|
|
1761
1943
|
// ../server/dist/browser.js
|
|
1762
1944
|
import { randomBytes } from "crypto";
|
|
@@ -3122,6 +3304,8 @@ async function startAppProcess(options) {
|
|
|
3122
3304
|
}
|
|
3123
3305
|
|
|
3124
3306
|
// ../server/dist/branches.js
|
|
3307
|
+
var BRANCH_IDLE_MS = 10 * 60 * 1e3;
|
|
3308
|
+
var BRANCH_SWEEP_MS = 3e4;
|
|
3125
3309
|
function publicBranchState(state) {
|
|
3126
3310
|
if (state.status === "ready")
|
|
3127
3311
|
return { status: "ready" };
|
|
@@ -3131,7 +3315,11 @@ function createBranchRegistry(options) {
|
|
|
3131
3315
|
const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
|
|
3132
3316
|
const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
|
|
3133
3317
|
const inflight = /* @__PURE__ */ new Map();
|
|
3318
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
3319
|
+
const lastActivity = /* @__PURE__ */ new Map();
|
|
3320
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
3134
3321
|
const boot = options.startWorktree ?? startWorktree;
|
|
3322
|
+
const proxy = options.startProxy ?? startProxyServer;
|
|
3135
3323
|
let closed = false;
|
|
3136
3324
|
let stopPromise = null;
|
|
3137
3325
|
const transition = (title, state) => {
|
|
@@ -3143,6 +3331,43 @@ function createBranchRegistry(options) {
|
|
|
3143
3331
|
options.onChange?.(title, state);
|
|
3144
3332
|
return state;
|
|
3145
3333
|
};
|
|
3334
|
+
const stopReady = (title, state, toIdle) => {
|
|
3335
|
+
const current = stopping.get(title);
|
|
3336
|
+
if (current !== void 0)
|
|
3337
|
+
return current;
|
|
3338
|
+
const pending = (async () => {
|
|
3339
|
+
await proxies.get(title)?.close().catch(() => {
|
|
3340
|
+
});
|
|
3341
|
+
await state.worktree.stop().catch(() => {
|
|
3342
|
+
});
|
|
3343
|
+
proxies.delete(title);
|
|
3344
|
+
lastActivity.delete(title);
|
|
3345
|
+
if (toIdle && !closed && states.get(title) === state) {
|
|
3346
|
+
transition(title, { status: "idle" });
|
|
3347
|
+
}
|
|
3348
|
+
})().finally(() => {
|
|
3349
|
+
stopping.delete(title);
|
|
3350
|
+
});
|
|
3351
|
+
stopping.set(title, pending);
|
|
3352
|
+
return pending;
|
|
3353
|
+
};
|
|
3354
|
+
const sweep = () => {
|
|
3355
|
+
const now = Date.now();
|
|
3356
|
+
for (const [title, state] of states) {
|
|
3357
|
+
const branchProxy = proxies.get(title);
|
|
3358
|
+
if (state.status !== "ready" || branchProxy === void 0 || stopping.has(title))
|
|
3359
|
+
continue;
|
|
3360
|
+
if (branchProxy.active()) {
|
|
3361
|
+
lastActivity.set(title, now);
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
const seen = lastActivity.get(title) ?? now;
|
|
3365
|
+
if (now - seen >= BRANCH_IDLE_MS)
|
|
3366
|
+
void stopReady(title, state, true);
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
const sweepTimer = setInterval(sweep, BRANCH_SWEEP_MS);
|
|
3370
|
+
sweepTimer.unref();
|
|
3146
3371
|
const begin = (title) => {
|
|
3147
3372
|
const preview = previews.get(title);
|
|
3148
3373
|
const current = states.get(title);
|
|
@@ -3170,9 +3395,21 @@ function createBranchRegistry(options) {
|
|
|
3170
3395
|
} catch (error) {
|
|
3171
3396
|
checkout = Promise.reject(error);
|
|
3172
3397
|
}
|
|
3173
|
-
const starting = checkout.then((worktree) => {
|
|
3398
|
+
const starting = checkout.then(async (worktree) => {
|
|
3174
3399
|
transition(title, { status: "starting", phase: "starting" });
|
|
3175
|
-
|
|
3400
|
+
try {
|
|
3401
|
+
const branchProxy = await proxy({
|
|
3402
|
+
target: worktree.url,
|
|
3403
|
+
onActivity: () => lastActivity.set(title, Date.now())
|
|
3404
|
+
});
|
|
3405
|
+
proxies.set(title, branchProxy);
|
|
3406
|
+
lastActivity.set(title, Date.now());
|
|
3407
|
+
return transition(title, { status: "ready", worktree });
|
|
3408
|
+
} catch (error) {
|
|
3409
|
+
await worktree.stop().catch(() => {
|
|
3410
|
+
});
|
|
3411
|
+
throw error;
|
|
3412
|
+
}
|
|
3176
3413
|
}).catch((error) => transition(title, {
|
|
3177
3414
|
status: "failed",
|
|
3178
3415
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -3184,15 +3421,17 @@ function createBranchRegistry(options) {
|
|
|
3184
3421
|
};
|
|
3185
3422
|
return {
|
|
3186
3423
|
state: (title) => states.get(title),
|
|
3424
|
+
url: (title) => proxies.get(title)?.url,
|
|
3187
3425
|
start: begin,
|
|
3188
3426
|
stop: () => {
|
|
3189
3427
|
if (stopPromise !== null)
|
|
3190
3428
|
return stopPromise;
|
|
3191
3429
|
closed = true;
|
|
3430
|
+
clearInterval(sweepTimer);
|
|
3192
3431
|
stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3432
|
+
await Promise.allSettled([...stopping.values()]);
|
|
3433
|
+
const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
|
|
3434
|
+
await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
|
|
3196
3435
|
});
|
|
3197
3436
|
return stopPromise;
|
|
3198
3437
|
}
|
|
@@ -5289,6 +5528,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
5289
5528
|
|
|
5290
5529
|
// ../server/dist/server.js
|
|
5291
5530
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5531
|
+
import { createHash as createHash2 } from "crypto";
|
|
5292
5532
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5293
5533
|
import http2 from "http";
|
|
5294
5534
|
import net3 from "net";
|
|
@@ -5367,6 +5607,30 @@ function sendJson(res, status, body) {
|
|
|
5367
5607
|
});
|
|
5368
5608
|
res.end(payload);
|
|
5369
5609
|
}
|
|
5610
|
+
function etagMatches(value, etag) {
|
|
5611
|
+
if (value === void 0)
|
|
5612
|
+
return false;
|
|
5613
|
+
const values = Array.isArray(value) ? value : [value];
|
|
5614
|
+
return values.some((header) => header.split(",").some((candidate) => {
|
|
5615
|
+
const tag = candidate.trim();
|
|
5616
|
+
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
5617
|
+
}));
|
|
5618
|
+
}
|
|
5619
|
+
function sendConditionalJson(req, res, body) {
|
|
5620
|
+
const payload = JSON.stringify(body);
|
|
5621
|
+
const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
|
|
5622
|
+
if (etagMatches(req.headers["if-none-match"], etag)) {
|
|
5623
|
+
res.writeHead(304, { etag, "cache-control": "private, no-cache" });
|
|
5624
|
+
res.end();
|
|
5625
|
+
return;
|
|
5626
|
+
}
|
|
5627
|
+
res.writeHead(200, {
|
|
5628
|
+
"content-type": "application/json; charset=utf-8",
|
|
5629
|
+
"cache-control": "private, no-cache",
|
|
5630
|
+
etag
|
|
5631
|
+
});
|
|
5632
|
+
res.end(payload);
|
|
5633
|
+
}
|
|
5370
5634
|
var CAPTURE_DEADLINE_MS = 15e3;
|
|
5371
5635
|
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
5372
5636
|
function captureSlug(title) {
|
|
@@ -5759,9 +6023,10 @@ async function startServer(options) {
|
|
|
5759
6023
|
return preview;
|
|
5760
6024
|
const state = branches.state(preview.title) ?? { status: "idle" };
|
|
5761
6025
|
const { url: route, ...withoutUrl } = preview;
|
|
6026
|
+
const branchUrl = branches.url(preview.title);
|
|
5762
6027
|
return state.status === "ready" ? {
|
|
5763
6028
|
...withoutUrl,
|
|
5764
|
-
url: `${state.worktree.url}${route}`,
|
|
6029
|
+
url: `${branchUrl ?? state.worktree.url}${route}`,
|
|
5765
6030
|
state: publicBranchState(state)
|
|
5766
6031
|
} : { ...withoutUrl, state: publicBranchState(state) };
|
|
5767
6032
|
};
|
|
@@ -5770,7 +6035,7 @@ async function startServer(options) {
|
|
|
5770
6035
|
if (preview.branch === void 0)
|
|
5771
6036
|
return preview;
|
|
5772
6037
|
const state = branches.state(preview.title);
|
|
5773
|
-
return state?.status === "ready" ? { ...preview, url: `${state.worktree.url}${preview.url}` } : null;
|
|
6038
|
+
return state?.status === "ready" ? { ...preview, url: `${branches.url(preview.title) ?? state.worktree.url}${preview.url}` } : null;
|
|
5774
6039
|
};
|
|
5775
6040
|
if (options.pool === void 0) {
|
|
5776
6041
|
void reapOrphanedBrowsers().catch(() => {
|
|
@@ -5833,7 +6098,7 @@ async function startServer(options) {
|
|
|
5833
6098
|
errors.push(notice);
|
|
5834
6099
|
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
5835
6100
|
if (localErrors.length > 0) {
|
|
5836
|
-
return
|
|
6101
|
+
return sendConditionalJson(req, res, {
|
|
5837
6102
|
project,
|
|
5838
6103
|
devServer: target,
|
|
5839
6104
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5846,7 +6111,7 @@ async function startServer(options) {
|
|
|
5846
6111
|
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
5847
6112
|
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
5848
6113
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
5849
|
-
|
|
6114
|
+
sendConditionalJson(req, res, {
|
|
5850
6115
|
project,
|
|
5851
6116
|
devServer: target,
|
|
5852
6117
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5854,7 +6119,7 @@ async function startServer(options) {
|
|
|
5854
6119
|
errors,
|
|
5855
6120
|
warnings: configWarnings
|
|
5856
6121
|
});
|
|
5857
|
-
}).catch(() =>
|
|
6122
|
+
}).catch(() => sendConditionalJson(req, res, {
|
|
5858
6123
|
project,
|
|
5859
6124
|
devServer: target,
|
|
5860
6125
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -6289,7 +6554,7 @@ async function startServer(options) {
|
|
|
6289
6554
|
waiting: null,
|
|
6290
6555
|
failedIds: []
|
|
6291
6556
|
};
|
|
6292
|
-
return void readRequests(cwd).then((requests) =>
|
|
6557
|
+
return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
|
|
6293
6558
|
requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
|
|
6294
6559
|
id,
|
|
6295
6560
|
title,
|
|
@@ -6392,7 +6657,7 @@ async function startServer(options) {
|
|
|
6392
6657
|
});
|
|
6393
6658
|
}
|
|
6394
6659
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
6395
|
-
return void readAnnotations(cwd).then((annotations) =>
|
|
6660
|
+
return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
|
|
6396
6661
|
}
|
|
6397
6662
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6398
6663
|
if (!hasJsonBody(req)) {
|
|
@@ -6518,7 +6783,7 @@ async function startServer(options) {
|
|
|
6518
6783
|
});
|
|
6519
6784
|
}
|
|
6520
6785
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
6521
|
-
return void probe(target).then((reachable) =>
|
|
6786
|
+
return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
|
|
6522
6787
|
}
|
|
6523
6788
|
if (path.startsWith(`${FILES_PREFIX}/`)) {
|
|
6524
6789
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
@@ -6708,7 +6973,7 @@ async function runInit(options, deps) {
|
|
|
6708
6973
|
|
|
6709
6974
|
// src/run-keep.ts
|
|
6710
6975
|
import { existsSync as existsSync4 } from "fs";
|
|
6711
|
-
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6976
|
+
import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6712
6977
|
import { dirname as dirname9, join as join14 } from "path";
|
|
6713
6978
|
|
|
6714
6979
|
// src/resolve-title.ts
|
|
@@ -6736,6 +7001,29 @@ function renameExport(source, to) {
|
|
|
6736
7001
|
to
|
|
6737
7002
|
);
|
|
6738
7003
|
}
|
|
7004
|
+
async function writeLogEntry(options) {
|
|
7005
|
+
const entry = composeEntry({
|
|
7006
|
+
surface: options.surface,
|
|
7007
|
+
won: options.won,
|
|
7008
|
+
previews: options.previews,
|
|
7009
|
+
requests: await readRequests(options.cwd),
|
|
7010
|
+
annotations: await readAnnotations(options.cwd),
|
|
7011
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
7012
|
+
});
|
|
7013
|
+
const dir = join14(options.cwd, options.logDir);
|
|
7014
|
+
await mkdir9(dir, { recursive: true });
|
|
7015
|
+
const file = join14(dir, `${entry.slug}.md`);
|
|
7016
|
+
await writeFile10(file, entry.markdown, "utf8");
|
|
7017
|
+
if (entry.pictures.length > 0) {
|
|
7018
|
+
const pictureDir = join14(dir, entry.slug);
|
|
7019
|
+
await mkdir9(pictureDir, { recursive: true });
|
|
7020
|
+
for (const picture of entry.pictures) {
|
|
7021
|
+
await copyFile2(join14(options.cwd, picture.from), join14(pictureDir, picture.to)).catch(() => {
|
|
7022
|
+
});
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
return `${options.logDir}/${entry.slug}.md`;
|
|
7026
|
+
}
|
|
6739
7027
|
async function runKeep(options, deps) {
|
|
6740
7028
|
const loaded = await loadConfig(options.cwd);
|
|
6741
7029
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -6764,6 +7052,20 @@ async function runKeep(options, deps) {
|
|
|
6764
7052
|
const source = await readFile13(from, "utf8");
|
|
6765
7053
|
await mkdir9(dirname9(to), { recursive: true });
|
|
6766
7054
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
7055
|
+
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
7056
|
+
let logged = null;
|
|
7057
|
+
let logError = null;
|
|
7058
|
+
try {
|
|
7059
|
+
logged = await writeLogEntry({
|
|
7060
|
+
cwd: options.cwd,
|
|
7061
|
+
logDir: loaded.config?.logDir ?? DEFAULT_LOG_DIR,
|
|
7062
|
+
surface,
|
|
7063
|
+
won: { title: resolved.title, to: plan.move.to },
|
|
7064
|
+
previews: previews.filter((preview) => plan.dropTitles.includes(preview.title))
|
|
7065
|
+
});
|
|
7066
|
+
} catch (error) {
|
|
7067
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
7068
|
+
}
|
|
6767
7069
|
await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6768
7070
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
6769
7071
|
if (options.json) {
|
|
@@ -6775,6 +7077,8 @@ async function runKeep(options, deps) {
|
|
|
6775
7077
|
exportName: plan.exportName,
|
|
6776
7078
|
removed: plan.removeDir,
|
|
6777
7079
|
droppedPreviews: dropped,
|
|
7080
|
+
logged,
|
|
7081
|
+
logError,
|
|
6778
7082
|
instructions: plan.instructions
|
|
6779
7083
|
})
|
|
6780
7084
|
);
|
|
@@ -6782,6 +7086,8 @@ async function runKeep(options, deps) {
|
|
|
6782
7086
|
}
|
|
6783
7087
|
deps.log(` kept ${plan.move.to}`);
|
|
6784
7088
|
deps.log(` removed ${plan.removeDir}`);
|
|
7089
|
+
if (logged !== null) deps.log(` logged ${logged}`);
|
|
7090
|
+
if (logError !== null) deps.error(` The decision log could not be written: ${logError}`);
|
|
6785
7091
|
if (dropped > 0) {
|
|
6786
7092
|
deps.log(` dropped ${dropped} direction${dropped === 1 ? "" : "s"} from the rail`);
|
|
6787
7093
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type LogDeps = {
|
|
2
|
+
log(line: string): void;
|
|
3
|
+
error(line: string): void;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Read what past explorations decided.
|
|
7
|
+
*
|
|
8
|
+
* The entries are plain markdown in a committed directory and are meant to be
|
|
9
|
+
* read that way, in a pull request or on GitHub. This exists because an agent
|
|
10
|
+
* asked to work on a surface should be able to find what was already tried
|
|
11
|
+
* there without being told where to look, and because a person coming back to
|
|
12
|
+
* a project should not have to know the directory's name.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runLog(options: {
|
|
15
|
+
entry: string | null;
|
|
16
|
+
json: boolean;
|
|
17
|
+
cwd: string;
|
|
18
|
+
}, deps: LogDeps): Promise<{
|
|
19
|
+
exitCode: number;
|
|
20
|
+
}>;
|