@livedesk/client 0.1.169 → 0.1.170

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,118 @@ 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
+ let previous = null;
54
+ try {
55
+ previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
56
+ } catch {
57
+ // The first update stage creates the file.
58
+ }
59
+ const replacesTerminalOperation = previous?.operationId
60
+ && previous.operationId !== operationId
61
+ && stage === 'scheduled'
62
+ && ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
63
+ if (previous?.operationId && previous.operationId !== operationId && !replacesTerminalOperation) return;
64
+ const targetProductVersion = String(
65
+ details.targetProductVersion
66
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
67
+ || (!replacesTerminalOperation ? previous?.targetProductVersion : '')
68
+ || ''
69
+ ).trim();
70
+ const targetAgentVersion = String(
71
+ details.targetAgentVersion
72
+ || details.targetVersion
73
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
74
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
75
+ || (!replacesTerminalOperation ? previous?.targetAgentVersion : '')
76
+ || (!replacesTerminalOperation ? previous?.targetVersion : '')
77
+ || ''
78
+ ).trim();
79
+ const state = {
80
+ ...(!replacesTerminalOperation && previous && typeof previous === 'object' ? previous : {}),
81
+ operationId,
82
+ stage,
83
+ targetVersion: targetAgentVersion,
84
+ targetProductVersion,
85
+ targetAgentVersion,
86
+ productVersion: PRODUCT_VERSION,
87
+ agentVersion: AGENT_VERSION,
88
+ agentPid: process.pid,
89
+ launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
90
+ attemptId: String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
91
+ updatedAt: new Date().toISOString(),
92
+ ...details
93
+ };
94
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
95
+ const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
96
+ try {
97
+ await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
98
+ await fs.rename(temporaryPath, statePath);
99
+ } finally {
100
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
101
+ }
102
+ }
103
+
104
+ async function waitForClientUpdateShutdownSignal(operationId, timeoutMs = 150_000) {
105
+ const statePath = getClientUpdateStatePath();
106
+ const deadline = Date.now() + timeoutMs;
107
+ while (Date.now() < deadline) {
108
+ try {
109
+ const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
110
+ if (state?.operationId !== operationId) {
111
+ await wait(200);
112
+ continue;
113
+ }
114
+ if (state.stage === 'waiting-for-shutdown') return true;
115
+ if (state.stage === 'failed') {
116
+ console.error(`LiveDesk client update preparation failed: ${state.error || 'unknown error'}`);
117
+ return false;
118
+ }
119
+ } catch {
120
+ // The detached supervisor may still be writing its first stage.
121
+ }
122
+ await wait(200);
123
+ }
124
+ await writeClientUpdateState('failed', {
125
+ operationId,
126
+ error: 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.'
127
+ }).catch(() => undefined);
128
+ console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
129
+ return false;
130
+ }
131
+
132
+ function markClientUpdateConnected() {
133
+ const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
134
+ if (!operationId) return;
135
+ void writeClientUpdateState('connected', {
136
+ operationId,
137
+ attemptId: String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
138
+ connectedAt: new Date().toISOString(),
139
+ restartVerified: true,
140
+ error: ''
141
+ }).catch(error => {
142
+ console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
143
+ });
144
+ }
33
145
 
34
146
  function printHelp() {
35
147
  console.log(`
@@ -1318,76 +1430,165 @@ function remotePolicyAllows(options, command) {
1318
1430
  return denied ? { ok: false, error: `${denied}-blocked-by-settings` } : { ok: true };
1319
1431
  }
1320
1432
 
1321
- function buildClientUpdateBootstrapScript() {
1322
- return [
1323
- 'const { spawn } = require("node:child_process");',
1433
+ function buildClientUpdateBootstrapScript() {
1434
+ return [
1435
+ 'const { spawn, spawnSync } = require("node:child_process");',
1324
1436
  'const fs = require("node:fs");',
1325
1437
  'const path = require("node:path");',
1438
+ 'const crypto = require("node:crypto");',
1326
1439
  'const waitPid = Number(process.env.LIVEDESK_UPDATE_WAIT_PID || 0);',
1327
1440
  '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);',
1441
+ 'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
1442
+ 'const statePath = String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json"));',
1443
+ 'const logPath = statePath.replace(/\\.json$/i, "") + ".log";',
1444
+ 'const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");',
1445
+ 'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
1446
+ 'const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);',
1447
+ 'const targetAgentVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || targetProductVersion);',
1448
+ 'const currentProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION || process.env.LIVEDESK_NPM_LAUNCHER_VERSION);',
1449
+ 'const currentAgentVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION || process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || currentProductVersion);',
1450
+ 'for (const [label, version] of [["target product", targetProductVersion], ["target Agent", targetAgentVersion], ["current product", currentProductVersion], ["current Agent", currentAgentVersion]]) { if (!versionPattern.test(version)) throw new Error("Invalid LiveDesk " + label + " update version: " + version); }',
1451
+ 'const packageSpec = "livedesk@" + targetProductVersion;',
1452
+ 'const localLauncher = String(process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER || process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || "").trim();',
1453
+ 'const preservedArgs = (() => { try { const decoded = JSON.parse(Buffer.from(String(process.env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 || ""), "base64").toString("utf8")); return Array.isArray(decoded) ? decoded.map(value => String(value)) : []; } catch { return []; } })();',
1454
+ 'const proofTimeoutMs = Math.max(500, Number(process.env.LIVEDESK_CLIENT_UPDATE_PROOF_TIMEOUT_MS || 30000));',
1455
+ 'const proofPollMs = Math.max(25, Number(process.env.LIVEDESK_CLIENT_UPDATE_PROOF_POLL_MS || 200));',
1456
+ 'const killGraceMs = Math.max(50, Number(process.env.LIVEDESK_CLIENT_UPDATE_KILL_GRACE_MS || 1500));',
1457
+ 'const targetLaunchLimit = 3;',
1458
+ 'const restoreLaunchLimit = 2;',
1459
+ 'const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };',
1460
+ 'const writeState = (stage, details = {}) => { fs.mkdirSync(path.dirname(statePath), { recursive: true }); const previous = readState(); const replacesTerminalOperation = previous.operationId && previous.operationId !== operationId && stage === "preparing-package" && ["connected", "failed", "restored"].includes(String(previous.stage || "")); if (previous.operationId && previous.operationId !== operationId && !replacesTerminalOperation) return false; const base = replacesTerminalOperation ? {} : previous; const next = { ...base, operationId, stage, targetVersion: targetAgentVersion, targetProductVersion, targetAgentVersion, currentProductVersion, currentAgentVersion, supervisorPid: process.pid, updatedAt: new Date().toISOString(), ...details }; const temporary = statePath + "." + process.pid + "." + Math.random().toString(16).slice(2) + ".tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return true; };',
1461
+ 'const log = message => { try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); fs.appendFileSync(logPath, new Date().toISOString() + " " + message + "\\n", "utf8"); } catch {} };',
1462
+ 'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } };',
1463
+ 'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
1464
+ 'const waitForExit = async (pid, timeoutMs) => { const deadline = Date.now() + timeoutMs; while (pid > 1 && Date.now() < deadline) { if (!isAlive(pid)) return true; await sleep(250); } return pid <= 1 || !isAlive(pid); };',
1465
+ 'const killProcessTree = async pid => { if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return; if (process.platform === "win32") { spawnSync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); } else { try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch {} } await sleep(killGraceMs); try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} } } await waitForExit(pid, killGraceMs); };',
1466
+ 'const ensureExit = async (pid, label) => { if (pid <= 1 || await waitForExit(pid, 20000)) return; log(label + " pid=" + pid + " did not exit gracefully; terminating its process tree."); await killProcessTree(pid); if (isAlive(pid)) throw new Error(label + " process " + pid + " did not exit before restart."); };',
1467
+ 'const nodeDir = path.dirname(process.execPath);',
1332
1468
  '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
1469
  'const npx = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || fs.existsSync(candidate)));',
1334
- 'if (!npx) throw new Error("LiveDesk npx executable was not found.");',
1470
+ 'if (!npx) { writeState("failed", { error: "LiveDesk npx executable was not found." }); throw new Error("LiveDesk npx executable was not found."); }',
1335
1471
  'const npmExecPath = String(process.env.npm_execpath || process.env.NPM_EXECPATH || "").trim();',
1336
1472
  '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
1473
  '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);',
1474
+ 'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
1475
+ 'const invocation = npxArgs => npxCli ? { command: process.execPath, args: [npxCli, ...npxArgs] } : process.platform === "win32" ? { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + npxArgs.map(quoteCmd).join(" ")] } : { command: npx, args: npxArgs };',
1476
+ 'const runToExit = (npxArgs, timeoutMs) => new Promise((resolve, reject) => { const call = invocation(npxArgs); const child = spawn(call.command, call.args, { env: process.env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; let settled = false; child.stdout?.on("data", chunk => { stdout = (stdout + chunk.toString()).slice(-65536); }); child.stderr?.on("data", chunk => { stderr = (stderr + chunk.toString()).slice(-65536); }); const timer = setTimeout(() => { if (settled) return; settled = true; void killProcessTree(child.pid || 0).finally(() => reject(new Error("LiveDesk package preparation timed out."))); }, timeoutMs); child.once("error", error => { if (settled) return; settled = true; clearTimeout(timer); reject(error); }); child.once("exit", (code, signal) => { if (settled) return; settled = true; clearTimeout(timer); resolve({ code: code ?? -1, signal, stdout, stderr }); }); });',
1477
+ 'const attempts = [];',
1478
+ 'const attemptIdFor = (kind, number) => operationId + "-" + kind + "-" + number + "-" + crypto.randomBytes(4).toString("hex");',
1479
+ 'const updateAttempt = (attempt, patch) => { Object.assign(attempt, patch, { updatedAt: new Date().toISOString() }); return attempts.map(item => ({ ...item })); };',
1480
+ 'const killAttempt = async (restartPid, attemptId) => { const state = readState(); const pids = new Set([Number(restartPid || 0)]); if (state.operationId === operationId && state.attemptId === attemptId) { pids.add(Number(state.launcherPid || 0)); pids.add(Number(state.agentPid || 0)); } for (const pid of pids) await killProcessTree(pid); };',
1481
+ 'const launchAndVerify = async ({ kind, number, expectedProductVersion, expectedAgentVersion, call }) => { const attemptId = attemptIdFor(kind, number); const attempt = { attemptId, kind, number, status: "launching", expectedProductVersion, expectedAgentVersion, startedAt: new Date().toISOString() }; attempts.push(attempt); const attemptEnv = { ...process.env, LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID: attemptId, LIVEDESK_CLIENT_UPDATE_ATTEMPT_KIND: kind, LIVEDESK_CLIENT_UPDATE_ATTEMPT_NUMBER: String(number), LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId, LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion, LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion, LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion }; const logFd = fs.openSync(logPath, "a"); let child; try { child = spawn(call.command, call.args, { cwd: process.env.LIVEDESK_CLIENT_UPDATE_CWD || process.cwd(), env: attemptEnv, detached: true, windowsHide: true, stdio: ["ignore", logFd, logFd] }); await new Promise((resolve, reject) => { child.once("error", reject); child.once("spawn", resolve); }); } finally { fs.closeSync(logFd); } const restartPid = Number(child?.pid || 0); updateAttempt(attempt, { status: "waiting-for-connected-proof", restartPid }); writeState(kind + "-launched", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, attempts: attempts.map(item => ({ ...item })), restartVerified: false, error: "" }); let exitResult = null; child.once("exit", (code, signal) => { exitResult = { code, signal }; }); const deadline = Date.now() + proofTimeoutMs; let proof = null; let failure = ""; while (Date.now() < deadline) { const state = readState(); if (state.operationId === operationId && state.attemptId === attemptId && state.stage === "connected") { const observedProduct = cleanVersion(state.productVersion); const observedAgent = cleanVersion(state.agentVersion); if (observedProduct === expectedProductVersion && observedAgent === expectedAgentVersion) { proof = state; break; } failure = "Connected version mismatch expected product " + expectedProductVersion + " / Agent " + expectedAgentVersion + ", observed product " + (observedProduct || "unknown") + " / Agent " + (observedAgent || "unknown") + "."; break; } if (exitResult) { failure = "Launcher exited before exact connected proof (code=" + (exitResult.code ?? "none") + ", signal=" + (exitResult.signal || "none") + ")."; break; } await sleep(proofPollMs); } if (proof) { updateAttempt(attempt, { status: "connected", connectedAt: proof.connectedAt || new Date().toISOString(), productVersion: cleanVersion(proof.productVersion), agentVersion: cleanVersion(proof.agentVersion) }); if (child && !exitResult) child.unref(); writeState(kind === "target" ? "connected" : "restored", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, launcherPid: Number(proof.launcherPid || 0), agentPid: Number(proof.agentPid || 0), productVersion: cleanVersion(proof.productVersion), agentVersion: cleanVersion(proof.agentVersion), attempts: attempts.map(item => ({ ...item })), restartVerified: kind === "target", restored: kind === "restore", connectedAt: proof.connectedAt || new Date().toISOString(), error: kind === "restore" ? "Target update failed; previous local Client was restored." : "" }); return true; } failure = failure || "Timed out waiting for exact connected proof for " + attemptId + "."; await killAttempt(restartPid, attemptId); updateAttempt(attempt, { status: "failed", failedAt: new Date().toISOString(), killed: true, error: failure }); writeState(kind + "-attempt-failed", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, attempts: attempts.map(item => ({ ...item })), restartVerified: false, error: failure }); log(attemptId + " failed: " + failure); return false; };',
1341
1482
  '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
- }
1483
+ '(async () => { writeState("preparing-package", { packageSpec, attempts: [], error: "" }); log("preparing " + packageSpec); const prepared = await runToExit(["-y", "--prefer-online", packageSpec, "--version"], 120000); if (prepared.code !== 0) throw new Error("LiveDesk package preparation failed (exit=" + prepared.code + "): " + (prepared.stderr || prepared.stdout).trim().slice(-2000)); const stdoutVersions = prepared.stdout.split(/\\r?\\n/).map(line => line.trim()).filter(line => versionPattern.test(line)); if (!stdoutVersions.includes(targetProductVersion)) throw new Error("Prepared LiveDesk stdout did not report requested product version " + targetProductVersion + ". stdout=" + prepared.stdout.trim().slice(-1000)); writeState("waiting-for-shutdown", { observedVersion: targetProductVersion, observedProductVersion: targetProductVersion, preparedAt: new Date().toISOString(), attempts: [], error: "" }); log("package ready product=" + targetProductVersion + "; waiting for current runtime shutdown"); await ensureExit(agentPid, "Agent"); await ensureExit(waitPid, "Launcher"); const roleArgs = ["--force-role", "client", "--no-login", "--no-open", ...preservedArgs]; const targetCall = invocation(["-y", "--prefer-online", packageSpec, ...roleArgs]); for (let number = 1; number <= targetLaunchLimit; number += 1) { if (await launchAndVerify({ kind: "target", number, expectedProductVersion: targetProductVersion, expectedAgentVersion: targetAgentVersion, call: targetCall })) { log("target connected with exact product " + targetProductVersion + " / Agent " + targetAgentVersion); return; } } writeState("restoring", { attempts: attempts.map(item => ({ ...item })), error: "All target launch attempts failed; restoring previous local Client." }); const restoreCall = localLauncher && fs.existsSync(localLauncher) ? { command: process.execPath, args: [localLauncher, ...roleArgs] } : invocation(["-y", "--prefer-offline", "livedesk@" + currentProductVersion, ...roleArgs]); for (let number = 1; number <= restoreLaunchLimit; number += 1) { if (await launchAndVerify({ kind: "restore", number, expectedProductVersion: currentProductVersion, expectedAgentVersion: currentAgentVersion, call: restoreCall })) { log("previous local Client restored at product " + currentProductVersion + " / Agent " + currentAgentVersion); process.exitCode = 1; return; } } throw new Error("Target update failed after 3 attempts and previous local Client restoration failed after 2 attempts."); })().catch(error => { const message = error?.stack || error?.message || String(error); log("failed: " + message); try { writeState("failed", { attempts: attempts.map(item => ({ ...item })), error: String(error?.message || error).slice(0, 4000), failedAt: new Date().toISOString(), restartVerified: false }); } catch {} process.exitCode = 1; });',
1484
+ ''
1485
+ ].join('\n');
1486
+ }
1487
+
1488
+ async function scheduleClientUpdate(options, payload = {}) {
1489
+ const operationId = String(payload.operationId || crypto.randomUUID()).trim();
1490
+ const statePath = getClientUpdateStatePath();
1491
+ let targetProductVersion = '';
1492
+ let targetAgentVersion = '';
1493
+ try {
1494
+ targetProductVersion = normalizeClientUpdateTargetVersion(
1495
+ payload.targetProductVersion
1496
+ || payload.productVersion
1497
+ || payload.managerVersion
1498
+ || payload.targetVersion
1499
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
1500
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1501
+ || ''
1502
+ );
1503
+ targetAgentVersion = normalizeClientUpdateTargetVersion(
1504
+ payload.targetAgentVersion
1505
+ || payload.clientVersion
1506
+ || payload.targetVersion
1507
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
1508
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1509
+ || targetProductVersion
1510
+ || ''
1511
+ );
1512
+ await writeClientUpdateState('scheduled', {
1513
+ operationId,
1514
+ targetVersion: targetAgentVersion,
1515
+ targetProductVersion,
1516
+ targetAgentVersion,
1517
+ requestedAt: new Date().toISOString(),
1518
+ restartVerified: false,
1519
+ error: ''
1520
+ });
1521
+ const configuredParentPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || process.ppid);
1522
+ const parentPid = Number.isInteger(configuredParentPid) && configuredParentPid > 1
1523
+ ? configuredParentPid
1524
+ : process.pid;
1525
+ if (!Number.isInteger(parentPid) || parentPid <= 1) {
1526
+ throw new Error('LiveDesk client launcher process id is unavailable.');
1527
+ }
1528
+ const nodeBinDir = path.dirname(process.execPath);
1529
+ const npxCandidates = process.platform === 'win32'
1530
+ ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx.cmd'), path.join(nodeBinDir, 'npx.exe'), 'npx.cmd']
1531
+ : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
1532
+ const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
1533
+ if (!command) {
1534
+ throw new Error('LiveDesk npx executable was not found.');
1535
+ }
1536
+ return await new Promise((resolve, reject) => {
1537
+ const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
1538
+ env: {
1539
+ ...process.env,
1540
+ LIVEDESK_NODE_EXECUTABLE: process.execPath,
1541
+ LIVEDESK_NPX_EXECUTABLE: command,
1542
+ LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1543
+ LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1544
+ LIVEDESK_SKIP_BROWSER_OPEN: '1',
1545
+ LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
1546
+ LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
1547
+ LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
1548
+ LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
1549
+ LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
1550
+ LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: String(
1551
+ process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
1552
+ || process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
1553
+ || ''
1554
+ ),
1555
+ LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
1556
+ LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
1557
+ LIVEDESK_CLIENT_UPDATE_CWD: process.cwd()
1558
+ },
1559
+ detached: true,
1560
+ stdio: 'ignore',
1561
+ windowsHide: true
1562
+ });
1563
+ child.once('error', reject);
1564
+ child.once('spawn', () => {
1565
+ child.unref();
1566
+ resolve({
1567
+ ok: true,
1568
+ status: 'update-scheduled',
1569
+ targetVersion: targetAgentVersion,
1570
+ targetProductVersion,
1571
+ targetAgentVersion,
1572
+ operationId,
1573
+ statePath,
1574
+ parentPid,
1575
+ restartAfterMs: 0
1576
+ });
1577
+ });
1578
+ });
1579
+ } catch (error) {
1580
+ await writeClientUpdateState('failed', {
1581
+ operationId,
1582
+ targetVersion: targetAgentVersion,
1583
+ targetProductVersion,
1584
+ targetAgentVersion,
1585
+ error: error?.message || String(error),
1586
+ failedAt: new Date().toISOString(),
1587
+ restartVerified: false
1588
+ }).catch(() => undefined);
1589
+ throw error;
1590
+ }
1591
+ }
1391
1592
 
1392
1593
  async function handleRemoteCommand(socket, options, message, nextFrameSeq, activeStreams) {
1393
1594
  const command = String(message.command || '');
@@ -1414,16 +1615,18 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1414
1615
  return;
1415
1616
  }
1416
1617
 
1417
- if (command === 'livedesk.client-update') {
1418
- try {
1419
- const result = await scheduleClientUpdate(options, message.payload || {});
1618
+ if (command === 'livedesk.client-update') {
1619
+ try {
1620
+ const result = await scheduleClientUpdate(options, message.payload || {});
1420
1621
  writeJsonLine(socket, {
1421
1622
  type: 'command.result',
1422
- commandId: message.commandId,
1423
- result
1424
- });
1425
- setTimeout(() => process.exit(EXIT_CLIENT_UPDATE), 500);
1426
- } catch (error) {
1623
+ commandId: message.commandId,
1624
+ result
1625
+ });
1626
+ if (await waitForClientUpdateShutdownSignal(result.operationId)) {
1627
+ process.exit(EXIT_CLIENT_UPDATE);
1628
+ }
1629
+ } catch (error) {
1427
1630
  writeJsonLine(socket, {
1428
1631
  type: 'command.result',
1429
1632
  commandId: message.commandId,
@@ -1751,9 +1954,10 @@ function connectOnce(options, deviceId) {
1751
1954
  hostname: os.hostname(),
1752
1955
  platform: os.platform(),
1753
1956
  arch: os.arch(),
1754
- pid: process.pid,
1755
- agentVersion: AGENT_VERSION,
1756
- capabilities: {
1957
+ pid: process.pid,
1958
+ agentVersion: AGENT_VERSION,
1959
+ productVersion: PRODUCT_VERSION,
1960
+ capabilities: {
1757
1961
  status: true,
1758
1962
  thumbnail: options.thumbnailEnabled,
1759
1963
  liveStream: options.liveEnabled,
@@ -1768,7 +1972,8 @@ function connectOnce(options, deviceId) {
1768
1972
  fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
1769
1973
  computerAgent: options.taskEnabled,
1770
1974
  taskDispatch: options.taskEnabled,
1771
- clientUpdate: true,
1975
+ clientUpdate: true,
1976
+ productVersion: PRODUCT_VERSION,
1772
1977
  agentApproval: options.taskEnabled,
1773
1978
  agentAudit: options.taskEnabled,
1774
1979
  agentTools: [...NODE_AGENT_OPERATIONS],
@@ -1793,11 +1998,12 @@ function connectOnce(options, deviceId) {
1793
1998
  }
1794
1999
 
1795
2000
  const message = JSON.parse(line);
1796
- if (message.type === 'welcome') {
2001
+ if (message.type === 'welcome') {
1797
2002
  options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
1798
2003
  ? message.effectivePolicy
1799
2004
  : null;
1800
- console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
2005
+ console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
2006
+ markClientUpdateConnected();
1801
2007
  writeJsonLine(socket, {
1802
2008
  type: 'status',
1803
2009
  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,83 @@ 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 previous = readClientUpdateState();
607
+ if (previous?.operationId && previous.operationId !== operationId) return;
608
+ const targetVersion = String(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || '').trim().replace(/^v/i, '');
609
+ const productVersion = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion()).trim();
610
+ const state = {
611
+ ...(previous && typeof previous === 'object' ? previous : {}),
612
+ operationId,
613
+ stage,
614
+ targetVersion,
615
+ productVersion,
616
+ agentVersion: readPackageVersion(),
617
+ launcherPid: process.pid,
618
+ updatedAt: new Date().toISOString(),
619
+ ...details
620
+ };
621
+ mkdirSync(dirname(CLIENT_UPDATE_STATE_PATH), { recursive: true });
622
+ const temporaryPath = `${CLIENT_UPDATE_STATE_PATH}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
623
+ try {
624
+ writeFileSync(temporaryPath, JSON.stringify(state, null, 2), 'utf8');
625
+ try { chmodSync(temporaryPath, 0o600); } catch { /* Windows profile ACLs remain authoritative. */ }
626
+ renameSync(temporaryPath, CLIENT_UPDATE_STATE_PATH);
627
+ } catch {
628
+ rmSync(temporaryPath, { force: true });
629
+ }
630
+ }
631
+
553
632
  function hubCacheOwnerForSession(session) {
554
633
  const userId = String(session?.user?.id || '').trim();
555
634
  return userId ? `user:${userId}` : '';
@@ -3487,7 +3566,7 @@ function resolveBundledFfmpegPaths() {
3487
3566
  return paths;
3488
3567
  }
3489
3568
 
3490
- function buildClientAgentEnvironment(prepared) {
3569
+ function buildClientAgentEnvironment(prepared) {
3491
3570
  const env = { ...process.env };
3492
3571
  env.LIVEDESK_CLIENT_PARENT_PID = String(process.pid);
3493
3572
  // The client can be started by Explorer, a startup shortcut, or a GUI shell
@@ -3515,10 +3594,14 @@ function buildClientAgentEnvironment(prepared) {
3515
3594
  env.LIVEDESK_CLIENT_MANAGER = String(prepared?.manager || env.LIVEDESK_CLIENT_MANAGER || '');
3516
3595
  env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
3517
3596
  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
- }
3597
+ env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
3598
+ env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
3599
+ env.LIVEDESK_CLIENT_UPDATE_STATE_PATH = CLIENT_UPDATE_STATE_PATH;
3600
+ env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 = Buffer
3601
+ .from(JSON.stringify(buildClientUpdateRestartArgs(prepared)), 'utf8')
3602
+ .toString('base64');
3603
+ return env;
3604
+ }
3522
3605
 
3523
3606
  function buildFastEnvironment(prepared) {
3524
3607
  const env = buildClientAgentEnvironment(prepared);
@@ -3721,13 +3804,17 @@ function resolveFastLaunch(runtime) {
3721
3804
  };
3722
3805
  }
3723
3806
 
3724
- function spawnAgent(command, args, env = process.env, onStart = null) {
3807
+ function spawnAgent(command, args, env = process.env, onStart = null) {
3725
3808
  const child = spawn(command, args, {
3726
3809
  env,
3727
3810
  stdio: 'inherit',
3728
3811
  windowsHide: true
3729
3812
  });
3730
- activeAgentProcess = child;
3813
+ activeAgentProcess = child;
3814
+ writeClientUpdateRuntimeStage('agent-started', {
3815
+ agentPid: Number(child.pid || 0),
3816
+ restartVerified: false
3817
+ });
3731
3818
  if (typeof onStart === 'function') {
3732
3819
  onStart(child);
3733
3820
  }
@@ -3771,10 +3858,13 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
3771
3858
  if (!isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)) {
3772
3859
  console.warn('[LiveDesk] @livedesk/client is deprecated. Use the unified livedesk package or LiveDesk Desktop.');
3773
3860
  }
3774
- const parsed = parseLauncherArgs(argv);
3775
- if (parsed.updateWaitPid) {
3776
- await waitForProcessExit(parsed.updateWaitPid);
3777
- }
3861
+ const parsed = parseLauncherArgs(argv);
3862
+ if (parsed.updateWaitPid) {
3863
+ await waitForProcessExit(parsed.updateWaitPid);
3864
+ }
3865
+ writeClientUpdateRuntimeStage('launcher-started', {
3866
+ restartVerified: false
3867
+ });
3778
3868
  if (parsed.help) {
3779
3869
  printHelp();
3780
3870
  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.170",
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.374",
43
+ "@livedesk/fast-osx-arm64": "0.1.376",
44
+ "@livedesk/fast-osx-x64": "0.1.376",
45
+ "@livedesk/fast-win-x64": "0.1.374"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"