@livedesk/client 0.1.52 → 0.1.53

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.
package/README.md CHANGED
@@ -13,6 +13,10 @@ ready yet, the client keeps checking every 5 seconds instead of exiting. Omit th
13
13
  number for first-available placement, or pass `1` to `999` to pin this machine
14
14
  to a screen wall slot.
15
15
 
16
+ On Windows, check **Start with Windows** on the connection page to reconnect this
17
+ client automatically after reboot. The startup entry reuses the saved Google
18
+ session and only opens the connection page again if sign-in is needed.
19
+
16
20
  On the Hub computer, open the LiveDesk dashboard and sign in first. The
17
21
  dashboard keeps its local Hub address and private pair token refreshed in the
18
22
  LiveDesk Supabase registry for the signed-in account.
@@ -20,6 +20,8 @@ const DEFAULT_AUTH_CALLBACK_PORT = 5198;
20
20
  const DISCOVERY_RETRY_MS = 5000;
21
21
  const EXIT_INVALID_PAIR_TOKEN = 23;
22
22
  const SESSION_REFRESH_SKEW_SECONDS = 60;
23
+ const WINDOWS_STARTUP_SCRIPT_NAME = 'LiveDesk Client.vbs';
24
+ const WINDOWS_STARTUP_LEGACY_CMD_NAME = 'LiveDesk Client.cmd';
23
25
  const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
24
26
  const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
25
27
  const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
@@ -64,6 +66,7 @@ Options:
64
66
 
65
67
  Auto uses C# RemoteFast when supported and falls back to Node for AI assist or
66
68
  when a packaged RemoteFast runtime is unavailable.
69
+ Enable Windows auto-start from the connection page when this client signs in.
67
70
  `.trimStart());
68
71
  }
69
72
 
@@ -106,6 +109,7 @@ function parseLauncherArgs(argv) {
106
109
  let command = 'connect';
107
110
  let nodeOnlyFeature = false;
108
111
  let fakeThumbnail = false;
112
+ let startupRun = false;
109
113
 
110
114
  for (let index = 0; index < argv.length; index += 1) {
111
115
  const arg = argv[index];
@@ -157,6 +161,10 @@ function parseLauncherArgs(argv) {
157
161
  index += 1;
158
162
  continue;
159
163
  }
164
+ if (arg === '--startup-run') {
165
+ startupRun = true;
166
+ continue;
167
+ }
160
168
  if (arg === '--manager') {
161
169
  manager = argv[index + 1] || manager;
162
170
  forwarded.push(arg, argv[index + 1]);
@@ -208,7 +216,8 @@ function parseLauncherArgs(argv) {
208
216
  slot,
209
217
  authPort,
210
218
  nodeOnlyFeature,
211
- fakeThumbnail
219
+ fakeThumbnail,
220
+ startupRun
212
221
  };
213
222
  }
214
223
 
@@ -249,6 +258,119 @@ function appendForwardedFlag(args, flag) {
249
258
  return [...args, flag];
250
259
  }
251
260
 
261
+ function getWindowsStartupDir() {
262
+ const appData = process.env.APPDATA || join(os.homedir(), 'AppData', 'Roaming');
263
+ return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
264
+ }
265
+
266
+ function getWindowsStartupScriptPath() {
267
+ return join(getWindowsStartupDir(), WINDOWS_STARTUP_SCRIPT_NAME);
268
+ }
269
+
270
+ function getLegacyWindowsStartupCommandPath() {
271
+ return join(getWindowsStartupDir(), WINDOWS_STARTUP_LEGACY_CMD_NAME);
272
+ }
273
+
274
+ function isWindowsStartupSupported() {
275
+ return os.platform() === 'win32';
276
+ }
277
+
278
+ function isWindowsStartupRegistered() {
279
+ return isWindowsStartupSupported() && existsSync(getWindowsStartupScriptPath());
280
+ }
281
+
282
+ function quoteCommandArg(value) {
283
+ const text = String(value ?? '');
284
+ if (/^[A-Za-z0-9@._:/=+-]+$/.test(text)) {
285
+ return text;
286
+ }
287
+ return `"${text.replaceAll('"', '\\"')}"`;
288
+ }
289
+
290
+ function escapeVbsString(value) {
291
+ return String(value ?? '').replaceAll('"', '""');
292
+ }
293
+
294
+ function buildStartupClientArgs(parsed) {
295
+ const args = [];
296
+ const slot = normalizeSlotNumber(parsed.slot);
297
+ if (slot) {
298
+ args.push(slot);
299
+ }
300
+ if (parsed.engine === 'fast') {
301
+ args.push('--fast');
302
+ } else if (parsed.engine === 'node') {
303
+ args.push('--node');
304
+ }
305
+
306
+ const forwarded = Array.isArray(parsed.forwarded) ? parsed.forwarded : [];
307
+ for (let index = 0; index < forwarded.length; index += 1) {
308
+ const arg = forwarded[index];
309
+ if (!arg || arg === 'connect') {
310
+ continue;
311
+ }
312
+ if (arg === '--slot' || arg === '--manager' || arg === '--pair' || arg === '--auth-port') {
313
+ index += 1;
314
+ continue;
315
+ }
316
+ if (arg === '--login' || arg === '--logout' || arg === '--no-login' || arg === '--startup-run') {
317
+ continue;
318
+ }
319
+ args.push(arg);
320
+ }
321
+ args.push('--startup-run');
322
+ return args;
323
+ }
324
+
325
+ function registerWindowsStartup(startupArgs = []) {
326
+ if (!isWindowsStartupSupported()) {
327
+ return { changed: false, supported: false, path: '' };
328
+ }
329
+ const startupDir = getWindowsStartupDir();
330
+ const scriptPath = getWindowsStartupScriptPath();
331
+ const clientCommand = ['npx', '-y', '--prefer-online', 'livedesk@latest', 'client', ...startupArgs]
332
+ .filter(Boolean)
333
+ .map(quoteCommandArg)
334
+ .join(' ');
335
+ const runCommand = `cmd.exe /d /s /c "${clientCommand}"`;
336
+ const script = [
337
+ 'Set shell = CreateObject("WScript.Shell")',
338
+ `shell.CurrentDirectory = "${escapeVbsString(os.homedir())}"`,
339
+ `shell.Run "${escapeVbsString(runCommand)}", 0, False`,
340
+ ''
341
+ ].join('\r\n');
342
+
343
+ mkdirSync(startupDir, { recursive: true });
344
+ writeFileSync(scriptPath, script, 'utf8');
345
+ rmSync(getLegacyWindowsStartupCommandPath(), { force: true });
346
+ return { changed: true, supported: true, path: scriptPath };
347
+ }
348
+
349
+ function unregisterWindowsStartup() {
350
+ if (!isWindowsStartupSupported()) {
351
+ return { changed: false, supported: false, path: '' };
352
+ }
353
+ const scriptPath = getWindowsStartupScriptPath();
354
+ const existed = existsSync(scriptPath) || existsSync(getLegacyWindowsStartupCommandPath());
355
+ rmSync(scriptPath, { force: true });
356
+ rmSync(getLegacyWindowsStartupCommandPath(), { force: true });
357
+ return { changed: existed, supported: true, path: scriptPath };
358
+ }
359
+
360
+ function normalizeStartupChoice(value, fallback = false) {
361
+ if (value === null || value === undefined) {
362
+ return Boolean(fallback);
363
+ }
364
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
365
+ }
366
+
367
+ function applyStartupPreference(enabled, startupArgs = []) {
368
+ if (!isWindowsStartupSupported()) {
369
+ return { changed: false, supported: false, path: '' };
370
+ }
371
+ return enabled ? registerWindowsStartup(startupArgs) : unregisterWindowsStartup();
372
+ }
373
+
252
374
  function createFileStorage(filePath) {
253
375
  function readState() {
254
376
  try {
@@ -428,12 +550,21 @@ function renderOAuthCallbackPage({ title, message, tone = 'neutral' }) {
428
550
  </html>`;
429
551
  }
430
552
 
431
- function renderConnectionChoicePage({ error = '', pin = '', slot = '' } = {}) {
553
+ function renderConnectionChoicePage({ error = '', pin = '', slot = '', startup = false, startupSupported = false } = {}) {
432
554
  const errorBlock = error
433
555
  ? `<div class="error">${escapeHtml(error)}</div>`
434
556
  : '';
435
557
  const normalizedSlot = normalizeSlotNumber(slot);
436
558
  const slotLabel = normalizedSlot ? `Slot ${String(normalizedSlot).padStart(3, '0')}` : 'First available';
559
+ const startupBlock = startupSupported
560
+ ? `<label class="startup-option">
561
+ <input id="startup-toggle" type="checkbox" ${startup ? 'checked' : ''}>
562
+ <span>
563
+ <strong>Start with Windows</strong>
564
+ <small>Reconnect automatically after reboot.</small>
565
+ </span>
566
+ </label>`
567
+ : '';
437
568
  return `<!doctype html>
438
569
  <html lang="en">
439
570
  <head>
@@ -539,6 +670,39 @@ function renderConnectionChoicePage({ error = '', pin = '', slot = '' } = {}) {
539
670
  font-size: 13px;
540
671
  font-weight: 900;
541
672
  }
673
+ .startup-option {
674
+ display: grid;
675
+ grid-template-columns: 18px 1fr;
676
+ gap: 10px;
677
+ align-items: center;
678
+ padding: 11px 12px;
679
+ border: 1px solid #d9dee7;
680
+ border-radius: 8px;
681
+ background: #ffffff;
682
+ color: #111827;
683
+ cursor: pointer;
684
+ user-select: none;
685
+ }
686
+ .startup-option input {
687
+ width: 16px;
688
+ height: 16px;
689
+ margin: 0;
690
+ accent-color: #111827;
691
+ }
692
+ .startup-option span {
693
+ display: grid;
694
+ gap: 2px;
695
+ }
696
+ .startup-option strong {
697
+ font-size: 13px;
698
+ font-weight: 900;
699
+ }
700
+ .startup-option small {
701
+ color: #64748b;
702
+ text-align: left;
703
+ font-size: 12px;
704
+ font-weight: 700;
705
+ }
542
706
  form {
543
707
  display: grid;
544
708
  gap: 12px;
@@ -723,15 +887,20 @@ function renderConnectionChoicePage({ error = '', pin = '', slot = '' } = {}) {
723
887
  </div>
724
888
  ${errorBlock}
725
889
  <section class="panel">
726
- <a class="google" href="/google">Continue with Google</a>
890
+ <form id="google-form" method="get" action="/google">
891
+ <input id="google-startup" type="hidden" name="startup" value="${startup ? '1' : '0'}">
892
+ <button class="google" type="submit">Continue with Google</button>
893
+ </form>
727
894
  <div class="divider">or</div>
728
- <form method="post" action="/pin">
895
+ <form id="pin-form" method="post" action="/pin">
896
+ <input id="pin-startup" type="hidden" name="startup" value="${startup ? '1' : '0'}">
729
897
  <label>
730
898
  Hub PIN
731
899
  <input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
732
900
  </label>
733
901
  <button type="submit">Connect with PIN</button>
734
902
  </form>
903
+ ${startupBlock}
735
904
  <div class="placement">
736
905
  <span>Placement</span>
737
906
  <strong>${escapeHtml(slotLabel)}</strong>
@@ -756,6 +925,21 @@ function renderConnectionChoicePage({ error = '', pin = '', slot = '' } = {}) {
756
925
  </div>
757
926
  </aside>
758
927
  </div>
928
+ <script>
929
+ const startupToggle = document.getElementById('startup-toggle');
930
+ const startupFields = [
931
+ document.getElementById('google-startup'),
932
+ document.getElementById('pin-startup')
933
+ ].filter(Boolean);
934
+ function syncStartupFields() {
935
+ const value = startupToggle && startupToggle.checked ? '1' : '0';
936
+ startupFields.forEach(field => { field.value = value; });
937
+ }
938
+ if (startupToggle) {
939
+ startupToggle.addEventListener('change', syncStartupFields);
940
+ syncStartupFields();
941
+ }
942
+ </script>
759
943
  </body>
760
944
  </html>`;
761
945
  }
@@ -820,6 +1004,8 @@ async function startConnectionChoiceServer(supabase, options = {}) {
820
1004
  const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
821
1005
  const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
822
1006
  const slot = normalizeSlotNumber(options.slot);
1007
+ const startupArgs = Array.isArray(options.startupArgs) ? options.startupArgs : [];
1008
+ let pendingStartup = isWindowsStartupRegistered();
823
1009
  let listeningPort = port;
824
1010
  let completed = false;
825
1011
  let completedTitle = 'LiveDesk connection complete';
@@ -838,7 +1024,12 @@ async function startConnectionChoiceServer(supabase, options = {}) {
838
1024
  settleChoice(choice);
839
1025
  setImmediate(() => server.close());
840
1026
  };
841
- const renderChoice = (props = {}) => renderConnectionChoicePage({ slot, ...props });
1027
+ const renderChoice = (props = {}) => renderConnectionChoicePage({
1028
+ slot,
1029
+ startup: pendingStartup,
1030
+ startupSupported: isWindowsStartupSupported(),
1031
+ ...props
1032
+ });
842
1033
 
843
1034
  const handleError = (res, error, pin = '') => {
844
1035
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -872,8 +1063,10 @@ async function startConnectionChoiceServer(supabase, options = {}) {
872
1063
  }
873
1064
 
874
1065
  if (requestUrl.pathname === '/google') {
1066
+ pendingStartup = normalizeStartupChoice(requestUrl.searchParams.get('startup'), pendingStartup);
875
1067
  const { data: existing } = await supabase.auth.getSession();
876
1068
  if (existing?.session?.access_token) {
1069
+ applyStartupPreference(pendingStartup, startupArgs);
877
1070
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
878
1071
  res.end(renderOAuthCallbackPage({
879
1072
  title: 'LiveDesk sign-in ready',
@@ -905,14 +1098,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
905
1098
 
906
1099
  if (requestUrl.pathname === '/pin') {
907
1100
  let pin = requestUrl.searchParams.get('pin') || '';
1101
+ let startupValue = requestUrl.searchParams.get('startup');
908
1102
  if (req.method !== 'GET') {
909
1103
  const body = await readRequestBody(req);
910
1104
  const params = new URLSearchParams(body);
911
1105
  pin = params.get('pin') || pin;
1106
+ startupValue = params.get('startup');
912
1107
  }
1108
+ pendingStartup = normalizeStartupChoice(startupValue, pendingStartup);
913
1109
  pin = normalizePairingPin(pin);
914
1110
  try {
915
1111
  const resolved = await resolveManagerFromPin(supabase, pin);
1112
+ applyStartupPreference(pendingStartup, startupArgs);
916
1113
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
917
1114
  res.end(renderOAuthCallbackPage({
918
1115
  title: 'LiveDesk PIN accepted',
@@ -961,6 +1158,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
961
1158
  server.close();
962
1159
  return;
963
1160
  }
1161
+ applyStartupPreference(pendingStartup, startupArgs);
964
1162
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
965
1163
  res.end(renderOAuthCallbackPage({
966
1164
  title: 'LiveDesk sign-in complete',
@@ -1014,7 +1212,8 @@ async function startConnectionChoiceServer(supabase, options = {}) {
1014
1212
  async function chooseClientConnection(supabase, options = {}) {
1015
1213
  const connectionPage = await startConnectionChoiceServer(supabase, {
1016
1214
  authPort: options.authPort,
1017
- slot: options.slot
1215
+ slot: options.slot,
1216
+ startupArgs: options.startupArgs
1018
1217
  });
1019
1218
  console.log('Opening LiveDesk connection page...');
1020
1219
  openBrowser(connectionPage.url);
@@ -1145,10 +1344,15 @@ async function prepareLoginConnection(parsed) {
1145
1344
 
1146
1345
  if (shouldLogin) {
1147
1346
  const supabase = await createSupabaseClient();
1148
- const choice = await chooseClientConnection(supabase, {
1149
- authPort: parsed.authPort,
1150
- slot: parsed.slot
1151
- });
1347
+ const startupArgs = buildStartupClientArgs(parsed);
1348
+ const savedSession = parsed.startupRun ? await refreshSessionIfNeeded(supabase) : null;
1349
+ const choice = savedSession?.access_token
1350
+ ? { type: 'google', session: savedSession }
1351
+ : await chooseClientConnection(supabase, {
1352
+ authPort: parsed.authPort,
1353
+ slot: parsed.slot,
1354
+ startupArgs
1355
+ });
1152
1356
  if (choice.type === 'pin') {
1153
1357
  manager = choice.manager;
1154
1358
  pair = choice.pair;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {