@livedesk/client 0.1.169 → 0.1.171

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.
@@ -9,14 +9,15 @@ import { spawn } from 'child_process';
9
9
  import { createRequire } from 'node:module';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
- const AGENT_VERSION = (() => {
12
+ const AGENT_VERSION = (() => {
13
13
  try {
14
14
  return String(require('../package.json').version || '0.0.0');
15
15
  } catch {
16
16
  return '0.0.0';
17
- }
18
- })();
19
- const DEFAULT_MANAGER = '127.0.0.1:5197';
17
+ }
18
+ })();
19
+ const PRODUCT_VERSION = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || AGENT_VERSION).trim() || AGENT_VERSION;
20
+ const DEFAULT_MANAGER = '127.0.0.1:5197';
20
21
  const DEFAULT_HEARTBEAT_MS = 5000;
21
22
  const DEFAULT_RECONNECT_MS = 5000;
22
23
  const EXIT_INVALID_PAIR_TOKEN = 23;
@@ -29,7 +30,133 @@ const MAX_AI_OUTPUT_CHARS = 6000;
29
30
  const MAX_FILE_TRANSFER_FILES = 24;
30
31
  const MAX_FILE_TRANSFER_BYTES = 24 * 1024 * 1024;
31
32
  const MAX_AGENT_OUTPUT_CHARS = 32000;
32
- const launchedProcessRegistry = new Map();
33
+ const launchedProcessRegistry = new Map();
34
+ const CLIENT_UPDATE_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
35
+
36
+ function normalizeClientUpdateTargetVersion(value) {
37
+ const version = String(value || '').trim().replace(/^v/i, '');
38
+ if (!version) return '';
39
+ if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
40
+ throw new Error(`Invalid LiveDesk product update version: ${version}`);
41
+ }
42
+ return version;
43
+ }
44
+
45
+ function getClientUpdateStatePath() {
46
+ return String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'));
47
+ }
48
+
49
+ async function writeClientUpdateState(stage, details = {}) {
50
+ const statePath = getClientUpdateStatePath();
51
+ const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
52
+ if (!operationId) return;
53
+ const attemptId = stage === 'scheduled'
54
+ ? ''
55
+ : String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
56
+ let previous = null;
57
+ try {
58
+ previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
59
+ } catch {
60
+ // The first update stage creates the file.
61
+ }
62
+ const replacesTerminalOperation = previous?.operationId
63
+ && previous.operationId !== operationId
64
+ && stage === 'scheduled'
65
+ && ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
66
+ if (previous?.operationId && previous.operationId !== operationId && !replacesTerminalOperation) return;
67
+ if (!replacesTerminalOperation
68
+ && previous?.attemptId
69
+ && attemptId
70
+ && previous.attemptId !== attemptId) {
71
+ return;
72
+ }
73
+ if (!replacesTerminalOperation
74
+ && previous?.stage === 'connected'
75
+ && previous?.restartVerified === true
76
+ && stage !== 'connected') {
77
+ return;
78
+ }
79
+ const targetProductVersion = String(
80
+ details.targetProductVersion
81
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
82
+ || (!replacesTerminalOperation ? previous?.targetProductVersion : '')
83
+ || ''
84
+ ).trim();
85
+ const targetAgentVersion = String(
86
+ details.targetAgentVersion
87
+ || details.targetVersion
88
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
89
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
90
+ || (!replacesTerminalOperation ? previous?.targetAgentVersion : '')
91
+ || (!replacesTerminalOperation ? previous?.targetVersion : '')
92
+ || ''
93
+ ).trim();
94
+ const state = {
95
+ ...(!replacesTerminalOperation && previous && typeof previous === 'object' ? previous : {}),
96
+ operationId,
97
+ stage,
98
+ targetVersion: targetAgentVersion,
99
+ targetProductVersion,
100
+ targetAgentVersion,
101
+ productVersion: PRODUCT_VERSION,
102
+ agentVersion: AGENT_VERSION,
103
+ agentPid: process.pid,
104
+ launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
105
+ attemptId,
106
+ updatedAt: new Date().toISOString(),
107
+ ...details
108
+ };
109
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
110
+ const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
111
+ try {
112
+ await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
113
+ await fs.rename(temporaryPath, statePath);
114
+ } finally {
115
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
116
+ }
117
+ }
118
+
119
+ async function waitForClientUpdateShutdownSignal(operationId, timeoutMs = 150_000) {
120
+ const statePath = getClientUpdateStatePath();
121
+ const deadline = Date.now() + timeoutMs;
122
+ while (Date.now() < deadline) {
123
+ try {
124
+ const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
125
+ if (state?.operationId !== operationId) {
126
+ await wait(200);
127
+ continue;
128
+ }
129
+ if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') return true;
130
+ if (state.stage === 'failed') {
131
+ console.error(`LiveDesk client update preparation failed: ${state.error || 'unknown error'}`);
132
+ return false;
133
+ }
134
+ } catch {
135
+ // The detached supervisor may still be writing its first stage.
136
+ }
137
+ await wait(200);
138
+ }
139
+ await writeClientUpdateState('failed', {
140
+ operationId,
141
+ error: 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.'
142
+ }).catch(() => undefined);
143
+ console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
144
+ return false;
145
+ }
146
+
147
+ function markClientUpdateConnected() {
148
+ const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
149
+ if (!operationId) return;
150
+ void writeClientUpdateState('connected', {
151
+ operationId,
152
+ attemptId: String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
153
+ connectedAt: new Date().toISOString(),
154
+ restartVerified: true,
155
+ error: ''
156
+ }).catch(error => {
157
+ console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
158
+ });
159
+ }
33
160
 
34
161
  function printHelp() {
35
162
  console.log(`
@@ -1318,76 +1445,137 @@ function remotePolicyAllows(options, command) {
1318
1445
  return denied ? { ok: false, error: `${denied}-blocked-by-settings` } : { ok: true };
1319
1446
  }
1320
1447
 
1321
- function buildClientUpdateBootstrapScript() {
1322
- return [
1323
- 'const { spawn } = require("node:child_process");',
1448
+ function buildClientUpdateBootstrapScript() {
1449
+ return [
1450
+ 'const { spawn } = require("node:child_process");',
1324
1451
  'const fs = require("node:fs");',
1325
1452
  'const path = require("node:path");',
1326
- 'const waitPid = Number(process.env.LIVEDESK_UPDATE_WAIT_PID || 0);',
1327
- 'const agentPid = Number(process.env.LIVEDESK_UPDATE_AGENT_PID || 0);',
1328
- 'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } };',
1329
- 'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
1330
- 'const waitForExit = async pid => { for (let attempt = 0; attempt < 360 && pid > 1; attempt += 1) { if (!isAlive(pid)) return; await sleep(500); } if (pid > 1 && isAlive(pid)) throw new Error("LiveDesk client launcher did not exit before update installation."); };',
1331
- 'const nodeDir = path.dirname(process.execPath);',
1332
- 'const npxCandidates = process.platform === "win32" ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeDir, "npx.cmd"), path.join(nodeDir, "npx.exe"), "npx.cmd"] : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeDir, "npx"), "npx"];',
1333
- 'const npx = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || fs.existsSync(candidate)));',
1334
- 'if (!npx) throw new Error("LiveDesk npx executable was not found.");',
1335
- 'const npmExecPath = String(process.env.npm_execpath || process.env.NPM_EXECPATH || "").trim();',
1336
- 'const npxCliCandidates = [process.env.LIVEDESK_NPX_CLI_PATH, npmExecPath ? path.join(path.dirname(npmExecPath), "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].filter(Boolean);',
1337
- 'const npxCli = npxCliCandidates.find(candidate => fs.existsSync(candidate));',
1338
- 'const npxArgs = ["-y", "--prefer-online", "livedesk@latest", "--force-role", "client", "--no-login", "--no-open"];',
1339
- 'const command = npxCli ? process.execPath : (process.platform === "win32" ? (process.env.ComSpec || "cmd.exe") : npx);',
1340
- 'const commandArgs = npxCli ? [npxCli, ...npxArgs] : (process.platform === "win32" ? ["/d", "/s", "/c", `call "${npx}" ${npxArgs.join(" ")}`] : npxArgs);',
1341
- 'process.env.LIVEDESK_SKIP_BROWSER_OPEN = "1";',
1342
- '(async () => { await waitForExit(waitPid); await waitForExit(agentPid); const updater = spawn(command, commandArgs, { detached: true, stdio: "ignore", windowsHide: true, env: process.env }); updater.once("error", error => { console.error("LiveDesk updater failed to start: " + error.message); process.exitCode = 1; }); updater.once("spawn", () => updater.unref()); })().catch(error => { console.error("LiveDesk updater failed: " + error.message); process.exitCode = 1; });',
1343
- ''
1344
- ].join('\n');
1345
- }
1346
-
1347
- function scheduleClientUpdate(options, payload = {}) {
1348
- const configuredParentPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || process.ppid);
1349
- const parentPid = Number.isInteger(configuredParentPid) && configuredParentPid > 1
1350
- ? configuredParentPid
1351
- : process.pid;
1352
- if (!Number.isInteger(parentPid) || parentPid <= 1) {
1353
- return Promise.reject(new Error('LiveDesk client launcher process id is unavailable.'));
1354
- }
1355
- const nodeBinDir = path.dirname(process.execPath);
1356
- const npxCandidates = process.platform === 'win32'
1357
- ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx.cmd'), path.join(nodeBinDir, 'npx.exe'), 'npx.cmd']
1358
- : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
1359
- const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
1360
- if (!command) {
1361
- return Promise.reject(new Error('LiveDesk npx executable was not found.'));
1362
- }
1363
- return new Promise((resolve, reject) => {
1364
- const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
1365
- env: {
1366
- ...process.env,
1367
- LIVEDESK_NODE_EXECUTABLE: process.execPath,
1368
- LIVEDESK_NPX_EXECUTABLE: command,
1369
- LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1370
- LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1371
- LIVEDESK_SKIP_BROWSER_OPEN: '1',
1372
- LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: String(payload.targetVersion || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || '')
1373
- },
1374
- detached: true,
1375
- stdio: 'ignore',
1376
- windowsHide: true
1377
- });
1378
- child.once('error', reject);
1379
- child.once('spawn', () => {
1380
- child.unref();
1381
- resolve({
1382
- ok: true,
1383
- status: 'update-scheduled',
1384
- targetVersion: String(payload.targetVersion || ''),
1385
- parentPid,
1386
- restartAfterMs: 500
1387
- });
1388
- });
1389
- });
1390
- }
1453
+ 'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
1454
+ 'const statePath = String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json"));',
1455
+ 'const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");',
1456
+ 'const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);',
1457
+ 'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
1458
+ 'const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };',
1459
+ 'const writeFailure = error => { try { fs.mkdirSync(path.dirname(statePath), { recursive: true }); const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); } catch {} process.stderr.write("LiveDesk update handoff failed: " + (error?.message || error) + "\\n"); process.exitCode = 1; };',
1460
+ 'const monitorPreflight = child => { let settled = false; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const inspect = () => { const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") finish(null, true); }; const poll = setInterval(inspect, 100); const timeout = setTimeout(() => { try { child.kill(); } catch {} finish(new Error("Timed out waiting for exact-package update preflight.")); }, Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000))); child.once("exit", (code, signal) => { inspect(); if (!settled) finish(new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ").")); }); inspect(); };',
1461
+ 'if (!operationId || !versionPattern.test(targetProductVersion)) { writeFailure(new Error("Invalid LiveDesk exact-package update handoff.")); } else {',
1462
+ 'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) };',
1463
+ 'const nodeDir = path.dirname(process.execPath);',
1464
+ 'const npxCli = [env.LIVEDESK_NPX_CLI_PATH, env.npm_execpath ? path.join(path.dirname(env.npm_execpath), "npx-cli.js") : "", env.LIVEDESK_NPX_EXECUTABLE ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), "node_modules", "npm", "bin", "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].find(value => value && fs.existsSync(value));',
1465
+ 'const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");',
1466
+ 'const args = ["-y", "--prefer-online", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
1467
+ 'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
1468
+ 'const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };',
1469
+ 'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { env, detached: true, stdio: "ignore", windowsHide: true }); child.once("error", writeFailure); child.once("spawn", () => monitorPreflight(child)); } catch (error) { writeFailure(error); } }',
1470
+ '}',
1471
+ ''
1472
+ ].join('\n');
1473
+ }
1474
+
1475
+ async function scheduleClientUpdate(options, payload = {}) {
1476
+ const operationId = String(payload.operationId || crypto.randomUUID()).trim();
1477
+ const statePath = getClientUpdateStatePath();
1478
+ let targetProductVersion = '';
1479
+ let targetAgentVersion = '';
1480
+ try {
1481
+ targetProductVersion = normalizeClientUpdateTargetVersion(
1482
+ payload.targetProductVersion
1483
+ || payload.productVersion
1484
+ || payload.managerVersion
1485
+ || payload.targetVersion
1486
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
1487
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1488
+ || ''
1489
+ );
1490
+ targetAgentVersion = normalizeClientUpdateTargetVersion(
1491
+ payload.targetAgentVersion
1492
+ || payload.clientVersion
1493
+ || payload.targetVersion
1494
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
1495
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1496
+ || targetProductVersion
1497
+ || ''
1498
+ );
1499
+ await writeClientUpdateState('scheduled', {
1500
+ operationId,
1501
+ targetVersion: targetAgentVersion,
1502
+ targetProductVersion,
1503
+ targetAgentVersion,
1504
+ requestedAt: new Date().toISOString(),
1505
+ restartVerified: false,
1506
+ error: ''
1507
+ });
1508
+ const configuredParentPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || process.ppid);
1509
+ const parentPid = Number.isInteger(configuredParentPid) && configuredParentPid > 1
1510
+ ? configuredParentPid
1511
+ : process.pid;
1512
+ if (!Number.isInteger(parentPid) || parentPid <= 1) {
1513
+ throw new Error('LiveDesk client launcher process id is unavailable.');
1514
+ }
1515
+ const nodeBinDir = path.dirname(process.execPath);
1516
+ const npxCandidates = process.platform === 'win32'
1517
+ ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx.cmd'), path.join(nodeBinDir, 'npx.exe'), 'npx.cmd']
1518
+ : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
1519
+ const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
1520
+ if (!command) {
1521
+ throw new Error('LiveDesk npx executable was not found.');
1522
+ }
1523
+ return await new Promise((resolve, reject) => {
1524
+ const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
1525
+ env: {
1526
+ ...process.env,
1527
+ LIVEDESK_NODE_EXECUTABLE: process.execPath,
1528
+ LIVEDESK_NPX_EXECUTABLE: command,
1529
+ LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1530
+ LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1531
+ LIVEDESK_SKIP_BROWSER_OPEN: '1',
1532
+ LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
1533
+ LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
1534
+ LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
1535
+ LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
1536
+ LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
1537
+ LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: String(
1538
+ process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
1539
+ || process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
1540
+ || ''
1541
+ ),
1542
+ LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
1543
+ LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
1544
+ LIVEDESK_CLIENT_UPDATE_CWD: process.cwd()
1545
+ },
1546
+ detached: true,
1547
+ stdio: 'ignore',
1548
+ windowsHide: true
1549
+ });
1550
+ child.once('error', reject);
1551
+ child.once('spawn', () => {
1552
+ child.unref();
1553
+ resolve({
1554
+ ok: true,
1555
+ status: 'update-scheduled',
1556
+ targetVersion: targetAgentVersion,
1557
+ targetProductVersion,
1558
+ targetAgentVersion,
1559
+ operationId,
1560
+ statePath,
1561
+ parentPid,
1562
+ restartAfterMs: 0
1563
+ });
1564
+ });
1565
+ });
1566
+ } catch (error) {
1567
+ await writeClientUpdateState('failed', {
1568
+ operationId,
1569
+ targetVersion: targetAgentVersion,
1570
+ targetProductVersion,
1571
+ targetAgentVersion,
1572
+ error: error?.message || String(error),
1573
+ failedAt: new Date().toISOString(),
1574
+ restartVerified: false
1575
+ }).catch(() => undefined);
1576
+ throw error;
1577
+ }
1578
+ }
1391
1579
 
1392
1580
  async function handleRemoteCommand(socket, options, message, nextFrameSeq, activeStreams) {
1393
1581
  const command = String(message.command || '');
@@ -1414,16 +1602,18 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1414
1602
  return;
1415
1603
  }
1416
1604
 
1417
- if (command === 'livedesk.client-update') {
1418
- try {
1419
- const result = await scheduleClientUpdate(options, message.payload || {});
1605
+ if (command === 'livedesk.client-update') {
1606
+ try {
1607
+ const result = await scheduleClientUpdate(options, message.payload || {});
1420
1608
  writeJsonLine(socket, {
1421
1609
  type: 'command.result',
1422
- commandId: message.commandId,
1423
- result
1424
- });
1425
- setTimeout(() => process.exit(EXIT_CLIENT_UPDATE), 500);
1426
- } catch (error) {
1610
+ commandId: message.commandId,
1611
+ result
1612
+ });
1613
+ if (await waitForClientUpdateShutdownSignal(result.operationId)) {
1614
+ process.exit(EXIT_CLIENT_UPDATE);
1615
+ }
1616
+ } catch (error) {
1427
1617
  writeJsonLine(socket, {
1428
1618
  type: 'command.result',
1429
1619
  commandId: message.commandId,
@@ -1751,9 +1941,10 @@ function connectOnce(options, deviceId) {
1751
1941
  hostname: os.hostname(),
1752
1942
  platform: os.platform(),
1753
1943
  arch: os.arch(),
1754
- pid: process.pid,
1755
- agentVersion: AGENT_VERSION,
1756
- capabilities: {
1944
+ pid: process.pid,
1945
+ agentVersion: AGENT_VERSION,
1946
+ productVersion: PRODUCT_VERSION,
1947
+ capabilities: {
1757
1948
  status: true,
1758
1949
  thumbnail: options.thumbnailEnabled,
1759
1950
  liveStream: options.liveEnabled,
@@ -1768,7 +1959,8 @@ function connectOnce(options, deviceId) {
1768
1959
  fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
1769
1960
  computerAgent: options.taskEnabled,
1770
1961
  taskDispatch: options.taskEnabled,
1771
- clientUpdate: true,
1962
+ clientUpdate: true,
1963
+ productVersion: PRODUCT_VERSION,
1772
1964
  agentApproval: options.taskEnabled,
1773
1965
  agentAudit: options.taskEnabled,
1774
1966
  agentTools: [...NODE_AGENT_OPERATIONS],
@@ -1793,11 +1985,12 @@ function connectOnce(options, deviceId) {
1793
1985
  }
1794
1986
 
1795
1987
  const message = JSON.parse(line);
1796
- if (message.type === 'welcome') {
1988
+ if (message.type === 'welcome') {
1797
1989
  options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
1798
1990
  ? message.effectivePolicy
1799
1991
  : null;
1800
- console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
1992
+ console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
1993
+ markClientUpdateConnected();
1801
1994
  writeJsonLine(socket, {
1802
1995
  type: 'status',
1803
1996
  status: getStatus(options)
@@ -46,6 +46,8 @@ const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
46
46
  const CLIENT_HUB_TARGET_CACHE_STORAGE_KEY = 'livedesk.client.last-hub-target';
47
47
  const DEVICE_ROLE_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-role.json');
48
48
  const FAST_PREFLIGHT_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'fast-preflight.json');
49
+ const CLIENT_UPDATE_STATE_PATH = process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH
50
+ || join(UNIFIED_CLIENT_STATE_DIR, 'client-update.json');
49
51
  let activeAgentProcess = null;
50
52
  let roleRestartRequest = null;
51
53
  let discoveryWakeController = new AbortController();
@@ -377,7 +379,7 @@ function upsertForwardedOption(args, flag, value) {
377
379
  return next;
378
380
  }
379
381
 
380
- function appendForwardedFlag(args, flag) {
382
+ function appendForwardedFlag(args, flag) {
381
383
  if (args.includes(flag)) {
382
384
  return args;
383
385
  }
@@ -550,6 +552,91 @@ function createFileStorage(filePath) {
550
552
  };
551
553
  }
552
554
 
555
+ const CLIENT_UPDATE_SINGLE_FLAGS = new Set([
556
+ '--help',
557
+ '-h',
558
+ '--version',
559
+ '--login',
560
+ '--logout',
561
+ '--no-login',
562
+ '--startup-run',
563
+ '--no-open',
564
+ '--exit-on-disconnect',
565
+ '--exit-on-invalid-pair',
566
+ '--once'
567
+ ]);
568
+ const CLIENT_UPDATE_VALUE_FLAGS = new Set([
569
+ '--update-wait-pid',
570
+ // Pairing credentials stay in the inherited private environment and must
571
+ // never be copied onto a replacement process command line.
572
+ '--pair'
573
+ ]);
574
+
575
+ export function buildClientUpdateRestartArgs(prepared = {}) {
576
+ const source = Array.isArray(prepared.forwarded) ? prepared.forwarded : [];
577
+ const args = [];
578
+ for (let index = 0; index < source.length; index += 1) {
579
+ const arg = String(source[index] || '');
580
+ if (!arg || arg === 'connect' || CLIENT_UPDATE_SINGLE_FLAGS.has(arg)) {
581
+ continue;
582
+ }
583
+ if (arg.startsWith('--pair=')) {
584
+ continue;
585
+ }
586
+ if (CLIENT_UPDATE_VALUE_FLAGS.has(arg)) {
587
+ index += 1;
588
+ continue;
589
+ }
590
+ args.push(arg);
591
+ }
592
+ return args;
593
+ }
594
+
595
+ function readClientUpdateState() {
596
+ try {
597
+ return JSON.parse(readFileSync(CLIENT_UPDATE_STATE_PATH, 'utf8'));
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
602
+
603
+ function writeClientUpdateRuntimeStage(stage, details = {}) {
604
+ const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
605
+ if (!operationId) return;
606
+ const attemptId = String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
607
+ const previous = readClientUpdateState();
608
+ if (previous?.operationId && previous.operationId !== operationId) return;
609
+ if (previous?.attemptId && attemptId && previous.attemptId !== attemptId) return;
610
+ if (previous?.stage === 'connected'
611
+ && previous?.restartVerified === true
612
+ && stage !== 'connected') {
613
+ return;
614
+ }
615
+ const targetVersion = String(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || '').trim().replace(/^v/i, '');
616
+ const productVersion = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion()).trim();
617
+ const state = {
618
+ ...(previous && typeof previous === 'object' ? previous : {}),
619
+ operationId,
620
+ stage,
621
+ targetVersion,
622
+ productVersion,
623
+ agentVersion: readPackageVersion(),
624
+ launcherPid: process.pid,
625
+ attemptId,
626
+ updatedAt: new Date().toISOString(),
627
+ ...details
628
+ };
629
+ mkdirSync(dirname(CLIENT_UPDATE_STATE_PATH), { recursive: true });
630
+ const temporaryPath = `${CLIENT_UPDATE_STATE_PATH}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
631
+ try {
632
+ writeFileSync(temporaryPath, JSON.stringify(state, null, 2), 'utf8');
633
+ try { chmodSync(temporaryPath, 0o600); } catch { /* Windows profile ACLs remain authoritative. */ }
634
+ renameSync(temporaryPath, CLIENT_UPDATE_STATE_PATH);
635
+ } catch {
636
+ rmSync(temporaryPath, { force: true });
637
+ }
638
+ }
639
+
553
640
  function hubCacheOwnerForSession(session) {
554
641
  const userId = String(session?.user?.id || '').trim();
555
642
  return userId ? `user:${userId}` : '';
@@ -3487,7 +3574,7 @@ function resolveBundledFfmpegPaths() {
3487
3574
  return paths;
3488
3575
  }
3489
3576
 
3490
- function buildClientAgentEnvironment(prepared) {
3577
+ function buildClientAgentEnvironment(prepared) {
3491
3578
  const env = { ...process.env };
3492
3579
  env.LIVEDESK_CLIENT_PARENT_PID = String(process.pid);
3493
3580
  // The client can be started by Explorer, a startup shortcut, or a GUI shell
@@ -3513,12 +3600,17 @@ function buildClientAgentEnvironment(prepared) {
3513
3600
  env.LIVEDESK_NPX_CLI_PATH = npxCliPath;
3514
3601
  }
3515
3602
  env.LIVEDESK_CLIENT_MANAGER = String(prepared?.manager || env.LIVEDESK_CLIENT_MANAGER || '');
3516
- env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
3517
- env.LIVEDESK_CLIENT_NAME = String(prepared?.name || env.LIVEDESK_CLIENT_NAME || '');
3518
- env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
3519
- env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
3520
- return env;
3521
- }
3603
+ env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
3604
+ env.LIVEDESK_CLIENT_NAME = String(prepared?.name || env.LIVEDESK_CLIENT_NAME || '');
3605
+ env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
3606
+ env.LIVEDESK_DEVICE_ID = String(prepared?.deviceId || env.LIVEDESK_DEVICE_ID || '');
3607
+ env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
3608
+ env.LIVEDESK_CLIENT_UPDATE_STATE_PATH = CLIENT_UPDATE_STATE_PATH;
3609
+ env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 = Buffer
3610
+ .from(JSON.stringify(buildClientUpdateRestartArgs(prepared)), 'utf8')
3611
+ .toString('base64');
3612
+ return env;
3613
+ }
3522
3614
 
3523
3615
  function buildFastEnvironment(prepared) {
3524
3616
  const env = buildClientAgentEnvironment(prepared);
@@ -3721,13 +3813,20 @@ function resolveFastLaunch(runtime) {
3721
3813
  };
3722
3814
  }
3723
3815
 
3724
- function spawnAgent(command, args, env = process.env, onStart = null) {
3725
- const child = spawn(command, args, {
3726
- env,
3727
- stdio: 'inherit',
3728
- windowsHide: true
3729
- });
3730
- activeAgentProcess = child;
3816
+ function spawnAgent(command, args, env = process.env, onStart = null) {
3817
+ // Publish only a pre-spawn stage. A very fast child can reach Hub welcome
3818
+ // before spawn() returns; a post-spawn write would then erase its one-shot
3819
+ // connected proof.
3820
+ writeClientUpdateRuntimeStage('agent-launching', {
3821
+ agentPid: 0,
3822
+ restartVerified: false
3823
+ });
3824
+ const child = spawn(command, args, {
3825
+ env,
3826
+ stdio: 'inherit',
3827
+ windowsHide: true
3828
+ });
3829
+ activeAgentProcess = child;
3731
3830
  if (typeof onStart === 'function') {
3732
3831
  onStart(child);
3733
3832
  }
@@ -3771,10 +3870,13 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
3771
3870
  if (!isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)) {
3772
3871
  console.warn('[LiveDesk] @livedesk/client is deprecated. Use the unified livedesk package or LiveDesk Desktop.');
3773
3872
  }
3774
- const parsed = parseLauncherArgs(argv);
3775
- if (parsed.updateWaitPid) {
3776
- await waitForProcessExit(parsed.updateWaitPid);
3777
- }
3873
+ const parsed = parseLauncherArgs(argv);
3874
+ if (parsed.updateWaitPid) {
3875
+ await waitForProcessExit(parsed.updateWaitPid);
3876
+ }
3877
+ writeClientUpdateRuntimeStage('launcher-started', {
3878
+ restartVerified: false
3879
+ });
3778
3880
  if (parsed.help) {
3779
3881
  printHelp();
3780
3882
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.169",
3
+ "version": "0.1.171",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,10 +39,10 @@
39
39
  "ws": "^8.18.3"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@livedesk/fast-linux-x64": "0.1.373",
43
- "@livedesk/fast-osx-arm64": "0.1.375",
44
- "@livedesk/fast-osx-x64": "0.1.375",
45
- "@livedesk/fast-win-x64": "0.1.373"
42
+ "@livedesk/fast-linux-x64": "0.1.377",
43
+ "@livedesk/fast-osx-arm64": "0.1.377",
44
+ "@livedesk/fast-osx-x64": "0.1.377",
45
+ "@livedesk/fast-win-x64": "0.1.377"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"