stim 1.13.0 → 1.15.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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
  5. package/dist/{android-qx_31wJo.mjs → android-Db2eEMaf.mjs} +152 -65
  6. package/dist/android-cas-PnKfAdB2.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{app-install-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
  9. package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
  10. package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
  16. package/dist/cli-0cX5s4NM.mjs +45 -0
  17. package/dist/cli.mjs +6 -42
  18. package/dist/client-C2IS36Qf.mjs +1333 -0
  19. package/dist/collector-run.mjs +8 -7
  20. package/dist/{command-output-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
  22. package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-2Wo81np9.mjs → deps-BFlKbOIp.mjs} +3 -3
  25. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  26. package/dist/dev-client-CjzLoNs0.mjs +80 -0
  27. package/dist/{device-Cf7vl-so.mjs → device-BZ1IyS5A.mjs} +7 -7
  28. package/dist/device-capacity-DMuKC7nx.mjs +136 -0
  29. package/dist/device-host-worker.d.mts +1 -0
  30. package/dist/device-host-worker.mjs +769 -0
  31. package/dist/device-ios-DMQBUPus.mjs +381 -0
  32. package/dist/{device-lease-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
  39. package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
  41. package/dist/{guide-status-BaMYu55M.mjs → guide-status-CIoWK6mG.mjs} +3 -3
  42. package/dist/hosted-macos-DwowNU7W.mjs +447 -0
  43. package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
  44. package/dist/in-use-CndPMilN.mjs +110 -0
  45. package/dist/{ios-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
  47. package/dist/{ios-device-BW-t_Lkv.mjs → ios-device-DERmMmk-.mjs} +12 -7
  48. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  49. package/dist/{launch-verify-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
  50. package/dist/{logs-DcSRbC2t.mjs → logs-h9h6MUvE.mjs} +95 -13
  51. package/dist/machine-usage-D_YYMzfR.mjs +283 -0
  52. package/dist/machines-CYtOCqcj.mjs +259 -0
  53. package/dist/macos-BwtbUtTN.mjs +468 -0
  54. package/dist/macos-run.d.mts +5 -0
  55. package/dist/macos-run.mjs +2 -0
  56. package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
  57. package/dist/{metro-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-C5UutqpJ.mjs → ports-DddwttKM.mjs} +3 -3
  66. package/dist/prebuild-BvxDlD6C.mjs +213 -0
  67. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  68. package/dist/{project-DGeclhvO.mjs → project-CV3tDFHl.mjs} +9 -18
  69. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  70. package/dist/pull-requests.d.mts +120 -0
  71. package/dist/pull-requests.mjs +2 -0
  72. package/dist/recordings-GEXp4iBT.mjs +37 -0
  73. package/dist/{reload-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
  77. package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/slot-launch-BY0BrOLh.mjs +65 -0
  84. package/dist/{spawn-claims-PT8aQpCa.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  85. package/dist/spawn-entry-DncpJvWL.mjs +16 -0
  86. package/dist/stage-CzaiwFsI.mjs +143 -0
  87. package/dist/start-CPO4wRg7.mjs +2 -0
  88. package/dist/{start-BZhItkiL.mjs → start-CUD9nasA.mjs} +108 -61
  89. package/dist/state-B4PQFXMW.d.mts +12 -0
  90. package/dist/state-BxCgeTQu.mjs +41 -0
  91. package/dist/{state-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
  95. package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -20
  96. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  97. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  98. package/dist/stim-footprint.swift +32 -0
  99. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  100. package/dist/{stop-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +44 -12
  105. package/dist/supervisor-run.mjs +276 -33
  106. package/dist/support-CDQbDkL5.mjs +315 -0
  107. package/dist/support-WLOzVKUv.mjs +176 -0
  108. package/dist/teardown-BYuQafiA.mjs +718 -0
  109. package/dist/{xcode-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
  113. package/dist/web-run.mjs +326 -13
  114. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  115. package/dist/workspace-state-CL2V2Ons.mjs +371 -0
  116. package/dist/worktree-BT_uULFx.mjs +2 -0
  117. package/dist/{worktree-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +55 -10
  121. package/shim/bundle-response.d.cts +1 -0
  122. package/shim/expo-metro-config.cjs +10 -2
  123. package/dist/build-progress-l7d-X-ZP.mjs +0 -247
  124. package/dist/in-use-wYEaKQM9.mjs +0 -249
  125. package/dist/ownership-PUP4Dwai.mjs +0 -1138
  126. package/dist/server-expo-BeYwP_Xy.mjs +0 -2
  127. package/dist/slot-launch-BIWEW-wI.mjs +0 -19
  128. package/dist/start-Rw2GD3nV.mjs +0 -2
  129. package/dist/stats-CaSwmsSZ.mjs +0 -62
  130. package/dist/stats-D8rUTPHd.mjs +0 -368
  131. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  132. package/dist/worktree-3cBa-bQt.mjs +0 -2
  133. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,28 +1,31 @@
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-0jCcMM0E.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-BPeY1YMh.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-DeBQSVtR.mjs";
5
- import { a as findProjectRoot, r as detectIsExpo } from "./project-DGeclhvO.mjs";
6
- import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-BIL7rlX7.mjs";
7
- import { d as removeExpiredLease, h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-CL9dZf73.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-qx_31wJo.mjs";
9
- import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-Cd558h31.mjs";
10
- import { l as listAllIosSims } from "./ios-CmEzZiOj.mjs";
11
- import { a as parseIdleDuration, c as recordGcResult, l as takeGcResults, o as shutDownIdleDevices, r as idleDeviceLines, t as collectIdleDevices } from "./idle-DxLi5OkN.mjs";
12
- import { i as canonicalPath, n as withIdleWorkspace, o as listBuildSlots, r as workspaceInUse } from "./in-use-wYEaKQM9.mjs";
13
- import { r as readCollectors } from "./ownership-Bz2kETAS.mjs";
14
- import { f as resolveEasCliBin } from "./remote-cache--aehr4Pr.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-DT9ngZiD.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-E5o91NR1.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-D4Do_XiQ.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";
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 { C as workspaceLogsDir, E as withDirLock$1, T as workspaceStateFile, b as workspaceDir, s as workspaceLastUsed } from "./workspace-state-CL2V2Ons.mjs";
6
+ import { C as sourceCheckoutOf, b as resolveFullRef, d as hasUncommittedWork, l as hasPopulatedSubmodules, m as listWorktrees, o as dirtyPaths, w as unpushedCommits } from "./worktree-C0c5Udwa.mjs";
7
+ import { a as findProjectRoot$1, r as detectIsExpo } from "./project-CV3tDFHl.mjs";
8
+ import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-CliHHn7a.mjs";
9
+ import { d as removeExpiredLease, h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-W5Pi1q38.mjs";
10
+ import { A as listAvds, H as parseAvdSystemImage, I as ownedAvdDirectory, M as listOrphanedAvdDirectories, V as parseAvdDeviceProfile, _ as avdStorageRoots, at as POOL_SETTING_REMEDY, d as androidToolPath, dt as parkedMaxSetting, ft as readParked, it as isStimOwnedSim, j as listInstalledSystemImages, m as avdNameAbsent, o as androidHome, p as avdIniPaths, rt as isStimOwnedAvd, z as ownedAvdSerialResolver } from "./android-Db2eEMaf.mjs";
11
+ import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-BwVb3liH.mjs";
12
+ import { u as listAllIosSims } from "./ios-3CtCRhwr.mjs";
13
+ import { d as parseIdleDuration, f as shutDownIdleDevices, h as takeGcResults, l as idleDeviceLines, m as recordGcResult, s as collectIdleDevices } from "./watchman-Bx0wceoF.mjs";
14
+ import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse } from "./in-use-CndPMilN.mjs";
15
+ import { n as listBuildSlots } from "./build-slots-Df1ApSuH.mjs";
16
+ import { r as readCollectors } from "./ownership-BkYxUtuj.mjs";
17
+ import { f as resolveEasCliBin } from "./remote-cache-CO2elm_b.mjs";
18
+ 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-9ulPsdhY.mjs";
19
+ import { $ as discoverCaches, A as withAndroidAvdSizes, B as trimCaches, C as describeUnverifiableDevices, D as findStaleDeviceRecords, E as findOrphanedDevices, F as isWorkspacesScope, G as memoryCacheKind, I as planCacheEmptying, J as memorySweepIsScoped, K as memoryLines, L as previewScopedEmptying, M as includesParkedDevices, N as includesRecordings, O as findStaleProjectDevices, P as includesWorkspaceOutputs, Q as phase, R as selectCaches, S as describeParkedSims, T as eraseParkedDevices, U as collectMemoryReport, X as reclaimMemory, Y as memoryWorkPending, Z as scopedMemoryReport, _ as collectParkedAvds, a as REBUILD_COST, b as deleteParkedSims, c as collectOrphanedWorkspaces, d as isInsideWorkspaces, et as sizeCaches, f as listWorkspaceDirs, g as collectDeviceLeases, h as DEVICE_LIST_TIMEOUT_MS, i as machineNumber, j as emptyCaches, k as unverifiedDeviceCommand, l as collectWorkspaceOutputs, m as sizeText, o as WORKSPACE_OUTPUT_DIRS, p as rootPresence, q as memoryScope, s as clearWorkspaceOutputs, u as deleteOrphanedWorkspaces, v as collectParkedSims, w as deviceSweepIsScoped, x as deleteProjectDevices, y as deleteParkedAvds, z as selectsBeyondCaches } from "./budget-CMiqghig.mjs";
20
+ import { f as removeWorktreeTarget, g as reclaimProject, h as describeDereferenced, i as matchWorktreeEntry, n as excludePodChurn, o as reclaimKeys, r as excludeWatchmanCookies } from "./worktree-qXKzADSC.mjs";
21
+ import { n as mergeState, t as fetchDefaultBranch } from "./merge-state-BnxLNQiM.mjs";
22
+ import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
23
+ import { basename, dirname, join } from "node:path";
21
24
  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";
25
+ import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1 } from "path";
23
26
  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";
27
+ import { InvalidArgumentError } from "commander";
28
+ import { listRecordedDevices, listStatusCacheEntries, recordedBytes, settingDefinition } from "@stim-cli/core/state";
26
29
  import chalk from "chalk";
27
30
  //#region src/commands/gc/eas-sessions.ts
28
31
  const EAS_OPERATION_TIMEOUT_MS = 3e4;
@@ -87,7 +90,7 @@ function readRecordedRemoteSessionIds(roots) {
87
90
  };
88
91
  }
89
92
  function claimStateAbsence(claim) {
90
- const stateDir = dirname(claim.stateFile);
93
+ const stateDir = dirname$1(claim.stateFile);
91
94
  let entries;
92
95
  try {
93
96
  if (!statSync$1(stateDir).isDirectory()) throw new Error("path is not a directory");
@@ -98,7 +101,7 @@ function claimStateAbsence(claim) {
98
101
  notice: `${stateDir} is not available for EAS ownership verification: ${describeError(error)}`
99
102
  };
100
103
  }
101
- if (!entries.includes(basename(claim.stateFile))) return {
104
+ if (!entries.includes(basename$1(claim.stateFile))) return {
102
105
  absent: true,
103
106
  notice: null
104
107
  };
@@ -135,7 +138,7 @@ function claimStateAbsence(claim) {
135
138
  };
136
139
  }
137
140
  function collectEasSessionSweep(config, deps) {
138
- const projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
141
+ const projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(process.cwd());
139
142
  const empty = (notices = []) => ({
140
143
  projectScope: projectRoot,
141
144
  orphaned: [],
@@ -230,7 +233,7 @@ function unavailableLockRootNotice(roots) {
230
233
  }
231
234
  function malformedRemoteSessionLockNotice(roots) {
232
235
  for (const root of roots) {
233
- const lockPath = join(workspaceDir(root), "remote-session.lock");
236
+ const lockPath = join$1(workspaceDir(root), "remote-session.lock");
234
237
  if (!existsSync$1(lockPath)) continue;
235
238
  if ((0, ownership_claim_exports.readClaimSet)(lockPath).unresolved.length === 0) continue;
236
239
  return `EAS session sweep skipped: the remote-session lock at ${lockPath} has a malformed owner record.`;
@@ -443,7 +446,7 @@ async function deleteEasSessions(easSessionSweep, deps) {
443
446
  //#endregion
444
447
  //#region src/commands/gc/worktrees.ts
445
448
  const DEFAULT_WORKTREE_IDLE_DAYS = 7;
446
- const DAY_MS = 864e5;
449
+ const DAY_MS$1 = 864e5;
447
450
  const MINUTE_MS = 6e4;
448
451
  const GRACE_SETTING = "gc.worktreeGraceMinutes";
449
452
  const MERGE_DECIDES = /* @__PURE__ */ new Set([
@@ -504,7 +507,7 @@ function removalBlocker(facts, olderThan) {
504
507
  if (facts.locked) return skip("locked", "locked with git worktree lock");
505
508
  if (facts.inUse.length) return skip("in-use", `in use: ${facts.inUse.join("; ")}`);
506
509
  if (facts.porcelain === null) return skip("status-unreadable", "git status could not be read");
507
- const dirty = excludePodChurn(facts.porcelain).lines.length;
510
+ const dirty = excludePodChurn(excludeWatchmanCookies(facts.porcelain).lines).lines.length;
508
511
  if (dirty) return skip("dirty", `dirty: ${plural(dirty, "uncommitted or untracked file")}`);
509
512
  if (facts.unpushed === null) return skip("unpushed-unchecked", "unpushed commits could not be checked");
510
513
  const merged = facts.merge?.merged ? facts.merge : null;
@@ -536,7 +539,7 @@ function lastUsedOf(keys) {
536
539
  }
537
540
  function idleDaysOf(keys, now) {
538
541
  const last = lastUsedOf(keys);
539
- return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS)) : null;
542
+ return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS$1)) : null;
540
543
  }
541
544
  function deviceReads() {
542
545
  let sims;
@@ -608,7 +611,7 @@ function logFiles$1(key) {
608
611
  const dir = workspaceLogsDir(key);
609
612
  try {
610
613
  return readdirSync$1(dir).map((name) => ({
611
- path: join(dir, name),
614
+ path: join$1(dir, name),
612
615
  basis: "a Stim log write"
613
616
  }));
614
617
  } catch (error) {
@@ -627,15 +630,15 @@ function worktreeActivityOf(path, keys) {
627
630
  if (!gitDir) return null;
628
631
  const paths = [
629
632
  {
630
- path: join(gitDir, "index"),
633
+ path: join$1(gitDir, "index"),
631
634
  basis: "a git index write"
632
635
  },
633
636
  {
634
- path: join(gitDir, "HEAD"),
637
+ path: join$1(gitDir, "HEAD"),
635
638
  basis: "a git HEAD change"
636
639
  },
637
640
  {
638
- path: join(gitDir, "logs", "HEAD"),
641
+ path: join$1(gitDir, "logs", "HEAD"),
639
642
  basis: "a git reflog entry"
640
643
  }
641
644
  ];
@@ -666,7 +669,7 @@ function porcelainOf(path, gitAnswered) {
666
669
  return lines.length ? lines : null;
667
670
  }
668
671
  function candidateRoots() {
669
- const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute);
672
+ const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute$1);
670
673
  const recorded = listWorkspaceDirs().flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : []);
671
674
  return [.../* @__PURE__ */ new Set([...registered, ...recorded])].filter((root) => existsSync$1(root)).toSorted();
672
675
  }
@@ -708,7 +711,9 @@ async function collectWorktreeSweep({ idle, olderThan, now }) {
708
711
  };
709
712
  const groups = /* @__PURE__ */ new Map();
710
713
  const outside = [];
711
- for (const root of candidateRoots()) {
714
+ const candidates = candidateRoots();
715
+ if (candidates.length) phase("worktrees", `checking ${plural(candidates.length, "project")} for merged or idle worktrees`);
716
+ for (const root of candidates) {
712
717
  const entry = matchWorktreeEntry(listWorktrees(root), root);
713
718
  if (!entry) {
714
719
  outside.push({
@@ -879,6 +884,206 @@ async function removeWorktrees(sweep, { now = Date.now() } = {}) {
879
884
  return failures;
880
885
  }
881
886
  //#endregion
887
+ //#region src/commands/gc/status-caches.ts
888
+ const KINDS = ["disk-usage", "pull-request"];
889
+ const KEPT_REASON = {
890
+ unreadable: "the file records no readable path of its own",
891
+ unknown: "Stim cannot tell whether the folder it measures still exists",
892
+ unmounted: "the volume of the folder it measures is not mounted"
893
+ };
894
+ function witnessPath(path) {
895
+ return basename(path).startsWith("node_modules") ? dirname(path) : path;
896
+ }
897
+ function judge(path, mountedVolumes) {
898
+ if (path === null) return "unreadable";
899
+ const witness = witnessPath(path);
900
+ const present = rootPresence(witness);
901
+ if (present === null) return "unknown";
902
+ if (present) return "present";
903
+ return isOnMountedVolume(witness, mountedVolumes) ? "gone" : "unmounted";
904
+ }
905
+ function classifyStatusCaches(entries, mountedVolumes) {
906
+ const stale = [];
907
+ const kept = /* @__PURE__ */ new Map();
908
+ for (const { kind, file, path, bytes } of entries) {
909
+ const verdict = judge(path, mountedVolumes);
910
+ if (verdict === "present") continue;
911
+ if (verdict === "gone") {
912
+ stale.push({
913
+ kind,
914
+ file,
915
+ path,
916
+ bytes
917
+ });
918
+ continue;
919
+ }
920
+ const dir = dirname(file);
921
+ const key = `${dir}\n${verdict}`;
922
+ const seen = kept.get(key);
923
+ if (seen) seen.count++;
924
+ else kept.set(key, {
925
+ dir,
926
+ verdict,
927
+ count: 1
928
+ });
929
+ }
930
+ return {
931
+ stale,
932
+ skipped: [...kept.values()].map(({ dir, verdict, count }) => ({
933
+ dir,
934
+ reason: `${count} entr${count === 1 ? "y" : "ies"} kept: ${KEPT_REASON[verdict]}`
935
+ }))
936
+ };
937
+ }
938
+ function collectStaleStatusCaches(mountedVolumes) {
939
+ return classifyStatusCaches(listStatusCacheEntries(), mountedVolumes);
940
+ }
941
+ function statusCacheLines(stale) {
942
+ if (!stale.length) return [];
943
+ const total = stale.reduce((sum, entry) => sum + entry.bytes, 0);
944
+ const lines = [`Stale status cache entries (${stale.length}, ${formatBytes(total)}) - the folder or worktree they measure is gone:`];
945
+ for (const kind of KINDS) {
946
+ const count = stale.filter((entry) => entry.kind === kind).length;
947
+ if (count) lines.push(` ${kind}: ${count}`);
948
+ }
949
+ lines.push(" --delete removes them; `stim status` measures a folder again if it comes back.");
950
+ return lines;
951
+ }
952
+ function deleteStaleStatusCaches(stale) {
953
+ const mountedVolumes = listMountedVolumes();
954
+ let failures = 0;
955
+ for (const kind of KINDS) {
956
+ let removed = 0;
957
+ let bytes = 0;
958
+ for (const entry of stale.filter((candidate) => candidate.kind === kind)) {
959
+ if (judge(entry.path, mountedVolumes) !== "gone") continue;
960
+ try {
961
+ rmSync(entry.file, { force: true });
962
+ removed++;
963
+ bytes += entry.bytes;
964
+ } catch (error) {
965
+ failures++;
966
+ const detail = error?.message || String(error);
967
+ console.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
968
+ recordGcResult("statusCache", "failed", `${kind} cache entry`, {
969
+ id: entry.file,
970
+ detail
971
+ });
972
+ }
973
+ }
974
+ if (!removed) continue;
975
+ const label = `${removed} stale ${kind} cache entr${removed === 1 ? "y" : "ies"}`;
976
+ console.log(chalk.green(`Removed ${label}`));
977
+ recordGcResult("statusCache", "done", label, { bytes });
978
+ }
979
+ return failures;
980
+ }
981
+ //#endregion
982
+ //#region src/commands/gc/recordings.ts
983
+ const DAY_MS = 864e5;
984
+ function oldSegments(devices, olderThan, now) {
985
+ const cutoff = now - olderThan * DAY_MS;
986
+ return devices.flatMap((device) => device.segments).filter((segment) => !segment.open && segment.end < cutoff);
987
+ }
988
+ function planRecordings(entries, { whole, olderThan, now }) {
989
+ return entries.map(({ recordings, projectRoot, problem, gone, devices }) => {
990
+ const bytes = recordedBytes(devices);
991
+ const entry = {
992
+ dir: recordings,
993
+ projectRoot,
994
+ bytes,
995
+ withWorkspace: false
996
+ };
997
+ const kept = (keptCode, keptReason) => ({
998
+ ...entry,
999
+ deleteBytes: 0,
1000
+ willDelete: false,
1001
+ keptCode,
1002
+ keptReason
1003
+ });
1004
+ const remove = (deleteBytes, withWorkspace = false) => ({
1005
+ ...entry,
1006
+ withWorkspace,
1007
+ deleteBytes,
1008
+ willDelete: true,
1009
+ keptCode: null,
1010
+ keptReason: null
1011
+ });
1012
+ if (projectRoot === null) return kept("unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`);
1013
+ if (gone) return remove(bytes, true);
1014
+ if (whole) return remove(bytes);
1015
+ if (olderThan !== null) {
1016
+ const old = oldSegments(devices, olderThan, now);
1017
+ return old.length ? remove(old.reduce((sum, segment) => sum + segment.bytes, 0)) : kept("recently-recorded", `nothing recorded more than ${olderThan}d ago`);
1018
+ }
1019
+ return kept("retained", "the last 15 minutes of footage per device stay until worktree remove, --cache recordings, --older-than or recording.enabled false");
1020
+ });
1021
+ }
1022
+ /**
1023
+ * The device recordings of every workspace directory. Recordings in `gone` directories, which the orphaned
1024
+ * workspace and dead project sweeps remove whole, are listed with them; entries whose project root is on an
1025
+ * unmounted volume are left out.
1026
+ */
1027
+ function collectRecordings(plan, gone = []) {
1028
+ const mountedVolumes = listMountedVolumes();
1029
+ 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, {
1030
+ gone: gone.includes(entry.dir),
1031
+ devices: listRecordedDevices(entry.recordings)
1032
+ })), plan);
1033
+ }
1034
+ /** Deletes what `planRecordings` chose, planning again from the files on disk. */
1035
+ function deleteRecordings(report, plan) {
1036
+ let failures = 0;
1037
+ for (const entry of report) {
1038
+ const label = entry.projectRoot ?? entry.dir;
1039
+ if (entry.withWorkspace) continue;
1040
+ if (!entry.willDelete) {
1041
+ if (entry.keptCode === "unresolved" || entry.keptCode === "recently-recorded") {
1042
+ console.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
1043
+ recordGcResult("recording", "kept", label, {
1044
+ bytes: entry.bytes,
1045
+ detail: entry.keptReason
1046
+ });
1047
+ }
1048
+ continue;
1049
+ }
1050
+ try {
1051
+ let deleted = 0;
1052
+ if (plan.whole) {
1053
+ deleted = recordedBytes(listRecordedDevices(entry.dir));
1054
+ rmSync$1(entry.dir, {
1055
+ recursive: true,
1056
+ force: true
1057
+ });
1058
+ } else if (plan.olderThan !== null) for (const segment of oldSegments(listRecordedDevices(entry.dir), plan.olderThan, plan.now)) {
1059
+ rmSync$1(segment.file, { force: true });
1060
+ deleted += segment.bytes;
1061
+ }
1062
+ console.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
1063
+ recordGcResult("recording", "done", label, { bytes: deleted });
1064
+ } catch (error) {
1065
+ failures++;
1066
+ const detail = error?.message || String(error);
1067
+ console.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
1068
+ recordGcResult("recording", "failed", label, {
1069
+ bytes: entry.bytes,
1070
+ detail
1071
+ });
1072
+ }
1073
+ }
1074
+ return failures;
1075
+ }
1076
+ function recordingLines(recordings) {
1077
+ if (!recordings.length) return [];
1078
+ const total = recordings.reduce((sum, entry) => sum + entry.bytes, 0);
1079
+ const lines = [`Device recordings (${formatBytes(total)} in ${recordings.length}):`];
1080
+ for (const entry of recordings) {
1081
+ lines.push(` ${formatBytes(entry.bytes).padStart(10)} ${entry.projectRoot ?? entry.dir}`);
1082
+ lines.push(entry.withWorkspace ? " would be DELETED with its gone workspace directory" : entry.willDelete ? ` would DELETE ${formatBytes(entry.deleteBytes)}` : ` kept: ${entry.keptReason}`);
1083
+ }
1084
+ return lines;
1085
+ }
1086
+ //#endregion
882
1087
  //#region src/commands/gc/report.ts
883
1088
  function shortKey(key) {
884
1089
  const text = String(key ?? "");
@@ -889,7 +1094,12 @@ function parkedAge(parkedAt, now) {
889
1094
  if (!Number.isFinite(at)) return "parked at an unknown time";
890
1095
  return `parked ${formatLongDuration(Math.max(0, now - at))} ago`;
891
1096
  }
892
- function formatParkedSimReport(parkedSims, now) {
1097
+ const PARKED_DELETE_NOTE = " --delete attempts verified deletions and keeps failures.";
1098
+ const PARKED_ERASE_NOTE = " --delete erases each verified device (apps and data) and keeps it parked.";
1099
+ function parkedApp(app) {
1100
+ return app ? ` ${app}` : "";
1101
+ }
1102
+ function formatParkedSimReport(parkedSims, now, erase) {
893
1103
  if (parkedSims.length === 0) return [];
894
1104
  const known = parkedSims.filter((sim) => sim.bytes !== null);
895
1105
  const total = known.reduce((sum, sim) => sum + (sim.bytes ?? 0), 0);
@@ -900,20 +1110,20 @@ function formatParkedSimReport(parkedSims, now) {
900
1110
  const age = parkedAge(sim.parkedAt, now);
901
1111
  const bytes = sim.bytes === null ? "" : ` ${formatBytes(sim.bytes)}`;
902
1112
  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}`);
1113
+ lines.push(` ios ${sim.name} (${shortUdid(sim.udid)})${model ? ` ${model}` : ""} ${age}${parkedApp(sim.app)}${bytes}${gone}`);
904
1114
  }
905
- lines.push(" --delete attempts verified deletions and keeps failures.");
1115
+ lines.push(erase ? PARKED_ERASE_NOTE : PARKED_DELETE_NOTE);
906
1116
  return lines;
907
1117
  }
908
- function formatParkedAvdReport(parkedAvds, now) {
1118
+ function formatParkedAvdReport(parkedAvds, now, erase) {
909
1119
  if (!parkedAvds.length) return [];
910
1120
  const lines = [];
911
1121
  lines.push(`Parked emulators (${parkedAvds.length}):`);
912
1122
  for (const avd of parkedAvds) {
913
1123
  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}`);
1124
+ 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
1125
  }
916
- lines.push(" --delete attempts verified deletions and keeps failures.");
1126
+ lines.push(erase ? PARKED_ERASE_NOTE : PARKED_DELETE_NOTE);
917
1127
  return lines;
918
1128
  }
919
1129
  function projectEntryLines(header, paths) {
@@ -928,7 +1138,11 @@ function unresolvedLockLines(locks, slots) {
928
1138
  " run that yourself once you know nothing is building with it"
929
1139
  ];
930
1140
  }
931
- function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], buildLocks = {
1141
+ function scopeLine(cacheScope) {
1142
+ if (memoryCacheKind(cacheScope)) return `Cache scope: "${cacheScope}". Only these helper processes were inspected.`;
1143
+ 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.`;
1144
+ }
1145
+ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleStatusCaches = [], buildLocks = {
932
1146
  stale: [],
933
1147
  live: [],
934
1148
  unresolved: []
@@ -944,13 +1158,13 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
944
1158
  orphaned: [],
945
1159
  notices: [],
946
1160
  deletionSafe: true
947
- }, caches = [], workspaceOutputs = null, workspaceLogs = [], worktreeSweep = null, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
1161
+ }, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
948
1162
  const lines = [];
949
1163
  const staleLocks = buildLocks?.stale ?? [];
950
1164
  const liveLocks = buildLocks?.live ?? [];
951
1165
  const staleSlots = buildSlots?.stale ?? [];
952
1166
  const expiredLeases = deviceLeases?.expired ?? [];
953
- if (cacheScope) lines.push(`Cache scope: "${cacheScope}". Devices, project entries and locks were not inspected.`);
1167
+ if (cacheScope) lines.push(scopeLine(cacheScope));
954
1168
  else if ([
955
1169
  deadProjects,
956
1170
  invalidProjects,
@@ -961,6 +1175,7 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
961
1175
  staleDevices,
962
1176
  staleDeviceRecords,
963
1177
  staleLedgerEntries,
1178
+ staleStatusCaches,
964
1179
  staleLocks,
965
1180
  staleSlots,
966
1181
  expiredLeases,
@@ -981,8 +1196,8 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
981
1196
  lines.push(...namedPortLines(orphanedPorts));
982
1197
  lines.push(...orphanedWorkspaceLines(orphanedWorkspaces));
983
1198
  lines.push(...worktreeSweepLines(worktreeSweep));
984
- lines.push(...formatParkedSimReport(parkedSims, now));
985
- lines.push(...formatParkedAvdReport(parkedAvds, now));
1199
+ lines.push(...formatParkedSimReport(parkedSims, now, cacheScope !== null));
1200
+ lines.push(...formatParkedAvdReport(parkedAvds, now, cacheScope !== null));
986
1201
  if (orphanedDevices.length) {
987
1202
  lines.push(`Orphaned devices (${orphanedDevices.length}):`);
988
1203
  for (const d of orphanedDevices) lines.push(` ${d.kind} ${d.name} (${d.id})${deviceSizeSuffix(d)}`);
@@ -1016,6 +1231,7 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
1016
1231
  for (const entry of staleLedgerEntries) lines.push(` ${entry.kind} ${entry.id} is not on this machine`);
1017
1232
  lines.push(" --delete forgets the LEDGER ENTRY only; there is no device left to touch.");
1018
1233
  }
1234
+ lines.push(...statusCacheLines(staleStatusCaches));
1019
1235
  if (easSessionSweep.orphaned.length) {
1020
1236
  lines.push(`Orphaned EAS sessions (${easSessionSweep.orphaned.length}) - current EAS project only:`);
1021
1237
  for (const session of easSessionSweep.orphaned) {
@@ -1061,7 +1277,9 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
1061
1277
  for (const entry of skipped) lines.push(` ${entry.dir}: ${entry.reason}`);
1062
1278
  }
1063
1279
  lines.push(...workspaceLogLines(workspaceLogs));
1280
+ lines.push(...recordingLines(recordings));
1064
1281
  lines.push(...cacheLines(caches, workspaceOutputs));
1282
+ lines.push(...memoryLines(memory, memoryCacheKind(cacheScope), now));
1065
1283
  return lines;
1066
1284
  }
1067
1285
  function workspaceLogLines(workspaceLogs) {
@@ -1117,7 +1335,7 @@ function workspaceOutputLines(outputs, bytes) {
1117
1335
  const lines = [
1118
1336
  ` ${formatBytes(bytes).padStart(10)} Workspace build outputs (detected)`,
1119
1337
  ` ${outputs.root}`,
1120
- ` ${WORKSPACE_OUTPUT_DIRS.join(", ")} of each workspace; workspace.json, state.json, logs and device records stay`,
1338
+ ` ${WORKSPACE_OUTPUT_DIRS.join(", ")}, macOS Swift scratch and staged app bundles of each workspace; workspace.json, state.json, logs and device records stay`,
1121
1339
  ` ${REBUILD_COST}`
1122
1340
  ];
1123
1341
  for (const w of outputs.workspaces) {
@@ -1142,6 +1360,7 @@ function cacheLines(caches, workspaceOutputs) {
1142
1360
  if (c.note) lines.push(` ${c.note}`);
1143
1361
  if (c.willEmpty) lines.push(" would be EMPTIED");
1144
1362
  else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
1363
+ else if (c.scopedEmpty?.emptySkipped) lines.push(` --delete --cache would leave it alone: ${c.scopedEmpty.emptySkipped}`);
1145
1364
  }
1146
1365
  lines.push(` total: ${formatBytes(total)}`);
1147
1366
  const doomed = caches.filter((c) => c.willEmpty);
@@ -1191,7 +1410,7 @@ function jsonPullRequest(lookup) {
1191
1410
  containsHead: pr.containsHead
1192
1411
  } : null;
1193
1412
  }
1194
- function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], buildLocks = {
1413
+ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleStatusCaches = [], buildLocks = {
1195
1414
  stale: [],
1196
1415
  live: [],
1197
1416
  unresolved: []
@@ -1207,7 +1426,7 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1207
1426
  orphaned: [],
1208
1427
  notices: [],
1209
1428
  deletionSafe: true
1210
- }, workspaceOutputs = null, workspaceLogs = [], caches = [] }) {
1429
+ }, workspaceOutputs = null, workspaceLogs = [], recordings = [], caches = [], memory = null }) {
1211
1430
  return {
1212
1431
  deadProjects: deadProjects.map((path) => ({ path })),
1213
1432
  invalidProjects: invalidProjects.map((path) => ({ path })),
@@ -1232,20 +1451,22 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1232
1451
  detail: w.skipped ?? worktreeRemovalReason(w),
1233
1452
  eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString()
1234
1453
  })),
1235
- parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, bytes, listed }) => ({
1454
+ parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, app, bytes, listed }) => ({
1236
1455
  udid,
1237
1456
  name,
1238
1457
  model,
1239
1458
  runtime,
1240
1459
  parkedAt,
1460
+ app,
1241
1461
  bytes,
1242
1462
  listed
1243
1463
  })),
1244
- parkedEmulators: parkedAvds.map(({ name, systemImage, deviceProfile, parkedAt, bytes, listed }) => ({
1464
+ parkedEmulators: parkedAvds.map(({ name, systemImage, deviceProfile, parkedAt, app, bytes, listed }) => ({
1245
1465
  name,
1246
1466
  systemImage,
1247
1467
  deviceProfile,
1248
1468
  parkedAt,
1469
+ app,
1249
1470
  bytes,
1250
1471
  listed
1251
1472
  })),
@@ -1281,6 +1502,12 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1281
1502
  kind,
1282
1503
  id
1283
1504
  })),
1505
+ staleStatusCaches: staleStatusCaches.map(({ kind, path, file, bytes }) => ({
1506
+ kind,
1507
+ path,
1508
+ file,
1509
+ bytes
1510
+ })),
1284
1511
  idleDevices: idleDevices.map(({ kind, id, name, project, slot, lastActivityAt, idleForMs, buildInProgress }) => ({
1285
1512
  kind,
1286
1513
  id,
@@ -1341,6 +1568,16 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1341
1568
  reason: w.keptCode,
1342
1569
  detail: w.keptReason
1343
1570
  })),
1571
+ recordings: recordings.map((r) => ({
1572
+ dir: r.dir,
1573
+ projectRoot: r.projectRoot,
1574
+ bytes: r.bytes,
1575
+ deleteBytes: r.deleteBytes,
1576
+ willDelete: r.willDelete,
1577
+ withWorkspace: r.withWorkspace,
1578
+ reason: r.keptCode,
1579
+ detail: r.keptReason
1580
+ })),
1344
1581
  workspaceBuildOutputs: (workspaceOutputs?.workspaces ?? []).map((w) => ({
1345
1582
  dir: w.dir,
1346
1583
  projectRoot: w.projectRoot,
@@ -1357,8 +1594,12 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1357
1594
  bytes: c.bytes ?? null,
1358
1595
  note: c.note || null,
1359
1596
  willEmpty: Boolean(c.willEmpty),
1360
- emptySkipped: c.emptySkipped ?? null
1361
- }))
1597
+ emptySkipped: c.emptySkipped ?? null,
1598
+ scopedEmpty: c.scopedEmpty ?? null
1599
+ })),
1600
+ memory: memory?.processes ?? [],
1601
+ watchmanRoots: memory?.watchmanRoots ?? [],
1602
+ memoryNotices: (memory?.notices ?? []).map((message) => ({ message }))
1362
1603
  };
1363
1604
  }
1364
1605
  //#endregion
@@ -1417,8 +1658,8 @@ function staleBrowserProfiles(ledger, exists = existsSync) {
1417
1658
  }));
1418
1659
  }
1419
1660
  function isStaleProfile(path, exists) {
1420
- const workspace = dirname$1(dirname$1(path));
1421
- return !exists(path) && exists(dirname$1(workspace)) && !isDanglingLink(workspace);
1661
+ const workspace = dirname(dirname(path));
1662
+ return !exists(path) && exists(dirname(workspace)) && !isDanglingLink(workspace);
1422
1663
  }
1423
1664
  function isDanglingLink(path) {
1424
1665
  try {
@@ -1464,7 +1705,7 @@ function fileSize(path) {
1464
1705
  }
1465
1706
  function cappedFiles(logsDir) {
1466
1707
  return CAPPED_LOGS.flatMap((name) => {
1467
- const log = join(logsDir, name);
1708
+ const log = join$1(logsDir, name);
1468
1709
  return [{
1469
1710
  file: log,
1470
1711
  log
@@ -1490,14 +1731,14 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1490
1731
  for (const { dir, projectRoot, problem } of listWorkspaceDirs()) {
1491
1732
  if (exclude.includes(dir)) continue;
1492
1733
  if (projectRoot !== null && !isOnMountedVolume(projectRoot, mountedVolumes)) continue;
1493
- const logsDir = join(dir, "logs");
1734
+ const logsDir = join$1(dir, "logs");
1494
1735
  let names;
1495
1736
  try {
1496
1737
  names = readdirSync$1(logsDir);
1497
1738
  } catch {
1498
1739
  continue;
1499
1740
  }
1500
- const bytes = names.reduce((sum, name) => sum + fileSize(join(logsDir, name)), 0);
1741
+ const bytes = names.reduce((sum, name) => sum + fileSize(join$1(logsDir, name)), 0);
1501
1742
  if (bytes === 0) continue;
1502
1743
  const trimBytes = excess(logsDir);
1503
1744
  let keptCode = null;
@@ -1532,7 +1773,7 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1532
1773
  }
1533
1774
  function trimToNewest({ file, log }, maxBytes) {
1534
1775
  if (!trimmable(fileSize(file))) return 0;
1535
- return withDirLock(`${log}.lock`, () => {
1776
+ return withDirLock$1(`${log}.lock`, () => {
1536
1777
  const { size, atime, mtime } = statSync$1(file);
1537
1778
  if (!trimmable(size)) return 0;
1538
1779
  const tail = Buffer.alloc(maxBytes);
@@ -1575,7 +1816,7 @@ async function trimWorkspaceLogs(entries) {
1575
1816
  };
1576
1817
  return {
1577
1818
  kept: null,
1578
- bytes: cappedFiles(join(entry.dir, "logs")).reduce((sum, capped) => sum + trimToNewest(capped, LOG_ROTATE_BYTES), 0)
1819
+ bytes: cappedFiles(join$1(entry.dir, "logs")).reduce((sum, capped) => sum + trimToNewest(capped, LOG_ROTATE_BYTES), 0)
1579
1820
  };
1580
1821
  }, { purpose: "gc" });
1581
1822
  } catch (error) {
@@ -1775,7 +2016,7 @@ function listAvdRecords(notices, roots = avdStorageRoots()) {
1775
2016
  if (records.has(name)) continue;
1776
2017
  let ini;
1777
2018
  try {
1778
- ini = readFileSync(join$1(root, entry), "utf8");
2019
+ ini = readFileSync(join(root, entry), "utf8");
1779
2020
  } catch (error) {
1780
2021
  if (!isMissing(error)) notices.push(unreadableNotice(`AVD ${name}`, error));
1781
2022
  continue;
@@ -1784,7 +2025,7 @@ function listAvdRecords(notices, roots = avdStorageRoots()) {
1784
2025
  let config = "";
1785
2026
  let unreadable = null;
1786
2027
  for (const candidate of avdIniPaths(root, ini)) try {
1787
- config = readFileSync(join$1(candidate, "config.ini"), "utf8");
2028
+ config = readFileSync(join(candidate, "config.ini"), "utf8");
1788
2029
  directory = candidate;
1789
2030
  break;
1790
2031
  } catch (error) {
@@ -1796,7 +2037,7 @@ function listAvdRecords(notices, roots = avdStorageRoots()) {
1796
2037
  directory,
1797
2038
  systemImage: parseAvdSystemImage(config),
1798
2039
  deviceProfile: parseAvdDeviceProfile(config),
1799
- lastUsedAt: directory ? modifiedAt(join$1(directory, "hardware-qemu.ini")) : null
2040
+ lastUsedAt: directory ? modifiedAt(join(directory, "hardware-qemu.ini")) : null
1800
2041
  });
1801
2042
  }
1802
2043
  }
@@ -1837,10 +2078,10 @@ function collectInventory(config, deadProjects) {
1837
2078
  notices.push(`simulator runtimes not listed: ${describeError(error)}`);
1838
2079
  }
1839
2080
  }
1840
- const imagesRoot = join$1(androidHome(), "system-images");
2081
+ const imagesRoot = join(androidHome(), "system-images");
1841
2082
  const images = listInstalledSystemImages((error) => notices.push(unreadableNotice("Android system images", error))).map((image) => ({
1842
2083
  pkg: image.pkg,
1843
- directory: join$1(imagesRoot, `android-${image.api}`, image.tag, image.arch)
2084
+ directory: join(imagesRoot, `android-${image.api}`, image.tag, image.arch)
1844
2085
  }));
1845
2086
  return {
1846
2087
  ...buildInventory({
@@ -1863,12 +2104,54 @@ function removeInvalidProjectEntries(invalidProjects) {
1863
2104
  recordGcResult("project", "done", path);
1864
2105
  }
1865
2106
  }
2107
+ function collectGcMemory(scope) {
2108
+ return memorySweepIsScoped() ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true);
2109
+ }
2110
+ function classifyProjectEntries(cfg, mountedVolumes) {
2111
+ const deadProjects = [];
2112
+ const invalidProjects = [];
2113
+ const skipped = [];
2114
+ for (const [path, project] of Object.entries(cfg?.projects || {})) {
2115
+ if (!isAbsolute$1(path)) {
2116
+ const claimed = describeDereferenced(project);
2117
+ if (claimed.length) skipped.push({
2118
+ dir: path,
2119
+ 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`
2120
+ });
2121
+ else invalidProjects.push(path);
2122
+ continue;
2123
+ }
2124
+ if (existsSync$1(path)) continue;
2125
+ if (!isOnMountedVolume(path, mountedVolumes)) {
2126
+ const volume = volumeRootFor(path);
2127
+ skipped.push({
2128
+ dir: path,
2129
+ reason: `volume ${volume} is not mounted`
2130
+ });
2131
+ } else deadProjects.push(path);
2132
+ }
2133
+ return {
2134
+ deadProjects,
2135
+ invalidProjects,
2136
+ skipped
2137
+ };
2138
+ }
2139
+ /** The workspace directories an unscoped run removes whole: those of dead projects and orphaned directories. */
2140
+ function removedWholeWorkspaceDirs() {
2141
+ const mountedVolumes = listMountedVolumes();
2142
+ const cfg = loadConfig$1();
2143
+ const { deadProjects } = classifyProjectEntries(cfg, mountedVolumes);
2144
+ const { orphaned } = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { measure: false });
2145
+ return [...orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
2146
+ }
1866
2147
  async function collectGcReport({ olderThan = null, cache = null, worktrees = false, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
1867
2148
  const scope = typeof cache === "string" && cache.trim() ? cache : null;
1868
2149
  const all = scope !== null && olderThan === null;
1869
2150
  const withWorkspaces = includesWorkspaceOutputs(scope);
1870
2151
  const selected = selectCaches(discoverCaches(), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
1871
- const caches = planCacheEmptying(sizeCaches(selected), all);
2152
+ if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`);
2153
+ const sized = sizeCaches(selected);
2154
+ const caches = scope === null ? previewScopedEmptying(sized) : planCacheEmptying(sized, all);
1872
2155
  if (scope) return {
1873
2156
  skipped: [],
1874
2157
  deadProjects: [],
@@ -1879,6 +2162,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1879
2162
  staleDevices: [],
1880
2163
  staleDeviceRecords: [],
1881
2164
  staleLedgerEntries: [],
2165
+ staleStatusCaches: [],
1882
2166
  buildLocks: {
1883
2167
  stale: [],
1884
2168
  live: [],
@@ -1901,15 +2185,28 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1901
2185
  notices: [],
1902
2186
  deletionSafe: true
1903
2187
  },
1904
- parkedSims: [],
1905
- parkedAvds: [],
2188
+ parkedSims: includesParkedDevices(scope) ? collectParkedSims(deps, {
2189
+ olderThanDays: olderThan,
2190
+ now
2191
+ }) : [],
2192
+ parkedAvds: includesParkedDevices(scope) ? collectParkedAvds(deps, {
2193
+ olderThanDays: olderThan,
2194
+ now
2195
+ }) : [],
1906
2196
  caches,
1907
2197
  workspaceOutputs: withWorkspaces ? collectWorkspaceOutputs({
1908
2198
  olderThan,
1909
- now
2199
+ now,
2200
+ exclude: isWorkspacesScope(scope) ? removedWholeWorkspaceDirs() : []
1910
2201
  }) : null,
1911
2202
  workspaceLogs: [],
2203
+ recordings: includesRecordings(scope) ? collectRecordings({
2204
+ whole: all,
2205
+ olderThan,
2206
+ now
2207
+ }) : [],
1912
2208
  worktreeSweep: null,
2209
+ memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope)) : null,
1913
2210
  cacheScope: scope,
1914
2211
  olderThan,
1915
2212
  all
@@ -1935,30 +2232,11 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1935
2232
  olderThanDays: olderThan,
1936
2233
  now
1937
2234
  });
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
- }
2235
+ const { deadProjects, invalidProjects, skipped } = classifyProjectEntries(cfg, mountedVolumes);
1960
2236
  const workspaceDirs = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
1961
2237
  skipped.push(...workspaceDirs.skipped);
2238
+ const statusCaches = collectStaleStatusCaches(mountedVolumes);
2239
+ skipped.push(...statusCaches.skipped);
1962
2240
  const deviceSweepNotices = [];
1963
2241
  let orphanedDevices = [];
1964
2242
  let unverifiedDevices = [];
@@ -1967,6 +2245,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1967
2245
  let staleLedgerEntries = staleBrowserProfiles(readCreatedDevices$1());
1968
2246
  let idleDevices = [];
1969
2247
  const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped() ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
2248
+ phase("devices", "listing simulators and emulators");
1970
2249
  if (unsweepableReason) {
1971
2250
  let simNames = [];
1972
2251
  let avdNames = [];
@@ -2072,6 +2351,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2072
2351
  staleDevices,
2073
2352
  staleDeviceRecords,
2074
2353
  staleLedgerEntries,
2354
+ staleStatusCaches: statusCaches.stale,
2075
2355
  buildLocks: {
2076
2356
  stale: locks.filter((l) => !l.alive && !l.unresolved),
2077
2357
  live: locks.filter((l) => l.alive),
@@ -2093,11 +2373,20 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2093
2373
  exclude: goneWorkspaceDirs
2094
2374
  }),
2095
2375
  workspaceLogs: collectWorkspaceLogs({ exclude: goneWorkspaceDirs }),
2376
+ recordings: collectRecordings({
2377
+ whole: false,
2378
+ olderThan,
2379
+ now
2380
+ }, goneWorkspaceDirs),
2096
2381
  worktreeSweep: await collectWorktreeSweep({
2097
2382
  idle: worktrees,
2098
2383
  olderThan,
2099
2384
  now
2100
2385
  }),
2386
+ memory: await collectGcMemory({
2387
+ watchman: true,
2388
+ gradle: true
2389
+ }),
2101
2390
  cacheScope: null,
2102
2391
  olderThan,
2103
2392
  all
@@ -2133,6 +2422,20 @@ async function sweep(opts, deps) {
2133
2422
  remedy
2134
2423
  };
2135
2424
  }
2425
+ const memoryKind = memoryCacheKind(opts.cache);
2426
+ if (memoryKind && opts.olderThan !== void 0) {
2427
+ const message = `--older-than does not apply to --cache ${memoryKind}, which acts on running processes.`;
2428
+ const remedy = `Run \`stim gc --cache ${memoryKind}\` without --older-than.`;
2429
+ console.error(chalk.red(message));
2430
+ console.error(chalk.dim(remedy));
2431
+ console.error(chalk.red("failed: STIM_BAD_ARG"));
2432
+ process.exitCode = 1;
2433
+ return {
2434
+ code: "STIM_BAD_ARG",
2435
+ message,
2436
+ remedy
2437
+ };
2438
+ }
2136
2439
  const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
2137
2440
  if (poolError) console.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2138
2441
  if (opts.cache) return runGcCore(opts, {
@@ -2146,7 +2449,7 @@ async function sweep(opts, deps) {
2146
2449
  });
2147
2450
  let projectRoot = null;
2148
2451
  try {
2149
- projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
2452
+ projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(process.cwd());
2150
2453
  if (!projectRoot) return runGcCore(opts, {
2151
2454
  ...deps,
2152
2455
  precollectedEasSessionSweep: {
@@ -2209,7 +2512,7 @@ async function pruneDeadProjects(deadProjects) {
2209
2512
  recordGcResult("project", "kept", path, { detail: "its absence can no longer be confirmed" });
2210
2513
  continue;
2211
2514
  }
2212
- const result = await reclaimProject(path).catch((error) => {
2515
+ const result = await reclaimProject(path, { stopRequester: { by: "stim gc --delete" } }).catch((error) => {
2213
2516
  deleteFailures++;
2214
2517
  console.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
2215
2518
  recordGcResult("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
@@ -2244,6 +2547,25 @@ async function pruneDeadProjects(deadProjects) {
2244
2547
  }
2245
2548
  return deleteFailures;
2246
2549
  }
2550
+ function scopedDryRunAction(cache) {
2551
+ const memory = memoryCacheKind(cache);
2552
+ if (memory === "watchman") return "remove the stale roots and shut down the unused watchman";
2553
+ if (memory === "gradle-daemons") return "stop the idle daemons";
2554
+ return includesParkedDevices(cache) ? "erase the parked devices" : "empty the caches";
2555
+ }
2556
+ function dryRunHint(cache, all, actionable, caches) {
2557
+ if (all && (!memoryCacheKind(cache) || actionable)) return `Dry run. Re-run with --delete to ${scopedDryRunAction(cache)} above.`;
2558
+ if (actionable) return "Dry run. Re-run with --delete to reclaim.";
2559
+ if (caches) return "Pass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them.";
2560
+ return null;
2561
+ }
2562
+ function pruneStatusCaches(report) {
2563
+ if (report.cacheScope !== null) return 0;
2564
+ return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale);
2565
+ }
2566
+ function reclaimParkedDevices(report, deps) {
2567
+ return report.cacheScope ? eraseParkedDevices(report.parkedSims, report.parkedAvds) : deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
2568
+ }
2247
2569
  async function runGcCore(opts, deps) {
2248
2570
  takeGcResults();
2249
2571
  const olderThan = typeof opts.olderThan === "number" ? opts.olderThan : null;
@@ -2253,7 +2575,7 @@ async function runGcCore(opts, deps) {
2253
2575
  cache,
2254
2576
  worktrees: Boolean(opts.worktrees)
2255
2577
  }, deps);
2256
- if (cache && report.caches.length === 0 && report.workspaceOutputs === null) {
2578
+ if (cache && report.caches.length === 0 && report.workspaceOutputs === null && !selectsBeyondCaches(cache)) {
2257
2579
  const names = [...new Set(discoverCaches().map((c) => c.name))];
2258
2580
  const message = `No shared cache carries "${cache}" in its name or directory.`;
2259
2581
  console.log(chalk.yellow(message));
@@ -2268,7 +2590,7 @@ async function runGcCore(opts, deps) {
2268
2590
  const all = report.all;
2269
2591
  for (const line of formatGcReport(report)) console.log(line);
2270
2592
  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;
2593
+ 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) || 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
2594
  const idle = opts.idle ?? null;
2273
2595
  const idleFailures = idle === null ? 0 : shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects)).failures;
2274
2596
  if (idleFailures) process.exitCode = 1;
@@ -2286,20 +2608,26 @@ async function runGcCore(opts, deps) {
2286
2608
  inventory
2287
2609
  });
2288
2610
  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."));
2611
+ const hint = dryRunHint(cache, all, actionable, caches.length > 0);
2612
+ if (hint) console.log(chalk.dim(`\n${hint}`));
2292
2613
  return payload(idle === null ? null : idleFailures);
2293
2614
  }
2294
2615
  let deleteFailures = idleFailures;
2295
2616
  deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, { olderThan })).failures : 0;
2296
2617
  deleteFailures += await trimWorkspaceLogs(report.workspaceLogs);
2297
- deleteFailures += deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
2618
+ deleteFailures += deleteRecordings(report.recordings, {
2619
+ whole: all,
2620
+ olderThan,
2621
+ now: Date.now()
2622
+ });
2623
+ deleteFailures += reclaimParkedDevices(report, deps);
2624
+ deleteFailures += await reclaimMemory(cache, report.memory);
2298
2625
  removeInvalidProjectEntries(invalidProjects);
2299
2626
  deleteFailures += await pruneDeadProjects(deadProjects);
2300
2627
  deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces);
2301
2628
  deleteFailures += deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
2302
2629
  deleteFailures += forgetStaleLedgerEntries(report.staleLedgerEntries);
2630
+ deleteFailures += pruneStatusCaches(report);
2303
2631
  for (const lock of buildLocks.stale) {
2304
2632
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2305
2633
  root: lock.path,
@@ -2378,11 +2706,11 @@ async function runGcCore(opts, deps) {
2378
2706
  return payload(deleteFailures);
2379
2707
  }
2380
2708
  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) => {
2709
+ 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
2710
  const n = parseInt(v, 10);
2383
2711
  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
2712
  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) => {
2713
+ }).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; all, workspaces, recordings, parked, 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. 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
2714
  if (!v.trim()) throw new InvalidArgumentError("must name a cache, e.g. --cache \"compilation cache\"");
2387
2715
  return v;
2388
2716
  }).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) => {