sootsim 0.0.1 → 0.0.2

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 (122) hide show
  1. package/README.md +12 -0
  2. package/dist-cli/bin.js +16 -10
  3. package/dist-cli/chunks/agent-3T4BJEZM.js +61 -0
  4. package/dist-cli/chunks/agent-wrapper-WCYNLWHZ.js +15 -0
  5. package/dist-cli/chunks/assert-FPFJEFF3.js +47 -0
  6. package/dist-cli/chunks/auto-bootstrap-HDW6N77H.js +2 -0
  7. package/dist-cli/chunks/chunk-3HBBSRLE.js +2 -0
  8. package/dist-cli/chunks/chunk-4372UQHZ.js +308 -0
  9. package/dist-cli/chunks/chunk-4GWEO5CL.js +1 -0
  10. package/dist-cli/chunks/{chunk-G5MR66EB.js → chunk-5C5I5OFM.js} +2 -2
  11. package/dist-cli/chunks/chunk-6IPY24VM.js +11 -0
  12. package/dist-cli/chunks/chunk-AS4V7TZU.js +2 -0
  13. package/dist-cli/chunks/chunk-B5R4K2DG.js +5 -0
  14. package/dist-cli/chunks/chunk-CXTA5VGA.js +4 -0
  15. package/dist-cli/chunks/chunk-CZZB4DWG.js +3 -0
  16. package/dist-cli/chunks/chunk-DW54UPRZ.js +119 -0
  17. package/dist-cli/chunks/chunk-EIZCWDRE.js +1 -0
  18. package/dist-cli/chunks/{chunk-KSACMDXK.js → chunk-ET3NNZAR.js} +2 -2
  19. package/dist-cli/chunks/chunk-EWEKADK4.js +5 -0
  20. package/dist-cli/chunks/{chunk-JSF5LPNT.js → chunk-EWMYTXM2.js} +5 -5
  21. package/dist-cli/chunks/chunk-FUQ4XA6I.js +16 -0
  22. package/dist-cli/chunks/chunk-GQUOQNTP.js +27 -0
  23. package/dist-cli/chunks/chunk-HBNVKYSC.js +2 -0
  24. package/dist-cli/chunks/{chunk-64TOMNZX.js → chunk-HORCHQT7.js} +2 -2
  25. package/dist-cli/chunks/{chunk-YCETS3B3.js → chunk-ISAMAM3I.js} +2 -2
  26. package/dist-cli/chunks/{chunk-GPVPHE2B.js → chunk-K6YUSCAC.js} +2 -2
  27. package/dist-cli/chunks/{chunk-E522F5JW.js → chunk-K7LDP7JL.js} +1 -1
  28. package/dist-cli/chunks/chunk-KZ2LIDW6.js +2 -0
  29. package/dist-cli/chunks/{chunk-J2S3OCWA.js → chunk-LOV766MI.js} +1 -1
  30. package/dist-cli/chunks/{chunk-OROM7DZI.js → chunk-LXCFGKL2.js} +1 -1
  31. package/dist-cli/chunks/{chunk-PWXPA745.js → chunk-NE62JSI6.js} +1 -1
  32. package/dist-cli/chunks/chunk-NHA3G6A3.js +22 -0
  33. package/dist-cli/chunks/chunk-NXWCDGWS.js +2 -0
  34. package/dist-cli/chunks/{chunk-QOBRRY5X.js → chunk-RJUBGX5M.js} +1 -1
  35. package/dist-cli/chunks/chunk-SLCVEGTW.js +4 -0
  36. package/dist-cli/chunks/chunk-TGDP3D3V.js +34 -0
  37. package/dist-cli/chunks/chunk-TSZBQS6W.js +62 -0
  38. package/dist-cli/chunks/chunk-XKDQEYTE.js +1 -0
  39. package/dist-cli/chunks/chunk-XXUAOYYT.js +4 -0
  40. package/dist-cli/chunks/{chunk-7X6OPSRD.js → chunk-YVSZHVLU.js} +2 -2
  41. package/dist-cli/chunks/{compat-MRN2ORY5.js → compat-3HMKLGXL.js} +4 -4
  42. package/dist-cli/chunks/{config-CO5IYWUY.js → config-IJQ3KANN.js} +5 -5
  43. package/dist-cli/chunks/control-3RAFI4AW.js +2 -0
  44. package/dist-cli/chunks/{daemon-G4XVRFHM.js → daemon-BBEQJLRY.js} +2 -2
  45. package/dist-cli/chunks/{debug-ZNSZTWT6.js → debug-SGZ5ZFQI.js} +4 -4
  46. package/dist-cli/chunks/demo-app-registry-NCYP3WA6.js +2 -0
  47. package/dist-cli/chunks/{detox-JEGYNTYV.js → detox-PK74V2Y7.js} +2 -2
  48. package/dist-cli/chunks/{device-BS34FAFM.js → device-MWNFX54L.js} +2 -2
  49. package/dist-cli/chunks/drivers-EXUREU4B.js +2 -0
  50. package/dist-cli/chunks/electron-3NIHSU2K.js +15 -0
  51. package/dist-cli/chunks/flow-6Y3E6E5P.js +2 -0
  52. package/dist-cli/chunks/{hints-7Z656W4H.js → hints-XZJLBIXW.js} +2 -2
  53. package/dist-cli/chunks/home-paths-BNRMUBJA.js +2 -0
  54. package/dist-cli/chunks/{inspect-NAHXP2M5.js → inspect-FGTUAK4C.js} +153 -165
  55. package/dist-cli/chunks/install-LCXALH26.js +65 -0
  56. package/dist-cli/chunks/{install-desktop-PYIZIH67.js → install-desktop-U3RQ6XUX.js} +8 -4
  57. package/dist-cli/chunks/install-dev-desktop-BLKRFI42.js +100 -0
  58. package/dist-cli/chunks/keys-N5LBDSD5.js +19 -0
  59. package/dist-cli/chunks/launch-NIMSJH5I.js +16 -0
  60. package/dist-cli/chunks/{login-Z5Z54HUJ.js → login-CQV2XBRM.js} +5 -5
  61. package/dist-cli/chunks/{logout-T2QDYGCB.js → logout-R56NWAWQ.js} +2 -2
  62. package/dist-cli/chunks/{maestro-4AXTS7OE.js → maestro-ZYUVTM7H.js} +2 -2
  63. package/dist-cli/chunks/{preview-NMGWHWMX.js → preview-AOAWAYEQ.js} +2 -2
  64. package/dist-cli/chunks/{profile-6RGJA4FR.js → profile-DDADDPRW.js} +3 -3
  65. package/dist-cli/chunks/record-3OIOTHP6.js +37 -0
  66. package/dist-cli/chunks/runtime-JTLZYEXK.js +25 -0
  67. package/dist-cli/chunks/{screenshot-R3GCCSCI.js → screenshot-Q6N2V5LL.js} +3 -3
  68. package/dist-cli/chunks/screenshot-mode-WWLWJWQD.js +17 -0
  69. package/dist-cli/chunks/{screenshots-4UQJE4NC.js → screenshots-2JEPJGZO.js} +2 -2
  70. package/dist-cli/chunks/server-VH34RVAX.js +29 -0
  71. package/dist-cli/chunks/{skills-2PPKPL4B.js → skills-PU4627FY.js} +2 -2
  72. package/dist-cli/chunks/store-U2VDD2S4.js +2 -0
  73. package/dist-cli/chunks/{test-5LFKOQ4M.js → test-AECE56E7.js} +3 -3
  74. package/dist-cli/chunks/upload-KPP7KG6E.js +2 -0
  75. package/dist-cli/chunks/{whoami-H6FW34JS.js → whoami-NCGRRR7X.js} +2 -2
  76. package/dist-lib/agent-daemon-client.cjs +414 -0
  77. package/dist-lib/agent-events.cjs +48 -0
  78. package/dist-lib/agent-sessions.cjs +692 -0
  79. package/dist-lib/attached-projects.cjs +448 -0
  80. package/dist-lib/auth/shared-session.cjs +174 -0
  81. package/dist-lib/backend-origin.cjs +70 -0
  82. package/dist-lib/bridge-constants.cjs +32 -0
  83. package/dist-lib/cli-constants.cjs +32 -0
  84. package/dist-lib/config.cjs +88 -0
  85. package/dist-lib/dev-bundle-resolution.cjs +236 -0
  86. package/dist-lib/home-paths.cjs +234 -0
  87. package/dist-lib/host/bridge-host.cjs +3458 -0
  88. package/dist-lib/index.cjs +361 -0
  89. package/dist-lib/metro.cjs +215 -0
  90. package/dist-lib/render-mode.cjs +54 -0
  91. package/dist-lib/vite-base.cjs +4217 -0
  92. package/dist-lib/vite.cjs +178 -0
  93. package/package.json +80 -13
  94. package/scripts/postinstall.cjs +70 -0
  95. package/dist-cli/chunks/bridge-host-2EY7Z4AO.js +0 -2
  96. package/dist-cli/chunks/chunk-3C3ZH7PP.js +0 -4
  97. package/dist-cli/chunks/chunk-3R4ZZESY.js +0 -119
  98. package/dist-cli/chunks/chunk-74XPLOV4.js +0 -2
  99. package/dist-cli/chunks/chunk-7LMDCMSI.js +0 -8
  100. package/dist-cli/chunks/chunk-A2CZQIWO.js +0 -1
  101. package/dist-cli/chunks/chunk-CKZ376AY.js +0 -322
  102. package/dist-cli/chunks/chunk-E5UBZEYR.js +0 -2
  103. package/dist-cli/chunks/chunk-HOIHCO7S.js +0 -3
  104. package/dist-cli/chunks/chunk-KQWZZ56P.js +0 -2
  105. package/dist-cli/chunks/chunk-KSB6MSZ4.js +0 -34
  106. package/dist-cli/chunks/chunk-KXYKAYYB.js +0 -51
  107. package/dist-cli/chunks/chunk-MBFP2LVH.js +0 -3
  108. package/dist-cli/chunks/chunk-MPSZ5EWF.js +0 -16
  109. package/dist-cli/chunks/chunk-X2U72K7X.js +0 -1
  110. package/dist-cli/chunks/control-Y7TKKB6D.js +0 -2
  111. package/dist-cli/chunks/dev-ZUKCZQEX.js +0 -25
  112. package/dist-cli/chunks/dev-checkout-IEZVVTCN.js +0 -2
  113. package/dist-cli/chunks/drivers-46PFFIDF.js +0 -2
  114. package/dist-cli/chunks/electron-P2KOPX2S.js +0 -15
  115. package/dist-cli/chunks/flow-VVOF6UNC.js +0 -2
  116. package/dist-cli/chunks/install-EPUJX4AT.js +0 -67
  117. package/dist-cli/chunks/record-IE27Z2GA.js +0 -37
  118. package/dist-cli/chunks/screenshot-mode-SZQDNGYE.js +0 -17
  119. package/dist-cli/chunks/server-AN2G5KO4.js +0 -21
  120. package/dist-cli/chunks/store-PU5ES4YQ.js +0 -2
  121. package/dist-cli/chunks/upload-BYNPC54C.js +0 -2
  122. package/dist-cli/chunks/vite-plugin-5AEUUBKP.js +0 -9
@@ -0,0 +1,4217 @@
1
+ /*! sootsim v0.0.2 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
+ let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
+
35
+ // src/bridge-constants.ts
36
+ var DEFAULT_SOOTSIM_BRIDGE_PORT;
37
+ var init_bridge_constants = __esm({
38
+ "src/bridge-constants.ts"() {
39
+ "use strict";
40
+ DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
41
+ }
42
+ });
43
+
44
+ // src/home-paths.ts
45
+ function sootsimHomeDir() {
46
+ const override = process.env[SOOTSIM_HOME_ENV];
47
+ if (override && override.length > 0) return import_path.default.resolve(override);
48
+ return import_path.default.join((0, import_os.homedir)(), ".sootsim");
49
+ }
50
+ function runtimesDir() {
51
+ return import_path.default.join(sootsimHomeDir(), "runtimes");
52
+ }
53
+ function runtimeDir(version) {
54
+ return import_path.default.join(runtimesDir(), version);
55
+ }
56
+ function activeRuntimeFile() {
57
+ return import_path.default.join(runtimesDir(), ACTIVE_RUNTIME_FILE);
58
+ }
59
+ function cacheDir() {
60
+ return import_path.default.join(sootsimHomeDir(), "cache");
61
+ }
62
+ function daemonLockfilePath() {
63
+ return import_path.default.join(sootsimHomeDir(), DAEMON_LOCKFILE);
64
+ }
65
+ function ensureSootsimHome() {
66
+ import_fs.default.mkdirSync(sootsimHomeDir(), { recursive: true });
67
+ import_fs.default.mkdirSync(runtimesDir(), { recursive: true });
68
+ import_fs.default.mkdirSync(cacheDir(), { recursive: true });
69
+ }
70
+ function readActiveRuntime() {
71
+ try {
72
+ const value = import_fs.default.readFileSync(activeRuntimeFile(), "utf8").trim();
73
+ return value.length > 0 ? value : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ function writeActiveRuntime(version) {
79
+ import_fs.default.mkdirSync(runtimesDir(), { recursive: true });
80
+ import_fs.default.writeFileSync(activeRuntimeFile(), `${version}
81
+ `, "utf8");
82
+ }
83
+ function listInstalledRuntimes() {
84
+ try {
85
+ return import_fs.default.readdirSync(runtimesDir(), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort(compareSemver);
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+ function compareSemver(a, b) {
91
+ const parse = (v) => {
92
+ const hyphen = v.indexOf("-");
93
+ const core = hyphen >= 0 ? v.slice(0, hyphen) : v;
94
+ const pre = hyphen >= 0 ? v.slice(hyphen + 1) : "";
95
+ const parts = core.split(".").map((n) => Number.parseInt(n, 10));
96
+ if (parts.some((n) => !Number.isFinite(n))) return [[Number.POSITIVE_INFINITY], v];
97
+ return [parts, pre];
98
+ };
99
+ const [an, ap] = parse(a);
100
+ const [bn, bp] = parse(b);
101
+ for (let i = 0; i < Math.max(an.length, bn.length); i++) {
102
+ const av = an[i] ?? 0;
103
+ const bv = bn[i] ?? 0;
104
+ if (av !== bv) return av - bv;
105
+ }
106
+ if (ap === bp) return 0;
107
+ if (!ap) return 1;
108
+ if (!bp) return -1;
109
+ return ap < bp ? -1 : 1;
110
+ }
111
+ function activeRuntimeDir() {
112
+ const version = readActiveRuntime();
113
+ if (!version) return null;
114
+ const dir = runtimeDir(version);
115
+ try {
116
+ if (import_fs.default.statSync(dir).isDirectory()) return dir;
117
+ } catch {
118
+ }
119
+ return null;
120
+ }
121
+ function readDaemonLockfile() {
122
+ try {
123
+ const fd = import_fs.default.openSync(daemonLockfilePath(), "r");
124
+ try {
125
+ const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
126
+ const bytesRead = import_fs.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
127
+ const raw = buf.subarray(0, bytesRead).toString("utf8");
128
+ const parsed = JSON.parse(raw);
129
+ 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") {
130
+ return parsed;
131
+ }
132
+ return null;
133
+ } finally {
134
+ import_fs.default.closeSync(fd);
135
+ }
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+ function isDaemonLockfileFresh(lock, now = Date.now()) {
141
+ if (!lock) return false;
142
+ if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
143
+ try {
144
+ process.kill(lock.pid, 0);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ function writeDaemonLockfile(data) {
151
+ ensureSootsimHome();
152
+ const tmp = `${daemonLockfilePath()}.tmp`;
153
+ import_fs.default.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
154
+ `, "utf8");
155
+ import_fs.default.renameSync(tmp, daemonLockfilePath());
156
+ }
157
+ function claimDaemonLockfile(data) {
158
+ ensureSootsimHome();
159
+ const existing = readDaemonLockfile();
160
+ if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
161
+ return false;
162
+ }
163
+ writeDaemonLockfile(data);
164
+ return true;
165
+ }
166
+ function removeDaemonLockfile() {
167
+ try {
168
+ import_fs.default.unlinkSync(daemonLockfilePath());
169
+ } catch {
170
+ }
171
+ }
172
+ var import_fs, import_os, import_path, SOOTSIM_HOME_ENV, ACTIVE_RUNTIME_FILE, DAEMON_LOCKFILE, DAEMON_HEARTBEAT_STALE_MS, DAEMON_LOCKFILE_MAX_BYTES;
173
+ var init_home_paths = __esm({
174
+ "src/home-paths.ts"() {
175
+ "use strict";
176
+ import_fs = __toESM(require("fs"), 1);
177
+ import_os = require("os");
178
+ import_path = __toESM(require("path"), 1);
179
+ SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
180
+ ACTIVE_RUNTIME_FILE = "active";
181
+ DAEMON_LOCKFILE = "daemon.json";
182
+ DAEMON_HEARTBEAT_STALE_MS = 3e4;
183
+ DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
184
+ }
185
+ });
186
+
187
+ // src/config.ts
188
+ function hasOwnKeys(value) {
189
+ return !!value && Object.keys(value).length > 0;
190
+ }
191
+ function hasSootSimConfig(config) {
192
+ if (!config) return false;
193
+ return hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.env) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialState);
194
+ }
195
+ function applySootSimConfigToUrl(url, config) {
196
+ const parsed = new URL(url);
197
+ if (hasSootSimConfig(config)) {
198
+ parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config));
199
+ } else {
200
+ parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM);
201
+ }
202
+ return parsed.toString();
203
+ }
204
+ var SOOTSIM_CONFIG_QUERY_PARAM;
205
+ var init_config = __esm({
206
+ "src/config.ts"() {
207
+ "use strict";
208
+ SOOTSIM_CONFIG_QUERY_PARAM = "sootsimConfig";
209
+ }
210
+ });
211
+
212
+ // src/native-dev-bundle-url.ts
213
+ function isAbsoluteHttpUrl(url) {
214
+ return /^https?:\/\//i.test(url);
215
+ }
216
+ function isNativeDevBundlePath(pathname) {
217
+ return pathname.endsWith(".bundle");
218
+ }
219
+ function normalizeNativeDevBundleUrl(bundleUrl) {
220
+ try {
221
+ const isAbsolute = isAbsoluteHttpUrl(bundleUrl);
222
+ const parsed = new URL(bundleUrl, "http://soot.local");
223
+ if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl;
224
+ if (!parsed.searchParams.has("dev")) parsed.searchParams.set("dev", "true");
225
+ if (!parsed.searchParams.has("minify")) {
226
+ parsed.searchParams.set("minify", "false");
227
+ }
228
+ if (isAbsolute) return parsed.toString();
229
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
230
+ } catch {
231
+ return bundleUrl;
232
+ }
233
+ }
234
+ var init_native_dev_bundle_url = __esm({
235
+ "src/native-dev-bundle-url.ts"() {
236
+ "use strict";
237
+ }
238
+ });
239
+
240
+ // scripts/demo-app-registry.ts
241
+ function findWorkspaceRoot(startDir) {
242
+ let dir = startDir;
243
+ while (true) {
244
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "pnpm-workspace.yaml")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "turbo.json")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "nx.json")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "lerna.json"))) {
245
+ return dir;
246
+ }
247
+ const packageJsonPath = (0, import_node_path.join)(dir, "package.json");
248
+ if ((0, import_node_fs.existsSync)(packageJsonPath)) {
249
+ try {
250
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
251
+ if (pkg.workspaces) return dir;
252
+ } catch {
253
+ }
254
+ }
255
+ const parent = (0, import_node_path.dirname)(dir);
256
+ if (parent === dir) return null;
257
+ dir = parent;
258
+ }
259
+ }
260
+ function resolveWorkspaceScriptPath(workspaceRelativePath, packageRelativePath) {
261
+ const workspaceRoot2 = findWorkspaceRoot(process.cwd());
262
+ const candidates = [
263
+ workspaceRoot2 ? (0, import_node_path.resolve)(workspaceRoot2, workspaceRelativePath) : null,
264
+ (0, import_node_path.resolve)(process.cwd(), workspaceRelativePath),
265
+ (0, import_node_path.resolve)(process.cwd(), packageRelativePath)
266
+ ].filter((candidate) => Boolean(candidate));
267
+ for (const candidate of candidates) {
268
+ if ((0, import_node_fs.existsSync)(candidate)) return candidate;
269
+ }
270
+ return candidates[0] ?? (0, import_node_path.resolve)(process.cwd(), workspaceRelativePath);
271
+ }
272
+ function parseEnvFile(filePath) {
273
+ if (!(0, import_node_fs.existsSync)(filePath)) return {};
274
+ const env = {};
275
+ const source = (0, import_node_fs.readFileSync)(filePath, "utf8");
276
+ for (const rawLine of source.split(/\r?\n/)) {
277
+ const line = rawLine.trim();
278
+ if (!line || line.startsWith("#")) continue;
279
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
280
+ if (!match) continue;
281
+ let value = match[2].trim();
282
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
283
+ value = value.slice(1, -1);
284
+ }
285
+ env[match[1]] = value;
286
+ }
287
+ return env;
288
+ }
289
+ function isUsableUniswapEnvValue(value) {
290
+ if (!value) return false;
291
+ const trimmed = value.trim();
292
+ if (!trimmed) return false;
293
+ if (trimmed.includes(UNISWAP_PLACEHOLDER)) return false;
294
+ if (trimmed === "TRADING_API_KEY" || trimmed === "UNISWAP_API_KEY") return false;
295
+ return true;
296
+ }
297
+ function pickEnvValue(sources, keys) {
298
+ for (const source of sources) {
299
+ for (const key of keys) {
300
+ const value = source[key];
301
+ if (isUsableUniswapEnvValue(value)) return value.trim();
302
+ }
303
+ }
304
+ return void 0;
305
+ }
306
+ function resolveUniswapDemoEnv() {
307
+ const localEnv = parseEnvFile(UNISWAP_ENV_LOCAL_FILE);
308
+ const webEnv = parseEnvFile((0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/web/.env"));
309
+ const sources = [
310
+ process.env,
311
+ localEnv,
312
+ webEnv
313
+ ];
314
+ const env = {};
315
+ const bindings = [
316
+ ["AMPLITUDE_PROXY_URL_OVERRIDE", ["REACT_APP_AMPLITUDE_PROXY_URL"]],
317
+ ["QUICKNODE_ENDPOINT_NAME", ["REACT_APP_QUICKNODE_ENDPOINT_NAME"]],
318
+ ["QUICKNODE_ENDPOINT_TOKEN", ["REACT_APP_QUICKNODE_ENDPOINT_TOKEN"]],
319
+ ["INFURA_KEY", ["REACT_APP_INFURA_KEY"]],
320
+ ["STATSIG_API_KEY", ["REACT_APP_STATSIG_API_KEY"]],
321
+ ["STATSIG_PROXY_URL_OVERRIDE", ["REACT_APP_STATSIG_PROXY_URL"]],
322
+ ["WALLETCONNECT_PROJECT_ID", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
323
+ ["WALLETCONNECT_PROJECT_ID_BETA", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
324
+ ["WALLETCONNECT_PROJECT_ID_DEV", ["REACT_APP_WALLET_CONNECT_PROJECT_ID"]],
325
+ ["TRADING_API_KEY", ["REACT_APP_TRADING_API_KEY"]],
326
+ ["UNISWAP_API_KEY", []]
327
+ ];
328
+ for (const [target, aliases] of bindings) {
329
+ const value = pickEnvValue(sources, [target, ...aliases]);
330
+ if (!value) continue;
331
+ env[target] = value;
332
+ for (const alias of aliases) {
333
+ env[alias] = value;
334
+ }
335
+ }
336
+ const hasPrivateGatewayKeys = isUsableUniswapEnvValue(env.TRADING_API_KEY) && isUsableUniswapEnvValue(env.UNISWAP_API_KEY);
337
+ if (!hasPrivateGatewayKeys) {
338
+ const publicGraphqlUrl = pickEnvValue(sources, ["GRAPHQL_URL_OVERRIDE", "REACT_APP_AWS_API_ENDPOINT"]) || "https://interface.gateway.uniswap.org/v1/graphql";
339
+ env.API_BASE_URL_OVERRIDE = "https://interface.gateway.uniswap.org";
340
+ env.API_BASE_URL_V2_OVERRIDE = "https://interface.gateway.uniswap.org/v2";
341
+ env.GRAPHQL_URL_OVERRIDE = publicGraphqlUrl;
342
+ env.TRADING_API_URL_OVERRIDE = "https://trading-api-labs.interface.gateway.uniswap.org";
343
+ env.FOR_API_URL_OVERRIDE = "https://for.interface.gateway.uniswap.org/v2/FOR.v1.FORService";
344
+ }
345
+ return env;
346
+ }
347
+ function ensureUniswapDemoEnvLocal() {
348
+ const existingSource = (0, import_node_fs.existsSync)(UNISWAP_ENV_LOCAL_FILE) ? (0, import_node_fs.readFileSync)(UNISWAP_ENV_LOCAL_FILE, "utf8") : "";
349
+ if (existingSource && !existingSource.includes(UNISWAP_DEMO_ENV_MARKER)) {
350
+ return;
351
+ }
352
+ const env = resolveUniswapDemoEnv();
353
+ const lines = [UNISWAP_DEMO_ENV_MARKER];
354
+ for (const [key, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) {
355
+ lines.push(`${key}=${JSON.stringify(value)}`);
356
+ }
357
+ lines.push("");
358
+ (0, import_node_fs.writeFileSync)(UNISWAP_ENV_LOCAL_FILE, `${lines.join("\n")}
359
+ `);
360
+ }
361
+ function ensureUniswapForceUpgradePatched() {
362
+ const hookNeedle = `export function useForceUpgradeStatus(): ForceUpgradeStatus {
363
+ `;
364
+ const hookPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
365
+ return 'not-required'
366
+
367
+ `;
368
+ const hookLegacyPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
369
+ if (process.env.${UNISWAP_FORCE_UPGRADE_PATCH_MARKER} === 'true') {
370
+ return 'not-required'
371
+ }
372
+
373
+ `;
374
+ const notificationNeedle = ` const getForceUpgradeStatus = (): ForceUpgradeStatus => {
375
+ `;
376
+ const notificationPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
377
+ return 'not-required'
378
+
379
+ `;
380
+ const notificationLegacyPatch = ` // sootsim demo: bypass the force-upgrade gate during local engine demos.
381
+ if (process.env.${UNISWAP_FORCE_UPGRADE_PATCH_MARKER} === 'true') {
382
+ return 'not-required'
383
+ }
384
+
385
+ `;
386
+ const patchWithMigration = (filePath, needle, patch, legacyPatch) => {
387
+ const source = (0, import_node_fs.readFileSync)(filePath, "utf8");
388
+ if (source.includes(patch)) return;
389
+ if (source.includes(legacyPatch)) {
390
+ (0, import_node_fs.writeFileSync)(filePath, source.replace(legacyPatch, patch));
391
+ return;
392
+ }
393
+ if (!source.includes(needle)) {
394
+ throw new Error(
395
+ `uniswap demo patch failed: expected snippet not found in ${filePath}`
396
+ );
397
+ }
398
+ (0, import_node_fs.writeFileSync)(filePath, source.replace(needle, `${needle}${patch}`));
399
+ };
400
+ patchWithMigration(
401
+ UNISWAP_FORCE_UPGRADE_HOOK_FILE,
402
+ hookNeedle,
403
+ hookPatch,
404
+ hookLegacyPatch
405
+ );
406
+ patchWithMigration(
407
+ UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE,
408
+ notificationNeedle,
409
+ notificationPatch,
410
+ notificationLegacyPatch
411
+ );
412
+ }
413
+ function readArtsyKeysPayload() {
414
+ if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE)) return void 0;
415
+ try {
416
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(ARTSY_KEYS_FILE, "utf8"));
417
+ if (!parsed || typeof parsed !== "object") return void 0;
418
+ const secure = parsed.secure && typeof parsed.secure === "object" ? parsed.secure : void 0;
419
+ const publicKeys = parsed.public && typeof parsed.public === "object" ? parsed.public : void 0;
420
+ if (!secure && !publicKeys) return void 0;
421
+ return { secure, public: publicKeys };
422
+ } catch {
423
+ return void 0;
424
+ }
425
+ }
426
+ function resolveArtsyRuntimeConfig() {
427
+ const email = process.env.SOOTSIM_ARTSY_EMAIL ?? process.env.MAESTRO_TEST_EMAIL;
428
+ const password = process.env.SOOTSIM_ARTSY_PASSWORD ?? process.env.MAESTRO_TEST_PASSWORD;
429
+ const keys = readArtsyKeysPayload();
430
+ const env = {};
431
+ if (email && password) {
432
+ env.SOOTSIM_LAUNCH_ARGUMENTS = JSON.stringify({
433
+ email,
434
+ password,
435
+ useMaestroInit: true
436
+ });
437
+ }
438
+ if (keys) {
439
+ env.SOOTSIM_REACT_NATIVE_KEYS_JSON = JSON.stringify(keys);
440
+ }
441
+ if (Object.keys(env).length === 0) return void 0;
442
+ return {
443
+ env
444
+ };
445
+ }
446
+ function ensureArtsySetup() {
447
+ if (!(0, import_node_fs.existsSync)(ARTSY_DIR)) return;
448
+ const { execSync } = require("node:child_process");
449
+ const yarnRelease = (0, import_node_path.join)(ARTSY_DIR, ".yarn/releases/yarn-4.10.3.cjs");
450
+ if ((0, import_node_fs.existsSync)(yarnRelease)) {
451
+ const src = (0, import_node_fs.readFileSync)(yarnRelease, "utf8");
452
+ const needle = '["clone","-c core.autocrlf=false",';
453
+ if (src.includes(needle)) {
454
+ (0, import_node_fs.writeFileSync)(
455
+ yarnRelease,
456
+ src.replace(needle, '["clone","-c","core.autocrlf=false",')
457
+ );
458
+ }
459
+ }
460
+ if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(ARTSY_DIR, "node_modules/.yarn-state.yml"))) {
461
+ execSync("yarn install", {
462
+ cwd: ARTSY_DIR,
463
+ stdio: "inherit",
464
+ env: { ...process.env, YARN_CHECKSUM_BEHAVIOR: "update" }
465
+ });
466
+ }
467
+ if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE) || !(0, import_node_fs.existsSync)(ARTSY_METAFLAGS_FILE)) {
468
+ try {
469
+ execSync("yarn setup:oss", { cwd: ARTSY_DIR, stdio: "inherit" });
470
+ } catch {
471
+ if (!(0, import_node_fs.existsSync)(ARTSY_KEYS_FILE) || !(0, import_node_fs.existsSync)(ARTSY_METAFLAGS_FILE)) {
472
+ throw new Error("artsy demo: setup:oss did not create keys/metaflags");
473
+ }
474
+ }
475
+ }
476
+ const rnlaPkgJson = (0, import_node_path.join)(
477
+ ARTSY_DIR,
478
+ "node_modules/react-native-launch-arguments/package.json"
479
+ );
480
+ if ((0, import_node_fs.existsSync)(rnlaPkgJson)) {
481
+ const raw = (0, import_node_fs.readFileSync)(rnlaPkgJson, "utf8");
482
+ if (raw.includes('"dist/index.js"')) {
483
+ (0, import_node_fs.writeFileSync)(rnlaPkgJson, raw.replace('"dist/index.js"', '"src/index.ts"'));
484
+ }
485
+ }
486
+ if (!(0, import_node_fs.existsSync)(ARTSY_RELAY_SENTINEL)) {
487
+ execSync("yarn relay", { cwd: ARTSY_DIR, stdio: "inherit" });
488
+ (0, import_node_fs.writeFileSync)(ARTSY_RELAY_SENTINEL, "");
489
+ }
490
+ }
491
+ var import_node_fs, import_node_os, import_node_path, HOME, getExpensifyProxyScript, EXPENSIFY_NATIVE_PROXY_ENV, UNISWAP_REPO_DIR, UNISWAP_APP_DIR, UNISWAP_ENV_LOCAL_FILE, UNISWAP_PLACEHOLDER, UNISWAP_DEMO_ENV_MARKER, UNISWAP_FORCE_UPGRADE_HOOK_FILE, UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE, UNISWAP_FORCE_UPGRADE_PATCH_MARKER, ARTSY_DIR, ARTSY_KEYS_FILE, ARTSY_METAFLAGS_FILE, ARTSY_RELAY_SENTINEL, APPS;
492
+ var init_demo_app_registry = __esm({
493
+ "scripts/demo-app-registry.ts"() {
494
+ "use strict";
495
+ import_node_fs = require("node:fs");
496
+ import_node_os = require("node:os");
497
+ import_node_path = require("node:path");
498
+ HOME = (0, import_node_os.homedir)();
499
+ getExpensifyProxyScript = () => resolveWorkspaceScriptPath(
500
+ "packages/sootsim-engine/scripts/expensify-web-proxy.ts",
501
+ "scripts/expensify-web-proxy.ts"
502
+ );
503
+ EXPENSIFY_NATIVE_PROXY_ENV = {
504
+ USE_NGROK: "true",
505
+ NGROK_URL: "http://localhost:9000/",
506
+ SECURE_NGROK_URL: "http://localhost:9000/"
507
+ };
508
+ UNISWAP_REPO_DIR = (0, import_node_path.join)(HOME, "github/uniswap-interface");
509
+ UNISWAP_APP_DIR = (0, import_node_path.join)(UNISWAP_REPO_DIR, "apps/mobile");
510
+ UNISWAP_ENV_LOCAL_FILE = (0, import_node_path.join)(UNISWAP_REPO_DIR, ".env.defaults.local");
511
+ UNISWAP_PLACEHOLDER = "stored-in-.env.local";
512
+ UNISWAP_DEMO_ENV_MARKER = "# sootsim demo env overrides";
513
+ UNISWAP_FORCE_UPGRADE_HOOK_FILE = (0, import_node_path.join)(
514
+ UNISWAP_REPO_DIR,
515
+ "packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts"
516
+ );
517
+ UNISWAP_FORCE_UPGRADE_NOTIFICATION_FILE = (0, import_node_path.join)(
518
+ UNISWAP_REPO_DIR,
519
+ "apps/mobile/src/notification-service/data-sources/createForceUpgradeNotificationDataSource.ts"
520
+ );
521
+ UNISWAP_FORCE_UPGRADE_PATCH_MARKER = "SOOTSIM_DEMO_DISABLE_FORCE_UPGRADE";
522
+ ARTSY_DIR = (0, import_node_path.join)(HOME, "github/eigen");
523
+ ARTSY_KEYS_FILE = (0, import_node_path.join)(ARTSY_DIR, "keys.shared.json");
524
+ ARTSY_METAFLAGS_FILE = (0, import_node_path.join)(ARTSY_DIR, "metaflags.json");
525
+ ARTSY_RELAY_SENTINEL = (0, import_node_path.join)(ARTSY_DIR, "src/__generated__/.relay-complete");
526
+ APPS = [
527
+ {
528
+ name: "bluesky",
529
+ label: "Bluesky",
530
+ dir: (0, import_node_path.join)(HOME, "github/bluesky"),
531
+ preferredPort: 8082,
532
+ framework: "expo",
533
+ command: (p) => ({ cmd: `npx expo start --port ${p}` }),
534
+ credentials: {
535
+ envVars: ["SOOTSIM_BLUESKY_PASSWORD"],
536
+ known: { HANDLE: "natew.bsky.social" }
537
+ }
538
+ },
539
+ {
540
+ name: "3pc",
541
+ label: "3PunchConvo",
542
+ dir: (0, import_node_path.join)(HOME, "lightstrike-labs/three-punch-convo-app/apps/one"),
543
+ preferredPort: 8081,
544
+ framework: "one",
545
+ command: (p) => ({ cmd: "npx one dev", env: { ONE_PORT: String(p) } })
546
+ },
547
+ {
548
+ name: "uniswap",
549
+ label: "Uniswap",
550
+ dir: UNISWAP_APP_DIR,
551
+ preferredPort: 8085,
552
+ framework: "expo",
553
+ prepare: () => {
554
+ ensureUniswapDemoEnvLocal();
555
+ ensureUniswapForceUpgradePatched();
556
+ },
557
+ command: (p) => ({
558
+ cmd: `npx expo start --clear --port ${p}`,
559
+ // prefer the real local mobile env when present, otherwise fall back
560
+ // to Uniswap's checked-in public web RPC settings so demo boots cleanly.
561
+ env: resolveUniswapDemoEnv()
562
+ })
563
+ },
564
+ {
565
+ name: "takeout",
566
+ label: "Takeout",
567
+ dir: (0, import_node_path.join)(HOME, "takeout"),
568
+ preferredPort: 8086,
569
+ framework: "one",
570
+ command: (p) => ({ cmd: "npx one dev", env: { ONE_PORT: String(p) } })
571
+ },
572
+ {
573
+ name: "expensify",
574
+ label: "Expensify",
575
+ dir: (0, import_node_path.join)(HOME, "github/expensify"),
576
+ preferredPort: 8087,
577
+ framework: "rock",
578
+ runtimeConfig: {
579
+ env: EXPENSIFY_NATIVE_PROXY_ENV
580
+ },
581
+ sidecars: [
582
+ {
583
+ name: "web-proxy",
584
+ port: 9e3,
585
+ readyPath: "/api/Ping",
586
+ command: () => ({
587
+ cmd: `bun ${JSON.stringify(getExpensifyProxyScript())}`,
588
+ env: EXPENSIFY_NATIVE_PROXY_ENV
589
+ })
590
+ }
591
+ ],
592
+ command: (p) => ({
593
+ cmd: `fnm exec --using=20.20.0 npx rock start --port ${p} --no-interactive`,
594
+ env: EXPENSIFY_NATIVE_PROXY_ENV
595
+ })
596
+ },
597
+ {
598
+ name: "artsy",
599
+ label: "Artsy",
600
+ dir: ARTSY_DIR,
601
+ preferredPort: 8088,
602
+ framework: "expo",
603
+ runtimeConfig: resolveArtsyRuntimeConfig(),
604
+ prepare: () => {
605
+ ensureArtsySetup();
606
+ },
607
+ command: (p) => ({
608
+ // eigen's `yarn start` wraps `react-native start` with a relay watcher
609
+ // via concurrently; for the demo we run relay once in prepare and
610
+ // invoke the metro server directly so --port is respected.
611
+ cmd: `npx react-native start --port ${p}`
612
+ }),
613
+ credentials: {
614
+ envVars: ["SOOTSIM_ARTSY_EMAIL", "SOOTSIM_ARTSY_PASSWORD"],
615
+ note: "auto-login reuses Artsy\u2019s built-in Maestro launch-arguments hook"
616
+ }
617
+ }
618
+ ];
619
+ }
620
+ });
621
+
622
+ // scripts/dev-server-scanner.ts
623
+ function tcpPing(port, timeout = TCP_GATE_MS) {
624
+ return new Promise((resolve2) => {
625
+ const sock = new import_net.default.Socket();
626
+ let settled = false;
627
+ const done = (ok) => {
628
+ if (settled) return;
629
+ settled = true;
630
+ sock.destroy();
631
+ resolve2(ok);
632
+ };
633
+ sock.setTimeout(timeout);
634
+ sock.once("connect", () => done(true));
635
+ sock.once("timeout", () => done(false));
636
+ sock.once("error", () => done(false));
637
+ sock.connect(port, "localhost");
638
+ });
639
+ }
640
+ function httpGet(port, path7, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
641
+ return new Promise((resolve2) => {
642
+ const req = import_http.default.request(
643
+ { hostname: "localhost", port, path: path7, method, timeout, headers },
644
+ (res) => {
645
+ let body = "";
646
+ res.on("data", (c) => body += c.toString());
647
+ res.on("end", () => resolve2({ statusCode: res.statusCode || 0, body }));
648
+ }
649
+ );
650
+ req.on("error", () => resolve2(null));
651
+ req.setTimeout(timeout, () => {
652
+ req.destroy();
653
+ resolve2(null);
654
+ });
655
+ req.end();
656
+ });
657
+ }
658
+ function acceptPort(port, excluded) {
659
+ if (port <= 0 || port >= 2e4) return false;
660
+ if (excluded.has(port)) return false;
661
+ if (port >= 5170 && port <= 5200) return false;
662
+ return true;
663
+ }
664
+ async function discoverListeningProcesses(excludePorts = []) {
665
+ const excluded = new Set(excludePorts);
666
+ try {
667
+ const { stdout } = await execP(
668
+ `lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun)'`,
669
+ { encoding: "utf8", timeout: 2e3 }
670
+ );
671
+ if (stdout.trim()) {
672
+ const seen = /* @__PURE__ */ new Map();
673
+ for (const line of stdout.trim().split("\n")) {
674
+ const parts = line.trim().split(/\s+/);
675
+ if (parts.length < 9) continue;
676
+ const pid = Number(parts[1]);
677
+ const addr = parts[8];
678
+ const m = addr.match(/:(\d+)$/);
679
+ if (!m) continue;
680
+ const port = Number(m[1]);
681
+ if (!acceptPort(port, excluded)) continue;
682
+ if (!seen.has(port)) seen.set(port, pid);
683
+ }
684
+ if (seen.size > 0) {
685
+ return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
686
+ }
687
+ }
688
+ } catch {
689
+ }
690
+ try {
691
+ const { stdout } = await execP(`ss -tlnp 2>/dev/null | grep -E '"(node|bun)"'`, {
692
+ encoding: "utf8",
693
+ timeout: 2e3
694
+ });
695
+ if (stdout.trim()) {
696
+ const seen = /* @__PURE__ */ new Map();
697
+ for (const line of stdout.trim().split("\n")) {
698
+ const portMatch = line.match(/:(\d+)\s/);
699
+ const pidMatch = line.match(/pid=(\d+)/);
700
+ if (!portMatch) continue;
701
+ const port = Number(portMatch[1]);
702
+ const pid = pidMatch ? Number(pidMatch[1]) : 0;
703
+ if (!acceptPort(port, excluded)) continue;
704
+ if (!seen.has(port)) seen.set(port, pid);
705
+ }
706
+ if (seen.size > 0) {
707
+ return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
708
+ }
709
+ }
710
+ } catch {
711
+ }
712
+ return FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded));
713
+ }
714
+ async function resolveProcessCwd(pid) {
715
+ if (pid <= 0) return null;
716
+ const cached = cwdByPid.get(pid);
717
+ if (cached) return cached;
718
+ try {
719
+ const { stdout } = await execP(`lsof -p ${pid} -a -d cwd -Fn 2>/dev/null`, {
720
+ encoding: "utf8",
721
+ timeout: 1500
722
+ });
723
+ for (const line of stdout.split("\n")) {
724
+ if (line.startsWith("n") && line.length > 1) {
725
+ const cwd = line.slice(1).trim();
726
+ if (cwd) {
727
+ cwdByPid.set(pid, cwd);
728
+ return cwd;
729
+ }
730
+ }
731
+ }
732
+ } catch {
733
+ }
734
+ return null;
735
+ }
736
+ function makeResult(port, framework) {
737
+ return {
738
+ port,
739
+ framework,
740
+ bundleUrl: withRuntimeConfig(
741
+ port,
742
+ `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
743
+ ),
744
+ hmrUrl: `ws://localhost:${port}/hot`,
745
+ lastSeen: Date.now()
746
+ };
747
+ }
748
+ function withRuntimeConfig(port, bundleUrl) {
749
+ const knownApp = APPS.find((app) => app.preferredPort === port);
750
+ const configured = knownApp?.runtimeConfig ? applySootSimConfigToUrl(bundleUrl, knownApp.runtimeConfig) : bundleUrl;
751
+ return normalizeNativeDevBundleUrl(configured);
752
+ }
753
+ function isDirectOneBundleUrl(bundleUrl) {
754
+ return bundleUrl.includes("/node_modules/one/metro-entry.bundle");
755
+ }
756
+ function applyManifest(result, manifestRes, buildIconProxyUrl) {
757
+ if (!manifestRes) return result;
758
+ try {
759
+ const manifest = JSON.parse(manifestRes.body);
760
+ const client = manifest?.extra?.expoClient || manifest?.extra || {};
761
+ if (client.name) result.projectName = client.name;
762
+ if (client.ios?.bundleIdentifier) result.bundleId = client.ios.bundleIdentifier;
763
+ if (result.framework === "metro" && client.sdkVersion) result.framework = "expo";
764
+ const launchUrl = manifest?.launchAsset?.url;
765
+ if (launchUrl && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
766
+ result.bundleUrl = withRuntimeConfig(result.port, launchUrl);
767
+ }
768
+ const rawIconUrl = client.iconUrl || client.ios?.iconUrl || client.icon || client.ios?.icon;
769
+ if (rawIconUrl) {
770
+ result.iconPath = rawIconUrl;
771
+ if (buildIconProxyUrl) {
772
+ if (rawIconUrl.startsWith("http")) {
773
+ result.iconUrl = buildIconProxyUrl(rawIconUrl);
774
+ } else {
775
+ const cleanPath = rawIconUrl.replace(/^\.\//, "");
776
+ result.iconUrl = buildIconProxyUrl(
777
+ `http://localhost:${result.port}/assets/${cleanPath}`
778
+ );
779
+ }
780
+ } else {
781
+ result.iconUrl = rawIconUrl.startsWith("http") ? rawIconUrl : `http://localhost:${result.port}/assets/${rawIconUrl.replace(/^\.\//, "")}`;
782
+ }
783
+ }
784
+ } catch {
785
+ }
786
+ return result;
787
+ }
788
+ async function probePort(port, buildIconProxyUrl) {
789
+ if (!await tcpPing(port)) return null;
790
+ const onePath = `/node_modules/one/metro-entry.bundle?platform=ios&dev=true`;
791
+ const [sootsimRes, statusRes, oneRes, manifestRes, expoRes] = await Promise.all([
792
+ knownNonPatched.has(port) ? Promise.resolve(null) : httpGet(port, "/__soot/"),
793
+ httpGet(port, "/status"),
794
+ httpGet(port, onePath, "HEAD"),
795
+ httpGet(port, "/", "GET", TIMEOUT_MS, { "expo-platform": "ios" }),
796
+ knownNonExpo.has(port) ? Promise.resolve(null) : httpGet(port, "/_expo/status")
797
+ ]);
798
+ if (expoRes && expoRes.statusCode === 200) {
799
+ knownNonExpo.delete(port);
800
+ } else if (!knownNonExpo.has(port)) {
801
+ knownNonExpo.add(port);
802
+ }
803
+ if (oneRes && oneRes.statusCode > 0 && oneRes.statusCode < 400) {
804
+ knownNonPatched.add(port);
805
+ return applyManifest(
806
+ {
807
+ port,
808
+ framework: "one",
809
+ bundleUrl: withRuntimeConfig(
810
+ port,
811
+ `http://localhost:${port}${onePath}&minify=false`
812
+ ),
813
+ hmrUrl: `ws://localhost:${port}/hot`,
814
+ lastSeen: Date.now()
815
+ },
816
+ manifestRes,
817
+ buildIconProxyUrl
818
+ );
819
+ }
820
+ if (statusRes && statusRes.body.includes("packager-status:running")) {
821
+ knownNonPatched.add(port);
822
+ return applyManifest(
823
+ makeResult(port, expoRes && expoRes.statusCode === 200 ? "expo" : "metro"),
824
+ manifestRes,
825
+ buildIconProxyUrl
826
+ );
827
+ }
828
+ if (manifestRes) {
829
+ try {
830
+ const manifest = JSON.parse(manifestRes.body);
831
+ const client = manifest?.extra?.expoClient || {};
832
+ if (client.name) {
833
+ const launchUrl = manifest?.launchAsset?.url || `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`;
834
+ knownNonPatched.add(port);
835
+ return applyManifest(
836
+ {
837
+ port,
838
+ framework: "one",
839
+ bundleUrl: withRuntimeConfig(port, launchUrl),
840
+ hmrUrl: `ws://localhost:${port}/hot`,
841
+ lastSeen: Date.now()
842
+ },
843
+ manifestRes,
844
+ buildIconProxyUrl
845
+ );
846
+ }
847
+ } catch {
848
+ }
849
+ }
850
+ if (sootsimRes && sootsimRes.statusCode === 200 && sootsimRes.body.includes("sootsim-patched")) {
851
+ knownNonPatched.delete(port);
852
+ return applyManifest(
853
+ {
854
+ port,
855
+ framework: "one",
856
+ bundleUrl: withRuntimeConfig(port, `http://localhost:${port}/__soot/bundle.js`),
857
+ hmrUrl: `ws://localhost:${port}/hot`,
858
+ lastSeen: Date.now(),
859
+ patched: true
860
+ },
861
+ manifestRes,
862
+ buildIconProxyUrl
863
+ );
864
+ }
865
+ knownNonPatched.add(port);
866
+ return null;
867
+ }
868
+ function isSootSelfServer(server) {
869
+ const projectName = server.projectName?.trim().toLowerCase();
870
+ if (projectName === "soot" || projectName === "sootsim") return true;
871
+ const bundleId = server.bundleId?.trim().toLowerCase();
872
+ if (bundleId?.startsWith("dev.soot")) return true;
873
+ return false;
874
+ }
875
+ function isWeakCachedResult(result) {
876
+ if (!result) return true;
877
+ if (result.framework === "metro" || result.framework === "unknown") return true;
878
+ return false;
879
+ }
880
+ function hasCurrentRuntimeConfig(result) {
881
+ if (!result) return true;
882
+ return withRuntimeConfig(result.port, result.bundleUrl) === result.bundleUrl;
883
+ }
884
+ function __shouldReuseScannerCacheEntry(entry, pid, now = Date.now()) {
885
+ if (pid === 0) return false;
886
+ if (entry.pid !== pid) return false;
887
+ if (!hasCurrentRuntimeConfig(entry.result)) return false;
888
+ const ageMs = now - entry.cachedAt;
889
+ if (entry.result === null && ageMs >= NEGATIVE_CACHE_TTL_MS) return false;
890
+ if (isWeakCachedResult(entry.result) && ageMs >= WEAK_RESULT_CACHE_TTL_MS) return false;
891
+ return true;
892
+ }
893
+ async function scanDevServers(opts = {}) {
894
+ const processes = await discoverListeningProcesses(opts.excludePorts);
895
+ const currentPorts = new Set(processes.map((p) => p.port));
896
+ for (const p of [...portCache.keys()]) {
897
+ if (!currentPorts.has(p)) portCache.delete(p);
898
+ }
899
+ for (const p of [...knownNonPatched]) {
900
+ if (!currentPorts.has(p)) knownNonPatched.delete(p);
901
+ }
902
+ for (const p of [...knownNonExpo]) {
903
+ if (!currentPorts.has(p)) knownNonExpo.delete(p);
904
+ }
905
+ const results = [];
906
+ const toProbe = [];
907
+ for (const { port, pid } of processes) {
908
+ const cached = portCache.get(port);
909
+ if (cached && __shouldReuseScannerCacheEntry(cached, pid)) {
910
+ if (cached.result) results.push(cached.result);
911
+ continue;
912
+ }
913
+ if (cached && cached.pid !== pid) {
914
+ knownNonPatched.delete(port);
915
+ knownNonExpo.delete(port);
916
+ }
917
+ toProbe.push({ port, pid });
918
+ }
919
+ if (toProbe.length > 0) {
920
+ const probed = await Promise.all(
921
+ toProbe.map((p) => probePort(p.port, opts.buildIconProxyUrl))
922
+ );
923
+ probed.forEach((result, i) => {
924
+ const { port, pid } = toProbe[i];
925
+ if (pid !== 0) portCache.set(port, { pid, result, cachedAt: Date.now() });
926
+ if (result) results.push(result);
927
+ });
928
+ }
929
+ const pidByPort = /* @__PURE__ */ new Map();
930
+ for (const { port, pid } of processes) {
931
+ if (pid > 0) pidByPort.set(port, pid);
932
+ }
933
+ await Promise.all(
934
+ results.map(async (result) => {
935
+ const pid = pidByPort.get(result.port);
936
+ if (!pid) return;
937
+ result.pid = pid;
938
+ const cwd = await resolveProcessCwd(pid);
939
+ if (cwd) result.cwd = cwd;
940
+ })
941
+ );
942
+ const livePids = new Set(pidByPort.values());
943
+ for (const pid of [...cwdByPid.keys()]) {
944
+ if (!livePids.has(pid)) cwdByPid.delete(pid);
945
+ }
946
+ return results.filter((r) => !isSootSelfServer(r));
947
+ }
948
+ var import_child_process, import_http, import_net, import_util, execP, TIMEOUT_MS, TCP_GATE_MS, FALLBACK_PORTS, cwdByPid, knownNonPatched, knownNonExpo, portCache, NEGATIVE_CACHE_TTL_MS, WEAK_RESULT_CACHE_TTL_MS;
949
+ var init_dev_server_scanner = __esm({
950
+ "scripts/dev-server-scanner.ts"() {
951
+ "use strict";
952
+ import_child_process = require("child_process");
953
+ import_http = __toESM(require("http"), 1);
954
+ import_net = __toESM(require("net"), 1);
955
+ import_util = require("util");
956
+ init_config();
957
+ init_native_dev_bundle_url();
958
+ init_demo_app_registry();
959
+ execP = (0, import_util.promisify)(import_child_process.exec);
960
+ TIMEOUT_MS = 250;
961
+ TCP_GATE_MS = 120;
962
+ FALLBACK_PORTS = [
963
+ 8081,
964
+ 8082,
965
+ 8083,
966
+ 8084,
967
+ 8085,
968
+ 8086,
969
+ 3e3,
970
+ 3001,
971
+ 19e3
972
+ ].map((port) => ({ port, pid: 0 }));
973
+ cwdByPid = /* @__PURE__ */ new Map();
974
+ knownNonPatched = /* @__PURE__ */ new Set();
975
+ knownNonExpo = /* @__PURE__ */ new Set();
976
+ portCache = /* @__PURE__ */ new Map();
977
+ NEGATIVE_CACHE_TTL_MS = 3e4;
978
+ WEAK_RESULT_CACHE_TTL_MS = 1500;
979
+ }
980
+ });
981
+
982
+ // src/agent-events.ts
983
+ function isAgentEvent(value) {
984
+ if (!value || typeof value !== "object") return false;
985
+ const v = value;
986
+ return typeof v.type === "string" && typeof v.ts === "number";
987
+ }
988
+ function parseAgentEventLine(line) {
989
+ const trimmed = line.trim();
990
+ if (!trimmed) return null;
991
+ try {
992
+ const parsed = JSON.parse(trimmed);
993
+ return isAgentEvent(parsed) ? parsed : null;
994
+ } catch {
995
+ return null;
996
+ }
997
+ }
998
+ var init_agent_events = __esm({
999
+ "src/agent-events.ts"() {
1000
+ "use strict";
1001
+ }
1002
+ });
1003
+
1004
+ // src/agent-prompt.ts
1005
+ function cleanPromptText(value) {
1006
+ return typeof value === "string" ? value.trim() : "";
1007
+ }
1008
+ function encodeAgentPromptEnvelope(input) {
1009
+ const text = cleanPromptText(input.text);
1010
+ if (!text) return "";
1011
+ const displayText = cleanPromptText(input.displayText);
1012
+ const inspectSummary = cleanPromptText(input.inspectSummary);
1013
+ const inspectTrace = cleanPromptText(input.inspectTrace);
1014
+ const needsEnvelope = !!inspectSummary || !!inspectTrace || /[\r\n]/.test(text) || !!displayText && displayText !== text;
1015
+ if (!needsEnvelope) return text;
1016
+ return JSON.stringify({
1017
+ __sootsimAgentPrompt: ENVELOPE_MARKER,
1018
+ text,
1019
+ displayText,
1020
+ inspectSummary,
1021
+ inspectTrace
1022
+ });
1023
+ }
1024
+ var ENVELOPE_MARKER;
1025
+ var init_agent_prompt = __esm({
1026
+ "src/agent-prompt.ts"() {
1027
+ "use strict";
1028
+ ENVELOPE_MARKER = "sootsim-agent-prompt-v1";
1029
+ }
1030
+ });
1031
+
1032
+ // src/attached-projects.ts
1033
+ function userDataDir() {
1034
+ if (overrideDir) return overrideDir;
1035
+ const fromEnv = process.env.SOOTSIM_USER_DATA_DIR;
1036
+ if (fromEnv) return fromEnv;
1037
+ try {
1038
+ const electron = require("electron");
1039
+ if (electron.app?.getPath) return electron.app.getPath("userData");
1040
+ } catch {
1041
+ }
1042
+ return platformDefaultUserDataDir();
1043
+ }
1044
+ function platformDefaultUserDataDir() {
1045
+ const home = import_node_os2.default.homedir();
1046
+ if (process.platform === "darwin") {
1047
+ return import_node_path2.default.join(home, "Library", "Application Support", "sootsim");
1048
+ }
1049
+ if (process.platform === "win32") {
1050
+ return import_node_path2.default.join(process.env.APPDATA || home, "sootsim");
1051
+ }
1052
+ const xdg = process.env.XDG_CONFIG_HOME || import_node_path2.default.join(home, ".config");
1053
+ return import_node_path2.default.join(xdg, "sootsim");
1054
+ }
1055
+ function getUserDataDir() {
1056
+ return userDataDir();
1057
+ }
1058
+ function storeFile() {
1059
+ return import_node_path2.default.join(userDataDir(), "attached-projects.json");
1060
+ }
1061
+ function cloneEmpty() {
1062
+ return {
1063
+ version: 1,
1064
+ attachedProjects: [],
1065
+ previewAttachments: [],
1066
+ agentSessions: []
1067
+ };
1068
+ }
1069
+ function loadStore() {
1070
+ const file = storeFile();
1071
+ let raw;
1072
+ try {
1073
+ raw = import_node_fs2.default.readFileSync(file, "utf8");
1074
+ } catch (err) {
1075
+ if (err.code === "ENOENT") return cloneEmpty();
1076
+ throw err;
1077
+ }
1078
+ try {
1079
+ const parsed = JSON.parse(raw);
1080
+ if (!parsed || typeof parsed !== "object") throw new Error("not an object");
1081
+ return {
1082
+ version: 1,
1083
+ attachedProjects: Array.isArray(parsed.attachedProjects) ? parsed.attachedProjects : [],
1084
+ previewAttachments: Array.isArray(parsed.previewAttachments) ? parsed.previewAttachments : [],
1085
+ agentSessions: Array.isArray(parsed.agentSessions) ? parsed.agentSessions : []
1086
+ };
1087
+ } catch (err) {
1088
+ const quarantine = `${file}.corrupt-${Date.now()}`;
1089
+ try {
1090
+ import_node_fs2.default.renameSync(file, quarantine);
1091
+ console.warn(
1092
+ `[sootsim] attached-projects.json was unparseable; quarantined to ${quarantine}. original error: ${err.message}`
1093
+ );
1094
+ } catch {
1095
+ }
1096
+ return cloneEmpty();
1097
+ }
1098
+ }
1099
+ function writeStore(store) {
1100
+ const file = storeFile();
1101
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
1102
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
1103
+ const fd = import_node_fs2.default.openSync(tmp, "w", 384);
1104
+ try {
1105
+ import_node_fs2.default.writeFileSync(fd, JSON.stringify(store, null, 2));
1106
+ import_node_fs2.default.fsyncSync(fd);
1107
+ } finally {
1108
+ import_node_fs2.default.closeSync(fd);
1109
+ }
1110
+ import_node_fs2.default.renameSync(tmp, file);
1111
+ }
1112
+ function mutateStore(fn) {
1113
+ const store = loadStore();
1114
+ fn(store);
1115
+ writeStore(store);
1116
+ return store;
1117
+ }
1118
+ function projectIdForCwd(cwd) {
1119
+ return (0, import_node_crypto.createHash)("sha256").update(import_node_path2.default.resolve(cwd)).digest("hex").slice(0, 16);
1120
+ }
1121
+ function newSessionId() {
1122
+ return `s_${(0, import_node_crypto.randomBytes)(10).toString("hex")}`;
1123
+ }
1124
+ function upsertProject(input) {
1125
+ const cwd = import_node_path2.default.resolve(input.cwd);
1126
+ const id = projectIdForCwd(cwd);
1127
+ let result;
1128
+ mutateStore((store) => {
1129
+ const existing = store.attachedProjects.find((p) => p.id === id);
1130
+ if (existing) {
1131
+ const merged = {
1132
+ ...existing,
1133
+ ...input,
1134
+ id,
1135
+ cwd,
1136
+ sourceRoots: input.sourceRoots ?? existing.sourceRoots,
1137
+ knownBundleUrls: input.knownBundleUrls ?? existing.knownBundleUrls,
1138
+ pinnedSourceResolutions: input.pinnedSourceResolutions ?? existing.pinnedSourceResolutions,
1139
+ telemetry: input.telemetry ?? existing.telemetry,
1140
+ updatedAt: Date.now(),
1141
+ createdAt: existing.createdAt
1142
+ };
1143
+ const idx = store.attachedProjects.indexOf(existing);
1144
+ store.attachedProjects[idx] = merged;
1145
+ result = merged;
1146
+ return;
1147
+ }
1148
+ const now = Date.now();
1149
+ const created = {
1150
+ id,
1151
+ name: input.name ?? import_node_path2.default.basename(cwd),
1152
+ cwd,
1153
+ repoRoot: input.repoRoot,
1154
+ sourceRoots: input.sourceRoots ?? [cwd],
1155
+ framework: input.framework ?? "unknown",
1156
+ bundleId: input.bundleId,
1157
+ knownBundleUrls: input.knownBundleUrls ?? [],
1158
+ preferredProvider: input.preferredProvider ?? "codex",
1159
+ preferredTransport: input.preferredTransport ?? "tmux",
1160
+ editorOpenCommand: input.editorOpenCommand,
1161
+ moshiWebhookToken: input.moshiWebhookToken,
1162
+ pinnedSourceResolutions: input.pinnedSourceResolutions ?? {},
1163
+ isolateDiscovery: input.isolateDiscovery,
1164
+ git: input.git,
1165
+ telemetry: input.telemetry ?? { lastOpened: 0, runsCompleted: 0 },
1166
+ createdAt: now,
1167
+ updatedAt: now
1168
+ };
1169
+ store.attachedProjects.push(created);
1170
+ result = created;
1171
+ });
1172
+ return result;
1173
+ }
1174
+ function findProjectById(id) {
1175
+ return loadStore().attachedProjects.find((p) => p.id === id) ?? null;
1176
+ }
1177
+ function listProjects() {
1178
+ return loadStore().attachedProjects;
1179
+ }
1180
+ function recordTurnTelemetry(projectId, input = {}) {
1181
+ mutateStore((store) => {
1182
+ const project = store.attachedProjects.find((p) => p.id === projectId);
1183
+ if (!project) return;
1184
+ const ts = input.ts ?? Date.now();
1185
+ project.telemetry.runsCompleted = (project.telemetry.runsCompleted ?? 0) + 1;
1186
+ if (typeof input.usd === "number" && Number.isFinite(input.usd) && input.usd >= 0) {
1187
+ const history = project.telemetry.costHistory ?? [];
1188
+ const cutoff = ts - COST_HISTORY_MAX_AGE_MS;
1189
+ const trimmed = history.filter((e) => e.ts >= cutoff);
1190
+ trimmed.push({ ts, usd: input.usd });
1191
+ project.telemetry.costHistory = trimmed;
1192
+ }
1193
+ project.updatedAt = ts;
1194
+ });
1195
+ }
1196
+ function deleteProject(id) {
1197
+ mutateStore((store) => {
1198
+ store.attachedProjects = store.attachedProjects.filter((p) => p.id !== id);
1199
+ store.agentSessions = store.agentSessions.filter((s) => s.projectId !== id);
1200
+ store.previewAttachments = store.previewAttachments.filter(
1201
+ (pa) => pa.projectId !== id
1202
+ );
1203
+ });
1204
+ }
1205
+ function upsertSession(input) {
1206
+ let result;
1207
+ mutateStore((store) => {
1208
+ if (input.id) {
1209
+ const existing = store.agentSessions.find((s) => s.id === input.id);
1210
+ if (existing) {
1211
+ const merged = {
1212
+ ...existing,
1213
+ ...input,
1214
+ lastSeenAt: Date.now()
1215
+ };
1216
+ const idx = store.agentSessions.indexOf(existing);
1217
+ store.agentSessions[idx] = merged;
1218
+ result = merged;
1219
+ return;
1220
+ }
1221
+ }
1222
+ const project = store.attachedProjects.find((p) => p.id === input.projectId);
1223
+ if (!project) {
1224
+ throw new Error(`upsertSession: no AttachedProject with id=${input.projectId}`);
1225
+ }
1226
+ const now = Date.now();
1227
+ const created = {
1228
+ id: input.id ?? newSessionId(),
1229
+ projectId: input.projectId,
1230
+ provider: input.provider,
1231
+ transport: input.transport ?? project.preferredTransport,
1232
+ cwd: input.cwd ?? project.cwd,
1233
+ claudeSessionUuid: input.claudeSessionUuid,
1234
+ tmuxSessionName: input.tmuxSessionName,
1235
+ wrapperPid: input.wrapperPid,
1236
+ status: input.status ?? "idle",
1237
+ needsAttention: input.needsAttention ?? false,
1238
+ lastPrompt: input.lastPrompt,
1239
+ lastSummary: input.lastSummary,
1240
+ lastTurnFiles: input.lastTurnFiles,
1241
+ currentlyEditing: input.currentlyEditing,
1242
+ lastSeenAt: now,
1243
+ createdAt: now
1244
+ };
1245
+ store.agentSessions.push(created);
1246
+ result = created;
1247
+ });
1248
+ return result;
1249
+ }
1250
+ function findSessionById(id) {
1251
+ return loadStore().agentSessions.find((s) => s.id === id) ?? null;
1252
+ }
1253
+ function listSessions(projectId) {
1254
+ const all = loadStore().agentSessions;
1255
+ return projectId ? all.filter((s) => s.projectId === projectId) : all;
1256
+ }
1257
+ function updateSessionStatus(id, patch) {
1258
+ mutateStore((store) => {
1259
+ const existing = store.agentSessions.find((s) => s.id === id);
1260
+ if (!existing) return;
1261
+ const idx = store.agentSessions.indexOf(existing);
1262
+ store.agentSessions[idx] = {
1263
+ ...existing,
1264
+ ...patch,
1265
+ id: existing.id,
1266
+ projectId: existing.projectId,
1267
+ createdAt: existing.createdAt,
1268
+ lastSeenAt: Date.now()
1269
+ };
1270
+ });
1271
+ }
1272
+ async function seedFromDemoAppRegistry() {
1273
+ const existing = loadStore().attachedProjects;
1274
+ if (existing.length > 0) return;
1275
+ let APPS2;
1276
+ try {
1277
+ const mod = await import("sootsim/scripts/demo-app-registry");
1278
+ APPS2 = mod.APPS;
1279
+ } catch (err) {
1280
+ console.warn(
1281
+ "[sootsim] seedFromDemoAppRegistry: could not load demo registry:",
1282
+ err.message
1283
+ );
1284
+ return;
1285
+ }
1286
+ if (!Array.isArray(APPS2)) return;
1287
+ const apps = APPS2;
1288
+ mutateStore((store) => {
1289
+ for (const app of apps) {
1290
+ if (!import_node_fs2.default.existsSync(app.dir)) continue;
1291
+ const cwd = import_node_path2.default.resolve(app.dir);
1292
+ const id = projectIdForCwd(cwd);
1293
+ if (store.attachedProjects.some((p) => p.id === id)) continue;
1294
+ const now = Date.now();
1295
+ store.attachedProjects.push({
1296
+ id,
1297
+ name: app.label,
1298
+ cwd,
1299
+ sourceRoots: [cwd],
1300
+ framework: app.framework,
1301
+ knownBundleUrls: [`http://localhost:${app.preferredPort}/index.bundle`],
1302
+ preferredProvider: "codex",
1303
+ preferredTransport: "tmux",
1304
+ pinnedSourceResolutions: {},
1305
+ telemetry: { lastOpened: 0, runsCompleted: 0 },
1306
+ createdAt: now,
1307
+ updatedAt: now
1308
+ });
1309
+ }
1310
+ });
1311
+ }
1312
+ var import_node_crypto, import_node_fs2, import_node_os2, import_node_path2, overrideDir, COST_HISTORY_MAX_AGE_MS;
1313
+ var init_attached_projects = __esm({
1314
+ "src/attached-projects.ts"() {
1315
+ "use strict";
1316
+ import_node_crypto = require("node:crypto");
1317
+ import_node_fs2 = __toESM(require("node:fs"), 1);
1318
+ import_node_os2 = __toESM(require("node:os"), 1);
1319
+ import_node_path2 = __toESM(require("node:path"), 1);
1320
+ overrideDir = null;
1321
+ COST_HISTORY_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
1322
+ }
1323
+ });
1324
+
1325
+ // src/agent-sessions.ts
1326
+ function sessionDir(sessionId) {
1327
+ return import_node_path3.default.join(getUserDataDir(), "sessions", sessionId);
1328
+ }
1329
+ function promptFifoPath(sessionId) {
1330
+ return import_node_path3.default.join(sessionDir(sessionId), "prompt.in");
1331
+ }
1332
+ function eventsFifoPath(sessionId) {
1333
+ return import_node_path3.default.join(sessionDir(sessionId), "events.out");
1334
+ }
1335
+ function transcriptPath(sessionId) {
1336
+ return import_node_path3.default.join(getUserDataDir(), "transcripts", `${sessionId}.log`);
1337
+ }
1338
+ function pidIsAlive(pid, sessionId) {
1339
+ if (!pid) return false;
1340
+ try {
1341
+ process.kill(pid, 0);
1342
+ } catch {
1343
+ return false;
1344
+ }
1345
+ if (sessionId) {
1346
+ if (!import_node_fs3.default.existsSync(sessionDir(sessionId))) return false;
1347
+ }
1348
+ return true;
1349
+ }
1350
+ function resolveSootsimInvocation() {
1351
+ if (process.env.SOOTSIM_BIN) {
1352
+ return { cmd: process.env.SOOTSIM_BIN, prefixArgs: [] };
1353
+ }
1354
+ if (process.versions.electron) {
1355
+ const resourcesPath = process.resourcesPath;
1356
+ if (resourcesPath) {
1357
+ const candidates = [
1358
+ import_node_path3.default.join(resourcesPath, "bin", "sootsim"),
1359
+ import_node_path3.default.join(resourcesPath, "bin", `sootsim-${process.platform}-${process.arch}`)
1360
+ ];
1361
+ for (const c of candidates) {
1362
+ if (import_node_fs3.default.existsSync(c)) return { cmd: c, prefixArgs: [] };
1363
+ }
1364
+ }
1365
+ }
1366
+ const workspace = tryWorkspaceSootsim();
1367
+ if (workspace) return workspace;
1368
+ const argv0 = process.argv[0];
1369
+ const argv1 = process.argv[1];
1370
+ if (argv1 && /\.(ts|tsx|mjs|cjs|js)$/.test(argv1)) {
1371
+ return { cmd: argv0, prefixArgs: [argv1] };
1372
+ }
1373
+ if (!argv1 || argv1.includes("/.bin/")) {
1374
+ throw new Error(
1375
+ "sootsim CLI not found. set SOOTSIM_BIN to the path of the sootsim binary, or build the workspace CLI via `bun run --cwd packages/sootsim build:cli`."
1376
+ );
1377
+ }
1378
+ return { cmd: argv0, prefixArgs: [] };
1379
+ }
1380
+ function tryWorkspaceSootsim() {
1381
+ try {
1382
+ const sootsimDir = resolveSootsimPackageDir();
1383
+ if (!sootsimDir) return null;
1384
+ const binaryName = `sootsim-${process.platform}-${process.arch}`;
1385
+ const distBinary = import_node_path3.default.join(sootsimDir, "dist-bin", binaryName);
1386
+ if (import_node_fs3.default.existsSync(distBinary)) return { cmd: distBinary, prefixArgs: [] };
1387
+ const distBin = import_node_path3.default.join(sootsimDir, "dist-cli", "bin.js");
1388
+ if (import_node_fs3.default.existsSync(distBin)) {
1389
+ try {
1390
+ const src = import_node_path3.default.join(sootsimDir, "cli", "commands", "agent-wrapper.ts");
1391
+ if (import_node_fs3.default.existsSync(src)) {
1392
+ const srcMtime = import_node_fs3.default.statSync(src).mtimeMs;
1393
+ const buildMtime = import_node_fs3.default.statSync(distBin).mtimeMs;
1394
+ if (buildMtime < srcMtime) {
1395
+ console.warn(
1396
+ `[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).`
1397
+ );
1398
+ }
1399
+ }
1400
+ } catch {
1401
+ }
1402
+ return { cmd: process.execPath, prefixArgs: [distBin] };
1403
+ }
1404
+ return null;
1405
+ } catch {
1406
+ return null;
1407
+ }
1408
+ }
1409
+ function resolveSootsimPackageDir() {
1410
+ try {
1411
+ const resolved = require.resolve("sootsim/package.json");
1412
+ return import_node_path3.default.dirname(resolved);
1413
+ } catch {
1414
+ }
1415
+ const here = fileFromImportMeta();
1416
+ if (!here) return null;
1417
+ let cur = import_node_path3.default.dirname(here);
1418
+ for (let i = 0; i < 8; i++) {
1419
+ const pkg = import_node_path3.default.join(cur, "package.json");
1420
+ try {
1421
+ if (import_node_fs3.default.existsSync(pkg)) {
1422
+ const parsed = JSON.parse(import_node_fs3.default.readFileSync(pkg, "utf8"));
1423
+ if (parsed.name === "sootsim") return cur;
1424
+ }
1425
+ } catch {
1426
+ }
1427
+ const parent = import_node_path3.default.dirname(cur);
1428
+ if (parent === cur) break;
1429
+ cur = parent;
1430
+ }
1431
+ return null;
1432
+ }
1433
+ function fileFromImportMeta() {
1434
+ try {
1435
+ const url = __sootsim_import_meta_url;
1436
+ if (!url || !url.startsWith("file://")) return null;
1437
+ return decodeURIComponent(url.slice("file://".length));
1438
+ } catch {
1439
+ return null;
1440
+ }
1441
+ }
1442
+ async function withStartLock(projectId, provider, fn) {
1443
+ const lockDir = import_node_path3.default.join(getUserDataDir(), "locks");
1444
+ import_node_fs3.default.mkdirSync(lockDir, { recursive: true });
1445
+ try {
1446
+ import_node_fs3.default.chmodSync(lockDir, 448);
1447
+ } catch {
1448
+ }
1449
+ const lockPath = import_node_path3.default.join(lockDir, `start-${projectId}-${provider}.lock`);
1450
+ const deadline = Date.now() + 4e3;
1451
+ let fd = null;
1452
+ while (fd === null) {
1453
+ try {
1454
+ fd = import_node_fs3.default.openSync(
1455
+ lockPath,
1456
+ import_node_fs3.constants.O_WRONLY | import_node_fs3.constants.O_CREAT | import_node_fs3.constants.O_EXCL,
1457
+ 384
1458
+ );
1459
+ } catch (err) {
1460
+ if (err.code !== "EEXIST") throw err;
1461
+ try {
1462
+ const stale = Number(import_node_fs3.default.readFileSync(lockPath, "utf8").trim());
1463
+ if (stale && !isProcessAlive(stale)) {
1464
+ import_node_fs3.default.unlinkSync(lockPath);
1465
+ continue;
1466
+ }
1467
+ } catch {
1468
+ }
1469
+ if (Date.now() > deadline) {
1470
+ throw new Error(
1471
+ `another start is in progress for project=${projectId} provider=${provider} (lock: ${lockPath})`
1472
+ );
1473
+ }
1474
+ await new Promise((r) => setTimeout(r, 50));
1475
+ }
1476
+ }
1477
+ try {
1478
+ import_node_fs3.default.writeFileSync(fd, String(process.pid));
1479
+ return await fn();
1480
+ } finally {
1481
+ try {
1482
+ import_node_fs3.default.closeSync(fd);
1483
+ } catch {
1484
+ }
1485
+ try {
1486
+ import_node_fs3.default.unlinkSync(lockPath);
1487
+ } catch {
1488
+ }
1489
+ }
1490
+ }
1491
+ function isProcessAlive(pid) {
1492
+ try {
1493
+ process.kill(pid, 0);
1494
+ return true;
1495
+ } catch {
1496
+ return false;
1497
+ }
1498
+ }
1499
+ function mkfifoSync(p) {
1500
+ const parent = import_node_path3.default.dirname(p);
1501
+ import_node_fs3.default.mkdirSync(parent, { recursive: true });
1502
+ try {
1503
+ import_node_fs3.default.chmodSync(parent, 448);
1504
+ } catch {
1505
+ }
1506
+ if (import_node_fs3.default.existsSync(p)) {
1507
+ try {
1508
+ const stat = import_node_fs3.default.statSync(p);
1509
+ if (stat.isFIFO()) {
1510
+ try {
1511
+ import_node_fs3.default.chmodSync(p, 384);
1512
+ } catch {
1513
+ }
1514
+ return;
1515
+ }
1516
+ import_node_fs3.default.unlinkSync(p);
1517
+ } catch {
1518
+ import_node_fs3.default.unlinkSync(p);
1519
+ }
1520
+ }
1521
+ const result = (0, import_node_child_process.spawnSync)("mkfifo", ["-m", "600", p]);
1522
+ if (result.status !== 0) {
1523
+ throw new Error(
1524
+ `mkfifo(${p}) failed: ${result.stderr?.toString().trim() || "unknown error"}`
1525
+ );
1526
+ }
1527
+ }
1528
+ async function startSession(opts) {
1529
+ const project = findProjectById(opts.projectId);
1530
+ if (!project) {
1531
+ throw new AgentSessionError("NO_PROJECT", `no project with id=${opts.projectId}`);
1532
+ }
1533
+ const provider = opts.provider || project.preferredProvider || "codex";
1534
+ return withStartLock(project.id, provider, async () => {
1535
+ const existingLive = listSessions(project.id).find(
1536
+ (s) => s.provider === provider && s.status !== "ended" && pidIsAlive(s.wrapperPid, s.id)
1537
+ );
1538
+ if (existingLive) {
1539
+ throw new AgentSessionError(
1540
+ "ALREADY_RUNNING",
1541
+ `session already running for project=${project.id} provider=${provider} (session ${existingLive.id}, pid ${existingLive.wrapperPid}). end it first with \`sootsim agent end <sessionId>\`.`
1542
+ );
1543
+ }
1544
+ const claudeSessionUuid = provider === "claude" ? (0, import_node_crypto2.randomUUID)() : void 0;
1545
+ const session = upsertSession({
1546
+ projectId: project.id,
1547
+ provider,
1548
+ transport: "pty",
1549
+ cwd: project.cwd,
1550
+ status: "idle",
1551
+ claudeSessionUuid
1552
+ });
1553
+ const promptIn = promptFifoPath(session.id);
1554
+ const eventsOut = eventsFifoPath(session.id);
1555
+ const transcript = transcriptPath(session.id);
1556
+ mkfifoSync(promptIn);
1557
+ mkfifoSync(eventsOut);
1558
+ const transcriptDir = import_node_path3.default.dirname(transcript);
1559
+ import_node_fs3.default.mkdirSync(transcriptDir, { recursive: true });
1560
+ try {
1561
+ import_node_fs3.default.chmodSync(transcriptDir, 448);
1562
+ } catch {
1563
+ }
1564
+ const { cmd, prefixArgs } = resolveSootsimInvocation();
1565
+ const wrapperArgs = [
1566
+ ...prefixArgs,
1567
+ "agent-wrapper",
1568
+ "--session-id",
1569
+ session.id,
1570
+ "--project-id",
1571
+ project.id,
1572
+ "--provider",
1573
+ provider,
1574
+ "--cwd",
1575
+ project.cwd,
1576
+ "--prompt-in",
1577
+ promptIn,
1578
+ "--events-out",
1579
+ eventsOut,
1580
+ "--transcript",
1581
+ transcript
1582
+ ];
1583
+ if (opts.codexBin) wrapperArgs.push("--codex-bin", opts.codexBin);
1584
+ if (opts.claudeBin) wrapperArgs.push("--claude-bin", opts.claudeBin);
1585
+ if (claudeSessionUuid) {
1586
+ wrapperArgs.push("--claude-session-uuid", claudeSessionUuid);
1587
+ }
1588
+ const child = (0, import_node_child_process.spawn)(cmd, wrapperArgs, {
1589
+ detached: true,
1590
+ stdio: "ignore",
1591
+ env: {
1592
+ ...process.env,
1593
+ SOOTSIM_USER_DATA_DIR: getUserDataDir()
1594
+ }
1595
+ });
1596
+ child.unref();
1597
+ const readyTimeout = opts.readyTimeoutMs ?? 6e3;
1598
+ const boot = await waitForFirstEvent(
1599
+ eventsOut,
1600
+ (e) => e.type === "ready" || e.type === "error",
1601
+ readyTimeout
1602
+ );
1603
+ if (!boot || boot.type === "error") {
1604
+ if (child.pid) {
1605
+ try {
1606
+ process.kill(child.pid, "SIGTERM");
1607
+ } catch {
1608
+ }
1609
+ }
1610
+ try {
1611
+ import_node_fs3.default.rmSync(sessionDir(session.id), { recursive: true, force: true });
1612
+ } catch {
1613
+ }
1614
+ updateSessionStatus(session.id, { status: "ended" });
1615
+ const reason = boot && boot.type === "error" ? boot.message : `no ready event within ${readyTimeout}ms`;
1616
+ throw new AgentSessionError("WRAPPER_FAILED", reason);
1617
+ }
1618
+ updateSessionStatus(session.id, {
1619
+ wrapperPid: child.pid,
1620
+ status: "idle"
1621
+ });
1622
+ const updated = findSessionById(session.id);
1623
+ return { session: updated, wrapperPid: child.pid };
1624
+ });
1625
+ }
1626
+ async function sendPrompt(sessionId, prompt) {
1627
+ const session = findSessionById(sessionId);
1628
+ if (!session) {
1629
+ throw new AgentSessionError("NO_SESSION", `no session with id=${sessionId}`);
1630
+ }
1631
+ if (!pidIsAlive(session.wrapperPid, sessionId)) {
1632
+ updateSessionStatus(sessionId, { status: "ended" });
1633
+ throw new AgentSessionError(
1634
+ "NOT_ALIVE",
1635
+ `session wrapper is not alive (pid=${session.wrapperPid}). start a new session.`
1636
+ );
1637
+ }
1638
+ const fifo = promptFifoPath(sessionId);
1639
+ if (!import_node_fs3.default.existsSync(fifo)) {
1640
+ throw new AgentSessionError("NO_FIFO", `prompt FIFO missing: ${fifo}`);
1641
+ }
1642
+ const fd = import_node_fs3.default.openSync(fifo, import_node_fs3.constants.O_WRONLY);
1643
+ try {
1644
+ const wireText = encodeAgentPromptEnvelope(prompt);
1645
+ if (!wireText) {
1646
+ throw new AgentSessionError("EMPTY_PROMPT", "prompt text is empty");
1647
+ }
1648
+ import_node_fs3.default.writeSync(fd, wireText + "\n");
1649
+ } finally {
1650
+ import_node_fs3.default.closeSync(fd);
1651
+ }
1652
+ updateSessionStatus(sessionId, {
1653
+ lastPrompt: prompt.displayText ?? prompt.text,
1654
+ status: "working"
1655
+ });
1656
+ }
1657
+ async function endSession(sessionId) {
1658
+ const session = findSessionById(sessionId);
1659
+ if (!session) {
1660
+ throw new AgentSessionError("NO_SESSION", `no session with id=${sessionId}`);
1661
+ }
1662
+ if (pidIsAlive(session.wrapperPid, sessionId)) {
1663
+ try {
1664
+ process.kill(session.wrapperPid, "SIGTERM");
1665
+ } catch {
1666
+ }
1667
+ }
1668
+ const dir = sessionDir(sessionId);
1669
+ const base = getUserDataDir();
1670
+ if (dir.startsWith(base)) {
1671
+ try {
1672
+ import_node_fs3.default.rmSync(dir, { recursive: true, force: true });
1673
+ } catch {
1674
+ }
1675
+ }
1676
+ updateSessionStatus(sessionId, { status: "ended", wrapperPid: void 0 });
1677
+ }
1678
+ function subscribeEvents(sessionId, onEvent) {
1679
+ const fifo = eventsFifoPath(sessionId);
1680
+ if (!import_node_fs3.default.existsSync(fifo)) {
1681
+ throw new AgentSessionError("NO_FIFO", `events FIFO missing: ${fifo}`);
1682
+ }
1683
+ const fd = import_node_fs3.default.openSync(fifo, import_node_fs3.constants.O_RDWR);
1684
+ const stream = import_node_fs3.default.createReadStream("", { fd, autoClose: true });
1685
+ const rl = import_node_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
1686
+ rl.on("line", (line) => {
1687
+ const event = parseAgentEventLine(line);
1688
+ if (event) onEvent(event);
1689
+ });
1690
+ let closed = false;
1691
+ return () => {
1692
+ if (closed) return;
1693
+ closed = true;
1694
+ try {
1695
+ rl.close();
1696
+ } catch {
1697
+ }
1698
+ try {
1699
+ stream.destroy();
1700
+ } catch {
1701
+ }
1702
+ };
1703
+ }
1704
+ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
1705
+ const fd = import_node_fs3.default.openSync(fifo, import_node_fs3.constants.O_RDWR | import_node_fs3.constants.O_NONBLOCK);
1706
+ const buf = Buffer.alloc(8192);
1707
+ let leftover = "";
1708
+ const deadline = Date.now() + timeoutMs;
1709
+ try {
1710
+ while (Date.now() < deadline) {
1711
+ let n = 0;
1712
+ try {
1713
+ n = import_node_fs3.default.readSync(fd, buf, 0, buf.length, null);
1714
+ } catch (err) {
1715
+ if (err.code !== "EAGAIN") throw err;
1716
+ n = 0;
1717
+ }
1718
+ if (n > 0) {
1719
+ leftover += buf.subarray(0, n).toString("utf8");
1720
+ let idx;
1721
+ while ((idx = leftover.indexOf("\n")) >= 0) {
1722
+ const line = leftover.slice(0, idx);
1723
+ leftover = leftover.slice(idx + 1);
1724
+ const event = parseAgentEventLine(line);
1725
+ if (event && predicate(event)) return event;
1726
+ }
1727
+ } else {
1728
+ await new Promise((r) => setTimeout(r, 30));
1729
+ }
1730
+ }
1731
+ return null;
1732
+ } finally {
1733
+ import_node_fs3.default.closeSync(fd);
1734
+ }
1735
+ }
1736
+ var import_node_child_process, import_node_crypto2, import_node_fs3, import_node_path3, import_node_readline, AgentSessionError;
1737
+ var init_agent_sessions = __esm({
1738
+ "src/agent-sessions.ts"() {
1739
+ "use strict";
1740
+ import_node_child_process = require("node:child_process");
1741
+ import_node_crypto2 = require("node:crypto");
1742
+ import_node_fs3 = __toESM(require("node:fs"), 1);
1743
+ import_node_path3 = __toESM(require("node:path"), 1);
1744
+ import_node_readline = __toESM(require("node:readline"), 1);
1745
+ init_agent_events();
1746
+ init_agent_prompt();
1747
+ init_attached_projects();
1748
+ AgentSessionError = class extends Error {
1749
+ code;
1750
+ constructor(code, message) {
1751
+ super(message);
1752
+ this.code = code;
1753
+ }
1754
+ };
1755
+ }
1756
+ });
1757
+
1758
+ // src/host/agent-host.ts
1759
+ function defaultExcludePorts() {
1760
+ return [
1761
+ Number(process.env.VITE_PORT_WEB || process.env.PORT || 3e3),
1762
+ Number(process.env.VITE_PORT_ZERO || 7849),
1763
+ Number(process.env.VITE_PORT_POSTGRES || 7432),
1764
+ Number(process.env.VITE_PORT_R2 || 9500)
1765
+ ].filter((p) => Number.isFinite(p) && p > 0);
1766
+ }
1767
+ function mapFrameworkToProjectFramework(fw) {
1768
+ if (fw === "expo") return "expo";
1769
+ if (fw === "one" || fw === "vxrn") return "one";
1770
+ return "unknown";
1771
+ }
1772
+ var import_node_fs4, import_node_path4, WS_OPEN, AgentHost;
1773
+ var init_agent_host = __esm({
1774
+ "src/host/agent-host.ts"() {
1775
+ "use strict";
1776
+ import_node_fs4 = __toESM(require("node:fs"), 1);
1777
+ import_node_path4 = __toESM(require("node:path"), 1);
1778
+ init_dev_server_scanner();
1779
+ init_agent_sessions();
1780
+ init_attached_projects();
1781
+ WS_OPEN = 1;
1782
+ AgentHost = class {
1783
+ // live fan-out, keyed by sessionId. refcount tracks how many sockets have
1784
+ // subscribed; the FIFO reader stays open as long as refcount > 0.
1785
+ subscriptions = /* @__PURE__ */ new Map();
1786
+ // per-socket subscription set so ws.close can clean up without scanning
1787
+ // every session.
1788
+ sessionsBySocket = /* @__PURE__ */ new Map();
1789
+ // every connected socket (regardless of role) — gets session-status
1790
+ // pushes so a CLI `sootsim agent sessions --watch` can react to state
1791
+ // changes driven by another client.
1792
+ allSockets = /* @__PURE__ */ new Set();
1793
+ // agent wrappers also emit prompt-received from the FIFO reader, but the
1794
+ // shell/daemon already knows the user-facing display text when send-prompt
1795
+ // is accepted. emit the friendly prompt immediately and suppress the raw
1796
+ // wrapper echoes that follow a moment later.
1797
+ pendingPromptEchoes = /* @__PURE__ */ new Map();
1798
+ // accepted prompts serialize inside the wrapper, so a second send while the
1799
+ // session is already working becomes queued follow-up work. keep a small
1800
+ // in-memory count so session status stays `working` until that backlog
1801
+ // truly drains.
1802
+ pendingTurns = /* @__PURE__ */ new Map();
1803
+ opts;
1804
+ constructor(opts = {}) {
1805
+ this.opts = opts;
1806
+ }
1807
+ registerSocket(ws) {
1808
+ this.allSockets.add(ws);
1809
+ }
1810
+ unregisterSocket(ws) {
1811
+ const sessions = this.sessionsBySocket.get(ws);
1812
+ if (sessions) {
1813
+ for (const sessionId of sessions) {
1814
+ this.decrementSubscription(sessionId);
1815
+ }
1816
+ this.sessionsBySocket.delete(ws);
1817
+ }
1818
+ this.allSockets.delete(ws);
1819
+ }
1820
+ /** handle an agent:* message. returns true iff the message was recognized
1821
+ * as an agent message (so the caller knows to stop dispatching). */
1822
+ async handleMessage(ws, msg) {
1823
+ const type = msg?.type;
1824
+ if (typeof type !== "string" || !type.startsWith("agent:")) return false;
1825
+ const id = msg.id;
1826
+ try {
1827
+ const result = await this.dispatch(ws, type, msg);
1828
+ this.respond(ws, id, result);
1829
+ } catch (err) {
1830
+ if (err instanceof AgentSessionError) {
1831
+ this.respondError(ws, id, err.message, err.code);
1832
+ } else {
1833
+ this.respondError(ws, id, err instanceof Error ? err.message : String(err));
1834
+ }
1835
+ }
1836
+ return true;
1837
+ }
1838
+ /** run once on daemon boot. idempotent — `seedFromDemoAppRegistry` no-ops
1839
+ * when the store already has projects. */
1840
+ async seedOnBoot() {
1841
+ try {
1842
+ await seedFromDemoAppRegistry();
1843
+ } catch (err) {
1844
+ process.stderr.write(
1845
+ `[sootsim-agent] seedFromDemoAppRegistry failed: ${err instanceof Error ? err.message : String(err)}
1846
+ `
1847
+ );
1848
+ }
1849
+ }
1850
+ /** terminate every subscription and drop every socket reference. called
1851
+ * by the bridge host during shutdown. */
1852
+ close() {
1853
+ for (const sub of this.subscriptions.values()) {
1854
+ try {
1855
+ sub.unsubscribe();
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ this.subscriptions.clear();
1860
+ this.sessionsBySocket.clear();
1861
+ this.allSockets.clear();
1862
+ }
1863
+ // --- dispatch ---
1864
+ async dispatch(ws, type, msg) {
1865
+ switch (type) {
1866
+ case "agent:list-projects":
1867
+ return listProjects();
1868
+ case "agent:upsert-project":
1869
+ return upsertProject(msg.input ?? {});
1870
+ case "agent:delete-project":
1871
+ deleteProject(String(msg.projectId));
1872
+ return { ok: true };
1873
+ case "agent:auto-attach-for-url":
1874
+ return this.autoAttachForUrl(msg.input ?? {});
1875
+ case "agent:list-sessions":
1876
+ return listSessions(msg.projectId ? String(msg.projectId) : void 0);
1877
+ case "agent:start-session":
1878
+ return this.doStartSession(msg.input ?? {});
1879
+ case "agent:send-prompt": {
1880
+ const sessionId = String(msg.sessionId);
1881
+ const session = findSessionById(sessionId);
1882
+ if (!session) {
1883
+ throw new AgentSessionError("NO_SESSION", `no session: ${sessionId}`);
1884
+ }
1885
+ const prompt = this.normalizePromptEnvelope(msg);
1886
+ await sendPrompt(sessionId, prompt);
1887
+ return this.notePromptAccepted(sessionId, prompt, session.status === "working");
1888
+ }
1889
+ case "agent:end-session":
1890
+ this.dropSessionFanout(String(msg.sessionId));
1891
+ await endSession(String(msg.sessionId));
1892
+ const ended = findSessionById(String(msg.sessionId));
1893
+ if (ended) {
1894
+ this.broadcastSessionStatus(ended);
1895
+ }
1896
+ return { ok: true };
1897
+ case "agent:get-transcript":
1898
+ return this.getTranscript(String(msg.sessionId));
1899
+ case "agent:get-paths":
1900
+ return this.getPaths();
1901
+ case "agent:subscribe-events":
1902
+ return this.subscribeSocket(ws, String(msg.sessionId));
1903
+ case "agent:unsubscribe-events":
1904
+ return this.unsubscribeSocket(ws, String(msg.sessionId));
1905
+ default:
1906
+ throw new AgentSessionError("UNKNOWN_AGENT_MSG", `unknown agent message: ${type}`);
1907
+ }
1908
+ }
1909
+ // --- operation impls ---
1910
+ async doStartSession(input) {
1911
+ const project = findProjectById(input.projectId);
1912
+ if (!project) {
1913
+ throw new AgentSessionError("NO_PROJECT", `no project: ${input.projectId}`);
1914
+ }
1915
+ const result = await startSession(input);
1916
+ this.broadcastSessionStatus(result.session);
1917
+ return result;
1918
+ }
1919
+ async autoAttachForUrl(input) {
1920
+ const bundleUrl = input.bundleUrl ?? "";
1921
+ const targetPort = (() => {
1922
+ try {
1923
+ return new URL(bundleUrl).port || null;
1924
+ } catch {
1925
+ return null;
1926
+ }
1927
+ })();
1928
+ if (!targetPort) return { project: null };
1929
+ const excludePorts = this.opts.getExcludePorts?.() ?? defaultExcludePorts();
1930
+ const servers = await scanDevServers({ excludePorts });
1931
+ const match = servers.find((s) => String(s.port) === targetPort);
1932
+ if (!match || !match.cwd) return { project: null };
1933
+ const existing = listProjects().find((p) => p.cwd === match.cwd) ?? null;
1934
+ const knownBundleUrls = Array.from(
1935
+ /* @__PURE__ */ new Set([...existing?.knownBundleUrls ?? [], match.bundleUrl, bundleUrl])
1936
+ );
1937
+ const project = upsertProject({
1938
+ cwd: match.cwd,
1939
+ name: match.projectName ?? import_node_path4.default.basename(match.cwd),
1940
+ preferredProvider: input.provider ?? existing?.preferredProvider,
1941
+ sourceRoots: existing?.sourceRoots ?? [match.cwd],
1942
+ knownBundleUrls,
1943
+ framework: existing?.framework ?? mapFrameworkToProjectFramework(match.framework),
1944
+ bundleId: match.bundleId ?? existing?.bundleId
1945
+ });
1946
+ return { project };
1947
+ }
1948
+ getTranscript(sessionId) {
1949
+ const p = transcriptPath(sessionId);
1950
+ if (!import_node_fs4.default.existsSync(p)) {
1951
+ return { error: "transcript not found", code: "NO_TRANSCRIPT" };
1952
+ }
1953
+ return import_node_fs4.default.readFileSync(p, "utf8");
1954
+ }
1955
+ getPaths() {
1956
+ const dir = getUserDataDir();
1957
+ return {
1958
+ userDataDir: dir,
1959
+ storeFile: import_node_path4.default.join(dir, "attached-projects.json"),
1960
+ sessionsDir: import_node_path4.default.join(dir, "sessions"),
1961
+ transcriptsDir: import_node_path4.default.join(dir, "transcripts")
1962
+ };
1963
+ }
1964
+ // --- subscription management ---
1965
+ subscribeSocket(ws, sessionId) {
1966
+ let sockets = this.sessionsBySocket.get(ws);
1967
+ if (!sockets) {
1968
+ sockets = /* @__PURE__ */ new Set();
1969
+ this.sessionsBySocket.set(ws, sockets);
1970
+ }
1971
+ if (sockets.has(sessionId)) {
1972
+ return { ok: true, refCount: this.subscriptions.get(sessionId)?.refCount ?? 1 };
1973
+ }
1974
+ sockets.add(sessionId);
1975
+ const existing = this.subscriptions.get(sessionId);
1976
+ if (existing) {
1977
+ existing.refCount++;
1978
+ return { ok: true, refCount: existing.refCount };
1979
+ }
1980
+ const unsubscribe = subscribeEvents(sessionId, (event) => {
1981
+ const coalesced = this.coalescePromptEcho(sessionId, event);
1982
+ if (coalesced) {
1983
+ this.applySessionEvent(sessionId, coalesced);
1984
+ this.fanOutEvent(sessionId, coalesced);
1985
+ }
1986
+ if (event.type === "turn-completed") {
1987
+ const session = findSessionById(sessionId);
1988
+ if (session) {
1989
+ try {
1990
+ recordTurnTelemetry(session.projectId, {
1991
+ usd: event.costUsd,
1992
+ ts: event.ts
1993
+ });
1994
+ } catch (err) {
1995
+ process.stderr.write(
1996
+ `[sootsim-agent] recordTurnTelemetry failed: ${err instanceof Error ? err.message : String(err)}
1997
+ `
1998
+ );
1999
+ }
2000
+ }
2001
+ }
2002
+ });
2003
+ this.subscriptions.set(sessionId, { unsubscribe, refCount: 1 });
2004
+ return { ok: true, refCount: 1 };
2005
+ }
2006
+ unsubscribeSocket(ws, sessionId) {
2007
+ const sockets = this.sessionsBySocket.get(ws);
2008
+ if (!sockets || !sockets.has(sessionId)) return { ok: true, refCount: 0 };
2009
+ sockets.delete(sessionId);
2010
+ return this.decrementSubscription(sessionId);
2011
+ }
2012
+ decrementSubscription(sessionId) {
2013
+ const existing = this.subscriptions.get(sessionId);
2014
+ if (!existing) return { ok: true, refCount: 0 };
2015
+ existing.refCount--;
2016
+ if (existing.refCount <= 0) {
2017
+ try {
2018
+ existing.unsubscribe();
2019
+ } catch {
2020
+ }
2021
+ this.subscriptions.delete(sessionId);
2022
+ return { ok: true, refCount: 0 };
2023
+ }
2024
+ return { ok: true, refCount: existing.refCount };
2025
+ }
2026
+ /** end-session tears the FIFO down, so drop our reader before it
2027
+ * disappears regardless of remaining subscriber refcount. */
2028
+ dropSessionFanout(sessionId) {
2029
+ const existing = this.subscriptions.get(sessionId);
2030
+ if (existing) {
2031
+ try {
2032
+ existing.unsubscribe();
2033
+ } catch {
2034
+ }
2035
+ this.subscriptions.delete(sessionId);
2036
+ }
2037
+ for (const sockets of this.sessionsBySocket.values()) {
2038
+ sockets.delete(sessionId);
2039
+ }
2040
+ this.clearPromptTracking(sessionId);
2041
+ }
2042
+ // --- wire pushes + responses ---
2043
+ normalizePromptEnvelope(msg) {
2044
+ if (msg?.prompt && typeof msg.prompt === "object") {
2045
+ const prompt = msg.prompt;
2046
+ return {
2047
+ text: String(prompt.text ?? ""),
2048
+ ...typeof prompt.displayText === "string" ? { displayText: prompt.displayText } : {},
2049
+ ...typeof prompt.inspectSummary === "string" ? { inspectSummary: prompt.inspectSummary } : {},
2050
+ ...typeof prompt.inspectTrace === "string" ? { inspectTrace: prompt.inspectTrace } : {}
2051
+ };
2052
+ }
2053
+ return {
2054
+ text: String(msg?.text ?? ""),
2055
+ ...typeof msg?.displayText === "string" ? { displayText: msg.displayText } : {},
2056
+ ...typeof msg?.inspectSummary === "string" ? { inspectSummary: msg.inspectSummary } : {},
2057
+ ...typeof msg?.inspectTrace === "string" ? { inspectTrace: msg.inspectTrace } : {}
2058
+ };
2059
+ }
2060
+ notePromptAccepted(sessionId, prompt, assumeQueued) {
2061
+ const now = Date.now();
2062
+ const echoes = this.pendingPromptEchoes.get(sessionId) ?? [];
2063
+ echoes.push({ sentAt: now });
2064
+ this.pendingPromptEchoes.set(sessionId, echoes);
2065
+ const pendingTurns = Math.max(this.pendingTurns.get(sessionId) ?? 0, assumeQueued ? 1 : 0) + 1;
2066
+ this.pendingTurns.set(sessionId, pendingTurns);
2067
+ const promptText = prompt.displayText ?? prompt.text;
2068
+ this.patchSession(sessionId, {
2069
+ lastPrompt: promptText,
2070
+ status: "working",
2071
+ needsAttention: false
2072
+ });
2073
+ this.fanOutEvent(sessionId, {
2074
+ type: "prompt-received",
2075
+ text: promptText,
2076
+ ...prompt.inspectSummary ? { inspectSummary: prompt.inspectSummary } : {},
2077
+ ...prompt.inspectTrace ? { inspectTrace: prompt.inspectTrace } : {},
2078
+ ts: now
2079
+ });
2080
+ return {
2081
+ ok: true,
2082
+ queued: pendingTurns > 1,
2083
+ pendingTurns,
2084
+ queueDepth: Math.max(0, pendingTurns - 1)
2085
+ };
2086
+ }
2087
+ applySessionEvent(sessionId, event) {
2088
+ switch (event.type) {
2089
+ case "prompt-received":
2090
+ case "turn-started":
2091
+ this.patchSession(sessionId, {
2092
+ status: "working",
2093
+ needsAttention: false
2094
+ });
2095
+ return;
2096
+ case "turn-completed": {
2097
+ const pendingTurns = this.consumeSettledTurn(sessionId);
2098
+ this.patchSession(sessionId, {
2099
+ status: pendingTurns > 0 ? "working" : "idle",
2100
+ needsAttention: false,
2101
+ lastTurnFiles: event.filesTouched,
2102
+ currentlyEditing: void 0
2103
+ });
2104
+ return;
2105
+ }
2106
+ case "approval-needed":
2107
+ this.patchSession(sessionId, {
2108
+ status: "needs-attention",
2109
+ needsAttention: true
2110
+ });
2111
+ return;
2112
+ case "error": {
2113
+ const pendingTurns = this.consumeSettledTurn(sessionId);
2114
+ this.patchSession(sessionId, {
2115
+ status: pendingTurns > 0 ? "working" : "needs-attention",
2116
+ needsAttention: pendingTurns <= 0,
2117
+ currentlyEditing: void 0
2118
+ });
2119
+ return;
2120
+ }
2121
+ case "exited":
2122
+ this.clearPromptTracking(sessionId);
2123
+ this.patchSession(sessionId, {
2124
+ status: "ended",
2125
+ needsAttention: false,
2126
+ wrapperPid: void 0,
2127
+ currentlyEditing: void 0
2128
+ });
2129
+ return;
2130
+ case "ready":
2131
+ case "turn-reasoning":
2132
+ case "turn-message":
2133
+ case "turn-plan":
2134
+ case "tool-call":
2135
+ case "file-edited":
2136
+ case "file-diff-delta":
2137
+ return;
2138
+ }
2139
+ }
2140
+ patchSession(sessionId, patch) {
2141
+ updateSessionStatus(sessionId, patch);
2142
+ const updated = findSessionById(sessionId);
2143
+ if (updated) {
2144
+ this.broadcastSessionStatus(updated);
2145
+ }
2146
+ }
2147
+ coalescePromptEcho(sessionId, event) {
2148
+ if (event.type !== "prompt-received") return event;
2149
+ const pending = this.pendingPromptEchoes.get(sessionId);
2150
+ if (!pending || pending.length === 0) return event;
2151
+ while (pending.length > 0 && Date.now() - pending[0].sentAt > 15e3) {
2152
+ pending.shift();
2153
+ }
2154
+ if (pending.length === 0) {
2155
+ this.pendingPromptEchoes.delete(sessionId);
2156
+ return event;
2157
+ }
2158
+ pending.shift();
2159
+ if (pending.length === 0) {
2160
+ this.pendingPromptEchoes.delete(sessionId);
2161
+ } else {
2162
+ this.pendingPromptEchoes.set(sessionId, pending);
2163
+ }
2164
+ return null;
2165
+ }
2166
+ consumeSettledTurn(sessionId) {
2167
+ const pendingTurns = Math.max(0, (this.pendingTurns.get(sessionId) ?? 1) - 1);
2168
+ if (pendingTurns > 0) {
2169
+ this.pendingTurns.set(sessionId, pendingTurns);
2170
+ } else {
2171
+ this.pendingTurns.delete(sessionId);
2172
+ }
2173
+ return pendingTurns;
2174
+ }
2175
+ clearPromptTracking(sessionId) {
2176
+ this.pendingPromptEchoes.delete(sessionId);
2177
+ this.pendingTurns.delete(sessionId);
2178
+ }
2179
+ fanOutEvent(sessionId, event) {
2180
+ const payload = JSON.stringify({ type: "agent:event", sessionId, event });
2181
+ for (const [ws, sessions] of this.sessionsBySocket) {
2182
+ if (!sessions.has(sessionId)) continue;
2183
+ if (ws.readyState !== WS_OPEN) continue;
2184
+ try {
2185
+ ws.send(payload);
2186
+ } catch {
2187
+ }
2188
+ }
2189
+ }
2190
+ broadcastSessionStatus(session) {
2191
+ const payload = JSON.stringify({ type: "agent:session-status", session });
2192
+ for (const ws of this.allSockets) {
2193
+ if (ws.readyState !== WS_OPEN) continue;
2194
+ try {
2195
+ ws.send(payload);
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ }
2200
+ respond(ws, id, result) {
2201
+ if (ws.readyState !== WS_OPEN) return;
2202
+ try {
2203
+ ws.send(JSON.stringify({ id, result }));
2204
+ } catch {
2205
+ }
2206
+ }
2207
+ respondError(ws, id, error, code) {
2208
+ if (ws.readyState !== WS_OPEN) return;
2209
+ try {
2210
+ ws.send(JSON.stringify({ id, error, ...code ? { code } : {} }));
2211
+ } catch {
2212
+ }
2213
+ }
2214
+ };
2215
+ }
2216
+ });
2217
+
2218
+ // src/host/bridge-host.ts
2219
+ var bridge_host_exports = {};
2220
+ __export(bridge_host_exports, {
2221
+ SootSimBridgeHost: () => SootSimBridgeHost
2222
+ });
2223
+ function shouldAcquireLease(msg) {
2224
+ if (!msg || typeof msg.type !== "string") return false;
2225
+ if (msg.acquireLock === true) return true;
2226
+ if (msg.readOnly === true) return false;
2227
+ return WRITE_COMMAND_TYPES.has(msg.type);
2228
+ }
2229
+ var import_child_process2, import_fs2, import_http2, import_path2, import_ws, WRITE_COMMAND_TYPES, DAEMON_HEARTBEAT_INTERVAL_MS, HTTP_MIME_TYPES, SootSimBridgeHost;
2230
+ var init_bridge_host = __esm({
2231
+ "src/host/bridge-host.ts"() {
2232
+ "use strict";
2233
+ import_child_process2 = require("child_process");
2234
+ import_fs2 = __toESM(require("fs"), 1);
2235
+ import_http2 = require("http");
2236
+ import_path2 = __toESM(require("path"), 1);
2237
+ import_ws = require("ws");
2238
+ init_bridge_constants();
2239
+ init_home_paths();
2240
+ init_agent_host();
2241
+ WRITE_COMMAND_TYPES = /* @__PURE__ */ new Set(["tap", "keyboard", "close"]);
2242
+ DAEMON_HEARTBEAT_INTERVAL_MS = 5e3;
2243
+ HTTP_MIME_TYPES = {
2244
+ ".html": "text/html; charset=utf-8",
2245
+ ".js": "application/javascript",
2246
+ ".cjs": "application/javascript",
2247
+ ".mjs": "application/javascript",
2248
+ ".css": "text/css; charset=utf-8",
2249
+ ".json": "application/json; charset=utf-8",
2250
+ ".png": "image/png",
2251
+ ".jpg": "image/jpeg",
2252
+ ".jpeg": "image/jpeg",
2253
+ ".gif": "image/gif",
2254
+ ".svg": "image/svg+xml",
2255
+ ".webp": "image/webp",
2256
+ ".avif": "image/avif",
2257
+ ".ico": "image/x-icon",
2258
+ ".wasm": "application/wasm",
2259
+ ".ttf": "font/ttf",
2260
+ ".otf": "font/otf",
2261
+ ".woff": "font/woff",
2262
+ ".woff2": "font/woff2",
2263
+ ".map": "application/json",
2264
+ ".txt": "text/plain; charset=utf-8"
2265
+ };
2266
+ SootSimBridgeHost = class _SootSimBridgeHost {
2267
+ port;
2268
+ openUrlHandler;
2269
+ httpServer = null;
2270
+ wss = null;
2271
+ nextCommandId = 1;
2272
+ nextBrowserId = 1;
2273
+ browsers = /* @__PURE__ */ new Map();
2274
+ primaryBrowserId = null;
2275
+ pendingCommands = /* @__PURE__ */ new Map();
2276
+ cliBySentId = /* @__PURE__ */ new Map();
2277
+ cliBrowserBySocket = /* @__PURE__ */ new Map();
2278
+ cliLastCommandAt = /* @__PURE__ */ new Map();
2279
+ cliSessionKeyBySocket = /* @__PURE__ */ new Map();
2280
+ cliLabelBySocket = /* @__PURE__ */ new Map();
2281
+ restorableBrowsers = /* @__PURE__ */ new Map();
2282
+ nextCliFallbackId = 1;
2283
+ cliIdleTimer = null;
2284
+ agentHost;
2285
+ static CLI_IDLE_TIMEOUT_MS = 6e4;
2286
+ static CLI_LEASE_TTL_MS = 6e5;
2287
+ static USER_ACTIVE_LEASE_TTL_MS = 8e3;
2288
+ static BROWSER_RECONNECT_TTL_MS = 3e4;
2289
+ preferredPort;
2290
+ portFallbackCount;
2291
+ shouldWriteLockfile;
2292
+ effectivePort = 0;
2293
+ startedAt = 0;
2294
+ heartbeatTimer = null;
2295
+ activeRuntimeVersion = null;
2296
+ activeRuntimeDirPath = null;
2297
+ constructor(opts = {}) {
2298
+ this.preferredPort = opts.port || DEFAULT_SOOTSIM_BRIDGE_PORT;
2299
+ this.port = this.preferredPort;
2300
+ this.shouldWriteLockfile = opts.writeLockfile === true;
2301
+ const defaultFallback = this.shouldWriteLockfile ? 1 : 10;
2302
+ this.portFallbackCount = Math.max(1, opts.portFallbackCount ?? defaultFallback);
2303
+ this.openUrlHandler = opts.openUrl;
2304
+ this.agentHost = new AgentHost({ getExcludePorts: opts.agentScanExcludes });
2305
+ }
2306
+ /** expose the agent host so tests and embedders can inspect state or
2307
+ * inject behavior. not part of the public WS protocol. */
2308
+ getAgentHost() {
2309
+ return this.agentHost;
2310
+ }
2311
+ /** synchronous wrapper around startAsync for callers that don't care
2312
+ * about port fallback outcomes. returns immediately; actual binding
2313
+ * happens on the event loop. callers that need to know the bound port
2314
+ * should await startAsync() instead. */
2315
+ start(options) {
2316
+ void this.startAsync(options);
2317
+ }
2318
+ async startAsync(options) {
2319
+ if (this.httpServer || this.wss) return this.effectivePort;
2320
+ this.refreshActiveRuntime();
2321
+ for (let attempt = 0; attempt < this.portFallbackCount; attempt++) {
2322
+ const candidate = this.preferredPort + attempt;
2323
+ try {
2324
+ await this.bindOnce(candidate, options?.silent === true);
2325
+ this.effectivePort = candidate;
2326
+ this.port = candidate;
2327
+ this.startedAt = Date.now();
2328
+ if (attempt > 0 && !options?.silent) {
2329
+ process.stderr.write(
2330
+ `ws bridge bound to port ${candidate} (preferred ${this.preferredPort} was taken)
2331
+ `
2332
+ );
2333
+ }
2334
+ this.afterBind();
2335
+ return candidate;
2336
+ } catch (err) {
2337
+ const e = err;
2338
+ if (e?.code !== "EADDRINUSE") {
2339
+ throw err;
2340
+ }
2341
+ if (!options?.silent) {
2342
+ process.stderr.write(
2343
+ `ws bridge port ${candidate} already in use, trying ${candidate + 1}
2344
+ `
2345
+ );
2346
+ }
2347
+ }
2348
+ }
2349
+ throw new Error(
2350
+ `could not bind ws bridge after ${this.portFallbackCount} attempts starting at ${this.preferredPort}`
2351
+ );
2352
+ }
2353
+ bindOnce(port, _silent) {
2354
+ return new Promise((resolve2, reject) => {
2355
+ const server = (0, import_http2.createServer)((req, res) => this.handleHttpRequest(req, res));
2356
+ let settled = false;
2357
+ const onError = (err) => {
2358
+ if (settled) return;
2359
+ settled = true;
2360
+ try {
2361
+ server.close();
2362
+ } catch {
2363
+ }
2364
+ this.httpServer = null;
2365
+ this.wss = null;
2366
+ reject(err);
2367
+ };
2368
+ server.once("error", onError);
2369
+ server.listen(port, "127.0.0.1", () => {
2370
+ if (settled) return;
2371
+ settled = true;
2372
+ server.removeListener("error", onError);
2373
+ server.on("error", (err) => {
2374
+ process.stderr.write(`ws bridge http error: ${String(err)}
2375
+ `);
2376
+ });
2377
+ this.httpServer = server;
2378
+ this.wss = new import_ws.WebSocketServer({ server });
2379
+ this.wireWebSocketServer();
2380
+ resolve2();
2381
+ });
2382
+ });
2383
+ }
2384
+ /** attach the WS connection handler to the current wss. called from
2385
+ * bindOnce() after WebSocketServer is freshly created. */
2386
+ wireWebSocketServer() {
2387
+ if (!this.wss) return;
2388
+ this.wss.on("connection", (ws, req) => {
2389
+ const origin = req.headers.origin;
2390
+ const role = origin ? "browser" : "cli";
2391
+ let browser = null;
2392
+ this.agentHost.registerSocket(ws);
2393
+ if (role === "browser") {
2394
+ browser = {
2395
+ id: `tab-${this.nextBrowserId++}`,
2396
+ ws,
2397
+ origin,
2398
+ connectedAt: Date.now(),
2399
+ lastSeenAt: Date.now(),
2400
+ lastActiveAt: 0,
2401
+ recentActions: []
2402
+ };
2403
+ this.browsers.set(browser.id, browser);
2404
+ if (this.shouldPromoteBrowser(browser)) {
2405
+ this.primaryBrowserId = browser.id;
2406
+ }
2407
+ this.broadcastBrowserAssignments();
2408
+ this.broadcastBrowserClientStates();
2409
+ } else {
2410
+ const fallbackKey = `ws-${this.nextCliFallbackId++}`;
2411
+ this.cliSessionKeyBySocket.set(ws, fallbackKey);
2412
+ }
2413
+ ws.on("message", (data) => {
2414
+ let msg;
2415
+ try {
2416
+ msg = JSON.parse(data.toString());
2417
+ } catch {
2418
+ return;
2419
+ }
2420
+ if (!msg || typeof msg !== "object") return;
2421
+ if (typeof msg.type === "string" && msg.type.startsWith("agent:")) {
2422
+ void this.agentHost.handleMessage(ws, msg);
2423
+ return;
2424
+ }
2425
+ if (msg.type === "runtime:list") {
2426
+ const versions = listInstalledRuntimes();
2427
+ const active = this.getActiveRuntime();
2428
+ const reply = {
2429
+ type: "runtime:list:ok",
2430
+ id: msg.id,
2431
+ installed: versions,
2432
+ active: active.version,
2433
+ activeRuntimeDir: active.runtimeDir
2434
+ };
2435
+ try {
2436
+ ws.send(JSON.stringify(reply));
2437
+ } catch {
2438
+ }
2439
+ return;
2440
+ }
2441
+ if (msg.type === "runtime:use") {
2442
+ const version = typeof msg.version === "string" ? msg.version : "";
2443
+ const installed = listInstalledRuntimes();
2444
+ if (!installed.includes(version)) {
2445
+ try {
2446
+ ws.send(
2447
+ JSON.stringify({
2448
+ type: "runtime:use:error",
2449
+ id: msg.id,
2450
+ error: `runtime ${version || "(missing)"} is not installed`
2451
+ })
2452
+ );
2453
+ } catch {
2454
+ }
2455
+ return;
2456
+ }
2457
+ const result = this.setActiveRuntime(version);
2458
+ try {
2459
+ ws.send(
2460
+ JSON.stringify({
2461
+ type: "runtime:use:ok",
2462
+ id: msg.id,
2463
+ version: result.version,
2464
+ runtimeDir: result.runtimeDir
2465
+ })
2466
+ );
2467
+ } catch {
2468
+ }
2469
+ return;
2470
+ }
2471
+ if (msg.type === "runtime:get") {
2472
+ const active = this.getActiveRuntime();
2473
+ try {
2474
+ ws.send(
2475
+ JSON.stringify({
2476
+ type: "runtime:get:ok",
2477
+ id: msg.id,
2478
+ active: active.version,
2479
+ activeRuntimeDir: active.runtimeDir
2480
+ })
2481
+ );
2482
+ } catch {
2483
+ }
2484
+ return;
2485
+ }
2486
+ if (role === "browser") {
2487
+ if (browser) {
2488
+ browser.lastSeenAt = Date.now();
2489
+ }
2490
+ if (msg.type === "bridge:register" && browser) {
2491
+ const registration = msg;
2492
+ const restored = this.tryRestoreBrowserId(browser, registration.browserId);
2493
+ browser.url = registration.url;
2494
+ browser.title = registration.title;
2495
+ browser.userAgent = registration.userAgent;
2496
+ if (restored) {
2497
+ this.broadcastBrowserAssignments();
2498
+ this.broadcastBrowserClientStates();
2499
+ }
2500
+ return;
2501
+ }
2502
+ if (msg.type === "bridge:user-focus-state" && browser) {
2503
+ const focusState = msg;
2504
+ this.updateUserFocusLease(browser, focusState.focused === true);
2505
+ return;
2506
+ }
2507
+ if (msg.type === "bridge:user-interact" && browser) {
2508
+ this.updateUserActivity(browser);
2509
+ return;
2510
+ }
2511
+ if (msg.type === "bridge:open-path") {
2512
+ const filePath = typeof msg.path === "string" ? msg.path : "";
2513
+ const line = typeof msg.line === "number" && Number.isFinite(msg.line) ? msg.line : void 0;
2514
+ const column = typeof msg.column === "number" && Number.isFinite(msg.column) ? msg.column : void 0;
2515
+ if (filePath) {
2516
+ void this.openPathInEditor(filePath, line, column);
2517
+ }
2518
+ return;
2519
+ }
2520
+ if (msg.type === "bridge:boot-clients" && browser) {
2521
+ const booted = [];
2522
+ for (const [cliWs, attachedBrowserId] of this.cliBrowserBySocket) {
2523
+ if (attachedBrowserId === browser.id) {
2524
+ booted.push(cliWs);
2525
+ }
2526
+ }
2527
+ for (const cliWs of booted) {
2528
+ this.cliBrowserBySocket.delete(cliWs);
2529
+ try {
2530
+ cliWs.close(1e3, "booted by browser");
2531
+ } catch {
2532
+ }
2533
+ }
2534
+ const hadLease = !!browser.cliLease;
2535
+ browser.cliLease = void 0;
2536
+ if (booted.length > 0 || hadLease) {
2537
+ process.stderr.write(
2538
+ `sootsim booted ${booted.length} cli client(s)${hadLease ? " + cleared lease" : ""} from [${browser.id}]
2539
+ `
2540
+ );
2541
+ this.recordBrowserAction(browser.id, "browser booted cli clients");
2542
+ this.broadcastBrowserClientStates();
2543
+ }
2544
+ return;
2545
+ }
2546
+ const internalPending = this.pendingCommands.get(msg.id);
2547
+ if (internalPending) {
2548
+ this.pendingCommands.delete(msg.id);
2549
+ if (msg.error) internalPending.reject(new Error(msg.error));
2550
+ else internalPending.resolve(msg.result);
2551
+ return;
2552
+ }
2553
+ const entry = this.cliBySentId.get(msg.id);
2554
+ if (entry) {
2555
+ this.cliBySentId.delete(msg.id);
2556
+ if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
2557
+ const otherCliCount = this.getOtherCliSessionCount(
2558
+ entry.ws,
2559
+ entry.browserId
2560
+ );
2561
+ const response = otherCliCount > 0 ? { ...msg, id: entry.originalId, _otherCliCount: otherCliCount } : { ...msg, id: entry.originalId };
2562
+ entry.ws.send(JSON.stringify(response));
2563
+ }
2564
+ }
2565
+ return;
2566
+ }
2567
+ void (async () => {
2568
+ this.cliLastCommandAt.set(ws, Date.now());
2569
+ try {
2570
+ if (msg.type === "bridge:bye") {
2571
+ const hadBrowser = this.cliBrowserBySocket.delete(ws);
2572
+ this.cliLastCommandAt.delete(ws);
2573
+ this.cliSessionKeyBySocket.delete(ws);
2574
+ this.cliLabelBySocket.delete(ws);
2575
+ for (const [sentId2, entry] of this.cliBySentId) {
2576
+ if (entry.ws === ws) this.cliBySentId.delete(sentId2);
2577
+ }
2578
+ if (hadBrowser) this.broadcastBrowserClientStates();
2579
+ return;
2580
+ }
2581
+ if (msg.type === "bridge:hello") {
2582
+ const key = typeof msg.cliSessionKey === "string" && msg.cliSessionKey.trim() ? msg.cliSessionKey.trim() : this.cliSessionKeyBySocket.get(ws) || `ws-${this.nextCliFallbackId++}`;
2583
+ this.cliSessionKeyBySocket.set(ws, key);
2584
+ if (typeof msg.cliLabel === "string" && msg.cliLabel.trim()) {
2585
+ this.cliLabelBySocket.set(ws, msg.cliLabel.trim());
2586
+ }
2587
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2588
+ ws.send(
2589
+ JSON.stringify({
2590
+ id: msg.id,
2591
+ result: {
2592
+ cliSessionKey: key,
2593
+ leaseTtlMs: _SootSimBridgeHost.CLI_LEASE_TTL_MS,
2594
+ leasing: true
2595
+ }
2596
+ })
2597
+ );
2598
+ }
2599
+ return;
2600
+ }
2601
+ if (msg.type === "bridge:list-browsers") {
2602
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2603
+ ws.send(
2604
+ JSON.stringify({
2605
+ id: msg.id,
2606
+ result: this.listBrowsers()
2607
+ })
2608
+ );
2609
+ }
2610
+ return;
2611
+ }
2612
+ if (msg.type === "bridge:open") {
2613
+ if (typeof msg.url !== "string" || !msg.url) {
2614
+ throw new Error("bridge:open requires a url");
2615
+ }
2616
+ await this.openUrl(msg.url);
2617
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2618
+ ws.send(
2619
+ JSON.stringify({
2620
+ id: msg.id,
2621
+ result: { ok: true, url: msg.url }
2622
+ })
2623
+ );
2624
+ }
2625
+ return;
2626
+ }
2627
+ if (msg.type === "bridge:claim") {
2628
+ const targetBrowser2 = await this.waitForBrowser(msg.browserId);
2629
+ const outcome = this.tryAcquireLease(ws, targetBrowser2, {
2630
+ force: msg.force === true
2631
+ });
2632
+ if (!outcome.granted) {
2633
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2634
+ ws.send(
2635
+ JSON.stringify({
2636
+ id: msg.id,
2637
+ error: `tab ${targetBrowser2.id} is locked by another cli`,
2638
+ _locked: outcome.lock
2639
+ })
2640
+ );
2641
+ }
2642
+ return;
2643
+ }
2644
+ this.setCliBrowserTarget(ws, targetBrowser2.id);
2645
+ this.recordBrowserAction(
2646
+ targetBrowser2.id,
2647
+ outcome.bootedCount > 0 ? `cli force-claimed tab (booted ${outcome.bootedCount})` : "cli claimed tab"
2648
+ );
2649
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2650
+ ws.send(
2651
+ JSON.stringify({
2652
+ id: msg.id,
2653
+ result: {
2654
+ browserId: targetBrowser2.id,
2655
+ lockedBy: outcome.lease.cliSessionKey,
2656
+ lockExpiresAt: outcome.lease.expiresAt,
2657
+ bootedCount: outcome.bootedCount
2658
+ }
2659
+ })
2660
+ );
2661
+ }
2662
+ return;
2663
+ }
2664
+ const targetBrowser = await this.waitForBrowser(msg.browserId);
2665
+ if (shouldAcquireLease(msg)) {
2666
+ const outcome = this.tryAcquireLease(ws, targetBrowser);
2667
+ if (!outcome.granted) {
2668
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2669
+ ws.send(
2670
+ JSON.stringify({
2671
+ id: msg.id,
2672
+ error: `tab ${targetBrowser.id} is locked by another cli \u2014 use \`sootsim claim ${targetBrowser.id} --force\` or \`sootsim open --new\``,
2673
+ _locked: outcome.lock
2674
+ })
2675
+ );
2676
+ }
2677
+ return;
2678
+ }
2679
+ } else {
2680
+ this.ensureCliSessionKey(ws);
2681
+ }
2682
+ this.setCliBrowserTarget(ws, targetBrowser.id);
2683
+ this.recordBrowserAction(targetBrowser.id, this.describeForwardedCommand(msg));
2684
+ const sentId = this.nextCommandId++;
2685
+ this.cliBySentId.set(sentId, {
2686
+ browserId: targetBrowser.id,
2687
+ ws,
2688
+ originalId: msg.id
2689
+ });
2690
+ const { browserId: _browserId, ...forwarded } = msg;
2691
+ targetBrowser.ws.send(JSON.stringify({ ...forwarded, id: sentId }));
2692
+ } catch (err) {
2693
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
2694
+ ws.send(
2695
+ JSON.stringify({
2696
+ id: msg.id,
2697
+ error: err instanceof Error ? err.message : String(err)
2698
+ })
2699
+ );
2700
+ }
2701
+ }
2702
+ })();
2703
+ });
2704
+ ws.on("close", () => {
2705
+ this.agentHost.unregisterSocket(ws);
2706
+ if (role === "browser" && browser) {
2707
+ this.rememberDisconnectedBrowser(browser);
2708
+ if (this.primaryBrowserId === browser.id) {
2709
+ this.primaryBrowserId = this.getOpenBrowser()?.id ?? null;
2710
+ }
2711
+ for (const [id, pending] of this.pendingCommands) {
2712
+ if (pending.browserId !== browser.id) continue;
2713
+ pending.reject(new Error("browser disconnected"));
2714
+ this.pendingCommands.delete(id);
2715
+ }
2716
+ for (const [sentId, entry] of this.cliBySentId) {
2717
+ if (entry.browserId !== browser.id) continue;
2718
+ if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
2719
+ entry.ws.send(
2720
+ JSON.stringify({
2721
+ id: entry.originalId,
2722
+ error: "browser disconnected before responding"
2723
+ })
2724
+ );
2725
+ }
2726
+ this.cliBySentId.delete(sentId);
2727
+ }
2728
+ this.broadcastBrowserAssignments();
2729
+ this.broadcastBrowserClientStates();
2730
+ } else if (role === "cli") {
2731
+ const detached = this.cliBrowserBySocket.delete(ws);
2732
+ this.cliLastCommandAt.delete(ws);
2733
+ this.cliSessionKeyBySocket.delete(ws);
2734
+ this.cliLabelBySocket.delete(ws);
2735
+ for (const [sentId, entry] of this.cliBySentId) {
2736
+ if (entry.ws === ws) this.cliBySentId.delete(sentId);
2737
+ }
2738
+ if (detached) {
2739
+ this.broadcastBrowserClientStates();
2740
+ }
2741
+ }
2742
+ });
2743
+ });
2744
+ }
2745
+ /** after a successful bind: start the cli idle sweep, write the daemon
2746
+ * lockfile (if this host owns it), seed the agent host, kick off the
2747
+ * heartbeat loop. idempotent across rebinds because close() tears down
2748
+ * every timer and the lockfile. */
2749
+ afterBind() {
2750
+ process.stderr.write(`ws bridge listening on port ${this.port}
2751
+ `);
2752
+ this.cliIdleTimer = setInterval(
2753
+ () => this.sweepIdleCliClients(),
2754
+ 3e4
2755
+ );
2756
+ this.cliIdleTimer.unref();
2757
+ if (this.shouldWriteLockfile) {
2758
+ try {
2759
+ ensureSootsimHome();
2760
+ const claimed = claimDaemonLockfile(this.buildLockfileSnapshot());
2761
+ if (!claimed) {
2762
+ throw new Error(
2763
+ "another sootsim daemon wrote the lockfile during startup \u2014 aborting"
2764
+ );
2765
+ }
2766
+ } catch (err) {
2767
+ process.stderr.write(
2768
+ `ws bridge failed to claim daemon lockfile: ${String(err)}
2769
+ `
2770
+ );
2771
+ throw err;
2772
+ }
2773
+ this.heartbeatTimer = setInterval(() => {
2774
+ try {
2775
+ this.writeLockfileSnapshot();
2776
+ } catch {
2777
+ }
2778
+ }, DAEMON_HEARTBEAT_INTERVAL_MS);
2779
+ this.heartbeatTimer.unref();
2780
+ }
2781
+ void this.agentHost.seedOnBoot();
2782
+ }
2783
+ buildLockfileSnapshot() {
2784
+ return {
2785
+ schema: 1,
2786
+ pid: process.pid,
2787
+ platform: process.platform,
2788
+ bridgePort: this.effectivePort,
2789
+ runtimePort: this.effectivePort,
2790
+ activeRuntime: this.activeRuntimeVersion,
2791
+ activeRuntimeDir: this.activeRuntimeDirPath,
2792
+ startedAt: this.startedAt,
2793
+ heartbeatAt: Date.now()
2794
+ };
2795
+ }
2796
+ writeLockfileSnapshot() {
2797
+ writeDaemonLockfile(this.buildLockfileSnapshot());
2798
+ }
2799
+ refreshActiveRuntime() {
2800
+ this.activeRuntimeVersion = readActiveRuntime();
2801
+ this.activeRuntimeDirPath = activeRuntimeDir();
2802
+ }
2803
+ /** update the active runtime on disk + in memory. the caller guarantees
2804
+ * the version directory exists. pushes a runtime:changed message to all
2805
+ * connected browsers so electron (or any renderer) can reload. */
2806
+ setActiveRuntime(version) {
2807
+ writeActiveRuntime(version);
2808
+ this.refreshActiveRuntime();
2809
+ if (this.shouldWriteLockfile && this.httpServer) {
2810
+ try {
2811
+ this.writeLockfileSnapshot();
2812
+ } catch {
2813
+ }
2814
+ }
2815
+ const payload = JSON.stringify({
2816
+ type: "runtime:changed",
2817
+ version,
2818
+ runtimeDir: this.activeRuntimeDirPath
2819
+ });
2820
+ for (const browser of this.browsers.values()) {
2821
+ if (browser.ws.readyState === import_ws.WebSocket.OPEN) {
2822
+ try {
2823
+ browser.ws.send(payload);
2824
+ } catch {
2825
+ }
2826
+ }
2827
+ }
2828
+ return { version, runtimeDir: this.activeRuntimeDirPath };
2829
+ }
2830
+ getActiveRuntime() {
2831
+ return {
2832
+ version: this.activeRuntimeVersion,
2833
+ runtimeDir: this.activeRuntimeDirPath
2834
+ };
2835
+ }
2836
+ /** last-ditch lockfile cleanup. safe to call from a synchronous
2837
+ * `process.on('exit', ...)` handler since it only does a fs.unlinkSync. */
2838
+ removeLockfile() {
2839
+ if (!this.shouldWriteLockfile) return;
2840
+ try {
2841
+ removeDaemonLockfile();
2842
+ } catch {
2843
+ }
2844
+ }
2845
+ /** minimal HTTP request handler attached to the same node http server
2846
+ * that hosts the WS upgrade. handles:
2847
+ * GET /healthz json status for supervisors / curl
2848
+ * GET / + everything serves from the active runtime dist, SPA fallback
2849
+ * non-upgrade routes that don't match serve index.html (SPA behavior) so
2850
+ * electron's webContents can navigate freely inside the runtime. */
2851
+ handleHttpRequest(req, res) {
2852
+ const method = (req.method || "GET").toUpperCase();
2853
+ if (method !== "GET" && method !== "HEAD") {
2854
+ res.writeHead(405, { Allow: "GET, HEAD" });
2855
+ res.end("method not allowed");
2856
+ return;
2857
+ }
2858
+ const url = new URL(req.url || "/", "http://localhost");
2859
+ if (url.pathname === "/__bundle-proxy") {
2860
+ const target = url.searchParams.get("url");
2861
+ if (!target) {
2862
+ res.writeHead(400, { "Content-Type": "text/plain" });
2863
+ res.end("bundle-proxy: missing url query param");
2864
+ return;
2865
+ }
2866
+ let parsedTarget;
2867
+ try {
2868
+ parsedTarget = new URL(target);
2869
+ } catch {
2870
+ res.writeHead(400, { "Content-Type": "text/plain" });
2871
+ res.end("bundle-proxy: invalid url");
2872
+ return;
2873
+ }
2874
+ const host = parsedTarget.hostname;
2875
+ const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
2876
+ if (!isLoopback) {
2877
+ res.writeHead(403, { "Content-Type": "text/plain" });
2878
+ res.end("bundle-proxy: only loopback targets allowed");
2879
+ return;
2880
+ }
2881
+ void (async () => {
2882
+ try {
2883
+ const upstream = await fetch(parsedTarget.toString(), {
2884
+ redirect: "follow"
2885
+ });
2886
+ const headers = {};
2887
+ const ct = upstream.headers.get("content-type");
2888
+ if (ct) headers["Content-Type"] = ct;
2889
+ headers["Cache-Control"] = "no-store";
2890
+ res.writeHead(upstream.status, headers);
2891
+ if (!upstream.body) {
2892
+ res.end();
2893
+ return;
2894
+ }
2895
+ const reader = upstream.body.getReader();
2896
+ while (true) {
2897
+ const { done, value } = await reader.read();
2898
+ if (done) break;
2899
+ res.write(Buffer.from(value));
2900
+ }
2901
+ res.end();
2902
+ } catch (err) {
2903
+ res.writeHead(502, { "Content-Type": "text/plain" });
2904
+ res.end(
2905
+ `bundle-proxy: upstream fetch failed: ${err instanceof Error ? err.message : String(err)}`
2906
+ );
2907
+ }
2908
+ })();
2909
+ return;
2910
+ }
2911
+ if (url.pathname === "/healthz") {
2912
+ res.writeHead(200, {
2913
+ "Content-Type": "application/json",
2914
+ "Cache-Control": "no-store"
2915
+ });
2916
+ res.end(
2917
+ JSON.stringify({
2918
+ ok: true,
2919
+ pid: process.pid,
2920
+ platform: process.platform,
2921
+ bridgePort: this.effectivePort,
2922
+ runtimePort: this.effectivePort,
2923
+ activeRuntime: this.activeRuntimeVersion,
2924
+ startedAt: this.startedAt,
2925
+ uptimeMs: this.startedAt > 0 ? Date.now() - this.startedAt : 0
2926
+ })
2927
+ );
2928
+ return;
2929
+ }
2930
+ this.refreshActiveRuntime();
2931
+ const baseDir = this.activeRuntimeDirPath;
2932
+ if (!baseDir) {
2933
+ res.writeHead(503, { "Content-Type": "text/plain; charset=utf-8" });
2934
+ res.end(
2935
+ "sootsim: no active runtime installed. run `sootsim runtime install` to fetch one."
2936
+ );
2937
+ return;
2938
+ }
2939
+ let rel = url.pathname;
2940
+ if (rel === "/runtime" || rel === "/runtime/") rel = "/";
2941
+ else if (rel.startsWith("/runtime/")) rel = rel.slice("/runtime".length);
2942
+ if (rel === "" || rel === "/") rel = "/index.html";
2943
+ if (rel.includes("\0")) {
2944
+ res.writeHead(400);
2945
+ res.end("bad request");
2946
+ return;
2947
+ }
2948
+ if (process.platform !== "win32" && rel.includes("\\")) {
2949
+ res.writeHead(400);
2950
+ res.end("bad request");
2951
+ return;
2952
+ }
2953
+ for (const segment of rel.split("/")) {
2954
+ if (segment === "..") {
2955
+ res.writeHead(403);
2956
+ res.end("forbidden");
2957
+ return;
2958
+ }
2959
+ }
2960
+ const resolved = import_path2.default.resolve(baseDir, "." + rel);
2961
+ const baseWithSep = baseDir.endsWith(import_path2.default.sep) ? baseDir : baseDir + import_path2.default.sep;
2962
+ if (!resolved.startsWith(baseWithSep) && resolved !== baseDir) {
2963
+ res.writeHead(403);
2964
+ res.end("forbidden");
2965
+ return;
2966
+ }
2967
+ import_fs2.default.realpath(resolved, (realErr, realResolved) => {
2968
+ const servePath = realErr ? resolved : realResolved;
2969
+ const servePathWithSep = servePath.endsWith(import_path2.default.sep) ? servePath : servePath + import_path2.default.sep;
2970
+ if (!realErr) {
2971
+ const realBaseWithSep = (() => {
2972
+ try {
2973
+ const rb = import_fs2.default.realpathSync(baseDir);
2974
+ return rb.endsWith(import_path2.default.sep) ? rb : rb + import_path2.default.sep;
2975
+ } catch {
2976
+ return baseWithSep;
2977
+ }
2978
+ })();
2979
+ if (!servePathWithSep.startsWith(realBaseWithSep) && servePath + import_path2.default.sep !== realBaseWithSep) {
2980
+ res.writeHead(403);
2981
+ res.end("forbidden");
2982
+ return;
2983
+ }
2984
+ }
2985
+ import_fs2.default.stat(servePath, (err, stats) => {
2986
+ if (err || !stats?.isFile()) {
2987
+ const ext2 = import_path2.default.extname(rel).toLowerCase();
2988
+ if (ext2 && ext2 !== ".html") {
2989
+ res.writeHead(404);
2990
+ res.end("not found");
2991
+ return;
2992
+ }
2993
+ const indexPath = import_path2.default.join(baseDir, "index.html");
2994
+ import_fs2.default.readFile(indexPath, (err2, data) => {
2995
+ if (err2) {
2996
+ res.writeHead(404);
2997
+ res.end("not found");
2998
+ return;
2999
+ }
3000
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3001
+ if (method === "HEAD") {
3002
+ res.end();
3003
+ return;
3004
+ }
3005
+ res.end(data);
3006
+ });
3007
+ return;
3008
+ }
3009
+ const ext = import_path2.default.extname(servePath).toLowerCase();
3010
+ const contentType = HTTP_MIME_TYPES[ext] || "application/octet-stream";
3011
+ res.writeHead(200, { "Content-Type": contentType });
3012
+ if (method === "HEAD") {
3013
+ res.end();
3014
+ return;
3015
+ }
3016
+ const stream = import_fs2.default.createReadStream(servePath);
3017
+ stream.pipe(res);
3018
+ stream.on("error", () => {
3019
+ try {
3020
+ res.end();
3021
+ } catch {
3022
+ }
3023
+ });
3024
+ });
3025
+ });
3026
+ }
3027
+ sweepIdleCliClients() {
3028
+ const now = Date.now();
3029
+ let swept = false;
3030
+ for (const [ws, browserId] of this.cliBrowserBySocket) {
3031
+ const lastCommand = this.cliLastCommandAt.get(ws) ?? 0;
3032
+ if (now - lastCommand < _SootSimBridgeHost.CLI_IDLE_TIMEOUT_MS) continue;
3033
+ this.cliBrowserBySocket.delete(ws);
3034
+ this.cliLastCommandAt.delete(ws);
3035
+ for (const [sentId, entry] of this.cliBySentId) {
3036
+ if (entry.ws === ws) this.cliBySentId.delete(sentId);
3037
+ }
3038
+ try {
3039
+ ws.close(1e3, "idle timeout");
3040
+ } catch {
3041
+ }
3042
+ swept = true;
3043
+ }
3044
+ if (swept) {
3045
+ this.broadcastBrowserClientStates();
3046
+ }
3047
+ this.sweepRestorableBrowsers(now);
3048
+ }
3049
+ listBrowsers() {
3050
+ return Array.from(this.browsers.values()).sort((a, b) => {
3051
+ if (a.id === this.primaryBrowserId) return -1;
3052
+ if (b.id === this.primaryBrowserId) return 1;
3053
+ return a.connectedAt - b.connectedAt;
3054
+ }).map((browser) => this.describeBrowser(browser));
3055
+ }
3056
+ async sendCommand(cmd) {
3057
+ const browser = await this.waitForBrowser(cmd.browserId);
3058
+ const id = this.nextCommandId++;
3059
+ return new Promise((resolve2, reject) => {
3060
+ const timeout = setTimeout(() => {
3061
+ this.pendingCommands.delete(id);
3062
+ this.broadcastBrowserClientStates();
3063
+ reject(new Error("command timed out after 30s"));
3064
+ }, 3e4);
3065
+ this.pendingCommands.set(id, {
3066
+ browserId: browser.id,
3067
+ resolve: (value) => {
3068
+ clearTimeout(timeout);
3069
+ this.pendingCommands.delete(id);
3070
+ this.broadcastBrowserClientStates();
3071
+ resolve2(value);
3072
+ },
3073
+ reject: (error) => {
3074
+ clearTimeout(timeout);
3075
+ this.pendingCommands.delete(id);
3076
+ this.broadcastBrowserClientStates();
3077
+ reject(error);
3078
+ }
3079
+ });
3080
+ this.broadcastBrowserClientStates();
3081
+ const { browserId: _browserId, ...forwarded } = cmd;
3082
+ browser.ws.send(JSON.stringify({ ...forwarded, id }));
3083
+ });
3084
+ }
3085
+ async evaluate(code, browserId) {
3086
+ return this.sendCommand({ type: "evaluate", code, browserId });
3087
+ }
3088
+ async focusBrowser(browserId) {
3089
+ return this.sendCommand({ type: "focus", browserId });
3090
+ }
3091
+ async closeBrowser(browserId) {
3092
+ return this.sendCommand({ type: "close", browserId });
3093
+ }
3094
+ async openPathInEditor(filePath, line, column) {
3095
+ const loc = line != null ? `:${line}${column != null ? `:${column}` : ""}` : "";
3096
+ const target = `${filePath}${loc}`;
3097
+ const trySpawn = (cmd, args) => new Promise((resolve2) => {
3098
+ try {
3099
+ const child = (0, import_child_process2.spawn)(cmd, args, { detached: true, stdio: "ignore" });
3100
+ let settled = false;
3101
+ child.on("error", () => {
3102
+ if (settled) return;
3103
+ settled = true;
3104
+ resolve2(false);
3105
+ });
3106
+ child.on("spawn", () => {
3107
+ if (settled) return;
3108
+ settled = true;
3109
+ child.unref();
3110
+ resolve2(true);
3111
+ });
3112
+ } catch {
3113
+ resolve2(false);
3114
+ }
3115
+ });
3116
+ const envEditor = process.env.REACT_EDITOR || process.env.EDITOR;
3117
+ if (envEditor) {
3118
+ const parts = envEditor.split(" ").filter(Boolean);
3119
+ if (parts.length && await trySpawn(parts[0], [...parts.slice(1), "-g", target]))
3120
+ return;
3121
+ }
3122
+ if (await trySpawn("cursor", ["-g", target])) return;
3123
+ if (await trySpawn("code", ["-g", target])) return;
3124
+ await this.openUrl(filePath);
3125
+ }
3126
+ async openUrl(url) {
3127
+ if (this.openUrlHandler) {
3128
+ await this.openUrlHandler(url);
3129
+ return;
3130
+ }
3131
+ if (process.platform === "darwin") {
3132
+ const child2 = (0, import_child_process2.spawn)("open", ["-g", url], { detached: true, stdio: "ignore" });
3133
+ child2.unref();
3134
+ return;
3135
+ }
3136
+ if (process.platform === "win32") {
3137
+ const child2 = (0, import_child_process2.spawn)("cmd", ["/c", "start", "", url], {
3138
+ detached: true,
3139
+ stdio: "ignore"
3140
+ });
3141
+ child2.unref();
3142
+ return;
3143
+ }
3144
+ const child = (0, import_child_process2.spawn)("xdg-open", [url], { detached: true, stdio: "ignore" });
3145
+ child.unref();
3146
+ }
3147
+ async close() {
3148
+ if (this.cliIdleTimer) {
3149
+ clearInterval(this.cliIdleTimer);
3150
+ this.cliIdleTimer = null;
3151
+ }
3152
+ if (this.heartbeatTimer) {
3153
+ clearInterval(this.heartbeatTimer);
3154
+ this.heartbeatTimer = null;
3155
+ }
3156
+ if (this.shouldWriteLockfile) {
3157
+ try {
3158
+ removeDaemonLockfile();
3159
+ } catch {
3160
+ }
3161
+ }
3162
+ this.effectivePort = 0;
3163
+ this.startedAt = 0;
3164
+ this.agentHost.close();
3165
+ for (const [id, pending] of this.pendingCommands) {
3166
+ pending.reject(new Error("server closing"));
3167
+ this.pendingCommands.delete(id);
3168
+ }
3169
+ for (const browser of this.browsers.values()) {
3170
+ browser.ws.close();
3171
+ }
3172
+ this.browsers.clear();
3173
+ this.primaryBrowserId = null;
3174
+ const wss = this.wss;
3175
+ const httpServer = this.httpServer;
3176
+ this.wss = null;
3177
+ this.httpServer = null;
3178
+ if (wss) {
3179
+ try {
3180
+ wss.close();
3181
+ } catch {
3182
+ }
3183
+ }
3184
+ if (httpServer) {
3185
+ try {
3186
+ httpServer.close();
3187
+ } catch {
3188
+ }
3189
+ }
3190
+ }
3191
+ describeBrowser(browser) {
3192
+ let readyState;
3193
+ try {
3194
+ readyState = browser.ws.readyState;
3195
+ } catch {
3196
+ readyState = import_ws.WebSocket.CLOSED;
3197
+ }
3198
+ const lease = this.getActiveLease(browser);
3199
+ return {
3200
+ id: browser.id,
3201
+ origin: browser.origin,
3202
+ url: browser.url,
3203
+ title: browser.title,
3204
+ userAgent: browser.userAgent,
3205
+ connectedAt: browser.connectedAt,
3206
+ lastSeenAt: browser.lastSeenAt,
3207
+ lastActiveAt: browser.lastActiveAt || void 0,
3208
+ isPrimary: browser.id === this.primaryBrowserId,
3209
+ readyState: readyState === import_ws.WebSocket.OPEN ? "open" : readyState === import_ws.WebSocket.CLOSING ? "closing" : "closed",
3210
+ attachedCliCount: this.getAttachedCliCount(browser.id),
3211
+ lockedBy: lease ? lease.cliLabel || lease.cliSessionKey : void 0,
3212
+ lockedByKind: lease ? lease.kind : void 0,
3213
+ lockExpiresAt: lease ? lease.expiresAt : void 0,
3214
+ userFocused: browser.userFocused || void 0
3215
+ };
3216
+ }
3217
+ getActiveLease(browser) {
3218
+ const lease = browser.cliLease;
3219
+ if (!lease) return null;
3220
+ if (Date.now() >= lease.expiresAt) {
3221
+ browser.cliLease = void 0;
3222
+ return null;
3223
+ }
3224
+ return lease;
3225
+ }
3226
+ tryAcquireLease(ws, browser, opts = {}) {
3227
+ const cliSessionKey = this.cliSessionKeyBySocket.get(ws) ?? (() => {
3228
+ const fallback = `ws-${this.nextCliFallbackId++}`;
3229
+ this.cliSessionKeyBySocket.set(ws, fallback);
3230
+ return fallback;
3231
+ })();
3232
+ const cliLabel = this.cliLabelBySocket.get(ws);
3233
+ const now = Date.now();
3234
+ const existing = this.getActiveLease(browser);
3235
+ const ownerMatches = existing && existing.cliSessionKey === cliSessionKey;
3236
+ let bootedCount = 0;
3237
+ if (existing && !ownerMatches && !opts.force) {
3238
+ return {
3239
+ granted: false,
3240
+ lease: existing,
3241
+ lock: {
3242
+ by: existing.cliLabel || existing.cliSessionKey,
3243
+ expiresInMs: Math.max(0, existing.expiresAt - now)
3244
+ },
3245
+ bootedCount: 0
3246
+ };
3247
+ }
3248
+ if (existing && !ownerMatches && opts.force) {
3249
+ for (const [cliWs, attachedBrowserId] of this.cliBrowserBySocket) {
3250
+ if (attachedBrowserId !== browser.id) continue;
3251
+ const otherKey = this.cliSessionKeyBySocket.get(cliWs);
3252
+ if (otherKey && otherKey !== cliSessionKey) {
3253
+ this.cliBrowserBySocket.delete(cliWs);
3254
+ try {
3255
+ cliWs.close(1e3, "lease claimed by another cli");
3256
+ } catch {
3257
+ }
3258
+ bootedCount++;
3259
+ }
3260
+ }
3261
+ }
3262
+ const lease = {
3263
+ kind: "cli",
3264
+ cliSessionKey,
3265
+ cliLabel,
3266
+ expiresAt: now + _SootSimBridgeHost.CLI_LEASE_TTL_MS
3267
+ };
3268
+ browser.cliLease = lease;
3269
+ return { granted: true, lease, bootedCount };
3270
+ }
3271
+ // user focus is advisory: we track it on the browser record so list/UI can
3272
+ // show "focused" alongside any cli lease, but focus alone never creates a
3273
+ // blocking lease. the old 15s user-focus lease meant clicking on the tab
3274
+ // locked out agent inspect calls for 15s — the opposite of what you want
3275
+ // when debugging something the user is actively looking at. use
3276
+ // updateUserActivity() to lock on real interaction instead.
3277
+ updateUserFocusLease(browser, focused) {
3278
+ const next = focused;
3279
+ if (browser.userFocused === next) return;
3280
+ browser.userFocused = next;
3281
+ this.broadcastBrowserClientStates();
3282
+ }
3283
+ // called when the browser reports a real user interaction (pointerdown,
3284
+ // keydown, wheel, touch). creates or refreshes a short `user-active` lease
3285
+ // that keeps agent writes from trampling a user who is driving the tab.
3286
+ // reads still pass through — shouldAcquireLease only blocks on writes.
3287
+ updateUserActivity(browser) {
3288
+ const existing = this.getActiveLease(browser);
3289
+ if (existing && existing.kind === "cli") {
3290
+ return;
3291
+ }
3292
+ browser.cliLease = {
3293
+ kind: "user-active",
3294
+ cliSessionKey: "__user-active__",
3295
+ cliLabel: "active user",
3296
+ expiresAt: Date.now() + _SootSimBridgeHost.USER_ACTIVE_LEASE_TTL_MS
3297
+ };
3298
+ this.broadcastBrowserClientStates();
3299
+ }
3300
+ ensureCliSessionKey(ws) {
3301
+ const existing = this.cliSessionKeyBySocket.get(ws);
3302
+ if (existing) return existing;
3303
+ const fallback = `ws-${this.nextCliFallbackId++}`;
3304
+ this.cliSessionKeyBySocket.set(ws, fallback);
3305
+ return fallback;
3306
+ }
3307
+ getOpenBrowser(browserId) {
3308
+ if (browserId) {
3309
+ const browser = this.browsers.get(browserId);
3310
+ if (browser?.ws.readyState === import_ws.WebSocket.OPEN) return browser;
3311
+ return null;
3312
+ }
3313
+ const primary = this.primaryBrowserId != null ? this.browsers.get(this.primaryBrowserId) : null;
3314
+ if (primary?.ws.readyState === import_ws.WebSocket.OPEN) return primary;
3315
+ for (const browser of this.browsers.values()) {
3316
+ if (browser.ws.readyState === import_ws.WebSocket.OPEN) return browser;
3317
+ }
3318
+ return null;
3319
+ }
3320
+ async waitForBrowser(browserId, options = {}) {
3321
+ const attempts = options.attempts ?? 10;
3322
+ const intervalMs = options.intervalMs ?? 200;
3323
+ for (let attempt = 0; attempt < attempts; attempt++) {
3324
+ const browser = this.getOpenBrowser(browserId);
3325
+ if (browser) return browser;
3326
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
3327
+ }
3328
+ throw new Error(
3329
+ browserId ? `no browser connected with id ${browserId}` : "no browser connected"
3330
+ );
3331
+ }
3332
+ shouldPromoteBrowser(browser) {
3333
+ const current = this.primaryBrowserId ? this.browsers.get(this.primaryBrowserId) : null;
3334
+ const isPrimaryCandidate = browser.origin?.includes(":5173");
3335
+ const currentIsPrimary = current?.origin?.includes(":5173");
3336
+ return !current || current.ws.readyState !== import_ws.WebSocket.OPEN || !!isPrimaryCandidate || !currentIsPrimary;
3337
+ }
3338
+ broadcastBrowserAssignments() {
3339
+ for (const browser of this.browsers.values()) {
3340
+ if (browser.ws.readyState !== import_ws.WebSocket.OPEN) continue;
3341
+ browser.ws.send(
3342
+ JSON.stringify({
3343
+ type: "bridge:welcome",
3344
+ browserId: browser.id,
3345
+ isPrimary: browser.id === this.primaryBrowserId
3346
+ })
3347
+ );
3348
+ }
3349
+ }
3350
+ broadcastBrowserClientStates() {
3351
+ for (const browser of this.browsers.values()) {
3352
+ if (browser.ws.readyState !== import_ws.WebSocket.OPEN) continue;
3353
+ const lease = this.getActiveLease(browser);
3354
+ const message = {
3355
+ type: "bridge:client-state",
3356
+ attachedCliCount: this.getAttachedCliCount(browser.id),
3357
+ activeAgentCommandCount: this.getActiveAgentCommandCount(browser.id),
3358
+ recentActions: browser.recentActions,
3359
+ lockedBy: lease ? lease.cliLabel || lease.cliSessionKey : void 0,
3360
+ lockedByKind: lease ? lease.kind : void 0,
3361
+ lockExpiresAt: lease ? lease.expiresAt : void 0,
3362
+ userFocused: browser.userFocused || void 0
3363
+ };
3364
+ browser.ws.send(JSON.stringify(message));
3365
+ }
3366
+ }
3367
+ setCliBrowserTarget(ws, browserId) {
3368
+ const prevBrowserId = this.cliBrowserBySocket.get(ws);
3369
+ if (prevBrowserId === browserId) return;
3370
+ this.cliBrowserBySocket.set(ws, browserId);
3371
+ this.recordBrowserAction(
3372
+ browserId,
3373
+ prevBrowserId ? "cli switched tabs" : "cli connected",
3374
+ false
3375
+ );
3376
+ this.broadcastBrowserClientStates();
3377
+ }
3378
+ recordBrowserAction(browserId, label, broadcast = true) {
3379
+ const normalized = label?.trim();
3380
+ if (!normalized) return;
3381
+ const browser = this.browsers.get(browserId);
3382
+ if (!browser) return;
3383
+ const now = Date.now();
3384
+ browser.lastActiveAt = now;
3385
+ browser.recentActions = [
3386
+ { label: normalized, at: now },
3387
+ ...browser.recentActions.filter((entry) => entry.label !== normalized)
3388
+ ].slice(0, 4);
3389
+ if (broadcast) this.broadcastBrowserClientStates();
3390
+ }
3391
+ describeForwardedCommand(msg) {
3392
+ switch (msg?.type) {
3393
+ case "evaluate":
3394
+ return "evaluated page state";
3395
+ case "screenshot":
3396
+ return "captured screenshot";
3397
+ case "tap":
3398
+ return "sent tap event";
3399
+ case "keyboard":
3400
+ return msg?.action === "type" ? "typed text" : "used keyboard";
3401
+ case "tree":
3402
+ return "dumped tree";
3403
+ case "focus":
3404
+ return "focused tab";
3405
+ case "close":
3406
+ return "requested close";
3407
+ default:
3408
+ return typeof msg?.type === "string" ? msg.type : null;
3409
+ }
3410
+ }
3411
+ // count distinct cli session keys attached to a browser, not raw sockets.
3412
+ // a single agent firing sequential cli commands opens a new ws per call —
3413
+ // counting sockets would report phantom peers until idle cleanup catches up.
3414
+ getAttachedCliCount(browserId) {
3415
+ const keys = /* @__PURE__ */ new Set();
3416
+ for (const [ws, attachedBrowserId] of this.cliBrowserBySocket) {
3417
+ if (attachedBrowserId !== browserId) continue;
3418
+ if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
3419
+ const key = this.cliSessionKeyBySocket.get(ws);
3420
+ keys.add(key ?? `ws-unknown-${keys.size}`);
3421
+ }
3422
+ return keys.size;
3423
+ }
3424
+ // count distinct session keys attached to this browser other than `selfWs`.
3425
+ // used to warn a cli that other agents/sessions are also targeting the tab.
3426
+ getOtherCliSessionCount(selfWs, browserId) {
3427
+ const selfKey = this.cliSessionKeyBySocket.get(selfWs);
3428
+ const keys = /* @__PURE__ */ new Set();
3429
+ for (const [ws, attachedBrowserId] of this.cliBrowserBySocket) {
3430
+ if (attachedBrowserId !== browserId) continue;
3431
+ if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
3432
+ const key = this.cliSessionKeyBySocket.get(ws);
3433
+ if (key && key === selfKey) continue;
3434
+ keys.add(key ?? `ws-unknown-${keys.size}`);
3435
+ }
3436
+ return keys.size;
3437
+ }
3438
+ getActiveAgentCommandCount(browserId) {
3439
+ let count = 0;
3440
+ for (const pending of this.pendingCommands.values()) {
3441
+ if (pending.browserId === browserId) count++;
3442
+ }
3443
+ return count;
3444
+ }
3445
+ tryRestoreBrowserId(browser, requestedId) {
3446
+ const nextId = requestedId?.trim();
3447
+ if (!nextId || nextId === browser.id) return false;
3448
+ const existing = this.browsers.get(nextId);
3449
+ if (existing && existing !== browser && existing.ws.readyState === import_ws.WebSocket.OPEN) {
3450
+ return false;
3451
+ }
3452
+ const restorable = this.getRestorableBrowserState(nextId);
3453
+ const prevId = browser.id;
3454
+ this.browsers.delete(prevId);
3455
+ browser.id = nextId;
3456
+ if (restorable) {
3457
+ browser.recentActions = restorable.recentActions.map((entry) => ({ ...entry }));
3458
+ browser.lastActiveAt = restorable.lastActiveAt;
3459
+ browser.cliLease = restorable.cliLease ? { ...restorable.cliLease } : void 0;
3460
+ this.restorableBrowsers.delete(nextId);
3461
+ }
3462
+ this.browsers.set(browser.id, browser);
3463
+ if (this.primaryBrowserId === prevId) {
3464
+ this.primaryBrowserId = browser.id;
3465
+ }
3466
+ for (const [ws, attachedBrowserId] of this.cliBrowserBySocket) {
3467
+ if (attachedBrowserId === prevId) {
3468
+ this.cliBrowserBySocket.set(ws, browser.id);
3469
+ }
3470
+ }
3471
+ return true;
3472
+ }
3473
+ rememberDisconnectedBrowser(browser) {
3474
+ const lease = this.getActiveLease(browser);
3475
+ this.restorableBrowsers.set(browser.id, {
3476
+ recentActions: browser.recentActions.map((entry) => ({ ...entry })),
3477
+ lastActiveAt: browser.lastActiveAt,
3478
+ cliLease: lease && lease.kind === "cli" ? { ...lease } : void 0,
3479
+ expiresAt: Date.now() + _SootSimBridgeHost.BROWSER_RECONNECT_TTL_MS
3480
+ });
3481
+ this.browsers.delete(browser.id);
3482
+ }
3483
+ getRestorableBrowserState(browserId) {
3484
+ const snapshot = this.restorableBrowsers.get(browserId);
3485
+ if (!snapshot) return null;
3486
+ if (snapshot.expiresAt <= Date.now()) {
3487
+ this.restorableBrowsers.delete(browserId);
3488
+ return null;
3489
+ }
3490
+ if (snapshot.cliLease && snapshot.cliLease.expiresAt <= Date.now()) {
3491
+ snapshot.cliLease = void 0;
3492
+ }
3493
+ return snapshot;
3494
+ }
3495
+ sweepRestorableBrowsers(now = Date.now()) {
3496
+ for (const [browserId, snapshot] of this.restorableBrowsers) {
3497
+ if (snapshot.expiresAt > now) continue;
3498
+ this.restorableBrowsers.delete(browserId);
3499
+ for (const [cliWs, attachedBrowserId] of this.cliBrowserBySocket) {
3500
+ if (attachedBrowserId === browserId) {
3501
+ this.cliBrowserBySocket.delete(cliWs);
3502
+ }
3503
+ }
3504
+ }
3505
+ }
3506
+ resetServerState() {
3507
+ if (this.cliIdleTimer) {
3508
+ clearInterval(this.cliIdleTimer);
3509
+ this.cliIdleTimer = null;
3510
+ }
3511
+ const wss = this.wss;
3512
+ const httpServer = this.httpServer;
3513
+ this.wss = null;
3514
+ this.httpServer = null;
3515
+ if (wss) {
3516
+ try {
3517
+ wss.close();
3518
+ } catch {
3519
+ }
3520
+ }
3521
+ if (httpServer) {
3522
+ try {
3523
+ httpServer.close();
3524
+ } catch {
3525
+ }
3526
+ }
3527
+ }
3528
+ };
3529
+ }
3530
+ });
3531
+
3532
+ // src/vite-plugin.ts
3533
+ var vite_plugin_exports = {};
3534
+ __export(vite_plugin_exports, {
3535
+ fixJsxRuntimeExports: () => fixJsxRuntimeExports,
3536
+ sootsim: () => sootsim,
3537
+ workerReactBridgePlugin: () => workerReactBridgePlugin,
3538
+ wsBridgePlugin: () => wsBridgePlugin
3539
+ });
3540
+ module.exports = __toCommonJS(vite_plugin_exports);
3541
+ var import_fs3 = __toESM(require("fs"), 1);
3542
+ var import_path3 = __toESM(require("path"), 1);
3543
+ var import_url = require("url");
3544
+ var import_vite = require("vite");
3545
+ init_bridge_constants();
3546
+ var sootsimRoot = import_path3.default.resolve(import_path3.default.dirname((0, import_url.fileURLToPath)(__sootsim_import_meta_url)), "..");
3547
+ var workspaceRoot = import_path3.default.resolve(sootsimRoot, "..", "..");
3548
+ var workspaceNodeModules = import_path3.default.resolve(workspaceRoot, "node_modules");
3549
+ var workspaceTamaguiDir = import_path3.default.resolve(workspaceNodeModules, "@tamagui");
3550
+ var engineRoot = import_path3.default.resolve(workspaceRoot, "packages/sootsim-engine");
3551
+ var rnShimPath = import_path3.default.resolve(engineRoot, "src/react-native/index.ts");
3552
+ var reactBridgePath = import_path3.default.resolve(engineRoot, "src/react-bridge.ts");
3553
+ var sootsimBrowserSourceAliases = [
3554
+ {
3555
+ find: "sootsim/backend-origin",
3556
+ replacement: import_path3.default.resolve(sootsimRoot, "src/backend-origin.ts")
3557
+ },
3558
+ {
3559
+ find: "sootsim/bridge-constants",
3560
+ replacement: import_path3.default.resolve(sootsimRoot, "src/bridge-constants.ts")
3561
+ },
3562
+ {
3563
+ find: "sootsim/dev-bundle-resolution",
3564
+ replacement: import_path3.default.resolve(sootsimRoot, "src/dev-bundle-resolution.ts")
3565
+ }
3566
+ ];
3567
+ var compatRoot = import_path3.default.resolve(sootsimRoot, "..", "compat");
3568
+ var compatStubsDir = import_path3.default.resolve(compatRoot, "src/stubs");
3569
+ var nativeAutoStubPath = import_path3.default.resolve(compatStubsDir, "native-auto-stub.ts");
3570
+ var rnDeepStubDefault = import_path3.default.resolve(compatStubsDir, "react-native-internals.ts");
3571
+ function getInternalTamaguiPackages() {
3572
+ try {
3573
+ const scoped = import_fs3.default.readdirSync(workspaceTamaguiDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => `@tamagui/${entry.name}`);
3574
+ return ["tamagui", ...scoped].sort();
3575
+ } catch {
3576
+ return ["tamagui"];
3577
+ }
3578
+ }
3579
+ var internalTamaguiPackages = getInternalTamaguiPackages();
3580
+ var rnLibraryStubs = {
3581
+ "react-native/Libraries/Pressability/usePressability": import_path3.default.resolve(
3582
+ compatStubsDir,
3583
+ "rn-libraries/usePressability.ts"
3584
+ ),
3585
+ "react-native/Libraries/Pressability/Pressability": import_path3.default.resolve(
3586
+ compatStubsDir,
3587
+ "rn-libraries/Pressability.ts"
3588
+ ),
3589
+ "react-native/Libraries/Renderer/shims/ReactFabric": import_path3.default.resolve(
3590
+ compatStubsDir,
3591
+ "rn-libraries/ReactFabric.ts"
3592
+ ),
3593
+ "react-native/Libraries/Renderer/shims/ReactNative": import_path3.default.resolve(
3594
+ compatStubsDir,
3595
+ "rn-libraries/ReactNative.ts"
3596
+ )
3597
+ };
3598
+ var builtinStubs = {
3599
+ "react-native-safe-area-context": "react-native-safe-area-context.ts",
3600
+ "react-native-gesture-handler": "react-native-gesture-handler.ts",
3601
+ "react-native-gesture-handler/ReanimatedSwipeable": "reanimated-swipeable.ts",
3602
+ // NOTE: 'soot-swipeable' is a real workspace package (packages/soot-swipeable),
3603
+ // resolved naturally by vite — no builtin stub entry needed.
3604
+ "react-native-reanimated": "react-native-reanimated.ts",
3605
+ "react-native-screens": "react-native-screens.ts",
3606
+ "@react-native-masked-view/masked-view": "masked-view.ts",
3607
+ "react-native-svg": "react-native-svg.ts",
3608
+ "react-native-keyboard-controller": "react-native-keyboard-controller.ts",
3609
+ "react-native-teleport": "react-native-teleport.tsx",
3610
+ "react-native-ios-context-menu": "react-native-ios-context-menu.ts",
3611
+ "react-native-ios-utilities": "react-native-ios-utilities.ts",
3612
+ "@react-native-menu/menu": "react-native-menu.ts",
3613
+ "expo-splash-screen": "expo-splash-screen.ts",
3614
+ "expo-font": "expo-font.ts",
3615
+ "react-native-launch-arguments": "react-native-launch-arguments.ts",
3616
+ expo: "expo.ts",
3617
+ "@react-native-async-storage/async-storage": "async-storage.ts",
3618
+ "expo-constants": "expo-constants.ts",
3619
+ "expo-image": "expo-image.ts",
3620
+ "expo-linear-gradient": "expo-linear-gradient.ts",
3621
+ "expo-blur": "expo-blur.ts",
3622
+ "expo-liquid-glass-view": "expo-liquid-glass-view.ts",
3623
+ "expo-glass-effect": "expo-glass-effect.ts",
3624
+ "react-native-bottom-tabs": "react-native-bottom-tabs.ts",
3625
+ "@callstack/react-native-bottom-tabs": "react-native-bottom-tabs.ts",
3626
+ "@expo/ui/swift-ui": "expo-ui.ts",
3627
+ "@expo/ui": "expo-ui.ts",
3628
+ "react-native-worklets": "react-native-worklets.ts",
3629
+ "react-native-webview": "react-native-webview.ts",
3630
+ "expo-haptics": "expo-haptics.ts",
3631
+ "expo-crypto": "expo-crypto.ts",
3632
+ "expo-clipboard": "expo-clipboard.ts",
3633
+ "expo-file-system": "expo-file-system.ts",
3634
+ "expo-linking": "expo-linking.ts",
3635
+ "expo-web-browser": "expo-web-browser.ts",
3636
+ "expo-secure-store": "expo-secure-store.ts",
3637
+ "expo-status-bar": "expo-status-bar.ts",
3638
+ "expo-device": "expo-device.ts",
3639
+ "expo-image-picker": "expo-image-picker.ts",
3640
+ "expo-document-picker": "expo-document-picker.ts",
3641
+ "expo-application": "expo-application.ts",
3642
+ "expo-notifications": "expo-notifications.ts",
3643
+ "expo-location": "expo-location.ts",
3644
+ "@shopify/flash-list": "flash-list.ts",
3645
+ "@react-native-community/netinfo": "netinfo.ts",
3646
+ // real package installed, let it resolve naturally
3647
+ "@react-native-community/blur": "react-native-community-blur.ts",
3648
+ "@react-native-community/checkbox": "react-native-community-checkbox.ts",
3649
+ "@react-native-picker/picker": "react-native-picker.ts",
3650
+ "@react-native-community/datetimepicker": "react-native-datetimepicker.ts"
3651
+ };
3652
+ var coreOwnedPackages = [
3653
+ "react",
3654
+ "react-dom",
3655
+ "react-reconciler",
3656
+ "react/jsx-runtime",
3657
+ "react/jsx-dev-runtime",
3658
+ "canvaskit-wasm",
3659
+ "canvaskit-wasm/full",
3660
+ "yoga-layout"
3661
+ ];
3662
+ var nativePackagePatterns = [
3663
+ /^expo-/,
3664
+ /^react-native-/,
3665
+ /^@react-native\//,
3666
+ /^@react-native-community\//,
3667
+ /^@expo\//,
3668
+ /^expo$/
3669
+ ];
3670
+ function getPackageName(source) {
3671
+ return source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0];
3672
+ }
3673
+ function isNativePackage(source) {
3674
+ return nativePackagePatterns.some((p) => p.test(source));
3675
+ }
3676
+ function moduleExistsIn(pkgName, dir) {
3677
+ try {
3678
+ return import_fs3.default.existsSync(import_path3.default.join(dir, "node_modules", pkgName));
3679
+ } catch {
3680
+ return false;
3681
+ }
3682
+ }
3683
+ function sootsim(options = {}) {
3684
+ const appDir = options.app ? import_path3.default.resolve(options.app) : "";
3685
+ const extraSources = (options.sources || []).map((s) => import_path3.default.resolve(s));
3686
+ const ownedPackages = /* @__PURE__ */ new Set([...coreOwnedPackages, ...options.ownedPackages || []]);
3687
+ function isAppSource(filePath) {
3688
+ if (filePath.includes("/node_modules/")) return false;
3689
+ if (appDir && filePath.startsWith(appDir)) return true;
3690
+ return extraSources.some((s) => filePath.startsWith(s));
3691
+ }
3692
+ return [
3693
+ wsBridgePlugin(),
3694
+ reactBridgePlugin(),
3695
+ sootsimConfigPlugin(appDir, extraSources),
3696
+ fixJsxRuntimeExports(),
3697
+ nodeModulesJsxTransform(),
3698
+ metroNativeResolve(isAppSource),
3699
+ reactNativeRequirePlugin(isAppSource),
3700
+ externalAppResolvePlugin(appDir, isAppSource, ownedPackages),
3701
+ externalAppTransformPlugin(isAppSource),
3702
+ stubMissingNativeDeps(appDir),
3703
+ stubMissingImages(isAppSource)
3704
+ ];
3705
+ }
3706
+ function wsBridgePlugin() {
3707
+ let bridgeHost = null;
3708
+ return {
3709
+ name: "sootsim-ws-bridge",
3710
+ configureServer(server) {
3711
+ if (bridgeHost) return;
3712
+ void Promise.resolve().then(() => (init_bridge_host(), bridge_host_exports)).then(({ SootSimBridgeHost: SootSimBridgeHost2 }) => {
3713
+ bridgeHost = new SootSimBridgeHost2({ port: DEFAULT_SOOTSIM_BRIDGE_PORT });
3714
+ bridgeHost.start?.({ silent: true });
3715
+ }).catch((err) => {
3716
+ const message = err instanceof Error ? err.message : String(err);
3717
+ console.warn("[sootsim] ws bridge failed to start:", message);
3718
+ });
3719
+ server.httpServer?.on("close", () => {
3720
+ if (bridgeHost) {
3721
+ void bridgeHost.close?.();
3722
+ bridgeHost = null;
3723
+ }
3724
+ });
3725
+ }
3726
+ };
3727
+ }
3728
+ function sootsimConfigPlugin(appDir, extraSources) {
3729
+ return {
3730
+ name: "sootsim-config",
3731
+ config(_, { mode }) {
3732
+ const fsAllow = ["."];
3733
+ for (const src of extraSources) fsAllow.push(src);
3734
+ if (appDir) {
3735
+ fsAllow.push(appDir);
3736
+ const appNodeModules = import_path3.default.join(appDir, "node_modules");
3737
+ if (import_fs3.default.existsSync(appNodeModules)) fsAllow.push(appNodeModules);
3738
+ }
3739
+ const stubAliases = Object.entries(builtinStubs).sort((a, b) => b[0].length - a[0].length).map(([find, file]) => ({
3740
+ find,
3741
+ replacement: import_path3.default.resolve(compatStubsDir, file)
3742
+ }));
3743
+ const dedupe = ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"];
3744
+ if (!appDir) {
3745
+ dedupe.push(...internalTamaguiPackages);
3746
+ }
3747
+ const optimizeDepsExclude = [
3748
+ "@tamagui/config",
3749
+ "@tamagui/demos",
3750
+ "react-native",
3751
+ // tamagui native variants reference RN internals that the metro transform
3752
+ // handles — must go through sootsim plugin, not rolldown pre-bundling
3753
+ ...!appDir ? internalTamaguiPackages : [],
3754
+ // ships .js with JSX that rolldown can't parse
3755
+ "react-native-actions-sheet",
3756
+ // these packages import `react-native-reanimated` and `react-native-gesture-handler`,
3757
+ // both of which we alias to local stubs. pre-bundling would resolve those imports
3758
+ // against the REAL packages (the alias only applies to main-plugin resolveId), so
3759
+ // the resulting dep has a different reanimated instance than the app graph. keep
3760
+ // them served live so our plugin aliases kick in.
3761
+ "@gorhom/bottom-sheet",
3762
+ "@gorhom/portal"
3763
+ ];
3764
+ return {
3765
+ define: {
3766
+ "process.env.NODE_ENV": JSON.stringify(mode),
3767
+ "process.env.TEST_NATIVE_PLATFORM": JSON.stringify(""),
3768
+ "process.env.TAMAGUI_TARGET": JSON.stringify("native"),
3769
+ global: "globalThis"
3770
+ },
3771
+ optimizeDeps: {
3772
+ include: [
3773
+ "react",
3774
+ "react-dom",
3775
+ "react-dom/client",
3776
+ "react-reconciler",
3777
+ "react-reconciler/constants",
3778
+ "react/jsx-runtime",
3779
+ "react/jsx-dev-runtime",
3780
+ "@react-native/normalize-color",
3781
+ // CJS deps transitively imported by @gorhom/bottom-sheet and
3782
+ // @gorhom/portal (both excluded above). vite's CJS→ESM interop
3783
+ // only kicks in via pre-bundle, so these must be pre-bundled even
3784
+ // though their importers aren't.
3785
+ "invariant",
3786
+ "nanoid/non-secure"
3787
+ ],
3788
+ exclude: optimizeDepsExclude,
3789
+ // vite 8: rolldown replaces esbuild for dep optimization
3790
+ rolldownOptions: {
3791
+ resolve: {
3792
+ conditionNames: ["react-native", "import", "require"],
3793
+ mainFields: ["react-native", "module", "jsnext:main", "jsnext"],
3794
+ extensions: [
3795
+ ".ios.tsx",
3796
+ ".ios.ts",
3797
+ ".ios.jsx",
3798
+ ".ios.js",
3799
+ ".native.tsx",
3800
+ ".native.ts",
3801
+ ".native.jsx",
3802
+ ".native.js",
3803
+ ".native.mjs",
3804
+ ".mjs",
3805
+ ".js",
3806
+ ".mts",
3807
+ ".ts",
3808
+ ".jsx",
3809
+ ".tsx",
3810
+ ".json"
3811
+ ]
3812
+ },
3813
+ plugins: [
3814
+ {
3815
+ name: "sootsim-stub-images",
3816
+ resolveId(source, importer) {
3817
+ if (/\.(jpg|png|gif)$/.test(source) && importer?.includes("node_modules")) {
3818
+ return { id: "\0stub-image" };
3819
+ }
3820
+ },
3821
+ load(id) {
3822
+ if (id === "\0stub-image") return 'export default ""';
3823
+ }
3824
+ }
3825
+ ]
3826
+ }
3827
+ },
3828
+ build: { target: "esnext" },
3829
+ oxc: { target: "esnext" },
3830
+ server: {
3831
+ fs: { allow: fsAllow }
3832
+ },
3833
+ resolve: {
3834
+ dedupe,
3835
+ conditions: ["react-native", "import", "require"],
3836
+ mainFields: ["react-native", "module", "jsnext:main", "jsnext"],
3837
+ extensions: [
3838
+ ".ios.tsx",
3839
+ ".ios.ts",
3840
+ ".ios.jsx",
3841
+ ".ios.js",
3842
+ ".native.tsx",
3843
+ ".native.ts",
3844
+ ".native.jsx",
3845
+ ".native.js",
3846
+ ".native.mjs",
3847
+ ".mjs",
3848
+ ".js",
3849
+ ".mts",
3850
+ ".ts",
3851
+ ".jsx",
3852
+ ".tsx",
3853
+ ".json"
3854
+ ],
3855
+ alias: [
3856
+ ...sootsimBrowserSourceAliases,
3857
+ ...Object.entries(rnLibraryStubs).map(([find, replacement]) => ({
3858
+ find,
3859
+ replacement
3860
+ })),
3861
+ { find: /^react-native\/Libraries\/.*/, replacement: rnDeepStubDefault },
3862
+ { find: "react-native", replacement: rnShimPath },
3863
+ { find: "react-native-web", replacement: rnShimPath },
3864
+ ...stubAliases
3865
+ ]
3866
+ }
3867
+ };
3868
+ }
3869
+ };
3870
+ }
3871
+ function reactBridgePlugin() {
3872
+ let isBuild = false;
3873
+ let redirectCount = 0;
3874
+ let skipCount = 0;
3875
+ return {
3876
+ name: "sootsim-react-bridge",
3877
+ enforce: "pre",
3878
+ configResolved(config) {
3879
+ isBuild = config.command === "build";
3880
+ console.log("[react-bridge-plugin] isBuild:", isBuild);
3881
+ },
3882
+ async resolveId(source, importer, options) {
3883
+ if (source !== "react" || !importer) return null;
3884
+ if (importer.includes("/compat/") || importer.includes("/sootsim/src/") || importer.includes("/sootsim-engine/src/")) {
3885
+ skipCount++;
3886
+ if (skipCount <= 5) {
3887
+ const skip = options?.scan || options?.ssr || isBuild;
3888
+ if (!skip) {
3889
+ console.log(
3890
+ "[react-bridge-plugin] react import from:",
3891
+ importer.split("/").slice(-3).join("/"),
3892
+ "skip:",
3893
+ skip
3894
+ );
3895
+ }
3896
+ }
3897
+ }
3898
+ if (options?.scan || options?.ssr || isBuild) return null;
3899
+ const shouldRedirect = importer.includes("/sootsim/src/") || importer.includes("/sootsim-engine/src/") || importer.includes("/compat/src/") || importer.includes("react-reconciler");
3900
+ if (!shouldRedirect) return null;
3901
+ if (importer.includes("react-bridge")) return null;
3902
+ if (importer.includes("stub-registry")) return null;
3903
+ redirectCount++;
3904
+ if (redirectCount <= 10) {
3905
+ console.log(
3906
+ "[react-bridge-plugin] REDIRECTING:",
3907
+ importer.split("/").slice(-3).join("/")
3908
+ );
3909
+ } else if (redirectCount === 11) {
3910
+ console.log("[react-bridge-plugin] ... more redirects");
3911
+ }
3912
+ return { id: reactBridgePath, external: false };
3913
+ }
3914
+ };
3915
+ }
3916
+ function workerReactBridgePlugin() {
3917
+ return {
3918
+ name: "sootsim-worker-react-bridge",
3919
+ enforce: "pre",
3920
+ async resolveId(source, importer, options) {
3921
+ if (source !== "react" || !importer) return null;
3922
+ if (options?.scan || options?.ssr) return null;
3923
+ const shouldRedirect = importer.includes("/sootsim/src/") || importer.includes("/sootsim-engine/src/") || importer.includes("/compat/src/") || importer.includes("react-reconciler");
3924
+ if (!shouldRedirect) return null;
3925
+ if (importer.includes("react-bridge")) return null;
3926
+ if (importer.includes("stub-registry")) return null;
3927
+ return { id: reactBridgePath, external: false };
3928
+ }
3929
+ };
3930
+ }
3931
+ function fixJsxRuntimeExports() {
3932
+ return {
3933
+ name: "sootsim-fix-jsx-runtime",
3934
+ enforce: "pre",
3935
+ async resolveId(source, importer, options) {
3936
+ if (source === "react/jsx-runtime" || source === "react/jsx-dev-runtime") {
3937
+ const resolved = await this.resolve(source, importer, {
3938
+ ...options,
3939
+ skipSelf: true
3940
+ });
3941
+ if (!resolved) return null;
3942
+ const prefix = source === "react/jsx-runtime" ? "\0sootsim:jsx-runtime:" : "\0sootsim:jsx-dev-runtime:";
3943
+ return prefix + resolved.id;
3944
+ }
3945
+ return null;
3946
+ },
3947
+ load(id) {
3948
+ if (id.startsWith("\0sootsim:jsx-runtime:")) {
3949
+ const realId = id.slice("\0sootsim:jsx-runtime:".length);
3950
+ return [
3951
+ `import * as _mod from ${JSON.stringify(realId)};`,
3952
+ `const _m = _mod.default || _mod;`,
3953
+ `export const jsx = _m.jsx || _mod.jsx;`,
3954
+ `export const jsxs = _m.jsxs || _mod.jsxs;`,
3955
+ `export const Fragment = _m.Fragment || _mod.Fragment;`
3956
+ ].join("\n");
3957
+ }
3958
+ if (id.startsWith("\0sootsim:jsx-dev-runtime:")) {
3959
+ const realId = id.slice("\0sootsim:jsx-dev-runtime:".length);
3960
+ return [
3961
+ `import * as _mod from ${JSON.stringify(realId)};`,
3962
+ `import * as _runtime from "react/jsx-runtime";`,
3963
+ `const _m = _mod.default || _mod;`,
3964
+ // production React builds may not expose jsxDEV from react/jsx-dev-runtime.
3965
+ // fall back to jsx so transformed modules still execute.
3966
+ `export const jsxDEV = _m.jsxDEV || _mod.jsxDEV || _m.jsx || _mod.jsx || _runtime.jsx;`,
3967
+ `export const Fragment = _m.Fragment || _mod.Fragment || _runtime.Fragment;`
3968
+ ].join("\n");
3969
+ }
3970
+ return null;
3971
+ }
3972
+ };
3973
+ }
3974
+ function nodeModulesJsxTransform() {
3975
+ return {
3976
+ name: "sootsim-node-modules-jsx",
3977
+ enforce: "pre",
3978
+ async transform(code, id) {
3979
+ if (!id.includes("/node_modules/")) return null;
3980
+ if (!id.endsWith(".js")) return null;
3981
+ if (!code.includes("<")) return null;
3982
+ if (!/[=(\s,]<[A-Z]/.test(code) && !/<\/[A-Z]/.test(code)) return null;
3983
+ const needsReact = !code.includes("import React") && !code.includes("import * as React");
3984
+ const withReact = needsReact ? `import React from 'react';
3985
+ ${code}` : code;
3986
+ const transformed = await (0, import_vite.transformWithOxc)(withReact, id, {
3987
+ lang: "jsx",
3988
+ jsx: { runtime: "classic" }
3989
+ });
3990
+ return { code: transformed.code, map: transformed.map };
3991
+ }
3992
+ };
3993
+ }
3994
+ function metroNativeResolve(isAppSource) {
3995
+ const platformExts = [
3996
+ ".ios.tsx",
3997
+ ".ios.ts",
3998
+ ".ios.jsx",
3999
+ ".ios.js",
4000
+ ".native.tsx",
4001
+ ".native.ts",
4002
+ ".native.jsx",
4003
+ ".native.js",
4004
+ ".native.mjs"
4005
+ ];
4006
+ const allExts = [...platformExts, ".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
4007
+ function tryResolve(base) {
4008
+ for (const ext of platformExts) {
4009
+ const candidate = base + ext;
4010
+ try {
4011
+ if (import_fs3.default.existsSync(candidate)) return candidate;
4012
+ } catch {
4013
+ }
4014
+ }
4015
+ for (const ext of platformExts) {
4016
+ const candidate = import_path3.default.join(base, "index" + ext);
4017
+ try {
4018
+ if (import_fs3.default.existsSync(candidate)) return candidate;
4019
+ } catch {
4020
+ }
4021
+ }
4022
+ return null;
4023
+ }
4024
+ return {
4025
+ name: "sootsim-metro-native-resolve",
4026
+ enforce: "pre",
4027
+ resolveId(source, importer) {
4028
+ if (!importer) return null;
4029
+ if (!importer.includes("node_modules") && !isAppSource(importer)) return null;
4030
+ if (!source.startsWith(".")) return null;
4031
+ if (platformExts.some((ext) => source.endsWith(ext))) return null;
4032
+ const hasBaseExt = allExts.some((ext) => source.endsWith(ext));
4033
+ const dir = import_path3.default.dirname(importer);
4034
+ if (hasBaseExt) {
4035
+ const resolved = import_path3.default.resolve(dir, source);
4036
+ for (const ext of allExts) {
4037
+ if (source.endsWith(ext)) {
4038
+ const found = tryResolve(resolved.slice(0, -ext.length));
4039
+ if (found) return found;
4040
+ break;
4041
+ }
4042
+ }
4043
+ } else {
4044
+ const found = tryResolve(import_path3.default.resolve(dir, source));
4045
+ if (found) return found;
4046
+ }
4047
+ return null;
4048
+ }
4049
+ };
4050
+ }
4051
+ function reactNativeRequirePlugin(isAppSource) {
4052
+ return {
4053
+ name: "sootsim-react-native-require",
4054
+ enforce: "pre",
4055
+ transform(code, id) {
4056
+ if (!id.includes("node_modules") && !isAppSource(id)) return null;
4057
+ if (!code.includes("react-native")) return null;
4058
+ let hasChanges = false;
4059
+ let imports = "";
4060
+ let result = code;
4061
+ let importCounter = 0;
4062
+ if (result.includes('require("react-native")') || result.includes("require('react-native')")) {
4063
+ imports += `import * as __soot_rn_shim__ from ${JSON.stringify(rnShimPath)};
4064
+ `;
4065
+ result = result.replace(/require\(["']react-native["']\)/g, "__soot_rn_shim__");
4066
+ hasChanges = true;
4067
+ }
4068
+ const deepRequireRegex = /require\(["'](react-native\/Libraries\/[^"']+)["']\)/g;
4069
+ const replacements = [];
4070
+ const seenPaths = /* @__PURE__ */ new Map();
4071
+ let match;
4072
+ deepRequireRegex.lastIndex = 0;
4073
+ while ((match = deepRequireRegex.exec(result)) !== null) {
4074
+ const importPath = match[1];
4075
+ if (!seenPaths.has(importPath)) {
4076
+ const varName = `__soot_rn_lib_${importCounter++}__`;
4077
+ const stubPath = rnLibraryStubs[importPath] || rnDeepStubDefault;
4078
+ imports += `import * as ${varName} from ${JSON.stringify(stubPath)};
4079
+ `;
4080
+ seenPaths.set(importPath, varName);
4081
+ }
4082
+ replacements.push({
4083
+ full: match[0],
4084
+ path: importPath,
4085
+ varName: seenPaths.get(importPath)
4086
+ });
4087
+ }
4088
+ if (replacements.length > 0) {
4089
+ for (const rep of replacements) {
4090
+ result = result.replace(rep.full, rep.varName);
4091
+ }
4092
+ hasChanges = true;
4093
+ }
4094
+ if (!hasChanges) return null;
4095
+ return { code: imports + result, map: null };
4096
+ }
4097
+ };
4098
+ }
4099
+ function externalAppResolvePlugin(appDir, isAppSource, ownedPackages) {
4100
+ if (!appDir) return { name: "sootsim-external-app-resolve" };
4101
+ const appVirtualImporter = import_path3.default.join(appDir, "_virtual_.js");
4102
+ return {
4103
+ name: "sootsim-external-app-resolve",
4104
+ enforce: "pre",
4105
+ async resolveId(source, importer, options) {
4106
+ if (!importer) return null;
4107
+ if (source.startsWith(".") || source.startsWith("/") || source.startsWith("\0"))
4108
+ return null;
4109
+ if (ownedPackages.has(source) || ownedPackages.has(getPackageName(source)))
4110
+ return null;
4111
+ if (!isAppSource(importer) && !importer.includes(import_path3.default.join(appDir, "node_modules")))
4112
+ return null;
4113
+ if (source === "react-native" || source.startsWith("react-native/")) return null;
4114
+ const resolved = await this.resolve(source, appVirtualImporter, {
4115
+ ...options,
4116
+ skipSelf: true
4117
+ });
4118
+ return resolved || null;
4119
+ }
4120
+ };
4121
+ }
4122
+ function externalAppTransformPlugin(isAppSource) {
4123
+ return {
4124
+ name: "sootsim-external-app-transform",
4125
+ enforce: "pre",
4126
+ async transform(code, id) {
4127
+ if (!isAppSource(id)) return null;
4128
+ let result = code;
4129
+ let imports = "";
4130
+ let importCounter = 0;
4131
+ let hasChanges = false;
4132
+ if (code.includes("require(")) {
4133
+ const requireRegex = /require\(["']([^"']+)["']\)/g;
4134
+ const replacements = [];
4135
+ const seenPaths = /* @__PURE__ */ new Map();
4136
+ let match;
4137
+ requireRegex.lastIndex = 0;
4138
+ while ((match = requireRegex.exec(result)) !== null) {
4139
+ const reqPath = match[1];
4140
+ if (reqPath === "react-native" || reqPath.startsWith("react-native/")) continue;
4141
+ if (!seenPaths.has(reqPath)) {
4142
+ const varName = `__soot_cjs_${importCounter++}__`;
4143
+ imports += `import * as ${varName} from ${JSON.stringify(reqPath)};
4144
+ `;
4145
+ seenPaths.set(reqPath, varName);
4146
+ }
4147
+ replacements.push({ full: match[0], varName: seenPaths.get(reqPath) });
4148
+ }
4149
+ if (replacements.length > 0) {
4150
+ for (const rep of replacements) {
4151
+ result = result.replace(rep.full, rep.varName);
4152
+ }
4153
+ hasChanges = true;
4154
+ }
4155
+ }
4156
+ if (imports) result = imports + result;
4157
+ if (id.endsWith(".js") && result.includes("<")) {
4158
+ const needsReact = !result.includes("import React") && !result.includes("import * as React");
4159
+ const withReact = needsReact ? `import React from 'react';
4160
+ ${result}` : result;
4161
+ const transformed = await (0, import_vite.transformWithOxc)(withReact, id, {
4162
+ lang: "jsx",
4163
+ jsx: { runtime: "classic" }
4164
+ });
4165
+ return { code: transformed.code, map: transformed.map };
4166
+ }
4167
+ if (!hasChanges) return null;
4168
+ return { code: result, map: null };
4169
+ }
4170
+ };
4171
+ }
4172
+ function stubMissingNativeDeps(appDir) {
4173
+ const checked = /* @__PURE__ */ new Map();
4174
+ function packageExists(pkgName) {
4175
+ const cached = checked.get(pkgName);
4176
+ if (cached !== void 0) return cached;
4177
+ let exists = moduleExistsIn(pkgName, sootsimRoot);
4178
+ if (!exists && appDir) exists = moduleExistsIn(pkgName, appDir);
4179
+ checked.set(pkgName, exists);
4180
+ return exists;
4181
+ }
4182
+ return {
4183
+ name: "sootsim-stub-missing-native-deps",
4184
+ resolveId(source) {
4185
+ if (!isNativePackage(source)) return null;
4186
+ const pkgName = getPackageName(source);
4187
+ if (packageExists(pkgName)) return null;
4188
+ console.log(`[sootsim] auto-stubbing missing native dep: ${source}`);
4189
+ return nativeAutoStubPath;
4190
+ }
4191
+ };
4192
+ }
4193
+ function stubMissingImages(isAppSource) {
4194
+ return {
4195
+ name: "sootsim-stub-images",
4196
+ enforce: "pre",
4197
+ resolveId(id, importer) {
4198
+ if (id.endsWith(".jpg") || id.endsWith(".png") || id.endsWith(".gif")) {
4199
+ if (importer?.includes("node_modules") || importer && isAppSource(importer)) {
4200
+ return "\0stub-image";
4201
+ }
4202
+ }
4203
+ return null;
4204
+ },
4205
+ load(id) {
4206
+ if (id === "\0stub-image") return 'export default ""';
4207
+ return null;
4208
+ }
4209
+ };
4210
+ }
4211
+ // Annotate the CommonJS export names for ESM import in node:
4212
+ 0 && (module.exports = {
4213
+ fixJsxRuntimeExports,
4214
+ sootsim,
4215
+ workerReactBridgePlugin,
4216
+ wsBridgePlugin
4217
+ });