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/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
- return { kind: "explore", surface, count, basedOn, json };
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>${note}</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 normalize2 } from "path";
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(text2) {
997
- return JSON.parse(text2);
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, images = []) => [
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
- ...images.flatMap((image) => ["-i", image]),
1369
+ ...images2.flatMap((image) => ["-i", image]),
1326
1370
  prompt
1327
1371
  ],
1328
- terminalArgs: (prompt, effort = null, images = []) => [
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
- ...images.flatMap((image) => ["-i", image]),
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, images = []) => [
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
- ...images.flatMap((image) => ["-i", image]),
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, readdir4 = readableDirectories) {
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: readdir4(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
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 readdir4 = search.readdir ?? readableDirectories;
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, readdir4);
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 = readdir4(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
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(readdir4(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
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: (text2) => socket.send(text2),
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((text2) => {
2572
+ socket.onMessage((text3) => {
2529
2573
  let message2;
2530
2574
  try {
2531
- const parsed = parseJson(text2);
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(text2, from, to) {
3388
- return text2.split(`${CAPTURES_DIR}/${from}/`).join(`${CAPTURES_DIR}/${to}/`);
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, images) {
4257
- if (images.length === 0)
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 images) {
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 = [], images = []) {
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, images), ...allow],
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, images), ...allow],
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 images = (await Promise.all((request.attachments ?? []).map(async (attachment) => await isOwnCapture(options.cwd, attachment.file) ? resolve2(options.cwd, attachment.file) : null))).filter((image) => image !== null);
5496
- let resolved = resolveCommand(choice, request.prompt, choice.agent === null ? null : resumable(choice.agent), allowedCommands, images);
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, images);
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 readdir3, writeFile as writeFile8 } from "fs/promises";
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 dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
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 readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
7352
- import { dirname as dirname7, join as join11 } from "path";
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 = join11(cwd, SERVER_INFO_PATH);
7356
- const directory = dirname7(path);
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 writeFile7(temporary, `${JSON.stringify({ ...info, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
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 readFile11(join11(cwd, SERVER_INFO_PATH), "utf8"));
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(join11(cwd, SERVER_INFO_PATH), { force: true });
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 relative6 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
7550
- const candidate = join12(dir, relative6);
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 relative6 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
7565
- const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
7566
- return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
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 stat5 = statSync(path);
7572
- return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
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 = join12(cwd, ".leglas");
8767
+ const leglasDir = join16(cwd, ".leglas");
7579
8768
  const targets = [
7580
- { path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
7581
- { path: join12(cwd, REQUESTS_PATH), change: "requests" },
7582
- { path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
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 = dirname8(configPath);
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
- }, HEALTH_PROBE_MS);
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 = relative3(cwd, current.path) || current.path;
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 = relative3(cwd, boot.path) || boot.path;
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 = relative3(cwd, current.path) || current.path;
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 === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
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(join12(cwd, REFERENCES_DIR), { recursive: true });
8216
- await writeFile8(join12(cwd, file), body);
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 readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
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
- }, CAPTURE_DEADLINE_MS);
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(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
8422
- await writeFile8(join12(cwd, relativeFile), shot.png);
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 relative6 = slash === -1 ? "" : rest.slice(slash + 1);
10059
+ let relative8 = slash === -1 ? "" : rest.slice(slash + 1);
8803
10060
  try {
8804
- relative6 = decodeURIComponent(relative6);
10061
+ relative8 = decodeURIComponent(relative8);
8805
10062
  } catch {
8806
- relative6 = "";
10063
+ relative8 = "";
8807
10064
  }
8808
10065
  const dir = fileMounts.get(slug);
8809
10066
  const serveMount = () => {
8810
- if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
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 (relative6.split("/").some((segment) => segment.startsWith("."))) {
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 writeFile9 } from "fs/promises";
8960
- import { homedir as homedir2 } from "os";
8961
- import { dirname as dirname9, join as join13, posix as posix2, win32 } from "path";
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 = normalize2(options.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 readFile12, writeFile as writeFile10 } from "fs/promises";
9021
- import { join as join14 } from "path";
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 readFile12(path, "utf8");
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(join14(options.cwd, "AGENTS.md")),
10326
+ agents: await readIfPresent(join18(options.cwd, "AGENTS.md")),
9033
10327
  config: existingConfig === null ? null : "present",
9034
- gitignore: await readIfPresent(join14(options.cwd, ".gitignore")),
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 writeFile10(join14(options.cwd, write2.path), write2.contents, "utf8");
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 writeFile10(join14(options.cwd, ".gitignore"), plan.gitignore, "utf8");
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 readFile13, rm as rm6, writeFile as writeFile11 } from "fs/promises";
9064
- import { dirname as dirname10, join as join15 } from "path";
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 = join15(options.cwd, options.logDir);
10391
+ const dir = join19(options.cwd, options.logDir);
9098
10392
  await mkdir10(dir, { recursive: true });
9099
- const file = join15(dir, `${entry.slug}.md`);
9100
- await writeFile11(file, entry.markdown, "utf8");
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 = join15(dir, entry.slug);
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(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
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 plan = planKeep({ title: resolved.title, previews, to: options.to });
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 = join15(options.cwd, plan.move.from);
9129
- const to = join15(options.cwd, plan.move.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 readFile13(from, "utf8");
9137
- await mkdir10(dirname10(to), { recursive: true });
9138
- await writeFile11(to, renameExport(source, plan.exportName), "utf8");
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(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
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 readFile14, writeFile as writeFile12 } from "fs/promises";
9193
- import { dirname as dirname11, join as join16 } from "path";
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 readFile14(path, "utf8");
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(join16(options.cwd, options.from));
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(join16(options.cwd, "package.json")),
9216
- gitignore: await readIfPresent2(join16(options.cwd, ".gitignore")),
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(join16(options.cwd, write2.path)));
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 = join16(options.cwd, write2.path);
9255
- await mkdir11(dirname11(target), { recursive: true });
9256
- await writeFile12(target, write2.contents, "utf8");
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 writeFile12(join16(options.cwd, ".gitignore"), plan.gitignore, "utf8");
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 readFile15, writeFile as writeFile13 } from "fs/promises";
9289
- import { join as join17 } from "path";
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 = join17(cwd, ".gitignore");
10589
+ const path = join21(cwd, ".gitignore");
9295
10590
  let current = null;
9296
10591
  try {
9297
- current = await readFile15(path, "utf8");
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 writeFile13(path, next, "utf8");
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((text2) => JSON.parse(text2)).catch(() => ({}));
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 readFile16, writeFile as writeFile14 } from "fs/promises";
9958
- import { dirname as dirname12, join as join18 } from "path";
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 = join18(cwd, WATCH_PATH);
11257
+ const path = join22(cwd, WATCH_PATH);
9963
11258
  let config = {};
9964
11259
  try {
9965
- const parsed = JSON.parse(await readFile16(path, "utf8"));
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(dirname12(path), { recursive: true });
9971
- await writeFile14(path, `${JSON.stringify(config, null, 2)}
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, { cwd, stdio: "inherit" });
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
- deps.error(
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 (synthesizedAgent !== null) {
10035
- deps.log(`Using ${synthesizedAgent}, chosen in the interface.`);
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
- deps.log("");
10046
- deps.log(` ${request.title}: ${request.intent}`);
10047
- if (request.target !== null) deps.log(` ${request.target}`);
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
- deps.log(` done ${request.title}`);
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
- deps.error(` failed ${request.title}: ${failure.message}`);
10064
- deps.error(" Left in the queue and not retried.");
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
- deps.error(` ! ${error instanceof Error ? error.message : String(error)}`);
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(() => resolve5({ exitCode: 0 }));
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 stat4 } from "fs/promises";
10110
- import { join as join19 } from "path";
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 stat4(join19(options.cwd, change.path)).then(
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 dirname13, join as join20, relative as relative5, resolve as resolve4 } from "path";
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 isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
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 = relative4(resolve3(parent), resolve3(child));
10220
- return path === "" || !path.startsWith("..") && !isAbsolute2(path);
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 = join20(dirname13(fileURLToPath(import.meta.url)), "shell");
10234
- if (existsSync7(join20(bundled, "index.html"))) return bundled;
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 dirname13(require2.resolve("@leglas/shell/dist/index.html"));
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 = join20(dirname13(fileURLToPath(import.meta.url)), "bin.js");
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 = join20(options.cwd, preview.file);
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, dirname13(absolute));
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 : dirname13(loaded.path)
10311
- ).catch(() => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path)));
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" : relative5(options.cwd, loaded.path) || loaded.path;
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)"}`