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,29 +1,35 @@
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-0jCcMM0E.mjs";
2
- import { i as registeredCaches } from "./cache-manifest-DWPF4RKJ.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-BPeY1YMh.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-DwAU2_-D.mjs";
6
- import { a as withWorkspaceProcessLock, o as workspaceProcessLockError } from "./named-ports-n7tnJsI8.mjs";
7
- import { l as phaseLine, u as plural } from "./command-output-BIL7rlX7.mjs";
8
- import { i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-CL9dZf73.mjs";
9
- import { o as workspaceActivity } from "./activity-DfVKZTEB.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-qx_31wJo.mjs";
11
- import { f as teardownOwnedAvd, g as teardownParkedIosSim, h as teardownParkedAvd, m as teardownOwnedIosSim, p as teardownOwnedBrowser, t as resolveSupervisorTarget } from "./ownership-PUP4Dwai.mjs";
12
- import { d as listIosDeviceTypes, g as parseRuntimeVersion, l as listAllIosSims } from "./ios-CmEzZiOj.mjs";
13
- import { c as recordGcResult, i as idleShutdownCandidates, n as collectOwnedDeviceActivity, o as shutDownIdleDevices, s as workspaceBuildInProgress, t as collectIdleDevices } from "./idle-DxLi5OkN.mjs";
14
- import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-wYEaKQM9.mjs";
15
- import { c as formatSpace, r as capacity, s as environmentState } from "./status-sszP4XKU.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
26
- async function stopOwnedMetro(root) {
32
+ async function stopOwnedMetro(root, requester) {
27
33
  const project = getProject(root);
28
34
  const port = project?.metroPort;
29
35
  const target = resolveSupervisorTarget({
@@ -44,7 +50,10 @@ async function stopOwnedMetro(root) {
44
50
  pid: target.pid,
45
51
  processToken: target.processToken
46
52
  };
47
- if (!killMetroTree(target.pid, target.processToken) || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
53
+ requestDevServerStop(root, target.processToken, requester);
54
+ const signalled = killMetroTree(target.pid, target.processToken);
55
+ if (!signalled) withdrawDevServerStopRequest(root, target.processToken);
56
+ if (!signalled || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
48
57
  status: "stuck",
49
58
  reason: `supervisor pid ${target.pid} did not exit`
50
59
  };
@@ -57,7 +66,7 @@ async function stopOwnedMetro(root) {
57
66
  return { status: "stopped" };
58
67
  }
59
68
  async function stopOwnedMetroForReset(root) {
60
- const stopped = await stopOwnedMetro(root);
69
+ const stopped = await stopOwnedMetro(root, { by: "stim start --reset-cache" });
61
70
  switch (stopped.status) {
62
71
  case "unverified": throw supervisorError("STIM_SUPERVISOR_EXITED", `Cannot reset Metro: ${stopped.reason}.`, "Stop it with the tool that started it, then retry. Stim leaves unverified processes alone.");
63
72
  case "external": throw supervisorError("STIM_SUPERVISOR_EXITED", "Cannot reset an externally started dev server.", "Stop it with the tool that started it, then retry `stim start --reset-cache`.");
@@ -68,10 +77,10 @@ async function stopOwnedMetroForReset(root) {
68
77
  //#endregion
69
78
  //#region src/cache/caches.ts
70
79
  function xcodeDerivedDataRoot() {
71
- return join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
80
+ return join$1(homedir(), "Library", "Developer", "Xcode", "DerivedData");
72
81
  }
73
82
  function compilationCache() {
74
- const dir = join(xcodeDerivedDataRoot(), "CompilationCache.noindex");
83
+ const dir = join$1(xcodeDerivedDataRoot(), "CompilationCache.noindex");
75
84
  if (!existsSync$1(dir)) return null;
76
85
  return {
77
86
  name: "Xcode compilation cache",
@@ -81,8 +90,8 @@ function compilationCache() {
81
90
  };
82
91
  }
83
92
  function gradleBuildCache() {
84
- const gradleHome = process.env.GRADLE_USER_HOME || join(homedir(), ".gradle");
85
- 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");
86
95
  if (!existsSync$1(dir)) return null;
87
96
  return {
88
97
  name: "Gradle build cache",
@@ -104,9 +113,9 @@ function metroFileMaps() {
104
113
  const files = [];
105
114
  let bytes = 0;
106
115
  for (const n of names) try {
107
- const st = statSync$1(join(root, n));
116
+ const st = statSync$1(join$1(root, n));
108
117
  if (uid !== void 0 && st.uid !== uid) continue;
109
- files.push(join(root, n));
118
+ files.push(join$1(root, n));
110
119
  bytes += st.size;
111
120
  } catch {}
112
121
  if (!files.length) return null;
@@ -133,7 +142,15 @@ function discoverCaches() {
133
142
  gradle,
134
143
  metroFileMaps()
135
144
  ].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
136
- 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}` }));
137
154
  }
138
155
  function suppressLegacyMetroAncestors(caches) {
139
156
  const current = caches.filter(isCurrentMetroStore);
@@ -165,7 +182,7 @@ function isMetroStore(cache) {
165
182
  function isNamedMetroChild(parent, candidate) {
166
183
  const dir = canonicalCacheDir(parent);
167
184
  const child = canonicalCacheDir(candidate);
168
- return dirname(child) === dir || canonicalCacheDir(dirname(candidate)) === dir;
185
+ return dirname$1(child) === dir || canonicalCacheDir(dirname$1(candidate)) === dir;
169
186
  }
170
187
  function mergeCacheDescriptors(caches, gradleDir) {
171
188
  const merged = [];
@@ -216,14 +233,14 @@ function cachePathsOverlap(left, right) {
216
233
  return cachePathContains(left, right) || cachePathContains(right, left);
217
234
  }
218
235
  function cachePathContains(parent, child) {
219
- const rel = relative(parent, child);
220
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
236
+ const rel = relative$1(parent, child);
237
+ return rel === "" || !rel.startsWith("..") && !isAbsolute$1(rel);
221
238
  }
222
239
  function canonicalCacheDir(dir) {
223
240
  try {
224
241
  return realpathSync$1(dir);
225
242
  } catch {
226
- return resolve(dir);
243
+ return resolve$1(dir);
227
244
  }
228
245
  }
229
246
  function sizeCaches(caches) {
@@ -285,13 +302,890 @@ function entriesAtDepth(dir, depth) {
285
302
  } catch {
286
303
  continue;
287
304
  }
288
- for (const name of names) next.push(join(parent, name));
305
+ for (const name of names) next.push(join$1(parent, name));
289
306
  }
290
307
  level = next;
291
308
  }
292
309
  return level;
293
310
  }
294
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
295
1189
  //#region src/commands/gc/caches.ts
296
1190
  const DAY_MS$2 = 864e5;
297
1191
  function cacheSweepIsScoped() {
@@ -300,8 +1194,8 @@ function cacheSweepIsScoped() {
300
1194
  function isInsideConfigDir(dir) {
301
1195
  const root = canonicalPath(getConfigDir());
302
1196
  const target = canonicalPath(dir);
303
- const rel = relative(root, target);
304
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
1197
+ const rel = relative$1(root, target);
1198
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
305
1199
  }
306
1200
  function machineGlobalReason(cache) {
307
1201
  if (!cacheSweepIsScoped()) return null;
@@ -311,43 +1205,63 @@ function machineGlobalReason(cache) {
311
1205
  }
312
1206
  const EVERY_CACHE = "all";
313
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
+ }
314
1220
  function includesWorkspaceOutputs(name) {
315
1221
  const wanted = name?.trim().toLowerCase();
316
1222
  return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
317
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
+ }
318
1228
  function selectCaches(caches, name) {
319
1229
  if (!name) return caches;
320
1230
  const wanted = name.trim().toLowerCase();
321
1231
  if (wanted === EVERY_CACHE) return caches;
322
- if (wanted === WORKSPACE_OUTPUTS) return [];
1232
+ if (wanted === WORKSPACE_OUTPUTS || wanted === RECORDINGS || wanted === PARKED_DEVICES || memoryCacheKind(wanted)) return [];
323
1233
  return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
324
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
+ }
325
1253
  function planCacheEmptying(caches, all) {
326
1254
  const annotated = caches.map((c) => Object.assign({}, c, { machineGlobal: machineGlobalReason(c) }));
327
- if (!all) return annotated;
328
- return annotated.map((c) => {
329
- if (c.prune === "report-only") return Object.assign({}, c, {
330
- willEmpty: false,
331
- emptySkipped: "report-only shared cache; Stim never deletes it"
332
- });
333
- if (c.machineGlobal) return Object.assign({}, c, {
334
- willEmpty: false,
335
- emptySkipped: c.machineGlobal
336
- });
337
- if (!ownsItsDirectory(c)) return Object.assign({}, c, {
338
- willEmpty: false,
339
- emptySkipped: `${c.dir} is not a directory this cache owns`
340
- });
341
- return Object.assign({}, c, {
342
- willEmpty: true,
343
- emptySkipped: null
344
- });
345
- });
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) }));
346
1260
  }
347
1261
  function ownsItsDirectory(cache) {
348
1262
  if (Array.isArray(cache.files)) return false;
349
1263
  const dir = canonicalPath(cache.dir);
350
- if (dirname(dir) === dir) return false;
1264
+ if (dirname$1(dir) === dir) return false;
351
1265
  return ![
352
1266
  homedir(),
353
1267
  tmpdir(),
@@ -377,7 +1291,7 @@ function emptyCache(cache) {
377
1291
  let removed = 0;
378
1292
  let failed = 0;
379
1293
  for (const name of names) try {
380
- rmSync$1(join(cache.dir, name), {
1294
+ rmSync$1(join$1(cache.dir, name), {
381
1295
  recursive: true,
382
1296
  force: true
383
1297
  });
@@ -567,7 +1481,7 @@ function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays,
567
1481
  const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
568
1482
  const stale = [];
569
1483
  for (const [path, proj] of Object.entries(config?.projects || {})) {
570
- if (dead.has(path) || !isAbsolute(path)) continue;
1484
+ if (dead.has(path) || !isAbsolute$1(path)) continue;
571
1485
  const touched = lastTouched(path);
572
1486
  if (!Number.isFinite(touched) || touched >= cutoff) continue;
573
1487
  const idleDays = Math.floor((now - touched) / DAY_MS$1);
@@ -655,6 +1569,7 @@ function describeParkedSims(records, sims, deviceTypes, { simsChecked = true } =
655
1569
  model: deviceTypes.find((d) => d.identifier === record.deviceTypeIdentifier)?.name ?? null,
656
1570
  runtime: sim ? parseRuntimeVersion(sim.runtime) : parseRuntimeVersion(record.runtimeIdentifier),
657
1571
  parkedAt: record.parkedAt,
1572
+ app: record.bundleId ?? null,
658
1573
  bytes: typeof sim?.dataPathSize === "number" ? sim.dataPathSize : null,
659
1574
  listed: simsChecked ? Boolean(sim) : null
660
1575
  };
@@ -710,11 +1625,58 @@ function collectParkedAvds(deps, age = {}) {
710
1625
  systemImage: record.systemImage,
711
1626
  deviceProfile: poolConfigurationDeviceProfile(record.configuration),
712
1627
  parkedAt: record.parkedAt,
1628
+ app: record.packageName ?? null,
713
1629
  bytes,
714
1630
  listed: avds === null ? null : avds.includes(record.name)
715
1631
  };
716
1632
  });
717
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
+ }
718
1680
  function deleteParkedAvds(avds) {
719
1681
  let failures = 0;
720
1682
  for (const avd of avds) {
@@ -891,16 +1853,16 @@ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords)
891
1853
  //#endregion
892
1854
  //#region src/commands/gc/workspaces.ts
893
1855
  function workspacesRoot() {
894
- return join(getConfigDir(), "workspaces");
1856
+ return join$1(getConfigDir(), "workspaces");
895
1857
  }
896
1858
  function isInsideWorkspaces(dir) {
897
- const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
898
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
1859
+ const rel = relative$1(canonicalPath(workspacesRoot()), canonicalPath(dir));
1860
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
899
1861
  }
900
1862
  function readRecordedRoot(dir, name) {
901
1863
  let parsed;
902
1864
  try {
903
- parsed = readJsonFile(join(dir, "workspace.json"));
1865
+ parsed = readJsonFile(join$1(dir, "workspace.json"));
904
1866
  } catch (error) {
905
1867
  return {
906
1868
  projectRoot: null,
@@ -908,7 +1870,7 @@ function readRecordedRoot(dir, name) {
908
1870
  };
909
1871
  }
910
1872
  const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
911
- if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
1873
+ if (typeof projectRoot !== "string" || !isAbsolute$1(projectRoot)) return {
912
1874
  projectRoot: null,
913
1875
  problem: "its workspace.json records no absolute project root"
914
1876
  };
@@ -931,7 +1893,7 @@ function listWorkspaceDirs() {
931
1893
  }
932
1894
  const entries = [];
933
1895
  for (const name of names) {
934
- const dir = join(root, name);
1896
+ const dir = join$1(root, name);
935
1897
  try {
936
1898
  if (!statSync$1(dir).isDirectory()) continue;
937
1899
  } catch {
@@ -999,7 +1961,7 @@ function rootPresence(path) {
999
1961
  return code === "ENOENT" || code === "ENOTDIR" ? false : null;
1000
1962
  }
1001
1963
  }
1002
- function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
1964
+ function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true } = {}) {
1003
1965
  const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
1004
1966
  registryKeys,
1005
1967
  exists: rootPresence,
@@ -1007,11 +1969,11 @@ function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
1007
1969
  inUse: (root) => workspaceInUse(root)
1008
1970
  });
1009
1971
  return {
1010
- 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,
1011
1973
  skipped: classified.skipped
1012
1974
  };
1013
1975
  }
1014
- 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.";
1015
1977
  const WORKSPACE_OUTPUT_DIRS = [
1016
1978
  "derived-data",
1017
1979
  "gradle-build",
@@ -1039,7 +2001,13 @@ function sizeOf(dir) {
1039
2001
  return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
1040
2002
  }
1041
2003
  function outputPaths(dir) {
1042
- 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;
1043
2011
  }
1044
2012
  function outputBytes(paths) {
1045
2013
  let total = 0;
@@ -1093,7 +2061,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1093
2061
  now
1094
2062
  });
1095
2063
  if (again?.keptReason) return again.keptReason;
1096
- for (const name of WORKSPACE_OUTPUT_DIRS) rmSync$1(join(entry.dir, name), {
2064
+ for (const path of outputPaths(entry.dir)) rmSync$1(path, {
1097
2065
  recursive: true,
1098
2066
  force: true
1099
2067
  });
@@ -1129,7 +2097,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1129
2097
  };
1130
2098
  }
1131
2099
  function stillOrphaned({ dir, projectRoot }) {
1132
- 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;
1133
2101
  return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
1134
2102
  }
1135
2103
  async function deleteOrphanedWorkspaces(orphaned) {
@@ -1245,7 +2213,7 @@ function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemory
1245
2213
  }
1246
2214
  function existingAncestor(path) {
1247
2215
  let current = path;
1248
- while (!existsSync(current) && dirname$1(current) !== current) current = dirname$1(current);
2216
+ while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
1249
2217
  return current;
1250
2218
  }
1251
2219
  function readVolumeSpace(paths) {
@@ -1413,7 +2381,10 @@ async function reclaimIdleDevServers({ root, dryRun, note, now }) {
1413
2381
  try {
1414
2382
  run = await withIdleWorkspace(path, async () => {
1415
2383
  try {
1416
- return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path) : "busy", {
2384
+ return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path, {
2385
+ by: "budget reclaim",
2386
+ workspace: root
2387
+ }) : "busy", {
1417
2388
  external: true,
1418
2389
  waitMs: 0,
1419
2390
  ownerPurpose: "budget reclaim"
@@ -1542,14 +2513,14 @@ function spaceUsers() {
1542
2513
  const candidates = [
1543
2514
  ["Stim state", getConfigDir()],
1544
2515
  ["shared build cache", sharedBuildCache()],
1545
- ...platform() === "darwin" ? [["simulators", join$1(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join$1(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
1546
- ["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")]
1547
2518
  ];
1548
2519
  const measured = /* @__PURE__ */ new Set();
1549
2520
  const users = [];
1550
2521
  for (const [label, path] of candidates) {
1551
2522
  const canonical = canonicalPath(path);
1552
- 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;
1553
2524
  measured.add(canonical);
1554
2525
  if (!existsSync(path)) continue;
1555
2526
  const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
@@ -1736,4 +2707,4 @@ function budgetLine(report) {
1736
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"}`;
1737
2708
  }
1738
2709
  //#endregion
1739
- 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 };