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.
- package/dist/{build-progress-BXx-ZBe4.mjs → activity-CCZJs__P.mjs} +9 -182
- package/dist/{android-787HhP5w.mjs → android-75Sk2rIR.mjs} +81 -19
- package/dist/{android-Bu4mbDzc.mjs → android-B7X-8H7h.mjs} +426 -106
- package/dist/{android-DPov-fv2.mjs → android-BQiKJYK0.mjs} +1 -1
- package/dist/{android-cas-CITmIeWY.mjs → android-cas-DzLPXiEn.mjs} +7 -7
- package/dist/{app-install-Cfmqr3TQ.mjs → app-install-CsZi0SAT.mjs} +1 -1
- package/dist/{workspaces-wrH6dXfu.mjs → budget-DbzW5VTD.mjs} +651 -32
- package/dist/{dev-client-DExzPiOa.mjs → build-plan-CqntbR7b.mjs} +514 -38
- package/dist/build-progress-h60GPV27.mjs +179 -0
- package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +16 -16
- package/dist/collector-run.mjs +4 -4
- package/dist/{command-output-D5z2_bcF.mjs → command-output-BWCj5Vh-.mjs} +1 -1
- package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
- package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
- package/dist/{deps-DP4eWqea.mjs → deps-CO-_7UOm.mjs} +2 -2
- package/dist/{device-Czh5kO8A.mjs → device-9r9xRYDa.mjs} +49 -11
- package/dist/{device-lease-CbEaIk7b.mjs → device-lease-B9LAXIUH.mjs} +55 -55
- package/dist/{device-pool-BBWLAr55.mjs → device-pool-BoezJd-c.mjs} +4 -4
- package/dist/{device-remote-powUHdbK.mjs → device-remote-C0_o-tJH.mjs} +254 -579
- package/dist/{doctor-CmqK1LcQ.mjs → doctor-LfDlbR-N.mjs} +20 -19
- package/dist/{error-diagnostics-xxWKNO3-.mjs → error-diagnostics-CCxjn31V.mjs} +7 -7
- package/dist/{gc-zCDpmdHU.mjs → gc-C4nQFcTX.mjs} +220 -282
- package/dist/{guide-DwmaD_Wa.mjs → guide-BPBlh9ML.mjs} +512 -73
- package/dist/{guide-status-CWvti2Lg.mjs → guide-status-i9ogHt4v.mjs} +2 -2
- package/dist/{idle--jn7pBNe.mjs → idle-S3tFZ53_.mjs} +7 -6
- package/dist/{in-use-CNIWU-r-.mjs → in-use-D9XYEgFD.mjs} +7 -7
- package/dist/{ios-CbdqTqOc.mjs → ios-CDzWL1mj.mjs} +302 -88
- package/dist/{ios-COGcGEcN.mjs → ios-CZWOHTTc.mjs} +24 -19
- package/dist/{ios-device-DNjPXBnb.mjs → ios-device-D9cLxVr5.mjs} +1 -1
- package/dist/{launch-verify-BhgkYAws.mjs → launch-verify-B2oR_kFN.mjs} +2 -2
- package/dist/logs-SzuYudOj.mjs +485 -0
- package/dist/merge-state-ytFQCeh4.mjs +262 -0
- package/dist/{metro-BWLOJ_Wr.mjs → metro-Dzo30hvJ.mjs} +1 -1
- package/dist/{named-ports-DiTfSz6p.mjs → named-ports-Dn1o61D9.mjs} +3 -3
- package/dist/{ownership-BfPNRf3-.mjs → ownership-B8d4kXBx.mjs} +49 -16
- package/dist/{ownership-D2xbLVoz.mjs → ownership-Dqnrjrio.mjs} +2 -2
- package/dist/{ports-CLSSFOuz.mjs → ports-Dp_QTwTC.mjs} +3 -3
- package/dist/{project-BXZzOABt.mjs → project-B8rbutVC.mjs} +4 -72
- package/dist/{reload-CymyF6Kk.mjs → reload-nocmx8KB.mjs} +9 -9
- package/dist/remote-cache-BRsRZru5.mjs +526 -0
- package/dist/{server-bare-DVbb7kXH.mjs → server-bare-Gn3ssSdK.mjs} +1 -1
- package/dist/{server-expo-BvOCeakp.mjs → server-expo-CeT1nIIk.mjs} +2 -2
- package/dist/server-expo-eWi9Coof.mjs +2 -0
- package/dist/{settings-DOugPlPz.mjs → settings-BlyDn2Oz.mjs} +14 -5
- package/dist/{settings-EWYNwe72.mjs → settings-bE-Gu3Ti.mjs} +23 -11
- package/dist/settings.schema.json +73 -1
- package/dist/{simslim-DZUxzsjq.mjs → simslim-COtEEP7x.mjs} +152 -96
- package/dist/{slot-launch-Dk4oTREM.mjs → slot-launch-DIACGEfH.mjs} +4 -4
- package/dist/{spawn-claims-CUleC2cM.mjs → spawn-claims-PT8aQpCa.mjs} +25 -2
- package/dist/{start-qWapeB4W.mjs → start-BFstVBlx.mjs} +1 -1
- package/dist/{start-RbmFPeZ7.mjs → start-BboE1HIS.mjs} +108 -17
- package/dist/{state-WGdIbi_d.mjs → state-DcDgzfu3.mjs} +15 -3
- package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
- package/dist/{stats-DBYJ5bxL.mjs → stats-DiCtmluY.mjs} +4 -4
- package/dist/{status-Dr4fRkym.mjs → status-BUASYyy_.mjs} +92 -33
- package/dist/{status-Zp8kvexe.mjs → status-DI9Tjo7T.mjs} +255 -38
- package/dist/stim-desktop-D0xhpZt8.mjs +29 -0
- package/dist/stop-BI7QY76M.mjs +3 -0
- package/dist/{stop-DcEv5sk-.mjs → stop-Bn6v-GvS.mjs} +205 -37
- package/dist/supervisor-run.mjs +11 -11
- package/dist/{workspace-process-lock-BXA96myD.mjs → workspace-process-lock-BvvJTfo_.mjs} +10 -5
- package/dist/{worktree-DEG2o9F8.mjs → worktree-B5sTRskL.mjs} +1 -1
- package/dist/{worktree-Bd4dRvx7.mjs → worktree-ChDL5fO1.mjs} +17 -16
- package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
- package/dist/{xcode-B2tBkf0m.mjs → xcode-BzplpQzc.mjs} +7 -7
- package/package.json +5 -5
- package/dist/budget-nDCEwtJv.mjs +0 -620
- package/dist/logs-MARIy6m9.mjs +0 -209
- package/dist/server-expo-DBVTdult.mjs +0 -2
- package/dist/stop-DI4BG4Os.mjs +0 -3
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { A as
|
|
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-
|
|
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-
|
|
5
|
-
import {
|
|
6
|
-
import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-
|
|
7
|
-
import { d as removeExpiredLease, h as listBuildLocks } from "./device-lease-
|
|
8
|
-
import { A as
|
|
9
|
-
import { h as POOL_SETTING_REMEDY, y as parkedMaxSetting } from "./ownership-
|
|
10
|
-
import { l as listAllIosSims } from "./ios-
|
|
11
|
-
import { a as parseIdleDuration, o as shutDownIdleDevices, r as idleDeviceLines, t as collectIdleDevices } from "./idle
|
|
12
|
-
import { i as canonicalPath, o as listBuildSlots, r as workspaceInUse } from "./in-use-
|
|
13
|
-
import {
|
|
14
|
-
import { A as
|
|
15
|
-
import { a as
|
|
16
|
-
import {
|
|
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
|
|
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
|
|
41
|
-
readdirSync
|
|
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
|
|
50
|
+
if (!existsSync(path)) continue;
|
|
49
51
|
let state;
|
|
50
52
|
try {
|
|
51
|
-
state = JSON.parse(readFileSync
|
|
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
|
|
88
|
-
entries = readdirSync
|
|
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
|
|
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
|
|
219
|
-
readdirSync
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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
|
|
604
|
+
return [.../* @__PURE__ */ new Set([...registered, ...recorded])].filter((root) => existsSync(root)).toSorted();
|
|
689
605
|
}
|
|
690
|
-
function checkMergeStates(pending, idle,
|
|
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,
|
|
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
|
|
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
|
|
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
|
}
|