sootsim 0.1.169 → 0.1.170

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 (184) hide show
  1. package/README.md +66 -63
  2. package/detox/index.ts +3 -3
  3. package/dist-cli/bin.js +3 -3
  4. package/dist-cli/chunks/agent-3C2BKQKX.js +46 -0
  5. package/dist-cli/chunks/{agent-wrapper-Y5FQA3RL.js → agent-wrapper-OIKCYOXZ.js} +2 -2
  6. package/dist-cli/chunks/{app-fonts-FYVA4XV6.js → app-fonts-25O32NAP.js} +2 -2
  7. package/dist-cli/chunks/{assert-U64QBDM2.js → assert-ONEP4NQU.js} +2 -2
  8. package/dist-cli/chunks/beta-4E5USTEI.js +2 -0
  9. package/dist-cli/chunks/{camera-3GXBVIAM.js → camera-RORW4MAA.js} +2 -2
  10. package/dist-cli/chunks/{chunk-5C6BR24W.js → chunk-25RFZ6WP.js} +6 -6
  11. package/dist-cli/chunks/chunk-35GMFXVJ.js +2 -0
  12. package/dist-cli/chunks/{chunk-GLRFKYDV.js → chunk-3TM7DHWE.js} +2 -2
  13. package/dist-cli/chunks/{chunk-4F3BBP7B.js → chunk-4XVY5TUF.js} +2 -2
  14. package/dist-cli/chunks/{chunk-LZEL2RU3.js → chunk-5ZHUPPS5.js} +1 -1
  15. package/dist-cli/chunks/{chunk-GC4THE6K.js → chunk-6FAEORJ2.js} +2 -2
  16. package/dist-cli/chunks/{chunk-G2ZFDIPK.js → chunk-6TSXXN2W.js} +4 -4
  17. package/dist-cli/chunks/chunk-AJF3QF4K.js +1 -0
  18. package/dist-cli/chunks/{chunk-FIDPRJYH.js → chunk-AUCNBB6Q.js} +2 -2
  19. package/dist-cli/chunks/{chunk-SIB6I72P.js → chunk-BP3EGFER.js} +2 -2
  20. package/dist-cli/chunks/{chunk-OGLWXPVG.js → chunk-DAXME77O.js} +3 -3
  21. package/dist-cli/chunks/chunk-DGJTEFE7.js +13 -0
  22. package/dist-cli/chunks/chunk-DIRDEKRW.js +23 -0
  23. package/dist-cli/chunks/{chunk-3L4NRJ6C.js → chunk-DKTC53HV.js} +1 -1
  24. package/dist-cli/chunks/{chunk-KUUFY6FC.js → chunk-DWXC25XG.js} +1 -1
  25. package/dist-cli/chunks/{chunk-CR6DOLUD.js → chunk-E3FKMY5E.js} +1 -1
  26. package/dist-cli/chunks/chunk-ER4GBJRY.js +2 -0
  27. package/dist-cli/chunks/chunk-EX6V4ZV6.js +6 -0
  28. package/dist-cli/chunks/{chunk-VMYMPL6H.js → chunk-GQH3PD3W.js} +5 -5
  29. package/dist-cli/chunks/{chunk-MVOHANJG.js → chunk-GST4YSBO.js} +4 -4
  30. package/dist-cli/chunks/{chunk-BIWJE7CB.js → chunk-I4RV5I6V.js} +1 -1
  31. package/dist-cli/chunks/{chunk-Y6IDAARY.js → chunk-I5WXVKR6.js} +1 -1
  32. package/dist-cli/chunks/{chunk-HC72ILJK.js → chunk-IO7KAVFK.js} +2 -2
  33. package/dist-cli/chunks/{chunk-XEH4QGWB.js → chunk-LMX7UB5T.js} +2 -2
  34. package/dist-cli/chunks/{chunk-YADWU7OZ.js → chunk-N4YCSOCR.js} +2 -2
  35. package/dist-cli/chunks/{chunk-IODOTOOP.js → chunk-NI3L4A65.js} +2 -2
  36. package/dist-cli/chunks/{chunk-IACEJQIO.js → chunk-ODGEH6OW.js} +2 -2
  37. package/dist-cli/chunks/{chunk-DNA6VS6M.js → chunk-Q4NDYO2U.js} +1 -1
  38. package/dist-cli/chunks/{chunk-PV6MBKKE.js → chunk-QPRF3UYX.js} +2 -2
  39. package/dist-cli/chunks/{chunk-M2TMFGZH.js → chunk-QXJMXSQP.js} +1 -1
  40. package/dist-cli/chunks/chunk-RLKSLUNR.js +2 -0
  41. package/dist-cli/chunks/{chunk-WZG3YVJR.js → chunk-S244IT4W.js} +1 -1
  42. package/dist-cli/chunks/chunk-SDHOM6ZG.js +62 -0
  43. package/dist-cli/chunks/{chunk-B7BMSU7H.js → chunk-TXKOJ3P5.js} +1 -1
  44. package/dist-cli/chunks/{chunk-IBEMMV5E.js → chunk-UDMJSFSH.js} +8 -8
  45. package/dist-cli/chunks/{chunk-DQZEN256.js → chunk-ULV4PWN4.js} +1 -1
  46. package/dist-cli/chunks/{chunk-2TYEWVS4.js → chunk-V4HTQPMA.js} +1 -1
  47. package/dist-cli/chunks/chunk-VATHADSI.js +2 -0
  48. package/dist-cli/chunks/{chunk-5Q7IHJ7K.js → chunk-VUOCUCJ2.js} +2 -2
  49. package/dist-cli/chunks/{chunk-JDGLBW64.js → chunk-WN7R4GL4.js} +1 -1
  50. package/dist-cli/chunks/chunk-XOTJD6PU.js +118 -0
  51. package/dist-cli/chunks/chunk-XUNPVF5K.js +1 -0
  52. package/dist-cli/chunks/{chunk-KZIXAYN5.js → chunk-YMFSRCHL.js} +4 -4
  53. package/dist-cli/chunks/{chunk-X2HNVXNM.js → chunk-YV74D4MG.js} +2 -2
  54. package/dist-cli/chunks/{chunk-SNX3M4XD.js → chunk-YVTWSC5Y.js} +3 -3
  55. package/dist-cli/chunks/cli-version-EKKWCHRX.js +2 -0
  56. package/dist-cli/chunks/{compat-ORRWNXVT.js → compat-ZVZM5TML.js} +3 -3
  57. package/dist-cli/chunks/control-TWINYHMY.js +2 -0
  58. package/dist-cli/chunks/{cpu-profile-6PRSBZ2F.js → cpu-profile-RWGQTWLP.js} +2 -2
  59. package/dist-cli/chunks/daemon-AOSMNBD4.js +2 -0
  60. package/dist-cli/chunks/{debug-Q3J2X4WW.js → debug-R7PL3MHF.js} +5 -5
  61. package/dist-cli/chunks/{detox-DN57GMMJ.js → detox-UIJMKIGQ.js} +2 -2
  62. package/dist-cli/chunks/{device-IL5TPYGU.js → device-7SRVX2PU.js} +2 -2
  63. package/dist-cli/chunks/{diagnose-BW6QWP4F.js → diagnose-OXAKLLZT.js} +2 -2
  64. package/dist-cli/chunks/drivers-BPPG22VP.js +2 -0
  65. package/dist-cli/chunks/{electron-DGMSXQWG.js → electron-CTCH4VOL.js} +3 -3
  66. package/dist-cli/chunks/flow-KA2WCWDL.js +2 -0
  67. package/dist-cli/chunks/help-XBQIK2XY.js +2 -0
  68. package/dist-cli/chunks/{hints-TK3EHB4U.js → hints-P3ZIAP3Z.js} +2 -2
  69. package/dist-cli/chunks/home-paths-ZSCTLOYE.js +2 -0
  70. package/dist-cli/chunks/{inspect-NFWFGU4B.js → inspect-2ABU5WET.js} +24 -24
  71. package/dist-cli/chunks/{install-desktop-GB3JKSNX.js → install-desktop-4NOQ3YY4.js} +3 -3
  72. package/dist-cli/chunks/{keys-RAAHRIFH.js → keys-RPVEUNUC.js} +2 -2
  73. package/dist-cli/chunks/{login-KQ37GHDE.js → login-6D6DXBO3.js} +4 -4
  74. package/dist-cli/chunks/{logout-63SH2Y25.js → logout-T5OSWDTJ.js} +2 -2
  75. package/dist-cli/chunks/{maestro-24BT3MML.js → maestro-MKCHEOKA.js} +2 -2
  76. package/dist-cli/chunks/optional-demo-registry-EYD2K3QH.js +2 -0
  77. package/dist-cli/chunks/{preview-KQAOVN6R.js → preview-S52ZNGN2.js} +2 -2
  78. package/dist-cli/chunks/{profile-AN2B4NJT.js → profile-IG5F3LTL.js} +2 -2
  79. package/dist-cli/chunks/{react-ATGV35B4.js → react-275HCOFC.js} +2 -2
  80. package/dist-cli/chunks/{record-36662T3R.js → record-YT63SUHO.js} +4 -4
  81. package/dist-cli/chunks/runtime-WYZMDZAK.js +2 -0
  82. package/dist-cli/chunks/{screenshot-K4N5MPAT.js → screenshot-5AZ4MY3A.js} +4 -4
  83. package/dist-cli/chunks/{screenshot-mode-4QOJDLSH.js → screenshot-mode-4S4EX2XJ.js} +2 -2
  84. package/dist-cli/chunks/{screenshots-TB35YMSR.js → screenshots-UUJNZSG7.js} +2 -2
  85. package/dist-cli/chunks/{screenshots-capture-W6X33BZ4.js → screenshots-capture-I66JD3AD.js} +2 -2
  86. package/dist-cli/chunks/{server-L2PKDJZ2.js → serve-O46Q4WAD.js} +18 -17
  87. package/dist-cli/chunks/setup-C44N54O7.js +77 -0
  88. package/dist-cli/chunks/skills-PPVS4XOQ.js +40 -0
  89. package/dist-cli/chunks/{slides-TORKO36P.js → slides-ZWSZJZUY.js} +2 -2
  90. package/dist-cli/chunks/store-3SLGP5O3.js +2 -0
  91. package/dist-cli/chunks/telemetry-2XLLWARK.js +2 -0
  92. package/dist-cli/chunks/{test-5VCENVW3.js → test-362G4M5A.js} +3 -3
  93. package/dist-cli/chunks/{three-mode-ZVS2DSX6.js → three-mode-JNVTM2UT.js} +2 -2
  94. package/dist-cli/chunks/{timeline-TNC5OWLF.js → timeline-VKC6ULRK.js} +2 -2
  95. package/dist-cli/chunks/{upgrade-E7IO6A7Z.js → upgrade-SJPX4SPA.js} +2 -2
  96. package/dist-cli/chunks/upload-MXOBV6RT.js +2 -0
  97. package/dist-cli/chunks/version-4Z7AFNH3.js +4 -0
  98. package/dist-cli/chunks/web-WB2I2AWL.js +2 -0
  99. package/dist-cli/chunks/{what-happened-RMHZ7DGX.js → what-happened-KQCWMQJT.js} +3 -3
  100. package/dist-cli/chunks/{whoami-D53ANHR5.js → whoami-K76PXAD4.js} +2 -2
  101. package/dist-lib/agent-daemon-client.cjs +4 -5
  102. package/dist-lib/agent-events.cjs +1 -1
  103. package/dist-lib/agent-sessions.cjs +1 -1
  104. package/dist-lib/attached-projects.cjs +1 -1
  105. package/dist-lib/auth/shared-session.cjs +1 -1
  106. package/dist-lib/backend-origin.cjs +1 -1
  107. package/dist-lib/beta.cjs +3 -6
  108. package/dist-lib/beta.mjs +3 -5
  109. package/dist-lib/bridge-constants.cjs +1 -1
  110. package/dist-lib/cli-constants.cjs +1 -1
  111. package/dist-lib/config.cjs +1 -1
  112. package/dist-lib/detox/index.cjs +4 -4
  113. package/dist-lib/dev-bundle-resolution.cjs +2 -9
  114. package/dist-lib/home-paths.cjs +59 -1
  115. package/dist-lib/host/bridge-host.cjs +67 -5
  116. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  117. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  118. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  119. package/dist-lib/host/websocket-proxy.cjs +1 -1
  120. package/dist-lib/index.cjs +2 -2
  121. package/dist-lib/menu.cjs +1 -1
  122. package/dist-lib/menu.mjs +1 -1
  123. package/dist-lib/metro.cjs +2 -2
  124. package/dist-lib/profiles.cjs +1 -1
  125. package/dist-lib/render-mode.cjs +1 -1
  126. package/dist-lib/scripts/dev-server-scanner.cjs +5 -4
  127. package/dist-lib/sdk.cjs +1 -1
  128. package/dist-lib/sdk.mjs +1 -1
  129. package/dist-lib/skills.cjs +124 -8
  130. package/dist-lib/vite.cjs +2 -2
  131. package/package.json +2 -1
  132. package/scripts/dev-server-scanner.ts +3 -4
  133. package/scripts/postinstall.cjs +2 -2
  134. package/scripts/preuninstall.cjs +5 -7
  135. package/skills/soot.md +178 -0
  136. package/skills/sootsim-debug.md +384 -0
  137. package/skills/sootsim-setup.md +96 -0
  138. package/skills/sootsim-test.md +292 -0
  139. package/skills/sootsim-visual.md +122 -0
  140. package/src/agent-daemon-client.ts +4 -14
  141. package/src/beta.ts +3 -5
  142. package/src/dev-bundle-resolution.ts +1 -14
  143. package/src/home-paths.ts +77 -2
  144. package/src/host/agent-host.ts +1 -1
  145. package/src/host/bridge-host.ts +65 -6
  146. package/src/metro-plugin.ts +1 -1
  147. package/src/runtime-assets.ts +1 -1
  148. package/src/vite-plugin.ts +1 -0
  149. package/dist-cli/chunks/agent-V3GVCRPL.js +0 -46
  150. package/dist-cli/chunks/auto-bootstrap-YXDP3WKH.js +0 -2
  151. package/dist-cli/chunks/beta-FGSMZWLP.js +0 -2
  152. package/dist-cli/chunks/chunk-2IFWVIKM.js +0 -2
  153. package/dist-cli/chunks/chunk-3T36XLP3.js +0 -119
  154. package/dist-cli/chunks/chunk-5KY7TQ42.js +0 -5
  155. package/dist-cli/chunks/chunk-BQOMDL4K.js +0 -1
  156. package/dist-cli/chunks/chunk-FHMPJZKM.js +0 -7
  157. package/dist-cli/chunks/chunk-GQAWFQNN.js +0 -23
  158. package/dist-cli/chunks/chunk-HJTKKF7M.js +0 -13
  159. package/dist-cli/chunks/chunk-K4JAWKOY.js +0 -1
  160. package/dist-cli/chunks/chunk-NXGQS7IR.js +0 -2
  161. package/dist-cli/chunks/chunk-R2INPDGW.js +0 -1
  162. package/dist-cli/chunks/chunk-SLL5SZN4.js +0 -2
  163. package/dist-cli/chunks/chunk-XM66K64H.js +0 -79
  164. package/dist-cli/chunks/cli-version-3L6FFDIX.js +0 -2
  165. package/dist-cli/chunks/config-5IB6ONJ2.js +0 -45
  166. package/dist-cli/chunks/control-BM4V27OB.js +0 -2
  167. package/dist-cli/chunks/daemon-62MZT4G4.js +0 -67
  168. package/dist-cli/chunks/drivers-V6QDFZQD.js +0 -2
  169. package/dist-cli/chunks/flow-VBM6TU32.js +0 -2
  170. package/dist-cli/chunks/help-XWKNL743.js +0 -2
  171. package/dist-cli/chunks/home-paths-G23H3RO6.js +0 -2
  172. package/dist-cli/chunks/install-5YL7FITA.js +0 -2
  173. package/dist-cli/chunks/launch-DYFE4CAD.js +0 -16
  174. package/dist-cli/chunks/optional-demo-registry-JJXPB3UD.js +0 -2
  175. package/dist-cli/chunks/runtime-NRMC3U3D.js +0 -2
  176. package/dist-cli/chunks/runtime-delivery-RO6CTYPQ.js +0 -2
  177. package/dist-cli/chunks/setup-repo-AK6QOUE7.js +0 -2
  178. package/dist-cli/chunks/skills-KDSOWUOE.js +0 -10
  179. package/dist-cli/chunks/start-GH2AOUX6.js +0 -23
  180. package/dist-cli/chunks/store-JHW65VGQ.js +0 -2
  181. package/dist-cli/chunks/telemetry-SZLRXJXW.js +0 -2
  182. package/dist-cli/chunks/upload-KKEWETA5.js +0 -2
  183. package/dist-cli/chunks/version-37OD33SE.js +0 -4
  184. package/dist-cli/chunks/web-4JVMIBTB.js +0 -2
@@ -0,0 +1,292 @@
1
+ ---
2
+ name: sootsim-test
3
+ description: Run automated tests against your React Native app under sootsim — native YAML flows, Maestro drop-in, or Detox drop-in
4
+ ---
5
+
6
+ # sootsim test
7
+
8
+ one skill for "run automated tests against sootsim". three runtimes share
9
+ the same shell and the same auto-settling contract; pick the one that
10
+ matches your existing toolchain and stay with it.
11
+
12
+ > needs a connected, pinned sim. if `sootsim describe` errors, load
13
+ > `/sootsim-setup` first.
14
+
15
+ ## route in (decision tree)
16
+
17
+ - have an existing `.maestro/` directory? → [maestro drop-in](#branch--maestro-drop-in)
18
+ - have existing Detox specs? → [detox drop-in](#branch--detox-drop-in)
19
+ - writing tests fresh, or want the recorder? → [native YAML flows](#branch--native-yaml-flows)
20
+
21
+ each runtime accepts the same compat surface — flows author once, run
22
+ many ways. start with whichever your team already uses; you don't pay a
23
+ migration tax to try sootsim.
24
+
25
+ ## anti-patterns (apply to every runtime)
26
+
27
+ - **`sleep` is almost always a bug.** the underlying CLI auto-settles
28
+ before reads and polls layout hash after writes. if a flow fails
29
+ without a `sleep`, the fix is `waitFor`, `waitForAnimationToEnd`, or
30
+ `wait idle` — not a blind timer. the one legitimate case: a
31
+ user-controlled debounce ("press and hold for 2s").
32
+ - **prefer testID (`tapId`, `by.id(testID)`) over text.** text breaks
33
+ under i18n, copy churn, and duplicate strings. add `testID` during
34
+ implementation and author flows against it.
35
+ - **pin a sim before running a suite.** parallel CI jobs colliding
36
+ on the default bridge port is the #1 flake source. pass `--port` in
37
+ CI and `sootsim use <id>` locally.
38
+ - **every flow ends with an assert, not a screenshot.** a screenshot
39
+ passes as long as the flow didn't throw. `assertVisible` (or
40
+ `expect(...).toBeVisible()`) actually verifies the happy path.
41
+ - **don't run a 40-step "smoke everything" flow.** when it fails, you
42
+ can't tell what regressed. split into `login.yaml`, `compose.yaml`,
43
+ `settings.yaml`, chain with `runFlow`.
44
+
45
+ ## you're done when
46
+
47
+ - the suite passes locally and in CI
48
+ - every flake is either fixed or marked `xfail` with a written reason
49
+ and a removal date
50
+ - the green run took noticeably less wall-clock than the previous
51
+ baseline (a suite that grows slower without growing wider is a smell)
52
+
53
+ ## branch — native YAML flows
54
+
55
+ write declarative YAML against sootsim's maestro-compatible verb set.
56
+ flows are portable, readable, and can record video for failing CI runs.
57
+
58
+ ```sh
59
+ sootsim flow flows/login.yaml # run a flow
60
+ sootsim flow flows/login.yaml --video # record webm
61
+ sootsim flow flows/login.yaml --profile # include perf stats per step
62
+ sootsim flow flows/ # every flow in a directory
63
+ ```
64
+
65
+ the recorder is the killer feature for writing a flow from a live
66
+ interaction — author by demonstration:
67
+
68
+ ```sh
69
+ sootsim flow start # begin recording a draft
70
+ sootsim do tap-id loginButton # drive the app via the CLI
71
+ sootsim flow keep # save the step
72
+ sootsim do type "user@example.com"
73
+ sootsim flow keep
74
+ sootsim do tap-id submit
75
+ sootsim flow keep
76
+ sootsim flow end --output flows/login.yaml
77
+ ```
78
+
79
+ the recorder emits minimal YAML — no coordinate dumps, no `sleep` noise —
80
+ because the underlying CLI already settled between every action.
81
+
82
+ exemplar flow:
83
+
84
+ ```yaml
85
+ # flows/login.yaml
86
+ - launchApp: {}
87
+
88
+ - waitFor:
89
+ text: 'Sign in'
90
+ timeout: 10000
91
+
92
+ - tapId: emailInput
93
+ - typeText: '${USERNAME}'
94
+
95
+ - tapId: passwordInput
96
+ - typeText: '${PASSWORD}'
97
+
98
+ - tapId: signInButton
99
+ - waitForAnimationToEnd: true
100
+
101
+ - assertVisible: 'Welcome back'
102
+ ```
103
+
104
+ run with env interpolation:
105
+
106
+ ```sh
107
+ sootsim flow flows/login.yaml \
108
+ --env USERNAME=alice \
109
+ --env PASSWORD=hunter2 \
110
+ --video --profile
111
+ ```
112
+
113
+ automatic settling inside flows: every tap, type, scroll, swipe, and
114
+ drag is backed by the same primitives that auto-wait for transitions on
115
+ the way in and layout stability on the way out. you rarely need
116
+ explicit waits; when you do, `waitForAnimationToEnd: true` maps to the
117
+ CLI's `wait idle` with a generous budget. use it before any
118
+ `assertVisible` that depends on a transition longer than ~1.2 s.
119
+
120
+ generating a starter flow by crawling the app:
121
+
122
+ ```sh
123
+ sootsim flow generate --max-screens 20
124
+ ```
125
+
126
+ useful as a parity-coverage starting point; always review before
127
+ committing — generated flows are conservative and skip assertions.
128
+
129
+ ## branch — maestro drop-in
130
+
131
+ if you already have a `.maestro/` directory, the migration is one
132
+ command change:
133
+
134
+ ```sh
135
+ # before
136
+ maestro test .maestro/login.yaml
137
+
138
+ # after
139
+ sootsim maestro test .maestro/login.yaml
140
+ ```
141
+
142
+ no test rewrites, no detox config, no simulator. the runtime auto-launches
143
+ a sootsim shell if one isn't running.
144
+
145
+ ```sh
146
+ sootsim maestro # auto-discover .maestro/ or maestro/
147
+ sootsim maestro test .maestro/login.yaml
148
+ sootsim maestro test .maestro/ # every flow in a directory
149
+ sootsim maestro init # scaffold a starter flow
150
+ sootsim maestro --list-compat # supported / partial / unsupported
151
+ sootsim maestro --env USERNAME=alice test .maestro/login.yaml
152
+ ```
153
+
154
+ ### supported verbs
155
+
156
+ `launchApp`, `stopApp`, `clearState`, `tapOn`, `tapAtCoords`, `inputText`,
157
+ `pressKey`, `dispatchKey`, `hideKeyboard`, `eraseText`, `assertVisible`,
158
+ `assertNotVisible`, `assertTreeContains`, `waitFor`, `extendedWaitUntil`,
159
+ `waitForAnimationToEnd`, `scroll`, `scrollUntilVisible`, `scrollTo`,
160
+ `swipe`, `pinch`, `takeScreenshot`, `dumpTree`, `back`, `repeat.times`,
161
+ `runFlow`, `when` (`visible` / `notVisible` / `platform` / `true`),
162
+ `optional: true`, `onFlowStart` / `onFlowComplete`, `copyTextFrom` +
163
+ `${maestroCopiedText}`, `evalScript`, `openLink`, env var `${NAME}`
164
+ interpolation.
165
+
166
+ ### partial
167
+
168
+ - `clearKeychain` — no keychain in sootsim; logs a warning and continues.
169
+ if a test depends on keychain state being reset, add an explicit
170
+ in-app "sign out" step instead.
171
+ - `when.platform` — sootsim emulates iOS; android branches are skipped.
172
+ - `openLink` — navigates `window.location`; no OS-level app routing.
173
+
174
+ ### not yet implemented
175
+
176
+ these throw `unsupported maestro verb: X`:
177
+
178
+ - `travel`, `setLocation`, `setAirplaneMode`, `killApp`
179
+ - `addMedia`
180
+ - `repeat.while` (use `repeat.times`)
181
+
182
+ ### troubleshooting
183
+
184
+ - **"no maestro flows found"** — pass an explicit path
185
+ (`sootsim maestro test path/to/flow.yaml`) or run `sootsim maestro
186
+ init` to scaffold one.
187
+ - **"missing environment variable for flow placeholder: FOO"** — pass
188
+ `--env FOO=bar` or export it. pick one mechanism per repo and document
189
+ it in the flow directory's README.
190
+ - **flow can't find an element** — sootsim's matcher uses testID + visible
191
+ text. UIKit-only labels that real maestro scrapes from the host OS won't
192
+ resolve here; switch to testID.
193
+ - **flow runs but nothing renders** — confirm the bundler your flow
194
+ targets is reachable and pass `--url <port>` or add `app: <port>`
195
+ frontmatter at the top of the YAML.
196
+
197
+ ## branch — detox drop-in
198
+
199
+ an existing detox suite runs against sootsim with **one line** of jest
200
+ config and zero test changes.
201
+
202
+ `import { by, device, element, expect, waitFor } from 'detox'` is
203
+ rewritten by jest to a sootsim-backed driver that drives the real shell
204
+ over playwright. matchers, assertions, and the `waitFor` API match the
205
+ upstream detox surface.
206
+
207
+ ### one-line install
208
+
209
+ ```js
210
+ // sootsim-detox.config.cjs
211
+ module.exports = {
212
+ preset: 'sootsim/detox/jest-preset',
213
+ rootDir: __dirname,
214
+ testMatch: ['<rootDir>/e2e/**/*.test.ts'],
215
+ }
216
+ ```
217
+
218
+ or, if you already have a jest config, pull in just the module mapper:
219
+
220
+ ```js
221
+ moduleNameMapper: {
222
+ '^detox$': require.resolve('sootsim/detox'),
223
+ }
224
+ ```
225
+
226
+ keep the **preset**, not just the mapper — the preset wires up the
227
+ global setup/teardown that tears the shell down between suites. mapper-only
228
+ works until your first hung shell.
229
+
230
+ ### commands
231
+
232
+ ```sh
233
+ sootsim detox # run e2e/, test/e2e/, or detox/
234
+ sootsim detox --config my.cjs # use a specific jest config
235
+ sootsim detox --watch # jest watch mode
236
+ sootsim detox -t "login" # --testNamePattern
237
+ sootsim detox --headed # keep the shell window visible
238
+ sootsim detox --no-launch # don't auto-launch a shell
239
+ sootsim detox --port 5173 # explicit bridge port for CI
240
+ sootsim detox init # scaffold config + e2e/example.test.ts
241
+ ```
242
+
243
+ ### compat surface
244
+
245
+ | detox API | sootsim |
246
+ | ---------------------------------- | ------- |
247
+ | `element(by.id(...))` | supported |
248
+ | `element(by.text(...))` | supported — but prefer `by.id` |
249
+ | `element(by.label(...))` | supported |
250
+ | `element(by.traits([...]))` | supported for the iOS trait set |
251
+ | `.toBeVisible()` / `.not.toBeVisible()` | supported |
252
+ | `.toHaveText(...)` / `.toHaveLabel(...)` | supported |
253
+ | `.tap()` / `.longPress()` | supported |
254
+ | `.typeText(...)` / `.replaceText(...)` | supported |
255
+ | `.scroll(...)` / `.scrollTo(...)` | supported |
256
+ | `.swipe(...)` | supported |
257
+ | `waitFor(...).toBeVisible().withTimeout(...)` | supported |
258
+ | `device.launchApp(...)` / `device.reloadReactNative()` | supported |
259
+ | `device.shake()` | partial — emits the JS event, no haptic |
260
+ | `device.sendUserNotification(...)` | not yet |
261
+
262
+ ### troubleshooting
263
+
264
+ - **"no sootsim shell reachable"** — start one yourself
265
+ (`sootsim open <port>`) or pass `--port <n>`.
266
+ - **"jest cannot find module 'detox'"** — the preset isn't wired. confirm
267
+ `preset: 'sootsim/detox/jest-preset'` is in the jest config jest
268
+ actually loaded (`--config` overrides package.json).
269
+ - **matchers fail on elements that exist** — canvas has no DOM. use
270
+ `by.id` (testID); `sootsim describe` shows what the shell sees.
271
+ - **passes locally, flakes in CI** — the shell boots asynchronously.
272
+ bump jest `testTimeout` to 10–15 s and add a `waitFor(element(...))
273
+ .toBeVisible().withTimeout(10000)` before the first assertion.
274
+ - **port conflicts under parallel CI** — pass `--port` explicitly per
275
+ job so workers don't collide on the default bridge port.
276
+
277
+ ## recovery — common failure modes (all runtimes)
278
+
279
+ - **"no flows found"** — confirm working directory and explicit path;
280
+ some runtimes look at `.maestro/` while others look at `maestro/` or
281
+ `e2e/`.
282
+ - **"missing env variable"** — set with `--env` (or jest env config) and
283
+ document the variable list in the flow directory's README so new
284
+ contributors aren't guessing.
285
+ - **CI flakes but local passes** — bump test timeouts to 10–15 s before
286
+ the first assertion, pin the bridge port, and run with `--video` so
287
+ failing runs land a webm next to the junit output.
288
+ - **a flake reproduces but resists fixing** — don't paper over with
289
+ `sleep`. open `/sootsim-debug` against the same sim, capture
290
+ the timeline (`sootsim timeline start` → reproduce → `sootsim
291
+ what-happened`), and look for the missing animation-completion or
292
+ the unresolved fetch that's racing the assertion.
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: sootsim-visual
3
+ description: Pixel-level rendering comparison between sootsim and native iOS — fonts, gradients, shadows, image fidelity
4
+ ---
5
+
6
+ # sootsim visual
7
+
8
+ pixel-level rendering fidelity: does the sootsim canvas match native iOS
9
+ for text, gradients, shadows, blur, and image rendering? the `sootsim`
10
+ CLI gives you two primitives — **canvas capture** (`sootsim screenshot`)
11
+ and **structural diff** (`sootsim debug snapshot`/`diff`). pixel
12
+ comparison itself is "capture both sides, diff the PNGs with a standard
13
+ image tool" — there is no single oracle-diff command, and that's fine:
14
+ the capture step is the hard part and it's fully reproducible.
15
+
16
+ > needs a connected, pinned sim. if `sootsim describe` errors,
17
+ > load `/sootsim-setup` first.
18
+
19
+ ## route in
20
+
21
+ - structural change ("which nodes / which props changed?") → load
22
+ `/sootsim-debug`, debugging branch — `debug snapshot` + `debug
23
+ diff` is faster, deterministic, and tells you *why*.
24
+ - visual change (color, gradient, shadow, font metric, blur, image)? →
25
+ continue here.
26
+ - comparing sootsim to your iOS simulator for fidelity? → continue.
27
+ - regressing your own app against itself across commits? → continue.
28
+
29
+ ## anti-patterns (read first)
30
+
31
+ - **run structural diff before pixel work.** `sootsim debug snapshot` /
32
+ `debug diff` (in `/sootsim-debug`) is cheaper, deterministic, and
33
+ points at the responsible nodes. only when it's empty (or the change is
34
+ pure rendering) does a pixel comparison add information.
35
+ - **don't single-screenshot animated surfaces.** a gradient
36
+ mid-animation never matches. gate the capture behind `sootsim wait
37
+ idle` first.
38
+ - **a > 5% pixel threshold hides regressions.** ~1% is the right
39
+ ballpark for catching real font / shadow / gradient drift without
40
+ flagging anti-alias noise. go higher only with a written reason.
41
+ - **pin device and theme.** capture both sides on the same simulated
42
+ device and appearance, or you're diffing layout, not rendering. use
43
+ `--themes` to capture light and dark together.
44
+ - **commit baselines alongside the code change that produced them.**
45
+ baseline churn without code churn means someone captured noise.
46
+
47
+ ## capturing — `sootsim screenshot`
48
+
49
+ fast, no extra setup, works on any loaded app:
50
+
51
+ ```sh
52
+ sootsim screenshot --output app.png # full canvas
53
+ sootsim screenshot --area 0,200,393,400 # crop to a logical rect
54
+ sootsim screenshot --id loginButton -o btn.png # crop to a node by testID
55
+ sootsim screenshot --text "Sign in" # crop to a node by text
56
+ sootsim screenshot --gallery --themes # crawl screens → HTML gallery, light+dark
57
+ sootsim screenshot --no-shell -o tenant.png # tenant surface only, no iOS chrome
58
+ sootsim screenshot --shell-only # only the simulated iOS chrome
59
+ ```
60
+
61
+ `--gallery` crawls reachable screens and writes a browsable HTML gallery
62
+ — the fastest way to eyeball a whole app for surprise rendering changes.
63
+
64
+ ## comparing pixels
65
+
66
+ capture both sides to PNGs, then diff with any standard image tool.
67
+
68
+ **sootsim vs sootsim (regression across your own commits):**
69
+
70
+ ```sh
71
+ sootsim screenshot -o before.png # baseline, before your change
72
+ # … make the rendering change, let the sim hot-reload …
73
+ sootsim screenshot -o after.png # after your change
74
+ npx pixelmatch before.png after.png diff.png 0.1 # writes diff.png
75
+ # or: magick compare before.png after.png diff.png
76
+ ```
77
+
78
+ **sootsim vs native iOS (fidelity to the real thing):**
79
+
80
+ ```sh
81
+ # native side — capture from your booted iOS simulator
82
+ xcrun simctl io booted screenshot native.png
83
+ # sootsim side — same screen, same appearance
84
+ sootsim screenshot -o sootsim.png
85
+ npx pixelmatch native.png sootsim.png diff.png 0.1
86
+ ```
87
+
88
+ crop both to the same region (`--area` / `--id`) when you only care
89
+ about one element — a full-screen diff buries a 3px font-baseline shift
90
+ under unrelated chrome.
91
+
92
+ ## typical workflow
93
+
94
+ 1. structural diff first — confirm the change is genuinely visual.
95
+ 2. capture the affected screen with `sootsim screenshot` (crop to the
96
+ element if you can).
97
+ 3. diff against the baseline (previous commit, or a native capture).
98
+ 4. iterate the rendering until the diff is under threshold.
99
+ 5. open the gallery (`--gallery`) and scroll through every screen — a red
100
+ patch on one icon is easy to miss when you're focused on one case.
101
+
102
+ ## you're done when
103
+
104
+ - the diff is under threshold on every affected case, **or**
105
+ - the differences are explicitly accepted, baselines re-captured, and
106
+ both halves (code + baselines) committed together.
107
+ - the gallery shows no surprise changes when you scroll through every
108
+ screen, not just the one you targeted.
109
+
110
+ ## recovery — common failure modes
111
+
112
+ - **a single icon/glyph diffs** — font-load race or image not yet
113
+ cached. recapture after `sootsim wait idle`, or navigate away and back
114
+ to seed the cache.
115
+ - **everything diffs by a constant offset** — you captured the two sides
116
+ on different devices or appearances. pin both, recapture.
117
+ - **baselines churn without a code change** — someone captured noise (a
118
+ lingering badge, half-open keyboard, a drifting clock). revert and
119
+ recapture from a clean, idle state.
120
+ - **persistent delta on the same font / shadow / gradient across
121
+ unrelated screens** — that's an engine-level rendering fidelity issue,
122
+ not your app. report it with the affected captures attached.
@@ -15,7 +15,7 @@ import net from 'node:net'
15
15
  import { WebSocket } from 'ws'
16
16
  import { resolveSootsimInvocation, type Provider } from './agent-sessions.ts'
17
17
  import { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants.ts'
18
- import { isDevWorkstation, shouldSkipPersistentDaemon } from './home-paths.ts'
18
+ import { shouldSkipPersistentDaemon } from './home-paths.ts'
19
19
  import type { AgentEvent } from './agent-events.ts'
20
20
  import type { AgentPromptEnvelope } from './agent-prompt.ts'
21
21
  import type { AgentSession, AttachedProject } from './attached-projects.ts'
@@ -346,24 +346,14 @@ export async function ensureDaemonRunning(
346
346
  if (await isBridgeUp(port)) {
347
347
  return { alreadyRunning: true }
348
348
  }
349
- // never spawn a standalone daemon on a dev host. on a dev workstation the
350
- // live `bun dev` stack already owns the bridge in-process; spawning one here
351
- // lands it on a fallback port serving the published runtime, which shadows
352
- // the dev build (the :7669 / 0.1.310 footgun). agents must connect to the
353
- // live bridge, not resurrect a stale one. override: SOOTSIM_FORCE_DAEMON_INSTALL=1.
354
349
  if (shouldSkipPersistentDaemon()) {
355
- const why = isDevWorkstation()
356
- ? `this is a dev workstation (IS_TAMAGUI_DEV=1)`
357
- : `this is a dev checkout`
358
350
  throw new AgentDaemonError(
359
- `no sootsim bridge on port ${port} and refusing to spawn a standalone daemon ${why}. ` +
360
- `start the live \`bun dev\` stack so the bridge is up, or set ` +
361
- `SOOTSIM_FORCE_DAEMON_INSTALL=1 to spawn one here on purpose.`,
351
+ `no sootsim bridge on port ${port}. run \`sootsim setup\` to install the background daemon, or \`sootsim serve\` in another shell.`,
362
352
  'DEV_HOST_REFUSED',
363
353
  )
364
354
  }
365
355
  const { cmd, prefixArgs } = resolveSootsimInvocation()
366
- const args = [...prefixArgs, 'server', '--quiet']
356
+ const args = [...prefixArgs, 'serve', '--quiet']
367
357
  if (port !== DEFAULT_SOOTSIM_BRIDGE_PORT) {
368
358
  args.push('--port', String(port))
369
359
  }
@@ -383,7 +373,7 @@ export async function ensureDaemonRunning(
383
373
  }
384
374
  throw new AgentDaemonError(
385
375
  `spawned sootsim daemon on port ${port} but it did not come up in time. ` +
386
- `run \`sootsim server\` manually to diagnose.`,
376
+ `run \`sootsim serve\` manually to diagnose.`,
387
377
  'SPAWN_TIMEOUT',
388
378
  )
389
379
  }
package/src/beta.ts CHANGED
@@ -7,15 +7,13 @@
7
7
 
8
8
  export const IS_BETA = true
9
9
 
10
- export const BETA_VERSION_TARGET = '1.0.0'
11
-
12
10
  // short label — used on the rail pill, electron title, etc.
13
11
  export const BETA_LABEL = 'public beta'
14
12
 
15
- // one-line tagline — used in the CLI banner + privacy disclosures.
16
- export const BETA_TAGLINE = `${BETA_LABEL} · free until ${BETA_VERSION_TARGET}`
13
+ // one-line tagline — used in the CLI banner.
14
+ export const BETA_TAGLINE = BETA_LABEL
17
15
 
18
16
  // what we ask for in return — kept here so the onboarding flow, the
19
17
  // privacy page, and any future surface render the same words.
20
18
  export const BETA_ASK_HEADLINE =
21
- 'free for everyone until 1.0 — we send anonymous error and dependency data while you use it'
19
+ 'we send anonymous error and dependency data while you use it'
@@ -202,20 +202,7 @@ export async function resolveConnectionInput(input: string): Promise<ResolvedDev
202
202
  const port = parseInt(trimmed, 10)
203
203
  const resolved = await probePortBundle(port)
204
204
  if (resolved) return resolved
205
- // soot's demo launcher allocates ports via `preferredPort` + shift if the
206
- // preferred port is in use, so a flow YAML that hardcodes `app: 8081`
207
- // breaks the day someone else binds 8081 first. probe a small window of
208
- // higher ports for a metro server before giving up. only the *first* hit
209
- // is used — we never scan past 3 to keep the search bounded and avoid
210
- // grabbing a completely unrelated dev server.
211
- const SHIFT_WINDOW = 3
212
- for (let offset = 1; offset <= SHIFT_WINDOW; offset++) {
213
- const shifted = await probePortBundle(port + offset)
214
- if (shifted) return shifted
215
- }
216
- throw buildUnresolvedTargetError(
217
- `localhost:${port} (also scanned +1..+${SHIFT_WINDOW})`,
218
- )
205
+ throw buildUnresolvedTargetError(`localhost:${port}`)
219
206
  }
220
207
 
221
208
  const bundleUrl = trimmed.startsWith('http') ? trimmed : `http://${trimmed}`
package/src/home-paths.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  // ├── cache/
15
15
  // │ └── sootsim-runtime-<version>.tar.gz
16
16
  // ├── daemon.json lockfile: pid, ports, active runtime, heartbeat
17
+ // ├── dev-bridge.json live Vite dev bridge pointer for local CLI commands
17
18
  // └── config.json user prefs: update channel, cdn origin override
18
19
 
19
20
  import fs from 'node:fs'
@@ -23,6 +24,7 @@ import path from 'node:path'
23
24
  export const SOOTSIM_HOME_ENV = 'SOOTSIM_HOME'
24
25
  export const ACTIVE_RUNTIME_FILE = 'active'
25
26
  export const DAEMON_LOCKFILE = 'daemon.json'
27
+ export const DEV_BRIDGE_LOCKFILE = 'dev-bridge.json'
26
28
  export const CONFIG_FILE = 'config.json'
27
29
  export const DAEMON_HEARTBEAT_STALE_MS = 30_000
28
30
 
@@ -84,8 +86,8 @@ export function isDevWorkstation(): boolean {
84
86
  // the persistent launchd / systemd daemon must never auto-install or run on a
85
87
  // dev checkout or a dev workstation. either makes the background agent point at
86
88
  // moving workspace artifacts / dev env and serve stale assets that shadow the
87
- // live dev stack. install, auto-bootstrap, and the launchd-spawned `sootsim
88
- // server` boot all consult this; a foreground `sootsim server` the dev starts
89
+ // live dev stack. setup and the launchd-spawned `sootsim serve` boot consult
90
+ // this; a foreground `sootsim serve` the dev starts
89
91
  // by hand is intentional and stays allowed.
90
92
  export function shouldSkipPersistentDaemon(): boolean {
91
93
  return isSootsimDevCheckout() || isDevWorkstation()
@@ -145,6 +147,10 @@ export function daemonLockfilePath(): string {
145
147
  return path.join(sootsimHomeDir(), DAEMON_LOCKFILE)
146
148
  }
147
149
 
150
+ export function devBridgeLockfilePath(): string {
151
+ return path.join(sootsimHomeDir(), DEV_BRIDGE_LOCKFILE)
152
+ }
153
+
148
154
  export function configFilePath(): string {
149
155
  return path.join(sootsimHomeDir(), CONFIG_FILE)
150
156
  }
@@ -320,6 +326,18 @@ export interface DaemonLockfile {
320
326
  bootstrapping?: boolean
321
327
  }
322
328
 
329
+ export interface DevBridgeLockfile {
330
+ schema: 1
331
+ pid: number
332
+ platform: NodeJS.Platform
333
+ bridgePort: number
334
+ runtimePort: number
335
+ cwd: string
336
+ startedAt: number
337
+ heartbeatAt: number
338
+ source: 'vite-dev'
339
+ }
340
+
323
341
  const DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024
324
342
 
325
343
  export function readDaemonLockfile(): DaemonLockfile | null {
@@ -352,6 +370,36 @@ export function readDaemonLockfile(): DaemonLockfile | null {
352
370
  }
353
371
  }
354
372
 
373
+ export function readDevBridgeLockfile(): DevBridgeLockfile | null {
374
+ try {
375
+ const fd = fs.openSync(devBridgeLockfilePath(), 'r')
376
+ try {
377
+ const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES)
378
+ const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0)
379
+ const raw = buf.subarray(0, bytesRead).toString('utf8')
380
+ const parsed = JSON.parse(raw) as Partial<DevBridgeLockfile>
381
+ if (
382
+ parsed &&
383
+ parsed.schema === 1 &&
384
+ parsed.source === 'vite-dev' &&
385
+ typeof parsed.pid === 'number' &&
386
+ typeof parsed.bridgePort === 'number' &&
387
+ typeof parsed.runtimePort === 'number' &&
388
+ typeof parsed.cwd === 'string' &&
389
+ typeof parsed.startedAt === 'number' &&
390
+ typeof parsed.heartbeatAt === 'number'
391
+ ) {
392
+ return parsed as DevBridgeLockfile
393
+ }
394
+ return null
395
+ } finally {
396
+ fs.closeSync(fd)
397
+ }
398
+ } catch {
399
+ return null
400
+ }
401
+ }
402
+
355
403
  /** true when the lockfile exists, the named pid is alive, and the heartbeat
356
404
  * is recent. callers should reach for this before trusting any of the
357
405
  * ports inside.
@@ -379,6 +427,20 @@ export function isDaemonLockfileFresh(
379
427
  }
380
428
  }
381
429
 
430
+ export function isDevBridgeLockfileFresh(
431
+ lock: DevBridgeLockfile | null,
432
+ now = Date.now(),
433
+ ): lock is DevBridgeLockfile {
434
+ if (!lock) return false
435
+ if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false
436
+ try {
437
+ process.kill(lock.pid, 0)
438
+ return true
439
+ } catch {
440
+ return false
441
+ }
442
+ }
443
+
382
444
  export function writeDaemonLockfile(data: DaemonLockfile): void {
383
445
  ensureSootsimHome()
384
446
  const tmp = `${daemonLockfilePath()}.tmp`
@@ -386,6 +448,13 @@ export function writeDaemonLockfile(data: DaemonLockfile): void {
386
448
  fs.renameSync(tmp, daemonLockfilePath())
387
449
  }
388
450
 
451
+ export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
452
+ ensureSootsimHome()
453
+ const tmp = `${devBridgeLockfilePath()}.tmp`
454
+ fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
455
+ fs.renameSync(tmp, devBridgeLockfilePath())
456
+ }
457
+
389
458
  /** try to claim the lockfile atomically on daemon boot. returns true if
390
459
  * we now own it. returns false when another fresh daemon beat us to the
391
460
  * punch — callers should refuse to start. a stale lockfile (dead pid or
@@ -406,3 +475,9 @@ export function removeDaemonLockfile(): void {
406
475
  fs.unlinkSync(daemonLockfilePath())
407
476
  } catch {}
408
477
  }
478
+
479
+ export function removeDevBridgeLockfile(): void {
480
+ try {
481
+ fs.unlinkSync(devBridgeLockfilePath())
482
+ } catch {}
483
+ }
@@ -61,7 +61,7 @@ interface PendingPromptEcho {
61
61
  export interface AgentHostOptions {
62
62
  /** callback so hosts with a soot-env context (electron, dev middleware)
63
63
  * can inject their own exclude list. defaults to the env-derived list
64
- * the rest of the repo uses, so `sootsim server` standalone does the
64
+ * the rest of the repo uses, so `sootsim serve` standalone does the
65
65
  * right thing. */
66
66
  getExcludePorts?: () => number[]
67
67
  }