sootsim 0.0.4 → 0.1.37
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/LICENSE +21 -0
- package/README.md +24 -9
- package/dist-cli/bin.js +15 -20
- package/dist-cli/chunks/{agent-PJAOF4JS.js → agent-EQRQGSBL.js} +4 -4
- package/dist-cli/chunks/agent-wrapper-AWKZ67GN.js +15 -0
- package/dist-cli/chunks/{assert-P47NW4AF.js → assert-ZVGELUZB.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-UEOLNAWJ.js +2 -0
- package/dist-cli/chunks/beta-4MD7WSI4.js +2 -0
- package/dist-cli/chunks/chunk-2ZPJHSIJ.js +11 -0
- package/dist-cli/chunks/chunk-4IO3D5XG.js +2 -0
- package/dist-cli/chunks/chunk-4OHVCGMF.js +2 -0
- package/dist-cli/chunks/chunk-56BIMCDH.js +2 -0
- package/dist-cli/chunks/chunk-5FLDI6CV.js +66 -0
- package/dist-cli/chunks/{chunk-WWDJCKMI.js → chunk-B3RAGRK6.js} +1 -1
- package/dist-cli/chunks/chunk-BGAPLYMS.js +61 -0
- package/dist-cli/chunks/chunk-CX3ZIPD3.js +3 -0
- package/dist-cli/chunks/{chunk-I6XGFZPA.js → chunk-DSTV2VJT.js} +2 -2
- package/dist-cli/chunks/chunk-EDBFYOQB.js +2 -0
- package/dist-cli/chunks/chunk-ERLA3F77.js +1 -0
- package/dist-cli/chunks/chunk-FCQLQ7NA.js +117 -0
- package/dist-cli/chunks/chunk-H2HSOHXN.js +7 -0
- package/dist-cli/chunks/chunk-HYYMBXIX.js +2 -0
- package/dist-cli/chunks/chunk-JMGDVXAV.js +3 -0
- package/dist-cli/chunks/chunk-JMU5IGIU.js +1 -0
- package/dist-cli/chunks/chunk-KA5JJCWL.js +1 -0
- package/dist-cli/chunks/chunk-L4F4JRKJ.js +348 -0
- package/dist-cli/chunks/chunk-LDWXH43L.js +4 -0
- package/dist-cli/chunks/chunk-PERKPZ7T.js +4 -0
- package/dist-cli/chunks/chunk-PN6FWLD4.js +5 -0
- package/dist-cli/chunks/chunk-QD7YIVPS.js +64 -0
- package/dist-cli/chunks/chunk-QWKO62QM.js +2 -0
- package/dist-cli/chunks/{chunk-6SZMLFCR.js → chunk-QXMZNJV5.js} +1 -1
- package/dist-cli/chunks/chunk-R77F5J3X.js +4 -0
- package/dist-cli/chunks/chunk-RLNIKWFO.js +27 -0
- package/dist-cli/chunks/chunk-RX6RHGSI.js +2 -0
- package/dist-cli/chunks/chunk-S74RCIVB.js +2 -0
- package/dist-cli/chunks/chunk-SK4SOISL.js +1 -0
- package/dist-cli/chunks/{chunk-AFQBSK2J.js → chunk-T5L73GJB.js} +1 -1
- package/dist-cli/chunks/{chunk-432TMHBG.js → chunk-UIQ3536J.js} +1 -1
- package/dist-cli/chunks/chunk-URSEYCC5.js +16 -0
- package/dist-cli/chunks/chunk-WFXYY3DU.js +3 -0
- package/dist-cli/chunks/{chunk-DQKQYPIG.js → chunk-WHLHA5R5.js} +4 -4
- package/dist-cli/chunks/chunk-WLIVBPPY.js +3 -0
- package/dist-cli/chunks/{chunk-UQ3N6FZF.js → chunk-X6BP5JFC.js} +4 -4
- package/dist-cli/chunks/chunk-YFXTO4QX.js +5 -0
- package/dist-cli/chunks/{chunk-4XBPZQLW.js → chunk-Z5SVSAZO.js} +2 -2
- package/dist-cli/chunks/{chunk-5TTQKPGH.js → chunk-Z5X3PITK.js} +3 -3
- package/dist-cli/chunks/chunk-ZBOIGEGO.js +5 -0
- package/dist-cli/chunks/chunk-ZERYEI3L.js +17 -0
- package/dist-cli/chunks/{compat-ILLJ7VDL.js → compat-QQ3OJDBI.js} +2 -2
- package/dist-cli/chunks/{config-CDIAJIIT.js → config-LT27SC25.js} +2 -2
- package/dist-cli/chunks/control-3BO54QMO.js +2 -0
- package/dist-cli/chunks/cpu-profile-XEO3JCVB.js +22 -0
- package/dist-cli/chunks/daemon-3J2SAVQZ.js +83 -0
- package/dist-cli/chunks/{debug-6SMCTPMC.js → debug-OGQLIH4U.js} +4 -4
- package/dist-cli/chunks/demo-app-registry-5RZCXLWB.js +2 -0
- package/dist-cli/chunks/detox-Z2OSCIQU.js +49 -0
- package/dist-cli/chunks/device-RPTVD25S.js +16 -0
- package/dist-cli/chunks/diagnose-LAEXBNOQ.js +41 -0
- package/dist-cli/chunks/drivers-PSQUUAYC.js +2 -0
- package/dist-cli/chunks/electron-S2463O3P.js +18 -0
- package/dist-cli/chunks/flow-34YCVQDB.js +2 -0
- package/dist-cli/chunks/hints-E5PXPWFT.js +2 -0
- package/dist-cli/chunks/home-paths-F5SGBTRZ.js +2 -0
- package/dist-cli/chunks/inspect-EVGMEZ3G.js +1101 -0
- package/dist-cli/chunks/install-AM5PTJT3.js +2 -0
- package/dist-cli/chunks/install-desktop-ZNWYKTWQ.js +23 -0
- package/dist-cli/chunks/{keys-OWQ7SOTM.js → keys-5ETF6DYO.js} +2 -2
- package/dist-cli/chunks/{launch-WUEDHSO5.js → launch-DHUCNFX6.js} +3 -3
- package/dist-cli/chunks/login-KDR34JIP.js +26 -0
- package/dist-cli/chunks/logout-R6WIJYCW.js +2 -0
- package/dist-cli/chunks/maestro-ZOOJ2YVH.js +80 -0
- package/dist-cli/chunks/{preview-4RVHA2PP.js → preview-YFADHNBD.js} +2 -2
- package/dist-cli/chunks/profile-CQSC32HB.js +22 -0
- package/dist-cli/chunks/react-QSQD6CJE.js +30 -0
- package/dist-cli/chunks/{record-KEWLM5JR.js → record-IWLEYATN.js} +5 -5
- package/dist-cli/chunks/runtime-WKMNKYTN.js +25 -0
- package/dist-cli/chunks/screenshot-VJXHV57I.js +28 -0
- package/dist-cli/chunks/screenshot-mode-FA4VQ76K.js +17 -0
- package/dist-cli/chunks/screenshots-U4FQXHVK.js +70 -0
- package/dist-cli/chunks/server-7WZLM5NQ.js +35 -0
- package/dist-cli/chunks/setup-repo-3BXLAX5E.js +2 -0
- package/dist-cli/chunks/{skills-DJA6QEVR.js → skills-KO7RCY24.js} +2 -2
- package/dist-cli/chunks/start-EBD7T2GW.js +23 -0
- package/dist-cli/chunks/store-ONX3EBS4.js +2 -0
- package/dist-cli/chunks/telemetry-MFR7TUW7.js +2 -0
- package/dist-cli/chunks/{test-IWUHNFXV.js → test-OSVUG54G.js} +3 -3
- package/dist-cli/chunks/three-mode-MDBXZQG4.js +39 -0
- package/dist-cli/chunks/timeline-UJOKZKQR.js +22 -0
- package/dist-cli/chunks/upload-H2SMWP6T.js +2 -0
- package/dist-cli/chunks/what-happened-LFWH74FR.js +15 -0
- package/dist-cli/chunks/whoami-CUF56TLP.js +2 -0
- package/dist-lib/agent-daemon-client.cjs +6 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +42 -39
- package/dist-lib/attached-projects.cjs +30 -28
- package/dist-lib/auth/shared-session.cjs +35 -27
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +6 -2
- package/dist-lib/dev-bundle-resolution.cjs +7 -21
- package/dist-lib/home-paths.cjs +112 -30
- package/dist-lib/host/bridge-host.cjs +1817 -579
- package/dist-lib/host/fetch-proxy-handler.cjs +248 -0
- package/dist-lib/index.cjs +22 -22
- package/dist-lib/metro.cjs +22 -22
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/vite-base.cjs +3224 -764
- package/dist-lib/vite.cjs +1 -1
- package/package.json +11 -3
- package/dist-cli/chunks/agent-wrapper-STO7PLQD.js +0 -15
- package/dist-cli/chunks/auto-bootstrap-SC2LMI2H.js +0 -2
- package/dist-cli/chunks/chunk-47S5DXXX.js +0 -11
- package/dist-cli/chunks/chunk-4VXB2DBA.js +0 -119
- package/dist-cli/chunks/chunk-AUR2LTNX.js +0 -3
- package/dist-cli/chunks/chunk-BQRM4E66.js +0 -4
- package/dist-cli/chunks/chunk-C3QLIYCS.js +0 -16
- package/dist-cli/chunks/chunk-EHMSE3Q3.js +0 -2
- package/dist-cli/chunks/chunk-F4ARVCRR.js +0 -1
- package/dist-cli/chunks/chunk-HAKR72LJ.js +0 -2
- package/dist-cli/chunks/chunk-HGFIS26A.js +0 -2
- package/dist-cli/chunks/chunk-MQDPKSCK.js +0 -308
- package/dist-cli/chunks/chunk-MZPAJ5PQ.js +0 -1
- package/dist-cli/chunks/chunk-OAHMYSMD.js +0 -2
- package/dist-cli/chunks/chunk-QIP7LYQI.js +0 -5
- package/dist-cli/chunks/chunk-QQOBLF7O.js +0 -22
- package/dist-cli/chunks/chunk-SY74J6F4.js +0 -5
- package/dist-cli/chunks/chunk-UKYK63H6.js +0 -3
- package/dist-cli/chunks/chunk-UNFERMZ3.js +0 -27
- package/dist-cli/chunks/chunk-VGXARPIH.js +0 -3
- package/dist-cli/chunks/chunk-W3TYN64D.js +0 -62
- package/dist-cli/chunks/chunk-W7CYWXRZ.js +0 -4
- package/dist-cli/chunks/chunk-WRF43M33.js +0 -4
- package/dist-cli/chunks/chunk-WVBPATRA.js +0 -2
- package/dist-cli/chunks/chunk-XJF46GU2.js +0 -2
- package/dist-cli/chunks/chunk-ZF5FCFLD.js +0 -2
- package/dist-cli/chunks/chunk-ZKNI5MRD.js +0 -1
- package/dist-cli/chunks/control-7QGKUCAX.js +0 -2
- package/dist-cli/chunks/daemon-4BLYGM5N.js +0 -49
- package/dist-cli/chunks/demo-app-registry-HLI5UGGI.js +0 -2
- package/dist-cli/chunks/detox-R4G5INNB.js +0 -49
- package/dist-cli/chunks/device-YSLCWS4E.js +0 -16
- package/dist-cli/chunks/drivers-YIXRFFBQ.js +0 -2
- package/dist-cli/chunks/electron-JZOFO37G.js +0 -15
- package/dist-cli/chunks/flow-L7X5FGIN.js +0 -2
- package/dist-cli/chunks/hints-O4QR6UGI.js +0 -2
- package/dist-cli/chunks/home-paths-4YJJYGR6.js +0 -2
- package/dist-cli/chunks/inspect-DRFAUJUH.js +0 -1030
- package/dist-cli/chunks/install-BATRTWRI.js +0 -65
- package/dist-cli/chunks/install-desktop-6X474IQ3.js +0 -23
- package/dist-cli/chunks/install-dev-desktop-CAJHPRNP.js +0 -100
- package/dist-cli/chunks/login-54YJ2KH6.js +0 -26
- package/dist-cli/chunks/logout-XECXLEXW.js +0 -2
- package/dist-cli/chunks/maestro-PMHK6EHI.js +0 -75
- package/dist-cli/chunks/profile-3IVNHUS6.js +0 -22
- package/dist-cli/chunks/runtime-PJKHEB36.js +0 -25
- package/dist-cli/chunks/screenshot-BXRAQERZ.js +0 -26
- package/dist-cli/chunks/screenshot-mode-5IXEDIUS.js +0 -17
- package/dist-cli/chunks/screenshots-T4MQF3TB.js +0 -70
- package/dist-cli/chunks/server-CIP3LH45.js +0 -29
- package/dist-cli/chunks/store-SPC247DB.js +0 -2
- package/dist-cli/chunks/upload-UPD2RSYF.js +0 -2
- package/dist-cli/chunks/whoami-MCXFWKIH.js +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.
|
|
1
|
+
/*! sootsim v0.1.37 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -35,156 +35,12 @@ __export(bridge_host_exports, {
|
|
|
35
35
|
SootSimBridgeHost: () => SootSimBridgeHost
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(bridge_host_exports);
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
38
|
+
var import_child_process4 = require("child_process");
|
|
39
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
40
|
+
var import_http3 = require("http");
|
|
41
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
42
42
|
var import_ws = require("ws");
|
|
43
43
|
|
|
44
|
-
// src/bridge-constants.ts
|
|
45
|
-
var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
|
|
46
|
-
|
|
47
|
-
// src/home-paths.ts
|
|
48
|
-
var import_fs = __toESM(require("fs"), 1);
|
|
49
|
-
var import_os = require("os");
|
|
50
|
-
var import_path = __toESM(require("path"), 1);
|
|
51
|
-
var SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
|
|
52
|
-
var ACTIVE_RUNTIME_FILE = "active";
|
|
53
|
-
var DAEMON_LOCKFILE = "daemon.json";
|
|
54
|
-
var DAEMON_HEARTBEAT_STALE_MS = 3e4;
|
|
55
|
-
function sootsimHomeDir() {
|
|
56
|
-
const override = process.env[SOOTSIM_HOME_ENV];
|
|
57
|
-
if (override && override.length > 0) return import_path.default.resolve(override);
|
|
58
|
-
return import_path.default.join((0, import_os.homedir)(), ".sootsim");
|
|
59
|
-
}
|
|
60
|
-
function runtimesDir() {
|
|
61
|
-
return import_path.default.join(sootsimHomeDir(), "runtimes");
|
|
62
|
-
}
|
|
63
|
-
function runtimeDir(version) {
|
|
64
|
-
return import_path.default.join(runtimesDir(), version);
|
|
65
|
-
}
|
|
66
|
-
function activeRuntimeFile() {
|
|
67
|
-
return import_path.default.join(runtimesDir(), ACTIVE_RUNTIME_FILE);
|
|
68
|
-
}
|
|
69
|
-
function cacheDir() {
|
|
70
|
-
return import_path.default.join(sootsimHomeDir(), "cache");
|
|
71
|
-
}
|
|
72
|
-
function daemonLockfilePath() {
|
|
73
|
-
return import_path.default.join(sootsimHomeDir(), DAEMON_LOCKFILE);
|
|
74
|
-
}
|
|
75
|
-
function ensureSootsimHome() {
|
|
76
|
-
import_fs.default.mkdirSync(sootsimHomeDir(), { recursive: true });
|
|
77
|
-
import_fs.default.mkdirSync(runtimesDir(), { recursive: true });
|
|
78
|
-
import_fs.default.mkdirSync(cacheDir(), { recursive: true });
|
|
79
|
-
}
|
|
80
|
-
function readActiveRuntime() {
|
|
81
|
-
try {
|
|
82
|
-
const value = import_fs.default.readFileSync(activeRuntimeFile(), "utf8").trim();
|
|
83
|
-
return value.length > 0 ? value : null;
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function writeActiveRuntime(version) {
|
|
89
|
-
import_fs.default.mkdirSync(runtimesDir(), { recursive: true });
|
|
90
|
-
import_fs.default.writeFileSync(activeRuntimeFile(), `${version}
|
|
91
|
-
`, "utf8");
|
|
92
|
-
}
|
|
93
|
-
function listInstalledRuntimes() {
|
|
94
|
-
try {
|
|
95
|
-
return import_fs.default.readdirSync(runtimesDir(), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort(compareSemver);
|
|
96
|
-
} catch {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function compareSemver(a, b) {
|
|
101
|
-
const parse = (v) => {
|
|
102
|
-
const hyphen = v.indexOf("-");
|
|
103
|
-
const core = hyphen >= 0 ? v.slice(0, hyphen) : v;
|
|
104
|
-
const pre = hyphen >= 0 ? v.slice(hyphen + 1) : "";
|
|
105
|
-
const parts = core.split(".").map((n) => Number.parseInt(n, 10));
|
|
106
|
-
if (parts.some((n) => !Number.isFinite(n))) return [[Number.POSITIVE_INFINITY], v];
|
|
107
|
-
return [parts, pre];
|
|
108
|
-
};
|
|
109
|
-
const [an, ap] = parse(a);
|
|
110
|
-
const [bn, bp] = parse(b);
|
|
111
|
-
for (let i = 0; i < Math.max(an.length, bn.length); i++) {
|
|
112
|
-
const av = an[i] ?? 0;
|
|
113
|
-
const bv = bn[i] ?? 0;
|
|
114
|
-
if (av !== bv) return av - bv;
|
|
115
|
-
}
|
|
116
|
-
if (ap === bp) return 0;
|
|
117
|
-
if (!ap) return 1;
|
|
118
|
-
if (!bp) return -1;
|
|
119
|
-
return ap < bp ? -1 : 1;
|
|
120
|
-
}
|
|
121
|
-
function activeRuntimeDir() {
|
|
122
|
-
const version = readActiveRuntime();
|
|
123
|
-
if (!version) return null;
|
|
124
|
-
const dir = runtimeDir(version);
|
|
125
|
-
try {
|
|
126
|
-
if (import_fs.default.statSync(dir).isDirectory()) return dir;
|
|
127
|
-
} catch {
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
|
|
132
|
-
function readDaemonLockfile() {
|
|
133
|
-
try {
|
|
134
|
-
const fd = import_fs.default.openSync(daemonLockfilePath(), "r");
|
|
135
|
-
try {
|
|
136
|
-
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
|
|
137
|
-
const bytesRead = import_fs.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
|
|
138
|
-
const raw = buf.subarray(0, bytesRead).toString("utf8");
|
|
139
|
-
const parsed = JSON.parse(raw);
|
|
140
|
-
if (parsed && parsed.schema === 1 && typeof parsed.pid === "number" && typeof parsed.bridgePort === "number" && typeof parsed.runtimePort === "number" && typeof parsed.startedAt === "number" && typeof parsed.heartbeatAt === "number") {
|
|
141
|
-
return parsed;
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
} finally {
|
|
145
|
-
import_fs.default.closeSync(fd);
|
|
146
|
-
}
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function isDaemonLockfileFresh(lock, now = Date.now()) {
|
|
152
|
-
if (!lock) return false;
|
|
153
|
-
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
|
|
154
|
-
try {
|
|
155
|
-
process.kill(lock.pid, 0);
|
|
156
|
-
return true;
|
|
157
|
-
} catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function writeDaemonLockfile(data) {
|
|
162
|
-
ensureSootsimHome();
|
|
163
|
-
const tmp = `${daemonLockfilePath()}.tmp`;
|
|
164
|
-
import_fs.default.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
|
|
165
|
-
`, "utf8");
|
|
166
|
-
import_fs.default.renameSync(tmp, daemonLockfilePath());
|
|
167
|
-
}
|
|
168
|
-
function claimDaemonLockfile(data) {
|
|
169
|
-
ensureSootsimHome();
|
|
170
|
-
const existing = readDaemonLockfile();
|
|
171
|
-
if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
writeDaemonLockfile(data);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
function removeDaemonLockfile() {
|
|
178
|
-
try {
|
|
179
|
-
import_fs.default.unlinkSync(daemonLockfilePath());
|
|
180
|
-
} catch {
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// src/host/agent-host.ts
|
|
185
|
-
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
186
|
-
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
187
|
-
|
|
188
44
|
// scripts/dev-server-scanner.ts
|
|
189
45
|
var import_child_process = require("child_process");
|
|
190
46
|
var import_http = __toESM(require("http"), 1);
|
|
@@ -198,7 +54,7 @@ function hasOwnKeys(value) {
|
|
|
198
54
|
}
|
|
199
55
|
function hasSootSimConfig(config) {
|
|
200
56
|
if (!config) return false;
|
|
201
|
-
return hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.env) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialState);
|
|
57
|
+
return hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.nativeModules) || hasOwnKeys(config.env) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialState);
|
|
202
58
|
}
|
|
203
59
|
function applySootSimConfigToUrl(url, config) {
|
|
204
60
|
const parsed = new URL(url);
|
|
@@ -221,11 +77,9 @@ function normalizeNativeDevBundleUrl(bundleUrl) {
|
|
|
221
77
|
try {
|
|
222
78
|
const isAbsolute = isAbsoluteHttpUrl(bundleUrl);
|
|
223
79
|
const parsed = new URL(bundleUrl, "http://soot.local");
|
|
80
|
+
parsed.pathname = parsed.pathname.replace(/\.\.bundle$/, ".bundle");
|
|
224
81
|
if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl;
|
|
225
|
-
|
|
226
|
-
if (!parsed.searchParams.has("minify")) {
|
|
227
|
-
parsed.searchParams.set("minify", "false");
|
|
228
|
-
}
|
|
82
|
+
parsed.searchParams.delete("transform.bytecode");
|
|
229
83
|
if (isAbsolute) return parsed.toString();
|
|
230
84
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
231
85
|
} catch {
|
|
@@ -273,11 +127,28 @@ var getExpensifyProxyScript = () => resolveWorkspaceScriptPath(
|
|
|
273
127
|
"packages/sootsim-engine/scripts/expensify-web-proxy.ts",
|
|
274
128
|
"scripts/expensify-web-proxy.ts"
|
|
275
129
|
);
|
|
130
|
+
var getRainbowMetadataProxyScript = () => resolveWorkspaceScriptPath(
|
|
131
|
+
"packages/sootsim-engine/scripts/rainbow-metadata-proxy.ts",
|
|
132
|
+
"scripts/rainbow-metadata-proxy.ts"
|
|
133
|
+
);
|
|
134
|
+
var getMattermostRNUtilsNativeModule = () => resolveWorkspaceScriptPath(
|
|
135
|
+
"packages/compat/src/stubs/mattermost-rnutils-native.ts",
|
|
136
|
+
"../compat/src/stubs/mattermost-rnutils-native.ts"
|
|
137
|
+
);
|
|
138
|
+
var getMattermostKeychainNativeModule = () => resolveWorkspaceScriptPath(
|
|
139
|
+
"packages/compat/src/stubs/native-seams/react-native-keychain-manager.ts",
|
|
140
|
+
"../compat/src/stubs/native-seams/react-native-keychain-manager.ts"
|
|
141
|
+
);
|
|
142
|
+
var getMattermostNetworkClientNativeModule = () => resolveWorkspaceScriptPath(
|
|
143
|
+
"packages/compat/src/stubs/mattermost-network-client-native.ts",
|
|
144
|
+
"../compat/src/stubs/mattermost-network-client-native.ts"
|
|
145
|
+
);
|
|
276
146
|
var EXPENSIFY_NATIVE_PROXY_ENV = {
|
|
277
147
|
USE_NGROK: "true",
|
|
278
148
|
NGROK_URL: "http://localhost:9000/",
|
|
279
149
|
SECURE_NGROK_URL: "http://localhost:9000/"
|
|
280
150
|
};
|
|
151
|
+
var MATTERMOST_DIR = (0, import_node_path.join)(HOME, "github/mattermost-mobile");
|
|
281
152
|
var UNISWAP_REPO_DIR = (0, import_node_path.join)(HOME, "github/uniswap-interface");
|
|
282
153
|
var UNISWAP_APP_DIR = (0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/mobile");
|
|
283
154
|
var UNISWAP_ENV_LOCAL_FILE = (0, import_node_path.join)(UNISWAP_REPO_DIR, ".env.defaults.local");
|
|
@@ -433,6 +304,217 @@ function ensureUniswapForceUpgradePatched() {
|
|
|
433
304
|
notificationLegacyPatch
|
|
434
305
|
);
|
|
435
306
|
}
|
|
307
|
+
var JOPLIN_DIR = (0, import_node_path.join)(HOME, "github/joplin");
|
|
308
|
+
var JOPLIN_APP_DIR = (0, import_node_path.join)(JOPLIN_DIR, "packages/app-mobile");
|
|
309
|
+
var JOPLIN_WATCH_ROOTS = [
|
|
310
|
+
"packages/lib",
|
|
311
|
+
"packages/renderer",
|
|
312
|
+
"packages/turndown",
|
|
313
|
+
"packages/turndown-plugin-gfm",
|
|
314
|
+
"packages/editor",
|
|
315
|
+
"packages/tools",
|
|
316
|
+
"packages/utils",
|
|
317
|
+
"packages/fork-htmlparser2",
|
|
318
|
+
"packages/fork-uslug",
|
|
319
|
+
"packages/fork-sax",
|
|
320
|
+
"packages/htmlpack",
|
|
321
|
+
"packages/react-native-saf-x",
|
|
322
|
+
"packages/react-native-alarm-notification"
|
|
323
|
+
];
|
|
324
|
+
function ensureJoplinWatchmanConfigs() {
|
|
325
|
+
if (!(0, import_node_fs.existsSync)(JOPLIN_DIR)) return;
|
|
326
|
+
for (const rel of JOPLIN_WATCH_ROOTS) {
|
|
327
|
+
const dir = (0, import_node_path.join)(JOPLIN_DIR, rel);
|
|
328
|
+
if (!(0, import_node_fs.existsSync)(dir)) continue;
|
|
329
|
+
const cfg = (0, import_node_path.join)(dir, ".watchmanconfig");
|
|
330
|
+
if (!(0, import_node_fs.existsSync)(cfg)) (0, import_node_fs.writeFileSync)(cfg, "{}\n");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
var JOPLIN_BUILD_SENTINELS = [
|
|
334
|
+
"packages/lib/models/Setting.js",
|
|
335
|
+
"packages/turndown/lib/turndown.cjs.js",
|
|
336
|
+
"packages/app-mobile/pluginAssets/index.js"
|
|
337
|
+
];
|
|
338
|
+
function ensureJoplinBuilt() {
|
|
339
|
+
if (!(0, import_node_fs.existsSync)(JOPLIN_DIR)) return;
|
|
340
|
+
const missing = JOPLIN_BUILD_SENTINELS.filter(
|
|
341
|
+
(rel) => !(0, import_node_fs.existsSync)((0, import_node_path.join)(JOPLIN_DIR, rel))
|
|
342
|
+
);
|
|
343
|
+
if (missing.length === 0) return;
|
|
344
|
+
const { execSync } = require("node:child_process");
|
|
345
|
+
execSync("yarn buildParallel", {
|
|
346
|
+
cwd: JOPLIN_DIR,
|
|
347
|
+
stdio: "inherit",
|
|
348
|
+
env: { ...process.env, NO_FLIPPER: "1", CI: "" }
|
|
349
|
+
});
|
|
350
|
+
const stillMissing = JOPLIN_BUILD_SENTINELS.filter(
|
|
351
|
+
(rel) => !(0, import_node_fs.existsSync)((0, import_node_path.join)(JOPLIN_DIR, rel))
|
|
352
|
+
);
|
|
353
|
+
if (stillMissing.length > 0) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`joplin demo: yarn buildParallel did not produce: ${stillMissing.join(", ")}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
var RAINBOW_DIR = (0, import_node_path.join)(HOME, "github/rainbow");
|
|
360
|
+
var RAINBOW_GRAPHQL_DIR = (0, import_node_path.join)(RAINBOW_DIR, "src/graphql");
|
|
361
|
+
var RAINBOW_GRAPHQL_CONFIG_FILE = (0, import_node_path.join)(RAINBOW_GRAPHQL_DIR, "config.js");
|
|
362
|
+
var RAINBOW_NETWORKS_FILE = (0, import_node_path.join)(RAINBOW_DIR, "src/references/networks.json");
|
|
363
|
+
var RAINBOW_METADATA_BASE_URL = "https://metadata.p.rainbow.me";
|
|
364
|
+
var RAINBOW_METADATA_PROXY_PORT = 9011;
|
|
365
|
+
var RAINBOW_DEMO_METADATA_BASE_URL = `http://127.0.0.1:${RAINBOW_METADATA_PROXY_PORT}`;
|
|
366
|
+
var RAINBOW_PUBLIC_ENS_GRAPHQL_URL = "https://api.thegraph.com/subgraphs/name/ensdomains/ens";
|
|
367
|
+
var RAINBOW_DEMO_QUOTE_SIGNER = "0x0000000000000000000000000000000000000000";
|
|
368
|
+
var RAINBOW_DEMO_RELAY_URL = "https://relay.rainbow.me";
|
|
369
|
+
var RAINBOW_DEMO_MASTER_KEY = "sootsim-rainbow-demo-master-key-do-not-use-for-real-wallets";
|
|
370
|
+
var RAINBOW_DEMO_RPC_PROXY_BASE_URL = "https://rpc.rainbow.me/v1";
|
|
371
|
+
var RAINBOW_DEMO_RPC_API_KEY = "";
|
|
372
|
+
var RAINBOW_DEMO_PUBLIC_RPC_URLS = {
|
|
373
|
+
"1": "https://ethereum-rpc.publicnode.com"
|
|
374
|
+
};
|
|
375
|
+
var RAINBOW_DEMO_SERVICE_API_KEY = "sootsim-rainbow-demo-api-key";
|
|
376
|
+
var RAINBOW_DEMO_SECURE_WALLET_HASH_KEY = "0x736f6f7473696d2d7261696e626f772d64656d6f2d686173682d6b6579000000";
|
|
377
|
+
var RAINBOW_GRAPHQL_SENTINELS = [
|
|
378
|
+
"src/graphql/__generated__/ens.ts",
|
|
379
|
+
"src/graphql/__generated__/metadata.ts",
|
|
380
|
+
"src/graphql/__generated__/metadataPOST.ts"
|
|
381
|
+
];
|
|
382
|
+
function runRainbowSetupCommand(command, cwd) {
|
|
383
|
+
const { execSync } = require("node:child_process");
|
|
384
|
+
execSync(command, {
|
|
385
|
+
cwd,
|
|
386
|
+
stdio: "inherit",
|
|
387
|
+
env: {
|
|
388
|
+
...process.env,
|
|
389
|
+
METADATA_BASE_URL: RAINBOW_METADATA_BASE_URL,
|
|
390
|
+
RAINBOW_RELAY_QUOTE_SIGNER: process.env.RAINBOW_RELAY_QUOTE_SIGNER ?? RAINBOW_DEMO_QUOTE_SIGNER
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function ensureRainbowGraphqlConfig() {
|
|
395
|
+
if (!(0, import_node_fs.existsSync)(RAINBOW_GRAPHQL_DIR)) return;
|
|
396
|
+
const source = (0, import_node_fs.existsSync)(RAINBOW_GRAPHQL_CONFIG_FILE) ? (0, import_node_fs.readFileSync)(RAINBOW_GRAPHQL_CONFIG_FILE, "utf8") : "";
|
|
397
|
+
if (source.includes(RAINBOW_PUBLIC_ENS_GRAPHQL_URL) && source.includes(RAINBOW_METADATA_BASE_URL)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
(0, import_node_fs.writeFileSync)(
|
|
401
|
+
RAINBOW_GRAPHQL_CONFIG_FILE,
|
|
402
|
+
`exports.config = {
|
|
403
|
+
ens: {
|
|
404
|
+
__name: 'ens',
|
|
405
|
+
document: './queries/ens.graphql',
|
|
406
|
+
schema: {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
url: '${RAINBOW_PUBLIC_ENS_GRAPHQL_URL}',
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
metadata: {
|
|
412
|
+
__name: 'metadata',
|
|
413
|
+
document: './queries/metadata.graphql',
|
|
414
|
+
schema: { method: 'GET', url: '${RAINBOW_METADATA_BASE_URL}/v1/graph' },
|
|
415
|
+
},
|
|
416
|
+
metadataPOST: {
|
|
417
|
+
__name: 'metadataPOST',
|
|
418
|
+
document: './queries/metadata.graphql',
|
|
419
|
+
schema: { method: 'POST', url: '${RAINBOW_METADATA_BASE_URL}/v1/graph' },
|
|
420
|
+
},
|
|
421
|
+
arc: {
|
|
422
|
+
__name: 'arc',
|
|
423
|
+
document: './queries/arc.graphql',
|
|
424
|
+
schema: {
|
|
425
|
+
method: 'GET',
|
|
426
|
+
url: 'https://arc-graphql.rainbow.me/graphql',
|
|
427
|
+
headers: {
|
|
428
|
+
'x-api-key': 'ARC_GRAPHQL_API_KEY',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
arcDev: {
|
|
433
|
+
__name: 'arcDev',
|
|
434
|
+
document: './queries/arc.graphql',
|
|
435
|
+
schema: {
|
|
436
|
+
method: 'GET',
|
|
437
|
+
url: 'https://arc-graphql.rainbowdotme.workers.dev/graphql',
|
|
438
|
+
headers: {},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
function resolveRainbowDemoEnv() {
|
|
446
|
+
const env = {
|
|
447
|
+
ENABLE_DEV_MODE: "1",
|
|
448
|
+
IS_TESTING: "false",
|
|
449
|
+
METADATA_BASE_URL: process.env.RAINBOW_METADATA_BASE_URL ?? RAINBOW_DEMO_METADATA_BASE_URL,
|
|
450
|
+
ADDYS_API_KEY: process.env.ADDYS_API_KEY ?? RAINBOW_DEMO_SERVICE_API_KEY,
|
|
451
|
+
ADDYS_BASE_URL: process.env.ADDYS_BASE_URL ?? RAINBOW_DEMO_METADATA_BASE_URL,
|
|
452
|
+
PLATFORM_API_KEY: process.env.PLATFORM_API_KEY ?? RAINBOW_DEMO_SERVICE_API_KEY,
|
|
453
|
+
PLATFORM_BASE_URL: process.env.PLATFORM_BASE_URL ?? RAINBOW_DEMO_METADATA_BASE_URL,
|
|
454
|
+
RAINBOW_MASTER_KEY: process.env.RAINBOW_MASTER_KEY ?? RAINBOW_DEMO_MASTER_KEY,
|
|
455
|
+
RAINBOW_RELAY_QUOTE_SIGNER: process.env.RAINBOW_RELAY_QUOTE_SIGNER ?? RAINBOW_DEMO_QUOTE_SIGNER,
|
|
456
|
+
RAINBOW_RELAY_URL: process.env.RAINBOW_RELAY_URL ?? RAINBOW_DEMO_RELAY_URL,
|
|
457
|
+
RPC_PROXY_API_KEY_PROD: RAINBOW_DEMO_RPC_API_KEY,
|
|
458
|
+
RPC_PROXY_BASE_URL_PROD: process.env.RAINBOW_RPC_PROXY_BASE_URL ?? process.env.RPC_PROXY_BASE_URL_PROD ?? RAINBOW_DEMO_RPC_PROXY_BASE_URL,
|
|
459
|
+
SECURE_WALLET_HASH_KEY: process.env.SECURE_WALLET_HASH_KEY ?? RAINBOW_DEMO_SECURE_WALLET_HASH_KEY
|
|
460
|
+
};
|
|
461
|
+
const optionalEnvVars = [
|
|
462
|
+
"ADDYS_API_KEY",
|
|
463
|
+
"ADDYS_BASE_URL",
|
|
464
|
+
"IMGIX_DOMAIN",
|
|
465
|
+
"IMGIX_TOKEN",
|
|
466
|
+
"PLATFORM_API_KEY",
|
|
467
|
+
"PLATFORM_BASE_URL",
|
|
468
|
+
"RAINBOW_TEST_WALLET",
|
|
469
|
+
"RAINBOW_RELAY_API_KEY",
|
|
470
|
+
"RAINBOW_RELAY_URL",
|
|
471
|
+
"SECURE_WALLET_HASH_KEY",
|
|
472
|
+
"TOKEN_SEARCH_URL",
|
|
473
|
+
"WC_PROJECT_ID"
|
|
474
|
+
];
|
|
475
|
+
for (const key of optionalEnvVars) {
|
|
476
|
+
const value = process.env[key];
|
|
477
|
+
if (value) env[key] = value;
|
|
478
|
+
}
|
|
479
|
+
if (!env.WC_PROJECT_ID && process.env.RAINBOW_WALLETCONNECT_PROJECT_ID) {
|
|
480
|
+
env.WC_PROJECT_ID = process.env.RAINBOW_WALLETCONNECT_PROJECT_ID;
|
|
481
|
+
}
|
|
482
|
+
return env;
|
|
483
|
+
}
|
|
484
|
+
function ensureRainbowDemoNetworks() {
|
|
485
|
+
if (!(0, import_node_fs.existsSync)(RAINBOW_NETWORKS_FILE)) return;
|
|
486
|
+
const payload = JSON.parse((0, import_node_fs.readFileSync)(RAINBOW_NETWORKS_FILE, "utf8"));
|
|
487
|
+
if (!Array.isArray(payload.networks)) return;
|
|
488
|
+
let changed = false;
|
|
489
|
+
for (const network of payload.networks) {
|
|
490
|
+
const url = network.id ? RAINBOW_DEMO_PUBLIC_RPC_URLS[network.id] : void 0;
|
|
491
|
+
if (!url || !network.defaultRPC) continue;
|
|
492
|
+
if (network.defaultRPC.url === url) continue;
|
|
493
|
+
network.defaultRPC.url = url;
|
|
494
|
+
changed = true;
|
|
495
|
+
}
|
|
496
|
+
if (changed) {
|
|
497
|
+
(0, import_node_fs.writeFileSync)(RAINBOW_NETWORKS_FILE, `${JSON.stringify(payload)}
|
|
498
|
+
`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function ensureRainbowSetup() {
|
|
502
|
+
if (!(0, import_node_fs.existsSync)(RAINBOW_DIR)) return;
|
|
503
|
+
ensureRainbowGraphqlConfig();
|
|
504
|
+
if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(RAINBOW_GRAPHQL_DIR, "node_modules/.bin/graphql-codegen"))) {
|
|
505
|
+
runRainbowSetupCommand("fnm exec --using=22 yarn install", RAINBOW_GRAPHQL_DIR);
|
|
506
|
+
}
|
|
507
|
+
const missingGraphql = RAINBOW_GRAPHQL_SENTINELS.some(
|
|
508
|
+
(rel) => !(0, import_node_fs.existsSync)((0, import_node_path.join)(RAINBOW_DIR, rel))
|
|
509
|
+
);
|
|
510
|
+
if (missingGraphql) {
|
|
511
|
+
runRainbowSetupCommand("fnm exec --using=22 yarn codegen", RAINBOW_GRAPHQL_DIR);
|
|
512
|
+
}
|
|
513
|
+
if (!(0, import_node_fs.existsSync)(RAINBOW_NETWORKS_FILE)) {
|
|
514
|
+
runRainbowSetupCommand("fnm exec --using=22 yarn fetch:networks", RAINBOW_DIR);
|
|
515
|
+
}
|
|
516
|
+
ensureRainbowDemoNetworks();
|
|
517
|
+
}
|
|
436
518
|
var ARTSY_DIR = (0, import_node_path.join)(HOME, "github/eigen");
|
|
437
519
|
var ARTSY_KEYS_FILE = (0, import_node_path.join)(ARTSY_DIR, "keys.shared.json");
|
|
438
520
|
var ARTSY_METAFLAGS_FILE = (0, import_node_path.join)(ARTSY_DIR, "metaflags.json");
|
|
@@ -559,7 +641,24 @@ var APPS = [
|
|
|
559
641
|
dir: (0, import_node_path.join)(HOME, "takeout"),
|
|
560
642
|
preferredPort: 8086,
|
|
561
643
|
framework: "one",
|
|
562
|
-
|
|
644
|
+
// takeout needs more than Metro for the demo to actually work: better-auth
|
|
645
|
+
// (login), zero-cache (sync), and a postgres for both. `bun lite` brings
|
|
646
|
+
// up the orez-backed stack (PG + zero + s3 in one binary) plus the One
|
|
647
|
+
// dev server. takeout's env system shifts every port (web, pg, zero,
|
|
648
|
+
// minio) uniformly by PORT_OFFSET, so we derive offset = port - 8081
|
|
649
|
+
// (the base web port) to keep all backend ports clear of the default
|
|
650
|
+
// dev stack while pinning One to the demo slot. OREZ_DATA_DIR is isolated
|
|
651
|
+
// to a per-demo path so we don't fight a soot dev orez that may already
|
|
652
|
+
// be holding pglite locks on ~/takeout/.orez (soot can attach takeout as
|
|
653
|
+
// a project and spin up its own orez against the same data dir).
|
|
654
|
+
readyTimeoutMs: 24e4,
|
|
655
|
+
command: (p) => ({
|
|
656
|
+
cmd: "bun lite",
|
|
657
|
+
env: {
|
|
658
|
+
PORT_OFFSET: String(p - 8081),
|
|
659
|
+
OREZ_DATA_DIR: `${HOME}/.cache/sootsim-demo/takeout-orez`
|
|
660
|
+
}
|
|
661
|
+
})
|
|
563
662
|
},
|
|
564
663
|
{
|
|
565
664
|
name: "expensify",
|
|
@@ -606,6 +705,151 @@ var APPS = [
|
|
|
606
705
|
envVars: ["SOOTSIM_ARTSY_EMAIL", "SOOTSIM_ARTSY_PASSWORD"],
|
|
607
706
|
note: "auto-login reuses Artsy\u2019s built-in Maestro launch-arguments hook"
|
|
608
707
|
}
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "rainbow",
|
|
711
|
+
label: "Rainbow",
|
|
712
|
+
dir: RAINBOW_DIR,
|
|
713
|
+
preferredPort: 8089,
|
|
714
|
+
framework: "rock",
|
|
715
|
+
sidecars: [
|
|
716
|
+
{
|
|
717
|
+
name: "metadata-proxy",
|
|
718
|
+
port: RAINBOW_METADATA_PROXY_PORT,
|
|
719
|
+
readyPath: "/health",
|
|
720
|
+
command: () => ({
|
|
721
|
+
cmd: `bun ${JSON.stringify(getRainbowMetadataProxyScript())}`,
|
|
722
|
+
env: {
|
|
723
|
+
PORT: String(RAINBOW_METADATA_PROXY_PORT),
|
|
724
|
+
RAINBOW_PUBLIC_RPC_URLS: JSON.stringify(RAINBOW_DEMO_PUBLIC_RPC_URLS),
|
|
725
|
+
RAINBOW_UPSTREAM_METADATA_BASE_URL: RAINBOW_METADATA_BASE_URL
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
],
|
|
730
|
+
prepare: () => {
|
|
731
|
+
ensureRainbowSetup();
|
|
732
|
+
},
|
|
733
|
+
command: (p) => ({
|
|
734
|
+
cmd: `fnm exec --using=22 yarn start --port ${p} --reset-cache`,
|
|
735
|
+
env: resolveRainbowDemoEnv()
|
|
736
|
+
}),
|
|
737
|
+
credentials: {
|
|
738
|
+
envVars: [
|
|
739
|
+
"RAINBOW_TEST_WALLET",
|
|
740
|
+
"RAINBOW_WALLETCONNECT_PROJECT_ID",
|
|
741
|
+
"WC_PROJECT_ID",
|
|
742
|
+
"IMGIX_DOMAIN",
|
|
743
|
+
"IMGIX_TOKEN"
|
|
744
|
+
],
|
|
745
|
+
note: "launcher supplies a demo encryption key and public mainnet RPC; use only a public throwaway mnemonic for RAINBOW_TEST_WALLET"
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: "rocket-chat",
|
|
750
|
+
label: "Rocket.Chat",
|
|
751
|
+
dir: (0, import_node_path.join)(HOME, "github/Rocket.Chat.ReactNative"),
|
|
752
|
+
preferredPort: 8093,
|
|
753
|
+
framework: "expo",
|
|
754
|
+
command: (p) => ({
|
|
755
|
+
cmd: `npx react-native start --port ${p}`,
|
|
756
|
+
env: { RUNNING_E2E_TESTS: "true" }
|
|
757
|
+
}),
|
|
758
|
+
credentials: {
|
|
759
|
+
envVars: [
|
|
760
|
+
"ROCKET_CHAT_DEMO_SERVER",
|
|
761
|
+
"ROCKET_CHAT_DEMO_USERNAME",
|
|
762
|
+
"ROCKET_CHAT_DEMO_PASSWORD"
|
|
763
|
+
],
|
|
764
|
+
known: { SERVER: "https://mobile.qa.rocket.chat" },
|
|
765
|
+
note: "use packages/sootsim-engine/scripts/rocket-chat-demo-auth.ts to create/login a disposable QA user and print the rocketchat://auth deep link"
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "mattermost",
|
|
770
|
+
label: "Mattermost",
|
|
771
|
+
dir: MATTERMOST_DIR,
|
|
772
|
+
preferredPort: 8090,
|
|
773
|
+
framework: "expo",
|
|
774
|
+
runtimeConfig: {
|
|
775
|
+
modules: {
|
|
776
|
+
// mattermost patches react-native-keychain's JS entry in its own
|
|
777
|
+
// repo; run that package and provide only the native manager seam.
|
|
778
|
+
"react-native-keychain": false,
|
|
779
|
+
"dist/assets/config.json": {
|
|
780
|
+
inline: {
|
|
781
|
+
AuthUrlScheme: "mmauth://",
|
|
782
|
+
AuthUrlSchemeDev: "mmauthbeta://",
|
|
783
|
+
DefaultServerUrl: "http://localhost:8065",
|
|
784
|
+
DefaultServerName: "Mattermost Demo",
|
|
785
|
+
TestServerUrl: "http://localhost:8065",
|
|
786
|
+
AutoSelectServerUrl: true,
|
|
787
|
+
WebsiteURL: "https://mattermost.com",
|
|
788
|
+
ServerNoticeURL: "https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt",
|
|
789
|
+
MobileNoticeURL: "https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt",
|
|
790
|
+
RudderApiKey: "",
|
|
791
|
+
SentryEnabled: false,
|
|
792
|
+
SentryDsnIos: "",
|
|
793
|
+
SentryDsnAndroid: "",
|
|
794
|
+
SentryOptions: {
|
|
795
|
+
deactivateStacktraceMerging: true,
|
|
796
|
+
autoBreadcrumbs: {
|
|
797
|
+
xhr: false,
|
|
798
|
+
console: true
|
|
799
|
+
},
|
|
800
|
+
severityLevelFilter: ["fatal"]
|
|
801
|
+
},
|
|
802
|
+
ShowReview: false,
|
|
803
|
+
ShowOnboarding: false,
|
|
804
|
+
ExperimentalNormalizeMarkdownLinks: false,
|
|
805
|
+
CustomRequestHeaders: {},
|
|
806
|
+
CollectNetworkMetrics: false
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
nativeModules: {
|
|
811
|
+
RNKeychainManager: { file: getMattermostKeychainNativeModule() },
|
|
812
|
+
RNUtils: { file: getMattermostRNUtilsNativeModule() },
|
|
813
|
+
GenericClient: { file: getMattermostNetworkClientNativeModule() },
|
|
814
|
+
ApiClient: { file: getMattermostNetworkClientNativeModule() },
|
|
815
|
+
WebSocketClient: { file: getMattermostNetworkClientNativeModule() }
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
command: (p) => ({
|
|
819
|
+
cmd: `npx react-native start --host 127.0.0.1 --port ${p}`
|
|
820
|
+
}),
|
|
821
|
+
credentials: {
|
|
822
|
+
known: {
|
|
823
|
+
SERVER: "http://localhost:8065",
|
|
824
|
+
USERNAME: "demo",
|
|
825
|
+
PASSWORD: "DemoPassword1!"
|
|
826
|
+
},
|
|
827
|
+
note: "local mattermost-preview seeded through the real REST API; no signup or OTP needed"
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "joplin",
|
|
832
|
+
label: "Joplin",
|
|
833
|
+
dir: JOPLIN_APP_DIR,
|
|
834
|
+
preferredPort: 8084,
|
|
835
|
+
framework: "expo",
|
|
836
|
+
// joplin is local-first: sync.target defaults to 0 ("None") and the
|
|
837
|
+
// mobile startup runs WelcomeUtils.install() on first launch, which
|
|
838
|
+
// seeds a "Welcome!" folder + welcome notes. no login or external
|
|
839
|
+
// credentials are needed for a usable demo — just boot it. tenant
|
|
840
|
+
// bedrock SQLite (via react-native-sqlite-storage stub) carries the
|
|
841
|
+
// seeded notes across reloads.
|
|
842
|
+
prepare: () => {
|
|
843
|
+
ensureJoplinWatchmanConfigs();
|
|
844
|
+
ensureJoplinBuilt();
|
|
845
|
+
},
|
|
846
|
+
command: (p) => ({
|
|
847
|
+
cmd: `npx expo start --port ${p}`,
|
|
848
|
+
env: { BROWSERSLIST_IGNORE_OLD_DATA: "true" }
|
|
849
|
+
}),
|
|
850
|
+
credentials: {
|
|
851
|
+
note: "no login required \u2014 sync.target=0 (None), seed via WelcomeUtils"
|
|
852
|
+
}
|
|
609
853
|
}
|
|
610
854
|
];
|
|
611
855
|
|
|
@@ -630,10 +874,10 @@ function tcpPing(port, timeout = TCP_GATE_MS) {
|
|
|
630
874
|
sock.connect(port, "localhost");
|
|
631
875
|
});
|
|
632
876
|
}
|
|
633
|
-
function httpGet(port,
|
|
877
|
+
function httpGet(port, path7, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
|
|
634
878
|
return new Promise((resolve2) => {
|
|
635
879
|
const req = import_http.default.request(
|
|
636
|
-
{ hostname: "localhost", port, path:
|
|
880
|
+
{ hostname: "localhost", port, path: path7, method, timeout, headers },
|
|
637
881
|
(res) => {
|
|
638
882
|
let body = "";
|
|
639
883
|
res.on("data", (c) => body += c.toString());
|
|
@@ -766,9 +1010,9 @@ function applyManifest(result, manifestRes, buildIconProxyUrl) {
|
|
|
766
1010
|
if (client.name) result.projectName = client.name;
|
|
767
1011
|
if (client.ios?.bundleIdentifier) result.bundleId = client.ios.bundleIdentifier;
|
|
768
1012
|
if (result.framework === "metro" && client.sdkVersion) result.framework = "expo";
|
|
769
|
-
const
|
|
770
|
-
if (
|
|
771
|
-
result.bundleUrl = withRuntimeConfig(result.port,
|
|
1013
|
+
const launchUrl2 = manifest?.launchAsset?.url;
|
|
1014
|
+
if (launchUrl2 && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
|
|
1015
|
+
result.bundleUrl = withRuntimeConfig(result.port, launchUrl2);
|
|
772
1016
|
}
|
|
773
1017
|
const rawIconUrl = client.iconUrl || client.ios?.iconUrl || client.icon || client.ios?.icon;
|
|
774
1018
|
if (rawIconUrl) {
|
|
@@ -792,6 +1036,7 @@ function applyManifest(result, manifestRes, buildIconProxyUrl) {
|
|
|
792
1036
|
}
|
|
793
1037
|
var knownNonPatched = /* @__PURE__ */ new Set();
|
|
794
1038
|
var knownNonExpo = /* @__PURE__ */ new Set();
|
|
1039
|
+
var knownOne = /* @__PURE__ */ new Set();
|
|
795
1040
|
async function probePort(port, buildIconProxyUrl) {
|
|
796
1041
|
if (!await tcpPing(port)) return null;
|
|
797
1042
|
const onePath = `/node_modules/one/metro-entry.bundle?platform=ios&dev=true`;
|
|
@@ -799,7 +1044,7 @@ async function probePort(port, buildIconProxyUrl) {
|
|
|
799
1044
|
knownNonPatched.has(port) ? Promise.resolve(null) : httpGet(port, "/__soot/"),
|
|
800
1045
|
httpGet(port, "/status"),
|
|
801
1046
|
httpGet(port, onePath, "HEAD"),
|
|
802
|
-
httpGet(port, "/", "GET", TIMEOUT_MS, { "expo-platform": "ios" }),
|
|
1047
|
+
knownOne.has(port) ? Promise.resolve(null) : httpGet(port, "/", "GET", TIMEOUT_MS, { "expo-platform": "ios" }),
|
|
803
1048
|
knownNonExpo.has(port) ? Promise.resolve(null) : httpGet(port, "/_expo/status")
|
|
804
1049
|
]);
|
|
805
1050
|
if (expoRes && expoRes.statusCode === 200) {
|
|
@@ -809,6 +1054,7 @@ async function probePort(port, buildIconProxyUrl) {
|
|
|
809
1054
|
}
|
|
810
1055
|
if (oneRes && oneRes.statusCode > 0 && oneRes.statusCode < 400) {
|
|
811
1056
|
knownNonPatched.add(port);
|
|
1057
|
+
knownOne.add(port);
|
|
812
1058
|
return applyManifest(
|
|
813
1059
|
{
|
|
814
1060
|
port,
|
|
@@ -837,13 +1083,13 @@ async function probePort(port, buildIconProxyUrl) {
|
|
|
837
1083
|
const manifest = JSON.parse(manifestRes.body);
|
|
838
1084
|
const client = manifest?.extra?.expoClient || {};
|
|
839
1085
|
if (client.name) {
|
|
840
|
-
const
|
|
1086
|
+
const launchUrl2 = manifest?.launchAsset?.url || `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`;
|
|
841
1087
|
knownNonPatched.add(port);
|
|
842
1088
|
return applyManifest(
|
|
843
1089
|
{
|
|
844
1090
|
port,
|
|
845
1091
|
framework: "one",
|
|
846
|
-
bundleUrl: withRuntimeConfig(port,
|
|
1092
|
+
bundleUrl: withRuntimeConfig(port, launchUrl2),
|
|
847
1093
|
hmrUrl: `ws://localhost:${port}/hot`,
|
|
848
1094
|
lastSeen: Date.now()
|
|
849
1095
|
},
|
|
@@ -912,6 +1158,9 @@ async function scanDevServers(opts = {}) {
|
|
|
912
1158
|
for (const p of [...knownNonExpo]) {
|
|
913
1159
|
if (!currentPorts.has(p)) knownNonExpo.delete(p);
|
|
914
1160
|
}
|
|
1161
|
+
for (const p of [...knownOne]) {
|
|
1162
|
+
if (!currentPorts.has(p)) knownOne.delete(p);
|
|
1163
|
+
}
|
|
915
1164
|
const results = [];
|
|
916
1165
|
const toProbe = [];
|
|
917
1166
|
for (const { port, pid } of processes) {
|
|
@@ -923,6 +1172,7 @@ async function scanDevServers(opts = {}) {
|
|
|
923
1172
|
if (cached && cached.pid !== pid) {
|
|
924
1173
|
knownNonPatched.delete(port);
|
|
925
1174
|
knownNonExpo.delete(port);
|
|
1175
|
+
knownOne.delete(port);
|
|
926
1176
|
}
|
|
927
1177
|
toProbe.push({ port, pid });
|
|
928
1178
|
}
|
|
@@ -940,27 +1190,422 @@ async function scanDevServers(opts = {}) {
|
|
|
940
1190
|
for (const { port, pid } of processes) {
|
|
941
1191
|
if (pid > 0) pidByPort.set(port, pid);
|
|
942
1192
|
}
|
|
943
|
-
await Promise.all(
|
|
944
|
-
results.map(async (result) => {
|
|
945
|
-
const pid = pidByPort.get(result.port);
|
|
946
|
-
if (!pid) return;
|
|
947
|
-
result.pid = pid;
|
|
948
|
-
const cwd = await resolveProcessCwd(pid);
|
|
949
|
-
if (cwd) result.cwd = cwd;
|
|
950
|
-
})
|
|
951
|
-
);
|
|
952
|
-
const livePids = new Set(pidByPort.values());
|
|
953
|
-
for (const pid of [...cwdByPid.keys()]) {
|
|
954
|
-
if (!livePids.has(pid)) cwdByPid.delete(pid);
|
|
1193
|
+
await Promise.all(
|
|
1194
|
+
results.map(async (result) => {
|
|
1195
|
+
const pid = pidByPort.get(result.port);
|
|
1196
|
+
if (!pid) return;
|
|
1197
|
+
result.pid = pid;
|
|
1198
|
+
const cwd = await resolveProcessCwd(pid);
|
|
1199
|
+
if (cwd) result.cwd = cwd;
|
|
1200
|
+
})
|
|
1201
|
+
);
|
|
1202
|
+
const livePids = new Set(pidByPort.values());
|
|
1203
|
+
for (const pid of [...cwdByPid.keys()]) {
|
|
1204
|
+
if (!livePids.has(pid)) cwdByPid.delete(pid);
|
|
1205
|
+
}
|
|
1206
|
+
return results.filter((r) => !isSootSelfServer(r));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/bridge-constants.ts
|
|
1210
|
+
var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
|
|
1211
|
+
|
|
1212
|
+
// src/home-paths.ts
|
|
1213
|
+
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
1214
|
+
var import_node_os2 = require("node:os");
|
|
1215
|
+
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
1216
|
+
var SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
|
|
1217
|
+
var ACTIVE_RUNTIME_FILE = "active";
|
|
1218
|
+
var DAEMON_LOCKFILE = "daemon.json";
|
|
1219
|
+
var CONFIG_FILE = "config.json";
|
|
1220
|
+
var DAEMON_HEARTBEAT_STALE_MS = 3e4;
|
|
1221
|
+
function sootsimHomeDir() {
|
|
1222
|
+
const override = process.env[SOOTSIM_HOME_ENV];
|
|
1223
|
+
if (override && override.length > 0) return import_node_path2.default.resolve(override);
|
|
1224
|
+
return import_node_path2.default.join((0, import_node_os2.homedir)(), ".sootsim");
|
|
1225
|
+
}
|
|
1226
|
+
function runtimesDir() {
|
|
1227
|
+
return import_node_path2.default.join(sootsimHomeDir(), "runtimes");
|
|
1228
|
+
}
|
|
1229
|
+
function runtimeDir(version) {
|
|
1230
|
+
return import_node_path2.default.join(runtimesDir(), version);
|
|
1231
|
+
}
|
|
1232
|
+
function activeRuntimeFile() {
|
|
1233
|
+
return import_node_path2.default.join(runtimesDir(), ACTIVE_RUNTIME_FILE);
|
|
1234
|
+
}
|
|
1235
|
+
function electronDir() {
|
|
1236
|
+
return import_node_path2.default.join(sootsimHomeDir(), "electron");
|
|
1237
|
+
}
|
|
1238
|
+
function electronUserDataDir() {
|
|
1239
|
+
return import_node_path2.default.join(electronDir(), "userData");
|
|
1240
|
+
}
|
|
1241
|
+
function profilesDir() {
|
|
1242
|
+
return import_node_path2.default.join(sootsimHomeDir(), "profiles");
|
|
1243
|
+
}
|
|
1244
|
+
function cacheDir() {
|
|
1245
|
+
return import_node_path2.default.join(sootsimHomeDir(), "cache");
|
|
1246
|
+
}
|
|
1247
|
+
function daemonLockfilePath() {
|
|
1248
|
+
return import_node_path2.default.join(sootsimHomeDir(), DAEMON_LOCKFILE);
|
|
1249
|
+
}
|
|
1250
|
+
function configFilePath() {
|
|
1251
|
+
return import_node_path2.default.join(sootsimHomeDir(), CONFIG_FILE);
|
|
1252
|
+
}
|
|
1253
|
+
function readSharedConfig() {
|
|
1254
|
+
try {
|
|
1255
|
+
const raw = import_node_fs2.default.readFileSync(configFilePath(), "utf8");
|
|
1256
|
+
const parsed = JSON.parse(raw);
|
|
1257
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1258
|
+
} catch {
|
|
1259
|
+
return {};
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function writeSharedConfig(patch) {
|
|
1263
|
+
ensureSootsimHome();
|
|
1264
|
+
const current = readSharedConfig();
|
|
1265
|
+
const next = { ...current, ...patch };
|
|
1266
|
+
if (patch.settings && typeof patch.settings === "object") {
|
|
1267
|
+
next.settings = {
|
|
1268
|
+
...current.settings && typeof current.settings === "object" ? current.settings : {},
|
|
1269
|
+
...patch.settings
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
const tmp = `${configFilePath()}.tmp`;
|
|
1273
|
+
import_node_fs2.default.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}
|
|
1274
|
+
`, "utf8");
|
|
1275
|
+
import_node_fs2.default.renameSync(tmp, configFilePath());
|
|
1276
|
+
return next;
|
|
1277
|
+
}
|
|
1278
|
+
function ensureSootsimHome() {
|
|
1279
|
+
import_node_fs2.default.mkdirSync(sootsimHomeDir(), { recursive: true });
|
|
1280
|
+
import_node_fs2.default.mkdirSync(runtimesDir(), { recursive: true });
|
|
1281
|
+
import_node_fs2.default.mkdirSync(electronDir(), { recursive: true });
|
|
1282
|
+
import_node_fs2.default.mkdirSync(profilesDir(), { recursive: true });
|
|
1283
|
+
import_node_fs2.default.mkdirSync(cacheDir(), { recursive: true });
|
|
1284
|
+
}
|
|
1285
|
+
function readActiveRuntime() {
|
|
1286
|
+
try {
|
|
1287
|
+
const value = import_node_fs2.default.readFileSync(activeRuntimeFile(), "utf8").trim();
|
|
1288
|
+
return value.length > 0 ? value : null;
|
|
1289
|
+
} catch {
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function writeActiveRuntime(version) {
|
|
1294
|
+
import_node_fs2.default.mkdirSync(runtimesDir(), { recursive: true });
|
|
1295
|
+
import_node_fs2.default.writeFileSync(activeRuntimeFile(), `${version}
|
|
1296
|
+
`, "utf8");
|
|
1297
|
+
}
|
|
1298
|
+
function listInstalledRuntimes() {
|
|
1299
|
+
try {
|
|
1300
|
+
return import_node_fs2.default.readdirSync(runtimesDir(), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort(compareSemver);
|
|
1301
|
+
} catch {
|
|
1302
|
+
return [];
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function compareSemver(a, b) {
|
|
1306
|
+
const parse = (v) => {
|
|
1307
|
+
const hyphen = v.indexOf("-");
|
|
1308
|
+
const core = hyphen >= 0 ? v.slice(0, hyphen) : v;
|
|
1309
|
+
const pre = hyphen >= 0 ? v.slice(hyphen + 1) : "";
|
|
1310
|
+
const parts = core.split(".").map((n) => Number.parseInt(n, 10));
|
|
1311
|
+
if (parts.some((n) => !Number.isFinite(n))) return [[Number.POSITIVE_INFINITY], v];
|
|
1312
|
+
return [parts, pre];
|
|
1313
|
+
};
|
|
1314
|
+
const [an, ap] = parse(a);
|
|
1315
|
+
const [bn, bp] = parse(b);
|
|
1316
|
+
for (let i = 0; i < Math.max(an.length, bn.length); i++) {
|
|
1317
|
+
const av = an[i] ?? 0;
|
|
1318
|
+
const bv = bn[i] ?? 0;
|
|
1319
|
+
if (av !== bv) return av - bv;
|
|
1320
|
+
}
|
|
1321
|
+
if (ap === bp) return 0;
|
|
1322
|
+
if (!ap) return 1;
|
|
1323
|
+
if (!bp) return -1;
|
|
1324
|
+
return ap < bp ? -1 : 1;
|
|
1325
|
+
}
|
|
1326
|
+
function activeRuntimeDir() {
|
|
1327
|
+
const version = readActiveRuntime();
|
|
1328
|
+
if (!version) return null;
|
|
1329
|
+
const dir = runtimeDir(version);
|
|
1330
|
+
try {
|
|
1331
|
+
if (import_node_fs2.default.statSync(dir).isDirectory()) return dir;
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
|
|
1337
|
+
function readDaemonLockfile() {
|
|
1338
|
+
try {
|
|
1339
|
+
const fd = import_node_fs2.default.openSync(daemonLockfilePath(), "r");
|
|
1340
|
+
try {
|
|
1341
|
+
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
|
|
1342
|
+
const bytesRead = import_node_fs2.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
|
|
1343
|
+
const raw = buf.subarray(0, bytesRead).toString("utf8");
|
|
1344
|
+
const parsed = JSON.parse(raw);
|
|
1345
|
+
if (parsed && parsed.schema === 1 && typeof parsed.pid === "number" && typeof parsed.bridgePort === "number" && typeof parsed.runtimePort === "number" && typeof parsed.startedAt === "number" && typeof parsed.heartbeatAt === "number") {
|
|
1346
|
+
return parsed;
|
|
1347
|
+
}
|
|
1348
|
+
return null;
|
|
1349
|
+
} finally {
|
|
1350
|
+
import_node_fs2.default.closeSync(fd);
|
|
1351
|
+
}
|
|
1352
|
+
} catch {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function isDaemonLockfileFresh(lock, now = Date.now()) {
|
|
1357
|
+
if (!lock) return false;
|
|
1358
|
+
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
|
|
1359
|
+
try {
|
|
1360
|
+
process.kill(lock.pid, 0);
|
|
1361
|
+
return true;
|
|
1362
|
+
} catch {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function writeDaemonLockfile(data) {
|
|
1367
|
+
ensureSootsimHome();
|
|
1368
|
+
const tmp = `${daemonLockfilePath()}.tmp`;
|
|
1369
|
+
import_node_fs2.default.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
|
|
1370
|
+
`, "utf8");
|
|
1371
|
+
import_node_fs2.default.renameSync(tmp, daemonLockfilePath());
|
|
1372
|
+
}
|
|
1373
|
+
function claimDaemonLockfile(data) {
|
|
1374
|
+
ensureSootsimHome();
|
|
1375
|
+
const existing = readDaemonLockfile();
|
|
1376
|
+
if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
writeDaemonLockfile(data);
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
function removeDaemonLockfile() {
|
|
1383
|
+
try {
|
|
1384
|
+
import_node_fs2.default.unlinkSync(daemonLockfilePath());
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/runtime-delivery.ts
|
|
1390
|
+
var import_child_process2 = require("child_process");
|
|
1391
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
1392
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
1393
|
+
var import_path = __toESM(require("path"), 1);
|
|
1394
|
+
var import_stream = require("stream");
|
|
1395
|
+
var import_promises = require("stream/promises");
|
|
1396
|
+
var DEFAULT_RUNTIME_CDN_ORIGIN = "https://sootbean.com";
|
|
1397
|
+
var RUNTIME_CDN_ORIGIN_ENV = "SOOTSIM_CDN_ORIGIN";
|
|
1398
|
+
var RUNTIME_CHANNEL_ENV = "SOOTSIM_RUNTIME_CHANNEL";
|
|
1399
|
+
function readConfig() {
|
|
1400
|
+
try {
|
|
1401
|
+
const parsed = JSON.parse(import_fs.default.readFileSync(configFilePath(), "utf8"));
|
|
1402
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1403
|
+
} catch {
|
|
1404
|
+
return {};
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function resolveRuntimeCdnOrigin(input) {
|
|
1408
|
+
const config = readConfig();
|
|
1409
|
+
const value = input || process.env[RUNTIME_CDN_ORIGIN_ENV] || config.cdnOrigin || DEFAULT_RUNTIME_CDN_ORIGIN;
|
|
1410
|
+
return value.replace(/\/+$/, "");
|
|
1411
|
+
}
|
|
1412
|
+
function resolveRuntimeChannel(input) {
|
|
1413
|
+
const config = readConfig();
|
|
1414
|
+
return input || process.env[RUNTIME_CHANNEL_ENV] || config.runtimeChannel || "stable";
|
|
1415
|
+
}
|
|
1416
|
+
function runtimeManifestUrl(cdnOrigin) {
|
|
1417
|
+
const url = new URL(`${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/manifest.json`);
|
|
1418
|
+
url.searchParams.set("t", String(Date.now()));
|
|
1419
|
+
return url.toString();
|
|
1420
|
+
}
|
|
1421
|
+
function runtimeTarballUrl(version, cdnOrigin) {
|
|
1422
|
+
return `${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/sootsim-runtime-${version}.tar.gz`;
|
|
1423
|
+
}
|
|
1424
|
+
async function fetchRuntimeManifest(cdnOrigin) {
|
|
1425
|
+
const url = runtimeManifestUrl(cdnOrigin);
|
|
1426
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
1427
|
+
if (!res.ok) {
|
|
1428
|
+
throw new Error(`manifest fetch failed: ${res.status} ${res.statusText} (${url})`);
|
|
1429
|
+
}
|
|
1430
|
+
return await res.json();
|
|
1431
|
+
}
|
|
1432
|
+
function resolveRuntimeVersion(manifest, opts = {}) {
|
|
1433
|
+
const channel = resolveRuntimeChannel(opts.channel);
|
|
1434
|
+
const version = opts.version || manifest.channels[channel]?.latest;
|
|
1435
|
+
if (!version) {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
`no version specified and channel '${channel}' has no latest entry in the manifest`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
const entry = manifest.versions[version];
|
|
1441
|
+
if (!entry) {
|
|
1442
|
+
throw new Error(
|
|
1443
|
+
`version ${version} not found in manifest; available: ${Object.keys(manifest.versions).slice(-10).join(", ") || "(none)"}`
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
return { version, channel, entry };
|
|
1447
|
+
}
|
|
1448
|
+
async function installRuntime(opts = {}) {
|
|
1449
|
+
ensureSootsimHome();
|
|
1450
|
+
const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin);
|
|
1451
|
+
const manifest = await fetchRuntimeManifest(cdnOrigin);
|
|
1452
|
+
const { version, channel, entry } = resolveRuntimeVersion(manifest, opts);
|
|
1453
|
+
const destDir = runtimeDir(version);
|
|
1454
|
+
const setActive = opts.setActive !== false;
|
|
1455
|
+
if (!opts.force && import_fs.default.existsSync(import_path.default.join(destDir, "index.html"))) {
|
|
1456
|
+
if (setActive) writeActiveRuntime(version);
|
|
1457
|
+
return {
|
|
1458
|
+
version,
|
|
1459
|
+
channel,
|
|
1460
|
+
cdnOrigin,
|
|
1461
|
+
runtimeDir: destDir,
|
|
1462
|
+
installed: false,
|
|
1463
|
+
activated: setActive,
|
|
1464
|
+
manifest
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
const tarUrl = entry.tarball || runtimeTarballUrl(version, cdnOrigin);
|
|
1468
|
+
const tarCachePath = import_path.default.join(cacheDir(), `sootsim-runtime-${version}.tar.gz`);
|
|
1469
|
+
process.stderr.write(`sootsim: downloading runtime ${version}\u2026
|
|
1470
|
+
`);
|
|
1471
|
+
await downloadToFile(tarUrl, tarCachePath);
|
|
1472
|
+
process.stderr.write(`sootsim: extracting runtime ${version}\u2026
|
|
1473
|
+
`);
|
|
1474
|
+
const actualSha = await sha256File(tarCachePath);
|
|
1475
|
+
if (actualSha !== entry.sha256) {
|
|
1476
|
+
import_fs.default.rmSync(tarCachePath, { force: true });
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`sha256 mismatch for runtime ${version}: expected ${entry.sha256}, actual ${actualSha}`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
const tmpDir = import_path.default.join(import_path.default.dirname(destDir), `.installing-${version}-${process.pid}`);
|
|
1482
|
+
import_fs.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
1483
|
+
import_fs.default.mkdirSync(tmpDir, { recursive: true });
|
|
1484
|
+
try {
|
|
1485
|
+
await extractTarball(tarCachePath, tmpDir);
|
|
1486
|
+
if (!import_fs.default.existsSync(import_path.default.join(tmpDir, "index.html"))) {
|
|
1487
|
+
throw new Error(`extracted tarball for runtime ${version} is missing index.html`);
|
|
1488
|
+
}
|
|
1489
|
+
import_fs.default.rmSync(destDir, { recursive: true, force: true });
|
|
1490
|
+
import_fs.default.renameSync(tmpDir, destDir);
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
import_fs.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
1493
|
+
throw err;
|
|
1494
|
+
}
|
|
1495
|
+
if (setActive) writeActiveRuntime(version);
|
|
1496
|
+
return {
|
|
1497
|
+
version,
|
|
1498
|
+
channel,
|
|
1499
|
+
cdnOrigin,
|
|
1500
|
+
runtimeDir: destDir,
|
|
1501
|
+
installed: true,
|
|
1502
|
+
activated: setActive,
|
|
1503
|
+
manifest
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function updateRuntimeToLatest(opts = {}) {
|
|
1507
|
+
ensureSootsimHome();
|
|
1508
|
+
const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin);
|
|
1509
|
+
const channel = resolveRuntimeChannel(opts.channel);
|
|
1510
|
+
const manifest = await fetchRuntimeManifest(cdnOrigin);
|
|
1511
|
+
const latestVersion = manifest.channels[channel]?.latest;
|
|
1512
|
+
if (!latestVersion) {
|
|
1513
|
+
return {
|
|
1514
|
+
checked: true,
|
|
1515
|
+
updated: false,
|
|
1516
|
+
reason: `channel '${channel}' has no latest runtime`,
|
|
1517
|
+
activeVersion: readActiveRuntime()
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const entry = manifest.versions[latestVersion];
|
|
1521
|
+
if (!entry) {
|
|
1522
|
+
return {
|
|
1523
|
+
checked: true,
|
|
1524
|
+
updated: false,
|
|
1525
|
+
reason: `manifest is missing version ${latestVersion}`,
|
|
1526
|
+
activeVersion: readActiveRuntime(),
|
|
1527
|
+
latestVersion
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
const activeVersion = readActiveRuntime();
|
|
1531
|
+
const activeDir = activeVersion ? runtimeDir(activeVersion) : null;
|
|
1532
|
+
const activeInstalled = activeDir ? import_fs.default.existsSync(import_path.default.join(activeDir, "index.html")) : false;
|
|
1533
|
+
const shouldInstall = !activeVersion || !activeInstalled || compareSemver(latestVersion, activeVersion) > 0;
|
|
1534
|
+
if (!shouldInstall) {
|
|
1535
|
+
return {
|
|
1536
|
+
checked: true,
|
|
1537
|
+
updated: false,
|
|
1538
|
+
reason: "active runtime is current",
|
|
1539
|
+
activeVersion,
|
|
1540
|
+
latestVersion
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
const install = await installRuntime({
|
|
1544
|
+
version: latestVersion,
|
|
1545
|
+
channel,
|
|
1546
|
+
cdnOrigin,
|
|
1547
|
+
setActive: false
|
|
1548
|
+
});
|
|
1549
|
+
return {
|
|
1550
|
+
checked: true,
|
|
1551
|
+
updated: true,
|
|
1552
|
+
activeVersion: latestVersion,
|
|
1553
|
+
latestVersion,
|
|
1554
|
+
install
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
async function downloadToFile(url, destPath) {
|
|
1558
|
+
const res = await fetch(url);
|
|
1559
|
+
if (!res.ok || !res.body) {
|
|
1560
|
+
throw new Error(`download failed: ${res.status} ${res.statusText} (${url})`);
|
|
955
1561
|
}
|
|
956
|
-
|
|
1562
|
+
import_fs.default.mkdirSync(import_path.default.dirname(destPath), { recursive: true });
|
|
1563
|
+
const tmp = `${destPath}.partial`;
|
|
1564
|
+
try {
|
|
1565
|
+
await (0, import_promises.pipeline)(
|
|
1566
|
+
import_stream.Readable.fromWeb(res.body),
|
|
1567
|
+
import_fs.default.createWriteStream(tmp)
|
|
1568
|
+
);
|
|
1569
|
+
import_fs.default.renameSync(tmp, destPath);
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
try {
|
|
1572
|
+
import_fs.default.unlinkSync(tmp);
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
throw err;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function sha256File(filePath) {
|
|
1579
|
+
return new Promise((resolve2, reject) => {
|
|
1580
|
+
const hash = import_crypto.default.createHash("sha256");
|
|
1581
|
+
const stream = import_fs.default.createReadStream(filePath);
|
|
1582
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1583
|
+
stream.on("error", reject);
|
|
1584
|
+
stream.on("end", () => resolve2(hash.digest("hex")));
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
function extractTarball(tarPath, destDir) {
|
|
1588
|
+
return new Promise((resolve2, reject) => {
|
|
1589
|
+
const child = (0, import_child_process2.spawn)("tar", ["-xzf", tarPath, "-C", destDir], {
|
|
1590
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
1591
|
+
});
|
|
1592
|
+
child.on("error", reject);
|
|
1593
|
+
child.on("exit", (code) => {
|
|
1594
|
+
if (code === 0) resolve2();
|
|
1595
|
+
else reject(new Error(`tar exited with code ${code}`));
|
|
1596
|
+
});
|
|
1597
|
+
});
|
|
957
1598
|
}
|
|
958
1599
|
|
|
1600
|
+
// src/host/agent-host.ts
|
|
1601
|
+
var import_node_fs5 = __toESM(require("node:fs"), 1);
|
|
1602
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
1603
|
+
|
|
959
1604
|
// src/agent-sessions.ts
|
|
960
1605
|
var import_node_child_process = require("node:child_process");
|
|
961
1606
|
var import_node_crypto2 = require("node:crypto");
|
|
962
|
-
var
|
|
963
|
-
var
|
|
1607
|
+
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
1608
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
964
1609
|
var import_node_readline = __toESM(require("node:readline"), 1);
|
|
965
1610
|
|
|
966
1611
|
// src/agent-events.ts
|
|
@@ -1004,37 +1649,20 @@ function encodeAgentPromptEnvelope(input) {
|
|
|
1004
1649
|
|
|
1005
1650
|
// src/attached-projects.ts
|
|
1006
1651
|
var import_node_crypto = require("node:crypto");
|
|
1007
|
-
var
|
|
1008
|
-
var
|
|
1009
|
-
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
1652
|
+
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
1653
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
1010
1654
|
var overrideDir = null;
|
|
1011
1655
|
function userDataDir() {
|
|
1012
1656
|
if (overrideDir) return overrideDir;
|
|
1013
1657
|
const fromEnv = process.env.SOOTSIM_USER_DATA_DIR;
|
|
1014
1658
|
if (fromEnv) return fromEnv;
|
|
1015
|
-
|
|
1016
|
-
const electron = require("electron");
|
|
1017
|
-
if (electron.app?.getPath) return electron.app.getPath("userData");
|
|
1018
|
-
} catch {
|
|
1019
|
-
}
|
|
1020
|
-
return platformDefaultUserDataDir();
|
|
1021
|
-
}
|
|
1022
|
-
function platformDefaultUserDataDir() {
|
|
1023
|
-
const home = import_node_os2.default.homedir();
|
|
1024
|
-
if (process.platform === "darwin") {
|
|
1025
|
-
return import_node_path2.default.join(home, "Library", "Application Support", "sootsim");
|
|
1026
|
-
}
|
|
1027
|
-
if (process.platform === "win32") {
|
|
1028
|
-
return import_node_path2.default.join(process.env.APPDATA || home, "sootsim");
|
|
1029
|
-
}
|
|
1030
|
-
const xdg = process.env.XDG_CONFIG_HOME || import_node_path2.default.join(home, ".config");
|
|
1031
|
-
return import_node_path2.default.join(xdg, "sootsim");
|
|
1659
|
+
return electronUserDataDir();
|
|
1032
1660
|
}
|
|
1033
1661
|
function getUserDataDir() {
|
|
1034
1662
|
return userDataDir();
|
|
1035
1663
|
}
|
|
1036
1664
|
function storeFile() {
|
|
1037
|
-
return
|
|
1665
|
+
return import_node_path3.default.join(userDataDir(), "attached-projects.json");
|
|
1038
1666
|
}
|
|
1039
1667
|
function cloneEmpty() {
|
|
1040
1668
|
return {
|
|
@@ -1048,7 +1676,7 @@ function loadStore() {
|
|
|
1048
1676
|
const file = storeFile();
|
|
1049
1677
|
let raw;
|
|
1050
1678
|
try {
|
|
1051
|
-
raw =
|
|
1679
|
+
raw = import_node_fs3.default.readFileSync(file, "utf8");
|
|
1052
1680
|
} catch (err) {
|
|
1053
1681
|
if (err.code === "ENOENT") return cloneEmpty();
|
|
1054
1682
|
throw err;
|
|
@@ -1065,7 +1693,7 @@ function loadStore() {
|
|
|
1065
1693
|
} catch (err) {
|
|
1066
1694
|
const quarantine = `${file}.corrupt-${Date.now()}`;
|
|
1067
1695
|
try {
|
|
1068
|
-
|
|
1696
|
+
import_node_fs3.default.renameSync(file, quarantine);
|
|
1069
1697
|
console.warn(
|
|
1070
1698
|
`[sootsim] attached-projects.json was unparseable; quarantined to ${quarantine}. original error: ${err.message}`
|
|
1071
1699
|
);
|
|
@@ -1076,16 +1704,16 @@ function loadStore() {
|
|
|
1076
1704
|
}
|
|
1077
1705
|
function writeStore(store) {
|
|
1078
1706
|
const file = storeFile();
|
|
1079
|
-
|
|
1707
|
+
import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
|
|
1080
1708
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
1081
|
-
const fd =
|
|
1709
|
+
const fd = import_node_fs3.default.openSync(tmp, "w", 384);
|
|
1082
1710
|
try {
|
|
1083
|
-
|
|
1084
|
-
|
|
1711
|
+
import_node_fs3.default.writeFileSync(fd, JSON.stringify(store, null, 2));
|
|
1712
|
+
import_node_fs3.default.fsyncSync(fd);
|
|
1085
1713
|
} finally {
|
|
1086
|
-
|
|
1714
|
+
import_node_fs3.default.closeSync(fd);
|
|
1087
1715
|
}
|
|
1088
|
-
|
|
1716
|
+
import_node_fs3.default.renameSync(tmp, file);
|
|
1089
1717
|
}
|
|
1090
1718
|
function mutateStore(fn) {
|
|
1091
1719
|
const store = loadStore();
|
|
@@ -1094,13 +1722,13 @@ function mutateStore(fn) {
|
|
|
1094
1722
|
return store;
|
|
1095
1723
|
}
|
|
1096
1724
|
function projectIdForCwd(cwd) {
|
|
1097
|
-
return (0, import_node_crypto.createHash)("sha256").update(
|
|
1725
|
+
return (0, import_node_crypto.createHash)("sha256").update(import_node_path3.default.resolve(cwd)).digest("hex").slice(0, 16);
|
|
1098
1726
|
}
|
|
1099
1727
|
function newSessionId() {
|
|
1100
1728
|
return `s_${(0, import_node_crypto.randomBytes)(10).toString("hex")}`;
|
|
1101
1729
|
}
|
|
1102
1730
|
function upsertProject(input) {
|
|
1103
|
-
const cwd =
|
|
1731
|
+
const cwd = import_node_path3.default.resolve(input.cwd);
|
|
1104
1732
|
const id = projectIdForCwd(cwd);
|
|
1105
1733
|
let result;
|
|
1106
1734
|
mutateStore((store) => {
|
|
@@ -1126,7 +1754,7 @@ function upsertProject(input) {
|
|
|
1126
1754
|
const now = Date.now();
|
|
1127
1755
|
const created = {
|
|
1128
1756
|
id,
|
|
1129
|
-
name: input.name ??
|
|
1757
|
+
name: input.name ?? import_node_path3.default.basename(cwd),
|
|
1130
1758
|
cwd,
|
|
1131
1759
|
repoRoot: input.repoRoot,
|
|
1132
1760
|
sourceRoots: input.sourceRoots ?? [cwd],
|
|
@@ -1266,8 +1894,8 @@ async function seedFromDemoAppRegistry() {
|
|
|
1266
1894
|
const apps = APPS2;
|
|
1267
1895
|
mutateStore((store) => {
|
|
1268
1896
|
for (const app of apps) {
|
|
1269
|
-
if (!
|
|
1270
|
-
const cwd =
|
|
1897
|
+
if (!import_node_fs3.default.existsSync(app.dir)) continue;
|
|
1898
|
+
const cwd = import_node_path3.default.resolve(app.dir);
|
|
1271
1899
|
const id = projectIdForCwd(cwd);
|
|
1272
1900
|
if (store.attachedProjects.some((p) => p.id === id)) continue;
|
|
1273
1901
|
const now = Date.now();
|
|
@@ -1291,16 +1919,16 @@ async function seedFromDemoAppRegistry() {
|
|
|
1291
1919
|
|
|
1292
1920
|
// src/agent-sessions.ts
|
|
1293
1921
|
function sessionDir(sessionId) {
|
|
1294
|
-
return
|
|
1922
|
+
return import_node_path4.default.join(getUserDataDir(), "sessions", sessionId);
|
|
1295
1923
|
}
|
|
1296
1924
|
function promptFifoPath(sessionId) {
|
|
1297
|
-
return
|
|
1925
|
+
return import_node_path4.default.join(sessionDir(sessionId), "prompt.in");
|
|
1298
1926
|
}
|
|
1299
1927
|
function eventsFifoPath(sessionId) {
|
|
1300
|
-
return
|
|
1928
|
+
return import_node_path4.default.join(sessionDir(sessionId), "events.out");
|
|
1301
1929
|
}
|
|
1302
1930
|
function transcriptPath(sessionId) {
|
|
1303
|
-
return
|
|
1931
|
+
return import_node_path4.default.join(getUserDataDir(), "transcripts", `${sessionId}.log`);
|
|
1304
1932
|
}
|
|
1305
1933
|
function pidIsAlive(pid, sessionId) {
|
|
1306
1934
|
if (!pid) return false;
|
|
@@ -1310,7 +1938,7 @@ function pidIsAlive(pid, sessionId) {
|
|
|
1310
1938
|
return false;
|
|
1311
1939
|
}
|
|
1312
1940
|
if (sessionId) {
|
|
1313
|
-
if (!
|
|
1941
|
+
if (!import_node_fs4.default.existsSync(sessionDir(sessionId))) return false;
|
|
1314
1942
|
}
|
|
1315
1943
|
return true;
|
|
1316
1944
|
}
|
|
@@ -1322,11 +1950,11 @@ function resolveSootsimInvocation() {
|
|
|
1322
1950
|
const resourcesPath = process.resourcesPath;
|
|
1323
1951
|
if (resourcesPath) {
|
|
1324
1952
|
const candidates = [
|
|
1325
|
-
|
|
1326
|
-
|
|
1953
|
+
import_node_path4.default.join(resourcesPath, "bin", "sootsim"),
|
|
1954
|
+
import_node_path4.default.join(resourcesPath, "bin", `sootsim-${process.platform}-${process.arch}`)
|
|
1327
1955
|
];
|
|
1328
1956
|
for (const c of candidates) {
|
|
1329
|
-
if (
|
|
1957
|
+
if (import_node_fs4.default.existsSync(c)) return { cmd: c, prefixArgs: [] };
|
|
1330
1958
|
}
|
|
1331
1959
|
}
|
|
1332
1960
|
}
|
|
@@ -1349,15 +1977,15 @@ function tryWorkspaceSootsim() {
|
|
|
1349
1977
|
const sootsimDir = resolveSootsimPackageDir();
|
|
1350
1978
|
if (!sootsimDir) return null;
|
|
1351
1979
|
const binaryName = `sootsim-${process.platform}-${process.arch}`;
|
|
1352
|
-
const distBinary =
|
|
1353
|
-
if (
|
|
1354
|
-
const distBin =
|
|
1355
|
-
if (
|
|
1980
|
+
const distBinary = import_node_path4.default.join(sootsimDir, "dist-bin", binaryName);
|
|
1981
|
+
if (import_node_fs4.default.existsSync(distBinary)) return { cmd: distBinary, prefixArgs: [] };
|
|
1982
|
+
const distBin = import_node_path4.default.join(sootsimDir, "dist-cli", "bin.js");
|
|
1983
|
+
if (import_node_fs4.default.existsSync(distBin)) {
|
|
1356
1984
|
try {
|
|
1357
|
-
const src =
|
|
1358
|
-
if (
|
|
1359
|
-
const srcMtime =
|
|
1360
|
-
const buildMtime =
|
|
1985
|
+
const src = import_node_path4.default.join(sootsimDir, "cli", "commands", "agent-wrapper.ts");
|
|
1986
|
+
if (import_node_fs4.default.existsSync(src)) {
|
|
1987
|
+
const srcMtime = import_node_fs4.default.statSync(src).mtimeMs;
|
|
1988
|
+
const buildMtime = import_node_fs4.default.statSync(distBin).mtimeMs;
|
|
1361
1989
|
if (buildMtime < srcMtime) {
|
|
1362
1990
|
console.warn(
|
|
1363
1991
|
`[sootsim] dist-cli/bin.js is older than agent-wrapper.ts \u2014 rebuild with \`bun run --cwd packages/sootsim build:cli\` (watch:cli:binary builds dist-bin/ instead).`
|
|
@@ -1376,22 +2004,22 @@ function tryWorkspaceSootsim() {
|
|
|
1376
2004
|
function resolveSootsimPackageDir() {
|
|
1377
2005
|
try {
|
|
1378
2006
|
const resolved = require.resolve("sootsim/package.json");
|
|
1379
|
-
return
|
|
2007
|
+
return import_node_path4.default.dirname(resolved);
|
|
1380
2008
|
} catch {
|
|
1381
2009
|
}
|
|
1382
2010
|
const here = fileFromImportMeta();
|
|
1383
2011
|
if (!here) return null;
|
|
1384
|
-
let cur =
|
|
2012
|
+
let cur = import_node_path4.default.dirname(here);
|
|
1385
2013
|
for (let i = 0; i < 8; i++) {
|
|
1386
|
-
const pkg =
|
|
2014
|
+
const pkg = import_node_path4.default.join(cur, "package.json");
|
|
1387
2015
|
try {
|
|
1388
|
-
if (
|
|
1389
|
-
const parsed = JSON.parse(
|
|
2016
|
+
if (import_node_fs4.default.existsSync(pkg)) {
|
|
2017
|
+
const parsed = JSON.parse(import_node_fs4.default.readFileSync(pkg, "utf8"));
|
|
1390
2018
|
if (parsed.name === "sootsim") return cur;
|
|
1391
2019
|
}
|
|
1392
2020
|
} catch {
|
|
1393
2021
|
}
|
|
1394
|
-
const parent =
|
|
2022
|
+
const parent = import_node_path4.default.dirname(cur);
|
|
1395
2023
|
if (parent === cur) break;
|
|
1396
2024
|
cur = parent;
|
|
1397
2025
|
}
|
|
@@ -1407,28 +2035,28 @@ function fileFromImportMeta() {
|
|
|
1407
2035
|
}
|
|
1408
2036
|
}
|
|
1409
2037
|
async function withStartLock(projectId, provider, fn) {
|
|
1410
|
-
const lockDir =
|
|
1411
|
-
|
|
2038
|
+
const lockDir = import_node_path4.default.join(getUserDataDir(), "locks");
|
|
2039
|
+
import_node_fs4.default.mkdirSync(lockDir, { recursive: true });
|
|
1412
2040
|
try {
|
|
1413
|
-
|
|
2041
|
+
import_node_fs4.default.chmodSync(lockDir, 448);
|
|
1414
2042
|
} catch {
|
|
1415
2043
|
}
|
|
1416
|
-
const lockPath =
|
|
2044
|
+
const lockPath = import_node_path4.default.join(lockDir, `start-${projectId}-${provider}.lock`);
|
|
1417
2045
|
const deadline = Date.now() + 4e3;
|
|
1418
2046
|
let fd = null;
|
|
1419
2047
|
while (fd === null) {
|
|
1420
2048
|
try {
|
|
1421
|
-
fd =
|
|
2049
|
+
fd = import_node_fs4.default.openSync(
|
|
1422
2050
|
lockPath,
|
|
1423
|
-
|
|
2051
|
+
import_node_fs4.constants.O_WRONLY | import_node_fs4.constants.O_CREAT | import_node_fs4.constants.O_EXCL,
|
|
1424
2052
|
384
|
|
1425
2053
|
);
|
|
1426
2054
|
} catch (err) {
|
|
1427
2055
|
if (err.code !== "EEXIST") throw err;
|
|
1428
2056
|
try {
|
|
1429
|
-
const stale = Number(
|
|
2057
|
+
const stale = Number(import_node_fs4.default.readFileSync(lockPath, "utf8").trim());
|
|
1430
2058
|
if (stale && !isProcessAlive(stale)) {
|
|
1431
|
-
|
|
2059
|
+
import_node_fs4.default.unlinkSync(lockPath);
|
|
1432
2060
|
continue;
|
|
1433
2061
|
}
|
|
1434
2062
|
} catch {
|
|
@@ -1442,15 +2070,15 @@ async function withStartLock(projectId, provider, fn) {
|
|
|
1442
2070
|
}
|
|
1443
2071
|
}
|
|
1444
2072
|
try {
|
|
1445
|
-
|
|
2073
|
+
import_node_fs4.default.writeFileSync(fd, String(process.pid));
|
|
1446
2074
|
return await fn();
|
|
1447
2075
|
} finally {
|
|
1448
2076
|
try {
|
|
1449
|
-
|
|
2077
|
+
import_node_fs4.default.closeSync(fd);
|
|
1450
2078
|
} catch {
|
|
1451
2079
|
}
|
|
1452
2080
|
try {
|
|
1453
|
-
|
|
2081
|
+
import_node_fs4.default.unlinkSync(lockPath);
|
|
1454
2082
|
} catch {
|
|
1455
2083
|
}
|
|
1456
2084
|
}
|
|
@@ -1464,25 +2092,25 @@ function isProcessAlive(pid) {
|
|
|
1464
2092
|
}
|
|
1465
2093
|
}
|
|
1466
2094
|
function mkfifoSync(p) {
|
|
1467
|
-
const parent =
|
|
1468
|
-
|
|
2095
|
+
const parent = import_node_path4.default.dirname(p);
|
|
2096
|
+
import_node_fs4.default.mkdirSync(parent, { recursive: true });
|
|
1469
2097
|
try {
|
|
1470
|
-
|
|
2098
|
+
import_node_fs4.default.chmodSync(parent, 448);
|
|
1471
2099
|
} catch {
|
|
1472
2100
|
}
|
|
1473
|
-
if (
|
|
2101
|
+
if (import_node_fs4.default.existsSync(p)) {
|
|
1474
2102
|
try {
|
|
1475
|
-
const stat =
|
|
2103
|
+
const stat = import_node_fs4.default.statSync(p);
|
|
1476
2104
|
if (stat.isFIFO()) {
|
|
1477
2105
|
try {
|
|
1478
|
-
|
|
2106
|
+
import_node_fs4.default.chmodSync(p, 384);
|
|
1479
2107
|
} catch {
|
|
1480
2108
|
}
|
|
1481
2109
|
return;
|
|
1482
2110
|
}
|
|
1483
|
-
|
|
2111
|
+
import_node_fs4.default.unlinkSync(p);
|
|
1484
2112
|
} catch {
|
|
1485
|
-
|
|
2113
|
+
import_node_fs4.default.unlinkSync(p);
|
|
1486
2114
|
}
|
|
1487
2115
|
}
|
|
1488
2116
|
const result = (0, import_node_child_process.spawnSync)("mkfifo", ["-m", "600", p]);
|
|
@@ -1529,10 +2157,10 @@ async function startSession(opts) {
|
|
|
1529
2157
|
const transcript = transcriptPath(session.id);
|
|
1530
2158
|
mkfifoSync(promptIn);
|
|
1531
2159
|
mkfifoSync(eventsOut);
|
|
1532
|
-
const transcriptDir =
|
|
1533
|
-
|
|
2160
|
+
const transcriptDir = import_node_path4.default.dirname(transcript);
|
|
2161
|
+
import_node_fs4.default.mkdirSync(transcriptDir, { recursive: true });
|
|
1534
2162
|
try {
|
|
1535
|
-
|
|
2163
|
+
import_node_fs4.default.chmodSync(transcriptDir, 448);
|
|
1536
2164
|
} catch {
|
|
1537
2165
|
}
|
|
1538
2166
|
const { cmd, prefixArgs } = resolveSootsimInvocation();
|
|
@@ -1556,6 +2184,7 @@ async function startSession(opts) {
|
|
|
1556
2184
|
];
|
|
1557
2185
|
if (opts.codexBin) wrapperArgs.push("--codex-bin", opts.codexBin);
|
|
1558
2186
|
if (opts.claudeBin) wrapperArgs.push("--claude-bin", opts.claudeBin);
|
|
2187
|
+
if (opts.freshThread) wrapperArgs.push("--fresh-thread");
|
|
1559
2188
|
if (claudeSessionUuid) {
|
|
1560
2189
|
wrapperArgs.push("--claude-session-uuid", claudeSessionUuid);
|
|
1561
2190
|
}
|
|
@@ -1582,7 +2211,7 @@ async function startSession(opts) {
|
|
|
1582
2211
|
}
|
|
1583
2212
|
}
|
|
1584
2213
|
try {
|
|
1585
|
-
|
|
2214
|
+
import_node_fs4.default.rmSync(sessionDir(session.id), { recursive: true, force: true });
|
|
1586
2215
|
} catch {
|
|
1587
2216
|
}
|
|
1588
2217
|
updateSessionStatus(session.id, { status: "ended" });
|
|
@@ -1610,18 +2239,18 @@ async function sendPrompt(sessionId, prompt) {
|
|
|
1610
2239
|
);
|
|
1611
2240
|
}
|
|
1612
2241
|
const fifo = promptFifoPath(sessionId);
|
|
1613
|
-
if (!
|
|
2242
|
+
if (!import_node_fs4.default.existsSync(fifo)) {
|
|
1614
2243
|
throw new AgentSessionError("NO_FIFO", `prompt FIFO missing: ${fifo}`);
|
|
1615
2244
|
}
|
|
1616
|
-
const fd =
|
|
2245
|
+
const fd = import_node_fs4.default.openSync(fifo, import_node_fs4.constants.O_WRONLY);
|
|
1617
2246
|
try {
|
|
1618
2247
|
const wireText = encodeAgentPromptEnvelope(prompt);
|
|
1619
2248
|
if (!wireText) {
|
|
1620
2249
|
throw new AgentSessionError("EMPTY_PROMPT", "prompt text is empty");
|
|
1621
2250
|
}
|
|
1622
|
-
|
|
2251
|
+
import_node_fs4.default.writeSync(fd, wireText + "\n");
|
|
1623
2252
|
} finally {
|
|
1624
|
-
|
|
2253
|
+
import_node_fs4.default.closeSync(fd);
|
|
1625
2254
|
}
|
|
1626
2255
|
updateSessionStatus(sessionId, {
|
|
1627
2256
|
lastPrompt: prompt.displayText ?? prompt.text,
|
|
@@ -1643,7 +2272,7 @@ async function endSession(sessionId) {
|
|
|
1643
2272
|
const base = getUserDataDir();
|
|
1644
2273
|
if (dir.startsWith(base)) {
|
|
1645
2274
|
try {
|
|
1646
|
-
|
|
2275
|
+
import_node_fs4.default.rmSync(dir, { recursive: true, force: true });
|
|
1647
2276
|
} catch {
|
|
1648
2277
|
}
|
|
1649
2278
|
}
|
|
@@ -1651,11 +2280,11 @@ async function endSession(sessionId) {
|
|
|
1651
2280
|
}
|
|
1652
2281
|
function subscribeEvents(sessionId, onEvent) {
|
|
1653
2282
|
const fifo = eventsFifoPath(sessionId);
|
|
1654
|
-
if (!
|
|
2283
|
+
if (!import_node_fs4.default.existsSync(fifo)) {
|
|
1655
2284
|
throw new AgentSessionError("NO_FIFO", `events FIFO missing: ${fifo}`);
|
|
1656
2285
|
}
|
|
1657
|
-
const fd =
|
|
1658
|
-
const stream =
|
|
2286
|
+
const fd = import_node_fs4.default.openSync(fifo, import_node_fs4.constants.O_RDWR);
|
|
2287
|
+
const stream = import_node_fs4.default.createReadStream("", { fd, autoClose: true });
|
|
1659
2288
|
const rl = import_node_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1660
2289
|
rl.on("line", (line) => {
|
|
1661
2290
|
const event = parseAgentEventLine(line);
|
|
@@ -1676,7 +2305,7 @@ function subscribeEvents(sessionId, onEvent) {
|
|
|
1676
2305
|
};
|
|
1677
2306
|
}
|
|
1678
2307
|
async function waitForFirstEvent(fifo, predicate, timeoutMs) {
|
|
1679
|
-
const fd =
|
|
2308
|
+
const fd = import_node_fs4.default.openSync(fifo, import_node_fs4.constants.O_RDWR | import_node_fs4.constants.O_NONBLOCK);
|
|
1680
2309
|
const buf = Buffer.alloc(8192);
|
|
1681
2310
|
let leftover = "";
|
|
1682
2311
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1684,7 +2313,7 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
|
|
|
1684
2313
|
while (Date.now() < deadline) {
|
|
1685
2314
|
let n = 0;
|
|
1686
2315
|
try {
|
|
1687
|
-
n =
|
|
2316
|
+
n = import_node_fs4.default.readSync(fd, buf, 0, buf.length, null);
|
|
1688
2317
|
} catch (err) {
|
|
1689
2318
|
if (err.code !== "EAGAIN") throw err;
|
|
1690
2319
|
n = 0;
|
|
@@ -1704,7 +2333,7 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
|
|
|
1704
2333
|
}
|
|
1705
2334
|
return null;
|
|
1706
2335
|
} finally {
|
|
1707
|
-
|
|
2336
|
+
import_node_fs4.default.closeSync(fd);
|
|
1708
2337
|
}
|
|
1709
2338
|
}
|
|
1710
2339
|
|
|
@@ -1875,7 +2504,7 @@ var AgentHost = class {
|
|
|
1875
2504
|
);
|
|
1876
2505
|
const project = upsertProject({
|
|
1877
2506
|
cwd: match.cwd,
|
|
1878
|
-
name: match.projectName ??
|
|
2507
|
+
name: match.projectName ?? import_node_path5.default.basename(match.cwd),
|
|
1879
2508
|
preferredProvider: input.provider ?? existing?.preferredProvider,
|
|
1880
2509
|
sourceRoots: existing?.sourceRoots ?? [match.cwd],
|
|
1881
2510
|
knownBundleUrls,
|
|
@@ -1886,18 +2515,18 @@ var AgentHost = class {
|
|
|
1886
2515
|
}
|
|
1887
2516
|
getTranscript(sessionId) {
|
|
1888
2517
|
const p = transcriptPath(sessionId);
|
|
1889
|
-
if (!
|
|
2518
|
+
if (!import_node_fs5.default.existsSync(p)) {
|
|
1890
2519
|
return { error: "transcript not found", code: "NO_TRANSCRIPT" };
|
|
1891
2520
|
}
|
|
1892
|
-
return
|
|
2521
|
+
return import_node_fs5.default.readFileSync(p, "utf8");
|
|
1893
2522
|
}
|
|
1894
2523
|
getPaths() {
|
|
1895
2524
|
const dir = getUserDataDir();
|
|
1896
2525
|
return {
|
|
1897
2526
|
userDataDir: dir,
|
|
1898
|
-
storeFile:
|
|
1899
|
-
sessionsDir:
|
|
1900
|
-
transcriptsDir:
|
|
2527
|
+
storeFile: import_node_path5.default.join(dir, "attached-projects.json"),
|
|
2528
|
+
sessionsDir: import_node_path5.default.join(dir, "sessions"),
|
|
2529
|
+
transcriptsDir: import_node_path5.default.join(dir, "transcripts")
|
|
1901
2530
|
};
|
|
1902
2531
|
}
|
|
1903
2532
|
// --- subscription management ---
|
|
@@ -2157,6 +2786,356 @@ function mapFrameworkToProjectFramework(fw) {
|
|
|
2157
2786
|
return "unknown";
|
|
2158
2787
|
}
|
|
2159
2788
|
|
|
2789
|
+
// src/host/fetch-proxy-handler.ts
|
|
2790
|
+
var import_http2 = __toESM(require("http"), 1);
|
|
2791
|
+
var import_https = __toESM(require("https"), 1);
|
|
2792
|
+
var FETCH_PROXY_USER_AGENT = "sootsim";
|
|
2793
|
+
var STRIP_FETCH_PROXY_HEADERS = /* @__PURE__ */ new Set([
|
|
2794
|
+
"host",
|
|
2795
|
+
"origin",
|
|
2796
|
+
"referer",
|
|
2797
|
+
"user-agent",
|
|
2798
|
+
"cookie",
|
|
2799
|
+
"connection",
|
|
2800
|
+
"keep-alive",
|
|
2801
|
+
"transfer-encoding",
|
|
2802
|
+
"upgrade",
|
|
2803
|
+
"content-length",
|
|
2804
|
+
"sec-fetch-site",
|
|
2805
|
+
"sec-fetch-mode",
|
|
2806
|
+
"sec-fetch-dest",
|
|
2807
|
+
"sec-ch-ua",
|
|
2808
|
+
"sec-ch-ua-mobile",
|
|
2809
|
+
"sec-ch-ua-platform"
|
|
2810
|
+
]);
|
|
2811
|
+
var FETCH_PROXY_CORS_HEADERS = {
|
|
2812
|
+
"access-control-allow-origin": "*",
|
|
2813
|
+
"access-control-allow-methods": "GET,POST,PUT,DELETE,PATCH,OPTIONS",
|
|
2814
|
+
"access-control-allow-headers": "*",
|
|
2815
|
+
"access-control-expose-headers": "*",
|
|
2816
|
+
"access-control-max-age": "3600"
|
|
2817
|
+
};
|
|
2818
|
+
function applyFetchProxyCors(res) {
|
|
2819
|
+
for (const [key, value] of Object.entries(FETCH_PROXY_CORS_HEADERS)) {
|
|
2820
|
+
res.setHeader(key, value);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
function formatFetchProxyError(targetUrl, err) {
|
|
2824
|
+
const details = [];
|
|
2825
|
+
const error = err;
|
|
2826
|
+
if (error?.code) details.push(error.code);
|
|
2827
|
+
if (error?.message) details.push(error.message);
|
|
2828
|
+
if (error?.cause?.code) details.push(error.cause.code);
|
|
2829
|
+
if (error?.cause?.message) details.push(error.cause.message);
|
|
2830
|
+
const uniqueDetails = [...new Set(details.filter(Boolean))];
|
|
2831
|
+
const message = uniqueDetails.join(" | ") || String(err);
|
|
2832
|
+
if (targetUrl.includes("stored-in-.env.local")) {
|
|
2833
|
+
return `${message} | upstream url still contains placeholder env values`;
|
|
2834
|
+
}
|
|
2835
|
+
return message;
|
|
2836
|
+
}
|
|
2837
|
+
function buildFetchProxyHeaders(reqHeaders) {
|
|
2838
|
+
const headers = {};
|
|
2839
|
+
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
2840
|
+
if (!value) continue;
|
|
2841
|
+
if (STRIP_FETCH_PROXY_HEADERS.has(key.toLowerCase())) continue;
|
|
2842
|
+
headers[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
2843
|
+
}
|
|
2844
|
+
headers["user-agent"] = FETCH_PROXY_USER_AGENT;
|
|
2845
|
+
return headers;
|
|
2846
|
+
}
|
|
2847
|
+
function isFetchProxyRequestUrl(rawUrl) {
|
|
2848
|
+
return rawUrl?.startsWith("/__fetch-proxy?") || rawUrl?.startsWith("/__proxy?") || false;
|
|
2849
|
+
}
|
|
2850
|
+
function isAppApiRequestUrl(rawUrl) {
|
|
2851
|
+
if (!rawUrl) return false;
|
|
2852
|
+
if (rawUrl.startsWith("/__app-api?")) return true;
|
|
2853
|
+
if (rawUrl.startsWith("/__app-api/")) return true;
|
|
2854
|
+
return false;
|
|
2855
|
+
}
|
|
2856
|
+
async function handleFetchProxyRequest(req, res) {
|
|
2857
|
+
if (req.method === "OPTIONS") {
|
|
2858
|
+
applyFetchProxyCors(res);
|
|
2859
|
+
res.writeHead(204);
|
|
2860
|
+
res.end();
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
const params = new URLSearchParams((req.url || "").split("?")[1] || "");
|
|
2864
|
+
const targetUrl = params.get("url");
|
|
2865
|
+
if (!targetUrl) {
|
|
2866
|
+
applyFetchProxyCors(res);
|
|
2867
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2868
|
+
res.end("missing url param");
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
let upstreamUrl;
|
|
2872
|
+
try {
|
|
2873
|
+
upstreamUrl = new URL(targetUrl);
|
|
2874
|
+
} catch {
|
|
2875
|
+
applyFetchProxyCors(res);
|
|
2876
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2877
|
+
res.end("invalid url param");
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
const headers = buildFetchProxyHeaders(req.headers);
|
|
2881
|
+
let body;
|
|
2882
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2883
|
+
const chunks = [];
|
|
2884
|
+
for await (const chunk of req) {
|
|
2885
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2886
|
+
}
|
|
2887
|
+
if (chunks.length > 0) body = Buffer.concat(chunks);
|
|
2888
|
+
}
|
|
2889
|
+
let upstream;
|
|
2890
|
+
try {
|
|
2891
|
+
upstream = await fetch(upstreamUrl.href, {
|
|
2892
|
+
method: req.method,
|
|
2893
|
+
headers,
|
|
2894
|
+
body,
|
|
2895
|
+
redirect: "follow"
|
|
2896
|
+
});
|
|
2897
|
+
} catch (err) {
|
|
2898
|
+
applyFetchProxyCors(res);
|
|
2899
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
2900
|
+
res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
upstream.headers.forEach((value, key) => {
|
|
2904
|
+
const lowerKey = key.toLowerCase();
|
|
2905
|
+
if (lowerKey === "content-encoding" || lowerKey === "transfer-encoding" || lowerKey === "content-length" || lowerKey === "set-cookie" || lowerKey.startsWith("access-control-")) {
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
res.setHeader(key, value);
|
|
2909
|
+
});
|
|
2910
|
+
applyFetchProxyCors(res);
|
|
2911
|
+
const setCookie = upstream.headers.getSetCookie?.() ?? [];
|
|
2912
|
+
if (setCookie.length > 0) {
|
|
2913
|
+
res.setHeader("x-sootsim-set-cookie", setCookie.join(", "));
|
|
2914
|
+
}
|
|
2915
|
+
res.statusCode = upstream.status;
|
|
2916
|
+
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
2917
|
+
res.end(buf);
|
|
2918
|
+
}
|
|
2919
|
+
function handleAppApiRequest(req, res) {
|
|
2920
|
+
const reqUrl = req.url || "";
|
|
2921
|
+
let targetPath = "";
|
|
2922
|
+
let targetOrigin = "";
|
|
2923
|
+
if (reqUrl.startsWith("/__app-api?")) {
|
|
2924
|
+
const parsed = new URL(reqUrl, "http://sootsim.local");
|
|
2925
|
+
targetPath = parsed.searchParams.get("path") || "";
|
|
2926
|
+
targetOrigin = parsed.searchParams.get("origin")?.trim() || "";
|
|
2927
|
+
} else if (reqUrl.startsWith("/__app-api/")) {
|
|
2928
|
+
targetPath = reqUrl.slice("/__app-api".length);
|
|
2929
|
+
} else {
|
|
2930
|
+
return false;
|
|
2931
|
+
}
|
|
2932
|
+
if (!targetOrigin) {
|
|
2933
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2934
|
+
res.end("app-api: missing origin query param");
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2937
|
+
if (req.method === "OPTIONS") {
|
|
2938
|
+
res.writeHead(204, {
|
|
2939
|
+
"Access-Control-Allow-Origin": req.headers.origin || "*",
|
|
2940
|
+
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
2941
|
+
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "*",
|
|
2942
|
+
"Access-Control-Allow-Credentials": "true",
|
|
2943
|
+
"Access-Control-Max-Age": "86400"
|
|
2944
|
+
});
|
|
2945
|
+
res.end();
|
|
2946
|
+
return true;
|
|
2947
|
+
}
|
|
2948
|
+
let targetUrl;
|
|
2949
|
+
try {
|
|
2950
|
+
targetUrl = new URL(targetPath, targetOrigin);
|
|
2951
|
+
} catch {
|
|
2952
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
2953
|
+
res.end("app-api: invalid origin or path");
|
|
2954
|
+
return true;
|
|
2955
|
+
}
|
|
2956
|
+
const transport = targetUrl.protocol === "https:" ? import_https.default : import_http2.default;
|
|
2957
|
+
const fwdHeaders = { ...req.headers };
|
|
2958
|
+
delete fwdHeaders.host;
|
|
2959
|
+
fwdHeaders.host = targetUrl.host;
|
|
2960
|
+
const proxyReq = transport.request(
|
|
2961
|
+
{
|
|
2962
|
+
hostname: targetUrl.hostname,
|
|
2963
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
2964
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
2965
|
+
method: req.method,
|
|
2966
|
+
headers: fwdHeaders
|
|
2967
|
+
},
|
|
2968
|
+
(proxyRes) => {
|
|
2969
|
+
const exposedHeaders = Object.keys(proxyRes.headers).filter((name) => {
|
|
2970
|
+
const lower = name.toLowerCase();
|
|
2971
|
+
return !lower.startsWith("access-control-") && lower !== "set-cookie";
|
|
2972
|
+
}).join(", ");
|
|
2973
|
+
res.writeHead(proxyRes.statusCode ?? 502, {
|
|
2974
|
+
...proxyRes.headers,
|
|
2975
|
+
"access-control-allow-origin": req.headers.origin || "*",
|
|
2976
|
+
"access-control-allow-credentials": "true",
|
|
2977
|
+
"access-control-expose-headers": exposedHeaders
|
|
2978
|
+
});
|
|
2979
|
+
proxyRes.pipe(res);
|
|
2980
|
+
}
|
|
2981
|
+
);
|
|
2982
|
+
proxyReq.on("error", (err) => {
|
|
2983
|
+
res.statusCode = 502;
|
|
2984
|
+
res.end(`app proxy error: ${err.message}`);
|
|
2985
|
+
});
|
|
2986
|
+
req.pipe(proxyReq);
|
|
2987
|
+
return true;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// src/host/open-url.ts
|
|
2991
|
+
var import_child_process3 = require("child_process");
|
|
2992
|
+
var import_fs2 = require("fs");
|
|
2993
|
+
var import_os = require("os");
|
|
2994
|
+
var import_path2 = require("path");
|
|
2995
|
+
var MAC_CHROMIUM_CANDIDATES = [
|
|
2996
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2997
|
+
"~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2998
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
2999
|
+
"~/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
3000
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
3001
|
+
"~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
3002
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
3003
|
+
"~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
3004
|
+
"/Applications/Arc.app/Contents/MacOS/Arc",
|
|
3005
|
+
"~/Applications/Arc.app/Contents/MacOS/Arc"
|
|
3006
|
+
];
|
|
3007
|
+
var LINUX_CHROMIUM_CANDIDATES = [
|
|
3008
|
+
"/usr/bin/google-chrome",
|
|
3009
|
+
"/usr/bin/chromium",
|
|
3010
|
+
"/usr/bin/chromium-browser",
|
|
3011
|
+
"/usr/bin/microsoft-edge",
|
|
3012
|
+
"/usr/bin/brave-browser",
|
|
3013
|
+
"/snap/bin/chromium"
|
|
3014
|
+
];
|
|
3015
|
+
var WINDOWS_CHROMIUM_CANDIDATES = [
|
|
3016
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
3017
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
3018
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
3019
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
3020
|
+
"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
|
|
3021
|
+
"C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
|
3022
|
+
];
|
|
3023
|
+
var UNIX_CHROMIUM_COMMANDS = [
|
|
3024
|
+
"google-chrome",
|
|
3025
|
+
"chromium",
|
|
3026
|
+
"chromium-browser",
|
|
3027
|
+
"microsoft-edge",
|
|
3028
|
+
"brave-browser"
|
|
3029
|
+
];
|
|
3030
|
+
var WINDOWS_CHROMIUM_COMMANDS = ["chrome", "msedge", "brave"];
|
|
3031
|
+
function expandHome(candidate) {
|
|
3032
|
+
return candidate.startsWith("~/") ? (0, import_path2.join)((0, import_os.homedir)(), candidate.slice(2)) : candidate;
|
|
3033
|
+
}
|
|
3034
|
+
function firstExisting(candidates) {
|
|
3035
|
+
for (const candidate of candidates) {
|
|
3036
|
+
const expanded = expandHome(candidate);
|
|
3037
|
+
if ((0, import_fs2.existsSync)(expanded)) return expanded;
|
|
3038
|
+
}
|
|
3039
|
+
return null;
|
|
3040
|
+
}
|
|
3041
|
+
function lookupExecutable(command, platform) {
|
|
3042
|
+
try {
|
|
3043
|
+
const tool = platform === "win32" ? "where" : "which";
|
|
3044
|
+
const out = (0, import_child_process3.execFileSync)(tool, [command], {
|
|
3045
|
+
encoding: "utf8",
|
|
3046
|
+
timeout: 1500,
|
|
3047
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3048
|
+
}).trim();
|
|
3049
|
+
return out ? out.split(/\r?\n/)[0] : null;
|
|
3050
|
+
} catch {
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
function resolveChromiumBinary(platform = process.platform) {
|
|
3055
|
+
const envCandidate = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
|
|
3056
|
+
if (envCandidate && (0, import_fs2.existsSync)(envCandidate)) return envCandidate;
|
|
3057
|
+
const direct = platform === "darwin" ? firstExisting(MAC_CHROMIUM_CANDIDATES) : platform === "win32" ? firstExisting(WINDOWS_CHROMIUM_CANDIDATES) : firstExisting(LINUX_CHROMIUM_CANDIDATES);
|
|
3058
|
+
if (direct) return direct;
|
|
3059
|
+
const commands = platform === "win32" ? WINDOWS_CHROMIUM_COMMANDS : UNIX_CHROMIUM_COMMANDS;
|
|
3060
|
+
for (const command of commands) {
|
|
3061
|
+
const found = lookupExecutable(command, platform);
|
|
3062
|
+
if (found) return found;
|
|
3063
|
+
}
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
function buildOpenUrlCommand(url, options = {}) {
|
|
3067
|
+
if (!url) throw new Error("openUrl requires a url");
|
|
3068
|
+
const platform = options.platform ?? process.platform;
|
|
3069
|
+
if (options.newWindow) {
|
|
3070
|
+
return buildChromiumUrlCommand(url, { ...options, platform, newWindow: true });
|
|
3071
|
+
}
|
|
3072
|
+
if (platform === "darwin") {
|
|
3073
|
+
return {
|
|
3074
|
+
command: "open",
|
|
3075
|
+
args: options.background === false ? [url] : ["-g", url],
|
|
3076
|
+
via: "system"
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
if (platform === "win32") {
|
|
3080
|
+
return {
|
|
3081
|
+
command: "cmd",
|
|
3082
|
+
args: ["/c", "start", "", url],
|
|
3083
|
+
via: "system"
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
return {
|
|
3087
|
+
command: "xdg-open",
|
|
3088
|
+
args: [url],
|
|
3089
|
+
via: "system"
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function buildChromiumUrlCommand(url, options = {}) {
|
|
3093
|
+
if (!url) throw new Error("openUrl requires a url");
|
|
3094
|
+
const platform = options.platform ?? process.platform;
|
|
3095
|
+
const chromiumBinary = "chromiumBinary" in options ? options.chromiumBinary : resolveChromiumBinary(platform);
|
|
3096
|
+
if (!chromiumBinary) {
|
|
3097
|
+
throw new Error("browser launch requires Chrome, Chromium, Edge, Brave, or Arc");
|
|
3098
|
+
}
|
|
3099
|
+
const args = [];
|
|
3100
|
+
if (options.headless) {
|
|
3101
|
+
args.push("--headless=new");
|
|
3102
|
+
} else if (options.newWindow !== false) {
|
|
3103
|
+
args.push("--new-window");
|
|
3104
|
+
}
|
|
3105
|
+
args.push(url);
|
|
3106
|
+
return {
|
|
3107
|
+
command: chromiumBinary,
|
|
3108
|
+
args,
|
|
3109
|
+
via: "chromium",
|
|
3110
|
+
target: chromiumBinary
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
async function spawnLaunch(command, args, detached) {
|
|
3114
|
+
return new Promise((resolve2, reject) => {
|
|
3115
|
+
const child = (0, import_child_process3.spawn)(command, args, {
|
|
3116
|
+
detached,
|
|
3117
|
+
stdio: "ignore"
|
|
3118
|
+
});
|
|
3119
|
+
child.once("error", reject);
|
|
3120
|
+
child.once("spawn", () => {
|
|
3121
|
+
if (detached) child.unref();
|
|
3122
|
+
resolve2(child.pid);
|
|
3123
|
+
});
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
async function launchUrl(url, options = {}) {
|
|
3127
|
+
const command = buildOpenUrlCommand(url, options);
|
|
3128
|
+
const pid = await spawnLaunch(command.command, command.args, options.detached ?? true);
|
|
3129
|
+
return {
|
|
3130
|
+
...command,
|
|
3131
|
+
pid,
|
|
3132
|
+
attachUrl: url
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
async function openUrl(url, options = {}) {
|
|
3136
|
+
await launchUrl(url, options);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2160
3139
|
// src/host/bridge-host.ts
|
|
2161
3140
|
var WRITE_COMMAND_TYPES = /* @__PURE__ */ new Set(["tap", "keyboard", "close"]);
|
|
2162
3141
|
function shouldAcquireLease(msg) {
|
|
@@ -2166,6 +3145,8 @@ function shouldAcquireLease(msg) {
|
|
|
2166
3145
|
return WRITE_COMMAND_TYPES.has(msg.type);
|
|
2167
3146
|
}
|
|
2168
3147
|
var DAEMON_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
3148
|
+
var DEFAULT_RUNTIME_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
3149
|
+
var RUNTIME_UPDATE_INTERVAL_ENV = "SOOTSIM_RUNTIME_UPDATE_INTERVAL_MS";
|
|
2169
3150
|
var HTTP_MIME_TYPES = {
|
|
2170
3151
|
".html": "text/html; charset=utf-8",
|
|
2171
3152
|
".js": "application/javascript",
|
|
@@ -2189,43 +3170,80 @@ var HTTP_MIME_TYPES = {
|
|
|
2189
3170
|
".map": "application/json",
|
|
2190
3171
|
".txt": "text/plain; charset=utf-8"
|
|
2191
3172
|
};
|
|
3173
|
+
function injectSharedConfigIntoHtml(data) {
|
|
3174
|
+
let payload;
|
|
3175
|
+
try {
|
|
3176
|
+
const cfg = readSharedConfig();
|
|
3177
|
+
payload = JSON.stringify(cfg);
|
|
3178
|
+
} catch {
|
|
3179
|
+
payload = "{}";
|
|
3180
|
+
}
|
|
3181
|
+
const tag = `<script>window.__sootsimSharedConfig=${payload};</script>`;
|
|
3182
|
+
const html = data.toString("utf8");
|
|
3183
|
+
if (html.includes("</head>")) return html.replace("</head>", tag + "</head>");
|
|
3184
|
+
if (html.includes("</body>")) return html.replace("</body>", tag + "</body>");
|
|
3185
|
+
return tag + html;
|
|
3186
|
+
}
|
|
2192
3187
|
var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
2193
3188
|
port;
|
|
2194
3189
|
openUrlHandler;
|
|
2195
3190
|
httpServer = null;
|
|
2196
3191
|
wss = null;
|
|
2197
3192
|
nextCommandId = 1;
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
3193
|
+
nextSimNumber = 161;
|
|
3194
|
+
sims = /* @__PURE__ */ new Map();
|
|
3195
|
+
primarySimId = null;
|
|
2201
3196
|
pendingCommands = /* @__PURE__ */ new Map();
|
|
2202
3197
|
cliBySentId = /* @__PURE__ */ new Map();
|
|
2203
|
-
|
|
3198
|
+
cliSimBySocket = /* @__PURE__ */ new Map();
|
|
2204
3199
|
cliLastCommandAt = /* @__PURE__ */ new Map();
|
|
2205
|
-
|
|
3200
|
+
cliIdentityKeyBySocket = /* @__PURE__ */ new Map();
|
|
2206
3201
|
cliLabelBySocket = /* @__PURE__ */ new Map();
|
|
2207
|
-
|
|
3202
|
+
restorableSims = /* @__PURE__ */ new Map();
|
|
2208
3203
|
nextCliFallbackId = 1;
|
|
2209
3204
|
cliIdleTimer = null;
|
|
2210
3205
|
agentHost;
|
|
2211
3206
|
static CLI_IDLE_TIMEOUT_MS = 6e4;
|
|
2212
3207
|
static CLI_LEASE_TTL_MS = 6e5;
|
|
2213
3208
|
static USER_ACTIVE_LEASE_TTL_MS = 8e3;
|
|
2214
|
-
|
|
3209
|
+
// explicit user actions (clicking Boot, focusing the sim to take it over)
|
|
3210
|
+
// hold the sim longer than passive canvas interaction so reconnecting clis
|
|
3211
|
+
// can't immediately reclaim while the user gets oriented.
|
|
3212
|
+
static USER_BOOT_LEASE_TTL_MS = 6e4;
|
|
3213
|
+
static SIM_RECONNECT_TTL_MS = 3e4;
|
|
2215
3214
|
preferredPort;
|
|
2216
3215
|
portFallbackCount;
|
|
2217
3216
|
shouldWriteLockfile;
|
|
2218
3217
|
effectivePort = 0;
|
|
2219
3218
|
startedAt = 0;
|
|
2220
3219
|
heartbeatTimer = null;
|
|
3220
|
+
// ws-level heartbeat: sims that hang up uncleanly (page navigated, network
|
|
3221
|
+
// dropped, sim crashed) leave their server-side WebSocket sitting "open"
|
|
3222
|
+
// forever. ping every WS_HEARTBEAT_INTERVAL_MS; if the previous round's
|
|
3223
|
+
// ping was never answered, terminate(). that fires 'close' which runs the
|
|
3224
|
+
// sim-cleanup path and stops `sootsim list` from showing 8 zombie
|
|
3225
|
+
// sims that all time out on every command.
|
|
3226
|
+
wsHeartbeatTimer = null;
|
|
3227
|
+
wsIsAlive = /* @__PURE__ */ new WeakMap();
|
|
3228
|
+
static WS_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3229
|
+
runtimeUpdateTimer = null;
|
|
3230
|
+
runtimeUpdateInFlight = null;
|
|
2221
3231
|
activeRuntimeVersion = null;
|
|
2222
3232
|
activeRuntimeDirPath = null;
|
|
3233
|
+
// /__server-scan cache. mirrors the shell vite dev-middleware so engine
|
|
3234
|
+
// ConnectRN / DemoConnectApp see the same JSON shape whether they boot
|
|
3235
|
+
// from vite dev or from this daemon. without this, the SPA fallback below
|
|
3236
|
+
// would serve index.html for /__server-scan and tenant-worker .json()
|
|
3237
|
+
// crashes with "Unexpected token '<', "<!doctype "... is not valid JSON".
|
|
3238
|
+
scanCache = null;
|
|
3239
|
+
scanCacheAt = 0;
|
|
3240
|
+
inflightScan = null;
|
|
3241
|
+
static SCAN_FRESH_MS = 2e3;
|
|
2223
3242
|
constructor(opts = {}) {
|
|
2224
3243
|
this.preferredPort = opts.port || DEFAULT_SOOTSIM_BRIDGE_PORT;
|
|
2225
3244
|
this.port = this.preferredPort;
|
|
2226
3245
|
this.shouldWriteLockfile = opts.writeLockfile === true;
|
|
2227
|
-
|
|
2228
|
-
this.portFallbackCount = Math.max(1, opts.portFallbackCount ?? defaultFallback);
|
|
3246
|
+
this.portFallbackCount = Math.max(1, opts.portFallbackCount ?? 10);
|
|
2229
3247
|
this.openUrlHandler = opts.openUrl;
|
|
2230
3248
|
this.agentHost = new AgentHost({ getExcludePorts: opts.agentScanExcludes });
|
|
2231
3249
|
}
|
|
@@ -2278,7 +3296,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2278
3296
|
}
|
|
2279
3297
|
bindOnce(port, _silent) {
|
|
2280
3298
|
return new Promise((resolve2, reject) => {
|
|
2281
|
-
const server = (0,
|
|
3299
|
+
const server = (0, import_http3.createServer)((req, res) => this.handleHttpRequest(req, res));
|
|
2282
3300
|
let settled = false;
|
|
2283
3301
|
const onError = (err) => {
|
|
2284
3302
|
if (settled) return;
|
|
@@ -2313,12 +3331,18 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2313
3331
|
if (!this.wss) return;
|
|
2314
3332
|
this.wss.on("connection", (ws, req) => {
|
|
2315
3333
|
const origin = req.headers.origin;
|
|
2316
|
-
const role = origin ? "
|
|
2317
|
-
let
|
|
3334
|
+
const role = origin ? "sim" : "cli";
|
|
3335
|
+
let sim = null;
|
|
3336
|
+
ws.on("error", () => {
|
|
3337
|
+
});
|
|
3338
|
+
this.wsIsAlive.set(ws, true);
|
|
3339
|
+
ws.on("pong", () => {
|
|
3340
|
+
this.wsIsAlive.set(ws, true);
|
|
3341
|
+
});
|
|
2318
3342
|
this.agentHost.registerSocket(ws);
|
|
2319
|
-
if (role === "
|
|
2320
|
-
|
|
2321
|
-
id:
|
|
3343
|
+
if (role === "sim") {
|
|
3344
|
+
sim = {
|
|
3345
|
+
id: this.allocateSimId(),
|
|
2322
3346
|
ws,
|
|
2323
3347
|
origin,
|
|
2324
3348
|
connectedAt: Date.now(),
|
|
@@ -2326,15 +3350,15 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2326
3350
|
lastActiveAt: 0,
|
|
2327
3351
|
recentActions: []
|
|
2328
3352
|
};
|
|
2329
|
-
this.
|
|
2330
|
-
if (this.
|
|
2331
|
-
this.
|
|
3353
|
+
this.sims.set(sim.id, sim);
|
|
3354
|
+
if (this.shouldPromoteSim(sim)) {
|
|
3355
|
+
this.primarySimId = sim.id;
|
|
2332
3356
|
}
|
|
2333
|
-
this.
|
|
2334
|
-
this.
|
|
3357
|
+
this.broadcastSimAssignments();
|
|
3358
|
+
this.broadcastSimClientStates();
|
|
2335
3359
|
} else {
|
|
2336
3360
|
const fallbackKey = `ws-${this.nextCliFallbackId++}`;
|
|
2337
|
-
this.
|
|
3361
|
+
this.cliIdentityKeyBySocket.set(ws, fallbackKey);
|
|
2338
3362
|
}
|
|
2339
3363
|
ws.on("message", (data) => {
|
|
2340
3364
|
let msg;
|
|
@@ -2409,29 +3433,55 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2409
3433
|
}
|
|
2410
3434
|
return;
|
|
2411
3435
|
}
|
|
2412
|
-
if (role === "
|
|
2413
|
-
if (
|
|
2414
|
-
|
|
3436
|
+
if (role === "sim") {
|
|
3437
|
+
if (sim) {
|
|
3438
|
+
sim.lastSeenAt = Date.now();
|
|
2415
3439
|
}
|
|
2416
|
-
if (msg.type === "bridge:register" &&
|
|
3440
|
+
if (msg.type === "bridge:register" && sim) {
|
|
2417
3441
|
const registration = msg;
|
|
2418
|
-
const restored = this.
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
3442
|
+
const restored = this.tryRestoreSimId(sim, registration.simId);
|
|
3443
|
+
sim.url = registration.url;
|
|
3444
|
+
sim.title = registration.title;
|
|
3445
|
+
sim.userAgent = registration.userAgent;
|
|
2422
3446
|
if (restored) {
|
|
2423
|
-
this.
|
|
2424
|
-
this.
|
|
3447
|
+
this.broadcastSimAssignments();
|
|
3448
|
+
this.broadcastSimClientStates();
|
|
2425
3449
|
}
|
|
2426
3450
|
return;
|
|
2427
3451
|
}
|
|
2428
|
-
if (msg.type === "bridge:user-focus-state" &&
|
|
3452
|
+
if (msg.type === "bridge:user-focus-state" && sim) {
|
|
2429
3453
|
const focusState = msg;
|
|
2430
|
-
this.updateUserFocusLease(
|
|
3454
|
+
this.updateUserFocusLease(sim, focusState.focused === true);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
if (msg.type === "bridge:user-interact" && sim) {
|
|
3458
|
+
this.updateUserActivity(sim);
|
|
2431
3459
|
return;
|
|
2432
3460
|
}
|
|
2433
|
-
if (msg.type === "bridge:
|
|
2434
|
-
|
|
3461
|
+
if (msg.type === "bridge:write-shared-config") {
|
|
3462
|
+
const patch = msg.patch && typeof msg.patch === "object" ? msg.patch : null;
|
|
3463
|
+
if (!patch) return;
|
|
3464
|
+
let next;
|
|
3465
|
+
try {
|
|
3466
|
+
next = writeSharedConfig(patch);
|
|
3467
|
+
} catch (err) {
|
|
3468
|
+
process.stderr.write(
|
|
3469
|
+
`sootsim: bridge:write-shared-config failed: ${err instanceof Error ? err.message : String(err)}
|
|
3470
|
+
`
|
|
3471
|
+
);
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
const payload = JSON.stringify({
|
|
3475
|
+
type: "bridge:shared-config-changed",
|
|
3476
|
+
config: next
|
|
3477
|
+
});
|
|
3478
|
+
for (const peer of this.sims.values()) {
|
|
3479
|
+
if (peer.ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
3480
|
+
try {
|
|
3481
|
+
peer.ws.send(payload);
|
|
3482
|
+
} catch {
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
2435
3485
|
return;
|
|
2436
3486
|
}
|
|
2437
3487
|
if (msg.type === "bridge:open-path") {
|
|
@@ -2443,30 +3493,33 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2443
3493
|
}
|
|
2444
3494
|
return;
|
|
2445
3495
|
}
|
|
2446
|
-
if (msg.type === "bridge:boot-clients" &&
|
|
3496
|
+
if (msg.type === "bridge:boot-clients" && sim) {
|
|
2447
3497
|
const booted = [];
|
|
2448
|
-
for (const [cliWs,
|
|
2449
|
-
if (
|
|
3498
|
+
for (const [cliWs, attachedSimId] of this.cliSimBySocket) {
|
|
3499
|
+
if (attachedSimId === sim.id) {
|
|
2450
3500
|
booted.push(cliWs);
|
|
2451
3501
|
}
|
|
2452
3502
|
}
|
|
2453
3503
|
for (const cliWs of booted) {
|
|
2454
|
-
this.
|
|
3504
|
+
this.cliSimBySocket.delete(cliWs);
|
|
2455
3505
|
try {
|
|
2456
|
-
cliWs.close(1e3, "booted by
|
|
3506
|
+
cliWs.close(1e3, "booted by sim");
|
|
2457
3507
|
} catch {
|
|
2458
3508
|
}
|
|
2459
3509
|
}
|
|
2460
|
-
const hadLease = !!
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
3510
|
+
const hadLease = !!sim.cliLease;
|
|
3511
|
+
sim.cliLease = {
|
|
3512
|
+
kind: "user-active",
|
|
3513
|
+
cliIdentityKey: "__user-active__",
|
|
3514
|
+
cliLabel: "active user",
|
|
3515
|
+
expiresAt: Date.now() + _SootSimBridgeHost.USER_BOOT_LEASE_TTL_MS
|
|
3516
|
+
};
|
|
3517
|
+
process.stderr.write(
|
|
3518
|
+
`sootsim booted ${booted.length} cli client(s)${hadLease ? " (overrode prior lease)" : ""}; held sim for user [${sim.id}]
|
|
2465
3519
|
`
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
}
|
|
3520
|
+
);
|
|
3521
|
+
this.recordSimAction(sim.id, "sim booted cli clients");
|
|
3522
|
+
this.broadcastSimClientStates();
|
|
2470
3523
|
return;
|
|
2471
3524
|
}
|
|
2472
3525
|
const internalPending = this.pendingCommands.get(msg.id);
|
|
@@ -2480,10 +3533,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2480
3533
|
if (entry) {
|
|
2481
3534
|
this.cliBySentId.delete(msg.id);
|
|
2482
3535
|
if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2483
|
-
const otherCliCount = this.
|
|
2484
|
-
entry.ws,
|
|
2485
|
-
entry.browserId
|
|
2486
|
-
);
|
|
3536
|
+
const otherCliCount = this.getOtherCliIdentityCount(entry.ws, entry.simId);
|
|
2487
3537
|
const response = otherCliCount > 0 ? { ...msg, id: entry.originalId, _otherCliCount: otherCliCount } : { ...msg, id: entry.originalId };
|
|
2488
3538
|
entry.ws.send(JSON.stringify(response));
|
|
2489
3539
|
}
|
|
@@ -2494,19 +3544,19 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2494
3544
|
this.cliLastCommandAt.set(ws, Date.now());
|
|
2495
3545
|
try {
|
|
2496
3546
|
if (msg.type === "bridge:bye") {
|
|
2497
|
-
const
|
|
3547
|
+
const hadSim = this.cliSimBySocket.delete(ws);
|
|
2498
3548
|
this.cliLastCommandAt.delete(ws);
|
|
2499
|
-
this.
|
|
3549
|
+
this.cliIdentityKeyBySocket.delete(ws);
|
|
2500
3550
|
this.cliLabelBySocket.delete(ws);
|
|
2501
3551
|
for (const [sentId2, entry] of this.cliBySentId) {
|
|
2502
3552
|
if (entry.ws === ws) this.cliBySentId.delete(sentId2);
|
|
2503
3553
|
}
|
|
2504
|
-
if (
|
|
3554
|
+
if (hadSim) this.broadcastSimClientStates();
|
|
2505
3555
|
return;
|
|
2506
3556
|
}
|
|
2507
3557
|
if (msg.type === "bridge:hello") {
|
|
2508
|
-
const key = typeof msg.
|
|
2509
|
-
this.
|
|
3558
|
+
const key = typeof msg.cliIdentityKey === "string" && msg.cliIdentityKey.trim() ? msg.cliIdentityKey.trim() : this.cliIdentityKeyBySocket.get(ws) || `ws-${this.nextCliFallbackId++}`;
|
|
3559
|
+
this.cliIdentityKeyBySocket.set(ws, key);
|
|
2510
3560
|
if (typeof msg.cliLabel === "string" && msg.cliLabel.trim()) {
|
|
2511
3561
|
this.cliLabelBySocket.set(ws, msg.cliLabel.trim());
|
|
2512
3562
|
}
|
|
@@ -2515,7 +3565,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2515
3565
|
JSON.stringify({
|
|
2516
3566
|
id: msg.id,
|
|
2517
3567
|
result: {
|
|
2518
|
-
|
|
3568
|
+
cliIdentityKey: key,
|
|
2519
3569
|
leaseTtlMs: _SootSimBridgeHost.CLI_LEASE_TTL_MS,
|
|
2520
3570
|
leasing: true
|
|
2521
3571
|
}
|
|
@@ -2524,12 +3574,12 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2524
3574
|
}
|
|
2525
3575
|
return;
|
|
2526
3576
|
}
|
|
2527
|
-
if (msg.type === "bridge:list-
|
|
3577
|
+
if (msg.type === "bridge:list-sims") {
|
|
2528
3578
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2529
3579
|
ws.send(
|
|
2530
3580
|
JSON.stringify({
|
|
2531
3581
|
id: msg.id,
|
|
2532
|
-
result: this.
|
|
3582
|
+
result: this.listSims()
|
|
2533
3583
|
})
|
|
2534
3584
|
);
|
|
2535
3585
|
}
|
|
@@ -2539,7 +3589,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2539
3589
|
if (typeof msg.url !== "string" || !msg.url) {
|
|
2540
3590
|
throw new Error("bridge:open requires a url");
|
|
2541
3591
|
}
|
|
2542
|
-
await this.openUrl(msg.url);
|
|
3592
|
+
await this.openUrl(msg.url, { newWindow: msg.newWindow === true });
|
|
2543
3593
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2544
3594
|
ws.send(
|
|
2545
3595
|
JSON.stringify({
|
|
@@ -2551,8 +3601,8 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2551
3601
|
return;
|
|
2552
3602
|
}
|
|
2553
3603
|
if (msg.type === "bridge:claim") {
|
|
2554
|
-
const
|
|
2555
|
-
const outcome = this.tryAcquireLease(ws,
|
|
3604
|
+
const targetSim2 = await this.waitForSim(msg.simId);
|
|
3605
|
+
const outcome = this.tryAcquireLease(ws, targetSim2, {
|
|
2556
3606
|
force: msg.force === true
|
|
2557
3607
|
});
|
|
2558
3608
|
if (!outcome.granted) {
|
|
@@ -2560,25 +3610,25 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2560
3610
|
ws.send(
|
|
2561
3611
|
JSON.stringify({
|
|
2562
3612
|
id: msg.id,
|
|
2563
|
-
error: `
|
|
3613
|
+
error: `sim ${targetSim2.id} is locked by another cli`,
|
|
2564
3614
|
_locked: outcome.lock
|
|
2565
3615
|
})
|
|
2566
3616
|
);
|
|
2567
3617
|
}
|
|
2568
3618
|
return;
|
|
2569
3619
|
}
|
|
2570
|
-
this.
|
|
2571
|
-
this.
|
|
2572
|
-
|
|
2573
|
-
outcome.bootedCount > 0 ? `cli force-claimed
|
|
3620
|
+
this.setCliSimTarget(ws, targetSim2.id);
|
|
3621
|
+
this.recordSimAction(
|
|
3622
|
+
targetSim2.id,
|
|
3623
|
+
outcome.bootedCount > 0 ? `cli force-claimed sim (booted ${outcome.bootedCount})` : "cli claimed sim"
|
|
2574
3624
|
);
|
|
2575
3625
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2576
3626
|
ws.send(
|
|
2577
3627
|
JSON.stringify({
|
|
2578
3628
|
id: msg.id,
|
|
2579
3629
|
result: {
|
|
2580
|
-
|
|
2581
|
-
lockedBy: outcome.lease.
|
|
3630
|
+
simId: targetSim2.id,
|
|
3631
|
+
lockedBy: outcome.lease.cliIdentityKey,
|
|
2582
3632
|
lockExpiresAt: outcome.lease.expiresAt,
|
|
2583
3633
|
bootedCount: outcome.bootedCount
|
|
2584
3634
|
}
|
|
@@ -2587,15 +3637,15 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2587
3637
|
}
|
|
2588
3638
|
return;
|
|
2589
3639
|
}
|
|
2590
|
-
const
|
|
3640
|
+
const targetSim = await this.waitForSim(msg.simId);
|
|
2591
3641
|
if (shouldAcquireLease(msg)) {
|
|
2592
|
-
const outcome = this.tryAcquireLease(ws,
|
|
3642
|
+
const outcome = this.tryAcquireLease(ws, targetSim);
|
|
2593
3643
|
if (!outcome.granted) {
|
|
2594
3644
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2595
3645
|
ws.send(
|
|
2596
3646
|
JSON.stringify({
|
|
2597
3647
|
id: msg.id,
|
|
2598
|
-
error: `
|
|
3648
|
+
error: `sim ${targetSim.id} is locked by another cli \u2014 use \`sootsim claim ${targetSim.id} --force\` or \`sootsim open --new\``,
|
|
2599
3649
|
_locked: outcome.lock
|
|
2600
3650
|
})
|
|
2601
3651
|
);
|
|
@@ -2603,18 +3653,18 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2603
3653
|
return;
|
|
2604
3654
|
}
|
|
2605
3655
|
} else {
|
|
2606
|
-
this.
|
|
3656
|
+
this.ensureCliIdentityKey(ws);
|
|
2607
3657
|
}
|
|
2608
|
-
this.
|
|
2609
|
-
this.
|
|
3658
|
+
this.setCliSimTarget(ws, targetSim.id);
|
|
3659
|
+
this.recordSimAction(targetSim.id, this.describeForwardedCommand(msg));
|
|
2610
3660
|
const sentId = this.nextCommandId++;
|
|
2611
3661
|
this.cliBySentId.set(sentId, {
|
|
2612
|
-
|
|
3662
|
+
simId: targetSim.id,
|
|
2613
3663
|
ws,
|
|
2614
3664
|
originalId: msg.id
|
|
2615
3665
|
});
|
|
2616
|
-
const {
|
|
2617
|
-
|
|
3666
|
+
const { simId: _simId, ...forwarded } = msg;
|
|
3667
|
+
targetSim.ws.send(JSON.stringify({ ...forwarded, id: sentId }));
|
|
2618
3668
|
} catch (err) {
|
|
2619
3669
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2620
3670
|
ws.send(
|
|
@@ -2629,40 +3679,40 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2629
3679
|
});
|
|
2630
3680
|
ws.on("close", () => {
|
|
2631
3681
|
this.agentHost.unregisterSocket(ws);
|
|
2632
|
-
if (role === "
|
|
2633
|
-
this.
|
|
2634
|
-
if (this.
|
|
2635
|
-
this.
|
|
3682
|
+
if (role === "sim" && sim) {
|
|
3683
|
+
this.rememberDisconnectedSim(sim);
|
|
3684
|
+
if (this.primarySimId === sim.id) {
|
|
3685
|
+
this.primarySimId = this.getOpenSim()?.id ?? null;
|
|
2636
3686
|
}
|
|
2637
3687
|
for (const [id, pending] of this.pendingCommands) {
|
|
2638
|
-
if (pending.
|
|
2639
|
-
pending.reject(new Error("
|
|
3688
|
+
if (pending.simId !== sim.id) continue;
|
|
3689
|
+
pending.reject(new Error("sim disconnected"));
|
|
2640
3690
|
this.pendingCommands.delete(id);
|
|
2641
3691
|
}
|
|
2642
3692
|
for (const [sentId, entry] of this.cliBySentId) {
|
|
2643
|
-
if (entry.
|
|
3693
|
+
if (entry.simId !== sim.id) continue;
|
|
2644
3694
|
if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2645
3695
|
entry.ws.send(
|
|
2646
3696
|
JSON.stringify({
|
|
2647
3697
|
id: entry.originalId,
|
|
2648
|
-
error: "
|
|
3698
|
+
error: "sim disconnected before responding"
|
|
2649
3699
|
})
|
|
2650
3700
|
);
|
|
2651
3701
|
}
|
|
2652
3702
|
this.cliBySentId.delete(sentId);
|
|
2653
3703
|
}
|
|
2654
|
-
this.
|
|
2655
|
-
this.
|
|
3704
|
+
this.broadcastSimAssignments();
|
|
3705
|
+
this.broadcastSimClientStates();
|
|
2656
3706
|
} else if (role === "cli") {
|
|
2657
|
-
const detached = this.
|
|
3707
|
+
const detached = this.cliSimBySocket.delete(ws);
|
|
2658
3708
|
this.cliLastCommandAt.delete(ws);
|
|
2659
|
-
this.
|
|
3709
|
+
this.cliIdentityKeyBySocket.delete(ws);
|
|
2660
3710
|
this.cliLabelBySocket.delete(ws);
|
|
2661
3711
|
for (const [sentId, entry] of this.cliBySentId) {
|
|
2662
3712
|
if (entry.ws === ws) this.cliBySentId.delete(sentId);
|
|
2663
3713
|
}
|
|
2664
3714
|
if (detached) {
|
|
2665
|
-
this.
|
|
3715
|
+
this.broadcastSimClientStates();
|
|
2666
3716
|
}
|
|
2667
3717
|
}
|
|
2668
3718
|
});
|
|
@@ -2680,6 +3730,11 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2680
3730
|
3e4
|
|
2681
3731
|
);
|
|
2682
3732
|
this.cliIdleTimer.unref();
|
|
3733
|
+
this.wsHeartbeatTimer = setInterval(
|
|
3734
|
+
() => this.sweepDeadWebSockets(),
|
|
3735
|
+
_SootSimBridgeHost.WS_HEARTBEAT_INTERVAL_MS
|
|
3736
|
+
);
|
|
3737
|
+
this.wsHeartbeatTimer.unref();
|
|
2683
3738
|
if (this.shouldWriteLockfile) {
|
|
2684
3739
|
try {
|
|
2685
3740
|
ensureSootsimHome();
|
|
@@ -2703,9 +3758,11 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2703
3758
|
}
|
|
2704
3759
|
}, DAEMON_HEARTBEAT_INTERVAL_MS);
|
|
2705
3760
|
this.heartbeatTimer.unref();
|
|
3761
|
+
this.startRuntimeUpdater();
|
|
2706
3762
|
}
|
|
2707
3763
|
void this.agentHost.seedOnBoot();
|
|
2708
3764
|
}
|
|
3765
|
+
bootstrapping = true;
|
|
2709
3766
|
buildLockfileSnapshot() {
|
|
2710
3767
|
return {
|
|
2711
3768
|
schema: 1,
|
|
@@ -2716,7 +3773,8 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2716
3773
|
activeRuntime: this.activeRuntimeVersion,
|
|
2717
3774
|
activeRuntimeDir: this.activeRuntimeDirPath,
|
|
2718
3775
|
startedAt: this.startedAt,
|
|
2719
|
-
heartbeatAt: Date.now()
|
|
3776
|
+
heartbeatAt: Date.now(),
|
|
3777
|
+
bootstrapping: this.bootstrapping
|
|
2720
3778
|
};
|
|
2721
3779
|
}
|
|
2722
3780
|
writeLockfileSnapshot() {
|
|
@@ -2726,9 +3784,104 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2726
3784
|
this.activeRuntimeVersion = readActiveRuntime();
|
|
2727
3785
|
this.activeRuntimeDirPath = activeRuntimeDir();
|
|
2728
3786
|
}
|
|
3787
|
+
runServerScan() {
|
|
3788
|
+
if (this.inflightScan) return this.inflightScan;
|
|
3789
|
+
const excludePorts = this.effectivePort > 0 ? [this.effectivePort] : [];
|
|
3790
|
+
this.inflightScan = scanDevServers({
|
|
3791
|
+
excludePorts,
|
|
3792
|
+
buildIconProxyUrl: (externalUrl) => `/__bundle-proxy?url=${encodeURIComponent(externalUrl)}`
|
|
3793
|
+
}).then((results) => {
|
|
3794
|
+
this.scanCache = results;
|
|
3795
|
+
this.scanCacheAt = Date.now();
|
|
3796
|
+
return results;
|
|
3797
|
+
}).catch((err) => {
|
|
3798
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3799
|
+
console.error("[sootsim] /__server-scan failed:", message);
|
|
3800
|
+
return this.scanCache ?? [];
|
|
3801
|
+
}).finally(() => {
|
|
3802
|
+
this.inflightScan = null;
|
|
3803
|
+
});
|
|
3804
|
+
return this.inflightScan;
|
|
3805
|
+
}
|
|
3806
|
+
handleServerScan(res) {
|
|
3807
|
+
const sendJson = (body) => {
|
|
3808
|
+
res.writeHead(200, {
|
|
3809
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
3810
|
+
"Cache-Control": "no-store"
|
|
3811
|
+
});
|
|
3812
|
+
res.end(JSON.stringify(body));
|
|
3813
|
+
};
|
|
3814
|
+
const age = Date.now() - this.scanCacheAt;
|
|
3815
|
+
if (this.scanCache && age < _SootSimBridgeHost.SCAN_FRESH_MS) {
|
|
3816
|
+
sendJson(this.scanCache);
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
if (this.scanCache) {
|
|
3820
|
+
sendJson(this.scanCache);
|
|
3821
|
+
void this.runServerScan().catch(() => {
|
|
3822
|
+
});
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
void this.runServerScan().then((results) => sendJson(results));
|
|
3826
|
+
}
|
|
3827
|
+
resolveRuntimeUpdateIntervalMs() {
|
|
3828
|
+
const raw = Number(process.env[RUNTIME_UPDATE_INTERVAL_ENV]);
|
|
3829
|
+
if (Number.isFinite(raw) && raw > 0) return Math.max(100, Math.round(raw));
|
|
3830
|
+
return DEFAULT_RUNTIME_UPDATE_INTERVAL_MS;
|
|
3831
|
+
}
|
|
3832
|
+
startRuntimeUpdater() {
|
|
3833
|
+
if (!this.shouldWriteLockfile || this.runtimeUpdateTimer) return;
|
|
3834
|
+
void this.runRuntimeUpdate("startup");
|
|
3835
|
+
const intervalMs = this.resolveRuntimeUpdateIntervalMs();
|
|
3836
|
+
this.runtimeUpdateTimer = setInterval(() => {
|
|
3837
|
+
void this.runRuntimeUpdate("periodic");
|
|
3838
|
+
}, intervalMs);
|
|
3839
|
+
this.runtimeUpdateTimer.unref();
|
|
3840
|
+
}
|
|
3841
|
+
runRuntimeUpdate(reason) {
|
|
3842
|
+
if (this.runtimeUpdateInFlight) return this.runtimeUpdateInFlight;
|
|
3843
|
+
this.runtimeUpdateInFlight = (async () => {
|
|
3844
|
+
try {
|
|
3845
|
+
if (reason === "startup") {
|
|
3846
|
+
process.stderr.write("sootsim: checking for runtime updates\u2026\n");
|
|
3847
|
+
}
|
|
3848
|
+
const result = await updateRuntimeToLatest();
|
|
3849
|
+
if (!result.updated || !result.latestVersion) {
|
|
3850
|
+
if (reason === "startup") {
|
|
3851
|
+
process.stderr.write(
|
|
3852
|
+
`sootsim: runtime ${this.activeRuntimeVersion ?? "(none)"} is current
|
|
3853
|
+
`
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
const active = this.setActiveRuntime(result.latestVersion);
|
|
3859
|
+
process.stderr.write(`sootsim runtime updated to ${active.version} (${reason})
|
|
3860
|
+
`);
|
|
3861
|
+
} catch (err) {
|
|
3862
|
+
process.stderr.write(
|
|
3863
|
+
`sootsim runtime update failed (${reason}): ${err instanceof Error ? err.message : String(err)}
|
|
3864
|
+
`
|
|
3865
|
+
);
|
|
3866
|
+
} finally {
|
|
3867
|
+
this.runtimeUpdateInFlight = null;
|
|
3868
|
+
if (reason === "startup" && this.bootstrapping) {
|
|
3869
|
+
this.bootstrapping = false;
|
|
3870
|
+
if (this.shouldWriteLockfile && this.httpServer) {
|
|
3871
|
+
try {
|
|
3872
|
+
this.writeLockfileSnapshot();
|
|
3873
|
+
} catch {
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
process.stderr.write("sootsim: ready\n");
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
})();
|
|
3880
|
+
return this.runtimeUpdateInFlight;
|
|
3881
|
+
}
|
|
2729
3882
|
/** update the active runtime on disk + in memory. the caller guarantees
|
|
2730
3883
|
* the version directory exists. pushes a runtime:changed message to all
|
|
2731
|
-
* connected
|
|
3884
|
+
* connected sims so electron (or any renderer) can reload. */
|
|
2732
3885
|
setActiveRuntime(version) {
|
|
2733
3886
|
writeActiveRuntime(version);
|
|
2734
3887
|
this.refreshActiveRuntime();
|
|
@@ -2743,10 +3896,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2743
3896
|
version,
|
|
2744
3897
|
runtimeDir: this.activeRuntimeDirPath
|
|
2745
3898
|
});
|
|
2746
|
-
for (const
|
|
2747
|
-
if (
|
|
3899
|
+
for (const sim of this.sims.values()) {
|
|
3900
|
+
if (sim.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2748
3901
|
try {
|
|
2749
|
-
|
|
3902
|
+
sim.ws.send(payload);
|
|
2750
3903
|
} catch {
|
|
2751
3904
|
}
|
|
2752
3905
|
}
|
|
@@ -2775,6 +3928,13 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2775
3928
|
* non-upgrade routes that don't match serve index.html (SPA behavior) so
|
|
2776
3929
|
* electron's webContents can navigate freely inside the runtime. */
|
|
2777
3930
|
handleHttpRequest(req, res) {
|
|
3931
|
+
if (isFetchProxyRequestUrl(req.url)) {
|
|
3932
|
+
void handleFetchProxyRequest(req, res);
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
if (isAppApiRequestUrl(req.url) && handleAppApiRequest(req, res)) {
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
2778
3938
|
const method = (req.method || "GET").toUpperCase();
|
|
2779
3939
|
if (method !== "GET" && method !== "HEAD") {
|
|
2780
3940
|
res.writeHead(405, { Allow: "GET, HEAD" });
|
|
@@ -2834,6 +3994,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2834
3994
|
})();
|
|
2835
3995
|
return;
|
|
2836
3996
|
}
|
|
3997
|
+
if (url.pathname === "/__server-scan") {
|
|
3998
|
+
this.handleServerScan(res);
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
2837
4001
|
if (url.pathname === "/healthz") {
|
|
2838
4002
|
res.writeHead(200, {
|
|
2839
4003
|
"Content-Type": "application/json",
|
|
@@ -2853,6 +4017,24 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2853
4017
|
);
|
|
2854
4018
|
return;
|
|
2855
4019
|
}
|
|
4020
|
+
if (url.pathname === "/__sootsim/shared-config") {
|
|
4021
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4022
|
+
res.setHeader("Cache-Control", "no-store");
|
|
4023
|
+
if (method === "GET" || method === "HEAD") {
|
|
4024
|
+
let body = "{}";
|
|
4025
|
+
try {
|
|
4026
|
+
body = JSON.stringify(readSharedConfig());
|
|
4027
|
+
} catch {
|
|
4028
|
+
}
|
|
4029
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4030
|
+
if (method === "HEAD") res.end();
|
|
4031
|
+
else res.end(body);
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
res.writeHead(405, { Allow: "GET, HEAD" });
|
|
4035
|
+
res.end("method not allowed (use the bridge over WS for writes)");
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
2856
4038
|
this.refreshActiveRuntime();
|
|
2857
4039
|
const baseDir = this.activeRuntimeDirPath;
|
|
2858
4040
|
if (!baseDir) {
|
|
@@ -2883,63 +4065,87 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2883
4065
|
return;
|
|
2884
4066
|
}
|
|
2885
4067
|
}
|
|
2886
|
-
const resolved =
|
|
2887
|
-
const baseWithSep = baseDir.endsWith(
|
|
4068
|
+
const resolved = import_path3.default.resolve(baseDir, "." + rel);
|
|
4069
|
+
const baseWithSep = baseDir.endsWith(import_path3.default.sep) ? baseDir : baseDir + import_path3.default.sep;
|
|
2888
4070
|
if (!resolved.startsWith(baseWithSep) && resolved !== baseDir) {
|
|
2889
4071
|
res.writeHead(403);
|
|
2890
4072
|
res.end("forbidden");
|
|
2891
4073
|
return;
|
|
2892
4074
|
}
|
|
2893
|
-
|
|
4075
|
+
import_fs3.default.realpath(resolved, (realErr, realResolved) => {
|
|
2894
4076
|
const servePath = realErr ? resolved : realResolved;
|
|
2895
|
-
const servePathWithSep = servePath.endsWith(
|
|
4077
|
+
const servePathWithSep = servePath.endsWith(import_path3.default.sep) ? servePath : servePath + import_path3.default.sep;
|
|
2896
4078
|
if (!realErr) {
|
|
2897
4079
|
const realBaseWithSep = (() => {
|
|
2898
4080
|
try {
|
|
2899
|
-
const rb =
|
|
2900
|
-
return rb.endsWith(
|
|
4081
|
+
const rb = import_fs3.default.realpathSync(baseDir);
|
|
4082
|
+
return rb.endsWith(import_path3.default.sep) ? rb : rb + import_path3.default.sep;
|
|
2901
4083
|
} catch {
|
|
2902
4084
|
return baseWithSep;
|
|
2903
4085
|
}
|
|
2904
4086
|
})();
|
|
2905
|
-
if (!servePathWithSep.startsWith(realBaseWithSep) && servePath +
|
|
4087
|
+
if (!servePathWithSep.startsWith(realBaseWithSep) && servePath + import_path3.default.sep !== realBaseWithSep) {
|
|
2906
4088
|
res.writeHead(403);
|
|
2907
4089
|
res.end("forbidden");
|
|
2908
4090
|
return;
|
|
2909
4091
|
}
|
|
2910
4092
|
}
|
|
2911
|
-
|
|
4093
|
+
import_fs3.default.stat(servePath, (err, stats) => {
|
|
2912
4094
|
if (err || !stats?.isFile()) {
|
|
2913
|
-
const ext2 =
|
|
4095
|
+
const ext2 = import_path3.default.extname(rel).toLowerCase();
|
|
2914
4096
|
if (ext2 && ext2 !== ".html") {
|
|
2915
4097
|
res.writeHead(404);
|
|
2916
4098
|
res.end("not found");
|
|
2917
4099
|
return;
|
|
2918
4100
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
4101
|
+
if (rel.startsWith("/__") || rel.startsWith("/api/") || rel === "/api") {
|
|
4102
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
4103
|
+
res.end("not found");
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
const indexPath = import_path3.default.join(baseDir, "index.html");
|
|
4107
|
+
import_fs3.default.readFile(indexPath, (err2, data) => {
|
|
2921
4108
|
if (err2) {
|
|
2922
4109
|
res.writeHead(404);
|
|
2923
4110
|
res.end("not found");
|
|
2924
4111
|
return;
|
|
2925
4112
|
}
|
|
2926
|
-
res.writeHead(200, {
|
|
4113
|
+
res.writeHead(200, {
|
|
4114
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
4115
|
+
"Cache-Control": "no-store"
|
|
4116
|
+
});
|
|
2927
4117
|
if (method === "HEAD") {
|
|
2928
4118
|
res.end();
|
|
2929
4119
|
return;
|
|
2930
4120
|
}
|
|
2931
|
-
res.end(data);
|
|
4121
|
+
res.end(injectSharedConfigIntoHtml(data));
|
|
2932
4122
|
});
|
|
2933
4123
|
return;
|
|
2934
4124
|
}
|
|
2935
|
-
const ext =
|
|
4125
|
+
const ext = import_path3.default.extname(servePath).toLowerCase();
|
|
2936
4126
|
const contentType = HTTP_MIME_TYPES[ext] || "application/octet-stream";
|
|
2937
|
-
res.writeHead(200, {
|
|
4127
|
+
res.writeHead(200, {
|
|
4128
|
+
"Content-Type": contentType,
|
|
4129
|
+
"Cache-Control": "no-store"
|
|
4130
|
+
});
|
|
2938
4131
|
if (method === "HEAD") {
|
|
2939
4132
|
res.end();
|
|
2940
4133
|
return;
|
|
2941
4134
|
}
|
|
2942
|
-
|
|
4135
|
+
if (ext === ".html") {
|
|
4136
|
+
import_fs3.default.readFile(servePath, (readErr, data) => {
|
|
4137
|
+
if (readErr) {
|
|
4138
|
+
try {
|
|
4139
|
+
res.end();
|
|
4140
|
+
} catch {
|
|
4141
|
+
}
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
res.end(injectSharedConfigIntoHtml(data));
|
|
4145
|
+
});
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
const stream = import_fs3.default.createReadStream(servePath);
|
|
2943
4149
|
stream.pipe(res);
|
|
2944
4150
|
stream.on("error", () => {
|
|
2945
4151
|
try {
|
|
@@ -2953,10 +4159,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2953
4159
|
sweepIdleCliClients() {
|
|
2954
4160
|
const now = Date.now();
|
|
2955
4161
|
let swept = false;
|
|
2956
|
-
for (const [ws,
|
|
4162
|
+
for (const [ws, simId] of this.cliSimBySocket) {
|
|
2957
4163
|
const lastCommand = this.cliLastCommandAt.get(ws) ?? 0;
|
|
2958
4164
|
if (now - lastCommand < _SootSimBridgeHost.CLI_IDLE_TIMEOUT_MS) continue;
|
|
2959
|
-
this.
|
|
4165
|
+
this.cliSimBySocket.delete(ws);
|
|
2960
4166
|
this.cliLastCommandAt.delete(ws);
|
|
2961
4167
|
for (const [sentId, entry] of this.cliBySentId) {
|
|
2962
4168
|
if (entry.ws === ws) this.cliBySentId.delete(sentId);
|
|
@@ -2968,61 +4174,87 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2968
4174
|
swept = true;
|
|
2969
4175
|
}
|
|
2970
4176
|
if (swept) {
|
|
2971
|
-
this.
|
|
4177
|
+
this.broadcastSimClientStates();
|
|
4178
|
+
}
|
|
4179
|
+
this.sweepRestorableSims(now);
|
|
4180
|
+
}
|
|
4181
|
+
// ping every connected ws; if the previous round's ping went unanswered,
|
|
4182
|
+
// terminate the socket so 'close' fires and the sim cleanup path
|
|
4183
|
+
// runs. matches the recommended ws-library heartbeat pattern.
|
|
4184
|
+
sweepDeadWebSockets() {
|
|
4185
|
+
if (!this.wss) return;
|
|
4186
|
+
for (const ws of this.wss.clients) {
|
|
4187
|
+
if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
4188
|
+
const alive = this.wsIsAlive.get(ws);
|
|
4189
|
+
if (alive === false) {
|
|
4190
|
+
try {
|
|
4191
|
+
ws.terminate();
|
|
4192
|
+
} catch {
|
|
4193
|
+
}
|
|
4194
|
+
continue;
|
|
4195
|
+
}
|
|
4196
|
+
this.wsIsAlive.set(ws, false);
|
|
4197
|
+
try {
|
|
4198
|
+
ws.ping();
|
|
4199
|
+
} catch {
|
|
4200
|
+
try {
|
|
4201
|
+
ws.terminate();
|
|
4202
|
+
} catch {
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
2972
4205
|
}
|
|
2973
|
-
this.sweepRestorableBrowsers(now);
|
|
2974
4206
|
}
|
|
2975
|
-
|
|
2976
|
-
return Array.from(this.
|
|
2977
|
-
if (a.id === this.
|
|
2978
|
-
if (b.id === this.
|
|
4207
|
+
listSims() {
|
|
4208
|
+
return Array.from(this.sims.values()).sort((a, b) => {
|
|
4209
|
+
if (a.id === this.primarySimId) return -1;
|
|
4210
|
+
if (b.id === this.primarySimId) return 1;
|
|
2979
4211
|
return a.connectedAt - b.connectedAt;
|
|
2980
|
-
}).map((
|
|
4212
|
+
}).map((sim) => this.describeSim(sim));
|
|
2981
4213
|
}
|
|
2982
4214
|
async sendCommand(cmd) {
|
|
2983
|
-
const
|
|
4215
|
+
const sim = await this.waitForSim(cmd.simId);
|
|
2984
4216
|
const id = this.nextCommandId++;
|
|
2985
4217
|
return new Promise((resolve2, reject) => {
|
|
2986
4218
|
const timeout = setTimeout(() => {
|
|
2987
4219
|
this.pendingCommands.delete(id);
|
|
2988
|
-
this.
|
|
4220
|
+
this.broadcastSimClientStates();
|
|
2989
4221
|
reject(new Error("command timed out after 30s"));
|
|
2990
4222
|
}, 3e4);
|
|
2991
4223
|
this.pendingCommands.set(id, {
|
|
2992
|
-
|
|
4224
|
+
simId: sim.id,
|
|
2993
4225
|
resolve: (value) => {
|
|
2994
4226
|
clearTimeout(timeout);
|
|
2995
4227
|
this.pendingCommands.delete(id);
|
|
2996
|
-
this.
|
|
4228
|
+
this.broadcastSimClientStates();
|
|
2997
4229
|
resolve2(value);
|
|
2998
4230
|
},
|
|
2999
4231
|
reject: (error) => {
|
|
3000
4232
|
clearTimeout(timeout);
|
|
3001
4233
|
this.pendingCommands.delete(id);
|
|
3002
|
-
this.
|
|
4234
|
+
this.broadcastSimClientStates();
|
|
3003
4235
|
reject(error);
|
|
3004
4236
|
}
|
|
3005
4237
|
});
|
|
3006
|
-
this.
|
|
3007
|
-
const {
|
|
3008
|
-
|
|
4238
|
+
this.broadcastSimClientStates();
|
|
4239
|
+
const { simId: _simId, ...forwarded } = cmd;
|
|
4240
|
+
sim.ws.send(JSON.stringify({ ...forwarded, id }));
|
|
3009
4241
|
});
|
|
3010
4242
|
}
|
|
3011
|
-
async evaluate(code,
|
|
3012
|
-
return this.sendCommand({ type: "evaluate", code,
|
|
4243
|
+
async evaluate(code, simId) {
|
|
4244
|
+
return this.sendCommand({ type: "evaluate", code, simId });
|
|
3013
4245
|
}
|
|
3014
|
-
async
|
|
3015
|
-
return this.sendCommand({ type: "focus",
|
|
4246
|
+
async focusSim(simId) {
|
|
4247
|
+
return this.sendCommand({ type: "focus", simId });
|
|
3016
4248
|
}
|
|
3017
|
-
async
|
|
3018
|
-
return this.sendCommand({ type: "close",
|
|
4249
|
+
async closeSim(simId) {
|
|
4250
|
+
return this.sendCommand({ type: "close", simId });
|
|
3019
4251
|
}
|
|
3020
4252
|
async openPathInEditor(filePath, line, column) {
|
|
3021
4253
|
const loc = line != null ? `:${line}${column != null ? `:${column}` : ""}` : "";
|
|
3022
4254
|
const target = `${filePath}${loc}`;
|
|
3023
4255
|
const trySpawn = (cmd, args) => new Promise((resolve2) => {
|
|
3024
4256
|
try {
|
|
3025
|
-
const child = (0,
|
|
4257
|
+
const child = (0, import_child_process4.spawn)(cmd, args, { detached: true, stdio: "ignore" });
|
|
3026
4258
|
let settled = false;
|
|
3027
4259
|
child.on("error", () => {
|
|
3028
4260
|
if (settled) return;
|
|
@@ -3049,26 +4281,12 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3049
4281
|
if (await trySpawn("code", ["-g", target])) return;
|
|
3050
4282
|
await this.openUrl(filePath);
|
|
3051
4283
|
}
|
|
3052
|
-
async openUrl(url) {
|
|
4284
|
+
async openUrl(url, options = {}) {
|
|
3053
4285
|
if (this.openUrlHandler) {
|
|
3054
|
-
await this.openUrlHandler(url);
|
|
4286
|
+
await this.openUrlHandler(url, options);
|
|
3055
4287
|
return;
|
|
3056
4288
|
}
|
|
3057
|
-
|
|
3058
|
-
const child2 = (0, import_child_process2.spawn)("open", ["-g", url], { detached: true, stdio: "ignore" });
|
|
3059
|
-
child2.unref();
|
|
3060
|
-
return;
|
|
3061
|
-
}
|
|
3062
|
-
if (process.platform === "win32") {
|
|
3063
|
-
const child2 = (0, import_child_process2.spawn)("cmd", ["/c", "start", "", url], {
|
|
3064
|
-
detached: true,
|
|
3065
|
-
stdio: "ignore"
|
|
3066
|
-
});
|
|
3067
|
-
child2.unref();
|
|
3068
|
-
return;
|
|
3069
|
-
}
|
|
3070
|
-
const child = (0, import_child_process2.spawn)("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
3071
|
-
child.unref();
|
|
4289
|
+
await openUrl(url, options);
|
|
3072
4290
|
}
|
|
3073
4291
|
async close() {
|
|
3074
4292
|
if (this.cliIdleTimer) {
|
|
@@ -3079,6 +4297,14 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3079
4297
|
clearInterval(this.heartbeatTimer);
|
|
3080
4298
|
this.heartbeatTimer = null;
|
|
3081
4299
|
}
|
|
4300
|
+
if (this.wsHeartbeatTimer) {
|
|
4301
|
+
clearInterval(this.wsHeartbeatTimer);
|
|
4302
|
+
this.wsHeartbeatTimer = null;
|
|
4303
|
+
}
|
|
4304
|
+
if (this.runtimeUpdateTimer) {
|
|
4305
|
+
clearInterval(this.runtimeUpdateTimer);
|
|
4306
|
+
this.runtimeUpdateTimer = null;
|
|
4307
|
+
}
|
|
3082
4308
|
if (this.shouldWriteLockfile) {
|
|
3083
4309
|
try {
|
|
3084
4310
|
removeDaemonLockfile();
|
|
@@ -3092,11 +4318,11 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3092
4318
|
pending.reject(new Error("server closing"));
|
|
3093
4319
|
this.pendingCommands.delete(id);
|
|
3094
4320
|
}
|
|
3095
|
-
for (const
|
|
3096
|
-
|
|
4321
|
+
for (const sim of this.sims.values()) {
|
|
4322
|
+
sim.ws.close();
|
|
3097
4323
|
}
|
|
3098
|
-
this.
|
|
3099
|
-
this.
|
|
4324
|
+
this.sims.clear();
|
|
4325
|
+
this.primarySimId = null;
|
|
3100
4326
|
const wss = this.wss;
|
|
3101
4327
|
const httpServer = this.httpServer;
|
|
3102
4328
|
this.wss = null;
|
|
@@ -3114,69 +4340,69 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3114
4340
|
}
|
|
3115
4341
|
}
|
|
3116
4342
|
}
|
|
3117
|
-
|
|
4343
|
+
describeSim(sim) {
|
|
3118
4344
|
let readyState;
|
|
3119
4345
|
try {
|
|
3120
|
-
readyState =
|
|
4346
|
+
readyState = sim.ws.readyState;
|
|
3121
4347
|
} catch {
|
|
3122
4348
|
readyState = import_ws.WebSocket.CLOSED;
|
|
3123
4349
|
}
|
|
3124
|
-
const lease = this.getActiveLease(
|
|
4350
|
+
const lease = this.getActiveLease(sim);
|
|
3125
4351
|
return {
|
|
3126
|
-
id:
|
|
3127
|
-
origin:
|
|
3128
|
-
url:
|
|
3129
|
-
title:
|
|
3130
|
-
userAgent:
|
|
3131
|
-
connectedAt:
|
|
3132
|
-
lastSeenAt:
|
|
3133
|
-
lastActiveAt:
|
|
3134
|
-
isPrimary:
|
|
4352
|
+
id: sim.id,
|
|
4353
|
+
origin: sim.origin,
|
|
4354
|
+
url: sim.url,
|
|
4355
|
+
title: sim.title,
|
|
4356
|
+
userAgent: sim.userAgent,
|
|
4357
|
+
connectedAt: sim.connectedAt,
|
|
4358
|
+
lastSeenAt: sim.lastSeenAt,
|
|
4359
|
+
lastActiveAt: sim.lastActiveAt || void 0,
|
|
4360
|
+
isPrimary: sim.id === this.primarySimId,
|
|
3135
4361
|
readyState: readyState === import_ws.WebSocket.OPEN ? "open" : readyState === import_ws.WebSocket.CLOSING ? "closing" : "closed",
|
|
3136
|
-
attachedCliCount: this.getAttachedCliCount(
|
|
3137
|
-
lockedBy: lease ? lease.cliLabel || lease.
|
|
4362
|
+
attachedCliCount: this.getAttachedCliCount(sim.id),
|
|
4363
|
+
lockedBy: lease ? lease.cliLabel || lease.cliIdentityKey : void 0,
|
|
3138
4364
|
lockedByKind: lease ? lease.kind : void 0,
|
|
3139
4365
|
lockExpiresAt: lease ? lease.expiresAt : void 0,
|
|
3140
|
-
userFocused:
|
|
4366
|
+
userFocused: sim.userFocused || void 0
|
|
3141
4367
|
};
|
|
3142
4368
|
}
|
|
3143
|
-
getActiveLease(
|
|
3144
|
-
const lease =
|
|
4369
|
+
getActiveLease(sim) {
|
|
4370
|
+
const lease = sim.cliLease;
|
|
3145
4371
|
if (!lease) return null;
|
|
3146
4372
|
if (Date.now() >= lease.expiresAt) {
|
|
3147
|
-
|
|
4373
|
+
sim.cliLease = void 0;
|
|
3148
4374
|
return null;
|
|
3149
4375
|
}
|
|
3150
4376
|
return lease;
|
|
3151
4377
|
}
|
|
3152
|
-
tryAcquireLease(ws,
|
|
3153
|
-
const
|
|
4378
|
+
tryAcquireLease(ws, sim, opts = {}) {
|
|
4379
|
+
const cliIdentityKey = this.cliIdentityKeyBySocket.get(ws) ?? (() => {
|
|
3154
4380
|
const fallback = `ws-${this.nextCliFallbackId++}`;
|
|
3155
|
-
this.
|
|
4381
|
+
this.cliIdentityKeyBySocket.set(ws, fallback);
|
|
3156
4382
|
return fallback;
|
|
3157
4383
|
})();
|
|
3158
4384
|
const cliLabel = this.cliLabelBySocket.get(ws);
|
|
3159
4385
|
const now = Date.now();
|
|
3160
|
-
const existing = this.getActiveLease(
|
|
3161
|
-
const ownerMatches = existing && existing.
|
|
4386
|
+
const existing = this.getActiveLease(sim);
|
|
4387
|
+
const ownerMatches = existing && existing.cliIdentityKey === cliIdentityKey;
|
|
3162
4388
|
let bootedCount = 0;
|
|
3163
4389
|
if (existing && !ownerMatches && !opts.force) {
|
|
3164
4390
|
return {
|
|
3165
4391
|
granted: false,
|
|
3166
4392
|
lease: existing,
|
|
3167
4393
|
lock: {
|
|
3168
|
-
by: existing.cliLabel || existing.
|
|
4394
|
+
by: existing.cliLabel || existing.cliIdentityKey,
|
|
3169
4395
|
expiresInMs: Math.max(0, existing.expiresAt - now)
|
|
3170
4396
|
},
|
|
3171
4397
|
bootedCount: 0
|
|
3172
4398
|
};
|
|
3173
4399
|
}
|
|
3174
4400
|
if (existing && !ownerMatches && opts.force) {
|
|
3175
|
-
for (const [cliWs,
|
|
3176
|
-
if (
|
|
3177
|
-
const otherKey = this.
|
|
3178
|
-
if (otherKey && otherKey !==
|
|
3179
|
-
this.
|
|
4401
|
+
for (const [cliWs, attachedSimId] of this.cliSimBySocket) {
|
|
4402
|
+
if (attachedSimId !== sim.id) continue;
|
|
4403
|
+
const otherKey = this.cliIdentityKeyBySocket.get(cliWs);
|
|
4404
|
+
if (otherKey && otherKey !== cliIdentityKey) {
|
|
4405
|
+
this.cliSimBySocket.delete(cliWs);
|
|
3180
4406
|
try {
|
|
3181
4407
|
cliWs.close(1e3, "lease claimed by another cli");
|
|
3182
4408
|
} catch {
|
|
@@ -3187,132 +4413,129 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3187
4413
|
}
|
|
3188
4414
|
const lease = {
|
|
3189
4415
|
kind: "cli",
|
|
3190
|
-
|
|
4416
|
+
cliIdentityKey,
|
|
3191
4417
|
cliLabel,
|
|
3192
4418
|
expiresAt: now + _SootSimBridgeHost.CLI_LEASE_TTL_MS
|
|
3193
4419
|
};
|
|
3194
|
-
|
|
4420
|
+
sim.cliLease = lease;
|
|
3195
4421
|
return { granted: true, lease, bootedCount };
|
|
3196
4422
|
}
|
|
3197
|
-
// user focus is advisory: we track it on the
|
|
4423
|
+
// user focus is advisory: we track it on the sim record so list/UI can
|
|
3198
4424
|
// show "focused" alongside any cli lease, but focus alone never creates a
|
|
3199
|
-
// blocking lease. the old 15s user-focus lease meant clicking on the
|
|
4425
|
+
// blocking lease. the old 15s user-focus lease meant clicking on the sim
|
|
3200
4426
|
// locked out agent inspect calls for 15s — the opposite of what you want
|
|
3201
4427
|
// when debugging something the user is actively looking at. use
|
|
3202
4428
|
// updateUserActivity() to lock on real interaction instead.
|
|
3203
|
-
updateUserFocusLease(
|
|
4429
|
+
updateUserFocusLease(sim, focused) {
|
|
3204
4430
|
const next = focused;
|
|
3205
|
-
if (
|
|
3206
|
-
|
|
3207
|
-
this.
|
|
4431
|
+
if (sim.userFocused === next) return;
|
|
4432
|
+
sim.userFocused = next;
|
|
4433
|
+
this.broadcastSimClientStates();
|
|
3208
4434
|
}
|
|
3209
|
-
// called when the
|
|
4435
|
+
// called when the sim reports a real user interaction (pointerdown,
|
|
3210
4436
|
// keydown, wheel, touch). creates or refreshes a short `user-active` lease
|
|
3211
|
-
// that keeps agent writes from trampling a user who is driving the
|
|
4437
|
+
// that keeps agent writes from trampling a user who is driving the sim.
|
|
3212
4438
|
// reads still pass through — shouldAcquireLease only blocks on writes.
|
|
3213
|
-
updateUserActivity(
|
|
3214
|
-
const existing = this.getActiveLease(
|
|
4439
|
+
updateUserActivity(sim) {
|
|
4440
|
+
const existing = this.getActiveLease(sim);
|
|
3215
4441
|
if (existing && existing.kind === "cli") {
|
|
3216
4442
|
return;
|
|
3217
4443
|
}
|
|
3218
|
-
|
|
4444
|
+
const now = Date.now();
|
|
4445
|
+
const refreshed = now + _SootSimBridgeHost.USER_ACTIVE_LEASE_TTL_MS;
|
|
4446
|
+
const expiresAt = existing && existing.kind === "user-active" ? Math.max(existing.expiresAt, refreshed) : refreshed;
|
|
4447
|
+
sim.cliLease = {
|
|
3219
4448
|
kind: "user-active",
|
|
3220
|
-
|
|
4449
|
+
cliIdentityKey: "__user-active__",
|
|
3221
4450
|
cliLabel: "active user",
|
|
3222
|
-
expiresAt
|
|
4451
|
+
expiresAt
|
|
3223
4452
|
};
|
|
3224
|
-
this.
|
|
4453
|
+
this.broadcastSimClientStates();
|
|
3225
4454
|
}
|
|
3226
|
-
|
|
3227
|
-
const existing = this.
|
|
4455
|
+
ensureCliIdentityKey(ws) {
|
|
4456
|
+
const existing = this.cliIdentityKeyBySocket.get(ws);
|
|
3228
4457
|
if (existing) return existing;
|
|
3229
4458
|
const fallback = `ws-${this.nextCliFallbackId++}`;
|
|
3230
|
-
this.
|
|
4459
|
+
this.cliIdentityKeyBySocket.set(ws, fallback);
|
|
3231
4460
|
return fallback;
|
|
3232
4461
|
}
|
|
3233
|
-
|
|
3234
|
-
if (
|
|
3235
|
-
const
|
|
3236
|
-
if (
|
|
4462
|
+
getOpenSim(simId) {
|
|
4463
|
+
if (simId) {
|
|
4464
|
+
const sim = this.sims.get(simId);
|
|
4465
|
+
if (sim?.ws.readyState === import_ws.WebSocket.OPEN) return sim;
|
|
3237
4466
|
return null;
|
|
3238
4467
|
}
|
|
3239
|
-
const primary = this.
|
|
4468
|
+
const primary = this.primarySimId != null ? this.sims.get(this.primarySimId) : null;
|
|
3240
4469
|
if (primary?.ws.readyState === import_ws.WebSocket.OPEN) return primary;
|
|
3241
|
-
for (const
|
|
3242
|
-
if (
|
|
4470
|
+
for (const sim of this.sims.values()) {
|
|
4471
|
+
if (sim.ws.readyState === import_ws.WebSocket.OPEN) return sim;
|
|
3243
4472
|
}
|
|
3244
4473
|
return null;
|
|
3245
4474
|
}
|
|
3246
|
-
async
|
|
4475
|
+
async waitForSim(simId, options = {}) {
|
|
3247
4476
|
const attempts = options.attempts ?? 10;
|
|
3248
4477
|
const intervalMs = options.intervalMs ?? 200;
|
|
3249
4478
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
3250
|
-
const
|
|
3251
|
-
if (
|
|
4479
|
+
const sim = this.getOpenSim(simId);
|
|
4480
|
+
if (sim) return sim;
|
|
3252
4481
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
3253
4482
|
}
|
|
3254
|
-
throw new Error(
|
|
3255
|
-
browserId ? `no browser connected with id ${browserId}` : "no browser connected"
|
|
3256
|
-
);
|
|
4483
|
+
throw new Error(simId ? `no sim connected with id ${simId}` : "no sim connected");
|
|
3257
4484
|
}
|
|
3258
|
-
|
|
3259
|
-
const current = this.
|
|
3260
|
-
const isPrimaryCandidate =
|
|
4485
|
+
shouldPromoteSim(sim) {
|
|
4486
|
+
const current = this.primarySimId ? this.sims.get(this.primarySimId) : null;
|
|
4487
|
+
const isPrimaryCandidate = sim.origin?.includes(":5173");
|
|
3261
4488
|
const currentIsPrimary = current?.origin?.includes(":5173");
|
|
3262
4489
|
return !current || current.ws.readyState !== import_ws.WebSocket.OPEN || !!isPrimaryCandidate || !currentIsPrimary;
|
|
3263
4490
|
}
|
|
3264
|
-
|
|
3265
|
-
for (const
|
|
3266
|
-
if (
|
|
3267
|
-
|
|
4491
|
+
broadcastSimAssignments() {
|
|
4492
|
+
for (const sim of this.sims.values()) {
|
|
4493
|
+
if (sim.ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
4494
|
+
sim.ws.send(
|
|
3268
4495
|
JSON.stringify({
|
|
3269
4496
|
type: "bridge:welcome",
|
|
3270
|
-
|
|
3271
|
-
isPrimary:
|
|
4497
|
+
simId: sim.id,
|
|
4498
|
+
isPrimary: sim.id === this.primarySimId
|
|
3272
4499
|
})
|
|
3273
4500
|
);
|
|
3274
4501
|
}
|
|
3275
4502
|
}
|
|
3276
|
-
|
|
3277
|
-
for (const
|
|
3278
|
-
if (
|
|
3279
|
-
const lease = this.getActiveLease(
|
|
4503
|
+
broadcastSimClientStates() {
|
|
4504
|
+
for (const sim of this.sims.values()) {
|
|
4505
|
+
if (sim.ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
4506
|
+
const lease = this.getActiveLease(sim);
|
|
3280
4507
|
const message = {
|
|
3281
4508
|
type: "bridge:client-state",
|
|
3282
|
-
attachedCliCount: this.getAttachedCliCount(
|
|
3283
|
-
activeAgentCommandCount: this.getActiveAgentCommandCount(
|
|
3284
|
-
recentActions:
|
|
3285
|
-
lockedBy: lease ? lease.cliLabel || lease.
|
|
4509
|
+
attachedCliCount: this.getAttachedCliCount(sim.id),
|
|
4510
|
+
activeAgentCommandCount: this.getActiveAgentCommandCount(sim.id),
|
|
4511
|
+
recentActions: sim.recentActions,
|
|
4512
|
+
lockedBy: lease ? lease.cliLabel || lease.cliIdentityKey : void 0,
|
|
3286
4513
|
lockedByKind: lease ? lease.kind : void 0,
|
|
3287
4514
|
lockExpiresAt: lease ? lease.expiresAt : void 0,
|
|
3288
|
-
userFocused:
|
|
4515
|
+
userFocused: sim.userFocused || void 0
|
|
3289
4516
|
};
|
|
3290
|
-
|
|
4517
|
+
sim.ws.send(JSON.stringify(message));
|
|
3291
4518
|
}
|
|
3292
4519
|
}
|
|
3293
|
-
|
|
3294
|
-
const
|
|
3295
|
-
if (
|
|
3296
|
-
this.
|
|
3297
|
-
this.
|
|
3298
|
-
|
|
3299
|
-
prevBrowserId ? "cli switched tabs" : "cli connected",
|
|
3300
|
-
false
|
|
3301
|
-
);
|
|
3302
|
-
this.broadcastBrowserClientStates();
|
|
4520
|
+
setCliSimTarget(ws, simId) {
|
|
4521
|
+
const prevSimId = this.cliSimBySocket.get(ws);
|
|
4522
|
+
if (prevSimId === simId) return;
|
|
4523
|
+
this.cliSimBySocket.set(ws, simId);
|
|
4524
|
+
this.recordSimAction(simId, prevSimId ? "cli switched sims" : "cli connected", false);
|
|
4525
|
+
this.broadcastSimClientStates();
|
|
3303
4526
|
}
|
|
3304
|
-
|
|
4527
|
+
recordSimAction(simId, label, broadcast = true) {
|
|
3305
4528
|
const normalized = label?.trim();
|
|
3306
4529
|
if (!normalized) return;
|
|
3307
|
-
const
|
|
3308
|
-
if (!
|
|
4530
|
+
const sim = this.sims.get(simId);
|
|
4531
|
+
if (!sim) return;
|
|
3309
4532
|
const now = Date.now();
|
|
3310
|
-
|
|
3311
|
-
|
|
4533
|
+
sim.lastActiveAt = now;
|
|
4534
|
+
sim.recentActions = [
|
|
3312
4535
|
{ label: normalized, at: now },
|
|
3313
|
-
...
|
|
4536
|
+
...sim.recentActions.filter((entry) => entry.label !== normalized)
|
|
3314
4537
|
].slice(0, 4);
|
|
3315
|
-
if (broadcast) this.
|
|
4538
|
+
if (broadcast) this.broadcastSimClientStates();
|
|
3316
4539
|
}
|
|
3317
4540
|
describeForwardedCommand(msg) {
|
|
3318
4541
|
switch (msg?.type) {
|
|
@@ -3327,90 +4550,97 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3327
4550
|
case "tree":
|
|
3328
4551
|
return "dumped tree";
|
|
3329
4552
|
case "focus":
|
|
3330
|
-
return "focused
|
|
4553
|
+
return "focused sim";
|
|
3331
4554
|
case "close":
|
|
3332
4555
|
return "requested close";
|
|
3333
4556
|
default:
|
|
3334
4557
|
return typeof msg?.type === "string" ? msg.type : null;
|
|
3335
4558
|
}
|
|
3336
4559
|
}
|
|
3337
|
-
// count distinct cli
|
|
4560
|
+
// count distinct cli identity keys attached to a sim, not raw sockets.
|
|
3338
4561
|
// a single agent firing sequential cli commands opens a new ws per call —
|
|
3339
4562
|
// counting sockets would report phantom peers until idle cleanup catches up.
|
|
3340
|
-
getAttachedCliCount(
|
|
4563
|
+
getAttachedCliCount(simId) {
|
|
3341
4564
|
const keys = /* @__PURE__ */ new Set();
|
|
3342
|
-
for (const [ws,
|
|
3343
|
-
if (
|
|
4565
|
+
for (const [ws, attachedSimId] of this.cliSimBySocket) {
|
|
4566
|
+
if (attachedSimId !== simId) continue;
|
|
3344
4567
|
if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
3345
|
-
const key = this.
|
|
4568
|
+
const key = this.cliIdentityKeyBySocket.get(ws);
|
|
3346
4569
|
keys.add(key ?? `ws-unknown-${keys.size}`);
|
|
3347
4570
|
}
|
|
3348
4571
|
return keys.size;
|
|
3349
4572
|
}
|
|
3350
|
-
// count distinct
|
|
3351
|
-
// used to warn a cli that other agents/
|
|
3352
|
-
|
|
3353
|
-
const selfKey = this.
|
|
4573
|
+
// count distinct identity keys attached to this sim other than `selfWs`.
|
|
4574
|
+
// used to warn a cli that other agents/identities are also targeting the sim.
|
|
4575
|
+
getOtherCliIdentityCount(selfWs, simId) {
|
|
4576
|
+
const selfKey = this.cliIdentityKeyBySocket.get(selfWs);
|
|
3354
4577
|
const keys = /* @__PURE__ */ new Set();
|
|
3355
|
-
for (const [ws,
|
|
3356
|
-
if (
|
|
4578
|
+
for (const [ws, attachedSimId] of this.cliSimBySocket) {
|
|
4579
|
+
if (attachedSimId !== simId) continue;
|
|
3357
4580
|
if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
3358
|
-
const key = this.
|
|
4581
|
+
const key = this.cliIdentityKeyBySocket.get(ws);
|
|
3359
4582
|
if (key && key === selfKey) continue;
|
|
3360
4583
|
keys.add(key ?? `ws-unknown-${keys.size}`);
|
|
3361
4584
|
}
|
|
3362
4585
|
return keys.size;
|
|
3363
4586
|
}
|
|
3364
|
-
getActiveAgentCommandCount(
|
|
4587
|
+
getActiveAgentCommandCount(simId) {
|
|
3365
4588
|
let count = 0;
|
|
3366
4589
|
for (const pending of this.pendingCommands.values()) {
|
|
3367
|
-
if (pending.
|
|
4590
|
+
if (pending.simId === simId) count++;
|
|
3368
4591
|
}
|
|
3369
4592
|
return count;
|
|
3370
4593
|
}
|
|
3371
|
-
|
|
4594
|
+
allocateSimId() {
|
|
4595
|
+
for (; ; ) {
|
|
4596
|
+
const id = this.nextSimNumber.toString(16);
|
|
4597
|
+
this.nextSimNumber++;
|
|
4598
|
+
if (!this.sims.has(id) && !this.restorableSims.has(id)) return id;
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
tryRestoreSimId(sim, requestedId) {
|
|
3372
4602
|
const nextId = requestedId?.trim();
|
|
3373
|
-
if (!nextId || nextId ===
|
|
3374
|
-
const existing = this.
|
|
3375
|
-
if (existing && existing !==
|
|
4603
|
+
if (!nextId || nextId === sim.id) return false;
|
|
4604
|
+
const existing = this.sims.get(nextId);
|
|
4605
|
+
if (existing && existing !== sim && existing.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
3376
4606
|
return false;
|
|
3377
4607
|
}
|
|
3378
|
-
const restorable = this.
|
|
3379
|
-
const prevId =
|
|
3380
|
-
this.
|
|
3381
|
-
|
|
4608
|
+
const restorable = this.getRestorableSimState(nextId);
|
|
4609
|
+
const prevId = sim.id;
|
|
4610
|
+
this.sims.delete(prevId);
|
|
4611
|
+
sim.id = nextId;
|
|
3382
4612
|
if (restorable) {
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
this.
|
|
4613
|
+
sim.recentActions = restorable.recentActions.map((entry) => ({ ...entry }));
|
|
4614
|
+
sim.lastActiveAt = restorable.lastActiveAt;
|
|
4615
|
+
sim.cliLease = restorable.cliLease ? { ...restorable.cliLease } : void 0;
|
|
4616
|
+
this.restorableSims.delete(nextId);
|
|
3387
4617
|
}
|
|
3388
|
-
this.
|
|
3389
|
-
if (this.
|
|
3390
|
-
this.
|
|
4618
|
+
this.sims.set(sim.id, sim);
|
|
4619
|
+
if (this.primarySimId === prevId) {
|
|
4620
|
+
this.primarySimId = sim.id;
|
|
3391
4621
|
}
|
|
3392
|
-
for (const [ws,
|
|
3393
|
-
if (
|
|
3394
|
-
this.
|
|
4622
|
+
for (const [ws, attachedSimId] of this.cliSimBySocket) {
|
|
4623
|
+
if (attachedSimId === prevId) {
|
|
4624
|
+
this.cliSimBySocket.set(ws, sim.id);
|
|
3395
4625
|
}
|
|
3396
4626
|
}
|
|
3397
4627
|
return true;
|
|
3398
4628
|
}
|
|
3399
|
-
|
|
3400
|
-
const lease = this.getActiveLease(
|
|
3401
|
-
this.
|
|
3402
|
-
recentActions:
|
|
3403
|
-
lastActiveAt:
|
|
4629
|
+
rememberDisconnectedSim(sim) {
|
|
4630
|
+
const lease = this.getActiveLease(sim);
|
|
4631
|
+
this.restorableSims.set(sim.id, {
|
|
4632
|
+
recentActions: sim.recentActions.map((entry) => ({ ...entry })),
|
|
4633
|
+
lastActiveAt: sim.lastActiveAt,
|
|
3404
4634
|
cliLease: lease && lease.kind === "cli" ? { ...lease } : void 0,
|
|
3405
|
-
expiresAt: Date.now() + _SootSimBridgeHost.
|
|
4635
|
+
expiresAt: Date.now() + _SootSimBridgeHost.SIM_RECONNECT_TTL_MS
|
|
3406
4636
|
});
|
|
3407
|
-
this.
|
|
4637
|
+
this.sims.delete(sim.id);
|
|
3408
4638
|
}
|
|
3409
|
-
|
|
3410
|
-
const snapshot = this.
|
|
4639
|
+
getRestorableSimState(simId) {
|
|
4640
|
+
const snapshot = this.restorableSims.get(simId);
|
|
3411
4641
|
if (!snapshot) return null;
|
|
3412
4642
|
if (snapshot.expiresAt <= Date.now()) {
|
|
3413
|
-
this.
|
|
4643
|
+
this.restorableSims.delete(simId);
|
|
3414
4644
|
return null;
|
|
3415
4645
|
}
|
|
3416
4646
|
if (snapshot.cliLease && snapshot.cliLease.expiresAt <= Date.now()) {
|
|
@@ -3418,13 +4648,13 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3418
4648
|
}
|
|
3419
4649
|
return snapshot;
|
|
3420
4650
|
}
|
|
3421
|
-
|
|
3422
|
-
for (const [
|
|
4651
|
+
sweepRestorableSims(now = Date.now()) {
|
|
4652
|
+
for (const [simId, snapshot] of this.restorableSims) {
|
|
3423
4653
|
if (snapshot.expiresAt > now) continue;
|
|
3424
|
-
this.
|
|
3425
|
-
for (const [cliWs,
|
|
3426
|
-
if (
|
|
3427
|
-
this.
|
|
4654
|
+
this.restorableSims.delete(simId);
|
|
4655
|
+
for (const [cliWs, attachedSimId] of this.cliSimBySocket) {
|
|
4656
|
+
if (attachedSimId === simId) {
|
|
4657
|
+
this.cliSimBySocket.delete(cliWs);
|
|
3428
4658
|
}
|
|
3429
4659
|
}
|
|
3430
4660
|
}
|
|
@@ -3434,6 +4664,14 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3434
4664
|
clearInterval(this.cliIdleTimer);
|
|
3435
4665
|
this.cliIdleTimer = null;
|
|
3436
4666
|
}
|
|
4667
|
+
if (this.wsHeartbeatTimer) {
|
|
4668
|
+
clearInterval(this.wsHeartbeatTimer);
|
|
4669
|
+
this.wsHeartbeatTimer = null;
|
|
4670
|
+
}
|
|
4671
|
+
if (this.runtimeUpdateTimer) {
|
|
4672
|
+
clearInterval(this.runtimeUpdateTimer);
|
|
4673
|
+
this.runtimeUpdateTimer = null;
|
|
4674
|
+
}
|
|
3437
4675
|
const wss = this.wss;
|
|
3438
4676
|
const httpServer = this.httpServer;
|
|
3439
4677
|
this.wss = null;
|