sootsim 0.0.4 → 0.1.37

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