herdr-remote 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/bin/herdr-remote.js +3 -221
  2. package/dist/agent-status.js +130 -0
  3. package/dist/binary-plist.js +157 -0
  4. package/dist/cli.js +255 -0
  5. package/dist/config.js +270 -0
  6. package/dist/connector/agent-watch.js +110 -0
  7. package/dist/connector/font-server.js +165 -0
  8. package/dist/connector/host-connector.js +430 -0
  9. package/dist/connector/lock.js +66 -0
  10. package/dist/connector/main.js +18 -0
  11. package/dist/connector/sessions.js +366 -0
  12. package/dist/connector/update-report.js +67 -0
  13. package/dist/connector/wire.js +42 -0
  14. package/{src → dist}/exit-codes.js +1 -4
  15. package/dist/font-subset.js +110 -0
  16. package/dist/herdr-api.js +246 -0
  17. package/dist/herdr-command.js +223 -0
  18. package/dist/herdr-plugin.js +116 -0
  19. package/dist/herdr-server.js +189 -0
  20. package/dist/i18n/en.js +223 -0
  21. package/dist/i18n/index.js +48 -0
  22. package/dist/i18n/zh.js +222 -0
  23. package/dist/keepalive/detached.js +78 -0
  24. package/dist/keepalive/environment.js +44 -0
  25. package/dist/keepalive/index.js +110 -0
  26. package/dist/keepalive/launchd.js +117 -0
  27. package/dist/keepalive/systemd.js +108 -0
  28. package/dist/keepalive/types.js +1 -0
  29. package/dist/lib/process.js +12 -0
  30. package/dist/lifecycle.js +59 -0
  31. package/dist/net-interfaces.js +73 -0
  32. package/dist/pasted-files.js +115 -0
  33. package/dist/paths.js +62 -0
  34. package/dist/pty-session.js +96 -0
  35. package/dist/relay-client.js +106 -0
  36. package/dist/relay-urls.js +99 -0
  37. package/dist/runtime.js +106 -0
  38. package/dist/service.js +260 -0
  39. package/dist/settings-model.js +298 -0
  40. package/dist/socket-discovery.js +48 -0
  41. package/dist/supervisor.js +319 -0
  42. package/dist/terminal-font/deps.js +73 -0
  43. package/dist/terminal-font/files.js +354 -0
  44. package/dist/terminal-font/formats.js +335 -0
  45. package/dist/terminal-font/index.js +187 -0
  46. package/dist/terminal-font/readers.js +316 -0
  47. package/dist/terminal-font/terminals.js +128 -0
  48. package/dist/terminal-palette.js +339 -0
  49. package/dist/tui/App.js +195 -0
  50. package/dist/tui/api.js +79 -0
  51. package/dist/tui/components/TextField.js +50 -0
  52. package/dist/tui/components/common.js +80 -0
  53. package/dist/tui/index.js +36 -0
  54. package/dist/tui/mouse/index.js +164 -0
  55. package/dist/tui/mouse/protocol.js +96 -0
  56. package/dist/tui/mouse/source.js +74 -0
  57. package/dist/tui/screens/About.js +166 -0
  58. package/dist/tui/screens/Herdr.js +74 -0
  59. package/dist/tui/screens/Keepalive.js +84 -0
  60. package/dist/tui/screens/Overview.js +36 -0
  61. package/dist/tui/screens/Pair.js +74 -0
  62. package/dist/tui/screens/Relay.js +242 -0
  63. package/dist/tui/screens/Services.js +63 -0
  64. package/dist/tui/screens/Wizard.js +235 -0
  65. package/dist/tui/terminal.js +39 -0
  66. package/dist/tui/theme.js +33 -0
  67. package/dist/updater.js +382 -0
  68. package/herdr-plugin.toml +1 -1
  69. package/package.json +10 -9
  70. package/dist/tui.mjs +0 -4613
  71. package/src/agent-status.js +0 -139
  72. package/src/config.js +0 -454
  73. package/src/herdr-api.js +0 -244
  74. package/src/herdr-command.js +0 -231
  75. package/src/herdr-plugin.js +0 -130
  76. package/src/herdr-server.js +0 -201
  77. package/src/host-connector.js +0 -927
  78. package/src/i18n/en.js +0 -240
  79. package/src/i18n/index.js +0 -56
  80. package/src/i18n/zh.js +0 -239
  81. package/src/keepalive.js +0 -460
  82. package/src/lifecycle.js +0 -62
  83. package/src/net-interfaces.js +0 -76
  84. package/src/pasted-files.js +0 -176
  85. package/src/pty-session.js +0 -92
  86. package/src/service.js +0 -516
  87. package/src/settings-model.js +0 -323
  88. package/src/socket-discovery.js +0 -60
  89. package/src/state.js +0 -47
  90. package/src/supervisor.js +0 -250
  91. package/src/terminal-palette.js +0 -325
  92. package/src/updater.js +0 -394
package/src/service.js DELETED
@@ -1,516 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const http = require('node:http');
5
- const https = require('node:https');
6
- const path = require('node:path');
7
- const { spawn } = require('node:child_process');
8
- const {
9
- PACKAGE_ROOT,
10
- bindAddress,
11
- configDir,
12
- loadConfig,
13
- resolveAdminOrigin,
14
- resolveHostRelayUrl,
15
- resolvePublicUrl,
16
- runsLocalRelay,
17
- runtimeStatePath,
18
- stateDir,
19
- } = require('./config');
20
- const { ensureDir, randomToken, readJson, writeJsonAtomic } = require('./state');
21
- const {
22
- probeTerminalPalette,
23
- paletteFromEnvironment,
24
- rememberTerminalPalette,
25
- rememberedTerminalPalette,
26
- } = require('./terminal-palette');
27
- const { resolveSocketPath } = require('./socket-discovery');
28
- const { preferredLanAddress } = require('./net-interfaces');
29
- const { MIN_HERDR_VERSION, findHerdrCommand, herdrVersion, resolveHerdrCommand } = require('./herdr-command');
30
-
31
- const RUNTIME_VERSION = 2;
32
-
33
- function pidAlive(pid) {
34
- if (!Number.isInteger(pid) || pid <= 0) return false;
35
- try {
36
- process.kill(pid, 0);
37
- return true;
38
- } catch (error) {
39
- return error.code === 'EPERM';
40
- }
41
- }
42
-
43
- function readRuntime() {
44
- const state = readJson(runtimeStatePath(), {});
45
- return state && typeof state === 'object' ? state : {};
46
- }
47
-
48
- /**
49
- * Load (creating if needed) the long-lived identifiers and secrets this
50
- * workstation uses. These live in the state directory rather than config.json
51
- * because they are credentials: writeJsonAtomic stores them mode 0600.
52
- */
53
- function ensureRuntime() {
54
- ensureDir(configDir());
55
- ensureDir(stateDir());
56
- const state = readRuntime();
57
- if (!state.version) state.version = RUNTIME_VERSION;
58
- if (!state.hostId) state.hostId = `host-${randomToken(9)}`;
59
- if (!state.hostToken) state.hostToken = randomToken(32);
60
- writeJsonAtomic(runtimeStatePath(), state);
61
- return state;
62
- }
63
-
64
- /**
65
- * Set the password used to join a relay. An empty value means "no password",
66
- * which is what a public relay expects.
67
- *
68
- * It lives in the state file rather than config.json because it is a secret:
69
- * writeJsonAtomic stores that file mode 0600.
70
- */
71
- function setRelayPassword(password) {
72
- const state = ensureRuntime();
73
- state.relayPassword = typeof password === 'string' ? password.trim() : '';
74
- writeJsonAtomic(runtimeStatePath(), state);
75
- return state;
76
- }
77
-
78
- /**
79
- * Issue this workstation a new identity on the relay.
80
- *
81
- * The host token is what proves ownership of this workstation, so replacing it
82
- * means the relay no longer recognises the old one — useful if it leaked, but
83
- * it also orphans the previous record on a relay that already enrolled it.
84
- */
85
- function regenerateHostIdentity() {
86
- const state = ensureRuntime();
87
- state.hostId = `host-${randomToken(9)}`;
88
- state.hostToken = randomToken(32);
89
- writeJsonAtomic(runtimeStatePath(), state);
90
- return state;
91
- }
92
-
93
- /**
94
- * The colors of the terminal this workstation is looked at through.
95
- *
96
- * Asked once per process, from whichever start path has a real terminal, and
97
- * remembered in the state file: a later start from a service manager has no
98
- * terminal to ask, and the workstation's appearance has not changed just
99
- * because systemd, and not a person, launched it this time.
100
- */
101
- let cachedTerminalPalette;
102
- function hostTerminalPalette({ refresh = false } = {}) {
103
- if (!refresh && cachedTerminalPalette !== undefined) return cachedTerminalPalette;
104
-
105
- const inherited = paletteFromEnvironment();
106
- if (inherited) {
107
- cachedTerminalPalette = inherited;
108
- return cachedTerminalPalette;
109
- }
110
-
111
- const probed = probeTerminalPalette();
112
- if (probed) {
113
- cachedTerminalPalette = rememberTerminalPalette(probed);
114
- return cachedTerminalPalette;
115
- }
116
-
117
- // Nothing to ask: fall back to what a start with a terminal wrote down,
118
- // re-validated, because a state file is not a trusted wire either.
119
- cachedTerminalPalette = rememberedTerminalPalette();
120
- return cachedTerminalPalette;
121
- }
122
-
123
- function logPath(name) {
124
- return path.join(stateDir(), `${name}.log`);
125
- }
126
-
127
- function relayBinPath() {
128
- // Resolved rather than hard-coded: the relay is a separate npm package, so
129
- // its location depends on how npm hoisted it.
130
- const manifest = require.resolve('herdr-remote-relay/package.json');
131
- return path.join(path.dirname(manifest), 'bin', 'herdr-remote-relay.js');
132
- }
133
-
134
- function relayAuthStatePath() {
135
- return path.join(stateDir(), 'relay-auth.json');
136
- }
137
-
138
- /**
139
- * The processes that make up a running herdr-remote, as declarative specs.
140
- *
141
- * Both the detached starter and the foreground supervisor build their children
142
- * from this one list so the two paths cannot drift in how they configure the
143
- * relay or the host connector.
144
- */
145
- function serviceSpecs(config = loadConfig(), state = ensureRuntime()) {
146
- const specs = [];
147
- const publicUrl = resolvePublicUrl(config, preferredLanAddress());
148
-
149
- if (runsLocalRelay(config)) {
150
- specs.push({
151
- name: 'relay',
152
- command: process.execPath,
153
- args: [relayBinPath()],
154
- env: {
155
- // Let the shared web UI tell a private workstation relay apart from
156
- // the operator-facing self-hosted relay. Standalone relay installs
157
- // default to remote mode.
158
- RELAY_DEPLOYMENT_MODE: 'local',
159
- RELAY_BIND: bindAddress(config),
160
- RELAY_PORT: String(config.relay.port),
161
- RELAY_PUBLIC_URL: publicUrl,
162
- // A relay we start ourselves is closed to everything but this
163
- // workstation: reusing the host token as its password costs nothing and
164
- // stops another machine on the LAN from enrolling into it.
165
- RELAY_PASSWORD: state.hostToken,
166
- RELAY_AUTH_STATE_FILE: relayAuthStatePath(),
167
- RELAY_ALLOWED_ORIGINS: (config.relay.allowedOrigins || []).join(','),
168
- RELAY_MAX_CLIENTS_PER_HOST: String(config.relay.maxClientsPerHost),
169
- RELAY_MAX_HOSTS: String(config.relay.maxHosts),
170
- RELAY_MAX_PENDING_HANDSHAKES: String(config.relay.maxPendingHandshakes),
171
- RELAY_MAX_BUFFERED_BYTES_PER_CLIENT: String(config.relay.maxBufferedBytesPerClient),
172
- RELAY_HOST_RECONNECT_GRACE_MS: String(config.relay.hostReconnectGraceMs),
173
- },
174
- });
175
- }
176
-
177
- const terminalPalette = hostTerminalPalette();
178
-
179
- specs.push({
180
- name: 'host',
181
- command: process.execPath,
182
- args: [path.join(PACKAGE_ROOT, 'src', 'host-connector.js')],
183
- env: {
184
- // Captured here, where a terminal may still be attached, because the
185
- // connector itself usually runs detached with no terminal to ask.
186
- ...(terminalPalette ? { HERDR_TERM_PALETTE_JSON: JSON.stringify(terminalPalette) } : {}),
187
- RELAY_URL: resolveHostRelayUrl(config),
188
- RELAY_HOST_ID: state.hostId,
189
- RELAY_HOST_TOKEN: state.hostToken,
190
- RELAY_PASSWORD: runsLocalRelay(config) ? state.hostToken : (state.relayPassword || ''),
191
- HERDR_SOCKET_PATH: resolveSocketPath(config.herdr.socketPath),
192
- HERDR_ARGS_JSON: JSON.stringify(config.herdr.args),
193
- HERDR_CWD: config.herdr.cwd,
194
- HERDR_BIN_PATH: resolveHerdrCommand(),
195
- },
196
- });
197
-
198
- return specs;
199
- }
200
-
201
- function baseEnvironment() {
202
- return { ...process.env, HERDR_REMOTE_SERVICE: '1' };
203
- }
204
-
205
- function spawnDetached(spec) {
206
- ensureDir(stateDir());
207
- const logFd = fs.openSync(logPath(spec.name), 'a');
208
- try {
209
- const child = spawn(spec.command, spec.args, {
210
- cwd: PACKAGE_ROOT,
211
- env: { ...baseEnvironment(), ...spec.env },
212
- detached: true,
213
- stdio: ['ignore', logFd, logFd],
214
- });
215
- child.unref();
216
- return child.pid;
217
- } finally {
218
- fs.closeSync(logFd);
219
- }
220
- }
221
-
222
- /**
223
- * Remember a process we started, in a list that survives being overwritten.
224
- *
225
- * `relayPid`/`hostPid` alone were not enough: the supervisor rewrote them with
226
- * its own children, which orphaned anything an earlier detached start had
227
- * spawned. Those orphans kept the port and the host session, so the relay could
228
- * never bind and "stop" had nothing left to kill. The ledger is append-only
229
- * (minus dead entries) precisely so no writer can lose another writer's pids.
230
- */
231
- function recordManagedPid(state, name, pid) {
232
- const existing = Array.isArray(state.managedPids) ? state.managedPids : [];
233
- const kept = existing.filter((entry) => entry && entry.pid !== pid && pidAlive(entry.pid));
234
- // Only track something that is actually running: a pid that already exited
235
- // would sit in the ledger until its number is recycled by an unrelated
236
- // process, which we would then happily signal.
237
- if (Number.isInteger(pid) && pidAlive(pid)) {
238
- kept.push({ name, pid, startedAt: new Date().toISOString() });
239
- }
240
- state.managedPids = kept;
241
- return state;
242
- }
243
-
244
- /** Every process we believe we started, alive right now. */
245
- function managedPids(state = readRuntime()) {
246
- const pids = new Map();
247
- // Supervisor first and never overwritten: `stopServices` relies on the order
248
- // to signal it before its children, and one pid can appear under several
249
- // fields.
250
- for (const [name, pid] of [['supervisor', state.supervisorPid], ['host', state.hostPid], ['relay', state.relayPid]]) {
251
- if (pidAlive(pid) && !pids.has(pid)) pids.set(pid, name);
252
- }
253
- for (const entry of state.managedPids || []) {
254
- if (entry && pidAlive(entry.pid) && !pids.has(entry.pid)) pids.set(entry.pid, entry.name);
255
- }
256
- return [...pids].map(([pid, name]) => ({ pid, name }));
257
- }
258
-
259
- function startServices() {
260
- const config = loadConfig();
261
- const state = ensureRuntime();
262
- const next = { ...state };
263
- const specs = serviceSpecs(config, state);
264
-
265
- for (const spec of specs) {
266
- const pidKey = `${spec.name}Pid`;
267
- if (pidAlive(next[pidKey])) continue;
268
- next[pidKey] = spawnDetached(spec);
269
- recordManagedPid(next, spec.name, next[pidKey]);
270
- }
271
- // A relay that is no longer part of the plan (switched to remote mode) must
272
- // not be left running on the old port.
273
- if (!specs.some((spec) => spec.name === 'relay') && pidAlive(next.relayPid)) {
274
- try { process.kill(next.relayPid, 'SIGTERM'); } catch {}
275
- next.relayPid = null;
276
- }
277
-
278
- next.startedAt = next.startedAt || new Date().toISOString();
279
- next.mode = config.relay.mode;
280
- writeJsonAtomic(runtimeStatePath(), next);
281
- return {
282
- ok: true,
283
- mode: config.relay.mode,
284
- relay: {
285
- local: runsLocalRelay(config),
286
- pid: next.relayPid || null,
287
- alive: pidAlive(next.relayPid),
288
- bind: runsLocalRelay(config) ? bindAddress(config) : null,
289
- port: config.relay.port,
290
- remoteUrl: config.relay.remoteUrl || null,
291
- },
292
- host: {
293
- pid: next.hostPid || null,
294
- alive: pidAlive(next.hostPid),
295
- socketPath: resolveSocketPath(config.herdr.socketPath),
296
- },
297
- publicUrl: resolvePublicUrl(config, preferredLanAddress()),
298
- };
299
- }
300
-
301
- function stopServices() {
302
- const state = readRuntime();
303
- const stopped = [];
304
-
305
- // Supervisor first: it would otherwise see its children die and restart them
306
- // faster than we can kill them.
307
- const targets = managedPids(state).sort((a, b) => (
308
- (a.name === 'supervisor' ? 0 : 1) - (b.name === 'supervisor' ? 0 : 1)
309
- ));
310
-
311
- for (const { pid, name } of targets) {
312
- try {
313
- process.kill(pid, 'SIGTERM');
314
- stopped.push({ name, pid });
315
- } catch (error) {
316
- process.stderr.write(`herdr-remote: could not stop ${name} (pid ${pid}): ${error.message}\n`);
317
- }
318
- }
319
-
320
- state.hostPid = null;
321
- state.relayPid = null;
322
- state.supervisorPid = null;
323
- state.managedPids = [];
324
- state.startedAt = null;
325
- ensureDir(stateDir());
326
- writeJsonAtomic(runtimeStatePath(), state);
327
- return { ok: true, stopped };
328
- }
329
-
330
- function restartServices() {
331
- stopServices();
332
- return startServices();
333
- }
334
-
335
- /**
336
- * Minimal JSON client. Picks http or https from the URL: a self-hosted relay is
337
- * reached over https, while the local relay is plain http on loopback.
338
- */
339
- function requestJson(urlString, options = {}) {
340
- return new Promise((resolve, reject) => {
341
- let url;
342
- try { url = new URL(urlString); } catch (error) { return reject(error); }
343
- const transport = url.protocol === 'https:' ? https : http;
344
- const request = transport.request(url, {
345
- method: options.method || 'GET',
346
- headers: options.headers || {},
347
- timeout: options.timeout || 1500,
348
- }, (response) => {
349
- const chunks = [];
350
- response.on('data', (chunk) => chunks.push(chunk));
351
- response.on('end', () => {
352
- const text = Buffer.concat(chunks).toString('utf8');
353
- let body;
354
- try { body = JSON.parse(text); } catch { body = { raw: text }; }
355
- if (response.statusCode >= 400) {
356
- const error = new Error(body.message || `HTTP ${response.statusCode}`);
357
- error.statusCode = response.statusCode;
358
- error.body = body;
359
- reject(error);
360
- } else resolve(body);
361
- });
362
- });
363
- request.on('timeout', () => request.destroy(new Error('request timed out')));
364
- request.on('error', reject);
365
- if (options.body) request.write(options.body);
366
- request.end();
367
- });
368
- }
369
-
370
- function healthUrl(config) {
371
- return `${resolveAdminOrigin(config)}/healthz`;
372
- }
373
-
374
- async function waitForRelay(config, { attempts = 20, delayMs = 100 } = {}) {
375
- let lastError;
376
- for (let attempt = 0; attempt < attempts; attempt += 1) {
377
- try { return await requestJson(healthUrl(config), { timeout: 800 }); } catch (error) { lastError = error; }
378
- await new Promise((resolve) => setTimeout(resolve, delayMs));
379
- }
380
- throw lastError || new Error('relay did not become ready');
381
- }
382
-
383
- async function waitForHost(config, { attempts = 30, delayMs = 100 } = {}) {
384
- const state = ensureRuntime();
385
- const statusEndpoint = `${resolveAdminOrigin(config)}/api/status`;
386
- let lastStatus = null;
387
- for (let attempt = 0; attempt < attempts; attempt += 1) {
388
- try {
389
- lastStatus = await requestJson(statusEndpoint, {
390
- timeout: 800,
391
- headers: {
392
- 'X-Herdr-Host-Id': state.hostId,
393
- 'X-Herdr-Host-Token': state.hostToken,
394
- },
395
- });
396
- if (lastStatus.hosts?.some((host) => host.id === state.hostId)) return lastStatus;
397
- } catch (error) {
398
- lastStatus = { ok: false, message: error.message };
399
- }
400
- await new Promise((resolve) => setTimeout(resolve, delayMs));
401
- }
402
- const error = new Error(lastStatus?.message || 'the Herdr host connector did not register with the relay');
403
- error.health = lastStatus;
404
- throw error;
405
- }
406
-
407
- async function statusServices() {
408
- const config = loadConfig();
409
- const state = readRuntime();
410
- const lanAddress = preferredLanAddress();
411
- let health = null;
412
- try {
413
- health = await requestJson(healthUrl(config), { timeout: 1200 });
414
- } catch (error) {
415
- health = { ok: false, message: error.message };
416
- }
417
- const socketPath = resolveSocketPath(config.herdr.socketPath);
418
- // Where Herdr was found, and whether it was found at all: under a service
419
- // manager this is the first thing to check when sessions will not start.
420
- const herdr = findHerdrCommand();
421
- // Only worth spawning when there is something to spawn; an unresolved command
422
- // has already answered the question the version would.
423
- const installed = herdr.found ? herdrVersion({ command: herdr.command }) : { version: null, supported: true };
424
- return {
425
- ok: true,
426
- mode: config.relay.mode,
427
- relay: {
428
- local: runsLocalRelay(config),
429
- pid: state.relayPid || null,
430
- alive: runsLocalRelay(config) ? pidAlive(state.relayPid) : null,
431
- bind: runsLocalRelay(config) ? bindAddress(config) : null,
432
- port: config.relay.port,
433
- remoteUrl: config.relay.remoteUrl || null,
434
- health,
435
- },
436
- host: {
437
- pid: state.hostPid || null,
438
- alive: pidAlive(state.hostPid),
439
- hostId: state.hostId || null,
440
- socketPath,
441
- socketExists: Boolean(socketPath) && fs.existsSync(socketPath),
442
- herdrCommand: herdr.command,
443
- herdrCommandFound: herdr.found,
444
- herdrCommandSource: herdr.source,
445
- herdrVersion: installed.version,
446
- herdrVersionSupported: installed.supported,
447
- herdrVersionMinimum: MIN_HERDR_VERSION,
448
- },
449
- publicUrl: resolvePublicUrl(config, lanAddress),
450
- startedAt: state.startedAt || null,
451
- };
452
- }
453
-
454
- async function pair() {
455
- const config = loadConfig();
456
- const state = ensureRuntime();
457
- if (runsLocalRelay(config)) startServices();
458
- await waitForRelay(config);
459
- await waitForHost(config);
460
- // Authenticated with this workstation's own host token: on a shared relay
461
- // that is what proves the pairing code is being minted for our terminal and
462
- // nobody else's.
463
- const pairing = await requestJson(`${resolveAdminOrigin(config)}/api/pair/start`, {
464
- method: 'POST',
465
- headers: {
466
- 'X-Herdr-Host-Id': state.hostId,
467
- 'X-Herdr-Host-Token': state.hostToken,
468
- },
469
- timeout: 5000,
470
- });
471
- return pairing;
472
- }
473
-
474
- function extractPairingCode(response) {
475
- const code = response?.code || response?.pairCode;
476
- if (typeof code !== 'string' || code.length === 0) {
477
- throw new Error('the relay response did not contain a valid pairing code');
478
- }
479
- return code;
480
- }
481
-
482
- function readLogTail(name, lines = 40) {
483
- try {
484
- const content = fs.readFileSync(logPath(name), 'utf8');
485
- return content.split('\n').filter(Boolean).slice(-lines);
486
- } catch {
487
- return [];
488
- }
489
- }
490
-
491
- module.exports = {
492
- RUNTIME_VERSION,
493
- pidAlive,
494
- readRuntime,
495
- ensureRuntime,
496
- recordManagedPid,
497
- managedPids,
498
- setRelayPassword,
499
- regenerateHostIdentity,
500
- serviceSpecs,
501
- baseEnvironment,
502
- hostTerminalPalette,
503
- startServices,
504
- stopServices,
505
- restartServices,
506
- statusServices,
507
- requestJson,
508
- waitForRelay,
509
- waitForHost,
510
- pair,
511
- extractPairingCode,
512
- relayBinPath,
513
- relayAuthStatePath,
514
- logPath,
515
- readLogTail,
516
- };