@livedesk/client 0.1.56 → 0.1.58

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.
@@ -26,6 +26,7 @@ const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngv
26
26
  const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
27
27
  const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
28
28
  const CLIENT_AUTH_PATH = join(CLIENT_STATE_DIR, 'auth.json');
29
+ const CLIENT_PIN_PATH = join(CLIENT_STATE_DIR, 'pin.json');
29
30
  const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
30
31
 
31
32
  function printHelp() {
@@ -38,6 +39,8 @@ Usage:
38
39
 
39
40
  Default flow:
40
41
  Opens a LiveDesk connection page with Google sign-in or a 6-digit PIN.
42
+ After auth, that page stays open as this computer's client dashboard.
43
+ Saved Google sign-in or saved PIN starts the client automatically.
41
44
  Omit the number for first-available placement, or pass 1-999 to pin a slot.
42
45
  If no LiveDesk Hub is active yet, the client keeps checking every 5 seconds.
43
46
 
@@ -940,6 +943,332 @@ function renderConnectionChoicePage({ error = '', pin = '', slot = '', startup =
940
943
  startupToggle.addEventListener('change', syncStartupFields);
941
944
  syncStartupFields();
942
945
  }
946
+ async function watchAutoConnection() {
947
+ try {
948
+ const response = await fetch('/api/state', { cache: 'no-store' });
949
+ if (!response.ok) return;
950
+ const state = await response.json();
951
+ if (state.completed) {
952
+ location.replace('/');
953
+ }
954
+ } catch {
955
+ }
956
+ }
957
+ setInterval(watchAutoConnection, 800);
958
+ watchAutoConnection();
959
+ </script>
960
+ </body>
961
+ </html>`;
962
+ }
963
+
964
+ function getChoiceEmail(choice) {
965
+ return choice?.session?.user?.email || '';
966
+ }
967
+
968
+ function getDashboardAuthLabel(choice, loggedOut = false) {
969
+ if (loggedOut) {
970
+ return 'Signed out';
971
+ }
972
+ if (choice?.type === 'pin') {
973
+ return 'PIN approved';
974
+ }
975
+ const email = getChoiceEmail(choice);
976
+ return email ? `Google - ${email}` : 'Google signed in';
977
+ }
978
+
979
+ function renderConnectionDashboardPage(state = {}) {
980
+ const choice = state.choice || null;
981
+ const connectedAt = state.connectedAt || new Date().toISOString();
982
+ const authLabel = getDashboardAuthLabel(choice, state.loggedOut);
983
+ const manager = state.manager || (choice?.manager || '');
984
+ const slot = normalizeSlotNumber(state.slot) ? `Slot ${String(state.slot).padStart(3, '0')}` : 'First available';
985
+ const statusLabel = state.loggedOut
986
+ ? 'Signed out for next restart'
987
+ : manager
988
+ ? 'Client starting'
989
+ : 'Finding Hub';
990
+ const message = state.message || (manager
991
+ ? 'The LiveDesk client is running from the terminal. Keep this tab open as the local client dashboard.'
992
+ : 'Sign-in is complete. The client is looking for the active LiveDesk Hub and will start automatically.');
993
+ const stateJson = JSON.stringify({
994
+ authLabel,
995
+ connectedAt,
996
+ manager,
997
+ message,
998
+ slot,
999
+ startup: Boolean(state.startup),
1000
+ completed: true,
1001
+ statusLabel
1002
+ }).replaceAll('<', '\\u003c');
1003
+
1004
+ return `<!doctype html>
1005
+ <html lang="en">
1006
+ <head>
1007
+ <meta charset="utf-8">
1008
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1009
+ <title>LiveDesk Client Dashboard</title>
1010
+ <style>
1011
+ :root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
1012
+ * { box-sizing: border-box; }
1013
+ html, body { width: 100%; min-height: 100%; margin: 0; }
1014
+ body {
1015
+ min-height: 100vh;
1016
+ display: grid;
1017
+ place-items: center;
1018
+ padding: 28px;
1019
+ background:
1020
+ radial-gradient(circle at 78% 10%, rgba(203, 213, 225, 0.46), transparent 24%),
1021
+ linear-gradient(180deg, #f8fafc, #eef2f7);
1022
+ color: #111827;
1023
+ }
1024
+ main {
1025
+ width: min(920px, 100%);
1026
+ display: grid;
1027
+ grid-template-columns: minmax(280px, 0.88fr) minmax(320px, 1.12fr);
1028
+ overflow: hidden;
1029
+ border: 1px solid rgba(148, 163, 184, 0.28);
1030
+ border-radius: 18px;
1031
+ background: rgba(255, 255, 255, 0.92);
1032
+ box-shadow: 0 36px 110px rgba(15, 23, 42, 0.14);
1033
+ }
1034
+ .hero {
1035
+ display: grid;
1036
+ align-content: space-between;
1037
+ gap: 34px;
1038
+ padding: 34px;
1039
+ border-right: 1px solid rgba(148, 163, 184, 0.22);
1040
+ background:
1041
+ linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.86)),
1042
+ #ffffff;
1043
+ }
1044
+ .brand {
1045
+ display: flex;
1046
+ align-items: center;
1047
+ gap: 13px;
1048
+ }
1049
+ .mark {
1050
+ width: 40px;
1051
+ height: 40px;
1052
+ display: grid;
1053
+ place-items: center;
1054
+ border-radius: 10px;
1055
+ background: linear-gradient(135deg, #2dd4bf, #2563eb);
1056
+ color: #ffffff;
1057
+ font-size: 13px;
1058
+ font-weight: 800;
1059
+ box-shadow: 0 14px 30px rgba(37, 99, 235, 0.18);
1060
+ }
1061
+ .brand-copy { display: grid; gap: 2px; line-height: 1; }
1062
+ .brand-copy strong { font-size: 15px; font-weight: 700; }
1063
+ .brand-copy span { color: #64748b; font-size: 12px; font-weight: 500; }
1064
+ .headline { display: grid; gap: 10px; }
1065
+ .eyebrow {
1066
+ color: #475569;
1067
+ font-size: 11px;
1068
+ font-weight: 800;
1069
+ letter-spacing: 0.08em;
1070
+ text-transform: uppercase;
1071
+ }
1072
+ h1 {
1073
+ margin: 0;
1074
+ font-size: 34px;
1075
+ line-height: 1.06;
1076
+ font-weight: 680;
1077
+ letter-spacing: 0;
1078
+ }
1079
+ p {
1080
+ margin: 0;
1081
+ color: #64748b;
1082
+ font-size: 14px;
1083
+ line-height: 1.62;
1084
+ font-weight: 450;
1085
+ }
1086
+ .panel {
1087
+ display: grid;
1088
+ gap: 16px;
1089
+ padding: 34px;
1090
+ }
1091
+ .status-card {
1092
+ display: grid;
1093
+ gap: 14px;
1094
+ padding: 18px;
1095
+ border: 1px solid rgba(148, 163, 184, 0.24);
1096
+ border-radius: 14px;
1097
+ background: #ffffff;
1098
+ box-shadow:
1099
+ 0 20px 50px rgba(15, 23, 42, 0.09),
1100
+ 0 1px 0 rgba(255, 255, 255, 0.9) inset;
1101
+ }
1102
+ .status-head {
1103
+ display: flex;
1104
+ align-items: center;
1105
+ justify-content: space-between;
1106
+ gap: 12px;
1107
+ }
1108
+ .status-pill {
1109
+ min-height: 28px;
1110
+ display: inline-flex;
1111
+ align-items: center;
1112
+ gap: 7px;
1113
+ padding: 0 10px;
1114
+ border: 1px solid rgba(20, 184, 166, 0.25);
1115
+ border-radius: 999px;
1116
+ background: rgba(20, 184, 166, 0.08);
1117
+ color: #0f766e;
1118
+ font-size: 12px;
1119
+ font-weight: 760;
1120
+ }
1121
+ .status-pill::before {
1122
+ content: "";
1123
+ width: 8px;
1124
+ height: 8px;
1125
+ border-radius: 999px;
1126
+ background: #14b8a6;
1127
+ box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.12);
1128
+ }
1129
+ .status-card h2 {
1130
+ margin: 0;
1131
+ color: #111827;
1132
+ font-size: 20px;
1133
+ line-height: 1.18;
1134
+ }
1135
+ dl {
1136
+ display: grid;
1137
+ grid-template-columns: 118px minmax(0, 1fr);
1138
+ gap: 9px 14px;
1139
+ margin: 0;
1140
+ padding-top: 4px;
1141
+ }
1142
+ dt {
1143
+ color: #64748b;
1144
+ font-size: 12px;
1145
+ font-weight: 650;
1146
+ }
1147
+ dd {
1148
+ min-width: 0;
1149
+ margin: 0;
1150
+ overflow: hidden;
1151
+ color: #111827;
1152
+ font-size: 13px;
1153
+ font-weight: 680;
1154
+ text-overflow: ellipsis;
1155
+ white-space: nowrap;
1156
+ }
1157
+ .actions {
1158
+ display: grid;
1159
+ grid-template-columns: 1fr 1fr;
1160
+ gap: 10px;
1161
+ }
1162
+ button, a.button {
1163
+ min-height: 40px;
1164
+ display: inline-flex;
1165
+ align-items: center;
1166
+ justify-content: center;
1167
+ border: 1px solid #111827;
1168
+ border-radius: 9px;
1169
+ background: #111827;
1170
+ color: #ffffff;
1171
+ font-size: 13px;
1172
+ font-weight: 680;
1173
+ text-decoration: none;
1174
+ cursor: pointer;
1175
+ box-shadow: 0 12px 26px rgba(15, 23, 42, 0.13);
1176
+ }
1177
+ button.secondary, a.secondary {
1178
+ border-color: rgba(148, 163, 184, 0.36);
1179
+ background: #ffffff;
1180
+ color: #1f2937;
1181
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
1182
+ }
1183
+ .muted-card {
1184
+ padding: 14px 16px;
1185
+ border: 1px dashed rgba(148, 163, 184, 0.34);
1186
+ border-radius: 12px;
1187
+ background: rgba(248, 250, 252, 0.8);
1188
+ color: #64748b;
1189
+ font-size: 13px;
1190
+ line-height: 1.55;
1191
+ font-weight: 500;
1192
+ }
1193
+ @media (max-width: 760px) {
1194
+ body { padding: 14px; }
1195
+ main { grid-template-columns: 1fr; }
1196
+ .hero { border-right: 0; border-bottom: 1px solid rgba(148, 163, 184, 0.22); }
1197
+ .hero, .panel { padding: 24px; }
1198
+ h1 { font-size: 28px; }
1199
+ .actions { grid-template-columns: 1fr; }
1200
+ }
1201
+ </style>
1202
+ </head>
1203
+ <body>
1204
+ <main>
1205
+ <section class="hero">
1206
+ <div class="brand">
1207
+ <div class="mark">LD</div>
1208
+ <div class="brand-copy">
1209
+ <strong>LiveDesk</strong>
1210
+ <span>Client dashboard</span>
1211
+ </div>
1212
+ </div>
1213
+ <div class="headline">
1214
+ <span class="eyebrow">This computer</span>
1215
+ <h1>Client is ready</h1>
1216
+ <p id="message">${escapeHtml(message)}</p>
1217
+ </div>
1218
+ <p>Keep this page open if you want a local control surface for this workstation. Reboots can reconnect automatically when startup is enabled.</p>
1219
+ </section>
1220
+ <section class="panel">
1221
+ <article class="status-card">
1222
+ <div class="status-head">
1223
+ <h2>Connection</h2>
1224
+ <span id="status" class="status-pill">${escapeHtml(statusLabel)}</span>
1225
+ </div>
1226
+ <dl>
1227
+ <dt>Auth</dt><dd id="auth">${escapeHtml(authLabel)}</dd>
1228
+ <dt>Hub</dt><dd id="manager">${escapeHtml(manager || 'waiting')}</dd>
1229
+ <dt>Placement</dt><dd id="slot">${escapeHtml(slot)}</dd>
1230
+ <dt>Startup</dt><dd id="startup">${state.startup ? 'enabled' : 'manual'}</dd>
1231
+ <dt>Started</dt><dd id="connected-at">${escapeHtml(connectedAt)}</dd>
1232
+ </dl>
1233
+ </article>
1234
+ <div class="actions">
1235
+ <a class="button secondary" href="/">Refresh dashboard</a>
1236
+ <form method="post" action="/logout">
1237
+ <button class="secondary" type="submit">Logout</button>
1238
+ </form>
1239
+ </div>
1240
+ <div class="muted-card">The terminal process starts the screen stream automatically after auth. This dashboard stays available for logout and future workstation controls.</div>
1241
+ </section>
1242
+ </main>
1243
+ <script>
1244
+ const initialState = ${stateJson};
1245
+ function setText(id, value) {
1246
+ const node = document.getElementById(id);
1247
+ if (node) node.textContent = value || '';
1248
+ }
1249
+ function applyState(state) {
1250
+ setText('status', state.statusLabel || initialState.statusLabel);
1251
+ setText('auth', state.authLabel || initialState.authLabel);
1252
+ setText('manager', state.manager || 'waiting');
1253
+ setText('slot', state.slot || initialState.slot);
1254
+ setText('startup', state.startup ? 'enabled' : 'manual');
1255
+ setText('connected-at', state.connectedAt || initialState.connectedAt);
1256
+ setText('message', state.message || initialState.message);
1257
+ }
1258
+ applyState(initialState);
1259
+ if (location.pathname === '/callback') {
1260
+ history.replaceState(null, '', '/');
1261
+ }
1262
+ async function refreshState() {
1263
+ try {
1264
+ const response = await fetch('/api/state', { cache: 'no-store' });
1265
+ if (!response.ok) return;
1266
+ applyState(await response.json());
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ setInterval(refreshState, 1000);
1271
+ refreshState();
943
1272
  </script>
944
1273
  </body>
945
1274
  </html>`;
@@ -950,6 +1279,32 @@ function normalizePairingPin(value) {
950
1279
  return /^\d{6}$/.test(pin) ? pin : '';
951
1280
  }
952
1281
 
1282
+ function readSavedPin() {
1283
+ try {
1284
+ const state = JSON.parse(readFileSync(CLIENT_PIN_PATH, 'utf8'));
1285
+ return normalizePairingPin(state?.pin);
1286
+ } catch {
1287
+ return '';
1288
+ }
1289
+ }
1290
+
1291
+ function writeSavedPin(pin) {
1292
+ const normalizedPin = normalizePairingPin(pin);
1293
+ if (!normalizedPin) {
1294
+ return false;
1295
+ }
1296
+ mkdirSync(dirname(CLIENT_PIN_PATH), { recursive: true });
1297
+ writeFileSync(CLIENT_PIN_PATH, JSON.stringify({
1298
+ pin: normalizedPin,
1299
+ updatedAt: new Date().toISOString()
1300
+ }, null, 2));
1301
+ return true;
1302
+ }
1303
+
1304
+ function clearSavedPin() {
1305
+ rmSync(CLIENT_PIN_PATH, { force: true });
1306
+ }
1307
+
953
1308
  function readRequestBody(req, maxBytes = 4096) {
954
1309
  return new Promise((resolve, reject) => {
955
1310
  let body = '';
@@ -1001,16 +1356,57 @@ async function resolveManagerFromPin(supabase, pin) {
1001
1356
  };
1002
1357
  }
1003
1358
 
1359
+ async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
1360
+ const intervalMs = Math.max(1000, Number(options.intervalMs || DISCOVERY_RETRY_MS));
1361
+ const intervalSeconds = Math.max(1, Math.round(intervalMs / 1000));
1362
+ const shouldStop = typeof options.shouldStop === 'function' ? options.shouldStop : () => false;
1363
+ let attempts = 0;
1364
+ let lastMessage = '';
1365
+ console.log(`Waiting for a LiveDesk Hub from saved PIN. This client will keep trying every ${intervalSeconds}s.`);
1366
+ while (true) {
1367
+ if (shouldStop()) {
1368
+ return null;
1369
+ }
1370
+ attempts += 1;
1371
+ try {
1372
+ return await resolveManagerFromPin(supabase, pin);
1373
+ } catch (err) {
1374
+ if (shouldStop()) {
1375
+ return null;
1376
+ }
1377
+ const message = formatDiscoveryError(err);
1378
+ if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
1379
+ const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
1380
+ console.log(`Still waiting for LiveDesk Hub by saved PIN${suffix}: ${message}`);
1381
+ lastMessage = message;
1382
+ }
1383
+ await sleep(intervalMs);
1384
+ }
1385
+ }
1386
+ }
1387
+
1004
1388
  async function startConnectionChoiceServer(supabase, options = {}) {
1005
1389
  const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
1006
1390
  const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
1007
1391
  const slot = normalizeSlotNumber(options.slot);
1008
1392
  const startupArgs = Array.isArray(options.startupArgs) ? options.startupArgs : [];
1393
+ const savedSession = options.savedSession?.access_token ? options.savedSession : null;
1394
+ const savedPin = normalizePairingPin(options.savedPin);
1009
1395
  let pendingStartup = isWindowsStartupRegistered();
1010
1396
  let listeningPort = port;
1011
1397
  let completed = false;
1012
1398
  let completedTitle = 'LiveDesk connection complete';
1013
- let completedMessage = 'You can close this tab and return to the terminal.';
1399
+ let completedMessage = 'The LiveDesk client is starting automatically.';
1400
+ const dashboardState = {
1401
+ choice: null,
1402
+ connectedAt: '',
1403
+ endpointCandidates: [],
1404
+ loggedOut: false,
1405
+ manager: '',
1406
+ message: completedMessage,
1407
+ slot,
1408
+ startup: pendingStartup
1409
+ };
1014
1410
  let settleChoice;
1015
1411
  let rejectChoice;
1016
1412
  const waitForChoice = new Promise((resolve, reject) => {
@@ -1022,8 +1418,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1022
1418
  completed = true;
1023
1419
  completedTitle = title;
1024
1420
  completedMessage = message;
1025
- settleChoice(choice);
1026
- setImmediate(() => server.close());
1421
+ dashboardState.choice = choice;
1422
+ dashboardState.connectedAt = new Date().toISOString();
1423
+ dashboardState.endpointCandidates = Array.isArray(choice?.endpointCandidates) ? choice.endpointCandidates : [];
1424
+ dashboardState.loggedOut = false;
1425
+ dashboardState.manager = choice?.manager || '';
1426
+ dashboardState.message = message;
1427
+ dashboardState.startup = pendingStartup;
1428
+ if (settleChoice) {
1429
+ const resolveChoice = settleChoice;
1430
+ settleChoice = null;
1431
+ resolveChoice(choice);
1432
+ }
1027
1433
  };
1028
1434
  const renderChoice = (props = {}) => renderConnectionChoicePage({
1029
1435
  slot,
@@ -1031,6 +1437,63 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1031
1437
  startupSupported: isWindowsStartupSupported(),
1032
1438
  ...props
1033
1439
  });
1440
+ const renderDashboard = (props = {}) => renderConnectionDashboardPage({
1441
+ ...dashboardState,
1442
+ slot,
1443
+ startup: pendingStartup,
1444
+ ...props
1445
+ });
1446
+ const getDashboardApiState = () => {
1447
+ const manager = dashboardState.manager || dashboardState.choice?.manager || '';
1448
+ return {
1449
+ authLabel: getDashboardAuthLabel(dashboardState.choice, dashboardState.loggedOut),
1450
+ connectedAt: dashboardState.connectedAt || '',
1451
+ manager,
1452
+ message: dashboardState.message || completedMessage,
1453
+ slot: normalizeSlotNumber(slot) ? `Slot ${String(slot).padStart(3, '0')}` : 'First available',
1454
+ startup: Boolean(pendingStartup),
1455
+ completed,
1456
+ statusLabel: dashboardState.loggedOut
1457
+ ? 'Signed out for next restart'
1458
+ : manager
1459
+ ? 'Client starting'
1460
+ : completed
1461
+ ? 'Finding Hub'
1462
+ : 'Waiting for auth'
1463
+ };
1464
+ };
1465
+
1466
+ const startSavedConnection = async () => {
1467
+ if (completed || dashboardState.loggedOut) {
1468
+ return;
1469
+ }
1470
+ if (savedSession?.access_token) {
1471
+ applyStartupPreference(pendingStartup, startupArgs);
1472
+ complete({ type: 'google', session: savedSession }, 'LiveDesk client dashboard', 'Saved Google sign-in found. The client is starting automatically.');
1473
+ return;
1474
+ }
1475
+ if (!savedPin) {
1476
+ return;
1477
+ }
1478
+ dashboardState.message = 'Saved LiveDesk PIN found. Waiting for the Hub to become reachable.';
1479
+ try {
1480
+ const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
1481
+ shouldStop: () => completed || dashboardState.loggedOut
1482
+ });
1483
+ if (!resolved) {
1484
+ return;
1485
+ }
1486
+ if (completed || dashboardState.loggedOut) {
1487
+ return;
1488
+ }
1489
+ applyStartupPreference(pendingStartup, startupArgs);
1490
+ complete({ type: 'pin', ...resolved }, 'LiveDesk client dashboard', 'Saved PIN accepted. The client is starting automatically.');
1491
+ } catch (err) {
1492
+ if (!completed) {
1493
+ dashboardState.message = `Saved PIN did not connect: ${formatDiscoveryError(err)}`;
1494
+ }
1495
+ }
1496
+ };
1034
1497
 
1035
1498
  const handleError = (res, error, pin = '') => {
1036
1499
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -1048,15 +1511,45 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1048
1511
  res.end();
1049
1512
  return;
1050
1513
  }
1051
- if (completed) {
1514
+
1515
+ if (requestUrl.pathname === '/api/state') {
1516
+ res.writeHead(200, {
1517
+ 'Content-Type': 'application/json; charset=utf-8',
1518
+ 'Cache-Control': 'no-store'
1519
+ });
1520
+ res.end(JSON.stringify(getDashboardApiState()));
1521
+ return;
1522
+ }
1523
+
1524
+ if (requestUrl.pathname === '/logout') {
1525
+ try {
1526
+ await supabase.auth.signOut();
1527
+ } catch {
1528
+ }
1529
+ rmSync(CLIENT_AUTH_PATH, { force: true });
1530
+ clearSavedPin();
1531
+ dashboardState.choice = null;
1532
+ dashboardState.loggedOut = true;
1533
+ dashboardState.manager = '';
1534
+ dashboardState.message = 'Saved Google sign-in was cleared. Restart this client to sign in again.';
1535
+ if (req.method !== 'GET') {
1536
+ res.writeHead(303, { Location: completed ? '/' : '/connect' });
1537
+ res.end();
1538
+ return;
1539
+ }
1052
1540
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1053
- res.end(renderOAuthCallbackPage({
1054
- title: completedTitle,
1055
- message: completedMessage
1541
+ res.end(completed ? renderDashboard() : renderChoice({
1542
+ error: 'Saved Google sign-in was cleared.'
1056
1543
  }));
1057
1544
  return;
1058
1545
  }
1059
1546
 
1547
+ if (completed) {
1548
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1549
+ res.end(renderDashboard());
1550
+ return;
1551
+ }
1552
+
1060
1553
  if (requestUrl.pathname === '/' || requestUrl.pathname === '/connect') {
1061
1554
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1062
1555
  res.end(renderChoice());
@@ -1068,12 +1561,10 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1068
1561
  const { data: existing } = await supabase.auth.getSession();
1069
1562
  if (existing?.session?.access_token) {
1070
1563
  applyStartupPreference(pendingStartup, startupArgs);
1564
+ const choice = { type: 'google', session: existing.session };
1565
+ complete(choice, 'LiveDesk client dashboard', 'Signed in. The client is starting automatically.');
1071
1566
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1072
- res.end(renderOAuthCallbackPage({
1073
- title: 'LiveDesk sign-in ready',
1074
- message: 'You can close this tab and return to the terminal.'
1075
- }));
1076
- complete({ type: 'google', session: existing.session }, 'LiveDesk sign-in ready');
1567
+ res.end(renderDashboard());
1077
1568
  return;
1078
1569
  }
1079
1570
 
@@ -1110,13 +1601,12 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1110
1601
  pin = normalizePairingPin(pin);
1111
1602
  try {
1112
1603
  const resolved = await resolveManagerFromPin(supabase, pin);
1604
+ writeSavedPin(pin);
1113
1605
  applyStartupPreference(pendingStartup, startupArgs);
1606
+ const choice = { type: 'pin', ...resolved };
1607
+ complete(choice, 'LiveDesk client dashboard', 'PIN accepted. The client is starting automatically.');
1114
1608
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1115
- res.end(renderOAuthCallbackPage({
1116
- title: 'LiveDesk PIN accepted',
1117
- message: 'You can close this tab and return to the terminal.'
1118
- }));
1119
- complete({ type: 'pin', ...resolved }, 'LiveDesk PIN accepted');
1609
+ res.end(renderDashboard());
1120
1610
  } catch (err) {
1121
1611
  handleError(res, err, pin);
1122
1612
  }
@@ -1134,7 +1624,9 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1134
1624
  tone: 'error'
1135
1625
  }));
1136
1626
  completed = true;
1137
- rejectChoice(new Error(error));
1627
+ if (rejectChoice) {
1628
+ rejectChoice(new Error(error));
1629
+ }
1138
1630
  server.close();
1139
1631
  return;
1140
1632
  }
@@ -1155,17 +1647,17 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1155
1647
  tone: 'error'
1156
1648
  }));
1157
1649
  completed = true;
1158
- rejectChoice(message instanceof Error ? message : new Error(String(message)));
1650
+ if (rejectChoice) {
1651
+ rejectChoice(message instanceof Error ? message : new Error(String(message)));
1652
+ }
1159
1653
  server.close();
1160
1654
  return;
1161
1655
  }
1162
1656
  applyStartupPreference(pendingStartup, startupArgs);
1657
+ const choice = { type: 'google', session: sessionData.session };
1658
+ complete(choice, 'LiveDesk client dashboard', 'Signed in. The client is starting automatically.');
1163
1659
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1164
- res.end(renderOAuthCallbackPage({
1165
- title: 'LiveDesk sign-in complete',
1166
- message: 'You can close this tab and return to the terminal.'
1167
- }));
1168
- complete({ type: 'google', session: sessionData.session }, 'LiveDesk sign-in complete');
1660
+ res.end(renderDashboard());
1169
1661
  return;
1170
1662
  }
1171
1663
 
@@ -1184,7 +1676,9 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1184
1676
  }));
1185
1677
  if (!completed) {
1186
1678
  completed = true;
1187
- rejectChoice(err instanceof Error ? err : new Error(String(err)));
1679
+ if (rejectChoice) {
1680
+ rejectChoice(err instanceof Error ? err : new Error(String(err)));
1681
+ }
1188
1682
  server.close();
1189
1683
  }
1190
1684
  });
@@ -1196,6 +1690,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1196
1690
  });
1197
1691
  const address = server.address();
1198
1692
  listeningPort = typeof address === 'object' && address ? address.port : port;
1693
+ if (savedSession?.access_token || savedPin) {
1694
+ setImmediate(() => {
1695
+ void startSavedConnection();
1696
+ });
1697
+ }
1199
1698
  } catch (err) {
1200
1699
  if (err?.code === 'EADDRINUSE') {
1201
1700
  throw new Error(`LiveDesk connection page port ${port} is already in use. Close the app using it or run with --auth-port <port> and add that callback URL in Supabase Auth redirect URLs.`);
@@ -1206,7 +1705,16 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1206
1705
  get url() {
1207
1706
  return `http://${host}:${listeningPort}/`;
1208
1707
  },
1209
- waitForChoice
1708
+ waitForChoice,
1709
+ close() {
1710
+ try {
1711
+ server.close();
1712
+ } catch {
1713
+ }
1714
+ },
1715
+ update(patch = {}) {
1716
+ Object.assign(dashboardState, patch);
1717
+ }
1210
1718
  };
1211
1719
  }
1212
1720
 
@@ -1218,7 +1726,8 @@ async function chooseClientConnection(supabase, options = {}) {
1218
1726
  });
1219
1727
  console.log('Opening LiveDesk connection page...');
1220
1728
  openBrowser(connectionPage.url);
1221
- return connectionPage.waitForChoice;
1729
+ const choice = await connectionPage.waitForChoice;
1730
+ return { ...choice, connectionPage };
1222
1731
  }
1223
1732
 
1224
1733
  function parseManagerEndpoint(value) {
@@ -1342,28 +1851,43 @@ async function prepareLoginConnection(parsed) {
1342
1851
  let forwarded = [...parsed.forwarded];
1343
1852
  let manager = parsed.manager;
1344
1853
  let pair = parsed.pair;
1854
+ let connectionPage = null;
1345
1855
 
1346
1856
  if (shouldLogin) {
1347
1857
  const supabase = await createSupabaseClient();
1348
1858
  const startupArgs = buildStartupClientArgs(parsed);
1349
1859
  let savedSession = null;
1350
- if (parsed.startupRun) {
1351
- try {
1352
- savedSession = await refreshSessionIfNeeded(supabase);
1353
- } catch (err) {
1354
- console.warn(`LiveDesk saved sign-in could not be refreshed: ${err?.message || err}`);
1355
- }
1860
+ try {
1861
+ savedSession = await refreshSessionIfNeeded(supabase);
1862
+ } catch (err) {
1863
+ console.warn(`LiveDesk saved sign-in could not be refreshed: ${err?.message || err}`);
1864
+ }
1865
+ const savedPin = readSavedPin();
1866
+ let choice = null;
1867
+ if (parsed.startupRun && savedSession?.access_token) {
1868
+ choice = { type: 'google', session: savedSession };
1869
+ } else if (parsed.startupRun && savedPin) {
1870
+ const resolved = await waitForManagerFromSavedPin(supabase, savedPin);
1871
+ choice = { type: 'pin', ...resolved };
1356
1872
  }
1357
- const choice = savedSession?.access_token
1358
- ? { type: 'google', session: savedSession }
1359
- : await chooseClientConnection(supabase, {
1873
+ if (!choice) {
1874
+ choice = await chooseClientConnection(supabase, {
1360
1875
  authPort: parsed.authPort,
1361
1876
  slot: parsed.slot,
1362
- startupArgs
1877
+ startupArgs,
1878
+ savedSession,
1879
+ savedPin
1363
1880
  });
1881
+ }
1882
+ connectionPage = choice.connectionPage || null;
1364
1883
  if (choice.type === 'pin') {
1365
1884
  manager = choice.manager;
1366
1885
  pair = choice.pair;
1886
+ connectionPage?.update({
1887
+ endpointCandidates: choice.endpointCandidates || [],
1888
+ manager,
1889
+ message: `Connected to LiveDesk Hub at ${manager}.`
1890
+ });
1367
1891
  console.log('Connected by LiveDesk PIN.');
1368
1892
  } else {
1369
1893
  const session = choice.session;
@@ -1372,6 +1896,11 @@ async function prepareLoginConnection(parsed) {
1372
1896
  const resolved = await waitForManagerFromSupabase(supabase);
1373
1897
  manager = resolved.manager;
1374
1898
  pair = resolved.pair;
1899
+ connectionPage?.update({
1900
+ endpointCandidates: resolved.endpointCandidates || [],
1901
+ manager,
1902
+ message: `Connected to LiveDesk Hub at ${manager}.`
1903
+ });
1375
1904
  }
1376
1905
  console.log(`Found LiveDesk Hub at ${manager}.`);
1377
1906
  }
@@ -1394,6 +1923,7 @@ async function prepareLoginConnection(parsed) {
1394
1923
  manager,
1395
1924
  pair,
1396
1925
  slot,
1926
+ connectionPage,
1397
1927
  forwarded,
1398
1928
  rediscoverOnInvalidPair: shouldLogin
1399
1929
  };
@@ -1674,6 +2204,7 @@ async function main() {
1674
2204
  result = await spawnAgent(fastLaunch.command, [...fastLaunch.argsPrefix, ...fastArgs], buildFastEnvironment());
1675
2205
  } else if (prepared.engine === 'fast') {
1676
2206
  console.error(`C# RemoteFast is unavailable: ${fastLaunch.reason}. Use --engine node to run the legacy Node agent.`);
2207
+ prepared.connectionPage?.close?.();
1677
2208
  process.exit(2);
1678
2209
  } else {
1679
2210
  console.warn(`C# RemoteFast is unavailable (${fastLaunch.reason}). Falling back to the Node remote agent.`);
@@ -1682,6 +2213,7 @@ async function main() {
1682
2213
  } else {
1683
2214
  result = await spawnAgent(process.execPath, [nodeAgentPath, ...prepared.forwarded]);
1684
2215
  }
2216
+ prepared.connectionPage?.close?.();
1685
2217
  if (result?.signal) {
1686
2218
  process.kill(process.pid, result.signal);
1687
2219
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {