sootsim 0.1.84 → 0.1.86

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 (153) hide show
  1. package/detox/element-types.ts +36 -0
  2. package/detox/expectations.ts +1 -1
  3. package/detox/index.ts +2 -35
  4. package/dist-cli/bin.js +3 -3
  5. package/dist-cli/chunks/{agent-2CWD6W6P.js → agent-S3WLX5Z4.js} +2 -2
  6. package/dist-cli/chunks/{agent-wrapper-5W3LOX6S.js → agent-wrapper-PFPEQTPG.js} +2 -2
  7. package/dist-cli/chunks/{assert-ZOMAMKRT.js → assert-RQD66YGE.js} +2 -2
  8. package/dist-cli/chunks/auto-bootstrap-PWF7OYCV.js +2 -0
  9. package/dist-cli/chunks/beta-2HH7F2DQ.js +2 -0
  10. package/dist-cli/chunks/{chunk-D4HUVLZR.js → chunk-2IYMBWHL.js} +1 -1
  11. package/dist-cli/chunks/{chunk-DUUSJDES.js → chunk-3GCSX5H5.js} +1 -1
  12. package/dist-cli/chunks/{chunk-EQCKGC4B.js → chunk-3TNIXR6J.js} +1 -1
  13. package/dist-cli/chunks/{chunk-4OWVPRZV.js → chunk-535UNERF.js} +2 -2
  14. package/dist-cli/chunks/{chunk-4K7BH2D4.js → chunk-5DFVKWYQ.js} +3 -3
  15. package/dist-cli/chunks/{chunk-AJVTY6KY.js → chunk-5NW6W7YF.js} +1 -1
  16. package/dist-cli/chunks/{chunk-XQ2OBHBE.js → chunk-5ZYANOOI.js} +2 -2
  17. package/dist-cli/chunks/{chunk-ELJLF4SG.js → chunk-6A7IWFXR.js} +63 -62
  18. package/dist-cli/chunks/{chunk-73UZXB4B.js → chunk-6LG7WJMD.js} +2 -2
  19. package/dist-cli/chunks/{chunk-OOOR7NT2.js → chunk-7YZHI7V6.js} +1 -1
  20. package/dist-cli/chunks/{chunk-TL7SIZ7S.js → chunk-CMAANHYQ.js} +1 -1
  21. package/dist-cli/chunks/{chunk-7NWNTUJF.js → chunk-CQAQTU5K.js} +1 -1
  22. package/dist-cli/chunks/{chunk-C3DPQZ4J.js → chunk-CSJS4MRN.js} +2 -2
  23. package/dist-cli/chunks/{chunk-QMSJR5R2.js → chunk-DKW7Q4F3.js} +2 -2
  24. package/dist-cli/chunks/{chunk-EQ7TFQ2F.js → chunk-EDWDFOPL.js} +1 -1
  25. package/dist-cli/chunks/{chunk-WNVNU2OW.js → chunk-FF5KD3BS.js} +2 -2
  26. package/dist-cli/chunks/{chunk-PPKKA5VW.js → chunk-FO52BFW4.js} +2 -2
  27. package/dist-cli/chunks/chunk-I3JMONYJ.js +2 -0
  28. package/dist-cli/chunks/{chunk-7YHDJLO2.js → chunk-JB467MUR.js} +45 -45
  29. package/dist-cli/chunks/{chunk-HYPJW65U.js → chunk-JBYW57OA.js} +2 -2
  30. package/dist-cli/chunks/{chunk-SQX5CAYG.js → chunk-JITAVV2G.js} +1 -1
  31. package/dist-cli/chunks/{chunk-V2GQ4WXJ.js → chunk-JMILXXI4.js} +2 -2
  32. package/dist-cli/chunks/chunk-KGYC4SZA.js +2 -0
  33. package/dist-cli/chunks/{chunk-BKBL6K2G.js → chunk-KQYOS5SM.js} +1 -1
  34. package/dist-cli/chunks/{chunk-VH7F45CN.js → chunk-M6GOCS27.js} +1 -1
  35. package/dist-cli/chunks/{chunk-3HXQ7MJK.js → chunk-NBBH2PVW.js} +2 -2
  36. package/dist-cli/chunks/chunk-NIRJVBXJ.js +1 -0
  37. package/dist-cli/chunks/{chunk-SQZAC7C4.js → chunk-NLH7FNSG.js} +1 -1
  38. package/dist-cli/chunks/{chunk-RIXUH3NK.js → chunk-OBHJKTWA.js} +2 -2
  39. package/dist-cli/chunks/{chunk-KU6MSPAH.js → chunk-OCTDP37S.js} +2 -2
  40. package/dist-cli/chunks/{chunk-SFGUPL2X.js → chunk-P7IKKZTG.js} +2 -2
  41. package/dist-cli/chunks/{chunk-5XCXOLG2.js → chunk-PVMX5UNR.js} +2 -2
  42. package/dist-cli/chunks/chunk-QUYC7CVV.js +1 -0
  43. package/dist-cli/chunks/{chunk-YCIA4BHJ.js → chunk-STHMWSVN.js} +2 -2
  44. package/dist-cli/chunks/{chunk-AWSQUOAS.js → chunk-TSNBQ4ZV.js} +10 -10
  45. package/dist-cli/chunks/{chunk-BCBNVJVG.js → chunk-UWSP2AT7.js} +1 -1
  46. package/dist-cli/chunks/{chunk-RF4R2U46.js → chunk-V7CFSKMC.js} +2 -2
  47. package/dist-cli/chunks/{chunk-TK3OJSEO.js → chunk-VFGAEMSI.js} +2 -2
  48. package/dist-cli/chunks/{chunk-4OPRODFA.js → chunk-VUYCS6QI.js} +2 -2
  49. package/dist-cli/chunks/chunk-XB4QIINM.js +24 -0
  50. package/dist-cli/chunks/{chunk-P7WDNKOS.js → chunk-XFJYEKYK.js} +3 -3
  51. package/dist-cli/chunks/{chunk-SV7FOGJ3.js → chunk-XZE53P4L.js} +2 -2
  52. package/dist-cli/chunks/chunk-Y7BXSTVX.js +1 -0
  53. package/dist-cli/chunks/cli-version-TGWWTYQX.js +2 -0
  54. package/dist-cli/chunks/{compat-FWSEEGEH.js → compat-I2U3P4KP.js} +3 -3
  55. package/dist-cli/chunks/{config-CYI2WAGP.js → config-S73CCGP5.js} +2 -2
  56. package/dist-cli/chunks/{control-UXY7YQVX.js → control-QR6MY7RA.js} +2 -2
  57. package/dist-cli/chunks/{cpu-profile-IKAE3KTY.js → cpu-profile-RFYCTVAF.js} +2 -2
  58. package/dist-cli/chunks/{daemon-ZUMF53YB.js → daemon-D5MV2B22.js} +2 -2
  59. package/dist-cli/chunks/{debug-P6KULKKS.js → debug-ZYEI75AG.js} +3 -3
  60. package/dist-cli/chunks/{detox-SPWAZCYG.js → detox-J5IH52RV.js} +2 -2
  61. package/dist-cli/chunks/{device-JWEPK6I2.js → device-NOBLSUOD.js} +2 -2
  62. package/dist-cli/chunks/{diagnose-IZODTXV2.js → diagnose-B6J5ZUHV.js} +2 -2
  63. package/dist-cli/chunks/drivers-RRHVOU6S.js +2 -0
  64. package/dist-cli/chunks/{electron-R5GP6RVB.js → electron-PSX4KDCC.js} +3 -3
  65. package/dist-cli/chunks/flow-FWNVFKMP.js +2 -0
  66. package/dist-cli/chunks/{hints-DYDNYX7N.js → hints-ZE4I3YO3.js} +2 -2
  67. package/dist-cli/chunks/{home-paths-GLMX5OKL.js → home-paths-N76MJE3D.js} +2 -2
  68. package/dist-cli/chunks/{inspect-FJOPCTY2.js → inspect-V2TXTDOG.js} +3 -3
  69. package/dist-cli/chunks/install-4PINRR2O.js +2 -0
  70. package/dist-cli/chunks/{install-desktop-YPJZMZM5.js → install-desktop-6ZRTRRCU.js} +3 -3
  71. package/dist-cli/chunks/{keys-GSYPHWNY.js → keys-L2RN4URM.js} +2 -2
  72. package/dist-cli/chunks/{launch-4G2PKW5X.js → launch-BJGXPNZR.js} +3 -3
  73. package/dist-cli/chunks/{login-KJQGHA64.js → login-HYNEMAYR.js} +4 -4
  74. package/dist-cli/chunks/{logout-XM2SYH5C.js → logout-AO4YS27T.js} +2 -2
  75. package/dist-cli/chunks/{maestro-EOWGI7DG.js → maestro-PRACYFKV.js} +2 -2
  76. package/dist-cli/chunks/{preview-F73TKK37.js → preview-ZTANXVEK.js} +2 -2
  77. package/dist-cli/chunks/{profile-22FDKBUO.js → profile-FNMAGUDB.js} +2 -2
  78. package/dist-cli/chunks/{react-5L6VPFUP.js → react-6ZV2FQIM.js} +2 -2
  79. package/dist-cli/chunks/{record-JZXCQ4IN.js → record-MLFVJZ6Y.js} +2 -2
  80. package/dist-cli/chunks/runtime-5762IE56.js +2 -0
  81. package/dist-cli/chunks/{runtime-delivery-LXUM3R4A.js → runtime-delivery-ATYW2SQR.js} +2 -2
  82. package/dist-cli/chunks/{screenshot-HDRRG33Q.js → screenshot-UOMYMFZ4.js} +2 -2
  83. package/dist-cli/chunks/{screenshot-mode-WY63LZIX.js → screenshot-mode-MWSVD4YG.js} +2 -2
  84. package/dist-cli/chunks/{screenshots-MPV2ENL5.js → screenshots-GSA3VCWB.js} +2 -2
  85. package/dist-cli/chunks/server-YPFC6POG.js +40 -0
  86. package/dist-cli/chunks/setup-repo-QBQ4VWFO.js +2 -0
  87. package/dist-cli/chunks/{skills-BQ73YOBF.js → skills-YE5OPWMQ.js} +2 -2
  88. package/dist-cli/chunks/{start-2WU4W6ZU.js → start-BSSQ5U2V.js} +4 -4
  89. package/dist-cli/chunks/store-EG4SONAH.js +2 -0
  90. package/dist-cli/chunks/telemetry-XXN4LRDS.js +2 -0
  91. package/dist-cli/chunks/{test-OVO4CQTG.js → test-5JMLBH2O.js} +3 -3
  92. package/dist-cli/chunks/{three-mode-BKM3KFM7.js → three-mode-TRBWZJQY.js} +2 -2
  93. package/dist-cli/chunks/{timeline-MDXGEDQL.js → timeline-YMZPIEB4.js} +2 -2
  94. package/dist-cli/chunks/{upgrade-JGQABWVF.js → upgrade-JLAS7FIF.js} +2 -2
  95. package/dist-cli/chunks/upload-K6UNCFQH.js +2 -0
  96. package/dist-cli/chunks/{web-WYFAYQ72.js → web-D6S5UXOO.js} +2 -2
  97. package/dist-cli/chunks/{what-happened-PZW2KW6A.js → what-happened-65NXWU2S.js} +2 -2
  98. package/dist-cli/chunks/{whoami-7ATWJQS6.js → whoami-6BSB6FQC.js} +2 -2
  99. package/dist-lib/agent-daemon-client.cjs +1 -1
  100. package/dist-lib/agent-events.cjs +1 -1
  101. package/dist-lib/agent-sessions.cjs +1 -1
  102. package/dist-lib/attached-projects.cjs +1 -1
  103. package/dist-lib/auth/shared-session.cjs +1 -1
  104. package/dist-lib/backend-origin.cjs +1 -1
  105. package/dist-lib/beta.cjs +1 -1
  106. package/dist-lib/beta.mjs +15 -0
  107. package/dist-lib/bridge-constants.cjs +1 -1
  108. package/dist-lib/cli-constants.cjs +1 -1
  109. package/dist-lib/config.cjs +1 -1
  110. package/dist-lib/detox/index.cjs +1 -1
  111. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  112. package/dist-lib/home-paths.cjs +1 -1
  113. package/dist-lib/host/bridge-host.cjs +244 -35
  114. package/dist-lib/host/fetch-proxy-handler.cjs +24 -4
  115. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  116. package/dist-lib/host/fetch-proxy-overrides.mjs +33 -0
  117. package/dist-lib/host/websocket-proxy.cjs +207 -0
  118. package/dist-lib/index.cjs +136 -138
  119. package/dist-lib/metro.cjs +31 -26
  120. package/dist-lib/profiles.cjs +1 -1
  121. package/dist-lib/render-mode.cjs +1 -1
  122. package/dist-lib/scripts/demo-app-registry.cjs +14 -3
  123. package/dist-lib/scripts/dev-server-scanner.cjs +14 -3
  124. package/dist-lib/skills.cjs +9737 -76
  125. package/dist-lib/vite.cjs +129 -39
  126. package/package.json +8 -6
  127. package/scripts/demo-app-registry.ts +17 -1
  128. package/src/host/bridge-host.ts +8 -1
  129. package/src/host/fetch-proxy-handler.ts +26 -3
  130. package/src/host/websocket-proxy.ts +201 -0
  131. package/src/metro-plugin.ts +9 -77
  132. package/src/runtime-assets.ts +84 -0
  133. package/src/skills/builtin/compat-check.ts +1 -1
  134. package/src/vite-plugin-one.ts +60 -58
  135. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +0 -2
  136. package/dist-cli/chunks/beta-4K2SQACK.js +0 -2
  137. package/dist-cli/chunks/chunk-67ZZ2CM5.js +0 -1
  138. package/dist-cli/chunks/chunk-D3ZSBIIY.js +0 -2
  139. package/dist-cli/chunks/chunk-FUCGLWNN.js +0 -1
  140. package/dist-cli/chunks/chunk-IILJQCZA.js +0 -2
  141. package/dist-cli/chunks/chunk-PS2G44GT.js +0 -24
  142. package/dist-cli/chunks/chunk-ZSMMJMPA.js +0 -1
  143. package/dist-cli/chunks/cli-version-QB4VH24H.js +0 -2
  144. package/dist-cli/chunks/drivers-MK6WJKBC.js +0 -2
  145. package/dist-cli/chunks/flow-6O4GEOPJ.js +0 -2
  146. package/dist-cli/chunks/install-A3TUGGHN.js +0 -2
  147. package/dist-cli/chunks/runtime-EEBX7CFV.js +0 -2
  148. package/dist-cli/chunks/server-5LBMCJ3G.js +0 -35
  149. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +0 -2
  150. package/dist-cli/chunks/store-RE45SUBF.js +0 -2
  151. package/dist-cli/chunks/telemetry-DG6GJLCP.js +0 -2
  152. package/dist-cli/chunks/upload-UJNUA4ZV.js +0 -2
  153. package/dist-lib/vite-base.cjs +0 -6937
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.84 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.86 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -36,11 +36,45 @@ __export(vite_plugin_one_exports, {
36
36
  sootsimPlugin: () => sootsimPlugin
37
37
  });
38
38
  module.exports = __toCommonJS(vite_plugin_one_exports);
39
+ var import_fs2 = __toESM(require("fs"), 1);
40
+ var import_path2 = __toESM(require("path"), 1);
41
+
42
+ // src/runtime-assets.ts
39
43
  var import_fs = __toESM(require("fs"), 1);
40
44
  var import_path = __toESM(require("path"), 1);
41
- var sootsimRoot = import_path.default.resolve(import_path.default.dirname(new URL(__sootsim_import_meta_url).pathname), "..");
42
- var distDir = import_path.default.join(sootsimRoot, "dist-plugin");
43
- var publicDir = import_path.default.join(sootsimRoot, "public");
45
+
46
+ // src/home-paths.ts
47
+ var import_node_fs = __toESM(require("node:fs"), 1);
48
+ var import_node_os = require("node:os");
49
+ var import_node_path = __toESM(require("node:path"), 1);
50
+ var SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
51
+ var ACTIVE_RUNTIME_FILE = "active";
52
+ function sootsimHomeDir() {
53
+ const override = process.env[SOOTSIM_HOME_ENV];
54
+ if (override && override.length > 0) return import_node_path.default.resolve(override);
55
+ return import_node_path.default.join((0, import_node_os.homedir)(), ".sootsim");
56
+ }
57
+ function runtimesDir() {
58
+ return import_node_path.default.join(sootsimHomeDir(), "runtimes");
59
+ }
60
+ function runtimeDir(version) {
61
+ return import_node_path.default.join(runtimesDir(), version);
62
+ }
63
+ function activeRuntimeFile() {
64
+ return import_node_path.default.join(runtimesDir(), ACTIVE_RUNTIME_FILE);
65
+ }
66
+ function readActiveRuntime() {
67
+ try {
68
+ const value = import_node_fs.default.readFileSync(activeRuntimeFile(), "utf8").trim();
69
+ return value.length > 0 ? value : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
75
+
76
+ // src/runtime-assets.ts
77
+ var SOOTSIM_RUNTIME_MISSING_MESSAGE = "[sootsim] no engine runtime installed \u2014 run `sootsim setup-repo` in your project, or `sootsim runtime install`";
44
78
  var MIME_TYPES = {
45
79
  ".js": "application/javascript",
46
80
  ".mjs": "application/javascript",
@@ -48,17 +82,64 @@ var MIME_TYPES = {
48
82
  ".html": "text/html",
49
83
  ".wasm": "application/wasm",
50
84
  ".json": "application/json",
85
+ ".jpg": "image/jpeg",
86
+ ".jpeg": "image/jpeg",
51
87
  ".png": "image/png",
52
88
  ".svg": "image/svg+xml",
89
+ ".webp": "image/webp",
90
+ ".glb": "model/gltf-binary",
53
91
  ".ttf": "font/ttf",
54
92
  ".otf": "font/otf",
55
93
  ".woff": "font/woff",
56
94
  ".woff2": "font/woff2",
57
95
  ".mp3": "audio/mpeg",
58
- ".wav": "audio/wav",
59
- ".jpg": "image/jpeg",
60
- ".webp": "image/webp"
96
+ ".wav": "audio/wav"
61
97
  };
98
+ var ROOT_RUNTIME_PATHS = [
99
+ "/assets/",
100
+ "/engine/",
101
+ "/engine-tenant/",
102
+ "/photos/",
103
+ "/three-mode/",
104
+ "/canvaskit.wasm",
105
+ "/fonts/",
106
+ "/icons/",
107
+ "/sounds/",
108
+ "/spike/",
109
+ "/test-wallpaper.jpg",
110
+ "/preview-sw.js"
111
+ ];
112
+ function resolveActiveRuntimeRoot() {
113
+ const active = readActiveRuntime();
114
+ if (!active) return null;
115
+ const dir = runtimeDir(active);
116
+ if (!import_fs.default.existsSync(import_path.default.join(dir, "index.html"))) return null;
117
+ return dir;
118
+ }
119
+ function isRootRuntimeAssetPath(pathname) {
120
+ return ROOT_RUNTIME_PATHS.some((p) => pathname === p || pathname.startsWith(p));
121
+ }
122
+ function resolveRuntimeFilePath(runtimeRoot, pathname) {
123
+ if (!pathname.startsWith("/")) return null;
124
+ if (pathname.includes("\0") || pathname.includes("\\")) return null;
125
+ for (const segment of pathname.split("/")) {
126
+ if (segment === "..") return null;
127
+ }
128
+ const fullPath = import_path.default.resolve(runtimeRoot, pathname.replace(/^\/+/, ""));
129
+ const rootWithSep = runtimeRoot.endsWith(import_path.default.sep) ? runtimeRoot : runtimeRoot + import_path.default.sep;
130
+ if (!fullPath.startsWith(rootWithSep) && fullPath !== runtimeRoot) return null;
131
+ if (!import_fs.default.existsSync(fullPath) || !import_fs.default.statSync(fullPath).isFile()) return null;
132
+ return fullPath;
133
+ }
134
+ function serveRuntimeFile(res, fullPath) {
135
+ const ext = import_path.default.extname(fullPath);
136
+ res.setHeader("content-type", MIME_TYPES[ext] || "application/octet-stream");
137
+ res.setHeader("cache-control", "max-age=31536000,immutable");
138
+ import_fs.default.createReadStream(fullPath).pipe(res);
139
+ }
140
+
141
+ // src/vite-plugin-one.ts
142
+ var sootsimRoot = import_path2.default.resolve(import_path2.default.dirname(new URL(__sootsim_import_meta_url).pathname), "..");
62
143
  function sootsimPlugin(options = {}) {
63
144
  if (options.enabled === false) return [];
64
145
  const prefix = options.prefix || "/__soot";
@@ -85,14 +166,27 @@ function sootsimPlugin(options = {}) {
85
166
  });
86
167
  server.middlewares.use((req, res, next) => {
87
168
  const url = req.url || "";
88
- if (url === prefix || url === prefix + "/" || url.startsWith(prefix + "/?")) {
89
- const htmlPath = import_path.default.join(distDir, "index.html");
90
- if (!import_fs.default.existsSync(htmlPath)) {
169
+ const pathname = url.split("?")[0];
170
+ const runtimeRoot = resolveActiveRuntimeRoot();
171
+ if (!runtimeRoot) {
172
+ if (pathname === prefix || pathname === prefix + "/" || pathname.startsWith(prefix + "/")) {
91
173
  res.statusCode = 500;
92
- res.end("[sootsim] dist-plugin not built. run: bun scripts/build-plugin.ts");
174
+ res.end(SOOTSIM_RUNTIME_MISSING_MESSAGE);
175
+ return;
176
+ }
177
+ next();
178
+ return;
179
+ }
180
+ if (isRootRuntimeAssetPath(pathname)) {
181
+ const fullPath = resolveRuntimeFilePath(runtimeRoot, pathname);
182
+ if (fullPath) {
183
+ serveRuntimeFile(res, fullPath);
93
184
  return;
94
185
  }
95
- let html = import_fs.default.readFileSync(htmlPath, "utf8");
186
+ }
187
+ if (pathname === prefix || pathname === prefix + "/") {
188
+ const htmlPath = import_path2.default.join(runtimeRoot, "index.html");
189
+ let html = import_fs2.default.readFileSync(htmlPath, "utf8");
96
190
  html = html.replace(
97
191
  "</head>",
98
192
  `<script>history.replaceState(null,'','${prefix}/?bundle=${encodeURIComponent(bundleUrl)}')</script></head>`
@@ -101,31 +195,25 @@ function sootsimPlugin(options = {}) {
101
195
  res.end(html);
102
196
  return;
103
197
  }
104
- if (url.startsWith(prefix + "/")) {
105
- const filePath = url.slice(prefix.length).split("?")[0];
106
- const fullPath = import_path.default.join(distDir, filePath);
107
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
108
- const ext = import_path.default.extname(fullPath);
109
- res.setHeader("content-type", MIME_TYPES[ext] || "application/octet-stream");
110
- res.setHeader("cache-control", "max-age=31536000,immutable");
111
- import_fs.default.createReadStream(fullPath).pipe(res);
112
- return;
113
- }
114
- const publicPath = import_path.default.join(publicDir, filePath);
115
- if (import_fs.default.existsSync(publicPath) && import_fs.default.statSync(publicPath).isFile()) {
116
- const ext = import_path.default.extname(publicPath);
117
- res.setHeader("content-type", MIME_TYPES[ext] || "application/octet-stream");
118
- import_fs.default.createReadStream(publicPath).pipe(res);
198
+ if (pathname.startsWith(prefix + "/")) {
199
+ const fullPath = resolveRuntimeFilePath(
200
+ runtimeRoot,
201
+ pathname.slice(prefix.length)
202
+ );
203
+ if (fullPath) {
204
+ serveRuntimeFile(res, fullPath);
119
205
  return;
120
206
  }
121
- }
122
- const staticRoots = ["/canvaskit.wasm", "/fonts/", "/icons/", "/sounds/"];
123
- if (staticRoots.some((p) => url.startsWith(p))) {
124
- const fullPath = import_path.default.join(publicDir, url.split("?")[0]);
125
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
126
- const ext = import_path.default.extname(fullPath);
127
- res.setHeader("content-type", MIME_TYPES[ext] || "application/octet-stream");
128
- import_fs.default.createReadStream(fullPath).pipe(res);
207
+ const ext = import_path2.default.extname(pathname);
208
+ if (!ext) {
209
+ const htmlPath = import_path2.default.join(runtimeRoot, "index.html");
210
+ let html = import_fs2.default.readFileSync(htmlPath, "utf8");
211
+ html = html.replace(
212
+ "</head>",
213
+ `<script>history.replaceState(null,'','${prefix}/?bundle=${encodeURIComponent(bundleUrl)}')</script></head>`
214
+ );
215
+ res.setHeader("content-type", "text/html");
216
+ res.end(html);
129
217
  return;
130
218
  }
131
219
  }
@@ -134,7 +222,9 @@ function sootsimPlugin(options = {}) {
134
222
  const port = server.config.server.port || 8081;
135
223
  const sootsimUrl = `http://localhost:${port}${prefix}/`;
136
224
  console.log(`[sootsim] serving at ${sootsimUrl}`);
137
- openElectronApp(sootsimUrl);
225
+ if (options.open !== false) {
226
+ openElectronApp(sootsimUrl);
227
+ }
138
228
  }
139
229
  }
140
230
  ];
@@ -150,10 +240,10 @@ async function openElectronApp(sootsimUrl) {
150
240
  }
151
241
  const candidates = [
152
242
  "/Applications/sootsim.app",
153
- import_path.default.join(process.env.HOME || "", "Applications/sootsim.app"),
154
- import_path.default.join(sootsimRoot, "release/mac-arm64/sootsim.app")
243
+ import_path2.default.join(process.env.HOME || "", "Applications/sootsim.app"),
244
+ import_path2.default.join(sootsimRoot, "release/mac-arm64/sootsim.app")
155
245
  ];
156
- let appPath = candidates.find((p) => import_fs.default.existsSync(p));
246
+ let appPath = candidates.find((p) => import_fs2.default.existsSync(p));
157
247
  if (!appPath) {
158
248
  try {
159
249
  const found = execSync(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sootsim",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "sootsim CLI + vite/metro plugins + skills registry. bridge client for driving the proprietary sootsim-engine over WebSocket.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "MIT",
@@ -25,10 +25,6 @@
25
25
  "source": "./src/vite-plugin-one.ts",
26
26
  "default": "./dist-lib/vite.cjs"
27
27
  },
28
- "./vite-base": {
29
- "source": "./src/vite-plugin.ts",
30
- "default": "./dist-lib/vite-base.cjs"
31
- },
32
28
  "./metro": {
33
29
  "source": "./src/metro-plugin.ts",
34
30
  "default": "./dist-lib/metro.cjs"
@@ -39,6 +35,7 @@
39
35
  },
40
36
  "./beta": {
41
37
  "source": "./src/beta.ts",
38
+ "import": "./dist-lib/beta.mjs",
42
39
  "default": "./dist-lib/beta.cjs"
43
40
  },
44
41
  "./agent-events": {
@@ -95,8 +92,13 @@
95
92
  },
96
93
  "./host/fetch-proxy-overrides": {
97
94
  "source": "./src/host/fetch-proxy-overrides.ts",
95
+ "import": "./dist-lib/host/fetch-proxy-overrides.mjs",
98
96
  "default": "./dist-lib/host/fetch-proxy-overrides.cjs"
99
97
  },
98
+ "./host/websocket-proxy": {
99
+ "source": "./src/host/websocket-proxy.ts",
100
+ "default": "./dist-lib/host/websocket-proxy.cjs"
101
+ },
100
102
  "./scripts/dev-server-scanner": {
101
103
  "source": "./scripts/dev-server-scanner.ts",
102
104
  "default": "./dist-lib/scripts/dev-server-scanner.cjs"
@@ -137,7 +139,6 @@
137
139
  "postinstall": "node ./scripts/postinstall.cjs"
138
140
  },
139
141
  "dependencies": {
140
- "@soot/compat": "workspace:*",
141
142
  "ws": "^8.18.0"
142
143
  },
143
144
  "peerDependencies": {
@@ -149,6 +150,7 @@
149
150
  }
150
151
  },
151
152
  "devDependencies": {
153
+ "@soot/compat": "workspace:*",
152
154
  "@soot/sootsim-globals": "workspace:*",
153
155
  "@soot/sootsim-skills": "workspace:*",
154
156
  "@types/ws": "^8.5.13",
@@ -11,6 +11,11 @@ export interface DemoApp {
11
11
  framework: 'expo' | 'one' | 'rock'
12
12
  runtimeConfig?: SootSimConfig
13
13
  sidecars?: DemoSidecar[]
14
+ // extra ports owned by the main demo command, derived from the chosen app
15
+ // port. unlike sidecars, these are not independently reusable services; the
16
+ // launcher clears/reserves them with the app command so stale child stacks
17
+ // cannot leave the app talking to the wrong backend.
18
+ managedPorts?: (port: number) => number[]
14
19
  prepare?: () => void | Promise<void>
15
20
  command: (port: number) => { cmd: string; env?: Record<string, string> }
16
21
  disabled?: boolean
@@ -775,11 +780,22 @@ export const APPS: DemoApp[] = [
775
780
  // be holding pglite locks on ~/takeout/.orez (soot can attach takeout as
776
781
  // a project and spin up its own orez against the same data dir).
777
782
  readyTimeoutMs: 240_000,
783
+ managedPorts: (p) => {
784
+ const offset = p - 8081
785
+ return [5433 + offset, 4848 + offset, 9200 + offset]
786
+ },
778
787
  command: (p) => ({
779
- cmd: 'bun lite',
788
+ cmd: 'bun lite:demo',
780
789
  env: {
790
+ TAKEOUT_ENV_MODE: 'development',
781
791
  PORT_OFFSET: String(p - 8081),
782
792
  OREZ_DATA_DIR: `${HOME}/.cache/sootsim-demo/takeout-orez`,
793
+ VITE_DEMO_MODE: '1',
794
+ ZERO_APP_ID: 'takeout',
795
+ ZERO_APP_PUBLICATIONS: 'zero_takeout',
796
+ ZERO_CVR_MAX_CONNS: '4',
797
+ ZERO_NUM_SYNC_WORKERS: '2',
798
+ ZERO_UPSTREAM_MAX_CONNS: '8',
783
799
  },
784
800
  }),
785
801
  },
@@ -36,6 +36,7 @@ import {
36
36
  isFetchProxyRequestUrl,
37
37
  } from './fetch-proxy-handler'
38
38
  import { openUrl as openUrlInBrowser, type OpenUrlOptions } from './open-url.ts'
39
+ import { handleWebSocketProxyUpgrade } from './websocket-proxy'
39
40
 
40
41
  export interface BridgeSimInfo {
41
42
  id: string
@@ -493,8 +494,14 @@ export class SootSimBridgeHost {
493
494
  process.stderr.write(`ws bridge http error: ${String(err)}\n`)
494
495
  })
495
496
  this.httpServer = server
496
- this.wss = new WebSocketServer({ server })
497
+ this.wss = new WebSocketServer({ noServer: true })
497
498
  this.wireWebSocketServer()
499
+ server.on('upgrade', (req, socket, head) => {
500
+ if (handleWebSocketProxyUpgrade(req, socket, head)) return
501
+ this.wss?.handleUpgrade(req, socket, head, (ws) => {
502
+ this.wss?.emit('connection', ws, req)
503
+ })
504
+ })
498
505
  resolve()
499
506
  })
500
507
  })
@@ -58,6 +58,15 @@ const FETCH_PROXY_CORS_HEADERS: Record<string, string> = {
58
58
  'access-control-max-age': '3600',
59
59
  }
60
60
 
61
+ const APP_API_HEADER_REWRITES = new Set([
62
+ 'host',
63
+ 'origin',
64
+ 'referer',
65
+ 'sec-fetch-site',
66
+ 'sec-fetch-mode',
67
+ 'sec-fetch-dest',
68
+ ])
69
+
61
70
  function applyFetchProxyCors(res: ServerResponse) {
62
71
  for (const [key, value] of Object.entries(FETCH_PROXY_CORS_HEADERS)) {
63
72
  res.setHeader(key, value)
@@ -108,6 +117,22 @@ export function buildFetchProxyHeaders(
108
117
  return headers
109
118
  }
110
119
 
120
+ export function buildAppApiProxyHeaders(
121
+ reqHeaders: Record<string, string | string[] | undefined>,
122
+ targetUrl: URL,
123
+ ): Record<string, string | string[]> {
124
+ const headers: Record<string, string | string[]> = {}
125
+ for (const [key, value] of Object.entries(reqHeaders)) {
126
+ if (!value) continue
127
+ if (APP_API_HEADER_REWRITES.has(key.toLowerCase())) continue
128
+ headers[key] = value
129
+ }
130
+ headers.host = targetUrl.host
131
+ headers.origin = targetUrl.origin
132
+ headers.referer = `${targetUrl.origin}/`
133
+ return headers
134
+ }
135
+
111
136
  export function isFetchProxyRequestUrl(rawUrl: string | undefined): boolean {
112
137
  return rawUrl?.startsWith('/__fetch-proxy?') || rawUrl?.startsWith('/__proxy?') || false
113
138
  }
@@ -251,9 +276,7 @@ export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse):
251
276
 
252
277
  const transport = targetUrl.protocol === 'https:' ? https : http
253
278
 
254
- const fwdHeaders = { ...req.headers }
255
- delete fwdHeaders.host
256
- fwdHeaders.host = targetUrl.host
279
+ const fwdHeaders = buildAppApiProxyHeaders(req.headers, targetUrl)
257
280
 
258
281
  const proxyReq = transport.request(
259
282
  {
@@ -0,0 +1,201 @@
1
+ import { WebSocket, WebSocketServer } from 'ws'
2
+ import type { IncomingMessage } from 'http'
3
+ import type { Duplex } from 'stream'
4
+
5
+ export const WEBSOCKET_PROXY_PATH = '/__websocket-proxy'
6
+
7
+ const STRIP_UPSTREAM_HEADERS = new Set([
8
+ 'host',
9
+ 'connection',
10
+ 'upgrade',
11
+ 'transfer-encoding',
12
+ 'content-length',
13
+ 'sec-websocket-accept',
14
+ 'sec-websocket-extensions',
15
+ 'sec-websocket-key',
16
+ 'sec-websocket-protocol',
17
+ 'sec-websocket-version',
18
+ ])
19
+
20
+ function rejectUpgrade(socket: Duplex, status: number, message: string) {
21
+ try {
22
+ socket.write(
23
+ `HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: ${message.length}\r\n\r\n${message}`,
24
+ )
25
+ } catch {}
26
+ socket.destroy()
27
+ }
28
+
29
+ function isSameOriginUpgrade(req: IncomingMessage): boolean {
30
+ const origin = req.headers.origin
31
+ const host = req.headers.host
32
+ if (!origin || !host) return false
33
+ try {
34
+ return new URL(origin).host === host
35
+ } catch {
36
+ return false
37
+ }
38
+ }
39
+
40
+ function decodeProxyHeaders(encoded: string | null): Record<string, string> {
41
+ if (!encoded) return {}
42
+ const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/')
43
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)
44
+ const parsed = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'))
45
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
46
+ const headers: Record<string, string> = {}
47
+ for (const [key, value] of Object.entries(parsed)) {
48
+ if (value == null) continue
49
+ if (STRIP_UPSTREAM_HEADERS.has(key.toLowerCase())) continue
50
+ headers[key] = Array.isArray(value) ? value.join(', ') : String(value)
51
+ }
52
+ return headers
53
+ }
54
+
55
+ function getDefaultWebSocketOrigin(targetUrl: URL): string {
56
+ const origin = new URL(targetUrl.href)
57
+ origin.protocol = targetUrl.protocol === 'wss:' ? 'https:' : 'http:'
58
+ return origin.origin
59
+ }
60
+
61
+ function getRequestedProtocols(req: IncomingMessage): string[] {
62
+ const header = req.headers['sec-websocket-protocol']
63
+ const value = Array.isArray(header) ? header.join(',') : header || ''
64
+ return value
65
+ .split(',')
66
+ .map((part) => part.trim())
67
+ .filter(Boolean)
68
+ }
69
+
70
+ function safeClose(ws: WebSocket, code: number, reason: string) {
71
+ if (ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
72
+ return
73
+ }
74
+ try {
75
+ ws.close(code, reason)
76
+ } catch {
77
+ ws.terminate()
78
+ }
79
+ }
80
+
81
+ function connectProxyPair(clientWs: WebSocket, upstream: WebSocket) {
82
+ let closing = false
83
+
84
+ const closeBoth = (
85
+ source: WebSocket,
86
+ target: WebSocket,
87
+ code: number,
88
+ reason: Buffer,
89
+ ) => {
90
+ if (closing) return
91
+ closing = true
92
+ safeClose(target, code, reason.toString())
93
+ if (source.readyState === WebSocket.OPEN) {
94
+ safeClose(source, code, reason.toString())
95
+ }
96
+ }
97
+
98
+ clientWs.on('message', (data, isBinary) => {
99
+ if (upstream.readyState === WebSocket.OPEN) {
100
+ upstream.send(data, { binary: isBinary })
101
+ }
102
+ })
103
+ upstream.on('message', (data, isBinary) => {
104
+ if (clientWs.readyState === WebSocket.OPEN) {
105
+ clientWs.send(data, { binary: isBinary })
106
+ }
107
+ })
108
+
109
+ clientWs.on('close', (code, reason) => closeBoth(clientWs, upstream, code, reason))
110
+ upstream.on('close', (code, reason) => closeBoth(upstream, clientWs, code, reason))
111
+ clientWs.on('error', () => safeClose(upstream, 1011, 'proxy client error'))
112
+ upstream.on('error', () => safeClose(clientWs, 1011, 'upstream websocket error'))
113
+ }
114
+
115
+ function createUpstreamWebSocket(
116
+ targetUrl: URL,
117
+ protocols: string[],
118
+ headers: Record<string, string>,
119
+ ): WebSocket {
120
+ const upstreamHeaders = { ...headers }
121
+ if (!Object.keys(upstreamHeaders).some((key) => key.toLowerCase() === 'origin')) {
122
+ upstreamHeaders.origin = getDefaultWebSocketOrigin(targetUrl)
123
+ }
124
+ return new WebSocket(targetUrl.href, protocols, {
125
+ headers: upstreamHeaders,
126
+ })
127
+ }
128
+
129
+ export function isWebSocketProxyRequestUrl(rawUrl: string | undefined): boolean {
130
+ if (!rawUrl) return false
131
+ try {
132
+ return new URL(rawUrl, 'http://localhost').pathname === WEBSOCKET_PROXY_PATH
133
+ } catch {
134
+ return false
135
+ }
136
+ }
137
+
138
+ export function handleWebSocketProxyUpgrade(
139
+ req: IncomingMessage,
140
+ socket: Duplex,
141
+ head: Buffer,
142
+ ): boolean {
143
+ if (!isWebSocketProxyRequestUrl(req.url)) return false
144
+ if (!isSameOriginUpgrade(req)) {
145
+ rejectUpgrade(socket, 403, 'forbidden websocket proxy origin')
146
+ return true
147
+ }
148
+
149
+ let targetUrl: URL
150
+ let headers: Record<string, string>
151
+ try {
152
+ const requestUrl = new URL(req.url || '/', 'http://localhost')
153
+ const target = requestUrl.searchParams.get('url')
154
+ if (!target) {
155
+ rejectUpgrade(socket, 400, 'missing websocket proxy url')
156
+ return true
157
+ }
158
+ targetUrl = new URL(target)
159
+ if (targetUrl.protocol !== 'ws:' && targetUrl.protocol !== 'wss:') {
160
+ rejectUpgrade(socket, 400, 'invalid websocket proxy protocol')
161
+ return true
162
+ }
163
+ headers = decodeProxyHeaders(requestUrl.searchParams.get('headers'))
164
+ } catch {
165
+ rejectUpgrade(socket, 400, 'invalid websocket proxy request')
166
+ return true
167
+ }
168
+
169
+ const protocols = getRequestedProtocols(req)
170
+ const upstream = createUpstreamWebSocket(targetUrl, protocols, headers)
171
+ let completed = false
172
+ socket.once('close', () => {
173
+ if (!completed) upstream.terminate()
174
+ })
175
+ upstream.once('open', () => {
176
+ if (completed) return
177
+ completed = true
178
+ const selectedProtocol = upstream.protocol
179
+ const proxyServer = new WebSocketServer({
180
+ noServer: true,
181
+ clientTracking: false,
182
+ handleProtocols(requestedProtocols) {
183
+ return selectedProtocol || requestedProtocols.values().next().value || false
184
+ },
185
+ })
186
+ proxyServer.handleUpgrade(req, socket, head, (clientWs) => {
187
+ connectProxyPair(clientWs, upstream)
188
+ })
189
+ })
190
+ upstream.once('error', () => {
191
+ if (completed) return
192
+ completed = true
193
+ rejectUpgrade(socket, 502, 'upstream websocket error')
194
+ })
195
+ upstream.once('close', () => {
196
+ if (completed) return
197
+ completed = true
198
+ rejectUpgrade(socket, 502, 'upstream websocket closed')
199
+ })
200
+ return true
201
+ }