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 +172 -46
- package/dist/index.js +172 -46
- package/dist/shell/assets/index-CpYCCElH.css +1 -0
- package/dist/shell/assets/index-Ct9pp-D-.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-DT33xxfg.css +0 -1
- package/dist/shell/assets/index-DbONqSbP.js +0 -14
package/dist/bin.js
CHANGED
|
@@ -2624,18 +2624,23 @@ async function freePort() {
|
|
|
2624
2624
|
});
|
|
2625
2625
|
});
|
|
2626
2626
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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(
|
|
2636
|
-
socket.once("timeout", () => settle(
|
|
2637
|
-
socket.once("error", () => settle(
|
|
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
|
-
|
|
2734
|
-
|
|
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
|
|
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([
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ...
|
|
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 +
|
|
7615
|
+
if (loaded.errors.length + previewErrors.length > 0) {
|
|
7488
7616
|
deps.log("");
|
|
7489
|
-
for (const error of [...loaded.errors, ...
|
|
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
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
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(
|
|
3012
|
-
socket.once("timeout", () => settle(
|
|
3013
|
-
socket.once("error", () => settle(
|
|
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
|
-
|
|
3110
|
-
|
|
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
|
|
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([
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ...
|
|
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 +
|
|
7605
|
+
if (loaded.errors.length + previewErrors.length > 0) {
|
|
7478
7606
|
deps.log("");
|
|
7479
|
-
for (const error of [...loaded.errors, ...
|
|
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();
|