sootsim 0.1.304 → 0.1.306

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 (171) hide show
  1. package/README.md +10 -4
  2. package/dist-cli/bin.js +5 -5
  3. package/dist-cli/chunks/{agent-EK355OQM.js → agent-43LRLQBS.js} +2 -2
  4. package/dist-cli/chunks/{agent-wrapper-LCJJWHZE.js → agent-wrapper-QYKQDC2Y.js} +2 -2
  5. package/dist-cli/chunks/{app-fonts-LIPU2DLT.js → app-fonts-IHE34DCY.js} +2 -2
  6. package/dist-cli/chunks/{assert-OASKCPBX.js → assert-LAXNLFZ6.js} +2 -2
  7. package/dist-cli/chunks/{auth-XYYKD33F.js → auth-CFTFPYKX.js} +2 -2
  8. package/dist-cli/chunks/{beta-TOL55XPM.js → beta-YPZFX3DF.js} +2 -2
  9. package/dist-cli/chunks/{camera-PHKVEQHR.js → camera-FHHHH6GY.js} +2 -2
  10. package/dist-cli/chunks/{chunk-XHYSASAX.js → chunk-2UC72ZR6.js} +1 -1
  11. package/dist-cli/chunks/{chunk-BJOT2LL6.js → chunk-3XUXMKKJ.js} +2 -2
  12. package/dist-cli/chunks/{chunk-HFA5ZACE.js → chunk-4735ZG3S.js} +1 -1
  13. package/dist-cli/chunks/{chunk-5NJTX5OP.js → chunk-4KABOHME.js} +2 -2
  14. package/dist-cli/chunks/chunk-4MLH5CMV.js +125 -0
  15. package/dist-cli/chunks/chunk-4TN4CD7M.js +15 -0
  16. package/dist-cli/chunks/{chunk-ITDTSNN2.js → chunk-4XGRVAXA.js} +2 -2
  17. package/dist-cli/chunks/{chunk-2MEUZOSD.js → chunk-6PIGQNS4.js} +1 -1
  18. package/dist-cli/chunks/{chunk-INBBOCU4.js → chunk-6SBFFSXO.js} +2 -2
  19. package/dist-cli/chunks/chunk-6SIWYEK5.js +4 -0
  20. package/dist-cli/chunks/{chunk-YAV6BH3A.js → chunk-6WCW6PES.js} +2 -2
  21. package/dist-cli/chunks/{chunk-2RLFUM3C.js → chunk-74Z7ENQL.js} +104 -127
  22. package/dist-cli/chunks/{chunk-RQJDIDJ3.js → chunk-7RZYUZEK.js} +1 -1
  23. package/dist-cli/chunks/{chunk-GF3LDVWF.js → chunk-7VVWOKJE.js} +2 -2
  24. package/dist-cli/chunks/chunk-B3CXF3EF.js +5 -0
  25. package/dist-cli/chunks/{chunk-RT7TUHH7.js → chunk-BPNVJLGT.js} +2 -2
  26. package/dist-cli/chunks/{chunk-3UUHIHA6.js → chunk-C6TKPNCZ.js} +2 -2
  27. package/dist-cli/chunks/{chunk-IWRRIQDJ.js → chunk-CLN6VD2I.js} +2 -2
  28. package/dist-cli/chunks/{chunk-FNI75AD7.js → chunk-FXC6BYKX.js} +1 -1
  29. package/dist-cli/chunks/{chunk-RJWQ3VAD.js → chunk-G73JPCHX.js} +11 -11
  30. package/dist-cli/chunks/{chunk-AJYA6R3N.js → chunk-HMW7QKJV.js} +2 -2
  31. package/dist-cli/chunks/{chunk-NXYKNKNL.js → chunk-HPCKCWSQ.js} +2 -2
  32. package/dist-cli/chunks/{chunk-EXW6TH3O.js → chunk-HTR4DSA5.js} +2 -2
  33. package/dist-cli/chunks/{chunk-RYQUO6LT.js → chunk-IGKZOI2V.js} +1 -1
  34. package/dist-cli/chunks/{chunk-VUR5QXVX.js → chunk-IXPUTLYO.js} +2 -2
  35. package/dist-cli/chunks/{chunk-GMILMS7P.js → chunk-JPQSUHXW.js} +2 -2
  36. package/dist-cli/chunks/chunk-KNPXTXMI.js +4 -0
  37. package/dist-cli/chunks/{chunk-Y5YM7XHU.js → chunk-LHCC656T.js} +1 -1
  38. package/dist-cli/chunks/{chunk-A4UYZTV7.js → chunk-LOXCEKKF.js} +2 -2
  39. package/dist-cli/chunks/{chunk-CMYVGJBQ.js → chunk-MBF4LLVB.js} +1 -1
  40. package/dist-cli/chunks/{chunk-V6JDDX4B.js → chunk-MCGRZYAP.js} +7 -7
  41. package/dist-cli/chunks/chunk-NH2KYSY7.js +4 -0
  42. package/dist-cli/chunks/{chunk-QWQHSUZC.js → chunk-NOLQ4UEP.js} +2 -2
  43. package/dist-cli/chunks/chunk-NQ56D5JF.js +4 -0
  44. package/dist-cli/chunks/{chunk-G4R7CPCY.js → chunk-OYFRTYRJ.js} +2 -2
  45. package/dist-cli/chunks/{chunk-ENXLKDQ6.js → chunk-QHGQTHHD.js} +3 -3
  46. package/dist-cli/chunks/{chunk-SVFN66PF.js → chunk-RYXC27XV.js} +15 -12
  47. package/dist-cli/chunks/{chunk-ZONISYZ5.js → chunk-TCB56ABT.js} +1 -1
  48. package/dist-cli/chunks/{chunk-UB3OWUVK.js → chunk-TR4FKAKQ.js} +1 -1
  49. package/dist-cli/chunks/{chunk-XXS4TCOO.js → chunk-U6NDLMBM.js} +24 -25
  50. package/dist-cli/chunks/{chunk-SN6ARNCL.js → chunk-UAYFVJ7N.js} +2 -2
  51. package/dist-cli/chunks/{chunk-HFKQUS4I.js → chunk-UBLLWSSF.js} +3 -3
  52. package/dist-cli/chunks/{chunk-MSYPDBFG.js → chunk-UTE2W4WC.js} +2 -2
  53. package/dist-cli/chunks/{chunk-XCVSB47R.js → chunk-UTHRVXG5.js} +2 -2
  54. package/dist-cli/chunks/{chunk-LRXSLMX4.js → chunk-V5PJXD2O.js} +1 -1
  55. package/dist-cli/chunks/{chunk-JDKWIB6V.js → chunk-VLKENZFJ.js} +1 -1
  56. package/dist-cli/chunks/{chunk-7RFXZXKH.js → chunk-VTPICQDM.js} +1 -1
  57. package/dist-cli/chunks/chunk-XZSXFV33.js +71 -0
  58. package/dist-cli/chunks/chunk-YBO7SFWN.js +6 -0
  59. package/dist-cli/chunks/{chunk-OSH6526H.js → chunk-YVWYARHY.js} +1 -1
  60. package/dist-cli/chunks/{cleanup-GCTEGJMP.js → cleanup-GYDCGNEJ.js} +2 -2
  61. package/dist-cli/chunks/{cli-version-G72HUH4E.js → cli-version-3HG7ILRV.js} +2 -2
  62. package/dist-cli/chunks/{compat-IC4JHGBH.js → compat-GS3SVTB4.js} +2 -2
  63. package/dist-cli/chunks/{config-C65MZFU3.js → config-W5LN7HWO.js} +2 -2
  64. package/dist-cli/chunks/control-BE55SA2W.js +4 -0
  65. package/dist-cli/chunks/daemon-3YMH272E.js +4 -0
  66. package/dist-cli/chunks/{debug-LEL32PDN.js → debug-2BBR66MJ.js} +3 -3
  67. package/dist-cli/chunks/desktop-PPB66SZO.js +31 -0
  68. package/dist-cli/chunks/{detox-SCCWUIKK.js → detox-P7SDHLB4.js} +2 -2
  69. package/dist-cli/chunks/{device-MTDEKDUK.js → device-JB4NSOZV.js} +2 -2
  70. package/dist-cli/chunks/{diagnose-UZJCKLPZ.js → diagnose-M662DIPM.js} +2 -2
  71. package/dist-cli/chunks/disk-cleanup-HN662P2U.js +4 -0
  72. package/dist-cli/chunks/drivers-ALHRJJGF.js +4 -0
  73. package/dist-cli/chunks/{film-YBE544PG.js → film-QZMDVCZI.js} +3 -3
  74. package/dist-cli/chunks/flow-BHSACBQ5.js +4 -0
  75. package/dist-cli/chunks/help-V2KS2AUT.js +4 -0
  76. package/dist-cli/chunks/{hidden-runtime-alias-KJYCJLSR.js → hidden-runtime-alias-LJXH6WPY.js} +2 -2
  77. package/dist-cli/chunks/{home-paths-NSHWNEGK.js → home-paths-TQ53NFW3.js} +2 -2
  78. package/dist-cli/chunks/inspect-6PW37GVK.js +4 -0
  79. package/dist-cli/chunks/install-desktop-VMVPZSXF.js +4 -0
  80. package/dist-cli/chunks/{login-AEVUU72R.js → login-7XXY5JP4.js} +4 -4
  81. package/dist-cli/chunks/{logout-72XHF6J2.js → logout-UNARNKTJ.js} +2 -2
  82. package/dist-cli/chunks/{maestro-OXSKQJ6B.js → maestro-66RNLMLU.js} +3 -3
  83. package/dist-cli/chunks/{maestro-generate-AKZFORVJ.js → maestro-generate-IO25AY3E.js} +3 -3
  84. package/dist-cli/chunks/{mode-U6KRG2PE.js → mode-DPGASAG4.js} +2 -2
  85. package/dist-cli/chunks/optional-demo-registry-O5GGFNAI.js +4 -0
  86. package/dist-cli/chunks/{perf-KF2BJZIN.js → perf-CBUP2ERS.js} +2 -2
  87. package/dist-cli/chunks/{permissions-TRJG4QAI.js → permissions-3AOMCAT3.js} +2 -2
  88. package/dist-cli/chunks/{record-TICJYZOA.js → record-Q7VRLH7A.js} +3 -3
  89. package/dist-cli/chunks/{report-issue-OQPLBQMJ.js → report-issue-DTWZECYM.js} +2 -2
  90. package/dist-cli/chunks/runtime-VGYKLLTE.js +4 -0
  91. package/dist-cli/chunks/{screenshot-command-XLERKHVW.js → screenshot-command-SSWKZ4U3.js} +6 -6
  92. package/dist-cli/chunks/screenshot-layers-2ABSTF2O.js +20 -0
  93. package/dist-cli/chunks/{screenshots-capture-B6OORSD4.js → screenshots-capture-QA7T5DED.js} +2 -2
  94. package/dist-cli/chunks/{serve-5M7MIPWF.js → serve-T4EQBJM6.js} +3 -3
  95. package/dist-cli/chunks/{setup-HJZDC4PK.js → setup-BXQI3YT4.js} +2 -2
  96. package/dist-cli/chunks/{skills-RXHLMGGL.js → skills-AYSO5FJB.js} +2 -2
  97. package/dist-cli/chunks/{storage-MMPD2XBS.js → storage-KBHG5M5F.js} +2 -2
  98. package/dist-cli/chunks/store-GUNEQ4EC.js +4 -0
  99. package/dist-cli/chunks/telemetry-4VZ43AHH.js +4 -0
  100. package/dist-cli/chunks/{timeline-6PF5BZ37.js → timeline-AB2R26RE.js} +2 -2
  101. package/dist-cli/chunks/{upgrade-QSMIK3RC.js → upgrade-5XXQ7OCE.js} +2 -2
  102. package/dist-cli/chunks/upload-M55IQQHQ.js +4 -0
  103. package/dist-cli/chunks/{version-WLTFXEDC.js → version-PH3OUNPP.js} +2 -2
  104. package/dist-cli/chunks/{web-DI73LQ7N.js → web-XTOGNFZ7.js} +2 -2
  105. package/dist-cli/chunks/{what-happened-4SAMGZWR.js → what-happened-JLI3NTEU.js} +2 -2
  106. package/dist-lib/agent-daemon-client.cjs +1 -1
  107. package/dist-lib/agent-events.cjs +1 -1
  108. package/dist-lib/agent-identity.cjs +1 -1
  109. package/dist-lib/agent-sessions.cjs +1 -1
  110. package/dist-lib/attached-projects.cjs +43 -21
  111. package/dist-lib/auth/shared-session.cjs +1 -1
  112. package/dist-lib/backend-origin.cjs +1 -1
  113. package/dist-lib/beta.cjs +1 -1
  114. package/dist-lib/beta.mjs +1 -1
  115. package/dist-lib/bridge-constants.cjs +8 -1
  116. package/dist-lib/cli-constants.cjs +1 -1
  117. package/dist-lib/config.cjs +1 -1
  118. package/dist-lib/detox/index.cjs +1 -1
  119. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  120. package/dist-lib/home-paths.cjs +1 -1
  121. package/dist-lib/host/bridge-host.cjs +222 -302
  122. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  123. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  124. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  125. package/dist-lib/host/websocket-proxy.cjs +1 -1
  126. package/dist-lib/index.cjs +1 -1
  127. package/dist-lib/jump-to-source-babel.cjs +1 -1
  128. package/dist-lib/menu.cjs +1 -1
  129. package/dist-lib/menu.mjs +1 -1
  130. package/dist-lib/metro.cjs +1 -1
  131. package/dist-lib/profiles.cjs +1 -1
  132. package/dist-lib/render-mode.cjs +1 -1
  133. package/dist-lib/scripts/dev-server-scanner.cjs +43 -13
  134. package/dist-lib/sdk.cjs +47 -10
  135. package/dist-lib/sdk.mjs +47 -10
  136. package/dist-lib/skills.cjs +5708 -1217
  137. package/dist-lib/vite.cjs +1 -1
  138. package/package.json +1 -1
  139. package/scripts/dev-server-scanner.ts +9 -15
  140. package/scripts/optional-demo-registry.ts +52 -10
  141. package/skills/sootsim-debug/SKILL.md +4 -5
  142. package/skills/sootsim-setup/SKILL.md +29 -49
  143. package/src/bridge-constants.ts +15 -0
  144. package/src/host/bridge-host.ts +0 -16
  145. package/src/host/open-url.ts +2 -147
  146. package/src/plist.ts +27 -0
  147. package/src/settings-bundle.ts +2 -23
  148. package/src/skills/builtin/maestro-test.ts +4 -1
  149. package/src/vite-plugin.ts +2 -1
  150. package/dist-cli/chunks/chunk-76OLOD2G.js +0 -15
  151. package/dist-cli/chunks/chunk-ABHNOM2C.js +0 -4
  152. package/dist-cli/chunks/chunk-BPRMA4IC.js +0 -4
  153. package/dist-cli/chunks/chunk-M3ETYVNL.js +0 -4
  154. package/dist-cli/chunks/chunk-W3CSYOSQ.js +0 -4
  155. package/dist-cli/chunks/chunk-WNDYTKLC.js +0 -4
  156. package/dist-cli/chunks/chunk-YXHO2OUL.js +0 -7
  157. package/dist-cli/chunks/chunk-ZDRI2LUG.js +0 -125
  158. package/dist-cli/chunks/control-RWLYBKQR.js +0 -4
  159. package/dist-cli/chunks/daemon-D77E5G6Z.js +0 -4
  160. package/dist-cli/chunks/desktop-RDSISJH2.js +0 -34
  161. package/dist-cli/chunks/disk-cleanup-HLWRVTVW.js +0 -4
  162. package/dist-cli/chunks/drivers-Q7GMKTBM.js +0 -4
  163. package/dist-cli/chunks/flow-5VOVAVZN.js +0 -4
  164. package/dist-cli/chunks/help-2PESJ7V2.js +0 -4
  165. package/dist-cli/chunks/inspect-OIA3Q5MD.js +0 -4
  166. package/dist-cli/chunks/install-desktop-7KLACQUW.js +0 -4
  167. package/dist-cli/chunks/optional-demo-registry-L3VIWBI2.js +0 -4
  168. package/dist-cli/chunks/runtime-HDMLWNXW.js +0 -4
  169. package/dist-cli/chunks/store-APKY72ZY.js +0 -4
  170. package/dist-cli/chunks/telemetry-W3YRHE67.js +0 -4
  171. package/dist-cli/chunks/upload-HQEOYILQ.js +0 -4
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.304 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.306 | (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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sootsim",
3
- "version": "0.1.304",
3
+ "version": "0.1.306",
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",
@@ -29,14 +29,6 @@ import {
29
29
  } from '../src/settings-bundle.ts'
30
30
  import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
31
31
 
32
- // pre-warm the optional internal demo registry once at module load. in-repo
33
- // (electron tray / `bun run demo` discovery) this resolves the source registry
34
- // so a launched demo's runtimeConfig is folded into its bundle URL; on a
35
- // published install the registry was never shipped, so this resolves to [] and
36
- // every dev server is treated generically. either way `withRuntimeConfig` below
37
- // reads the synchronous cached view.
38
- void loadOptionalDemoApps()
39
-
40
32
  export interface DiscoveredServer {
41
33
  port: number
42
34
  framework: 'metro' | 'expo' | 'vxrn' | 'one' | 'unknown'
@@ -556,6 +548,11 @@ export async function probePort(
556
548
  port: number,
557
549
  buildIconProxyUrl?: (externalUrl: string) => string,
558
550
  ): Promise<DiscoveredServer | null> {
551
+ // callers such as `sootsim open <port>` probe one port directly instead of
552
+ // going through scanDevServers. wait for the optional in-repo registry here
553
+ // too, or a cold CLI process can build the known app's bundle url before its
554
+ // runtime config is available.
555
+ await loadOptionalDemoApps()
559
556
  if (!(await tcpPing(port))) return null
560
557
 
561
558
  const onePath = `/node_modules/one/metro-entry.bundle?platform=ios&dev=true`
@@ -824,13 +821,10 @@ export const __attachAppSettingsBundleForTests = attachAppSettingsBundle
824
821
  export async function scanDevServers(
825
822
  opts: ScanOptions = {},
826
823
  ): Promise<DiscoveredServer[]> {
827
- // await the registry rather than trusting the module-init prewarm to have
828
- // won. optionalDemoAppsSync() returns [] until that dynamic import resolves,
829
- // and an empty registry is indistinguishable from a published install, so a
830
- // scan in that window silently drops a known app's runtimeConfig and hands
831
- // back a bundle URL with no sootsimConfig at all. it is already resolved by
832
- // the time any real scan runs, which is exactly why the window is easy to
833
- // miss; it opens when disk is contended. memoized, so this is free after the
824
+ // optionalDemoAppsSync() returns [] until the dynamic import resolves, and
825
+ // an empty registry is indistinguishable from a published install. await it
826
+ // before cache validation so a known app cannot lose its runtimeConfig when
827
+ // disk contention delays the import. memoized, so this is free after the
834
828
  // first call.
835
829
  await loadOptionalDemoApps()
836
830
  const processes = await discoverListeningProcesses(opts.excludePorts)
@@ -8,17 +8,54 @@
8
8
  // apps — shipping the registry once matched a brand-new user's `expo start` on
9
9
  // the default port 8081 against the 3pc `one` entry and crashed their metro
10
10
  // (repo-agnostic-pipeline violation). so the registry is NOT in the package
11
- // `files` list and is marked external in `scripts/build-cli.ts`, which leaves
12
- // this `import()` as a runtime dynamic import: it resolves the source registry
13
- // in-repo and resolves to nothing (empty list) on a published install.
11
+ // `files` list, and the `import()` below is resolved to an absolute path at
12
+ // runtime: it finds the source registry in-repo and nothing (empty list) on a
13
+ // published install. no bundler can see through that, so it never inlines.
14
14
  //
15
15
  // one path, no env toggle: every consumer goes through this loader; whether the
16
16
  // registry resolves is purely a function of whether the file exists on disk.
17
17
 
18
- // the path is assembled at runtime so neither esbuild nor a bundler treats it
19
- // as a static dependency to inline — it stays a true dynamic import resolved
20
- // against the package's own scripts dir at runtime.
21
- const REGISTRY_SPECIFIER = './demo-app-registry.ts'
18
+ import { existsSync } from 'node:fs'
19
+ import { basename, dirname, join } from 'node:path'
20
+ import { fileURLToPath, pathToFileURL } from 'node:url'
21
+
22
+ // this module is emitted at three different depths — the source at
23
+ // packages/sootsim/scripts/, the built CLI at packages/sootsim/dist-cli/chunks/,
24
+ // and the electron main bundle at packages/sootsim-engine/dist-electron/ — so a
25
+ // relative './demo-app-registry.ts' is only correct from the source copy. from
26
+ // the other two it points at a file that does not exist, the catch below
27
+ // swallows that, and every demo app then boots with no runtimeConfig at all
28
+ // (mattermost stops on the server-select screen because AutoSelectServerUrl
29
+ // never reaches it). the one description that holds from every emit site is the
30
+ // ancestor that holds the registry at its repo path, which in-repo is the
31
+ // monorepo root.
32
+ const REGISTRY_REPO_PATH = 'packages/sootsim/scripts/demo-app-registry.ts'
33
+
34
+ // the walk must never leave the installed package. `sootsim` publishes this
35
+ // loader's source, so on a published install it runs from
36
+ // node_modules/sootsim/scripts/ (or dist-cli/chunks/) with the consumer's whole
37
+ // filesystem above it — and consumers include this repo's own templates and
38
+ // examples, which install sootsim while sitting inside the monorepo. an
39
+ // unbounded walk from there reaches the real registry and hands a user's
40
+ // `expo start` on port 8081 the internal 3pc entry, which is the exact incident
41
+ // the header describes. a node_modules ancestor means "installed as a
42
+ // dependency", so the search stops there and the registry stays absent.
43
+ const MAX_ANCESTOR_STEPS = 8
44
+
45
+ /** absolute path to the internal registry, or null when this copy is running
46
+ * outside the monorepo (a published install) and must not have one. */
47
+ export function findInternalDemoRegistry(startDir: string): string | null {
48
+ let dir = startDir
49
+ for (let step = 0; step < MAX_ANCESTOR_STEPS; step++) {
50
+ if (basename(dir) === 'node_modules') return null
51
+ const candidate = join(dir, REGISTRY_REPO_PATH)
52
+ if (existsSync(candidate)) return candidate
53
+ const parent = dirname(dir)
54
+ if (parent === dir) return null
55
+ dir = parent
56
+ }
57
+ return null
58
+ }
22
59
 
23
60
  export interface OptionalDemoApp {
24
61
  name: string
@@ -35,8 +72,13 @@ let cached: OptionalDemoApp[] | null = null
35
72
  * returns [] on a published install where the registry was not shipped. */
36
73
  export async function loadOptionalDemoApps(): Promise<OptionalDemoApp[]> {
37
74
  if (cached) return cached
75
+ const registryPath = findInternalDemoRegistry(dirname(fileURLToPath(import.meta.url)))
76
+ if (!registryPath) {
77
+ cached = []
78
+ return cached
79
+ }
38
80
  try {
39
- const mod = (await import(/* @vite-ignore */ REGISTRY_SPECIFIER)) as {
81
+ const mod = (await import(/* @vite-ignore */ pathToFileURL(registryPath).href)) as {
40
82
  APPS?: OptionalDemoApp[]
41
83
  }
42
84
  cached = Array.isArray(mod.APPS) ? mod.APPS : []
@@ -47,8 +89,8 @@ export async function loadOptionalDemoApps(): Promise<OptionalDemoApp[]> {
47
89
  }
48
90
 
49
91
  /** synchronous view of the registry — empty until `loadOptionalDemoApps()` has
50
- * resolved at least once. the scanner pre-warms this at module init so its
51
- * synchronous per-port lookup can stay synchronous. */
92
+ * resolved at least once. every caller that builds a result from the registry
93
+ * awaits the load first. */
52
94
  export function optionalDemoAppsSync(): OptionalDemoApp[] {
53
95
  return cached ?? []
54
96
  }
@@ -64,11 +64,10 @@ regression test. a pile of ad-hoc logs helps no one and gets deleted.
64
64
  - **pin a sim once.** if more than one is live, an unpinned command
65
65
  refuses to choose. run `sootsim use <id>` at the start of the workflow.
66
66
  - **keep one sim and reuse its browser tab.** before `sootsim open`, run
67
- `sootsim list --all`. if the target is already connected, pin it and use
68
- `sootsim do reload`; do not open another tab to reload or retry a capture.
69
- re-run `list --all` after each reload or reconnect; when another entry appears,
70
- run `sootsim close --others` against the pinned sim immediately.
71
- if the current sim must be replaced, close it before opening the replacement.
67
+ `sootsim list --all`. pin the target and use `sootsim do reload`; a plain
68
+ `sootsim open <target>` also navigates the saved sim. reserve `--new` for a
69
+ genuinely concurrent sim. repeated browser trees consume memory and CPU on a
70
+ shared machine.
72
71
  - **`debug eval` is an escape hatch.** if a top-level command feels
73
72
  broken or missing, file an issue — every agent after you benefits from
74
73
  a better CLI, not a pile of ad-hoc scripts.
@@ -15,60 +15,45 @@ debugging or `/sootsim-test` for writing tests.
15
15
  ## anti-patterns (read first)
16
16
 
17
17
  - **never `sootsim claim --force` a sim held by another CLI.** the lease
18
- exists for a reason — forcing strands the other agent and corrupts the
18
+ exists for a reason. forcing strands the other agent and corrupts the
19
19
  bridge state. use `sootsim open --new` to start a fresh sim instead.
20
20
  - **don't switch sims mid-investigation.** pin once with `sootsim use
21
21
  <id>` and keep it pinned. an unpinned command refuses to choose when more
22
22
  than one driveable sim is live.
23
- - **an empty `sootsim list` is not "broken sootsim"** — it usually means the
23
+ - **an empty `sootsim list` is not "broken sootsim".** it usually means the
24
24
  daemon is down, the dev server port is wrong, or the runtime is missing.
25
25
  walk the recovery checklist before reinstalling.
26
- - **one sim for your whole session. switch APPS by navigating that sim, never by
27
- opening another.** `sootsim open <port> --sim <id>` resolves that exact sim and
28
- navigates it in place — no new tab, no new browser tree. that is the supported
29
- way to move between apps and ports:
26
+ - **keep one sim for your whole session.** the first `sootsim open <port>`
27
+ launches Chrome for Testing with an isolated profile. later opens reuse the
28
+ saved sim and navigate it in place. use `sootsim do reload` when the target
29
+ has not changed:
30
30
 
31
31
  ```sh
32
- sootsim open 8086 --sim <id> # one app, in your sim
33
- sootsim open 8090 --sim <id> # a different app, same tab
34
- sootsim reload --sim <id> # reload whatever it is showing
35
- sootsim close <id> # dispose it when you are done
32
+ sootsim open 8086 # first call creates your isolated sim
33
+ sootsim open 8090 # same sim, different app
34
+ sootsim do reload # same sim, same app
35
+ sootsim close <id> # dispose it when you are done
36
36
  ```
37
37
 
38
- every `--new` spawns another browser tree, and each abandoned tab holds a
39
- renderer process for the life of the browser. these land in somebody's actual
40
- desktop Chrome. a run that opened one per app left 14 renderers against 2 live
41
- sims on a shared machine and the owner noticed before we did.
42
- - **sharing a machine with other agents? open ONE sim with `--new`, pin it, and
43
- pass `--sim <id>` from then on.** that single `--new` is the only one you get.
44
- an unpinned command has no sim of its own, so it falls back to whatever sim is
45
- currently PRIMARY — and that can be another agent's live sim, mid-run. a plain
46
- `sootsim open <port>` from an unpinned CLI will happily retarget somebody
47
- else's sim to your port and take over their session; the warning it prints is
48
- easy to miss in a transcript. the current-sim pin is stored per CLI identity
49
- (`sootsim-current-sim-<identity>.json`, keyed by your process-tree root), so
50
- pinning is genuinely per-agent and does not fight anyone else:
38
+ `--new`, `--profile`, and `--ephemeral` create another isolated browser
39
+ tree. use them only when separate storage or a genuinely concurrent sim is
40
+ required. repeated new trees consume enough memory and CPU to starve other
41
+ work on a shared machine.
42
+ - **pin the sim before driving it.** `sootsim open` records the new sim for the
43
+ current CLI identity. read its id and pin it explicitly before a longer flow:
51
44
 
52
45
  ```sh
53
- sootsim open --new 8086 # your own sim; also records it as yours
54
- sootsim list # read back its id
55
- sootsim use <id> # pin it explicitly
56
- sootsim describe --sim <id> # name it on every command after that
46
+ sootsim open 8086
47
+ sootsim list
48
+ sootsim use <id>
49
+ sootsim describe --sim <id>
57
50
  ```
58
51
 
59
- `--new`, `--profile`, and `--ephemeral` are rejected outright when combined
60
- with `--sim`, so if a command needs one of them you are starting a second sim
61
- whether you meant to or not. if yours died, run `sootsim list --all` and adopt
62
- an open unlocked sim with `sootsim use <id>` before you consider opening one.
63
- always close yours when you finish.
64
52
  - **never retry-loop `sootsim open` against a wedged or slow stack.** a
65
- connect-timeout ("timed out waiting for opened sim to connect") means STOP and
66
- diagnose — not retry. each retried open spawns a new browser tree (a
67
- software-render `headless_shell` under the bundled chromium), and a pile of
68
- them is a machine-meltdown vector (incident 2026-07-08: a retry-loop of opens
69
- against a wedged mobile stack drove a load emergency). reuse ONE persistent
70
- sim tab and reload — never new-tab-per-attempt — and launch GPU chrome
71
- (`channel:'chrome'`), not the bundled `headless_shell`.
53
+ connect timeout means diagnose the stack. do not retry. if the first launch
54
+ never connects, each retry can create another browser tree before there is a
55
+ saved sim to reuse. verify the development server and SootSim runtime, then
56
+ retry once after fixing the cause.
72
57
 
73
58
  ## first move
74
59
 
@@ -151,16 +136,11 @@ sootsim close <id> # release it cleanly
151
136
  Run `sootsim runtime install`, then retry `sootsim open <port>`.
152
137
 
153
138
  **`sootsim open` times out ("timed out waiting for opened sim to connect").**
154
- do NOT retry the open in a loop — each attempt spawns a browser tree, and
155
- reopening a wedged/slow stack repeatedly is a machine-meltdown pattern. diagnose
156
- instead: check machine load (`sysctl -n vm.loadavg` — a starved box boots the
157
- engine too slowly to register) and the dev stack's health (`/__soot/` should
158
- 200; a partial `dev:*` stack with no engine watchers can serve a stale engine
159
- dist that never connects, and a wedged one-dev logging unhandled rejections
160
- won't boot a fresh sim). if load is high, let it settle before ONE more try. if
161
- the stack is wedged, restart it cleanly rather than reopening. always launch GPU
162
- chrome (`channel:'chrome'`), never the bundled `headless_shell` — software
163
- rendering pegs every core for the canvas's whole lifetime.
139
+ do not retry in a loop. verify the development server first: `__soot/` must
140
+ respond and the engine watchers must be running. inspect the driver diagnostic
141
+ path printed by the failed command. restart the broken development stack at its
142
+ supervisor, then retry once. SootSim owns the isolated browser profile and its
143
+ process tree; do not open the shell URL through an operating-system browser.
164
144
 
165
145
  **bridge disconnects mid-sim.** the WebSocket dropped (laptop slept,
166
146
  network blip). re-pin: `sootsim use <id>`. the daemon retries reconnection
@@ -71,6 +71,21 @@ function closestLocalDevBaseHttpPort(httpPort: number): number | null {
71
71
  return closest?.basePort ?? null
72
72
  }
73
73
 
74
+ /** the vite dev shell that shares a world with a bridge port. the two are one
75
+ * offset apart by construction (bridge 7668 + N, shell 5173 + N), so a caller
76
+ * that pinned an explicit bridge port can name its own shell directly. that
77
+ * matters because the dev-bridge lockfile records only the stack that
78
+ * heartbeated last: on a machine running several worktree stacks it routinely
79
+ * names someone else's shell, and a page opened there while we poll our own
80
+ * bridge never connects. null when the port is not a local dev bridge port. */
81
+ export function resolveSootsimShellBaseUrlForBridgePort(
82
+ bridgePort: number,
83
+ ): string | null {
84
+ const offset = bridgePort - DEFAULT_SOOTSIM_BRIDGE_PORT
85
+ if (offset < 0 || offset > MAX_LOCAL_DEV_PORT_OFFSET) return null
86
+ return `http://localhost:${SOOTSIM_SHELL_DEV_HTTP_BASE_PORT + offset}/`
87
+ }
88
+
74
89
  export function resolveSootsimBridgePortForRuntimeLocation(
75
90
  input: {
76
91
  explicitPort?: unknown
@@ -939,22 +939,6 @@ export class SootSimBridgeHost {
939
939
  return
940
940
  }
941
941
 
942
- if (msg.type === 'bridge:open') {
943
- if (typeof msg.url !== 'string' || !msg.url) {
944
- throw new Error('bridge:open requires a url')
945
- }
946
- await this.openUrl(msg.url, { newWindow: msg.newWindow === true })
947
- if (ws.readyState === WebSocket.OPEN) {
948
- ws.send(
949
- JSON.stringify({
950
- id: msg.id,
951
- result: { ok: true, url: msg.url },
952
- }),
953
- )
954
- }
955
- return
956
- }
957
-
958
942
  if (msg.type === 'bridge:claim') {
959
943
  const targetSim = await this.waitForSim(msg.simId)
960
944
  const outcome = this.tryAcquireLease(ws, targetSim, {
@@ -1,19 +1,14 @@
1
- import { execFileSync, spawn } from 'child_process'
2
- import { existsSync } from 'fs'
3
- import { homedir } from 'os'
4
- import { join } from 'path'
1
+ import { spawn } from 'child_process'
5
2
 
6
- export type OpenUrlLaunchVia = 'chromium' | 'system'
3
+ export type OpenUrlLaunchVia = 'system'
7
4
 
8
5
  export interface OpenUrlOptions {
9
- newWindow?: boolean
10
6
  detached?: boolean
11
7
  background?: boolean
12
8
  }
13
9
 
14
10
  export interface OpenUrlCommandOptions extends OpenUrlOptions {
15
11
  platform?: NodeJS.Platform
16
- chromiumBinary?: string | null
17
12
  }
18
13
 
19
14
  export interface OpenUrlCommand {
@@ -28,97 +23,6 @@ export interface OpenUrlLaunchResult extends OpenUrlCommand {
28
23
  attachUrl: string
29
24
  }
30
25
 
31
- const MAC_CHROMIUM_CANDIDATES = [
32
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
33
- '~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
34
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
35
- '~/Applications/Chromium.app/Contents/MacOS/Chromium',
36
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
37
- '~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
38
- '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
39
- '~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
40
- '/Applications/Arc.app/Contents/MacOS/Arc',
41
- '~/Applications/Arc.app/Contents/MacOS/Arc',
42
- ]
43
-
44
- const LINUX_CHROMIUM_CANDIDATES = [
45
- '/usr/bin/google-chrome',
46
- '/usr/bin/chromium',
47
- '/usr/bin/chromium-browser',
48
- '/usr/bin/microsoft-edge',
49
- '/usr/bin/brave-browser',
50
- '/snap/bin/chromium',
51
- ]
52
-
53
- const WINDOWS_CHROMIUM_CANDIDATES = [
54
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
55
- 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
56
- 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
57
- 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
58
- 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
59
- 'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
60
- ]
61
-
62
- const UNIX_CHROMIUM_COMMANDS = [
63
- 'google-chrome',
64
- 'chromium',
65
- 'chromium-browser',
66
- 'microsoft-edge',
67
- 'brave-browser',
68
- ]
69
-
70
- const WINDOWS_CHROMIUM_COMMANDS = ['chrome', 'msedge', 'brave']
71
-
72
- function expandHome(candidate: string): string {
73
- return candidate.startsWith('~/') ? join(homedir(), candidate.slice(2)) : candidate
74
- }
75
-
76
- function firstExisting(candidates: string[]): string | null {
77
- for (const candidate of candidates) {
78
- const expanded = expandHome(candidate)
79
- if (existsSync(expanded)) return expanded
80
- }
81
- return null
82
- }
83
-
84
- function lookupExecutable(command: string, platform: NodeJS.Platform): string | null {
85
- try {
86
- const tool = platform === 'win32' ? 'where' : 'which'
87
- const out = execFileSync(tool, [command], {
88
- encoding: 'utf8',
89
- timeout: 1500,
90
- stdio: ['ignore', 'pipe', 'ignore'],
91
- }).trim()
92
- return out ? out.split(/\r?\n/)[0] : null
93
- } catch {
94
- return null
95
- }
96
- }
97
-
98
- export function resolveChromiumBinary(
99
- platform: NodeJS.Platform = process.platform,
100
- ): string | null {
101
- const envCandidate = process.env.CHROME_PATH || process.env.CHROMIUM_PATH
102
- if (envCandidate && existsSync(envCandidate)) return envCandidate
103
-
104
- const direct =
105
- platform === 'darwin'
106
- ? firstExisting(MAC_CHROMIUM_CANDIDATES)
107
- : platform === 'win32'
108
- ? firstExisting(WINDOWS_CHROMIUM_CANDIDATES)
109
- : firstExisting(LINUX_CHROMIUM_CANDIDATES)
110
- if (direct) return direct
111
-
112
- const commands =
113
- platform === 'win32' ? WINDOWS_CHROMIUM_COMMANDS : UNIX_CHROMIUM_COMMANDS
114
- for (const command of commands) {
115
- const found = lookupExecutable(command, platform)
116
- if (found) return found
117
- }
118
-
119
- return null
120
- }
121
-
122
26
  export function buildOpenUrlCommand(
123
27
  url: string,
124
28
  options: OpenUrlCommandOptions = {},
@@ -126,10 +30,6 @@ export function buildOpenUrlCommand(
126
30
  if (!url) throw new Error('openUrl requires a url')
127
31
 
128
32
  const platform = options.platform ?? process.platform
129
- if (options.newWindow) {
130
- return buildChromiumUrlCommand(url, { ...options, platform, newWindow: true })
131
- }
132
-
133
33
  if (platform === 'darwin') {
134
34
  return {
135
35
  command: 'open',
@@ -153,38 +53,6 @@ export function buildOpenUrlCommand(
153
53
  }
154
54
  }
155
55
 
156
- export function buildChromiumUrlCommand(
157
- url: string,
158
- options: OpenUrlCommandOptions = {},
159
- ): OpenUrlCommand {
160
- if (!url) throw new Error('openUrl requires a url')
161
-
162
- const platform = options.platform ?? process.platform
163
- const chromiumBinary =
164
- 'chromiumBinary' in options ? options.chromiumBinary : resolveChromiumBinary(platform)
165
- if (!chromiumBinary) {
166
- throw new Error('browser launch requires Chrome, Chromium, Edge, Brave, or Arc')
167
- }
168
-
169
- // the chromium driver always launches a visible system browser window.
170
- // headless is intentionally NOT supported here: bare `chrome --headless=new
171
- // <url>` renders the page once and exits — nothing keeps the process (and
172
- // therefore the sim) alive without an attached CDP client. headless
173
- // automation is the playwright driver's job. see chromium.ts `launch`.
174
- const args: string[] = []
175
- if (options.newWindow !== false) {
176
- args.push('--new-window')
177
- }
178
- args.push(url)
179
-
180
- return {
181
- command: chromiumBinary,
182
- args,
183
- via: 'chromium',
184
- target: chromiumBinary,
185
- }
186
- }
187
-
188
56
  async function spawnLaunch(
189
57
  command: string,
190
58
  args: string[],
@@ -216,19 +84,6 @@ export async function launchUrl(
216
84
  }
217
85
  }
218
86
 
219
- export async function launchChromiumUrl(
220
- url: string,
221
- options: OpenUrlCommandOptions = {},
222
- ): Promise<OpenUrlLaunchResult> {
223
- const command = buildChromiumUrlCommand(url, options)
224
- const pid = await spawnLaunch(command.command, command.args, options.detached ?? true)
225
- return {
226
- ...command,
227
- pid,
228
- attachUrl: url,
229
- }
230
- }
231
-
232
87
  export async function openUrl(url: string, options: OpenUrlOptions = {}): Promise<void> {
233
88
  await launchUrl(url, options)
234
89
  }
package/src/plist.ts ADDED
@@ -0,0 +1,27 @@
1
+ import plistModule from '@expo/plist'
2
+
3
+ function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
5
+ }
6
+
7
+ function resolvePlistParser(value: unknown): ((source: string) => unknown) | null {
8
+ if (!isRecord(value)) return null
9
+ const direct = value.parse
10
+ if (typeof direct === 'function') {
11
+ return (source) => Reflect.apply(direct, value, [source])
12
+ }
13
+ const nested = value.default
14
+ if (!isRecord(nested)) return null
15
+ const nestedParse = nested.parse
16
+ return typeof nestedParse === 'function'
17
+ ? (source) => Reflect.apply(nestedParse, nested, [source])
18
+ : null
19
+ }
20
+
21
+ // bun consumes @expo/plist's default export directly, while the compiled commonjs
22
+ // scanner receives the package namespace with that export nested under default.
23
+ const parsePlist = resolvePlistParser(plistModule)
24
+
25
+ export function parsePlistSource(source: string): unknown {
26
+ return parsePlist?.(source) ?? null
27
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFile, readdir, stat } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
- import plistModule from '@expo/plist'
3
+ import { parsePlistSource } from './plist.ts'
4
4
  import type { AppPreferenceValue, AppSettingsSpecifier } from '@sootsim/globals'
5
5
 
6
6
  const MAX_PLIST_BYTES = 1_048_576
@@ -26,26 +26,6 @@ function isRecord(value: unknown): value is PlistRecord {
26
26
  return typeof value === 'object' && value !== null && !Array.isArray(value)
27
27
  }
28
28
 
29
- function resolvePlistParser(value: unknown): ((source: string) => unknown) | null {
30
- if (!isRecord(value)) return null
31
- const direct = value.parse
32
- if (typeof direct === 'function') {
33
- return (source) => Reflect.apply(direct, value, [source])
34
- }
35
- const nested = value.default
36
- if (!isRecord(nested)) return null
37
- const nestedParse = nested.parse
38
- return typeof nestedParse === 'function'
39
- ? (source) => Reflect.apply(nestedParse, nested, [source])
40
- : null
41
- }
42
-
43
- // bun consumes @expo/plist's default export directly, while the compiled commonjs
44
- // scanner receives the package namespace with that export nested under
45
- // `default`. resolve the parser once at this package boundary so source and
46
- // published scanners execute the same implementation.
47
- const parsePlist = resolvePlistParser(plistModule)
48
-
49
29
  function optionalString(value: unknown): string | undefined {
50
30
  return typeof value === 'string' ? value : undefined
51
31
  }
@@ -196,11 +176,10 @@ async function settingsBundleFingerprint(bundlePath: string): Promise<string | n
196
176
 
197
177
  async function parsePlistRecord(path: string): Promise<PlistRecord | null> {
198
178
  try {
199
- if (!parsePlist) return null
200
179
  const metadata = await stat(path)
201
180
  if (!metadata.isFile() || metadata.size > MAX_PLIST_BYTES) return null
202
181
  const source = await readFile(path, 'utf8')
203
- const parsed = parsePlist(source)
182
+ const parsed = parsePlistSource(source)
204
183
  return isRecord(parsed) ? parsed : null
205
184
  } catch {
206
185
  return null
@@ -31,6 +31,7 @@ export const skill: SootSimSkill = {
31
31
  const { DEFAULT_SOOTSIM_BRIDGE_PORT } = await import('../../bridge-constants')
32
32
  const { SootSimBridgeHost } = await import('../../host/bridge-host')
33
33
  const { createBridge } = await import('../../../cli/ws-bridge')
34
+ const { runOpenCommand } = await import('../../../cli/commands/control')
34
35
  const { SootSimBridgeFlowRunner } =
35
36
  await import('../../../cli/bridge-flow-runner')
36
37
 
@@ -44,7 +45,9 @@ export const skill: SootSimSkill = {
44
45
  const steps = yaml.parse(stepsBody)
45
46
  const beforeIds = new Set(host.listSims().map((sim) => sim.id))
46
47
 
47
- await host.openUrl(context.url)
48
+ await runOpenCommand([context.url, '--new', '--headless', '--no-describe'], {
49
+ port: DEFAULT_SOOTSIM_BRIDGE_PORT,
50
+ })
48
51
 
49
52
  let simId: string | null = null
50
53
  for (let i = 0; i < 60; i++) {
@@ -54,6 +54,7 @@ const compatRoot = path.resolve(sootsimRoot, '..', 'compat')
54
54
  // renames of either package don't break things.
55
55
  const engineRoot = path.resolve(workspaceRoot, 'packages/sootsim-engine')
56
56
  const rnShimPath = path.resolve(engineRoot, 'src/react-native/index.ts')
57
+ const rnWebShimPath = path.resolve(engineRoot, 'src/react-native-web.ts')
57
58
  const reactBridgePath = path.resolve(compatRoot, 'src/react-bridge.ts')
58
59
  const sootsimBrowserSourceAliases = [
59
60
  {
@@ -1007,7 +1008,7 @@ function sootsimConfigPlugin(
1007
1008
  })),
1008
1009
  { find: /^react-native\/Libraries\/.*/, replacement: rnDeepStubDefault },
1009
1010
  { find: 'react-native', replacement: rnShimPath },
1010
- { find: 'react-native-web', replacement: rnShimPath },
1011
+ { find: 'react-native-web', replacement: rnWebShimPath },
1011
1012
  // resolved-path alias for the synthetic specifier the
1012
1013
  // @react-navigation/native compat wrapper imports from. terminates
1013
1014
  // the wrapper's own real-package import without re-entering the