sootsim 0.1.294 → 0.1.296

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 (186) hide show
  1. package/README.md +13 -4
  2. package/dist-cli/bin.js +5 -5
  3. package/dist-cli/chunks/{agent-GDFF6G7N.js → agent-3F4K6MQK.js} +2 -2
  4. package/dist-cli/chunks/{agent-wrapper-ZUKQUOD2.js → agent-wrapper-5LLVCQRZ.js} +2 -2
  5. package/dist-cli/chunks/app-fonts-LPTY7A5G.js +4 -0
  6. package/dist-cli/chunks/{assert-GXOGZUKO.js → assert-R2ZUXEMS.js} +2 -2
  7. package/dist-cli/chunks/{auth-P7FAKLHQ.js → auth-CA62Q6IS.js} +2 -2
  8. package/dist-cli/chunks/{beta-RXT5RU3A.js → beta-4AXSOQLU.js} +2 -2
  9. package/dist-cli/chunks/{camera-RNVAC4IO.js → camera-Y4RBIONE.js} +2 -2
  10. package/dist-cli/chunks/{chunk-OYYQQBKP.js → chunk-222Y7NQA.js} +1 -1
  11. package/dist-cli/chunks/chunk-2LQOLMRK.js +4 -0
  12. package/dist-cli/chunks/{chunk-AFLGWOX3.js → chunk-2XUKWIAL.js} +1 -1
  13. package/dist-cli/chunks/{chunk-DI55E6CS.js → chunk-3PAZJSIG.js} +2 -2
  14. package/dist-cli/chunks/{chunk-2M7OZ6Y4.js → chunk-4MDLYOEI.js} +2 -2
  15. package/dist-cli/chunks/{chunk-EXTV6NWM.js → chunk-4NFGOGB7.js} +2 -2
  16. package/dist-cli/chunks/chunk-54TQ3ZGI.js +30 -0
  17. package/dist-cli/chunks/chunk-5FNPAMP4.js +125 -0
  18. package/dist-cli/chunks/{chunk-4Z4URZOG.js → chunk-6C24WCDK.js} +2 -2
  19. package/dist-cli/chunks/{chunk-KWIURH7N.js → chunk-74FOMZP7.js} +2 -2
  20. package/dist-cli/chunks/{chunk-JUOQ4ZPA.js → chunk-7GQWF7HE.js} +1 -1
  21. package/dist-cli/chunks/{chunk-BXO2PTNM.js → chunk-7V3JNANH.js} +4 -4
  22. package/dist-cli/chunks/{chunk-DUHBRW7J.js → chunk-7YQXQSG7.js} +1 -1
  23. package/dist-cli/chunks/{chunk-FSRRJGYC.js → chunk-AUBP44CY.js} +2 -2
  24. package/dist-cli/chunks/chunk-B2FB6IYX.js +4 -0
  25. package/dist-cli/chunks/{chunk-FNCSUHUR.js → chunk-B2LDO55K.js} +1 -1
  26. package/dist-cli/chunks/{chunk-K53YCAQM.js → chunk-BVWIR4CD.js} +1 -1
  27. package/dist-cli/chunks/{chunk-RWUNC66S.js → chunk-C462URRQ.js} +2 -2
  28. package/dist-cli/chunks/chunk-CAAADPRH.js +10 -0
  29. package/dist-cli/chunks/{chunk-VXGCZUX4.js → chunk-CDCVWGNU.js} +3 -3
  30. package/dist-cli/chunks/{chunk-WYRLRNIP.js → chunk-CS23DQB2.js} +2 -2
  31. package/dist-cli/chunks/chunk-DUYKBCM7.js +4 -0
  32. package/dist-cli/chunks/chunk-EISE56VD.js +6 -0
  33. package/dist-cli/chunks/{chunk-VVVORIQI.js → chunk-EJ6BAJKX.js} +1 -1
  34. package/dist-cli/chunks/chunk-F2MS365Z.js +4 -0
  35. package/dist-cli/chunks/{chunk-AMKQUZA2.js → chunk-FROCFR77.js} +1 -1
  36. package/dist-cli/chunks/{chunk-I233T2T4.js → chunk-FUPQIX4U.js} +2 -2
  37. package/dist-cli/chunks/{chunk-V2MYTL4C.js → chunk-G2SO5FPN.js} +2 -2
  38. package/dist-cli/chunks/chunk-GG5PWLGC.js +4 -0
  39. package/dist-cli/chunks/chunk-GG6XOQ5P.js +15 -0
  40. package/dist-cli/chunks/chunk-HSGS2U7P.js +6 -0
  41. package/dist-cli/chunks/{chunk-PZTKQG6H.js → chunk-IR3DUBRF.js} +5 -5
  42. package/dist-cli/chunks/chunk-LHUY46GE.js +4 -0
  43. package/dist-cli/chunks/{chunk-N4MT5KEB.js → chunk-MDBR4XTV.js} +3 -3
  44. package/dist-cli/chunks/{chunk-SLMUW2WU.js → chunk-MQ6QX46T.js} +2 -2
  45. package/dist-cli/chunks/chunk-N3FZRJT7.js +4 -0
  46. package/dist-cli/chunks/{chunk-ISJ4ACWL.js → chunk-NEARYPSC.js} +11 -11
  47. package/dist-cli/chunks/chunk-P7HBJBHH.js +4 -0
  48. package/dist-cli/chunks/{chunk-24BJFJFB.js → chunk-PEUWZRXA.js} +5 -5
  49. package/dist-cli/chunks/{chunk-PSOUSJSZ.js → chunk-QIDFGREY.js} +1 -1
  50. package/dist-cli/chunks/{chunk-ZUX5PIGI.js → chunk-R65XMN4D.js} +1 -1
  51. package/dist-cli/chunks/{chunk-AO65WEF7.js → chunk-S5Z4HKWX.js} +1 -1
  52. package/dist-cli/chunks/chunk-SUJSGGSS.js +23 -0
  53. package/dist-cli/chunks/{chunk-O6I5ZGMY.js → chunk-TIOPG6QZ.js} +1 -1
  54. package/dist-cli/chunks/chunk-WJVBD2UL.js +9 -0
  55. package/dist-cli/chunks/{chunk-D2R7PDN5.js → chunk-X7B5KJ6N.js} +1 -1
  56. package/dist-cli/chunks/{chunk-7HVMAGDK.js → chunk-XMVVZCIM.js} +2 -2
  57. package/dist-cli/chunks/chunk-ZIBKGZJD.js +4 -0
  58. package/dist-cli/chunks/chunk-ZPBIHFQW.js +12 -0
  59. package/dist-cli/chunks/cleanup-4RSKYPQ7.js +25 -0
  60. package/dist-cli/chunks/{cli-version-2JGMYL4O.js → cli-version-VZFF7GGM.js} +2 -2
  61. package/dist-cli/chunks/compat-FABLN4UI.js +20 -0
  62. package/dist-cli/chunks/config-74J37LKC.js +13 -0
  63. package/dist-cli/chunks/control-VBK32Y7F.js +4 -0
  64. package/dist-cli/chunks/daemon-VFO7LRR4.js +4 -0
  65. package/dist-cli/chunks/{debug-XUGWZLIW.js → debug-AOYA2CLR.js} +3 -3
  66. package/dist-cli/chunks/{desktop-IO5HPX5Z.js → desktop-G7XWDCTM.js} +3 -3
  67. package/dist-cli/chunks/{detox-E42OEJJZ.js → detox-QXJ3L2ZI.js} +2 -2
  68. package/dist-cli/chunks/{device-XA67L2EB.js → device-NZZ5ZT3Q.js} +2 -2
  69. package/dist-cli/chunks/{diagnose-36FD6ECF.js → diagnose-5CAEJ42M.js} +2 -2
  70. package/dist-cli/chunks/disk-cleanup-GZSUZJ6Y.js +4 -0
  71. package/dist-cli/chunks/drivers-OLJCUSPO.js +4 -0
  72. package/dist-cli/chunks/{film-XARTGFIZ.js → film-C25VRUNC.js} +3 -3
  73. package/dist-cli/chunks/flow-IW3WUF5D.js +4 -0
  74. package/dist-cli/chunks/help-GEVJF2DA.js +4 -0
  75. package/dist-cli/chunks/{hidden-runtime-alias-T4ZALBE2.js → hidden-runtime-alias-NSLIK6XT.js} +2 -2
  76. package/dist-cli/chunks/home-paths-OC6JZXIM.js +4 -0
  77. package/dist-cli/chunks/inspect-KXJNI7CU.js +4 -0
  78. package/dist-cli/chunks/install-desktop-IYWPRFPB.js +4 -0
  79. package/dist-cli/chunks/{login-GF6WVVER.js → login-LB4TMM4T.js} +4 -4
  80. package/dist-cli/chunks/{logout-6F6UKUNI.js → logout-33IOBD4X.js} +2 -2
  81. package/dist-cli/chunks/{maestro-FMPKHEMX.js → maestro-EEVXX536.js} +3 -3
  82. package/dist-cli/chunks/{maestro-generate-QGPGNXI6.js → maestro-generate-ICPWVXGA.js} +3 -3
  83. package/dist-cli/chunks/{mode-JSNMAHDZ.js → mode-6IXOZDK3.js} +2 -2
  84. package/dist-cli/chunks/{optional-demo-registry-K4VERLJV.js → optional-demo-registry-KBMC7SNI.js} +2 -2
  85. package/dist-cli/chunks/{perf-PO7ARUTG.js → perf-CTTJSAIJ.js} +2 -2
  86. package/dist-cli/chunks/{permissions-XI2QUAEZ.js → permissions-BFQS4DIY.js} +2 -2
  87. package/dist-cli/chunks/{record-4CJCJAFD.js → record-MJR5ELHE.js} +3 -3
  88. package/dist-cli/chunks/report-issue-FMJRNDKH.js +29 -0
  89. package/dist-cli/chunks/runtime-PV4HH2OB.js +4 -0
  90. package/dist-cli/chunks/{screenshot-command-NDVULWMZ.js → screenshot-command-MGNASAXR.js} +3 -3
  91. package/dist-cli/chunks/{screenshots-capture-Q7UTXF3V.js → screenshots-capture-N6PANAVT.js} +2 -2
  92. package/dist-cli/chunks/serve-RS6TIO7U.js +44 -0
  93. package/dist-cli/chunks/setup-MFYGYR2F.js +32 -0
  94. package/dist-cli/chunks/{skills-NFG6VAO5.js → skills-TD35LDKK.js} +2 -2
  95. package/dist-cli/chunks/{storage-POWCOUCQ.js → storage-VCFXDJNO.js} +2 -2
  96. package/dist-cli/chunks/store-VNSTKYBQ.js +4 -0
  97. package/dist-cli/chunks/telemetry-GPNHSYFF.js +4 -0
  98. package/dist-cli/chunks/{timeline-MNHZCVAL.js → timeline-UJCZAWJV.js} +2 -2
  99. package/dist-cli/chunks/upgrade-S24NH4YG.js +7 -0
  100. package/dist-cli/chunks/upload-CCKAO2M5.js +4 -0
  101. package/dist-cli/chunks/version-IVPWT2ZQ.js +6 -0
  102. package/dist-cli/chunks/web-6BMWAWJL.js +4 -0
  103. package/dist-cli/chunks/{what-happened-H74HHGRK.js → what-happened-HRHB6ETL.js} +2 -2
  104. package/dist-lib/agent-daemon-client.cjs +2 -1
  105. package/dist-lib/agent-events.cjs +1 -1
  106. package/dist-lib/agent-identity.cjs +1 -1
  107. package/dist-lib/agent-sessions.cjs +1 -1
  108. package/dist-lib/attached-projects.cjs +1 -1
  109. package/dist-lib/auth/shared-session.cjs +1 -1
  110. package/dist-lib/backend-origin.cjs +1 -1
  111. package/dist-lib/beta.cjs +1 -1
  112. package/dist-lib/beta.mjs +1 -1
  113. package/dist-lib/bridge-constants.cjs +4 -1
  114. package/dist-lib/cli-constants.cjs +1 -1
  115. package/dist-lib/config.cjs +3 -2
  116. package/dist-lib/detox/index.cjs +1 -1
  117. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  118. package/dist-lib/home-paths.cjs +41 -14
  119. package/dist-lib/host/bridge-host.cjs +216 -84
  120. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  121. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  122. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  123. package/dist-lib/host/websocket-proxy.cjs +1 -1
  124. package/dist-lib/index.cjs +5 -2
  125. package/dist-lib/jump-to-source-babel.cjs +1 -1
  126. package/dist-lib/menu.cjs +2 -3
  127. package/dist-lib/menu.mjs +2 -3
  128. package/dist-lib/metro.cjs +1 -1
  129. package/dist-lib/profiles.cjs +1 -1
  130. package/dist-lib/render-mode.cjs +1 -1
  131. package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
  132. package/dist-lib/sdk.cjs +1 -1
  133. package/dist-lib/sdk.mjs +1 -1
  134. package/dist-lib/skills.cjs +373 -159
  135. package/dist-lib/vite.cjs +3 -2
  136. package/package.json +2 -1
  137. package/scripts/postinstall.cjs +2 -78
  138. package/skills/contrast/SKILL.md +3 -2
  139. package/skills/sootsim-setup/SKILL.md +25 -0
  140. package/src/bridge-constants.ts +1 -0
  141. package/src/config.ts +4 -0
  142. package/src/disk-cleanup.ts +5 -26
  143. package/src/home-paths.ts +58 -23
  144. package/src/host/bridge-host.ts +152 -57
  145. package/src/index.ts +4 -1
  146. package/src/menu.ts +0 -1
  147. package/src/runtime-assets.ts +35 -0
  148. package/src/runtime-delivery.ts +5 -8
  149. package/src/skills/builtin/compat-check.ts +32 -65
  150. package/dist-cli/chunks/app-fonts-IMYBO6AQ.js +0 -4
  151. package/dist-cli/chunks/chunk-37ZXWXM3.js +0 -4
  152. package/dist-cli/chunks/chunk-3F2GAJ4Z.js +0 -6
  153. package/dist-cli/chunks/chunk-5S3GPTX7.js +0 -6
  154. package/dist-cli/chunks/chunk-6YN3IAWJ.js +0 -4
  155. package/dist-cli/chunks/chunk-7Q77LTPT.js +0 -15
  156. package/dist-cli/chunks/chunk-BBVBMGTT.js +0 -31
  157. package/dist-cli/chunks/chunk-CTMPC5DN.js +0 -6
  158. package/dist-cli/chunks/chunk-JFWEDISV.js +0 -120
  159. package/dist-cli/chunks/chunk-JGGXQHXH.js +0 -10
  160. package/dist-cli/chunks/chunk-OKSGM6IG.js +0 -4
  161. package/dist-cli/chunks/chunk-SPQKJDIK.js +0 -4
  162. package/dist-cli/chunks/chunk-W44PDEQH.js +0 -4
  163. package/dist-cli/chunks/chunk-WQL3BLFD.js +0 -9
  164. package/dist-cli/chunks/chunk-XYFP466E.js +0 -12
  165. package/dist-cli/chunks/chunk-ZFMJFX27.js +0 -4
  166. package/dist-cli/chunks/cleanup-74SKOD2P.js +0 -25
  167. package/dist-cli/chunks/compat-ME6HXEEU.js +0 -35
  168. package/dist-cli/chunks/config-PM67EWT3.js +0 -13
  169. package/dist-cli/chunks/control-HV7GZXAH.js +0 -4
  170. package/dist-cli/chunks/daemon-LKRHF5OK.js +0 -4
  171. package/dist-cli/chunks/disk-cleanup-DDCFGFWK.js +0 -4
  172. package/dist-cli/chunks/drivers-C2SH3DWS.js +0 -4
  173. package/dist-cli/chunks/flow-E64G5LF3.js +0 -4
  174. package/dist-cli/chunks/help-4ZSRG4F4.js +0 -4
  175. package/dist-cli/chunks/home-paths-5L7N2FLS.js +0 -4
  176. package/dist-cli/chunks/inspect-U2SV25ER.js +0 -4
  177. package/dist-cli/chunks/install-desktop-LTGVT7ID.js +0 -4
  178. package/dist-cli/chunks/runtime-SN2RWDES.js +0 -4
  179. package/dist-cli/chunks/serve-UEUIBHCK.js +0 -44
  180. package/dist-cli/chunks/setup-ZEEWYK4B.js +0 -99
  181. package/dist-cli/chunks/store-BJK7V7FL.js +0 -4
  182. package/dist-cli/chunks/telemetry-EJP5EW7K.js +0 -4
  183. package/dist-cli/chunks/upgrade-X57SIG7L.js +0 -6
  184. package/dist-cli/chunks/upload-JABM3ITZ.js +0 -4
  185. package/dist-cli/chunks/version-V4IKIDX7.js +0 -6
  186. package/dist-cli/chunks/web-F5CLF34I.js +0 -4
@@ -27,11 +27,16 @@ import {
27
27
  writeDevBridgeLockfile,
28
28
  writeRuntimeUpgradeNotice,
29
29
  writeSharedConfig,
30
+ sootsimHomeDir,
30
31
  type DaemonLockfile,
31
32
  type DevBridgeLockfile,
32
33
  type SharedConfig,
33
34
  } from '../home-paths.ts'
34
- import { resolveDevCheckoutRuntimeRoot } from '../runtime-assets.ts'
35
+ import {
36
+ resolveDevCheckoutRuntimeRoot,
37
+ resolveInstalledRuntimeRoot,
38
+ runtimeVersionFromHostname,
39
+ } from '../runtime-assets.ts'
35
40
  import { sootsimRuntime } from '../runtime-delivery.ts'
36
41
  import { AgentHost, type AgentHostOptions } from './agent-host.ts'
37
42
  import {
@@ -616,6 +621,7 @@ export class SootSimBridgeHost {
616
621
  recentActions: [],
617
622
  }
618
623
  this.sims.set(sim.id, sim)
624
+ this.writeConnectedRuntimeSnapshot()
619
625
  if (this.shouldPromoteSim(sim)) {
620
626
  this.primarySimId = sim.id
621
627
  }
@@ -722,6 +728,7 @@ export class SootSimBridgeHost {
722
728
  sim = null
723
729
  role = 'cli'
724
730
  this.cliIdentityKeyBySocket.set(ws, `ws-${this.nextCliFallbackId++}`)
731
+ this.writeConnectedRuntimeSnapshot()
725
732
  this.broadcastSimAssignments()
726
733
  this.broadcastSimClientStates()
727
734
  }
@@ -736,6 +743,7 @@ export class SootSimBridgeHost {
736
743
  sim.url = registration.url
737
744
  sim.title = registration.title
738
745
  sim.userAgent = registration.userAgent
746
+ this.writeConnectedRuntimeSnapshot()
739
747
  // kind/meta are optional and free-form. only update if the
740
748
  // sender supplied a value so we don't accidentally clear
741
749
  // state on a heartbeat-style re-register.
@@ -779,30 +787,20 @@ export class SootSimBridgeHost {
779
787
  // tab that has the engine config global picks up the change live
780
788
  // — same shape as electron's `config:changed` IPC.
781
789
  if (msg.type === 'bridge:write-shared-config') {
782
- const patch =
783
- msg.patch && typeof msg.patch === 'object'
784
- ? (msg.patch as Partial<SharedConfig>)
785
- : null
786
- if (!patch) return
787
- let next: SharedConfig
790
+ if (!msg.patch || typeof msg.patch !== 'object' || Array.isArray(msg.patch)) {
791
+ return
792
+ }
793
+ const patch = Object.fromEntries(
794
+ Object.entries(msg.patch),
795
+ ) satisfies Partial<SharedConfig>
788
796
  try {
789
- next = writeSharedConfig(patch)
797
+ this.writeAndBroadcastSharedConfig(patch)
790
798
  } catch (err) {
791
799
  process.stderr.write(
792
800
  `sootsim: bridge:write-shared-config failed: ${err instanceof Error ? err.message : String(err)}\n`,
793
801
  )
794
802
  return
795
803
  }
796
- const payload = JSON.stringify({
797
- type: 'bridge:shared-config-changed',
798
- config: next,
799
- })
800
- for (const peer of this.sims.values()) {
801
- if (peer.ws.readyState !== WebSocket.OPEN) continue
802
- try {
803
- peer.ws.send(payload)
804
- } catch {}
805
- }
806
804
  return
807
805
  }
808
806
 
@@ -1177,6 +1175,18 @@ export class SootSimBridgeHost {
1177
1175
 
1178
1176
  private bootstrapping = true
1179
1177
 
1178
+ private connectedRuntimeVersions(): string[] {
1179
+ const versions = new Set<string>()
1180
+ for (const sim of this.sims.values()) {
1181
+ try {
1182
+ const url = new URL(sim.url || sim.origin || 'http://localhost')
1183
+ const version = runtimeVersionFromHostname(url.hostname)
1184
+ if (version) versions.add(version)
1185
+ } catch {}
1186
+ }
1187
+ return [...versions].sort()
1188
+ }
1189
+
1180
1190
  private buildLockfileSnapshot(): DaemonLockfile {
1181
1191
  return {
1182
1192
  schema: 1,
@@ -1186,6 +1196,7 @@ export class SootSimBridgeHost {
1186
1196
  runtimePort: this.effectivePort,
1187
1197
  activeRuntime: this.activeRuntimeVersion,
1188
1198
  activeRuntimeDir: this.activeRuntimeDirPath,
1199
+ servedRuntimes: this.connectedRuntimeVersions(),
1189
1200
  startedAt: this.startedAt,
1190
1201
  heartbeatAt: Date.now(),
1191
1202
  bootstrapping: this.bootstrapping,
@@ -1205,6 +1216,7 @@ export class SootSimBridgeHost {
1205
1216
  startedAt: this.startedAt,
1206
1217
  heartbeatAt: Date.now(),
1207
1218
  source: 'vite-dev',
1219
+ servedRuntimes: this.connectedRuntimeVersions(),
1208
1220
  }
1209
1221
  }
1210
1222
 
@@ -1216,6 +1228,19 @@ export class SootSimBridgeHost {
1216
1228
  writeDevBridgeLockfile(this.buildDevLockfileSnapshot())
1217
1229
  }
1218
1230
 
1231
+ private writeConnectedRuntimeSnapshot() {
1232
+ if (this.shouldWriteLockfile && this.httpServer) {
1233
+ try {
1234
+ this.writeLockfileSnapshot()
1235
+ } catch {}
1236
+ }
1237
+ if (this.shouldWriteDevLockfile && this.httpServer) {
1238
+ try {
1239
+ this.writeDevLockfileSnapshot()
1240
+ } catch {}
1241
+ }
1242
+ }
1243
+
1219
1244
  private refreshActiveRuntime() {
1220
1245
  this.activeRuntimeVersion = readActiveRuntime()
1221
1246
  // mode boundary (see runtime-assets resolveDevCheckoutRuntimeRoot): inside
@@ -1280,12 +1305,13 @@ export class SootSimBridgeHost {
1280
1305
 
1281
1306
  private startRuntimeUpdater() {
1282
1307
  if (!this.shouldWriteLockfile || this.runtimeUpdateTimer) return
1308
+ if (fs.existsSync(path.join(sootsimHomeDir(), 'runtime-update-disabled'))) return
1283
1309
  // an isolated dev daemon (explicit SOOTSIM_HOME, dev checkout, nothing
1284
1310
  // staged) serves the repo's public/sootsim build. auto-installing the
1285
1311
  // published runtime here would stage it active and shadow that dev build
1286
1312
  // (runtime-assets prefers a staged runtime under an explicit home), so the
1287
1313
  // daemon would silently validate published code instead of the checkout.
1288
- // a home where CI/local-publish already staged a runtime keeps updating.
1314
+ // local-publish writes the marker above when it stages a source build.
1289
1315
  if (process.env.SOOTSIM_HOME && isSootsimDevCheckout() && !readActiveRuntime()) {
1290
1316
  return
1291
1317
  }
@@ -1304,7 +1330,9 @@ export class SootSimBridgeHost {
1304
1330
  if (reason === 'startup') {
1305
1331
  process.stderr.write('sootsim: checking for runtime updates…\n')
1306
1332
  }
1307
- const result = await sootsimRuntime.updateToLatest()
1333
+ const result = await sootsimRuntime.updateToLatest({
1334
+ protectVersions: this.connectedRuntimeVersions(),
1335
+ })
1308
1336
  if (!result.updated || !result.latestVersion) {
1309
1337
  if (reason === 'startup') {
1310
1338
  process.stderr.write(
@@ -1371,6 +1399,10 @@ export class SootSimBridgeHost {
1371
1399
  runtimeDir: this.activeRuntimeDirPath,
1372
1400
  })
1373
1401
  for (const sim of this.sims.values()) {
1402
+ try {
1403
+ const simUrl = new URL(sim.url || sim.origin || 'http://localhost')
1404
+ if (runtimeVersionFromHostname(simUrl.hostname)) continue
1405
+ } catch {}
1374
1406
  if (sim.ws.readyState === WebSocket.OPEN) {
1375
1407
  try {
1376
1408
  sim.ws.send(payload)
@@ -1434,17 +1466,85 @@ export class SootSimBridgeHost {
1434
1466
  return
1435
1467
  }
1436
1468
 
1437
- // only accept read methods. anything else returns 405 — we never
1438
- // mutate via http; the WS channel owns all writes.
1439
1469
  const method = (req.method || 'GET').toUpperCase()
1470
+ let url: URL
1471
+ try {
1472
+ url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
1473
+ } catch {
1474
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
1475
+ res.end('invalid request URL or host')
1476
+ return
1477
+ }
1478
+
1479
+ // /__sootsim/shared-config is the one awaited HTTP write. ordinary
1480
+ // settings fan out over WS, but destructive reloads need confirmation
1481
+ // that the disk source of truth accepted their final reset first.
1482
+ if (url.pathname === '/__sootsim/shared-config') {
1483
+ res.setHeader('Access-Control-Allow-Origin', '*')
1484
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
1485
+ res.setHeader('Cache-Control', 'no-store')
1486
+ if (method === 'OPTIONS') {
1487
+ res.writeHead(204, { Allow: 'GET, HEAD, POST, OPTIONS' })
1488
+ res.end()
1489
+ return
1490
+ }
1491
+ if (method === 'GET' || method === 'HEAD') {
1492
+ let body = '{}'
1493
+ try {
1494
+ body = JSON.stringify(readSharedConfig())
1495
+ } catch {}
1496
+ res.writeHead(200, { 'Content-Type': 'application/json' })
1497
+ if (method === 'HEAD') res.end()
1498
+ else res.end(body)
1499
+ return
1500
+ }
1501
+ if (method === 'POST') {
1502
+ void (async () => {
1503
+ try {
1504
+ const chunks: Buffer[] = []
1505
+ let size = 0
1506
+ for await (const chunk of req) {
1507
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
1508
+ size += buffer.byteLength
1509
+ if (size > 8 * 1024 * 1024) {
1510
+ res.writeHead(413, { 'Content-Type': 'text/plain; charset=utf-8' })
1511
+ res.end('shared config patch is too large')
1512
+ return
1513
+ }
1514
+ chunks.push(buffer)
1515
+ }
1516
+ const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
1517
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1518
+ throw new Error('shared config patch must be an object')
1519
+ }
1520
+ const patch = Object.fromEntries(
1521
+ Object.entries(parsed),
1522
+ ) satisfies Partial<SharedConfig>
1523
+ const next = this.writeAndBroadcastSharedConfig(patch)
1524
+ res.writeHead(200, { 'Content-Type': 'application/json' })
1525
+ res.end(JSON.stringify(next))
1526
+ } catch (error) {
1527
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
1528
+ res.end(
1529
+ `invalid shared config patch: ${error instanceof Error ? error.message : String(error)}`,
1530
+ )
1531
+ }
1532
+ })()
1533
+ return
1534
+ }
1535
+ res.writeHead(405, { Allow: 'GET, HEAD, POST, OPTIONS' })
1536
+ res.end('method not allowed')
1537
+ return
1538
+ }
1539
+
1540
+ // every other daemon route is read-only unless its owning proxy handled
1541
+ // the mutation above.
1440
1542
  if (method !== 'GET' && method !== 'HEAD') {
1441
1543
  res.writeHead(405, { Allow: 'GET, HEAD' })
1442
1544
  res.end('method not allowed')
1443
1545
  return
1444
1546
  }
1445
1547
 
1446
- const url = new URL(req.url || '/', 'http://localhost')
1447
-
1448
1548
  // /__bundle-proxy?url=<encoded> — runtime/worker fetches metro bundles +
1449
1549
  // assets through here. needed because workers can't directly fetch a
1450
1550
  // cross-origin URL without CORS headers, and metro doesn't set them.
@@ -1544,42 +1644,21 @@ export class SootSimBridgeHost {
1544
1644
  return
1545
1645
  }
1546
1646
 
1547
- // /__sootsim/shared-config — bridge over ~/.sootsim/config.json for
1548
- // browser tabs that have no other path to disk (private windows,
1549
- // statically hosted runtime). GET returns the parsed config; POST
1550
- // merges a partial patch via writeSharedConfig (same shallow-merge
1551
- // semantics used by electron). matches the shell dev server's
1552
- // route at the same path so the engine's persistence layer can use
1553
- // one url regardless of host. cors-permissive because the runtime
1554
- // and the page may have different origins (electron loads from
1555
- // bridgePort; demo apps may iframe across origins).
1556
- if (url.pathname === '/__sootsim/shared-config') {
1557
- res.setHeader('Access-Control-Allow-Origin', '*')
1558
- res.setHeader('Cache-Control', 'no-store')
1559
- if (method === 'GET' || method === 'HEAD') {
1560
- let body = '{}'
1561
- try {
1562
- body = JSON.stringify(readSharedConfig())
1563
- } catch {}
1564
- res.writeHead(200, { 'Content-Type': 'application/json' })
1565
- if (method === 'HEAD') res.end()
1566
- else res.end(body)
1567
- return
1568
- }
1569
- res.writeHead(405, { Allow: 'GET, HEAD' })
1570
- res.end('method not allowed (use the bridge over WS for writes)')
1571
- return
1572
- }
1573
-
1574
- // runtime may have been swapped on disk (sootsim runtime use via file
1575
- // write, or another cli process) — re-read per-request so we serve
1576
- // the current active version. cost is one readFileSync + one statSync.
1577
- this.refreshActiveRuntime()
1578
- const baseDir = this.activeRuntimeDirPath
1647
+ // a repo-selected app runtime opens on its own versioned localhost origin. this
1648
+ // keeps root-relative runtime assets on the same version without changing
1649
+ // the machine's default runtime or interfering with another app. ordinary
1650
+ // localhost requests continue to follow the active stable runtime.
1651
+ const requestedRuntime = runtimeVersionFromHostname(url.hostname)
1652
+ if (!requestedRuntime) this.refreshActiveRuntime()
1653
+ const baseDir = requestedRuntime
1654
+ ? resolveInstalledRuntimeRoot(requestedRuntime)
1655
+ : this.activeRuntimeDirPath
1579
1656
  if (!baseDir) {
1580
1657
  res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' })
1581
1658
  res.end(
1582
- 'sootsim: no active runtime installed. run `sootsim runtime install` to fetch one.',
1659
+ requestedRuntime
1660
+ ? `sootsim: runtime ${requestedRuntime} is not installed. run \`sootsim open\` from that app again.`
1661
+ : 'sootsim: no active runtime installed. run `sootsim runtime install` to fetch one.',
1583
1662
  )
1584
1663
  return
1585
1664
  }
@@ -2333,6 +2412,21 @@ export class SootSimBridgeHost {
2333
2412
  }
2334
2413
  }
2335
2414
 
2415
+ private writeAndBroadcastSharedConfig(patch: Partial<SharedConfig>): SharedConfig {
2416
+ const next = writeSharedConfig(patch)
2417
+ const payload = JSON.stringify({
2418
+ type: 'bridge:shared-config-changed',
2419
+ config: next,
2420
+ })
2421
+ for (const peer of this.sims.values()) {
2422
+ if (peer.ws.readyState !== WebSocket.OPEN) continue
2423
+ try {
2424
+ peer.ws.send(payload)
2425
+ } catch {}
2426
+ }
2427
+ return next
2428
+ }
2429
+
2336
2430
  private broadcastSimClientStates() {
2337
2431
  for (const sim of this.sims.values()) {
2338
2432
  if (sim.ws.readyState !== WebSocket.OPEN) continue
@@ -2488,6 +2582,7 @@ export class SootSimBridgeHost {
2488
2582
  expiresAt: Date.now() + SootSimBridgeHost.SIM_RECONNECT_TTL_MS,
2489
2583
  })
2490
2584
  this.sims.delete(sim.id)
2585
+ this.writeConnectedRuntimeSnapshot()
2491
2586
  }
2492
2587
 
2493
2588
  private getRestorableSimState(simId: string): BridgeRestorableSimState | null {
package/src/index.ts CHANGED
@@ -6,5 +6,8 @@
6
6
  export { sootsimPlugin } from './vite-plugin-one'
7
7
  export { default as metroPlugin } from './metro-plugin'
8
8
  export { default as jumpToSourceBabelPlugin } from './jump-to-source-babel'
9
- export { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants'
9
+ export {
10
+ DEFAULT_SOOTSIM_BRIDGE_PORT,
11
+ DEFAULT_SOOTSIM_BRIDGE_URL,
12
+ } from './bridge-constants'
10
13
  export { DEFAULT_SOOTSIM_SHELL_URL } from './cli-constants'
package/src/menu.ts CHANGED
@@ -493,7 +493,6 @@ function helpMenu(): SootSimMenuDef {
493
493
  action: 'open-devtools-report',
494
494
  showInShortcutDocs: true,
495
495
  },
496
- { label: 'Show Onboarding', action: 'open-devtools-onboard' },
497
496
  ],
498
497
  }
499
498
  }
@@ -5,6 +5,7 @@
5
5
  import fs from 'fs'
6
6
  import path from 'path'
7
7
  import { fileURLToPath } from 'url'
8
+ import { isValidRuntimeVersion } from '@contrast/runtime-delivery'
8
9
  import { readActiveRuntime, runtimeDir } from './home-paths'
9
10
 
10
11
  export const SOOTSIM_RUNTIME_MISSING_MESSAGE =
@@ -124,6 +125,40 @@ export function resolveActiveRuntimeRoot(): string | null {
124
125
  return dir
125
126
  }
126
127
 
128
+ export function runtimeVersionHostname(version: string): string {
129
+ const trimmed = version.trim()
130
+ if (!isValidRuntimeVersion(trimmed)) {
131
+ throw new Error(`invalid SootSim runtime version: ${version}`)
132
+ }
133
+ const encoded = Buffer.from(trimmed, 'utf8').toString('hex')
134
+ const labels = encoded.match(/.{1,60}/g)
135
+ if (!labels) throw new Error(`invalid SootSim runtime version: ${version}`)
136
+ return `${labels.join('.')}.runtime.localhost`
137
+ }
138
+
139
+ export function runtimeVersionFromHostname(hostname: string): string | null {
140
+ const suffix = '.runtime.localhost'
141
+ if (!hostname.toLowerCase().endsWith(suffix)) return null
142
+ const labels = hostname.slice(0, -suffix.length).split('.')
143
+ if (labels.length === 0 || labels.some((label) => !/^[0-9a-f]{1,60}$/i.test(label))) {
144
+ return null
145
+ }
146
+ const encoded = labels.join('')
147
+ if (encoded.length % 2 !== 0) return null
148
+ try {
149
+ const version = Buffer.from(encoded, 'hex').toString('utf8')
150
+ return isValidRuntimeVersion(version) ? version : null
151
+ } catch {
152
+ return null
153
+ }
154
+ }
155
+
156
+ export function resolveInstalledRuntimeRoot(version: string): string | null {
157
+ const dir = runtimeDir(version)
158
+ if (!fs.existsSync(path.join(dir, 'index.html'))) return null
159
+ return dir
160
+ }
161
+
127
162
  export function isRootRuntimeAssetPath(pathname: string): boolean {
128
163
  return ROOT_RUNTIME_PATHS.some((p) => pathname === p || pathname.startsWith(p))
129
164
  }
@@ -1,8 +1,8 @@
1
1
  // sootsim's binding of the shared runtime-delivery machinery.
2
2
  //
3
3
  // the machinery itself (hosted manifest, sha256-verified tarballs, channels,
4
- // pinning, auto-update) lives in @contrast/runtime-delivery and knows nothing
5
- // about sootsim. this file supplies sootsim's identity: ~/.sootsim, the
4
+ // channel resolution, and auto-update) lives in @contrast/runtime-delivery and
5
+ // knows nothing about sootsim. this file supplies sootsim's identity: ~/.sootsim, the
6
6
  // SOOTSIM_* env overrides, and the contrast.dev/runtimes hosting layout.
7
7
  //
8
8
  // @contrast/runtime-delivery is an internal workspace package that is never
@@ -23,12 +23,10 @@ import {
23
23
 
24
24
  export const DEFAULT_RUNTIME_CDN_ORIGIN = 'https://contrast.dev'
25
25
 
26
- /** website URL where the sootsim changelog is rendered. the CLI used to drain
27
- * a paginated /api/changelog/sootsim endpoint here; the changelog is now a
28
- * static page checked into the Contrast repo (one MDX file per runtime release
29
- * under src/features/site/changelog/sootsim-entries/), so `sootsim runtime
30
- * notes` points users at the page. */
26
+ /** the official curated changelog and the machine-readable release feed. the
27
+ * SootSim site is static, so its /api traffic is owned by contrast.dev. */
31
28
  export const SOOTSIM_CHANGELOG_URL = 'https://sootsim.com/changelog'
29
+ export const SOOTSIM_CHANGELOG_API_URL = 'https://contrast.dev/api/changelog/sootsim'
32
30
 
33
31
  export const SOOTSIM_RUNTIME_PRODUCT: RuntimeProduct = {
34
32
  id: 'sootsim',
@@ -42,7 +40,6 @@ export const SOOTSIM_RUNTIME_PRODUCT: RuntimeProduct = {
42
40
  env: {
43
41
  cdnOrigin: 'SOOTSIM_CDN_ORIGIN',
44
42
  channel: 'SOOTSIM_RUNTIME_CHANNEL',
45
- pin: 'SOOTSIM_RUNTIME_VERSION',
46
43
  },
47
44
  ensureHome: ensureSootsimHome,
48
45
  configFile: configFilePath,
@@ -2,15 +2,6 @@
2
2
 
3
3
  import type { SootSimSkill } from '../types'
4
4
 
5
- function getCompatStatus(entry: {
6
- stubType: 'native' | 'works' | 'build-only'
7
- versions: Array<{ coverage: number }>
8
- }): 'full' | 'partial' | 'auto-stub' {
9
- if (entry.stubType === 'works') return 'full'
10
- if (entry.stubType === 'build-only') return 'full'
11
- return entry.versions.some((version) => version.coverage >= 1) ? 'full' : 'auto-stub'
12
- }
13
-
14
5
  export const skill: SootSimSkill = {
15
6
  name: 'compat-check',
16
7
  description: 'Check package compatibility with sootsim',
@@ -28,77 +19,53 @@ export const skill: SootSimSkill = {
28
19
  },
29
20
  },
30
21
  async execute(params, context) {
31
- const fs = await import('fs')
32
- const path = await import('path')
33
-
34
- const { POLYFILL_REGISTRY } = await import('../../../../compat/src/web.ts')
35
-
36
- if (params.packageName) {
37
- const entry = POLYFILL_REGISTRY[params.packageName]
38
- if (!entry) {
39
- return {
40
- success: true,
41
- message: `${params.packageName}: not in registry (may be auto-stubbed if native)`,
42
- data: { package: params.packageName, status: 'unknown' },
43
- }
44
- }
45
- const status = getCompatStatus(entry)
46
- return {
47
- success: true,
48
- message: `${params.packageName}: ${status}`,
49
- data: { package: params.packageName, status, ...entry },
50
- }
51
- }
22
+ const fs = await import('node:fs')
23
+ const path = await import('node:path')
24
+ const { COMPAT_RESULT_CAVEAT, formatPackageStatus, formatScanCounts, scanDeps } =
25
+ await import('../../../../compat/src/web.ts')
52
26
 
53
- // scan project deps
54
- const pkgPath = path.join(context.projectDir, 'package.json')
55
- if (!fs.existsSync(pkgPath)) {
27
+ const packagePath = path.join(context.projectDir, 'package.json')
28
+ if (!fs.existsSync(packagePath)) {
56
29
  return { success: false, message: 'No package.json found' }
57
30
  }
58
-
59
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
60
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
61
-
62
- const results: Record<string, string[]> = {
63
- full: [],
64
- partial: [],
65
- 'auto-stub': [],
66
- unsupported: [],
67
- unknown: [],
31
+ const parsed: unknown = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
32
+ if (!parsed || typeof parsed !== 'object') {
33
+ return { success: false, message: 'package.json is not an object' }
68
34
  }
69
35
 
70
- for (const dep of Object.keys(allDeps)) {
71
- const entry = POLYFILL_REGISTRY[dep]
72
- if (entry) {
73
- const status = getCompatStatus(entry)
74
- if (results[status]) results[status].push(dep)
75
- } else if (isNative(dep)) {
76
- results.unknown.push(dep)
77
- }
78
- }
36
+ const dependencies = readDependencies(parsed)
37
+ const scan = params.packageName
38
+ ? scanDeps({
39
+ dependencies: {
40
+ [params.packageName]: dependencies[params.packageName] ?? '*',
41
+ },
42
+ })
43
+ : scanDeps(parsed)
44
+ const packageResult = params.packageName ? scan.packages[0] : undefined
45
+ const message = packageResult
46
+ ? `${params.packageName}: ${formatPackageStatus(packageResult)}. ${COMPAT_RESULT_CAVEAT}`
47
+ : `${formatScanCounts(scan)}. ${COMPAT_RESULT_CAVEAT}`
79
48
 
80
49
  const reportPath = path.join(context.outputDir, 'compat-report.json')
81
50
  fs.mkdirSync(path.dirname(reportPath), { recursive: true })
82
- fs.writeFileSync(reportPath, JSON.stringify(results, null, 2))
51
+ fs.writeFileSync(reportPath, `${JSON.stringify(scan, null, 2)}\n`)
83
52
 
84
53
  return {
85
- success: results.unsupported.length === 0,
86
- message: `${results.full.length} full, ${results.partial.length} partial, ${results.unsupported.length} unsupported, ${results.unknown.length} unknown`,
54
+ success: true,
55
+ message,
87
56
  artifacts: [{ type: 'report', name: 'compat-report', path: reportPath }],
88
- data: results,
57
+ data: scan,
89
58
  }
90
59
  },
91
60
  },
92
61
  ],
93
62
  }
94
63
 
95
- function isNative(name: string): boolean {
96
- return (
97
- name.startsWith('expo-') ||
98
- name.startsWith('react-native-') ||
99
- name.startsWith('@react-native/') ||
100
- name.startsWith('@react-native-community/') ||
101
- name.startsWith('@expo/') ||
102
- name === 'expo'
103
- )
64
+ function readDependencies(value: object): Record<string, string> {
65
+ const dependencies = Reflect.get(value, 'dependencies')
66
+ const devDependencies = Reflect.get(value, 'devDependencies')
67
+ return {
68
+ ...(dependencies && typeof dependencies === 'object' ? dependencies : {}),
69
+ ...(devDependencies && typeof devDependencies === 'object' ? devDependencies : {}),
70
+ }
104
71
  }
@@ -1,4 +0,0 @@
1
- /*! sootsim v0.1.294 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import { createRequire as __sootsimCreateRequire } from 'node:module'
3
- var require = __sootsimCreateRequire(import.meta.url)
4
- import{d as p}from"./chunk-37ZXWXM3.js";import"./chunk-D2R7PDN5.js";import{existsSync as h,mkdirSync as d,readFileSync as y,writeFileSync as g}from"node:fs";import{join as m}from"node:path";import{gzipSync as w}from"node:zlib";function f(t,e){let o=t.indexOf(`--${e}`);if(o!==-1&&o+1<t.length)return t[o+1];let i=t.find(r=>r.startsWith(`--${e}=`));return i?i.slice(e.length+3):void 0}async function S(t){let e=f(t,"wire")||"",o=f(t,"out-dir"),i=f(t,"manifest");if((!o||!i)&&(console.error(" usage: sootsim app-fonts stage --wire <wire> --out-dir <dir> --manifest <path>"),process.exit(1)),!e.trim()){console.log("no app fonts declared \u2014 nothing to stage");return}d(o,{recursive:!0});let r=await p(e,{onStaged:({url:n,byteLength:s})=>console.log(` staged app font: ${n} (${(s/1024).toFixed(1)} KiB)`),onError:(n,s)=>console.error(` warning: failed to fetch app font ${n}: ${s instanceof Error?s.message:s}`)}),a=[];for(let n of r){let s=w(n.bytes);g(m(o,n.urlhash),s),a.push({url:n.url,urlhash:n.urlhash,contentType:n.contentType,encoding:"gzip",sizeBytes:s.length,rawBytes:n.bytes.byteLength})}let c=[];if(h(i))try{let n=JSON.parse(y(i,"utf8"));Array.isArray(n)&&(c=n)}catch{}let u=new Set(c.map(n=>n.urlhash)),l=[...c,...a.filter(n=>!u.has(n.urlhash))];g(i,JSON.stringify(l)),console.log(`staged ${a.length} app font(s); manifest now has ${l.length} file(s)`)}async function E(t){let e=t[0];if(e==="stage"){await S(t.slice(1));return}console.error(` unknown app-fonts subcommand: ${e??"(none)"}`),console.error(" available: stage"),process.exit(1)}export{E as runAppFonts};
@@ -1,4 +0,0 @@
1
- /*! sootsim v0.1.294 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import { createRequire as __sootsimCreateRequire } from 'node:module'
3
- var require = __sootsimCreateRequire(import.meta.url)
4
- import{createHash as U}from"node:crypto";import w from"node:fs";import{createRequire as D}from"node:module";import s from"node:path";var R=/\.(ttf|otf|woff|woff2)$/i;function N(t,e,o){if(!t||typeof t!="object")return;let n=t.fonts;if(Array.isArray(n))for(let i of n)typeof i=="string"&&o.push(i);let r=t[e];if(!r||typeof r!="object")return;let c=r.fonts;if(Array.isArray(c))for(let i of c){if(typeof i=="string"){o.push(i);continue}if(!i||typeof i!="object")continue;let f=i.fontDefinitions;if(Array.isArray(f))for(let y of f){if(!y||typeof y!="object")continue;let h=y.path;typeof h=="string"&&o.push(h)}}}function C(t,e){if(!Array.isArray(t))return[];let o=[];for(let n of t){let r=Array.isArray(n)?n[0]:n;if(typeof r!="string"||!/(^|\/)expo-font$/.test(r))continue;let c=Array.isArray(n)?n[1]??{}:{};N(c,e,o)}return o}function L(t){return t.slice(t.lastIndexOf("/")+1).replace(R,"")}function S(t,e){let o=e.replace(/\/+$/,""),n=new Set,r=[];for(let c of t){let i=c.replace(/^\.\//,"").replace(/^\/+/,"");!i||n.has(i)||(n.add(i),R.test(i)&&r.push({family:L(i),url:`${o}/assets/${i}`}))}return r}var k=D(import.meta.url);function _(t){try{let e=new URL(t),o=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(o==="localhost"||o.endsWith(".localhost")||o==="0.0.0.0"||o==="::1"||/^127(?:\.\d{1,3}){3}$/.test(o))return e.origin}catch{}return null}var M=["app.config.js","app.config.ts","app.config.cjs","app.config.mjs","app.json"];function $(t){for(let e of M)try{if(w.statSync(s.join(t,e)).isFile())return!0}catch{}return!1}var W=new Set(["node_modules","artifacts","dist","build","out","coverage","tmp","public","ios","android","macos","windows","__fixtures__","__tests__","examples","example"]);function T(t,e){$(t)&&e.push(t);let o;try{o=w.readdirSync(t,{withFileTypes:!0})}catch{return}for(let n of o)n.isDirectory()&&(n.name.startsWith(".")||W.has(n.name)||T(s.join(t,n.name),e))}function q(t){try{return w.statSync(s.join(t,"package.json")).isFile()}catch{return!1}}function Q(t){let e=_(t.bundleUrl);if(!e)return[];let o=t.repoDir||process.env.CONTRAST_REPO_DIR,n=o?s.resolve(o):process.cwd();if(!o&&!q(n))return[];let r=[];o?(T(n,r),r.includes(n)||r.push(n)):r.push(n);let c=(t.platform||process.env.CONTRAST_PLATFORM||"ios").toLowerCase(),i=null;if(o||$(n))try{let l=k.resolve("@expo/config",{paths:[...r,n]}),p=k(l);typeof p.getConfig=="function"&&(i=p.getConfig)}catch{}let f=n,y=[];if(i)for(let l of r)try{let p=i(l,{skipSDKVersionRequirement:!0});if(p?.exp&&typeof p.exp=="object"){let u=p.exp.plugins;if(Array.isArray(u)){f=l,y=u;break}}}catch{}let h=C(y,c==="android"?"android":"ios"),A=(l,p)=>{let u=[l];for(;u.length>0;){let a=u.pop();if(!a)continue;let g;try{g=w.readdirSync(a,{withFileTypes:!0})}catch{continue}for(let d of g){let m=s.join(a,d.name);if(d.isDirectory()){u.push(m);continue}if(!d.isFile()||!/\.(ttf|otf|woff|woff2)$/i.test(d.name))continue;let F=s.relative(l,m).split(s.sep).join("/");h.push(s.posix.join(p,F))}}};try{let l=JSON.parse(w.readFileSync(s.join(f,"package.json"),"utf8")),p=new Set;for(let a of["dependencies","devDependencies","optionalDependencies"]){let g=Reflect.get(l,a);if(!(!g||typeof g!="object"))for(let d of Object.keys(g))p.add(d)}let u=[...p].filter(a=>a==="react-native-vector-icons"||a.startsWith("@react-native-vector-icons/"));if(u.length>0){let a=Reflect.get(l,"reactNativeVectorIcons"),g=a&&typeof a=="object"?Reflect.get(a,"fontDir"):null,d=typeof g=="string"?g:"rnvi-fonts",m=s.resolve(f,d),F=s.relative(f,m);!F.startsWith("..")&&!s.isAbsolute(F)&&A(m,F.split(s.sep).join("/"));let E=D(s.join(f,"package.json"));for(let x of u)try{let j=E.resolve(x),O=x.split("/").join(s.sep),v=`${s.sep}node_modules${s.sep}${O}${s.sep}`,P=j.lastIndexOf(v);if(P===-1)continue;let I=j.slice(0,P+v.length-1),b=x==="react-native-vector-icons"?"Fonts":"fonts";A(s.join(I,b),s.posix.join("node_modules",x,b))}catch{}}}catch{}return S(h,e)}async function Y(t,e){let o=_(t);if(!o)return null;let n=new AbortController,r=setTimeout(()=>n.abort(),e?.timeoutMs??1500);try{let c=await fetch(`${o}/`,{headers:{"expo-platform":"ios",Accept:"application/json"},signal:n.signal});if(!c.ok)return null;let f=(await c.json())?.extra?.expoClient?._internal?.projectRoot;return typeof f=="string"&&f.length>0?f:null}catch{return null}finally{clearTimeout(r)}}function Z(t){return t.map(e=>`${encodeURIComponent(e.url)}::${encodeURIComponent(e.family)}`).join(",")}function G(t){let e=[];for(let o of t.split(",")){let n=o.trim();if(!n)continue;let r=n.indexOf("::"),c=r===-1?n:n.slice(0,r);try{e.push(decodeURIComponent(c))}catch{e.push(c)}}return e.filter(Boolean)}function J(t){let e=(t.split("?")[0].split(".").pop()||"").toLowerCase();return e==="ttf"?"font/ttf":e==="otf"?"font/otf":e==="woff"?"font/woff":e==="woff2"?"font/woff2":"application/octet-stream"}async function tt(t,e){let o=[],n=new Set;for(let r of G(t)){let c=U("sha256").update(r).digest("hex");if(!n.has(c)){n.add(c);try{let i=await fetch(r);if(!i.ok){e?.onError?.(r,new Error(`${i.status} ${i.statusText}`));continue}let f=new Uint8Array(await i.arrayBuffer());o.push({url:r,urlhash:c,contentType:J(r),bytes:f}),e?.onStaged?.({url:r,byteLength:f.byteLength})}catch(i){e?.onError?.(r,i)}}}return o}export{Q as a,Y as b,Z as c,tt as d};
@@ -1,6 +0,0 @@
1
- /*! sootsim v0.1.294 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import { createRequire as __sootsimCreateRequire } from 'node:module'
3
- var require = __sootsimCreateRequire(import.meta.url)
4
- import{createInterface as i}from"readline";var c=()=>i({input:process.stdin,output:process.stdout});function s(o){return new Promise(n=>{let r=c();r.question(o,t=>{r.close(),n(t.trim())})})}async function u(o,n=!0){let r=n?"[Y/n]":"[y/N]";if(!process.stdin.isTTY)return console.log(` ${o} ${r} ${n?"y":"n"} (non-interactive)`),n;let t=await s(` ${o} ${r} `);return t===""?n:t.toLowerCase().startsWith("y")}async function a(o,n){console.log(`
5
- ${o}
6
- `);for(let e=0;e<n.length;e++)console.log(` ${e+1}. ${n[e]}`);if(console.log(),!process.stdin.isTTY)return console.log(` choose [1-${n.length}]: 1 (non-interactive)`),0;let r=await s(` choose [1-${n.length}]: `),t=parseInt(r,10);return t>=1&&t<=n.length?t-1:0}export{u as a,a as b};
@@ -1,6 +0,0 @@
1
- /*! sootsim v0.1.294 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import { createRequire as __sootsimCreateRequire } from 'node:module'
3
- var require = __sootsimCreateRequire(import.meta.url)
4
- import{d as h}from"./chunk-CTMPC5DN.js";import{b as w}from"./chunk-AO65WEF7.js";import{G as v,K as B,M as C,g as S}from"./chunk-WQL3BLFD.js";import a from"node:fs";import c from"node:path";var g=1,E=`automatic-cleanup-v${g}.json`,L=`automatic-cleanup-v${g}.lock`,P=360*60*1e3;function k(){let i=c.join(S(),E);try{return JSON.parse(a.readFileSync(i,"utf8")).generation===g}catch{return!1}}function M(i){if(i<1024)return`${i} B`;let e=["KiB","MiB","GiB","TiB"],n=i/1024,r=e[0];for(let s=1;s<e.length&&n>=1024;s++)n/=1024,r=e[s];return`${n.toFixed(n>=10?1:2)} ${r}`}function p(i){let e;try{e=a.lstatSync(i)}catch{return 0}let n=e.blocks>0?e.blocks*512:e.size;if(!e.isDirectory()||e.isSymbolicLink())return n;let r=n;try{for(let s of a.readdirSync(i))r+=p(c.join(i,s))}catch{}return r}function R(i){for(let e of["SingletonLock","SingletonSocket","lockfile"])try{if(a.lstatSync(c.join(i,e)))return!0}catch{}return!1}function b(i,e){let n;try{n=a.readdirSync(i,{withFileTypes:!0})}catch{return[]}let r=R(e),s=[];for(let t of n){if(!t.isDirectory()||t.isSymbolicLink())continue;let o=c.join(i,t.name);if(w(t.name)){s.push({path:o,bytes:p(o),kind:"browser-cache",state:r?"in-use":"safe",selected:!r,detail:r?"browser profile is open; close it before cleanup":"disposable browser HTTP, bytecode, or GPU cache"});continue}s.push(...b(o,e))}return s}function U(i){let e=c.join(i,"profiles","playwright"),n;try{n=a.readdirSync(e,{withFileTypes:!0})}catch{return[]}return n.flatMap(r=>{if(!r.isDirectory()||r.isSymbolicLink())return[];let s=c.join(e,r.name);return b(s,s)})}function V(i){let e=c.join(i,"electron","userData");return b(e,e)}function D(i,e){let n=c.join(i,"recordings"),r;try{r=a.readdirSync(n,{withFileTypes:!0})}catch{return[]}return r.map(s=>{let t=c.join(n,s.name);return{path:t,bytes:p(t),kind:"recording",state:"aggressive",selected:e,detail:"user-created recording or captured-frame output"}})}function A(){let i=B();return!C(i)||!i.activeRuntime?[]:[i.activeRuntime]}function x(i){let e=new Set(A()),n=[...e],r=h.planLocalRetention({retainVersions:2,protectVersions:n}),s=h.planLocalRetention({retainVersions:1,protectVersions:n}),t=new Set(r.removeVersions),o=new Set(s.removeVersions),u=new Set([...r.keepVersions,...r.removeVersions,...s.keepVersions,...s.removeVersions]),m=[];for(let l of[...u].sort()){let d=h.product.runtimeDir(l),y="retained",f="retained runtime";e.has(l)&&l!==r.activeVersion&&l!==r.pinnedVersion?(y="in-use",f="runtime is served by the running daemon"):t.has(l)?(y="safe",f="inactive runtime outside the two-version rollback window"):o.has(l)?(y="aggressive",f="most recent rollback runtime"):l===r.activeVersion?f="active runtime; always protected":l===r.pinnedVersion&&(f="pinned runtime; always protected"),m.push({path:d,bytes:p(d),kind:"runtime-version",state:y,selected:y==="safe"||y==="aggressive"&&i,detail:f})}for(let l of r.removeCacheFiles)m.push({path:l,bytes:p(l),kind:"runtime-archive",state:"safe",selected:!0,detail:"downloaded archive is unnecessary after successful extraction"});return{activeRuntime:r.activeVersion,pinnedRuntime:r.pinnedVersion,entries:m}}function T(i={}){let e=i.aggressive===!0,n=S(),r=x(e),s=[...r.entries,...U(n),...V(n),...D(n,e)].sort((t,o)=>o.bytes-t.bytes||t.path.localeCompare(o.path));return{home:n,activeRuntime:r.activeRuntime,pinnedRuntime:r.pinnedRuntime,entries:s,reclaimableBytes:s.reduce((t,o)=>t+(o.selected?o.bytes:0),0),protectedBytes:s.reduce((t,o)=>t+(o.selected?0:o.bytes),0),totalBytes:p(n)}}function I(i,e){let n=c.relative(i,e);if(!n||n.startsWith("..")||c.isAbsolute(n))throw new Error(`refusing cleanup target outside the SootSim home: ${e}`)}function F(i={}){let e=T(i),n=i.aggressive===!0?1:2;h.pruneLocalRetention({retainVersions:n,protectVersions:A()});for(let t of e.entries)if(t.selected&&!(t.kind==="runtime-archive"||t.kind==="runtime-version")){if(t.kind==="browser-cache"){let u=c.relative(e.home,t.path).split(c.sep),m=u[0]==="profiles"&&u[1]==="playwright"&&u[2]?c.join(e.home,"profiles","playwright",u[2]):u[0]==="electron"&&u[1]==="userData"?c.join(e.home,"electron","userData"):null;if(m&&R(m)){t.state="in-use",t.selected=!1,t.detail="browser profile opened while cleanup was starting";continue}}I(e.home,t.path),a.rmSync(t.path,{recursive:!0,force:!0})}let r=e.entries.filter(t=>t.selected&&!a.existsSync(t.path)).map(t=>t.path),s=p(e.home);return{...e,removedPaths:r,reclaimedBytes:Math.max(0,e.totalBytes-s),remainingBytes:s}}function K(){v();let i=S(),e=c.join(i,E),n=c.join(i,L);if(k())return{status:"already-complete",reclaimedBytes:0,remainingBytes:0,deferredInUseBytes:0};let r=null;for(let s=0;s<2&&r===null;s++)try{let t=a.openSync(n,"wx");try{a.writeFileSync(t,`${JSON.stringify({pid:process.pid,startedAt:Date.now()})}
5
- `,"utf8"),r=t}catch(o){a.closeSync(t);try{a.unlinkSync(n)}catch{}throw o}}catch(t){if((t instanceof Error&&"code"in t?t.code:null)!=="EEXIST")throw t;let u=0,m=0,l=null;try{l=a.lstatSync(n);let d=JSON.parse(a.readFileSync(n,"utf8"));u=typeof d.pid=="number"?d.pid:0,m=typeof d.startedAt=="number"?d.startedAt:0}catch{}if(u>0&&m>0&&Date.now()-m<P)try{return process.kill(u,0),{status:"busy",reclaimedBytes:0,remainingBytes:0,deferredInUseBytes:0}}catch{}if(!l)continue;try{let d=a.lstatSync(n);if(d.dev!==l.dev||d.ino!==l.ino)return{status:"busy",reclaimedBytes:0,remainingBytes:0,deferredInUseBytes:0};a.unlinkSync(n)}catch{}}if(r===null)return{status:"busy",reclaimedBytes:0,remainingBytes:0,deferredInUseBytes:0};try{if(k())return{status:"already-complete",reclaimedBytes:0,remainingBytes:0,deferredInUseBytes:0};let s=F(),t=s.entries.reduce((u,m)=>u+(m.state==="in-use"?m.bytes:0),0);if(t>0)return{status:"deferred",reclaimedBytes:s.reclaimedBytes,remainingBytes:s.remainingBytes,deferredInUseBytes:t};let o=`${e}.${process.pid}.tmp`;return a.writeFileSync(o,`${JSON.stringify({generation:g,completedAt:Date.now(),reclaimedBytes:s.reclaimedBytes})}
6
- `,"utf8"),a.renameSync(o,e),{status:"complete",reclaimedBytes:s.reclaimedBytes,remainingBytes:s.remainingBytes,deferredInUseBytes:0}}finally{a.closeSync(r);try{a.unlinkSync(n)}catch{}}}export{k as a,M as b,T as c,F as d,K as e};
@@ -1,4 +0,0 @@
1
- /*! sootsim v0.1.294 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import { createRequire as __sootsimCreateRequire } from 'node:module'
3
- var require = __sootsimCreateRequire(import.meta.url)
4
- import{createHash as D}from"node:crypto";import y from"node:fs";import{createRequire as I}from"node:module";import h from"node:path";var w=/(^|\/)expo-splash-screen$/,R=/(^|\/)react-native-bootsplash$/;function U(n,e){let r=e.replace(/\/+$/,""),o=n.replace(/^\.\//,"").replace(/^\/+/,"");if(o)return`${r}/assets/${o}`}function d(n,e,r){return r?r(n):U(n,e)}function C(n){if(n==="cover")return"cover";if(n==="contain"||n==="native")return"contain"}function p(n){return typeof n=="string"&&n.length>0?n:void 0}function k(n){return typeof n=="number"&&Number.isFinite(n)?n:void 0}function v(n,e,r){if(!n||typeof n!="object")return null;let o=n,s={source:"expo-splash-screen",backgroundColor:p(o.backgroundColor)??"#ffffff"},a=p(o.image);a&&(s.imageUrl=d(a,e,r));let t=k(o.imageWidth);t!=null&&(s.imageWidthPx=t);let c=C(o.resizeMode);c&&(s.resizeMode=c);let l=o.dark;if(l&&typeof l=="object"){let u=l,g=p(u.backgroundColor),f=p(u.image);(g||f)&&(s.dark={},g&&(s.dark.backgroundColor=g),f&&(s.dark.imageUrl=d(f,e,r)))}return s}function j(n,e,r,o){let i=n[e],s=i&&typeof i=="object"?i.splash:void 0,a=n.splash,t=s&&typeof s=="object"?s:a;if(!t||typeof t!="object")return null;let l={source:"expo-legacy",backgroundColor:p(t.backgroundColor)??"#ffffff",imageWidthPx:k(t.imageWidth)??200,resizeMode:C(t.resizeMode)??"contain"},u=p(t.image);u&&(l.imageUrl=d(u,r,o));let g=t.dark;if(g&&typeof g=="object"){let f=g,S=p(f.backgroundColor),m=p(f.image);(S||m)&&(l.dark={},S&&(l.dark.backgroundColor=S),m&&(l.dark.imageUrl=d(m,r,o)))}return l}function _(n,e,r){if(!n||typeof n!="object")return null;let o=n,i=p(o.logo);if(!i)return null;let s={source:"react-native-bootsplash",backgroundColor:p(o.background)??"#fff",imageUrl:d(i,e,r),imageWidthPx:k(o.logoWidth)??100,resizeMode:"contain"},a=p(o.darkBackground),t=p(o.darkLogo);return(a||t)&&(s.dark={},a&&(s.dark.backgroundColor=a),t&&(s.dark.imageUrl=d(t,e,r))),s}function b(n,e,r,o={}){if(!n||typeof n!="object")return null;let i=n,s=i.plugins,a=null,t=null;if(Array.isArray(s))for(let c of s){let l=Array.isArray(c)?c[0]:c;if(typeof l!="string")continue;let u=Array.isArray(c)?c[1]??{}:{};!a&&w.test(l)?a=v(u,r,o.resolveAssetUrl):!t&&R.test(l)&&(t=_(u,r,o.resolveAssetUrl))}return t||a||j(i,e,r,o.resolveAssetUrl)}var x=I(import.meta.url);function E(n){try{let e=new URL(n),r=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(r==="localhost"||r.endsWith(".localhost")||r==="0.0.0.0"||r==="::1"||/^127(?:\.\d{1,3}){3}$/.test(r))return e.origin}catch{}return null}var L=["app.config.js","app.config.ts","app.config.cjs","app.config.mjs","app.json"];function P(n){for(let e of L)try{if(y.statSync(h.join(n,e)).isFile())return!0}catch{}return!1}var F=new Set(["node_modules","artifacts","dist","build","out","coverage","tmp","public","ios","android","macos","windows","__fixtures__","__tests__","examples","example"]);function A(n,e){P(n)&&e.push(n);let r;try{r=y.readdirSync(n,{withFileTypes:!0})}catch{return}for(let o of r)o.isDirectory()&&(o.name.startsWith(".")||F.has(o.name)||A(h.join(n,o.name),e))}function M(n){try{return y.statSync(h.join(n,"package.json")).isFile()}catch{return!1}}function H(n){let e=E(n.bundleUrl);if(!e)return null;let r=n.repoDir||process.env.CONTRAST_REPO_DIR,o=r?h.resolve(r):process.cwd();if(!r&&!M(o))return null;let i=[];if(r)A(o,i),i.includes(o)||i.push(o);else{if(!P(o))return null;i.push(o)}if(i.length===0)return null;let s=(n.platform||process.env.CONTRAST_PLATFORM||"ios").toLowerCase(),a=null;try{let c=x.resolve("@expo/config",{paths:[...i,o]}),l=x(c);typeof l.getConfig=="function"&&(a=l.getConfig)}catch{}if(!a)return null;let t;for(let c of i)try{let l=a(c,{skipSDKVersionRequirement:!0});if(l?.exp&&typeof l.exp=="object"){t=l.exp;break}}catch{}return!t||typeof t!="object"?null:b(t,s==="android"?"android":"ios",e)}function J(n){return encodeURIComponent(JSON.stringify(n))}function z(n){if(!n)return null;try{let e=JSON.parse(decodeURIComponent(n));if(!e||typeof e!="object")return null;let r=e;return r.source!=="expo-splash-screen"&&r.source!=="expo-legacy"&&r.source!=="react-native-bootsplash"||typeof r.backgroundColor!="string"?null:e}catch{return null}}function W(n){let e=new Set;return n.imageUrl&&e.add(n.imageUrl),n.dark?.imageUrl&&e.add(n.dark.imageUrl),[...e]}function N(n){let e=(n.split("?")[0].split(".").pop()||"").toLowerCase();return e==="png"?"image/png":e==="jpg"||e==="jpeg"?"image/jpeg":e==="webp"?"image/webp":e==="gif"?"image/gif":e==="svg"?"image/svg+xml":"application/octet-stream"}async function K(n,e){let r=z(n);if(!r)return[];let o=[],i=new Set;for(let s of W(r)){let a=D("sha256").update(s).digest("hex");if(!i.has(a)){i.add(a);try{let t=await fetch(s);if(!t.ok){e?.onError?.(s,new Error(`${t.status} ${t.statusText}`));continue}let c=new Uint8Array(await t.arrayBuffer());o.push({url:s,urlhash:a,contentType:N(s),bytes:c}),e?.onStaged?.({url:s,byteLength:c.byteLength})}catch(t){e?.onError?.(s,t)}}}return o}export{H as a,J as b,K as c};