leglas 1.2.0 → 1.3.0

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