herdr-remote 0.2.39 → 0.3.32

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 (205) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/chunks/3FQAXVGC.js +3 -0
  4. package/dist/chunks/42KARAL4.js +31 -0
  5. package/dist/chunks/44IGVPMA.js +5 -0
  6. package/dist/chunks/5LZFJ6UZ.js +3 -0
  7. package/dist/chunks/7SMZ43LI.js +9 -0
  8. package/dist/chunks/AX4EPJCP.js +4 -0
  9. package/dist/chunks/B76DFI4H.js +16 -0
  10. package/dist/chunks/BYGDPZZD.js +3 -0
  11. package/dist/chunks/C2UUZY6J.js +2 -0
  12. package/dist/chunks/CVJ4HBMP.js +3 -0
  13. package/dist/chunks/DGUZCGQN.js +3 -0
  14. package/dist/chunks/FONUO5O5.js +4 -0
  15. package/dist/chunks/H24ZP5D5.js +16 -0
  16. package/dist/chunks/I6NRCYLI.js +3 -0
  17. package/dist/chunks/JJGEV4EZ.js +3 -0
  18. package/dist/chunks/K5RFPHSF.js +5 -0
  19. package/dist/chunks/KGOQKOHH.js +5 -0
  20. package/dist/chunks/KRVF37OW.js +3 -0
  21. package/dist/chunks/KUDCC7ZO.js +6 -0
  22. package/dist/chunks/LPR662JH.js +77 -0
  23. package/dist/chunks/LY2ZGCXN.js +4 -0
  24. package/dist/chunks/M52LJEES.js +6 -0
  25. package/dist/chunks/MCUVFSSK.js +3 -0
  26. package/dist/chunks/MEFDV375.js +10 -0
  27. package/dist/chunks/MKAEXKXG.js +10 -0
  28. package/dist/chunks/O2LNVFUT.js +3 -0
  29. package/dist/chunks/PBMBFWLQ.js +3 -0
  30. package/dist/chunks/PBRQ3HEG.js +3 -0
  31. package/dist/chunks/QCSILMFB.js +3 -0
  32. package/dist/chunks/QRGNEKME.js +5 -0
  33. package/dist/chunks/RMHH3IAX.js +63 -0
  34. package/dist/chunks/TNLCNVA2.js +3 -0
  35. package/dist/chunks/U6QAYG3E.js +3 -0
  36. package/dist/chunks/VD54DF7R.js +45 -0
  37. package/dist/chunks/WY7WHEJ4.js +3 -0
  38. package/dist/cli.js +21 -294
  39. package/dist/connector/admin-broker.js +3 -303
  40. package/dist/connector/main.js +20 -18
  41. package/dist/harfbuzz-subset.wasm +0 -0
  42. package/dist/postinstall.js +17 -0
  43. package/herdr-plugin.toml +1 -1
  44. package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
  45. package/node_modules/herdr-remote-relay/package.json +3 -46
  46. package/package.json +8 -27
  47. package/dist/agent-status.js +0 -130
  48. package/dist/binary-plist.js +0 -157
  49. package/dist/config.js +0 -278
  50. package/dist/connector/admin-broker-setup.js +0 -40
  51. package/dist/connector/admin-shell.js +0 -67
  52. package/dist/connector/admin-tab.js +0 -90
  53. package/dist/connector/adminBrokerEndpoint.js +0 -68
  54. package/dist/connector/agent-watch.js +0 -123
  55. package/dist/connector/elevatedPty.js +0 -141
  56. package/dist/connector/font-server.js +0 -165
  57. package/dist/connector/herdr-prefix-keys.js +0 -28
  58. package/dist/connector/host-connector.js +0 -433
  59. package/dist/connector/host-hello.js +0 -32
  60. package/dist/connector/lock.js +0 -73
  61. package/dist/connector/sessions.js +0 -381
  62. package/dist/connector/update-report.js +0 -70
  63. package/dist/connector/wire.js +0 -42
  64. package/dist/exit-codes.js +0 -13
  65. package/dist/font-subset.js +0 -116
  66. package/dist/herdr-api.js +0 -254
  67. package/dist/herdr-command.js +0 -245
  68. package/dist/herdr-plugin.js +0 -120
  69. package/dist/herdr-server.js +0 -190
  70. package/dist/i18n/en.js +0 -257
  71. package/dist/i18n/index.js +0 -48
  72. package/dist/i18n/zh.js +0 -256
  73. package/dist/keepalive/detached.js +0 -97
  74. package/dist/keepalive/environment.js +0 -44
  75. package/dist/keepalive/index.js +0 -129
  76. package/dist/keepalive/launchd.js +0 -117
  77. package/dist/keepalive/systemd.js +0 -112
  78. package/dist/keepalive/types.js +0 -1
  79. package/dist/keepalive/windows-service-setup.js +0 -138
  80. package/dist/keepalive/windows-service.js +0 -272
  81. package/dist/keepalive/windows-task.js +0 -191
  82. package/dist/lib/process.js +0 -132
  83. package/dist/lifecycle.js +0 -79
  84. package/dist/net-interfaces.js +0 -82
  85. package/dist/node-pty-loader.js +0 -80
  86. package/dist/pasted-files.js +0 -115
  87. package/dist/paths.js +0 -74
  88. package/dist/pty-session.js +0 -133
  89. package/dist/relay-client.js +0 -106
  90. package/dist/relay-urls.js +0 -99
  91. package/dist/runtime.js +0 -107
  92. package/dist/service.js +0 -267
  93. package/dist/settings-model.js +0 -298
  94. package/dist/socket-discovery.js +0 -70
  95. package/dist/supervisor.js +0 -323
  96. package/dist/terminal-font/deps.js +0 -83
  97. package/dist/terminal-font/files.js +0 -357
  98. package/dist/terminal-font/formats.js +0 -355
  99. package/dist/terminal-font/index.js +0 -187
  100. package/dist/terminal-font/readers.js +0 -373
  101. package/dist/terminal-font/terminals.js +0 -147
  102. package/dist/terminal-palette.js +0 -399
  103. package/dist/tui/App.js +0 -199
  104. package/dist/tui/api.js +0 -91
  105. package/dist/tui/components/TextField.js +0 -50
  106. package/dist/tui/components/common.js +0 -80
  107. package/dist/tui/index.js +0 -36
  108. package/dist/tui/mouse/index.js +0 -164
  109. package/dist/tui/mouse/protocol.js +0 -96
  110. package/dist/tui/mouse/source.js +0 -79
  111. package/dist/tui/screens/About.js +0 -172
  112. package/dist/tui/screens/Herdr.js +0 -75
  113. package/dist/tui/screens/Keepalive.js +0 -141
  114. package/dist/tui/screens/Overview.js +0 -36
  115. package/dist/tui/screens/Pair.js +0 -74
  116. package/dist/tui/screens/Relay.js +0 -243
  117. package/dist/tui/screens/Services.js +0 -63
  118. package/dist/tui/screens/Wizard.js +0 -241
  119. package/dist/tui/terminal.js +0 -39
  120. package/dist/tui/theme.js +0 -33
  121. package/dist/update-command.js +0 -118
  122. package/dist/updater/in-place.js +0 -83
  123. package/dist/updater/npm.js +0 -62
  124. package/dist/updater.js +0 -401
  125. package/node_modules/herdr-remote-relay/README.md +0 -76
  126. package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
  127. package/node_modules/herdr-remote-relay/config.example.json +0 -18
  128. package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
  129. package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
  130. package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
  131. package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
  132. package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
  133. package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
  134. package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
  135. package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
  136. package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
  137. package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
  138. package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
  139. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
  140. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
  141. package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
  142. package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
  143. package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
  144. package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
  145. package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
  146. package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
  147. package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
  148. package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
  149. package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
  150. package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
  151. package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
  152. package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
  153. package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
  154. package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
  155. package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
  156. package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
  157. package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
  158. package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
  159. package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
  160. package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
  161. package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
  162. package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
  163. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
  164. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
  165. package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
  166. package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
  167. package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
  168. package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
  169. package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
  170. package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
  171. package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
  172. package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
  173. package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
  174. package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
  175. package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
  176. package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
  177. package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
  178. package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
  179. package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
  180. package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
  181. package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
  182. package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
  183. package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
  184. package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
  185. package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
  186. package/node_modules/herdr-remote-relay/dist/state.js +0 -47
  187. package/node_modules/ws/LICENSE +0 -20
  188. package/node_modules/ws/README.md +0 -548
  189. package/node_modules/ws/browser.js +0 -8
  190. package/node_modules/ws/index.js +0 -22
  191. package/node_modules/ws/lib/buffer-util.js +0 -131
  192. package/node_modules/ws/lib/constants.js +0 -19
  193. package/node_modules/ws/lib/event-target.js +0 -292
  194. package/node_modules/ws/lib/extension.js +0 -203
  195. package/node_modules/ws/lib/limiter.js +0 -55
  196. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  197. package/node_modules/ws/lib/receiver.js +0 -743
  198. package/node_modules/ws/lib/sender.js +0 -607
  199. package/node_modules/ws/lib/stream.js +0 -161
  200. package/node_modules/ws/lib/subprotocol.js +0 -62
  201. package/node_modules/ws/lib/validation.js +0 -152
  202. package/node_modules/ws/lib/websocket-server.js +0 -562
  203. package/node_modules/ws/lib/websocket.js +0 -1407
  204. package/node_modules/ws/package.json +0 -74
  205. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,241 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { theme } from '../theme.js';
5
- import { FieldRow, Message, Panel, Selectable } from '../components/common.js';
6
- import { TextField } from '../components/TextField.js';
7
- import { ChoiceList } from './Relay.js';
8
- import { OFFICIAL_RELAY_URL, configPath, keepalive, listReachableAddresses, loadConfig, saveDraft, selectedMode, setField, setRelayPassword, startAll, } from '../api.js';
9
- /**
10
- * The questions worth asking for a given access mode, in order.
11
- *
12
- * `remoteUrl` matters because the official relay is a remote relay whose
13
- * address and credentials are already known, so it asks neither question. The
14
- * step counter is derived from this list, so it has to agree with the route
15
- * actually taken or the wizard reports "step 5 of 5" on its third screen.
16
- */
17
- function stepsFor(mode, remoteUrl) {
18
- const steps = ['language', 'access'];
19
- if (mode === 'lan')
20
- steps.push('address');
21
- if (mode === 'remote' && remoteUrl !== OFFICIAL_RELAY_URL)
22
- steps.push('relayUrl', 'password');
23
- steps.push('finish');
24
- return steps;
25
- }
26
- /**
27
- * First-run setup.
28
- *
29
- * The one decision a new install cannot make for the user is how they intend to
30
- * reach the machine, because it decides whether anything listens beyond
31
- * loopback. Asking once, up front, beats a default that is either useless on a
32
- * phone or wider than the user expected.
33
- */
34
- export function Wizard({ ctx, onDone }) {
35
- const { t, draft } = ctx;
36
- const [step, setStep] = useState('language');
37
- const [password, setPassword] = useState(ctx.runtime.relayPassword || '');
38
- const [startNow, setStartNow] = useState(true);
39
- const [installKeepalive, setInstallKeepalive] = useState(true);
40
- const [finishSelection, setFinishSelection] = useState('startNow');
41
- const addresses = useMemo(() => listReachableAddresses({ includeLoopback: false }), []);
42
- const steps = useMemo(() => stepsFor(draft.relay.mode, draft.relay.remoteUrl), [draft.relay.mode, draft.relay.remoteUrl]);
43
- const stepIndex = Math.max(0, steps.indexOf(step));
44
- /**
45
- * Apply several fields as one edit. Each `setField` builds on the previous
46
- * result: `draft` is the value captured when this render began, so applying
47
- * two fields through two separate calls committed the second over the first.
48
- */
49
- const applyAll = (updates) => {
50
- let next = draft;
51
- for (const [id, value] of updates) {
52
- const result = setField(next, id, value);
53
- const { errorKey } = result;
54
- if (errorKey) {
55
- ctx.notify((t) => t(errorKey), 'error');
56
- return false;
57
- }
58
- next = result.draft;
59
- }
60
- ctx.updateDraft(next);
61
- ctx.notify('', 'info');
62
- return true;
63
- };
64
- const apply = (id, value) => applyAll([[id, value]]);
65
- /**
66
- * Move to the next step.
67
- *
68
- * `order` is passed explicitly by the access step because the step list
69
- * depends on the mode that was just chosen, and the draft holding it has not
70
- * re-rendered yet — reading the memo here would use the previous mode and
71
- * skip straight past the relay questions.
72
- */
73
- const advance = (from, order = steps) => {
74
- const index = order.indexOf(from);
75
- setStep(order[Math.min(order.length - 1, index + 1)]);
76
- };
77
- const back = () => {
78
- const index = steps.indexOf(step);
79
- if (index > 0)
80
- setStep(steps[index - 1]);
81
- };
82
- const finish = () => {
83
- ctx.run(async () => {
84
- if (draft.relay.mode === 'remote') {
85
- // Empty is meaningful: it is how you join a public relay.
86
- setRelayPassword(password);
87
- }
88
- saveDraft(draft);
89
- ctx.reloadConfig();
90
- const finalConfig = loadConfig();
91
- // Keep-alive first, deliberately. Installing it starts the services
92
- // through the service manager, so starting them here beforehand would
93
- // leave a second, detached copy holding the relay port and a second host
94
- // connector fighting the first for the same host id — which browsers see
95
- // as an endless reconnect loop. `startAll` defers to the manager once it
96
- // is installed, so the two options can no longer both spawn.
97
- let keepaliveInstalled = false;
98
- if (installKeepalive) {
99
- try {
100
- if (keepalive.detectManager(finalConfig.keepalive.manager) === 'windows-service')
101
- ctx.notify((t) => t('keepalive.serviceWindowHint'), 'info');
102
- await keepalive.install(finalConfig);
103
- keepaliveInstalled = true;
104
- }
105
- catch (error) {
106
- // Keep-alive is a convenience; a container without systemd should not
107
- // block a working setup.
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');
112
- }
113
- }
114
- if (startNow && !keepaliveInstalled)
115
- startAll(finalConfig);
116
- onDone();
117
- });
118
- };
119
- const header = (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, children: t('wizard.title') }), _jsx(Text, { color: theme.muted, children: t('wizard.step', { current: stepIndex + 1, total: steps.length }) })] }));
120
- if (step === 'language') {
121
- return (_jsxs(Panel, { title: t('wizard.languageTitle'), children: [header, _jsx(ChoiceList, { options: [
122
- { id: 'auto', label: t('about.languageAuto', { detected: ctx.locale }) },
123
- { id: 'zh', label: t('about.languageZh') },
124
- { id: 'en', label: t('about.languageEn') },
125
- ], current: draft.ui.language, onPick: (language) => {
126
- if (apply('language', language))
127
- advance('language');
128
- }, onCancel: () => { } }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
129
- }
130
- if (step === 'access') {
131
- return (_jsxs(Panel, { title: t('wizard.accessTitle'), children: [header, _jsx(ChoiceList, { options: [
132
- ...['local', 'lan'].map((mode) => ({
133
- id: mode,
134
- label: t(`mode.${mode}`),
135
- description: t(`mode.${mode}.description`),
136
- })),
137
- // Offered ahead of the self-hosted option because it is the one
138
- // that needs no server. It is still a separate, explicit choice:
139
- // it routes the terminal through a relay this user does not own.
140
- {
141
- id: 'official',
142
- label: t('mode.official'),
143
- description: t('mode.official.description'),
144
- },
145
- {
146
- id: 'remote',
147
- label: t('mode.remote'),
148
- description: t('mode.remote.description'),
149
- },
150
- ], current: selectedMode(draft), onPick: (choice) => {
151
- if (choice === 'official') {
152
- // The official relay is "remote" with the address already known,
153
- // so the URL question is answered and skipped. It is a public
154
- // relay, so there is no join password to ask for either — and any
155
- // password left over from a previous self-hosted answer would
156
- // only be sent to a relay that does not want it.
157
- if (!apply('mode', 'official'))
158
- return;
159
- setRelayPassword('');
160
- advance('access', stepsFor('remote', OFFICIAL_RELAY_URL));
161
- return;
162
- }
163
- if (apply('mode', choice)) {
164
- // Leaving the official relay clears its address, so the URL step
165
- // that follows starts empty rather than pre-loaded with ours.
166
- advance('access', stepsFor(choice, choice === 'remote' ? '' : draft.relay.remoteUrl));
167
- }
168
- }, onCancel: back }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.accessHint') }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
169
- }
170
- if (step === 'address') {
171
- return (_jsxs(Panel, { title: t('wizard.addressTitle'), children: [header, addresses.length === 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.warn, children: t('relay.noAddresses') }), _jsx(Box, { marginTop: 1, children: _jsx(Selectable, { selected: true, onSelect: () => advance('address'), children: t('wizard.finish') }) }), _jsx(SkipKey, { onSkip: () => advance('address'), onBack: back })] })) : (_jsx(ChoiceList, { options: addresses.map((address) => ({
172
- id: address.address,
173
- label: `${address.address} (${address.name}, ${address.kind})`,
174
- })), current: draft.relay.lanHost || addresses[0].address, onPick: (address) => {
175
- if (apply('lanHost', address))
176
- advance('address');
177
- }, onCancel: back })), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
178
- }
179
- if (step === 'relayUrl') {
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) => {
181
- if (!value) {
182
- ctx.notify((t) => t('error.remoteUrlRequired'), 'error');
183
- return;
184
- }
185
- if (apply('remoteUrl', value))
186
- advance('relayUrl');
187
- }, onCancel: back }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.relayHint') }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
188
- }
189
- if (step === 'password') {
190
- return (_jsxs(Panel, { title: t('wizard.passwordTitle'), children: [header, _jsx(FieldRow, { label: t('relay.password'), selected: true, onSelect: () => { }, children: _jsx(TextField, { value: password, placeholder: t('relay.passwordEmpty'), active: true, mask: true, onSubmit: (value) => {
191
- setPassword(value);
192
- advance('password');
193
- }, onCancel: back }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.passwordHint') }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
194
- }
195
- const finishItems = [
196
- { id: 'startNow', label: `${t('wizard.startNow')} [${startNow ? '×' : ' '}]` },
197
- {
198
- id: 'installKeepalive',
199
- label: `${t('wizard.installKeepalive')} [${installKeepalive ? '×' : ' '}]`,
200
- },
201
- { id: 'finish', label: t('wizard.finish') },
202
- ];
203
- const activateFinish = (id) => {
204
- if (id === 'startNow') {
205
- setStartNow((value) => !value);
206
- return;
207
- }
208
- if (id === 'installKeepalive') {
209
- setInstallKeepalive((value) => !value);
210
- return;
211
- }
212
- finish();
213
- };
214
- return (_jsxs(Panel, { title: t('wizard.finishTitle'), children: [header, _jsx(FinishKeys, { items: finishItems.map((item) => item.id), selected: finishSelection, onSelect: setFinishSelection, onActivate: activateFinish, onBack: back }), finishItems.map((item) => (_jsx(Selectable, { selected: finishSelection === item.id, onSelect: () => {
215
- setFinishSelection(item.id);
216
- activateFinish(item.id);
217
- }, onHover: () => setFinishSelection(item.id), children: item.label }, item.id))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.finishHint', { path: configPath() }) }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
218
- }
219
- function FinishKeys({ items, selected, onSelect, onActivate, onBack, }) {
220
- useInput((_input, key) => {
221
- const index = Math.max(0, items.indexOf(selected));
222
- if (key.escape)
223
- onBack();
224
- else if (key.upArrow)
225
- onSelect(items[(index - 1 + items.length) % items.length]);
226
- else if (key.downArrow)
227
- onSelect(items[(index + 1) % items.length]);
228
- else if (key.return)
229
- onActivate(selected);
230
- });
231
- return null;
232
- }
233
- function SkipKey({ onSkip, onBack }) {
234
- useInput((_input, key) => {
235
- if (key.return)
236
- onSkip();
237
- else if (key.escape)
238
- onBack();
239
- });
240
- return null;
241
- }
@@ -1,39 +0,0 @@
1
- import { DISABLE_MOUSE } from './mouse/protocol.js';
2
- const ESC = String.fromCharCode(27);
3
- const ENTER_ALT_SCREEN = `${ESC}[?1049h${ESC}[H`;
4
- const EXIT_ALT_SCREEN = `${ESC}[?1049l`;
5
- const SHOW_CURSOR = `${ESC}[?25h`;
6
- /**
7
- * Take over the alternate screen.
8
- *
9
- * Two reasons. The interface is full-screen, so the shell's scrollback should
10
- * come back untouched on exit. More importantly, mouse reports arrive in
11
- * absolute terminal coordinates while element positions are computed relative
12
- * to the Ink root — those only agree when the app starts at row 1. Rendering
13
- * under a shell prompt shifted every click down by however many lines happened
14
- * to be on screen.
15
- */
16
- export function enterFullScreen(output = process.stdout) {
17
- try {
18
- output.write(ENTER_ALT_SCREEN);
19
- }
20
- catch {
21
- // Not a terminal we can drive; rendering still works, clicks may be offset.
22
- }
23
- }
24
- /**
25
- * Undo everything we turned on.
26
- *
27
- * Ink and the mouse layer both clean up on an orderly unmount, but a signal or
28
- * an uncaught error bypasses that, and a terminal left in mouse-reporting mode
29
- * swallows text selection in every command the user runs afterwards. Writing
30
- * the sequences unconditionally is harmless when they were never enabled.
31
- */
32
- export function restoreTerminal(output = process.stdout) {
33
- try {
34
- output.write(`${DISABLE_MOUSE}${SHOW_CURSOR}${EXIT_ALT_SCREEN}`);
35
- }
36
- catch {
37
- // The stream may already be closed during shutdown; nothing useful to do.
38
- }
39
- }
package/dist/tui/theme.js DELETED
@@ -1,33 +0,0 @@
1
- // A deliberately quiet palette.
2
- //
3
- // The previous TUI painted 24-bit terracotta on filled backgrounds, which
4
- // fought with every terminal theme it ran in. These are the eight basic ANSI
5
- // colours instead, so the interface inherits whatever palette the user already
6
- // chose: one accent for "where you are", and colour elsewhere only where it
7
- // carries meaning (a service is up, down, or in between).
8
- export const theme = {
9
- /** Selection and focus. */
10
- accent: 'cyan',
11
- /** Frame lines and separators. */
12
- border: 'gray',
13
- /** Secondary text: labels, hints, paths. */
14
- muted: 'gray',
15
- ok: 'green',
16
- warn: 'yellow',
17
- bad: 'red',
18
- };
19
- export const SELECTED_MARKER = '▸';
20
- export const UNSELECTED_MARKER = ' ';
21
- export const STATUS_COLOR = {
22
- ok: theme.ok,
23
- warn: theme.warn,
24
- bad: theme.bad,
25
- idle: theme.muted,
26
- };
27
- /** Filled for a live state, hollow for an inactive one. */
28
- export const STATUS_GLYPH = {
29
- ok: '●',
30
- warn: '●',
31
- bad: '●',
32
- idle: '○',
33
- };
@@ -1,118 +0,0 @@
1
- // `herdr-remote update`: install the newest release and restart what was running.
2
- //
3
- // The TUI's update row installs and leaves the restart to a second key press;
4
- // a command run over SSH or from a script has nobody to press it. The restart
5
- // is run by the release just installed, not by this process: this process
6
- // still runs the old code, and would start the services as the old release
7
- // described them.
8
- import { spawnSync } from 'node:child_process';
9
- import { cliTarballUrl } from 'herdr-remote-relay/protocol';
10
- import { loadConfig } from './config.js';
11
- import { startAdminBrokerTask, stopAdminBroker, waitForAdminBroker, } from './connector/admin-broker.js';
12
- import { cliEntryPoint } from './keepalive/environment.js';
13
- import { servicesRunning, startAll, stopAll } from './lifecycle.js';
14
- import { checkForUpdate, performUpdate } from './updater.js';
15
- import { heldByAnotherProcess } from './updater/in-place.js';
16
- const RESTART_TIMEOUT_MS = 120_000;
17
- function restartWithInstalledRelease() {
18
- const result = spawnSync(process.execPath, [cliEntryPoint(), 'restart', '--json'], {
19
- encoding: 'utf8',
20
- timeout: RESTART_TIMEOUT_MS,
21
- windowsHide: true,
22
- });
23
- if (!result.error && result.status === 0)
24
- return { ok: true, message: '' };
25
- const output = String(result.stderr || result.stdout || '').trim();
26
- return { ok: false, message: result.error?.message || output || `exit ${result.status}` };
27
- }
28
- export async function runUpdate(t, options = {}) {
29
- const config = () => options.config ?? loadConfig();
30
- const { platform = process.platform, checkCli = () => checkForUpdate(), installCli = (check, onAttempt) => performUpdate({
31
- source: check.source,
32
- sources: check.sources,
33
- version: check.latest,
34
- onAttempt,
35
- }), isRunning = () => servicesRunning(config()), stop = () => stopAll(config()), start = () => startAll(config()), restartInstalled = restartWithInstalledRelease, stopBroker = stopAdminBroker, startBroker = async () => {
36
- try {
37
- startAdminBrokerTask(t);
38
- }
39
- catch {
40
- // Denied or no task: the broker does not answer, which is reported next.
41
- }
42
- return waitForAdminBroker();
43
- }, report = (line) => process.stdout.write(`${line}\n`), } = options;
44
- report(t('update.checking'));
45
- const cli = await checkCli();
46
- const { current } = cli;
47
- if (!cli.ok) {
48
- report(t('update.errorNetworkDetail', { message: cli.message ?? '' }));
49
- return { ok: false, current, updated: null, restarted: false, errorKey: cli.errorKey };
50
- }
51
- if (!cli.updateAvailable) {
52
- report(t('update.upToDate', { version: current }));
53
- return { ok: true, current, updated: null, restarted: false };
54
- }
55
- const updated = 'cli';
56
- const version = cli.latest ?? '';
57
- const wasRunning = isRunning();
58
- // Windows refuses to replace files a running process holds, as services
59
- // started by older releases do. Elsewhere they keep serving until the restart.
60
- const stopped = platform === 'win32' && wasRunning;
61
- if (stopped) {
62
- report(t('update.stoppingServices'));
63
- stop();
64
- }
65
- // The admin broker stops too, so its task starts it on the new release.
66
- const broker = platform === 'win32' ? await stopBroker() : 'unavailable';
67
- const restoreBroker = async () => {
68
- if (broker !== 'stopped')
69
- return broker === 'busy' ? 'busy' : null;
70
- const back = await startBroker();
71
- report(t(back ? 'update.brokerRestarted' : 'update.brokerRestartFailed'));
72
- return back ? 'restarted' : 'failed';
73
- };
74
- report(t('update.updating', { version }));
75
- const onAttempt = ({ attempt }) => {
76
- if (attempt > 1)
77
- report(t('update.updatingRetry', { version, attempt }));
78
- };
79
- const result = await installCli(cli, onAttempt);
80
- if (!result.ok) {
81
- // Nothing was replaced, so the old release starts as it was.
82
- if (stopped)
83
- start();
84
- const adminBroker = await restoreBroker();
85
- report(t(result.errorKey ?? 'update.errorFailed', {
86
- version,
87
- installed: result.installed ?? '',
88
- url: cliTarballUrl(version),
89
- }));
90
- if (result.summary)
91
- report(t('update.errorFailedDetail', { message: result.summary }));
92
- if (platform === 'win32' && heldByAnotherProcess(result.output)) {
93
- report(t('update.errorBusy'));
94
- }
95
- const { errorKey, summary } = result;
96
- return { ok: false, current, updated, restarted: false, adminBroker, errorKey, summary };
97
- }
98
- const installed = result.installed ?? version;
99
- report(t('update.done', { version: installed }));
100
- let ok = true;
101
- let restarted = false;
102
- if (!wasRunning) {
103
- report(t('update.notRunning'));
104
- }
105
- else {
106
- report(t('update.restarting'));
107
- const restart = restartInstalled();
108
- ok = restart.ok;
109
- restarted = restart.ok;
110
- report(restart.ok
111
- ? t('services.restarted')
112
- : t('update.restartFailed', { message: restart.message }));
113
- }
114
- if (broker === 'busy')
115
- report(t('update.brokerBusy'));
116
- const adminBroker = await restoreBroker();
117
- return { ok, current, updated, installed, restarted, adminBroker };
118
- }
@@ -1,83 +0,0 @@
1
- // Installing a release when npm cannot replace this one on Windows.
2
- //
3
- // npm replaces a global package by renaming its directory, and Windows refuses
4
- // while any process works in that directory or runs a file from it: a shell or
5
- // editor left there, a program an older release started. Nobody can find and
6
- // stop them all. The files inside can still be replaced, so npm installs the
7
- // release into a staging prefix and it is copied over this one file by file.
8
- import fs from 'node:fs';
9
- import path from 'node:path';
10
- import { PACKAGE_ROOT, stateDir } from '../paths.js';
11
- const HELD_FILE_CODES = ['EBUSY', 'EPERM', 'EACCES'];
12
- /** npm could not rename the package directory because something holds it. */
13
- export function heldByAnotherProcess(npmOutput) {
14
- return /\b(EBUSY|EPERM)\b/.test(npmOutput ?? '');
15
- }
16
- function replaceFile(source, target, copyFile) {
17
- try {
18
- copyFile(source, target);
19
- }
20
- catch (error) {
21
- if (!HELD_FILE_CODES.includes(error.code ?? ''))
22
- throw error;
23
- // A running program or a loaded module cannot be overwritten, but it can be renamed.
24
- fs.renameSync(target, `${target}.${process.pid}.old`);
25
- copyFile(source, target);
26
- }
27
- }
28
- /** Make `target` hold what `source` holds, without renaming any directory. */
29
- export function replaceTree(source, target, copyFile = fs.copyFileSync) {
30
- fs.mkdirSync(target, { recursive: true });
31
- // Lowercase because Windows reads names that differ only in case as one file.
32
- const kept = new Set();
33
- for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
34
- kept.add(entry.name.toLowerCase());
35
- const from = path.join(source, entry.name);
36
- const to = path.join(target, entry.name);
37
- const existing = fs.lstatSync(to, { throwIfNoEntry: false });
38
- if (existing && existing.isDirectory() !== entry.isDirectory())
39
- fs.rmSync(to, { recursive: true, force: true });
40
- if (entry.isDirectory())
41
- replaceTree(from, to, copyFile);
42
- else
43
- replaceFile(from, to, copyFile);
44
- }
45
- for (const name of fs.readdirSync(target)) {
46
- if (kept.has(name.toLowerCase()))
47
- continue;
48
- try {
49
- fs.rmSync(path.join(target, name), { recursive: true, force: true });
50
- }
51
- catch {
52
- // Still held, like a file renamed aside above; the next update removes it.
53
- }
54
- }
55
- }
56
- /** Run the failed `npm install -g` again into a staging prefix, then copy the result over `target`. */
57
- export async function installInPlace(args, run, { target = PACKAGE_ROOT, stage = path.join(stateDir(), 'update-stage'), platform = process.platform, } = {}) {
58
- fs.rmSync(stage, { recursive: true, force: true });
59
- try {
60
- const result = await run([...args, '--prefix', stage]);
61
- if (!result.ok)
62
- return result;
63
- // npm's global layout under a prefix.
64
- const modules = platform === 'win32'
65
- ? path.join(stage, 'node_modules')
66
- : path.join(stage, 'lib', 'node_modules');
67
- try {
68
- replaceTree(path.join(modules, path.basename(target)), target);
69
- }
70
- catch (error) {
71
- return { ok: false, output: `${result.output}\n${error.message}` };
72
- }
73
- return result;
74
- }
75
- finally {
76
- try {
77
- fs.rmSync(stage, { recursive: true, force: true });
78
- }
79
- catch {
80
- // A stage left behind is emptied before the next one is used.
81
- }
82
- }
83
- }
@@ -1,62 +0,0 @@
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
- }