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,75 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { theme } from '../theme.js';
5
- import { FieldRow, Message, Panel, Row, Selectable, StatusDot } from '../components/common.js';
6
- import { TextField } from '../components/TextField.js';
7
- import { MIN_HERDR_VERSION, getField, getFieldPlaceholder, herdrVersion, saveDraft, setField, } from '../api.js';
8
- export function HerdrScreen({ ctx }) {
9
- const { t, draft, editingId } = ctx;
10
- const [selected, setSelected] = useState('socketPath');
11
- const [installed, setInstalled] = useState(null);
12
- useEffect(() => {
13
- setInstalled(herdrVersion());
14
- }, []);
15
- const entries = [
16
- { id: 'socketPath', kind: 'field', label: t('herdr.socketPath') },
17
- { id: 'herdrArgs', kind: 'field', label: t('herdr.args') },
18
- { id: 'herdrAutoStart', kind: 'toggle', label: t('field.herdrAutoStart') },
19
- { id: 'save', kind: 'action', label: t('common.save') },
20
- ];
21
- const applyField = (id, value) => {
22
- const result = setField(draft, id, value);
23
- const { errorKey } = result;
24
- if (errorKey) {
25
- ctx.notify((t) => t(errorKey), 'error');
26
- return false;
27
- }
28
- ctx.updateDraft(result.draft);
29
- return true;
30
- };
31
- const save = () => {
32
- try {
33
- const result = saveDraft(draft);
34
- ctx.reloadConfig();
35
- ctx.notify((t) => t('common.saved', { path: result.path }), 'success');
36
- }
37
- catch (error) {
38
- ctx.notify((t) => t('error.saveFailed', { message: error.message }), 'error');
39
- }
40
- };
41
- const activate = (id) => {
42
- if (id === 'socketPath' || id === 'herdrArgs') {
43
- ctx.setEditing(id);
44
- return;
45
- }
46
- if (id === 'herdrAutoStart') {
47
- applyField(id, draft.herdr.autoStart ? 'off' : 'on');
48
- return;
49
- }
50
- if (id === 'save')
51
- save();
52
- };
53
- useInput((input, key) => {
54
- const index = entries.findIndex((entry) => entry.id === selected);
55
- const safeIndex = index >= 0 ? index : 0;
56
- if (key.upArrow)
57
- setSelected(entries[(safeIndex - 1 + entries.length) % entries.length].id);
58
- else if (key.downArrow)
59
- setSelected(entries[(safeIndex + 1) % entries.length].id);
60
- else if (key.return)
61
- activate(selected);
62
- else if (input === 's')
63
- save();
64
- }, { isActive: editingId === null });
65
- return (_jsxs(Panel, { title: t('herdr.title'), children: [entries.map((entry) => entry.kind === 'toggle' ? (_jsx(FieldRow, { label: entry.label, selected: selected === entry.id, onSelect: () => {
66
- setSelected(entry.id);
67
- activate(entry.id);
68
- }, onHover: () => setSelected(entry.id), children: _jsx(Text, { color: draft.herdr.autoStart ? theme.ok : theme.muted, children: draft.herdr.autoStart ? `[×] ${t('common.on')}` : `[ ] ${t('common.off')}` }) }, entry.id)) : entry.kind === 'field' ? (_jsx(FieldRow, { label: entry.label, selected: selected === entry.id, onSelect: () => activate(entry.id), onHover: () => setSelected(entry.id), children: _jsx(TextField, { value: getField(draft, entry.id), placeholder: ctx.t(getFieldPlaceholder(draft, entry.id)), active: editingId === entry.id, onSubmit: (value) => {
69
- if (applyField(entry.id, value))
70
- ctx.setEditing(null);
71
- }, onCancel: () => ctx.setEditing(null) }) }, entry.id)) : (_jsx(Box, { marginTop: 1, children: _jsx(Selectable, { selected: selected === entry.id, onSelect: () => {
72
- setSelected(entry.id);
73
- activate(entry.id);
74
- }, onHover: () => setSelected(entry.id), children: entry.label }) }, entry.id))), selected === 'herdrAutoStart' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('herdr.autoStartHint') }) })) : null, ctx.dirty ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.warn, children: t('hint.unsavedChanges') }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [installed?.version ? (_jsx(Row, { label: t('herdr.version'), children: _jsxs(Box, { children: [_jsx(StatusDot, { level: installed.supported ? 'ok' : 'warn' }), _jsx(Text, { children: ` ${installed.version}` })] }) })) : null, installed?.version && !installed.supported ? (_jsx(Text, { color: theme.warn, children: t('herdr.versionOutdated', { version: installed.version, minimum: MIN_HERDR_VERSION }) })) : null, installed && !installed.ok ? (_jsx(Text, { color: theme.warn, children: t('herdr.cliMissing') })) : null] }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
75
- }
@@ -1,141 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { theme } from '../theme.js';
5
- import { Message, Panel, Row, Selectable, StatusDot } from '../components/common.js';
6
- import { ChoiceList } from './Relay.js';
7
- import { KEEPALIVE_MANAGERS, adminBrokerStatus, configureAdminBroker, keepalive, saveDraft, setField, } from '../api.js';
8
- export function Keepalive({ ctx }) {
9
- const { t, status, draft, editingId } = ctx;
10
- const [selected, setSelected] = useState('install');
11
- const [brokerReady, setBrokerReady] = useState(null);
12
- useEffect(() => {
13
- if (process.platform !== 'win32')
14
- return undefined;
15
- let cancelled = false;
16
- adminBrokerStatus()
17
- .then((available) => {
18
- if (!cancelled)
19
- setBrokerReady(available);
20
- })
21
- .catch(() => {
22
- if (!cancelled)
23
- setBrokerReady(false);
24
- });
25
- return () => {
26
- cancelled = true;
27
- };
28
- }, []);
29
- const current = status?.keepalive;
30
- const entries = [
31
- { id: 'install', label: t('keepalive.install') },
32
- { id: 'restart', label: t('keepalive.restart') },
33
- { id: 'uninstall', label: t('keepalive.uninstall') },
34
- { id: 'manager', label: `${t('keepalive.manager')}: ${draft.keepalive.manager}` },
35
- ...(process.platform === 'linux' && current?.manager === 'systemd' && !current?.linger
36
- ? [{ id: 'linger', label: t('keepalive.enableLinger') }]
37
- : []),
38
- ...(process.platform === 'win32'
39
- ? [
40
- { id: 'adminBrokerInstall', label: t('adminBroker.install') },
41
- { id: 'adminBrokerUninstall', label: t('adminBroker.uninstall') },
42
- ]
43
- : []),
44
- ];
45
- const activate = (id) => {
46
- if (id === 'manager') {
47
- ctx.setEditing('keepaliveManager');
48
- return;
49
- }
50
- if (id === 'adminBrokerInstall' || id === 'adminBrokerUninstall') {
51
- const action = id === 'adminBrokerInstall' ? 'install' : 'uninstall';
52
- ctx.run(async () => {
53
- let ready = false;
54
- try {
55
- await configureAdminBroker(action, t);
56
- ready = action === 'install' && (await adminBrokerStatus({ waitForStart: true }));
57
- }
58
- catch (error) {
59
- if (error.code === 'ELEVATION_CANCELLED') {
60
- ctx.notify((t) => t('adminBroker.elevationCancelled'), 'info');
61
- return;
62
- }
63
- throw error;
64
- }
65
- setBrokerReady(ready);
66
- ctx.notify((t) => t(`adminBroker.${action}Done`), 'success');
67
- });
68
- return;
69
- }
70
- ctx.run(async () => {
71
- if (id === 'install') {
72
- if (keepalive.detectManager(ctx.config.keepalive.manager) === 'windows-service')
73
- ctx.notify((t) => t('keepalive.serviceWindowHint'), 'info');
74
- try {
75
- const result = await keepalive.install(ctx.config);
76
- ctx.notify((t) => t('keepalive.installed', { manager: result.manager }), 'success');
77
- }
78
- catch (error) {
79
- if (error.code === 'ELEVATION_CANCELLED') {
80
- ctx.notify((t) => t('keepalive.elevationCancelled'), 'info');
81
- return;
82
- }
83
- throw error;
84
- }
85
- return;
86
- }
87
- if (id === 'uninstall') {
88
- await keepalive.uninstall(ctx.config);
89
- ctx.notify((t) => t('keepalive.uninstalled'), 'success');
90
- return;
91
- }
92
- if (id === 'restart') {
93
- keepalive.restart(ctx.config);
94
- ctx.notify((t) => t('keepalive.restarted'), 'success');
95
- return;
96
- }
97
- if (id === 'linger') {
98
- const result = keepalive.enableLinger();
99
- ctx.notify((t) => t('keepalive.lingerDone', { username: result.username }), 'success');
100
- }
101
- });
102
- };
103
- useInput((_input, key) => {
104
- const index = entries.findIndex((entry) => entry.id === selected);
105
- if (key.upArrow)
106
- setSelected(entries[(index - 1 + entries.length) % entries.length].id);
107
- else if (key.downArrow)
108
- setSelected(entries[(index + 1) % entries.length].id);
109
- else if (key.return)
110
- activate(selected);
111
- }, { isActive: editingId === null });
112
- if (editingId === 'keepaliveManager') {
113
- 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) => {
114
- const result = setField(draft, 'keepaliveManager', manager);
115
- const { errorKey } = result;
116
- if (errorKey) {
117
- ctx.notify((t) => t(errorKey), 'error');
118
- return;
119
- }
120
- ctx.updateDraft(result.draft);
121
- try {
122
- saveDraft(result.draft);
123
- ctx.reloadConfig();
124
- }
125
- catch (error) {
126
- ctx.notify((t) => t('error.saveFailed', { message: error.message }), 'error');
127
- }
128
- ctx.setEditing(null);
129
- }, onCancel: () => ctx.setEditing(null) }) }));
130
- }
131
- return (_jsxs(Panel, { title: t('keepalive.title'), children: [_jsx(Row, { label: t('keepalive.manager'), children: _jsx(Text, { children: current?.manager ?? t('common.unknown') }) }), _jsx(Row, { label: t('keepalive.state'), children: _jsxs(Box, { children: [_jsx(StatusDot, { level: current?.active ? 'ok' : current?.installed ? 'warn' : 'idle' }), _jsxs(Text, { children: [' ', current?.active
132
- ? t('common.running')
133
- : current?.installed
134
- ? t('common.installed')
135
- : t('common.notInstalled')] })] }) }), process.platform === 'win32' ? (_jsxs(_Fragment, { children: [_jsx(Row, { label: t('adminBroker.title'), children: _jsx(Text, { color: brokerReady ? theme.ok : theme.warn, children: brokerReady === null
136
- ? t('adminBroker.checking')
137
- : t(brokerReady ? 'adminBroker.ready' : 'adminBroker.notReady') }) }), _jsx(Text, { color: theme.muted, children: t('adminBroker.tuiHint') })] })) : null, 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: () => {
138
- setSelected(entry.id);
139
- activate(entry.id);
140
- }, 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-task' ? (_jsx(Text, { color: theme.muted, children: t('keepalive.windowsTaskNote') })) : 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' })] }));
141
- }
@@ -1,36 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { theme } from '../theme.js';
4
- import { Message, Panel, Row, StatusDot } from '../components/common.js';
5
- import { formatUptime } from '../api.js';
6
- function level(alive) {
7
- if (alive === null || alive === undefined)
8
- return 'idle';
9
- return alive ? 'ok' : 'bad';
10
- }
11
- export function Overview({ ctx }) {
12
- const { t, status, config } = ctx;
13
- if (!status) {
14
- return (_jsx(Panel, { title: t('overview.title'), children: _jsx(Text, { color: theme.muted, children: t('common.loading') }) }));
15
- }
16
- const health = status.relay.health || {};
17
- const relayReachable = health.ok !== false;
18
- const relayLevel = status.relay.local
19
- ? level(status.relay.alive)
20
- : relayReachable
21
- ? 'ok'
22
- : 'bad';
23
- const keepaliveLevel = status.keepalive.active
24
- ? 'ok'
25
- : status.keepalive.installed
26
- ? 'warn'
27
- : 'idle';
28
- const nothingRunning = !status.host.alive && (status.relay.local ? !status.relay.alive : !relayReachable);
29
- return (_jsxs(Panel, { title: t('overview.title'), children: [_jsx(Row, { label: t('overview.mode'), children: _jsx(Text, { children: t(`mode.${status.mode}`) }) }), _jsx(Row, { label: t('overview.relay'), children: _jsxs(Box, { children: [_jsx(StatusDot, { level: relayLevel }), _jsxs(Text, { children: [' ', status.relay.local
30
- ? t('overview.relayLocal', { bind: status.relay.bind ?? '', port: status.relay.port })
31
- : t('overview.relayRemote', { url: status.relay.remoteUrl ?? '' })] }), status.relay.local && status.relay.pid ? (_jsx(Text, { color: theme.muted, children: ` ${t('common.pid', { pid: status.relay.pid })}` })) : null] }) }), _jsx(Row, { label: t('overview.host'), children: _jsxs(Box, { children: [_jsx(StatusDot, { level: level(status.host.alive) }), _jsx(Text, { children: ` ${status.host.alive ? t('common.running') : t('common.stopped')}` }), status.host.pid ? (_jsx(Text, { color: theme.muted, children: ` ${t('common.pid', { pid: status.host.pid })}` })) : null] }) }), _jsx(Row, { label: t('overview.socket'), children: _jsxs(Box, { children: [_jsx(StatusDot, { level: status.host.socketExists ? 'ok' : 'warn' }), _jsx(Text, { color: theme.muted, children: ` ${status.host.socketPath}` }), status.host.socketExists ? null : (_jsx(Text, { color: theme.warn, children: ` ${t('overview.socketMissing')}` }))] }) }), _jsx(Row, { label: t('overview.webUrl'), children: _jsx(Text, { color: theme.accent, children: status.publicUrl }) }), _jsx(Row, { label: t('overview.keepalive'), children: _jsxs(Box, { children: [_jsx(StatusDot, { level: keepaliveLevel }), _jsxs(Text, { children: [` ${status.keepalive.manager} — `, status.keepalive.active
32
- ? t('common.running')
33
- : status.keepalive.installed
34
- ? t('common.installed')
35
- : t('common.notInstalled')] })] }) }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: relayReachable ? (_jsxs(_Fragment, { children: [typeof health.hosts === 'number' ? (_jsx(Row, { label: t('overview.hosts'), children: _jsx(Text, { children: health.hosts }) })) : null, typeof health.clients === 'number' ? (_jsx(Row, { label: t('overview.devices'), children: _jsx(Text, { children: health.clients }) })) : null, _jsx(Row, { label: t('overview.uptime'), children: _jsx(Text, { color: theme.muted, children: formatUptime(health.uptimeSeconds, t) }) })] })) : (_jsx(Text, { color: theme.warn, children: t('overview.unreachable', { message: health.message ?? '' }) })) }), nothingRunning ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('overview.notStarted') }) })) : null, config.relay.mode === 'local' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('mode.local.description') }) })) : null, _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
36
- }
@@ -1,74 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Box, Text, useInput, useWindowSize } from 'ink';
4
- import QRCode from 'qrcode';
5
- import { theme } from '../theme.js';
6
- import { Menu, Message, Panel } from '../components/common.js';
7
- import { extractPairingCode, pair } from '../api.js';
8
- // A QR block needs room for the symbol plus its quiet zone; below this the
9
- // output wraps into noise, so the URL alone is the honest thing to show.
10
- const MIN_QR_COLUMNS = 44;
11
- export function PairScreen({ ctx }) {
12
- const { t, run } = ctx;
13
- const { columns } = useWindowSize();
14
- const [pairing, setPairing] = useState(null);
15
- const [qr, setQr] = useState(null);
16
- const [remainingMs, setRemainingMs] = useState(0);
17
- const [selected, setSelected] = useState('generate');
18
- useEffect(() => {
19
- if (!pairing)
20
- return undefined;
21
- const tick = () => setRemainingMs(Math.max(0, pairing.expiresAt - Date.now()));
22
- tick();
23
- const timer = setInterval(tick, 1000);
24
- return () => clearInterval(timer);
25
- }, [pairing]);
26
- useEffect(() => {
27
- if (!pairing || columns < MIN_QR_COLUMNS) {
28
- setQr(null);
29
- return;
30
- }
31
- let cancelled = false;
32
- QRCode.toString(pairing.pairUrl, { type: 'terminal', small: true, errorCorrectionLevel: 'L' })
33
- .then((value) => {
34
- if (!cancelled)
35
- setQr(value.replace(/\n$/, ''));
36
- })
37
- .catch(() => {
38
- if (!cancelled)
39
- setQr(null);
40
- });
41
- return () => {
42
- cancelled = true;
43
- };
44
- }, [pairing, columns]);
45
- const generate = () => run(async () => {
46
- ctx.notify((t) => t('pair.working'));
47
- try {
48
- const result = await pair();
49
- const code = extractPairingCode(result);
50
- setPairing({ ...result, code });
51
- ctx.notify('', 'info');
52
- }
53
- catch (error) {
54
- const message = error.message;
55
- setPairing(null);
56
- throw new Error(/host_offline|no Herdr host|did not register/i.test(message)
57
- ? t('pair.hostOffline')
58
- : t('pair.failed', { message }));
59
- }
60
- });
61
- useInput((_input, key) => {
62
- if (key.return)
63
- generate();
64
- }, { isActive: ctx.editingId === null });
65
- const expired = pairing !== null && remainingMs <= 0;
66
- const minutes = Math.floor(remainingMs / 60000);
67
- const seconds = Math.floor((remainingMs % 60000) / 1000);
68
- return (_jsxs(Panel, { title: t('pair.title'), children: [_jsx(Menu, { items: [{ id: 'generate', label: pairing ? t('pair.regenerate') : t('pair.generate') }], selectedId: selected, onChange: setSelected, onSelect: generate }), pairing ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: `${t('pair.code')} ` }), _jsx(Text, { bold: true, color: expired ? theme.muted : theme.accent, children: pairing.code.split('').join(' ') })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: `${t('pair.url')} ` }), _jsx(Text, { children: pairing.pairUrl })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: expired ? theme.bad : theme.muted, children: expired
69
- ? t('pair.expired')
70
- : t('pair.expires', {
71
- minutes: `${minutes}:${String(seconds).padStart(2, '0')}`,
72
- time: new Date(pairing.expiresAt).toLocaleTimeString(),
73
- }) }) }), !expired && qr ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { children: qr }), _jsx(Text, { color: theme.muted, children: t('pair.qrHint') })] })) : null, !expired && !qr && columns < MIN_QR_COLUMNS ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('pair.qrUnavailable') }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('pair.instructions') }) })] })) : null, _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
74
- }
@@ -1,243 +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, Menu, Message, Panel, Row, Selectable } from '../components/common.js';
6
- import { TextField } from '../components/TextField.js';
7
- import { bindAddress, fieldsForMode, getField, getFieldPlaceholder, isOfficialRelay, listReachableAddresses, probeRelay, regenerateHostIdentity, relayStartCommand, saveDraft, selectedMode, setField, setRelayPassword, } from '../api.js';
8
- export function RelayScreen({ ctx }) {
9
- const { t, draft, editingId } = ctx;
10
- const [selected, setSelected] = useState('mode');
11
- const [revealTokens, setRevealTokens] = useState(false);
12
- const [showEnv, setShowEnv] = useState(false);
13
- const [password, setPassword] = useState(ctx.runtime.relayPassword || '');
14
- // The official relay is ours: its address is fixed and it takes no password.
15
- // Both are therefore facts to *show*, not fields to fill in — an editable box
16
- // holding a value that must not change is an invitation to break the setup.
17
- const official = isOfficialRelay(draft);
18
- const fields = useMemo(() => fieldsForMode(draft.relay.mode).filter((field) => ['mode', 'port', 'lanHost', 'remoteUrl', 'publicUrl'].includes(field.id)), [draft.relay.mode]);
19
- const entries = [
20
- ...fields.map((field) => ({
21
- id: field.id,
22
- // The official relay's address is ours and its browser URL follows from
23
- // it, so neither is offered for editing: an editable box holding a value
24
- // that must not change is an invitation to break the setup.
25
- kind: (official && (field.id === 'remoteUrl' || field.id === 'publicUrl')
26
- ? 'readonly'
27
- : 'field'),
28
- fieldKind: field.kind,
29
- label: t(field.labelKey),
30
- })),
31
- // The password only matters when talking to a relay somebody else started;
32
- // a local relay is configured with our own token automatically, and the
33
- // official one authenticates workstations by their own host token.
34
- ...(draft.relay.mode === 'remote' && !official
35
- ? [{ id: 'password', kind: 'password', label: t('relay.password') }]
36
- : []),
37
- // Saving needs a row of its own. Every field here only edits a draft, and
38
- // the services restart from what is on disk — with `s` as the sole way to
39
- // commit, a change could look applied, survive a restart, and never take
40
- // effect. It is also the only way to save with the mouse.
41
- { id: 'save', kind: 'action', label: t('common.save') },
42
- { id: 'test', kind: 'action', label: t('relay.test') },
43
- ...(draft.relay.mode === 'remote' && !official
44
- ? [
45
- {
46
- id: 'reveal',
47
- kind: 'action',
48
- label: revealTokens ? t('relay.hidePassword') : t('relay.showPassword'),
49
- },
50
- { id: 'env', kind: 'action', label: t('relay.envSnippet') },
51
- ]
52
- : []),
53
- { id: 'regenerate', kind: 'action', label: t('relay.regenerate') },
54
- ];
55
- const addresses = useMemo(() => listReachableAddresses({ includeLoopback: false }), []);
56
- /**
57
- * Apply several fields as one edit.
58
- *
59
- * Each `setField` has to build on the previous result rather than on `draft`,
60
- * which is the value captured when this render began. Calling a single-field
61
- * helper twice in a row silently dropped the first change: the second call
62
- * started from the stale draft and committed it back over the first.
63
- */
64
- const applyFields = (updates) => {
65
- let next = draft;
66
- for (const [id, value] of updates) {
67
- const result = setField(next, id, value);
68
- const { errorKey } = result;
69
- if (errorKey) {
70
- ctx.notify((t) => t(errorKey), 'error');
71
- return false;
72
- }
73
- next = result.draft;
74
- }
75
- ctx.updateDraft(next);
76
- ctx.notify('', 'info');
77
- return true;
78
- };
79
- const applyField = (id, value) => applyFields([[id, value]]);
80
- const save = () => {
81
- try {
82
- const result = saveDraft(draft);
83
- ctx.reloadConfig();
84
- ctx.notify((t) => t('common.saved', { path: result.path }), 'success');
85
- }
86
- catch (error) {
87
- const problems = error.problems;
88
- ctx.notify((t) => problems
89
- ? problems.map((key) => t(key)).join(' ')
90
- : t('error.saveFailed', { message: error.message }), 'error');
91
- }
92
- };
93
- const activate = (id) => {
94
- const entry = entries.find((candidate) => candidate.id === id);
95
- if (!entry)
96
- return;
97
- if (entry.kind === 'readonly') {
98
- ctx.notify((t) => t('relay.officialFixed'), 'info');
99
- return;
100
- }
101
- if (entry.kind === 'field' || entry.kind === 'password') {
102
- ctx.setEditing(id);
103
- return;
104
- }
105
- if (id === 'save') {
106
- save();
107
- return;
108
- }
109
- if (id === 'reveal') {
110
- setRevealTokens((current) => !current);
111
- return;
112
- }
113
- if (id === 'env') {
114
- setShowEnv((current) => !current);
115
- return;
116
- }
117
- if (id === 'regenerate') {
118
- ctx.run(() => {
119
- regenerateHostIdentity();
120
- ctx.reloadConfig();
121
- ctx.notify((t) => t('relay.regenerated'), 'success');
122
- });
123
- return;
124
- }
125
- if (id === 'test') {
126
- ctx.run(async () => {
127
- const result = await probeRelay(ctx.config);
128
- ctx.notify((t) => result.ok
129
- ? t('relay.testOk', { version: result.version ?? '?', hosts: result.hosts ?? 0 })
130
- : t('relay.testFailed', { message: result.message ?? '' }), result.ok ? 'success' : 'error');
131
- });
132
- }
133
- };
134
- useInput((input, key) => {
135
- const index = entries.findIndex((entry) => entry.id === selected);
136
- if (key.upArrow)
137
- setSelected(entries[(index - 1 + entries.length) % entries.length].id);
138
- else if (key.downArrow)
139
- setSelected(entries[(index + 1) % entries.length].id);
140
- else if (key.return)
141
- activate(selected);
142
- else if (input === 's')
143
- save();
144
- }, { isActive: editingId === null });
145
- // Choice editors take over the panel so the option list is unambiguous.
146
- if (editingId === 'mode') {
147
- return (_jsx(Panel, { title: t('field.mode'), children: _jsx(ChoiceList, { options: [
148
- ...['local', 'lan'].map((mode) => ({
149
- id: mode,
150
- label: t(`mode.${mode}`),
151
- description: t(`mode.${mode}.description`),
152
- })),
153
- {
154
- id: 'official',
155
- label: t('mode.official'),
156
- description: t('mode.official.description'),
157
- },
158
- { id: 'remote', label: t('mode.remote'), description: t('mode.remote.description') },
159
- ],
160
- // Reflects the official relay as its own selection rather than as an
161
- // indistinguishable "self-hosted" row that happens to hold our URL.
162
- current: selectedMode(draft), onPick: (choice) => {
163
- // `official` is a mode the settings model understands: it sets the
164
- // address along with the mode, and choosing the self-hosted relay
165
- // clears it again. The screen does not have to keep the two fields
166
- // in step by hand, which is what let the picker land on "official"
167
- // while every row below still described a self-hosted relay.
168
- if (!applyField('mode', choice))
169
- return;
170
- if (choice === 'official') {
171
- // A password typed for somebody else's relay is not a credential
172
- // the official one wants, and leaving it behind would send it
173
- // there on the next connection.
174
- setPassword('');
175
- setRelayPassword('');
176
- }
177
- ctx.setEditing(null);
178
- }, onCancel: () => ctx.setEditing(null) }) }));
179
- }
180
- if (editingId === 'lanHost') {
181
- const options = addresses.map((address) => ({
182
- id: address.address,
183
- label: `${address.address} (${address.name}, ${t(addressKindKey(address.kind))})`,
184
- }));
185
- return (_jsx(Panel, { title: t('relay.selectAddress'), children: options.length === 0 ? (_jsx(Text, { color: theme.warn, children: t('relay.noAddresses') })) : (_jsx(ChoiceList, { options: options, current: draft.relay.lanHost || options[0].id, onPick: (address) => {
186
- applyField('lanHost', address);
187
- ctx.setEditing(null);
188
- }, onCancel: () => ctx.setEditing(null) })) }));
189
- }
190
- return (_jsxs(Panel, { title: t('relay.title'), children: [draft.relay.mode !== 'remote' ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Row, { label: t('relay.listenAddress'), children: _jsx(Text, { color: theme.muted, children: `${bindAddress(draft)}:${draft.relay.port}` }) }), _jsx(Text, { color: theme.muted, children: t('relay.listenAddressHint') })] })) : null, entries.map((entry) => entry.kind === 'action' ? (_jsx(Selectable, { selected: selected === entry.id, onSelect: () => {
191
- setSelected(entry.id);
192
- activate(entry.id);
193
- }, onHover: () => setSelected(entry.id), children: entry.label }, entry.id)) : (_jsx(FieldRow, { label: entry.label, selected: selected === entry.id, onSelect: () => activate(entry.id), onHover: () => setSelected(entry.id), children: entry.kind === 'password' ? (_jsx(TextField, { value: password, placeholder: t('relay.passwordEmpty'), active: editingId === 'password', mask: !revealTokens, onSubmit: (value) => {
194
- setPassword(value);
195
- setRelayPassword(value);
196
- ctx.setEditing(null);
197
- ctx.notify((t) => t('relay.passwordSaved'), 'success');
198
- }, onCancel: () => ctx.setEditing(null) })) : entry.kind === 'readonly' ? (
199
- // Fixed by us, so it is printed rather than offered for editing.
200
- _jsx(Text, { color: theme.muted, children: `${getField(draft, entry.id) || placeholderText(ctx, entry.id)} (${t('relay.locked')})` })) : entry.fieldKind === 'choice' ? (
201
- // Choice fields are picked from a list, never typed, so they
202
- // show the translated label rather than the stored id — and the
203
- // official relay is named as itself, not as "self-hosted".
204
- _jsx(Text, { children: t(`mode.${selectedMode(draft)}`) })) : (_jsx(TextField, { value: getField(draft, entry.id), placeholder: placeholderText(ctx, entry.id), active: editingId === entry.id, onSubmit: (value) => {
205
- if (applyField(entry.id, value))
206
- ctx.setEditing(null);
207
- }, onCancel: () => ctx.setEditing(null) })) }, entry.id))), ctx.dirty ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.warn, children: t('hint.unsavedChanges') }) })) : null, draft.relay.mode === 'remote' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: official ? t('relay.officialHint') : t('relay.passwordHint') }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Row, { label: t('relay.identity'), children: _jsx(Text, { color: theme.muted, children: ctx.runtime.hostId }) }) }), showEnv ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [_jsx(Text, { color: theme.muted, children: t('relay.envHint') }), _jsx(Text, { children: relayStartCommand(ctx.config, password) })] })) : null, official ? null : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('relay.docsHint') }) })), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
208
- }
209
- function placeholderText(ctx, id) {
210
- const raw = getFieldPlaceholder(ctx.draft, id);
211
- // Placeholders that name a translation key are resolved here so the model
212
- // stays free of language concerns.
213
- return raw.startsWith('placeholder.') ? ctx.t(raw) : raw;
214
- }
215
- function addressKindKey(kind) {
216
- return `relay.address${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
217
- }
218
- /**
219
- * A modal list of options driven by both arrow keys and the mouse.
220
- *
221
- * It owns the cursor so the highlighted row and the keyboard agree; the parent
222
- * only learns which option was chosen.
223
- */
224
- export function ChoiceList({ options, current, onPick, onCancel, }) {
225
- const [cursor, setCursor] = useState(() => {
226
- const index = options.findIndex((option) => option.id === current);
227
- return index >= 0 ? index : 0;
228
- });
229
- const active = options[Math.min(cursor, options.length - 1)];
230
- useInput((_input, key) => {
231
- if (key.escape) {
232
- onCancel();
233
- return;
234
- }
235
- if (key.upArrow)
236
- setCursor((value) => (value - 1 + options.length) % options.length);
237
- else if (key.downArrow)
238
- setCursor((value) => (value + 1) % options.length);
239
- else if (key.return && active)
240
- onPick(active.id);
241
- });
242
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Menu, { items: options.map((option) => ({ id: option.id, label: option.label })), selectedId: active?.id ?? '', onChange: (id) => setCursor(options.findIndex((option) => option.id === id)), onSelect: onPick }), active?.description ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: active.description }) })) : null] }));
243
- }
@@ -1,63 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { theme } from '../theme.js';
5
- import { Menu, Message, Panel } from '../components/common.js';
6
- import { readLogTail, restartAll, startAll, stopAll } from '../api.js';
7
- const LOG_LINES = 8;
8
- export function Services({ ctx }) {
9
- const { t, run, status } = ctx;
10
- const [selected, setSelected] = useState('start');
11
- const items = [
12
- { id: 'start', label: t('services.start') },
13
- { id: 'stop', label: t('services.stop') },
14
- { id: 'restart', label: t('services.restart') },
15
- ];
16
- const activate = (id) => {
17
- // Starting or restarting reads the configuration from disk, so doing it on
18
- // top of an unsaved draft silently applies the old settings — which reads
19
- // as "I changed the relay and it never took effect". Stopping is unaffected.
20
- if (ctx.dirty && id !== 'stop') {
21
- ctx.notify((t) => t('services.unsavedBlocked'), 'error');
22
- return;
23
- }
24
- return run(() => {
25
- const config = ctx.config;
26
- if (id === 'start') {
27
- const result = startAll(config);
28
- ctx.notify((t) => result.managed
29
- ? t('services.managedNotice', { manager: result.manager ?? '' })
30
- : t('services.started'), 'success');
31
- return;
32
- }
33
- if (id === 'stop') {
34
- const result = stopAll(config);
35
- ctx.notify((t) => result.managed
36
- ? t('services.managedNotice', { manager: result.manager ?? '' })
37
- : t('services.stopped'), 'success');
38
- return;
39
- }
40
- const result = restartAll(config);
41
- ctx.notify((t) => result.managed
42
- ? t('services.managedNotice', { manager: result.manager ?? '' })
43
- : t('services.restarted'), 'success');
44
- });
45
- };
46
- useInput((_input, key) => {
47
- const index = items.findIndex((item) => item.id === selected);
48
- if (key.upArrow)
49
- setSelected(items[(index - 1 + items.length) % items.length].id);
50
- else if (key.downArrow)
51
- setSelected(items[(index + 1) % items.length].id);
52
- else if (key.return)
53
- activate(selected);
54
- }, { isActive: ctx.editingId === null });
55
- const relayLog = readLogTail('relay', LOG_LINES);
56
- const hostLog = readLogTail('host', LOG_LINES);
57
- return (_jsxs(Panel, { title: t('services.title'), children: [_jsx(Menu, { items: items, selectedId: selected, onChange: setSelected, onSelect: activate }), ctx.dirty ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.warn, children: t('services.unsavedBlocked') }) })) : null, status?.keepalive.installed ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('services.managedNotice', { manager: status.keepalive.manager }) }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.muted, children: t('services.logs') }), _jsx(LogBlock, { title: t('services.logRelay'), lines: relayLog, emptyText: t('services.logEmpty') }), _jsx(LogBlock, { title: t('services.logHost'), lines: hostLog, emptyText: t('services.logEmpty') })] }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
58
- }
59
- function LogBlock({ title, lines, emptyText, }) {
60
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.muted, children: title }), lines.length === 0 ? (_jsx(Text, { color: theme.muted, children: emptyText })) : (lines.map((line, index) => (
61
- // biome-ignore lint/suspicious/noArrayIndexKey: log lines are positional and may repeat
62
- _jsx(Text, { color: theme.muted, wrap: "truncate-end", children: line }, index))))] }));
63
- }