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