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.
Files changed (161) hide show
  1. package/README.md +20 -5
  2. package/dist-cli/bin.js +15 -20
  3. package/dist-cli/chunks/{agent-YZB6D3DR.js → agent-EQRQGSBL.js} +2 -2
  4. package/dist-cli/chunks/{agent-wrapper-VHCVS22I.js → agent-wrapper-AWKZ67GN.js} +10 -10
  5. package/dist-cli/chunks/{assert-AIVCKKLG.js → assert-ZVGELUZB.js} +2 -2
  6. package/dist-cli/chunks/auto-bootstrap-UEOLNAWJ.js +2 -0
  7. package/dist-cli/chunks/beta-4MD7WSI4.js +2 -0
  8. package/dist-cli/chunks/chunk-2ZPJHSIJ.js +11 -0
  9. package/dist-cli/chunks/{chunk-A5BRCXYE.js → chunk-4IO3D5XG.js} +1 -1
  10. package/dist-cli/chunks/chunk-4OHVCGMF.js +2 -0
  11. package/dist-cli/chunks/chunk-56BIMCDH.js +2 -0
  12. package/dist-cli/chunks/chunk-5FLDI6CV.js +66 -0
  13. package/dist-cli/chunks/{chunk-LHDWH7VS.js → chunk-B3RAGRK6.js} +1 -1
  14. package/dist-cli/chunks/{chunk-27P763IZ.js → chunk-BGAPLYMS.js} +2 -2
  15. package/dist-cli/chunks/chunk-CX3ZIPD3.js +3 -0
  16. package/dist-cli/chunks/{chunk-HWCKZXNJ.js → chunk-DSTV2VJT.js} +2 -2
  17. package/dist-cli/chunks/chunk-EDBFYOQB.js +2 -0
  18. package/dist-cli/chunks/chunk-ERLA3F77.js +1 -0
  19. package/dist-cli/chunks/chunk-FCQLQ7NA.js +117 -0
  20. package/dist-cli/chunks/chunk-H2HSOHXN.js +7 -0
  21. package/dist-cli/chunks/chunk-HYYMBXIX.js +2 -0
  22. package/dist-cli/chunks/chunk-JMGDVXAV.js +3 -0
  23. package/dist-cli/chunks/chunk-JMU5IGIU.js +1 -0
  24. package/dist-cli/chunks/chunk-KA5JJCWL.js +1 -0
  25. package/dist-cli/chunks/chunk-L4F4JRKJ.js +348 -0
  26. package/dist-cli/chunks/{chunk-G7XQD4KC.js → chunk-LDWXH43L.js} +2 -2
  27. package/dist-cli/chunks/chunk-PERKPZ7T.js +4 -0
  28. package/dist-cli/chunks/chunk-PN6FWLD4.js +5 -0
  29. package/dist-cli/chunks/chunk-QD7YIVPS.js +64 -0
  30. package/dist-cli/chunks/chunk-QWKO62QM.js +2 -0
  31. package/dist-cli/chunks/{chunk-VFDRZNPN.js → chunk-QXMZNJV5.js} +1 -1
  32. package/dist-cli/chunks/chunk-R77F5J3X.js +4 -0
  33. package/dist-cli/chunks/chunk-RLNIKWFO.js +27 -0
  34. package/dist-cli/chunks/chunk-RX6RHGSI.js +2 -0
  35. package/dist-cli/chunks/{chunk-IJMYFYDZ.js → chunk-S74RCIVB.js} +2 -2
  36. package/dist-cli/chunks/chunk-SK4SOISL.js +1 -0
  37. package/dist-cli/chunks/{chunk-YIO6S3R5.js → chunk-T5L73GJB.js} +1 -1
  38. package/dist-cli/chunks/{chunk-KAXZHEKM.js → chunk-UIQ3536J.js} +1 -1
  39. package/dist-cli/chunks/chunk-URSEYCC5.js +16 -0
  40. package/dist-cli/chunks/chunk-WFXYY3DU.js +3 -0
  41. package/dist-cli/chunks/{chunk-EWSQSALM.js → chunk-WHLHA5R5.js} +4 -4
  42. package/dist-cli/chunks/chunk-WLIVBPPY.js +3 -0
  43. package/dist-cli/chunks/{chunk-CYCXOAVZ.js → chunk-X6BP5JFC.js} +4 -4
  44. package/dist-cli/chunks/chunk-YFXTO4QX.js +5 -0
  45. package/dist-cli/chunks/{chunk-RMW5BO3S.js → chunk-Z5SVSAZO.js} +2 -2
  46. package/dist-cli/chunks/{chunk-OXN2PEB7.js → chunk-Z5X3PITK.js} +3 -3
  47. package/dist-cli/chunks/chunk-ZBOIGEGO.js +5 -0
  48. package/dist-cli/chunks/chunk-ZERYEI3L.js +17 -0
  49. package/dist-cli/chunks/{compat-Y2O2U7FL.js → compat-QQ3OJDBI.js} +2 -2
  50. package/dist-cli/chunks/{config-SRBOFUCI.js → config-LT27SC25.js} +2 -2
  51. package/dist-cli/chunks/control-3BO54QMO.js +2 -0
  52. package/dist-cli/chunks/cpu-profile-XEO3JCVB.js +22 -0
  53. package/dist-cli/chunks/daemon-3J2SAVQZ.js +83 -0
  54. package/dist-cli/chunks/{debug-BIDMW2PE.js → debug-OGQLIH4U.js} +4 -4
  55. package/dist-cli/chunks/demo-app-registry-5RZCXLWB.js +2 -0
  56. package/dist-cli/chunks/detox-Z2OSCIQU.js +49 -0
  57. package/dist-cli/chunks/device-RPTVD25S.js +16 -0
  58. package/dist-cli/chunks/diagnose-LAEXBNOQ.js +41 -0
  59. package/dist-cli/chunks/drivers-PSQUUAYC.js +2 -0
  60. package/dist-cli/chunks/electron-S2463O3P.js +18 -0
  61. package/dist-cli/chunks/flow-34YCVQDB.js +2 -0
  62. package/dist-cli/chunks/hints-E5PXPWFT.js +2 -0
  63. package/dist-cli/chunks/home-paths-F5SGBTRZ.js +2 -0
  64. package/dist-cli/chunks/inspect-EVGMEZ3G.js +1101 -0
  65. package/dist-cli/chunks/install-AM5PTJT3.js +2 -0
  66. package/dist-cli/chunks/{install-desktop-2MYEI4FM.js → install-desktop-ZNWYKTWQ.js} +3 -3
  67. package/dist-cli/chunks/{keys-7PNASIQR.js → keys-5ETF6DYO.js} +2 -2
  68. package/dist-cli/chunks/{launch-JNS47LAQ.js → launch-DHUCNFX6.js} +3 -3
  69. package/dist-cli/chunks/{login-YWZWUHBS.js → login-KDR34JIP.js} +4 -4
  70. package/dist-cli/chunks/{logout-O6SXMSBP.js → logout-R6WIJYCW.js} +2 -2
  71. package/dist-cli/chunks/maestro-ZOOJ2YVH.js +80 -0
  72. package/dist-cli/chunks/{preview-WGKJO5FS.js → preview-YFADHNBD.js} +2 -2
  73. package/dist-cli/chunks/profile-CQSC32HB.js +22 -0
  74. package/dist-cli/chunks/react-QSQD6CJE.js +30 -0
  75. package/dist-cli/chunks/{record-QPWLYH5R.js → record-IWLEYATN.js} +5 -5
  76. package/dist-cli/chunks/{runtime-KEMO2MSB.js → runtime-WKMNKYTN.js} +3 -3
  77. package/dist-cli/chunks/screenshot-VJXHV57I.js +28 -0
  78. package/dist-cli/chunks/screenshot-mode-FA4VQ76K.js +17 -0
  79. package/dist-cli/chunks/screenshots-U4FQXHVK.js +70 -0
  80. package/dist-cli/chunks/server-7WZLM5NQ.js +35 -0
  81. package/dist-cli/chunks/setup-repo-3BXLAX5E.js +2 -0
  82. package/dist-cli/chunks/{skills-MO7BFNVM.js → skills-KO7RCY24.js} +2 -2
  83. package/dist-cli/chunks/start-EBD7T2GW.js +23 -0
  84. package/dist-cli/chunks/store-ONX3EBS4.js +2 -0
  85. package/dist-cli/chunks/telemetry-MFR7TUW7.js +2 -0
  86. package/dist-cli/chunks/{test-XUI3KNNQ.js → test-OSVUG54G.js} +3 -3
  87. package/dist-cli/chunks/three-mode-MDBXZQG4.js +39 -0
  88. package/dist-cli/chunks/timeline-UJOKZKQR.js +22 -0
  89. package/dist-cli/chunks/upload-H2SMWP6T.js +2 -0
  90. package/dist-cli/chunks/what-happened-LFWH74FR.js +15 -0
  91. package/dist-cli/chunks/whoami-CUF56TLP.js +2 -0
  92. package/dist-lib/agent-daemon-client.cjs +4 -1
  93. package/dist-lib/agent-events.cjs +1 -1
  94. package/dist-lib/agent-sessions.cjs +41 -39
  95. package/dist-lib/attached-projects.cjs +30 -28
  96. package/dist-lib/auth/shared-session.cjs +35 -27
  97. package/dist-lib/backend-origin.cjs +1 -1
  98. package/dist-lib/bridge-constants.cjs +1 -1
  99. package/dist-lib/cli-constants.cjs +1 -1
  100. package/dist-lib/config.cjs +6 -2
  101. package/dist-lib/dev-bundle-resolution.cjs +5 -21
  102. package/dist-lib/home-paths.cjs +94 -38
  103. package/dist-lib/host/bridge-host.cjs +2131 -1333
  104. package/dist-lib/host/fetch-proxy-handler.cjs +248 -0
  105. package/dist-lib/index.cjs +21 -21
  106. package/dist-lib/metro.cjs +21 -21
  107. package/dist-lib/profiles.cjs +246 -0
  108. package/dist-lib/render-mode.cjs +1 -1
  109. package/dist-lib/vite-base.cjs +3402 -1640
  110. package/dist-lib/vite.cjs +1 -1
  111. package/package.json +7 -1
  112. package/dist-cli/chunks/auto-bootstrap-MLNTX23H.js +0 -2
  113. package/dist-cli/chunks/chunk-3UIWOHC2.js +0 -62
  114. package/dist-cli/chunks/chunk-5KGFHWVR.js +0 -1
  115. package/dist-cli/chunks/chunk-5QIUJNT3.js +0 -5
  116. package/dist-cli/chunks/chunk-6GGMKFWJ.js +0 -4
  117. package/dist-cli/chunks/chunk-6Z275LCY.js +0 -2
  118. package/dist-cli/chunks/chunk-75LBYBKW.js +0 -11
  119. package/dist-cli/chunks/chunk-DFN3GGH7.js +0 -5
  120. package/dist-cli/chunks/chunk-EBEHZJRG.js +0 -117
  121. package/dist-cli/chunks/chunk-EJLNUMMP.js +0 -3
  122. package/dist-cli/chunks/chunk-FE7UI3MT.js +0 -4
  123. package/dist-cli/chunks/chunk-G663654J.js +0 -1
  124. package/dist-cli/chunks/chunk-GW7XY5KC.js +0 -2
  125. package/dist-cli/chunks/chunk-H2QO4TDV.js +0 -22
  126. package/dist-cli/chunks/chunk-HWFHBMAQ.js +0 -27
  127. package/dist-cli/chunks/chunk-J7CTD37P.js +0 -1
  128. package/dist-cli/chunks/chunk-N32NCVL2.js +0 -3
  129. package/dist-cli/chunks/chunk-NIZBR7EK.js +0 -2
  130. package/dist-cli/chunks/chunk-NYY36OKU.js +0 -308
  131. package/dist-cli/chunks/chunk-PJL25JQV.js +0 -5
  132. package/dist-cli/chunks/chunk-SHO54NET.js +0 -2
  133. package/dist-cli/chunks/chunk-SMVJOWSV.js +0 -16
  134. package/dist-cli/chunks/chunk-TC6V7YFC.js +0 -3
  135. package/dist-cli/chunks/chunk-YLIIVTTQ.js +0 -3
  136. package/dist-cli/chunks/chunk-YR7BGGYE.js +0 -2
  137. package/dist-cli/chunks/chunk-ZEW3RF5Q.js +0 -1
  138. package/dist-cli/chunks/control-PL2V2O6S.js +0 -2
  139. package/dist-cli/chunks/daemon-IZC32PZW.js +0 -50
  140. package/dist-cli/chunks/demo-app-registry-5JFOUU3D.js +0 -2
  141. package/dist-cli/chunks/detox-B3FDOIS3.js +0 -49
  142. package/dist-cli/chunks/device-ZZSI363W.js +0 -16
  143. package/dist-cli/chunks/drivers-S4NGK4DB.js +0 -2
  144. package/dist-cli/chunks/electron-5YFHXEOI.js +0 -15
  145. package/dist-cli/chunks/flow-JJBO6TFY.js +0 -2
  146. package/dist-cli/chunks/hints-G5HBBV2O.js +0 -2
  147. package/dist-cli/chunks/home-paths-VWC3FWA3.js +0 -2
  148. package/dist-cli/chunks/inspect-POOPWUQI.js +0 -1034
  149. package/dist-cli/chunks/install-MP6FHXNZ.js +0 -2
  150. package/dist-cli/chunks/install-dev-desktop-SKH3KEHY.js +0 -100
  151. package/dist-cli/chunks/maestro-CW6XVUKV.js +0 -75
  152. package/dist-cli/chunks/profile-SUOBRPIC.js +0 -22
  153. package/dist-cli/chunks/screenshot-JTY46V7G.js +0 -26
  154. package/dist-cli/chunks/screenshot-mode-7OYBBX6D.js +0 -17
  155. package/dist-cli/chunks/screenshots-QISKC4GD.js +0 -70
  156. package/dist-cli/chunks/server-YSFJAKAV.js +0 -34
  157. package/dist-cli/chunks/setup-repo-LFB3HBEO.js +0 -2
  158. package/dist-cli/chunks/store-6MFL53I4.js +0 -2
  159. package/dist-cli/chunks/telemetry-CN42GMVC.js +0 -2
  160. package/dist-cli/chunks/upload-6FUT7AX5.js +0 -2
  161. package/dist-cli/chunks/whoami-TQFHY42N.js +0 -2
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.36 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
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 import_fs4 = __toESM(require("fs"), 1);
40
- var import_http2 = require("http");
41
- var import_path4 = __toESM(require("path"), 1);
39
+ var import_fs3 = __toESM(require("fs"), 1);
40
+ var import_http3 = require("http");
41
+ var import_path3 = __toESM(require("path"), 1);
42
42
  var import_ws = require("ws");
43
43
 
44
- // src/bridge-constants.ts
45
- var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
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/home-paths.ts
48
- var import_fs = __toESM(require("fs"), 1);
49
- var import_os = require("os");
50
- var import_path = __toESM(require("path"), 1);
51
- var SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
52
- var ACTIVE_RUNTIME_FILE = "active";
53
- var DAEMON_LOCKFILE = "daemon.json";
54
- var 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 ensureSootsimHome() {
80
- import_fs.default.mkdirSync(sootsimHomeDir(), { recursive: true });
81
- import_fs.default.mkdirSync(runtimesDir(), { recursive: true });
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 readActiveRuntime() {
85
- try {
86
- const value = import_fs.default.readFileSync(activeRuntimeFile(), "utf8").trim();
87
- return value.length > 0 ? value : null;
88
- } catch {
89
- return null;
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
- function writeActiveRuntime(version) {
93
- import_fs.default.mkdirSync(runtimesDir(), { recursive: true });
94
- import_fs.default.writeFileSync(activeRuntimeFile(), `${version}
95
- `, "utf8");
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 compareSemver(a, b) {
105
- const parse = (v) => {
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 activeRuntimeDir() {
126
- const version = readActiveRuntime();
127
- if (!version) return null;
128
- const dir = runtimeDir(version);
76
+ function normalizeNativeDevBundleUrl(bundleUrl) {
129
77
  try {
130
- if (import_fs.default.statSync(dir).isDirectory()) return dir;
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
- var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
136
- function readDaemonLockfile() {
137
- try {
138
- const fd = import_fs.default.openSync(daemonLockfilePath(), "r");
139
- try {
140
- const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
141
- const bytesRead = import_fs.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
142
- const raw = buf.subarray(0, bytesRead).toString("utf8");
143
- const parsed = JSON.parse(raw);
144
- 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") {
145
- return parsed;
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
- } catch {
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 isDaemonLockfileFresh(lock, now = Date.now()) {
156
- if (!lock) return false;
157
- if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
158
- try {
159
- process.kill(lock.pid, 0);
160
- return true;
161
- } catch {
162
- return false;
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
- function writeDaemonLockfile(data) {
166
- ensureSootsimHome();
167
- const tmp = `${daemonLockfilePath()}.tmp`;
168
- import_fs.default.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
169
- `, "utf8");
170
- import_fs.default.renameSync(tmp, daemonLockfilePath());
171
- }
172
- function claimDaemonLockfile(data) {
173
- ensureSootsimHome();
174
- const existing = readDaemonLockfile();
175
- if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
176
- return false;
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
- writeDaemonLockfile(data);
179
- return true;
181
+ return env;
180
182
  }
181
- function removeDaemonLockfile() {
182
- try {
183
- import_fs.default.unlinkSync(daemonLockfilePath());
184
- } catch {
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
- // src/runtime-delivery.ts
189
- var import_child_process = require("child_process");
190
- var import_crypto = __toESM(require("crypto"), 1);
191
- var import_fs2 = __toESM(require("fs"), 1);
192
- var import_path2 = __toESM(require("path"), 1);
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 resolveRuntimeCdnOrigin(input) {
207
- const config = readConfig();
208
- const value = input || process.env[RUNTIME_CDN_ORIGIN_ENV] || config.cdnOrigin || DEFAULT_RUNTIME_CDN_ORIGIN;
209
- return value.replace(/\/+$/, "");
210
- }
211
- function resolveRuntimeChannel(input) {
212
- const config = readConfig();
213
- return input || process.env[RUNTIME_CHANNEL_ENV] || config.runtimeChannel || "stable";
214
- }
215
- function runtimeManifestUrl(cdnOrigin) {
216
- const url = new URL(`${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/manifest.json`);
217
- url.searchParams.set("t", String(Date.now()));
218
- return url.toString();
219
- }
220
- function runtimeTarballUrl(version, cdnOrigin) {
221
- return `${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/sootsim-runtime-${version}.tar.gz`;
222
- }
223
- async function fetchRuntimeManifest(cdnOrigin) {
224
- const url = runtimeManifestUrl(cdnOrigin);
225
- const res = await fetch(url, { headers: { Accept: "application/json" } });
226
- if (!res.ok) {
227
- throw new Error(`manifest fetch failed: ${res.status} ${res.statusText} (${url})`);
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
- return await res.json();
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 resolveRuntimeVersion(manifest, opts = {}) {
232
- const channel = resolveRuntimeChannel(opts.channel);
233
- const version = opts.version || manifest.channels[channel]?.latest;
234
- if (!version) {
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 entry = manifest.versions[version];
240
- if (!entry) {
241
- throw new Error(
242
- `version ${version} not found in manifest; available: ${Object.keys(manifest.versions).slice(-10).join(", ") || "(none)"}`
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
- return { version, channel, entry };
251
+ lines.push("");
252
+ (0, import_node_fs.writeFileSync)(UNISWAP_ENV_LOCAL_FILE, `${lines.join("\n")}
253
+ `);
246
254
  }
247
- async function installRuntime(opts = {}) {
248
- ensureSootsimHome();
249
- const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin);
250
- const manifest = await fetchRuntimeManifest(cdnOrigin);
251
- const { version, channel, entry } = resolveRuntimeVersion(manifest, opts);
252
- const destDir = runtimeDir(version);
253
- const setActive = opts.setActive !== false;
254
- if (!opts.force && import_fs2.default.existsSync(import_path2.default.join(destDir, "index.html"))) {
255
- if (setActive) writeActiveRuntime(version);
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
- const tarUrl = entry.tarball || runtimeTarballUrl(version, cdnOrigin);
267
- const tarCachePath = import_path2.default.join(cacheDir(), `sootsim-runtime-${version}.tar.gz`);
268
- process.stderr.write(`sootsim: downloading runtime ${version}\u2026
269
- `);
270
- await downloadToFile(tarUrl, tarCachePath);
271
- process.stderr.write(`sootsim: extracting runtime ${version}\u2026
272
- `);
273
- const actualSha = await sha256File(tarCachePath);
274
- if (actualSha !== entry.sha256) {
275
- import_fs2.default.rmSync(tarCachePath, { force: true });
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
- `sha256 mismatch for runtime ${version}: expected ${entry.sha256}, actual ${actualSha}`
355
+ `joplin demo: yarn buildParallel did not produce: ${stillMissing.join(", ")}`
278
356
  );
279
357
  }
280
- const tmpDir = import_path2.default.join(import_path2.default.dirname(destDir), `.installing-${version}-${process.pid}`);
281
- import_fs2.default.rmSync(tmpDir, { recursive: true, force: true });
282
- import_fs2.default.mkdirSync(tmpDir, { recursive: true });
283
- try {
284
- await extractTarball(tarCachePath, tmpDir);
285
- if (!import_fs2.default.existsSync(import_path2.default.join(tmpDir, "index.html"))) {
286
- throw new Error(`extracted tarball for runtime ${version} is missing index.html`);
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
- import_fs2.default.rmSync(destDir, { recursive: true, force: true });
289
- import_fs2.default.renameSync(tmpDir, destDir);
290
- } catch (err) {
291
- import_fs2.default.rmSync(tmpDir, { recursive: true, force: true });
292
- throw err;
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
- if (setActive) writeActiveRuntime(version);
295
- return {
296
- version,
297
- channel,
298
- cdnOrigin,
299
- runtimeDir: destDir,
300
- installed: true,
301
- activated: setActive,
302
- manifest
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
- async function updateRuntimeToLatest(opts = {}) {
306
- ensureSootsimHome();
307
- const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin);
308
- const channel = resolveRuntimeChannel(opts.channel);
309
- const manifest = await fetchRuntimeManifest(cdnOrigin);
310
- const latestVersion = manifest.channels[channel]?.latest;
311
- if (!latestVersion) {
312
- return {
313
- checked: true,
314
- updated: false,
315
- reason: `channel '${channel}' has no latest runtime`,
316
- activeVersion: readActiveRuntime()
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
- const entry = manifest.versions[latestVersion];
320
- if (!entry) {
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
- const activeVersion = readActiveRuntime();
330
- const activeDir = activeVersion ? runtimeDir(activeVersion) : null;
331
- const activeInstalled = activeDir ? import_fs2.default.existsSync(import_path2.default.join(activeDir, "index.html")) : false;
332
- const shouldInstall = !activeVersion || !activeInstalled || compareSemver(latestVersion, activeVersion) > 0;
333
- if (!shouldInstall) {
334
- return {
335
- checked: true,
336
- updated: false,
337
- reason: "active runtime is current",
338
- activeVersion,
339
- latestVersion
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
- async function downloadToFile(url, destPath) {
357
- const res = await fetch(url);
358
- if (!res.ok || !res.body) {
359
- throw new Error(`download failed: ${res.status} ${res.statusText} (${url})`);
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
- import_fs2.default.mkdirSync(import_path2.default.dirname(destPath), { recursive: true });
362
- const tmp = `${destPath}.partial`;
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
- function sha256File(filePath) {
378
- return new Promise((resolve2, reject) => {
379
- const hash = import_crypto.default.createHash("sha256");
380
- const stream = import_fs2.default.createReadStream(filePath);
381
- stream.on("data", (chunk) => hash.update(chunk));
382
- stream.on("error", reject);
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 isAbsolute = isAbsoluteHttpUrl(bundleUrl);
438
- const parsed = new URL(bundleUrl, "http://soot.local");
439
- parsed.pathname = parsed.pathname.replace(/\.\.bundle$/, ".bundle");
440
- if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl;
441
- if (!parsed.searchParams.has("dev")) parsed.searchParams.set("dev", "true");
442
- if (!parsed.searchParams.has("minify")) {
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 bundleUrl;
532
+ return void 0;
450
533
  }
451
534
  }
452
-
453
- // scripts/demo-app-registry.ts
454
- var import_node_fs = require("node:fs");
455
- var import_node_os = require("node:os");
456
- var import_node_path = require("node:path");
457
- var HOME = (0, import_node_os.homedir)();
458
- function findWorkspaceRoot(startDir) {
459
- let dir = startDir;
460
- while (true) {
461
- 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"))) {
462
- return dir;
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
- function resolveWorkspaceScriptPath(workspaceRelativePath, packageRelativePath) {
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
- return candidates[0] ?? (0, import_node_path.resolve)(process.cwd(), workspaceRelativePath);
550
+ if (Object.keys(env).length === 0) return void 0;
551
+ return {
552
+ env
553
+ };
488
554
  }
489
- var getExpensifyProxyScript = () => resolveWorkspaceScriptPath(
490
- "packages/sootsim-engine/scripts/expensify-web-proxy.ts",
491
- "scripts/expensify-web-proxy.ts"
492
- );
493
- var EXPENSIFY_NATIVE_PROXY_ENV = {
494
- USE_NGROK: "true",
495
- NGROK_URL: "http://localhost:9000/",
496
- SECURE_NGROK_URL: "http://localhost:9000/"
497
- };
498
- var UNISWAP_REPO_DIR = (0, import_node_path.join)(HOME, "github/uniswap-interface");
499
- var UNISWAP_APP_DIR = (0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/mobile");
500
- var UNISWAP_ENV_LOCAL_FILE = (0, import_node_path.join)(UNISWAP_REPO_DIR, ".env.defaults.local");
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
- return env;
528
- }
529
- function isUsableUniswapEnvValue(value) {
530
- if (!value) return false;
531
- const trimmed = value.trim();
532
- if (!trimmed) return false;
533
- if (trimmed.includes(UNISWAP_PLACEHOLDER)) return false;
534
- if (trimmed === "TRADING_API_KEY" || trimmed === "UNISWAP_API_KEY") return false;
535
- return true;
536
- }
537
- function pickEnvValue(sources, keys) {
538
- for (const source of sources) {
539
- for (const key of keys) {
540
- const value = source[key];
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
- return void 0;
545
- }
546
- function resolveUniswapDemoEnv() {
547
- const localEnv = parseEnvFile(UNISWAP_ENV_LOCAL_FILE);
548
- const webEnv = parseEnvFile((0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/web/.env"));
549
- const sources = [
550
- process.env,
551
- localEnv,
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
- const hasPrivateGatewayKeys = isUsableUniswapEnvValue(env.TRADING_API_KEY) && isUsableUniswapEnvValue(env.UNISWAP_API_KEY);
577
- if (!hasPrivateGatewayKeys) {
578
- const publicGraphqlUrl = pickEnvValue(sources, ["GRAPHQL_URL_OVERRIDE", "REACT_APP_AWS_API_ENDPOINT"]) || "https://interface.gateway.uniswap.org/v1/graphql";
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
- command: (p) => ({ cmd: "npx one dev", env: { ONE_PORT: String(p) } })
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
- // scripts/dev-server-scanner.ts
830
- var execP = (0, import_util.promisify)(import_child_process2.exec);
831
- var TIMEOUT_MS = 250;
832
- var TCP_GATE_MS = 120;
833
- function tcpPing(port, timeout = TCP_GATE_MS) {
834
- return new Promise((resolve2) => {
835
- const sock = new import_net.default.Socket();
836
- let settled = false;
837
- const done = (ok) => {
838
- if (settled) return;
839
- settled = true;
840
- sock.destroy();
841
- resolve2(ok);
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
- sock.setTimeout(timeout);
844
- sock.once("connect", () => done(true));
845
- sock.once("timeout", () => done(false));
846
- sock.once("error", () => done(false));
847
- sock.connect(port, "localhost");
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 httpGet(port, path7, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
851
- return new Promise((resolve2) => {
852
- const req = import_http.default.request(
853
- { hostname: "localhost", port, path: path7, method, timeout, headers },
854
- (res) => {
855
- let body = "";
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
- var FALLBACK_PORTS = [
869
- 8081,
870
- 8082,
871
- 8083,
872
- 8084,
873
- 8085,
874
- 8086,
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
- async function discoverListeningProcesses(excludePorts = []) {
886
- const excluded = new Set(excludePorts);
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
- const { stdout } = await execP(
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
- const { stdout } = await execP(`ss -tlnp 2>/dev/null | grep -E '"(node|bun)"'`, {
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 FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded));
1334
+ return null;
934
1335
  }
935
- var cwdByPid = /* @__PURE__ */ new Map();
936
- async function resolveProcessCwd(pid) {
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 { stdout } = await execP(`lsof -p ${pid} -a -d cwd -Fn 2>/dev/null`, {
942
- encoding: "utf8",
943
- timeout: 1500
944
- });
945
- for (const line of stdout.split("\n")) {
946
- if (line.startsWith("n") && line.length > 1) {
947
- const cwd = line.slice(1).trim();
948
- if (cwd) {
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 makeResult(port, framework) {
959
- return {
960
- port,
961
- framework,
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 withRuntimeConfig(port, bundleUrl) {
971
- const knownApp = APPS.find((app) => app.preferredPort === port);
972
- const configured = knownApp?.runtimeConfig ? applySootSimConfigToUrl(bundleUrl, knownApp.runtimeConfig) : bundleUrl;
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 isDirectOneBundleUrl(bundleUrl) {
976
- return bundleUrl.includes("/node_modules/one/metro-entry.bundle");
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 applyManifest(result, manifestRes, buildIconProxyUrl) {
979
- if (!manifestRes) return result;
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
- var knownNonPatched = /* @__PURE__ */ new Set();
1011
- var knownNonExpo = /* @__PURE__ */ new Set();
1012
- async function probePort(port, buildIconProxyUrl) {
1013
- if (!await tcpPing(port)) return null;
1014
- const onePath = `/node_modules/one/metro-entry.bundle?platform=ios&dev=true`;
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
- if (oneRes && oneRes.statusCode > 0 && oneRes.statusCode < 400) {
1028
- knownNonPatched.add(port);
1029
- return applyManifest(
1030
- {
1031
- port,
1032
- framework: "one",
1033
- bundleUrl: withRuntimeConfig(
1034
- port,
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
- if (statusRes && statusRes.body.includes("packager-status:running")) {
1045
- knownNonPatched.add(port);
1046
- return applyManifest(
1047
- makeResult(port, expoRes && expoRes.statusCode === 200 ? "expo" : "metro"),
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
- if (manifestRes) {
1053
- try {
1054
- const manifest = JSON.parse(manifestRes.body);
1055
- const client = manifest?.extra?.expoClient || {};
1056
- if (client.name) {
1057
- const launchUrl2 = manifest?.launchAsset?.url || `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`;
1058
- knownNonPatched.add(port);
1059
- return applyManifest(
1060
- {
1061
- port,
1062
- framework: "one",
1063
- bundleUrl: withRuntimeConfig(port, launchUrl2),
1064
- hmrUrl: `ws://localhost:${port}/hot`,
1065
- lastSeen: Date.now()
1066
- },
1067
- manifestRes,
1068
- buildIconProxyUrl
1069
- );
1070
- }
1071
- } catch {
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
- if (sootsimRes && sootsimRes.statusCode === 200 && sootsimRes.body.includes("sootsim-patched")) {
1075
- knownNonPatched.delete(port);
1076
- return applyManifest(
1077
- {
1078
- port,
1079
- framework: "one",
1080
- bundleUrl: withRuntimeConfig(port, `http://localhost:${port}/__soot/bundle.js`),
1081
- hmrUrl: `ws://localhost:${port}/hot`,
1082
- lastSeen: Date.now(),
1083
- patched: true
1084
- },
1085
- manifestRes,
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
- knownNonPatched.add(port);
1090
- return null;
1091
- }
1092
- function isSootSelfServer(server) {
1093
- const projectName = server.projectName?.trim().toLowerCase();
1094
- if (projectName === "soot" || projectName === "sootsim") return true;
1095
- const bundleId = server.bundleId?.trim().toLowerCase();
1096
- if (bundleId?.startsWith("dev.soot")) return true;
1097
- return false;
1098
- }
1099
- var portCache = /* @__PURE__ */ new Map();
1100
- var NEGATIVE_CACHE_TTL_MS = 3e4;
1101
- var WEAK_RESULT_CACHE_TTL_MS = 1500;
1102
- function isWeakCachedResult(result) {
1103
- if (!result) return true;
1104
- if (result.framework === "metro" || result.framework === "unknown") return true;
1105
- return false;
1106
- }
1107
- function hasCurrentRuntimeConfig(result) {
1108
- if (!result) return true;
1109
- return withRuntimeConfig(result.port, result.bundleUrl) === result.bundleUrl;
1110
- }
1111
- function __shouldReuseScannerCacheEntry(entry, pid, now = Date.now()) {
1112
- if (pid === 0) return false;
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 scanDevServers(opts = {}) {
1121
- const processes = await discoverListeningProcesses(opts.excludePorts);
1122
- const currentPorts = new Set(processes.map((p) => p.port));
1123
- for (const p of [...portCache.keys()]) {
1124
- if (!currentPorts.has(p)) portCache.delete(p);
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
- for (const p of [...knownNonPatched]) {
1127
- if (!currentPorts.has(p)) knownNonPatched.delete(p);
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
- for (const p of [...knownNonExpo]) {
1130
- if (!currentPorts.has(p)) knownNonExpo.delete(p);
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 results = [];
1133
- const toProbe = [];
1134
- for (const { port, pid } of processes) {
1135
- const cached = portCache.get(port);
1136
- if (cached && __shouldReuseScannerCacheEntry(cached, pid)) {
1137
- if (cached.result) results.push(cached.result);
1138
- continue;
1139
- }
1140
- if (cached && cached.pid !== pid) {
1141
- knownNonPatched.delete(port);
1142
- knownNonExpo.delete(port);
1143
- }
1144
- toProbe.push({ port, pid });
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
- if (toProbe.length > 0) {
1147
- const probed = await Promise.all(
1148
- toProbe.map((p) => probePort(p.port, opts.buildIconProxyUrl))
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
- probed.forEach((result, i) => {
1151
- const { port, pid } = toProbe[i];
1152
- if (pid !== 0) portCache.set(port, { pid, result, cachedAt: Date.now() });
1153
- if (result) results.push(result);
1154
- });
1155
- }
1156
- const pidByPort = /* @__PURE__ */ new Map();
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 import_node_fs3 = __toESM(require("node:fs"), 1);
1180
- var import_node_path3 = __toESM(require("node:path"), 1);
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 import_node_fs2 = __toESM(require("node:fs"), 1);
1225
- var import_node_os2 = __toESM(require("node:os"), 1);
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
- try {
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 import_node_path2.default.join(userDataDir(), "attached-projects.json");
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 = import_node_fs2.default.readFileSync(file, "utf8");
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
- import_node_fs2.default.renameSync(file, quarantine);
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
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
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 = import_node_fs2.default.openSync(tmp, "w", 384);
1709
+ const fd = import_node_fs3.default.openSync(tmp, "w", 384);
1299
1710
  try {
1300
- import_node_fs2.default.writeFileSync(fd, JSON.stringify(store, null, 2));
1301
- import_node_fs2.default.fsyncSync(fd);
1711
+ import_node_fs3.default.writeFileSync(fd, JSON.stringify(store, null, 2));
1712
+ import_node_fs3.default.fsyncSync(fd);
1302
1713
  } finally {
1303
- import_node_fs2.default.closeSync(fd);
1714
+ import_node_fs3.default.closeSync(fd);
1304
1715
  }
1305
- import_node_fs2.default.renameSync(tmp, file);
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(import_node_path2.default.resolve(cwd)).digest("hex").slice(0, 16);
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 = import_node_path2.default.resolve(input.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 ?? import_node_path2.default.basename(cwd),
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 (!import_node_fs2.default.existsSync(app.dir)) continue;
1487
- const cwd = import_node_path2.default.resolve(app.dir);
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 import_node_path3.default.join(getUserDataDir(), "sessions", sessionId);
1922
+ return import_node_path4.default.join(getUserDataDir(), "sessions", sessionId);
1512
1923
  }
1513
1924
  function promptFifoPath(sessionId) {
1514
- return import_node_path3.default.join(sessionDir(sessionId), "prompt.in");
1925
+ return import_node_path4.default.join(sessionDir(sessionId), "prompt.in");
1515
1926
  }
1516
1927
  function eventsFifoPath(sessionId) {
1517
- return import_node_path3.default.join(sessionDir(sessionId), "events.out");
1928
+ return import_node_path4.default.join(sessionDir(sessionId), "events.out");
1518
1929
  }
1519
1930
  function transcriptPath(sessionId) {
1520
- return import_node_path3.default.join(getUserDataDir(), "transcripts", `${sessionId}.log`);
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 (!import_node_fs3.default.existsSync(sessionDir(sessionId))) return false;
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
- import_node_path3.default.join(resourcesPath, "bin", "sootsim"),
1543
- import_node_path3.default.join(resourcesPath, "bin", `sootsim-${process.platform}-${process.arch}`)
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 (import_node_fs3.default.existsSync(c)) return { cmd: c, prefixArgs: [] };
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 = import_node_path3.default.join(sootsimDir, "dist-bin", binaryName);
1570
- if (import_node_fs3.default.existsSync(distBinary)) return { cmd: distBinary, prefixArgs: [] };
1571
- const distBin = import_node_path3.default.join(sootsimDir, "dist-cli", "bin.js");
1572
- if (import_node_fs3.default.existsSync(distBin)) {
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 = import_node_path3.default.join(sootsimDir, "cli", "commands", "agent-wrapper.ts");
1575
- if (import_node_fs3.default.existsSync(src)) {
1576
- const srcMtime = import_node_fs3.default.statSync(src).mtimeMs;
1577
- const buildMtime = import_node_fs3.default.statSync(distBin).mtimeMs;
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 import_node_path3.default.dirname(resolved);
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 = import_node_path3.default.dirname(here);
2012
+ let cur = import_node_path4.default.dirname(here);
1602
2013
  for (let i = 0; i < 8; i++) {
1603
- const pkg = import_node_path3.default.join(cur, "package.json");
2014
+ const pkg = import_node_path4.default.join(cur, "package.json");
1604
2015
  try {
1605
- if (import_node_fs3.default.existsSync(pkg)) {
1606
- const parsed = JSON.parse(import_node_fs3.default.readFileSync(pkg, "utf8"));
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 = import_node_path3.default.dirname(cur);
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 = import_node_path3.default.join(getUserDataDir(), "locks");
1628
- import_node_fs3.default.mkdirSync(lockDir, { recursive: true });
2038
+ const lockDir = import_node_path4.default.join(getUserDataDir(), "locks");
2039
+ import_node_fs4.default.mkdirSync(lockDir, { recursive: true });
1629
2040
  try {
1630
- import_node_fs3.default.chmodSync(lockDir, 448);
2041
+ import_node_fs4.default.chmodSync(lockDir, 448);
1631
2042
  } catch {
1632
2043
  }
1633
- const lockPath = import_node_path3.default.join(lockDir, `start-${projectId}-${provider}.lock`);
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 = import_node_fs3.default.openSync(
2049
+ fd = import_node_fs4.default.openSync(
1639
2050
  lockPath,
1640
- import_node_fs3.constants.O_WRONLY | import_node_fs3.constants.O_CREAT | import_node_fs3.constants.O_EXCL,
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(import_node_fs3.default.readFileSync(lockPath, "utf8").trim());
2057
+ const stale = Number(import_node_fs4.default.readFileSync(lockPath, "utf8").trim());
1647
2058
  if (stale && !isProcessAlive(stale)) {
1648
- import_node_fs3.default.unlinkSync(lockPath);
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
- import_node_fs3.default.writeFileSync(fd, String(process.pid));
2073
+ import_node_fs4.default.writeFileSync(fd, String(process.pid));
1663
2074
  return await fn();
1664
2075
  } finally {
1665
2076
  try {
1666
- import_node_fs3.default.closeSync(fd);
2077
+ import_node_fs4.default.closeSync(fd);
1667
2078
  } catch {
1668
2079
  }
1669
2080
  try {
1670
- import_node_fs3.default.unlinkSync(lockPath);
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 = import_node_path3.default.dirname(p);
1685
- import_node_fs3.default.mkdirSync(parent, { recursive: true });
2095
+ const parent = import_node_path4.default.dirname(p);
2096
+ import_node_fs4.default.mkdirSync(parent, { recursive: true });
1686
2097
  try {
1687
- import_node_fs3.default.chmodSync(parent, 448);
2098
+ import_node_fs4.default.chmodSync(parent, 448);
1688
2099
  } catch {
1689
2100
  }
1690
- if (import_node_fs3.default.existsSync(p)) {
2101
+ if (import_node_fs4.default.existsSync(p)) {
1691
2102
  try {
1692
- const stat = import_node_fs3.default.statSync(p);
2103
+ const stat = import_node_fs4.default.statSync(p);
1693
2104
  if (stat.isFIFO()) {
1694
2105
  try {
1695
- import_node_fs3.default.chmodSync(p, 384);
2106
+ import_node_fs4.default.chmodSync(p, 384);
1696
2107
  } catch {
1697
2108
  }
1698
2109
  return;
1699
2110
  }
1700
- import_node_fs3.default.unlinkSync(p);
2111
+ import_node_fs4.default.unlinkSync(p);
1701
2112
  } catch {
1702
- import_node_fs3.default.unlinkSync(p);
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 = import_node_path3.default.dirname(transcript);
1750
- import_node_fs3.default.mkdirSync(transcriptDir, { recursive: true });
2160
+ const transcriptDir = import_node_path4.default.dirname(transcript);
2161
+ import_node_fs4.default.mkdirSync(transcriptDir, { recursive: true });
1751
2162
  try {
1752
- import_node_fs3.default.chmodSync(transcriptDir, 448);
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
- import_node_fs3.default.rmSync(sessionDir(session.id), { recursive: true, force: true });
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 (!import_node_fs3.default.existsSync(fifo)) {
2242
+ if (!import_node_fs4.default.existsSync(fifo)) {
1832
2243
  throw new AgentSessionError("NO_FIFO", `prompt FIFO missing: ${fifo}`);
1833
2244
  }
1834
- const fd = import_node_fs3.default.openSync(fifo, import_node_fs3.constants.O_WRONLY);
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
- import_node_fs3.default.writeSync(fd, wireText + "\n");
2251
+ import_node_fs4.default.writeSync(fd, wireText + "\n");
1841
2252
  } finally {
1842
- import_node_fs3.default.closeSync(fd);
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
- import_node_fs3.default.rmSync(dir, { recursive: true, force: true });
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 (!import_node_fs3.default.existsSync(fifo)) {
2283
+ if (!import_node_fs4.default.existsSync(fifo)) {
1873
2284
  throw new AgentSessionError("NO_FIFO", `events FIFO missing: ${fifo}`);
1874
2285
  }
1875
- const fd = import_node_fs3.default.openSync(fifo, import_node_fs3.constants.O_RDWR);
1876
- const stream = import_node_fs3.default.createReadStream("", { fd, autoClose: true });
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 = import_node_fs3.default.openSync(fifo, import_node_fs3.constants.O_RDWR | import_node_fs3.constants.O_NONBLOCK);
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 = import_node_fs3.default.readSync(fd, buf, 0, buf.length, null);
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
- import_node_fs3.default.closeSync(fd);
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 ?? import_node_path4.default.basename(match.cwd),
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 (!import_node_fs4.default.existsSync(p)) {
2518
+ if (!import_node_fs5.default.existsSync(p)) {
2108
2519
  return { error: "transcript not found", code: "NO_TRANSCRIPT" };
2109
2520
  }
2110
- return import_node_fs4.default.readFileSync(p, "utf8");
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: import_node_path4.default.join(dir, "attached-projects.json"),
2117
- sessionsDir: import_node_path4.default.join(dir, "sessions"),
2118
- transcriptsDir: import_node_path4.default.join(dir, "transcripts")
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 import_fs3 = require("fs");
2381
- var import_os2 = require("os");
2382
- var import_path3 = require("path");
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, import_path3.join)((0, import_os2.homedir)(), candidate.slice(2)) : candidate;
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, import_fs3.existsSync)(expanded)) return expanded;
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, import_fs3.existsSync)(envCandidate)) return envCandidate;
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
- nextBrowserId = 1;
2568
- browsers = /* @__PURE__ */ new Map();
2569
- primaryBrowserId = null;
3193
+ nextSimNumber = 161;
3194
+ sims = /* @__PURE__ */ new Map();
3195
+ primarySimId = null;
2570
3196
  pendingCommands = /* @__PURE__ */ new Map();
2571
3197
  cliBySentId = /* @__PURE__ */ new Map();
2572
- cliBrowserBySocket = /* @__PURE__ */ new Map();
3198
+ cliSimBySocket = /* @__PURE__ */ new Map();
2573
3199
  cliLastCommandAt = /* @__PURE__ */ new Map();
2574
- cliSessionKeyBySocket = /* @__PURE__ */ new Map();
3200
+ cliIdentityKeyBySocket = /* @__PURE__ */ new Map();
2575
3201
  cliLabelBySocket = /* @__PURE__ */ new Map();
2576
- restorableBrowsers = /* @__PURE__ */ new Map();
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 tab to take it over)
2584
- // hold the tab longer than passive canvas interaction so reconnecting clis
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 BROWSER_RECONNECT_TTL_MS = 3e4;
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, import_http2.createServer)((req, res) => this.handleHttpRequest(req, res));
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 ? "browser" : "cli";
2691
- let browser = null;
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 === "browser") {
2696
- browser = {
2697
- id: `tab-${this.nextBrowserId++}`,
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.browsers.set(browser.id, browser);
2706
- if (this.shouldPromoteBrowser(browser)) {
2707
- this.primaryBrowserId = browser.id;
3353
+ this.sims.set(sim.id, sim);
3354
+ if (this.shouldPromoteSim(sim)) {
3355
+ this.primarySimId = sim.id;
2708
3356
  }
2709
- this.broadcastBrowserAssignments();
2710
- this.broadcastBrowserClientStates();
3357
+ this.broadcastSimAssignments();
3358
+ this.broadcastSimClientStates();
2711
3359
  } else {
2712
3360
  const fallbackKey = `ws-${this.nextCliFallbackId++}`;
2713
- this.cliSessionKeyBySocket.set(ws, fallbackKey);
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 === "browser") {
2789
- if (browser) {
2790
- browser.lastSeenAt = Date.now();
3436
+ if (role === "sim") {
3437
+ if (sim) {
3438
+ sim.lastSeenAt = Date.now();
2791
3439
  }
2792
- if (msg.type === "bridge:register" && browser) {
3440
+ if (msg.type === "bridge:register" && sim) {
2793
3441
  const registration = msg;
2794
- const restored = this.tryRestoreBrowserId(browser, registration.browserId);
2795
- browser.url = registration.url;
2796
- browser.title = registration.title;
2797
- browser.userAgent = registration.userAgent;
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.broadcastBrowserAssignments();
2800
- this.broadcastBrowserClientStates();
3447
+ this.broadcastSimAssignments();
3448
+ this.broadcastSimClientStates();
2801
3449
  }
2802
3450
  return;
2803
3451
  }
2804
- if (msg.type === "bridge:user-focus-state" && browser) {
3452
+ if (msg.type === "bridge:user-focus-state" && sim) {
2805
3453
  const focusState = msg;
2806
- this.updateUserFocusLease(browser, focusState.focused === true);
3454
+ this.updateUserFocusLease(sim, focusState.focused === true);
2807
3455
  return;
2808
3456
  }
2809
- if (msg.type === "bridge:user-interact" && browser) {
2810
- this.updateUserActivity(browser);
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" && browser) {
3496
+ if (msg.type === "bridge:boot-clients" && sim) {
2823
3497
  const booted = [];
2824
- for (const [cliWs, attachedBrowserId] of this.cliBrowserBySocket) {
2825
- if (attachedBrowserId === browser.id) {
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.cliBrowserBySocket.delete(cliWs);
3504
+ this.cliSimBySocket.delete(cliWs);
2831
3505
  try {
2832
- cliWs.close(1e3, "booted by browser");
3506
+ cliWs.close(1e3, "booted by sim");
2833
3507
  } catch {
2834
3508
  }
2835
3509
  }
2836
- const hadLease = !!browser.cliLease;
2837
- browser.cliLease = {
3510
+ const hadLease = !!sim.cliLease;
3511
+ sim.cliLease = {
2838
3512
  kind: "user-active",
2839
- cliSessionKey: "__user-active__",
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 tab for user [${browser.id}]
3518
+ `sootsim booted ${booted.length} cli client(s)${hadLease ? " (overrode prior lease)" : ""}; held sim for user [${sim.id}]
2845
3519
  `
2846
3520
  );
2847
- this.recordBrowserAction(browser.id, "browser booted cli clients");
2848
- this.broadcastBrowserClientStates();
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.getOtherCliSessionCount(
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 hadBrowser = this.cliBrowserBySocket.delete(ws);
3547
+ const hadSim = this.cliSimBySocket.delete(ws);
2877
3548
  this.cliLastCommandAt.delete(ws);
2878
- this.cliSessionKeyBySocket.delete(ws);
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 (hadBrowser) this.broadcastBrowserClientStates();
3554
+ if (hadSim) this.broadcastSimClientStates();
2884
3555
  return;
2885
3556
  }
2886
3557
  if (msg.type === "bridge:hello") {
2887
- const key = typeof msg.cliSessionKey === "string" && msg.cliSessionKey.trim() ? msg.cliSessionKey.trim() : this.cliSessionKeyBySocket.get(ws) || `ws-${this.nextCliFallbackId++}`;
2888
- this.cliSessionKeyBySocket.set(ws, key);
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
- cliSessionKey: key,
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-browsers") {
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.listBrowsers()
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 targetBrowser2 = await this.waitForBrowser(msg.browserId);
2934
- const outcome = this.tryAcquireLease(ws, targetBrowser2, {
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: `tab ${targetBrowser2.id} is locked by another cli`,
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.setCliBrowserTarget(ws, targetBrowser2.id);
2950
- this.recordBrowserAction(
2951
- targetBrowser2.id,
2952
- outcome.bootedCount > 0 ? `cli force-claimed tab (booted ${outcome.bootedCount})` : "cli claimed tab"
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
- browserId: targetBrowser2.id,
2960
- lockedBy: outcome.lease.cliSessionKey,
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 targetBrowser = await this.waitForBrowser(msg.browserId);
3640
+ const targetSim = await this.waitForSim(msg.simId);
2970
3641
  if (shouldAcquireLease(msg)) {
2971
- const outcome = this.tryAcquireLease(ws, targetBrowser);
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: `tab ${targetBrowser.id} is locked by another cli \u2014 use \`sootsim claim ${targetBrowser.id} --force\` or \`sootsim open --new\``,
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.ensureCliSessionKey(ws);
3656
+ this.ensureCliIdentityKey(ws);
2986
3657
  }
2987
- this.setCliBrowserTarget(ws, targetBrowser.id);
2988
- this.recordBrowserAction(targetBrowser.id, this.describeForwardedCommand(msg));
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
- browserId: targetBrowser.id,
3662
+ simId: targetSim.id,
2992
3663
  ws,
2993
3664
  originalId: msg.id
2994
3665
  });
2995
- const { browserId: _browserId, ...forwarded } = msg;
2996
- targetBrowser.ws.send(JSON.stringify({ ...forwarded, id: sentId }));
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 === "browser" && browser) {
3012
- this.rememberDisconnectedBrowser(browser);
3013
- if (this.primaryBrowserId === browser.id) {
3014
- this.primaryBrowserId = this.getOpenBrowser()?.id ?? null;
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.browserId !== browser.id) continue;
3018
- pending.reject(new Error("browser disconnected"));
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.browserId !== browser.id) continue;
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: "browser disconnected before responding"
3698
+ error: "sim disconnected before responding"
3028
3699
  })
3029
3700
  );
3030
3701
  }
3031
3702
  this.cliBySentId.delete(sentId);
3032
3703
  }
3033
- this.broadcastBrowserAssignments();
3034
- this.broadcastBrowserClientStates();
3704
+ this.broadcastSimAssignments();
3705
+ this.broadcastSimClientStates();
3035
3706
  } else if (role === "cli") {
3036
- const detached = this.cliBrowserBySocket.delete(ws);
3707
+ const detached = this.cliSimBySocket.delete(ws);
3037
3708
  this.cliLastCommandAt.delete(ws);
3038
- this.cliSessionKeyBySocket.delete(ws);
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.broadcastBrowserClientStates();
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 browsers so electron (or any renderer) can reload. */
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 browser of this.browsers.values()) {
3184
- if (browser.ws.readyState === import_ws.WebSocket.OPEN) {
3899
+ for (const sim of this.sims.values()) {
3900
+ if (sim.ws.readyState === import_ws.WebSocket.OPEN) {
3185
3901
  try {
3186
- browser.ws.send(payload);
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 = import_path4.default.resolve(baseDir, "." + rel);
3324
- const baseWithSep = baseDir.endsWith(import_path4.default.sep) ? baseDir : baseDir + import_path4.default.sep;
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
- import_fs4.default.realpath(resolved, (realErr, realResolved) => {
4075
+ import_fs3.default.realpath(resolved, (realErr, realResolved) => {
3331
4076
  const servePath = realErr ? resolved : realResolved;
3332
- const servePathWithSep = servePath.endsWith(import_path4.default.sep) ? servePath : servePath + import_path4.default.sep;
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 = import_fs4.default.realpathSync(baseDir);
3337
- return rb.endsWith(import_path4.default.sep) ? rb : rb + import_path4.default.sep;
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 + import_path4.default.sep !== realBaseWithSep) {
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
- import_fs4.default.stat(servePath, (err, stats) => {
4093
+ import_fs3.default.stat(servePath, (err, stats) => {
3349
4094
  if (err || !stats?.isFile()) {
3350
- const ext2 = import_path4.default.extname(rel).toLowerCase();
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
- const indexPath = import_path4.default.join(baseDir, "index.html");
3357
- import_fs4.default.readFile(indexPath, (err2, data) => {
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 = import_path4.default.extname(servePath).toLowerCase();
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
- const stream = import_fs4.default.createReadStream(servePath);
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, browserId] of this.cliBrowserBySocket) {
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.cliBrowserBySocket.delete(ws);
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.broadcastBrowserClientStates();
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
- listBrowsers() {
3419
- return Array.from(this.browsers.values()).sort((a, b) => {
3420
- if (a.id === this.primaryBrowserId) return -1;
3421
- if (b.id === this.primaryBrowserId) return 1;
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((browser) => this.describeBrowser(browser));
4212
+ }).map((sim) => this.describeSim(sim));
3424
4213
  }
3425
4214
  async sendCommand(cmd) {
3426
- const browser = await this.waitForBrowser(cmd.browserId);
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.broadcastBrowserClientStates();
4220
+ this.broadcastSimClientStates();
3432
4221
  reject(new Error("command timed out after 30s"));
3433
4222
  }, 3e4);
3434
4223
  this.pendingCommands.set(id, {
3435
- browserId: browser.id,
4224
+ simId: sim.id,
3436
4225
  resolve: (value) => {
3437
4226
  clearTimeout(timeout);
3438
4227
  this.pendingCommands.delete(id);
3439
- this.broadcastBrowserClientStates();
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.broadcastBrowserClientStates();
4234
+ this.broadcastSimClientStates();
3446
4235
  reject(error);
3447
4236
  }
3448
4237
  });
3449
- this.broadcastBrowserClientStates();
3450
- const { browserId: _browserId, ...forwarded } = cmd;
3451
- browser.ws.send(JSON.stringify({ ...forwarded, id }));
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, browserId) {
3455
- return this.sendCommand({ type: "evaluate", code, browserId });
4243
+ async evaluate(code, simId) {
4244
+ return this.sendCommand({ type: "evaluate", code, simId });
3456
4245
  }
3457
- async focusBrowser(browserId) {
3458
- return this.sendCommand({ type: "focus", browserId });
4246
+ async focusSim(simId) {
4247
+ return this.sendCommand({ type: "focus", simId });
3459
4248
  }
3460
- async closeBrowser(browserId) {
3461
- return this.sendCommand({ type: "close", browserId });
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 browser of this.browsers.values()) {
3529
- browser.ws.close();
4321
+ for (const sim of this.sims.values()) {
4322
+ sim.ws.close();
3530
4323
  }
3531
- this.browsers.clear();
3532
- this.primaryBrowserId = null;
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
- describeBrowser(browser) {
4343
+ describeSim(sim) {
3551
4344
  let readyState;
3552
4345
  try {
3553
- readyState = browser.ws.readyState;
4346
+ readyState = sim.ws.readyState;
3554
4347
  } catch {
3555
4348
  readyState = import_ws.WebSocket.CLOSED;
3556
4349
  }
3557
- const lease = this.getActiveLease(browser);
4350
+ const lease = this.getActiveLease(sim);
3558
4351
  return {
3559
- id: browser.id,
3560
- origin: browser.origin,
3561
- url: browser.url,
3562
- title: browser.title,
3563
- userAgent: browser.userAgent,
3564
- connectedAt: browser.connectedAt,
3565
- lastSeenAt: browser.lastSeenAt,
3566
- lastActiveAt: browser.lastActiveAt || void 0,
3567
- isPrimary: browser.id === this.primaryBrowserId,
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(browser.id),
3570
- lockedBy: lease ? lease.cliLabel || lease.cliSessionKey : void 0,
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: browser.userFocused || void 0
4366
+ userFocused: sim.userFocused || void 0
3574
4367
  };
3575
4368
  }
3576
- getActiveLease(browser) {
3577
- const lease = browser.cliLease;
4369
+ getActiveLease(sim) {
4370
+ const lease = sim.cliLease;
3578
4371
  if (!lease) return null;
3579
4372
  if (Date.now() >= lease.expiresAt) {
3580
- browser.cliLease = void 0;
4373
+ sim.cliLease = void 0;
3581
4374
  return null;
3582
4375
  }
3583
4376
  return lease;
3584
4377
  }
3585
- tryAcquireLease(ws, browser, opts = {}) {
3586
- const cliSessionKey = this.cliSessionKeyBySocket.get(ws) ?? (() => {
4378
+ tryAcquireLease(ws, sim, opts = {}) {
4379
+ const cliIdentityKey = this.cliIdentityKeyBySocket.get(ws) ?? (() => {
3587
4380
  const fallback = `ws-${this.nextCliFallbackId++}`;
3588
- this.cliSessionKeyBySocket.set(ws, fallback);
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(browser);
3594
- const ownerMatches = existing && existing.cliSessionKey === cliSessionKey;
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.cliSessionKey,
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, attachedBrowserId] of this.cliBrowserBySocket) {
3609
- if (attachedBrowserId !== browser.id) continue;
3610
- const otherKey = this.cliSessionKeyBySocket.get(cliWs);
3611
- if (otherKey && otherKey !== cliSessionKey) {
3612
- this.cliBrowserBySocket.delete(cliWs);
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
- cliSessionKey,
4416
+ cliIdentityKey,
3624
4417
  cliLabel,
3625
4418
  expiresAt: now + _SootSimBridgeHost.CLI_LEASE_TTL_MS
3626
4419
  };
3627
- browser.cliLease = lease;
4420
+ sim.cliLease = lease;
3628
4421
  return { granted: true, lease, bootedCount };
3629
4422
  }
3630
- // user focus is advisory: we track it on the browser record so list/UI can
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 tab
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(browser, focused) {
4429
+ updateUserFocusLease(sim, focused) {
3637
4430
  const next = focused;
3638
- if (browser.userFocused === next) return;
3639
- browser.userFocused = next;
3640
- this.broadcastBrowserClientStates();
4431
+ if (sim.userFocused === next) return;
4432
+ sim.userFocused = next;
4433
+ this.broadcastSimClientStates();
3641
4434
  }
3642
- // called when the browser reports a real user interaction (pointerdown,
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 tab.
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(browser) {
3647
- const existing = this.getActiveLease(browser);
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
- browser.cliLease = {
4447
+ sim.cliLease = {
3655
4448
  kind: "user-active",
3656
- cliSessionKey: "__user-active__",
4449
+ cliIdentityKey: "__user-active__",
3657
4450
  cliLabel: "active user",
3658
4451
  expiresAt
3659
4452
  };
3660
- this.broadcastBrowserClientStates();
4453
+ this.broadcastSimClientStates();
3661
4454
  }
3662
- ensureCliSessionKey(ws) {
3663
- const existing = this.cliSessionKeyBySocket.get(ws);
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.cliSessionKeyBySocket.set(ws, fallback);
4459
+ this.cliIdentityKeyBySocket.set(ws, fallback);
3667
4460
  return fallback;
3668
4461
  }
3669
- getOpenBrowser(browserId) {
3670
- if (browserId) {
3671
- const browser = this.browsers.get(browserId);
3672
- if (browser?.ws.readyState === import_ws.WebSocket.OPEN) return browser;
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.primaryBrowserId != null ? this.browsers.get(this.primaryBrowserId) : null;
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 browser of this.browsers.values()) {
3678
- if (browser.ws.readyState === import_ws.WebSocket.OPEN) return browser;
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 waitForBrowser(browserId, options = {}) {
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 browser = this.getOpenBrowser(browserId);
3687
- if (browser) return browser;
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
- shouldPromoteBrowser(browser) {
3695
- const current = this.primaryBrowserId ? this.browsers.get(this.primaryBrowserId) : null;
3696
- const isPrimaryCandidate = browser.origin?.includes(":5173");
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
- broadcastBrowserAssignments() {
3701
- for (const browser of this.browsers.values()) {
3702
- if (browser.ws.readyState !== import_ws.WebSocket.OPEN) continue;
3703
- browser.ws.send(
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
- browserId: browser.id,
3707
- isPrimary: browser.id === this.primaryBrowserId
4497
+ simId: sim.id,
4498
+ isPrimary: sim.id === this.primarySimId
3708
4499
  })
3709
4500
  );
3710
4501
  }
3711
4502
  }
3712
- broadcastBrowserClientStates() {
3713
- for (const browser of this.browsers.values()) {
3714
- if (browser.ws.readyState !== import_ws.WebSocket.OPEN) continue;
3715
- const lease = this.getActiveLease(browser);
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(browser.id),
3719
- activeAgentCommandCount: this.getActiveAgentCommandCount(browser.id),
3720
- recentActions: browser.recentActions,
3721
- lockedBy: lease ? lease.cliLabel || lease.cliSessionKey : void 0,
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: browser.userFocused || void 0
4515
+ userFocused: sim.userFocused || void 0
3725
4516
  };
3726
- browser.ws.send(JSON.stringify(message));
4517
+ sim.ws.send(JSON.stringify(message));
3727
4518
  }
3728
4519
  }
3729
- setCliBrowserTarget(ws, browserId) {
3730
- const prevBrowserId = this.cliBrowserBySocket.get(ws);
3731
- if (prevBrowserId === browserId) return;
3732
- this.cliBrowserBySocket.set(ws, browserId);
3733
- this.recordBrowserAction(
3734
- browserId,
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
- recordBrowserAction(browserId, label, broadcast = true) {
4527
+ recordSimAction(simId, label, broadcast = true) {
3741
4528
  const normalized = label?.trim();
3742
4529
  if (!normalized) return;
3743
- const browser = this.browsers.get(browserId);
3744
- if (!browser) return;
4530
+ const sim = this.sims.get(simId);
4531
+ if (!sim) return;
3745
4532
  const now = Date.now();
3746
- browser.lastActiveAt = now;
3747
- browser.recentActions = [
4533
+ sim.lastActiveAt = now;
4534
+ sim.recentActions = [
3748
4535
  { label: normalized, at: now },
3749
- ...browser.recentActions.filter((entry) => entry.label !== normalized)
4536
+ ...sim.recentActions.filter((entry) => entry.label !== normalized)
3750
4537
  ].slice(0, 4);
3751
- if (broadcast) this.broadcastBrowserClientStates();
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 tab";
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 session keys attached to a browser, not raw sockets.
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(browserId) {
4563
+ getAttachedCliCount(simId) {
3777
4564
  const keys = /* @__PURE__ */ new Set();
3778
- for (const [ws, attachedBrowserId] of this.cliBrowserBySocket) {
3779
- if (attachedBrowserId !== browserId) continue;
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.cliSessionKeyBySocket.get(ws);
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 session keys attached to this browser other than `selfWs`.
3787
- // used to warn a cli that other agents/sessions are also targeting the tab.
3788
- getOtherCliSessionCount(selfWs, browserId) {
3789
- const selfKey = this.cliSessionKeyBySocket.get(selfWs);
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, attachedBrowserId] of this.cliBrowserBySocket) {
3792
- if (attachedBrowserId !== browserId) continue;
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.cliSessionKeyBySocket.get(ws);
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(browserId) {
4587
+ getActiveAgentCommandCount(simId) {
3801
4588
  let count = 0;
3802
4589
  for (const pending of this.pendingCommands.values()) {
3803
- if (pending.browserId === browserId) count++;
4590
+ if (pending.simId === simId) count++;
3804
4591
  }
3805
4592
  return count;
3806
4593
  }
3807
- tryRestoreBrowserId(browser, requestedId) {
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 === browser.id) return false;
3810
- const existing = this.browsers.get(nextId);
3811
- if (existing && existing !== browser && existing.ws.readyState === import_ws.WebSocket.OPEN) {
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.getRestorableBrowserState(nextId);
3815
- const prevId = browser.id;
3816
- this.browsers.delete(prevId);
3817
- browser.id = nextId;
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
- browser.recentActions = restorable.recentActions.map((entry) => ({ ...entry }));
3820
- browser.lastActiveAt = restorable.lastActiveAt;
3821
- browser.cliLease = restorable.cliLease ? { ...restorable.cliLease } : void 0;
3822
- this.restorableBrowsers.delete(nextId);
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.browsers.set(browser.id, browser);
3825
- if (this.primaryBrowserId === prevId) {
3826
- this.primaryBrowserId = browser.id;
4618
+ this.sims.set(sim.id, sim);
4619
+ if (this.primarySimId === prevId) {
4620
+ this.primarySimId = sim.id;
3827
4621
  }
3828
- for (const [ws, attachedBrowserId] of this.cliBrowserBySocket) {
3829
- if (attachedBrowserId === prevId) {
3830
- this.cliBrowserBySocket.set(ws, browser.id);
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
- rememberDisconnectedBrowser(browser) {
3836
- const lease = this.getActiveLease(browser);
3837
- this.restorableBrowsers.set(browser.id, {
3838
- recentActions: browser.recentActions.map((entry) => ({ ...entry })),
3839
- lastActiveAt: browser.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.BROWSER_RECONNECT_TTL_MS
4635
+ expiresAt: Date.now() + _SootSimBridgeHost.SIM_RECONNECT_TTL_MS
3842
4636
  });
3843
- this.browsers.delete(browser.id);
4637
+ this.sims.delete(sim.id);
3844
4638
  }
3845
- getRestorableBrowserState(browserId) {
3846
- const snapshot = this.restorableBrowsers.get(browserId);
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.restorableBrowsers.delete(browserId);
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
- sweepRestorableBrowsers(now = Date.now()) {
3858
- for (const [browserId, snapshot] of this.restorableBrowsers) {
4651
+ sweepRestorableSims(now = Date.now()) {
4652
+ for (const [simId, snapshot] of this.restorableSims) {
3859
4653
  if (snapshot.expiresAt > now) continue;
3860
- this.restorableBrowsers.delete(browserId);
3861
- for (const [cliWs, attachedBrowserId] of this.cliBrowserBySocket) {
3862
- if (attachedBrowserId === browserId) {
3863
- this.cliBrowserBySocket.delete(cliWs);
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;