sootsim 0.1.285 → 0.1.287

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 (157) hide show
  1. package/README.md +28 -4
  2. package/dist-cli/bin.js +7 -5
  3. package/dist-cli/chunks/{agent-2TBHFU5M.js → agent-3Y7PIJAD.js} +2 -2
  4. package/dist-cli/chunks/{agent-wrapper-LJ4N3HMI.js → agent-wrapper-M2GB3CFF.js} +2 -2
  5. package/dist-cli/chunks/{app-fonts-GQDMATKJ.js → app-fonts-OSUZGXLN.js} +2 -2
  6. package/dist-cli/chunks/{assert-RD4NYOEL.js → assert-VGMB3XLU.js} +2 -2
  7. package/dist-cli/chunks/{auth-3EU7SXIS.js → auth-C2D2XI5T.js} +2 -2
  8. package/dist-cli/chunks/beta-VB4CHGW7.js +2 -0
  9. package/dist-cli/chunks/{camera-2J7LHKH2.js → camera-2EP7WI7X.js} +2 -2
  10. package/dist-cli/chunks/{chunk-FECFOHRP.js → chunk-2ED7AEFH.js} +2 -2
  11. package/dist-cli/chunks/{chunk-R5AGV63G.js → chunk-4LQHUMJD.js} +1 -1
  12. package/dist-cli/chunks/{chunk-RF2D4REN.js → chunk-4YUQA5EZ.js} +1 -1
  13. package/dist-cli/chunks/{chunk-NSMCMJG3.js → chunk-5BH4EGSX.js} +1 -1
  14. package/dist-cli/chunks/{chunk-UTAHGCSP.js → chunk-6JYHFQMU.js} +2 -2
  15. package/dist-cli/chunks/{chunk-MCHPBSUT.js → chunk-7NHETHT2.js} +2 -2
  16. package/dist-cli/chunks/{chunk-QWSPXKQJ.js → chunk-7TTDBWWX.js} +3 -3
  17. package/dist-cli/chunks/{chunk-E6HIXOCT.js → chunk-7XETMEVG.js} +2 -2
  18. package/dist-cli/chunks/{chunk-CAXY5LGF.js → chunk-AUPDIMGR.js} +3 -3
  19. package/dist-cli/chunks/{chunk-FD6OUECL.js → chunk-B4OWAS67.js} +4 -4
  20. package/dist-cli/chunks/{chunk-UPHIANHH.js → chunk-CAVS43NZ.js} +2 -2
  21. package/dist-cli/chunks/{chunk-H6VB7Q6V.js → chunk-D32WJN5T.js} +1 -1
  22. package/dist-cli/chunks/{chunk-D7T3DWLA.js → chunk-DH4IAIMY.js} +2 -2
  23. package/dist-cli/chunks/{chunk-J6PGQR2G.js → chunk-DMJCPQ5G.js} +2 -2
  24. package/dist-cli/chunks/{chunk-NGL23MAI.js → chunk-DWZBBA2R.js} +2 -2
  25. package/dist-cli/chunks/{chunk-ITXBTNQO.js → chunk-EOWY6ENX.js} +1 -1
  26. package/dist-cli/chunks/chunk-FAKE23IY.js +2 -0
  27. package/dist-cli/chunks/{chunk-UBCUQYQB.js → chunk-FJXEQORS.js} +1 -1
  28. package/dist-cli/chunks/{chunk-TV2JCOBT.js → chunk-GJWLFLCH.js} +1 -1
  29. package/dist-cli/chunks/{chunk-MDI4VFTU.js → chunk-IL67IGWO.js} +1 -1
  30. package/dist-cli/chunks/{chunk-7QVXBXHE.js → chunk-JMCFZU73.js} +1 -1
  31. package/dist-cli/chunks/{chunk-6Z43B67G.js → chunk-JNF5Q27W.js} +3 -3
  32. package/dist-cli/chunks/{chunk-6KJIC7PM.js → chunk-KNANFWH6.js} +32 -8
  33. package/dist-cli/chunks/{chunk-RJPFXKUC.js → chunk-LNEO6ZTY.js} +3 -3
  34. package/dist-cli/chunks/chunk-LS4XU3D6.js +1 -0
  35. package/dist-cli/chunks/{chunk-ZCMLLZW7.js → chunk-LSARMXMK.js} +2 -2
  36. package/dist-cli/chunks/{chunk-ENRZ35MX.js → chunk-LWCQCC4X.js} +1 -1
  37. package/dist-cli/chunks/chunk-MS3I76VW.js +2 -0
  38. package/dist-cli/chunks/{chunk-EUOPHGBA.js → chunk-O564XXIK.js} +2 -2
  39. package/dist-cli/chunks/chunk-O5JBF7EI.js +2 -0
  40. package/dist-cli/chunks/{chunk-K3VOQJT4.js → chunk-O722YE2D.js} +3 -3
  41. package/dist-cli/chunks/{chunk-MVF6FAIR.js → chunk-OZSLCAR3.js} +1 -1
  42. package/dist-cli/chunks/chunk-PVO7KJ4U.js +118 -0
  43. package/dist-cli/chunks/chunk-SALM6SYB.js +4 -0
  44. package/dist-cli/chunks/{chunk-XHGF55GE.js → chunk-UYVRZAAM.js} +1 -1
  45. package/dist-cli/chunks/{chunk-EPCTUT5H.js → chunk-V7QFQAEP.js} +1 -1
  46. package/dist-cli/chunks/chunk-VG4BRVYH.js +72 -0
  47. package/dist-cli/chunks/{chunk-KYD5EOWU.js → chunk-VYCARR66.js} +1 -1
  48. package/dist-cli/chunks/{chunk-L6JX65OD.js → chunk-W2N5SGMP.js} +1 -1
  49. package/dist-cli/chunks/{chunk-H3USGI4Z.js → chunk-WDDAJEF2.js} +2 -2
  50. package/dist-cli/chunks/{chunk-WKK5ABFW.js → chunk-WO5JLB3Y.js} +1 -1
  51. package/dist-cli/chunks/chunk-X2QGJ365.js +4 -0
  52. package/dist-cli/chunks/{chunk-RNUOUDDU.js → chunk-XNAXG3CO.js} +2 -2
  53. package/dist-cli/chunks/{chunk-LMXSSAVD.js → chunk-XNNQOIEW.js} +1 -1
  54. package/dist-cli/chunks/{chunk-II5VUIPW.js → chunk-XYW7VEC5.js} +2 -2
  55. package/dist-cli/chunks/{chunk-NQQPRV7X.js → chunk-YMRVH5M7.js} +2 -2
  56. package/dist-cli/chunks/cleanup-4CMHC5XY.js +23 -0
  57. package/dist-cli/chunks/cli-version-TZMWSGH2.js +2 -0
  58. package/dist-cli/chunks/{compat-LEM4VU7P.js → compat-AEK4ZDP6.js} +3 -3
  59. package/dist-cli/chunks/{config-HS34TNDH.js → config-E2DPFBWE.js} +2 -2
  60. package/dist-cli/chunks/control-JNIZAS3K.js +2 -0
  61. package/dist-cli/chunks/daemon-7PINPJHO.js +2 -0
  62. package/dist-cli/chunks/{debug-WVZX3PQR.js → debug-GCMVHL3Y.js} +3 -3
  63. package/dist-cli/chunks/{desktop-CLQJRQZG.js → desktop-U34JDZSL.js} +3 -3
  64. package/dist-cli/chunks/detox-HFGJMV6S.js +50 -0
  65. package/dist-cli/chunks/{device-OD752JLS.js → device-2JSOB3ND.js} +2 -2
  66. package/dist-cli/chunks/{diagnose-6ESOVAFY.js → diagnose-VGZOE32C.js} +2 -2
  67. package/dist-cli/chunks/disk-cleanup-II246KS3.js +2 -0
  68. package/dist-cli/chunks/drivers-U44673LK.js +2 -0
  69. package/dist-cli/chunks/{film-B4LMPUP7.js → film-4B5RQBZA.js} +3 -3
  70. package/dist-cli/chunks/flow-4PV7VMJX.js +2 -0
  71. package/dist-cli/chunks/help-ZY4B4G45.js +2 -0
  72. package/dist-cli/chunks/{hidden-runtime-alias-UKOTOYAU.js → hidden-runtime-alias-XZPYIJJL.js} +2 -2
  73. package/dist-cli/chunks/{home-paths-YR2DGQKT.js → home-paths-URBZE5DX.js} +2 -2
  74. package/dist-cli/chunks/inspect-2KZSDWZ2.js +2 -0
  75. package/dist-cli/chunks/install-desktop-66NOHS5U.js +2 -0
  76. package/dist-cli/chunks/{login-7LKF3FZ5.js → login-6ZZQHORX.js} +4 -4
  77. package/dist-cli/chunks/{logout-T6UDYMX7.js → logout-R3H4H5XS.js} +2 -2
  78. package/dist-cli/chunks/{maestro-GFPPYRP7.js → maestro-TJ5IJVWZ.js} +3 -3
  79. package/dist-cli/chunks/{maestro-generate-INLKO6J5.js → maestro-generate-BM3TFJM3.js} +3 -3
  80. package/dist-cli/chunks/{mode-D4CFDDM5.js → mode-QEHTO5QB.js} +2 -2
  81. package/dist-cli/chunks/optional-demo-registry-DHBZACNG.js +2 -0
  82. package/dist-cli/chunks/{perf-ZHZEQ5CJ.js → perf-GQ6H7BMI.js} +2 -2
  83. package/dist-cli/chunks/{record-YMQVDSIC.js → record-UNDNOP7I.js} +3 -3
  84. package/dist-cli/chunks/runtime-U6UYTNPH.js +2 -0
  85. package/dist-cli/chunks/{screenshot-command-Y4BENO6D.js → screenshot-command-ONR3W4IB.js} +3 -3
  86. package/dist-cli/chunks/{screenshots-capture-MT4ZDZUF.js → screenshots-capture-ME4I6HDK.js} +2 -2
  87. package/dist-cli/chunks/{serve-G633HTUD.js → serve-76PISKNL.js} +18 -18
  88. package/dist-cli/chunks/{setup-FNATHLVM.js → setup-TJMKUTMZ.js} +3 -3
  89. package/dist-cli/chunks/{skills-WZND2RAU.js → skills-RWONRRTS.js} +2 -2
  90. package/dist-cli/chunks/{storage-YRWEDN56.js → storage-K42LEKBP.js} +2 -2
  91. package/dist-cli/chunks/store-GZ3LRYT5.js +2 -0
  92. package/dist-cli/chunks/telemetry-2QOKHTLI.js +2 -0
  93. package/dist-cli/chunks/{timeline-WSS6HKAH.js → timeline-W7O56I3Y.js} +2 -2
  94. package/dist-cli/chunks/{upgrade-I5GYH34L.js → upgrade-E42I2WQ6.js} +4 -4
  95. package/dist-cli/chunks/upload-TPETTHGS.js +2 -0
  96. package/dist-cli/chunks/{version-O4HGTRSA.js → version-KYDANRIA.js} +3 -3
  97. package/dist-cli/chunks/{web-L7JORNHF.js → web-BZX7OTFR.js} +2 -2
  98. package/dist-cli/chunks/{what-happened-ALECI5TF.js → what-happened-EU22AIF6.js} +2 -2
  99. package/dist-lib/agent-daemon-client.cjs +1 -1
  100. package/dist-lib/agent-events.cjs +1 -1
  101. package/dist-lib/agent-identity.cjs +1 -1
  102. package/dist-lib/agent-sessions.cjs +1 -1
  103. package/dist-lib/attached-projects.cjs +1 -1
  104. package/dist-lib/auth/shared-session.cjs +1 -1
  105. package/dist-lib/backend-origin.cjs +1 -1
  106. package/dist-lib/beta.cjs +1 -1
  107. package/dist-lib/beta.mjs +1 -1
  108. package/dist-lib/bridge-constants.cjs +1 -1
  109. package/dist-lib/cli-constants.cjs +1 -1
  110. package/dist-lib/config.cjs +1 -1
  111. package/dist-lib/detox/index.cjs +1 -1
  112. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  113. package/dist-lib/home-paths.cjs +1 -1
  114. package/dist-lib/host/bridge-host.cjs +74 -9
  115. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  116. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  117. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  118. package/dist-lib/host/websocket-proxy.cjs +1 -1
  119. package/dist-lib/index.cjs +1 -1
  120. package/dist-lib/jump-to-source-babel.cjs +1 -1
  121. package/dist-lib/menu.cjs +1 -1
  122. package/dist-lib/menu.mjs +1 -1
  123. package/dist-lib/metro.cjs +1 -1
  124. package/dist-lib/profiles.cjs +1 -1
  125. package/dist-lib/render-mode.cjs +1 -1
  126. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  127. package/dist-lib/sdk.cjs +1 -1
  128. package/dist-lib/sdk.mjs +1 -1
  129. package/dist-lib/skills.cjs +76 -10
  130. package/dist-lib/vite.cjs +1 -1
  131. package/package.json +1 -1
  132. package/scripts/preuninstall.cjs +2 -0
  133. package/skills/contrast/SKILL.md +1 -0
  134. package/src/browser-cache.ts +16 -0
  135. package/src/disk-cleanup.ts +487 -0
  136. package/src/home-paths.ts +2 -0
  137. package/dist-cli/chunks/beta-YS62NT2Q.js +0 -2
  138. package/dist-cli/chunks/chunk-4ER5VV7N.js +0 -118
  139. package/dist-cli/chunks/chunk-HMZEXILJ.js +0 -2
  140. package/dist-cli/chunks/chunk-J6JBIQCU.js +0 -1
  141. package/dist-cli/chunks/chunk-LC7PERGZ.js +0 -4
  142. package/dist-cli/chunks/chunk-N5HM6MVL.js +0 -2
  143. package/dist-cli/chunks/chunk-P5ZWHBTB.js +0 -62
  144. package/dist-cli/chunks/cli-version-TXYQS4RH.js +0 -2
  145. package/dist-cli/chunks/control-B6AQSK5M.js +0 -2
  146. package/dist-cli/chunks/daemon-CXYLZLTA.js +0 -2
  147. package/dist-cli/chunks/detox-WUKQEG6W.js +0 -50
  148. package/dist-cli/chunks/drivers-QNTNLCAA.js +0 -2
  149. package/dist-cli/chunks/flow-RBT7ONHX.js +0 -2
  150. package/dist-cli/chunks/help-XSIDFM7C.js +0 -2
  151. package/dist-cli/chunks/inspect-X5QK55XO.js +0 -2
  152. package/dist-cli/chunks/install-desktop-2I52RLEI.js +0 -2
  153. package/dist-cli/chunks/optional-demo-registry-B4BZNEGJ.js +0 -2
  154. package/dist-cli/chunks/runtime-YSOWH56F.js +0 -2
  155. package/dist-cli/chunks/store-2CQBXLIL.js +0 -2
  156. package/dist-cli/chunks/telemetry-2ZOO7LP5.js +0 -2
  157. package/dist-cli/chunks/upload-TRUPFXJR.js +0 -2
@@ -0,0 +1,487 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { isDisposableBrowserCacheDir } from './browser-cache'
4
+ import {
5
+ ensureSootsimHome,
6
+ isDaemonLockfileFresh,
7
+ readDaemonLockfile,
8
+ sootsimHomeDir,
9
+ } from './home-paths'
10
+ import { sootsimRuntime } from './runtime-delivery'
11
+
12
+ const AUTOMATIC_CLEANUP_GENERATION = 1
13
+ const AUTOMATIC_CLEANUP_COMPLETE_FILE = `automatic-cleanup-v${AUTOMATIC_CLEANUP_GENERATION}.json`
14
+ const AUTOMATIC_CLEANUP_LOCK_FILE = `automatic-cleanup-v${AUTOMATIC_CLEANUP_GENERATION}.lock`
15
+ const AUTOMATIC_CLEANUP_LOCK_STALE_MS = 6 * 60 * 60 * 1000
16
+
17
+ export type SootsimCleanupKind =
18
+ | 'browser-cache'
19
+ | 'recording'
20
+ | 'runtime-archive'
21
+ | 'runtime-version'
22
+
23
+ export type SootsimCleanupState = 'aggressive' | 'in-use' | 'retained' | 'safe'
24
+
25
+ export interface SootsimCleanupEntry {
26
+ path: string
27
+ bytes: number
28
+ kind: SootsimCleanupKind
29
+ state: SootsimCleanupState
30
+ selected: boolean
31
+ detail: string
32
+ }
33
+
34
+ export interface SootsimCleanupOptions {
35
+ aggressive?: boolean
36
+ }
37
+
38
+ export interface SootsimCleanupPlan {
39
+ home: string
40
+ activeRuntime: string | null
41
+ pinnedRuntime: string | null
42
+ entries: SootsimCleanupEntry[]
43
+ reclaimableBytes: number
44
+ protectedBytes: number
45
+ totalBytes: number
46
+ }
47
+
48
+ export interface SootsimCleanupResult extends SootsimCleanupPlan {
49
+ removedPaths: string[]
50
+ reclaimedBytes: number
51
+ remainingBytes: number
52
+ }
53
+
54
+ export interface AutomaticSootsimCleanupResult {
55
+ status: 'already-complete' | 'busy' | 'complete' | 'deferred'
56
+ reclaimedBytes: number
57
+ remainingBytes: number
58
+ deferredInUseBytes: number
59
+ }
60
+
61
+ export function isAutomaticSootsimCleanupComplete(): boolean {
62
+ const completePath = path.join(sootsimHomeDir(), AUTOMATIC_CLEANUP_COMPLETE_FILE)
63
+ try {
64
+ const parsed = JSON.parse(fs.readFileSync(completePath, 'utf8')) as {
65
+ generation?: unknown
66
+ }
67
+ return parsed.generation === AUTOMATIC_CLEANUP_GENERATION
68
+ } catch {
69
+ return false
70
+ }
71
+ }
72
+
73
+ export function formatSootsimBytes(bytes: number): string {
74
+ if (bytes < 1024) return `${bytes} B`
75
+ const units = ['KiB', 'MiB', 'GiB', 'TiB']
76
+ let value = bytes / 1024
77
+ let unit = units[0]
78
+ for (let i = 1; i < units.length && value >= 1024; i++) {
79
+ value /= 1024
80
+ unit = units[i]
81
+ }
82
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`
83
+ }
84
+
85
+ function allocatedBytes(target: string): number {
86
+ let stat: fs.Stats
87
+ try {
88
+ stat = fs.lstatSync(target)
89
+ } catch {
90
+ return 0
91
+ }
92
+ const ownBytes = stat.blocks > 0 ? stat.blocks * 512 : stat.size
93
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return ownBytes
94
+ let total = ownBytes
95
+ try {
96
+ for (const child of fs.readdirSync(target)) {
97
+ total += allocatedBytes(path.join(target, child))
98
+ }
99
+ } catch {}
100
+ return total
101
+ }
102
+
103
+ function hasBrowserLock(profileRoot: string): boolean {
104
+ for (const name of ['SingletonLock', 'SingletonSocket', 'lockfile']) {
105
+ try {
106
+ if (fs.lstatSync(path.join(profileRoot, name))) return true
107
+ } catch {}
108
+ }
109
+ return false
110
+ }
111
+
112
+ function collectCacheDirs(root: string, profileRoot: string): SootsimCleanupEntry[] {
113
+ let children: fs.Dirent[]
114
+ try {
115
+ children = fs.readdirSync(root, { withFileTypes: true })
116
+ } catch {
117
+ return []
118
+ }
119
+ const inUse = hasBrowserLock(profileRoot)
120
+ const entries: SootsimCleanupEntry[] = []
121
+ for (const child of children) {
122
+ if (!child.isDirectory() || child.isSymbolicLink()) continue
123
+ const childPath = path.join(root, child.name)
124
+ if (isDisposableBrowserCacheDir(child.name)) {
125
+ entries.push({
126
+ path: childPath,
127
+ bytes: allocatedBytes(childPath),
128
+ kind: 'browser-cache',
129
+ state: inUse ? 'in-use' : 'safe',
130
+ selected: !inUse,
131
+ detail: inUse
132
+ ? 'browser profile is open; close it before cleanup'
133
+ : 'disposable browser HTTP, bytecode, or GPU cache',
134
+ })
135
+ continue
136
+ }
137
+ entries.push(...collectCacheDirs(childPath, profileRoot))
138
+ }
139
+ return entries
140
+ }
141
+
142
+ function collectPlaywrightCaches(home: string): SootsimCleanupEntry[] {
143
+ const root = path.join(home, 'profiles', 'playwright')
144
+ let profiles: fs.Dirent[]
145
+ try {
146
+ profiles = fs.readdirSync(root, { withFileTypes: true })
147
+ } catch {
148
+ return []
149
+ }
150
+ return profiles.flatMap((profile) => {
151
+ if (!profile.isDirectory() || profile.isSymbolicLink()) return []
152
+ const profileRoot = path.join(root, profile.name)
153
+ return collectCacheDirs(profileRoot, profileRoot)
154
+ })
155
+ }
156
+
157
+ function collectElectronCaches(home: string): SootsimCleanupEntry[] {
158
+ const root = path.join(home, 'electron', 'userData')
159
+ return collectCacheDirs(root, root)
160
+ }
161
+
162
+ function collectRecordingEntries(
163
+ home: string,
164
+ aggressive: boolean,
165
+ ): SootsimCleanupEntry[] {
166
+ const root = path.join(home, 'recordings')
167
+ let recordings: fs.Dirent[]
168
+ try {
169
+ recordings = fs.readdirSync(root, { withFileTypes: true })
170
+ } catch {
171
+ return []
172
+ }
173
+ return recordings.map((recording) => {
174
+ const recordingPath = path.join(root, recording.name)
175
+ return {
176
+ path: recordingPath,
177
+ bytes: allocatedBytes(recordingPath),
178
+ kind: 'recording',
179
+ state: 'aggressive',
180
+ selected: aggressive,
181
+ detail: 'user-created recording or captured-frame output',
182
+ }
183
+ })
184
+ }
185
+
186
+ function inUseRuntimeVersions(): string[] {
187
+ const lock = readDaemonLockfile()
188
+ if (!isDaemonLockfileFresh(lock) || !lock.activeRuntime) return []
189
+ return [lock.activeRuntime]
190
+ }
191
+
192
+ function runtimeEntries(aggressive: boolean): {
193
+ activeRuntime: string | null
194
+ pinnedRuntime: string | null
195
+ entries: SootsimCleanupEntry[]
196
+ } {
197
+ const inUseVersions = new Set(inUseRuntimeVersions())
198
+ const protectVersions = [...inUseVersions]
199
+ const normal = sootsimRuntime.planLocalRetention({
200
+ retainVersions: 2,
201
+ protectVersions,
202
+ })
203
+ const minimal = sootsimRuntime.planLocalRetention({
204
+ retainVersions: 1,
205
+ protectVersions,
206
+ })
207
+ const normallyRemoved = new Set(normal.removeVersions)
208
+ const aggressivelyRemoved = new Set(minimal.removeVersions)
209
+ const allVersions = new Set([
210
+ ...normal.keepVersions,
211
+ ...normal.removeVersions,
212
+ ...minimal.keepVersions,
213
+ ...minimal.removeVersions,
214
+ ])
215
+ const entries: SootsimCleanupEntry[] = []
216
+
217
+ for (const version of [...allVersions].sort()) {
218
+ const runtimePath = sootsimRuntime.product.runtimeDir(version)
219
+ let state: SootsimCleanupState = 'retained'
220
+ let detail = 'retained runtime'
221
+ if (
222
+ inUseVersions.has(version) &&
223
+ version !== normal.activeVersion &&
224
+ version !== normal.pinnedVersion
225
+ ) {
226
+ state = 'in-use'
227
+ detail = 'runtime is served by the running daemon'
228
+ } else if (normallyRemoved.has(version)) {
229
+ state = 'safe'
230
+ detail = 'inactive runtime outside the two-version rollback window'
231
+ } else if (aggressivelyRemoved.has(version)) {
232
+ state = 'aggressive'
233
+ detail = 'most recent rollback runtime'
234
+ } else if (version === normal.activeVersion) {
235
+ detail = 'active runtime; always protected'
236
+ } else if (version === normal.pinnedVersion) {
237
+ detail = 'pinned runtime; always protected'
238
+ }
239
+ entries.push({
240
+ path: runtimePath,
241
+ bytes: allocatedBytes(runtimePath),
242
+ kind: 'runtime-version',
243
+ state,
244
+ selected: state === 'safe' || (state === 'aggressive' && aggressive),
245
+ detail,
246
+ })
247
+ }
248
+
249
+ for (const cacheFile of normal.removeCacheFiles) {
250
+ entries.push({
251
+ path: cacheFile,
252
+ bytes: allocatedBytes(cacheFile),
253
+ kind: 'runtime-archive',
254
+ state: 'safe',
255
+ selected: true,
256
+ detail: 'downloaded archive is unnecessary after successful extraction',
257
+ })
258
+ }
259
+
260
+ return {
261
+ activeRuntime: normal.activeVersion,
262
+ pinnedRuntime: normal.pinnedVersion,
263
+ entries,
264
+ }
265
+ }
266
+
267
+ export function planSootsimCleanup(
268
+ options: SootsimCleanupOptions = {},
269
+ ): SootsimCleanupPlan {
270
+ const aggressive = options.aggressive === true
271
+ const home = sootsimHomeDir()
272
+ const runtimes = runtimeEntries(aggressive)
273
+ const entries = [
274
+ ...runtimes.entries,
275
+ ...collectPlaywrightCaches(home),
276
+ ...collectElectronCaches(home),
277
+ ...collectRecordingEntries(home, aggressive),
278
+ ].sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path))
279
+ return {
280
+ home,
281
+ activeRuntime: runtimes.activeRuntime,
282
+ pinnedRuntime: runtimes.pinnedRuntime,
283
+ entries,
284
+ reclaimableBytes: entries.reduce(
285
+ (total, entry) => total + (entry.selected ? entry.bytes : 0),
286
+ 0,
287
+ ),
288
+ protectedBytes: entries.reduce(
289
+ (total, entry) => total + (!entry.selected ? entry.bytes : 0),
290
+ 0,
291
+ ),
292
+ totalBytes: allocatedBytes(home),
293
+ }
294
+ }
295
+
296
+ function assertInsideHome(home: string, target: string): void {
297
+ const relative = path.relative(home, target)
298
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
299
+ throw new Error(`refusing cleanup target outside the SootSim home: ${target}`)
300
+ }
301
+ }
302
+
303
+ export function applySootsimCleanup(
304
+ options: SootsimCleanupOptions = {},
305
+ ): SootsimCleanupResult {
306
+ const plan = planSootsimCleanup(options)
307
+ const retainVersions = options.aggressive === true ? 1 : 2
308
+ sootsimRuntime.pruneLocalRetention({
309
+ retainVersions,
310
+ protectVersions: inUseRuntimeVersions(),
311
+ })
312
+
313
+ for (const entry of plan.entries) {
314
+ if (!entry.selected) continue
315
+ if (entry.kind === 'runtime-archive' || entry.kind === 'runtime-version') continue
316
+ if (entry.kind === 'browser-cache') {
317
+ const relative = path.relative(plan.home, entry.path)
318
+ const parts = relative.split(path.sep)
319
+ const profileRoot =
320
+ parts[0] === 'profiles' && parts[1] === 'playwright' && parts[2]
321
+ ? path.join(plan.home, 'profiles', 'playwright', parts[2])
322
+ : parts[0] === 'electron' && parts[1] === 'userData'
323
+ ? path.join(plan.home, 'electron', 'userData')
324
+ : null
325
+ if (profileRoot && hasBrowserLock(profileRoot)) {
326
+ entry.state = 'in-use'
327
+ entry.selected = false
328
+ entry.detail = 'browser profile opened while cleanup was starting'
329
+ continue
330
+ }
331
+ }
332
+ assertInsideHome(plan.home, entry.path)
333
+ fs.rmSync(entry.path, { recursive: true, force: true })
334
+ }
335
+
336
+ const removedPaths = plan.entries
337
+ .filter((entry) => entry.selected && !fs.existsSync(entry.path))
338
+ .map((entry) => entry.path)
339
+ const remainingBytes = allocatedBytes(plan.home)
340
+ return {
341
+ ...plan,
342
+ removedPaths,
343
+ reclaimedBytes: Math.max(0, plan.totalBytes - remainingBytes),
344
+ remainingBytes,
345
+ }
346
+ }
347
+
348
+ export function applyAutomaticSootsimCleanup(): AutomaticSootsimCleanupResult {
349
+ ensureSootsimHome()
350
+ const home = sootsimHomeDir()
351
+ const completePath = path.join(home, AUTOMATIC_CLEANUP_COMPLETE_FILE)
352
+ const lockPath = path.join(home, AUTOMATIC_CLEANUP_LOCK_FILE)
353
+
354
+ if (isAutomaticSootsimCleanupComplete()) {
355
+ return {
356
+ status: 'already-complete',
357
+ reclaimedBytes: 0,
358
+ remainingBytes: 0,
359
+ deferredInUseBytes: 0,
360
+ }
361
+ }
362
+
363
+ let lockFd: number | null = null
364
+ for (let attempt = 0; attempt < 2 && lockFd === null; attempt++) {
365
+ try {
366
+ const candidateFd = fs.openSync(lockPath, 'wx')
367
+ try {
368
+ fs.writeFileSync(
369
+ candidateFd,
370
+ `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`,
371
+ 'utf8',
372
+ )
373
+ lockFd = candidateFd
374
+ } catch (error) {
375
+ fs.closeSync(candidateFd)
376
+ try {
377
+ fs.unlinkSync(lockPath)
378
+ } catch {}
379
+ throw error
380
+ }
381
+ } catch (error) {
382
+ const code = error instanceof Error && 'code' in error ? error.code : null
383
+ if (code !== 'EEXIST') throw error
384
+
385
+ let ownerPid = 0
386
+ let ownerStartedAt = 0
387
+ let originalStat: fs.Stats | null = null
388
+ try {
389
+ originalStat = fs.lstatSync(lockPath)
390
+ const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as {
391
+ pid?: unknown
392
+ startedAt?: unknown
393
+ }
394
+ ownerPid = typeof owner.pid === 'number' ? owner.pid : 0
395
+ ownerStartedAt = typeof owner.startedAt === 'number' ? owner.startedAt : 0
396
+ } catch {}
397
+ if (
398
+ ownerPid > 0 &&
399
+ ownerStartedAt > 0 &&
400
+ Date.now() - ownerStartedAt < AUTOMATIC_CLEANUP_LOCK_STALE_MS
401
+ ) {
402
+ try {
403
+ process.kill(ownerPid, 0)
404
+ return {
405
+ status: 'busy',
406
+ reclaimedBytes: 0,
407
+ remainingBytes: 0,
408
+ deferredInUseBytes: 0,
409
+ }
410
+ } catch {}
411
+ }
412
+ if (!originalStat) continue
413
+ try {
414
+ const currentStat = fs.lstatSync(lockPath)
415
+ if (
416
+ currentStat.dev !== originalStat.dev ||
417
+ currentStat.ino !== originalStat.ino
418
+ ) {
419
+ return {
420
+ status: 'busy',
421
+ reclaimedBytes: 0,
422
+ remainingBytes: 0,
423
+ deferredInUseBytes: 0,
424
+ }
425
+ }
426
+ fs.unlinkSync(lockPath)
427
+ } catch {}
428
+ }
429
+ }
430
+
431
+ if (lockFd === null) {
432
+ return {
433
+ status: 'busy',
434
+ reclaimedBytes: 0,
435
+ remainingBytes: 0,
436
+ deferredInUseBytes: 0,
437
+ }
438
+ }
439
+
440
+ try {
441
+ if (isAutomaticSootsimCleanupComplete()) {
442
+ return {
443
+ status: 'already-complete',
444
+ reclaimedBytes: 0,
445
+ remainingBytes: 0,
446
+ deferredInUseBytes: 0,
447
+ }
448
+ }
449
+
450
+ const result = applySootsimCleanup()
451
+ const deferredInUseBytes = result.entries.reduce(
452
+ (total, entry) => total + (entry.state === 'in-use' ? entry.bytes : 0),
453
+ 0,
454
+ )
455
+ if (deferredInUseBytes > 0) {
456
+ return {
457
+ status: 'deferred',
458
+ reclaimedBytes: result.reclaimedBytes,
459
+ remainingBytes: result.remainingBytes,
460
+ deferredInUseBytes,
461
+ }
462
+ }
463
+
464
+ const tmp = `${completePath}.${process.pid}.tmp`
465
+ fs.writeFileSync(
466
+ tmp,
467
+ `${JSON.stringify({
468
+ generation: AUTOMATIC_CLEANUP_GENERATION,
469
+ completedAt: Date.now(),
470
+ reclaimedBytes: result.reclaimedBytes,
471
+ })}\n`,
472
+ 'utf8',
473
+ )
474
+ fs.renameSync(tmp, completePath)
475
+ return {
476
+ status: 'complete',
477
+ reclaimedBytes: result.reclaimedBytes,
478
+ remainingBytes: result.remainingBytes,
479
+ deferredInUseBytes: 0,
480
+ }
481
+ } finally {
482
+ fs.closeSync(lockFd)
483
+ try {
484
+ fs.unlinkSync(lockPath)
485
+ } catch {}
486
+ }
487
+ }
package/src/home-paths.ts CHANGED
@@ -18,6 +18,8 @@
18
18
  // ├── runtime-upgraded.json one-shot notice written by the daemon's
19
19
  // │ background updater, consumed (printed + deleted)
20
20
  // │ by the next interactive CLI run
21
+ // ├── automatic-cleanup-v1.json completion marker for the one-time safe
22
+ // │ cleanup introduced for legacy unbounded data
21
23
  // └── config.json user prefs: runtime pin, update channel, cdn
22
24
  // origin override, telemetry, engine settings
23
25
 
@@ -1,2 +0,0 @@
1
- /*! sootsim v0.1.285 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import{a,b,c,d}from"./chunk-7QVXBXHE.js";import"./chunk-XHGF55GE.js";export{d as BETA_ASK_HEADLINE,b as BETA_LABEL,c as BETA_TAGLINE,a as IS_BETA};