leglas 1.2.0 → 1.3.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 +49 -39
- package/dist/args.d.ts +5 -0
- package/dist/bin-start.d.ts +5 -0
- package/dist/bin.js +1933 -407
- package/dist/index.js +1555 -171
- package/dist/run-explore-build.d.ts +27 -0
- package/dist/run-watch.d.ts +1 -0
- package/dist/shell/assets/index-C59b2VHc.css +1 -0
- package/dist/shell/assets/index-CcHf_UOb.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-DfiRUTCS.css +0 -1
- package/dist/shell/assets/index-lVZbvEvf.js +0 -14
package/dist/bin.js
CHANGED
|
@@ -169,8 +169,13 @@ function parseClassify(rest) {
|
|
|
169
169
|
function parseWatch(rest) {
|
|
170
170
|
let run4;
|
|
171
171
|
let port;
|
|
172
|
+
let json = false;
|
|
172
173
|
for (let index = 0; index < rest.length; index += 1) {
|
|
173
174
|
const argument = rest[index];
|
|
175
|
+
if (argument === "--json") {
|
|
176
|
+
json = true;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
174
179
|
const equals = argument.indexOf("=");
|
|
175
180
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
176
181
|
if (flag !== "--run" && flag !== "--port") {
|
|
@@ -191,7 +196,7 @@ function parseWatch(rest) {
|
|
|
191
196
|
if ("error" in parsed2) return { kind: "error", message: parsed2.error };
|
|
192
197
|
port = parsed2.port;
|
|
193
198
|
}
|
|
194
|
-
return { kind: "watch", run: run4, port };
|
|
199
|
+
return { kind: "watch", run: run4, port, json };
|
|
195
200
|
}
|
|
196
201
|
function parseShare(rest) {
|
|
197
202
|
const titles = [];
|
|
@@ -330,12 +335,38 @@ function parseArgs(argv) {
|
|
|
330
335
|
let count = 3;
|
|
331
336
|
let basedOn = null;
|
|
332
337
|
let json = false;
|
|
338
|
+
let build = false;
|
|
339
|
+
let brief = null;
|
|
340
|
+
let port = null;
|
|
333
341
|
for (let index = 0; index < rest.length; index += 1) {
|
|
334
342
|
const argument = rest[index];
|
|
335
343
|
if (argument === "--json") {
|
|
336
344
|
json = true;
|
|
337
345
|
continue;
|
|
338
346
|
}
|
|
347
|
+
if (argument === "--build") {
|
|
348
|
+
build = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (argument === "--brief" || argument.startsWith("--brief=")) {
|
|
352
|
+
const raw = argument.includes("=") ? argument.slice(argument.indexOf("=") + 1) : rest[index += 1];
|
|
353
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
354
|
+
return {
|
|
355
|
+
kind: "error",
|
|
356
|
+
message: '--brief needs a description, for example --brief "A hero for a cooking app".'
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
brief = raw;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (argument === "--port" || argument.startsWith("--port=")) {
|
|
363
|
+
const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
|
|
364
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) {
|
|
365
|
+
return { kind: "error", message: "--port needs a number, for example --port 4000." };
|
|
366
|
+
}
|
|
367
|
+
port = Number(raw);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
339
370
|
if (argument === "--count" || argument.startsWith("--count=")) {
|
|
340
371
|
const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
|
|
341
372
|
if (raw === void 0 || !/^\d+$/.test(raw)) {
|
|
@@ -369,7 +400,19 @@ function parseArgs(argv) {
|
|
|
369
400
|
message: "leglas explore needs a surface name, for example: npx leglas explore hero --count 6"
|
|
370
401
|
};
|
|
371
402
|
}
|
|
372
|
-
|
|
403
|
+
if (build && brief === null && basedOn === null) {
|
|
404
|
+
return {
|
|
405
|
+
kind: "error",
|
|
406
|
+
message: 'leglas explore --build needs a brief, for example: npx leglas explore hero --build --brief "A hero for a cooking app". To vary a direction instead, name it with --based-on "<title>".'
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (!build && brief !== null) {
|
|
410
|
+
return {
|
|
411
|
+
kind: "error",
|
|
412
|
+
message: "--brief goes with --build: add --build to have Leglas build the set."
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return { kind: "explore", surface, count, basedOn, json, build, brief, port };
|
|
373
416
|
}
|
|
374
417
|
if (argv[0] === "requests") {
|
|
375
418
|
const rest = argv.slice(1);
|
|
@@ -515,8 +558,8 @@ function parseArgs(argv) {
|
|
|
515
558
|
}
|
|
516
559
|
|
|
517
560
|
// src/run-classify.ts
|
|
518
|
-
import { stat as
|
|
519
|
-
import { join as
|
|
561
|
+
import { stat as stat5 } from "fs/promises";
|
|
562
|
+
import { join as join18 } from "path";
|
|
520
563
|
|
|
521
564
|
// ../server/dist/log.js
|
|
522
565
|
var DEFAULT_LOG_DIR = "design-log";
|
|
@@ -597,8 +640,8 @@ function composeEntry(input) {
|
|
|
597
640
|
}
|
|
598
641
|
|
|
599
642
|
// ../server/dist/json.js
|
|
600
|
-
function parseJson(
|
|
601
|
-
return JSON.parse(
|
|
643
|
+
function parseJson(text3) {
|
|
644
|
+
return JSON.parse(text3);
|
|
602
645
|
}
|
|
603
646
|
function isJsonRecord(value) {
|
|
604
647
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -918,7 +961,7 @@ var KNOWN_AGENTS = {
|
|
|
918
961
|
// moves that precondition and only that: `-s workspace-write` still
|
|
919
962
|
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
920
963
|
// and in a git repository the flag does nothing at all.
|
|
921
|
-
args: (prompt, effort = null,
|
|
964
|
+
args: (prompt, effort = null, images2 = []) => [
|
|
922
965
|
"exec",
|
|
923
966
|
"--json",
|
|
924
967
|
...CODEX_WORKSPACE_CONFIG,
|
|
@@ -926,23 +969,23 @@ var KNOWN_AGENTS = {
|
|
|
926
969
|
"-s",
|
|
927
970
|
"workspace-write",
|
|
928
971
|
"--skip-git-repo-check",
|
|
929
|
-
...
|
|
972
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
930
973
|
prompt
|
|
931
974
|
],
|
|
932
|
-
terminalArgs: (prompt, effort = null,
|
|
975
|
+
terminalArgs: (prompt, effort = null, images2 = []) => [
|
|
933
976
|
"exec",
|
|
934
977
|
...CODEX_WORKSPACE_CONFIG,
|
|
935
978
|
...codexEffortConfig(effort),
|
|
936
979
|
"-s",
|
|
937
980
|
"workspace-write",
|
|
938
981
|
"--skip-git-repo-check",
|
|
939
|
-
...
|
|
982
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
940
983
|
prompt
|
|
941
984
|
],
|
|
942
985
|
// No sandbox flag here: `codex exec resume` refuses it and inherits the
|
|
943
986
|
// session's own sandbox, which the first turn set to workspace-write. The
|
|
944
987
|
// repository check is per invocation, so resume needs the flag of its own.
|
|
945
|
-
resumeArgs: (sessionId, prompt, effort = null,
|
|
988
|
+
resumeArgs: (sessionId, prompt, effort = null, images2 = []) => [
|
|
946
989
|
"exec",
|
|
947
990
|
"resume",
|
|
948
991
|
sessionId,
|
|
@@ -950,7 +993,7 @@ var KNOWN_AGENTS = {
|
|
|
950
993
|
...CODEX_WORKSPACE_CONFIG,
|
|
951
994
|
...codexEffortConfig(effort),
|
|
952
995
|
"--skip-git-repo-check",
|
|
953
|
-
...
|
|
996
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
954
997
|
prompt
|
|
955
998
|
],
|
|
956
999
|
sessionFrom: (event) => event.type === "thread.started" && isString(event.thread_id) ? event.thread_id : null,
|
|
@@ -1785,12 +1828,12 @@ function buildNumber(entry) {
|
|
|
1785
1828
|
const digits = /(\d+)\s*$/.exec(entry)?.[1];
|
|
1786
1829
|
return digits === void 0 ? 0 : Number(digits);
|
|
1787
1830
|
}
|
|
1788
|
-
function cacheRoots(platform, home,
|
|
1831
|
+
function cacheRoots(platform, home, readdir7 = readableDirectories) {
|
|
1789
1832
|
const playwright = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
1790
1833
|
const puppeteer = join4(home, ".cache", "puppeteer");
|
|
1791
1834
|
const newestFirst = (dir, keep = () => true) => ({
|
|
1792
1835
|
root: dir,
|
|
1793
|
-
entries:
|
|
1836
|
+
entries: readdir7(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
|
|
1794
1837
|
});
|
|
1795
1838
|
return [
|
|
1796
1839
|
newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
|
|
@@ -1811,13 +1854,13 @@ function findBrowser(search = {}) {
|
|
|
1811
1854
|
const home = search.home ?? homedir();
|
|
1812
1855
|
const exists = search.exists ?? existsSync2;
|
|
1813
1856
|
const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
|
|
1814
|
-
const
|
|
1857
|
+
const readdir7 = search.readdir ?? readableDirectories;
|
|
1815
1858
|
const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
|
|
1816
1859
|
for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
|
|
1817
1860
|
if (isString(candidate) && candidate !== "" && exists(candidate))
|
|
1818
1861
|
return candidate;
|
|
1819
1862
|
}
|
|
1820
|
-
const caches = cacheRoots(platform, home,
|
|
1863
|
+
const caches = cacheRoots(platform, home, readdir7);
|
|
1821
1864
|
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join4(root, entry, ...rest)))));
|
|
1822
1865
|
if (shell !== null)
|
|
1823
1866
|
return shell;
|
|
@@ -1847,7 +1890,7 @@ function findBrowser(search = {}) {
|
|
|
1847
1890
|
}
|
|
1848
1891
|
if (platform === "darwin" || platform === "linux") {
|
|
1849
1892
|
const playwrightRoot = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
1850
|
-
const playwright =
|
|
1893
|
+
const playwright = readdir7(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
1851
1894
|
const root = join4(playwrightRoot, entry);
|
|
1852
1895
|
return [
|
|
1853
1896
|
...FOR_TESTING.map((rest) => join4(root, ...rest)),
|
|
@@ -1864,7 +1907,7 @@ function findBrowser(search = {}) {
|
|
|
1864
1907
|
const puppeteerCache = join4(home, ".cache", "puppeteer");
|
|
1865
1908
|
for (const kind of ["chrome", "chrome-headless-shell"]) {
|
|
1866
1909
|
const kindRoot = join4(puppeteerCache, kind);
|
|
1867
|
-
const found = firstExisting(
|
|
1910
|
+
const found = firstExisting(readdir7(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
1868
1911
|
const root = join4(kindRoot, entry);
|
|
1869
1912
|
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join4(root, ...rest));
|
|
1870
1913
|
}));
|
|
@@ -1904,7 +1947,7 @@ async function connectWebSocket(url) {
|
|
|
1904
1947
|
socket.addEventListener("error", failed, { once: true });
|
|
1905
1948
|
});
|
|
1906
1949
|
return {
|
|
1907
|
-
send: (
|
|
1950
|
+
send: (text3) => socket.send(text3),
|
|
1908
1951
|
onMessage: (listener) => socket.addEventListener("message", (event) => listener(String(event.data))),
|
|
1909
1952
|
onClose: (listener) => socket.addEventListener("close", listener),
|
|
1910
1953
|
close: () => socket.close()
|
|
@@ -2129,10 +2172,10 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2129
2172
|
socketClosed = true;
|
|
2130
2173
|
rejectPending();
|
|
2131
2174
|
});
|
|
2132
|
-
socket.onMessage((
|
|
2175
|
+
socket.onMessage((text3) => {
|
|
2133
2176
|
let message2;
|
|
2134
2177
|
try {
|
|
2135
|
-
const parsed2 = parseJson(
|
|
2178
|
+
const parsed2 = parseJson(text3);
|
|
2136
2179
|
if (!isJsonRecord(parsed2))
|
|
2137
2180
|
return;
|
|
2138
2181
|
message2 = parsed2;
|
|
@@ -2427,6 +2470,12 @@ var LOCATOR = `(function (selector, text, tag) {
|
|
|
2427
2470
|
if (r.width === 0 && r.height === 0) return null;
|
|
2428
2471
|
return { x: r.left + window.scrollX, y: r.top + window.scrollY, width: r.width, height: r.height };
|
|
2429
2472
|
})`;
|
|
2473
|
+
var DRAWN_WITH = /* @__PURE__ */ new Set(["Script", "Stylesheet", "Font", "Image"]);
|
|
2474
|
+
var LAID_OUT = `(async () => {
|
|
2475
|
+
await new Promise((next) => requestAnimationFrame(() => requestAnimationFrame(next)));
|
|
2476
|
+
if (document.fonts) await document.fonts.ready;
|
|
2477
|
+
return true;
|
|
2478
|
+
})()`;
|
|
2430
2479
|
function clamp(value, low, high) {
|
|
2431
2480
|
return Math.min(high, Math.max(low, value));
|
|
2432
2481
|
}
|
|
@@ -2491,6 +2540,16 @@ function validBox(value) {
|
|
|
2491
2540
|
return false;
|
|
2492
2541
|
return [value.x, value.y, value.width, value.height].every((entry) => isNumber(entry) && Number.isFinite(entry));
|
|
2493
2542
|
}
|
|
2543
|
+
function resourced(text3, url) {
|
|
2544
|
+
const line = String(text3 ?? "");
|
|
2545
|
+
if (!isString(url) || url === "")
|
|
2546
|
+
return line;
|
|
2547
|
+
try {
|
|
2548
|
+
return `${line} (${new URL(url).pathname})`;
|
|
2549
|
+
} catch {
|
|
2550
|
+
return line;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2494
2553
|
function locatorExpression(focus) {
|
|
2495
2554
|
return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
|
|
2496
2555
|
}
|
|
@@ -2508,6 +2567,22 @@ async function render(page, input) {
|
|
|
2508
2567
|
errors.push(message2);
|
|
2509
2568
|
};
|
|
2510
2569
|
let documentStatus = null;
|
|
2570
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2571
|
+
const afterLoadScripts = /* @__PURE__ */ new Set();
|
|
2572
|
+
let loadFired = false;
|
|
2573
|
+
let scriptLanded = null;
|
|
2574
|
+
let asked = 0;
|
|
2575
|
+
let emptied = () => {
|
|
2576
|
+
};
|
|
2577
|
+
const received = (id) => {
|
|
2578
|
+
if (afterLoadScripts.delete(id))
|
|
2579
|
+
scriptLanded = Date.now();
|
|
2580
|
+
if (pending.delete(id) && pending.size === 0)
|
|
2581
|
+
emptied();
|
|
2582
|
+
};
|
|
2583
|
+
const landed = () => pending.size === 0 ? Promise.resolve() : new Promise((resolve5) => {
|
|
2584
|
+
emptied = resolve5;
|
|
2585
|
+
});
|
|
2511
2586
|
const unlisten = [
|
|
2512
2587
|
page.on("Network.responseReceived", (params) => {
|
|
2513
2588
|
if (documentStatus !== null || params?.type !== "Document")
|
|
@@ -2516,6 +2591,25 @@ async function render(page, input) {
|
|
|
2516
2591
|
if (isNumber(status))
|
|
2517
2592
|
documentStatus = status;
|
|
2518
2593
|
}),
|
|
2594
|
+
page.on("Network.requestWillBeSent", (params) => {
|
|
2595
|
+
const id = params?.requestId;
|
|
2596
|
+
if (!DRAWN_WITH.has(params?.type) || !isString(id))
|
|
2597
|
+
return;
|
|
2598
|
+
if (!pending.has(id)) {
|
|
2599
|
+
asked += 1;
|
|
2600
|
+
if (loadFired && params?.type === "Script")
|
|
2601
|
+
afterLoadScripts.add(id);
|
|
2602
|
+
}
|
|
2603
|
+
pending.add(id);
|
|
2604
|
+
}),
|
|
2605
|
+
page.on("Network.loadingFinished", (params) => {
|
|
2606
|
+
if (isString(params?.requestId))
|
|
2607
|
+
received(params.requestId);
|
|
2608
|
+
}),
|
|
2609
|
+
page.on("Network.loadingFailed", (params) => {
|
|
2610
|
+
if (isString(params?.requestId))
|
|
2611
|
+
received(params.requestId);
|
|
2612
|
+
}),
|
|
2519
2613
|
page.on("Runtime.exceptionThrown", (params) => remember(params?.exceptionDetails?.exception?.description ?? params?.exceptionDetails?.text)),
|
|
2520
2614
|
page.on("Runtime.consoleAPICalled", (params) => {
|
|
2521
2615
|
if (params?.type !== "error")
|
|
@@ -2524,7 +2618,7 @@ async function render(page, input) {
|
|
|
2524
2618
|
}),
|
|
2525
2619
|
page.on("Log.entryAdded", (params) => {
|
|
2526
2620
|
if (params?.entry?.level === "error")
|
|
2527
|
-
remember(params.entry.text);
|
|
2621
|
+
remember(resourced(params.entry.text, params.entry.url));
|
|
2528
2622
|
})
|
|
2529
2623
|
];
|
|
2530
2624
|
try {
|
|
@@ -2542,7 +2636,10 @@ async function render(page, input) {
|
|
|
2542
2636
|
const load = new Promise((resolve5) => {
|
|
2543
2637
|
loaded = resolve5;
|
|
2544
2638
|
});
|
|
2545
|
-
const stopLoad = page.on("Page.loadEventFired", () =>
|
|
2639
|
+
const stopLoad = page.on("Page.loadEventFired", () => {
|
|
2640
|
+
loadFired = true;
|
|
2641
|
+
loaded();
|
|
2642
|
+
});
|
|
2546
2643
|
unlisten.push(stopLoad);
|
|
2547
2644
|
const navigation = await page.send("Page.navigate", { url: input.url });
|
|
2548
2645
|
if (isString(navigation.errorText) && navigation.errorText !== "") {
|
|
@@ -2553,12 +2650,28 @@ async function render(page, input) {
|
|
|
2553
2650
|
if (documentStatus !== null && documentStatus >= 500) {
|
|
2554
2651
|
throw new Error(`The page did not load: the app answered HTTP ${documentStatus}.`);
|
|
2555
2652
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2653
|
+
const until = Date.now() + 2e3;
|
|
2654
|
+
let waitedForScript = null;
|
|
2655
|
+
while (Date.now() < until) {
|
|
2656
|
+
await bounded(landed(), until - Date.now(), void 0);
|
|
2657
|
+
const before = asked;
|
|
2658
|
+
const answered = await bounded(page.send("Runtime.evaluate", {
|
|
2659
|
+
expression: LAID_OUT,
|
|
2660
|
+
awaitPromise: true,
|
|
2661
|
+
returnByValue: true
|
|
2662
|
+
}), Math.max(0, until - Date.now()), void 0).then(() => true, () => false);
|
|
2663
|
+
if (!answered)
|
|
2664
|
+
break;
|
|
2665
|
+
if (pending.size !== 0 || asked !== before)
|
|
2666
|
+
continue;
|
|
2667
|
+
const latestScript = scriptLanded;
|
|
2668
|
+
if (latestScript === null || latestScript === waitedForScript)
|
|
2669
|
+
break;
|
|
2670
|
+
const remaining = Math.min(latestScript + 300, until) - Date.now();
|
|
2671
|
+
if (remaining > 0)
|
|
2672
|
+
await new Promise((resolve5) => setTimeout(resolve5, remaining));
|
|
2673
|
+
waitedForScript = latestScript;
|
|
2674
|
+
}
|
|
2562
2675
|
await bounded(page.send("Runtime.evaluate", {
|
|
2563
2676
|
expression: `(async () => {
|
|
2564
2677
|
// The page stops itself before the caller's deadline can. Without
|
|
@@ -2928,8 +3041,8 @@ async function rehomeCaptures(cwd, from, to, attachments) {
|
|
|
2928
3041
|
file: attachment.file.startsWith(prefix) ? `${CAPTURES_DIR}/${to}/${attachment.file.slice(prefix.length)}` : attachment.file
|
|
2929
3042
|
}));
|
|
2930
3043
|
}
|
|
2931
|
-
function rehomeText(
|
|
2932
|
-
return
|
|
3044
|
+
function rehomeText(text3, from, to) {
|
|
3045
|
+
return text3.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
|
|
2933
3046
|
}
|
|
2934
3047
|
async function ownCapturesRoot(cwd) {
|
|
2935
3048
|
const root = join5(cwd, CAPTURES_DIR);
|
|
@@ -3797,13 +3910,13 @@ function imageMediaType(path) {
|
|
|
3797
3910
|
return "image/gif";
|
|
3798
3911
|
return null;
|
|
3799
3912
|
}
|
|
3800
|
-
async function claudeContent(prompt,
|
|
3801
|
-
if (
|
|
3913
|
+
async function claudeContent(prompt, images2) {
|
|
3914
|
+
if (images2.length === 0)
|
|
3802
3915
|
return prompt;
|
|
3803
3916
|
const blocks = [
|
|
3804
3917
|
{ type: "text", text: prompt }
|
|
3805
3918
|
];
|
|
3806
|
-
for (const path of
|
|
3919
|
+
for (const path of images2) {
|
|
3807
3920
|
const mediaType = imageMediaType(path);
|
|
3808
3921
|
if (mediaType === null)
|
|
3809
3922
|
continue;
|
|
@@ -4706,7 +4819,7 @@ function isStateUpdater(value) {
|
|
|
4706
4819
|
return typeof value === "function";
|
|
4707
4820
|
}
|
|
4708
4821
|
var SESSION_TURNS_CAP = 8;
|
|
4709
|
-
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
4822
|
+
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images2 = []) {
|
|
4710
4823
|
if (choice.agent === null)
|
|
4711
4824
|
return null;
|
|
4712
4825
|
if (choice.agent === "custom") {
|
|
@@ -4733,24 +4846,24 @@ function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
|
4733
4846
|
agent: choice.agent,
|
|
4734
4847
|
name: adapter.name,
|
|
4735
4848
|
command: adapter.binary,
|
|
4736
|
-
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort,
|
|
4849
|
+
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort, images2), ...allow],
|
|
4737
4850
|
prompt,
|
|
4738
4851
|
effort: choice.effort,
|
|
4739
4852
|
sessionId,
|
|
4740
4853
|
resumed: true,
|
|
4741
|
-
images
|
|
4854
|
+
images: images2
|
|
4742
4855
|
};
|
|
4743
4856
|
}
|
|
4744
4857
|
return {
|
|
4745
4858
|
agent: choice.agent,
|
|
4746
4859
|
name: adapter.name,
|
|
4747
4860
|
command: adapter.binary,
|
|
4748
|
-
args: [...adapter.args(prompt, choice.effort,
|
|
4861
|
+
args: [...adapter.args(prompt, choice.effort, images2), ...allow],
|
|
4749
4862
|
prompt,
|
|
4750
4863
|
effort: choice.effort,
|
|
4751
4864
|
sessionId: null,
|
|
4752
4865
|
resumed: false,
|
|
4753
|
-
images
|
|
4866
|
+
images: images2
|
|
4754
4867
|
};
|
|
4755
4868
|
}
|
|
4756
4869
|
function lineReader(stream, onLine) {
|
|
@@ -5036,8 +5149,8 @@ function startRunner(options) {
|
|
|
5036
5149
|
`${options.leglasCommand} show`,
|
|
5037
5150
|
...request.mode === "variant" ? [registrationCommand(options.leglasCommand)] : []
|
|
5038
5151
|
];
|
|
5039
|
-
const
|
|
5040
|
-
let resolved = resolveCommand(choice, request.prompt, choice.agent === null ? null : resumable(choice.agent), allowedCommands,
|
|
5152
|
+
const images2 = (await Promise.all((request.attachments ?? []).map(async (attachment) => await isOwnCapture(options.cwd, attachment.file) ? resolve2(options.cwd, attachment.file) : null))).filter((image) => image !== null);
|
|
5153
|
+
let resolved = resolveCommand(choice, request.prompt, choice.agent === null ? null : resumable(choice.agent), allowedCommands, images2);
|
|
5041
5154
|
if (resolved === null)
|
|
5042
5155
|
return;
|
|
5043
5156
|
const lines2 = [];
|
|
@@ -5083,7 +5196,7 @@ function startRunner(options) {
|
|
|
5083
5196
|
// nothing says "cancelled". Stopped still means stopped.
|
|
5084
5197
|
!stopped) {
|
|
5085
5198
|
sessions.delete(resolved.agent);
|
|
5086
|
-
const cold = resolveCommand(choice, request.prompt, null, allowedCommands,
|
|
5199
|
+
const cold = resolveCommand(choice, request.prompt, null, allowedCommands, images2);
|
|
5087
5200
|
if (cold !== null) {
|
|
5088
5201
|
resolved = cold;
|
|
5089
5202
|
observed.sessionId = null;
|
|
@@ -6777,10 +6890,10 @@ function resolveTitle(input, titles, renames) {
|
|
|
6777
6890
|
// ../server/dist/server.js
|
|
6778
6891
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
6779
6892
|
import { createHash as createHash2 } from "crypto";
|
|
6780
|
-
import { mkdir as mkdir8, readdir as
|
|
6893
|
+
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile9 } from "fs/promises";
|
|
6781
6894
|
import http4 from "http";
|
|
6782
6895
|
import net3 from "net";
|
|
6783
|
-
import { basename as basename3, dirname as
|
|
6896
|
+
import { basename as basename3, dirname as dirname10, extname as extname3, join as join16, normalize as normalize2, relative as relative4 } from "path";
|
|
6784
6897
|
|
|
6785
6898
|
// ../server/dist/frame-policy.js
|
|
6786
6899
|
var FRAMING_TIMEOUT_MS = 4e3;
|
|
@@ -6891,195 +7004,1330 @@ async function checkFraming(target, embedder, fetcher = fetch) {
|
|
|
6891
7004
|
}
|
|
6892
7005
|
}
|
|
6893
7006
|
|
|
6894
|
-
// ../server/dist/
|
|
6895
|
-
import {
|
|
6896
|
-
import {
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
await rename2(temporary, path).catch(async (cause) => {
|
|
6911
|
-
await rm4(temporary, { force: true }).catch(() => {
|
|
6912
|
-
});
|
|
6913
|
-
throw cause;
|
|
6914
|
-
});
|
|
7007
|
+
// ../server/dist/generation/generation.js
|
|
7008
|
+
import { spawn as nodeSpawn4 } from "child_process";
|
|
7009
|
+
import { readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
7010
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
7011
|
+
|
|
7012
|
+
// ../server/dist/generation/claude.js
|
|
7013
|
+
var GENERATION_EFFORT = "medium";
|
|
7014
|
+
var RESTRICTED = [
|
|
7015
|
+
"--output-format",
|
|
7016
|
+
"stream-json",
|
|
7017
|
+
"--verbose",
|
|
7018
|
+
"--restricted",
|
|
7019
|
+
"--strict-mcp-config"
|
|
7020
|
+
];
|
|
7021
|
+
function planArgs(prompt, effort = GENERATION_EFFORT) {
|
|
7022
|
+
return ["-p", prompt, ...RESTRICTED, "--tools", "", "--effort", effort];
|
|
6915
7023
|
}
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
7024
|
+
function buildArgs(prompt, effort = GENERATION_EFFORT) {
|
|
7025
|
+
return [
|
|
7026
|
+
"-p",
|
|
7027
|
+
prompt,
|
|
7028
|
+
...RESTRICTED,
|
|
7029
|
+
"--tools",
|
|
7030
|
+
"Read,Write,Edit",
|
|
7031
|
+
"--permission-mode",
|
|
7032
|
+
"acceptEdits",
|
|
7033
|
+
"--effort",
|
|
7034
|
+
effort
|
|
7035
|
+
];
|
|
6925
7036
|
}
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
const
|
|
6929
|
-
if (
|
|
6930
|
-
|
|
7037
|
+
function resultText(lines2) {
|
|
7038
|
+
for (let index = lines2.length - 1; index >= 0; index -= 1) {
|
|
7039
|
+
const line = lines2[index]?.trim() ?? "";
|
|
7040
|
+
if (!line.startsWith("{"))
|
|
7041
|
+
continue;
|
|
7042
|
+
try {
|
|
7043
|
+
const event = parseJson(line);
|
|
7044
|
+
if (isJsonRecord(event) && event.type === "result" && isString(event.result))
|
|
7045
|
+
return event.result;
|
|
7046
|
+
} catch {
|
|
7047
|
+
}
|
|
6931
7048
|
}
|
|
6932
|
-
|
|
7049
|
+
return null;
|
|
6933
7050
|
}
|
|
6934
7051
|
|
|
6935
|
-
// ../server/dist/
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
var
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
"
|
|
6943
|
-
"
|
|
6944
|
-
"
|
|
6945
|
-
|
|
6946
|
-
"
|
|
6947
|
-
"
|
|
6948
|
-
"
|
|
6949
|
-
".
|
|
6950
|
-
"
|
|
6951
|
-
".
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
return false;
|
|
6973
|
-
const values = Array.isArray(value) ? value : [value];
|
|
6974
|
-
return values.some((header) => header.split(",").some((candidate) => {
|
|
6975
|
-
const tag = candidate.trim();
|
|
6976
|
-
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
6977
|
-
}));
|
|
6978
|
-
}
|
|
6979
|
-
function sendConditionalJson(req, res, body) {
|
|
6980
|
-
const payload = JSON.stringify(body);
|
|
6981
|
-
const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
|
|
6982
|
-
if (etagMatches(req.headers["if-none-match"], etag)) {
|
|
6983
|
-
res.writeHead(304, { etag, "cache-control": "private, no-cache" });
|
|
6984
|
-
res.end();
|
|
6985
|
-
return;
|
|
7052
|
+
// ../server/dist/generation/codex.js
|
|
7053
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
7054
|
+
import { homedir as homedir2 } from "os";
|
|
7055
|
+
import { join as join11 } from "path";
|
|
7056
|
+
var RESTRICTED2 = [
|
|
7057
|
+
"exec",
|
|
7058
|
+
"--json",
|
|
7059
|
+
"--ephemeral",
|
|
7060
|
+
"--skip-git-repo-check",
|
|
7061
|
+
"-c",
|
|
7062
|
+
`model_reasoning_effort=${GENERATION_EFFORT}`,
|
|
7063
|
+
"-c",
|
|
7064
|
+
"project_doc_max_bytes=0",
|
|
7065
|
+
"-c",
|
|
7066
|
+
"skills.include_instructions=false",
|
|
7067
|
+
"-c",
|
|
7068
|
+
"skills.bundled.enabled=false",
|
|
7069
|
+
// Through the config, not `--disable`: a Codex that lacks one of these
|
|
7070
|
+
// ignores it here, where `--disable` refuses to start at all.
|
|
7071
|
+
...[
|
|
7072
|
+
"multi_agent",
|
|
7073
|
+
"apps",
|
|
7074
|
+
"plugins",
|
|
7075
|
+
"memories",
|
|
7076
|
+
"goals",
|
|
7077
|
+
"browser_use",
|
|
7078
|
+
"computer_use",
|
|
7079
|
+
"image_generation"
|
|
7080
|
+
].flatMap((feature) => ["-c", `features.${feature}=false`])
|
|
7081
|
+
];
|
|
7082
|
+
var BARE = /^[A-Za-z0-9_-]+$/;
|
|
7083
|
+
async function codexServers(home = defaultCodexHome()) {
|
|
7084
|
+
let config;
|
|
7085
|
+
try {
|
|
7086
|
+
config = await readFile11(join11(home, "config.toml"), "utf8");
|
|
7087
|
+
} catch {
|
|
7088
|
+
return [];
|
|
6986
7089
|
}
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
var CAPTURE_DEADLINE_MS = 15e3;
|
|
6995
|
-
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
6996
|
-
function captureSlug(title) {
|
|
6997
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "direction";
|
|
6998
|
-
}
|
|
6999
|
-
function referenceName(value) {
|
|
7000
|
-
const raw = Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
|
7001
|
-
const safe = [...raw].filter((character) => {
|
|
7002
|
-
const code = character.charCodeAt(0);
|
|
7003
|
-
return code >= 32 && code <= 126 && character !== "/" && character !== "\\";
|
|
7004
|
-
}).join("").trim().slice(0, 80);
|
|
7005
|
-
return safe || "image";
|
|
7090
|
+
const names = /* @__PURE__ */ new Set();
|
|
7091
|
+
for (const match of config.matchAll(/^\s*\[\s*mcp_servers\s*\.\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+))\s*\]\s*(?:#.*)?$/gm)) {
|
|
7092
|
+
const name = match[1] ?? match[2] ?? match[3] ?? "";
|
|
7093
|
+
if (BARE.test(name))
|
|
7094
|
+
names.add(name);
|
|
7095
|
+
}
|
|
7096
|
+
return [...names];
|
|
7006
7097
|
}
|
|
7007
|
-
function
|
|
7008
|
-
return
|
|
7098
|
+
function defaultCodexHome(env = process.env) {
|
|
7099
|
+
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME : join11(homedir2(), ".codex");
|
|
7009
7100
|
}
|
|
7010
|
-
|
|
7011
|
-
|
|
7101
|
+
var serversOff = (servers) => servers.flatMap((name) => ["-c", `mcp_servers.${name}.enabled=false`]);
|
|
7102
|
+
function codexPlanArgs(prompt, servers) {
|
|
7103
|
+
return [...RESTRICTED2, ...serversOff(servers), "-s", "read-only", prompt];
|
|
7012
7104
|
}
|
|
7013
|
-
function
|
|
7014
|
-
|
|
7015
|
-
if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1")
|
|
7016
|
-
return true;
|
|
7017
|
-
if (bare.endsWith(".local"))
|
|
7018
|
-
return true;
|
|
7019
|
-
if (!net3.isIPv4(bare))
|
|
7020
|
-
return false;
|
|
7021
|
-
const [first, second] = bare.split(".").map(Number);
|
|
7022
|
-
return first === 10 || first === 172 && second !== void 0 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
7105
|
+
function codexBuildArgs(prompt, servers) {
|
|
7106
|
+
return [...RESTRICTED2, ...serversOff(servers), "-s", "workspace-write", prompt];
|
|
7023
7107
|
}
|
|
7024
|
-
function
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7108
|
+
function codexResultText(lines2) {
|
|
7109
|
+
for (let index = lines2.length - 1; index >= 0; index -= 1) {
|
|
7110
|
+
const line = lines2[index]?.trim() ?? "";
|
|
7111
|
+
if (!line.startsWith("{"))
|
|
7112
|
+
continue;
|
|
7113
|
+
try {
|
|
7114
|
+
const event = parseJson(line);
|
|
7115
|
+
const item = isJsonRecord(event) ? event.item : void 0;
|
|
7116
|
+
if (isJsonRecord(event) && event.type === "item.completed" && isJsonRecord(item) && item.type === "agent_message" && isString(item.text))
|
|
7117
|
+
return item.text;
|
|
7118
|
+
} catch {
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
return null;
|
|
7028
7122
|
}
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7123
|
+
|
|
7124
|
+
// ../server/dist/generation/facts.js
|
|
7125
|
+
import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
|
|
7126
|
+
import { dirname as dirname7, join as join13, normalize } from "path";
|
|
7127
|
+
|
|
7128
|
+
// ../server/dist/generation/switch-file.js
|
|
7129
|
+
import { readFile as readFile12, readdir as readdir3, stat as stat4 } from "fs/promises";
|
|
7130
|
+
import { join as join12, relative as relative3 } from "path";
|
|
7131
|
+
var MAP = /const DIRECTIONS = \{([\s\S]*?)\} as const;/;
|
|
7132
|
+
var SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
7133
|
+
var SCAN_LIMIT = 3e3;
|
|
7134
|
+
async function isFile(path) {
|
|
7035
7135
|
try {
|
|
7036
|
-
|
|
7136
|
+
return (await stat4(path)).isFile();
|
|
7037
7137
|
} catch {
|
|
7038
7138
|
return false;
|
|
7039
7139
|
}
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
const
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7140
|
+
}
|
|
7141
|
+
async function findSwitch(cwd, slug) {
|
|
7142
|
+
for (const name of ["switch.tsx", "switch.jsx"]) {
|
|
7143
|
+
const path = join12(".leglas", "variants", slug, name);
|
|
7144
|
+
if (await isFile(join12(cwd, path)))
|
|
7145
|
+
return path;
|
|
7146
|
+
}
|
|
7147
|
+
const wanted = `"v-${slug}"`;
|
|
7148
|
+
let seen = 0;
|
|
7149
|
+
const walk = async (folder) => {
|
|
7150
|
+
let items;
|
|
7151
|
+
try {
|
|
7152
|
+
items = await readdir3(join12(cwd, folder), { withFileTypes: true });
|
|
7153
|
+
} catch {
|
|
7154
|
+
return null;
|
|
7155
|
+
}
|
|
7156
|
+
for (const item of items) {
|
|
7157
|
+
if (seen >= SCAN_LIMIT || item.name.startsWith(".") || item.name === "node_modules")
|
|
7158
|
+
continue;
|
|
7159
|
+
const path = join12(folder, item.name);
|
|
7160
|
+
if (item.isDirectory()) {
|
|
7161
|
+
const found = await walk(path);
|
|
7162
|
+
if (found !== null)
|
|
7163
|
+
return found;
|
|
7164
|
+
continue;
|
|
7165
|
+
}
|
|
7166
|
+
if (!SOURCE.test(item.name))
|
|
7167
|
+
continue;
|
|
7168
|
+
seen += 1;
|
|
7169
|
+
const source = await readFile12(join12(cwd, path), "utf8").catch(() => "");
|
|
7170
|
+
if (source.includes(wanted) && MAP.test(source))
|
|
7171
|
+
return path;
|
|
7172
|
+
}
|
|
7173
|
+
return null;
|
|
7174
|
+
};
|
|
7175
|
+
for (const root of ["src", "app"]) {
|
|
7176
|
+
const found = await walk(root);
|
|
7177
|
+
if (found !== null)
|
|
7178
|
+
return relative3(cwd, join12(cwd, found));
|
|
7050
7179
|
}
|
|
7180
|
+
return null;
|
|
7051
7181
|
}
|
|
7052
|
-
function
|
|
7053
|
-
|
|
7182
|
+
function readDirections(source) {
|
|
7183
|
+
const imports = /* @__PURE__ */ new Map();
|
|
7184
|
+
for (const match of source.matchAll(/^import \{ (\w+) \} from "([^"]+)";$/gm)) {
|
|
7185
|
+
imports.set(match[1] ?? "", match[2] ?? "");
|
|
7186
|
+
}
|
|
7187
|
+
const body = MAP.exec(source)?.[1] ?? "";
|
|
7188
|
+
const directions = [];
|
|
7189
|
+
for (const match of body.matchAll(/"?([\w-]+)"?\s*:\s*(\w+)/g)) {
|
|
7190
|
+
const key = match[1] ?? "";
|
|
7191
|
+
const name = match[2] ?? "";
|
|
7192
|
+
const from = imports.get(name);
|
|
7193
|
+
if (from !== void 0)
|
|
7194
|
+
directions.push({ key, name, from });
|
|
7195
|
+
}
|
|
7196
|
+
return directions;
|
|
7197
|
+
}
|
|
7198
|
+
function directionKeys(source) {
|
|
7199
|
+
const body = MAP.exec(source)?.[1] ?? "";
|
|
7200
|
+
return [...body.matchAll(/"?([\w-]+)"?\s*:/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
7201
|
+
}
|
|
7202
|
+
function fallbackKey(source) {
|
|
7203
|
+
return /const FALLBACK: \w+ = "([\w-]+)";/.exec(source)?.[1] ?? null;
|
|
7204
|
+
}
|
|
7205
|
+
function addSlots(source, slots) {
|
|
7206
|
+
if (!MAP.test(source))
|
|
7207
|
+
throw new Error("The switch file has no DIRECTIONS map.");
|
|
7208
|
+
const lines2 = source.split("\n");
|
|
7209
|
+
let last = -1;
|
|
7210
|
+
lines2.forEach((line, index) => {
|
|
7211
|
+
if (line.startsWith("import "))
|
|
7212
|
+
last = index;
|
|
7213
|
+
});
|
|
7214
|
+
lines2.splice(last + 1, 0, ...slots.map((slot) => `import { ${slot.name} } from "./${slot.key}";`));
|
|
7215
|
+
const added = slots.map((slot) => ` "${slot.key}": ${slot.name},`).join("\n");
|
|
7216
|
+
return lines2.join("\n").replace(MAP, (_, body) => `const DIRECTIONS = {
|
|
7217
|
+
${body.replace(/^\s*\n|\s+$/g, "").replace(/,?$/, ",")}
|
|
7218
|
+
${added}
|
|
7219
|
+
} as const;`);
|
|
7220
|
+
}
|
|
7221
|
+
function placeholderSource(name) {
|
|
7222
|
+
return `export function ${name}() {
|
|
7223
|
+
return null;
|
|
7054
7224
|
}
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7225
|
+
`;
|
|
7226
|
+
}
|
|
7227
|
+
|
|
7228
|
+
// ../server/dist/generation/facts.js
|
|
7229
|
+
var IMAGE = /\.(png|jpe?g|webp|avif|gif|svg)$/i;
|
|
7230
|
+
var IMAGE_LIMIT = 24;
|
|
7231
|
+
var EXAMPLE_LINES = 250;
|
|
7232
|
+
var VARIED_LINES = 600;
|
|
7233
|
+
function clip(source, limit) {
|
|
7234
|
+
const lines2 = source.replace(/\n$/, "").split("\n");
|
|
7235
|
+
if (lines2.length <= limit)
|
|
7236
|
+
return source;
|
|
7237
|
+
const hidden = lines2.length - limit;
|
|
7238
|
+
return [
|
|
7239
|
+
...lines2.slice(0, limit),
|
|
7240
|
+
`// \u2026 ${hidden} more ${hidden === 1 ? "line" : "lines"}, not shown`
|
|
7241
|
+
].join("\n");
|
|
7242
|
+
}
|
|
7243
|
+
var BASELINE_LINES = 15;
|
|
7244
|
+
var STUB = /Re-export what your|A first direction for/;
|
|
7245
|
+
async function text2(path) {
|
|
7246
|
+
return readFile13(path, "utf8").catch(() => null);
|
|
7247
|
+
}
|
|
7248
|
+
async function firstFile(cwd, candidates) {
|
|
7249
|
+
for (const candidate of candidates) {
|
|
7250
|
+
if (await text2(join13(cwd, candidate)) !== null)
|
|
7251
|
+
return candidate;
|
|
7061
7252
|
}
|
|
7253
|
+
return null;
|
|
7062
7254
|
}
|
|
7063
|
-
function
|
|
7064
|
-
|
|
7065
|
-
return "";
|
|
7066
|
-
return isString(value) ? value.trim() : null;
|
|
7255
|
+
function cssImports(source, from) {
|
|
7256
|
+
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) => normalize(join13(dirname7(from), match[1] ?? "")));
|
|
7067
7257
|
}
|
|
7068
|
-
function
|
|
7069
|
-
const
|
|
7070
|
-
|
|
7258
|
+
async function globalStylesheet(cwd) {
|
|
7259
|
+
const entries = [];
|
|
7260
|
+
const html = await text2(join13(cwd, "index.html"));
|
|
7261
|
+
const script = html === null ? null : /<script[^>]+type="module"[^>]+src="\/?([^"]+)"/.exec(html);
|
|
7262
|
+
if (script?.[1] !== void 0)
|
|
7263
|
+
entries.push(script[1]);
|
|
7264
|
+
for (const root of ["app", "src/app"]) {
|
|
7265
|
+
for (const extension of ["tsx", "jsx", "ts", "js"])
|
|
7266
|
+
entries.push(`${root}/layout.${extension}`);
|
|
7267
|
+
}
|
|
7268
|
+
const sheets = [];
|
|
7269
|
+
for (const entry of entries) {
|
|
7270
|
+
const source = await text2(join13(cwd, entry));
|
|
7271
|
+
if (source !== null)
|
|
7272
|
+
sheets.push(...cssImports(source, entry));
|
|
7273
|
+
}
|
|
7274
|
+
for (const sheet of sheets) {
|
|
7275
|
+
const source = await text2(join13(cwd, sheet));
|
|
7276
|
+
if (source !== null && /:root\s*\{|@font-face/.test(source))
|
|
7277
|
+
return sheet;
|
|
7278
|
+
}
|
|
7279
|
+
return firstFile(cwd, sheets);
|
|
7071
7280
|
}
|
|
7072
|
-
function
|
|
7073
|
-
|
|
7074
|
-
|
|
7281
|
+
async function images(cwd) {
|
|
7282
|
+
const found = [];
|
|
7283
|
+
const walk = async (folder) => {
|
|
7284
|
+
let items;
|
|
7075
7285
|
try {
|
|
7076
|
-
|
|
7286
|
+
items = await readdir4(join13(cwd, "public", folder), { withFileTypes: true });
|
|
7077
7287
|
} catch {
|
|
7078
|
-
return
|
|
7288
|
+
return;
|
|
7079
7289
|
}
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7290
|
+
for (const item of items) {
|
|
7291
|
+
if (found.length >= IMAGE_LIMIT || item.name.startsWith("."))
|
|
7292
|
+
continue;
|
|
7293
|
+
const path = folder === "" ? item.name : `${folder}/${item.name}`;
|
|
7294
|
+
if (item.isDirectory())
|
|
7295
|
+
await walk(path);
|
|
7296
|
+
else if (IMAGE.test(item.name))
|
|
7297
|
+
found.push(`/${path}`);
|
|
7298
|
+
}
|
|
7299
|
+
};
|
|
7300
|
+
await walk("");
|
|
7301
|
+
return found;
|
|
7302
|
+
}
|
|
7303
|
+
async function componentFile(cwd, from, specifier) {
|
|
7304
|
+
const base = normalize(join13(dirname7(from), specifier));
|
|
7305
|
+
return firstFile(cwd, [
|
|
7306
|
+
base,
|
|
7307
|
+
...["tsx", "jsx", "ts", "js"].map((extension) => `${base}.${extension}`)
|
|
7308
|
+
]);
|
|
7309
|
+
}
|
|
7310
|
+
async function example(cwd, switchPath, prefer) {
|
|
7311
|
+
const source = await text2(join13(cwd, switchPath));
|
|
7312
|
+
if (source === null)
|
|
7313
|
+
return null;
|
|
7314
|
+
const directions = readDirections(source);
|
|
7315
|
+
const first = prefer ?? fallbackKey(source);
|
|
7316
|
+
const ordered = [...directions].sort((a, b) => Number(b.key === first) - Number(a.key === first));
|
|
7317
|
+
for (const direction of ordered) {
|
|
7318
|
+
const limit = direction.key === prefer ? VARIED_LINES : EXAMPLE_LINES;
|
|
7319
|
+
const path = await componentFile(cwd, switchPath, direction.from);
|
|
7320
|
+
const body = path === null ? null : await text2(join13(cwd, path));
|
|
7321
|
+
if (path === null || body === null || body === placeholderSource(direction.name) || STUB.test(body))
|
|
7322
|
+
continue;
|
|
7323
|
+
if (body.split("\n").length < BASELINE_LINES) {
|
|
7324
|
+
const target = /from\s+["'](\.[^"']+)["']/.exec(body)?.[1];
|
|
7325
|
+
const followed = target === void 0 ? null : await componentFile(cwd, path, target);
|
|
7326
|
+
const real = followed === null ? null : await text2(join13(cwd, followed));
|
|
7327
|
+
if (followed !== null && real !== null) {
|
|
7328
|
+
return {
|
|
7329
|
+
key: direction.key,
|
|
7330
|
+
path: followed,
|
|
7331
|
+
source: clip(real, limit)
|
|
7332
|
+
};
|
|
7333
|
+
}
|
|
7334
|
+
continue;
|
|
7335
|
+
}
|
|
7336
|
+
return { key: direction.key, path, source: clip(body, limit) };
|
|
7337
|
+
}
|
|
7338
|
+
return null;
|
|
7339
|
+
}
|
|
7340
|
+
async function dependencyNames(cwd) {
|
|
7341
|
+
try {
|
|
7342
|
+
const pkg = parseJson(await text2(join13(cwd, "package.json")) ?? "");
|
|
7343
|
+
return isJsonRecord(pkg) && isJsonRecord(pkg.dependencies) ? Object.keys(pkg.dependencies) : [];
|
|
7344
|
+
} catch {
|
|
7345
|
+
return [];
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
async function readProjectFacts(cwd, switchPath, prefer = null) {
|
|
7349
|
+
const dependencies = await dependencyNames(cwd);
|
|
7350
|
+
const sheetPath = await globalStylesheet(cwd);
|
|
7351
|
+
const css = sheetPath === null ? null : await text2(join13(cwd, sheetPath));
|
|
7352
|
+
return {
|
|
7353
|
+
dependencies,
|
|
7354
|
+
stylesheet: sheetPath === null || css === null ? null : {
|
|
7355
|
+
path: sheetPath,
|
|
7356
|
+
fonts: [
|
|
7357
|
+
...new Set([...css.matchAll(/font-family:\s*["']([^"']+)["']/g)].map((match) => match[1] ?? ""))
|
|
7358
|
+
],
|
|
7359
|
+
tokens: /:root\s*\{[\s\S]*?\n\}/.exec(css)?.[0] ?? ""
|
|
7360
|
+
},
|
|
7361
|
+
images: await images(cwd),
|
|
7362
|
+
example: await example(cwd, switchPath, prefer)
|
|
7363
|
+
};
|
|
7364
|
+
}
|
|
7365
|
+
function factsBlock(facts, surface, varied = null) {
|
|
7366
|
+
const lines2 = [
|
|
7367
|
+
"Leglas read the project for you. Follow its house style: differ in the idea, not in the craft."
|
|
7368
|
+
];
|
|
7369
|
+
if (facts.dependencies.length > 0)
|
|
7370
|
+
lines2.push(`Dependencies: ${facts.dependencies.join(", ")}.`);
|
|
7371
|
+
if (facts.stylesheet !== null) {
|
|
7372
|
+
const fonts = facts.stylesheet.fonts.length === 0 ? "" : ` It declares the fonts ${facts.stylesheet.fonts.join(", ")}.`;
|
|
7373
|
+
lines2.push(`The global stylesheet ${facts.stylesheet.path} is already loaded on every page.${fonts}`);
|
|
7374
|
+
if (facts.stylesheet.tokens !== "")
|
|
7375
|
+
lines2.push("It sets these tokens:", "```css", facts.stylesheet.tokens, "```");
|
|
7376
|
+
}
|
|
7377
|
+
if (facts.images.length > 0)
|
|
7378
|
+
lines2.push(`Images the project already serves from its root: ${facts.images.join(", ")}.`);
|
|
7379
|
+
if (facts.example !== null) {
|
|
7380
|
+
lines2.push(varied?.key === facts.example.key ? `${varied.title}, the direction being varied, is ${facts.example.path}. Its imports are relative to that file:` : `The ${surface}'s current direction, ${facts.example.path}, shows how directions here are written:`, "```tsx", facts.example.source.trimEnd(), "```");
|
|
7381
|
+
}
|
|
7382
|
+
return lines2.join("\n");
|
|
7383
|
+
}
|
|
7384
|
+
|
|
7385
|
+
// ../server/dist/generation/prompts.js
|
|
7386
|
+
function described(base) {
|
|
7387
|
+
const idea = base.idea.replace(/\.$/, "");
|
|
7388
|
+
return idea === "" ? `"${base.title}"` : `"${base.title}" (${idea})`;
|
|
7389
|
+
}
|
|
7390
|
+
function steer(brief) {
|
|
7391
|
+
if (brief === "")
|
|
7392
|
+
return ".";
|
|
7393
|
+
return `. What to explore: ${/[.!?]$/.test(brief) ? brief : `${brief}.`}`;
|
|
7394
|
+
}
|
|
7395
|
+
var ANSWER = "Answer from this brief alone: do not read, search or run anything. Reply with only a JSON array of objects with key (kebab-case), title and idea (one line).";
|
|
7396
|
+
function planPrompt(input) {
|
|
7397
|
+
const { base } = input;
|
|
7398
|
+
if (base !== void 0 && base !== null) {
|
|
7399
|
+
return `Propose ${input.count} variations of the ${input.surface} direction ${described(base)}${steer(input.brief)} Every variation stays recognisably ${base.title} and changes one deliberate thing; name each for what it changes. Decide them together so no two change the same thing. ${ANSWER}`;
|
|
7400
|
+
}
|
|
7401
|
+
const already = input.existing.length === 0 ? "" : ` The ${input.surface} already has these directions: ${input.existing.join(", ")}; yours must differ from them too.`;
|
|
7402
|
+
return `Propose ${input.count} genuinely different design directions for the ${input.surface} of this product: ${input.brief} Decide all of them together; if two would look alike at a glance, replace one.${already} ${ANSWER}`;
|
|
7403
|
+
}
|
|
7404
|
+
function replacePrompt2(input) {
|
|
7405
|
+
const avoid = input.avoid.map((concept) => `${concept.title} (${concept.idea})`).join("; ");
|
|
7406
|
+
const { base } = input;
|
|
7407
|
+
if (base !== void 0 && base !== null) {
|
|
7408
|
+
return `Propose 1 more variation of the ${input.surface} direction ${described(base)}${steer(input.brief)} It stays recognisably ${base.title} and changes something none of these change: ${avoid}. ${ANSWER}`;
|
|
7409
|
+
}
|
|
7410
|
+
return `Propose 1 design direction for the ${input.surface} of this product: ${input.brief} It must not look like any of these: ${avoid}. ${ANSWER}`;
|
|
7411
|
+
}
|
|
7412
|
+
function buildPrompt(input) {
|
|
7413
|
+
const others = input.others.map((other) => `${other.title} (${other.idea})`).join("; ");
|
|
7414
|
+
const { base } = input;
|
|
7415
|
+
const varying = base !== void 0 && base !== null;
|
|
7416
|
+
const rivals = others === "" ? "" : varying ? ` The other variations are ${others}; change something different from them.` : ` The other directions are ${others}; yours must not look like any of them.`;
|
|
7417
|
+
const what = varying ? `Build this variation of the ${input.surface} direction "${base.title}": ${input.concept.title}, ${input.concept.idea.replace(/\.$/, "")}. Keep it recognisably ${base.title} and change only what the variation names.` : `Build this design direction for the ${input.surface}: ${input.concept.title}, ${input.concept.idea.replace(/\.$/, "")}.`;
|
|
7418
|
+
const product = varying ? steer(input.brief).slice(1) : ` The product: ${input.brief}`;
|
|
7419
|
+
return `${what}${product}${rivals}
|
|
7420
|
+
|
|
7421
|
+
Everything you need is in this message, so do not look around the project. It is ${input.stack}. Your file is ${input.file}. It holds a placeholder: read it once, then replace it in a single write with a component exported as ${input.name}. Keep every new style in that file, in a <style> element or inline.
|
|
7422
|
+
|
|
7423
|
+
${input.facts}
|
|
7424
|
+
|
|
7425
|
+
This is a first draft for someone comparing directions side by side, not a finished page. Aim for about 250 lines and write it in one pass: no second drafts, and no review, lint, tests or screenshots. Leglas renders and captures it for you. Your task ends when the file is written.`;
|
|
7426
|
+
}
|
|
7427
|
+
function fixPrompt(input) {
|
|
7428
|
+
return `Leglas rendered ${input.file} and the page reported: ${input.errors.slice(0, 3).join(" | ")}. Fix what your file causes, in as few edits as you can, and change nothing else. If nothing in your file causes it, change nothing. Do not look around the project.`;
|
|
7429
|
+
}
|
|
7430
|
+
function kebab(value) {
|
|
7431
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7432
|
+
}
|
|
7433
|
+
function pascal(key) {
|
|
7434
|
+
return key.split("-").filter((part) => part !== "").map((part) => /^\d/.test(part) ? `N${part}` : part[0].toUpperCase() + part.slice(1)).join("");
|
|
7435
|
+
}
|
|
7436
|
+
function parseConcepts(reply, count) {
|
|
7437
|
+
const start = reply.indexOf("[");
|
|
7438
|
+
const end = reply.lastIndexOf("]");
|
|
7439
|
+
if (start === -1 || end < start)
|
|
7440
|
+
throw new Error("The plan reply held no JSON array.");
|
|
7441
|
+
let parsed2;
|
|
7442
|
+
try {
|
|
7443
|
+
parsed2 = parseJson(reply.slice(start, end + 1));
|
|
7444
|
+
} catch {
|
|
7445
|
+
throw new Error("The plan reply's JSON did not parse.");
|
|
7446
|
+
}
|
|
7447
|
+
if (!Array.isArray(parsed2))
|
|
7448
|
+
throw new Error("The plan reply held no JSON array.");
|
|
7449
|
+
const concepts = [];
|
|
7450
|
+
for (const entry of parsed2) {
|
|
7451
|
+
if (!isJsonRecord(entry))
|
|
7452
|
+
continue;
|
|
7453
|
+
const title = isString(entry.title) ? entry.title.trim() : "";
|
|
7454
|
+
const key = kebab(isString(entry.key) && entry.key.trim() !== "" ? entry.key : title);
|
|
7455
|
+
if (title === "" || key === "" || concepts.some((concept) => concept.key === key))
|
|
7456
|
+
continue;
|
|
7457
|
+
concepts.push({ key, title, idea: isString(entry.idea) ? entry.idea.trim() : "" });
|
|
7458
|
+
if (concepts.length === count)
|
|
7459
|
+
return concepts;
|
|
7460
|
+
}
|
|
7461
|
+
throw new Error(`The plan reply named fewer than ${count} distinct directions.`);
|
|
7462
|
+
}
|
|
7463
|
+
|
|
7464
|
+
// ../server/dist/generation/generation.js
|
|
7465
|
+
var MAX_DIRECTIONS = 6;
|
|
7466
|
+
var BUILD_DEADLINE_MS = 5 * 6e4;
|
|
7467
|
+
var PLAN_DEADLINE_MS = 2 * 6e4;
|
|
7468
|
+
var FIX_DEADLINE_MS = 3 * 6e4;
|
|
7469
|
+
var KILL_GRACE_MS = 5e3;
|
|
7470
|
+
var RECHECK_MS = 1500;
|
|
7471
|
+
var OWN_FAILURES = /* @__PURE__ */ new Set([
|
|
7472
|
+
"broken",
|
|
7473
|
+
"not-written",
|
|
7474
|
+
"too-slow",
|
|
7475
|
+
"unexpected"
|
|
7476
|
+
]);
|
|
7477
|
+
var BUSY = "A set of directions is already being built. Wait for it, or stop it first.";
|
|
7478
|
+
var KEPT_JOBS = 5;
|
|
7479
|
+
var CLAUDE = {
|
|
7480
|
+
id: "claude",
|
|
7481
|
+
name: "Claude",
|
|
7482
|
+
plan: (prompt) => planArgs(prompt),
|
|
7483
|
+
build: (prompt) => buildArgs(prompt),
|
|
7484
|
+
result: resultText
|
|
7485
|
+
};
|
|
7486
|
+
function codex(servers) {
|
|
7487
|
+
return {
|
|
7488
|
+
id: "codex",
|
|
7489
|
+
name: "Codex",
|
|
7490
|
+
plan: (prompt) => codexPlanArgs(prompt, servers),
|
|
7491
|
+
build: (prompt) => codexBuildArgs(prompt, servers),
|
|
7492
|
+
result: codexResultText
|
|
7493
|
+
};
|
|
7494
|
+
}
|
|
7495
|
+
function wait(milliseconds) {
|
|
7496
|
+
return new Promise((resolve5) => {
|
|
7497
|
+
setTimeout(resolve5, milliseconds).unref?.();
|
|
7498
|
+
});
|
|
7499
|
+
}
|
|
7500
|
+
function surfaceSlug(surface) {
|
|
7501
|
+
return surface.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7502
|
+
}
|
|
7503
|
+
function baseOf(preview, surface) {
|
|
7504
|
+
let key;
|
|
7505
|
+
try {
|
|
7506
|
+
key = new URL(preview.url, "http://leglas.invalid").searchParams.get(`v-${surfaceSlug(surface)}`);
|
|
7507
|
+
} catch {
|
|
7508
|
+
return null;
|
|
7509
|
+
}
|
|
7510
|
+
return key === null || key === "" ? null : { title: preview.title, key, idea: preview.note ?? "" };
|
|
7511
|
+
}
|
|
7512
|
+
async function stackOf(cwd, switchPath) {
|
|
7513
|
+
let deps = {};
|
|
7514
|
+
try {
|
|
7515
|
+
const pkg = parseJson(await readFile14(join14(cwd, "package.json"), "utf8"));
|
|
7516
|
+
if (isJsonRecord(pkg)) {
|
|
7517
|
+
for (const group of [pkg.devDependencies, pkg.dependencies]) {
|
|
7518
|
+
if (!isJsonRecord(group))
|
|
7519
|
+
continue;
|
|
7520
|
+
for (const [name, version2] of Object.entries(group)) {
|
|
7521
|
+
if (isString(version2))
|
|
7522
|
+
deps[name] = version2;
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
}
|
|
7526
|
+
} catch {
|
|
7527
|
+
deps = {};
|
|
7528
|
+
}
|
|
7529
|
+
const major = /\d+/.exec(deps.react ?? "")?.[0];
|
|
7530
|
+
const react = major === void 0 ? "React" : `React ${major}`;
|
|
7531
|
+
const typescript = switchPath.endsWith(".tsx") ? " and TypeScript" : "";
|
|
7532
|
+
if (deps.next !== void 0)
|
|
7533
|
+
return `a Next.js app with ${react}${typescript}`;
|
|
7534
|
+
if (deps.vite !== void 0)
|
|
7535
|
+
return `a Vite app with ${react}${typescript}`;
|
|
7536
|
+
return `an app with ${react}${typescript}`;
|
|
7537
|
+
}
|
|
7538
|
+
function provenance(job) {
|
|
7539
|
+
const words = job.surface.replace(/-/g, " ");
|
|
7540
|
+
return {
|
|
7541
|
+
askedFor: job.brief === "" ? void 0 : job.brief,
|
|
7542
|
+
// Variations sit under the direction they vary, as any family does on the rail.
|
|
7543
|
+
basedOn: job.basedOn ?? void 0,
|
|
7544
|
+
tags: [words.charAt(0).toUpperCase() + words.slice(1)]
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
function copy(job) {
|
|
7548
|
+
return {
|
|
7549
|
+
...job,
|
|
7550
|
+
slots: job.slots.map((slot) => ({
|
|
7551
|
+
...slot,
|
|
7552
|
+
failure: slot.failure === null ? null : { ...slot.failure }
|
|
7553
|
+
}))
|
|
7554
|
+
};
|
|
7555
|
+
}
|
|
7556
|
+
function createGenerations(deps) {
|
|
7557
|
+
const now = deps.now ?? Date.now;
|
|
7558
|
+
const spawn7 = deps.spawn ?? ((command, args, options) => nodeSpawn4(command, args, { ...options, env: agentEnvironment() }));
|
|
7559
|
+
const lives = [];
|
|
7560
|
+
let closed = false;
|
|
7561
|
+
const busy = () => lives.some((live) => live.job.state === "planning" || live.job.state === "building");
|
|
7562
|
+
const changed = () => deps.onChange();
|
|
7563
|
+
let registry = Promise.resolve();
|
|
7564
|
+
const serially = (task) => {
|
|
7565
|
+
const next = registry.then(task, task);
|
|
7566
|
+
registry = next.catch(() => {
|
|
7567
|
+
});
|
|
7568
|
+
return next;
|
|
7569
|
+
};
|
|
7570
|
+
const run4 = (agent, args, deadlineMs, onLine) => {
|
|
7571
|
+
const lines2 = [];
|
|
7572
|
+
let timedOut2 = false;
|
|
7573
|
+
let child = null;
|
|
7574
|
+
const done = new Promise((resolve5) => {
|
|
7575
|
+
if (closed) {
|
|
7576
|
+
resolve5({ code: null, lines: lines2, error: "Leglas is closing.", timedOut: timedOut2 });
|
|
7577
|
+
return;
|
|
7578
|
+
}
|
|
7579
|
+
try {
|
|
7580
|
+
child = spawn7(agent.id, args, {
|
|
7581
|
+
cwd: deps.cwd,
|
|
7582
|
+
shell: false,
|
|
7583
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7584
|
+
...ownGroup()
|
|
7585
|
+
});
|
|
7586
|
+
} catch (error) {
|
|
7587
|
+
resolve5({
|
|
7588
|
+
code: null,
|
|
7589
|
+
lines: lines2,
|
|
7590
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7591
|
+
timedOut: timedOut2
|
|
7592
|
+
});
|
|
7593
|
+
return;
|
|
7594
|
+
}
|
|
7595
|
+
let buffered = "";
|
|
7596
|
+
const take = (chunk) => {
|
|
7597
|
+
buffered += chunk.toString();
|
|
7598
|
+
const parts = buffered.split("\n");
|
|
7599
|
+
buffered = parts.pop() ?? "";
|
|
7600
|
+
lines2.push(...parts);
|
|
7601
|
+
if (onLine !== void 0)
|
|
7602
|
+
for (const line of parts)
|
|
7603
|
+
onLine(line);
|
|
7604
|
+
};
|
|
7605
|
+
child.stdout.on("data", take);
|
|
7606
|
+
child.stderr.on("data", take);
|
|
7607
|
+
const deadline = setTimeout(() => {
|
|
7608
|
+
timedOut2 = true;
|
|
7609
|
+
if (child !== null)
|
|
7610
|
+
signalTree(child, "SIGTERM");
|
|
7611
|
+
setTimeout(() => {
|
|
7612
|
+
if (child !== null)
|
|
7613
|
+
signalTree(child, "SIGKILL");
|
|
7614
|
+
}, KILL_GRACE_MS).unref?.();
|
|
7615
|
+
}, deadlineMs);
|
|
7616
|
+
deadline.unref?.();
|
|
7617
|
+
child.once("error", (error) => {
|
|
7618
|
+
clearTimeout(deadline);
|
|
7619
|
+
resolve5({ code: null, lines: lines2, error: error.message, timedOut: timedOut2 });
|
|
7620
|
+
});
|
|
7621
|
+
child.once("close", (code) => {
|
|
7622
|
+
clearTimeout(deadline);
|
|
7623
|
+
if (buffered !== "")
|
|
7624
|
+
lines2.push(buffered);
|
|
7625
|
+
resolve5({ code, lines: lines2, error: null, timedOut: timedOut2 });
|
|
7626
|
+
});
|
|
7627
|
+
});
|
|
7628
|
+
return {
|
|
7629
|
+
done,
|
|
7630
|
+
stop: async () => {
|
|
7631
|
+
if (child === null)
|
|
7632
|
+
return;
|
|
7633
|
+
signalTree(child, "SIGTERM");
|
|
7634
|
+
const exited = await Promise.race([
|
|
7635
|
+
done.then(() => true),
|
|
7636
|
+
wait(KILL_GRACE_MS).then(() => false)
|
|
7637
|
+
]);
|
|
7638
|
+
if (!exited && child !== null) {
|
|
7639
|
+
signalTree(child, "SIGKILL");
|
|
7640
|
+
await Promise.race([done, wait(KILL_GRACE_MS)]);
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
};
|
|
7644
|
+
};
|
|
7645
|
+
const failureOf2 = (agent, outcome) => classifyFailure({
|
|
7646
|
+
agent: agent.name,
|
|
7647
|
+
error: outcome.error,
|
|
7648
|
+
exitCode: outcome.code,
|
|
7649
|
+
lines: outcome.lines.slice(-40)
|
|
7650
|
+
});
|
|
7651
|
+
const settle = (live) => {
|
|
7652
|
+
const { job } = live;
|
|
7653
|
+
for (const slot of job.slots) {
|
|
7654
|
+
if (slot.state !== "building" && slot.state !== "checking")
|
|
7655
|
+
slot.activity = null;
|
|
7656
|
+
}
|
|
7657
|
+
if (job.state === "planning" || job.state === "failed")
|
|
7658
|
+
return;
|
|
7659
|
+
if (job.slots.some((slot) => slot.state === "building" || slot.state === "checking")) {
|
|
7660
|
+
job.state = "building";
|
|
7661
|
+
return;
|
|
7662
|
+
}
|
|
7663
|
+
job.state = job.slots.every((slot) => slot.state === "stopped") ? "stopped" : "done";
|
|
7664
|
+
job.endedAt = now();
|
|
7665
|
+
};
|
|
7666
|
+
const restore = async (slot) => {
|
|
7667
|
+
await writeFile7(join14(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), "utf8").catch(() => {
|
|
7668
|
+
});
|
|
7669
|
+
};
|
|
7670
|
+
const begin = (live, slot) => {
|
|
7671
|
+
const attempt = (live.attempts.get(slot.key) ?? 0) + 1;
|
|
7672
|
+
live.attempts.set(slot.key, attempt);
|
|
7673
|
+
return attempt;
|
|
7674
|
+
};
|
|
7675
|
+
const owns = (live, slot, attempt) => live.attempts.get(slot.key) === attempt;
|
|
7676
|
+
const guarded = (work, onError) => work.catch((error) => onError(error instanceof Error ? error.message : String(error)));
|
|
7677
|
+
const detach = (live, work, onError) => {
|
|
7678
|
+
const kept = guarded(work, onError);
|
|
7679
|
+
live.pending.add(kept);
|
|
7680
|
+
void kept.then(() => live.pending.delete(kept));
|
|
7681
|
+
};
|
|
7682
|
+
const drain = async (live) => {
|
|
7683
|
+
while (live.pending.size > 0)
|
|
7684
|
+
await Promise.all(live.pending);
|
|
7685
|
+
};
|
|
7686
|
+
const release = async (live, slot) => {
|
|
7687
|
+
const running = live.runs.get(slot.key);
|
|
7688
|
+
await running?.stop();
|
|
7689
|
+
await restore(slot);
|
|
7690
|
+
if (running !== void 0 && live.runs.get(slot.key) === running)
|
|
7691
|
+
live.runs.delete(slot.key);
|
|
7692
|
+
};
|
|
7693
|
+
const fail = (live, slot, failure) => {
|
|
7694
|
+
slot.state = "failed";
|
|
7695
|
+
slot.failure = failure;
|
|
7696
|
+
slot.endedAt = now();
|
|
7697
|
+
detach(live, restore(slot), () => {
|
|
7698
|
+
});
|
|
7699
|
+
if (OWN_FAILURES.has(failure.code))
|
|
7700
|
+
return;
|
|
7701
|
+
const same = live.job.slots.filter((other) => other.failure?.code === failure.code).length;
|
|
7702
|
+
if (same >= 2) {
|
|
7703
|
+
for (const other of live.job.slots) {
|
|
7704
|
+
if (other.state !== "building" && other.state !== "checking")
|
|
7705
|
+
continue;
|
|
7706
|
+
begin(live, other);
|
|
7707
|
+
other.state = "stopped";
|
|
7708
|
+
other.failure = {
|
|
7709
|
+
code: "same-failure",
|
|
7710
|
+
message: `Stopped after two directions failed the same way: ${failure.message}`
|
|
7711
|
+
};
|
|
7712
|
+
other.endedAt = now();
|
|
7713
|
+
detach(live, release(live, other), () => {
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
};
|
|
7718
|
+
const lost = (live, slot, attempt, message2) => {
|
|
7719
|
+
if (!owns(live, slot, attempt))
|
|
7720
|
+
return;
|
|
7721
|
+
fail(live, slot, { code: "unexpected", message: message2 });
|
|
7722
|
+
settle(live);
|
|
7723
|
+
changed();
|
|
7724
|
+
};
|
|
7725
|
+
const follow = (live, slot, attempt) => (line) => {
|
|
7726
|
+
const activity = activityFrom(live.agent.id, line, deps.cwd);
|
|
7727
|
+
if (activity === null || activity === slot.activity || !owns(live, slot, attempt))
|
|
7728
|
+
return;
|
|
7729
|
+
slot.activity = activity;
|
|
7730
|
+
changed();
|
|
7731
|
+
};
|
|
7732
|
+
const build = async (live, slot, attempt) => {
|
|
7733
|
+
const concept = live.concepts.get(slot.key);
|
|
7734
|
+
if (concept === void 0 || !owns(live, slot, attempt))
|
|
7735
|
+
return;
|
|
7736
|
+
slot.state = "building";
|
|
7737
|
+
if (slot.startedAt === null)
|
|
7738
|
+
slot.startedAt = now();
|
|
7739
|
+
slot.endedAt = null;
|
|
7740
|
+
slot.failure = null;
|
|
7741
|
+
slot.fixed = false;
|
|
7742
|
+
slot.activity = null;
|
|
7743
|
+
changed();
|
|
7744
|
+
const others = [...live.concepts.values()].filter((other) => other !== concept);
|
|
7745
|
+
const prompt = buildPrompt({
|
|
7746
|
+
surface: live.job.surface,
|
|
7747
|
+
brief: live.job.brief,
|
|
7748
|
+
concept,
|
|
7749
|
+
others,
|
|
7750
|
+
file: slot.file,
|
|
7751
|
+
name: pascal(slot.key),
|
|
7752
|
+
stack: live.stack,
|
|
7753
|
+
facts: live.facts ?? "",
|
|
7754
|
+
base: live.base
|
|
7755
|
+
});
|
|
7756
|
+
const building = run4(live.agent, live.agent.build(prompt), BUILD_DEADLINE_MS, follow(live, slot, attempt));
|
|
7757
|
+
live.runs.set(slot.key, building);
|
|
7758
|
+
const outcome = await building.done;
|
|
7759
|
+
if (!owns(live, slot, attempt))
|
|
7760
|
+
return;
|
|
7761
|
+
if (live.runs.get(slot.key) === building)
|
|
7762
|
+
live.runs.delete(slot.key);
|
|
7763
|
+
const written = await readFile14(join14(deps.cwd, slot.file), "utf8").catch(() => "");
|
|
7764
|
+
if (!owns(live, slot, attempt))
|
|
7765
|
+
return;
|
|
7766
|
+
if (outcome.timedOut) {
|
|
7767
|
+
fail(live, slot, {
|
|
7768
|
+
code: "too-slow",
|
|
7769
|
+
message: `The build took longer than ${BUILD_DEADLINE_MS / 6e4} minutes, so Leglas stopped it.`
|
|
7770
|
+
});
|
|
7771
|
+
} else if (outcome.error !== null || outcome.code !== 0) {
|
|
7772
|
+
fail(live, slot, failureOf2(live.agent, outcome));
|
|
7773
|
+
} else if (written === placeholderSource(pascal(slot.key))) {
|
|
7774
|
+
fail(live, slot, {
|
|
7775
|
+
code: "not-written",
|
|
7776
|
+
message: "The build finished without writing its file."
|
|
7777
|
+
});
|
|
7778
|
+
} else {
|
|
7779
|
+
slot.state = "checking";
|
|
7780
|
+
slot.activity = null;
|
|
7781
|
+
changed();
|
|
7782
|
+
await check(live, slot, attempt);
|
|
7783
|
+
}
|
|
7784
|
+
settle(live);
|
|
7785
|
+
changed();
|
|
7786
|
+
};
|
|
7787
|
+
const ownErrors = async (live, slot, attempt) => {
|
|
7788
|
+
const others = live.job.slots.filter((other) => other !== slot);
|
|
7789
|
+
const until = now() + BUILD_DEADLINE_MS + FIX_DEADLINE_MS;
|
|
7790
|
+
for (; ; ) {
|
|
7791
|
+
const report = await deps.render(slot.title);
|
|
7792
|
+
if (!owns(live, slot, attempt))
|
|
7793
|
+
return null;
|
|
7794
|
+
const errors = report?.errors ?? [];
|
|
7795
|
+
const names = (file) => errors.some((error) => error.includes(file));
|
|
7796
|
+
const waiting = !names(slot.file) && others.some((other) => (other.state === "building" || other.state === "checking") && names(other.file));
|
|
7797
|
+
if (!waiting || now() >= until) {
|
|
7798
|
+
return errors.filter((error) => !others.some((other) => error.includes(other.file)));
|
|
7799
|
+
}
|
|
7800
|
+
await wait(RECHECK_MS);
|
|
7801
|
+
if (!owns(live, slot, attempt))
|
|
7802
|
+
return null;
|
|
7803
|
+
}
|
|
7804
|
+
};
|
|
7805
|
+
const check = async (live, slot, attempt) => {
|
|
7806
|
+
let errors = await ownErrors(live, slot, attempt);
|
|
7807
|
+
if (errors === null)
|
|
7808
|
+
return;
|
|
7809
|
+
if (errors.length > 0) {
|
|
7810
|
+
const fixing = run4(live.agent, live.agent.build(fixPrompt({ file: slot.file, errors })), FIX_DEADLINE_MS, follow(live, slot, attempt));
|
|
7811
|
+
live.runs.set(slot.key, fixing);
|
|
7812
|
+
const outcome = await fixing.done;
|
|
7813
|
+
if (!owns(live, slot, attempt))
|
|
7814
|
+
return;
|
|
7815
|
+
slot.activity = null;
|
|
7816
|
+
changed();
|
|
7817
|
+
if (live.runs.get(slot.key) === fixing)
|
|
7818
|
+
live.runs.delete(slot.key);
|
|
7819
|
+
if (outcome.timedOut || outcome.error !== null || outcome.code !== 0) {
|
|
7820
|
+
fail(live, slot, outcome.timedOut ? { code: "too-slow", message: "The fix took too long, so Leglas stopped it." } : failureOf2(live.agent, outcome));
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
7823
|
+
slot.fixed = true;
|
|
7824
|
+
errors = await ownErrors(live, slot, attempt);
|
|
7825
|
+
if (errors === null)
|
|
7826
|
+
return;
|
|
7827
|
+
if (errors.length > 0) {
|
|
7828
|
+
fail(live, slot, {
|
|
7829
|
+
code: "broken",
|
|
7830
|
+
message: `The page still reports: ${errors[0] ?? ""}`
|
|
7831
|
+
});
|
|
7832
|
+
return;
|
|
7833
|
+
}
|
|
7834
|
+
}
|
|
7835
|
+
slot.state = "ready";
|
|
7836
|
+
slot.endedAt = now();
|
|
7837
|
+
};
|
|
7838
|
+
const freeTitle = (title, taken) => {
|
|
7839
|
+
let candidate = title;
|
|
7840
|
+
for (let suffix = 2; taken.has(candidate); suffix += 1)
|
|
7841
|
+
candidate = `${title} ${suffix}`;
|
|
7842
|
+
taken.add(candidate);
|
|
7843
|
+
return candidate;
|
|
7844
|
+
};
|
|
7845
|
+
const plan = async (live, existing) => {
|
|
7846
|
+
const { job } = live;
|
|
7847
|
+
const planning = run4(live.agent, live.agent.plan(planPrompt({
|
|
7848
|
+
surface: job.surface,
|
|
7849
|
+
brief: job.brief,
|
|
7850
|
+
count: job.count,
|
|
7851
|
+
existing,
|
|
7852
|
+
base: live.base
|
|
7853
|
+
})), PLAN_DEADLINE_MS);
|
|
7854
|
+
live.plan = planning;
|
|
7855
|
+
const outcome = await planning.done;
|
|
7856
|
+
live.plan = null;
|
|
7857
|
+
if (job.state !== "planning")
|
|
7858
|
+
return;
|
|
7859
|
+
if (outcome.timedOut || outcome.error !== null || outcome.code !== 0) {
|
|
7860
|
+
job.state = "failed";
|
|
7861
|
+
job.error = outcome.timedOut ? "Planning took too long, so Leglas stopped it." : failureOf2(live.agent, outcome).message;
|
|
7862
|
+
job.endedAt = now();
|
|
7863
|
+
changed();
|
|
7864
|
+
return;
|
|
7865
|
+
}
|
|
7866
|
+
let concepts;
|
|
7867
|
+
try {
|
|
7868
|
+
concepts = parseConcepts(live.agent.result(outcome.lines) ?? "", job.count);
|
|
7869
|
+
} catch (error) {
|
|
7870
|
+
job.state = "failed";
|
|
7871
|
+
job.error = error instanceof Error ? error.message : String(error);
|
|
7872
|
+
job.endedAt = now();
|
|
7873
|
+
changed();
|
|
7874
|
+
return;
|
|
7875
|
+
}
|
|
7876
|
+
live.facts = factsBlock(await readProjectFacts(deps.cwd, live.switchPath, live.base?.key ?? null), job.surface, live.base);
|
|
7877
|
+
const slug = surfaceSlug(job.surface);
|
|
7878
|
+
const keys = new Set(existing);
|
|
7879
|
+
const titles = new Set(await deps.titles());
|
|
7880
|
+
const folder = dirname8(live.switchPath);
|
|
7881
|
+
const slots = [];
|
|
7882
|
+
const extension = live.switchPath.endsWith(".jsx") ? "jsx" : "tsx";
|
|
7883
|
+
const taken = async (key) => keys.has(key) || await isFile(join14(deps.cwd, folder, `${key}.${extension}`));
|
|
7884
|
+
for (const concept of concepts) {
|
|
7885
|
+
let key = `${slug}-${concept.key}`;
|
|
7886
|
+
for (let suffix = 2; await taken(key); suffix += 1)
|
|
7887
|
+
key = `${slug}-${concept.key}-${suffix}`;
|
|
7888
|
+
keys.add(key);
|
|
7889
|
+
const title = freeTitle(concept.title, titles);
|
|
7890
|
+
live.concepts.set(key, { ...concept, title });
|
|
7891
|
+
slots.push({
|
|
7892
|
+
key,
|
|
7893
|
+
title,
|
|
7894
|
+
idea: concept.idea,
|
|
7895
|
+
file: join14(folder, `${key}.${extension}`),
|
|
7896
|
+
state: "building",
|
|
7897
|
+
startedAt: null,
|
|
7898
|
+
endedAt: null,
|
|
7899
|
+
failure: null,
|
|
7900
|
+
fixed: false,
|
|
7901
|
+
activity: null
|
|
7902
|
+
});
|
|
7903
|
+
}
|
|
7904
|
+
const source = await readFile14(join14(deps.cwd, live.switchPath), "utf8");
|
|
7905
|
+
for (const slot of slots) {
|
|
7906
|
+
await writeFile7(join14(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), {
|
|
7907
|
+
encoding: "utf8",
|
|
7908
|
+
flag: "wx"
|
|
7909
|
+
});
|
|
7910
|
+
}
|
|
7911
|
+
await writeFile7(join14(deps.cwd, live.switchPath), addSlots(source, slots.map((slot) => ({ key: slot.key, name: pascal(slot.key) }))), "utf8");
|
|
7912
|
+
for (const slot of slots) {
|
|
7913
|
+
await serially(() => deps.register({
|
|
7914
|
+
title: slot.title,
|
|
7915
|
+
url: `/?v-${slug}=${slot.key}`,
|
|
7916
|
+
note: slot.idea,
|
|
7917
|
+
...provenance(job)
|
|
7918
|
+
}));
|
|
7919
|
+
}
|
|
7920
|
+
job.slots = slots;
|
|
7921
|
+
if (job.state !== "planning") {
|
|
7922
|
+
for (const slot of slots) {
|
|
7923
|
+
slot.state = "stopped";
|
|
7924
|
+
slot.endedAt = now();
|
|
7925
|
+
}
|
|
7926
|
+
changed();
|
|
7927
|
+
return;
|
|
7928
|
+
}
|
|
7929
|
+
job.state = "building";
|
|
7930
|
+
job.plannedAt = now();
|
|
7931
|
+
changed();
|
|
7932
|
+
await Promise.all(job.slots.map((slot) => {
|
|
7933
|
+
const attempt = begin(live, slot);
|
|
7934
|
+
return guarded(build(live, slot, attempt), (message2) => lost(live, slot, attempt, message2));
|
|
7935
|
+
}));
|
|
7936
|
+
};
|
|
7937
|
+
const find = (id) => lives.find((live) => live.job.id === id);
|
|
7938
|
+
const stop = async (id, key) => {
|
|
7939
|
+
const live = find(id);
|
|
7940
|
+
if (live === void 0)
|
|
7941
|
+
return false;
|
|
7942
|
+
const { job } = live;
|
|
7943
|
+
if (key === void 0 && job.state === "planning") {
|
|
7944
|
+
job.state = "stopped";
|
|
7945
|
+
job.endedAt = now();
|
|
7946
|
+
changed();
|
|
7947
|
+
await live.plan?.stop();
|
|
7948
|
+
await drain(live);
|
|
7949
|
+
return true;
|
|
7950
|
+
}
|
|
7951
|
+
const targets = job.slots.filter((slot) => (key === void 0 || slot.key === key) && (slot.state === "building" || slot.state === "checking"));
|
|
7952
|
+
for (const slot of targets) {
|
|
7953
|
+
begin(live, slot);
|
|
7954
|
+
slot.state = "stopped";
|
|
7955
|
+
slot.endedAt = now();
|
|
7956
|
+
}
|
|
7957
|
+
settle(live);
|
|
7958
|
+
changed();
|
|
7959
|
+
await Promise.all(targets.map((slot) => release(live, slot)));
|
|
7960
|
+
return targets.length > 0;
|
|
7961
|
+
};
|
|
7962
|
+
return {
|
|
7963
|
+
async start(request) {
|
|
7964
|
+
if (closed)
|
|
7965
|
+
return { ok: false, error: "Leglas is closing." };
|
|
7966
|
+
const agent = request.agent.agent;
|
|
7967
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
7968
|
+
return {
|
|
7969
|
+
ok: false,
|
|
7970
|
+
error: "Building directions runs on Claude or Codex. Choose one of them as the agent to use it."
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7973
|
+
const count = Math.floor(request.count);
|
|
7974
|
+
if (!Number.isFinite(count) || count < 1 || count > MAX_DIRECTIONS) {
|
|
7975
|
+
return { ok: false, error: `Ask for between 1 and ${MAX_DIRECTIONS} directions.` };
|
|
7976
|
+
}
|
|
7977
|
+
const brief = request.brief.trim();
|
|
7978
|
+
const base = request.basedOn ?? null;
|
|
7979
|
+
if (brief === "" && base === null)
|
|
7980
|
+
return { ok: false, error: "Describe what the directions are for." };
|
|
7981
|
+
const slug = surfaceSlug(request.surface);
|
|
7982
|
+
if (slug === "")
|
|
7983
|
+
return { ok: false, error: "Name the surface to build directions for." };
|
|
7984
|
+
if (busy())
|
|
7985
|
+
return { ok: false, error: BUSY };
|
|
7986
|
+
const switchPath = await findSwitch(deps.cwd, slug);
|
|
7987
|
+
if (switchPath === null) {
|
|
7988
|
+
return {
|
|
7989
|
+
ok: false,
|
|
7990
|
+
error: `The ${slug} has no switch file yet. Run \`leglas new ${slug} --from <your component>\` and render ${pascal(slug)}Switch where the ${slug} is, then build directions into it.`
|
|
7991
|
+
};
|
|
7992
|
+
}
|
|
7993
|
+
const existing = directionKeys(await readFile14(join14(deps.cwd, switchPath), "utf8"));
|
|
7994
|
+
if (base !== null && !existing.includes(base.key)) {
|
|
7995
|
+
return {
|
|
7996
|
+
ok: false,
|
|
7997
|
+
error: `${base.title} is not a direction of the ${slug}, so Leglas cannot build variations of it.`
|
|
7998
|
+
};
|
|
7999
|
+
}
|
|
8000
|
+
const unbuilt = base === null ? void 0 : lives.flatMap((kept) => kept.job.slots).find((slot) => slot.key === base.key && slot.state !== "ready");
|
|
8001
|
+
if (base !== null && unbuilt !== void 0) {
|
|
8002
|
+
return {
|
|
8003
|
+
ok: false,
|
|
8004
|
+
error: `${base.title} has no finished design yet, so there is nothing to vary. Retry it first.`
|
|
8005
|
+
};
|
|
8006
|
+
}
|
|
8007
|
+
const job = {
|
|
8008
|
+
id: `gen-${now().toString(36)}`,
|
|
8009
|
+
surface: slug,
|
|
8010
|
+
brief,
|
|
8011
|
+
count,
|
|
8012
|
+
state: "planning",
|
|
8013
|
+
startedAt: now(),
|
|
8014
|
+
plannedAt: null,
|
|
8015
|
+
endedAt: null,
|
|
8016
|
+
error: null,
|
|
8017
|
+
slots: [],
|
|
8018
|
+
basedOn: base?.title ?? null,
|
|
8019
|
+
agent
|
|
8020
|
+
};
|
|
8021
|
+
const live = {
|
|
8022
|
+
job,
|
|
8023
|
+
concepts: /* @__PURE__ */ new Map(),
|
|
8024
|
+
runs: /* @__PURE__ */ new Map(),
|
|
8025
|
+
plan: null,
|
|
8026
|
+
switchPath,
|
|
8027
|
+
facts: null,
|
|
8028
|
+
stack: await stackOf(deps.cwd, switchPath),
|
|
8029
|
+
attempts: /* @__PURE__ */ new Map(),
|
|
8030
|
+
base,
|
|
8031
|
+
agent: agent === "codex" ? codex(await codexServers(deps.codexHome)) : CLAUDE,
|
|
8032
|
+
pending: /* @__PURE__ */ new Set()
|
|
8033
|
+
};
|
|
8034
|
+
if (busy())
|
|
8035
|
+
return { ok: false, error: BUSY };
|
|
8036
|
+
lives.push(live);
|
|
8037
|
+
while (lives.length > KEPT_JOBS)
|
|
8038
|
+
lives.shift();
|
|
8039
|
+
changed();
|
|
8040
|
+
detach(live, plan(live, existing), (message2) => {
|
|
8041
|
+
job.state = "failed";
|
|
8042
|
+
job.error = message2;
|
|
8043
|
+
job.endedAt = now();
|
|
8044
|
+
changed();
|
|
8045
|
+
});
|
|
8046
|
+
return { ok: true, job: copy(job) };
|
|
8047
|
+
},
|
|
8048
|
+
snapshot: () => lives.map((live) => copy(live.job)),
|
|
8049
|
+
stop,
|
|
8050
|
+
retry(id, key) {
|
|
8051
|
+
const live = find(id);
|
|
8052
|
+
const slot = live?.job.slots.find((candidate) => candidate.key === key);
|
|
8053
|
+
if (closed || live === void 0 || slot === void 0 || slot.state !== "failed" && slot.state !== "stopped" || live.runs.has(slot.key))
|
|
8054
|
+
return false;
|
|
8055
|
+
const attempt = begin(live, slot);
|
|
8056
|
+
slot.state = "building";
|
|
8057
|
+
slot.startedAt = now();
|
|
8058
|
+
slot.failure = null;
|
|
8059
|
+
detach(live, restore(slot).then(() => build(live, slot, attempt)), (message2) => lost(live, slot, attempt, message2));
|
|
8060
|
+
live.job.state = "building";
|
|
8061
|
+
live.job.endedAt = null;
|
|
8062
|
+
changed();
|
|
8063
|
+
return true;
|
|
8064
|
+
},
|
|
8065
|
+
replace(id, key) {
|
|
8066
|
+
const live = find(id);
|
|
8067
|
+
const slot = live?.job.slots.find((candidate) => candidate.key === key);
|
|
8068
|
+
if (closed || live === void 0 || slot === void 0 || slot.state === "building" || slot.state === "checking" || live.runs.has(slot.key))
|
|
8069
|
+
return false;
|
|
8070
|
+
const attempt = begin(live, slot);
|
|
8071
|
+
slot.state = "building";
|
|
8072
|
+
slot.startedAt = now();
|
|
8073
|
+
slot.failure = null;
|
|
8074
|
+
live.job.state = "building";
|
|
8075
|
+
live.job.endedAt = null;
|
|
8076
|
+
changed();
|
|
8077
|
+
const replacing = async () => {
|
|
8078
|
+
const asking = run4(live.agent, live.agent.plan(replacePrompt2({
|
|
8079
|
+
surface: live.job.surface,
|
|
8080
|
+
brief: live.job.brief,
|
|
8081
|
+
avoid: [...live.concepts.values()],
|
|
8082
|
+
base: live.base
|
|
8083
|
+
})), PLAN_DEADLINE_MS);
|
|
8084
|
+
live.runs.set(slot.key, asking);
|
|
8085
|
+
const outcome = await asking.done;
|
|
8086
|
+
if (!owns(live, slot, attempt))
|
|
8087
|
+
return;
|
|
8088
|
+
if (live.runs.get(slot.key) === asking)
|
|
8089
|
+
live.runs.delete(slot.key);
|
|
8090
|
+
let concept;
|
|
8091
|
+
try {
|
|
8092
|
+
if (outcome.code !== 0)
|
|
8093
|
+
throw new Error(failureOf2(live.agent, outcome).message);
|
|
8094
|
+
concept = parseConcepts(live.agent.result(outcome.lines) ?? "", 1)[0];
|
|
8095
|
+
} catch (error) {
|
|
8096
|
+
fail(live, slot, {
|
|
8097
|
+
code: "unreadable-plan",
|
|
8098
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8099
|
+
});
|
|
8100
|
+
settle(live);
|
|
8101
|
+
changed();
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
8104
|
+
const titles = new Set(await deps.titles());
|
|
8105
|
+
if (!owns(live, slot, attempt))
|
|
8106
|
+
return;
|
|
8107
|
+
titles.delete(slot.title);
|
|
8108
|
+
const title = freeTitle(concept.title, titles);
|
|
8109
|
+
await serially(async () => {
|
|
8110
|
+
await deps.unregister([slot.title]);
|
|
8111
|
+
return deps.register({
|
|
8112
|
+
title,
|
|
8113
|
+
url: `/?v-${surfaceSlug(live.job.surface)}=${slot.key}`,
|
|
8114
|
+
note: concept.idea,
|
|
8115
|
+
...provenance(live.job)
|
|
8116
|
+
});
|
|
8117
|
+
});
|
|
8118
|
+
live.concepts.set(slot.key, { ...concept, title });
|
|
8119
|
+
slot.title = title;
|
|
8120
|
+
slot.idea = concept.idea;
|
|
8121
|
+
if (!owns(live, slot, attempt))
|
|
8122
|
+
return;
|
|
8123
|
+
await restore(slot);
|
|
8124
|
+
await build(live, slot, attempt);
|
|
8125
|
+
};
|
|
8126
|
+
detach(live, replacing(), (message2) => lost(live, slot, attempt, message2));
|
|
8127
|
+
return true;
|
|
8128
|
+
},
|
|
8129
|
+
async close() {
|
|
8130
|
+
closed = true;
|
|
8131
|
+
await Promise.race([
|
|
8132
|
+
Promise.all(lives.map(async (live) => {
|
|
8133
|
+
await stop(live.job.id);
|
|
8134
|
+
await drain(live);
|
|
8135
|
+
})),
|
|
8136
|
+
wait(KILL_GRACE_MS * 2 + 1e3)
|
|
8137
|
+
]);
|
|
8138
|
+
}
|
|
8139
|
+
};
|
|
8140
|
+
}
|
|
8141
|
+
|
|
8142
|
+
// ../server/dist/server-info.js
|
|
8143
|
+
import { lstat as lstat2, mkdir as mkdir7, readFile as readFile15, rename as rename2, rm as rm4, writeFile as writeFile8 } from "fs/promises";
|
|
8144
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
8145
|
+
var SERVER_INFO_PATH = ".leglas/server.json";
|
|
8146
|
+
async function writeServerInfo(cwd, info) {
|
|
8147
|
+
const path = join15(cwd, SERVER_INFO_PATH);
|
|
8148
|
+
const directory = dirname9(path);
|
|
8149
|
+
await mkdir7(directory, { recursive: true });
|
|
8150
|
+
if (!(await lstat2(directory)).isDirectory())
|
|
8151
|
+
return;
|
|
8152
|
+
const existing = await lstat2(path).catch(() => null);
|
|
8153
|
+
if (existing !== null && !existing.isFile())
|
|
8154
|
+
return;
|
|
8155
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
8156
|
+
await writeFile8(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
8157
|
+
`, "utf8");
|
|
8158
|
+
await rename2(temporary, path).catch(async (cause) => {
|
|
8159
|
+
await rm4(temporary, { force: true }).catch(() => {
|
|
8160
|
+
});
|
|
8161
|
+
throw cause;
|
|
8162
|
+
});
|
|
8163
|
+
}
|
|
8164
|
+
async function readServerInfo(cwd) {
|
|
8165
|
+
try {
|
|
8166
|
+
const value = parseJson(await readFile15(join15(cwd, SERVER_INFO_PATH), "utf8"));
|
|
8167
|
+
if (!isJsonRecord(value) || !isNumber(value.port) || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535 || !isString(value.url) || value.url === "" || !isNumber(value.pid) || !Number.isInteger(value.pid))
|
|
8168
|
+
return null;
|
|
8169
|
+
return { port: value.port, url: value.url, pid: value.pid };
|
|
8170
|
+
} catch {
|
|
8171
|
+
return null;
|
|
8172
|
+
}
|
|
8173
|
+
}
|
|
8174
|
+
async function removeServerInfo(cwd, expected) {
|
|
8175
|
+
if (expected !== void 0) {
|
|
8176
|
+
const current = await readServerInfo(cwd);
|
|
8177
|
+
if (current !== null && (current.port !== expected.port || current.pid !== expected.pid))
|
|
8178
|
+
return;
|
|
8179
|
+
}
|
|
8180
|
+
await rm4(join15(cwd, SERVER_INFO_PATH), { force: true });
|
|
8181
|
+
}
|
|
8182
|
+
|
|
8183
|
+
// ../server/dist/server.js
|
|
8184
|
+
var LEGLAS_PREFIX = "/leglas";
|
|
8185
|
+
var DEFAULT_PORT = 4100;
|
|
8186
|
+
var PORT_ATTEMPTS = 20;
|
|
8187
|
+
var REFERENCE_MAX_BYTES = 1e7;
|
|
8188
|
+
var ATTACHED_WINDOW_MS = 6e3;
|
|
8189
|
+
var CONTENT_TYPES = {
|
|
8190
|
+
".css": "text/css; charset=utf-8",
|
|
8191
|
+
".gif": "image/gif",
|
|
8192
|
+
".html": "text/html; charset=utf-8",
|
|
8193
|
+
".ico": "image/x-icon",
|
|
8194
|
+
".jpeg": "image/jpeg",
|
|
8195
|
+
".jpg": "image/jpeg",
|
|
8196
|
+
".js": "text/javascript; charset=utf-8",
|
|
8197
|
+
".json": "application/json; charset=utf-8",
|
|
8198
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
8199
|
+
".png": "image/png",
|
|
8200
|
+
".svg": "image/svg+xml",
|
|
8201
|
+
".txt": "text/plain; charset=utf-8",
|
|
8202
|
+
".webp": "image/webp",
|
|
8203
|
+
".woff": "font/woff",
|
|
8204
|
+
".woff2": "font/woff2"
|
|
8205
|
+
};
|
|
8206
|
+
function isContentExtension(extension) {
|
|
8207
|
+
return Object.hasOwn(CONTENT_TYPES, extension);
|
|
8208
|
+
}
|
|
8209
|
+
var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
|
|
8210
|
+
function sendJson2(res, status, body) {
|
|
8211
|
+
const payload = JSON.stringify(body);
|
|
8212
|
+
res.writeHead(status, {
|
|
8213
|
+
"content-type": "application/json; charset=utf-8",
|
|
8214
|
+
"cache-control": "no-store"
|
|
8215
|
+
});
|
|
8216
|
+
res.end(payload);
|
|
8217
|
+
}
|
|
8218
|
+
function etagMatches(value, etag) {
|
|
8219
|
+
if (value === void 0)
|
|
8220
|
+
return false;
|
|
8221
|
+
const values = Array.isArray(value) ? value : [value];
|
|
8222
|
+
return values.some((header) => header.split(",").some((candidate) => {
|
|
8223
|
+
const tag = candidate.trim();
|
|
8224
|
+
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
8225
|
+
}));
|
|
8226
|
+
}
|
|
8227
|
+
function sendConditionalJson(req, res, body) {
|
|
8228
|
+
const payload = JSON.stringify(body);
|
|
8229
|
+
const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
|
|
8230
|
+
if (etagMatches(req.headers["if-none-match"], etag)) {
|
|
8231
|
+
res.writeHead(304, { etag, "cache-control": "private, no-cache" });
|
|
8232
|
+
res.end();
|
|
8233
|
+
return;
|
|
8234
|
+
}
|
|
8235
|
+
res.writeHead(200, {
|
|
8236
|
+
"content-type": "application/json; charset=utf-8",
|
|
8237
|
+
"cache-control": "private, no-cache",
|
|
8238
|
+
etag
|
|
8239
|
+
});
|
|
8240
|
+
res.end(payload);
|
|
8241
|
+
}
|
|
8242
|
+
var CAPTURE_DEADLINE_MS = 15e3;
|
|
8243
|
+
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
8244
|
+
function captureSlug(title) {
|
|
8245
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "direction";
|
|
8246
|
+
}
|
|
8247
|
+
function referenceName(value) {
|
|
8248
|
+
const raw = Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
|
8249
|
+
const safe = [...raw].filter((character) => {
|
|
8250
|
+
const code = character.charCodeAt(0);
|
|
8251
|
+
return code >= 32 && code <= 126 && character !== "/" && character !== "\\";
|
|
8252
|
+
}).join("").trim().slice(0, 80);
|
|
8253
|
+
return safe || "image";
|
|
8254
|
+
}
|
|
8255
|
+
function isAddressInUse(cause) {
|
|
8256
|
+
return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EADDRINUSE";
|
|
8257
|
+
}
|
|
8258
|
+
function isKnownAgent(value) {
|
|
8259
|
+
return isString(value) && Object.hasOwn(KNOWN_AGENTS, value);
|
|
8260
|
+
}
|
|
8261
|
+
function isAllowedMutationHost(hostname) {
|
|
8262
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
8263
|
+
if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1")
|
|
8264
|
+
return true;
|
|
8265
|
+
if (bare.endsWith(".local"))
|
|
8266
|
+
return true;
|
|
8267
|
+
if (!net3.isIPv4(bare))
|
|
8268
|
+
return false;
|
|
8269
|
+
const [first, second] = bare.split(".").map(Number);
|
|
8270
|
+
return first === 10 || first === 172 && second !== void 0 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
8271
|
+
}
|
|
8272
|
+
function isLoopbackAddress(address) {
|
|
8273
|
+
if (address === void 0)
|
|
8274
|
+
return false;
|
|
8275
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1" || address.startsWith("127.");
|
|
8276
|
+
}
|
|
8277
|
+
function isTrustedMutation(req) {
|
|
8278
|
+
if (!isLoopbackAddress(req.socket.remoteAddress))
|
|
8279
|
+
return false;
|
|
8280
|
+
if (!isString(req.headers.host))
|
|
8281
|
+
return false;
|
|
8282
|
+
let host;
|
|
8283
|
+
try {
|
|
8284
|
+
host = new URL(`http://${req.headers.host}`);
|
|
8285
|
+
} catch {
|
|
8286
|
+
return false;
|
|
8287
|
+
}
|
|
8288
|
+
if (!isAllowedMutationHost(host.hostname))
|
|
8289
|
+
return false;
|
|
8290
|
+
const rawOrigin = req.headers.origin;
|
|
8291
|
+
if (rawOrigin === void 0)
|
|
8292
|
+
return true;
|
|
8293
|
+
try {
|
|
8294
|
+
const origin = new URL(rawOrigin);
|
|
8295
|
+
return origin.protocol === "http:" && origin.host === host.host;
|
|
8296
|
+
} catch {
|
|
8297
|
+
return false;
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
function isEnded(request, failedIds) {
|
|
8301
|
+
return isTerminal(request.status) || failedIds.includes(request.id);
|
|
8302
|
+
}
|
|
8303
|
+
function jsonBody(body) {
|
|
8304
|
+
try {
|
|
8305
|
+
const parsed2 = parseJson(body || "{}");
|
|
8306
|
+
return isJsonRecord(parsed2) ? parsed2 : null;
|
|
8307
|
+
} catch {
|
|
8308
|
+
return null;
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
function requestIntent(value) {
|
|
8312
|
+
if (value === null || value === void 0)
|
|
8313
|
+
return "";
|
|
8314
|
+
return isString(value) ? value.trim() : null;
|
|
8315
|
+
}
|
|
8316
|
+
function hasJsonBody(req) {
|
|
8317
|
+
const contentType = req.headers["content-type"];
|
|
8318
|
+
return isString(contentType) && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
8319
|
+
}
|
|
8320
|
+
function probe(target, timeoutMs = 1e3) {
|
|
8321
|
+
return new Promise((resolve5) => {
|
|
8322
|
+
let url;
|
|
8323
|
+
try {
|
|
8324
|
+
url = new URL(target);
|
|
8325
|
+
} catch {
|
|
8326
|
+
return resolve5(false);
|
|
8327
|
+
}
|
|
8328
|
+
const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
8329
|
+
const socket = net3.connect(port, url.hostname);
|
|
8330
|
+
const settle = (reachable) => {
|
|
7083
8331
|
socket.destroy();
|
|
7084
8332
|
resolve5(reachable);
|
|
7085
8333
|
};
|
|
@@ -7090,8 +8338,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
7090
8338
|
});
|
|
7091
8339
|
}
|
|
7092
8340
|
function serveFrom(res, dir, relativePath) {
|
|
7093
|
-
const
|
|
7094
|
-
const candidate =
|
|
8341
|
+
const relative8 = normalize2(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
8342
|
+
const candidate = join16(dir, relative8);
|
|
7095
8343
|
if (!candidate.startsWith(dir))
|
|
7096
8344
|
return false;
|
|
7097
8345
|
if (!existsSync3(candidate) || !statSync(candidate).isFile())
|
|
@@ -7105,25 +8353,25 @@ function serveFrom(res, dir, relativePath) {
|
|
|
7105
8353
|
return true;
|
|
7106
8354
|
}
|
|
7107
8355
|
function serveShellFile(res, shellDir, urlPath) {
|
|
7108
|
-
const
|
|
7109
|
-
const isRoot =
|
|
7110
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
8356
|
+
const relative8 = normalize2(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
8357
|
+
const isRoot = relative8 === "" || relative8 === "." || relative8 === "/";
|
|
8358
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative8);
|
|
7111
8359
|
}
|
|
7112
8360
|
var HEALTH_PROBE_MS = 3e3;
|
|
7113
8361
|
function fileStamp(path) {
|
|
7114
8362
|
try {
|
|
7115
|
-
const
|
|
7116
|
-
return `${
|
|
8363
|
+
const stat6 = statSync(path);
|
|
8364
|
+
return `${stat6.dev}:${stat6.ino}:${stat6.size}:${stat6.mtimeMs}:${stat6.ctimeMs}`;
|
|
7117
8365
|
} catch {
|
|
7118
8366
|
return null;
|
|
7119
8367
|
}
|
|
7120
8368
|
}
|
|
7121
8369
|
function watchLiveFiles(cwd, configPath, live) {
|
|
7122
|
-
const leglasDir =
|
|
8370
|
+
const leglasDir = join16(cwd, ".leglas");
|
|
7123
8371
|
const targets = [
|
|
7124
|
-
{ path:
|
|
7125
|
-
{ path:
|
|
7126
|
-
{ path:
|
|
8372
|
+
{ path: join16(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
8373
|
+
{ path: join16(cwd, REQUESTS_PATH), change: "requests" },
|
|
8374
|
+
{ path: join16(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
7127
8375
|
];
|
|
7128
8376
|
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
7129
8377
|
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
@@ -7237,7 +8485,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7237
8485
|
}
|
|
7238
8486
|
if (configPath !== null) {
|
|
7239
8487
|
try {
|
|
7240
|
-
const directory =
|
|
8488
|
+
const directory = dirname10(configPath);
|
|
7241
8489
|
const name = basename3(configPath);
|
|
7242
8490
|
const target = { path: configPath, change: "config" };
|
|
7243
8491
|
known.set(configPath, fileStamp(configPath));
|
|
@@ -7275,7 +8523,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7275
8523
|
}
|
|
7276
8524
|
};
|
|
7277
8525
|
}
|
|
7278
|
-
function watchHealth(target, live) {
|
|
8526
|
+
function watchHealth(target, live, intervalMs) {
|
|
7279
8527
|
let previous = null;
|
|
7280
8528
|
let probing = false;
|
|
7281
8529
|
let closed = false;
|
|
@@ -7298,7 +8546,7 @@ function watchHealth(target, live) {
|
|
|
7298
8546
|
}).finally(() => {
|
|
7299
8547
|
probing = false;
|
|
7300
8548
|
});
|
|
7301
|
-
},
|
|
8549
|
+
}, intervalMs);
|
|
7302
8550
|
timer.unref?.();
|
|
7303
8551
|
return {
|
|
7304
8552
|
close: () => {
|
|
@@ -7322,15 +8570,15 @@ function configStalenessNotice(cwd, boot, current) {
|
|
|
7322
8570
|
if (boot === null && current === null)
|
|
7323
8571
|
return null;
|
|
7324
8572
|
if (boot === null && current !== null) {
|
|
7325
|
-
const label =
|
|
8573
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7326
8574
|
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
7327
8575
|
}
|
|
7328
8576
|
if (boot !== null && current === null) {
|
|
7329
|
-
const label =
|
|
8577
|
+
const label = relative4(cwd, boot.path) || boot.path;
|
|
7330
8578
|
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
7331
8579
|
}
|
|
7332
8580
|
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
7333
|
-
const label =
|
|
8581
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7334
8582
|
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
7335
8583
|
}
|
|
7336
8584
|
return null;
|
|
@@ -7376,7 +8624,7 @@ async function bind2(server, requested) {
|
|
|
7376
8624
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
7377
8625
|
}
|
|
7378
8626
|
async function startServer(options) {
|
|
7379
|
-
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
8627
|
+
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents(), captureDeadlineMs = CAPTURE_DEADLINE_MS, healthProbeMs = HEALTH_PROBE_MS } = options;
|
|
7380
8628
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7381
8629
|
let shares = null;
|
|
7382
8630
|
let liveHealth = null;
|
|
@@ -7421,6 +8669,7 @@ async function startServer(options) {
|
|
|
7421
8669
|
let lastSeen = null;
|
|
7422
8670
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
7423
8671
|
let runner = null;
|
|
8672
|
+
let generations = null;
|
|
7424
8673
|
let agentsCache = null;
|
|
7425
8674
|
let agentsInflight = null;
|
|
7426
8675
|
const AGENTS_FRESH_MS = 3e4;
|
|
@@ -7471,11 +8720,36 @@ async function startServer(options) {
|
|
|
7471
8720
|
});
|
|
7472
8721
|
});
|
|
7473
8722
|
};
|
|
8723
|
+
const readGenerationBody = (req, res, run4) => {
|
|
8724
|
+
if (!hasJsonBody(req)) {
|
|
8725
|
+
return sendJson2(res, 400, { ok: false, error: "A generation request must be JSON." });
|
|
8726
|
+
}
|
|
8727
|
+
let body = "";
|
|
8728
|
+
req.on("data", (chunk) => body += chunk);
|
|
8729
|
+
req.on("end", () => {
|
|
8730
|
+
const parsed2 = jsonBody(body);
|
|
8731
|
+
if (parsed2 === null)
|
|
8732
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
8733
|
+
if (generations === null)
|
|
8734
|
+
return sendJson2(res, 503, { ok: false, error: "Leglas is still starting." });
|
|
8735
|
+
void run4(parsed2, generations);
|
|
8736
|
+
});
|
|
8737
|
+
};
|
|
8738
|
+
const actOnGeneration = async (res, action, parsed2, act) => {
|
|
8739
|
+
if (!isString(parsed2.id) || parsed2.slot !== void 0 && !isString(parsed2.slot)) {
|
|
8740
|
+
return sendJson2(res, 400, {
|
|
8741
|
+
ok: false,
|
|
8742
|
+
error: "Name the generation by its id, and the direction by its slot."
|
|
8743
|
+
});
|
|
8744
|
+
}
|
|
8745
|
+
const done = await act(parsed2.id, isString(parsed2.slot) ? parsed2.slot : void 0);
|
|
8746
|
+
return done ? sendJson2(res, 200, { ok: true }) : sendJson2(res, 409, { ok: false, error: `Nothing to ${action} there right now.` });
|
|
8747
|
+
};
|
|
7474
8748
|
const handleRequest = (req, res, context) => {
|
|
7475
8749
|
const url = req.url ?? "/";
|
|
7476
8750
|
const path = url.split("?")[0] ?? "/";
|
|
7477
8751
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
7478
|
-
if (!context.remote && req.method
|
|
8752
|
+
if (!context.remote && req.method !== "GET" && req.method !== "HEAD" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7479
8753
|
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7480
8754
|
}
|
|
7481
8755
|
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
@@ -7493,6 +8767,48 @@ async function startServer(options) {
|
|
|
7493
8767
|
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7494
8768
|
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7495
8769
|
}
|
|
8770
|
+
if (path === `${LEGLAS_PREFIX}/api/generate` && req.method === "GET") {
|
|
8771
|
+
return sendJson2(res, 200, { ok: true, jobs: generations?.snapshot() ?? [] });
|
|
8772
|
+
}
|
|
8773
|
+
if (path === `${LEGLAS_PREFIX}/api/generate` && req.method === "POST") {
|
|
8774
|
+
return void readGenerationBody(req, res, async (parsed2, running) => {
|
|
8775
|
+
if (!isString(parsed2.surface) || !isString(parsed2.brief)) {
|
|
8776
|
+
return sendJson2(res, 400, {
|
|
8777
|
+
ok: false,
|
|
8778
|
+
error: "Name a surface and describe the directions in a brief."
|
|
8779
|
+
});
|
|
8780
|
+
}
|
|
8781
|
+
let basedOn = null;
|
|
8782
|
+
if (parsed2.basedOn !== void 0 && parsed2.basedOn !== null && parsed2.basedOn !== "") {
|
|
8783
|
+
const title = parsed2.basedOn;
|
|
8784
|
+
const preview = isString(title) ? (await livePreviews()).find((entry) => entry.title === title) : void 0;
|
|
8785
|
+
basedOn = preview === void 0 ? null : baseOf(preview, parsed2.surface);
|
|
8786
|
+
if (basedOn === null) {
|
|
8787
|
+
return sendJson2(res, 422, {
|
|
8788
|
+
ok: false,
|
|
8789
|
+
error: `${isString(title) ? title : "That"} is not a direction of the ${surfaceSlug(parsed2.surface)}, so Leglas cannot build variations of it.`
|
|
8790
|
+
});
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8793
|
+
const started = await running.start({
|
|
8794
|
+
surface: parsed2.surface,
|
|
8795
|
+
brief: parsed2.brief,
|
|
8796
|
+
count: isNumber(parsed2.count) ? parsed2.count : 3,
|
|
8797
|
+
agent: await readAgentChoice(cwd),
|
|
8798
|
+
basedOn
|
|
8799
|
+
});
|
|
8800
|
+
return started.ok ? sendJson2(res, 202, { ok: true, job: started.job }) : sendJson2(res, 422, { ok: false, error: started.error });
|
|
8801
|
+
});
|
|
8802
|
+
}
|
|
8803
|
+
if (path === `${LEGLAS_PREFIX}/api/generate/stop` && req.method === "POST") {
|
|
8804
|
+
return void readGenerationBody(req, res, (parsed2, running) => actOnGeneration(res, "stop", parsed2, (id, slot) => running.stop(id, slot)));
|
|
8805
|
+
}
|
|
8806
|
+
if (path === `${LEGLAS_PREFIX}/api/generate/retry` && req.method === "POST") {
|
|
8807
|
+
return void readGenerationBody(req, res, (parsed2, running) => actOnGeneration(res, "retry", parsed2, (id, slot) => slot === void 0 ? false : running.retry(id, slot)));
|
|
8808
|
+
}
|
|
8809
|
+
if (path === `${LEGLAS_PREFIX}/api/generate/replace` && req.method === "POST") {
|
|
8810
|
+
return void readGenerationBody(req, res, (parsed2, running) => actOnGeneration(res, "replace", parsed2, (id, slot) => slot === void 0 ? false : running.replace(id, slot)));
|
|
8811
|
+
}
|
|
7496
8812
|
if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
|
|
7497
8813
|
const updates = options.updates;
|
|
7498
8814
|
if (updates === void 0) {
|
|
@@ -7756,8 +9072,8 @@ async function startServer(options) {
|
|
|
7756
9072
|
const id = newRequestId();
|
|
7757
9073
|
const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
|
|
7758
9074
|
try {
|
|
7759
|
-
await mkdir8(
|
|
7760
|
-
await
|
|
9075
|
+
await mkdir8(join16(cwd, REFERENCES_DIR), { recursive: true });
|
|
9076
|
+
await writeFile9(join16(cwd, file), body);
|
|
7761
9077
|
void pruneReferences(cwd).catch(() => {
|
|
7762
9078
|
});
|
|
7763
9079
|
return sendJson2(res, 200, {
|
|
@@ -7799,7 +9115,7 @@ async function startServer(options) {
|
|
|
7799
9115
|
}
|
|
7800
9116
|
const references = parsed2.references ?? [];
|
|
7801
9117
|
if (references.length > 0) {
|
|
7802
|
-
const present = new Set((await
|
|
9118
|
+
const present = new Set((await readdir5(join16(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
7803
9119
|
const gone = references.filter((id2) => !present.has(id2));
|
|
7804
9120
|
if (gone.length > 0) {
|
|
7805
9121
|
return sendJson2(res, 410, {
|
|
@@ -7929,7 +9245,7 @@ async function startServer(options) {
|
|
|
7929
9245
|
const timer = setTimeout(() => {
|
|
7930
9246
|
timedOut2();
|
|
7931
9247
|
controller.abort();
|
|
7932
|
-
},
|
|
9248
|
+
}, captureDeadlineMs);
|
|
7933
9249
|
timer.unref?.();
|
|
7934
9250
|
try {
|
|
7935
9251
|
const captureInput = {
|
|
@@ -7962,8 +9278,8 @@ async function startServer(options) {
|
|
|
7962
9278
|
const noteSuffix = isString(parsed2.note) ? `-${parsed2.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
|
|
7963
9279
|
const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
|
|
7964
9280
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
7965
|
-
await mkdir8(
|
|
7966
|
-
await
|
|
9281
|
+
await mkdir8(join16(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
9282
|
+
await writeFile9(join16(cwd, relativeFile), shot.png);
|
|
7967
9283
|
return sendJson2(res, 200, {
|
|
7968
9284
|
ok: true,
|
|
7969
9285
|
file: relativeFile,
|
|
@@ -8343,15 +9659,15 @@ async function startServer(options) {
|
|
|
8343
9659
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
8344
9660
|
const slash = rest.indexOf("/");
|
|
8345
9661
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
8346
|
-
let
|
|
9662
|
+
let relative8 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
8347
9663
|
try {
|
|
8348
|
-
|
|
9664
|
+
relative8 = decodeURIComponent(relative8);
|
|
8349
9665
|
} catch {
|
|
8350
|
-
|
|
9666
|
+
relative8 = "";
|
|
8351
9667
|
}
|
|
8352
9668
|
const dir = fileMounts.get(slug);
|
|
8353
9669
|
const serveMount = () => {
|
|
8354
|
-
if (dir !== void 0 &&
|
|
9670
|
+
if (dir !== void 0 && relative8 !== "" && serveFrom(res, dir, relative8))
|
|
8355
9671
|
return;
|
|
8356
9672
|
res.writeHead(404, {
|
|
8357
9673
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -8361,7 +9677,7 @@ async function startServer(options) {
|
|
|
8361
9677
|
};
|
|
8362
9678
|
if (!context.remote)
|
|
8363
9679
|
return serveMount();
|
|
8364
|
-
if (
|
|
9680
|
+
if (relative8.split("/").some((segment) => segment.startsWith("."))) {
|
|
8365
9681
|
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8366
9682
|
}
|
|
8367
9683
|
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
@@ -8442,7 +9758,7 @@ async function startServer(options) {
|
|
|
8442
9758
|
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
8443
9759
|
options.updates?.setPort(port);
|
|
8444
9760
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
8445
|
-
liveHealth = watchHealth(target, live);
|
|
9761
|
+
liveHealth = watchHealth(target, live, healthProbeMs);
|
|
8446
9762
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
8447
9763
|
});
|
|
8448
9764
|
await writeServerInfo(cwd, {
|
|
@@ -8462,6 +9778,42 @@ async function startServer(options) {
|
|
|
8462
9778
|
if (options.claudeAgentSession !== void 0)
|
|
8463
9779
|
runnerOptions.claudeAgentSession = options.claudeAgentSession;
|
|
8464
9780
|
runner = startRunner(runnerOptions);
|
|
9781
|
+
const renderDirection = async (title) => {
|
|
9782
|
+
const preview = (await livePreviews()).find((entry) => entry.title === title);
|
|
9783
|
+
if (preview === void 0)
|
|
9784
|
+
return null;
|
|
9785
|
+
const browser = await browserPool.acquire();
|
|
9786
|
+
if (browser === null)
|
|
9787
|
+
return null;
|
|
9788
|
+
const address = server.address();
|
|
9789
|
+
const renderPort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
|
|
9790
|
+
try {
|
|
9791
|
+
const captured = await capturePage(browser, {
|
|
9792
|
+
url: previewUrl(`http://127.0.0.1:${renderPort}`, preview),
|
|
9793
|
+
width: 1440,
|
|
9794
|
+
timeoutMs: CAPTURE_LOAD_MS
|
|
9795
|
+
});
|
|
9796
|
+
return { errors: captured.errors };
|
|
9797
|
+
} catch (error) {
|
|
9798
|
+
return { errors: [error instanceof Error ? error.message : String(error)] };
|
|
9799
|
+
}
|
|
9800
|
+
};
|
|
9801
|
+
const generationDeps = {
|
|
9802
|
+
cwd,
|
|
9803
|
+
render: renderDirection,
|
|
9804
|
+
register: (input) => addLocalPreview(cwd, input, (config?.previews ?? []).filter((entry) => entry.local !== true)),
|
|
9805
|
+
unregister: async (titles) => {
|
|
9806
|
+
await dropLocalPreviews(cwd, titles);
|
|
9807
|
+
},
|
|
9808
|
+
titles: async () => {
|
|
9809
|
+
const local = await readLocalPreviews(cwd).catch(() => null);
|
|
9810
|
+
return new Set([...config?.previews ?? [], ...local?.previews ?? []].map((entry) => entry.title));
|
|
9811
|
+
},
|
|
9812
|
+
onChange: () => live.nudge("generation")
|
|
9813
|
+
};
|
|
9814
|
+
if (options.generationSpawn !== void 0)
|
|
9815
|
+
generationDeps.spawn = options.generationSpawn;
|
|
9816
|
+
generations = createGenerations(generationDeps);
|
|
8465
9817
|
options.updates?.onBusy(() => runner?.snapshot().running ?? false);
|
|
8466
9818
|
options.updates?.onChange(() => live.nudge("update"));
|
|
8467
9819
|
let closePromise = null;
|
|
@@ -8479,6 +9831,7 @@ async function startServer(options) {
|
|
|
8479
9831
|
shares?.close() ?? Promise.resolve(),
|
|
8480
9832
|
branches.stop(),
|
|
8481
9833
|
runner.stop(),
|
|
9834
|
+
generations?.close() ?? Promise.resolve(),
|
|
8482
9835
|
browserPool.close(),
|
|
8483
9836
|
live.close()
|
|
8484
9837
|
]);
|
|
@@ -8500,9 +9853,9 @@ async function startServer(options) {
|
|
|
8500
9853
|
// ../server/dist/update.js
|
|
8501
9854
|
import { spawn as spawnChild2 } from "child_process";
|
|
8502
9855
|
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
8503
|
-
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as
|
|
8504
|
-
import { homedir as
|
|
8505
|
-
import { dirname as
|
|
9856
|
+
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
9857
|
+
import { homedir as homedir3 } from "os";
|
|
9858
|
+
import { dirname as dirname11, join as join17, posix as posix2, win32 } from "path";
|
|
8506
9859
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8507
9860
|
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
8508
9861
|
var REGISTRY = "https://registry.npmjs.org";
|
|
@@ -8702,7 +10055,7 @@ function restartCommand(install, argv, latest, port, options) {
|
|
|
8702
10055
|
return spawnCommand(["leglas", ...rest], options.platform);
|
|
8703
10056
|
}
|
|
8704
10057
|
if (install.kind === "project" && install.root !== void 0) {
|
|
8705
|
-
const shim = (windows ? win32.join :
|
|
10058
|
+
const shim = (windows ? win32.join : join17)(install.root, "node_modules", ".bin", windows ? "leglas.cmd" : "leglas");
|
|
8706
10059
|
return spawnCommand(options.exists(shim) ? [shim, ...rest] : ["yarn", "leglas", ...rest], options.platform);
|
|
8707
10060
|
}
|
|
8708
10061
|
throw new Error(CHECKOUT_NOTICE);
|
|
@@ -8733,7 +10086,7 @@ function readState(path) {
|
|
|
8733
10086
|
}
|
|
8734
10087
|
function defaultStatePath(home) {
|
|
8735
10088
|
try {
|
|
8736
|
-
return
|
|
10089
|
+
return join17(home(), ".leglas", "update.json");
|
|
8737
10090
|
} catch {
|
|
8738
10091
|
return null;
|
|
8739
10092
|
}
|
|
@@ -8751,16 +10104,16 @@ function mergeState(current, saved) {
|
|
|
8751
10104
|
async function writeState(path, state) {
|
|
8752
10105
|
let temporary = null;
|
|
8753
10106
|
try {
|
|
8754
|
-
const directory =
|
|
10107
|
+
const directory = dirname11(path);
|
|
8755
10108
|
await mkdir9(directory, { recursive: true });
|
|
8756
10109
|
if (!(await lstat3(directory)).isDirectory())
|
|
8757
10110
|
return;
|
|
8758
10111
|
const existing = await lstat3(path).catch(() => null);
|
|
8759
10112
|
if (existing !== null && !existing.isFile())
|
|
8760
10113
|
return;
|
|
8761
|
-
temporary = await mkdtemp(
|
|
8762
|
-
const file =
|
|
8763
|
-
await
|
|
10114
|
+
temporary = await mkdtemp(join17(directory, ".update-"));
|
|
10115
|
+
const file = join17(temporary, "update.json");
|
|
10116
|
+
await writeFile10(file, `${JSON.stringify(state, null, 2)}
|
|
8764
10117
|
`, "utf8");
|
|
8765
10118
|
await rename3(file, path);
|
|
8766
10119
|
} catch {
|
|
@@ -8821,7 +10174,7 @@ function installerReason(stdout, stderr) {
|
|
|
8821
10174
|
const lines2 = (output2) => output2.split(/\r?\n/).filter((line) => !/^\s+at /.test(line) && !/A complete log of this run|info Visit https:\/\/yarnpkg\.com/.test(line)).map((line) => line.trim()).filter(Boolean);
|
|
8822
10175
|
const errors = lines2(stderr);
|
|
8823
10176
|
const output = lines2(stdout);
|
|
8824
|
-
const npm = [...errors, ...output].find((line) => /^npm error\s+\S/.test(line) && !/^npm error code\s/.test(line));
|
|
10177
|
+
const npm = [...errors, ...output].find((line) => /^npm error\s+\S/.test(line) && !/^npm error (code|syscall|file|path|dest|errno)\s/.test(line));
|
|
8825
10178
|
return npm?.replace(/^npm error\s+/, "") ?? errors[0] ?? output[0] ?? null;
|
|
8826
10179
|
}
|
|
8827
10180
|
function installVersion(install, version2, deps) {
|
|
@@ -8921,7 +10274,7 @@ function createUpdateService(input) {
|
|
|
8921
10274
|
const fetcher = deps.fetch ?? fetch;
|
|
8922
10275
|
const now = deps.now ?? Date.now;
|
|
8923
10276
|
const timeoutMs = deps.timeoutMs ?? 4e3;
|
|
8924
|
-
const statePath = deps.statePath === void 0 ? defaultStatePath(deps.homedir ??
|
|
10277
|
+
const statePath = deps.statePath === void 0 ? defaultStatePath(deps.homedir ?? homedir3) : deps.statePath;
|
|
8925
10278
|
const exists = deps.exists ?? existsSync4;
|
|
8926
10279
|
const env = deps.env ?? process.env;
|
|
8927
10280
|
const install = detectInstall(input.entry, input.cwd, exists, deps.realpath ?? realPath, env);
|
|
@@ -9144,7 +10497,7 @@ async function runClassify(options, deps) {
|
|
|
9144
10497
|
const declared = await Promise.all(
|
|
9145
10498
|
options.changes.map(async (change) => ({
|
|
9146
10499
|
...change,
|
|
9147
|
-
exists: await
|
|
10500
|
+
exists: await stat5(join18(options.cwd, change.path)).then(
|
|
9148
10501
|
() => true,
|
|
9149
10502
|
() => false
|
|
9150
10503
|
)
|
|
@@ -9231,7 +10584,7 @@ function detectFramework(packageJson) {
|
|
|
9231
10584
|
return "react";
|
|
9232
10585
|
}
|
|
9233
10586
|
}
|
|
9234
|
-
function
|
|
10587
|
+
function surfaceSlug2(surface) {
|
|
9235
10588
|
return surface.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
9236
10589
|
}
|
|
9237
10590
|
function titleCase(slug) {
|
|
@@ -9260,8 +10613,10 @@ type SearchParams = Record<string, string | string[] | undefined>;
|
|
|
9260
10613
|
/**
|
|
9261
10614
|
* Production always renders the fallback, whatever the URL says, so a branch
|
|
9262
10615
|
* point that reaches a deployed build cannot expose an unreleased direction.
|
|
10616
|
+
* It is not exported because a file whose exports are all components hot-swaps
|
|
10617
|
+
* in place, while an extra export makes every edit reload the page.
|
|
9263
10618
|
*/
|
|
9264
|
-
|
|
10619
|
+
function resolve${titleCase(slug).replace(/\s/g, "")}(searchParams: SearchParams): Direction {
|
|
9265
10620
|
if (process.env.NODE_ENV === "production") return FALLBACK;
|
|
9266
10621
|
const raw = searchParams["v-${slug}"];
|
|
9267
10622
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
@@ -9307,8 +10662,10 @@ function isProduction(): boolean {
|
|
|
9307
10662
|
/**
|
|
9308
10663
|
* Production always renders the fallback, whatever the URL says, so a branch
|
|
9309
10664
|
* point that reaches a deployed build cannot expose an unreleased direction.
|
|
10665
|
+
* It is not exported because a file whose exports are all components hot-swaps
|
|
10666
|
+
* in place, while an extra export makes every edit reload the page.
|
|
9310
10667
|
*/
|
|
9311
|
-
|
|
10668
|
+
function resolve${name}(): Direction {
|
|
9312
10669
|
if (isProduction()) return FALLBACK;
|
|
9313
10670
|
const value = new URLSearchParams(window.location.search).get("v-${slug}");
|
|
9314
10671
|
return value !== null && value in DIRECTIONS ? (value as Direction) : FALLBACK;
|
|
@@ -9324,14 +10681,14 @@ function placeholder(label, note) {
|
|
|
9324
10681
|
return `export function ${label}() {
|
|
9325
10682
|
return (
|
|
9326
10683
|
<div style={{ padding: "3rem", fontFamily: "system-ui" }}>
|
|
9327
|
-
<p
|
|
10684
|
+
<p>{${JSON.stringify(note)}}</p>
|
|
9328
10685
|
</div>
|
|
9329
10686
|
);
|
|
9330
10687
|
}
|
|
9331
10688
|
`;
|
|
9332
10689
|
}
|
|
9333
10690
|
function planNew(options) {
|
|
9334
|
-
const slug =
|
|
10691
|
+
const slug = surfaceSlug2(options.surface);
|
|
9335
10692
|
const framework = detectFramework(options.packageJson);
|
|
9336
10693
|
const name = titleCase(slug).replace(/\s/g, "");
|
|
9337
10694
|
const dir = `.leglas/variants/${slug}`;
|
|
@@ -9375,7 +10732,7 @@ Rewriting your component automatically is how a tool breaks a codebase it does n
|
|
|
9375
10732
|
|
|
9376
10733
|
// src/explore.ts
|
|
9377
10734
|
function planExplore(surface, count, basedOn = null) {
|
|
9378
|
-
const slug =
|
|
10735
|
+
const slug = surfaceSlug2(surface);
|
|
9379
10736
|
const goal = basedOn === null ? `Build ${count} design directions for "${surface}".
|
|
9380
10737
|
|
|
9381
10738
|
The set exists to be chosen from, and the choice only means something if the directions genuinely disagree: ${count} variants of one idea would make it empty. What counts as different is yours to decide, and the strongest sets disagree about more than styling.
|
|
@@ -9413,9 +10770,176 @@ function runExplore(options, deps) {
|
|
|
9413
10770
|
return { exitCode: 0 };
|
|
9414
10771
|
}
|
|
9415
10772
|
|
|
10773
|
+
// src/json.ts
|
|
10774
|
+
function isJsonObject(value) {
|
|
10775
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10776
|
+
}
|
|
10777
|
+
function isString2(value) {
|
|
10778
|
+
return typeof value === "string";
|
|
10779
|
+
}
|
|
10780
|
+
function isNumber2(value) {
|
|
10781
|
+
return typeof value === "number";
|
|
10782
|
+
}
|
|
10783
|
+
async function bodyOf(response) {
|
|
10784
|
+
try {
|
|
10785
|
+
return JSON.parse(await response.text());
|
|
10786
|
+
} catch {
|
|
10787
|
+
return null;
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
|
|
10791
|
+
// src/running.ts
|
|
10792
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
10793
|
+
var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
|
|
10794
|
+
async function sameDirectory(left, right) {
|
|
10795
|
+
const [a, b] = await Promise.all([
|
|
10796
|
+
realpath2(left).catch(() => left),
|
|
10797
|
+
realpath2(right).catch(() => right)
|
|
10798
|
+
]);
|
|
10799
|
+
return a === b;
|
|
10800
|
+
}
|
|
10801
|
+
async function findLeglas(cwd, port, request) {
|
|
10802
|
+
const server = port === null ? await readServerInfo(cwd) : null;
|
|
10803
|
+
const chosen = port ?? server?.port ?? DEFAULT_PORT;
|
|
10804
|
+
try {
|
|
10805
|
+
const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
|
|
10806
|
+
signal: AbortSignal.timeout(2e3)
|
|
10807
|
+
});
|
|
10808
|
+
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
10809
|
+
const answered = await health.text().then((text3) => JSON.parse(text3)).catch(() => ({}));
|
|
10810
|
+
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
10811
|
+
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
10812
|
+
return {
|
|
10813
|
+
ok: false,
|
|
10814
|
+
error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
|
|
10818
|
+
} catch {
|
|
10819
|
+
return { ok: false, error: NOT_RUNNING };
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
|
|
10823
|
+
// src/run-explore-build.ts
|
|
10824
|
+
var POLL_MS2 = 1e3;
|
|
10825
|
+
var WAIT_MS = 20 * 6e4;
|
|
10826
|
+
function readJob(value) {
|
|
10827
|
+
if (value === void 0 || !isJsonObject(value)) return null;
|
|
10828
|
+
const { id, state, slots, error, startedAt, endedAt } = value;
|
|
10829
|
+
if (!isString2(id) || !isString2(state) || !Array.isArray(slots)) return null;
|
|
10830
|
+
const read = slots.flatMap((entry) => {
|
|
10831
|
+
if (!isJsonObject(entry) || !isString2(entry.key) || !isString2(entry.title) || !isString2(entry.state))
|
|
10832
|
+
return [];
|
|
10833
|
+
const failure = entry.failure === void 0 || entry.failure === null || !isJsonObject(entry.failure) ? null : entry.failure;
|
|
10834
|
+
return [
|
|
10835
|
+
{
|
|
10836
|
+
key: entry.key,
|
|
10837
|
+
title: entry.title,
|
|
10838
|
+
state: entry.state,
|
|
10839
|
+
failure: failure !== null && isString2(failure.message) ? { message: failure.message } : null,
|
|
10840
|
+
fixed: entry.fixed === true
|
|
10841
|
+
}
|
|
10842
|
+
];
|
|
10843
|
+
});
|
|
10844
|
+
return {
|
|
10845
|
+
id,
|
|
10846
|
+
state,
|
|
10847
|
+
error: isString2(error) ? error : null,
|
|
10848
|
+
slots: read,
|
|
10849
|
+
startedAt: isNumber2(startedAt) ? startedAt : Date.now(),
|
|
10850
|
+
endedAt: isNumber2(endedAt) ? endedAt : null
|
|
10851
|
+
};
|
|
10852
|
+
}
|
|
10853
|
+
async function runExploreBuild(options, deps) {
|
|
10854
|
+
const request = deps.fetch ?? fetch;
|
|
10855
|
+
const sleep = deps.sleep ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
|
|
10856
|
+
const fail = (error) => {
|
|
10857
|
+
if (options.json) deps.log(JSON.stringify({ ok: false, error }));
|
|
10858
|
+
else deps.error(error);
|
|
10859
|
+
return { exitCode: 1 };
|
|
10860
|
+
};
|
|
10861
|
+
const found = await findLeglas(options.cwd, options.port, request);
|
|
10862
|
+
if (!found.ok) return fail(found.error);
|
|
10863
|
+
const base = `http://127.0.0.1:${found.port}/leglas/api/generate`;
|
|
10864
|
+
let started;
|
|
10865
|
+
try {
|
|
10866
|
+
started = await bodyOf(
|
|
10867
|
+
await request(base, {
|
|
10868
|
+
method: "POST",
|
|
10869
|
+
headers: { "content-type": "application/json", origin: `http://127.0.0.1:${found.port}` },
|
|
10870
|
+
body: JSON.stringify({
|
|
10871
|
+
surface: options.surface,
|
|
10872
|
+
brief: options.brief,
|
|
10873
|
+
count: options.count,
|
|
10874
|
+
basedOn: options.basedOn
|
|
10875
|
+
})
|
|
10876
|
+
})
|
|
10877
|
+
);
|
|
10878
|
+
} catch {
|
|
10879
|
+
return fail("Leglas did not answer. Is it still running?");
|
|
10880
|
+
}
|
|
10881
|
+
const answer = isJsonObject(started) ? started : null;
|
|
10882
|
+
const first = readJob(answer?.job);
|
|
10883
|
+
if (answer?.ok !== true || first === null) {
|
|
10884
|
+
return fail(isString2(answer?.error) ? answer.error : "Leglas could not start the build.");
|
|
10885
|
+
}
|
|
10886
|
+
if (!options.json) {
|
|
10887
|
+
deps.log(
|
|
10888
|
+
options.basedOn === null ? `Planning ${options.count} directions for the ${options.surface}\u2026` : `Planning ${options.count} variations of ${options.basedOn}\u2026`
|
|
10889
|
+
);
|
|
10890
|
+
}
|
|
10891
|
+
const said = /* @__PURE__ */ new Map();
|
|
10892
|
+
let announced = false;
|
|
10893
|
+
let job = first;
|
|
10894
|
+
const deadline = Date.now() + WAIT_MS;
|
|
10895
|
+
while (Date.now() < deadline) {
|
|
10896
|
+
await sleep(POLL_MS2);
|
|
10897
|
+
let listed;
|
|
10898
|
+
try {
|
|
10899
|
+
listed = await bodyOf(await request(base));
|
|
10900
|
+
} catch {
|
|
10901
|
+
return fail("Leglas stopped answering while the directions were being built.");
|
|
10902
|
+
}
|
|
10903
|
+
const jobs = isJsonObject(listed) ? listed.jobs : void 0;
|
|
10904
|
+
const latest = Array.isArray(jobs) ? jobs.map(readJob).find((candidate) => candidate?.id === first.id) : void 0;
|
|
10905
|
+
if (latest === void 0 || latest === null) continue;
|
|
10906
|
+
job = latest;
|
|
10907
|
+
if (!options.json && !announced && job.slots.length > 0) {
|
|
10908
|
+
announced = true;
|
|
10909
|
+
deps.log(
|
|
10910
|
+
`On the rail: ${job.slots.map((slot) => slot.title).join(", ")}. Building them now.`
|
|
10911
|
+
);
|
|
10912
|
+
}
|
|
10913
|
+
for (const slot of job.slots) {
|
|
10914
|
+
if (options.json || said.get(slot.key) === slot.state) continue;
|
|
10915
|
+
said.set(slot.key, slot.state);
|
|
10916
|
+
if (slot.state === "ready")
|
|
10917
|
+
deps.log(
|
|
10918
|
+
`Ready: ${slot.title}${slot.fixed ? " (its first version did not render; fixed)" : ""}`
|
|
10919
|
+
);
|
|
10920
|
+
else if (slot.state === "failed")
|
|
10921
|
+
deps.log(`Failed: ${slot.title}. ${slot.failure?.message ?? ""}`.trim());
|
|
10922
|
+
else if (slot.state === "stopped") deps.log(`Stopped: ${slot.title}`);
|
|
10923
|
+
}
|
|
10924
|
+
if (job.state === "done" || job.state === "failed" || job.state === "stopped") break;
|
|
10925
|
+
}
|
|
10926
|
+
const ready = job.slots.filter((slot) => slot.state === "ready").length;
|
|
10927
|
+
const ok = job.state === "done" && ready === job.slots.length && ready > 0;
|
|
10928
|
+
if (options.json) {
|
|
10929
|
+
deps.log(JSON.stringify({ ok, job }));
|
|
10930
|
+
return { exitCode: ok ? 0 : 1 };
|
|
10931
|
+
}
|
|
10932
|
+
if (job.state === "failed") return fail(job.error ?? "The build failed.");
|
|
10933
|
+
const seconds = job.endedAt === null ? null : Math.round((job.endedAt - job.startedAt) / 1e3);
|
|
10934
|
+
deps.log(
|
|
10935
|
+
`${ready} of ${job.slots.length} directions ready${seconds === null ? "" : ` in ${seconds} s`}. Compare them in the interface, or keep one with leglas keep.`
|
|
10936
|
+
);
|
|
10937
|
+
return { exitCode: ok ? 0 : 1 };
|
|
10938
|
+
}
|
|
10939
|
+
|
|
9416
10940
|
// src/run-init.ts
|
|
9417
|
-
import { readFile as
|
|
9418
|
-
import { join as
|
|
10941
|
+
import { readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
|
|
10942
|
+
import { join as join19 } from "path";
|
|
9419
10943
|
|
|
9420
10944
|
// src/init.ts
|
|
9421
10945
|
var AGENTS_MARKER_START = "<!-- leglas:start -->";
|
|
@@ -9534,7 +11058,8 @@ Useful to know:
|
|
|
9534
11058
|
they have asked for that is not done yet. Add \`--screenshot\` to render a PNG
|
|
9535
11059
|
you can inspect. Run it when handed a direction you did not register yourself.
|
|
9536
11060
|
- Every command accepts \`--json\` and prints one envelope with a stable exit
|
|
9537
|
-
code, so you can drive it without parsing prose.
|
|
11061
|
+
code, so you can drive it without parsing prose. \`watch\` keeps running, so
|
|
11062
|
+
it prints one JSON line per event instead.
|
|
9538
11063
|
|
|
9539
11064
|
${AGENTS_MARKER_END}
|
|
9540
11065
|
`;
|
|
@@ -9576,7 +11101,7 @@ ${AGENTS_SECTION}`
|
|
|
9576
11101
|
// src/run-init.ts
|
|
9577
11102
|
async function readIfPresent(path) {
|
|
9578
11103
|
try {
|
|
9579
|
-
return await
|
|
11104
|
+
return await readFile16(path, "utf8");
|
|
9580
11105
|
} catch {
|
|
9581
11106
|
return null;
|
|
9582
11107
|
}
|
|
@@ -9584,18 +11109,18 @@ async function readIfPresent(path) {
|
|
|
9584
11109
|
async function runInit(options, deps) {
|
|
9585
11110
|
const existingConfig = findConfigFile(options.cwd);
|
|
9586
11111
|
const plan = planInit({
|
|
9587
|
-
agents: await readIfPresent(
|
|
11112
|
+
agents: await readIfPresent(join19(options.cwd, "AGENTS.md")),
|
|
9588
11113
|
config: existingConfig === null ? null : "present",
|
|
9589
|
-
gitignore: await readIfPresent(
|
|
11114
|
+
gitignore: await readIfPresent(join19(options.cwd, ".gitignore")),
|
|
9590
11115
|
force: options.force
|
|
9591
11116
|
});
|
|
9592
11117
|
const touched = [];
|
|
9593
11118
|
for (const write2 of plan.writes) {
|
|
9594
|
-
await
|
|
11119
|
+
await writeFile11(join19(options.cwd, write2.path), write2.contents, "utf8");
|
|
9595
11120
|
touched.push(write2.path);
|
|
9596
11121
|
}
|
|
9597
11122
|
if (plan.gitignore !== null) {
|
|
9598
|
-
await
|
|
11123
|
+
await writeFile11(join19(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9599
11124
|
touched.push(".gitignore");
|
|
9600
11125
|
}
|
|
9601
11126
|
if (options.json) {
|
|
@@ -9615,11 +11140,11 @@ async function runInit(options, deps) {
|
|
|
9615
11140
|
|
|
9616
11141
|
// src/run-keep.ts
|
|
9617
11142
|
import { existsSync as existsSync5 } from "fs";
|
|
9618
|
-
import { copyFile as copyFile2, mkdir as mkdir10, readFile as
|
|
9619
|
-
import { dirname as
|
|
11143
|
+
import { copyFile as copyFile2, mkdir as mkdir10, readFile as readFile17, rm as rm6, writeFile as writeFile12 } from "fs/promises";
|
|
11144
|
+
import { dirname as dirname12, isAbsolute as isAbsolute3, join as join20, relative as relative5 } from "path";
|
|
9620
11145
|
|
|
9621
11146
|
// src/keep.ts
|
|
9622
|
-
import { basename as basename4, extname as extname4, normalize as
|
|
11147
|
+
import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, normalize as normalize3 } from "path";
|
|
9623
11148
|
function surfaceOf(url) {
|
|
9624
11149
|
if (!url.startsWith("/") || !url.includes("?")) return null;
|
|
9625
11150
|
for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
|
|
@@ -9648,8 +11173,8 @@ function planKeep(options) {
|
|
|
9648
11173
|
error: `Cannot tell which file produces ${JSON.stringify(options.title)}. Its URL (${winner.url}) was not generated by leglas new, so there is nothing safe to move.`
|
|
9649
11174
|
};
|
|
9650
11175
|
}
|
|
9651
|
-
const to =
|
|
9652
|
-
if (to.startsWith("..")) {
|
|
11176
|
+
const to = normalize3(options.to);
|
|
11177
|
+
if (to.startsWith("..") || isAbsolute2(to)) {
|
|
9653
11178
|
return { ok: false, error: "The destination has to be inside the project." };
|
|
9654
11179
|
}
|
|
9655
11180
|
if (to.split(/[/\\]/)[0] === ".leglas") {
|
|
@@ -9704,15 +11229,15 @@ async function writeLogEntry(options) {
|
|
|
9704
11229
|
annotations: await readAnnotations(options.cwd),
|
|
9705
11230
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
9706
11231
|
});
|
|
9707
|
-
const dir =
|
|
11232
|
+
const dir = join20(options.cwd, options.logDir);
|
|
9708
11233
|
await mkdir10(dir, { recursive: true });
|
|
9709
|
-
const file =
|
|
9710
|
-
await
|
|
11234
|
+
const file = join20(dir, `${entry.slug}.md`);
|
|
11235
|
+
await writeFile12(file, entry.markdown, "utf8");
|
|
9711
11236
|
if (entry.pictures.length > 0) {
|
|
9712
|
-
const pictureDir =
|
|
11237
|
+
const pictureDir = join20(dir, entry.slug);
|
|
9713
11238
|
await mkdir10(pictureDir, { recursive: true });
|
|
9714
11239
|
for (const picture of entry.pictures) {
|
|
9715
|
-
await copyFile2(
|
|
11240
|
+
await copyFile2(join20(options.cwd, picture.from), join20(pictureDir, picture.to)).catch(() => {
|
|
9716
11241
|
});
|
|
9717
11242
|
}
|
|
9718
11243
|
}
|
|
@@ -9733,19 +11258,20 @@ async function runKeep(options, deps) {
|
|
|
9733
11258
|
await readRenames(options.cwd)
|
|
9734
11259
|
);
|
|
9735
11260
|
if (!resolved.ok) return fail(resolved.error);
|
|
9736
|
-
const
|
|
11261
|
+
const destination = isAbsolute3(options.to) ? relative5(options.cwd, options.to) : options.to;
|
|
11262
|
+
const plan = planKeep({ title: resolved.title, previews, to: destination });
|
|
9737
11263
|
if (!plan.ok) return fail(plan.error);
|
|
9738
|
-
const from =
|
|
9739
|
-
const to =
|
|
11264
|
+
const from = join20(options.cwd, plan.move.from);
|
|
11265
|
+
const to = join20(options.cwd, plan.move.to);
|
|
9740
11266
|
if (!existsSync5(from)) {
|
|
9741
11267
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
9742
11268
|
}
|
|
9743
11269
|
if (existsSync5(to)) {
|
|
9744
11270
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
9745
11271
|
}
|
|
9746
|
-
const source = await
|
|
9747
|
-
await mkdir10(
|
|
9748
|
-
await
|
|
11272
|
+
const source = await readFile17(from, "utf8");
|
|
11273
|
+
await mkdir10(dirname12(to), { recursive: true });
|
|
11274
|
+
await writeFile12(to, renameExport(source, plan.exportName), "utf8");
|
|
9749
11275
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
9750
11276
|
let logged = null;
|
|
9751
11277
|
let logError = null;
|
|
@@ -9760,7 +11286,7 @@ async function runKeep(options, deps) {
|
|
|
9760
11286
|
} catch (error) {
|
|
9761
11287
|
logError = error instanceof Error ? error.message : String(error);
|
|
9762
11288
|
}
|
|
9763
|
-
await rm6(
|
|
11289
|
+
await rm6(join20(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
9764
11290
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
9765
11291
|
if (options.json) {
|
|
9766
11292
|
deps.log(
|
|
@@ -9799,11 +11325,11 @@ async function runKeep(options, deps) {
|
|
|
9799
11325
|
|
|
9800
11326
|
// src/run-new.ts
|
|
9801
11327
|
import { existsSync as existsSync6 } from "fs";
|
|
9802
|
-
import { mkdir as mkdir11, readFile as
|
|
9803
|
-
import { dirname as
|
|
11328
|
+
import { mkdir as mkdir11, readFile as readFile18, writeFile as writeFile13 } from "fs/promises";
|
|
11329
|
+
import { dirname as dirname13, join as join21 } from "path";
|
|
9804
11330
|
async function readIfPresent2(path) {
|
|
9805
11331
|
try {
|
|
9806
|
-
return await
|
|
11332
|
+
return await readFile18(path, "utf8");
|
|
9807
11333
|
} catch {
|
|
9808
11334
|
return null;
|
|
9809
11335
|
}
|
|
@@ -9811,7 +11337,7 @@ async function readIfPresent2(path) {
|
|
|
9811
11337
|
async function runNew(options, deps) {
|
|
9812
11338
|
let from;
|
|
9813
11339
|
if (options.from !== void 0) {
|
|
9814
|
-
const contents = await readIfPresent2(
|
|
11340
|
+
const contents = await readIfPresent2(join21(options.cwd, options.from));
|
|
9815
11341
|
if (contents === null) {
|
|
9816
11342
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
9817
11343
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -9822,8 +11348,8 @@ async function runNew(options, deps) {
|
|
|
9822
11348
|
}
|
|
9823
11349
|
const plan = planNew({
|
|
9824
11350
|
surface: options.surface,
|
|
9825
|
-
packageJson: await readIfPresent2(
|
|
9826
|
-
gitignore: await readIfPresent2(
|
|
11351
|
+
packageJson: await readIfPresent2(join21(options.cwd, "package.json")),
|
|
11352
|
+
gitignore: await readIfPresent2(join21(options.cwd, ".gitignore")),
|
|
9827
11353
|
from
|
|
9828
11354
|
});
|
|
9829
11355
|
const fail = (error) => {
|
|
@@ -9853,7 +11379,7 @@ async function runNew(options, deps) {
|
|
|
9853
11379
|
deps.log(plan.instructions);
|
|
9854
11380
|
return { exitCode: 0, written: [] };
|
|
9855
11381
|
}
|
|
9856
|
-
const existing = plan.writes.filter((write2) => existsSync6(
|
|
11382
|
+
const existing = plan.writes.filter((write2) => existsSync6(join21(options.cwd, write2.path)));
|
|
9857
11383
|
if (existing.length > 0) {
|
|
9858
11384
|
return fail(
|
|
9859
11385
|
`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
|
|
@@ -9861,13 +11387,13 @@ async function runNew(options, deps) {
|
|
|
9861
11387
|
}
|
|
9862
11388
|
const written = [];
|
|
9863
11389
|
for (const write2 of plan.writes) {
|
|
9864
|
-
const target =
|
|
9865
|
-
await mkdir11(
|
|
9866
|
-
await
|
|
11390
|
+
const target = join21(options.cwd, write2.path);
|
|
11391
|
+
await mkdir11(dirname13(target), { recursive: true });
|
|
11392
|
+
await writeFile13(target, write2.contents, "utf8");
|
|
9867
11393
|
written.push(write2.path);
|
|
9868
11394
|
}
|
|
9869
11395
|
if (plan.gitignore !== null) {
|
|
9870
|
-
await
|
|
11396
|
+
await writeFile13(join21(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9871
11397
|
written.push(".gitignore");
|
|
9872
11398
|
}
|
|
9873
11399
|
if (options.json) {
|
|
@@ -9895,8 +11421,8 @@ async function runNew(options, deps) {
|
|
|
9895
11421
|
}
|
|
9896
11422
|
|
|
9897
11423
|
// src/run-log.ts
|
|
9898
|
-
import { readFile as
|
|
9899
|
-
import { join as
|
|
11424
|
+
import { readFile as readFile19, readdir as readdir6 } from "fs/promises";
|
|
11425
|
+
import { join as join22 } from "path";
|
|
9900
11426
|
function headline(markdown) {
|
|
9901
11427
|
const first = markdown.split("\n", 1)[0] ?? "";
|
|
9902
11428
|
return first.replace(/^#\s*/, "").trim();
|
|
@@ -9906,7 +11432,7 @@ async function runLog(options, deps) {
|
|
|
9906
11432
|
const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
|
|
9907
11433
|
let names;
|
|
9908
11434
|
try {
|
|
9909
|
-
names = (await
|
|
11435
|
+
names = (await readdir6(join22(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
|
|
9910
11436
|
} catch {
|
|
9911
11437
|
names = [];
|
|
9912
11438
|
}
|
|
@@ -9919,7 +11445,7 @@ async function runLog(options, deps) {
|
|
|
9919
11445
|
else deps.error(error);
|
|
9920
11446
|
return { exitCode: 1 };
|
|
9921
11447
|
}
|
|
9922
|
-
const markdown = await
|
|
11448
|
+
const markdown = await readFile19(join22(options.cwd, dir, found), "utf8");
|
|
9923
11449
|
if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
|
|
9924
11450
|
else deps.log(markdown.trimEnd());
|
|
9925
11451
|
return { exitCode: 0 };
|
|
@@ -9927,7 +11453,7 @@ async function runLog(options, deps) {
|
|
|
9927
11453
|
const entries = await Promise.all(
|
|
9928
11454
|
names.map(async (name) => ({
|
|
9929
11455
|
entry: name.replace(/\.md$/, ""),
|
|
9930
|
-
title: headline(await
|
|
11456
|
+
title: headline(await readFile19(join22(options.cwd, dir, name), "utf8")),
|
|
9931
11457
|
file: `${dir}/${name}`
|
|
9932
11458
|
}))
|
|
9933
11459
|
);
|
|
@@ -9945,21 +11471,21 @@ async function runLog(options, deps) {
|
|
|
9945
11471
|
}
|
|
9946
11472
|
|
|
9947
11473
|
// src/run-previews.ts
|
|
9948
|
-
import { readFile as
|
|
9949
|
-
import { join as
|
|
11474
|
+
import { readFile as readFile20, writeFile as writeFile14 } from "fs/promises";
|
|
11475
|
+
import { join as join23 } from "path";
|
|
9950
11476
|
function envelope(deps, ok, body) {
|
|
9951
11477
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
9952
11478
|
}
|
|
9953
11479
|
async function ensureIgnored(cwd) {
|
|
9954
|
-
const path =
|
|
11480
|
+
const path = join23(cwd, ".gitignore");
|
|
9955
11481
|
let current = null;
|
|
9956
11482
|
try {
|
|
9957
|
-
current = await
|
|
11483
|
+
current = await readFile20(path, "utf8");
|
|
9958
11484
|
} catch {
|
|
9959
11485
|
current = null;
|
|
9960
11486
|
}
|
|
9961
11487
|
const next = ignoreEntry(current);
|
|
9962
|
-
if (next !== null) await
|
|
11488
|
+
if (next !== null) await writeFile14(path, next, "utf8");
|
|
9963
11489
|
}
|
|
9964
11490
|
async function runAdd(options, deps) {
|
|
9965
11491
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -10100,56 +11626,6 @@ async function runRequests(options, deps) {
|
|
|
10100
11626
|
return { exitCode: 0 };
|
|
10101
11627
|
}
|
|
10102
11628
|
|
|
10103
|
-
// src/json.ts
|
|
10104
|
-
function isJsonObject(value) {
|
|
10105
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10106
|
-
}
|
|
10107
|
-
function isString2(value) {
|
|
10108
|
-
return typeof value === "string";
|
|
10109
|
-
}
|
|
10110
|
-
function isNumber2(value) {
|
|
10111
|
-
return typeof value === "number";
|
|
10112
|
-
}
|
|
10113
|
-
async function bodyOf(response) {
|
|
10114
|
-
try {
|
|
10115
|
-
return JSON.parse(await response.text());
|
|
10116
|
-
} catch {
|
|
10117
|
-
return null;
|
|
10118
|
-
}
|
|
10119
|
-
}
|
|
10120
|
-
|
|
10121
|
-
// src/running.ts
|
|
10122
|
-
import { realpath as realpath2 } from "fs/promises";
|
|
10123
|
-
var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
|
|
10124
|
-
async function sameDirectory(left, right) {
|
|
10125
|
-
const [a, b] = await Promise.all([
|
|
10126
|
-
realpath2(left).catch(() => left),
|
|
10127
|
-
realpath2(right).catch(() => right)
|
|
10128
|
-
]);
|
|
10129
|
-
return a === b;
|
|
10130
|
-
}
|
|
10131
|
-
async function findLeglas(cwd, port, request) {
|
|
10132
|
-
const server = port === null ? await readServerInfo(cwd) : null;
|
|
10133
|
-
const chosen = port ?? server?.port ?? DEFAULT_PORT;
|
|
10134
|
-
try {
|
|
10135
|
-
const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
|
|
10136
|
-
signal: AbortSignal.timeout(2e3)
|
|
10137
|
-
});
|
|
10138
|
-
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
10139
|
-
const answered = await health.text().then((text2) => JSON.parse(text2)).catch(() => ({}));
|
|
10140
|
-
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
10141
|
-
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
10142
|
-
return {
|
|
10143
|
-
ok: false,
|
|
10144
|
-
error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
|
|
10145
|
-
};
|
|
10146
|
-
}
|
|
10147
|
-
return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
|
|
10148
|
-
} catch {
|
|
10149
|
-
return { ok: false, error: NOT_RUNNING };
|
|
10150
|
-
}
|
|
10151
|
-
}
|
|
10152
|
-
|
|
10153
11629
|
// src/run-share.ts
|
|
10154
11630
|
var TUNNEL_WAIT_MS = 6e4;
|
|
10155
11631
|
var TUNNEL_POLL_MS = 500;
|
|
@@ -10600,24 +12076,24 @@ async function runShow(options, deps) {
|
|
|
10600
12076
|
|
|
10601
12077
|
// src/run-watch.ts
|
|
10602
12078
|
import { spawn as spawn4 } from "child_process";
|
|
10603
|
-
import { mkdir as mkdir12, readFile as
|
|
10604
|
-
import { dirname as
|
|
10605
|
-
var
|
|
12079
|
+
import { mkdir as mkdir12, readFile as readFile21, writeFile as writeFile15 } from "fs/promises";
|
|
12080
|
+
import { dirname as dirname14, join as join24 } from "path";
|
|
12081
|
+
var POLL_MS3 = 2e3;
|
|
10606
12082
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
10607
12083
|
async function saveTemplate(cwd, run4) {
|
|
10608
|
-
const path =
|
|
12084
|
+
const path = join24(cwd, WATCH_PATH);
|
|
10609
12085
|
let config = {};
|
|
10610
12086
|
try {
|
|
10611
|
-
const parsed2 = JSON.parse(await
|
|
12087
|
+
const parsed2 = JSON.parse(await readFile21(path, "utf8"));
|
|
10612
12088
|
if (isJsonObject(parsed2)) config = parsed2;
|
|
10613
12089
|
} catch {
|
|
10614
12090
|
}
|
|
10615
12091
|
config.run = run4;
|
|
10616
|
-
await mkdir12(
|
|
10617
|
-
await
|
|
12092
|
+
await mkdir12(dirname14(path), { recursive: true });
|
|
12093
|
+
await writeFile15(path, `${JSON.stringify(config, null, 2)}
|
|
10618
12094
|
`, "utf8");
|
|
10619
12095
|
}
|
|
10620
|
-
function spawnAgent(command, args, cwd) {
|
|
12096
|
+
function spawnAgent(command, args, cwd, json) {
|
|
10621
12097
|
return new Promise((resolve5) => {
|
|
10622
12098
|
let settled = false;
|
|
10623
12099
|
const settle = (outcome) => {
|
|
@@ -10625,7 +12101,10 @@ function spawnAgent(command, args, cwd) {
|
|
|
10625
12101
|
settled = true;
|
|
10626
12102
|
resolve5(outcome);
|
|
10627
12103
|
};
|
|
10628
|
-
const child = spawn4(command, args, {
|
|
12104
|
+
const child = spawn4(command, args, {
|
|
12105
|
+
cwd,
|
|
12106
|
+
stdio: json ? ["inherit", 2, "inherit"] : "inherit"
|
|
12107
|
+
});
|
|
10629
12108
|
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
10630
12109
|
child.on(
|
|
10631
12110
|
"close",
|
|
@@ -10636,6 +12115,13 @@ function spawnAgent(command, args, cwd) {
|
|
|
10636
12115
|
});
|
|
10637
12116
|
}
|
|
10638
12117
|
async function runWatch(options, deps) {
|
|
12118
|
+
const json = options.json === true;
|
|
12119
|
+
const emit = (event) => deps.log(JSON.stringify(event));
|
|
12120
|
+
const refuse2 = (error) => {
|
|
12121
|
+
if (json) deps.log(JSON.stringify({ ok: false, error }));
|
|
12122
|
+
else deps.error(error);
|
|
12123
|
+
return { exitCode: 1 };
|
|
12124
|
+
};
|
|
10639
12125
|
const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
|
|
10640
12126
|
const raw = options.run ?? saved.run;
|
|
10641
12127
|
let template;
|
|
@@ -10643,10 +12129,7 @@ async function runWatch(options, deps) {
|
|
|
10643
12129
|
let synthesizedAgent = null;
|
|
10644
12130
|
if (raw !== null) {
|
|
10645
12131
|
const parsed2 = parseTemplate(raw);
|
|
10646
|
-
if (!parsed2.ok)
|
|
10647
|
-
deps.error(parsed2.error);
|
|
10648
|
-
return { exitCode: 1 };
|
|
10649
|
-
}
|
|
12132
|
+
if (!parsed2.ok) return refuse2(parsed2.error);
|
|
10650
12133
|
template = parsed2.template;
|
|
10651
12134
|
shownCommand2 = raw;
|
|
10652
12135
|
} else if (saved.agent !== null && saved.agent !== "custom") {
|
|
@@ -10658,10 +12141,9 @@ async function runWatch(options, deps) {
|
|
|
10658
12141
|
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
10659
12142
|
synthesizedAgent = adapter.name;
|
|
10660
12143
|
} else {
|
|
10661
|
-
|
|
12144
|
+
return refuse2(
|
|
10662
12145
|
'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
|
|
10663
12146
|
);
|
|
10664
|
-
return { exitCode: 1 };
|
|
10665
12147
|
}
|
|
10666
12148
|
if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
|
|
10667
12149
|
});
|
|
@@ -10677,26 +12159,36 @@ async function runWatch(options, deps) {
|
|
|
10677
12159
|
} catch {
|
|
10678
12160
|
}
|
|
10679
12161
|
};
|
|
10680
|
-
if (
|
|
10681
|
-
|
|
12162
|
+
if (json) {
|
|
12163
|
+
emit({ event: "watching", command: shownCommand2, agent: synthesizedAgent });
|
|
12164
|
+
} else {
|
|
12165
|
+
if (synthesizedAgent !== null) {
|
|
12166
|
+
deps.log(`Using ${synthesizedAgent}, chosen in the interface.`);
|
|
12167
|
+
}
|
|
12168
|
+
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
12169
|
+
deps.log("Stop with Ctrl-C.");
|
|
10682
12170
|
}
|
|
10683
|
-
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
10684
|
-
deps.log("Stop with Ctrl-C.");
|
|
10685
12171
|
const failed = /* @__PURE__ */ new Set();
|
|
10686
12172
|
let stopped = false;
|
|
10687
12173
|
let busy = false;
|
|
10688
12174
|
let announced = false;
|
|
10689
12175
|
let inflight = null;
|
|
10690
12176
|
const handle = async (request) => {
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
12177
|
+
const { id, title, intent, target } = request;
|
|
12178
|
+
if (json) {
|
|
12179
|
+
emit({ event: "started", id, title, intent, target });
|
|
12180
|
+
} else {
|
|
12181
|
+
deps.log("");
|
|
12182
|
+
deps.log(` ${title}: ${intent}`);
|
|
12183
|
+
if (target !== null) deps.log(` ${target}`);
|
|
12184
|
+
}
|
|
10694
12185
|
await markPickedUp(options.cwd, request.id);
|
|
10695
12186
|
const { command, args } = commandFor(template, request.prompt);
|
|
10696
|
-
const outcome = await spawnAgent(command, args, options.cwd);
|
|
12187
|
+
const outcome = await spawnAgent(command, args, options.cwd, json);
|
|
10697
12188
|
if (outcome.ok && outcome.code === 0) {
|
|
10698
12189
|
await removeRequest(options.cwd, request.id);
|
|
10699
|
-
|
|
12190
|
+
if (json) emit({ event: "done", id, title });
|
|
12191
|
+
else deps.log(` done ${title}`);
|
|
10700
12192
|
return;
|
|
10701
12193
|
}
|
|
10702
12194
|
failed.add(request.id);
|
|
@@ -10706,8 +12198,12 @@ async function runWatch(options, deps) {
|
|
|
10706
12198
|
exitCode: outcome.ok ? outcome.code : null
|
|
10707
12199
|
});
|
|
10708
12200
|
await markFailed(options.cwd, request.id, failure);
|
|
10709
|
-
|
|
10710
|
-
|
|
12201
|
+
if (json) {
|
|
12202
|
+
emit({ event: "failed", id, title, code: failure.code, reason: failure.message });
|
|
12203
|
+
} else {
|
|
12204
|
+
deps.error(` failed ${title}: ${failure.message}`);
|
|
12205
|
+
deps.error(" Left in the queue and not retried.");
|
|
12206
|
+
}
|
|
10711
12207
|
};
|
|
10712
12208
|
const tick = async () => {
|
|
10713
12209
|
if (stopped) return;
|
|
@@ -10726,14 +12222,16 @@ async function runWatch(options, deps) {
|
|
|
10726
12222
|
await inflight;
|
|
10727
12223
|
}
|
|
10728
12224
|
} catch (error) {
|
|
10729
|
-
|
|
12225
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
12226
|
+
if (json) emit({ event: "error", message: message2 });
|
|
12227
|
+
else deps.error(` ! ${message2}`);
|
|
10730
12228
|
} finally {
|
|
10731
12229
|
inflight = null;
|
|
10732
12230
|
busy = false;
|
|
10733
12231
|
}
|
|
10734
12232
|
};
|
|
10735
12233
|
return new Promise((resolve5) => {
|
|
10736
|
-
const timer = setInterval(() => void tick(),
|
|
12234
|
+
const timer = setInterval(() => void tick(), POLL_MS3);
|
|
10737
12235
|
const stop = () => {
|
|
10738
12236
|
if (stopped) return;
|
|
10739
12237
|
stopped = true;
|
|
@@ -10741,7 +12239,10 @@ async function runWatch(options, deps) {
|
|
|
10741
12239
|
process.off("SIGINT", stop);
|
|
10742
12240
|
process.off("SIGTERM", stop);
|
|
10743
12241
|
void Promise.resolve(inflight).catch(() => {
|
|
10744
|
-
}).then(() => heartbeat(false)).then(() =>
|
|
12242
|
+
}).then(() => heartbeat(false)).then(() => {
|
|
12243
|
+
if (json) emit({ event: "stopped" });
|
|
12244
|
+
resolve5({ exitCode: 0 });
|
|
12245
|
+
});
|
|
10745
12246
|
};
|
|
10746
12247
|
process.on("SIGINT", stop);
|
|
10747
12248
|
process.on("SIGTERM", stop);
|
|
@@ -10831,13 +12332,13 @@ function createHandoff() {
|
|
|
10831
12332
|
import { existsSync as existsSync7 } from "fs";
|
|
10832
12333
|
import { realpath as realpath4 } from "fs/promises";
|
|
10833
12334
|
import { createRequire } from "module";
|
|
10834
|
-
import { basename as basename6, dirname as
|
|
12335
|
+
import { basename as basename6, dirname as dirname15, join as join25, relative as relative7, resolve as resolve4 } from "path";
|
|
10835
12336
|
import { fileURLToPath } from "url";
|
|
10836
12337
|
|
|
10837
12338
|
// src/dev-server-owner.ts
|
|
10838
12339
|
import { execFile as execFile2 } from "child_process";
|
|
10839
12340
|
import { realpath as realpath3 } from "fs/promises";
|
|
10840
|
-
import { basename as basename5, isAbsolute as
|
|
12341
|
+
import { basename as basename5, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve3 } from "path";
|
|
10841
12342
|
import { promisify as promisify2 } from "util";
|
|
10842
12343
|
var run2 = promisify2(execFile2);
|
|
10843
12344
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -10906,8 +12407,8 @@ async function inspectLocalDevServer(origin) {
|
|
|
10906
12407
|
}
|
|
10907
12408
|
}
|
|
10908
12409
|
function isInside(parent, child) {
|
|
10909
|
-
const path =
|
|
10910
|
-
return path === "" || !path.startsWith("..") && !
|
|
12410
|
+
const path = relative6(resolve3(parent), resolve3(child));
|
|
12411
|
+
return path === "" || !path.startsWith("..") && !isAbsolute4(path);
|
|
10911
12412
|
}
|
|
10912
12413
|
function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
10913
12414
|
const port = localDevServerPort(origin);
|
|
@@ -10920,11 +12421,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
10920
12421
|
|
|
10921
12422
|
// src/run.ts
|
|
10922
12423
|
function findShellDir() {
|
|
10923
|
-
const bundled =
|
|
10924
|
-
if (existsSync7(
|
|
12424
|
+
const bundled = join25(dirname15(fileURLToPath(import.meta.url)), "shell");
|
|
12425
|
+
if (existsSync7(join25(bundled, "index.html"))) return bundled;
|
|
10925
12426
|
try {
|
|
10926
12427
|
const require2 = createRequire(import.meta.url);
|
|
10927
|
-
return
|
|
12428
|
+
return dirname15(require2.resolve("@leglas/shell/dist/index.html"));
|
|
10928
12429
|
} catch {
|
|
10929
12430
|
return null;
|
|
10930
12431
|
}
|
|
@@ -10935,7 +12436,7 @@ function shellWord(value) {
|
|
|
10935
12436
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
10936
12437
|
}
|
|
10937
12438
|
function embeddedLeglasCommand() {
|
|
10938
|
-
const entry =
|
|
12439
|
+
const entry = join25(dirname15(fileURLToPath(import.meta.url)), "bin.js");
|
|
10939
12440
|
if (!existsSync7(entry)) return "npx -y leglas";
|
|
10940
12441
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
10941
12442
|
}
|
|
@@ -10974,7 +12475,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10974
12475
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
10975
12476
|
for (const preview of merged?.previews ?? []) {
|
|
10976
12477
|
if (preview.file !== void 0) {
|
|
10977
|
-
const absolute =
|
|
12478
|
+
const absolute = join25(options.cwd, preview.file);
|
|
10978
12479
|
if (!existsSync7(absolute)) {
|
|
10979
12480
|
previewErrors.push(
|
|
10980
12481
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -10985,7 +12486,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10985
12486
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
10986
12487
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
10987
12488
|
}
|
|
10988
|
-
fileMounts.set(slug,
|
|
12489
|
+
fileMounts.set(slug, dirname15(absolute));
|
|
10989
12490
|
previews.push({
|
|
10990
12491
|
...preview,
|
|
10991
12492
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
@@ -10997,8 +12498,8 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10997
12498
|
const config = merged === null ? null : { ...merged, previews };
|
|
10998
12499
|
const configWarnings = [];
|
|
10999
12500
|
const projectRoot = await realpath4(
|
|
11000
|
-
loaded.path === null ? options.cwd :
|
|
11001
|
-
).catch(() => resolve4(loaded.path === null ? options.cwd :
|
|
12501
|
+
loaded.path === null ? options.cwd : dirname15(loaded.path)
|
|
12502
|
+
).catch(() => resolve4(loaded.path === null ? options.cwd : dirname15(loaded.path)));
|
|
11002
12503
|
const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
11003
12504
|
const serverOptions = {
|
|
11004
12505
|
config,
|
|
@@ -11036,7 +12537,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11036
12537
|
})
|
|
11037
12538
|
);
|
|
11038
12539
|
} else {
|
|
11039
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
12540
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative7(options.cwd, loaded.path) || loaded.path;
|
|
11040
12541
|
deps.log(`Leglas ${url}`);
|
|
11041
12542
|
deps.log(
|
|
11042
12543
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|
|
@@ -11113,18 +12614,17 @@ async function startViewer(options, deps) {
|
|
|
11113
12614
|
updates
|
|
11114
12615
|
});
|
|
11115
12616
|
const { handOff, handedOff } = createHandoff();
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
);
|
|
12617
|
+
const handoff = deps.handoff ?? {
|
|
12618
|
+
spawn: spawn5,
|
|
12619
|
+
exit: (code) => process.exit(code),
|
|
12620
|
+
target: process
|
|
12621
|
+
};
|
|
12622
|
+
updates.onRestart((command) => handOff(command, result.stop, handoff));
|
|
11123
12623
|
(deps.installShutdown ?? installShutdown)(async () => {
|
|
11124
12624
|
if (handedOff()) return;
|
|
11125
12625
|
await result.stop();
|
|
11126
|
-
|
|
11127
|
-
});
|
|
12626
|
+
handoff.exit(0);
|
|
12627
|
+
}, handoff.target);
|
|
11128
12628
|
}
|
|
11129
12629
|
|
|
11130
12630
|
// src/bin.ts
|
|
@@ -11134,7 +12634,7 @@ Usage
|
|
|
11134
12634
|
leglas init Prepare a project and teach its agents
|
|
11135
12635
|
leglas [options] Start the server and open the interface
|
|
11136
12636
|
leglas new <surface> Scaffold a branch point for a surface
|
|
11137
|
-
leglas explore <surface> Brief an agent's exploration of a surface
|
|
12637
|
+
leglas explore <surface> Brief an agent's exploration of a surface, or build it
|
|
11138
12638
|
leglas classify Decide where a direction should live
|
|
11139
12639
|
leglas add --title T --url U Register a preview on this machine
|
|
11140
12640
|
leglas list Show every preview, shared and local
|
|
@@ -11159,13 +12659,19 @@ Options for new
|
|
|
11159
12659
|
--from <path> Use an existing component as the baseline
|
|
11160
12660
|
|
|
11161
12661
|
Options for explore
|
|
11162
|
-
--count <n> How many directions (default 3)
|
|
12662
|
+
--count <n> How many directions (default 3; up to 6 with --build)
|
|
11163
12663
|
--based-on <title> Variants of an existing direction instead of new ones
|
|
12664
|
+
--build Build the set with your agent (Claude or Codex) in the
|
|
12665
|
+
running Leglas, instead of printing a brief for an agent
|
|
12666
|
+
--brief <text> What the directions are for (needs --build; optional
|
|
12667
|
+
with --based-on)
|
|
12668
|
+
--port <port> Running Leglas port (needs --build)
|
|
11164
12669
|
|
|
11165
12670
|
Options for watch
|
|
11166
12671
|
--run <command> Your agent, with {prompt} where the request goes, for
|
|
11167
12672
|
example "claude -p {prompt}". Remembered after first use
|
|
11168
12673
|
--port <port> Port Leglas itself is on (default: 4100)
|
|
12674
|
+
--json One JSON line per event; the agent's output goes to stderr
|
|
11169
12675
|
|
|
11170
12676
|
Options for classify
|
|
11171
12677
|
--change <path> A file the direction creates or wires up (repeatable)
|
|
@@ -11261,6 +12767,26 @@ if (parsed.kind === "keep") {
|
|
|
11261
12767
|
);
|
|
11262
12768
|
process.exit(outcome.exitCode);
|
|
11263
12769
|
}
|
|
12770
|
+
if (parsed.kind === "explore" && parsed.build) {
|
|
12771
|
+
const outcome = await runExploreBuild(
|
|
12772
|
+
{
|
|
12773
|
+
surface: parsed.surface,
|
|
12774
|
+
brief: parsed.brief ?? "",
|
|
12775
|
+
count: parsed.count,
|
|
12776
|
+
basedOn: parsed.basedOn,
|
|
12777
|
+
json: parsed.json,
|
|
12778
|
+
cwd: process.cwd(),
|
|
12779
|
+
port: parsed.port
|
|
12780
|
+
},
|
|
12781
|
+
{
|
|
12782
|
+
log: (line) => process.stdout.write(`${line}
|
|
12783
|
+
`),
|
|
12784
|
+
error: (line) => process.stderr.write(`${line}
|
|
12785
|
+
`)
|
|
12786
|
+
}
|
|
12787
|
+
);
|
|
12788
|
+
process.exit(outcome.exitCode);
|
|
12789
|
+
}
|
|
11264
12790
|
if (parsed.kind === "explore") {
|
|
11265
12791
|
const outcome = runExplore(
|
|
11266
12792
|
{ surface: parsed.surface, count: parsed.count, basedOn: parsed.basedOn, json: parsed.json },
|
|
@@ -11278,7 +12804,7 @@ if (parsed.kind === "requests") {
|
|
|
11278
12804
|
}
|
|
11279
12805
|
if (parsed.kind === "watch") {
|
|
11280
12806
|
const outcome = await runWatch(
|
|
11281
|
-
{ run: parsed.run, port: parsed.port, cwd: process.cwd() },
|
|
12807
|
+
{ run: parsed.run, port: parsed.port, cwd: process.cwd(), json: parsed.json },
|
|
11282
12808
|
previewDeps
|
|
11283
12809
|
);
|
|
11284
12810
|
process.exit(outcome.exitCode);
|