@livedesk/client 0.1.251 → 0.1.252
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 +10 -10
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/bin/client-version.js +1 -1
- package/bin/livedesk-client-node.js +102 -102
- package/bin/livedesk-client-update-bootstrap.cjs +12 -12
- package/bin/livedesk-client.js +136 -122
- package/package.json +7 -7
- package/src/runtime/agent-process-lifecycle.js +10 -10
- package/src/runtime/agent-shell.js +66 -66
- package/src/runtime/client-runtime-server.js +12 -11
- package/src/runtime/linux-video-acceleration.js +1 -1
- package/src/runtime/windows-owned-process-manifest.js +9 -9
- package/tests/client-version.test.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VuvoDesk Client
|
|
2
2
|
|
|
3
|
-
For normal users, run the unified
|
|
3
|
+
For normal users, run the unified VuvoDesk application:
|
|
4
4
|
|
|
5
5
|
```powershell
|
|
6
6
|
npx -y --prefer-online livedesk@latest
|
|
@@ -20,7 +20,7 @@ aligned.
|
|
|
20
20
|
The default flow lets the local Client runtime own Google authorization,
|
|
21
21
|
callback exchange, and saved authentication as one path. The browser only
|
|
22
22
|
shows progress; it does not maintain a second session that must be transferred
|
|
23
|
-
back to the Client.
|
|
23
|
+
back to the Client. VuvoDesk then waits for the active Hub published by the
|
|
24
24
|
same account and connects locally. If the Hub is not
|
|
25
25
|
ready yet, the client waits for a Hub-online event and polls the registry at
|
|
26
26
|
most every six seconds as a fallback. Missing a wake event can never leave a
|
|
@@ -34,10 +34,10 @@ On Windows, check **Start with Windows** on the connection page to reconnect thi
|
|
|
34
34
|
client automatically after reboot. The startup entry reuses the saved Google
|
|
35
35
|
session and only opens the connection page again if sign-in is needed.
|
|
36
36
|
|
|
37
|
-
On the Hub computer, open the
|
|
37
|
+
On the Hub computer, open the VuvoDesk dashboard, sign in, and run **Sync
|
|
38
38
|
Server** once. The dashboard transfers the signed-in session to the local Hub;
|
|
39
39
|
the Hub process then keeps its own local address and private pair token
|
|
40
|
-
refreshed in the
|
|
40
|
+
refreshed in the VuvoDesk Supabase registry. The dashboard can be closed after
|
|
41
41
|
that while the Hub remains running.
|
|
42
42
|
|
|
43
43
|
Supabase Auth must allow the CLI callback URL:
|
|
@@ -55,12 +55,12 @@ can receive safe task-only instructions. The C# RemoteFast engine also supports
|
|
|
55
55
|
keyboard/mouse control on Windows, macOS, and Linux/X11, file transfer, and
|
|
56
56
|
system-audio playback from Windows, macOS, and Linux. Linux captures the default
|
|
57
57
|
output through PipeWire `pw-record` or PulseAudio `parec`. macOS control requires Accessibility
|
|
58
|
-
permission for the terminal app that started
|
|
59
|
-
system-audio capture require Screen Recording permission.
|
|
58
|
+
permission for the terminal app that started VuvoDesk; screen streaming and
|
|
59
|
+
system-audio capture require Screen Recording permission. VuvoDesk checks these
|
|
60
60
|
permissions without opening repeated system dialogs and prints the matching
|
|
61
61
|
System Settings path when access is missing; after granting access, restart the
|
|
62
62
|
client. When file transfer is enabled by the Hub, received files are saved
|
|
63
|
-
to `Desktop/
|
|
63
|
+
to `Desktop/VuvoDeskFiles` unless the Hub sets another destination folder.
|
|
64
64
|
|
|
65
65
|
By default, the launcher uses the packaged C# RemoteFast engine when supported.
|
|
66
66
|
It falls back to the Node engine only when a compatible RemoteFast runtime is
|
|
@@ -80,7 +80,7 @@ npx -y --prefer-online livedesk@latest client 3
|
|
|
80
80
|
npx -y --prefer-online livedesk@latest client --no-thumbnail
|
|
81
81
|
npx -y --prefer-online livedesk@latest client --no-live
|
|
82
82
|
npx -y --prefer-online livedesk@latest client --no-audio
|
|
83
|
-
npx -y --prefer-online livedesk@latest client --files-dir "D:\
|
|
83
|
+
npx -y --prefer-online livedesk@latest client --files-dir "D:\VuvoDeskFiles"
|
|
84
84
|
npx -y --prefer-online livedesk@latest client --engine node
|
|
85
85
|
npx -y --prefer-online livedesk@latest client --engine fast --trace-frames
|
|
86
86
|
npx -y --prefer-online livedesk@latest client --auth-port 5200
|
|
@@ -91,7 +91,7 @@ Frame pipeline roadmap:
|
|
|
91
91
|
|
|
92
92
|
- Mode 1: `mode1-jpeg` - current test path using screen capture, resize, and JPEG binary frames.
|
|
93
93
|
- Mode 2: `mode2-lzo` - cross-platform wall path using independent RGB565LE frames capped at 320x180 and compressed as LZO1X blocks. The wall defaults to 8 fps. macOS and Linux keep one persistent local capture helper per client process. Legacy `mode2-lz4` settings migrate to this mode.
|
|
94
|
-
- Mode 3: `mode3-h264-hw` - OS-specific H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV before falling back to the bundled ffmpeg `libx264` encoder. Linux users do not need to install ffmpeg separately when starting
|
|
94
|
+
- Mode 3: `mode3-h264-hw` - OS-specific H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV before falling back to the bundled ffmpeg `libx264` encoder. Linux users do not need to install ffmpeg separately when starting VuvoDesk through the published npm package. macOS emits one startup key frame and then an approximately half-second recovery GOP. The launcher uses bundled ffmpeg for fallback paths unless `LIVEDESK_FFMPEG` points to a custom binary. On macOS, set `LIVEDESK_FFMPEG_AVFOUNDATION_INPUT` only when the ScreenCaptureKit helper is unavailable and the AVFoundation fallback input is not `1:none`.
|
|
95
95
|
|
|
96
96
|
Legacy mode names such as `remote-fast` and `remote-quality` are treated as
|
|
97
97
|
Mode 1 aliases.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## lzokay
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
VuvoDesk's LZO1X block encoder and decoder follow the instruction layout of
|
|
6
6
|
[lzokay](https://github.com/AxioDL/lzokay), Copyright (c) 2018 Jack Andersen.
|
|
7
7
|
|
|
8
8
|
The MIT License
|
package/bin/client-version.js
CHANGED
|
@@ -19,5 +19,5 @@ export function formatClientVersionBanner(env = process.env) {
|
|
|
19
19
|
const launcherVersion = String(env.LIVEDESK_NPM_LAUNCHER_VERSION || '').trim();
|
|
20
20
|
const productName = launcherName || 'livedesk';
|
|
21
21
|
const productVersion = launcherVersion || readClientPackageVersion();
|
|
22
|
-
return `[
|
|
22
|
+
return `[VuvoDesk Client] ${productName}@${productVersion}`;
|
|
23
23
|
}
|
|
@@ -7,8 +7,8 @@ import crypto from 'crypto';
|
|
|
7
7
|
import { existsSync, promises as fs, statfsSync } from 'fs';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { resolveAgentShellCommand } from '../src/runtime/agent-shell.js';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { resolveAgentShellCommand } from '../src/runtime/agent-shell.js';
|
|
12
12
|
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
14
14
|
const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
|
|
@@ -40,7 +40,7 @@ function normalizeClientUpdateTargetVersion(value) {
|
|
|
40
40
|
const version = String(value || '').trim().replace(/^v/i, '');
|
|
41
41
|
if (!version) return '';
|
|
42
42
|
if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
|
|
43
|
-
throw new Error(`Invalid
|
|
43
|
+
throw new Error(`Invalid VuvoDesk product update version: ${version}`);
|
|
44
44
|
}
|
|
45
45
|
return version;
|
|
46
46
|
}
|
|
@@ -64,7 +64,7 @@ async function prepareClientUpdateNeutralCwd(operationId) {
|
|
|
64
64
|
const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
|
|
65
65
|
if (unexpectedEntry) {
|
|
66
66
|
const shadowPath = path.join(neutralCwd, unexpectedEntry);
|
|
67
|
-
throw new Error(`
|
|
67
|
+
throw new Error(`VuvoDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
68
68
|
}
|
|
69
69
|
return neutralCwd;
|
|
70
70
|
}
|
|
@@ -228,8 +228,8 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
228
228
|
return { ready: true, error: '' };
|
|
229
229
|
}
|
|
230
230
|
if (state.stage === 'failed') {
|
|
231
|
-
const error = String(state.error || '
|
|
232
|
-
console.error(`
|
|
231
|
+
const error = String(state.error || 'VuvoDesk client update preparation failed.');
|
|
232
|
+
console.error(`VuvoDesk client update preparation failed: ${error}`);
|
|
233
233
|
return { ready: false, error };
|
|
234
234
|
}
|
|
235
235
|
} catch {
|
|
@@ -239,8 +239,8 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
239
239
|
}
|
|
240
240
|
const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
|
|
241
241
|
const error = deadlineExpired
|
|
242
|
-
? 'The absolute
|
|
243
|
-
: 'Timed out waiting for the
|
|
242
|
+
? 'The absolute VuvoDesk Client update deadline expired before shutdown.'
|
|
243
|
+
: 'Timed out waiting for the VuvoDesk update supervisor to prepare the exact package.';
|
|
244
244
|
await writeClientUpdateState('failed', {
|
|
245
245
|
operationId,
|
|
246
246
|
error,
|
|
@@ -248,7 +248,7 @@ async function waitForClientUpdateShutdownSignal(
|
|
|
248
248
|
deadlineExpired,
|
|
249
249
|
failedAt: new Date().toISOString()
|
|
250
250
|
}).catch(() => undefined);
|
|
251
|
-
console.error('
|
|
251
|
+
console.error('VuvoDesk client update preparation timed out; the current Agent will remain running.');
|
|
252
252
|
return { ready: false, error };
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -262,7 +262,7 @@ function markClientUpdateConnected() {
|
|
|
262
262
|
restartVerified: true,
|
|
263
263
|
error: ''
|
|
264
264
|
}).catch(error => {
|
|
265
|
-
console.error(`
|
|
265
|
+
console.error(`VuvoDesk client update diagnostics could not be persisted: ${error?.message || error}`);
|
|
266
266
|
});
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -319,14 +319,14 @@ async function publishClientUpdateSupervisorProof(socket) {
|
|
|
319
319
|
|
|
320
320
|
function printHelp() {
|
|
321
321
|
console.log(`
|
|
322
|
-
|
|
322
|
+
VuvoDesk Client
|
|
323
323
|
|
|
324
324
|
Usage:
|
|
325
325
|
npx @livedesk/client connect --manager 127.0.0.1:5197 --pair <token>
|
|
326
326
|
|
|
327
327
|
Options:
|
|
328
|
-
--manager <host:port>
|
|
329
|
-
--pair <token>
|
|
328
|
+
--manager <host:port> VuvoDesk Hub address. Default: ${DEFAULT_MANAGER}
|
|
329
|
+
--pair <token> VuvoDesk Hub pairing token. Can also use LIVEDESK_CLIENT_PAIR_TOKEN.
|
|
330
330
|
--slot <number> Screen wall slot number for this computer.
|
|
331
331
|
--name <name> Friendly device name. Default: OS hostname.
|
|
332
332
|
--heartbeat <ms> Status heartbeat interval. Default: ${DEFAULT_HEARTBEAT_MS}
|
|
@@ -337,7 +337,7 @@ Options:
|
|
|
337
337
|
--no-live Disable focused live screen streaming.
|
|
338
338
|
--tasks Enable safe remote task inbox. Default on.
|
|
339
339
|
--no-tasks Disable remote task dispatch capability.
|
|
340
|
-
--files-dir <path> Default folder for received files. Default: ~/Desktop/
|
|
340
|
+
--files-dir <path> Default folder for received files. Default: ~/Desktop/VuvoDeskFiles.
|
|
341
341
|
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
342
342
|
--exit-on-disconnect Exit after Hub disconnect. Useful for tests.
|
|
343
343
|
--exit-on-invalid-pair Exit when the Hub rejects the pair token.
|
|
@@ -685,7 +685,7 @@ async function waitForNodeServiceState(executable, service, expectedState, timeo
|
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
function getDefaultFilesDir() {
|
|
688
|
-
return path.join(os.homedir(), 'Desktop', '
|
|
688
|
+
return path.join(os.homedir(), 'Desktop', 'VuvoDeskFiles');
|
|
689
689
|
}
|
|
690
690
|
|
|
691
691
|
function normalizeDirectoryPath(value, fallback = getDefaultFilesDir()) {
|
|
@@ -862,7 +862,7 @@ function createFakeThumbnailFrame(options, payload, frameSeq) {
|
|
|
862
862
|
'<rect width="100%" height="100%" fill="#0f172a"/>',
|
|
863
863
|
'<rect x="14" y="14" width="calc(100% - 28px)" height="calc(100% - 28px)" rx="12" fill="#1e293b" stroke="#38bdf8" stroke-width="2"/>',
|
|
864
864
|
`<text x="28" y="48" fill="#e2e8f0" font-family="Arial, sans-serif" font-size="20" font-weight="700">${title}</text>`,
|
|
865
|
-
`<text x="28" y="82" fill="#93c5fd" font-family="Arial, sans-serif" font-size="14">
|
|
865
|
+
`<text x="28" y="82" fill="#93c5fd" font-family="Arial, sans-serif" font-size="14">VuvoDesk Client Thumbnail</text>`,
|
|
866
866
|
`<text x="28" y="${height - 30}" fill="#94a3b8" font-family="Consolas, monospace" font-size="12">${now}</text>`,
|
|
867
867
|
'</svg>'
|
|
868
868
|
].join('');
|
|
@@ -1119,7 +1119,7 @@ async function assertAgentPathDoesNotTraverseLink(base, resolved) {
|
|
|
1119
1119
|
}
|
|
1120
1120
|
const canonicalCurrent = await fs.realpath(current);
|
|
1121
1121
|
if (!isPathWithinAgentRoot(canonicalRoot, canonicalCurrent)) {
|
|
1122
|
-
throw new Error('Safe Agent path resolves outside the
|
|
1122
|
+
throw new Error('Safe Agent path resolves outside the VuvoDesk files directory.');
|
|
1123
1123
|
}
|
|
1124
1124
|
return;
|
|
1125
1125
|
} catch (error) {
|
|
@@ -1137,16 +1137,16 @@ async function resolveAgentPath(options, value, permissionMode, rejectSensitive
|
|
|
1137
1137
|
if (!text || text.length > 600) throw new Error('path is invalid');
|
|
1138
1138
|
const resolved = path.resolve(path.isAbsolute(text) ? text : path.join(base, text));
|
|
1139
1139
|
const relative = path.relative(base, resolved);
|
|
1140
|
-
if (permissionMode !== 'full-access' && (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))) throw new Error('path is outside the
|
|
1140
|
+
if (permissionMode !== 'full-access' && (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))) throw new Error('path is outside the VuvoDesk files directory for this permission mode');
|
|
1141
1141
|
if (permissionMode !== 'full-access') await assertAgentPathDoesNotTraverseLink(base, resolved);
|
|
1142
1142
|
if (rejectSensitive && isSensitiveAgentPath(resolved)) throw new Error('credential and secret paths are not available');
|
|
1143
1143
|
return resolved;
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
|
-
function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined, maxTimeoutMs = 30000) {
|
|
1147
|
-
return new Promise((resolve, reject) => {
|
|
1148
|
-
const startedAt = Date.now();
|
|
1149
|
-
const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1146
|
+
function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined, maxTimeoutMs = 30000) {
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
const startedAt = Date.now();
|
|
1149
|
+
const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1150
1150
|
let output = '';
|
|
1151
1151
|
let timedOut = false;
|
|
1152
1152
|
const append = chunk => { output = `${output}${String(chunk || '')}`.replace(/[\0\r]/g, ' ').slice(0, MAX_AGENT_OUTPUT_CHARS); };
|
|
@@ -1156,55 +1156,55 @@ function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefine
|
|
|
1156
1156
|
timedOut = true;
|
|
1157
1157
|
child.kill('SIGTERM');
|
|
1158
1158
|
setTimeout(() => child.kill('SIGKILL'), 1000).unref?.();
|
|
1159
|
-
}, Math.max(1000, Math.min(maxTimeoutMs, Number(timeoutMs) || 15000)));
|
|
1160
|
-
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
1161
|
-
child.once('close', code => { clearTimeout(timer); resolve({ output: redactAgentOutput(`${output}${timedOut ? '\n[timeout]' : ''}`.trim()), exitCode: timedOut ? -1 : code ?? -1, timedOut, durationMs: Date.now() - startedAt }); });
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
async function searchNodeAgentFiles(options, args, permissionMode) {
|
|
1166
|
-
const root = await resolveAgentPath(options, args.path, permissionMode);
|
|
1167
|
-
const query = String(args.query || '').trim().toLowerCase();
|
|
1168
|
-
if (!query || query.length > 160 || /[\0\r\n]/.test(query)) throw new Error('search query is invalid');
|
|
1169
|
-
const maxResults = Math.max(1, Math.min(200, Number(args.maxResults) || 100));
|
|
1170
|
-
const maxDepth = Math.max(0, Math.min(8, Number(args.maxDepth) || 4));
|
|
1171
|
-
const maxScannedEntries = Math.max(maxResults, Math.min(5000, Number(args.maxScannedEntries) || 2000));
|
|
1172
|
-
const queue = [{ directory: root, depth: 0 }];
|
|
1173
|
-
const matches = [];
|
|
1174
|
-
let scannedEntries = 0;
|
|
1175
|
-
while (queue.length > 0 && matches.length < maxResults && scannedEntries < maxScannedEntries) {
|
|
1176
|
-
const current = queue.shift();
|
|
1177
|
-
let entries;
|
|
1178
|
-
try {
|
|
1179
|
-
entries = await fs.readdir(current.directory, { withFileTypes: true });
|
|
1180
|
-
} catch {
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
for (const entry of entries) {
|
|
1184
|
-
if (matches.length >= maxResults || scannedEntries >= maxScannedEntries) break;
|
|
1185
|
-
scannedEntries += 1;
|
|
1186
|
-
const entryPath = path.join(current.directory, entry.name);
|
|
1187
|
-
if (isSensitiveAgentPath(entryPath)) continue;
|
|
1188
|
-
const isDirectory = entry.isDirectory();
|
|
1189
|
-
if (entry.name.toLowerCase().includes(query)) {
|
|
1190
|
-
matches.push({ name: entry.name, path: entryPath, type: isDirectory ? 'directory' : 'file' });
|
|
1191
|
-
}
|
|
1192
|
-
if (isDirectory && current.depth < maxDepth && !entry.isSymbolicLink()) {
|
|
1193
|
-
queue.push({ directory: entryPath, depth: current.depth + 1 });
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
return {
|
|
1198
|
-
summary: `Found ${matches.length} matching path${matches.length === 1 ? '' : 's'}.`,
|
|
1199
|
-
data: {
|
|
1200
|
-
path: root,
|
|
1201
|
-
query,
|
|
1202
|
-
matches,
|
|
1203
|
-
scannedEntries,
|
|
1204
|
-
truncated: matches.length >= maxResults || scannedEntries >= maxScannedEntries
|
|
1205
|
-
}
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1159
|
+
}, Math.max(1000, Math.min(maxTimeoutMs, Number(timeoutMs) || 15000)));
|
|
1160
|
+
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
1161
|
+
child.once('close', code => { clearTimeout(timer); resolve({ output: redactAgentOutput(`${output}${timedOut ? '\n[timeout]' : ''}`.trim()), exitCode: timedOut ? -1 : code ?? -1, timedOut, durationMs: Date.now() - startedAt }); });
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function searchNodeAgentFiles(options, args, permissionMode) {
|
|
1166
|
+
const root = await resolveAgentPath(options, args.path, permissionMode);
|
|
1167
|
+
const query = String(args.query || '').trim().toLowerCase();
|
|
1168
|
+
if (!query || query.length > 160 || /[\0\r\n]/.test(query)) throw new Error('search query is invalid');
|
|
1169
|
+
const maxResults = Math.max(1, Math.min(200, Number(args.maxResults) || 100));
|
|
1170
|
+
const maxDepth = Math.max(0, Math.min(8, Number(args.maxDepth) || 4));
|
|
1171
|
+
const maxScannedEntries = Math.max(maxResults, Math.min(5000, Number(args.maxScannedEntries) || 2000));
|
|
1172
|
+
const queue = [{ directory: root, depth: 0 }];
|
|
1173
|
+
const matches = [];
|
|
1174
|
+
let scannedEntries = 0;
|
|
1175
|
+
while (queue.length > 0 && matches.length < maxResults && scannedEntries < maxScannedEntries) {
|
|
1176
|
+
const current = queue.shift();
|
|
1177
|
+
let entries;
|
|
1178
|
+
try {
|
|
1179
|
+
entries = await fs.readdir(current.directory, { withFileTypes: true });
|
|
1180
|
+
} catch {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
for (const entry of entries) {
|
|
1184
|
+
if (matches.length >= maxResults || scannedEntries >= maxScannedEntries) break;
|
|
1185
|
+
scannedEntries += 1;
|
|
1186
|
+
const entryPath = path.join(current.directory, entry.name);
|
|
1187
|
+
if (isSensitiveAgentPath(entryPath)) continue;
|
|
1188
|
+
const isDirectory = entry.isDirectory();
|
|
1189
|
+
if (entry.name.toLowerCase().includes(query)) {
|
|
1190
|
+
matches.push({ name: entry.name, path: entryPath, type: isDirectory ? 'directory' : 'file' });
|
|
1191
|
+
}
|
|
1192
|
+
if (isDirectory && current.depth < maxDepth && !entry.isSymbolicLink()) {
|
|
1193
|
+
queue.push({ directory: entryPath, depth: current.depth + 1 });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
summary: `Found ${matches.length} matching path${matches.length === 1 ? '' : 's'}.`,
|
|
1199
|
+
data: {
|
|
1200
|
+
path: root,
|
|
1201
|
+
query,
|
|
1202
|
+
matches,
|
|
1203
|
+
scannedEntries,
|
|
1204
|
+
truncated: matches.length >= maxResults || scannedEntries >= maxScannedEntries
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
1208
|
|
|
1209
1209
|
function redactAgentOutput(value) {
|
|
1210
1210
|
let output = String(value || '').replace(/[\0\r]/g, ' ');
|
|
@@ -1330,7 +1330,7 @@ async function persistNodeEnvironmentVariable(name, value) {
|
|
|
1330
1330
|
return { persisted: true, scope: 'user', output: result.output };
|
|
1331
1331
|
}
|
|
1332
1332
|
const profilePath = path.join(os.homedir(), '.profile');
|
|
1333
|
-
const marker = `#
|
|
1333
|
+
const marker = `# VuvoDesk managed environment: ${variableName}`;
|
|
1334
1334
|
const quoted = `'${variableValue.replaceAll("'", "'\\\"'\\\"'")}'`;
|
|
1335
1335
|
let profile = '';
|
|
1336
1336
|
try { profile = await fs.readFile(profilePath, 'utf8'); } catch { /* create on demand */ }
|
|
@@ -1370,7 +1370,7 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1370
1370
|
else await fs.unlink(filePath);
|
|
1371
1371
|
return { summary: `Deleted ${path.basename(filePath)}.`, data: { path: filePath, recursive: args.recursive === true } };
|
|
1372
1372
|
}
|
|
1373
|
-
if (operation === 'file.list') {
|
|
1373
|
+
if (operation === 'file.list') {
|
|
1374
1374
|
const directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1375
1375
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
1376
1376
|
const maxEntries = Math.max(1, Math.min(500, Number(args.maxEntries) || 200));
|
|
@@ -1380,15 +1380,15 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1380
1380
|
if (isSensitiveAgentPath(entryPath)) continue;
|
|
1381
1381
|
data.push({ name: entry.name, path: entryPath, type: entry.isDirectory() ? 'directory' : 'file', sizeBytes: entry.isDirectory() ? 0 : (await fs.stat(entryPath)).size });
|
|
1382
1382
|
}
|
|
1383
|
-
return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
|
|
1384
|
-
}
|
|
1385
|
-
if (operation === 'file.search') return searchNodeAgentFiles(options, args, permissionMode);
|
|
1386
|
-
if (operation === 'directory.create') {
|
|
1387
|
-
let directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1388
|
-
await fs.mkdir(directory, { recursive: true });
|
|
1389
|
-
directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1390
|
-
return { summary: `Created directory ${path.basename(directory)}.`, data: { path: directory, created: true } };
|
|
1391
|
-
}
|
|
1383
|
+
return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
|
|
1384
|
+
}
|
|
1385
|
+
if (operation === 'file.search') return searchNodeAgentFiles(options, args, permissionMode);
|
|
1386
|
+
if (operation === 'directory.create') {
|
|
1387
|
+
let directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1388
|
+
await fs.mkdir(directory, { recursive: true });
|
|
1389
|
+
directory = await resolveAgentPath(options, args.path, permissionMode);
|
|
1390
|
+
return { summary: `Created directory ${path.basename(directory)}.`, data: { path: directory, created: true } };
|
|
1391
|
+
}
|
|
1392
1392
|
if (operation === 'application.launch') {
|
|
1393
1393
|
const executable = String(args.executable || '').trim();
|
|
1394
1394
|
if (!executable || executable.length > 400 || /[\0\r\n]/.test(executable)) throw new Error('executable is invalid');
|
|
@@ -1442,13 +1442,13 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1442
1442
|
if (!ok) throw new Error(`Service ${action} failed for ${service}.`);
|
|
1443
1443
|
return { summary: `Service ${action} completed for ${service}.`, data: { service, action, results } };
|
|
1444
1444
|
}
|
|
1445
|
-
if (operation === 'command.run') {
|
|
1446
|
-
const command = String(args.command || '');
|
|
1447
|
-
if (!command || command.length > 16000 || /\0/.test(command)) throw new Error('command is invalid');
|
|
1448
|
-
const shellCommand = resolveAgentShellCommand({ shell: args.shell, command });
|
|
1449
|
-
const workingDirectory = args.workingDirectory ? await resolveAgentPath(options, args.workingDirectory, permissionMode, false) : undefined;
|
|
1450
|
-
const result = await runNodeAgentProcess(shellCommand.executable, shellCommand.args, args.timeoutMs, workingDirectory, 300000);
|
|
1451
|
-
return { summary: result.timedOut ? `${shellCommand.shell} command timed out.` : `${shellCommand.shell} command exited with code ${result.exitCode}.`, data: { ...result, output: redactAgentOutput(result.output), shell: shellCommand.shell, workingDirectory: workingDirectory || process.cwd() } };
|
|
1445
|
+
if (operation === 'command.run') {
|
|
1446
|
+
const command = String(args.command || '');
|
|
1447
|
+
if (!command || command.length > 16000 || /\0/.test(command)) throw new Error('command is invalid');
|
|
1448
|
+
const shellCommand = resolveAgentShellCommand({ shell: args.shell, command });
|
|
1449
|
+
const workingDirectory = args.workingDirectory ? await resolveAgentPath(options, args.workingDirectory, permissionMode, false) : undefined;
|
|
1450
|
+
const result = await runNodeAgentProcess(shellCommand.executable, shellCommand.args, args.timeoutMs, workingDirectory, 300000);
|
|
1451
|
+
return { summary: result.timedOut ? `${shellCommand.shell} command timed out.` : `${shellCommand.shell} command exited with code ${result.exitCode}.`, data: { ...result, output: redactAgentOutput(result.output), shell: shellCommand.shell, workingDirectory: workingDirectory || process.cwd() } };
|
|
1452
1452
|
}
|
|
1453
1453
|
if (operation === 'script.run') {
|
|
1454
1454
|
const scriptPath = await resolveAgentPath(options, args.path, permissionMode);
|
|
@@ -1535,7 +1535,7 @@ function normalizeNodeAgentTaskResult(result) {
|
|
|
1535
1535
|
return { ...result, ok, status: ok ? 'completed' : 'failed', error };
|
|
1536
1536
|
}
|
|
1537
1537
|
|
|
1538
|
-
const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'file.search', 'directory.create', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
|
|
1538
|
+
const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'file.search', 'directory.create', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
|
|
1539
1539
|
|
|
1540
1540
|
function remotePolicyAllows(options, command) {
|
|
1541
1541
|
const policy = options.effectivePolicy;
|
|
@@ -1587,7 +1587,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1587
1587
|
if (!Number.isSafeInteger(updateTimeoutMs)
|
|
1588
1588
|
|| updateTimeoutMs < 1_000
|
|
1589
1589
|
|| updateTimeoutMs > 90 * 60_000) {
|
|
1590
|
-
throw new Error('
|
|
1590
|
+
throw new Error('VuvoDesk Client update requires a bounded deadline budget.');
|
|
1591
1591
|
}
|
|
1592
1592
|
updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
|
|
1593
1593
|
const originalCwd = path.resolve(process.cwd());
|
|
@@ -1608,7 +1608,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1608
1608
|
? configuredParentPid
|
|
1609
1609
|
: process.pid;
|
|
1610
1610
|
if (!Number.isInteger(parentPid) || parentPid <= 1) {
|
|
1611
|
-
throw new Error('
|
|
1611
|
+
throw new Error('VuvoDesk client launcher process id is unavailable.');
|
|
1612
1612
|
}
|
|
1613
1613
|
const nodeBinDir = path.dirname(process.execPath);
|
|
1614
1614
|
const npxCandidates = process.platform === 'win32'
|
|
@@ -1616,7 +1616,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1616
1616
|
: [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
|
|
1617
1617
|
const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
|
|
1618
1618
|
if (!command) {
|
|
1619
|
-
throw new Error('
|
|
1619
|
+
throw new Error('VuvoDesk npx executable was not found.');
|
|
1620
1620
|
}
|
|
1621
1621
|
const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
|
|
1622
1622
|
? command
|
|
@@ -1651,7 +1651,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1651
1651
|
}, neutralCwd);
|
|
1652
1652
|
const bootstrapStat = await fs.stat(CLIENT_UPDATE_BOOTSTRAP_PATH);
|
|
1653
1653
|
if (!bootstrapStat.isFile() || bootstrapStat.size <= 0) {
|
|
1654
|
-
throw new Error(`
|
|
1654
|
+
throw new Error(`VuvoDesk Client update bootstrap is unavailable: ${CLIENT_UPDATE_BOOTSTRAP_PATH}`);
|
|
1655
1655
|
}
|
|
1656
1656
|
return await new Promise((resolve, reject) => {
|
|
1657
1657
|
const child = spawn(process.execPath, [CLIENT_UPDATE_BOOTSTRAP_PATH], {
|
|
@@ -1777,7 +1777,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1777
1777
|
summary: result.summary,
|
|
1778
1778
|
error: result.error || undefined,
|
|
1779
1779
|
data: result.data,
|
|
1780
|
-
sideEffects: ['file.read', 'file.list', 'file.search', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
|
|
1780
|
+
sideEffects: ['file.read', 'file.list', 'file.search', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
|
|
1781
1781
|
completedAt: new Date().toISOString()
|
|
1782
1782
|
}
|
|
1783
1783
|
});
|
|
@@ -2008,7 +2008,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
2008
2008
|
function connectOnce(options, deviceId) {
|
|
2009
2009
|
const manager = parseManagerAddress(options.manager);
|
|
2010
2010
|
if (!isPrivateLanHost(manager.host) && !isTruthy(process.env.LIVEDESK_ALLOW_UNENCRYPTED_LAN)) {
|
|
2011
|
-
return Promise.reject(new Error('Plain TCP
|
|
2011
|
+
return Promise.reject(new Error('Plain TCP VuvoDesk connections are limited to loopback/private LAN. Use an encrypted endpoint for external connections.'));
|
|
2012
2012
|
}
|
|
2013
2013
|
return new Promise((resolve, reject) => {
|
|
2014
2014
|
const socket = net.createConnection({
|
|
@@ -2106,11 +2106,11 @@ function connectOnce(options, deviceId) {
|
|
|
2106
2106
|
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2107
2107
|
? message.effectivePolicy
|
|
2108
2108
|
: null;
|
|
2109
|
-
console.log(`Connected to
|
|
2109
|
+
console.log(`Connected to VuvoDesk Hub as ${options.name} (${deviceId})`);
|
|
2110
2110
|
markClientUpdateConnected();
|
|
2111
2111
|
void publishClientUpdateSupervisorProof(socket).catch(error => {
|
|
2112
2112
|
if (!socket.destroyed) {
|
|
2113
|
-
console.error(`
|
|
2113
|
+
console.error(`VuvoDesk client update verification proof failed: ${error?.message || error}`);
|
|
2114
2114
|
}
|
|
2115
2115
|
});
|
|
2116
2116
|
writeJsonLine(socket, {
|
|
@@ -2147,7 +2147,7 @@ function connectOnce(options, deviceId) {
|
|
|
2147
2147
|
finish();
|
|
2148
2148
|
return;
|
|
2149
2149
|
} else if (message.type === 'error') {
|
|
2150
|
-
const remoteError = new Error(message.error || '
|
|
2150
|
+
const remoteError = new Error(message.error || 'VuvoDesk Hub rejected the connection.');
|
|
2151
2151
|
if (message.error === 'invalid-pair-token' && options.exitOnInvalidPair) {
|
|
2152
2152
|
remoteError.exitCode = EXIT_INVALID_PAIR_TOKEN;
|
|
2153
2153
|
}
|
|
@@ -2164,7 +2164,7 @@ function connectOnce(options, deviceId) {
|
|
|
2164
2164
|
|
|
2165
2165
|
async function connectWithRetry(options) {
|
|
2166
2166
|
if (!options.pair) {
|
|
2167
|
-
throw new Error('Missing --pair token. Sign in with Google or get it from
|
|
2167
|
+
throw new Error('Missing --pair token. Sign in with Google or get it from VuvoDesk Hub status.');
|
|
2168
2168
|
}
|
|
2169
2169
|
|
|
2170
2170
|
const deviceId = await getDeviceId(options.deviceId);
|
|
@@ -2173,7 +2173,7 @@ async function connectWithRetry(options) {
|
|
|
2173
2173
|
|
|
2174
2174
|
process.once('SIGINT', () => {
|
|
2175
2175
|
stopping = true;
|
|
2176
|
-
console.log('\nStopping
|
|
2176
|
+
console.log('\nStopping VuvoDesk Client...');
|
|
2177
2177
|
});
|
|
2178
2178
|
process.once('SIGTERM', () => {
|
|
2179
2179
|
stopping = true;
|
|
@@ -2192,7 +2192,7 @@ async function connectWithRetry(options) {
|
|
|
2192
2192
|
throw err;
|
|
2193
2193
|
}
|
|
2194
2194
|
const delayMs = DEFAULT_RECONNECT_MS;
|
|
2195
|
-
console.error(`
|
|
2195
|
+
console.error(`VuvoDesk Hub connection failed: ${err?.message || err}. Retrying in ${delayMs}ms.`);
|
|
2196
2196
|
await wait(delayMs);
|
|
2197
2197
|
}
|
|
2198
2198
|
}
|
|
@@ -79,7 +79,7 @@ function withStateLock(callback) {
|
|
|
79
79
|
}
|
|
80
80
|
if (error?.code !== 'EEXIST') throw error;
|
|
81
81
|
if (removeAbandonedStateLock()) continue;
|
|
82
|
-
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the
|
|
82
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the VuvoDesk update state lock: ${stateLockPath}`);
|
|
83
83
|
Atomics.wait(lockWaitBuffer, 0, 0, Math.min(25, Math.max(1, deadline - Date.now())));
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -125,26 +125,26 @@ function writeFailure(error, cancelRequested = false) {
|
|
|
125
125
|
};
|
|
126
126
|
}, 'handoff-failed');
|
|
127
127
|
} catch { /* preserve the original handoff failure */ }
|
|
128
|
-
process.stderr.write(`
|
|
128
|
+
process.stderr.write(`VuvoDesk update handoff failed: ${error?.message || error}\n`);
|
|
129
129
|
process.exitCode = 1;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function prepareNeutralCwd() {
|
|
133
|
-
if (!neutralCwd) throw new Error('
|
|
133
|
+
if (!neutralCwd) throw new Error('VuvoDesk update neutral working directory is unavailable.');
|
|
134
134
|
fs.mkdirSync(neutralCwd, { recursive: true });
|
|
135
135
|
const unexpectedEntry = fs.readdirSync(neutralCwd)[0];
|
|
136
136
|
if (unexpectedEntry) {
|
|
137
|
-
throw new Error(`
|
|
137
|
+
throw new Error(`VuvoDesk update neutral working directory is shadowed by ${path.join(neutralCwd, unexpectedEntry)}.`);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
function writeHandoffStarted() {
|
|
142
142
|
return writeState(previous => {
|
|
143
143
|
if (Date.now() >= updateDeadlineEpochMs) {
|
|
144
|
-
throw new Error('The absolute
|
|
144
|
+
throw new Error('The absolute VuvoDesk Client update deadline expired before handoff.');
|
|
145
145
|
}
|
|
146
146
|
if (previous.operationId !== operationId) {
|
|
147
|
-
throw new Error('
|
|
147
|
+
throw new Error('VuvoDesk update handoff was superseded before exact-package launch.');
|
|
148
148
|
}
|
|
149
149
|
const now = new Date().toISOString();
|
|
150
150
|
return {
|
|
@@ -175,7 +175,7 @@ function submitUpdateHostJob(job) {
|
|
|
175
175
|
|| status?.protocolVersion !== UPDATE_HOST_PROTOCOL_VERSION
|
|
176
176
|
|| !endpoint
|
|
177
177
|
|| !/^[a-f0-9]{64}$/i.test(token)) {
|
|
178
|
-
rejectRequest(new Error('The independent
|
|
178
|
+
rejectRequest(new Error('The independent VuvoDesk Update Host is unavailable; the existing Client was left running.'));
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
const request = JSON.stringify({
|
|
@@ -185,7 +185,7 @@ function submitUpdateHostJob(job) {
|
|
|
185
185
|
job
|
|
186
186
|
});
|
|
187
187
|
if (Buffer.byteLength(request, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
|
|
188
|
-
rejectRequest(new Error('The
|
|
188
|
+
rejectRequest(new Error('The VuvoDesk Client update request exceeded the Update Host message limit.'));
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
191
|
const socket = net.createConnection(endpoint);
|
|
@@ -207,7 +207,7 @@ function submitUpdateHostJob(job) {
|
|
|
207
207
|
socket.on('data', chunk => {
|
|
208
208
|
responseText += chunk;
|
|
209
209
|
if (Buffer.byteLength(responseText, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
|
|
210
|
-
finish(new Error('The
|
|
210
|
+
finish(new Error('The VuvoDesk Update Host response exceeded its message limit.'));
|
|
211
211
|
}
|
|
212
212
|
});
|
|
213
213
|
socket.once('error', error => finish(error));
|
|
@@ -215,7 +215,7 @@ function submitUpdateHostJob(job) {
|
|
|
215
215
|
try {
|
|
216
216
|
const response = JSON.parse(responseText || '{}');
|
|
217
217
|
if (response?.ok !== true || response?.accepted !== true || Number(response?.job?.workerPid || 0) <= 1) {
|
|
218
|
-
throw new Error(String(response?.error || 'The
|
|
218
|
+
throw new Error(String(response?.error || 'The VuvoDesk Update Host did not accept the Client update.'));
|
|
219
219
|
}
|
|
220
220
|
finish(null, response);
|
|
221
221
|
} catch (error) {
|
|
@@ -269,7 +269,7 @@ function buildWorkerInvocation(env) {
|
|
|
269
269
|
async function main() {
|
|
270
270
|
if (!operationId || !versionPattern.test(targetProductVersion)
|
|
271
271
|
|| !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs) {
|
|
272
|
-
throw new Error('Invalid or expired
|
|
272
|
+
throw new Error('Invalid or expired VuvoDesk exact-package update handoff.');
|
|
273
273
|
}
|
|
274
274
|
prepareNeutralCwd();
|
|
275
275
|
writeHandoffStarted();
|
|
@@ -289,7 +289,7 @@ async function main() {
|
|
|
289
289
|
|
|
290
290
|
const invocation = buildWorkerInvocation(env);
|
|
291
291
|
if (!path.isAbsolute(invocation.command) || !fs.existsSync(invocation.command)) {
|
|
292
|
-
throw new Error('
|
|
292
|
+
throw new Error('VuvoDesk could not prepare an absolute Client update worker command for the independent Update Host.');
|
|
293
293
|
}
|
|
294
294
|
const result = await submitUpdateHostJob({
|
|
295
295
|
jobId: `client-${operationId.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160)}`,
|