leglas 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -39
- package/dist/args.d.ts +5 -0
- package/dist/bin-start.d.ts +5 -0
- package/dist/bin.js +1933 -407
- package/dist/index.js +1555 -171
- package/dist/run-explore-build.d.ts +27 -0
- package/dist/run-watch.d.ts +1 -0
- package/dist/shell/assets/index-C59b2VHc.css +1 -0
- package/dist/shell/assets/index-CcHf_UOb.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-DfiRUTCS.css +0 -1
- package/dist/shell/assets/index-lVZbvEvf.js +0 -14
package/dist/index.js
CHANGED
|
@@ -162,8 +162,13 @@ function parseClassify(rest) {
|
|
|
162
162
|
function parseWatch(rest) {
|
|
163
163
|
let run4;
|
|
164
164
|
let port;
|
|
165
|
+
let json = false;
|
|
165
166
|
for (let index = 0; index < rest.length; index += 1) {
|
|
166
167
|
const argument = rest[index];
|
|
168
|
+
if (argument === "--json") {
|
|
169
|
+
json = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
167
172
|
const equals = argument.indexOf("=");
|
|
168
173
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
169
174
|
if (flag !== "--run" && flag !== "--port") {
|
|
@@ -184,7 +189,7 @@ function parseWatch(rest) {
|
|
|
184
189
|
if ("error" in parsed) return { kind: "error", message: parsed.error };
|
|
185
190
|
port = parsed.port;
|
|
186
191
|
}
|
|
187
|
-
return { kind: "watch", run: run4, port };
|
|
192
|
+
return { kind: "watch", run: run4, port, json };
|
|
188
193
|
}
|
|
189
194
|
function parseShare(rest) {
|
|
190
195
|
const titles = [];
|
|
@@ -323,12 +328,38 @@ function parseArgs(argv) {
|
|
|
323
328
|
let count = 3;
|
|
324
329
|
let basedOn = null;
|
|
325
330
|
let json = false;
|
|
331
|
+
let build = false;
|
|
332
|
+
let brief = null;
|
|
333
|
+
let port = null;
|
|
326
334
|
for (let index = 0; index < rest.length; index += 1) {
|
|
327
335
|
const argument = rest[index];
|
|
328
336
|
if (argument === "--json") {
|
|
329
337
|
json = true;
|
|
330
338
|
continue;
|
|
331
339
|
}
|
|
340
|
+
if (argument === "--build") {
|
|
341
|
+
build = true;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (argument === "--brief" || argument.startsWith("--brief=")) {
|
|
345
|
+
const raw = argument.includes("=") ? argument.slice(argument.indexOf("=") + 1) : rest[index += 1];
|
|
346
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
347
|
+
return {
|
|
348
|
+
kind: "error",
|
|
349
|
+
message: '--brief needs a description, for example --brief "A hero for a cooking app".'
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
brief = raw;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (argument === "--port" || argument.startsWith("--port=")) {
|
|
356
|
+
const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
|
|
357
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) {
|
|
358
|
+
return { kind: "error", message: "--port needs a number, for example --port 4000." };
|
|
359
|
+
}
|
|
360
|
+
port = Number(raw);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
332
363
|
if (argument === "--count" || argument.startsWith("--count=")) {
|
|
333
364
|
const raw = argument.includes("=") ? argument.split("=")[1] : rest[index += 1];
|
|
334
365
|
if (raw === void 0 || !/^\d+$/.test(raw)) {
|
|
@@ -362,7 +393,19 @@ function parseArgs(argv) {
|
|
|
362
393
|
message: "leglas explore needs a surface name, for example: npx leglas explore hero --count 6"
|
|
363
394
|
};
|
|
364
395
|
}
|
|
365
|
-
|
|
396
|
+
if (build && brief === null && basedOn === null) {
|
|
397
|
+
return {
|
|
398
|
+
kind: "error",
|
|
399
|
+
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>".'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (!build && brief !== null) {
|
|
403
|
+
return {
|
|
404
|
+
kind: "error",
|
|
405
|
+
message: "--brief goes with --build: add --build to have Leglas build the set."
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return { kind: "explore", surface, count, basedOn, json, build, brief, port };
|
|
366
409
|
}
|
|
367
410
|
if (argv[0] === "requests") {
|
|
368
411
|
const rest = argv.slice(1);
|
|
@@ -598,8 +641,10 @@ type SearchParams = Record<string, string | string[] | undefined>;
|
|
|
598
641
|
/**
|
|
599
642
|
* Production always renders the fallback, whatever the URL says, so a branch
|
|
600
643
|
* point that reaches a deployed build cannot expose an unreleased direction.
|
|
644
|
+
* It is not exported because a file whose exports are all components hot-swaps
|
|
645
|
+
* in place, while an extra export makes every edit reload the page.
|
|
601
646
|
*/
|
|
602
|
-
|
|
647
|
+
function resolve${titleCase(slug).replace(/\s/g, "")}(searchParams: SearchParams): Direction {
|
|
603
648
|
if (process.env.NODE_ENV === "production") return FALLBACK;
|
|
604
649
|
const raw = searchParams["v-${slug}"];
|
|
605
650
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
@@ -645,8 +690,10 @@ function isProduction(): boolean {
|
|
|
645
690
|
/**
|
|
646
691
|
* Production always renders the fallback, whatever the URL says, so a branch
|
|
647
692
|
* point that reaches a deployed build cannot expose an unreleased direction.
|
|
693
|
+
* It is not exported because a file whose exports are all components hot-swaps
|
|
694
|
+
* in place, while an extra export makes every edit reload the page.
|
|
648
695
|
*/
|
|
649
|
-
|
|
696
|
+
function resolve${name}(): Direction {
|
|
650
697
|
if (isProduction()) return FALLBACK;
|
|
651
698
|
const value = new URLSearchParams(window.location.search).get("v-${slug}");
|
|
652
699
|
return value !== null && value in DIRECTIONS ? (value as Direction) : FALLBACK;
|
|
@@ -662,7 +709,7 @@ function placeholder(label, note) {
|
|
|
662
709
|
return `export function ${label}() {
|
|
663
710
|
return (
|
|
664
711
|
<div style={{ padding: "3rem", fontFamily: "system-ui" }}>
|
|
665
|
-
<p
|
|
712
|
+
<p>{${JSON.stringify(note)}}</p>
|
|
666
713
|
</div>
|
|
667
714
|
);
|
|
668
715
|
}
|
|
@@ -868,7 +915,8 @@ Useful to know:
|
|
|
868
915
|
they have asked for that is not done yet. Add \`--screenshot\` to render a PNG
|
|
869
916
|
you can inspect. Run it when handed a direction you did not register yourself.
|
|
870
917
|
- Every command accepts \`--json\` and prints one envelope with a stable exit
|
|
871
|
-
code, so you can drive it without parsing prose.
|
|
918
|
+
code, so you can drive it without parsing prose. \`watch\` keeps running, so
|
|
919
|
+
it prints one JSON line per event instead.
|
|
872
920
|
|
|
873
921
|
${AGENTS_MARKER_END}
|
|
874
922
|
`;
|
|
@@ -908,7 +956,7 @@ ${AGENTS_SECTION}`
|
|
|
908
956
|
}
|
|
909
957
|
|
|
910
958
|
// src/keep.ts
|
|
911
|
-
import { basename as basename4, extname as extname4, normalize as
|
|
959
|
+
import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, normalize as normalize3 } from "path";
|
|
912
960
|
|
|
913
961
|
// ../server/dist/log.js
|
|
914
962
|
var DEFAULT_LOG_DIR = "design-log";
|
|
@@ -989,8 +1037,8 @@ function composeEntry(input) {
|
|
|
989
1037
|
}
|
|
990
1038
|
|
|
991
1039
|
// ../server/dist/json.js
|
|
992
|
-
function parseJson(
|
|
993
|
-
return JSON.parse(
|
|
1040
|
+
function parseJson(text3) {
|
|
1041
|
+
return JSON.parse(text3);
|
|
994
1042
|
}
|
|
995
1043
|
function isJsonRecord(value) {
|
|
996
1044
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1310,7 +1358,7 @@ var KNOWN_AGENTS = {
|
|
|
1310
1358
|
// moves that precondition and only that: `-s workspace-write` still
|
|
1311
1359
|
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
1312
1360
|
// and in a git repository the flag does nothing at all.
|
|
1313
|
-
args: (prompt, effort = null,
|
|
1361
|
+
args: (prompt, effort = null, images2 = []) => [
|
|
1314
1362
|
"exec",
|
|
1315
1363
|
"--json",
|
|
1316
1364
|
...CODEX_WORKSPACE_CONFIG,
|
|
@@ -1318,23 +1366,23 @@ var KNOWN_AGENTS = {
|
|
|
1318
1366
|
"-s",
|
|
1319
1367
|
"workspace-write",
|
|
1320
1368
|
"--skip-git-repo-check",
|
|
1321
|
-
...
|
|
1369
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
1322
1370
|
prompt
|
|
1323
1371
|
],
|
|
1324
|
-
terminalArgs: (prompt, effort = null,
|
|
1372
|
+
terminalArgs: (prompt, effort = null, images2 = []) => [
|
|
1325
1373
|
"exec",
|
|
1326
1374
|
...CODEX_WORKSPACE_CONFIG,
|
|
1327
1375
|
...codexEffortConfig(effort),
|
|
1328
1376
|
"-s",
|
|
1329
1377
|
"workspace-write",
|
|
1330
1378
|
"--skip-git-repo-check",
|
|
1331
|
-
...
|
|
1379
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
1332
1380
|
prompt
|
|
1333
1381
|
],
|
|
1334
1382
|
// No sandbox flag here: `codex exec resume` refuses it and inherits the
|
|
1335
1383
|
// session's own sandbox, which the first turn set to workspace-write. The
|
|
1336
1384
|
// repository check is per invocation, so resume needs the flag of its own.
|
|
1337
|
-
resumeArgs: (sessionId, prompt, effort = null,
|
|
1385
|
+
resumeArgs: (sessionId, prompt, effort = null, images2 = []) => [
|
|
1338
1386
|
"exec",
|
|
1339
1387
|
"resume",
|
|
1340
1388
|
sessionId,
|
|
@@ -1342,7 +1390,7 @@ var KNOWN_AGENTS = {
|
|
|
1342
1390
|
...CODEX_WORKSPACE_CONFIG,
|
|
1343
1391
|
...codexEffortConfig(effort),
|
|
1344
1392
|
"--skip-git-repo-check",
|
|
1345
|
-
...
|
|
1393
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
1346
1394
|
prompt
|
|
1347
1395
|
],
|
|
1348
1396
|
sessionFrom: (event) => event.type === "thread.started" && isString(event.thread_id) ? event.thread_id : null,
|
|
@@ -2177,12 +2225,12 @@ function buildNumber(entry) {
|
|
|
2177
2225
|
const digits = /(\d+)\s*$/.exec(entry)?.[1];
|
|
2178
2226
|
return digits === void 0 ? 0 : Number(digits);
|
|
2179
2227
|
}
|
|
2180
|
-
function cacheRoots(platform, home,
|
|
2228
|
+
function cacheRoots(platform, home, readdir6 = readableDirectories) {
|
|
2181
2229
|
const playwright = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
2182
2230
|
const puppeteer = join4(home, ".cache", "puppeteer");
|
|
2183
2231
|
const newestFirst = (dir, keep = () => true) => ({
|
|
2184
2232
|
root: dir,
|
|
2185
|
-
entries:
|
|
2233
|
+
entries: readdir6(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
|
|
2186
2234
|
});
|
|
2187
2235
|
return [
|
|
2188
2236
|
newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
|
|
@@ -2203,13 +2251,13 @@ function findBrowser(search = {}) {
|
|
|
2203
2251
|
const home = search.home ?? homedir();
|
|
2204
2252
|
const exists = search.exists ?? existsSync2;
|
|
2205
2253
|
const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
|
|
2206
|
-
const
|
|
2254
|
+
const readdir6 = search.readdir ?? readableDirectories;
|
|
2207
2255
|
const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
|
|
2208
2256
|
for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
|
|
2209
2257
|
if (isString(candidate) && candidate !== "" && exists(candidate))
|
|
2210
2258
|
return candidate;
|
|
2211
2259
|
}
|
|
2212
|
-
const caches = cacheRoots(platform, home,
|
|
2260
|
+
const caches = cacheRoots(platform, home, readdir6);
|
|
2213
2261
|
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join4(root, entry, ...rest)))));
|
|
2214
2262
|
if (shell !== null)
|
|
2215
2263
|
return shell;
|
|
@@ -2239,7 +2287,7 @@ function findBrowser(search = {}) {
|
|
|
2239
2287
|
}
|
|
2240
2288
|
if (platform === "darwin" || platform === "linux") {
|
|
2241
2289
|
const playwrightRoot = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
2242
|
-
const playwright =
|
|
2290
|
+
const playwright = readdir6(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2243
2291
|
const root = join4(playwrightRoot, entry);
|
|
2244
2292
|
return [
|
|
2245
2293
|
...FOR_TESTING.map((rest) => join4(root, ...rest)),
|
|
@@ -2256,7 +2304,7 @@ function findBrowser(search = {}) {
|
|
|
2256
2304
|
const puppeteerCache = join4(home, ".cache", "puppeteer");
|
|
2257
2305
|
for (const kind of ["chrome", "chrome-headless-shell"]) {
|
|
2258
2306
|
const kindRoot = join4(puppeteerCache, kind);
|
|
2259
|
-
const found = firstExisting(
|
|
2307
|
+
const found = firstExisting(readdir6(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2260
2308
|
const root = join4(kindRoot, entry);
|
|
2261
2309
|
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join4(root, ...rest));
|
|
2262
2310
|
}));
|
|
@@ -2296,7 +2344,7 @@ async function connectWebSocket(url) {
|
|
|
2296
2344
|
socket.addEventListener("error", failed, { once: true });
|
|
2297
2345
|
});
|
|
2298
2346
|
return {
|
|
2299
|
-
send: (
|
|
2347
|
+
send: (text3) => socket.send(text3),
|
|
2300
2348
|
onMessage: (listener) => socket.addEventListener("message", (event) => listener(String(event.data))),
|
|
2301
2349
|
onClose: (listener) => socket.addEventListener("close", listener),
|
|
2302
2350
|
close: () => socket.close()
|
|
@@ -2521,10 +2569,10 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2521
2569
|
socketClosed = true;
|
|
2522
2570
|
rejectPending();
|
|
2523
2571
|
});
|
|
2524
|
-
socket.onMessage((
|
|
2572
|
+
socket.onMessage((text3) => {
|
|
2525
2573
|
let message2;
|
|
2526
2574
|
try {
|
|
2527
|
-
const parsed = parseJson(
|
|
2575
|
+
const parsed = parseJson(text3);
|
|
2528
2576
|
if (!isJsonRecord(parsed))
|
|
2529
2577
|
return;
|
|
2530
2578
|
message2 = parsed;
|
|
@@ -2819,6 +2867,12 @@ var LOCATOR = `(function (selector, text, tag) {
|
|
|
2819
2867
|
if (r.width === 0 && r.height === 0) return null;
|
|
2820
2868
|
return { x: r.left + window.scrollX, y: r.top + window.scrollY, width: r.width, height: r.height };
|
|
2821
2869
|
})`;
|
|
2870
|
+
var DRAWN_WITH = /* @__PURE__ */ new Set(["Script", "Stylesheet", "Font", "Image"]);
|
|
2871
|
+
var LAID_OUT = `(async () => {
|
|
2872
|
+
await new Promise((next) => requestAnimationFrame(() => requestAnimationFrame(next)));
|
|
2873
|
+
if (document.fonts) await document.fonts.ready;
|
|
2874
|
+
return true;
|
|
2875
|
+
})()`;
|
|
2822
2876
|
function clamp(value, low, high) {
|
|
2823
2877
|
return Math.min(high, Math.max(low, value));
|
|
2824
2878
|
}
|
|
@@ -2883,6 +2937,16 @@ function validBox(value) {
|
|
|
2883
2937
|
return false;
|
|
2884
2938
|
return [value.x, value.y, value.width, value.height].every((entry) => isNumber(entry) && Number.isFinite(entry));
|
|
2885
2939
|
}
|
|
2940
|
+
function resourced(text3, url) {
|
|
2941
|
+
const line = String(text3 ?? "");
|
|
2942
|
+
if (!isString(url) || url === "")
|
|
2943
|
+
return line;
|
|
2944
|
+
try {
|
|
2945
|
+
return `${line} (${new URL(url).pathname})`;
|
|
2946
|
+
} catch {
|
|
2947
|
+
return line;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2886
2950
|
function locatorExpression(focus) {
|
|
2887
2951
|
return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
|
|
2888
2952
|
}
|
|
@@ -2900,6 +2964,22 @@ async function render(page, input) {
|
|
|
2900
2964
|
errors.push(message2);
|
|
2901
2965
|
};
|
|
2902
2966
|
let documentStatus = null;
|
|
2967
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2968
|
+
const afterLoadScripts = /* @__PURE__ */ new Set();
|
|
2969
|
+
let loadFired = false;
|
|
2970
|
+
let scriptLanded = null;
|
|
2971
|
+
let asked = 0;
|
|
2972
|
+
let emptied = () => {
|
|
2973
|
+
};
|
|
2974
|
+
const received = (id) => {
|
|
2975
|
+
if (afterLoadScripts.delete(id))
|
|
2976
|
+
scriptLanded = Date.now();
|
|
2977
|
+
if (pending.delete(id) && pending.size === 0)
|
|
2978
|
+
emptied();
|
|
2979
|
+
};
|
|
2980
|
+
const landed = () => pending.size === 0 ? Promise.resolve() : new Promise((resolve5) => {
|
|
2981
|
+
emptied = resolve5;
|
|
2982
|
+
});
|
|
2903
2983
|
const unlisten = [
|
|
2904
2984
|
page.on("Network.responseReceived", (params) => {
|
|
2905
2985
|
if (documentStatus !== null || params?.type !== "Document")
|
|
@@ -2908,6 +2988,25 @@ async function render(page, input) {
|
|
|
2908
2988
|
if (isNumber(status))
|
|
2909
2989
|
documentStatus = status;
|
|
2910
2990
|
}),
|
|
2991
|
+
page.on("Network.requestWillBeSent", (params) => {
|
|
2992
|
+
const id = params?.requestId;
|
|
2993
|
+
if (!DRAWN_WITH.has(params?.type) || !isString(id))
|
|
2994
|
+
return;
|
|
2995
|
+
if (!pending.has(id)) {
|
|
2996
|
+
asked += 1;
|
|
2997
|
+
if (loadFired && params?.type === "Script")
|
|
2998
|
+
afterLoadScripts.add(id);
|
|
2999
|
+
}
|
|
3000
|
+
pending.add(id);
|
|
3001
|
+
}),
|
|
3002
|
+
page.on("Network.loadingFinished", (params) => {
|
|
3003
|
+
if (isString(params?.requestId))
|
|
3004
|
+
received(params.requestId);
|
|
3005
|
+
}),
|
|
3006
|
+
page.on("Network.loadingFailed", (params) => {
|
|
3007
|
+
if (isString(params?.requestId))
|
|
3008
|
+
received(params.requestId);
|
|
3009
|
+
}),
|
|
2911
3010
|
page.on("Runtime.exceptionThrown", (params) => remember(params?.exceptionDetails?.exception?.description ?? params?.exceptionDetails?.text)),
|
|
2912
3011
|
page.on("Runtime.consoleAPICalled", (params) => {
|
|
2913
3012
|
if (params?.type !== "error")
|
|
@@ -2916,7 +3015,7 @@ async function render(page, input) {
|
|
|
2916
3015
|
}),
|
|
2917
3016
|
page.on("Log.entryAdded", (params) => {
|
|
2918
3017
|
if (params?.entry?.level === "error")
|
|
2919
|
-
remember(params.entry.text);
|
|
3018
|
+
remember(resourced(params.entry.text, params.entry.url));
|
|
2920
3019
|
})
|
|
2921
3020
|
];
|
|
2922
3021
|
try {
|
|
@@ -2934,7 +3033,10 @@ async function render(page, input) {
|
|
|
2934
3033
|
const load = new Promise((resolve5) => {
|
|
2935
3034
|
loaded = resolve5;
|
|
2936
3035
|
});
|
|
2937
|
-
const stopLoad = page.on("Page.loadEventFired", () =>
|
|
3036
|
+
const stopLoad = page.on("Page.loadEventFired", () => {
|
|
3037
|
+
loadFired = true;
|
|
3038
|
+
loaded();
|
|
3039
|
+
});
|
|
2938
3040
|
unlisten.push(stopLoad);
|
|
2939
3041
|
const navigation = await page.send("Page.navigate", { url: input.url });
|
|
2940
3042
|
if (isString(navigation.errorText) && navigation.errorText !== "") {
|
|
@@ -2945,12 +3047,28 @@ async function render(page, input) {
|
|
|
2945
3047
|
if (documentStatus !== null && documentStatus >= 500) {
|
|
2946
3048
|
throw new Error(`The page did not load: the app answered HTTP ${documentStatus}.`);
|
|
2947
3049
|
}
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3050
|
+
const until = Date.now() + 2e3;
|
|
3051
|
+
let waitedForScript = null;
|
|
3052
|
+
while (Date.now() < until) {
|
|
3053
|
+
await bounded(landed(), until - Date.now(), void 0);
|
|
3054
|
+
const before = asked;
|
|
3055
|
+
const answered = await bounded(page.send("Runtime.evaluate", {
|
|
3056
|
+
expression: LAID_OUT,
|
|
3057
|
+
awaitPromise: true,
|
|
3058
|
+
returnByValue: true
|
|
3059
|
+
}), Math.max(0, until - Date.now()), void 0).then(() => true, () => false);
|
|
3060
|
+
if (!answered)
|
|
3061
|
+
break;
|
|
3062
|
+
if (pending.size !== 0 || asked !== before)
|
|
3063
|
+
continue;
|
|
3064
|
+
const latestScript = scriptLanded;
|
|
3065
|
+
if (latestScript === null || latestScript === waitedForScript)
|
|
3066
|
+
break;
|
|
3067
|
+
const remaining = Math.min(latestScript + 300, until) - Date.now();
|
|
3068
|
+
if (remaining > 0)
|
|
3069
|
+
await new Promise((resolve5) => setTimeout(resolve5, remaining));
|
|
3070
|
+
waitedForScript = latestScript;
|
|
3071
|
+
}
|
|
2954
3072
|
await bounded(page.send("Runtime.evaluate", {
|
|
2955
3073
|
expression: `(async () => {
|
|
2956
3074
|
// The page stops itself before the caller's deadline can. Without
|
|
@@ -3320,8 +3438,8 @@ async function rehomeCaptures(cwd, from, to, attachments) {
|
|
|
3320
3438
|
file: attachment.file.startsWith(prefix) ? `${CAPTURES_DIR}/${to}/${attachment.file.slice(prefix.length)}` : attachment.file
|
|
3321
3439
|
}));
|
|
3322
3440
|
}
|
|
3323
|
-
function rehomeText(
|
|
3324
|
-
return
|
|
3441
|
+
function rehomeText(text3, from, to) {
|
|
3442
|
+
return text3.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
|
|
3325
3443
|
}
|
|
3326
3444
|
async function ownCapturesRoot(cwd) {
|
|
3327
3445
|
const root = join5(cwd, CAPTURES_DIR);
|
|
@@ -4189,13 +4307,13 @@ function imageMediaType(path) {
|
|
|
4189
4307
|
return "image/gif";
|
|
4190
4308
|
return null;
|
|
4191
4309
|
}
|
|
4192
|
-
async function claudeContent(prompt,
|
|
4193
|
-
if (
|
|
4310
|
+
async function claudeContent(prompt, images2) {
|
|
4311
|
+
if (images2.length === 0)
|
|
4194
4312
|
return prompt;
|
|
4195
4313
|
const blocks = [
|
|
4196
4314
|
{ type: "text", text: prompt }
|
|
4197
4315
|
];
|
|
4198
|
-
for (const path of
|
|
4316
|
+
for (const path of images2) {
|
|
4199
4317
|
const mediaType = imageMediaType(path);
|
|
4200
4318
|
if (mediaType === null)
|
|
4201
4319
|
continue;
|
|
@@ -5098,7 +5216,7 @@ function isStateUpdater(value) {
|
|
|
5098
5216
|
return typeof value === "function";
|
|
5099
5217
|
}
|
|
5100
5218
|
var SESSION_TURNS_CAP = 8;
|
|
5101
|
-
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
5219
|
+
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images2 = []) {
|
|
5102
5220
|
if (choice.agent === null)
|
|
5103
5221
|
return null;
|
|
5104
5222
|
if (choice.agent === "custom") {
|
|
@@ -5125,24 +5243,24 @@ function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
|
5125
5243
|
agent: choice.agent,
|
|
5126
5244
|
name: adapter.name,
|
|
5127
5245
|
command: adapter.binary,
|
|
5128
|
-
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort,
|
|
5246
|
+
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort, images2), ...allow],
|
|
5129
5247
|
prompt,
|
|
5130
5248
|
effort: choice.effort,
|
|
5131
5249
|
sessionId,
|
|
5132
5250
|
resumed: true,
|
|
5133
|
-
images
|
|
5251
|
+
images: images2
|
|
5134
5252
|
};
|
|
5135
5253
|
}
|
|
5136
5254
|
return {
|
|
5137
5255
|
agent: choice.agent,
|
|
5138
5256
|
name: adapter.name,
|
|
5139
5257
|
command: adapter.binary,
|
|
5140
|
-
args: [...adapter.args(prompt, choice.effort,
|
|
5258
|
+
args: [...adapter.args(prompt, choice.effort, images2), ...allow],
|
|
5141
5259
|
prompt,
|
|
5142
5260
|
effort: choice.effort,
|
|
5143
5261
|
sessionId: null,
|
|
5144
5262
|
resumed: false,
|
|
5145
|
-
images
|
|
5263
|
+
images: images2
|
|
5146
5264
|
};
|
|
5147
5265
|
}
|
|
5148
5266
|
function lineReader(stream, onLine) {
|
|
@@ -5428,8 +5546,8 @@ function startRunner(options) {
|
|
|
5428
5546
|
`${options.leglasCommand} show`,
|
|
5429
5547
|
...request.mode === "variant" ? [registrationCommand(options.leglasCommand)] : []
|
|
5430
5548
|
];
|
|
5431
|
-
const
|
|
5432
|
-
let resolved = resolveCommand(choice, request.prompt, choice.agent === null ? null : resumable(choice.agent), allowedCommands,
|
|
5549
|
+
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);
|
|
5550
|
+
let resolved = resolveCommand(choice, request.prompt, choice.agent === null ? null : resumable(choice.agent), allowedCommands, images2);
|
|
5433
5551
|
if (resolved === null)
|
|
5434
5552
|
return;
|
|
5435
5553
|
const lines2 = [];
|
|
@@ -5475,7 +5593,7 @@ function startRunner(options) {
|
|
|
5475
5593
|
// nothing says "cancelled". Stopped still means stopped.
|
|
5476
5594
|
!stopped) {
|
|
5477
5595
|
sessions.delete(resolved.agent);
|
|
5478
|
-
const cold = resolveCommand(choice, request.prompt, null, allowedCommands,
|
|
5596
|
+
const cold = resolveCommand(choice, request.prompt, null, allowedCommands, images2);
|
|
5479
5597
|
if (cold !== null) {
|
|
5480
5598
|
resolved = cold;
|
|
5481
5599
|
observed.sessionId = null;
|
|
@@ -7169,10 +7287,10 @@ function resolveTitle(input, titles, renames) {
|
|
|
7169
7287
|
// ../server/dist/server.js
|
|
7170
7288
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
7171
7289
|
import { createHash as createHash2 } from "crypto";
|
|
7172
|
-
import { mkdir as mkdir8, readdir as
|
|
7290
|
+
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile9 } from "fs/promises";
|
|
7173
7291
|
import http4 from "http";
|
|
7174
7292
|
import net3 from "net";
|
|
7175
|
-
import { basename as basename3, dirname as
|
|
7293
|
+
import { basename as basename3, dirname as dirname10, extname as extname3, join as join16, normalize as normalize2, relative as relative4 } from "path";
|
|
7176
7294
|
|
|
7177
7295
|
// ../server/dist/frame-policy.js
|
|
7178
7296
|
var FRAMING_TIMEOUT_MS = 4e3;
|
|
@@ -7283,13 +7401,1148 @@ async function checkFraming(target, embedder, fetcher = fetch) {
|
|
|
7283
7401
|
}
|
|
7284
7402
|
}
|
|
7285
7403
|
|
|
7404
|
+
// ../server/dist/generation/generation.js
|
|
7405
|
+
import { spawn as nodeSpawn4 } from "child_process";
|
|
7406
|
+
import { readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
7407
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
7408
|
+
|
|
7409
|
+
// ../server/dist/generation/claude.js
|
|
7410
|
+
var GENERATION_EFFORT = "medium";
|
|
7411
|
+
var RESTRICTED = [
|
|
7412
|
+
"--output-format",
|
|
7413
|
+
"stream-json",
|
|
7414
|
+
"--verbose",
|
|
7415
|
+
"--restricted",
|
|
7416
|
+
"--strict-mcp-config"
|
|
7417
|
+
];
|
|
7418
|
+
function planArgs(prompt, effort = GENERATION_EFFORT) {
|
|
7419
|
+
return ["-p", prompt, ...RESTRICTED, "--tools", "", "--effort", effort];
|
|
7420
|
+
}
|
|
7421
|
+
function buildArgs(prompt, effort = GENERATION_EFFORT) {
|
|
7422
|
+
return [
|
|
7423
|
+
"-p",
|
|
7424
|
+
prompt,
|
|
7425
|
+
...RESTRICTED,
|
|
7426
|
+
"--tools",
|
|
7427
|
+
"Read,Write,Edit",
|
|
7428
|
+
"--permission-mode",
|
|
7429
|
+
"acceptEdits",
|
|
7430
|
+
"--effort",
|
|
7431
|
+
effort
|
|
7432
|
+
];
|
|
7433
|
+
}
|
|
7434
|
+
function resultText(lines2) {
|
|
7435
|
+
for (let index = lines2.length - 1; index >= 0; index -= 1) {
|
|
7436
|
+
const line = lines2[index]?.trim() ?? "";
|
|
7437
|
+
if (!line.startsWith("{"))
|
|
7438
|
+
continue;
|
|
7439
|
+
try {
|
|
7440
|
+
const event = parseJson(line);
|
|
7441
|
+
if (isJsonRecord(event) && event.type === "result" && isString(event.result))
|
|
7442
|
+
return event.result;
|
|
7443
|
+
} catch {
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
return null;
|
|
7447
|
+
}
|
|
7448
|
+
|
|
7449
|
+
// ../server/dist/generation/codex.js
|
|
7450
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
7451
|
+
import { homedir as homedir2 } from "os";
|
|
7452
|
+
import { join as join11 } from "path";
|
|
7453
|
+
var RESTRICTED2 = [
|
|
7454
|
+
"exec",
|
|
7455
|
+
"--json",
|
|
7456
|
+
"--ephemeral",
|
|
7457
|
+
"--skip-git-repo-check",
|
|
7458
|
+
"-c",
|
|
7459
|
+
`model_reasoning_effort=${GENERATION_EFFORT}`,
|
|
7460
|
+
"-c",
|
|
7461
|
+
"project_doc_max_bytes=0",
|
|
7462
|
+
"-c",
|
|
7463
|
+
"skills.include_instructions=false",
|
|
7464
|
+
"-c",
|
|
7465
|
+
"skills.bundled.enabled=false",
|
|
7466
|
+
// Through the config, not `--disable`: a Codex that lacks one of these
|
|
7467
|
+
// ignores it here, where `--disable` refuses to start at all.
|
|
7468
|
+
...[
|
|
7469
|
+
"multi_agent",
|
|
7470
|
+
"apps",
|
|
7471
|
+
"plugins",
|
|
7472
|
+
"memories",
|
|
7473
|
+
"goals",
|
|
7474
|
+
"browser_use",
|
|
7475
|
+
"computer_use",
|
|
7476
|
+
"image_generation"
|
|
7477
|
+
].flatMap((feature) => ["-c", `features.${feature}=false`])
|
|
7478
|
+
];
|
|
7479
|
+
var BARE = /^[A-Za-z0-9_-]+$/;
|
|
7480
|
+
async function codexServers(home = defaultCodexHome()) {
|
|
7481
|
+
let config;
|
|
7482
|
+
try {
|
|
7483
|
+
config = await readFile11(join11(home, "config.toml"), "utf8");
|
|
7484
|
+
} catch {
|
|
7485
|
+
return [];
|
|
7486
|
+
}
|
|
7487
|
+
const names = /* @__PURE__ */ new Set();
|
|
7488
|
+
for (const match of config.matchAll(/^\s*\[\s*mcp_servers\s*\.\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+))\s*\]\s*(?:#.*)?$/gm)) {
|
|
7489
|
+
const name = match[1] ?? match[2] ?? match[3] ?? "";
|
|
7490
|
+
if (BARE.test(name))
|
|
7491
|
+
names.add(name);
|
|
7492
|
+
}
|
|
7493
|
+
return [...names];
|
|
7494
|
+
}
|
|
7495
|
+
function defaultCodexHome(env = process.env) {
|
|
7496
|
+
return env.CODEX_HOME !== void 0 && env.CODEX_HOME !== "" ? env.CODEX_HOME : join11(homedir2(), ".codex");
|
|
7497
|
+
}
|
|
7498
|
+
var serversOff = (servers) => servers.flatMap((name) => ["-c", `mcp_servers.${name}.enabled=false`]);
|
|
7499
|
+
function codexPlanArgs(prompt, servers) {
|
|
7500
|
+
return [...RESTRICTED2, ...serversOff(servers), "-s", "read-only", prompt];
|
|
7501
|
+
}
|
|
7502
|
+
function codexBuildArgs(prompt, servers) {
|
|
7503
|
+
return [...RESTRICTED2, ...serversOff(servers), "-s", "workspace-write", prompt];
|
|
7504
|
+
}
|
|
7505
|
+
function codexResultText(lines2) {
|
|
7506
|
+
for (let index = lines2.length - 1; index >= 0; index -= 1) {
|
|
7507
|
+
const line = lines2[index]?.trim() ?? "";
|
|
7508
|
+
if (!line.startsWith("{"))
|
|
7509
|
+
continue;
|
|
7510
|
+
try {
|
|
7511
|
+
const event = parseJson(line);
|
|
7512
|
+
const item = isJsonRecord(event) ? event.item : void 0;
|
|
7513
|
+
if (isJsonRecord(event) && event.type === "item.completed" && isJsonRecord(item) && item.type === "agent_message" && isString(item.text))
|
|
7514
|
+
return item.text;
|
|
7515
|
+
} catch {
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
return null;
|
|
7519
|
+
}
|
|
7520
|
+
|
|
7521
|
+
// ../server/dist/generation/facts.js
|
|
7522
|
+
import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
|
|
7523
|
+
import { dirname as dirname7, join as join13, normalize } from "path";
|
|
7524
|
+
|
|
7525
|
+
// ../server/dist/generation/switch-file.js
|
|
7526
|
+
import { readFile as readFile12, readdir as readdir3, stat as stat4 } from "fs/promises";
|
|
7527
|
+
import { join as join12, relative as relative3 } from "path";
|
|
7528
|
+
var MAP = /const DIRECTIONS = \{([\s\S]*?)\} as const;/;
|
|
7529
|
+
var SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
7530
|
+
var SCAN_LIMIT = 3e3;
|
|
7531
|
+
async function isFile(path) {
|
|
7532
|
+
try {
|
|
7533
|
+
return (await stat4(path)).isFile();
|
|
7534
|
+
} catch {
|
|
7535
|
+
return false;
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
async function findSwitch(cwd, slug) {
|
|
7539
|
+
for (const name of ["switch.tsx", "switch.jsx"]) {
|
|
7540
|
+
const path = join12(".leglas", "variants", slug, name);
|
|
7541
|
+
if (await isFile(join12(cwd, path)))
|
|
7542
|
+
return path;
|
|
7543
|
+
}
|
|
7544
|
+
const wanted = `"v-${slug}"`;
|
|
7545
|
+
let seen = 0;
|
|
7546
|
+
const walk = async (folder) => {
|
|
7547
|
+
let items;
|
|
7548
|
+
try {
|
|
7549
|
+
items = await readdir3(join12(cwd, folder), { withFileTypes: true });
|
|
7550
|
+
} catch {
|
|
7551
|
+
return null;
|
|
7552
|
+
}
|
|
7553
|
+
for (const item of items) {
|
|
7554
|
+
if (seen >= SCAN_LIMIT || item.name.startsWith(".") || item.name === "node_modules")
|
|
7555
|
+
continue;
|
|
7556
|
+
const path = join12(folder, item.name);
|
|
7557
|
+
if (item.isDirectory()) {
|
|
7558
|
+
const found = await walk(path);
|
|
7559
|
+
if (found !== null)
|
|
7560
|
+
return found;
|
|
7561
|
+
continue;
|
|
7562
|
+
}
|
|
7563
|
+
if (!SOURCE.test(item.name))
|
|
7564
|
+
continue;
|
|
7565
|
+
seen += 1;
|
|
7566
|
+
const source = await readFile12(join12(cwd, path), "utf8").catch(() => "");
|
|
7567
|
+
if (source.includes(wanted) && MAP.test(source))
|
|
7568
|
+
return path;
|
|
7569
|
+
}
|
|
7570
|
+
return null;
|
|
7571
|
+
};
|
|
7572
|
+
for (const root of ["src", "app"]) {
|
|
7573
|
+
const found = await walk(root);
|
|
7574
|
+
if (found !== null)
|
|
7575
|
+
return relative3(cwd, join12(cwd, found));
|
|
7576
|
+
}
|
|
7577
|
+
return null;
|
|
7578
|
+
}
|
|
7579
|
+
function readDirections(source) {
|
|
7580
|
+
const imports = /* @__PURE__ */ new Map();
|
|
7581
|
+
for (const match of source.matchAll(/^import \{ (\w+) \} from "([^"]+)";$/gm)) {
|
|
7582
|
+
imports.set(match[1] ?? "", match[2] ?? "");
|
|
7583
|
+
}
|
|
7584
|
+
const body = MAP.exec(source)?.[1] ?? "";
|
|
7585
|
+
const directions = [];
|
|
7586
|
+
for (const match of body.matchAll(/"?([\w-]+)"?\s*:\s*(\w+)/g)) {
|
|
7587
|
+
const key = match[1] ?? "";
|
|
7588
|
+
const name = match[2] ?? "";
|
|
7589
|
+
const from = imports.get(name);
|
|
7590
|
+
if (from !== void 0)
|
|
7591
|
+
directions.push({ key, name, from });
|
|
7592
|
+
}
|
|
7593
|
+
return directions;
|
|
7594
|
+
}
|
|
7595
|
+
function directionKeys(source) {
|
|
7596
|
+
const body = MAP.exec(source)?.[1] ?? "";
|
|
7597
|
+
return [...body.matchAll(/"?([\w-]+)"?\s*:/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
7598
|
+
}
|
|
7599
|
+
function fallbackKey(source) {
|
|
7600
|
+
return /const FALLBACK: \w+ = "([\w-]+)";/.exec(source)?.[1] ?? null;
|
|
7601
|
+
}
|
|
7602
|
+
function addSlots(source, slots) {
|
|
7603
|
+
if (!MAP.test(source))
|
|
7604
|
+
throw new Error("The switch file has no DIRECTIONS map.");
|
|
7605
|
+
const lines2 = source.split("\n");
|
|
7606
|
+
let last = -1;
|
|
7607
|
+
lines2.forEach((line, index) => {
|
|
7608
|
+
if (line.startsWith("import "))
|
|
7609
|
+
last = index;
|
|
7610
|
+
});
|
|
7611
|
+
lines2.splice(last + 1, 0, ...slots.map((slot) => `import { ${slot.name} } from "./${slot.key}";`));
|
|
7612
|
+
const added = slots.map((slot) => ` "${slot.key}": ${slot.name},`).join("\n");
|
|
7613
|
+
return lines2.join("\n").replace(MAP, (_, body) => `const DIRECTIONS = {
|
|
7614
|
+
${body.replace(/^\s*\n|\s+$/g, "").replace(/,?$/, ",")}
|
|
7615
|
+
${added}
|
|
7616
|
+
} as const;`);
|
|
7617
|
+
}
|
|
7618
|
+
function placeholderSource(name) {
|
|
7619
|
+
return `export function ${name}() {
|
|
7620
|
+
return null;
|
|
7621
|
+
}
|
|
7622
|
+
`;
|
|
7623
|
+
}
|
|
7624
|
+
|
|
7625
|
+
// ../server/dist/generation/facts.js
|
|
7626
|
+
var IMAGE = /\.(png|jpe?g|webp|avif|gif|svg)$/i;
|
|
7627
|
+
var IMAGE_LIMIT = 24;
|
|
7628
|
+
var EXAMPLE_LINES = 250;
|
|
7629
|
+
var VARIED_LINES = 600;
|
|
7630
|
+
function clip(source, limit) {
|
|
7631
|
+
const lines2 = source.replace(/\n$/, "").split("\n");
|
|
7632
|
+
if (lines2.length <= limit)
|
|
7633
|
+
return source;
|
|
7634
|
+
const hidden = lines2.length - limit;
|
|
7635
|
+
return [
|
|
7636
|
+
...lines2.slice(0, limit),
|
|
7637
|
+
`// \u2026 ${hidden} more ${hidden === 1 ? "line" : "lines"}, not shown`
|
|
7638
|
+
].join("\n");
|
|
7639
|
+
}
|
|
7640
|
+
var BASELINE_LINES = 15;
|
|
7641
|
+
var STUB = /Re-export what your|A first direction for/;
|
|
7642
|
+
async function text2(path) {
|
|
7643
|
+
return readFile13(path, "utf8").catch(() => null);
|
|
7644
|
+
}
|
|
7645
|
+
async function firstFile(cwd, candidates) {
|
|
7646
|
+
for (const candidate of candidates) {
|
|
7647
|
+
if (await text2(join13(cwd, candidate)) !== null)
|
|
7648
|
+
return candidate;
|
|
7649
|
+
}
|
|
7650
|
+
return null;
|
|
7651
|
+
}
|
|
7652
|
+
function cssImports(source, from) {
|
|
7653
|
+
return [...source.matchAll(/import\s+["']([^"']+\.css)["']/g)].map((match) => normalize(join13(dirname7(from), match[1] ?? "")));
|
|
7654
|
+
}
|
|
7655
|
+
async function globalStylesheet(cwd) {
|
|
7656
|
+
const entries = [];
|
|
7657
|
+
const html = await text2(join13(cwd, "index.html"));
|
|
7658
|
+
const script = html === null ? null : /<script[^>]+type="module"[^>]+src="\/?([^"]+)"/.exec(html);
|
|
7659
|
+
if (script?.[1] !== void 0)
|
|
7660
|
+
entries.push(script[1]);
|
|
7661
|
+
for (const root of ["app", "src/app"]) {
|
|
7662
|
+
for (const extension of ["tsx", "jsx", "ts", "js"])
|
|
7663
|
+
entries.push(`${root}/layout.${extension}`);
|
|
7664
|
+
}
|
|
7665
|
+
const sheets = [];
|
|
7666
|
+
for (const entry of entries) {
|
|
7667
|
+
const source = await text2(join13(cwd, entry));
|
|
7668
|
+
if (source !== null)
|
|
7669
|
+
sheets.push(...cssImports(source, entry));
|
|
7670
|
+
}
|
|
7671
|
+
for (const sheet of sheets) {
|
|
7672
|
+
const source = await text2(join13(cwd, sheet));
|
|
7673
|
+
if (source !== null && /:root\s*\{|@font-face/.test(source))
|
|
7674
|
+
return sheet;
|
|
7675
|
+
}
|
|
7676
|
+
return firstFile(cwd, sheets);
|
|
7677
|
+
}
|
|
7678
|
+
async function images(cwd) {
|
|
7679
|
+
const found = [];
|
|
7680
|
+
const walk = async (folder) => {
|
|
7681
|
+
let items;
|
|
7682
|
+
try {
|
|
7683
|
+
items = await readdir4(join13(cwd, "public", folder), { withFileTypes: true });
|
|
7684
|
+
} catch {
|
|
7685
|
+
return;
|
|
7686
|
+
}
|
|
7687
|
+
for (const item of items) {
|
|
7688
|
+
if (found.length >= IMAGE_LIMIT || item.name.startsWith("."))
|
|
7689
|
+
continue;
|
|
7690
|
+
const path = folder === "" ? item.name : `${folder}/${item.name}`;
|
|
7691
|
+
if (item.isDirectory())
|
|
7692
|
+
await walk(path);
|
|
7693
|
+
else if (IMAGE.test(item.name))
|
|
7694
|
+
found.push(`/${path}`);
|
|
7695
|
+
}
|
|
7696
|
+
};
|
|
7697
|
+
await walk("");
|
|
7698
|
+
return found;
|
|
7699
|
+
}
|
|
7700
|
+
async function componentFile(cwd, from, specifier) {
|
|
7701
|
+
const base = normalize(join13(dirname7(from), specifier));
|
|
7702
|
+
return firstFile(cwd, [
|
|
7703
|
+
base,
|
|
7704
|
+
...["tsx", "jsx", "ts", "js"].map((extension) => `${base}.${extension}`)
|
|
7705
|
+
]);
|
|
7706
|
+
}
|
|
7707
|
+
async function example(cwd, switchPath, prefer) {
|
|
7708
|
+
const source = await text2(join13(cwd, switchPath));
|
|
7709
|
+
if (source === null)
|
|
7710
|
+
return null;
|
|
7711
|
+
const directions = readDirections(source);
|
|
7712
|
+
const first = prefer ?? fallbackKey(source);
|
|
7713
|
+
const ordered = [...directions].sort((a, b) => Number(b.key === first) - Number(a.key === first));
|
|
7714
|
+
for (const direction of ordered) {
|
|
7715
|
+
const limit = direction.key === prefer ? VARIED_LINES : EXAMPLE_LINES;
|
|
7716
|
+
const path = await componentFile(cwd, switchPath, direction.from);
|
|
7717
|
+
const body = path === null ? null : await text2(join13(cwd, path));
|
|
7718
|
+
if (path === null || body === null || body === placeholderSource(direction.name) || STUB.test(body))
|
|
7719
|
+
continue;
|
|
7720
|
+
if (body.split("\n").length < BASELINE_LINES) {
|
|
7721
|
+
const target = /from\s+["'](\.[^"']+)["']/.exec(body)?.[1];
|
|
7722
|
+
const followed = target === void 0 ? null : await componentFile(cwd, path, target);
|
|
7723
|
+
const real = followed === null ? null : await text2(join13(cwd, followed));
|
|
7724
|
+
if (followed !== null && real !== null) {
|
|
7725
|
+
return {
|
|
7726
|
+
key: direction.key,
|
|
7727
|
+
path: followed,
|
|
7728
|
+
source: clip(real, limit)
|
|
7729
|
+
};
|
|
7730
|
+
}
|
|
7731
|
+
continue;
|
|
7732
|
+
}
|
|
7733
|
+
return { key: direction.key, path, source: clip(body, limit) };
|
|
7734
|
+
}
|
|
7735
|
+
return null;
|
|
7736
|
+
}
|
|
7737
|
+
async function dependencyNames(cwd) {
|
|
7738
|
+
try {
|
|
7739
|
+
const pkg = parseJson(await text2(join13(cwd, "package.json")) ?? "");
|
|
7740
|
+
return isJsonRecord(pkg) && isJsonRecord(pkg.dependencies) ? Object.keys(pkg.dependencies) : [];
|
|
7741
|
+
} catch {
|
|
7742
|
+
return [];
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
async function readProjectFacts(cwd, switchPath, prefer = null) {
|
|
7746
|
+
const dependencies = await dependencyNames(cwd);
|
|
7747
|
+
const sheetPath = await globalStylesheet(cwd);
|
|
7748
|
+
const css = sheetPath === null ? null : await text2(join13(cwd, sheetPath));
|
|
7749
|
+
return {
|
|
7750
|
+
dependencies,
|
|
7751
|
+
stylesheet: sheetPath === null || css === null ? null : {
|
|
7752
|
+
path: sheetPath,
|
|
7753
|
+
fonts: [
|
|
7754
|
+
...new Set([...css.matchAll(/font-family:\s*["']([^"']+)["']/g)].map((match) => match[1] ?? ""))
|
|
7755
|
+
],
|
|
7756
|
+
tokens: /:root\s*\{[\s\S]*?\n\}/.exec(css)?.[0] ?? ""
|
|
7757
|
+
},
|
|
7758
|
+
images: await images(cwd),
|
|
7759
|
+
example: await example(cwd, switchPath, prefer)
|
|
7760
|
+
};
|
|
7761
|
+
}
|
|
7762
|
+
function factsBlock(facts, surface, varied = null) {
|
|
7763
|
+
const lines2 = [
|
|
7764
|
+
"Leglas read the project for you. Follow its house style: differ in the idea, not in the craft."
|
|
7765
|
+
];
|
|
7766
|
+
if (facts.dependencies.length > 0)
|
|
7767
|
+
lines2.push(`Dependencies: ${facts.dependencies.join(", ")}.`);
|
|
7768
|
+
if (facts.stylesheet !== null) {
|
|
7769
|
+
const fonts = facts.stylesheet.fonts.length === 0 ? "" : ` It declares the fonts ${facts.stylesheet.fonts.join(", ")}.`;
|
|
7770
|
+
lines2.push(`The global stylesheet ${facts.stylesheet.path} is already loaded on every page.${fonts}`);
|
|
7771
|
+
if (facts.stylesheet.tokens !== "")
|
|
7772
|
+
lines2.push("It sets these tokens:", "```css", facts.stylesheet.tokens, "```");
|
|
7773
|
+
}
|
|
7774
|
+
if (facts.images.length > 0)
|
|
7775
|
+
lines2.push(`Images the project already serves from its root: ${facts.images.join(", ")}.`);
|
|
7776
|
+
if (facts.example !== null) {
|
|
7777
|
+
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(), "```");
|
|
7778
|
+
}
|
|
7779
|
+
return lines2.join("\n");
|
|
7780
|
+
}
|
|
7781
|
+
|
|
7782
|
+
// ../server/dist/generation/prompts.js
|
|
7783
|
+
function described(base) {
|
|
7784
|
+
const idea = base.idea.replace(/\.$/, "");
|
|
7785
|
+
return idea === "" ? `"${base.title}"` : `"${base.title}" (${idea})`;
|
|
7786
|
+
}
|
|
7787
|
+
function steer(brief) {
|
|
7788
|
+
if (brief === "")
|
|
7789
|
+
return ".";
|
|
7790
|
+
return `. What to explore: ${/[.!?]$/.test(brief) ? brief : `${brief}.`}`;
|
|
7791
|
+
}
|
|
7792
|
+
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).";
|
|
7793
|
+
function planPrompt(input) {
|
|
7794
|
+
const { base } = input;
|
|
7795
|
+
if (base !== void 0 && base !== null) {
|
|
7796
|
+
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}`;
|
|
7797
|
+
}
|
|
7798
|
+
const already = input.existing.length === 0 ? "" : ` The ${input.surface} already has these directions: ${input.existing.join(", ")}; yours must differ from them too.`;
|
|
7799
|
+
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}`;
|
|
7800
|
+
}
|
|
7801
|
+
function replacePrompt2(input) {
|
|
7802
|
+
const avoid = input.avoid.map((concept) => `${concept.title} (${concept.idea})`).join("; ");
|
|
7803
|
+
const { base } = input;
|
|
7804
|
+
if (base !== void 0 && base !== null) {
|
|
7805
|
+
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}`;
|
|
7806
|
+
}
|
|
7807
|
+
return `Propose 1 design direction for the ${input.surface} of this product: ${input.brief} It must not look like any of these: ${avoid}. ${ANSWER}`;
|
|
7808
|
+
}
|
|
7809
|
+
function buildPrompt(input) {
|
|
7810
|
+
const others = input.others.map((other) => `${other.title} (${other.idea})`).join("; ");
|
|
7811
|
+
const { base } = input;
|
|
7812
|
+
const varying = base !== void 0 && base !== null;
|
|
7813
|
+
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.`;
|
|
7814
|
+
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(/\.$/, "")}.`;
|
|
7815
|
+
const product = varying ? steer(input.brief).slice(1) : ` The product: ${input.brief}`;
|
|
7816
|
+
return `${what}${product}${rivals}
|
|
7817
|
+
|
|
7818
|
+
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.
|
|
7819
|
+
|
|
7820
|
+
${input.facts}
|
|
7821
|
+
|
|
7822
|
+
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.`;
|
|
7823
|
+
}
|
|
7824
|
+
function fixPrompt(input) {
|
|
7825
|
+
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.`;
|
|
7826
|
+
}
|
|
7827
|
+
function kebab(value) {
|
|
7828
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7829
|
+
}
|
|
7830
|
+
function pascal(key) {
|
|
7831
|
+
return key.split("-").filter((part) => part !== "").map((part) => /^\d/.test(part) ? `N${part}` : part[0].toUpperCase() + part.slice(1)).join("");
|
|
7832
|
+
}
|
|
7833
|
+
function parseConcepts(reply, count) {
|
|
7834
|
+
const start = reply.indexOf("[");
|
|
7835
|
+
const end = reply.lastIndexOf("]");
|
|
7836
|
+
if (start === -1 || end < start)
|
|
7837
|
+
throw new Error("The plan reply held no JSON array.");
|
|
7838
|
+
let parsed;
|
|
7839
|
+
try {
|
|
7840
|
+
parsed = parseJson(reply.slice(start, end + 1));
|
|
7841
|
+
} catch {
|
|
7842
|
+
throw new Error("The plan reply's JSON did not parse.");
|
|
7843
|
+
}
|
|
7844
|
+
if (!Array.isArray(parsed))
|
|
7845
|
+
throw new Error("The plan reply held no JSON array.");
|
|
7846
|
+
const concepts = [];
|
|
7847
|
+
for (const entry of parsed) {
|
|
7848
|
+
if (!isJsonRecord(entry))
|
|
7849
|
+
continue;
|
|
7850
|
+
const title = isString(entry.title) ? entry.title.trim() : "";
|
|
7851
|
+
const key = kebab(isString(entry.key) && entry.key.trim() !== "" ? entry.key : title);
|
|
7852
|
+
if (title === "" || key === "" || concepts.some((concept) => concept.key === key))
|
|
7853
|
+
continue;
|
|
7854
|
+
concepts.push({ key, title, idea: isString(entry.idea) ? entry.idea.trim() : "" });
|
|
7855
|
+
if (concepts.length === count)
|
|
7856
|
+
return concepts;
|
|
7857
|
+
}
|
|
7858
|
+
throw new Error(`The plan reply named fewer than ${count} distinct directions.`);
|
|
7859
|
+
}
|
|
7860
|
+
|
|
7861
|
+
// ../server/dist/generation/generation.js
|
|
7862
|
+
var MAX_DIRECTIONS = 6;
|
|
7863
|
+
var BUILD_DEADLINE_MS = 5 * 6e4;
|
|
7864
|
+
var PLAN_DEADLINE_MS = 2 * 6e4;
|
|
7865
|
+
var FIX_DEADLINE_MS = 3 * 6e4;
|
|
7866
|
+
var KILL_GRACE_MS = 5e3;
|
|
7867
|
+
var RECHECK_MS = 1500;
|
|
7868
|
+
var OWN_FAILURES = /* @__PURE__ */ new Set([
|
|
7869
|
+
"broken",
|
|
7870
|
+
"not-written",
|
|
7871
|
+
"too-slow",
|
|
7872
|
+
"unexpected"
|
|
7873
|
+
]);
|
|
7874
|
+
var BUSY = "A set of directions is already being built. Wait for it, or stop it first.";
|
|
7875
|
+
var KEPT_JOBS = 5;
|
|
7876
|
+
var CLAUDE = {
|
|
7877
|
+
id: "claude",
|
|
7878
|
+
name: "Claude",
|
|
7879
|
+
plan: (prompt) => planArgs(prompt),
|
|
7880
|
+
build: (prompt) => buildArgs(prompt),
|
|
7881
|
+
result: resultText
|
|
7882
|
+
};
|
|
7883
|
+
function codex(servers) {
|
|
7884
|
+
return {
|
|
7885
|
+
id: "codex",
|
|
7886
|
+
name: "Codex",
|
|
7887
|
+
plan: (prompt) => codexPlanArgs(prompt, servers),
|
|
7888
|
+
build: (prompt) => codexBuildArgs(prompt, servers),
|
|
7889
|
+
result: codexResultText
|
|
7890
|
+
};
|
|
7891
|
+
}
|
|
7892
|
+
function wait(milliseconds) {
|
|
7893
|
+
return new Promise((resolve5) => {
|
|
7894
|
+
setTimeout(resolve5, milliseconds).unref?.();
|
|
7895
|
+
});
|
|
7896
|
+
}
|
|
7897
|
+
function surfaceSlug2(surface) {
|
|
7898
|
+
return surface.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7899
|
+
}
|
|
7900
|
+
function baseOf(preview, surface) {
|
|
7901
|
+
let key;
|
|
7902
|
+
try {
|
|
7903
|
+
key = new URL(preview.url, "http://leglas.invalid").searchParams.get(`v-${surfaceSlug2(surface)}`);
|
|
7904
|
+
} catch {
|
|
7905
|
+
return null;
|
|
7906
|
+
}
|
|
7907
|
+
return key === null || key === "" ? null : { title: preview.title, key, idea: preview.note ?? "" };
|
|
7908
|
+
}
|
|
7909
|
+
async function stackOf(cwd, switchPath) {
|
|
7910
|
+
let deps = {};
|
|
7911
|
+
try {
|
|
7912
|
+
const pkg = parseJson(await readFile14(join14(cwd, "package.json"), "utf8"));
|
|
7913
|
+
if (isJsonRecord(pkg)) {
|
|
7914
|
+
for (const group of [pkg.devDependencies, pkg.dependencies]) {
|
|
7915
|
+
if (!isJsonRecord(group))
|
|
7916
|
+
continue;
|
|
7917
|
+
for (const [name, version] of Object.entries(group)) {
|
|
7918
|
+
if (isString(version))
|
|
7919
|
+
deps[name] = version;
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
} catch {
|
|
7924
|
+
deps = {};
|
|
7925
|
+
}
|
|
7926
|
+
const major = /\d+/.exec(deps.react ?? "")?.[0];
|
|
7927
|
+
const react = major === void 0 ? "React" : `React ${major}`;
|
|
7928
|
+
const typescript = switchPath.endsWith(".tsx") ? " and TypeScript" : "";
|
|
7929
|
+
if (deps.next !== void 0)
|
|
7930
|
+
return `a Next.js app with ${react}${typescript}`;
|
|
7931
|
+
if (deps.vite !== void 0)
|
|
7932
|
+
return `a Vite app with ${react}${typescript}`;
|
|
7933
|
+
return `an app with ${react}${typescript}`;
|
|
7934
|
+
}
|
|
7935
|
+
function provenance(job) {
|
|
7936
|
+
const words = job.surface.replace(/-/g, " ");
|
|
7937
|
+
return {
|
|
7938
|
+
askedFor: job.brief === "" ? void 0 : job.brief,
|
|
7939
|
+
// Variations sit under the direction they vary, as any family does on the rail.
|
|
7940
|
+
basedOn: job.basedOn ?? void 0,
|
|
7941
|
+
tags: [words.charAt(0).toUpperCase() + words.slice(1)]
|
|
7942
|
+
};
|
|
7943
|
+
}
|
|
7944
|
+
function copy(job) {
|
|
7945
|
+
return {
|
|
7946
|
+
...job,
|
|
7947
|
+
slots: job.slots.map((slot) => ({
|
|
7948
|
+
...slot,
|
|
7949
|
+
failure: slot.failure === null ? null : { ...slot.failure }
|
|
7950
|
+
}))
|
|
7951
|
+
};
|
|
7952
|
+
}
|
|
7953
|
+
function createGenerations(deps) {
|
|
7954
|
+
const now = deps.now ?? Date.now;
|
|
7955
|
+
const spawn5 = deps.spawn ?? ((command, args, options) => nodeSpawn4(command, args, { ...options, env: agentEnvironment() }));
|
|
7956
|
+
const lives = [];
|
|
7957
|
+
let closed = false;
|
|
7958
|
+
const busy = () => lives.some((live) => live.job.state === "planning" || live.job.state === "building");
|
|
7959
|
+
const changed = () => deps.onChange();
|
|
7960
|
+
let registry = Promise.resolve();
|
|
7961
|
+
const serially = (task) => {
|
|
7962
|
+
const next = registry.then(task, task);
|
|
7963
|
+
registry = next.catch(() => {
|
|
7964
|
+
});
|
|
7965
|
+
return next;
|
|
7966
|
+
};
|
|
7967
|
+
const run4 = (agent, args, deadlineMs, onLine) => {
|
|
7968
|
+
const lines2 = [];
|
|
7969
|
+
let timedOut = false;
|
|
7970
|
+
let child = null;
|
|
7971
|
+
const done = new Promise((resolve5) => {
|
|
7972
|
+
if (closed) {
|
|
7973
|
+
resolve5({ code: null, lines: lines2, error: "Leglas is closing.", timedOut });
|
|
7974
|
+
return;
|
|
7975
|
+
}
|
|
7976
|
+
try {
|
|
7977
|
+
child = spawn5(agent.id, args, {
|
|
7978
|
+
cwd: deps.cwd,
|
|
7979
|
+
shell: false,
|
|
7980
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7981
|
+
...ownGroup()
|
|
7982
|
+
});
|
|
7983
|
+
} catch (error) {
|
|
7984
|
+
resolve5({
|
|
7985
|
+
code: null,
|
|
7986
|
+
lines: lines2,
|
|
7987
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7988
|
+
timedOut
|
|
7989
|
+
});
|
|
7990
|
+
return;
|
|
7991
|
+
}
|
|
7992
|
+
let buffered = "";
|
|
7993
|
+
const take = (chunk) => {
|
|
7994
|
+
buffered += chunk.toString();
|
|
7995
|
+
const parts = buffered.split("\n");
|
|
7996
|
+
buffered = parts.pop() ?? "";
|
|
7997
|
+
lines2.push(...parts);
|
|
7998
|
+
if (onLine !== void 0)
|
|
7999
|
+
for (const line of parts)
|
|
8000
|
+
onLine(line);
|
|
8001
|
+
};
|
|
8002
|
+
child.stdout.on("data", take);
|
|
8003
|
+
child.stderr.on("data", take);
|
|
8004
|
+
const deadline = setTimeout(() => {
|
|
8005
|
+
timedOut = true;
|
|
8006
|
+
if (child !== null)
|
|
8007
|
+
signalTree(child, "SIGTERM");
|
|
8008
|
+
setTimeout(() => {
|
|
8009
|
+
if (child !== null)
|
|
8010
|
+
signalTree(child, "SIGKILL");
|
|
8011
|
+
}, KILL_GRACE_MS).unref?.();
|
|
8012
|
+
}, deadlineMs);
|
|
8013
|
+
deadline.unref?.();
|
|
8014
|
+
child.once("error", (error) => {
|
|
8015
|
+
clearTimeout(deadline);
|
|
8016
|
+
resolve5({ code: null, lines: lines2, error: error.message, timedOut });
|
|
8017
|
+
});
|
|
8018
|
+
child.once("close", (code) => {
|
|
8019
|
+
clearTimeout(deadline);
|
|
8020
|
+
if (buffered !== "")
|
|
8021
|
+
lines2.push(buffered);
|
|
8022
|
+
resolve5({ code, lines: lines2, error: null, timedOut });
|
|
8023
|
+
});
|
|
8024
|
+
});
|
|
8025
|
+
return {
|
|
8026
|
+
done,
|
|
8027
|
+
stop: async () => {
|
|
8028
|
+
if (child === null)
|
|
8029
|
+
return;
|
|
8030
|
+
signalTree(child, "SIGTERM");
|
|
8031
|
+
const exited = await Promise.race([
|
|
8032
|
+
done.then(() => true),
|
|
8033
|
+
wait(KILL_GRACE_MS).then(() => false)
|
|
8034
|
+
]);
|
|
8035
|
+
if (!exited && child !== null) {
|
|
8036
|
+
signalTree(child, "SIGKILL");
|
|
8037
|
+
await Promise.race([done, wait(KILL_GRACE_MS)]);
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
};
|
|
8041
|
+
};
|
|
8042
|
+
const failureOf2 = (agent, outcome) => classifyFailure({
|
|
8043
|
+
agent: agent.name,
|
|
8044
|
+
error: outcome.error,
|
|
8045
|
+
exitCode: outcome.code,
|
|
8046
|
+
lines: outcome.lines.slice(-40)
|
|
8047
|
+
});
|
|
8048
|
+
const settle = (live) => {
|
|
8049
|
+
const { job } = live;
|
|
8050
|
+
for (const slot of job.slots) {
|
|
8051
|
+
if (slot.state !== "building" && slot.state !== "checking")
|
|
8052
|
+
slot.activity = null;
|
|
8053
|
+
}
|
|
8054
|
+
if (job.state === "planning" || job.state === "failed")
|
|
8055
|
+
return;
|
|
8056
|
+
if (job.slots.some((slot) => slot.state === "building" || slot.state === "checking")) {
|
|
8057
|
+
job.state = "building";
|
|
8058
|
+
return;
|
|
8059
|
+
}
|
|
8060
|
+
job.state = job.slots.every((slot) => slot.state === "stopped") ? "stopped" : "done";
|
|
8061
|
+
job.endedAt = now();
|
|
8062
|
+
};
|
|
8063
|
+
const restore = async (slot) => {
|
|
8064
|
+
await writeFile7(join14(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), "utf8").catch(() => {
|
|
8065
|
+
});
|
|
8066
|
+
};
|
|
8067
|
+
const begin = (live, slot) => {
|
|
8068
|
+
const attempt = (live.attempts.get(slot.key) ?? 0) + 1;
|
|
8069
|
+
live.attempts.set(slot.key, attempt);
|
|
8070
|
+
return attempt;
|
|
8071
|
+
};
|
|
8072
|
+
const owns = (live, slot, attempt) => live.attempts.get(slot.key) === attempt;
|
|
8073
|
+
const guarded = (work, onError) => work.catch((error) => onError(error instanceof Error ? error.message : String(error)));
|
|
8074
|
+
const detach = (live, work, onError) => {
|
|
8075
|
+
const kept = guarded(work, onError);
|
|
8076
|
+
live.pending.add(kept);
|
|
8077
|
+
void kept.then(() => live.pending.delete(kept));
|
|
8078
|
+
};
|
|
8079
|
+
const drain = async (live) => {
|
|
8080
|
+
while (live.pending.size > 0)
|
|
8081
|
+
await Promise.all(live.pending);
|
|
8082
|
+
};
|
|
8083
|
+
const release = async (live, slot) => {
|
|
8084
|
+
const running = live.runs.get(slot.key);
|
|
8085
|
+
await running?.stop();
|
|
8086
|
+
await restore(slot);
|
|
8087
|
+
if (running !== void 0 && live.runs.get(slot.key) === running)
|
|
8088
|
+
live.runs.delete(slot.key);
|
|
8089
|
+
};
|
|
8090
|
+
const fail = (live, slot, failure) => {
|
|
8091
|
+
slot.state = "failed";
|
|
8092
|
+
slot.failure = failure;
|
|
8093
|
+
slot.endedAt = now();
|
|
8094
|
+
detach(live, restore(slot), () => {
|
|
8095
|
+
});
|
|
8096
|
+
if (OWN_FAILURES.has(failure.code))
|
|
8097
|
+
return;
|
|
8098
|
+
const same = live.job.slots.filter((other) => other.failure?.code === failure.code).length;
|
|
8099
|
+
if (same >= 2) {
|
|
8100
|
+
for (const other of live.job.slots) {
|
|
8101
|
+
if (other.state !== "building" && other.state !== "checking")
|
|
8102
|
+
continue;
|
|
8103
|
+
begin(live, other);
|
|
8104
|
+
other.state = "stopped";
|
|
8105
|
+
other.failure = {
|
|
8106
|
+
code: "same-failure",
|
|
8107
|
+
message: `Stopped after two directions failed the same way: ${failure.message}`
|
|
8108
|
+
};
|
|
8109
|
+
other.endedAt = now();
|
|
8110
|
+
detach(live, release(live, other), () => {
|
|
8111
|
+
});
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
};
|
|
8115
|
+
const lost = (live, slot, attempt, message2) => {
|
|
8116
|
+
if (!owns(live, slot, attempt))
|
|
8117
|
+
return;
|
|
8118
|
+
fail(live, slot, { code: "unexpected", message: message2 });
|
|
8119
|
+
settle(live);
|
|
8120
|
+
changed();
|
|
8121
|
+
};
|
|
8122
|
+
const follow = (live, slot, attempt) => (line) => {
|
|
8123
|
+
const activity = activityFrom(live.agent.id, line, deps.cwd);
|
|
8124
|
+
if (activity === null || activity === slot.activity || !owns(live, slot, attempt))
|
|
8125
|
+
return;
|
|
8126
|
+
slot.activity = activity;
|
|
8127
|
+
changed();
|
|
8128
|
+
};
|
|
8129
|
+
const build = async (live, slot, attempt) => {
|
|
8130
|
+
const concept = live.concepts.get(slot.key);
|
|
8131
|
+
if (concept === void 0 || !owns(live, slot, attempt))
|
|
8132
|
+
return;
|
|
8133
|
+
slot.state = "building";
|
|
8134
|
+
if (slot.startedAt === null)
|
|
8135
|
+
slot.startedAt = now();
|
|
8136
|
+
slot.endedAt = null;
|
|
8137
|
+
slot.failure = null;
|
|
8138
|
+
slot.fixed = false;
|
|
8139
|
+
slot.activity = null;
|
|
8140
|
+
changed();
|
|
8141
|
+
const others = [...live.concepts.values()].filter((other) => other !== concept);
|
|
8142
|
+
const prompt = buildPrompt({
|
|
8143
|
+
surface: live.job.surface,
|
|
8144
|
+
brief: live.job.brief,
|
|
8145
|
+
concept,
|
|
8146
|
+
others,
|
|
8147
|
+
file: slot.file,
|
|
8148
|
+
name: pascal(slot.key),
|
|
8149
|
+
stack: live.stack,
|
|
8150
|
+
facts: live.facts ?? "",
|
|
8151
|
+
base: live.base
|
|
8152
|
+
});
|
|
8153
|
+
const building = run4(live.agent, live.agent.build(prompt), BUILD_DEADLINE_MS, follow(live, slot, attempt));
|
|
8154
|
+
live.runs.set(slot.key, building);
|
|
8155
|
+
const outcome = await building.done;
|
|
8156
|
+
if (!owns(live, slot, attempt))
|
|
8157
|
+
return;
|
|
8158
|
+
if (live.runs.get(slot.key) === building)
|
|
8159
|
+
live.runs.delete(slot.key);
|
|
8160
|
+
const written = await readFile14(join14(deps.cwd, slot.file), "utf8").catch(() => "");
|
|
8161
|
+
if (!owns(live, slot, attempt))
|
|
8162
|
+
return;
|
|
8163
|
+
if (outcome.timedOut) {
|
|
8164
|
+
fail(live, slot, {
|
|
8165
|
+
code: "too-slow",
|
|
8166
|
+
message: `The build took longer than ${BUILD_DEADLINE_MS / 6e4} minutes, so Leglas stopped it.`
|
|
8167
|
+
});
|
|
8168
|
+
} else if (outcome.error !== null || outcome.code !== 0) {
|
|
8169
|
+
fail(live, slot, failureOf2(live.agent, outcome));
|
|
8170
|
+
} else if (written === placeholderSource(pascal(slot.key))) {
|
|
8171
|
+
fail(live, slot, {
|
|
8172
|
+
code: "not-written",
|
|
8173
|
+
message: "The build finished without writing its file."
|
|
8174
|
+
});
|
|
8175
|
+
} else {
|
|
8176
|
+
slot.state = "checking";
|
|
8177
|
+
slot.activity = null;
|
|
8178
|
+
changed();
|
|
8179
|
+
await check(live, slot, attempt);
|
|
8180
|
+
}
|
|
8181
|
+
settle(live);
|
|
8182
|
+
changed();
|
|
8183
|
+
};
|
|
8184
|
+
const ownErrors = async (live, slot, attempt) => {
|
|
8185
|
+
const others = live.job.slots.filter((other) => other !== slot);
|
|
8186
|
+
const until = now() + BUILD_DEADLINE_MS + FIX_DEADLINE_MS;
|
|
8187
|
+
for (; ; ) {
|
|
8188
|
+
const report = await deps.render(slot.title);
|
|
8189
|
+
if (!owns(live, slot, attempt))
|
|
8190
|
+
return null;
|
|
8191
|
+
const errors = report?.errors ?? [];
|
|
8192
|
+
const names = (file) => errors.some((error) => error.includes(file));
|
|
8193
|
+
const waiting = !names(slot.file) && others.some((other) => (other.state === "building" || other.state === "checking") && names(other.file));
|
|
8194
|
+
if (!waiting || now() >= until) {
|
|
8195
|
+
return errors.filter((error) => !others.some((other) => error.includes(other.file)));
|
|
8196
|
+
}
|
|
8197
|
+
await wait(RECHECK_MS);
|
|
8198
|
+
if (!owns(live, slot, attempt))
|
|
8199
|
+
return null;
|
|
8200
|
+
}
|
|
8201
|
+
};
|
|
8202
|
+
const check = async (live, slot, attempt) => {
|
|
8203
|
+
let errors = await ownErrors(live, slot, attempt);
|
|
8204
|
+
if (errors === null)
|
|
8205
|
+
return;
|
|
8206
|
+
if (errors.length > 0) {
|
|
8207
|
+
const fixing = run4(live.agent, live.agent.build(fixPrompt({ file: slot.file, errors })), FIX_DEADLINE_MS, follow(live, slot, attempt));
|
|
8208
|
+
live.runs.set(slot.key, fixing);
|
|
8209
|
+
const outcome = await fixing.done;
|
|
8210
|
+
if (!owns(live, slot, attempt))
|
|
8211
|
+
return;
|
|
8212
|
+
slot.activity = null;
|
|
8213
|
+
changed();
|
|
8214
|
+
if (live.runs.get(slot.key) === fixing)
|
|
8215
|
+
live.runs.delete(slot.key);
|
|
8216
|
+
if (outcome.timedOut || outcome.error !== null || outcome.code !== 0) {
|
|
8217
|
+
fail(live, slot, outcome.timedOut ? { code: "too-slow", message: "The fix took too long, so Leglas stopped it." } : failureOf2(live.agent, outcome));
|
|
8218
|
+
return;
|
|
8219
|
+
}
|
|
8220
|
+
slot.fixed = true;
|
|
8221
|
+
errors = await ownErrors(live, slot, attempt);
|
|
8222
|
+
if (errors === null)
|
|
8223
|
+
return;
|
|
8224
|
+
if (errors.length > 0) {
|
|
8225
|
+
fail(live, slot, {
|
|
8226
|
+
code: "broken",
|
|
8227
|
+
message: `The page still reports: ${errors[0] ?? ""}`
|
|
8228
|
+
});
|
|
8229
|
+
return;
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
slot.state = "ready";
|
|
8233
|
+
slot.endedAt = now();
|
|
8234
|
+
};
|
|
8235
|
+
const freeTitle = (title, taken) => {
|
|
8236
|
+
let candidate = title;
|
|
8237
|
+
for (let suffix = 2; taken.has(candidate); suffix += 1)
|
|
8238
|
+
candidate = `${title} ${suffix}`;
|
|
8239
|
+
taken.add(candidate);
|
|
8240
|
+
return candidate;
|
|
8241
|
+
};
|
|
8242
|
+
const plan = async (live, existing) => {
|
|
8243
|
+
const { job } = live;
|
|
8244
|
+
const planning = run4(live.agent, live.agent.plan(planPrompt({
|
|
8245
|
+
surface: job.surface,
|
|
8246
|
+
brief: job.brief,
|
|
8247
|
+
count: job.count,
|
|
8248
|
+
existing,
|
|
8249
|
+
base: live.base
|
|
8250
|
+
})), PLAN_DEADLINE_MS);
|
|
8251
|
+
live.plan = planning;
|
|
8252
|
+
const outcome = await planning.done;
|
|
8253
|
+
live.plan = null;
|
|
8254
|
+
if (job.state !== "planning")
|
|
8255
|
+
return;
|
|
8256
|
+
if (outcome.timedOut || outcome.error !== null || outcome.code !== 0) {
|
|
8257
|
+
job.state = "failed";
|
|
8258
|
+
job.error = outcome.timedOut ? "Planning took too long, so Leglas stopped it." : failureOf2(live.agent, outcome).message;
|
|
8259
|
+
job.endedAt = now();
|
|
8260
|
+
changed();
|
|
8261
|
+
return;
|
|
8262
|
+
}
|
|
8263
|
+
let concepts;
|
|
8264
|
+
try {
|
|
8265
|
+
concepts = parseConcepts(live.agent.result(outcome.lines) ?? "", job.count);
|
|
8266
|
+
} catch (error) {
|
|
8267
|
+
job.state = "failed";
|
|
8268
|
+
job.error = error instanceof Error ? error.message : String(error);
|
|
8269
|
+
job.endedAt = now();
|
|
8270
|
+
changed();
|
|
8271
|
+
return;
|
|
8272
|
+
}
|
|
8273
|
+
live.facts = factsBlock(await readProjectFacts(deps.cwd, live.switchPath, live.base?.key ?? null), job.surface, live.base);
|
|
8274
|
+
const slug = surfaceSlug2(job.surface);
|
|
8275
|
+
const keys = new Set(existing);
|
|
8276
|
+
const titles = new Set(await deps.titles());
|
|
8277
|
+
const folder = dirname8(live.switchPath);
|
|
8278
|
+
const slots = [];
|
|
8279
|
+
const extension = live.switchPath.endsWith(".jsx") ? "jsx" : "tsx";
|
|
8280
|
+
const taken = async (key) => keys.has(key) || await isFile(join14(deps.cwd, folder, `${key}.${extension}`));
|
|
8281
|
+
for (const concept of concepts) {
|
|
8282
|
+
let key = `${slug}-${concept.key}`;
|
|
8283
|
+
for (let suffix = 2; await taken(key); suffix += 1)
|
|
8284
|
+
key = `${slug}-${concept.key}-${suffix}`;
|
|
8285
|
+
keys.add(key);
|
|
8286
|
+
const title = freeTitle(concept.title, titles);
|
|
8287
|
+
live.concepts.set(key, { ...concept, title });
|
|
8288
|
+
slots.push({
|
|
8289
|
+
key,
|
|
8290
|
+
title,
|
|
8291
|
+
idea: concept.idea,
|
|
8292
|
+
file: join14(folder, `${key}.${extension}`),
|
|
8293
|
+
state: "building",
|
|
8294
|
+
startedAt: null,
|
|
8295
|
+
endedAt: null,
|
|
8296
|
+
failure: null,
|
|
8297
|
+
fixed: false,
|
|
8298
|
+
activity: null
|
|
8299
|
+
});
|
|
8300
|
+
}
|
|
8301
|
+
const source = await readFile14(join14(deps.cwd, live.switchPath), "utf8");
|
|
8302
|
+
for (const slot of slots) {
|
|
8303
|
+
await writeFile7(join14(deps.cwd, slot.file), placeholderSource(pascal(slot.key)), {
|
|
8304
|
+
encoding: "utf8",
|
|
8305
|
+
flag: "wx"
|
|
8306
|
+
});
|
|
8307
|
+
}
|
|
8308
|
+
await writeFile7(join14(deps.cwd, live.switchPath), addSlots(source, slots.map((slot) => ({ key: slot.key, name: pascal(slot.key) }))), "utf8");
|
|
8309
|
+
for (const slot of slots) {
|
|
8310
|
+
await serially(() => deps.register({
|
|
8311
|
+
title: slot.title,
|
|
8312
|
+
url: `/?v-${slug}=${slot.key}`,
|
|
8313
|
+
note: slot.idea,
|
|
8314
|
+
...provenance(job)
|
|
8315
|
+
}));
|
|
8316
|
+
}
|
|
8317
|
+
job.slots = slots;
|
|
8318
|
+
if (job.state !== "planning") {
|
|
8319
|
+
for (const slot of slots) {
|
|
8320
|
+
slot.state = "stopped";
|
|
8321
|
+
slot.endedAt = now();
|
|
8322
|
+
}
|
|
8323
|
+
changed();
|
|
8324
|
+
return;
|
|
8325
|
+
}
|
|
8326
|
+
job.state = "building";
|
|
8327
|
+
job.plannedAt = now();
|
|
8328
|
+
changed();
|
|
8329
|
+
await Promise.all(job.slots.map((slot) => {
|
|
8330
|
+
const attempt = begin(live, slot);
|
|
8331
|
+
return guarded(build(live, slot, attempt), (message2) => lost(live, slot, attempt, message2));
|
|
8332
|
+
}));
|
|
8333
|
+
};
|
|
8334
|
+
const find = (id) => lives.find((live) => live.job.id === id);
|
|
8335
|
+
const stop = async (id, key) => {
|
|
8336
|
+
const live = find(id);
|
|
8337
|
+
if (live === void 0)
|
|
8338
|
+
return false;
|
|
8339
|
+
const { job } = live;
|
|
8340
|
+
if (key === void 0 && job.state === "planning") {
|
|
8341
|
+
job.state = "stopped";
|
|
8342
|
+
job.endedAt = now();
|
|
8343
|
+
changed();
|
|
8344
|
+
await live.plan?.stop();
|
|
8345
|
+
await drain(live);
|
|
8346
|
+
return true;
|
|
8347
|
+
}
|
|
8348
|
+
const targets = job.slots.filter((slot) => (key === void 0 || slot.key === key) && (slot.state === "building" || slot.state === "checking"));
|
|
8349
|
+
for (const slot of targets) {
|
|
8350
|
+
begin(live, slot);
|
|
8351
|
+
slot.state = "stopped";
|
|
8352
|
+
slot.endedAt = now();
|
|
8353
|
+
}
|
|
8354
|
+
settle(live);
|
|
8355
|
+
changed();
|
|
8356
|
+
await Promise.all(targets.map((slot) => release(live, slot)));
|
|
8357
|
+
return targets.length > 0;
|
|
8358
|
+
};
|
|
8359
|
+
return {
|
|
8360
|
+
async start(request) {
|
|
8361
|
+
if (closed)
|
|
8362
|
+
return { ok: false, error: "Leglas is closing." };
|
|
8363
|
+
const agent = request.agent.agent;
|
|
8364
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
8365
|
+
return {
|
|
8366
|
+
ok: false,
|
|
8367
|
+
error: "Building directions runs on Claude or Codex. Choose one of them as the agent to use it."
|
|
8368
|
+
};
|
|
8369
|
+
}
|
|
8370
|
+
const count = Math.floor(request.count);
|
|
8371
|
+
if (!Number.isFinite(count) || count < 1 || count > MAX_DIRECTIONS) {
|
|
8372
|
+
return { ok: false, error: `Ask for between 1 and ${MAX_DIRECTIONS} directions.` };
|
|
8373
|
+
}
|
|
8374
|
+
const brief = request.brief.trim();
|
|
8375
|
+
const base = request.basedOn ?? null;
|
|
8376
|
+
if (brief === "" && base === null)
|
|
8377
|
+
return { ok: false, error: "Describe what the directions are for." };
|
|
8378
|
+
const slug = surfaceSlug2(request.surface);
|
|
8379
|
+
if (slug === "")
|
|
8380
|
+
return { ok: false, error: "Name the surface to build directions for." };
|
|
8381
|
+
if (busy())
|
|
8382
|
+
return { ok: false, error: BUSY };
|
|
8383
|
+
const switchPath = await findSwitch(deps.cwd, slug);
|
|
8384
|
+
if (switchPath === null) {
|
|
8385
|
+
return {
|
|
8386
|
+
ok: false,
|
|
8387
|
+
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.`
|
|
8388
|
+
};
|
|
8389
|
+
}
|
|
8390
|
+
const existing = directionKeys(await readFile14(join14(deps.cwd, switchPath), "utf8"));
|
|
8391
|
+
if (base !== null && !existing.includes(base.key)) {
|
|
8392
|
+
return {
|
|
8393
|
+
ok: false,
|
|
8394
|
+
error: `${base.title} is not a direction of the ${slug}, so Leglas cannot build variations of it.`
|
|
8395
|
+
};
|
|
8396
|
+
}
|
|
8397
|
+
const unbuilt = base === null ? void 0 : lives.flatMap((kept) => kept.job.slots).find((slot) => slot.key === base.key && slot.state !== "ready");
|
|
8398
|
+
if (base !== null && unbuilt !== void 0) {
|
|
8399
|
+
return {
|
|
8400
|
+
ok: false,
|
|
8401
|
+
error: `${base.title} has no finished design yet, so there is nothing to vary. Retry it first.`
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
8404
|
+
const job = {
|
|
8405
|
+
id: `gen-${now().toString(36)}`,
|
|
8406
|
+
surface: slug,
|
|
8407
|
+
brief,
|
|
8408
|
+
count,
|
|
8409
|
+
state: "planning",
|
|
8410
|
+
startedAt: now(),
|
|
8411
|
+
plannedAt: null,
|
|
8412
|
+
endedAt: null,
|
|
8413
|
+
error: null,
|
|
8414
|
+
slots: [],
|
|
8415
|
+
basedOn: base?.title ?? null,
|
|
8416
|
+
agent
|
|
8417
|
+
};
|
|
8418
|
+
const live = {
|
|
8419
|
+
job,
|
|
8420
|
+
concepts: /* @__PURE__ */ new Map(),
|
|
8421
|
+
runs: /* @__PURE__ */ new Map(),
|
|
8422
|
+
plan: null,
|
|
8423
|
+
switchPath,
|
|
8424
|
+
facts: null,
|
|
8425
|
+
stack: await stackOf(deps.cwd, switchPath),
|
|
8426
|
+
attempts: /* @__PURE__ */ new Map(),
|
|
8427
|
+
base,
|
|
8428
|
+
agent: agent === "codex" ? codex(await codexServers(deps.codexHome)) : CLAUDE,
|
|
8429
|
+
pending: /* @__PURE__ */ new Set()
|
|
8430
|
+
};
|
|
8431
|
+
if (busy())
|
|
8432
|
+
return { ok: false, error: BUSY };
|
|
8433
|
+
lives.push(live);
|
|
8434
|
+
while (lives.length > KEPT_JOBS)
|
|
8435
|
+
lives.shift();
|
|
8436
|
+
changed();
|
|
8437
|
+
detach(live, plan(live, existing), (message2) => {
|
|
8438
|
+
job.state = "failed";
|
|
8439
|
+
job.error = message2;
|
|
8440
|
+
job.endedAt = now();
|
|
8441
|
+
changed();
|
|
8442
|
+
});
|
|
8443
|
+
return { ok: true, job: copy(job) };
|
|
8444
|
+
},
|
|
8445
|
+
snapshot: () => lives.map((live) => copy(live.job)),
|
|
8446
|
+
stop,
|
|
8447
|
+
retry(id, key) {
|
|
8448
|
+
const live = find(id);
|
|
8449
|
+
const slot = live?.job.slots.find((candidate) => candidate.key === key);
|
|
8450
|
+
if (closed || live === void 0 || slot === void 0 || slot.state !== "failed" && slot.state !== "stopped" || live.runs.has(slot.key))
|
|
8451
|
+
return false;
|
|
8452
|
+
const attempt = begin(live, slot);
|
|
8453
|
+
slot.state = "building";
|
|
8454
|
+
slot.startedAt = now();
|
|
8455
|
+
slot.failure = null;
|
|
8456
|
+
detach(live, restore(slot).then(() => build(live, slot, attempt)), (message2) => lost(live, slot, attempt, message2));
|
|
8457
|
+
live.job.state = "building";
|
|
8458
|
+
live.job.endedAt = null;
|
|
8459
|
+
changed();
|
|
8460
|
+
return true;
|
|
8461
|
+
},
|
|
8462
|
+
replace(id, key) {
|
|
8463
|
+
const live = find(id);
|
|
8464
|
+
const slot = live?.job.slots.find((candidate) => candidate.key === key);
|
|
8465
|
+
if (closed || live === void 0 || slot === void 0 || slot.state === "building" || slot.state === "checking" || live.runs.has(slot.key))
|
|
8466
|
+
return false;
|
|
8467
|
+
const attempt = begin(live, slot);
|
|
8468
|
+
slot.state = "building";
|
|
8469
|
+
slot.startedAt = now();
|
|
8470
|
+
slot.failure = null;
|
|
8471
|
+
live.job.state = "building";
|
|
8472
|
+
live.job.endedAt = null;
|
|
8473
|
+
changed();
|
|
8474
|
+
const replacing = async () => {
|
|
8475
|
+
const asking = run4(live.agent, live.agent.plan(replacePrompt2({
|
|
8476
|
+
surface: live.job.surface,
|
|
8477
|
+
brief: live.job.brief,
|
|
8478
|
+
avoid: [...live.concepts.values()],
|
|
8479
|
+
base: live.base
|
|
8480
|
+
})), PLAN_DEADLINE_MS);
|
|
8481
|
+
live.runs.set(slot.key, asking);
|
|
8482
|
+
const outcome = await asking.done;
|
|
8483
|
+
if (!owns(live, slot, attempt))
|
|
8484
|
+
return;
|
|
8485
|
+
if (live.runs.get(slot.key) === asking)
|
|
8486
|
+
live.runs.delete(slot.key);
|
|
8487
|
+
let concept;
|
|
8488
|
+
try {
|
|
8489
|
+
if (outcome.code !== 0)
|
|
8490
|
+
throw new Error(failureOf2(live.agent, outcome).message);
|
|
8491
|
+
concept = parseConcepts(live.agent.result(outcome.lines) ?? "", 1)[0];
|
|
8492
|
+
} catch (error) {
|
|
8493
|
+
fail(live, slot, {
|
|
8494
|
+
code: "unreadable-plan",
|
|
8495
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8496
|
+
});
|
|
8497
|
+
settle(live);
|
|
8498
|
+
changed();
|
|
8499
|
+
return;
|
|
8500
|
+
}
|
|
8501
|
+
const titles = new Set(await deps.titles());
|
|
8502
|
+
if (!owns(live, slot, attempt))
|
|
8503
|
+
return;
|
|
8504
|
+
titles.delete(slot.title);
|
|
8505
|
+
const title = freeTitle(concept.title, titles);
|
|
8506
|
+
await serially(async () => {
|
|
8507
|
+
await deps.unregister([slot.title]);
|
|
8508
|
+
return deps.register({
|
|
8509
|
+
title,
|
|
8510
|
+
url: `/?v-${surfaceSlug2(live.job.surface)}=${slot.key}`,
|
|
8511
|
+
note: concept.idea,
|
|
8512
|
+
...provenance(live.job)
|
|
8513
|
+
});
|
|
8514
|
+
});
|
|
8515
|
+
live.concepts.set(slot.key, { ...concept, title });
|
|
8516
|
+
slot.title = title;
|
|
8517
|
+
slot.idea = concept.idea;
|
|
8518
|
+
if (!owns(live, slot, attempt))
|
|
8519
|
+
return;
|
|
8520
|
+
await restore(slot);
|
|
8521
|
+
await build(live, slot, attempt);
|
|
8522
|
+
};
|
|
8523
|
+
detach(live, replacing(), (message2) => lost(live, slot, attempt, message2));
|
|
8524
|
+
return true;
|
|
8525
|
+
},
|
|
8526
|
+
async close() {
|
|
8527
|
+
closed = true;
|
|
8528
|
+
await Promise.race([
|
|
8529
|
+
Promise.all(lives.map(async (live) => {
|
|
8530
|
+
await stop(live.job.id);
|
|
8531
|
+
await drain(live);
|
|
8532
|
+
})),
|
|
8533
|
+
wait(KILL_GRACE_MS * 2 + 1e3)
|
|
8534
|
+
]);
|
|
8535
|
+
}
|
|
8536
|
+
};
|
|
8537
|
+
}
|
|
8538
|
+
|
|
7286
8539
|
// ../server/dist/server-info.js
|
|
7287
|
-
import { lstat as lstat2, mkdir as mkdir7, readFile as
|
|
7288
|
-
import { dirname as
|
|
8540
|
+
import { lstat as lstat2, mkdir as mkdir7, readFile as readFile15, rename as rename2, rm as rm4, writeFile as writeFile8 } from "fs/promises";
|
|
8541
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
7289
8542
|
var SERVER_INFO_PATH = ".leglas/server.json";
|
|
7290
8543
|
async function writeServerInfo(cwd, info) {
|
|
7291
|
-
const path =
|
|
7292
|
-
const directory =
|
|
8544
|
+
const path = join15(cwd, SERVER_INFO_PATH);
|
|
8545
|
+
const directory = dirname9(path);
|
|
7293
8546
|
await mkdir7(directory, { recursive: true });
|
|
7294
8547
|
if (!(await lstat2(directory)).isDirectory())
|
|
7295
8548
|
return;
|
|
@@ -7297,7 +8550,7 @@ async function writeServerInfo(cwd, info) {
|
|
|
7297
8550
|
if (existing !== null && !existing.isFile())
|
|
7298
8551
|
return;
|
|
7299
8552
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
7300
|
-
await
|
|
8553
|
+
await writeFile8(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
7301
8554
|
`, "utf8");
|
|
7302
8555
|
await rename2(temporary, path).catch(async (cause) => {
|
|
7303
8556
|
await rm4(temporary, { force: true }).catch(() => {
|
|
@@ -7307,7 +8560,7 @@ async function writeServerInfo(cwd, info) {
|
|
|
7307
8560
|
}
|
|
7308
8561
|
async function readServerInfo(cwd) {
|
|
7309
8562
|
try {
|
|
7310
|
-
const value = parseJson(await
|
|
8563
|
+
const value = parseJson(await readFile15(join15(cwd, SERVER_INFO_PATH), "utf8"));
|
|
7311
8564
|
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))
|
|
7312
8565
|
return null;
|
|
7313
8566
|
return { port: value.port, url: value.url, pid: value.pid };
|
|
@@ -7321,7 +8574,7 @@ async function removeServerInfo(cwd, expected) {
|
|
|
7321
8574
|
if (current !== null && (current.port !== expected.port || current.pid !== expected.pid))
|
|
7322
8575
|
return;
|
|
7323
8576
|
}
|
|
7324
|
-
await rm4(
|
|
8577
|
+
await rm4(join15(cwd, SERVER_INFO_PATH), { force: true });
|
|
7325
8578
|
}
|
|
7326
8579
|
|
|
7327
8580
|
// ../server/dist/server.js
|
|
@@ -7482,8 +8735,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
7482
8735
|
});
|
|
7483
8736
|
}
|
|
7484
8737
|
function serveFrom(res, dir, relativePath) {
|
|
7485
|
-
const
|
|
7486
|
-
const candidate =
|
|
8738
|
+
const relative8 = normalize2(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
8739
|
+
const candidate = join16(dir, relative8);
|
|
7487
8740
|
if (!candidate.startsWith(dir))
|
|
7488
8741
|
return false;
|
|
7489
8742
|
if (!existsSync3(candidate) || !statSync(candidate).isFile())
|
|
@@ -7497,25 +8750,25 @@ function serveFrom(res, dir, relativePath) {
|
|
|
7497
8750
|
return true;
|
|
7498
8751
|
}
|
|
7499
8752
|
function serveShellFile(res, shellDir, urlPath) {
|
|
7500
|
-
const
|
|
7501
|
-
const isRoot =
|
|
7502
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
8753
|
+
const relative8 = normalize2(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
8754
|
+
const isRoot = relative8 === "" || relative8 === "." || relative8 === "/";
|
|
8755
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative8);
|
|
7503
8756
|
}
|
|
7504
8757
|
var HEALTH_PROBE_MS = 3e3;
|
|
7505
8758
|
function fileStamp(path) {
|
|
7506
8759
|
try {
|
|
7507
|
-
const
|
|
7508
|
-
return `${
|
|
8760
|
+
const stat6 = statSync(path);
|
|
8761
|
+
return `${stat6.dev}:${stat6.ino}:${stat6.size}:${stat6.mtimeMs}:${stat6.ctimeMs}`;
|
|
7509
8762
|
} catch {
|
|
7510
8763
|
return null;
|
|
7511
8764
|
}
|
|
7512
8765
|
}
|
|
7513
8766
|
function watchLiveFiles(cwd, configPath, live) {
|
|
7514
|
-
const leglasDir =
|
|
8767
|
+
const leglasDir = join16(cwd, ".leglas");
|
|
7515
8768
|
const targets = [
|
|
7516
|
-
{ path:
|
|
7517
|
-
{ path:
|
|
7518
|
-
{ path:
|
|
8769
|
+
{ path: join16(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
8770
|
+
{ path: join16(cwd, REQUESTS_PATH), change: "requests" },
|
|
8771
|
+
{ path: join16(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
7519
8772
|
];
|
|
7520
8773
|
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
7521
8774
|
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
@@ -7629,7 +8882,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7629
8882
|
}
|
|
7630
8883
|
if (configPath !== null) {
|
|
7631
8884
|
try {
|
|
7632
|
-
const directory =
|
|
8885
|
+
const directory = dirname10(configPath);
|
|
7633
8886
|
const name = basename3(configPath);
|
|
7634
8887
|
const target = { path: configPath, change: "config" };
|
|
7635
8888
|
known.set(configPath, fileStamp(configPath));
|
|
@@ -7667,7 +8920,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7667
8920
|
}
|
|
7668
8921
|
};
|
|
7669
8922
|
}
|
|
7670
|
-
function watchHealth(target, live) {
|
|
8923
|
+
function watchHealth(target, live, intervalMs) {
|
|
7671
8924
|
let previous = null;
|
|
7672
8925
|
let probing = false;
|
|
7673
8926
|
let closed = false;
|
|
@@ -7690,7 +8943,7 @@ function watchHealth(target, live) {
|
|
|
7690
8943
|
}).finally(() => {
|
|
7691
8944
|
probing = false;
|
|
7692
8945
|
});
|
|
7693
|
-
},
|
|
8946
|
+
}, intervalMs);
|
|
7694
8947
|
timer.unref?.();
|
|
7695
8948
|
return {
|
|
7696
8949
|
close: () => {
|
|
@@ -7714,15 +8967,15 @@ function configStalenessNotice(cwd, boot, current) {
|
|
|
7714
8967
|
if (boot === null && current === null)
|
|
7715
8968
|
return null;
|
|
7716
8969
|
if (boot === null && current !== null) {
|
|
7717
|
-
const label =
|
|
8970
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7718
8971
|
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
7719
8972
|
}
|
|
7720
8973
|
if (boot !== null && current === null) {
|
|
7721
|
-
const label =
|
|
8974
|
+
const label = relative4(cwd, boot.path) || boot.path;
|
|
7722
8975
|
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
7723
8976
|
}
|
|
7724
8977
|
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
7725
|
-
const label =
|
|
8978
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7726
8979
|
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
7727
8980
|
}
|
|
7728
8981
|
return null;
|
|
@@ -7768,7 +9021,7 @@ async function bind2(server, requested) {
|
|
|
7768
9021
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
7769
9022
|
}
|
|
7770
9023
|
async function startServer(options) {
|
|
7771
|
-
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
9024
|
+
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;
|
|
7772
9025
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7773
9026
|
let shares = null;
|
|
7774
9027
|
let liveHealth = null;
|
|
@@ -7813,6 +9066,7 @@ async function startServer(options) {
|
|
|
7813
9066
|
let lastSeen = null;
|
|
7814
9067
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
7815
9068
|
let runner = null;
|
|
9069
|
+
let generations = null;
|
|
7816
9070
|
let agentsCache = null;
|
|
7817
9071
|
let agentsInflight = null;
|
|
7818
9072
|
const AGENTS_FRESH_MS = 3e4;
|
|
@@ -7863,11 +9117,36 @@ async function startServer(options) {
|
|
|
7863
9117
|
});
|
|
7864
9118
|
});
|
|
7865
9119
|
};
|
|
9120
|
+
const readGenerationBody = (req, res, run4) => {
|
|
9121
|
+
if (!hasJsonBody(req)) {
|
|
9122
|
+
return sendJson2(res, 400, { ok: false, error: "A generation request must be JSON." });
|
|
9123
|
+
}
|
|
9124
|
+
let body = "";
|
|
9125
|
+
req.on("data", (chunk) => body += chunk);
|
|
9126
|
+
req.on("end", () => {
|
|
9127
|
+
const parsed = jsonBody(body);
|
|
9128
|
+
if (parsed === null)
|
|
9129
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
9130
|
+
if (generations === null)
|
|
9131
|
+
return sendJson2(res, 503, { ok: false, error: "Leglas is still starting." });
|
|
9132
|
+
void run4(parsed, generations);
|
|
9133
|
+
});
|
|
9134
|
+
};
|
|
9135
|
+
const actOnGeneration = async (res, action, parsed, act) => {
|
|
9136
|
+
if (!isString(parsed.id) || parsed.slot !== void 0 && !isString(parsed.slot)) {
|
|
9137
|
+
return sendJson2(res, 400, {
|
|
9138
|
+
ok: false,
|
|
9139
|
+
error: "Name the generation by its id, and the direction by its slot."
|
|
9140
|
+
});
|
|
9141
|
+
}
|
|
9142
|
+
const done = await act(parsed.id, isString(parsed.slot) ? parsed.slot : void 0);
|
|
9143
|
+
return done ? sendJson2(res, 200, { ok: true }) : sendJson2(res, 409, { ok: false, error: `Nothing to ${action} there right now.` });
|
|
9144
|
+
};
|
|
7866
9145
|
const handleRequest = (req, res, context) => {
|
|
7867
9146
|
const url = req.url ?? "/";
|
|
7868
9147
|
const path = url.split("?")[0] ?? "/";
|
|
7869
9148
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
7870
|
-
if (!context.remote && req.method
|
|
9149
|
+
if (!context.remote && req.method !== "GET" && req.method !== "HEAD" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7871
9150
|
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7872
9151
|
}
|
|
7873
9152
|
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
@@ -7885,6 +9164,48 @@ async function startServer(options) {
|
|
|
7885
9164
|
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7886
9165
|
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7887
9166
|
}
|
|
9167
|
+
if (path === `${LEGLAS_PREFIX}/api/generate` && req.method === "GET") {
|
|
9168
|
+
return sendJson2(res, 200, { ok: true, jobs: generations?.snapshot() ?? [] });
|
|
9169
|
+
}
|
|
9170
|
+
if (path === `${LEGLAS_PREFIX}/api/generate` && req.method === "POST") {
|
|
9171
|
+
return void readGenerationBody(req, res, async (parsed, running) => {
|
|
9172
|
+
if (!isString(parsed.surface) || !isString(parsed.brief)) {
|
|
9173
|
+
return sendJson2(res, 400, {
|
|
9174
|
+
ok: false,
|
|
9175
|
+
error: "Name a surface and describe the directions in a brief."
|
|
9176
|
+
});
|
|
9177
|
+
}
|
|
9178
|
+
let basedOn = null;
|
|
9179
|
+
if (parsed.basedOn !== void 0 && parsed.basedOn !== null && parsed.basedOn !== "") {
|
|
9180
|
+
const title = parsed.basedOn;
|
|
9181
|
+
const preview = isString(title) ? (await livePreviews()).find((entry) => entry.title === title) : void 0;
|
|
9182
|
+
basedOn = preview === void 0 ? null : baseOf(preview, parsed.surface);
|
|
9183
|
+
if (basedOn === null) {
|
|
9184
|
+
return sendJson2(res, 422, {
|
|
9185
|
+
ok: false,
|
|
9186
|
+
error: `${isString(title) ? title : "That"} is not a direction of the ${surfaceSlug2(parsed.surface)}, so Leglas cannot build variations of it.`
|
|
9187
|
+
});
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
const started = await running.start({
|
|
9191
|
+
surface: parsed.surface,
|
|
9192
|
+
brief: parsed.brief,
|
|
9193
|
+
count: isNumber(parsed.count) ? parsed.count : 3,
|
|
9194
|
+
agent: await readAgentChoice(cwd),
|
|
9195
|
+
basedOn
|
|
9196
|
+
});
|
|
9197
|
+
return started.ok ? sendJson2(res, 202, { ok: true, job: started.job }) : sendJson2(res, 422, { ok: false, error: started.error });
|
|
9198
|
+
});
|
|
9199
|
+
}
|
|
9200
|
+
if (path === `${LEGLAS_PREFIX}/api/generate/stop` && req.method === "POST") {
|
|
9201
|
+
return void readGenerationBody(req, res, (parsed, running) => actOnGeneration(res, "stop", parsed, (id, slot) => running.stop(id, slot)));
|
|
9202
|
+
}
|
|
9203
|
+
if (path === `${LEGLAS_PREFIX}/api/generate/retry` && req.method === "POST") {
|
|
9204
|
+
return void readGenerationBody(req, res, (parsed, running) => actOnGeneration(res, "retry", parsed, (id, slot) => slot === void 0 ? false : running.retry(id, slot)));
|
|
9205
|
+
}
|
|
9206
|
+
if (path === `${LEGLAS_PREFIX}/api/generate/replace` && req.method === "POST") {
|
|
9207
|
+
return void readGenerationBody(req, res, (parsed, running) => actOnGeneration(res, "replace", parsed, (id, slot) => slot === void 0 ? false : running.replace(id, slot)));
|
|
9208
|
+
}
|
|
7888
9209
|
if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
|
|
7889
9210
|
const updates = options.updates;
|
|
7890
9211
|
if (updates === void 0) {
|
|
@@ -8148,8 +9469,8 @@ async function startServer(options) {
|
|
|
8148
9469
|
const id = newRequestId();
|
|
8149
9470
|
const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
|
|
8150
9471
|
try {
|
|
8151
|
-
await mkdir8(
|
|
8152
|
-
await
|
|
9472
|
+
await mkdir8(join16(cwd, REFERENCES_DIR), { recursive: true });
|
|
9473
|
+
await writeFile9(join16(cwd, file), body);
|
|
8153
9474
|
void pruneReferences(cwd).catch(() => {
|
|
8154
9475
|
});
|
|
8155
9476
|
return sendJson2(res, 200, {
|
|
@@ -8191,7 +9512,7 @@ async function startServer(options) {
|
|
|
8191
9512
|
}
|
|
8192
9513
|
const references = parsed.references ?? [];
|
|
8193
9514
|
if (references.length > 0) {
|
|
8194
|
-
const present = new Set((await
|
|
9515
|
+
const present = new Set((await readdir5(join16(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
8195
9516
|
const gone = references.filter((id2) => !present.has(id2));
|
|
8196
9517
|
if (gone.length > 0) {
|
|
8197
9518
|
return sendJson2(res, 410, {
|
|
@@ -8321,7 +9642,7 @@ async function startServer(options) {
|
|
|
8321
9642
|
const timer = setTimeout(() => {
|
|
8322
9643
|
timedOut();
|
|
8323
9644
|
controller.abort();
|
|
8324
|
-
},
|
|
9645
|
+
}, captureDeadlineMs);
|
|
8325
9646
|
timer.unref?.();
|
|
8326
9647
|
try {
|
|
8327
9648
|
const captureInput = {
|
|
@@ -8354,8 +9675,8 @@ async function startServer(options) {
|
|
|
8354
9675
|
const noteSuffix = isString(parsed.note) ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
|
|
8355
9676
|
const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
|
|
8356
9677
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
8357
|
-
await mkdir8(
|
|
8358
|
-
await
|
|
9678
|
+
await mkdir8(join16(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
9679
|
+
await writeFile9(join16(cwd, relativeFile), shot.png);
|
|
8359
9680
|
return sendJson2(res, 200, {
|
|
8360
9681
|
ok: true,
|
|
8361
9682
|
file: relativeFile,
|
|
@@ -8735,15 +10056,15 @@ async function startServer(options) {
|
|
|
8735
10056
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
8736
10057
|
const slash = rest.indexOf("/");
|
|
8737
10058
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
8738
|
-
let
|
|
10059
|
+
let relative8 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
8739
10060
|
try {
|
|
8740
|
-
|
|
10061
|
+
relative8 = decodeURIComponent(relative8);
|
|
8741
10062
|
} catch {
|
|
8742
|
-
|
|
10063
|
+
relative8 = "";
|
|
8743
10064
|
}
|
|
8744
10065
|
const dir = fileMounts.get(slug);
|
|
8745
10066
|
const serveMount = () => {
|
|
8746
|
-
if (dir !== void 0 &&
|
|
10067
|
+
if (dir !== void 0 && relative8 !== "" && serveFrom(res, dir, relative8))
|
|
8747
10068
|
return;
|
|
8748
10069
|
res.writeHead(404, {
|
|
8749
10070
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -8753,7 +10074,7 @@ async function startServer(options) {
|
|
|
8753
10074
|
};
|
|
8754
10075
|
if (!context.remote)
|
|
8755
10076
|
return serveMount();
|
|
8756
|
-
if (
|
|
10077
|
+
if (relative8.split("/").some((segment) => segment.startsWith("."))) {
|
|
8757
10078
|
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8758
10079
|
}
|
|
8759
10080
|
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
@@ -8834,7 +10155,7 @@ async function startServer(options) {
|
|
|
8834
10155
|
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
8835
10156
|
options.updates?.setPort(port);
|
|
8836
10157
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
8837
|
-
liveHealth = watchHealth(target, live);
|
|
10158
|
+
liveHealth = watchHealth(target, live, healthProbeMs);
|
|
8838
10159
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
8839
10160
|
});
|
|
8840
10161
|
await writeServerInfo(cwd, {
|
|
@@ -8854,6 +10175,42 @@ async function startServer(options) {
|
|
|
8854
10175
|
if (options.claudeAgentSession !== void 0)
|
|
8855
10176
|
runnerOptions.claudeAgentSession = options.claudeAgentSession;
|
|
8856
10177
|
runner = startRunner(runnerOptions);
|
|
10178
|
+
const renderDirection = async (title) => {
|
|
10179
|
+
const preview = (await livePreviews()).find((entry) => entry.title === title);
|
|
10180
|
+
if (preview === void 0)
|
|
10181
|
+
return null;
|
|
10182
|
+
const browser = await browserPool.acquire();
|
|
10183
|
+
if (browser === null)
|
|
10184
|
+
return null;
|
|
10185
|
+
const address = server.address();
|
|
10186
|
+
const renderPort = address !== null && !isString(address) ? address.port : options.port ?? DEFAULT_PORT;
|
|
10187
|
+
try {
|
|
10188
|
+
const captured = await capturePage(browser, {
|
|
10189
|
+
url: previewUrl(`http://127.0.0.1:${renderPort}`, preview),
|
|
10190
|
+
width: 1440,
|
|
10191
|
+
timeoutMs: CAPTURE_LOAD_MS
|
|
10192
|
+
});
|
|
10193
|
+
return { errors: captured.errors };
|
|
10194
|
+
} catch (error) {
|
|
10195
|
+
return { errors: [error instanceof Error ? error.message : String(error)] };
|
|
10196
|
+
}
|
|
10197
|
+
};
|
|
10198
|
+
const generationDeps = {
|
|
10199
|
+
cwd,
|
|
10200
|
+
render: renderDirection,
|
|
10201
|
+
register: (input) => addLocalPreview(cwd, input, (config?.previews ?? []).filter((entry) => entry.local !== true)),
|
|
10202
|
+
unregister: async (titles) => {
|
|
10203
|
+
await dropLocalPreviews(cwd, titles);
|
|
10204
|
+
},
|
|
10205
|
+
titles: async () => {
|
|
10206
|
+
const local = await readLocalPreviews(cwd).catch(() => null);
|
|
10207
|
+
return new Set([...config?.previews ?? [], ...local?.previews ?? []].map((entry) => entry.title));
|
|
10208
|
+
},
|
|
10209
|
+
onChange: () => live.nudge("generation")
|
|
10210
|
+
};
|
|
10211
|
+
if (options.generationSpawn !== void 0)
|
|
10212
|
+
generationDeps.spawn = options.generationSpawn;
|
|
10213
|
+
generations = createGenerations(generationDeps);
|
|
8857
10214
|
options.updates?.onBusy(() => runner?.snapshot().running ?? false);
|
|
8858
10215
|
options.updates?.onChange(() => live.nudge("update"));
|
|
8859
10216
|
let closePromise = null;
|
|
@@ -8871,6 +10228,7 @@ async function startServer(options) {
|
|
|
8871
10228
|
shares?.close() ?? Promise.resolve(),
|
|
8872
10229
|
branches.stop(),
|
|
8873
10230
|
runner.stop(),
|
|
10231
|
+
generations?.close() ?? Promise.resolve(),
|
|
8874
10232
|
browserPool.close(),
|
|
8875
10233
|
live.close()
|
|
8876
10234
|
]);
|
|
@@ -8892,9 +10250,9 @@ async function startServer(options) {
|
|
|
8892
10250
|
// ../server/dist/update.js
|
|
8893
10251
|
import { spawn as spawnChild2 } from "child_process";
|
|
8894
10252
|
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
8895
|
-
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as
|
|
8896
|
-
import { homedir as
|
|
8897
|
-
import { dirname as
|
|
10253
|
+
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
10254
|
+
import { homedir as homedir3 } from "os";
|
|
10255
|
+
import { dirname as dirname11, join as join17, posix as posix2, win32 } from "path";
|
|
8898
10256
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8899
10257
|
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
8900
10258
|
|
|
@@ -8927,8 +10285,8 @@ function planKeep(options) {
|
|
|
8927
10285
|
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.`
|
|
8928
10286
|
};
|
|
8929
10287
|
}
|
|
8930
|
-
const to =
|
|
8931
|
-
if (to.startsWith("..")) {
|
|
10288
|
+
const to = normalize3(options.to);
|
|
10289
|
+
if (to.startsWith("..") || isAbsolute2(to)) {
|
|
8932
10290
|
return { ok: false, error: "The destination has to be inside the project." };
|
|
8933
10291
|
}
|
|
8934
10292
|
if (to.split(/[/\\]/)[0] === ".leglas") {
|
|
@@ -8953,11 +10311,11 @@ function planKeep(options) {
|
|
|
8953
10311
|
}
|
|
8954
10312
|
|
|
8955
10313
|
// src/run-init.ts
|
|
8956
|
-
import { readFile as
|
|
8957
|
-
import { join as
|
|
10314
|
+
import { readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
|
|
10315
|
+
import { join as join18 } from "path";
|
|
8958
10316
|
async function readIfPresent(path) {
|
|
8959
10317
|
try {
|
|
8960
|
-
return await
|
|
10318
|
+
return await readFile16(path, "utf8");
|
|
8961
10319
|
} catch {
|
|
8962
10320
|
return null;
|
|
8963
10321
|
}
|
|
@@ -8965,18 +10323,18 @@ async function readIfPresent(path) {
|
|
|
8965
10323
|
async function runInit(options, deps) {
|
|
8966
10324
|
const existingConfig = findConfigFile(options.cwd);
|
|
8967
10325
|
const plan = planInit({
|
|
8968
|
-
agents: await readIfPresent(
|
|
10326
|
+
agents: await readIfPresent(join18(options.cwd, "AGENTS.md")),
|
|
8969
10327
|
config: existingConfig === null ? null : "present",
|
|
8970
|
-
gitignore: await readIfPresent(
|
|
10328
|
+
gitignore: await readIfPresent(join18(options.cwd, ".gitignore")),
|
|
8971
10329
|
force: options.force
|
|
8972
10330
|
});
|
|
8973
10331
|
const touched = [];
|
|
8974
10332
|
for (const write2 of plan.writes) {
|
|
8975
|
-
await
|
|
10333
|
+
await writeFile11(join18(options.cwd, write2.path), write2.contents, "utf8");
|
|
8976
10334
|
touched.push(write2.path);
|
|
8977
10335
|
}
|
|
8978
10336
|
if (plan.gitignore !== null) {
|
|
8979
|
-
await
|
|
10337
|
+
await writeFile11(join18(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
8980
10338
|
touched.push(".gitignore");
|
|
8981
10339
|
}
|
|
8982
10340
|
if (options.json) {
|
|
@@ -8996,8 +10354,8 @@ async function runInit(options, deps) {
|
|
|
8996
10354
|
|
|
8997
10355
|
// src/run-keep.ts
|
|
8998
10356
|
import { existsSync as existsSync5 } from "fs";
|
|
8999
|
-
import { copyFile as copyFile2, mkdir as mkdir10, readFile as
|
|
9000
|
-
import { dirname as
|
|
10357
|
+
import { copyFile as copyFile2, mkdir as mkdir10, readFile as readFile17, rm as rm6, writeFile as writeFile12 } from "fs/promises";
|
|
10358
|
+
import { dirname as dirname12, isAbsolute as isAbsolute3, join as join19, relative as relative5 } from "path";
|
|
9001
10359
|
|
|
9002
10360
|
// src/resolve-title.ts
|
|
9003
10361
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -9030,15 +10388,15 @@ async function writeLogEntry(options) {
|
|
|
9030
10388
|
annotations: await readAnnotations(options.cwd),
|
|
9031
10389
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
9032
10390
|
});
|
|
9033
|
-
const dir =
|
|
10391
|
+
const dir = join19(options.cwd, options.logDir);
|
|
9034
10392
|
await mkdir10(dir, { recursive: true });
|
|
9035
|
-
const file =
|
|
9036
|
-
await
|
|
10393
|
+
const file = join19(dir, `${entry.slug}.md`);
|
|
10394
|
+
await writeFile12(file, entry.markdown, "utf8");
|
|
9037
10395
|
if (entry.pictures.length > 0) {
|
|
9038
|
-
const pictureDir =
|
|
10396
|
+
const pictureDir = join19(dir, entry.slug);
|
|
9039
10397
|
await mkdir10(pictureDir, { recursive: true });
|
|
9040
10398
|
for (const picture of entry.pictures) {
|
|
9041
|
-
await copyFile2(
|
|
10399
|
+
await copyFile2(join19(options.cwd, picture.from), join19(pictureDir, picture.to)).catch(() => {
|
|
9042
10400
|
});
|
|
9043
10401
|
}
|
|
9044
10402
|
}
|
|
@@ -9059,19 +10417,20 @@ async function runKeep(options, deps) {
|
|
|
9059
10417
|
await readRenames(options.cwd)
|
|
9060
10418
|
);
|
|
9061
10419
|
if (!resolved.ok) return fail(resolved.error);
|
|
9062
|
-
const
|
|
10420
|
+
const destination = isAbsolute3(options.to) ? relative5(options.cwd, options.to) : options.to;
|
|
10421
|
+
const plan = planKeep({ title: resolved.title, previews, to: destination });
|
|
9063
10422
|
if (!plan.ok) return fail(plan.error);
|
|
9064
|
-
const from =
|
|
9065
|
-
const to =
|
|
10423
|
+
const from = join19(options.cwd, plan.move.from);
|
|
10424
|
+
const to = join19(options.cwd, plan.move.to);
|
|
9066
10425
|
if (!existsSync5(from)) {
|
|
9067
10426
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
9068
10427
|
}
|
|
9069
10428
|
if (existsSync5(to)) {
|
|
9070
10429
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
9071
10430
|
}
|
|
9072
|
-
const source = await
|
|
9073
|
-
await mkdir10(
|
|
9074
|
-
await
|
|
10431
|
+
const source = await readFile17(from, "utf8");
|
|
10432
|
+
await mkdir10(dirname12(to), { recursive: true });
|
|
10433
|
+
await writeFile12(to, renameExport(source, plan.exportName), "utf8");
|
|
9075
10434
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
9076
10435
|
let logged = null;
|
|
9077
10436
|
let logError = null;
|
|
@@ -9086,7 +10445,7 @@ async function runKeep(options, deps) {
|
|
|
9086
10445
|
} catch (error) {
|
|
9087
10446
|
logError = error instanceof Error ? error.message : String(error);
|
|
9088
10447
|
}
|
|
9089
|
-
await rm6(
|
|
10448
|
+
await rm6(join19(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
9090
10449
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
9091
10450
|
if (options.json) {
|
|
9092
10451
|
deps.log(
|
|
@@ -9125,11 +10484,11 @@ async function runKeep(options, deps) {
|
|
|
9125
10484
|
|
|
9126
10485
|
// src/run-new.ts
|
|
9127
10486
|
import { existsSync as existsSync6 } from "fs";
|
|
9128
|
-
import { mkdir as mkdir11, readFile as
|
|
9129
|
-
import { dirname as
|
|
10487
|
+
import { mkdir as mkdir11, readFile as readFile18, writeFile as writeFile13 } from "fs/promises";
|
|
10488
|
+
import { dirname as dirname13, join as join20 } from "path";
|
|
9130
10489
|
async function readIfPresent2(path) {
|
|
9131
10490
|
try {
|
|
9132
|
-
return await
|
|
10491
|
+
return await readFile18(path, "utf8");
|
|
9133
10492
|
} catch {
|
|
9134
10493
|
return null;
|
|
9135
10494
|
}
|
|
@@ -9137,7 +10496,7 @@ async function readIfPresent2(path) {
|
|
|
9137
10496
|
async function runNew(options, deps) {
|
|
9138
10497
|
let from;
|
|
9139
10498
|
if (options.from !== void 0) {
|
|
9140
|
-
const contents = await readIfPresent2(
|
|
10499
|
+
const contents = await readIfPresent2(join20(options.cwd, options.from));
|
|
9141
10500
|
if (contents === null) {
|
|
9142
10501
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
9143
10502
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -9148,8 +10507,8 @@ async function runNew(options, deps) {
|
|
|
9148
10507
|
}
|
|
9149
10508
|
const plan = planNew({
|
|
9150
10509
|
surface: options.surface,
|
|
9151
|
-
packageJson: await readIfPresent2(
|
|
9152
|
-
gitignore: await readIfPresent2(
|
|
10510
|
+
packageJson: await readIfPresent2(join20(options.cwd, "package.json")),
|
|
10511
|
+
gitignore: await readIfPresent2(join20(options.cwd, ".gitignore")),
|
|
9153
10512
|
from
|
|
9154
10513
|
});
|
|
9155
10514
|
const fail = (error) => {
|
|
@@ -9179,7 +10538,7 @@ async function runNew(options, deps) {
|
|
|
9179
10538
|
deps.log(plan.instructions);
|
|
9180
10539
|
return { exitCode: 0, written: [] };
|
|
9181
10540
|
}
|
|
9182
|
-
const existing = plan.writes.filter((write2) => existsSync6(
|
|
10541
|
+
const existing = plan.writes.filter((write2) => existsSync6(join20(options.cwd, write2.path)));
|
|
9183
10542
|
if (existing.length > 0) {
|
|
9184
10543
|
return fail(
|
|
9185
10544
|
`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
|
|
@@ -9187,13 +10546,13 @@ async function runNew(options, deps) {
|
|
|
9187
10546
|
}
|
|
9188
10547
|
const written = [];
|
|
9189
10548
|
for (const write2 of plan.writes) {
|
|
9190
|
-
const target =
|
|
9191
|
-
await mkdir11(
|
|
9192
|
-
await
|
|
10549
|
+
const target = join20(options.cwd, write2.path);
|
|
10550
|
+
await mkdir11(dirname13(target), { recursive: true });
|
|
10551
|
+
await writeFile13(target, write2.contents, "utf8");
|
|
9193
10552
|
written.push(write2.path);
|
|
9194
10553
|
}
|
|
9195
10554
|
if (plan.gitignore !== null) {
|
|
9196
|
-
await
|
|
10555
|
+
await writeFile13(join20(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9197
10556
|
written.push(".gitignore");
|
|
9198
10557
|
}
|
|
9199
10558
|
if (options.json) {
|
|
@@ -9221,21 +10580,21 @@ async function runNew(options, deps) {
|
|
|
9221
10580
|
}
|
|
9222
10581
|
|
|
9223
10582
|
// src/run-previews.ts
|
|
9224
|
-
import { readFile as
|
|
9225
|
-
import { join as
|
|
10583
|
+
import { readFile as readFile19, writeFile as writeFile14 } from "fs/promises";
|
|
10584
|
+
import { join as join21 } from "path";
|
|
9226
10585
|
function envelope(deps, ok, body) {
|
|
9227
10586
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
9228
10587
|
}
|
|
9229
10588
|
async function ensureIgnored(cwd) {
|
|
9230
|
-
const path =
|
|
10589
|
+
const path = join21(cwd, ".gitignore");
|
|
9231
10590
|
let current = null;
|
|
9232
10591
|
try {
|
|
9233
|
-
current = await
|
|
10592
|
+
current = await readFile19(path, "utf8");
|
|
9234
10593
|
} catch {
|
|
9235
10594
|
current = null;
|
|
9236
10595
|
}
|
|
9237
10596
|
const next = ignoreEntry(current);
|
|
9238
|
-
if (next !== null) await
|
|
10597
|
+
if (next !== null) await writeFile14(path, next, "utf8");
|
|
9239
10598
|
}
|
|
9240
10599
|
async function runAdd(options, deps) {
|
|
9241
10600
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -9458,7 +10817,7 @@ async function findLeglas(cwd, port, request) {
|
|
|
9458
10817
|
signal: AbortSignal.timeout(2e3)
|
|
9459
10818
|
});
|
|
9460
10819
|
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
9461
|
-
const answered = await health.text().then((
|
|
10820
|
+
const answered = await health.text().then((text3) => JSON.parse(text3)).catch(() => ({}));
|
|
9462
10821
|
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
9463
10822
|
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
9464
10823
|
return {
|
|
@@ -9890,24 +11249,24 @@ function installShutdown(stop, target = process) {
|
|
|
9890
11249
|
|
|
9891
11250
|
// src/run-watch.ts
|
|
9892
11251
|
import { spawn as spawn4 } from "child_process";
|
|
9893
|
-
import { mkdir as mkdir12, readFile as
|
|
9894
|
-
import { dirname as
|
|
11252
|
+
import { mkdir as mkdir12, readFile as readFile20, writeFile as writeFile15 } from "fs/promises";
|
|
11253
|
+
import { dirname as dirname14, join as join22 } from "path";
|
|
9895
11254
|
var POLL_MS2 = 2e3;
|
|
9896
11255
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
9897
11256
|
async function saveTemplate(cwd, run4) {
|
|
9898
|
-
const path =
|
|
11257
|
+
const path = join22(cwd, WATCH_PATH);
|
|
9899
11258
|
let config = {};
|
|
9900
11259
|
try {
|
|
9901
|
-
const parsed = JSON.parse(await
|
|
11260
|
+
const parsed = JSON.parse(await readFile20(path, "utf8"));
|
|
9902
11261
|
if (isJsonObject(parsed)) config = parsed;
|
|
9903
11262
|
} catch {
|
|
9904
11263
|
}
|
|
9905
11264
|
config.run = run4;
|
|
9906
|
-
await mkdir12(
|
|
9907
|
-
await
|
|
11265
|
+
await mkdir12(dirname14(path), { recursive: true });
|
|
11266
|
+
await writeFile15(path, `${JSON.stringify(config, null, 2)}
|
|
9908
11267
|
`, "utf8");
|
|
9909
11268
|
}
|
|
9910
|
-
function spawnAgent(command, args, cwd) {
|
|
11269
|
+
function spawnAgent(command, args, cwd, json) {
|
|
9911
11270
|
return new Promise((resolve5) => {
|
|
9912
11271
|
let settled = false;
|
|
9913
11272
|
const settle = (outcome) => {
|
|
@@ -9915,7 +11274,10 @@ function spawnAgent(command, args, cwd) {
|
|
|
9915
11274
|
settled = true;
|
|
9916
11275
|
resolve5(outcome);
|
|
9917
11276
|
};
|
|
9918
|
-
const child = spawn4(command, args, {
|
|
11277
|
+
const child = spawn4(command, args, {
|
|
11278
|
+
cwd,
|
|
11279
|
+
stdio: json ? ["inherit", 2, "inherit"] : "inherit"
|
|
11280
|
+
});
|
|
9919
11281
|
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
9920
11282
|
child.on(
|
|
9921
11283
|
"close",
|
|
@@ -9926,6 +11288,13 @@ function spawnAgent(command, args, cwd) {
|
|
|
9926
11288
|
});
|
|
9927
11289
|
}
|
|
9928
11290
|
async function runWatch(options, deps) {
|
|
11291
|
+
const json = options.json === true;
|
|
11292
|
+
const emit = (event) => deps.log(JSON.stringify(event));
|
|
11293
|
+
const refuse2 = (error) => {
|
|
11294
|
+
if (json) deps.log(JSON.stringify({ ok: false, error }));
|
|
11295
|
+
else deps.error(error);
|
|
11296
|
+
return { exitCode: 1 };
|
|
11297
|
+
};
|
|
9929
11298
|
const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
|
|
9930
11299
|
const raw = options.run ?? saved.run;
|
|
9931
11300
|
let template;
|
|
@@ -9933,10 +11302,7 @@ async function runWatch(options, deps) {
|
|
|
9933
11302
|
let synthesizedAgent = null;
|
|
9934
11303
|
if (raw !== null) {
|
|
9935
11304
|
const parsed = parseTemplate(raw);
|
|
9936
|
-
if (!parsed.ok)
|
|
9937
|
-
deps.error(parsed.error);
|
|
9938
|
-
return { exitCode: 1 };
|
|
9939
|
-
}
|
|
11305
|
+
if (!parsed.ok) return refuse2(parsed.error);
|
|
9940
11306
|
template = parsed.template;
|
|
9941
11307
|
shownCommand2 = raw;
|
|
9942
11308
|
} else if (saved.agent !== null && saved.agent !== "custom") {
|
|
@@ -9948,10 +11314,9 @@ async function runWatch(options, deps) {
|
|
|
9948
11314
|
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
9949
11315
|
synthesizedAgent = adapter.name;
|
|
9950
11316
|
} else {
|
|
9951
|
-
|
|
11317
|
+
return refuse2(
|
|
9952
11318
|
'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
|
|
9953
11319
|
);
|
|
9954
|
-
return { exitCode: 1 };
|
|
9955
11320
|
}
|
|
9956
11321
|
if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
|
|
9957
11322
|
});
|
|
@@ -9967,26 +11332,36 @@ async function runWatch(options, deps) {
|
|
|
9967
11332
|
} catch {
|
|
9968
11333
|
}
|
|
9969
11334
|
};
|
|
9970
|
-
if (
|
|
9971
|
-
|
|
11335
|
+
if (json) {
|
|
11336
|
+
emit({ event: "watching", command: shownCommand2, agent: synthesizedAgent });
|
|
11337
|
+
} else {
|
|
11338
|
+
if (synthesizedAgent !== null) {
|
|
11339
|
+
deps.log(`Using ${synthesizedAgent}, chosen in the interface.`);
|
|
11340
|
+
}
|
|
11341
|
+
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
11342
|
+
deps.log("Stop with Ctrl-C.");
|
|
9972
11343
|
}
|
|
9973
|
-
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
9974
|
-
deps.log("Stop with Ctrl-C.");
|
|
9975
11344
|
const failed = /* @__PURE__ */ new Set();
|
|
9976
11345
|
let stopped = false;
|
|
9977
11346
|
let busy = false;
|
|
9978
11347
|
let announced = false;
|
|
9979
11348
|
let inflight = null;
|
|
9980
11349
|
const handle = async (request) => {
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
11350
|
+
const { id, title, intent, target } = request;
|
|
11351
|
+
if (json) {
|
|
11352
|
+
emit({ event: "started", id, title, intent, target });
|
|
11353
|
+
} else {
|
|
11354
|
+
deps.log("");
|
|
11355
|
+
deps.log(` ${title}: ${intent}`);
|
|
11356
|
+
if (target !== null) deps.log(` ${target}`);
|
|
11357
|
+
}
|
|
9984
11358
|
await markPickedUp(options.cwd, request.id);
|
|
9985
11359
|
const { command, args } = commandFor(template, request.prompt);
|
|
9986
|
-
const outcome = await spawnAgent(command, args, options.cwd);
|
|
11360
|
+
const outcome = await spawnAgent(command, args, options.cwd, json);
|
|
9987
11361
|
if (outcome.ok && outcome.code === 0) {
|
|
9988
11362
|
await removeRequest(options.cwd, request.id);
|
|
9989
|
-
|
|
11363
|
+
if (json) emit({ event: "done", id, title });
|
|
11364
|
+
else deps.log(` done ${title}`);
|
|
9990
11365
|
return;
|
|
9991
11366
|
}
|
|
9992
11367
|
failed.add(request.id);
|
|
@@ -9996,8 +11371,12 @@ async function runWatch(options, deps) {
|
|
|
9996
11371
|
exitCode: outcome.ok ? outcome.code : null
|
|
9997
11372
|
});
|
|
9998
11373
|
await markFailed(options.cwd, request.id, failure);
|
|
9999
|
-
|
|
10000
|
-
|
|
11374
|
+
if (json) {
|
|
11375
|
+
emit({ event: "failed", id, title, code: failure.code, reason: failure.message });
|
|
11376
|
+
} else {
|
|
11377
|
+
deps.error(` failed ${title}: ${failure.message}`);
|
|
11378
|
+
deps.error(" Left in the queue and not retried.");
|
|
11379
|
+
}
|
|
10001
11380
|
};
|
|
10002
11381
|
const tick = async () => {
|
|
10003
11382
|
if (stopped) return;
|
|
@@ -10016,7 +11395,9 @@ async function runWatch(options, deps) {
|
|
|
10016
11395
|
await inflight;
|
|
10017
11396
|
}
|
|
10018
11397
|
} catch (error) {
|
|
10019
|
-
|
|
11398
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
11399
|
+
if (json) emit({ event: "error", message: message2 });
|
|
11400
|
+
else deps.error(` ! ${message2}`);
|
|
10020
11401
|
} finally {
|
|
10021
11402
|
inflight = null;
|
|
10022
11403
|
busy = false;
|
|
@@ -10031,7 +11412,10 @@ async function runWatch(options, deps) {
|
|
|
10031
11412
|
process.off("SIGINT", stop);
|
|
10032
11413
|
process.off("SIGTERM", stop);
|
|
10033
11414
|
void Promise.resolve(inflight).catch(() => {
|
|
10034
|
-
}).then(() => heartbeat(false)).then(() =>
|
|
11415
|
+
}).then(() => heartbeat(false)).then(() => {
|
|
11416
|
+
if (json) emit({ event: "stopped" });
|
|
11417
|
+
resolve5({ exitCode: 0 });
|
|
11418
|
+
});
|
|
10035
11419
|
};
|
|
10036
11420
|
process.on("SIGINT", stop);
|
|
10037
11421
|
process.on("SIGTERM", stop);
|
|
@@ -10042,13 +11426,13 @@ async function runWatch(options, deps) {
|
|
|
10042
11426
|
}
|
|
10043
11427
|
|
|
10044
11428
|
// src/run-classify.ts
|
|
10045
|
-
import { stat as
|
|
10046
|
-
import { join as
|
|
11429
|
+
import { stat as stat5 } from "fs/promises";
|
|
11430
|
+
import { join as join23 } from "path";
|
|
10047
11431
|
async function runClassify(options, deps) {
|
|
10048
11432
|
const declared = await Promise.all(
|
|
10049
11433
|
options.changes.map(async (change) => ({
|
|
10050
11434
|
...change,
|
|
10051
|
-
exists: await
|
|
11435
|
+
exists: await stat5(join23(options.cwd, change.path)).then(
|
|
10052
11436
|
() => true,
|
|
10053
11437
|
() => false
|
|
10054
11438
|
)
|
|
@@ -10077,13 +11461,13 @@ async function runClassify(options, deps) {
|
|
|
10077
11461
|
import { existsSync as existsSync7 } from "fs";
|
|
10078
11462
|
import { realpath as realpath4 } from "fs/promises";
|
|
10079
11463
|
import { createRequire } from "module";
|
|
10080
|
-
import { basename as basename6, dirname as
|
|
11464
|
+
import { basename as basename6, dirname as dirname15, join as join24, relative as relative7, resolve as resolve4 } from "path";
|
|
10081
11465
|
import { fileURLToPath } from "url";
|
|
10082
11466
|
|
|
10083
11467
|
// src/dev-server-owner.ts
|
|
10084
11468
|
import { execFile as execFile2 } from "child_process";
|
|
10085
11469
|
import { realpath as realpath3 } from "fs/promises";
|
|
10086
|
-
import { basename as basename5, isAbsolute as
|
|
11470
|
+
import { basename as basename5, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve3 } from "path";
|
|
10087
11471
|
import { promisify as promisify2 } from "util";
|
|
10088
11472
|
var run2 = promisify2(execFile2);
|
|
10089
11473
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -10152,8 +11536,8 @@ async function inspectLocalDevServer(origin) {
|
|
|
10152
11536
|
}
|
|
10153
11537
|
}
|
|
10154
11538
|
function isInside(parent, child) {
|
|
10155
|
-
const path =
|
|
10156
|
-
return path === "" || !path.startsWith("..") && !
|
|
11539
|
+
const path = relative6(resolve3(parent), resolve3(child));
|
|
11540
|
+
return path === "" || !path.startsWith("..") && !isAbsolute4(path);
|
|
10157
11541
|
}
|
|
10158
11542
|
function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
10159
11543
|
const port = localDevServerPort(origin);
|
|
@@ -10166,11 +11550,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
10166
11550
|
|
|
10167
11551
|
// src/run.ts
|
|
10168
11552
|
function findShellDir() {
|
|
10169
|
-
const bundled =
|
|
10170
|
-
if (existsSync7(
|
|
11553
|
+
const bundled = join24(dirname15(fileURLToPath(import.meta.url)), "shell");
|
|
11554
|
+
if (existsSync7(join24(bundled, "index.html"))) return bundled;
|
|
10171
11555
|
try {
|
|
10172
11556
|
const require2 = createRequire(import.meta.url);
|
|
10173
|
-
return
|
|
11557
|
+
return dirname15(require2.resolve("@leglas/shell/dist/index.html"));
|
|
10174
11558
|
} catch {
|
|
10175
11559
|
return null;
|
|
10176
11560
|
}
|
|
@@ -10181,7 +11565,7 @@ function shellWord(value) {
|
|
|
10181
11565
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
10182
11566
|
}
|
|
10183
11567
|
function embeddedLeglasCommand() {
|
|
10184
|
-
const entry =
|
|
11568
|
+
const entry = join24(dirname15(fileURLToPath(import.meta.url)), "bin.js");
|
|
10185
11569
|
if (!existsSync7(entry)) return "npx -y leglas";
|
|
10186
11570
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
10187
11571
|
}
|
|
@@ -10220,7 +11604,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10220
11604
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
10221
11605
|
for (const preview of merged?.previews ?? []) {
|
|
10222
11606
|
if (preview.file !== void 0) {
|
|
10223
|
-
const absolute =
|
|
11607
|
+
const absolute = join24(options.cwd, preview.file);
|
|
10224
11608
|
if (!existsSync7(absolute)) {
|
|
10225
11609
|
previewErrors.push(
|
|
10226
11610
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -10231,7 +11615,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10231
11615
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
10232
11616
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
10233
11617
|
}
|
|
10234
|
-
fileMounts.set(slug,
|
|
11618
|
+
fileMounts.set(slug, dirname15(absolute));
|
|
10235
11619
|
previews.push({
|
|
10236
11620
|
...preview,
|
|
10237
11621
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
@@ -10243,8 +11627,8 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10243
11627
|
const config = merged === null ? null : { ...merged, previews };
|
|
10244
11628
|
const configWarnings = [];
|
|
10245
11629
|
const projectRoot = await realpath4(
|
|
10246
|
-
loaded.path === null ? options.cwd :
|
|
10247
|
-
).catch(() => resolve4(loaded.path === null ? options.cwd :
|
|
11630
|
+
loaded.path === null ? options.cwd : dirname15(loaded.path)
|
|
11631
|
+
).catch(() => resolve4(loaded.path === null ? options.cwd : dirname15(loaded.path)));
|
|
10248
11632
|
const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
10249
11633
|
const serverOptions = {
|
|
10250
11634
|
config,
|
|
@@ -10282,7 +11666,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10282
11666
|
})
|
|
10283
11667
|
);
|
|
10284
11668
|
} else {
|
|
10285
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
11669
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative7(options.cwd, loaded.path) || loaded.path;
|
|
10286
11670
|
deps.log(`Leglas ${url}`);
|
|
10287
11671
|
deps.log(
|
|
10288
11672
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|