leglas 0.7.2 → 0.7.4

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
@@ -2624,18 +2624,23 @@ async function freePort() {
2624
2624
  });
2625
2625
  });
2626
2626
  }
2627
- function answers(port) {
2628
- return new Promise((resolve5) => {
2629
- const socket = net2.connect(port, "127.0.0.1");
2627
+ var LOOPBACK = ["127.0.0.1", "::1"];
2628
+ function forUrl(host) {
2629
+ return host.includes(":") ? `[${host}]` : host;
2630
+ }
2631
+ async function answeringHost(port) {
2632
+ const reached = await Promise.all(LOOPBACK.map((host) => new Promise((resolve5) => {
2633
+ const socket = net2.connect({ port, host });
2630
2634
  const settle = (value) => {
2631
2635
  socket.destroy();
2632
2636
  resolve5(value);
2633
2637
  };
2634
2638
  socket.setTimeout(400);
2635
- socket.once("connect", () => settle(true));
2636
- socket.once("timeout", () => settle(false));
2637
- socket.once("error", () => settle(false));
2638
- });
2639
+ socket.once("connect", () => settle(host));
2640
+ socket.once("timeout", () => settle(null));
2641
+ socket.once("error", () => settle(null));
2642
+ })));
2643
+ return reached.find((host) => host !== null) ?? null;
2639
2644
  }
2640
2645
  async function startWorktree(options) {
2641
2646
  const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
@@ -2730,8 +2735,9 @@ async function startAppProcess(options) {
2730
2735
  if (exited !== null) {
2731
2736
  throw new Error(`${options.label} did not start: its dev command exited with code ${exited}.`);
2732
2737
  }
2733
- if (await answers(port)) {
2734
- return { port, url: `http://127.0.0.1:${port}`, stop };
2738
+ const host = await answeringHost(port);
2739
+ if (host !== null) {
2740
+ return { port, url: `http://${forUrl(host)}:${port}`, stop };
2735
2741
  }
2736
2742
  await new Promise((resolve5) => setTimeout(resolve5, 250));
2737
2743
  }
@@ -2739,6 +2745,84 @@ async function startAppProcess(options) {
2739
2745
  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.`);
2740
2746
  }
2741
2747
 
2748
+ // ../server/dist/branches.js
2749
+ function publicBranchState(state) {
2750
+ if (state.status === "ready")
2751
+ return { status: "ready" };
2752
+ return state;
2753
+ }
2754
+ function createBranchRegistry(options) {
2755
+ const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
2756
+ const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
2757
+ const inflight = /* @__PURE__ */ new Map();
2758
+ const boot = options.startWorktree ?? startWorktree;
2759
+ let closed = false;
2760
+ let stopPromise = null;
2761
+ const transition = (title, state) => {
2762
+ const previous = states.get(title);
2763
+ if (previous?.status === "starting" && state.status === "starting" && previous.phase === state.phase) {
2764
+ return previous;
2765
+ }
2766
+ states.set(title, state);
2767
+ options.onChange?.(title, state);
2768
+ return state;
2769
+ };
2770
+ const begin = (title) => {
2771
+ const preview = previews.get(title);
2772
+ const current = states.get(title);
2773
+ if (preview === void 0 || current === void 0 || closed)
2774
+ return void 0;
2775
+ if (current.status === "starting")
2776
+ return inflight.get(title);
2777
+ if (current.status === "ready")
2778
+ return Promise.resolve(current);
2779
+ transition(title, { status: "starting", phase: "checking out" });
2780
+ let checkout;
2781
+ try {
2782
+ checkout = Promise.resolve(boot({
2783
+ cwd: options.cwd,
2784
+ branch: preview.branch,
2785
+ installCommand: options.installCommand,
2786
+ devCommand: options.devCommand ?? "",
2787
+ onLog: (line) => {
2788
+ transition(title, {
2789
+ status: "starting",
2790
+ phase: line.startsWith("installing ") ? "installing" : "starting"
2791
+ });
2792
+ }
2793
+ }));
2794
+ } catch (error) {
2795
+ checkout = Promise.reject(error);
2796
+ }
2797
+ const starting = checkout.then((worktree) => {
2798
+ transition(title, { status: "starting", phase: "starting" });
2799
+ return transition(title, { status: "ready", worktree });
2800
+ }).catch((error) => transition(title, {
2801
+ status: "failed",
2802
+ reason: error instanceof Error ? error.message : String(error)
2803
+ })).finally(() => {
2804
+ inflight.delete(title);
2805
+ });
2806
+ inflight.set(title, starting);
2807
+ return starting;
2808
+ };
2809
+ return {
2810
+ state: (title) => states.get(title),
2811
+ start: begin,
2812
+ stop: () => {
2813
+ if (stopPromise !== null)
2814
+ return stopPromise;
2815
+ closed = true;
2816
+ stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
2817
+ const worktrees = [...states.values()].filter((state) => state.status === "ready").map((state) => state.worktree);
2818
+ await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
2819
+ })));
2820
+ });
2821
+ return stopPromise;
2822
+ }
2823
+ };
2824
+ }
2825
+
2742
2826
  // ../server/dist/failure.js
2743
2827
  var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
2744
2828
  var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
@@ -5286,6 +5370,32 @@ async function startServer(options) {
5286
5370
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
5287
5371
  const browserPool = options.pool ?? createBrowserPool();
5288
5372
  const live = options.live ?? createLiveHub();
5373
+ const branches = createBranchRegistry({
5374
+ cwd,
5375
+ previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
5376
+ installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
5377
+ devCommand: config?.devCommand,
5378
+ onChange: () => live.nudge("config"),
5379
+ ...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
5380
+ });
5381
+ const previewForConfig = (preview) => {
5382
+ if (preview.branch === void 0)
5383
+ return preview;
5384
+ const state = branches.state(preview.title) ?? { status: "idle" };
5385
+ const { url: route, ...withoutUrl } = preview;
5386
+ return state.status === "ready" ? {
5387
+ ...withoutUrl,
5388
+ url: `${state.worktree.url}${route}`,
5389
+ state: publicBranchState(state)
5390
+ } : { ...withoutUrl, state: publicBranchState(state) };
5391
+ };
5392
+ const previewsForConfig = (previews) => previews.map(previewForConfig);
5393
+ const readyPreview = (preview) => {
5394
+ if (preview.branch === void 0)
5395
+ return preview;
5396
+ const state = branches.state(preview.title);
5397
+ return state?.status === "ready" ? { ...preview, url: `${state.worktree.url}${preview.url}` } : null;
5398
+ };
5289
5399
  if (options.pool === void 0) {
5290
5400
  void reapOrphanedBrowsers().catch(() => {
5291
5401
  });
@@ -5319,7 +5429,7 @@ async function startServer(options) {
5319
5429
  }
5320
5430
  return Promise.resolve(agentsCache.agents);
5321
5431
  };
5322
- const livePreviews = async () => {
5432
+ const livePreviewDefinitions = async () => {
5323
5433
  const localRead = await readLocalPreviews(cwd).catch(() => null);
5324
5434
  const local = localRead?.errors.length === 0 ? localRead.previews : [];
5325
5435
  const localTitles = new Set(local.map((entry) => entry.title));
@@ -5329,6 +5439,7 @@ async function startServer(options) {
5329
5439
  const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
5330
5440
  return [...boot, ...fresh];
5331
5441
  };
5442
+ const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
5332
5443
  void probeAgents().catch(() => {
5333
5444
  });
5334
5445
  const server = http2.createServer((req, res) => {
@@ -5350,7 +5461,7 @@ async function startServer(options) {
5350
5461
  project,
5351
5462
  devServer: target,
5352
5463
  scanPreviews: config?.scanPreviews ?? true,
5353
- previews: boot,
5464
+ previews: previewsForConfig(boot),
5354
5465
  errors,
5355
5466
  warnings: configWarnings
5356
5467
  });
@@ -5363,7 +5474,7 @@ async function startServer(options) {
5363
5474
  project,
5364
5475
  devServer: target,
5365
5476
  scanPreviews: config?.scanPreviews ?? true,
5366
- previews: [...currentBoot, ...fresh],
5477
+ previews: previewsForConfig([...currentBoot, ...fresh]),
5367
5478
  errors,
5368
5479
  warnings: configWarnings
5369
5480
  });
@@ -5371,11 +5482,46 @@ async function startServer(options) {
5371
5482
  project,
5372
5483
  devServer: target,
5373
5484
  scanPreviews: config?.scanPreviews ?? true,
5374
- previews: boot,
5485
+ previews: previewsForConfig(boot),
5375
5486
  errors,
5376
5487
  warnings: configWarnings
5377
5488
  }));
5378
5489
  }
5490
+ if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
5491
+ let body = "";
5492
+ req.on("data", (chunk) => body += chunk);
5493
+ return void req.on("end", async () => {
5494
+ const parsed2 = jsonBody(body);
5495
+ if (parsed2 === null) {
5496
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
5497
+ }
5498
+ if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
5499
+ return sendJson(res, 400, { ok: false, error: "Body needs a direction title." });
5500
+ }
5501
+ const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed2.title);
5502
+ if (preview === void 0) {
5503
+ return sendJson(res, 404, { ok: false, error: "No such direction." });
5504
+ }
5505
+ if (preview.branch === void 0) {
5506
+ return sendJson(res, 400, {
5507
+ ok: false,
5508
+ error: `"${preview.title}" is not a branch preview.`
5509
+ });
5510
+ }
5511
+ if (config?.devCommand === void 0) {
5512
+ return sendJson(res, 400, {
5513
+ ok: false,
5514
+ error: `"${preview.title}" cannot start because the config sets no devCommand.`
5515
+ });
5516
+ }
5517
+ void branches.start(preview.title);
5518
+ const state = branches.state(preview.title);
5519
+ if (state === void 0) {
5520
+ return sendJson(res, 404, { ok: false, error: "No such branch preview." });
5521
+ }
5522
+ return sendJson(res, 200, { ok: true, state: publicBranchState(state) });
5523
+ });
5524
+ }
5379
5525
  if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
5380
5526
  let body = "";
5381
5527
  req.on("data", (chunk) => body += chunk);
@@ -6070,7 +6216,12 @@ async function startServer(options) {
6070
6216
  return closePromise;
6071
6217
  liveFiles.close();
6072
6218
  liveHealth.close();
6073
- closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
6219
+ closePromise = Promise.all([
6220
+ branches.stop(),
6221
+ runner.stop(),
6222
+ browserPool.close(),
6223
+ live.close()
6224
+ ]).then(() => new Promise((done) => {
6074
6225
  for (const socket of sockets)
6075
6226
  socket.destroy();
6076
6227
  sockets.clear();
@@ -7371,8 +7522,7 @@ async function run3(options, deps) {
7371
7522
  const local = await readLocalPreviews(options.cwd);
7372
7523
  let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
7373
7524
  const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
7374
- const worktrees = [];
7375
- const worktreeErrors = [];
7525
+ const previewErrors = [];
7376
7526
  const previews = [];
7377
7527
  let app = null;
7378
7528
  const needsApp = (merged?.previews ?? []).some(
@@ -7389,7 +7539,7 @@ async function run3(options, deps) {
7389
7539
  devServer = app.url;
7390
7540
  merged.devServer = app.url;
7391
7541
  } catch (error) {
7392
- worktreeErrors.push(error instanceof Error ? error.message : String(error));
7542
+ previewErrors.push(error instanceof Error ? error.message : String(error));
7393
7543
  }
7394
7544
  }
7395
7545
  const fileMounts = /* @__PURE__ */ new Map();
@@ -7397,7 +7547,7 @@ async function run3(options, deps) {
7397
7547
  if (preview.file !== void 0) {
7398
7548
  const absolute = join19(options.cwd, preview.file);
7399
7549
  if (!existsSync6(absolute)) {
7400
- worktreeErrors.push(
7550
+ previewErrors.push(
7401
7551
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
7402
7552
  );
7403
7553
  continue;
@@ -7413,29 +7563,7 @@ async function run3(options, deps) {
7413
7563
  });
7414
7564
  continue;
7415
7565
  }
7416
- if (preview.branch === void 0) {
7417
- previews.push(preview);
7418
- continue;
7419
- }
7420
- if (merged?.devCommand === void 0) {
7421
- worktreeErrors.push(
7422
- `"${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.`
7423
- );
7424
- continue;
7425
- }
7426
- if (!options.json) deps.log(` starting ${preview.branch}\u2026`);
7427
- try {
7428
- const worktree = await startWorktree({
7429
- cwd: options.cwd,
7430
- branch: preview.branch,
7431
- installCommand: merged.installCommand,
7432
- devCommand: merged.devCommand
7433
- });
7434
- worktrees.push(worktree);
7435
- previews.push({ ...preview, url: `${worktree.url}${preview.url}` });
7436
- } catch (error) {
7437
- worktreeErrors.push(error instanceof Error ? error.message : String(error));
7438
- }
7566
+ previews.push(preview);
7439
7567
  }
7440
7568
  const config = merged === null ? null : { ...merged, previews };
7441
7569
  const configWarnings = [];
@@ -7445,7 +7573,7 @@ async function run3(options, deps) {
7445
7573
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7446
7574
  const serverPromise = startServer({
7447
7575
  config,
7448
- configErrors: [...loaded.errors, ...local.errors, ...worktreeErrors],
7576
+ configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
7449
7577
  configWarnings,
7450
7578
  fileMounts,
7451
7579
  shellDir: findShellDir(),
@@ -7484,9 +7612,9 @@ async function run3(options, deps) {
7484
7612
  );
7485
7613
  deps.log(`config ${configLabel}`);
7486
7614
  deps.log(` ${previewCount} preview${previewCount === 1 ? "" : "s"}`);
7487
- if (loaded.errors.length + worktreeErrors.length > 0) {
7615
+ if (loaded.errors.length + previewErrors.length > 0) {
7488
7616
  deps.log("");
7489
- for (const error of [...loaded.errors, ...worktreeErrors]) deps.log(` ! ${error}`);
7617
+ for (const error of [...loaded.errors, ...previewErrors]) deps.log(` ! ${error}`);
7490
7618
  deps.log(" Fix the config and reload; Leglas will pick it up on restart.");
7491
7619
  }
7492
7620
  if (configWarnings.length > 0) {
@@ -7509,8 +7637,6 @@ async function run3(options, deps) {
7509
7637
  devServer,
7510
7638
  previewCount,
7511
7639
  stop: async () => {
7512
- await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
7513
- })));
7514
7640
  await app?.stop().catch(() => {
7515
7641
  });
7516
7642
  await server.close();
package/dist/index.js CHANGED
@@ -3000,18 +3000,23 @@ async function freePort() {
3000
3000
  });
3001
3001
  });
3002
3002
  }
3003
- function answers(port) {
3004
- return new Promise((resolve5) => {
3005
- const socket = net2.connect(port, "127.0.0.1");
3003
+ var LOOPBACK = ["127.0.0.1", "::1"];
3004
+ function forUrl(host) {
3005
+ return host.includes(":") ? `[${host}]` : host;
3006
+ }
3007
+ async function answeringHost(port) {
3008
+ const reached = await Promise.all(LOOPBACK.map((host) => new Promise((resolve5) => {
3009
+ const socket = net2.connect({ port, host });
3006
3010
  const settle = (value) => {
3007
3011
  socket.destroy();
3008
3012
  resolve5(value);
3009
3013
  };
3010
3014
  socket.setTimeout(400);
3011
- socket.once("connect", () => settle(true));
3012
- socket.once("timeout", () => settle(false));
3013
- socket.once("error", () => settle(false));
3014
- });
3015
+ socket.once("connect", () => settle(host));
3016
+ socket.once("timeout", () => settle(null));
3017
+ socket.once("error", () => settle(null));
3018
+ })));
3019
+ return reached.find((host) => host !== null) ?? null;
3015
3020
  }
3016
3021
  async function startWorktree(options) {
3017
3022
  const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
@@ -3106,8 +3111,9 @@ async function startAppProcess(options) {
3106
3111
  if (exited !== null) {
3107
3112
  throw new Error(`${options.label} did not start: its dev command exited with code ${exited}.`);
3108
3113
  }
3109
- if (await answers(port)) {
3110
- return { port, url: `http://127.0.0.1:${port}`, stop };
3114
+ const host = await answeringHost(port);
3115
+ if (host !== null) {
3116
+ return { port, url: `http://${forUrl(host)}:${port}`, stop };
3111
3117
  }
3112
3118
  await new Promise((resolve5) => setTimeout(resolve5, 250));
3113
3119
  }
@@ -3115,6 +3121,84 @@ async function startAppProcess(options) {
3115
3121
  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.`);
3116
3122
  }
3117
3123
 
3124
+ // ../server/dist/branches.js
3125
+ function publicBranchState(state) {
3126
+ if (state.status === "ready")
3127
+ return { status: "ready" };
3128
+ return state;
3129
+ }
3130
+ function createBranchRegistry(options) {
3131
+ const states = new Map(options.previews.map((preview) => [preview.title, { status: "idle" }]));
3132
+ const previews = new Map(options.previews.map((preview) => [preview.title, preview]));
3133
+ const inflight = /* @__PURE__ */ new Map();
3134
+ const boot = options.startWorktree ?? startWorktree;
3135
+ let closed = false;
3136
+ let stopPromise = null;
3137
+ const transition = (title, state) => {
3138
+ const previous = states.get(title);
3139
+ if (previous?.status === "starting" && state.status === "starting" && previous.phase === state.phase) {
3140
+ return previous;
3141
+ }
3142
+ states.set(title, state);
3143
+ options.onChange?.(title, state);
3144
+ return state;
3145
+ };
3146
+ const begin = (title) => {
3147
+ const preview = previews.get(title);
3148
+ const current = states.get(title);
3149
+ if (preview === void 0 || current === void 0 || closed)
3150
+ return void 0;
3151
+ if (current.status === "starting")
3152
+ return inflight.get(title);
3153
+ if (current.status === "ready")
3154
+ return Promise.resolve(current);
3155
+ transition(title, { status: "starting", phase: "checking out" });
3156
+ let checkout;
3157
+ try {
3158
+ checkout = Promise.resolve(boot({
3159
+ cwd: options.cwd,
3160
+ branch: preview.branch,
3161
+ installCommand: options.installCommand,
3162
+ devCommand: options.devCommand ?? "",
3163
+ onLog: (line) => {
3164
+ transition(title, {
3165
+ status: "starting",
3166
+ phase: line.startsWith("installing ") ? "installing" : "starting"
3167
+ });
3168
+ }
3169
+ }));
3170
+ } catch (error) {
3171
+ checkout = Promise.reject(error);
3172
+ }
3173
+ const starting = checkout.then((worktree) => {
3174
+ transition(title, { status: "starting", phase: "starting" });
3175
+ return transition(title, { status: "ready", worktree });
3176
+ }).catch((error) => transition(title, {
3177
+ status: "failed",
3178
+ reason: error instanceof Error ? error.message : String(error)
3179
+ })).finally(() => {
3180
+ inflight.delete(title);
3181
+ });
3182
+ inflight.set(title, starting);
3183
+ return starting;
3184
+ };
3185
+ return {
3186
+ state: (title) => states.get(title),
3187
+ start: begin,
3188
+ stop: () => {
3189
+ if (stopPromise !== null)
3190
+ return stopPromise;
3191
+ closed = true;
3192
+ stopPromise = Promise.allSettled([...inflight.values()]).then(async () => {
3193
+ const worktrees = [...states.values()].filter((state) => state.status === "ready").map((state) => state.worktree);
3194
+ await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
3195
+ })));
3196
+ });
3197
+ return stopPromise;
3198
+ }
3199
+ };
3200
+ }
3201
+
3118
3202
  // ../server/dist/failure.js
3119
3203
  var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
3120
3204
  var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
@@ -5662,6 +5746,32 @@ async function startServer(options) {
5662
5746
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
5663
5747
  const browserPool = options.pool ?? createBrowserPool();
5664
5748
  const live = options.live ?? createLiveHub();
5749
+ const branches = createBranchRegistry({
5750
+ cwd,
5751
+ previews: (config?.previews ?? []).flatMap((preview) => preview.branch === void 0 ? [] : [{ title: preview.title, branch: preview.branch }]),
5752
+ installCommand: config?.installCommand ?? DEFAULT_INSTALL_COMMAND,
5753
+ devCommand: config?.devCommand,
5754
+ onChange: () => live.nudge("config"),
5755
+ ...options.startWorktree === void 0 ? {} : { startWorktree: options.startWorktree }
5756
+ });
5757
+ const previewForConfig = (preview) => {
5758
+ if (preview.branch === void 0)
5759
+ return preview;
5760
+ const state = branches.state(preview.title) ?? { status: "idle" };
5761
+ const { url: route, ...withoutUrl } = preview;
5762
+ return state.status === "ready" ? {
5763
+ ...withoutUrl,
5764
+ url: `${state.worktree.url}${route}`,
5765
+ state: publicBranchState(state)
5766
+ } : { ...withoutUrl, state: publicBranchState(state) };
5767
+ };
5768
+ const previewsForConfig = (previews) => previews.map(previewForConfig);
5769
+ const readyPreview = (preview) => {
5770
+ if (preview.branch === void 0)
5771
+ return preview;
5772
+ const state = branches.state(preview.title);
5773
+ return state?.status === "ready" ? { ...preview, url: `${state.worktree.url}${preview.url}` } : null;
5774
+ };
5665
5775
  if (options.pool === void 0) {
5666
5776
  void reapOrphanedBrowsers().catch(() => {
5667
5777
  });
@@ -5695,7 +5805,7 @@ async function startServer(options) {
5695
5805
  }
5696
5806
  return Promise.resolve(agentsCache.agents);
5697
5807
  };
5698
- const livePreviews = async () => {
5808
+ const livePreviewDefinitions = async () => {
5699
5809
  const localRead = await readLocalPreviews(cwd).catch(() => null);
5700
5810
  const local = localRead?.errors.length === 0 ? localRead.previews : [];
5701
5811
  const localTitles = new Set(local.map((entry) => entry.title));
@@ -5705,6 +5815,7 @@ async function startServer(options) {
5705
5815
  const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
5706
5816
  return [...boot, ...fresh];
5707
5817
  };
5818
+ const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
5708
5819
  void probeAgents().catch(() => {
5709
5820
  });
5710
5821
  const server = http2.createServer((req, res) => {
@@ -5726,7 +5837,7 @@ async function startServer(options) {
5726
5837
  project,
5727
5838
  devServer: target,
5728
5839
  scanPreviews: config?.scanPreviews ?? true,
5729
- previews: boot,
5840
+ previews: previewsForConfig(boot),
5730
5841
  errors,
5731
5842
  warnings: configWarnings
5732
5843
  });
@@ -5739,7 +5850,7 @@ async function startServer(options) {
5739
5850
  project,
5740
5851
  devServer: target,
5741
5852
  scanPreviews: config?.scanPreviews ?? true,
5742
- previews: [...currentBoot, ...fresh],
5853
+ previews: previewsForConfig([...currentBoot, ...fresh]),
5743
5854
  errors,
5744
5855
  warnings: configWarnings
5745
5856
  });
@@ -5747,11 +5858,46 @@ async function startServer(options) {
5747
5858
  project,
5748
5859
  devServer: target,
5749
5860
  scanPreviews: config?.scanPreviews ?? true,
5750
- previews: boot,
5861
+ previews: previewsForConfig(boot),
5751
5862
  errors,
5752
5863
  warnings: configWarnings
5753
5864
  }));
5754
5865
  }
5866
+ if (path === `${LEGLAS_PREFIX}/api/previews/start` && req.method === "POST") {
5867
+ let body = "";
5868
+ req.on("data", (chunk) => body += chunk);
5869
+ return void req.on("end", async () => {
5870
+ const parsed = jsonBody(body);
5871
+ if (parsed === null) {
5872
+ return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
5873
+ }
5874
+ if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
5875
+ return sendJson(res, 400, { ok: false, error: "Body needs a direction title." });
5876
+ }
5877
+ const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
5878
+ if (preview === void 0) {
5879
+ return sendJson(res, 404, { ok: false, error: "No such direction." });
5880
+ }
5881
+ if (preview.branch === void 0) {
5882
+ return sendJson(res, 400, {
5883
+ ok: false,
5884
+ error: `"${preview.title}" is not a branch preview.`
5885
+ });
5886
+ }
5887
+ if (config?.devCommand === void 0) {
5888
+ return sendJson(res, 400, {
5889
+ ok: false,
5890
+ error: `"${preview.title}" cannot start because the config sets no devCommand.`
5891
+ });
5892
+ }
5893
+ void branches.start(preview.title);
5894
+ const state = branches.state(preview.title);
5895
+ if (state === void 0) {
5896
+ return sendJson(res, 404, { ok: false, error: "No such branch preview." });
5897
+ }
5898
+ return sendJson(res, 200, { ok: true, state: publicBranchState(state) });
5899
+ });
5900
+ }
5755
5901
  if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
5756
5902
  let body = "";
5757
5903
  req.on("data", (chunk) => body += chunk);
@@ -6446,7 +6592,12 @@ async function startServer(options) {
6446
6592
  return closePromise;
6447
6593
  liveFiles.close();
6448
6594
  liveHealth.close();
6449
- closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
6595
+ closePromise = Promise.all([
6596
+ branches.stop(),
6597
+ runner.stop(),
6598
+ browserPool.close(),
6599
+ live.close()
6600
+ ]).then(() => new Promise((done) => {
6450
6601
  for (const socket of sockets)
6451
6602
  socket.destroy();
6452
6603
  sockets.clear();
@@ -7361,8 +7512,7 @@ async function run3(options, deps) {
7361
7512
  const local = await readLocalPreviews(options.cwd);
7362
7513
  let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
7363
7514
  const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
7364
- const worktrees = [];
7365
- const worktreeErrors = [];
7515
+ const previewErrors = [];
7366
7516
  const previews = [];
7367
7517
  let app = null;
7368
7518
  const needsApp = (merged?.previews ?? []).some(
@@ -7379,7 +7529,7 @@ async function run3(options, deps) {
7379
7529
  devServer = app.url;
7380
7530
  merged.devServer = app.url;
7381
7531
  } catch (error) {
7382
- worktreeErrors.push(error instanceof Error ? error.message : String(error));
7532
+ previewErrors.push(error instanceof Error ? error.message : String(error));
7383
7533
  }
7384
7534
  }
7385
7535
  const fileMounts = /* @__PURE__ */ new Map();
@@ -7387,7 +7537,7 @@ async function run3(options, deps) {
7387
7537
  if (preview.file !== void 0) {
7388
7538
  const absolute = join19(options.cwd, preview.file);
7389
7539
  if (!existsSync6(absolute)) {
7390
- worktreeErrors.push(
7540
+ previewErrors.push(
7391
7541
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
7392
7542
  );
7393
7543
  continue;
@@ -7403,29 +7553,7 @@ async function run3(options, deps) {
7403
7553
  });
7404
7554
  continue;
7405
7555
  }
7406
- if (preview.branch === void 0) {
7407
- previews.push(preview);
7408
- continue;
7409
- }
7410
- if (merged?.devCommand === void 0) {
7411
- worktreeErrors.push(
7412
- `"${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.`
7413
- );
7414
- continue;
7415
- }
7416
- if (!options.json) deps.log(` starting ${preview.branch}\u2026`);
7417
- try {
7418
- const worktree = await startWorktree({
7419
- cwd: options.cwd,
7420
- branch: preview.branch,
7421
- installCommand: merged.installCommand,
7422
- devCommand: merged.devCommand
7423
- });
7424
- worktrees.push(worktree);
7425
- previews.push({ ...preview, url: `${worktree.url}${preview.url}` });
7426
- } catch (error) {
7427
- worktreeErrors.push(error instanceof Error ? error.message : String(error));
7428
- }
7556
+ previews.push(preview);
7429
7557
  }
7430
7558
  const config = merged === null ? null : { ...merged, previews };
7431
7559
  const configWarnings = [];
@@ -7435,7 +7563,7 @@ async function run3(options, deps) {
7435
7563
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
7436
7564
  const serverPromise = startServer({
7437
7565
  config,
7438
- configErrors: [...loaded.errors, ...local.errors, ...worktreeErrors],
7566
+ configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
7439
7567
  configWarnings,
7440
7568
  fileMounts,
7441
7569
  shellDir: findShellDir(),
@@ -7474,9 +7602,9 @@ async function run3(options, deps) {
7474
7602
  );
7475
7603
  deps.log(`config ${configLabel}`);
7476
7604
  deps.log(` ${previewCount} preview${previewCount === 1 ? "" : "s"}`);
7477
- if (loaded.errors.length + worktreeErrors.length > 0) {
7605
+ if (loaded.errors.length + previewErrors.length > 0) {
7478
7606
  deps.log("");
7479
- for (const error of [...loaded.errors, ...worktreeErrors]) deps.log(` ! ${error}`);
7607
+ for (const error of [...loaded.errors, ...previewErrors]) deps.log(` ! ${error}`);
7480
7608
  deps.log(" Fix the config and reload; Leglas will pick it up on restart.");
7481
7609
  }
7482
7610
  if (configWarnings.length > 0) {
@@ -7499,8 +7627,6 @@ async function run3(options, deps) {
7499
7627
  devServer,
7500
7628
  previewCount,
7501
7629
  stop: async () => {
7502
- await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
7503
- })));
7504
7630
  await app?.stop().catch(() => {
7505
7631
  });
7506
7632
  await server.close();