herdr-remote 0.2.20 → 0.2.22

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 (46) hide show
  1. package/README.md +12 -2
  2. package/README.zh-CN.md +11 -1
  3. package/dist/cli.js +23 -4
  4. package/dist/config.js +8 -1
  5. package/dist/herdr-api.js +5 -3
  6. package/dist/herdr-command.js +32 -15
  7. package/dist/herdr-plugin.js +5 -1
  8. package/dist/herdr-server.js +3 -2
  9. package/dist/i18n/en.js +20 -0
  10. package/dist/i18n/zh.js +20 -0
  11. package/dist/keepalive/detached.js +13 -3
  12. package/dist/keepalive/index.js +16 -8
  13. package/dist/keepalive/launchd.js +4 -4
  14. package/dist/keepalive/systemd.js +8 -4
  15. package/dist/keepalive/windows-service-setup.js +129 -0
  16. package/dist/keepalive/windows-service.js +272 -0
  17. package/dist/lifecycle.js +15 -1
  18. package/dist/net-interfaces.js +15 -6
  19. package/dist/paths.js +16 -4
  20. package/dist/pty-session.js +10 -2
  21. package/dist/relay-updater.js +81 -0
  22. package/dist/service.js +2 -0
  23. package/dist/socket-discovery.js +15 -3
  24. package/dist/supervisor.js +1 -0
  25. package/dist/terminal-font/deps.js +11 -1
  26. package/dist/terminal-font/files.js +13 -10
  27. package/dist/terminal-font/readers.js +60 -3
  28. package/dist/terminal-font/terminals.js +3 -0
  29. package/dist/terminal-palette.js +78 -18
  30. package/dist/tui/App.js +34 -8
  31. package/dist/tui/api.js +6 -4
  32. package/dist/tui/screens/About.js +30 -15
  33. package/dist/tui/screens/Herdr.js +5 -4
  34. package/dist/tui/screens/Keepalive.js +25 -14
  35. package/dist/tui/screens/Pair.js +1 -1
  36. package/dist/tui/screens/Relay.js +9 -8
  37. package/dist/tui/screens/Services.js +4 -4
  38. package/dist/tui/screens/Wizard.js +11 -5
  39. package/dist/tui/screens/useRelayUpdate.js +122 -0
  40. package/dist/updater/npm.js +62 -0
  41. package/dist/updater.js +8 -37
  42. package/herdr-plugin.toml +2 -2
  43. package/package.json +2 -1
  44. package/vendor/winsw/LICENSE +21 -0
  45. package/vendor/winsw/README.md +9 -0
  46. package/vendor/winsw/WinSW.NET461.exe +0 -0
@@ -20,8 +20,9 @@ export function HerdrScreen({ ctx }) {
20
20
  ];
21
21
  const applyField = (id, value) => {
22
22
  const result = setField(draft, id, value);
23
- if (result.errorKey) {
24
- ctx.notify(t(result.errorKey), 'error');
23
+ const { errorKey } = result;
24
+ if (errorKey) {
25
+ ctx.notify((t) => t(errorKey), 'error');
25
26
  return false;
26
27
  }
27
28
  ctx.updateDraft(result.draft);
@@ -31,10 +32,10 @@ export function HerdrScreen({ ctx }) {
31
32
  try {
32
33
  const result = saveDraft(draft);
33
34
  ctx.reloadConfig();
34
- ctx.notify(t('common.saved', { path: result.path }), 'success');
35
+ ctx.notify((t) => t('common.saved', { path: result.path }), 'success');
35
36
  }
36
37
  catch (error) {
37
- ctx.notify(t('error.saveFailed', { message: error.message }), 'error');
38
+ ctx.notify((t) => t('error.saveFailed', { message: error.message }), 'error');
38
39
  }
39
40
  };
40
41
  const activate = (id) => {
@@ -4,8 +4,7 @@ import { Box, Text, useInput } from 'ink';
4
4
  import { theme } from '../theme.js';
5
5
  import { Message, Panel, Row, Selectable, StatusDot } from '../components/common.js';
6
6
  import { ChoiceList } from './Relay.js';
7
- import { keepalive, saveDraft, setField } from '../api.js';
8
- const MANAGERS = ['auto', 'systemd', 'launchd', 'supervisor', 'none'];
7
+ import { KEEPALIVE_MANAGERS, keepalive, saveDraft, setField } from '../api.js';
9
8
  export function Keepalive({ ctx }) {
10
9
  const { t, status, draft, editingId } = ctx;
11
10
  const [selected, setSelected] = useState('install');
@@ -24,25 +23,36 @@ export function Keepalive({ ctx }) {
24
23
  ctx.setEditing('keepaliveManager');
25
24
  return;
26
25
  }
27
- ctx.run(() => {
26
+ ctx.run(async () => {
28
27
  if (id === 'install') {
29
- const result = keepalive.install(ctx.config);
30
- ctx.notify(t('keepalive.installed', { manager: result.manager }), 'success');
28
+ if (keepalive.detectManager(ctx.config.keepalive.manager) === 'windows-service')
29
+ ctx.notify((t) => t('keepalive.serviceWindowHint'), 'info');
30
+ try {
31
+ const result = await keepalive.install(ctx.config);
32
+ ctx.notify((t) => t('keepalive.installed', { manager: result.manager }), 'success');
33
+ }
34
+ catch (error) {
35
+ if (error.code === 'ELEVATION_CANCELLED') {
36
+ ctx.notify((t) => t('keepalive.elevationCancelled'), 'info');
37
+ return;
38
+ }
39
+ throw error;
40
+ }
31
41
  return;
32
42
  }
33
43
  if (id === 'uninstall') {
34
- keepalive.uninstall(ctx.config);
35
- ctx.notify(t('keepalive.uninstalled'), 'success');
44
+ await keepalive.uninstall(ctx.config);
45
+ ctx.notify((t) => t('keepalive.uninstalled'), 'success');
36
46
  return;
37
47
  }
38
48
  if (id === 'restart') {
39
49
  keepalive.restart(ctx.config);
40
- ctx.notify(t('keepalive.restarted'), 'success');
50
+ ctx.notify((t) => t('keepalive.restarted'), 'success');
41
51
  return;
42
52
  }
43
53
  if (id === 'linger') {
44
54
  const result = keepalive.enableLinger();
45
- ctx.notify(t('keepalive.lingerDone', { username: result.username }), 'success');
55
+ ctx.notify((t) => t('keepalive.lingerDone', { username: result.username }), 'success');
46
56
  }
47
57
  });
48
58
  };
@@ -56,10 +66,11 @@ export function Keepalive({ ctx }) {
56
66
  activate(selected);
57
67
  }, { isActive: editingId === null });
58
68
  if (editingId === 'keepaliveManager') {
59
- return (_jsx(Panel, { title: t('field.keepalive'), children: _jsx(ChoiceList, { options: MANAGERS.map((manager) => ({ id: manager, label: manager })), current: draft.keepalive.manager, onPick: (manager) => {
69
+ return (_jsx(Panel, { title: t('field.keepalive'), children: _jsx(ChoiceList, { options: KEEPALIVE_MANAGERS.map((manager) => ({ id: manager, label: manager })), current: draft.keepalive.manager, onPick: (manager) => {
60
70
  const result = setField(draft, 'keepaliveManager', manager);
61
- if (result.errorKey) {
62
- ctx.notify(t(result.errorKey), 'error');
71
+ const { errorKey } = result;
72
+ if (errorKey) {
73
+ ctx.notify((t) => t(errorKey), 'error');
63
74
  return;
64
75
  }
65
76
  ctx.updateDraft(result.draft);
@@ -68,7 +79,7 @@ export function Keepalive({ ctx }) {
68
79
  ctx.reloadConfig();
69
80
  }
70
81
  catch (error) {
71
- ctx.notify(t('error.saveFailed', { message: error.message }), 'error');
82
+ ctx.notify((t) => t('error.saveFailed', { message: error.message }), 'error');
72
83
  }
73
84
  ctx.setEditing(null);
74
85
  }, onCancel: () => ctx.setEditing(null) }) }));
@@ -80,5 +91,5 @@ export function Keepalive({ ctx }) {
80
91
  : t('common.notInstalled')] })] }) }), current?.unitPath ? (_jsx(Row, { label: t('keepalive.unitPath'), children: _jsx(Text, { color: theme.muted, children: current.unitPath }) })) : null, _jsx(Box, { marginTop: 1, flexDirection: "column", children: entries.map((entry) => (_jsx(Selectable, { selected: selected === entry.id, onSelect: () => {
81
92
  setSelected(entry.id);
82
93
  activate(entry.id);
83
- }, onHover: () => setSelected(entry.id), children: entry.label }, entry.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [current?.manager === 'systemd' ? (_jsx(Text, { color: current.linger ? theme.muted : theme.warn, children: current.linger ? t('keepalive.lingerEnabled') : t('keepalive.lingerDisabled') })) : null, current?.manager === 'supervisor' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.fallbackNote') })) : null, _jsx(Text, { color: theme.muted, children: t('keepalive.logsHint', { command: status?.logsHint ?? '' }) })] }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
94
+ }, onHover: () => setSelected(entry.id), children: entry.label }, entry.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [current?.manager === 'systemd' ? (_jsx(Text, { color: current.linger ? theme.muted : theme.warn, children: current.linger ? t('keepalive.lingerEnabled') : t('keepalive.lingerDisabled') })) : null, current?.manager === 'supervisor' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.fallbackNote') })) : null, current?.manager === 'windows-service' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.serviceNote') })) : null, _jsx(Text, { color: theme.muted, children: t('keepalive.logsHint', { command: status?.logsHint ?? '' }) })] }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
84
95
  }
@@ -43,7 +43,7 @@ export function PairScreen({ ctx }) {
43
43
  };
44
44
  }, [pairing, columns]);
45
45
  const generate = () => run(async () => {
46
- ctx.notify(t('pair.working'));
46
+ ctx.notify((t) => t('pair.working'));
47
47
  try {
48
48
  const result = await pair();
49
49
  const code = extractPairingCode(result);
@@ -65,8 +65,9 @@ export function RelayScreen({ ctx }) {
65
65
  let next = draft;
66
66
  for (const [id, value] of updates) {
67
67
  const result = setField(next, id, value);
68
- if (result.errorKey) {
69
- ctx.notify(t(result.errorKey), 'error');
68
+ const { errorKey } = result;
69
+ if (errorKey) {
70
+ ctx.notify((t) => t(errorKey), 'error');
70
71
  return false;
71
72
  }
72
73
  next = result.draft;
@@ -80,11 +81,11 @@ export function RelayScreen({ ctx }) {
80
81
  try {
81
82
  const result = saveDraft(draft);
82
83
  ctx.reloadConfig();
83
- ctx.notify(t('common.saved', { path: result.path }), 'success');
84
+ ctx.notify((t) => t('common.saved', { path: result.path }), 'success');
84
85
  }
85
86
  catch (error) {
86
87
  const problems = error.problems;
87
- ctx.notify(problems
88
+ ctx.notify((t) => problems
88
89
  ? problems.map((key) => t(key)).join(' ')
89
90
  : t('error.saveFailed', { message: error.message }), 'error');
90
91
  }
@@ -94,7 +95,7 @@ export function RelayScreen({ ctx }) {
94
95
  if (!entry)
95
96
  return;
96
97
  if (entry.kind === 'readonly') {
97
- ctx.notify(t('relay.officialFixed'), 'info');
98
+ ctx.notify((t) => t('relay.officialFixed'), 'info');
98
99
  return;
99
100
  }
100
101
  if (entry.kind === 'field' || entry.kind === 'password') {
@@ -117,14 +118,14 @@ export function RelayScreen({ ctx }) {
117
118
  ctx.run(() => {
118
119
  regenerateHostIdentity();
119
120
  ctx.reloadConfig();
120
- ctx.notify(t('relay.regenerated'), 'success');
121
+ ctx.notify((t) => t('relay.regenerated'), 'success');
121
122
  });
122
123
  return;
123
124
  }
124
125
  if (id === 'test') {
125
126
  ctx.run(async () => {
126
127
  const result = await probeRelay(ctx.config);
127
- ctx.notify(result.ok
128
+ ctx.notify((t) => result.ok
128
129
  ? t('relay.testOk', { version: result.version ?? '?', hosts: result.hosts ?? 0 })
129
130
  : t('relay.testFailed', { message: result.message ?? '' }), result.ok ? 'success' : 'error');
130
131
  });
@@ -193,7 +194,7 @@ export function RelayScreen({ ctx }) {
193
194
  setPassword(value);
194
195
  setRelayPassword(value);
195
196
  ctx.setEditing(null);
196
- ctx.notify(t('relay.passwordSaved'), 'success');
197
+ ctx.notify((t) => t('relay.passwordSaved'), 'success');
197
198
  }, onCancel: () => ctx.setEditing(null) })) : entry.kind === 'readonly' ? (
198
199
  // Fixed by us, so it is printed rather than offered for editing.
199
200
  _jsx(Text, { color: theme.muted, children: `${getField(draft, entry.id) || placeholderText(ctx, entry.id)} (${t('relay.locked')})` })) : entry.fieldKind === 'choice' ? (
@@ -18,27 +18,27 @@ export function Services({ ctx }) {
18
18
  // top of an unsaved draft silently applies the old settings — which reads
19
19
  // as "I changed the relay and it never took effect". Stopping is unaffected.
20
20
  if (ctx.dirty && id !== 'stop') {
21
- ctx.notify(t('services.unsavedBlocked'), 'error');
21
+ ctx.notify((t) => t('services.unsavedBlocked'), 'error');
22
22
  return;
23
23
  }
24
24
  return run(() => {
25
25
  const config = ctx.config;
26
26
  if (id === 'start') {
27
27
  const result = startAll(config);
28
- ctx.notify(result.managed
28
+ ctx.notify((t) => result.managed
29
29
  ? t('services.managedNotice', { manager: result.manager ?? '' })
30
30
  : t('services.started'), 'success');
31
31
  return;
32
32
  }
33
33
  if (id === 'stop') {
34
34
  const result = stopAll(config);
35
- ctx.notify(result.managed
35
+ ctx.notify((t) => result.managed
36
36
  ? t('services.managedNotice', { manager: result.manager ?? '' })
37
37
  : t('services.stopped'), 'success');
38
38
  return;
39
39
  }
40
40
  const result = restartAll(config);
41
- ctx.notify(result.managed
41
+ ctx.notify((t) => result.managed
42
42
  ? t('services.managedNotice', { manager: result.manager ?? '' })
43
43
  : t('services.restarted'), 'success');
44
44
  });
@@ -50,8 +50,9 @@ export function Wizard({ ctx, onDone }) {
50
50
  let next = draft;
51
51
  for (const [id, value] of updates) {
52
52
  const result = setField(next, id, value);
53
- if (result.errorKey) {
54
- ctx.notify(t(result.errorKey), 'error');
53
+ const { errorKey } = result;
54
+ if (errorKey) {
55
+ ctx.notify((t) => t(errorKey), 'error');
55
56
  return false;
56
57
  }
57
58
  next = result.draft;
@@ -96,13 +97,18 @@ export function Wizard({ ctx, onDone }) {
96
97
  let keepaliveInstalled = false;
97
98
  if (installKeepalive) {
98
99
  try {
99
- keepalive.install(finalConfig);
100
+ if (keepalive.detectManager(finalConfig.keepalive.manager) === 'windows-service')
101
+ ctx.notify((t) => t('keepalive.serviceWindowHint'), 'info');
102
+ await keepalive.install(finalConfig);
100
103
  keepaliveInstalled = true;
101
104
  }
102
105
  catch (error) {
103
106
  // Keep-alive is a convenience; a container without systemd should not
104
107
  // block a working setup.
105
- ctx.notify(t('keepalive.failed', { message: error.message }), 'error');
108
+ if (error.code === 'ELEVATION_CANCELLED')
109
+ ctx.notify((t) => t('keepalive.elevationCancelled'), 'info');
110
+ else
111
+ ctx.notify((t) => t('keepalive.failed', { message: error.message }), 'error');
106
112
  }
107
113
  }
108
114
  if (startNow && !keepaliveInstalled)
@@ -173,7 +179,7 @@ export function Wizard({ ctx, onDone }) {
173
179
  if (step === 'relayUrl') {
174
180
  return (_jsxs(Panel, { title: t('wizard.relayTitle'), children: [header, _jsx(FieldRow, { label: t('wizard.relayUrlLabel'), selected: true, onSelect: () => { }, children: _jsx(TextField, { value: draft.relay.remoteUrl, placeholder: "wss://relay.example.com", active: true, onSubmit: (value) => {
175
181
  if (!value) {
176
- ctx.notify(t('error.remoteUrlRequired'), 'error');
182
+ ctx.notify((t) => t('error.remoteUrlRequired'), 'error');
177
183
  return;
178
184
  }
179
185
  if (apply('remoteUrl', value))
@@ -0,0 +1,122 @@
1
+ // The relay row on the About tab: check for, install and switch to a newer
2
+ // relay. Only a machine that runs its own relay has one to update.
3
+ import { useEffect, useState } from 'react';
4
+ import { canSelfUpdate, checkForRelayUpdate, compareVersions, currentVersion, installKind, installedRelayVersion, performRelayUpdate, restartAll, runsLocalRelay, whileServicesStopped, } from '../api.js';
5
+ export function useRelayUpdate(ctx) {
6
+ const { t } = ctx;
7
+ const [state, setState] = useState({ phase: 'idle' });
8
+ const [source, setSource] = useState({
9
+ registry: '',
10
+ sources: [],
11
+ });
12
+ const visible = runsLocalRelay(ctx.config);
13
+ const installed = installedRelayVersion();
14
+ const health = ctx.status?.relay.health;
15
+ const running = typeof health?.version === 'string' ? health.version : null;
16
+ const busy = state.phase === 'checking' || state.phase === 'updating';
17
+ // Installed but not yet running: an update from here, or from npm by hand.
18
+ const restartNeeded = !busy &&
19
+ state.phase !== 'restarting' &&
20
+ !!installed &&
21
+ !!running &&
22
+ compareVersions(installed, running) > 0;
23
+ const applyCheck = (result) => {
24
+ setSource({ registry: result.registry ?? '', sources: result.sources ?? [] });
25
+ const latest = result.latest;
26
+ if (result.needsNewerCli)
27
+ setState({ phase: 'needsCli', latest });
28
+ else if (result.updateAvailable)
29
+ setState({ phase: 'available', latest });
30
+ else
31
+ setState({ phase: 'current', version: result.current });
32
+ };
33
+ // The TUI asked when it opened; start from that answer.
34
+ // biome-ignore lint/correctness/useExhaustiveDependencies: only a new answer from the opening check matters, not each render's applyCheck or phase
35
+ useEffect(() => {
36
+ if (ctx.relayUpdateCheck?.ok && state.phase === 'idle')
37
+ applyCheck(ctx.relayUpdateCheck);
38
+ }, [ctx.relayUpdateCheck]);
39
+ const check = async () => {
40
+ setState({ phase: 'checking' });
41
+ const result = await checkForRelayUpdate();
42
+ if (!result.ok) {
43
+ setState({ phase: 'error', messageKey: result.errorKey ?? 'update.errorNetwork' });
44
+ const { message } = result;
45
+ if (message)
46
+ ctx.notify((t) => t('update.errorNetworkDetail', { message }), 'error');
47
+ return;
48
+ }
49
+ ctx.setRelayUpdateCheck(result);
50
+ applyCheck(result);
51
+ };
52
+ const install = async (latest) => {
53
+ setState({ phase: 'updating', latest, attempt: 1 });
54
+ const result = await whileServicesStopped(ctx.config, () => performRelayUpdate({
55
+ ...source,
56
+ relayVersion: latest,
57
+ onAttempt: ({ attempt }) => setState({ phase: 'updating', latest, attempt }),
58
+ }));
59
+ if (!result.ok) {
60
+ const params = { version: currentVersion(), installed: result.installed ?? '' };
61
+ const messageKey = result.errorKey ?? 'update.errorFailed';
62
+ setState({ phase: 'error', messageKey, params });
63
+ const { summary } = result;
64
+ ctx.notify((t) => summary ? t('update.errorFailedDetail', { message: summary }) : t(messageKey, params), 'error');
65
+ return;
66
+ }
67
+ ctx.setRelayUpdateCheck(null);
68
+ setState({ phase: 'current', version: latest });
69
+ };
70
+ const restart = () => {
71
+ setState({ phase: 'restarting' });
72
+ ctx.run(() => {
73
+ restartAll(ctx.config);
74
+ const version = installed ?? '';
75
+ setState({ phase: 'current', version });
76
+ ctx.notify((t) => t('relayUpdate.restarted', { version }), 'success');
77
+ });
78
+ };
79
+ /** One row: restart onto a relay already installed, install one found, or check. */
80
+ const activate = () => {
81
+ if (busy || state.phase === 'restarting')
82
+ return;
83
+ if (restartNeeded) {
84
+ restart();
85
+ return;
86
+ }
87
+ if (state.phase === 'available') {
88
+ if (!canSelfUpdate()) {
89
+ setState({ phase: 'error', messageKey: `update.cannot.${installKind()}` });
90
+ return;
91
+ }
92
+ void install(state.latest);
93
+ return;
94
+ }
95
+ void check();
96
+ };
97
+ const label = (() => {
98
+ if (restartNeeded)
99
+ return t('relayUpdate.restart', { version: installed ?? '', running: running ?? '' });
100
+ switch (state.phase) {
101
+ case 'checking':
102
+ return t('relayUpdate.checking');
103
+ case 'current':
104
+ return t('relayUpdate.upToDate', { version: state.version });
105
+ case 'available':
106
+ return t('relayUpdate.available', { latest: state.latest, installed: installed ?? '?' });
107
+ case 'needsCli':
108
+ return t('relayUpdate.needsCli', { latest: state.latest });
109
+ case 'updating':
110
+ return state.attempt > 1
111
+ ? t('relayUpdate.updatingRetry', { version: state.latest, attempt: state.attempt })
112
+ : t('relayUpdate.updating', { version: state.latest });
113
+ case 'restarting':
114
+ return t('relayUpdate.restarting');
115
+ case 'error':
116
+ return t(state.messageKey, state.params);
117
+ default:
118
+ return t('relayUpdate.check', { version: installed ?? '?' });
119
+ }
120
+ })();
121
+ return { visible, installed, label, activate };
122
+ }
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * After the CVE-2024-27980 fix, spawning `.cmd` without a shell returns EINVAL;
5
+ * npm-cli.js lets the current Node process run npm without a shell. `npm.cmd`
6
+ * requires cmd.exe, whose quote rules cannot stop `%` expansion in registry URLs.
7
+ */
8
+ export function npmInvocation({ args, platform = process.platform, env = process.env, execPath = process.execPath, exists = fs.existsSync, }) {
9
+ if (platform !== 'win32')
10
+ return { command: 'npm', args };
11
+ const npmExecPath = env.npm_execpath;
12
+ if (npmExecPath && /npm-cli\.js$/i.test(npmExecPath) && exists(npmExecPath)) {
13
+ return { command: execPath, args: [npmExecPath, ...args] };
14
+ }
15
+ const adjacentNpmCli = path.join(path.dirname(execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
16
+ if (exists(adjacentNpmCli))
17
+ return { command: execPath, args: [adjacentNpmCli, ...args] };
18
+ return null;
19
+ }
20
+ export function runNpm(spawnImpl, args, timeoutMs, invocationOptions = {}) {
21
+ return new Promise((resolve) => {
22
+ let child;
23
+ try {
24
+ const invocation = npmInvocation({ args, ...invocationOptions });
25
+ if (!invocation) {
26
+ resolve({
27
+ ok: false,
28
+ spawnFailed: true,
29
+ output: 'npm-cli.js was not found next to this Node.js; run npm install -g herdr-remote yourself.',
30
+ });
31
+ return;
32
+ }
33
+ child = spawnImpl(invocation.command, invocation.args, {
34
+ timeout: timeoutMs,
35
+ windowsHide: true,
36
+ });
37
+ }
38
+ catch (error) {
39
+ resolve({
40
+ ok: false,
41
+ spawnFailed: true,
42
+ output: String(error?.message || error),
43
+ });
44
+ return;
45
+ }
46
+ let output = '';
47
+ let settled = false;
48
+ const finish = (result) => {
49
+ if (!settled) {
50
+ settled = true;
51
+ resolve(result);
52
+ }
53
+ };
54
+ const collect = (chunk) => {
55
+ output += String(chunk);
56
+ };
57
+ child.stdout?.on('data', collect);
58
+ child.stderr?.on('data', collect);
59
+ child.on('error', (error) => finish({ ok: false, spawnFailed: true, output: error.message }));
60
+ child.on('close', (code) => finish({ ok: code === 0, output: output.trim() }));
61
+ });
62
+ }
package/dist/updater.js CHANGED
@@ -9,6 +9,7 @@ import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { spawn } from 'node:child_process';
11
11
  import { PACKAGE_ROOT } from './paths.js';
12
+ import { runNpm } from './updater/npm.js';
12
13
  const PACKAGE_NAME = 'herdr-remote';
13
14
  const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
14
15
  /**
@@ -165,13 +166,13 @@ async function fetchJson(url, { timeoutMs, fetchImpl }) {
165
166
  * format, which npmjs refuses on that path with a 406, so every check quietly
166
167
  * fell through to a mirror that had not synced the release yet.
167
168
  */
168
- async function askRegistry(registry, { timeoutMs, fetchImpl }) {
169
+ async function askRegistry(registry, { timeoutMs, fetchImpl, packageName, }) {
169
170
  const endpoints = [
170
171
  {
171
- url: `${registry}/-/package/${PACKAGE_NAME}/dist-tags`,
172
+ url: `${registry}/-/package/${packageName}/dist-tags`,
172
173
  read: (body) => body?.latest,
173
174
  },
174
- { url: `${registry}/${PACKAGE_NAME}/latest`, read: (body) => body?.version },
175
+ { url: `${registry}/${packageName}/latest`, read: (body) => body?.version },
175
176
  ];
176
177
  let message = null;
177
178
  for (const endpoint of endpoints) {
@@ -199,11 +200,12 @@ async function askRegistry(registry, { timeoutMs, fetchImpl }) {
199
200
  * newest version, preferred order first, which is where an install should come
200
201
  * from; `behind` lists the ones that do not, so the screen can say why.
201
202
  */
202
- async function checkForUpdate({ timeoutMs = 6000, fetchImpl = globalThis.fetch, registries = registryCandidates(), } = {}) {
203
- const current = currentVersion();
203
+ async function checkForUpdate({ timeoutMs = 6000, fetchImpl = globalThis.fetch, registries = registryCandidates(),
204
+ // Another package this one installs, and the version of it on disk: the relay.
205
+ packageName = PACKAGE_NAME, current = currentVersion(), } = {}) {
204
206
  const attempts = await Promise.all(registries.map(async (registry) => ({
205
207
  registry,
206
- ...(await askRegistry(registry, { timeoutMs, fetchImpl })),
208
+ ...(await askRegistry(registry, { timeoutMs, fetchImpl, packageName })),
207
209
  })));
208
210
  const answers = attempts.filter((attempt) => attempt.ok);
209
211
  if (answers.length === 0) {
@@ -267,37 +269,6 @@ function installedVersionOnDisk() {
267
269
  return null;
268
270
  }
269
271
  }
270
- function runNpm(spawnImpl, args, timeoutMs) {
271
- return new Promise((resolve) => {
272
- let child;
273
- try {
274
- child = spawnImpl('npm', args, { timeout: timeoutMs });
275
- }
276
- catch (error) {
277
- resolve({
278
- ok: false,
279
- spawnFailed: true,
280
- output: String(error?.message || error),
281
- });
282
- return;
283
- }
284
- let output = '';
285
- let settled = false;
286
- const finish = (result) => {
287
- if (!settled) {
288
- settled = true;
289
- resolve(result);
290
- }
291
- };
292
- const collect = (chunk) => {
293
- output += String(chunk);
294
- };
295
- child.stdout?.on('data', collect);
296
- child.stderr?.on('data', collect);
297
- child.on('error', (error) => finish({ ok: false, spawnFailed: true, output: error.message }));
298
- child.on('close', (code) => finish({ ok: code === 0, output: output.trim() }));
299
- });
300
- }
301
272
  const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
302
273
  /**
303
274
  * Install a release over this one.
package/herdr-plugin.toml CHANGED
@@ -1,9 +1,9 @@
1
1
  id = "herdr.remote.web"
2
2
  name = "Herdr Remote Web"
3
- version = "0.2.20"
3
+ version = "0.2.22"
4
4
  min_herdr_version = "0.9.1"
5
5
  description = "Browser access to Herdr workspaces via local or self-hosted relay"
6
- platforms = ["linux", "macos"]
6
+ platforms = ["linux", "macos", "windows"]
7
7
 
8
8
  # No [[build]] steps: this package is installed from npm with its TUI bundle and
9
9
  # web assets already built. Herdr should never compile anything on the user's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herdr-remote",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Browser access to Herdr workspaces: plugin, host connector, and configuration TUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "files": [
32
32
  "bin",
33
33
  "dist",
34
+ "vendor",
34
35
  "herdr-plugin.toml",
35
36
  "config.example.json",
36
37
  "README.zh-CN.md"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2008-2020 Kohsuke Kawaguchi, Sun Microsystems, Inc., CloudBees, Inc., Oleg Nenashev and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ # WinSW
2
+
3
+ - Source: https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe
4
+ - Version: 2.12.0 (`WinSW.NET461.exe`)
5
+ - SHA-256: `b5066b7bbdfba1293e5d15cda3caaea88fbeab35bd5b38c41c913d492aadfc4f`
6
+ - License: MIT; see [LICENSE](./LICENSE).
7
+ - Purpose: hosts `herdr-remote run --daemon` as a Windows service.
8
+
9
+ To upgrade, replace the executable as a whole and update its SHA-256 here.
Binary file