leglas 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -287,6 +287,19 @@ function parseArgs(argv) {
287
287
  }
288
288
  return { kind: "requests", json: rest.includes("--json"), clear: rest.includes("--clear") };
289
289
  }
290
+ if (argv[0] === "log") {
291
+ const rest = argv.slice(1);
292
+ const flags = rest.filter((argument) => argument.startsWith("--"));
293
+ const unknown = flags.find((flag) => flag !== "--json");
294
+ if (unknown !== void 0) {
295
+ return { kind: "error", message: `leglas log does not take ${unknown}.` };
296
+ }
297
+ const names = rest.filter((argument) => !argument.startsWith("--"));
298
+ if (names.length > 1) {
299
+ return { kind: "error", message: "leglas log takes one entry at most." };
300
+ }
301
+ return { kind: "log", entry: names[0] ?? null, json: flags.includes("--json") };
302
+ }
290
303
  if (argv[0] === "list") {
291
304
  const rest = argv.slice(1);
292
305
  const unknown = rest.find((argument) => argument !== "--json");
@@ -409,6 +422,84 @@ function parseArgs(argv) {
409
422
  import { stat as stat4 } from "fs/promises";
410
423
  import { join as join13 } from "path";
411
424
 
425
+ // ../server/dist/log.js
426
+ var DEFAULT_LOG_DIR = "design-log";
427
+ function slugify(value) {
428
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
429
+ }
430
+ function frameFor(title, requests) {
431
+ let found = null;
432
+ for (const request of requests) {
433
+ if (request.title !== title)
434
+ continue;
435
+ for (const attachment of request.attachments ?? []) {
436
+ if (attachment.kind === "frame")
437
+ found = attachment;
438
+ }
439
+ }
440
+ return found;
441
+ }
442
+ function askedOf(title, requests) {
443
+ return requests.filter((request) => request.title === title && request.status !== "failed" && request.intent.trim() !== "").map((request) => request.intent.trim());
444
+ }
445
+ function composeEntry(input) {
446
+ const slug = `${input.date}-${slugify(input.surface)}`;
447
+ const pictures = [];
448
+ const lines2 = [];
449
+ lines2.push(`# ${input.surface}, ${input.date}`);
450
+ lines2.push("");
451
+ lines2.push(`**${input.won.title}** won and became \`${input.won.to}\`. ${input.previews.length === 1 ? "It was the only direction." : `${input.previews.length} directions were compared.`}`);
452
+ lines2.push("");
453
+ for (const preview of input.previews) {
454
+ const won = preview.title === input.won.title;
455
+ lines2.push(`## ${preview.title}${won ? " \u2014 kept" : ""}`);
456
+ lines2.push("");
457
+ if (preview.note !== void 0 && preview.note.trim() !== "") {
458
+ lines2.push(preview.note.trim());
459
+ lines2.push("");
460
+ }
461
+ const frame = frameFor(preview.title, input.requests);
462
+ if (frame !== null) {
463
+ const name = `${slugify(preview.title)}.png`;
464
+ pictures.push({ from: frame.file, to: name });
465
+ lines2.push(`![${preview.title}](${slug}/${name})`);
466
+ lines2.push("");
467
+ }
468
+ if (preview.basedOn !== void 0) {
469
+ lines2.push(`A variant of ${preview.basedOn}.`);
470
+ lines2.push("");
471
+ }
472
+ const asked = askedOf(preview.title, input.requests);
473
+ if (asked.length > 0) {
474
+ lines2.push("Asked for:");
475
+ lines2.push("");
476
+ for (const words of asked)
477
+ lines2.push(`- ${words}`);
478
+ lines2.push("");
479
+ }
480
+ const notes = input.annotations.filter((note) => note.title === preview.title);
481
+ if (notes.length > 0) {
482
+ lines2.push("Marked on the design:");
483
+ lines2.push("");
484
+ for (const note of notes)
485
+ lines2.push(`- ${note.note}`);
486
+ lines2.push("");
487
+ }
488
+ }
489
+ const failed = input.requests.filter((request) => request.status === "failed");
490
+ if (failed.length > 0) {
491
+ lines2.push("## Changes that did not land");
492
+ lines2.push("");
493
+ for (const request of failed) {
494
+ const why = request.failure?.message;
495
+ lines2.push(`- ${request.title}: ${request.intent}${why === void 0 ? "" : ` (${why})`}`);
496
+ }
497
+ lines2.push("");
498
+ }
499
+ return { slug, markdown: `${lines2.join("\n").trimEnd()}
500
+ `, pictures };
501
+ }
502
+
412
503
  // ../server/dist/config.js
413
504
  var DEFAULT_DEV_SERVER = "http://localhost:3000";
414
505
  var DEFAULT_INSTALL_COMMAND = "npm install";
@@ -527,6 +618,10 @@ function normalizeConfig(raw, options = {}) {
527
618
  if (requireDevCommand && previews.some((preview) => preview.branch !== void 0) && devCommand === void 0) {
528
619
  errors.push("A preview names a branch, so devCommand is required: Leglas has to start that checkout itself.");
529
620
  }
621
+ const logDir = source["logDir"] ?? DEFAULT_LOG_DIR;
622
+ if (typeof logDir !== "string" || logDir.trim() === "") {
623
+ errors.push("logDir must be a non-empty string.");
624
+ }
530
625
  const installCommand = source["installCommand"] ?? DEFAULT_INSTALL_COMMAND;
531
626
  if (typeof installCommand !== "string" || installCommand.trim() === "") {
532
627
  errors.push("installCommand must be a non-empty string.");
@@ -543,7 +638,8 @@ function normalizeConfig(raw, options = {}) {
543
638
  previews,
544
639
  scanPreviews,
545
640
  devCommand: typeof devCommand === "string" ? devCommand : void 0,
546
- installCommand
641
+ installCommand,
642
+ logDir
547
643
  },
548
644
  errors: []
549
645
  };
@@ -1321,6 +1417,7 @@ import net from "net";
1321
1417
  function createProxyHandler(options) {
1322
1418
  const target = new URL(options.target);
1323
1419
  const host = target.hostname;
1420
+ const dialHost = host.replace(/^\[|\]$/g, "");
1324
1421
  const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
1325
1422
  const authority = target.port ? `${host}:${target.port}` : host;
1326
1423
  function upstreamHeaders(req) {
@@ -1337,7 +1434,19 @@ function createProxyHandler(options) {
1337
1434
  }
1338
1435
  return {
1339
1436
  request(req, res, publicOrigin) {
1340
- const upstream = http.request({ host, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
1437
+ options.onActivity?.();
1438
+ options.onOpen?.();
1439
+ let open = true;
1440
+ const close = () => {
1441
+ if (!open)
1442
+ return;
1443
+ open = false;
1444
+ options.onActivity?.();
1445
+ options.onClose?.();
1446
+ };
1447
+ res.once("finish", close);
1448
+ res.once("close", close);
1449
+ const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
1341
1450
  const headers = { ...upstreamRes.headers };
1342
1451
  const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin);
1343
1452
  if (location !== void 0)
@@ -1359,7 +1468,17 @@ Start it, or point Leglas somewhere else with --user-port.`);
1359
1468
  req.pipe(upstream);
1360
1469
  },
1361
1470
  upgrade(req, socket, head) {
1362
- const upstream = net.connect(port, host, () => {
1471
+ options.onActivity?.();
1472
+ options.onOpen?.();
1473
+ let open = true;
1474
+ const closeActivity = () => {
1475
+ if (!open)
1476
+ return;
1477
+ open = false;
1478
+ options.onActivity?.();
1479
+ options.onClose?.();
1480
+ };
1481
+ const upstream = net.connect(port, dialHost, () => {
1363
1482
  const headers = Object.entries(upstreamHeaders(req)).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value}\r
1364
1483
  `).join("");
1365
1484
  upstream.write(`${req.method} ${req.url} HTTP/1.1\r
@@ -1371,6 +1490,7 @@ ${headers}\r
1371
1490
  socket.pipe(upstream);
1372
1491
  });
1373
1492
  const shutdown = () => {
1493
+ closeActivity();
1374
1494
  upstream.destroy();
1375
1495
  socket.destroy();
1376
1496
  };
@@ -1381,6 +1501,62 @@ ${headers}\r
1381
1501
  }
1382
1502
  };
1383
1503
  }
1504
+ function startProxyServer(options) {
1505
+ return new Promise((resolve5, reject) => {
1506
+ let open = 0;
1507
+ const handler = createProxyHandler({
1508
+ ...options,
1509
+ onOpen: () => {
1510
+ open += 1;
1511
+ options.onOpen?.();
1512
+ },
1513
+ onClose: () => {
1514
+ open = Math.max(0, open - 1);
1515
+ options.onClose?.();
1516
+ }
1517
+ });
1518
+ const server = http.createServer((req, res) => {
1519
+ const address = server.address();
1520
+ const port = typeof address === "object" && address !== null ? address.port : 0;
1521
+ handler.request(req, res, `http://127.0.0.1:${port}`);
1522
+ });
1523
+ const sockets = /* @__PURE__ */ new Set();
1524
+ server.on("connection", (socket) => {
1525
+ sockets.add(socket);
1526
+ socket.once("close", () => sockets.delete(socket));
1527
+ });
1528
+ server.on("upgrade", (req, socket, head) => handler.upgrade(req, socket, head));
1529
+ const onError = (error) => {
1530
+ server.removeListener("listening", onListening);
1531
+ reject(error);
1532
+ };
1533
+ const onListening = () => {
1534
+ server.removeListener("error", onError);
1535
+ const address = server.address();
1536
+ const port = typeof address === "object" && address !== null ? address.port : 0;
1537
+ let closed = null;
1538
+ resolve5({
1539
+ active: () => open > 0,
1540
+ close: () => {
1541
+ if (closed !== null)
1542
+ return closed;
1543
+ closed = new Promise((done) => {
1544
+ for (const socket of sockets)
1545
+ socket.destroy();
1546
+ sockets.clear();
1547
+ server.closeAllConnections();
1548
+ server.close(() => done());
1549
+ });
1550
+ return closed;
1551
+ },
1552
+ url: `http://127.0.0.1:${port}`
1553
+ });
1554
+ };
1555
+ server.once("error", onError);
1556
+ server.once("listening", onListening);
1557
+ server.listen(0, "127.0.0.1");
1558
+ });
1559
+ }
1384
1560
 
1385
1561
  // ../server/dist/browser.js
1386
1562
  import { randomBytes } from "crypto";
@@ -1448,12 +1624,12 @@ function buildNumber(entry) {
1448
1624
  const digits = /(\d+)\s*$/.exec(entry)?.[1];
1449
1625
  return digits === void 0 ? 0 : Number(digits);
1450
1626
  }
1451
- function cacheRoots(platform, home, readdir4 = readableDirectories) {
1627
+ function cacheRoots(platform, home, readdir5 = readableDirectories) {
1452
1628
  const playwright = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
1453
1629
  const puppeteer = join4(home, ".cache", "puppeteer");
1454
1630
  const newestFirst = (dir, keep = () => true) => ({
1455
1631
  root: dir,
1456
- entries: readdir4(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
1632
+ entries: readdir5(dir).filter(keep).sort((left, right) => buildNumber(right) - buildNumber(left))
1457
1633
  });
1458
1634
  return [
1459
1635
  newestFirst(playwright, (entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")),
@@ -1474,13 +1650,13 @@ function findBrowser(search = {}) {
1474
1650
  const home = search.home ?? homedir();
1475
1651
  const exists = search.exists ?? existsSync2;
1476
1652
  const onPath = search.onPath ?? ((name) => firstOnPath(name, env));
1477
- const readdir4 = search.readdir ?? readableDirectories;
1653
+ const readdir5 = search.readdir ?? readableDirectories;
1478
1654
  const firstExisting = (paths) => paths.find((path) => exists(path)) ?? null;
1479
1655
  for (const candidate of [env.LEGLAS_BROWSER, env.CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH]) {
1480
1656
  if (typeof candidate === "string" && candidate !== "" && exists(candidate))
1481
1657
  return candidate;
1482
1658
  }
1483
- const caches = cacheRoots(platform, home, readdir4);
1659
+ const caches = cacheRoots(platform, home, readdir5);
1484
1660
  const shell = firstExisting(caches.flatMap(({ root, entries }) => entries.flatMap((entry) => HEADLESS_SHELL.map((rest) => join4(root, entry, ...rest)))));
1485
1661
  if (shell !== null)
1486
1662
  return shell;
@@ -1510,7 +1686,7 @@ function findBrowser(search = {}) {
1510
1686
  }
1511
1687
  if (platform === "darwin" || platform === "linux") {
1512
1688
  const playwrightRoot = platform === "darwin" ? join4(home, "Library", "Caches", "ms-playwright") : join4(home, ".cache", "ms-playwright");
1513
- const playwright = readdir4(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
1689
+ const playwright = readdir5(playwrightRoot).filter((entry) => entry.startsWith("chromium-") || entry.startsWith("chromium_headless_shell-")).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
1514
1690
  const root = join4(playwrightRoot, entry);
1515
1691
  return [
1516
1692
  ...FOR_TESTING.map((rest) => join4(root, ...rest)),
@@ -1527,7 +1703,7 @@ function findBrowser(search = {}) {
1527
1703
  const puppeteerCache = join4(home, ".cache", "puppeteer");
1528
1704
  for (const kind of ["chrome", "chrome-headless-shell"]) {
1529
1705
  const kindRoot = join4(puppeteerCache, kind);
1530
- const found = firstExisting(readdir4(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
1706
+ const found = firstExisting(readdir5(kindRoot).sort((left, right) => buildNumber(right) - buildNumber(left)).flatMap((entry) => {
1531
1707
  const root = join4(kindRoot, entry);
1532
1708
  return [...FOR_TESTING, ...HEADLESS_SHELL].map((rest) => join4(root, ...rest));
1533
1709
  }));
@@ -2745,6 +2921,141 @@ async function startAppProcess(options) {
2745
2921
  throw new Error(`${options.label} did not start within ${Math.round(readyTimeoutMs / 1e3)}s. Check that its dev command serves the port it is given.`);
2746
2922
  }
2747
2923
 
2924
+ // ../server/dist/branches.js
2925
+ var BRANCH_IDLE_MS = 10 * 60 * 1e3;
2926
+ var BRANCH_SWEEP_MS = 3e4;
2927
+ function publicBranchState(state) {
2928
+ if (state.status === "ready")
2929
+ return { status: "ready" };
2930
+ return state;
2931
+ }
2932
+ function createBranchRegistry(options) {
2933
+ const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
2934
+ const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
2935
+ const inflight = /* @__PURE__ */ new Map();
2936
+ const stopping = /* @__PURE__ */ new Map();
2937
+ const lastActivity = /* @__PURE__ */ new Map();
2938
+ const proxies = /* @__PURE__ */ new Map();
2939
+ const boot = options.startWorktree ?? startWorktree;
2940
+ const proxy = options.startProxy ?? startProxyServer;
2941
+ let closed = false;
2942
+ let stopPromise = null;
2943
+ const transition = (title, state) => {
2944
+ const previous = states.get(title);
2945
+ if (previous?.status === "starting" && state.status === "starting" && previous.phase === state.phase) {
2946
+ return previous;
2947
+ }
2948
+ states.set(title, state);
2949
+ options.onChange?.(title, state);
2950
+ return state;
2951
+ };
2952
+ const stopReady = (title, state, toIdle) => {
2953
+ const current = stopping.get(title);
2954
+ if (current !== void 0)
2955
+ return current;
2956
+ const pending = (async () => {
2957
+ await proxies.get(title)?.close().catch(() => {
2958
+ });
2959
+ await state.worktree.stop().catch(() => {
2960
+ });
2961
+ proxies.delete(title);
2962
+ lastActivity.delete(title);
2963
+ if (toIdle && !closed && states.get(title) === state) {
2964
+ transition(title, { status: "idle" });
2965
+ }
2966
+ })().finally(() => {
2967
+ stopping.delete(title);
2968
+ });
2969
+ stopping.set(title, pending);
2970
+ return pending;
2971
+ };
2972
+ const sweep = () => {
2973
+ const now = Date.now();
2974
+ for (const [title, state] of states) {
2975
+ const branchProxy = proxies.get(title);
2976
+ if (state.status !== "ready" || branchProxy === void 0 || stopping.has(title))
2977
+ continue;
2978
+ if (branchProxy.active()) {
2979
+ lastActivity.set(title, now);
2980
+ continue;
2981
+ }
2982
+ const seen = lastActivity.get(title) ?? now;
2983
+ if (now - seen >= BRANCH_IDLE_MS)
2984
+ void stopReady(title, state, true);
2985
+ }
2986
+ };
2987
+ const sweepTimer = setInterval(sweep, BRANCH_SWEEP_MS);
2988
+ sweepTimer.unref();
2989
+ const begin = (title) => {
2990
+ const preview = previews.get(title);
2991
+ const current = states.get(title);
2992
+ if (preview === void 0 || current === void 0 || closed)
2993
+ return void 0;
2994
+ if (current.status === "starting")
2995
+ return inflight.get(title);
2996
+ if (current.status === "ready")
2997
+ return Promise.resolve(current);
2998
+ transition(title, { status: "starting", phase: "checking out" });
2999
+ let checkout;
3000
+ try {
3001
+ checkout = Promise.resolve(boot({
3002
+ cwd: options.cwd,
3003
+ branch: preview.branch,
3004
+ installCommand: options.installCommand,
3005
+ devCommand: options.devCommand ?? "",
3006
+ onLog: (line) => {
3007
+ transition(title, {
3008
+ status: "starting",
3009
+ phase: line.startsWith("installing ") ? "installing" : "starting"
3010
+ });
3011
+ }
3012
+ }));
3013
+ } catch (error) {
3014
+ checkout = Promise.reject(error);
3015
+ }
3016
+ const starting = checkout.then(async (worktree) => {
3017
+ transition(title, { status: "starting", phase: "starting" });
3018
+ try {
3019
+ const branchProxy = await proxy({
3020
+ target: worktree.url,
3021
+ onActivity: () => lastActivity.set(title, Date.now())
3022
+ });
3023
+ proxies.set(title, branchProxy);
3024
+ lastActivity.set(title, Date.now());
3025
+ return transition(title, { status: "ready", worktree });
3026
+ } catch (error) {
3027
+ await worktree.stop().catch(() => {
3028
+ });
3029
+ throw error;
3030
+ }
3031
+ }).catch((error) => transition(title, {
3032
+ status: "failed",
3033
+ reason: error instanceof Error ? error.message : String(error)
3034
+ })).finally(() => {
3035
+ inflight.delete(title);
3036
+ });
3037
+ inflight.set(title, starting);
3038
+ return starting;
3039
+ };
3040
+ return {
3041
+ state: (title) => states.get(title),
3042
+ url: (title) => proxies.get(title)?.url,
3043
+ start: begin,
3044
+ stop: () => {
3045
+ if (stopPromise !== null)
3046
+ return stopPromise;
3047
+ closed = true;
3048
+ clearInterval(sweepTimer);
3049
+ stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
3050
+ await Promise.allSettled([...stopping.values()]);
3051
+ const ready = [...states.entries()].filter((entry) => entry[1].status === "ready" && proxies.has(entry[0]));
3052
+ await Promise.all(ready.map(([title, state]) => stopReady(title, state, false)));
3053
+ });
3054
+ return stopPromise;
3055
+ }
3056
+ };
3057
+ }
3058
+
2748
3059
  // ../server/dist/failure.js
2749
3060
  var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
2750
3061
  var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
@@ -4835,6 +5146,7 @@ function resolveTitle(input, titles, renames) {
4835
5146
 
4836
5147
  // ../server/dist/server.js
4837
5148
  import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
5149
+ import { createHash as createHash2 } from "crypto";
4838
5150
  import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
4839
5151
  import http2 from "http";
4840
5152
  import net3 from "net";
@@ -4913,6 +5225,30 @@ function sendJson(res, status, body) {
4913
5225
  });
4914
5226
  res.end(payload);
4915
5227
  }
5228
+ function etagMatches(value, etag) {
5229
+ if (value === void 0)
5230
+ return false;
5231
+ const values = Array.isArray(value) ? value : [value];
5232
+ return values.some((header) => header.split(",").some((candidate) => {
5233
+ const tag = candidate.trim();
5234
+ return tag === "*" || tag === etag || tag === `W/${etag}`;
5235
+ }));
5236
+ }
5237
+ function sendConditionalJson(req, res, body) {
5238
+ const payload = JSON.stringify(body);
5239
+ const etag = `"${createHash2("sha256").update(payload).digest("base64url")}"`;
5240
+ if (etagMatches(req.headers["if-none-match"], etag)) {
5241
+ res.writeHead(304, { etag, "cache-control": "private, no-cache" });
5242
+ res.end();
5243
+ return;
5244
+ }
5245
+ res.writeHead(200, {
5246
+ "content-type": "application/json; charset=utf-8",
5247
+ "cache-control": "private, no-cache",
5248
+ etag
5249
+ });
5250
+ res.end(payload);
5251
+ }
4916
5252
  var CAPTURE_DEADLINE_MS = 15e3;
4917
5253
  var CAPTURE_LOAD_MS = Math.floor(CAPTURE_DEADLINE_MS * LOAD_SHARE);
4918
5254
  function captureSlug(title) {
@@ -5292,6 +5628,33 @@ async function startServer(options) {
5292
5628
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
5293
5629
  const browserPool = options.pool ?? createBrowserPool();
5294
5630
  const live = options.live ?? createLiveHub();
5631
+ const branches = createBranchRegistry({
5632
+ cwd,
5633
+ previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
5634
+ installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
5635
+ devCommand: config?.devCommand,
5636
+ onChange: () => live.nudge("config"),
5637
+ ...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
5638
+ });
5639
+ const previewForConfig = (preview) => {
5640
+ if (preview.branch === void 0)
5641
+ return preview;
5642
+ const state = branches.state(preview.title) ?? { status: "idle" };
5643
+ const { url: route, ...withoutUrl } = preview;
5644
+ const branchUrl = branches.url(preview.title);
5645
+ return state.status === "ready" ? {
5646
+ ...withoutUrl,
5647
+ url: `${branchUrl ?? state.worktree.url}${route}`,
5648
+ state: publicBranchState(state)
5649
+ } : { ...withoutUrl, state: publicBranchState(state) };
5650
+ };
5651
+ const previewsForConfig = (previews) => previews.map(previewForConfig);
5652
+ const readyPreview = (preview) => {
5653
+ if (preview.branch === void 0)
5654
+ return preview;
5655
+ const state = branches.state(preview.title);
5656
+ return state?.status === "ready" ? { ...preview, url: `${branches.url(preview.title) ?? state.worktree.url}${preview.url}` } : null;
5657
+ };
5295
5658
  if (options.pool === void 0) {
5296
5659
  void reapOrphanedBrowsers().catch(() => {
5297
5660
  });
@@ -5325,7 +5688,7 @@ async function startServer(options) {
5325
5688
  }
5326
5689
  return Promise.resolve(agentsCache.agents);
5327
5690
  };
5328
- const livePreviews = async () => {
5691
+ const livePreviewDefinitions = async () => {
5329
5692
  const localRead = await readLocalPreviews(cwd).catch(() => null);
5330
5693
  const local = localRead?.errors.length === 0 ? localRead.previews : [];
5331
5694
  const localTitles = new Set(local.map((entry) => entry.title));
@@ -5335,6 +5698,7 @@ async function startServer(options) {
5335
5698
  const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
5336
5699
  return [...boot, ...fresh];
5337
5700
  };
5701
+ const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
5338
5702
  void probeAgents().catch(() => {
5339
5703
  });
5340
5704
  const server = http2.createServer((req, res) => {
@@ -5352,11 +5716,11 @@ async function startServer(options) {
5352
5716
  errors.push(notice);
5353
5717
  return void readLocalPreviews(cwd).then(({ previews: local, errors: localErrors }) => {
5354
5718
  if (localErrors.length > 0) {
5355
- return sendJson(res, 200, {
5719
+ return sendConditionalJson(req, res, {
5356
5720
  project,
5357
5721
  devServer: target,
5358
5722
  scanPreviews: config?.scanPreviews ?? true,
5359
- previews: boot,
5723
+ previews: previewsForConfig(boot),
5360
5724
  errors,
5361
5725
  warnings: configWarnings
5362
5726
  });
@@ -5365,23 +5729,58 @@ async function startServer(options) {
5365
5729
  const currentBoot = boot.filter((preview) => preview.local !== true || localTitles.has(preview.title));
5366
5730
  const known = new Set(currentBoot.map((preview) => preview.title));
5367
5731
  const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
5368
- sendJson(res, 200, {
5732
+ sendConditionalJson(req, res, {
5369
5733
  project,
5370
5734
  devServer: target,
5371
5735
  scanPreviews: config?.scanPreviews ?? true,
5372
- previews: [...currentBoot, ...fresh],
5736
+ previews: previewsForConfig([...currentBoot, ...fresh]),
5373
5737
  errors,
5374
5738
  warnings: configWarnings
5375
5739
  });
5376
- }).catch(() => sendJson(res, 200, {
5740
+ }).catch(() => sendConditionalJson(req, res, {
5377
5741
  project,
5378
5742
  devServer: target,
5379
5743
  scanPreviews: config?.scanPreviews ?? true,
5380
- previews: boot,
5744
+ previews: previewsForConfig(boot),
5381
5745
  errors,
5382
5746
  warnings: configWarnings
5383
5747
  }));
5384
5748
  }
5749
+ if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
5750
+ let body = "";
5751
+ req.on("data", (chunk) => body += chunk);
5752
+ return void req.on("end", async () => {
5753
+ const parsed2 = jsonBody(body);
5754
+ if (parsed2 === null) {
5755
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
5756
+ }
5757
+ if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
5758
+ return sendJson(res, 400, { ok: false, error: "Body needs a direction title." });
5759
+ }
5760
+ const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed2.title);
5761
+ if (preview === void 0) {
5762
+ return sendJson(res, 404, { ok: false, error: "No such direction." });
5763
+ }
5764
+ if (preview.branch === void 0) {
5765
+ return sendJson(res, 400, {
5766
+ ok: false,
5767
+ error: `"${preview.title}" is not a branch preview.`
5768
+ });
5769
+ }
5770
+ if (config?.devCommand === void 0) {
5771
+ return sendJson(res, 400, {
5772
+ ok: false,
5773
+ error: `"${preview.title}" cannot start because the config sets no devCommand.`
5774
+ });
5775
+ }
5776
+ void branches.start(preview.title);
5777
+ const state = branches.state(preview.title);
5778
+ if (state === void 0) {
5779
+ return sendJson(res, 404, { ok: false, error: "No such branch preview." });
5780
+ }
5781
+ return sendJson(res, 200, { ok: true, state: publicBranchState(state) });
5782
+ });
5783
+ }
5385
5784
  if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
5386
5785
  let body = "";
5387
5786
  req.on("data", (chunk) => body += chunk);
@@ -5773,7 +6172,7 @@ async function startServer(options) {
5773
6172
  waiting: null,
5774
6173
  failedIds: []
5775
6174
  };
5776
- return void readRequests(cwd).then((requests) => sendJson(res, 200, {
6175
+ return void readRequests(cwd).then((requests) => sendConditionalJson(req, res, {
5777
6176
  requests: requests.map(({ id, title, intent, mode, status, failure, notes }) => ({
5778
6177
  id,
5779
6178
  title,
@@ -5876,7 +6275,7 @@ async function startServer(options) {
5876
6275
  });
5877
6276
  }
5878
6277
  if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "GET") {
5879
- return void readAnnotations(cwd).then((annotations) => sendJson(res, 200, { annotations }));
6278
+ return void readAnnotations(cwd).then((annotations) => sendConditionalJson(req, res, { annotations }));
5880
6279
  }
5881
6280
  if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
5882
6281
  if (!hasJsonBody(req)) {
@@ -6002,7 +6401,7 @@ async function startServer(options) {
6002
6401
  });
6003
6402
  }
6004
6403
  if (path === `${LEGLAS_PREFIX}/api/health`) {
6005
- return void probe(target).then((reachable) => sendJson(res, 200, { devServer: target, reachable, cwd }));
6404
+ return void probe(target).then((reachable) => sendConditionalJson(req, res, { devServer: target, reachable, cwd }));
6006
6405
  }
6007
6406
  if (path.startsWith(`${FILES_PREFIX}/`)) {
6008
6407
  const rest = path.slice(FILES_PREFIX.length + 1);
@@ -6076,7 +6475,12 @@ async function startServer(options) {
6076
6475
  return closePromise;
6077
6476
  liveFiles.close();
6078
6477
  liveHealth.close();
6079
- closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
6478
+ closePromise = Promise.all([
6479
+ branches.stop(),
6480
+ runner.stop(),
6481
+ browserPool.close(),
6482
+ live.close()
6483
+ ]).then(() => new Promise((done) => {
6080
6484
  for (const socket of sockets)
6081
6485
  socket.destroy();
6082
6486
  sockets.clear();
@@ -6449,6 +6853,12 @@ out of the ignored directory, deletes the rest of the exploration, and drops
6449
6853
  them from the rail. Then change their component to use the kept component
6450
6854
  instead of the switcher.
6451
6855
 
6856
+ Keeping also writes what the exploration was into \`design-log/\`, which is
6857
+ committed. Before exploring a surface, read \`npx leglas log --json\` and any
6858
+ entry for that surface: it says what was already tried there, in the user's own
6859
+ words, and which direction won. Proposing something that was already rejected
6860
+ wastes their time, and the record is there so you do not have to ask.
6861
+
6452
6862
  Useful to know:
6453
6863
 
6454
6864
  - \`.leglas/\` is gitignored. Exploration is disposable and nothing in there
@@ -6548,7 +6958,7 @@ async function runInit(options, deps) {
6548
6958
 
6549
6959
  // src/run-keep.ts
6550
6960
  import { existsSync as existsSync4 } from "fs";
6551
- import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
6961
+ import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
6552
6962
  import { dirname as dirname9, join as join15 } from "path";
6553
6963
 
6554
6964
  // src/keep.ts
@@ -6631,6 +7041,29 @@ function renameExport(source, to) {
6631
7041
  to
6632
7042
  );
6633
7043
  }
7044
+ async function writeLogEntry(options) {
7045
+ const entry = composeEntry({
7046
+ surface: options.surface,
7047
+ won: options.won,
7048
+ previews: options.previews,
7049
+ requests: await readRequests(options.cwd),
7050
+ annotations: await readAnnotations(options.cwd),
7051
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
7052
+ });
7053
+ const dir = join15(options.cwd, options.logDir);
7054
+ await mkdir9(dir, { recursive: true });
7055
+ const file = join15(dir, `${entry.slug}.md`);
7056
+ await writeFile10(file, entry.markdown, "utf8");
7057
+ if (entry.pictures.length > 0) {
7058
+ const pictureDir = join15(dir, entry.slug);
7059
+ await mkdir9(pictureDir, { recursive: true });
7060
+ for (const picture of entry.pictures) {
7061
+ await copyFile2(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
7062
+ });
7063
+ }
7064
+ }
7065
+ return `${options.logDir}/${entry.slug}.md`;
7066
+ }
6634
7067
  async function runKeep(options, deps) {
6635
7068
  const loaded = await loadConfig(options.cwd);
6636
7069
  const local = await readLocalPreviews(options.cwd);
@@ -6659,6 +7092,20 @@ async function runKeep(options, deps) {
6659
7092
  const source = await readFile13(from, "utf8");
6660
7093
  await mkdir9(dirname9(to), { recursive: true });
6661
7094
  await writeFile10(to, renameExport(source, plan.exportName), "utf8");
7095
+ const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
7096
+ let logged = null;
7097
+ let logError = null;
7098
+ try {
7099
+ logged = await writeLogEntry({
7100
+ cwd: options.cwd,
7101
+ logDir: loaded.config?.logDir ?? DEFAULT_LOG_DIR,
7102
+ surface,
7103
+ won: { title: resolved.title, to: plan.move.to },
7104
+ previews: previews.filter((preview) => plan.dropTitles.includes(preview.title))
7105
+ });
7106
+ } catch (error) {
7107
+ logError = error instanceof Error ? error.message : String(error);
7108
+ }
6662
7109
  await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
6663
7110
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
6664
7111
  if (options.json) {
@@ -6670,6 +7117,8 @@ async function runKeep(options, deps) {
6670
7117
  exportName: plan.exportName,
6671
7118
  removed: plan.removeDir,
6672
7119
  droppedPreviews: dropped,
7120
+ logged,
7121
+ logError,
6673
7122
  instructions: plan.instructions
6674
7123
  })
6675
7124
  );
@@ -6677,6 +7126,8 @@ async function runKeep(options, deps) {
6677
7126
  }
6678
7127
  deps.log(` kept ${plan.move.to}`);
6679
7128
  deps.log(` removed ${plan.removeDir}`);
7129
+ if (logged !== null) deps.log(` logged ${logged}`);
7130
+ if (logError !== null) deps.error(` The decision log could not be written: ${logError}`);
6680
7131
  if (dropped > 0) {
6681
7132
  deps.log(` dropped ${dropped} direction${dropped === 1 ? "" : "s"} from the rail`);
6682
7133
  }
@@ -6773,17 +7224,67 @@ async function runNew(options, deps) {
6773
7224
  return { exitCode: 0, written };
6774
7225
  }
6775
7226
 
6776
- // src/run-previews.ts
6777
- import { readFile as readFile15, writeFile as writeFile12 } from "fs/promises";
7227
+ // src/run-log.ts
7228
+ import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
6778
7229
  import { join as join17 } from "path";
7230
+ function headline(markdown) {
7231
+ const first = markdown.split("\n", 1)[0] ?? "";
7232
+ return first.replace(/^#\s*/, "").trim();
7233
+ }
7234
+ async function runLog(options, deps) {
7235
+ const loaded = await loadConfig(options.cwd);
7236
+ const dir = loaded.config?.logDir ?? DEFAULT_LOG_DIR;
7237
+ let names;
7238
+ try {
7239
+ names = (await readdir4(join17(options.cwd, dir))).filter((name) => name.endsWith(".md")).sort().reverse();
7240
+ } catch {
7241
+ names = [];
7242
+ }
7243
+ if (options.entry !== null) {
7244
+ const wanted = options.entry.replace(/\.md$/, "");
7245
+ const found = names.find((name) => name === `${wanted}.md`);
7246
+ if (found === void 0) {
7247
+ const error = `No entry called ${JSON.stringify(options.entry)} in ${dir}.`;
7248
+ if (options.json) deps.log(JSON.stringify({ ok: false, error }));
7249
+ else deps.error(error);
7250
+ return { exitCode: 1 };
7251
+ }
7252
+ const markdown = await readFile15(join17(options.cwd, dir, found), "utf8");
7253
+ if (options.json) deps.log(JSON.stringify({ ok: true, entry: wanted, markdown }));
7254
+ else deps.log(markdown.trimEnd());
7255
+ return { exitCode: 0 };
7256
+ }
7257
+ const entries = await Promise.all(
7258
+ names.map(async (name) => ({
7259
+ entry: name.replace(/\.md$/, ""),
7260
+ title: headline(await readFile15(join17(options.cwd, dir, name), "utf8")),
7261
+ file: `${dir}/${name}`
7262
+ }))
7263
+ );
7264
+ if (options.json) {
7265
+ deps.log(JSON.stringify({ ok: true, dir, entries }));
7266
+ return { exitCode: 0 };
7267
+ }
7268
+ if (entries.length === 0) {
7269
+ deps.log(` No decisions recorded yet. One is written each time you run leglas keep.`);
7270
+ return { exitCode: 0 };
7271
+ }
7272
+ const width = Math.max(...entries.map((entry) => entry.entry.length));
7273
+ for (const entry of entries) deps.log(` ${entry.entry.padEnd(width)} ${entry.title}`);
7274
+ return { exitCode: 0 };
7275
+ }
7276
+
7277
+ // src/run-previews.ts
7278
+ import { readFile as readFile16, writeFile as writeFile12 } from "fs/promises";
7279
+ import { join as join18 } from "path";
6779
7280
  function envelope(deps, ok, body) {
6780
7281
  deps.log(JSON.stringify({ ok, ...body }));
6781
7282
  }
6782
7283
  async function ensureIgnored(cwd) {
6783
- const path = join17(cwd, ".gitignore");
7284
+ const path = join18(cwd, ".gitignore");
6784
7285
  let current = null;
6785
7286
  try {
6786
- current = await readFile15(path, "utf8");
7287
+ current = await readFile16(path, "utf8");
6787
7288
  } catch {
6788
7289
  current = null;
6789
7290
  }
@@ -7110,15 +7611,15 @@ async function runShow(options, deps) {
7110
7611
 
7111
7612
  // src/run-watch.ts
7112
7613
  import { spawn as spawn3 } from "child_process";
7113
- import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
7114
- import { dirname as dirname11, join as join18 } from "path";
7614
+ import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile13 } from "fs/promises";
7615
+ import { dirname as dirname11, join as join19 } from "path";
7115
7616
  var POLL_MS2 = 2e3;
7116
7617
  var HEARTBEAT_TIMEOUT_MS = 1e3;
7117
7618
  async function saveTemplate(cwd, run4) {
7118
- const path = join18(cwd, WATCH_PATH);
7619
+ const path = join19(cwd, WATCH_PATH);
7119
7620
  let config = {};
7120
7621
  try {
7121
- const parsed2 = JSON.parse(await readFile16(path, "utf8"));
7622
+ const parsed2 = JSON.parse(await readFile17(path, "utf8"));
7122
7623
  if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
7123
7624
  config = parsed2;
7124
7625
  }
@@ -7267,7 +7768,7 @@ async function runWatch(options, deps) {
7267
7768
  import { existsSync as existsSync6 } from "fs";
7268
7769
  import { realpath as realpath4 } from "fs/promises";
7269
7770
  import { createRequire } from "module";
7270
- import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
7771
+ import { basename as basename6, dirname as dirname12, join as join20, relative as relative5, resolve as resolve4 } from "path";
7271
7772
  import { fileURLToPath } from "url";
7272
7773
 
7273
7774
  // src/dev-server-owner.ts
@@ -7353,8 +7854,8 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
7353
7854
 
7354
7855
  // src/run.ts
7355
7856
  function findShellDir() {
7356
- const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
7357
- if (existsSync6(join19(bundled, "index.html"))) return bundled;
7857
+ const bundled = join20(dirname12(fileURLToPath(import.meta.url)), "shell");
7858
+ if (existsSync6(join20(bundled, "index.html"))) return bundled;
7358
7859
  try {
7359
7860
  const require2 = createRequire(import.meta.url);
7360
7861
  return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
@@ -7368,7 +7869,7 @@ function shellWord(value) {
7368
7869
  return `'${value.replaceAll("'", `'\\''`)}'`;
7369
7870
  }
7370
7871
  function embeddedLeglasCommand() {
7371
- const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
7872
+ const entry = join20(dirname12(fileURLToPath(import.meta.url)), "bin.js");
7372
7873
  if (!existsSync6(entry)) return "npx -y leglas";
7373
7874
  return [process.execPath, entry].map(shellWord).join(" ");
7374
7875
  }
@@ -7377,8 +7878,7 @@ async function run3(options, deps) {
7377
7878
  const local = await readLocalPreviews(options.cwd);
7378
7879
  let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
7379
7880
  const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
7380
- const worktrees = [];
7381
- const worktreeErrors = [];
7881
+ const previewErrors = [];
7382
7882
  const previews = [];
7383
7883
  let app = null;
7384
7884
  const needsApp = (merged?.previews ?? []).some(
@@ -7395,15 +7895,15 @@ async function run3(options, deps) {
7395
7895
  devServer = app.url;
7396
7896
  merged.devServer = app.url;
7397
7897
  } catch (error) {
7398
- worktreeErrors.push(error instanceof Error ? error.message : String(error));
7898
+ previewErrors.push(error instanceof Error ? error.message : String(error));
7399
7899
  }
7400
7900
  }
7401
7901
  const fileMounts = /* @__PURE__ */ new Map();
7402
7902
  for (const preview of merged?.previews ?? []) {
7403
7903
  if (preview.file !== void 0) {
7404
- const absolute = join19(options.cwd, preview.file);
7904
+ const absolute = join20(options.cwd, preview.file);
7405
7905
  if (!existsSync6(absolute)) {
7406
- worktreeErrors.push(
7906
+ previewErrors.push(
7407
7907
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
7408
7908
  );
7409
7909
  continue;
@@ -7419,29 +7919,7 @@ async function run3(options, deps) {
7419
7919
  });
7420
7920
  continue;
7421
7921
  }
7422
- if (preview.branch === void 0) {
7423
- previews.push(preview);
7424
- continue;
7425
- }
7426
- if (merged?.devCommand === void 0) {
7427
- worktreeErrors.push(
7428
- `"${preview.title}" names branch ${preview.branch}, but the config sets no devCommand, so Leglas cannot start that checkout. Add devCommand (with {port}) to the config.`
7429
- );
7430
- continue;
7431
- }
7432
- if (!options.json) deps.log(` starting ${preview.branch}\u2026`);
7433
- try {
7434
- const worktree = await startWorktree({
7435
- cwd: options.cwd,
7436
- branch: preview.branch,
7437
- installCommand: merged.installCommand,
7438
- devCommand: merged.devCommand
7439
- });
7440
- worktrees.push(worktree);
7441
- previews.push({ ...preview, url: `${worktree.url}${preview.url}` });
7442
- } catch (error) {
7443
- worktreeErrors.push(error instanceof Error ? error.message : String(error));
7444
- }
7922
+ previews.push(preview);
7445
7923
  }
7446
7924
  const config = merged === null ? null : { ...merged, previews };
7447
7925
  const configWarnings = [];
@@ -7451,7 +7929,7 @@ async function run3(options, deps) {
7451
7929
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7452
7930
  const serverPromise = startServer({
7453
7931
  config,
7454
- configErrors: [...loaded.errors, ...local.errors, ...worktreeErrors],
7932
+ configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
7455
7933
  configWarnings,
7456
7934
  fileMounts,
7457
7935
  shellDir: findShellDir(),
@@ -7490,9 +7968,9 @@ async function run3(options, deps) {
7490
7968
  );
7491
7969
  deps.log(`config ${configLabel}`);
7492
7970
  deps.log(` ${previewCount} preview${previewCount === 1 ? "" : "s"}`);
7493
- if (loaded.errors.length + worktreeErrors.length > 0) {
7971
+ if (loaded.errors.length + previewErrors.length > 0) {
7494
7972
  deps.log("");
7495
- for (const error of [...loaded.errors, ...worktreeErrors]) deps.log(` ! ${error}`);
7973
+ for (const error of [...loaded.errors, ...previewErrors]) deps.log(` ! ${error}`);
7496
7974
  deps.log(" Fix the config and reload; Leglas will pick it up on restart.");
7497
7975
  }
7498
7976
  if (configWarnings.length > 0) {
@@ -7515,8 +7993,6 @@ async function run3(options, deps) {
7515
7993
  devServer,
7516
7994
  previewCount,
7517
7995
  stop: async () => {
7518
- await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
7519
- })));
7520
7996
  await app?.stop().catch(() => {
7521
7997
  });
7522
7998
  await server.close();
@@ -7547,6 +8023,7 @@ Usage
7547
8023
  leglas classify Decide where a direction should live
7548
8024
  leglas add --title T --url U Register a preview on this machine
7549
8025
  leglas list Show every preview, shared and local
8026
+ leglas log [entry] What past explorations decided
7550
8027
  leglas show <title> Everything Leglas knows about one direction
7551
8028
  leglas requests Show change requests made from the interface
7552
8029
  leglas watch --run "<cmd>" Hand each request to your agent as it arrives
@@ -7684,6 +8161,13 @@ if (parsed.kind === "watch") {
7684
8161
  );
7685
8162
  process.exit(outcome.exitCode);
7686
8163
  }
8164
+ if (parsed.kind === "log") {
8165
+ const outcome = await runLog(
8166
+ { entry: parsed.entry, json: parsed.json, cwd: process.cwd() },
8167
+ previewDeps
8168
+ );
8169
+ process.exit(outcome.exitCode);
8170
+ }
7687
8171
  if (parsed.kind === "list") {
7688
8172
  const outcome = await runList({ json: parsed.json, cwd: process.cwd() }, previewDeps);
7689
8173
  process.exit(outcome.exitCode);