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