sootsim 0.1.36 → 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/README.md +20 -5
- package/dist-cli/bin.js +15 -20
- package/dist-cli/chunks/{agent-YZB6D3DR.js → agent-EQRQGSBL.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-VHCVS22I.js → agent-wrapper-AWKZ67GN.js} +10 -10
- package/dist-cli/chunks/{assert-AIVCKKLG.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-A5BRCXYE.js → chunk-4IO3D5XG.js} +1 -1
- 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-LHDWH7VS.js → chunk-B3RAGRK6.js} +1 -1
- package/dist-cli/chunks/{chunk-27P763IZ.js → chunk-BGAPLYMS.js} +2 -2
- package/dist-cli/chunks/chunk-CX3ZIPD3.js +3 -0
- package/dist-cli/chunks/{chunk-HWCKZXNJ.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-G7XQD4KC.js → chunk-LDWXH43L.js} +2 -2
- 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-VFDRZNPN.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-IJMYFYDZ.js → chunk-S74RCIVB.js} +2 -2
- package/dist-cli/chunks/chunk-SK4SOISL.js +1 -0
- package/dist-cli/chunks/{chunk-YIO6S3R5.js → chunk-T5L73GJB.js} +1 -1
- package/dist-cli/chunks/{chunk-KAXZHEKM.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-EWSQSALM.js → chunk-WHLHA5R5.js} +4 -4
- package/dist-cli/chunks/chunk-WLIVBPPY.js +3 -0
- package/dist-cli/chunks/{chunk-CYCXOAVZ.js → chunk-X6BP5JFC.js} +4 -4
- package/dist-cli/chunks/chunk-YFXTO4QX.js +5 -0
- package/dist-cli/chunks/{chunk-RMW5BO3S.js → chunk-Z5SVSAZO.js} +2 -2
- package/dist-cli/chunks/{chunk-OXN2PEB7.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-Y2O2U7FL.js → compat-QQ3OJDBI.js} +2 -2
- package/dist-cli/chunks/{config-SRBOFUCI.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-BIDMW2PE.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-2MYEI4FM.js → install-desktop-ZNWYKTWQ.js} +3 -3
- package/dist-cli/chunks/{keys-7PNASIQR.js → keys-5ETF6DYO.js} +2 -2
- package/dist-cli/chunks/{launch-JNS47LAQ.js → launch-DHUCNFX6.js} +3 -3
- package/dist-cli/chunks/{login-YWZWUHBS.js → login-KDR34JIP.js} +4 -4
- package/dist-cli/chunks/{logout-O6SXMSBP.js → logout-R6WIJYCW.js} +2 -2
- package/dist-cli/chunks/maestro-ZOOJ2YVH.js +80 -0
- package/dist-cli/chunks/{preview-WGKJO5FS.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-QPWLYH5R.js → record-IWLEYATN.js} +5 -5
- package/dist-cli/chunks/{runtime-KEMO2MSB.js → runtime-WKMNKYTN.js} +3 -3
- 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-MO7BFNVM.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-XUI3KNNQ.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 +4 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +41 -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 +5 -21
- package/dist-lib/home-paths.cjs +94 -38
- package/dist-lib/host/bridge-host.cjs +2131 -1333
- package/dist-lib/host/fetch-proxy-handler.cjs +248 -0
- package/dist-lib/index.cjs +21 -21
- package/dist-lib/metro.cjs +21 -21
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/vite-base.cjs +3402 -1640
- package/dist-lib/vite.cjs +1 -1
- package/package.json +7 -1
- package/dist-cli/chunks/auto-bootstrap-MLNTX23H.js +0 -2
- package/dist-cli/chunks/chunk-3UIWOHC2.js +0 -62
- package/dist-cli/chunks/chunk-5KGFHWVR.js +0 -1
- package/dist-cli/chunks/chunk-5QIUJNT3.js +0 -5
- package/dist-cli/chunks/chunk-6GGMKFWJ.js +0 -4
- package/dist-cli/chunks/chunk-6Z275LCY.js +0 -2
- package/dist-cli/chunks/chunk-75LBYBKW.js +0 -11
- package/dist-cli/chunks/chunk-DFN3GGH7.js +0 -5
- package/dist-cli/chunks/chunk-EBEHZJRG.js +0 -117
- package/dist-cli/chunks/chunk-EJLNUMMP.js +0 -3
- package/dist-cli/chunks/chunk-FE7UI3MT.js +0 -4
- package/dist-cli/chunks/chunk-G663654J.js +0 -1
- package/dist-cli/chunks/chunk-GW7XY5KC.js +0 -2
- package/dist-cli/chunks/chunk-H2QO4TDV.js +0 -22
- package/dist-cli/chunks/chunk-HWFHBMAQ.js +0 -27
- package/dist-cli/chunks/chunk-J7CTD37P.js +0 -1
- package/dist-cli/chunks/chunk-N32NCVL2.js +0 -3
- package/dist-cli/chunks/chunk-NIZBR7EK.js +0 -2
- package/dist-cli/chunks/chunk-NYY36OKU.js +0 -308
- package/dist-cli/chunks/chunk-PJL25JQV.js +0 -5
- package/dist-cli/chunks/chunk-SHO54NET.js +0 -2
- package/dist-cli/chunks/chunk-SMVJOWSV.js +0 -16
- package/dist-cli/chunks/chunk-TC6V7YFC.js +0 -3
- package/dist-cli/chunks/chunk-YLIIVTTQ.js +0 -3
- package/dist-cli/chunks/chunk-YR7BGGYE.js +0 -2
- package/dist-cli/chunks/chunk-ZEW3RF5Q.js +0 -1
- package/dist-cli/chunks/control-PL2V2O6S.js +0 -2
- package/dist-cli/chunks/daemon-IZC32PZW.js +0 -50
- package/dist-cli/chunks/demo-app-registry-5JFOUU3D.js +0 -2
- package/dist-cli/chunks/detox-B3FDOIS3.js +0 -49
- package/dist-cli/chunks/device-ZZSI363W.js +0 -16
- package/dist-cli/chunks/drivers-S4NGK4DB.js +0 -2
- package/dist-cli/chunks/electron-5YFHXEOI.js +0 -15
- package/dist-cli/chunks/flow-JJBO6TFY.js +0 -2
- package/dist-cli/chunks/hints-G5HBBV2O.js +0 -2
- package/dist-cli/chunks/home-paths-VWC3FWA3.js +0 -2
- package/dist-cli/chunks/inspect-POOPWUQI.js +0 -1034
- package/dist-cli/chunks/install-MP6FHXNZ.js +0 -2
- package/dist-cli/chunks/install-dev-desktop-SKH3KEHY.js +0 -100
- package/dist-cli/chunks/maestro-CW6XVUKV.js +0 -75
- package/dist-cli/chunks/profile-SUOBRPIC.js +0 -22
- package/dist-cli/chunks/screenshot-JTY46V7G.js +0 -26
- package/dist-cli/chunks/screenshot-mode-7OYBBX6D.js +0 -17
- package/dist-cli/chunks/screenshots-QISKC4GD.js +0 -70
- package/dist-cli/chunks/server-YSFJAKAV.js +0 -34
- package/dist-cli/chunks/setup-repo-LFB3HBEO.js +0 -2
- package/dist-cli/chunks/store-6MFL53I4.js +0 -2
- package/dist-cli/chunks/telemetry-CN42GMVC.js +0 -2
- package/dist-cli/chunks/upload-6FUT7AX5.js +0 -2
- package/dist-cli/chunks/whoami-TQFHY42N.js +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! sootsim v0.1.
|
|
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;
|
|
@@ -36,700 +36,565 @@ __export(bridge_host_exports, {
|
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(bridge_host_exports);
|
|
38
38
|
var import_child_process4 = require("child_process");
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
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
|
-
//
|
|
45
|
-
var
|
|
44
|
+
// scripts/dev-server-scanner.ts
|
|
45
|
+
var import_child_process = require("child_process");
|
|
46
|
+
var import_http = __toESM(require("http"), 1);
|
|
47
|
+
var import_net = __toESM(require("net"), 1);
|
|
48
|
+
var import_util = require("util");
|
|
46
49
|
|
|
47
|
-
// src/
|
|
48
|
-
var
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
var SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
|
|
52
|
-
var ACTIVE_RUNTIME_FILE = "active";
|
|
53
|
-
var DAEMON_LOCKFILE = "daemon.json";
|
|
54
|
-
var CONFIG_FILE = "config.json";
|
|
55
|
-
var DAEMON_HEARTBEAT_STALE_MS = 3e4;
|
|
56
|
-
function sootsimHomeDir() {
|
|
57
|
-
const override = process.env[SOOTSIM_HOME_ENV];
|
|
58
|
-
if (override && override.length > 0) return import_path.default.resolve(override);
|
|
59
|
-
return import_path.default.join((0, import_os.homedir)(), ".sootsim");
|
|
60
|
-
}
|
|
61
|
-
function runtimesDir() {
|
|
62
|
-
return import_path.default.join(sootsimHomeDir(), "runtimes");
|
|
63
|
-
}
|
|
64
|
-
function runtimeDir(version) {
|
|
65
|
-
return import_path.default.join(runtimesDir(), version);
|
|
66
|
-
}
|
|
67
|
-
function activeRuntimeFile() {
|
|
68
|
-
return import_path.default.join(runtimesDir(), ACTIVE_RUNTIME_FILE);
|
|
69
|
-
}
|
|
70
|
-
function cacheDir() {
|
|
71
|
-
return import_path.default.join(sootsimHomeDir(), "cache");
|
|
72
|
-
}
|
|
73
|
-
function daemonLockfilePath() {
|
|
74
|
-
return import_path.default.join(sootsimHomeDir(), DAEMON_LOCKFILE);
|
|
75
|
-
}
|
|
76
|
-
function configFilePath() {
|
|
77
|
-
return import_path.default.join(sootsimHomeDir(), CONFIG_FILE);
|
|
50
|
+
// src/config.ts
|
|
51
|
+
var SOOTSIM_CONFIG_QUERY_PARAM = "sootsimConfig";
|
|
52
|
+
function hasOwnKeys(value) {
|
|
53
|
+
return !!value && Object.keys(value).length > 0;
|
|
78
54
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
import_fs.default.mkdirSync(cacheDir(), { recursive: true });
|
|
55
|
+
function hasSootSimConfig(config) {
|
|
56
|
+
if (!config) return false;
|
|
57
|
+
return hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.nativeModules) || hasOwnKeys(config.env) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialState);
|
|
83
58
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
59
|
+
function applySootSimConfigToUrl(url, config) {
|
|
60
|
+
const parsed = new URL(url);
|
|
61
|
+
if (hasSootSimConfig(config)) {
|
|
62
|
+
parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config));
|
|
63
|
+
} else {
|
|
64
|
+
parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM);
|
|
90
65
|
}
|
|
66
|
+
return parsed.toString();
|
|
91
67
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
function listInstalledRuntimes() {
|
|
98
|
-
try {
|
|
99
|
-
return import_fs.default.readdirSync(runtimesDir(), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort(compareSemver);
|
|
100
|
-
} catch {
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
68
|
+
|
|
69
|
+
// src/native-dev-bundle-url.ts
|
|
70
|
+
function isAbsoluteHttpUrl(url) {
|
|
71
|
+
return /^https?:\/\//i.test(url);
|
|
103
72
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
const hyphen = v.indexOf("-");
|
|
107
|
-
const core = hyphen >= 0 ? v.slice(0, hyphen) : v;
|
|
108
|
-
const pre = hyphen >= 0 ? v.slice(hyphen + 1) : "";
|
|
109
|
-
const parts = core.split(".").map((n) => Number.parseInt(n, 10));
|
|
110
|
-
if (parts.some((n) => !Number.isFinite(n))) return [[Number.POSITIVE_INFINITY], v];
|
|
111
|
-
return [parts, pre];
|
|
112
|
-
};
|
|
113
|
-
const [an, ap] = parse(a);
|
|
114
|
-
const [bn, bp] = parse(b);
|
|
115
|
-
for (let i = 0; i < Math.max(an.length, bn.length); i++) {
|
|
116
|
-
const av = an[i] ?? 0;
|
|
117
|
-
const bv = bn[i] ?? 0;
|
|
118
|
-
if (av !== bv) return av - bv;
|
|
119
|
-
}
|
|
120
|
-
if (ap === bp) return 0;
|
|
121
|
-
if (!ap) return 1;
|
|
122
|
-
if (!bp) return -1;
|
|
123
|
-
return ap < bp ? -1 : 1;
|
|
73
|
+
function isNativeDevBundlePath(pathname) {
|
|
74
|
+
return pathname.endsWith(".bundle");
|
|
124
75
|
}
|
|
125
|
-
function
|
|
126
|
-
const version = readActiveRuntime();
|
|
127
|
-
if (!version) return null;
|
|
128
|
-
const dir = runtimeDir(version);
|
|
76
|
+
function normalizeNativeDevBundleUrl(bundleUrl) {
|
|
129
77
|
try {
|
|
130
|
-
|
|
78
|
+
const isAbsolute = isAbsoluteHttpUrl(bundleUrl);
|
|
79
|
+
const parsed = new URL(bundleUrl, "http://soot.local");
|
|
80
|
+
parsed.pathname = parsed.pathname.replace(/\.\.bundle$/, ".bundle");
|
|
81
|
+
if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl;
|
|
82
|
+
parsed.searchParams.delete("transform.bytecode");
|
|
83
|
+
if (isAbsolute) return parsed.toString();
|
|
84
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
131
85
|
} catch {
|
|
86
|
+
return bundleUrl;
|
|
132
87
|
}
|
|
133
|
-
return null;
|
|
134
88
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
89
|
+
|
|
90
|
+
// scripts/demo-app-registry.ts
|
|
91
|
+
var import_node_fs = require("node:fs");
|
|
92
|
+
var import_node_os = require("node:os");
|
|
93
|
+
var import_node_path = require("node:path");
|
|
94
|
+
var HOME = (0, import_node_os.homedir)();
|
|
95
|
+
function findWorkspaceRoot(startDir) {
|
|
96
|
+
let dir = startDir;
|
|
97
|
+
while (true) {
|
|
98
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "pnpm-workspace.yaml")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "turbo.json")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "nx.json")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "lerna.json"))) {
|
|
99
|
+
return dir;
|
|
100
|
+
}
|
|
101
|
+
const packageJsonPath = (0, import_node_path.join)(dir, "package.json");
|
|
102
|
+
if ((0, import_node_fs.existsSync)(packageJsonPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
|
|
105
|
+
if (pkg.workspaces) return dir;
|
|
106
|
+
} catch {
|
|
146
107
|
}
|
|
147
|
-
return null;
|
|
148
|
-
} finally {
|
|
149
|
-
import_fs.default.closeSync(fd);
|
|
150
108
|
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
109
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
110
|
+
if (parent === dir) return null;
|
|
111
|
+
dir = parent;
|
|
153
112
|
}
|
|
154
113
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
process.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
114
|
+
function resolveWorkspaceScriptPath(workspaceRelativePath, packageRelativePath) {
|
|
115
|
+
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
|
116
|
+
const candidates = [
|
|
117
|
+
workspaceRoot ? (0, import_node_path.resolve)(workspaceRoot, workspaceRelativePath) : null,
|
|
118
|
+
(0, import_node_path.resolve)(process.cwd(), workspaceRelativePath),
|
|
119
|
+
(0, import_node_path.resolve)(process.cwd(), packageRelativePath)
|
|
120
|
+
].filter((candidate) => Boolean(candidate));
|
|
121
|
+
for (const candidate of candidates) {
|
|
122
|
+
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
163
123
|
}
|
|
124
|
+
return candidates[0] ?? (0, import_node_path.resolve)(process.cwd(), workspaceRelativePath);
|
|
164
125
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
126
|
+
var getExpensifyProxyScript = () => resolveWorkspaceScriptPath(
|
|
127
|
+
"packages/sootsim-engine/scripts/expensify-web-proxy.ts",
|
|
128
|
+
"scripts/expensify-web-proxy.ts"
|
|
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
|
+
);
|
|
146
|
+
var EXPENSIFY_NATIVE_PROXY_ENV = {
|
|
147
|
+
USE_NGROK: "true",
|
|
148
|
+
NGROK_URL: "http://localhost:9000/",
|
|
149
|
+
SECURE_NGROK_URL: "http://localhost:9000/"
|
|
150
|
+
};
|
|
151
|
+
var MATTERMOST_DIR = (0, import_node_path.join)(HOME, "github/mattermost-mobile");
|
|
152
|
+
var UNISWAP_REPO_DIR = (0, import_node_path.join)(HOME, "github/uniswap-interface");
|
|
153
|
+
var UNISWAP_APP_DIR = (0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/mobile");
|
|
154
|
+
var UNISWAP_ENV_LOCAL_FILE = (0, import_node_path.join)(UNISWAP_REPO_DIR, ".env.defaults.local");
|
|
155
|
+
var UNISWAP_PLACEHOLDER = "stored-in-.env.local";
|
|
156
|
+
var UNISWAP_DEMO_ENV_MARKER = "# sootsim demo env overrides";
|
|
157
|
+
var UNISWAP_FORCE_UPGRADE_HOOK_FILE = (0, import_node_path.join)(
|
|
158
|
+
UNISWAP_REPO_DIR,
|
|
159
|
+
"packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts"
|
|
160
|
+
);
|
|
161
|
+
var UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE = (0, import_node_path.join)(
|
|
162
|
+
UNISWAP_REPO_DIR,
|
|
163
|
+
"apps/mobile/src/notification-service/data-sources/createForceUpgradeNotificationDataSource.ts"
|
|
164
|
+
);
|
|
165
|
+
var UNISWAP_FORCE_UPGRADE_PATCH_MARKER = "SOOTSIM_DEMO_DISABLE_FORCE_UPGRADE";
|
|
166
|
+
function parseEnvFile(filePath) {
|
|
167
|
+
if (!(0, import_node_fs.existsSync)(filePath)) return {};
|
|
168
|
+
const env = {};
|
|
169
|
+
const source = (0, import_node_fs.readFileSync)(filePath, "utf8");
|
|
170
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
171
|
+
const line = rawLine.trim();
|
|
172
|
+
if (!line || line.startsWith("#")) continue;
|
|
173
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
174
|
+
if (!match) continue;
|
|
175
|
+
let value = match[2].trim();
|
|
176
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
177
|
+
value = value.slice(1, -1);
|
|
178
|
+
}
|
|
179
|
+
env[match[1]] = value;
|
|
177
180
|
}
|
|
178
|
-
|
|
179
|
-
return true;
|
|
181
|
+
return env;
|
|
180
182
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
function isUsableUniswapEnvValue(value) {
|
|
184
|
+
if (!value) return false;
|
|
185
|
+
const trimmed = value.trim();
|
|
186
|
+
if (!trimmed) return false;
|
|
187
|
+
if (trimmed.includes(UNISWAP_PLACEHOLDER)) return false;
|
|
188
|
+
if (trimmed === "TRADING_API_KEY" || trimmed === "UNISWAP_API_KEY") return false;
|
|
189
|
+
return true;
|
|
186
190
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
var import_stream = require("stream");
|
|
194
|
-
var import_promises = require("stream/promises");
|
|
195
|
-
var DEFAULT_RUNTIME_CDN_ORIGIN = "https://sootbean.com";
|
|
196
|
-
var RUNTIME_CDN_ORIGIN_ENV = "SOOTSIM_CDN_ORIGIN";
|
|
197
|
-
var RUNTIME_CHANNEL_ENV = "SOOTSIM_RUNTIME_CHANNEL";
|
|
198
|
-
function readConfig() {
|
|
199
|
-
try {
|
|
200
|
-
const parsed = JSON.parse(import_fs2.default.readFileSync(configFilePath(), "utf8"));
|
|
201
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
202
|
-
} catch {
|
|
203
|
-
return {};
|
|
191
|
+
function pickEnvValue(sources, keys) {
|
|
192
|
+
for (const source of sources) {
|
|
193
|
+
for (const key of keys) {
|
|
194
|
+
const value = source[key];
|
|
195
|
+
if (isUsableUniswapEnvValue(value)) return value.trim();
|
|
196
|
+
}
|
|
204
197
|
}
|
|
198
|
+
return void 0;
|
|
205
199
|
}
|
|
206
|
-
function
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
200
|
+
function resolveUniswapDemoEnv() {
|
|
201
|
+
const localEnv = parseEnvFile(UNISWAP_ENV_LOCAL_FILE);
|
|
202
|
+
const webEnv = parseEnvFile((0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/web/.env"));
|
|
203
|
+
const sources = [
|
|
204
|
+
process.env,
|
|
205
|
+
localEnv,
|
|
206
|
+
webEnv
|
|
207
|
+
];
|
|
208
|
+
const env = {};
|
|
209
|
+
const bindings = [
|
|
210
|
+
["AMPLITUDE_PROXY_URL_OVERRIDE", ["REACT_APP_AMPLITUDE_PROXY_URL"]],
|
|
211
|
+
["QUICKNODE_ENDPOINT_NAME", ["REACT_APP_QUICKNODE_ENDPOINT_NAME"]],
|
|
212
|
+
["QUICKNODE_ENDPOINT_TOKEN", ["REACT_APP_QUICKNODE_ENDPOINT_TOKEN"]],
|
|
213
|
+
["INFURA_KEY", ["REACT_APP_INFURA_KEY"]],
|
|
214
|
+
["STATSIG_API_KEY", ["REACT_APP_STATSIG_API_KEY"]],
|
|
215
|
+
["STATSIG_PROXY_URL_OVERRIDE", ["REACT_APP_STATSIG_PROXY_URL"]],
|
|
216
|
+
["WALLETCONNECT_PROJECT_ID", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
|
|
217
|
+
["WALLETCONNECT_PROJECT_ID_BETA", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
|
|
218
|
+
["WALLETCONNECT_PROJECT_ID_DEV", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
|
|
219
|
+
["TRADING_API_KEY", ["REACT_APP_TRADING_API_KEY"]],
|
|
220
|
+
["UNISWAP_API_KEY", []]
|
|
221
|
+
];
|
|
222
|
+
for (const [target, aliases] of bindings) {
|
|
223
|
+
const value = pickEnvValue(sources, [target, ...aliases]);
|
|
224
|
+
if (!value) continue;
|
|
225
|
+
env[target] = value;
|
|
226
|
+
for (const alias of aliases) {
|
|
227
|
+
env[alias] = value;
|
|
228
|
+
}
|
|
228
229
|
}
|
|
229
|
-
|
|
230
|
+
const hasPrivateGatewayKeys = isUsableUniswapEnvValue(env.TRADING_API_KEY) && isUsableUniswapEnvValue(env.UNISWAP_API_KEY);
|
|
231
|
+
if (!hasPrivateGatewayKeys) {
|
|
232
|
+
const publicGraphqlUrl = pickEnvValue(sources, ["GRAPHQL_URL_OVERRIDE", "REACT_APP_AWS_API_ENDPOINT"]) || "https://interface.gateway.uniswap.org/v1/graphql";
|
|
233
|
+
env.API_BASE_URL_OVERRIDE = "https://interface.gateway.uniswap.org";
|
|
234
|
+
env.API_BASE_URL_V2_OVERRIDE = "https://interface.gateway.uniswap.org/v2";
|
|
235
|
+
env.GRAPHQL_URL_OVERRIDE = publicGraphqlUrl;
|
|
236
|
+
env.TRADING_API_URL_OVERRIDE = "https://trading-api-labs.interface.gateway.uniswap.org";
|
|
237
|
+
env.FOR_API_URL_OVERRIDE = "https://for.interface.gateway.uniswap.org/v2/FOR.v1.FORService";
|
|
238
|
+
}
|
|
239
|
+
return env;
|
|
230
240
|
}
|
|
231
|
-
function
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
throw new Error(
|
|
236
|
-
`no version specified and channel '${channel}' has no latest entry in the manifest`
|
|
237
|
-
);
|
|
241
|
+
function ensureUniswapDemoEnvLocal() {
|
|
242
|
+
const existingSource = (0, import_node_fs.existsSync)(UNISWAP_ENV_LOCAL_FILE) ? (0, import_node_fs.readFileSync)(UNISWAP_ENV_LOCAL_FILE, "utf8") : "";
|
|
243
|
+
if (existingSource && !existingSource.includes(UNISWAP_DEMO_ENV_MARKER)) {
|
|
244
|
+
return;
|
|
238
245
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
);
|
|
246
|
+
const env = resolveUniswapDemoEnv();
|
|
247
|
+
const lines = [UNISWAP_DEMO_ENV_MARKER];
|
|
248
|
+
for (const [key, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) {
|
|
249
|
+
lines.push(`${key}=${JSON.stringify(value)}`);
|
|
244
250
|
}
|
|
245
|
-
|
|
251
|
+
lines.push("");
|
|
252
|
+
(0, import_node_fs.writeFileSync)(UNISWAP_ENV_LOCAL_FILE, `${lines.join("\n")}
|
|
253
|
+
`);
|
|
246
254
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return
|
|
257
|
-
version,
|
|
258
|
-
channel,
|
|
259
|
-
cdnOrigin,
|
|
260
|
-
runtimeDir: destDir,
|
|
261
|
-
installed: false,
|
|
262
|
-
activated: setActive,
|
|
263
|
-
manifest
|
|
264
|
-
};
|
|
255
|
+
function ensureUniswapForceUpgradePatched() {
|
|
256
|
+
const hookNeedle = `export function useForceUpgradeStatus(): ForceUpgradeStatus {
|
|
257
|
+
`;
|
|
258
|
+
const hookPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
259
|
+
return 'not-required'
|
|
260
|
+
|
|
261
|
+
`;
|
|
262
|
+
const hookLegacyPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
263
|
+
if (process.env.${UNISWAP_FORCE_UPGRADE_PATCH_MARKER} === 'true') {
|
|
264
|
+
return 'not-required'
|
|
265
265
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
|
|
267
|
+
`;
|
|
268
|
+
const notificationNeedle = ` const getForceUpgradeStatus = (): ForceUpgradeStatus => {
|
|
269
|
+
`;
|
|
270
|
+
const notificationPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
271
|
+
return 'not-required'
|
|
272
|
+
|
|
273
|
+
`;
|
|
274
|
+
const notificationLegacyPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
275
|
+
if (process.env.${UNISWAP_FORCE_UPGRADE_PATCH_MARKER} === 'true') {
|
|
276
|
+
return 'not-required'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
`;
|
|
280
|
+
const patchWithMigration = (filePath, needle, patch, legacyPatch) => {
|
|
281
|
+
const source = (0, import_node_fs.readFileSync)(filePath, "utf8");
|
|
282
|
+
if (source.includes(patch)) return;
|
|
283
|
+
if (source.includes(legacyPatch)) {
|
|
284
|
+
(0, import_node_fs.writeFileSync)(filePath, source.replace(legacyPatch, patch));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!source.includes(needle)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`uniswap demo patch failed: expected snippet not found in ${filePath}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
(0, import_node_fs.writeFileSync)(filePath, source.replace(needle, `${needle}${patch}`));
|
|
293
|
+
};
|
|
294
|
+
patchWithMigration(
|
|
295
|
+
UNISWAP_FORCE_UPGRADE_HOOK_FILE,
|
|
296
|
+
hookNeedle,
|
|
297
|
+
hookPatch,
|
|
298
|
+
hookLegacyPatch
|
|
299
|
+
);
|
|
300
|
+
patchWithMigration(
|
|
301
|
+
UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE,
|
|
302
|
+
notificationNeedle,
|
|
303
|
+
notificationPatch,
|
|
304
|
+
notificationLegacyPatch
|
|
305
|
+
);
|
|
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) {
|
|
276
354
|
throw new Error(
|
|
277
|
-
`
|
|
355
|
+
`joplin demo: yarn buildParallel did not produce: ${stillMissing.join(", ")}`
|
|
278
356
|
);
|
|
279
357
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
287
391
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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;
|
|
293
399
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
);
|
|
304
444
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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;
|
|
318
478
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return {
|
|
322
|
-
checked: true,
|
|
323
|
-
updated: false,
|
|
324
|
-
reason: `manifest is missing version ${latestVersion}`,
|
|
325
|
-
activeVersion: readActiveRuntime(),
|
|
326
|
-
latestVersion
|
|
327
|
-
};
|
|
479
|
+
if (!env.WC_PROJECT_ID && process.env.RAINBOW_WALLETCONNECT_PROJECT_ID) {
|
|
480
|
+
env.WC_PROJECT_ID = process.env.RAINBOW_WALLETCONNECT_PROJECT_ID;
|
|
328
481
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
`);
|
|
341
499
|
}
|
|
342
|
-
const install = await installRuntime({
|
|
343
|
-
version: latestVersion,
|
|
344
|
-
channel,
|
|
345
|
-
cdnOrigin,
|
|
346
|
-
setActive: false
|
|
347
|
-
});
|
|
348
|
-
return {
|
|
349
|
-
checked: true,
|
|
350
|
-
updated: true,
|
|
351
|
-
activeVersion: latestVersion,
|
|
352
|
-
latestVersion,
|
|
353
|
-
install
|
|
354
|
-
};
|
|
355
500
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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);
|
|
360
512
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
await (0, import_promises.pipeline)(
|
|
365
|
-
import_stream.Readable.fromWeb(res.body),
|
|
366
|
-
import_fs2.default.createWriteStream(tmp)
|
|
367
|
-
);
|
|
368
|
-
import_fs2.default.renameSync(tmp, destPath);
|
|
369
|
-
} catch (err) {
|
|
370
|
-
try {
|
|
371
|
-
import_fs2.default.unlinkSync(tmp);
|
|
372
|
-
} catch {
|
|
373
|
-
}
|
|
374
|
-
throw err;
|
|
513
|
+
if (!(0, import_node_fs.existsSync)(RAINBOW_NETWORKS_FILE)) {
|
|
514
|
+
runRainbowSetupCommand("fnm exec --using=22 yarn fetch:networks", RAINBOW_DIR);
|
|
375
515
|
}
|
|
516
|
+
ensureRainbowDemoNetworks();
|
|
376
517
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
stream.on("end", () => resolve2(hash.digest("hex")));
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
function extractTarball(tarPath, destDir) {
|
|
387
|
-
return new Promise((resolve2, reject) => {
|
|
388
|
-
const child = (0, import_child_process.spawn)("tar", ["-xzf", tarPath, "-C", destDir], {
|
|
389
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
390
|
-
});
|
|
391
|
-
child.on("error", reject);
|
|
392
|
-
child.on("exit", (code) => {
|
|
393
|
-
if (code === 0) resolve2();
|
|
394
|
-
else reject(new Error(`tar exited with code ${code}`));
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// src/host/agent-host.ts
|
|
400
|
-
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
401
|
-
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
402
|
-
|
|
403
|
-
// scripts/dev-server-scanner.ts
|
|
404
|
-
var import_child_process2 = require("child_process");
|
|
405
|
-
var import_http = __toESM(require("http"), 1);
|
|
406
|
-
var import_net = __toESM(require("net"), 1);
|
|
407
|
-
var import_util = require("util");
|
|
408
|
-
|
|
409
|
-
// src/config.ts
|
|
410
|
-
var SOOTSIM_CONFIG_QUERY_PARAM = "sootsimConfig";
|
|
411
|
-
function hasOwnKeys(value) {
|
|
412
|
-
return !!value && Object.keys(value).length > 0;
|
|
413
|
-
}
|
|
414
|
-
function hasSootSimConfig(config) {
|
|
415
|
-
if (!config) return false;
|
|
416
|
-
return hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.env) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialState);
|
|
417
|
-
}
|
|
418
|
-
function applySootSimConfigToUrl(url, config) {
|
|
419
|
-
const parsed = new URL(url);
|
|
420
|
-
if (hasSootSimConfig(config)) {
|
|
421
|
-
parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config));
|
|
422
|
-
} else {
|
|
423
|
-
parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM);
|
|
424
|
-
}
|
|
425
|
-
return parsed.toString();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// src/native-dev-bundle-url.ts
|
|
429
|
-
function isAbsoluteHttpUrl(url) {
|
|
430
|
-
return /^https?:\/\//i.test(url);
|
|
431
|
-
}
|
|
432
|
-
function isNativeDevBundlePath(pathname) {
|
|
433
|
-
return pathname.endsWith(".bundle");
|
|
434
|
-
}
|
|
435
|
-
function normalizeNativeDevBundleUrl(bundleUrl) {
|
|
518
|
+
var ARTSY_DIR = (0, import_node_path.join)(HOME, "github/eigen");
|
|
519
|
+
var ARTSY_KEYS_FILE = (0, import_node_path.join)(ARTSY_DIR, "keys.shared.json");
|
|
520
|
+
var ARTSY_METAFLAGS_FILE = (0, import_node_path.join)(ARTSY_DIR, "metaflags.json");
|
|
521
|
+
var ARTSY_RELAY_SENTINEL = (0, import_node_path.join)(ARTSY_DIR, "src/__generated__/.relay-complete");
|
|
522
|
+
function readArtsyKeysPayload() {
|
|
523
|
+
if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE)) return void 0;
|
|
436
524
|
try {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
parsed.
|
|
440
|
-
|
|
441
|
-
if (!
|
|
442
|
-
|
|
443
|
-
parsed.searchParams.set("minify", "false");
|
|
444
|
-
}
|
|
445
|
-
parsed.searchParams.delete("transform.bytecode");
|
|
446
|
-
if (isAbsolute) return parsed.toString();
|
|
447
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
525
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(ARTSY_KEYS_FILE, "utf8"));
|
|
526
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
527
|
+
const secure = parsed.secure && typeof parsed.secure === "object" ? parsed.secure : void 0;
|
|
528
|
+
const publicKeys = parsed.public && typeof parsed.public === "object" ? parsed.public : void 0;
|
|
529
|
+
if (!secure && !publicKeys) return void 0;
|
|
530
|
+
return { secure, public: publicKeys };
|
|
448
531
|
} catch {
|
|
449
|
-
return
|
|
532
|
+
return void 0;
|
|
450
533
|
}
|
|
451
534
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
const packageJsonPath = (0, import_node_path.join)(dir, "package.json");
|
|
465
|
-
if ((0, import_node_fs.existsSync)(packageJsonPath)) {
|
|
466
|
-
try {
|
|
467
|
-
const pkg = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
|
|
468
|
-
if (pkg.workspaces) return dir;
|
|
469
|
-
} catch {
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
const parent = (0, import_node_path.dirname)(dir);
|
|
473
|
-
if (parent === dir) return null;
|
|
474
|
-
dir = parent;
|
|
535
|
+
function resolveArtsyRuntimeConfig() {
|
|
536
|
+
const email = process.env.SOOTSIM_ARTSY_EMAIL ?? process.env.MAESTRO_TEST_EMAIL;
|
|
537
|
+
const password = process.env.SOOTSIM_ARTSY_PASSWORD ?? process.env.MAESTRO_TEST_PASSWORD;
|
|
538
|
+
const keys = readArtsyKeysPayload();
|
|
539
|
+
const env = {};
|
|
540
|
+
if (email && password) {
|
|
541
|
+
env.SOOTSIM_LAUNCH_ARGUMENTS = JSON.stringify({
|
|
542
|
+
email,
|
|
543
|
+
password,
|
|
544
|
+
useMaestroInit: true
|
|
545
|
+
});
|
|
475
546
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
|
479
|
-
const candidates = [
|
|
480
|
-
workspaceRoot ? (0, import_node_path.resolve)(workspaceRoot, workspaceRelativePath) : null,
|
|
481
|
-
(0, import_node_path.resolve)(process.cwd(), workspaceRelativePath),
|
|
482
|
-
(0, import_node_path.resolve)(process.cwd(), packageRelativePath)
|
|
483
|
-
].filter((candidate) => Boolean(candidate));
|
|
484
|
-
for (const candidate of candidates) {
|
|
485
|
-
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
547
|
+
if (keys) {
|
|
548
|
+
env.SOOTSIM_REACT_NATIVE_KEYS_JSON = JSON.stringify(keys);
|
|
486
549
|
}
|
|
487
|
-
|
|
550
|
+
if (Object.keys(env).length === 0) return void 0;
|
|
551
|
+
return {
|
|
552
|
+
env
|
|
553
|
+
};
|
|
488
554
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
var UNISWAP_PLACEHOLDER = "stored-in-.env.local";
|
|
502
|
-
var UNISWAP_DEMO_ENV_MARKER = "# sootsim demo env overrides";
|
|
503
|
-
var UNISWAP_FORCE_UPGRADE_HOOK_FILE = (0, import_node_path.join)(
|
|
504
|
-
UNISWAP_REPO_DIR,
|
|
505
|
-
"packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts"
|
|
506
|
-
);
|
|
507
|
-
var UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE = (0, import_node_path.join)(
|
|
508
|
-
UNISWAP_REPO_DIR,
|
|
509
|
-
"apps/mobile/src/notification-service/data-sources/createForceUpgradeNotificationDataSource.ts"
|
|
510
|
-
);
|
|
511
|
-
var UNISWAP_FORCE_UPGRADE_PATCH_MARKER = "SOOTSIM_DEMO_DISABLE_FORCE_UPGRADE";
|
|
512
|
-
function parseEnvFile(filePath) {
|
|
513
|
-
if (!(0, import_node_fs.existsSync)(filePath)) return {};
|
|
514
|
-
const env = {};
|
|
515
|
-
const source = (0, import_node_fs.readFileSync)(filePath, "utf8");
|
|
516
|
-
for (const rawLine of source.split(/\r?\n/)) {
|
|
517
|
-
const line = rawLine.trim();
|
|
518
|
-
if (!line || line.startsWith("#")) continue;
|
|
519
|
-
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
520
|
-
if (!match) continue;
|
|
521
|
-
let value = match[2].trim();
|
|
522
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
523
|
-
value = value.slice(1, -1);
|
|
555
|
+
function ensureArtsySetup() {
|
|
556
|
+
if (!(0, import_node_fs.existsSync)(ARTSY_DIR)) return;
|
|
557
|
+
const { execSync } = require("node:child_process");
|
|
558
|
+
const yarnRelease = (0, import_node_path.join)(ARTSY_DIR, ".yarn/releases/yarn-4.10.3.cjs");
|
|
559
|
+
if ((0, import_node_fs.existsSync)(yarnRelease)) {
|
|
560
|
+
const src = (0, import_node_fs.readFileSync)(yarnRelease, "utf8");
|
|
561
|
+
const needle = '["clone","-c core.autocrlf=false",';
|
|
562
|
+
if (src.includes(needle)) {
|
|
563
|
+
(0, import_node_fs.writeFileSync)(
|
|
564
|
+
yarnRelease,
|
|
565
|
+
src.replace(needle, '["clone","-c","core.autocrlf=false",')
|
|
566
|
+
);
|
|
524
567
|
}
|
|
525
|
-
env[match[1]] = value;
|
|
526
568
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (isUsableUniswapEnvValue(value)) return value.trim();
|
|
569
|
+
if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(ARTSY_DIR, "node_modules/.yarn-state.yml"))) {
|
|
570
|
+
execSync("yarn install", {
|
|
571
|
+
cwd: ARTSY_DIR,
|
|
572
|
+
stdio: "inherit",
|
|
573
|
+
env: { ...process.env, YARN_CHECKSUM_BEHAVIOR: "update" }
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE) || !(0, import_node_fs.existsSync)(ARTSY_METAFLAGS_FILE)) {
|
|
577
|
+
try {
|
|
578
|
+
execSync("yarn setup:oss", { cwd: ARTSY_DIR, stdio: "inherit" });
|
|
579
|
+
} catch {
|
|
580
|
+
if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE) || !(0, import_node_fs.existsSync)(ARTSY_METAFLAGS_FILE)) {
|
|
581
|
+
throw new Error("artsy demo: setup:oss did not create keys/metaflags");
|
|
582
|
+
}
|
|
542
583
|
}
|
|
543
584
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
webEnv
|
|
553
|
-
];
|
|
554
|
-
const env = {};
|
|
555
|
-
const bindings = [
|
|
556
|
-
["AMPLITUDE_PROXY_URL_OVERRIDE", ["REACT_APP_AMPLITUDE_PROXY_URL"]],
|
|
557
|
-
["QUICKNODE_ENDPOINT_NAME", ["REACT_APP_QUICKNODE_ENDPOINT_NAME"]],
|
|
558
|
-
["QUICKNODE_ENDPOINT_TOKEN", ["REACT_APP_QUICKNODE_ENDPOINT_TOKEN"]],
|
|
559
|
-
["INFURA_KEY", ["REACT_APP_INFURA_KEY"]],
|
|
560
|
-
["STATSIG_API_KEY", ["REACT_APP_STATSIG_API_KEY"]],
|
|
561
|
-
["STATSIG_PROXY_URL_OVERRIDE", ["REACT_APP_STATSIG_PROXY_URL"]],
|
|
562
|
-
["WALLETCONNECT_PROJECT_ID", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
|
|
563
|
-
["WALLETCONNECT_PROJECT_ID_BETA", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
|
|
564
|
-
["WALLETCONNECT_PROJECT_ID_DEV", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
|
|
565
|
-
["TRADING_API_KEY", ["REACT_APP_TRADING_API_KEY"]],
|
|
566
|
-
["UNISWAP_API_KEY", []]
|
|
567
|
-
];
|
|
568
|
-
for (const [target, aliases] of bindings) {
|
|
569
|
-
const value = pickEnvValue(sources, [target, ...aliases]);
|
|
570
|
-
if (!value) continue;
|
|
571
|
-
env[target] = value;
|
|
572
|
-
for (const alias of aliases) {
|
|
573
|
-
env[alias] = value;
|
|
585
|
+
const rnlaPkgJson = (0, import_node_path.join)(
|
|
586
|
+
ARTSY_DIR,
|
|
587
|
+
"node_modules/react-native-launch-arguments/package.json"
|
|
588
|
+
);
|
|
589
|
+
if ((0, import_node_fs.existsSync)(rnlaPkgJson)) {
|
|
590
|
+
const raw = (0, import_node_fs.readFileSync)(rnlaPkgJson, "utf8");
|
|
591
|
+
if (raw.includes('"dist/index.js"')) {
|
|
592
|
+
(0, import_node_fs.writeFileSync)(rnlaPkgJson, raw.replace('"dist/index.js"', '"src/index.ts"'));
|
|
574
593
|
}
|
|
575
594
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
env.API_BASE_URL_OVERRIDE = "https://interface.gateway.uniswap.org";
|
|
580
|
-
env.API_BASE_URL_V2_OVERRIDE = "https://interface.gateway.uniswap.org/v2";
|
|
581
|
-
env.GRAPHQL_URL_OVERRIDE = publicGraphqlUrl;
|
|
582
|
-
env.TRADING_API_URL_OVERRIDE = "https://trading-api-labs.interface.gateway.uniswap.org";
|
|
583
|
-
env.FOR_API_URL_OVERRIDE = "https://for.interface.gateway.uniswap.org/v2/FOR.v1.FORService";
|
|
584
|
-
}
|
|
585
|
-
return env;
|
|
586
|
-
}
|
|
587
|
-
function ensureUniswapDemoEnvLocal() {
|
|
588
|
-
const existingSource = (0, import_node_fs.existsSync)(UNISWAP_ENV_LOCAL_FILE) ? (0, import_node_fs.readFileSync)(UNISWAP_ENV_LOCAL_FILE, "utf8") : "";
|
|
589
|
-
if (existingSource && !existingSource.includes(UNISWAP_DEMO_ENV_MARKER)) {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const env = resolveUniswapDemoEnv();
|
|
593
|
-
const lines = [UNISWAP_DEMO_ENV_MARKER];
|
|
594
|
-
for (const [key, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) {
|
|
595
|
-
lines.push(`${key}=${JSON.stringify(value)}`);
|
|
596
|
-
}
|
|
597
|
-
lines.push("");
|
|
598
|
-
(0, import_node_fs.writeFileSync)(UNISWAP_ENV_LOCAL_FILE, `${lines.join("\n")}
|
|
599
|
-
`);
|
|
600
|
-
}
|
|
601
|
-
function ensureUniswapForceUpgradePatched() {
|
|
602
|
-
const hookNeedle = `export function useForceUpgradeStatus(): ForceUpgradeStatus {
|
|
603
|
-
`;
|
|
604
|
-
const hookPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
605
|
-
return 'not-required'
|
|
606
|
-
|
|
607
|
-
`;
|
|
608
|
-
const hookLegacyPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
609
|
-
if (process.env.${UNISWAP_FORCE_UPGRADE_PATCH_MARKER} === 'true') {
|
|
610
|
-
return 'not-required'
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
`;
|
|
614
|
-
const notificationNeedle = ` const getForceUpgradeStatus = (): ForceUpgradeStatus => {
|
|
615
|
-
`;
|
|
616
|
-
const notificationPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
617
|
-
return 'not-required'
|
|
618
|
-
|
|
619
|
-
`;
|
|
620
|
-
const notificationLegacyPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
|
|
621
|
-
if (process.env.${UNISWAP_FORCE_UPGRADE_PATCH_MARKER} === 'true') {
|
|
622
|
-
return 'not-required'
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
`;
|
|
626
|
-
const patchWithMigration = (filePath, needle, patch, legacyPatch) => {
|
|
627
|
-
const source = (0, import_node_fs.readFileSync)(filePath, "utf8");
|
|
628
|
-
if (source.includes(patch)) return;
|
|
629
|
-
if (source.includes(legacyPatch)) {
|
|
630
|
-
(0, import_node_fs.writeFileSync)(filePath, source.replace(legacyPatch, patch));
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
if (!source.includes(needle)) {
|
|
634
|
-
throw new Error(
|
|
635
|
-
`uniswap demo patch failed: expected snippet not found in ${filePath}`
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
(0, import_node_fs.writeFileSync)(filePath, source.replace(needle, `${needle}${patch}`));
|
|
639
|
-
};
|
|
640
|
-
patchWithMigration(
|
|
641
|
-
UNISWAP_FORCE_UPGRADE_HOOK_FILE,
|
|
642
|
-
hookNeedle,
|
|
643
|
-
hookPatch,
|
|
644
|
-
hookLegacyPatch
|
|
645
|
-
);
|
|
646
|
-
patchWithMigration(
|
|
647
|
-
UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE,
|
|
648
|
-
notificationNeedle,
|
|
649
|
-
notificationPatch,
|
|
650
|
-
notificationLegacyPatch
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
var ARTSY_DIR = (0, import_node_path.join)(HOME, "github/eigen");
|
|
654
|
-
var ARTSY_KEYS_FILE = (0, import_node_path.join)(ARTSY_DIR, "keys.shared.json");
|
|
655
|
-
var ARTSY_METAFLAGS_FILE = (0, import_node_path.join)(ARTSY_DIR, "metaflags.json");
|
|
656
|
-
var ARTSY_RELAY_SENTINEL = (0, import_node_path.join)(ARTSY_DIR, "src/__generated__/.relay-complete");
|
|
657
|
-
function readArtsyKeysPayload() {
|
|
658
|
-
if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE)) return void 0;
|
|
659
|
-
try {
|
|
660
|
-
const parsed = JSON.parse((0, import_node_fs.readFileSync)(ARTSY_KEYS_FILE, "utf8"));
|
|
661
|
-
if (!parsed || typeof parsed !== "object") return void 0;
|
|
662
|
-
const secure = parsed.secure && typeof parsed.secure === "object" ? parsed.secure : void 0;
|
|
663
|
-
const publicKeys = parsed.public && typeof parsed.public === "object" ? parsed.public : void 0;
|
|
664
|
-
if (!secure && !publicKeys) return void 0;
|
|
665
|
-
return { secure, public: publicKeys };
|
|
666
|
-
} catch {
|
|
667
|
-
return void 0;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
function resolveArtsyRuntimeConfig() {
|
|
671
|
-
const email = process.env.SOOTSIM_ARTSY_EMAIL ?? process.env.MAESTRO_TEST_EMAIL;
|
|
672
|
-
const password = process.env.SOOTSIM_ARTSY_PASSWORD ?? process.env.MAESTRO_TEST_PASSWORD;
|
|
673
|
-
const keys = readArtsyKeysPayload();
|
|
674
|
-
const env = {};
|
|
675
|
-
if (email && password) {
|
|
676
|
-
env.SOOTSIM_LAUNCH_ARGUMENTS = JSON.stringify({
|
|
677
|
-
email,
|
|
678
|
-
password,
|
|
679
|
-
useMaestroInit: true
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
if (keys) {
|
|
683
|
-
env.SOOTSIM_REACT_NATIVE_KEYS_JSON = JSON.stringify(keys);
|
|
684
|
-
}
|
|
685
|
-
if (Object.keys(env).length === 0) return void 0;
|
|
686
|
-
return {
|
|
687
|
-
env
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
function ensureArtsySetup() {
|
|
691
|
-
if (!(0, import_node_fs.existsSync)(ARTSY_DIR)) return;
|
|
692
|
-
const { execSync } = require("node:child_process");
|
|
693
|
-
const yarnRelease = (0, import_node_path.join)(ARTSY_DIR, ".yarn/releases/yarn-4.10.3.cjs");
|
|
694
|
-
if ((0, import_node_fs.existsSync)(yarnRelease)) {
|
|
695
|
-
const src = (0, import_node_fs.readFileSync)(yarnRelease, "utf8");
|
|
696
|
-
const needle = '["clone","-c core.autocrlf=false",';
|
|
697
|
-
if (src.includes(needle)) {
|
|
698
|
-
(0, import_node_fs.writeFileSync)(
|
|
699
|
-
yarnRelease,
|
|
700
|
-
src.replace(needle, '["clone","-c","core.autocrlf=false",')
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(ARTSY_DIR, "node_modules/.yarn-state.yml"))) {
|
|
705
|
-
execSync("yarn install", {
|
|
706
|
-
cwd: ARTSY_DIR,
|
|
707
|
-
stdio: "inherit",
|
|
708
|
-
env: { ...process.env, YARN_CHECKSUM_BEHAVIOR: "update" }
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE) || !(0, import_node_fs.existsSync)(ARTSY_METAFLAGS_FILE)) {
|
|
712
|
-
try {
|
|
713
|
-
execSync("yarn setup:oss", { cwd: ARTSY_DIR, stdio: "inherit" });
|
|
714
|
-
} catch {
|
|
715
|
-
if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE) || !(0, import_node_fs.existsSync)(ARTSY_METAFLAGS_FILE)) {
|
|
716
|
-
throw new Error("artsy demo: setup:oss did not create keys/metaflags");
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
const rnlaPkgJson = (0, import_node_path.join)(
|
|
721
|
-
ARTSY_DIR,
|
|
722
|
-
"node_modules/react-native-launch-arguments/package.json"
|
|
723
|
-
);
|
|
724
|
-
if ((0, import_node_fs.existsSync)(rnlaPkgJson)) {
|
|
725
|
-
const raw = (0, import_node_fs.readFileSync)(rnlaPkgJson, "utf8");
|
|
726
|
-
if (raw.includes('"dist/index.js"')) {
|
|
727
|
-
(0, import_node_fs.writeFileSync)(rnlaPkgJson, raw.replace('"dist/index.js"', '"src/index.ts"'));
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
if (!(0, import_node_fs.existsSync)(ARTSY_RELAY_SENTINEL)) {
|
|
731
|
-
execSync("yarn relay", { cwd: ARTSY_DIR, stdio: "inherit" });
|
|
732
|
-
(0, import_node_fs.writeFileSync)(ARTSY_RELAY_SENTINEL, "");
|
|
595
|
+
if (!(0, import_node_fs.existsSync)(ARTSY_RELAY_SENTINEL)) {
|
|
596
|
+
execSync("yarn relay", { cwd: ARTSY_DIR, stdio: "inherit" });
|
|
597
|
+
(0, import_node_fs.writeFileSync)(ARTSY_RELAY_SENTINEL, "");
|
|
733
598
|
}
|
|
734
599
|
}
|
|
735
600
|
var APPS = [
|
|
@@ -776,7 +641,24 @@ var APPS = [
|
|
|
776
641
|
dir: (0, import_node_path.join)(HOME, "takeout"),
|
|
777
642
|
preferredPort: 8086,
|
|
778
643
|
framework: "one",
|
|
779
|
-
|
|
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
|
+
})
|
|
780
662
|
},
|
|
781
663
|
{
|
|
782
664
|
name: "expensify",
|
|
@@ -823,361 +705,907 @@ var APPS = [
|
|
|
823
705
|
envVars: ["SOOTSIM_ARTSY_EMAIL", "SOOTSIM_ARTSY_PASSWORD"],
|
|
824
706
|
note: "auto-login reuses Artsy\u2019s built-in Maestro launch-arguments hook"
|
|
825
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
|
+
}
|
|
853
|
+
}
|
|
854
|
+
];
|
|
855
|
+
|
|
856
|
+
// scripts/dev-server-scanner.ts
|
|
857
|
+
var execP = (0, import_util.promisify)(import_child_process.exec);
|
|
858
|
+
var TIMEOUT_MS = 250;
|
|
859
|
+
var TCP_GATE_MS = 120;
|
|
860
|
+
function tcpPing(port, timeout = TCP_GATE_MS) {
|
|
861
|
+
return new Promise((resolve2) => {
|
|
862
|
+
const sock = new import_net.default.Socket();
|
|
863
|
+
let settled = false;
|
|
864
|
+
const done = (ok) => {
|
|
865
|
+
if (settled) return;
|
|
866
|
+
settled = true;
|
|
867
|
+
sock.destroy();
|
|
868
|
+
resolve2(ok);
|
|
869
|
+
};
|
|
870
|
+
sock.setTimeout(timeout);
|
|
871
|
+
sock.once("connect", () => done(true));
|
|
872
|
+
sock.once("timeout", () => done(false));
|
|
873
|
+
sock.once("error", () => done(false));
|
|
874
|
+
sock.connect(port, "localhost");
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function httpGet(port, path7, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
|
|
878
|
+
return new Promise((resolve2) => {
|
|
879
|
+
const req = import_http.default.request(
|
|
880
|
+
{ hostname: "localhost", port, path: path7, method, timeout, headers },
|
|
881
|
+
(res) => {
|
|
882
|
+
let body = "";
|
|
883
|
+
res.on("data", (c) => body += c.toString());
|
|
884
|
+
res.on("end", () => resolve2({ statusCode: res.statusCode || 0, body }));
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
req.on("error", () => resolve2(null));
|
|
888
|
+
req.setTimeout(timeout, () => {
|
|
889
|
+
req.destroy();
|
|
890
|
+
resolve2(null);
|
|
891
|
+
});
|
|
892
|
+
req.end();
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
var FALLBACK_PORTS = [
|
|
896
|
+
8081,
|
|
897
|
+
8082,
|
|
898
|
+
8083,
|
|
899
|
+
8084,
|
|
900
|
+
8085,
|
|
901
|
+
8086,
|
|
902
|
+
3e3,
|
|
903
|
+
3001,
|
|
904
|
+
19e3
|
|
905
|
+
].map((port) => ({ port, pid: 0 }));
|
|
906
|
+
function acceptPort(port, excluded) {
|
|
907
|
+
if (port <= 0 || port >= 2e4) return false;
|
|
908
|
+
if (excluded.has(port)) return false;
|
|
909
|
+
if (port >= 5170 && port <= 5200) return false;
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
async function discoverListeningProcesses(excludePorts = []) {
|
|
913
|
+
const excluded = new Set(excludePorts);
|
|
914
|
+
try {
|
|
915
|
+
const { stdout } = await execP(
|
|
916
|
+
`lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun)'`,
|
|
917
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
918
|
+
);
|
|
919
|
+
if (stdout.trim()) {
|
|
920
|
+
const seen = /* @__PURE__ */ new Map();
|
|
921
|
+
for (const line of stdout.trim().split("\n")) {
|
|
922
|
+
const parts = line.trim().split(/\s+/);
|
|
923
|
+
if (parts.length < 9) continue;
|
|
924
|
+
const pid = Number(parts[1]);
|
|
925
|
+
const addr = parts[8];
|
|
926
|
+
const m = addr.match(/:(\d+)$/);
|
|
927
|
+
if (!m) continue;
|
|
928
|
+
const port = Number(m[1]);
|
|
929
|
+
if (!acceptPort(port, excluded)) continue;
|
|
930
|
+
if (!seen.has(port)) seen.set(port, pid);
|
|
931
|
+
}
|
|
932
|
+
if (seen.size > 0) {
|
|
933
|
+
return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
const { stdout } = await execP(`ss -tlnp 2>/dev/null | grep -E '"(node|bun)"'`, {
|
|
940
|
+
encoding: "utf8",
|
|
941
|
+
timeout: 2e3
|
|
942
|
+
});
|
|
943
|
+
if (stdout.trim()) {
|
|
944
|
+
const seen = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const line of stdout.trim().split("\n")) {
|
|
946
|
+
const portMatch = line.match(/:(\d+)\s/);
|
|
947
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
948
|
+
if (!portMatch) continue;
|
|
949
|
+
const port = Number(portMatch[1]);
|
|
950
|
+
const pid = pidMatch ? Number(pidMatch[1]) : 0;
|
|
951
|
+
if (!acceptPort(port, excluded)) continue;
|
|
952
|
+
if (!seen.has(port)) seen.set(port, pid);
|
|
953
|
+
}
|
|
954
|
+
if (seen.size > 0) {
|
|
955
|
+
return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
} catch {
|
|
959
|
+
}
|
|
960
|
+
return FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded));
|
|
961
|
+
}
|
|
962
|
+
var cwdByPid = /* @__PURE__ */ new Map();
|
|
963
|
+
async function resolveProcessCwd(pid) {
|
|
964
|
+
if (pid <= 0) return null;
|
|
965
|
+
const cached = cwdByPid.get(pid);
|
|
966
|
+
if (cached) return cached;
|
|
967
|
+
try {
|
|
968
|
+
const { stdout } = await execP(`lsof -p ${pid} -a -d cwd -Fn 2>/dev/null`, {
|
|
969
|
+
encoding: "utf8",
|
|
970
|
+
timeout: 1500
|
|
971
|
+
});
|
|
972
|
+
for (const line of stdout.split("\n")) {
|
|
973
|
+
if (line.startsWith("n") && line.length > 1) {
|
|
974
|
+
const cwd = line.slice(1).trim();
|
|
975
|
+
if (cwd) {
|
|
976
|
+
cwdByPid.set(pid, cwd);
|
|
977
|
+
return cwd;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
function makeResult(port, framework) {
|
|
986
|
+
return {
|
|
987
|
+
port,
|
|
988
|
+
framework,
|
|
989
|
+
bundleUrl: withRuntimeConfig(
|
|
990
|
+
port,
|
|
991
|
+
`http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
|
|
992
|
+
),
|
|
993
|
+
hmrUrl: `ws://localhost:${port}/hot`,
|
|
994
|
+
lastSeen: Date.now()
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function withRuntimeConfig(port, bundleUrl) {
|
|
998
|
+
const knownApp = APPS.find((app) => app.preferredPort === port);
|
|
999
|
+
const configured = knownApp?.runtimeConfig ? applySootSimConfigToUrl(bundleUrl, knownApp.runtimeConfig) : bundleUrl;
|
|
1000
|
+
return normalizeNativeDevBundleUrl(configured);
|
|
1001
|
+
}
|
|
1002
|
+
function isDirectOneBundleUrl(bundleUrl) {
|
|
1003
|
+
return bundleUrl.includes("/node_modules/one/metro-entry.bundle");
|
|
1004
|
+
}
|
|
1005
|
+
function applyManifest(result, manifestRes, buildIconProxyUrl) {
|
|
1006
|
+
if (!manifestRes) return result;
|
|
1007
|
+
try {
|
|
1008
|
+
const manifest = JSON.parse(manifestRes.body);
|
|
1009
|
+
const client = manifest?.extra?.expoClient || manifest?.extra || {};
|
|
1010
|
+
if (client.name) result.projectName = client.name;
|
|
1011
|
+
if (client.ios?.bundleIdentifier) result.bundleId = client.ios.bundleIdentifier;
|
|
1012
|
+
if (result.framework === "metro" && client.sdkVersion) result.framework = "expo";
|
|
1013
|
+
const launchUrl2 = manifest?.launchAsset?.url;
|
|
1014
|
+
if (launchUrl2 && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
|
|
1015
|
+
result.bundleUrl = withRuntimeConfig(result.port, launchUrl2);
|
|
1016
|
+
}
|
|
1017
|
+
const rawIconUrl = client.iconUrl || client.ios?.iconUrl || client.icon || client.ios?.icon;
|
|
1018
|
+
if (rawIconUrl) {
|
|
1019
|
+
result.iconPath = rawIconUrl;
|
|
1020
|
+
if (buildIconProxyUrl) {
|
|
1021
|
+
if (rawIconUrl.startsWith("http")) {
|
|
1022
|
+
result.iconUrl = buildIconProxyUrl(rawIconUrl);
|
|
1023
|
+
} else {
|
|
1024
|
+
const cleanPath = rawIconUrl.replace(/^\.\//, "");
|
|
1025
|
+
result.iconUrl = buildIconProxyUrl(
|
|
1026
|
+
`http://localhost:${result.port}/assets/${cleanPath}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
result.iconUrl = rawIconUrl.startsWith("http") ? rawIconUrl : `http://localhost:${result.port}/assets/${rawIconUrl.replace(/^\.\//, "")}`;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
return result;
|
|
1036
|
+
}
|
|
1037
|
+
var knownNonPatched = /* @__PURE__ */ new Set();
|
|
1038
|
+
var knownNonExpo = /* @__PURE__ */ new Set();
|
|
1039
|
+
var knownOne = /* @__PURE__ */ new Set();
|
|
1040
|
+
async function probePort(port, buildIconProxyUrl) {
|
|
1041
|
+
if (!await tcpPing(port)) return null;
|
|
1042
|
+
const onePath = `/node_modules/one/metro-entry.bundle?platform=ios&dev=true`;
|
|
1043
|
+
const [sootsimRes, statusRes, oneRes, manifestRes, expoRes] = await Promise.all([
|
|
1044
|
+
knownNonPatched.has(port) ? Promise.resolve(null) : httpGet(port, "/__soot/"),
|
|
1045
|
+
httpGet(port, "/status"),
|
|
1046
|
+
httpGet(port, onePath, "HEAD"),
|
|
1047
|
+
knownOne.has(port) ? Promise.resolve(null) : httpGet(port, "/", "GET", TIMEOUT_MS, { "expo-platform": "ios" }),
|
|
1048
|
+
knownNonExpo.has(port) ? Promise.resolve(null) : httpGet(port, "/_expo/status")
|
|
1049
|
+
]);
|
|
1050
|
+
if (expoRes && expoRes.statusCode === 200) {
|
|
1051
|
+
knownNonExpo.delete(port);
|
|
1052
|
+
} else if (!knownNonExpo.has(port)) {
|
|
1053
|
+
knownNonExpo.add(port);
|
|
1054
|
+
}
|
|
1055
|
+
if (oneRes && oneRes.statusCode > 0 && oneRes.statusCode < 400) {
|
|
1056
|
+
knownNonPatched.add(port);
|
|
1057
|
+
knownOne.add(port);
|
|
1058
|
+
return applyManifest(
|
|
1059
|
+
{
|
|
1060
|
+
port,
|
|
1061
|
+
framework: "one",
|
|
1062
|
+
bundleUrl: withRuntimeConfig(
|
|
1063
|
+
port,
|
|
1064
|
+
`http://localhost:${port}${onePath}&minify=false`
|
|
1065
|
+
),
|
|
1066
|
+
hmrUrl: `ws://localhost:${port}/hot`,
|
|
1067
|
+
lastSeen: Date.now()
|
|
1068
|
+
},
|
|
1069
|
+
manifestRes,
|
|
1070
|
+
buildIconProxyUrl
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (statusRes && statusRes.body.includes("packager-status:running")) {
|
|
1074
|
+
knownNonPatched.add(port);
|
|
1075
|
+
return applyManifest(
|
|
1076
|
+
makeResult(port, expoRes && expoRes.statusCode === 200 ? "expo" : "metro"),
|
|
1077
|
+
manifestRes,
|
|
1078
|
+
buildIconProxyUrl
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (manifestRes) {
|
|
1082
|
+
try {
|
|
1083
|
+
const manifest = JSON.parse(manifestRes.body);
|
|
1084
|
+
const client = manifest?.extra?.expoClient || {};
|
|
1085
|
+
if (client.name) {
|
|
1086
|
+
const launchUrl2 = manifest?.launchAsset?.url || `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`;
|
|
1087
|
+
knownNonPatched.add(port);
|
|
1088
|
+
return applyManifest(
|
|
1089
|
+
{
|
|
1090
|
+
port,
|
|
1091
|
+
framework: "one",
|
|
1092
|
+
bundleUrl: withRuntimeConfig(port, launchUrl2),
|
|
1093
|
+
hmrUrl: `ws://localhost:${port}/hot`,
|
|
1094
|
+
lastSeen: Date.now()
|
|
1095
|
+
},
|
|
1096
|
+
manifestRes,
|
|
1097
|
+
buildIconProxyUrl
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (sootsimRes && sootsimRes.statusCode === 200 && sootsimRes.body.includes("sootsim-patched")) {
|
|
1104
|
+
knownNonPatched.delete(port);
|
|
1105
|
+
return applyManifest(
|
|
1106
|
+
{
|
|
1107
|
+
port,
|
|
1108
|
+
framework: "one",
|
|
1109
|
+
bundleUrl: withRuntimeConfig(port, `http://localhost:${port}/__soot/bundle.js`),
|
|
1110
|
+
hmrUrl: `ws://localhost:${port}/hot`,
|
|
1111
|
+
lastSeen: Date.now(),
|
|
1112
|
+
patched: true
|
|
1113
|
+
},
|
|
1114
|
+
manifestRes,
|
|
1115
|
+
buildIconProxyUrl
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
knownNonPatched.add(port);
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
function isSootSelfServer(server) {
|
|
1122
|
+
const projectName = server.projectName?.trim().toLowerCase();
|
|
1123
|
+
if (projectName === "soot" || projectName === "sootsim") return true;
|
|
1124
|
+
const bundleId = server.bundleId?.trim().toLowerCase();
|
|
1125
|
+
if (bundleId?.startsWith("dev.soot")) return true;
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
var portCache = /* @__PURE__ */ new Map();
|
|
1129
|
+
var NEGATIVE_CACHE_TTL_MS = 3e4;
|
|
1130
|
+
var WEAK_RESULT_CACHE_TTL_MS = 1500;
|
|
1131
|
+
function isWeakCachedResult(result) {
|
|
1132
|
+
if (!result) return true;
|
|
1133
|
+
if (result.framework === "metro" || result.framework === "unknown") return true;
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
function hasCurrentRuntimeConfig(result) {
|
|
1137
|
+
if (!result) return true;
|
|
1138
|
+
return withRuntimeConfig(result.port, result.bundleUrl) === result.bundleUrl;
|
|
1139
|
+
}
|
|
1140
|
+
function __shouldReuseScannerCacheEntry(entry, pid, now = Date.now()) {
|
|
1141
|
+
if (pid === 0) return false;
|
|
1142
|
+
if (entry.pid !== pid) return false;
|
|
1143
|
+
if (!hasCurrentRuntimeConfig(entry.result)) return false;
|
|
1144
|
+
const ageMs = now - entry.cachedAt;
|
|
1145
|
+
if (entry.result === null && ageMs >= NEGATIVE_CACHE_TTL_MS) return false;
|
|
1146
|
+
if (isWeakCachedResult(entry.result) && ageMs >= WEAK_RESULT_CACHE_TTL_MS) return false;
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
async function scanDevServers(opts = {}) {
|
|
1150
|
+
const processes = await discoverListeningProcesses(opts.excludePorts);
|
|
1151
|
+
const currentPorts = new Set(processes.map((p) => p.port));
|
|
1152
|
+
for (const p of [...portCache.keys()]) {
|
|
1153
|
+
if (!currentPorts.has(p)) portCache.delete(p);
|
|
1154
|
+
}
|
|
1155
|
+
for (const p of [...knownNonPatched]) {
|
|
1156
|
+
if (!currentPorts.has(p)) knownNonPatched.delete(p);
|
|
1157
|
+
}
|
|
1158
|
+
for (const p of [...knownNonExpo]) {
|
|
1159
|
+
if (!currentPorts.has(p)) knownNonExpo.delete(p);
|
|
1160
|
+
}
|
|
1161
|
+
for (const p of [...knownOne]) {
|
|
1162
|
+
if (!currentPorts.has(p)) knownOne.delete(p);
|
|
1163
|
+
}
|
|
1164
|
+
const results = [];
|
|
1165
|
+
const toProbe = [];
|
|
1166
|
+
for (const { port, pid } of processes) {
|
|
1167
|
+
const cached = portCache.get(port);
|
|
1168
|
+
if (cached && __shouldReuseScannerCacheEntry(cached, pid)) {
|
|
1169
|
+
if (cached.result) results.push(cached.result);
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (cached && cached.pid !== pid) {
|
|
1173
|
+
knownNonPatched.delete(port);
|
|
1174
|
+
knownNonExpo.delete(port);
|
|
1175
|
+
knownOne.delete(port);
|
|
1176
|
+
}
|
|
1177
|
+
toProbe.push({ port, pid });
|
|
1178
|
+
}
|
|
1179
|
+
if (toProbe.length > 0) {
|
|
1180
|
+
const probed = await Promise.all(
|
|
1181
|
+
toProbe.map((p) => probePort(p.port, opts.buildIconProxyUrl))
|
|
1182
|
+
);
|
|
1183
|
+
probed.forEach((result, i) => {
|
|
1184
|
+
const { port, pid } = toProbe[i];
|
|
1185
|
+
if (pid !== 0) portCache.set(port, { pid, result, cachedAt: Date.now() });
|
|
1186
|
+
if (result) results.push(result);
|
|
1187
|
+
});
|
|
826
1188
|
}
|
|
827
|
-
|
|
1189
|
+
const pidByPort = /* @__PURE__ */ new Map();
|
|
1190
|
+
for (const { port, pid } of processes) {
|
|
1191
|
+
if (pid > 0) pidByPort.set(port, pid);
|
|
1192
|
+
}
|
|
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
|
+
}
|
|
828
1208
|
|
|
829
|
-
//
|
|
830
|
-
var
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
|
842
1270
|
};
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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;
|
|
849
1277
|
}
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
res.on("data", (c) => body += c.toString());
|
|
857
|
-
res.on("end", () => resolve2({ statusCode: res.statusCode || 0, body }));
|
|
858
|
-
}
|
|
859
|
-
);
|
|
860
|
-
req.on("error", () => resolve2(null));
|
|
861
|
-
req.setTimeout(timeout, () => {
|
|
862
|
-
req.destroy();
|
|
863
|
-
resolve2(null);
|
|
864
|
-
});
|
|
865
|
-
req.end();
|
|
866
|
-
});
|
|
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 });
|
|
867
1284
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
3e3,
|
|
876
|
-
3001,
|
|
877
|
-
19e3
|
|
878
|
-
].map((port) => ({ port, pid: 0 }));
|
|
879
|
-
function acceptPort(port, excluded) {
|
|
880
|
-
if (port <= 0 || port >= 2e4) return false;
|
|
881
|
-
if (excluded.has(port)) return false;
|
|
882
|
-
if (port >= 5170 && port <= 5200) return false;
|
|
883
|
-
return true;
|
|
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
|
+
}
|
|
884
1292
|
}
|
|
885
|
-
|
|
886
|
-
|
|
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() {
|
|
887
1299
|
try {
|
|
888
|
-
|
|
889
|
-
`lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun)'`,
|
|
890
|
-
{ encoding: "utf8", timeout: 2e3 }
|
|
891
|
-
);
|
|
892
|
-
if (stdout.trim()) {
|
|
893
|
-
const seen = /* @__PURE__ */ new Map();
|
|
894
|
-
for (const line of stdout.trim().split("\n")) {
|
|
895
|
-
const parts = line.trim().split(/\s+/);
|
|
896
|
-
if (parts.length < 9) continue;
|
|
897
|
-
const pid = Number(parts[1]);
|
|
898
|
-
const addr = parts[8];
|
|
899
|
-
const m = addr.match(/:(\d+)$/);
|
|
900
|
-
if (!m) continue;
|
|
901
|
-
const port = Number(m[1]);
|
|
902
|
-
if (!acceptPort(port, excluded)) continue;
|
|
903
|
-
if (!seen.has(port)) seen.set(port, pid);
|
|
904
|
-
}
|
|
905
|
-
if (seen.size > 0) {
|
|
906
|
-
return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
1300
|
+
return import_node_fs2.default.readdirSync(runtimesDir(), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort(compareSemver);
|
|
909
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;
|
|
910
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);
|
|
911
1330
|
try {
|
|
912
|
-
|
|
913
|
-
encoding: "utf8",
|
|
914
|
-
timeout: 2e3
|
|
915
|
-
});
|
|
916
|
-
if (stdout.trim()) {
|
|
917
|
-
const seen = /* @__PURE__ */ new Map();
|
|
918
|
-
for (const line of stdout.trim().split("\n")) {
|
|
919
|
-
const portMatch = line.match(/:(\d+)\s/);
|
|
920
|
-
const pidMatch = line.match(/pid=(\d+)/);
|
|
921
|
-
if (!portMatch) continue;
|
|
922
|
-
const port = Number(portMatch[1]);
|
|
923
|
-
const pid = pidMatch ? Number(pidMatch[1]) : 0;
|
|
924
|
-
if (!acceptPort(port, excluded)) continue;
|
|
925
|
-
if (!seen.has(port)) seen.set(port, pid);
|
|
926
|
-
}
|
|
927
|
-
if (seen.size > 0) {
|
|
928
|
-
return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
|
|
929
|
-
}
|
|
930
|
-
}
|
|
1331
|
+
if (import_node_fs2.default.statSync(dir).isDirectory()) return dir;
|
|
931
1332
|
} catch {
|
|
932
1333
|
}
|
|
933
|
-
return
|
|
1334
|
+
return null;
|
|
934
1335
|
}
|
|
935
|
-
var
|
|
936
|
-
|
|
937
|
-
if (pid <= 0) return null;
|
|
938
|
-
const cached = cwdByPid.get(pid);
|
|
939
|
-
if (cached) return cached;
|
|
1336
|
+
var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
|
|
1337
|
+
function readDaemonLockfile() {
|
|
940
1338
|
try {
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
cwdByPid.set(pid, cwd);
|
|
950
|
-
return cwd;
|
|
951
|
-
}
|
|
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;
|
|
952
1347
|
}
|
|
1348
|
+
return null;
|
|
1349
|
+
} finally {
|
|
1350
|
+
import_node_fs2.default.closeSync(fd);
|
|
953
1351
|
}
|
|
954
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 {};
|
|
955
1405
|
}
|
|
956
|
-
return null;
|
|
957
1406
|
}
|
|
958
|
-
function
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
bundleUrl: withRuntimeConfig(
|
|
963
|
-
port,
|
|
964
|
-
`http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
|
|
965
|
-
),
|
|
966
|
-
hmrUrl: `ws://localhost:${port}/hot`,
|
|
967
|
-
lastSeen: Date.now()
|
|
968
|
-
};
|
|
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(/\/+$/, "");
|
|
969
1411
|
}
|
|
970
|
-
function
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
return normalizeNativeDevBundleUrl(configured);
|
|
1412
|
+
function resolveRuntimeChannel(input) {
|
|
1413
|
+
const config = readConfig();
|
|
1414
|
+
return input || process.env[RUNTIME_CHANNEL_ENV] || config.runtimeChannel || "stable";
|
|
974
1415
|
}
|
|
975
|
-
function
|
|
976
|
-
|
|
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();
|
|
977
1420
|
}
|
|
978
|
-
function
|
|
979
|
-
|
|
980
|
-
try {
|
|
981
|
-
const manifest = JSON.parse(manifestRes.body);
|
|
982
|
-
const client = manifest?.extra?.expoClient || manifest?.extra || {};
|
|
983
|
-
if (client.name) result.projectName = client.name;
|
|
984
|
-
if (client.ios?.bundleIdentifier) result.bundleId = client.ios.bundleIdentifier;
|
|
985
|
-
if (result.framework === "metro" && client.sdkVersion) result.framework = "expo";
|
|
986
|
-
const launchUrl2 = manifest?.launchAsset?.url;
|
|
987
|
-
if (launchUrl2 && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
|
|
988
|
-
result.bundleUrl = withRuntimeConfig(result.port, launchUrl2);
|
|
989
|
-
}
|
|
990
|
-
const rawIconUrl = client.iconUrl || client.ios?.iconUrl || client.icon || client.ios?.icon;
|
|
991
|
-
if (rawIconUrl) {
|
|
992
|
-
result.iconPath = rawIconUrl;
|
|
993
|
-
if (buildIconProxyUrl) {
|
|
994
|
-
if (rawIconUrl.startsWith("http")) {
|
|
995
|
-
result.iconUrl = buildIconProxyUrl(rawIconUrl);
|
|
996
|
-
} else {
|
|
997
|
-
const cleanPath = rawIconUrl.replace(/^\.\//, "");
|
|
998
|
-
result.iconUrl = buildIconProxyUrl(
|
|
999
|
-
`http://localhost:${result.port}/assets/${cleanPath}`
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
} else {
|
|
1003
|
-
result.iconUrl = rawIconUrl.startsWith("http") ? rawIconUrl : `http://localhost:${result.port}/assets/${rawIconUrl.replace(/^\.\//, "")}`;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
} catch {
|
|
1007
|
-
}
|
|
1008
|
-
return result;
|
|
1421
|
+
function runtimeTarballUrl(version, cdnOrigin) {
|
|
1422
|
+
return `${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/sootsim-runtime-${version}.tar.gz`;
|
|
1009
1423
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (!
|
|
1014
|
-
|
|
1015
|
-
const [sootsimRes, statusRes, oneRes, manifestRes, expoRes] = await Promise.all([
|
|
1016
|
-
knownNonPatched.has(port) ? Promise.resolve(null) : httpGet(port, "/__soot/"),
|
|
1017
|
-
httpGet(port, "/status"),
|
|
1018
|
-
httpGet(port, onePath, "HEAD"),
|
|
1019
|
-
httpGet(port, "/", "GET", TIMEOUT_MS, { "expo-platform": "ios" }),
|
|
1020
|
-
knownNonExpo.has(port) ? Promise.resolve(null) : httpGet(port, "/_expo/status")
|
|
1021
|
-
]);
|
|
1022
|
-
if (expoRes && expoRes.statusCode === 200) {
|
|
1023
|
-
knownNonExpo.delete(port);
|
|
1024
|
-
} else if (!knownNonExpo.has(port)) {
|
|
1025
|
-
knownNonExpo.add(port);
|
|
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})`);
|
|
1026
1429
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
`http://localhost:${port}${onePath}&minify=false`
|
|
1036
|
-
),
|
|
1037
|
-
hmrUrl: `ws://localhost:${port}/hot`,
|
|
1038
|
-
lastSeen: Date.now()
|
|
1039
|
-
},
|
|
1040
|
-
manifestRes,
|
|
1041
|
-
buildIconProxyUrl
|
|
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`
|
|
1042
1438
|
);
|
|
1043
1439
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
manifestRes,
|
|
1049
|
-
buildIconProxyUrl
|
|
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)"}`
|
|
1050
1444
|
);
|
|
1051
1445
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
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
|
+
};
|
|
1073
1466
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
buildIconProxyUrl
|
|
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}`
|
|
1087
1479
|
);
|
|
1088
1480
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (entry.pid !== pid) return false;
|
|
1114
|
-
if (!hasCurrentRuntimeConfig(entry.result)) return false;
|
|
1115
|
-
const ageMs = now - entry.cachedAt;
|
|
1116
|
-
if (entry.result === null && ageMs >= NEGATIVE_CACHE_TTL_MS) return false;
|
|
1117
|
-
if (isWeakCachedResult(entry.result) && ageMs >= WEAK_RESULT_CACHE_TTL_MS) return false;
|
|
1118
|
-
return true;
|
|
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
|
+
};
|
|
1119
1505
|
}
|
|
1120
|
-
async function
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
+
};
|
|
1125
1519
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
+
};
|
|
1128
1529
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
+
};
|
|
1131
1542
|
}
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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})`);
|
|
1145
1561
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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)
|
|
1149
1568
|
);
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
for (const { port, pid } of processes) {
|
|
1158
|
-
if (pid > 0) pidByPort.set(port, pid);
|
|
1159
|
-
}
|
|
1160
|
-
await Promise.all(
|
|
1161
|
-
results.map(async (result) => {
|
|
1162
|
-
const pid = pidByPort.get(result.port);
|
|
1163
|
-
if (!pid) return;
|
|
1164
|
-
result.pid = pid;
|
|
1165
|
-
const cwd = await resolveProcessCwd(pid);
|
|
1166
|
-
if (cwd) result.cwd = cwd;
|
|
1167
|
-
})
|
|
1168
|
-
);
|
|
1169
|
-
const livePids = new Set(pidByPort.values());
|
|
1170
|
-
for (const pid of [...cwdByPid.keys()]) {
|
|
1171
|
-
if (!livePids.has(pid)) cwdByPid.delete(pid);
|
|
1569
|
+
import_fs.default.renameSync(tmp, destPath);
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
try {
|
|
1572
|
+
import_fs.default.unlinkSync(tmp);
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
throw err;
|
|
1172
1576
|
}
|
|
1173
|
-
return results.filter((r) => !isSootSelfServer(r));
|
|
1174
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
|
+
});
|
|
1598
|
+
}
|
|
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);
|
|
1175
1603
|
|
|
1176
1604
|
// src/agent-sessions.ts
|
|
1177
1605
|
var import_node_child_process = require("node:child_process");
|
|
1178
1606
|
var import_node_crypto2 = require("node:crypto");
|
|
1179
|
-
var
|
|
1180
|
-
var
|
|
1607
|
+
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
1608
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
1181
1609
|
var import_node_readline = __toESM(require("node:readline"), 1);
|
|
1182
1610
|
|
|
1183
1611
|
// src/agent-events.ts
|
|
@@ -1221,37 +1649,20 @@ function encodeAgentPromptEnvelope(input) {
|
|
|
1221
1649
|
|
|
1222
1650
|
// src/attached-projects.ts
|
|
1223
1651
|
var import_node_crypto = require("node:crypto");
|
|
1224
|
-
var
|
|
1225
|
-
var
|
|
1226
|
-
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);
|
|
1227
1654
|
var overrideDir = null;
|
|
1228
1655
|
function userDataDir() {
|
|
1229
1656
|
if (overrideDir) return overrideDir;
|
|
1230
1657
|
const fromEnv = process.env.SOOTSIM_USER_DATA_DIR;
|
|
1231
1658
|
if (fromEnv) return fromEnv;
|
|
1232
|
-
|
|
1233
|
-
const electron = require("electron");
|
|
1234
|
-
if (electron.app?.getPath) return electron.app.getPath("userData");
|
|
1235
|
-
} catch {
|
|
1236
|
-
}
|
|
1237
|
-
return platformDefaultUserDataDir();
|
|
1238
|
-
}
|
|
1239
|
-
function platformDefaultUserDataDir() {
|
|
1240
|
-
const home = import_node_os2.default.homedir();
|
|
1241
|
-
if (process.platform === "darwin") {
|
|
1242
|
-
return import_node_path2.default.join(home, "Library", "Application Support", "sootsim");
|
|
1243
|
-
}
|
|
1244
|
-
if (process.platform === "win32") {
|
|
1245
|
-
return import_node_path2.default.join(process.env.APPDATA || home, "sootsim");
|
|
1246
|
-
}
|
|
1247
|
-
const xdg = process.env.XDG_CONFIG_HOME || import_node_path2.default.join(home, ".config");
|
|
1248
|
-
return import_node_path2.default.join(xdg, "sootsim");
|
|
1659
|
+
return electronUserDataDir();
|
|
1249
1660
|
}
|
|
1250
1661
|
function getUserDataDir() {
|
|
1251
1662
|
return userDataDir();
|
|
1252
1663
|
}
|
|
1253
1664
|
function storeFile() {
|
|
1254
|
-
return
|
|
1665
|
+
return import_node_path3.default.join(userDataDir(), "attached-projects.json");
|
|
1255
1666
|
}
|
|
1256
1667
|
function cloneEmpty() {
|
|
1257
1668
|
return {
|
|
@@ -1265,7 +1676,7 @@ function loadStore() {
|
|
|
1265
1676
|
const file = storeFile();
|
|
1266
1677
|
let raw;
|
|
1267
1678
|
try {
|
|
1268
|
-
raw =
|
|
1679
|
+
raw = import_node_fs3.default.readFileSync(file, "utf8");
|
|
1269
1680
|
} catch (err) {
|
|
1270
1681
|
if (err.code === "ENOENT") return cloneEmpty();
|
|
1271
1682
|
throw err;
|
|
@@ -1282,7 +1693,7 @@ function loadStore() {
|
|
|
1282
1693
|
} catch (err) {
|
|
1283
1694
|
const quarantine = `${file}.corrupt-${Date.now()}`;
|
|
1284
1695
|
try {
|
|
1285
|
-
|
|
1696
|
+
import_node_fs3.default.renameSync(file, quarantine);
|
|
1286
1697
|
console.warn(
|
|
1287
1698
|
`[sootsim] attached-projects.json was unparseable; quarantined to ${quarantine}. original error: ${err.message}`
|
|
1288
1699
|
);
|
|
@@ -1293,16 +1704,16 @@ function loadStore() {
|
|
|
1293
1704
|
}
|
|
1294
1705
|
function writeStore(store) {
|
|
1295
1706
|
const file = storeFile();
|
|
1296
|
-
|
|
1707
|
+
import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
|
|
1297
1708
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
1298
|
-
const fd =
|
|
1709
|
+
const fd = import_node_fs3.default.openSync(tmp, "w", 384);
|
|
1299
1710
|
try {
|
|
1300
|
-
|
|
1301
|
-
|
|
1711
|
+
import_node_fs3.default.writeFileSync(fd, JSON.stringify(store, null, 2));
|
|
1712
|
+
import_node_fs3.default.fsyncSync(fd);
|
|
1302
1713
|
} finally {
|
|
1303
|
-
|
|
1714
|
+
import_node_fs3.default.closeSync(fd);
|
|
1304
1715
|
}
|
|
1305
|
-
|
|
1716
|
+
import_node_fs3.default.renameSync(tmp, file);
|
|
1306
1717
|
}
|
|
1307
1718
|
function mutateStore(fn) {
|
|
1308
1719
|
const store = loadStore();
|
|
@@ -1311,13 +1722,13 @@ function mutateStore(fn) {
|
|
|
1311
1722
|
return store;
|
|
1312
1723
|
}
|
|
1313
1724
|
function projectIdForCwd(cwd) {
|
|
1314
|
-
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);
|
|
1315
1726
|
}
|
|
1316
1727
|
function newSessionId() {
|
|
1317
1728
|
return `s_${(0, import_node_crypto.randomBytes)(10).toString("hex")}`;
|
|
1318
1729
|
}
|
|
1319
1730
|
function upsertProject(input) {
|
|
1320
|
-
const cwd =
|
|
1731
|
+
const cwd = import_node_path3.default.resolve(input.cwd);
|
|
1321
1732
|
const id = projectIdForCwd(cwd);
|
|
1322
1733
|
let result;
|
|
1323
1734
|
mutateStore((store) => {
|
|
@@ -1343,7 +1754,7 @@ function upsertProject(input) {
|
|
|
1343
1754
|
const now = Date.now();
|
|
1344
1755
|
const created = {
|
|
1345
1756
|
id,
|
|
1346
|
-
name: input.name ??
|
|
1757
|
+
name: input.name ?? import_node_path3.default.basename(cwd),
|
|
1347
1758
|
cwd,
|
|
1348
1759
|
repoRoot: input.repoRoot,
|
|
1349
1760
|
sourceRoots: input.sourceRoots ?? [cwd],
|
|
@@ -1483,8 +1894,8 @@ async function seedFromDemoAppRegistry() {
|
|
|
1483
1894
|
const apps = APPS2;
|
|
1484
1895
|
mutateStore((store) => {
|
|
1485
1896
|
for (const app of apps) {
|
|
1486
|
-
if (!
|
|
1487
|
-
const cwd =
|
|
1897
|
+
if (!import_node_fs3.default.existsSync(app.dir)) continue;
|
|
1898
|
+
const cwd = import_node_path3.default.resolve(app.dir);
|
|
1488
1899
|
const id = projectIdForCwd(cwd);
|
|
1489
1900
|
if (store.attachedProjects.some((p) => p.id === id)) continue;
|
|
1490
1901
|
const now = Date.now();
|
|
@@ -1508,16 +1919,16 @@ async function seedFromDemoAppRegistry() {
|
|
|
1508
1919
|
|
|
1509
1920
|
// src/agent-sessions.ts
|
|
1510
1921
|
function sessionDir(sessionId) {
|
|
1511
|
-
return
|
|
1922
|
+
return import_node_path4.default.join(getUserDataDir(), "sessions", sessionId);
|
|
1512
1923
|
}
|
|
1513
1924
|
function promptFifoPath(sessionId) {
|
|
1514
|
-
return
|
|
1925
|
+
return import_node_path4.default.join(sessionDir(sessionId), "prompt.in");
|
|
1515
1926
|
}
|
|
1516
1927
|
function eventsFifoPath(sessionId) {
|
|
1517
|
-
return
|
|
1928
|
+
return import_node_path4.default.join(sessionDir(sessionId), "events.out");
|
|
1518
1929
|
}
|
|
1519
1930
|
function transcriptPath(sessionId) {
|
|
1520
|
-
return
|
|
1931
|
+
return import_node_path4.default.join(getUserDataDir(), "transcripts", `${sessionId}.log`);
|
|
1521
1932
|
}
|
|
1522
1933
|
function pidIsAlive(pid, sessionId) {
|
|
1523
1934
|
if (!pid) return false;
|
|
@@ -1527,7 +1938,7 @@ function pidIsAlive(pid, sessionId) {
|
|
|
1527
1938
|
return false;
|
|
1528
1939
|
}
|
|
1529
1940
|
if (sessionId) {
|
|
1530
|
-
if (!
|
|
1941
|
+
if (!import_node_fs4.default.existsSync(sessionDir(sessionId))) return false;
|
|
1531
1942
|
}
|
|
1532
1943
|
return true;
|
|
1533
1944
|
}
|
|
@@ -1539,11 +1950,11 @@ function resolveSootsimInvocation() {
|
|
|
1539
1950
|
const resourcesPath = process.resourcesPath;
|
|
1540
1951
|
if (resourcesPath) {
|
|
1541
1952
|
const candidates = [
|
|
1542
|
-
|
|
1543
|
-
|
|
1953
|
+
import_node_path4.default.join(resourcesPath, "bin", "sootsim"),
|
|
1954
|
+
import_node_path4.default.join(resourcesPath, "bin", `sootsim-${process.platform}-${process.arch}`)
|
|
1544
1955
|
];
|
|
1545
1956
|
for (const c of candidates) {
|
|
1546
|
-
if (
|
|
1957
|
+
if (import_node_fs4.default.existsSync(c)) return { cmd: c, prefixArgs: [] };
|
|
1547
1958
|
}
|
|
1548
1959
|
}
|
|
1549
1960
|
}
|
|
@@ -1566,15 +1977,15 @@ function tryWorkspaceSootsim() {
|
|
|
1566
1977
|
const sootsimDir = resolveSootsimPackageDir();
|
|
1567
1978
|
if (!sootsimDir) return null;
|
|
1568
1979
|
const binaryName = `sootsim-${process.platform}-${process.arch}`;
|
|
1569
|
-
const distBinary =
|
|
1570
|
-
if (
|
|
1571
|
-
const distBin =
|
|
1572
|
-
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)) {
|
|
1573
1984
|
try {
|
|
1574
|
-
const src =
|
|
1575
|
-
if (
|
|
1576
|
-
const srcMtime =
|
|
1577
|
-
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;
|
|
1578
1989
|
if (buildMtime < srcMtime) {
|
|
1579
1990
|
console.warn(
|
|
1580
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).`
|
|
@@ -1593,22 +2004,22 @@ function tryWorkspaceSootsim() {
|
|
|
1593
2004
|
function resolveSootsimPackageDir() {
|
|
1594
2005
|
try {
|
|
1595
2006
|
const resolved = require.resolve("sootsim/package.json");
|
|
1596
|
-
return
|
|
2007
|
+
return import_node_path4.default.dirname(resolved);
|
|
1597
2008
|
} catch {
|
|
1598
2009
|
}
|
|
1599
2010
|
const here = fileFromImportMeta();
|
|
1600
2011
|
if (!here) return null;
|
|
1601
|
-
let cur =
|
|
2012
|
+
let cur = import_node_path4.default.dirname(here);
|
|
1602
2013
|
for (let i = 0; i < 8; i++) {
|
|
1603
|
-
const pkg =
|
|
2014
|
+
const pkg = import_node_path4.default.join(cur, "package.json");
|
|
1604
2015
|
try {
|
|
1605
|
-
if (
|
|
1606
|
-
const parsed = JSON.parse(
|
|
2016
|
+
if (import_node_fs4.default.existsSync(pkg)) {
|
|
2017
|
+
const parsed = JSON.parse(import_node_fs4.default.readFileSync(pkg, "utf8"));
|
|
1607
2018
|
if (parsed.name === "sootsim") return cur;
|
|
1608
2019
|
}
|
|
1609
2020
|
} catch {
|
|
1610
2021
|
}
|
|
1611
|
-
const parent =
|
|
2022
|
+
const parent = import_node_path4.default.dirname(cur);
|
|
1612
2023
|
if (parent === cur) break;
|
|
1613
2024
|
cur = parent;
|
|
1614
2025
|
}
|
|
@@ -1624,28 +2035,28 @@ function fileFromImportMeta() {
|
|
|
1624
2035
|
}
|
|
1625
2036
|
}
|
|
1626
2037
|
async function withStartLock(projectId, provider, fn) {
|
|
1627
|
-
const lockDir =
|
|
1628
|
-
|
|
2038
|
+
const lockDir = import_node_path4.default.join(getUserDataDir(), "locks");
|
|
2039
|
+
import_node_fs4.default.mkdirSync(lockDir, { recursive: true });
|
|
1629
2040
|
try {
|
|
1630
|
-
|
|
2041
|
+
import_node_fs4.default.chmodSync(lockDir, 448);
|
|
1631
2042
|
} catch {
|
|
1632
2043
|
}
|
|
1633
|
-
const lockPath =
|
|
2044
|
+
const lockPath = import_node_path4.default.join(lockDir, `start-${projectId}-${provider}.lock`);
|
|
1634
2045
|
const deadline = Date.now() + 4e3;
|
|
1635
2046
|
let fd = null;
|
|
1636
2047
|
while (fd === null) {
|
|
1637
2048
|
try {
|
|
1638
|
-
fd =
|
|
2049
|
+
fd = import_node_fs4.default.openSync(
|
|
1639
2050
|
lockPath,
|
|
1640
|
-
|
|
2051
|
+
import_node_fs4.constants.O_WRONLY | import_node_fs4.constants.O_CREAT | import_node_fs4.constants.O_EXCL,
|
|
1641
2052
|
384
|
|
1642
2053
|
);
|
|
1643
2054
|
} catch (err) {
|
|
1644
2055
|
if (err.code !== "EEXIST") throw err;
|
|
1645
2056
|
try {
|
|
1646
|
-
const stale = Number(
|
|
2057
|
+
const stale = Number(import_node_fs4.default.readFileSync(lockPath, "utf8").trim());
|
|
1647
2058
|
if (stale && !isProcessAlive(stale)) {
|
|
1648
|
-
|
|
2059
|
+
import_node_fs4.default.unlinkSync(lockPath);
|
|
1649
2060
|
continue;
|
|
1650
2061
|
}
|
|
1651
2062
|
} catch {
|
|
@@ -1659,15 +2070,15 @@ async function withStartLock(projectId, provider, fn) {
|
|
|
1659
2070
|
}
|
|
1660
2071
|
}
|
|
1661
2072
|
try {
|
|
1662
|
-
|
|
2073
|
+
import_node_fs4.default.writeFileSync(fd, String(process.pid));
|
|
1663
2074
|
return await fn();
|
|
1664
2075
|
} finally {
|
|
1665
2076
|
try {
|
|
1666
|
-
|
|
2077
|
+
import_node_fs4.default.closeSync(fd);
|
|
1667
2078
|
} catch {
|
|
1668
2079
|
}
|
|
1669
2080
|
try {
|
|
1670
|
-
|
|
2081
|
+
import_node_fs4.default.unlinkSync(lockPath);
|
|
1671
2082
|
} catch {
|
|
1672
2083
|
}
|
|
1673
2084
|
}
|
|
@@ -1681,25 +2092,25 @@ function isProcessAlive(pid) {
|
|
|
1681
2092
|
}
|
|
1682
2093
|
}
|
|
1683
2094
|
function mkfifoSync(p) {
|
|
1684
|
-
const parent =
|
|
1685
|
-
|
|
2095
|
+
const parent = import_node_path4.default.dirname(p);
|
|
2096
|
+
import_node_fs4.default.mkdirSync(parent, { recursive: true });
|
|
1686
2097
|
try {
|
|
1687
|
-
|
|
2098
|
+
import_node_fs4.default.chmodSync(parent, 448);
|
|
1688
2099
|
} catch {
|
|
1689
2100
|
}
|
|
1690
|
-
if (
|
|
2101
|
+
if (import_node_fs4.default.existsSync(p)) {
|
|
1691
2102
|
try {
|
|
1692
|
-
const stat =
|
|
2103
|
+
const stat = import_node_fs4.default.statSync(p);
|
|
1693
2104
|
if (stat.isFIFO()) {
|
|
1694
2105
|
try {
|
|
1695
|
-
|
|
2106
|
+
import_node_fs4.default.chmodSync(p, 384);
|
|
1696
2107
|
} catch {
|
|
1697
2108
|
}
|
|
1698
2109
|
return;
|
|
1699
2110
|
}
|
|
1700
|
-
|
|
2111
|
+
import_node_fs4.default.unlinkSync(p);
|
|
1701
2112
|
} catch {
|
|
1702
|
-
|
|
2113
|
+
import_node_fs4.default.unlinkSync(p);
|
|
1703
2114
|
}
|
|
1704
2115
|
}
|
|
1705
2116
|
const result = (0, import_node_child_process.spawnSync)("mkfifo", ["-m", "600", p]);
|
|
@@ -1746,10 +2157,10 @@ async function startSession(opts) {
|
|
|
1746
2157
|
const transcript = transcriptPath(session.id);
|
|
1747
2158
|
mkfifoSync(promptIn);
|
|
1748
2159
|
mkfifoSync(eventsOut);
|
|
1749
|
-
const transcriptDir =
|
|
1750
|
-
|
|
2160
|
+
const transcriptDir = import_node_path4.default.dirname(transcript);
|
|
2161
|
+
import_node_fs4.default.mkdirSync(transcriptDir, { recursive: true });
|
|
1751
2162
|
try {
|
|
1752
|
-
|
|
2163
|
+
import_node_fs4.default.chmodSync(transcriptDir, 448);
|
|
1753
2164
|
} catch {
|
|
1754
2165
|
}
|
|
1755
2166
|
const { cmd, prefixArgs } = resolveSootsimInvocation();
|
|
@@ -1800,7 +2211,7 @@ async function startSession(opts) {
|
|
|
1800
2211
|
}
|
|
1801
2212
|
}
|
|
1802
2213
|
try {
|
|
1803
|
-
|
|
2214
|
+
import_node_fs4.default.rmSync(sessionDir(session.id), { recursive: true, force: true });
|
|
1804
2215
|
} catch {
|
|
1805
2216
|
}
|
|
1806
2217
|
updateSessionStatus(session.id, { status: "ended" });
|
|
@@ -1828,18 +2239,18 @@ async function sendPrompt(sessionId, prompt) {
|
|
|
1828
2239
|
);
|
|
1829
2240
|
}
|
|
1830
2241
|
const fifo = promptFifoPath(sessionId);
|
|
1831
|
-
if (!
|
|
2242
|
+
if (!import_node_fs4.default.existsSync(fifo)) {
|
|
1832
2243
|
throw new AgentSessionError("NO_FIFO", `prompt FIFO missing: ${fifo}`);
|
|
1833
2244
|
}
|
|
1834
|
-
const fd =
|
|
2245
|
+
const fd = import_node_fs4.default.openSync(fifo, import_node_fs4.constants.O_WRONLY);
|
|
1835
2246
|
try {
|
|
1836
2247
|
const wireText = encodeAgentPromptEnvelope(prompt);
|
|
1837
2248
|
if (!wireText) {
|
|
1838
2249
|
throw new AgentSessionError("EMPTY_PROMPT", "prompt text is empty");
|
|
1839
2250
|
}
|
|
1840
|
-
|
|
2251
|
+
import_node_fs4.default.writeSync(fd, wireText + "\n");
|
|
1841
2252
|
} finally {
|
|
1842
|
-
|
|
2253
|
+
import_node_fs4.default.closeSync(fd);
|
|
1843
2254
|
}
|
|
1844
2255
|
updateSessionStatus(sessionId, {
|
|
1845
2256
|
lastPrompt: prompt.displayText ?? prompt.text,
|
|
@@ -1861,7 +2272,7 @@ async function endSession(sessionId) {
|
|
|
1861
2272
|
const base = getUserDataDir();
|
|
1862
2273
|
if (dir.startsWith(base)) {
|
|
1863
2274
|
try {
|
|
1864
|
-
|
|
2275
|
+
import_node_fs4.default.rmSync(dir, { recursive: true, force: true });
|
|
1865
2276
|
} catch {
|
|
1866
2277
|
}
|
|
1867
2278
|
}
|
|
@@ -1869,11 +2280,11 @@ async function endSession(sessionId) {
|
|
|
1869
2280
|
}
|
|
1870
2281
|
function subscribeEvents(sessionId, onEvent) {
|
|
1871
2282
|
const fifo = eventsFifoPath(sessionId);
|
|
1872
|
-
if (!
|
|
2283
|
+
if (!import_node_fs4.default.existsSync(fifo)) {
|
|
1873
2284
|
throw new AgentSessionError("NO_FIFO", `events FIFO missing: ${fifo}`);
|
|
1874
2285
|
}
|
|
1875
|
-
const fd =
|
|
1876
|
-
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 });
|
|
1877
2288
|
const rl = import_node_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1878
2289
|
rl.on("line", (line) => {
|
|
1879
2290
|
const event = parseAgentEventLine(line);
|
|
@@ -1894,7 +2305,7 @@ function subscribeEvents(sessionId, onEvent) {
|
|
|
1894
2305
|
};
|
|
1895
2306
|
}
|
|
1896
2307
|
async function waitForFirstEvent(fifo, predicate, timeoutMs) {
|
|
1897
|
-
const fd =
|
|
2308
|
+
const fd = import_node_fs4.default.openSync(fifo, import_node_fs4.constants.O_RDWR | import_node_fs4.constants.O_NONBLOCK);
|
|
1898
2309
|
const buf = Buffer.alloc(8192);
|
|
1899
2310
|
let leftover = "";
|
|
1900
2311
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1902,7 +2313,7 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
|
|
|
1902
2313
|
while (Date.now() < deadline) {
|
|
1903
2314
|
let n = 0;
|
|
1904
2315
|
try {
|
|
1905
|
-
n =
|
|
2316
|
+
n = import_node_fs4.default.readSync(fd, buf, 0, buf.length, null);
|
|
1906
2317
|
} catch (err) {
|
|
1907
2318
|
if (err.code !== "EAGAIN") throw err;
|
|
1908
2319
|
n = 0;
|
|
@@ -1922,7 +2333,7 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
|
|
|
1922
2333
|
}
|
|
1923
2334
|
return null;
|
|
1924
2335
|
} finally {
|
|
1925
|
-
|
|
2336
|
+
import_node_fs4.default.closeSync(fd);
|
|
1926
2337
|
}
|
|
1927
2338
|
}
|
|
1928
2339
|
|
|
@@ -2093,7 +2504,7 @@ var AgentHost = class {
|
|
|
2093
2504
|
);
|
|
2094
2505
|
const project = upsertProject({
|
|
2095
2506
|
cwd: match.cwd,
|
|
2096
|
-
name: match.projectName ??
|
|
2507
|
+
name: match.projectName ?? import_node_path5.default.basename(match.cwd),
|
|
2097
2508
|
preferredProvider: input.provider ?? existing?.preferredProvider,
|
|
2098
2509
|
sourceRoots: existing?.sourceRoots ?? [match.cwd],
|
|
2099
2510
|
knownBundleUrls,
|
|
@@ -2104,18 +2515,18 @@ var AgentHost = class {
|
|
|
2104
2515
|
}
|
|
2105
2516
|
getTranscript(sessionId) {
|
|
2106
2517
|
const p = transcriptPath(sessionId);
|
|
2107
|
-
if (!
|
|
2518
|
+
if (!import_node_fs5.default.existsSync(p)) {
|
|
2108
2519
|
return { error: "transcript not found", code: "NO_TRANSCRIPT" };
|
|
2109
2520
|
}
|
|
2110
|
-
return
|
|
2521
|
+
return import_node_fs5.default.readFileSync(p, "utf8");
|
|
2111
2522
|
}
|
|
2112
2523
|
getPaths() {
|
|
2113
2524
|
const dir = getUserDataDir();
|
|
2114
2525
|
return {
|
|
2115
2526
|
userDataDir: dir,
|
|
2116
|
-
storeFile:
|
|
2117
|
-
sessionsDir:
|
|
2118
|
-
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")
|
|
2119
2530
|
};
|
|
2120
2531
|
}
|
|
2121
2532
|
// --- subscription management ---
|
|
@@ -2375,11 +2786,212 @@ function mapFrameworkToProjectFramework(fw) {
|
|
|
2375
2786
|
return "unknown";
|
|
2376
2787
|
}
|
|
2377
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
|
+
|
|
2378
2990
|
// src/host/open-url.ts
|
|
2379
2991
|
var import_child_process3 = require("child_process");
|
|
2380
|
-
var
|
|
2381
|
-
var
|
|
2382
|
-
var
|
|
2992
|
+
var import_fs2 = require("fs");
|
|
2993
|
+
var import_os = require("os");
|
|
2994
|
+
var import_path2 = require("path");
|
|
2383
2995
|
var MAC_CHROMIUM_CANDIDATES = [
|
|
2384
2996
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2385
2997
|
"~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -2417,12 +3029,12 @@ var UNIX_CHROMIUM_COMMANDS = [
|
|
|
2417
3029
|
];
|
|
2418
3030
|
var WINDOWS_CHROMIUM_COMMANDS = ["chrome", "msedge", "brave"];
|
|
2419
3031
|
function expandHome(candidate) {
|
|
2420
|
-
return candidate.startsWith("~/") ? (0,
|
|
3032
|
+
return candidate.startsWith("~/") ? (0, import_path2.join)((0, import_os.homedir)(), candidate.slice(2)) : candidate;
|
|
2421
3033
|
}
|
|
2422
3034
|
function firstExisting(candidates) {
|
|
2423
3035
|
for (const candidate of candidates) {
|
|
2424
3036
|
const expanded = expandHome(candidate);
|
|
2425
|
-
if ((0,
|
|
3037
|
+
if ((0, import_fs2.existsSync)(expanded)) return expanded;
|
|
2426
3038
|
}
|
|
2427
3039
|
return null;
|
|
2428
3040
|
}
|
|
@@ -2441,7 +3053,7 @@ function lookupExecutable(command, platform) {
|
|
|
2441
3053
|
}
|
|
2442
3054
|
function resolveChromiumBinary(platform = process.platform) {
|
|
2443
3055
|
const envCandidate = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
|
|
2444
|
-
if (envCandidate && (0,
|
|
3056
|
+
if (envCandidate && (0, import_fs2.existsSync)(envCandidate)) return envCandidate;
|
|
2445
3057
|
const direct = platform === "darwin" ? firstExisting(MAC_CHROMIUM_CANDIDATES) : platform === "win32" ? firstExisting(WINDOWS_CHROMIUM_CANDIDATES) : firstExisting(LINUX_CHROMIUM_CANDIDATES);
|
|
2446
3058
|
if (direct) return direct;
|
|
2447
3059
|
const commands = platform === "win32" ? WINDOWS_CHROMIUM_COMMANDS : UNIX_CHROMIUM_COMMANDS;
|
|
@@ -2558,43 +3170,75 @@ var HTTP_MIME_TYPES = {
|
|
|
2558
3170
|
".map": "application/json",
|
|
2559
3171
|
".txt": "text/plain; charset=utf-8"
|
|
2560
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
|
+
}
|
|
2561
3187
|
var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
2562
3188
|
port;
|
|
2563
3189
|
openUrlHandler;
|
|
2564
3190
|
httpServer = null;
|
|
2565
3191
|
wss = null;
|
|
2566
3192
|
nextCommandId = 1;
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
3193
|
+
nextSimNumber = 161;
|
|
3194
|
+
sims = /* @__PURE__ */ new Map();
|
|
3195
|
+
primarySimId = null;
|
|
2570
3196
|
pendingCommands = /* @__PURE__ */ new Map();
|
|
2571
3197
|
cliBySentId = /* @__PURE__ */ new Map();
|
|
2572
|
-
|
|
3198
|
+
cliSimBySocket = /* @__PURE__ */ new Map();
|
|
2573
3199
|
cliLastCommandAt = /* @__PURE__ */ new Map();
|
|
2574
|
-
|
|
3200
|
+
cliIdentityKeyBySocket = /* @__PURE__ */ new Map();
|
|
2575
3201
|
cliLabelBySocket = /* @__PURE__ */ new Map();
|
|
2576
|
-
|
|
3202
|
+
restorableSims = /* @__PURE__ */ new Map();
|
|
2577
3203
|
nextCliFallbackId = 1;
|
|
2578
3204
|
cliIdleTimer = null;
|
|
2579
3205
|
agentHost;
|
|
2580
3206
|
static CLI_IDLE_TIMEOUT_MS = 6e4;
|
|
2581
3207
|
static CLI_LEASE_TTL_MS = 6e5;
|
|
2582
3208
|
static USER_ACTIVE_LEASE_TTL_MS = 8e3;
|
|
2583
|
-
// explicit user actions (clicking Boot, focusing the
|
|
2584
|
-
// hold the
|
|
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
|
|
2585
3211
|
// can't immediately reclaim while the user gets oriented.
|
|
2586
3212
|
static USER_BOOT_LEASE_TTL_MS = 6e4;
|
|
2587
|
-
static
|
|
3213
|
+
static SIM_RECONNECT_TTL_MS = 3e4;
|
|
2588
3214
|
preferredPort;
|
|
2589
3215
|
portFallbackCount;
|
|
2590
3216
|
shouldWriteLockfile;
|
|
2591
3217
|
effectivePort = 0;
|
|
2592
3218
|
startedAt = 0;
|
|
2593
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;
|
|
2594
3229
|
runtimeUpdateTimer = null;
|
|
2595
3230
|
runtimeUpdateInFlight = null;
|
|
2596
3231
|
activeRuntimeVersion = null;
|
|
2597
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;
|
|
2598
3242
|
constructor(opts = {}) {
|
|
2599
3243
|
this.preferredPort = opts.port || DEFAULT_SOOTSIM_BRIDGE_PORT;
|
|
2600
3244
|
this.port = this.preferredPort;
|
|
@@ -2652,7 +3296,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2652
3296
|
}
|
|
2653
3297
|
bindOnce(port, _silent) {
|
|
2654
3298
|
return new Promise((resolve2, reject) => {
|
|
2655
|
-
const server = (0,
|
|
3299
|
+
const server = (0, import_http3.createServer)((req, res) => this.handleHttpRequest(req, res));
|
|
2656
3300
|
let settled = false;
|
|
2657
3301
|
const onError = (err) => {
|
|
2658
3302
|
if (settled) return;
|
|
@@ -2687,14 +3331,18 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2687
3331
|
if (!this.wss) return;
|
|
2688
3332
|
this.wss.on("connection", (ws, req) => {
|
|
2689
3333
|
const origin = req.headers.origin;
|
|
2690
|
-
const role = origin ? "
|
|
2691
|
-
let
|
|
3334
|
+
const role = origin ? "sim" : "cli";
|
|
3335
|
+
let sim = null;
|
|
2692
3336
|
ws.on("error", () => {
|
|
2693
3337
|
});
|
|
3338
|
+
this.wsIsAlive.set(ws, true);
|
|
3339
|
+
ws.on("pong", () => {
|
|
3340
|
+
this.wsIsAlive.set(ws, true);
|
|
3341
|
+
});
|
|
2694
3342
|
this.agentHost.registerSocket(ws);
|
|
2695
|
-
if (role === "
|
|
2696
|
-
|
|
2697
|
-
id:
|
|
3343
|
+
if (role === "sim") {
|
|
3344
|
+
sim = {
|
|
3345
|
+
id: this.allocateSimId(),
|
|
2698
3346
|
ws,
|
|
2699
3347
|
origin,
|
|
2700
3348
|
connectedAt: Date.now(),
|
|
@@ -2702,15 +3350,15 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2702
3350
|
lastActiveAt: 0,
|
|
2703
3351
|
recentActions: []
|
|
2704
3352
|
};
|
|
2705
|
-
this.
|
|
2706
|
-
if (this.
|
|
2707
|
-
this.
|
|
3353
|
+
this.sims.set(sim.id, sim);
|
|
3354
|
+
if (this.shouldPromoteSim(sim)) {
|
|
3355
|
+
this.primarySimId = sim.id;
|
|
2708
3356
|
}
|
|
2709
|
-
this.
|
|
2710
|
-
this.
|
|
3357
|
+
this.broadcastSimAssignments();
|
|
3358
|
+
this.broadcastSimClientStates();
|
|
2711
3359
|
} else {
|
|
2712
3360
|
const fallbackKey = `ws-${this.nextCliFallbackId++}`;
|
|
2713
|
-
this.
|
|
3361
|
+
this.cliIdentityKeyBySocket.set(ws, fallbackKey);
|
|
2714
3362
|
}
|
|
2715
3363
|
ws.on("message", (data) => {
|
|
2716
3364
|
let msg;
|
|
@@ -2785,29 +3433,55 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2785
3433
|
}
|
|
2786
3434
|
return;
|
|
2787
3435
|
}
|
|
2788
|
-
if (role === "
|
|
2789
|
-
if (
|
|
2790
|
-
|
|
3436
|
+
if (role === "sim") {
|
|
3437
|
+
if (sim) {
|
|
3438
|
+
sim.lastSeenAt = Date.now();
|
|
2791
3439
|
}
|
|
2792
|
-
if (msg.type === "bridge:register" &&
|
|
3440
|
+
if (msg.type === "bridge:register" && sim) {
|
|
2793
3441
|
const registration = msg;
|
|
2794
|
-
const restored = this.
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3442
|
+
const restored = this.tryRestoreSimId(sim, registration.simId);
|
|
3443
|
+
sim.url = registration.url;
|
|
3444
|
+
sim.title = registration.title;
|
|
3445
|
+
sim.userAgent = registration.userAgent;
|
|
2798
3446
|
if (restored) {
|
|
2799
|
-
this.
|
|
2800
|
-
this.
|
|
3447
|
+
this.broadcastSimAssignments();
|
|
3448
|
+
this.broadcastSimClientStates();
|
|
2801
3449
|
}
|
|
2802
3450
|
return;
|
|
2803
3451
|
}
|
|
2804
|
-
if (msg.type === "bridge:user-focus-state" &&
|
|
3452
|
+
if (msg.type === "bridge:user-focus-state" && sim) {
|
|
2805
3453
|
const focusState = msg;
|
|
2806
|
-
this.updateUserFocusLease(
|
|
3454
|
+
this.updateUserFocusLease(sim, focusState.focused === true);
|
|
2807
3455
|
return;
|
|
2808
3456
|
}
|
|
2809
|
-
if (msg.type === "bridge:user-interact" &&
|
|
2810
|
-
this.updateUserActivity(
|
|
3457
|
+
if (msg.type === "bridge:user-interact" && sim) {
|
|
3458
|
+
this.updateUserActivity(sim);
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
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
|
+
}
|
|
2811
3485
|
return;
|
|
2812
3486
|
}
|
|
2813
3487
|
if (msg.type === "bridge:open-path") {
|
|
@@ -2819,33 +3493,33 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2819
3493
|
}
|
|
2820
3494
|
return;
|
|
2821
3495
|
}
|
|
2822
|
-
if (msg.type === "bridge:boot-clients" &&
|
|
3496
|
+
if (msg.type === "bridge:boot-clients" && sim) {
|
|
2823
3497
|
const booted = [];
|
|
2824
|
-
for (const [cliWs,
|
|
2825
|
-
if (
|
|
3498
|
+
for (const [cliWs, attachedSimId] of this.cliSimBySocket) {
|
|
3499
|
+
if (attachedSimId === sim.id) {
|
|
2826
3500
|
booted.push(cliWs);
|
|
2827
3501
|
}
|
|
2828
3502
|
}
|
|
2829
3503
|
for (const cliWs of booted) {
|
|
2830
|
-
this.
|
|
3504
|
+
this.cliSimBySocket.delete(cliWs);
|
|
2831
3505
|
try {
|
|
2832
|
-
cliWs.close(1e3, "booted by
|
|
3506
|
+
cliWs.close(1e3, "booted by sim");
|
|
2833
3507
|
} catch {
|
|
2834
3508
|
}
|
|
2835
3509
|
}
|
|
2836
|
-
const hadLease = !!
|
|
2837
|
-
|
|
3510
|
+
const hadLease = !!sim.cliLease;
|
|
3511
|
+
sim.cliLease = {
|
|
2838
3512
|
kind: "user-active",
|
|
2839
|
-
|
|
3513
|
+
cliIdentityKey: "__user-active__",
|
|
2840
3514
|
cliLabel: "active user",
|
|
2841
3515
|
expiresAt: Date.now() + _SootSimBridgeHost.USER_BOOT_LEASE_TTL_MS
|
|
2842
3516
|
};
|
|
2843
3517
|
process.stderr.write(
|
|
2844
|
-
`sootsim booted ${booted.length} cli client(s)${hadLease ? " (overrode prior lease)" : ""}; held
|
|
3518
|
+
`sootsim booted ${booted.length} cli client(s)${hadLease ? " (overrode prior lease)" : ""}; held sim for user [${sim.id}]
|
|
2845
3519
|
`
|
|
2846
3520
|
);
|
|
2847
|
-
this.
|
|
2848
|
-
this.
|
|
3521
|
+
this.recordSimAction(sim.id, "sim booted cli clients");
|
|
3522
|
+
this.broadcastSimClientStates();
|
|
2849
3523
|
return;
|
|
2850
3524
|
}
|
|
2851
3525
|
const internalPending = this.pendingCommands.get(msg.id);
|
|
@@ -2859,10 +3533,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2859
3533
|
if (entry) {
|
|
2860
3534
|
this.cliBySentId.delete(msg.id);
|
|
2861
3535
|
if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2862
|
-
const otherCliCount = this.
|
|
2863
|
-
entry.ws,
|
|
2864
|
-
entry.browserId
|
|
2865
|
-
);
|
|
3536
|
+
const otherCliCount = this.getOtherCliIdentityCount(entry.ws, entry.simId);
|
|
2866
3537
|
const response = otherCliCount > 0 ? { ...msg, id: entry.originalId, _otherCliCount: otherCliCount } : { ...msg, id: entry.originalId };
|
|
2867
3538
|
entry.ws.send(JSON.stringify(response));
|
|
2868
3539
|
}
|
|
@@ -2873,19 +3544,19 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2873
3544
|
this.cliLastCommandAt.set(ws, Date.now());
|
|
2874
3545
|
try {
|
|
2875
3546
|
if (msg.type === "bridge:bye") {
|
|
2876
|
-
const
|
|
3547
|
+
const hadSim = this.cliSimBySocket.delete(ws);
|
|
2877
3548
|
this.cliLastCommandAt.delete(ws);
|
|
2878
|
-
this.
|
|
3549
|
+
this.cliIdentityKeyBySocket.delete(ws);
|
|
2879
3550
|
this.cliLabelBySocket.delete(ws);
|
|
2880
3551
|
for (const [sentId2, entry] of this.cliBySentId) {
|
|
2881
3552
|
if (entry.ws === ws) this.cliBySentId.delete(sentId2);
|
|
2882
3553
|
}
|
|
2883
|
-
if (
|
|
3554
|
+
if (hadSim) this.broadcastSimClientStates();
|
|
2884
3555
|
return;
|
|
2885
3556
|
}
|
|
2886
3557
|
if (msg.type === "bridge:hello") {
|
|
2887
|
-
const key = typeof msg.
|
|
2888
|
-
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);
|
|
2889
3560
|
if (typeof msg.cliLabel === "string" && msg.cliLabel.trim()) {
|
|
2890
3561
|
this.cliLabelBySocket.set(ws, msg.cliLabel.trim());
|
|
2891
3562
|
}
|
|
@@ -2894,7 +3565,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2894
3565
|
JSON.stringify({
|
|
2895
3566
|
id: msg.id,
|
|
2896
3567
|
result: {
|
|
2897
|
-
|
|
3568
|
+
cliIdentityKey: key,
|
|
2898
3569
|
leaseTtlMs: _SootSimBridgeHost.CLI_LEASE_TTL_MS,
|
|
2899
3570
|
leasing: true
|
|
2900
3571
|
}
|
|
@@ -2903,12 +3574,12 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2903
3574
|
}
|
|
2904
3575
|
return;
|
|
2905
3576
|
}
|
|
2906
|
-
if (msg.type === "bridge:list-
|
|
3577
|
+
if (msg.type === "bridge:list-sims") {
|
|
2907
3578
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2908
3579
|
ws.send(
|
|
2909
3580
|
JSON.stringify({
|
|
2910
3581
|
id: msg.id,
|
|
2911
|
-
result: this.
|
|
3582
|
+
result: this.listSims()
|
|
2912
3583
|
})
|
|
2913
3584
|
);
|
|
2914
3585
|
}
|
|
@@ -2930,8 +3601,8 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2930
3601
|
return;
|
|
2931
3602
|
}
|
|
2932
3603
|
if (msg.type === "bridge:claim") {
|
|
2933
|
-
const
|
|
2934
|
-
const outcome = this.tryAcquireLease(ws,
|
|
3604
|
+
const targetSim2 = await this.waitForSim(msg.simId);
|
|
3605
|
+
const outcome = this.tryAcquireLease(ws, targetSim2, {
|
|
2935
3606
|
force: msg.force === true
|
|
2936
3607
|
});
|
|
2937
3608
|
if (!outcome.granted) {
|
|
@@ -2939,25 +3610,25 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2939
3610
|
ws.send(
|
|
2940
3611
|
JSON.stringify({
|
|
2941
3612
|
id: msg.id,
|
|
2942
|
-
error: `
|
|
3613
|
+
error: `sim ${targetSim2.id} is locked by another cli`,
|
|
2943
3614
|
_locked: outcome.lock
|
|
2944
3615
|
})
|
|
2945
3616
|
);
|
|
2946
3617
|
}
|
|
2947
3618
|
return;
|
|
2948
3619
|
}
|
|
2949
|
-
this.
|
|
2950
|
-
this.
|
|
2951
|
-
|
|
2952
|
-
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"
|
|
2953
3624
|
);
|
|
2954
3625
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2955
3626
|
ws.send(
|
|
2956
3627
|
JSON.stringify({
|
|
2957
3628
|
id: msg.id,
|
|
2958
3629
|
result: {
|
|
2959
|
-
|
|
2960
|
-
lockedBy: outcome.lease.
|
|
3630
|
+
simId: targetSim2.id,
|
|
3631
|
+
lockedBy: outcome.lease.cliIdentityKey,
|
|
2961
3632
|
lockExpiresAt: outcome.lease.expiresAt,
|
|
2962
3633
|
bootedCount: outcome.bootedCount
|
|
2963
3634
|
}
|
|
@@ -2966,15 +3637,15 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2966
3637
|
}
|
|
2967
3638
|
return;
|
|
2968
3639
|
}
|
|
2969
|
-
const
|
|
3640
|
+
const targetSim = await this.waitForSim(msg.simId);
|
|
2970
3641
|
if (shouldAcquireLease(msg)) {
|
|
2971
|
-
const outcome = this.tryAcquireLease(ws,
|
|
3642
|
+
const outcome = this.tryAcquireLease(ws, targetSim);
|
|
2972
3643
|
if (!outcome.granted) {
|
|
2973
3644
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2974
3645
|
ws.send(
|
|
2975
3646
|
JSON.stringify({
|
|
2976
3647
|
id: msg.id,
|
|
2977
|
-
error: `
|
|
3648
|
+
error: `sim ${targetSim.id} is locked by another cli \u2014 use \`sootsim claim ${targetSim.id} --force\` or \`sootsim open --new\``,
|
|
2978
3649
|
_locked: outcome.lock
|
|
2979
3650
|
})
|
|
2980
3651
|
);
|
|
@@ -2982,18 +3653,18 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
2982
3653
|
return;
|
|
2983
3654
|
}
|
|
2984
3655
|
} else {
|
|
2985
|
-
this.
|
|
3656
|
+
this.ensureCliIdentityKey(ws);
|
|
2986
3657
|
}
|
|
2987
|
-
this.
|
|
2988
|
-
this.
|
|
3658
|
+
this.setCliSimTarget(ws, targetSim.id);
|
|
3659
|
+
this.recordSimAction(targetSim.id, this.describeForwardedCommand(msg));
|
|
2989
3660
|
const sentId = this.nextCommandId++;
|
|
2990
3661
|
this.cliBySentId.set(sentId, {
|
|
2991
|
-
|
|
3662
|
+
simId: targetSim.id,
|
|
2992
3663
|
ws,
|
|
2993
3664
|
originalId: msg.id
|
|
2994
3665
|
});
|
|
2995
|
-
const {
|
|
2996
|
-
|
|
3666
|
+
const { simId: _simId, ...forwarded } = msg;
|
|
3667
|
+
targetSim.ws.send(JSON.stringify({ ...forwarded, id: sentId }));
|
|
2997
3668
|
} catch (err) {
|
|
2998
3669
|
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
2999
3670
|
ws.send(
|
|
@@ -3008,40 +3679,40 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3008
3679
|
});
|
|
3009
3680
|
ws.on("close", () => {
|
|
3010
3681
|
this.agentHost.unregisterSocket(ws);
|
|
3011
|
-
if (role === "
|
|
3012
|
-
this.
|
|
3013
|
-
if (this.
|
|
3014
|
-
this.
|
|
3682
|
+
if (role === "sim" && sim) {
|
|
3683
|
+
this.rememberDisconnectedSim(sim);
|
|
3684
|
+
if (this.primarySimId === sim.id) {
|
|
3685
|
+
this.primarySimId = this.getOpenSim()?.id ?? null;
|
|
3015
3686
|
}
|
|
3016
3687
|
for (const [id, pending] of this.pendingCommands) {
|
|
3017
|
-
if (pending.
|
|
3018
|
-
pending.reject(new Error("
|
|
3688
|
+
if (pending.simId !== sim.id) continue;
|
|
3689
|
+
pending.reject(new Error("sim disconnected"));
|
|
3019
3690
|
this.pendingCommands.delete(id);
|
|
3020
3691
|
}
|
|
3021
3692
|
for (const [sentId, entry] of this.cliBySentId) {
|
|
3022
|
-
if (entry.
|
|
3693
|
+
if (entry.simId !== sim.id) continue;
|
|
3023
3694
|
if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
3024
3695
|
entry.ws.send(
|
|
3025
3696
|
JSON.stringify({
|
|
3026
3697
|
id: entry.originalId,
|
|
3027
|
-
error: "
|
|
3698
|
+
error: "sim disconnected before responding"
|
|
3028
3699
|
})
|
|
3029
3700
|
);
|
|
3030
3701
|
}
|
|
3031
3702
|
this.cliBySentId.delete(sentId);
|
|
3032
3703
|
}
|
|
3033
|
-
this.
|
|
3034
|
-
this.
|
|
3704
|
+
this.broadcastSimAssignments();
|
|
3705
|
+
this.broadcastSimClientStates();
|
|
3035
3706
|
} else if (role === "cli") {
|
|
3036
|
-
const detached = this.
|
|
3707
|
+
const detached = this.cliSimBySocket.delete(ws);
|
|
3037
3708
|
this.cliLastCommandAt.delete(ws);
|
|
3038
|
-
this.
|
|
3709
|
+
this.cliIdentityKeyBySocket.delete(ws);
|
|
3039
3710
|
this.cliLabelBySocket.delete(ws);
|
|
3040
3711
|
for (const [sentId, entry] of this.cliBySentId) {
|
|
3041
3712
|
if (entry.ws === ws) this.cliBySentId.delete(sentId);
|
|
3042
3713
|
}
|
|
3043
3714
|
if (detached) {
|
|
3044
|
-
this.
|
|
3715
|
+
this.broadcastSimClientStates();
|
|
3045
3716
|
}
|
|
3046
3717
|
}
|
|
3047
3718
|
});
|
|
@@ -3059,6 +3730,11 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3059
3730
|
3e4
|
|
3060
3731
|
);
|
|
3061
3732
|
this.cliIdleTimer.unref();
|
|
3733
|
+
this.wsHeartbeatTimer = setInterval(
|
|
3734
|
+
() => this.sweepDeadWebSockets(),
|
|
3735
|
+
_SootSimBridgeHost.WS_HEARTBEAT_INTERVAL_MS
|
|
3736
|
+
);
|
|
3737
|
+
this.wsHeartbeatTimer.unref();
|
|
3062
3738
|
if (this.shouldWriteLockfile) {
|
|
3063
3739
|
try {
|
|
3064
3740
|
ensureSootsimHome();
|
|
@@ -3108,6 +3784,46 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3108
3784
|
this.activeRuntimeVersion = readActiveRuntime();
|
|
3109
3785
|
this.activeRuntimeDirPath = activeRuntimeDir();
|
|
3110
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
|
+
}
|
|
3111
3827
|
resolveRuntimeUpdateIntervalMs() {
|
|
3112
3828
|
const raw = Number(process.env[RUNTIME_UPDATE_INTERVAL_ENV]);
|
|
3113
3829
|
if (Number.isFinite(raw) && raw > 0) return Math.max(100, Math.round(raw));
|
|
@@ -3165,7 +3881,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3165
3881
|
}
|
|
3166
3882
|
/** update the active runtime on disk + in memory. the caller guarantees
|
|
3167
3883
|
* the version directory exists. pushes a runtime:changed message to all
|
|
3168
|
-
* connected
|
|
3884
|
+
* connected sims so electron (or any renderer) can reload. */
|
|
3169
3885
|
setActiveRuntime(version) {
|
|
3170
3886
|
writeActiveRuntime(version);
|
|
3171
3887
|
this.refreshActiveRuntime();
|
|
@@ -3180,10 +3896,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3180
3896
|
version,
|
|
3181
3897
|
runtimeDir: this.activeRuntimeDirPath
|
|
3182
3898
|
});
|
|
3183
|
-
for (const
|
|
3184
|
-
if (
|
|
3899
|
+
for (const sim of this.sims.values()) {
|
|
3900
|
+
if (sim.ws.readyState === import_ws.WebSocket.OPEN) {
|
|
3185
3901
|
try {
|
|
3186
|
-
|
|
3902
|
+
sim.ws.send(payload);
|
|
3187
3903
|
} catch {
|
|
3188
3904
|
}
|
|
3189
3905
|
}
|
|
@@ -3212,6 +3928,13 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3212
3928
|
* non-upgrade routes that don't match serve index.html (SPA behavior) so
|
|
3213
3929
|
* electron's webContents can navigate freely inside the runtime. */
|
|
3214
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
|
+
}
|
|
3215
3938
|
const method = (req.method || "GET").toUpperCase();
|
|
3216
3939
|
if (method !== "GET" && method !== "HEAD") {
|
|
3217
3940
|
res.writeHead(405, { Allow: "GET, HEAD" });
|
|
@@ -3271,6 +3994,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3271
3994
|
})();
|
|
3272
3995
|
return;
|
|
3273
3996
|
}
|
|
3997
|
+
if (url.pathname === "/__server-scan") {
|
|
3998
|
+
this.handleServerScan(res);
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
3274
4001
|
if (url.pathname === "/healthz") {
|
|
3275
4002
|
res.writeHead(200, {
|
|
3276
4003
|
"Content-Type": "application/json",
|
|
@@ -3290,6 +4017,24 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3290
4017
|
);
|
|
3291
4018
|
return;
|
|
3292
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
|
+
}
|
|
3293
4038
|
this.refreshActiveRuntime();
|
|
3294
4039
|
const baseDir = this.activeRuntimeDirPath;
|
|
3295
4040
|
if (!baseDir) {
|
|
@@ -3320,41 +4065,46 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3320
4065
|
return;
|
|
3321
4066
|
}
|
|
3322
4067
|
}
|
|
3323
|
-
const resolved =
|
|
3324
|
-
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;
|
|
3325
4070
|
if (!resolved.startsWith(baseWithSep) && resolved !== baseDir) {
|
|
3326
4071
|
res.writeHead(403);
|
|
3327
4072
|
res.end("forbidden");
|
|
3328
4073
|
return;
|
|
3329
4074
|
}
|
|
3330
|
-
|
|
4075
|
+
import_fs3.default.realpath(resolved, (realErr, realResolved) => {
|
|
3331
4076
|
const servePath = realErr ? resolved : realResolved;
|
|
3332
|
-
const servePathWithSep = servePath.endsWith(
|
|
4077
|
+
const servePathWithSep = servePath.endsWith(import_path3.default.sep) ? servePath : servePath + import_path3.default.sep;
|
|
3333
4078
|
if (!realErr) {
|
|
3334
4079
|
const realBaseWithSep = (() => {
|
|
3335
4080
|
try {
|
|
3336
|
-
const rb =
|
|
3337
|
-
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;
|
|
3338
4083
|
} catch {
|
|
3339
4084
|
return baseWithSep;
|
|
3340
4085
|
}
|
|
3341
4086
|
})();
|
|
3342
|
-
if (!servePathWithSep.startsWith(realBaseWithSep) && servePath +
|
|
4087
|
+
if (!servePathWithSep.startsWith(realBaseWithSep) && servePath + import_path3.default.sep !== realBaseWithSep) {
|
|
3343
4088
|
res.writeHead(403);
|
|
3344
4089
|
res.end("forbidden");
|
|
3345
4090
|
return;
|
|
3346
4091
|
}
|
|
3347
4092
|
}
|
|
3348
|
-
|
|
4093
|
+
import_fs3.default.stat(servePath, (err, stats) => {
|
|
3349
4094
|
if (err || !stats?.isFile()) {
|
|
3350
|
-
const ext2 =
|
|
4095
|
+
const ext2 = import_path3.default.extname(rel).toLowerCase();
|
|
3351
4096
|
if (ext2 && ext2 !== ".html") {
|
|
3352
4097
|
res.writeHead(404);
|
|
3353
4098
|
res.end("not found");
|
|
3354
4099
|
return;
|
|
3355
4100
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
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) => {
|
|
3358
4108
|
if (err2) {
|
|
3359
4109
|
res.writeHead(404);
|
|
3360
4110
|
res.end("not found");
|
|
@@ -3368,11 +4118,11 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3368
4118
|
res.end();
|
|
3369
4119
|
return;
|
|
3370
4120
|
}
|
|
3371
|
-
res.end(data);
|
|
4121
|
+
res.end(injectSharedConfigIntoHtml(data));
|
|
3372
4122
|
});
|
|
3373
4123
|
return;
|
|
3374
4124
|
}
|
|
3375
|
-
const ext =
|
|
4125
|
+
const ext = import_path3.default.extname(servePath).toLowerCase();
|
|
3376
4126
|
const contentType = HTTP_MIME_TYPES[ext] || "application/octet-stream";
|
|
3377
4127
|
res.writeHead(200, {
|
|
3378
4128
|
"Content-Type": contentType,
|
|
@@ -3382,7 +4132,20 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3382
4132
|
res.end();
|
|
3383
4133
|
return;
|
|
3384
4134
|
}
|
|
3385
|
-
|
|
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);
|
|
3386
4149
|
stream.pipe(res);
|
|
3387
4150
|
stream.on("error", () => {
|
|
3388
4151
|
try {
|
|
@@ -3396,10 +4159,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3396
4159
|
sweepIdleCliClients() {
|
|
3397
4160
|
const now = Date.now();
|
|
3398
4161
|
let swept = false;
|
|
3399
|
-
for (const [ws,
|
|
4162
|
+
for (const [ws, simId] of this.cliSimBySocket) {
|
|
3400
4163
|
const lastCommand = this.cliLastCommandAt.get(ws) ?? 0;
|
|
3401
4164
|
if (now - lastCommand < _SootSimBridgeHost.CLI_IDLE_TIMEOUT_MS) continue;
|
|
3402
|
-
this.
|
|
4165
|
+
this.cliSimBySocket.delete(ws);
|
|
3403
4166
|
this.cliLastCommandAt.delete(ws);
|
|
3404
4167
|
for (const [sentId, entry] of this.cliBySentId) {
|
|
3405
4168
|
if (entry.ws === ws) this.cliBySentId.delete(sentId);
|
|
@@ -3411,54 +4174,80 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3411
4174
|
swept = true;
|
|
3412
4175
|
}
|
|
3413
4176
|
if (swept) {
|
|
3414
|
-
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
|
+
}
|
|
3415
4205
|
}
|
|
3416
|
-
this.sweepRestorableBrowsers(now);
|
|
3417
4206
|
}
|
|
3418
|
-
|
|
3419
|
-
return Array.from(this.
|
|
3420
|
-
if (a.id === this.
|
|
3421
|
-
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;
|
|
3422
4211
|
return a.connectedAt - b.connectedAt;
|
|
3423
|
-
}).map((
|
|
4212
|
+
}).map((sim) => this.describeSim(sim));
|
|
3424
4213
|
}
|
|
3425
4214
|
async sendCommand(cmd) {
|
|
3426
|
-
const
|
|
4215
|
+
const sim = await this.waitForSim(cmd.simId);
|
|
3427
4216
|
const id = this.nextCommandId++;
|
|
3428
4217
|
return new Promise((resolve2, reject) => {
|
|
3429
4218
|
const timeout = setTimeout(() => {
|
|
3430
4219
|
this.pendingCommands.delete(id);
|
|
3431
|
-
this.
|
|
4220
|
+
this.broadcastSimClientStates();
|
|
3432
4221
|
reject(new Error("command timed out after 30s"));
|
|
3433
4222
|
}, 3e4);
|
|
3434
4223
|
this.pendingCommands.set(id, {
|
|
3435
|
-
|
|
4224
|
+
simId: sim.id,
|
|
3436
4225
|
resolve: (value) => {
|
|
3437
4226
|
clearTimeout(timeout);
|
|
3438
4227
|
this.pendingCommands.delete(id);
|
|
3439
|
-
this.
|
|
4228
|
+
this.broadcastSimClientStates();
|
|
3440
4229
|
resolve2(value);
|
|
3441
4230
|
},
|
|
3442
4231
|
reject: (error) => {
|
|
3443
4232
|
clearTimeout(timeout);
|
|
3444
4233
|
this.pendingCommands.delete(id);
|
|
3445
|
-
this.
|
|
4234
|
+
this.broadcastSimClientStates();
|
|
3446
4235
|
reject(error);
|
|
3447
4236
|
}
|
|
3448
4237
|
});
|
|
3449
|
-
this.
|
|
3450
|
-
const {
|
|
3451
|
-
|
|
4238
|
+
this.broadcastSimClientStates();
|
|
4239
|
+
const { simId: _simId, ...forwarded } = cmd;
|
|
4240
|
+
sim.ws.send(JSON.stringify({ ...forwarded, id }));
|
|
3452
4241
|
});
|
|
3453
4242
|
}
|
|
3454
|
-
async evaluate(code,
|
|
3455
|
-
return this.sendCommand({ type: "evaluate", code,
|
|
4243
|
+
async evaluate(code, simId) {
|
|
4244
|
+
return this.sendCommand({ type: "evaluate", code, simId });
|
|
3456
4245
|
}
|
|
3457
|
-
async
|
|
3458
|
-
return this.sendCommand({ type: "focus",
|
|
4246
|
+
async focusSim(simId) {
|
|
4247
|
+
return this.sendCommand({ type: "focus", simId });
|
|
3459
4248
|
}
|
|
3460
|
-
async
|
|
3461
|
-
return this.sendCommand({ type: "close",
|
|
4249
|
+
async closeSim(simId) {
|
|
4250
|
+
return this.sendCommand({ type: "close", simId });
|
|
3462
4251
|
}
|
|
3463
4252
|
async openPathInEditor(filePath, line, column) {
|
|
3464
4253
|
const loc = line != null ? `:${line}${column != null ? `:${column}` : ""}` : "";
|
|
@@ -3508,6 +4297,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3508
4297
|
clearInterval(this.heartbeatTimer);
|
|
3509
4298
|
this.heartbeatTimer = null;
|
|
3510
4299
|
}
|
|
4300
|
+
if (this.wsHeartbeatTimer) {
|
|
4301
|
+
clearInterval(this.wsHeartbeatTimer);
|
|
4302
|
+
this.wsHeartbeatTimer = null;
|
|
4303
|
+
}
|
|
3511
4304
|
if (this.runtimeUpdateTimer) {
|
|
3512
4305
|
clearInterval(this.runtimeUpdateTimer);
|
|
3513
4306
|
this.runtimeUpdateTimer = null;
|
|
@@ -3525,11 +4318,11 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3525
4318
|
pending.reject(new Error("server closing"));
|
|
3526
4319
|
this.pendingCommands.delete(id);
|
|
3527
4320
|
}
|
|
3528
|
-
for (const
|
|
3529
|
-
|
|
4321
|
+
for (const sim of this.sims.values()) {
|
|
4322
|
+
sim.ws.close();
|
|
3530
4323
|
}
|
|
3531
|
-
this.
|
|
3532
|
-
this.
|
|
4324
|
+
this.sims.clear();
|
|
4325
|
+
this.primarySimId = null;
|
|
3533
4326
|
const wss = this.wss;
|
|
3534
4327
|
const httpServer = this.httpServer;
|
|
3535
4328
|
this.wss = null;
|
|
@@ -3547,69 +4340,69 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3547
4340
|
}
|
|
3548
4341
|
}
|
|
3549
4342
|
}
|
|
3550
|
-
|
|
4343
|
+
describeSim(sim) {
|
|
3551
4344
|
let readyState;
|
|
3552
4345
|
try {
|
|
3553
|
-
readyState =
|
|
4346
|
+
readyState = sim.ws.readyState;
|
|
3554
4347
|
} catch {
|
|
3555
4348
|
readyState = import_ws.WebSocket.CLOSED;
|
|
3556
4349
|
}
|
|
3557
|
-
const lease = this.getActiveLease(
|
|
4350
|
+
const lease = this.getActiveLease(sim);
|
|
3558
4351
|
return {
|
|
3559
|
-
id:
|
|
3560
|
-
origin:
|
|
3561
|
-
url:
|
|
3562
|
-
title:
|
|
3563
|
-
userAgent:
|
|
3564
|
-
connectedAt:
|
|
3565
|
-
lastSeenAt:
|
|
3566
|
-
lastActiveAt:
|
|
3567
|
-
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,
|
|
3568
4361
|
readyState: readyState === import_ws.WebSocket.OPEN ? "open" : readyState === import_ws.WebSocket.CLOSING ? "closing" : "closed",
|
|
3569
|
-
attachedCliCount: this.getAttachedCliCount(
|
|
3570
|
-
lockedBy: lease ? lease.cliLabel || lease.
|
|
4362
|
+
attachedCliCount: this.getAttachedCliCount(sim.id),
|
|
4363
|
+
lockedBy: lease ? lease.cliLabel || lease.cliIdentityKey : void 0,
|
|
3571
4364
|
lockedByKind: lease ? lease.kind : void 0,
|
|
3572
4365
|
lockExpiresAt: lease ? lease.expiresAt : void 0,
|
|
3573
|
-
userFocused:
|
|
4366
|
+
userFocused: sim.userFocused || void 0
|
|
3574
4367
|
};
|
|
3575
4368
|
}
|
|
3576
|
-
getActiveLease(
|
|
3577
|
-
const lease =
|
|
4369
|
+
getActiveLease(sim) {
|
|
4370
|
+
const lease = sim.cliLease;
|
|
3578
4371
|
if (!lease) return null;
|
|
3579
4372
|
if (Date.now() >= lease.expiresAt) {
|
|
3580
|
-
|
|
4373
|
+
sim.cliLease = void 0;
|
|
3581
4374
|
return null;
|
|
3582
4375
|
}
|
|
3583
4376
|
return lease;
|
|
3584
4377
|
}
|
|
3585
|
-
tryAcquireLease(ws,
|
|
3586
|
-
const
|
|
4378
|
+
tryAcquireLease(ws, sim, opts = {}) {
|
|
4379
|
+
const cliIdentityKey = this.cliIdentityKeyBySocket.get(ws) ?? (() => {
|
|
3587
4380
|
const fallback = `ws-${this.nextCliFallbackId++}`;
|
|
3588
|
-
this.
|
|
4381
|
+
this.cliIdentityKeyBySocket.set(ws, fallback);
|
|
3589
4382
|
return fallback;
|
|
3590
4383
|
})();
|
|
3591
4384
|
const cliLabel = this.cliLabelBySocket.get(ws);
|
|
3592
4385
|
const now = Date.now();
|
|
3593
|
-
const existing = this.getActiveLease(
|
|
3594
|
-
const ownerMatches = existing && existing.
|
|
4386
|
+
const existing = this.getActiveLease(sim);
|
|
4387
|
+
const ownerMatches = existing && existing.cliIdentityKey === cliIdentityKey;
|
|
3595
4388
|
let bootedCount = 0;
|
|
3596
4389
|
if (existing && !ownerMatches && !opts.force) {
|
|
3597
4390
|
return {
|
|
3598
4391
|
granted: false,
|
|
3599
4392
|
lease: existing,
|
|
3600
4393
|
lock: {
|
|
3601
|
-
by: existing.cliLabel || existing.
|
|
4394
|
+
by: existing.cliLabel || existing.cliIdentityKey,
|
|
3602
4395
|
expiresInMs: Math.max(0, existing.expiresAt - now)
|
|
3603
4396
|
},
|
|
3604
4397
|
bootedCount: 0
|
|
3605
4398
|
};
|
|
3606
4399
|
}
|
|
3607
4400
|
if (existing && !ownerMatches && opts.force) {
|
|
3608
|
-
for (const [cliWs,
|
|
3609
|
-
if (
|
|
3610
|
-
const otherKey = this.
|
|
3611
|
-
if (otherKey && otherKey !==
|
|
3612
|
-
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);
|
|
3613
4406
|
try {
|
|
3614
4407
|
cliWs.close(1e3, "lease claimed by another cli");
|
|
3615
4408
|
} catch {
|
|
@@ -3620,135 +4413,129 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3620
4413
|
}
|
|
3621
4414
|
const lease = {
|
|
3622
4415
|
kind: "cli",
|
|
3623
|
-
|
|
4416
|
+
cliIdentityKey,
|
|
3624
4417
|
cliLabel,
|
|
3625
4418
|
expiresAt: now + _SootSimBridgeHost.CLI_LEASE_TTL_MS
|
|
3626
4419
|
};
|
|
3627
|
-
|
|
4420
|
+
sim.cliLease = lease;
|
|
3628
4421
|
return { granted: true, lease, bootedCount };
|
|
3629
4422
|
}
|
|
3630
|
-
// 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
|
|
3631
4424
|
// show "focused" alongside any cli lease, but focus alone never creates a
|
|
3632
|
-
// 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
|
|
3633
4426
|
// locked out agent inspect calls for 15s — the opposite of what you want
|
|
3634
4427
|
// when debugging something the user is actively looking at. use
|
|
3635
4428
|
// updateUserActivity() to lock on real interaction instead.
|
|
3636
|
-
updateUserFocusLease(
|
|
4429
|
+
updateUserFocusLease(sim, focused) {
|
|
3637
4430
|
const next = focused;
|
|
3638
|
-
if (
|
|
3639
|
-
|
|
3640
|
-
this.
|
|
4431
|
+
if (sim.userFocused === next) return;
|
|
4432
|
+
sim.userFocused = next;
|
|
4433
|
+
this.broadcastSimClientStates();
|
|
3641
4434
|
}
|
|
3642
|
-
// called when the
|
|
4435
|
+
// called when the sim reports a real user interaction (pointerdown,
|
|
3643
4436
|
// keydown, wheel, touch). creates or refreshes a short `user-active` lease
|
|
3644
|
-
// 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.
|
|
3645
4438
|
// reads still pass through — shouldAcquireLease only blocks on writes.
|
|
3646
|
-
updateUserActivity(
|
|
3647
|
-
const existing = this.getActiveLease(
|
|
4439
|
+
updateUserActivity(sim) {
|
|
4440
|
+
const existing = this.getActiveLease(sim);
|
|
3648
4441
|
if (existing && existing.kind === "cli") {
|
|
3649
4442
|
return;
|
|
3650
4443
|
}
|
|
3651
4444
|
const now = Date.now();
|
|
3652
4445
|
const refreshed = now + _SootSimBridgeHost.USER_ACTIVE_LEASE_TTL_MS;
|
|
3653
4446
|
const expiresAt = existing && existing.kind === "user-active" ? Math.max(existing.expiresAt, refreshed) : refreshed;
|
|
3654
|
-
|
|
4447
|
+
sim.cliLease = {
|
|
3655
4448
|
kind: "user-active",
|
|
3656
|
-
|
|
4449
|
+
cliIdentityKey: "__user-active__",
|
|
3657
4450
|
cliLabel: "active user",
|
|
3658
4451
|
expiresAt
|
|
3659
4452
|
};
|
|
3660
|
-
this.
|
|
4453
|
+
this.broadcastSimClientStates();
|
|
3661
4454
|
}
|
|
3662
|
-
|
|
3663
|
-
const existing = this.
|
|
4455
|
+
ensureCliIdentityKey(ws) {
|
|
4456
|
+
const existing = this.cliIdentityKeyBySocket.get(ws);
|
|
3664
4457
|
if (existing) return existing;
|
|
3665
4458
|
const fallback = `ws-${this.nextCliFallbackId++}`;
|
|
3666
|
-
this.
|
|
4459
|
+
this.cliIdentityKeyBySocket.set(ws, fallback);
|
|
3667
4460
|
return fallback;
|
|
3668
4461
|
}
|
|
3669
|
-
|
|
3670
|
-
if (
|
|
3671
|
-
const
|
|
3672
|
-
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;
|
|
3673
4466
|
return null;
|
|
3674
4467
|
}
|
|
3675
|
-
const primary = this.
|
|
4468
|
+
const primary = this.primarySimId != null ? this.sims.get(this.primarySimId) : null;
|
|
3676
4469
|
if (primary?.ws.readyState === import_ws.WebSocket.OPEN) return primary;
|
|
3677
|
-
for (const
|
|
3678
|
-
if (
|
|
4470
|
+
for (const sim of this.sims.values()) {
|
|
4471
|
+
if (sim.ws.readyState === import_ws.WebSocket.OPEN) return sim;
|
|
3679
4472
|
}
|
|
3680
4473
|
return null;
|
|
3681
4474
|
}
|
|
3682
|
-
async
|
|
4475
|
+
async waitForSim(simId, options = {}) {
|
|
3683
4476
|
const attempts = options.attempts ?? 10;
|
|
3684
4477
|
const intervalMs = options.intervalMs ?? 200;
|
|
3685
4478
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
3686
|
-
const
|
|
3687
|
-
if (
|
|
4479
|
+
const sim = this.getOpenSim(simId);
|
|
4480
|
+
if (sim) return sim;
|
|
3688
4481
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
3689
4482
|
}
|
|
3690
|
-
throw new Error(
|
|
3691
|
-
browserId ? `no browser connected with id ${browserId}` : "no browser connected"
|
|
3692
|
-
);
|
|
4483
|
+
throw new Error(simId ? `no sim connected with id ${simId}` : "no sim connected");
|
|
3693
4484
|
}
|
|
3694
|
-
|
|
3695
|
-
const current = this.
|
|
3696
|
-
const isPrimaryCandidate =
|
|
4485
|
+
shouldPromoteSim(sim) {
|
|
4486
|
+
const current = this.primarySimId ? this.sims.get(this.primarySimId) : null;
|
|
4487
|
+
const isPrimaryCandidate = sim.origin?.includes(":5173");
|
|
3697
4488
|
const currentIsPrimary = current?.origin?.includes(":5173");
|
|
3698
4489
|
return !current || current.ws.readyState !== import_ws.WebSocket.OPEN || !!isPrimaryCandidate || !currentIsPrimary;
|
|
3699
4490
|
}
|
|
3700
|
-
|
|
3701
|
-
for (const
|
|
3702
|
-
if (
|
|
3703
|
-
|
|
4491
|
+
broadcastSimAssignments() {
|
|
4492
|
+
for (const sim of this.sims.values()) {
|
|
4493
|
+
if (sim.ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
4494
|
+
sim.ws.send(
|
|
3704
4495
|
JSON.stringify({
|
|
3705
4496
|
type: "bridge:welcome",
|
|
3706
|
-
|
|
3707
|
-
isPrimary:
|
|
4497
|
+
simId: sim.id,
|
|
4498
|
+
isPrimary: sim.id === this.primarySimId
|
|
3708
4499
|
})
|
|
3709
4500
|
);
|
|
3710
4501
|
}
|
|
3711
4502
|
}
|
|
3712
|
-
|
|
3713
|
-
for (const
|
|
3714
|
-
if (
|
|
3715
|
-
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);
|
|
3716
4507
|
const message = {
|
|
3717
4508
|
type: "bridge:client-state",
|
|
3718
|
-
attachedCliCount: this.getAttachedCliCount(
|
|
3719
|
-
activeAgentCommandCount: this.getActiveAgentCommandCount(
|
|
3720
|
-
recentActions:
|
|
3721
|
-
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,
|
|
3722
4513
|
lockedByKind: lease ? lease.kind : void 0,
|
|
3723
4514
|
lockExpiresAt: lease ? lease.expiresAt : void 0,
|
|
3724
|
-
userFocused:
|
|
4515
|
+
userFocused: sim.userFocused || void 0
|
|
3725
4516
|
};
|
|
3726
|
-
|
|
4517
|
+
sim.ws.send(JSON.stringify(message));
|
|
3727
4518
|
}
|
|
3728
4519
|
}
|
|
3729
|
-
|
|
3730
|
-
const
|
|
3731
|
-
if (
|
|
3732
|
-
this.
|
|
3733
|
-
this.
|
|
3734
|
-
|
|
3735
|
-
prevBrowserId ? "cli switched tabs" : "cli connected",
|
|
3736
|
-
false
|
|
3737
|
-
);
|
|
3738
|
-
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();
|
|
3739
4526
|
}
|
|
3740
|
-
|
|
4527
|
+
recordSimAction(simId, label, broadcast = true) {
|
|
3741
4528
|
const normalized = label?.trim();
|
|
3742
4529
|
if (!normalized) return;
|
|
3743
|
-
const
|
|
3744
|
-
if (!
|
|
4530
|
+
const sim = this.sims.get(simId);
|
|
4531
|
+
if (!sim) return;
|
|
3745
4532
|
const now = Date.now();
|
|
3746
|
-
|
|
3747
|
-
|
|
4533
|
+
sim.lastActiveAt = now;
|
|
4534
|
+
sim.recentActions = [
|
|
3748
4535
|
{ label: normalized, at: now },
|
|
3749
|
-
...
|
|
4536
|
+
...sim.recentActions.filter((entry) => entry.label !== normalized)
|
|
3750
4537
|
].slice(0, 4);
|
|
3751
|
-
if (broadcast) this.
|
|
4538
|
+
if (broadcast) this.broadcastSimClientStates();
|
|
3752
4539
|
}
|
|
3753
4540
|
describeForwardedCommand(msg) {
|
|
3754
4541
|
switch (msg?.type) {
|
|
@@ -3763,90 +4550,97 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3763
4550
|
case "tree":
|
|
3764
4551
|
return "dumped tree";
|
|
3765
4552
|
case "focus":
|
|
3766
|
-
return "focused
|
|
4553
|
+
return "focused sim";
|
|
3767
4554
|
case "close":
|
|
3768
4555
|
return "requested close";
|
|
3769
4556
|
default:
|
|
3770
4557
|
return typeof msg?.type === "string" ? msg.type : null;
|
|
3771
4558
|
}
|
|
3772
4559
|
}
|
|
3773
|
-
// count distinct cli
|
|
4560
|
+
// count distinct cli identity keys attached to a sim, not raw sockets.
|
|
3774
4561
|
// a single agent firing sequential cli commands opens a new ws per call —
|
|
3775
4562
|
// counting sockets would report phantom peers until idle cleanup catches up.
|
|
3776
|
-
getAttachedCliCount(
|
|
4563
|
+
getAttachedCliCount(simId) {
|
|
3777
4564
|
const keys = /* @__PURE__ */ new Set();
|
|
3778
|
-
for (const [ws,
|
|
3779
|
-
if (
|
|
4565
|
+
for (const [ws, attachedSimId] of this.cliSimBySocket) {
|
|
4566
|
+
if (attachedSimId !== simId) continue;
|
|
3780
4567
|
if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
3781
|
-
const key = this.
|
|
4568
|
+
const key = this.cliIdentityKeyBySocket.get(ws);
|
|
3782
4569
|
keys.add(key ?? `ws-unknown-${keys.size}`);
|
|
3783
4570
|
}
|
|
3784
4571
|
return keys.size;
|
|
3785
4572
|
}
|
|
3786
|
-
// count distinct
|
|
3787
|
-
// used to warn a cli that other agents/
|
|
3788
|
-
|
|
3789
|
-
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);
|
|
3790
4577
|
const keys = /* @__PURE__ */ new Set();
|
|
3791
|
-
for (const [ws,
|
|
3792
|
-
if (
|
|
4578
|
+
for (const [ws, attachedSimId] of this.cliSimBySocket) {
|
|
4579
|
+
if (attachedSimId !== simId) continue;
|
|
3793
4580
|
if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
|
|
3794
|
-
const key = this.
|
|
4581
|
+
const key = this.cliIdentityKeyBySocket.get(ws);
|
|
3795
4582
|
if (key && key === selfKey) continue;
|
|
3796
4583
|
keys.add(key ?? `ws-unknown-${keys.size}`);
|
|
3797
4584
|
}
|
|
3798
4585
|
return keys.size;
|
|
3799
4586
|
}
|
|
3800
|
-
getActiveAgentCommandCount(
|
|
4587
|
+
getActiveAgentCommandCount(simId) {
|
|
3801
4588
|
let count = 0;
|
|
3802
4589
|
for (const pending of this.pendingCommands.values()) {
|
|
3803
|
-
if (pending.
|
|
4590
|
+
if (pending.simId === simId) count++;
|
|
3804
4591
|
}
|
|
3805
4592
|
return count;
|
|
3806
4593
|
}
|
|
3807
|
-
|
|
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) {
|
|
3808
4602
|
const nextId = requestedId?.trim();
|
|
3809
|
-
if (!nextId || nextId ===
|
|
3810
|
-
const existing = this.
|
|
3811
|
-
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) {
|
|
3812
4606
|
return false;
|
|
3813
4607
|
}
|
|
3814
|
-
const restorable = this.
|
|
3815
|
-
const prevId =
|
|
3816
|
-
this.
|
|
3817
|
-
|
|
4608
|
+
const restorable = this.getRestorableSimState(nextId);
|
|
4609
|
+
const prevId = sim.id;
|
|
4610
|
+
this.sims.delete(prevId);
|
|
4611
|
+
sim.id = nextId;
|
|
3818
4612
|
if (restorable) {
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
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);
|
|
3823
4617
|
}
|
|
3824
|
-
this.
|
|
3825
|
-
if (this.
|
|
3826
|
-
this.
|
|
4618
|
+
this.sims.set(sim.id, sim);
|
|
4619
|
+
if (this.primarySimId === prevId) {
|
|
4620
|
+
this.primarySimId = sim.id;
|
|
3827
4621
|
}
|
|
3828
|
-
for (const [ws,
|
|
3829
|
-
if (
|
|
3830
|
-
this.
|
|
4622
|
+
for (const [ws, attachedSimId] of this.cliSimBySocket) {
|
|
4623
|
+
if (attachedSimId === prevId) {
|
|
4624
|
+
this.cliSimBySocket.set(ws, sim.id);
|
|
3831
4625
|
}
|
|
3832
4626
|
}
|
|
3833
4627
|
return true;
|
|
3834
4628
|
}
|
|
3835
|
-
|
|
3836
|
-
const lease = this.getActiveLease(
|
|
3837
|
-
this.
|
|
3838
|
-
recentActions:
|
|
3839
|
-
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,
|
|
3840
4634
|
cliLease: lease && lease.kind === "cli" ? { ...lease } : void 0,
|
|
3841
|
-
expiresAt: Date.now() + _SootSimBridgeHost.
|
|
4635
|
+
expiresAt: Date.now() + _SootSimBridgeHost.SIM_RECONNECT_TTL_MS
|
|
3842
4636
|
});
|
|
3843
|
-
this.
|
|
4637
|
+
this.sims.delete(sim.id);
|
|
3844
4638
|
}
|
|
3845
|
-
|
|
3846
|
-
const snapshot = this.
|
|
4639
|
+
getRestorableSimState(simId) {
|
|
4640
|
+
const snapshot = this.restorableSims.get(simId);
|
|
3847
4641
|
if (!snapshot) return null;
|
|
3848
4642
|
if (snapshot.expiresAt <= Date.now()) {
|
|
3849
|
-
this.
|
|
4643
|
+
this.restorableSims.delete(simId);
|
|
3850
4644
|
return null;
|
|
3851
4645
|
}
|
|
3852
4646
|
if (snapshot.cliLease && snapshot.cliLease.expiresAt <= Date.now()) {
|
|
@@ -3854,13 +4648,13 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3854
4648
|
}
|
|
3855
4649
|
return snapshot;
|
|
3856
4650
|
}
|
|
3857
|
-
|
|
3858
|
-
for (const [
|
|
4651
|
+
sweepRestorableSims(now = Date.now()) {
|
|
4652
|
+
for (const [simId, snapshot] of this.restorableSims) {
|
|
3859
4653
|
if (snapshot.expiresAt > now) continue;
|
|
3860
|
-
this.
|
|
3861
|
-
for (const [cliWs,
|
|
3862
|
-
if (
|
|
3863
|
-
this.
|
|
4654
|
+
this.restorableSims.delete(simId);
|
|
4655
|
+
for (const [cliWs, attachedSimId] of this.cliSimBySocket) {
|
|
4656
|
+
if (attachedSimId === simId) {
|
|
4657
|
+
this.cliSimBySocket.delete(cliWs);
|
|
3864
4658
|
}
|
|
3865
4659
|
}
|
|
3866
4660
|
}
|
|
@@ -3870,6 +4664,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
|
|
|
3870
4664
|
clearInterval(this.cliIdleTimer);
|
|
3871
4665
|
this.cliIdleTimer = null;
|
|
3872
4666
|
}
|
|
4667
|
+
if (this.wsHeartbeatTimer) {
|
|
4668
|
+
clearInterval(this.wsHeartbeatTimer);
|
|
4669
|
+
this.wsHeartbeatTimer = null;
|
|
4670
|
+
}
|
|
3873
4671
|
if (this.runtimeUpdateTimer) {
|
|
3874
4672
|
clearInterval(this.runtimeUpdateTimer);
|
|
3875
4673
|
this.runtimeUpdateTimer = null;
|