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/updater.js DELETED
@@ -1,394 +0,0 @@
1
- 'use strict';
2
-
3
- // Self-update for the npm-installed CLI.
4
- //
5
- // The update path is only meaningful for a package installed from npm. A source
6
- // checkout is managed by git and a linked development copy by the developer, so
7
- // this refuses to touch either: silently running `npm install -g` over a
8
- // checkout would replace the tree someone is working in.
9
-
10
- const fs = require('node:fs');
11
- const os = require('node:os');
12
- const path = require('node:path');
13
- const { spawn } = require('node:child_process');
14
- const { PACKAGE_ROOT } = require('./config');
15
-
16
- const PACKAGE_NAME = 'herdr-remote';
17
- const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
18
-
19
- /**
20
- * A public mirror, tried last.
21
- *
22
- * `registry.npmjs.org` is not reachable from every network this runs on — in
23
- * mainland China it usually is not — and "could not reach npm registry" on a
24
- * machine that installs packages perfectly well is a bug report, not a
25
- * diagnosis. The mirror is read-only and only ever asked for a version number.
26
- */
27
- const MIRROR_REGISTRY = 'https://registry.npmmirror.com';
28
-
29
- function currentVersion() {
30
- try {
31
- return require(path.join(PACKAGE_ROOT, 'package.json')).version;
32
- } catch {
33
- return '0.0.0';
34
- }
35
- }
36
-
37
- /**
38
- * How this copy got here.
39
- *
40
- * - `npm` installed from the registry; updating is `npm install -g`.
41
- * - `linked` `npm link`ed into a global tree from a working copy.
42
- * - `source` run straight out of a checkout.
43
- *
44
- * The distinction is drawn from the path rather than from npm, which would
45
- * mean shelling out on every render.
46
- */
47
- function installKind() {
48
- const root = PACKAGE_ROOT;
49
- // A real install always lives inside a node_modules tree.
50
- if (!root.split(path.sep).includes('node_modules')) return 'source';
51
- try {
52
- // `npm link` leaves a symlink where a published install has a directory.
53
- if (fs.lstatSync(root).isSymbolicLink()) return 'linked';
54
- } catch {
55
- // Unreadable: treat as a normal install and let npm report the problem.
56
- }
57
- return 'npm';
58
- }
59
-
60
- /** `HERDR_REMOTE_UPDATE_CHECK=0` turns the automatic checks off (offline machines, tests). */
61
- function updateChecksEnabled(env = process.env) {
62
- return env.HERDR_REMOTE_UPDATE_CHECK !== '0';
63
- }
64
-
65
- function canSelfUpdate() {
66
- return installKind() === 'npm';
67
- }
68
-
69
- function normalizeRegistry(value) {
70
- if (typeof value !== 'string') return null;
71
- const trimmed = value.trim().replace(/\/+$/, '');
72
- if (!/^https?:\/\//i.test(trimmed)) return null;
73
- return trimmed;
74
- }
75
-
76
- /** The `registry=` line from an npmrc file, if it has one. */
77
- function registryFromNpmrc(filePath) {
78
- let contents;
79
- try {
80
- contents = fs.readFileSync(filePath, 'utf8');
81
- } catch {
82
- return null;
83
- }
84
- let found = null;
85
- for (const rawLine of contents.split(/\r?\n/)) {
86
- const line = rawLine.trim();
87
- if (!line || line.startsWith('#') || line.startsWith(';')) continue;
88
- const match = /^registry\s*=\s*(.+)$/i.exec(line);
89
- // The last assignment wins, as it does for npm itself.
90
- if (match) found = normalizeRegistry(match[1].replace(/^["']|["']$/g, ''));
91
- }
92
- return found;
93
- }
94
-
95
- /**
96
- * Where to ask, in the order to ask.
97
- *
98
- * Whatever npm itself is configured to use comes first: a machine behind a
99
- * corporate proxy or on a mirror has already answered this question, and
100
- * ignoring that answer is what made the check fail on a machine where
101
- * `npm install` works. The public registry and then a public mirror follow, so
102
- * a private registry that does not carry this package is not the end of it.
103
- */
104
- function registryCandidates({ env = process.env, home = os.homedir(), cwd = process.cwd() } = {}) {
105
- const candidates = [];
106
- const add = (value) => {
107
- const normalized = normalizeRegistry(value);
108
- if (normalized && !candidates.includes(normalized)) candidates.push(normalized);
109
- };
110
-
111
- // npm exports its whole config into the environment of anything it runs.
112
- add(env.npm_config_registry);
113
- add(env.NPM_CONFIG_REGISTRY);
114
- add(env.HERDR_REMOTE_REGISTRY);
115
- add(registryFromNpmrc(path.join(cwd, '.npmrc')));
116
- add(registryFromNpmrc(path.join(home, '.npmrc')));
117
- add(DEFAULT_REGISTRY);
118
- add(MIRROR_REGISTRY);
119
- return candidates;
120
- }
121
-
122
- /** Compare two `MAJOR.MINOR.PATCH` strings. Returns 1, -1 or 0. */
123
- function compareVersions(a, b) {
124
- const parse = (value) => String(value)
125
- .split('-')[0]
126
- .split('.')
127
- .map((part) => Number.parseInt(part, 10) || 0);
128
- const left = parse(a);
129
- const right = parse(b);
130
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
131
- const diff = (left[index] || 0) - (right[index] || 0);
132
- if (diff > 0) return 1;
133
- if (diff < 0) return -1;
134
- }
135
- return 0;
136
- }
137
-
138
- /** One JSON request with a deadline. Never throws. */
139
- async function fetchJson(url, { timeoutMs, fetchImpl }) {
140
- const controller = new AbortController();
141
- const timer = setTimeout(() => controller.abort(), timeoutMs);
142
- try {
143
- const response = await fetchImpl(url, {
144
- signal: controller.signal,
145
- headers: { Accept: 'application/json', 'Cache-Control': 'no-cache' },
146
- });
147
- if (!response.ok) return { ok: false, message: `HTTP ${response.status ?? '?'}` };
148
- return { ok: true, body: await response.json() };
149
- } catch (error) {
150
- return { ok: false, message: error?.name === 'AbortError' ? 'timed out' : String(error?.message || error) };
151
- } finally {
152
- clearTimeout(timer);
153
- }
154
- }
155
-
156
- /**
157
- * One registry, one question: what is the latest published version?
158
- *
159
- * The dist-tags endpoint first: a few bytes, answered fresh by npmjs and
160
- * npmmirror alike. `/<package>/latest` is the fallback for registries without
161
- * it, asked for plain JSON. It used to be asked for npm's abbreviated install
162
- * format, which npmjs refuses on that path with a 406, so every check quietly
163
- * fell through to a mirror that had not synced the release yet.
164
- */
165
- async function askRegistry(registry, { timeoutMs, fetchImpl }) {
166
- const endpoints = [
167
- { url: `${registry}/-/package/${PACKAGE_NAME}/dist-tags`, read: (body) => body?.latest },
168
- { url: `${registry}/${PACKAGE_NAME}/latest`, read: (body) => body?.version },
169
- ];
170
- let message = null;
171
- for (const endpoint of endpoints) {
172
- const attempt = await fetchJson(endpoint.url, { timeoutMs, fetchImpl });
173
- if (!attempt.ok) {
174
- message = attempt.message;
175
- continue;
176
- }
177
- const latest = endpoint.read(attempt.body);
178
- if (typeof latest === 'string' && latest) return { ok: true, latest };
179
- message = 'registry answered without a version';
180
- }
181
- return { ok: false, message };
182
- }
183
-
184
- /**
185
- * Ask what the current release is.
186
- *
187
- * Never throws: an update check is a convenience, and a machine that is offline
188
- * or behind a proxy should still get a working settings screen.
189
- *
190
- * Every registry is asked at once and the newest answer wins. Taking the first
191
- * answer let a mirror a few minutes (or hours) behind report the release before
192
- * last as current. `sources` lists the registries that already carry the
193
- * newest version, preferred order first, which is where an install should come
194
- * from; `behind` lists the ones that do not, so the screen can say why.
195
- */
196
- async function checkForUpdate({
197
- timeoutMs = 6000,
198
- fetchImpl = globalThis.fetch,
199
- registries = registryCandidates(),
200
- } = {}) {
201
- const current = currentVersion();
202
- const attempts = await Promise.all(registries.map(async (registry) => ({
203
- registry,
204
- ...(await askRegistry(registry, { timeoutMs, fetchImpl })),
205
- })));
206
- const answers = attempts.filter((attempt) => attempt.ok);
207
-
208
- if (answers.length === 0) {
209
- // The reason is carried out with the failure. "Could not reach npm
210
- // registry" with nothing after it leaves the user guessing between DNS, a
211
- // proxy, a firewall and a mirror that does not carry the package.
212
- return {
213
- ok: false,
214
- current,
215
- errorKey: 'update.errorNetwork',
216
- message: attempts.map((attempt) => `${attempt.registry}: ${attempt.message}`).join('; '),
217
- triedRegistries: registries,
218
- };
219
- }
220
-
221
- const latest = answers
222
- .map((answer) => answer.latest)
223
- .reduce((best, version) => (compareVersions(version, best) > 0 ? version : best));
224
- const sources = answers
225
- .filter((answer) => compareVersions(answer.latest, latest) === 0)
226
- .map((answer) => answer.registry);
227
- const behind = answers
228
- .filter((answer) => compareVersions(answer.latest, latest) < 0)
229
- .map((answer) => ({ registry: answer.registry, version: answer.latest }));
230
-
231
- return {
232
- ok: true,
233
- current,
234
- latest,
235
- registry: sources[0],
236
- sources,
237
- behind,
238
- updateAvailable: compareVersions(latest, current) > 0,
239
- };
240
- }
241
-
242
- /** Just the lines npm meant for a person: its `npm error` / `npm ERR!` summary. */
243
- function npmErrorSummary(output) {
244
- const lines = String(output || '').split(/\r?\n/)
245
- .map((line) => line.trim())
246
- .filter((line) => /^npm (error|ERR!)/.test(line) && !/complete log of this run/i.test(line))
247
- .map((line) => line.replace(/^npm (error|ERR!)\s*/, ''))
248
- .filter(Boolean);
249
- return (lines.length > 0 ? lines.slice(0, 3) : String(output || '').trim().split(/\r?\n/).slice(-3)).join(' ');
250
- }
251
-
252
- /** "No matching version": the registry has not caught up with its own dist-tag. */
253
- function isNotYetPublished(output) {
254
- return /\bETARGET\b|notarget|No matching version found/i.test(String(output || ''));
255
- }
256
-
257
- /** What is actually installed now, read from disk rather than the require cache. */
258
- function installedVersionOnDisk() {
259
- try {
260
- return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
261
- } catch {
262
- return null;
263
- }
264
- }
265
-
266
- function runNpm(spawnImpl, args, timeoutMs) {
267
- return new Promise((resolve) => {
268
- let child;
269
- try {
270
- child = spawnImpl('npm', args, { timeout: timeoutMs });
271
- } catch (error) {
272
- resolve({ ok: false, spawnFailed: true, output: String(error?.message || error) });
273
- return;
274
- }
275
- let output = '';
276
- let settled = false;
277
- const finish = (result) => { if (!settled) { settled = true; resolve(result); } };
278
- const collect = (chunk) => { output += String(chunk); };
279
- child.stdout?.on('data', collect);
280
- child.stderr?.on('data', collect);
281
- child.on('error', (error) => finish({ ok: false, spawnFailed: true, output: error.message }));
282
- child.on('close', (code) => finish({ ok: code === 0, output: output.trim() }));
283
- });
284
- }
285
-
286
- const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
287
-
288
- /**
289
- * Install a release over this one.
290
- *
291
- * Resolves with `{ ok, output }` rather than rejecting, so the caller can show
292
- * npm's own message; npm's diagnostics are more useful than anything that could
293
- * be invented here.
294
- *
295
- * - The exact version the check found is installed, not `@latest`, from a
296
- * registry that reported it, with `--prefer-online` so npm revalidates the
297
- * package metadata it cached before the release existed.
298
- * - Right after a release, npm's view of the package can still lack the new
299
- * version although its dist-tag already points at it: npm answers ETARGET.
300
- * That is a wait, not a failure, so it is retried a few times, then the next
301
- * registry that carries the version is tried.
302
- * - Success is what is on disk afterwards, not npm's exit code.
303
- *
304
- * @param {{
305
- * spawnImpl?: Function,
306
- * timeoutMs?: number,
307
- * installKindImpl?: () => string,
308
- * registry?: string,
309
- * sources?: string[],
310
- * version?: string | null,
311
- * attempts?: number,
312
- * retryDelayMs?: number,
313
- * sleep?: (ms: number) => Promise<unknown>,
314
- * readInstalledVersion?: () => string | null,
315
- * onAttempt?: (progress: { registry: string, attempt: number }) => void,
316
- * }} [options]
317
- * @returns {Promise<{ ok: boolean, errorKey?: string, installed?: string | null, output?: string, summary?: string }>}
318
- */
319
- async function performUpdate({
320
- spawnImpl = spawn,
321
- timeoutMs = 180_000,
322
- // Seam for tests: the suite runs from a checkout, where the guard below is
323
- // correctly the only reachable outcome.
324
- installKindImpl = installKind,
325
- // The registries that reported `version`, preferred first. Empty means
326
- // "whatever npm is configured with", which is right when no check has run.
327
- registry = '',
328
- sources = [],
329
- version = null,
330
- attempts = 3,
331
- retryDelayMs = 15_000,
332
- sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
333
- readInstalledVersion = installedVersionOnDisk,
334
- onAttempt = () => {},
335
- } = {}) {
336
- const kind = installKindImpl();
337
- if (kind !== 'npm') return { ok: false, errorKey: `update.cannot.${kind}` };
338
-
339
- const target = typeof version === 'string' && VERSION_PATTERN.test(version) ? version : null;
340
- const spec = `${PACKAGE_NAME}@${target || 'latest'}`;
341
- const registries = [];
342
- for (const candidate of [registry, ...sources]) {
343
- const normalized = normalizeRegistry(candidate);
344
- if (normalized && !registries.includes(normalized)) registries.push(normalized);
345
- }
346
- if (registries.length === 0) registries.push('');
347
-
348
- let last = { output: '' };
349
- let notYetPublished = false;
350
- for (const source of registries) {
351
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
352
- onAttempt({ registry: source, attempt });
353
- const args = ['install', '-g', spec, '--prefer-online'];
354
- if (source) args.push('--registry', source);
355
- const result = await runNpm(spawnImpl, args, timeoutMs);
356
- if (result.ok) {
357
- const installed = readInstalledVersion();
358
- if (target && installed && compareVersions(installed, target) < 0) {
359
- return { ok: false, errorKey: 'update.errorNotApplied', installed, output: result.output.slice(-2000) };
360
- }
361
- return { ok: true, installed, output: result.output.slice(-2000) };
362
- }
363
- last = result;
364
- if (result.spawnFailed) {
365
- return { ok: false, errorKey: 'update.errorFailed', output: result.output, summary: result.output };
366
- }
367
- notYetPublished = isNotYetPublished(result.output);
368
- if (!notYetPublished) break; // Not a wait: this registry will not do better.
369
- if (attempt < attempts) await sleep(retryDelayMs);
370
- }
371
- }
372
- return {
373
- ok: false,
374
- errorKey: notYetPublished ? 'update.errorNotYetPublished' : 'update.errorFailed',
375
- output: String(last.output || '').slice(-2000),
376
- summary: npmErrorSummary(last.output),
377
- };
378
- }
379
-
380
- module.exports = {
381
- PACKAGE_NAME,
382
- DEFAULT_REGISTRY,
383
- MIRROR_REGISTRY,
384
- canSelfUpdate,
385
- checkForUpdate,
386
- compareVersions,
387
- currentVersion,
388
- installKind,
389
- installedVersionOnDisk,
390
- npmErrorSummary,
391
- performUpdate,
392
- registryCandidates,
393
- updateChecksEnabled,
394
- };