herdr-remote 0.2.18 → 0.2.20

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 (95) hide show
  1. package/bin/herdr-remote.js +3 -225
  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 +8 -8
  70. package/dist/tui.mjs +0 -5775
  71. package/src/agent-status.js +0 -139
  72. package/src/binary-plist.js +0 -146
  73. package/src/config.js +0 -454
  74. package/src/font-subset.js +0 -110
  75. package/src/herdr-api.js +0 -244
  76. package/src/herdr-command.js +0 -231
  77. package/src/herdr-plugin.js +0 -130
  78. package/src/herdr-server.js +0 -201
  79. package/src/host-connector.js +0 -1069
  80. package/src/i18n/en.js +0 -240
  81. package/src/i18n/index.js +0 -56
  82. package/src/i18n/zh.js +0 -239
  83. package/src/keepalive.js +0 -460
  84. package/src/lifecycle.js +0 -62
  85. package/src/net-interfaces.js +0 -76
  86. package/src/pasted-files.js +0 -176
  87. package/src/pty-session.js +0 -92
  88. package/src/service.js +0 -530
  89. package/src/settings-model.js +0 -323
  90. package/src/socket-discovery.js +0 -60
  91. package/src/state.js +0 -47
  92. package/src/supervisor.js +0 -250
  93. package/src/terminal-font.js +0 -1204
  94. package/src/terminal-palette.js +0 -325
  95. package/src/updater.js +0 -394
package/src/keepalive.js DELETED
@@ -1,460 +0,0 @@
1
- 'use strict';
2
-
3
- // Keep-alive integration.
4
- //
5
- // "Start the services" and "keep the services running" are different problems:
6
- // a detached spawn dies with the first crash and never comes back. This module
7
- // hands supervision to the platform's service manager where one exists
8
- // (systemd --user on Linux, launchd on macOS) and falls back to a detached copy
9
- // of our own supervisor where neither does.
10
-
11
- const fs = require('node:fs');
12
- const os = require('node:os');
13
- const path = require('node:path');
14
- const { spawn, spawnSync } = require('node:child_process');
15
- const { PACKAGE_ROOT, loadConfig, stateDir } = require('./config');
16
- const { ensureDir, readJson, writeJsonAtomic } = require('./state');
17
- const { logPath, pidAlive } = require('./service');
18
- const { findHerdrCommand } = require('./herdr-command');
19
-
20
- const SYSTEMD_UNIT_NAME = 'herdr-remote.service';
21
- const LAUNCHD_LABEL = 'dev.herdr.remote';
22
- const FALLBACK_PID_FILE = 'supervisor.pid';
23
-
24
- function cliEntryPoint() {
25
- return path.join(PACKAGE_ROOT, 'bin', 'herdr-remote.js');
26
- }
27
-
28
- function systemdUnitPath() {
29
- const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
30
- return path.join(configHome, 'systemd', 'user', SYSTEMD_UNIT_NAME);
31
- }
32
-
33
- function launchdPlistPath() {
34
- return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
35
- }
36
-
37
- function fallbackPidPath() {
38
- return path.join(stateDir(), FALLBACK_PID_FILE);
39
- }
40
-
41
- /**
42
- * Look for an executable on PATH without going through a shell: `spawnSync`
43
- * with `shell: true` concatenates rather than escapes its arguments, and Node
44
- * now warns about it on every call.
45
- */
46
- function commandExists(command) {
47
- const searchPath = process.env.PATH || '';
48
- return searchPath.split(path.delimiter).filter(Boolean).some((directory) => {
49
- try {
50
- fs.accessSync(path.join(directory, command), fs.constants.X_OK);
51
- return true;
52
- } catch {
53
- return false;
54
- }
55
- });
56
- }
57
-
58
- /** Which manager to use given the platform, the user's preference and reality. */
59
- function detectManager(preference = 'auto') {
60
- if (preference && preference !== 'auto') return preference;
61
- if (process.platform === 'linux') {
62
- // `systemctl --user` needs a user bus; containers and bare TTY logins often
63
- // have systemd installed but no session bus, where it would fail at runtime.
64
- if (commandExists('systemctl') && (process.env.DBUS_SESSION_BUS_ADDRESS || process.env.XDG_RUNTIME_DIR)) {
65
- return 'systemd';
66
- }
67
- return 'supervisor';
68
- }
69
- if (process.platform === 'darwin') {
70
- return commandExists('launchctl') ? 'launchd' : 'supervisor';
71
- }
72
- return 'supervisor';
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // The environment a supervised copy needs
77
- // ---------------------------------------------------------------------------
78
-
79
- /**
80
- * `PATH` for the unit: the one that is resolving commands right now, plus the
81
- * directory Herdr was found in.
82
- *
83
- * A service manager does not inherit the shell's environment. `systemd --user`
84
- * hands a unit something close to `/usr/local/bin:/usr/bin:/bin` and `launchd`
85
- * is no more generous, so a copy that works from a terminal loses `~/.local/bin`
86
- * — and with it `herdr` — the moment it is installed as a service. Freezing the
87
- * installing shell's `PATH` into the unit keeps the supervised copy able to find
88
- * Herdr, and Herdr able to find the tools it spawns in turn.
89
- */
90
- function servicePath({ env = process.env, herdrCommand = null } = {}) {
91
- const entries = String(env.PATH || '')
92
- .split(path.delimiter)
93
- .map((entry) => entry.trim())
94
- .filter(Boolean);
95
- // The override already pins the exact binary; this is only so the session
96
- // itself can still reach it by name.
97
- if (herdrCommand) entries.push(path.dirname(herdrCommand));
98
- const seen = new Set();
99
- const unique = entries.filter((entry) => (seen.has(entry) ? false : seen.add(entry)));
100
- return unique.join(path.delimiter);
101
- }
102
-
103
- /**
104
- * What has to be written into the unit file for a supervised start to behave
105
- * like the one the user just ran by hand.
106
- */
107
- function serviceEnvironment({ env = process.env, home = os.homedir(), directories } = {}) {
108
- const environment = {};
109
- const herdr = findHerdrCommand({ env, home, ...(directories ? { directories } : {}) });
110
- if (herdr.found) environment.HERDR_BIN_PATH = herdr.command;
111
- const searchPath = servicePath({ env, herdrCommand: herdr.found ? herdr.command : null });
112
- if (searchPath) environment.PATH = searchPath;
113
- return environment;
114
- }
115
-
116
- // ---------------------------------------------------------------------------
117
- // Unit file rendering (pure, so it can be asserted in tests)
118
- // ---------------------------------------------------------------------------
119
-
120
- /**
121
- * One `KEY=value` per directive. An unquoted systemd value ends at the first
122
- * space, and `%` starts a specifier, so a `PATH` with either in it would be
123
- * silently truncated or rewritten.
124
- */
125
- function systemdEnvironmentLine(key, value) {
126
- const text = String(value).replace(/[\r\n]+/g, ' ').replace(/%/g, '%%');
127
- if (/^[\w@+=:,./-]*$/.test(text)) return `Environment=${key}=${text}`;
128
- return `Environment="${key}=${text.replace(/([\\"])/g, '\\$1')}"`;
129
- }
130
-
131
- function renderSystemdUnit({ nodePath = process.execPath, entryPoint = cliEntryPoint(), environment = {} } = {}) {
132
- const environmentLines = Object.entries(environment)
133
- .map(([key, value]) => systemdEnvironmentLine(key, value))
134
- .join('\n');
135
- return `[Unit]
136
- Description=Herdr Remote (relay and host connector)
137
- Documentation=https://github.com/herdr/herdr-remote
138
- After=default.target
139
-
140
- [Service]
141
- Type=simple
142
- ExecStart=${nodePath} ${entryPoint} run
143
- Restart=always
144
- RestartSec=3
145
- # Give the relay and host connector time to close sessions cleanly.
146
- TimeoutStopSec=15
147
- ${environmentLines}
148
-
149
- [Install]
150
- WantedBy=default.target
151
- `;
152
- }
153
-
154
- function escapeXml(value) {
155
- return String(value)
156
- .replace(/&/g, '&')
157
- .replace(/</g, '&lt;')
158
- .replace(/>/g, '&gt;')
159
- .replace(/"/g, '&quot;')
160
- .replace(/'/g, '&apos;');
161
- }
162
-
163
- function renderLaunchdPlist({
164
- nodePath = process.execPath,
165
- entryPoint = cliEntryPoint(),
166
- label = LAUNCHD_LABEL,
167
- outLog = logPath('supervisor'),
168
- errLog = logPath('supervisor'),
169
- environment = {},
170
- } = {}) {
171
- const environmentEntries = Object.entries(environment)
172
- .map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`)
173
- .join('\n');
174
- const environmentBlock = environmentEntries
175
- ? ` <key>EnvironmentVariables</key>\n <dict>\n${environmentEntries}\n </dict>\n`
176
- : '';
177
- return `<?xml version="1.0" encoding="UTF-8"?>
178
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
179
- <plist version="1.0">
180
- <dict>
181
- <key>Label</key>
182
- <string>${escapeXml(label)}</string>
183
- <key>ProgramArguments</key>
184
- <array>
185
- <string>${escapeXml(nodePath)}</string>
186
- <string>${escapeXml(entryPoint)}</string>
187
- <string>run</string>
188
- </array>
189
- <key>RunAtLoad</key>
190
- <true/>
191
- <key>KeepAlive</key>
192
- <true/>
193
- <key>ProcessType</key>
194
- <string>Background</string>
195
- ${environmentBlock} <key>StandardOutPath</key>
196
- <string>${escapeXml(outLog)}</string>
197
- <key>StandardErrorPath</key>
198
- <string>${escapeXml(errLog)}</string>
199
- </dict>
200
- </plist>
201
- `;
202
- }
203
-
204
- // ---------------------------------------------------------------------------
205
- // systemd
206
- // ---------------------------------------------------------------------------
207
-
208
- function systemctl(args, { capture = true } = {}) {
209
- return spawnSync('systemctl', ['--user', ...args], {
210
- encoding: 'utf8',
211
- stdio: capture ? 'pipe' : 'inherit',
212
- });
213
- }
214
-
215
- function systemdStatus() {
216
- const installed = fs.existsSync(systemdUnitPath());
217
- if (!installed) return { manager: 'systemd', installed: false, active: false, enabled: false };
218
- const active = systemctl(['is-active', SYSTEMD_UNIT_NAME]);
219
- const enabled = systemctl(['is-enabled', SYSTEMD_UNIT_NAME]);
220
- const lingering = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8' });
221
- return {
222
- manager: 'systemd',
223
- installed: true,
224
- active: String(active.stdout || '').trim() === 'active',
225
- enabled: String(enabled.stdout || '').trim().startsWith('enabled'),
226
- linger: String(lingering.stdout || '').includes('Linger=yes'),
227
- unitPath: systemdUnitPath(),
228
- state: String(active.stdout || active.stderr || '').trim(),
229
- };
230
- }
231
-
232
- function systemdInstall() {
233
- const unitPath = systemdUnitPath();
234
- ensureDir(path.dirname(unitPath));
235
- fs.writeFileSync(unitPath, renderSystemdUnit({ environment: serviceEnvironment() }), { mode: 0o644 });
236
- const reload = systemctl(['daemon-reload']);
237
- if (reload.status !== 0) {
238
- throw new Error(`systemctl --user daemon-reload failed: ${String(reload.stderr || '').trim()}`);
239
- }
240
- const enable = systemctl(['enable', '--now', SYSTEMD_UNIT_NAME]);
241
- if (enable.status !== 0) {
242
- throw new Error(`systemctl --user enable --now failed: ${String(enable.stderr || '').trim()}`);
243
- }
244
- return { ok: true, unitPath, hint: `loginctl enable-linger ${os.userInfo().username}` };
245
- }
246
-
247
- function systemdUninstall() {
248
- const unitPath = systemdUnitPath();
249
- systemctl(['disable', '--now', SYSTEMD_UNIT_NAME]);
250
- if (fs.existsSync(unitPath)) fs.rmSync(unitPath, { force: true });
251
- systemctl(['daemon-reload']);
252
- return { ok: true, unitPath };
253
- }
254
-
255
- // ---------------------------------------------------------------------------
256
- // launchd
257
- // ---------------------------------------------------------------------------
258
-
259
- function launchdDomainTarget() {
260
- return `gui/${process.getuid ? process.getuid() : ''}`;
261
- }
262
-
263
- function launchdStatus() {
264
- const plistPath = launchdPlistPath();
265
- const installed = fs.existsSync(plistPath);
266
- if (!installed) return { manager: 'launchd', installed: false, active: false, enabled: false };
267
- const result = spawnSync('launchctl', ['print', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { encoding: 'utf8' });
268
- const output = String(result.stdout || '');
269
- return {
270
- manager: 'launchd',
271
- installed: true,
272
- active: result.status === 0 && /state = running/.test(output),
273
- enabled: result.status === 0,
274
- unitPath: plistPath,
275
- state: result.status === 0 ? 'loaded' : 'not loaded',
276
- };
277
- }
278
-
279
- function launchdInstall() {
280
- const plistPath = launchdPlistPath();
281
- ensureDir(path.dirname(plistPath));
282
- ensureDir(stateDir());
283
- fs.writeFileSync(plistPath, renderLaunchdPlist({ environment: serviceEnvironment() }), { mode: 0o644 });
284
- // bootout first so a re-install picks up the rewritten plist.
285
- spawnSync('launchctl', ['bootout', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
286
- const result = spawnSync('launchctl', ['bootstrap', launchdDomainTarget(), plistPath], { encoding: 'utf8' });
287
- if (result.status !== 0) {
288
- throw new Error(`launchctl bootstrap failed: ${String(result.stderr || '').trim()}`);
289
- }
290
- spawnSync('launchctl', ['enable', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
291
- return { ok: true, unitPath: plistPath };
292
- }
293
-
294
- function launchdUninstall() {
295
- const plistPath = launchdPlistPath();
296
- spawnSync('launchctl', ['bootout', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
297
- if (fs.existsSync(plistPath)) fs.rmSync(plistPath, { force: true });
298
- return { ok: true, unitPath: plistPath };
299
- }
300
-
301
- // ---------------------------------------------------------------------------
302
- // Fallback supervisor daemon
303
- // ---------------------------------------------------------------------------
304
-
305
- function readFallbackPid() {
306
- const record = readJson(fallbackPidPath(), {});
307
- return Number.isInteger(record.pid) ? record.pid : null;
308
- }
309
-
310
- function fallbackStatus() {
311
- const pid = readFallbackPid();
312
- const alive = pidAlive(pid);
313
- return {
314
- manager: 'supervisor',
315
- installed: alive,
316
- active: alive,
317
- enabled: false,
318
- pid: alive ? pid : null,
319
- unitPath: fallbackPidPath(),
320
- state: alive ? 'running' : 'stopped',
321
- };
322
- }
323
-
324
- function fallbackInstall() {
325
- if (pidAlive(readFallbackPid())) return { ok: true, alreadyRunning: true, pid: readFallbackPid() };
326
- ensureDir(stateDir());
327
- const logFd = fs.openSync(logPath('supervisor'), 'a');
328
- try {
329
- const child = spawn(process.execPath, [cliEntryPoint(), 'run', '--daemon'], {
330
- cwd: PACKAGE_ROOT,
331
- env: { ...process.env, HERDR_REMOTE_SERVICE: '1', ...serviceEnvironment() },
332
- detached: true,
333
- stdio: ['ignore', logFd, logFd],
334
- });
335
- child.unref();
336
- writeJsonAtomic(fallbackPidPath(), { pid: child.pid, startedAt: new Date().toISOString() });
337
- return { ok: true, pid: child.pid, note: 'this fallback does not survive a reboot' };
338
- } finally {
339
- fs.closeSync(logFd);
340
- }
341
- }
342
-
343
- function fallbackUninstall() {
344
- const pid = readFallbackPid();
345
- if (pidAlive(pid)) {
346
- try { process.kill(pid, 'SIGTERM'); } catch {}
347
- }
348
- try { fs.rmSync(fallbackPidPath(), { force: true }); } catch {}
349
- return { ok: true, stoppedPid: pid };
350
- }
351
-
352
- // ---------------------------------------------------------------------------
353
- // Public surface
354
- // ---------------------------------------------------------------------------
355
-
356
- function resolveManager(config = loadConfig()) {
357
- return detectManager(config.keepalive?.manager || 'auto');
358
- }
359
-
360
- function status(config = loadConfig()) {
361
- const manager = resolveManager(config);
362
- if (manager === 'none') return { manager: 'none', installed: false, active: false, enabled: false };
363
- if (manager === 'systemd') return systemdStatus();
364
- if (manager === 'launchd') return launchdStatus();
365
- return fallbackStatus();
366
- }
367
-
368
- function install(config = loadConfig()) {
369
- const manager = resolveManager(config);
370
- if (manager === 'systemd') return { manager, ...systemdInstall() };
371
- if (manager === 'launchd') return { manager, ...launchdInstall() };
372
- if (manager === 'none') throw new Error('keep-alive is disabled in the configuration');
373
- return { manager: 'supervisor', ...fallbackInstall() };
374
- }
375
-
376
- function uninstall(config = loadConfig()) {
377
- const manager = resolveManager(config);
378
- if (manager === 'systemd') return { manager, ...systemdUninstall() };
379
- if (manager === 'launchd') return { manager, ...launchdUninstall() };
380
- return { manager: 'supervisor', ...fallbackUninstall() };
381
- }
382
-
383
- /** Restart whatever manages the services, so config edits take effect. */
384
- function restart(config = loadConfig()) {
385
- const manager = resolveManager(config);
386
- if (manager === 'systemd') {
387
- const result = systemctl(['restart', SYSTEMD_UNIT_NAME]);
388
- if (result.status !== 0) throw new Error(String(result.stderr || '').trim() || 'systemctl restart failed');
389
- return { manager, ok: true };
390
- }
391
- if (manager === 'launchd') {
392
- spawnSync('launchctl', ['kickstart', '-k', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
393
- return { manager, ok: true };
394
- }
395
- fallbackUninstall();
396
- return { manager: 'supervisor', ...fallbackInstall() };
397
- }
398
-
399
- /**
400
- * Stop the managed services. Killing the child pids directly would only make
401
- * the service manager start them again, so an installed manager is asked to
402
- * stop instead.
403
- */
404
- function stopManaged(config = loadConfig()) {
405
- const current = status(config);
406
- if (!current.installed && !current.active) return { managed: false };
407
- if (current.manager === 'systemd') {
408
- systemctl(['stop', SYSTEMD_UNIT_NAME]);
409
- return { managed: true, manager: 'systemd' };
410
- }
411
- if (current.manager === 'launchd') {
412
- spawnSync('launchctl', ['bootout', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
413
- return { managed: true, manager: 'launchd' };
414
- }
415
- if (current.manager === 'supervisor' && current.active) {
416
- fallbackUninstall();
417
- return { managed: true, manager: 'supervisor' };
418
- }
419
- return { managed: false };
420
- }
421
-
422
- function enableLinger() {
423
- const username = os.userInfo().username;
424
- const result = spawnSync('loginctl', ['enable-linger', username], { encoding: 'utf8' });
425
- if (result.status !== 0) {
426
- throw new Error(String(result.stderr || '').trim() || 'loginctl enable-linger failed');
427
- }
428
- return { ok: true, username };
429
- }
430
-
431
- function logsHint(config = loadConfig()) {
432
- const manager = resolveManager(config);
433
- if (manager === 'systemd') return `journalctl --user -u ${SYSTEMD_UNIT_NAME} -f`;
434
- if (manager === 'launchd') return `tail -f ${logPath('supervisor')}`;
435
- return `tail -f ${logPath('supervisor')}`;
436
- }
437
-
438
- module.exports = {
439
- SYSTEMD_UNIT_NAME,
440
- LAUNCHD_LABEL,
441
- cliEntryPoint,
442
- systemdUnitPath,
443
- launchdPlistPath,
444
- fallbackPidPath,
445
- detectManager,
446
- resolveManager,
447
- renderSystemdUnit,
448
- renderLaunchdPlist,
449
- escapeXml,
450
- servicePath,
451
- serviceEnvironment,
452
- systemdEnvironmentLine,
453
- status,
454
- install,
455
- uninstall,
456
- restart,
457
- stopManaged,
458
- enableLinger,
459
- logsHint,
460
- };
package/src/lifecycle.js DELETED
@@ -1,62 +0,0 @@
1
- 'use strict';
2
-
3
- // Start/stop/restart that respect the keep-alive manager.
4
- //
5
- // When systemd or launchd supervises the services, killing the processes just
6
- // makes the manager start them again — and starting them by hand produces a
7
- // second, unmanaged copy fighting for the port. Every entry point goes through
8
- // here so the managed and unmanaged paths behave the same from the outside.
9
-
10
- const { loadConfig } = require('./config');
11
- const keepalive = require('./keepalive');
12
- const { restartServices, startServices, statusServices, stopServices } = require('./service');
13
-
14
- function managerInUse(config) {
15
- const status = keepalive.status(config);
16
- return status.installed || status.active ? status : null;
17
- }
18
-
19
- function startAll(config = loadConfig()) {
20
- const managed = managerInUse(config);
21
- if (managed) {
22
- // Start means "make it run", not "restart it". Herdr runs this through the
23
- // plugin's startup hook every time a Herdr server starts, and a restart
24
- // there dropped every browser attached to the workstation — including the
25
- // one that had just asked for that Herdr to be started.
26
- if (!managed.active) keepalive.restart(config);
27
- return { ok: true, managed: true, manager: managed.manager, alreadyRunning: Boolean(managed.active) };
28
- }
29
- return { ...startServices(), managed: false };
30
- }
31
-
32
- function stopAll(config = loadConfig()) {
33
- const result = keepalive.stopManaged(config);
34
- if (result.managed) {
35
- // The manager stops the supervisor, which stops its children; clear the
36
- // recorded pids so status does not report ghosts.
37
- stopServices();
38
- return { ok: true, managed: true, manager: result.manager };
39
- }
40
- return { ...stopServices(), managed: false };
41
- }
42
-
43
- function restartAll(config = loadConfig()) {
44
- const managed = managerInUse(config);
45
- if (managed) {
46
- keepalive.restart(config);
47
- return { ok: true, managed: true, manager: managed.manager };
48
- }
49
- return { ...restartServices(), managed: false };
50
- }
51
-
52
- /** Service status plus the keep-alive picture, which is what the TUI shows. */
53
- async function fullStatus(config = loadConfig()) {
54
- const [services, keepaliveStatus] = [await statusServices(), keepalive.status(config)];
55
- return {
56
- ...services,
57
- keepalive: keepaliveStatus,
58
- logsHint: keepalive.logsHint(config),
59
- };
60
- }
61
-
62
- module.exports = { startAll, stopAll, restartAll, fullStatus, managerInUse };
@@ -1,76 +0,0 @@
1
- 'use strict';
2
-
3
- const os = require('node:os');
4
-
5
- // Interface names that are almost always virtual bridges rather than something
6
- // a phone can reach. They are still listed, just ranked last.
7
- const VIRTUAL_NAME_PATTERN = /^(docker|br-|virbr|veth|vmnet|vboxnet|lxcbr|cni|flannel|kube)/i;
8
- const TAILSCALE_NAME_PATTERN = /^(tailscale|ts)\d*$/i;
9
-
10
- /**
11
- * Is this address inside Tailscale's 100.64.0.0/10 CGNAT range? Interface names
12
- * differ per platform (tailscale0 on Linux, utunN on macOS), so the address
13
- * range is the portable signal.
14
- */
15
- function isTailscaleAddress(address) {
16
- const octets = String(address).split('.');
17
- if (octets.length !== 4) return false;
18
- const first = Number(octets[0]);
19
- const second = Number(octets[1]);
20
- if (!Number.isInteger(first) || !Number.isInteger(second)) return false;
21
- return first === 100 && second >= 64 && second <= 127;
22
- }
23
-
24
- function classify(name, info) {
25
- if (info.internal) return 'loopback';
26
- if (TAILSCALE_NAME_PATTERN.test(name) || isTailscaleAddress(info.address)) return 'tailscale';
27
- if (VIRTUAL_NAME_PATTERN.test(name)) return 'virtual';
28
- return 'lan';
29
- }
30
-
31
- const KIND_ORDER = { tailscale: 0, lan: 1, virtual: 2, loopback: 3 };
32
-
33
- /**
34
- * Addresses this machine can be reached at, best candidate first.
35
- *
36
- * Tailscale addresses come first because a device on the tailnet reaches them
37
- * from anywhere, which is exactly the "works away from home without running a
38
- * relay" case; plain LAN addresses follow, then virtual bridges, then loopback.
39
- */
40
- function listReachableAddresses({ includeLoopback = true, includeIpv6 = false } = {}) {
41
- const interfaces = os.networkInterfaces();
42
- const results = [];
43
- for (const [name, entries] of Object.entries(interfaces)) {
44
- for (const info of entries || []) {
45
- const family = typeof info.family === 'string' ? info.family : `IPv${info.family}`;
46
- if (family !== 'IPv4' && !(includeIpv6 && family === 'IPv6')) continue;
47
- const kind = classify(name, info);
48
- if (kind === 'loopback' && !includeLoopback) continue;
49
- results.push({
50
- name,
51
- address: info.address,
52
- family,
53
- kind,
54
- internal: Boolean(info.internal),
55
- });
56
- }
57
- }
58
- results.sort((a, b) => {
59
- const byKind = KIND_ORDER[a.kind] - KIND_ORDER[b.kind];
60
- if (byKind !== 0) return byKind;
61
- return a.address.localeCompare(b.address);
62
- });
63
- return results;
64
- }
65
-
66
- /** Best guess for the address to advertise when binding to every interface. */
67
- function preferredLanAddress() {
68
- const candidates = listReachableAddresses({ includeLoopback: false });
69
- return candidates.length > 0 ? candidates[0].address : null;
70
- }
71
-
72
- module.exports = {
73
- isTailscaleAddress,
74
- listReachableAddresses,
75
- preferredLanAddress,
76
- };