livedesk 0.1.592 → 0.1.593

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.
Files changed (64) hide show
  1. package/README.md +8 -8
  2. package/bin/livedesk.js +115 -112
  3. package/bootstrap/client-process-identity.js +1 -1
  4. package/bootstrap/device-role.js +1 -1
  5. package/bootstrap/legacy-client-update.js +15 -15
  6. package/bootstrap/role-bootstrap-server.js +2 -2
  7. package/bootstrap/runtime-lock.js +4 -4
  8. package/bootstrap/runtime-shutdown.js +3 -3
  9. package/bootstrap/update-host-client.mjs +9 -9
  10. package/bootstrap/update-host.mjs +6 -6
  11. package/client/README.md +10 -10
  12. package/client/THIRD_PARTY_NOTICES.md +1 -1
  13. package/client/bin/client-version.js +1 -1
  14. package/client/bin/livedesk-client-node.js +102 -102
  15. package/client/bin/livedesk-client-update-bootstrap.cjs +12 -12
  16. package/client/bin/livedesk-client.js +136 -122
  17. package/client/package.json +7 -7
  18. package/client/src/runtime/agent-process-lifecycle.js +10 -10
  19. package/client/src/runtime/agent-shell.js +66 -66
  20. package/client/src/runtime/client-runtime-server.js +12 -11
  21. package/client/src/runtime/linux-video-acceleration.js +1 -1
  22. package/client/src/runtime/windows-owned-process-manifest.js +9 -9
  23. package/client/tests/client-version.test.mjs +2 -2
  24. package/diagnostics/livedesk-mac-physical-gate-core.mjs +5 -5
  25. package/diagnostics/livedesk-mode3-capture-smoke.mjs +7 -7
  26. package/diagnostics/livedesk-transport-physical.mjs +3 -3
  27. package/electron/auth-session-owner.mjs +24 -24
  28. package/electron/desktop-clipboard-owner.mjs +382 -382
  29. package/electron/electron-builder.e2e.yml +1 -1
  30. package/electron/electron-builder.mac-arm64.yml +1 -1
  31. package/electron/electron-builder.mac-x64.yml +1 -1
  32. package/electron/electron-builder.win-dev.yml +1 -1
  33. package/electron/electron-builder.win.yml +2 -2
  34. package/electron/electron-builder.yml +2 -2
  35. package/electron/main.mjs +175 -174
  36. package/electron/preload.cjs +13 -13
  37. package/electron/runtime-role-transition.mjs +17 -17
  38. package/hub/package.json +4 -4
  39. package/hub/src/agents/agent-manager.js +2 -2
  40. package/hub/src/agents/agent-permissions.js +2 -2
  41. package/hub/src/agents/agent-tool-registry.js +36 -36
  42. package/hub/src/agents/codex-agent-runtime.js +16 -16
  43. package/hub/src/agents/codex-mcp-server.js +5 -5
  44. package/hub/src/filesystem/roots.js +20 -20
  45. package/hub/src/filesystem/shared-folders.js +2 -2
  46. package/hub/src/live-desk-update.js +9 -9
  47. package/hub/src/remote-clipboard-contract.mjs +482 -482
  48. package/hub/src/remote-hub.js +1025 -1025
  49. package/hub/src/server.js +146 -146
  50. package/hub/src/settings/effective-device-policy.js +31 -31
  51. package/hub/src/settings/settings-schema.js +2 -2
  52. package/package.json +7 -7
  53. package/runtime-core/package.json +2 -2
  54. package/runtime-core/src/auth-config.js +1 -1
  55. package/scripts/sync-web-dist.js +9 -9
  56. package/web/dist/app.webmanifest +4 -4
  57. package/web/dist/assets/{LiveDeskApp-CTj_tAlH.js → LiveDeskApp-Djx1CsIp.js} +13 -13
  58. package/web/dist/assets/{index-CSz9F86o.js → index-DjIbOQxl.js} +6 -6
  59. package/web/dist/desktop-auth-callback.html +4 -4
  60. package/web/dist/desktop-auth-callback.js +13 -13
  61. package/web/dist/index.html +9 -8
  62. package/web/dist/livedesk-build-evidence.json +21 -21
  63. package/web/dist/offline.html +2 -2
  64. package/web/dist/sw.js +11 -11
@@ -1,5 +1,5 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.win.yml
2
- artifactName: LiveDesk-${version}-${arch}-e2e.${ext}
2
+ artifactName: VuvoDesk-${version}-${arch}-e2e.${ext}
3
3
  directories:
4
4
  output: dist/desktop-e2e
5
5
  win:
@@ -38,7 +38,7 @@ mac:
38
38
  arch:
39
39
  - arm64
40
40
  extendInfo:
41
- NSMicrophoneUsageDescription: LiveDesk uses audio only when remote computer audio is enabled.
41
+ NSMicrophoneUsageDescription: VuvoDesk uses audio only when remote computer audio is enabled.
42
42
  publish:
43
43
  provider: generic
44
44
  url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/macos/arm64
@@ -38,7 +38,7 @@ mac:
38
38
  arch:
39
39
  - x64
40
40
  extendInfo:
41
- NSMicrophoneUsageDescription: LiveDesk uses audio only when remote computer audio is enabled.
41
+ NSMicrophoneUsageDescription: VuvoDesk uses audio only when remote computer audio is enabled.
42
42
  publish:
43
43
  provider: generic
44
44
  url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/macos/x64
@@ -1,5 +1,5 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.win.yml
2
- artifactName: LiveDesk-${version}-dev-${arch}.${ext}
2
+ artifactName: VuvoDesk-${version}-dev-${arch}.${ext}
3
3
  directories:
4
4
  output: dist/desktop-dev
5
5
  extraMetadata:
@@ -29,14 +29,14 @@ win:
29
29
  - target: nsis
30
30
  arch:
31
31
  - x64
32
- executableName: LiveDesk
32
+ executableName: VuvoDesk
33
33
  nsis:
34
34
  oneClick: false
35
35
  perMachine: false
36
36
  allowToChangeInstallationDirectory: true
37
37
  createDesktopShortcut: true
38
38
  createStartMenuShortcut: true
39
- shortcutName: LiveDesk
39
+ shortcutName: VuvoDesk
40
40
  deleteAppDataOnUninstall: false
41
41
  publish:
42
42
  provider: generic
@@ -1,6 +1,6 @@
1
1
  appId: com.livedesk.desktop
2
- productName: LiveDesk
3
- artifactName: LiveDesk-${version}-${arch}.${ext}
2
+ productName: VuvoDesk
3
+ artifactName: VuvoDesk-${version}-${arch}.${ext}
4
4
  asar: true
5
5
  directories:
6
6
  app: packages/livedesk
package/electron/main.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, powerMonitor, safeStorage, shell, Tray } from 'electron';
2
2
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { dirname, join, resolve } from 'node:path';
5
- import { spawn } from 'node:child_process';
6
- import { createRequire } from 'node:module';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { createRequire } from 'node:module';
7
7
  import { createProductUpdateManager } from './update-manager.mjs';
8
8
  import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
9
9
  import { createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
@@ -27,8 +27,8 @@ import {
27
27
  registerOwnedWindowsChild,
28
28
  terminateExactRegisteredWindowsTreeUntilStopped
29
29
  } from '../runtime-core/src/windows-owned-process-tree.js';
30
- import {
31
- DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS,
30
+ import {
31
+ DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS,
32
32
  DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS,
33
33
  DEFAULT_RUNTIME_STOP_TIMEOUT_MS,
34
34
  claimRuntimeIntentionalStop,
@@ -37,14 +37,14 @@ import {
37
37
  remainingRuntimeDeadlineMs,
38
38
  shouldRestartOwnedRuntimeAfterExit,
39
39
  terminateOwnedUnixRuntimeTree,
40
- waitForOwnedRuntimeTreeExit,
41
- waitForPromiseBeforeDeadline
42
- } from './runtime-process-owner.mjs';
43
- import {
44
- desktopHubTakeoverPublicationConfirmed,
45
- resolveDesktopHubTakeoverPending
46
- } from './runtime-role-transition.mjs';
47
- import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
40
+ waitForOwnedRuntimeTreeExit,
41
+ waitForPromiseBeforeDeadline
42
+ } from './runtime-process-owner.mjs';
43
+ import {
44
+ desktopHubTakeoverPublicationConfirmed,
45
+ resolveDesktopHubTakeoverPending
46
+ } from './runtime-role-transition.mjs';
47
+ import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
48
48
 
49
49
  // Chromium otherwise leaves an out-of-process Audio Service (~80-90 MiB on
50
50
  // Windows) alive after the exact Remote Audio owner, AudioContext, worklet,
@@ -53,7 +53,8 @@ import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
53
53
  // warm-idle process baseline instead of retaining a hidden utility process.
54
54
  app.commandLine.appendSwitch('disable-features', 'AudioServiceOutOfProcess');
55
55
 
56
- const APP_NAME = 'LiveDesk Desktop';
56
+ const APP_NAME = 'VuvoDesk Desktop';
57
+ const DESKTOP_STARTUP_ENTRY_NAME = 'LiveDesk Desktop';
57
58
  const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
58
59
  const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
59
60
  const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
@@ -77,11 +78,11 @@ const SUPABASE_URL = DESKTOP_AUTH_CONFIG.supabaseUrl;
77
78
  const SUPABASE_PUBLISHABLE_KEY = DESKTOP_AUTH_CONFIG.publishableKey;
78
79
  const OAUTH_REDIRECT_URI = DESKTOP_AUTH_CONFIG.oauthRedirectUri;
79
80
  const OAUTH_PENDING_TTL_MS = 10 * 60 * 1000;
80
- const LEGACY_STARTUP_ENTRY_NAMES = ['LiveDesk Client.vbs', 'LiveDesk Client.cmd', 'LiveDesk Desktop.vbs'];
81
+ const LEGACY_STARTUP_ENTRY_NAMES = ['LiveDesk Client.vbs', 'LiveDesk Client.cmd', 'LiveDesk Desktop.vbs'];
81
82
  const configuredStateRoot = process.env.LIVEDESK_DESKTOP_STATE_DIR || process.env.LIVEDESK_STATE_DIR || '';
82
83
  const stateRoot = configuredStateRoot
83
84
  ? resolve(configuredStateRoot)
84
- : join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
85
+ : join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
85
86
  const logDir = join(stateRoot, 'logs');
86
87
  const logPath = join(logDir, 'desktop.log');
87
88
  const authSessionPath = join(stateRoot, 'auth-session.json');
@@ -93,50 +94,50 @@ const unpackedAppPath = appPath.replace(/app\.asar([\\/]|$)/, 'app.asar.unpacked
93
94
  const appRoot = [appPath, unpackedAppPath, resolve(appPath, '..'), resolve(unpackedAppPath, '..')]
94
95
  .find(candidate => existsSync(join(candidate, 'bin', 'livedesk.js')) || existsSync(join(candidate, 'package.json')))
95
96
  || appPath;
96
- const runtimeCwd = existsSync(join(unpackedAppPath, 'bin', 'livedesk.js')) ? unpackedAppPath : appRoot;
97
- const require = createRequire(import.meta.url);
97
+ const runtimeCwd = existsSync(join(unpackedAppPath, 'bin', 'livedesk.js')) ? unpackedAppPath : appRoot;
98
+ const require = createRequire(import.meta.url);
99
+
100
+ function appResource(...parts) {
101
+ const unpacked = resolve(unpackedAppPath, ...parts);
102
+ return existsSync(unpacked) ? unpacked : resolve(appRoot, ...parts);
103
+ }
98
104
 
99
- function appResource(...parts) {
100
- const unpacked = resolve(unpackedAppPath, ...parts);
101
- return existsSync(unpacked) ? unpacked : resolve(appRoot, ...parts);
102
- }
103
-
104
- function resolveClipboardHelperPath() {
105
- const override = String(process.env.LIVEDESK_CLIPBOARD_HELPER_PATH || '').trim();
106
- if (override && existsSync(override)) return resolve(override);
107
- const platform = process.platform;
108
- const arch = process.arch;
109
- const rid = platform === 'win32' && arch === 'x64'
110
- ? 'win-x64'
111
- : platform === 'darwin' && arch === 'arm64'
112
- ? 'osx-arm64'
113
- : platform === 'darwin' && arch === 'x64'
114
- ? 'osx-x64'
115
- : platform === 'linux' && arch === 'x64'
116
- ? 'linux-x64'
117
- : '';
118
- if (!rid) return '';
119
- const executableName = platform === 'win32' ? 'livedesk-client-fast.exe' : 'livedesk-client-fast';
120
- const packageName = `@livedesk/fast-${rid}`;
121
- const candidates = [];
122
- const sourceCandidates = [
123
- resolve(appRoot, '..', `fast-${rid}`, 'fast', executableName),
124
- resolve(appRoot, '..', 'client', 'fast', rid, executableName),
125
- resolve(appRoot, '..', '..', 'packages', `fast-${rid}`, 'fast', executableName)
126
- ];
127
- if (!app.isPackaged) candidates.push(...sourceCandidates);
128
- try {
129
- const packagePath = require.resolve(`${packageName}/package.json`);
130
- const packagedPath = join(dirname(packagePath), 'fast', executableName);
131
- candidates.push(packagedPath.replace(/app\.asar([\\/]|$)/, 'app.asar.unpacked$1'), packagedPath);
132
- } catch {
133
- // Development checkouts use the source/package staging candidates below.
134
- }
135
- candidates.push(
136
- appResource('client', 'fast', rid, executableName),
137
- ...sourceCandidates);
138
- return candidates.find(candidate => existsSync(candidate)) || '';
139
- }
105
+ function resolveClipboardHelperPath() {
106
+ const override = String(process.env.LIVEDESK_CLIPBOARD_HELPER_PATH || '').trim();
107
+ if (override && existsSync(override)) return resolve(override);
108
+ const platform = process.platform;
109
+ const arch = process.arch;
110
+ const rid = platform === 'win32' && arch === 'x64'
111
+ ? 'win-x64'
112
+ : platform === 'darwin' && arch === 'arm64'
113
+ ? 'osx-arm64'
114
+ : platform === 'darwin' && arch === 'x64'
115
+ ? 'osx-x64'
116
+ : platform === 'linux' && arch === 'x64'
117
+ ? 'linux-x64'
118
+ : '';
119
+ if (!rid) return '';
120
+ const executableName = platform === 'win32' ? 'livedesk-client-fast.exe' : 'livedesk-client-fast';
121
+ const packageName = `@livedesk/fast-${rid}`;
122
+ const candidates = [];
123
+ const sourceCandidates = [
124
+ resolve(appRoot, '..', `fast-${rid}`, 'fast', executableName),
125
+ resolve(appRoot, '..', 'client', 'fast', rid, executableName),
126
+ resolve(appRoot, '..', '..', 'packages', `fast-${rid}`, 'fast', executableName)
127
+ ];
128
+ if (!app.isPackaged) candidates.push(...sourceCandidates);
129
+ try {
130
+ const packagePath = require.resolve(`${packageName}/package.json`);
131
+ const packagedPath = join(dirname(packagePath), 'fast', executableName);
132
+ candidates.push(packagedPath.replace(/app\.asar([\\/]|$)/, 'app.asar.unpacked$1'), packagedPath);
133
+ } catch {
134
+ // Development checkouts use the source/package staging candidates below.
135
+ }
136
+ candidates.push(
137
+ appResource('client', 'fast', rid, executableName),
138
+ ...sourceCandidates);
139
+ return candidates.find(candidate => existsSync(candidate)) || '';
140
+ }
140
141
 
141
142
  // Electron's ASAR loader keeps unpacked files addressable through the virtual
142
143
  // app.asar path while resolving their package dependencies from the same tree.
@@ -151,11 +152,11 @@ let runtimeChild = null;
151
152
  let isQuitting = false;
152
153
  let runtimeRestartCount = 0;
153
154
  let runtimeRestartWindowStartedAt = 0;
154
- let runtimeRestartTimer = null;
155
- let runtimeRestartPromise = null;
156
- let runtimeLaunchStartedAt = 0;
157
- let runtimeRoleTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
158
- let quitSequenceStarted = false;
155
+ let runtimeRestartTimer = null;
156
+ let runtimeRestartPromise = null;
157
+ let runtimeLaunchStartedAt = 0;
158
+ let runtimeRoleTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
159
+ let quitSequenceStarted = false;
159
160
  let updateInstallSequenceStarted = false;
160
161
  let productUpdates = null;
161
162
  let recurringProductUpdates = null;
@@ -165,10 +166,10 @@ let runtimeLogoutPromise = null;
165
166
  let desktopAuthRefreshTimer = null;
166
167
  let desktopSessionRestorePromise = null;
167
168
  let lastDesktopResumeRecoveryAt = 0;
168
- let desktopSessionEpoch = 0;
169
- let lastKnownDesktopSession = null;
170
- let desktopClipboardOwner = null;
171
- const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
169
+ let desktopSessionEpoch = 0;
170
+ let lastKnownDesktopSession = null;
171
+ let desktopClipboardOwner = null;
172
+ const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
172
173
  const FALLBACK_ICON_DATA_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none"><rect x="1" y="1" width="30" height="30" rx="7" fill="#111827"/><g transform="translate(5 5) scale(.785714)"><rect x="2.5" y="4" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".42"/><path d="M7 16v1.5h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".42"/><rect x="10.5" y="2.5" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".62"/><path d="M15 14.5V16h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".62"/><rect x="6.5" y="8.5" width="15" height="10" rx="1.7" fill="#F8FAFC" fill-opacity=".12" stroke="#F8FAFC" stroke-width="1.9"/><path d="M12 21v1.5H9.5M16 21v1.5h2.5M9.5 22.5h9" stroke="#F8FAFC" stroke-width="1.9" stroke-linecap="round"/></g></svg>')}`;
173
174
  const desktopLogger = createBoundedDesktopLogger({ logPath });
174
175
 
@@ -402,23 +403,23 @@ async function drainRuntimeTreeUntilStopped(child, deadline) {
402
403
  );
403
404
  }
404
405
 
405
- function startRuntime(options = {}) {
406
- if (runtimeChild) return runtimeChild;
407
- runtimeLaunchStartedAt = Date.now();
408
- const preResolvedRole = options?.role === 'hub' || options?.role === 'client' ? options.role : '';
409
- const roleTakeover = options?.roleTakeover === true || runtimeRoleTakeoverPending;
410
- if (roleTakeover) runtimeRoleTakeoverPending = true;
411
- const ownsUnixProcessGroup = process.platform !== 'win32';
406
+ function startRuntime(options = {}) {
407
+ if (runtimeChild) return runtimeChild;
408
+ runtimeLaunchStartedAt = Date.now();
409
+ const preResolvedRole = options?.role === 'hub' || options?.role === 'client' ? options.role : '';
410
+ const roleTakeover = options?.roleTakeover === true || runtimeRoleTakeoverPending;
411
+ if (roleTakeover) runtimeRoleTakeoverPending = true;
412
+ const ownsUnixProcessGroup = process.platform !== 'win32';
412
413
  const child = spawn(runtimeNode(), [launcherEntry, '--no-open'], {
413
414
  cwd: runtimeCwd,
414
415
  env: {
415
416
  ...process.env,
416
417
  LIVEDESK_DESKTOP_HOST: '1',
417
418
  LIVEDESK_MANAGER_VERSION: app.getVersion(),
418
- LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
419
- LIVEDESK_NPM_LAUNCHER_VERSION: app.getVersion(),
420
- LIVEDESK_STATE_DIR: runtimeStateDir,
421
- LIVEDESK_ROLE_TAKEOVER: roleTakeover ? '1' : '',
419
+ LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
420
+ LIVEDESK_NPM_LAUNCHER_VERSION: app.getVersion(),
421
+ LIVEDESK_STATE_DIR: runtimeStateDir,
422
+ LIVEDESK_ROLE_TAKEOVER: roleTakeover ? '1' : '',
422
423
  ...(preResolvedRole ? {
423
424
  LIVEDESK_PRE_RESOLVED_ROLE: preResolvedRole,
424
425
  LIVEDESK_ROLE_SOURCE: String(options.source || 'supabase'),
@@ -450,23 +451,23 @@ function startRuntime(options = {}) {
450
451
  // `exit` starts descendant cleanup without waiting for inherited stdio to
451
452
  // close. `close` is the failed-spawn fallback because Node does not promise
452
453
  // a corresponding `exit` event after every ChildProcess `error`.
453
- const handleRuntimeTerminal = (code, signal, source) => {
454
+ const handleRuntimeTerminal = (code, signal, source) => {
454
455
  if (runtimeTerminalHandled) return;
455
- runtimeTerminalHandled = true;
456
- log(`runtime terminal source=${source} code=${code ?? 'null'} signal=${signal || 'none'}`);
457
- const expectedRoleAfterExit = expectedRuntimeRole();
458
- const takeoverWasPending = runtimeRoleTakeoverPending;
459
- runtimeRoleTakeoverPending = resolveDesktopHubTakeoverPending({
460
- pending: runtimeRoleTakeoverPending,
461
- exitCode: code,
462
- signal,
463
- expectedRole: expectedRoleAfterExit
464
- });
465
- if (runtimeRoleTakeoverPending !== takeoverWasPending) {
466
- log(runtimeRoleTakeoverPending
467
- ? 'desktop role transition retained one-shot Hub target takeover authority'
468
- : 'desktop role transition cleared Hub target takeover authority for Client startup');
469
- }
456
+ runtimeTerminalHandled = true;
457
+ log(`runtime terminal source=${source} code=${code ?? 'null'} signal=${signal || 'none'}`);
458
+ const expectedRoleAfterExit = expectedRuntimeRole();
459
+ const takeoverWasPending = runtimeRoleTakeoverPending;
460
+ runtimeRoleTakeoverPending = resolveDesktopHubTakeoverPending({
461
+ pending: runtimeRoleTakeoverPending,
462
+ exitCode: code,
463
+ signal,
464
+ expectedRole: expectedRoleAfterExit
465
+ });
466
+ if (runtimeRoleTakeoverPending !== takeoverWasPending) {
467
+ log(runtimeRoleTakeoverPending
468
+ ? 'desktop role transition retained one-shot Hub target takeover authority'
469
+ : 'desktop role transition cleared Hub target takeover authority for Client startup');
470
+ }
470
471
  const continueAfterExactDrain = async () => {
471
472
  try {
472
473
  const drainDeadline = Number(child.livedeskRuntimeStopDeadline || 0)
@@ -506,42 +507,42 @@ function startRuntime(options = {}) {
506
507
  };
507
508
  void continueAfterExactDrain();
508
509
  };
509
- child.once('exit', (code, signal) => handleRuntimeTerminal(code, signal, 'exit'));
510
- child.once('close', (code, signal) => handleRuntimeTerminal(code, signal, 'close'));
511
- if (roleTakeover) void monitorDesktopHubTakeoverPublication(child);
512
- log(`runtime started pid=${child.pid}`);
513
- return child;
514
- }
515
-
516
- async function monitorDesktopHubTakeoverPublication(child) {
517
- while (!isQuitting && runtimeChild === child && runtimeRoleTakeoverPending) {
518
- try {
519
- const deadline = Date.now() + 1_000;
520
- const status = await fetchBeforeRuntimeDeadline(
521
- fetch,
522
- `${LOCAL_RUNTIME_URL}api/hub/status`,
523
- { cache: 'no-store' },
524
- {
525
- deadline,
526
- maxDurationMs: remainingRuntimeDeadlineMs(deadline),
527
- label: 'hub-role-takeover-publication-deadline',
528
- consumeResponse: response => response.json()
529
- }
530
- );
531
- if (desktopHubTakeoverPublicationConfirmed(status)) {
532
- if (runtimeChild === child) {
533
- runtimeRoleTakeoverPending = false;
534
- log('desktop Hub target takeover completed and one-shot authority was released');
535
- }
536
- return true;
537
- }
538
- } catch {
539
- // The replacement Hub may still be starting or retrying its saved session.
540
- }
541
- await new Promise(resolveDelay => setTimeout(resolveDelay, 1_000));
542
- }
543
- return false;
544
- }
510
+ child.once('exit', (code, signal) => handleRuntimeTerminal(code, signal, 'exit'));
511
+ child.once('close', (code, signal) => handleRuntimeTerminal(code, signal, 'close'));
512
+ if (roleTakeover) void monitorDesktopHubTakeoverPublication(child);
513
+ log(`runtime started pid=${child.pid}`);
514
+ return child;
515
+ }
516
+
517
+ async function monitorDesktopHubTakeoverPublication(child) {
518
+ while (!isQuitting && runtimeChild === child && runtimeRoleTakeoverPending) {
519
+ try {
520
+ const deadline = Date.now() + 1_000;
521
+ const status = await fetchBeforeRuntimeDeadline(
522
+ fetch,
523
+ `${LOCAL_RUNTIME_URL}api/hub/status`,
524
+ { cache: 'no-store' },
525
+ {
526
+ deadline,
527
+ maxDurationMs: remainingRuntimeDeadlineMs(deadline),
528
+ label: 'hub-role-takeover-publication-deadline',
529
+ consumeResponse: response => response.json()
530
+ }
531
+ );
532
+ if (desktopHubTakeoverPublicationConfirmed(status)) {
533
+ if (runtimeChild === child) {
534
+ runtimeRoleTakeoverPending = false;
535
+ log('desktop Hub target takeover completed and one-shot authority was released');
536
+ }
537
+ return true;
538
+ }
539
+ } catch {
540
+ // The replacement Hub may still be starting or retrying its saved session.
541
+ }
542
+ await new Promise(resolveDelay => setTimeout(resolveDelay, 1_000));
543
+ }
544
+ return false;
545
+ }
545
546
 
546
547
  function expectedRuntimeRole() {
547
548
  const forcedRole = String(process.env.LIVEDESK_FORCE_ROLE || '').trim().toLowerCase();
@@ -1071,11 +1072,11 @@ function registerIpc() {
1071
1072
  return { ok: false, error: 'application-exit-already-started' };
1072
1073
  }
1073
1074
  quitSequenceStarted = true;
1074
- isQuitting = true;
1075
- try {
1076
- await stopRuntime();
1077
- await desktopClipboardOwner?.close();
1078
- await closeDesktopLog();
1075
+ isQuitting = true;
1076
+ try {
1077
+ await stopRuntime();
1078
+ await desktopClipboardOwner?.close();
1079
+ await closeDesktopLog();
1079
1080
  app.relaunch();
1080
1081
  app.exit(0);
1081
1082
  return { ok: true };
@@ -1142,32 +1143,32 @@ function registerIpc() {
1142
1143
  } catch (error) { return { ok: false, error: mask(error?.message || error) }; }
1143
1144
  });
1144
1145
  handle('logs:open-folder', () => shell.openPath(logDir));
1145
- handle('logs:copy-diagnostics', async () => {
1146
+ handle('logs:copy-diagnostics', async () => {
1146
1147
  const status = await fetch(`${LOCAL_RUNTIME_URL}api/runtime/status`).then(response => response.json()).catch(error => ({ ok: false, error: mask(error?.message || error) }));
1147
1148
  const diagnostics = JSON.stringify({ appVersion: app.getVersion(), runtime: status, logDir }, null, 2);
1148
- clipboard.writeText(mask(diagnostics));
1149
- return { ok: true };
1150
- });
1151
- handle('clipboard:read-snapshot', operationId => desktopClipboardOwner?.readSnapshot(operationId)
1152
- || Promise.reject(new Error('clipboard-helper-unavailable')));
1153
- handle('clipboard:read-chunk', request => desktopClipboardOwner?.readChunk(request)
1154
- || Promise.reject(new Error('clipboard-helper-unavailable')));
1155
- handle('clipboard:release-read', operationId => desktopClipboardOwner?.releaseRead(operationId)
1156
- || Promise.resolve({ ok: true }));
1157
- handle('clipboard:begin-write', manifest => desktopClipboardOwner?.beginWrite(manifest)
1158
- || Promise.reject(new Error('clipboard-helper-unavailable')));
1159
- handle('clipboard:write-chunk', request => desktopClipboardOwner?.writeChunk(request)
1160
- || Promise.reject(new Error('clipboard-helper-unavailable')));
1161
- handle('clipboard:commit-write', operationId => desktopClipboardOwner?.commitWrite(operationId)
1162
- || Promise.reject(new Error('clipboard-helper-unavailable')));
1163
- handle('clipboard:cancel-write', operationId => desktopClipboardOwner?.cancelWrite(operationId)
1164
- || Promise.resolve({ ok: true }));
1149
+ clipboard.writeText(mask(diagnostics));
1150
+ return { ok: true };
1151
+ });
1152
+ handle('clipboard:read-snapshot', operationId => desktopClipboardOwner?.readSnapshot(operationId)
1153
+ || Promise.reject(new Error('clipboard-helper-unavailable')));
1154
+ handle('clipboard:read-chunk', request => desktopClipboardOwner?.readChunk(request)
1155
+ || Promise.reject(new Error('clipboard-helper-unavailable')));
1156
+ handle('clipboard:release-read', operationId => desktopClipboardOwner?.releaseRead(operationId)
1157
+ || Promise.resolve({ ok: true }));
1158
+ handle('clipboard:begin-write', manifest => desktopClipboardOwner?.beginWrite(manifest)
1159
+ || Promise.reject(new Error('clipboard-helper-unavailable')));
1160
+ handle('clipboard:write-chunk', request => desktopClipboardOwner?.writeChunk(request)
1161
+ || Promise.reject(new Error('clipboard-helper-unavailable')));
1162
+ handle('clipboard:commit-write', operationId => desktopClipboardOwner?.commitWrite(operationId)
1163
+ || Promise.reject(new Error('clipboard-helper-unavailable')));
1164
+ handle('clipboard:cancel-write', operationId => desktopClipboardOwner?.cancelWrite(operationId)
1165
+ || Promise.resolve({ ok: true }));
1165
1166
  handle('app:about', async () => {
1166
1167
  await dialog.showMessageBox(mainWindow, {
1167
1168
  type: 'info',
1168
1169
  title: APP_NAME,
1169
1170
  message: APP_NAME,
1170
- detail: `Version ${app.getVersion()}\nOne LiveDesk runtime with Hub and Client roles.`
1171
+ detail: `Version ${app.getVersion()}\nOne VuvoDesk runtime with Hub and Client roles.`
1171
1172
  });
1172
1173
  return { ok: true };
1173
1174
  });
@@ -1180,14 +1181,14 @@ function createTray() {
1180
1181
  const iconPath = appResource('electron', process.platform === 'win32' ? 'tray.ico' : 'tray.png');
1181
1182
  tray = new Tray(existsSync(iconPath) ? iconPath : nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL));
1182
1183
  const menu = Menu.buildFromTemplate([
1183
- { label: 'Show LiveDesk', click: showWindow },
1184
+ { label: 'Show VuvoDesk', click: showWindow },
1184
1185
  { label: 'Restart runtime', click: () => requestRuntimeRestart('tray') },
1185
1186
  { label: 'Check for updates', click: () => { void productUpdates?.check(); } },
1186
- { label: 'About LiveDesk', click: () => { void dialog.showMessageBox(mainWindow, { type: 'info', title: APP_NAME, message: APP_NAME, detail: `Version ${app.getVersion()}\nOne LiveDesk runtime with Hub and Client roles.` }); } },
1187
+ { label: 'About VuvoDesk', click: () => { void dialog.showMessageBox(mainWindow, { type: 'info', title: APP_NAME, message: APP_NAME, detail: `Version ${app.getVersion()}\nOne VuvoDesk runtime with Hub and Client roles.` }); } },
1187
1188
  { label: 'Open logs', click: () => { void shell.openPath(logDir); } },
1188
1189
  { type: 'separator' },
1189
- { label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name: APP_NAME }) },
1190
- { label: 'Quit LiveDesk', click: () => { app.quit(); } }
1190
+ { label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name: DESKTOP_STARTUP_ENTRY_NAME }) },
1191
+ { label: 'Quit VuvoDesk', click: () => { app.quit(); } }
1191
1192
  ]);
1192
1193
  tray.setContextMenu(menu);
1193
1194
  tray.on('click', showWindow);
@@ -1226,20 +1227,20 @@ if (!app.requestSingleInstanceLock()) {
1226
1227
  beforeInstall: prepareRuntimeForUpdateInstall,
1227
1228
  onInstallError: recoverRuntimeAfterUpdateInstallError
1228
1229
  });
1229
- recurringProductUpdates = createRecurringProductUpdateOwner({
1230
+ recurringProductUpdates = createRecurringProductUpdateOwner({
1230
1231
  manager: productUpdates,
1231
1232
  canRun: () => !isQuitting && !quitSequenceStarted && !updateInstallSequenceStarted,
1232
1233
  initialDelayMs: 15_000,
1233
1234
  intervalMs: 30_000,
1234
- onError: error => log(`recurring desktop update failed: ${error?.message || error}`)
1235
- });
1236
- const clipboardHelperPath = resolveClipboardHelperPath();
1237
- desktopClipboardOwner = createDesktopClipboardOwner({
1238
- rootDir: join(stateRoot, 'clipboard-staging'),
1239
- helperPath: clipboardHelperPath
1240
- });
1241
- if (!clipboardHelperPath) log('desktop clipboard helper unavailable for this platform');
1242
- registerIpc();
1235
+ onError: error => log(`recurring desktop update failed: ${error?.message || error}`)
1236
+ });
1237
+ const clipboardHelperPath = resolveClipboardHelperPath();
1238
+ desktopClipboardOwner = createDesktopClipboardOwner({
1239
+ rootDir: join(stateRoot, 'clipboard-staging'),
1240
+ helperPath: clipboardHelperPath
1241
+ });
1242
+ if (!clipboardHelperPath) log('desktop clipboard helper unavailable for this platform');
1243
+ registerIpc();
1243
1244
  powerMonitor.on('resume', () => requestDesktopResumeAuthRecovery('system-resume'));
1244
1245
  powerMonitor.on('unlock-screen', () => requestDesktopResumeAuthRecovery('screen-unlock'));
1245
1246
  if (DESKTOP_BUILD_FLAVOR !== 'e2e') {
@@ -1277,11 +1278,11 @@ if (!app.requestSingleInstanceLock()) {
1277
1278
  event.preventDefault();
1278
1279
  recurringProductUpdates?.stop();
1279
1280
  quitSequenceStarted = true;
1280
- isQuitting = true;
1281
- void stopRuntime().then(() => {
1282
- return desktopClipboardOwner?.close();
1283
- }).then(() => {
1284
- return closeDesktopLog();
1281
+ isQuitting = true;
1282
+ void stopRuntime().then(() => {
1283
+ return desktopClipboardOwner?.close();
1284
+ }).then(() => {
1285
+ return closeDesktopLog();
1285
1286
  }).then(() => {
1286
1287
  app.quit();
1287
1288
  }).catch(error => {
@@ -24,20 +24,20 @@ contextBridge.exposeInMainWorld('liveDesk', {
24
24
  restart: () => ipcRenderer.invoke('runtime:restart'),
25
25
  switchRole: role => ipcRenderer.invoke('runtime:switch-role', role)
26
26
  },
27
- logs: {
27
+ logs: {
28
28
  openFolder: () => ipcRenderer.invoke('logs:open-folder'),
29
- copyDiagnostics: () => ipcRenderer.invoke('logs:copy-diagnostics')
30
- },
31
- clipboard: {
32
- readSnapshot: operationId => ipcRenderer.invoke('clipboard:read-snapshot', operationId),
33
- readChunk: request => ipcRenderer.invoke('clipboard:read-chunk', request),
34
- releaseRead: operationId => ipcRenderer.invoke('clipboard:release-read', operationId),
35
- beginWrite: manifest => ipcRenderer.invoke('clipboard:begin-write', manifest),
36
- writeChunk: request => ipcRenderer.invoke('clipboard:write-chunk', request),
37
- commitWrite: operationId => ipcRenderer.invoke('clipboard:commit-write', operationId),
38
- cancelWrite: operationId => ipcRenderer.invoke('clipboard:cancel-write', operationId)
39
- },
40
- updates: {
29
+ copyDiagnostics: () => ipcRenderer.invoke('logs:copy-diagnostics')
30
+ },
31
+ clipboard: {
32
+ readSnapshot: operationId => ipcRenderer.invoke('clipboard:read-snapshot', operationId),
33
+ readChunk: request => ipcRenderer.invoke('clipboard:read-chunk', request),
34
+ releaseRead: operationId => ipcRenderer.invoke('clipboard:release-read', operationId),
35
+ beginWrite: manifest => ipcRenderer.invoke('clipboard:begin-write', manifest),
36
+ writeChunk: request => ipcRenderer.invoke('clipboard:write-chunk', request),
37
+ commitWrite: operationId => ipcRenderer.invoke('clipboard:commit-write', operationId),
38
+ cancelWrite: operationId => ipcRenderer.invoke('clipboard:cancel-write', operationId)
39
+ },
40
+ updates: {
41
41
  getStatus: () => ipcRenderer.invoke('updates:get-status'),
42
42
  check: () => ipcRenderer.invoke('updates:check'),
43
43
  install: () => ipcRenderer.invoke('updates:install'),
@@ -1,17 +1,17 @@
1
- export const DESKTOP_ROLE_TRANSITION_EXIT_CODE = 43;
2
-
3
- export function resolveDesktopHubTakeoverPending(options = {}) {
4
- const pending = options.pending === true;
5
- const signal = String(options.signal || '').trim();
6
- const exitCode = Number(options.exitCode);
7
- if (signal || exitCode !== DESKTOP_ROLE_TRANSITION_EXIT_CODE) {
8
- return pending;
9
- }
10
- return String(options.expectedRole || '').trim().toLowerCase() === 'hub';
11
- }
12
-
13
- export function desktopHubTakeoverPublicationConfirmed(status = {}) {
14
- return String(status.role || '').trim().toLowerCase() === 'hub'
15
- && status.hostTargetActive === true
16
- && String(status.hostTargetLease?.state || '').trim().toLowerCase() === 'active';
17
- }
1
+ export const DESKTOP_ROLE_TRANSITION_EXIT_CODE = 43;
2
+
3
+ export function resolveDesktopHubTakeoverPending(options = {}) {
4
+ const pending = options.pending === true;
5
+ const signal = String(options.signal || '').trim();
6
+ const exitCode = Number(options.exitCode);
7
+ if (signal || exitCode !== DESKTOP_ROLE_TRANSITION_EXIT_CODE) {
8
+ return pending;
9
+ }
10
+ return String(options.expectedRole || '').trim().toLowerCase() === 'hub';
11
+ }
12
+
13
+ export function desktopHubTakeoverPublicationConfirmed(status = {}) {
14
+ return String(status.role || '').trim().toLowerCase() === 'hub'
15
+ && status.hostTargetActive === true
16
+ && String(status.hostTargetLease?.state || '').trim().toLowerCase() === 'active';
17
+ }