stim 1.14.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 (130) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-CYVDcrt3.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-XpvCrV7q.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-LbPFRWVL.mjs → android-Cu_L4RpL.mjs} +502 -407
  5. package/dist/{android-d6rGq1pA.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-BpgSgkJj.mjs → app-install-CAAplXAt.mjs} +95 -161
  9. package/dist/{budget-CGDkhvv_.mjs → budget-CMiqghig.mjs} +1038 -73
  10. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-aeGMFX7e.mjs} +193 -653
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  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-DVcpw6eE.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  22. package/dist/{created-devices-CllH_8eb.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-D9IcKqXS.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-CjuuAJjQ.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-5c3WrVRm.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-BiyDyPP-.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-D87zEzaq.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-x9pnU9fq.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-ko6mUAYD.mjs → gc-BuL1Genr.mjs} +429 -101
  39. package/dist/{android-cas-iZK3DOCr.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-6R0VNFqI.mjs → guide-cCSGRFd3.mjs} +1959 -222
  41. package/dist/{guide-status-BZrFPIA_.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-BwDPrnYQ.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-P1f5nm2n.mjs → ios-Chob8ny7.mjs} +512 -263
  47. package/dist/{ios-device-DGglU3E_.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-Deg4tB3Z.mjs → launch-verify-CLlofxRt.mjs} +4 -4
  50. package/dist/{logs-Civ4gbBH.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-CcEo8Fl8.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Dj_XBNaL.mjs} +6 -59
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-i6OkVg-w.mjs} +9 -5
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-DyYDF5ww.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-BguS83ex.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-CWlvEaeN.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-BiniwsSZ.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-X7PUgizL.mjs} +33 -15
  77. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-D61iNOsB.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-B-fsC-Om.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-DrDN4WuV.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-BY0BrOLh.mjs} +6 -6
  84. package/dist/{spawn-claims-Drw_o-Fb.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-s4RXp1wy.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-DHqvsbqC.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-Dk9RVXI7.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-BsPAnfOI.mjs → status-DYXxNOT0.mjs} +423 -268
  95. package/dist/{status-BgAuxESg.mjs → status-l84Gbvp2.mjs} +116 -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-BzreOWJp.mjs → stop-Bad61z_2.mjs} +38 -12
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-BWIF_6BK.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +43 -11
  105. package/dist/supervisor-run.mjs +227 -19
  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-Cp5KvK9x.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DXtx3RbX.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-BsQs1WlO.mjs → web-k6w8-HnE.mjs} +37 -20
  113. package/dist/web-run.mjs +309 -12
  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-BVrHsYZG.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-UrQWXSeb.mjs → worktree-qXKzADSC.mjs} +154 -252
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +28 -12
  121. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  122. package/dist/in-use-DlqBDKqY.mjs +0 -249
  123. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  124. package/dist/start-Cetfvy8l.mjs +0 -2
  125. package/dist/stats-CFpFGlJV.mjs +0 -62
  126. package/dist/stats-CX1Z8Tca.mjs +0 -368
  127. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  128. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  129. package/dist/worktree-CHWWvm5v.mjs +0 -2
  130. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,25 +1,31 @@
1
- import { B as deviceSlotPlatforms, E as withConfigLock$1, F as measuredDirectorySize, I as volumeRootFor, M as formatBytes, N as isOnMountedVolume, O as process_identity_exports, P as listMountedVolumes, U as projectDeviceSlots, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as directorySize, o as clearSupervisor, p as getProject } from "./config-lvc7d3C2.mjs";
2
- import { i as registeredCaches } from "./cache-manifest-C68QQ0ua.mjs";
3
- import { C as sharedBuildCache, N as workspaceName$1, c as resolveProjectMetro, h as readWorkspaceState$1, k as workspaceDir, n as killMetroTree, y as workspaceLastUsed } from "./metro-CcEo8Fl8.mjs";
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, P as measuredDirectorySize, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as formatBytes, o as clearSupervisor, p as getProject, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
3
+ import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
4
+ import { i as registeredCaches } from "./cache-manifest-DnUHBJaU.mjs";
5
+ import { b as workspaceDir, m as sharedBuildCache, r as readWorkspaceState$1, s as workspaceLastUsed, w as workspaceName$1 } from "./workspace-state-CL2V2Ons.mjs";
4
6
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
5
- import { s as clearWorkspaceSupervisor } from "./state-DHqvsbqC.mjs";
6
- import { a as withWorkspaceProcessLock, o as workspaceProcessLockError } from "./named-ports-Blg8IxOh.mjs";
7
- import { l as phaseLine, u as plural } from "./command-output-DVcpw6eE.mjs";
8
- import { i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-5c3WrVRm.mjs";
9
- import { o as workspaceActivity } from "./activity-CYVDcrt3.mjs";
10
- import { F as ownedAvdDirectory, R as ownedAvdSerialResolver, at as dropParked, k as listAvds, lt as readParked, nt as isStimOwnedSim, tt as isStimOwnedAvd } from "./android-d6rGq1pA.mjs";
11
- import { C as teardownOwnedIosSim, S as teardownOwnedBrowser, T as teardownParkedIosSim, l as withdrawDevServerStopRequest, s as requestDevServerStop, u as resolveSupervisorTarget, w as teardownParkedAvd, x as teardownOwnedAvd } from "./stop-cause-hjGBOZTG.mjs";
12
- import { d as listIosDeviceTypes, g as parseRuntimeVersion, l as listAllIosSims } from "./ios-BwDPrnYQ.mjs";
13
- import { c as recordGcResult, i as idleShutdownCandidates, n as collectOwnedDeviceActivity, o as shutDownIdleDevices, s as workspaceBuildInProgress, t as collectIdleDevices } from "./idle-DXtx3RbX.mjs";
14
- import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-DlqBDKqY.mjs";
15
- import { c as formatSpace, r as capacity, s as environmentState } from "./status-BgAuxESg.mjs";
16
- import { existsSync, statSync, statfsSync } from "node:fs";
7
+ import { l as readPidFile, s as clearWorkspaceSupervisor } from "./state-CaeaHbDA.mjs";
8
+ import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-BL7Q0HCO.mjs";
9
+ import { l as workspaceActivity, r as createDeviceProcessTables } from "./activity-B2-kb7k0.mjs";
10
+ import { l as phaseLine, o as formatLongDuration, u as plural } from "./command-output-CliHHn7a.mjs";
11
+ import { h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-W5Pi1q38.mjs";
12
+ import { A as listAvds, I as ownedAvdDirectory, ft as readParked, it as isStimOwnedSim, rt as isStimOwnedAvd, st as dropParked, z as ownedAvdSerialResolver } from "./android-Db2eEMaf.mjs";
13
+ import { l as resolveProjectMetro, r as killMetroTree } from "./metro-Di8EN2Tb.mjs";
14
+ import { a as teardownOwnedBrowser, c as teardownParkedIosSim, i as teardownOwnedAvd, n as eraseParkedIosSim, o as teardownOwnedIosSim, s as teardownParkedAvd, t as eraseParkedAvd } from "./teardown-BYuQafiA.mjs";
15
+ import { _ as parseRuntimeVersion, f as listIosDeviceTypes, u as listAllIosSims } from "./ios-3CtCRhwr.mjs";
16
+ import { a as parseWatchmanPid, c as collectOwnedDeviceActivity, f as shutDownIdleDevices, i as parseWatchmanClients, m as recordGcResult, n as parseTriggerCount, p as workspaceBuildInProgress, r as parseWatchRoots, s as collectIdleDevices, t as parseSubscriberNames, u as idleShutdownCandidates } from "./watchman-Bx0wceoF.mjs";
17
+ import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-CndPMilN.mjs";
18
+ import { n as listBuildSlots } from "./build-slots-Df1ApSuH.mjs";
19
+ import { l as withdrawDevServerStopRequest, s as requestDevServerStop, u as resolveSupervisorTarget } from "./stop-cause-CZ_Mr-v9.mjs";
20
+ import { a as capacity, l as environmentState, u as formatSpace } from "./status-l84Gbvp2.mjs";
21
+ import { r as readFootprints, t as STIM_SERVER_COMMAND } from "./machine-usage-D_YYMzfR.mjs";
22
+ import { existsSync, readFileSync, statSync, statfsSync } from "node:fs";
23
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
17
24
  import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
18
- import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
19
- import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
20
- import { isJsonObject, readJsonFile, settingDefinition, settingValueError } from "@stim-cli/core/state";
21
- import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
22
25
  import { homedir, tmpdir } from "os";
26
+ import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
27
+ import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
28
+ import { buildWorkerRoot, isJsonObject, loadConfig, readJsonFile, settingDefinition, settingValueError } from "@stim-cli/core/state";
23
29
  import { homedir as homedir$1, platform, totalmem as totalmem$1 } from "node:os";
24
30
  import chalk from "chalk";
25
31
  //#region src/supervisor/cache-reset.ts
@@ -71,10 +77,10 @@ async function stopOwnedMetroForReset(root) {
71
77
  //#endregion
72
78
  //#region src/cache/caches.ts
73
79
  function xcodeDerivedDataRoot() {
74
- return join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
80
+ return join$1(homedir(), "Library", "Developer", "Xcode", "DerivedData");
75
81
  }
76
82
  function compilationCache() {
77
- const dir = join(xcodeDerivedDataRoot(), "CompilationCache.noindex");
83
+ const dir = join$1(xcodeDerivedDataRoot(), "CompilationCache.noindex");
78
84
  if (!existsSync$1(dir)) return null;
79
85
  return {
80
86
  name: "Xcode compilation cache",
@@ -84,8 +90,8 @@ function compilationCache() {
84
90
  };
85
91
  }
86
92
  function gradleBuildCache() {
87
- const gradleHome = process.env.GRADLE_USER_HOME || join(homedir(), ".gradle");
88
- const dir = resolve(gradleHome, "caches", "build-cache-1");
93
+ const gradleHome = process.env.GRADLE_USER_HOME || join$1(homedir(), ".gradle");
94
+ const dir = resolve$1(gradleHome, "caches", "build-cache-1");
89
95
  if (!existsSync$1(dir)) return null;
90
96
  return {
91
97
  name: "Gradle build cache",
@@ -107,9 +113,9 @@ function metroFileMaps() {
107
113
  const files = [];
108
114
  let bytes = 0;
109
115
  for (const n of names) try {
110
- const st = statSync$1(join(root, n));
116
+ const st = statSync$1(join$1(root, n));
111
117
  if (uid !== void 0 && st.uid !== uid) continue;
112
- files.push(join(root, n));
118
+ files.push(join$1(root, n));
113
119
  bytes += st.size;
114
120
  } catch {}
115
121
  if (!files.length) return null;
@@ -136,7 +142,15 @@ function discoverCaches() {
136
142
  gradle,
137
143
  metroFileMaps()
138
144
  ].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
139
- return mergeCacheDescriptors([...registered, ...detected], gradleDir);
145
+ return distinguishSharedNames(mergeCacheDescriptors([...registered, ...detected], gradleDir));
146
+ }
147
+ function distinguishSharedNames(caches) {
148
+ const counts = /* @__PURE__ */ new Map();
149
+ for (const cache of caches) counts.set(cache.name, (counts.get(cache.name) ?? 0) + 1);
150
+ const labelled = caches.map((cache) => (counts.get(cache.name) ?? 0) < 2 ? cache : Object.assign({}, cache, { name: `${cache.name}: ${basename$1(cache.dir) || cache.dir}` }));
151
+ const labels = /* @__PURE__ */ new Map();
152
+ for (const cache of labelled) labels.set(cache.name, (labels.get(cache.name) ?? 0) + 1);
153
+ return labelled.map((cache, i) => (labels.get(cache.name) ?? 0) < 2 || cache === caches[i] ? cache : Object.assign({}, cache, { name: `${caches[i].name}: ${cache.dir}` }));
140
154
  }
141
155
  function suppressLegacyMetroAncestors(caches) {
142
156
  const current = caches.filter(isCurrentMetroStore);
@@ -168,7 +182,7 @@ function isMetroStore(cache) {
168
182
  function isNamedMetroChild(parent, candidate) {
169
183
  const dir = canonicalCacheDir(parent);
170
184
  const child = canonicalCacheDir(candidate);
171
- return dirname(child) === dir || canonicalCacheDir(dirname(candidate)) === dir;
185
+ return dirname$1(child) === dir || canonicalCacheDir(dirname$1(candidate)) === dir;
172
186
  }
173
187
  function mergeCacheDescriptors(caches, gradleDir) {
174
188
  const merged = [];
@@ -219,14 +233,14 @@ function cachePathsOverlap(left, right) {
219
233
  return cachePathContains(left, right) || cachePathContains(right, left);
220
234
  }
221
235
  function cachePathContains(parent, child) {
222
- const rel = relative(parent, child);
223
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
236
+ const rel = relative$1(parent, child);
237
+ return rel === "" || !rel.startsWith("..") && !isAbsolute$1(rel);
224
238
  }
225
239
  function canonicalCacheDir(dir) {
226
240
  try {
227
241
  return realpathSync$1(dir);
228
242
  } catch {
229
- return resolve(dir);
243
+ return resolve$1(dir);
230
244
  }
231
245
  }
232
246
  function sizeCaches(caches) {
@@ -288,13 +302,890 @@ function entriesAtDepth(dir, depth) {
288
302
  } catch {
289
303
  continue;
290
304
  }
291
- for (const name of names) next.push(join(parent, name));
305
+ for (const name of names) next.push(join$1(parent, name));
292
306
  }
293
307
  level = next;
294
308
  }
295
309
  return level;
296
310
  }
297
311
  //#endregion
312
+ //#region src/commands/gc/progress.ts
313
+ function phase(label, text) {
314
+ console.error(chalk.dim(phaseLine(label, text)));
315
+ }
316
+ //#endregion
317
+ //#region src/commands/gc/memory.ts
318
+ const WATCHMAN_KIND = "watchman";
319
+ const GRADLE_DAEMONS_KIND = "gradle-daemons";
320
+ /** The memory kind `--cache` names, or null for any other name. */
321
+ function memoryCacheKind(cache) {
322
+ const wanted = cache?.trim().toLowerCase();
323
+ return wanted === "watchman" || wanted === "gradle-daemons" ? wanted : null;
324
+ }
325
+ /** Which helpers a gc run inspects: both without --cache, one for its own kind, none for any other --cache. */
326
+ function memoryScope(cache) {
327
+ if (!cache?.trim()) return {
328
+ watchman: true,
329
+ gradle: true
330
+ };
331
+ const kind = memoryCacheKind(cache);
332
+ return kind ? {
333
+ watchman: kind === WATCHMAN_KIND,
334
+ gradle: kind === GRADLE_DAEMONS_KIND
335
+ } : null;
336
+ }
337
+ const WATCHMAN_TIMEOUT_MS = 5e3;
338
+ const WATCHMAN_DOCTOR_BYTES = 2 * 1024 ** 3;
339
+ const GRADLE_STATUS_TIMEOUT_MS = 2e4;
340
+ const GRADLE_STOP_TIMEOUT_MS = 6e4;
341
+ const LSOF_TIMEOUT_MS = 1e4;
342
+ const ROOT_PROBES_AT_ONCE = 4;
343
+ const GRADLE_DAEMON_CLASS = /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\s+(\S+)/;
344
+ const KOTLIN_DAEMON_CLASS = /^\S.*?\/bin\/java\s.*\sorg\.jetbrains\.kotlin\.daemon\.KotlinCompileDaemon(\s|$)/;
345
+ /**
346
+ * The Gradle version, Java home and distribution of a `GradleDaemon` process from its command line. The
347
+ * distribution is the directory whose `lib/` holds the daemon's main jar (`gradle-daemon-main-<v>.jar`, or
348
+ * `gradle-launcher-<v>.jar` before Gradle 8.13), so `<distribution>/bin/gradle` is the daemon's own launcher.
349
+ */
350
+ function parseGradleDaemonCommand(command) {
351
+ const version = GRADLE_DAEMON_CLASS.exec(command)?.[1];
352
+ const java = /^(\/.*?)\/bin\/java\s/.exec(command)?.[1];
353
+ if (!version || !java) return null;
354
+ const jar = /(?:^|\s)-(?:cp|classpath)\s+(?:[^\s]*:)?(\/.*?\/lib\/gradle-(?:daemon-main|launcher)-[^/\s:]+\.jar)(?=[\s:])/.exec(command)?.[1];
355
+ return {
356
+ version,
357
+ javaHome: java,
358
+ distribution: jar ? dirname(dirname(jar)) : null
359
+ };
360
+ }
361
+ function isKotlinDaemonCommand(command) {
362
+ return KOTLIN_DAEMON_CLASS.test(command) && !GRADLE_DAEMON_CLASS.test(command);
363
+ }
364
+ /**
365
+ * The offload client whose Gradle home under `workerRoot` a daemon uses: named by its classpath, as stim-server
366
+ * matches it, or by the Gradle home it runs in.
367
+ */
368
+ function offloadClientOf(command, workerRoot, gradleHome) {
369
+ const root = resolve(workerRoot).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
370
+ const home = new RegExp(`${root}/([^/\\s]+)/cache/gradle(/|$)`);
371
+ return home.exec(command)?.[1] ?? (gradleHome ? home.exec(gradleHome)?.[1] ?? null : null);
372
+ }
373
+ /** `gradle --status` rows: the state of each daemon pid it lists, lowercased (idle, busy, stopped, ...). */
374
+ function parseGradleStatus(output) {
375
+ const states = /* @__PURE__ */ new Map();
376
+ for (const line of output.split("\n")) {
377
+ const match = /^\s*(\d+)\s+([A-Z][A-Z_]*)\s+\S/.exec(line);
378
+ if (match) states.set(Number(match[1]), match[2].toLowerCase());
379
+ }
380
+ return states;
381
+ }
382
+ /** `lsof -F pnT` output: each pid's open file names and its ESTABLISHED TCP connections. */
383
+ function parseLsof(output) {
384
+ const processes = /* @__PURE__ */ new Map();
385
+ let current = null;
386
+ let name = null;
387
+ for (const line of output.split("\n")) if (line.startsWith("p")) {
388
+ const pid = Number(line.slice(1));
389
+ current = processes.get(pid) ?? {
390
+ files: [],
391
+ connections: []
392
+ };
393
+ processes.set(pid, current);
394
+ } else if (current && line.startsWith("n")) {
395
+ name = line.slice(1);
396
+ current.files.push(name);
397
+ } else if (current && name?.includes("->") && line === "TST=ESTABLISHED") current.connections.push(name);
398
+ return processes;
399
+ }
400
+ /**
401
+ * The process at the other end of each of `pid`'s connections, found among the other processes lsof listed by the
402
+ * reversed `remote->local` name; null for a peer lsof did not list.
403
+ */
404
+ function connectionPeers(pid, lsof) {
405
+ return (lsof.get(pid)?.connections ?? []).map((connection) => {
406
+ const [local, remote] = connection.split("->");
407
+ const reversed = `${remote}->${local}`;
408
+ for (const [other, entry] of lsof) if (other !== pid && entry.connections.includes(reversed)) return other;
409
+ return null;
410
+ });
411
+ }
412
+ /** The Gradle user home a daemon runs in: the one whose `daemon/<version>/daemon-<pid>.out.log` it holds open. */
413
+ function gradleHomeFromFiles(pid, version, files) {
414
+ const suffix = `/daemon/${version}/daemon-${pid}.out.log`;
415
+ const log = files.find((file) => file.endsWith(suffix));
416
+ return log ? log.slice(0, -suffix.length) : null;
417
+ }
418
+ const fsProbe = {
419
+ exists: existsSync,
420
+ isFile(path) {
421
+ try {
422
+ return statSync(path).isFile();
423
+ } catch {
424
+ return false;
425
+ }
426
+ },
427
+ read(path) {
428
+ try {
429
+ return readFileSync(path, "utf8");
430
+ } catch {
431
+ return null;
432
+ }
433
+ },
434
+ mounted: (path) => isOnMountedVolume(path)
435
+ };
436
+ /**
437
+ * Why a watch root is stale: its directory is gone from a mounted volume, or the nearest `.git` above it is a
438
+ * linked worktree's `.git` file whose gitdir no longer exists (git pruned the worktree and left the directory).
439
+ * Null when it is live or when that cannot be proven, such as a path on a volume that is not mounted.
440
+ */
441
+ function watchmanRootStaleness(path, probe = fsProbe) {
442
+ if (!probe.exists(path)) return probe.mounted(path) ? "missing" : null;
443
+ for (let dir = path;; dir = dirname(dir)) {
444
+ const git = join(dir, ".git");
445
+ if (probe.exists(git)) {
446
+ if (!probe.isFile(git)) return null;
447
+ const target = /^gitdir:\s*(.+?)\s*$/m.exec(probe.read(git) ?? "")?.[1];
448
+ if (!target) return null;
449
+ const gitdir = isAbsolute(target) ? target : resolve(dir, target);
450
+ return probe.exists(gitdir) || !probe.mounted(gitdir) ? null : "pruned-worktree";
451
+ }
452
+ if (dirname(dir) === dir) return null;
453
+ }
454
+ }
455
+ function staleText(stale) {
456
+ return stale === "missing" ? "its directory is gone" : "its linked worktree was pruned";
457
+ }
458
+ function planWatchmanRoot(root) {
459
+ const subscriptions = root.subscribers?.length ?? null;
460
+ const base = {
461
+ path: root.path,
462
+ stale: root.stale,
463
+ subscriptions,
464
+ triggers: root.triggers
465
+ };
466
+ if (root.stale === null) return {
467
+ ...base,
468
+ removable: false,
469
+ detail: null
470
+ };
471
+ const kept = subscriptions === null ? "could not list its subscriptions" : subscriptions > 0 ? `${subscriptions} subscription(s) still use it` : root.triggers === null ? "could not list its triggers" : root.triggers > 0 ? `it has ${root.triggers} trigger(s)` : null;
472
+ return {
473
+ ...base,
474
+ removable: kept === null,
475
+ detail: kept ? `${staleText(root.stale)}, but ${kept}` : staleText(root.stale)
476
+ };
477
+ }
478
+ /**
479
+ * Decides the watchman roots gc removes and whether it may shut the daemon down. A stale root goes only while no
480
+ * subscription or trigger uses it. The daemon goes only when `debug-status` lists no client besides gc's own call and
481
+ * every root's subscriptions and triggers could be read, no kept root has a trigger, and no root has a subscription;
482
+ * `describeClient` names each client that keeps it.
483
+ */
484
+ function planWatchman(facts, describeClient) {
485
+ const roots = (facts.roots ?? []).map(planWatchmanRoot);
486
+ const unreadRoots = facts.roots === null || roots.some((root) => root.subscriptions === null || root.triggers === null);
487
+ const triggered = roots.filter((root) => !root.removable && (root.triggers ?? 0) > 0);
488
+ let reason = null;
489
+ let detail = null;
490
+ if (facts.clients === null || unreadRoots) {
491
+ reason = "unknown";
492
+ detail = facts.clients === null ? "watchman debug-status did not list its clients" : "could not list the subscriptions and triggers of every root";
493
+ } else if (facts.clients.length > 0) {
494
+ reason = "in-use";
495
+ detail = `used by ${[...new Set(facts.clients.map(describeClient))].join(", ")}; shutting it down would break their file watching`;
496
+ } else if (roots.some((root) => !root.removable && (root.subscriptions ?? 0) > 0)) {
497
+ reason = "in-use";
498
+ detail = "a root still has a subscription";
499
+ } else if (triggered.length > 0) {
500
+ reason = "in-use";
501
+ detail = `${triggered.map((root) => root.path).join(", ")} ha${triggered.length === 1 ? "s" : "ve"} triggers that stop firing while watchman is down`;
502
+ }
503
+ return {
504
+ process: {
505
+ kind: "watchman",
506
+ cacheKind: WATCHMAN_KIND,
507
+ pid: facts.pid,
508
+ startedAt: facts.startedAt,
509
+ bytes: facts.bytes,
510
+ measure: facts.measure,
511
+ version: null,
512
+ gradleHome: null,
513
+ offloadClient: null,
514
+ state: reason === "in-use" ? "busy" : reason === "unknown" ? "unknown" : "idle",
515
+ reclaimable: reason === null,
516
+ reason,
517
+ detail
518
+ },
519
+ roots
520
+ };
521
+ }
522
+ /**
523
+ * Decides which Gradle and Kotlin daemons gc may stop. A running Stim build (`buildRunning`) keeps every one. An
524
+ * offload worker's daemon is left to stim-server while it runs. A Gradle daemon goes only when its own `gradle
525
+ * --status` says IDLE. A Kotlin daemon goes only when every Gradle daemon is idle and each of its connections comes from
526
+ * a Gradle daemon gc stops in the same run.
527
+ */
528
+ function planDaemons({ gradle, kotlin, buildRunning, stimServerRunning }) {
529
+ const plans = [];
530
+ const gradleBusy = gradle.some((daemon) => !("state" in daemon.status) || daemon.status.state !== "idle");
531
+ for (const daemon of gradle) {
532
+ const state = "state" in daemon.status ? daemon.status.state : null;
533
+ const idle = state === "idle";
534
+ let reason = null;
535
+ let detail = null;
536
+ if (daemon.offloadClient !== null && stimServerRunning) {
537
+ reason = "stim-server";
538
+ detail = `stim-server stops the daemons of offload client ${daemon.offloadClient} when that client loses build or memory runs low`;
539
+ } else if (buildRunning) {
540
+ reason = "build-running";
541
+ detail = buildRunning;
542
+ } else if (state === "busy") {
543
+ reason = "busy";
544
+ detail = "a build is using it";
545
+ } else if (!idle) {
546
+ reason = "unknown";
547
+ detail = "unknown" in daemon.status ? daemon.status.unknown : `gradle --status reports it ${state}`;
548
+ }
549
+ plans.push({
550
+ kind: "gradleDaemon",
551
+ cacheKind: GRADLE_DAEMONS_KIND,
552
+ pid: daemon.pid,
553
+ startedAt: daemon.startedAt,
554
+ bytes: daemon.bytes,
555
+ measure: daemon.measure,
556
+ version: daemon.command.version,
557
+ gradleHome: daemon.gradleHome,
558
+ offloadClient: daemon.offloadClient,
559
+ state: idle ? "idle" : state === "busy" ? "busy" : "unknown",
560
+ reclaimable: reason === null,
561
+ reason,
562
+ detail
563
+ });
564
+ }
565
+ const stopping = new Set(plans.filter((plan) => plan.reclaimable).map((plan) => plan.pid));
566
+ for (const daemon of kotlin) {
567
+ const clients = daemon.peers?.filter((peer) => peer === null || !stopping.has(peer)) ?? [];
568
+ let reason = null;
569
+ let detail = null;
570
+ if (buildRunning) {
571
+ reason = "build-running";
572
+ detail = buildRunning;
573
+ } else if (daemon.peers === null) {
574
+ reason = "unknown";
575
+ detail = "lsof could not list its connections";
576
+ } else if (gradleBusy) {
577
+ reason = "busy";
578
+ detail = "a Gradle daemon is busy or its state is unknown, and it may be compiling through this one";
579
+ } else if (clients.length > 0) {
580
+ reason = "busy";
581
+ detail = `${clients.length} connection(s) from a process gc does not stop are open`;
582
+ }
583
+ plans.push({
584
+ kind: "kotlinDaemon",
585
+ cacheKind: GRADLE_DAEMONS_KIND,
586
+ pid: daemon.pid,
587
+ startedAt: daemon.startedAt,
588
+ bytes: daemon.bytes,
589
+ measure: daemon.measure,
590
+ version: null,
591
+ gradleHome: null,
592
+ offloadClient: null,
593
+ state: reason === null ? "idle" : reason === "unknown" ? "unknown" : "busy",
594
+ reclaimable: reason === null,
595
+ reason,
596
+ detail
597
+ });
598
+ }
599
+ return plans;
600
+ }
601
+ /**
602
+ * Why no Gradle or Kotlin daemon may be stopped now, or null. An Android build lock, or a build slot that no live iOS
603
+ * build lock of the same process holds, counts as a running build; one whose holder is unresolved counts too.
604
+ */
605
+ function runningBuild(locks, slots) {
606
+ const android = locks.find((lock) => lock.platform === "android" && (lock.alive || lock.unresolved));
607
+ if (android) return `${android.unresolved ? "an unresolved" : "a running"} Android build holds ${android.path} (pid ${android.pid ?? "?"}, ${android.projectRoot || "unrecorded workspace"})`;
608
+ const ios = new Set(locks.filter((lock) => lock.platform === "ios" && lock.alive).map((lock) => lock.pid));
609
+ const slot = slots.find((s) => (s.alive || s.unresolved) && !(s.alive && s.pid !== null && ios.has(s.pid)));
610
+ if (slot) return `${slot.unresolved ? "an unresolved" : "a running"} build holds build slot ${slot.index ?? "?"} (pid ${slot.pid ?? "?"}, ${slot.projectRoot || "unrecorded workspace"})`;
611
+ return null;
612
+ }
613
+ /** A watchman client's pid resolved to the Stim workspace whose supervisor it is or descends from. */
614
+ function stimWorkspaceOf(pid, parentOf, supervisors) {
615
+ let current = pid;
616
+ for (let depth = 0; current !== void 0 && current > 1 && depth < 64; depth++) {
617
+ const workspace = supervisors.get(current);
618
+ if (workspace) return workspace;
619
+ current = parentOf.get(current);
620
+ }
621
+ return null;
622
+ }
623
+ function reclaimableBytes(report, kind) {
624
+ return report.processes.filter((entry) => entry.reclaimable && (kind === void 0 || entry.cacheKind === kind)).reduce((sum, entry) => sum + entry.bytes, 0);
625
+ }
626
+ const boundWatchman = async (args) => {
627
+ const answer = JSON.parse(await getExecutor().runFileAsync("watchman", ["--no-spawn", ...args], { timeoutMs: WATCHMAN_TIMEOUT_MS }));
628
+ const error = answer?.error;
629
+ if (error !== void 0) throw new Error(String(error));
630
+ return answer;
631
+ };
632
+ async function quietly(fn) {
633
+ try {
634
+ return await fn();
635
+ } catch {
636
+ return null;
637
+ }
638
+ }
639
+ /**
640
+ * `watchman debug-status` with gc's own call left out of its clients. The call is spawned directly so its pid is
641
+ * known: debug-status lists the asking CLI as a client too.
642
+ */
643
+ function watchmanClients() {
644
+ return new Promise((done) => {
645
+ let child;
646
+ try {
647
+ child = getExecutor().spawn("watchman", ["--no-spawn", "debug-status"], { stdio: [
648
+ "ignore",
649
+ "pipe",
650
+ "ignore"
651
+ ] });
652
+ } catch {
653
+ done(null);
654
+ return;
655
+ }
656
+ const self = child.pid;
657
+ const chunks = [];
658
+ const timer = setTimeout(() => child.kill("SIGTERM"), WATCHMAN_TIMEOUT_MS);
659
+ child.stdout?.on("data", (chunk) => chunks.push(chunk));
660
+ child.on("error", () => {
661
+ clearTimeout(timer);
662
+ done(null);
663
+ });
664
+ child.on("close", (status) => {
665
+ clearTimeout(timer);
666
+ let clients = null;
667
+ try {
668
+ if (status === 0 && self !== void 0) clients = parseWatchmanClients(JSON.parse(Buffer.concat(chunks).toString("utf8")));
669
+ } catch {}
670
+ done(clients ? clients.filter((client) => client.pid !== self) : null);
671
+ });
672
+ });
673
+ }
674
+ async function collectWatchmanRoots(pid, progress) {
675
+ const watchman = boundWatchman;
676
+ const paths = parseWatchRoots(await quietly(() => watchman(["watch-list"])));
677
+ if (!paths) return null;
678
+ if (progress) phase("daemons", `watchman pid ${pid}: checking ${plural(paths.length, "root")}`);
679
+ const roots = [];
680
+ let next = 0;
681
+ const probe = async () => {
682
+ while (next < paths.length) {
683
+ const index = next++;
684
+ const path = paths[index];
685
+ const [subscriptions, triggers] = await Promise.all([quietly(() => watchman(["debug-get-subscriptions", path])), quietly(() => watchman(["trigger-list", path]))]);
686
+ roots[index] = {
687
+ path,
688
+ stale: watchmanRootStaleness(path),
689
+ subscribers: parseSubscriberNames(subscriptions),
690
+ triggers: parseTriggerCount(triggers)
691
+ };
692
+ }
693
+ };
694
+ await Promise.all(Array.from({ length: Math.min(ROOT_PROBES_AT_ONCE, paths.length) }, probe));
695
+ return roots;
696
+ }
697
+ function measureOf(pid, rows, footprints) {
698
+ const footprint = footprints?.get(pid);
699
+ if (footprint !== void 0) return {
700
+ bytes: footprint,
701
+ measure: "footprint"
702
+ };
703
+ return {
704
+ bytes: (rows.get(pid)?.rssKb ?? 0) * 1024,
705
+ measure: "rss"
706
+ };
707
+ }
708
+ function supervisorPids() {
709
+ const supervisors = /* @__PURE__ */ new Map();
710
+ for (const root of Object.keys(loadConfig()?.projects ?? {})) {
711
+ const pid = readPidFile(root);
712
+ if (pid) supervisors.set(pid, root);
713
+ }
714
+ return supervisors;
715
+ }
716
+ async function readLsof(pids) {
717
+ if (pids.length === 0) return /* @__PURE__ */ new Map();
718
+ try {
719
+ return parseLsof(await getExecutor().runFileAsync("lsof", [
720
+ "-n",
721
+ "-P",
722
+ "-F",
723
+ "pnT",
724
+ "-p",
725
+ pids.join(",")
726
+ ], { timeoutMs: LSOF_TIMEOUT_MS }));
727
+ } catch (error) {
728
+ const { status, signal: killed, stdout } = error;
729
+ return status === 1 && !killed && typeof stdout === "string" && stdout ? parseLsof(stdout) : null;
730
+ }
731
+ }
732
+ async function gradleStatus(command, home) {
733
+ if (!command.distribution) return { unknown: "could not find its Gradle distribution on its classpath" };
734
+ const launcher = join(command.distribution, "bin", "gradle");
735
+ if (!existsSync(launcher)) return { unknown: `its distribution has no ${launcher}` };
736
+ try {
737
+ return parseGradleStatus(await getExecutor().runFileAsync(launcher, [
738
+ "--status",
739
+ "-q",
740
+ "-g",
741
+ home
742
+ ], {
743
+ cwd: home,
744
+ env: { JAVA_HOME: command.javaHome },
745
+ omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
746
+ timeoutMs: GRADLE_STATUS_TIMEOUT_MS
747
+ }));
748
+ } catch (error) {
749
+ return { unknown: `gradle --status failed: ${error.message.split("\n")[0]}` };
750
+ }
751
+ }
752
+ function groupKey(command, home) {
753
+ return JSON.stringify([
754
+ home,
755
+ command.version,
756
+ command.distribution,
757
+ command.javaHome
758
+ ]);
759
+ }
760
+ async function collectGradleFacts(rows, byPid, footprints, stimServerRunning, progress) {
761
+ const gradleRows = rows.flatMap((row) => {
762
+ const command = parseGradleDaemonCommand(row.command);
763
+ return command ? [{
764
+ row,
765
+ command
766
+ }] : [];
767
+ });
768
+ const kotlinRows = rows.filter((row) => isKotlinDaemonCommand(row.command));
769
+ if (gradleRows.length === 0 && kotlinRows.length === 0) return {
770
+ gradle: [],
771
+ kotlin: []
772
+ };
773
+ if (progress) phase("daemons", `checking ${plural(gradleRows.length, "Gradle daemon")} and ${plural(kotlinRows.length, "Kotlin daemon")}`);
774
+ const lsof = await readLsof([...gradleRows.map(({ row }) => row.pid), ...kotlinRows.map((row) => row.pid)]);
775
+ const workerRoot = buildWorkerRoot();
776
+ const homes = gradleRows.map(({ row, command }) => {
777
+ const files = lsof?.get(row.pid)?.files;
778
+ return files ? gradleHomeFromFiles(row.pid, command.version, files) : null;
779
+ });
780
+ const offload = gradleRows.map(({ row }, index) => offloadClientOf(row.command, workerRoot, homes[index] ?? null));
781
+ const statuses = /* @__PURE__ */ new Map();
782
+ gradleRows.forEach(({ command }, index) => {
783
+ const home = homes[index];
784
+ if (offload[index] !== null && stimServerRunning) return;
785
+ if (home && !statuses.has(groupKey(command, home))) {
786
+ if (progress) phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
787
+ statuses.set(groupKey(command, home), gradleStatus(command, home));
788
+ }
789
+ });
790
+ const gradle = [];
791
+ for (const [index, { row, command }] of gradleRows.entries()) {
792
+ const home = homes[index] ?? null;
793
+ let status;
794
+ if (offload[index] !== null && stimServerRunning) status = { unknown: "stim-server manages it" };
795
+ else if (!home) status = { unknown: lsof ? "could not find its Gradle user home among its open files" : "lsof failed" };
796
+ else {
797
+ const states = await statuses.get(groupKey(command, home));
798
+ if (states instanceof Map) {
799
+ const state = states.get(row.pid);
800
+ status = state ? { state } : { unknown: "gradle --status did not list it" };
801
+ } else status = states;
802
+ }
803
+ gradle.push({
804
+ pid: row.pid,
805
+ startedAt: row.startedAt,
806
+ ...measureOf(row.pid, byPid, footprints),
807
+ command,
808
+ gradleHome: home,
809
+ offloadClient: offload[index] ?? null,
810
+ status
811
+ });
812
+ }
813
+ return {
814
+ gradle,
815
+ kotlin: kotlinRows.map((row) => Object.assign(measureOf(row.pid, byPid, footprints), {
816
+ pid: row.pid,
817
+ startedAt: row.startedAt,
818
+ peers: lsof?.get(row.pid) ? connectionPeers(row.pid, lsof) : null
819
+ }))
820
+ };
821
+ }
822
+ async function collectWatchmanFacts(byPid, footprints, progress) {
823
+ const pid = parseWatchmanPid(await quietly(() => boundWatchman(["get-pid"])));
824
+ if (pid === null) return null;
825
+ const roots = await collectWatchmanRoots(pid, progress);
826
+ const clients = (await watchmanClients())?.filter((client) => client.pid !== pid) ?? null;
827
+ return {
828
+ pid,
829
+ startedAt: byPid.get(pid)?.startedAt ?? null,
830
+ ...measureOf(pid, byPid, footprints),
831
+ roots,
832
+ clients
833
+ };
834
+ }
835
+ function watchmanClientDescriber(rows) {
836
+ const parentOf = new Map(rows.map((row) => [row.pid, row.ppid]));
837
+ const supervisors = supervisorPids();
838
+ return (client) => {
839
+ const workspace = stimWorkspaceOf(client.pid, parentOf, supervisors);
840
+ return workspace ? `the Stim workspace ${workspace}` : `${client.name ?? "a client"} (pid ${client.pid})`;
841
+ };
842
+ }
843
+ function memorySweepIsScoped() {
844
+ return Boolean(process.env.STIM_HOME);
845
+ }
846
+ /** The report of a run whose STIM_HOME is set: the helpers are machine-global, so none is inspected or stopped. */
847
+ function scopedMemoryReport() {
848
+ return {
849
+ processes: [],
850
+ watchmanRoots: [],
851
+ notices: ["memory sweep skipped: STIM_HOME scopes this config, but watchman and the Gradle and Kotlin daemons are machine-global"]
852
+ };
853
+ }
854
+ async function collectMemoryReport(scope, progress = false) {
855
+ const report = {
856
+ processes: [],
857
+ watchmanRoots: [],
858
+ notices: []
859
+ };
860
+ if (process.platform === "win32") return report;
861
+ const rows = createDeviceProcessTables().host();
862
+ if (rows === null) {
863
+ report.notices.push("memory sweep skipped: ps did not answer");
864
+ return report;
865
+ }
866
+ const byPid = new Map(rows.map((row) => [row.pid, row]));
867
+ const footprints = await readFootprints();
868
+ if (scope.watchman) {
869
+ const facts = await collectWatchmanFacts(byPid, footprints, progress);
870
+ if (facts) {
871
+ const plan = planWatchman(facts, watchmanClientDescriber(rows));
872
+ report.processes.push(plan.process);
873
+ report.watchmanRoots = plan.roots;
874
+ } else if (!scope.gradle) report.notices.push("no watchman daemon answered");
875
+ }
876
+ if (scope.gradle) {
877
+ const stimServerRunning = rows.some((row) => STIM_SERVER_COMMAND.test(row.command));
878
+ const facts = await collectGradleFacts(rows, byPid, footprints, stimServerRunning, progress);
879
+ report.processes.push(...planDaemons({
880
+ ...facts,
881
+ buildRunning: facts.gradle.length || facts.kotlin.length ? runningBuild(listBuildLocks(), listBuildSlots()) : null,
882
+ stimServerRunning
883
+ }));
884
+ if (!scope.watchman && report.processes.length === 0) report.notices.push("no Gradle or Kotlin daemon is running");
885
+ }
886
+ return report;
887
+ }
888
+ function processLabel(entry) {
889
+ if (entry.kind === "watchman") return `watchman pid ${entry.pid}`;
890
+ if (entry.kind === "kotlinDaemon") return `Kotlin compile daemon pid ${entry.pid}`;
891
+ const where = entry.offloadClient ? `offload client ${entry.offloadClient}` : entry.gradleHome ?? "unknown Gradle home";
892
+ return `Gradle ${entry.version ?? "?"} daemon pid ${entry.pid} (${where})`;
893
+ }
894
+ function uptime(startedAt, now) {
895
+ const at = startedAt ? Date.parse(startedAt) : NaN;
896
+ return Number.isFinite(at) ? `up ${formatLongDuration(Math.max(0, now - at))}` : "uptime unknown";
897
+ }
898
+ function memoryLines(report, scoped, now = Date.now()) {
899
+ if (!report || report.processes.length === 0 && report.notices.length === 0) return [];
900
+ const lines = [`Memory (${report.processes.some((entry) => entry.measure === "rss") ? "footprint, resident size where unreadable" : "physical footprint"}) - shared helpers that grow while they run; only --cache watchman or gradle-daemons stops them:`];
901
+ for (const entry of report.processes) {
902
+ const state = entry.reclaimable ? "idle" : entry.kind === "watchman" && entry.state === "busy" ? "in use" : entry.state;
903
+ lines.push(` ${processLabel(entry)} ${formatBytes(entry.bytes)}, ${uptime(entry.startedAt, now)}, ${state}`);
904
+ if (entry.kind === "watchman") {
905
+ const stale = report.watchmanRoots.filter((root) => root.stale !== null).length;
906
+ lines.push(` ${report.watchmanRoots.length} root(s), ${stale} stale`);
907
+ for (const root of report.watchmanRoots.filter((r) => r.stale !== null)) lines.push(` stale root ${root.path}: ${root.detail}${root.removable ? "" : "; kept"}`);
908
+ }
909
+ if (entry.detail) lines.push(` ${entry.reclaimable ? "" : "kept: "}${entry.detail}`);
910
+ }
911
+ for (const notice of report.notices) lines.push(` ${notice}`);
912
+ const kinds = scoped ? [scoped] : [WATCHMAN_KIND, GRADLE_DAEMONS_KIND];
913
+ const parts = [];
914
+ for (const kind of kinds) {
915
+ if (!report.processes.some((entry) => entry.cacheKind === kind)) continue;
916
+ const roots = kind === "watchman" ? report.watchmanRoots.filter((root) => root.removable).length : 0;
917
+ const note = roots ? ` (and ${roots} stale root(s) removed)` : "";
918
+ parts.push(`${formatBytes(reclaimableBytes(report, kind))} with stim gc --delete --cache ${kind}${note}`);
919
+ }
920
+ if (parts.length) lines.push(` reclaimable ${formatBytes(reclaimableBytes(report))}: ${parts.join("; ")}`);
921
+ return lines;
922
+ }
923
+ function sameProcess(entry, rows) {
924
+ const row = rows?.get(entry.pid);
925
+ return Boolean(row && entry.startedAt !== null && row.startedAt === entry.startedAt);
926
+ }
927
+ function signal(pid) {
928
+ try {
929
+ process.kill(pid, "SIGTERM");
930
+ return null;
931
+ } catch (error) {
932
+ return error.code === "ESRCH" ? null : error.message;
933
+ }
934
+ }
935
+ function keep(entry, kind, detail) {
936
+ console.log(chalk.yellow(`Kept ${processLabel(entry)}: ${detail}`));
937
+ recordGcResult(kind, "kept", processLabel(entry), {
938
+ id: String(entry.pid),
939
+ bytes: entry.bytes,
940
+ detail
941
+ });
942
+ }
943
+ async function reclaimWatchman(before) {
944
+ let failures = 0;
945
+ const fresh = await collectMemoryReport({
946
+ watchman: true,
947
+ gradle: false
948
+ }, true);
949
+ const daemon = fresh.processes.find((entry) => entry.kind === "watchman");
950
+ const planned = before.processes.find((entry) => entry.kind === "watchman");
951
+ if (!daemon || !planned || daemon.pid !== planned.pid) {
952
+ if (planned) keep(planned, "watchman", "the daemon changed or stopped since the report");
953
+ return 0;
954
+ }
955
+ for (const root of fresh.watchmanRoots.filter((r) => r.stale !== null)) {
956
+ if (!root.removable) {
957
+ console.log(chalk.yellow(`Kept the watchman root ${root.path}: ${root.detail}`));
958
+ recordGcResult("watchmanRoot", "kept", root.path, { detail: root.detail });
959
+ continue;
960
+ }
961
+ try {
962
+ phase("daemons", `removing stale watchman root ${root.path}`);
963
+ await boundWatchman(["watch-del", root.path]);
964
+ console.log(chalk.green(`Removed the stale watchman root ${root.path} (${root.detail})`));
965
+ recordGcResult("watchmanRoot", "done", root.path, { detail: root.detail });
966
+ } catch (error) {
967
+ failures++;
968
+ console.log(chalk.red(`Could not remove the watchman root ${root.path}: ${error.message}`));
969
+ recordGcResult("watchmanRoot", "failed", root.path, { detail: error.message });
970
+ }
971
+ }
972
+ const current = (await collectMemoryReport({
973
+ watchman: true,
974
+ gradle: false
975
+ }, true)).processes.find((entry) => entry.kind === "watchman");
976
+ if (!current || current.pid !== daemon.pid) return failures;
977
+ if (!current.reclaimable) {
978
+ keep(current, "watchman", current.detail ?? "it could not be proven unused");
979
+ return failures;
980
+ }
981
+ try {
982
+ phase("daemons", `shutting down watchman pid ${current.pid}: no client uses it`);
983
+ await boundWatchman(["shutdown-server"]);
984
+ console.log(chalk.green(`Shut down watchman pid ${current.pid}, freeing ${formatBytes(current.bytes)}; the next client that needs it starts it again`));
985
+ recordGcResult("watchman", "done", processLabel(current), {
986
+ id: String(current.pid),
987
+ bytes: current.bytes
988
+ });
989
+ } catch (error) {
990
+ failures++;
991
+ console.log(chalk.red(`Could not shut down watchman pid ${current.pid}: ${error.message}`));
992
+ recordGcResult("watchman", "failed", processLabel(current), {
993
+ id: String(current.pid),
994
+ bytes: current.bytes,
995
+ detail: error.message
996
+ });
997
+ }
998
+ return failures;
999
+ }
1000
+ function stopKotlin(entry, rows) {
1001
+ if (!sameProcess(entry, rows)) {
1002
+ keep(entry, "kotlinDaemon", "the process changed or exited since the check");
1003
+ return 0;
1004
+ }
1005
+ const error = signal(entry.pid);
1006
+ if (error) {
1007
+ console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
1008
+ recordGcResult("kotlinDaemon", "failed", processLabel(entry), {
1009
+ id: String(entry.pid),
1010
+ bytes: entry.bytes,
1011
+ detail: error
1012
+ });
1013
+ return 1;
1014
+ }
1015
+ console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
1016
+ recordGcResult("kotlinDaemon", "done", processLabel(entry), {
1017
+ id: String(entry.pid),
1018
+ bytes: entry.bytes
1019
+ });
1020
+ return 0;
1021
+ }
1022
+ /**
1023
+ * Stops the Gradle daemons a fresh check still finds idle. When every daemon `gradle --status` lists for a home and
1024
+ * version is one of them, the daemon's own `gradle --stop` runs for that home; otherwise each idle one gets SIGTERM,
1025
+ * because `--stop` also stops the busy daemons of that home and version.
1026
+ */
1027
+ async function stopGradle(idle, rows) {
1028
+ let failures = 0;
1029
+ const groups = /* @__PURE__ */ new Map();
1030
+ for (const entry of idle) {
1031
+ const command = parseGradleDaemonCommand(rows?.get(entry.pid)?.command ?? "");
1032
+ if (!command || !entry.gradleHome || !sameProcess(entry, rows)) {
1033
+ keep(entry, "gradleDaemon", "the process changed or exited since the check");
1034
+ continue;
1035
+ }
1036
+ const key = groupKey(command, entry.gradleHome);
1037
+ groups.set(key, [...groups.get(key) ?? [], entry]);
1038
+ }
1039
+ for (const [key, entries] of groups) {
1040
+ const [home, , distribution, javaHome] = JSON.parse(key);
1041
+ const command = parseGradleDaemonCommand(rows.get(entries[0].pid).command);
1042
+ const building = runningBuild(listBuildLocks(), listBuildSlots());
1043
+ if (building) {
1044
+ for (const entry of entries) keep(entry, "gradleDaemon", building);
1045
+ continue;
1046
+ }
1047
+ phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
1048
+ let states = await gradleStatus(command, home);
1049
+ let current = hostRows();
1050
+ const listed = states instanceof Map ? [...states].filter(([, state]) => state !== "stopped") : [];
1051
+ if (distribution !== null && listed.length > 0 && listed.every(([pid, state]) => state === "idle" && entries.some((entry) => entry.pid === pid))) try {
1052
+ phase("daemons", `gradle --stop for Gradle ${command.version} in ${home}`);
1053
+ await getExecutor().runFileAsync(join(distribution, "bin", "gradle"), [
1054
+ "--stop",
1055
+ "-q",
1056
+ "-g",
1057
+ home
1058
+ ], {
1059
+ cwd: home,
1060
+ env: { JAVA_HOME: javaHome },
1061
+ omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
1062
+ timeoutMs: GRADLE_STOP_TIMEOUT_MS
1063
+ });
1064
+ for (const entry of entries) {
1065
+ console.log(chalk.green(`Stopped ${processLabel(entry)} with gradle --stop, freeing ${formatBytes(entry.bytes)}`));
1066
+ recordGcResult("gradleDaemon", "done", processLabel(entry), {
1067
+ id: String(entry.pid),
1068
+ bytes: entry.bytes
1069
+ });
1070
+ }
1071
+ continue;
1072
+ } catch (error) {
1073
+ console.log(chalk.dim(`gradle --stop failed for ${home}; signalling each idle daemon: ${error.message.split("\n")[0]}`));
1074
+ states = await gradleStatus(command, home);
1075
+ current = hostRows();
1076
+ }
1077
+ for (const entry of entries) {
1078
+ const state = states instanceof Map ? states.get(entry.pid) : void 0;
1079
+ if (!sameProcess(entry, current)) {
1080
+ keep(entry, "gradleDaemon", "the process changed or exited since the check");
1081
+ continue;
1082
+ }
1083
+ if (state !== "idle") {
1084
+ keep(entry, "gradleDaemon", state ? `gradle --status now reports it ${state}` : "gradle --status no longer lists it idle");
1085
+ continue;
1086
+ }
1087
+ const error = signal(entry.pid);
1088
+ if (error) {
1089
+ failures++;
1090
+ console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
1091
+ recordGcResult("gradleDaemon", "failed", processLabel(entry), {
1092
+ id: String(entry.pid),
1093
+ bytes: entry.bytes,
1094
+ detail: error
1095
+ });
1096
+ continue;
1097
+ }
1098
+ console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
1099
+ recordGcResult("gradleDaemon", "done", processLabel(entry), {
1100
+ id: String(entry.pid),
1101
+ bytes: entry.bytes
1102
+ });
1103
+ }
1104
+ }
1105
+ return failures;
1106
+ }
1107
+ function stillPlanned(fresh, before, kind) {
1108
+ const planned = new Map(before.processes.filter((entry) => entry.kind === kind).map((entry) => [entry.pid, entry]));
1109
+ return fresh.processes.filter((entry) => entry.kind === kind && planned.get(entry.pid)?.startedAt === entry.startedAt);
1110
+ }
1111
+ function hostRows() {
1112
+ const rows = createDeviceProcessTables().host();
1113
+ return rows ? new Map(rows.map((row) => [row.pid, row])) : null;
1114
+ }
1115
+ const EXIT_WAIT_MS = 1e4;
1116
+ function alive(pid) {
1117
+ try {
1118
+ process.kill(pid, 0);
1119
+ return true;
1120
+ } catch (error) {
1121
+ return error.code !== "ESRCH";
1122
+ }
1123
+ }
1124
+ async function waitForExit(pids) {
1125
+ const running = pids.filter(alive);
1126
+ if (running.length === 0) return;
1127
+ phase("daemons", `waiting up to ${EXIT_WAIT_MS / 1e3}s for ${plural(running.length, "Gradle daemon")} to exit: pid ${running.join(", ")}`);
1128
+ const deadline = Date.now() + EXIT_WAIT_MS;
1129
+ while (running.some(alive) && Date.now() < deadline) await new Promise((done) => setTimeout(done, 200));
1130
+ }
1131
+ /** Stops the Gradle daemons first, then re-checks the Kotlin daemons, whose clients those Gradle daemons were. */
1132
+ async function reclaimDaemons(before) {
1133
+ const scope = {
1134
+ watchman: false,
1135
+ gradle: true
1136
+ };
1137
+ const gradle = stillPlanned(await collectMemoryReport(scope, true), before, "gradleDaemon");
1138
+ for (const entry of gradle.filter((e) => !e.reclaimable)) keep(entry, "gradleDaemon", entry.detail ?? "it could not be proven idle");
1139
+ const stopping = gradle.filter((entry) => entry.reclaimable);
1140
+ let failures = await stopGradle(stopping, hostRows());
1141
+ if (!before.processes.some((entry) => entry.kind === "kotlinDaemon")) return failures;
1142
+ await waitForExit(stopping.map((entry) => entry.pid));
1143
+ const kotlin = stillPlanned(await collectMemoryReport(scope, true), before, "kotlinDaemon");
1144
+ for (const entry of before.processes.filter((e) => e.kind === "kotlinDaemon" && e.reclaimable)) {
1145
+ if (kotlin.some((current) => current.pid === entry.pid) || alive(entry.pid)) continue;
1146
+ console.log(chalk.green(`${processLabel(entry)} exited with its Gradle daemon, freeing ${formatBytes(entry.bytes)}`));
1147
+ recordGcResult("kotlinDaemon", "done", processLabel(entry), {
1148
+ id: String(entry.pid),
1149
+ bytes: entry.bytes
1150
+ });
1151
+ }
1152
+ const rows = hostRows();
1153
+ for (const entry of kotlin) if (entry.reclaimable) failures += stopKotlin(entry, rows);
1154
+ else keep(entry, "kotlinDaemon", entry.detail ?? "it could not be proven idle");
1155
+ return failures;
1156
+ }
1157
+ /** Whether `gc --delete --cache <cache>` would act on a memory kind: stop a process or remove a watchman root. */
1158
+ function memoryWorkPending(cache, report) {
1159
+ if (!memoryCacheKind(cache) || !report) return false;
1160
+ return reclaimableBytes(report) > 0 || report.watchmanRoots.some((root) => root.removable);
1161
+ }
1162
+ /** Acts on the one memory kind `--cache` names: never on an unscoped run or `--cache all`. */
1163
+ async function reclaimMemory(cache, report) {
1164
+ const kind = memoryCacheKind(cache);
1165
+ if (!kind || !report || report.processes.length === 0) return 0;
1166
+ return kind === "watchman" ? reclaimWatchman(report) : reclaimDaemons(report);
1167
+ }
1168
+ /** A doctor note when watchman's footprint is over 2 GiB: only a restart shrinks it, and gc says when one is safe. */
1169
+ function watchmanFinding(report) {
1170
+ const daemon = report.processes.find((entry) => entry.kind === "watchman");
1171
+ if (!daemon || daemon.bytes < WATCHMAN_DOCTOR_BYTES) return null;
1172
+ const stale = report.watchmanRoots.filter((root) => root.stale !== null).length;
1173
+ const state = daemon.reclaimable ? "no client uses it now" : `it is kept: ${daemon.detail}`;
1174
+ return {
1175
+ level: "note",
1176
+ title: `watchman uses ${formatBytes(daemon.bytes)}`,
1177
+ detail: `The shared watchman daemon (pid ${daemon.pid}) has a ${formatBytes(daemon.bytes)} ${daemon.measure} with ${report.watchmanRoots.length} watched root(s), ${stale} of them stale; ${state}. Removing roots stops their recrawls, but only a restart returns the memory.`,
1178
+ fix: "Run `stim gc --cache watchman` to see its roots and clients, then `stim gc --delete --cache watchman` once no workspace uses it."
1179
+ };
1180
+ }
1181
+ async function inspectWatchmanMemory() {
1182
+ if (process.platform === "win32" || memorySweepIsScoped()) return null;
1183
+ return watchmanFinding(await collectMemoryReport({
1184
+ watchman: true,
1185
+ gradle: false
1186
+ }));
1187
+ }
1188
+ //#endregion
298
1189
  //#region src/commands/gc/caches.ts
299
1190
  const DAY_MS$2 = 864e5;
300
1191
  function cacheSweepIsScoped() {
@@ -303,8 +1194,8 @@ function cacheSweepIsScoped() {
303
1194
  function isInsideConfigDir(dir) {
304
1195
  const root = canonicalPath(getConfigDir());
305
1196
  const target = canonicalPath(dir);
306
- const rel = relative(root, target);
307
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
1197
+ const rel = relative$1(root, target);
1198
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
308
1199
  }
309
1200
  function machineGlobalReason(cache) {
310
1201
  if (!cacheSweepIsScoped()) return null;
@@ -314,43 +1205,63 @@ function machineGlobalReason(cache) {
314
1205
  }
315
1206
  const EVERY_CACHE = "all";
316
1207
  const WORKSPACE_OUTPUTS = "workspaces";
1208
+ const RECORDINGS = "recordings";
1209
+ const PARKED_DEVICES = "parked";
1210
+ function includesParkedDevices(name) {
1211
+ return name?.trim().toLowerCase() === PARKED_DEVICES;
1212
+ }
1213
+ function includesRecordings(name) {
1214
+ const wanted = name?.trim().toLowerCase();
1215
+ return !wanted || wanted === EVERY_CACHE || wanted === RECORDINGS;
1216
+ }
1217
+ function isWorkspacesScope(name) {
1218
+ return name?.trim().toLowerCase() === WORKSPACE_OUTPUTS;
1219
+ }
317
1220
  function includesWorkspaceOutputs(name) {
318
1221
  const wanted = name?.trim().toLowerCase();
319
1222
  return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
320
1223
  }
1224
+ /** Whether a --cache name selects something besides shared caches: recordings, parked devices or a memory kind. */
1225
+ function selectsBeyondCaches(name) {
1226
+ return includesRecordings(name) || includesParkedDevices(name) || memoryCacheKind(name) !== null;
1227
+ }
321
1228
  function selectCaches(caches, name) {
322
1229
  if (!name) return caches;
323
1230
  const wanted = name.trim().toLowerCase();
324
1231
  if (wanted === EVERY_CACHE) return caches;
325
- if (wanted === WORKSPACE_OUTPUTS) return [];
1232
+ if (wanted === WORKSPACE_OUTPUTS || wanted === RECORDINGS || wanted === PARKED_DEVICES || memoryCacheKind(wanted)) return [];
326
1233
  return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
327
1234
  }
1235
+ function emptyVerdict(c) {
1236
+ if (c.prune === "report-only") return {
1237
+ willEmpty: false,
1238
+ emptySkipped: "report-only shared cache; Stim never deletes it"
1239
+ };
1240
+ if (c.machineGlobal) return {
1241
+ willEmpty: false,
1242
+ emptySkipped: c.machineGlobal
1243
+ };
1244
+ if (!ownsItsDirectory(c)) return {
1245
+ willEmpty: false,
1246
+ emptySkipped: `${c.dir} is not a directory this cache owns`
1247
+ };
1248
+ return {
1249
+ willEmpty: true,
1250
+ emptySkipped: null
1251
+ };
1252
+ }
328
1253
  function planCacheEmptying(caches, all) {
329
1254
  const annotated = caches.map((c) => Object.assign({}, c, { machineGlobal: machineGlobalReason(c) }));
330
- if (!all) return annotated;
331
- return annotated.map((c) => {
332
- if (c.prune === "report-only") return Object.assign({}, c, {
333
- willEmpty: false,
334
- emptySkipped: "report-only shared cache; Stim never deletes it"
335
- });
336
- if (c.machineGlobal) return Object.assign({}, c, {
337
- willEmpty: false,
338
- emptySkipped: c.machineGlobal
339
- });
340
- if (!ownsItsDirectory(c)) return Object.assign({}, c, {
341
- willEmpty: false,
342
- emptySkipped: `${c.dir} is not a directory this cache owns`
343
- });
344
- return Object.assign({}, c, {
345
- willEmpty: true,
346
- emptySkipped: null
347
- });
348
- });
1255
+ return all ? annotated.map((c) => Object.assign({}, c, emptyVerdict(c))) : annotated;
1256
+ }
1257
+ /** What `gc --delete --cache <name>` would do to each cache, for a report that does not run that scope. */
1258
+ function previewScopedEmptying(caches) {
1259
+ return planCacheEmptying(caches, false).map((c) => Object.assign({}, c, { scopedEmpty: emptyVerdict(c) }));
349
1260
  }
350
1261
  function ownsItsDirectory(cache) {
351
1262
  if (Array.isArray(cache.files)) return false;
352
1263
  const dir = canonicalPath(cache.dir);
353
- if (dirname(dir) === dir) return false;
1264
+ if (dirname$1(dir) === dir) return false;
354
1265
  return ![
355
1266
  homedir(),
356
1267
  tmpdir(),
@@ -380,7 +1291,7 @@ function emptyCache(cache) {
380
1291
  let removed = 0;
381
1292
  let failed = 0;
382
1293
  for (const name of names) try {
383
- rmSync$1(join(cache.dir, name), {
1294
+ rmSync$1(join$1(cache.dir, name), {
384
1295
  recursive: true,
385
1296
  force: true
386
1297
  });
@@ -570,7 +1481,7 @@ function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays,
570
1481
  const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
571
1482
  const stale = [];
572
1483
  for (const [path, proj] of Object.entries(config?.projects || {})) {
573
- if (dead.has(path) || !isAbsolute(path)) continue;
1484
+ if (dead.has(path) || !isAbsolute$1(path)) continue;
574
1485
  const touched = lastTouched(path);
575
1486
  if (!Number.isFinite(touched) || touched >= cutoff) continue;
576
1487
  const idleDays = Math.floor((now - touched) / DAY_MS$1);
@@ -658,6 +1569,7 @@ function describeParkedSims(records, sims, deviceTypes, { simsChecked = true } =
658
1569
  model: deviceTypes.find((d) => d.identifier === record.deviceTypeIdentifier)?.name ?? null,
659
1570
  runtime: sim ? parseRuntimeVersion(sim.runtime) : parseRuntimeVersion(record.runtimeIdentifier),
660
1571
  parkedAt: record.parkedAt,
1572
+ app: record.bundleId ?? null,
661
1573
  bytes: typeof sim?.dataPathSize === "number" ? sim.dataPathSize : null,
662
1574
  listed: simsChecked ? Boolean(sim) : null
663
1575
  };
@@ -713,11 +1625,58 @@ function collectParkedAvds(deps, age = {}) {
713
1625
  systemImage: record.systemImage,
714
1626
  deviceProfile: poolConfigurationDeviceProfile(record.configuration),
715
1627
  parkedAt: record.parkedAt,
1628
+ app: record.packageName ?? null,
716
1629
  bytes,
717
1630
  listed: avds === null ? null : avds.includes(record.name)
718
1631
  };
719
1632
  });
720
1633
  }
1634
+ function eraseParkedDevices(sims, avds) {
1635
+ let failures = 0;
1636
+ const devices = [...sims.map((sim) => ({
1637
+ platform: "ios",
1638
+ id: sim.udid,
1639
+ name: sim.name,
1640
+ listed: sim.listed,
1641
+ bytes: sim.bytes
1642
+ })), ...avds.map((avd) => ({
1643
+ platform: "android",
1644
+ id: avd.name,
1645
+ name: avd.name,
1646
+ listed: avd.listed,
1647
+ bytes: avd.bytes
1648
+ }))];
1649
+ for (const device of devices) {
1650
+ const what = `parked ${device.platform === "ios" ? "ios sim" : "android emulator"} ${device.name}`;
1651
+ if (device.listed !== true) {
1652
+ const unverified = device.listed === null;
1653
+ const detail = unverified ? "Stim could not list devices to verify it; it was kept. Retry stim gc --cache parked --delete." : "it is not on this machine; stim gc --delete drops its pool record.";
1654
+ if (unverified) failures++;
1655
+ console.log((unverified ? chalk.red : chalk.dim)(`Did not erase ${what}: ${detail}`));
1656
+ recordGcResult("parkedDevice", unverified ? "failed" : "kept", device.name, {
1657
+ id: device.id,
1658
+ detail
1659
+ });
1660
+ continue;
1661
+ }
1662
+ const result = device.platform === "ios" ? eraseParkedIosSim(device.id, { label: device.name }) : eraseParkedAvd(device.id);
1663
+ if (result.status === "torn-down") {
1664
+ console.log(chalk.green(`Erased ${what}; it stays parked`));
1665
+ recordGcResult("parkedDevice", "done", device.name, {
1666
+ id: device.id,
1667
+ bytes: device.bytes
1668
+ });
1669
+ } else if (result.status === "failed") {
1670
+ failures++;
1671
+ console.log(chalk.red(`Failed to erase ${what}: ${result.reason}`));
1672
+ recordGcResult("parkedDevice", "failed", device.name, {
1673
+ id: device.id,
1674
+ detail: result.reason
1675
+ });
1676
+ } else console.log(chalk.dim(`Skipped ${device.name}; it is no longer parked.`));
1677
+ }
1678
+ return failures;
1679
+ }
721
1680
  function deleteParkedAvds(avds) {
722
1681
  let failures = 0;
723
1682
  for (const avd of avds) {
@@ -894,16 +1853,16 @@ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords)
894
1853
  //#endregion
895
1854
  //#region src/commands/gc/workspaces.ts
896
1855
  function workspacesRoot() {
897
- return join(getConfigDir(), "workspaces");
1856
+ return join$1(getConfigDir(), "workspaces");
898
1857
  }
899
1858
  function isInsideWorkspaces(dir) {
900
- const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
901
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
1859
+ const rel = relative$1(canonicalPath(workspacesRoot()), canonicalPath(dir));
1860
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
902
1861
  }
903
1862
  function readRecordedRoot(dir, name) {
904
1863
  let parsed;
905
1864
  try {
906
- parsed = readJsonFile(join(dir, "workspace.json"));
1865
+ parsed = readJsonFile(join$1(dir, "workspace.json"));
907
1866
  } catch (error) {
908
1867
  return {
909
1868
  projectRoot: null,
@@ -911,7 +1870,7 @@ function readRecordedRoot(dir, name) {
911
1870
  };
912
1871
  }
913
1872
  const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
914
- if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
1873
+ if (typeof projectRoot !== "string" || !isAbsolute$1(projectRoot)) return {
915
1874
  projectRoot: null,
916
1875
  problem: "its workspace.json records no absolute project root"
917
1876
  };
@@ -934,7 +1893,7 @@ function listWorkspaceDirs() {
934
1893
  }
935
1894
  const entries = [];
936
1895
  for (const name of names) {
937
- const dir = join(root, name);
1896
+ const dir = join$1(root, name);
938
1897
  try {
939
1898
  if (!statSync$1(dir).isDirectory()) continue;
940
1899
  } catch {
@@ -1002,7 +1961,7 @@ function rootPresence(path) {
1002
1961
  return code === "ENOENT" || code === "ENOTDIR" ? false : null;
1003
1962
  }
1004
1963
  }
1005
- function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
1964
+ function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true } = {}) {
1006
1965
  const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
1007
1966
  registryKeys,
1008
1967
  exists: rootPresence,
@@ -1010,11 +1969,11 @@ function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
1010
1969
  inUse: (root) => workspaceInUse(root)
1011
1970
  });
1012
1971
  return {
1013
- orphaned: classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })),
1972
+ orphaned: measure ? classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })) : classified.orphaned,
1014
1973
  skipped: classified.skipped
1015
1974
  };
1016
1975
  }
1017
- const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift.";
1976
+ const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
1018
1977
  const WORKSPACE_OUTPUT_DIRS = [
1019
1978
  "derived-data",
1020
1979
  "gradle-build",
@@ -1042,7 +2001,13 @@ function sizeOf(dir) {
1042
2001
  return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
1043
2002
  }
1044
2003
  function outputPaths(dir) {
1045
- return WORKSPACE_OUTPUT_DIRS.map((name) => join(dir, name)).filter((path) => existsSync$1(path));
2004
+ const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
2005
+ const macos = join$1(dir, "macos");
2006
+ try {
2007
+ if (!lstatSync$1(macos).isDirectory()) return paths;
2008
+ for (const entry of readdirSync$1(macos, { withFileTypes: true })) if (entry.name === "build" || entry.name.endsWith(".app") || entry.name.startsWith("staging-") && entry.isDirectory()) paths.push(join$1(macos, entry.name));
2009
+ } catch {}
2010
+ return paths;
1046
2011
  }
1047
2012
  function outputBytes(paths) {
1048
2013
  let total = 0;
@@ -1096,7 +2061,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1096
2061
  now
1097
2062
  });
1098
2063
  if (again?.keptReason) return again.keptReason;
1099
- for (const name of WORKSPACE_OUTPUT_DIRS) rmSync$1(join(entry.dir, name), {
2064
+ for (const path of outputPaths(entry.dir)) rmSync$1(path, {
1100
2065
  recursive: true,
1101
2066
  force: true
1102
2067
  });
@@ -1132,7 +2097,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1132
2097
  };
1133
2098
  }
1134
2099
  function stillOrphaned({ dir, projectRoot }) {
1135
- if (readRecordedRoot(dir, basename(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
2100
+ if (readRecordedRoot(dir, basename$1(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
1136
2101
  return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
1137
2102
  }
1138
2103
  async function deleteOrphanedWorkspaces(orphaned) {
@@ -1248,7 +2213,7 @@ function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemory
1248
2213
  }
1249
2214
  function existingAncestor(path) {
1250
2215
  let current = path;
1251
- while (!existsSync(current) && dirname$1(current) !== current) current = dirname$1(current);
2216
+ while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
1252
2217
  return current;
1253
2218
  }
1254
2219
  function readVolumeSpace(paths) {
@@ -1548,14 +2513,14 @@ function spaceUsers() {
1548
2513
  const candidates = [
1549
2514
  ["Stim state", getConfigDir()],
1550
2515
  ["shared build cache", sharedBuildCache()],
1551
- ...platform() === "darwin" ? [["simulators", join$1(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join$1(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
1552
- ["Android emulators", join$1(home, ".android", "avd")]
2516
+ ...platform() === "darwin" ? [["simulators", join(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
2517
+ ["Android emulators", join(home, ".android", "avd")]
1553
2518
  ];
1554
2519
  const measured = /* @__PURE__ */ new Set();
1555
2520
  const users = [];
1556
2521
  for (const [label, path] of candidates) {
1557
2522
  const canonical = canonicalPath(path);
1558
- if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep$1}`))) continue;
2523
+ if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep}`))) continue;
1559
2524
  measured.add(canonical);
1560
2525
  if (!existsSync(path)) continue;
1561
2526
  const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
@@ -1742,4 +2707,4 @@ function budgetLine(report) {
1742
2707
  return `${report.volumes.length ? `${report.volumes.map((v) => `${v.freeGb} GB free on ${v.volume}`).join(", ")} (reclaims below ${Math.max(budget.minFreeDiskGb, budget.hardFloorDiskGb)} GB, ${budget.hardFloorDiskGb ? `refuses below ${budget.hardFloorDiskGb} GB` : "never refuses"})` : "disk budget off"}; ${report.memory ? `roughly ${report.memory.committedGb} GB of a ${budget.maxCommittedMemoryGb} GB memory budget committed` : "memory budget off"}`;
1743
2708
  }
1744
2709
  //#endregion
1745
- export { includesWorkspaceOutputs as A, deviceSweepIsScoped as C, unverifiedDeviceCommand as D, findStaleProjectDevices as E, sizeCaches as F, stopOwnedMetroForReset as I, selectCaches as M, trimCaches as N, withAndroidAvdSizes as O, discoverCaches as P, describeUnverifiableDevices as S, findStaleDeviceRecords as T, collectParkedSims as _, REBUILD_COST as a, deleteProjectDevices as b, collectOrphanedWorkspaces as c, isInsideWorkspaces as d, listWorkspaceDirs as f, collectParkedAvds as g, collectDeviceLeases as h, machineNumber as i, planCacheEmptying as j, emptyCaches as k, collectWorkspaceOutputs as l, DEVICE_LIST_TIMEOUT_MS$1 as m, budgetLine as n, WORKSPACE_OUTPUT_DIRS as o, sizeText as p, inspectBudget as r, clearWorkspaceOutputs as s, budgetGate as t, deleteOrphanedWorkspaces as u, deleteParkedAvds as v, findOrphanedDevices as w, describeParkedSims as x, deleteParkedSims as y };
2710
+ export { discoverCaches as $, withAndroidAvdSizes as A, trimCaches as B, describeUnverifiableDevices as C, findStaleDeviceRecords as D, findOrphanedDevices as E, isWorkspacesScope as F, memoryCacheKind as G, WATCHMAN_KIND as H, planCacheEmptying as I, memorySweepIsScoped as J, memoryLines as K, previewScopedEmptying as L, includesParkedDevices as M, includesRecordings as N, findStaleProjectDevices as O, includesWorkspaceOutputs as P, phase as Q, selectCaches as R, describeParkedSims as S, eraseParkedDevices as T, collectMemoryReport as U, GRADLE_DAEMONS_KIND as V, inspectWatchmanMemory as W, reclaimMemory as X, memoryWorkPending as Y, scopedMemoryReport as Z, collectParkedAvds as _, REBUILD_COST as a, deleteParkedSims as b, collectOrphanedWorkspaces as c, isInsideWorkspaces as d, sizeCaches as et, listWorkspaceDirs as f, collectDeviceLeases as g, DEVICE_LIST_TIMEOUT_MS$1 as h, machineNumber as i, emptyCaches as j, unverifiedDeviceCommand as k, collectWorkspaceOutputs as l, sizeText as m, budgetLine as n, WORKSPACE_OUTPUT_DIRS as o, rootPresence as p, memoryScope as q, inspectBudget as r, clearWorkspaceOutputs as s, budgetGate as t, stopOwnedMetroForReset as tt, deleteOrphanedWorkspaces as u, collectParkedSims as v, deviceSweepIsScoped as w, deleteProjectDevices as x, deleteParkedAvds as y, selectsBeyondCaches as z };