rnxsim 0.0.0 → 0.1.313

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
  25. package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
  26. package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
  27. package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
  28. package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
  29. package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
  30. package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
  31. package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
  32. package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
  33. package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
  34. package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
  35. package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
  36. package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
  37. package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
  38. package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
  39. package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
  40. package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
  41. package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
  42. package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
  43. package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
  44. package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
  45. package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
  46. package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
  47. package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
  48. package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
  49. package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
  50. package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
  51. package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
  52. package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
  53. package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
  54. package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
  55. package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
  56. package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
  57. package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
  58. package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
  59. package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
  60. package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
  61. package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
  62. package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
  63. package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
  64. package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
  65. package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
  66. package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
  67. package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
  68. package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
  69. package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
  70. package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
  71. package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
  72. package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
  73. package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
  74. package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
  75. package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
  76. package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
  77. package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
  78. package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
  79. package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
  80. package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
  81. package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
  82. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
  83. package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
  84. package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
  85. package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
  86. package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
  87. package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
  88. package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
  89. package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
  90. package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
  91. package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
  93. package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
  94. package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
  95. package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
  96. package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
  98. package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
  99. package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
  100. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
  101. package/dist-cli/chunks/login-FJ737MWG.js +28 -0
  102. package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
  103. package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
  105. package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
  107. package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
  108. package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
  109. package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
  110. package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
  111. package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
  115. package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
  116. package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
  117. package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
  118. package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
  119. package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
  120. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
  121. package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
  122. package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
  123. package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
  124. package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
  125. package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
  126. package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23922 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
@@ -0,0 +1,466 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { isDisposableBrowserCacheDir } from './browser-cache'
4
+ import { ensureSootsimHome, readLiveRuntimeVersions, sootsimHomeDir } from './home-paths'
5
+ import { sootsimRuntime } from './runtime-delivery'
6
+
7
+ const AUTOMATIC_CLEANUP_GENERATION = 1
8
+ const AUTOMATIC_CLEANUP_COMPLETE_FILE = `automatic-cleanup-v${AUTOMATIC_CLEANUP_GENERATION}.json`
9
+ const AUTOMATIC_CLEANUP_LOCK_FILE = `automatic-cleanup-v${AUTOMATIC_CLEANUP_GENERATION}.lock`
10
+ const AUTOMATIC_CLEANUP_LOCK_STALE_MS = 6 * 60 * 60 * 1000
11
+
12
+ export type SootsimCleanupKind =
13
+ | 'browser-cache'
14
+ | 'recording'
15
+ | 'runtime-archive'
16
+ | 'runtime-version'
17
+
18
+ export type SootsimCleanupState = 'aggressive' | 'in-use' | 'retained' | 'safe'
19
+
20
+ export interface SootsimCleanupEntry {
21
+ path: string
22
+ bytes: number
23
+ kind: SootsimCleanupKind
24
+ state: SootsimCleanupState
25
+ selected: boolean
26
+ detail: string
27
+ }
28
+
29
+ export interface SootsimCleanupOptions {
30
+ aggressive?: boolean
31
+ }
32
+
33
+ export interface SootsimCleanupPlan {
34
+ home: string
35
+ activeRuntime: string | null
36
+ entries: SootsimCleanupEntry[]
37
+ reclaimableBytes: number
38
+ protectedBytes: number
39
+ totalBytes: number
40
+ }
41
+
42
+ export interface SootsimCleanupResult extends SootsimCleanupPlan {
43
+ removedPaths: string[]
44
+ reclaimedBytes: number
45
+ remainingBytes: number
46
+ }
47
+
48
+ export interface AutomaticSootsimCleanupResult {
49
+ status: 'already-complete' | 'busy' | 'complete' | 'deferred'
50
+ reclaimedBytes: number
51
+ remainingBytes: number
52
+ deferredInUseBytes: number
53
+ }
54
+
55
+ export function isAutomaticSootsimCleanupComplete(): boolean {
56
+ const completePath = path.join(sootsimHomeDir(), AUTOMATIC_CLEANUP_COMPLETE_FILE)
57
+ try {
58
+ const parsed = JSON.parse(fs.readFileSync(completePath, 'utf8')) as {
59
+ generation?: unknown
60
+ }
61
+ return parsed.generation === AUTOMATIC_CLEANUP_GENERATION
62
+ } catch {
63
+ return false
64
+ }
65
+ }
66
+
67
+ export function formatSootsimBytes(bytes: number): string {
68
+ if (bytes < 1024) return `${bytes} B`
69
+ const units = ['KiB', 'MiB', 'GiB', 'TiB']
70
+ let value = bytes / 1024
71
+ let unit = units[0]
72
+ for (let i = 1; i < units.length && value >= 1024; i++) {
73
+ value /= 1024
74
+ unit = units[i]
75
+ }
76
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`
77
+ }
78
+
79
+ function allocatedBytes(target: string): number {
80
+ let stat: fs.Stats
81
+ try {
82
+ stat = fs.lstatSync(target)
83
+ } catch {
84
+ return 0
85
+ }
86
+ const ownBytes = stat.blocks > 0 ? stat.blocks * 512 : stat.size
87
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return ownBytes
88
+ let total = ownBytes
89
+ try {
90
+ for (const child of fs.readdirSync(target)) {
91
+ total += allocatedBytes(path.join(target, child))
92
+ }
93
+ } catch {}
94
+ return total
95
+ }
96
+
97
+ function hasBrowserLock(profileRoot: string): boolean {
98
+ for (const name of ['SingletonLock', 'SingletonSocket', 'lockfile']) {
99
+ try {
100
+ if (fs.lstatSync(path.join(profileRoot, name))) return true
101
+ } catch {}
102
+ }
103
+ return false
104
+ }
105
+
106
+ function collectCacheDirs(root: string, profileRoot: string): SootsimCleanupEntry[] {
107
+ let children: fs.Dirent[]
108
+ try {
109
+ children = fs.readdirSync(root, { withFileTypes: true })
110
+ } catch {
111
+ return []
112
+ }
113
+ const inUse = hasBrowserLock(profileRoot)
114
+ const entries: SootsimCleanupEntry[] = []
115
+ for (const child of children) {
116
+ if (!child.isDirectory() || child.isSymbolicLink()) continue
117
+ const childPath = path.join(root, child.name)
118
+ if (isDisposableBrowserCacheDir(child.name)) {
119
+ entries.push({
120
+ path: childPath,
121
+ bytes: allocatedBytes(childPath),
122
+ kind: 'browser-cache',
123
+ state: inUse ? 'in-use' : 'safe',
124
+ selected: !inUse,
125
+ detail: inUse
126
+ ? 'browser profile is open; close it before cleanup'
127
+ : 'disposable browser HTTP, bytecode, or GPU cache',
128
+ })
129
+ continue
130
+ }
131
+ entries.push(...collectCacheDirs(childPath, profileRoot))
132
+ }
133
+ return entries
134
+ }
135
+
136
+ function collectPlaywrightCaches(home: string): SootsimCleanupEntry[] {
137
+ const root = path.join(home, 'profiles', 'playwright')
138
+ let profiles: fs.Dirent[]
139
+ try {
140
+ profiles = fs.readdirSync(root, { withFileTypes: true })
141
+ } catch {
142
+ return []
143
+ }
144
+ return profiles.flatMap((profile) => {
145
+ if (!profile.isDirectory() || profile.isSymbolicLink()) return []
146
+ const profileRoot = path.join(root, profile.name)
147
+ return collectCacheDirs(profileRoot, profileRoot)
148
+ })
149
+ }
150
+
151
+ function collectElectronCaches(home: string): SootsimCleanupEntry[] {
152
+ const root = path.join(home, 'electron', 'userData')
153
+ return collectCacheDirs(root, root)
154
+ }
155
+
156
+ function collectRecordingEntries(
157
+ home: string,
158
+ aggressive: boolean,
159
+ ): SootsimCleanupEntry[] {
160
+ const root = path.join(home, 'recordings')
161
+ let recordings: fs.Dirent[]
162
+ try {
163
+ recordings = fs.readdirSync(root, { withFileTypes: true })
164
+ } catch {
165
+ return []
166
+ }
167
+ return recordings.map((recording) => {
168
+ const recordingPath = path.join(root, recording.name)
169
+ return {
170
+ path: recordingPath,
171
+ bytes: allocatedBytes(recordingPath),
172
+ kind: 'recording',
173
+ state: 'aggressive',
174
+ selected: aggressive,
175
+ detail: 'user-created recording or captured-frame output',
176
+ }
177
+ })
178
+ }
179
+
180
+ function runtimeEntries(aggressive: boolean): {
181
+ activeRuntime: string | null
182
+ entries: SootsimCleanupEntry[]
183
+ } {
184
+ const inUseVersions = new Set(readLiveRuntimeVersions())
185
+ const protectVersions = [...inUseVersions]
186
+ const normal = sootsimRuntime.planLocalRetention({
187
+ retainVersions: 2,
188
+ protectVersions,
189
+ })
190
+ const minimal = sootsimRuntime.planLocalRetention({
191
+ retainVersions: 1,
192
+ protectVersions,
193
+ })
194
+ const normallyRemoved = new Set(normal.removeVersions)
195
+ const aggressivelyRemoved = new Set(minimal.removeVersions)
196
+ const allVersions = new Set([
197
+ ...normal.keepVersions,
198
+ ...normal.removeVersions,
199
+ ...minimal.keepVersions,
200
+ ...minimal.removeVersions,
201
+ ])
202
+ const entries: SootsimCleanupEntry[] = []
203
+
204
+ for (const version of [...allVersions].sort()) {
205
+ const runtimePath = sootsimRuntime.product.runtimeDir(version)
206
+ let state: SootsimCleanupState = 'retained'
207
+ let detail = 'retained runtime'
208
+ if (inUseVersions.has(version) && version !== normal.activeVersion) {
209
+ state = 'in-use'
210
+ detail = 'runtime is serving a connected simulator'
211
+ } else if (normallyRemoved.has(version)) {
212
+ state = 'safe'
213
+ detail = 'inactive runtime outside the two-version rollback window'
214
+ } else if (aggressivelyRemoved.has(version)) {
215
+ state = 'aggressive'
216
+ detail = 'most recent rollback runtime'
217
+ } else if (version === normal.activeVersion) {
218
+ detail = 'active runtime; always protected'
219
+ }
220
+ entries.push({
221
+ path: runtimePath,
222
+ bytes: allocatedBytes(runtimePath),
223
+ kind: 'runtime-version',
224
+ state,
225
+ selected: state === 'safe' || (state === 'aggressive' && aggressive),
226
+ detail,
227
+ })
228
+ }
229
+
230
+ for (const cacheFile of normal.removeCacheFiles) {
231
+ entries.push({
232
+ path: cacheFile,
233
+ bytes: allocatedBytes(cacheFile),
234
+ kind: 'runtime-archive',
235
+ state: 'safe',
236
+ selected: true,
237
+ detail: 'downloaded archive is unnecessary after successful extraction',
238
+ })
239
+ }
240
+
241
+ return {
242
+ activeRuntime: normal.activeVersion,
243
+ entries,
244
+ }
245
+ }
246
+
247
+ export function planSootsimCleanup(
248
+ options: SootsimCleanupOptions = {},
249
+ ): SootsimCleanupPlan {
250
+ const aggressive = options.aggressive === true
251
+ const home = sootsimHomeDir()
252
+ const runtimes = runtimeEntries(aggressive)
253
+ const entries = [
254
+ ...runtimes.entries,
255
+ ...collectPlaywrightCaches(home),
256
+ ...collectElectronCaches(home),
257
+ ...collectRecordingEntries(home, aggressive),
258
+ ].sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path))
259
+ return {
260
+ home,
261
+ activeRuntime: runtimes.activeRuntime,
262
+ entries,
263
+ reclaimableBytes: entries.reduce(
264
+ (total, entry) => total + (entry.selected ? entry.bytes : 0),
265
+ 0,
266
+ ),
267
+ protectedBytes: entries.reduce(
268
+ (total, entry) => total + (!entry.selected ? entry.bytes : 0),
269
+ 0,
270
+ ),
271
+ totalBytes: allocatedBytes(home),
272
+ }
273
+ }
274
+
275
+ function assertInsideHome(home: string, target: string): void {
276
+ const relative = path.relative(home, target)
277
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
278
+ throw new Error(`refusing cleanup target outside the rnx data home: ${target}`)
279
+ }
280
+ }
281
+
282
+ export function applySootsimCleanup(
283
+ options: SootsimCleanupOptions = {},
284
+ ): SootsimCleanupResult {
285
+ const plan = planSootsimCleanup(options)
286
+ const retainVersions = options.aggressive === true ? 1 : 2
287
+ sootsimRuntime.pruneLocalRetention({
288
+ retainVersions,
289
+ protectVersions: readLiveRuntimeVersions(),
290
+ })
291
+
292
+ for (const entry of plan.entries) {
293
+ if (!entry.selected) continue
294
+ if (entry.kind === 'runtime-archive' || entry.kind === 'runtime-version') continue
295
+ if (entry.kind === 'browser-cache') {
296
+ const relative = path.relative(plan.home, entry.path)
297
+ const parts = relative.split(path.sep)
298
+ const profileRoot =
299
+ parts[0] === 'profiles' && parts[1] === 'playwright' && parts[2]
300
+ ? path.join(plan.home, 'profiles', 'playwright', parts[2])
301
+ : parts[0] === 'electron' && parts[1] === 'userData'
302
+ ? path.join(plan.home, 'electron', 'userData')
303
+ : null
304
+ if (profileRoot && hasBrowserLock(profileRoot)) {
305
+ entry.state = 'in-use'
306
+ entry.selected = false
307
+ entry.detail = 'browser profile opened while cleanup was starting'
308
+ continue
309
+ }
310
+ }
311
+ assertInsideHome(plan.home, entry.path)
312
+ fs.rmSync(entry.path, { recursive: true, force: true })
313
+ }
314
+
315
+ const removedPaths = plan.entries
316
+ .filter((entry) => entry.selected && !fs.existsSync(entry.path))
317
+ .map((entry) => entry.path)
318
+ const remainingBytes = allocatedBytes(plan.home)
319
+ return {
320
+ ...plan,
321
+ removedPaths,
322
+ reclaimedBytes: Math.max(0, plan.totalBytes - remainingBytes),
323
+ remainingBytes,
324
+ }
325
+ }
326
+
327
+ export function applyAutomaticSootsimCleanup(): AutomaticSootsimCleanupResult {
328
+ ensureSootsimHome()
329
+ const home = sootsimHomeDir()
330
+ const completePath = path.join(home, AUTOMATIC_CLEANUP_COMPLETE_FILE)
331
+ const lockPath = path.join(home, AUTOMATIC_CLEANUP_LOCK_FILE)
332
+
333
+ if (isAutomaticSootsimCleanupComplete()) {
334
+ return {
335
+ status: 'already-complete',
336
+ reclaimedBytes: 0,
337
+ remainingBytes: 0,
338
+ deferredInUseBytes: 0,
339
+ }
340
+ }
341
+
342
+ let lockFd: number | null = null
343
+ for (let attempt = 0; attempt < 2 && lockFd === null; attempt++) {
344
+ try {
345
+ const candidateFd = fs.openSync(lockPath, 'wx')
346
+ try {
347
+ fs.writeFileSync(
348
+ candidateFd,
349
+ `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`,
350
+ 'utf8',
351
+ )
352
+ lockFd = candidateFd
353
+ } catch (error) {
354
+ fs.closeSync(candidateFd)
355
+ try {
356
+ fs.unlinkSync(lockPath)
357
+ } catch {}
358
+ throw error
359
+ }
360
+ } catch (error) {
361
+ const code = error instanceof Error && 'code' in error ? error.code : null
362
+ if (code !== 'EEXIST') throw error
363
+
364
+ let ownerPid = 0
365
+ let ownerStartedAt = 0
366
+ let originalStat: fs.Stats | null = null
367
+ try {
368
+ originalStat = fs.lstatSync(lockPath)
369
+ const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as {
370
+ pid?: unknown
371
+ startedAt?: unknown
372
+ }
373
+ ownerPid = typeof owner.pid === 'number' ? owner.pid : 0
374
+ ownerStartedAt = typeof owner.startedAt === 'number' ? owner.startedAt : 0
375
+ } catch {}
376
+ if (
377
+ ownerPid > 0 &&
378
+ ownerStartedAt > 0 &&
379
+ Date.now() - ownerStartedAt < AUTOMATIC_CLEANUP_LOCK_STALE_MS
380
+ ) {
381
+ try {
382
+ process.kill(ownerPid, 0)
383
+ return {
384
+ status: 'busy',
385
+ reclaimedBytes: 0,
386
+ remainingBytes: 0,
387
+ deferredInUseBytes: 0,
388
+ }
389
+ } catch {}
390
+ }
391
+ if (!originalStat) continue
392
+ try {
393
+ const currentStat = fs.lstatSync(lockPath)
394
+ if (
395
+ currentStat.dev !== originalStat.dev ||
396
+ currentStat.ino !== originalStat.ino
397
+ ) {
398
+ return {
399
+ status: 'busy',
400
+ reclaimedBytes: 0,
401
+ remainingBytes: 0,
402
+ deferredInUseBytes: 0,
403
+ }
404
+ }
405
+ fs.unlinkSync(lockPath)
406
+ } catch {}
407
+ }
408
+ }
409
+
410
+ if (lockFd === null) {
411
+ return {
412
+ status: 'busy',
413
+ reclaimedBytes: 0,
414
+ remainingBytes: 0,
415
+ deferredInUseBytes: 0,
416
+ }
417
+ }
418
+
419
+ try {
420
+ if (isAutomaticSootsimCleanupComplete()) {
421
+ return {
422
+ status: 'already-complete',
423
+ reclaimedBytes: 0,
424
+ remainingBytes: 0,
425
+ deferredInUseBytes: 0,
426
+ }
427
+ }
428
+
429
+ const result = applySootsimCleanup()
430
+ const deferredInUseBytes = result.entries.reduce(
431
+ (total, entry) => total + (entry.state === 'in-use' ? entry.bytes : 0),
432
+ 0,
433
+ )
434
+ if (deferredInUseBytes > 0) {
435
+ return {
436
+ status: 'deferred',
437
+ reclaimedBytes: result.reclaimedBytes,
438
+ remainingBytes: result.remainingBytes,
439
+ deferredInUseBytes,
440
+ }
441
+ }
442
+
443
+ const tmp = `${completePath}.${process.pid}.tmp`
444
+ fs.writeFileSync(
445
+ tmp,
446
+ `${JSON.stringify({
447
+ generation: AUTOMATIC_CLEANUP_GENERATION,
448
+ completedAt: Date.now(),
449
+ reclaimedBytes: result.reclaimedBytes,
450
+ })}\n`,
451
+ 'utf8',
452
+ )
453
+ fs.renameSync(tmp, completePath)
454
+ return {
455
+ status: 'complete',
456
+ reclaimedBytes: result.reclaimedBytes,
457
+ remainingBytes: result.remainingBytes,
458
+ deferredInUseBytes: 0,
459
+ }
460
+ } finally {
461
+ fs.closeSync(lockFd)
462
+ try {
463
+ fs.unlinkSync(lockPath)
464
+ } catch {}
465
+ }
466
+ }