stim 1.9.0 → 1.10.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 (83) hide show
  1. package/dist/{android-_y4Ohi5z.mjs → android-787HhP5w.mjs} +119 -29
  2. package/dist/{android-BPGizezh.mjs → android-DBkeHwpm.mjs} +168 -95
  3. package/dist/{android-DEgo4oCo.mjs → android-DPov-fv2.mjs} +2 -2
  4. package/dist/{android-cas-eiExNLf7.mjs → android-cas-CITmIeWY.mjs} +9 -8
  5. package/dist/android-cas-compiler.mjs +1 -1
  6. package/dist/{app-install-Uf3a83Tv.mjs → app-install-CrJ6t_bv.mjs} +2 -2
  7. package/dist/budget-BP4jZZL4.mjs +620 -0
  8. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  9. package/dist/{cache-manifest-Dg7OnMrp.mjs → cache-manifest-Pubfp8wV.mjs} +1 -1
  10. package/dist/cache-manifest.mjs +1 -1
  11. package/dist/cli.mjs +16 -16
  12. package/dist/collector-run.d.mts +5 -1
  13. package/dist/collector-run.mjs +7 -7
  14. package/dist/{command-output-gByVmIjy.mjs → command-output-D5z2_bcF.mjs} +1 -1
  15. package/dist/{config-Cp9lM3wX.mjs → config-BEq0Qw_a.mjs} +14 -131
  16. package/dist/{dependency-state-Bq5DcLGn.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  17. package/dist/{deps-Cip5vUm1.mjs → deps-DP4eWqea.mjs} +3 -3
  18. package/dist/{detached-entry-KEu0_AXL.mjs → detached-entry-DJHoca49.mjs} +1 -1
  19. package/dist/{dev-client-HyqISM0h.mjs → dev-client-92U593KT.mjs} +137 -250
  20. package/dist/{device-D4U-xLty.mjs → device-DhTZe7L4.mjs} +7 -7
  21. package/dist/{build-lock-DPXPLgTc.mjs → device-lease-CbEaIk7b.mjs} +370 -12
  22. package/dist/{device-pool-CnTAz8X1.mjs → device-pool-Di8Q-9Ro.mjs} +4 -4
  23. package/dist/{device-remote-AQ5rQPqr.mjs → device-remote-Dy1jGj3U.mjs} +547 -468
  24. package/dist/{doctor-B_Ei3bZu.mjs → doctor-B-inAJmt.mjs} +58 -22
  25. package/dist/{error-diagnostics-Vfz83GCF.mjs → error-diagnostics-xxWKNO3-.mjs} +11 -12
  26. package/dist/{exec-jIAlgilk.mjs → exec-DK8KaVHK.mjs} +3 -2
  27. package/dist/{gc-Ckw8RQHi.mjs → gc-ChPFZMRC.mjs} +500 -1114
  28. package/dist/{guide-BiAbhs1J.mjs → guide-DwmaD_Wa.mjs} +882 -528
  29. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  30. package/dist/idle-DpRLp5lj.mjs +157 -0
  31. package/dist/in-use-CjUluqIX.mjs +245 -0
  32. package/dist/{ios-BL2NFckr.mjs → ios-B2Fpqvlb.mjs} +25 -59
  33. package/dist/{ios-BzUJWk1K.mjs → ios-Dd-hYJBh.mjs} +130 -89
  34. package/dist/{ios-device-DPsSm0v_.mjs → ios-device-BiOsSVzd.mjs} +1 -1
  35. package/dist/{ios-device-D972i9wf.mjs → ios-device-CtRdEVjx.mjs} +3 -3
  36. package/dist/{launch-verify-Q_OtW1Rf.mjs → launch-verify-CQS_Kg-C.mjs} +5 -5
  37. package/dist/{logs-CI9NQSK7.mjs → logs-MARIy6m9.mjs} +9 -9
  38. package/dist/{metro-DrLHaMn2.mjs → metro-BWLOJ_Wr.mjs} +116 -10
  39. package/dist/{named-ports-CmLQSIRj.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  40. package/dist/{ndjson-9uFNRwfe.mjs → ndjson-CDY9IkVC.mjs} +3 -56
  41. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  42. package/dist/{ownership-D15Op8vN.mjs → ownership-D2xbLVoz.mjs} +2 -3
  43. package/dist/ownership-DZjdx8Gq.mjs +1180 -0
  44. package/dist/{ports-BNKoWVWZ.mjs → ports-CLSSFOuz.mjs} +3 -3
  45. package/dist/{project-DS112DYB.mjs → project-BXZzOABt.mjs} +2 -2
  46. package/dist/{reload-ZNJUo1pl.mjs → reload-DoETJ3rl.mjs} +9 -10
  47. package/dist/{server-bare-DWkkcIxV.mjs → server-bare-DVbb7kXH.mjs} +1 -1
  48. package/dist/{server-expo-B_lctBR3.mjs → server-expo-BvOCeakp.mjs} +4 -4
  49. package/dist/server-expo-DBVTdult.mjs +2 -0
  50. package/dist/{settings-D030k0t4.mjs → settings-DOugPlPz.mjs} +16 -428
  51. package/dist/{settings-BKNh5rl4.mjs → settings-EWYNwe72.mjs} +16 -14
  52. package/dist/settings.schema.json +75 -0
  53. package/dist/{simslim-B_LsGLuJ.mjs → simslim-Ca3487H6.mjs} +182 -5
  54. package/dist/{slot-launch-DU9A-gYw.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  55. package/dist/{spawn-claims-Dn3C_N6w.mjs → spawn-claims-CUleC2cM.mjs} +1 -1
  56. package/dist/{start-DYkkKF0s.mjs → start-F9gv5dcC.mjs} +147 -98
  57. package/dist/start-VoFsPSes.mjs +2 -0
  58. package/dist/{state-NcJzMEBF.mjs → state-WGdIbi_d.mjs} +2 -2
  59. package/dist/{stats-DG9e66Rz.mjs → stats-BAgJaLA4.mjs} +1 -1
  60. package/dist/{stats-B63-JVAY.mjs → stats-DBYJ5bxL.mjs} +4 -4
  61. package/dist/{status-S6ehL4Ui.mjs → status-DLCD-o3s.mjs} +63 -231
  62. package/dist/status-Dr4fRkym.mjs +216 -0
  63. package/dist/{guide-status-BAMVU0ZA.mjs → stim-installations-CINXy_su.mjs} +3 -124
  64. package/dist/{stop-BO28HGqF.mjs → stop-Dj4pfmyv.mjs} +22 -18
  65. package/dist/{stop-T1R-cZRz.mjs → stop-ZvOeTmKb.mjs} +2 -2
  66. package/dist/supervisor-run.d.mts +11 -2
  67. package/dist/supervisor-run.mjs +185 -17
  68. package/dist/{workspace-process-lock-DWaiRD0e.mjs → workspace-process-lock-BXA96myD.mjs} +2 -2
  69. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  70. package/dist/{worktree-DKhSF0UX.mjs → worktree-4aZg2NnI.mjs} +1 -1
  71. package/dist/{worktree-LGB-UwgG.mjs → worktree-BMS7GeqB.mjs} +40 -122
  72. package/dist/{worktree-R_cUlE7s.mjs → worktree-BlizKDpZ.mjs} +143 -18
  73. package/dist/{xcode-BZqyxh3e.mjs → xcode-B2tBkf0m.mjs} +9 -158
  74. package/package.json +6 -5
  75. package/dist/build-progress-CVqkfPC6.mjs +0 -188
  76. package/dist/device-lease-Crs-HkJY.mjs +0 -366
  77. package/dist/logs-query-Cqma3WIu.mjs +0 -298
  78. package/dist/ownership-DCQPuWEc.mjs +0 -489
  79. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  80. package/dist/server-expo-CeILEu86.mjs +0 -2
  81. package/dist/sim-pool-KaIsp2Ft.mjs +0 -723
  82. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  83. package/dist/workspace-state-DF2gCFhx.mjs +0 -169
@@ -0,0 +1,1003 @@
1
+ import { A as directorySize, H as projectDeviceSlots, M as isOnMountedVolume, N as listMountedVolumes, P as measuredDirectorySize, T as withConfigLock$1, f as getProject, g as loadConfig$1, h as isPathPrefix, i as clearDevice, j as formatBytes, u as getConfigDir, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
2
+ import { i as registeredCaches } from "./cache-manifest-Pubfp8wV.mjs";
3
+ import { M as workspaceName$1, v as workspaceLastUsed } from "./metro-BWLOJ_Wr.mjs";
4
+ import { u as plural } from "./command-output-D5z2_bcF.mjs";
5
+ import { i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-CbEaIk7b.mjs";
6
+ import { A as ownedAvdDirectory, C as isStimOwnedAvdName, K as isStimOwnedSim, T as listAvds } from "./android-787HhP5w.mjs";
7
+ import { _ as dropParked, b as readParked, d as teardownOwnedAvd, f as teardownOwnedIosSim, m as teardownParkedIosSim, p as teardownParkedAvd } from "./ownership-DZjdx8Gq.mjs";
8
+ import { d as listIosDeviceTypes, g as parseRuntimeVersion, l as listAllIosSims } from "./ios-B2Fpqvlb.mjs";
9
+ import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-CjUluqIX.mjs";
10
+ import { existsSync, lstatSync, readdirSync, realpathSync, rmSync, statSync } from "fs";
11
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
12
+ import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
13
+ import { isJsonObject, readJsonFile } from "@stim-cli/core/state";
14
+ import { homedir, tmpdir } from "os";
15
+ import chalk from "chalk";
16
+ //#region src/cache/caches.ts
17
+ function xcodeDerivedDataRoot() {
18
+ return join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
19
+ }
20
+ function compilationCache() {
21
+ const dir = join(xcodeDerivedDataRoot(), "CompilationCache.noindex");
22
+ if (!existsSync(dir)) return null;
23
+ return {
24
+ name: "Xcode compilation cache",
25
+ dir,
26
+ prune: "atomic",
27
+ note: "index-backed, so it can only be emptied whole"
28
+ };
29
+ }
30
+ function gradleBuildCache() {
31
+ const gradleHome = process.env.GRADLE_USER_HOME || join(homedir(), ".gradle");
32
+ const dir = resolve(gradleHome, "caches", "build-cache-1");
33
+ if (!existsSync(dir)) return null;
34
+ return {
35
+ name: "Gradle build cache",
36
+ dir,
37
+ prune: "report-only",
38
+ note: "shared with every Gradle build; report only, never pruned or emptied by Stim"
39
+ };
40
+ }
41
+ function metroFileMaps() {
42
+ const root = tmpdir();
43
+ if (!existsSync(root)) return null;
44
+ let names;
45
+ try {
46
+ names = readdirSync(root).filter((n) => n.startsWith("metro-file-map-"));
47
+ } catch {
48
+ return null;
49
+ }
50
+ const uid = process.getuid?.();
51
+ const files = [];
52
+ let bytes = 0;
53
+ for (const n of names) try {
54
+ const st = statSync(join(root, n));
55
+ if (uid !== void 0 && st.uid !== uid) continue;
56
+ files.push(join(root, n));
57
+ bytes += st.size;
58
+ } catch {}
59
+ if (!files.length) return null;
60
+ return {
61
+ name: "Metro file maps",
62
+ dir: root,
63
+ files,
64
+ bytes,
65
+ prune: "entries",
66
+ note: `${files.length} file(s), one per project root Metro has served`
67
+ };
68
+ }
69
+ function discoverCaches() {
70
+ const gradle = gradleBuildCache();
71
+ const gradleDir = gradle ? canonicalCacheDir(gradle.dir) : null;
72
+ const registered = protectNestedMetroAncestors(suppressLegacyMetroAncestors(registeredCaches().map((c) => Object.assign({}, c, {
73
+ name: c.name ?? c.dir,
74
+ note: c.note ?? "registered",
75
+ prune: c.prune,
76
+ source: "registered"
77
+ }))));
78
+ const detected = [
79
+ compilationCache(),
80
+ gradle,
81
+ metroFileMaps()
82
+ ].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
83
+ return mergeCacheDescriptors([...registered, ...detected], gradleDir);
84
+ }
85
+ function suppressLegacyMetroAncestors(caches) {
86
+ const current = caches.filter(isCurrentMetroStore);
87
+ return caches.filter((cache) => {
88
+ if (!isLegacyMetroStore(cache)) return true;
89
+ return !current.some((candidate) => isNamedMetroChild(cache.dir, candidate.dir));
90
+ });
91
+ }
92
+ function protectNestedMetroAncestors(caches) {
93
+ const current = caches.filter(isCurrentMetroStore);
94
+ return caches.map((cache) => {
95
+ if (!isCurrentMetroStore(cache)) return cache;
96
+ if (!current.some((candidate) => isNamedMetroChild(cache.dir, candidate.dir))) return cache;
97
+ return Object.assign({}, cache, {
98
+ prune: "report-only",
99
+ note: "named Metro store is also an override parent; report only while its child is registered"
100
+ });
101
+ });
102
+ }
103
+ function isLegacyMetroStore(cache) {
104
+ return isMetroStore(cache) && cache.layout === void 0;
105
+ }
106
+ function isCurrentMetroStore(cache) {
107
+ return isMetroStore(cache) && cache.layout === METRO_NAMED_CACHE_LAYOUT;
108
+ }
109
+ function isMetroStore(cache) {
110
+ return cache.source === "registered" && cache.name === "Metro transform cache" && cache.prune === "entries" && cache.entriesDepth === 2;
111
+ }
112
+ function isNamedMetroChild(parent, candidate) {
113
+ const dir = canonicalCacheDir(parent);
114
+ const child = canonicalCacheDir(candidate);
115
+ return dirname(child) === dir || canonicalCacheDir(dirname(candidate)) === dir;
116
+ }
117
+ function mergeCacheDescriptors(caches, gradleDir) {
118
+ const merged = [];
119
+ for (const cache of caches) {
120
+ const descriptor = protectGradleCache(cache, gradleDir);
121
+ const dir = canonicalCacheDir(descriptor.dir);
122
+ const exact = merged.findIndex((candidate) => canonicalCacheDir(candidate.dir) === dir);
123
+ if (exact !== -1) {
124
+ const current = merged[exact];
125
+ if (pruneStrength(descriptor.prune) > pruneStrength(current.prune)) merged[exact] = Object.assign({}, current, {
126
+ prune: descriptor.prune,
127
+ note: descriptor.note
128
+ });
129
+ continue;
130
+ }
131
+ if (gradleDir && isGradleProtectedDescriptor(descriptor, gradleDir)) {
132
+ if (merged.some((candidate) => {
133
+ const candidateDir = canonicalCacheDir(candidate.dir);
134
+ return isGradleProtectedDescriptor(candidate, gradleDir) && cachePathContains(candidateDir, dir);
135
+ })) continue;
136
+ for (let i = merged.length - 1; i >= 0; i--) {
137
+ const candidate = merged[i];
138
+ const candidateDir = canonicalCacheDir(candidate.dir);
139
+ if (isGradleProtectedDescriptor(candidate, gradleDir) && cachePathContains(dir, candidateDir)) merged.splice(i, 1);
140
+ }
141
+ }
142
+ merged.push(descriptor);
143
+ }
144
+ return merged;
145
+ }
146
+ function protectGradleCache(cache, gradleDir) {
147
+ if (cache.files) return cache;
148
+ if (!gradleDir || !cachePathsOverlap(canonicalCacheDir(cache.dir), gradleDir)) return cache;
149
+ return Object.assign({}, cache, {
150
+ prune: "report-only",
151
+ note: "shared with every Gradle build; report only, never pruned or emptied by Stim"
152
+ });
153
+ }
154
+ function isGradleProtectedDescriptor(cache, gradleDir) {
155
+ return cache.prune === "report-only" && cachePathsOverlap(canonicalCacheDir(cache.dir), gradleDir);
156
+ }
157
+ function pruneStrength(prune) {
158
+ if (prune === "report-only") return 3;
159
+ if (prune === "atomic") return 2;
160
+ return 1;
161
+ }
162
+ function cachePathsOverlap(left, right) {
163
+ return cachePathContains(left, right) || cachePathContains(right, left);
164
+ }
165
+ function cachePathContains(parent, child) {
166
+ const rel = relative(parent, child);
167
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
168
+ }
169
+ function canonicalCacheDir(dir) {
170
+ try {
171
+ return realpathSync(dir);
172
+ } catch {
173
+ return resolve(dir);
174
+ }
175
+ }
176
+ function sizeCaches(caches) {
177
+ return caches.map((c) => Object.assign({}, c, { bytes: c.bytes ?? directorySize(c.dir) }));
178
+ }
179
+ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
180
+ const cutoff = now - olderThanDays * 24 * 60 * 60 * 1e3;
181
+ if (cache.prune === "report-only") return {
182
+ removed: 0,
183
+ bytes: 0,
184
+ skipped: "report-only shared cache; Stim never deletes it"
185
+ };
186
+ if (cache.prune === "atomic") return {
187
+ removed: 0,
188
+ bytes: 0,
189
+ skipped: "index-backed; empty it whole or not at all"
190
+ };
191
+ const entries = cache.files ?? entriesAtDepth(cache.dir, cache.entriesDepth ?? 1);
192
+ let removed = 0;
193
+ let bytes = 0;
194
+ let failed = 0;
195
+ for (const entry of entries) {
196
+ let used;
197
+ let size;
198
+ try {
199
+ const st = statSync(entry);
200
+ used = Math.max(st.atimeMs, st.mtimeMs);
201
+ size = st.isDirectory() ? directorySize(entry) : st.size;
202
+ } catch {
203
+ continue;
204
+ }
205
+ if (used >= cutoff) continue;
206
+ try {
207
+ rmSync(entry, {
208
+ recursive: true,
209
+ force: true
210
+ });
211
+ removed++;
212
+ bytes += size;
213
+ } catch {
214
+ failed++;
215
+ }
216
+ }
217
+ return {
218
+ removed,
219
+ bytes,
220
+ failed,
221
+ skipped: null
222
+ };
223
+ }
224
+ function entriesAtDepth(dir, depth) {
225
+ let level = [dir];
226
+ for (let i = 0; i < depth; i++) {
227
+ const next = [];
228
+ for (const parent of level) {
229
+ let names;
230
+ try {
231
+ names = readdirSync(parent);
232
+ } catch {
233
+ continue;
234
+ }
235
+ for (const name of names) next.push(join(parent, name));
236
+ }
237
+ level = next;
238
+ }
239
+ return level;
240
+ }
241
+ //#endregion
242
+ //#region src/commands/gc/caches.ts
243
+ const DAY_MS$2 = 864e5;
244
+ function cacheSweepIsScoped() {
245
+ return Boolean(process.env.STIM_HOME);
246
+ }
247
+ function isInsideConfigDir(dir) {
248
+ const root = canonicalPath(getConfigDir());
249
+ const target = canonicalPath(dir);
250
+ const rel = relative(root, target);
251
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
252
+ }
253
+ function machineGlobalReason(cache) {
254
+ if (!cacheSweepIsScoped()) return null;
255
+ if (cache.source === "registered") return null;
256
+ if (isInsideConfigDir(cache.dir)) return null;
257
+ return `STIM_HOME scopes this config, but ${cache.dir} is outside it and therefore machine-global`;
258
+ }
259
+ const EVERY_CACHE = "all";
260
+ const WORKSPACE_OUTPUTS = "workspaces";
261
+ function includesWorkspaceOutputs(name) {
262
+ const wanted = name?.trim().toLowerCase();
263
+ return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
264
+ }
265
+ function selectCaches(caches, name) {
266
+ if (!name) return caches;
267
+ const wanted = name.trim().toLowerCase();
268
+ if (wanted === EVERY_CACHE) return caches;
269
+ if (wanted === WORKSPACE_OUTPUTS) return [];
270
+ return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
271
+ }
272
+ function planCacheEmptying(caches, all) {
273
+ const annotated = caches.map((c) => Object.assign({}, c, { machineGlobal: machineGlobalReason(c) }));
274
+ if (!all) return annotated;
275
+ return annotated.map((c) => {
276
+ if (c.prune === "report-only") return Object.assign({}, c, {
277
+ willEmpty: false,
278
+ emptySkipped: "report-only shared cache; Stim never deletes it"
279
+ });
280
+ if (c.machineGlobal) return Object.assign({}, c, {
281
+ willEmpty: false,
282
+ emptySkipped: c.machineGlobal
283
+ });
284
+ if (!ownsItsDirectory(c)) return Object.assign({}, c, {
285
+ willEmpty: false,
286
+ emptySkipped: `${c.dir} is not a directory this cache owns`
287
+ });
288
+ return Object.assign({}, c, {
289
+ willEmpty: true,
290
+ emptySkipped: null
291
+ });
292
+ });
293
+ }
294
+ function ownsItsDirectory(cache) {
295
+ if (Array.isArray(cache.files)) return false;
296
+ const dir = canonicalPath(cache.dir);
297
+ if (dirname(dir) === dir) return false;
298
+ return ![
299
+ homedir(),
300
+ tmpdir(),
301
+ getConfigDir()
302
+ ].map(canonicalPath).includes(dir);
303
+ }
304
+ function emptyCache(cache) {
305
+ if (cache.prune === "report-only") return {
306
+ removed: 0,
307
+ bytes: 0,
308
+ skipped: "report-only shared cache; Stim never deletes it"
309
+ };
310
+ if (cache.prune !== "atomic") return pruneCache(cache, {
311
+ olderThanDays: 0,
312
+ now: Date.now() + DAY_MS$2
313
+ });
314
+ let names;
315
+ try {
316
+ names = readdirSync(cache.dir);
317
+ } catch (e) {
318
+ return {
319
+ removed: 0,
320
+ bytes: 0,
321
+ skipped: `could not read ${cache.dir}: ${e.message}`
322
+ };
323
+ }
324
+ let removed = 0;
325
+ let failed = 0;
326
+ for (const name of names) try {
327
+ rmSync(join(cache.dir, name), {
328
+ recursive: true,
329
+ force: true
330
+ });
331
+ removed++;
332
+ } catch {
333
+ failed++;
334
+ }
335
+ return {
336
+ removed,
337
+ bytes: failed ? 0 : cache.bytes ?? 0,
338
+ failed,
339
+ skipped: null
340
+ };
341
+ }
342
+ function reportFailures(cache, failed) {
343
+ if (!failed) return;
344
+ console.log(chalk.red(`${cache.name}: ${failed} entr${failed === 1 ? "y" : "ies"} in ${cache.dir} could not be removed`));
345
+ process.exitCode = 1;
346
+ }
347
+ function trimCaches(caches, olderThan) {
348
+ let cacheBytes = 0;
349
+ const trimmed = [];
350
+ for (const c of caches) {
351
+ if (c.machineGlobal) {
352
+ console.log(chalk.yellow(`Left ${c.name} alone: ${c.machineGlobal}`));
353
+ continue;
354
+ }
355
+ const r = pruneCache(c, { olderThanDays: olderThan });
356
+ if (r.skipped) console.log(chalk.yellow(`Left ${c.name} alone: ${r.skipped}`));
357
+ else if (r.removed) {
358
+ cacheBytes += r.bytes;
359
+ trimmed.push(c);
360
+ console.log(chalk.green(`Trimmed ${c.name}: ${r.removed} entr${r.removed === 1 ? "y" : "ies"} (${formatBytes(r.bytes)})`));
361
+ } else if (!r.failed) console.log(chalk.dim(`${c.name}: nothing older than ${olderThan}d`));
362
+ reportFailures(c, r.failed);
363
+ }
364
+ if (cacheBytes) console.log(chalk.dim(`Trimmed ${formatBytes(cacheBytes)} of shared cache. The next build that wanted those entries pays to rebuild them.`));
365
+ return trimmed;
366
+ }
367
+ function emptyCaches(caches) {
368
+ let cacheBytes = 0;
369
+ for (const c of caches) {
370
+ if (!c.willEmpty) {
371
+ console.log(chalk.yellow(`Left ${c.name} alone: ${c.emptySkipped}`));
372
+ continue;
373
+ }
374
+ const r = emptyCache(c);
375
+ if (r.skipped) console.log(chalk.yellow(`Left ${c.name} alone: ${r.skipped}`));
376
+ else if (r.removed) {
377
+ cacheBytes += r.bytes;
378
+ console.log(chalk.green(`Emptied ${c.name}: ${r.removed} entr${r.removed === 1 ? "y" : "ies"} (${formatBytes(r.bytes)})`));
379
+ } else if (!r.failed) console.log(chalk.dim(`${c.name}: already empty`));
380
+ reportFailures(c, r.failed);
381
+ }
382
+ if (cacheBytes) console.log(chalk.dim(`Emptied ${formatBytes(cacheBytes)} of shared cache. Every build that wanted any of it now pays to rebuild it.`));
383
+ }
384
+ //#endregion
385
+ //#region src/commands/gc/devices.ts
386
+ const DEVICE_LIST_TIMEOUT_MS = 3e4;
387
+ const AVD_SIZE_TIMEOUT_MS = 5e3;
388
+ const DAY_MS$1 = 864e5;
389
+ function describeKept(ref) {
390
+ return ref.mounted ? `referenced by ${ref.path}` : `referenced by ${ref.path} (volume not mounted; kept just in case)`;
391
+ }
392
+ function findOrphanedDevices({ sims = [], avds = [], config, isMounted, deadProjects = [], isOwned = isStimCreated }) {
393
+ const dead = new Set(deadProjects);
394
+ const referenced = /* @__PURE__ */ new Map();
395
+ for (const [path, proj] of Object.entries(config?.projects || {})) {
396
+ if (dead.has(path)) continue;
397
+ const mounted = isMounted ? isMounted(path) : true;
398
+ for (const { platforms } of projectDeviceSlots(proj)) {
399
+ const ios = platforms.ios;
400
+ if (ios?.deviceUdid) referenced.set(ios.deviceUdid, {
401
+ path,
402
+ mounted
403
+ });
404
+ const android = platforms.android;
405
+ if (android?.avdName) referenced.set(android.avdName, {
406
+ path,
407
+ mounted
408
+ });
409
+ }
410
+ }
411
+ for (const sim of readParked("ios", { config })) referenced.set(sim.udid, {
412
+ path: "the simulator pool",
413
+ mounted: true
414
+ });
415
+ for (const avd of readParked("android", { config })) referenced.set(avd.name, {
416
+ path: "the emulator pool",
417
+ mounted: true
418
+ });
419
+ const orphaned = [];
420
+ const kept = [];
421
+ const unverified = [];
422
+ for (const sim of sims) {
423
+ if (!sim?.name?.startsWith("stim-")) continue;
424
+ const ref = referenced.get(sim.udid);
425
+ const device = {
426
+ kind: "ios",
427
+ id: sim.udid,
428
+ name: sim.name
429
+ };
430
+ if (!ref && !isOwned(device)) unverified.push(device);
431
+ else if (!ref) orphaned.push(device);
432
+ else kept.push({
433
+ kind: "ios",
434
+ id: sim.udid,
435
+ name: sim.name,
436
+ reason: describeKept(ref)
437
+ });
438
+ }
439
+ for (const avdName of avds) {
440
+ if (!avdName?.startsWith("stim-")) continue;
441
+ const ref = referenced.get(avdName);
442
+ const device = {
443
+ kind: "android",
444
+ id: avdName,
445
+ name: avdName
446
+ };
447
+ if (!ref && !isOwned(device)) unverified.push(device);
448
+ else if (!ref) orphaned.push(device);
449
+ else kept.push({
450
+ kind: "android",
451
+ id: avdName,
452
+ name: avdName,
453
+ reason: describeKept(ref)
454
+ });
455
+ }
456
+ return {
457
+ orphaned,
458
+ kept,
459
+ unverified
460
+ };
461
+ }
462
+ function isStimCreated(device) {
463
+ return device.kind === "ios" ? isStimOwnedSim({
464
+ udid: device.id,
465
+ name: device.name
466
+ }) : isStimOwnedAvdName(device.id);
467
+ }
468
+ function unverifiedDeviceCommand(device) {
469
+ return device.kind === "ios" ? `xcrun simctl delete ${device.id}` : `avdmanager delete avd -n ${JSON.stringify(device.id)}`;
470
+ }
471
+ function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays, now = Date.now(), lastTouched, deadProjects = [] }) {
472
+ if (!Number.isFinite(olderThanDays) || typeof lastTouched !== "function") return [];
473
+ const cutoff = now - olderThanDays * DAY_MS$1;
474
+ const dead = new Set(deadProjects);
475
+ const liveSims = new Map(sims.filter((s) => s?.name?.startsWith("stim-")).map((s) => [s.udid, s.name]));
476
+ const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
477
+ const stale = [];
478
+ for (const [path, proj] of Object.entries(config?.projects || {})) {
479
+ if (dead.has(path) || !isAbsolute(path)) continue;
480
+ const touched = lastTouched(path);
481
+ if (!Number.isFinite(touched) || touched >= cutoff) continue;
482
+ const idleDays = Math.floor((now - touched) / DAY_MS$1);
483
+ for (const { slot, platforms } of projectDeviceSlots(proj)) {
484
+ const ios = platforms.ios;
485
+ if (ios?.owned && ios.deviceUdid && liveSims.has(ios.deviceUdid)) stale.push({
486
+ kind: "ios",
487
+ id: ios.deviceUdid,
488
+ name: liveSims.get(ios.deviceUdid),
489
+ project: path,
490
+ ...slot === "default" ? {} : { slot },
491
+ idleDays
492
+ });
493
+ const android = platforms.android;
494
+ if (android?.owned && android.avdName && liveAvds.has(android.avdName)) stale.push({
495
+ kind: "android",
496
+ id: android.avdName,
497
+ name: android.avdName,
498
+ project: path,
499
+ idleDays,
500
+ ...slot === "default" ? {} : { slot }
501
+ });
502
+ }
503
+ }
504
+ return stale;
505
+ }
506
+ function findStaleDeviceRecords({ config, sims = [], avds = [], deadProjects = [], simsChecked = true, avdsChecked = true }) {
507
+ const dead = new Set(deadProjects);
508
+ const liveSims = new Set(sims.map((s) => s?.udid).filter(Boolean));
509
+ const liveAvds = new Set(avds.filter((a) => typeof a === "string"));
510
+ const stale = [];
511
+ for (const [path, proj] of Object.entries(config?.projects || {})) {
512
+ if (dead.has(path)) continue;
513
+ for (const { slot, platforms } of projectDeviceSlots(proj)) {
514
+ const ios = platforms.ios;
515
+ if (simsChecked && ios?.deviceUdid && !liveSims.has(ios.deviceUdid)) stale.push({
516
+ kind: "ios",
517
+ id: ios.deviceUdid,
518
+ project: path,
519
+ ...slot === "default" ? {} : { slot },
520
+ owned: Boolean(ios.owned)
521
+ });
522
+ const android = platforms.android;
523
+ if (avdsChecked && android?.avdName && !liveAvds.has(android.avdName)) stale.push({
524
+ kind: "android",
525
+ id: android.avdName,
526
+ project: path,
527
+ ...slot === "default" ? {} : { slot },
528
+ owned: Boolean(android.owned)
529
+ });
530
+ }
531
+ }
532
+ return stale;
533
+ }
534
+ function describeUnverifiableDevices(simNames = [], avdNames = [], { reason = "no Stim config found" } = {}) {
535
+ const ours = [...simNames, ...avdNames].filter((n) => typeof n === "string" && n.startsWith("stim-"));
536
+ if (ours.length === 0) return [`${reason}; device sweep skipped`];
537
+ return [`${reason}, so ${ours.length} stim-created device(s) cannot be verified as orphaned: ${ours.join(", ")}`, "they were NOT touched. If they are stale, delete them with `xcrun simctl delete <udid>` or `avdmanager delete avd -n <name>`"];
538
+ }
539
+ function withAndroidAvdSizes(devices, { avdDirectory = ownedAvdDirectory, size = directorySize } = {}) {
540
+ return devices.map((device) => {
541
+ if (device.kind !== "android") return device;
542
+ const dir = device.orphanedDirectory?.directory ?? avdDirectory(device.id);
543
+ if (!dir) return device;
544
+ try {
545
+ const bytes = size(dir, { timeoutMs: AVD_SIZE_TIMEOUT_MS });
546
+ return bytes > 0 ? {
547
+ ...device,
548
+ bytes
549
+ } : device;
550
+ } catch {
551
+ return device;
552
+ }
553
+ });
554
+ }
555
+ function describeParkedSims(records, sims, deviceTypes, { simsChecked = true } = {}) {
556
+ const listed = new Map(sims.map((sim) => [sim.udid, sim]));
557
+ return records.map((record) => {
558
+ const sim = listed.get(record.udid);
559
+ return {
560
+ udid: record.udid,
561
+ name: sim?.name ?? record.name,
562
+ model: deviceTypes.find((d) => d.identifier === record.deviceTypeIdentifier)?.name ?? null,
563
+ runtime: sim ? parseRuntimeVersion(sim.runtime) : parseRuntimeVersion(record.runtimeIdentifier),
564
+ parkedAt: record.parkedAt,
565
+ bytes: typeof sim?.dataPathSize === "number" ? sim.dataPathSize : null,
566
+ listed: simsChecked ? Boolean(sim) : null
567
+ };
568
+ });
569
+ }
570
+ function parkedAtLeast(records, { olderThanDays = null, now = Date.now() }) {
571
+ if (olderThanDays === null) return [...records];
572
+ const cutoff = now - olderThanDays * DAY_MS$1;
573
+ return records.filter((record) => Date.parse(record.parkedAt) <= cutoff);
574
+ }
575
+ function collectParkedSims(deps, age = {}) {
576
+ const records = parkedAtLeast(readParked("ios"), age);
577
+ if (records.length === 0) return [];
578
+ let sims;
579
+ let deviceTypes = [];
580
+ try {
581
+ sims = (deps.listAllIosSims ?? listAllIosSims)({
582
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
583
+ includeUnavailable: true
584
+ });
585
+ } catch {
586
+ return describeParkedSims(records, [], [], { simsChecked: false });
587
+ }
588
+ try {
589
+ deviceTypes = (deps.listIosDeviceTypes ?? listIosDeviceTypes)();
590
+ } catch {}
591
+ return describeParkedSims(records, sims, deviceTypes);
592
+ }
593
+ function collectParkedAvds(deps, age = {}) {
594
+ const records = parkedAtLeast(readParked("android"), age);
595
+ if (!records.length) return [];
596
+ let avds = null;
597
+ try {
598
+ avds = (deps.listAvds ?? listAvds)();
599
+ } catch {}
600
+ return records.map((record) => {
601
+ let bytes = null;
602
+ try {
603
+ const directory = (deps.avdDirectory ?? ownedAvdDirectory)(record.name);
604
+ if (directory) bytes = (deps.directorySize ?? directorySize)(directory, { timeoutMs: AVD_SIZE_TIMEOUT_MS });
605
+ } catch {}
606
+ return {
607
+ name: record.name,
608
+ systemImage: record.systemImage,
609
+ parkedAt: record.parkedAt,
610
+ bytes,
611
+ listed: avds === null ? null : avds.includes(record.name)
612
+ };
613
+ });
614
+ }
615
+ function deleteParkedAvds(avds) {
616
+ let failures = 0;
617
+ for (const avd of avds) {
618
+ if (avd.listed === null) {
619
+ failures++;
620
+ console.log(chalk.red(`Could not verify parked android emulator ${avd.name}; its pool record was kept.`));
621
+ continue;
622
+ }
623
+ const result = teardownParkedAvd(avd.name);
624
+ if (result.status === "failed") {
625
+ failures++;
626
+ console.log(chalk.red(`Failed to delete parked android emulator ${avd.name}: ${result.reason}`));
627
+ } else if (result.status === "torn-down") console.log(chalk.green(`Deleted parked android emulator ${avd.name}`));
628
+ else console.log(chalk.dim(`Skipped ${avd.name}; it is no longer parked.`));
629
+ }
630
+ return failures;
631
+ }
632
+ function deleteParkedSims(parkedSims, deps = {}) {
633
+ let failures = 0;
634
+ let emptied = 0;
635
+ for (const sim of parkedSims) {
636
+ if (sim.listed === null) {
637
+ failures++;
638
+ console.log(chalk.red(`Could not verify parked ios sim ${sim.name} (${sim.udid}); its pool record was kept.`));
639
+ continue;
640
+ }
641
+ try {
642
+ const teardown = sim.listed ? teardownParkedIosSim(sim.udid, {
643
+ label: sim.name,
644
+ deleteSim: deps.deleteParkedIosSim
645
+ }) : null;
646
+ if (!(sim.listed ? teardown?.status === "torn-down" : dropParked("ios", sim.udid))) {
647
+ if (teardown?.status === "failed") {
648
+ failures++;
649
+ console.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${teardown.reason}`));
650
+ } else console.log(chalk.dim(`Skipped ${sim.name} (${sim.udid}); it is no longer parked.`));
651
+ continue;
652
+ }
653
+ emptied++;
654
+ console.log(chalk.green(sim.listed ? `Deleted parked ios sim ${sim.name} (${sim.udid})` : `Dropped the parked record for ${sim.name} (${sim.udid}); it is not on this machine.`));
655
+ } catch (err) {
656
+ failures++;
657
+ console.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${err?.message || err}`));
658
+ }
659
+ }
660
+ if (emptied) console.log(chalk.dim(` emptied ${plural(emptied, "parked simulator")} from the pool`));
661
+ return failures;
662
+ }
663
+ function deviceSweepIsScoped() {
664
+ return Boolean(process.env.STIM_HOME);
665
+ }
666
+ function collectDeviceLeases(now) {
667
+ const expired = [];
668
+ const kept = [];
669
+ for (const entry of listLeaseFiles()) {
670
+ const lease = entry.lease;
671
+ if (!lease) {
672
+ kept.push({
673
+ name: entry.name,
674
+ path: entry.path,
675
+ reason: "it does not parse as a lease, so no run may take that device"
676
+ });
677
+ continue;
678
+ }
679
+ if (leaseIsExpired(lease, now)) {
680
+ expired.push(entry);
681
+ continue;
682
+ }
683
+ if (!existsSync(lease.holder)) kept.push({
684
+ name: entry.name,
685
+ path: entry.path,
686
+ reason: `${lease.holder} is not on this machine, but the lease runs until ${lease.expiresAt}`
687
+ });
688
+ }
689
+ return {
690
+ expired,
691
+ kept
692
+ };
693
+ }
694
+ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords) {
695
+ let deleteFailures = 0;
696
+ function reap(d, onlyIfMissing = false) {
697
+ const r = d.kind === "ios" ? teardownOwnedIosSim(d.id, {
698
+ del: true,
699
+ label: d.name
700
+ }) : teardownOwnedAvd(d.name, {
701
+ del: true,
702
+ onlyIfMissing,
703
+ ..."project" in d ? { owner: {
704
+ projectPath: d.project,
705
+ slot: d.slot
706
+ } } : {},
707
+ ..."project" in d ? { onRemoved: () => {
708
+ if (clearDevice(d.project, d.kind, d.slot, d.id)) console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
709
+ } } : {},
710
+ ..."orphanedDirectory" in d ? { orphanedDirectory: d.orphanedDirectory } : {}
711
+ });
712
+ const what = d.kind === "ios" ? `ios sim ${d.name} (${d.id})` : `android avd ${d.name}`;
713
+ if (r.status === "torn-down") console.log(chalk.green(`Deleted ${what}`));
714
+ else if (r.status === "missing") console.log(chalk.dim(`${what} is already gone; nothing to delete.`));
715
+ else if (r.status === "skipped") console.log(chalk.yellow(`Skipped ${what}: ${r.reason} -- left for a later gc`));
716
+ else {
717
+ deleteFailures++;
718
+ console.log(chalk.red(`Failed to delete ${d.kind} device ${d.name}: ${r.reason}`));
719
+ }
720
+ return r.status;
721
+ }
722
+ for (const d of orphanedDevices) reap(d);
723
+ for (const d of staleDevices) {
724
+ const status = reap(d);
725
+ if (d.kind === "ios" && (status === "torn-down" || status === "missing")) {
726
+ clearDevice(d.project, d.kind, d.slot, d.id);
727
+ console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
728
+ }
729
+ }
730
+ for (const r of staleDeviceRecords) if (r.kind === "android" && r.owned) reap({
731
+ ...r,
732
+ name: r.id,
733
+ idleDays: 0
734
+ }, true);
735
+ else if (withConfigLock$1(() => {
736
+ if (r.kind === "android" && deviceSlotPlatforms(getProject(r.project), r.slot)?.android?.owned) return false;
737
+ return clearDevice(r.project, r.kind, r.slot, r.id);
738
+ })) console.log(chalk.green(`Cleared the ${r.kind} record for ${r.project} (${r.id} is not on this machine)`));
739
+ return deleteFailures;
740
+ }
741
+ //#endregion
742
+ //#region src/commands/gc/workspaces.ts
743
+ function workspacesRoot() {
744
+ return join(getConfigDir(), "workspaces");
745
+ }
746
+ function isInsideWorkspaces(dir) {
747
+ const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
748
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
749
+ }
750
+ function readRecordedRoot(dir, name) {
751
+ let parsed;
752
+ try {
753
+ parsed = readJsonFile(join(dir, "workspace.json"));
754
+ } catch (error) {
755
+ return {
756
+ projectRoot: null,
757
+ problem: error?.code === "ENOENT" ? "it has no workspace.json" : "its workspace.json does not parse"
758
+ };
759
+ }
760
+ const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
761
+ if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
762
+ projectRoot: null,
763
+ problem: "its workspace.json records no absolute project root"
764
+ };
765
+ if (workspaceName$1(projectRoot) !== name) return {
766
+ projectRoot: null,
767
+ problem: `its workspace.json records ${projectRoot}, whose workspace directory has another name`
768
+ };
769
+ return {
770
+ projectRoot,
771
+ problem: null
772
+ };
773
+ }
774
+ function listWorkspaceDirs() {
775
+ const root = workspacesRoot();
776
+ let names;
777
+ try {
778
+ names = readdirSync(root).toSorted();
779
+ } catch {
780
+ return [];
781
+ }
782
+ const entries = [];
783
+ for (const name of names) {
784
+ const dir = join(root, name);
785
+ try {
786
+ if (!statSync(dir).isDirectory()) continue;
787
+ } catch {
788
+ continue;
789
+ }
790
+ entries.push({
791
+ dir,
792
+ ...readRecordedRoot(dir, name)
793
+ });
794
+ }
795
+ return entries;
796
+ }
797
+ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse }) {
798
+ const orphaned = [];
799
+ const skipped = [];
800
+ for (const { dir, projectRoot, problem } of entries) {
801
+ if (projectRoot === null) {
802
+ skipped.push({
803
+ dir,
804
+ reason: `workspace directory not resolved: ${problem ?? "unknown project root"}`
805
+ });
806
+ continue;
807
+ }
808
+ const present = exists(projectRoot);
809
+ if (present === null) {
810
+ skipped.push({
811
+ dir,
812
+ reason: `cannot tell whether its project root ${projectRoot} still exists`
813
+ });
814
+ continue;
815
+ }
816
+ if (present) continue;
817
+ if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
818
+ if (!isMounted(projectRoot)) {
819
+ skipped.push({
820
+ dir,
821
+ reason: `the volume of its project root ${projectRoot} is not mounted`
822
+ });
823
+ continue;
824
+ }
825
+ const reasons = inUse(projectRoot);
826
+ if (reasons.length) {
827
+ skipped.push({
828
+ dir,
829
+ reason: `its project root ${projectRoot} is gone, but ${reasons.join("; ")}`
830
+ });
831
+ continue;
832
+ }
833
+ orphaned.push({
834
+ dir,
835
+ projectRoot
836
+ });
837
+ }
838
+ return {
839
+ orphaned,
840
+ skipped
841
+ };
842
+ }
843
+ function rootPresence(path) {
844
+ try {
845
+ lstatSync(path);
846
+ return true;
847
+ } catch (error) {
848
+ const code = error?.code;
849
+ return code === "ENOENT" || code === "ENOTDIR" ? false : null;
850
+ }
851
+ }
852
+ function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
853
+ const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
854
+ registryKeys,
855
+ exists: rootPresence,
856
+ isMounted: (path) => isOnMountedVolume(path, mountedVolumes),
857
+ inUse: (root) => workspaceInUse(root)
858
+ });
859
+ return {
860
+ orphaned: classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })),
861
+ skipped: classified.skipped
862
+ };
863
+ }
864
+ 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.";
865
+ const WORKSPACE_OUTPUT_DIRS = [
866
+ "derived-data",
867
+ "gradle-build",
868
+ "android-cas",
869
+ "cache-provider"
870
+ ];
871
+ const DAY_MS = 864e5;
872
+ function planWorkspaceOutputs(entries, { olderThan, now }) {
873
+ return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
874
+ const idleDays = Number.isFinite(lastUsed) ? Math.max(0, Math.floor((now - lastUsed) / DAY_MS)) : null;
875
+ const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : inUse.length ? ["in-use", `in use: ${inUse.join("; ")}`] : olderThan === null ? [null, null] : idleDays === null ? ["last-use-unknown", "its last use is unknown"] : idleDays < olderThan ? ["recently-used", `used ${idleDays}d ago, within --older-than ${olderThan}`] : [null, null];
876
+ return {
877
+ dir,
878
+ projectRoot,
879
+ bytes,
880
+ idleDays,
881
+ willClear: keptCode === null,
882
+ keptCode,
883
+ keptReason
884
+ };
885
+ });
886
+ }
887
+ const SIZE_TIMEOUT_MS = 6e4;
888
+ function sizeOf(dir) {
889
+ return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
890
+ }
891
+ function outputPaths(dir) {
892
+ return WORKSPACE_OUTPUT_DIRS.map((name) => join(dir, name)).filter((path) => existsSync(path));
893
+ }
894
+ function outputBytes(paths) {
895
+ let total = 0;
896
+ for (const path of paths) {
897
+ const size = sizeOf(path);
898
+ if (size === null) return null;
899
+ total += size;
900
+ }
901
+ return total;
902
+ }
903
+ function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true }) {
904
+ const mountedVolumes = listMountedVolumes();
905
+ const entries = listWorkspaceDirs().filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0).map(({ paths, ...entry }) => Object.assign({}, entry, { bytes: measure ? outputBytes(paths) : null })).map((entry) => Object.assign({}, entry, {
906
+ lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
907
+ inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
908
+ }));
909
+ return {
910
+ root: workspacesRoot(),
911
+ workspaces: planWorkspaceOutputs(entries, {
912
+ olderThan,
913
+ now
914
+ })
915
+ };
916
+ }
917
+ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
918
+ let failures = 0;
919
+ let cleared = 0;
920
+ const clearedRoots = [];
921
+ for (const entry of report.workspaces) {
922
+ const root = entry.projectRoot;
923
+ if (!entry.willClear || root === null) {
924
+ if (entry.keptReason) console.log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
925
+ continue;
926
+ }
927
+ let run;
928
+ try {
929
+ run = await withIdleWorkspace(root, () => {
930
+ const [again] = planWorkspaceOutputs([{
931
+ ...entry,
932
+ problem: null,
933
+ lastUsed: workspaceLastUsed(root),
934
+ inUse: []
935
+ }], {
936
+ olderThan,
937
+ now
938
+ });
939
+ if (again?.keptReason) return again.keptReason;
940
+ for (const name of WORKSPACE_OUTPUT_DIRS) rmSync(join(entry.dir, name), {
941
+ recursive: true,
942
+ force: true
943
+ });
944
+ return null;
945
+ }, { purpose: "gc" });
946
+ } catch (error) {
947
+ failures++;
948
+ console.log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
949
+ continue;
950
+ }
951
+ const kept = run.ran ? run.value : `in use: ${run.reasons.join("; ")}`;
952
+ if (kept) {
953
+ console.log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
954
+ continue;
955
+ }
956
+ cleared += entry.bytes ?? 0;
957
+ clearedRoots.push(root);
958
+ console.log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
959
+ }
960
+ if (cleared) console.log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
961
+ return {
962
+ failures,
963
+ cleared: clearedRoots
964
+ };
965
+ }
966
+ function stillOrphaned({ dir, projectRoot }) {
967
+ if (readRecordedRoot(dir, basename(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
968
+ return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
969
+ }
970
+ async function deleteOrphanedWorkspaces(orphaned) {
971
+ let failures = 0;
972
+ for (const entry of orphaned) {
973
+ if (!existsSync(entry.dir)) continue;
974
+ let run;
975
+ try {
976
+ run = await withIdleWorkspace(entry.projectRoot, () => {
977
+ if (!stillOrphaned(entry)) return false;
978
+ emptyWorkspaceDir(entry.dir);
979
+ return true;
980
+ }, { purpose: "gc" });
981
+ } catch (error) {
982
+ failures++;
983
+ console.log(chalk.red(`Could not remove ${entry.dir}: ${error?.message || error}`));
984
+ continue;
985
+ }
986
+ if (!run.ran) {
987
+ console.log(chalk.yellow(`Kept ${entry.dir}: ${run.reasons.join("; ")}`));
988
+ continue;
989
+ }
990
+ if (!run.value) {
991
+ console.log(chalk.yellow(`Kept ${entry.dir}: it can no longer be confirmed orphaned.`));
992
+ continue;
993
+ }
994
+ const size = entry.bytes === void 0 ? "" : ` (${sizeText(entry.bytes)})`;
995
+ console.log(chalk.green(`Removed the orphaned workspace directory ${entry.dir}${size}`));
996
+ }
997
+ return failures;
998
+ }
999
+ function sizeText(bytes) {
1000
+ return bytes === null ? "size unknown" : formatBytes(bytes);
1001
+ }
1002
+ //#endregion
1003
+ export { discoverCaches as A, unverifiedDeviceCommand as C, planCacheEmptying as D, includesWorkspaceOutputs as E, selectCaches as O, findStaleProjectDevices as S, emptyCaches as T, describeParkedSims as _, collectWorkspaceOutputs as a, findOrphanedDevices as b, listWorkspaceDirs as c, collectDeviceLeases as d, collectParkedAvds as f, deleteProjectDevices as g, deleteParkedSims as h, collectOrphanedWorkspaces as i, sizeCaches as j, trimCaches as k, sizeText as l, deleteParkedAvds as m, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, collectParkedSims as p, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, DEVICE_LIST_TIMEOUT_MS as u, describeUnverifiableDevices as v, withAndroidAvdSizes as w, findStaleDeviceRecords as x, deviceSweepIsScoped as y };