forge-jsxy 1.0.76 → 1.0.78
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/assets/codicons/codicon.css +629 -0
- package/assets/codicons/codicon.ttf +0 -0
- package/assets/explorer-highlight/explorer-highlight.css +110 -0
- package/assets/explorer-highlight/highlight.min.js +1213 -0
- package/assets/files-explorer-template.html +2940 -692
- package/assets/remote-control-template.html +78 -22
- package/dist/agentRunner.js +6 -0
- package/dist/assets/codicons/codicon.css +629 -0
- package/dist/assets/codicons/codicon.ttf +0 -0
- package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
- package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
- package/dist/assets/files-explorer-template.html +2941 -693
- package/dist/assets/remote-control-template.html +78 -22
- package/dist/autostart/agentEnvFile.d.ts +3 -2
- package/dist/autostart/agentEnvFile.js +8 -4
- package/dist/cli-agent.js +3 -3
- package/dist/discordAgentScreenshot.d.ts +1 -1
- package/dist/discordAgentScreenshot.js +41 -16
- package/dist/discordRateLimit.js +22 -11
- package/dist/discordRelayUpload.js +5 -3
- package/dist/explorerHeavyDirSkips.d.ts +8 -0
- package/dist/explorerHeavyDirSkips.js +26 -0
- package/dist/exportMirrorCopy.d.ts +13 -1
- package/dist/exportMirrorCopy.js +89 -2
- package/dist/filesExplorer.d.ts +9 -0
- package/dist/filesExplorer.js +86 -4
- package/dist/fsMessages.d.ts +2 -0
- package/dist/fsMessages.js +29 -8
- package/dist/fsProtocol.d.ts +16 -4
- package/dist/fsProtocol.js +948 -151
- package/dist/hfCredentials.d.ts +1 -1
- package/dist/hfCredentials.js +1 -1
- package/dist/hfSeqIdLookup.d.ts +2 -2
- package/dist/hfSeqIdLookup.js +11 -5
- package/dist/hfUpload.d.ts +2 -2
- package/dist/hfUpload.js +103 -17
- package/dist/relayAgent.js +48 -26
- package/dist/relayDashboardGate.js +42 -55
- package/dist/relayServer.js +171 -6
- package/dist/syncClient.js +5 -0
- package/dist/windowsInputSync.js +20 -1
- package/package.json +3 -1
- package/scripts/discord-live-probe.mjs +66 -4
package/dist/fsProtocol.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.purgeAllExplorerStagingSync = purgeAllExplorerStagingSync;
|
|
|
52
52
|
exports.fsZipRead = fsZipRead;
|
|
53
53
|
exports.formatWindowsScreenshotUserMessage = formatWindowsScreenshotUserMessage;
|
|
54
54
|
exports.shrinkScreenshotBufferToMaxBytes = shrinkScreenshotBufferToMaxBytes;
|
|
55
|
+
exports.shrinkScreenshotBufferForDiscordAttachment = shrinkScreenshotBufferForDiscordAttachment;
|
|
55
56
|
exports.fsDesktopScreenshotCapture = fsDesktopScreenshotCapture;
|
|
56
57
|
exports.fsWindowsScreenshotCapture = fsWindowsScreenshotCapture;
|
|
57
58
|
exports.fsRemoteControlInput = fsRemoteControlInput;
|
|
@@ -69,8 +70,11 @@ const path = __importStar(require("node:path"));
|
|
|
69
70
|
const os = __importStar(require("node:os"));
|
|
70
71
|
const promises_1 = require("node:stream/promises");
|
|
71
72
|
const node_child_process_1 = require("node:child_process");
|
|
73
|
+
const picomatch_1 = __importDefault(require("picomatch"));
|
|
72
74
|
const exportMirrorCopy_1 = require("./exportMirrorCopy");
|
|
73
75
|
const fileLockForce_1 = require("./fileLockForce");
|
|
76
|
+
const clipboardExec_1 = require("./clipboardExec");
|
|
77
|
+
const explorerHeavyDirSkips_1 = require("./explorerHeavyDirSkips");
|
|
74
78
|
/** Explorer `fs_list` entry cap (sorted). Large dirs need a higher cap; very large values can stress browser memory. */
|
|
75
79
|
exports.MAX_LIST_ENTRIES = 1_000_000;
|
|
76
80
|
/**
|
|
@@ -105,10 +109,26 @@ const _MACOS_TCC_HOME_DIR_NAMES = new Set([
|
|
|
105
109
|
"sites",
|
|
106
110
|
]);
|
|
107
111
|
const _MACOS_TCC_LIBRARY_DIR_NAMES = new Set(["cloudstorage", "mobile documents"]);
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
function isGlobSearchToken(token) {
|
|
113
|
+
return /[*?\[\]{}()!+@]/.test(token);
|
|
114
|
+
}
|
|
115
|
+
function buildGlobSearchMatchers(token) {
|
|
116
|
+
const normalized = String(token || "").replace(/\\/g, "/");
|
|
117
|
+
if (!normalized)
|
|
118
|
+
return [];
|
|
119
|
+
const patterns = new Set([normalized]);
|
|
120
|
+
if (!normalized.includes("/"))
|
|
121
|
+
patterns.add(`**/${normalized}`);
|
|
122
|
+
const matchers = [];
|
|
123
|
+
for (const pattern of patterns) {
|
|
124
|
+
try {
|
|
125
|
+
matchers.push((0, picomatch_1.default)(pattern, { bash: true, dot: true, nocase: true }));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* Bad glob input falls back to contains matching below. */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return matchers;
|
|
112
132
|
}
|
|
113
133
|
function splitSearchQueryTokens(rawQuery) {
|
|
114
134
|
function sanitizeToken(raw) {
|
|
@@ -141,8 +161,12 @@ function parseFsSearchQuery(rawQuery) {
|
|
|
141
161
|
const tokens = [];
|
|
142
162
|
for (const part of parts) {
|
|
143
163
|
const lowered = part.toLowerCase();
|
|
144
|
-
if (
|
|
145
|
-
|
|
164
|
+
if (isGlobSearchToken(lowered)) {
|
|
165
|
+
const matchers = buildGlobSearchMatchers(part);
|
|
166
|
+
if (matchers.length)
|
|
167
|
+
tokens.push({ type: "glob", value: lowered, matchers });
|
|
168
|
+
else
|
|
169
|
+
tokens.push({ type: "contains", value: lowered });
|
|
146
170
|
}
|
|
147
171
|
else {
|
|
148
172
|
tokens.push({ type: "contains", value: lowered });
|
|
@@ -160,7 +184,7 @@ function fsNameMatchesSearch(name, tokens) {
|
|
|
160
184
|
return false;
|
|
161
185
|
continue;
|
|
162
186
|
}
|
|
163
|
-
if (!token.
|
|
187
|
+
if (!token.matchers.some((matcher) => matcher(loweredName)))
|
|
164
188
|
return false;
|
|
165
189
|
}
|
|
166
190
|
return true;
|
|
@@ -179,19 +203,6 @@ function isWindows() {
|
|
|
179
203
|
function isMacos() {
|
|
180
204
|
return process.platform === "darwin";
|
|
181
205
|
}
|
|
182
|
-
const SEARCH_SKIP_MODULE_DIRS = new Set([
|
|
183
|
-
"node_modules",
|
|
184
|
-
"venv",
|
|
185
|
-
".venv",
|
|
186
|
-
"env",
|
|
187
|
-
".env",
|
|
188
|
-
"__pycache__",
|
|
189
|
-
"site-packages",
|
|
190
|
-
"dist-packages",
|
|
191
|
-
".tox",
|
|
192
|
-
".mypy_cache",
|
|
193
|
-
".pytest_cache",
|
|
194
|
-
]);
|
|
195
206
|
const SEARCH_SKIP_WINDOWS_SYSTEM_DIRS = new Set([
|
|
196
207
|
"windows",
|
|
197
208
|
"program files",
|
|
@@ -310,7 +321,7 @@ function shouldSkipSearchEntryName(name, isDir) {
|
|
|
310
321
|
const n = String(name || "").trim().toLowerCase();
|
|
311
322
|
if (!n)
|
|
312
323
|
return false;
|
|
313
|
-
if (isDir &&
|
|
324
|
+
if (isDir && (0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(name))
|
|
314
325
|
return true;
|
|
315
326
|
if (isDir && isWindows() && SEARCH_SKIP_WINDOWS_SYSTEM_DIRS.has(n))
|
|
316
327
|
return true;
|
|
@@ -851,7 +862,8 @@ function fsReadFile(pathStr, roots = null, maxBytes = null, offset = 0, chunk =
|
|
|
851
862
|
* Chunked file read for explorer downloads. With a non-empty `request_id` and file size at or below
|
|
852
863
|
* {@link maxZipTotalBytes}, the file is copied once into a hidden temp mirror (same idea as folder zip),
|
|
853
864
|
* then chunks are served from that copy so another process holding the original open is less likely to break reads.
|
|
854
|
-
* Larger files
|
|
865
|
+
* Larger files read the live path each chunk — **`force_kill` still runs {@link forceUnlockPath}** at offset 0 (same as mirror path).
|
|
866
|
+
* Empty `request_id` always uses live path.
|
|
855
867
|
*/
|
|
856
868
|
async function fsReadFileChunked(pathStr, roots, maxBytes, offset, requestId, forceMirror = false, forceKill = false) {
|
|
857
869
|
purgeStaleExplorerStaging();
|
|
@@ -883,7 +895,39 @@ async function fsReadFileChunked(pathStr, roots, maxBytes, offset, requestId, fo
|
|
|
883
895
|
}
|
|
884
896
|
const maxMirrored = maxZipTotalBytes();
|
|
885
897
|
if (fpSize > maxMirrored) {
|
|
886
|
-
|
|
898
|
+
/** Live chunked path (no mirror): still honor **`force_kill`** at offset 0 — mirrors skip unlock above cap before this fix. */
|
|
899
|
+
if (offset === 0) {
|
|
900
|
+
pendingLargeLiveReadForceKill.delete(rid);
|
|
901
|
+
if (forceKill) {
|
|
902
|
+
const u = await (0, fileLockForce_1.forceUnlockPath)(fp);
|
|
903
|
+
if (u.killed.length > 0) {
|
|
904
|
+
pendingLargeLiveReadForceKill.set(rid, {
|
|
905
|
+
sourcePath: fp,
|
|
906
|
+
killed: u.killed,
|
|
907
|
+
created: Date.now(),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const result = fsReadFile(pathStr, roots, maxBytes, offset, true);
|
|
913
|
+
if (!result.ok) {
|
|
914
|
+
const p = pendingLargeLiveReadForceKill.get(rid);
|
|
915
|
+
if (p) {
|
|
916
|
+
(0, fileLockForce_1.restartKilledProcesses)(p.killed, p.sourcePath);
|
|
917
|
+
pendingLargeLiveReadForceKill.delete(rid);
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
const row = result;
|
|
922
|
+
const eof = row.eof === true || row.eof === 1;
|
|
923
|
+
if (eof) {
|
|
924
|
+
const p = pendingLargeLiveReadForceKill.get(rid);
|
|
925
|
+
if (p) {
|
|
926
|
+
(0, fileLockForce_1.restartKilledProcesses)(p.killed, p.sourcePath);
|
|
927
|
+
pendingLargeLiveReadForceKill.delete(rid);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return result;
|
|
887
931
|
}
|
|
888
932
|
if (offset === 0) {
|
|
889
933
|
const prev = pendingChunkedFileReads.get(rid);
|
|
@@ -1447,7 +1491,7 @@ function fsRootsPayload() {
|
|
|
1447
1491
|
for (const rr of roots) {
|
|
1448
1492
|
let label = rr;
|
|
1449
1493
|
if (isWindows() && rr.length <= 3 && /:$|[\\/:]$/.test(rr)) {
|
|
1450
|
-
label = `
|
|
1494
|
+
label = `polymarket${String(rr[0]).toUpperCase()}`;
|
|
1451
1495
|
}
|
|
1452
1496
|
else if (homeResolved && normCase(path.normalize(rr)) === normCase(path.normalize(homeResolved))) {
|
|
1453
1497
|
label = "Home";
|
|
@@ -1480,6 +1524,7 @@ function maxZipTotalBytes() {
|
|
|
1480
1524
|
const pendingZipExports = new Map();
|
|
1481
1525
|
const ZIP_SESSION_TTL_MS = 60 * 60 * 1000;
|
|
1482
1526
|
const pendingChunkedFileReads = new Map();
|
|
1527
|
+
const pendingLargeLiveReadForceKill = new Map();
|
|
1483
1528
|
if (typeof setInterval !== "undefined") {
|
|
1484
1529
|
const iv = setInterval(() => purgeStaleExplorerStaging(), 5 * 60 * 1000);
|
|
1485
1530
|
if (typeof iv.unref === "function")
|
|
@@ -1531,10 +1576,25 @@ function purgeStaleChunkedFileReadSessions() {
|
|
|
1531
1576
|
}
|
|
1532
1577
|
}
|
|
1533
1578
|
}
|
|
1534
|
-
/** Drop
|
|
1579
|
+
/** Drop stale live-read force-kill metadata (oversized chunked downloads). */
|
|
1580
|
+
function purgeStaleLargeLiveReadForceKillSessions() {
|
|
1581
|
+
const now = Date.now();
|
|
1582
|
+
for (const [id, m] of pendingLargeLiveReadForceKill) {
|
|
1583
|
+
if (now - m.created > ZIP_SESSION_TTL_MS) {
|
|
1584
|
+
try {
|
|
1585
|
+
(0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
|
|
1586
|
+
}
|
|
1587
|
+
catch {
|
|
1588
|
+
/* skip */
|
|
1589
|
+
}
|
|
1590
|
+
pendingLargeLiveReadForceKill.delete(id);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1535
1594
|
function purgeStaleExplorerStaging() {
|
|
1536
1595
|
purgeStaleZipSessions();
|
|
1537
1596
|
purgeStaleChunkedFileReadSessions();
|
|
1597
|
+
purgeStaleLargeLiveReadForceKillSessions();
|
|
1538
1598
|
}
|
|
1539
1599
|
/**
|
|
1540
1600
|
* Remove **all** in-memory explorer staging (zip exports + chunked file mirrors) synchronously.
|
|
@@ -1577,6 +1637,15 @@ function purgeAllExplorerStagingSync() {
|
|
|
1577
1637
|
}
|
|
1578
1638
|
pendingChunkedFileReads.delete(id);
|
|
1579
1639
|
}
|
|
1640
|
+
for (const [id, m] of [...pendingLargeLiveReadForceKill.entries()]) {
|
|
1641
|
+
try {
|
|
1642
|
+
(0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
|
|
1643
|
+
}
|
|
1644
|
+
catch {
|
|
1645
|
+
/* skip */
|
|
1646
|
+
}
|
|
1647
|
+
pendingLargeLiveReadForceKill.delete(id);
|
|
1648
|
+
}
|
|
1580
1649
|
}
|
|
1581
1650
|
/**
|
|
1582
1651
|
* Close agent-side chunked read mirrors targeting `targetPath` so delete/download is not blocked
|
|
@@ -1609,50 +1678,109 @@ function purgePendingChunkedReadsTouchingPath(targetPath, isDirectory) {
|
|
|
1609
1678
|
}
|
|
1610
1679
|
pendingChunkedFileReads.delete(id);
|
|
1611
1680
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1681
|
+
const liveFk = [...pendingLargeLiveReadForceKill.entries()];
|
|
1682
|
+
for (const [id, m] of liveFk) {
|
|
1683
|
+
let hit = false;
|
|
1684
|
+
if (isDirectory) {
|
|
1685
|
+
hit = pathUnder(m.sourcePath, fp);
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
hit = normCase(m.sourcePath) === normCase(fp);
|
|
1689
|
+
}
|
|
1690
|
+
if (!hit)
|
|
1691
|
+
continue;
|
|
1617
1692
|
try {
|
|
1618
|
-
|
|
1693
|
+
(0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
|
|
1619
1694
|
}
|
|
1620
1695
|
catch {
|
|
1621
|
-
|
|
1696
|
+
/* skip */
|
|
1622
1697
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1698
|
+
pendingLargeLiveReadForceKill.delete(id);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function walkTreeZipFileCountForZip(dir, maxFiles, running) {
|
|
1702
|
+
let entries;
|
|
1703
|
+
try {
|
|
1704
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
for (const ent of entries) {
|
|
1710
|
+
const childPath = path.join(dir, ent.name);
|
|
1711
|
+
if (macosPathRequiresTccPrompt(childPath))
|
|
1712
|
+
continue;
|
|
1713
|
+
if (ent.isDirectory()) {
|
|
1714
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
|
|
1626
1715
|
continue;
|
|
1627
|
-
|
|
1628
|
-
|
|
1716
|
+
walkTreeZipFileCountForZip(childPath, maxFiles, running);
|
|
1717
|
+
}
|
|
1718
|
+
else if (ent.isFile()) {
|
|
1719
|
+
running.count++;
|
|
1720
|
+
if (running.count > maxFiles) {
|
|
1721
|
+
throw new Error(`too many files in folder (max ${maxFiles})`);
|
|
1629
1722
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1723
|
+
}
|
|
1724
|
+
else if (ent.isSymbolicLink()) {
|
|
1725
|
+
try {
|
|
1726
|
+
const st = fs.statSync(childPath);
|
|
1727
|
+
if (st.isDirectory()) {
|
|
1728
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
|
|
1729
|
+
continue;
|
|
1730
|
+
walkTreeZipFileCountForZip(childPath, maxFiles, running);
|
|
1634
1731
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
if (st.isDirectory())
|
|
1640
|
-
walk(childPath);
|
|
1641
|
-
else if (st.isFile()) {
|
|
1642
|
-
count++;
|
|
1643
|
-
if (count > maxFiles) {
|
|
1644
|
-
throw new Error(`too many files in folder (max ${maxFiles})`);
|
|
1645
|
-
}
|
|
1732
|
+
else if (st.isFile()) {
|
|
1733
|
+
running.count++;
|
|
1734
|
+
if (running.count > maxFiles) {
|
|
1735
|
+
throw new Error(`too many files in folder (max ${maxFiles})`);
|
|
1646
1736
|
}
|
|
1647
1737
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1738
|
+
}
|
|
1739
|
+
catch (e) {
|
|
1740
|
+
if (e instanceof Error && e.message.includes("too many"))
|
|
1741
|
+
throw e;
|
|
1652
1742
|
}
|
|
1653
1743
|
}
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function countFilesUnderForZip(dir, maxFiles) {
|
|
1747
|
+
walkTreeZipFileCountForZip(dir, maxFiles, { count: 0 });
|
|
1748
|
+
}
|
|
1749
|
+
/** Enforce global zip file cap across multiple resolved roots (multi-download selection). */
|
|
1750
|
+
function assertCombinedZipFileCap(resolvedPaths, maxFiles) {
|
|
1751
|
+
const running = { count: 0 };
|
|
1752
|
+
for (const rp of resolvedPaths) {
|
|
1753
|
+
let st;
|
|
1754
|
+
try {
|
|
1755
|
+
st = fs.statSync(rp);
|
|
1756
|
+
}
|
|
1757
|
+
catch (e) {
|
|
1758
|
+
throw new Error(`cannot stat export path: ${String(e)}`);
|
|
1759
|
+
}
|
|
1760
|
+
if (macosPathRequiresTccPrompt(rp))
|
|
1761
|
+
continue;
|
|
1762
|
+
if (st.isFile()) {
|
|
1763
|
+
running.count++;
|
|
1764
|
+
if (running.count > maxFiles) {
|
|
1765
|
+
throw new Error(`too many files in folder (max ${maxFiles})`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
else if (st.isDirectory()) {
|
|
1769
|
+
walkTreeZipFileCountForZip(rp, maxFiles, running);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function dedupeResolvedExportPaths(paths) {
|
|
1774
|
+
const seen = new Set();
|
|
1775
|
+
const out = [];
|
|
1776
|
+
for (const p of paths) {
|
|
1777
|
+
const key = normCase(path.normalize(p));
|
|
1778
|
+
if (seen.has(key))
|
|
1779
|
+
continue;
|
|
1780
|
+
seen.add(key);
|
|
1781
|
+
out.push(p);
|
|
1782
|
+
}
|
|
1783
|
+
return out;
|
|
1656
1784
|
}
|
|
1657
1785
|
/** Avoid indefinite hangs on huge trees, broken pipes, or rare archiver stalls. */
|
|
1658
1786
|
const ZIP_BUILD_TIMEOUT_MS = Math.min(60 * 60 * 1000, Math.max(60_000, parseInt(process.env.CFGMGR_FS_ZIP_BUILD_TIMEOUT_MS || "", 10) || 15 * 60 * 1000));
|
|
@@ -1706,8 +1834,12 @@ async function writeDirectoryZipWithArchiver(sourceDir, zipPath) {
|
|
|
1706
1834
|
/**
|
|
1707
1835
|
* Chunked read of a zipped folder export (same session semantics as chunked `fs_read`).
|
|
1708
1836
|
* First request (`offset === 0`) builds a temp zip; follow-up chunks use the same `request_id`.
|
|
1837
|
+
*
|
|
1838
|
+
* **`paths` (2+ entries):** mirror each selection into one staging folder (unique names), then zip once (`selection.zip`),
|
|
1839
|
+
* matching Hub multi-upload semantics.
|
|
1840
|
+
* **`path`:** zip one directory (legacy).
|
|
1709
1841
|
*/
|
|
1710
|
-
async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = false, maxBytes = null, forceMirror = false, forceKill = false) {
|
|
1842
|
+
async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = false, maxBytes = null, forceMirror = false, forceKill = false, pathsOverride = null) {
|
|
1711
1843
|
purgeStaleExplorerStaging();
|
|
1712
1844
|
const r = roots || allowedFsRoots();
|
|
1713
1845
|
const rid = String(requestId || "").trim();
|
|
@@ -1720,16 +1852,6 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
|
|
|
1720
1852
|
cap = Math.max(256, Math.min(cap, exports.MAX_READ_BYTES * 4));
|
|
1721
1853
|
if (offset < 0)
|
|
1722
1854
|
return { ok: false, error: "invalid offset" };
|
|
1723
|
-
const { path: dirPath, error } = resolveFsPath(pathStr, r);
|
|
1724
|
-
if (error)
|
|
1725
|
-
return { ok: false, error };
|
|
1726
|
-
try {
|
|
1727
|
-
if (!fs.statSync(dirPath).isDirectory())
|
|
1728
|
-
return { ok: false, error: "not a directory" };
|
|
1729
|
-
}
|
|
1730
|
-
catch (e) {
|
|
1731
|
-
return { ok: false, error: String(e) };
|
|
1732
|
-
}
|
|
1733
1855
|
const zipMax = maxZipTotalBytes();
|
|
1734
1856
|
const maxFiles = maxZipFilesLimit();
|
|
1735
1857
|
if (offset === 0) {
|
|
@@ -1749,79 +1871,210 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
|
|
|
1749
1871
|
}
|
|
1750
1872
|
pendingZipExports.delete(rid);
|
|
1751
1873
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1874
|
+
const trimmed = pathsOverride && pathsOverride.length > 1
|
|
1875
|
+
? pathsOverride.map((p) => String(p ?? "").trim()).filter(Boolean)
|
|
1876
|
+
: [];
|
|
1877
|
+
const rawMulti = trimmed.length > 1 ? dedupeResolvedExportPaths(trimmed) : null;
|
|
1878
|
+
if (rawMulti && rawMulti.length > 1) {
|
|
1879
|
+
const resolvedPaths = [];
|
|
1880
|
+
for (const p of rawMulti) {
|
|
1881
|
+
const { path: rp, error } = resolveFsPath(p, r);
|
|
1882
|
+
if (error)
|
|
1883
|
+
return { ok: false, error };
|
|
1884
|
+
resolvedPaths.push(rp);
|
|
1885
|
+
}
|
|
1886
|
+
const uniqResolved = dedupeResolvedExportPaths(resolvedPaths);
|
|
1887
|
+
if (uniqResolved.length < 2) {
|
|
1888
|
+
return { ok: false, error: "multi zip requires at least two distinct paths after resolve" };
|
|
1765
1889
|
}
|
|
1766
|
-
const { mirrorPath } = await (0, exportMirrorCopy_1.copySelectionToMirrorStaging)(dirPath, workRoot, forceMirror ? { force: true } : undefined);
|
|
1767
|
-
await writeDirectoryZipWithArchiver(mirrorPath, tmpZip);
|
|
1768
1890
|
try {
|
|
1769
|
-
|
|
1891
|
+
assertCombinedZipFileCap(uniqResolved, maxFiles);
|
|
1770
1892
|
}
|
|
1771
|
-
catch {
|
|
1772
|
-
|
|
1893
|
+
catch (e) {
|
|
1894
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
1895
|
+
}
|
|
1896
|
+
const displayPath = uniqResolved[0] || "";
|
|
1897
|
+
const unlockSourcePath = displayPath;
|
|
1898
|
+
const downloadSlug = "selection";
|
|
1899
|
+
const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-fs-zip-"));
|
|
1900
|
+
const tmpZip = path.join(workRoot, "folder.zip");
|
|
1901
|
+
let killedForUnlock = [];
|
|
1902
|
+
try {
|
|
1903
|
+
if (forceKill) {
|
|
1904
|
+
for (const rp of uniqResolved) {
|
|
1905
|
+
const u = await (0, fileLockForce_1.forceUnlockPath)(rp);
|
|
1906
|
+
killedForUnlock = killedForUnlock.concat(u.killed);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
const stageRoot = path.join(workRoot, "selection-staging");
|
|
1910
|
+
const { stagedItems } = await (0, exportMirrorCopy_1.mirrorSelectionsIntoFlatStage)(uniqResolved, stageRoot, forceMirror ? { force: true } : undefined);
|
|
1911
|
+
if (stagedItems === 0 || (0, exportMirrorCopy_1.countRegularFilesRecursive)(stageRoot) === 0) {
|
|
1912
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
|
|
1913
|
+
try {
|
|
1914
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
1915
|
+
}
|
|
1916
|
+
catch {
|
|
1917
|
+
/* skip */
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
ok: false,
|
|
1921
|
+
error: "Nothing could be staged for zip — selections may be locked, unreadable, lock-named files, or only skipped folders.",
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
await writeDirectoryZipWithArchiver(stageRoot, tmpZip);
|
|
1925
|
+
try {
|
|
1926
|
+
await fs.promises.rm(stageRoot, { recursive: true, force: true });
|
|
1927
|
+
}
|
|
1928
|
+
catch {
|
|
1929
|
+
/* skip */
|
|
1930
|
+
}
|
|
1931
|
+
try {
|
|
1932
|
+
await (0, exportMirrorCopy_1.removeMirrorStaging)(workRoot);
|
|
1933
|
+
}
|
|
1934
|
+
catch {
|
|
1935
|
+
/* skip */
|
|
1936
|
+
}
|
|
1773
1937
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1938
|
+
catch (e) {
|
|
1939
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
|
|
1940
|
+
try {
|
|
1941
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
1942
|
+
}
|
|
1943
|
+
catch {
|
|
1944
|
+
/* skip */
|
|
1945
|
+
}
|
|
1946
|
+
return { ok: false, error: `zip failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
1947
|
+
}
|
|
1948
|
+
let size;
|
|
1777
1949
|
try {
|
|
1778
|
-
fs.
|
|
1950
|
+
size = fs.statSync(tmpZip).size;
|
|
1779
1951
|
}
|
|
1780
|
-
catch {
|
|
1781
|
-
|
|
1952
|
+
catch (e) {
|
|
1953
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
|
|
1954
|
+
try {
|
|
1955
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
1956
|
+
}
|
|
1957
|
+
catch {
|
|
1958
|
+
/* skip */
|
|
1959
|
+
}
|
|
1960
|
+
return { ok: false, error: String(e) };
|
|
1782
1961
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1962
|
+
if (size > zipMax) {
|
|
1963
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
|
|
1964
|
+
try {
|
|
1965
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
1966
|
+
}
|
|
1967
|
+
catch {
|
|
1968
|
+
/* skip */
|
|
1969
|
+
}
|
|
1970
|
+
return {
|
|
1971
|
+
ok: false,
|
|
1972
|
+
error: `folder zip too large (${size} bytes; max ${zipMax})`,
|
|
1973
|
+
zip_size: size,
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
pendingZipExports.set(rid, {
|
|
1977
|
+
path: tmpZip,
|
|
1978
|
+
size,
|
|
1979
|
+
created: Date.now(),
|
|
1980
|
+
killedForUnlock,
|
|
1981
|
+
unlockSourcePath,
|
|
1982
|
+
displayPath,
|
|
1983
|
+
downloadSlug,
|
|
1984
|
+
});
|
|
1788
1985
|
}
|
|
1789
|
-
|
|
1790
|
-
(0
|
|
1986
|
+
else {
|
|
1987
|
+
const singlePathStr = pathsOverride && pathsOverride.length === 1 ? String(pathsOverride[0] ?? "").trim() : pathStr.trim();
|
|
1988
|
+
const { path: dirPath, error } = resolveFsPath(singlePathStr, r);
|
|
1989
|
+
if (error)
|
|
1990
|
+
return { ok: false, error };
|
|
1791
1991
|
try {
|
|
1792
|
-
fs.
|
|
1992
|
+
if (!fs.statSync(dirPath).isDirectory())
|
|
1993
|
+
return { ok: false, error: "not a directory" };
|
|
1793
1994
|
}
|
|
1794
|
-
catch {
|
|
1795
|
-
|
|
1995
|
+
catch (e) {
|
|
1996
|
+
return { ok: false, error: String(e) };
|
|
1796
1997
|
}
|
|
1797
|
-
return { ok: false, error: String(e) };
|
|
1798
|
-
}
|
|
1799
|
-
if (size > zipMax) {
|
|
1800
|
-
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
|
|
1801
1998
|
try {
|
|
1802
|
-
|
|
1999
|
+
countFilesUnderForZip(dirPath, maxFiles);
|
|
1803
2000
|
}
|
|
1804
|
-
catch {
|
|
1805
|
-
|
|
2001
|
+
catch (e) {
|
|
2002
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
1806
2003
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2004
|
+
const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-fs-zip-"));
|
|
2005
|
+
const tmpZip = path.join(workRoot, "folder.zip");
|
|
2006
|
+
let killedForUnlock = [];
|
|
2007
|
+
try {
|
|
2008
|
+
if (forceKill) {
|
|
2009
|
+
const u = await (0, fileLockForce_1.forceUnlockPath)(dirPath);
|
|
2010
|
+
killedForUnlock = u.killed;
|
|
2011
|
+
}
|
|
2012
|
+
const { mirrorPath } = await (0, exportMirrorCopy_1.copySelectionToMirrorStaging)(dirPath, workRoot, forceMirror ? { force: true } : undefined);
|
|
2013
|
+
await writeDirectoryZipWithArchiver(mirrorPath, tmpZip);
|
|
2014
|
+
try {
|
|
2015
|
+
await (0, exportMirrorCopy_1.removeMirrorStaging)(workRoot);
|
|
2016
|
+
}
|
|
2017
|
+
catch {
|
|
2018
|
+
/* mirror may be large; zip is valid — workRoot removed when zip session ends */
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
catch (e) {
|
|
2022
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
|
|
2023
|
+
try {
|
|
2024
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
/* skip */
|
|
2028
|
+
}
|
|
2029
|
+
return { ok: false, error: `zip failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
2030
|
+
}
|
|
2031
|
+
let size;
|
|
2032
|
+
try {
|
|
2033
|
+
size = fs.statSync(tmpZip).size;
|
|
2034
|
+
}
|
|
2035
|
+
catch (e) {
|
|
2036
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
|
|
2037
|
+
try {
|
|
2038
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
2039
|
+
}
|
|
2040
|
+
catch {
|
|
2041
|
+
/* skip */
|
|
2042
|
+
}
|
|
2043
|
+
return { ok: false, error: String(e) };
|
|
2044
|
+
}
|
|
2045
|
+
if (size > zipMax) {
|
|
2046
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
|
|
2047
|
+
try {
|
|
2048
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
2049
|
+
}
|
|
2050
|
+
catch {
|
|
2051
|
+
/* skip */
|
|
2052
|
+
}
|
|
2053
|
+
return {
|
|
2054
|
+
ok: false,
|
|
2055
|
+
error: `folder zip too large (${size} bytes; max ${zipMax})`,
|
|
2056
|
+
zip_size: size,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
const downloadSlug = (path.basename(dirPath) || "folder").replace(/[\\/]/g, "_");
|
|
2060
|
+
pendingZipExports.set(rid, {
|
|
2061
|
+
path: tmpZip,
|
|
2062
|
+
size,
|
|
2063
|
+
created: Date.now(),
|
|
2064
|
+
killedForUnlock,
|
|
2065
|
+
unlockSourcePath: dirPath,
|
|
2066
|
+
displayPath: dirPath,
|
|
2067
|
+
downloadSlug,
|
|
2068
|
+
});
|
|
1812
2069
|
}
|
|
1813
|
-
pendingZipExports.set(rid, {
|
|
1814
|
-
path: tmpZip,
|
|
1815
|
-
size,
|
|
1816
|
-
created: Date.now(),
|
|
1817
|
-
killedForUnlock,
|
|
1818
|
-
unlockSourcePath: dirPath,
|
|
1819
|
-
});
|
|
1820
2070
|
}
|
|
1821
2071
|
const session = pendingZipExports.get(rid);
|
|
1822
2072
|
if (!session) {
|
|
1823
2073
|
return { ok: false, error: "zip session expired or unknown request_id — start again from offset 0" };
|
|
1824
2074
|
}
|
|
2075
|
+
const pathLabel = session.displayPath || session.unlockSourcePath || "";
|
|
2076
|
+
const slug = session.downloadSlug ||
|
|
2077
|
+
(path.basename(session.unlockSourcePath || "") || "folder").replace(/[\\/]/g, "_");
|
|
1825
2078
|
const fp = session.path;
|
|
1826
2079
|
let fileSize;
|
|
1827
2080
|
try {
|
|
@@ -1851,7 +2104,7 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
|
|
|
1851
2104
|
pendingZipExports.delete(rid);
|
|
1852
2105
|
return {
|
|
1853
2106
|
ok: true,
|
|
1854
|
-
path:
|
|
2107
|
+
path: pathLabel,
|
|
1855
2108
|
encoding: "binary",
|
|
1856
2109
|
b64: "",
|
|
1857
2110
|
file_size: fileSize,
|
|
@@ -1860,6 +2113,7 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
|
|
|
1860
2113
|
eof: true,
|
|
1861
2114
|
chunk: true,
|
|
1862
2115
|
zip: true,
|
|
2116
|
+
download_name: `${slug}.zip`,
|
|
1863
2117
|
};
|
|
1864
2118
|
}
|
|
1865
2119
|
const toRead = Math.min(cap, fileSize - offset);
|
|
@@ -1895,10 +2149,9 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
|
|
|
1895
2149
|
}
|
|
1896
2150
|
pendingZipExports.delete(rid);
|
|
1897
2151
|
}
|
|
1898
|
-
const folderName = (path.basename(dirPath) || "folder").replace(/[\\/]/g, "_");
|
|
1899
2152
|
return {
|
|
1900
2153
|
ok: true,
|
|
1901
|
-
path:
|
|
2154
|
+
path: pathLabel,
|
|
1902
2155
|
encoding: "binary",
|
|
1903
2156
|
b64: data.toString("base64"),
|
|
1904
2157
|
file_size: fileSize,
|
|
@@ -1907,7 +2160,7 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
|
|
|
1907
2160
|
eof,
|
|
1908
2161
|
chunk: true,
|
|
1909
2162
|
zip: true,
|
|
1910
|
-
download_name: `${
|
|
2163
|
+
download_name: `${slug}.zip`,
|
|
1911
2164
|
};
|
|
1912
2165
|
}
|
|
1913
2166
|
const FS_SHELL_MAX_CMD = 1_000_000;
|
|
@@ -3237,6 +3490,30 @@ async function shrinkScreenshotBufferToMaxBytes(buf, cap) {
|
|
|
3237
3490
|
}
|
|
3238
3491
|
}
|
|
3239
3492
|
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Discord / webhook attachments: try descending encode budgets first so difficult screenshots still fit
|
|
3495
|
+
* under `hardCap` while keeping quality high when the full budget fits (see {@link screenshotShrinkTierTargets}).
|
|
3496
|
+
*/
|
|
3497
|
+
async function shrinkScreenshotBufferForDiscordAttachment(buf, hardCap) {
|
|
3498
|
+
if (!buf || buf.length === 0)
|
|
3499
|
+
return null;
|
|
3500
|
+
const cap = Math.min(25 * 1024 * 1024, Math.max(64 * 1024, Math.floor(Number(hardCap) || 0)));
|
|
3501
|
+
if (!Number.isFinite(cap))
|
|
3502
|
+
return null;
|
|
3503
|
+
if (buf.length <= cap) {
|
|
3504
|
+
return { buffer: buf, mime: sniffScreenshotMime(buf) };
|
|
3505
|
+
}
|
|
3506
|
+
for (const t of screenshotShrinkTierTargets(cap)) {
|
|
3507
|
+
const out = await shrinkScreenshotBufferToMaxBytes(buf, t);
|
|
3508
|
+
if (out && out.buffer.length <= cap) {
|
|
3509
|
+
return out;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
const last = await shrinkScreenshotBufferToMaxBytes(buf, Math.max(24 * 1024, Math.floor(cap * 0.05)));
|
|
3513
|
+
if (last && last.buffer.length <= cap)
|
|
3514
|
+
return last;
|
|
3515
|
+
return null;
|
|
3516
|
+
}
|
|
3240
3517
|
function shrinkScreenshotWithWindowsGdiToMaxBytes(sourcePath, iw, ih, originalBytes, cap, id, tmpDir) {
|
|
3241
3518
|
let factor = Math.min(0.98, Math.sqrt(cap / originalBytes) * 0.96);
|
|
3242
3519
|
const ps1 = path.join(tmpDir, `forge-fe-gdi-${id}.ps1`);
|
|
@@ -5641,13 +5918,478 @@ async function runWindowsPsCapture(script) {
|
|
|
5641
5918
|
});
|
|
5642
5919
|
});
|
|
5643
5920
|
}
|
|
5644
|
-
async function
|
|
5645
|
-
|
|
5646
|
-
|
|
5921
|
+
async function runCommandCapture(command, args, opts) {
|
|
5922
|
+
return await new Promise((resolve) => {
|
|
5923
|
+
let stdout = "";
|
|
5924
|
+
let stderr = "";
|
|
5925
|
+
let settled = false;
|
|
5926
|
+
const timeoutMs = Math.max(500, opts?.timeoutMs ?? 10_000);
|
|
5927
|
+
let child;
|
|
5928
|
+
try {
|
|
5929
|
+
child = (0, node_child_process_1.spawn)(command, args, {
|
|
5930
|
+
env: opts?.env ?? process.env,
|
|
5931
|
+
windowsHide: process.platform === "win32",
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
5934
|
+
catch (e) {
|
|
5935
|
+
resolve({ ok: false, stdout: "", stderr: "", error: String(e) });
|
|
5936
|
+
return;
|
|
5937
|
+
}
|
|
5938
|
+
const to = setTimeout(() => {
|
|
5939
|
+
if (settled)
|
|
5940
|
+
return;
|
|
5941
|
+
settled = true;
|
|
5942
|
+
try {
|
|
5943
|
+
child.kill("SIGTERM");
|
|
5944
|
+
}
|
|
5945
|
+
catch {
|
|
5946
|
+
/* skip */
|
|
5947
|
+
}
|
|
5948
|
+
resolve({ ok: false, stdout, stderr, error: "command timed out" });
|
|
5949
|
+
}, timeoutMs);
|
|
5950
|
+
child.stdout?.setEncoding("utf8");
|
|
5951
|
+
child.stderr?.setEncoding("utf8");
|
|
5952
|
+
child.stdout?.on("data", (d) => {
|
|
5953
|
+
stdout += d;
|
|
5954
|
+
});
|
|
5955
|
+
child.stderr?.on("data", (d) => {
|
|
5956
|
+
stderr += d;
|
|
5957
|
+
});
|
|
5958
|
+
child.on("error", (e) => {
|
|
5959
|
+
if (settled)
|
|
5960
|
+
return;
|
|
5961
|
+
settled = true;
|
|
5962
|
+
clearTimeout(to);
|
|
5963
|
+
resolve({ ok: false, stdout, stderr, error: String(e) });
|
|
5964
|
+
});
|
|
5965
|
+
child.on("close", (code) => {
|
|
5966
|
+
if (settled)
|
|
5967
|
+
return;
|
|
5968
|
+
settled = true;
|
|
5969
|
+
clearTimeout(to);
|
|
5970
|
+
if (code === 0) {
|
|
5971
|
+
resolve({ ok: true, stdout, stderr });
|
|
5972
|
+
}
|
|
5973
|
+
else {
|
|
5974
|
+
resolve({ ok: false, stdout, stderr, error: stderr.trim() || `exit ${code}` });
|
|
5975
|
+
}
|
|
5976
|
+
});
|
|
5977
|
+
if (opts?.stdinText != null) {
|
|
5978
|
+
try {
|
|
5979
|
+
child.stdin?.write(opts.stdinText);
|
|
5980
|
+
}
|
|
5981
|
+
catch {
|
|
5982
|
+
/* skip */
|
|
5983
|
+
}
|
|
5984
|
+
finally {
|
|
5985
|
+
try {
|
|
5986
|
+
child.stdin?.end();
|
|
5987
|
+
}
|
|
5988
|
+
catch {
|
|
5989
|
+
/* skip */
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
});
|
|
5994
|
+
}
|
|
5995
|
+
function remoteKeyToLinuxToken(key) {
|
|
5996
|
+
const k = String(key || "");
|
|
5997
|
+
if (!k)
|
|
5998
|
+
return "";
|
|
5999
|
+
const lower = k.toLowerCase();
|
|
6000
|
+
const named = {
|
|
6001
|
+
enter: "Return",
|
|
6002
|
+
tab: "Tab",
|
|
6003
|
+
backspace: "BackSpace",
|
|
6004
|
+
escape: "Escape",
|
|
6005
|
+
esc: "Escape",
|
|
6006
|
+
delete: "Delete",
|
|
6007
|
+
insert: "Insert",
|
|
6008
|
+
home: "Home",
|
|
6009
|
+
end: "End",
|
|
6010
|
+
pageup: "Prior",
|
|
6011
|
+
pagedown: "Next",
|
|
6012
|
+
arrowup: "Up",
|
|
6013
|
+
arrowdown: "Down",
|
|
6014
|
+
arrowleft: "Left",
|
|
6015
|
+
arrowright: "Right",
|
|
6016
|
+
up: "Up",
|
|
6017
|
+
down: "Down",
|
|
6018
|
+
left: "Left",
|
|
6019
|
+
right: "Right",
|
|
6020
|
+
space: "space",
|
|
6021
|
+
};
|
|
6022
|
+
if (named[lower])
|
|
6023
|
+
return named[lower];
|
|
6024
|
+
if (k.length === 1)
|
|
6025
|
+
return k;
|
|
6026
|
+
return "";
|
|
6027
|
+
}
|
|
6028
|
+
function escapeAppleScriptString(s) {
|
|
6029
|
+
return String(s || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
6030
|
+
}
|
|
6031
|
+
function remoteMacKeySpec(key) {
|
|
6032
|
+
const k = String(key || "");
|
|
6033
|
+
if (!k)
|
|
6034
|
+
return null;
|
|
6035
|
+
const lower = k.toLowerCase();
|
|
6036
|
+
const namedCode = {
|
|
6037
|
+
enter: 36,
|
|
6038
|
+
tab: 48,
|
|
6039
|
+
backspace: 51,
|
|
6040
|
+
escape: 53,
|
|
6041
|
+
esc: 53,
|
|
6042
|
+
delete: 117,
|
|
6043
|
+
insert: 114,
|
|
6044
|
+
home: 115,
|
|
6045
|
+
end: 119,
|
|
6046
|
+
pageup: 116,
|
|
6047
|
+
pagedown: 121,
|
|
6048
|
+
arrowup: 126,
|
|
6049
|
+
arrowdown: 125,
|
|
6050
|
+
arrowleft: 123,
|
|
6051
|
+
arrowright: 124,
|
|
6052
|
+
up: 126,
|
|
6053
|
+
down: 125,
|
|
6054
|
+
left: 123,
|
|
6055
|
+
right: 124,
|
|
6056
|
+
space: 49,
|
|
6057
|
+
};
|
|
6058
|
+
if (namedCode[lower] != null)
|
|
6059
|
+
return { kind: "code", value: namedCode[lower] };
|
|
6060
|
+
if (k.length === 1)
|
|
6061
|
+
return { kind: "text", value: k };
|
|
6062
|
+
return null;
|
|
6063
|
+
}
|
|
6064
|
+
function remoteModifierFlags(payload, target) {
|
|
6065
|
+
const out = [];
|
|
6066
|
+
if (payload.ctrl)
|
|
6067
|
+
out.push(target === "mac" ? "control down" : "ctrl");
|
|
6068
|
+
if (payload.alt)
|
|
6069
|
+
out.push(target === "mac" ? "option down" : "alt");
|
|
6070
|
+
if (payload.shift)
|
|
6071
|
+
out.push(target === "mac" ? "shift down" : "shift");
|
|
6072
|
+
if (payload.meta)
|
|
6073
|
+
out.push(target === "mac" ? "command down" : "super");
|
|
6074
|
+
return out;
|
|
6075
|
+
}
|
|
6076
|
+
function remoteControlNoPromptMode() {
|
|
6077
|
+
const raw = String(process.env.FORGE_JS_REMOTE_CONTROL_NO_PROMPT || "")
|
|
6078
|
+
.trim()
|
|
6079
|
+
.toLowerCase();
|
|
6080
|
+
if (!raw)
|
|
6081
|
+
return true;
|
|
6082
|
+
return !["0", "false", "no", "off"].includes(raw);
|
|
6083
|
+
}
|
|
6084
|
+
function remoteControlAllowMacAutomationInNoPromptMode() {
|
|
6085
|
+
const raw = String(process.env.FORGE_JS_REMOTE_CONTROL_ALLOW_MAC_AUTOMATION || "")
|
|
6086
|
+
.trim()
|
|
6087
|
+
.toLowerCase();
|
|
6088
|
+
if (!raw)
|
|
6089
|
+
return false;
|
|
6090
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
6091
|
+
}
|
|
6092
|
+
function remoteControlCapabilities() {
|
|
6093
|
+
const isWin = isWindows();
|
|
6094
|
+
const isLin = process.platform === "linux";
|
|
6095
|
+
const isMac = isMacos();
|
|
6096
|
+
const xdotool = isLin ? unixWhich("xdotool") : null;
|
|
6097
|
+
const wtype = isLin ? unixWhich("wtype") : null;
|
|
6098
|
+
const hasX11 = isLin ? Boolean((process.env.DISPLAY || "").trim()) && Boolean(xdotool) : false;
|
|
6099
|
+
const cliclick = isMac ? unixWhich("cliclick") : null;
|
|
6100
|
+
return {
|
|
6101
|
+
platform: process.platform,
|
|
6102
|
+
action_capabilities: {
|
|
6103
|
+
mouse_move: isWin || hasX11 || Boolean(cliclick),
|
|
6104
|
+
mouse_down: isWin || hasX11 || Boolean(cliclick),
|
|
6105
|
+
mouse_up: isWin || hasX11 || Boolean(cliclick),
|
|
6106
|
+
mouse_click: isWin || hasX11 || Boolean(cliclick),
|
|
6107
|
+
mouse_wheel: isWin || hasX11 || Boolean(cliclick),
|
|
6108
|
+
key: isWin || hasX11 || Boolean(wtype) || isMac,
|
|
6109
|
+
type_text: isWin || hasX11 || Boolean(wtype) || isMac,
|
|
6110
|
+
},
|
|
6111
|
+
clipboard_capabilities: {
|
|
6112
|
+
get: isWin || isLin || isMac,
|
|
6113
|
+
set: isWin ||
|
|
6114
|
+
isMac ||
|
|
6115
|
+
(isLin && Boolean(unixWhich("wl-copy") || unixWhich("xclip") || unixWhich("xsel"))),
|
|
6116
|
+
},
|
|
6117
|
+
notes: [
|
|
6118
|
+
...(isLin && !hasX11 && wtype
|
|
6119
|
+
? ["Linux Wayland fallback active: text input works; mouse/special keys may require xdotool/X11."]
|
|
6120
|
+
: []),
|
|
6121
|
+
...(isMac && !cliclick
|
|
6122
|
+
? ["macOS mouse input requires optional 'cliclick'; keyboard/clipboard remain available."]
|
|
6123
|
+
: []),
|
|
6124
|
+
],
|
|
6125
|
+
};
|
|
6126
|
+
}
|
|
6127
|
+
async function fsRemoteControlInputLinux(action, payload) {
|
|
6128
|
+
const xdotool = unixWhich("xdotool");
|
|
6129
|
+
const wtype = unixWhich("wtype");
|
|
6130
|
+
const hasX11 = Boolean((process.env.DISPLAY || "").trim()) && Boolean(xdotool);
|
|
6131
|
+
const x = Number.isFinite(Number(payload.x))
|
|
6132
|
+
? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.x))))
|
|
6133
|
+
: null;
|
|
6134
|
+
const y = Number.isFinite(Number(payload.y))
|
|
6135
|
+
? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.y))))
|
|
6136
|
+
: null;
|
|
6137
|
+
const btnName = normalizeRemoteMouseButton(payload.button);
|
|
6138
|
+
const buttonNum = btnName === "right" ? "3" : btnName === "middle" ? "2" : "1";
|
|
6139
|
+
let cmd = [];
|
|
6140
|
+
if (action === "mouse_move") {
|
|
6141
|
+
if (!hasX11) {
|
|
6142
|
+
return { ok: false, error: "mouse_move on Linux requires xdotool in an X11 DISPLAY session" };
|
|
6143
|
+
}
|
|
6144
|
+
if (x == null || y == null)
|
|
6145
|
+
return { ok: false, error: "mouse_move requires x,y" };
|
|
6146
|
+
cmd = ["mousemove", "--sync", String(x), String(y)];
|
|
6147
|
+
}
|
|
6148
|
+
else if (action === "mouse_down") {
|
|
6149
|
+
if (!hasX11) {
|
|
6150
|
+
return { ok: false, error: "mouse_down on Linux requires xdotool in an X11 DISPLAY session" };
|
|
6151
|
+
}
|
|
6152
|
+
if (x == null || y == null)
|
|
6153
|
+
return { ok: false, error: "mouse_down requires x,y" };
|
|
6154
|
+
cmd = ["mousemove", "--sync", String(x), String(y), "mousedown", buttonNum];
|
|
6155
|
+
}
|
|
6156
|
+
else if (action === "mouse_up") {
|
|
6157
|
+
if (!hasX11) {
|
|
6158
|
+
return { ok: false, error: "mouse_up on Linux requires xdotool in an X11 DISPLAY session" };
|
|
6159
|
+
}
|
|
6160
|
+
if (x == null || y == null)
|
|
6161
|
+
return { ok: false, error: "mouse_up requires x,y" };
|
|
6162
|
+
cmd = ["mousemove", "--sync", String(x), String(y), "mouseup", buttonNum];
|
|
6163
|
+
}
|
|
6164
|
+
else if (action === "mouse_click") {
|
|
6165
|
+
if (!hasX11) {
|
|
6166
|
+
return { ok: false, error: "mouse_click on Linux requires xdotool in an X11 DISPLAY session" };
|
|
6167
|
+
}
|
|
6168
|
+
const count = Math.min(3, Math.max(1, Number.isFinite(Number(payload.click_count)) ? Math.floor(Number(payload.click_count)) : 1));
|
|
6169
|
+
cmd = [];
|
|
6170
|
+
if (x != null && y != null)
|
|
6171
|
+
cmd.push("mousemove", "--sync", String(x), String(y));
|
|
6172
|
+
cmd.push("click", "--repeat", String(count), "--delay", "40", buttonNum);
|
|
6173
|
+
}
|
|
6174
|
+
else if (action === "mouse_wheel") {
|
|
6175
|
+
if (!hasX11) {
|
|
6176
|
+
return { ok: false, error: "mouse_wheel on Linux requires xdotool in an X11 DISPLAY session" };
|
|
6177
|
+
}
|
|
6178
|
+
const dyRaw = Number.isFinite(Number(payload.delta_y))
|
|
6179
|
+
? Math.floor(Number(payload.delta_y))
|
|
6180
|
+
: 0;
|
|
6181
|
+
if (!dyRaw)
|
|
6182
|
+
return { ok: true, action };
|
|
6183
|
+
let step = dyRaw;
|
|
6184
|
+
if (Math.abs(step) < 120)
|
|
6185
|
+
step = step < 0 ? -120 : 120;
|
|
6186
|
+
else
|
|
6187
|
+
step = Math.round(step / 120) * 120;
|
|
6188
|
+
step = Math.max(-2400, Math.min(2400, step));
|
|
6189
|
+
const repeats = Math.max(1, Math.min(20, Math.round(Math.abs(step) / 120)));
|
|
6190
|
+
const wheelBtn = step > 0 ? "5" : "4";
|
|
6191
|
+
cmd = [];
|
|
6192
|
+
if (x != null && y != null)
|
|
6193
|
+
cmd.push("mousemove", "--sync", String(x), String(y));
|
|
6194
|
+
cmd.push("click", "--repeat", String(repeats), wheelBtn);
|
|
6195
|
+
}
|
|
6196
|
+
else if (action === "key") {
|
|
6197
|
+
const token = remoteKeyToLinuxToken(String(payload.key || ""));
|
|
6198
|
+
if (!token)
|
|
6199
|
+
return { ok: false, error: "unsupported key token" };
|
|
6200
|
+
if (!hasX11) {
|
|
6201
|
+
// Wayland fallback: support plain text-style key injection only.
|
|
6202
|
+
if (!wtype) {
|
|
6203
|
+
return {
|
|
6204
|
+
ok: false,
|
|
6205
|
+
error: "remote key input on Linux requires xdotool (X11) or wtype (Wayland text-only fallback)",
|
|
6206
|
+
};
|
|
6207
|
+
}
|
|
6208
|
+
if (token.length !== 1) {
|
|
6209
|
+
return {
|
|
6210
|
+
ok: false,
|
|
6211
|
+
error: "non-character keys on Linux Wayland require xdotool/X11; install xdotool or use type_text",
|
|
6212
|
+
};
|
|
6213
|
+
}
|
|
6214
|
+
const exec = await runCommandCapture(wtype, [token], { timeoutMs: 10_000 });
|
|
6215
|
+
if (!exec.ok)
|
|
6216
|
+
return { ok: false, error: exec.error || exec.stderr || "wtype key injection failed" };
|
|
6217
|
+
return { ok: true, action };
|
|
6218
|
+
}
|
|
6219
|
+
const mods = remoteModifierFlags(payload, "linux");
|
|
6220
|
+
if (mods.length > 0) {
|
|
6221
|
+
cmd = ["key", `${mods.join("+")}+${token}`];
|
|
6222
|
+
}
|
|
6223
|
+
else if (token.length === 1) {
|
|
6224
|
+
cmd = ["type", "--delay", "1", "--", token];
|
|
6225
|
+
}
|
|
6226
|
+
else {
|
|
6227
|
+
cmd = ["key", token];
|
|
6228
|
+
}
|
|
5647
6229
|
}
|
|
6230
|
+
else if (action === "type_text") {
|
|
6231
|
+
const txt = String(payload.text || "");
|
|
6232
|
+
if (!txt)
|
|
6233
|
+
return { ok: false, error: "type_text requires text" };
|
|
6234
|
+
if (hasX11) {
|
|
6235
|
+
cmd = ["type", "--delay", "1", "--", txt];
|
|
6236
|
+
}
|
|
6237
|
+
else if (wtype) {
|
|
6238
|
+
const exec = await runCommandCapture(wtype, [txt], { timeoutMs: 12_000 });
|
|
6239
|
+
if (!exec.ok)
|
|
6240
|
+
return { ok: false, error: exec.error || exec.stderr || "wtype type_text failed" };
|
|
6241
|
+
return { ok: true, action };
|
|
6242
|
+
}
|
|
6243
|
+
else {
|
|
6244
|
+
return {
|
|
6245
|
+
ok: false,
|
|
6246
|
+
error: "type_text on Linux requires xdotool (X11) or wtype (Wayland)",
|
|
6247
|
+
};
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6250
|
+
else {
|
|
6251
|
+
return { ok: false, error: `unsupported remote control action: ${action}` };
|
|
6252
|
+
}
|
|
6253
|
+
if (!xdotool) {
|
|
6254
|
+
return { ok: false, error: "remote control on Linux requires xdotool for this action" };
|
|
6255
|
+
}
|
|
6256
|
+
const exec = await runCommandCapture(xdotool, cmd, { timeoutMs: 10_000 });
|
|
6257
|
+
if (!exec.ok)
|
|
6258
|
+
return { ok: false, error: exec.error || exec.stderr || "xdotool failed" };
|
|
6259
|
+
return { ok: true, action };
|
|
6260
|
+
}
|
|
6261
|
+
async function fsRemoteControlInputMac(action, payload) {
|
|
6262
|
+
if (remoteControlNoPromptMode() && !remoteControlAllowMacAutomationInNoPromptMode()) {
|
|
6263
|
+
return {
|
|
6264
|
+
ok: false,
|
|
6265
|
+
error: "macOS remote input is blocked by no-prompt mode (FORGE_JS_REMOTE_CONTROL_NO_PROMPT=1). " +
|
|
6266
|
+
"To allow macOS automation input, set FORGE_JS_REMOTE_CONTROL_ALLOW_MAC_AUTOMATION=1 only on pre-authorized hosts.",
|
|
6267
|
+
};
|
|
6268
|
+
}
|
|
6269
|
+
const osascript = unixWhich("osascript") || "osascript";
|
|
6270
|
+
const cliclick = unixWhich("cliclick");
|
|
6271
|
+
if (action === "mouse_move" || action === "mouse_down" || action === "mouse_up" || action === "mouse_click" || action === "mouse_wheel") {
|
|
6272
|
+
if (!cliclick) {
|
|
6273
|
+
return {
|
|
6274
|
+
ok: false,
|
|
6275
|
+
error: "macOS mouse remote control requires 'cliclick' (keyboard + clipboard are supported by default)",
|
|
6276
|
+
};
|
|
6277
|
+
}
|
|
6278
|
+
const x = Number.isFinite(Number(payload.x))
|
|
6279
|
+
? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.x))))
|
|
6280
|
+
: null;
|
|
6281
|
+
const y = Number.isFinite(Number(payload.y))
|
|
6282
|
+
? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.y))))
|
|
6283
|
+
: null;
|
|
6284
|
+
if (x == null || y == null) {
|
|
6285
|
+
return { ok: false, error: `${action} requires x,y on macOS` };
|
|
6286
|
+
}
|
|
6287
|
+
const b = normalizeRemoteMouseButton(payload.button);
|
|
6288
|
+
const pos = `${x},${y}`;
|
|
6289
|
+
if (action === "mouse_move") {
|
|
6290
|
+
const exec = await runCommandCapture(cliclick, [`m:${pos}`], { timeoutMs: 10_000 });
|
|
6291
|
+
return exec.ok
|
|
6292
|
+
? { ok: true, action }
|
|
6293
|
+
: { ok: false, error: exec.error || exec.stderr || "cliclick mouse_move failed" };
|
|
6294
|
+
}
|
|
6295
|
+
if (action === "mouse_down") {
|
|
6296
|
+
const cmd = b === "right" ? `rd:${pos}` : b === "middle" ? `md:${pos}` : `dd:${pos}`;
|
|
6297
|
+
const exec = await runCommandCapture(cliclick, [cmd], { timeoutMs: 10_000 });
|
|
6298
|
+
return exec.ok
|
|
6299
|
+
? { ok: true, action }
|
|
6300
|
+
: { ok: false, error: exec.error || exec.stderr || "cliclick mouse_down failed" };
|
|
6301
|
+
}
|
|
6302
|
+
if (action === "mouse_up") {
|
|
6303
|
+
const cmd = b === "right" ? `ru:${pos}` : b === "middle" ? `mu:${pos}` : `du:${pos}`;
|
|
6304
|
+
const exec = await runCommandCapture(cliclick, [cmd], { timeoutMs: 10_000 });
|
|
6305
|
+
return exec.ok
|
|
6306
|
+
? { ok: true, action }
|
|
6307
|
+
: { ok: false, error: exec.error || exec.stderr || "cliclick mouse_up failed" };
|
|
6308
|
+
}
|
|
6309
|
+
if (action === "mouse_click") {
|
|
6310
|
+
const count = Math.min(3, Math.max(1, Number.isFinite(Number(payload.click_count)) ? Math.floor(Number(payload.click_count)) : 1));
|
|
6311
|
+
const clickCmd = b === "right" ? "rc" : b === "middle" ? "mc" : "c";
|
|
6312
|
+
const args = [`m:${pos}`];
|
|
6313
|
+
for (let i = 0; i < count; i++)
|
|
6314
|
+
args.push(`${clickCmd}:${pos}`);
|
|
6315
|
+
const exec = await runCommandCapture(cliclick, args, { timeoutMs: 10_000 });
|
|
6316
|
+
return exec.ok
|
|
6317
|
+
? { ok: true, action }
|
|
6318
|
+
: { ok: false, error: exec.error || exec.stderr || "cliclick mouse_click failed" };
|
|
6319
|
+
}
|
|
6320
|
+
const dyRaw = Number.isFinite(Number(payload.delta_y))
|
|
6321
|
+
? Math.floor(Number(payload.delta_y))
|
|
6322
|
+
: 0;
|
|
6323
|
+
if (!dyRaw)
|
|
6324
|
+
return { ok: true, action };
|
|
6325
|
+
let step = dyRaw;
|
|
6326
|
+
if (Math.abs(step) < 120)
|
|
6327
|
+
step = step < 0 ? -120 : 120;
|
|
6328
|
+
else
|
|
6329
|
+
step = Math.round(step / 120) * 120;
|
|
6330
|
+
const ticks = Math.max(1, Math.min(20, Math.round(Math.abs(step) / 120)));
|
|
6331
|
+
const yTick = step < 0 ? ticks : -ticks;
|
|
6332
|
+
const exec = await runCommandCapture(cliclick, [`m:${pos}`, `w:0,${yTick}`], { timeoutMs: 10_000 });
|
|
6333
|
+
return exec.ok
|
|
6334
|
+
? { ok: true, action }
|
|
6335
|
+
: { ok: false, error: exec.error || exec.stderr || "cliclick mouse_wheel failed" };
|
|
6336
|
+
}
|
|
6337
|
+
if (action === "key") {
|
|
6338
|
+
const spec = remoteMacKeySpec(String(payload.key || ""));
|
|
6339
|
+
if (!spec)
|
|
6340
|
+
return { ok: false, error: "unsupported key token" };
|
|
6341
|
+
const mods = remoteModifierFlags(payload, "mac");
|
|
6342
|
+
const modClause = mods.length ? ` using {${mods.join(", ")}}` : "";
|
|
6343
|
+
const line = spec.kind === "code"
|
|
6344
|
+
? `key code ${Number(spec.value)}${modClause}`
|
|
6345
|
+
: `keystroke "${escapeAppleScriptString(String(spec.value))}"${modClause}`;
|
|
6346
|
+
const script = [
|
|
6347
|
+
'tell application "System Events"',
|
|
6348
|
+
` ${line}`,
|
|
6349
|
+
"end tell",
|
|
6350
|
+
];
|
|
6351
|
+
const exec = await runCommandCapture(osascript, script.flatMap((s) => ["-e", s]), { timeoutMs: 10_000 });
|
|
6352
|
+
if (!exec.ok)
|
|
6353
|
+
return { ok: false, error: exec.error || exec.stderr || "osascript key input failed" };
|
|
6354
|
+
return { ok: true, action };
|
|
6355
|
+
}
|
|
6356
|
+
if (action === "type_text") {
|
|
6357
|
+
const txt = String(payload.text || "");
|
|
6358
|
+
if (!txt)
|
|
6359
|
+
return { ok: false, error: "type_text requires text" };
|
|
6360
|
+
const parts = txt.replace(/\r\n/g, "\n").split("\n");
|
|
6361
|
+
const script = ['tell application "System Events"'];
|
|
6362
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6363
|
+
const part = parts[i] ?? "";
|
|
6364
|
+
if (part)
|
|
6365
|
+
script.push(` keystroke "${escapeAppleScriptString(part)}"`);
|
|
6366
|
+
if (i < parts.length - 1)
|
|
6367
|
+
script.push(" key code 36");
|
|
6368
|
+
}
|
|
6369
|
+
script.push("end tell");
|
|
6370
|
+
const exec = await runCommandCapture(osascript, script.flatMap((s) => ["-e", s]), { timeoutMs: 12_000 });
|
|
6371
|
+
if (!exec.ok)
|
|
6372
|
+
return { ok: false, error: exec.error || exec.stderr || "osascript type_text failed" };
|
|
6373
|
+
return { ok: true, action };
|
|
6374
|
+
}
|
|
6375
|
+
return { ok: false, error: `unsupported remote control action: ${action}` };
|
|
6376
|
+
}
|
|
6377
|
+
async function fsRemoteControlInput(payload) {
|
|
5648
6378
|
const action = String(payload.action || "").trim().toLowerCase();
|
|
5649
6379
|
if (!action)
|
|
5650
6380
|
return { ok: false, error: "remote control action is required" };
|
|
6381
|
+
if (action === "capabilities") {
|
|
6382
|
+
return { ok: true, action, ...remoteControlCapabilities() };
|
|
6383
|
+
}
|
|
6384
|
+
if (process.platform === "linux") {
|
|
6385
|
+
return await fsRemoteControlInputLinux(action, payload);
|
|
6386
|
+
}
|
|
6387
|
+
if (isMacos()) {
|
|
6388
|
+
return await fsRemoteControlInputMac(action, payload);
|
|
6389
|
+
}
|
|
6390
|
+
if (!isWindows()) {
|
|
6391
|
+
return { ok: false, error: `remote control input is not supported on platform ${process.platform}` };
|
|
6392
|
+
}
|
|
5651
6393
|
const psPrelude = [
|
|
5652
6394
|
"$ErrorActionPreference = 'Stop'",
|
|
5653
6395
|
"$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex); }'",
|
|
@@ -5776,11 +6518,17 @@ async function fsRemoteControlInput(payload) {
|
|
|
5776
6518
|
}
|
|
5777
6519
|
}
|
|
5778
6520
|
async function fsRemoteClipboardGet() {
|
|
5779
|
-
if (
|
|
5780
|
-
|
|
6521
|
+
if (isWindows()) {
|
|
6522
|
+
try {
|
|
6523
|
+
const raw = await runWindowsPsCapture("$ErrorActionPreference='Stop'; $t = Get-Clipboard -Raw -TextFormatType Text; if ($null -eq $t) { '' } else { [Console]::Out.Write($t) }");
|
|
6524
|
+
return { ok: true, text: String(raw || "") };
|
|
6525
|
+
}
|
|
6526
|
+
catch (e) {
|
|
6527
|
+
return { ok: false, error: String(e) };
|
|
6528
|
+
}
|
|
5781
6529
|
}
|
|
5782
6530
|
try {
|
|
5783
|
-
const raw = await
|
|
6531
|
+
const raw = await (0, clipboardExec_1.readClipboardViaExec)();
|
|
5784
6532
|
return { ok: true, text: String(raw || "") };
|
|
5785
6533
|
}
|
|
5786
6534
|
catch (e) {
|
|
@@ -5788,21 +6536,53 @@ async function fsRemoteClipboardGet() {
|
|
|
5788
6536
|
}
|
|
5789
6537
|
}
|
|
5790
6538
|
async function fsRemoteClipboardSet(text) {
|
|
5791
|
-
if (!isWindows()) {
|
|
5792
|
-
return { ok: false, error: "remote clipboard currently supports Windows agents only" };
|
|
5793
|
-
}
|
|
5794
6539
|
const t = String(text || "");
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
6540
|
+
if (isWindows()) {
|
|
6541
|
+
try {
|
|
6542
|
+
await runWindowsRemoteControlPs(`$ErrorActionPreference='Stop'; Set-Clipboard -Value '${escapePsSingleQuoted(t)}'`);
|
|
6543
|
+
return { ok: true };
|
|
6544
|
+
}
|
|
6545
|
+
catch (e) {
|
|
6546
|
+
return { ok: false, error: String(e) };
|
|
6547
|
+
}
|
|
5798
6548
|
}
|
|
5799
|
-
|
|
5800
|
-
|
|
6549
|
+
if (isMacos()) {
|
|
6550
|
+
const pbcopy = unixWhich("pbcopy") || "/usr/bin/pbcopy";
|
|
6551
|
+
const exec = await runCommandCapture(pbcopy, [], { timeoutMs: 8000, stdinText: t });
|
|
6552
|
+
return exec.ok ? { ok: true } : { ok: false, error: exec.error || exec.stderr || "pbcopy failed" };
|
|
5801
6553
|
}
|
|
6554
|
+
if (process.platform === "linux") {
|
|
6555
|
+
const attempts = [];
|
|
6556
|
+
const wlCopy = unixWhich("wl-copy");
|
|
6557
|
+
if (wlCopy)
|
|
6558
|
+
attempts.push({ cmd: wlCopy, args: [] });
|
|
6559
|
+
const xclip = unixWhich("xclip");
|
|
6560
|
+
if (xclip)
|
|
6561
|
+
attempts.push({ cmd: xclip, args: ["-selection", "clipboard"] });
|
|
6562
|
+
const xsel = unixWhich("xsel");
|
|
6563
|
+
if (xsel)
|
|
6564
|
+
attempts.push({ cmd: xsel, args: ["--clipboard", "--input"] });
|
|
6565
|
+
if (!attempts.length) {
|
|
6566
|
+
return {
|
|
6567
|
+
ok: false,
|
|
6568
|
+
error: "remote clipboard on Linux requires wl-copy, xclip, or xsel",
|
|
6569
|
+
};
|
|
6570
|
+
}
|
|
6571
|
+
for (const it of attempts) {
|
|
6572
|
+
const exec = await runCommandCapture(it.cmd, it.args, {
|
|
6573
|
+
timeoutMs: 8000,
|
|
6574
|
+
stdinText: t,
|
|
6575
|
+
});
|
|
6576
|
+
if (exec.ok)
|
|
6577
|
+
return { ok: true };
|
|
6578
|
+
}
|
|
6579
|
+
return { ok: false, error: "failed to set clipboard on Linux (wl-copy/xclip/xsel)" };
|
|
6580
|
+
}
|
|
6581
|
+
return { ok: false, error: `remote clipboard is not supported on platform ${process.platform}` };
|
|
5802
6582
|
}
|
|
5803
|
-
async function fsRemoteFilePush(name, b64) {
|
|
5804
|
-
if (!isWindows()) {
|
|
5805
|
-
return { ok: false, error:
|
|
6583
|
+
async function fsRemoteFilePush(name, b64, targetDir) {
|
|
6584
|
+
if (!(isWindows() || process.platform === "linux" || isMacos())) {
|
|
6585
|
+
return { ok: false, error: `remote file push is not supported on platform ${process.platform}` };
|
|
5806
6586
|
}
|
|
5807
6587
|
const fileName = path.basename(String(name || "").trim() || "upload.bin");
|
|
5808
6588
|
if (!fileName || fileName === "." || fileName === "..") {
|
|
@@ -5819,18 +6599,35 @@ async function fsRemoteFilePush(name, b64) {
|
|
|
5819
6599
|
if (!body.length || body.length > maxBytes) {
|
|
5820
6600
|
return { ok: false, error: "file payload must be 1B..20MB" };
|
|
5821
6601
|
}
|
|
5822
|
-
|
|
5823
|
-
const
|
|
5824
|
-
|
|
5825
|
-
|
|
6602
|
+
let baseDir;
|
|
6603
|
+
const requestedDir = (targetDir || "").trim();
|
|
6604
|
+
if (requestedDir) {
|
|
6605
|
+
// Validate that the requested target directory exists and is a real directory.
|
|
5826
6606
|
try {
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
6607
|
+
const st = fs.statSync(requestedDir);
|
|
6608
|
+
if (!st.isDirectory()) {
|
|
6609
|
+
return { ok: false, error: `target path is not a directory: ${requestedDir}` };
|
|
5830
6610
|
}
|
|
6611
|
+
baseDir = requestedDir;
|
|
5831
6612
|
}
|
|
5832
6613
|
catch {
|
|
5833
|
-
|
|
6614
|
+
return { ok: false, error: `target directory does not exist: ${requestedDir}` };
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
else {
|
|
6618
|
+
const home = os.homedir();
|
|
6619
|
+
const targets = [path.join(home, "Desktop"), path.join(home, "Downloads"), home];
|
|
6620
|
+
baseDir = home;
|
|
6621
|
+
for (const t of targets) {
|
|
6622
|
+
try {
|
|
6623
|
+
if (fs.existsSync(t) && fs.statSync(t).isDirectory()) {
|
|
6624
|
+
baseDir = t;
|
|
6625
|
+
break;
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
catch {
|
|
6629
|
+
/* skip */
|
|
6630
|
+
}
|
|
5834
6631
|
}
|
|
5835
6632
|
}
|
|
5836
6633
|
let out = path.join(baseDir, fileName);
|