@livedesk/client 0.1.172 → 0.1.174

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.
@@ -14,6 +14,7 @@ import { createClientRuntimeServer } from '../src/runtime/client-runtime-server.
14
14
  import { installAgentTerminationHandlers } from '../src/runtime/agent-process-lifecycle.js';
15
15
  import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
16
16
  import { normalizeRuntimeAuthSession } from '../../runtime-core/src/auth-session.js';
17
+ import { startRoleTransitionSupervisor } from '../../runtime-core/src/role-transition-supervisor.js';
17
18
 
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  const require = createRequire(import.meta.url);
@@ -21,6 +22,7 @@ const packageRoot = resolve(__dirname, '..');
21
22
  const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
22
23
  const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
23
24
  const DEFAULT_MANAGER = '127.0.0.1:5197';
25
+ const DEFAULT_HUB_CLIENT_PORT = 5197;
24
26
  const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
25
27
  const DEFAULT_AUTH_CALLBACK_PORT = 5179;
26
28
  const ENDPOINT_PROBE_TIMEOUT_MS = 900;
@@ -112,30 +114,53 @@ function writeLocalRoleCache(role, deviceId, roleVersion = 0, assignedHubId = ''
112
114
  return true;
113
115
  }
114
116
 
115
- function spawnUnifiedRoleRestart(role) {
116
- if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
117
- console.log(`[LiveDesk] Desktop supervisor will restart the runtime as ${role}.`);
118
- return false;
119
- }
120
- const unifiedEntry = String(process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || '').trim();
121
- const child = unifiedEntry && existsSync(unifiedEntry)
122
- ? spawn(process.execPath, [unifiedEntry, '--force-role', role, '--no-open'], {
123
- env: {
124
- ...process.env,
125
- LIVEDESK_ROLE_TRANSITION: '1',
126
- LIVEDESK_RESTART_WAIT_PID: String(process.ppid),
127
- LIVEDESK_SKIP_BROWSER_OPEN: '1'
128
- },
129
- detached: true,
130
- stdio: 'ignore',
131
- windowsHide: true
132
- })
133
- : process.platform === 'win32'
134
- ? spawn('npx.cmd', ['-y', '--prefer-online', 'livedesk@latest', '--force-role', role, '--no-open'], { detached: true, stdio: 'ignore', windowsHide: true })
135
- : spawn('npx', ['-y', '--prefer-online', 'livedesk@latest', '--force-role', role, '--no-open'], { detached: true, stdio: 'ignore' });
136
- child.unref();
137
- return true;
138
- }
117
+ export function buildUnifiedRoleRestartEnvironment(role, baseEnv = process.env, runtimePid = process.pid) {
118
+ const normalizedRole = String(role || '').trim().toLowerCase();
119
+ const waitPid = Number(runtimePid);
120
+ if (normalizedRole !== 'hub' && normalizedRole !== 'client') {
121
+ throw new Error(`Unsupported LiveDesk role restart: ${role}`);
122
+ }
123
+ if (!Number.isInteger(waitPid) || waitPid <= 1) {
124
+ throw new Error(`Invalid LiveDesk runtime PID for role restart: ${runtimePid}`);
125
+ }
126
+ return {
127
+ ...baseEnv,
128
+ LIVEDESK_ROLE_TRANSITION: '1',
129
+ // The replacement must wait only for this Client runtime. Waiting for
130
+ // npm, npx, PowerShell, or another terminal parent can deadlock the
131
+ // takeover even after Supabase has selected this device as Hub.
132
+ LIVEDESK_RESTART_WAIT_PID: String(waitPid),
133
+ LIVEDESK_ROLE_TAKEOVER: normalizedRole === 'hub' ? '1' : '',
134
+ LIVEDESK_SKIP_BROWSER_OPEN: '1'
135
+ };
136
+ }
137
+
138
+ async function spawnUnifiedRoleRestart(role) {
139
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
140
+ console.log(`[LiveDesk] Desktop supervisor will restart the runtime as ${role}.`);
141
+ return false;
142
+ }
143
+ const unifiedEntry = String(process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || '').trim();
144
+ const restartEnvironment = buildUnifiedRoleRestartEnvironment(role);
145
+ if (!unifiedEntry || !existsSync(unifiedEntry)) {
146
+ throw new Error(`LiveDesk unified role launcher is unavailable: ${unifiedEntry || 'missing path'}`);
147
+ }
148
+ const handoff = await startRoleTransitionSupervisor({
149
+ role,
150
+ previousPid: process.pid,
151
+ launcherEntry: unifiedEntry,
152
+ stateDir: UNIFIED_CLIENT_STATE_DIR,
153
+ runtimePort: Number(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || DEFAULT_AUTH_CALLBACK_PORT),
154
+ remotePort: resolveHubClientPort(),
155
+ env: restartEnvironment,
156
+ cwd: process.cwd()
157
+ });
158
+ console.log(
159
+ `[LiveDesk] Role transition supervisor claimed target=${role} `
160
+ + `pid=${handoff.supervisorPid}. Diagnostics: ${handoff.logPath}`
161
+ );
162
+ return true;
163
+ }
139
164
 
140
165
  function printHelp() {
141
166
  process.stdout.write(`
@@ -349,15 +374,64 @@ function parseLauncherArgs(argv) {
349
374
  };
350
375
  }
351
376
 
352
- function normalizePort(value) {
377
+ function normalizePort(value) {
353
378
  const port = Number(String(value || '').trim());
354
379
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
355
380
  return 0;
356
381
  }
357
382
  return port;
358
- }
359
-
360
- function normalizeSlotNumber(value) {
383
+ }
384
+
385
+ export function resolveHubClientPort(env = process.env) {
386
+ return normalizePort(env.REMOTE_HUB_PORT || env.LIVEDESK_HUB_REMOTE_PORT) || DEFAULT_HUB_CLIENT_PORT;
387
+ }
388
+
389
+ export function preflightHubClientPort(port = resolveHubClientPort()) {
390
+ const normalizedPort = normalizePort(port);
391
+ if (!normalizedPort) {
392
+ return Promise.resolve({ ok: false, port: Number(port) || 0, code: 'invalid-hub-client-port' });
393
+ }
394
+ return new Promise(resolvePreflight => {
395
+ const probe = net.createServer();
396
+ let completed = false;
397
+ const finish = result => {
398
+ if (completed) return;
399
+ completed = true;
400
+ resolvePreflight({ port: normalizedPort, ...result });
401
+ };
402
+ probe.unref?.();
403
+ probe.once('error', error => {
404
+ finish({
405
+ ok: false,
406
+ code: error?.code === 'EADDRINUSE' ? 'hub-client-port-in-use' : 'hub-client-port-unavailable',
407
+ systemCode: String(error?.code || '')
408
+ });
409
+ });
410
+ probe.listen({ host: '0.0.0.0', port: normalizedPort, exclusive: true }, () => {
411
+ probe.close(error => {
412
+ if (error) {
413
+ finish({ ok: false, code: 'hub-client-port-unavailable', systemCode: String(error?.code || '') });
414
+ return;
415
+ }
416
+ finish({ ok: true, code: 'ok' });
417
+ });
418
+ });
419
+ });
420
+ }
421
+
422
+ function hubClientPortPreflightError(preflight) {
423
+ const port = Number(preflight?.port || resolveHubClientPort());
424
+ return {
425
+ ok: false,
426
+ code: String(preflight?.code || 'hub-client-port-unavailable'),
427
+ port,
428
+ error: preflight?.code === 'hub-client-port-in-use'
429
+ ? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
430
+ : `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
431
+ };
432
+ }
433
+
434
+ function normalizeSlotNumber(value) {
361
435
  const number = Number(String(value || '').trim());
362
436
  if (!Number.isInteger(number) || number < 1 || number > 999) {
363
437
  return '';
@@ -2628,12 +2702,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2628
2702
  payload = Object.fromEntries(params.entries());
2629
2703
  }
2630
2704
  const nextRole = String(payload.role || '').trim().toLowerCase();
2631
- if (nextRole !== 'hub') {
2705
+ if (nextRole !== 'hub') {
2632
2706
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
2633
2707
  res.end(JSON.stringify({ ok: false, error: 'client-can-only-transition-to-hub' }));
2634
- return;
2635
- }
2636
- const session = await refreshSessionIfNeeded(supabase);
2708
+ return;
2709
+ }
2710
+ const portPreflight = await preflightHubClientPort();
2711
+ if (!portPreflight.ok) {
2712
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
2713
+ res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
2714
+ return;
2715
+ }
2716
+ const session = await refreshSessionIfNeeded(supabase);
2637
2717
  const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
2638
2718
  const { data, error } = await supabase.rpc('set_livedesk_device_role', {
2639
2719
  p_device_id: deviceId,
@@ -2888,15 +2968,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2888
2968
 
2889
2969
  async function chooseClientConnection(supabase, options = {}) {
2890
2970
  if (isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)) {
2891
- const connectionPage = createClientRuntimeServer({
2892
- host: process.env.LIVEDESK_CLIENT_RUNTIME_HOST || '127.0.0.1',
2893
- port: options.authPort || DEFAULT_AUTH_CALLBACK_PORT,
2894
- webDist: process.env.LIVEDESK_WEB_DIST || '',
2971
+ const connectionPage = createClientRuntimeServer({
2972
+ host: process.env.LIVEDESK_CLIENT_RUNTIME_HOST || '127.0.0.1',
2973
+ port: options.authPort || DEFAULT_AUTH_CALLBACK_PORT,
2974
+ webDist: process.env.LIVEDESK_WEB_DIST || '',
2895
2975
  appVersion: process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion(),
2896
- deviceId: options.deviceId,
2897
- engine: options.engine,
2976
+ deviceId: options.deviceId,
2977
+ engine: options.engine,
2898
2978
  savedSession: options.savedSession,
2979
+ initialChoice: options.initialChoice,
2980
+ initialChoiceMessage: options.initialChoiceMessage,
2899
2981
  beginGoogleSignIn: async redirectTo => {
2982
+ if (!supabase?.auth) throw new Error('supabase-session-required');
2900
2983
  const { data, error } = await supabase.auth.signInWithOAuth({
2901
2984
  provider: 'google',
2902
2985
  options: {
@@ -2910,15 +2993,26 @@ async function chooseClientConnection(supabase, options = {}) {
2910
2993
  return { url: data.url };
2911
2994
  },
2912
2995
  exchangeGoogleCode: async code => {
2996
+ if (!supabase?.auth) throw new Error('supabase-session-required');
2913
2997
  const { data, error } = await supabase.auth.exchangeCodeForSession(code);
2914
2998
  if (error) throw error;
2915
2999
  if (!data?.session) throw new Error('google-session-missing');
2916
3000
  if (!writeSavedSessionToFile(data.session)) throw new Error('refresh-token-required');
2917
3001
  return data.session;
2918
3002
  },
2919
- resolvePin: pin => resolveManagerFromPin(supabase, pin),
2920
- changeRole: async (role, snapshot) => {
2921
- const session = await refreshSessionIfNeeded(supabase);
3003
+ resolvePin: pin => {
3004
+ if (!supabase) throw new Error('supabase-session-required');
3005
+ return resolveManagerFromPin(supabase, pin);
3006
+ },
3007
+ changeRole: async (role, snapshot) => {
3008
+ if (!supabase) {
3009
+ return { ok: false, error: 'supabase-session-required' };
3010
+ }
3011
+ const portPreflight = await preflightHubClientPort();
3012
+ if (!portPreflight.ok) {
3013
+ return hubClientPortPreflightError(portPreflight);
3014
+ }
3015
+ const session = await refreshSessionIfNeeded(supabase);
2922
3016
  if (!session?.access_token) {
2923
3017
  return { ok: false, error: 'supabase-session-required' };
2924
3018
  }
@@ -3255,7 +3349,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3255
3349
  let connectionPage = null;
3256
3350
  let discoverySource = '';
3257
3351
 
3258
- if (shouldLogin) {
3352
+ if (shouldLogin) {
3259
3353
  const supabase = await createSupabaseClient();
3260
3354
  const startupArgs = buildStartupClientArgs(parsed);
3261
3355
  let savedSession = null;
@@ -3362,10 +3456,38 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3362
3456
  message: `Connected to LiveDesk Hub at ${manager}.`
3363
3457
  });
3364
3458
  }
3365
- console.log(`Found LiveDesk Hub at ${manager}.`);
3366
- }
3367
-
3368
- const slot = normalizeSlotNumber(parsed.slot);
3459
+ console.log(`Found LiveDesk Hub at ${manager}.`);
3460
+ }
3461
+ // Exact-package updates preserve the already paired Hub endpoint and pass
3462
+ // --no-login so the replacement cannot block on browser auth. The unified
3463
+ // launcher must still own its local runtime/status API; the package
3464
+ // supervisor uses that API to prove the new launcher and Agent are stable
3465
+ // before it commits the update.
3466
+ if (!shouldLogin
3467
+ && !existingConnectionPage
3468
+ && isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)
3469
+ && manager
3470
+ && pair) {
3471
+ const preservedSession = readSavedSessionFromFile();
3472
+ const explicitConnection = await chooseClientConnection(null, {
3473
+ authPort: parsed.authPort,
3474
+ deviceId: parsed.deviceId,
3475
+ engine: parsed.engine,
3476
+ savedSession: preservedSession,
3477
+ openBrowser: false,
3478
+ initialChoice: {
3479
+ type: 'explicit',
3480
+ manager,
3481
+ pair,
3482
+ endpointCandidates: [manager],
3483
+ ...(preservedSession?.access_token ? { session: preservedSession } : {})
3484
+ },
3485
+ initialChoiceMessage: `Using the existing LiveDesk Hub pairing at ${manager}.`
3486
+ });
3487
+ connectionPage = explicitConnection.connectionPage || null;
3488
+ }
3489
+
3490
+ const slot = normalizeSlotNumber(parsed.slot);
3369
3491
  if (manager) {
3370
3492
  forwarded = upsertForwardedOption(forwarded, '--manager', manager);
3371
3493
  }
@@ -3892,12 +4014,12 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
3892
4014
  while (true) {
3893
4015
  const prepared = await prepareLoginConnection(parsed, connectionPage);
3894
4016
  connectionPage = prepared.connectionPage || connectionPage;
3895
- const roleRestartBeforeAgent = connectionPage?.getRoleRestartRequest?.() || roleRestartRequest;
3896
- if (roleRestartBeforeAgent?.role) {
3897
- connectionPage?.close?.();
3898
- if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
3899
- spawnUnifiedRoleRestart(roleRestartBeforeAgent.role);
3900
- process.exit(0);
4017
+ const roleRestartBeforeAgent = connectionPage?.getRoleRestartRequest?.() || roleRestartRequest;
4018
+ if (roleRestartBeforeAgent?.role) {
4019
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
4020
+ await spawnUnifiedRoleRestart(roleRestartBeforeAgent.role);
4021
+ connectionPage?.close?.();
4022
+ process.exit(0);
3901
4023
  }
3902
4024
  const fastRuntime = getFastRuntime();
3903
4025
  const useFast = shouldTryFast(prepared, fastRuntime);
@@ -3986,12 +4108,12 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
3986
4108
  });
3987
4109
  });
3988
4110
  }
3989
- const requestedRoleRestart = connectionPage?.getRoleRestartRequest?.();
3990
- if (requestedRoleRestart?.role) {
3991
- connectionPage?.close?.();
3992
- if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
3993
- spawnUnifiedRoleRestart(requestedRoleRestart.role);
3994
- process.exit(0);
4111
+ const requestedRoleRestart = connectionPage?.getRoleRestartRequest?.();
4112
+ if (requestedRoleRestart?.role) {
4113
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
4114
+ await spawnUnifiedRoleRestart(requestedRoleRestart.role);
4115
+ connectionPage?.close?.();
4116
+ process.exit(0);
3995
4117
  }
3996
4118
  if (result?.signal) {
3997
4119
  connectionPage?.close?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.172",
3
+ "version": "0.1.174",
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.378",
43
- "@livedesk/fast-osx-arm64": "0.1.378",
44
- "@livedesk/fast-osx-x64": "0.1.378",
45
- "@livedesk/fast-win-x64": "0.1.378"
42
+ "@livedesk/fast-linux-x64": "0.1.380",
43
+ "@livedesk/fast-osx-arm64": "0.1.380",
44
+ "@livedesk/fast-osx-x64": "0.1.380",
45
+ "@livedesk/fast-win-x64": "0.1.380"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
@@ -514,9 +514,16 @@ function sendText(res, status, body, type = 'text/plain; charset=utf-8') {
514
514
  res.end(body);
515
515
  }
516
516
 
517
- export function createClientRuntimeServer(options = {}) {
518
- const host = normalizeString(options.host, 80) || DEFAULT_HOST;
517
+ export function createClientRuntimeServer(options = {}) {
518
+ const host = normalizeString(options.host, 80) || DEFAULT_HOST;
519
519
  const port = normalizePort(options.port, DEFAULT_PORT);
520
+ const initialChoice = options.initialChoice && typeof options.initialChoice === 'object'
521
+ ? options.initialChoice
522
+ : null;
523
+ const initialChoiceMessage = normalizeString(
524
+ options.initialChoiceMessage,
525
+ 1000
526
+ ) || 'Existing LiveDesk credentials accepted. Starting the Client.';
520
527
  const trustedWebOrigins = createTrustedWebOrigins(port);
521
528
  const webDist = resolve(String(options.webDist || process.env.LIVEDESK_WEB_DIST || '').trim() || join(process.cwd(), 'apps', 'web', 'dist'));
522
529
  const deviceId = normalizeString(options.deviceId || process.env.LIVEDESK_DEVICE_ID, 160);
@@ -732,6 +739,7 @@ export function createClientRuntimeServer(options = {}) {
732
739
  osVersion: os.release(),
733
740
  pid: process.pid,
734
741
  cpuUsagePercent: Math.round(cpuUsagePercent * 10) / 10,
742
+ cpuLogicalCores: Math.max(1, os.cpus().length),
735
743
  memoryTotalBytes: totalMemoryBytes,
736
744
  memoryUsedBytes: Math.max(0, totalMemoryBytes - freeMemoryBytes),
737
745
  uptimeSeconds: Math.max(0, Math.floor(os.uptime())),
@@ -1030,23 +1038,29 @@ export function createClientRuntimeServer(options = {}) {
1030
1038
  else res.end();
1031
1039
  });
1032
1040
  });
1033
- server.once('error', rejectStart);
1034
- server.listen(port, host, () => {
1035
- runtime.emit('runtime.http.started', { host, port });
1036
- const saved = normalizeRuntimeAuthSession(options.savedSession, { requireRefreshToken: true });
1037
- if (saved.ok) {
1041
+ server.once('error', rejectStart);
1042
+ server.listen(port, host, () => {
1043
+ runtime.emit('runtime.http.started', { host, port });
1044
+ if (initialChoice) {
1038
1045
  setImmediate(() => {
1039
- try {
1040
- if (!writeSavedSession(saved.session)) throw new Error('refresh-token-required');
1041
- state.auth.persisted = true;
1042
- complete({ type: 'google', session: saved.session }, 'Saved sign-in found. Finding the LiveDesk Hub.');
1043
- } catch (error) {
1044
- recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
1045
- }
1046
+ complete(initialChoice, initialChoiceMessage);
1046
1047
  });
1048
+ } else {
1049
+ const saved = normalizeRuntimeAuthSession(options.savedSession, { requireRefreshToken: true });
1050
+ if (saved.ok) {
1051
+ setImmediate(() => {
1052
+ try {
1053
+ if (!writeSavedSession(saved.session)) throw new Error('refresh-token-required');
1054
+ state.auth.persisted = true;
1055
+ complete({ type: 'google', session: saved.session }, 'Saved sign-in found. Finding the LiveDesk Hub.');
1056
+ } catch (error) {
1057
+ recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
1058
+ }
1059
+ });
1060
+ }
1047
1061
  }
1048
- resolveStart();
1049
- });
1062
+ resolveStart();
1063
+ });
1050
1064
  });
1051
1065
 
1052
1066
  return {