leglas 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.d.ts +5 -0
- package/dist/bin-start.d.ts +5 -0
- package/dist/bin.js +1834 -372
- package/dist/index.js +1482 -162
- package/dist/run-explore-build.d.ts +27 -0
- package/dist/run-watch.d.ts +1 -0
- package/dist/shell/assets/index-C59b2VHc.css +1 -0
- package/dist/shell/assets/index-CcHf_UOb.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +3 -3
- package/dist/shell/assets/index-DfiRUTCS.css +0 -1
- package/dist/shell/assets/index-lVZbvEvf.js +0 -14
package/dist/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);
|
|
@@ -666,7 +709,7 @@ function placeholder(label, note) {
|
|
|
666
709
|
return `export function ${label}() {
|
|
667
710
|
return (
|
|
668
711
|
<div style={{ padding: "3rem", fontFamily: "system-ui" }}>
|
|
669
|
-
<p
|
|
712
|
+
<p>{${JSON.stringify(note)}}</p>
|
|
670
713
|
</div>
|
|
671
714
|
);
|
|
672
715
|
}
|
|
@@ -872,7 +915,8 @@ Useful to know:
|
|
|
872
915
|
they have asked for that is not done yet. Add \`--screenshot\` to render a PNG
|
|
873
916
|
you can inspect. Run it when handed a direction you did not register yourself.
|
|
874
917
|
- Every command accepts \`--json\` and prints one envelope with a stable exit
|
|
875
|
-
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.
|
|
876
920
|
|
|
877
921
|
${AGENTS_MARKER_END}
|
|
878
922
|
`;
|
|
@@ -912,7 +956,7 @@ ${AGENTS_SECTION}`
|
|
|
912
956
|
}
|
|
913
957
|
|
|
914
958
|
// src/keep.ts
|
|
915
|
-
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";
|
|
916
960
|
|
|
917
961
|
// ../server/dist/log.js
|
|
918
962
|
var DEFAULT_LOG_DIR = "design-log";
|
|
@@ -993,8 +1037,8 @@ function composeEntry(input) {
|
|
|
993
1037
|
}
|
|
994
1038
|
|
|
995
1039
|
// ../server/dist/json.js
|
|
996
|
-
function parseJson(
|
|
997
|
-
return JSON.parse(
|
|
1040
|
+
function parseJson(text3) {
|
|
1041
|
+
return JSON.parse(text3);
|
|
998
1042
|
}
|
|
999
1043
|
function isJsonRecord(value) {
|
|
1000
1044
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1314,7 +1358,7 @@ var KNOWN_AGENTS = {
|
|
|
1314
1358
|
// moves that precondition and only that: `-s workspace-write` still
|
|
1315
1359
|
// confines writes to the project, so the sandbox boundary is unchanged,
|
|
1316
1360
|
// and in a git repository the flag does nothing at all.
|
|
1317
|
-
args: (prompt, effort = null,
|
|
1361
|
+
args: (prompt, effort = null, images2 = []) => [
|
|
1318
1362
|
"exec",
|
|
1319
1363
|
"--json",
|
|
1320
1364
|
...CODEX_WORKSPACE_CONFIG,
|
|
@@ -1322,23 +1366,23 @@ var KNOWN_AGENTS = {
|
|
|
1322
1366
|
"-s",
|
|
1323
1367
|
"workspace-write",
|
|
1324
1368
|
"--skip-git-repo-check",
|
|
1325
|
-
...
|
|
1369
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
1326
1370
|
prompt
|
|
1327
1371
|
],
|
|
1328
|
-
terminalArgs: (prompt, effort = null,
|
|
1372
|
+
terminalArgs: (prompt, effort = null, images2 = []) => [
|
|
1329
1373
|
"exec",
|
|
1330
1374
|
...CODEX_WORKSPACE_CONFIG,
|
|
1331
1375
|
...codexEffortConfig(effort),
|
|
1332
1376
|
"-s",
|
|
1333
1377
|
"workspace-write",
|
|
1334
1378
|
"--skip-git-repo-check",
|
|
1335
|
-
...
|
|
1379
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
1336
1380
|
prompt
|
|
1337
1381
|
],
|
|
1338
1382
|
// No sandbox flag here: `codex exec resume` refuses it and inherits the
|
|
1339
1383
|
// session's own sandbox, which the first turn set to workspace-write. The
|
|
1340
1384
|
// repository check is per invocation, so resume needs the flag of its own.
|
|
1341
|
-
resumeArgs: (sessionId, prompt, effort = null,
|
|
1385
|
+
resumeArgs: (sessionId, prompt, effort = null, images2 = []) => [
|
|
1342
1386
|
"exec",
|
|
1343
1387
|
"resume",
|
|
1344
1388
|
sessionId,
|
|
@@ -1346,7 +1390,7 @@ var KNOWN_AGENTS = {
|
|
|
1346
1390
|
...CODEX_WORKSPACE_CONFIG,
|
|
1347
1391
|
...codexEffortConfig(effort),
|
|
1348
1392
|
"--skip-git-repo-check",
|
|
1349
|
-
...
|
|
1393
|
+
...images2.flatMap((image) => ["-i", image]),
|
|
1350
1394
|
prompt
|
|
1351
1395
|
],
|
|
1352
1396
|
sessionFrom: (event) => event.type === "thread.started" && isString(event.thread_id) ? event.thread_id : null,
|
|
@@ -2181,12 +2225,12 @@ function buildNumber(entry) {
|
|
|
2181
2225
|
const digits = /(\d+)\s*$/.exec(entry)?.[1];
|
|
2182
2226
|
return digits === void 0 ? 0 : Number(digits);
|
|
2183
2227
|
}
|
|
2184
|
-
function cacheRoots(platform, home,
|
|
2228
|
+
function cacheRoots(platform, home, readdir6 = readableDirectories) {
|
|
2185
2229
|
const playwright = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
2186
2230
|
const puppeteer = join4(home, ".cache", "puppeteer");
|
|
2187
2231
|
const newestFirst = (dir, keep = () => true) => ({
|
|
2188
2232
|
root: dir,
|
|
2189
|
-
entries:
|
|
2233
|
+
entries: readdir6(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
|
|
2190
2234
|
});
|
|
2191
2235
|
return [
|
|
2192
2236
|
newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
|
|
@@ -2207,13 +2251,13 @@ function findBrowser(search = {}) {
|
|
|
2207
2251
|
const home = search.home ?? homedir();
|
|
2208
2252
|
const exists = search.exists ?? existsSync2;
|
|
2209
2253
|
const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
|
|
2210
|
-
const
|
|
2254
|
+
const readdir6 = search.readdir ?? readableDirectories;
|
|
2211
2255
|
const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
|
|
2212
2256
|
for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
|
|
2213
2257
|
if (isString(candidate) && candidate !== "" && exists(candidate))
|
|
2214
2258
|
return candidate;
|
|
2215
2259
|
}
|
|
2216
|
-
const caches = cacheRoots(platform, home,
|
|
2260
|
+
const caches = cacheRoots(platform, home, readdir6);
|
|
2217
2261
|
const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join4(root, entry, ...rest)))));
|
|
2218
2262
|
if (shell !== null)
|
|
2219
2263
|
return shell;
|
|
@@ -2243,7 +2287,7 @@ function findBrowser(search = {}) {
|
|
|
2243
2287
|
}
|
|
2244
2288
|
if (platform === "darwin" || platform === "linux") {
|
|
2245
2289
|
const playwrightRoot = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
|
|
2246
|
-
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) => {
|
|
2247
2291
|
const root = join4(playwrightRoot, entry);
|
|
2248
2292
|
return [
|
|
2249
2293
|
...FOR_TESTING.map((rest) => join4(root, ...rest)),
|
|
@@ -2260,7 +2304,7 @@ function findBrowser(search = {}) {
|
|
|
2260
2304
|
const puppeteerCache = join4(home, ".cache", "puppeteer");
|
|
2261
2305
|
for (const kind of ["chrome", "chrome-headless-shell"]) {
|
|
2262
2306
|
const kindRoot = join4(puppeteerCache, kind);
|
|
2263
|
-
const found = firstExisting(
|
|
2307
|
+
const found = firstExisting(readdir6(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
|
|
2264
2308
|
const root = join4(kindRoot, entry);
|
|
2265
2309
|
return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join4(root, ...rest));
|
|
2266
2310
|
}));
|
|
@@ -2300,7 +2344,7 @@ async function connectWebSocket(url) {
|
|
|
2300
2344
|
socket.addEventListener("error", failed, { once: true });
|
|
2301
2345
|
});
|
|
2302
2346
|
return {
|
|
2303
|
-
send: (
|
|
2347
|
+
send: (text3) => socket.send(text3),
|
|
2304
2348
|
onMessage: (listener) => socket.addEventListener("message", (event) => listener(String(event.data))),
|
|
2305
2349
|
onClose: (listener) => socket.addEventListener("close", listener),
|
|
2306
2350
|
close: () => socket.close()
|
|
@@ -2525,10 +2569,10 @@ async function launchBrowser(executable, options = {}) {
|
|
|
2525
2569
|
socketClosed = true;
|
|
2526
2570
|
rejectPending();
|
|
2527
2571
|
});
|
|
2528
|
-
socket.onMessage((
|
|
2572
|
+
socket.onMessage((text3) => {
|
|
2529
2573
|
let message2;
|
|
2530
2574
|
try {
|
|
2531
|
-
const parsed = parseJson(
|
|
2575
|
+
const parsed = parseJson(text3);
|
|
2532
2576
|
if (!isJsonRecord(parsed))
|
|
2533
2577
|
return;
|
|
2534
2578
|
message2 = parsed;
|
|
@@ -2893,6 +2937,16 @@ function validBox(value) {
|
|
|
2893
2937
|
return false;
|
|
2894
2938
|
return [value.x, value.y, value.width, value.height].every((entry) => isNumber(entry) && Number.isFinite(entry));
|
|
2895
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
|
+
}
|
|
2896
2950
|
function locatorExpression(focus) {
|
|
2897
2951
|
return `${LOCATOR}(${JSON.stringify(focus.selector)}, ${JSON.stringify(focus.text)}, ${JSON.stringify(focus.tag)})`;
|
|
2898
2952
|
}
|
|
@@ -2961,7 +3015,7 @@ async function render(page, input) {
|
|
|
2961
3015
|
}),
|
|
2962
3016
|
page.on("Log.entryAdded", (params) => {
|
|
2963
3017
|
if (params?.entry?.level === "error")
|
|
2964
|
-
remember(params.entry.text);
|
|
3018
|
+
remember(resourced(params.entry.text, params.entry.url));
|
|
2965
3019
|
})
|
|
2966
3020
|
];
|
|
2967
3021
|
try {
|
|
@@ -3384,8 +3438,8 @@ async function rehomeCaptures(cwd, from, to, attachments) {
|
|
|
3384
3438
|
file: attachment.file.startsWith(prefix) ? `${CAPTURES_DIR}/${to}/${attachment.file.slice(prefix.length)}` : attachment.file
|
|
3385
3439
|
}));
|
|
3386
3440
|
}
|
|
3387
|
-
function rehomeText(
|
|
3388
|
-
return
|
|
3441
|
+
function rehomeText(text3, from, to) {
|
|
3442
|
+
return text3.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
|
|
3389
3443
|
}
|
|
3390
3444
|
async function ownCapturesRoot(cwd) {
|
|
3391
3445
|
const root = join5(cwd, CAPTURES_DIR);
|
|
@@ -4253,13 +4307,13 @@ function imageMediaType(path) {
|
|
|
4253
4307
|
return "image/gif";
|
|
4254
4308
|
return null;
|
|
4255
4309
|
}
|
|
4256
|
-
async function claudeContent(prompt,
|
|
4257
|
-
if (
|
|
4310
|
+
async function claudeContent(prompt, images2) {
|
|
4311
|
+
if (images2.length === 0)
|
|
4258
4312
|
return prompt;
|
|
4259
4313
|
const blocks = [
|
|
4260
4314
|
{ type: "text", text: prompt }
|
|
4261
4315
|
];
|
|
4262
|
-
for (const path of
|
|
4316
|
+
for (const path of images2) {
|
|
4263
4317
|
const mediaType = imageMediaType(path);
|
|
4264
4318
|
if (mediaType === null)
|
|
4265
4319
|
continue;
|
|
@@ -5162,7 +5216,7 @@ function isStateUpdater(value) {
|
|
|
5162
5216
|
return typeof value === "function";
|
|
5163
5217
|
}
|
|
5164
5218
|
var SESSION_TURNS_CAP = 8;
|
|
5165
|
-
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
5219
|
+
function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [], images2 = []) {
|
|
5166
5220
|
if (choice.agent === null)
|
|
5167
5221
|
return null;
|
|
5168
5222
|
if (choice.agent === "custom") {
|
|
@@ -5189,24 +5243,24 @@ function resolveCommand(choice, prompt, sessionId = null, allowedCommands = [],
|
|
|
5189
5243
|
agent: choice.agent,
|
|
5190
5244
|
name: adapter.name,
|
|
5191
5245
|
command: adapter.binary,
|
|
5192
|
-
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort,
|
|
5246
|
+
args: [...adapter.resumeArgs(sessionId, prompt, choice.effort, images2), ...allow],
|
|
5193
5247
|
prompt,
|
|
5194
5248
|
effort: choice.effort,
|
|
5195
5249
|
sessionId,
|
|
5196
5250
|
resumed: true,
|
|
5197
|
-
images
|
|
5251
|
+
images: images2
|
|
5198
5252
|
};
|
|
5199
5253
|
}
|
|
5200
5254
|
return {
|
|
5201
5255
|
agent: choice.agent,
|
|
5202
5256
|
name: adapter.name,
|
|
5203
5257
|
command: adapter.binary,
|
|
5204
|
-
args: [...adapter.args(prompt, choice.effort,
|
|
5258
|
+
args: [...adapter.args(prompt, choice.effort, images2), ...allow],
|
|
5205
5259
|
prompt,
|
|
5206
5260
|
effort: choice.effort,
|
|
5207
5261
|
sessionId: null,
|
|
5208
5262
|
resumed: false,
|
|
5209
|
-
images
|
|
5263
|
+
images: images2
|
|
5210
5264
|
};
|
|
5211
5265
|
}
|
|
5212
5266
|
function lineReader(stream, onLine) {
|
|
@@ -5492,8 +5546,8 @@ function startRunner(options) {
|
|
|
5492
5546
|
`${options.leglasCommand} show`,
|
|
5493
5547
|
...request.mode === "variant" ? [registrationCommand(options.leglasCommand)] : []
|
|
5494
5548
|
];
|
|
5495
|
-
const
|
|
5496
|
-
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);
|
|
5497
5551
|
if (resolved === null)
|
|
5498
5552
|
return;
|
|
5499
5553
|
const lines2 = [];
|
|
@@ -5539,7 +5593,7 @@ function startRunner(options) {
|
|
|
5539
5593
|
// nothing says "cancelled". Stopped still means stopped.
|
|
5540
5594
|
!stopped) {
|
|
5541
5595
|
sessions.delete(resolved.agent);
|
|
5542
|
-
const cold = resolveCommand(choice, request.prompt, null, allowedCommands,
|
|
5596
|
+
const cold = resolveCommand(choice, request.prompt, null, allowedCommands, images2);
|
|
5543
5597
|
if (cold !== null) {
|
|
5544
5598
|
resolved = cold;
|
|
5545
5599
|
observed.sessionId = null;
|
|
@@ -7233,10 +7287,10 @@ function resolveTitle(input, titles, renames) {
|
|
|
7233
7287
|
// ../server/dist/server.js
|
|
7234
7288
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
7235
7289
|
import { createHash as createHash2 } from "crypto";
|
|
7236
|
-
import { mkdir as mkdir8, readdir as
|
|
7290
|
+
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile9 } from "fs/promises";
|
|
7237
7291
|
import http4 from "http";
|
|
7238
7292
|
import net3 from "net";
|
|
7239
|
-
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";
|
|
7240
7294
|
|
|
7241
7295
|
// ../server/dist/frame-policy.js
|
|
7242
7296
|
var FRAMING_TIMEOUT_MS = 4e3;
|
|
@@ -7347,13 +7401,1148 @@ async function checkFraming(target, embedder, fetcher = fetch) {
|
|
|
7347
7401
|
}
|
|
7348
7402
|
}
|
|
7349
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
|
+
|
|
7350
8539
|
// ../server/dist/server-info.js
|
|
7351
|
-
import { lstat as lstat2, mkdir as mkdir7, readFile as
|
|
7352
|
-
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";
|
|
7353
8542
|
var SERVER_INFO_PATH = ".leglas/server.json";
|
|
7354
8543
|
async function writeServerInfo(cwd, info) {
|
|
7355
|
-
const path =
|
|
7356
|
-
const directory =
|
|
8544
|
+
const path = join15(cwd, SERVER_INFO_PATH);
|
|
8545
|
+
const directory = dirname9(path);
|
|
7357
8546
|
await mkdir7(directory, { recursive: true });
|
|
7358
8547
|
if (!(await lstat2(directory)).isDirectory())
|
|
7359
8548
|
return;
|
|
@@ -7361,7 +8550,7 @@ async function writeServerInfo(cwd, info) {
|
|
|
7361
8550
|
if (existing !== null && !existing.isFile())
|
|
7362
8551
|
return;
|
|
7363
8552
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
7364
|
-
await
|
|
8553
|
+
await writeFile8(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
7365
8554
|
`, "utf8");
|
|
7366
8555
|
await rename2(temporary, path).catch(async (cause) => {
|
|
7367
8556
|
await rm4(temporary, { force: true }).catch(() => {
|
|
@@ -7371,7 +8560,7 @@ async function writeServerInfo(cwd, info) {
|
|
|
7371
8560
|
}
|
|
7372
8561
|
async function readServerInfo(cwd) {
|
|
7373
8562
|
try {
|
|
7374
|
-
const value = parseJson(await
|
|
8563
|
+
const value = parseJson(await readFile15(join15(cwd, SERVER_INFO_PATH), "utf8"));
|
|
7375
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))
|
|
7376
8565
|
return null;
|
|
7377
8566
|
return { port: value.port, url: value.url, pid: value.pid };
|
|
@@ -7385,7 +8574,7 @@ async function removeServerInfo(cwd, expected) {
|
|
|
7385
8574
|
if (current !== null && (current.port !== expected.port || current.pid !== expected.pid))
|
|
7386
8575
|
return;
|
|
7387
8576
|
}
|
|
7388
|
-
await rm4(
|
|
8577
|
+
await rm4(join15(cwd, SERVER_INFO_PATH), { force: true });
|
|
7389
8578
|
}
|
|
7390
8579
|
|
|
7391
8580
|
// ../server/dist/server.js
|
|
@@ -7546,8 +8735,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
7546
8735
|
});
|
|
7547
8736
|
}
|
|
7548
8737
|
function serveFrom(res, dir, relativePath) {
|
|
7549
|
-
const
|
|
7550
|
-
const candidate =
|
|
8738
|
+
const relative8 = normalize2(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
8739
|
+
const candidate = join16(dir, relative8);
|
|
7551
8740
|
if (!candidate.startsWith(dir))
|
|
7552
8741
|
return false;
|
|
7553
8742
|
if (!existsSync3(candidate) || !statSync(candidate).isFile())
|
|
@@ -7561,25 +8750,25 @@ function serveFrom(res, dir, relativePath) {
|
|
|
7561
8750
|
return true;
|
|
7562
8751
|
}
|
|
7563
8752
|
function serveShellFile(res, shellDir, urlPath) {
|
|
7564
|
-
const
|
|
7565
|
-
const isRoot =
|
|
7566
|
-
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);
|
|
7567
8756
|
}
|
|
7568
8757
|
var HEALTH_PROBE_MS = 3e3;
|
|
7569
8758
|
function fileStamp(path) {
|
|
7570
8759
|
try {
|
|
7571
|
-
const
|
|
7572
|
-
return `${
|
|
8760
|
+
const stat6 = statSync(path);
|
|
8761
|
+
return `${stat6.dev}:${stat6.ino}:${stat6.size}:${stat6.mtimeMs}:${stat6.ctimeMs}`;
|
|
7573
8762
|
} catch {
|
|
7574
8763
|
return null;
|
|
7575
8764
|
}
|
|
7576
8765
|
}
|
|
7577
8766
|
function watchLiveFiles(cwd, configPath, live) {
|
|
7578
|
-
const leglasDir =
|
|
8767
|
+
const leglasDir = join16(cwd, ".leglas");
|
|
7579
8768
|
const targets = [
|
|
7580
|
-
{ path:
|
|
7581
|
-
{ path:
|
|
7582
|
-
{ 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" }
|
|
7583
8772
|
];
|
|
7584
8773
|
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
7585
8774
|
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
@@ -7693,7 +8882,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7693
8882
|
}
|
|
7694
8883
|
if (configPath !== null) {
|
|
7695
8884
|
try {
|
|
7696
|
-
const directory =
|
|
8885
|
+
const directory = dirname10(configPath);
|
|
7697
8886
|
const name = basename3(configPath);
|
|
7698
8887
|
const target = { path: configPath, change: "config" };
|
|
7699
8888
|
known.set(configPath, fileStamp(configPath));
|
|
@@ -7731,7 +8920,7 @@ function watchLiveFiles(cwd, configPath, live) {
|
|
|
7731
8920
|
}
|
|
7732
8921
|
};
|
|
7733
8922
|
}
|
|
7734
|
-
function watchHealth(target, live) {
|
|
8923
|
+
function watchHealth(target, live, intervalMs) {
|
|
7735
8924
|
let previous = null;
|
|
7736
8925
|
let probing = false;
|
|
7737
8926
|
let closed = false;
|
|
@@ -7754,7 +8943,7 @@ function watchHealth(target, live) {
|
|
|
7754
8943
|
}).finally(() => {
|
|
7755
8944
|
probing = false;
|
|
7756
8945
|
});
|
|
7757
|
-
},
|
|
8946
|
+
}, intervalMs);
|
|
7758
8947
|
timer.unref?.();
|
|
7759
8948
|
return {
|
|
7760
8949
|
close: () => {
|
|
@@ -7778,15 +8967,15 @@ function configStalenessNotice(cwd, boot, current) {
|
|
|
7778
8967
|
if (boot === null && current === null)
|
|
7779
8968
|
return null;
|
|
7780
8969
|
if (boot === null && current !== null) {
|
|
7781
|
-
const label =
|
|
8970
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7782
8971
|
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
7783
8972
|
}
|
|
7784
8973
|
if (boot !== null && current === null) {
|
|
7785
|
-
const label =
|
|
8974
|
+
const label = relative4(cwd, boot.path) || boot.path;
|
|
7786
8975
|
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
7787
8976
|
}
|
|
7788
8977
|
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
7789
|
-
const label =
|
|
8978
|
+
const label = relative4(cwd, current.path) || current.path;
|
|
7790
8979
|
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
7791
8980
|
}
|
|
7792
8981
|
return null;
|
|
@@ -7832,7 +9021,7 @@ async function bind2(server, requested) {
|
|
|
7832
9021
|
throw new Error(`No free port between ${requested} and ${requested + PORT_ATTEMPTS - 1}.`);
|
|
7833
9022
|
}
|
|
7834
9023
|
async function startServer(options) {
|
|
7835
|
-
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;
|
|
7836
9025
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7837
9026
|
let shares = null;
|
|
7838
9027
|
let liveHealth = null;
|
|
@@ -7877,6 +9066,7 @@ async function startServer(options) {
|
|
|
7877
9066
|
let lastSeen = null;
|
|
7878
9067
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
7879
9068
|
let runner = null;
|
|
9069
|
+
let generations = null;
|
|
7880
9070
|
let agentsCache = null;
|
|
7881
9071
|
let agentsInflight = null;
|
|
7882
9072
|
const AGENTS_FRESH_MS = 3e4;
|
|
@@ -7927,11 +9117,36 @@ async function startServer(options) {
|
|
|
7927
9117
|
});
|
|
7928
9118
|
});
|
|
7929
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
|
+
};
|
|
7930
9145
|
const handleRequest = (req, res, context) => {
|
|
7931
9146
|
const url = req.url ?? "/";
|
|
7932
9147
|
const path = url.split("?")[0] ?? "/";
|
|
7933
9148
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
7934
|
-
if (!context.remote && req.method
|
|
9149
|
+
if (!context.remote && req.method !== "GET" && req.method !== "HEAD" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7935
9150
|
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7936
9151
|
}
|
|
7937
9152
|
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
@@ -7949,6 +9164,48 @@ async function startServer(options) {
|
|
|
7949
9164
|
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7950
9165
|
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7951
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
|
+
}
|
|
7952
9209
|
if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
|
|
7953
9210
|
const updates = options.updates;
|
|
7954
9211
|
if (updates === void 0) {
|
|
@@ -8212,8 +9469,8 @@ async function startServer(options) {
|
|
|
8212
9469
|
const id = newRequestId();
|
|
8213
9470
|
const file = `${REFERENCES_DIR}/${id}.${image.kind}`;
|
|
8214
9471
|
try {
|
|
8215
|
-
await mkdir8(
|
|
8216
|
-
await
|
|
9472
|
+
await mkdir8(join16(cwd, REFERENCES_DIR), { recursive: true });
|
|
9473
|
+
await writeFile9(join16(cwd, file), body);
|
|
8217
9474
|
void pruneReferences(cwd).catch(() => {
|
|
8218
9475
|
});
|
|
8219
9476
|
return sendJson2(res, 200, {
|
|
@@ -8255,7 +9512,7 @@ async function startServer(options) {
|
|
|
8255
9512
|
}
|
|
8256
9513
|
const references = parsed.references ?? [];
|
|
8257
9514
|
if (references.length > 0) {
|
|
8258
|
-
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("."))));
|
|
8259
9516
|
const gone = references.filter((id2) => !present.has(id2));
|
|
8260
9517
|
if (gone.length > 0) {
|
|
8261
9518
|
return sendJson2(res, 410, {
|
|
@@ -8385,7 +9642,7 @@ async function startServer(options) {
|
|
|
8385
9642
|
const timer = setTimeout(() => {
|
|
8386
9643
|
timedOut();
|
|
8387
9644
|
controller.abort();
|
|
8388
|
-
},
|
|
9645
|
+
}, captureDeadlineMs);
|
|
8389
9646
|
timer.unref?.();
|
|
8390
9647
|
try {
|
|
8391
9648
|
const captureInput = {
|
|
@@ -8418,8 +9675,8 @@ async function startServer(options) {
|
|
|
8418
9675
|
const noteSuffix = isString(parsed.note) ? `-${parsed.note.replace(/[^A-Za-z0-9_-]+/g, "-")}` : "";
|
|
8419
9676
|
const name = `${captureSlug(preview.title)}-${result.frame.width}${noteSuffix}.png`;
|
|
8420
9677
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
8421
|
-
await mkdir8(
|
|
8422
|
-
await
|
|
9678
|
+
await mkdir8(join16(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
9679
|
+
await writeFile9(join16(cwd, relativeFile), shot.png);
|
|
8423
9680
|
return sendJson2(res, 200, {
|
|
8424
9681
|
ok: true,
|
|
8425
9682
|
file: relativeFile,
|
|
@@ -8799,15 +10056,15 @@ async function startServer(options) {
|
|
|
8799
10056
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
8800
10057
|
const slash = rest.indexOf("/");
|
|
8801
10058
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
8802
|
-
let
|
|
10059
|
+
let relative8 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
8803
10060
|
try {
|
|
8804
|
-
|
|
10061
|
+
relative8 = decodeURIComponent(relative8);
|
|
8805
10062
|
} catch {
|
|
8806
|
-
|
|
10063
|
+
relative8 = "";
|
|
8807
10064
|
}
|
|
8808
10065
|
const dir = fileMounts.get(slug);
|
|
8809
10066
|
const serveMount = () => {
|
|
8810
|
-
if (dir !== void 0 &&
|
|
10067
|
+
if (dir !== void 0 && relative8 !== "" && serveFrom(res, dir, relative8))
|
|
8811
10068
|
return;
|
|
8812
10069
|
res.writeHead(404, {
|
|
8813
10070
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -8817,7 +10074,7 @@ async function startServer(options) {
|
|
|
8817
10074
|
};
|
|
8818
10075
|
if (!context.remote)
|
|
8819
10076
|
return serveMount();
|
|
8820
|
-
if (
|
|
10077
|
+
if (relative8.split("/").some((segment) => segment.startsWith("."))) {
|
|
8821
10078
|
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8822
10079
|
}
|
|
8823
10080
|
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
@@ -8898,7 +10155,7 @@ async function startServer(options) {
|
|
|
8898
10155
|
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
8899
10156
|
options.updates?.setPort(port);
|
|
8900
10157
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
8901
|
-
liveHealth = watchHealth(target, live);
|
|
10158
|
+
liveHealth = watchHealth(target, live, healthProbeMs);
|
|
8902
10159
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
8903
10160
|
});
|
|
8904
10161
|
await writeServerInfo(cwd, {
|
|
@@ -8918,6 +10175,42 @@ async function startServer(options) {
|
|
|
8918
10175
|
if (options.claudeAgentSession !== void 0)
|
|
8919
10176
|
runnerOptions.claudeAgentSession = options.claudeAgentSession;
|
|
8920
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);
|
|
8921
10214
|
options.updates?.onBusy(() => runner?.snapshot().running ?? false);
|
|
8922
10215
|
options.updates?.onChange(() => live.nudge("update"));
|
|
8923
10216
|
let closePromise = null;
|
|
@@ -8935,6 +10228,7 @@ async function startServer(options) {
|
|
|
8935
10228
|
shares?.close() ?? Promise.resolve(),
|
|
8936
10229
|
branches.stop(),
|
|
8937
10230
|
runner.stop(),
|
|
10231
|
+
generations?.close() ?? Promise.resolve(),
|
|
8938
10232
|
browserPool.close(),
|
|
8939
10233
|
live.close()
|
|
8940
10234
|
]);
|
|
@@ -8956,9 +10250,9 @@ async function startServer(options) {
|
|
|
8956
10250
|
// ../server/dist/update.js
|
|
8957
10251
|
import { spawn as spawnChild2 } from "child_process";
|
|
8958
10252
|
import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
|
|
8959
|
-
import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as
|
|
8960
|
-
import { homedir as
|
|
8961
|
-
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";
|
|
8962
10256
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8963
10257
|
var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
|
|
8964
10258
|
|
|
@@ -8991,8 +10285,8 @@ function planKeep(options) {
|
|
|
8991
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.`
|
|
8992
10286
|
};
|
|
8993
10287
|
}
|
|
8994
|
-
const to =
|
|
8995
|
-
if (to.startsWith("..")) {
|
|
10288
|
+
const to = normalize3(options.to);
|
|
10289
|
+
if (to.startsWith("..") || isAbsolute2(to)) {
|
|
8996
10290
|
return { ok: false, error: "The destination has to be inside the project." };
|
|
8997
10291
|
}
|
|
8998
10292
|
if (to.split(/[/\\]/)[0] === ".leglas") {
|
|
@@ -9017,11 +10311,11 @@ function planKeep(options) {
|
|
|
9017
10311
|
}
|
|
9018
10312
|
|
|
9019
10313
|
// src/run-init.ts
|
|
9020
|
-
import { readFile as
|
|
9021
|
-
import { join as
|
|
10314
|
+
import { readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
|
|
10315
|
+
import { join as join18 } from "path";
|
|
9022
10316
|
async function readIfPresent(path) {
|
|
9023
10317
|
try {
|
|
9024
|
-
return await
|
|
10318
|
+
return await readFile16(path, "utf8");
|
|
9025
10319
|
} catch {
|
|
9026
10320
|
return null;
|
|
9027
10321
|
}
|
|
@@ -9029,18 +10323,18 @@ async function readIfPresent(path) {
|
|
|
9029
10323
|
async function runInit(options, deps) {
|
|
9030
10324
|
const existingConfig = findConfigFile(options.cwd);
|
|
9031
10325
|
const plan = planInit({
|
|
9032
|
-
agents: await readIfPresent(
|
|
10326
|
+
agents: await readIfPresent(join18(options.cwd, "AGENTS.md")),
|
|
9033
10327
|
config: existingConfig === null ? null : "present",
|
|
9034
|
-
gitignore: await readIfPresent(
|
|
10328
|
+
gitignore: await readIfPresent(join18(options.cwd, ".gitignore")),
|
|
9035
10329
|
force: options.force
|
|
9036
10330
|
});
|
|
9037
10331
|
const touched = [];
|
|
9038
10332
|
for (const write2 of plan.writes) {
|
|
9039
|
-
await
|
|
10333
|
+
await writeFile11(join18(options.cwd, write2.path), write2.contents, "utf8");
|
|
9040
10334
|
touched.push(write2.path);
|
|
9041
10335
|
}
|
|
9042
10336
|
if (plan.gitignore !== null) {
|
|
9043
|
-
await
|
|
10337
|
+
await writeFile11(join18(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9044
10338
|
touched.push(".gitignore");
|
|
9045
10339
|
}
|
|
9046
10340
|
if (options.json) {
|
|
@@ -9060,8 +10354,8 @@ async function runInit(options, deps) {
|
|
|
9060
10354
|
|
|
9061
10355
|
// src/run-keep.ts
|
|
9062
10356
|
import { existsSync as existsSync5 } from "fs";
|
|
9063
|
-
import { copyFile as copyFile2, mkdir as mkdir10, readFile as
|
|
9064
|
-
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";
|
|
9065
10359
|
|
|
9066
10360
|
// src/resolve-title.ts
|
|
9067
10361
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -9094,15 +10388,15 @@ async function writeLogEntry(options) {
|
|
|
9094
10388
|
annotations: await readAnnotations(options.cwd),
|
|
9095
10389
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
9096
10390
|
});
|
|
9097
|
-
const dir =
|
|
10391
|
+
const dir = join19(options.cwd, options.logDir);
|
|
9098
10392
|
await mkdir10(dir, { recursive: true });
|
|
9099
|
-
const file =
|
|
9100
|
-
await
|
|
10393
|
+
const file = join19(dir, `${entry.slug}.md`);
|
|
10394
|
+
await writeFile12(file, entry.markdown, "utf8");
|
|
9101
10395
|
if (entry.pictures.length > 0) {
|
|
9102
|
-
const pictureDir =
|
|
10396
|
+
const pictureDir = join19(dir, entry.slug);
|
|
9103
10397
|
await mkdir10(pictureDir, { recursive: true });
|
|
9104
10398
|
for (const picture of entry.pictures) {
|
|
9105
|
-
await copyFile2(
|
|
10399
|
+
await copyFile2(join19(options.cwd, picture.from), join19(pictureDir, picture.to)).catch(() => {
|
|
9106
10400
|
});
|
|
9107
10401
|
}
|
|
9108
10402
|
}
|
|
@@ -9123,19 +10417,20 @@ async function runKeep(options, deps) {
|
|
|
9123
10417
|
await readRenames(options.cwd)
|
|
9124
10418
|
);
|
|
9125
10419
|
if (!resolved.ok) return fail(resolved.error);
|
|
9126
|
-
const
|
|
10420
|
+
const destination = isAbsolute3(options.to) ? relative5(options.cwd, options.to) : options.to;
|
|
10421
|
+
const plan = planKeep({ title: resolved.title, previews, to: destination });
|
|
9127
10422
|
if (!plan.ok) return fail(plan.error);
|
|
9128
|
-
const from =
|
|
9129
|
-
const to =
|
|
10423
|
+
const from = join19(options.cwd, plan.move.from);
|
|
10424
|
+
const to = join19(options.cwd, plan.move.to);
|
|
9130
10425
|
if (!existsSync5(from)) {
|
|
9131
10426
|
return fail(`${plan.move.from} does not exist. Nothing to keep.`);
|
|
9132
10427
|
}
|
|
9133
10428
|
if (existsSync5(to)) {
|
|
9134
10429
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
9135
10430
|
}
|
|
9136
|
-
const source = await
|
|
9137
|
-
await mkdir10(
|
|
9138
|
-
await
|
|
10431
|
+
const source = await readFile17(from, "utf8");
|
|
10432
|
+
await mkdir10(dirname12(to), { recursive: true });
|
|
10433
|
+
await writeFile12(to, renameExport(source, plan.exportName), "utf8");
|
|
9139
10434
|
const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
|
|
9140
10435
|
let logged = null;
|
|
9141
10436
|
let logError = null;
|
|
@@ -9150,7 +10445,7 @@ async function runKeep(options, deps) {
|
|
|
9150
10445
|
} catch (error) {
|
|
9151
10446
|
logError = error instanceof Error ? error.message : String(error);
|
|
9152
10447
|
}
|
|
9153
|
-
await rm6(
|
|
10448
|
+
await rm6(join19(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
9154
10449
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
9155
10450
|
if (options.json) {
|
|
9156
10451
|
deps.log(
|
|
@@ -9189,11 +10484,11 @@ async function runKeep(options, deps) {
|
|
|
9189
10484
|
|
|
9190
10485
|
// src/run-new.ts
|
|
9191
10486
|
import { existsSync as existsSync6 } from "fs";
|
|
9192
|
-
import { mkdir as mkdir11, readFile as
|
|
9193
|
-
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";
|
|
9194
10489
|
async function readIfPresent2(path) {
|
|
9195
10490
|
try {
|
|
9196
|
-
return await
|
|
10491
|
+
return await readFile18(path, "utf8");
|
|
9197
10492
|
} catch {
|
|
9198
10493
|
return null;
|
|
9199
10494
|
}
|
|
@@ -9201,7 +10496,7 @@ async function readIfPresent2(path) {
|
|
|
9201
10496
|
async function runNew(options, deps) {
|
|
9202
10497
|
let from;
|
|
9203
10498
|
if (options.from !== void 0) {
|
|
9204
|
-
const contents = await readIfPresent2(
|
|
10499
|
+
const contents = await readIfPresent2(join20(options.cwd, options.from));
|
|
9205
10500
|
if (contents === null) {
|
|
9206
10501
|
const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
|
|
9207
10502
|
if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
|
|
@@ -9212,8 +10507,8 @@ async function runNew(options, deps) {
|
|
|
9212
10507
|
}
|
|
9213
10508
|
const plan = planNew({
|
|
9214
10509
|
surface: options.surface,
|
|
9215
|
-
packageJson: await readIfPresent2(
|
|
9216
|
-
gitignore: await readIfPresent2(
|
|
10510
|
+
packageJson: await readIfPresent2(join20(options.cwd, "package.json")),
|
|
10511
|
+
gitignore: await readIfPresent2(join20(options.cwd, ".gitignore")),
|
|
9217
10512
|
from
|
|
9218
10513
|
});
|
|
9219
10514
|
const fail = (error) => {
|
|
@@ -9243,7 +10538,7 @@ async function runNew(options, deps) {
|
|
|
9243
10538
|
deps.log(plan.instructions);
|
|
9244
10539
|
return { exitCode: 0, written: [] };
|
|
9245
10540
|
}
|
|
9246
|
-
const existing = plan.writes.filter((write2) => existsSync6(
|
|
10541
|
+
const existing = plan.writes.filter((write2) => existsSync6(join20(options.cwd, write2.path)));
|
|
9247
10542
|
if (existing.length > 0) {
|
|
9248
10543
|
return fail(
|
|
9249
10544
|
`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`
|
|
@@ -9251,13 +10546,13 @@ async function runNew(options, deps) {
|
|
|
9251
10546
|
}
|
|
9252
10547
|
const written = [];
|
|
9253
10548
|
for (const write2 of plan.writes) {
|
|
9254
|
-
const target =
|
|
9255
|
-
await mkdir11(
|
|
9256
|
-
await
|
|
10549
|
+
const target = join20(options.cwd, write2.path);
|
|
10550
|
+
await mkdir11(dirname13(target), { recursive: true });
|
|
10551
|
+
await writeFile13(target, write2.contents, "utf8");
|
|
9257
10552
|
written.push(write2.path);
|
|
9258
10553
|
}
|
|
9259
10554
|
if (plan.gitignore !== null) {
|
|
9260
|
-
await
|
|
10555
|
+
await writeFile13(join20(options.cwd, ".gitignore"), plan.gitignore, "utf8");
|
|
9261
10556
|
written.push(".gitignore");
|
|
9262
10557
|
}
|
|
9263
10558
|
if (options.json) {
|
|
@@ -9285,21 +10580,21 @@ async function runNew(options, deps) {
|
|
|
9285
10580
|
}
|
|
9286
10581
|
|
|
9287
10582
|
// src/run-previews.ts
|
|
9288
|
-
import { readFile as
|
|
9289
|
-
import { join as
|
|
10583
|
+
import { readFile as readFile19, writeFile as writeFile14 } from "fs/promises";
|
|
10584
|
+
import { join as join21 } from "path";
|
|
9290
10585
|
function envelope(deps, ok, body) {
|
|
9291
10586
|
deps.log(JSON.stringify({ ok, ...body }));
|
|
9292
10587
|
}
|
|
9293
10588
|
async function ensureIgnored(cwd) {
|
|
9294
|
-
const path =
|
|
10589
|
+
const path = join21(cwd, ".gitignore");
|
|
9295
10590
|
let current = null;
|
|
9296
10591
|
try {
|
|
9297
|
-
current = await
|
|
10592
|
+
current = await readFile19(path, "utf8");
|
|
9298
10593
|
} catch {
|
|
9299
10594
|
current = null;
|
|
9300
10595
|
}
|
|
9301
10596
|
const next = ignoreEntry(current);
|
|
9302
|
-
if (next !== null) await
|
|
10597
|
+
if (next !== null) await writeFile14(path, next, "utf8");
|
|
9303
10598
|
}
|
|
9304
10599
|
async function runAdd(options, deps) {
|
|
9305
10600
|
const loaded = await loadConfig(options.cwd);
|
|
@@ -9522,7 +10817,7 @@ async function findLeglas(cwd, port, request) {
|
|
|
9522
10817
|
signal: AbortSignal.timeout(2e3)
|
|
9523
10818
|
});
|
|
9524
10819
|
if (health.status !== 200) return { ok: false, error: NOT_RUNNING };
|
|
9525
|
-
const answered = await health.text().then((
|
|
10820
|
+
const answered = await health.text().then((text3) => JSON.parse(text3)).catch(() => ({}));
|
|
9526
10821
|
if (answered === null) return { ok: false, error: NOT_RUNNING };
|
|
9527
10822
|
if (isJsonObject(answered) && isString2(answered.cwd) && !await sameDirectory(answered.cwd, cwd)) {
|
|
9528
10823
|
return {
|
|
@@ -9954,24 +11249,24 @@ function installShutdown(stop, target = process) {
|
|
|
9954
11249
|
|
|
9955
11250
|
// src/run-watch.ts
|
|
9956
11251
|
import { spawn as spawn4 } from "child_process";
|
|
9957
|
-
import { mkdir as mkdir12, readFile as
|
|
9958
|
-
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";
|
|
9959
11254
|
var POLL_MS2 = 2e3;
|
|
9960
11255
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
9961
11256
|
async function saveTemplate(cwd, run4) {
|
|
9962
|
-
const path =
|
|
11257
|
+
const path = join22(cwd, WATCH_PATH);
|
|
9963
11258
|
let config = {};
|
|
9964
11259
|
try {
|
|
9965
|
-
const parsed = JSON.parse(await
|
|
11260
|
+
const parsed = JSON.parse(await readFile20(path, "utf8"));
|
|
9966
11261
|
if (isJsonObject(parsed)) config = parsed;
|
|
9967
11262
|
} catch {
|
|
9968
11263
|
}
|
|
9969
11264
|
config.run = run4;
|
|
9970
|
-
await mkdir12(
|
|
9971
|
-
await
|
|
11265
|
+
await mkdir12(dirname14(path), { recursive: true });
|
|
11266
|
+
await writeFile15(path, `${JSON.stringify(config, null, 2)}
|
|
9972
11267
|
`, "utf8");
|
|
9973
11268
|
}
|
|
9974
|
-
function spawnAgent(command, args, cwd) {
|
|
11269
|
+
function spawnAgent(command, args, cwd, json) {
|
|
9975
11270
|
return new Promise((resolve5) => {
|
|
9976
11271
|
let settled = false;
|
|
9977
11272
|
const settle = (outcome) => {
|
|
@@ -9979,7 +11274,10 @@ function spawnAgent(command, args, cwd) {
|
|
|
9979
11274
|
settled = true;
|
|
9980
11275
|
resolve5(outcome);
|
|
9981
11276
|
};
|
|
9982
|
-
const child = spawn4(command, args, {
|
|
11277
|
+
const child = spawn4(command, args, {
|
|
11278
|
+
cwd,
|
|
11279
|
+
stdio: json ? ["inherit", 2, "inherit"] : "inherit"
|
|
11280
|
+
});
|
|
9983
11281
|
child.on("error", (error) => settle({ ok: false, error: error.message }));
|
|
9984
11282
|
child.on(
|
|
9985
11283
|
"close",
|
|
@@ -9990,6 +11288,13 @@ function spawnAgent(command, args, cwd) {
|
|
|
9990
11288
|
});
|
|
9991
11289
|
}
|
|
9992
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
|
+
};
|
|
9993
11298
|
const saved = options.run === void 0 ? await readAgentChoice(options.cwd) : { agent: null, run: null };
|
|
9994
11299
|
const raw = options.run ?? saved.run;
|
|
9995
11300
|
let template;
|
|
@@ -9997,10 +11302,7 @@ async function runWatch(options, deps) {
|
|
|
9997
11302
|
let synthesizedAgent = null;
|
|
9998
11303
|
if (raw !== null) {
|
|
9999
11304
|
const parsed = parseTemplate(raw);
|
|
10000
|
-
if (!parsed.ok)
|
|
10001
|
-
deps.error(parsed.error);
|
|
10002
|
-
return { exitCode: 1 };
|
|
10003
|
-
}
|
|
11305
|
+
if (!parsed.ok) return refuse2(parsed.error);
|
|
10004
11306
|
template = parsed.template;
|
|
10005
11307
|
shownCommand2 = raw;
|
|
10006
11308
|
} else if (saved.agent !== null && saved.agent !== "custom") {
|
|
@@ -10012,10 +11314,9 @@ async function runWatch(options, deps) {
|
|
|
10012
11314
|
shownCommand2 = [template.command, ...template.args].join(" ");
|
|
10013
11315
|
synthesizedAgent = adapter.name;
|
|
10014
11316
|
} else {
|
|
10015
|
-
|
|
11317
|
+
return refuse2(
|
|
10016
11318
|
'Watch needs an agent command the first time: pick an agent in the interface, or pass --run "claude -p {prompt}".'
|
|
10017
11319
|
);
|
|
10018
|
-
return { exitCode: 1 };
|
|
10019
11320
|
}
|
|
10020
11321
|
if (options.run !== void 0) await saveTemplate(options.cwd, options.run).catch(() => {
|
|
10021
11322
|
});
|
|
@@ -10031,26 +11332,36 @@ async function runWatch(options, deps) {
|
|
|
10031
11332
|
} catch {
|
|
10032
11333
|
}
|
|
10033
11334
|
};
|
|
10034
|
-
if (
|
|
10035
|
-
|
|
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.");
|
|
10036
11343
|
}
|
|
10037
|
-
deps.log(`Watching for change requests. Each one runs: ${shownCommand2}`);
|
|
10038
|
-
deps.log("Stop with Ctrl-C.");
|
|
10039
11344
|
const failed = /* @__PURE__ */ new Set();
|
|
10040
11345
|
let stopped = false;
|
|
10041
11346
|
let busy = false;
|
|
10042
11347
|
let announced = false;
|
|
10043
11348
|
let inflight = null;
|
|
10044
11349
|
const handle = async (request) => {
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
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
|
+
}
|
|
10048
11358
|
await markPickedUp(options.cwd, request.id);
|
|
10049
11359
|
const { command, args } = commandFor(template, request.prompt);
|
|
10050
|
-
const outcome = await spawnAgent(command, args, options.cwd);
|
|
11360
|
+
const outcome = await spawnAgent(command, args, options.cwd, json);
|
|
10051
11361
|
if (outcome.ok && outcome.code === 0) {
|
|
10052
11362
|
await removeRequest(options.cwd, request.id);
|
|
10053
|
-
|
|
11363
|
+
if (json) emit({ event: "done", id, title });
|
|
11364
|
+
else deps.log(` done ${title}`);
|
|
10054
11365
|
return;
|
|
10055
11366
|
}
|
|
10056
11367
|
failed.add(request.id);
|
|
@@ -10060,8 +11371,12 @@ async function runWatch(options, deps) {
|
|
|
10060
11371
|
exitCode: outcome.ok ? outcome.code : null
|
|
10061
11372
|
});
|
|
10062
11373
|
await markFailed(options.cwd, request.id, failure);
|
|
10063
|
-
|
|
10064
|
-
|
|
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
|
+
}
|
|
10065
11380
|
};
|
|
10066
11381
|
const tick = async () => {
|
|
10067
11382
|
if (stopped) return;
|
|
@@ -10080,7 +11395,9 @@ async function runWatch(options, deps) {
|
|
|
10080
11395
|
await inflight;
|
|
10081
11396
|
}
|
|
10082
11397
|
} catch (error) {
|
|
10083
|
-
|
|
11398
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
11399
|
+
if (json) emit({ event: "error", message: message2 });
|
|
11400
|
+
else deps.error(` ! ${message2}`);
|
|
10084
11401
|
} finally {
|
|
10085
11402
|
inflight = null;
|
|
10086
11403
|
busy = false;
|
|
@@ -10095,7 +11412,10 @@ async function runWatch(options, deps) {
|
|
|
10095
11412
|
process.off("SIGINT", stop);
|
|
10096
11413
|
process.off("SIGTERM", stop);
|
|
10097
11414
|
void Promise.resolve(inflight).catch(() => {
|
|
10098
|
-
}).then(() => heartbeat(false)).then(() =>
|
|
11415
|
+
}).then(() => heartbeat(false)).then(() => {
|
|
11416
|
+
if (json) emit({ event: "stopped" });
|
|
11417
|
+
resolve5({ exitCode: 0 });
|
|
11418
|
+
});
|
|
10099
11419
|
};
|
|
10100
11420
|
process.on("SIGINT", stop);
|
|
10101
11421
|
process.on("SIGTERM", stop);
|
|
@@ -10106,13 +11426,13 @@ async function runWatch(options, deps) {
|
|
|
10106
11426
|
}
|
|
10107
11427
|
|
|
10108
11428
|
// src/run-classify.ts
|
|
10109
|
-
import { stat as
|
|
10110
|
-
import { join as
|
|
11429
|
+
import { stat as stat5 } from "fs/promises";
|
|
11430
|
+
import { join as join23 } from "path";
|
|
10111
11431
|
async function runClassify(options, deps) {
|
|
10112
11432
|
const declared = await Promise.all(
|
|
10113
11433
|
options.changes.map(async (change) => ({
|
|
10114
11434
|
...change,
|
|
10115
|
-
exists: await
|
|
11435
|
+
exists: await stat5(join23(options.cwd, change.path)).then(
|
|
10116
11436
|
() => true,
|
|
10117
11437
|
() => false
|
|
10118
11438
|
)
|
|
@@ -10141,13 +11461,13 @@ async function runClassify(options, deps) {
|
|
|
10141
11461
|
import { existsSync as existsSync7 } from "fs";
|
|
10142
11462
|
import { realpath as realpath4 } from "fs/promises";
|
|
10143
11463
|
import { createRequire } from "module";
|
|
10144
|
-
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";
|
|
10145
11465
|
import { fileURLToPath } from "url";
|
|
10146
11466
|
|
|
10147
11467
|
// src/dev-server-owner.ts
|
|
10148
11468
|
import { execFile as execFile2 } from "child_process";
|
|
10149
11469
|
import { realpath as realpath3 } from "fs/promises";
|
|
10150
|
-
import { basename as basename5, isAbsolute as
|
|
11470
|
+
import { basename as basename5, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve3 } from "path";
|
|
10151
11471
|
import { promisify as promisify2 } from "util";
|
|
10152
11472
|
var run2 = promisify2(execFile2);
|
|
10153
11473
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -10216,8 +11536,8 @@ async function inspectLocalDevServer(origin) {
|
|
|
10216
11536
|
}
|
|
10217
11537
|
}
|
|
10218
11538
|
function isInside(parent, child) {
|
|
10219
|
-
const path =
|
|
10220
|
-
return path === "" || !path.startsWith("..") && !
|
|
11539
|
+
const path = relative6(resolve3(parent), resolve3(child));
|
|
11540
|
+
return path === "" || !path.startsWith("..") && !isAbsolute4(path);
|
|
10221
11541
|
}
|
|
10222
11542
|
function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
10223
11543
|
const port = localDevServerPort(origin);
|
|
@@ -10230,11 +11550,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
10230
11550
|
|
|
10231
11551
|
// src/run.ts
|
|
10232
11552
|
function findShellDir() {
|
|
10233
|
-
const bundled =
|
|
10234
|
-
if (existsSync7(
|
|
11553
|
+
const bundled = join24(dirname15(fileURLToPath(import.meta.url)), "shell");
|
|
11554
|
+
if (existsSync7(join24(bundled, "index.html"))) return bundled;
|
|
10235
11555
|
try {
|
|
10236
11556
|
const require2 = createRequire(import.meta.url);
|
|
10237
|
-
return
|
|
11557
|
+
return dirname15(require2.resolve("@leglas/shell/dist/index.html"));
|
|
10238
11558
|
} catch {
|
|
10239
11559
|
return null;
|
|
10240
11560
|
}
|
|
@@ -10245,7 +11565,7 @@ function shellWord(value) {
|
|
|
10245
11565
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
10246
11566
|
}
|
|
10247
11567
|
function embeddedLeglasCommand() {
|
|
10248
|
-
const entry =
|
|
11568
|
+
const entry = join24(dirname15(fileURLToPath(import.meta.url)), "bin.js");
|
|
10249
11569
|
if (!existsSync7(entry)) return "npx -y leglas";
|
|
10250
11570
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
10251
11571
|
}
|
|
@@ -10284,7 +11604,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10284
11604
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
10285
11605
|
for (const preview of merged?.previews ?? []) {
|
|
10286
11606
|
if (preview.file !== void 0) {
|
|
10287
|
-
const absolute =
|
|
11607
|
+
const absolute = join24(options.cwd, preview.file);
|
|
10288
11608
|
if (!existsSync7(absolute)) {
|
|
10289
11609
|
previewErrors.push(
|
|
10290
11610
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
@@ -10295,7 +11615,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10295
11615
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
10296
11616
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
10297
11617
|
}
|
|
10298
|
-
fileMounts.set(slug,
|
|
11618
|
+
fileMounts.set(slug, dirname15(absolute));
|
|
10299
11619
|
previews.push({
|
|
10300
11620
|
...preview,
|
|
10301
11621
|
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
@@ -10307,8 +11627,8 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10307
11627
|
const config = merged === null ? null : { ...merged, previews };
|
|
10308
11628
|
const configWarnings = [];
|
|
10309
11629
|
const projectRoot = await realpath4(
|
|
10310
|
-
loaded.path === null ? options.cwd :
|
|
10311
|
-
).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)));
|
|
10312
11632
|
const ownerWarning = needsApp && app === null ? (services.inspectLocalDevServer ?? inspectLocalDevServer)(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
10313
11633
|
const serverOptions = {
|
|
10314
11634
|
config,
|
|
@@ -10346,7 +11666,7 @@ async function runWithServices(options, deps, services = {}) {
|
|
|
10346
11666
|
})
|
|
10347
11667
|
);
|
|
10348
11668
|
} else {
|
|
10349
|
-
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;
|
|
10350
11670
|
deps.log(`Leglas ${url}`);
|
|
10351
11671
|
deps.log(
|
|
10352
11672
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|