livedesk 0.1.261 → 0.1.262

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/bin/livedesk.js CHANGED
@@ -5,7 +5,7 @@ import net from 'node:net';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { execFile, spawn } from 'node:child_process';
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
9
  import { randomBytes } from 'node:crypto';
10
10
  import os from 'node:os';
11
11
 
@@ -17,7 +17,8 @@ const DEFAULT_REMOTE_HUB_PORT = 5197;
17
17
  const DEFAULT_MANAGER_URL = `http://127.0.0.1:${DEFAULT_MANAGER_HTTP_PORT}`;
18
18
  const PORT_CLEANUP_TIMEOUT_MS = 3000;
19
19
  const PORT_CLEANUP_POLL_MS = 100;
20
- const HUB_STARTUP_RETRY_LIMIT = 1;
20
+ const HUB_STARTUP_RETRY_LIMIT = 1;
21
+ const HUB_UPDATE_POLL_MS = 250;
21
22
  const MANAGER_STATE_DIR = join(os.homedir(), '.livedesk');
22
23
  const MANAGER_STATE_PATH = join(MANAGER_STATE_DIR, 'manager.json');
23
24
 
@@ -75,13 +76,21 @@ Common options:
75
76
  `.trimStart());
76
77
  }
77
78
 
78
- function readVersion() {
79
+ function readVersion() {
79
80
  try {
80
81
  return require('../package.json').version || '0.0.0';
81
82
  } catch {
82
83
  return '0.0.0';
83
84
  }
84
- }
85
+ }
86
+
87
+ function readClientVersion() {
88
+ try {
89
+ return require('../package.json').dependencies?.['@livedesk/client'] || '';
90
+ } catch {
91
+ return '';
92
+ }
93
+ }
85
94
 
86
95
  function parseManagerArgs(args) {
87
96
  const forwarded = [];
@@ -501,7 +510,7 @@ function resolvePackageEntry(packageName, fallback) {
501
510
  }
502
511
  }
503
512
 
504
- async function runManager(args) {
513
+ async function runManager(args) {
505
514
  const options = parseManagerArgs(args);
506
515
  const httpPort = normalizePort(
507
516
  options.port || process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT,
@@ -514,10 +523,13 @@ async function runManager(args) {
514
523
  const openUrl = options.openUrlExplicit
515
524
  ? (options.openUrl || `http://127.0.0.1:${httpPort}`)
516
525
  : `http://127.0.0.1:${httpPort}`;
517
- const pairToken = process.env.REMOTE_HUB_PAIR_TOKEN
518
- || process.env.LIVEDESK_CLIENT_PAIR_TOKEN
519
- || process.env.MINDEXEC_REMOTE_PAIR_TOKEN
520
- || getStablePairToken();
526
+ const pairToken = process.env.REMOTE_HUB_PAIR_TOKEN
527
+ || process.env.LIVEDESK_CLIENT_PAIR_TOKEN
528
+ || process.env.MINDEXEC_REMOTE_PAIR_TOKEN
529
+ || getStablePairToken();
530
+ mkdirSync(MANAGER_STATE_DIR, { recursive: true });
531
+ const updateRequestPath = join(MANAGER_STATE_DIR, `hub-update-${httpPort}-${remotePort}.json`);
532
+ rmSync(updateRequestPath, { force: true });
521
533
 
522
534
  if (options.cleanPortsOnStart) {
523
535
  await stopProcessesOnPorts([httpPort, remotePort]);
@@ -531,21 +543,61 @@ async function runManager(args) {
531
543
  const env = {
532
544
  ...process.env,
533
545
  LIVEDESK_HUB_HTTP_HOST: options.host || process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1',
534
- LIVEDESK_HUB_HTTP_PORT: String(httpPort),
535
- LIVEDESK_MANAGER_VERSION: readVersion(),
546
+ LIVEDESK_HUB_HTTP_PORT: String(httpPort),
547
+ LIVEDESK_MANAGER_VERSION: readVersion(),
548
+ LIVEDESK_CLIENT_PACKAGE_VERSION: readClientVersion(),
549
+ LIVEDESK_HUB_UPDATE_REQUEST_PATH: updateRequestPath,
536
550
  REMOTE_HUB_PORT: String(remotePort),
537
551
  REMOTE_HUB_PAIR_TOKEN: pairToken,
538
552
  LIVEDESK_WEB_DIST: existsSync(resolve(packagedWebDist, 'index.html')) ? packagedWebDist : (process.env.LIVEDESK_WEB_DIST || '')
539
553
  };
540
554
 
541
- let startupRetryCount = 0;
542
- const startHubProcess = () => {
555
+ let startupRetryCount = 0;
556
+ let restartRequested = false;
557
+ let activeChild = null;
558
+ let updatePollId = null;
559
+
560
+ const restartWithLatest = () => {
561
+ const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
562
+ const next = spawn(npxCommand, ['-y', '--prefer-online', 'livedesk@latest', 'hub', ...args], {
563
+ env: process.env,
564
+ stdio: 'inherit',
565
+ detached: true,
566
+ windowsHide: false
567
+ });
568
+ next.once('error', error => {
569
+ console.error(`Failed to relaunch LiveDesk Hub from npm: ${error.message}`);
570
+ });
571
+ next.unref();
572
+ console.log('[LiveDesk Hub] Update approved. The latest Hub launcher is starting.');
573
+ setTimeout(() => process.exit(0), 100);
574
+ };
575
+
576
+ const pollUpdateRequest = () => {
577
+ if (!existsSync(updateRequestPath)) return;
578
+ try {
579
+ const request = JSON.parse(readFileSync(updateRequestPath, 'utf8'));
580
+ rmSync(updateRequestPath, { force: true });
581
+ if (request?.pid && Number(request.pid) !== Number(activeChild?.pid || 0)) {
582
+ restartRequested = true;
583
+ console.log(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'}.`);
584
+ if (activeChild && !activeChild.killed) {
585
+ activeChild.kill('SIGTERM');
586
+ }
587
+ }
588
+ } catch {
589
+ // The Hub writes the request atomically; a partial/stale file is retried.
590
+ }
591
+ };
592
+
593
+ const startHubProcess = () => {
543
594
  const stderrChunks = [];
544
- const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
595
+ const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
545
596
  env,
546
597
  stdio: ['inherit', 'inherit', 'pipe'],
547
598
  windowsHide: false
548
- });
599
+ });
600
+ activeChild = child;
549
601
 
550
602
  child.stderr.on('data', chunk => {
551
603
  const text = chunk.toString();
@@ -560,8 +612,17 @@ async function runManager(args) {
560
612
  console.error(`Failed to start LiveDesk Hub: ${error.message}`);
561
613
  process.exitCode = 1;
562
614
  });
563
- child.once('exit', (code, signal) => {
564
- const startupOutput = stderrChunks.join('');
615
+ child.once('exit', (code, signal) => {
616
+ const startupOutput = stderrChunks.join('');
617
+ if (restartRequested) {
618
+ restartRequested = false;
619
+ if (updatePollId) {
620
+ clearInterval(updatePollId);
621
+ updatePollId = null;
622
+ }
623
+ restartWithLatest();
624
+ return;
625
+ }
565
626
  if (!signal
566
627
  && code !== 0
567
628
  && options.cleanPortsOnStart
@@ -585,10 +646,11 @@ async function runManager(args) {
585
646
  });
586
647
  };
587
648
 
588
- startHubProcess();
649
+ startHubProcess();
650
+ updatePollId = setInterval(pollUpdateRequest, HUB_UPDATE_POLL_MS);
589
651
 
590
652
  if (options.openBrowserOnStart) {
591
- void waitForManager(openUrl).then(ok => {
653
+ void waitForManager(openUrl).then(ok => {
592
654
  if (ok) {
593
655
  openBrowser(openUrl);
594
656
  } else {
@@ -0,0 +1,383 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
4
+ export const LIVE_DESK_UPDATE_TIMEOUT_MS = 120_000;
5
+ export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 5 * 60_000;
6
+
7
+ function cleanVersion(value) {
8
+ return String(value || '').trim().replace(/^v/i, '');
9
+ }
10
+
11
+ export function compareVersions(left, right) {
12
+ const a = cleanVersion(left).split(/[+-]/, 1)[0].split('.').map(part => Number(part) || 0);
13
+ const b = cleanVersion(right).split(/[+-]/, 1)[0].split('.').map(part => Number(part) || 0);
14
+ const length = Math.max(a.length, b.length, 3);
15
+ for (let index = 0; index < length; index += 1) {
16
+ const difference = (a[index] || 0) - (b[index] || 0);
17
+ if (difference !== 0) return difference;
18
+ }
19
+ return 0;
20
+ }
21
+
22
+ export function isVersionAtLeast(candidate, required) {
23
+ return !!cleanVersion(candidate) && !!cleanVersion(required) && compareVersions(candidate, required) >= 0;
24
+ }
25
+
26
+ function encodePowerShell(value) {
27
+ return Buffer.from(String(value || ''), 'utf16le').toString('base64');
28
+ }
29
+
30
+ function quotePowerShell(value) {
31
+ return `'${String(value ?? '').replaceAll("'", "''")}'`;
32
+ }
33
+
34
+ function buildWindowsLegacyUpdateCommand({ manager, pair, name, slot, targetVersion }) {
35
+ const script = [
36
+ '$ErrorActionPreference = "Stop"',
37
+ '$cursor = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f $PID)',
38
+ '$launcherPid = 0',
39
+ 'for ($depth = 0; $depth -lt 12 -and $cursor; $depth++) {',
40
+ ' $parent = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f [int]$cursor.ParentProcessId)',
41
+ ' if (-not $parent) { break }',
42
+ ' $commandLine = [string]$parent.CommandLine',
43
+ ' if ($commandLine -match "(?i)(livedesk-client\\.js|livedesk\\.js)") { $launcherPid = [int]$parent.ProcessId; break }',
44
+ ' $cursor = $parent',
45
+ '}',
46
+ 'if ($launcherPid -le 0) { throw "LiveDesk client launcher process was not found." }',
47
+ `$env:LIVEDESK_CLIENT_MANAGER = ${quotePowerShell(manager)}`,
48
+ `$env:LIVEDESK_CLIENT_PAIR_TOKEN = ${quotePowerShell(pair)}`,
49
+ `$env:LIVEDESK_CLIENT_NAME = ${quotePowerShell(name)}`,
50
+ `$env:LIVEDESK_CLIENT_SLOT = ${quotePowerShell(slot || '')}`,
51
+ `$env:LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${quotePowerShell(targetVersion)}`,
52
+ '$npx = (Get-Command npx.cmd -ErrorAction SilentlyContinue).Source',
53
+ 'if (-not $npx) { $npx = "npx.cmd" }',
54
+ 'Start-Process -FilePath $npx -ArgumentList @("-y", "--prefer-online", "livedesk@latest", "client", "--no-login", "--update-wait-pid", [string]$launcherPid) -WindowStyle Hidden',
55
+ 'Start-Sleep -Milliseconds 500',
56
+ 'Start-Process -FilePath "taskkill.exe" -ArgumentList @("/PID", [string]$launcherPid, "/T", "/F") -WindowStyle Hidden',
57
+ ''
58
+ ].join('\r\n');
59
+ return `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShell(script)}`;
60
+ }
61
+
62
+ function buildUnixLegacyUpdateCommand({ manager, pair, name, slot, targetVersion }) {
63
+ const script = [
64
+ 'const { execFileSync, spawn } = require("node:child_process");',
65
+ 'const fs = require("node:fs");',
66
+ 'const os = require("node:os");',
67
+ 'const path = require("node:path");',
68
+ 'const self = process.pid;',
69
+ 'const parentOf = pid => { try { return Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" }).trim()) || 0; } catch { return 0; } };',
70
+ 'const commandOf = pid => { try { return execFileSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8" }); } catch { return ""; } };',
71
+ 'let cursor = parentOf(self); let launcherPid = 0;',
72
+ 'for (let depth = 0; depth < 12 && cursor; depth++) { const command = commandOf(cursor); if (/livedesk-client\\.js|livedesk\\.js/i.test(command)) { launcherPid = cursor; break; } cursor = parentOf(cursor); }',
73
+ 'if (!launcherPid) throw new Error("LiveDesk client launcher process was not found.");',
74
+ `process.env.LIVEDESK_CLIENT_MANAGER = ${JSON.stringify(String(manager || ''))};`,
75
+ `process.env.LIVEDESK_CLIENT_PAIR_TOKEN = ${JSON.stringify(String(pair || ''))};`,
76
+ `process.env.LIVEDESK_CLIENT_NAME = ${JSON.stringify(String(name || ''))};`,
77
+ `process.env.LIVEDESK_CLIENT_SLOT = ${JSON.stringify(String(slot || ''))};`,
78
+ `process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${JSON.stringify(String(targetVersion || ''))};`,
79
+ 'const updater = spawn("npx", ["-y", "--prefer-online", "livedesk@latest", "client", "--no-login", "--update-wait-pid", String(launcherPid)], { detached: true, stdio: "ignore" });',
80
+ 'updater.unref();',
81
+ 'setTimeout(() => { try { process.kill(launcherPid, "SIGTERM"); } catch {} }, 500);',
82
+ ''
83
+ ].join('\n');
84
+ return `node -e ${JSON.stringify(script)}`;
85
+ }
86
+
87
+ function buildLegacyUpdateCommand(device, credentials, targetVersion) {
88
+ const payload = {
89
+ manager: credentials.manager,
90
+ pair: credentials.pairToken,
91
+ name: device.deviceName || device.hostname || '',
92
+ slot: device.slotNumber || '',
93
+ targetVersion
94
+ };
95
+ return ['win32', 'windows'].includes(String(device.platform || '').toLowerCase())
96
+ ? buildWindowsLegacyUpdateCommand(payload)
97
+ : buildUnixLegacyUpdateCommand(payload);
98
+ }
99
+
100
+ async function fetchLatestPackage(packageName, fetchImpl) {
101
+ const encodedName = packageName.startsWith('@') ? packageName.replace('/', '%2F') : packageName;
102
+ const response = await fetchImpl(`https://registry.npmjs.org/${encodedName}/latest`, {
103
+ headers: { Accept: 'application/json' },
104
+ signal: AbortSignal.timeout(12_000)
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(`npm registry returned HTTP ${response.status} for ${packageName}.`);
108
+ }
109
+ const payload = await response.json();
110
+ const version = cleanVersion(payload?.version);
111
+ if (!version) throw new Error(`npm registry returned no version for ${packageName}.`);
112
+ return { version, package: payload };
113
+ }
114
+
115
+ export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
116
+ if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for LiveDesk update checks.');
117
+ const [manager, client] = await Promise.all([
118
+ fetchLatestPackage('livedesk', fetchImpl),
119
+ fetchLatestPackage('@livedesk/client', fetchImpl)
120
+ ]);
121
+ return {
122
+ latestManagerVersion: manager.version,
123
+ latestClientVersion: client.version,
124
+ managerPackage: manager.package,
125
+ clientPackage: client.package,
126
+ checkedAt: new Date().toISOString()
127
+ };
128
+ }
129
+
130
+ function statusForRun(run) {
131
+ if (!run) return null;
132
+ return {
133
+ operationId: run.operationId,
134
+ state: run.state,
135
+ startedAt: run.startedAt,
136
+ updatedAt: run.updatedAt,
137
+ targetCount: run.targets.length,
138
+ completedCount: run.targets.filter(target => target.state === 'completed').length,
139
+ waitingCount: run.targets.filter(target => target.state === 'waiting').length,
140
+ failedCount: run.targets.filter(target => target.state === 'failed').length,
141
+ error: run.error || '',
142
+ targets: run.targets.map(target => ({ ...target }))
143
+ };
144
+ }
145
+
146
+ function connectedDevices(remoteHub) {
147
+ return remoteHub.listDevices({ includeDataUrl: false })
148
+ .filter(device => device.connected === true && device.synthetic !== true)
149
+ .slice(0, 500);
150
+ }
151
+
152
+ export function createLiveDeskUpdateManager({
153
+ remoteHub,
154
+ currentManagerVersion,
155
+ currentClientVersion,
156
+ restartSupported = false,
157
+ requestHubRestart,
158
+ fetchImpl = globalThis.fetch,
159
+ now = () => Date.now()
160
+ }) {
161
+ let latestRelease = null;
162
+ let checkError = '';
163
+ let checkPromise = null;
164
+ let run = null;
165
+ let checkTimer = null;
166
+ let runTimer = null;
167
+
168
+ const touch = () => {
169
+ if (run) run.updatedAt = new Date(now()).toISOString();
170
+ };
171
+
172
+ const checkLatest = async () => {
173
+ if (checkPromise) return checkPromise;
174
+ checkPromise = fetchLatestLiveDeskRelease(fetchImpl)
175
+ .then(release => {
176
+ latestRelease = release;
177
+ checkError = '';
178
+ return release;
179
+ })
180
+ .catch(error => {
181
+ checkError = error instanceof Error ? error.message : String(error);
182
+ return null;
183
+ })
184
+ .finally(() => {
185
+ checkPromise = null;
186
+ });
187
+ return checkPromise;
188
+ };
189
+
190
+ const getStatus = () => {
191
+ const updateAvailable = !!latestRelease
192
+ && compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
193
+ const activeRun = statusForRun(run);
194
+ return {
195
+ currentVersion: String(currentManagerVersion || ''),
196
+ currentClientVersion: String(currentClientVersion || ''),
197
+ latestVersion: latestRelease?.latestManagerVersion || '',
198
+ latestClientVersion: latestRelease?.latestClientVersion || '',
199
+ updateAvailable,
200
+ checkedAt: latestRelease?.checkedAt || '',
201
+ checkError,
202
+ restartSupported: !!restartSupported,
203
+ canApply: updateAvailable && !!restartSupported,
204
+ ...(activeRun || { state: 'idle', operationId: '', targetCount: 0, completedCount: 0, waitingCount: 0, failedCount: 0, targets: [] })
205
+ };
206
+ };
207
+
208
+ const failRun = (message) => {
209
+ if (!run) return;
210
+ run.state = 'failed';
211
+ run.error = String(message || 'LiveDesk update failed.');
212
+ touch();
213
+ if (runTimer) {
214
+ clearInterval(runTimer);
215
+ runTimer = null;
216
+ }
217
+ };
218
+
219
+ const requestRestart = () => {
220
+ if (!restartSupported) {
221
+ failRun('Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.');
222
+ return false;
223
+ }
224
+ const result = requestHubRestart?.({
225
+ operationId: run?.operationId || '',
226
+ latestVersion: latestRelease?.latestManagerVersion || '',
227
+ latestClientVersion: latestRelease?.latestClientVersion || ''
228
+ });
229
+ if (result?.ok !== true) {
230
+ failRun(result?.error || 'Hub launcher restart request failed.');
231
+ return false;
232
+ }
233
+ if (run) {
234
+ run.state = 'hub-restart-requested';
235
+ touch();
236
+ }
237
+ return true;
238
+ };
239
+
240
+ const verifyTargets = () => {
241
+ if (!run || run.state !== 'waiting-for-clients') return;
242
+ const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
243
+ for (const target of run.targets) {
244
+ if (target.state === 'failed' || target.state === 'completed') continue;
245
+ const device = devices.get(target.deviceId);
246
+ if (device?.connected === true && isVersionAtLeast(device.agentVersion, run.latestClientVersion)) {
247
+ target.state = 'completed';
248
+ target.currentVersion = String(device.agentVersion || '');
249
+ target.completedAt = new Date(now()).toISOString();
250
+ } else {
251
+ target.state = 'waiting';
252
+ }
253
+ }
254
+ touch();
255
+ if (run.targets.every(target => target.state === 'completed')) {
256
+ if (runTimer) {
257
+ clearInterval(runTimer);
258
+ runTimer = null;
259
+ }
260
+ requestRestart();
261
+ return;
262
+ }
263
+ if (now() - Date.parse(run.startedAt) >= LIVE_DESK_UPDATE_TIMEOUT_MS) {
264
+ failRun(`Timed out waiting for ${run.targets.filter(target => target.state !== 'completed').length} client(s) to reconnect with ${run.latestClientVersion}.`);
265
+ }
266
+ };
267
+
268
+ const dispatchTarget = (target, credentials) => {
269
+ const device = target.device;
270
+ const supportsDedicated = device.capabilities?.clientUpdate === true;
271
+ const payload = {
272
+ targetVersion: run.latestClientVersion,
273
+ managerVersion: run.latestManagerVersion,
274
+ operationId: run.operationId
275
+ };
276
+ const result = supportsDedicated
277
+ ? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
278
+ : remoteHub.requestAgentTask(device.deviceId, {
279
+ operation: 'command.run',
280
+ title: 'Update LiveDesk client',
281
+ instruction: 'Update and restart this LiveDesk client.',
282
+ toolArguments: {
283
+ command: buildLegacyUpdateCommand(device, credentials, run.latestClientVersion),
284
+ timeoutMs: 30_000
285
+ },
286
+ permissionMode: 'full-access',
287
+ approvalLevel: 'once'
288
+ });
289
+ if (result?.ok !== true) {
290
+ target.state = 'failed';
291
+ target.error = String(result?.error || 'client-update-dispatch-failed');
292
+ return false;
293
+ }
294
+ target.state = 'waiting';
295
+ target.method = supportsDedicated ? 'dedicated' : 'legacy-command-run';
296
+ target.commandId = String(result.commandId || result.taskId || '');
297
+ return true;
298
+ };
299
+
300
+ const startUpdate = async () => {
301
+ if (run && ['dispatching', 'waiting-for-clients', 'hub-restart-requested'].includes(run.state)) {
302
+ return { ok: true, ...getStatus() };
303
+ }
304
+ const release = latestRelease || await checkLatest();
305
+ if (!release) return { ok: false, error: checkError || 'LiveDesk update check failed.', ...getStatus() };
306
+ if (compareVersions(release.latestManagerVersion, currentManagerVersion) <= 0) {
307
+ return { ok: false, error: 'LiveDesk is already up to date.', ...getStatus() };
308
+ }
309
+ if (!restartSupported) {
310
+ return { ok: false, error: 'Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.', ...getStatus() };
311
+ }
312
+ const targets = connectedDevices(remoteHub);
313
+ const credentials = remoteHub.getStatus({ includeSecrets: true });
314
+ run = {
315
+ operationId: randomUUID(),
316
+ state: 'dispatching',
317
+ startedAt: new Date(now()).toISOString(),
318
+ updatedAt: new Date(now()).toISOString(),
319
+ latestManagerVersion: release.latestManagerVersion,
320
+ latestClientVersion: release.latestClientVersion,
321
+ error: '',
322
+ targets: targets.map(device => ({
323
+ deviceId: String(device.deviceId || ''),
324
+ deviceName: String(device.deviceName || device.hostname || device.deviceId || ''),
325
+ oldVersion: String(device.agentVersion || ''),
326
+ currentVersion: '',
327
+ state: 'queued',
328
+ method: '',
329
+ commandId: '',
330
+ error: '',
331
+ device
332
+ }))
333
+ };
334
+ for (const target of run.targets) dispatchTarget(target, credentials);
335
+ run.targets = run.targets.map(({ device, ...target }) => target);
336
+ touch();
337
+ if (run.targets.some(target => target.state === 'failed')) {
338
+ failRun('One or more clients could not be scheduled for update.');
339
+ return { ok: false, error: run.error, ...getStatus() };
340
+ }
341
+ if (run.targets.length === 0) {
342
+ requestRestart();
343
+ return { ok: true, ...getStatus() };
344
+ }
345
+ run.state = 'waiting-for-clients';
346
+ runTimer = setInterval(verifyTargets, 1000);
347
+ runTimer.unref?.();
348
+ verifyTargets();
349
+ return { ok: true, ...getStatus() };
350
+ };
351
+
352
+ const handleRemoteEvent = (type, event) => {
353
+ if (!run || run.state !== 'waiting-for-clients' || type !== 'RemoteCommandResult') return;
354
+ const deviceId = String(event?.device?.deviceId || '');
355
+ const commandId = String(event?.commandId || '');
356
+ const target = run.targets.find(item => item.deviceId === deviceId && item.commandId === commandId);
357
+ if (!target || target.method !== 'dedicated') return;
358
+ const result = event?.result;
359
+ if (event?.error || result?.ok === false || result?.status === 'failed') {
360
+ target.state = 'failed';
361
+ target.error = String(event.error || result?.error || 'client-update-command-failed');
362
+ touch();
363
+ failRun('A client rejected the update request. Hub was kept running.');
364
+ }
365
+ };
366
+
367
+ checkTimer = setInterval(() => { void checkLatest(); }, LIVE_DESK_UPDATE_CHECK_INTERVAL_MS);
368
+ checkTimer.unref?.();
369
+ void checkLatest();
370
+
371
+ return {
372
+ checkLatest,
373
+ getStatus,
374
+ startUpdate,
375
+ handleRemoteEvent,
376
+ close() {
377
+ if (checkTimer) clearInterval(checkTimer);
378
+ if (runTimer) clearInterval(runTimer);
379
+ checkTimer = null;
380
+ runTimer = null;
381
+ }
382
+ };
383
+ }
@@ -4490,15 +4490,17 @@ export function createRemoteHub(options = {}) {
4490
4490
  function sendCommand(deviceId, command) {
4491
4491
  const device = devices.get(String(deviceId || ''));
4492
4492
  const commandName = safeString(command?.command || 'ping', 80);
4493
- const requiredPermission = commandName === 'input.control'
4494
- ? 'allowControl'
4495
- : commandName.startsWith('file.transfer')
4496
- ? 'allowFileTransfer'
4497
- : commandName === 'audio.start'
4498
- ? 'allowRemoteAudio'
4499
- : commandName.startsWith('agent.')
4500
- ? 'allowAgent'
4501
- : '';
4493
+ const requiredPermission = commandName === 'input.control'
4494
+ ? 'allowControl'
4495
+ : commandName.startsWith('file.transfer')
4496
+ ? 'allowFileTransfer'
4497
+ : commandName === 'audio.start'
4498
+ ? 'allowRemoteAudio'
4499
+ : commandName === 'livedesk.client-update'
4500
+ ? 'allowAgent'
4501
+ : commandName.startsWith('agent.')
4502
+ ? 'allowAgent'
4503
+ : '';
4502
4504
  const denied = policyError(device, requiredPermission);
4503
4505
  if (denied) return { ok: false, error: denied };
4504
4506
  if (device?.synthetic === true && device.connected) {