herdr-remote 0.2.17 → 0.2.19

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.
Files changed (92) hide show
  1. package/bin/herdr-remote.js +3 -221
  2. package/dist/agent-status.js +130 -0
  3. package/dist/binary-plist.js +157 -0
  4. package/dist/cli.js +255 -0
  5. package/dist/config.js +270 -0
  6. package/dist/connector/agent-watch.js +110 -0
  7. package/dist/connector/font-server.js +165 -0
  8. package/dist/connector/host-connector.js +430 -0
  9. package/dist/connector/lock.js +66 -0
  10. package/dist/connector/main.js +18 -0
  11. package/dist/connector/sessions.js +366 -0
  12. package/dist/connector/update-report.js +67 -0
  13. package/dist/connector/wire.js +42 -0
  14. package/{src → dist}/exit-codes.js +1 -4
  15. package/dist/font-subset.js +110 -0
  16. package/dist/herdr-api.js +246 -0
  17. package/dist/herdr-command.js +223 -0
  18. package/dist/herdr-plugin.js +116 -0
  19. package/dist/herdr-server.js +189 -0
  20. package/dist/i18n/en.js +223 -0
  21. package/dist/i18n/index.js +48 -0
  22. package/dist/i18n/zh.js +222 -0
  23. package/dist/keepalive/detached.js +78 -0
  24. package/dist/keepalive/environment.js +44 -0
  25. package/dist/keepalive/index.js +110 -0
  26. package/dist/keepalive/launchd.js +117 -0
  27. package/dist/keepalive/systemd.js +108 -0
  28. package/dist/keepalive/types.js +1 -0
  29. package/dist/lib/process.js +12 -0
  30. package/dist/lifecycle.js +59 -0
  31. package/dist/net-interfaces.js +73 -0
  32. package/dist/pasted-files.js +115 -0
  33. package/dist/paths.js +62 -0
  34. package/dist/pty-session.js +96 -0
  35. package/dist/relay-client.js +106 -0
  36. package/dist/relay-urls.js +99 -0
  37. package/dist/runtime.js +106 -0
  38. package/dist/service.js +260 -0
  39. package/dist/settings-model.js +298 -0
  40. package/dist/socket-discovery.js +48 -0
  41. package/dist/supervisor.js +319 -0
  42. package/dist/terminal-font/deps.js +73 -0
  43. package/dist/terminal-font/files.js +354 -0
  44. package/dist/terminal-font/formats.js +335 -0
  45. package/dist/terminal-font/index.js +187 -0
  46. package/dist/terminal-font/readers.js +316 -0
  47. package/dist/terminal-font/terminals.js +128 -0
  48. package/dist/terminal-palette.js +339 -0
  49. package/dist/tui/App.js +195 -0
  50. package/dist/tui/api.js +79 -0
  51. package/dist/tui/components/TextField.js +50 -0
  52. package/dist/tui/components/common.js +80 -0
  53. package/dist/tui/index.js +36 -0
  54. package/dist/tui/mouse/index.js +164 -0
  55. package/dist/tui/mouse/protocol.js +96 -0
  56. package/dist/tui/mouse/source.js +74 -0
  57. package/dist/tui/screens/About.js +166 -0
  58. package/dist/tui/screens/Herdr.js +74 -0
  59. package/dist/tui/screens/Keepalive.js +84 -0
  60. package/dist/tui/screens/Overview.js +36 -0
  61. package/dist/tui/screens/Pair.js +74 -0
  62. package/dist/tui/screens/Relay.js +242 -0
  63. package/dist/tui/screens/Services.js +63 -0
  64. package/dist/tui/screens/Wizard.js +235 -0
  65. package/dist/tui/terminal.js +39 -0
  66. package/dist/tui/theme.js +33 -0
  67. package/dist/updater.js +382 -0
  68. package/herdr-plugin.toml +1 -1
  69. package/package.json +10 -9
  70. package/dist/tui.mjs +0 -4613
  71. package/src/agent-status.js +0 -139
  72. package/src/config.js +0 -454
  73. package/src/herdr-api.js +0 -244
  74. package/src/herdr-command.js +0 -231
  75. package/src/herdr-plugin.js +0 -130
  76. package/src/herdr-server.js +0 -201
  77. package/src/host-connector.js +0 -927
  78. package/src/i18n/en.js +0 -240
  79. package/src/i18n/index.js +0 -56
  80. package/src/i18n/zh.js +0 -239
  81. package/src/keepalive.js +0 -460
  82. package/src/lifecycle.js +0 -62
  83. package/src/net-interfaces.js +0 -76
  84. package/src/pasted-files.js +0 -176
  85. package/src/pty-session.js +0 -92
  86. package/src/service.js +0 -516
  87. package/src/settings-model.js +0 -323
  88. package/src/socket-discovery.js +0 -60
  89. package/src/state.js +0 -47
  90. package/src/supervisor.js +0 -250
  91. package/src/terminal-palette.js +0 -325
  92. package/src/updater.js +0 -394
package/dist/tui.mjs DELETED
@@ -1,4613 +0,0 @@
1
- import { createRequire as __createRequire } from 'node:module';
2
- import { dirname as __dirnameOf } from 'node:path';
3
- import { fileURLToPath as __fileURLToPath } from 'node:url';
4
- const require = __createRequire(import.meta.url);
5
- const __filename = __fileURLToPath(import.meta.url);
6
- const __dirname = __dirnameOf(__filename);
7
- var __create = Object.create;
8
- var __defProp = Object.defineProperty;
9
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
- var __getOwnPropNames = Object.getOwnPropertyNames;
11
- var __getProtoOf = Object.getPrototypeOf;
12
- var __hasOwnProp = Object.prototype.hasOwnProperty;
13
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
14
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
15
- }) : x)(function(x) {
16
- if (typeof require !== "undefined") return require.apply(this, arguments);
17
- throw Error('Dynamic require of "' + x + '" is not supported');
18
- });
19
- var __commonJS = (cb, mod) => function __require2() {
20
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
21
- };
22
- var __copyProps = (to, from, except, desc) => {
23
- if (from && typeof from === "object" || typeof from === "function") {
24
- for (let key of __getOwnPropNames(from))
25
- if (!__hasOwnProp.call(to, key) && key !== except)
26
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
27
- }
28
- return to;
29
- };
30
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
31
- // If the importer is in node compatibility mode or this is not an ESM
32
- // file that has been converted to a CommonJS file using a Babel-
33
- // compatible transform (i.e. "__esModule" has not been set), then set
34
- // "default" to the CommonJS "module.exports" for node compatibility.
35
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
36
- mod
37
- ));
38
-
39
- // src/config.js
40
- var require_config = __commonJS({
41
- "src/config.js"(exports, module) {
42
- "use strict";
43
- var fs = __require("node:fs");
44
- var os = __require("node:os");
45
- var path = __require("node:path");
46
- function findPackageRoot(start) {
47
- let directory = start;
48
- for (let depth = 0; depth < 8; depth += 1) {
49
- const candidate = path.join(directory, "package.json");
50
- try {
51
- if (JSON.parse(fs.readFileSync(candidate, "utf8")).name === "herdr-remote") return directory;
52
- } catch {
53
- }
54
- const parent = path.dirname(directory);
55
- if (parent === directory) break;
56
- directory = parent;
57
- }
58
- return path.resolve(start, "..");
59
- }
60
- var PACKAGE_ROOT = findPackageRoot(__dirname);
61
- var ACCESS_MODES2 = ["local", "lan", "remote"];
62
- var OFFICIAL_RELAY_URL2 = "wss://herdr-remote.564616.xyz";
63
- var LANGUAGES = ["auto", "zh", "en"];
64
- var KEEPALIVE_MANAGERS = ["auto", "systemd", "launchd", "supervisor", "none"];
65
- var DEFAULTS = {
66
- ui: {
67
- language: "auto"
68
- },
69
- relay: {
70
- mode: "local",
71
- port: 8787,
72
- // Address advertised in pairing URLs when mode is "lan". Empty means "pick
73
- // the first non-internal IPv4 automatically".
74
- lanHost: "",
75
- // Manual override for the URL browsers open. Empty means "derive it".
76
- publicUrl: "",
77
- // Operator-run relay, e.g. wss://herdr.example.com (mode "remote" only).
78
- remoteUrl: "",
79
- // Kept in step with the relay package's own default: the CLI starts a
80
- // relay of its own, and a smaller ceiling here would reject an upload the
81
- // hosted relay accepts.
82
- maxPayloadBytes: 5 * 1024 * 1024,
83
- maxClientsPerHost: 16,
84
- maxHosts: 1024,
85
- maxPendingHandshakes: 1024,
86
- maxBufferedBytesPerClient: 4 * 1024 * 1024,
87
- hostReconnectGraceMs: 30 * 1e3,
88
- allowedOrigins: []
89
- },
90
- herdr: {
91
- socketPath: null,
92
- args: [],
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
97
- },
98
- auth: {
99
- pairingTtlMs: 10 * 60 * 1e3,
100
- deviceTtlMs: 30 * 24 * 60 * 60 * 1e3,
101
- maxDevices: 32
102
- },
103
- cleanup: {
104
- intervalMs: 60 * 1e3,
105
- heartbeatIntervalMs: 30 * 1e3,
106
- staleAfterMs: 90 * 1e3
107
- },
108
- keepalive: {
109
- manager: "auto"
110
- }
111
- };
112
- function clone(value) {
113
- return JSON.parse(JSON.stringify(value));
114
- }
115
- function configDir() {
116
- return process.env.HERDR_REMOTE_CONFIG_DIR || path.join(os.homedir(), ".config", "herdr-remote");
117
- }
118
- function stateDir2() {
119
- return process.env.HERDR_REMOTE_STATE_DIR || path.join(os.homedir(), ".local", "state", "herdr-remote");
120
- }
121
- function configPath2() {
122
- return path.join(configDir(), "config.json");
123
- }
124
- function runtimeStatePath() {
125
- return path.join(stateDir2(), "runtime.json");
126
- }
127
- function legacyConfigPath() {
128
- return process.env.HERDR_PLUGIN_CONFIG_DIR ? path.join(process.env.HERDR_PLUGIN_CONFIG_DIR, "config.json") : null;
129
- }
130
- function readJson(filePath) {
131
- try {
132
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
133
- } catch (error) {
134
- if (error.code !== "ENOENT") {
135
- process.stderr.write(`herdr-remote: ignoring invalid JSON at ${filePath}: ${error.message}
136
- `);
137
- }
138
- return {};
139
- }
140
- }
141
- function migrateLegacyConfig() {
142
- const target = configPath2();
143
- if (fs.existsSync(target)) return { migrated: false, reason: "config already exists" };
144
- const legacy = legacyConfigPath();
145
- if (!legacy || !fs.existsSync(legacy)) return { migrated: false, reason: "no legacy config" };
146
- try {
147
- fs.mkdirSync(path.dirname(target), { recursive: true, mode: 448 });
148
- fs.copyFileSync(legacy, target);
149
- return { migrated: true, from: legacy, to: target };
150
- } catch (error) {
151
- return { migrated: false, reason: error.message };
152
- }
153
- }
154
- function mergeConfig(fileConfig) {
155
- const config = clone(DEFAULTS);
156
- for (const section of Object.keys(config)) {
157
- if (fileConfig && fileConfig[section] && typeof fileConfig[section] === "object") {
158
- Object.assign(config[section], fileConfig[section]);
159
- }
160
- }
161
- return config;
162
- }
163
- function normalizeLegacyFields(config, fileConfig) {
164
- const legacy = fileConfig && fileConfig.relay || {};
165
- const hasExplicitMode = ACCESS_MODES2.includes(legacy.mode);
166
- if (!hasExplicitMode) {
167
- if (legacy.local === false && typeof legacy.url === "string" && legacy.url) {
168
- config.relay.mode = "remote";
169
- if (!config.relay.remoteUrl) config.relay.remoteUrl = legacy.url;
170
- } else if (typeof legacy.host === "string" && legacy.host && !isLoopbackHost(legacy.host)) {
171
- config.relay.mode = "lan";
172
- } else {
173
- config.relay.mode = "local";
174
- }
175
- }
176
- if (config.relay.mode === "lan" && !config.relay.lanHost && typeof legacy.host === "string" && !isLoopbackHost(legacy.host) && legacy.host !== "0.0.0.0") {
177
- config.relay.lanHost = legacy.host;
178
- }
179
- delete config.relay.local;
180
- delete config.relay.host;
181
- delete config.relay.url;
182
- if (typeof config.relay.publicUrl === "string" && /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/?$/.test(config.relay.publicUrl)) {
183
- config.relay.publicUrl = "";
184
- }
185
- }
186
- function parseInteger(value, fallback, min, max) {
187
- const numeric = Number(value);
188
- if (!Number.isInteger(numeric) || numeric < min || numeric > max) return fallback;
189
- return numeric;
190
- }
191
- function isLoopbackHost(value) {
192
- return value === "127.0.0.1" || value === "localhost" || value === "::1" || value === "[::1]";
193
- }
194
- function isUnspecifiedAddress(value) {
195
- const address = typeof value === "string" ? value.trim().toLowerCase() : "";
196
- return address === "0.0.0.0" || address === "::" || address === "[::]";
197
- }
198
- function isUnspecifiedHost(value) {
199
- try {
200
- const hostname = new URL(value).hostname.toLowerCase();
201
- return hostname === "0.0.0.0" || hostname === "::" || hostname === "[::]";
202
- } catch {
203
- return false;
204
- }
205
- }
206
- function normalizeUrl(value) {
207
- return typeof value === "string" ? value.trim().replace(/\/+$/, "") : "";
208
- }
209
- function validate(config) {
210
- if (!ACCESS_MODES2.includes(config.relay.mode)) config.relay.mode = DEFAULTS.relay.mode;
211
- if (!LANGUAGES.includes(config.ui.language)) config.ui.language = DEFAULTS.ui.language;
212
- if (!KEEPALIVE_MANAGERS.includes(config.keepalive.manager)) config.keepalive.manager = DEFAULTS.keepalive.manager;
213
- config.relay.port = parseInteger(config.relay.port, DEFAULTS.relay.port, 1, 65535);
214
- config.relay.maxPayloadBytes = parseInteger(config.relay.maxPayloadBytes, DEFAULTS.relay.maxPayloadBytes, 4096, 16 * 1024 * 1024);
215
- config.relay.maxClientsPerHost = parseInteger(config.relay.maxClientsPerHost, DEFAULTS.relay.maxClientsPerHost, 1, 256);
216
- config.relay.maxHosts = parseInteger(config.relay.maxHosts, DEFAULTS.relay.maxHosts, 1, 1e5);
217
- config.relay.maxPendingHandshakes = parseInteger(config.relay.maxPendingHandshakes, DEFAULTS.relay.maxPendingHandshakes, 16, 1e5);
218
- config.relay.maxBufferedBytesPerClient = parseInteger(
219
- config.relay.maxBufferedBytesPerClient,
220
- DEFAULTS.relay.maxBufferedBytesPerClient,
221
- 64 * 1024,
222
- 256 * 1024 * 1024
223
- );
224
- config.relay.hostReconnectGraceMs = parseInteger(
225
- config.relay.hostReconnectGraceMs,
226
- DEFAULTS.relay.hostReconnectGraceMs,
227
- 1e3,
228
- 24 * 60 * 60 * 1e3
229
- );
230
- config.auth.pairingTtlMs = parseInteger(config.auth.pairingTtlMs, DEFAULTS.auth.pairingTtlMs, 30 * 1e3, 24 * 60 * 60 * 1e3);
231
- config.auth.deviceTtlMs = parseInteger(config.auth.deviceTtlMs, DEFAULTS.auth.deviceTtlMs, 60 * 1e3, 365 * 24 * 60 * 60 * 1e3);
232
- config.auth.maxDevices = parseInteger(config.auth.maxDevices, DEFAULTS.auth.maxDevices, 1, 1e4);
233
- config.cleanup.intervalMs = parseInteger(config.cleanup.intervalMs, DEFAULTS.cleanup.intervalMs, 1e3, 24 * 60 * 60 * 1e3);
234
- config.cleanup.heartbeatIntervalMs = parseInteger(config.cleanup.heartbeatIntervalMs, DEFAULTS.cleanup.heartbeatIntervalMs, 1e3, 10 * 60 * 1e3);
235
- config.cleanup.staleAfterMs = parseInteger(config.cleanup.staleAfterMs, DEFAULTS.cleanup.staleAfterMs, config.cleanup.heartbeatIntervalMs * 2, 24 * 60 * 60 * 1e3);
236
- config.relay.publicUrl = normalizeUrl(config.relay.publicUrl);
237
- config.relay.remoteUrl = normalizeUrl(config.relay.remoteUrl);
238
- config.relay.lanHost = typeof config.relay.lanHost === "string" ? config.relay.lanHost.trim() : "";
239
- if (config.relay.mode === "lan" && (isLoopbackHost(config.relay.lanHost) || isUnspecifiedAddress(config.relay.lanHost))) {
240
- config.relay.lanHost = "";
241
- }
242
- if (isUnspecifiedHost(config.relay.publicUrl)) config.relay.publicUrl = "";
243
- if (!Array.isArray(config.relay.allowedOrigins)) config.relay.allowedOrigins = [];
244
- if (!Array.isArray(config.herdr.args) || !config.herdr.args.every((arg) => typeof arg === "string")) {
245
- config.herdr.args = [];
246
- }
247
- if (config.herdr.args.includes("--no-session")) {
248
- config.herdr.args = config.herdr.args.filter((arg) => arg !== "--no-session");
249
- process.stderr.write("herdr-remote: ignoring removed --no-session argument (removed in Herdr 0.9.0)\n");
250
- }
251
- if (typeof config.herdr.socketPath !== "string" || config.herdr.socketPath.length === 0) {
252
- config.herdr.socketPath = null;
253
- }
254
- if (typeof config.herdr.cwd !== "string" || config.herdr.cwd.length === 0) {
255
- config.herdr.cwd = os.homedir();
256
- }
257
- config.herdr.autoStart = config.herdr.autoStart === true;
258
- if (config.relay.mode === "remote" && !config.relay.remoteUrl) {
259
- config.relay.mode = "local";
260
- }
261
- return config;
262
- }
263
- function applyEnvironment(config) {
264
- const env = process.env;
265
- if (env.HERDR_REMOTE_LANG && LANGUAGES.includes(env.HERDR_REMOTE_LANG)) config.ui.language = env.HERDR_REMOTE_LANG;
266
- if (env.HERDR_REMOTE_MODE && ACCESS_MODES2.includes(env.HERDR_REMOTE_MODE)) config.relay.mode = env.HERDR_REMOTE_MODE;
267
- if (env.RELAY_PORT) config.relay.port = env.RELAY_PORT;
268
- if (env.RELAY_PUBLIC_URL) config.relay.publicUrl = env.RELAY_PUBLIC_URL;
269
- if (env.RELAY_REMOTE_URL) config.relay.remoteUrl = env.RELAY_REMOTE_URL;
270
- if (env.RELAY_MAX_HOSTS) config.relay.maxHosts = env.RELAY_MAX_HOSTS;
271
- if (env.RELAY_MAX_PENDING_HANDSHAKES) config.relay.maxPendingHandshakes = env.RELAY_MAX_PENDING_HANDSHAKES;
272
- if (env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT) config.relay.maxBufferedBytesPerClient = env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT;
273
- if (env.RELAY_HOST_RECONNECT_GRACE_MS) config.relay.hostReconnectGraceMs = env.RELAY_HOST_RECONNECT_GRACE_MS;
274
- if (env.HERDR_SOCKET_PATH) config.herdr.socketPath = env.HERDR_SOCKET_PATH;
275
- if (env.HERDR_CWD) config.herdr.cwd = env.HERDR_CWD;
276
- if (env.HERDR_ARGS_JSON) {
277
- try {
278
- const args = JSON.parse(env.HERDR_ARGS_JSON);
279
- if (Array.isArray(args)) config.herdr.args = args;
280
- } catch (error) {
281
- process.stderr.write(`herdr-remote: invalid HERDR_ARGS_JSON: ${error.message}
282
- `);
283
- }
284
- }
285
- }
286
- function loadConfig2() {
287
- const fileConfig = readJson(configPath2());
288
- const config = mergeConfig(fileConfig);
289
- normalizeLegacyFields(config, fileConfig);
290
- applyEnvironment(config);
291
- validate(config);
292
- return config;
293
- }
294
- function configExists2() {
295
- return fs.existsSync(configPath2());
296
- }
297
- function runsLocalRelay(config) {
298
- return config.relay.mode !== "remote";
299
- }
300
- function bindAddress2(config) {
301
- return config.relay.mode === "lan" ? "0.0.0.0" : "127.0.0.1";
302
- }
303
- function advertisedHost(config, fallbackLanHost = null) {
304
- if (config.relay.mode === "lan") {
305
- const configured = config.relay.lanHost;
306
- if (configured && !isLoopbackHost(configured) && !isUnspecifiedAddress(configured)) return configured;
307
- if (fallbackLanHost && !isLoopbackHost(fallbackLanHost) && !isUnspecifiedAddress(fallbackLanHost)) {
308
- return fallbackLanHost;
309
- }
310
- return "127.0.0.1";
311
- }
312
- return "127.0.0.1";
313
- }
314
- function resolvePublicUrl2(config, fallbackLanHost = null) {
315
- if (config.relay.publicUrl) return config.relay.publicUrl;
316
- if (config.relay.mode === "remote") return httpOrigin(config.relay.remoteUrl);
317
- return `http://${advertisedHost(config, fallbackLanHost)}:${config.relay.port}`;
318
- }
319
- function resolveAdminOrigin2(config) {
320
- if (config.relay.mode === "remote") return httpOrigin(config.relay.remoteUrl);
321
- return `http://127.0.0.1:${config.relay.port}`;
322
- }
323
- function resolveHostRelayUrl(config) {
324
- const base = config.relay.mode === "remote" ? config.relay.remoteUrl : `ws://127.0.0.1:${config.relay.port}`;
325
- return hostWebSocketUrl(base);
326
- }
327
- function httpOrigin(value) {
328
- try {
329
- const url = new URL(value);
330
- if (url.protocol === "ws:") url.protocol = "http:";
331
- if (url.protocol === "wss:") url.protocol = "https:";
332
- const pathname = url.pathname.replace(/\/+$/, "");
333
- return `${url.origin}${pathname}`;
334
- } catch {
335
- return normalizeUrl(value);
336
- }
337
- }
338
- function hostWebSocketUrl(base) {
339
- const url = new URL(base);
340
- if (url.protocol === "http:") url.protocol = "ws:";
341
- if (url.protocol === "https:") url.protocol = "wss:";
342
- const pathname = url.pathname.replace(/\/+$/, "");
343
- if (!pathname || pathname === "/") {
344
- url.pathname = "/ws/host";
345
- } else if (!pathname.endsWith("/ws/host")) {
346
- url.pathname = `${pathname}/ws/host`;
347
- }
348
- return url.toString();
349
- }
350
- function clientWebSocketUrl(locationLike) {
351
- const url = new URL(locationLike);
352
- if (url.protocol === "http:") url.protocol = "ws:";
353
- if (url.protocol === "https:") url.protocol = "wss:";
354
- const pathname = url.pathname.replace(/\/+$/, "");
355
- url.pathname = !pathname || pathname === "/" ? "/ws/client" : `${pathname}/ws/client`;
356
- return url.toString();
357
- }
358
- module.exports = {
359
- PACKAGE_ROOT,
360
- DEFAULTS,
361
- ACCESS_MODES: ACCESS_MODES2,
362
- OFFICIAL_RELAY_URL: OFFICIAL_RELAY_URL2,
363
- LANGUAGES,
364
- KEEPALIVE_MANAGERS,
365
- configDir,
366
- configPath: configPath2,
367
- configExists: configExists2,
368
- stateDir: stateDir2,
369
- runtimeStatePath,
370
- legacyConfigPath,
371
- migrateLegacyConfig,
372
- loadConfig: loadConfig2,
373
- validate,
374
- runsLocalRelay,
375
- bindAddress: bindAddress2,
376
- advertisedHost,
377
- resolvePublicUrl: resolvePublicUrl2,
378
- resolveAdminOrigin: resolveAdminOrigin2,
379
- resolveHostRelayUrl,
380
- hostWebSocketUrl,
381
- clientWebSocketUrl,
382
- httpOrigin,
383
- isLoopbackHost,
384
- isUnspecifiedAddress,
385
- isUnspecifiedHost
386
- };
387
- }
388
- });
389
-
390
- // src/updater.js
391
- var require_updater = __commonJS({
392
- "src/updater.js"(exports, module) {
393
- "use strict";
394
- var fs = __require("node:fs");
395
- var os = __require("node:os");
396
- var path = __require("node:path");
397
- var { spawn } = __require("node:child_process");
398
- var { PACKAGE_ROOT } = require_config();
399
- var PACKAGE_NAME = "herdr-remote";
400
- var DEFAULT_REGISTRY = "https://registry.npmjs.org";
401
- var MIRROR_REGISTRY = "https://registry.npmmirror.com";
402
- function currentVersion() {
403
- try {
404
- return __require(path.join(PACKAGE_ROOT, "package.json")).version;
405
- } catch {
406
- return "0.0.0";
407
- }
408
- }
409
- function installKind2() {
410
- const root = PACKAGE_ROOT;
411
- if (!root.split(path.sep).includes("node_modules")) return "source";
412
- try {
413
- if (fs.lstatSync(root).isSymbolicLink()) return "linked";
414
- } catch {
415
- }
416
- return "npm";
417
- }
418
- function updateChecksEnabled2(env = process.env) {
419
- return env.HERDR_REMOTE_UPDATE_CHECK !== "0";
420
- }
421
- function canSelfUpdate2() {
422
- return installKind2() === "npm";
423
- }
424
- function normalizeRegistry(value) {
425
- if (typeof value !== "string") return null;
426
- const trimmed = value.trim().replace(/\/+$/, "");
427
- if (!/^https?:\/\//i.test(trimmed)) return null;
428
- return trimmed;
429
- }
430
- function registryFromNpmrc(filePath) {
431
- let contents;
432
- try {
433
- contents = fs.readFileSync(filePath, "utf8");
434
- } catch {
435
- return null;
436
- }
437
- let found = null;
438
- for (const rawLine of contents.split(/\r?\n/)) {
439
- const line = rawLine.trim();
440
- if (!line || line.startsWith("#") || line.startsWith(";")) continue;
441
- const match = /^registry\s*=\s*(.+)$/i.exec(line);
442
- if (match) found = normalizeRegistry(match[1].replace(/^["']|["']$/g, ""));
443
- }
444
- return found;
445
- }
446
- function registryCandidates({ env = process.env, home = os.homedir(), cwd = process.cwd() } = {}) {
447
- const candidates = [];
448
- const add = (value) => {
449
- const normalized = normalizeRegistry(value);
450
- if (normalized && !candidates.includes(normalized)) candidates.push(normalized);
451
- };
452
- add(env.npm_config_registry);
453
- add(env.NPM_CONFIG_REGISTRY);
454
- add(env.HERDR_REMOTE_REGISTRY);
455
- add(registryFromNpmrc(path.join(cwd, ".npmrc")));
456
- add(registryFromNpmrc(path.join(home, ".npmrc")));
457
- add(DEFAULT_REGISTRY);
458
- add(MIRROR_REGISTRY);
459
- return candidates;
460
- }
461
- function compareVersions(a, b) {
462
- const parse = (value) => String(value).split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
463
- const left = parse(a);
464
- const right = parse(b);
465
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
466
- const diff = (left[index] || 0) - (right[index] || 0);
467
- if (diff > 0) return 1;
468
- if (diff < 0) return -1;
469
- }
470
- return 0;
471
- }
472
- async function fetchJson(url, { timeoutMs, fetchImpl }) {
473
- const controller = new AbortController();
474
- const timer = setTimeout(() => controller.abort(), timeoutMs);
475
- try {
476
- const response = await fetchImpl(url, {
477
- signal: controller.signal,
478
- headers: { Accept: "application/json", "Cache-Control": "no-cache" }
479
- });
480
- if (!response.ok) return { ok: false, message: `HTTP ${response.status ?? "?"}` };
481
- return { ok: true, body: await response.json() };
482
- } catch (error) {
483
- return { ok: false, message: error?.name === "AbortError" ? "timed out" : String(error?.message || error) };
484
- } finally {
485
- clearTimeout(timer);
486
- }
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
- }
506
- async function checkForUpdate2({
507
- timeoutMs = 6e3,
508
- fetchImpl = globalThis.fetch,
509
- registries = registryCandidates()
510
- } = {}) {
511
- const current = currentVersion();
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
- };
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 }));
529
- return {
530
- ok: true,
531
- current,
532
- latest,
533
- registry: sources[0],
534
- sources,
535
- behind,
536
- updateAvailable: compareVersions(latest, current) > 0
537
- };
538
- }
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) {
554
- return new Promise((resolve) => {
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) });
560
- return;
561
- }
562
- let output = "";
563
- let settled = false;
564
- const finish = (result) => {
565
- if (!settled) {
566
- settled = true;
567
- resolve(result);
568
- }
569
- };
570
- const collect = (chunk) => {
571
- output += String(chunk);
572
- };
573
- child.stdout?.on("data", collect);
574
- child.stderr?.on("data", collect);
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() }));
577
- });
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
- }
639
- module.exports = {
640
- PACKAGE_NAME,
641
- DEFAULT_REGISTRY,
642
- MIRROR_REGISTRY,
643
- canSelfUpdate: canSelfUpdate2,
644
- checkForUpdate: checkForUpdate2,
645
- compareVersions,
646
- currentVersion,
647
- installKind: installKind2,
648
- installedVersionOnDisk,
649
- npmErrorSummary,
650
- performUpdate: performUpdate2,
651
- registryCandidates,
652
- updateChecksEnabled: updateChecksEnabled2
653
- };
654
- }
655
- });
656
-
657
- // src/herdr-command.js
658
- var require_herdr_command = __commonJS({
659
- "src/herdr-command.js"(exports, module) {
660
- "use strict";
661
- var fs = __require("node:fs");
662
- var os = __require("node:os");
663
- var path = __require("node:path");
664
- var { spawnSync } = __require("node:child_process");
665
- var { compareVersions } = require_updater();
666
- var COMMAND_NAME = "herdr";
667
- var MIN_HERDR_VERSION2 = "0.9.1";
668
- var FALLBACK_DIRECTORIES = [
669
- "~/.local/bin",
670
- "~/.cargo/bin",
671
- "~/bin",
672
- "/opt/homebrew/bin",
673
- "/usr/local/bin",
674
- "/home/linuxbrew/.linuxbrew/bin",
675
- "/usr/bin"
676
- ];
677
- function expandHome(directory, home) {
678
- if (directory === "~") return home;
679
- if (directory.startsWith("~/")) return path.join(home, directory.slice(2));
680
- return directory;
681
- }
682
- function candidateNames(base = COMMAND_NAME) {
683
- return process.platform === "win32" ? [base, `${base}.exe`, `${base}.cmd`, `${base}.bat`] : [base];
684
- }
685
- function isExecutableFile(candidate) {
686
- try {
687
- if (!fs.statSync(candidate).isFile()) return false;
688
- fs.accessSync(candidate, fs.constants.X_OK);
689
- return true;
690
- } catch {
691
- return false;
692
- }
693
- }
694
- function findInDirectory(directory, names = candidateNames()) {
695
- if (!directory) return null;
696
- for (const name of names) {
697
- const candidate = path.join(directory, name);
698
- if (isExecutableFile(candidate)) return candidate;
699
- }
700
- return null;
701
- }
702
- function findOnSearchPath(searchPath, names = candidateNames()) {
703
- for (const entry of String(searchPath || "").split(path.delimiter)) {
704
- const found = findInDirectory(entry.trim(), names);
705
- if (found) return found;
706
- }
707
- return null;
708
- }
709
- function fallbackDirectories(home = os.homedir()) {
710
- return FALLBACK_DIRECTORIES.map((directory) => expandHome(directory, home));
711
- }
712
- function looksLikePath(value) {
713
- return value.includes("/") || value.includes(path.sep);
714
- }
715
- function resolveOverride(value, searchPath) {
716
- const trimmed = String(value || "").trim();
717
- if (!trimmed) return null;
718
- if (looksLikePath(trimmed)) {
719
- const absolute = path.resolve(trimmed);
720
- if (isExecutableFile(absolute)) return absolute;
721
- return findInDirectory(absolute);
722
- }
723
- return findOnSearchPath(searchPath, candidateNames(trimmed));
724
- }
725
- function findHerdrCommand({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home) } = {}) {
726
- const override = resolveOverride(env.HERDR_BIN_PATH, env.PATH);
727
- if (override) return { command: override, source: "env", found: true };
728
- const onPath = findOnSearchPath(env.PATH);
729
- if (onPath) return { command: onPath, source: "path", found: true };
730
- for (const directory of directories) {
731
- const found = findInDirectory(directory);
732
- if (found) return { command: found, source: "fallback", found: true };
733
- }
734
- return { command: COMMAND_NAME, source: "unresolved", found: false };
735
- }
736
- function resolveHerdrCommand(options) {
737
- return findHerdrCommand(options).command;
738
- }
739
- function verifyHerdrCommand(command, options = {}) {
740
- if (typeof command === "string" && looksLikePath(command) && isExecutableFile(command)) {
741
- return { command, source: "verified", found: true };
742
- }
743
- return findHerdrCommand(options);
744
- }
745
- function parseHerdrVersion(output) {
746
- const match = /(\d+)\.(\d+)\.(\d+)/.exec(String(output || ""));
747
- return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
748
- }
749
- function meetsMinimum(version, minimum = MIN_HERDR_VERSION2) {
750
- if (!version) return true;
751
- return compareVersions(version, minimum) >= 0;
752
- }
753
- function herdrVersion2({ command, timeout = 1e4, ...lookup } = {}) {
754
- const resolved = command || resolveHerdrCommand(lookup);
755
- const unknown = { command: resolved, version: null, raw: null, supported: true, ok: false };
756
- let result;
757
- try {
758
- result = spawnSync(resolved, ["--version"], { encoding: "utf8", timeout });
759
- } catch {
760
- return unknown;
761
- }
762
- if (result.error || result.status !== 0 && result.status !== 1) return unknown;
763
- const raw = `${result.stdout || ""}${result.stderr || ""}`.trim();
764
- const version = parseHerdrVersion(raw);
765
- return { command: resolved, version, raw: raw || null, supported: meetsMinimum(version), ok: true };
766
- }
767
- function herdrOutdatedMessage(version) {
768
- 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.`;
769
- }
770
- function herdrNotFoundMessage({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home) } = {}) {
771
- const override = String(env.HERDR_BIN_PATH || "").trim();
772
- const parts = [`Herdr executable "${COMMAND_NAME}" was not found.`];
773
- if (override) parts.push(`HERDR_BIN_PATH=${override} does not point at an executable.`);
774
- parts.push(directories.length ? `Searched PATH and ${directories.join(", ")}.` : "Searched PATH.");
775
- parts.push("Install Herdr or set HERDR_BIN_PATH to its full path, then restart herdr-remote.");
776
- return parts.join(" ");
777
- }
778
- module.exports = {
779
- COMMAND_NAME,
780
- FALLBACK_DIRECTORIES,
781
- MIN_HERDR_VERSION: MIN_HERDR_VERSION2,
782
- fallbackDirectories,
783
- findHerdrCommand,
784
- herdrNotFoundMessage,
785
- herdrOutdatedMessage,
786
- herdrVersion: herdrVersion2,
787
- meetsMinimum,
788
- parseHerdrVersion,
789
- resolveHerdrCommand,
790
- verifyHerdrCommand
791
- };
792
- }
793
- });
794
-
795
- // src/i18n/en.js
796
- var require_en = __commonJS({
797
- "src/i18n/en.js"(exports, module) {
798
- "use strict";
799
- module.exports = {
800
- "app.name": "Herdr Remote",
801
- "app.tagline": "Remote browser access to Herdr workspaces",
802
- "common.yes": "Yes",
803
- "common.no": "No",
804
- "common.on": "on",
805
- "common.off": "off",
806
- "common.none": "none",
807
- "common.auto": "auto",
808
- "common.unknown": "unknown",
809
- "common.running": "running",
810
- "common.stopped": "stopped",
811
- "common.connected": "connected",
812
- "common.disconnected": "disconnected",
813
- "common.installed": "installed",
814
- "common.notInstalled": "not installed",
815
- "common.enabled": "enabled",
816
- "common.disabled": "disabled",
817
- "common.loading": "Working\u2026",
818
- "common.save": "Save settings",
819
- "common.saved": "Configuration saved to {path}",
820
- "common.back": "Back",
821
- "common.cancel": "Cancel",
822
- "common.confirm": "Confirm",
823
- "common.pid": "pid {pid}",
824
- "common.notApplicable": "n/a",
825
- "nav.overview": "Overview",
826
- "nav.pair": "Pair a device",
827
- "nav.services": "Services",
828
- "nav.relay": "Relay",
829
- "nav.keepalive": "Keep-alive",
830
- "nav.herdr": "Herdr",
831
- "nav.about": "Language & about",
832
- "mode.local": "This machine only",
833
- "mode.lan": "Local network / Tailscale",
834
- "mode.official": "Official relay",
835
- "mode.remote": "Self-hosted relay",
836
- "mode.local.description": "Relay listens on 127.0.0.1. Local browser only.",
837
- "mode.lan.description": "Relay listens on 0.0.0.0. Accessible over LAN or Tailscale.",
838
- "mode.official.description": "Connect via official relay. No server needed.",
839
- "mode.remote.description": "Connect via self-hosted relay for external access.",
840
- "overview.title": "Status",
841
- "overview.mode": "Access mode",
842
- "overview.relay": "Relay",
843
- "overview.host": "Host connector",
844
- "overview.socket": "Herdr socket",
845
- "overview.webUrl": "Web UI",
846
- "overview.keepalive": "Keep-alive",
847
- "overview.devices": "Browsers",
848
- "overview.hosts": "Workstations",
849
- "overview.uptime": "Relay uptime",
850
- "overview.relayLocal": "local, {bind}:{port}",
851
- "overview.relayRemote": "remote, {url}",
852
- "overview.socketMissing": "not found \u2014 is Herdr running?",
853
- "overview.unreachable": "unreachable: {message}",
854
- "overview.notStarted": "Services not running. Open Services tab to start.",
855
- "pair.title": "Pair a device",
856
- "pair.generate": "Generate pairing code",
857
- "pair.regenerate": "Regenerate code",
858
- "pair.working": "Generating pairing code\u2026",
859
- "pair.code": "Pairing code",
860
- "pair.url": "Access URL",
861
- "pair.expires": "Expires in {minutes} (at {time})",
862
- "pair.expired": "Code expired. Generate a new one.",
863
- "pair.instructions": "Open URL and enter code to pair.",
864
- "pair.qrHint": "Scan QR code to open URL.",
865
- "pair.qrUnavailable": "Terminal too narrow for QR code; use URL above.",
866
- "pair.failed": "Could not create pairing code: {message}",
867
- "pair.hostOffline": "Relay has no workstation connected. Start services first.",
868
- "services.title": "Services",
869
- "services.start": "Start services",
870
- "services.stop": "Stop services",
871
- "services.restart": "Restart services",
872
- "services.started": "Services started.",
873
- "services.stopped": "Services stopped.",
874
- "services.restarted": "Services restarted.",
875
- "services.startFailed": "Could not start services: {message}",
876
- "services.stopFailed": "Could not stop services: {message}",
877
- "services.unsavedBlocked": "Unsaved changes. Save before restarting.",
878
- "services.managedNotice": "Managed by {manager}; applied via keep-alive service.",
879
- "services.logs": "Recent logs",
880
- "services.logEmpty": "No logs.",
881
- "services.logRelay": "Relay",
882
- "services.logHost": "Host connector",
883
- "relay.title": "Relay settings",
884
- "relay.settings": "Settings",
885
- "relay.locked": "fixed",
886
- "relay.officialFixed": "The official relay address is fixed and cannot be edited.",
887
- "relay.officialHint": "Official relay: address is fixed and no password is needed.",
888
- "relay.password": "Relay password",
889
- "relay.passwordHint": "Must match RELAY_PASSWORD on relay. Leave empty if public.",
890
- "relay.passwordSet": "set",
891
- "relay.passwordEmpty": "not set (public)",
892
- "relay.passwordSaved": "Relay password saved. Restart services to apply.",
893
- "relay.showPassword": "Show password",
894
- "relay.hidePassword": "Hide password",
895
- "relay.identity": "Workstation ID",
896
- "relay.regenerate": "Generate new ID",
897
- "relay.regenerated": "New workstation identity generated. Restart services to re-register.",
898
- "relay.envSnippet": "Relay startup command",
899
- "relay.envHint": "Run this command on the relay host.",
900
- "relay.test": "Test relay connection",
901
- "relay.testOk": "Relay reachable: version {version}, {hosts} workstation(s) connected.",
902
- "relay.testFailed": "Relay unreachable: {message}",
903
- "relay.selectAddress": "Choose advertised address",
904
- "relay.listenAddress": "Listen address",
905
- "relay.listenAddressHint": "Server bind address. Browsers use advertised address below.",
906
- "relay.addressTailscale": "Tailscale",
907
- "relay.addressLan": "LAN",
908
- "relay.addressVirtual": "virtual",
909
- "relay.addressLoopback": "loopback",
910
- "relay.noAddresses": "No non-loopback addresses found.",
911
- "relay.docsHint": "Self-hosting guide: docs/self-hosted-relay.md",
912
- "keepalive.title": "Keep-alive service",
913
- "keepalive.manager": "Manager",
914
- "keepalive.state": "State",
915
- "keepalive.install": "Install and start",
916
- "keepalive.uninstall": "Stop and remove",
917
- "keepalive.restart": "Restart service",
918
- "keepalive.installed": "Keep-alive installed with {manager}.",
919
- "keepalive.uninstalled": "Keep-alive removed.",
920
- "keepalive.restarted": "Keep-alive service restarted.",
921
- "keepalive.failed": "Keep-alive operation failed: {message}",
922
- "keepalive.unitPath": "Unit file",
923
- "keepalive.logsHint": "View logs: {command}",
924
- "keepalive.linger": "Start at boot",
925
- "keepalive.lingerEnabled": "Start at boot enabled.",
926
- "keepalive.lingerDisabled": "Start at boot disabled (runs while logged in).",
927
- "keepalive.enableLinger": "Enable start at boot",
928
- "keepalive.lingerDone": "Start at boot enabled for {username}.",
929
- "keepalive.fallbackNote": "No system service manager found; background process will not survive reboot.",
930
- "herdr.title": "Herdr integration",
931
- "herdr.socketPath": "Socket path",
932
- "herdr.args": "Extra arguments",
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.",
934
- "herdr.cliMissing": "Command herdr not found. Set HERDR_BIN_PATH to its full path.",
935
- "herdr.version": "Herdr version",
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.',
937
- "about.title": "Language & about",
938
- "about.language": "Interface language",
939
- "about.languageAuto": "Follow system ({detected})",
940
- "about.languageZh": "\u4E2D\u6587",
941
- "about.languageEn": "English",
942
- "about.version": "Version",
943
- "about.configPath": "Config file",
944
- "about.statePath": "State directory",
945
- "about.relayPackage": "Relay package",
946
- "about.docs": "Self-hosting guide: docs/self-hosted-relay.md",
947
- "wizard.title": "First-time setup",
948
- "wizard.step": "Step {current} of {total}",
949
- "wizard.languageTitle": "Choose language",
950
- "wizard.accessTitle": "Access mode",
951
- "wizard.accessHint": "Can be changed later in Relay settings.",
952
- "wizard.addressTitle": "Browser access address",
953
- "wizard.relayTitle": "Relay server",
954
- "wizard.relayUrlLabel": "Relay URL (wss://\u2026)",
955
- "wizard.relayHint": "Relay must be running. See docs/self-hosted-relay.md.",
956
- "wizard.passwordTitle": "Relay password",
957
- "wizard.passwordHint": "RELAY_PASSWORD of the relay. Leave empty if unset.",
958
- "wizard.finishTitle": "Finish",
959
- "wizard.finishHint": "Configuration will be saved to {path}.",
960
- "wizard.startNow": "Start services now",
961
- "wizard.installKeepalive": "Run in background",
962
- "wizard.finish": "Save and finish",
963
- "field.mode": "Access mode",
964
- "field.port": "Relay port",
965
- "field.lanHost": "Browser address",
966
- "field.remoteUrl": "Relay URL",
967
- "field.publicUrl": "Browser URL override",
968
- "field.socketPath": "Herdr socket path",
969
- "field.herdrArgs": "Herdr arguments",
970
- "field.herdrAutoStart": "Auto-start Herdr",
971
- "field.language": "Language",
972
- "field.keepalive": "Keep-alive manager",
973
- "placeholder.autoDiscovered": "auto-discovered",
974
- "placeholder.none": "none",
975
- "error.invalidMode": "Unknown access mode.",
976
- "error.invalidPort": "Port must be a number between 1 and 65535.",
977
- "error.invalidLanHost": "Browser address must be a reachable LAN or Tailscale address, not loopback or 0.0.0.0.",
978
- "error.invalidRelayUrl": "Relay URL must start with wss://, ws://, https:// or http://.",
979
- "error.invalidPublicUrl": "Browser URL must start with https:// or http:// and cannot use 0.0.0.0 or ::.",
980
- "error.invalidLanguage": "Unknown language.",
981
- "error.invalidKeepalive": "Unknown keep-alive manager.",
982
- "error.unknownField": "Unknown setting.",
983
- "error.remoteUrlRequired": "Relay URL is required for self-hosted relay mode.",
984
- "error.removedHerdrArg": "Herdr 0.9.0 removed --no-session; remove it from the extra arguments.",
985
- "error.saveFailed": "Could not save configuration: {message}",
986
- "hint.navigate": "\u2191\u2193 move",
987
- "hint.select": "\u21B5 select",
988
- "hint.edit": "\u21B5 edit",
989
- "hint.tabs": "\u2190 \u2192 switch tab",
990
- "hint.back": "esc back",
991
- "hint.quit": "q quit",
992
- "hint.mouseOn": "m mouse off",
993
- "hint.mouseOff": "m mouse on",
994
- "hint.mouseUnsupported": "mouse unsupported",
995
- "hint.restartRequired": "Restart services to apply changes.",
996
- "hint.unsavedChanges": "Unsaved changes (press s to save).",
997
- "hint.save": "s save",
998
- "hint.editing": "\u21B5 confirm \xB7 esc cancel",
999
- "update.title": "Updates",
1000
- "update.banner": "herdr-remote {latest} is out (running {current}). Press 7 and Enter on the update row to install.",
1001
- "update.check": "Check for updates",
1002
- "update.checking": "Checking npm\u2026",
1003
- "update.upToDate": "Up to date ({version})",
1004
- "update.available": "Update {version} available (press Enter to install)",
1005
- "update.updating": "Installing {version}\u2026",
1006
- "update.updatingRetry": "Installing {version}\u2026 (npm has not caught up yet, attempt {attempt})",
1007
- "update.done": "Updated to {version}",
1008
- "update.restartHint": "Update installed. Restart herdr-remote to apply.",
1009
- "update.errorNetwork": "Could not reach any npm registry",
1010
- "update.errorNetworkDetail": "No registry answered: {message}",
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}",
1016
- "update.cannot.source": "Source checkout \u2014 update with git",
1017
- "update.cannot.linked": "Linked package \u2014 managed by npm link"
1018
- };
1019
- }
1020
- });
1021
-
1022
- // src/i18n/zh.js
1023
- var require_zh = __commonJS({
1024
- "src/i18n/zh.js"(exports, module) {
1025
- "use strict";
1026
- module.exports = {
1027
- "app.name": "Herdr Remote",
1028
- "app.tagline": "\u5728\u6D4F\u89C8\u5668\u4E2D\u4F7F\u7528 Herdr \u5DE5\u4F5C\u533A",
1029
- "common.yes": "\u662F",
1030
- "common.no": "\u5426",
1031
- "common.on": "\u5F00",
1032
- "common.off": "\u5173",
1033
- "common.none": "\u65E0",
1034
- "common.auto": "\u81EA\u52A8",
1035
- "common.unknown": "\u672A\u77E5",
1036
- "common.running": "\u8FD0\u884C\u4E2D",
1037
- "common.stopped": "\u5DF2\u505C\u6B62",
1038
- "common.connected": "\u5DF2\u8FDE\u63A5",
1039
- "common.disconnected": "\u672A\u8FDE\u63A5",
1040
- "common.installed": "\u5DF2\u5B89\u88C5",
1041
- "common.notInstalled": "\u672A\u5B89\u88C5",
1042
- "common.enabled": "\u5DF2\u542F\u7528",
1043
- "common.disabled": "\u5DF2\u505C\u7528",
1044
- "common.loading": "\u5904\u7406\u4E2D\u2026",
1045
- "common.save": "\u4FDD\u5B58\u8BBE\u7F6E",
1046
- "common.saved": "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 {path}",
1047
- "common.back": "\u8FD4\u56DE",
1048
- "common.cancel": "\u53D6\u6D88",
1049
- "common.confirm": "\u786E\u8BA4",
1050
- "common.pid": "PID {pid}",
1051
- "common.notApplicable": "\u4E0D\u9002\u7528",
1052
- "nav.overview": "\u6982\u89C8",
1053
- "nav.pair": "\u914D\u5BF9\u8BBE\u5907",
1054
- "nav.services": "\u670D\u52A1",
1055
- "nav.relay": "Relay",
1056
- "nav.keepalive": "\u4FDD\u6D3B",
1057
- "nav.herdr": "Herdr",
1058
- "nav.about": "\u8BED\u8A00\u4E0E\u5173\u4E8E",
1059
- "mode.local": "\u4EC5\u672C\u673A",
1060
- "mode.lan": "\u5C40\u57DF\u7F51 / Tailscale",
1061
- "mode.official": "\u5B98\u65B9 Relay",
1062
- "mode.remote": "\u81EA\u5EFA Relay",
1063
- "mode.local.description": "\u76D1\u542C 127.0.0.1\uFF0C\u4EC5\u672C\u673A\u53EF\u8BBF\u95EE\u3002",
1064
- "mode.lan.description": "\u76D1\u542C 0.0.0.0\uFF0C\u5C40\u57DF\u7F51\u4E0E Tailscale \u8BBE\u5907\u53EF\u8BBF\u95EE\u3002",
1065
- "mode.official.description": "\u4F7F\u7528\u5B98\u65B9 Relay \u8DE8\u7F51\u8BBF\u95EE\uFF0C\u65E0\u9700\u81EA\u5EFA\u670D\u52A1\u5668\u3002",
1066
- "mode.remote.description": "\u8FDE\u63A5\u81EA\u5EFA Relay \u8DE8\u7F51\u8BBF\u95EE\u3002",
1067
- "overview.title": "\u72B6\u6001",
1068
- "overview.mode": "\u8BBF\u95EE\u65B9\u5F0F",
1069
- "overview.relay": "Relay",
1070
- "overview.host": "\u4E3B\u673A\u8FDE\u63A5\u5668",
1071
- "overview.socket": "Herdr \u5957\u63A5\u5B57",
1072
- "overview.webUrl": "Web \u754C\u9762",
1073
- "overview.keepalive": "\u4FDD\u6D3B\u670D\u52A1",
1074
- "overview.devices": "\u6D4F\u89C8\u5668",
1075
- "overview.hosts": "\u5DE5\u4F5C\u7AD9",
1076
- "overview.uptime": "Relay \u8FD0\u884C\u65F6\u95F4",
1077
- "overview.relayLocal": "\u672C\u5730\uFF0C{bind}:{port}",
1078
- "overview.relayRemote": "\u8FDC\u7A0B\uFF0C{url}",
1079
- "overview.socketMissing": "\u672A\u627E\u5230\uFF0C\u8BF7\u786E\u8BA4 Herdr \u662F\u5426\u8FD0\u884C",
1080
- "overview.unreachable": "\u65E0\u6CD5\u8BBF\u95EE\uFF1A{message}",
1081
- "overview.notStarted": "\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u53EF\u524D\u5F80\u300C\u670D\u52A1\u300D\u542F\u52A8\u3002",
1082
- "pair.title": "\u914D\u5BF9\u8BBE\u5907",
1083
- "pair.generate": "\u751F\u6210\u914D\u5BF9\u7801",
1084
- "pair.regenerate": "\u91CD\u65B0\u751F\u6210",
1085
- "pair.working": "\u751F\u6210\u914D\u5BF9\u7801\u4E2D\u2026",
1086
- "pair.code": "\u914D\u5BF9\u7801",
1087
- "pair.url": "\u8BBF\u95EE\u5730\u5740",
1088
- "pair.expires": "\u5269\u4F59 {minutes}\uFF08\u81F3 {time}\uFF09",
1089
- "pair.expired": "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u9700\u91CD\u65B0\u751F\u6210\u3002",
1090
- "pair.instructions": "\u6253\u5F00\u94FE\u63A5\u5E76\u8F93\u5165\u914D\u5BF9\u7801\u3002",
1091
- "pair.qrHint": "\u626B\u63CF\u4E8C\u7EF4\u7801\u6253\u5F00\u94FE\u63A5\u3002",
1092
- "pair.qrUnavailable": "\u7EC8\u7AEF\u5BBD\u5EA6\u4E0D\u8DB3\uFF0C\u65E0\u6CD5\u663E\u793A\u4E8C\u7EF4\u7801\u3002",
1093
- "pair.failed": "\u751F\u6210\u914D\u5BF9\u7801\u5931\u8D25\uFF1A{message}",
1094
- "pair.hostOffline": "Relay \u672A\u8FDE\u63A5\u5DE5\u4F5C\u7AD9\uFF0C\u9700\u5148\u542F\u52A8\u670D\u52A1\u3002",
1095
- "services.title": "\u670D\u52A1",
1096
- "services.start": "\u542F\u52A8\u670D\u52A1",
1097
- "services.stop": "\u505C\u6B62\u670D\u52A1",
1098
- "services.restart": "\u91CD\u542F\u670D\u52A1",
1099
- "services.started": "\u670D\u52A1\u5DF2\u542F\u52A8\u3002",
1100
- "services.stopped": "\u670D\u52A1\u5DF2\u505C\u6B62\u3002",
1101
- "services.restarted": "\u670D\u52A1\u5DF2\u91CD\u542F\u3002",
1102
- "services.startFailed": "\u542F\u52A8\u670D\u52A1\u5931\u8D25\uFF1A{message}",
1103
- "services.stopFailed": "\u505C\u6B62\u670D\u52A1\u5931\u8D25\uFF1A{message}",
1104
- "services.unsavedBlocked": "\u914D\u7F6E\u672A\u4FDD\u5B58\uFF0C\u8BF7\u5148\u4FDD\u5B58\u518D\u91CD\u542F\u3002",
1105
- "services.managedNotice": "\u7531 {manager} \u6258\u7BA1\uFF0C\u5DF2\u901A\u8FC7\u4FDD\u6D3B\u670D\u52A1\u6267\u884C\u3002",
1106
- "services.logs": "\u6700\u8FD1\u65E5\u5FD7",
1107
- "services.logEmpty": "\u6682\u65E0\u65E5\u5FD7\u3002",
1108
- "services.logRelay": "Relay",
1109
- "services.logHost": "\u4E3B\u673A\u8FDE\u63A5\u5668",
1110
- "relay.title": "Relay \u8BBE\u7F6E",
1111
- "relay.settings": "\u8BBE\u7F6E",
1112
- "relay.locked": "\u56FA\u5B9A",
1113
- "relay.officialFixed": "\u5B98\u65B9 relay \u5730\u5740\u56FA\u5B9A\uFF0C\u4E0D\u53EF\u4FEE\u6539\u3002",
1114
- "relay.officialHint": "\u5B98\u65B9 relay\uFF1A\u5730\u5740\u56FA\u5B9A\uFF0C\u65E0\u9700\u5BC6\u7801\u3002",
1115
- "relay.password": "Relay \u5BC6\u7801",
1116
- "relay.passwordHint": "\u987B\u4E0E Relay \u7AEF\u7684 RELAY_PASSWORD \u4E00\u81F4\u3002\u7559\u7A7A\u4E3A\u516C\u7528 Relay\u3002",
1117
- "relay.passwordSet": "\u5DF2\u8BBE\u7F6E",
1118
- "relay.passwordEmpty": "\u672A\u8BBE\u7F6E\uFF08\u516C\u7528\uFF09",
1119
- "relay.passwordSaved": "Relay \u5BC6\u7801\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F\u670D\u52A1\u751F\u6548\u3002",
1120
- "relay.showPassword": "\u663E\u793A\u5BC6\u7801",
1121
- "relay.hidePassword": "\u9690\u85CF\u5BC6\u7801",
1122
- "relay.identity": "\u5DE5\u4F5C\u7AD9\u6807\u8BC6",
1123
- "relay.regenerate": "\u91CD\u65B0\u751F\u6210\u6807\u8BC6",
1124
- "relay.regenerated": "\u5DF2\u751F\u6210\u65B0\u6807\u8BC6\uFF0C\u91CD\u542F\u540E\u91CD\u65B0\u6CE8\u518C\u3002",
1125
- "relay.envSnippet": "Relay \u542F\u52A8\u547D\u4EE4",
1126
- "relay.envHint": "\u5728 Relay \u4E3B\u673A\u6267\u884C\u6B64\u547D\u4EE4\u3002",
1127
- "relay.test": "\u6D4B\u8BD5 Relay \u8FDE\u63A5",
1128
- "relay.testOk": "Relay \u53EF\u8BBF\u95EE\uFF1A\u7248\u672C {version}\uFF0C\u5DF2\u8FDE\u63A5 {hosts} \u53F0\u5DE5\u4F5C\u7AD9\u3002",
1129
- "relay.testFailed": "Relay \u65E0\u6CD5\u8BBF\u95EE\uFF1A{message}",
1130
- "relay.selectAddress": "\u9009\u62E9\u516C\u5E03\u5730\u5740",
1131
- "relay.listenAddress": "\u76D1\u542C\u5730\u5740",
1132
- "relay.listenAddressHint": "\u670D\u52A1\u5668\u7ED1\u5B9A\u5730\u5740\uFF1B\u6D4F\u89C8\u5668\u8BBF\u95EE\u4E0B\u65B9\u516C\u5E03\u5730\u5740\u3002",
1133
- "relay.addressTailscale": "Tailscale",
1134
- "relay.addressLan": "\u5C40\u57DF\u7F51",
1135
- "relay.addressVirtual": "\u865A\u62DF\u7F51\u5361",
1136
- "relay.addressLoopback": "\u56DE\u73AF",
1137
- "relay.noAddresses": "\u672A\u627E\u5230\u975E\u56DE\u73AF\u5730\u5740\u3002",
1138
- "relay.docsHint": "\u81EA\u5EFA\u6307\u5357\uFF1Adocs/self-hosted-relay.zh-CN.md",
1139
- "keepalive.title": "\u540E\u53F0\u4FDD\u6D3B\u670D\u52A1",
1140
- "keepalive.manager": "\u7BA1\u7406\u65B9\u5F0F",
1141
- "keepalive.state": "\u72B6\u6001",
1142
- "keepalive.install": "\u5B89\u88C5\u5E76\u542F\u52A8",
1143
- "keepalive.uninstall": "\u505C\u6B62\u5E76\u79FB\u9664",
1144
- "keepalive.restart": "\u91CD\u542F\u4FDD\u6D3B\u670D\u52A1",
1145
- "keepalive.installed": "\u5DF2\u901A\u8FC7 {manager} \u5B89\u88C5\u4FDD\u6D3B\u670D\u52A1\u3002",
1146
- "keepalive.uninstalled": "\u4FDD\u6D3B\u670D\u52A1\u5DF2\u79FB\u9664\u3002",
1147
- "keepalive.restarted": "\u4FDD\u6D3B\u670D\u52A1\u5DF2\u91CD\u542F\u3002",
1148
- "keepalive.failed": "\u4FDD\u6D3B\u64CD\u4F5C\u5931\u8D25\uFF1A{message}",
1149
- "keepalive.unitPath": "\u5355\u5143\u6587\u4EF6",
1150
- "keepalive.logsHint": "\u67E5\u770B\u65E5\u5FD7\uFF1A{command}",
1151
- "keepalive.linger": "\u5F00\u673A\u81EA\u542F",
1152
- "keepalive.lingerEnabled": "\u5DF2\u5F00\u542F\u5F00\u673A\u81EA\u542F\u3002",
1153
- "keepalive.lingerDisabled": "\u672A\u5F00\u542F\u5F00\u673A\u81EA\u542F\uFF08\u4EC5\u767B\u5F55\u671F\u95F4\u8FD0\u884C\uFF09\u3002",
1154
- "keepalive.enableLinger": "\u5F00\u542F\u5F00\u673A\u81EA\u542F",
1155
- "keepalive.lingerDone": "\u5DF2\u4E3A {username} \u5F00\u542F\u5F00\u673A\u81EA\u542F\u3002",
1156
- "keepalive.fallbackNote": "\u65E0\u7CFB\u7EDF\u670D\u52A1\u7BA1\u7406\u5668\uFF0C\u4F7F\u7528\u540E\u53F0\u5B88\u62A4\u8FDB\u7A0B\uFF0C\u91CD\u542F\u540E\u4E0D\u81EA\u52A8\u6062\u590D\u3002",
1157
- "herdr.title": "Herdr \u96C6\u6210",
1158
- "herdr.socketPath": "\u5957\u63A5\u5B57\u8DEF\u5F84",
1159
- "herdr.args": "\u9644\u52A0\u53C2\u6570",
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",
1161
- "herdr.cliMissing": "\u672A\u627E\u5230 herdr \u547D\u4EE4\uFF0C\u53EF\u5C06 HERDR_BIN_PATH \u8BBE\u4E3A\u5176\u5B8C\u6574\u8DEF\u5F84\u3002",
1162
- "herdr.version": "Herdr \u7248\u672C",
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",
1164
- "about.title": "\u8BED\u8A00\u4E0E\u5173\u4E8E",
1165
- "about.language": "\u754C\u9762\u8BED\u8A00",
1166
- "about.languageAuto": "\u8DDF\u968F\u7CFB\u7EDF\uFF08{detected}\uFF09",
1167
- "about.languageZh": "\u4E2D\u6587",
1168
- "about.languageEn": "English",
1169
- "about.version": "\u7248\u672C",
1170
- "about.configPath": "\u914D\u7F6E\u6587\u4EF6",
1171
- "about.statePath": "\u72B6\u6001\u76EE\u5F55",
1172
- "about.relayPackage": "Relay \u5305",
1173
- "about.docs": "\u81EA\u5EFA\u6307\u5357\uFF1Adocs/self-hosted-relay.zh-CN.md",
1174
- "wizard.title": "\u9996\u6B21\u8BBE\u7F6E",
1175
- "wizard.step": "\u7B2C {current} / {total} \u6B65",
1176
- "wizard.languageTitle": "\u9009\u62E9\u8BED\u8A00",
1177
- "wizard.accessTitle": "\u9009\u62E9\u8BBF\u95EE\u65B9\u5F0F",
1178
- "wizard.accessHint": "\u540E\u7EED\u53EF\u5728 Relay \u9875\u9762\u4FEE\u6539\u3002",
1179
- "wizard.addressTitle": "\u9009\u62E9\u8BBF\u95EE\u5730\u5740",
1180
- "wizard.relayTitle": "Relay \u670D\u52A1\u5668",
1181
- "wizard.relayUrlLabel": "Relay \u5730\u5740\uFF08wss://\u2026\uFF09",
1182
- "wizard.relayHint": "Relay \u9700\u5148\u8FD0\u884C\u3002\u642D\u5EFA\u6307\u5357\u89C1 docs/self-hosted-relay.zh-CN.md\u3002",
1183
- "wizard.passwordTitle": "Relay \u5BC6\u7801",
1184
- "wizard.passwordHint": "Relay \u7684 RELAY_PASSWORD\u3002\u672A\u8BBE\u7559\u7A7A\u3002",
1185
- "wizard.finishTitle": "\u5B8C\u6210",
1186
- "wizard.finishHint": "\u914D\u7F6E\u5C06\u4FDD\u5B58\u81F3 {path}\u3002",
1187
- "wizard.startNow": "\u7ACB\u5373\u542F\u52A8\u670D\u52A1",
1188
- "wizard.installKeepalive": "\u540E\u53F0\u4FDD\u6D3B",
1189
- "wizard.finish": "\u4FDD\u5B58\u5E76\u5B8C\u6210",
1190
- "field.mode": "\u8BBF\u95EE\u65B9\u5F0F",
1191
- "field.port": "Relay \u7AEF\u53E3",
1192
- "field.lanHost": "\u6D4F\u89C8\u5668\u5730\u5740",
1193
- "field.remoteUrl": "Relay \u5730\u5740",
1194
- "field.publicUrl": "\u6D4F\u89C8\u5668\u5730\u5740\uFF08\u8986\u76D6\uFF09",
1195
- "field.socketPath": "Herdr \u5957\u63A5\u5B57\u8DEF\u5F84",
1196
- "field.herdrArgs": "Herdr \u53C2\u6570",
1197
- "field.herdrAutoStart": "\u968F\u670D\u52A1\u542F\u52A8 Herdr",
1198
- "field.language": "\u8BED\u8A00",
1199
- "field.keepalive": "\u4FDD\u6D3B\u7BA1\u7406\u65B9\u5F0F",
1200
- "placeholder.autoDiscovered": "\u81EA\u52A8\u63A2\u6D4B",
1201
- "placeholder.none": "\u65E0",
1202
- "error.invalidMode": "\u672A\u77E5\u7684\u8BBF\u95EE\u65B9\u5F0F\u3002",
1203
- "error.invalidPort": "\u7AEF\u53E3\u987B\u4E3A 1 \u5230 65535 \u4E4B\u95F4\u7684\u6570\u5B57\u3002",
1204
- "error.invalidLanHost": "\u6D4F\u89C8\u5668\u5730\u5740\u987B\u4E3A\u53EF\u8BBF\u95EE\u5C40\u57DF\u7F51\u6216 Tailscale \u5730\u5740\uFF0C\u4E0D\u80FD\u4E3A\u56DE\u73AF\u6216 0.0.0.0\u3002",
1205
- "error.invalidRelayUrl": "Relay \u5730\u5740\u987B\u4EE5 wss://\u3001ws://\u3001https:// \u6216 http:// \u5F00\u5934\u3002",
1206
- "error.invalidPublicUrl": "\u6D4F\u89C8\u5668\u5730\u5740\u987B\u4EE5 https:// \u6216 http:// \u5F00\u5934\uFF0C\u4E0D\u80FD\u4E3A 0.0.0.0 \u6216 ::\u3002",
1207
- "error.invalidLanguage": "\u672A\u77E5\u7684\u8BED\u8A00\u3002",
1208
- "error.invalidKeepalive": "\u672A\u77E5\u7684\u4FDD\u6D3B\u7BA1\u7406\u65B9\u5F0F\u3002",
1209
- "error.unknownField": "\u672A\u77E5\u7684\u8BBE\u7F6E\u9879\u3002",
1210
- "error.remoteUrlRequired": "\u81EA\u5EFA Relay \u6A21\u5F0F\u987B\u586B\u5199 Relay \u5730\u5740\u3002",
1211
- "error.removedHerdrArg": "Herdr 0.9.0 \u5DF2\u79FB\u9664 --no-session\uFF0C\u8BF7\u4ECE\u9644\u52A0\u53C2\u6570\u4E2D\u79FB\u9664\u3002",
1212
- "error.saveFailed": "\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25\uFF1A{message}",
1213
- "hint.navigate": "\u2191\u2193 \u79FB\u52A8",
1214
- "hint.select": "\u21B5 \u9009\u62E9",
1215
- "hint.edit": "\u21B5 \u7F16\u8F91",
1216
- "hint.tabs": "\u2190 \u2192 \u5207\u6362\u9875\u9762",
1217
- "hint.back": "esc \u8FD4\u56DE",
1218
- "hint.quit": "q \u9000\u51FA",
1219
- "hint.mouseOn": "m \u5173\u95ED\u9F20\u6807",
1220
- "hint.mouseOff": "m \u5F00\u542F\u9F20\u6807",
1221
- "hint.mouseUnsupported": "\u4E0D\u652F\u6301\u9F20\u6807",
1222
- "hint.restartRequired": "\u91CD\u542F\u670D\u52A1\u751F\u6548\u3002",
1223
- "hint.unsavedChanges": "\u6709\u672A\u4FDD\u5B58\u4FEE\u6539\uFF08\u6309 s \u4FDD\u5B58\uFF09\u3002",
1224
- "hint.save": "s \u4FDD\u5B58",
1225
- "hint.editing": "\u21B5 \u786E\u8BA4 \xB7 esc \u53D6\u6D88",
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",
1228
- "update.check": "\u68C0\u67E5\u66F4\u65B0",
1229
- "update.checking": "\u67E5\u8BE2 npm \u4E2D\u2026",
1230
- "update.upToDate": "\u5DF2\u662F\u6700\u65B0\uFF08{version}\uFF09",
1231
- "update.available": "\u65B0\u7248\u672C {version} \u53EF\u7528\uFF08\u6309 Enter \u5B89\u88C5\uFF09",
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",
1234
- "update.done": "\u5DF2\u66F4\u65B0\u81F3 {version}",
1235
- "update.restartHint": "\u66F4\u65B0\u5DF2\u5B89\u88C5\uFF0C\u91CD\u542F herdr-remote \u751F\u6548\u3002",
1236
- "update.errorNetwork": "\u6240\u6709 npm registry \u5747\u65E0\u6CD5\u8FDE\u63A5",
1237
- "update.errorNetworkDetail": "\u65E0 registry \u5E94\u7B54\uFF1A{message}",
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",
1243
- "update.cannot.source": "\u6E90\u7801\u76EE\u5F55\u8FD0\u884C\uFF0C\u8BF7\u4F7F\u7528 git \u66F4\u65B0",
1244
- "update.cannot.linked": "npm link \u5DE5\u4F5C\u526F\u672C\uFF0C\u7531 npm link \u7BA1\u7406"
1245
- };
1246
- }
1247
- });
1248
-
1249
- // src/i18n/index.js
1250
- var require_i18n = __commonJS({
1251
- "src/i18n/index.js"(exports, module) {
1252
- "use strict";
1253
- var en = require_en();
1254
- var zh = require_zh();
1255
- var CATALOGUES = { en, zh };
1256
- var DEFAULT_LOCALE = "en";
1257
- function detectLocale2({ env = process.env, preference = "auto" } = {}) {
1258
- if (preference && preference !== "auto" && CATALOGUES[preference]) return preference;
1259
- if (env.HERDR_REMOTE_LANG && CATALOGUES[env.HERDR_REMOTE_LANG]) return env.HERDR_REMOTE_LANG;
1260
- const raw = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE || "";
1261
- const primary = String(raw).split(/[:.@]/)[0].toLowerCase();
1262
- if (primary.startsWith("zh")) return "zh";
1263
- return DEFAULT_LOCALE;
1264
- }
1265
- function interpolate(template, values) {
1266
- if (!values) return template;
1267
- return template.replace(/\{(\w+)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match);
1268
- }
1269
- function createTranslator2(locale = DEFAULT_LOCALE) {
1270
- const active = CATALOGUES[locale] || CATALOGUES[DEFAULT_LOCALE];
1271
- const fallback = CATALOGUES[DEFAULT_LOCALE];
1272
- const t = (key, values) => {
1273
- const template = active[key] ?? fallback[key] ?? key;
1274
- return interpolate(template, values);
1275
- };
1276
- t.locale = locale;
1277
- t.has = (key) => Object.prototype.hasOwnProperty.call(active, key);
1278
- return t;
1279
- }
1280
- module.exports = {
1281
- CATALOGUES,
1282
- DEFAULT_LOCALE,
1283
- detectLocale: detectLocale2,
1284
- createTranslator: createTranslator2,
1285
- interpolate
1286
- };
1287
- }
1288
- });
1289
-
1290
- // src/net-interfaces.js
1291
- var require_net_interfaces = __commonJS({
1292
- "src/net-interfaces.js"(exports, module) {
1293
- "use strict";
1294
- var os = __require("node:os");
1295
- var VIRTUAL_NAME_PATTERN = /^(docker|br-|virbr|veth|vmnet|vboxnet|lxcbr|cni|flannel|kube)/i;
1296
- var TAILSCALE_NAME_PATTERN = /^(tailscale|ts)\d*$/i;
1297
- function isTailscaleAddress(address) {
1298
- const octets = String(address).split(".");
1299
- if (octets.length !== 4) return false;
1300
- const first = Number(octets[0]);
1301
- const second = Number(octets[1]);
1302
- if (!Number.isInteger(first) || !Number.isInteger(second)) return false;
1303
- return first === 100 && second >= 64 && second <= 127;
1304
- }
1305
- function classify(name, info) {
1306
- if (info.internal) return "loopback";
1307
- if (TAILSCALE_NAME_PATTERN.test(name) || isTailscaleAddress(info.address)) return "tailscale";
1308
- if (VIRTUAL_NAME_PATTERN.test(name)) return "virtual";
1309
- return "lan";
1310
- }
1311
- var KIND_ORDER = { tailscale: 0, lan: 1, virtual: 2, loopback: 3 };
1312
- function listReachableAddresses2({ includeLoopback = true, includeIpv6 = false } = {}) {
1313
- const interfaces = os.networkInterfaces();
1314
- const results = [];
1315
- for (const [name, entries] of Object.entries(interfaces)) {
1316
- for (const info of entries || []) {
1317
- const family = typeof info.family === "string" ? info.family : `IPv${info.family}`;
1318
- if (family !== "IPv4" && !(includeIpv6 && family === "IPv6")) continue;
1319
- const kind = classify(name, info);
1320
- if (kind === "loopback" && !includeLoopback) continue;
1321
- results.push({
1322
- name,
1323
- address: info.address,
1324
- family,
1325
- kind,
1326
- internal: Boolean(info.internal)
1327
- });
1328
- }
1329
- }
1330
- results.sort((a, b) => {
1331
- const byKind = KIND_ORDER[a.kind] - KIND_ORDER[b.kind];
1332
- if (byKind !== 0) return byKind;
1333
- return a.address.localeCompare(b.address);
1334
- });
1335
- return results;
1336
- }
1337
- function preferredLanAddress2() {
1338
- const candidates = listReachableAddresses2({ includeLoopback: false });
1339
- return candidates.length > 0 ? candidates[0].address : null;
1340
- }
1341
- module.exports = {
1342
- isTailscaleAddress,
1343
- listReachableAddresses: listReachableAddresses2,
1344
- preferredLanAddress: preferredLanAddress2
1345
- };
1346
- }
1347
- });
1348
-
1349
- // src/state.js
1350
- var require_state = __commonJS({
1351
- "src/state.js"(exports, module) {
1352
- "use strict";
1353
- var crypto = __require("node:crypto");
1354
- var fs = __require("node:fs");
1355
- var path = __require("node:path");
1356
- function ensureDir(dirPath) {
1357
- fs.mkdirSync(dirPath, { recursive: true, mode: 448 });
1358
- try {
1359
- fs.chmodSync(dirPath, 448);
1360
- } catch {
1361
- }
1362
- }
1363
- function writeJsonAtomic(filePath, value) {
1364
- ensureDir(path.dirname(filePath));
1365
- const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
1366
- fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}
1367
- `, { mode: 384 });
1368
- try {
1369
- fs.chmodSync(tempPath, 384);
1370
- } catch {
1371
- }
1372
- fs.renameSync(tempPath, filePath);
1373
- try {
1374
- fs.chmodSync(filePath, 384);
1375
- } catch {
1376
- }
1377
- }
1378
- function readJson(filePath, fallback) {
1379
- try {
1380
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
1381
- } catch (error) {
1382
- if (error.code !== "ENOENT") {
1383
- process.stderr.write(`herdr-remote: invalid state at ${filePath}: ${error.message}
1384
- `);
1385
- }
1386
- return fallback;
1387
- }
1388
- }
1389
- function randomToken(bytes = 32) {
1390
- return crypto.randomBytes(bytes).toString("base64url");
1391
- }
1392
- module.exports = {
1393
- ensureDir,
1394
- writeJsonAtomic,
1395
- readJson,
1396
- randomToken
1397
- };
1398
- }
1399
- });
1400
-
1401
- // src/settings-model.js
1402
- var require_settings_model = __commonJS({
1403
- "src/settings-model.js"(exports, module) {
1404
- "use strict";
1405
- var {
1406
- ACCESS_MODES: ACCESS_MODES2,
1407
- KEEPALIVE_MANAGERS,
1408
- LANGUAGES,
1409
- OFFICIAL_RELAY_URL: OFFICIAL_RELAY_URL2,
1410
- configDir,
1411
- configPath: configPath2,
1412
- isLoopbackHost,
1413
- isUnspecifiedAddress,
1414
- isUnspecifiedHost,
1415
- loadConfig: loadConfig2,
1416
- resolvePublicUrl: resolvePublicUrl2
1417
- } = require_config();
1418
- var { ensureDir, readJson, writeJsonAtomic } = require_state();
1419
- var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
1420
- var FIELDS2 = [
1421
- { id: "mode", kind: "choice", choices: ACCESS_MODES2, labelKey: "field.mode" },
1422
- { id: "port", kind: "text", labelKey: "field.port", visibleFor: ["local", "lan"] },
1423
- { id: "lanHost", kind: "address", labelKey: "field.lanHost", visibleFor: ["lan"] },
1424
- { id: "remoteUrl", kind: "text", labelKey: "field.remoteUrl", visibleFor: ["remote"] },
1425
- { id: "publicUrl", kind: "text", labelKey: "field.publicUrl" },
1426
- { id: "socketPath", kind: "text", labelKey: "field.socketPath" },
1427
- { id: "herdrArgs", kind: "text", labelKey: "field.herdrArgs" },
1428
- { id: "herdrAutoStart", kind: "toggle", labelKey: "field.herdrAutoStart" },
1429
- { id: "language", kind: "choice", choices: LANGUAGES, labelKey: "field.language" },
1430
- { id: "keepaliveManager", kind: "choice", choices: KEEPALIVE_MANAGERS, labelKey: "field.keepalive" }
1431
- ];
1432
- var EMPTY = "";
1433
- var SELECTABLE_MODES2 = ["local", "lan", "official", "remote"];
1434
- function selectedMode2(draft) {
1435
- if (draft.relay.mode === "remote" && draft.relay.remoteUrl === OFFICIAL_RELAY_URL2) return "official";
1436
- return draft.relay.mode;
1437
- }
1438
- function isOfficialRelay2(draft) {
1439
- return selectedMode2(draft) === "official";
1440
- }
1441
- function clone(value) {
1442
- return JSON.parse(JSON.stringify(value));
1443
- }
1444
- function createDraft2(config = loadConfig2()) {
1445
- return clone(config);
1446
- }
1447
- function fieldsForMode2(mode) {
1448
- return FIELDS2.filter((field) => !field.visibleFor || field.visibleFor.includes(mode));
1449
- }
1450
- function getField2(draft, id) {
1451
- switch (id) {
1452
- case "mode":
1453
- return draft.relay.mode;
1454
- case "port":
1455
- return String(draft.relay.port);
1456
- case "lanHost":
1457
- return draft.relay.lanHost || EMPTY;
1458
- case "remoteUrl":
1459
- return draft.relay.remoteUrl || EMPTY;
1460
- case "publicUrl":
1461
- return draft.relay.publicUrl || EMPTY;
1462
- case "socketPath":
1463
- return draft.herdr.socketPath || EMPTY;
1464
- case "herdrArgs":
1465
- return (draft.herdr.args || []).join(" ");
1466
- case "herdrAutoStart":
1467
- return draft.herdr.autoStart ? "on" : "off";
1468
- case "language":
1469
- return draft.ui.language;
1470
- case "keepaliveManager":
1471
- return draft.keepalive.manager;
1472
- default:
1473
- return EMPTY;
1474
- }
1475
- }
1476
- function getFieldPlaceholder2(draft, id) {
1477
- switch (id) {
1478
- case "lanHost":
1479
- return preferredLanAddress2() || "0.0.0.0";
1480
- case "publicUrl":
1481
- return resolvePublicUrl2(draft, preferredLanAddress2());
1482
- case "socketPath":
1483
- return "placeholder.autoDiscovered";
1484
- case "herdrArgs":
1485
- return "placeholder.none";
1486
- case "remoteUrl":
1487
- return "wss://relay.example.com";
1488
- default:
1489
- return EMPTY;
1490
- }
1491
- }
1492
- function isValidUrl(value, protocols) {
1493
- try {
1494
- const url = new URL(value);
1495
- return protocols.includes(url.protocol);
1496
- } catch {
1497
- return false;
1498
- }
1499
- }
1500
- function setField2(draft, id, rawValue) {
1501
- const next = clone(draft);
1502
- const value = typeof rawValue === "string" ? rawValue.trim() : rawValue;
1503
- switch (id) {
1504
- case "mode": {
1505
- if (!SELECTABLE_MODES2.includes(value)) return { draft, errorKey: "error.invalidMode" };
1506
- if (value === "official") {
1507
- next.relay.mode = "remote";
1508
- next.relay.remoteUrl = OFFICIAL_RELAY_URL2;
1509
- next.relay.publicUrl = EMPTY;
1510
- break;
1511
- }
1512
- if (value === "remote" && next.relay.remoteUrl === OFFICIAL_RELAY_URL2) {
1513
- next.relay.remoteUrl = EMPTY;
1514
- }
1515
- next.relay.mode = value;
1516
- if (value === "lan" && (isLoopbackHost(next.relay.lanHost) || isUnspecifiedAddress(next.relay.lanHost))) {
1517
- next.relay.lanHost = EMPTY;
1518
- }
1519
- next.relay.publicUrl = EMPTY;
1520
- break;
1521
- }
1522
- case "port": {
1523
- const port = Number.parseInt(value, 10);
1524
- if (!Number.isInteger(port) || port < 1 || port > 65535) return { draft, errorKey: "error.invalidPort" };
1525
- next.relay.port = port;
1526
- break;
1527
- }
1528
- case "lanHost": {
1529
- if (value && (isLoopbackHost(value) || isUnspecifiedAddress(value))) {
1530
- return { draft, errorKey: "error.invalidLanHost" };
1531
- }
1532
- next.relay.lanHost = value || EMPTY;
1533
- break;
1534
- }
1535
- case "remoteUrl": {
1536
- if (value && !isValidUrl(value, ["ws:", "wss:", "http:", "https:"])) {
1537
- return { draft, errorKey: "error.invalidRelayUrl" };
1538
- }
1539
- next.relay.remoteUrl = value.replace(/\/+$/, "");
1540
- break;
1541
- }
1542
- case "publicUrl": {
1543
- if (value && (!isValidUrl(value, ["http:", "https:"]) || isUnspecifiedHost(value))) {
1544
- return { draft, errorKey: "error.invalidPublicUrl" };
1545
- }
1546
- next.relay.publicUrl = value.replace(/\/+$/, "");
1547
- break;
1548
- }
1549
- case "socketPath": {
1550
- next.herdr.socketPath = value || null;
1551
- break;
1552
- }
1553
- case "herdrArgs": {
1554
- const args = value ? value.split(/\s+/).filter(Boolean) : [];
1555
- if (args.includes("--no-session")) {
1556
- return { draft, errorKey: "error.removedHerdrArg" };
1557
- }
1558
- next.herdr.args = args;
1559
- break;
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
- }
1568
- case "language": {
1569
- if (!LANGUAGES.includes(value)) return { draft, errorKey: "error.invalidLanguage" };
1570
- next.ui.language = value;
1571
- break;
1572
- }
1573
- case "keepaliveManager": {
1574
- if (!KEEPALIVE_MANAGERS.includes(value)) return { draft, errorKey: "error.invalidKeepalive" };
1575
- next.keepalive.manager = value;
1576
- break;
1577
- }
1578
- default:
1579
- return { draft, errorKey: "error.unknownField" };
1580
- }
1581
- return { draft: next, errorKey: null };
1582
- }
1583
- function validateDraft2(draft) {
1584
- const problems = [];
1585
- if (draft.relay.mode === "remote" && !draft.relay.remoteUrl) problems.push("error.remoteUrlRequired");
1586
- return problems;
1587
- }
1588
- function saveDraft2(draft) {
1589
- const problems = validateDraft2(draft);
1590
- if (problems.length > 0) {
1591
- const error = new Error(`configuration is incomplete: ${problems.join(", ")}`);
1592
- error.problems = problems;
1593
- throw error;
1594
- }
1595
- const current = readJson(configPath2(), {}) || {};
1596
- const merged = { ...current };
1597
- merged.ui = { ...current.ui || {}, language: draft.ui.language };
1598
- merged.relay = {
1599
- ...current.relay || {},
1600
- mode: draft.relay.mode,
1601
- port: draft.relay.port,
1602
- lanHost: draft.relay.mode === "lan" && (isLoopbackHost(draft.relay.lanHost) || isUnspecifiedAddress(draft.relay.lanHost)) ? EMPTY : draft.relay.lanHost,
1603
- publicUrl: draft.relay.publicUrl,
1604
- remoteUrl: draft.relay.remoteUrl
1605
- };
1606
- delete merged.relay.local;
1607
- delete merged.relay.host;
1608
- delete merged.relay.url;
1609
- delete merged.patch;
1610
- merged.herdr = {
1611
- ...current.herdr || {},
1612
- socketPath: draft.herdr.socketPath,
1613
- args: draft.herdr.args,
1614
- autoStart: draft.herdr.autoStart === true
1615
- };
1616
- if (!merged.herdr.socketPath) delete merged.herdr.socketPath;
1617
- merged.keepalive = { ...current.keepalive || {}, manager: draft.keepalive.manager };
1618
- ensureDir(configDir());
1619
- writeJsonAtomic(configPath2(), merged);
1620
- return { ok: true, path: configPath2() };
1621
- }
1622
- function requiresRestart2(before, after) {
1623
- return before.relay.mode !== after.relay.mode || before.relay.port !== after.relay.port || before.relay.lanHost !== after.relay.lanHost || before.relay.remoteUrl !== after.relay.remoteUrl || before.relay.publicUrl !== after.relay.publicUrl || before.herdr.socketPath !== after.herdr.socketPath || before.herdr.args.join(" ") !== after.herdr.args.join(" ");
1624
- }
1625
- module.exports = {
1626
- FIELDS: FIELDS2,
1627
- SELECTABLE_MODES: SELECTABLE_MODES2,
1628
- createDraft: createDraft2,
1629
- fieldsForMode: fieldsForMode2,
1630
- isOfficialRelay: isOfficialRelay2,
1631
- selectedMode: selectedMode2,
1632
- getField: getField2,
1633
- getFieldPlaceholder: getFieldPlaceholder2,
1634
- setField: setField2,
1635
- validateDraft: validateDraft2,
1636
- saveDraft: saveDraft2,
1637
- requiresRestart: requiresRestart2
1638
- };
1639
- }
1640
- });
1641
-
1642
- // src/terminal-palette.js
1643
- var require_terminal_palette = __commonJS({
1644
- "src/terminal-palette.js"(exports, module) {
1645
- "use strict";
1646
- var fs = __require("node:fs");
1647
- var { spawnSync } = __require("node:child_process");
1648
- var { ANSI_PALETTE_KEYS, sanitizeTerminalPalette } = __require("herdr-remote-relay/protocol");
1649
- var { runtimeStatePath, stateDir: stateDir2 } = require_config();
1650
- var { ensureDir, readJson, writeJsonAtomic } = require_state();
1651
- var ANSI_SLOTS = ANSI_PALETTE_KEYS.length;
1652
- var READ_TIMEOUT_TENTHS = "3";
1653
- var MAX_IDLE_READS = 2;
1654
- var TOTAL_TIMEOUT_MS = 3e3;
1655
- var ANSI_KEYS = ANSI_PALETTE_KEYS;
1656
- function parseXColor(value) {
1657
- if (typeof value !== "string") return null;
1658
- const match = /^rgba?:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i.exec(value.trim());
1659
- if (!match) return null;
1660
- const channels = match.slice(1, 4).map((raw) => {
1661
- const width = raw.length;
1662
- if (width === 0 || width > 4) return null;
1663
- const scaled = Math.round(parseInt(raw, 16) / (16 ** width - 1) * 255);
1664
- return Math.max(0, Math.min(255, scaled));
1665
- });
1666
- if (channels.some((channel) => channel === null)) return null;
1667
- return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
1668
- }
1669
- function findOscColorReply(text, expectedPrefix) {
1670
- if (typeof text !== "string") return null;
1671
- const start = text.indexOf(`${expectedPrefix};`);
1672
- if (start === -1) return null;
1673
- const stringTerminator = text.indexOf("\x1B\\", start);
1674
- const bell = text.indexOf("\x07", start);
1675
- let bodyEnd = -1;
1676
- let end = -1;
1677
- if (stringTerminator !== -1 && (bell === -1 || stringTerminator < bell)) {
1678
- bodyEnd = stringTerminator;
1679
- end = stringTerminator + 2;
1680
- } else if (bell !== -1) {
1681
- bodyEnd = bell;
1682
- end = bell + 1;
1683
- } else {
1684
- return null;
1685
- }
1686
- const color = parseXColor(text.slice(start + expectedPrefix.length + 1, bodyEnd).trim());
1687
- return color ? { color, start, end } : null;
1688
- }
1689
- function parseOscColorReply(reply, expectedPrefix) {
1690
- return findOscColorReply(reply, expectedPrefix)?.color ?? null;
1691
- }
1692
- var sanitizePalette = sanitizeTerminalPalette;
1693
- function paletteFromEnvironment(env = process.env) {
1694
- const raw = env.HERDR_TERM_PALETTE_JSON;
1695
- if (!raw) return null;
1696
- try {
1697
- return sanitizePalette(JSON.parse(raw));
1698
- } catch {
1699
- return null;
1700
- }
1701
- }
1702
- function runStty(args, ttyPath) {
1703
- return spawnSync("stty", [...args, "-F", ttyPath], { encoding: "utf8", timeout: 1e3 });
1704
- }
1705
- function takeOscColorReply(state, expectedPrefix) {
1706
- const found = findOscColorReply(state.pending, expectedPrefix);
1707
- if (!found) return null;
1708
- state.pending = state.pending.slice(0, found.start) + state.pending.slice(found.end);
1709
- return found.color;
1710
- }
1711
- function collectPalette(ask) {
1712
- const palette = {};
1713
- const foreground = ask("\x1B]10;?\x1B\\", "\x1B]10");
1714
- if (foreground) palette.foreground = foreground;
1715
- const background = ask("\x1B]11;?\x1B\\", "\x1B]11");
1716
- if (background) palette.background = background;
1717
- const cursor = ask("\x1B]12;?\x1B\\", "\x1B]12");
1718
- if (cursor) palette.cursor = cursor;
1719
- const ansi = {};
1720
- for (let slot = 0; slot < ANSI_SLOTS; slot += 1) {
1721
- const color = ask(`\x1B]4;${slot};?\x1B\\`, `\x1B]4;${slot}`);
1722
- if (!color) break;
1723
- ansi[ANSI_KEYS[slot]] = color;
1724
- }
1725
- if (Object.keys(ansi).length === ANSI_SLOTS) palette.ansi = ansi;
1726
- return sanitizePalette(palette);
1727
- }
1728
- function askTerminal(fd, query, expectedPrefix, state) {
1729
- fs.writeSync(fd, query);
1730
- const buffer = Buffer.alloc(256);
1731
- let idleReads = 0;
1732
- while (idleReads < MAX_IDLE_READS && Date.now() < state.overallDeadline) {
1733
- const matched = takeOscColorReply(state, expectedPrefix);
1734
- if (matched) return matched;
1735
- let bytesRead = 0;
1736
- try {
1737
- bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
1738
- } catch (error) {
1739
- if (error.code === "EAGAIN") {
1740
- idleReads += 1;
1741
- continue;
1742
- }
1743
- return null;
1744
- }
1745
- if (bytesRead > 0) {
1746
- state.pending += buffer.toString("latin1", 0, bytesRead);
1747
- idleReads = 0;
1748
- } else {
1749
- idleReads += 1;
1750
- }
1751
- }
1752
- return takeOscColorReply(state, expectedPrefix);
1753
- }
1754
- function probeTerminalPalette({ ttyPath = "/dev/tty", timeoutMs = TOTAL_TIMEOUT_MS } = {}) {
1755
- if (process.platform === "win32") return null;
1756
- let fd = null;
1757
- let savedMode = null;
1758
- try {
1759
- fd = fs.openSync(ttyPath, "r+");
1760
- } catch {
1761
- return null;
1762
- }
1763
- try {
1764
- const saved = runStty(["-g"], ttyPath);
1765
- if (saved.status !== 0) return null;
1766
- savedMode = saved.stdout.trim();
1767
- if (runStty(["raw", "-echo", "min", "0", "time", READ_TIMEOUT_TENTHS], ttyPath).status !== 0) return null;
1768
- const state = { pending: "", overallDeadline: Date.now() + timeoutMs };
1769
- return collectPalette((query, prefix) => askTerminal(fd, query, prefix, state));
1770
- } catch {
1771
- return null;
1772
- } finally {
1773
- if (savedMode) runStty([savedMode], ttyPath);
1774
- if (fd !== null) {
1775
- try {
1776
- fs.closeSync(fd);
1777
- } catch {
1778
- }
1779
- }
1780
- }
1781
- }
1782
- function rememberTerminalPalette(palette) {
1783
- const clean = sanitizePalette(palette);
1784
- if (!clean) return null;
1785
- try {
1786
- ensureDir(stateDir2());
1787
- const state = readJson(runtimeStatePath(), {});
1788
- writeJsonAtomic(runtimeStatePath(), { ...state, terminalPalette: clean });
1789
- } catch {
1790
- }
1791
- return clean;
1792
- }
1793
- function rememberedTerminalPalette() {
1794
- try {
1795
- return sanitizePalette(readJson(runtimeStatePath(), {}).terminalPalette);
1796
- } catch {
1797
- return null;
1798
- }
1799
- }
1800
- function resolveHostPalette({ env = process.env, probe = probeTerminalPalette } = {}) {
1801
- return paletteFromEnvironment(env) || probe() || null;
1802
- }
1803
- function captureTerminalPalette({ env = process.env, probe = probeTerminalPalette } = {}) {
1804
- if (env.HERDR_TERM_PALETTE_JSON) return paletteFromEnvironment(env);
1805
- if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
1806
- const palette = probe();
1807
- if (!palette) return null;
1808
- env.HERDR_TERM_PALETTE_JSON = JSON.stringify(palette);
1809
- rememberTerminalPalette(palette);
1810
- return palette;
1811
- }
1812
- module.exports = {
1813
- ANSI_KEYS,
1814
- ANSI_SLOTS,
1815
- parseXColor,
1816
- parseOscColorReply,
1817
- takeOscColorReply,
1818
- collectPalette,
1819
- sanitizePalette,
1820
- paletteFromEnvironment,
1821
- probeTerminalPalette,
1822
- resolveHostPalette,
1823
- captureTerminalPalette,
1824
- rememberTerminalPalette,
1825
- rememberedTerminalPalette
1826
- };
1827
- }
1828
- });
1829
-
1830
- // src/socket-discovery.js
1831
- var require_socket_discovery = __commonJS({
1832
- "src/socket-discovery.js"(exports, module) {
1833
- "use strict";
1834
- var fs = __require("node:fs");
1835
- var os = __require("node:os");
1836
- var path = __require("node:path");
1837
- function defaultSocketPath(env = process.env, platform = process.platform) {
1838
- if (env.HERDR_SOCKET_PATH) return env.HERDR_SOCKET_PATH;
1839
- if (platform === "win32") {
1840
- const appData = env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
1841
- return path.join(appData, "herdr", "herdr.sock");
1842
- }
1843
- const configHome = env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
1844
- return path.join(configHome, "herdr", "herdr.sock");
1845
- }
1846
- function resolveSocketPath(configuredPath = null, env = process.env) {
1847
- return configuredPath || env.HERDR_SOCKET_PATH || defaultSocketPath(env);
1848
- }
1849
- function inspectSocket(socketPath, options = {}) {
1850
- if (typeof socketPath !== "string" || socketPath.length === 0) {
1851
- return { ok: false, reason: "socket path is empty", path: socketPath };
1852
- }
1853
- try {
1854
- const stat = fs.statSync(socketPath);
1855
- if (process.platform !== "win32" && !stat.isSocket()) {
1856
- return { ok: false, reason: "path is not a Unix socket", path: socketPath };
1857
- }
1858
- if (options.requireOwner !== false && typeof process.getuid === "function" && stat.uid !== process.getuid()) {
1859
- return { ok: false, reason: "socket is not owned by the current user", path: socketPath, uid: stat.uid };
1860
- }
1861
- return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
1862
- } catch (error) {
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
- };
1869
- }
1870
- }
1871
- function assertSocket(socketPath, options = {}) {
1872
- const result = inspectSocket(socketPath, options);
1873
- if (!result.ok) {
1874
- const error = new Error(`Herdr socket unavailable at ${socketPath}: ${result.reason}`);
1875
- error.code = "HERDR_SOCKET_UNAVAILABLE";
1876
- error.details = result;
1877
- throw error;
1878
- }
1879
- return result;
1880
- }
1881
- module.exports = {
1882
- defaultSocketPath,
1883
- resolveSocketPath,
1884
- inspectSocket,
1885
- assertSocket
1886
- };
1887
- }
1888
- });
1889
-
1890
- // src/service.js
1891
- var require_service = __commonJS({
1892
- "src/service.js"(exports, module) {
1893
- "use strict";
1894
- var fs = __require("node:fs");
1895
- var http = __require("node:http");
1896
- var https = __require("node:https");
1897
- var path = __require("node:path");
1898
- var { spawn } = __require("node:child_process");
1899
- var {
1900
- PACKAGE_ROOT,
1901
- bindAddress: bindAddress2,
1902
- configDir,
1903
- loadConfig: loadConfig2,
1904
- resolveAdminOrigin: resolveAdminOrigin2,
1905
- resolveHostRelayUrl,
1906
- resolvePublicUrl: resolvePublicUrl2,
1907
- runsLocalRelay,
1908
- runtimeStatePath,
1909
- stateDir: stateDir2
1910
- } = require_config();
1911
- var { ensureDir, randomToken, readJson, writeJsonAtomic } = require_state();
1912
- var {
1913
- probeTerminalPalette,
1914
- paletteFromEnvironment,
1915
- rememberTerminalPalette,
1916
- rememberedTerminalPalette
1917
- } = require_terminal_palette();
1918
- var { resolveSocketPath } = require_socket_discovery();
1919
- var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
1920
- var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion: herdrVersion2, resolveHerdrCommand } = require_herdr_command();
1921
- var RUNTIME_VERSION = 2;
1922
- function pidAlive(pid) {
1923
- if (!Number.isInteger(pid) || pid <= 0) return false;
1924
- try {
1925
- process.kill(pid, 0);
1926
- return true;
1927
- } catch (error) {
1928
- return error.code === "EPERM";
1929
- }
1930
- }
1931
- function readRuntime2() {
1932
- const state = readJson(runtimeStatePath(), {});
1933
- return state && typeof state === "object" ? state : {};
1934
- }
1935
- function ensureRuntime2() {
1936
- ensureDir(configDir());
1937
- ensureDir(stateDir2());
1938
- const state = readRuntime2();
1939
- if (!state.version) state.version = RUNTIME_VERSION;
1940
- if (!state.hostId) state.hostId = `host-${randomToken(9)}`;
1941
- if (!state.hostToken) state.hostToken = randomToken(32);
1942
- writeJsonAtomic(runtimeStatePath(), state);
1943
- return state;
1944
- }
1945
- function setRelayPassword2(password) {
1946
- const state = ensureRuntime2();
1947
- state.relayPassword = typeof password === "string" ? password.trim() : "";
1948
- writeJsonAtomic(runtimeStatePath(), state);
1949
- return state;
1950
- }
1951
- function regenerateHostIdentity2() {
1952
- const state = ensureRuntime2();
1953
- state.hostId = `host-${randomToken(9)}`;
1954
- state.hostToken = randomToken(32);
1955
- writeJsonAtomic(runtimeStatePath(), state);
1956
- return state;
1957
- }
1958
- var cachedTerminalPalette;
1959
- function hostTerminalPalette({ refresh = false } = {}) {
1960
- if (!refresh && cachedTerminalPalette !== void 0) return cachedTerminalPalette;
1961
- const inherited = paletteFromEnvironment();
1962
- if (inherited) {
1963
- cachedTerminalPalette = inherited;
1964
- return cachedTerminalPalette;
1965
- }
1966
- const probed = probeTerminalPalette();
1967
- if (probed) {
1968
- cachedTerminalPalette = rememberTerminalPalette(probed);
1969
- return cachedTerminalPalette;
1970
- }
1971
- cachedTerminalPalette = rememberedTerminalPalette();
1972
- return cachedTerminalPalette;
1973
- }
1974
- function logPath(name) {
1975
- return path.join(stateDir2(), `${name}.log`);
1976
- }
1977
- function relayBinPath() {
1978
- const manifest = __require.resolve("herdr-remote-relay/package.json");
1979
- return path.join(path.dirname(manifest), "bin", "herdr-remote-relay.js");
1980
- }
1981
- function relayAuthStatePath() {
1982
- return path.join(stateDir2(), "relay-auth.json");
1983
- }
1984
- function serviceSpecs(config = loadConfig2(), state = ensureRuntime2()) {
1985
- const specs = [];
1986
- const publicUrl = resolvePublicUrl2(config, preferredLanAddress2());
1987
- if (runsLocalRelay(config)) {
1988
- specs.push({
1989
- name: "relay",
1990
- command: process.execPath,
1991
- args: [relayBinPath()],
1992
- env: {
1993
- // Let the shared web UI tell a private workstation relay apart from
1994
- // the operator-facing self-hosted relay. Standalone relay installs
1995
- // default to remote mode.
1996
- RELAY_DEPLOYMENT_MODE: "local",
1997
- RELAY_BIND: bindAddress2(config),
1998
- RELAY_PORT: String(config.relay.port),
1999
- RELAY_PUBLIC_URL: publicUrl,
2000
- // A relay we start ourselves is closed to everything but this
2001
- // workstation: reusing the host token as its password costs nothing and
2002
- // stops another machine on the LAN from enrolling into it.
2003
- RELAY_PASSWORD: state.hostToken,
2004
- RELAY_AUTH_STATE_FILE: relayAuthStatePath(),
2005
- RELAY_ALLOWED_ORIGINS: (config.relay.allowedOrigins || []).join(","),
2006
- RELAY_MAX_CLIENTS_PER_HOST: String(config.relay.maxClientsPerHost),
2007
- RELAY_MAX_HOSTS: String(config.relay.maxHosts),
2008
- RELAY_MAX_PENDING_HANDSHAKES: String(config.relay.maxPendingHandshakes),
2009
- RELAY_MAX_BUFFERED_BYTES_PER_CLIENT: String(config.relay.maxBufferedBytesPerClient),
2010
- RELAY_HOST_RECONNECT_GRACE_MS: String(config.relay.hostReconnectGraceMs)
2011
- }
2012
- });
2013
- }
2014
- const terminalPalette = hostTerminalPalette();
2015
- specs.push({
2016
- name: "host",
2017
- command: process.execPath,
2018
- args: [path.join(PACKAGE_ROOT, "src", "host-connector.js")],
2019
- env: {
2020
- // Captured here, where a terminal may still be attached, because the
2021
- // connector itself usually runs detached with no terminal to ask.
2022
- ...terminalPalette ? { HERDR_TERM_PALETTE_JSON: JSON.stringify(terminalPalette) } : {},
2023
- RELAY_URL: resolveHostRelayUrl(config),
2024
- RELAY_HOST_ID: state.hostId,
2025
- RELAY_HOST_TOKEN: state.hostToken,
2026
- RELAY_PASSWORD: runsLocalRelay(config) ? state.hostToken : state.relayPassword || "",
2027
- HERDR_SOCKET_PATH: resolveSocketPath(config.herdr.socketPath),
2028
- HERDR_ARGS_JSON: JSON.stringify(config.herdr.args),
2029
- HERDR_CWD: config.herdr.cwd,
2030
- HERDR_BIN_PATH: resolveHerdrCommand()
2031
- }
2032
- });
2033
- return specs;
2034
- }
2035
- function baseEnvironment() {
2036
- return { ...process.env, HERDR_REMOTE_SERVICE: "1" };
2037
- }
2038
- function spawnDetached(spec) {
2039
- ensureDir(stateDir2());
2040
- const logFd = fs.openSync(logPath(spec.name), "a");
2041
- try {
2042
- const child = spawn(spec.command, spec.args, {
2043
- cwd: PACKAGE_ROOT,
2044
- env: { ...baseEnvironment(), ...spec.env },
2045
- detached: true,
2046
- stdio: ["ignore", logFd, logFd]
2047
- });
2048
- child.unref();
2049
- return child.pid;
2050
- } finally {
2051
- fs.closeSync(logFd);
2052
- }
2053
- }
2054
- function recordManagedPid(state, name, pid) {
2055
- const existing = Array.isArray(state.managedPids) ? state.managedPids : [];
2056
- const kept = existing.filter((entry) => entry && entry.pid !== pid && pidAlive(entry.pid));
2057
- if (Number.isInteger(pid) && pidAlive(pid)) {
2058
- kept.push({ name, pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
2059
- }
2060
- state.managedPids = kept;
2061
- return state;
2062
- }
2063
- function managedPids(state = readRuntime2()) {
2064
- const pids = /* @__PURE__ */ new Map();
2065
- for (const [name, pid] of [["supervisor", state.supervisorPid], ["host", state.hostPid], ["relay", state.relayPid]]) {
2066
- if (pidAlive(pid) && !pids.has(pid)) pids.set(pid, name);
2067
- }
2068
- for (const entry of state.managedPids || []) {
2069
- if (entry && pidAlive(entry.pid) && !pids.has(entry.pid)) pids.set(entry.pid, entry.name);
2070
- }
2071
- return [...pids].map(([pid, name]) => ({ pid, name }));
2072
- }
2073
- function startServices() {
2074
- const config = loadConfig2();
2075
- const state = ensureRuntime2();
2076
- const next = { ...state };
2077
- const specs = serviceSpecs(config, state);
2078
- for (const spec of specs) {
2079
- const pidKey = `${spec.name}Pid`;
2080
- if (pidAlive(next[pidKey])) continue;
2081
- next[pidKey] = spawnDetached(spec);
2082
- recordManagedPid(next, spec.name, next[pidKey]);
2083
- }
2084
- if (!specs.some((spec) => spec.name === "relay") && pidAlive(next.relayPid)) {
2085
- try {
2086
- process.kill(next.relayPid, "SIGTERM");
2087
- } catch {
2088
- }
2089
- next.relayPid = null;
2090
- }
2091
- next.startedAt = next.startedAt || (/* @__PURE__ */ new Date()).toISOString();
2092
- next.mode = config.relay.mode;
2093
- writeJsonAtomic(runtimeStatePath(), next);
2094
- return {
2095
- ok: true,
2096
- mode: config.relay.mode,
2097
- relay: {
2098
- local: runsLocalRelay(config),
2099
- pid: next.relayPid || null,
2100
- alive: pidAlive(next.relayPid),
2101
- bind: runsLocalRelay(config) ? bindAddress2(config) : null,
2102
- port: config.relay.port,
2103
- remoteUrl: config.relay.remoteUrl || null
2104
- },
2105
- host: {
2106
- pid: next.hostPid || null,
2107
- alive: pidAlive(next.hostPid),
2108
- socketPath: resolveSocketPath(config.herdr.socketPath)
2109
- },
2110
- publicUrl: resolvePublicUrl2(config, preferredLanAddress2())
2111
- };
2112
- }
2113
- function stopServices() {
2114
- const state = readRuntime2();
2115
- const stopped = [];
2116
- const targets = managedPids(state).sort((a, b) => (a.name === "supervisor" ? 0 : 1) - (b.name === "supervisor" ? 0 : 1));
2117
- for (const { pid, name } of targets) {
2118
- try {
2119
- process.kill(pid, "SIGTERM");
2120
- stopped.push({ name, pid });
2121
- } catch (error) {
2122
- process.stderr.write(`herdr-remote: could not stop ${name} (pid ${pid}): ${error.message}
2123
- `);
2124
- }
2125
- }
2126
- state.hostPid = null;
2127
- state.relayPid = null;
2128
- state.supervisorPid = null;
2129
- state.managedPids = [];
2130
- state.startedAt = null;
2131
- ensureDir(stateDir2());
2132
- writeJsonAtomic(runtimeStatePath(), state);
2133
- return { ok: true, stopped };
2134
- }
2135
- function restartServices() {
2136
- stopServices();
2137
- return startServices();
2138
- }
2139
- function requestJson2(urlString, options = {}) {
2140
- return new Promise((resolve, reject) => {
2141
- let url;
2142
- try {
2143
- url = new URL(urlString);
2144
- } catch (error) {
2145
- return reject(error);
2146
- }
2147
- const transport = url.protocol === "https:" ? https : http;
2148
- const request = transport.request(url, {
2149
- method: options.method || "GET",
2150
- headers: options.headers || {},
2151
- timeout: options.timeout || 1500
2152
- }, (response) => {
2153
- const chunks = [];
2154
- response.on("data", (chunk) => chunks.push(chunk));
2155
- response.on("end", () => {
2156
- const text = Buffer.concat(chunks).toString("utf8");
2157
- let body;
2158
- try {
2159
- body = JSON.parse(text);
2160
- } catch {
2161
- body = { raw: text };
2162
- }
2163
- if (response.statusCode >= 400) {
2164
- const error = new Error(body.message || `HTTP ${response.statusCode}`);
2165
- error.statusCode = response.statusCode;
2166
- error.body = body;
2167
- reject(error);
2168
- } else resolve(body);
2169
- });
2170
- });
2171
- request.on("timeout", () => request.destroy(new Error("request timed out")));
2172
- request.on("error", reject);
2173
- if (options.body) request.write(options.body);
2174
- request.end();
2175
- });
2176
- }
2177
- function healthUrl(config) {
2178
- return `${resolveAdminOrigin2(config)}/healthz`;
2179
- }
2180
- async function waitForRelay(config, { attempts = 20, delayMs = 100 } = {}) {
2181
- let lastError;
2182
- for (let attempt = 0; attempt < attempts; attempt += 1) {
2183
- try {
2184
- return await requestJson2(healthUrl(config), { timeout: 800 });
2185
- } catch (error) {
2186
- lastError = error;
2187
- }
2188
- await new Promise((resolve) => setTimeout(resolve, delayMs));
2189
- }
2190
- throw lastError || new Error("relay did not become ready");
2191
- }
2192
- async function waitForHost(config, { attempts = 30, delayMs = 100 } = {}) {
2193
- const state = ensureRuntime2();
2194
- const statusEndpoint = `${resolveAdminOrigin2(config)}/api/status`;
2195
- let lastStatus = null;
2196
- for (let attempt = 0; attempt < attempts; attempt += 1) {
2197
- try {
2198
- lastStatus = await requestJson2(statusEndpoint, {
2199
- timeout: 800,
2200
- headers: {
2201
- "X-Herdr-Host-Id": state.hostId,
2202
- "X-Herdr-Host-Token": state.hostToken
2203
- }
2204
- });
2205
- if (lastStatus.hosts?.some((host) => host.id === state.hostId)) return lastStatus;
2206
- } catch (error2) {
2207
- lastStatus = { ok: false, message: error2.message };
2208
- }
2209
- await new Promise((resolve) => setTimeout(resolve, delayMs));
2210
- }
2211
- const error = new Error(lastStatus?.message || "the Herdr host connector did not register with the relay");
2212
- error.health = lastStatus;
2213
- throw error;
2214
- }
2215
- async function statusServices() {
2216
- const config = loadConfig2();
2217
- const state = readRuntime2();
2218
- const lanAddress = preferredLanAddress2();
2219
- let health = null;
2220
- try {
2221
- health = await requestJson2(healthUrl(config), { timeout: 1200 });
2222
- } catch (error) {
2223
- health = { ok: false, message: error.message };
2224
- }
2225
- const socketPath = resolveSocketPath(config.herdr.socketPath);
2226
- const herdr = findHerdrCommand();
2227
- const installed = herdr.found ? herdrVersion2({ command: herdr.command }) : { version: null, supported: true };
2228
- return {
2229
- ok: true,
2230
- mode: config.relay.mode,
2231
- relay: {
2232
- local: runsLocalRelay(config),
2233
- pid: state.relayPid || null,
2234
- alive: runsLocalRelay(config) ? pidAlive(state.relayPid) : null,
2235
- bind: runsLocalRelay(config) ? bindAddress2(config) : null,
2236
- port: config.relay.port,
2237
- remoteUrl: config.relay.remoteUrl || null,
2238
- health
2239
- },
2240
- host: {
2241
- pid: state.hostPid || null,
2242
- alive: pidAlive(state.hostPid),
2243
- hostId: state.hostId || null,
2244
- socketPath,
2245
- socketExists: Boolean(socketPath) && fs.existsSync(socketPath),
2246
- herdrCommand: herdr.command,
2247
- herdrCommandFound: herdr.found,
2248
- herdrCommandSource: herdr.source,
2249
- herdrVersion: installed.version,
2250
- herdrVersionSupported: installed.supported,
2251
- herdrVersionMinimum: MIN_HERDR_VERSION2
2252
- },
2253
- publicUrl: resolvePublicUrl2(config, lanAddress),
2254
- startedAt: state.startedAt || null
2255
- };
2256
- }
2257
- async function pair2() {
2258
- const config = loadConfig2();
2259
- const state = ensureRuntime2();
2260
- if (runsLocalRelay(config)) startServices();
2261
- await waitForRelay(config);
2262
- await waitForHost(config);
2263
- const pairing = await requestJson2(`${resolveAdminOrigin2(config)}/api/pair/start`, {
2264
- method: "POST",
2265
- headers: {
2266
- "X-Herdr-Host-Id": state.hostId,
2267
- "X-Herdr-Host-Token": state.hostToken
2268
- },
2269
- timeout: 5e3
2270
- });
2271
- return pairing;
2272
- }
2273
- function extractPairingCode2(response) {
2274
- const code = response?.code || response?.pairCode;
2275
- if (typeof code !== "string" || code.length === 0) {
2276
- throw new Error("the relay response did not contain a valid pairing code");
2277
- }
2278
- return code;
2279
- }
2280
- function readLogTail2(name, lines = 40) {
2281
- try {
2282
- const content = fs.readFileSync(logPath(name), "utf8");
2283
- return content.split("\n").filter(Boolean).slice(-lines);
2284
- } catch {
2285
- return [];
2286
- }
2287
- }
2288
- module.exports = {
2289
- RUNTIME_VERSION,
2290
- pidAlive,
2291
- readRuntime: readRuntime2,
2292
- ensureRuntime: ensureRuntime2,
2293
- recordManagedPid,
2294
- managedPids,
2295
- setRelayPassword: setRelayPassword2,
2296
- regenerateHostIdentity: regenerateHostIdentity2,
2297
- serviceSpecs,
2298
- baseEnvironment,
2299
- hostTerminalPalette,
2300
- startServices,
2301
- stopServices,
2302
- restartServices,
2303
- statusServices,
2304
- requestJson: requestJson2,
2305
- waitForRelay,
2306
- waitForHost,
2307
- pair: pair2,
2308
- extractPairingCode: extractPairingCode2,
2309
- relayBinPath,
2310
- relayAuthStatePath,
2311
- logPath,
2312
- readLogTail: readLogTail2
2313
- };
2314
- }
2315
- });
2316
-
2317
- // src/keepalive.js
2318
- var require_keepalive = __commonJS({
2319
- "src/keepalive.js"(exports, module) {
2320
- "use strict";
2321
- var fs = __require("node:fs");
2322
- var os = __require("node:os");
2323
- var path = __require("node:path");
2324
- var { spawn, spawnSync } = __require("node:child_process");
2325
- var { PACKAGE_ROOT, loadConfig: loadConfig2, stateDir: stateDir2 } = require_config();
2326
- var { ensureDir, readJson, writeJsonAtomic } = require_state();
2327
- var { logPath, pidAlive } = require_service();
2328
- var { findHerdrCommand } = require_herdr_command();
2329
- var SYSTEMD_UNIT_NAME = "herdr-remote.service";
2330
- var LAUNCHD_LABEL = "dev.herdr.remote";
2331
- var FALLBACK_PID_FILE = "supervisor.pid";
2332
- function cliEntryPoint() {
2333
- return path.join(PACKAGE_ROOT, "bin", "herdr-remote.js");
2334
- }
2335
- function systemdUnitPath() {
2336
- const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
2337
- return path.join(configHome, "systemd", "user", SYSTEMD_UNIT_NAME);
2338
- }
2339
- function launchdPlistPath() {
2340
- return path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
2341
- }
2342
- function fallbackPidPath() {
2343
- return path.join(stateDir2(), FALLBACK_PID_FILE);
2344
- }
2345
- function commandExists(command) {
2346
- const searchPath = process.env.PATH || "";
2347
- return searchPath.split(path.delimiter).filter(Boolean).some((directory) => {
2348
- try {
2349
- fs.accessSync(path.join(directory, command), fs.constants.X_OK);
2350
- return true;
2351
- } catch {
2352
- return false;
2353
- }
2354
- });
2355
- }
2356
- function detectManager(preference = "auto") {
2357
- if (preference && preference !== "auto") return preference;
2358
- if (process.platform === "linux") {
2359
- if (commandExists("systemctl") && (process.env.DBUS_SESSION_BUS_ADDRESS || process.env.XDG_RUNTIME_DIR)) {
2360
- return "systemd";
2361
- }
2362
- return "supervisor";
2363
- }
2364
- if (process.platform === "darwin") {
2365
- return commandExists("launchctl") ? "launchd" : "supervisor";
2366
- }
2367
- return "supervisor";
2368
- }
2369
- function servicePath({ env = process.env, herdrCommand = null } = {}) {
2370
- const entries = String(env.PATH || "").split(path.delimiter).map((entry) => entry.trim()).filter(Boolean);
2371
- if (herdrCommand) entries.push(path.dirname(herdrCommand));
2372
- const seen = /* @__PURE__ */ new Set();
2373
- const unique = entries.filter((entry) => seen.has(entry) ? false : seen.add(entry));
2374
- return unique.join(path.delimiter);
2375
- }
2376
- function serviceEnvironment({ env = process.env, home = os.homedir(), directories } = {}) {
2377
- const environment = {};
2378
- const herdr = findHerdrCommand({ env, home, ...directories ? { directories } : {} });
2379
- if (herdr.found) environment.HERDR_BIN_PATH = herdr.command;
2380
- const searchPath = servicePath({ env, herdrCommand: herdr.found ? herdr.command : null });
2381
- if (searchPath) environment.PATH = searchPath;
2382
- return environment;
2383
- }
2384
- function systemdEnvironmentLine(key, value) {
2385
- const text = String(value).replace(/[\r\n]+/g, " ").replace(/%/g, "%%");
2386
- if (/^[\w@+=:,./-]*$/.test(text)) return `Environment=${key}=${text}`;
2387
- return `Environment="${key}=${text.replace(/([\\"])/g, "\\$1")}"`;
2388
- }
2389
- function renderSystemdUnit({ nodePath = process.execPath, entryPoint = cliEntryPoint(), environment = {} } = {}) {
2390
- const environmentLines = Object.entries(environment).map(([key, value]) => systemdEnvironmentLine(key, value)).join("\n");
2391
- return `[Unit]
2392
- Description=Herdr Remote (relay and host connector)
2393
- Documentation=https://github.com/herdr/herdr-remote
2394
- After=default.target
2395
-
2396
- [Service]
2397
- Type=simple
2398
- ExecStart=${nodePath} ${entryPoint} run
2399
- Restart=always
2400
- RestartSec=3
2401
- # Give the relay and host connector time to close sessions cleanly.
2402
- TimeoutStopSec=15
2403
- ${environmentLines}
2404
-
2405
- [Install]
2406
- WantedBy=default.target
2407
- `;
2408
- }
2409
- function escapeXml(value) {
2410
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2411
- }
2412
- function renderLaunchdPlist({
2413
- nodePath = process.execPath,
2414
- entryPoint = cliEntryPoint(),
2415
- label = LAUNCHD_LABEL,
2416
- outLog = logPath("supervisor"),
2417
- errLog = logPath("supervisor"),
2418
- environment = {}
2419
- } = {}) {
2420
- const environmentEntries = Object.entries(environment).map(([key, value]) => ` <key>${escapeXml(key)}</key>
2421
- <string>${escapeXml(value)}</string>`).join("\n");
2422
- const environmentBlock = environmentEntries ? ` <key>EnvironmentVariables</key>
2423
- <dict>
2424
- ${environmentEntries}
2425
- </dict>
2426
- ` : "";
2427
- return `<?xml version="1.0" encoding="UTF-8"?>
2428
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2429
- <plist version="1.0">
2430
- <dict>
2431
- <key>Label</key>
2432
- <string>${escapeXml(label)}</string>
2433
- <key>ProgramArguments</key>
2434
- <array>
2435
- <string>${escapeXml(nodePath)}</string>
2436
- <string>${escapeXml(entryPoint)}</string>
2437
- <string>run</string>
2438
- </array>
2439
- <key>RunAtLoad</key>
2440
- <true/>
2441
- <key>KeepAlive</key>
2442
- <true/>
2443
- <key>ProcessType</key>
2444
- <string>Background</string>
2445
- ${environmentBlock} <key>StandardOutPath</key>
2446
- <string>${escapeXml(outLog)}</string>
2447
- <key>StandardErrorPath</key>
2448
- <string>${escapeXml(errLog)}</string>
2449
- </dict>
2450
- </plist>
2451
- `;
2452
- }
2453
- function systemctl(args, { capture = true } = {}) {
2454
- return spawnSync("systemctl", ["--user", ...args], {
2455
- encoding: "utf8",
2456
- stdio: capture ? "pipe" : "inherit"
2457
- });
2458
- }
2459
- function systemdStatus() {
2460
- const installed = fs.existsSync(systemdUnitPath());
2461
- if (!installed) return { manager: "systemd", installed: false, active: false, enabled: false };
2462
- const active = systemctl(["is-active", SYSTEMD_UNIT_NAME]);
2463
- const enabled = systemctl(["is-enabled", SYSTEMD_UNIT_NAME]);
2464
- const lingering = spawnSync("loginctl", ["show-user", os.userInfo().username, "--property=Linger"], { encoding: "utf8" });
2465
- return {
2466
- manager: "systemd",
2467
- installed: true,
2468
- active: String(active.stdout || "").trim() === "active",
2469
- enabled: String(enabled.stdout || "").trim().startsWith("enabled"),
2470
- linger: String(lingering.stdout || "").includes("Linger=yes"),
2471
- unitPath: systemdUnitPath(),
2472
- state: String(active.stdout || active.stderr || "").trim()
2473
- };
2474
- }
2475
- function systemdInstall() {
2476
- const unitPath = systemdUnitPath();
2477
- ensureDir(path.dirname(unitPath));
2478
- fs.writeFileSync(unitPath, renderSystemdUnit({ environment: serviceEnvironment() }), { mode: 420 });
2479
- const reload = systemctl(["daemon-reload"]);
2480
- if (reload.status !== 0) {
2481
- throw new Error(`systemctl --user daemon-reload failed: ${String(reload.stderr || "").trim()}`);
2482
- }
2483
- const enable = systemctl(["enable", "--now", SYSTEMD_UNIT_NAME]);
2484
- if (enable.status !== 0) {
2485
- throw new Error(`systemctl --user enable --now failed: ${String(enable.stderr || "").trim()}`);
2486
- }
2487
- return { ok: true, unitPath, hint: `loginctl enable-linger ${os.userInfo().username}` };
2488
- }
2489
- function systemdUninstall() {
2490
- const unitPath = systemdUnitPath();
2491
- systemctl(["disable", "--now", SYSTEMD_UNIT_NAME]);
2492
- if (fs.existsSync(unitPath)) fs.rmSync(unitPath, { force: true });
2493
- systemctl(["daemon-reload"]);
2494
- return { ok: true, unitPath };
2495
- }
2496
- function launchdDomainTarget() {
2497
- return `gui/${process.getuid ? process.getuid() : ""}`;
2498
- }
2499
- function launchdStatus() {
2500
- const plistPath = launchdPlistPath();
2501
- const installed = fs.existsSync(plistPath);
2502
- if (!installed) return { manager: "launchd", installed: false, active: false, enabled: false };
2503
- const result = spawnSync("launchctl", ["print", `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
2504
- const output = String(result.stdout || "");
2505
- return {
2506
- manager: "launchd",
2507
- installed: true,
2508
- active: result.status === 0 && /state = running/.test(output),
2509
- enabled: result.status === 0,
2510
- unitPath: plistPath,
2511
- state: result.status === 0 ? "loaded" : "not loaded"
2512
- };
2513
- }
2514
- function launchdInstall() {
2515
- const plistPath = launchdPlistPath();
2516
- ensureDir(path.dirname(plistPath));
2517
- ensureDir(stateDir2());
2518
- fs.writeFileSync(plistPath, renderLaunchdPlist({ environment: serviceEnvironment() }), { mode: 420 });
2519
- spawnSync("launchctl", ["bootout", `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: "ignore" });
2520
- const result = spawnSync("launchctl", ["bootstrap", launchdDomainTarget(), plistPath], { encoding: "utf8" });
2521
- if (result.status !== 0) {
2522
- throw new Error(`launchctl bootstrap failed: ${String(result.stderr || "").trim()}`);
2523
- }
2524
- spawnSync("launchctl", ["enable", `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: "ignore" });
2525
- return { ok: true, unitPath: plistPath };
2526
- }
2527
- function launchdUninstall() {
2528
- const plistPath = launchdPlistPath();
2529
- spawnSync("launchctl", ["bootout", `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: "ignore" });
2530
- if (fs.existsSync(plistPath)) fs.rmSync(plistPath, { force: true });
2531
- return { ok: true, unitPath: plistPath };
2532
- }
2533
- function readFallbackPid() {
2534
- const record = readJson(fallbackPidPath(), {});
2535
- return Number.isInteger(record.pid) ? record.pid : null;
2536
- }
2537
- function fallbackStatus() {
2538
- const pid = readFallbackPid();
2539
- const alive = pidAlive(pid);
2540
- return {
2541
- manager: "supervisor",
2542
- installed: alive,
2543
- active: alive,
2544
- enabled: false,
2545
- pid: alive ? pid : null,
2546
- unitPath: fallbackPidPath(),
2547
- state: alive ? "running" : "stopped"
2548
- };
2549
- }
2550
- function fallbackInstall() {
2551
- if (pidAlive(readFallbackPid())) return { ok: true, alreadyRunning: true, pid: readFallbackPid() };
2552
- ensureDir(stateDir2());
2553
- const logFd = fs.openSync(logPath("supervisor"), "a");
2554
- try {
2555
- const child = spawn(process.execPath, [cliEntryPoint(), "run", "--daemon"], {
2556
- cwd: PACKAGE_ROOT,
2557
- env: { ...process.env, HERDR_REMOTE_SERVICE: "1", ...serviceEnvironment() },
2558
- detached: true,
2559
- stdio: ["ignore", logFd, logFd]
2560
- });
2561
- child.unref();
2562
- writeJsonAtomic(fallbackPidPath(), { pid: child.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
2563
- return { ok: true, pid: child.pid, note: "this fallback does not survive a reboot" };
2564
- } finally {
2565
- fs.closeSync(logFd);
2566
- }
2567
- }
2568
- function fallbackUninstall() {
2569
- const pid = readFallbackPid();
2570
- if (pidAlive(pid)) {
2571
- try {
2572
- process.kill(pid, "SIGTERM");
2573
- } catch {
2574
- }
2575
- }
2576
- try {
2577
- fs.rmSync(fallbackPidPath(), { force: true });
2578
- } catch {
2579
- }
2580
- return { ok: true, stoppedPid: pid };
2581
- }
2582
- function resolveManager(config = loadConfig2()) {
2583
- return detectManager(config.keepalive?.manager || "auto");
2584
- }
2585
- function status(config = loadConfig2()) {
2586
- const manager = resolveManager(config);
2587
- if (manager === "none") return { manager: "none", installed: false, active: false, enabled: false };
2588
- if (manager === "systemd") return systemdStatus();
2589
- if (manager === "launchd") return launchdStatus();
2590
- return fallbackStatus();
2591
- }
2592
- function install(config = loadConfig2()) {
2593
- const manager = resolveManager(config);
2594
- if (manager === "systemd") return { manager, ...systemdInstall() };
2595
- if (manager === "launchd") return { manager, ...launchdInstall() };
2596
- if (manager === "none") throw new Error("keep-alive is disabled in the configuration");
2597
- return { manager: "supervisor", ...fallbackInstall() };
2598
- }
2599
- function uninstall(config = loadConfig2()) {
2600
- const manager = resolveManager(config);
2601
- if (manager === "systemd") return { manager, ...systemdUninstall() };
2602
- if (manager === "launchd") return { manager, ...launchdUninstall() };
2603
- return { manager: "supervisor", ...fallbackUninstall() };
2604
- }
2605
- function restart(config = loadConfig2()) {
2606
- const manager = resolveManager(config);
2607
- if (manager === "systemd") {
2608
- const result = systemctl(["restart", SYSTEMD_UNIT_NAME]);
2609
- if (result.status !== 0) throw new Error(String(result.stderr || "").trim() || "systemctl restart failed");
2610
- return { manager, ok: true };
2611
- }
2612
- if (manager === "launchd") {
2613
- spawnSync("launchctl", ["kickstart", "-k", `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: "ignore" });
2614
- return { manager, ok: true };
2615
- }
2616
- fallbackUninstall();
2617
- return { manager: "supervisor", ...fallbackInstall() };
2618
- }
2619
- function stopManaged(config = loadConfig2()) {
2620
- const current = status(config);
2621
- if (!current.installed && !current.active) return { managed: false };
2622
- if (current.manager === "systemd") {
2623
- systemctl(["stop", SYSTEMD_UNIT_NAME]);
2624
- return { managed: true, manager: "systemd" };
2625
- }
2626
- if (current.manager === "launchd") {
2627
- spawnSync("launchctl", ["bootout", `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: "ignore" });
2628
- return { managed: true, manager: "launchd" };
2629
- }
2630
- if (current.manager === "supervisor" && current.active) {
2631
- fallbackUninstall();
2632
- return { managed: true, manager: "supervisor" };
2633
- }
2634
- return { managed: false };
2635
- }
2636
- function enableLinger() {
2637
- const username = os.userInfo().username;
2638
- const result = spawnSync("loginctl", ["enable-linger", username], { encoding: "utf8" });
2639
- if (result.status !== 0) {
2640
- throw new Error(String(result.stderr || "").trim() || "loginctl enable-linger failed");
2641
- }
2642
- return { ok: true, username };
2643
- }
2644
- function logsHint(config = loadConfig2()) {
2645
- const manager = resolveManager(config);
2646
- if (manager === "systemd") return `journalctl --user -u ${SYSTEMD_UNIT_NAME} -f`;
2647
- if (manager === "launchd") return `tail -f ${logPath("supervisor")}`;
2648
- return `tail -f ${logPath("supervisor")}`;
2649
- }
2650
- module.exports = {
2651
- SYSTEMD_UNIT_NAME,
2652
- LAUNCHD_LABEL,
2653
- cliEntryPoint,
2654
- systemdUnitPath,
2655
- launchdPlistPath,
2656
- fallbackPidPath,
2657
- detectManager,
2658
- resolveManager,
2659
- renderSystemdUnit,
2660
- renderLaunchdPlist,
2661
- escapeXml,
2662
- servicePath,
2663
- serviceEnvironment,
2664
- systemdEnvironmentLine,
2665
- status,
2666
- install,
2667
- uninstall,
2668
- restart,
2669
- stopManaged,
2670
- enableLinger,
2671
- logsHint
2672
- };
2673
- }
2674
- });
2675
-
2676
- // src/lifecycle.js
2677
- var require_lifecycle = __commonJS({
2678
- "src/lifecycle.js"(exports, module) {
2679
- "use strict";
2680
- var { loadConfig: loadConfig2 } = require_config();
2681
- var keepalive2 = require_keepalive();
2682
- var { restartServices, startServices, statusServices, stopServices } = require_service();
2683
- function managerInUse(config) {
2684
- const status = keepalive2.status(config);
2685
- return status.installed || status.active ? status : null;
2686
- }
2687
- function startAll2(config = loadConfig2()) {
2688
- const managed = managerInUse(config);
2689
- if (managed) {
2690
- if (!managed.active) keepalive2.restart(config);
2691
- return { ok: true, managed: true, manager: managed.manager, alreadyRunning: Boolean(managed.active) };
2692
- }
2693
- return { ...startServices(), managed: false };
2694
- }
2695
- function stopAll2(config = loadConfig2()) {
2696
- const result = keepalive2.stopManaged(config);
2697
- if (result.managed) {
2698
- stopServices();
2699
- return { ok: true, managed: true, manager: result.manager };
2700
- }
2701
- return { ...stopServices(), managed: false };
2702
- }
2703
- function restartAll2(config = loadConfig2()) {
2704
- const managed = managerInUse(config);
2705
- if (managed) {
2706
- keepalive2.restart(config);
2707
- return { ok: true, managed: true, manager: managed.manager };
2708
- }
2709
- return { ...restartServices(), managed: false };
2710
- }
2711
- async function fullStatus2(config = loadConfig2()) {
2712
- const [services, keepaliveStatus] = [await statusServices(), keepalive2.status(config)];
2713
- return {
2714
- ...services,
2715
- keepalive: keepaliveStatus,
2716
- logsHint: keepalive2.logsHint(config)
2717
- };
2718
- }
2719
- module.exports = { startAll: startAll2, stopAll: stopAll2, restartAll: restartAll2, fullStatus: fullStatus2, managerInUse };
2720
- }
2721
- });
2722
-
2723
- // tui/src/index.tsx
2724
- import { render } from "ink";
2725
-
2726
- // tui/src/App.tsx
2727
- import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef3, useState as useState10 } from "react";
2728
- import { Box as Box11, Text as Text11, useApp, useInput as useInput9 } from "ink";
2729
-
2730
- // tui/src/mouse/index.tsx
2731
- import {
2732
- createContext,
2733
- useCallback,
2734
- useContext,
2735
- useEffect,
2736
- useMemo,
2737
- useRef,
2738
- useState
2739
- } from "react";
2740
-
2741
- // tui/src/mouse/source.ts
2742
- import { PassThrough } from "node:stream";
2743
-
2744
- // tui/src/mouse/protocol.ts
2745
- var ESC = String.fromCharCode(27);
2746
- var ENABLE_MOUSE = `${ESC}[?1000h${ESC}[?1002h${ESC}[?1003h${ESC}[?1006h`;
2747
- var DISABLE_MOUSE = `${ESC}[?1006l${ESC}[?1003l${ESC}[?1002l${ESC}[?1000l`;
2748
- var SGR_PATTERN = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
2749
- var MAX_PENDING = 32;
2750
- function decodeButton(code, isRelease) {
2751
- if (code & 64) return (code & 3) === 1 ? "wheel-down" : "wheel-up";
2752
- if (isRelease) return "none";
2753
- switch (code & 3) {
2754
- case 0:
2755
- return "left";
2756
- case 1:
2757
- return "middle";
2758
- case 2:
2759
- return "right";
2760
- default:
2761
- return "none";
2762
- }
2763
- }
2764
- function couldBeMousePrefix(value) {
2765
- return `${ESC}[<`.startsWith(value.slice(0, 3)) && value.length <= MAX_PENDING && /^\x1b(\[(<[\d;]*)?)?$/.test(value);
2766
- }
2767
- function splitMouseInput(buffer) {
2768
- const events = [];
2769
- let passthrough = "";
2770
- let index = 0;
2771
- while (index < buffer.length) {
2772
- const escapeAt = buffer.indexOf(ESC, index);
2773
- if (escapeAt === -1) {
2774
- passthrough += buffer.slice(index);
2775
- return { events, passthrough, pending: "" };
2776
- }
2777
- passthrough += buffer.slice(index, escapeAt);
2778
- const rest = buffer.slice(escapeAt);
2779
- const match = SGR_PATTERN.exec(rest);
2780
- if (match) {
2781
- const code = Number(match[1]);
2782
- const isRelease = match[4] === "m";
2783
- const isMotion = (code & 32) !== 0;
2784
- const isWheel = (code & 64) !== 0;
2785
- events.push({
2786
- type: isWheel ? "wheel" : isMotion ? "move" : isRelease ? "release" : "press",
2787
- button: decodeButton(code, isRelease),
2788
- x: Number(match[2]),
2789
- y: Number(match[3]),
2790
- shift: (code & 4) !== 0,
2791
- alt: (code & 8) !== 0,
2792
- ctrl: (code & 16) !== 0
2793
- });
2794
- index = escapeAt + match[0].length;
2795
- continue;
2796
- }
2797
- if (couldBeMousePrefix(rest)) {
2798
- return { events, passthrough, pending: rest };
2799
- }
2800
- passthrough += ESC;
2801
- index = escapeAt + 1;
2802
- }
2803
- return { events, passthrough, pending: "" };
2804
- }
2805
- function createMouseSplitter() {
2806
- let pending = "";
2807
- return (chunk) => {
2808
- const result = splitMouseInput(pending + chunk);
2809
- pending = result.pending;
2810
- return { events: result.events, passthrough: result.passthrough };
2811
- };
2812
- }
2813
-
2814
- // tui/src/mouse/source.ts
2815
- function createMouseSource(input = process.stdin, output = process.stdout) {
2816
- const supported = Boolean(input.isTTY && output.isTTY);
2817
- const listeners = /* @__PURE__ */ new Set();
2818
- const split = createMouseSplitter();
2819
- const forwarded = new PassThrough();
2820
- const onData = (chunk) => {
2821
- const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
2822
- const { events, passthrough } = split(text);
2823
- for (const event of events) {
2824
- for (const listener of listeners) listener(event);
2825
- }
2826
- if (passthrough.length > 0) forwarded.write(passthrough);
2827
- };
2828
- if (supported) input.on("data", onData);
2829
- const stdin = forwarded;
2830
- stdin.isTTY = input.isTTY;
2831
- stdin.setRawMode = ((mode) => {
2832
- input.setRawMode?.(mode);
2833
- return stdin;
2834
- });
2835
- stdin.ref = (() => {
2836
- input.ref?.();
2837
- return stdin;
2838
- });
2839
- stdin.unref = (() => {
2840
- input.unref?.();
2841
- return stdin;
2842
- });
2843
- let enabled = false;
2844
- const enable = () => {
2845
- if (!supported || enabled) return;
2846
- output.write(ENABLE_MOUSE);
2847
- enabled = true;
2848
- };
2849
- const disable = () => {
2850
- if (!enabled) return;
2851
- output.write(DISABLE_MOUSE);
2852
- enabled = false;
2853
- };
2854
- return {
2855
- stdin,
2856
- supported,
2857
- isEnabled: () => enabled,
2858
- enable,
2859
- disable,
2860
- subscribe(listener) {
2861
- listeners.add(listener);
2862
- return () => listeners.delete(listener);
2863
- },
2864
- dispose() {
2865
- disable();
2866
- listeners.clear();
2867
- if (supported) input.off("data", onData);
2868
- }
2869
- };
2870
- }
2871
-
2872
- // tui/src/mouse/index.tsx
2873
- import { jsx } from "react/jsx-runtime";
2874
- function measureElement(node) {
2875
- if (!node?.yogaNode) return null;
2876
- const layout = node.yogaNode.getComputedLayout();
2877
- let left = 0;
2878
- let top = 0;
2879
- let current = node;
2880
- while (current?.yogaNode) {
2881
- const own = current.yogaNode.getComputedLayout();
2882
- left += own.left;
2883
- top += own.top;
2884
- current = current.parentNode ?? void 0;
2885
- }
2886
- return { left: left + 1, top: top + 1, width: layout.width, height: layout.height };
2887
- }
2888
- function rectContains(rect, x, y) {
2889
- return x >= rect.left && x < rect.left + rect.width && y >= rect.top && y < rect.top + rect.height;
2890
- }
2891
- var MouseRegistryContext = createContext(null);
2892
- var MouseStateContext = createContext(null);
2893
- function MouseProvider({ source, children }) {
2894
- const targets = useRef(/* @__PURE__ */ new Set());
2895
- const pressed = useRef(null);
2896
- const [hovered, setHovered] = useState(null);
2897
- const [enabled, setEnabled] = useState(false);
2898
- const register = useCallback((target) => {
2899
- targets.current.add(target);
2900
- return () => {
2901
- targets.current.delete(target);
2902
- if (pressed.current === target) pressed.current = null;
2903
- setHovered((current) => current === target ? null : current);
2904
- };
2905
- }, []);
2906
- const hitTest = useCallback((x, y) => {
2907
- let best = null;
2908
- let bestArea = Number.POSITIVE_INFINITY;
2909
- for (const target of targets.current) {
2910
- const rect = measureElement(target.ref.current);
2911
- if (!rect || rect.width <= 0 || rect.height <= 0) continue;
2912
- if (!rectContains(rect, x, y)) continue;
2913
- const area = rect.width * rect.height;
2914
- if (area < bestArea) {
2915
- best = target;
2916
- bestArea = area;
2917
- }
2918
- }
2919
- return best;
2920
- }, []);
2921
- useEffect(() => {
2922
- if (!source) return void 0;
2923
- return source.subscribe((event) => {
2924
- if (event.type === "wheel") return;
2925
- const target = hitTest(event.x, event.y);
2926
- if (event.type === "move") {
2927
- setHovered((current) => current === target ? current : target);
2928
- return;
2929
- }
2930
- if (event.type === "press") {
2931
- if (event.button !== "left") return;
2932
- pressed.current = target;
2933
- setHovered(target);
2934
- return;
2935
- }
2936
- if (pressed.current && pressed.current === target) target?.onClick?.();
2937
- pressed.current = null;
2938
- });
2939
- }, [source, hitTest]);
2940
- useEffect(() => {
2941
- const previous = hovered;
2942
- previous?.onHover?.(true);
2943
- return () => previous?.onHover?.(false);
2944
- }, [hovered]);
2945
- const enable = useCallback(() => {
2946
- if (!source) return;
2947
- source.enable();
2948
- setEnabled(source.isEnabled());
2949
- }, [source]);
2950
- const disable = useCallback(() => {
2951
- if (!source) return;
2952
- source.disable();
2953
- setEnabled(source.isEnabled());
2954
- }, [source]);
2955
- const registry = useMemo(() => ({ register }), [register]);
2956
- const state = useMemo(() => ({
2957
- supported: Boolean(source?.supported),
2958
- enabled,
2959
- enable,
2960
- disable
2961
- }), [source, enabled, enable, disable]);
2962
- return /* @__PURE__ */ jsx(MouseRegistryContext.Provider, { value: registry, children: /* @__PURE__ */ jsx(MouseStateContext.Provider, { value: state, children }) });
2963
- }
2964
- var KEYBOARD_ONLY = {
2965
- supported: false,
2966
- enabled: false,
2967
- enable: () => {
2968
- },
2969
- disable: () => {
2970
- }
2971
- };
2972
- function useMouse() {
2973
- return useContext(MouseStateContext) ?? KEYBOARD_ONLY;
2974
- }
2975
- function useMouseTarget(ref, { onClick, disabled = false } = {}) {
2976
- const context = useContext(MouseRegistryContext);
2977
- const [hovered, setHovered] = useState(false);
2978
- const handler = useRef(onClick);
2979
- handler.current = onClick;
2980
- const target = useMemo(() => ({
2981
- ref,
2982
- onClick: () => handler.current?.(),
2983
- onHover: setHovered
2984
- }), [ref]);
2985
- useEffect(() => {
2986
- if (!context || disabled) return void 0;
2987
- return context.register(target);
2988
- }, [context, target, disabled]);
2989
- useEffect(() => {
2990
- if (disabled) setHovered(false);
2991
- }, [disabled]);
2992
- return hovered && !disabled;
2993
- }
2994
-
2995
- // tui/src/theme.ts
2996
- var theme = {
2997
- /** Selection and focus. */
2998
- accent: "cyan",
2999
- /** Frame lines and separators. */
3000
- border: "gray",
3001
- /** Secondary text: labels, hints, paths. */
3002
- muted: "gray",
3003
- ok: "green",
3004
- warn: "yellow",
3005
- bad: "red"
3006
- };
3007
- var SELECTED_MARKER = "\u25B8";
3008
- var UNSELECTED_MARKER = " ";
3009
- var STATUS_COLOR = {
3010
- ok: theme.ok,
3011
- warn: theme.warn,
3012
- bad: theme.bad,
3013
- idle: theme.muted
3014
- };
3015
- var STATUS_GLYPH = {
3016
- ok: "\u25CF",
3017
- warn: "\u25CF",
3018
- bad: "\u25CF",
3019
- idle: "\u25CB"
3020
- };
3021
-
3022
- // tui/src/api.ts
3023
- var import_config = __toESM(require_config());
3024
- var import_herdr_command = __toESM(require_herdr_command());
3025
- var import_i18n = __toESM(require_i18n());
3026
- var import_net_interfaces = __toESM(require_net_interfaces());
3027
- var import_settings_model = __toESM(require_settings_model());
3028
- var import_service = __toESM(require_service());
3029
- var import_lifecycle = __toESM(require_lifecycle());
3030
- var keepalive = __toESM(require_keepalive());
3031
- var import_updater = __toESM(require_updater());
3032
- function detectLocale(options = {}) {
3033
- return (0, import_i18n.detectLocale)(options);
3034
- }
3035
- function listReachableAddresses(options = {}) {
3036
- return (0, import_net_interfaces.listReachableAddresses)(options);
3037
- }
3038
- function relayStartCommand(config, password) {
3039
- const publicUrl = config.relay.remoteUrl ? config.relay.remoteUrl.replace(/^ws/, "http") : "https://relay.example.com";
3040
- const parts = ["herdr-remote-relay", `--public-url ${publicUrl}`, "--trust-proxy"];
3041
- if (password) parts.push(`--password '${password.replace(/'/g, "'\\''")}'`);
3042
- return parts.join(" ");
3043
- }
3044
- async function probeRelay(config) {
3045
- const origin = (0, import_config.resolveAdminOrigin)(config);
3046
- let health;
3047
- try {
3048
- health = await (0, import_service.requestJson)(`${origin}/healthz`, { timeout: 4e3 });
3049
- } catch (error) {
3050
- return { ok: false, message: error.message };
3051
- }
3052
- try {
3053
- const runtime = (0, import_service.ensureRuntime)();
3054
- const status = await (0, import_service.requestJson)(`${origin}/api/status`, {
3055
- timeout: 4e3,
3056
- headers: {
3057
- "X-Herdr-Host-Id": runtime.hostId,
3058
- "X-Herdr-Host-Token": runtime.hostToken
3059
- }
3060
- });
3061
- return { ok: true, version: String(health.version || status.version || ""), hosts: Array.isArray(status.hosts) ? status.hosts.length : 0 };
3062
- } catch {
3063
- return { ok: true, version: String(health.version || ""), hosts: void 0 };
3064
- }
3065
- }
3066
- function formatUptime(seconds, t) {
3067
- if (!Number.isFinite(seconds)) return t("common.unknown");
3068
- const total = Math.max(0, Math.floor(seconds));
3069
- const hours = Math.floor(total / 3600);
3070
- const minutes = Math.floor(total % 3600 / 60);
3071
- if (hours > 0) return `${hours}h ${minutes}m`;
3072
- if (minutes > 0) return `${minutes}m ${total % 60}s`;
3073
- return `${total}s`;
3074
- }
3075
-
3076
- // tui/src/screens/Overview.tsx
3077
- import { Box as Box2, Text as Text2 } from "ink";
3078
-
3079
- // tui/src/components/common.tsx
3080
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
3081
- import { Box, Text } from "ink";
3082
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
3083
- function StatusDot({ level: level2 }) {
3084
- return /* @__PURE__ */ jsx2(Text, { color: STATUS_COLOR[level2], children: STATUS_GLYPH[level2] });
3085
- }
3086
- var LABEL_COLUMN_WIDTH = 20;
3087
- function Row({
3088
- label,
3089
- labelWidth = LABEL_COLUMN_WIDTH,
3090
- children
3091
- }) {
3092
- return /* @__PURE__ */ jsxs(Box, { children: [
3093
- /* @__PURE__ */ jsx2(Box, { width: labelWidth, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text, { color: theme.muted, wrap: "truncate-end", children: label }) }),
3094
- /* @__PURE__ */ jsx2(Box, { flexGrow: 1, children })
3095
- ] });
3096
- }
3097
- function Selectable({ selected, disabled = false, onSelect, onHover, children }) {
3098
- const ref = useRef2(null);
3099
- const hovered = useMouseTarget(ref, { disabled, onClick: onSelect });
3100
- useFollowPointer(hovered, onHover);
3101
- const active = selected || hovered;
3102
- const color = disabled ? theme.muted : active ? theme.accent : void 0;
3103
- return /* @__PURE__ */ jsxs(Box, { ref, children: [
3104
- /* @__PURE__ */ jsxs(Text, { color: active ? theme.accent : void 0, children: [
3105
- active ? SELECTED_MARKER : UNSELECTED_MARKER,
3106
- " "
3107
- ] }),
3108
- /* @__PURE__ */ jsx2(Text, { color, bold: active, dimColor: disabled, children })
3109
- ] });
3110
- }
3111
- function useFollowPointer(hovered, onHover) {
3112
- const handler = useRef2(onHover);
3113
- handler.current = onHover;
3114
- useEffect2(() => {
3115
- if (hovered) handler.current?.();
3116
- }, [hovered]);
3117
- }
3118
- function Menu({
3119
- items,
3120
- selectedId,
3121
- onChange,
3122
- onSelect
3123
- }) {
3124
- return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", children: items.map((item) => /* @__PURE__ */ jsx2(
3125
- Selectable,
3126
- {
3127
- selected: item.id === selectedId,
3128
- disabled: item.disabled,
3129
- onSelect: () => onSelect(item.id),
3130
- onHover: () => onChange(item.id),
3131
- children: item.label
3132
- },
3133
- item.id
3134
- )) });
3135
- }
3136
- function FieldRow({
3137
- label,
3138
- selected,
3139
- disabled = false,
3140
- labelWidth = 22,
3141
- onSelect,
3142
- onHover,
3143
- children
3144
- }) {
3145
- const ref = useRef2(null);
3146
- const hovered = useMouseTarget(ref, { disabled, onClick: onSelect });
3147
- useFollowPointer(hovered, onHover);
3148
- const active = selected || hovered;
3149
- return /* @__PURE__ */ jsxs(Box, { ref, children: [
3150
- /* @__PURE__ */ jsxs(Box, { width: labelWidth, flexShrink: 0, children: [
3151
- /* @__PURE__ */ jsxs(Text, { color: active ? theme.accent : void 0, children: [
3152
- active ? SELECTED_MARKER : UNSELECTED_MARKER,
3153
- " "
3154
- ] }),
3155
- /* @__PURE__ */ jsx2(Text, { color: active ? theme.accent : theme.muted, bold: active, wrap: "truncate-end", children: label })
3156
- ] }),
3157
- /* @__PURE__ */ jsx2(Box, { flexGrow: 1, children })
3158
- ] });
3159
- }
3160
- function Panel({ title, children }) {
3161
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, flexGrow: 1, children: [
3162
- title ? /* @__PURE__ */ jsx2(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text, { bold: true, children: title }) }) : null,
3163
- children
3164
- ] });
3165
- }
3166
- function Message({ text, level: level2 }) {
3167
- if (!text) return null;
3168
- const color = level2 === "error" ? theme.bad : level2 === "success" ? theme.ok : theme.muted;
3169
- return /* @__PURE__ */ jsx2(Box, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text, { color, children: text }) });
3170
- }
3171
-
3172
- // tui/src/screens/Overview.tsx
3173
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
3174
- function level(alive) {
3175
- if (alive === null || alive === void 0) return "idle";
3176
- return alive ? "ok" : "bad";
3177
- }
3178
- function Overview({ ctx }) {
3179
- const { t, status, config } = ctx;
3180
- if (!status) {
3181
- return /* @__PURE__ */ jsx3(Panel, { title: t("overview.title"), children: /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: t("common.loading") }) });
3182
- }
3183
- const health = status.relay.health || {};
3184
- const relayReachable = health.ok !== false;
3185
- const relayLevel = status.relay.local ? level(status.relay.alive) : relayReachable ? "ok" : "bad";
3186
- const keepaliveLevel = status.keepalive.active ? "ok" : status.keepalive.installed ? "warn" : "idle";
3187
- const nothingRunning = !status.host.alive && (status.relay.local ? !status.relay.alive : !relayReachable);
3188
- return /* @__PURE__ */ jsxs2(Panel, { title: t("overview.title"), children: [
3189
- /* @__PURE__ */ jsx3(Row, { label: t("overview.mode"), children: /* @__PURE__ */ jsx3(Text2, { children: t(`mode.${status.mode}`) }) }),
3190
- /* @__PURE__ */ jsx3(Row, { label: t("overview.relay"), children: /* @__PURE__ */ jsxs2(Box2, { children: [
3191
- /* @__PURE__ */ jsx3(StatusDot, { level: relayLevel }),
3192
- /* @__PURE__ */ jsxs2(Text2, { children: [
3193
- " ",
3194
- status.relay.local ? t("overview.relayLocal", { bind: status.relay.bind ?? "", port: status.relay.port }) : t("overview.relayRemote", { url: status.relay.remoteUrl ?? "" })
3195
- ] }),
3196
- status.relay.local && status.relay.pid ? /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: ` ${t("common.pid", { pid: status.relay.pid })}` }) : null
3197
- ] }) }),
3198
- /* @__PURE__ */ jsx3(Row, { label: t("overview.host"), children: /* @__PURE__ */ jsxs2(Box2, { children: [
3199
- /* @__PURE__ */ jsx3(StatusDot, { level: level(status.host.alive) }),
3200
- /* @__PURE__ */ jsx3(Text2, { children: ` ${status.host.alive ? t("common.running") : t("common.stopped")}` }),
3201
- status.host.pid ? /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: ` ${t("common.pid", { pid: status.host.pid })}` }) : null
3202
- ] }) }),
3203
- /* @__PURE__ */ jsx3(Row, { label: t("overview.socket"), children: /* @__PURE__ */ jsxs2(Box2, { children: [
3204
- /* @__PURE__ */ jsx3(StatusDot, { level: status.host.socketExists ? "ok" : "warn" }),
3205
- /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: ` ${status.host.socketPath}` }),
3206
- status.host.socketExists ? null : /* @__PURE__ */ jsx3(Text2, { color: theme.warn, children: ` ${t("overview.socketMissing")}` })
3207
- ] }) }),
3208
- /* @__PURE__ */ jsx3(Row, { label: t("overview.webUrl"), children: /* @__PURE__ */ jsx3(Text2, { color: theme.accent, children: status.publicUrl }) }),
3209
- /* @__PURE__ */ jsx3(Row, { label: t("overview.keepalive"), children: /* @__PURE__ */ jsxs2(Box2, { children: [
3210
- /* @__PURE__ */ jsx3(StatusDot, { level: keepaliveLevel }),
3211
- /* @__PURE__ */ jsxs2(Text2, { children: [
3212
- ` ${status.keepalive.manager} \u2014 `,
3213
- status.keepalive.active ? t("common.running") : status.keepalive.installed ? t("common.installed") : t("common.notInstalled")
3214
- ] })
3215
- ] }) }),
3216
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, flexDirection: "column", children: relayReachable ? /* @__PURE__ */ jsxs2(Fragment, { children: [
3217
- /* @__PURE__ */ jsx3(Row, { label: t("overview.hosts"), children: /* @__PURE__ */ jsx3(Text2, { children: health.hosts ?? 0 }) }),
3218
- /* @__PURE__ */ jsx3(Row, { label: t("overview.devices"), children: /* @__PURE__ */ jsx3(Text2, { children: health.clients ?? 0 }) }),
3219
- /* @__PURE__ */ jsx3(Row, { label: t("overview.uptime"), children: /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: formatUptime(health.uptimeSeconds, t) }) })
3220
- ] }) : /* @__PURE__ */ jsx3(Text2, { color: theme.warn, children: t("overview.unreachable", { message: health.message ?? "" }) }) }),
3221
- nothingRunning ? /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: t("overview.notStarted") }) }) : null,
3222
- config.relay.mode === "local" ? /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { color: theme.muted, children: t("mode.local.description") }) }) : null,
3223
- /* @__PURE__ */ jsx3(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3224
- ] });
3225
- }
3226
-
3227
- // tui/src/screens/Pair.tsx
3228
- import { useEffect as useEffect3, useState as useState2 } from "react";
3229
- import { Box as Box3, Text as Text3, useInput, useWindowSize } from "ink";
3230
- import QRCode from "qrcode";
3231
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3232
- var MIN_QR_COLUMNS = 44;
3233
- function PairScreen({ ctx }) {
3234
- const { t, run } = ctx;
3235
- const { columns } = useWindowSize();
3236
- const [pairing, setPairing] = useState2(null);
3237
- const [qr, setQr] = useState2(null);
3238
- const [remainingMs, setRemainingMs] = useState2(0);
3239
- const [selected, setSelected] = useState2("generate");
3240
- useEffect3(() => {
3241
- if (!pairing) return void 0;
3242
- const tick = () => setRemainingMs(Math.max(0, pairing.expiresAt - Date.now()));
3243
- tick();
3244
- const timer = setInterval(tick, 1e3);
3245
- return () => clearInterval(timer);
3246
- }, [pairing]);
3247
- useEffect3(() => {
3248
- if (!pairing || columns < MIN_QR_COLUMNS) {
3249
- setQr(null);
3250
- return;
3251
- }
3252
- let cancelled = false;
3253
- QRCode.toString(pairing.pairUrl, { type: "terminal", small: true, errorCorrectionLevel: "L" }).then((value) => {
3254
- if (!cancelled) setQr(value.replace(/\n$/, ""));
3255
- }).catch(() => {
3256
- if (!cancelled) setQr(null);
3257
- });
3258
- return () => {
3259
- cancelled = true;
3260
- };
3261
- }, [pairing, columns]);
3262
- const generate = () => run(async () => {
3263
- ctx.notify(t("pair.working"));
3264
- try {
3265
- const result = await (0, import_service.pair)();
3266
- const code = (0, import_service.extractPairingCode)(result);
3267
- setPairing({ ...result, code });
3268
- ctx.notify("", "info");
3269
- } catch (error) {
3270
- const message = error.message;
3271
- setPairing(null);
3272
- throw new Error(/host_offline|no Herdr host|did not register/i.test(message) ? t("pair.hostOffline") : t("pair.failed", { message }));
3273
- }
3274
- });
3275
- useInput((_input, key) => {
3276
- if (key.return) generate();
3277
- }, { isActive: ctx.editingId === null });
3278
- const expired = pairing !== null && remainingMs <= 0;
3279
- const minutes = Math.floor(remainingMs / 6e4);
3280
- const seconds = Math.floor(remainingMs % 6e4 / 1e3);
3281
- return /* @__PURE__ */ jsxs3(Panel, { title: t("pair.title"), children: [
3282
- /* @__PURE__ */ jsx4(
3283
- Menu,
3284
- {
3285
- items: [{ id: "generate", label: pairing ? t("pair.regenerate") : t("pair.generate") }],
3286
- selectedId: selected,
3287
- onChange: setSelected,
3288
- onSelect: generate
3289
- }
3290
- ),
3291
- pairing ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
3292
- /* @__PURE__ */ jsxs3(Box3, { children: [
3293
- /* @__PURE__ */ jsx4(Text3, { color: theme.muted, children: `${t("pair.code")} ` }),
3294
- /* @__PURE__ */ jsx4(Text3, { bold: true, color: expired ? theme.muted : theme.accent, children: pairing.code.split("").join(" ") })
3295
- ] }),
3296
- /* @__PURE__ */ jsxs3(Box3, { children: [
3297
- /* @__PURE__ */ jsx4(Text3, { color: theme.muted, children: `${t("pair.url")} ` }),
3298
- /* @__PURE__ */ jsx4(Text3, { children: pairing.pairUrl })
3299
- ] }),
3300
- /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text3, { color: expired ? theme.bad : theme.muted, children: expired ? t("pair.expired") : t("pair.expires", {
3301
- minutes: `${minutes}:${String(seconds).padStart(2, "0")}`,
3302
- time: new Date(pairing.expiresAt).toLocaleTimeString()
3303
- }) }) }),
3304
- !expired && qr ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
3305
- /* @__PURE__ */ jsx4(Text3, { children: qr }),
3306
- /* @__PURE__ */ jsx4(Text3, { color: theme.muted, children: t("pair.qrHint") })
3307
- ] }) : null,
3308
- !expired && !qr && columns < MIN_QR_COLUMNS ? /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text3, { color: theme.muted, children: t("pair.qrUnavailable") }) }) : null,
3309
- /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text3, { color: theme.muted, children: t("pair.instructions") }) })
3310
- ] }) : null,
3311
- /* @__PURE__ */ jsx4(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3312
- ] });
3313
- }
3314
-
3315
- // tui/src/screens/Services.tsx
3316
- import { useState as useState3 } from "react";
3317
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
3318
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3319
- var LOG_LINES = 8;
3320
- function Services({ ctx }) {
3321
- const { t, run, status } = ctx;
3322
- const [selected, setSelected] = useState3("start");
3323
- const items = [
3324
- { id: "start", label: t("services.start") },
3325
- { id: "stop", label: t("services.stop") },
3326
- { id: "restart", label: t("services.restart") }
3327
- ];
3328
- const activate = (id) => {
3329
- if (ctx.dirty && id !== "stop") {
3330
- ctx.notify(t("services.unsavedBlocked"), "error");
3331
- return;
3332
- }
3333
- return run(() => {
3334
- const config = ctx.config;
3335
- if (id === "start") {
3336
- const result2 = (0, import_lifecycle.startAll)(config);
3337
- ctx.notify(result2.managed ? t("services.managedNotice", { manager: result2.manager ?? "" }) : t("services.started"), "success");
3338
- return;
3339
- }
3340
- if (id === "stop") {
3341
- const result2 = (0, import_lifecycle.stopAll)(config);
3342
- ctx.notify(result2.managed ? t("services.managedNotice", { manager: result2.manager ?? "" }) : t("services.stopped"), "success");
3343
- return;
3344
- }
3345
- const result = (0, import_lifecycle.restartAll)(config);
3346
- ctx.notify(result.managed ? t("services.managedNotice", { manager: result.manager ?? "" }) : t("services.restarted"), "success");
3347
- });
3348
- };
3349
- useInput2((_input, key) => {
3350
- const index = items.findIndex((item) => item.id === selected);
3351
- if (key.upArrow) setSelected(items[(index - 1 + items.length) % items.length].id);
3352
- else if (key.downArrow) setSelected(items[(index + 1) % items.length].id);
3353
- else if (key.return) activate(selected);
3354
- }, { isActive: ctx.editingId === null });
3355
- const relayLog = (0, import_service.readLogTail)("relay", LOG_LINES);
3356
- const hostLog = (0, import_service.readLogTail)("host", LOG_LINES);
3357
- return /* @__PURE__ */ jsxs4(Panel, { title: t("services.title"), children: [
3358
- /* @__PURE__ */ jsx5(Menu, { items, selectedId: selected, onChange: setSelected, onSelect: activate }),
3359
- ctx.dirty ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { color: theme.warn, children: t("services.unsavedBlocked") }) }) : null,
3360
- status?.keepalive.installed ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { color: theme.muted, children: t("services.managedNotice", { manager: status.keepalive.manager }) }) }) : null,
3361
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
3362
- /* @__PURE__ */ jsx5(Text4, { color: theme.muted, children: t("services.logs") }),
3363
- /* @__PURE__ */ jsx5(LogBlock, { title: t("services.logRelay"), lines: relayLog, emptyText: t("services.logEmpty") }),
3364
- /* @__PURE__ */ jsx5(LogBlock, { title: t("services.logHost"), lines: hostLog, emptyText: t("services.logEmpty") })
3365
- ] }),
3366
- /* @__PURE__ */ jsx5(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3367
- ] });
3368
- }
3369
- function LogBlock({ title, lines, emptyText }) {
3370
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
3371
- /* @__PURE__ */ jsx5(Text4, { bold: true, color: theme.muted, children: title }),
3372
- lines.length === 0 ? /* @__PURE__ */ jsx5(Text4, { color: theme.muted, children: emptyText }) : lines.map((line, index) => (
3373
- // Log lines are positional and may repeat, so the index is the only
3374
- // stable identity available here.
3375
- // eslint-disable-next-line react/no-array-index-key
3376
- /* @__PURE__ */ jsx5(Text4, { color: theme.muted, wrap: "truncate-end", children: line }, index)
3377
- ))
3378
- ] });
3379
- }
3380
-
3381
- // tui/src/screens/Relay.tsx
3382
- import { useMemo as useMemo2, useState as useState5 } from "react";
3383
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
3384
-
3385
- // tui/src/components/TextField.tsx
3386
- import { useEffect as useEffect4, useState as useState4 } from "react";
3387
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
3388
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3389
- var CONTROL_CHARACTERS = /[\x00-\x1f\x7f]/;
3390
- function TextField({ value, placeholder, active, mask = false, onSubmit, onCancel }) {
3391
- const [buffer, setBuffer] = useState4(value);
3392
- useEffect4(() => {
3393
- if (active) setBuffer(value);
3394
- }, [active, value]);
3395
- useInput3((input, key) => {
3396
- if (key.return) {
3397
- onSubmit(buffer);
3398
- return;
3399
- }
3400
- if (key.escape) {
3401
- setBuffer(value);
3402
- onCancel();
3403
- return;
3404
- }
3405
- if (key.backspace || key.delete) {
3406
- setBuffer((current) => current.slice(0, -1));
3407
- return;
3408
- }
3409
- if (key.ctrl && input === "u") {
3410
- setBuffer("");
3411
- return;
3412
- }
3413
- if (input && !key.ctrl && !key.meta && !CONTROL_CHARACTERS.test(input)) {
3414
- setBuffer((current) => current + input);
3415
- }
3416
- }, { isActive: active });
3417
- const display = active ? buffer : value;
3418
- const shown = mask && display ? "\u2022".repeat(Math.min(display.length, 32)) : display;
3419
- if (!active) {
3420
- return shown ? /* @__PURE__ */ jsx6(Text5, { children: shown }) : /* @__PURE__ */ jsx6(Text5, { color: theme.muted, children: placeholder ?? "" });
3421
- }
3422
- return /* @__PURE__ */ jsxs5(Box5, { children: [
3423
- /* @__PURE__ */ jsx6(Text5, { color: theme.accent, children: shown }),
3424
- /* @__PURE__ */ jsx6(Text5, { color: theme.accent, inverse: true, children: " " })
3425
- ] });
3426
- }
3427
-
3428
- // tui/src/screens/Relay.tsx
3429
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3430
- function RelayScreen({ ctx }) {
3431
- const { t, draft, editingId } = ctx;
3432
- const [selected, setSelected] = useState5("mode");
3433
- const [revealTokens, setRevealTokens] = useState5(false);
3434
- const [showEnv, setShowEnv] = useState5(false);
3435
- const [password, setPassword] = useState5(ctx.runtime.relayPassword || "");
3436
- const official = (0, import_settings_model.isOfficialRelay)(draft);
3437
- const fields = useMemo2(
3438
- () => (0, import_settings_model.fieldsForMode)(draft.relay.mode).filter((field) => ["mode", "port", "lanHost", "remoteUrl", "publicUrl"].includes(field.id)),
3439
- [draft.relay.mode]
3440
- );
3441
- const entries = [
3442
- ...fields.map((field) => ({
3443
- id: field.id,
3444
- // The official relay's address is ours and its browser URL follows from
3445
- // it, so neither is offered for editing: an editable box holding a value
3446
- // that must not change is an invitation to break the setup.
3447
- kind: official && (field.id === "remoteUrl" || field.id === "publicUrl") ? "readonly" : "field",
3448
- fieldKind: field.kind,
3449
- label: t(field.labelKey)
3450
- })),
3451
- // The password only matters when talking to a relay somebody else started;
3452
- // a local relay is configured with our own token automatically, and the
3453
- // official one authenticates workstations by their own host token.
3454
- ...draft.relay.mode === "remote" && !official ? [{ id: "password", kind: "password", label: t("relay.password") }] : [],
3455
- // Saving needs a row of its own. Every field here only edits a draft, and
3456
- // the services restart from what is on disk — with `s` as the sole way to
3457
- // commit, a change could look applied, survive a restart, and never take
3458
- // effect. It is also the only way to save with the mouse.
3459
- { id: "save", kind: "action", label: t("common.save") },
3460
- { id: "test", kind: "action", label: t("relay.test") },
3461
- ...draft.relay.mode === "remote" && !official ? [
3462
- { id: "reveal", kind: "action", label: revealTokens ? t("relay.hidePassword") : t("relay.showPassword") },
3463
- { id: "env", kind: "action", label: t("relay.envSnippet") }
3464
- ] : [],
3465
- { id: "regenerate", kind: "action", label: t("relay.regenerate") }
3466
- ];
3467
- const addresses = useMemo2(() => listReachableAddresses({ includeLoopback: false }), []);
3468
- const applyFields = (updates) => {
3469
- let next = draft;
3470
- for (const [id, value] of updates) {
3471
- const result = (0, import_settings_model.setField)(next, id, value);
3472
- if (result.errorKey) {
3473
- ctx.notify(t(result.errorKey), "error");
3474
- return false;
3475
- }
3476
- next = result.draft;
3477
- }
3478
- ctx.updateDraft(next);
3479
- ctx.notify("", "info");
3480
- return true;
3481
- };
3482
- const applyField = (id, value) => applyFields([[id, value]]);
3483
- const save = () => {
3484
- try {
3485
- const result = (0, import_settings_model.saveDraft)(draft);
3486
- ctx.reloadConfig();
3487
- ctx.notify(t("common.saved", { path: result.path }), "success");
3488
- } catch (error) {
3489
- const problems = error.problems;
3490
- ctx.notify(problems ? problems.map((key) => t(key)).join(" ") : t("error.saveFailed", { message: error.message }), "error");
3491
- }
3492
- };
3493
- const activate = (id) => {
3494
- const entry = entries.find((candidate) => candidate.id === id);
3495
- if (!entry) return;
3496
- if (entry.kind === "readonly") {
3497
- ctx.notify(t("relay.officialFixed"), "info");
3498
- return;
3499
- }
3500
- if (entry.kind === "field" || entry.kind === "password") {
3501
- ctx.setEditing(id);
3502
- return;
3503
- }
3504
- if (id === "save") {
3505
- save();
3506
- return;
3507
- }
3508
- if (id === "reveal") {
3509
- setRevealTokens((current) => !current);
3510
- return;
3511
- }
3512
- if (id === "env") {
3513
- setShowEnv((current) => !current);
3514
- return;
3515
- }
3516
- if (id === "regenerate") {
3517
- ctx.run(() => {
3518
- (0, import_service.regenerateHostIdentity)();
3519
- ctx.reloadConfig();
3520
- ctx.notify(t("relay.regenerated"), "success");
3521
- });
3522
- return;
3523
- }
3524
- if (id === "test") {
3525
- ctx.run(async () => {
3526
- const result = await probeRelay(ctx.config);
3527
- ctx.notify(
3528
- result.ok ? t("relay.testOk", { version: result.version ?? "?", hosts: result.hosts ?? 0 }) : t("relay.testFailed", { message: result.message ?? "" }),
3529
- result.ok ? "success" : "error"
3530
- );
3531
- });
3532
- }
3533
- };
3534
- useInput4((input, key) => {
3535
- const index = entries.findIndex((entry) => entry.id === selected);
3536
- if (key.upArrow) setSelected(entries[(index - 1 + entries.length) % entries.length].id);
3537
- else if (key.downArrow) setSelected(entries[(index + 1) % entries.length].id);
3538
- else if (key.return) activate(selected);
3539
- else if (input === "s") save();
3540
- }, { isActive: editingId === null });
3541
- if (editingId === "mode") {
3542
- return /* @__PURE__ */ jsx7(Panel, { title: t("field.mode"), children: /* @__PURE__ */ jsx7(
3543
- ChoiceList,
3544
- {
3545
- options: [
3546
- ...["local", "lan"].map((mode) => ({
3547
- id: mode,
3548
- label: t(`mode.${mode}`),
3549
- description: t(`mode.${mode}.description`)
3550
- })),
3551
- { id: "official", label: t("mode.official"), description: t("mode.official.description") },
3552
- { id: "remote", label: t("mode.remote"), description: t("mode.remote.description") }
3553
- ],
3554
- current: (0, import_settings_model.selectedMode)(draft),
3555
- onPick: (choice) => {
3556
- if (!applyField("mode", choice)) return;
3557
- if (choice === "official") {
3558
- setPassword("");
3559
- (0, import_service.setRelayPassword)("");
3560
- }
3561
- ctx.setEditing(null);
3562
- },
3563
- onCancel: () => ctx.setEditing(null)
3564
- }
3565
- ) });
3566
- }
3567
- if (editingId === "lanHost") {
3568
- const options = addresses.map((address) => ({
3569
- id: address.address,
3570
- label: `${address.address} (${address.name}, ${t(addressKindKey(address.kind))})`
3571
- }));
3572
- return /* @__PURE__ */ jsx7(Panel, { title: t("relay.selectAddress"), children: options.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: theme.warn, children: t("relay.noAddresses") }) : /* @__PURE__ */ jsx7(
3573
- ChoiceList,
3574
- {
3575
- options,
3576
- current: draft.relay.lanHost || options[0].id,
3577
- onPick: (address) => {
3578
- applyField("lanHost", address);
3579
- ctx.setEditing(null);
3580
- },
3581
- onCancel: () => ctx.setEditing(null)
3582
- }
3583
- ) });
3584
- }
3585
- return /* @__PURE__ */ jsxs6(Panel, { title: t("relay.title"), children: [
3586
- draft.relay.mode !== "remote" ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 1, children: [
3587
- /* @__PURE__ */ jsx7(Row, { label: t("relay.listenAddress"), children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: `${(0, import_config.bindAddress)(draft)}:${draft.relay.port}` }) }),
3588
- /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.listenAddressHint") })
3589
- ] }) : null,
3590
- entries.map((entry) => entry.kind === "action" ? /* @__PURE__ */ jsx7(
3591
- Selectable,
3592
- {
3593
- selected: selected === entry.id,
3594
- onSelect: () => {
3595
- setSelected(entry.id);
3596
- activate(entry.id);
3597
- },
3598
- onHover: () => setSelected(entry.id),
3599
- children: entry.label
3600
- },
3601
- entry.id
3602
- ) : /* @__PURE__ */ jsx7(
3603
- FieldRow,
3604
- {
3605
- label: entry.label,
3606
- selected: selected === entry.id,
3607
- onSelect: () => activate(entry.id),
3608
- onHover: () => setSelected(entry.id),
3609
- children: entry.kind === "password" ? /* @__PURE__ */ jsx7(
3610
- TextField,
3611
- {
3612
- value: password,
3613
- placeholder: t("relay.passwordEmpty"),
3614
- active: editingId === "password",
3615
- mask: !revealTokens,
3616
- onSubmit: (value) => {
3617
- setPassword(value);
3618
- (0, import_service.setRelayPassword)(value);
3619
- ctx.setEditing(null);
3620
- ctx.notify(t("relay.passwordSaved"), "success");
3621
- },
3622
- onCancel: () => ctx.setEditing(null)
3623
- }
3624
- ) : entry.kind === "readonly" ? (
3625
- // Fixed by us, so it is printed rather than offered for editing.
3626
- /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: `${(0, import_settings_model.getField)(draft, entry.id) || placeholderText(ctx, entry.id)} (${t("relay.locked")})` })
3627
- ) : entry.fieldKind === "choice" ? (
3628
- // Choice fields are picked from a list, never typed, so they
3629
- // show the translated label rather than the stored id — and the
3630
- // official relay is named as itself, not as "self-hosted".
3631
- /* @__PURE__ */ jsx7(Text6, { children: t(`mode.${(0, import_settings_model.selectedMode)(draft)}`) })
3632
- ) : /* @__PURE__ */ jsx7(
3633
- TextField,
3634
- {
3635
- value: (0, import_settings_model.getField)(draft, entry.id),
3636
- placeholder: placeholderText(ctx, entry.id),
3637
- active: editingId === entry.id,
3638
- onSubmit: (value) => {
3639
- if (applyField(entry.id, value)) ctx.setEditing(null);
3640
- },
3641
- onCancel: () => ctx.setEditing(null)
3642
- }
3643
- )
3644
- },
3645
- entry.id
3646
- )),
3647
- ctx.dirty ? /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.warn, children: t("hint.unsavedChanges") }) }) : null,
3648
- draft.relay.mode === "remote" ? /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: official ? t("relay.officialHint") : t("relay.passwordHint") }) }) : null,
3649
- /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Row, { label: t("relay.identity"), children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: ctx.runtime.hostId }) }) }),
3650
- showEnv ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [
3651
- /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.envHint") }),
3652
- /* @__PURE__ */ jsx7(Text6, { children: relayStartCommand(ctx.config, password) })
3653
- ] }) : null,
3654
- official ? null : /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.docsHint") }) }),
3655
- /* @__PURE__ */ jsx7(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3656
- ] });
3657
- }
3658
- function placeholderText(ctx, id) {
3659
- const raw = (0, import_settings_model.getFieldPlaceholder)(ctx.draft, id);
3660
- return raw.startsWith("placeholder.") ? ctx.t(raw) : raw;
3661
- }
3662
- function addressKindKey(kind) {
3663
- return `relay.address${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
3664
- }
3665
- function ChoiceList({
3666
- options,
3667
- current,
3668
- onPick,
3669
- onCancel
3670
- }) {
3671
- const [cursor, setCursor] = useState5(() => {
3672
- const index = options.findIndex((option) => option.id === current);
3673
- return index >= 0 ? index : 0;
3674
- });
3675
- const active = options[Math.min(cursor, options.length - 1)];
3676
- useInput4((_input, key) => {
3677
- if (key.escape) {
3678
- onCancel();
3679
- return;
3680
- }
3681
- if (key.upArrow) setCursor((value) => (value - 1 + options.length) % options.length);
3682
- else if (key.downArrow) setCursor((value) => (value + 1) % options.length);
3683
- else if (key.return && active) onPick(active.id);
3684
- });
3685
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3686
- /* @__PURE__ */ jsx7(
3687
- Menu,
3688
- {
3689
- items: options.map((option) => ({ id: option.id, label: option.label })),
3690
- selectedId: active?.id ?? "",
3691
- onChange: (id) => setCursor(options.findIndex((option) => option.id === id)),
3692
- onSelect: onPick
3693
- }
3694
- ),
3695
- active?.description ? /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: active.description }) }) : null
3696
- ] });
3697
- }
3698
-
3699
- // tui/src/screens/Keepalive.tsx
3700
- import { useState as useState6 } from "react";
3701
- import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
3702
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3703
- var MANAGERS = ["auto", "systemd", "launchd", "supervisor", "none"];
3704
- function Keepalive({ ctx }) {
3705
- const { t, status, draft, editingId } = ctx;
3706
- const [selected, setSelected] = useState6("install");
3707
- const current = status?.keepalive;
3708
- const entries = [
3709
- { id: "install", label: t("keepalive.install") },
3710
- { id: "restart", label: t("keepalive.restart") },
3711
- { id: "uninstall", label: t("keepalive.uninstall") },
3712
- { id: "manager", label: `${t("keepalive.manager")}: ${draft.keepalive.manager}` },
3713
- ...process.platform === "linux" && current?.manager === "systemd" && !current?.linger ? [{ id: "linger", label: t("keepalive.enableLinger") }] : []
3714
- ];
3715
- const activate = (id) => {
3716
- if (id === "manager") {
3717
- ctx.setEditing("keepaliveManager");
3718
- return;
3719
- }
3720
- ctx.run(() => {
3721
- if (id === "install") {
3722
- const result = keepalive.install(ctx.config);
3723
- ctx.notify(t("keepalive.installed", { manager: result.manager }), "success");
3724
- return;
3725
- }
3726
- if (id === "uninstall") {
3727
- keepalive.uninstall(ctx.config);
3728
- ctx.notify(t("keepalive.uninstalled"), "success");
3729
- return;
3730
- }
3731
- if (id === "restart") {
3732
- keepalive.restart(ctx.config);
3733
- ctx.notify(t("keepalive.restarted"), "success");
3734
- return;
3735
- }
3736
- if (id === "linger") {
3737
- const result = keepalive.enableLinger();
3738
- ctx.notify(t("keepalive.lingerDone", { username: result.username }), "success");
3739
- }
3740
- });
3741
- };
3742
- useInput5((_input, key) => {
3743
- const index = entries.findIndex((entry) => entry.id === selected);
3744
- if (key.upArrow) setSelected(entries[(index - 1 + entries.length) % entries.length].id);
3745
- else if (key.downArrow) setSelected(entries[(index + 1) % entries.length].id);
3746
- else if (key.return) activate(selected);
3747
- }, { isActive: editingId === null });
3748
- if (editingId === "keepaliveManager") {
3749
- return /* @__PURE__ */ jsx8(Panel, { title: t("field.keepalive"), children: /* @__PURE__ */ jsx8(
3750
- ChoiceList,
3751
- {
3752
- options: MANAGERS.map((manager) => ({ id: manager, label: manager })),
3753
- current: draft.keepalive.manager,
3754
- onPick: (manager) => {
3755
- const result = (0, import_settings_model.setField)(draft, "keepaliveManager", manager);
3756
- if (result.errorKey) {
3757
- ctx.notify(t(result.errorKey), "error");
3758
- return;
3759
- }
3760
- ctx.updateDraft(result.draft);
3761
- try {
3762
- (0, import_settings_model.saveDraft)(result.draft);
3763
- ctx.reloadConfig();
3764
- } catch (error) {
3765
- ctx.notify(t("error.saveFailed", { message: error.message }), "error");
3766
- }
3767
- ctx.setEditing(null);
3768
- },
3769
- onCancel: () => ctx.setEditing(null)
3770
- }
3771
- ) });
3772
- }
3773
- return /* @__PURE__ */ jsxs7(Panel, { title: t("keepalive.title"), children: [
3774
- /* @__PURE__ */ jsx8(Row, { label: t("keepalive.manager"), children: /* @__PURE__ */ jsx8(Text7, { children: current?.manager ?? t("common.unknown") }) }),
3775
- /* @__PURE__ */ jsx8(Row, { label: t("keepalive.state"), children: /* @__PURE__ */ jsxs7(Box7, { children: [
3776
- /* @__PURE__ */ jsx8(StatusDot, { level: current?.active ? "ok" : current?.installed ? "warn" : "idle" }),
3777
- /* @__PURE__ */ jsxs7(Text7, { children: [
3778
- " ",
3779
- current?.active ? t("common.running") : current?.installed ? t("common.installed") : t("common.notInstalled")
3780
- ] })
3781
- ] }) }),
3782
- current?.unitPath ? /* @__PURE__ */ jsx8(Row, { label: t("keepalive.unitPath"), children: /* @__PURE__ */ jsx8(Text7, { color: theme.muted, children: current.unitPath }) }) : null,
3783
- /* @__PURE__ */ jsx8(Box7, { marginTop: 1, flexDirection: "column", children: entries.map((entry) => /* @__PURE__ */ jsx8(
3784
- Selectable,
3785
- {
3786
- selected: selected === entry.id,
3787
- onSelect: () => {
3788
- setSelected(entry.id);
3789
- activate(entry.id);
3790
- },
3791
- onHover: () => setSelected(entry.id),
3792
- children: entry.label
3793
- },
3794
- entry.id
3795
- )) }),
3796
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3797
- current?.manager === "systemd" ? /* @__PURE__ */ jsx8(Text7, { color: current.linger ? theme.muted : theme.warn, children: current.linger ? t("keepalive.lingerEnabled") : t("keepalive.lingerDisabled") }) : null,
3798
- current?.manager === "supervisor" ? /* @__PURE__ */ jsx8(Text7, { color: theme.warn, children: t("keepalive.fallbackNote") }) : null,
3799
- /* @__PURE__ */ jsx8(Text7, { color: theme.muted, children: t("keepalive.logsHint", { command: status?.logsHint ?? "" }) })
3800
- ] }),
3801
- /* @__PURE__ */ jsx8(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3802
- ] });
3803
- }
3804
-
3805
- // tui/src/screens/Herdr.tsx
3806
- import { useEffect as useEffect5, useState as useState7 } from "react";
3807
- import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
3808
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3809
- function HerdrScreen({ ctx }) {
3810
- const { t, draft, editingId } = ctx;
3811
- const [selected, setSelected] = useState7("socketPath");
3812
- const [installed, setInstalled] = useState7(null);
3813
- useEffect5(() => {
3814
- setInstalled((0, import_herdr_command.herdrVersion)());
3815
- }, []);
3816
- const entries = [
3817
- { id: "socketPath", kind: "field", label: t("herdr.socketPath") },
3818
- { id: "herdrArgs", kind: "field", label: t("herdr.args") },
3819
- { id: "herdrAutoStart", kind: "toggle", label: t("field.herdrAutoStart") },
3820
- { id: "save", kind: "action", label: t("common.save") }
3821
- ];
3822
- const applyField = (id, value) => {
3823
- const result = (0, import_settings_model.setField)(draft, id, value);
3824
- if (result.errorKey) {
3825
- ctx.notify(t(result.errorKey), "error");
3826
- return false;
3827
- }
3828
- ctx.updateDraft(result.draft);
3829
- return true;
3830
- };
3831
- const save = () => {
3832
- try {
3833
- const result = (0, import_settings_model.saveDraft)(draft);
3834
- ctx.reloadConfig();
3835
- ctx.notify(t("common.saved", { path: result.path }), "success");
3836
- } catch (error) {
3837
- ctx.notify(t("error.saveFailed", { message: error.message }), "error");
3838
- }
3839
- };
3840
- const activate = (id) => {
3841
- if (id === "socketPath" || id === "herdrArgs") {
3842
- ctx.setEditing(id);
3843
- return;
3844
- }
3845
- if (id === "herdrAutoStart") {
3846
- applyField(id, draft.herdr.autoStart ? "off" : "on");
3847
- return;
3848
- }
3849
- if (id === "save") save();
3850
- };
3851
- useInput6((input, key) => {
3852
- const index = entries.findIndex((entry) => entry.id === selected);
3853
- const safeIndex = index >= 0 ? index : 0;
3854
- if (key.upArrow) setSelected(entries[(safeIndex - 1 + entries.length) % entries.length].id);
3855
- else if (key.downArrow) setSelected(entries[(safeIndex + 1) % entries.length].id);
3856
- else if (key.return) activate(selected);
3857
- else if (input === "s") save();
3858
- }, { isActive: editingId === null });
3859
- return /* @__PURE__ */ jsxs8(Panel, { title: t("herdr.title"), children: [
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(
3874
- FieldRow,
3875
- {
3876
- label: entry.label,
3877
- selected: selected === entry.id,
3878
- onSelect: () => activate(entry.id),
3879
- onHover: () => setSelected(entry.id),
3880
- children: /* @__PURE__ */ jsx9(
3881
- TextField,
3882
- {
3883
- value: (0, import_settings_model.getField)(draft, entry.id),
3884
- placeholder: ctx.t((0, import_settings_model.getFieldPlaceholder)(draft, entry.id)),
3885
- active: editingId === entry.id,
3886
- onSubmit: (value) => {
3887
- if (applyField(entry.id, value)) ctx.setEditing(null);
3888
- },
3889
- onCancel: () => ctx.setEditing(null)
3890
- }
3891
- )
3892
- },
3893
- entry.id
3894
- ) : /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(
3895
- Selectable,
3896
- {
3897
- selected: selected === entry.id,
3898
- onSelect: () => {
3899
- setSelected(entry.id);
3900
- activate(entry.id);
3901
- },
3902
- onHover: () => setSelected(entry.id),
3903
- children: entry.label
3904
- }
3905
- ) }, entry.id)),
3906
- selected === "herdrAutoStart" ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: theme.muted, children: t("herdr.autoStartHint") }) }) : null,
3907
- ctx.dirty ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("hint.unsavedChanges") }) }) : null,
3908
- /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
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}` })
3912
- ] }) }) : 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
3915
- ] }),
3916
- /* @__PURE__ */ jsx9(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3917
- ] });
3918
- }
3919
-
3920
- // tui/src/screens/About.tsx
3921
- import { useEffect as useEffect6, useState as useState8 } from "react";
3922
- import { Box as Box9, Text as Text9, useInput as useInput7 } from "ink";
3923
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3924
- var registryHost = (registry) => registry.replace(/^https?:\/\//, "");
3925
- function About({ ctx }) {
3926
- const { t, draft } = ctx;
3927
- const [selected, setSelected] = useState8("auto");
3928
- const [update, setUpdate] = useState8({ phase: "idle" });
3929
- const [sourceNote, setSourceNote] = useState8(null);
3930
- const checkRef = useState8(() => ({ current: { registry: "", sources: [] } }))[0];
3931
- const detected = detectLocale({ preference: "auto" });
3932
- const languageOptions = [
3933
- { id: "auto", label: t("about.languageAuto", { detected }) },
3934
- { id: "zh", label: t("about.languageZh") },
3935
- { id: "en", label: t("about.languageEn") }
3936
- ];
3937
- const updatable = (0, import_updater.canSelfUpdate)();
3938
- const updateLabel = (() => {
3939
- switch (update.phase) {
3940
- case "checking":
3941
- return t("update.checking");
3942
- case "current":
3943
- return t("update.upToDate", { version: update.latest });
3944
- case "available":
3945
- return t("update.available", { version: update.latest });
3946
- case "updating":
3947
- return update.attempt > 1 ? t("update.updatingRetry", { version: update.latest, attempt: update.attempt }) : t("update.updating", { version: update.latest });
3948
- case "done":
3949
- return t("update.done", { version: update.latest });
3950
- case "error":
3951
- return t(update.messageKey, update.params);
3952
- default:
3953
- return t("update.check");
3954
- }
3955
- })();
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]);
3969
- const runCheck = async () => {
3970
- setUpdate({ phase: "checking" });
3971
- setSourceNote(null);
3972
- const result = await (0, import_updater.checkForUpdate)();
3973
- if (!result.ok) {
3974
- setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorNetwork" });
3975
- if (result.message) ctx.notify(t("update.errorNetworkDetail", { message: result.message }), "error");
3976
- return;
3977
- }
3978
- ctx.setUpdateCheck(result);
3979
- applyCheck(result);
3980
- };
3981
- const runUpdate = async (latest) => {
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
- });
3989
- if (!result.ok) {
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");
3993
- return;
3994
- }
3995
- setUpdate({ phase: "done", latest });
3996
- setSourceNote(null);
3997
- ctx.setUpdateCheck(null);
3998
- ctx.notify(t("update.restartHint"), "success");
3999
- };
4000
- const activateUpdate = () => {
4001
- if (update.phase === "checking" || update.phase === "updating") return;
4002
- if (update.phase === "available") {
4003
- if (!updatable) {
4004
- setUpdate({ phase: "error", messageKey: `update.cannot.${(0, import_updater.installKind)()}` });
4005
- return;
4006
- }
4007
- void runUpdate(update.latest);
4008
- return;
4009
- }
4010
- void runCheck();
4011
- };
4012
- const choose = (language) => {
4013
- const result = (0, import_settings_model.setField)(draft, "language", language);
4014
- if (result.errorKey) {
4015
- ctx.notify(t(result.errorKey), "error");
4016
- return;
4017
- }
4018
- ctx.updateDraft(result.draft);
4019
- try {
4020
- (0, import_settings_model.saveDraft)(result.draft);
4021
- ctx.reloadConfig();
4022
- ctx.notify(t("common.saved", { path: (0, import_config.configPath)() }), "success");
4023
- } catch (error) {
4024
- ctx.notify(t("error.saveFailed", { message: error.message }), "error");
4025
- }
4026
- };
4027
- const activate = (id) => {
4028
- if (id === "update") {
4029
- activateUpdate();
4030
- return;
4031
- }
4032
- choose(id);
4033
- };
4034
- useInput7((_input, key) => {
4035
- const index = options.findIndex((option) => option.id === selected);
4036
- if (key.upArrow) setSelected(options[(index - 1 + options.length) % options.length].id);
4037
- else if (key.downArrow) setSelected(options[(index + 1) % options.length].id);
4038
- else if (key.return) activate(selected);
4039
- }, { isActive: ctx.editingId === null });
4040
- return /* @__PURE__ */ jsxs9(Panel, { title: t("about.title"), children: [
4041
- /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.language") }),
4042
- /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: languageOptions.map((option) => /* @__PURE__ */ jsx10(
4043
- Selectable,
4044
- {
4045
- selected: selected === option.id,
4046
- onSelect: () => {
4047
- setSelected(option.id);
4048
- choose(option.id);
4049
- },
4050
- onHover: () => setSelected(option.id),
4051
- children: `${option.label}${draft.ui.language === option.id ? " \u2713" : ""}`
4052
- },
4053
- option.id
4054
- )) }),
4055
- /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("update.title") }),
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" }) }),
4072
- /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
4073
- /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
4074
- /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
4075
- /* @__PURE__ */ jsx10(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4076
- ] });
4077
- }
4078
-
4079
- // tui/src/screens/Wizard.tsx
4080
- import { useMemo as useMemo3, useState as useState9 } from "react";
4081
- import { Box as Box10, Text as Text10, useInput as useInput8 } from "ink";
4082
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
4083
- function stepsFor(mode, remoteUrl) {
4084
- const steps = ["language", "access"];
4085
- if (mode === "lan") steps.push("address");
4086
- if (mode === "remote" && remoteUrl !== import_config.OFFICIAL_RELAY_URL) steps.push("relayUrl", "password");
4087
- steps.push("finish");
4088
- return steps;
4089
- }
4090
- function Wizard({ ctx, onDone }) {
4091
- const { t, draft, editingId } = ctx;
4092
- const [step, setStep] = useState9("language");
4093
- const [password, setPassword] = useState9(ctx.runtime.relayPassword || "");
4094
- const [startNow, setStartNow] = useState9(true);
4095
- const [installKeepalive, setInstallKeepalive] = useState9(true);
4096
- const [finishSelection, setFinishSelection] = useState9("startNow");
4097
- const addresses = useMemo3(
4098
- () => listReachableAddresses({ includeLoopback: false }),
4099
- []
4100
- );
4101
- const steps = useMemo3(
4102
- () => stepsFor(draft.relay.mode, draft.relay.remoteUrl),
4103
- [draft.relay.mode, draft.relay.remoteUrl]
4104
- );
4105
- const stepIndex = Math.max(0, steps.indexOf(step));
4106
- const applyAll = (updates) => {
4107
- let next = draft;
4108
- for (const [id, value] of updates) {
4109
- const result = (0, import_settings_model.setField)(next, id, value);
4110
- if (result.errorKey) {
4111
- ctx.notify(t(result.errorKey), "error");
4112
- return false;
4113
- }
4114
- next = result.draft;
4115
- }
4116
- ctx.updateDraft(next);
4117
- ctx.notify("", "info");
4118
- return true;
4119
- };
4120
- const apply = (id, value) => applyAll([[id, value]]);
4121
- const advance = (from, order = steps) => {
4122
- const index = order.indexOf(from);
4123
- setStep(order[Math.min(order.length - 1, index + 1)]);
4124
- };
4125
- const back = () => {
4126
- const index = steps.indexOf(step);
4127
- if (index > 0) setStep(steps[index - 1]);
4128
- };
4129
- const finish = () => {
4130
- ctx.run(async () => {
4131
- if (draft.relay.mode === "remote") {
4132
- (0, import_service.setRelayPassword)(password);
4133
- }
4134
- (0, import_settings_model.saveDraft)(draft);
4135
- ctx.reloadConfig();
4136
- const finalConfig = (0, import_config.loadConfig)();
4137
- let keepaliveInstalled = false;
4138
- if (installKeepalive) {
4139
- try {
4140
- keepalive.install(finalConfig);
4141
- keepaliveInstalled = true;
4142
- } catch (error) {
4143
- ctx.notify(t("keepalive.failed", { message: error.message }), "error");
4144
- }
4145
- }
4146
- if (startNow && !keepaliveInstalled) (0, import_lifecycle.startAll)(finalConfig);
4147
- onDone();
4148
- });
4149
- };
4150
- const header = /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginBottom: 1, children: [
4151
- /* @__PURE__ */ jsx11(Text10, { bold: true, children: t("wizard.title") }),
4152
- /* @__PURE__ */ jsx11(Text10, { color: theme.muted, children: t("wizard.step", { current: stepIndex + 1, total: steps.length }) })
4153
- ] });
4154
- if (step === "language") {
4155
- return /* @__PURE__ */ jsxs10(Panel, { title: t("wizard.languageTitle"), children: [
4156
- header,
4157
- /* @__PURE__ */ jsx11(
4158
- ChoiceList,
4159
- {
4160
- options: [
4161
- { id: "auto", label: t("about.languageAuto", { detected: ctx.locale }) },
4162
- { id: "zh", label: t("about.languageZh") },
4163
- { id: "en", label: t("about.languageEn") }
4164
- ],
4165
- current: draft.ui.language,
4166
- onPick: (language) => {
4167
- if (apply("language", language)) advance("language");
4168
- },
4169
- onCancel: () => {
4170
- }
4171
- }
4172
- ),
4173
- /* @__PURE__ */ jsx11(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4174
- ] });
4175
- }
4176
- if (step === "access") {
4177
- return /* @__PURE__ */ jsxs10(Panel, { title: t("wizard.accessTitle"), children: [
4178
- header,
4179
- /* @__PURE__ */ jsx11(
4180
- ChoiceList,
4181
- {
4182
- options: [
4183
- ...["local", "lan"].map((mode) => ({
4184
- id: mode,
4185
- label: t(`mode.${mode}`),
4186
- description: t(`mode.${mode}.description`)
4187
- })),
4188
- // Offered ahead of the self-hosted option because it is the one
4189
- // that needs no server. It is still a separate, explicit choice:
4190
- // it routes the terminal through a relay this user does not own.
4191
- {
4192
- id: "official",
4193
- label: t("mode.official"),
4194
- description: t("mode.official.description")
4195
- },
4196
- {
4197
- id: "remote",
4198
- label: t("mode.remote"),
4199
- description: t("mode.remote.description")
4200
- }
4201
- ],
4202
- current: (0, import_settings_model.selectedMode)(draft),
4203
- onPick: (choice) => {
4204
- if (choice === "official") {
4205
- if (!apply("mode", "official")) return;
4206
- (0, import_service.setRelayPassword)("");
4207
- advance("access", stepsFor("remote", import_config.OFFICIAL_RELAY_URL));
4208
- return;
4209
- }
4210
- if (apply("mode", choice)) {
4211
- advance("access", stepsFor(choice, choice === "remote" ? "" : draft.relay.remoteUrl));
4212
- }
4213
- },
4214
- onCancel: back
4215
- }
4216
- ),
4217
- /* @__PURE__ */ jsx11(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text10, { color: theme.muted, children: t("wizard.accessHint") }) }),
4218
- /* @__PURE__ */ jsx11(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4219
- ] });
4220
- }
4221
- if (step === "address") {
4222
- return /* @__PURE__ */ jsxs10(Panel, { title: t("wizard.addressTitle"), children: [
4223
- header,
4224
- addresses.length === 0 ? /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
4225
- /* @__PURE__ */ jsx11(Text10, { color: theme.warn, children: t("relay.noAddresses") }),
4226
- /* @__PURE__ */ jsx11(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx11(Selectable, { selected: true, onSelect: () => advance("address"), children: t("wizard.finish") }) }),
4227
- /* @__PURE__ */ jsx11(SkipKey, { onSkip: () => advance("address"), onBack: back })
4228
- ] }) : /* @__PURE__ */ jsx11(
4229
- ChoiceList,
4230
- {
4231
- options: addresses.map((address) => ({
4232
- id: address.address,
4233
- label: `${address.address} (${address.name}, ${address.kind})`
4234
- })),
4235
- current: draft.relay.lanHost || addresses[0].address,
4236
- onPick: (address) => {
4237
- if (apply("lanHost", address)) advance("address");
4238
- },
4239
- onCancel: back
4240
- }
4241
- ),
4242
- /* @__PURE__ */ jsx11(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4243
- ] });
4244
- }
4245
- if (step === "relayUrl") {
4246
- return /* @__PURE__ */ jsxs10(Panel, { title: t("wizard.relayTitle"), children: [
4247
- header,
4248
- /* @__PURE__ */ jsx11(FieldRow, { label: t("wizard.relayUrlLabel"), selected: true, onSelect: () => {
4249
- }, children: /* @__PURE__ */ jsx11(
4250
- TextField,
4251
- {
4252
- value: draft.relay.remoteUrl,
4253
- placeholder: "wss://relay.example.com",
4254
- active: true,
4255
- onSubmit: (value) => {
4256
- if (!value) {
4257
- ctx.notify(t("error.remoteUrlRequired"), "error");
4258
- return;
4259
- }
4260
- if (apply("remoteUrl", value)) advance("relayUrl");
4261
- },
4262
- onCancel: back
4263
- }
4264
- ) }),
4265
- /* @__PURE__ */ jsx11(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text10, { color: theme.muted, children: t("wizard.relayHint") }) }),
4266
- /* @__PURE__ */ jsx11(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4267
- ] });
4268
- }
4269
- if (step === "password") {
4270
- return /* @__PURE__ */ jsxs10(Panel, { title: t("wizard.passwordTitle"), children: [
4271
- header,
4272
- /* @__PURE__ */ jsx11(FieldRow, { label: t("relay.password"), selected: true, onSelect: () => {
4273
- }, children: /* @__PURE__ */ jsx11(
4274
- TextField,
4275
- {
4276
- value: password,
4277
- placeholder: t("relay.passwordEmpty"),
4278
- active: true,
4279
- mask: true,
4280
- onSubmit: (value) => {
4281
- setPassword(value);
4282
- advance("password");
4283
- },
4284
- onCancel: back
4285
- }
4286
- ) }),
4287
- /* @__PURE__ */ jsx11(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text10, { color: theme.muted, children: t("wizard.passwordHint") }) }),
4288
- /* @__PURE__ */ jsx11(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4289
- ] });
4290
- }
4291
- const finishItems = [
4292
- { id: "startNow", label: `${t("wizard.startNow")} [${startNow ? "\xD7" : " "}]` },
4293
- { id: "installKeepalive", label: `${t("wizard.installKeepalive")} [${installKeepalive ? "\xD7" : " "}]` },
4294
- { id: "finish", label: t("wizard.finish") }
4295
- ];
4296
- const activateFinish = (id) => {
4297
- if (id === "startNow") {
4298
- setStartNow((value) => !value);
4299
- return;
4300
- }
4301
- if (id === "installKeepalive") {
4302
- setInstallKeepalive((value) => !value);
4303
- return;
4304
- }
4305
- finish();
4306
- };
4307
- return /* @__PURE__ */ jsxs10(Panel, { title: t("wizard.finishTitle"), children: [
4308
- header,
4309
- /* @__PURE__ */ jsx11(
4310
- FinishKeys,
4311
- {
4312
- items: finishItems.map((item) => item.id),
4313
- selected: finishSelection,
4314
- onSelect: setFinishSelection,
4315
- onActivate: activateFinish,
4316
- onBack: back
4317
- }
4318
- ),
4319
- finishItems.map((item) => /* @__PURE__ */ jsx11(
4320
- Selectable,
4321
- {
4322
- selected: finishSelection === item.id,
4323
- onSelect: () => {
4324
- setFinishSelection(item.id);
4325
- activateFinish(item.id);
4326
- },
4327
- onHover: () => setFinishSelection(item.id),
4328
- children: item.label
4329
- },
4330
- item.id
4331
- )),
4332
- /* @__PURE__ */ jsx11(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text10, { color: theme.muted, children: t("wizard.finishHint", { path: (0, import_config.configPath)() }) }) }),
4333
- /* @__PURE__ */ jsx11(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
4334
- ] });
4335
- }
4336
- function FinishKeys({
4337
- items,
4338
- selected,
4339
- onSelect,
4340
- onActivate,
4341
- onBack
4342
- }) {
4343
- useInput8((_input, key) => {
4344
- const index = Math.max(0, items.indexOf(selected));
4345
- if (key.escape) onBack();
4346
- else if (key.upArrow) onSelect(items[(index - 1 + items.length) % items.length]);
4347
- else if (key.downArrow) onSelect(items[(index + 1) % items.length]);
4348
- else if (key.return) onActivate(selected);
4349
- });
4350
- return null;
4351
- }
4352
- function SkipKey({ onSkip, onBack }) {
4353
- useInput8((_input, key) => {
4354
- if (key.return) onSkip();
4355
- else if (key.escape) onBack();
4356
- });
4357
- return null;
4358
- }
4359
-
4360
- // tui/src/App.tsx
4361
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
4362
- var STATUS_POLL_MS = 3e3;
4363
- var TABS = [
4364
- { id: "overview", labelKey: "nav.overview" },
4365
- { id: "pair", labelKey: "nav.pair" },
4366
- { id: "services", labelKey: "nav.services" },
4367
- { id: "relay", labelKey: "nav.relay" },
4368
- { id: "keepalive", labelKey: "nav.keepalive" },
4369
- { id: "herdr", labelKey: "nav.herdr" },
4370
- { id: "about", labelKey: "nav.about" }
4371
- ];
4372
- function Tab({ label, active, onSelect }) {
4373
- const ref = useRef3(null);
4374
- const hovered = useMouseTarget(ref, { onClick: onSelect });
4375
- return /* @__PURE__ */ jsx12(Box11, { ref, marginRight: 2, children: /* @__PURE__ */ jsx12(
4376
- Text11,
4377
- {
4378
- color: active || hovered ? theme.accent : theme.muted,
4379
- bold: active,
4380
- underline: active || hovered,
4381
- children: label
4382
- }
4383
- ) });
4384
- }
4385
- function Footer({ hints }) {
4386
- return /* @__PURE__ */ jsx12(Box11, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx12(Text11, { color: theme.muted, children: hints.join(" \xB7 ") }) });
4387
- }
4388
- function App({
4389
- initialLanguage,
4390
- needsWizard,
4391
- updateChecker = (0, import_updater.updateChecksEnabled)() ? import_updater.checkForUpdate : null
4392
- }) {
4393
- const { exit } = useApp();
4394
- const mouse = useMouse();
4395
- const [config, setConfig] = useState10(() => (0, import_config.loadConfig)());
4396
- const [draft, setDraft] = useState10(() => (0, import_settings_model.createDraft)());
4397
- const [runtime, setRuntime] = useState10(() => (0, import_service.ensureRuntime)());
4398
- const [status, setStatus] = useState10(null);
4399
- const [tab, setTab] = useState10("overview");
4400
- const [message, setMessage] = useState10(null);
4401
- const [busy, setBusy] = useState10(false);
4402
- const [editingId, setEditingId] = useState10(null);
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]);
4416
- const locale = useMemo4(
4417
- () => detectLocale({ preference: initialLanguage ?? draft.ui.language }),
4418
- [initialLanguage, draft.ui.language]
4419
- );
4420
- const t = useMemo4(() => (0, import_i18n.createTranslator)(locale), [locale]);
4421
- const dirty = useMemo4(() => JSON.stringify(config) !== JSON.stringify(draft), [config, draft]);
4422
- const restartPending = useMemo4(() => dirty && (0, import_settings_model.requiresRestart)(config, draft), [config, draft, dirty]);
4423
- const notify = useCallback2((text, level2 = "info") => {
4424
- setMessage({ text, level: level2 });
4425
- }, []);
4426
- const refresh = useCallback2(() => {
4427
- (0, import_lifecycle.fullStatus)((0, import_config.loadConfig)()).then((next) => setStatus(next)).catch((error) => notify(error.message, "error"));
4428
- }, [notify]);
4429
- const reloadConfig = useCallback2(() => {
4430
- const next = (0, import_config.loadConfig)();
4431
- setConfig(next);
4432
- setDraft((0, import_settings_model.createDraft)(next));
4433
- setRuntime((0, import_service.ensureRuntime)());
4434
- }, []);
4435
- const run = useCallback2((task) => {
4436
- setBusy(true);
4437
- Promise.resolve().then(task).catch((error) => notify(error.message, "error")).finally(() => {
4438
- setBusy(false);
4439
- refresh();
4440
- });
4441
- }, [notify, refresh]);
4442
- useEffect7(() => {
4443
- if (!wizardDone) return void 0;
4444
- refresh();
4445
- const timer = setInterval(() => {
4446
- if (!editingId) refresh();
4447
- }, STATUS_POLL_MS);
4448
- return () => clearInterval(timer);
4449
- }, [wizardDone, refresh, editingId]);
4450
- const mouseChoice = useRef3(null);
4451
- useEffect7(() => {
4452
- if (mouseChoice.current !== null) return;
4453
- if (mouse.supported && !mouse.enabled) mouse.enable();
4454
- }, [mouse.supported, mouse.enabled, mouse]);
4455
- const context = {
4456
- t,
4457
- locale,
4458
- config,
4459
- draft,
4460
- status,
4461
- runtime,
4462
- busy,
4463
- dirty,
4464
- editingId,
4465
- updateDraft: setDraft,
4466
- reloadConfig,
4467
- setEditing: setEditingId,
4468
- notify,
4469
- run,
4470
- refresh,
4471
- message,
4472
- updateCheck,
4473
- setUpdateCheck
4474
- };
4475
- useInput9((input, key) => {
4476
- if (key.ctrl && input === "c") {
4477
- exit();
4478
- return;
4479
- }
4480
- if (input === "q") {
4481
- exit();
4482
- return;
4483
- }
4484
- if (input === "r") {
4485
- setMessage(null);
4486
- refresh();
4487
- return;
4488
- }
4489
- if (input === "m") {
4490
- if (!mouse.supported) {
4491
- notify(t("hint.mouseUnsupported"), "info");
4492
- return;
4493
- }
4494
- mouseChoice.current = !mouse.enabled;
4495
- if (mouse.enabled) mouse.disable();
4496
- else mouse.enable();
4497
- return;
4498
- }
4499
- const index = TABS.findIndex((entry) => entry.id === tab);
4500
- if (key.rightArrow || key.tab && !key.shift) {
4501
- setTab(TABS[(index + 1) % TABS.length].id);
4502
- return;
4503
- }
4504
- if (key.leftArrow || key.tab && key.shift) {
4505
- setTab(TABS[(index - 1 + TABS.length) % TABS.length].id);
4506
- return;
4507
- }
4508
- const digit = Number.parseInt(input, 10);
4509
- if (Number.isInteger(digit) && digit >= 1 && digit <= TABS.length) setTab(TABS[digit - 1].id);
4510
- }, { isActive: wizardDone && editingId === null });
4511
- if (!wizardDone) {
4512
- return /* @__PURE__ */ jsx12(
4513
- Wizard,
4514
- {
4515
- ctx: context,
4516
- onDone: () => {
4517
- reloadConfig();
4518
- setWizardDone(true);
4519
- refresh();
4520
- }
4521
- }
4522
- );
4523
- }
4524
- const hints = [
4525
- t("hint.navigate"),
4526
- t("hint.select"),
4527
- t("hint.tabs"),
4528
- dirty ? t("hint.save") : null,
4529
- mouse.supported ? mouse.enabled ? t("hint.mouseOn") : t("hint.mouseOff") : t("hint.mouseUnsupported"),
4530
- t("hint.quit")
4531
- ].filter(Boolean);
4532
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
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
4539
- ] }),
4540
- /* @__PURE__ */ jsx12(Box11, { marginBottom: 1, children: TABS.map((entry, entryIndex) => /* @__PURE__ */ jsx12(
4541
- Tab,
4542
- {
4543
- label: `${entryIndex + 1} ${t(entry.labelKey)}`,
4544
- active: entry.id === tab,
4545
- onSelect: () => setTab(entry.id)
4546
- },
4547
- entry.id
4548
- )) }),
4549
- tab === "overview" ? /* @__PURE__ */ jsx12(Overview, { ctx: context }) : null,
4550
- tab === "pair" ? /* @__PURE__ */ jsx12(PairScreen, { ctx: context }) : null,
4551
- tab === "services" ? /* @__PURE__ */ jsx12(Services, { ctx: context }) : null,
4552
- tab === "relay" ? /* @__PURE__ */ jsx12(RelayScreen, { ctx: context }) : null,
4553
- tab === "keepalive" ? /* @__PURE__ */ jsx12(Keepalive, { ctx: context }) : null,
4554
- tab === "herdr" ? /* @__PURE__ */ jsx12(HerdrScreen, { ctx: context }) : null,
4555
- tab === "about" ? /* @__PURE__ */ jsx12(About, { ctx: context }) : null,
4556
- restartPending ? /* @__PURE__ */ jsx12(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color: theme.warn, children: t("hint.restartRequired") }) }) : null,
4557
- /* @__PURE__ */ jsx12(Footer, { hints })
4558
- ] });
4559
- }
4560
-
4561
- // tui/src/terminal.ts
4562
- var ESC2 = String.fromCharCode(27);
4563
- var ENTER_ALT_SCREEN = `${ESC2}[?1049h${ESC2}[H`;
4564
- var EXIT_ALT_SCREEN = `${ESC2}[?1049l`;
4565
- var SHOW_CURSOR = `${ESC2}[?25h`;
4566
- function enterFullScreen(output = process.stdout) {
4567
- try {
4568
- output.write(ENTER_ALT_SCREEN);
4569
- } catch {
4570
- }
4571
- }
4572
- function restoreTerminal(output = process.stdout) {
4573
- try {
4574
- output.write(`${DISABLE_MOUSE}${SHOW_CURSOR}${EXIT_ALT_SCREEN}`);
4575
- } catch {
4576
- }
4577
- }
4578
-
4579
- // tui/src/index.tsx
4580
- import { jsx as jsx13 } from "react/jsx-runtime";
4581
- async function startTui({ language = null } = {}) {
4582
- const needsWizard = !(0, import_config.configExists)();
4583
- const mouse = createMouseSource(process.stdin, process.stdout);
4584
- enterFullScreen();
4585
- const instance = render(
4586
- /* @__PURE__ */ jsx13(MouseProvider, { source: mouse, children: /* @__PURE__ */ jsx13(App, { initialLanguage: language, needsWizard }) }),
4587
- { stdin: mouse.stdin, exitOnCtrlC: false }
4588
- );
4589
- const cleanup = () => {
4590
- mouse.dispose();
4591
- restoreTerminal();
4592
- };
4593
- const onSignal = () => {
4594
- instance.unmount();
4595
- cleanup();
4596
- process.exit(0);
4597
- };
4598
- process.once("SIGINT", onSignal);
4599
- process.once("SIGTERM", onSignal);
4600
- process.once("exit", cleanup);
4601
- try {
4602
- await instance.waitUntilExit();
4603
- } finally {
4604
- cleanup();
4605
- process.off("SIGINT", onSignal);
4606
- process.off("SIGTERM", onSignal);
4607
- }
4608
- }
4609
- export {
4610
- App,
4611
- Wizard,
4612
- startTui
4613
- };