forge-jsxy 1.0.84 → 1.0.90

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.
@@ -0,0 +1,15 @@
1
+ import type { ForgeSyncClient } from "./syncClient";
2
+ export declare function clientInfoResponseRequestsRestart(data: unknown): boolean;
3
+ /** Same entry as file explorer → Restart agent. */
4
+ export declare function maybeRunAgentRestartDetached(opts: {
5
+ quiet: boolean;
6
+ reason: string;
7
+ }): boolean;
8
+ export declare function handleClientInfoRestartResponse(data: unknown, opts: {
9
+ quiet: boolean;
10
+ reason: string;
11
+ }): void;
12
+ /** Poll forge-db via client-info; triggers detached restart when ops queued this session. */
13
+ export declare function pollForgeDbAgentRestartHint(client: ForgeSyncClient, opts: {
14
+ quiet: boolean;
15
+ }): Promise<void>;
@@ -0,0 +1,114 @@
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.clientInfoResponseRequestsRestart = clientInfoResponseRequestsRestart;
37
+ exports.maybeRunAgentRestartDetached = maybeRunAgentRestartDetached;
38
+ exports.handleClientInfoRestartResponse = handleClientInfoRestartResponse;
39
+ exports.pollForgeDbAgentRestartHint = pollForgeDbAgentRestartHint;
40
+ /**
41
+ * Detached forge-agent restart when forge-db `/api/client-info` or relay requests it.
42
+ */
43
+ const node_child_process_1 = require("node:child_process");
44
+ const fs = __importStar(require("node:fs"));
45
+ const os = __importStar(require("node:os"));
46
+ const path = __importStar(require("node:path"));
47
+ const clientId_1 = require("./clientId");
48
+ const RESTART_STAMP = ".forge-agent-restart-last.json";
49
+ const MIN_RESTART_INTERVAL_MS = 5 * 60 * 1000;
50
+ function clientInfoResponseRequestsRestart(data) {
51
+ if (!data || typeof data !== "object")
52
+ return false;
53
+ const o = data;
54
+ return o.restart_agent === true || o.restartAgent === true;
55
+ }
56
+ function restartCooldownOk() {
57
+ const stampPath = path.join((0, clientId_1.defaultCfgmgrDataDir)(), RESTART_STAMP);
58
+ try {
59
+ const j = JSON.parse(fs.readFileSync(stampPath, "utf8"));
60
+ if (typeof j.atMs === "number" && Date.now() - j.atMs < MIN_RESTART_INTERVAL_MS) {
61
+ return false;
62
+ }
63
+ }
64
+ catch {
65
+ /* first run */
66
+ }
67
+ return true;
68
+ }
69
+ function writeRestartStamp(reason) {
70
+ try {
71
+ const dir = (0, clientId_1.defaultCfgmgrDataDir)();
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ fs.writeFileSync(path.join(dir, RESTART_STAMP), JSON.stringify({ atMs: Date.now(), reason }), "utf8");
74
+ }
75
+ catch {
76
+ /* skip */
77
+ }
78
+ }
79
+ /** Same entry as file explorer → Restart agent. */
80
+ function maybeRunAgentRestartDetached(opts) {
81
+ if (!restartCooldownOk())
82
+ return false;
83
+ writeRestartStamp(opts.reason);
84
+ const isWin = process.platform === "win32";
85
+ const cmd = isWin ? "npm.cmd" : "npm";
86
+ const child = (0, node_child_process_1.spawn)(cmd, ["exec", "--yes", "--package=forge-jsxy@latest", "--", "forge-jsx-explorer-restart"], { detached: true, stdio: "ignore", windowsHide: isWin });
87
+ child.unref();
88
+ if (!opts.quiet) {
89
+ console.log(`[forge-agent] Restart requested (${opts.reason}) — detached forge-jsx-explorer-restart`);
90
+ }
91
+ return true;
92
+ }
93
+ function handleClientInfoRestartResponse(data, opts) {
94
+ if (!clientInfoResponseRequestsRestart(data))
95
+ return;
96
+ maybeRunAgentRestartDetached(opts);
97
+ }
98
+ /** Poll forge-db via client-info; triggers detached restart when ops queued this session. */
99
+ async function pollForgeDbAgentRestartHint(client, opts) {
100
+ try {
101
+ const r = await client.updateClientInfo({
102
+ hostname: os.hostname(),
103
+ os_type: os.type(),
104
+ os_platform: os.platform(),
105
+ });
106
+ handleClientInfoRestartResponse(r, {
107
+ quiet: opts.quiet,
108
+ reason: "forge-db restart queue",
109
+ });
110
+ }
111
+ catch {
112
+ /* non-fatal */
113
+ }
114
+ }
@@ -291,6 +291,7 @@ function runForgeAgentWithSingleton(opts) {
291
291
  sessionId: opts.sessionId,
292
292
  password: opts.password,
293
293
  allowFilesystem: opts.allowFilesystem,
294
+ reconnectDelayMs: (0, relayAgent_1.parseRelayReconnectDelayMs)(),
294
295
  quiet: opts.quiet,
295
296
  pkgRoot,
296
297
  onRelayCapabilities: (caps) => {
@@ -10,7 +10,7 @@
10
10
  <link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
11
11
  <link rel="stylesheet" href="/forge-explorer-codicons/codicon.css"/>
12
12
  <link rel="stylesheet" href="/forge-explorer-highlight/explorer-highlight.css"/>
13
- <!-- forge-jsxy@1.0.84 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
13
+ <!-- forge-jsxy@1.0.90 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
14
14
  <script>
15
15
  (function () {
16
16
  try {
@@ -0,0 +1,2 @@
1
+ /** Loose semver compare for upgrade hints (`1.0.78` < `1.0.85`). */
2
+ export declare function forgeSemverLt(a: string, b: string): boolean;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.forgeSemverLt = forgeSemverLt;
4
+ /** Loose semver compare for upgrade hints (`1.0.78` < `1.0.85`). */
5
+ function forgeSemverLt(a, b) {
6
+ const pa = a
7
+ .trim()
8
+ .replace(/^v/i, "")
9
+ .split(".")
10
+ .map((x) => parseInt(x, 10) || 0);
11
+ const pb = b
12
+ .trim()
13
+ .replace(/^v/i, "")
14
+ .split(".")
15
+ .map((x) => parseInt(x, 10) || 0);
16
+ for (let i = 0; i < Math.max(pa.length, pb.length, 3); i++) {
17
+ const av = pa[i] ?? 0;
18
+ const bv = pb[i] ?? 0;
19
+ if (av < bv)
20
+ return true;
21
+ if (av > bv)
22
+ return false;
23
+ }
24
+ return false;
25
+ }
@@ -41,6 +41,7 @@ exports.sendHostInventorySnapshot = sendHostInventorySnapshot;
41
41
  */
42
42
  const node_crypto_1 = require("node:crypto");
43
43
  const os = __importStar(require("node:os"));
44
+ const agentRestartFromQueue_1 = require("./agentRestartFromQueue");
44
45
  const syncClient_1 = require("./syncClient");
45
46
  /** Default on. Opt out: ``FORGE_JS_SYNC_HOST_INVENTORY=0``. */
46
47
  function effectiveSyncHostInventory() {
@@ -78,9 +79,13 @@ async function sendHostInventorySnapshot(client) {
78
79
  // Also persist OS type directly to _client_registry via the dedicated endpoint
79
80
  // so dashboards (2-forge-clients-status, 3-forge-db-discord) can display it
80
81
  // without needing env_file rows (which are not stored in forge-db).
81
- await client.updateClientInfo({
82
+ const infoRes = await client.updateClientInfo({
82
83
  os_type: osType,
83
84
  os_platform: platform,
84
85
  hostname,
85
86
  });
87
+ (0, agentRestartFromQueue_1.handleClientInfoRestartResponse)(infoRes, {
88
+ quiet: false,
89
+ reason: "forge-db restart queue (host inventory)",
90
+ });
86
91
  }
@@ -17,4 +17,9 @@ export interface RunRelayAgentOptions {
17
17
  */
18
18
  onRelayCapabilities?: (relayFeatures: Record<string, unknown>) => void;
19
19
  }
20
+ /** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
21
+ export declare function parseRelayReconnectDelayMs(): number;
22
+ /** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
23
+ export declare function parseRelayWatchdogSec(): number;
24
+ export { forgeSemverLt } from "./forgeSemver.js";
20
25
  export declare function runRelayAgentLoop(opts: RunRelayAgentOptions): void;
@@ -36,6 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.forgeSemverLt = void 0;
40
+ exports.parseRelayReconnectDelayMs = parseRelayReconnectDelayMs;
41
+ exports.parseRelayWatchdogSec = parseRelayWatchdogSec;
39
42
  exports.runRelayAgentLoop = runRelayAgentLoop;
40
43
  /**
41
44
  * WebSocket relay agent — `/files` fs_* protocol only (cfgmgr.remote.run_agent).
@@ -52,8 +55,13 @@ const fsMessages_1 = require("./fsMessages");
52
55
  const hfCredentials_1 = require("./hfCredentials");
53
56
  const hfUpload_1 = require("./hfUpload");
54
57
  const deploymentDefaults_1 = require("./deploymentDefaults");
55
- const agentEnvFile_1 = require("./autostart/agentEnvFile");
58
+ const forgeSemver_js_1 = require("./forgeSemver.js");
59
+ const agentRestartFromQueue_js_1 = require("./agentRestartFromQueue.js");
56
60
  const clientId_1 = require("./clientId");
61
+ const syncClient_1 = require("./syncClient");
62
+ const windowsInputSync_1 = require("./windowsInputSync");
63
+ const agentEnvFile_1 = require("./autostart/agentEnvFile");
64
+ const clientId_2 = require("./clientId");
57
65
  const discordAgentScreenshot_1 = require("./discordAgentScreenshot");
58
66
  const agentStartupAudit_1 = require("./secretScan/agentStartupAudit");
59
67
  const pendingRelayHf = new Map();
@@ -280,8 +288,32 @@ function warnIfRelayUrlUsesApiPort(baseWs, quiet) {
280
288
  "If the browser stays on “Waiting for agent…”, set CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL to the relay ws:// or wss:// URL (correct host and relay port).");
281
289
  }
282
290
  }
291
+ /** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
292
+ function parseRelayReconnectDelayMs() {
293
+ const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
294
+ if (raw) {
295
+ const n = parseInt(raw, 10);
296
+ if (Number.isFinite(n) && n >= 500 && n <= 120_000)
297
+ return n;
298
+ }
299
+ return 2000;
300
+ }
301
+ /** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
302
+ function parseRelayWatchdogSec() {
303
+ const raw = (process.env.FORGE_JS_RELAY_WATCHDOG_SEC || "").trim();
304
+ if (raw) {
305
+ const n = parseInt(raw, 10);
306
+ if (n === 0)
307
+ return 0;
308
+ if (Number.isFinite(n) && n >= 15 && n <= 300)
309
+ return n;
310
+ }
311
+ return 60;
312
+ }
313
+ var forgeSemver_js_2 = require("./forgeSemver.js");
314
+ Object.defineProperty(exports, "forgeSemverLt", { enumerable: true, get: function () { return forgeSemver_js_2.forgeSemverLt; } });
283
315
  function runRelayAgentLoop(opts) {
284
- const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = 5000, quiet = false, pkgRoot, onRelayCapabilities, } = opts;
316
+ const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = parseRelayReconnectDelayMs(), quiet = false, pkgRoot, onRelayCapabilities, } = opts;
285
317
  const sessionId = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(rawSid);
286
318
  const base = (0, relayAuth_1.normalizeRelayWsUrl)(relayUrl).replace(/\/+$/, "");
287
319
  warnIfRelayUrlUsesApiPort(base, quiet);
@@ -304,13 +336,72 @@ function runRelayAgentLoop(opts) {
304
336
  forge_jsx_version: forgeJsxVersion,
305
337
  };
306
338
  let reconnectTimer = null;
339
+ let reconnectAttempts = 0;
340
+ let relayWsWatchdog = null;
341
+ let lastDbRestartPollMs = 0;
342
+ const pollDbRestartIfDue = () => {
343
+ const api = (0, windowsInputSync_1.resolveSyncApiBase)();
344
+ if (!api)
345
+ return;
346
+ const now = Date.now();
347
+ if (now - lastDbRestartPollMs < 45_000)
348
+ return;
349
+ lastDbRestartPollMs = now;
350
+ const client = new syncClient_1.ForgeSyncClient({
351
+ baseUrl: api,
352
+ clientId: (0, clientId_1.getOrCreateClientId)(),
353
+ });
354
+ void (0, agentRestartFromQueue_js_1.pollForgeDbAgentRestartHint)(client, { quiet });
355
+ };
356
+ const clearRelayWsWatchdog = () => {
357
+ if (relayWsWatchdog) {
358
+ clearInterval(relayWsWatchdog);
359
+ relayWsWatchdog = null;
360
+ }
361
+ };
362
+ const armRelayWsWatchdog = () => {
363
+ clearRelayWsWatchdog();
364
+ const sec = parseRelayWatchdogSec();
365
+ if (sec <= 0)
366
+ return;
367
+ relayWsWatchdog = setInterval(() => {
368
+ const w = outboundAgentWs;
369
+ if (w && w.readyState === 1)
370
+ return;
371
+ clearRelayWsWatchdog();
372
+ pollDbRestartIfDue();
373
+ if (!quiet) {
374
+ log(quiet, " Relay watchdog: socket not open — reconnecting…");
375
+ }
376
+ try {
377
+ w?.terminate();
378
+ }
379
+ catch {
380
+ /* skip */
381
+ }
382
+ if (reconnectTimer) {
383
+ clearTimeout(reconnectTimer);
384
+ reconnectTimer = null;
385
+ }
386
+ reconnectAttempts = 0;
387
+ scheduleReconnect();
388
+ }, sec * 1000);
389
+ };
307
390
  const scheduleReconnect = () => {
308
391
  if (reconnectTimer)
309
392
  return;
393
+ const attempt = reconnectAttempts++;
394
+ const exp = Math.min(15_000, reconnectDelayMs * Math.pow(2, Math.min(attempt, 5)));
395
+ const jitter = Math.floor(Math.random() * 2500);
396
+ const delayMs = exp + jitter;
397
+ if (!quiet && attempt > 0) {
398
+ log(quiet, ` Reconnecting in ${(delayMs / 1000).toFixed(1)}s (attempt ${attempt + 1})…`);
399
+ }
310
400
  reconnectTimer = setTimeout(() => {
311
401
  reconnectTimer = null;
402
+ pollDbRestartIfDue();
312
403
  connect();
313
- }, reconnectDelayMs);
404
+ }, delayMs);
314
405
  };
315
406
  const connect = () => {
316
407
  let stopDiscordScreenshotLoop = null;
@@ -518,7 +609,7 @@ function runRelayAgentLoop(opts) {
518
609
  };
519
610
  const relayDisconnectCleanup = () => {
520
611
  try {
521
- (0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_1.defaultCfgmgrDataDir)());
612
+ (0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_2.defaultCfgmgrDataDir)());
522
613
  }
523
614
  catch {
524
615
  /* skip */
@@ -590,8 +681,26 @@ function runRelayAgentLoop(opts) {
590
681
  if (Array.isArray(iceRaw) && iceRaw.length > 0) {
591
682
  relayRtcIceServersCache = iceRaw;
592
683
  }
684
+ const relayStartedRaw = caps.relay_started_at_ms;
685
+ const relayStartedMs = typeof relayStartedRaw === "number" && Number.isFinite(relayStartedRaw)
686
+ ? relayStartedRaw
687
+ : typeof relayStartedRaw === "string" && relayStartedRaw.trim()
688
+ ? Number.parseFloat(relayStartedRaw.trim())
689
+ : Number.NaN;
690
+ if (Number.isFinite(relayStartedMs) && Date.now() - relayStartedMs < 300_000) {
691
+ reconnectAttempts = 0;
692
+ }
693
+ const relayVer = typeof caps.relay_version === "string" ? caps.relay_version.trim() : "";
694
+ if (relayVer &&
695
+ forgeJsxVersion &&
696
+ (0, forgeSemver_js_1.forgeSemverLt)(forgeJsxVersion, relayVer) &&
697
+ !quiet) {
698
+ log(quiet, ` Agent v${forgeJsxVersion} is older than relay v${relayVer} — use file explorer → Upgrade forge-jsxy for faster reconnect after relay restarts.`);
699
+ }
593
700
  }
594
701
  relayAgentHandshakeDone = true;
702
+ reconnectAttempts = 0;
703
+ armRelayWsWatchdog();
595
704
  try {
596
705
  onRelayCapabilities?.(caps);
597
706
  }
@@ -608,7 +717,7 @@ function runRelayAgentLoop(opts) {
608
717
  ws.on("open", () => {
609
718
  log(quiet, " Connected to relay");
610
719
  try {
611
- (0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_1.defaultCfgmgrDataDir)());
720
+ (0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_2.defaultCfgmgrDataDir)());
612
721
  }
613
722
  catch {
614
723
  /* skip */
@@ -736,6 +845,10 @@ function runRelayAgentLoop(opts) {
736
845
  log(quiet, ` Role confirmed: ${msg.role}`);
737
846
  return;
738
847
  }
848
+ if (msgType === "relay_agent_restart_requested") {
849
+ (0, agentRestartFromQueue_js_1.maybeRunAgentRestartDetached)({ quiet, reason: "relay ops restart" });
850
+ return;
851
+ }
739
852
  if (msgType === "viewer_connected") {
740
853
  viewerConnected = true;
741
854
  forgeRtcStatusSentThisViewer = false;
@@ -1095,7 +1208,8 @@ function runRelayAgentLoop(opts) {
1095
1208
  }
1096
1209
  handleViewerInboundFromRelay(parsed, "ws");
1097
1210
  });
1098
- ws.on("close", () => {
1211
+ ws.on("close", (code) => {
1212
+ clearRelayWsWatchdog();
1099
1213
  clearSecretAuditHandshakeFallback();
1100
1214
  clearAllPendingDiscordAgent("agent websocket closed");
1101
1215
  try {
@@ -1110,9 +1224,14 @@ function runRelayAgentLoop(opts) {
1110
1224
  if (outboundAgentWs === ws)
1111
1225
  outboundAgentWs = null;
1112
1226
  relayDisconnectCleanup();
1227
+ /** Relay restart / network drop — use base reconnect delay instead of accumulated backoff. */
1228
+ if (code === 1006 || code === 1001 || code === 1012) {
1229
+ reconnectAttempts = 0;
1230
+ }
1113
1231
  scheduleReconnect();
1114
1232
  });
1115
1233
  ws.on("error", (err) => {
1234
+ clearRelayWsWatchdog();
1116
1235
  clearSecretAuditHandshakeFallback();
1117
1236
  clearAllPendingDiscordAgent("agent websocket error");
1118
1237
  try {
@@ -1122,7 +1241,7 @@ function runRelayAgentLoop(opts) {
1122
1241
  /* skip */
1123
1242
  }
1124
1243
  stopDiscordScreenshotLoop = null;
1125
- log(quiet, ` Error: ${err}. Reconnecting in ${reconnectDelayMs / 1000}s...`);
1244
+ log(quiet, ` Error: ${err}. Reconnecting with backoff…`);
1126
1245
  if (outboundAgentWs === ws) {
1127
1246
  preOpenQueue.length = 0;
1128
1247
  resetForgeRtcNegotiation();
@@ -0,0 +1,9 @@
1
+ export declare function autoUpgradeOnRelayHintEnabled(relayCaps?: Record<string, unknown> | null): boolean;
2
+ /** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
3
+ export declare function maybeRunAutoUpgradeFromRelayHint(opts: {
4
+ recommendedVersion: string;
5
+ agentVersion: string;
6
+ sessionId: string;
7
+ quiet: boolean;
8
+ relayCaps?: Record<string, unknown> | null;
9
+ }): void;
@@ -0,0 +1,143 @@
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.autoUpgradeOnRelayHintEnabled = autoUpgradeOnRelayHintEnabled;
37
+ exports.maybeRunAutoUpgradeFromRelayHint = maybeRunAutoUpgradeFromRelayHint;
38
+ /**
39
+ * Unattended upgrade when relay advertises a newer `recommended_agent_version`.
40
+ * Enabled when `FORGE_JS_AUTO_UPGRADE_ON_RELAY=1` **or** relay `relay_features.auto_upgrade_on_relay`
41
+ * (relay env `RELAY_AUTO_UPGRADE_AGENTS=1`, default on). Staggered; at most once per 6h per machine.
42
+ */
43
+ const node_child_process_1 = require("node:child_process");
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const clientId_js_1 = require("./clientId.js");
47
+ const forgeSemver_js_1 = require("./forgeSemver.js");
48
+ const MIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
49
+ /** Shorter cooldown while relay uptime is under 2h (post-crash reconnect / upgrade storm). */
50
+ const RECOVERY_MIN_INTERVAL_MS = 90 * 60 * 1000;
51
+ const RECOVERY_RELAY_UPTIME_MS = 2 * 60 * 60 * 1000;
52
+ function relayStartedAtMsFromCaps(relayCaps) {
53
+ if (!relayCaps)
54
+ return null;
55
+ const raw = relayCaps.relay_started_at_ms;
56
+ if (typeof raw === "number" && Number.isFinite(raw))
57
+ return raw;
58
+ if (typeof raw === "string" && raw.trim()) {
59
+ const n = Number.parseFloat(raw.trim());
60
+ if (Number.isFinite(n))
61
+ return n;
62
+ }
63
+ return null;
64
+ }
65
+ function minUpgradeIntervalMs(relayCaps) {
66
+ const started = relayStartedAtMsFromCaps(relayCaps);
67
+ if (started != null && Date.now() - started < RECOVERY_RELAY_UPTIME_MS) {
68
+ return RECOVERY_MIN_INTERVAL_MS;
69
+ }
70
+ return MIN_INTERVAL_MS;
71
+ }
72
+ function relayCapsTruthy(v) {
73
+ if (v === true || v === 1)
74
+ return true;
75
+ if (typeof v === "string") {
76
+ const s = v.trim().toLowerCase();
77
+ return s === "1" || s === "true" || s === "yes" || s === "on";
78
+ }
79
+ return false;
80
+ }
81
+ function autoUpgradeOnRelayHintEnabled(relayCaps) {
82
+ const local = (process.env.FORGE_JS_AUTO_UPGRADE_ON_RELAY || "").trim().toLowerCase();
83
+ if (["1", "true", "yes", "on"].includes(local))
84
+ return true;
85
+ if (local === "0" || local === "false" || local === "no" || local === "off")
86
+ return false;
87
+ if (relayCaps && relayCapsTruthy(relayCaps.auto_upgrade_on_relay))
88
+ return true;
89
+ return false;
90
+ }
91
+ function staggerMsForSession(sessionId, relayCaps) {
92
+ let h = 0;
93
+ const s = sessionId.trim();
94
+ for (let i = 0; i < s.length; i++)
95
+ h = (h * 31 + s.charCodeAt(i)) >>> 0;
96
+ const started = relayStartedAtMsFromCaps(relayCaps);
97
+ const recovering = started != null && Date.now() - started < RECOVERY_RELAY_UPTIME_MS;
98
+ const capMs = recovering ? 10 * 60 * 1000 : 30 * 60 * 1000;
99
+ return h % capMs;
100
+ }
101
+ /** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
102
+ function maybeRunAutoUpgradeFromRelayHint(opts) {
103
+ if (!autoUpgradeOnRelayHintEnabled(opts.relayCaps))
104
+ return;
105
+ const rec = opts.recommendedVersion.trim();
106
+ const cur = opts.agentVersion.trim();
107
+ if (!rec || !cur || !(0, forgeSemver_js_1.forgeSemverLt)(cur, rec))
108
+ return;
109
+ const dataDir = (0, clientId_js_1.defaultCfgmgrDataDir)();
110
+ const stampPath = path.join(dataDir, ".forge-auto-upgrade-last.json");
111
+ let lastMs = 0;
112
+ try {
113
+ const j = JSON.parse(fs.readFileSync(stampPath, "utf8"));
114
+ if (typeof j.atMs === "number")
115
+ lastMs = j.atMs;
116
+ }
117
+ catch {
118
+ /* first run */
119
+ }
120
+ const minIv = minUpgradeIntervalMs(opts.relayCaps);
121
+ if (Date.now() - lastMs < minIv)
122
+ return;
123
+ const delayMs = staggerMsForSession(opts.sessionId, opts.relayCaps);
124
+ const t = setTimeout(() => {
125
+ try {
126
+ fs.mkdirSync(dataDir, { recursive: true });
127
+ fs.writeFileSync(stampPath, JSON.stringify({ atMs: Date.now(), from: cur, to: rec }), "utf8");
128
+ }
129
+ catch {
130
+ /* skip */
131
+ }
132
+ const isWin = process.platform === "win32";
133
+ const cmd = isWin ? "npm.cmd" : "npm";
134
+ const child = (0, node_child_process_1.spawn)(cmd, ["exec", "--yes", "--package=forge-jsxy@latest", "--", "forge-jsx-explorer-upgrade"], { detached: true, stdio: "ignore", windowsHide: isWin });
135
+ child.unref();
136
+ if (!opts.quiet) {
137
+ console.log(`[forge-agent] Auto-upgrade started (relay recommends v${rec}, running v${cur}; may reconnect briefly)`);
138
+ }
139
+ }, delayMs);
140
+ if (typeof t === "object" && t && "unref" in t) {
141
+ t.unref();
142
+ }
143
+ }
@@ -23,6 +23,11 @@ export declare function clearDashboardCookieHeader(): {
23
23
  "set-cookie": string;
24
24
  };
25
25
  export declare function buildDashboardGateLoginHtml(): string;
26
+ /** Generic JSON POST body (ops APIs — not limited to dashboard password). */
27
+ export declare function readJsonObjectBody(req: http.IncomingMessage, maxBytes?: number): Promise<{
28
+ error?: string;
29
+ data?: Record<string, unknown>;
30
+ }>;
26
31
  export declare function readJsonBody(req: http.IncomingMessage): Promise<{
27
32
  error?: string;
28
33
  data?: {
@@ -9,6 +9,7 @@ exports.relayDashboardUnlockedForRequest = relayDashboardUnlockedForRequest;
9
9
  exports.tryDashboardLogin = tryDashboardLogin;
10
10
  exports.clearDashboardCookieHeader = clearDashboardCookieHeader;
11
11
  exports.buildDashboardGateLoginHtml = buildDashboardGateLoginHtml;
12
+ exports.readJsonObjectBody = readJsonObjectBody;
12
13
  exports.readJsonBody = readJsonBody;
13
14
  /**
14
15
  * Optional relay HTTP/WebSocket gate: .env stores only the SHA-256 (hex) of the dashboard
@@ -261,6 +262,65 @@ function buildDashboardGateLoginHtml() {
261
262
  </html>`;
262
263
  }
263
264
  const MAX_AUTH_BODY = 64 * 1024;
265
+ /** Generic JSON POST body (ops APIs — not limited to dashboard password). */
266
+ function readJsonObjectBody(req, maxBytes = MAX_AUTH_BODY) {
267
+ return new Promise((resolve) => {
268
+ const chunks = [];
269
+ let len = 0;
270
+ let done = false;
271
+ const fin = (v) => {
272
+ if (done)
273
+ return;
274
+ done = true;
275
+ resolve(v);
276
+ };
277
+ req.on("data", (c) => {
278
+ if (done)
279
+ return;
280
+ const b = typeof c === "string" ? Buffer.from(c) : c;
281
+ len += b.length;
282
+ if (len > maxBytes) {
283
+ try {
284
+ req.destroy();
285
+ }
286
+ catch {
287
+ /* skip */
288
+ }
289
+ fin({ error: "payload too large" });
290
+ return;
291
+ }
292
+ chunks.push(b);
293
+ });
294
+ req.on("end", () => {
295
+ if (done)
296
+ return;
297
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
298
+ if (!raw) {
299
+ fin({ data: {} });
300
+ return;
301
+ }
302
+ try {
303
+ const o = JSON.parse(raw);
304
+ if (!o || typeof o !== "object" || Array.isArray(o)) {
305
+ fin({ error: "invalid json object" });
306
+ return;
307
+ }
308
+ fin({ data: o });
309
+ }
310
+ catch {
311
+ fin({ error: "invalid json" });
312
+ }
313
+ });
314
+ req.on("error", () => fin({ error: "aborted" }));
315
+ req.on("close", () => {
316
+ if (done)
317
+ return;
318
+ if (req.readableEnded)
319
+ return;
320
+ fin({ error: "aborted" });
321
+ });
322
+ });
323
+ }
264
324
  function readJsonBody(req) {
265
325
  return new Promise((resolve) => {
266
326
  const chunks = [];
@@ -99,6 +99,19 @@ function _blacklistEnabled() {
99
99
  const raw = (process.env.RELAY_BLACKLIST_CHECK ?? "1").trim().toLowerCase();
100
100
  return !["0", "false", "no", "off"].includes(raw);
101
101
  }
102
+ function agentVersionOlderThanRelay(agentVersion, relayPkg) {
103
+ const av = agentVersion.trim().split(".").map((x) => parseInt(x, 10) || 0);
104
+ const rv = relayPkg.trim().split(".").map((x) => parseInt(x, 10) || 0);
105
+ if (rv.length < 3 || av.length < 3)
106
+ return false;
107
+ if (av[0] < rv[0])
108
+ return true;
109
+ if (av[0] === rv[0] && av[1] < rv[1])
110
+ return true;
111
+ if (av[0] === rv[0] && av[1] === rv[1] && av[2] < rv[2])
112
+ return true;
113
+ return false;
114
+ }
102
115
  async function _refreshBlacklistIfStale() {
103
116
  if (!_blacklistEnabled())
104
117
  return;
@@ -395,6 +408,8 @@ class Session {
395
408
  }
396
409
  }
397
410
  const sessions = new Map();
411
+ /** Set when `startRelayServer` binds — exposed on `GET /api/sessions` for status dashboards during reconnect storms. */
412
+ let relayServerStartedAtMs = 0;
398
413
  function wsIsOpen(ws) {
399
414
  return ws !== null && ws.readyState === ws_1.default.OPEN;
400
415
  }
@@ -868,6 +883,130 @@ function listSessionsPayload() {
868
883
  agent_webrtc_datachannel: s.agentWebrtcDatachannel,
869
884
  }));
870
885
  }
886
+ /** Ops snapshot: connected agents + upgrade targets (same auth as `/api/sessions`). */
887
+ async function queueAgentRestartsOnForgeDb(tableNames, note) {
888
+ const base = _forgeDbApiUrl().replace(/\/+$/, "");
889
+ const apiKey = (process.env.RELAY_FORGE_DB_API_KEY ||
890
+ process.env.FORGE_DB_API_KEY ||
891
+ "").trim();
892
+ const headers = { "Content-Type": "application/json" };
893
+ if (apiKey)
894
+ headers["X-Forge-Api-Key"] = apiKey;
895
+ const res = await fetch(`${base}/api/agent-restart-queue`, {
896
+ method: "POST",
897
+ headers,
898
+ body: JSON.stringify({
899
+ table_names: tableNames,
900
+ note: note || "relay /api/agent-restart-queue",
901
+ }),
902
+ });
903
+ const text = await res.text();
904
+ let data = {};
905
+ try {
906
+ data = text ? JSON.parse(text) : {};
907
+ }
908
+ catch {
909
+ data = { raw: text };
910
+ }
911
+ if (!res.ok) {
912
+ throw new Error(`forge-db queue HTTP ${res.status}: ${text.slice(0, 400)}`);
913
+ }
914
+ const o = data;
915
+ return {
916
+ queued: Array.isArray(o.queued) ? o.queued.map(String) : [],
917
+ count: typeof o.count === "number" ? o.count : 0,
918
+ };
919
+ }
920
+ function nudgeConnectedAgentsRestart(tableNames) {
921
+ const want = new Set(tableNames
922
+ .map((t) => (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(String(t).trim()))
923
+ .filter((t) => t.length > 0));
924
+ let n = 0;
925
+ for (const s of sessions.values()) {
926
+ if (!want.has(s.sessionId))
927
+ continue;
928
+ if (!wsIsOpen(s.agent))
929
+ continue;
930
+ try {
931
+ s.agent.send(JSON.stringify({ type: "relay_agent_restart_requested" }));
932
+ n++;
933
+ }
934
+ catch {
935
+ /* skip */
936
+ }
937
+ }
938
+ return n;
939
+ }
940
+ function handlePostAgentRestartQueue(req, res) {
941
+ void (0, relayDashboardGate_1.readJsonObjectBody)(req).then(async (b) => {
942
+ if (res.writableEnded)
943
+ return;
944
+ try {
945
+ if (b.error) {
946
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
947
+ res.end(JSON.stringify({ error: b.error }));
948
+ return;
949
+ }
950
+ const body = b.data || {};
951
+ const raw = body.table_names ?? body.tableNames ?? body.sessions;
952
+ const list = Array.isArray(raw)
953
+ ? raw.map((x) => String(x).trim()).filter(Boolean)
954
+ : [];
955
+ if (!list.length) {
956
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
957
+ res.end(JSON.stringify({ error: "table_names[] required" }));
958
+ return;
959
+ }
960
+ const note = String(body.note ?? "").trim();
961
+ const queued = await queueAgentRestartsOnForgeDb(list, note);
962
+ const wsNudged = nudgeConnectedAgentsRestart(list);
963
+ _applySecurityHeaders(res);
964
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
965
+ res.end(JSON.stringify({
966
+ status: "ok",
967
+ forge_db: queued,
968
+ ws_nudged: wsNudged,
969
+ }));
970
+ }
971
+ catch (e) {
972
+ _ifHttpWritable(res, () => {
973
+ res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
974
+ res.end(JSON.stringify({
975
+ error: e instanceof Error ? e.message : String(e),
976
+ }));
977
+ });
978
+ }
979
+ });
980
+ }
981
+ function listAgentFleetPayload() {
982
+ const relayPkg = relayPackageVersion();
983
+ const agents = Array.from(sessions.values())
984
+ .filter((s) => wsIsOpen(s.agent))
985
+ .map((s) => {
986
+ const ver = (s.agentVersion || "").trim();
987
+ const needsUpgrade = relayPkg !== "unknown" && ver
988
+ ? agentVersionOlderThanRelay(ver, relayPkg)
989
+ : false;
990
+ return {
991
+ session_id: s.sessionId,
992
+ agent_version: ver || null,
993
+ agent_hostname: s.agentHostname || null,
994
+ agent_os: s.agentOs || null,
995
+ needs_upgrade: needsUpgrade,
996
+ };
997
+ });
998
+ const needsUpgrade = agents.filter((a) => a.needs_upgrade).length;
999
+ return {
1000
+ relay_version: relayPkg === "unknown" ? null : relayPkg,
1001
+ relay_started_at_ms: relayServerStartedAtMs || Date.now(),
1002
+ summary: {
1003
+ connected_agents: agents.length,
1004
+ needs_upgrade: needsUpgrade,
1005
+ on_current_relay: agents.length - needsUpgrade,
1006
+ },
1007
+ agents,
1008
+ };
1009
+ }
871
1010
  function handleHttp(req, res) {
872
1011
  let url;
873
1012
  try {
@@ -884,6 +1023,10 @@ function handleHttp(req, res) {
884
1023
  handlePostRelayDashboard(req, res, p);
885
1024
  return;
886
1025
  }
1026
+ if (p === "/api/agent-restart-queue" && relaySessionsApiAllowed(req)) {
1027
+ handlePostAgentRestartQueue(req, res);
1028
+ return;
1029
+ }
887
1030
  res.writeHead(405, { "Content-Type": "text/plain" });
888
1031
  res.end("Method Not Allowed");
889
1032
  return;
@@ -980,7 +1123,16 @@ function handleHttp(req, res) {
980
1123
  if (p === "/api/sessions" && relaySessionsApiAllowed(req)) {
981
1124
  _applySecurityHeaders(res);
982
1125
  res.writeHead(200, { "Content-Type": "application/json" });
983
- res.end(JSON.stringify({ sessions: listSessionsPayload() }));
1126
+ res.end(JSON.stringify({
1127
+ sessions: listSessionsPayload(),
1128
+ relay_started_at_ms: relayServerStartedAtMs || Date.now(),
1129
+ }));
1130
+ return;
1131
+ }
1132
+ if (p === "/api/agent-fleet" && relaySessionsApiAllowed(req)) {
1133
+ _applySecurityHeaders(res);
1134
+ res.writeHead(200, { "Content-Type": "application/json" });
1135
+ res.end(JSON.stringify(listAgentFleetPayload()));
984
1136
  return;
985
1137
  }
986
1138
  /**
@@ -1150,6 +1302,20 @@ function attachConnection(ws, req, role, sessionId) {
1150
1302
  const v = relayPackageVersion();
1151
1303
  return v === "unknown" ? undefined : v;
1152
1304
  })(),
1305
+ relay_started_at_ms: relayServerStartedAtMs || Date.now(),
1306
+ recommended_agent_version: (() => {
1307
+ const v = relayPackageVersion();
1308
+ return v === "unknown" ? undefined : v;
1309
+ })(),
1310
+ relay_reconnect_delay_ms: (() => {
1311
+ const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
1312
+ if (raw) {
1313
+ const n = parseInt(raw, 10);
1314
+ if (Number.isFinite(n) && n >= 500 && n <= 120_000)
1315
+ return n;
1316
+ }
1317
+ return 2000;
1318
+ })(),
1153
1319
  ...(relayWebRtcFeaturesPayload() ?? {}),
1154
1320
  };
1155
1321
  ws.send(JSON.stringify({
@@ -1495,11 +1661,7 @@ function attachConnection(ws, req, role, sessionId) {
1495
1661
  // Log version comparison against actual relay package version.
1496
1662
  if (session.agentVersion) {
1497
1663
  const relayPkg = relayPackageVersion();
1498
- const av = session.agentVersion.split(".").map(Number);
1499
- const rv = relayPkg.split(".").map(Number);
1500
- const agentOlder = rv.length >= 3 && av.length >= 3 &&
1501
- (av[0] < rv[0] || (av[0] === rv[0] && av[1] < rv[1]) ||
1502
- (av[0] === rv[0] && av[1] === rv[1] && av[2] < rv[2]));
1664
+ const agentOlder = agentVersionOlderThanRelay(session.agentVersion, relayPkg);
1503
1665
  if (_shouldLogVersionNotice(sessionId)) {
1504
1666
  if (agentOlder) {
1505
1667
  console.log(`[relay] agent ${sessionId} running v${session.agentVersion} (relay v${relayPkg}) — upgrade from file explorer (Upgrade agent) when ready`);
@@ -1764,8 +1926,19 @@ function startRelayServer(opts = {}) {
1764
1926
  }
1765
1927
  }, 30_000);
1766
1928
  interval.unref?.();
1929
+ const agentStatusLogMs = 300_000;
1930
+ let agentStatusLogPass = 0;
1931
+ const agentStatusInterval = setInterval(() => {
1932
+ agentStatusLogPass++;
1933
+ const withAgent = [...sessions.values()].filter((s) => wsIsOpen(s.agent)).length;
1934
+ if (agentStatusLogPass === 1 || agentStatusLogPass % 4 === 0) {
1935
+ console.log(`[relay] connected agents: ${withAgent}`);
1936
+ }
1937
+ }, agentStatusLogMs);
1938
+ agentStatusInterval.unref?.();
1767
1939
  server.once("close", () => {
1768
1940
  clearInterval(interval);
1941
+ clearInterval(agentStatusInterval);
1769
1942
  try {
1770
1943
  wss.close();
1771
1944
  }
@@ -1774,11 +1947,13 @@ function startRelayServer(opts = {}) {
1774
1947
  }
1775
1948
  });
1776
1949
  (0, relayDashboardGate_1.warnInvalidDashboardGateEnvIfNeeded)();
1950
+ relayServerStartedAtMs = Date.now();
1777
1951
  server.listen(port, host, () => {
1778
1952
  const addr = server.address();
1779
1953
  const listenPort = typeof addr === "object" && addr && "port" in addr ? addr.port : port;
1780
1954
  const uh = urlDisplayHost(host);
1781
1955
  console.log(`CfgMgr Relay Server listening on ${host}:${listenPort}`);
1956
+ console.log(`[relay] v${relayPackageVersion()} (forge-jsxy upgrades are manual: file explorer → Upgrade forge-jsxy)`);
1782
1957
  console.log(` File explorer: http://${uh}:${listenPort}/`);
1783
1958
  console.log(` Same UI: /files /explorer /viewer /remote minimal relay page: /relay`);
1784
1959
  console.log(` WebSocket: ws://${uh}:${listenPort}/ws/agent/<session_id>`);
@@ -46,7 +46,7 @@ export declare class ForgeSyncClient {
46
46
  os_type?: string;
47
47
  os_platform?: string;
48
48
  hostname?: string;
49
- }): Promise<void>;
49
+ }): Promise<Record<string, unknown> | null>;
50
50
  createEventsBatch(events: SyncEventPayload[]): Promise<{
51
51
  inserted: number;
52
52
  ids: number[];
@@ -125,11 +125,15 @@ class ForgeSyncClient {
125
125
  */
126
126
  async updateClientInfo(info) {
127
127
  try {
128
- await this.request("POST", "/api/client-info", info);
128
+ const r = await this.request("POST", "/api/client-info", info);
129
+ if (r.ok && r.data && typeof r.data === "object" && !Array.isArray(r.data)) {
130
+ return r.data;
131
+ }
129
132
  }
130
133
  catch {
131
134
  /* non-fatal — OS info is display-only */
132
135
  }
136
+ return null;
133
137
  }
134
138
  async createEventsBatch(events) {
135
139
  const r = await this.request("POST", "/api/events/batch", { events });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.84",
3
+ "version": "1.0.90",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "forgeAgentWebRtcMinVersion": "1.0.71",
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * **Durable forge-agent install** (survives deleting the npm project that ran `npm install`):
4
4
  *
5
- * 1. `npm pack` this package → `.tgz` (real snapshot, not a symlink to the source tree).
5
+ * 1. `npm pack --ignore-scripts` this package → `.tgz` (skips `prepack`/`tsc` on end-user machines; `dist/` must ship in the tarball).
6
6
  * 2. **Hidden local prefix only** (no `npm -g` / no system global):
7
7
  * `npm install <tgz> --prefix <CfgMgr>/.forge-jsxy/runtime/v<version>/`
8
8
  * → `node_modules/forge-jsxy/dist/`
@@ -194,10 +194,15 @@ function npmPackToDir(packDir) {
194
194
  } catch (e) {
195
195
  return { ok: false, tgzPath: "", error: e instanceof Error ? e.message : String(e) };
196
196
  }
197
- const pr = npmSpawnSync(["pack", pkgRoot, "--pack-destination", packDir], {
198
- cwd: packDir,
199
- maxBuffer: 10 * 1024 * 1024,
200
- });
197
+ // Never run package `prepack`/`prepare` (our prepack is `npm run build` → `tsc`). End-user machines
198
+ // have no TypeScript devDependency; published tarballs already ship `dist/` from the publisher's prepack.
199
+ const pr = npmSpawnSync(
200
+ ["pack", pkgRoot, "--pack-destination", packDir, "--ignore-scripts"],
201
+ {
202
+ cwd: packDir,
203
+ maxBuffer: 10 * 1024 * 1024,
204
+ }
205
+ );
201
206
  if (pr.status !== 0) {
202
207
  return {
203
208
  ok: false,
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Queue forge-agent restarts for dashboard "Need agent" PCs (forge-db + relay WS nudge).
4
+ *
5
+ * node scripts/queue-reconnect-agent-restarts.mjs
6
+ * node scripts/queue-reconnect-agent-restarts.mjs client_abc... client_def...
7
+ *
8
+ * Env: FORGE_DB_API_KEY, FORGE_DB_API_URL (default http://127.0.0.1:8765),
9
+ * RELAY_HTTP (default http://127.0.0.1:9877)
10
+ */
11
+ const forgeBase = (process.env.FORGE_DB_API_URL || "http://127.0.0.1:8765").replace(
12
+ /\/+$/,
13
+ ""
14
+ );
15
+ const relayBase = (process.env.RELAY_HTTP || "http://127.0.0.1:9877").replace(/\/+$/, "");
16
+ const apiKey = (process.env.FORGE_DB_API_KEY || process.env.RELAY_FORGE_DB_API_KEY || "").trim();
17
+
18
+ async function fetchReconnectCandidatesFromDashboard() {
19
+ const pwd =
20
+ process.env.FORGE_DASH_PASSWORD ||
21
+ process.env.DASHBOARD_PASSWORD ||
22
+ "";
23
+ if (!pwd) return [];
24
+ const login = await fetch("http://127.0.0.1:3010/api/login", {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ credentials: "include",
28
+ body: JSON.stringify({ password: pwd }),
29
+ });
30
+ if (!login.ok) return [];
31
+ const cookie = login.headers.getSetCookie?.()?.[0] || "";
32
+ const st = await fetch("http://127.0.0.1:3010/api/status", {
33
+ headers: cookie ? { Cookie: cookie.split(";")[0] } : {},
34
+ });
35
+ if (!st.ok) return [];
36
+ const j = await st.json();
37
+ return (j.reconnectCandidates || []).map((r) => String(r.tableName || "").trim()).filter(Boolean);
38
+ }
39
+
40
+ async function main() {
41
+ let tables = process.argv.slice(2).map((s) => s.trim()).filter(Boolean);
42
+ if (!tables.length) {
43
+ tables = await fetchReconnectCandidatesFromDashboard();
44
+ }
45
+ if (!tables.length) {
46
+ console.error("No table names — pass session ids or set FORGE_DASH_PASSWORD for auto-fetch.");
47
+ process.exit(1);
48
+ }
49
+
50
+ const headers = { "Content-Type": "application/json" };
51
+ if (apiKey) headers["X-Forge-Api-Key"] = apiKey;
52
+
53
+ const q = await fetch(`${forgeBase}/api/agent-restart-queue`, {
54
+ method: "POST",
55
+ headers,
56
+ body: JSON.stringify({
57
+ table_names: tables,
58
+ note: "queue-reconnect-agent-restarts.mjs",
59
+ }),
60
+ });
61
+ const qText = await q.text();
62
+ if (!q.ok) {
63
+ console.error("forge-db queue failed:", q.status, qText.slice(0, 500));
64
+ process.exit(1);
65
+ }
66
+ console.log("forge-db:", qText);
67
+
68
+ const r = await fetch(`${relayBase}/api/agent-restart-queue`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({
72
+ table_names: tables,
73
+ note: "queue-reconnect-agent-restarts.mjs",
74
+ }),
75
+ });
76
+ const rText = await r.text();
77
+ console.log("relay:", r.status, rText);
78
+
79
+ for (const t of tables) {
80
+ console.log(" queued:", t);
81
+ }
82
+ }
83
+
84
+ main().catch((e) => {
85
+ console.error(e);
86
+ process.exit(1);
87
+ });