herdr-remote 0.2.23 → 0.2.25

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 CHANGED
@@ -57,8 +57,9 @@ herdr-remote plugin link
57
57
  The `herdr` binary is looked up in `HERDR_BIN_PATH`, then `PATH`, then the usual
58
58
  install directories (`~/.local/bin`, `~/.cargo/bin`, `~/bin`, `/opt/homebrew/bin`,
59
59
  `/usr/local/bin`, …). `herdr-remote keepalive install` writes the binary it found
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.
60
+ and the current `PATH` into the systemd unit, launchd agent, Windows logon task
61
+ (`windows-task`), or Windows service (`windows-service`), because managed processes
62
+ do not inherit your shell's environment.
62
63
 
63
64
  On Windows, only `herdr.exe` is searched for, including `%LOCALAPPDATA%\Programs\Herdr\bin`,
64
65
  `~\.cargo\bin`, and `~\scoop\shims`.
package/README.zh-CN.md CHANGED
@@ -55,9 +55,9 @@ herdr-remote plugin link
55
55
  ## herdr 命令的查找方式
56
56
 
57
57
  依次查找 `HERDR_BIN_PATH`、`PATH`,以及常见安装目录(`~/.local/bin`、`~/.cargo/bin`、
58
- `~/bin`、`/opt/homebrew/bin`、`/usr/local/bin` 等)。服务管理器不会继承 shell 环境,
58
+ `~/bin`、`/opt/homebrew/bin`、`/usr/local/bin` 等)。后台管理器不会继承 shell 环境,
59
59
  因此 `herdr-remote keepalive install` 会把找到的可执行文件路径和当前 `PATH` 写入
60
- systemd unit、launchd plist 或 Windows 服务(`windows-service`)。
60
+ systemd unit、launchd plist、Windows 登录任务(`windows-task`)或 Windows 服务(`windows-service`)。
61
61
 
62
62
  Windows 上只查找 `herdr.exe`,还会搜索 `%LOCALAPPDATA%\Programs\Herdr\bin`、
63
63
  `~\.cargo\bin` 和 `~\scoop\shims`。
package/dist/config.js CHANGED
@@ -25,6 +25,7 @@ const KEEPALIVE_MANAGERS = [
25
25
  'systemd',
26
26
  'launchd',
27
27
  'supervisor',
28
+ 'windows-task',
28
29
  'windows-service',
29
30
  'none',
30
31
  ];
package/dist/i18n/en.js CHANGED
@@ -131,11 +131,12 @@ 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.',
134
+ 'keepalive.windowsTaskNote': 'Starts after you sign in as your account. It needs no password or administrator rights and does not run before sign-in.',
135
+ 'keepalive.serviceNote': 'Runs as a Windows service under your account and starts before sign-in. Installing or removing it asks for administrator rights, and Windows may require your account password.',
135
136
  'keepalive.serviceWindowHint': 'Finish the installation in the administrator window that opens.',
136
137
  '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.',
138
+ 'keepalive.passwordPrompt': 'Windows account password for {account} (not a PIN; press Enter if there is no password):',
139
+ 'keepalive.passwordRejected': 'Windows could not start the service with those credentials. Check the account password and service logon policy.',
139
140
  'keepalive.serviceInstalled': 'Service {id} is installed and running.',
140
141
  'keepalive.wrongAccount': 'Run this as the account that will use herdr-remote, not another administrator account.',
141
142
  'keepalive.pressEnter': 'Press Enter to close this window.',
package/dist/i18n/zh.js CHANGED
@@ -130,11 +130,12 @@ const zh = {
130
130
  'keepalive.enableLinger': '开启开机自启',
131
131
  'keepalive.lingerDone': '已为 {username} 开启开机自启。',
132
132
  'keepalive.fallbackNote': '无系统服务管理器,使用后台守护进程,重启后不自动恢复。',
133
- 'keepalive.serviceNote': '以你的账户作为 Windows 服务运行,开机后、登录前自动启动。安装或移除需要管理员权限。',
133
+ 'keepalive.windowsTaskNote': '在你的账户登录后启动,无需密码或管理员权限;登录前不会运行。',
134
+ 'keepalive.serviceNote': '以你的账户作为 Windows 服务运行,在登录前启动。安装或移除需要管理员权限,Windows 也可能要求账户密码。',
134
135
  'keepalive.serviceWindowHint': '请在随后打开的管理员窗口中完成安装。',
135
136
  'keepalive.elevationCancelled': '未授予管理员权限,未安装服务。',
136
- 'keepalive.passwordPrompt': '请输入 {account} 的 Windows 密码(服务将以你的账户登录):',
137
- 'keepalive.passwordRejected': 'Windows 不接受这个密码,请重试。',
137
+ 'keepalive.passwordPrompt': '请输入 {account} 的 Windows 账户密码(不是 PIN;账户无密码时直接按 Enter):',
138
+ 'keepalive.passwordRejected': 'Windows 未能使用这些凭据启动服务。请检查账户密码和“作为服务登录”策略。',
138
139
  'keepalive.serviceInstalled': '服务 {id} 已安装并正在运行。',
139
140
  'keepalive.wrongAccount': '请使用将运行 herdr-remote 的账户执行此操作,不要换用其他管理员账户。',
140
141
  'keepalive.pressEnter': '按 Enter 关闭此窗口。',
@@ -2,9 +2,8 @@
2
2
  //
3
3
  // "Start the services" and "keep the services running" are different problems:
4
4
  // a detached spawn dies with the first crash and never comes back. This module
5
- // hands supervision to the platform's service manager where one exists
6
- // (systemd --user on Linux, launchd on macOS) and falls back to a detached copy
7
- // of our own supervisor where neither does.
5
+ // hands supervision to the platform's manager where one exists (systemd --user,
6
+ // launchd, or Task Scheduler) and falls back to a detached supervisor elsewhere.
8
7
  import { spawnSync } from 'node:child_process';
9
8
  import fs from 'node:fs';
10
9
  import os from 'node:os';
@@ -14,6 +13,7 @@ import { detached } from './detached.js';
14
13
  import { launchd } from './launchd.js';
15
14
  import { systemd } from './systemd.js';
16
15
  import { windowsService } from './windows-service.js';
16
+ import { windowsTask } from './windows-task.js';
17
17
  export { serviceEnvironment, servicePath } from './environment.js';
18
18
  export { escapeXml, renderLaunchdPlist } from './launchd.js';
19
19
  export { renderSystemdUnit, systemdEnvironmentLine } from './systemd.js';
@@ -41,7 +41,7 @@ export function detectManager(preference = 'auto', platform = process.platform)
41
41
  if (preference && preference !== 'auto')
42
42
  return preference;
43
43
  if (platform === 'win32')
44
- return 'windows-service';
44
+ return 'windows-task';
45
45
  if (platform === 'linux') {
46
46
  // `systemctl --user` needs a user bus; containers and bare TTY logins often
47
47
  // have systemd installed but no session bus, where it would fail at runtime.
@@ -60,16 +60,27 @@ function managerOf(config) {
60
60
  return detectManager(config.keepalive?.manager || 'auto');
61
61
  }
62
62
  /** The backend for `config`. With keep-alive off, uninstalling still cleans up the fallback. */
63
- function backendOf(config) {
64
- const manager = managerOf(config);
63
+ function configuredBackend(manager) {
65
64
  if (manager === 'systemd')
66
65
  return systemd;
67
66
  if (manager === 'launchd')
68
67
  return launchd;
68
+ if (manager === 'windows-task')
69
+ return windowsTask;
69
70
  if (manager === 'windows-service')
70
71
  return windowsService;
71
72
  return detached;
72
73
  }
74
+ function backendOf(config) {
75
+ const manager = managerOf(config);
76
+ if (manager === 'windows-task') {
77
+ if (windowsTask.status().installed)
78
+ return windowsTask;
79
+ if (windowsService.status().installed)
80
+ return windowsService;
81
+ }
82
+ return configuredBackend(manager);
83
+ }
73
84
  export function status(config = loadConfig()) {
74
85
  if (managerOf(config) === 'none')
75
86
  return { manager: 'none', installed: false, active: false, enabled: false };
@@ -78,7 +89,7 @@ export function status(config = loadConfig()) {
78
89
  export async function install(config = loadConfig()) {
79
90
  if (managerOf(config) === 'none')
80
91
  throw new Error('keep-alive is disabled in the configuration');
81
- const backend = backendOf(config);
92
+ const backend = configuredBackend(managerOf(config));
82
93
  return { manager: backend.name, ...(await backend.install()) };
83
94
  }
84
95
  export async function uninstall(config = loadConfig()) {
@@ -59,6 +59,30 @@ function failure(label, result) {
59
59
  const output = String(result.stdout || result.stderr || '').trim();
60
60
  return new Error(`${label} failed (${String(result.status)}): ${output}`);
61
61
  }
62
+ export async function configureAndStartServiceAccount(id, account, t, dependencies = {}) {
63
+ const setAccount = dependencies.setAccount || setServiceAccount;
64
+ const start = dependencies.start || startService;
65
+ const prompt = dependencies.prompt ||
66
+ ((name) => promptSecret(t('keepalive.passwordPrompt', { account: name })));
67
+ const write = dependencies.write || ((message) => process.stdout.write(message));
68
+ for (let attempt = 0; attempt <= MAX_PASSWORD_ATTEMPTS; attempt += 1) {
69
+ const password = attempt === 0 ? '' : await prompt(account);
70
+ if (password === null)
71
+ return false;
72
+ const changed = setAccount(id, account, password);
73
+ if (changed.error || changed.status !== 0)
74
+ throw failure('Setting the Windows service account', changed);
75
+ const started = classifyStartResult(start());
76
+ if (started === 'ok')
77
+ return true;
78
+ if (started !== 'logon-failed') {
79
+ write(`${started.message}\n`);
80
+ return false;
81
+ }
82
+ write(`${t('keepalive.passwordRejected')}\n`);
83
+ }
84
+ return false;
85
+ }
62
86
  /** Complete service installation inside the elevated console. */
63
87
  export async function completeServiceInstall(t) {
64
88
  try {
@@ -87,24 +111,9 @@ export async function completeServiceInstall(t) {
87
111
  throw failure('WinSW install', installed);
88
112
  const { domain, user } = currentServiceAccount();
89
113
  const account = `${domain}\\${user}`;
90
- for (let attempt = 0; attempt < MAX_PASSWORD_ATTEMPTS; attempt += 1) {
91
- const password = await promptSecret(t('keepalive.passwordPrompt', { account }));
92
- if (password === null)
93
- return 1;
94
- const changed = setServiceAccount(paths.id, account, password);
95
- if (changed.error || changed.status !== 0)
96
- throw failure('Setting the Windows service account', changed);
97
- const started = classifyStartResult(startService());
98
- if (started === 'ok') {
99
- process.stdout.write(`${t('keepalive.serviceInstalled', { id: paths.id })}\n`);
100
- return 0;
101
- }
102
- if (started === 'logon-failed') {
103
- process.stdout.write(`${t('keepalive.passwordRejected')}\n`);
104
- continue;
105
- }
106
- process.stdout.write(`${started.message}\n`);
107
- return 1;
114
+ if (await configureAndStartServiceAccount(paths.id, account, t)) {
115
+ process.stdout.write(`${t('keepalive.serviceInstalled', { id: paths.id })}\n`);
116
+ return 0;
108
117
  }
109
118
  return 1;
110
119
  }
@@ -0,0 +1,191 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { configDir, stateDir } from '../paths.js';
5
+ import { detachedLogsHint } from './detached.js';
6
+ import { cliEntryPoint, serviceEnvironment } from './environment.js';
7
+ import { currentServiceAccount, encodePowerShellCommand, escapePowerShellSingleQuoted, serviceId, windowsService, } from './windows-service.js';
8
+ const TASK_START_TIMEOUT_MS = 15_000;
9
+ const TASK_STOP_TIMEOUT_MS = 15_000;
10
+ const TASK_POLL_INTERVAL_MS = 250;
11
+ export function windowsTaskName(username = currentServiceAccount().user) {
12
+ return serviceId(username);
13
+ }
14
+ export function renderWindowsTaskLaunchScript(options) {
15
+ const environment = {
16
+ ...options.environment,
17
+ HERDR_REMOTE_CONFIG_DIR: options.configDirectory,
18
+ HERDR_REMOTE_STATE_DIR: options.stateDirectory,
19
+ HERDR_REMOTE_SERVICE: '1',
20
+ };
21
+ const environmentLines = Object.entries(environment).map(([key, value]) => `$env:${key} = '${escapePowerShellSingleQuoted(value)}'`);
22
+ return `$ErrorActionPreference = 'Stop'
23
+ ${environmentLines.join('\n')}
24
+ Set-Location -LiteralPath '${escapePowerShellSingleQuoted(options.workingDirectory)}'
25
+ & '${escapePowerShellSingleQuoted(options.nodePath)}' '${escapePowerShellSingleQuoted(options.entryPoint)}' run --daemon
26
+ exit $LASTEXITCODE`;
27
+ }
28
+ export function renderWindowsTaskRegistrationScript(options) {
29
+ const taskName = escapePowerShellSingleQuoted(options.taskName);
30
+ const account = escapePowerShellSingleQuoted(options.account);
31
+ const powershellPath = escapePowerShellSingleQuoted(options.powershellPath);
32
+ const workingDirectory = escapePowerShellSingleQuoted(options.workingDirectory);
33
+ const launchArguments = `-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodePowerShellCommand(options.launchScript)}`;
34
+ return `$ErrorActionPreference = 'Stop'
35
+ $action = New-ScheduledTaskAction -Execute '${powershellPath}' -Argument '${launchArguments}' -WorkingDirectory '${workingDirectory}'
36
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User '${account}'
37
+ # Interactive logon uses the signed-in token and avoids storing account credentials.
38
+ $principal = New-ScheduledTaskPrincipal -UserId '${account}' -LogonType Interactive -RunLevel Limited
39
+ $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
40
+ Register-ScheduledTask -TaskName '${taskName}' -Description 'Herdr Remote keep-alive' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
41
+ Enable-ScheduledTask -TaskName '${taskName}' | Out-Null
42
+ Start-ScheduledTask -TaskName '${taskName}'`;
43
+ }
44
+ export function parseWindowsTaskState(output) {
45
+ try {
46
+ const state = JSON.parse(output.replace(/^\uFEFF/, '').trim()).State;
47
+ return state === 'Ready' || state === 'Queued' || state === 'Running' || state === 'Disabled'
48
+ ? state
49
+ : 'Absent';
50
+ }
51
+ catch {
52
+ // Task status is best-effort; malformed scheduler output means no known task.
53
+ return 'Absent';
54
+ }
55
+ }
56
+ function windowsPowerShellPath() {
57
+ const root = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
58
+ return path.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
59
+ }
60
+ function taskStateScript(taskName) {
61
+ const safeName = escapePowerShellSingleQuoted(taskName);
62
+ return `$ErrorActionPreference = 'Stop'
63
+ $task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
64
+ if ($null -eq $task) { '{"State":"Absent"}'; exit 0 }
65
+ [pscustomobject]@{ State = [string]$task.State } | ConvertTo-Json -Compress`;
66
+ }
67
+ function invokePowerShell(script, label) {
68
+ const result = spawnSync(windowsPowerShellPath(), [
69
+ '-NoLogo',
70
+ '-NoProfile',
71
+ '-NonInteractive',
72
+ '-EncodedCommand',
73
+ encodePowerShellCommand(script),
74
+ ], { encoding: 'utf8', windowsHide: true });
75
+ if (result.error || result.status !== 0) {
76
+ const output = String(result.stderr || result.stdout || '').trim();
77
+ throw new Error(`${label} failed (${String(result.status)}): ${output}`);
78
+ }
79
+ return String(result.stdout || '').trim();
80
+ }
81
+ function taskState() {
82
+ const result = spawnSync(windowsPowerShellPath(), [
83
+ '-NoProfile',
84
+ '-NoLogo',
85
+ '-NonInteractive',
86
+ '-EncodedCommand',
87
+ encodePowerShellCommand(taskStateScript(windowsTaskName())),
88
+ ], { encoding: 'utf8', windowsHide: true });
89
+ if (result.error || result.status !== 0) {
90
+ // A status query must not make unrelated CLI commands fail on an unavailable scheduler.
91
+ return 'Absent';
92
+ }
93
+ return parseWindowsTaskState(String(result.stdout || ''));
94
+ }
95
+ function accountName() {
96
+ const { domain, user } = currentServiceAccount();
97
+ return domain ? `${domain}\\${user}` : user;
98
+ }
99
+ function stopTaskScript(taskName) {
100
+ const safeName = escapePowerShellSingleQuoted(taskName);
101
+ return `$ErrorActionPreference = 'Stop'
102
+ $task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
103
+ if ($null -eq $task -or ($task.State -ne 'Running' -and $task.State -ne 'Queued')) { exit 0 }
104
+ Stop-ScheduledTask -TaskName '${safeName}'
105
+ $deadline = [DateTime]::UtcNow.AddMilliseconds(${TASK_STOP_TIMEOUT_MS})
106
+ do {
107
+ Start-Sleep -Milliseconds ${TASK_POLL_INTERVAL_MS}
108
+ $task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
109
+ } while ($task -and ($task.State -eq 'Running' -or $task.State -eq 'Queued') -and [DateTime]::UtcNow -lt $deadline)
110
+ if ($task -and ($task.State -eq 'Running' -or $task.State -eq 'Queued')) { throw 'Scheduled task did not stop in time.' }`;
111
+ }
112
+ function stopTask() {
113
+ const state = taskState();
114
+ if (state === 'Running' || state === 'Queued')
115
+ invokePowerShell(stopTaskScript(windowsTaskName()), 'Stopping the Windows scheduled task');
116
+ }
117
+ function launchOptions() {
118
+ const workingDirectory = stateDir();
119
+ return {
120
+ nodePath: process.execPath,
121
+ entryPoint: cliEntryPoint(),
122
+ workingDirectory,
123
+ configDirectory: configDir(),
124
+ stateDirectory: workingDirectory,
125
+ environment: serviceEnvironment(),
126
+ };
127
+ }
128
+ function status() {
129
+ const state = taskState();
130
+ const installed = state !== 'Absent';
131
+ return {
132
+ manager: 'windows-task',
133
+ installed,
134
+ active: state === 'Running',
135
+ enabled: installed && state !== 'Disabled',
136
+ state: state.toLowerCase(),
137
+ };
138
+ }
139
+ async function install() {
140
+ if (taskState() !== 'Absent')
141
+ stopTask();
142
+ // Remove the earlier service before starting a second supervisor for this user.
143
+ if (windowsService.status().installed)
144
+ await windowsService.uninstall();
145
+ const options = launchOptions();
146
+ await fs.promises.mkdir(options.workingDirectory, { recursive: true });
147
+ const launchScript = renderWindowsTaskLaunchScript(options);
148
+ const script = renderWindowsTaskRegistrationScript({
149
+ taskName: windowsTaskName(),
150
+ account: accountName(),
151
+ powershellPath: windowsPowerShellPath(),
152
+ workingDirectory: options.workingDirectory,
153
+ launchScript,
154
+ });
155
+ invokePowerShell(script, 'Installing the Windows scheduled task');
156
+ const deadline = Date.now() + TASK_START_TIMEOUT_MS;
157
+ let state = taskState();
158
+ while (state !== 'Running' && state !== 'Absent' && Date.now() < deadline) {
159
+ await new Promise((resolve) => setTimeout(resolve, TASK_POLL_INTERVAL_MS));
160
+ state = taskState();
161
+ }
162
+ if (state !== 'Running')
163
+ throw new Error(`Windows scheduled task did not start (state: ${state.toLowerCase()}).`);
164
+ return { ok: true, taskName: windowsTaskName(), state };
165
+ }
166
+ async function uninstall() {
167
+ const name = windowsTaskName();
168
+ if (taskState() !== 'Absent') {
169
+ stopTask();
170
+ const safeName = escapePowerShellSingleQuoted(name);
171
+ invokePowerShell(`Unregister-ScheduledTask -TaskName '${safeName}' -Confirm:$false -ErrorAction Stop`, 'Removing the Windows scheduled task');
172
+ }
173
+ if (windowsService.status().installed)
174
+ await windowsService.uninstall();
175
+ return { ok: true, taskName: name };
176
+ }
177
+ function restart() {
178
+ stopTask();
179
+ const safeName = escapePowerShellSingleQuoted(windowsTaskName());
180
+ invokePowerShell(`Start-ScheduledTask -TaskName '${safeName}'`, 'Starting the Windows scheduled task');
181
+ return { ok: true };
182
+ }
183
+ export const windowsTask = {
184
+ name: 'windows-task',
185
+ status,
186
+ install,
187
+ uninstall,
188
+ restart,
189
+ stop: stopTask,
190
+ logsHint: () => detachedLogsHint('win32'),
191
+ };
package/dist/lifecycle.js CHANGED
@@ -49,7 +49,7 @@ function restartAll(config = loadConfig()) {
49
49
  }
50
50
  return { ...restartServices(), managed: false };
51
51
  }
52
- /** Release Windows service binaries while npm replaces their package files. */
52
+ /** Stop Windows keep-alive processes while npm replaces their package files. */
53
53
  async function whileServicesStopped(config, task, { platform = process.platform, isRunning = () => Boolean(managerInUse(config)?.active || pidAlive(readRuntime().hostPid)), stop = stopAll, start = startAll, } = {}) {
54
54
  if (platform !== 'win32' || !isRunning())
55
55
  return task();
@@ -91,5 +91,5 @@ export function Keepalive({ ctx }) {
91
91
  : t('common.notInstalled')] })] }) }), 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: () => {
92
92
  setSelected(entry.id);
93
93
  activate(entry.id);
94
- }, 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-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' })] }));
94
+ }, 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' })] }));
95
95
  }
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "herdr.remote.web"
2
2
  name = "Herdr Remote Web"
3
- version = "0.2.23"
3
+ version = "0.2.25"
4
4
  min_herdr_version = "0.9.1"
5
5
  description = "Browser access to Herdr workspaces via local or self-hosted relay"
6
6
  platforms = ["linux", "macos", "windows"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herdr-remote",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Browser access to Herdr workspaces: plugin, host connector, and configuration TUI",
5
5
  "license": "MIT",
6
6
  "type": "module",