herdr-remote 0.2.20 → 0.2.22
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/README.md +12 -2
- package/README.zh-CN.md +11 -1
- package/dist/cli.js +23 -4
- package/dist/config.js +8 -1
- package/dist/herdr-api.js +5 -3
- package/dist/herdr-command.js +32 -15
- package/dist/herdr-plugin.js +5 -1
- package/dist/herdr-server.js +3 -2
- package/dist/i18n/en.js +20 -0
- package/dist/i18n/zh.js +20 -0
- package/dist/keepalive/detached.js +13 -3
- package/dist/keepalive/index.js +16 -8
- package/dist/keepalive/launchd.js +4 -4
- package/dist/keepalive/systemd.js +8 -4
- package/dist/keepalive/windows-service-setup.js +129 -0
- package/dist/keepalive/windows-service.js +272 -0
- package/dist/lifecycle.js +15 -1
- package/dist/net-interfaces.js +15 -6
- package/dist/paths.js +16 -4
- package/dist/pty-session.js +10 -2
- package/dist/relay-updater.js +81 -0
- package/dist/service.js +2 -0
- package/dist/socket-discovery.js +15 -3
- package/dist/supervisor.js +1 -0
- package/dist/terminal-font/deps.js +11 -1
- package/dist/terminal-font/files.js +13 -10
- package/dist/terminal-font/readers.js +60 -3
- package/dist/terminal-font/terminals.js +3 -0
- package/dist/terminal-palette.js +78 -18
- package/dist/tui/App.js +34 -8
- package/dist/tui/api.js +6 -4
- package/dist/tui/screens/About.js +30 -15
- package/dist/tui/screens/Herdr.js +5 -4
- package/dist/tui/screens/Keepalive.js +25 -14
- package/dist/tui/screens/Pair.js +1 -1
- package/dist/tui/screens/Relay.js +9 -8
- package/dist/tui/screens/Services.js +4 -4
- package/dist/tui/screens/Wizard.js +11 -5
- package/dist/tui/screens/useRelayUpdate.js +122 -0
- package/dist/updater/npm.js +62 -0
- package/dist/updater.js +8 -37
- package/herdr-plugin.toml +2 -2
- package/package.json +2 -1
- package/vendor/winsw/LICENSE +21 -0
- package/vendor/winsw/README.md +9 -0
- package/vendor/winsw/WinSW.NET461.exe +0 -0
package/README.md
CHANGED
|
@@ -13,6 +13,11 @@ herdr-remote
|
|
|
13
13
|
|
|
14
14
|
Running `herdr-remote` without arguments launches the setup wizard and configuration TUI.
|
|
15
15
|
|
|
16
|
+
Native Windows 10/11 is supported with Node.js 22+; install Herdr from PowerShell with
|
|
17
|
+
`powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"`.
|
|
18
|
+
Herdr's Windows plugin support is in preview; on the first LAN launch, allow `node.exe`
|
|
19
|
+
through Windows Defender Firewall on private networks.
|
|
20
|
+
|
|
16
21
|
## Connection Modes
|
|
17
22
|
|
|
18
23
|
- **This machine only** *(default)*: Local workstation browser (127.0.0.1).
|
|
@@ -43,15 +48,20 @@ herdr-remote plugin link
|
|
|
43
48
|
## Configuration
|
|
44
49
|
|
|
45
50
|
- Settings: `~/.config/herdr-remote/config.json`
|
|
51
|
+
- Windows settings: `%APPDATA%\herdr-remote\config.json`
|
|
46
52
|
- Runtime state: `~/.local/state/herdr-remote/runtime.json` (mode `0600`)
|
|
53
|
+
- Windows runtime state: `%LOCALAPPDATA%\herdr-remote\runtime.json`
|
|
47
54
|
|
|
48
55
|
## Locating Herdr
|
|
49
56
|
|
|
50
57
|
The `herdr` binary is looked up in `HERDR_BIN_PATH`, then `PATH`, then the usual
|
|
51
58
|
install directories (`~/.local/bin`, `~/.cargo/bin`, `~/bin`, `/opt/homebrew/bin`,
|
|
52
59
|
`/usr/local/bin`, …). `herdr-remote keepalive install` writes the binary it found
|
|
53
|
-
and the current `PATH` into the systemd unit
|
|
54
|
-
manager does not inherit your shell's environment.
|
|
60
|
+
and the current `PATH` into the systemd unit, launchd agent, or Windows service
|
|
61
|
+
(`windows-service`), because a service manager does not inherit your shell's environment.
|
|
62
|
+
|
|
63
|
+
On Windows, only `herdr.exe` is searched for, including `%LOCALAPPDATA%\Programs\Herdr\bin`,
|
|
64
|
+
`~\.cargo\bin`, and `~\scoop\shims`.
|
|
55
65
|
|
|
56
66
|
`herdr-remote status --json` reports `host.herdrCommand` and `host.herdrCommandFound`.
|
|
57
67
|
If it was not found, set `HERDR_BIN_PATH` to the full path and reinstall the
|
package/README.zh-CN.md
CHANGED
|
@@ -13,6 +13,11 @@ herdr-remote
|
|
|
13
13
|
|
|
14
14
|
直接运行 `herdr-remote` 即可启动向导并进入配置 TUI。
|
|
15
15
|
|
|
16
|
+
已支持 Windows 10/11 原生运行(需 Node.js 22+),请在 PowerShell 中执行
|
|
17
|
+
`powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"` 安装 Herdr。
|
|
18
|
+
Herdr 的 Windows 插件功能仍处于 preview 阶段;首次以局域网模式启动时,请在 Windows
|
|
19
|
+
Defender 防火墙提示中允许 `node.exe` 通过专用网络通信。
|
|
20
|
+
|
|
16
21
|
## 访问模式
|
|
17
22
|
|
|
18
23
|
- **仅本机** *(默认)*:仅本机浏览器可访问 (127.0.0.1)。
|
|
@@ -43,14 +48,19 @@ herdr-remote plugin link
|
|
|
43
48
|
## 配置路径
|
|
44
49
|
|
|
45
50
|
- 配置文件:`~/.config/herdr-remote/config.json`
|
|
51
|
+
- Windows 配置文件:`%APPDATA%\herdr-remote\config.json`
|
|
46
52
|
- 运行状态:`~/.local/state/herdr-remote/runtime.json` (权限 `0600`)
|
|
53
|
+
- Windows 运行状态:`%LOCALAPPDATA%\herdr-remote\runtime.json`
|
|
47
54
|
|
|
48
55
|
## herdr 命令的查找方式
|
|
49
56
|
|
|
50
57
|
依次查找 `HERDR_BIN_PATH`、`PATH`,以及常见安装目录(`~/.local/bin`、`~/.cargo/bin`、
|
|
51
58
|
`~/bin`、`/opt/homebrew/bin`、`/usr/local/bin` 等)。服务管理器不会继承 shell 环境,
|
|
52
59
|
因此 `herdr-remote keepalive install` 会把找到的可执行文件路径和当前 `PATH` 写入
|
|
53
|
-
systemd unit 或
|
|
60
|
+
systemd unit、launchd plist 或 Windows 服务(`windows-service`)。
|
|
61
|
+
|
|
62
|
+
Windows 上只查找 `herdr.exe`,还会搜索 `%LOCALAPPDATA%\Programs\Herdr\bin`、
|
|
63
|
+
`~\.cargo\bin` 和 `~\scoop\shims`。
|
|
54
64
|
|
|
55
65
|
`herdr-remote status --json` 会输出 `host.herdrCommand` 与 `host.herdrCommandFound`。
|
|
56
66
|
若显示未找到,请将 `HERDR_BIN_PATH` 设为完整路径后重新安装保活服务。
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// the subcommands exist for scripting and for the actions declared in
|
|
5
5
|
// herdr-plugin.toml, which Herdr runs without a terminal attached.
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { LANGUAGES, loadConfig, migrateLegacyConfig } from './config.js';
|
|
9
10
|
import { PACKAGE_ROOT, configPath, stateDir } from './paths.js';
|
|
@@ -49,6 +50,10 @@ function parseArgs(argv) {
|
|
|
49
50
|
flags.daemon = true;
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
53
|
+
if (arg === '--elevated') {
|
|
54
|
+
flags.elevated = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
52
57
|
if (arg === '--help' || arg === '-h') {
|
|
53
58
|
flags.help = true;
|
|
54
59
|
continue;
|
|
@@ -108,6 +113,16 @@ async function runTui(options) {
|
|
|
108
113
|
}
|
|
109
114
|
async function main(argv = process.argv.slice(2)) {
|
|
110
115
|
const { positional, flags } = parseArgs(argv);
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
const relative = path.relative(PACKAGE_ROOT, process.cwd());
|
|
118
|
+
const insidePackage = relative === '' ||
|
|
119
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
120
|
+
if (insidePackage) {
|
|
121
|
+
// Herdr starts plugin panels in PACKAGE_ROOT; Windows locks the cwd,
|
|
122
|
+
// preventing npm from replacing that directory.
|
|
123
|
+
process.chdir(os.homedir());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
111
126
|
const command = positional[0] || null;
|
|
112
127
|
// Answered before anything reads the config or queries the terminal.
|
|
113
128
|
if (flags.help) {
|
|
@@ -128,7 +143,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
128
143
|
// TUI takes the screen a few lines below, and the services this command
|
|
129
144
|
// starts usually run detached with nothing to ask. Everything downstream
|
|
130
145
|
// inherits the answer through the environment.
|
|
131
|
-
(await import('./terminal-palette.js')).captureTerminalPalette();
|
|
146
|
+
await (await import('./terminal-palette.js')).captureTerminalPalette();
|
|
132
147
|
// The font, likewise, is read while this process still knows which terminal
|
|
133
148
|
// it runs in: its variables and its parent process are gone once a service
|
|
134
149
|
// manager or Herdr's detached server starts the connector.
|
|
@@ -210,10 +225,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
210
225
|
case 'keepalive': {
|
|
211
226
|
const keepalive = await import('./keepalive/index.js');
|
|
212
227
|
const action = positional[1] || 'status';
|
|
213
|
-
if (action === 'install')
|
|
214
|
-
|
|
228
|
+
if (action === 'install' && flags.elevated) {
|
|
229
|
+
const { completeServiceInstall } = await import('./keepalive/windows-service-setup.js');
|
|
230
|
+
process.exitCode = await completeServiceInstall(t);
|
|
231
|
+
}
|
|
232
|
+
else if (action === 'install')
|
|
233
|
+
printJson(await keepalive.install(config));
|
|
215
234
|
else if (action === 'uninstall')
|
|
216
|
-
printJson(keepalive.uninstall(config));
|
|
235
|
+
printJson(await keepalive.uninstall(config));
|
|
217
236
|
else if (action === 'restart')
|
|
218
237
|
printJson(keepalive.restart(config));
|
|
219
238
|
else if (action === 'status')
|
package/dist/config.js
CHANGED
|
@@ -20,7 +20,14 @@ const ACCESS_MODES = ['local', 'lan', 'remote'];
|
|
|
20
20
|
// workstation stays reachable only through its own host token.
|
|
21
21
|
const OFFICIAL_RELAY_URL = 'wss://herdr-remote.564616.xyz';
|
|
22
22
|
const LANGUAGES = ['auto', 'zh', 'en'];
|
|
23
|
-
const KEEPALIVE_MANAGERS = [
|
|
23
|
+
const KEEPALIVE_MANAGERS = [
|
|
24
|
+
'auto',
|
|
25
|
+
'systemd',
|
|
26
|
+
'launchd',
|
|
27
|
+
'supervisor',
|
|
28
|
+
'windows-service',
|
|
29
|
+
'none',
|
|
30
|
+
];
|
|
24
31
|
function isOneOf(list, value) {
|
|
25
32
|
return list.includes(value);
|
|
26
33
|
}
|
package/dist/herdr-api.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* phone can answer "does anything need me?" without navigating a TUI through a
|
|
8
8
|
* 40-column viewport.
|
|
9
9
|
*
|
|
10
|
-
* The wire format is newline-delimited JSON over the Unix socket
|
|
10
|
+
* The wire format is newline-delimited JSON over the Unix socket or, on
|
|
11
|
+
* Windows, the named pipe it names:
|
|
11
12
|
*
|
|
12
13
|
* request {"id":"…","method":"session.snapshot","params":{}}
|
|
13
14
|
* response {"id":"…","result":{…}} or {"id":"…","error":{"code","message"}}
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
* the PTYs keep streaming while this quietly fails and is asked again later.
|
|
29
30
|
*/
|
|
30
31
|
import net from 'node:net';
|
|
32
|
+
import { herdrEndpoint } from './socket-discovery.js';
|
|
31
33
|
/** How long one question waits, connection included. */
|
|
32
34
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
33
35
|
/** An answer this long is not an answer. Guards a socket that never newlines. */
|
|
@@ -69,7 +71,7 @@ function requestHerdr(socketPath, method, params = {}, options = {}) {
|
|
|
69
71
|
if (typeof timer.unref === 'function')
|
|
70
72
|
timer.unref();
|
|
71
73
|
try {
|
|
72
|
-
socket = connect(socketPath);
|
|
74
|
+
socket = connect(herdrEndpoint(socketPath));
|
|
73
75
|
}
|
|
74
76
|
catch (error) {
|
|
75
77
|
finish(error);
|
|
@@ -145,7 +147,7 @@ function subscribeHerdr(socketPath, subscriptions, onEvent, options = {}) {
|
|
|
145
147
|
return;
|
|
146
148
|
let current;
|
|
147
149
|
try {
|
|
148
|
-
current = connect(socketPath);
|
|
150
|
+
current = connect(herdrEndpoint(socketPath));
|
|
149
151
|
}
|
|
150
152
|
catch {
|
|
151
153
|
scheduleReconnect();
|
package/dist/herdr-command.js
CHANGED
|
@@ -48,11 +48,11 @@ function expandHome(directory, home) {
|
|
|
48
48
|
return path.join(home, directory.slice(2));
|
|
49
49
|
return directory;
|
|
50
50
|
}
|
|
51
|
-
/** Windows
|
|
52
|
-
function candidateNames(base = COMMAND_NAME) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
/** Windows only publishes Herdr as an executable; shell scripts need a shell. */
|
|
52
|
+
function candidateNames(base = COMMAND_NAME, platform = process.platform) {
|
|
53
|
+
if (platform !== 'win32')
|
|
54
|
+
return [base];
|
|
55
|
+
return [/\.exe$/i.test(base) ? base : `${base}.exe`];
|
|
56
56
|
}
|
|
57
57
|
function isExecutableFile(candidate) {
|
|
58
58
|
try {
|
|
@@ -83,7 +83,14 @@ function findOnSearchPath(searchPath, names = candidateNames()) {
|
|
|
83
83
|
}
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
|
-
function fallbackDirectories(home = os.homedir()) {
|
|
86
|
+
function fallbackDirectories(home = os.homedir(), platform = process.platform, env = process.env) {
|
|
87
|
+
if (platform === 'win32') {
|
|
88
|
+
return [
|
|
89
|
+
path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Programs', 'Herdr', 'bin'),
|
|
90
|
+
path.join(home, '.cargo', 'bin'),
|
|
91
|
+
path.join(home, 'scoop', 'shims'),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
87
94
|
return FALLBACK_DIRECTORIES.map((directory) => expandHome(directory, home));
|
|
88
95
|
}
|
|
89
96
|
function looksLikePath(value) {
|
|
@@ -98,7 +105,7 @@ function looksLikePath(value) {
|
|
|
98
105
|
* treated as absent rather than fatal: a stale override left behind by a
|
|
99
106
|
* moved install is precisely the case we are trying to survive.
|
|
100
107
|
*/
|
|
101
|
-
function resolveOverride(value, searchPath) {
|
|
108
|
+
function resolveOverride(value, searchPath, platform) {
|
|
102
109
|
const trimmed = String(value || '').trim();
|
|
103
110
|
if (!trimmed)
|
|
104
111
|
return null;
|
|
@@ -106,9 +113,14 @@ function resolveOverride(value, searchPath) {
|
|
|
106
113
|
const absolute = path.resolve(trimmed);
|
|
107
114
|
if (isExecutableFile(absolute))
|
|
108
115
|
return absolute;
|
|
109
|
-
|
|
116
|
+
if (platform === 'win32' && path.extname(absolute) === '') {
|
|
117
|
+
const executable = `${absolute}.exe`;
|
|
118
|
+
if (isExecutableFile(executable))
|
|
119
|
+
return executable;
|
|
120
|
+
}
|
|
121
|
+
return findInDirectory(absolute, candidateNames(COMMAND_NAME, platform));
|
|
110
122
|
}
|
|
111
|
-
return findOnSearchPath(searchPath, candidateNames(trimmed));
|
|
123
|
+
return findOnSearchPath(searchPath, candidateNames(trimmed, platform));
|
|
112
124
|
}
|
|
113
125
|
/**
|
|
114
126
|
* Everything known about where Herdr is, so callers can report it and not only
|
|
@@ -120,15 +132,16 @@ function resolveOverride(value, searchPath) {
|
|
|
120
132
|
* bare name: `PATH` may be right in a way we cannot see from here, and letting
|
|
121
133
|
* the spawn proceed keeps the old behaviour for anyone relying on it.
|
|
122
134
|
*/
|
|
123
|
-
function findHerdrCommand({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home), } = {}) {
|
|
124
|
-
const
|
|
135
|
+
function findHerdrCommand({ env = process.env, home = os.homedir(), platform = process.platform, directories = fallbackDirectories(home, platform, env), } = {}) {
|
|
136
|
+
const names = candidateNames(COMMAND_NAME, platform);
|
|
137
|
+
const override = resolveOverride(env.HERDR_BIN_PATH, env.PATH, platform);
|
|
125
138
|
if (override)
|
|
126
139
|
return { command: override, source: 'env', found: true };
|
|
127
|
-
const onPath = findOnSearchPath(env.PATH);
|
|
140
|
+
const onPath = findOnSearchPath(env.PATH, names);
|
|
128
141
|
if (onPath)
|
|
129
142
|
return { command: onPath, source: 'path', found: true };
|
|
130
143
|
for (const directory of directories) {
|
|
131
|
-
const found = findInDirectory(directory);
|
|
144
|
+
const found = findInDirectory(directory, names);
|
|
132
145
|
if (found)
|
|
133
146
|
return { command: found, source: 'fallback', found: true };
|
|
134
147
|
}
|
|
@@ -185,7 +198,11 @@ function herdrVersion({ command, timeout = 10_000, ...lookup } = {}) {
|
|
|
185
198
|
};
|
|
186
199
|
let result;
|
|
187
200
|
try {
|
|
188
|
-
result = spawnSync(resolved, ['--version'], {
|
|
201
|
+
result = spawnSync(resolved, ['--version'], {
|
|
202
|
+
encoding: 'utf8',
|
|
203
|
+
timeout,
|
|
204
|
+
windowsHide: true,
|
|
205
|
+
});
|
|
189
206
|
}
|
|
190
207
|
catch {
|
|
191
208
|
return unknown;
|
|
@@ -211,7 +228,7 @@ function herdrOutdatedMessage(version) {
|
|
|
211
228
|
'misbehave in browser windows before that release.');
|
|
212
229
|
}
|
|
213
230
|
/** The one message a user needs to fix a missing install themselves. */
|
|
214
|
-
function herdrNotFoundMessage({ env = process.env, home = os.homedir(), directories = fallbackDirectories(home), } = {}) {
|
|
231
|
+
function herdrNotFoundMessage({ env = process.env, home = os.homedir(), platform = process.platform, directories = fallbackDirectories(home, platform, env), } = {}) {
|
|
215
232
|
const override = String(env.HERDR_BIN_PATH || '').trim();
|
|
216
233
|
const parts = [`Herdr executable "${COMMAND_NAME}" was not found.`];
|
|
217
234
|
if (override)
|
package/dist/herdr-plugin.js
CHANGED
|
@@ -17,7 +17,11 @@ function manifestPath() {
|
|
|
17
17
|
// manager, neither of which is guaranteed the PATH the user installed with, so
|
|
18
18
|
// the command is resolved rather than named.
|
|
19
19
|
function runHerdr(args, { timeout = 15_000 } = {}) {
|
|
20
|
-
const result = spawnSync(resolveHerdrCommand(), args, {
|
|
20
|
+
const result = spawnSync(resolveHerdrCommand(), args, {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
timeout,
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
});
|
|
21
25
|
if (result.error?.code === 'ENOENT') {
|
|
22
26
|
throw Object.assign(new Error('herdr command not found'), { code: 'HERDR_NOT_FOUND' });
|
|
23
27
|
}
|
package/dist/herdr-server.js
CHANGED
|
@@ -21,7 +21,7 @@ import fs from 'node:fs';
|
|
|
21
21
|
import net from 'node:net';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { spawn } from 'node:child_process';
|
|
24
|
-
import { inspectSocket } from './socket-discovery.js';
|
|
24
|
+
import { herdrEndpoint, inspectSocket } from './socket-discovery.js';
|
|
25
25
|
import { ensureDir } from 'herdr-remote-relay/state';
|
|
26
26
|
/** A Unix socket either accepts at once or is dead; this only guards a hang. */
|
|
27
27
|
const PROBE_TIMEOUT_MS = 1_000;
|
|
@@ -58,7 +58,7 @@ function probeHerdrServer(socketPath, { connect = net.connect, timeout = PROBE_T
|
|
|
58
58
|
const timer = setTimeout(() => finish({ state: 'running' }), timeout);
|
|
59
59
|
if (typeof timer.unref === 'function')
|
|
60
60
|
timer.unref();
|
|
61
|
-
socket = connect(socketPath);
|
|
61
|
+
socket = connect(herdrEndpoint(socketPath));
|
|
62
62
|
socket.once('connect', () => finish({ state: 'running' }));
|
|
63
63
|
socket.once('error', (error) => finish(error.code === 'ECONNREFUSED' || error.code === 'ENOENT'
|
|
64
64
|
? { state: 'stopped', stale: true }
|
|
@@ -130,6 +130,7 @@ function launchHerdrServer({ command, args = [], socketPath, cwd, logPath, spawn
|
|
|
130
130
|
cwd,
|
|
131
131
|
env: serverEnv(socketPath),
|
|
132
132
|
detached: true,
|
|
133
|
+
windowsHide: true,
|
|
133
134
|
stdio: ['ignore', out, out],
|
|
134
135
|
});
|
|
135
136
|
}
|
package/dist/i18n/en.js
CHANGED
|
@@ -131,6 +131,14 @@ const en = {
|
|
|
131
131
|
'keepalive.enableLinger': 'Enable start at boot',
|
|
132
132
|
'keepalive.lingerDone': 'Start at boot enabled for {username}.',
|
|
133
133
|
'keepalive.fallbackNote': 'No system service manager found; background process will not survive reboot.',
|
|
134
|
+
'keepalive.serviceNote': 'Runs as a Windows service under your account and starts at boot, before anyone signs in. Installing or removing it asks for administrator rights.',
|
|
135
|
+
'keepalive.serviceWindowHint': 'Finish the installation in the administrator window that opens.',
|
|
136
|
+
'keepalive.elevationCancelled': 'Administrator rights were not granted; nothing was installed.',
|
|
137
|
+
'keepalive.passwordPrompt': 'Windows password for {account} (the service signs in as you):',
|
|
138
|
+
'keepalive.passwordRejected': 'Windows did not accept that password. Try again.',
|
|
139
|
+
'keepalive.serviceInstalled': 'Service {id} is installed and running.',
|
|
140
|
+
'keepalive.wrongAccount': 'Run this as the account that will use herdr-remote, not another administrator account.',
|
|
141
|
+
'keepalive.pressEnter': 'Press Enter to close this window.',
|
|
134
142
|
'herdr.title': 'Herdr integration',
|
|
135
143
|
'herdr.socketPath': 'Socket path',
|
|
136
144
|
'herdr.args': 'Extra arguments',
|
|
@@ -219,5 +227,17 @@ const en = {
|
|
|
219
227
|
'update.mirrorBehind': '{registries} still has an older version; installing from {source}',
|
|
220
228
|
'update.cannot.source': 'Source checkout — update with git',
|
|
221
229
|
'update.cannot.linked': 'Linked package — managed by npm link',
|
|
230
|
+
'relayUpdate.check': 'Relay {version}: check for updates',
|
|
231
|
+
'relayUpdate.checking': 'Relay: checking npm…',
|
|
232
|
+
'relayUpdate.upToDate': 'Relay up to date ({version})',
|
|
233
|
+
'relayUpdate.available': 'Relay {latest} available, installed {installed} (press Enter to install)',
|
|
234
|
+
'relayUpdate.needsCli': 'Relay {latest} needs a newer herdr-remote: update herdr-remote first',
|
|
235
|
+
'relayUpdate.updating': 'Installing relay {version}…',
|
|
236
|
+
'relayUpdate.updatingRetry': 'Installing relay {version}… (npm has not caught up yet, attempt {attempt})',
|
|
237
|
+
'relayUpdate.restart': 'Relay {version} installed, {running} still running (press Enter to restart)',
|
|
238
|
+
'relayUpdate.restarting': 'Restarting herdr-remote…',
|
|
239
|
+
'relayUpdate.restarted': 'herdr-remote restarted; the relay now runs {version}.',
|
|
240
|
+
'relayUpdate.errorNotApplied': 'npm finished, but relay {installed} is still installed',
|
|
241
|
+
'relayUpdate.banner': 'Relay {latest} is out (installed {current}). Press 7 and Enter on the relay row to install.',
|
|
222
242
|
};
|
|
223
243
|
export default en;
|
package/dist/i18n/zh.js
CHANGED
|
@@ -130,6 +130,14 @@ const zh = {
|
|
|
130
130
|
'keepalive.enableLinger': '开启开机自启',
|
|
131
131
|
'keepalive.lingerDone': '已为 {username} 开启开机自启。',
|
|
132
132
|
'keepalive.fallbackNote': '无系统服务管理器,使用后台守护进程,重启后不自动恢复。',
|
|
133
|
+
'keepalive.serviceNote': '以你的账户作为 Windows 服务运行,开机后、登录前自动启动。安装或移除需要管理员权限。',
|
|
134
|
+
'keepalive.serviceWindowHint': '请在随后打开的管理员窗口中完成安装。',
|
|
135
|
+
'keepalive.elevationCancelled': '未授予管理员权限,未安装服务。',
|
|
136
|
+
'keepalive.passwordPrompt': '请输入 {account} 的 Windows 密码(服务将以你的账户登录):',
|
|
137
|
+
'keepalive.passwordRejected': 'Windows 不接受这个密码,请重试。',
|
|
138
|
+
'keepalive.serviceInstalled': '服务 {id} 已安装并正在运行。',
|
|
139
|
+
'keepalive.wrongAccount': '请使用将运行 herdr-remote 的账户执行此操作,不要换用其他管理员账户。',
|
|
140
|
+
'keepalive.pressEnter': '按 Enter 关闭此窗口。',
|
|
133
141
|
'herdr.title': 'Herdr 集成',
|
|
134
142
|
'herdr.socketPath': '套接字路径',
|
|
135
143
|
'herdr.args': '附加参数',
|
|
@@ -218,5 +226,17 @@ const zh = {
|
|
|
218
226
|
'update.mirrorBehind': '{registries} 仍是旧版本,将从 {source} 安装',
|
|
219
227
|
'update.cannot.source': '源码目录运行,请使用 git 更新',
|
|
220
228
|
'update.cannot.linked': 'npm link 工作副本,由 npm link 管理',
|
|
229
|
+
'relayUpdate.check': 'Relay {version}:检查更新',
|
|
230
|
+
'relayUpdate.checking': 'Relay:查询 npm 中…',
|
|
231
|
+
'relayUpdate.upToDate': 'Relay 已是最新({version})',
|
|
232
|
+
'relayUpdate.available': 'Relay 新版本 {latest} 可用,当前 {installed}(按 Enter 安装)',
|
|
233
|
+
'relayUpdate.needsCli': 'Relay {latest} 需要更新 herdr-remote,请先更新上一行',
|
|
234
|
+
'relayUpdate.updating': '安装 relay {version} 中…',
|
|
235
|
+
'relayUpdate.updatingRetry': '安装 relay {version} 中…(npm 尚未同步,第 {attempt} 次尝试)',
|
|
236
|
+
'relayUpdate.restart': 'Relay {version} 已安装,运行中的仍是 {running}(按 Enter 重启生效)',
|
|
237
|
+
'relayUpdate.restarting': '正在重启 herdr-remote…',
|
|
238
|
+
'relayUpdate.restarted': 'herdr-remote 已重启,relay 现在运行 {version}。',
|
|
239
|
+
'relayUpdate.errorNotApplied': 'npm 已完成,但安装的仍是 relay {installed}',
|
|
240
|
+
'relayUpdate.banner': 'Relay {latest} 已发布(当前 {current}),按 7 进入「语言与关于」,在 Relay 一行按 Enter 安装。',
|
|
221
241
|
};
|
|
222
242
|
export default zh;
|
|
@@ -25,6 +25,7 @@ function start() {
|
|
|
25
25
|
cwd: PACKAGE_ROOT,
|
|
26
26
|
env: { ...process.env, HERDR_REMOTE_SERVICE: '1', ...serviceEnvironment() },
|
|
27
27
|
detached: true,
|
|
28
|
+
windowsHide: true,
|
|
28
29
|
stdio: ['ignore', logFd, logFd],
|
|
29
30
|
});
|
|
30
31
|
child.unref();
|
|
@@ -48,6 +49,11 @@ function stopAndForget() {
|
|
|
48
49
|
fs.rmSync(fallbackPidPath(), { force: true });
|
|
49
50
|
return { ok: true, stoppedPid: pid };
|
|
50
51
|
}
|
|
52
|
+
export function detachedLogsHint(platform = process.platform, logFile = logPath('supervisor')) {
|
|
53
|
+
if (platform === 'win32')
|
|
54
|
+
return `Get-Content -Wait -Tail 40 "${logFile}"`;
|
|
55
|
+
return `tail -f ${logFile}`;
|
|
56
|
+
}
|
|
51
57
|
export const detached = {
|
|
52
58
|
name: 'supervisor',
|
|
53
59
|
status() {
|
|
@@ -63,8 +69,12 @@ export const detached = {
|
|
|
63
69
|
state: alive ? 'running' : 'stopped',
|
|
64
70
|
};
|
|
65
71
|
},
|
|
66
|
-
install
|
|
67
|
-
|
|
72
|
+
async install() {
|
|
73
|
+
return start();
|
|
74
|
+
},
|
|
75
|
+
async uninstall() {
|
|
76
|
+
return stopAndForget();
|
|
77
|
+
},
|
|
68
78
|
restart() {
|
|
69
79
|
stopAndForget();
|
|
70
80
|
return start();
|
|
@@ -73,6 +83,6 @@ export const detached = {
|
|
|
73
83
|
stopAndForget();
|
|
74
84
|
},
|
|
75
85
|
logsHint() {
|
|
76
|
-
return
|
|
86
|
+
return detachedLogsHint();
|
|
77
87
|
},
|
|
78
88
|
};
|
package/dist/keepalive/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loadConfig } from '../config.js';
|
|
|
13
13
|
import { detached } from './detached.js';
|
|
14
14
|
import { launchd } from './launchd.js';
|
|
15
15
|
import { systemd } from './systemd.js';
|
|
16
|
+
import { windowsService } from './windows-service.js';
|
|
16
17
|
export { serviceEnvironment, servicePath } from './environment.js';
|
|
17
18
|
export { escapeXml, renderLaunchdPlist } from './launchd.js';
|
|
18
19
|
export { renderSystemdUnit, systemdEnvironmentLine } from './systemd.js';
|
|
@@ -36,10 +37,12 @@ function commandExists(command) {
|
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
/** Which manager to use given the platform, the user's preference and reality. */
|
|
39
|
-
export function detectManager(preference = 'auto') {
|
|
40
|
+
export function detectManager(preference = 'auto', platform = process.platform) {
|
|
40
41
|
if (preference && preference !== 'auto')
|
|
41
42
|
return preference;
|
|
42
|
-
if (
|
|
43
|
+
if (platform === 'win32')
|
|
44
|
+
return 'windows-service';
|
|
45
|
+
if (platform === 'linux') {
|
|
43
46
|
// `systemctl --user` needs a user bus; containers and bare TTY logins often
|
|
44
47
|
// have systemd installed but no session bus, where it would fail at runtime.
|
|
45
48
|
if (commandExists('systemctl') &&
|
|
@@ -48,7 +51,7 @@ export function detectManager(preference = 'auto') {
|
|
|
48
51
|
}
|
|
49
52
|
return 'supervisor';
|
|
50
53
|
}
|
|
51
|
-
if (
|
|
54
|
+
if (platform === 'darwin') {
|
|
52
55
|
return commandExists('launchctl') ? 'launchd' : 'supervisor';
|
|
53
56
|
}
|
|
54
57
|
return 'supervisor';
|
|
@@ -63,6 +66,8 @@ function backendOf(config) {
|
|
|
63
66
|
return systemd;
|
|
64
67
|
if (manager === 'launchd')
|
|
65
68
|
return launchd;
|
|
69
|
+
if (manager === 'windows-service')
|
|
70
|
+
return windowsService;
|
|
66
71
|
return detached;
|
|
67
72
|
}
|
|
68
73
|
export function status(config = loadConfig()) {
|
|
@@ -70,15 +75,15 @@ export function status(config = loadConfig()) {
|
|
|
70
75
|
return { manager: 'none', installed: false, active: false, enabled: false };
|
|
71
76
|
return backendOf(config).status();
|
|
72
77
|
}
|
|
73
|
-
export function install(config = loadConfig()) {
|
|
78
|
+
export async function install(config = loadConfig()) {
|
|
74
79
|
if (managerOf(config) === 'none')
|
|
75
80
|
throw new Error('keep-alive is disabled in the configuration');
|
|
76
81
|
const backend = backendOf(config);
|
|
77
|
-
return { manager: backend.name, ...backend.install() };
|
|
82
|
+
return { manager: backend.name, ...(await backend.install()) };
|
|
78
83
|
}
|
|
79
|
-
export function uninstall(config = loadConfig()) {
|
|
84
|
+
export async function uninstall(config = loadConfig()) {
|
|
80
85
|
const backend = backendOf(config);
|
|
81
|
-
return { manager: backend.name, ...backend.uninstall() };
|
|
86
|
+
return { manager: backend.name, ...(await backend.uninstall()) };
|
|
82
87
|
}
|
|
83
88
|
/** Restart whatever manages the services, so config edits take effect. */
|
|
84
89
|
export function restart(config = loadConfig()) {
|
|
@@ -99,7 +104,10 @@ export function stopManaged(config = loadConfig()) {
|
|
|
99
104
|
}
|
|
100
105
|
export function enableLinger() {
|
|
101
106
|
const username = os.userInfo().username;
|
|
102
|
-
const result = spawnSync('loginctl', ['enable-linger', username], {
|
|
107
|
+
const result = spawnSync('loginctl', ['enable-linger', username], {
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
windowsHide: true,
|
|
110
|
+
});
|
|
103
111
|
if (result.status !== 0) {
|
|
104
112
|
throw new Error(String(result.stderr || '').trim() || 'loginctl enable-linger failed');
|
|
105
113
|
}
|
|
@@ -58,11 +58,11 @@ function serviceTarget() {
|
|
|
58
58
|
return `${domainTarget()}/${LAUNCHD_LABEL}`;
|
|
59
59
|
}
|
|
60
60
|
function launchctl(args) {
|
|
61
|
-
return spawnSync('launchctl', args, { encoding: 'utf8' });
|
|
61
|
+
return spawnSync('launchctl', args, { encoding: 'utf8', windowsHide: true });
|
|
62
62
|
}
|
|
63
63
|
/** A launchctl call whose outcome does not matter (bootout of nothing, say). */
|
|
64
64
|
function launchctlQuietly(args) {
|
|
65
|
-
spawnSync('launchctl', args, { stdio: 'ignore' });
|
|
65
|
+
spawnSync('launchctl', args, { stdio: 'ignore', windowsHide: true });
|
|
66
66
|
}
|
|
67
67
|
export const launchd = {
|
|
68
68
|
name: 'launchd',
|
|
@@ -81,7 +81,7 @@ export const launchd = {
|
|
|
81
81
|
state: result.status === 0 ? 'loaded' : 'not loaded',
|
|
82
82
|
};
|
|
83
83
|
},
|
|
84
|
-
install() {
|
|
84
|
+
async install() {
|
|
85
85
|
const plistPath = launchdPlistPath();
|
|
86
86
|
ensureDir(path.dirname(plistPath));
|
|
87
87
|
ensureDir(stateDir());
|
|
@@ -97,7 +97,7 @@ export const launchd = {
|
|
|
97
97
|
launchctlQuietly(['enable', serviceTarget()]);
|
|
98
98
|
return { ok: true, unitPath: plistPath };
|
|
99
99
|
},
|
|
100
|
-
uninstall() {
|
|
100
|
+
async uninstall() {
|
|
101
101
|
const plistPath = launchdPlistPath();
|
|
102
102
|
launchctlQuietly(['bootout', serviceTarget()]);
|
|
103
103
|
fs.rmSync(plistPath, { force: true });
|
|
@@ -46,7 +46,11 @@ WantedBy=default.target
|
|
|
46
46
|
`;
|
|
47
47
|
}
|
|
48
48
|
function systemctl(args) {
|
|
49
|
-
return spawnSync('systemctl', ['--user', ...args], {
|
|
49
|
+
return spawnSync('systemctl', ['--user', ...args], {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: 'pipe',
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
50
54
|
}
|
|
51
55
|
/** Run a systemctl command that must succeed, throwing its stderr when it does not. */
|
|
52
56
|
function systemctlOrThrow(args, failure) {
|
|
@@ -63,7 +67,7 @@ export const systemd = {
|
|
|
63
67
|
return { manager: 'systemd', installed: false, active: false, enabled: false };
|
|
64
68
|
const active = systemctl(['is-active', SYSTEMD_UNIT_NAME]);
|
|
65
69
|
const enabled = systemctl(['is-enabled', SYSTEMD_UNIT_NAME]);
|
|
66
|
-
const lingering = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8' });
|
|
70
|
+
const lingering = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8', windowsHide: true });
|
|
67
71
|
return {
|
|
68
72
|
manager: 'systemd',
|
|
69
73
|
installed: true,
|
|
@@ -76,7 +80,7 @@ export const systemd = {
|
|
|
76
80
|
state: String(active.stdout || active.stderr || '').trim(),
|
|
77
81
|
};
|
|
78
82
|
},
|
|
79
|
-
install() {
|
|
83
|
+
async install() {
|
|
80
84
|
const unitPath = systemdUnitPath();
|
|
81
85
|
ensureDir(path.dirname(unitPath));
|
|
82
86
|
fs.writeFileSync(unitPath, renderSystemdUnit({ environment: serviceEnvironment() }), {
|
|
@@ -86,7 +90,7 @@ export const systemd = {
|
|
|
86
90
|
systemctlOrThrow(['enable', '--now', SYSTEMD_UNIT_NAME], 'systemctl --user enable --now failed');
|
|
87
91
|
return { ok: true, unitPath, hint: `loginctl enable-linger ${os.userInfo().username}` };
|
|
88
92
|
},
|
|
89
|
-
uninstall() {
|
|
93
|
+
async uninstall() {
|
|
90
94
|
const unitPath = systemdUnitPath();
|
|
91
95
|
systemctl(['disable', '--now', SYSTEMD_UNIT_NAME]);
|
|
92
96
|
fs.rmSync(unitPath, { force: true });
|