stim 1.14.0 → 1.16.0

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 (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,29 +1,120 @@
1
- import { A as ownership_claim_exports, E as withConfigLock$1, I as volumeRootFor, M as formatBytes, N as isOnMountedVolume, P as listMountedVolumes, U as projectDeviceSlots, _ as loadConfig$1, j as directorySize, k as acquireAvdClaim, p as getProject, x as removeProject } from "./config-lvc7d3C2.mjs";
2
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { F as withDirLock, M as workspaceLogsDir, P as workspaceStateFile, k as workspaceDir, y as workspaceLastUsed } from "./metro-CcEo8Fl8.mjs";
4
- import { C as sourceCheckoutOf, b as resolveFullRef, d as hasUncommittedWork, l as hasPopulatedSubmodules, m as listWorktrees, o as dirtyPaths, w as unpushedCommits } from "./worktree-BVrHsYZG.mjs";
5
- import { a as findProjectRoot, r as detectIsExpo } from "./project-CWlvEaeN.mjs";
6
- import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-DVcpw6eE.mjs";
7
- import { d as removeExpiredLease, h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-5c3WrVRm.mjs";
8
- import { A as listInstalledSystemImages, B as parseAvdDeviceProfile, F as ownedAvdDirectory, R as ownedAvdSerialResolver, V as parseAvdSystemImage, a as androidHome, ct as parkedMaxSetting, f as avdIniPaths, g as avdStorageRoots, j as listOrphanedAvdDirectories, k as listAvds, lt as readParked, nt as isStimOwnedSim, p as avdNameAbsent, rt as POOL_SETTING_REMEDY, tt as isStimOwnedAvd, u as androidToolPath } from "./android-d6rGq1pA.mjs";
9
- import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-CllH_8eb.mjs";
10
- import { l as listAllIosSims } from "./ios-BwDPrnYQ.mjs";
11
- import { a as parseIdleDuration, c as recordGcResult, l as takeGcResults, o as shutDownIdleDevices, r as idleDeviceLines, t as collectIdleDevices } from "./idle-DXtx3RbX.mjs";
12
- import { i as canonicalPath, n as withIdleWorkspace, o as listBuildSlots, r as workspaceInUse } from "./in-use-DlqBDKqY.mjs";
13
- import { r as readCollectors } from "./ownership-DyYDF5ww.mjs";
14
- import { f as resolveEasCliBin } from "./remote-cache-CcR6o6lI.mjs";
15
- import { A as findOrphanedOwnedSessions, C as withEasProjectLock, E as removeEasSessionClaim, I as parseSessionListPage, L as stopSessionArgs, M as inspectSessionForTeardown, N as isDefinitiveMissingSessionError, P as listOwnedSessionsArgs, R as verifyStoppedSession, T as readEasSessionLedger$1, d as withRemoteSessionLock, j as getSessionArgs, w as easMachineStateRoot$1 } from "./device-remote-D87zEzaq.mjs";
16
- import { A as includesWorkspaceOutputs, C as deviceSweepIsScoped, D as unverifiedDeviceCommand, E as findStaleProjectDevices, F as sizeCaches, M as selectCaches, N as trimCaches, O as withAndroidAvdSizes, P as discoverCaches, S as describeUnverifiableDevices, T as findStaleDeviceRecords, _ as collectParkedSims, a as REBUILD_COST, b as deleteProjectDevices, c as collectOrphanedWorkspaces, d as isInsideWorkspaces, f as listWorkspaceDirs, g as collectParkedAvds, h as collectDeviceLeases, i as machineNumber, j as planCacheEmptying, k as emptyCaches, l as collectWorkspaceOutputs, m as DEVICE_LIST_TIMEOUT_MS, o as WORKSPACE_OUTPUT_DIRS, p as sizeText, s as clearWorkspaceOutputs, u as deleteOrphanedWorkspaces, v as deleteParkedAvds, w as findOrphanedDevices, x as describeParkedSims, y as deleteParkedSims } from "./budget-CGDkhvv_.mjs";
17
- import { _ as reclaimProject, a as reclaimKeys, d as removeWorktreeTarget, g as describeDereferenced, h as pullRequestLookups, m as endedPullRequest, n as excludePodChurn, p as describePullRequest, r as matchWorktreeEntry } from "./worktree-UrQWXSeb.mjs";
18
- import { n as mergeState, t as fetchDefaultBranch } from "./merge-state-ytFQCeh4.mjs";
19
- import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
20
- import { InvalidArgumentError } from "commander";
21
- import { closeSync as closeSync$1, existsSync as existsSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, utimesSync, writeFileSync as writeFileSync$1 } from "fs";
22
- import { basename, dirname, isAbsolute, join } from "path";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { A as directorySize, D as withConfigLock$1, F as volumeRootFor, H as projectDeviceSlots, M as isOnMountedVolume, N as listMountedVolumes, S as removeProject, _ as loadConfig$1, j as formatBytes, k as acquireAvdClaim, p as getProject } from "./config-D0DbpZtM.mjs";
3
+ import { n as ownership_claim_exports } from "./process-identity-BmRm1qV0.mjs";
4
+ import { n as describePullRequest, o as pullRequestLookups, r as endedPullRequest } from "./pull-request-pxnDCQzn.mjs";
5
+ import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-DnJMdEY-.mjs";
6
+ import { _t as resolveFullRef, bt as sourceCheckoutOf, ct as hasUncommittedWork, dt as listWorktrees, ot as hasPopulatedSubmodules, rt as dirtyPaths, xt as unpushedCommits } from "./settings-oTSkkNrM.mjs";
7
+ import { o as findProjectRoot$1, r as detectIsExpo } from "./project-Du91Ilvc.mjs";
8
+ import { C as workspaceStateFile, s as workspaceLastUsed, v as workspaceDir, w as withDirLock$1, x as workspaceLogsDir$1 } from "./workspace-state-B8-aJ1_5.mjs";
9
+ import { A as isWorkspacesScope, B as memoryCacheKind, C as unverifiedDeviceCommand, D as includesParkedDevices, E as includesArchives, F as trimCaches, G as reclaimMemory, H as memoryScope, K as scopedMemoryReport, M as previewScopedEmptying, N as selectCaches, O as includesRecordings, P as selectsBeyondCaches, R as collectMemoryReport, S as findStaleProjectDevices, T as emptyCaches, U as memorySweepIsScoped, V as memoryLines, W as memoryWorkPending, X as discoverCaches, Z as sizeCaches, _ as describeUnverifiableDevices, a as machineNumber, b as findOrphanedDevices, d as collectParkedAvds, f as collectParkedSims, g as describeParkedSims, h as deleteProjectDevices, j as planCacheEmptying, k as includesWorkspaceOutputs, l as DEVICE_LIST_TIMEOUT_MS, m as deleteParkedSims, p as deleteParkedAvds, q as phase, u as collectDeviceLeases, v as deviceSweepIsScoped, w as withAndroidAvdSizes, x as findStaleDeviceRecords, y as eraseParkedDevices } from "./budget-DjrURfQn.mjs";
10
+ import { a as takeGcResults, i as recordGcResult, n as withIdleWorkspace, o as canonicalPath, r as workspaceInUse } from "./in-use-DvBw0Kxo.mjs";
11
+ import { d as removeExpiredLease, h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-D-hLqx1b.mjs";
12
+ import { n as listBuildSlots } from "./build-slots-rUk77xqh.mjs";
13
+ import { a as parseIdleDuration, o as shutDownIdleDevices, r as idleDeviceLines, t as collectIdleDevices } from "./idle-DYVymG92.mjs";
14
+ import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-D5owjN4C.mjs";
15
+ import { A as listAvds, H as parseAvdSystemImage, I as ownedAvdDirectory, M as listOrphanedAvdDirectories, V as parseAvdDeviceProfile, _ as avdStorageRoots, at as isStimOwnedSim, d as androidToolPath, ft as parkedMaxSetting, it as isStimOwnedAvd, j as listInstalledSystemImages, m as avdNameAbsent, o as androidHome, ot as POOL_SETTING_REMEDY, p as avdIniPaths, pt as readParked, z as ownedAvdSerialResolver } from "./android-B_NumVyQ.mjs";
16
+ import { u as listAllIosSims } from "./ios-BaInn3sW.mjs";
17
+ import { a as collectWorkspaceOutputs, c as listWorkspaceDirs, i as collectOrphanedWorkspaces, l as rootPresence, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText } from "./workspaces-BV4pdHFz.mjs";
18
+ import { n as previewMaintenance } from "./preview-DhVbvPAZ.mjs";
19
+ import { r as readCollectors } from "./ownership-DiA16AsN.mjs";
20
+ import { f as resolveEasCliBin } from "./remote-cache-C8-V13XN.mjs";
21
+ import { C as listOwnedSessionsArgs, D as verifyStoppedSession, E as stopSessionArgs, S as isDefinitiveMissingSessionError, T as parseSessionListPage, b as getSessionArgs, d as withRemoteSessionLock, f as withEasProjectLock, h as removeEasSessionClaim, m as readEasSessionLedger$1, p as easMachineStateRoot$1, x as inspectSessionForTeardown, y as findOrphanedOwnedSessions } from "./device-remote-D5WVWqOq.mjs";
22
+ import { o as fetchDefaultBranch, s as mergeState } from "./warm-progress-CpGv3Wb6.mjs";
23
+ import { _ as deleteSelectedArchives, f as removeWorktreeTarget, g as reclaimProject, h as describeDereferenced, i as matchWorktreeEntry, n as excludePodChurn, o as reclaimKeys, r as excludeWatchmanCookies, v as sweepArchiveStaging } from "./worktree-K-xj9Db0.mjs";
24
+ import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-Dpih1Tfa.mjs";
25
+ import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
26
+ import { basename, dirname, join } from "node:path";
27
+ import { closeSync as closeSync$1, existsSync as existsSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, utimesSync as utimesSync$1, writeFileSync as writeFileSync$1 } from "fs";
28
+ import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1 } from "path";
23
29
  import { LOG_ROTATE_BYTES, rotatedLogPath } from "@stim-cli/core";
24
- import { settingDefinition } from "@stim-cli/core/state";
25
- import { dirname as dirname$1, join as join$1 } from "node:path";
30
+ import { InvalidArgumentError } from "commander";
31
+ import { listRecordedDevices, listStatusCacheEntries, maintenanceRunClaims, readArchives, recordedBytes, settingDefinition } from "@stim-cli/core/state";
26
32
  import chalk from "chalk";
33
+ //#region src/commands/gc/archives.ts
34
+ function stagingOrNone() {
35
+ try {
36
+ return sweepArchiveStaging(false, 5e3);
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+ function collectArchives(scope, olderThan, now) {
42
+ const wanted = scope.trim().toLowerCase();
43
+ const records = readArchives();
44
+ const id = wanted.startsWith("archived:") ? scope.trim().slice(9) : null;
45
+ const kind = wanted === "archived-logs" ? "logs" : wanted === "archived-recordings" ? "recordings" : wanted === "archived-agent" ? "agentActions" : null;
46
+ const match = id !== null ? records.find((record) => record.id.toLowerCase() === id.toLowerCase()) : null;
47
+ return {
48
+ records: records.filter((record) => id === null || record.id === match?.id).map((record) => ({
49
+ id: record.id,
50
+ kinds: kind ? [kind] : [
51
+ "logs",
52
+ "recordings",
53
+ "agentActions",
54
+ "record"
55
+ ].filter((key) => record.bytes[key] > 0),
56
+ bytes: kind ? record.bytes[kind] : record.bytes.total,
57
+ expires: record.expires,
58
+ willDelete: (id === null && olderThan !== null ? now - Date.parse(record.removedAt) >= olderThan * 864e5 : true) && (!kind || record.bytes[kind] > 0)
59
+ })),
60
+ staging: stagingOrNone(),
61
+ unknownId: id !== null && !match ? id : null,
62
+ knownIds: records.map((record) => record.id),
63
+ kind
64
+ };
65
+ }
66
+ function archiveLines(selection) {
67
+ if (!selection) return [];
68
+ return [
69
+ `Archived workspaces (${selection.records.length}):`,
70
+ ...selection.records.map((record) => ` ${record.id}: ${record.kinds.join(", ")}, ${formatBytes(record.bytes)}, expires ${JSON.stringify(record.expires)}${record.willDelete ? "" : " (kept)"}`),
71
+ ...selection.staging.map((entry) => ` ${entry.path}: ${entry.kept ? `kept: ${entry.reason}` : "abandoned staging"}${entry.removeCommand ? `; ${entry.removeCommand}` : ""}`)
72
+ ];
73
+ }
74
+ function deleteArchives(selection) {
75
+ if (!selection) return 0;
76
+ let failures = 0;
77
+ for (const record of selection.records.filter((entry) => entry.willDelete)) try {
78
+ deleteSelectedArchives([record.id], selection.kind);
79
+ recordGcResult("archive", "done", record.id, {
80
+ id: record.id,
81
+ bytes: record.bytes
82
+ });
83
+ } catch (error) {
84
+ failures++;
85
+ recordGcResult("archive", "failed", record.id, {
86
+ id: record.id,
87
+ detail: error.message
88
+ });
89
+ console.error(`Could not delete ${record.id}: ${error.message}`);
90
+ }
91
+ try {
92
+ const remaining = new Set(sweepArchiveStaging(true, 5e3).map((entry) => entry.path));
93
+ for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path)) recordGcResult("archive", "done", "archive staging", { id: entry.path });
94
+ } catch (error) {
95
+ failures++;
96
+ console.error(`Could not clear archive staging: ${error.message}`);
97
+ }
98
+ return failures;
99
+ }
100
+ function archiveRefusal(selection) {
101
+ if (selection?.unknownId === void 0 || selection.unknownId === null) return null;
102
+ process.exitCode = 1;
103
+ const message = `Unknown archive "${selection.unknownId}".`;
104
+ const remedy = `Known archive ids: ${selection.knownIds.join(", ") || "(none)"}`;
105
+ console.error(chalk.red(message));
106
+ console.error(chalk.dim(remedy));
107
+ console.error(chalk.red("failed: STIM_BAD_ARG"));
108
+ return {
109
+ code: "STIM_BAD_ARG",
110
+ message,
111
+ remedy
112
+ };
113
+ }
114
+ function archiveWorkPending(selection, otherWork) {
115
+ return otherWork || Boolean(selection?.records.some((record) => record.willDelete) || selection?.staging.some((entry) => !entry.kept));
116
+ }
117
+ //#endregion
27
118
  //#region src/commands/gc/eas-sessions.ts
28
119
  const EAS_OPERATION_TIMEOUT_MS = 3e4;
29
120
  const EAS_COLLECTION_TIMEOUT_MS = 6e4;
@@ -87,7 +178,7 @@ function readRecordedRemoteSessionIds(roots) {
87
178
  };
88
179
  }
89
180
  function claimStateAbsence(claim) {
90
- const stateDir = dirname(claim.stateFile);
181
+ const stateDir = dirname$1(claim.stateFile);
91
182
  let entries;
92
183
  try {
93
184
  if (!statSync$1(stateDir).isDirectory()) throw new Error("path is not a directory");
@@ -98,7 +189,7 @@ function claimStateAbsence(claim) {
98
189
  notice: `${stateDir} is not available for EAS ownership verification: ${describeError(error)}`
99
190
  };
100
191
  }
101
- if (!entries.includes(basename(claim.stateFile))) return {
192
+ if (!entries.includes(basename$1(claim.stateFile))) return {
102
193
  absent: true,
103
194
  notice: null
104
195
  };
@@ -135,7 +226,7 @@ function claimStateAbsence(claim) {
135
226
  };
136
227
  }
137
228
  function collectEasSessionSweep(config, deps) {
138
- const projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
229
+ const projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(process.cwd());
139
230
  const empty = (notices = []) => ({
140
231
  projectScope: projectRoot,
141
232
  orphaned: [],
@@ -230,7 +321,7 @@ function unavailableLockRootNotice(roots) {
230
321
  }
231
322
  function malformedRemoteSessionLockNotice(roots) {
232
323
  for (const root of roots) {
233
- const lockPath = join(workspaceDir(root), "remote-session.lock");
324
+ const lockPath = join$1(workspaceDir(root), "remote-session.lock");
234
325
  if (!existsSync$1(lockPath)) continue;
235
326
  if ((0, ownership_claim_exports.readClaimSet)(lockPath).unresolved.length === 0) continue;
236
327
  return `EAS session sweep skipped: the remote-session lock at ${lockPath} has a malformed owner record.`;
@@ -443,7 +534,7 @@ async function deleteEasSessions(easSessionSweep, deps) {
443
534
  //#endregion
444
535
  //#region src/commands/gc/worktrees.ts
445
536
  const DEFAULT_WORKTREE_IDLE_DAYS = 7;
446
- const DAY_MS = 864e5;
537
+ const DAY_MS$1 = 864e5;
447
538
  const MINUTE_MS = 6e4;
448
539
  const GRACE_SETTING = "gc.worktreeGraceMinutes";
449
540
  const MERGE_DECIDES = /* @__PURE__ */ new Set([
@@ -504,7 +595,7 @@ function removalBlocker(facts, olderThan) {
504
595
  if (facts.locked) return skip("locked", "locked with git worktree lock");
505
596
  if (facts.inUse.length) return skip("in-use", `in use: ${facts.inUse.join("; ")}`);
506
597
  if (facts.porcelain === null) return skip("status-unreadable", "git status could not be read");
507
- const dirty = excludePodChurn(facts.porcelain).lines.length;
598
+ const dirty = excludePodChurn(excludeWatchmanCookies(facts.porcelain).lines).lines.length;
508
599
  if (dirty) return skip("dirty", `dirty: ${plural(dirty, "uncommitted or untracked file")}`);
509
600
  if (facts.unpushed === null) return skip("unpushed-unchecked", "unpushed commits could not be checked");
510
601
  const merged = facts.merge?.merged ? facts.merge : null;
@@ -536,7 +627,7 @@ function lastUsedOf(keys) {
536
627
  }
537
628
  function idleDaysOf(keys, now) {
538
629
  const last = lastUsedOf(keys);
539
- return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS)) : null;
630
+ return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS$1)) : null;
540
631
  }
541
632
  function deviceReads() {
542
633
  let sims;
@@ -605,10 +696,10 @@ function newestMtime(paths) {
605
696
  return newest;
606
697
  }
607
698
  function logFiles$1(key) {
608
- const dir = workspaceLogsDir(key);
699
+ const dir = workspaceLogsDir$1(key);
609
700
  try {
610
701
  return readdirSync$1(dir).map((name) => ({
611
- path: join(dir, name),
702
+ path: join$1(dir, name),
612
703
  basis: "a Stim log write"
613
704
  }));
614
705
  } catch (error) {
@@ -627,15 +718,15 @@ function worktreeActivityOf(path, keys) {
627
718
  if (!gitDir) return null;
628
719
  const paths = [
629
720
  {
630
- path: join(gitDir, "index"),
721
+ path: join$1(gitDir, "index"),
631
722
  basis: "a git index write"
632
723
  },
633
724
  {
634
- path: join(gitDir, "HEAD"),
725
+ path: join$1(gitDir, "HEAD"),
635
726
  basis: "a git HEAD change"
636
727
  },
637
728
  {
638
- path: join(gitDir, "logs", "HEAD"),
729
+ path: join$1(gitDir, "logs", "HEAD"),
639
730
  basis: "a git reflog entry"
640
731
  }
641
732
  ];
@@ -666,7 +757,7 @@ function porcelainOf(path, gitAnswered) {
666
757
  return lines.length ? lines : null;
667
758
  }
668
759
  function candidateRoots() {
669
- const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute);
760
+ const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute$1);
670
761
  const recorded = listWorkspaceDirs().flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : []);
671
762
  return [.../* @__PURE__ */ new Set([...registered, ...recorded])].filter((root) => existsSync$1(root)).toSorted();
672
763
  }
@@ -708,7 +799,9 @@ async function collectWorktreeSweep({ idle, olderThan, now }) {
708
799
  };
709
800
  const groups = /* @__PURE__ */ new Map();
710
801
  const outside = [];
711
- for (const root of candidateRoots()) {
802
+ const candidates = candidateRoots();
803
+ if (candidates.length) phase("worktrees", `checking ${plural(candidates.length, "project")} for merged or idle worktrees`);
804
+ for (const root of candidates) {
712
805
  const entry = matchWorktreeEntry(listWorktrees(root), root);
713
806
  if (!entry) {
714
807
  outside.push({
@@ -879,6 +972,206 @@ async function removeWorktrees(sweep, { now = Date.now() } = {}) {
879
972
  return failures;
880
973
  }
881
974
  //#endregion
975
+ //#region src/commands/gc/status-caches.ts
976
+ const KINDS = ["disk-usage", "pull-request"];
977
+ const KEPT_REASON = {
978
+ unreadable: "the file records no readable path of its own",
979
+ unknown: "Stim cannot tell whether the folder it measures still exists",
980
+ unmounted: "the volume of the folder it measures is not mounted"
981
+ };
982
+ function witnessPath(path) {
983
+ return basename(path).startsWith("node_modules") ? dirname(path) : path;
984
+ }
985
+ function judge(path, mountedVolumes) {
986
+ if (path === null) return "unreadable";
987
+ const witness = witnessPath(path);
988
+ const present = rootPresence(witness);
989
+ if (present === null) return "unknown";
990
+ if (present) return "present";
991
+ return isOnMountedVolume(witness, mountedVolumes) ? "gone" : "unmounted";
992
+ }
993
+ function classifyStatusCaches(entries, mountedVolumes) {
994
+ const stale = [];
995
+ const kept = /* @__PURE__ */ new Map();
996
+ for (const { kind, file, path, bytes } of entries) {
997
+ const verdict = judge(path, mountedVolumes);
998
+ if (verdict === "present") continue;
999
+ if (verdict === "gone") {
1000
+ stale.push({
1001
+ kind,
1002
+ file,
1003
+ path,
1004
+ bytes
1005
+ });
1006
+ continue;
1007
+ }
1008
+ const dir = dirname(file);
1009
+ const key = `${dir}\n${verdict}`;
1010
+ const seen = kept.get(key);
1011
+ if (seen) seen.count++;
1012
+ else kept.set(key, {
1013
+ dir,
1014
+ verdict,
1015
+ count: 1
1016
+ });
1017
+ }
1018
+ return {
1019
+ stale,
1020
+ skipped: [...kept.values()].map(({ dir, verdict, count }) => ({
1021
+ dir,
1022
+ reason: `${count} entr${count === 1 ? "y" : "ies"} kept: ${KEPT_REASON[verdict]}`
1023
+ }))
1024
+ };
1025
+ }
1026
+ function collectStaleStatusCaches(mountedVolumes) {
1027
+ return classifyStatusCaches(listStatusCacheEntries(), mountedVolumes);
1028
+ }
1029
+ function statusCacheLines(stale) {
1030
+ if (!stale.length) return [];
1031
+ const total = stale.reduce((sum, entry) => sum + entry.bytes, 0);
1032
+ const lines = [`Stale status cache entries (${stale.length}, ${formatBytes(total)}) - the folder or worktree they measure is gone:`];
1033
+ for (const kind of KINDS) {
1034
+ const count = stale.filter((entry) => entry.kind === kind).length;
1035
+ if (count) lines.push(` ${kind}: ${count}`);
1036
+ }
1037
+ lines.push(" --delete removes them; `stim status` measures a folder again if it comes back.");
1038
+ return lines;
1039
+ }
1040
+ function deleteStaleStatusCaches(stale) {
1041
+ const mountedVolumes = listMountedVolumes();
1042
+ let failures = 0;
1043
+ for (const kind of KINDS) {
1044
+ let removed = 0;
1045
+ let bytes = 0;
1046
+ for (const entry of stale.filter((candidate) => candidate.kind === kind)) {
1047
+ if (judge(entry.path, mountedVolumes) !== "gone") continue;
1048
+ try {
1049
+ rmSync(entry.file, { force: true });
1050
+ removed++;
1051
+ bytes += entry.bytes;
1052
+ } catch (error) {
1053
+ failures++;
1054
+ const detail = error?.message || String(error);
1055
+ console.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
1056
+ recordGcResult("statusCache", "failed", `${kind} cache entry`, {
1057
+ id: entry.file,
1058
+ detail
1059
+ });
1060
+ }
1061
+ }
1062
+ if (!removed) continue;
1063
+ const label = `${removed} stale ${kind} cache entr${removed === 1 ? "y" : "ies"}`;
1064
+ console.log(chalk.green(`Removed ${label}`));
1065
+ recordGcResult("statusCache", "done", label, { bytes });
1066
+ }
1067
+ return failures;
1068
+ }
1069
+ //#endregion
1070
+ //#region src/commands/gc/recordings.ts
1071
+ const DAY_MS = 864e5;
1072
+ function oldSegments(devices, olderThan, now) {
1073
+ const cutoff = now - olderThan * DAY_MS;
1074
+ return devices.flatMap((device) => device.segments).filter((segment) => !segment.open && segment.end < cutoff);
1075
+ }
1076
+ function planRecordings(entries, { whole, olderThan, now }) {
1077
+ return entries.map(({ recordings, projectRoot, problem, gone, devices }) => {
1078
+ const bytes = recordedBytes(devices);
1079
+ const entry = {
1080
+ dir: recordings,
1081
+ projectRoot,
1082
+ bytes,
1083
+ withWorkspace: false
1084
+ };
1085
+ const kept = (keptCode, keptReason) => ({
1086
+ ...entry,
1087
+ deleteBytes: 0,
1088
+ willDelete: false,
1089
+ keptCode,
1090
+ keptReason
1091
+ });
1092
+ const remove = (deleteBytes, withWorkspace = false) => ({
1093
+ ...entry,
1094
+ withWorkspace,
1095
+ deleteBytes,
1096
+ willDelete: true,
1097
+ keptCode: null,
1098
+ keptReason: null
1099
+ });
1100
+ if (projectRoot === null) return kept("unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`);
1101
+ if (gone) return remove(bytes, true);
1102
+ if (whole) return remove(bytes);
1103
+ if (olderThan !== null) {
1104
+ const old = oldSegments(devices, olderThan, now);
1105
+ return old.length ? remove(old.reduce((sum, segment) => sum + segment.bytes, 0)) : kept("recently-recorded", `nothing recorded more than ${olderThan}d ago`);
1106
+ }
1107
+ return kept("retained", "the last 15 minutes of footage per device stay until worktree remove, --cache recordings, --older-than or recording.enabled false");
1108
+ });
1109
+ }
1110
+ /**
1111
+ * The device recordings of every workspace directory. Recordings in `gone` directories, which the orphaned
1112
+ * workspace and dead project sweeps remove whole, are listed with them; entries whose project root is on an
1113
+ * unmounted volume are left out.
1114
+ */
1115
+ function collectRecordings(plan, gone = []) {
1116
+ const mountedVolumes = listMountedVolumes();
1117
+ return planRecordings(listWorkspaceDirs().filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { recordings: join$1(entry.dir, "recordings") })).filter((entry) => existsSync$1(entry.recordings)).map((entry) => Object.assign({}, entry, {
1118
+ gone: gone.includes(entry.dir),
1119
+ devices: listRecordedDevices(entry.recordings)
1120
+ })), plan);
1121
+ }
1122
+ /** Deletes what `planRecordings` chose, planning again from the files on disk. */
1123
+ function deleteRecordings(report, plan) {
1124
+ let failures = 0;
1125
+ for (const entry of report) {
1126
+ const label = entry.projectRoot ?? entry.dir;
1127
+ if (entry.withWorkspace) continue;
1128
+ if (!entry.willDelete) {
1129
+ if (entry.keptCode === "unresolved" || entry.keptCode === "recently-recorded") {
1130
+ console.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
1131
+ recordGcResult("recording", "kept", label, {
1132
+ bytes: entry.bytes,
1133
+ detail: entry.keptReason
1134
+ });
1135
+ }
1136
+ continue;
1137
+ }
1138
+ try {
1139
+ let deleted = 0;
1140
+ if (plan.whole) {
1141
+ deleted = recordedBytes(listRecordedDevices(entry.dir));
1142
+ rmSync$1(entry.dir, {
1143
+ recursive: true,
1144
+ force: true
1145
+ });
1146
+ } else if (plan.olderThan !== null) for (const segment of oldSegments(listRecordedDevices(entry.dir), plan.olderThan, plan.now)) {
1147
+ rmSync$1(segment.file, { force: true });
1148
+ deleted += segment.bytes;
1149
+ }
1150
+ console.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
1151
+ recordGcResult("recording", "done", label, { bytes: deleted });
1152
+ } catch (error) {
1153
+ failures++;
1154
+ const detail = error?.message || String(error);
1155
+ console.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
1156
+ recordGcResult("recording", "failed", label, {
1157
+ bytes: entry.bytes,
1158
+ detail
1159
+ });
1160
+ }
1161
+ }
1162
+ return failures;
1163
+ }
1164
+ function recordingLines(recordings) {
1165
+ if (!recordings.length) return [];
1166
+ const total = recordings.reduce((sum, entry) => sum + entry.bytes, 0);
1167
+ const lines = [`Device recordings (${formatBytes(total)} in ${recordings.length}):`];
1168
+ for (const entry of recordings) {
1169
+ lines.push(` ${formatBytes(entry.bytes).padStart(10)} ${entry.projectRoot ?? entry.dir}`);
1170
+ lines.push(entry.withWorkspace ? " would be DELETED with its gone workspace directory" : entry.willDelete ? ` would DELETE ${formatBytes(entry.deleteBytes)}` : ` kept: ${entry.keptReason}`);
1171
+ }
1172
+ return lines;
1173
+ }
1174
+ //#endregion
882
1175
  //#region src/commands/gc/report.ts
883
1176
  function shortKey(key) {
884
1177
  const text = String(key ?? "");
@@ -889,7 +1182,12 @@ function parkedAge(parkedAt, now) {
889
1182
  if (!Number.isFinite(at)) return "parked at an unknown time";
890
1183
  return `parked ${formatLongDuration(Math.max(0, now - at))} ago`;
891
1184
  }
892
- function formatParkedSimReport(parkedSims, now) {
1185
+ const PARKED_DELETE_NOTE = " --delete attempts verified deletions and keeps failures.";
1186
+ const PARKED_ERASE_NOTE = " --delete erases each verified device (apps and data) and keeps it parked.";
1187
+ function parkedApp(app) {
1188
+ return app ? ` ${app}` : "";
1189
+ }
1190
+ function formatParkedSimReport(parkedSims, now, erase) {
893
1191
  if (parkedSims.length === 0) return [];
894
1192
  const known = parkedSims.filter((sim) => sim.bytes !== null);
895
1193
  const total = known.reduce((sum, sim) => sum + (sim.bytes ?? 0), 0);
@@ -900,20 +1198,20 @@ function formatParkedSimReport(parkedSims, now) {
900
1198
  const age = parkedAge(sim.parkedAt, now);
901
1199
  const bytes = sim.bytes === null ? "" : ` ${formatBytes(sim.bytes)}`;
902
1200
  const gone = sim.listed === false ? " - not on this machine" : sim.listed === null ? " - listing unavailable; kept" : "";
903
- lines.push(` ios ${sim.name} (${shortUdid(sim.udid)})${model ? ` ${model}` : ""} ${age}${bytes}${gone}`);
1201
+ lines.push(` ios ${sim.name} (${shortUdid(sim.udid)})${model ? ` ${model}` : ""} ${age}${parkedApp(sim.app)}${bytes}${gone}`);
904
1202
  }
905
- lines.push(" --delete attempts verified deletions and keeps failures.");
1203
+ lines.push(erase ? PARKED_ERASE_NOTE : PARKED_DELETE_NOTE);
906
1204
  return lines;
907
1205
  }
908
- function formatParkedAvdReport(parkedAvds, now) {
1206
+ function formatParkedAvdReport(parkedAvds, now, erase) {
909
1207
  if (!parkedAvds.length) return [];
910
1208
  const lines = [];
911
1209
  lines.push(`Parked emulators (${parkedAvds.length}):`);
912
1210
  for (const avd of parkedAvds) {
913
1211
  const listed = avd.listed === false ? " - not on this machine" : avd.listed === null ? " - listing unavailable; kept" : "";
914
- lines.push(` android ${avd.name} ${avd.systemImage}${avd.deviceProfile ? ` ${avd.deviceProfile}` : ""} ${parkedAge(avd.parkedAt, now)}${avd.bytes === null ? "" : ` ${formatBytes(avd.bytes)}`}${listed}`);
1212
+ lines.push(` android ${avd.name} ${avd.systemImage}${avd.deviceProfile ? ` ${avd.deviceProfile}` : ""} ${parkedAge(avd.parkedAt, now)}${parkedApp(avd.app)}${avd.bytes === null ? "" : ` ${formatBytes(avd.bytes)}`}${listed}`);
915
1213
  }
916
- lines.push(" --delete attempts verified deletions and keeps failures.");
1214
+ lines.push(erase ? PARKED_ERASE_NOTE : PARKED_DELETE_NOTE);
917
1215
  return lines;
918
1216
  }
919
1217
  function projectEntryLines(header, paths) {
@@ -928,7 +1226,23 @@ function unresolvedLockLines(locks, slots) {
928
1226
  " run that yourself once you know nothing is building with it"
929
1227
  ];
930
1228
  }
931
- function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], buildLocks = {
1229
+ function scopeLine(cacheScope) {
1230
+ if (memoryCacheKind(cacheScope)) return `Cache scope: "${cacheScope}". Only these helper processes were inspected.`;
1231
+ return includesParkedDevices(cacheScope) ? `Cache scope: "${cacheScope}". Unparked devices, project entries and locks were not inspected.` : `Cache scope: "${cacheScope}". Devices, project entries and locks were not inspected.`;
1232
+ }
1233
+ function maintenanceReportLines(maintenance) {
1234
+ const lines = [];
1235
+ if (maintenance) {
1236
+ lines.push(`Automatic maintenance (${maintenance.mode === "report" ? "report only" : "off"}):`);
1237
+ if (maintenance.note) lines.push(` ${maintenance.note}`);
1238
+ for (const action of maintenance.actions) lines.push(` ${action.kind}: ${action.target} (${formatBytes(action.bytes)}): ${action.reason}`);
1239
+ for (const skip of maintenance.skips) lines.push(` kept ${skip.target}: ${skip.reason}`);
1240
+ for (const reason of maintenance.blocked) lines.push(` blocked: ${reason}`);
1241
+ if (!maintenance.actions.length && !maintenance.blocked.length) lines.push(" no actions planned");
1242
+ }
1243
+ return lines;
1244
+ }
1245
+ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleStatusCaches = [], buildLocks = {
932
1246
  stale: [],
933
1247
  live: [],
934
1248
  unresolved: []
@@ -944,13 +1258,13 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
944
1258
  orphaned: [],
945
1259
  notices: [],
946
1260
  deletionSafe: true
947
- }, caches = [], workspaceOutputs = null, workspaceLogs = [], worktreeSweep = null, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
948
- const lines = [];
1261
+ }, agentDeviceUsage = null, swiftpmCacheUsage, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
1262
+ const lines = archiveLines(archives);
949
1263
  const staleLocks = buildLocks?.stale ?? [];
950
1264
  const liveLocks = buildLocks?.live ?? [];
951
1265
  const staleSlots = buildSlots?.stale ?? [];
952
1266
  const expiredLeases = deviceLeases?.expired ?? [];
953
- if (cacheScope) lines.push(`Cache scope: "${cacheScope}". Devices, project entries and locks were not inspected.`);
1267
+ if (cacheScope) lines.push(scopeLine(cacheScope));
954
1268
  else if ([
955
1269
  deadProjects,
956
1270
  invalidProjects,
@@ -961,6 +1275,7 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
961
1275
  staleDevices,
962
1276
  staleDeviceRecords,
963
1277
  staleLedgerEntries,
1278
+ staleStatusCaches,
964
1279
  staleLocks,
965
1280
  staleSlots,
966
1281
  expiredLeases,
@@ -981,8 +1296,8 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
981
1296
  lines.push(...namedPortLines(orphanedPorts));
982
1297
  lines.push(...orphanedWorkspaceLines(orphanedWorkspaces));
983
1298
  lines.push(...worktreeSweepLines(worktreeSweep));
984
- lines.push(...formatParkedSimReport(parkedSims, now));
985
- lines.push(...formatParkedAvdReport(parkedAvds, now));
1299
+ lines.push(...formatParkedSimReport(parkedSims, now, cacheScope !== null));
1300
+ lines.push(...formatParkedAvdReport(parkedAvds, now, cacheScope !== null));
986
1301
  if (orphanedDevices.length) {
987
1302
  lines.push(`Orphaned devices (${orphanedDevices.length}):`);
988
1303
  for (const d of orphanedDevices) lines.push(` ${d.kind} ${d.name} (${d.id})${deviceSizeSuffix(d)}`);
@@ -1016,6 +1331,7 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
1016
1331
  for (const entry of staleLedgerEntries) lines.push(` ${entry.kind} ${entry.id} is not on this machine`);
1017
1332
  lines.push(" --delete forgets the LEDGER ENTRY only; there is no device left to touch.");
1018
1333
  }
1334
+ lines.push(...statusCacheLines(staleStatusCaches));
1019
1335
  if (easSessionSweep.orphaned.length) {
1020
1336
  lines.push(`Orphaned EAS sessions (${easSessionSweep.orphaned.length}) - current EAS project only:`);
1021
1337
  for (const session of easSessionSweep.orphaned) {
@@ -1061,7 +1377,12 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
1061
1377
  for (const entry of skipped) lines.push(` ${entry.dir}: ${entry.reason}`);
1062
1378
  }
1063
1379
  lines.push(...workspaceLogLines(workspaceLogs));
1380
+ lines.push(...recordingLines(recordings));
1064
1381
  lines.push(...cacheLines(caches, workspaceOutputs));
1382
+ lines.push(...agentDeviceLines(agentDeviceUsage, true, now));
1383
+ lines.push(...swiftpmCacheLines(swiftpmCacheUsage, now));
1384
+ lines.push(...memoryLines(memory, memoryCacheKind(cacheScope), now));
1385
+ lines.push(...maintenanceReportLines(maintenance));
1065
1386
  return lines;
1066
1387
  }
1067
1388
  function workspaceLogLines(workspaceLogs) {
@@ -1117,7 +1438,7 @@ function workspaceOutputLines(outputs, bytes) {
1117
1438
  const lines = [
1118
1439
  ` ${formatBytes(bytes).padStart(10)} Workspace build outputs (detected)`,
1119
1440
  ` ${outputs.root}`,
1120
- ` ${WORKSPACE_OUTPUT_DIRS.join(", ")} of each workspace; workspace.json, state.json, logs and device records stay`,
1441
+ ` ${WORKSPACE_OUTPUT_DIRS.join(", ")}, macOS Swift scratch and staged app bundles of each workspace; workspace.json, state.json, logs and device records stay`,
1121
1442
  ` ${REBUILD_COST}`
1122
1443
  ];
1123
1444
  for (const w of outputs.workspaces) {
@@ -1142,6 +1463,7 @@ function cacheLines(caches, workspaceOutputs) {
1142
1463
  if (c.note) lines.push(` ${c.note}`);
1143
1464
  if (c.willEmpty) lines.push(" would be EMPTIED");
1144
1465
  else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
1466
+ else if (c.scopedEmpty?.emptySkipped) lines.push(` --delete --cache would leave it alone: ${c.scopedEmpty.emptySkipped}`);
1145
1467
  }
1146
1468
  lines.push(` total: ${formatBytes(total)}`);
1147
1469
  const doomed = caches.filter((c) => c.willEmpty);
@@ -1191,7 +1513,7 @@ function jsonPullRequest(lookup) {
1191
1513
  containsHead: pr.containsHead
1192
1514
  } : null;
1193
1515
  }
1194
- function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], buildLocks = {
1516
+ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleStatusCaches = [], buildLocks = {
1195
1517
  stale: [],
1196
1518
  live: [],
1197
1519
  unresolved: []
@@ -1207,8 +1529,12 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1207
1529
  orphaned: [],
1208
1530
  notices: [],
1209
1531
  deletionSafe: true
1210
- }, workspaceOutputs = null, workspaceLogs = [], caches = [] }) {
1532
+ }, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, caches = [], memory = null }) {
1211
1533
  return {
1534
+ ...archives ? {
1535
+ archived: archives.records,
1536
+ archiveStaging: archives.staging
1537
+ } : {},
1212
1538
  deadProjects: deadProjects.map((path) => ({ path })),
1213
1539
  invalidProjects: invalidProjects.map((path) => ({ path })),
1214
1540
  orphanedPorts: orphanedPorts.map(({ project, label, port }) => ({
@@ -1232,20 +1558,22 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1232
1558
  detail: w.skipped ?? worktreeRemovalReason(w),
1233
1559
  eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString()
1234
1560
  })),
1235
- parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, bytes, listed }) => ({
1561
+ parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, app, bytes, listed }) => ({
1236
1562
  udid,
1237
1563
  name,
1238
1564
  model,
1239
1565
  runtime,
1240
1566
  parkedAt,
1567
+ app,
1241
1568
  bytes,
1242
1569
  listed
1243
1570
  })),
1244
- parkedEmulators: parkedAvds.map(({ name, systemImage, deviceProfile, parkedAt, bytes, listed }) => ({
1571
+ parkedEmulators: parkedAvds.map(({ name, systemImage, deviceProfile, parkedAt, app, bytes, listed }) => ({
1245
1572
  name,
1246
1573
  systemImage,
1247
1574
  deviceProfile,
1248
1575
  parkedAt,
1576
+ app,
1249
1577
  bytes,
1250
1578
  listed
1251
1579
  })),
@@ -1281,6 +1609,12 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1281
1609
  kind,
1282
1610
  id
1283
1611
  })),
1612
+ staleStatusCaches: staleStatusCaches.map(({ kind, path, file, bytes }) => ({
1613
+ kind,
1614
+ path,
1615
+ file,
1616
+ bytes
1617
+ })),
1284
1618
  idleDevices: idleDevices.map(({ kind, id, name, project, slot, lastActivityAt, idleForMs, buildInProgress }) => ({
1285
1619
  kind,
1286
1620
  id,
@@ -1341,6 +1675,16 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1341
1675
  reason: w.keptCode,
1342
1676
  detail: w.keptReason
1343
1677
  })),
1678
+ recordings: recordings.map((r) => ({
1679
+ dir: r.dir,
1680
+ projectRoot: r.projectRoot,
1681
+ bytes: r.bytes,
1682
+ deleteBytes: r.deleteBytes,
1683
+ willDelete: r.willDelete,
1684
+ withWorkspace: r.withWorkspace,
1685
+ reason: r.keptCode,
1686
+ detail: r.keptReason
1687
+ })),
1344
1688
  workspaceBuildOutputs: (workspaceOutputs?.workspaces ?? []).map((w) => ({
1345
1689
  dir: w.dir,
1346
1690
  projectRoot: w.projectRoot,
@@ -1357,8 +1701,14 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1357
1701
  bytes: c.bytes ?? null,
1358
1702
  note: c.note || null,
1359
1703
  willEmpty: Boolean(c.willEmpty),
1360
- emptySkipped: c.emptySkipped ?? null
1361
- }))
1704
+ emptySkipped: c.emptySkipped ?? null,
1705
+ scopedEmpty: c.scopedEmpty ?? null
1706
+ })),
1707
+ agentDevice: agentDeviceUsage,
1708
+ swiftpmCache: swiftpmCacheUsage,
1709
+ memory: memory?.processes ?? [],
1710
+ watchmanRoots: memory?.watchmanRoots ?? [],
1711
+ memoryNotices: (memory?.notices ?? []).map((message) => ({ message }))
1362
1712
  };
1363
1713
  }
1364
1714
  //#endregion
@@ -1417,8 +1767,8 @@ function staleBrowserProfiles(ledger, exists = existsSync) {
1417
1767
  }));
1418
1768
  }
1419
1769
  function isStaleProfile(path, exists) {
1420
- const workspace = dirname$1(dirname$1(path));
1421
- return !exists(path) && exists(dirname$1(workspace)) && !isDanglingLink(workspace);
1770
+ const workspace = dirname(dirname(path));
1771
+ return !exists(path) && exists(dirname(workspace)) && !isDanglingLink(workspace);
1422
1772
  }
1423
1773
  function isDanglingLink(path) {
1424
1774
  try {
@@ -1464,7 +1814,7 @@ function fileSize(path) {
1464
1814
  }
1465
1815
  function cappedFiles(logsDir) {
1466
1816
  return CAPPED_LOGS.flatMap((name) => {
1467
- const log = join(logsDir, name);
1817
+ const log = join$1(logsDir, name);
1468
1818
  return [{
1469
1819
  file: log,
1470
1820
  log
@@ -1490,14 +1840,14 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1490
1840
  for (const { dir, projectRoot, problem } of listWorkspaceDirs()) {
1491
1841
  if (exclude.includes(dir)) continue;
1492
1842
  if (projectRoot !== null && !isOnMountedVolume(projectRoot, mountedVolumes)) continue;
1493
- const logsDir = join(dir, "logs");
1843
+ const logsDir = join$1(dir, "logs");
1494
1844
  let names;
1495
1845
  try {
1496
1846
  names = readdirSync$1(logsDir);
1497
1847
  } catch {
1498
1848
  continue;
1499
1849
  }
1500
- const bytes = names.reduce((sum, name) => sum + fileSize(join(logsDir, name)), 0);
1850
+ const bytes = names.reduce((sum, name) => sum + fileSize(join$1(logsDir, name)), 0);
1501
1851
  if (bytes === 0) continue;
1502
1852
  const trimBytes = excess(logsDir);
1503
1853
  let keptCode = null;
@@ -1532,7 +1882,7 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1532
1882
  }
1533
1883
  function trimToNewest({ file, log }, maxBytes) {
1534
1884
  if (!trimmable(fileSize(file))) return 0;
1535
- return withDirLock(`${log}.lock`, () => {
1885
+ return withDirLock$1(`${log}.lock`, () => {
1536
1886
  const { size, atime, mtime } = statSync$1(file);
1537
1887
  if (!trimmable(size)) return 0;
1538
1888
  const tail = Buffer.alloc(maxBytes);
@@ -1547,7 +1897,7 @@ function trimToNewest({ file, log }, maxBytes) {
1547
1897
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1548
1898
  try {
1549
1899
  writeFileSync$1(tmp, kept);
1550
- utimesSync(tmp, atime, mtime);
1900
+ utimesSync$1(tmp, atime, mtime);
1551
1901
  renameSync$1(tmp, file);
1552
1902
  } catch (error) {
1553
1903
  rmSync$1(tmp, { force: true });
@@ -1575,7 +1925,7 @@ async function trimWorkspaceLogs(entries) {
1575
1925
  };
1576
1926
  return {
1577
1927
  kept: null,
1578
- bytes: cappedFiles(join(entry.dir, "logs")).reduce((sum, capped) => sum + trimToNewest(capped, LOG_ROTATE_BYTES), 0)
1928
+ bytes: cappedFiles(join$1(entry.dir, "logs")).reduce((sum, capped) => sum + trimToNewest(capped, LOG_ROTATE_BYTES), 0)
1579
1929
  };
1580
1930
  }, { purpose: "gc" });
1581
1931
  } catch (error) {
@@ -1775,7 +2125,7 @@ function listAvdRecords(notices, roots = avdStorageRoots()) {
1775
2125
  if (records.has(name)) continue;
1776
2126
  let ini;
1777
2127
  try {
1778
- ini = readFileSync(join$1(root, entry), "utf8");
2128
+ ini = readFileSync(join(root, entry), "utf8");
1779
2129
  } catch (error) {
1780
2130
  if (!isMissing(error)) notices.push(unreadableNotice(`AVD ${name}`, error));
1781
2131
  continue;
@@ -1784,7 +2134,7 @@ function listAvdRecords(notices, roots = avdStorageRoots()) {
1784
2134
  let config = "";
1785
2135
  let unreadable = null;
1786
2136
  for (const candidate of avdIniPaths(root, ini)) try {
1787
- config = readFileSync(join$1(candidate, "config.ini"), "utf8");
2137
+ config = readFileSync(join(candidate, "config.ini"), "utf8");
1788
2138
  directory = candidate;
1789
2139
  break;
1790
2140
  } catch (error) {
@@ -1796,7 +2146,7 @@ function listAvdRecords(notices, roots = avdStorageRoots()) {
1796
2146
  directory,
1797
2147
  systemImage: parseAvdSystemImage(config),
1798
2148
  deviceProfile: parseAvdDeviceProfile(config),
1799
- lastUsedAt: directory ? modifiedAt(join$1(directory, "hardware-qemu.ini")) : null
2149
+ lastUsedAt: directory ? modifiedAt(join(directory, "hardware-qemu.ini")) : null
1800
2150
  });
1801
2151
  }
1802
2152
  }
@@ -1837,10 +2187,10 @@ function collectInventory(config, deadProjects) {
1837
2187
  notices.push(`simulator runtimes not listed: ${describeError(error)}`);
1838
2188
  }
1839
2189
  }
1840
- const imagesRoot = join$1(androidHome(), "system-images");
2190
+ const imagesRoot = join(androidHome(), "system-images");
1841
2191
  const images = listInstalledSystemImages((error) => notices.push(unreadableNotice("Android system images", error))).map((image) => ({
1842
2192
  pkg: image.pkg,
1843
- directory: join$1(imagesRoot, `android-${image.api}`, image.tag, image.arch)
2193
+ directory: join(imagesRoot, `android-${image.api}`, image.tag, image.arch)
1844
2194
  }));
1845
2195
  return {
1846
2196
  ...buildInventory({
@@ -1863,13 +2213,56 @@ function removeInvalidProjectEntries(invalidProjects) {
1863
2213
  recordGcResult("project", "done", path);
1864
2214
  }
1865
2215
  }
2216
+ function collectGcMemory(scope) {
2217
+ return memorySweepIsScoped() ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true);
2218
+ }
2219
+ function classifyProjectEntries(cfg, mountedVolumes) {
2220
+ const deadProjects = [];
2221
+ const invalidProjects = [];
2222
+ const skipped = [];
2223
+ for (const [path, project] of Object.entries(cfg?.projects || {})) {
2224
+ if (!isAbsolute$1(path)) {
2225
+ const claimed = describeDereferenced(project);
2226
+ if (claimed.length) skipped.push({
2227
+ dir: path,
2228
+ reason: `not an absolute path, so this record is invalid; kept because it still claims ${claimed.join(" and ")}; gc removes it once that claim is gone, or drop the entry from the config file by hand`
2229
+ });
2230
+ else invalidProjects.push(path);
2231
+ continue;
2232
+ }
2233
+ if (existsSync$1(path)) continue;
2234
+ if (!isOnMountedVolume(path, mountedVolumes)) {
2235
+ const volume = volumeRootFor(path);
2236
+ skipped.push({
2237
+ dir: path,
2238
+ reason: `volume ${volume} is not mounted`
2239
+ });
2240
+ } else deadProjects.push(path);
2241
+ }
2242
+ return {
2243
+ deadProjects,
2244
+ invalidProjects,
2245
+ skipped
2246
+ };
2247
+ }
2248
+ /** The workspace directories an unscoped run removes whole: those of dead projects and orphaned directories. */
2249
+ function removedWholeWorkspaceDirs() {
2250
+ const mountedVolumes = listMountedVolumes();
2251
+ const cfg = loadConfig$1();
2252
+ const { deadProjects } = classifyProjectEntries(cfg, mountedVolumes);
2253
+ const { orphaned } = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { measure: false });
2254
+ return [...orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
2255
+ }
1866
2256
  async function collectGcReport({ olderThan = null, cache = null, worktrees = false, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
1867
2257
  const scope = typeof cache === "string" && cache.trim() ? cache : null;
1868
2258
  const all = scope !== null && olderThan === null;
1869
2259
  const withWorkspaces = includesWorkspaceOutputs(scope);
1870
2260
  const selected = selectCaches(discoverCaches(), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
1871
- const caches = planCacheEmptying(sizeCaches(selected), all);
2261
+ if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`);
2262
+ const sized = sizeCaches(selected);
2263
+ const caches = scope === null ? previewScopedEmptying(sized) : planCacheEmptying(sized, all);
1872
2264
  if (scope) return {
2265
+ archives: includesArchives(scope) ? collectArchives(scope, olderThan, now) : void 0,
1873
2266
  skipped: [],
1874
2267
  deadProjects: [],
1875
2268
  invalidProjects: [],
@@ -1879,6 +2272,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1879
2272
  staleDevices: [],
1880
2273
  staleDeviceRecords: [],
1881
2274
  staleLedgerEntries: [],
2275
+ staleStatusCaches: [],
1882
2276
  buildLocks: {
1883
2277
  stale: [],
1884
2278
  live: [],
@@ -1901,19 +2295,38 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1901
2295
  notices: [],
1902
2296
  deletionSafe: true
1903
2297
  },
1904
- parkedSims: [],
1905
- parkedAvds: [],
2298
+ parkedSims: includesParkedDevices(scope) ? collectParkedSims(deps, {
2299
+ olderThanDays: olderThan,
2300
+ now
2301
+ }) : [],
2302
+ parkedAvds: includesParkedDevices(scope) ? collectParkedAvds(deps, {
2303
+ olderThanDays: olderThan,
2304
+ now
2305
+ }) : [],
1906
2306
  caches,
1907
2307
  workspaceOutputs: withWorkspaces ? collectWorkspaceOutputs({
1908
2308
  olderThan,
1909
- now
2309
+ now,
2310
+ exclude: isWorkspacesScope(scope) ? removedWholeWorkspaceDirs() : []
1910
2311
  }) : null,
1911
2312
  workspaceLogs: [],
2313
+ recordings: includesRecordings(scope) ? collectRecordings({
2314
+ whole: all,
2315
+ olderThan,
2316
+ now
2317
+ }) : [],
1912
2318
  worktreeSweep: null,
2319
+ memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope)) : null,
2320
+ agentDeviceUsage: null,
2321
+ swiftpmCacheUsage: null,
1913
2322
  cacheScope: scope,
1914
2323
  olderThan,
1915
2324
  all
1916
2325
  };
2326
+ phase("agent-device", "measuring report-only state");
2327
+ const agentDeviceUsage = await getAgentDeviceUsage();
2328
+ phase("SwiftPM cache", "measuring report-only state");
2329
+ const swiftpmCacheUsage = await getSwiftpmCacheUsage();
1917
2330
  const mountedVolumes = listMountedVolumes();
1918
2331
  const cfg = loadConfig$1();
1919
2332
  let easSessionSweep = deps.precollectedEasSessionSweep;
@@ -1935,30 +2348,11 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1935
2348
  olderThanDays: olderThan,
1936
2349
  now
1937
2350
  });
1938
- const deadProjects = [];
1939
- const invalidProjects = [];
1940
- const skipped = [];
1941
- for (const [path, project] of Object.entries(cfg?.projects || {})) {
1942
- if (!isAbsolute(path)) {
1943
- const claimed = describeDereferenced(project);
1944
- if (claimed.length) skipped.push({
1945
- dir: path,
1946
- reason: `not an absolute path, so this record is invalid; kept because it still claims ${claimed.join(" and ")}; gc removes it once that claim is gone, or drop the entry from the config file by hand`
1947
- });
1948
- else invalidProjects.push(path);
1949
- continue;
1950
- }
1951
- if (existsSync$1(path)) continue;
1952
- if (!isOnMountedVolume(path, mountedVolumes)) {
1953
- const volume = volumeRootFor(path);
1954
- skipped.push({
1955
- dir: path,
1956
- reason: `volume ${volume} is not mounted`
1957
- });
1958
- } else deadProjects.push(path);
1959
- }
2351
+ const { deadProjects, invalidProjects, skipped } = classifyProjectEntries(cfg, mountedVolumes);
1960
2352
  const workspaceDirs = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
1961
2353
  skipped.push(...workspaceDirs.skipped);
2354
+ const statusCaches = collectStaleStatusCaches(mountedVolumes);
2355
+ skipped.push(...statusCaches.skipped);
1962
2356
  const deviceSweepNotices = [];
1963
2357
  let orphanedDevices = [];
1964
2358
  let unverifiedDevices = [];
@@ -1967,6 +2361,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1967
2361
  let staleLedgerEntries = staleBrowserProfiles(readCreatedDevices$1());
1968
2362
  let idleDevices = [];
1969
2363
  const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped() ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
2364
+ phase("devices", "listing simulators and emulators");
1970
2365
  if (unsweepableReason) {
1971
2366
  let simNames = [];
1972
2367
  let avdNames = [];
@@ -2072,6 +2467,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2072
2467
  staleDevices,
2073
2468
  staleDeviceRecords,
2074
2469
  staleLedgerEntries,
2470
+ staleStatusCaches: statusCaches.stale,
2075
2471
  buildLocks: {
2076
2472
  stale: locks.filter((l) => !l.alive && !l.unresolved),
2077
2473
  live: locks.filter((l) => l.alive),
@@ -2093,11 +2489,22 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2093
2489
  exclude: goneWorkspaceDirs
2094
2490
  }),
2095
2491
  workspaceLogs: collectWorkspaceLogs({ exclude: goneWorkspaceDirs }),
2492
+ recordings: collectRecordings({
2493
+ whole: false,
2494
+ olderThan,
2495
+ now
2496
+ }, goneWorkspaceDirs),
2096
2497
  worktreeSweep: await collectWorktreeSweep({
2097
2498
  idle: worktrees,
2098
2499
  olderThan,
2099
2500
  now
2100
2501
  }),
2502
+ memory: await collectGcMemory({
2503
+ watchman: true,
2504
+ gradle: true
2505
+ }),
2506
+ agentDeviceUsage,
2507
+ swiftpmCacheUsage,
2101
2508
  cacheScope: null,
2102
2509
  olderThan,
2103
2510
  all
@@ -2105,19 +2512,53 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2105
2512
  }
2106
2513
  async function runGc(opts = {}, deps = {}) {
2107
2514
  if (!opts.json) {
2108
- await sweep(opts, deps);
2515
+ await maintenanceGuardedSweep(opts, deps);
2109
2516
  return;
2110
2517
  }
2111
2518
  const log = console.log;
2112
2519
  console.log = console.error;
2113
2520
  let payload;
2114
2521
  try {
2115
- payload = await sweep(opts, deps);
2522
+ payload = await maintenanceGuardedSweep(opts, deps);
2116
2523
  } finally {
2117
2524
  console.log = log;
2118
2525
  }
2119
2526
  console.log(JSON.stringify(payload));
2120
2527
  }
2528
+ async function maintenanceGuardedSweep(opts, deps) {
2529
+ if (!opts.delete) return sweep(opts, deps);
2530
+ let claim;
2531
+ try {
2532
+ const root = maintenanceRunClaims();
2533
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
2534
+ root,
2535
+ mode: "exclusive",
2536
+ label: "maintenance",
2537
+ details: { trigger: "gc --delete" }
2538
+ });
2539
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
2540
+ claim = attempt.acquired;
2541
+ if (!claim) {
2542
+ const holder = attempt.held ?? attempt.waitingFor?.[0];
2543
+ throw new ownership_claim_exports.ClaimRefusedError({
2544
+ root,
2545
+ claimPath: holder?.path ?? root,
2546
+ label: "maintenance",
2547
+ reason: `held by pid ${holder?.owner.pid ?? "?"} since ${holder?.startedAt ?? "?"} (${String(holder?.details.trigger ?? "maintenance")})`
2548
+ });
2549
+ }
2550
+ return await sweep(opts, deps);
2551
+ } catch (error) {
2552
+ const failure = (0, ownership_claim_exports.claimFailure)(error, "stim gc --delete");
2553
+ if (!failure) throw error;
2554
+ console.error(`${failure.code}: ${failure.message}`);
2555
+ console.error(failure.remedy);
2556
+ process.exitCode = 1;
2557
+ return failure;
2558
+ } finally {
2559
+ if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
2560
+ }
2561
+ }
2121
2562
  async function sweep(opts, deps) {
2122
2563
  if (opts.cache && (opts.worktrees || opts.idle !== void 0)) {
2123
2564
  const flag = opts.worktrees ? "--worktrees" : "--idle";
@@ -2133,6 +2574,20 @@ async function sweep(opts, deps) {
2133
2574
  remedy
2134
2575
  };
2135
2576
  }
2577
+ const memoryKind = memoryCacheKind(opts.cache);
2578
+ if (memoryKind && opts.olderThan !== void 0) {
2579
+ const message = `--older-than does not apply to --cache ${memoryKind}, which acts on running processes.`;
2580
+ const remedy = `Run \`stim gc --cache ${memoryKind}\` without --older-than.`;
2581
+ console.error(chalk.red(message));
2582
+ console.error(chalk.dim(remedy));
2583
+ console.error(chalk.red("failed: STIM_BAD_ARG"));
2584
+ process.exitCode = 1;
2585
+ return {
2586
+ code: "STIM_BAD_ARG",
2587
+ message,
2588
+ remedy
2589
+ };
2590
+ }
2136
2591
  const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
2137
2592
  if (poolError) console.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2138
2593
  if (opts.cache) return runGcCore(opts, {
@@ -2146,7 +2601,7 @@ async function sweep(opts, deps) {
2146
2601
  });
2147
2602
  let projectRoot = null;
2148
2603
  try {
2149
- projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
2604
+ projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(process.cwd());
2150
2605
  if (!projectRoot) return runGcCore(opts, {
2151
2606
  ...deps,
2152
2607
  precollectedEasSessionSweep: {
@@ -2209,7 +2664,10 @@ async function pruneDeadProjects(deadProjects) {
2209
2664
  recordGcResult("project", "kept", path, { detail: "its absence can no longer be confirmed" });
2210
2665
  continue;
2211
2666
  }
2212
- const result = await reclaimProject(path, { stopRequester: { by: "stim gc --delete" } }).catch((error) => {
2667
+ const result = await reclaimProject(path, {
2668
+ archive: { removedBy: "gc" },
2669
+ stopRequester: { by: "stim gc --delete" }
2670
+ }).catch((error) => {
2213
2671
  deleteFailures++;
2214
2672
  console.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
2215
2673
  recordGcResult("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
@@ -2244,6 +2702,28 @@ async function pruneDeadProjects(deadProjects) {
2244
2702
  }
2245
2703
  return deleteFailures;
2246
2704
  }
2705
+ async function attachMaintenancePreview(report, deleting, cache) {
2706
+ if (!deleting) report.maintenance = await previewMaintenance({ devices: !cache });
2707
+ }
2708
+ function scopedDryRunAction(cache) {
2709
+ const memory = memoryCacheKind(cache);
2710
+ if (memory === "watchman") return "remove the stale roots and shut down the unused watchman";
2711
+ if (memory === "gradle-daemons") return "stop the idle daemons";
2712
+ return includesParkedDevices(cache) ? "erase the parked devices" : "empty the caches";
2713
+ }
2714
+ function dryRunHint(cache, all, actionable, caches) {
2715
+ if (all && (!memoryCacheKind(cache) || actionable)) return `Dry run. Re-run with --delete to ${scopedDryRunAction(cache)} above.`;
2716
+ if (actionable) return "Dry run. Re-run with --delete to reclaim.";
2717
+ if (caches) return "Pass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them.";
2718
+ return null;
2719
+ }
2720
+ function pruneStatusCaches(report) {
2721
+ if (report.cacheScope !== null) return 0;
2722
+ return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale);
2723
+ }
2724
+ function reclaimParkedDevices(report, deps) {
2725
+ return report.cacheScope ? eraseParkedDevices(report.parkedSims, report.parkedAvds) : deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
2726
+ }
2247
2727
  async function runGcCore(opts, deps) {
2248
2728
  takeGcResults();
2249
2729
  const olderThan = typeof opts.olderThan === "number" ? opts.olderThan : null;
@@ -2253,7 +2733,7 @@ async function runGcCore(opts, deps) {
2253
2733
  cache,
2254
2734
  worktrees: Boolean(opts.worktrees)
2255
2735
  }, deps);
2256
- if (cache && report.caches.length === 0 && report.workspaceOutputs === null) {
2736
+ if (cache && report.caches.length === 0 && report.workspaceOutputs === null && !selectsBeyondCaches(cache)) {
2257
2737
  const names = [...new Set(discoverCaches().map((c) => c.name))];
2258
2738
  const message = `No shared cache carries "${cache}" in its name or directory.`;
2259
2739
  console.log(chalk.yellow(message));
@@ -2265,10 +2745,13 @@ async function runGcCore(opts, deps) {
2265
2745
  remedy: names.length ? `Pass --cache all or a name from: ${names.join(", ")}.` : "No shared cache was found on this machine."
2266
2746
  };
2267
2747
  }
2748
+ await attachMaintenancePreview(report, opts.delete === true, cache);
2749
+ const refusal = archiveRefusal(report.archives);
2750
+ if (refusal !== null) return refusal;
2268
2751
  const all = report.all;
2269
2752
  for (const line of formatGcReport(report)) console.log(line);
2270
2753
  const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches } = report;
2271
- const actionable = deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || report.workspaceLogs.some((entry) => entry.willTrim) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length > 0 || orphanedDevices.length > 0 || staleDevices.length > 0 || staleDeviceRecords.length > 0 || report.staleLedgerEntries.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0;
2754
+ const actionable = archiveWorkPending(report.archives, deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || report.workspaceLogs.some((entry) => entry.willTrim) || report.recordings.some((entry) => entry.willDelete && !entry.withWorkspace) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length > 0 || orphanedDevices.length > 0 || staleDevices.length > 0 || staleDeviceRecords.length > 0 || report.staleLedgerEntries.length > 0 || report.staleStatusCaches.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0 || memoryWorkPending(cache, report.memory));
2272
2755
  const idle = opts.idle ?? null;
2273
2756
  const idleFailures = idle === null ? 0 : shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects)).failures;
2274
2757
  if (idleFailures) process.exitCode = 1;
@@ -2282,24 +2765,32 @@ async function runGcCore(opts, deps) {
2282
2765
  actionable,
2283
2766
  failures,
2284
2767
  sections: gcReportSections(report),
2768
+ ...report.maintenance ? { maintenance: report.maintenance } : {},
2285
2769
  results: takeGcResults(),
2286
2770
  inventory
2287
2771
  });
2288
2772
  if (!opts.delete) {
2289
- if (all) console.log(chalk.dim("\nDry run. Re-run with --delete to empty the caches above."));
2290
- else if (actionable) console.log(chalk.dim("\nDry run. Re-run with --delete to reclaim."));
2291
- else if (caches.length) console.log(chalk.dim("\nPass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them."));
2773
+ const hint = dryRunHint(cache, all, actionable, caches.length > 0);
2774
+ if (hint) console.log(chalk.dim(`\n${hint}`));
2292
2775
  return payload(idle === null ? null : idleFailures);
2293
2776
  }
2294
2777
  let deleteFailures = idleFailures;
2778
+ deleteFailures += deleteArchives(report.archives);
2295
2779
  deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, { olderThan })).failures : 0;
2296
2780
  deleteFailures += await trimWorkspaceLogs(report.workspaceLogs);
2297
- deleteFailures += deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
2781
+ deleteFailures += deleteRecordings(report.recordings, {
2782
+ whole: all,
2783
+ olderThan,
2784
+ now: Date.now()
2785
+ });
2786
+ deleteFailures += reclaimParkedDevices(report, deps);
2787
+ deleteFailures += await reclaimMemory(cache, report.memory);
2298
2788
  removeInvalidProjectEntries(invalidProjects);
2299
2789
  deleteFailures += await pruneDeadProjects(deadProjects);
2300
2790
  deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces);
2301
2791
  deleteFailures += deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
2302
2792
  deleteFailures += forgetStaleLedgerEntries(report.staleLedgerEntries);
2793
+ deleteFailures += pruneStatusCaches(report);
2303
2794
  for (const lock of buildLocks.stale) {
2304
2795
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2305
2796
  root: lock.path,
@@ -2378,11 +2869,11 @@ async function runGcCore(opts, deps) {
2378
2869
  return payload(deleteFailures);
2379
2870
  }
2380
2871
  function gcCommand(program) {
2381
- program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, clean Stim-managed linked worktrees whose branch is merged or whose pull request was merged or closed, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, and the build outputs and logs of each workspace. Reports by default; pass --delete to act.").option("--worktrees", "also remove clean, idle Stim-managed linked worktrees, not only merged ones: with --delete run `stim worktree remove` (never --force) on each; idle means unused for --older-than days, 7 without it").option("--delete", "actually prune the reported entries, reap the reported devices, clear the build outputs of every workspace not in use, and trim its Metro, client and device logs to their newest 8 MiB").option("--older-than <days>", "also reap owned devices whose workspace has not been used this long, trim shared cache entries nothing has used in that time, and clear workspace build outputs only for workspaces idle this long and parked devices only once parked this long", (v) => {
2872
+ program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, clean Stim-managed linked worktrees whose branch is merged or whose pull request was merged or closed, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, the build outputs and logs of each workspace, and the memory of watchman and of the Gradle and Kotlin daemons. Reports by default; pass --delete to act.").option("--worktrees", "also remove clean, idle Stim-managed linked worktrees, not only merged ones: with --delete run `stim worktree remove` (never --force) on each; idle means unused for --older-than days, 7 without it").option("--delete", "actually prune the reported entries, reap the reported devices, clear the build outputs of every workspace not in use, and trim its Metro, client and device logs to their newest 8 MiB").option("--older-than <days>", "also reap owned devices whose workspace has not been used this long, trim shared cache entries nothing has used in that time and device recordings made before it, and clear workspace build outputs only for workspaces idle this long and parked devices only once parked this long", (v) => {
2382
2873
  const n = parseInt(v, 10);
2383
2874
  if (!Number.isFinite(n) || String(n) !== String(v).trim()) throw new InvalidArgumentError("must be a whole number of days, e.g. --older-than 30");
2384
2875
  return n;
2385
- }).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache and the workspace build outputs with --cache all, or only the workspace build outputs with --cache workspaces; all and workspaces are reserved names that never select a single cache. With --delete they are emptied whole, which is the only way to clear an index-backed cache; add --older-than <days> to trim them by age instead. Only those caches are reported; devices and project entries are not inspected. Caches outside the config dir are refused while STIM_HOME is set.", (v) => {
2876
+ }).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache, the workspace build outputs and the device recordings with --cache all, only the workspace build outputs with --cache workspaces, only the device recordings with --cache recordings, only the parked simulators and emulators with --cache parked, the watchman daemon and its stale roots with --cache watchman, or the Gradle and Kotlin compile daemons with --cache gradle-daemons; archived, archived:<id>, archived-logs, archived-recordings and archived-agent select archived history explicitly; --cache all excludes archives. all, workspaces, recordings, parked, archived, archived:<id>, archived-logs, archived-recordings, archived-agent, watchman and gradle-daemons are reserved names that never select a single cache, and --cache all never includes watchman or gradle-daemons. With --delete they are emptied whole (a parked device is erased and stays parked, watchman loses its stale roots and shuts down only when no client uses it, and only daemons proven idle stop), which is the only way to clear an index-backed cache; add --older-than <days> to trim them by age instead. Archive ids ignore --older-than; archived and per-kind archive selectors filter by removal age. Only those caches are reported; other devices and project entries are not inspected. Caches outside the config dir are refused while STIM_HOME is set.", (v) => {
2386
2877
  if (!v.trim()) throw new InvalidArgumentError("must name a cache, e.g. --cache \"compilation cache\"");
2387
2878
  return v;
2388
2879
  }).option("--idle <duration>", "shut down (never delete) owned simulators and emulators that have had no driver, claim, or activity for at least <duration>, such as 30m, 2h, or 1d; acts without --delete", (v) => {