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
@@ -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
+ };