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