@livedesk/client 0.1.0 → 0.1.1
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 +18 -9
- package/bin/livedesk-client-node.js +19 -4
- package/bin/livedesk-client.js +402 -28
- package/fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/fast/osx-arm64/mindexec-remote-fast.pdb +0 -0
- package/fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/fast/osx-x64/mindexec-remote-fast.pdb +0 -0
- package/fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/fast/win-x64/mindexec-remote-fast.pdb +0 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -3,18 +3,26 @@
|
|
|
3
3
|
Run a direct LiveDesk remote client from a controlled computer.
|
|
4
4
|
|
|
5
5
|
```powershell
|
|
6
|
-
npx @livedesk/client connect
|
|
6
|
+
npx @livedesk/client connect
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
and
|
|
9
|
+
The default flow opens Google sign-in, finds the active LiveDesk manager
|
|
10
|
+
published by the same account, and asks for a computer number to place this
|
|
11
|
+
machine on the screen wall.
|
|
12
|
+
|
|
13
|
+
On the manager computer, open the LiveDesk dashboard and sign in first. The
|
|
14
|
+
dashboard keeps its local hub address and private pair token refreshed in the
|
|
15
|
+
LiveDesk Supabase registry for the signed-in account.
|
|
16
|
+
|
|
17
|
+
For manual LAN fallback, start `@livedesk/hub` with an externally reachable
|
|
18
|
+
RemoteHub host and pass the address/token explicitly:
|
|
11
19
|
|
|
12
20
|
```powershell
|
|
13
21
|
$env:REMOTE_HUB_HOST="0.0.0.0"
|
|
14
22
|
$env:REMOTE_HUB_PORT="5197"
|
|
15
23
|
npm run dev:hub
|
|
16
24
|
|
|
17
|
-
npx @livedesk/client connect --manager 192.168.0.10:5197 --pair <strong-token>
|
|
25
|
+
npx @livedesk/client connect --no-login --manager 192.168.0.10:5197 --pair <strong-token> --slot 2
|
|
18
26
|
```
|
|
19
27
|
|
|
20
28
|
The client registers the device, sends status heartbeats, can return
|
|
@@ -28,9 +36,10 @@ is unavailable.
|
|
|
28
36
|
Useful flags:
|
|
29
37
|
|
|
30
38
|
```powershell
|
|
31
|
-
npx @livedesk/client connect --
|
|
32
|
-
npx @livedesk/client connect --
|
|
33
|
-
npx @livedesk/client connect --
|
|
34
|
-
npx @livedesk/client connect --
|
|
35
|
-
npx @livedesk/client connect --
|
|
39
|
+
npx @livedesk/client connect --slot 3
|
|
40
|
+
npx @livedesk/client connect --no-thumbnail
|
|
41
|
+
npx @livedesk/client connect --no-live
|
|
42
|
+
npx @livedesk/client connect --engine node
|
|
43
|
+
npx @livedesk/client connect --engine fast --trace-frames
|
|
44
|
+
npx @livedesk/client connect --logout
|
|
36
45
|
```
|
|
@@ -25,6 +25,7 @@ Usage:
|
|
|
25
25
|
Options:
|
|
26
26
|
--manager <host:port> RemoteHub address. Default: ${DEFAULT_MANAGER}
|
|
27
27
|
--pair <token> RemoteHub pairing token. Can also use LIVEDESK_CLIENT_PAIR_TOKEN.
|
|
28
|
+
--slot <number> Screen wall slot number for this computer.
|
|
28
29
|
--name <name> Friendly device name. Default: OS hostname.
|
|
29
30
|
--heartbeat <ms> Status heartbeat interval. Default: ${DEFAULT_HEARTBEAT_MS}
|
|
30
31
|
--device-id <id> Stable device id. Default: generated and saved per OS user.
|
|
@@ -60,6 +61,7 @@ function parseArgs(argv) {
|
|
|
60
61
|
command: 'connect',
|
|
61
62
|
manager: process.env.LIVEDESK_CLIENT_MANAGER || process.env.MINDEXEC_REMOTE_MANAGER || DEFAULT_MANAGER,
|
|
62
63
|
pair: process.env.LIVEDESK_CLIENT_PAIR_TOKEN || process.env.MINDEXEC_REMOTE_PAIR_TOKEN || '',
|
|
64
|
+
slotNumber: normalizeSlotNumber(process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT),
|
|
63
65
|
name: process.env.LIVEDESK_CLIENT_NAME || process.env.MINDEXEC_REMOTE_NAME || os.hostname(),
|
|
64
66
|
heartbeatMs: DEFAULT_HEARTBEAT_MS,
|
|
65
67
|
deviceId: '',
|
|
@@ -91,6 +93,9 @@ function parseArgs(argv) {
|
|
|
91
93
|
case '--pair':
|
|
92
94
|
result.pair = args[++index] || '';
|
|
93
95
|
break;
|
|
96
|
+
case '--slot':
|
|
97
|
+
result.slotNumber = normalizeSlotNumber(args[++index] || result.slotNumber);
|
|
98
|
+
break;
|
|
94
99
|
case '--name':
|
|
95
100
|
result.name = args[++index] || result.name;
|
|
96
101
|
break;
|
|
@@ -186,6 +191,11 @@ function normalizeDeviceId(value) {
|
|
|
186
191
|
return String(value || '').trim().replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
|
|
187
192
|
}
|
|
188
193
|
|
|
194
|
+
function normalizeSlotNumber(value) {
|
|
195
|
+
const number = Number(String(value || '').trim());
|
|
196
|
+
return Number.isInteger(number) && number >= 1 && number <= 99 ? number : 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
189
199
|
async function getDeviceId(explicitDeviceId = '') {
|
|
190
200
|
const explicit = normalizeDeviceId(explicitDeviceId);
|
|
191
201
|
if (explicit) {
|
|
@@ -213,10 +223,10 @@ async function getDeviceId(explicitDeviceId = '') {
|
|
|
213
223
|
return deviceId;
|
|
214
224
|
}
|
|
215
225
|
|
|
216
|
-
function getStatus() {
|
|
226
|
+
function getStatus(options = {}) {
|
|
217
227
|
const totalMem = os.totalmem();
|
|
218
228
|
const freeMem = os.freemem();
|
|
219
|
-
|
|
229
|
+
const status = {
|
|
220
230
|
uptimeSec: Math.round(os.uptime()),
|
|
221
231
|
loadavg: os.loadavg(),
|
|
222
232
|
totalMem,
|
|
@@ -226,6 +236,10 @@ function getStatus() {
|
|
|
226
236
|
release: os.release(),
|
|
227
237
|
timestamp: new Date().toISOString()
|
|
228
238
|
};
|
|
239
|
+
if (options.slotNumber) {
|
|
240
|
+
status.slotNumber = options.slotNumber;
|
|
241
|
+
}
|
|
242
|
+
return status;
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
function writeJsonLine(socket, payload) {
|
|
@@ -797,6 +811,7 @@ function connectOnce(options, deviceId) {
|
|
|
797
811
|
pairToken: options.pair,
|
|
798
812
|
deviceId,
|
|
799
813
|
deviceName: options.name,
|
|
814
|
+
slotNumber: options.slotNumber || undefined,
|
|
800
815
|
hostname: os.hostname(),
|
|
801
816
|
platform: os.platform(),
|
|
802
817
|
arch: os.arch(),
|
|
@@ -833,7 +848,7 @@ function connectOnce(options, deviceId) {
|
|
|
833
848
|
console.log(`Connected to RemoteHub as ${options.name} (${deviceId})`);
|
|
834
849
|
writeJsonLine(socket, {
|
|
835
850
|
type: 'status',
|
|
836
|
-
status: getStatus()
|
|
851
|
+
status: getStatus(options)
|
|
837
852
|
});
|
|
838
853
|
if (options.once) {
|
|
839
854
|
finish();
|
|
@@ -843,7 +858,7 @@ function connectOnce(options, deviceId) {
|
|
|
843
858
|
heartbeatTimer = setInterval(() => {
|
|
844
859
|
writeJsonLine(socket, {
|
|
845
860
|
type: 'status',
|
|
846
|
-
status: getStatus()
|
|
861
|
+
status: getStatus(options)
|
|
847
862
|
});
|
|
848
863
|
}, options.heartbeatMs);
|
|
849
864
|
} else if (message.type === 'command') {
|
package/bin/livedesk-client.js
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { chmodSync, existsSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { createServer } from 'node:http';
|
|
8
|
+
import net from 'node:net';
|
|
7
9
|
import os from 'node:os';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
11
|
|
|
9
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
13
|
const packageRoot = resolve(__dirname, '..');
|
|
11
14
|
const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
|
|
12
15
|
const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
|
|
16
|
+
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
17
|
+
const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
|
|
18
|
+
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
19
|
+
const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
|
|
20
|
+
const CLIENT_AUTH_PATH = join(CLIENT_STATE_DIR, 'auth.json');
|
|
21
|
+
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
13
22
|
|
|
14
23
|
function printHelp() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
process.stdout.write(`
|
|
25
|
+
LiveDesk Client
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
npx @livedesk/client connect
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
Default flow:
|
|
31
|
+
Opens Google sign-in, finds the signed-in manager, asks for this computer's
|
|
32
|
+
screen-wall number, then connects locally.
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--slot <number> Screen wall slot number for this computer.
|
|
36
|
+
--name <name> Friendly device name. Default: OS hostname.
|
|
22
37
|
--engine auto|fast|csharp|node Select agent engine. Default: auto.
|
|
23
38
|
--fast Force C# RemoteFast frame.binary engine.
|
|
24
39
|
--node Force the legacy Node agent.
|
|
25
40
|
--control Enable RemoteFast keyboard/mouse control.
|
|
26
41
|
--no-control Disable RemoteFast keyboard/mouse control.
|
|
27
|
-
--
|
|
42
|
+
--thumbnail Enable thumbnail capture when supported.
|
|
43
|
+
--no-thumbnail Disable thumbnail capture capability.
|
|
44
|
+
--live Enable focused live screen streaming.
|
|
45
|
+
--no-live Disable focused live screen streaming.
|
|
46
|
+
--tasks Enable safe remote task inbox.
|
|
47
|
+
--no-tasks Disable remote task dispatch capability.
|
|
48
|
+
--login Sign in with Google and auto-discover manager.
|
|
49
|
+
--logout Forget saved Google session before connecting.
|
|
50
|
+
--version Show the agent version.
|
|
51
|
+
--help Show this help.
|
|
52
|
+
|
|
53
|
+
Manual fallback:
|
|
54
|
+
npx @livedesk/client connect --no-login --manager 192.168.0.10:5197 --pair <token> --slot 2
|
|
28
55
|
|
|
29
56
|
Auto uses C# RemoteFast when supported and falls back to Node for AI assist or
|
|
30
57
|
when a packaged RemoteFast runtime is unavailable.
|
|
31
|
-
|
|
58
|
+
`.trimStart());
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
function normalizeEngine(value) {
|
|
@@ -46,11 +73,27 @@ function parseLauncherArgs(argv) {
|
|
|
46
73
|
const forwarded = [];
|
|
47
74
|
let engine = normalizeEngine(process.env.LIVEDESK_CLIENT_ENGINE || process.env.MINDEXEC_REMOTE_ENGINE);
|
|
48
75
|
let help = false;
|
|
76
|
+
let loginRequested = false;
|
|
77
|
+
let loginDisabled = false;
|
|
78
|
+
let logout = false;
|
|
79
|
+
let manager = process.env.LIVEDESK_CLIENT_MANAGER || process.env.MINDEXEC_REMOTE_MANAGER || '';
|
|
80
|
+
let pair = process.env.LIVEDESK_CLIENT_PAIR_TOKEN || process.env.MINDEXEC_REMOTE_PAIR_TOKEN || '';
|
|
81
|
+
let slot = process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT || '';
|
|
82
|
+
let command = 'connect';
|
|
49
83
|
let nodeOnlyFeature = false;
|
|
50
84
|
let fakeThumbnail = false;
|
|
51
85
|
|
|
86
|
+
const args = [...argv];
|
|
87
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
88
|
+
command = args[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
for (let index = 0; index < argv.length; index += 1) {
|
|
53
92
|
const arg = argv[index];
|
|
93
|
+
if (index === 0 && arg && !arg.startsWith('-')) {
|
|
94
|
+
forwarded.push(arg);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
54
97
|
if (arg === '--engine') {
|
|
55
98
|
engine = normalizeEngine(argv[index + 1]);
|
|
56
99
|
index += 1;
|
|
@@ -64,6 +107,36 @@ function parseLauncherArgs(argv) {
|
|
|
64
107
|
engine = 'node';
|
|
65
108
|
continue;
|
|
66
109
|
}
|
|
110
|
+
if (arg === '--login') {
|
|
111
|
+
loginRequested = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg === '--no-login') {
|
|
115
|
+
loginDisabled = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (arg === '--logout') {
|
|
119
|
+
logout = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (arg === '--manager') {
|
|
123
|
+
manager = argv[index + 1] || manager;
|
|
124
|
+
forwarded.push(arg, argv[index + 1]);
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg === '--pair') {
|
|
129
|
+
pair = argv[index + 1] || pair;
|
|
130
|
+
forwarded.push(arg, argv[index + 1]);
|
|
131
|
+
index += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (arg === '--slot') {
|
|
135
|
+
slot = argv[index + 1] || slot;
|
|
136
|
+
forwarded.push(arg, argv[index + 1]);
|
|
137
|
+
index += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
67
140
|
|
|
68
141
|
if (arg === '--help' || arg === '-h') {
|
|
69
142
|
help = true;
|
|
@@ -81,11 +154,304 @@ function parseLauncherArgs(argv) {
|
|
|
81
154
|
engine,
|
|
82
155
|
forwarded,
|
|
83
156
|
help,
|
|
157
|
+
command,
|
|
158
|
+
loginRequested,
|
|
159
|
+
loginDisabled,
|
|
160
|
+
logout,
|
|
161
|
+
manager,
|
|
162
|
+
pair,
|
|
163
|
+
slot,
|
|
84
164
|
nodeOnlyFeature,
|
|
85
165
|
fakeThumbnail
|
|
86
166
|
};
|
|
87
167
|
}
|
|
88
168
|
|
|
169
|
+
function normalizeSlotNumber(value) {
|
|
170
|
+
const number = Number(String(value || '').trim());
|
|
171
|
+
if (!Number.isInteger(number) || number < 1 || number > 99) {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
return String(number);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function upsertForwardedOption(args, flag, value) {
|
|
178
|
+
if (!value) {
|
|
179
|
+
return args;
|
|
180
|
+
}
|
|
181
|
+
const next = [...args];
|
|
182
|
+
const index = next.indexOf(flag);
|
|
183
|
+
if (index >= 0) {
|
|
184
|
+
next[index + 1] = value;
|
|
185
|
+
return next;
|
|
186
|
+
}
|
|
187
|
+
next.push(flag, value);
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createFileStorage(filePath) {
|
|
192
|
+
function readState() {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
195
|
+
} catch {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function writeState(state) {
|
|
201
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
202
|
+
writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
getItem(key) {
|
|
207
|
+
const state = readState();
|
|
208
|
+
return typeof state[key] === 'string' ? state[key] : null;
|
|
209
|
+
},
|
|
210
|
+
setItem(key, value) {
|
|
211
|
+
const state = readState();
|
|
212
|
+
state[key] = String(value);
|
|
213
|
+
writeState(state);
|
|
214
|
+
},
|
|
215
|
+
removeItem(key) {
|
|
216
|
+
const state = readState();
|
|
217
|
+
delete state[key];
|
|
218
|
+
writeState(state);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function createSupabaseClient() {
|
|
224
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
225
|
+
return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
226
|
+
auth: {
|
|
227
|
+
autoRefreshToken: false,
|
|
228
|
+
persistSession: true,
|
|
229
|
+
detectSessionInUrl: false,
|
|
230
|
+
flowType: 'pkce',
|
|
231
|
+
storageKey: CLIENT_AUTH_STORAGE_KEY,
|
|
232
|
+
storage: createFileStorage(CLIENT_AUTH_PATH)
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function openBrowser(url) {
|
|
238
|
+
if (os.platform() === 'win32') {
|
|
239
|
+
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const command = os.platform() === 'darwin' ? 'open' : 'xdg-open';
|
|
243
|
+
spawn(command, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function startOAuthCallbackServer() {
|
|
247
|
+
let settleCode;
|
|
248
|
+
let rejectCode;
|
|
249
|
+
const waitForCode = new Promise((resolve, reject) => {
|
|
250
|
+
settleCode = resolve;
|
|
251
|
+
rejectCode = reject;
|
|
252
|
+
});
|
|
253
|
+
const server = createServer((req, res) => {
|
|
254
|
+
const requestUrl = new URL(req.url || '/', `http://127.0.0.1:${server.address().port}`);
|
|
255
|
+
const code = requestUrl.searchParams.get('code');
|
|
256
|
+
const error = requestUrl.searchParams.get('error_description') || requestUrl.searchParams.get('error');
|
|
257
|
+
if (error) {
|
|
258
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
259
|
+
res.end('<h1>LiveDesk sign-in failed</h1><p>You can close this tab.</p>');
|
|
260
|
+
rejectCode(new Error(error));
|
|
261
|
+
server.close();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!code) {
|
|
265
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
266
|
+
res.end('LiveDesk sign-in callback is waiting for an auth code.');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
270
|
+
res.end('<h1>LiveDesk sign-in complete</h1><p>You can close this tab and return to the terminal.</p>');
|
|
271
|
+
settleCode(code);
|
|
272
|
+
server.close();
|
|
273
|
+
});
|
|
274
|
+
await new Promise((resolve, reject) => {
|
|
275
|
+
server.once('error', reject);
|
|
276
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
get redirectTo() {
|
|
280
|
+
return `http://127.0.0.1:${server.address().port}/callback`;
|
|
281
|
+
},
|
|
282
|
+
waitForCode
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function signInWithGoogle(supabase) {
|
|
287
|
+
const { data: existing } = await supabase.auth.getSession();
|
|
288
|
+
if (existing?.session?.access_token) {
|
|
289
|
+
return existing.session;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const callback = await startOAuthCallbackServer();
|
|
293
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
294
|
+
provider: 'google',
|
|
295
|
+
options: {
|
|
296
|
+
redirectTo: callback.redirectTo,
|
|
297
|
+
scopes: 'email profile'
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
if (error) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
if (!data?.url) {
|
|
304
|
+
throw new Error('Supabase did not return a Google sign-in URL.');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log('Opening Google sign-in for LiveDesk...');
|
|
308
|
+
openBrowser(data.url);
|
|
309
|
+
const code = await callback.waitForCode;
|
|
310
|
+
const { data: sessionData, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
|
|
311
|
+
if (exchangeError) {
|
|
312
|
+
throw exchangeError;
|
|
313
|
+
}
|
|
314
|
+
if (!sessionData?.session) {
|
|
315
|
+
throw new Error('Google sign-in did not return a LiveDesk session.');
|
|
316
|
+
}
|
|
317
|
+
return sessionData.session;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parseManagerEndpoint(value) {
|
|
321
|
+
let text = String(value || '').trim().replace(/^tcp:\/\//i, '');
|
|
322
|
+
const separator = text.lastIndexOf(':');
|
|
323
|
+
if (separator <= 0) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const host = text.slice(0, separator).replace(/^\[/, '').replace(/\]$/, '').trim();
|
|
327
|
+
const port = Number(text.slice(separator + 1));
|
|
328
|
+
if (!host || !Number.isFinite(port)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return { host, port };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function canConnectToEndpoint(endpoint, timeoutMs = 1200) {
|
|
335
|
+
const parsedEndpoint = parseManagerEndpoint(endpoint);
|
|
336
|
+
if (!parsedEndpoint) {
|
|
337
|
+
return Promise.resolve(false);
|
|
338
|
+
}
|
|
339
|
+
return new Promise(resolve => {
|
|
340
|
+
const socket = net.createConnection(parsedEndpoint);
|
|
341
|
+
const finish = ok => {
|
|
342
|
+
socket.removeAllListeners();
|
|
343
|
+
socket.destroy();
|
|
344
|
+
resolve(ok);
|
|
345
|
+
};
|
|
346
|
+
socket.setTimeout(timeoutMs);
|
|
347
|
+
socket.once('connect', () => finish(true));
|
|
348
|
+
socket.once('timeout', () => finish(false));
|
|
349
|
+
socket.once('error', () => finish(false));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function chooseReachableEndpoint(candidates) {
|
|
354
|
+
for (const endpoint of candidates) {
|
|
355
|
+
if (await canConnectToEndpoint(endpoint)) {
|
|
356
|
+
return endpoint;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return candidates[0] || DEFAULT_MANAGER;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function normalizeEndpointCandidates(target) {
|
|
363
|
+
const rawCandidates = [
|
|
364
|
+
target?.endpoint,
|
|
365
|
+
...(Array.isArray(target?.endpoint_candidates) ? target.endpoint_candidates : [])
|
|
366
|
+
];
|
|
367
|
+
return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function resolveManagerFromSupabase(supabase) {
|
|
371
|
+
const { data, error } = await supabase
|
|
372
|
+
.from('livedesk_remote_host_targets')
|
|
373
|
+
.select('endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
374
|
+
.eq('product_key', 'livedesk')
|
|
375
|
+
.maybeSingle();
|
|
376
|
+
if (error) {
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
if (!data?.active) {
|
|
380
|
+
throw new Error('No active LiveDesk manager was found for this Google account. Start LiveDesk on the manager computer and sign in there first.');
|
|
381
|
+
}
|
|
382
|
+
if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
|
|
383
|
+
throw new Error('The LiveDesk manager record is expired. Open the manager screen again while signed in.');
|
|
384
|
+
}
|
|
385
|
+
if (!data.pair_token) {
|
|
386
|
+
throw new Error('The LiveDesk manager record is missing its private connection token.');
|
|
387
|
+
}
|
|
388
|
+
const endpointCandidates = normalizeEndpointCandidates(data);
|
|
389
|
+
if (endpointCandidates.length === 0) {
|
|
390
|
+
throw new Error('The LiveDesk manager record does not contain a reachable local address.');
|
|
391
|
+
}
|
|
392
|
+
const manager = await chooseReachableEndpoint(endpointCandidates);
|
|
393
|
+
return {
|
|
394
|
+
manager,
|
|
395
|
+
pair: data.pair_token,
|
|
396
|
+
endpointCandidates
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function promptForSlotNumber(currentSlot) {
|
|
401
|
+
const normalized = normalizeSlotNumber(currentSlot);
|
|
402
|
+
if (normalized || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
403
|
+
return normalized;
|
|
404
|
+
}
|
|
405
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
406
|
+
try {
|
|
407
|
+
const answer = await rl.question('Computer number for screen wall (1-99, Enter to skip): ');
|
|
408
|
+
return normalizeSlotNumber(answer);
|
|
409
|
+
} finally {
|
|
410
|
+
rl.close();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function prepareLoginConnection(parsed) {
|
|
415
|
+
if (parsed.logout) {
|
|
416
|
+
rmSync(CLIENT_AUTH_PATH, { force: true });
|
|
417
|
+
}
|
|
418
|
+
const shouldLogin = !parsed.loginDisabled
|
|
419
|
+
&& parsed.command === 'connect'
|
|
420
|
+
&& (parsed.loginRequested || !parsed.pair);
|
|
421
|
+
let forwarded = [...parsed.forwarded];
|
|
422
|
+
let manager = parsed.manager;
|
|
423
|
+
let pair = parsed.pair;
|
|
424
|
+
|
|
425
|
+
if (shouldLogin) {
|
|
426
|
+
const supabase = await createSupabaseClient();
|
|
427
|
+
const session = await signInWithGoogle(supabase);
|
|
428
|
+
const email = session.user?.email ? ` as ${session.user.email}` : '';
|
|
429
|
+
console.log(`Signed in to LiveDesk${email}.`);
|
|
430
|
+
const resolved = await resolveManagerFromSupabase(supabase);
|
|
431
|
+
manager = resolved.manager;
|
|
432
|
+
pair = resolved.pair;
|
|
433
|
+
console.log(`Found LiveDesk manager at ${manager}.`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const slot = await promptForSlotNumber(parsed.slot);
|
|
437
|
+
if (manager) {
|
|
438
|
+
forwarded = upsertForwardedOption(forwarded, '--manager', manager);
|
|
439
|
+
}
|
|
440
|
+
if (pair) {
|
|
441
|
+
forwarded = upsertForwardedOption(forwarded, '--pair', pair);
|
|
442
|
+
}
|
|
443
|
+
if (slot) {
|
|
444
|
+
forwarded = upsertForwardedOption(forwarded, '--slot', slot);
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
...parsed,
|
|
448
|
+
manager,
|
|
449
|
+
pair,
|
|
450
|
+
slot,
|
|
451
|
+
forwarded
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
89
455
|
function getFastRuntime() {
|
|
90
456
|
const platform = os.platform();
|
|
91
457
|
const arch = os.arch();
|
|
@@ -281,26 +647,34 @@ function shouldUseFast(parsed) {
|
|
|
281
647
|
return !parsed.nodeOnlyFeature;
|
|
282
648
|
}
|
|
283
649
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
650
|
+
async function main() {
|
|
651
|
+
const parsed = parseLauncherArgs(process.argv.slice(2));
|
|
652
|
+
if (parsed.help) {
|
|
653
|
+
printHelp();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
289
656
|
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
657
|
+
const prepared = await prepareLoginConnection(parsed);
|
|
658
|
+
const fastRuntime = getFastRuntime();
|
|
659
|
+
const useFast = shouldUseFast(prepared);
|
|
660
|
+
if (useFast) {
|
|
661
|
+
const fastArgs = buildFastArgs(prepared.forwarded, prepared.fakeThumbnail);
|
|
662
|
+
const fastLaunch = resolveFastLaunch(fastRuntime);
|
|
663
|
+
if (fastLaunch.ok) {
|
|
664
|
+
spawnAgent(fastLaunch.command, [...fastLaunch.argsPrefix, ...fastArgs]);
|
|
665
|
+
} else if (prepared.engine === 'fast') {
|
|
666
|
+
console.error(`C# RemoteFast is unavailable: ${fastLaunch.reason}. Use --engine node to run the legacy Node agent.`);
|
|
667
|
+
process.exit(2);
|
|
668
|
+
} else {
|
|
669
|
+
console.warn(`C# RemoteFast is unavailable (${fastLaunch.reason}). Falling back to the Node remote agent.`);
|
|
670
|
+
spawnAgent(process.execPath, [nodeAgentPath, ...prepared.forwarded]);
|
|
671
|
+
}
|
|
300
672
|
} else {
|
|
301
|
-
|
|
302
|
-
spawnAgent(process.execPath, [nodeAgentPath, ...parsed.forwarded]);
|
|
673
|
+
spawnAgent(process.execPath, [nodeAgentPath, ...prepared.forwarded]);
|
|
303
674
|
}
|
|
304
|
-
} else {
|
|
305
|
-
spawnAgent(process.execPath, [nodeAgentPath, ...parsed.forwarded]);
|
|
306
675
|
}
|
|
676
|
+
|
|
677
|
+
main().catch(error => {
|
|
678
|
+
console.error(error?.message || error);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"node": ">=20"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@supabase/supabase-js": "^2.110.0",
|
|
32
33
|
"node-screenshots": "^0.2.8",
|
|
33
34
|
"openai": "^6.42.0"
|
|
34
35
|
},
|