@livedesk/client 0.1.51 → 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,10 +550,21 @@ function renderOAuthCallbackPage({ title, message, tone = 'neutral' }) {
428
550
  </html>`;
429
551
  }
430
552
 
431
- function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
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
  : '';
557
+ const normalizedSlot = normalizeSlotNumber(slot);
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
+ : '';
435
568
  return `<!doctype html>
436
569
  <html lang="en">
437
570
  <head>
@@ -443,48 +576,132 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
443
576
  * { box-sizing: border-box; }
444
577
  html, body { width: 100%; min-height: 100%; margin: 0; }
445
578
  body {
446
- display: grid;
447
- place-items: center;
448
- padding: 24px;
449
- background:
450
- radial-gradient(circle at 50% 0%, rgba(148, 163, 184, 0.22), transparent 34%),
451
- linear-gradient(180deg, #f8fafc, #eef2f7);
579
+ min-height: 100vh;
580
+ background: #f6f7f9;
452
581
  color: #111827;
453
582
  }
583
+ .shell {
584
+ min-height: 100vh;
585
+ display: grid;
586
+ grid-template-columns: minmax(360px, 462px) minmax(0, 1fr);
587
+ }
454
588
  main {
455
- width: min(440px, 100%);
589
+ display: flex;
590
+ flex-direction: column;
591
+ justify-content: center;
592
+ min-height: 100vh;
593
+ padding: 42px 54px;
594
+ border-right: 1px solid #d8dde5;
595
+ background: rgba(255, 255, 255, 0.96);
596
+ box-shadow: 18px 0 55px rgba(15, 23, 42, 0.08);
597
+ }
598
+ .stack {
599
+ width: min(100%, 386px);
456
600
  display: grid;
457
- gap: 18px;
458
- border: 1px solid rgba(148, 163, 184, 0.26);
459
- border-radius: 18px;
460
- padding: 28px;
461
- background: rgba(255, 255, 255, 0.92);
462
- box-shadow: 0 24px 70px rgba(15, 23, 42, 0.14);
601
+ gap: 24px;
602
+ }
603
+ .topbar {
604
+ position: absolute;
605
+ top: 28px;
606
+ left: 54px;
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 11px;
463
610
  }
464
611
  .brand {
465
612
  display: flex;
466
613
  align-items: center;
467
- gap: 12px;
614
+ gap: 13px;
468
615
  }
469
616
  .mark {
470
- width: 42px;
471
- height: 42px;
617
+ width: 38px;
618
+ height: 38px;
472
619
  display: grid;
473
620
  place-items: center;
474
- border-radius: 10px;
475
- background: #111827;
621
+ border-radius: 9px;
622
+ background: linear-gradient(135deg, #2dd4bf, #2563eb);
476
623
  color: #ffffff;
477
624
  font-weight: 950;
625
+ box-shadow: 0 12px 28px rgba(37, 99, 235, 0.18);
478
626
  }
479
- h1 { margin: 0; font-size: 28px; line-height: 1.05; letter-spacing: 0; }
480
- p { margin: 5px 0 0; color: #64748b; font-size: 14px; line-height: 1.45; font-weight: 650; }
627
+ .brand-copy {
628
+ display: grid;
629
+ gap: 2px;
630
+ line-height: 1;
631
+ }
632
+ .brand-copy strong {
633
+ font-size: 15px;
634
+ letter-spacing: 0;
635
+ }
636
+ .brand-copy span {
637
+ color: #64748b;
638
+ font-size: 12px;
639
+ font-weight: 750;
640
+ }
641
+ .headline {
642
+ display: grid;
643
+ gap: 9px;
644
+ padding-top: 16px;
645
+ }
646
+ h1 { margin: 0; font-size: 32px; line-height: 1.08; letter-spacing: 0; }
647
+ p { margin: 0; color: #64748b; font-size: 14px; line-height: 1.55; font-weight: 650; }
481
648
  .panel {
482
649
  display: grid;
483
- gap: 12px;
484
- border: 1px solid rgba(148, 163, 184, 0.22);
485
- border-radius: 14px;
486
- padding: 14px;
650
+ gap: 14px;
651
+ }
652
+ .placement {
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: space-between;
656
+ min-height: 38px;
657
+ padding: 0 12px;
658
+ border: 1px solid #d9dee7;
659
+ border-radius: 8px;
660
+ background: #ffffff;
661
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
662
+ }
663
+ .placement span {
664
+ color: #64748b;
665
+ font-size: 12px;
666
+ font-weight: 800;
667
+ }
668
+ .placement strong {
669
+ color: #111827;
670
+ font-size: 13px;
671
+ font-weight: 900;
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;
487
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;
488
705
  }
489
706
  form {
490
707
  display: grid;
@@ -501,12 +718,13 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
501
718
  letter-spacing: 0.04em;
502
719
  }
503
720
  input {
504
- height: 48px;
505
- border: 1px solid rgba(148, 163, 184, 0.45);
506
- border-radius: 12px;
507
- padding: 0 14px;
721
+ height: 42px;
722
+ border: 1px solid #cfd6e1;
723
+ border-radius: 8px;
724
+ padding: 0 12px;
725
+ background: rgba(17, 24, 39, 0.026);
508
726
  color: #111827;
509
- font-size: 24px;
727
+ font-size: 22px;
510
728
  font-weight: 950;
511
729
  letter-spacing: 0.18em;
512
730
  text-align: center;
@@ -517,25 +735,36 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
517
735
  box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
518
736
  }
519
737
  button, .google {
520
- height: 44px;
738
+ height: 42px;
521
739
  display: inline-flex;
522
740
  align-items: center;
523
741
  justify-content: center;
524
742
  gap: 8px;
525
- border: 0;
526
- border-radius: 12px;
743
+ border-radius: 8px;
527
744
  background: #111827;
528
745
  color: #ffffff;
529
746
  font-size: 14px;
530
747
  font-weight: 900;
531
748
  text-decoration: none;
532
749
  cursor: pointer;
750
+ transition: border-color 160ms ease, background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
751
+ }
752
+ button {
753
+ border: 1px solid #111827;
754
+ box-shadow: 0 10px 24px rgba(17, 24, 39, 0.12);
755
+ }
756
+ button:hover, .google:hover {
757
+ transform: translateY(-1px);
533
758
  }
534
759
  .google {
535
760
  background: #ffffff;
536
- border: 1px solid rgba(148, 163, 184, 0.35);
761
+ border: 1px solid #cfd6e1;
537
762
  color: #111827;
538
763
  }
764
+ .google:hover {
765
+ border-color: #111827;
766
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
767
+ }
539
768
  .divider {
540
769
  display: grid;
541
770
  grid-template-columns: 1fr auto 1fr;
@@ -550,11 +779,11 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
550
779
  .divider::before, .divider::after {
551
780
  content: "";
552
781
  height: 1px;
553
- background: rgba(148, 163, 184, 0.3);
782
+ background: #d9dee7;
554
783
  }
555
784
  .error {
556
- border: 1px solid rgba(220, 38, 38, 0.22);
557
- border-radius: 12px;
785
+ border: 1px solid rgba(220, 38, 38, 0.24);
786
+ border-radius: 8px;
558
787
  padding: 10px 12px;
559
788
  background: rgba(254, 226, 226, 0.75);
560
789
  color: #991b1b;
@@ -567,31 +796,150 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
567
796
  font-size: 12px;
568
797
  font-weight: 750;
569
798
  }
799
+ aside {
800
+ display: grid;
801
+ place-items: center;
802
+ min-height: 100vh;
803
+ padding: 56px;
804
+ background:
805
+ radial-gradient(circle at 58% 28%, rgba(203, 213, 225, 0.5), transparent 28%),
806
+ linear-gradient(180deg, #f8fafc, #eef2f7);
807
+ }
808
+ .preview {
809
+ width: min(560px, 90%);
810
+ display: grid;
811
+ gap: 14px;
812
+ padding: 18px;
813
+ border: 1px solid rgba(203, 213, 225, 0.76);
814
+ border-radius: 14px;
815
+ background: rgba(255, 255, 255, 0.76);
816
+ box-shadow: 0 28px 90px rgba(15, 23, 42, 0.13);
817
+ }
818
+ .preview-title {
819
+ display: flex;
820
+ align-items: center;
821
+ justify-content: space-between;
822
+ color: #475569;
823
+ font-size: 12px;
824
+ font-weight: 900;
825
+ text-transform: uppercase;
826
+ letter-spacing: 0.06em;
827
+ }
828
+ .preview-dot {
829
+ width: 9px;
830
+ height: 9px;
831
+ border-radius: 999px;
832
+ background: #14b8a6;
833
+ box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.12);
834
+ }
835
+ .screen-grid {
836
+ display: grid;
837
+ grid-template-columns: repeat(2, minmax(0, 1fr));
838
+ gap: 12px;
839
+ }
840
+ .screen {
841
+ aspect-ratio: 16 / 9;
842
+ border: 1px solid #d8dee8;
843
+ border-radius: 8px;
844
+ background:
845
+ linear-gradient(rgba(148, 163, 184, 0.11) 1px, transparent 1px),
846
+ linear-gradient(90deg, rgba(148, 163, 184, 0.11) 1px, transparent 1px),
847
+ #ffffff;
848
+ background-size: 32px 32px;
849
+ box-shadow: inset 0 -16px 26px rgba(15, 23, 42, 0.04);
850
+ }
851
+ .caption {
852
+ color: #64748b;
853
+ font-size: 13px;
854
+ font-weight: 700;
855
+ line-height: 1.55;
856
+ }
857
+ @media (max-width: 860px) {
858
+ .shell { grid-template-columns: 1fr; }
859
+ main {
860
+ min-height: 100vh;
861
+ padding: 84px 28px 36px;
862
+ border-right: 0;
863
+ }
864
+ .topbar {
865
+ left: 28px;
866
+ top: 24px;
867
+ }
868
+ .stack { justify-self: center; }
869
+ aside { display: none; }
870
+ }
570
871
  </style>
571
872
  </head>
572
873
  <body>
573
- <main>
574
- <div class="brand">
575
- <div class="mark">LD</div>
576
- <div>
577
- <h1>Connect LiveDesk</h1>
578
- <p>Use Google for your own Hub, or enter the 6-digit PIN shown on the screen wall.</p>
874
+ <div class="shell">
875
+ <main>
876
+ <div class="topbar">
877
+ <div class="mark">LD</div>
878
+ <div class="brand-copy">
879
+ <strong>LiveDesk</strong>
880
+ <span>Client connect</span>
881
+ </div>
579
882
  </div>
580
- </div>
581
- ${errorBlock}
582
- <section class="panel">
583
- <form method="post" action="/pin">
584
- <label>
585
- Pairing PIN
586
- <input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
587
- </label>
588
- <button type="submit">Connect with PIN</button>
589
- </form>
590
- </section>
591
- <div class="divider">or</div>
592
- <a class="google" href="/google">Continue with Google</a>
593
- <small>LiveDesk Client</small>
594
- </main>
883
+ <div class="stack">
884
+ <div class="headline">
885
+ <h1>Connect this computer</h1>
886
+ <p>Use the same Google account as your Hub, or enter the 6-digit Hub PIN from the screen wall.</p>
887
+ </div>
888
+ ${errorBlock}
889
+ <section class="panel">
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>
894
+ <div class="divider">or</div>
895
+ <form id="pin-form" method="post" action="/pin">
896
+ <input id="pin-startup" type="hidden" name="startup" value="${startup ? '1' : '0'}">
897
+ <label>
898
+ Hub PIN
899
+ <input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
900
+ </label>
901
+ <button type="submit">Connect with PIN</button>
902
+ </form>
903
+ ${startupBlock}
904
+ <div class="placement">
905
+ <span>Placement</span>
906
+ <strong>${escapeHtml(slotLabel)}</strong>
907
+ </div>
908
+ </section>
909
+ <small>This tab can stay open until the connection is ready.</small>
910
+ </div>
911
+ </main>
912
+ <aside aria-hidden="true">
913
+ <div class="preview">
914
+ <div class="preview-title">
915
+ <span>Screen wall ready</span>
916
+ <span class="preview-dot"></span>
917
+ </div>
918
+ <div class="screen-grid">
919
+ <div class="screen"></div>
920
+ <div class="screen"></div>
921
+ <div class="screen"></div>
922
+ <div class="screen"></div>
923
+ </div>
924
+ <div class="caption">LiveDesk places this computer on the Hub wall after sign-in or PIN approval.</div>
925
+ </div>
926
+ </aside>
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>
595
943
  </body>
596
944
  </html>`;
597
945
  }
@@ -655,6 +1003,9 @@ async function resolveManagerFromPin(supabase, pin) {
655
1003
  async function startConnectionChoiceServer(supabase, options = {}) {
656
1004
  const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
657
1005
  const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
1006
+ const slot = normalizeSlotNumber(options.slot);
1007
+ const startupArgs = Array.isArray(options.startupArgs) ? options.startupArgs : [];
1008
+ let pendingStartup = isWindowsStartupRegistered();
658
1009
  let listeningPort = port;
659
1010
  let completed = false;
660
1011
  let completedTitle = 'LiveDesk connection complete';
@@ -673,10 +1024,16 @@ async function startConnectionChoiceServer(supabase, options = {}) {
673
1024
  settleChoice(choice);
674
1025
  setImmediate(() => server.close());
675
1026
  };
1027
+ const renderChoice = (props = {}) => renderConnectionChoicePage({
1028
+ slot,
1029
+ startup: pendingStartup,
1030
+ startupSupported: isWindowsStartupSupported(),
1031
+ ...props
1032
+ });
676
1033
 
677
1034
  const handleError = (res, error, pin = '') => {
678
1035
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
679
- res.end(renderConnectionChoicePage({
1036
+ res.end(renderChoice({
680
1037
  error: error instanceof Error ? error.message : String(error),
681
1038
  pin
682
1039
  }));
@@ -701,13 +1058,15 @@ async function startConnectionChoiceServer(supabase, options = {}) {
701
1058
 
702
1059
  if (requestUrl.pathname === '/' || requestUrl.pathname === '/connect') {
703
1060
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
704
- res.end(renderConnectionChoicePage());
1061
+ res.end(renderChoice());
705
1062
  return;
706
1063
  }
707
1064
 
708
1065
  if (requestUrl.pathname === '/google') {
1066
+ pendingStartup = normalizeStartupChoice(requestUrl.searchParams.get('startup'), pendingStartup);
709
1067
  const { data: existing } = await supabase.auth.getSession();
710
1068
  if (existing?.session?.access_token) {
1069
+ applyStartupPreference(pendingStartup, startupArgs);
711
1070
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
712
1071
  res.end(renderOAuthCallbackPage({
713
1072
  title: 'LiveDesk sign-in ready',
@@ -739,14 +1098,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
739
1098
 
740
1099
  if (requestUrl.pathname === '/pin') {
741
1100
  let pin = requestUrl.searchParams.get('pin') || '';
1101
+ let startupValue = requestUrl.searchParams.get('startup');
742
1102
  if (req.method !== 'GET') {
743
1103
  const body = await readRequestBody(req);
744
1104
  const params = new URLSearchParams(body);
745
1105
  pin = params.get('pin') || pin;
1106
+ startupValue = params.get('startup');
746
1107
  }
1108
+ pendingStartup = normalizeStartupChoice(startupValue, pendingStartup);
747
1109
  pin = normalizePairingPin(pin);
748
1110
  try {
749
1111
  const resolved = await resolveManagerFromPin(supabase, pin);
1112
+ applyStartupPreference(pendingStartup, startupArgs);
750
1113
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
751
1114
  res.end(renderOAuthCallbackPage({
752
1115
  title: 'LiveDesk PIN accepted',
@@ -776,7 +1139,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
776
1139
  }
777
1140
  if (!code) {
778
1141
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
779
- res.end(renderConnectionChoicePage({
1142
+ res.end(renderChoice({
780
1143
  error: 'Google did not return an auth code. Try Google sign-in again.'
781
1144
  }));
782
1145
  return;
@@ -795,6 +1158,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
795
1158
  server.close();
796
1159
  return;
797
1160
  }
1161
+ applyStartupPreference(pendingStartup, startupArgs);
798
1162
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
799
1163
  res.end(renderOAuthCallbackPage({
800
1164
  title: 'LiveDesk sign-in complete',
@@ -805,7 +1169,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
805
1169
  }
806
1170
 
807
1171
  res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
808
- res.end(renderConnectionChoicePage({
1172
+ res.end(renderChoice({
809
1173
  error: 'Unknown LiveDesk connection route.'
810
1174
  }));
811
1175
  })().catch(err => {
@@ -846,7 +1210,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
846
1210
  }
847
1211
 
848
1212
  async function chooseClientConnection(supabase, options = {}) {
849
- const connectionPage = await startConnectionChoiceServer(supabase, { authPort: options.authPort });
1213
+ const connectionPage = await startConnectionChoiceServer(supabase, {
1214
+ authPort: options.authPort,
1215
+ slot: options.slot,
1216
+ startupArgs: options.startupArgs
1217
+ });
850
1218
  console.log('Opening LiveDesk connection page...');
851
1219
  openBrowser(connectionPage.url);
852
1220
  return connectionPage.waitForChoice;
@@ -976,7 +1344,15 @@ async function prepareLoginConnection(parsed) {
976
1344
 
977
1345
  if (shouldLogin) {
978
1346
  const supabase = await createSupabaseClient();
979
- const choice = await chooseClientConnection(supabase, { authPort: parsed.authPort });
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
+ });
980
1356
  if (choice.type === 'pin') {
981
1357
  manager = choice.manager;
982
1358
  pair = choice.pair;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {