herdr-remote 0.2.15 → 0.2.17

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/tui.mjs CHANGED
@@ -90,7 +90,10 @@ var require_config = __commonJS({
90
90
  herdr: {
91
91
  socketPath: null,
92
92
  args: [],
93
- cwd: os.homedir()
93
+ cwd: os.homedir(),
94
+ // Start Herdr's server when herdr-remote starts (at boot, under keep-alive).
95
+ // Off: Herdr starts when the user runs it, or confirms it from the WebUI.
96
+ autoStart: false
94
97
  },
95
98
  auth: {
96
99
  pairingTtlMs: 10 * 60 * 1e3,
@@ -251,6 +254,7 @@ var require_config = __commonJS({
251
254
  if (typeof config.herdr.cwd !== "string" || config.herdr.cwd.length === 0) {
252
255
  config.herdr.cwd = os.homedir();
253
256
  }
257
+ config.herdr.autoStart = config.herdr.autoStart === true;
254
258
  if (config.relay.mode === "remote" && !config.relay.remoteUrl) {
255
259
  config.relay.mode = "local";
256
260
  }
@@ -411,6 +415,9 @@ var require_updater = __commonJS({
411
415
  }
412
416
  return "npm";
413
417
  }
418
+ function updateChecksEnabled2(env = process.env) {
419
+ return env.HERDR_REMOTE_UPDATE_CHECK !== "0";
420
+ }
414
421
  function canSelfUpdate2() {
415
422
  return installKind2() === "npm";
416
423
  }
@@ -462,92 +469,173 @@ var require_updater = __commonJS({
462
469
  }
463
470
  return 0;
464
471
  }
465
- async function askRegistry(registry, { timeoutMs, fetchImpl }) {
472
+ async function fetchJson(url, { timeoutMs, fetchImpl }) {
466
473
  const controller = new AbortController();
467
474
  const timer = setTimeout(() => controller.abort(), timeoutMs);
468
475
  try {
469
- const response = await fetchImpl(`${registry}/${PACKAGE_NAME}/latest`, {
476
+ const response = await fetchImpl(url, {
470
477
  signal: controller.signal,
471
- headers: { Accept: "application/vnd.npm.install-v1+json" }
478
+ headers: { Accept: "application/json", "Cache-Control": "no-cache" }
472
479
  });
473
480
  if (!response.ok) return { ok: false, message: `HTTP ${response.status ?? "?"}` };
474
- const body = await response.json();
475
- const latest = typeof body?.version === "string" ? body.version : null;
476
- if (!latest) return { ok: false, message: "registry answered without a version" };
477
- return { ok: true, latest };
481
+ return { ok: true, body: await response.json() };
478
482
  } catch (error) {
479
483
  return { ok: false, message: error?.name === "AbortError" ? "timed out" : String(error?.message || error) };
480
484
  } finally {
481
485
  clearTimeout(timer);
482
486
  }
483
487
  }
488
+ async function askRegistry(registry, { timeoutMs, fetchImpl }) {
489
+ const endpoints = [
490
+ { url: `${registry}/-/package/${PACKAGE_NAME}/dist-tags`, read: (body) => body?.latest },
491
+ { url: `${registry}/${PACKAGE_NAME}/latest`, read: (body) => body?.version }
492
+ ];
493
+ let message = null;
494
+ for (const endpoint of endpoints) {
495
+ const attempt = await fetchJson(endpoint.url, { timeoutMs, fetchImpl });
496
+ if (!attempt.ok) {
497
+ message = attempt.message;
498
+ continue;
499
+ }
500
+ const latest = endpoint.read(attempt.body);
501
+ if (typeof latest === "string" && latest) return { ok: true, latest };
502
+ message = "registry answered without a version";
503
+ }
504
+ return { ok: false, message };
505
+ }
484
506
  async function checkForUpdate2({
485
507
  timeoutMs = 6e3,
486
508
  fetchImpl = globalThis.fetch,
487
509
  registries = registryCandidates()
488
510
  } = {}) {
489
511
  const current = currentVersion();
490
- const failures = [];
491
- for (const registry of registries) {
492
- const attempt = await askRegistry(registry, { timeoutMs, fetchImpl });
493
- if (attempt.ok) {
494
- return {
495
- ok: true,
496
- current,
497
- latest: attempt.latest,
498
- registry,
499
- updateAvailable: compareVersions(attempt.latest, current) > 0
500
- };
501
- }
502
- failures.push(`${registry}: ${attempt.message}`);
512
+ const attempts = await Promise.all(registries.map(async (registry) => ({
513
+ registry,
514
+ ...await askRegistry(registry, { timeoutMs, fetchImpl })
515
+ })));
516
+ const answers = attempts.filter((attempt) => attempt.ok);
517
+ if (answers.length === 0) {
518
+ return {
519
+ ok: false,
520
+ current,
521
+ errorKey: "update.errorNetwork",
522
+ message: attempts.map((attempt) => `${attempt.registry}: ${attempt.message}`).join("; "),
523
+ triedRegistries: registries
524
+ };
503
525
  }
526
+ const latest = answers.map((answer) => answer.latest).reduce((best, version) => compareVersions(version, best) > 0 ? version : best);
527
+ const sources = answers.filter((answer) => compareVersions(answer.latest, latest) === 0).map((answer) => answer.registry);
528
+ const behind = answers.filter((answer) => compareVersions(answer.latest, latest) < 0).map((answer) => ({ registry: answer.registry, version: answer.latest }));
504
529
  return {
505
- ok: false,
530
+ ok: true,
506
531
  current,
507
- errorKey: "update.errorNetwork",
508
- message: failures.join("; "),
509
- triedRegistries: registries
532
+ latest,
533
+ registry: sources[0],
534
+ sources,
535
+ behind,
536
+ updateAvailable: compareVersions(latest, current) > 0
510
537
  };
511
538
  }
512
- function performUpdate2({
513
- spawnImpl = spawn,
514
- timeoutMs = 18e4,
515
- // Seam for tests: the suite runs from a checkout, where the guard below is
516
- // correctly the only reachable outcome.
517
- installKindImpl = installKind2,
518
- // The registry that just answered the version check. Installing from
519
- // somewhere the machine has been shown to reach beats installing from a
520
- // default it may have no route to. Empty means "whatever npm is configured
521
- // with", which is the right answer when no check has run.
522
- registry = ""
523
- } = {}) {
539
+ function npmErrorSummary(output) {
540
+ const lines = String(output || "").split(/\r?\n/).map((line) => line.trim()).filter((line) => /^npm (error|ERR!)/.test(line) && !/complete log of this run/i.test(line)).map((line) => line.replace(/^npm (error|ERR!)\s*/, "")).filter(Boolean);
541
+ return (lines.length > 0 ? lines.slice(0, 3) : String(output || "").trim().split(/\r?\n/).slice(-3)).join(" ");
542
+ }
543
+ function isNotYetPublished(output) {
544
+ return /\bETARGET\b|notarget|No matching version found/i.test(String(output || ""));
545
+ }
546
+ function installedVersionOnDisk() {
547
+ try {
548
+ return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version;
549
+ } catch {
550
+ return null;
551
+ }
552
+ }
553
+ function runNpm(spawnImpl, args, timeoutMs) {
524
554
  return new Promise((resolve) => {
525
- const kind = installKindImpl();
526
- if (kind !== "npm") {
527
- resolve({ ok: false, errorKey: `update.cannot.${kind}` });
555
+ let child;
556
+ try {
557
+ child = spawnImpl("npm", args, { timeout: timeoutMs });
558
+ } catch (error) {
559
+ resolve({ ok: false, spawnFailed: true, output: String(error?.message || error) });
528
560
  return;
529
561
  }
530
- const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
531
- const normalizedRegistry = normalizeRegistry(registry);
532
- if (normalizedRegistry) args.push("--registry", normalizedRegistry);
533
- const child = spawnImpl("npm", args, {
534
- encoding: "utf8",
535
- timeout: timeoutMs
536
- });
537
562
  let output = "";
563
+ let settled = false;
564
+ const finish = (result) => {
565
+ if (!settled) {
566
+ settled = true;
567
+ resolve(result);
568
+ }
569
+ };
538
570
  const collect = (chunk) => {
539
571
  output += String(chunk);
540
572
  };
541
573
  child.stdout?.on("data", collect);
542
574
  child.stderr?.on("data", collect);
543
- child.on("error", (error) => {
544
- resolve({ ok: false, errorKey: "update.errorFailed", output: error.message });
545
- });
546
- child.on("close", (code) => {
547
- resolve({ ok: code === 0, output: output.trim().slice(-2e3) });
548
- });
575
+ child.on("error", (error) => finish({ ok: false, spawnFailed: true, output: error.message }));
576
+ child.on("close", (code) => finish({ ok: code === 0, output: output.trim() }));
549
577
  });
550
578
  }
579
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
580
+ async function performUpdate2({
581
+ spawnImpl = spawn,
582
+ timeoutMs = 18e4,
583
+ // Seam for tests: the suite runs from a checkout, where the guard below is
584
+ // correctly the only reachable outcome.
585
+ installKindImpl = installKind2,
586
+ // The registries that reported `version`, preferred first. Empty means
587
+ // "whatever npm is configured with", which is right when no check has run.
588
+ registry = "",
589
+ sources = [],
590
+ version = null,
591
+ attempts = 3,
592
+ retryDelayMs = 15e3,
593
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
594
+ readInstalledVersion = installedVersionOnDisk,
595
+ onAttempt = () => {
596
+ }
597
+ } = {}) {
598
+ const kind = installKindImpl();
599
+ if (kind !== "npm") return { ok: false, errorKey: `update.cannot.${kind}` };
600
+ const target = typeof version === "string" && VERSION_PATTERN.test(version) ? version : null;
601
+ const spec = `${PACKAGE_NAME}@${target || "latest"}`;
602
+ const registries = [];
603
+ for (const candidate of [registry, ...sources]) {
604
+ const normalized = normalizeRegistry(candidate);
605
+ if (normalized && !registries.includes(normalized)) registries.push(normalized);
606
+ }
607
+ if (registries.length === 0) registries.push("");
608
+ let last = { output: "" };
609
+ let notYetPublished = false;
610
+ for (const source of registries) {
611
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
612
+ onAttempt({ registry: source, attempt });
613
+ const args = ["install", "-g", spec, "--prefer-online"];
614
+ if (source) args.push("--registry", source);
615
+ const result = await runNpm(spawnImpl, args, timeoutMs);
616
+ if (result.ok) {
617
+ const installed = readInstalledVersion();
618
+ if (target && installed && compareVersions(installed, target) < 0) {
619
+ return { ok: false, errorKey: "update.errorNotApplied", installed, output: result.output.slice(-2e3) };
620
+ }
621
+ return { ok: true, installed, output: result.output.slice(-2e3) };
622
+ }
623
+ last = result;
624
+ if (result.spawnFailed) {
625
+ return { ok: false, errorKey: "update.errorFailed", output: result.output, summary: result.output };
626
+ }
627
+ notYetPublished = isNotYetPublished(result.output);
628
+ if (!notYetPublished) break;
629
+ if (attempt < attempts) await sleep(retryDelayMs);
630
+ }
631
+ }
632
+ return {
633
+ ok: false,
634
+ errorKey: notYetPublished ? "update.errorNotYetPublished" : "update.errorFailed",
635
+ output: String(last.output || "").slice(-2e3),
636
+ summary: npmErrorSummary(last.output)
637
+ };
638
+ }
551
639
  module.exports = {
552
640
  PACKAGE_NAME,
553
641
  DEFAULT_REGISTRY,
@@ -557,8 +645,11 @@ var require_updater = __commonJS({
557
645
  compareVersions,
558
646
  currentVersion,
559
647
  installKind: installKind2,
648
+ installedVersionOnDisk,
649
+ npmErrorSummary,
560
650
  performUpdate: performUpdate2,
561
- registryCandidates
651
+ registryCandidates,
652
+ updateChecksEnabled: updateChecksEnabled2
562
653
  };
563
654
  }
564
655
  });
@@ -659,7 +750,7 @@ var require_herdr_command = __commonJS({
659
750
  if (!version) return true;
660
751
  return compareVersions(version, minimum) >= 0;
661
752
  }
662
- function herdrVersion({ command, timeout = 1e4, ...lookup } = {}) {
753
+ function herdrVersion2({ command, timeout = 1e4, ...lookup } = {}) {
663
754
  const resolved = command || resolveHerdrCommand(lookup);
664
755
  const unknown = { command: resolved, version: null, raw: null, supported: true, ok: false };
665
756
  let result;
@@ -692,7 +783,7 @@ var require_herdr_command = __commonJS({
692
783
  findHerdrCommand,
693
784
  herdrNotFoundMessage,
694
785
  herdrOutdatedMessage,
695
- herdrVersion,
786
+ herdrVersion: herdrVersion2,
696
787
  meetsMinimum,
697
788
  parseHerdrVersion,
698
789
  resolveHerdrCommand,
@@ -839,14 +930,7 @@ var require_en = __commonJS({
839
930
  "herdr.title": "Herdr integration",
840
931
  "herdr.socketPath": "Socket path",
841
932
  "herdr.args": "Extra arguments",
842
- "herdr.plugin": "Plugin registration",
843
- "herdr.pluginRegistered": "Registered with Herdr.",
844
- "herdr.pluginMissing": "Not registered with Herdr.",
845
- "herdr.register": "Register as Herdr plugin",
846
- "herdr.unregister": "Unregister plugin",
847
- "herdr.registerDone": "Registered: {path}",
848
- "herdr.unregisterDone": "Plugin unregistered.",
849
- "herdr.registerFailed": "Registration failed: {message}",
933
+ "herdr.autoStartHint": "Off: Herdr starts only when you run it, or when you confirm it from a browser. On: herdr-remote also starts it every time it starts, at boot included. Takes effect the next time herdr-remote starts.",
850
934
  "herdr.cliMissing": "Command herdr not found. Set HERDR_BIN_PATH to its full path.",
851
935
  "herdr.version": "Herdr version",
852
936
  "herdr.versionOutdated": 'Herdr {version} is older than {minimum}. Run "herdr update": window titles, background machine activation and large pastes misbehave in browser windows before that release.',
@@ -883,6 +967,7 @@ var require_en = __commonJS({
883
967
  "field.publicUrl": "Browser URL override",
884
968
  "field.socketPath": "Herdr socket path",
885
969
  "field.herdrArgs": "Herdr arguments",
970
+ "field.herdrAutoStart": "Auto-start Herdr",
886
971
  "field.language": "Language",
887
972
  "field.keepalive": "Keep-alive manager",
888
973
  "placeholder.autoDiscovered": "auto-discovered",
@@ -912,16 +997,22 @@ var require_en = __commonJS({
912
997
  "hint.save": "s save",
913
998
  "hint.editing": "\u21B5 confirm \xB7 esc cancel",
914
999
  "update.title": "Updates",
1000
+ "update.banner": "herdr-remote {latest} is out (running {current}). Press 7 and Enter on the update row to install.",
915
1001
  "update.check": "Check for updates",
916
1002
  "update.checking": "Checking npm\u2026",
917
1003
  "update.upToDate": "Up to date ({version})",
918
1004
  "update.available": "Update {version} available (press Enter to install)",
919
1005
  "update.updating": "Installing {version}\u2026",
1006
+ "update.updatingRetry": "Installing {version}\u2026 (npm has not caught up yet, attempt {attempt})",
920
1007
  "update.done": "Updated to {version}",
921
1008
  "update.restartHint": "Update installed. Restart herdr-remote to apply.",
922
1009
  "update.errorNetwork": "Could not reach any npm registry",
923
1010
  "update.errorNetworkDetail": "No registry answered: {message}",
924
- "update.errorFailed": "Update failed. Run: npm install -g herdr-remote@latest",
1011
+ "update.errorFailed": "Update failed. Run: npm install -g herdr-remote@{version} --prefer-online",
1012
+ "update.errorFailedDetail": "npm: {message}",
1013
+ "update.errorNotYetPublished": "npm has not caught up with {version} yet (common in the first minutes after a release). Press Enter to try again shortly.",
1014
+ "update.errorNotApplied": "npm reported success, but {installed} is still installed",
1015
+ "update.mirrorBehind": "{registries} still has an older version; installing from {source}",
925
1016
  "update.cannot.source": "Source checkout \u2014 update with git",
926
1017
  "update.cannot.linked": "Linked package \u2014 managed by npm link"
927
1018
  };
@@ -1066,14 +1157,7 @@ var require_zh = __commonJS({
1066
1157
  "herdr.title": "Herdr \u96C6\u6210",
1067
1158
  "herdr.socketPath": "\u5957\u63A5\u5B57\u8DEF\u5F84",
1068
1159
  "herdr.args": "\u9644\u52A0\u53C2\u6570",
1069
- "herdr.plugin": "\u63D2\u4EF6\u6CE8\u518C",
1070
- "herdr.pluginRegistered": "\u5DF2\u6CE8\u518C\u5230 Herdr\u3002",
1071
- "herdr.pluginMissing": "\u5C1A\u672A\u6CE8\u518C\u5230 Herdr\u3002",
1072
- "herdr.register": "\u6CE8\u518C\u4E3A Herdr \u63D2\u4EF6",
1073
- "herdr.unregister": "\u53D6\u6D88\u6CE8\u518C",
1074
- "herdr.registerDone": "\u5DF2\u6CE8\u518C\uFF1A{path}",
1075
- "herdr.unregisterDone": "\u5DF2\u53D6\u6D88\u6CE8\u518C\u3002",
1076
- "herdr.registerFailed": "\u6CE8\u518C\u5931\u8D25\uFF1A{message}",
1160
+ "herdr.autoStartHint": "\u5173\uFF1AHerdr \u53EA\u5728\u4F60\u624B\u52A8\u8FD0\u884C\uFF0C\u6216\u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u540E\u624D\u542F\u52A8\u3002\u5F00\uFF1Aherdr-remote \u6BCF\u6B21\u542F\u52A8\uFF08\u5305\u62EC\u5F00\u673A\uFF09\u65F6\u4E00\u5E76\u542F\u52A8 Herdr\u3002\u4E0B\u6B21 herdr-remote \u542F\u52A8\u65F6\u751F\u6548\u3002",
1077
1161
  "herdr.cliMissing": "\u672A\u627E\u5230 herdr \u547D\u4EE4\uFF0C\u53EF\u5C06 HERDR_BIN_PATH \u8BBE\u4E3A\u5176\u5B8C\u6574\u8DEF\u5F84\u3002",
1078
1162
  "herdr.version": "Herdr \u7248\u672C",
1079
1163
  "herdr.versionOutdated": "Herdr {version} \u4F4E\u4E8E {minimum}\uFF0C\u8BF7\u8FD0\u884C \u201Cherdr update\u201D\uFF1A\u5728\u8BE5\u7248\u672C\u4E4B\u524D\uFF0C\u6D4F\u89C8\u5668\u7A97\u53E3\u7684\u6807\u9898\u3001\u540E\u53F0\u673A\u5668\u6FC0\u6D3B\u4E0E\u5927\u6BB5\u7C98\u8D34\u90FD\u4F1A\u51FA\u95EE\u9898\u3002",
@@ -1110,6 +1194,7 @@ var require_zh = __commonJS({
1110
1194
  "field.publicUrl": "\u6D4F\u89C8\u5668\u5730\u5740\uFF08\u8986\u76D6\uFF09",
1111
1195
  "field.socketPath": "Herdr \u5957\u63A5\u5B57\u8DEF\u5F84",
1112
1196
  "field.herdrArgs": "Herdr \u53C2\u6570",
1197
+ "field.herdrAutoStart": "\u968F\u670D\u52A1\u542F\u52A8 Herdr",
1113
1198
  "field.language": "\u8BED\u8A00",
1114
1199
  "field.keepalive": "\u4FDD\u6D3B\u7BA1\u7406\u65B9\u5F0F",
1115
1200
  "placeholder.autoDiscovered": "\u81EA\u52A8\u63A2\u6D4B",
@@ -1139,16 +1224,22 @@ var require_zh = __commonJS({
1139
1224
  "hint.save": "s \u4FDD\u5B58",
1140
1225
  "hint.editing": "\u21B5 \u786E\u8BA4 \xB7 esc \u53D6\u6D88",
1141
1226
  "update.title": "\u66F4\u65B0",
1227
+ "update.banner": "herdr-remote {latest} \u5DF2\u53D1\u5E03\uFF08\u5F53\u524D {current}\uFF09\uFF0C\u6309 7 \u8FDB\u5165\u300C\u8BED\u8A00\u4E0E\u5173\u4E8E\u300D\uFF0C\u5728\u66F4\u65B0\u4E00\u884C\u6309 Enter \u5B89\u88C5\u3002",
1142
1228
  "update.check": "\u68C0\u67E5\u66F4\u65B0",
1143
1229
  "update.checking": "\u67E5\u8BE2 npm \u4E2D\u2026",
1144
1230
  "update.upToDate": "\u5DF2\u662F\u6700\u65B0\uFF08{version}\uFF09",
1145
1231
  "update.available": "\u65B0\u7248\u672C {version} \u53EF\u7528\uFF08\u6309 Enter \u5B89\u88C5\uFF09",
1146
1232
  "update.updating": "\u5B89\u88C5 {version} \u4E2D\u2026",
1233
+ "update.updatingRetry": "\u5B89\u88C5 {version} \u4E2D\u2026\uFF08npm \u5C1A\u672A\u540C\u6B65\uFF0C\u7B2C {attempt} \u6B21\u5C1D\u8BD5\uFF09",
1147
1234
  "update.done": "\u5DF2\u66F4\u65B0\u81F3 {version}",
1148
1235
  "update.restartHint": "\u66F4\u65B0\u5DF2\u5B89\u88C5\uFF0C\u91CD\u542F herdr-remote \u751F\u6548\u3002",
1149
1236
  "update.errorNetwork": "\u6240\u6709 npm registry \u5747\u65E0\u6CD5\u8FDE\u63A5",
1150
1237
  "update.errorNetworkDetail": "\u65E0 registry \u5E94\u7B54\uFF1A{message}",
1151
- "update.errorFailed": "\u66F4\u65B0\u5931\u8D25\uFF0C\u53EF\u624B\u52A8\u6267\u884C\uFF1Anpm install -g herdr-remote@latest",
1238
+ "update.errorFailed": "\u66F4\u65B0\u5931\u8D25\uFF0C\u53EF\u624B\u52A8\u6267\u884C\uFF1Anpm install -g herdr-remote@{version} --prefer-online",
1239
+ "update.errorFailedDetail": "npm\uFF1A{message}",
1240
+ "update.errorNotYetPublished": "npm \u8FD8\u6CA1\u540C\u6B65\u5230 {version}\uFF08\u521A\u53D1\u5E03\u7684\u51E0\u5206\u949F\u5185\u5E38\u89C1\uFF09\uFF0C\u7A0D\u540E\u6309 Enter \u91CD\u8BD5\u3002",
1241
+ "update.errorNotApplied": "npm \u62A5\u544A\u6210\u529F\uFF0C\u4F46\u5B89\u88C5\u7684\u4ECD\u662F {installed}",
1242
+ "update.mirrorBehind": "{registries} \u4ECD\u662F\u65E7\u7248\u672C\uFF0C\u5C06\u4ECE {source} \u5B89\u88C5",
1152
1243
  "update.cannot.source": "\u6E90\u7801\u76EE\u5F55\u8FD0\u884C\uFF0C\u8BF7\u4F7F\u7528 git \u66F4\u65B0",
1153
1244
  "update.cannot.linked": "npm link \u5DE5\u4F5C\u526F\u672C\uFF0C\u7531 npm link \u7BA1\u7406"
1154
1245
  };
@@ -1334,6 +1425,7 @@ var require_settings_model = __commonJS({
1334
1425
  { id: "publicUrl", kind: "text", labelKey: "field.publicUrl" },
1335
1426
  { id: "socketPath", kind: "text", labelKey: "field.socketPath" },
1336
1427
  { id: "herdrArgs", kind: "text", labelKey: "field.herdrArgs" },
1428
+ { id: "herdrAutoStart", kind: "toggle", labelKey: "field.herdrAutoStart" },
1337
1429
  { id: "language", kind: "choice", choices: LANGUAGES, labelKey: "field.language" },
1338
1430
  { id: "keepaliveManager", kind: "choice", choices: KEEPALIVE_MANAGERS, labelKey: "field.keepalive" }
1339
1431
  ];
@@ -1371,6 +1463,8 @@ var require_settings_model = __commonJS({
1371
1463
  return draft.herdr.socketPath || EMPTY;
1372
1464
  case "herdrArgs":
1373
1465
  return (draft.herdr.args || []).join(" ");
1466
+ case "herdrAutoStart":
1467
+ return draft.herdr.autoStart ? "on" : "off";
1374
1468
  case "language":
1375
1469
  return draft.ui.language;
1376
1470
  case "keepaliveManager":
@@ -1464,6 +1558,13 @@ var require_settings_model = __commonJS({
1464
1558
  next.herdr.args = args;
1465
1559
  break;
1466
1560
  }
1561
+ case "herdrAutoStart": {
1562
+ if (value !== true && value !== false && value !== "on" && value !== "off") {
1563
+ return { draft, errorKey: "error.unknownField" };
1564
+ }
1565
+ next.herdr.autoStart = value === true || value === "on";
1566
+ break;
1567
+ }
1467
1568
  case "language": {
1468
1569
  if (!LANGUAGES.includes(value)) return { draft, errorKey: "error.invalidLanguage" };
1469
1570
  next.ui.language = value;
@@ -1509,7 +1610,8 @@ var require_settings_model = __commonJS({
1509
1610
  merged.herdr = {
1510
1611
  ...current.herdr || {},
1511
1612
  socketPath: draft.herdr.socketPath,
1512
- args: draft.herdr.args
1613
+ args: draft.herdr.args,
1614
+ autoStart: draft.herdr.autoStart === true
1513
1615
  };
1514
1616
  if (!merged.herdr.socketPath) delete merged.herdr.socketPath;
1515
1617
  merged.keepalive = { ...current.keepalive || {}, manager: draft.keepalive.manager };
@@ -1758,7 +1860,12 @@ var require_socket_discovery = __commonJS({
1758
1860
  }
1759
1861
  return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
1760
1862
  } catch (error) {
1761
- return { ok: false, reason: error.code === "ENOENT" ? "socket does not exist" : error.message, path: socketPath };
1863
+ return {
1864
+ ok: false,
1865
+ missing: error.code === "ENOENT",
1866
+ reason: error.code === "ENOENT" ? "socket does not exist" : error.message,
1867
+ path: socketPath
1868
+ };
1762
1869
  }
1763
1870
  }
1764
1871
  function assertSocket(socketPath, options = {}) {
@@ -1810,7 +1917,7 @@ var require_service = __commonJS({
1810
1917
  } = require_terminal_palette();
1811
1918
  var { resolveSocketPath } = require_socket_discovery();
1812
1919
  var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
1813
- var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion, resolveHerdrCommand } = require_herdr_command();
1920
+ var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion: herdrVersion2, resolveHerdrCommand } = require_herdr_command();
1814
1921
  var RUNTIME_VERSION = 2;
1815
1922
  function pidAlive(pid) {
1816
1923
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -2117,7 +2224,7 @@ var require_service = __commonJS({
2117
2224
  }
2118
2225
  const socketPath = resolveSocketPath(config.herdr.socketPath);
2119
2226
  const herdr = findHerdrCommand();
2120
- const installed = herdr.found ? herdrVersion({ command: herdr.command }) : { version: null, supported: true };
2227
+ const installed = herdr.found ? herdrVersion2({ command: herdr.command }) : { version: null, supported: true };
2121
2228
  return {
2122
2229
  ok: true,
2123
2230
  mode: config.relay.mode,
@@ -2580,8 +2687,8 @@ var require_lifecycle = __commonJS({
2580
2687
  function startAll2(config = loadConfig2()) {
2581
2688
  const managed = managerInUse(config);
2582
2689
  if (managed) {
2583
- keepalive2.restart(config);
2584
- return { ok: true, managed: true, manager: managed.manager };
2690
+ if (!managed.active) keepalive2.restart(config);
2691
+ return { ok: true, managed: true, manager: managed.manager, alreadyRunning: Boolean(managed.active) };
2585
2692
  }
2586
2693
  return { ...startServices(), managed: false };
2587
2694
  }
@@ -2613,116 +2720,11 @@ var require_lifecycle = __commonJS({
2613
2720
  }
2614
2721
  });
2615
2722
 
2616
- // src/herdr-plugin.js
2617
- var require_herdr_plugin = __commonJS({
2618
- "src/herdr-plugin.js"(exports, module) {
2619
- "use strict";
2620
- var fs = __require("node:fs");
2621
- var path = __require("node:path");
2622
- var { spawnSync } = __require("node:child_process");
2623
- var { PACKAGE_ROOT } = require_config();
2624
- var { herdrVersion, resolveHerdrCommand } = require_herdr_command();
2625
- var PLUGIN_ID = "herdr.remote.web";
2626
- var MANIFEST_NAME = "herdr-plugin.toml";
2627
- function manifestPath() {
2628
- return path.join(PACKAGE_ROOT, MANIFEST_NAME);
2629
- }
2630
- function herdrAvailable() {
2631
- return herdrVersion().ok;
2632
- }
2633
- function runHerdr(args, { timeout = 15e3 } = {}) {
2634
- const result = spawnSync(resolveHerdrCommand(), args, { encoding: "utf8", timeout });
2635
- if (result.error && result.error.code === "ENOENT") {
2636
- const error = new Error("herdr command not found");
2637
- error.code = "HERDR_NOT_FOUND";
2638
- throw error;
2639
- }
2640
- return result;
2641
- }
2642
- function parsePluginList(output) {
2643
- const plugins = [];
2644
- for (const line of String(output || "").split("\n")) {
2645
- const match = /^-\s+(\S+)\s+\((.*?)\)\s+(\S+)(?:\s+\[(.*)\])?/.exec(line.trim());
2646
- if (!match) continue;
2647
- const [, id, name, state, rawSource] = match;
2648
- const source = rawSource ? rawSource.split(";")[0].trim() : null;
2649
- plugins.push({
2650
- id,
2651
- name,
2652
- enabled: state === "enabled",
2653
- source,
2654
- warnings: rawSource && rawSource.includes(";") ? rawSource.slice(rawSource.indexOf(";") + 1).trim() : null,
2655
- localPath: source && source.startsWith("local:") ? source.slice("local:".length) : null
2656
- });
2657
- }
2658
- return plugins;
2659
- }
2660
- function registrationStatus() {
2661
- if (!fs.existsSync(manifestPath())) {
2662
- return { available: false, registered: false, reason: "manifest missing" };
2663
- }
2664
- const { version, supported } = herdrVersion();
2665
- const installed = { version, versionSupported: supported };
2666
- let result;
2667
- try {
2668
- result = runHerdr(["plugin", "list"]);
2669
- } catch (error) {
2670
- if (error.code === "HERDR_NOT_FOUND") return { available: false, registered: false, reason: "herdr not found" };
2671
- throw error;
2672
- }
2673
- if (result.status !== 0) {
2674
- return { available: true, registered: false, reason: String(result.stderr || "").trim(), ...installed };
2675
- }
2676
- const plugins = parsePluginList(result.stdout);
2677
- const entry = plugins.find((plugin) => plugin.id === PLUGIN_ID);
2678
- if (!entry) return { available: true, registered: false, packageRoot: PACKAGE_ROOT, ...installed };
2679
- return {
2680
- available: true,
2681
- registered: true,
2682
- enabled: entry.enabled,
2683
- linkedPath: entry.localPath,
2684
- // A stale link pointing at an old checkout is the main failure mode after
2685
- // switching from a source install to npm.
2686
- stale: Boolean(entry.localPath) && path.resolve(entry.localPath) !== path.resolve(PACKAGE_ROOT),
2687
- packageRoot: PACKAGE_ROOT,
2688
- ...installed
2689
- };
2690
- }
2691
- function register() {
2692
- const current = registrationStatus();
2693
- if (current.registered && current.stale) {
2694
- runHerdr(["plugin", "unlink", PLUGIN_ID]);
2695
- }
2696
- const result = runHerdr(["plugin", "link", PACKAGE_ROOT]);
2697
- if (result.status !== 0) {
2698
- throw new Error(String(result.stderr || result.stdout || "").trim() || "herdr plugin link failed");
2699
- }
2700
- return { ok: true, path: PACKAGE_ROOT, output: String(result.stdout || "").trim() };
2701
- }
2702
- function unregister() {
2703
- const result = runHerdr(["plugin", "unlink", PLUGIN_ID]);
2704
- if (result.status !== 0) {
2705
- throw new Error(String(result.stderr || result.stdout || "").trim() || "herdr plugin unlink failed");
2706
- }
2707
- return { ok: true, output: String(result.stdout || "").trim() };
2708
- }
2709
- module.exports = {
2710
- PLUGIN_ID,
2711
- manifestPath,
2712
- herdrAvailable,
2713
- parsePluginList,
2714
- registrationStatus,
2715
- register,
2716
- unregister
2717
- };
2718
- }
2719
- });
2720
-
2721
2723
  // tui/src/index.tsx
2722
2724
  import { render } from "ink";
2723
2725
 
2724
2726
  // tui/src/App.tsx
2725
- import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef3, useState as useState10 } from "react";
2727
+ import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef3, useState as useState10 } from "react";
2726
2728
  import { Box as Box11, Text as Text11, useApp, useInput as useInput9 } from "ink";
2727
2729
 
2728
2730
  // tui/src/mouse/index.tsx
@@ -3026,7 +3028,6 @@ var import_settings_model = __toESM(require_settings_model());
3026
3028
  var import_service = __toESM(require_service());
3027
3029
  var import_lifecycle = __toESM(require_lifecycle());
3028
3030
  var keepalive = __toESM(require_keepalive());
3029
- var herdrPlugin = __toESM(require_herdr_plugin());
3030
3031
  var import_updater = __toESM(require_updater());
3031
3032
  function detectLocale(options = {}) {
3032
3033
  return (0, import_i18n.detectLocale)(options);
@@ -3808,24 +3809,15 @@ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3808
3809
  function HerdrScreen({ ctx }) {
3809
3810
  const { t, draft, editingId } = ctx;
3810
3811
  const [selected, setSelected] = useState7("socketPath");
3811
- const [registration, setRegistration] = useState7(null);
3812
- const reloadRegistration = () => {
3813
- try {
3814
- setRegistration(herdrPlugin.registrationStatus());
3815
- } catch (error) {
3816
- ctx.notify(error.message, "error");
3817
- }
3818
- };
3819
- useEffect5(reloadRegistration, []);
3812
+ const [installed, setInstalled] = useState7(null);
3813
+ useEffect5(() => {
3814
+ setInstalled((0, import_herdr_command.herdrVersion)());
3815
+ }, []);
3820
3816
  const entries = [
3821
3817
  { id: "socketPath", kind: "field", label: t("herdr.socketPath") },
3822
3818
  { id: "herdrArgs", kind: "field", label: t("herdr.args") },
3823
- { id: "save", kind: "action", label: t("common.save") },
3824
- {
3825
- id: registration?.registered && !registration?.stale ? "unregister" : "register",
3826
- kind: "action",
3827
- label: registration?.registered && !registration?.stale ? t("herdr.unregister") : t("herdr.register")
3828
- }
3819
+ { id: "herdrAutoStart", kind: "toggle", label: t("field.herdrAutoStart") },
3820
+ { id: "save", kind: "action", label: t("common.save") }
3829
3821
  ];
3830
3822
  const applyField = (id, value) => {
3831
3823
  const result = (0, import_settings_model.setField)(draft, id, value);
@@ -3850,26 +3842,11 @@ function HerdrScreen({ ctx }) {
3850
3842
  ctx.setEditing(id);
3851
3843
  return;
3852
3844
  }
3853
- if (id === "save") {
3854
- save();
3845
+ if (id === "herdrAutoStart") {
3846
+ applyField(id, draft.herdr.autoStart ? "off" : "on");
3855
3847
  return;
3856
3848
  }
3857
- ctx.run(() => {
3858
- try {
3859
- if (id === "register") {
3860
- const result = herdrPlugin.register();
3861
- ctx.notify(t("herdr.registerDone", { path: result.path }), "success");
3862
- } else {
3863
- herdrPlugin.unregister();
3864
- ctx.notify(t("herdr.unregisterDone"), "success");
3865
- }
3866
- } catch (error) {
3867
- const failure = error;
3868
- throw new Error(failure.code === "HERDR_NOT_FOUND" ? t("herdr.cliMissing") : t("herdr.registerFailed", { message: failure.message }));
3869
- } finally {
3870
- reloadRegistration();
3871
- }
3872
- });
3849
+ if (id === "save") save();
3873
3850
  };
3874
3851
  useInput6((input, key) => {
3875
3852
  const index = entries.findIndex((entry) => entry.id === selected);
@@ -3880,7 +3857,20 @@ function HerdrScreen({ ctx }) {
3880
3857
  else if (input === "s") save();
3881
3858
  }, { isActive: editingId === null });
3882
3859
  return /* @__PURE__ */ jsxs8(Panel, { title: t("herdr.title"), children: [
3883
- entries.map((entry) => entry.kind === "field" ? /* @__PURE__ */ jsx9(
3860
+ entries.map((entry) => entry.kind === "toggle" ? /* @__PURE__ */ jsx9(
3861
+ FieldRow,
3862
+ {
3863
+ label: entry.label,
3864
+ selected: selected === entry.id,
3865
+ onSelect: () => {
3866
+ setSelected(entry.id);
3867
+ activate(entry.id);
3868
+ },
3869
+ onHover: () => setSelected(entry.id),
3870
+ children: /* @__PURE__ */ jsx9(Text8, { color: draft.herdr.autoStart ? theme.ok : theme.muted, children: draft.herdr.autoStart ? `[\xD7] ${t("common.on")}` : `[ ] ${t("common.off")}` })
3871
+ },
3872
+ entry.id
3873
+ ) : entry.kind === "field" ? /* @__PURE__ */ jsx9(
3884
3874
  FieldRow,
3885
3875
  {
3886
3876
  label: entry.label,
@@ -3913,41 +3903,31 @@ function HerdrScreen({ ctx }) {
3913
3903
  children: entry.label
3914
3904
  }
3915
3905
  ) }, entry.id)),
3906
+ selected === "herdrAutoStart" ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: theme.muted, children: t("herdr.autoStartHint") }) }) : null,
3916
3907
  ctx.dirty ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("hint.unsavedChanges") }) }) : null,
3917
3908
  /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
3918
- /* @__PURE__ */ jsx9(Row, { label: t("herdr.plugin"), children: /* @__PURE__ */ jsxs8(Box8, { children: [
3919
- /* @__PURE__ */ jsx9(StatusDot, { level: registration?.registered && !registration?.stale ? "ok" : registration?.stale ? "warn" : "idle" }),
3920
- /* @__PURE__ */ jsxs8(Text8, { children: [
3921
- " ",
3922
- registration?.registered ? t("herdr.pluginRegistered") : t("herdr.pluginMissing")
3923
- ] })
3924
- ] }) }),
3925
- registration?.packageRoot ? /* @__PURE__ */ jsx9(Row, { label: "", children: /* @__PURE__ */ jsx9(Text8, { color: theme.muted, children: registration.packageRoot }) }) : null,
3926
- registration?.version ? /* @__PURE__ */ jsx9(Row, { label: t("herdr.version"), children: /* @__PURE__ */ jsxs8(Box8, { children: [
3927
- /* @__PURE__ */ jsx9(StatusDot, { level: registration.versionSupported === false ? "warn" : "ok" }),
3928
- /* @__PURE__ */ jsx9(Text8, { children: ` ${registration.version}` })
3909
+ installed?.version ? /* @__PURE__ */ jsx9(Row, { label: t("herdr.version"), children: /* @__PURE__ */ jsxs8(Box8, { children: [
3910
+ /* @__PURE__ */ jsx9(StatusDot, { level: installed.supported ? "ok" : "warn" }),
3911
+ /* @__PURE__ */ jsx9(Text8, { children: ` ${installed.version}` })
3929
3912
  ] }) }) : null,
3930
- registration?.versionSupported === false ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.versionOutdated", { version: registration.version ?? "", minimum: import_herdr_command.MIN_HERDR_VERSION }) }) : null,
3931
- registration && !registration.available ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.cliMissing") }) : null,
3932
- registration?.stale && registration.linkedPath ? (
3933
- // A leftover link to an old source checkout is the usual state after
3934
- // switching to the npm install; re-registering repoints it.
3935
- /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: registration.linkedPath })
3936
- ) : null
3913
+ installed?.version && !installed.supported ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.versionOutdated", { version: installed.version, minimum: import_herdr_command.MIN_HERDR_VERSION }) }) : null,
3914
+ installed && !installed.ok ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.cliMissing") }) : null
3937
3915
  ] }),
3938
3916
  /* @__PURE__ */ jsx9(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3939
3917
  ] });
3940
3918
  }
3941
3919
 
3942
3920
  // tui/src/screens/About.tsx
3943
- import { useState as useState8 } from "react";
3921
+ import { useEffect as useEffect6, useState as useState8 } from "react";
3944
3922
  import { Box as Box9, Text as Text9, useInput as useInput7 } from "ink";
3945
3923
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3924
+ var registryHost = (registry) => registry.replace(/^https?:\/\//, "");
3946
3925
  function About({ ctx }) {
3947
3926
  const { t, draft } = ctx;
3948
3927
  const [selected, setSelected] = useState8("auto");
3949
3928
  const [update, setUpdate] = useState8({ phase: "idle" });
3950
- const registryRef = useState8(() => ({ current: "" }))[0];
3929
+ const [sourceNote, setSourceNote] = useState8(null);
3930
+ const checkRef = useState8(() => ({ current: { registry: "", sources: [] } }))[0];
3951
3931
  const detected = detectLocale({ preference: "auto" });
3952
3932
  const languageOptions = [
3953
3933
  { id: "auto", label: t("about.languageAuto", { detected }) },
@@ -3964,36 +3944,57 @@ function About({ ctx }) {
3964
3944
  case "available":
3965
3945
  return t("update.available", { version: update.latest });
3966
3946
  case "updating":
3967
- return t("update.updating", { version: update.latest });
3947
+ return update.attempt > 1 ? t("update.updatingRetry", { version: update.latest, attempt: update.attempt }) : t("update.updating", { version: update.latest });
3968
3948
  case "done":
3969
3949
  return t("update.done", { version: update.latest });
3970
3950
  case "error":
3971
- return t(update.messageKey);
3951
+ return t(update.messageKey, update.params);
3972
3952
  default:
3973
3953
  return t("update.check");
3974
3954
  }
3975
3955
  })();
3976
3956
  const options = [...languageOptions, { id: "update", label: updateLabel }];
3957
+ const applyCheck = (result) => {
3958
+ checkRef.current = { registry: result.registry ?? "", sources: result.sources ?? [] };
3959
+ const behind = result.behind ?? [];
3960
+ setSourceNote(result.updateAvailable && behind.length > 0 && result.registry ? t("update.mirrorBehind", {
3961
+ registries: behind.map((entry) => `${registryHost(entry.registry)} ${entry.version ?? ""}`.trim()).join(", "),
3962
+ source: registryHost(result.registry)
3963
+ }) : null);
3964
+ setUpdate(result.updateAvailable ? { phase: "available", latest: result.latest } : { phase: "current", latest: result.latest });
3965
+ };
3966
+ useEffect6(() => {
3967
+ if (ctx.updateCheck?.ok && update.phase === "idle") applyCheck(ctx.updateCheck);
3968
+ }, [ctx.updateCheck]);
3977
3969
  const runCheck = async () => {
3978
3970
  setUpdate({ phase: "checking" });
3971
+ setSourceNote(null);
3979
3972
  const result = await (0, import_updater.checkForUpdate)();
3980
3973
  if (!result.ok) {
3981
3974
  setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorNetwork" });
3982
3975
  if (result.message) ctx.notify(t("update.errorNetworkDetail", { message: result.message }), "error");
3983
3976
  return;
3984
3977
  }
3985
- registryRef.current = result.registry ?? "";
3986
- setUpdate(result.updateAvailable ? { phase: "available", latest: result.latest } : { phase: "current", latest: result.latest });
3978
+ ctx.setUpdateCheck(result);
3979
+ applyCheck(result);
3987
3980
  };
3988
3981
  const runUpdate = async (latest) => {
3989
- setUpdate({ phase: "updating", latest });
3990
- const result = await (0, import_updater.performUpdate)({ registry: registryRef.current });
3982
+ setUpdate({ phase: "updating", latest, attempt: 1 });
3983
+ const result = await (0, import_updater.performUpdate)({
3984
+ registry: checkRef.current.registry,
3985
+ sources: checkRef.current.sources,
3986
+ version: latest,
3987
+ onAttempt: ({ attempt }) => setUpdate({ phase: "updating", latest, attempt })
3988
+ });
3991
3989
  if (!result.ok) {
3992
- setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorFailed" });
3993
- ctx.notify(t("update.errorFailed"), "error");
3990
+ const params = { version: latest, installed: result.installed ?? "" };
3991
+ setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorFailed", params });
3992
+ ctx.notify(result.summary ? t("update.errorFailedDetail", { message: result.summary }) : t(result.errorKey ?? "update.errorFailed", params), "error");
3994
3993
  return;
3995
3994
  }
3996
3995
  setUpdate({ phase: "done", latest });
3996
+ setSourceNote(null);
3997
+ ctx.setUpdateCheck(null);
3997
3998
  ctx.notify(t("update.restartHint"), "success");
3998
3999
  };
3999
4000
  const activateUpdate = () => {
@@ -4052,19 +4053,22 @@ function About({ ctx }) {
4052
4053
  option.id
4053
4054
  )) }),
4054
4055
  /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("update.title") }),
4055
- /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx10(
4056
- Selectable,
4057
- {
4058
- selected: selected === "update",
4059
- onSelect: () => {
4060
- setSelected("update");
4061
- activateUpdate();
4062
- },
4063
- onHover: () => setSelected("update"),
4064
- children: updateLabel
4065
- }
4066
- ) }),
4067
- /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.15" }) }),
4056
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
4057
+ /* @__PURE__ */ jsx10(
4058
+ Selectable,
4059
+ {
4060
+ selected: selected === "update",
4061
+ onSelect: () => {
4062
+ setSelected("update");
4063
+ activateUpdate();
4064
+ },
4065
+ onHover: () => setSelected("update"),
4066
+ children: updateLabel
4067
+ }
4068
+ ),
4069
+ sourceNote ? /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: ` ${sourceNote}` }) : null
4070
+ ] }),
4071
+ /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.17" }) }),
4068
4072
  /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
4069
4073
  /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
4070
4074
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
@@ -4381,7 +4385,11 @@ function Tab({ label, active, onSelect }) {
4381
4385
  function Footer({ hints }) {
4382
4386
  return /* @__PURE__ */ jsx12(Box11, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: hints.join(" \xB7 ") }) });
4383
4387
  }
4384
- function App({ initialLanguage, needsWizard }) {
4388
+ function App({
4389
+ initialLanguage,
4390
+ needsWizard,
4391
+ updateChecker = (0, import_updater.updateChecksEnabled)() ? import_updater.checkForUpdate : null
4392
+ }) {
4385
4393
  const { exit } = useApp();
4386
4394
  const mouse = useMouse();
4387
4395
  const [config, setConfig] = useState10(() => (0, import_config.loadConfig)());
@@ -4393,6 +4401,18 @@ function App({ initialLanguage, needsWizard }) {
4393
4401
  const [busy, setBusy] = useState10(false);
4394
4402
  const [editingId, setEditingId] = useState10(null);
4395
4403
  const [wizardDone, setWizardDone] = useState10(!needsWizard);
4404
+ const [updateCheck, setUpdateCheck] = useState10(null);
4405
+ useEffect7(() => {
4406
+ if (!updateChecker) return void 0;
4407
+ let cancelled = false;
4408
+ updateChecker().then((result) => {
4409
+ if (!cancelled && result?.ok) setUpdateCheck(result);
4410
+ }).catch(() => {
4411
+ });
4412
+ return () => {
4413
+ cancelled = true;
4414
+ };
4415
+ }, [updateChecker]);
4396
4416
  const locale = useMemo4(
4397
4417
  () => detectLocale({ preference: initialLanguage ?? draft.ui.language }),
4398
4418
  [initialLanguage, draft.ui.language]
@@ -4419,7 +4439,7 @@ function App({ initialLanguage, needsWizard }) {
4419
4439
  refresh();
4420
4440
  });
4421
4441
  }, [notify, refresh]);
4422
- useEffect6(() => {
4442
+ useEffect7(() => {
4423
4443
  if (!wizardDone) return void 0;
4424
4444
  refresh();
4425
4445
  const timer = setInterval(() => {
@@ -4428,7 +4448,7 @@ function App({ initialLanguage, needsWizard }) {
4428
4448
  return () => clearInterval(timer);
4429
4449
  }, [wizardDone, refresh, editingId]);
4430
4450
  const mouseChoice = useRef3(null);
4431
- useEffect6(() => {
4451
+ useEffect7(() => {
4432
4452
  if (mouseChoice.current !== null) return;
4433
4453
  if (mouse.supported && !mouse.enabled) mouse.enable();
4434
4454
  }, [mouse.supported, mouse.enabled, mouse]);
@@ -4448,7 +4468,9 @@ function App({ initialLanguage, needsWizard }) {
4448
4468
  notify,
4449
4469
  run,
4450
4470
  refresh,
4451
- message
4471
+ message,
4472
+ updateCheck,
4473
+ setUpdateCheck
4452
4474
  };
4453
4475
  useInput9((input, key) => {
4454
4476
  if (key.ctrl && input === "c") {
@@ -4508,9 +4530,12 @@ function App({ initialLanguage, needsWizard }) {
4508
4530
  t("hint.quit")
4509
4531
  ].filter(Boolean);
4510
4532
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
4511
- /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, children: [
4512
- /* @__PURE__ */ jsx12(Text11, { bold: true, children: t("app.name") }),
4513
- /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: ` ${t("app.tagline")}` })
4533
+ /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, flexDirection: "column", children: [
4534
+ /* @__PURE__ */ jsxs11(Box11, { children: [
4535
+ /* @__PURE__ */ jsx12(Text11, { bold: true, children: t("app.name") }),
4536
+ /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: ` ${t("app.tagline")}` })
4537
+ ] }),
4538
+ updateCheck?.updateAvailable ? /* @__PURE__ */ jsx12(Text11, { color: theme.warn, children: t("update.banner", { latest: updateCheck.latest ?? "", current: updateCheck.current }) }) : null
4514
4539
  ] }),
4515
4540
  /* @__PURE__ */ jsx12(Box11, { marginBottom: 1, children: TABS.map((entry, entryIndex) => /* @__PURE__ */ jsx12(
4516
4541
  Tab,