leglas 0.7.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -6
- package/dist/args.d.ts +4 -0
- package/dist/bin.js +462 -41
- package/dist/index.js +385 -22
- package/dist/run-log.d.ts +20 -0
- package/dist/shell/assets/index-BMpAHiSA.js +14 -0
- package/dist/shell/assets/index-pfmBGQQ0.css +1 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-CpYCCElH.css +0 -1
- package/dist/shell/assets/index-Ct9pp-D-.js +0 -14
package/dist/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");
|
|
@@ -675,6 +688,14 @@ When asked for design variations, alternatives, or "a few options":
|
|
|
675
688
|
and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
676
689
|
(the config needs \`devCommand\` with \`{port}\`). Everything below is the
|
|
677
690
|
ordinary, in-app path.
|
|
691
|
+
A page the app rebuilds in the browser after load (anything that hydrates:
|
|
692
|
+
Next, Nuxt, SvelteKit, a captured production site) is not its served HTML.
|
|
693
|
+
Markup edited there shows for a moment and is then replaced from the app's
|
|
694
|
+
own JavaScript and data, so make the change where that JavaScript gets what
|
|
695
|
+
it renders. When that is a script other directions share, give it a
|
|
696
|
+
per-direction override that defaults to what it renders today: every other
|
|
697
|
+
direction renders exactly as before, which is adding beside, not rewriting.
|
|
698
|
+
\`npx leglas show\` says when a page was rebuilt after load.
|
|
678
699
|
2. Run \`npx leglas explore <surface> --count <n>\` first, adding
|
|
679
700
|
\`--based-on "<title>"\` when the user wants variations of a direction they
|
|
680
701
|
already like. It prints what the set needs and how to register it. In
|
|
@@ -725,6 +746,12 @@ out of the ignored directory, deletes the rest of the exploration, and drops
|
|
|
725
746
|
them from the rail. Then change their component to use the kept component
|
|
726
747
|
instead of the switcher.
|
|
727
748
|
|
|
749
|
+
Keeping also writes what the exploration was into \`design-log/\`, which is
|
|
750
|
+
committed. Before exploring a surface, read \`npx leglas log --json\` and any
|
|
751
|
+
entry for that surface: it says what was already tried there, in the user's own
|
|
752
|
+
words, and which direction won. Proposing something that was already rejected
|
|
753
|
+
wastes their time, and the record is there so you do not have to ask.
|
|
754
|
+
|
|
728
755
|
Useful to know:
|
|
729
756
|
|
|
730
757
|
- \`.leglas/\` is gitignored. Exploration is disposable and nothing in there
|
|
@@ -785,6 +812,84 @@ ${AGENTS_SECTION}`
|
|
|
785
812
|
// src/keep.ts
|
|
786
813
|
import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
|
|
787
814
|
|
|
815
|
+
// ../server/dist/log.js
|
|
816
|
+
var DEFAULT_LOG_DIR = "design-log";
|
|
817
|
+
function slugify(value) {
|
|
818
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
|
|
819
|
+
}
|
|
820
|
+
function frameFor(title, requests) {
|
|
821
|
+
let found = null;
|
|
822
|
+
for (const request of requests) {
|
|
823
|
+
if (request.title !== title)
|
|
824
|
+
continue;
|
|
825
|
+
for (const attachment of request.attachments ?? []) {
|
|
826
|
+
if (attachment.kind === "frame")
|
|
827
|
+
found = attachment;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return found;
|
|
831
|
+
}
|
|
832
|
+
function askedOf(title, requests) {
|
|
833
|
+
return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
|
|
834
|
+
}
|
|
835
|
+
function composeEntry(input) {
|
|
836
|
+
const slug = `${input.date}-${slugify(input.surface)}`;
|
|
837
|
+
const pictures = [];
|
|
838
|
+
const lines2 = [];
|
|
839
|
+
lines2.push(`# ${input.surface}, ${input.date}`);
|
|
840
|
+
lines2.push("");
|
|
841
|
+
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.`}`);
|
|
842
|
+
lines2.push("");
|
|
843
|
+
for (const preview of input.previews) {
|
|
844
|
+
const won = preview.title === input.won.title;
|
|
845
|
+
lines2.push(`## ${preview.title}${won ? " \u2014 kept" : ""}`);
|
|
846
|
+
lines2.push("");
|
|
847
|
+
if (preview.note !== void 0 && preview.note.trim() !== "") {
|
|
848
|
+
lines2.push(preview.note.trim());
|
|
849
|
+
lines2.push("");
|
|
850
|
+
}
|
|
851
|
+
const frame = frameFor(preview.title, input.requests);
|
|
852
|
+
if (frame !== null) {
|
|
853
|
+
const name = `${slugify(preview.title)}.png`;
|
|
854
|
+
pictures.push({ from: frame.file, to: name });
|
|
855
|
+
lines2.push(``);
|
|
856
|
+
lines2.push("");
|
|
857
|
+
}
|
|
858
|
+
if (preview.basedOn !== void 0) {
|
|
859
|
+
lines2.push(`A variant of ${preview.basedOn}.`);
|
|
860
|
+
lines2.push("");
|
|
861
|
+
}
|
|
862
|
+
const asked = askedOf(preview.title, input.requests);
|
|
863
|
+
if (asked.length > 0) {
|
|
864
|
+
lines2.push("Asked for:");
|
|
865
|
+
lines2.push("");
|
|
866
|
+
for (const words of asked)
|
|
867
|
+
lines2.push(`- ${words}`);
|
|
868
|
+
lines2.push("");
|
|
869
|
+
}
|
|
870
|
+
const notes = input.annotations.filter((note) => note.title === preview.title);
|
|
871
|
+
if (notes.length > 0) {
|
|
872
|
+
lines2.push("Marked on the design:");
|
|
873
|
+
lines2.push("");
|
|
874
|
+
for (const note of notes)
|
|
875
|
+
lines2.push(`- ${note.note}`);
|
|
876
|
+
lines2.push("");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const failed = input.requests.filter((request) => request.status === "failed");
|
|
880
|
+
if (failed.length > 0) {
|
|
881
|
+
lines2.push("## Changes that did not land");
|
|
882
|
+
lines2.push("");
|
|
883
|
+
for (const request of failed) {
|
|
884
|
+
const why = request.failure?.message;
|
|
885
|
+
lines2.push(`- ${request.title}: ${request.intent}${why === void 0 ? "" : ` (${why})`}`);
|
|
886
|
+
}
|
|
887
|
+
lines2.push("");
|
|
888
|
+
}
|
|
889
|
+
return { slug, markdown: `${lines2.join("\n").trimEnd()}
|
|
890
|
+
`, pictures };
|
|
891
|
+
}
|
|
892
|
+
|
|
788
893
|
// ../server/dist/config.js
|
|
789
894
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
790
895
|
var DEFAULT_INSTALL_COMMAND = "npm install";
|
|
@@ -903,6 +1008,10 @@ function normalizeConfig(raw, options = {}) {
|
|
|
903
1008
|
if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
|
|
904
1009
|
errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
|
|
905
1010
|
}
|
|
1011
|
+
const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
|
|
1012
|
+
if (typeof logDir !== "string" || logDir.trim() === "") {
|
|
1013
|
+
errors.push("logDir must be a non-empty string.");
|
|
1014
|
+
}
|
|
906
1015
|
const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
|
|
907
1016
|
if (typeof installCommand !== "string" || installCommand.trim() === "") {
|
|
908
1017
|
errors.push("installCommand must be a non-empty string.");
|
|
@@ -919,7 +1028,8 @@ function normalizeConfig(raw, options = {}) {
|
|
|
919
1028
|
previews,
|
|
920
1029
|
scanPreviews,
|
|
921
1030
|
devCommand: typeof devCommand === "string" ? devCommand : void 0,
|
|
922
|
-
installCommand
|
|
1031
|
+
installCommand,
|
|
1032
|
+
logDir
|
|
923
1033
|
},
|
|
924
1034
|
errors: []
|
|
925
1035
|
};
|
|
@@ -1697,6 +1807,7 @@ import net from "net";
|
|
|
1697
1807
|
function createProxyHandler(options) {
|
|
1698
1808
|
const target = new URL(options.target);
|
|
1699
1809
|
const host = target.hostname;
|
|
1810
|
+
const dialHost = host.replace(/^\[|\]$/g, "");
|
|
1700
1811
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
1701
1812
|
const authority = target.port ? `${host}:${target.port}` : host;
|
|
1702
1813
|
function upstreamHeaders(req) {
|
|
@@ -1713,7 +1824,19 @@ function createProxyHandler(options) {
|
|
|
1713
1824
|
}
|
|
1714
1825
|
return {
|
|
1715
1826
|
request(req, res, publicOrigin) {
|
|
1716
|
-
|
|
1827
|
+
options.onActivity?.();
|
|
1828
|
+
options.onOpen?.();
|
|
1829
|
+
let open = true;
|
|
1830
|
+
const close = () => {
|
|
1831
|
+
if (!open)
|
|
1832
|
+
return;
|
|
1833
|
+
open = false;
|
|
1834
|
+
options.onActivity?.();
|
|
1835
|
+
options.onClose?.();
|
|
1836
|
+
};
|
|
1837
|
+
res.once("finish", close);
|
|
1838
|
+
res.once("close", close);
|
|
1839
|
+
const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
|
|
1717
1840
|
const headers = { ...upstreamRes.headers };
|
|
1718
1841
|
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin);
|
|
1719
1842
|
if (location !== void 0)
|
|
@@ -1735,7 +1858,17 @@ Start it, or point Leglas somewhere else with --user-port.`);
|
|
|
1735
1858
|
req.pipe(upstream);
|
|
1736
1859
|
},
|
|
1737
1860
|
upgrade(req, socket, head) {
|
|
1738
|
-
|
|
1861
|
+
options.onActivity?.();
|
|
1862
|
+
options.onOpen?.();
|
|
1863
|
+
let open = true;
|
|
1864
|
+
const closeActivity = () => {
|
|
1865
|
+
if (!open)
|
|
1866
|
+
return;
|
|
1867
|
+
open = false;
|
|
1868
|
+
options.onActivity?.();
|
|
1869
|
+
options.onClose?.();
|
|
1870
|
+
};
|
|
1871
|
+
const upstream = net.connect(port, dialHost, () => {
|
|
1739
1872
|
const headers = Object.entries(upstreamHeaders(req)).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value}\r
|
|
1740
1873
|
`).join("");
|
|
1741
1874
|
upstream.write(`${req.method} ${req.url} HTTP/1.1\r
|
|
@@ -1747,6 +1880,7 @@ ${headers}\r
|
|
|
1747
1880
|
socket.pipe(upstream);
|
|
1748
1881
|
});
|
|
1749
1882
|
const shutdown = () => {
|
|
1883
|
+
closeActivity();
|
|
1750
1884
|
upstream.destroy();
|
|
1751
1885
|
socket.destroy();
|
|
1752
1886
|
};
|
|
@@ -1757,6 +1891,62 @@ ${headers}\r
|
|
|
1757
1891
|
}
|
|
1758
1892
|
};
|
|
1759
1893
|
}
|
|
1894
|
+
function startProxyServer(options) {
|
|
1895
|
+
return new Promise((resolve5, reject) => {
|
|
1896
|
+
let open = 0;
|
|
1897
|
+
const handler = createProxyHandler({
|
|
1898
|
+
...options,
|
|
1899
|
+
onOpen: () => {
|
|
1900
|
+
open += 1;
|
|
1901
|
+
options.onOpen?.();
|
|
1902
|
+
},
|
|
1903
|
+
onClose: () => {
|
|
1904
|
+
open = Math.max(0, open - 1);
|
|
1905
|
+
options.onClose?.();
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
const server = http.createServer((req, res) => {
|
|
1909
|
+
const address = server.address();
|
|
1910
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1911
|
+
handler.request(req, res, `http://127.0.0.1:${port}`);
|
|
1912
|
+
});
|
|
1913
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
1914
|
+
server.on("connection", (socket) => {
|
|
1915
|
+
sockets.add(socket);
|
|
1916
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1917
|
+
});
|
|
1918
|
+
server.on("upgrade", (req, socket, head) => handler.upgrade(req, socket, head));
|
|
1919
|
+
const onError = (error) => {
|
|
1920
|
+
server.removeListener("listening", onListening);
|
|
1921
|
+
reject(error);
|
|
1922
|
+
};
|
|
1923
|
+
const onListening = () => {
|
|
1924
|
+
server.removeListener("error", onError);
|
|
1925
|
+
const address = server.address();
|
|
1926
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1927
|
+
let closed = null;
|
|
1928
|
+
resolve5({
|
|
1929
|
+
active: () => open > 0,
|
|
1930
|
+
close: () => {
|
|
1931
|
+
if (closed !== null)
|
|
1932
|
+
return closed;
|
|
1933
|
+
closed = new Promise((done) => {
|
|
1934
|
+
for (const socket of sockets)
|
|
1935
|
+
socket.destroy();
|
|
1936
|
+
sockets.clear();
|
|
1937
|
+
server.closeAllConnections();
|
|
1938
|
+
server.close(() => done());
|
|
1939
|
+
});
|
|
1940
|
+
return closed;
|
|
1941
|
+
},
|
|
1942
|
+
url: `http://127.0.0.1:${port}`
|
|
1943
|
+
});
|
|
1944
|
+
};
|
|
1945
|
+
server.once("error", onError);
|
|
1946
|
+
server.once("listening", onListening);
|
|
1947
|
+
server.listen(0, "127.0.0.1");
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1760
1950
|
|
|
1761
1951
|
// ../server/dist/browser.js
|
|
1762
1952
|
import { randomBytes } from "crypto";
|
|
@@ -2411,6 +2601,32 @@ function createBrowserPool(options = {}) {
|
|
|
2411
2601
|
};
|
|
2412
2602
|
}
|
|
2413
2603
|
|
|
2604
|
+
// ../server/dist/hydration.js
|
|
2605
|
+
function hydrationEvidence(messages) {
|
|
2606
|
+
for (const raw of messages) {
|
|
2607
|
+
const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
|
|
2608
|
+
if (/Minified React error #(418|419|422|423|425)\b/.test(message2)) {
|
|
2609
|
+
return { framework: "React", message: message2 };
|
|
2610
|
+
}
|
|
2611
|
+
if (/Hydration failed because/.test(message2) || /error while hydrating/i.test(message2) || /Text content (did not|does not) match/i.test(message2) || /Expected server HTML to contain/i.test(message2) || /did not match\. Server:/.test(message2)) {
|
|
2612
|
+
return { framework: "React", message: message2 };
|
|
2613
|
+
}
|
|
2614
|
+
if (/Hydration (node|text|children|class|style|attribute) mismatch/i.test(message2) || /Hydration completed but contains mismatches/i.test(message2)) {
|
|
2615
|
+
return { framework: "Vue", message: message2 };
|
|
2616
|
+
}
|
|
2617
|
+
if (/hydration_mismatch/.test(message2)) {
|
|
2618
|
+
return { framework: "Svelte", message: message2 };
|
|
2619
|
+
}
|
|
2620
|
+
if (/Hydration Mismatch\. Unable to find DOM nodes/.test(message2)) {
|
|
2621
|
+
return { framework: "Solid", message: message2 };
|
|
2622
|
+
}
|
|
2623
|
+
if (/hydrat/i.test(message2) && (/expected .+ but found/i.test(message2) || /mismatch/i.test(message2) && /(node|element|markup|dom|tag|text|attribute|server|client)/i.test(message2))) {
|
|
2624
|
+
return { framework: "the app", message: message2 };
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2414
2630
|
// ../server/dist/capture.js
|
|
2415
2631
|
var FRAME_MAX_HEIGHT = 4e3;
|
|
2416
2632
|
var MIN_WIDTH = 320;
|
|
@@ -2504,10 +2720,12 @@ function locatorExpression(focus) {
|
|
|
2504
2720
|
async function render(page, input) {
|
|
2505
2721
|
const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
|
|
2506
2722
|
const errors = [];
|
|
2723
|
+
let hydration = null;
|
|
2507
2724
|
const remember = (value) => {
|
|
2725
|
+
const message2 = String(value ?? "").trim().slice(0, 240);
|
|
2726
|
+
hydration ??= hydrationEvidence([message2]);
|
|
2508
2727
|
if (errors.length >= 10)
|
|
2509
2728
|
return;
|
|
2510
|
-
const message2 = String(value ?? "").slice(0, 240);
|
|
2511
2729
|
if (message2 === "" || /favicon/i.test(message2))
|
|
2512
2730
|
return;
|
|
2513
2731
|
errors.push(message2);
|
|
@@ -2655,7 +2873,7 @@ async function render(page, input) {
|
|
|
2655
2873
|
resolved
|
|
2656
2874
|
});
|
|
2657
2875
|
}
|
|
2658
|
-
return { frame, crops, errors, cut };
|
|
2876
|
+
return { frame, crops, errors, hydration, cut };
|
|
2659
2877
|
} finally {
|
|
2660
2878
|
for (const stop of unlisten)
|
|
2661
2879
|
stop();
|
|
@@ -2799,7 +3017,13 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2799
3017
|
const capture = deps.capture ?? capturePage;
|
|
2800
3018
|
const deadlineMs = deps.deadlineMs ?? 12e3;
|
|
2801
3019
|
const destination = join5(cwd, CAPTURES_DIR, requestId);
|
|
2802
|
-
const captured = {
|
|
3020
|
+
const captured = {
|
|
3021
|
+
attachments: [],
|
|
3022
|
+
errors: [],
|
|
3023
|
+
hydration: null,
|
|
3024
|
+
cut: false,
|
|
3025
|
+
skipped: null
|
|
3026
|
+
};
|
|
2803
3027
|
const references = [];
|
|
2804
3028
|
requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
|
|
2805
3029
|
const controller = new AbortController();
|
|
@@ -2849,6 +3073,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2849
3073
|
viewport: direction.frame.width
|
|
2850
3074
|
});
|
|
2851
3075
|
captured.errors = direction.errors;
|
|
3076
|
+
captured.hydration = direction.hydration;
|
|
2852
3077
|
captured.cut = direction.cut;
|
|
2853
3078
|
for (let index = 0; index < direction.crops.length; index += 1) {
|
|
2854
3079
|
const crop = direction.crops[index];
|
|
@@ -3122,6 +3347,8 @@ async function startAppProcess(options) {
|
|
|
3122
3347
|
}
|
|
3123
3348
|
|
|
3124
3349
|
// ../server/dist/branches.js
|
|
3350
|
+
var BRANCH_IDLE_MS = 10 * 60 * 1e3;
|
|
3351
|
+
var BRANCH_SWEEP_MS = 3e4;
|
|
3125
3352
|
function publicBranchState(state) {
|
|
3126
3353
|
if (state.status === "ready")
|
|
3127
3354
|
return { status: "ready" };
|
|
@@ -3131,7 +3358,11 @@ function createBranchRegistry(options) {
|
|
|
3131
3358
|
const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
|
|
3132
3359
|
const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
|
|
3133
3360
|
const inflight = /* @__PURE__ */ new Map();
|
|
3361
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
3362
|
+
const lastActivity = /* @__PURE__ */ new Map();
|
|
3363
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
3134
3364
|
const boot = options.startWorktree ?? startWorktree;
|
|
3365
|
+
const proxy = options.startProxy ?? startProxyServer;
|
|
3135
3366
|
let closed = false;
|
|
3136
3367
|
let stopPromise = null;
|
|
3137
3368
|
const transition = (title, state) => {
|
|
@@ -3143,6 +3374,43 @@ function createBranchRegistry(options) {
|
|
|
3143
3374
|
options.onChange?.(title, state);
|
|
3144
3375
|
return state;
|
|
3145
3376
|
};
|
|
3377
|
+
const stopReady = (title, state, toIdle) => {
|
|
3378
|
+
const current = stopping.get(title);
|
|
3379
|
+
if (current !== void 0)
|
|
3380
|
+
return current;
|
|
3381
|
+
const pending = (async () => {
|
|
3382
|
+
await proxies.get(title)?.close().catch(() => {
|
|
3383
|
+
});
|
|
3384
|
+
await state.worktree.stop().catch(() => {
|
|
3385
|
+
});
|
|
3386
|
+
proxies.delete(title);
|
|
3387
|
+
lastActivity.delete(title);
|
|
3388
|
+
if (toIdle && !closed && states.get(title) === state) {
|
|
3389
|
+
transition(title, { status: "idle" });
|
|
3390
|
+
}
|
|
3391
|
+
})().finally(() => {
|
|
3392
|
+
stopping.delete(title);
|
|
3393
|
+
});
|
|
3394
|
+
stopping.set(title, pending);
|
|
3395
|
+
return pending;
|
|
3396
|
+
};
|
|
3397
|
+
const sweep = () => {
|
|
3398
|
+
const now = Date.now();
|
|
3399
|
+
for (const [title, state] of states) {
|
|
3400
|
+
const branchProxy = proxies.get(title);
|
|
3401
|
+
if (state.status !== "ready" || branchProxy === void 0 || stopping.has(title))
|
|
3402
|
+
continue;
|
|
3403
|
+
if (branchProxy.active()) {
|
|
3404
|
+
lastActivity.set(title, now);
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3407
|
+
const seen = lastActivity.get(title) ?? now;
|
|
3408
|
+
if (now - seen >= BRANCH_IDLE_MS)
|
|
3409
|
+
void stopReady(title, state, true);
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
const sweepTimer = setInterval(sweep, BRANCH_SWEEP_MS);
|
|
3413
|
+
sweepTimer.unref();
|
|
3146
3414
|
const begin = (title) => {
|
|
3147
3415
|
const preview = previews.get(title);
|
|
3148
3416
|
const current = states.get(title);
|
|
@@ -3170,9 +3438,21 @@ function createBranchRegistry(options) {
|
|
|
3170
3438
|
} catch (error) {
|
|
3171
3439
|
checkout = Promise.reject(error);
|
|
3172
3440
|
}
|
|
3173
|
-
const starting = checkout.then((worktree) => {
|
|
3441
|
+
const starting = checkout.then(async (worktree) => {
|
|
3174
3442
|
transition(title, { status: "starting", phase: "starting" });
|
|
3175
|
-
|
|
3443
|
+
try {
|
|
3444
|
+
const branchProxy = await proxy({
|
|
3445
|
+
target: worktree.url,
|
|
3446
|
+
onActivity: () => lastActivity.set(title, Date.now())
|
|
3447
|
+
});
|
|
3448
|
+
proxies.set(title, branchProxy);
|
|
3449
|
+
lastActivity.set(title, Date.now());
|
|
3450
|
+
return transition(title, { status: "ready", worktree });
|
|
3451
|
+
} catch (error) {
|
|
3452
|
+
await worktree.stop().catch(() => {
|
|
3453
|
+
});
|
|
3454
|
+
throw error;
|
|
3455
|
+
}
|
|
3176
3456
|
}).catch((error) => transition(title, {
|
|
3177
3457
|
status: "failed",
|
|
3178
3458
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -3184,15 +3464,17 @@ function createBranchRegistry(options) {
|
|
|
3184
3464
|
};
|
|
3185
3465
|
return {
|
|
3186
3466
|
state: (title) => states.get(title),
|
|
3467
|
+
url: (title) => proxies.get(title)?.url,
|
|
3187
3468
|
start: begin,
|
|
3188
3469
|
stop: () => {
|
|
3189
3470
|
if (stopPromise !== null)
|
|
3190
3471
|
return stopPromise;
|
|
3191
3472
|
closed = true;
|
|
3473
|
+
clearInterval(sweepTimer);
|
|
3192
3474
|
stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3475
|
+
await Promise.allSettled([...stopping.values()]);
|
|
3476
|
+
const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
|
|
3477
|
+
await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
|
|
3196
3478
|
});
|
|
3197
3479
|
return stopPromise;
|
|
3198
3480
|
}
|
|
@@ -3487,10 +3769,10 @@ function scope(leglasCommand, quotedTitle) {
|
|
|
3487
3769
|
|
|
3488
3770
|
This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
|
|
3489
3771
|
|
|
3490
|
-
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on.`;
|
|
3772
|
+
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on. A shared script may gain one small per-direction override, at the point it reads what it renders, that defaults to what it renders today; every other direction then renders exactly as before, so that counts as additive.`;
|
|
3491
3773
|
}
|
|
3492
3774
|
function capturedBlock(captured) {
|
|
3493
|
-
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.skipped === null)
|
|
3775
|
+
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.hydration === null && captured.skipped === null)
|
|
3494
3776
|
return "";
|
|
3495
3777
|
const lines2 = [];
|
|
3496
3778
|
const frames = captured.attachments.filter((attachment) => attachment.kind === "frame" || attachment.kind === "note");
|
|
@@ -3522,6 +3804,9 @@ function capturedBlock(captured) {
|
|
|
3522
3804
|
for (const error of captured.errors)
|
|
3523
3805
|
lines2.push(` - ${error}`);
|
|
3524
3806
|
}
|
|
3807
|
+
if (captured.hydration !== null) {
|
|
3808
|
+
lines2.push(`After load, ${captured.hydration.framework} rebuilt this page in the browser from the app's own JavaScript and data (${captured.hydration.message}). Markup edited in the served HTML shows for a moment and is then replaced, so make the change where that JavaScript gets what it renders: the data or source it reads, or a per-direction override that a shared script reads with the original as its default. Look at the result a few seconds after load, not at first paint.`);
|
|
3809
|
+
}
|
|
3525
3810
|
if (captured.skipped !== null) {
|
|
3526
3811
|
lines2.push(`(${captured.skipped} Use the live preview instead.)`);
|
|
3527
3812
|
}
|
|
@@ -5289,6 +5574,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
5289
5574
|
|
|
5290
5575
|
// ../server/dist/server.js
|
|
5291
5576
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5577
|
+
import { createHash as createHash2 } from "crypto";
|
|
5292
5578
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5293
5579
|
import http2 from "http";
|
|
5294
5580
|
import net3 from "net";
|
|
@@ -5367,6 +5653,30 @@ function sendJson(res, status, body) {
|
|
|
5367
5653
|
});
|
|
5368
5654
|
res.end(payload);
|
|
5369
5655
|
}
|
|
5656
|
+
function etagMatches(value, etag) {
|
|
5657
|
+
if (value === void 0)
|
|
5658
|
+
return false;
|
|
5659
|
+
const values = Array.isArray(value) ? value : [value];
|
|
5660
|
+
return values.some((header) => header.split(",").some((candidate) => {
|
|
5661
|
+
const tag = candidate.trim();
|
|
5662
|
+
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
5663
|
+
}));
|
|
5664
|
+
}
|
|
5665
|
+
function sendConditionalJson(req, res, body) {
|
|
5666
|
+
const payload = JSON.stringify(body);
|
|
5667
|
+
const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
|
|
5668
|
+
if (etagMatches(req.headers["if-none-match"], etag)) {
|
|
5669
|
+
res.writeHead(304, { etag, "cache-control": "private, no-cache" });
|
|
5670
|
+
res.end();
|
|
5671
|
+
return;
|
|
5672
|
+
}
|
|
5673
|
+
res.writeHead(200, {
|
|
5674
|
+
"content-type": "application/json; charset=utf-8",
|
|
5675
|
+
"cache-control": "private, no-cache",
|
|
5676
|
+
etag
|
|
5677
|
+
});
|
|
5678
|
+
res.end(payload);
|
|
5679
|
+
}
|
|
5370
5680
|
var CAPTURE_DEADLINE_MS = 15e3;
|
|
5371
5681
|
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
5372
5682
|
function captureSlug(title) {
|
|
@@ -5759,9 +6069,10 @@ async function startServer(options) {
|
|
|
5759
6069
|
return preview;
|
|
5760
6070
|
const state = branches.state(preview.title) ?? { status: "idle" };
|
|
5761
6071
|
const { url: route, ...withoutUrl } = preview;
|
|
6072
|
+
const branchUrl = branches.url(preview.title);
|
|
5762
6073
|
return state.status === "ready" ? {
|
|
5763
6074
|
...withoutUrl,
|
|
5764
|
-
url: `${state.worktree.url}${route}`,
|
|
6075
|
+
url: `${branchUrl ?? state.worktree.url}${route}`,
|
|
5765
6076
|
state: publicBranchState(state)
|
|
5766
6077
|
} : { ...withoutUrl, state: publicBranchState(state) };
|
|
5767
6078
|
};
|
|
@@ -5770,7 +6081,7 @@ async function startServer(options) {
|
|
|
5770
6081
|
if (preview.branch === void 0)
|
|
5771
6082
|
return preview;
|
|
5772
6083
|
const state = branches.state(preview.title);
|
|
5773
|
-
return state?.status === "ready" ? { ...preview, url: `${state.worktree.url}${preview.url}` } : null;
|
|
6084
|
+
return state?.status === "ready" ? { ...preview, url: `${branches.url(preview.title) ?? state.worktree.url}${preview.url}` } : null;
|
|
5774
6085
|
};
|
|
5775
6086
|
if (options.pool === void 0) {
|
|
5776
6087
|
void reapOrphanedBrowsers().catch(() => {
|
|
@@ -5833,7 +6144,7 @@ async function startServer(options) {
|
|
|
5833
6144
|
errors.push(notice);
|
|
5834
6145
|
return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
|
|
5835
6146
|
if (localErrors.length > 0) {
|
|
5836
|
-
return
|
|
6147
|
+
return sendConditionalJson(req, res, {
|
|
5837
6148
|
project,
|
|
5838
6149
|
devServer: target,
|
|
5839
6150
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5846,7 +6157,7 @@ async function startServer(options) {
|
|
|
5846
6157
|
const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
|
|
5847
6158
|
const known = new Set(currentBoot.map((preview) => preview.title));
|
|
5848
6159
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
5849
|
-
|
|
6160
|
+
sendConditionalJson(req, res, {
|
|
5850
6161
|
project,
|
|
5851
6162
|
devServer: target,
|
|
5852
6163
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -5854,7 +6165,7 @@ async function startServer(options) {
|
|
|
5854
6165
|
errors,
|
|
5855
6166
|
warnings: configWarnings
|
|
5856
6167
|
});
|
|
5857
|
-
}).catch(() =>
|
|
6168
|
+
}).catch(() => sendConditionalJson(req, res, {
|
|
5858
6169
|
project,
|
|
5859
6170
|
devServer: target,
|
|
5860
6171
|
scanPreviews: config?.scanPreviews ?? true,
|
|
@@ -6177,6 +6488,7 @@ async function startServer(options) {
|
|
|
6177
6488
|
height: shot.height,
|
|
6178
6489
|
viewport: result.frame.width,
|
|
6179
6490
|
errors: result.errors,
|
|
6491
|
+
hydration: result.hydration,
|
|
6180
6492
|
cut: result.cut
|
|
6181
6493
|
});
|
|
6182
6494
|
} catch (error) {
|
|
@@ -6289,7 +6601,7 @@ async function startServer(options) {
|
|
|
6289
6601
|
waiting: null,
|
|
6290
6602
|
failedIds: []
|
|
6291
6603
|
};
|
|
6292
|
-
return void readRequests(cwd).then((requests) =>
|
|
6604
|
+
return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
|
|
6293
6605
|
requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
|
|
6294
6606
|
id,
|
|
6295
6607
|
title,
|
|
@@ -6392,7 +6704,7 @@ async function startServer(options) {
|
|
|
6392
6704
|
});
|
|
6393
6705
|
}
|
|
6394
6706
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
|
|
6395
|
-
return void readAnnotations(cwd).then((annotations) =>
|
|
6707
|
+
return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
|
|
6396
6708
|
}
|
|
6397
6709
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6398
6710
|
if (!hasJsonBody(req)) {
|
|
@@ -6518,7 +6830,7 @@ async function startServer(options) {
|
|
|
6518
6830
|
});
|
|
6519
6831
|
}
|
|
6520
6832
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
6521
|
-
return void probe(target).then((reachable) =>
|
|
6833
|
+
return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
|
|
6522
6834
|
}
|
|
6523
6835
|
if (path.startsWith(`${FILES_PREFIX}/`)) {
|
|
6524
6836
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
@@ -6708,7 +7020,7 @@ async function runInit(options, deps) {
|
|
|
6708
7020
|
|
|
6709
7021
|
// src/run-keep.ts
|
|
6710
7022
|
import { existsSync as existsSync4 } from "fs";
|
|
6711
|
-
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
7023
|
+
import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6712
7024
|
import { dirname as dirname9, join as join14 } from "path";
|
|
6713
7025
|
|
|
6714
7026
|
// src/resolve-title.ts
|
|
@@ -6736,6 +7048,29 @@ function renameExport(source, to) {
|
|
|
6736
7048
|
to
|
|
6737
7049
|
);
|
|
6738
7050
|
}
|
|
7051
|
+
async function writeLogEntry(options) {
|
|
7052
|
+
const entry = composeEntry({
|
|
7053
|
+
surface: options.surface,
|
|
7054
|
+
won: options.won,
|
|
7055
|
+
previews: options.previews,
|
|
7056
|
+
requests: await readRequests(options.cwd),
|
|
7057
|
+
annotations: await readAnnotations(options.cwd),
|
|
7058
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
7059
|
+
});
|
|
7060
|
+
const dir = join14(options.cwd, options.logDir);
|
|
7061
|
+
await mkdir9(dir, { recursive: true });
|
|
7062
|
+
const file = join14(dir, `${entry.slug}.md`);
|
|
7063
|
+
await writeFile10(file, entry.markdown, "utf8");
|
|
7064
|
+
if (entry.pictures.length > 0) {
|
|
7065
|
+
const pictureDir = join14(dir, entry.slug);
|
|
7066
|
+
await mkdir9(pictureDir, { recursive: true });
|
|
7067
|
+
for (const picture of entry.pictures) {
|
|
7068
|
+
await copyFile2(join14(options.cwd, picture.from), join14(pictureDir, picture.to)).catch(() => {
|
|
7069
|
+
});
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
return `${options.logDir}/${entry.slug}.md`;
|
|
7073
|
+
}
|
|
6739
7074
|
async function runKeep(options, deps) {
|
|
6740
7075
|
const loaded = await loadConfig(options.cwd);
|
|
6741
7076
|
const local = await readLocalPreviews(options.cwd);
|
|
@@ -6764,6 +7099,20 @@ async function runKeep(options, deps) {
|
|
|
6764
7099
|
const source = await readFile13(from, "utf8");
|
|
6765
7100
|
await mkdir9(dirname9(to), { recursive: true });
|
|
6766
7101
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
7102
|
+
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
7103
|
+
let logged = null;
|
|
7104
|
+
let logError = null;
|
|
7105
|
+
try {
|
|
7106
|
+
logged = await writeLogEntry({
|
|
7107
|
+
cwd: options.cwd,
|
|
7108
|
+
logDir: loaded.config?.logDir ?? DEFAULT_LOG_DIR,
|
|
7109
|
+
surface,
|
|
7110
|
+
won: { title: resolved.title, to: plan.move.to },
|
|
7111
|
+
previews: previews.filter((preview) => plan.dropTitles.includes(preview.title))
|
|
7112
|
+
});
|
|
7113
|
+
} catch (error) {
|
|
7114
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
7115
|
+
}
|
|
6767
7116
|
await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6768
7117
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
6769
7118
|
if (options.json) {
|
|
@@ -6775,6 +7124,8 @@ async function runKeep(options, deps) {
|
|
|
6775
7124
|
exportName: plan.exportName,
|
|
6776
7125
|
removed: plan.removeDir,
|
|
6777
7126
|
droppedPreviews: dropped,
|
|
7127
|
+
logged,
|
|
7128
|
+
logError,
|
|
6778
7129
|
instructions: plan.instructions
|
|
6779
7130
|
})
|
|
6780
7131
|
);
|
|
@@ -6782,6 +7133,8 @@ async function runKeep(options, deps) {
|
|
|
6782
7133
|
}
|
|
6783
7134
|
deps.log(` kept ${plan.move.to}`);
|
|
6784
7135
|
deps.log(` removed ${plan.removeDir}`);
|
|
7136
|
+
if (logged !== null) deps.log(` logged ${logged}`);
|
|
7137
|
+
if (logError !== null) deps.error(` The decision log could not be written: ${logError}`);
|
|
6785
7138
|
if (dropped > 0) {
|
|
6786
7139
|
deps.log(` dropped ${dropped} direction${dropped === 1 ? "" : "s"} from the rail`);
|
|
6787
7140
|
}
|
|
@@ -7168,6 +7521,10 @@ async function runShow(options, deps) {
|
|
|
7168
7521
|
height: captured.height,
|
|
7169
7522
|
viewport: captured.viewport,
|
|
7170
7523
|
errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
|
|
7524
|
+
hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
|
|
7525
|
+
framework: captured.hydration.framework,
|
|
7526
|
+
message: captured.hydration.message
|
|
7527
|
+
} : null,
|
|
7171
7528
|
cut: captured.cut === true
|
|
7172
7529
|
};
|
|
7173
7530
|
}
|
|
@@ -7195,6 +7552,12 @@ async function runShow(options, deps) {
|
|
|
7195
7552
|
if (envelope2.screenshot.cut) {
|
|
7196
7553
|
deps.log(" the top of the page only; it is taller than one capture");
|
|
7197
7554
|
}
|
|
7555
|
+
if (envelope2.screenshot.hydration !== null) {
|
|
7556
|
+
deps.log(
|
|
7557
|
+
` hydration ${envelope2.screenshot.hydration.framework} rebuilt the page in the browser after load; the served markup is not what is on screen`
|
|
7558
|
+
);
|
|
7559
|
+
deps.log(` ${envelope2.screenshot.hydration.message}`);
|
|
7560
|
+
}
|
|
7198
7561
|
if (envelope2.screenshot.errors.length > 0) {
|
|
7199
7562
|
const count = envelope2.screenshot.errors.length;
|
|
7200
7563
|
deps.log(` console ${count} ${count === 1 ? "error" : "errors"} on load`);
|