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.
- package/README.md +66 -63
- package/detox/index.ts +3 -3
- package/dist-cli/bin.js +3 -3
- package/dist-cli/chunks/agent-3C2BKQKX.js +46 -0
- package/dist-cli/chunks/{agent-wrapper-Y5FQA3RL.js → agent-wrapper-OIKCYOXZ.js} +2 -2
- package/dist-cli/chunks/{app-fonts-FYVA4XV6.js → app-fonts-25O32NAP.js} +2 -2
- package/dist-cli/chunks/{assert-U64QBDM2.js → assert-ONEP4NQU.js} +2 -2
- package/dist-cli/chunks/beta-4E5USTEI.js +2 -0
- package/dist-cli/chunks/{camera-3GXBVIAM.js → camera-RORW4MAA.js} +2 -2
- package/dist-cli/chunks/{chunk-5C6BR24W.js → chunk-25RFZ6WP.js} +6 -6
- package/dist-cli/chunks/chunk-35GMFXVJ.js +2 -0
- package/dist-cli/chunks/{chunk-GLRFKYDV.js → chunk-3TM7DHWE.js} +2 -2
- package/dist-cli/chunks/{chunk-4F3BBP7B.js → chunk-4XVY5TUF.js} +2 -2
- package/dist-cli/chunks/{chunk-LZEL2RU3.js → chunk-5ZHUPPS5.js} +1 -1
- package/dist-cli/chunks/{chunk-GC4THE6K.js → chunk-6FAEORJ2.js} +2 -2
- package/dist-cli/chunks/{chunk-G2ZFDIPK.js → chunk-6TSXXN2W.js} +4 -4
- package/dist-cli/chunks/chunk-AJF3QF4K.js +1 -0
- package/dist-cli/chunks/{chunk-FIDPRJYH.js → chunk-AUCNBB6Q.js} +2 -2
- package/dist-cli/chunks/{chunk-SIB6I72P.js → chunk-BP3EGFER.js} +2 -2
- package/dist-cli/chunks/{chunk-OGLWXPVG.js → chunk-DAXME77O.js} +3 -3
- package/dist-cli/chunks/chunk-DGJTEFE7.js +13 -0
- package/dist-cli/chunks/chunk-DIRDEKRW.js +23 -0
- package/dist-cli/chunks/{chunk-3L4NRJ6C.js → chunk-DKTC53HV.js} +1 -1
- package/dist-cli/chunks/{chunk-KUUFY6FC.js → chunk-DWXC25XG.js} +1 -1
- package/dist-cli/chunks/{chunk-CR6DOLUD.js → chunk-E3FKMY5E.js} +1 -1
- package/dist-cli/chunks/chunk-ER4GBJRY.js +2 -0
- package/dist-cli/chunks/chunk-EX6V4ZV6.js +6 -0
- package/dist-cli/chunks/{chunk-VMYMPL6H.js → chunk-GQH3PD3W.js} +5 -5
- package/dist-cli/chunks/{chunk-MVOHANJG.js → chunk-GST4YSBO.js} +4 -4
- package/dist-cli/chunks/{chunk-BIWJE7CB.js → chunk-I4RV5I6V.js} +1 -1
- package/dist-cli/chunks/{chunk-Y6IDAARY.js → chunk-I5WXVKR6.js} +1 -1
- package/dist-cli/chunks/{chunk-HC72ILJK.js → chunk-IO7KAVFK.js} +2 -2
- package/dist-cli/chunks/{chunk-XEH4QGWB.js → chunk-LMX7UB5T.js} +2 -2
- package/dist-cli/chunks/{chunk-YADWU7OZ.js → chunk-N4YCSOCR.js} +2 -2
- package/dist-cli/chunks/{chunk-IODOTOOP.js → chunk-NI3L4A65.js} +2 -2
- package/dist-cli/chunks/{chunk-IACEJQIO.js → chunk-ODGEH6OW.js} +2 -2
- package/dist-cli/chunks/{chunk-DNA6VS6M.js → chunk-Q4NDYO2U.js} +1 -1
- package/dist-cli/chunks/{chunk-PV6MBKKE.js → chunk-QPRF3UYX.js} +2 -2
- package/dist-cli/chunks/{chunk-M2TMFGZH.js → chunk-QXJMXSQP.js} +1 -1
- package/dist-cli/chunks/chunk-RLKSLUNR.js +2 -0
- package/dist-cli/chunks/{chunk-WZG3YVJR.js → chunk-S244IT4W.js} +1 -1
- package/dist-cli/chunks/chunk-SDHOM6ZG.js +62 -0
- package/dist-cli/chunks/{chunk-B7BMSU7H.js → chunk-TXKOJ3P5.js} +1 -1
- package/dist-cli/chunks/{chunk-IBEMMV5E.js → chunk-UDMJSFSH.js} +8 -8
- package/dist-cli/chunks/{chunk-DQZEN256.js → chunk-ULV4PWN4.js} +1 -1
- package/dist-cli/chunks/{chunk-2TYEWVS4.js → chunk-V4HTQPMA.js} +1 -1
- package/dist-cli/chunks/chunk-VATHADSI.js +2 -0
- package/dist-cli/chunks/{chunk-5Q7IHJ7K.js → chunk-VUOCUCJ2.js} +2 -2
- package/dist-cli/chunks/{chunk-JDGLBW64.js → chunk-WN7R4GL4.js} +1 -1
- package/dist-cli/chunks/chunk-XOTJD6PU.js +118 -0
- package/dist-cli/chunks/chunk-XUNPVF5K.js +1 -0
- package/dist-cli/chunks/{chunk-KZIXAYN5.js → chunk-YMFSRCHL.js} +4 -4
- package/dist-cli/chunks/{chunk-X2HNVXNM.js → chunk-YV74D4MG.js} +2 -2
- package/dist-cli/chunks/{chunk-SNX3M4XD.js → chunk-YVTWSC5Y.js} +3 -3
- package/dist-cli/chunks/cli-version-EKKWCHRX.js +2 -0
- package/dist-cli/chunks/{compat-ORRWNXVT.js → compat-ZVZM5TML.js} +3 -3
- package/dist-cli/chunks/control-TWINYHMY.js +2 -0
- package/dist-cli/chunks/{cpu-profile-6PRSBZ2F.js → cpu-profile-RWGQTWLP.js} +2 -2
- package/dist-cli/chunks/daemon-AOSMNBD4.js +2 -0
- package/dist-cli/chunks/{debug-Q3J2X4WW.js → debug-R7PL3MHF.js} +5 -5
- package/dist-cli/chunks/{detox-DN57GMMJ.js → detox-UIJMKIGQ.js} +2 -2
- package/dist-cli/chunks/{device-IL5TPYGU.js → device-7SRVX2PU.js} +2 -2
- package/dist-cli/chunks/{diagnose-BW6QWP4F.js → diagnose-OXAKLLZT.js} +2 -2
- package/dist-cli/chunks/drivers-BPPG22VP.js +2 -0
- package/dist-cli/chunks/{electron-DGMSXQWG.js → electron-CTCH4VOL.js} +3 -3
- package/dist-cli/chunks/flow-KA2WCWDL.js +2 -0
- package/dist-cli/chunks/help-XBQIK2XY.js +2 -0
- package/dist-cli/chunks/{hints-TK3EHB4U.js → hints-P3ZIAP3Z.js} +2 -2
- package/dist-cli/chunks/home-paths-ZSCTLOYE.js +2 -0
- package/dist-cli/chunks/{inspect-NFWFGU4B.js → inspect-2ABU5WET.js} +24 -24
- package/dist-cli/chunks/{install-desktop-GB3JKSNX.js → install-desktop-4NOQ3YY4.js} +3 -3
- package/dist-cli/chunks/{keys-RAAHRIFH.js → keys-RPVEUNUC.js} +2 -2
- package/dist-cli/chunks/{login-KQ37GHDE.js → login-6D6DXBO3.js} +4 -4
- package/dist-cli/chunks/{logout-63SH2Y25.js → logout-T5OSWDTJ.js} +2 -2
- package/dist-cli/chunks/{maestro-24BT3MML.js → maestro-MKCHEOKA.js} +2 -2
- package/dist-cli/chunks/optional-demo-registry-EYD2K3QH.js +2 -0
- package/dist-cli/chunks/{preview-KQAOVN6R.js → preview-S52ZNGN2.js} +2 -2
- package/dist-cli/chunks/{profile-AN2B4NJT.js → profile-IG5F3LTL.js} +2 -2
- package/dist-cli/chunks/{react-ATGV35B4.js → react-275HCOFC.js} +2 -2
- package/dist-cli/chunks/{record-36662T3R.js → record-YT63SUHO.js} +4 -4
- package/dist-cli/chunks/runtime-WYZMDZAK.js +2 -0
- package/dist-cli/chunks/{screenshot-K4N5MPAT.js → screenshot-5AZ4MY3A.js} +4 -4
- package/dist-cli/chunks/{screenshot-mode-4QOJDLSH.js → screenshot-mode-4S4EX2XJ.js} +2 -2
- package/dist-cli/chunks/{screenshots-TB35YMSR.js → screenshots-UUJNZSG7.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-W6X33BZ4.js → screenshots-capture-I66JD3AD.js} +2 -2
- package/dist-cli/chunks/{server-L2PKDJZ2.js → serve-O46Q4WAD.js} +18 -17
- package/dist-cli/chunks/setup-C44N54O7.js +77 -0
- package/dist-cli/chunks/skills-PPVS4XOQ.js +40 -0
- package/dist-cli/chunks/{slides-TORKO36P.js → slides-ZWSZJZUY.js} +2 -2
- package/dist-cli/chunks/store-3SLGP5O3.js +2 -0
- package/dist-cli/chunks/telemetry-2XLLWARK.js +2 -0
- package/dist-cli/chunks/{test-5VCENVW3.js → test-362G4M5A.js} +3 -3
- package/dist-cli/chunks/{three-mode-ZVS2DSX6.js → three-mode-JNVTM2UT.js} +2 -2
- package/dist-cli/chunks/{timeline-TNC5OWLF.js → timeline-VKC6ULRK.js} +2 -2
- package/dist-cli/chunks/{upgrade-E7IO6A7Z.js → upgrade-SJPX4SPA.js} +2 -2
- package/dist-cli/chunks/upload-MXOBV6RT.js +2 -0
- package/dist-cli/chunks/version-4Z7AFNH3.js +4 -0
- package/dist-cli/chunks/web-WB2I2AWL.js +2 -0
- package/dist-cli/chunks/{what-happened-RMHZ7DGX.js → what-happened-KQCWMQJT.js} +3 -3
- package/dist-cli/chunks/{whoami-D53ANHR5.js → whoami-K76PXAD4.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +4 -5
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +3 -6
- package/dist-lib/beta.mjs +3 -5
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +4 -4
- package/dist-lib/dev-bundle-resolution.cjs +2 -9
- package/dist-lib/home-paths.cjs +59 -1
- package/dist-lib/host/bridge-host.cjs +67 -5
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2 -2
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +2 -2
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +5 -4
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +124 -8
- package/dist-lib/vite.cjs +2 -2
- package/package.json +2 -1
- package/scripts/dev-server-scanner.ts +3 -4
- package/scripts/postinstall.cjs +2 -2
- package/scripts/preuninstall.cjs +5 -7
- package/skills/soot.md +178 -0
- package/skills/sootsim-debug.md +384 -0
- package/skills/sootsim-setup.md +96 -0
- package/skills/sootsim-test.md +292 -0
- package/skills/sootsim-visual.md +122 -0
- package/src/agent-daemon-client.ts +4 -14
- package/src/beta.ts +3 -5
- package/src/dev-bundle-resolution.ts +1 -14
- package/src/home-paths.ts +77 -2
- package/src/host/agent-host.ts +1 -1
- package/src/host/bridge-host.ts +65 -6
- package/src/metro-plugin.ts +1 -1
- package/src/runtime-assets.ts +1 -1
- package/src/vite-plugin.ts +1 -0
- package/dist-cli/chunks/agent-V3GVCRPL.js +0 -46
- package/dist-cli/chunks/auto-bootstrap-YXDP3WKH.js +0 -2
- package/dist-cli/chunks/beta-FGSMZWLP.js +0 -2
- package/dist-cli/chunks/chunk-2IFWVIKM.js +0 -2
- package/dist-cli/chunks/chunk-3T36XLP3.js +0 -119
- package/dist-cli/chunks/chunk-5KY7TQ42.js +0 -5
- package/dist-cli/chunks/chunk-BQOMDL4K.js +0 -1
- package/dist-cli/chunks/chunk-FHMPJZKM.js +0 -7
- package/dist-cli/chunks/chunk-GQAWFQNN.js +0 -23
- package/dist-cli/chunks/chunk-HJTKKF7M.js +0 -13
- package/dist-cli/chunks/chunk-K4JAWKOY.js +0 -1
- package/dist-cli/chunks/chunk-NXGQS7IR.js +0 -2
- package/dist-cli/chunks/chunk-R2INPDGW.js +0 -1
- package/dist-cli/chunks/chunk-SLL5SZN4.js +0 -2
- package/dist-cli/chunks/chunk-XM66K64H.js +0 -79
- package/dist-cli/chunks/cli-version-3L6FFDIX.js +0 -2
- package/dist-cli/chunks/config-5IB6ONJ2.js +0 -45
- package/dist-cli/chunks/control-BM4V27OB.js +0 -2
- package/dist-cli/chunks/daemon-62MZT4G4.js +0 -67
- package/dist-cli/chunks/drivers-V6QDFZQD.js +0 -2
- package/dist-cli/chunks/flow-VBM6TU32.js +0 -2
- package/dist-cli/chunks/help-XWKNL743.js +0 -2
- package/dist-cli/chunks/home-paths-G23H3RO6.js +0 -2
- package/dist-cli/chunks/install-5YL7FITA.js +0 -2
- package/dist-cli/chunks/launch-DYFE4CAD.js +0 -16
- package/dist-cli/chunks/optional-demo-registry-JJXPB3UD.js +0 -2
- package/dist-cli/chunks/runtime-NRMC3U3D.js +0 -2
- package/dist-cli/chunks/runtime-delivery-RO6CTYPQ.js +0 -2
- package/dist-cli/chunks/setup-repo-AK6QOUE7.js +0 -2
- package/dist-cli/chunks/skills-KDSOWUOE.js +0 -10
- package/dist-cli/chunks/start-GH2AOUX6.js +0 -23
- package/dist-cli/chunks/store-JHW65VGQ.js +0 -2
- package/dist-cli/chunks/telemetry-SZLRXJXW.js +0 -2
- package/dist-cli/chunks/upload-KKEWETA5.js +0 -2
- package/dist-cli/chunks/version-37OD33SE.js +0 -4
- 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 {
|
|
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}
|
|
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, '
|
|
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
|
|
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
|
|
16
|
-
export const BETA_TAGLINE =
|
|
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
|
-
'
|
|
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
|
-
|
|
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.
|
|
88
|
-
//
|
|
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
|
+
}
|
package/src/host/agent-host.ts
CHANGED
|
@@ -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
|
|
64
|
+
* the rest of the repo uses, so `sootsim serve` standalone does the
|
|
65
65
|
* right thing. */
|
|
66
66
|
getExcludePorts?: () => number[]
|
|
67
67
|
}
|