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