leglas 1.2.1 → 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/dist/args.d.ts +5 -0
- package/dist/bin-start.d.ts +5 -0
- package/dist/bin.js +1834 -372
- package/dist/index.js +1482 -162
- 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 +3 -3
- 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;
|
|
@@ -2497,6 +2540,16 @@ function validBox(value) {
|
|
|
2497
2540
|
return false;
|
|
2498
2541
|
return [value.x, value.y, value.width, value.height].every((entry) => isNumber(entry) && Number.isFinite(entry));
|
|
2499
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
|
+
}
|
|
2500
2553
|
function locatorExpression(focus) {
|
|
2501
2554
|
return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
|
|
2502
2555
|
}
|
|
@@ -2565,7 +2618,7 @@ async function render(page, input) {
|
|
|
2565
2618
|
}),
|
|
2566
2619
|
page.on("Log.entryAdded", (params) => {
|
|
2567
2620
|
if (params?.entry?.level === "error")
|
|
2568
|
-
remember(params.entry.text);
|
|
2621
|
+
remember(resourced(params.entry.text, params.entry.url));
|
|
2569
2622
|
})
|
|
2570
2623
|
];
|
|
2571
2624
|
try {
|
|
@@ -2988,8 +3041,8 @@ async function rehomeCaptures(cwd, from, to, attachments) {
|
|
|
2988
3041
|
file: attachment.file.startsWith(prefix) ? `${CAPTURES_DIR}/${to}/${attachment.file.slice(prefix.length)}` : attachment.file
|
|
2989
3042
|
}));
|
|
2990
3043
|
}
|
|
2991
|
-
function rehomeText(
|
|
2992
|
-
return
|
|
3044
|
+
function rehomeText(text3, from, to) {
|
|
3045
|
+
return text3.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
|
|
2993
3046
|
}
|
|
2994
3047
|
async function ownCapturesRoot(cwd) {
|
|
2995
3048
|
const root = join5(cwd, CAPTURES_DIR);
|
|
@@ -3857,13 +3910,13 @@ function imageMediaType(path) {
|
|
|
3857
3910
|
return "image/gif";
|
|
3858
3911
|
return null;
|
|
3859
3912
|
}
|
|
3860
|
-
async function claudeContent(prompt,
|
|
3861
|
-
if (
|
|
3913
|
+
async function claudeContent(prompt, images2) {
|
|
3914
|
+
if (images2.length === 0)
|
|
3862
3915
|
return prompt;
|
|
3863
3916
|
const blocks = [
|
|
3864
3917
|
{ type: "text", text: prompt }
|
|
3865
3918
|
];
|
|
3866
|
-
for (const path of
|
|
3919
|
+
for (const path of images2) {
|
|
3867
3920
|
const mediaType = imageMediaType(path);
|
|
3868
3921
|
if (mediaType === null)
|
|
3869
3922
|
continue;
|
|
@@ -4766,7 +4819,7 @@ function isStateUpdater(value) {
|
|
|
4766
4819
|
return typeof value === "function";
|
|
4767
4820
|
}
|
|
4768
4821
|
var SESSION_TURNS_CAP = 8;
|
|
4769
|
-
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
4822
|
+
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images2 = []) {
|
|
4770
4823
|
if (choice.agent === null)
|
|
4771
4824
|
return null;
|
|
4772
4825
|
if (choice.agent === "custom") {
|
|
@@ -4793,24 +4846,24 @@ function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
|
4793
4846
|
agent: choice.agent,
|
|
4794
4847
|
name: adapter.name,
|
|
4795
4848
|
command: adapter.binary,
|
|
4796
|
-
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort,
|
|
4849
|
+
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort, images2), ...allow],
|
|
4797
4850
|
prompt,
|
|
4798
4851
|
effort: choice.effort,
|
|
4799
4852
|
sessionId,
|
|
4800
4853
|
resumed: true,
|
|
4801
|
-
images
|
|
4854
|
+
images: images2
|
|
4802
4855
|
};
|
|
4803
4856
|
}
|
|
4804
4857
|
return {
|
|
4805
4858
|
agent: choice.agent,
|
|
4806
4859
|
name: adapter.name,
|
|
4807
4860
|
command: adapter.binary,
|
|
4808
|
-
args: [...adapter.args(prompt, choice.effort,
|
|
4861
|
+
args: [...adapter.args(prompt, choice.effort, images2), ...allow],
|
|
4809
4862
|
prompt,
|
|
4810
4863
|
effort: choice.effort,
|
|
4811
4864
|
sessionId: null,
|
|
4812
4865
|
resumed: false,
|
|
4813
|
-
images
|
|
4866
|
+
images: images2
|
|
4814
4867
|
};
|
|
4815
4868
|
}
|
|
4816
4869
|
function lineReader(stream, onLine) {
|
|
@@ -5096,8 +5149,8 @@ function startRunner(options) {
|
|
|
5096
5149
|
`${options.leglasCommand} show`,
|
|
5097
5150
|
...request.mode === "variant" ? [registrationCommand(options.leglasCommand)] : []
|
|
5098
5151
|
];
|
|
5099
|
-
const
|
|
5100
|
-
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);
|
|
5101
5154
|
if (resolved === null)
|
|
5102
5155
|
return;
|
|
5103
5156
|
const lines2 = [];
|
|
@@ -5143,7 +5196,7 @@ function startRunner(options) {
|
|
|
5143
5196
|
// nothing says "cancelled". Stopped still means stopped.
|
|
5144
5197
|
!stopped) {
|
|
5145
5198
|
sessions.delete(resolved.agent);
|
|
5146
|
-
const cold = resolveCommand(choice, request.prompt, null, allowedCommands,
|
|
5199
|
+
const cold = resolveCommand(choice, request.prompt, null, allowedCommands, images2);
|
|
5147
5200
|
if (cold !== null) {
|
|
5148
5201
|
resolved = cold;
|
|
5149
5202
|
observed.sessionId = null;
|
|
@@ -6837,10 +6890,10 @@ function resolveTitle(input, titles, renames) {
|
|
|
6837
6890
|
// ../server/dist/server.js
|
|
6838
6891
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
6839
6892
|
import { createHash as createHash2 } from "crypto";
|
|
6840
|
-
import { mkdir as mkdir8, readdir as
|
|
6893
|
+
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile9 } from "fs/promises";
|
|
6841
6894
|
import http4 from "http";
|
|
6842
6895
|
import net3 from "net";
|
|
6843
|
-
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";
|
|
6844
6897
|
|
|
6845
6898
|
// ../server/dist/frame-policy.js
|
|
6846
6899
|
var FRAMING_TIMEOUT_MS = 4e3;
|
|
@@ -6951,176 +7004,1311 @@ async function checkFraming(target, embedder, fetcher = fetch) {
|
|
|
6951
7004
|
}
|
|
6952
7005
|
}
|
|
6953
7006
|
|
|
6954
|
-
// ../server/dist/
|
|
6955
|
-
import {
|
|
6956
|
-
import {
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
await rename2(temporary, path).catch(async (cause) => {
|
|
6971
|
-
await rm4(temporary, { force: true }).catch(() => {
|
|
6972
|
-
});
|
|
6973
|
-
throw cause;
|
|
6974
|
-
});
|
|
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];
|
|
6975
7023
|
}
|
|
6976
|
-
|
|
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
|
+
];
|
|
7036
|
+
}
|
|
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
|
+
}
|
|
7048
|
+
}
|
|
7049
|
+
return null;
|
|
7050
|
+
}
|
|
7051
|
+
|
|
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;
|
|
6977
7085
|
try {
|
|
6978
|
-
|
|
6979
|
-
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))
|
|
6980
|
-
return null;
|
|
6981
|
-
return { port: value.port, url: value.url, pid: value.pid };
|
|
7086
|
+
config = await readFile11(join11(home, "config.toml"), "utf8");
|
|
6982
7087
|
} catch {
|
|
6983
|
-
return
|
|
7088
|
+
return [];
|
|
6984
7089
|
}
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
return;
|
|
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);
|
|
6991
7095
|
}
|
|
6992
|
-
|
|
7096
|
+
return [...names];
|
|
6993
7097
|
}
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
var LEGLAS_PREFIX = "/leglas";
|
|
6997
|
-
var DEFAULT_PORT = 4100;
|
|
6998
|
-
var PORT_ATTEMPTS = 20;
|
|
6999
|
-
var REFERENCE_MAX_BYTES = 1e7;
|
|
7000
|
-
var ATTACHED_WINDOW_MS = 6e3;
|
|
7001
|
-
var CONTENT_TYPES = {
|
|
7002
|
-
".css": "text/css; charset=utf-8",
|
|
7003
|
-
".gif": "image/gif",
|
|
7004
|
-
".html": "text/html; charset=utf-8",
|
|
7005
|
-
".ico": "image/x-icon",
|
|
7006
|
-
".jpeg": "image/jpeg",
|
|
7007
|
-
".jpg": "image/jpeg",
|
|
7008
|
-
".js": "text/javascript; charset=utf-8",
|
|
7009
|
-
".json": "application/json; charset=utf-8",
|
|
7010
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
7011
|
-
".png": "image/png",
|
|
7012
|
-
".svg": "image/svg+xml",
|
|
7013
|
-
".txt": "text/plain; charset=utf-8",
|
|
7014
|
-
".webp": "image/webp",
|
|
7015
|
-
".woff": "font/woff",
|
|
7016
|
-
".woff2": "font/woff2"
|
|
7017
|
-
};
|
|
7018
|
-
function isContentExtension(extension) {
|
|
7019
|
-
return Object.hasOwn(CONTENT_TYPES, extension);
|
|
7098
|
+
function defaultCodexHome(env = process.env) {
|
|
7099
|
+
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME : join11(homedir2(), ".codex");
|
|
7020
7100
|
}
|
|
7021
|
-
var
|
|
7022
|
-
function
|
|
7023
|
-
|
|
7024
|
-
res.writeHead(status, {
|
|
7025
|
-
"content-type": "application/json; charset=utf-8",
|
|
7026
|
-
"cache-control": "no-store"
|
|
7027
|
-
});
|
|
7028
|
-
res.end(payload);
|
|
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];
|
|
7029
7104
|
}
|
|
7030
|
-
function
|
|
7031
|
-
|
|
7105
|
+
function codexBuildArgs(prompt, servers) {
|
|
7106
|
+
return [...RESTRICTED2, ...serversOff(servers), "-s", "workspace-write", prompt];
|
|
7107
|
+
}
|
|
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;
|
|
7122
|
+
}
|
|
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) {
|
|
7135
|
+
try {
|
|
7136
|
+
return (await stat4(path)).isFile();
|
|
7137
|
+
} catch {
|
|
7032
7138
|
return false;
|
|
7033
|
-
|
|
7034
|
-
return values.some((header) => header.split(",").some((candidate) => {
|
|
7035
|
-
const tag = candidate.trim();
|
|
7036
|
-
return tag === "*" || tag === etag || tag === `W/${etag}`;
|
|
7037
|
-
}));
|
|
7139
|
+
}
|
|
7038
7140
|
}
|
|
7039
|
-
function
|
|
7040
|
-
const
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
res.end();
|
|
7045
|
-
return;
|
|
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;
|
|
7046
7146
|
}
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
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));
|
|
7179
|
+
}
|
|
7180
|
+
return null;
|
|
7181
|
+
}
|
|
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;
|
|
7051
7213
|
});
|
|
7052
|
-
|
|
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;
|
|
7053
7224
|
}
|
|
7054
|
-
|
|
7055
|
-
var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
|
|
7056
|
-
function captureSlug(title) {
|
|
7057
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "direction";
|
|
7225
|
+
`;
|
|
7058
7226
|
}
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
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;
|
|
7252
|
+
}
|
|
7253
|
+
return null;
|
|
7066
7254
|
}
|
|
7067
|
-
function
|
|
7068
|
-
return
|
|
7255
|
+
function cssImports(source, from) {
|
|
7256
|
+
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) => normalize(join13(dirname7(from), match[1] ?? "")));
|
|
7069
7257
|
}
|
|
7070
|
-
function
|
|
7071
|
-
|
|
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);
|
|
7072
7280
|
}
|
|
7073
|
-
function
|
|
7074
|
-
const
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7281
|
+
async function images(cwd) {
|
|
7282
|
+
const found = [];
|
|
7283
|
+
const walk = async (folder) => {
|
|
7284
|
+
let items;
|
|
7285
|
+
try {
|
|
7286
|
+
items = await readdir4(join13(cwd, "public", folder), { withFileTypes: true });
|
|
7287
|
+
} catch {
|
|
7288
|
+
return;
|
|
7289
|
+
}
|
|
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;
|
|
7083
7302
|
}
|
|
7084
|
-
function
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
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
|
+
]);
|
|
7088
7309
|
}
|
|
7089
|
-
function
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
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) {
|
|
7095
7341
|
try {
|
|
7096
|
-
|
|
7342
|
+
const pkg = parseJson(await text2(join13(cwd, "package.json")) ?? "");
|
|
7343
|
+
return isJsonRecord(pkg) && isJsonRecord(pkg.dependencies) ? Object.keys(pkg.dependencies) : [];
|
|
7097
7344
|
} catch {
|
|
7098
|
-
return
|
|
7345
|
+
return [];
|
|
7099
7346
|
}
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
const
|
|
7103
|
-
|
|
7104
|
-
|
|
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;
|
|
7105
7442
|
try {
|
|
7106
|
-
|
|
7107
|
-
return origin.protocol === "http:" && origin.host === host.host;
|
|
7443
|
+
parsed2 = parseJson(reply.slice(start, end + 1));
|
|
7108
7444
|
} catch {
|
|
7109
|
-
|
|
7445
|
+
throw new Error("The plan reply's JSON did not parse.");
|
|
7110
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
|
+
};
|
|
7111
7494
|
}
|
|
7112
|
-
function
|
|
7113
|
-
return
|
|
7495
|
+
function wait(milliseconds) {
|
|
7496
|
+
return new Promise((resolve5) => {
|
|
7497
|
+
setTimeout(resolve5, milliseconds).unref?.();
|
|
7498
|
+
});
|
|
7114
7499
|
}
|
|
7115
|
-
function
|
|
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;
|
|
7116
7505
|
try {
|
|
7117
|
-
|
|
7118
|
-
return isJsonRecord(parsed2) ? parsed2 : null;
|
|
7506
|
+
key = new URL(preview.url, "http://leglas.invalid").searchParams.get(`v-${surfaceSlug(surface)}`);
|
|
7119
7507
|
} catch {
|
|
7120
7508
|
return null;
|
|
7121
7509
|
}
|
|
7510
|
+
return key === null || key === "" ? null : { title: preview.title, key, idea: preview.note ?? "" };
|
|
7122
7511
|
}
|
|
7123
|
-
function
|
|
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) {
|
|
7124
8312
|
if (value === null || value === void 0)
|
|
7125
8313
|
return "";
|
|
7126
8314
|
return isString(value) ? value.trim() : null;
|
|
@@ -7150,8 +8338,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
7150
8338
|
});
|
|
7151
8339
|
}
|
|
7152
8340
|
function serveFrom(res, dir, relativePath) {
|
|
7153
|
-
const
|
|
7154
|
-
const candidate =
|
|
8341
|
+
const relative8 = normalize2(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
8342
|
+
const candidate = join16(dir, relative8);
|
|
7155
8343
|
if (!candidate.startsWith(dir))
|
|
7156
8344
|
return false;
|
|
7157
8345
|
if (!existsSync3(candidate) || !statSync(candidate).isFile())
|
|
@@ -7165,25 +8353,25 @@ function serveFrom(res, dir, relativePath) {
|
|
|
7165
8353
|
return true;
|
|
7166
8354
|
}
|
|
7167
8355
|
function serveShellFile(res, shellDir, urlPath) {
|
|
7168
|
-
const
|
|
7169
|
-
const isRoot =
|
|
7170
|
-
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);
|
|
7171
8359
|
}
|
|
7172
8360
|
var HEALTH_PROBE_MS = 3e3;
|
|
7173
8361
|
function fileStamp(path) {
|
|
7174
8362
|
try {
|
|
7175
|
-
const
|
|
7176
|
-
return `${
|
|
8363
|
+
const stat6 = statSync(path);
|
|
8364
|
+
return `${stat6.dev}:${stat6.ino}:${stat6.size}:${stat6.mtimeMs}:${stat6.ctimeMs}`;
|
|
7177
8365
|
} catch {
|
|
7178
8366
|
return null;
|
|
7179
8367
|
}
|
|
7180
8368
|
}
|
|
7181
8369
|
function watchLiveFiles(cwd, configPath, live) {
|
|
7182
|
-
const leglasDir =
|
|
8370
|
+
const leglasDir = join16(cwd, ".leglas");
|
|
7183
8371
|
const targets = [
|
|
7184
|
-
{ path:
|
|
7185
|
-
{ path:
|
|
7186
|
-
{ 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" }
|
|
7187
8375
|
];
|
|
7188
8376
|
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
7189
8377
|
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
@@ -7297,7 +8485,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7297
8485
|
}
|
|
7298
8486
|
if (configPath !== null) {
|
|
7299
8487
|
try {
|
|
7300
|
-
const directory =
|
|
8488
|
+
const directory = dirname10(configPath);
|
|
7301
8489
|
const name = basename3(configPath);
|
|
7302
8490
|
const target = { path: configPath, change: "config" };
|
|
7303
8491
|
known.set(configPath, fileStamp(configPath));
|
|
@@ -7335,7 +8523,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7335
8523
|
}
|
|
7336
8524
|
};
|
|
7337
8525
|
}
|
|
7338
|
-
function watchHealth(target, live) {
|
|
8526
|
+
function watchHealth(target, live, intervalMs) {
|
|
7339
8527
|
let previous = null;
|
|
7340
8528
|
let probing = false;
|
|
7341
8529
|
let closed = false;
|
|
@@ -7358,7 +8546,7 @@ function watchHealth(target, live) {
|
|
|
7358
8546
|
}).finally(() => {
|
|
7359
8547
|
probing = false;
|
|
7360
8548
|
});
|
|
7361
|
-
},
|
|
8549
|
+
}, intervalMs);
|
|
7362
8550
|
timer.unref?.();
|
|
7363
8551
|
return {
|
|
7364
8552
|
close: () => {
|
|
@@ -7382,15 +8570,15 @@ function configStalenessNotice(cwd, boot, current) {
|
|
|
7382
8570
|
if (boot === null && current === null)
|
|
7383
8571
|
return null;
|
|
7384
8572
|
if (boot === null && current !== null) {
|
|
7385
|
-
const label =
|
|
8573
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7386
8574
|
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
7387
8575
|
}
|
|
7388
8576
|
if (boot !== null && current === null) {
|
|
7389
|
-
const label =
|
|
8577
|
+
const label = relative4(cwd, boot.path) || boot.path;
|
|
7390
8578
|
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
7391
8579
|
}
|
|
7392
8580
|
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
7393
|
-
const label =
|
|
8581
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7394
8582
|
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
7395
8583
|
}
|
|
7396
8584
|
return null;
|
|
@@ -7436,7 +8624,7 @@ async function bind2(server, requested) {
|
|
|
7436
8624
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
7437
8625
|
}
|
|
7438
8626
|
async function startServer(options) {
|
|
7439
|
-
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;
|
|
7440
8628
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7441
8629
|
let shares = null;
|
|
7442
8630
|
let liveHealth = null;
|
|
@@ -7481,6 +8669,7 @@ async function startServer(options) {
|
|
|
7481
8669
|
let lastSeen = null;
|
|
7482
8670
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
7483
8671
|
let runner = null;
|
|
8672
|
+
let generations = null;
|
|
7484
8673
|
let agentsCache = null;
|
|
7485
8674
|
let agentsInflight = null;
|
|
7486
8675
|
const AGENTS_FRESH_MS = 3e4;
|
|
@@ -7531,11 +8720,36 @@ async function startServer(options) {
|
|
|
7531
8720
|
});
|
|
7532
8721
|
});
|
|
7533
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
|
+
};
|
|
7534
8748
|
const handleRequest = (req, res, context) => {
|
|
7535
8749
|
const url = req.url ?? "/";
|
|
7536
8750
|
const path = url.split("?")[0] ?? "/";
|
|
7537
8751
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
7538
|
-
if (!context.remote && req.method
|
|
8752
|
+
if (!context.remote && req.method !== "GET" && req.method !== "HEAD" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7539
8753
|
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7540
8754
|
}
|
|
7541
8755
|
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
@@ -7553,6 +8767,48 @@ async function startServer(options) {
|
|
|
7553
8767
|
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7554
8768
|
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7555
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
|
+
}
|
|
7556
8812
|
if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
|
|
7557
8813
|
const updates = options.updates;
|
|
7558
8814
|
if (updates === void 0) {
|
|
@@ -7816,8 +9072,8 @@ async function startServer(options) {
|
|
|
7816
9072
|
const id = newRequestId();
|
|
7817
9073
|
const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
|
|
7818
9074
|
try {
|
|
7819
|
-
await mkdir8(
|
|
7820
|
-
await
|
|
9075
|
+
await mkdir8(join16(cwd, REFERENCES_DIR), { recursive: true });
|
|
9076
|
+
await writeFile9(join16(cwd, file), body);
|
|
7821
9077
|
void pruneReferences(cwd).catch(() => {
|
|
7822
9078
|
});
|
|
7823
9079
|
return sendJson2(res, 200, {
|
|
@@ -7859,7 +9115,7 @@ async function startServer(options) {
|
|
|
7859
9115
|
}
|
|
7860
9116
|
const references = parsed2.references ?? [];
|
|
7861
9117
|
if (references.length > 0) {
|
|
7862
|
-
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("."))));
|
|
7863
9119
|
const gone = references.filter((id2) => !present.has(id2));
|
|
7864
9120
|
if (gone.length > 0) {
|
|
7865
9121
|
return sendJson2(res, 410, {
|
|
@@ -7989,7 +9245,7 @@ async function startServer(options) {
|
|
|
7989
9245
|
const timer = setTimeout(() => {
|
|
7990
9246
|
timedOut2();
|
|
7991
9247
|
controller.abort();
|
|
7992
|
-
},
|
|
9248
|
+
}, captureDeadlineMs);
|
|
7993
9249
|
timer.unref?.();
|
|
7994
9250
|
try {
|
|
7995
9251
|
const captureInput = {
|
|
@@ -8022,8 +9278,8 @@ async function startServer(options) {
|
|
|
8022
9278
|
const noteSuffix = isString(parsed2.note) ? `-${parsed2.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
|
|
8023
9279
|
const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
|
|
8024
9280
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
8025
|
-
await mkdir8(
|
|
8026
|
-
await
|
|
9281
|
+
await mkdir8(join16(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
9282
|
+
await writeFile9(join16(cwd, relativeFile), shot.png);
|
|
8027
9283
|
return sendJson2(res, 200, {
|
|
8028
9284
|
ok: true,
|
|
8029
9285
|
file: relativeFile,
|
|
@@ -8403,15 +9659,15 @@ async function startServer(options) {
|
|
|
8403
9659
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
8404
9660
|
const slash = rest.indexOf("/");
|
|
8405
9661
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
8406
|
-
let
|
|
9662
|
+
let relative8 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
8407
9663
|
try {
|
|
8408
|
-
|
|
9664
|
+
relative8 = decodeURIComponent(relative8);
|
|
8409
9665
|
} catch {
|
|
8410
|
-
|
|
9666
|
+
relative8 = "";
|
|
8411
9667
|
}
|
|
8412
9668
|
const dir = fileMounts.get(slug);
|
|
8413
9669
|
const serveMount = () => {
|
|
8414
|
-
if (dir !== void 0 &&
|
|
9670
|
+
if (dir !== void 0 && relative8 !== "" && serveFrom(res, dir, relative8))
|
|
8415
9671
|
return;
|
|
8416
9672
|
res.writeHead(404, {
|
|
8417
9673
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -8421,7 +9677,7 @@ async function startServer(options) {
|
|
|
8421
9677
|
};
|
|
8422
9678
|
if (!context.remote)
|
|
8423
9679
|
return serveMount();
|
|
8424
|
-
if (
|
|
9680
|
+
if (relative8.split("/").some((segment) => segment.startsWith("."))) {
|
|
8425
9681
|
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8426
9682
|
}
|
|
8427
9683
|
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
@@ -8502,7 +9758,7 @@ async function startServer(options) {
|
|
|
8502
9758
|
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
8503
9759
|
options.updates?.setPort(port);
|
|
8504
9760
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
8505
|
-
liveHealth = watchHealth(target, live);
|
|
9761
|
+
liveHealth = watchHealth(target, live, healthProbeMs);
|
|
8506
9762
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
8507
9763
|
});
|
|
8508
9764
|
await writeServerInfo(cwd, {
|
|
@@ -8522,6 +9778,42 @@ async function startServer(options) {
|
|
|
8522
9778
|
if (options.claudeAgentSession !== void 0)
|
|
8523
9779
|
runnerOptions.claudeAgentSession = options.claudeAgentSession;
|
|
8524
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);
|
|
8525
9817
|
options.updates?.onBusy(() => runner?.snapshot().running ?? false);
|
|
8526
9818
|
options.updates?.onChange(() => live.nudge("update"));
|
|
8527
9819
|
let closePromise = null;
|
|
@@ -8539,6 +9831,7 @@ async function startServer(options) {
|
|
|
8539
9831
|
shares?.close() ?? Promise.resolve(),
|
|
8540
9832
|
branches.stop(),
|
|
8541
9833
|
runner.stop(),
|
|
9834
|
+
generations?.close() ?? Promise.resolve(),
|
|
8542
9835
|
browserPool.close(),
|
|
8543
9836
|
live.close()
|
|
8544
9837
|
]);
|
|
@@ -8560,9 +9853,9 @@ async function startServer(options) {
|
|
|
8560
9853
|
// ../server/dist/update.js
|
|
8561
9854
|
import { spawn as spawnChild2 } from "child_process";
|
|
8562
9855
|
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
8563
|
-
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as
|
|
8564
|
-
import { homedir as
|
|
8565
|
-
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";
|
|
8566
9859
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8567
9860
|
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
8568
9861
|
var REGISTRY = "https://registry.npmjs.org";
|
|
@@ -8762,7 +10055,7 @@ function restartCommand(install, argv, latest, port, options) {
|
|
|
8762
10055
|
return spawnCommand(["leglas", ...rest], options.platform);
|
|
8763
10056
|
}
|
|
8764
10057
|
if (install.kind === "project" && install.root !== void 0) {
|
|
8765
|
-
const shim = (windows ? win32.join :
|
|
10058
|
+
const shim = (windows ? win32.join : join17)(install.root, "node_modules", ".bin", windows ? "leglas.cmd" : "leglas");
|
|
8766
10059
|
return spawnCommand(options.exists(shim) ? [shim, ...rest] : ["yarn", "leglas", ...rest], options.platform);
|
|
8767
10060
|
}
|
|
8768
10061
|
throw new Error(CHECKOUT_NOTICE);
|
|
@@ -8793,7 +10086,7 @@ function readState(path) {
|
|
|
8793
10086
|
}
|
|
8794
10087
|
function defaultStatePath(home) {
|
|
8795
10088
|
try {
|
|
8796
|
-
return
|
|
10089
|
+
return join17(home(), ".leglas", "update.json");
|
|
8797
10090
|
} catch {
|
|
8798
10091
|
return null;
|
|
8799
10092
|
}
|
|
@@ -8811,16 +10104,16 @@ function mergeState(current, saved) {
|
|
|
8811
10104
|
async function writeState(path, state) {
|
|
8812
10105
|
let temporary = null;
|
|
8813
10106
|
try {
|
|
8814
|
-
const directory =
|
|
10107
|
+
const directory = dirname11(path);
|
|
8815
10108
|
await mkdir9(directory, { recursive: true });
|
|
8816
10109
|
if (!(await lstat3(directory)).isDirectory())
|
|
8817
10110
|
return;
|
|
8818
10111
|
const existing = await lstat3(path).catch(() => null);
|
|
8819
10112
|
if (existing !== null && !existing.isFile())
|
|
8820
10113
|
return;
|
|
8821
|
-
temporary = await mkdtemp(
|
|
8822
|
-
const file =
|
|
8823
|
-
await
|
|
10114
|
+
temporary = await mkdtemp(join17(directory, ".update-"));
|
|
10115
|
+
const file = join17(temporary, "update.json");
|
|
10116
|
+
await writeFile10(file, `${JSON.stringify(state, null, 2)}
|
|
8824
10117
|
`, "utf8");
|
|
8825
10118
|
await rename3(file, path);
|
|
8826
10119
|
} catch {
|
|
@@ -8881,7 +10174,7 @@ function installerReason(stdout, stderr) {
|
|
|
8881
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);
|
|
8882
10175
|
const errors = lines2(stderr);
|
|
8883
10176
|
const output = lines2(stdout);
|
|
8884
|
-
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));
|
|
8885
10178
|
return npm?.replace(/^npm error\s+/, "") ?? errors[0] ?? output[0] ?? null;
|
|
8886
10179
|
}
|
|
8887
10180
|
function installVersion(install, version2, deps) {
|
|
@@ -8981,7 +10274,7 @@ function createUpdateService(input) {
|
|
|
8981
10274
|
const fetcher = deps.fetch ?? fetch;
|
|
8982
10275
|
const now = deps.now ?? Date.now;
|
|
8983
10276
|
const timeoutMs = deps.timeoutMs ?? 4e3;
|
|
8984
|
-
const statePath = deps.statePath === void 0 ? defaultStatePath(deps.homedir ??
|
|
10277
|
+
const statePath = deps.statePath === void 0 ? defaultStatePath(deps.homedir ?? homedir3) : deps.statePath;
|
|
8985
10278
|
const exists = deps.exists ?? existsSync4;
|
|
8986
10279
|
const env = deps.env ?? process.env;
|
|
8987
10280
|
const install = detectInstall(input.entry, input.cwd, exists, deps.realpath ?? realPath, env);
|
|
@@ -9204,7 +10497,7 @@ async function runClassify(options, deps) {
|
|
|
9204
10497
|
const declared = await Promise.all(
|
|
9205
10498
|
options.changes.map(async (change) => ({
|
|
9206
10499
|
...change,
|
|
9207
|
-
exists: await
|
|
10500
|
+
exists: await stat5(join18(options.cwd, change.path)).then(
|
|
9208
10501
|
() => true,
|
|
9209
10502
|
() => false
|
|
9210
10503
|
)
|
|
@@ -9291,7 +10584,7 @@ function detectFramework(packageJson) {
|
|
|
9291
10584
|
return "react";
|
|
9292
10585
|
}
|
|
9293
10586
|
}
|
|
9294
|
-
function
|
|
10587
|
+
function surfaceSlug2(surface) {
|
|
9295
10588
|
return surface.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
9296
10589
|
}
|
|
9297
10590
|
function titleCase(slug) {
|
|
@@ -9388,14 +10681,14 @@ function placeholder(label, note) {
|
|
|
9388
10681
|
return `export function ${label}() {
|
|
9389
10682
|
return (
|
|
9390
10683
|
<div style={{ padding: "3rem", fontFamily: "system-ui" }}>
|
|
9391
|
-
<p
|
|
10684
|
+
<p>{${JSON.stringify(note)}}</p>
|
|
9392
10685
|
</div>
|
|
9393
10686
|
);
|
|
9394
10687
|
}
|
|
9395
10688
|
`;
|
|
9396
10689
|
}
|
|
9397
10690
|
function planNew(options) {
|
|
9398
|
-
const slug =
|
|
10691
|
+
const slug = surfaceSlug2(options.surface);
|
|
9399
10692
|
const framework = detectFramework(options.packageJson);
|
|
9400
10693
|
const name = titleCase(slug).replace(/\s/g, "");
|
|
9401
10694
|
const dir = `.leglas/variants/${slug}`;
|
|
@@ -9439,7 +10732,7 @@ Rewriting your component automatically is how a tool breaks a codebase it does n
|
|
|
9439
10732
|
|
|
9440
10733
|
// src/explore.ts
|
|
9441
10734
|
function planExplore(surface, count, basedOn = null) {
|
|
9442
|
-
const slug =
|
|
10735
|
+
const slug = surfaceSlug2(surface);
|
|
9443
10736
|
const goal = basedOn === null ? `Build ${count} design directions for "${surface}".
|
|
9444
10737
|
|
|
9445
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.
|
|
@@ -9477,9 +10770,176 @@ function runExplore(options, deps) {
|
|
|
9477
10770
|
return { exitCode: 0 };
|
|
9478
10771
|
}
|
|
9479
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
|
+
|
|
9480
10940
|
// src/run-init.ts
|
|
9481
|
-
import { readFile as
|
|
9482
|
-
import { join as
|
|
10941
|
+
import { readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
|
|
10942
|
+
import { join as join19 } from "path";
|
|
9483
10943
|
|
|
9484
10944
|
// src/init.ts
|
|
9485
10945
|
var AGENTS_MARKER_START = "<!-- leglas:start -->";
|
|
@@ -9598,7 +11058,8 @@ Useful to know:
|
|
|
9598
11058
|
they have asked for that is not done yet. Add \`--screenshot\` to render a PNG
|
|
9599
11059
|
you can inspect. Run it when handed a direction you did not register yourself.
|
|
9600
11060
|
- Every command accepts \`--json\` and prints one envelope with a stable exit
|
|
9601
|
-
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.
|
|
9602
11063
|
|
|
9603
11064
|
${AGENTS_MARKER_END}
|
|
9604
11065
|
`;
|
|
@@ -9640,7 +11101,7 @@ ${AGENTS_SECTION}`
|
|
|
9640
11101
|
// src/run-init.ts
|
|
9641
11102
|
async function readIfPresent(path) {
|
|
9642
11103
|
try {
|
|
9643
|
-
return await
|
|
11104
|
+
return await readFile16(path, "utf8");
|
|
9644
11105
|
} catch {
|
|
9645
11106
|
return null;
|
|
9646
11107
|
}
|
|
@@ -9648,18 +11109,18 @@ async function readIfPresent(path) {
|
|
|
9648
11109
|
async function runInit(options, deps) {
|
|
9649
11110
|
const existingConfig = findConfigFile(options.cwd);
|
|
9650
11111
|
const plan = planInit({
|
|
9651
|
-
agents: await readIfPresent(
|
|
11112
|
+
agents: await readIfPresent(join19(options.cwd, "AGENTS.md")),
|
|
9652
11113
|
config: existingConfig === null ? null : "present",
|
|
9653
|
-
gitignore: await readIfPresent(
|
|
11114
|
+
gitignore: await readIfPresent(join19(options.cwd, ".gitignore")),
|
|
9654
11115
|
force: options.force
|
|
9655
11116
|
});
|
|
9656
11117
|
const touched = [];
|
|
9657
11118
|
for (const write2 of plan.writes) {
|
|
9658
|
-
await
|
|
11119
|
+
await writeFile11(join19(options.cwd, write2.path), write2.contents, "utf8");
|
|
9659
11120
|
touched.push(write2.path);
|
|
9660
11121
|
}
|
|
9661
11122
|
if (plan.gitignore !== null) {
|
|
9662
|
-
await
|
|
11123
|
+
await writeFile11(join19(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9663
11124
|
touched.push(".gitignore");
|
|
9664
11125
|
}
|
|
9665
11126
|
if (options.json) {
|
|
@@ -9679,11 +11140,11 @@ async function runInit(options, deps) {
|
|
|
9679
11140
|
|
|
9680
11141
|
// src/run-keep.ts
|
|
9681
11142
|
import { existsSync as existsSync5 } from "fs";
|
|
9682
|
-
import { copyFile as copyFile2, mkdir as mkdir10, readFile as
|
|
9683
|
-
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";
|
|
9684
11145
|
|
|
9685
11146
|
// src/keep.ts
|
|
9686
|
-
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";
|
|
9687
11148
|
function surfaceOf(url) {
|
|
9688
11149
|
if (!url.startsWith("/") || !url.includes("?")) return null;
|
|
9689
11150
|
for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
|
|
@@ -9712,8 +11173,8 @@ function planKeep(options) {
|
|
|
9712
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.`
|
|
9713
11174
|
};
|
|
9714
11175
|
}
|
|
9715
|
-
const to =
|
|
9716
|
-
if (to.startsWith("..")) {
|
|
11176
|
+
const to = normalize3(options.to);
|
|
11177
|
+
if (to.startsWith("..") || isAbsolute2(to)) {
|
|
9717
11178
|
return { ok: false, error: "The destination has to be inside the project." };
|
|
9718
11179
|
}
|
|
9719
11180
|
if (to.split(/[/\\]/)[0] === ".leglas") {
|
|
@@ -9768,15 +11229,15 @@ async function writeLogEntry(options) {
|
|
|
9768
11229
|
annotations: await readAnnotations(options.cwd),
|
|
9769
11230
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
9770
11231
|
});
|
|
9771
|
-
const dir =
|
|
11232
|
+
const dir = join20(options.cwd, options.logDir);
|
|
9772
11233
|
await mkdir10(dir, { recursive: true });
|
|
9773
|
-
const file =
|
|
9774
|
-
await
|
|
11234
|
+
const file = join20(dir, `${entry.slug}.md`);
|
|
11235
|
+
await writeFile12(file, entry.markdown, "utf8");
|
|
9775
11236
|
if (entry.pictures.length > 0) {
|
|
9776
|
-
const pictureDir =
|
|
11237
|
+
const pictureDir = join20(dir, entry.slug);
|
|
9777
11238
|
await mkdir10(pictureDir, { recursive: true });
|
|
9778
11239
|
for (const picture of entry.pictures) {
|
|
9779
|
-
await copyFile2(
|
|
11240
|
+
await copyFile2(join20(options.cwd, picture.from), join20(pictureDir, picture.to)).catch(() => {
|
|
9780
11241
|
});
|
|
9781
11242
|
}
|
|
9782
11243
|
}
|
|
@@ -9797,19 +11258,20 @@ async function runKeep(options, deps) {
|
|
|
9797
11258
|
await readRenames(options.cwd)
|
|
9798
11259
|
);
|
|
9799
11260
|
if (!resolved.ok) return fail(resolved.error);
|
|
9800
|
-
const
|
|
11261
|
+
const destination = isAbsolute3(options.to) ? relative5(options.cwd, options.to) : options.to;
|
|
11262
|
+
const plan = planKeep({ title: resolved.title, previews, to: destination });
|
|
9801
11263
|
if (!plan.ok) return fail(plan.error);
|
|
9802
|
-
const from =
|
|
9803
|
-
const to =
|
|
11264
|
+
const from = join20(options.cwd, plan.move.from);
|
|
11265
|
+
const to = join20(options.cwd, plan.move.to);
|
|
9804
11266
|
if (!existsSync5(from)) {
|
|
9805
11267
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
9806
11268
|
}
|
|
9807
11269
|
if (existsSync5(to)) {
|
|
9808
11270
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
9809
11271
|
}
|
|
9810
|
-
const source = await
|
|
9811
|
-
await mkdir10(
|
|
9812
|
-
await
|
|
11272
|
+
const source = await readFile17(from, "utf8");
|
|
11273
|
+
await mkdir10(dirname12(to), { recursive: true });
|
|
11274
|
+
await writeFile12(to, renameExport(source, plan.exportName), "utf8");
|
|
9813
11275
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
9814
11276
|
let logged = null;
|
|
9815
11277
|
let logError = null;
|
|
@@ -9824,7 +11286,7 @@ async function runKeep(options, deps) {
|
|
|
9824
11286
|
} catch (error) {
|
|
9825
11287
|
logError = error instanceof Error ? error.message : String(error);
|
|
9826
11288
|
}
|
|
9827
|
-
await rm6(
|
|
11289
|
+
await rm6(join20(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
9828
11290
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
9829
11291
|
if (options.json) {
|
|
9830
11292
|
deps.log(
|
|
@@ -9863,11 +11325,11 @@ async function runKeep(options, deps) {
|
|
|
9863
11325
|
|
|
9864
11326
|
// src/run-new.ts
|
|
9865
11327
|
import { existsSync as existsSync6 } from "fs";
|
|
9866
|
-
import { mkdir as mkdir11, readFile as
|
|
9867
|
-
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";
|
|
9868
11330
|
async function readIfPresent2(path) {
|
|
9869
11331
|
try {
|
|
9870
|
-
return await
|
|
11332
|
+
return await readFile18(path, "utf8");
|
|
9871
11333
|
} catch {
|
|
9872
11334
|
return null;
|
|
9873
11335
|
}
|
|
@@ -9875,7 +11337,7 @@ async function readIfPresent2(path) {
|
|
|
9875
11337
|
async function runNew(options, deps) {
|
|
9876
11338
|
let from;
|
|
9877
11339
|
if (options.from !== void 0) {
|
|
9878
|
-
const contents = await readIfPresent2(
|
|
11340
|
+
const contents = await readIfPresent2(join21(options.cwd, options.from));
|
|
9879
11341
|
if (contents === null) {
|
|
9880
11342
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
9881
11343
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -9886,8 +11348,8 @@ async function runNew(options, deps) {
|
|
|
9886
11348
|
}
|
|
9887
11349
|
const plan = planNew({
|
|
9888
11350
|
surface: options.surface,
|
|
9889
|
-
packageJson: await readIfPresent2(
|
|
9890
|
-
gitignore: await readIfPresent2(
|
|
11351
|
+
packageJson: await readIfPresent2(join21(options.cwd, "package.json")),
|
|
11352
|
+
gitignore: await readIfPresent2(join21(options.cwd, ".gitignore")),
|
|
9891
11353
|
from
|
|
9892
11354
|
});
|
|
9893
11355
|
const fail = (error) => {
|
|
@@ -9917,7 +11379,7 @@ async function runNew(options, deps) {
|
|
|
9917
11379
|
deps.log(plan.instructions);
|
|
9918
11380
|
return { exitCode: 0, written: [] };
|
|
9919
11381
|
}
|
|
9920
|
-
const existing = plan.writes.filter((write2) => existsSync6(
|
|
11382
|
+
const existing = plan.writes.filter((write2) => existsSync6(join21(options.cwd, write2.path)));
|
|
9921
11383
|
if (existing.length > 0) {
|
|
9922
11384
|
return fail(
|
|
9923
11385
|
`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
|
|
@@ -9925,13 +11387,13 @@ async function runNew(options, deps) {
|
|
|
9925
11387
|
}
|
|
9926
11388
|
const written = [];
|
|
9927
11389
|
for (const write2 of plan.writes) {
|
|
9928
|
-
const target =
|
|
9929
|
-
await mkdir11(
|
|
9930
|
-
await
|
|
11390
|
+
const target = join21(options.cwd, write2.path);
|
|
11391
|
+
await mkdir11(dirname13(target), { recursive: true });
|
|
11392
|
+
await writeFile13(target, write2.contents, "utf8");
|
|
9931
11393
|
written.push(write2.path);
|
|
9932
11394
|
}
|
|
9933
11395
|
if (plan.gitignore !== null) {
|
|
9934
|
-
await
|
|
11396
|
+
await writeFile13(join21(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9935
11397
|
written.push(".gitignore");
|
|
9936
11398
|
}
|
|
9937
11399
|
if (options.json) {
|
|
@@ -9959,8 +11421,8 @@ async function runNew(options, deps) {
|
|
|
9959
11421
|
}
|
|
9960
11422
|
|
|
9961
11423
|
// src/run-log.ts
|
|
9962
|
-
import { readFile as
|
|
9963
|
-
import { join as
|
|
11424
|
+
import { readFile as readFile19, readdir as readdir6 } from "fs/promises";
|
|
11425
|
+
import { join as join22 } from "path";
|
|
9964
11426
|
function headline(markdown) {
|
|
9965
11427
|
const first = markdown.split("\n", 1)[0] ?? "";
|
|
9966
11428
|
return first.replace(/^#\s*/, "").trim();
|
|
@@ -9970,7 +11432,7 @@ async function runLog(options, deps) {
|
|
|
9970
11432
|
const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
|
|
9971
11433
|
let names;
|
|
9972
11434
|
try {
|
|
9973
|
-
names = (await
|
|
11435
|
+
names = (await readdir6(join22(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
|
|
9974
11436
|
} catch {
|
|
9975
11437
|
names = [];
|
|
9976
11438
|
}
|
|
@@ -9983,7 +11445,7 @@ async function runLog(options, deps) {
|
|
|
9983
11445
|
else deps.error(error);
|
|
9984
11446
|
return { exitCode: 1 };
|
|
9985
11447
|
}
|
|
9986
|
-
const markdown = await
|
|
11448
|
+
const markdown = await readFile19(join22(options.cwd, dir, found), "utf8");
|
|
9987
11449
|
if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
|
|
9988
11450
|
else deps.log(markdown.trimEnd());
|
|
9989
11451
|
return { exitCode: 0 };
|
|
@@ -9991,7 +11453,7 @@ async function runLog(options, deps) {
|
|
|
9991
11453
|
const entries = await Promise.all(
|
|
9992
11454
|
names.map(async (name) => ({
|
|
9993
11455
|
entry: name.replace(/\.md$/, ""),
|
|
9994
|
-
title: headline(await
|
|
11456
|
+
title: headline(await readFile19(join22(options.cwd, dir, name), "utf8")),
|
|
9995
11457
|
file: `${dir}/${name}`
|
|
9996
11458
|
}))
|
|
9997
11459
|
);
|
|
@@ -10009,21 +11471,21 @@ async function runLog(options, deps) {
|
|
|
10009
11471
|
}
|
|
10010
11472
|
|
|
10011
11473
|
// src/run-previews.ts
|
|
10012
|
-
import { readFile as
|
|
10013
|
-
import { join as
|
|
11474
|
+
import { readFile as readFile20, writeFile as writeFile14 } from "fs/promises";
|
|
11475
|
+
import { join as join23 } from "path";
|
|
10014
11476
|
function envelope(deps, ok, body) {
|
|
10015
11477
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
10016
11478
|
}
|
|
10017
11479
|
async function ensureIgnored(cwd) {
|
|
10018
|
-
const path =
|
|
11480
|
+
const path = join23(cwd, ".gitignore");
|
|
10019
11481
|
let current = null;
|
|
10020
11482
|
try {
|
|
10021
|
-
current = await
|
|
11483
|
+
current = await readFile20(path, "utf8");
|
|
10022
11484
|
} catch {
|
|
10023
11485
|
current = null;
|
|
10024
11486
|
}
|
|
10025
11487
|
const next = ignoreEntry(current);
|
|
10026
|
-
if (next !== null) await
|
|
11488
|
+
if (next !== null) await writeFile14(path, next, "utf8");
|
|
10027
11489
|
}
|
|
10028
11490
|
async function runAdd(options, deps) {
|
|
10029
11491
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -10164,56 +11626,6 @@ async function runRequests(options, deps) {
|
|
|
10164
11626
|
return { exitCode: 0 };
|
|
10165
11627
|
}
|
|
10166
11628
|
|
|
10167
|
-
// src/json.ts
|
|
10168
|
-
function isJsonObject(value) {
|
|
10169
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10170
|
-
}
|
|
10171
|
-
function isString2(value) {
|
|
10172
|
-
return typeof value === "string";
|
|
10173
|
-
}
|
|
10174
|
-
function isNumber2(value) {
|
|
10175
|
-
return typeof value === "number";
|
|
10176
|
-
}
|
|
10177
|
-
async function bodyOf(response) {
|
|
10178
|
-
try {
|
|
10179
|
-
return JSON.parse(await response.text());
|
|
10180
|
-
} catch {
|
|
10181
|
-
return null;
|
|
10182
|
-
}
|
|
10183
|
-
}
|
|
10184
|
-
|
|
10185
|
-
// src/running.ts
|
|
10186
|
-
import { realpath as realpath2 } from "fs/promises";
|
|
10187
|
-
var NOT_RUNNING = "Leglas is not running here. Start it with npx leglas, then try again.";
|
|
10188
|
-
async function sameDirectory(left, right) {
|
|
10189
|
-
const [a, b] = await Promise.all([
|
|
10190
|
-
realpath2(left).catch(() => left),
|
|
10191
|
-
realpath2(right).catch(() => right)
|
|
10192
|
-
]);
|
|
10193
|
-
return a === b;
|
|
10194
|
-
}
|
|
10195
|
-
async function findLeglas(cwd, port, request) {
|
|
10196
|
-
const server = port === null ? await readServerInfo(cwd) : null;
|
|
10197
|
-
const chosen = port ?? server?.port ?? DEFAULT_PORT;
|
|
10198
|
-
try {
|
|
10199
|
-
const health = await request(`http://127.0.0.1:${chosen}/leglas/api/health`, {
|
|
10200
|
-
signal: AbortSignal.timeout(2e3)
|
|
10201
|
-
});
|
|
10202
|
-
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
10203
|
-
const answered = await health.text().then((text2) => JSON.parse(text2)).catch(() => ({}));
|
|
10204
|
-
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
10205
|
-
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
10206
|
-
return {
|
|
10207
|
-
ok: false,
|
|
10208
|
-
error: `The Leglas on port ${chosen} serves another project. Start one here with npx leglas, or name the right one with --port.`
|
|
10209
|
-
};
|
|
10210
|
-
}
|
|
10211
|
-
return { ok: true, port: chosen, base: `http://127.0.0.1:${chosen}` };
|
|
10212
|
-
} catch {
|
|
10213
|
-
return { ok: false, error: NOT_RUNNING };
|
|
10214
|
-
}
|
|
10215
|
-
}
|
|
10216
|
-
|
|
10217
11629
|
// src/run-share.ts
|
|
10218
11630
|
var TUNNEL_WAIT_MS = 6e4;
|
|
10219
11631
|
var TUNNEL_POLL_MS = 500;
|
|
@@ -10664,24 +12076,24 @@ async function runShow(options, deps) {
|
|
|
10664
12076
|
|
|
10665
12077
|
// src/run-watch.ts
|
|
10666
12078
|
import { spawn as spawn4 } from "child_process";
|
|
10667
|
-
import { mkdir as mkdir12, readFile as
|
|
10668
|
-
import { dirname as
|
|
10669
|
-
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;
|
|
10670
12082
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
10671
12083
|
async function saveTemplate(cwd, run4) {
|
|
10672
|
-
const path =
|
|
12084
|
+
const path = join24(cwd, WATCH_PATH);
|
|
10673
12085
|
let config = {};
|
|
10674
12086
|
try {
|
|
10675
|
-
const parsed2 = JSON.parse(await
|
|
12087
|
+
const parsed2 = JSON.parse(await readFile21(path, "utf8"));
|
|
10676
12088
|
if (isJsonObject(parsed2)) config = parsed2;
|
|
10677
12089
|
} catch {
|
|
10678
12090
|
}
|
|
10679
12091
|
config.run = run4;
|
|
10680
|
-
await mkdir12(
|
|
10681
|
-
await
|
|
12092
|
+
await mkdir12(dirname14(path), { recursive: true });
|
|
12093
|
+
await writeFile15(path, `${JSON.stringify(config, null, 2)}
|
|
10682
12094
|
`, "utf8");
|
|
10683
12095
|
}
|
|
10684
|
-
function spawnAgent(command, args, cwd) {
|
|
12096
|
+
function spawnAgent(command, args, cwd, json) {
|
|
10685
12097
|
return new Promise((resolve5) => {
|
|
10686
12098
|
let settled = false;
|
|
10687
12099
|
const settle = (outcome) => {
|
|
@@ -10689,7 +12101,10 @@ function spawnAgent(command, args, cwd) {
|
|
|
10689
12101
|
settled = true;
|
|
10690
12102
|
resolve5(outcome);
|
|
10691
12103
|
};
|
|
10692
|
-
const child = spawn4(command, args, {
|
|
12104
|
+
const child = spawn4(command, args, {
|
|
12105
|
+
cwd,
|
|
12106
|
+
stdio: json ? ["inherit", 2, "inherit"] : "inherit"
|
|
12107
|
+
});
|
|
10693
12108
|
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
10694
12109
|
child.on(
|
|
10695
12110
|
"close",
|
|
@@ -10700,6 +12115,13 @@ function spawnAgent(command, args, cwd) {
|
|
|
10700
12115
|
});
|
|
10701
12116
|
}
|
|
10702
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
|
+
};
|
|
10703
12125
|
const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
|
|
10704
12126
|
const raw = options.run ?? saved.run;
|
|
10705
12127
|
let template;
|
|
@@ -10707,10 +12129,7 @@ async function runWatch(options, deps) {
|
|
|
10707
12129
|
let synthesizedAgent = null;
|
|
10708
12130
|
if (raw !== null) {
|
|
10709
12131
|
const parsed2 = parseTemplate(raw);
|
|
10710
|
-
if (!parsed2.ok)
|
|
10711
|
-
deps.error(parsed2.error);
|
|
10712
|
-
return { exitCode: 1 };
|
|
10713
|
-
}
|
|
12132
|
+
if (!parsed2.ok) return refuse2(parsed2.error);
|
|
10714
12133
|
template = parsed2.template;
|
|
10715
12134
|
shownCommand2 = raw;
|
|
10716
12135
|
} else if (saved.agent !== null && saved.agent !== "custom") {
|
|
@@ -10722,10 +12141,9 @@ async function runWatch(options, deps) {
|
|
|
10722
12141
|
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
10723
12142
|
synthesizedAgent = adapter.name;
|
|
10724
12143
|
} else {
|
|
10725
|
-
|
|
12144
|
+
return refuse2(
|
|
10726
12145
|
'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
|
|
10727
12146
|
);
|
|
10728
|
-
return { exitCode: 1 };
|
|
10729
12147
|
}
|
|
10730
12148
|
if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
|
|
10731
12149
|
});
|
|
@@ -10741,26 +12159,36 @@ async function runWatch(options, deps) {
|
|
|
10741
12159
|
} catch {
|
|
10742
12160
|
}
|
|
10743
12161
|
};
|
|
10744
|
-
if (
|
|
10745
|
-
|
|
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.");
|
|
10746
12170
|
}
|
|
10747
|
-
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
10748
|
-
deps.log("Stop with Ctrl-C.");
|
|
10749
12171
|
const failed = /* @__PURE__ */ new Set();
|
|
10750
12172
|
let stopped = false;
|
|
10751
12173
|
let busy = false;
|
|
10752
12174
|
let announced = false;
|
|
10753
12175
|
let inflight = null;
|
|
10754
12176
|
const handle = async (request) => {
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
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
|
+
}
|
|
10758
12185
|
await markPickedUp(options.cwd, request.id);
|
|
10759
12186
|
const { command, args } = commandFor(template, request.prompt);
|
|
10760
|
-
const outcome = await spawnAgent(command, args, options.cwd);
|
|
12187
|
+
const outcome = await spawnAgent(command, args, options.cwd, json);
|
|
10761
12188
|
if (outcome.ok && outcome.code === 0) {
|
|
10762
12189
|
await removeRequest(options.cwd, request.id);
|
|
10763
|
-
|
|
12190
|
+
if (json) emit({ event: "done", id, title });
|
|
12191
|
+
else deps.log(` done ${title}`);
|
|
10764
12192
|
return;
|
|
10765
12193
|
}
|
|
10766
12194
|
failed.add(request.id);
|
|
@@ -10770,8 +12198,12 @@ async function runWatch(options, deps) {
|
|
|
10770
12198
|
exitCode: outcome.ok ? outcome.code : null
|
|
10771
12199
|
});
|
|
10772
12200
|
await markFailed(options.cwd, request.id, failure);
|
|
10773
|
-
|
|
10774
|
-
|
|
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
|
+
}
|
|
10775
12207
|
};
|
|
10776
12208
|
const tick = async () => {
|
|
10777
12209
|
if (stopped) return;
|
|
@@ -10790,14 +12222,16 @@ async function runWatch(options, deps) {
|
|
|
10790
12222
|
await inflight;
|
|
10791
12223
|
}
|
|
10792
12224
|
} catch (error) {
|
|
10793
|
-
|
|
12225
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
12226
|
+
if (json) emit({ event: "error", message: message2 });
|
|
12227
|
+
else deps.error(` ! ${message2}`);
|
|
10794
12228
|
} finally {
|
|
10795
12229
|
inflight = null;
|
|
10796
12230
|
busy = false;
|
|
10797
12231
|
}
|
|
10798
12232
|
};
|
|
10799
12233
|
return new Promise((resolve5) => {
|
|
10800
|
-
const timer = setInterval(() => void tick(),
|
|
12234
|
+
const timer = setInterval(() => void tick(), POLL_MS3);
|
|
10801
12235
|
const stop = () => {
|
|
10802
12236
|
if (stopped) return;
|
|
10803
12237
|
stopped = true;
|
|
@@ -10805,7 +12239,10 @@ async function runWatch(options, deps) {
|
|
|
10805
12239
|
process.off("SIGINT", stop);
|
|
10806
12240
|
process.off("SIGTERM", stop);
|
|
10807
12241
|
void Promise.resolve(inflight).catch(() => {
|
|
10808
|
-
}).then(() => heartbeat(false)).then(() =>
|
|
12242
|
+
}).then(() => heartbeat(false)).then(() => {
|
|
12243
|
+
if (json) emit({ event: "stopped" });
|
|
12244
|
+
resolve5({ exitCode: 0 });
|
|
12245
|
+
});
|
|
10809
12246
|
};
|
|
10810
12247
|
process.on("SIGINT", stop);
|
|
10811
12248
|
process.on("SIGTERM", stop);
|
|
@@ -10895,13 +12332,13 @@ function createHandoff() {
|
|
|
10895
12332
|
import { existsSync as existsSync7 } from "fs";
|
|
10896
12333
|
import { realpath as realpath4 } from "fs/promises";
|
|
10897
12334
|
import { createRequire } from "module";
|
|
10898
|
-
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";
|
|
10899
12336
|
import { fileURLToPath } from "url";
|
|
10900
12337
|
|
|
10901
12338
|
// src/dev-server-owner.ts
|
|
10902
12339
|
import { execFile as execFile2 } from "child_process";
|
|
10903
12340
|
import { realpath as realpath3 } from "fs/promises";
|
|
10904
|
-
import { basename as basename5, isAbsolute as
|
|
12341
|
+
import { basename as basename5, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve3 } from "path";
|
|
10905
12342
|
import { promisify as promisify2 } from "util";
|
|
10906
12343
|
var run2 = promisify2(execFile2);
|
|
10907
12344
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -10970,8 +12407,8 @@ async function inspectLocalDevServer(origin) {
|
|
|
10970
12407
|
}
|
|
10971
12408
|
}
|
|
10972
12409
|
function isInside(parent, child) {
|
|
10973
|
-
const path =
|
|
10974
|
-
return path === "" || !path.startsWith("..") && !
|
|
12410
|
+
const path = relative6(resolve3(parent), resolve3(child));
|
|
12411
|
+
return path === "" || !path.startsWith("..") && !isAbsolute4(path);
|
|
10975
12412
|
}
|
|
10976
12413
|
function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
10977
12414
|
const port = localDevServerPort(origin);
|
|
@@ -10984,11 +12421,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
10984
12421
|
|
|
10985
12422
|
// src/run.ts
|
|
10986
12423
|
function findShellDir() {
|
|
10987
|
-
const bundled =
|
|
10988
|
-
if (existsSync7(
|
|
12424
|
+
const bundled = join25(dirname15(fileURLToPath(import.meta.url)), "shell");
|
|
12425
|
+
if (existsSync7(join25(bundled, "index.html"))) return bundled;
|
|
10989
12426
|
try {
|
|
10990
12427
|
const require2 = createRequire(import.meta.url);
|
|
10991
|
-
return
|
|
12428
|
+
return dirname15(require2.resolve("@leglas/shell/dist/index.html"));
|
|
10992
12429
|
} catch {
|
|
10993
12430
|
return null;
|
|
10994
12431
|
}
|
|
@@ -10999,7 +12436,7 @@ function shellWord(value) {
|
|
|
10999
12436
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
11000
12437
|
}
|
|
11001
12438
|
function embeddedLeglasCommand() {
|
|
11002
|
-
const entry =
|
|
12439
|
+
const entry = join25(dirname15(fileURLToPath(import.meta.url)), "bin.js");
|
|
11003
12440
|
if (!existsSync7(entry)) return "npx -y leglas";
|
|
11004
12441
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
11005
12442
|
}
|
|
@@ -11038,7 +12475,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11038
12475
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
11039
12476
|
for (const preview of merged?.previews ?? []) {
|
|
11040
12477
|
if (preview.file !== void 0) {
|
|
11041
|
-
const absolute =
|
|
12478
|
+
const absolute = join25(options.cwd, preview.file);
|
|
11042
12479
|
if (!existsSync7(absolute)) {
|
|
11043
12480
|
previewErrors.push(
|
|
11044
12481
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -11049,7 +12486,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11049
12486
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
11050
12487
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
11051
12488
|
}
|
|
11052
|
-
fileMounts.set(slug,
|
|
12489
|
+
fileMounts.set(slug, dirname15(absolute));
|
|
11053
12490
|
previews.push({
|
|
11054
12491
|
...preview,
|
|
11055
12492
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
@@ -11061,8 +12498,8 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11061
12498
|
const config = merged === null ? null : { ...merged, previews };
|
|
11062
12499
|
const configWarnings = [];
|
|
11063
12500
|
const projectRoot = await realpath4(
|
|
11064
|
-
loaded.path === null ? options.cwd :
|
|
11065
|
-
).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)));
|
|
11066
12503
|
const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
11067
12504
|
const serverOptions = {
|
|
11068
12505
|
config,
|
|
@@ -11100,7 +12537,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
11100
12537
|
})
|
|
11101
12538
|
);
|
|
11102
12539
|
} else {
|
|
11103
|
-
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;
|
|
11104
12541
|
deps.log(`Leglas ${url}`);
|
|
11105
12542
|
deps.log(
|
|
11106
12543
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|
|
@@ -11177,18 +12614,17 @@ async function startViewer(options, deps) {
|
|
|
11177
12614
|
updates
|
|
11178
12615
|
});
|
|
11179
12616
|
const { handOff, handedOff } = createHandoff();
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
);
|
|
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));
|
|
11187
12623
|
(deps.installShutdown ?? installShutdown)(async () => {
|
|
11188
12624
|
if (handedOff()) return;
|
|
11189
12625
|
await result.stop();
|
|
11190
|
-
|
|
11191
|
-
});
|
|
12626
|
+
handoff.exit(0);
|
|
12627
|
+
}, handoff.target);
|
|
11192
12628
|
}
|
|
11193
12629
|
|
|
11194
12630
|
// src/bin.ts
|
|
@@ -11198,7 +12634,7 @@ Usage
|
|
|
11198
12634
|
leglas init Prepare a project and teach its agents
|
|
11199
12635
|
leglas [options] Start the server and open the interface
|
|
11200
12636
|
leglas new <surface> Scaffold a branch point for a surface
|
|
11201
|
-
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
|
|
11202
12638
|
leglas classify Decide where a direction should live
|
|
11203
12639
|
leglas add --title T --url U Register a preview on this machine
|
|
11204
12640
|
leglas list Show every preview, shared and local
|
|
@@ -11223,13 +12659,19 @@ Options for new
|
|
|
11223
12659
|
--from <path> Use an existing component as the baseline
|
|
11224
12660
|
|
|
11225
12661
|
Options for explore
|
|
11226
|
-
--count <n> How many directions (default 3)
|
|
12662
|
+
--count <n> How many directions (default 3; up to 6 with --build)
|
|
11227
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)
|
|
11228
12669
|
|
|
11229
12670
|
Options for watch
|
|
11230
12671
|
--run <command> Your agent, with {prompt} where the request goes, for
|
|
11231
12672
|
example "claude -p {prompt}". Remembered after first use
|
|
11232
12673
|
--port <port> Port Leglas itself is on (default: 4100)
|
|
12674
|
+
--json One JSON line per event; the agent's output goes to stderr
|
|
11233
12675
|
|
|
11234
12676
|
Options for classify
|
|
11235
12677
|
--change <path> A file the direction creates or wires up (repeatable)
|
|
@@ -11325,6 +12767,26 @@ if (parsed.kind === "keep") {
|
|
|
11325
12767
|
);
|
|
11326
12768
|
process.exit(outcome.exitCode);
|
|
11327
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
|
+
}
|
|
11328
12790
|
if (parsed.kind === "explore") {
|
|
11329
12791
|
const outcome = runExplore(
|
|
11330
12792
|
{ surface: parsed.surface, count: parsed.count, basedOn: parsed.basedOn, json: parsed.json },
|
|
@@ -11342,7 +12804,7 @@ if (parsed.kind === "requests") {
|
|
|
11342
12804
|
}
|
|
11343
12805
|
if (parsed.kind === "watch") {
|
|
11344
12806
|
const outcome = await runWatch(
|
|
11345
|
-
{ run: parsed.run, port: parsed.port, cwd: process.cwd() },
|
|
12807
|
+
{ run: parsed.run, port: parsed.port, cwd: process.cwd(), json: parsed.json },
|
|
11346
12808
|
previewDeps
|
|
11347
12809
|
);
|
|
11348
12810
|
process.exit(outcome.exitCode);
|