herdr-remote 0.2.12 → 0.2.14

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
@@ -383,6 +383,324 @@ var require_config = __commonJS({
383
383
  }
384
384
  });
385
385
 
386
+ // src/updater.js
387
+ var require_updater = __commonJS({
388
+ "src/updater.js"(exports, module) {
389
+ "use strict";
390
+ var fs = __require("node:fs");
391
+ var os = __require("node:os");
392
+ var path = __require("node:path");
393
+ var { spawn } = __require("node:child_process");
394
+ var { PACKAGE_ROOT } = require_config();
395
+ var PACKAGE_NAME = "herdr-remote";
396
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
397
+ var MIRROR_REGISTRY = "https://registry.npmmirror.com";
398
+ function currentVersion() {
399
+ try {
400
+ return __require(path.join(PACKAGE_ROOT, "package.json")).version;
401
+ } catch {
402
+ return "0.0.0";
403
+ }
404
+ }
405
+ function installKind2() {
406
+ const root = PACKAGE_ROOT;
407
+ if (!root.split(path.sep).includes("node_modules")) return "source";
408
+ try {
409
+ if (fs.lstatSync(root).isSymbolicLink()) return "linked";
410
+ } catch {
411
+ }
412
+ return "npm";
413
+ }
414
+ function canSelfUpdate2() {
415
+ return installKind2() === "npm";
416
+ }
417
+ function normalizeRegistry(value) {
418
+ if (typeof value !== "string") return null;
419
+ const trimmed = value.trim().replace(/\/+$/, "");
420
+ if (!/^https?:\/\//i.test(trimmed)) return null;
421
+ return trimmed;
422
+ }
423
+ function registryFromNpmrc(filePath) {
424
+ let contents;
425
+ try {
426
+ contents = fs.readFileSync(filePath, "utf8");
427
+ } catch {
428
+ return null;
429
+ }
430
+ let found = null;
431
+ for (const rawLine of contents.split(/\r?\n/)) {
432
+ const line = rawLine.trim();
433
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
434
+ const match = /^registry\s*=\s*(.+)$/i.exec(line);
435
+ if (match) found = normalizeRegistry(match[1].replace(/^["']|["']$/g, ""));
436
+ }
437
+ return found;
438
+ }
439
+ function registryCandidates({ env = process.env, home = os.homedir(), cwd = process.cwd() } = {}) {
440
+ const candidates = [];
441
+ const add = (value) => {
442
+ const normalized = normalizeRegistry(value);
443
+ if (normalized && !candidates.includes(normalized)) candidates.push(normalized);
444
+ };
445
+ add(env.npm_config_registry);
446
+ add(env.NPM_CONFIG_REGISTRY);
447
+ add(env.HERDR_REMOTE_REGISTRY);
448
+ add(registryFromNpmrc(path.join(cwd, ".npmrc")));
449
+ add(registryFromNpmrc(path.join(home, ".npmrc")));
450
+ add(DEFAULT_REGISTRY);
451
+ add(MIRROR_REGISTRY);
452
+ return candidates;
453
+ }
454
+ function compareVersions(a, b) {
455
+ const parse = (value) => String(value).split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
456
+ const left = parse(a);
457
+ const right = parse(b);
458
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
459
+ const diff = (left[index] || 0) - (right[index] || 0);
460
+ if (diff > 0) return 1;
461
+ if (diff < 0) return -1;
462
+ }
463
+ return 0;
464
+ }
465
+ async function askRegistry(registry, { timeoutMs, fetchImpl }) {
466
+ const controller = new AbortController();
467
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
468
+ try {
469
+ const response = await fetchImpl(`${registry}/${PACKAGE_NAME}/latest`, {
470
+ signal: controller.signal,
471
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
472
+ });
473
+ 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 };
478
+ } catch (error) {
479
+ return { ok: false, message: error?.name === "AbortError" ? "timed out" : String(error?.message || error) };
480
+ } finally {
481
+ clearTimeout(timer);
482
+ }
483
+ }
484
+ async function checkForUpdate2({
485
+ timeoutMs = 6e3,
486
+ fetchImpl = globalThis.fetch,
487
+ registries = registryCandidates()
488
+ } = {}) {
489
+ 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}`);
503
+ }
504
+ return {
505
+ ok: false,
506
+ current,
507
+ errorKey: "update.errorNetwork",
508
+ message: failures.join("; "),
509
+ triedRegistries: registries
510
+ };
511
+ }
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
+ } = {}) {
524
+ return new Promise((resolve) => {
525
+ const kind = installKindImpl();
526
+ if (kind !== "npm") {
527
+ resolve({ ok: false, errorKey: `update.cannot.${kind}` });
528
+ return;
529
+ }
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
+ let output = "";
538
+ const collect = (chunk) => {
539
+ output += String(chunk);
540
+ };
541
+ child.stdout?.on("data", collect);
542
+ 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
+ });
549
+ });
550
+ }
551
+ module.exports = {
552
+ PACKAGE_NAME,
553
+ DEFAULT_REGISTRY,
554
+ MIRROR_REGISTRY,
555
+ canSelfUpdate: canSelfUpdate2,
556
+ checkForUpdate: checkForUpdate2,
557
+ compareVersions,
558
+ currentVersion,
559
+ installKind: installKind2,
560
+ performUpdate: performUpdate2,
561
+ registryCandidates
562
+ };
563
+ }
564
+ });
565
+
566
+ // src/herdr-command.js
567
+ var require_herdr_command = __commonJS({
568
+ "src/herdr-command.js"(exports, module) {
569
+ "use strict";
570
+ var fs = __require("node:fs");
571
+ var os = __require("node:os");
572
+ var path = __require("node:path");
573
+ var { spawnSync } = __require("node:child_process");
574
+ var { compareVersions } = require_updater();
575
+ var COMMAND_NAME = "herdr";
576
+ var MIN_HERDR_VERSION2 = "0.9.1";
577
+ var FALLBACK_DIRECTORIES = [
578
+ "~/.local/bin",
579
+ "~/.cargo/bin",
580
+ "~/bin",
581
+ "/opt/homebrew/bin",
582
+ "/usr/local/bin",
583
+ "/home/linuxbrew/.linuxbrew/bin",
584
+ "/usr/bin"
585
+ ];
586
+ function expandHome(directory, home) {
587
+ if (directory === "~") return home;
588
+ if (directory.startsWith("~/")) return path.join(home, directory.slice(2));
589
+ return directory;
590
+ }
591
+ function candidateNames(base = COMMAND_NAME) {
592
+ return process.platform === "win32" ? [base, `${base}.exe`, `${base}.cmd`, `${base}.bat`] : [base];
593
+ }
594
+ function isExecutableFile(candidate) {
595
+ try {
596
+ if (!fs.statSync(candidate).isFile()) return false;
597
+ fs.accessSync(candidate, fs.constants.X_OK);
598
+ return true;
599
+ } catch {
600
+ return false;
601
+ }
602
+ }
603
+ function findInDirectory(directory, names = candidateNames()) {
604
+ if (!directory) return null;
605
+ for (const name of names) {
606
+ const candidate = path.join(directory, name);
607
+ if (isExecutableFile(candidate)) return candidate;
608
+ }
609
+ return null;
610
+ }
611
+ function findOnSearchPath(searchPath, names = candidateNames()) {
612
+ for (const entry of String(searchPath || "").split(path.delimiter)) {
613
+ const found = findInDirectory(entry.trim(), names);
614
+ if (found) return found;
615
+ }
616
+ return null;
617
+ }
618
+ function fallbackDirectories(home = os.homedir()) {
619
+ return FALLBACK_DIRECTORIES.map((directory) => expandHome(directory, home));
620
+ }
621
+ function looksLikePath(value) {
622
+ return value.includes("/") || value.includes(path.sep);
623
+ }
624
+ function resolveOverride(value, searchPath) {
625
+ const trimmed = String(value || "").trim();
626
+ if (!trimmed) return null;
627
+ if (looksLikePath(trimmed)) {
628
+ const absolute = path.resolve(trimmed);
629
+ if (isExecutableFile(absolute)) return absolute;
630
+ return findInDirectory(absolute);
631
+ }
632
+ return findOnSearchPath(searchPath, candidateNames(trimmed));
633
+ }
634
+ function findHerdrCommand({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home) } = {}) {
635
+ const override = resolveOverride(env.HERDR_BIN_PATH, env.PATH);
636
+ if (override) return { command: override, source: "env", found: true };
637
+ const onPath = findOnSearchPath(env.PATH);
638
+ if (onPath) return { command: onPath, source: "path", found: true };
639
+ for (const directory of directories) {
640
+ const found = findInDirectory(directory);
641
+ if (found) return { command: found, source: "fallback", found: true };
642
+ }
643
+ return { command: COMMAND_NAME, source: "unresolved", found: false };
644
+ }
645
+ function resolveHerdrCommand(options) {
646
+ return findHerdrCommand(options).command;
647
+ }
648
+ function verifyHerdrCommand(command, options = {}) {
649
+ if (typeof command === "string" && looksLikePath(command) && isExecutableFile(command)) {
650
+ return { command, source: "verified", found: true };
651
+ }
652
+ return findHerdrCommand(options);
653
+ }
654
+ function parseHerdrVersion(output) {
655
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(String(output || ""));
656
+ return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
657
+ }
658
+ function meetsMinimum(version, minimum = MIN_HERDR_VERSION2) {
659
+ if (!version) return true;
660
+ return compareVersions(version, minimum) >= 0;
661
+ }
662
+ function herdrVersion({ command, timeout = 1e4, ...lookup } = {}) {
663
+ const resolved = command || resolveHerdrCommand(lookup);
664
+ const unknown = { command: resolved, version: null, raw: null, supported: true, ok: false };
665
+ let result;
666
+ try {
667
+ result = spawnSync(resolved, ["--version"], { encoding: "utf8", timeout });
668
+ } catch {
669
+ return unknown;
670
+ }
671
+ if (result.error || result.status !== 0 && result.status !== 1) return unknown;
672
+ const raw = `${result.stdout || ""}${result.stderr || ""}`.trim();
673
+ const version = parseHerdrVersion(raw);
674
+ return { command: resolved, version, raw: raw || null, supported: meetsMinimum(version), ok: true };
675
+ }
676
+ function herdrOutdatedMessage(version) {
677
+ return `Herdr ${version} is older than ${MIN_HERDR_VERSION2}, which herdr-remote is written against. Run "herdr update" \u2014 window titles, background machine activation and large pastes all misbehave in browser windows before that release.`;
678
+ }
679
+ function herdrNotFoundMessage({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home) } = {}) {
680
+ const override = String(env.HERDR_BIN_PATH || "").trim();
681
+ const parts = [`Herdr executable "${COMMAND_NAME}" was not found.`];
682
+ if (override) parts.push(`HERDR_BIN_PATH=${override} does not point at an executable.`);
683
+ parts.push(directories.length ? `Searched PATH and ${directories.join(", ")}.` : "Searched PATH.");
684
+ parts.push("Install Herdr or set HERDR_BIN_PATH to its full path, then restart herdr-remote.");
685
+ return parts.join(" ");
686
+ }
687
+ module.exports = {
688
+ COMMAND_NAME,
689
+ FALLBACK_DIRECTORIES,
690
+ MIN_HERDR_VERSION: MIN_HERDR_VERSION2,
691
+ fallbackDirectories,
692
+ findHerdrCommand,
693
+ herdrNotFoundMessage,
694
+ herdrOutdatedMessage,
695
+ herdrVersion,
696
+ meetsMinimum,
697
+ parseHerdrVersion,
698
+ resolveHerdrCommand,
699
+ verifyHerdrCommand
700
+ };
701
+ }
702
+ });
703
+
386
704
  // src/i18n/en.js
387
705
  var require_en = __commonJS({
388
706
  "src/i18n/en.js"(exports, module) {
@@ -530,6 +848,8 @@ var require_en = __commonJS({
530
848
  "herdr.unregisterDone": "Plugin unregistered.",
531
849
  "herdr.registerFailed": "Registration failed: {message}",
532
850
  "herdr.cliMissing": "Command herdr not found. Set HERDR_BIN_PATH to its full path.",
851
+ "herdr.version": "Herdr version",
852
+ "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.',
533
853
  "about.title": "Language & about",
534
854
  "about.language": "Interface language",
535
855
  "about.languageAuto": "Follow system ({detected})",
@@ -755,6 +1075,8 @@ var require_zh = __commonJS({
755
1075
  "herdr.unregisterDone": "\u5DF2\u53D6\u6D88\u6CE8\u518C\u3002",
756
1076
  "herdr.registerFailed": "\u6CE8\u518C\u5931\u8D25\uFF1A{message}",
757
1077
  "herdr.cliMissing": "\u672A\u627E\u5230 herdr \u547D\u4EE4\uFF0C\u53EF\u5C06 HERDR_BIN_PATH \u8BBE\u4E3A\u5176\u5B8C\u6574\u8DEF\u5F84\u3002",
1078
+ "herdr.version": "Herdr \u7248\u672C",
1079
+ "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",
758
1080
  "about.title": "\u8BED\u8A00\u4E0E\u5173\u4E8E",
759
1081
  "about.language": "\u754C\u9762\u8BED\u8A00",
760
1082
  "about.languageAuto": "\u8DDF\u968F\u7CFB\u7EDF\uFF08{detected}\uFF09",
@@ -1416,149 +1738,44 @@ var require_socket_discovery = __commonJS({
1416
1738
  const appData = env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
1417
1739
  return path.join(appData, "herdr", "herdr.sock");
1418
1740
  }
1419
- const configHome = env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
1420
- return path.join(configHome, "herdr", "herdr.sock");
1421
- }
1422
- function resolveSocketPath(configuredPath = null, env = process.env) {
1423
- return configuredPath || env.HERDR_SOCKET_PATH || defaultSocketPath(env);
1424
- }
1425
- function inspectSocket(socketPath, options = {}) {
1426
- if (typeof socketPath !== "string" || socketPath.length === 0) {
1427
- return { ok: false, reason: "socket path is empty", path: socketPath };
1428
- }
1429
- try {
1430
- const stat = fs.statSync(socketPath);
1431
- if (process.platform !== "win32" && !stat.isSocket()) {
1432
- return { ok: false, reason: "path is not a Unix socket", path: socketPath };
1433
- }
1434
- if (options.requireOwner !== false && typeof process.getuid === "function" && stat.uid !== process.getuid()) {
1435
- return { ok: false, reason: "socket is not owned by the current user", path: socketPath, uid: stat.uid };
1436
- }
1437
- return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
1438
- } catch (error) {
1439
- return { ok: false, reason: error.code === "ENOENT" ? "socket does not exist" : error.message, path: socketPath };
1440
- }
1441
- }
1442
- function assertSocket(socketPath, options = {}) {
1443
- const result = inspectSocket(socketPath, options);
1444
- if (!result.ok) {
1445
- const error = new Error(`Herdr socket unavailable at ${socketPath}: ${result.reason}`);
1446
- error.code = "HERDR_SOCKET_UNAVAILABLE";
1447
- error.details = result;
1448
- throw error;
1449
- }
1450
- return result;
1451
- }
1452
- module.exports = {
1453
- defaultSocketPath,
1454
- resolveSocketPath,
1455
- inspectSocket,
1456
- assertSocket
1457
- };
1458
- }
1459
- });
1460
-
1461
- // src/herdr-command.js
1462
- var require_herdr_command = __commonJS({
1463
- "src/herdr-command.js"(exports, module) {
1464
- "use strict";
1465
- var fs = __require("node:fs");
1466
- var os = __require("node:os");
1467
- var path = __require("node:path");
1468
- var COMMAND_NAME = "herdr";
1469
- var FALLBACK_DIRECTORIES = [
1470
- "~/.local/bin",
1471
- "~/.cargo/bin",
1472
- "~/bin",
1473
- "/opt/homebrew/bin",
1474
- "/usr/local/bin",
1475
- "/home/linuxbrew/.linuxbrew/bin",
1476
- "/usr/bin"
1477
- ];
1478
- function expandHome(directory, home) {
1479
- if (directory === "~") return home;
1480
- if (directory.startsWith("~/")) return path.join(home, directory.slice(2));
1481
- return directory;
1482
- }
1483
- function candidateNames(base = COMMAND_NAME) {
1484
- return process.platform === "win32" ? [base, `${base}.exe`, `${base}.cmd`, `${base}.bat`] : [base];
1485
- }
1486
- function isExecutableFile(candidate) {
1487
- try {
1488
- if (!fs.statSync(candidate).isFile()) return false;
1489
- fs.accessSync(candidate, fs.constants.X_OK);
1490
- return true;
1491
- } catch {
1492
- return false;
1493
- }
1494
- }
1495
- function findInDirectory(directory, names = candidateNames()) {
1496
- if (!directory) return null;
1497
- for (const name of names) {
1498
- const candidate = path.join(directory, name);
1499
- if (isExecutableFile(candidate)) return candidate;
1500
- }
1501
- return null;
1502
- }
1503
- function findOnSearchPath(searchPath, names = candidateNames()) {
1504
- for (const entry of String(searchPath || "").split(path.delimiter)) {
1505
- const found = findInDirectory(entry.trim(), names);
1506
- if (found) return found;
1507
- }
1508
- return null;
1509
- }
1510
- function fallbackDirectories(home = os.homedir()) {
1511
- return FALLBACK_DIRECTORIES.map((directory) => expandHome(directory, home));
1741
+ const configHome = env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
1742
+ return path.join(configHome, "herdr", "herdr.sock");
1512
1743
  }
1513
- function looksLikePath(value) {
1514
- return value.includes("/") || value.includes(path.sep);
1744
+ function resolveSocketPath(configuredPath = null, env = process.env) {
1745
+ return configuredPath || env.HERDR_SOCKET_PATH || defaultSocketPath(env);
1515
1746
  }
1516
- function resolveOverride(value, searchPath) {
1517
- const trimmed = String(value || "").trim();
1518
- if (!trimmed) return null;
1519
- if (looksLikePath(trimmed)) {
1520
- const absolute = path.resolve(trimmed);
1521
- if (isExecutableFile(absolute)) return absolute;
1522
- return findInDirectory(absolute);
1747
+ function inspectSocket(socketPath, options = {}) {
1748
+ if (typeof socketPath !== "string" || socketPath.length === 0) {
1749
+ return { ok: false, reason: "socket path is empty", path: socketPath };
1523
1750
  }
1524
- return findOnSearchPath(searchPath, candidateNames(trimmed));
1525
- }
1526
- function findHerdrCommand({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home) } = {}) {
1527
- const override = resolveOverride(env.HERDR_BIN_PATH, env.PATH);
1528
- if (override) return { command: override, source: "env", found: true };
1529
- const onPath = findOnSearchPath(env.PATH);
1530
- if (onPath) return { command: onPath, source: "path", found: true };
1531
- for (const directory of directories) {
1532
- const found = findInDirectory(directory);
1533
- if (found) return { command: found, source: "fallback", found: true };
1751
+ try {
1752
+ const stat = fs.statSync(socketPath);
1753
+ if (process.platform !== "win32" && !stat.isSocket()) {
1754
+ return { ok: false, reason: "path is not a Unix socket", path: socketPath };
1755
+ }
1756
+ if (options.requireOwner !== false && typeof process.getuid === "function" && stat.uid !== process.getuid()) {
1757
+ return { ok: false, reason: "socket is not owned by the current user", path: socketPath, uid: stat.uid };
1758
+ }
1759
+ return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
1760
+ } catch (error) {
1761
+ return { ok: false, reason: error.code === "ENOENT" ? "socket does not exist" : error.message, path: socketPath };
1534
1762
  }
1535
- return { command: COMMAND_NAME, source: "unresolved", found: false };
1536
- }
1537
- function resolveHerdrCommand(options) {
1538
- return findHerdrCommand(options).command;
1539
1763
  }
1540
- function verifyHerdrCommand(command, options = {}) {
1541
- if (typeof command === "string" && looksLikePath(command) && isExecutableFile(command)) {
1542
- return { command, source: "verified", found: true };
1764
+ function assertSocket(socketPath, options = {}) {
1765
+ const result = inspectSocket(socketPath, options);
1766
+ if (!result.ok) {
1767
+ const error = new Error(`Herdr socket unavailable at ${socketPath}: ${result.reason}`);
1768
+ error.code = "HERDR_SOCKET_UNAVAILABLE";
1769
+ error.details = result;
1770
+ throw error;
1543
1771
  }
1544
- return findHerdrCommand(options);
1545
- }
1546
- function herdrNotFoundMessage({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home) } = {}) {
1547
- const override = String(env.HERDR_BIN_PATH || "").trim();
1548
- const parts = [`Herdr executable "${COMMAND_NAME}" was not found.`];
1549
- if (override) parts.push(`HERDR_BIN_PATH=${override} does not point at an executable.`);
1550
- parts.push(directories.length ? `Searched PATH and ${directories.join(", ")}.` : "Searched PATH.");
1551
- parts.push("Install Herdr or set HERDR_BIN_PATH to its full path, then restart herdr-remote.");
1552
- return parts.join(" ");
1772
+ return result;
1553
1773
  }
1554
1774
  module.exports = {
1555
- COMMAND_NAME,
1556
- FALLBACK_DIRECTORIES,
1557
- fallbackDirectories,
1558
- findHerdrCommand,
1559
- herdrNotFoundMessage,
1560
- resolveHerdrCommand,
1561
- verifyHerdrCommand
1775
+ defaultSocketPath,
1776
+ resolveSocketPath,
1777
+ inspectSocket,
1778
+ assertSocket
1562
1779
  };
1563
1780
  }
1564
1781
  });
@@ -1593,7 +1810,7 @@ var require_service = __commonJS({
1593
1810
  } = require_terminal_palette();
1594
1811
  var { resolveSocketPath } = require_socket_discovery();
1595
1812
  var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
1596
- var { findHerdrCommand, resolveHerdrCommand } = require_herdr_command();
1813
+ var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion, resolveHerdrCommand } = require_herdr_command();
1597
1814
  var RUNTIME_VERSION = 2;
1598
1815
  function pidAlive(pid) {
1599
1816
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -1900,6 +2117,7 @@ var require_service = __commonJS({
1900
2117
  }
1901
2118
  const socketPath = resolveSocketPath(config.herdr.socketPath);
1902
2119
  const herdr = findHerdrCommand();
2120
+ const installed = herdr.found ? herdrVersion({ command: herdr.command }) : { version: null, supported: true };
1903
2121
  return {
1904
2122
  ok: true,
1905
2123
  mode: config.relay.mode,
@@ -1920,7 +2138,10 @@ var require_service = __commonJS({
1920
2138
  socketExists: Boolean(socketPath) && fs.existsSync(socketPath),
1921
2139
  herdrCommand: herdr.command,
1922
2140
  herdrCommandFound: herdr.found,
1923
- herdrCommandSource: herdr.source
2141
+ herdrCommandSource: herdr.source,
2142
+ herdrVersion: installed.version,
2143
+ herdrVersionSupported: installed.supported,
2144
+ herdrVersionMinimum: MIN_HERDR_VERSION2
1924
2145
  },
1925
2146
  publicUrl: resolvePublicUrl2(config, lanAddress),
1926
2147
  startedAt: state.startedAt || null
@@ -2400,15 +2621,14 @@ var require_herdr_plugin = __commonJS({
2400
2621
  var path = __require("node:path");
2401
2622
  var { spawnSync } = __require("node:child_process");
2402
2623
  var { PACKAGE_ROOT } = require_config();
2403
- var { resolveHerdrCommand } = require_herdr_command();
2624
+ var { herdrVersion, resolveHerdrCommand } = require_herdr_command();
2404
2625
  var PLUGIN_ID = "herdr.remote.web";
2405
2626
  var MANIFEST_NAME = "herdr-plugin.toml";
2406
2627
  function manifestPath() {
2407
2628
  return path.join(PACKAGE_ROOT, MANIFEST_NAME);
2408
2629
  }
2409
2630
  function herdrAvailable() {
2410
- const result = spawnSync(resolveHerdrCommand(), ["--version"], { stdio: "ignore" });
2411
- return result.status === 0 || result.status === 1;
2631
+ return herdrVersion().ok;
2412
2632
  }
2413
2633
  function runHerdr(args, { timeout = 15e3 } = {}) {
2414
2634
  const result = spawnSync(resolveHerdrCommand(), args, { encoding: "utf8", timeout });
@@ -2441,6 +2661,8 @@ var require_herdr_plugin = __commonJS({
2441
2661
  if (!fs.existsSync(manifestPath())) {
2442
2662
  return { available: false, registered: false, reason: "manifest missing" };
2443
2663
  }
2664
+ const { version, supported } = herdrVersion();
2665
+ const installed = { version, versionSupported: supported };
2444
2666
  let result;
2445
2667
  try {
2446
2668
  result = runHerdr(["plugin", "list"]);
@@ -2449,11 +2671,11 @@ var require_herdr_plugin = __commonJS({
2449
2671
  throw error;
2450
2672
  }
2451
2673
  if (result.status !== 0) {
2452
- return { available: true, registered: false, reason: String(result.stderr || "").trim() };
2674
+ return { available: true, registered: false, reason: String(result.stderr || "").trim(), ...installed };
2453
2675
  }
2454
2676
  const plugins = parsePluginList(result.stdout);
2455
2677
  const entry = plugins.find((plugin) => plugin.id === PLUGIN_ID);
2456
- if (!entry) return { available: true, registered: false, packageRoot: PACKAGE_ROOT };
2678
+ if (!entry) return { available: true, registered: false, packageRoot: PACKAGE_ROOT, ...installed };
2457
2679
  return {
2458
2680
  available: true,
2459
2681
  registered: true,
@@ -2462,7 +2684,8 @@ var require_herdr_plugin = __commonJS({
2462
2684
  // A stale link pointing at an old checkout is the main failure mode after
2463
2685
  // switching from a source install to npm.
2464
2686
  stale: Boolean(entry.localPath) && path.resolve(entry.localPath) !== path.resolve(PACKAGE_ROOT),
2465
- packageRoot: PACKAGE_ROOT
2687
+ packageRoot: PACKAGE_ROOT,
2688
+ ...installed
2466
2689
  };
2467
2690
  }
2468
2691
  function register() {
@@ -2495,186 +2718,6 @@ var require_herdr_plugin = __commonJS({
2495
2718
  }
2496
2719
  });
2497
2720
 
2498
- // src/updater.js
2499
- var require_updater = __commonJS({
2500
- "src/updater.js"(exports, module) {
2501
- "use strict";
2502
- var fs = __require("node:fs");
2503
- var os = __require("node:os");
2504
- var path = __require("node:path");
2505
- var { spawn } = __require("node:child_process");
2506
- var { PACKAGE_ROOT } = require_config();
2507
- var PACKAGE_NAME = "herdr-remote";
2508
- var DEFAULT_REGISTRY = "https://registry.npmjs.org";
2509
- var MIRROR_REGISTRY = "https://registry.npmmirror.com";
2510
- function currentVersion() {
2511
- try {
2512
- return __require(path.join(PACKAGE_ROOT, "package.json")).version;
2513
- } catch {
2514
- return "0.0.0";
2515
- }
2516
- }
2517
- function installKind2() {
2518
- const root = PACKAGE_ROOT;
2519
- if (!root.split(path.sep).includes("node_modules")) return "source";
2520
- try {
2521
- if (fs.lstatSync(root).isSymbolicLink()) return "linked";
2522
- } catch {
2523
- }
2524
- return "npm";
2525
- }
2526
- function canSelfUpdate2() {
2527
- return installKind2() === "npm";
2528
- }
2529
- function normalizeRegistry(value) {
2530
- if (typeof value !== "string") return null;
2531
- const trimmed = value.trim().replace(/\/+$/, "");
2532
- if (!/^https?:\/\//i.test(trimmed)) return null;
2533
- return trimmed;
2534
- }
2535
- function registryFromNpmrc(filePath) {
2536
- let contents;
2537
- try {
2538
- contents = fs.readFileSync(filePath, "utf8");
2539
- } catch {
2540
- return null;
2541
- }
2542
- let found = null;
2543
- for (const rawLine of contents.split(/\r?\n/)) {
2544
- const line = rawLine.trim();
2545
- if (!line || line.startsWith("#") || line.startsWith(";")) continue;
2546
- const match = /^registry\s*=\s*(.+)$/i.exec(line);
2547
- if (match) found = normalizeRegistry(match[1].replace(/^["']|["']$/g, ""));
2548
- }
2549
- return found;
2550
- }
2551
- function registryCandidates({ env = process.env, home = os.homedir(), cwd = process.cwd() } = {}) {
2552
- const candidates = [];
2553
- const add = (value) => {
2554
- const normalized = normalizeRegistry(value);
2555
- if (normalized && !candidates.includes(normalized)) candidates.push(normalized);
2556
- };
2557
- add(env.npm_config_registry);
2558
- add(env.NPM_CONFIG_REGISTRY);
2559
- add(env.HERDR_REMOTE_REGISTRY);
2560
- add(registryFromNpmrc(path.join(cwd, ".npmrc")));
2561
- add(registryFromNpmrc(path.join(home, ".npmrc")));
2562
- add(DEFAULT_REGISTRY);
2563
- add(MIRROR_REGISTRY);
2564
- return candidates;
2565
- }
2566
- function compareVersions(a, b) {
2567
- const parse = (value) => String(value).split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
2568
- const left = parse(a);
2569
- const right = parse(b);
2570
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
2571
- const diff = (left[index] || 0) - (right[index] || 0);
2572
- if (diff > 0) return 1;
2573
- if (diff < 0) return -1;
2574
- }
2575
- return 0;
2576
- }
2577
- async function askRegistry(registry, { timeoutMs, fetchImpl }) {
2578
- const controller = new AbortController();
2579
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2580
- try {
2581
- const response = await fetchImpl(`${registry}/${PACKAGE_NAME}/latest`, {
2582
- signal: controller.signal,
2583
- headers: { Accept: "application/vnd.npm.install-v1+json" }
2584
- });
2585
- if (!response.ok) return { ok: false, message: `HTTP ${response.status ?? "?"}` };
2586
- const body = await response.json();
2587
- const latest = typeof body?.version === "string" ? body.version : null;
2588
- if (!latest) return { ok: false, message: "registry answered without a version" };
2589
- return { ok: true, latest };
2590
- } catch (error) {
2591
- return { ok: false, message: error?.name === "AbortError" ? "timed out" : String(error?.message || error) };
2592
- } finally {
2593
- clearTimeout(timer);
2594
- }
2595
- }
2596
- async function checkForUpdate2({
2597
- timeoutMs = 6e3,
2598
- fetchImpl = globalThis.fetch,
2599
- registries = registryCandidates()
2600
- } = {}) {
2601
- const current = currentVersion();
2602
- const failures = [];
2603
- for (const registry of registries) {
2604
- const attempt = await askRegistry(registry, { timeoutMs, fetchImpl });
2605
- if (attempt.ok) {
2606
- return {
2607
- ok: true,
2608
- current,
2609
- latest: attempt.latest,
2610
- registry,
2611
- updateAvailable: compareVersions(attempt.latest, current) > 0
2612
- };
2613
- }
2614
- failures.push(`${registry}: ${attempt.message}`);
2615
- }
2616
- return {
2617
- ok: false,
2618
- current,
2619
- errorKey: "update.errorNetwork",
2620
- message: failures.join("; "),
2621
- triedRegistries: registries
2622
- };
2623
- }
2624
- function performUpdate2({
2625
- spawnImpl = spawn,
2626
- timeoutMs = 18e4,
2627
- // Seam for tests: the suite runs from a checkout, where the guard below is
2628
- // correctly the only reachable outcome.
2629
- installKindImpl = installKind2,
2630
- // The registry that just answered the version check. Installing from
2631
- // somewhere the machine has been shown to reach beats installing from a
2632
- // default it may have no route to. Empty means "whatever npm is configured
2633
- // with", which is the right answer when no check has run.
2634
- registry = ""
2635
- } = {}) {
2636
- return new Promise((resolve) => {
2637
- const kind = installKindImpl();
2638
- if (kind !== "npm") {
2639
- resolve({ ok: false, errorKey: `update.cannot.${kind}` });
2640
- return;
2641
- }
2642
- const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
2643
- const normalizedRegistry = normalizeRegistry(registry);
2644
- if (normalizedRegistry) args.push("--registry", normalizedRegistry);
2645
- const child = spawnImpl("npm", args, {
2646
- encoding: "utf8",
2647
- timeout: timeoutMs
2648
- });
2649
- let output = "";
2650
- const collect = (chunk) => {
2651
- output += String(chunk);
2652
- };
2653
- child.stdout?.on("data", collect);
2654
- child.stderr?.on("data", collect);
2655
- child.on("error", (error) => {
2656
- resolve({ ok: false, errorKey: "update.errorFailed", output: error.message });
2657
- });
2658
- child.on("close", (code) => {
2659
- resolve({ ok: code === 0, output: output.trim().slice(-2e3) });
2660
- });
2661
- });
2662
- }
2663
- module.exports = {
2664
- PACKAGE_NAME,
2665
- DEFAULT_REGISTRY,
2666
- MIRROR_REGISTRY,
2667
- canSelfUpdate: canSelfUpdate2,
2668
- checkForUpdate: checkForUpdate2,
2669
- compareVersions,
2670
- currentVersion,
2671
- installKind: installKind2,
2672
- performUpdate: performUpdate2,
2673
- registryCandidates
2674
- };
2675
- }
2676
- });
2677
-
2678
2721
  // tui/src/index.tsx
2679
2722
  import { render } from "ink";
2680
2723
 
@@ -2976,6 +3019,7 @@ var STATUS_GLYPH = {
2976
3019
 
2977
3020
  // tui/src/api.ts
2978
3021
  var import_config = __toESM(require_config());
3022
+ var import_herdr_command = __toESM(require_herdr_command());
2979
3023
  var import_i18n = __toESM(require_i18n());
2980
3024
  var import_net_interfaces = __toESM(require_net_interfaces());
2981
3025
  var import_settings_model = __toESM(require_settings_model());
@@ -3879,6 +3923,11 @@ function HerdrScreen({ ctx }) {
3879
3923
  ] })
3880
3924
  ] }) }),
3881
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}` })
3929
+ ] }) }) : 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,
3882
3931
  registration && !registration.available ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.cliMissing") }) : null,
3883
3932
  registration?.stale && registration.linkedPath ? (
3884
3933
  // A leftover link to an old source checkout is the usual state after
@@ -4015,7 +4064,7 @@ function About({ ctx }) {
4015
4064
  children: updateLabel
4016
4065
  }
4017
4066
  ) }),
4018
- /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.12" }) }),
4067
+ /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.14" }) }),
4019
4068
  /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
4020
4069
  /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
4021
4070
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),