herdr-remote 0.2.0 → 0.2.2
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/LICENSE +0 -0
- package/README.md +0 -0
- package/config.example.json +0 -0
- package/dist/tui.mjs +267 -22
- package/herdr-plugin.toml +1 -1
- package/package.json +2 -2
- package/src/config.js +8 -0
- package/src/exit-codes.js +0 -0
- package/src/herdr-command.js +0 -0
- package/src/herdr-plugin.js +0 -0
- package/src/host-connector.js +0 -0
- package/src/i18n/en.js +14 -0
- package/src/i18n/index.js +0 -0
- package/src/i18n/zh.js +14 -0
- package/src/keepalive.js +0 -0
- package/src/lifecycle.js +0 -0
- package/src/net-interfaces.js +0 -0
- package/src/pty-session.js +0 -0
- package/src/service.js +0 -0
- package/src/settings-model.js +0 -0
- package/src/socket-discovery.js +0 -0
- package/src/state.js +0 -0
- package/src/supervisor.js +0 -0
- package/src/terminal-palette.js +0 -0
- package/src/updater.js +148 -0
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/config.example.json
CHANGED
|
File without changes
|
package/dist/tui.mjs
CHANGED
|
@@ -59,6 +59,7 @@ var require_config = __commonJS({
|
|
|
59
59
|
}
|
|
60
60
|
var PACKAGE_ROOT = findPackageRoot(__dirname);
|
|
61
61
|
var ACCESS_MODES2 = ["local", "lan", "remote"];
|
|
62
|
+
var OFFICIAL_RELAY_URL2 = "wss://herdr-remote.564616.xyz";
|
|
62
63
|
var LANGUAGES = ["auto", "zh", "en"];
|
|
63
64
|
var KEEPALIVE_MANAGERS = ["auto", "systemd", "launchd", "supervisor", "none"];
|
|
64
65
|
var DEFAULTS = {
|
|
@@ -325,6 +326,7 @@ var require_config = __commonJS({
|
|
|
325
326
|
PACKAGE_ROOT,
|
|
326
327
|
DEFAULTS,
|
|
327
328
|
ACCESS_MODES: ACCESS_MODES2,
|
|
329
|
+
OFFICIAL_RELAY_URL: OFFICIAL_RELAY_URL2,
|
|
328
330
|
LANGUAGES,
|
|
329
331
|
KEEPALIVE_MANAGERS,
|
|
330
332
|
configDir,
|
|
@@ -391,9 +393,11 @@ var require_en = __commonJS({
|
|
|
391
393
|
"nav.about": "Language & about",
|
|
392
394
|
"mode.local": "This machine only",
|
|
393
395
|
"mode.lan": "Local network / Tailscale",
|
|
396
|
+
"mode.official": "Official relay (no server needed)",
|
|
394
397
|
"mode.remote": "Self-hosted relay",
|
|
395
398
|
"mode.local.description": "The relay listens on 127.0.0.1. Only a browser on this machine can reach it.",
|
|
396
399
|
"mode.lan.description": "The relay listens on 0.0.0.0 (every interface), so phones on your LAN or tailnet can reach it.",
|
|
400
|
+
"mode.official.description": "Reach this machine from anywhere through the relay run by the Herdr Remote project. Nothing to deploy. Your terminal traffic passes through a server you do not control, and only devices you pair can open it.",
|
|
397
401
|
"mode.remote.description": "No local relay. The workstation dials a relay you run, which is the only way in from outside your network.",
|
|
398
402
|
"overview.title": "Status",
|
|
399
403
|
"overview.mode": "Access mode",
|
|
@@ -553,7 +557,19 @@ var require_en = __commonJS({
|
|
|
553
557
|
"hint.restartRequired": "Restart the services to apply these changes.",
|
|
554
558
|
"hint.unsavedChanges": "Unsaved changes \u2014 press s to save.",
|
|
555
559
|
"hint.save": "s save",
|
|
556
|
-
"hint.editing": "\u21B5 confirm \xB7 esc cancel"
|
|
560
|
+
"hint.editing": "\u21B5 confirm \xB7 esc cancel",
|
|
561
|
+
"update.title": "Updates",
|
|
562
|
+
"update.check": "Check for updates",
|
|
563
|
+
"update.checking": "Checking npm\u2026",
|
|
564
|
+
"update.upToDate": "Up to date ({version})",
|
|
565
|
+
"update.available": "Update available: {version} \u2014 press Enter to install",
|
|
566
|
+
"update.updating": "Installing {version}\u2026",
|
|
567
|
+
"update.done": "Updated to {version}",
|
|
568
|
+
"update.restartHint": "Update installed. Restart herdr-remote to use it.",
|
|
569
|
+
"update.errorNetwork": "Could not reach the npm registry",
|
|
570
|
+
"update.errorFailed": "The update failed. Try: npm install -g herdr-remote@latest",
|
|
571
|
+
"update.cannot.source": "Running from a source checkout \u2014 update with git, not npm",
|
|
572
|
+
"update.cannot.linked": "Running from a linked working copy \u2014 npm link manages this one"
|
|
557
573
|
};
|
|
558
574
|
}
|
|
559
575
|
});
|
|
@@ -597,9 +613,11 @@ var require_zh = __commonJS({
|
|
|
597
613
|
"nav.about": "\u8BED\u8A00\u4E0E\u5173\u4E8E",
|
|
598
614
|
"mode.local": "\u4EC5\u672C\u673A",
|
|
599
615
|
"mode.lan": "\u5C40\u57DF\u7F51 / Tailscale",
|
|
616
|
+
"mode.official": "\u5B98\u65B9 relay\uFF08\u65E0\u9700\u81EA\u5EFA\u670D\u52A1\u5668\uFF09",
|
|
600
617
|
"mode.remote": "\u81EA\u5EFA relay",
|
|
601
618
|
"mode.local.description": "relay \u76D1\u542C 127.0.0.1\uFF0C\u53EA\u6709\u672C\u673A\u6D4F\u89C8\u5668\u80FD\u8BBF\u95EE\u3002",
|
|
602
619
|
"mode.lan.description": "relay \u76D1\u542C 0.0.0.0\uFF08\u6240\u6709\u7F51\u5361\uFF09\uFF0C\u5C40\u57DF\u7F51\u6216 tailnet \u91CC\u7684\u624B\u673A\u90FD\u80FD\u8BBF\u95EE\u3002",
|
|
620
|
+
"mode.official.description": "\u901A\u8FC7 Herdr Remote \u5B98\u65B9 relay \u4ECE\u4EFB\u610F\u7F51\u7EDC\u8BBF\u95EE\u672C\u673A\uFF0C\u65E0\u9700\u90E8\u7F72\u4EFB\u4F55\u670D\u52A1\u5668\u3002\u7EC8\u7AEF\u6D41\u91CF\u4F1A\u7ECF\u8FC7\u4E00\u53F0\u4F60\u65E0\u6CD5\u63A7\u5236\u7684\u670D\u52A1\u5668\uFF0C\u4F46\u53EA\u6709\u4F60\u914D\u5BF9\u8FC7\u7684\u8BBE\u5907\u624D\u80FD\u6253\u5F00\u7EC8\u7AEF\u3002",
|
|
603
621
|
"mode.remote.description": "\u4E0D\u542F\u52A8\u672C\u5730 relay\u3002\u672C\u673A\u8FDE\u63A5\u4F60\u81EA\u5EFA\u7684 relay\uFF0C\u8FD9\u662F\u4ECE\u5916\u7F51\u8BBF\u95EE\u7684\u552F\u4E00\u65B9\u5F0F\u3002",
|
|
604
622
|
"overview.title": "\u72B6\u6001",
|
|
605
623
|
"overview.mode": "\u8BBF\u95EE\u65B9\u5F0F",
|
|
@@ -759,7 +777,19 @@ var require_zh = __commonJS({
|
|
|
759
777
|
"hint.restartRequired": "\u9700\u8981\u91CD\u542F\u670D\u52A1\u624D\u80FD\u751F\u6548\u3002",
|
|
760
778
|
"hint.unsavedChanges": "\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539 \u2014 \u6309 s \u4FDD\u5B58\u3002",
|
|
761
779
|
"hint.save": "s \u4FDD\u5B58",
|
|
762
|
-
"hint.editing": "\u21B5 \u786E\u8BA4 \xB7 esc \u53D6\u6D88"
|
|
780
|
+
"hint.editing": "\u21B5 \u786E\u8BA4 \xB7 esc \u53D6\u6D88",
|
|
781
|
+
"update.title": "\u66F4\u65B0",
|
|
782
|
+
"update.check": "\u68C0\u67E5\u66F4\u65B0",
|
|
783
|
+
"update.checking": "\u6B63\u5728\u67E5\u8BE2 npm\u2026",
|
|
784
|
+
"update.upToDate": "\u5DF2\u662F\u6700\u65B0\uFF08{version}\uFF09",
|
|
785
|
+
"update.available": "\u6709\u65B0\u7248\u672C {version} \u2014\u2014 \u6309 Enter \u5B89\u88C5",
|
|
786
|
+
"update.updating": "\u6B63\u5728\u5B89\u88C5 {version}\u2026",
|
|
787
|
+
"update.done": "\u5DF2\u66F4\u65B0\u5230 {version}",
|
|
788
|
+
"update.restartHint": "\u66F4\u65B0\u5DF2\u5B89\u88C5\uFF0C\u91CD\u542F herdr-remote \u540E\u751F\u6548\u3002",
|
|
789
|
+
"update.errorNetwork": "\u65E0\u6CD5\u8FDE\u63A5 npm registry",
|
|
790
|
+
"update.errorFailed": "\u66F4\u65B0\u5931\u8D25\uFF0C\u53EF\u624B\u52A8\u6267\u884C\uFF1Anpm install -g herdr-remote@latest",
|
|
791
|
+
"update.cannot.source": "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C \u2014\u2014 \u8BF7\u7528 git \u66F4\u65B0\uFF0C\u800C\u4E0D\u662F npm",
|
|
792
|
+
"update.cannot.linked": "\u5F53\u524D\u662F npm link \u7684\u5DE5\u4F5C\u526F\u672C \u2014\u2014 \u7531 npm link \u7BA1\u7406"
|
|
763
793
|
};
|
|
764
794
|
}
|
|
765
795
|
});
|
|
@@ -2266,6 +2296,116 @@ var require_herdr_plugin = __commonJS({
|
|
|
2266
2296
|
}
|
|
2267
2297
|
});
|
|
2268
2298
|
|
|
2299
|
+
// src/updater.js
|
|
2300
|
+
var require_updater = __commonJS({
|
|
2301
|
+
"src/updater.js"(exports, module) {
|
|
2302
|
+
"use strict";
|
|
2303
|
+
var fs = __require("node:fs");
|
|
2304
|
+
var path = __require("node:path");
|
|
2305
|
+
var { spawn } = __require("node:child_process");
|
|
2306
|
+
var { PACKAGE_ROOT } = require_config();
|
|
2307
|
+
var PACKAGE_NAME = "herdr-remote";
|
|
2308
|
+
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
2309
|
+
function currentVersion() {
|
|
2310
|
+
try {
|
|
2311
|
+
return __require(path.join(PACKAGE_ROOT, "package.json")).version;
|
|
2312
|
+
} catch {
|
|
2313
|
+
return "0.0.0";
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function installKind2() {
|
|
2317
|
+
const root = PACKAGE_ROOT;
|
|
2318
|
+
if (!root.split(path.sep).includes("node_modules")) return "source";
|
|
2319
|
+
try {
|
|
2320
|
+
if (fs.lstatSync(root).isSymbolicLink()) return "linked";
|
|
2321
|
+
} catch {
|
|
2322
|
+
}
|
|
2323
|
+
return "npm";
|
|
2324
|
+
}
|
|
2325
|
+
function canSelfUpdate2() {
|
|
2326
|
+
return installKind2() === "npm";
|
|
2327
|
+
}
|
|
2328
|
+
function compareVersions(a, b) {
|
|
2329
|
+
const parse = (value) => String(value).split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
2330
|
+
const left = parse(a);
|
|
2331
|
+
const right = parse(b);
|
|
2332
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
2333
|
+
const diff = (left[index] || 0) - (right[index] || 0);
|
|
2334
|
+
if (diff > 0) return 1;
|
|
2335
|
+
if (diff < 0) return -1;
|
|
2336
|
+
}
|
|
2337
|
+
return 0;
|
|
2338
|
+
}
|
|
2339
|
+
async function checkForUpdate2({ timeoutMs = 8e3, fetchImpl = globalThis.fetch } = {}) {
|
|
2340
|
+
const current = currentVersion();
|
|
2341
|
+
const controller = new AbortController();
|
|
2342
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2343
|
+
try {
|
|
2344
|
+
const response = await fetchImpl(REGISTRY_URL, {
|
|
2345
|
+
signal: controller.signal,
|
|
2346
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
2347
|
+
});
|
|
2348
|
+
if (!response.ok) {
|
|
2349
|
+
return { ok: false, current, errorKey: "update.errorNetwork" };
|
|
2350
|
+
}
|
|
2351
|
+
const body = await response.json();
|
|
2352
|
+
const latest = typeof body?.version === "string" ? body.version : null;
|
|
2353
|
+
if (!latest) return { ok: false, current, errorKey: "update.errorNetwork" };
|
|
2354
|
+
return {
|
|
2355
|
+
ok: true,
|
|
2356
|
+
current,
|
|
2357
|
+
latest,
|
|
2358
|
+
updateAvailable: compareVersions(latest, current) > 0
|
|
2359
|
+
};
|
|
2360
|
+
} catch {
|
|
2361
|
+
return { ok: false, current, errorKey: "update.errorNetwork" };
|
|
2362
|
+
} finally {
|
|
2363
|
+
clearTimeout(timer);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function performUpdate2({
|
|
2367
|
+
spawnImpl = spawn,
|
|
2368
|
+
timeoutMs = 18e4,
|
|
2369
|
+
// Seam for tests: the suite runs from a checkout, where the guard below is
|
|
2370
|
+
// correctly the only reachable outcome.
|
|
2371
|
+
installKindImpl = installKind2
|
|
2372
|
+
} = {}) {
|
|
2373
|
+
return new Promise((resolve) => {
|
|
2374
|
+
const kind = installKindImpl();
|
|
2375
|
+
if (kind !== "npm") {
|
|
2376
|
+
resolve({ ok: false, errorKey: `update.cannot.${kind}` });
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const child = spawnImpl("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
2380
|
+
encoding: "utf8",
|
|
2381
|
+
timeout: timeoutMs
|
|
2382
|
+
});
|
|
2383
|
+
let output = "";
|
|
2384
|
+
const collect = (chunk) => {
|
|
2385
|
+
output += String(chunk);
|
|
2386
|
+
};
|
|
2387
|
+
child.stdout?.on("data", collect);
|
|
2388
|
+
child.stderr?.on("data", collect);
|
|
2389
|
+
child.on("error", (error) => {
|
|
2390
|
+
resolve({ ok: false, errorKey: "update.errorFailed", output: error.message });
|
|
2391
|
+
});
|
|
2392
|
+
child.on("close", (code) => {
|
|
2393
|
+
resolve({ ok: code === 0, output: output.trim().slice(-2e3) });
|
|
2394
|
+
});
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
module.exports = {
|
|
2398
|
+
PACKAGE_NAME,
|
|
2399
|
+
canSelfUpdate: canSelfUpdate2,
|
|
2400
|
+
checkForUpdate: checkForUpdate2,
|
|
2401
|
+
compareVersions,
|
|
2402
|
+
currentVersion,
|
|
2403
|
+
installKind: installKind2,
|
|
2404
|
+
performUpdate: performUpdate2
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2269
2409
|
// tui/src/index.tsx
|
|
2270
2410
|
import { render } from "ink";
|
|
2271
2411
|
|
|
@@ -2574,6 +2714,7 @@ var import_service = __toESM(require_service());
|
|
|
2574
2714
|
var import_lifecycle = __toESM(require_lifecycle());
|
|
2575
2715
|
var keepalive = __toESM(require_keepalive());
|
|
2576
2716
|
var herdrPlugin = __toESM(require_herdr_plugin());
|
|
2717
|
+
var import_updater = __toESM(require_updater());
|
|
2577
2718
|
function detectLocale(options = {}) {
|
|
2578
2719
|
return (0, import_i18n.detectLocale)(options);
|
|
2579
2720
|
}
|
|
@@ -3059,14 +3200,23 @@ function RelayScreen({ ctx }) {
|
|
|
3059
3200
|
return /* @__PURE__ */ jsx7(Panel, { title: t("field.mode"), children: /* @__PURE__ */ jsx7(
|
|
3060
3201
|
ChoiceList,
|
|
3061
3202
|
{
|
|
3062
|
-
options: [
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3203
|
+
options: [
|
|
3204
|
+
...["local", "lan"].map((mode) => ({
|
|
3205
|
+
id: mode,
|
|
3206
|
+
label: t(`mode.${mode}`),
|
|
3207
|
+
description: t(`mode.${mode}.description`)
|
|
3208
|
+
})),
|
|
3209
|
+
{ id: "official", label: t("mode.official"), description: t("mode.official.description") },
|
|
3210
|
+
{ id: "remote", label: t("mode.remote"), description: t("mode.remote.description") }
|
|
3211
|
+
],
|
|
3212
|
+
current: draft.relay.mode === "remote" && draft.relay.remoteUrl === import_config.OFFICIAL_RELAY_URL ? "official" : draft.relay.mode,
|
|
3213
|
+
onPick: (choice) => {
|
|
3214
|
+
if (choice === "official") {
|
|
3215
|
+
if (applyField("mode", "remote")) applyField("remoteUrl", import_config.OFFICIAL_RELAY_URL);
|
|
3216
|
+
ctx.setEditing(null);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
applyField("mode", choice);
|
|
3070
3220
|
ctx.setEditing(null);
|
|
3071
3221
|
},
|
|
3072
3222
|
onCancel: () => ctx.setEditing(null)
|
|
@@ -3447,12 +3597,65 @@ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
|
3447
3597
|
function About({ ctx }) {
|
|
3448
3598
|
const { t, draft } = ctx;
|
|
3449
3599
|
const [selected, setSelected] = useState8("auto");
|
|
3600
|
+
const [update, setUpdate] = useState8({ phase: "idle" });
|
|
3450
3601
|
const detected = detectLocale({ preference: "auto" });
|
|
3451
|
-
const
|
|
3602
|
+
const languageOptions = [
|
|
3452
3603
|
{ id: "auto", label: t("about.languageAuto", { detected }) },
|
|
3453
3604
|
{ id: "zh", label: t("about.languageZh") },
|
|
3454
3605
|
{ id: "en", label: t("about.languageEn") }
|
|
3455
3606
|
];
|
|
3607
|
+
const updatable = (0, import_updater.canSelfUpdate)();
|
|
3608
|
+
const updateLabel = (() => {
|
|
3609
|
+
switch (update.phase) {
|
|
3610
|
+
case "checking":
|
|
3611
|
+
return t("update.checking");
|
|
3612
|
+
case "current":
|
|
3613
|
+
return t("update.upToDate", { version: update.latest });
|
|
3614
|
+
case "available":
|
|
3615
|
+
return t("update.available", { version: update.latest });
|
|
3616
|
+
case "updating":
|
|
3617
|
+
return t("update.updating", { version: update.latest });
|
|
3618
|
+
case "done":
|
|
3619
|
+
return t("update.done", { version: update.latest });
|
|
3620
|
+
case "error":
|
|
3621
|
+
return t(update.messageKey);
|
|
3622
|
+
default:
|
|
3623
|
+
return t("update.check");
|
|
3624
|
+
}
|
|
3625
|
+
})();
|
|
3626
|
+
const options = [...languageOptions, { id: "update", label: updateLabel }];
|
|
3627
|
+
const runCheck = async () => {
|
|
3628
|
+
setUpdate({ phase: "checking" });
|
|
3629
|
+
const result = await (0, import_updater.checkForUpdate)();
|
|
3630
|
+
if (!result.ok) {
|
|
3631
|
+
setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorNetwork" });
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
setUpdate(result.updateAvailable ? { phase: "available", latest: result.latest } : { phase: "current", latest: result.latest });
|
|
3635
|
+
};
|
|
3636
|
+
const runUpdate = async (latest) => {
|
|
3637
|
+
setUpdate({ phase: "updating", latest });
|
|
3638
|
+
const result = await (0, import_updater.performUpdate)();
|
|
3639
|
+
if (!result.ok) {
|
|
3640
|
+
setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorFailed" });
|
|
3641
|
+
ctx.notify(t("update.errorFailed"), "error");
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
setUpdate({ phase: "done", latest });
|
|
3645
|
+
ctx.notify(t("update.restartHint"), "success");
|
|
3646
|
+
};
|
|
3647
|
+
const activateUpdate = () => {
|
|
3648
|
+
if (update.phase === "checking" || update.phase === "updating") return;
|
|
3649
|
+
if (update.phase === "available") {
|
|
3650
|
+
if (!updatable) {
|
|
3651
|
+
setUpdate({ phase: "error", messageKey: `update.cannot.${(0, import_updater.installKind)()}` });
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
void runUpdate(update.latest);
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
void runCheck();
|
|
3658
|
+
};
|
|
3456
3659
|
const choose = (language) => {
|
|
3457
3660
|
const result = (0, import_settings_model.setField)(draft, "language", language);
|
|
3458
3661
|
if (result.errorKey) {
|
|
@@ -3468,15 +3671,22 @@ function About({ ctx }) {
|
|
|
3468
3671
|
ctx.notify(t("error.saveFailed", { message: error.message }), "error");
|
|
3469
3672
|
}
|
|
3470
3673
|
};
|
|
3674
|
+
const activate = (id) => {
|
|
3675
|
+
if (id === "update") {
|
|
3676
|
+
activateUpdate();
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
choose(id);
|
|
3680
|
+
};
|
|
3471
3681
|
useInput7((_input, key) => {
|
|
3472
3682
|
const index = options.findIndex((option) => option.id === selected);
|
|
3473
3683
|
if (key.upArrow) setSelected(options[(index - 1 + options.length) % options.length].id);
|
|
3474
3684
|
else if (key.downArrow) setSelected(options[(index + 1) % options.length].id);
|
|
3475
|
-
else if (key.return)
|
|
3685
|
+
else if (key.return) activate(selected);
|
|
3476
3686
|
}, { isActive: ctx.editingId === null });
|
|
3477
3687
|
return /* @__PURE__ */ jsxs9(Panel, { title: t("about.title"), children: [
|
|
3478
3688
|
/* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.language") }),
|
|
3479
|
-
/* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children:
|
|
3689
|
+
/* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: languageOptions.map((option) => /* @__PURE__ */ jsx10(
|
|
3480
3690
|
Selectable,
|
|
3481
3691
|
{
|
|
3482
3692
|
selected: selected === option.id,
|
|
@@ -3489,7 +3699,20 @@ function About({ ctx }) {
|
|
|
3489
3699
|
},
|
|
3490
3700
|
option.id
|
|
3491
3701
|
)) }),
|
|
3492
|
-
/* @__PURE__ */ jsx10(
|
|
3702
|
+
/* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("update.title") }),
|
|
3703
|
+
/* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx10(
|
|
3704
|
+
Selectable,
|
|
3705
|
+
{
|
|
3706
|
+
selected: selected === "update",
|
|
3707
|
+
onSelect: () => {
|
|
3708
|
+
setSelected("update");
|
|
3709
|
+
activateUpdate();
|
|
3710
|
+
},
|
|
3711
|
+
onHover: () => setSelected("update"),
|
|
3712
|
+
children: updateLabel
|
|
3713
|
+
}
|
|
3714
|
+
) }),
|
|
3715
|
+
/* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.2" }) }),
|
|
3493
3716
|
/* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
|
|
3494
3717
|
/* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
|
|
3495
3718
|
/* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
|
|
@@ -3592,14 +3815,36 @@ function Wizard({ ctx, onDone }) {
|
|
|
3592
3815
|
/* @__PURE__ */ jsx11(
|
|
3593
3816
|
ChoiceList,
|
|
3594
3817
|
{
|
|
3595
|
-
options: [
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3818
|
+
options: [
|
|
3819
|
+
...["local", "lan"].map((mode) => ({
|
|
3820
|
+
id: mode,
|
|
3821
|
+
label: t(`mode.${mode}`),
|
|
3822
|
+
description: t(`mode.${mode}.description`)
|
|
3823
|
+
})),
|
|
3824
|
+
// Offered ahead of the self-hosted option because it is the one
|
|
3825
|
+
// that needs no server. It is still a separate, explicit choice:
|
|
3826
|
+
// it routes the terminal through a relay this user does not own.
|
|
3827
|
+
{
|
|
3828
|
+
id: "official",
|
|
3829
|
+
label: t("mode.official"),
|
|
3830
|
+
description: t("mode.official.description")
|
|
3831
|
+
},
|
|
3832
|
+
{
|
|
3833
|
+
id: "remote",
|
|
3834
|
+
label: t("mode.remote"),
|
|
3835
|
+
description: t("mode.remote.description")
|
|
3836
|
+
}
|
|
3837
|
+
],
|
|
3838
|
+
current: draft.relay.mode === "remote" && draft.relay.remoteUrl === import_config.OFFICIAL_RELAY_URL ? "official" : draft.relay.mode,
|
|
3839
|
+
onPick: (choice) => {
|
|
3840
|
+
if (choice === "official") {
|
|
3841
|
+
if (!apply("mode", "remote")) return;
|
|
3842
|
+
if (!apply("remoteUrl", import_config.OFFICIAL_RELAY_URL)) return;
|
|
3843
|
+
(0, import_service.setRelayPassword)("");
|
|
3844
|
+
advance("access", ["language", "access", "finish"]);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
if (apply("mode", choice)) advance("access", stepsFor(choice));
|
|
3603
3848
|
},
|
|
3604
3849
|
onCancel: back
|
|
3605
3850
|
}
|
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "herdr-remote",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Remote browser access to your Herdr terminal workspaces: Herdr plugin, host connector, and bilingual configuration TUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"start": "node bin/herdr-remote.js"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"herdr-remote-relay": "^0.2.
|
|
45
|
+
"herdr-remote-relay": "^0.2.1",
|
|
46
46
|
"ink": "^7.1.1",
|
|
47
47
|
"node-pty": "^1.1.0",
|
|
48
48
|
"qrcode": "^1.5.4",
|
package/src/config.js
CHANGED
|
@@ -38,6 +38,13 @@ const PACKAGE_ROOT = findPackageRoot(__dirname);
|
|
|
38
38
|
// remote — no local relay; the host connector dials an operator-run relay,
|
|
39
39
|
// which is the only way in from outside the local network.
|
|
40
40
|
const ACCESS_MODES = ['local', 'lan', 'remote'];
|
|
41
|
+
|
|
42
|
+
// The relay this project runs for people who do not want to host one. Offered
|
|
43
|
+
// as a one-keystroke choice during setup; it is never selected automatically,
|
|
44
|
+
// because routing a terminal through someone else's server has to be a
|
|
45
|
+
// deliberate decision. It is a plain public relay: no join password, and each
|
|
46
|
+
// workstation stays reachable only through its own host token.
|
|
47
|
+
const OFFICIAL_RELAY_URL = 'wss://herdr-remote.564616.xyz';
|
|
41
48
|
const LANGUAGES = ['auto', 'zh', 'en'];
|
|
42
49
|
const KEEPALIVE_MANAGERS = ['auto', 'systemd', 'launchd', 'supervisor', 'none'];
|
|
43
50
|
|
|
@@ -385,6 +392,7 @@ module.exports = {
|
|
|
385
392
|
PACKAGE_ROOT,
|
|
386
393
|
DEFAULTS,
|
|
387
394
|
ACCESS_MODES,
|
|
395
|
+
OFFICIAL_RELAY_URL,
|
|
388
396
|
LANGUAGES,
|
|
389
397
|
KEEPALIVE_MANAGERS,
|
|
390
398
|
configDir,
|
package/src/exit-codes.js
CHANGED
|
File without changes
|
package/src/herdr-command.js
CHANGED
|
File without changes
|
package/src/herdr-plugin.js
CHANGED
|
File without changes
|
package/src/host-connector.js
CHANGED
|
File without changes
|
package/src/i18n/en.js
CHANGED
|
@@ -41,9 +41,11 @@ module.exports = {
|
|
|
41
41
|
|
|
42
42
|
'mode.local': 'This machine only',
|
|
43
43
|
'mode.lan': 'Local network / Tailscale',
|
|
44
|
+
'mode.official': 'Official relay (no server needed)',
|
|
44
45
|
'mode.remote': 'Self-hosted relay',
|
|
45
46
|
'mode.local.description': 'The relay listens on 127.0.0.1. Only a browser on this machine can reach it.',
|
|
46
47
|
'mode.lan.description': 'The relay listens on 0.0.0.0 (every interface), so phones on your LAN or tailnet can reach it.',
|
|
48
|
+
'mode.official.description': 'Reach this machine from anywhere through the relay run by the Herdr Remote project. Nothing to deploy. Your terminal traffic passes through a server you do not control, and only devices you pair can open it.',
|
|
47
49
|
'mode.remote.description': 'No local relay. The workstation dials a relay you run, which is the only way in from outside your network.',
|
|
48
50
|
|
|
49
51
|
'overview.title': 'Status',
|
|
@@ -216,4 +218,16 @@ module.exports = {
|
|
|
216
218
|
'hint.unsavedChanges': 'Unsaved changes — press s to save.',
|
|
217
219
|
'hint.save': 's save',
|
|
218
220
|
'hint.editing': '↵ confirm · esc cancel',
|
|
221
|
+
'update.title': 'Updates',
|
|
222
|
+
'update.check': 'Check for updates',
|
|
223
|
+
'update.checking': 'Checking npm…',
|
|
224
|
+
'update.upToDate': 'Up to date ({version})',
|
|
225
|
+
'update.available': 'Update available: {version} — press Enter to install',
|
|
226
|
+
'update.updating': 'Installing {version}…',
|
|
227
|
+
'update.done': 'Updated to {version}',
|
|
228
|
+
'update.restartHint': 'Update installed. Restart herdr-remote to use it.',
|
|
229
|
+
'update.errorNetwork': 'Could not reach the npm registry',
|
|
230
|
+
'update.errorFailed': 'The update failed. Try: npm install -g herdr-remote@latest',
|
|
231
|
+
'update.cannot.source': 'Running from a source checkout — update with git, not npm',
|
|
232
|
+
'update.cannot.linked': 'Running from a linked working copy — npm link manages this one',
|
|
219
233
|
};
|
package/src/i18n/index.js
CHANGED
|
File without changes
|
package/src/i18n/zh.js
CHANGED
|
@@ -40,9 +40,11 @@ module.exports = {
|
|
|
40
40
|
|
|
41
41
|
'mode.local': '仅本机',
|
|
42
42
|
'mode.lan': '局域网 / Tailscale',
|
|
43
|
+
'mode.official': '官方 relay(无需自建服务器)',
|
|
43
44
|
'mode.remote': '自建 relay',
|
|
44
45
|
'mode.local.description': 'relay 监听 127.0.0.1,只有本机浏览器能访问。',
|
|
45
46
|
'mode.lan.description': 'relay 监听 0.0.0.0(所有网卡),局域网或 tailnet 里的手机都能访问。',
|
|
47
|
+
'mode.official.description': '通过 Herdr Remote 官方 relay 从任意网络访问本机,无需部署任何服务器。终端流量会经过一台你无法控制的服务器,但只有你配对过的设备才能打开终端。',
|
|
46
48
|
'mode.remote.description': '不启动本地 relay。本机连接你自建的 relay,这是从外网访问的唯一方式。',
|
|
47
49
|
|
|
48
50
|
'overview.title': '状态',
|
|
@@ -215,4 +217,16 @@ module.exports = {
|
|
|
215
217
|
'hint.unsavedChanges': '有未保存的修改 — 按 s 保存。',
|
|
216
218
|
'hint.save': 's 保存',
|
|
217
219
|
'hint.editing': '↵ 确认 · esc 取消',
|
|
220
|
+
'update.title': '更新',
|
|
221
|
+
'update.check': '检查更新',
|
|
222
|
+
'update.checking': '正在查询 npm…',
|
|
223
|
+
'update.upToDate': '已是最新({version})',
|
|
224
|
+
'update.available': '有新版本 {version} —— 按 Enter 安装',
|
|
225
|
+
'update.updating': '正在安装 {version}…',
|
|
226
|
+
'update.done': '已更新到 {version}',
|
|
227
|
+
'update.restartHint': '更新已安装,重启 herdr-remote 后生效。',
|
|
228
|
+
'update.errorNetwork': '无法连接 npm registry',
|
|
229
|
+
'update.errorFailed': '更新失败,可手动执行:npm install -g herdr-remote@latest',
|
|
230
|
+
'update.cannot.source': '当前从源码目录运行 —— 请用 git 更新,而不是 npm',
|
|
231
|
+
'update.cannot.linked': '当前是 npm link 的工作副本 —— 由 npm link 管理',
|
|
218
232
|
};
|
package/src/keepalive.js
CHANGED
|
File without changes
|
package/src/lifecycle.js
CHANGED
|
File without changes
|
package/src/net-interfaces.js
CHANGED
|
File without changes
|
package/src/pty-session.js
CHANGED
|
File without changes
|
package/src/service.js
CHANGED
|
File without changes
|
package/src/settings-model.js
CHANGED
|
File without changes
|
package/src/socket-discovery.js
CHANGED
|
File without changes
|
package/src/state.js
CHANGED
|
File without changes
|
package/src/supervisor.js
CHANGED
|
File without changes
|
package/src/terminal-palette.js
CHANGED
|
File without changes
|
package/src/updater.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Self-update for the npm-installed CLI.
|
|
4
|
+
//
|
|
5
|
+
// The update path is only meaningful for a package installed from npm. A source
|
|
6
|
+
// checkout is managed by git and a linked development copy by the developer, so
|
|
7
|
+
// this refuses to touch either: silently running `npm install -g` over a
|
|
8
|
+
// checkout would replace the tree someone is working in.
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { spawn } = require('node:child_process');
|
|
13
|
+
const { PACKAGE_ROOT } = require('./config');
|
|
14
|
+
|
|
15
|
+
const PACKAGE_NAME = 'herdr-remote';
|
|
16
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
17
|
+
|
|
18
|
+
function currentVersion() {
|
|
19
|
+
try {
|
|
20
|
+
return require(path.join(PACKAGE_ROOT, 'package.json')).version;
|
|
21
|
+
} catch {
|
|
22
|
+
return '0.0.0';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How this copy got here.
|
|
28
|
+
*
|
|
29
|
+
* - `npm` installed from the registry; updating is `npm install -g`.
|
|
30
|
+
* - `linked` `npm link`ed into a global tree from a working copy.
|
|
31
|
+
* - `source` run straight out of a checkout.
|
|
32
|
+
*
|
|
33
|
+
* The distinction is drawn from the path rather than from npm, which would
|
|
34
|
+
* mean shelling out on every render.
|
|
35
|
+
*/
|
|
36
|
+
function installKind() {
|
|
37
|
+
const root = PACKAGE_ROOT;
|
|
38
|
+
// A real install always lives inside a node_modules tree.
|
|
39
|
+
if (!root.split(path.sep).includes('node_modules')) return 'source';
|
|
40
|
+
try {
|
|
41
|
+
// `npm link` leaves a symlink where a published install has a directory.
|
|
42
|
+
if (fs.lstatSync(root).isSymbolicLink()) return 'linked';
|
|
43
|
+
} catch {
|
|
44
|
+
// Unreadable: treat as a normal install and let npm report the problem.
|
|
45
|
+
}
|
|
46
|
+
return 'npm';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function canSelfUpdate() {
|
|
50
|
+
return installKind() === 'npm';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compare two `MAJOR.MINOR.PATCH` strings. Returns 1, -1 or 0. */
|
|
54
|
+
function compareVersions(a, b) {
|
|
55
|
+
const parse = (value) => String(value)
|
|
56
|
+
.split('-')[0]
|
|
57
|
+
.split('.')
|
|
58
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
59
|
+
const left = parse(a);
|
|
60
|
+
const right = parse(b);
|
|
61
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
62
|
+
const diff = (left[index] || 0) - (right[index] || 0);
|
|
63
|
+
if (diff > 0) return 1;
|
|
64
|
+
if (diff < 0) return -1;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ask the registry what the current release is.
|
|
71
|
+
*
|
|
72
|
+
* Never throws: an update check is a convenience, and a machine that is offline
|
|
73
|
+
* or behind a proxy should still get a working settings screen.
|
|
74
|
+
*/
|
|
75
|
+
async function checkForUpdate({ timeoutMs = 8000, fetchImpl = globalThis.fetch } = {}) {
|
|
76
|
+
const current = currentVersion();
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetchImpl(REGISTRY_URL, {
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
headers: { Accept: 'application/vnd.npm.install-v1+json' },
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
return { ok: false, current, errorKey: 'update.errorNetwork' };
|
|
86
|
+
}
|
|
87
|
+
const body = await response.json();
|
|
88
|
+
const latest = typeof body?.version === 'string' ? body.version : null;
|
|
89
|
+
if (!latest) return { ok: false, current, errorKey: 'update.errorNetwork' };
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
current,
|
|
93
|
+
latest,
|
|
94
|
+
updateAvailable: compareVersions(latest, current) > 0,
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
return { ok: false, current, errorKey: 'update.errorNetwork' };
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Install the newest release over this one.
|
|
105
|
+
*
|
|
106
|
+
* Resolves with `{ ok, output }` rather than rejecting, so the caller can show
|
|
107
|
+
* npm's own message; npm's diagnostics are more useful than anything that could
|
|
108
|
+
* be invented here.
|
|
109
|
+
*/
|
|
110
|
+
function performUpdate({
|
|
111
|
+
spawnImpl = spawn,
|
|
112
|
+
timeoutMs = 180_000,
|
|
113
|
+
// Seam for tests: the suite runs from a checkout, where the guard below is
|
|
114
|
+
// correctly the only reachable outcome.
|
|
115
|
+
installKindImpl = installKind,
|
|
116
|
+
} = {}) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const kind = installKindImpl();
|
|
119
|
+
if (kind !== 'npm') {
|
|
120
|
+
resolve({ ok: false, errorKey: `update.cannot.${kind}` });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const child = spawnImpl('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
|
|
124
|
+
encoding: 'utf8',
|
|
125
|
+
timeout: timeoutMs,
|
|
126
|
+
});
|
|
127
|
+
let output = '';
|
|
128
|
+
const collect = (chunk) => { output += String(chunk); };
|
|
129
|
+
child.stdout?.on('data', collect);
|
|
130
|
+
child.stderr?.on('data', collect);
|
|
131
|
+
child.on('error', (error) => {
|
|
132
|
+
resolve({ ok: false, errorKey: 'update.errorFailed', output: error.message });
|
|
133
|
+
});
|
|
134
|
+
child.on('close', (code) => {
|
|
135
|
+
resolve({ ok: code === 0, output: output.trim().slice(-2000) });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
PACKAGE_NAME,
|
|
142
|
+
canSelfUpdate,
|
|
143
|
+
checkForUpdate,
|
|
144
|
+
compareVersions,
|
|
145
|
+
currentVersion,
|
|
146
|
+
installKind,
|
|
147
|
+
performUpdate,
|
|
148
|
+
};
|