peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,540 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { isDisposableBrowserCacheDir } from './browser-cache'
4
+ import { ensureRnxHome, readLiveRuntimeVersions, rnxHomeDir } from './home-paths'
5
+ import { rnxRuntime } 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
+ // a deferral is recorded so the retry does not happen on literally every
12
+ // command. cleanup defers whenever anything is in use, and on a machine that
13
+ // always has a sim or daemon running that is the permanent state: the complete
14
+ // marker is never written, so every rnx invocation spawned a detached worker
15
+ // that rescanned the home directory, reclaimed nothing, and printed the same
16
+ // "N is in use and will be retried on a later run" line into the caller's
17
+ // stderr. measured on this machine: an identical 662.2 MiB across every
18
+ // invocation over several hours. the retry itself is right, its cadence was
19
+ // not, so it is throttled rather than removed.
20
+ const AUTOMATIC_CLEANUP_DEFERRED_FILE = `automatic-cleanup-v${AUTOMATIC_CLEANUP_GENERATION}.deferred.json`
21
+ const AUTOMATIC_CLEANUP_DEFER_RETRY_MS = 30 * 60 * 1000
22
+
23
+ export type SootsimCleanupKind =
24
+ | 'browser-cache'
25
+ | 'camera-fixture'
26
+ | 'recording'
27
+ | 'runtime-archive'
28
+ | 'runtime-version'
29
+
30
+ export type SootsimCleanupState = 'aggressive' | 'in-use' | 'retained' | 'safe'
31
+
32
+ export interface SootsimCleanupEntry {
33
+ path: string
34
+ bytes: number
35
+ kind: SootsimCleanupKind
36
+ state: SootsimCleanupState
37
+ selected: boolean
38
+ detail: string
39
+ }
40
+
41
+ export interface SootsimCleanupOptions {
42
+ aggressive?: boolean
43
+ }
44
+
45
+ export interface SootsimCleanupPlan {
46
+ home: string
47
+ activeRuntime: string | null
48
+ entries: SootsimCleanupEntry[]
49
+ reclaimableBytes: number
50
+ protectedBytes: number
51
+ totalBytes: number
52
+ }
53
+
54
+ export interface SootsimCleanupResult extends SootsimCleanupPlan {
55
+ removedPaths: string[]
56
+ reclaimedBytes: number
57
+ remainingBytes: number
58
+ }
59
+
60
+ export interface AutomaticSootsimCleanupResult {
61
+ status: 'already-complete' | 'busy' | 'complete' | 'deferred'
62
+ reclaimedBytes: number
63
+ remainingBytes: number
64
+ deferredInUseBytes: number
65
+ }
66
+
67
+ export function isAutomaticSootsimCleanupComplete(): boolean {
68
+ const completePath = path.join(rnxHomeDir(), AUTOMATIC_CLEANUP_COMPLETE_FILE)
69
+ try {
70
+ const parsed = JSON.parse(fs.readFileSync(completePath, 'utf8')) as {
71
+ generation?: unknown
72
+ }
73
+ return parsed.generation === AUTOMATIC_CLEANUP_GENERATION
74
+ } catch {
75
+ return false
76
+ }
77
+ }
78
+
79
+ // what the CLI asks before spawning a worker: is there anything for one to do
80
+ // right now. complete means never again; a recent deferral means not yet.
81
+ export function shouldSkipAutomaticSootsimCleanup(now = Date.now()): boolean {
82
+ if (isAutomaticSootsimCleanupComplete()) return true
83
+ const deferredPath = path.join(rnxHomeDir(), AUTOMATIC_CLEANUP_DEFERRED_FILE)
84
+ try {
85
+ const parsed = JSON.parse(fs.readFileSync(deferredPath, 'utf8')) as {
86
+ generation?: unknown
87
+ attemptedAt?: unknown
88
+ }
89
+ if (parsed.generation !== AUTOMATIC_CLEANUP_GENERATION) return false
90
+ if (typeof parsed.attemptedAt !== 'number') return false
91
+ // a clock that moved backwards must not park cleanup forever
92
+ const age = now - parsed.attemptedAt
93
+ return age >= 0 && age < AUTOMATIC_CLEANUP_DEFER_RETRY_MS
94
+ } catch {
95
+ return false
96
+ }
97
+ }
98
+
99
+ export function formatSootsimBytes(bytes: number): string {
100
+ if (bytes < 1024) return `${bytes} B`
101
+ const units = ['KiB', 'MiB', 'GiB', 'TiB']
102
+ let value = bytes / 1024
103
+ let unit = units[0]
104
+ for (let i = 1; i < units.length && value >= 1024; i++) {
105
+ value /= 1024
106
+ unit = units[i]
107
+ }
108
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`
109
+ }
110
+
111
+ function allocatedBytes(target: string): number {
112
+ let stat: fs.Stats
113
+ try {
114
+ stat = fs.lstatSync(target)
115
+ } catch {
116
+ return 0
117
+ }
118
+ const ownBytes = stat.blocks > 0 ? stat.blocks * 512 : stat.size
119
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return ownBytes
120
+ let total = ownBytes
121
+ try {
122
+ for (const child of fs.readdirSync(target)) {
123
+ total += allocatedBytes(path.join(target, child))
124
+ }
125
+ } catch {}
126
+ return total
127
+ }
128
+
129
+ function hasBrowserLock(profileRoot: string): boolean {
130
+ for (const name of ['SingletonLock', 'SingletonSocket', 'lockfile']) {
131
+ try {
132
+ if (fs.lstatSync(path.join(profileRoot, name))) return true
133
+ } catch {}
134
+ }
135
+ return false
136
+ }
137
+
138
+ function collectCacheDirs(root: string, profileRoot: string): SootsimCleanupEntry[] {
139
+ let children: fs.Dirent[]
140
+ try {
141
+ children = fs.readdirSync(root, { withFileTypes: true })
142
+ } catch {
143
+ return []
144
+ }
145
+ const inUse = hasBrowserLock(profileRoot)
146
+ const entries: SootsimCleanupEntry[] = []
147
+ for (const child of children) {
148
+ if (!child.isDirectory() || child.isSymbolicLink()) continue
149
+ const childPath = path.join(root, child.name)
150
+ if (isDisposableBrowserCacheDir(child.name)) {
151
+ entries.push({
152
+ path: childPath,
153
+ bytes: allocatedBytes(childPath),
154
+ kind: 'browser-cache',
155
+ state: inUse ? 'in-use' : 'safe',
156
+ selected: !inUse,
157
+ detail: inUse
158
+ ? 'browser profile is open; close it before cleanup'
159
+ : 'disposable browser HTTP, bytecode, or GPU cache',
160
+ })
161
+ continue
162
+ }
163
+ entries.push(...collectCacheDirs(childPath, profileRoot))
164
+ }
165
+ return entries
166
+ }
167
+
168
+ function collectPlaywrightCaches(home: string): SootsimCleanupEntry[] {
169
+ const root = path.join(home, 'profiles', 'playwright')
170
+ let profiles: fs.Dirent[]
171
+ try {
172
+ profiles = fs.readdirSync(root, { withFileTypes: true })
173
+ } catch {
174
+ return []
175
+ }
176
+ return profiles.flatMap((profile) => {
177
+ if (!profile.isDirectory() || profile.isSymbolicLink()) return []
178
+ const profileRoot = path.join(root, profile.name)
179
+ return collectCacheDirs(profileRoot, profileRoot)
180
+ })
181
+ }
182
+
183
+ function collectElectronCaches(home: string): SootsimCleanupEntry[] {
184
+ const root = path.join(home, 'electron', 'userData')
185
+ return collectCacheDirs(root, root)
186
+ }
187
+
188
+ function collectRecordingEntries(
189
+ home: string,
190
+ aggressive: boolean,
191
+ ): SootsimCleanupEntry[] {
192
+ const root = path.join(home, 'recordings')
193
+ let recordings: fs.Dirent[]
194
+ try {
195
+ recordings = fs.readdirSync(root, { withFileTypes: true })
196
+ } catch {
197
+ return []
198
+ }
199
+ return recordings.map((recording) => {
200
+ const recordingPath = path.join(root, recording.name)
201
+ return {
202
+ path: recordingPath,
203
+ bytes: allocatedBytes(recordingPath),
204
+ kind: 'recording',
205
+ state: 'aggressive',
206
+ selected: aggressive,
207
+ detail: 'user-created recording or captured-frame output',
208
+ }
209
+ })
210
+ }
211
+
212
+ // videos staged by `rnx camera play`. a fixture is only a copy of a file
213
+ // the user still has, so it is cheap to reclaim, but the automatic sweep must
214
+ // not pull one out from under a sim that is playing it right now. aggressive
215
+ // only, matching recordings.
216
+ function collectCameraFixtureEntries(
217
+ home: string,
218
+ aggressive: boolean,
219
+ ): SootsimCleanupEntry[] {
220
+ const root = path.join(home, 'camera-fixtures')
221
+ let fixtures: fs.Dirent[]
222
+ try {
223
+ fixtures = fs.readdirSync(root, { withFileTypes: true })
224
+ } catch {
225
+ return []
226
+ }
227
+ return fixtures.map((fixture) => {
228
+ const fixturePath = path.join(root, fixture.name)
229
+ return {
230
+ path: fixturePath,
231
+ bytes: allocatedBytes(fixturePath),
232
+ kind: 'camera-fixture',
233
+ state: 'aggressive',
234
+ selected: aggressive,
235
+ detail: 'staged camera fixture video',
236
+ }
237
+ })
238
+ }
239
+
240
+ function runtimeEntries(aggressive: boolean): {
241
+ activeRuntime: string | null
242
+ entries: SootsimCleanupEntry[]
243
+ } {
244
+ const inUseVersions = new Set(readLiveRuntimeVersions())
245
+ const protectVersions = [...inUseVersions]
246
+ const normal = rnxRuntime.planLocalRetention({
247
+ retainVersions: 2,
248
+ protectVersions,
249
+ })
250
+ const minimal = rnxRuntime.planLocalRetention({
251
+ retainVersions: 1,
252
+ protectVersions,
253
+ })
254
+ const normallyRemoved = new Set(normal.removeVersions)
255
+ const aggressivelyRemoved = new Set(minimal.removeVersions)
256
+ const allVersions = new Set([
257
+ ...normal.keepVersions,
258
+ ...normal.removeVersions,
259
+ ...minimal.keepVersions,
260
+ ...minimal.removeVersions,
261
+ ])
262
+ const entries: SootsimCleanupEntry[] = []
263
+
264
+ for (const version of [...allVersions].sort()) {
265
+ const runtimePath = rnxRuntime.product.runtimeDir(version)
266
+ let state: SootsimCleanupState = 'retained'
267
+ let detail = 'retained runtime'
268
+ if (inUseVersions.has(version) && version !== normal.activeVersion) {
269
+ state = 'in-use'
270
+ detail = 'runtime is serving a connected simulator'
271
+ } else if (normallyRemoved.has(version)) {
272
+ state = 'safe'
273
+ detail = 'inactive runtime outside the two-version rollback window'
274
+ } else if (aggressivelyRemoved.has(version)) {
275
+ state = 'aggressive'
276
+ detail = 'most recent rollback runtime'
277
+ } else if (version === normal.activeVersion) {
278
+ detail = 'active runtime; always protected'
279
+ }
280
+ entries.push({
281
+ path: runtimePath,
282
+ bytes: allocatedBytes(runtimePath),
283
+ kind: 'runtime-version',
284
+ state,
285
+ selected: state === 'safe' || (state === 'aggressive' && aggressive),
286
+ detail,
287
+ })
288
+ }
289
+
290
+ for (const cacheFile of normal.removeCacheFiles) {
291
+ entries.push({
292
+ path: cacheFile,
293
+ bytes: allocatedBytes(cacheFile),
294
+ kind: 'runtime-archive',
295
+ state: 'safe',
296
+ selected: true,
297
+ detail: 'downloaded archive is unnecessary after successful extraction',
298
+ })
299
+ }
300
+
301
+ return {
302
+ activeRuntime: normal.activeVersion,
303
+ entries,
304
+ }
305
+ }
306
+
307
+ export function planSootsimCleanup(
308
+ options: SootsimCleanupOptions = {},
309
+ ): SootsimCleanupPlan {
310
+ const aggressive = options.aggressive === true
311
+ const home = rnxHomeDir()
312
+ const runtimes = runtimeEntries(aggressive)
313
+ const entries = [
314
+ ...runtimes.entries,
315
+ ...collectPlaywrightCaches(home),
316
+ ...collectElectronCaches(home),
317
+ ...collectRecordingEntries(home, aggressive),
318
+ ...collectCameraFixtureEntries(home, aggressive),
319
+ ].sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path))
320
+ return {
321
+ home,
322
+ activeRuntime: runtimes.activeRuntime,
323
+ entries,
324
+ reclaimableBytes: entries.reduce(
325
+ (total, entry) => total + (entry.selected ? entry.bytes : 0),
326
+ 0,
327
+ ),
328
+ protectedBytes: entries.reduce(
329
+ (total, entry) => total + (!entry.selected ? entry.bytes : 0),
330
+ 0,
331
+ ),
332
+ totalBytes: allocatedBytes(home),
333
+ }
334
+ }
335
+
336
+ function assertInsideHome(home: string, target: string): void {
337
+ const relative = path.relative(home, target)
338
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
339
+ throw new Error(`refusing cleanup target outside the rnx data home: ${target}`)
340
+ }
341
+ }
342
+
343
+ export function applySootsimCleanup(
344
+ options: SootsimCleanupOptions = {},
345
+ ): SootsimCleanupResult {
346
+ const plan = planSootsimCleanup(options)
347
+ const retainVersions = options.aggressive === true ? 1 : 2
348
+ rnxRuntime.pruneLocalRetention({
349
+ retainVersions,
350
+ protectVersions: readLiveRuntimeVersions(),
351
+ })
352
+
353
+ for (const entry of plan.entries) {
354
+ if (!entry.selected) continue
355
+ if (entry.kind === 'runtime-archive' || entry.kind === 'runtime-version') continue
356
+ if (entry.kind === 'browser-cache') {
357
+ const relative = path.relative(plan.home, entry.path)
358
+ const parts = relative.split(path.sep)
359
+ const profileRoot =
360
+ parts[0] === 'profiles' && parts[1] === 'playwright' && parts[2]
361
+ ? path.join(plan.home, 'profiles', 'playwright', parts[2])
362
+ : parts[0] === 'electron' && parts[1] === 'userData'
363
+ ? path.join(plan.home, 'electron', 'userData')
364
+ : null
365
+ if (profileRoot && hasBrowserLock(profileRoot)) {
366
+ entry.state = 'in-use'
367
+ entry.selected = false
368
+ entry.detail = 'browser profile opened while cleanup was starting'
369
+ continue
370
+ }
371
+ }
372
+ assertInsideHome(plan.home, entry.path)
373
+ fs.rmSync(entry.path, { recursive: true, force: true })
374
+ }
375
+
376
+ const removedPaths = plan.entries
377
+ .filter((entry) => entry.selected && !fs.existsSync(entry.path))
378
+ .map((entry) => entry.path)
379
+ const remainingBytes = allocatedBytes(plan.home)
380
+ return {
381
+ ...plan,
382
+ removedPaths,
383
+ reclaimedBytes: Math.max(0, plan.totalBytes - remainingBytes),
384
+ remainingBytes,
385
+ }
386
+ }
387
+
388
+ export function applyAutomaticSootsimCleanup(): AutomaticSootsimCleanupResult {
389
+ ensureRnxHome()
390
+ const home = rnxHomeDir()
391
+ const completePath = path.join(home, AUTOMATIC_CLEANUP_COMPLETE_FILE)
392
+ const deferredPath = path.join(home, AUTOMATIC_CLEANUP_DEFERRED_FILE)
393
+ const lockPath = path.join(home, AUTOMATIC_CLEANUP_LOCK_FILE)
394
+
395
+ if (isAutomaticSootsimCleanupComplete()) {
396
+ return {
397
+ status: 'already-complete',
398
+ reclaimedBytes: 0,
399
+ remainingBytes: 0,
400
+ deferredInUseBytes: 0,
401
+ }
402
+ }
403
+
404
+ let lockFd: number | null = null
405
+ for (let attempt = 0; attempt < 2 && lockFd === null; attempt++) {
406
+ try {
407
+ const candidateFd = fs.openSync(lockPath, 'wx')
408
+ try {
409
+ fs.writeFileSync(
410
+ candidateFd,
411
+ `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`,
412
+ 'utf8',
413
+ )
414
+ lockFd = candidateFd
415
+ } catch (error) {
416
+ fs.closeSync(candidateFd)
417
+ try {
418
+ fs.unlinkSync(lockPath)
419
+ } catch {}
420
+ throw error
421
+ }
422
+ } catch (error) {
423
+ const code = error instanceof Error && 'code' in error ? error.code : null
424
+ if (code !== 'EEXIST') throw error
425
+
426
+ let ownerPid = 0
427
+ let ownerStartedAt = 0
428
+ let originalStat: fs.Stats | null = null
429
+ try {
430
+ originalStat = fs.lstatSync(lockPath)
431
+ const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as {
432
+ pid?: unknown
433
+ startedAt?: unknown
434
+ }
435
+ ownerPid = typeof owner.pid === 'number' ? owner.pid : 0
436
+ ownerStartedAt = typeof owner.startedAt === 'number' ? owner.startedAt : 0
437
+ } catch {}
438
+ if (
439
+ ownerPid > 0 &&
440
+ ownerStartedAt > 0 &&
441
+ Date.now() - ownerStartedAt < AUTOMATIC_CLEANUP_LOCK_STALE_MS
442
+ ) {
443
+ try {
444
+ process.kill(ownerPid, 0)
445
+ return {
446
+ status: 'busy',
447
+ reclaimedBytes: 0,
448
+ remainingBytes: 0,
449
+ deferredInUseBytes: 0,
450
+ }
451
+ } catch {}
452
+ }
453
+ if (!originalStat) continue
454
+ try {
455
+ const currentStat = fs.lstatSync(lockPath)
456
+ if (
457
+ currentStat.dev !== originalStat.dev ||
458
+ currentStat.ino !== originalStat.ino
459
+ ) {
460
+ return {
461
+ status: 'busy',
462
+ reclaimedBytes: 0,
463
+ remainingBytes: 0,
464
+ deferredInUseBytes: 0,
465
+ }
466
+ }
467
+ fs.unlinkSync(lockPath)
468
+ } catch {}
469
+ }
470
+ }
471
+
472
+ if (lockFd === null) {
473
+ return {
474
+ status: 'busy',
475
+ reclaimedBytes: 0,
476
+ remainingBytes: 0,
477
+ deferredInUseBytes: 0,
478
+ }
479
+ }
480
+
481
+ try {
482
+ if (isAutomaticSootsimCleanupComplete()) {
483
+ return {
484
+ status: 'already-complete',
485
+ reclaimedBytes: 0,
486
+ remainingBytes: 0,
487
+ deferredInUseBytes: 0,
488
+ }
489
+ }
490
+
491
+ const result = applySootsimCleanup()
492
+ const deferredInUseBytes = result.entries.reduce(
493
+ (total, entry) => total + (entry.state === 'in-use' ? entry.bytes : 0),
494
+ 0,
495
+ )
496
+ if (deferredInUseBytes > 0) {
497
+ const deferredTmp = `${deferredPath}.${process.pid}.tmp`
498
+ fs.writeFileSync(
499
+ deferredTmp,
500
+ `${JSON.stringify({
501
+ generation: AUTOMATIC_CLEANUP_GENERATION,
502
+ attemptedAt: Date.now(),
503
+ deferredInUseBytes,
504
+ })}\n`,
505
+ 'utf8',
506
+ )
507
+ fs.renameSync(deferredTmp, deferredPath)
508
+ return {
509
+ status: 'deferred',
510
+ reclaimedBytes: result.reclaimedBytes,
511
+ remainingBytes: result.remainingBytes,
512
+ deferredInUseBytes,
513
+ }
514
+ }
515
+
516
+ const tmp = `${completePath}.${process.pid}.tmp`
517
+ fs.writeFileSync(
518
+ tmp,
519
+ `${JSON.stringify({
520
+ generation: AUTOMATIC_CLEANUP_GENERATION,
521
+ completedAt: Date.now(),
522
+ reclaimedBytes: result.reclaimedBytes,
523
+ })}\n`,
524
+ 'utf8',
525
+ )
526
+ fs.renameSync(tmp, completePath)
527
+ fs.rmSync(deferredPath, { force: true })
528
+ return {
529
+ status: 'complete',
530
+ reclaimedBytes: result.reclaimedBytes,
531
+ remainingBytes: result.remainingBytes,
532
+ deferredInUseBytes: 0,
533
+ }
534
+ } finally {
535
+ fs.closeSync(lockFd)
536
+ try {
537
+ fs.unlinkSync(lockPath)
538
+ } catch {}
539
+ }
540
+ }
@@ -0,0 +1,15 @@
1
+ // hosted-runner previews reach first-render much slower than local dev: the
2
+ // guest bundle (often tens of MB) re-evals and re-hydrates on every launchApp
3
+ // hard reload, and it is served over a cloudflared tunnel. run.sh sets
4
+ // SOOTSIM_FLOW_TIMEOUT_SCALE for the recording lane so a slow-but-working app
5
+ // clears its waits without per-app timeouts. off (1x) locally, clamped to 1..10.
6
+ //
7
+ // every deadline a hosted launchApp has to clear must use this, or the lane is
8
+ // only half-scaled: the flow runner's own waits scaled while the CLI's
9
+ // post-navigation readiness poll kept a flat ceiling, so a launchApp that was
10
+ // working but slow failed the flow.
11
+ export const FLOW_TIMEOUT_SCALE_ENV = 'SOOTSIM_FLOW_TIMEOUT_SCALE'
12
+
13
+ export function flowTimeoutScale(): number {
14
+ return Math.min(10, Math.max(1, Number(process.env[FLOW_TIMEOUT_SCALE_ENV]) || 1))
15
+ }