leglas 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export { runWatch } from "./run-watch.js";
16
16
  export type { TemplateResult, WatchTemplate } from "./watch.js";
17
17
  export type { WatchDeps } from "./run-watch.js";
18
18
  export { CAPTURES_DIR, DEFAULT_PORT, LEGLAS_PREFIX, NO_BROWSER, REFERENCES_DIR, SERVER_INFO_PATH, findBrowser, isOwnCapture, readRequests, readServerInfo, } from "@leglas/server";
19
- export type { Attachment, AttachmentKind, Failure, FailureCode, PendingRequest, RequestStatus, ServerInfo, } from "@leglas/server";
19
+ export type { Attachment, AttachmentKind, Failure, FailureCode, PendingRequest, RequestStatus, ServerInfo, UpdateService, UpdateStatus, } from "@leglas/server";
20
20
  export { runClassify } from "./run-classify.js";
21
21
  export { run } from "./run.js";
22
22
  export type { ParseResult, RunOptions } from "./args.js";
package/dist/index.js CHANGED
@@ -1247,13 +1247,21 @@ var KNOWN_AGENTS = {
1247
1247
  name: "Cursor",
1248
1248
  binary: "cursor-agent",
1249
1249
  efforts: [],
1250
+ // `--trust` on every invocation. Without it, print mode stops at a
1251
+ // "Workspace Trust Required" prompt nothing can answer and exits 1 with
1252
+ // no events, in any directory Cursor has not been trusted for by hand:
1253
+ // measured against cursor-agent 2026.09.02, one second, zero output. The
1254
+ // project is the one the user pointed Leglas at, which is the trust the
1255
+ // flag grants. It is the only permission the run needs: with it alone,
1256
+ // the edit and the shell command in the same run both executed.
1250
1257
  args: (prompt, _effort = null, _images = []) => [
1251
1258
  "-p",
1252
1259
  prompt,
1253
1260
  "--output-format",
1254
- "stream-json"
1261
+ "stream-json",
1262
+ "--trust"
1255
1263
  ],
1256
- terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt],
1264
+ terminalArgs: (prompt, _effort = null, _images = []) => ["-p", prompt, "--trust"],
1257
1265
  // `--resume [chatId]` is documented alongside `--continue` in the CLI
1258
1266
  // parameter reference, and every stream-json event carries the
1259
1267
  // `session_id` to feed it. Cursor exposes no persistent transport the way
@@ -1264,11 +1272,16 @@ var KNOWN_AGENTS = {
1264
1272
  // Images are accepted and ignored, like its other argument builders:
1265
1273
  // `cursor-agent` documents no flag for them, and the capture paths reach
1266
1274
  // it as text in the prompt either way.
1267
- resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json"],
1275
+ resumeArgs: (sessionId, prompt, _effort = null, _images = []) => ["-p", "--resume", sessionId, prompt, "--output-format", "stream-json", "--trust"],
1268
1276
  sessionFrom: (event) => typeof event.session_id === "string" && event.session_id !== "" ? event.session_id : null,
1277
+ // Read against cursor-agent 2026.09.02: every event carries the id, a
1278
+ // resume in this argument order answers under the same id and remembers
1279
+ // the earlier turn.
1280
+ activityVerified: true,
1269
1281
  authArgs: ["status"],
1270
- // UNVERIFIED: cursor-agent was not available on the build machine. The
1271
- // reading is deliberately loose, and anything ambiguous stays unknown.
1282
+ // The signed-in form was read from the CLI: "✓ Logged in as <email>",
1283
+ // exit 0. The signed-out form has not been, so that side of the reading
1284
+ // stays loose, and anything ambiguous stays unknown.
1272
1285
  authVerdict: (result) => {
1273
1286
  if (/logged in|signed in/i.test(result.stdout))
1274
1287
  return "ok";
@@ -1472,15 +1485,15 @@ function cursorActivity(event, cwd) {
1472
1485
  const wrapper = record(event.tool_call);
1473
1486
  if (wrapper === null)
1474
1487
  return null;
1475
- const key = Object.keys(wrapper)[0];
1488
+ const key = Object.keys(wrapper).find((name) => name.endsWith("ToolCall"));
1476
1489
  if (key === void 0)
1477
1490
  return null;
1478
1491
  const call = record(wrapper[key]);
1479
1492
  const args = record(call?.args);
1480
1493
  const tool = key.replace(/ToolCall$/, "");
1481
- if (tool === "write") {
1494
+ if (tool === "edit" || tool === "write") {
1482
1495
  const path2 = shownPath(args?.path, cwd);
1483
- return path2 === null ? "using write" : `editing ${path2}`;
1496
+ return path2 === null ? "editing a file" : `editing ${path2}`;
1484
1497
  }
1485
1498
  if (tool === "read") {
1486
1499
  const path2 = shownPath(args?.path, cwd);
@@ -7514,6 +7527,39 @@ async function startServer(options) {
7514
7527
  if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
7515
7528
  return sendJson2(res, 403, { error: "Not available to viewers." });
7516
7529
  }
7530
+ if (path === `${LEGLAS_PREFIX}/api/update` && req.method === "GET" || req.method === "POST" && ["check", "skip", "install"].some((action) => path === `${LEGLAS_PREFIX}/api/update/${action}`)) {
7531
+ const updates = options.updates;
7532
+ if (updates === void 0) {
7533
+ return sendJson2(res, 404, { ok: false, error: "Updates are not available here." });
7534
+ }
7535
+ if (req.method === "GET")
7536
+ return sendJson2(res, 200, updates.status());
7537
+ if (path.endsWith("/check")) {
7538
+ return void updates.check({ force: true }).then((status) => sendJson2(res, 200, status));
7539
+ }
7540
+ if (path.endsWith("/install")) {
7541
+ return void updates.update().then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 409, {
7542
+ ok: false,
7543
+ error: error instanceof Error ? error.message : String(error)
7544
+ }));
7545
+ }
7546
+ if (!hasJsonBody(req))
7547
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7548
+ let body = "";
7549
+ req.on("data", (chunk) => body += chunk);
7550
+ return void req.on("end", () => {
7551
+ const parsed = jsonBody(body);
7552
+ if (parsed === null)
7553
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
7554
+ if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
7555
+ return sendJson2(res, 400, { ok: false, error: "Body needs a version." });
7556
+ }
7557
+ void updates.skip(parsed.version).then((status) => sendJson2(res, 200, status), (error) => sendJson2(res, 400, {
7558
+ ok: false,
7559
+ error: error instanceof Error ? error.message : String(error)
7560
+ }));
7561
+ });
7562
+ }
7517
7563
  if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "GET") {
7518
7564
  return void shares?.tunnels().then((tunnels) => sendJson2(res, 200, { share: shares?.status() ?? null, tunnels }));
7519
7565
  }
@@ -8360,6 +8406,7 @@ async function startServer(options) {
8360
8406
  ...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
8361
8407
  });
8362
8408
  port = await bind2(server, options.port ?? DEFAULT_PORT);
8409
+ options.updates?.setPort(port);
8363
8410
  const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
8364
8411
  liveHealth = watchHealth(target, live);
8365
8412
  await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
@@ -8378,6 +8425,8 @@ async function startServer(options) {
8378
8425
  ...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
8379
8426
  ...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
8380
8427
  });
8428
+ options.updates?.onBusy(() => runner?.snapshot().running ?? false);
8429
+ options.updates?.onChange(() => live.nudge("update"));
8381
8430
  let closePromise = null;
8382
8431
  return {
8383
8432
  port,
@@ -8388,6 +8437,7 @@ async function startServer(options) {
8388
8437
  closePromise = (async () => {
8389
8438
  liveFiles.close();
8390
8439
  liveHealth?.close();
8440
+ await options.updates?.close();
8391
8441
  await Promise.all([
8392
8442
  shares?.close() ?? Promise.resolve(),
8393
8443
  branches.stop(),
@@ -8410,6 +8460,15 @@ async function startServer(options) {
8410
8460
  };
8411
8461
  }
8412
8462
 
8463
+ // ../server/dist/update.js
8464
+ import { spawn as spawnChild2 } from "child_process";
8465
+ import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
8466
+ import { lstat as lstat3, mkdir as mkdir9, mkdtemp, rename as rename3, rm as rm5, writeFile as writeFile9 } from "fs/promises";
8467
+ import { homedir as homedir2 } from "os";
8468
+ import { dirname as dirname9, join as join13, posix as posix2, win32 } from "path";
8469
+ var DAY_MS = 24 * 60 * 60 * 1e3;
8470
+ var INSTALL_DEADLINE_MS = 5 * 60 * 1e3;
8471
+
8413
8472
  // src/keep.ts
8414
8473
  function surfaceOf(url) {
8415
8474
  if (!url.startsWith("/") || !url.includes("?")) return null;
@@ -8465,8 +8524,8 @@ function planKeep(options) {
8465
8524
  }
8466
8525
 
8467
8526
  // src/run-init.ts
8468
- import { readFile as readFile12, writeFile as writeFile9 } from "fs/promises";
8469
- import { join as join13 } from "path";
8527
+ import { readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
8528
+ import { join as join14 } from "path";
8470
8529
  async function readIfPresent(path) {
8471
8530
  try {
8472
8531
  return await readFile12(path, "utf8");
@@ -8477,18 +8536,18 @@ async function readIfPresent(path) {
8477
8536
  async function runInit(options, deps) {
8478
8537
  const existingConfig = findConfigFile(options.cwd);
8479
8538
  const plan = planInit({
8480
- agents: await readIfPresent(join13(options.cwd, "AGENTS.md")),
8539
+ agents: await readIfPresent(join14(options.cwd, "AGENTS.md")),
8481
8540
  config: existingConfig === null ? null : "present",
8482
- gitignore: await readIfPresent(join13(options.cwd, ".gitignore")),
8541
+ gitignore: await readIfPresent(join14(options.cwd, ".gitignore")),
8483
8542
  force: options.force
8484
8543
  });
8485
8544
  const touched = [];
8486
8545
  for (const write2 of plan.writes) {
8487
- await writeFile9(join13(options.cwd, write2.path), write2.contents, "utf8");
8546
+ await writeFile10(join14(options.cwd, write2.path), write2.contents, "utf8");
8488
8547
  touched.push(write2.path);
8489
8548
  }
8490
8549
  if (plan.gitignore !== null) {
8491
- await writeFile9(join13(options.cwd, ".gitignore"), plan.gitignore, "utf8");
8550
+ await writeFile10(join14(options.cwd, ".gitignore"), plan.gitignore, "utf8");
8492
8551
  touched.push(".gitignore");
8493
8552
  }
8494
8553
  if (options.json) {
@@ -8507,9 +8566,9 @@ async function runInit(options, deps) {
8507
8566
  }
8508
8567
 
8509
8568
  // src/run-keep.ts
8510
- import { existsSync as existsSync4 } from "fs";
8511
- import { copyFile as copyFile2, mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
8512
- import { dirname as dirname9, join as join14 } from "path";
8569
+ import { existsSync as existsSync5 } from "fs";
8570
+ import { copyFile as copyFile2, mkdir as mkdir10, readFile as readFile13, rm as rm6, writeFile as writeFile11 } from "fs/promises";
8571
+ import { dirname as dirname10, join as join15 } from "path";
8513
8572
 
8514
8573
  // src/resolve-title.ts
8515
8574
  function resolveOrExplain(input, titles, renames) {
@@ -8545,15 +8604,15 @@ async function writeLogEntry(options) {
8545
8604
  annotations: await readAnnotations(options.cwd),
8546
8605
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
8547
8606
  });
8548
- const dir = join14(options.cwd, options.logDir);
8549
- await mkdir9(dir, { recursive: true });
8550
- const file = join14(dir, `${entry.slug}.md`);
8551
- await writeFile10(file, entry.markdown, "utf8");
8607
+ const dir = join15(options.cwd, options.logDir);
8608
+ await mkdir10(dir, { recursive: true });
8609
+ const file = join15(dir, `${entry.slug}.md`);
8610
+ await writeFile11(file, entry.markdown, "utf8");
8552
8611
  if (entry.pictures.length > 0) {
8553
- const pictureDir = join14(dir, entry.slug);
8554
- await mkdir9(pictureDir, { recursive: true });
8612
+ const pictureDir = join15(dir, entry.slug);
8613
+ await mkdir10(pictureDir, { recursive: true });
8555
8614
  for (const picture of entry.pictures) {
8556
- await copyFile2(join14(options.cwd, picture.from), join14(pictureDir, picture.to)).catch(() => {
8615
+ await copyFile2(join15(options.cwd, picture.from), join15(pictureDir, picture.to)).catch(() => {
8557
8616
  });
8558
8617
  }
8559
8618
  }
@@ -8576,17 +8635,17 @@ async function runKeep(options, deps) {
8576
8635
  if (!resolved.ok) return fail(resolved.error);
8577
8636
  const plan = planKeep({ title: resolved.title, previews, to: options.to });
8578
8637
  if (!plan.ok) return fail(plan.error);
8579
- const from = join14(options.cwd, plan.move.from);
8580
- const to = join14(options.cwd, plan.move.to);
8581
- if (!existsSync4(from)) {
8638
+ const from = join15(options.cwd, plan.move.from);
8639
+ const to = join15(options.cwd, plan.move.to);
8640
+ if (!existsSync5(from)) {
8582
8641
  return fail(`${plan.move.from} does not exist. Nothing to keep.`);
8583
8642
  }
8584
- if (existsSync4(to)) {
8643
+ if (existsSync5(to)) {
8585
8644
  return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
8586
8645
  }
8587
8646
  const source = await readFile13(from, "utf8");
8588
- await mkdir9(dirname9(to), { recursive: true });
8589
- await writeFile10(to, renameExport(source, plan.exportName), "utf8");
8647
+ await mkdir10(dirname10(to), { recursive: true });
8648
+ await writeFile11(to, renameExport(source, plan.exportName), "utf8");
8590
8649
  const surface = plan.removeDir.slice(plan.removeDir.lastIndexOf("/") + 1);
8591
8650
  let logged = null;
8592
8651
  let logError = null;
@@ -8601,7 +8660,7 @@ async function runKeep(options, deps) {
8601
8660
  } catch (error) {
8602
8661
  logError = error instanceof Error ? error.message : String(error);
8603
8662
  }
8604
- await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
8663
+ await rm6(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
8605
8664
  const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
8606
8665
  if (options.json) {
8607
8666
  deps.log(
@@ -8639,9 +8698,9 @@ async function runKeep(options, deps) {
8639
8698
  }
8640
8699
 
8641
8700
  // src/run-new.ts
8642
- import { existsSync as existsSync5 } from "fs";
8643
- import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8644
- import { dirname as dirname10, join as join15 } from "path";
8701
+ import { existsSync as existsSync6 } from "fs";
8702
+ import { mkdir as mkdir11, readFile as readFile14, writeFile as writeFile12 } from "fs/promises";
8703
+ import { dirname as dirname11, join as join16 } from "path";
8645
8704
  async function readIfPresent2(path) {
8646
8705
  try {
8647
8706
  return await readFile14(path, "utf8");
@@ -8652,7 +8711,7 @@ async function readIfPresent2(path) {
8652
8711
  async function runNew(options, deps) {
8653
8712
  let from;
8654
8713
  if (options.from !== void 0) {
8655
- const contents = await readIfPresent2(join15(options.cwd, options.from));
8714
+ const contents = await readIfPresent2(join16(options.cwd, options.from));
8656
8715
  if (contents === null) {
8657
8716
  const message2 = `${options.from} does not exist, so there is nothing to use as the baseline.`;
8658
8717
  if (options.json) deps.log(JSON.stringify({ ok: false, error: message2 }));
@@ -8663,8 +8722,8 @@ async function runNew(options, deps) {
8663
8722
  }
8664
8723
  const plan = planNew({
8665
8724
  surface: options.surface,
8666
- packageJson: await readIfPresent2(join15(options.cwd, "package.json")),
8667
- gitignore: await readIfPresent2(join15(options.cwd, ".gitignore")),
8725
+ packageJson: await readIfPresent2(join16(options.cwd, "package.json")),
8726
+ gitignore: await readIfPresent2(join16(options.cwd, ".gitignore")),
8668
8727
  from
8669
8728
  });
8670
8729
  const fail = (error) => {
@@ -8687,19 +8746,19 @@ async function runNew(options, deps) {
8687
8746
  deps.log(plan.instructions);
8688
8747
  return { exitCode: 0, written: [] };
8689
8748
  }
8690
- const existing = plan.writes.filter((write2) => existsSync5(join15(options.cwd, write2.path)));
8749
+ const existing = plan.writes.filter((write2) => existsSync6(join16(options.cwd, write2.path)));
8691
8750
  if (existing.length > 0) {
8692
8751
  return fail(`${existing[0]?.path} already exists. Delete it first, or pick another surface name.`);
8693
8752
  }
8694
8753
  const written = [];
8695
8754
  for (const write2 of plan.writes) {
8696
- const target = join15(options.cwd, write2.path);
8697
- await mkdir10(dirname10(target), { recursive: true });
8698
- await writeFile11(target, write2.contents, "utf8");
8755
+ const target = join16(options.cwd, write2.path);
8756
+ await mkdir11(dirname11(target), { recursive: true });
8757
+ await writeFile12(target, write2.contents, "utf8");
8699
8758
  written.push(write2.path);
8700
8759
  }
8701
8760
  if (plan.gitignore !== null) {
8702
- await writeFile11(join15(options.cwd, ".gitignore"), plan.gitignore, "utf8");
8761
+ await writeFile12(join16(options.cwd, ".gitignore"), plan.gitignore, "utf8");
8703
8762
  written.push(".gitignore");
8704
8763
  }
8705
8764
  if (options.json) {
@@ -8720,13 +8779,13 @@ async function runNew(options, deps) {
8720
8779
  }
8721
8780
 
8722
8781
  // src/run-previews.ts
8723
- import { readFile as readFile15, writeFile as writeFile12 } from "fs/promises";
8724
- import { join as join16 } from "path";
8782
+ import { readFile as readFile15, writeFile as writeFile13 } from "fs/promises";
8783
+ import { join as join17 } from "path";
8725
8784
  function envelope(deps, ok, body) {
8726
8785
  deps.log(JSON.stringify({ ok, ...body }));
8727
8786
  }
8728
8787
  async function ensureIgnored(cwd) {
8729
- const path = join16(cwd, ".gitignore");
8788
+ const path = join17(cwd, ".gitignore");
8730
8789
  let current = null;
8731
8790
  try {
8732
8791
  current = await readFile15(path, "utf8");
@@ -8734,7 +8793,7 @@ async function ensureIgnored(cwd) {
8734
8793
  current = null;
8735
8794
  }
8736
8795
  const next = ignoreEntry(current);
8737
- if (next !== null) await writeFile12(path, next, "utf8");
8796
+ if (next !== null) await writeFile13(path, next, "utf8");
8738
8797
  }
8739
8798
  async function runAdd(options, deps) {
8740
8799
  const loaded = await loadConfig(options.cwd);
@@ -9064,12 +9123,12 @@ async function runShow(options, deps) {
9064
9123
 
9065
9124
  // src/run-watch.ts
9066
9125
  import { spawn as spawn3 } from "child_process";
9067
- import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
9068
- import { dirname as dirname11, join as join17 } from "path";
9126
+ import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile14 } from "fs/promises";
9127
+ import { dirname as dirname12, join as join18 } from "path";
9069
9128
  var POLL_MS2 = 2e3;
9070
9129
  var HEARTBEAT_TIMEOUT_MS = 1e3;
9071
9130
  async function saveTemplate(cwd, run4) {
9072
- const path = join17(cwd, WATCH_PATH);
9131
+ const path = join18(cwd, WATCH_PATH);
9073
9132
  let config = {};
9074
9133
  try {
9075
9134
  const parsed = JSON.parse(await readFile16(path, "utf8"));
@@ -9079,8 +9138,8 @@ async function saveTemplate(cwd, run4) {
9079
9138
  } catch {
9080
9139
  }
9081
9140
  config.run = run4;
9082
- await mkdir11(dirname11(path), { recursive: true });
9083
- await writeFile13(path, `${JSON.stringify(config, null, 2)}
9141
+ await mkdir12(dirname12(path), { recursive: true });
9142
+ await writeFile14(path, `${JSON.stringify(config, null, 2)}
9084
9143
  `, "utf8");
9085
9144
  }
9086
9145
  function spawnAgent(command, args, cwd) {
@@ -9219,12 +9278,12 @@ async function runWatch(options, deps) {
9219
9278
 
9220
9279
  // src/run-classify.ts
9221
9280
  import { stat as stat4 } from "fs/promises";
9222
- import { join as join18 } from "path";
9281
+ import { join as join19 } from "path";
9223
9282
  async function runClassify(options, deps) {
9224
9283
  const declared = await Promise.all(
9225
9284
  options.changes.map(async (change) => ({
9226
9285
  ...change,
9227
- exists: await stat4(join18(options.cwd, change.path)).then(
9286
+ exists: await stat4(join19(options.cwd, change.path)).then(
9228
9287
  () => true,
9229
9288
  () => false
9230
9289
  )
@@ -9250,10 +9309,10 @@ async function runClassify(options, deps) {
9250
9309
  }
9251
9310
 
9252
9311
  // src/run.ts
9253
- import { existsSync as existsSync6 } from "fs";
9312
+ import { existsSync as existsSync7 } from "fs";
9254
9313
  import { realpath as realpath4 } from "fs/promises";
9255
9314
  import { createRequire } from "module";
9256
- import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
9315
+ import { basename as basename6, dirname as dirname13, join as join20, relative as relative5, resolve as resolve4 } from "path";
9257
9316
  import { fileURLToPath } from "url";
9258
9317
 
9259
9318
  // src/dev-server-owner.ts
@@ -9339,11 +9398,11 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
9339
9398
 
9340
9399
  // src/run.ts
9341
9400
  function findShellDir() {
9342
- const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
9343
- if (existsSync6(join19(bundled, "index.html"))) return bundled;
9401
+ const bundled = join20(dirname13(fileURLToPath(import.meta.url)), "shell");
9402
+ if (existsSync7(join20(bundled, "index.html"))) return bundled;
9344
9403
  try {
9345
9404
  const require2 = createRequire(import.meta.url);
9346
- return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
9405
+ return dirname13(require2.resolve("@leglas/shell/dist/index.html"));
9347
9406
  } catch {
9348
9407
  return null;
9349
9408
  }
@@ -9354,10 +9413,14 @@ function shellWord(value) {
9354
9413
  return `'${value.replaceAll("'", `'\\''`)}'`;
9355
9414
  }
9356
9415
  function embeddedLeglasCommand() {
9357
- const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
9358
- if (!existsSync6(entry)) return "npx -y leglas";
9416
+ const entry = join20(dirname13(fileURLToPath(import.meta.url)), "bin.js");
9417
+ if (!existsSync7(entry)) return "npx -y leglas";
9359
9418
  return [process.execPath, entry].map(shellWord).join(" ");
9360
9419
  }
9420
+ function skipStartupCheck(env) {
9421
+ const off = (value) => value === void 0 || value === "" || value === "0" || value === "false";
9422
+ return env.CI !== void 0 && env.CI !== "" && env.CI !== "false" || !off(env.LEGLAS_NO_UPDATE_CHECK);
9423
+ }
9361
9424
  async function run3(options, deps) {
9362
9425
  const loaded = await loadConfig(options.cwd);
9363
9426
  const local = await readLocalPreviews(options.cwd);
@@ -9386,8 +9449,8 @@ async function run3(options, deps) {
9386
9449
  const fileMounts = /* @__PURE__ */ new Map();
9387
9450
  for (const preview of merged?.previews ?? []) {
9388
9451
  if (preview.file !== void 0) {
9389
- const absolute = join19(options.cwd, preview.file);
9390
- if (!existsSync6(absolute)) {
9452
+ const absolute = join20(options.cwd, preview.file);
9453
+ if (!existsSync7(absolute)) {
9391
9454
  previewErrors.push(
9392
9455
  `"${preview.title}" names file ${preview.file}, which does not exist. The preview is skipped.`
9393
9456
  );
@@ -9397,7 +9460,7 @@ async function run3(options, deps) {
9397
9460
  for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
9398
9461
  slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
9399
9462
  }
9400
- fileMounts.set(slug, dirname12(absolute));
9463
+ fileMounts.set(slug, dirname13(absolute));
9401
9464
  previews.push({
9402
9465
  ...preview,
9403
9466
  url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
@@ -9408,8 +9471,8 @@ async function run3(options, deps) {
9408
9471
  }
9409
9472
  const config = merged === null ? null : { ...merged, previews };
9410
9473
  const configWarnings = [];
9411
- const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
9412
- () => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
9474
+ const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname13(loaded.path)).catch(
9475
+ () => resolve4(loaded.path === null ? options.cwd : dirname13(loaded.path))
9413
9476
  );
9414
9477
  const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
9415
9478
  const serverPromise = startServer({
@@ -9423,6 +9486,7 @@ async function run3(options, deps) {
9423
9486
  project: loaded.path ?? options.cwd,
9424
9487
  cwd: options.cwd,
9425
9488
  leglasCommand: embeddedLeglasCommand(),
9489
+ ...deps.updates === void 0 ? {} : { updates: deps.updates },
9426
9490
  ...options.port === void 0 ? {} : { port: options.port }
9427
9491
  });
9428
9492
  const [server, warning] = await Promise.all([serverPromise, ownerWarning]);
@@ -9472,12 +9536,26 @@ async function run3(options, deps) {
9472
9536
  }
9473
9537
  }
9474
9538
  if (options.open) await deps.open(url);
9539
+ let stopped = false;
9540
+ let updateTimer = null;
9541
+ if (!options.json && deps.updates !== void 0 && !skipStartupCheck(process.env)) {
9542
+ void deps.updates.check().then(() => {
9543
+ if (stopped) return;
9544
+ const line = deps.updates?.notice();
9545
+ if (line !== null && line !== void 0) deps.log(line);
9546
+ });
9547
+ const updates = deps.updates;
9548
+ updateTimer = setInterval(() => void updates.check(), 60 * 6e4);
9549
+ updateTimer.unref?.();
9550
+ }
9475
9551
  return {
9476
9552
  exitCode: 0,
9477
9553
  url,
9478
9554
  devServer,
9479
9555
  previewCount,
9480
9556
  stop: async () => {
9557
+ stopped = true;
9558
+ if (updateTimer !== null) clearInterval(updateTimer);
9481
9559
  await app?.stop().catch(() => {
9482
9560
  });
9483
9561
  await server.close();
@@ -0,0 +1,13 @@
1
+ import type { spawn as spawnChild } from "node:child_process";
2
+ import type { RestartCommand } from "@leglas/server";
3
+ import { type ShutdownSignal, type SignalTarget } from "./shutdown.js";
4
+ export declare function createHandoff(): {
5
+ handOff: (command: RestartCommand, stop: () => Promise<void>, deps: {
6
+ spawn: typeof spawnChild;
7
+ exit: (code: number) => void;
8
+ target: SignalTarget & {
9
+ off(signal: ShutdownSignal, listener: () => void): unknown;
10
+ };
11
+ }) => Promise<void>;
12
+ handedOff: () => boolean;
13
+ };
package/dist/run.d.ts CHANGED
@@ -1,8 +1,13 @@
1
+ import { type UpdateService } from "@leglas/server";
1
2
  import type { RunOptions } from "./args.js";
2
3
  export type RunDeps = {
3
4
  open(url: string): Promise<void>;
4
5
  log(line: string): void;
6
+ /** The CLI supplies updates; programmatic hosts can omit them. */
7
+ updates?: UpdateService;
5
8
  };
9
+ /** Automated runs and an explicit opt-out should never ask npm at startup. */
10
+ export declare function skipStartupCheck(env: NodeJS.ProcessEnv): boolean;
6
11
  export type RunResult = {
7
12
  exitCode: number;
8
13
  /** Where the interface lives, which is what gets opened and reported. */