@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 +4 -0
- package/bin/livedesk-client.js +214 -10
- package/package.json +1 -1
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.
|
package/bin/livedesk-client.js
CHANGED
|
@@ -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
|
-
<
|
|
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({
|
|
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
|
|
1149
|
-
|
|
1150
|
-
|
|
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;
|