herdr-remote 0.2.38 → 0.3.31

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 (203) hide show
  1. package/dist/chunks/353DMT35.js +3 -0
  2. package/dist/chunks/42KARAL4.js +31 -0
  3. package/dist/chunks/7AUQHB3O.js +10 -0
  4. package/dist/chunks/7SMZ43LI.js +9 -0
  5. package/dist/chunks/AM3OEMFK.js +16 -0
  6. package/dist/chunks/ANNJESOL.js +10 -0
  7. package/dist/chunks/AWB4A65P.js +63 -0
  8. package/dist/chunks/C2UUZY6J.js +2 -0
  9. package/dist/chunks/DGUZCGQN.js +3 -0
  10. package/dist/chunks/H24ZP5D5.js +16 -0
  11. package/dist/chunks/I6NRCYLI.js +3 -0
  12. package/dist/chunks/JJGEV4EZ.js +3 -0
  13. package/dist/chunks/KRSURB5L.js +5 -0
  14. package/dist/chunks/KRVF37OW.js +3 -0
  15. package/dist/chunks/KUDCC7ZO.js +6 -0
  16. package/dist/chunks/M3NRPJK2.js +77 -0
  17. package/dist/chunks/M4AFZHKH.js +3 -0
  18. package/dist/chunks/M52LJEES.js +6 -0
  19. package/dist/chunks/O2LNVFUT.js +3 -0
  20. package/dist/chunks/OJPBI7KB.js +3 -0
  21. package/dist/chunks/OMN6C7J6.js +3 -0
  22. package/dist/chunks/PBMBFWLQ.js +3 -0
  23. package/dist/chunks/PBRQ3HEG.js +3 -0
  24. package/dist/chunks/PFFVGFPL.js +45 -0
  25. package/dist/chunks/QRGNEKME.js +5 -0
  26. package/dist/chunks/TNLCNVA2.js +3 -0
  27. package/dist/chunks/U27JJKJ6.js +3 -0
  28. package/dist/chunks/U6QAYG3E.js +3 -0
  29. package/dist/chunks/UEOQE77N.js +4 -0
  30. package/dist/chunks/WSACSE45.js +4 -0
  31. package/dist/chunks/WY7WHEJ4.js +3 -0
  32. package/dist/chunks/XJKWLK43.js +4 -0
  33. package/dist/chunks/XN6D5S55.js +3 -0
  34. package/dist/chunks/YD3MIP7B.js +5 -0
  35. package/dist/chunks/YXDQ62BB.js +4 -0
  36. package/dist/cli.js +21 -294
  37. package/dist/connector/admin-broker.js +3 -302
  38. package/dist/connector/main.js +20 -18
  39. package/dist/harfbuzz-subset.wasm +0 -0
  40. package/dist/postinstall.js +17 -0
  41. package/herdr-plugin.toml +1 -1
  42. package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
  43. package/node_modules/herdr-remote-relay/package.json +3 -46
  44. package/package.json +8 -27
  45. package/dist/agent-status.js +0 -130
  46. package/dist/binary-plist.js +0 -157
  47. package/dist/config.js +0 -278
  48. package/dist/connector/admin-broker-setup.js +0 -40
  49. package/dist/connector/admin-shell.js +0 -67
  50. package/dist/connector/admin-tab.js +0 -90
  51. package/dist/connector/adminBrokerEndpoint.js +0 -68
  52. package/dist/connector/agent-watch.js +0 -123
  53. package/dist/connector/elevatedPty.js +0 -141
  54. package/dist/connector/font-server.js +0 -165
  55. package/dist/connector/herdr-prefix-keys.js +0 -28
  56. package/dist/connector/host-connector.js +0 -433
  57. package/dist/connector/host-hello.js +0 -32
  58. package/dist/connector/lock.js +0 -73
  59. package/dist/connector/sessions.js +0 -381
  60. package/dist/connector/update-report.js +0 -70
  61. package/dist/connector/wire.js +0 -42
  62. package/dist/exit-codes.js +0 -13
  63. package/dist/font-subset.js +0 -110
  64. package/dist/herdr-api.js +0 -254
  65. package/dist/herdr-command.js +0 -245
  66. package/dist/herdr-plugin.js +0 -120
  67. package/dist/herdr-server.js +0 -190
  68. package/dist/i18n/en.js +0 -257
  69. package/dist/i18n/index.js +0 -48
  70. package/dist/i18n/zh.js +0 -256
  71. package/dist/keepalive/detached.js +0 -97
  72. package/dist/keepalive/environment.js +0 -44
  73. package/dist/keepalive/index.js +0 -129
  74. package/dist/keepalive/launchd.js +0 -117
  75. package/dist/keepalive/systemd.js +0 -112
  76. package/dist/keepalive/types.js +0 -1
  77. package/dist/keepalive/windows-service-setup.js +0 -138
  78. package/dist/keepalive/windows-service.js +0 -272
  79. package/dist/keepalive/windows-task.js +0 -191
  80. package/dist/lib/process.js +0 -132
  81. package/dist/lifecycle.js +0 -79
  82. package/dist/net-interfaces.js +0 -82
  83. package/dist/node-pty-loader.js +0 -80
  84. package/dist/pasted-files.js +0 -115
  85. package/dist/paths.js +0 -74
  86. package/dist/pty-session.js +0 -133
  87. package/dist/relay-client.js +0 -106
  88. package/dist/relay-urls.js +0 -99
  89. package/dist/runtime.js +0 -107
  90. package/dist/service.js +0 -267
  91. package/dist/settings-model.js +0 -298
  92. package/dist/socket-discovery.js +0 -70
  93. package/dist/supervisor.js +0 -323
  94. package/dist/terminal-font/deps.js +0 -83
  95. package/dist/terminal-font/files.js +0 -357
  96. package/dist/terminal-font/formats.js +0 -355
  97. package/dist/terminal-font/index.js +0 -187
  98. package/dist/terminal-font/readers.js +0 -373
  99. package/dist/terminal-font/terminals.js +0 -147
  100. package/dist/terminal-palette.js +0 -399
  101. package/dist/tui/App.js +0 -199
  102. package/dist/tui/api.js +0 -91
  103. package/dist/tui/components/TextField.js +0 -50
  104. package/dist/tui/components/common.js +0 -80
  105. package/dist/tui/index.js +0 -36
  106. package/dist/tui/mouse/index.js +0 -164
  107. package/dist/tui/mouse/protocol.js +0 -96
  108. package/dist/tui/mouse/source.js +0 -79
  109. package/dist/tui/screens/About.js +0 -172
  110. package/dist/tui/screens/Herdr.js +0 -75
  111. package/dist/tui/screens/Keepalive.js +0 -141
  112. package/dist/tui/screens/Overview.js +0 -36
  113. package/dist/tui/screens/Pair.js +0 -74
  114. package/dist/tui/screens/Relay.js +0 -243
  115. package/dist/tui/screens/Services.js +0 -63
  116. package/dist/tui/screens/Wizard.js +0 -241
  117. package/dist/tui/terminal.js +0 -39
  118. package/dist/tui/theme.js +0 -33
  119. package/dist/update-command.js +0 -118
  120. package/dist/updater/in-place.js +0 -83
  121. package/dist/updater/npm.js +0 -62
  122. package/dist/updater.js +0 -401
  123. package/node_modules/herdr-remote-relay/README.md +0 -76
  124. package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
  125. package/node_modules/herdr-remote-relay/config.example.json +0 -18
  126. package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
  127. package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
  128. package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
  129. package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
  130. package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
  131. package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
  132. package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
  133. package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
  134. package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
  135. package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
  136. package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
  137. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
  138. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
  139. package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
  140. package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
  141. package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
  142. package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
  143. package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
  144. package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
  145. package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
  146. package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
  147. package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
  148. package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
  149. package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
  150. package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
  151. package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
  152. package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
  153. package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
  154. package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
  155. package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
  156. package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
  157. package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
  158. package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
  159. package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
  160. package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
  161. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
  162. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
  163. package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
  164. package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
  165. package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
  166. package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
  167. package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
  168. package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
  169. package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
  170. package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
  171. package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
  172. package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
  173. package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
  174. package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
  175. package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
  176. package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
  177. package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
  178. package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
  179. package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
  180. package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
  181. package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
  182. package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
  183. package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
  184. package/node_modules/herdr-remote-relay/dist/state.js +0 -47
  185. package/node_modules/ws/LICENSE +0 -20
  186. package/node_modules/ws/README.md +0 -548
  187. package/node_modules/ws/browser.js +0 -8
  188. package/node_modules/ws/index.js +0 -22
  189. package/node_modules/ws/lib/buffer-util.js +0 -131
  190. package/node_modules/ws/lib/constants.js +0 -19
  191. package/node_modules/ws/lib/event-target.js +0 -292
  192. package/node_modules/ws/lib/extension.js +0 -203
  193. package/node_modules/ws/lib/limiter.js +0 -55
  194. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  195. package/node_modules/ws/lib/receiver.js +0 -743
  196. package/node_modules/ws/lib/sender.js +0 -607
  197. package/node_modules/ws/lib/stream.js +0 -161
  198. package/node_modules/ws/lib/subprotocol.js +0 -62
  199. package/node_modules/ws/lib/validation.js +0 -152
  200. package/node_modules/ws/lib/websocket-server.js +0 -562
  201. package/node_modules/ws/lib/websocket.js +0 -1407
  202. package/node_modules/ws/package.json +0 -74
  203. 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
- }