stim 1.10.1 → 1.12.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 (72) hide show
  1. package/dist/{build-progress-BXx-ZBe4.mjs → activity-CCZJs__P.mjs} +9 -182
  2. package/dist/{android-787HhP5w.mjs → android-75Sk2rIR.mjs} +81 -19
  3. package/dist/{android-Bu4mbDzc.mjs → android-B7X-8H7h.mjs} +426 -106
  4. package/dist/{android-DPov-fv2.mjs → android-BQiKJYK0.mjs} +1 -1
  5. package/dist/{android-cas-CITmIeWY.mjs → android-cas-DzLPXiEn.mjs} +7 -7
  6. package/dist/{app-install-Cfmqr3TQ.mjs → app-install-CsZi0SAT.mjs} +1 -1
  7. package/dist/{workspaces-wrH6dXfu.mjs → budget-DbzW5VTD.mjs} +651 -32
  8. package/dist/{dev-client-DExzPiOa.mjs → build-plan-CqntbR7b.mjs} +514 -38
  9. package/dist/build-progress-h60GPV27.mjs +179 -0
  10. package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +16 -16
  13. package/dist/collector-run.mjs +4 -4
  14. package/dist/{command-output-D5z2_bcF.mjs → command-output-BWCj5Vh-.mjs} +1 -1
  15. package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
  16. package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
  17. package/dist/{deps-DP4eWqea.mjs → deps-CO-_7UOm.mjs} +2 -2
  18. package/dist/{device-Czh5kO8A.mjs → device-9r9xRYDa.mjs} +49 -11
  19. package/dist/{device-lease-CbEaIk7b.mjs → device-lease-B9LAXIUH.mjs} +55 -55
  20. package/dist/{device-pool-BBWLAr55.mjs → device-pool-BoezJd-c.mjs} +4 -4
  21. package/dist/{device-remote-powUHdbK.mjs → device-remote-C0_o-tJH.mjs} +254 -579
  22. package/dist/{doctor-CmqK1LcQ.mjs → doctor-LfDlbR-N.mjs} +20 -19
  23. package/dist/{error-diagnostics-xxWKNO3-.mjs → error-diagnostics-CCxjn31V.mjs} +7 -7
  24. package/dist/{gc-zCDpmdHU.mjs → gc-C4nQFcTX.mjs} +220 -282
  25. package/dist/{guide-DwmaD_Wa.mjs → guide-BPBlh9ML.mjs} +512 -73
  26. package/dist/{guide-status-CWvti2Lg.mjs → guide-status-i9ogHt4v.mjs} +2 -2
  27. package/dist/{idle--jn7pBNe.mjs → idle-S3tFZ53_.mjs} +7 -6
  28. package/dist/{in-use-CNIWU-r-.mjs → in-use-D9XYEgFD.mjs} +7 -7
  29. package/dist/{ios-CbdqTqOc.mjs → ios-CDzWL1mj.mjs} +302 -88
  30. package/dist/{ios-COGcGEcN.mjs → ios-CZWOHTTc.mjs} +24 -19
  31. package/dist/{ios-device-DNjPXBnb.mjs → ios-device-D9cLxVr5.mjs} +1 -1
  32. package/dist/{launch-verify-BhgkYAws.mjs → launch-verify-B2oR_kFN.mjs} +2 -2
  33. package/dist/logs-SzuYudOj.mjs +485 -0
  34. package/dist/merge-state-ytFQCeh4.mjs +262 -0
  35. package/dist/{metro-BWLOJ_Wr.mjs → metro-Dzo30hvJ.mjs} +1 -1
  36. package/dist/{named-ports-DiTfSz6p.mjs → named-ports-Dn1o61D9.mjs} +3 -3
  37. package/dist/{ownership-BfPNRf3-.mjs → ownership-B8d4kXBx.mjs} +49 -16
  38. package/dist/{ownership-D2xbLVoz.mjs → ownership-Dqnrjrio.mjs} +2 -2
  39. package/dist/{ports-CLSSFOuz.mjs → ports-Dp_QTwTC.mjs} +3 -3
  40. package/dist/{project-BXZzOABt.mjs → project-B8rbutVC.mjs} +4 -72
  41. package/dist/{reload-CymyF6Kk.mjs → reload-nocmx8KB.mjs} +9 -9
  42. package/dist/remote-cache-BRsRZru5.mjs +526 -0
  43. package/dist/{server-bare-DVbb7kXH.mjs → server-bare-Gn3ssSdK.mjs} +1 -1
  44. package/dist/{server-expo-BvOCeakp.mjs → server-expo-CeT1nIIk.mjs} +2 -2
  45. package/dist/server-expo-eWi9Coof.mjs +2 -0
  46. package/dist/{settings-DOugPlPz.mjs → settings-BlyDn2Oz.mjs} +14 -5
  47. package/dist/{settings-EWYNwe72.mjs → settings-bE-Gu3Ti.mjs} +23 -11
  48. package/dist/settings.schema.json +73 -1
  49. package/dist/{simslim-DZUxzsjq.mjs → simslim-COtEEP7x.mjs} +152 -96
  50. package/dist/{slot-launch-Dk4oTREM.mjs → slot-launch-DIACGEfH.mjs} +4 -4
  51. package/dist/{spawn-claims-CUleC2cM.mjs → spawn-claims-PT8aQpCa.mjs} +25 -2
  52. package/dist/{start-qWapeB4W.mjs → start-BFstVBlx.mjs} +1 -1
  53. package/dist/{start-RbmFPeZ7.mjs → start-BboE1HIS.mjs} +108 -17
  54. package/dist/{state-WGdIbi_d.mjs → state-DcDgzfu3.mjs} +15 -3
  55. package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
  56. package/dist/{stats-DBYJ5bxL.mjs → stats-DiCtmluY.mjs} +4 -4
  57. package/dist/{status-Dr4fRkym.mjs → status-BUASYyy_.mjs} +92 -33
  58. package/dist/{status-Zp8kvexe.mjs → status-DI9Tjo7T.mjs} +255 -38
  59. package/dist/stim-desktop-D0xhpZt8.mjs +29 -0
  60. package/dist/stop-BI7QY76M.mjs +3 -0
  61. package/dist/{stop-DcEv5sk-.mjs → stop-Bn6v-GvS.mjs} +205 -37
  62. package/dist/supervisor-run.mjs +11 -11
  63. package/dist/{workspace-process-lock-BXA96myD.mjs → workspace-process-lock-BvvJTfo_.mjs} +10 -5
  64. package/dist/{worktree-DEG2o9F8.mjs → worktree-B5sTRskL.mjs} +1 -1
  65. package/dist/{worktree-Bd4dRvx7.mjs → worktree-ChDL5fO1.mjs} +17 -16
  66. package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
  67. package/dist/{xcode-B2tBkf0m.mjs → xcode-BzplpQzc.mjs} +7 -7
  68. package/package.json +5 -5
  69. package/dist/budget-nDCEwtJv.mjs +0 -620
  70. package/dist/logs-MARIy6m9.mjs +0 -209
  71. package/dist/server-expo-DBVTdult.mjs +0 -2
  72. package/dist/stop-DI4BG4Os.mjs +0 -3
@@ -1,22 +1,24 @@
1
- import { A as directorySize, F as volumeRootFor, M as isOnMountedVolume, N as listMountedVolumes, b as removeProject, g as loadConfig, j as formatBytes, k as ownership_claim_exports } from "./config-BEq0Qw_a.mjs";
1
+ import { A as ownership_claim_exports, I as volumeRootFor, M as formatBytes, N as isOnMountedVolume, P as listMountedVolumes, U as projectDeviceSlots, _ as loadConfig$1, j as directorySize, p as getProject, x as removeProject } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { N as workspaceStateFile, O as workspaceDir, v as workspaceLastUsed } from "./metro-BWLOJ_Wr.mjs";
4
- import { C as sourceCheckoutOf, b as resolveFullRef, d as hasUncommittedWork, l as hasPopulatedSubmodules, m as listWorktrees, o as dirtyPaths, w as unpushedCommits } from "./worktree-BlizKDpZ.mjs";
5
- import { a as detectIsExpo, o as findProjectRoot } from "./project-BXZzOABt.mjs";
6
- import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-D5z2_bcF.mjs";
7
- import { d as removeExpiredLease, h as listBuildLocks } from "./device-lease-CbEaIk7b.mjs";
8
- import { A as ownedAvdDirectory, C as isStimOwnedAvdName, D as listOrphanedAvdDirectories, K as isStimOwnedSim, T as listAvds } from "./android-787HhP5w.mjs";
9
- import { h as POOL_SETTING_REMEDY, y as parkedMaxSetting } from "./ownership-BfPNRf3-.mjs";
10
- import { l as listAllIosSims } from "./ios-COGcGEcN.mjs";
11
- import { a as parseIdleDuration, o as shutDownIdleDevices, r as idleDeviceLines, t as collectIdleDevices } from "./idle--jn7pBNe.mjs";
12
- import { i as canonicalPath, o as listBuildSlots, r as workspaceInUse } from "./in-use-CNIWU-r-.mjs";
13
- import { C as listOwnedSessionsArgs, D as verifyStoppedSession, E as stopSessionArgs, S as isDefinitiveMissingSessionError, T as parseSessionListPage, b as getSessionArgs, d as withRemoteSessionLock, f as withEasProjectLock, h as removeEasSessionClaim, m as readEasSessionLedger, p as easMachineStateRoot, x as inspectSessionForTeardown, y as findOrphanedOwnedSessions, z as resolveEasCliBin } from "./device-remote-powUHdbK.mjs";
14
- import { A as discoverCaches, C as unverifiedDeviceCommand, D as planCacheEmptying, E as includesWorkspaceOutputs, O as selectCaches, S as findStaleProjectDevices, T as emptyCaches, _ as describeParkedSims, a as collectWorkspaceOutputs, b as findOrphanedDevices, c as listWorkspaceDirs, d as collectDeviceLeases, f as collectParkedAvds, g as deleteProjectDevices, h as deleteParkedSims, i as collectOrphanedWorkspaces, j as sizeCaches, k as trimCaches, l as sizeText, m as deleteParkedAvds, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, p as collectParkedSims, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as DEVICE_LIST_TIMEOUT_MS, v as describeUnverifiableDevices, w as withAndroidAvdSizes, x as findStaleDeviceRecords, y as deviceSweepIsScoped } from "./workspaces-wrH6dXfu.mjs";
15
- import { a as reclaimKeys, d as removeWorktreeTarget, m as reclaimProject, n as excludePodChurn, p as describeDereferenced, r as matchWorktreeEntry } from "./worktree-Bd4dRvx7.mjs";
16
- import { statSync } from "node:fs";
3
+ import { N as workspaceStateFile, O as workspaceDir, j as workspaceLogsDir, v as workspaceLastUsed } from "./metro-Dzo30hvJ.mjs";
4
+ import { C as sourceCheckoutOf, b as resolveFullRef, d as hasUncommittedWork, l as hasPopulatedSubmodules, m as listWorktrees, o as dirtyPaths, w as unpushedCommits } from "./worktree-DeBQSVtR.mjs";
5
+ import { i as findProjectRoot, r as detectIsExpo } from "./project-B8rbutVC.mjs";
6
+ import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-BWCj5Vh-.mjs";
7
+ import { d as removeExpiredLease, h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-B9LAXIUH.mjs";
8
+ import { A as listOrphanedAvdDirectories, L as ownedAvdSerialResolver, O as listAvds, P as ownedAvdDirectory, Q as isStimOwnedSim, T as isStimOwnedAvdName } from "./android-75Sk2rIR.mjs";
9
+ import { h as POOL_SETTING_REMEDY, y as parkedMaxSetting } from "./ownership-B8d4kXBx.mjs";
10
+ import { l as listAllIosSims } from "./ios-CZWOHTTc.mjs";
11
+ import { a as parseIdleDuration, o as shutDownIdleDevices, r as idleDeviceLines, t as collectIdleDevices } from "./idle-S3tFZ53_.mjs";
12
+ import { i as canonicalPath, o as listBuildSlots, r as workspaceInUse } from "./in-use-D9XYEgFD.mjs";
13
+ import { f as resolveEasCliBin } from "./remote-cache-BRsRZru5.mjs";
14
+ import { A as findOrphanedOwnedSessions, C as withEasProjectLock, E as removeEasSessionClaim, I as parseSessionListPage, L as stopSessionArgs, M as inspectSessionForTeardown, N as isDefinitiveMissingSessionError, P as listOwnedSessionsArgs, R as verifyStoppedSession, T as readEasSessionLedger$1, d as withRemoteSessionLock, j as getSessionArgs, w as easMachineStateRoot$1 } from "./device-remote-C0_o-tJH.mjs";
15
+ import { A as includesWorkspaceOutputs, C as deviceSweepIsScoped, D as unverifiedDeviceCommand, E as findStaleProjectDevices, F as sizeCaches, M as selectCaches, N as trimCaches, O as withAndroidAvdSizes, P as discoverCaches, S as describeUnverifiableDevices, T as findStaleDeviceRecords, _ as collectParkedSims, a as REBUILD_COST, b as deleteProjectDevices, c as collectOrphanedWorkspaces, d as isInsideWorkspaces, f as listWorkspaceDirs, g as collectParkedAvds, h as collectDeviceLeases, i as machineNumber, j as planCacheEmptying, k as emptyCaches, l as collectWorkspaceOutputs, m as DEVICE_LIST_TIMEOUT_MS, o as WORKSPACE_OUTPUT_DIRS, p as sizeText, s as clearWorkspaceOutputs, u as deleteOrphanedWorkspaces, v as deleteParkedAvds, w as findOrphanedDevices, x as describeParkedSims, y as deleteParkedSims } from "./budget-DbzW5VTD.mjs";
16
+ import { a as reclaimKeys, d as removeWorktreeTarget, m as reclaimProject, n as excludePodChurn, p as describeDereferenced, r as matchWorktreeEntry } from "./worktree-ChDL5fO1.mjs";
17
+ import { n as mergeState, t as fetchDefaultBranch } from "./merge-state-ytFQCeh4.mjs";
17
18
  import { InvalidArgumentError } from "commander";
18
- import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, statSync as statSync$1 } from "fs";
19
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
19
20
  import { basename, dirname, isAbsolute, join } from "path";
21
+ import { settingDefinition } from "@stim-cli/core/state";
20
22
  import chalk from "chalk";
21
23
  //#region src/commands/gc/eas-sessions.ts
22
24
  const EAS_OPERATION_TIMEOUT_MS = 3e4;
@@ -37,18 +39,18 @@ function readRecordedRemoteSessionIds(roots) {
37
39
  let safe = true;
38
40
  for (const root of new Set(roots)) {
39
41
  try {
40
- if (!statSync$1(root).isDirectory()) throw new Error("path is not a directory");
41
- readdirSync$1(root);
42
+ if (!statSync(root).isDirectory()) throw new Error("path is not a directory");
43
+ readdirSync(root);
42
44
  } catch (error) {
43
45
  safe = false;
44
46
  notices.push(`${root} is not available as a readable workspace directory: ${describeError(error)}`);
45
47
  continue;
46
48
  }
47
49
  const path = workspaceStateFile(root);
48
- if (!existsSync$1(path)) continue;
50
+ if (!existsSync(path)) continue;
49
51
  let state;
50
52
  try {
51
- state = JSON.parse(readFileSync$1(path, "utf-8"));
53
+ state = JSON.parse(readFileSync(path, "utf-8"));
52
54
  } catch (error) {
53
55
  safe = false;
54
56
  notices.push(`${path} could not be read as valid JSON: ${describeError(error)}`);
@@ -84,8 +86,8 @@ function claimStateAbsence(claim) {
84
86
  const stateDir = dirname(claim.stateFile);
85
87
  let entries;
86
88
  try {
87
- if (!statSync$1(stateDir).isDirectory()) throw new Error("path is not a directory");
88
- entries = readdirSync$1(stateDir);
89
+ if (!statSync(stateDir).isDirectory()) throw new Error("path is not a directory");
90
+ entries = readdirSync(stateDir);
89
91
  } catch (error) {
90
92
  return {
91
93
  absent: false,
@@ -98,7 +100,7 @@ function claimStateAbsence(claim) {
98
100
  };
99
101
  let state;
100
102
  try {
101
- state = JSON.parse(readFileSync$1(claim.stateFile, "utf-8"));
103
+ state = JSON.parse(readFileSync(claim.stateFile, "utf-8"));
102
104
  } catch (error) {
103
105
  return {
104
106
  absent: false,
@@ -141,8 +143,8 @@ function collectEasSessionSweep(config, deps) {
141
143
  const eas = (deps.resolveEasCliBin ?? resolveEasCliBin)(projectRoot);
142
144
  if (!eas) return empty(["EAS session sweep skipped: eas-cli is not available for the current Expo project."]);
143
145
  const recorded = readRecordedRemoteSessionIds([...Object.keys(config?.projects ?? {}), projectRoot]);
144
- const ledgerRoot = deps.easLedgerRoot ?? easMachineStateRoot();
145
- const ledger = readEasSessionLedger(ledgerRoot);
146
+ const ledgerRoot = deps.easLedgerRoot ?? easMachineStateRoot$1();
147
+ const ledger = readEasSessionLedger$1(ledgerRoot);
146
148
  const run = deps.runEasFile ?? ((file, args, options) => getExecutor().runFile(file, args, options));
147
149
  const sessions = [];
148
150
  const seenCursors = /* @__PURE__ */ new Set();
@@ -180,7 +182,7 @@ function collectEasSessionSweep(config, deps) {
180
182
  }
181
183
  const lockedRoots = deps.easLockedRoots;
182
184
  if (lockedRoots) {
183
- if (easLockRoots(projectRoot, loadConfig()).some((root) => !lockedRoots.includes(root))) return empty([...recorded.notices, "EAS session sweep skipped: registered workspace roots changed after remote-session locks were acquired."]);
185
+ if (easLockRoots(projectRoot, loadConfig$1()).some((root) => !lockedRoots.includes(root))) return empty([...recorded.notices, "EAS session sweep skipped: registered workspace roots changed after remote-session locks were acquired."]);
184
186
  }
185
187
  const compared = findOrphanedOwnedSessions({
186
188
  sessions,
@@ -215,8 +217,8 @@ function easLockRoots(projectRoot, config) {
215
217
  }
216
218
  function unavailableLockRootNotice(roots) {
217
219
  for (const root of roots) try {
218
- if (!statSync$1(root).isDirectory()) throw new Error("path is not a directory");
219
- readdirSync$1(root);
220
+ if (!statSync(root).isDirectory()) throw new Error("path is not a directory");
221
+ readdirSync(root);
220
222
  } catch (error) {
221
223
  return `EAS session sweep skipped: ${root} is not available for remote-session lock inspection: ${describeError(error)}`;
222
224
  }
@@ -225,7 +227,7 @@ function unavailableLockRootNotice(roots) {
225
227
  function malformedRemoteSessionLockNotice(roots) {
226
228
  for (const root of roots) {
227
229
  const lockPath = join(workspaceDir(root), "remote-session.lock");
228
- if (!existsSync$1(lockPath)) continue;
230
+ if (!existsSync(lockPath)) continue;
229
231
  if ((0, ownership_claim_exports.readClaimSet)(lockPath).unresolved.length === 0) continue;
230
232
  return `EAS session sweep skipped: the remote-session lock at ${lockPath} has a malformed owner record.`;
231
233
  }
@@ -236,7 +238,7 @@ async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
236
238
  const waitMs = deps.easLockWaitMs ?? EAS_LOCK_WAIT_MS;
237
239
  const maxSnapshots = deps.easLockSnapshotRetries ?? EAS_LOCK_SNAPSHOT_RETRIES;
238
240
  for (let attempt = 0; attempt < maxSnapshots; attempt++) {
239
- const roots = easLockRoots(projectRoot, loadConfig());
241
+ const roots = easLockRoots(projectRoot, loadConfig$1());
240
242
  const unavailable = unavailableLockRootNotice(roots);
241
243
  if (unavailable) return fn({
242
244
  ...deps,
@@ -250,7 +252,7 @@ async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
250
252
  let expanded = false;
251
253
  let coreStarted = false;
252
254
  const acquire = (index) => index === roots.length ? (async () => {
253
- if (easLockRoots(projectRoot, loadConfig()).some((root) => !roots.includes(root))) {
255
+ if (easLockRoots(projectRoot, loadConfig$1()).some((root) => !roots.includes(root))) {
254
256
  expanded = true;
255
257
  return null;
256
258
  }
@@ -293,12 +295,12 @@ async function deleteEasSessions(easSessionSweep, deps) {
293
295
  return;
294
296
  }
295
297
  const coordinatedDeps = lockedDeps;
296
- const currentConfig = loadConfig();
298
+ const currentConfig = loadConfig$1();
297
299
  const currentRoots = easLockRoots(projectScope, currentConfig);
298
300
  const registryExpanded = coordinatedDeps.easLockedRoots && currentRoots.some((root) => !coordinatedDeps.easLockedRoots.includes(root));
299
301
  const currentRecords = registryExpanded ? null : readRecordedRemoteSessionIds([...Object.keys(currentConfig?.projects ?? {}), projectScope]);
300
- const ledgerRoot = coordinatedDeps.easLedgerRoot ?? easMachineStateRoot();
301
- const ledger = readEasSessionLedger(ledgerRoot);
302
+ const ledgerRoot = coordinatedDeps.easLedgerRoot ?? easMachineStateRoot$1();
303
+ const ledger = readEasSessionLedger$1(ledgerRoot);
302
304
  const removeClaim = coordinatedDeps.removeEasSessionClaim ?? removeEasSessionClaim;
303
305
  const reconcileClaim = (sessionId) => {
304
306
  let detail = "the claim store did not remove it";
@@ -399,236 +401,11 @@ async function deleteEasSessions(easSessionSweep, deps) {
399
401
  return deleteFailures;
400
402
  }
401
403
  //#endregion
402
- //#region src/workspace/merge-state.ts
403
- const FETCH_TIMEOUT_MS = 3e4;
404
- const FETCH_FRESH_MS = 6e5;
405
- const GIT_TIMEOUT_MS = 6e4;
406
- const REMOTE_PREFIX = "refs/remotes/origin/";
407
- function failure(error) {
408
- const { stderr, message } = error;
409
- const text = String(stderr ?? "").trim() || String(message ?? error);
410
- return text.split("\n")[0] ?? text;
411
- }
412
- function fetchedRecently(repo, now) {
413
- const path = getExecutor().runFileQuiet("git", [
414
- "-C",
415
- repo,
416
- "rev-parse",
417
- "--path-format=absolute",
418
- "--git-path",
419
- "FETCH_HEAD"
420
- ])?.trim();
421
- try {
422
- return Boolean(path) && now - statSync(path).mtimeMs < FETCH_FRESH_MS;
423
- } catch {
424
- return false;
425
- }
426
- }
427
- /**
428
- * Fetches the default branch that `origin/HEAD` names into its remote-tracking ref, bounded by a timeout and never
429
- * prompting for credentials. It skips the fetch when the checkout fetched anything in the last 10 minutes.
430
- */
431
- function fetchDefaultBranch(repo, now = Date.now()) {
432
- const exec = getExecutor();
433
- const ref = exec.runFileQuiet("git", [
434
- "-C",
435
- repo,
436
- "symbolic-ref",
437
- "--quiet",
438
- "refs/remotes/origin/HEAD"
439
- ])?.trim();
440
- if (!ref?.startsWith(REMOTE_PREFIX)) return { error: `origin/HEAD is not set; run \`git -C ${repo} remote set-head origin --auto\`` };
441
- const branch = ref.slice(20);
442
- if (fetchedRecently(repo, now)) return {
443
- ref,
444
- name: `origin/${branch}`
445
- };
446
- try {
447
- exec.runFile("git", [
448
- "-C",
449
- repo,
450
- "fetch",
451
- "--quiet",
452
- "--no-tags",
453
- "--no-recurse-submodules",
454
- "origin",
455
- `+refs/heads/${branch}:${ref}`
456
- ], {
457
- timeoutMs: FETCH_TIMEOUT_MS,
458
- env: { GIT_TERMINAL_PROMPT: "0" }
459
- });
460
- } catch (error) {
461
- return { error: `git fetch origin ${branch} failed: ${failure(error)}` };
462
- }
463
- return {
464
- ref,
465
- name: `origin/${branch}`
466
- };
467
- }
468
- function notMerged(detail) {
469
- return {
470
- merged: false,
471
- unknown: false,
472
- detail
473
- };
474
- }
475
- function currentBranch(path) {
476
- const ref = getExecutor().runFileQuiet("git", [
477
- "-C",
478
- path,
479
- "symbolic-ref",
480
- "--quiet",
481
- "HEAD"
482
- ])?.trim();
483
- return ref?.startsWith("refs/heads/") ? ref : null;
484
- }
485
- function upstreamGone(path, branch) {
486
- if (!branch) return false;
487
- const [upstream, state] = (getExecutor().runFileQuiet("git", [
488
- "-C",
489
- path,
490
- "for-each-ref",
491
- "--format=%(upstream)%00%(upstream:track)",
492
- branch
493
- ]) ?? "").trim().split("\0");
494
- return Boolean(upstream) && state === "[gone]";
495
- }
496
- function committedOn(path, branch, head) {
497
- if (!branch) return false;
498
- const exec = getExecutor();
499
- return (exec.runFileQuiet("git", [
500
- "-C",
501
- path,
502
- "reflog",
503
- "show",
504
- "--format=%H %gs",
505
- branch
506
- ]) ?? "").split("\n").some((entry) => {
507
- const [sha = "", ...subject] = entry.split(" ");
508
- return /^(commit|cherry-pick|rebase|revert)\b/.test(subject.join(" ")) && exec.runFileQuiet("git", [
509
- "-C",
510
- path,
511
- "merge-base",
512
- "--is-ancestor",
513
- sha,
514
- head
515
- ]) !== null;
516
- });
517
- }
518
- /**
519
- * Whether the worktree's HEAD is merged into `target`. The signals, all local git:
520
- * - HEAD is an ancestor of the default branch, off its first-parent line, and the branch's reflog shows a commit made
521
- * on it that HEAD contains, so a merge commit brought the branch's own work in. A branch with no commit of its own
522
- * is not merged.
523
- * - The branch changes the tree, has no merge commits, and every commit since the merge base has the same
524
- * `git patch-id --verbatim` as a commit on the default branch (a rebase merge).
525
- * - The branch changes the tree and its whole diff since the merge base has the same verbatim patch id as a commit on
526
- * the default branch, limited to the files the branch changes (a squash merge).
527
- * Anything git cannot answer is unknown, never merged. `coversUnpushed` is true for a patch-equivalent HEAD whose
528
- * upstream branch was deleted: its commits exist only locally, but their change is on the default branch.
529
- */
530
- function mergeState(path, { ref, name }) {
531
- const git = (args, input) => getExecutor().runFile("git", [
532
- "--literal-pathspecs",
533
- "-C",
534
- path,
535
- ...args
536
- ], {
537
- timeoutMs: GIT_TIMEOUT_MS,
538
- input
539
- });
540
- const patch = (args) => getExecutor().runFile("git", [
541
- "--literal-pathspecs",
542
- "-C",
543
- path,
544
- ...args
545
- ], {
546
- timeoutMs: GIT_TIMEOUT_MS,
547
- untrimmed: true
548
- });
549
- const patchIds = (text) => text ? git(["patch-id", "--verbatim"], text).split("\n").flatMap((line) => line.split(" ")[0] || []) : [];
550
- const diffOptions = ["--no-color", "--no-ext-diff"];
551
- const noOwnCommits = notMerged(`no commits of its own beyond ${name}`);
552
- try {
553
- const head = git([
554
- "rev-parse",
555
- "--verify",
556
- "HEAD^{commit}"
557
- ]);
558
- const base = git([
559
- "merge-base",
560
- head,
561
- ref
562
- ]);
563
- const branch = currentBranch(path);
564
- if (base === head) {
565
- if (git(["rev-parse", ref]) === head || git([
566
- "rev-list",
567
- "--first-parent",
568
- "--parents",
569
- `${head}..${ref}`
570
- ]).split("\n").some((line) => line.split(" ")[1] === head) || !committedOn(path, branch, head)) return noOwnCommits;
571
- return {
572
- merged: true,
573
- into: name,
574
- head,
575
- coversUnpushed: false
576
- };
577
- }
578
- const files = git([
579
- "diff",
580
- "--name-only",
581
- "--no-renames",
582
- "-z",
583
- base,
584
- head
585
- ]).split("\0").filter(Boolean);
586
- if (!files.length) return notMerged(`no net change beyond ${name}`);
587
- const log = (range, pathspec = []) => patch([
588
- "log",
589
- "-p",
590
- "--no-merges",
591
- ...diffOptions,
592
- "--format=commit %H",
593
- range,
594
- "--",
595
- ...pathspec
596
- ]);
597
- const upstream = new Set(patchIds(log(`${base}..${ref}`, files)));
598
- const patchEquivalent = () => ({
599
- merged: true,
600
- into: name,
601
- head,
602
- coversUnpushed: upstreamGone(path, branch)
603
- });
604
- const squash = patchIds(patch([
605
- "diff",
606
- ...diffOptions,
607
- base,
608
- head
609
- ]));
610
- if (squash.length === 1 && upstream.has(squash[0])) return patchEquivalent();
611
- if (!git([
612
- "rev-list",
613
- "--merges",
614
- `${base}..${head}`
615
- ])) {
616
- const own = patchIds(log(`${base}..${head}`));
617
- if (own.length && own.every((id) => upstream.has(id))) return patchEquivalent();
618
- }
619
- return notMerged(`not merged into ${name}`);
620
- } catch (error) {
621
- return {
622
- merged: false,
623
- unknown: true,
624
- detail: `merge state unknown: ${failure(error)}`
625
- };
626
- }
627
- }
628
- //#endregion
629
404
  //#region src/commands/gc/worktrees.ts
630
405
  const DEFAULT_WORKTREE_IDLE_DAYS = 7;
631
406
  const DAY_MS = 864e5;
407
+ const MINUTE_MS = 6e4;
408
+ const GRACE_SETTING = "gc.worktreeGraceMinutes";
632
409
  const MERGE_DECIDES = /* @__PURE__ */ new Set([
633
410
  "unpushed",
634
411
  "not-merged",
@@ -641,7 +418,34 @@ function skip(code, text) {
641
418
  text
642
419
  };
643
420
  }
644
- function worktreeSkipReason(facts, olderThan) {
421
+ /**
422
+ * Why gc keeps a worktree, or null when it removes it. A worktree that would be removed is still kept while its last
423
+ * activity, or its merge into the default branch, is less than `grace.ms` old, and kept when that activity is unknown.
424
+ */
425
+ function worktreeSkipReason(facts, olderThan, grace = {
426
+ ms: 0,
427
+ now: 0
428
+ }) {
429
+ return removalBlocker(facts, olderThan) ?? graceBlocker(facts, grace);
430
+ }
431
+ function graceBlocker(facts, grace) {
432
+ if (grace.ms <= 0) return null;
433
+ if (!facts.activity) return skip("activity-unknown", "its last activity could not be read");
434
+ const merged = facts.merge?.merged ? facts.merge : null;
435
+ const latest = merged && merged.mergedAt > facts.activity.at ? {
436
+ at: merged.mergedAt,
437
+ basis: `merged into ${merged.into}`
438
+ } : facts.activity;
439
+ const eligibleAt = latest.at + grace.ms;
440
+ if (grace.now >= eligibleAt) return null;
441
+ const ago = formatLongDuration(Math.max(0, grace.now - latest.at));
442
+ return {
443
+ code: "recent-activity",
444
+ text: `recent activity: ${latest.basis} ${ago} ago; removable after ${new Date(eligibleAt).toISOString()}`,
445
+ eligibleAt
446
+ };
447
+ }
448
+ function removalBlocker(facts, olderThan) {
645
449
  if (facts.bare) return skip("bare-repository", "bare repository");
646
450
  if (typeof facts.source === "object") return skip("source-checkout-unknown", `source checkout unknown: ${facts.source.refusal}`);
647
451
  if (facts.source === "source") return skip("source-checkout", "source checkout");
@@ -674,7 +478,119 @@ function idleDaysOf(keys, now) {
674
478
  return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS)) : null;
675
479
  }
676
480
  function inUseOf(keys, checks) {
677
- return [...new Set(keys.flatMap((key) => workspaceInUse(key, checks)))];
481
+ return [.../* @__PURE__ */ new Set([...keys.flatMap((key) => workspaceInUse(key, checks)), ...deviceUseOf(keys)])];
482
+ }
483
+ function deviceUseOf(keys) {
484
+ const reasons = [];
485
+ let sims;
486
+ let avdSerial;
487
+ for (const key of keys) {
488
+ let slots;
489
+ try {
490
+ slots = projectDeviceSlots(getProject(key));
491
+ } catch (error) {
492
+ reasons.push(`its device records cannot be read: ${error.message}`);
493
+ continue;
494
+ }
495
+ for (const { platforms } of slots) {
496
+ const ios = platforms.ios;
497
+ if (ios?.owned && ios.deviceUdid) {
498
+ if (sims === void 0) try {
499
+ sims = listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
500
+ } catch {
501
+ sims = null;
502
+ }
503
+ const sim = sims?.find((entry) => entry.udid === ios.deviceUdid);
504
+ if (sims === null) reasons.push(`the state of its owned simulator ${ios.deviceUdid} cannot be read`);
505
+ else if (sim && sim.state !== "Shutdown") reasons.push(`its owned simulator ${sim.name} is ${sim.state}; \`stim stop\` in ${key} shuts it down`);
506
+ }
507
+ const android = platforms.android;
508
+ if (android?.owned && android.avdName) {
509
+ avdSerial ??= ownedAvdSerialResolver({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
510
+ try {
511
+ if (avdSerial(android.avdName).serial) reasons.push(`its owned emulator ${android.avdName} is running; \`stim stop\` in ${key} shuts it down`);
512
+ } catch {
513
+ reasons.push(`the state of its owned emulator ${android.avdName} cannot be read`);
514
+ }
515
+ }
516
+ }
517
+ }
518
+ const holders = new Set(keys.map(canonicalPath));
519
+ const now = Date.now();
520
+ for (const { lease } of listLeaseFiles()) if (lease && !leaseIsExpired(lease, now) && holders.has(canonicalPath(lease.holder))) reasons.push(`it holds the ${lease.platform} device lease on ${lease.deviceName ?? lease.id}`);
521
+ return reasons;
522
+ }
523
+ function newestMtime(paths) {
524
+ let newest = null;
525
+ for (const { path, basis } of paths) {
526
+ let mtime;
527
+ try {
528
+ mtime = statSync(path).mtimeMs;
529
+ } catch (error) {
530
+ if (error.code === "ENOENT") continue;
531
+ return "unreadable";
532
+ }
533
+ if (!newest || mtime > newest.at) newest = {
534
+ at: mtime,
535
+ basis
536
+ };
537
+ }
538
+ return newest;
539
+ }
540
+ function logFiles$1(key) {
541
+ const dir = workspaceLogsDir(key);
542
+ try {
543
+ return readdirSync(dir).map((name) => ({
544
+ path: join(dir, name),
545
+ basis: "a Stim log write"
546
+ }));
547
+ } catch (error) {
548
+ return error.code === "ENOENT" ? [] : null;
549
+ }
550
+ }
551
+ function worktreeActivityOf(path, keys) {
552
+ const gitDir = getExecutor().runFileQuiet("git", [
553
+ "--no-optional-locks",
554
+ "-C",
555
+ path,
556
+ "rev-parse",
557
+ "--path-format=absolute",
558
+ "--git-dir"
559
+ ])?.trim();
560
+ if (!gitDir) return null;
561
+ const paths = [
562
+ {
563
+ path: join(gitDir, "index"),
564
+ basis: "a git index write"
565
+ },
566
+ {
567
+ path: join(gitDir, "HEAD"),
568
+ basis: "a git HEAD change"
569
+ },
570
+ {
571
+ path: join(gitDir, "logs", "HEAD"),
572
+ basis: "a git reflog entry"
573
+ }
574
+ ];
575
+ for (const key of keys) {
576
+ const logs = logFiles$1(key);
577
+ if (logs === null) return null;
578
+ paths.push({
579
+ path: workspaceStateFile(key),
580
+ basis: "a Stim workspace state write"
581
+ }, ...logs);
582
+ }
583
+ const newest = newestMtime(paths);
584
+ return newest === "unreadable" ? null : newest;
585
+ }
586
+ function graceMsSetting() {
587
+ const { value, error } = machineNumber(GRACE_SETTING, loadConfig$1(), process.env);
588
+ if (error) {
589
+ const fallback = Number(settingDefinition(GRACE_SETTING)?.default);
590
+ console.error(chalk.yellow(`${error} gc uses the default of ${fallback} minutes.`));
591
+ return fallback * MINUTE_MS;
592
+ }
593
+ return (value ?? 0) * MINUTE_MS;
678
594
  }
679
595
  function porcelainOf(path, gitAnswered) {
680
596
  if (gitAnswered === null) return null;
@@ -683,15 +599,15 @@ function porcelainOf(path, gitAnswered) {
683
599
  return lines.length ? lines : null;
684
600
  }
685
601
  function candidateRoots() {
686
- const registered = Object.keys(loadConfig()?.projects ?? {}).filter(isAbsolute);
602
+ const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute);
687
603
  const recorded = listWorkspaceDirs().flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : []);
688
- return [.../* @__PURE__ */ new Set([...registered, ...recorded])].filter((root) => existsSync$1(root)).toSorted();
604
+ return [.../* @__PURE__ */ new Set([...registered, ...recorded])].filter((root) => existsSync(root)).toSorted();
689
605
  }
690
- function checkMergeStates(pending, idle, now) {
606
+ function checkMergeStates(pending, idle, grace) {
691
607
  const repos = /* @__PURE__ */ new Map();
692
608
  for (const entry of pending) repos.set(entry.repo, [...repos.get(entry.repo) ?? [], entry]);
693
609
  for (const [repo, entries] of repos) {
694
- const target = fetchDefaultBranch(repo, now);
610
+ const target = fetchDefaultBranch(repo, grace.now);
695
611
  for (const { candidate, facts } of entries) {
696
612
  const merge = "error" in target ? {
697
613
  merged: false,
@@ -701,11 +617,12 @@ function checkMergeStates(pending, idle, now) {
701
617
  const verdict = worktreeSkipReason({
702
618
  ...facts,
703
619
  merge
704
- }, idle);
620
+ }, idle, grace);
705
621
  Object.assign(candidate, {
706
622
  merge,
707
623
  skipCode: verdict?.code ?? null,
708
- skipped: verdict?.text ?? null
624
+ skipped: verdict?.text ?? null,
625
+ eligibleAt: verdict?.eligibleAt ?? null
709
626
  });
710
627
  }
711
628
  }
@@ -716,6 +633,10 @@ function checkMergeStates(pending, idle, now) {
716
633
  */
717
634
  function collectWorktreeSweep({ idle, olderThan, now }) {
718
635
  const days = idle ? olderThan ?? DEFAULT_WORKTREE_IDLE_DAYS : null;
636
+ const grace = {
637
+ ms: graceMsSetting(),
638
+ now
639
+ };
719
640
  const groups = /* @__PURE__ */ new Map();
720
641
  const outside = [];
721
642
  for (const root of candidateRoots()) {
@@ -727,7 +648,8 @@ function collectWorktreeSweep({ idle, olderThan, now }) {
727
648
  idleDays: null,
728
649
  merge: null,
729
650
  skipCode: "not-a-worktree",
730
- skipped: "not inside a git worktree"
651
+ skipped: "not inside a git worktree",
652
+ eligibleAt: null
731
653
  });
732
654
  continue;
733
655
  }
@@ -742,6 +664,7 @@ function collectWorktreeSweep({ idle, olderThan, now }) {
742
664
  const keys = [.../* @__PURE__ */ new Set([...roots, ...reclaimKeys(path)])];
743
665
  const idleDays = idleDaysOf(keys, now);
744
666
  const linked = !("refusal" in source) && entry !== null && source.path !== entry.path;
667
+ const activity = linked && grace.ms > 0 ? worktreeActivityOf(path, keys) : null;
745
668
  const gitAnswered = linked ? hasUncommittedWork(path) : null;
746
669
  const facts = {
747
670
  source: "refusal" in source ? source : linked ? "linked" : "source",
@@ -752,16 +675,18 @@ function collectWorktreeSweep({ idle, olderThan, now }) {
752
675
  submodules: linked && hasPopulatedSubmodules(path),
753
676
  inUse: linked ? inUseOf(keys, { managedLocks: true }) : [],
754
677
  idleDays,
755
- merge: null
678
+ merge: null,
679
+ activity
756
680
  };
757
- const verdict = worktreeSkipReason(facts, days);
681
+ const verdict = worktreeSkipReason(facts, days, grace);
758
682
  const candidate = {
759
683
  path,
760
684
  keys,
761
685
  idleDays,
762
686
  merge: null,
763
687
  skipCode: verdict?.code ?? null,
764
- skipped: verdict?.text ?? null
688
+ skipped: verdict?.text ?? null,
689
+ eligibleAt: verdict?.eligibleAt ?? null
765
690
  };
766
691
  if (verdict && MERGE_DECIDES.has(verdict.code) && !("refusal" in source)) pending.push({
767
692
  candidate,
@@ -770,13 +695,14 @@ function collectWorktreeSweep({ idle, olderThan, now }) {
770
695
  });
771
696
  worktrees.push(candidate);
772
697
  }
773
- checkMergeStates(pending, days, now);
698
+ checkMergeStates(pending, days, grace);
774
699
  const listed = [...worktrees, ...outside].filter((w) => idle || w.skipCode !== "not-a-worktree" && w.skipCode !== "source-checkout");
775
700
  return {
776
701
  idle: days === null ? null : {
777
702
  olderThan: days,
778
703
  defaulted: olderThan === null
779
704
  },
705
+ graceMs: grace.ms,
780
706
  worktrees: listed
781
707
  };
782
708
  }
@@ -795,6 +721,16 @@ async function removeWorktrees(sweep, { now = Date.now() } = {}) {
795
721
  const idleDays = idleDaysOf(keys, now);
796
722
  if (idleDays === null || !sweep.idle || idleDays < sweep.idle.olderThan) reasons.push(`used ${idleDays === null ? "at an unknown time" : `${idleDays}d ago`} since gc checked it`);
797
723
  }
724
+ if (sweep.graceMs > 0) {
725
+ const recent = graceBlocker({
726
+ merge: candidate.merge,
727
+ activity: worktreeActivityOf(candidate.path, keys)
728
+ }, {
729
+ ms: sweep.graceMs,
730
+ now: Date.now()
731
+ });
732
+ if (recent) reasons.push(recent.text);
733
+ }
798
734
  kept = reasons;
799
735
  return reasons;
800
736
  };
@@ -851,7 +787,7 @@ function formatParkedAvdReport(parkedAvds, now) {
851
787
  lines.push(`Parked emulators (${parkedAvds.length}):`);
852
788
  for (const avd of parkedAvds) {
853
789
  const listed = avd.listed === false ? " - not on this machine" : avd.listed === null ? " - listing unavailable; kept" : "";
854
- lines.push(` android ${avd.name} ${avd.systemImage} ${parkedAge(avd.parkedAt, now)}${avd.bytes === null ? "" : ` ${formatBytes(avd.bytes)}`}${listed}`);
790
+ lines.push(` android ${avd.name} ${avd.systemImage}${avd.deviceProfile ? ` ${avd.deviceProfile}` : ""} ${parkedAge(avd.parkedAt, now)}${avd.bytes === null ? "" : ` ${formatBytes(avd.bytes)}`}${listed}`);
855
791
  }
856
792
  lines.push(" --delete attempts verified deletions and keeps failures.");
857
793
  return lines;
@@ -1136,7 +1072,8 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1136
1072
  mergedInto: w.merge?.merged ? w.merge.into : null,
1137
1073
  willRemove: w.skipCode === null,
1138
1074
  reason: w.skipCode,
1139
- detail: w.skipped ?? worktreeRemovalReason(w)
1075
+ detail: w.skipped ?? worktreeRemovalReason(w),
1076
+ eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString()
1140
1077
  })),
1141
1078
  parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, bytes, listed }) => ({
1142
1079
  udid,
@@ -1147,9 +1084,10 @@ function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [],
1147
1084
  bytes,
1148
1085
  listed
1149
1086
  })),
1150
- parkedEmulators: parkedAvds.map(({ name, systemImage, parkedAt, bytes, listed }) => ({
1087
+ parkedEmulators: parkedAvds.map(({ name, systemImage, deviceProfile, parkedAt, bytes, listed }) => ({
1151
1088
  name,
1152
1089
  systemImage,
1090
+ deviceProfile,
1153
1091
  parkedAt,
1154
1092
  bytes,
1155
1093
  listed
@@ -1311,7 +1249,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1311
1249
  all
1312
1250
  };
1313
1251
  const mountedVolumes = listMountedVolumes();
1314
- const cfg = loadConfig();
1252
+ const cfg = loadConfig$1();
1315
1253
  let easSessionSweep = deps.precollectedEasSessionSweep;
1316
1254
  if (!easSessionSweep) try {
1317
1255
  easSessionSweep = collectEasSessionSweep(cfg, deps);
@@ -1344,7 +1282,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1344
1282
  else invalidProjects.push(path);
1345
1283
  continue;
1346
1284
  }
1347
- if (existsSync$1(path)) continue;
1285
+ if (existsSync(path)) continue;
1348
1286
  if (!isOnMountedVolume(path, mountedVolumes)) {
1349
1287
  const volume = volumeRootFor(path);
1350
1288
  skipped.push({
@@ -1567,7 +1505,7 @@ async function sweep(opts, deps) {
1567
1505
  try {
1568
1506
  easSessionSweep = await withProjectLock(projectRoot, () => {
1569
1507
  sweepStarted = true;
1570
- return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => Promise.resolve(collectEasSessionSweep(loadConfig(), coordinatedDeps)));
1508
+ return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => Promise.resolve(collectEasSessionSweep(loadConfig$1(), coordinatedDeps)));
1571
1509
  }, {
1572
1510
  waitMs: 0,
1573
1511
  ownerPurpose: "EAS orphan sweep",
@@ -1611,7 +1549,7 @@ async function runGcCore(opts, deps) {
1611
1549
  const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches } = report;
1612
1550
  const actionable = deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length > 0 || orphanedDevices.length > 0 || staleDevices.length > 0 || staleDeviceRecords.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0;
1613
1551
  const idle = opts.idle ?? null;
1614
- const idleFailures = idle === null ? 0 : shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects)).failures;
1552
+ const idleFailures = idle === null ? 0 : shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects)).failures;
1615
1553
  if (idleFailures) process.exitCode = 1;
1616
1554
  const payload = (failures) => ({
1617
1555
  mode: opts.delete ? "delete" : "dry-run",
@@ -1634,7 +1572,7 @@ async function runGcCore(opts, deps) {
1634
1572
  deleteFailures += deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
1635
1573
  removeInvalidProjectEntries(invalidProjects);
1636
1574
  for (const path of deadProjects) {
1637
- if (existsSync$1(path) || !isOnMountedVolume(path)) {
1575
+ if (existsSync(path) || !isOnMountedVolume(path)) {
1638
1576
  console.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
1639
1577
  continue;
1640
1578
  }