herdr-remote 0.2.18 → 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 (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
@@ -0,0 +1,235 @@
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
+ if (result.errorKey) {
54
+ ctx.notify(t(result.errorKey), 'error');
55
+ return false;
56
+ }
57
+ next = result.draft;
58
+ }
59
+ ctx.updateDraft(next);
60
+ ctx.notify('', 'info');
61
+ return true;
62
+ };
63
+ const apply = (id, value) => applyAll([[id, value]]);
64
+ /**
65
+ * Move to the next step.
66
+ *
67
+ * `order` is passed explicitly by the access step because the step list
68
+ * depends on the mode that was just chosen, and the draft holding it has not
69
+ * re-rendered yet — reading the memo here would use the previous mode and
70
+ * skip straight past the relay questions.
71
+ */
72
+ const advance = (from, order = steps) => {
73
+ const index = order.indexOf(from);
74
+ setStep(order[Math.min(order.length - 1, index + 1)]);
75
+ };
76
+ const back = () => {
77
+ const index = steps.indexOf(step);
78
+ if (index > 0)
79
+ setStep(steps[index - 1]);
80
+ };
81
+ const finish = () => {
82
+ ctx.run(async () => {
83
+ if (draft.relay.mode === 'remote') {
84
+ // Empty is meaningful: it is how you join a public relay.
85
+ setRelayPassword(password);
86
+ }
87
+ saveDraft(draft);
88
+ ctx.reloadConfig();
89
+ const finalConfig = loadConfig();
90
+ // Keep-alive first, deliberately. Installing it starts the services
91
+ // through the service manager, so starting them here beforehand would
92
+ // leave a second, detached copy holding the relay port and a second host
93
+ // connector fighting the first for the same host id — which browsers see
94
+ // as an endless reconnect loop. `startAll` defers to the manager once it
95
+ // is installed, so the two options can no longer both spawn.
96
+ let keepaliveInstalled = false;
97
+ if (installKeepalive) {
98
+ try {
99
+ keepalive.install(finalConfig);
100
+ keepaliveInstalled = true;
101
+ }
102
+ catch (error) {
103
+ // Keep-alive is a convenience; a container without systemd should not
104
+ // block a working setup.
105
+ ctx.notify(t('keepalive.failed', { message: error.message }), 'error');
106
+ }
107
+ }
108
+ if (startNow && !keepaliveInstalled)
109
+ startAll(finalConfig);
110
+ onDone();
111
+ });
112
+ };
113
+ 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 }) })] }));
114
+ if (step === 'language') {
115
+ return (_jsxs(Panel, { title: t('wizard.languageTitle'), children: [header, _jsx(ChoiceList, { options: [
116
+ { id: 'auto', label: t('about.languageAuto', { detected: ctx.locale }) },
117
+ { id: 'zh', label: t('about.languageZh') },
118
+ { id: 'en', label: t('about.languageEn') },
119
+ ], current: draft.ui.language, onPick: (language) => {
120
+ if (apply('language', language))
121
+ advance('language');
122
+ }, onCancel: () => { } }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
123
+ }
124
+ if (step === 'access') {
125
+ return (_jsxs(Panel, { title: t('wizard.accessTitle'), children: [header, _jsx(ChoiceList, { options: [
126
+ ...['local', 'lan'].map((mode) => ({
127
+ id: mode,
128
+ label: t(`mode.${mode}`),
129
+ description: t(`mode.${mode}.description`),
130
+ })),
131
+ // Offered ahead of the self-hosted option because it is the one
132
+ // that needs no server. It is still a separate, explicit choice:
133
+ // it routes the terminal through a relay this user does not own.
134
+ {
135
+ id: 'official',
136
+ label: t('mode.official'),
137
+ description: t('mode.official.description'),
138
+ },
139
+ {
140
+ id: 'remote',
141
+ label: t('mode.remote'),
142
+ description: t('mode.remote.description'),
143
+ },
144
+ ], current: selectedMode(draft), onPick: (choice) => {
145
+ if (choice === 'official') {
146
+ // The official relay is "remote" with the address already known,
147
+ // so the URL question is answered and skipped. It is a public
148
+ // relay, so there is no join password to ask for either — and any
149
+ // password left over from a previous self-hosted answer would
150
+ // only be sent to a relay that does not want it.
151
+ if (!apply('mode', 'official'))
152
+ return;
153
+ setRelayPassword('');
154
+ advance('access', stepsFor('remote', OFFICIAL_RELAY_URL));
155
+ return;
156
+ }
157
+ if (apply('mode', choice)) {
158
+ // Leaving the official relay clears its address, so the URL step
159
+ // that follows starts empty rather than pre-loaded with ours.
160
+ advance('access', stepsFor(choice, choice === 'remote' ? '' : draft.relay.remoteUrl));
161
+ }
162
+ }, 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' })] }));
163
+ }
164
+ if (step === 'address') {
165
+ 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) => ({
166
+ id: address.address,
167
+ label: `${address.address} (${address.name}, ${address.kind})`,
168
+ })), current: draft.relay.lanHost || addresses[0].address, onPick: (address) => {
169
+ if (apply('lanHost', address))
170
+ advance('address');
171
+ }, onCancel: back })), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
172
+ }
173
+ if (step === 'relayUrl') {
174
+ 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
+ if (!value) {
176
+ ctx.notify(t('error.remoteUrlRequired'), 'error');
177
+ return;
178
+ }
179
+ if (apply('remoteUrl', value))
180
+ advance('relayUrl');
181
+ }, 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' })] }));
182
+ }
183
+ if (step === 'password') {
184
+ 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) => {
185
+ setPassword(value);
186
+ advance('password');
187
+ }, 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' })] }));
188
+ }
189
+ const finishItems = [
190
+ { id: 'startNow', label: `${t('wizard.startNow')} [${startNow ? '×' : ' '}]` },
191
+ {
192
+ id: 'installKeepalive',
193
+ label: `${t('wizard.installKeepalive')} [${installKeepalive ? '×' : ' '}]`,
194
+ },
195
+ { id: 'finish', label: t('wizard.finish') },
196
+ ];
197
+ const activateFinish = (id) => {
198
+ if (id === 'startNow') {
199
+ setStartNow((value) => !value);
200
+ return;
201
+ }
202
+ if (id === 'installKeepalive') {
203
+ setInstallKeepalive((value) => !value);
204
+ return;
205
+ }
206
+ finish();
207
+ };
208
+ 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: () => {
209
+ setFinishSelection(item.id);
210
+ activateFinish(item.id);
211
+ }, 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' })] }));
212
+ }
213
+ function FinishKeys({ items, selected, onSelect, onActivate, onBack, }) {
214
+ useInput((_input, key) => {
215
+ const index = Math.max(0, items.indexOf(selected));
216
+ if (key.escape)
217
+ onBack();
218
+ else if (key.upArrow)
219
+ onSelect(items[(index - 1 + items.length) % items.length]);
220
+ else if (key.downArrow)
221
+ onSelect(items[(index + 1) % items.length]);
222
+ else if (key.return)
223
+ onActivate(selected);
224
+ });
225
+ return null;
226
+ }
227
+ function SkipKey({ onSkip, onBack }) {
228
+ useInput((_input, key) => {
229
+ if (key.return)
230
+ onSkip();
231
+ else if (key.escape)
232
+ onBack();
233
+ });
234
+ return null;
235
+ }
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,33 @@
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
+ };