leglas 0.7.3 → 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 +157 -37
- package/dist/index.js +157 -37
- 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
|
@@ -2745,6 +2745,84 @@ async function startAppProcess(options) {
|
|
|
2745
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.`);
|
|
2746
2746
|
}
|
|
2747
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
|
+
|
|
2748
2826
|
// ../server/dist/failure.js
|
|
2749
2827
|
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
2750
2828
|
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
@@ -5292,6 +5370,32 @@ async function startServer(options) {
|
|
|
5292
5370
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5293
5371
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5294
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
|
+
};
|
|
5295
5399
|
if (options.pool === void 0) {
|
|
5296
5400
|
void reapOrphanedBrowsers().catch(() => {
|
|
5297
5401
|
});
|
|
@@ -5325,7 +5429,7 @@ async function startServer(options) {
|
|
|
5325
5429
|
}
|
|
5326
5430
|
return Promise.resolve(agentsCache.agents);
|
|
5327
5431
|
};
|
|
5328
|
-
const
|
|
5432
|
+
const livePreviewDefinitions = async () => {
|
|
5329
5433
|
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
5330
5434
|
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
5331
5435
|
const localTitles = new Set(local.map((entry) => entry.title));
|
|
@@ -5335,6 +5439,7 @@ async function startServer(options) {
|
|
|
5335
5439
|
const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
|
|
5336
5440
|
return [...boot, ...fresh];
|
|
5337
5441
|
};
|
|
5442
|
+
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
5338
5443
|
void probeAgents().catch(() => {
|
|
5339
5444
|
});
|
|
5340
5445
|
const server = http2.createServer((req, res) => {
|
|
@@ -5356,7 +5461,7 @@ async function startServer(options) {
|
|
|
5356
5461
|
project,
|
|
5357
5462
|
devServer: target,
|
|
5358
5463
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5359
|
-
previews: boot,
|
|
5464
|
+
previews: previewsForConfig(boot),
|
|
5360
5465
|
errors,
|
|
5361
5466
|
warnings: configWarnings
|
|
5362
5467
|
});
|
|
@@ -5369,7 +5474,7 @@ async function startServer(options) {
|
|
|
5369
5474
|
project,
|
|
5370
5475
|
devServer: target,
|
|
5371
5476
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5372
|
-
previews: [...currentBoot, ...fresh],
|
|
5477
|
+
previews: previewsForConfig([...currentBoot, ...fresh]),
|
|
5373
5478
|
errors,
|
|
5374
5479
|
warnings: configWarnings
|
|
5375
5480
|
});
|
|
@@ -5377,11 +5482,46 @@ async function startServer(options) {
|
|
|
5377
5482
|
project,
|
|
5378
5483
|
devServer: target,
|
|
5379
5484
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5380
|
-
previews: boot,
|
|
5485
|
+
previews: previewsForConfig(boot),
|
|
5381
5486
|
errors,
|
|
5382
5487
|
warnings: configWarnings
|
|
5383
5488
|
}));
|
|
5384
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
|
+
}
|
|
5385
5525
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
5386
5526
|
let body = "";
|
|
5387
5527
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -6076,7 +6216,12 @@ async function startServer(options) {
|
|
|
6076
6216
|
return closePromise;
|
|
6077
6217
|
liveFiles.close();
|
|
6078
6218
|
liveHealth.close();
|
|
6079
|
-
closePromise = Promise.all([
|
|
6219
|
+
closePromise = Promise.all([
|
|
6220
|
+
branches.stop(),
|
|
6221
|
+
runner.stop(),
|
|
6222
|
+
browserPool.close(),
|
|
6223
|
+
live.close()
|
|
6224
|
+
]).then(() => new Promise((done) => {
|
|
6080
6225
|
for (const socket of sockets)
|
|
6081
6226
|
socket.destroy();
|
|
6082
6227
|
sockets.clear();
|
|
@@ -7377,8 +7522,7 @@ async function run3(options, deps) {
|
|
|
7377
7522
|
const local = await readLocalPreviews(options.cwd);
|
|
7378
7523
|
let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
|
|
7379
7524
|
const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
|
|
7380
|
-
const
|
|
7381
|
-
const worktreeErrors = [];
|
|
7525
|
+
const previewErrors = [];
|
|
7382
7526
|
const previews = [];
|
|
7383
7527
|
let app = null;
|
|
7384
7528
|
const needsApp = (merged?.previews ?? []).some(
|
|
@@ -7395,7 +7539,7 @@ async function run3(options, deps) {
|
|
|
7395
7539
|
devServer = app.url;
|
|
7396
7540
|
merged.devServer = app.url;
|
|
7397
7541
|
} catch (error) {
|
|
7398
|
-
|
|
7542
|
+
previewErrors.push(error instanceof Error ? error.message : String(error));
|
|
7399
7543
|
}
|
|
7400
7544
|
}
|
|
7401
7545
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
@@ -7403,7 +7547,7 @@ async function run3(options, deps) {
|
|
|
7403
7547
|
if (preview.file !== void 0) {
|
|
7404
7548
|
const absolute = join19(options.cwd, preview.file);
|
|
7405
7549
|
if (!existsSync6(absolute)) {
|
|
7406
|
-
|
|
7550
|
+
previewErrors.push(
|
|
7407
7551
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
7408
7552
|
);
|
|
7409
7553
|
continue;
|
|
@@ -7419,29 +7563,7 @@ async function run3(options, deps) {
|
|
|
7419
7563
|
});
|
|
7420
7564
|
continue;
|
|
7421
7565
|
}
|
|
7422
|
-
|
|
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
|
-
}
|
|
7566
|
+
previews.push(preview);
|
|
7445
7567
|
}
|
|
7446
7568
|
const config = merged === null ? null : { ...merged, previews };
|
|
7447
7569
|
const configWarnings = [];
|
|
@@ -7451,7 +7573,7 @@ async function run3(options, deps) {
|
|
|
7451
7573
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7452
7574
|
const serverPromise = startServer({
|
|
7453
7575
|
config,
|
|
7454
|
-
configErrors: [...loaded.errors, ...local.errors, ...
|
|
7576
|
+
configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
|
|
7455
7577
|
configWarnings,
|
|
7456
7578
|
fileMounts,
|
|
7457
7579
|
shellDir: findShellDir(),
|
|
@@ -7490,9 +7612,9 @@ async function run3(options, deps) {
|
|
|
7490
7612
|
);
|
|
7491
7613
|
deps.log(`config ${configLabel}`);
|
|
7492
7614
|
deps.log(` ${previewCount} preview${previewCount === 1 ? "" : "s"}`);
|
|
7493
|
-
if (loaded.errors.length +
|
|
7615
|
+
if (loaded.errors.length + previewErrors.length > 0) {
|
|
7494
7616
|
deps.log("");
|
|
7495
|
-
for (const error of [...loaded.errors, ...
|
|
7617
|
+
for (const error of [...loaded.errors, ...previewErrors]) deps.log(` ! ${error}`);
|
|
7496
7618
|
deps.log(" Fix the config and reload; Leglas will pick it up on restart.");
|
|
7497
7619
|
}
|
|
7498
7620
|
if (configWarnings.length > 0) {
|
|
@@ -7515,8 +7637,6 @@ async function run3(options, deps) {
|
|
|
7515
7637
|
devServer,
|
|
7516
7638
|
previewCount,
|
|
7517
7639
|
stop: async () => {
|
|
7518
|
-
await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
|
|
7519
|
-
})));
|
|
7520
7640
|
await app?.stop().catch(() => {
|
|
7521
7641
|
});
|
|
7522
7642
|
await server.close();
|
package/dist/index.js
CHANGED
|
@@ -3121,6 +3121,84 @@ async function startAppProcess(options) {
|
|
|
3121
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.`);
|
|
3122
3122
|
}
|
|
3123
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
|
+
|
|
3124
3202
|
// ../server/dist/failure.js
|
|
3125
3203
|
var NEEDS_TRUST = /not inside a trusted directory|--skip-git-repo-check/i;
|
|
3126
3204
|
var MISSING_BINARY = /\b(ENOENT|EACCES|ENOTDIR)\b/;
|
|
@@ -5668,6 +5746,32 @@ async function startServer(options) {
|
|
|
5668
5746
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5669
5747
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5670
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
|
+
};
|
|
5671
5775
|
if (options.pool === void 0) {
|
|
5672
5776
|
void reapOrphanedBrowsers().catch(() => {
|
|
5673
5777
|
});
|
|
@@ -5701,7 +5805,7 @@ async function startServer(options) {
|
|
|
5701
5805
|
}
|
|
5702
5806
|
return Promise.resolve(agentsCache.agents);
|
|
5703
5807
|
};
|
|
5704
|
-
const
|
|
5808
|
+
const livePreviewDefinitions = async () => {
|
|
5705
5809
|
const localRead = await readLocalPreviews(cwd).catch(() => null);
|
|
5706
5810
|
const local = localRead?.errors.length === 0 ? localRead.previews : [];
|
|
5707
5811
|
const localTitles = new Set(local.map((entry) => entry.title));
|
|
@@ -5711,6 +5815,7 @@ async function startServer(options) {
|
|
|
5711
5815
|
const fresh = local.filter((entry) => !known.has(entry.title) && entry.branch === void 0 && entry.file === void 0);
|
|
5712
5816
|
return [...boot, ...fresh];
|
|
5713
5817
|
};
|
|
5818
|
+
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
5714
5819
|
void probeAgents().catch(() => {
|
|
5715
5820
|
});
|
|
5716
5821
|
const server = http2.createServer((req, res) => {
|
|
@@ -5732,7 +5837,7 @@ async function startServer(options) {
|
|
|
5732
5837
|
project,
|
|
5733
5838
|
devServer: target,
|
|
5734
5839
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5735
|
-
previews: boot,
|
|
5840
|
+
previews: previewsForConfig(boot),
|
|
5736
5841
|
errors,
|
|
5737
5842
|
warnings: configWarnings
|
|
5738
5843
|
});
|
|
@@ -5745,7 +5850,7 @@ async function startServer(options) {
|
|
|
5745
5850
|
project,
|
|
5746
5851
|
devServer: target,
|
|
5747
5852
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5748
|
-
previews: [...currentBoot, ...fresh],
|
|
5853
|
+
previews: previewsForConfig([...currentBoot, ...fresh]),
|
|
5749
5854
|
errors,
|
|
5750
5855
|
warnings: configWarnings
|
|
5751
5856
|
});
|
|
@@ -5753,11 +5858,46 @@ async function startServer(options) {
|
|
|
5753
5858
|
project,
|
|
5754
5859
|
devServer: target,
|
|
5755
5860
|
scanPreviews: config?.scanPreviews ?? true,
|
|
5756
|
-
previews: boot,
|
|
5861
|
+
previews: previewsForConfig(boot),
|
|
5757
5862
|
errors,
|
|
5758
5863
|
warnings: configWarnings
|
|
5759
5864
|
}));
|
|
5760
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
|
+
}
|
|
5761
5901
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
5762
5902
|
let body = "";
|
|
5763
5903
|
req.on("data", (chunk) => body += chunk);
|
|
@@ -6452,7 +6592,12 @@ async function startServer(options) {
|
|
|
6452
6592
|
return closePromise;
|
|
6453
6593
|
liveFiles.close();
|
|
6454
6594
|
liveHealth.close();
|
|
6455
|
-
closePromise = Promise.all([
|
|
6595
|
+
closePromise = Promise.all([
|
|
6596
|
+
branches.stop(),
|
|
6597
|
+
runner.stop(),
|
|
6598
|
+
browserPool.close(),
|
|
6599
|
+
live.close()
|
|
6600
|
+
]).then(() => new Promise((done) => {
|
|
6456
6601
|
for (const socket of sockets)
|
|
6457
6602
|
socket.destroy();
|
|
6458
6603
|
sockets.clear();
|
|
@@ -7367,8 +7512,7 @@ async function run3(options, deps) {
|
|
|
7367
7512
|
const local = await readLocalPreviews(options.cwd);
|
|
7368
7513
|
let devServer = options.userPort === void 0 ? loaded.config?.devServer ?? "http://localhost:3000" : `http://localhost:${options.userPort}`;
|
|
7369
7514
|
const merged = loaded.config === null ? null : { ...loaded.config, devServer, previews: [...loaded.config.previews, ...local.previews] };
|
|
7370
|
-
const
|
|
7371
|
-
const worktreeErrors = [];
|
|
7515
|
+
const previewErrors = [];
|
|
7372
7516
|
const previews = [];
|
|
7373
7517
|
let app = null;
|
|
7374
7518
|
const needsApp = (merged?.previews ?? []).some(
|
|
@@ -7385,7 +7529,7 @@ async function run3(options, deps) {
|
|
|
7385
7529
|
devServer = app.url;
|
|
7386
7530
|
merged.devServer = app.url;
|
|
7387
7531
|
} catch (error) {
|
|
7388
|
-
|
|
7532
|
+
previewErrors.push(error instanceof Error ? error.message : String(error));
|
|
7389
7533
|
}
|
|
7390
7534
|
}
|
|
7391
7535
|
const fileMounts = /* @__PURE__ */ new Map();
|
|
@@ -7393,7 +7537,7 @@ async function run3(options, deps) {
|
|
|
7393
7537
|
if (preview.file !== void 0) {
|
|
7394
7538
|
const absolute = join19(options.cwd, preview.file);
|
|
7395
7539
|
if (!existsSync6(absolute)) {
|
|
7396
|
-
|
|
7540
|
+
previewErrors.push(
|
|
7397
7541
|
`"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
|
|
7398
7542
|
);
|
|
7399
7543
|
continue;
|
|
@@ -7409,29 +7553,7 @@ async function run3(options, deps) {
|
|
|
7409
7553
|
});
|
|
7410
7554
|
continue;
|
|
7411
7555
|
}
|
|
7412
|
-
|
|
7413
|
-
previews.push(preview);
|
|
7414
|
-
continue;
|
|
7415
|
-
}
|
|
7416
|
-
if (merged?.devCommand === void 0) {
|
|
7417
|
-
worktreeErrors.push(
|
|
7418
|
-
`"${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.`
|
|
7419
|
-
);
|
|
7420
|
-
continue;
|
|
7421
|
-
}
|
|
7422
|
-
if (!options.json) deps.log(` starting ${preview.branch}\u2026`);
|
|
7423
|
-
try {
|
|
7424
|
-
const worktree = await startWorktree({
|
|
7425
|
-
cwd: options.cwd,
|
|
7426
|
-
branch: preview.branch,
|
|
7427
|
-
installCommand: merged.installCommand,
|
|
7428
|
-
devCommand: merged.devCommand
|
|
7429
|
-
});
|
|
7430
|
-
worktrees.push(worktree);
|
|
7431
|
-
previews.push({ ...preview, url: `${worktree.url}${preview.url}` });
|
|
7432
|
-
} catch (error) {
|
|
7433
|
-
worktreeErrors.push(error instanceof Error ? error.message : String(error));
|
|
7434
|
-
}
|
|
7556
|
+
previews.push(preview);
|
|
7435
7557
|
}
|
|
7436
7558
|
const config = merged === null ? null : { ...merged, previews };
|
|
7437
7559
|
const configWarnings = [];
|
|
@@ -7441,7 +7563,7 @@ async function run3(options, deps) {
|
|
|
7441
7563
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7442
7564
|
const serverPromise = startServer({
|
|
7443
7565
|
config,
|
|
7444
|
-
configErrors: [...loaded.errors, ...local.errors, ...
|
|
7566
|
+
configErrors: [...loaded.errors, ...local.errors, ...previewErrors],
|
|
7445
7567
|
configWarnings,
|
|
7446
7568
|
fileMounts,
|
|
7447
7569
|
shellDir: findShellDir(),
|
|
@@ -7480,9 +7602,9 @@ async function run3(options, deps) {
|
|
|
7480
7602
|
);
|
|
7481
7603
|
deps.log(`config ${configLabel}`);
|
|
7482
7604
|
deps.log(` ${previewCount} preview${previewCount === 1 ? "" : "s"}`);
|
|
7483
|
-
if (loaded.errors.length +
|
|
7605
|
+
if (loaded.errors.length + previewErrors.length > 0) {
|
|
7484
7606
|
deps.log("");
|
|
7485
|
-
for (const error of [...loaded.errors, ...
|
|
7607
|
+
for (const error of [...loaded.errors, ...previewErrors]) deps.log(` ! ${error}`);
|
|
7486
7608
|
deps.log(" Fix the config and reload; Leglas will pick it up on restart.");
|
|
7487
7609
|
}
|
|
7488
7610
|
if (configWarnings.length > 0) {
|
|
@@ -7505,8 +7627,6 @@ async function run3(options, deps) {
|
|
|
7505
7627
|
devServer,
|
|
7506
7628
|
previewCount,
|
|
7507
7629
|
stop: async () => {
|
|
7508
|
-
await Promise.all(worktrees.map((worktree) => worktree.stop().catch(() => {
|
|
7509
|
-
})));
|
|
7510
7630
|
await app?.stop().catch(() => {
|
|
7511
7631
|
});
|
|
7512
7632
|
await server.close();
|