stim 1.8.0 → 1.9.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/README.md +23 -155
- package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
- package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
- package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
- package/dist/app-install-Uf3a83Tv.mjs +819 -0
- package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
- package/dist/build-progress-CVqkfPC6.mjs +188 -0
- package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +22 -19
- package/dist/collector-run.d.mts +2 -0
- package/dist/collector-run.mjs +10 -9
- package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
- package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
- package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
- package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
- package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
- package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
- package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
- package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
- package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
- package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
- package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
- package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
- package/dist/exec-jIAlgilk.mjs +132 -0
- package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
- package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
- package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
- package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
- package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
- package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
- package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
- package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
- package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
- package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
- package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
- package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
- package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
- package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
- package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
- package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
- package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
- package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
- package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
- package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
- package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
- package/dist/server-expo-CeILEu86.mjs +2 -0
- package/dist/settings-BKNh5rl4.mjs +376 -0
- package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
- package/dist/settings.schema.json +989 -0
- package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
- package/dist/simslim-B_LsGLuJ.mjs +259 -0
- package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
- package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
- package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
- package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
- package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
- package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
- package/dist/status-S6ehL4Ui.mjs +932 -0
- package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
- package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
- package/dist/supervisor-run.mjs +52 -34
- package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
- package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
- package/dist/worktree-DKhSF0UX.mjs +2 -0
- package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
- package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
- package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
- package/package.json +5 -6
- package/skill/SKILL.md +2 -0
- package/dist/app-install-BDbsSgCl.mjs +0 -1175
- package/dist/build-slots-C-oAHRT1.mjs +0 -151
- package/dist/dev-client-BmT-ioFI.mjs +0 -797
- package/dist/exec-B29XNuhP.mjs +0 -77
- package/dist/reclaim-KJ-0uWEF.mjs +0 -361
- package/dist/server-expo-YDDG3eP7.mjs +0 -2
- package/dist/status-DdkLki47.mjs +0 -426
- package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { i as registeredCaches } from "./cache-manifest-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { a as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { r as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { n as reclaimProject, t as describeDereferenced } from "./reclaim-KJ-0uWEF.mjs";
|
|
1
|
+
import { F as formatBytes, I as isOnMountedVolume, K as projectDeviceSlots, L as listMountedVolumes, M as readJsonFile, P as directorySize, R as measuredDirectorySize, T as withConfigLock, U as deviceSlotPlatforms, b as removeProject, f as getProject, g as loadConfig, h as isPathPrefix, i as clearDevice, j as isJsonObject, k as ownership_claim_exports, u as getConfigDir, z as volumeRootFor } from "./config-Cp9lM3wX.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-jIAlgilk.mjs";
|
|
3
|
+
import { i as registeredCaches } from "./cache-manifest-Dg7OnMrp.mjs";
|
|
4
|
+
import { S as workspaceStateFile, _ as workspaceDir, s as workspaceLastUsed, x as workspaceName$1 } from "./workspace-state-DF2gCFhx.mjs";
|
|
5
|
+
import { S as unpushedCommits, c as hasPopulatedSubmodules, f as listWorktrees, o as dirtyPaths, u as hasUncommittedWork, x as sourceCheckoutOf } from "./worktree-R_cUlE7s.mjs";
|
|
6
|
+
import { a as detectIsExpo, o as findProjectRoot } from "./project-DS112DYB.mjs";
|
|
7
|
+
import { E as listOrphanedAvdDirectories, k as ownedAvdDirectory, w as listAvds } from "./android-_y4Ohi5z.mjs";
|
|
8
|
+
import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-gByVmIjy.mjs";
|
|
9
|
+
import { n as listBuildLocks } from "./build-lock-DPXPLgTc.mjs";
|
|
10
|
+
import { b as resolveEasCliBin, o as parkedMaxSetting, r as dropParked, s as readParked, t as POOL_SETTING_REMEDY } from "./sim-pool-KaIsp2Ft.mjs";
|
|
11
|
+
import { _ as parseRuntimeVersion, f as listIosDeviceTypes, u as listAllIosSims } from "./ios-BL2NFckr.mjs";
|
|
12
|
+
import { i as listLeaseFiles, r as leaseIsExpired, u as removeExpiredLease } from "./device-lease-Crs-HkJY.mjs";
|
|
13
|
+
import { _ as listBuildSlots } from "./xcode-BZqyxh3e.mjs";
|
|
14
|
+
import { C as verifyStoppedSession, D as teardownParkedAvd, E as teardownOwnedIosSim, O as teardownParkedIosSim, S as stopSessionArgs, T as teardownOwnedAvd, _ as inspectSessionForTeardown, d as withEasProjectLock, f as easMachineStateRoot, g as getSessionArgs, h as findOrphanedOwnedSessions, m as removeEasSessionClaim, p as readEasSessionLedger, u as withRemoteSessionLock, v as isDefinitiveMissingSessionError, x as parseSessionListPage, y as listOwnedSessionsArgs } from "./device-remote-AQ5rQPqr.mjs";
|
|
15
|
+
import { _ as workspaceInUse, a as reclaimKeys, d as removeWorktreeTarget, g as withIdleWorkspace, h as emptyWorkspaceDir, m as reclaimProject, n as excludePodChurn, p as describeDereferenced, r as matchWorktreeEntry, v as canonicalPath } from "./worktree-LGB-UwgG.mjs";
|
|
17
16
|
import { InvalidArgumentError } from "commander";
|
|
18
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "fs";
|
|
19
|
-
import { homedir, tmpdir } from "os";
|
|
17
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "fs";
|
|
20
18
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
21
19
|
import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
|
|
20
|
+
import { homedir, tmpdir } from "os";
|
|
22
21
|
import chalk from "chalk";
|
|
23
22
|
//#region src/cache/caches.ts
|
|
24
23
|
function xcodeDerivedDataRoot() {
|
|
@@ -54,45 +53,26 @@ function metroFileMaps() {
|
|
|
54
53
|
} catch {
|
|
55
54
|
return null;
|
|
56
55
|
}
|
|
57
|
-
|
|
56
|
+
const uid = process.getuid?.();
|
|
57
|
+
const files = [];
|
|
58
58
|
let bytes = 0;
|
|
59
59
|
for (const n of names) try {
|
|
60
|
-
|
|
60
|
+
const st = statSync(join(root, n));
|
|
61
|
+
if (uid !== void 0 && st.uid !== uid) continue;
|
|
62
|
+
files.push(join(root, n));
|
|
63
|
+
bytes += st.size;
|
|
61
64
|
} catch {}
|
|
65
|
+
if (!files.length) return null;
|
|
62
66
|
return {
|
|
63
67
|
name: "Metro file maps",
|
|
64
68
|
dir: root,
|
|
65
|
-
files
|
|
69
|
+
files,
|
|
66
70
|
bytes,
|
|
67
71
|
prune: "entries",
|
|
68
|
-
note: `${
|
|
72
|
+
note: `${files.length} file(s), one per project root Metro has served`
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
|
-
function
|
|
72
|
-
return (paths || []).map((p) => resolve(p.startsWith("~") ? join(homedir(), p.slice(1)) : p)).filter((p) => existsSync(p)).map((dir) => ({
|
|
73
|
-
name: "declared",
|
|
74
|
-
dir,
|
|
75
|
-
prune: "entries",
|
|
76
|
-
note: "from the `caches` setting"
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
function projectSettings(cwd) {
|
|
80
|
-
const root = findProjectRoot(cwd);
|
|
81
|
-
if (!root) return {};
|
|
82
|
-
return resolveSettings({
|
|
83
|
-
projectPath: root,
|
|
84
|
-
gitCommonDir: gitCommonDir(root),
|
|
85
|
-
repoRoot: repoRoot(root)
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
function declaredCachePaths(cwd = process.cwd()) {
|
|
89
|
-
const declared = projectSettings(cwd).caches;
|
|
90
|
-
return Array.isArray(declared) ? declared : [];
|
|
91
|
-
}
|
|
92
|
-
function projectSettingShapeErrors(cwd = process.cwd()) {
|
|
93
|
-
return settingShapeErrors(projectSettings(cwd));
|
|
94
|
-
}
|
|
95
|
-
function discoverCaches({ declared = [] } = {}) {
|
|
75
|
+
function discoverCaches() {
|
|
96
76
|
const gradle = gradleBuildCache();
|
|
97
77
|
const gradleDir = gradle ? canonicalCacheDir(gradle.dir) : null;
|
|
98
78
|
const registered = protectNestedMetroAncestors(suppressLegacyMetroAncestors(registeredCaches().map((c) => Object.assign({}, c, {
|
|
@@ -104,8 +84,7 @@ function discoverCaches({ declared = [] } = {}) {
|
|
|
104
84
|
const detected = [
|
|
105
85
|
compilationCache(),
|
|
106
86
|
gradle,
|
|
107
|
-
metroFileMaps()
|
|
108
|
-
...declaredCaches(declared)
|
|
87
|
+
metroFileMaps()
|
|
109
88
|
].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
|
|
110
89
|
return mergeCacheDescriptors([...registered, ...detected], gradleDir);
|
|
111
90
|
}
|
|
@@ -218,6 +197,7 @@ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
|
218
197
|
const entries = cache.files ?? entriesAtDepth(cache.dir, cache.entriesDepth ?? 1);
|
|
219
198
|
let removed = 0;
|
|
220
199
|
let bytes = 0;
|
|
200
|
+
let failed = 0;
|
|
221
201
|
for (const entry of entries) {
|
|
222
202
|
let used;
|
|
223
203
|
let size;
|
|
@@ -236,11 +216,14 @@ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
|
236
216
|
});
|
|
237
217
|
removed++;
|
|
238
218
|
bytes += size;
|
|
239
|
-
} catch {
|
|
219
|
+
} catch {
|
|
220
|
+
failed++;
|
|
221
|
+
}
|
|
240
222
|
}
|
|
241
223
|
return {
|
|
242
224
|
removed,
|
|
243
225
|
bytes,
|
|
226
|
+
failed,
|
|
244
227
|
skipped: null
|
|
245
228
|
};
|
|
246
229
|
}
|
|
@@ -262,17 +245,8 @@ function entriesAtDepth(dir, depth) {
|
|
|
262
245
|
return level;
|
|
263
246
|
}
|
|
264
247
|
//#endregion
|
|
265
|
-
//#region src/commands/gc/paths.ts
|
|
266
|
-
function canonicalPath(p) {
|
|
267
|
-
try {
|
|
268
|
-
return realpathSync(p);
|
|
269
|
-
} catch {
|
|
270
|
-
return resolve(p);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
//#endregion
|
|
274
248
|
//#region src/commands/gc/caches.ts
|
|
275
|
-
const DAY_MS$
|
|
249
|
+
const DAY_MS$3 = 864e5;
|
|
276
250
|
function cacheSweepIsScoped() {
|
|
277
251
|
return Boolean(process.env.STIM_HOME);
|
|
278
252
|
}
|
|
@@ -289,10 +263,16 @@ function machineGlobalReason(cache) {
|
|
|
289
263
|
return `STIM_HOME scopes this config, but ${cache.dir} is outside it and therefore machine-global`;
|
|
290
264
|
}
|
|
291
265
|
const EVERY_CACHE = "all";
|
|
266
|
+
const WORKSPACE_OUTPUTS = "workspaces";
|
|
267
|
+
function includesWorkspaceOutputs(name) {
|
|
268
|
+
const wanted = name?.trim().toLowerCase();
|
|
269
|
+
return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
|
|
270
|
+
}
|
|
292
271
|
function selectCaches(caches, name) {
|
|
293
272
|
if (!name) return caches;
|
|
294
273
|
const wanted = name.trim().toLowerCase();
|
|
295
274
|
if (wanted === EVERY_CACHE) return caches;
|
|
275
|
+
if (wanted === WORKSPACE_OUTPUTS) return [];
|
|
296
276
|
return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
|
|
297
277
|
}
|
|
298
278
|
function planCacheEmptying(caches, all) {
|
|
@@ -335,7 +315,7 @@ function emptyCache(cache) {
|
|
|
335
315
|
};
|
|
336
316
|
if (cache.prune !== "atomic") return pruneCache(cache, {
|
|
337
317
|
olderThanDays: 0,
|
|
338
|
-
now: Date.now() + DAY_MS$
|
|
318
|
+
now: Date.now() + DAY_MS$3
|
|
339
319
|
});
|
|
340
320
|
let names;
|
|
341
321
|
try {
|
|
@@ -365,6 +345,11 @@ function emptyCache(cache) {
|
|
|
365
345
|
skipped: null
|
|
366
346
|
};
|
|
367
347
|
}
|
|
348
|
+
function reportFailures(cache, failed) {
|
|
349
|
+
if (!failed) return;
|
|
350
|
+
console.log(chalk.red(`${cache.name}: ${failed} entr${failed === 1 ? "y" : "ies"} in ${cache.dir} could not be removed`));
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
}
|
|
368
353
|
function trimCaches(caches, olderThan) {
|
|
369
354
|
let cacheBytes = 0;
|
|
370
355
|
for (const c of caches) {
|
|
@@ -377,7 +362,8 @@ function trimCaches(caches, olderThan) {
|
|
|
377
362
|
else if (r.removed) {
|
|
378
363
|
cacheBytes += r.bytes;
|
|
379
364
|
console.log(chalk.green(`Trimmed ${c.name}: ${r.removed} entr${r.removed === 1 ? "y" : "ies"} (${formatBytes(r.bytes)})`));
|
|
380
|
-
} else console.log(chalk.dim(`${c.name}: nothing older than ${olderThan}d`));
|
|
365
|
+
} else if (!r.failed) console.log(chalk.dim(`${c.name}: nothing older than ${olderThan}d`));
|
|
366
|
+
reportFailures(c, r.failed);
|
|
381
367
|
}
|
|
382
368
|
if (cacheBytes) console.log(chalk.dim(`Trimmed ${formatBytes(cacheBytes)} of shared cache. The next build that wanted those entries pays to rebuild them.`));
|
|
383
369
|
}
|
|
@@ -393,8 +379,8 @@ function emptyCaches(caches) {
|
|
|
393
379
|
else if (r.removed) {
|
|
394
380
|
cacheBytes += r.bytes;
|
|
395
381
|
console.log(chalk.green(`Emptied ${c.name}: ${r.removed} entr${r.removed === 1 ? "y" : "ies"} (${formatBytes(r.bytes)})`));
|
|
396
|
-
} else console.log(chalk.dim(`${c.name}: already empty`));
|
|
397
|
-
|
|
382
|
+
} else if (!r.failed) console.log(chalk.dim(`${c.name}: already empty`));
|
|
383
|
+
reportFailures(c, r.failed);
|
|
398
384
|
}
|
|
399
385
|
if (cacheBytes) console.log(chalk.dim(`Emptied ${formatBytes(cacheBytes)} of shared cache. Every build that wanted any of it now pays to rebuild it.`));
|
|
400
386
|
}
|
|
@@ -402,7 +388,7 @@ function emptyCaches(caches) {
|
|
|
402
388
|
//#region src/commands/gc/devices.ts
|
|
403
389
|
const DEVICE_LIST_TIMEOUT_MS = 3e4;
|
|
404
390
|
const AVD_SIZE_TIMEOUT_MS = 5e3;
|
|
405
|
-
const DAY_MS = 864e5;
|
|
391
|
+
const DAY_MS$2 = 864e5;
|
|
406
392
|
function describeKept(ref) {
|
|
407
393
|
return ref.mounted ? `referenced by ${ref.path}` : `referenced by ${ref.path} (volume not mounted; kept just in case)`;
|
|
408
394
|
}
|
|
@@ -472,7 +458,7 @@ function findOrphanedDevices({ sims = [], avds = [], config, isMounted, deadProj
|
|
|
472
458
|
}
|
|
473
459
|
function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays, now = Date.now(), lastTouched, deadProjects = [] }) {
|
|
474
460
|
if (!Number.isFinite(olderThanDays) || typeof lastTouched !== "function") return [];
|
|
475
|
-
const cutoff = now - olderThanDays * DAY_MS;
|
|
461
|
+
const cutoff = now - olderThanDays * DAY_MS$2;
|
|
476
462
|
const dead = new Set(deadProjects);
|
|
477
463
|
const liveSims = new Map(sims.filter((s) => s?.name?.startsWith("stim-")).map((s) => [s.udid, s.name]));
|
|
478
464
|
const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
|
|
@@ -481,7 +467,7 @@ function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays,
|
|
|
481
467
|
if (dead.has(path) || !isAbsolute(path)) continue;
|
|
482
468
|
const touched = lastTouched(path);
|
|
483
469
|
if (!Number.isFinite(touched) || touched >= cutoff) continue;
|
|
484
|
-
const idleDays = Math.floor((now - touched) / DAY_MS);
|
|
470
|
+
const idleDays = Math.floor((now - touched) / DAY_MS$2);
|
|
485
471
|
for (const { slot, platforms } of projectDeviceSlots(proj)) {
|
|
486
472
|
const ios = platforms.ios;
|
|
487
473
|
if (ios?.owned && ios.deviceUdid && liveSims.has(ios.deviceUdid)) stale.push({
|
|
@@ -1117,6 +1103,262 @@ async function deleteEasSessions(easSessionSweep, deps) {
|
|
|
1117
1103
|
return deleteFailures;
|
|
1118
1104
|
}
|
|
1119
1105
|
//#endregion
|
|
1106
|
+
//#region src/commands/gc/workspaces.ts
|
|
1107
|
+
function workspacesRoot() {
|
|
1108
|
+
return join(getConfigDir(), "workspaces");
|
|
1109
|
+
}
|
|
1110
|
+
function isInsideWorkspaces(dir) {
|
|
1111
|
+
const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
|
|
1112
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1113
|
+
}
|
|
1114
|
+
function readRecordedRoot(dir, name) {
|
|
1115
|
+
let parsed;
|
|
1116
|
+
try {
|
|
1117
|
+
parsed = readJsonFile(join(dir, "workspace.json"));
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
return {
|
|
1120
|
+
projectRoot: null,
|
|
1121
|
+
problem: error?.code === "ENOENT" ? "it has no workspace.json" : "its workspace.json does not parse"
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
|
|
1125
|
+
if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
|
|
1126
|
+
projectRoot: null,
|
|
1127
|
+
problem: "its workspace.json records no absolute project root"
|
|
1128
|
+
};
|
|
1129
|
+
if (workspaceName$1(projectRoot) !== name) return {
|
|
1130
|
+
projectRoot: null,
|
|
1131
|
+
problem: `its workspace.json records ${projectRoot}, whose workspace directory has another name`
|
|
1132
|
+
};
|
|
1133
|
+
return {
|
|
1134
|
+
projectRoot,
|
|
1135
|
+
problem: null
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function listWorkspaceDirs() {
|
|
1139
|
+
const root = workspacesRoot();
|
|
1140
|
+
let names;
|
|
1141
|
+
try {
|
|
1142
|
+
names = readdirSync(root).toSorted();
|
|
1143
|
+
} catch {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
1146
|
+
const entries = [];
|
|
1147
|
+
for (const name of names) {
|
|
1148
|
+
const dir = join(root, name);
|
|
1149
|
+
try {
|
|
1150
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
1151
|
+
} catch {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
entries.push({
|
|
1155
|
+
dir,
|
|
1156
|
+
...readRecordedRoot(dir, name)
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
return entries;
|
|
1160
|
+
}
|
|
1161
|
+
function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse }) {
|
|
1162
|
+
const orphaned = [];
|
|
1163
|
+
const skipped = [];
|
|
1164
|
+
for (const { dir, projectRoot, problem } of entries) {
|
|
1165
|
+
if (projectRoot === null) {
|
|
1166
|
+
skipped.push({
|
|
1167
|
+
dir,
|
|
1168
|
+
reason: `workspace directory not resolved: ${problem ?? "unknown project root"}`
|
|
1169
|
+
});
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const present = exists(projectRoot);
|
|
1173
|
+
if (present === null) {
|
|
1174
|
+
skipped.push({
|
|
1175
|
+
dir,
|
|
1176
|
+
reason: `cannot tell whether its project root ${projectRoot} still exists`
|
|
1177
|
+
});
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
if (present) continue;
|
|
1181
|
+
if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
|
|
1182
|
+
if (!isMounted(projectRoot)) {
|
|
1183
|
+
skipped.push({
|
|
1184
|
+
dir,
|
|
1185
|
+
reason: `the volume of its project root ${projectRoot} is not mounted`
|
|
1186
|
+
});
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
const reasons = inUse(projectRoot);
|
|
1190
|
+
if (reasons.length) {
|
|
1191
|
+
skipped.push({
|
|
1192
|
+
dir,
|
|
1193
|
+
reason: `its project root ${projectRoot} is gone, but ${reasons.join("; ")}`
|
|
1194
|
+
});
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
orphaned.push({
|
|
1198
|
+
dir,
|
|
1199
|
+
projectRoot
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
orphaned,
|
|
1204
|
+
skipped
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
function rootPresence(path) {
|
|
1208
|
+
try {
|
|
1209
|
+
lstatSync(path);
|
|
1210
|
+
return true;
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
const code = error?.code;
|
|
1213
|
+
return code === "ENOENT" || code === "ENOTDIR" ? false : null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
|
|
1217
|
+
const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
|
|
1218
|
+
registryKeys,
|
|
1219
|
+
exists: rootPresence,
|
|
1220
|
+
isMounted: (path) => isOnMountedVolume(path, mountedVolumes),
|
|
1221
|
+
inUse: (root) => workspaceInUse(root)
|
|
1222
|
+
});
|
|
1223
|
+
return {
|
|
1224
|
+
orphaned: classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })),
|
|
1225
|
+
skipped: classified.skipped
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
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.";
|
|
1229
|
+
const WORKSPACE_OUTPUT_DIRS = [
|
|
1230
|
+
"derived-data",
|
|
1231
|
+
"gradle-build",
|
|
1232
|
+
"android-cas",
|
|
1233
|
+
"cache-provider"
|
|
1234
|
+
];
|
|
1235
|
+
const DAY_MS$1 = 864e5;
|
|
1236
|
+
function planWorkspaceOutputs(entries, { olderThan, now }) {
|
|
1237
|
+
return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
|
|
1238
|
+
const idleDays = Number.isFinite(lastUsed) ? Math.max(0, Math.floor((now - lastUsed) / DAY_MS$1)) : null;
|
|
1239
|
+
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];
|
|
1240
|
+
return {
|
|
1241
|
+
dir,
|
|
1242
|
+
projectRoot,
|
|
1243
|
+
bytes,
|
|
1244
|
+
idleDays,
|
|
1245
|
+
willClear: keptCode === null,
|
|
1246
|
+
keptCode,
|
|
1247
|
+
keptReason
|
|
1248
|
+
};
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
const SIZE_TIMEOUT_MS = 6e4;
|
|
1252
|
+
function sizeOf(dir) {
|
|
1253
|
+
return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
|
|
1254
|
+
}
|
|
1255
|
+
function outputPaths(dir) {
|
|
1256
|
+
return WORKSPACE_OUTPUT_DIRS.map((name) => join(dir, name)).filter((path) => existsSync(path));
|
|
1257
|
+
}
|
|
1258
|
+
function outputBytes(paths) {
|
|
1259
|
+
let total = 0;
|
|
1260
|
+
for (const path of paths) {
|
|
1261
|
+
const size = sizeOf(path);
|
|
1262
|
+
if (size === null) return null;
|
|
1263
|
+
total += size;
|
|
1264
|
+
}
|
|
1265
|
+
return total;
|
|
1266
|
+
}
|
|
1267
|
+
function collectWorkspaceOutputs({ olderThan, now, exclude = [] }) {
|
|
1268
|
+
const mountedVolumes = listMountedVolumes();
|
|
1269
|
+
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: outputBytes(paths) })).map((entry) => Object.assign({}, entry, {
|
|
1270
|
+
lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
|
|
1271
|
+
inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
|
|
1272
|
+
}));
|
|
1273
|
+
return {
|
|
1274
|
+
root: workspacesRoot(),
|
|
1275
|
+
workspaces: planWorkspaceOutputs(entries, {
|
|
1276
|
+
olderThan,
|
|
1277
|
+
now
|
|
1278
|
+
})
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
1282
|
+
let failures = 0;
|
|
1283
|
+
let cleared = 0;
|
|
1284
|
+
for (const entry of report.workspaces) {
|
|
1285
|
+
const root = entry.projectRoot;
|
|
1286
|
+
if (!entry.willClear || root === null) {
|
|
1287
|
+
if (entry.keptReason) console.log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
let run;
|
|
1291
|
+
try {
|
|
1292
|
+
run = await withIdleWorkspace(root, () => {
|
|
1293
|
+
const [again] = planWorkspaceOutputs([{
|
|
1294
|
+
...entry,
|
|
1295
|
+
problem: null,
|
|
1296
|
+
lastUsed: workspaceLastUsed(root),
|
|
1297
|
+
inUse: []
|
|
1298
|
+
}], {
|
|
1299
|
+
olderThan,
|
|
1300
|
+
now
|
|
1301
|
+
});
|
|
1302
|
+
if (again?.keptReason) return again.keptReason;
|
|
1303
|
+
for (const name of WORKSPACE_OUTPUT_DIRS) rmSync(join(entry.dir, name), {
|
|
1304
|
+
recursive: true,
|
|
1305
|
+
force: true
|
|
1306
|
+
});
|
|
1307
|
+
return null;
|
|
1308
|
+
}, { purpose: "gc" });
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
failures++;
|
|
1311
|
+
console.log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const kept = run.ran ? run.value : `in use: ${run.reasons.join("; ")}`;
|
|
1315
|
+
if (kept) {
|
|
1316
|
+
console.log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
cleared += entry.bytes ?? 0;
|
|
1320
|
+
console.log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
1321
|
+
}
|
|
1322
|
+
if (cleared) console.log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
|
|
1323
|
+
return failures;
|
|
1324
|
+
}
|
|
1325
|
+
function stillOrphaned({ dir, projectRoot }) {
|
|
1326
|
+
if (readRecordedRoot(dir, basename(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
|
|
1327
|
+
return !Object.keys(loadConfig()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
|
|
1328
|
+
}
|
|
1329
|
+
async function deleteOrphanedWorkspaces(orphaned) {
|
|
1330
|
+
let failures = 0;
|
|
1331
|
+
for (const entry of orphaned) {
|
|
1332
|
+
if (!existsSync(entry.dir)) continue;
|
|
1333
|
+
let run;
|
|
1334
|
+
try {
|
|
1335
|
+
run = await withIdleWorkspace(entry.projectRoot, () => {
|
|
1336
|
+
if (!stillOrphaned(entry)) return false;
|
|
1337
|
+
emptyWorkspaceDir(entry.dir);
|
|
1338
|
+
return true;
|
|
1339
|
+
}, { purpose: "gc" });
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
failures++;
|
|
1342
|
+
console.log(chalk.red(`Could not remove ${entry.dir}: ${error?.message || error}`));
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (!run.ran) {
|
|
1346
|
+
console.log(chalk.yellow(`Kept ${entry.dir}: ${run.reasons.join("; ")}`));
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (!run.value) {
|
|
1350
|
+
console.log(chalk.yellow(`Kept ${entry.dir}: it can no longer be confirmed orphaned.`));
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
const size = entry.bytes === void 0 ? "" : ` (${sizeText(entry.bytes)})`;
|
|
1354
|
+
console.log(chalk.green(`Removed the orphaned workspace directory ${entry.dir}${size}`));
|
|
1355
|
+
}
|
|
1356
|
+
return failures;
|
|
1357
|
+
}
|
|
1358
|
+
function sizeText(bytes) {
|
|
1359
|
+
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
1360
|
+
}
|
|
1361
|
+
//#endregion
|
|
1120
1362
|
//#region src/commands/gc/report.ts
|
|
1121
1363
|
function shortKey(key) {
|
|
1122
1364
|
const text = String(key ?? "");
|
|
@@ -1162,11 +1404,11 @@ function unresolvedLockLines(locks, slots) {
|
|
|
1162
1404
|
if (entries.length === 0) return [];
|
|
1163
1405
|
return [
|
|
1164
1406
|
`Build locks and slots Stim cannot resolve (${entries.length}) - NOT touched, because a dead holder and a live one cannot be told apart:`,
|
|
1165
|
-
...entries.
|
|
1166
|
-
"
|
|
1407
|
+
...entries.flatMap((entry) => [` ${entry.path}`, ` ${(0, ownership_claim_exports.claimRemoveCommand)(entry.path)}`]),
|
|
1408
|
+
" run that yourself once you know nothing is building with it"
|
|
1167
1409
|
];
|
|
1168
1410
|
}
|
|
1169
|
-
function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], staleDevices = [], staleDeviceRecords = [], buildLocks = {
|
|
1411
|
+
function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], staleDevices = [], staleDeviceRecords = [], buildLocks = {
|
|
1170
1412
|
stale: [],
|
|
1171
1413
|
live: [],
|
|
1172
1414
|
unresolved: []
|
|
@@ -1182,14 +1424,29 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
|
|
|
1182
1424
|
orphaned: [],
|
|
1183
1425
|
notices: [],
|
|
1184
1426
|
deletionSafe: true
|
|
1185
|
-
}, caches = [], cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
|
|
1427
|
+
}, caches = [], workspaceOutputs = null, worktreeSweep = null, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
|
|
1186
1428
|
const lines = [];
|
|
1187
1429
|
const staleLocks = buildLocks?.stale ?? [];
|
|
1188
1430
|
const liveLocks = buildLocks?.live ?? [];
|
|
1189
1431
|
const staleSlots = buildSlots?.stale ?? [];
|
|
1190
1432
|
const expiredLeases = deviceLeases?.expired ?? [];
|
|
1191
1433
|
if (cacheScope) lines.push(`Cache scope: "${cacheScope}". Devices, project entries and locks were not inspected.`);
|
|
1192
|
-
else if (
|
|
1434
|
+
else if ([
|
|
1435
|
+
deadProjects,
|
|
1436
|
+
invalidProjects,
|
|
1437
|
+
orphanedWorkspaces,
|
|
1438
|
+
parkedSims,
|
|
1439
|
+
parkedAvds,
|
|
1440
|
+
orphanedDevices,
|
|
1441
|
+
staleDevices,
|
|
1442
|
+
staleDeviceRecords,
|
|
1443
|
+
staleLocks,
|
|
1444
|
+
staleSlots,
|
|
1445
|
+
expiredLeases,
|
|
1446
|
+
easSessionSweep.orphaned,
|
|
1447
|
+
workspaceOutputs?.workspaces.filter((entry) => entry.willClear) ?? [],
|
|
1448
|
+
worktreeSweep?.worktrees.filter((entry) => !entry.skipped) ?? []
|
|
1449
|
+
].every((found) => found.length === 0)) {
|
|
1193
1450
|
const reasons = [];
|
|
1194
1451
|
if (skipped.length > 0) reasons.push(`${skipped.length} entr${skipped.length === 1 ? "y" : "ies"} could not be checked`);
|
|
1195
1452
|
if (deviceSweepNotices.length > 0) reasons.push("device sweep incomplete");
|
|
@@ -1200,6 +1457,8 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
|
|
|
1200
1457
|
lines.push(...projectEntryLines(`Dead project entries (${deadProjects.length}):`, deadProjects));
|
|
1201
1458
|
lines.push(...projectEntryLines(`Invalid project entries (${invalidProjects.length}) - the key is not an absolute path:`, invalidProjects));
|
|
1202
1459
|
lines.push(...namedPortLines(orphanedPorts));
|
|
1460
|
+
lines.push(...orphanedWorkspaceLines(orphanedWorkspaces));
|
|
1461
|
+
lines.push(...worktreeSweepLines(worktreeSweep));
|
|
1203
1462
|
lines.push(...formatParkedSimReport(parkedSims, now));
|
|
1204
1463
|
lines.push(...formatParkedAvdReport(parkedAvds, now));
|
|
1205
1464
|
if (orphanedDevices.length) {
|
|
@@ -1207,7 +1466,7 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
|
|
|
1207
1466
|
for (const d of orphanedDevices) lines.push(` ${d.kind} ${d.name} (${d.id})${deviceSizeSuffix(d)}`);
|
|
1208
1467
|
}
|
|
1209
1468
|
if (staleDevices.length) {
|
|
1210
|
-
lines.push(`Stale owned devices (${staleDevices.length}) -
|
|
1469
|
+
lines.push(`Stale owned devices (${staleDevices.length}) - workspace unused for ${olderThan ?? "?"}d or more:`);
|
|
1211
1470
|
for (const d of staleDevices) {
|
|
1212
1471
|
lines.push(` ${d.kind} ${d.name} (${d.id})${deviceSizeSuffix(d)}`);
|
|
1213
1472
|
lines.push(` ${d.project} (idle ${d.idleDays}d)`);
|
|
@@ -1265,24 +1524,7 @@ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invali
|
|
|
1265
1524
|
lines.push(`Skipped (${skipped.length}) - not classified as dead:`);
|
|
1266
1525
|
for (const entry of skipped) lines.push(` ${entry.dir}: ${entry.reason}`);
|
|
1267
1526
|
}
|
|
1268
|
-
|
|
1269
|
-
const total = caches.reduce((n, c) => n + (c.bytes ?? 0), 0);
|
|
1270
|
-
lines.push(`Shared build caches (${caches.length}) - alive, not garbage:`);
|
|
1271
|
-
for (const c of caches) {
|
|
1272
|
-
const tag = c.source ? ` (${c.source})` : "";
|
|
1273
|
-
lines.push(` ${formatBytes(c.bytes ?? 0).padStart(10)} ${c.name}${tag}`);
|
|
1274
|
-
lines.push(` ${c.dir}`);
|
|
1275
|
-
if (c.note) lines.push(` ${c.note}`);
|
|
1276
|
-
if (c.willEmpty) lines.push(" would be EMPTIED");
|
|
1277
|
-
else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
|
|
1278
|
-
}
|
|
1279
|
-
lines.push(` total: ${formatBytes(total)}`);
|
|
1280
|
-
const doomed = caches.filter((c) => c.willEmpty);
|
|
1281
|
-
if (doomed.length) {
|
|
1282
|
-
const doomedBytes = doomed.reduce((n, c) => n + (c.bytes ?? 0), 0);
|
|
1283
|
-
lines.push(` would empty ${doomed.length} of these (${formatBytes(doomedBytes)})`);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1527
|
+
lines.push(...cacheLines(caches, workspaceOutputs));
|
|
1286
1528
|
return lines;
|
|
1287
1529
|
}
|
|
1288
1530
|
function leaseGarbageLines({ expired, kept }) {
|
|
@@ -1313,6 +1555,348 @@ function namedPortLines(ports = []) {
|
|
|
1313
1555
|
" --delete stops TCP listeners and releases these allocations."
|
|
1314
1556
|
];
|
|
1315
1557
|
}
|
|
1558
|
+
function orphanedWorkspaceLines(orphaned) {
|
|
1559
|
+
if (!orphaned.length) return [];
|
|
1560
|
+
const lines = [`Orphaned workspace directories (${orphaned.length}):`];
|
|
1561
|
+
for (const entry of orphaned) {
|
|
1562
|
+
lines.push(` ${entry.dir}${entry.bytes === void 0 ? "" : ` - ${sizeText(entry.bytes)}`}`);
|
|
1563
|
+
lines.push(` recorded project root ${entry.projectRoot} is gone and no registry entry names it`);
|
|
1564
|
+
}
|
|
1565
|
+
lines.push(" --delete re-checks each directory, then removes it whole.");
|
|
1566
|
+
return lines;
|
|
1567
|
+
}
|
|
1568
|
+
function workspaceOutputLines(outputs, bytes) {
|
|
1569
|
+
const lines = [
|
|
1570
|
+
` ${formatBytes(bytes).padStart(10)} Workspace build outputs (detected)`,
|
|
1571
|
+
` ${outputs.root}`,
|
|
1572
|
+
` ${WORKSPACE_OUTPUT_DIRS.join(", ")} of each workspace; workspace.json, state.json, logs and device records stay`,
|
|
1573
|
+
` ${REBUILD_COST}`
|
|
1574
|
+
];
|
|
1575
|
+
for (const w of outputs.workspaces) {
|
|
1576
|
+
const idle = w.idleDays === null ? "last use unknown" : `idle ${w.idleDays}d`;
|
|
1577
|
+
lines.push(` ${sizeText(w.bytes).padStart(8)} ${w.projectRoot ?? w.dir} (${idle})`);
|
|
1578
|
+
lines.push(w.willClear ? " would be CLEARED" : ` kept: ${w.keptReason}`);
|
|
1579
|
+
}
|
|
1580
|
+
return lines;
|
|
1581
|
+
}
|
|
1582
|
+
function cacheLines(caches, workspaceOutputs) {
|
|
1583
|
+
const lines = [];
|
|
1584
|
+
const outputs = workspaceOutputs?.workspaces.length ? workspaceOutputs : null;
|
|
1585
|
+
if (caches.length || outputs) {
|
|
1586
|
+
const outputBytes = outputs ? outputs.workspaces.reduce((n, w) => n + (w.bytes ?? 0), 0) : 0;
|
|
1587
|
+
const total = caches.reduce((n, c) => n + (c.bytes ?? 0), 0) + outputBytes;
|
|
1588
|
+
lines.push(`Shared build caches (${caches.length + (outputs ? 1 : 0)}) - alive, not garbage:`);
|
|
1589
|
+
if (outputs) lines.push(...workspaceOutputLines(outputs, outputBytes));
|
|
1590
|
+
for (const c of caches) {
|
|
1591
|
+
const tag = c.source ? ` (${c.source})` : "";
|
|
1592
|
+
lines.push(` ${formatBytes(c.bytes ?? 0).padStart(10)} ${c.name}${tag}`);
|
|
1593
|
+
lines.push(` ${c.dir}`);
|
|
1594
|
+
if (c.note) lines.push(` ${c.note}`);
|
|
1595
|
+
if (c.willEmpty) lines.push(" would be EMPTIED");
|
|
1596
|
+
else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
|
|
1597
|
+
}
|
|
1598
|
+
lines.push(` total: ${formatBytes(total)}`);
|
|
1599
|
+
const doomed = caches.filter((c) => c.willEmpty);
|
|
1600
|
+
if (doomed.length) {
|
|
1601
|
+
const doomedBytes = doomed.reduce((n, c) => n + (c.bytes ?? 0), 0);
|
|
1602
|
+
lines.push(` would empty ${doomed.length} of these (${formatBytes(doomedBytes)})`);
|
|
1603
|
+
}
|
|
1604
|
+
const clearing = outputs?.workspaces.filter((w) => w.willClear) ?? [];
|
|
1605
|
+
if (clearing.length) {
|
|
1606
|
+
const clearBytes = clearing.reduce((n, w) => n + (w.bytes ?? 0), 0);
|
|
1607
|
+
lines.push(` would clear the build outputs of ${clearing.length} workspace${clearing.length === 1 ? "" : "s"} (${formatBytes(clearBytes)})`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return lines;
|
|
1611
|
+
}
|
|
1612
|
+
function worktreeSweepLines(sweep) {
|
|
1613
|
+
if (!sweep) return [];
|
|
1614
|
+
const removable = sweep.worktrees.filter((w) => !w.skipped);
|
|
1615
|
+
const idle = sweep.defaulted ? `idle ${sweep.olderThan}d or more (the default without --older-than)` : `idle ${sweep.olderThan}d or more`;
|
|
1616
|
+
const lines = [`Linked worktrees (${removable.length} removable, ${sweep.worktrees.length - removable.length} kept) - clean, pushed, ${idle}:`];
|
|
1617
|
+
for (const w of sweep.worktrees) {
|
|
1618
|
+
const age = w.idleDays === null ? "" : ` (idle ${w.idleDays}d)`;
|
|
1619
|
+
lines.push(` ${w.path}${age}`);
|
|
1620
|
+
lines.push(w.skipped ? ` kept: ${w.skipped}` : " would be REMOVED by `stim worktree remove`");
|
|
1621
|
+
}
|
|
1622
|
+
if (removable.length) lines.push(" --delete runs it without --force; use and idleness are re-checked under its removal locks.");
|
|
1623
|
+
return lines;
|
|
1624
|
+
}
|
|
1625
|
+
function jsonLock({ path, platform, key, pid, projectRoot }) {
|
|
1626
|
+
return {
|
|
1627
|
+
path,
|
|
1628
|
+
platform,
|
|
1629
|
+
key,
|
|
1630
|
+
pid,
|
|
1631
|
+
projectRoot
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function gcReportSections({ skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], orphanedDevices = [], staleDevices = [], staleDeviceRecords = [], buildLocks = {
|
|
1635
|
+
stale: [],
|
|
1636
|
+
live: [],
|
|
1637
|
+
unresolved: []
|
|
1638
|
+
}, buildSlots = {
|
|
1639
|
+
stale: [],
|
|
1640
|
+
live: [],
|
|
1641
|
+
unresolved: []
|
|
1642
|
+
}, deviceLeases = {
|
|
1643
|
+
expired: [],
|
|
1644
|
+
kept: []
|
|
1645
|
+
}, deviceSweepNotices = [], easSessionSweep = {
|
|
1646
|
+
projectScope: null,
|
|
1647
|
+
orphaned: [],
|
|
1648
|
+
notices: [],
|
|
1649
|
+
deletionSafe: true
|
|
1650
|
+
}, workspaceOutputs = null, caches = [] }) {
|
|
1651
|
+
return {
|
|
1652
|
+
deadProjects: deadProjects.map((path) => ({ path })),
|
|
1653
|
+
invalidProjects: invalidProjects.map((path) => ({ path })),
|
|
1654
|
+
orphanedPorts: orphanedPorts.map(({ project, label, port }) => ({
|
|
1655
|
+
project,
|
|
1656
|
+
label,
|
|
1657
|
+
port
|
|
1658
|
+
})),
|
|
1659
|
+
orphanedWorkspaces: orphanedWorkspaces.map(({ dir, projectRoot, bytes }) => ({
|
|
1660
|
+
dir,
|
|
1661
|
+
projectRoot,
|
|
1662
|
+
bytes: bytes ?? null
|
|
1663
|
+
})),
|
|
1664
|
+
linkedWorktrees: (worktreeSweep?.worktrees ?? []).map((w) => ({
|
|
1665
|
+
path: w.path,
|
|
1666
|
+
idleDays: w.idleDays,
|
|
1667
|
+
willRemove: w.skipCode === null,
|
|
1668
|
+
reason: w.skipCode,
|
|
1669
|
+
detail: w.skipped
|
|
1670
|
+
})),
|
|
1671
|
+
parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, bytes, listed }) => ({
|
|
1672
|
+
udid,
|
|
1673
|
+
name,
|
|
1674
|
+
model,
|
|
1675
|
+
runtime,
|
|
1676
|
+
parkedAt,
|
|
1677
|
+
bytes,
|
|
1678
|
+
listed
|
|
1679
|
+
})),
|
|
1680
|
+
parkedEmulators: parkedAvds.map(({ name, systemImage, parkedAt, bytes, listed }) => ({
|
|
1681
|
+
name,
|
|
1682
|
+
systemImage,
|
|
1683
|
+
parkedAt,
|
|
1684
|
+
bytes,
|
|
1685
|
+
listed
|
|
1686
|
+
})),
|
|
1687
|
+
orphanedDevices: orphanedDevices.map((d) => ({
|
|
1688
|
+
kind: d.kind,
|
|
1689
|
+
id: d.id,
|
|
1690
|
+
name: d.name,
|
|
1691
|
+
bytes: d.bytes ?? null,
|
|
1692
|
+
directory: d.orphanedDirectory?.directory ?? null
|
|
1693
|
+
})),
|
|
1694
|
+
staleDevices: staleDevices.map((d) => ({
|
|
1695
|
+
kind: d.kind,
|
|
1696
|
+
id: d.id,
|
|
1697
|
+
name: d.name,
|
|
1698
|
+
project: d.project,
|
|
1699
|
+
slot: d.slot ?? null,
|
|
1700
|
+
idleDays: d.idleDays,
|
|
1701
|
+
bytes: d.bytes ?? null
|
|
1702
|
+
})),
|
|
1703
|
+
staleDeviceRecords: staleDeviceRecords.map((r) => ({
|
|
1704
|
+
kind: r.kind,
|
|
1705
|
+
id: r.id,
|
|
1706
|
+
project: r.project,
|
|
1707
|
+
slot: r.slot ?? null
|
|
1708
|
+
})),
|
|
1709
|
+
orphanedEasSessions: easSessionSweep.orphaned.map((s) => ({
|
|
1710
|
+
id: s.id,
|
|
1711
|
+
name: s.name,
|
|
1712
|
+
platform: s.platform,
|
|
1713
|
+
status: s.status,
|
|
1714
|
+
projectScope: s.projectScope
|
|
1715
|
+
})),
|
|
1716
|
+
staleBuildLocks: buildLocks.stale.map(jsonLock),
|
|
1717
|
+
staleBuildSlots: buildSlots.stale.map((s) => ({
|
|
1718
|
+
path: s.path,
|
|
1719
|
+
index: s.index,
|
|
1720
|
+
pid: s.pid,
|
|
1721
|
+
projectRoot: s.projectRoot
|
|
1722
|
+
})),
|
|
1723
|
+
unresolvedBuildClaims: [...(buildLocks.unresolved ?? []).map((l) => ({
|
|
1724
|
+
kind: "lock",
|
|
1725
|
+
path: l.path
|
|
1726
|
+
})), ...(buildSlots.unresolved ?? []).map((s) => ({
|
|
1727
|
+
kind: "slot",
|
|
1728
|
+
path: s.path
|
|
1729
|
+
}))],
|
|
1730
|
+
buildsInProgress: buildLocks.live.map(jsonLock),
|
|
1731
|
+
expiredDeviceLeases: deviceLeases.expired.map((entry) => ({
|
|
1732
|
+
path: entry.path,
|
|
1733
|
+
platform: entry.platform,
|
|
1734
|
+
id: entry.id ?? entry.name,
|
|
1735
|
+
deviceName: entry.lease?.deviceName ?? null,
|
|
1736
|
+
holder: entry.lease?.holder ?? null,
|
|
1737
|
+
expiresAt: entry.lease?.expiresAt ?? null
|
|
1738
|
+
})),
|
|
1739
|
+
keptDeviceLeases: deviceLeases.kept.map(({ name, path, reason }) => ({
|
|
1740
|
+
name,
|
|
1741
|
+
path,
|
|
1742
|
+
detail: reason
|
|
1743
|
+
})),
|
|
1744
|
+
deviceSweepNotices: deviceSweepNotices.map((message) => ({ message })),
|
|
1745
|
+
easSessionSweepNotices: easSessionSweep.notices.map((message) => ({ message })),
|
|
1746
|
+
skipped: skipped.map(({ dir, reason }) => ({
|
|
1747
|
+
path: dir,
|
|
1748
|
+
detail: reason
|
|
1749
|
+
})),
|
|
1750
|
+
workspaceBuildOutputs: (workspaceOutputs?.workspaces ?? []).map((w) => ({
|
|
1751
|
+
dir: w.dir,
|
|
1752
|
+
projectRoot: w.projectRoot,
|
|
1753
|
+
bytes: w.bytes,
|
|
1754
|
+
idleDays: w.idleDays,
|
|
1755
|
+
willClear: w.willClear,
|
|
1756
|
+
reason: w.keptCode,
|
|
1757
|
+
detail: w.keptReason
|
|
1758
|
+
})),
|
|
1759
|
+
caches: caches.map((c) => ({
|
|
1760
|
+
name: c.name,
|
|
1761
|
+
dir: c.dir,
|
|
1762
|
+
source: c.source ?? null,
|
|
1763
|
+
bytes: c.bytes ?? null,
|
|
1764
|
+
note: c.note || null,
|
|
1765
|
+
willEmpty: Boolean(c.willEmpty),
|
|
1766
|
+
emptySkipped: c.emptySkipped ?? null
|
|
1767
|
+
}))
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/commands/gc/worktrees.ts
|
|
1772
|
+
const DEFAULT_WORKTREE_IDLE_DAYS = 7;
|
|
1773
|
+
const DAY_MS = 864e5;
|
|
1774
|
+
function skip(code, text) {
|
|
1775
|
+
return {
|
|
1776
|
+
code,
|
|
1777
|
+
text
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function worktreeSkipReason(facts, olderThan) {
|
|
1781
|
+
if (facts.bare) return skip("bare-repository", "bare repository");
|
|
1782
|
+
if (typeof facts.source === "object") return skip("source-checkout-unknown", `source checkout unknown: ${facts.source.refusal}`);
|
|
1783
|
+
if (facts.source === "source") return skip("source-checkout", "source checkout");
|
|
1784
|
+
if (facts.locked) return skip("locked", "locked with git worktree lock");
|
|
1785
|
+
if (facts.inUse.length) return skip("in-use", `in use: ${facts.inUse.join("; ")}`);
|
|
1786
|
+
if (facts.porcelain === null) return skip("status-unreadable", "git status could not be read");
|
|
1787
|
+
if (excludePodChurn(facts.porcelain).lines.length) return skip("dirty", "dirty: uncommitted changes or untracked files");
|
|
1788
|
+
if (facts.unpushed === null) return skip("unpushed-unchecked", "unpushed commits could not be checked");
|
|
1789
|
+
if (facts.unpushed.length) return skip("unpushed", `unpushed: ${plural(facts.unpushed.length, "commit")} on no remote or other branch`);
|
|
1790
|
+
if (facts.submodules) return skip("submodules", "initialized submodules");
|
|
1791
|
+
if (facts.idleDays === null) return skip("last-use-unknown", "recently used: its last use is unknown");
|
|
1792
|
+
if (facts.idleDays < olderThan) return skip("recently-used", `recently used ${facts.idleDays}d ago`);
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
function lastUsedOf(keys) {
|
|
1796
|
+
const times = keys.map(workspaceLastUsed).filter(Number.isFinite);
|
|
1797
|
+
return times.length ? Math.max(...times) : NaN;
|
|
1798
|
+
}
|
|
1799
|
+
function idleDaysOf(keys, now) {
|
|
1800
|
+
const last = lastUsedOf(keys);
|
|
1801
|
+
return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS)) : null;
|
|
1802
|
+
}
|
|
1803
|
+
function inUseOf(keys, checks) {
|
|
1804
|
+
return [...new Set(keys.flatMap((key) => workspaceInUse(key, checks)))];
|
|
1805
|
+
}
|
|
1806
|
+
function porcelainOf(path, gitAnswered) {
|
|
1807
|
+
if (gitAnswered === null) return null;
|
|
1808
|
+
if (!gitAnswered) return [];
|
|
1809
|
+
const lines = dirtyPaths(path, { limit: Infinity });
|
|
1810
|
+
return lines.length ? lines : null;
|
|
1811
|
+
}
|
|
1812
|
+
function candidateRoots() {
|
|
1813
|
+
const registered = Object.keys(loadConfig()?.projects ?? {}).filter(isAbsolute);
|
|
1814
|
+
const recorded = listWorkspaceDirs().flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : []);
|
|
1815
|
+
return [.../* @__PURE__ */ new Set([...registered, ...recorded])].filter((root) => existsSync(root)).toSorted();
|
|
1816
|
+
}
|
|
1817
|
+
function collectWorktreeSweep({ olderThan, now }) {
|
|
1818
|
+
const days = olderThan ?? DEFAULT_WORKTREE_IDLE_DAYS;
|
|
1819
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1820
|
+
const outside = [];
|
|
1821
|
+
for (const root of candidateRoots()) {
|
|
1822
|
+
const entry = matchWorktreeEntry(listWorktrees(root), root);
|
|
1823
|
+
if (!entry) {
|
|
1824
|
+
outside.push({
|
|
1825
|
+
path: root,
|
|
1826
|
+
keys: [root],
|
|
1827
|
+
idleDays: null,
|
|
1828
|
+
skipCode: "not-a-worktree",
|
|
1829
|
+
skipped: "not inside a git worktree"
|
|
1830
|
+
});
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
groups.set(entry.path, [...groups.get(entry.path) ?? [], root]);
|
|
1834
|
+
}
|
|
1835
|
+
const worktrees = [];
|
|
1836
|
+
for (const [path, roots] of groups) {
|
|
1837
|
+
const entries = listWorktrees(path);
|
|
1838
|
+
const entry = matchWorktreeEntry(entries, path);
|
|
1839
|
+
const source = sourceCheckoutOf(entries);
|
|
1840
|
+
const keys = [.../* @__PURE__ */ new Set([...roots, ...reclaimKeys(path)])];
|
|
1841
|
+
const idleDays = idleDaysOf(keys, now);
|
|
1842
|
+
const linked = !("refusal" in source) && entry !== null && source.path !== entry.path;
|
|
1843
|
+
const gitAnswered = linked ? hasUncommittedWork(path) : null;
|
|
1844
|
+
const verdict = worktreeSkipReason({
|
|
1845
|
+
source: "refusal" in source ? source : linked ? "linked" : "source",
|
|
1846
|
+
bare: Boolean(entry?.bare),
|
|
1847
|
+
locked: Boolean(entry?.locked),
|
|
1848
|
+
porcelain: porcelainOf(path, gitAnswered),
|
|
1849
|
+
unpushed: linked ? unpushedCommits(path) : null,
|
|
1850
|
+
submodules: linked && hasPopulatedSubmodules(path),
|
|
1851
|
+
inUse: linked ? inUseOf(keys, { managedLocks: true }) : [],
|
|
1852
|
+
idleDays
|
|
1853
|
+
}, days);
|
|
1854
|
+
worktrees.push({
|
|
1855
|
+
path,
|
|
1856
|
+
keys,
|
|
1857
|
+
idleDays,
|
|
1858
|
+
skipCode: verdict?.code ?? null,
|
|
1859
|
+
skipped: verdict?.text ?? null
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
olderThan: days,
|
|
1864
|
+
defaulted: olderThan === null,
|
|
1865
|
+
worktrees: [...worktrees, ...outside]
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
async function removeWorktrees(sweep, { now = Date.now() } = {}) {
|
|
1869
|
+
let failures = 0;
|
|
1870
|
+
for (const candidate of sweep.worktrees) {
|
|
1871
|
+
if (candidate.skipped) continue;
|
|
1872
|
+
let kept = [];
|
|
1873
|
+
const guard = (lockedKeys) => {
|
|
1874
|
+
const keys = [.../* @__PURE__ */ new Set([...candidate.keys, ...lockedKeys])];
|
|
1875
|
+
const unlocked = keys.filter((key) => !lockedKeys.includes(key));
|
|
1876
|
+
const reasons = [...inUseOf(lockedKeys, { managedLocks: false }), ...inUseOf(unlocked, { managedLocks: true })].map((r) => `in use: ${r}`);
|
|
1877
|
+
const idleDays = idleDaysOf(keys, now);
|
|
1878
|
+
if (idleDays === null || idleDays < sweep.olderThan) reasons.push(`used ${idleDays === null ? "at an unknown time" : `${idleDays}d ago`} since gc checked it`);
|
|
1879
|
+
kept = reasons;
|
|
1880
|
+
return reasons;
|
|
1881
|
+
};
|
|
1882
|
+
let removed = false;
|
|
1883
|
+
try {
|
|
1884
|
+
removed = await removeWorktreeTarget(candidate.path, {
|
|
1885
|
+
linkedOnly: true,
|
|
1886
|
+
guard
|
|
1887
|
+
});
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
console.error(chalk.red(`Could not remove ${candidate.path}: ${error?.message || String(error)}`));
|
|
1890
|
+
}
|
|
1891
|
+
if (removed) console.log(chalk.green(`Removed the worktree ${candidate.path}`));
|
|
1892
|
+
else if (kept.length) console.log(chalk.yellow(`Kept the worktree ${candidate.path}: ${kept.join("; ")}`));
|
|
1893
|
+
else {
|
|
1894
|
+
failures++;
|
|
1895
|
+
console.error(chalk.yellow(`Kept the worktree ${candidate.path}; see the lines above for why.`));
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return failures;
|
|
1899
|
+
}
|
|
1316
1900
|
//#endregion
|
|
1317
1901
|
//#region src/commands/gc.ts
|
|
1318
1902
|
function removeInvalidProjectEntries(invalidProjects) {
|
|
@@ -1321,21 +1905,16 @@ function removeInvalidProjectEntries(invalidProjects) {
|
|
|
1321
1905
|
console.log(chalk.green(`Removed the invalid registry entry ${path}`));
|
|
1322
1906
|
}
|
|
1323
1907
|
}
|
|
1324
|
-
function
|
|
1325
|
-
try {
|
|
1326
|
-
return statSync(path).mtimeMs;
|
|
1327
|
-
} catch {
|
|
1328
|
-
return NaN;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
async function collectGcReport({ olderThan = null, cache = null, now = Date.now(), lastTouched = projectLastTouched } = {}, deps = {}) {
|
|
1908
|
+
async function collectGcReport({ olderThan = null, cache = null, worktrees = false, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
|
|
1332
1909
|
const scope = typeof cache === "string" && cache.trim() ? cache : null;
|
|
1333
1910
|
const all = scope !== null && olderThan === null;
|
|
1334
|
-
const
|
|
1911
|
+
const withWorkspaces = includesWorkspaceOutputs(scope);
|
|
1912
|
+
const caches = planCacheEmptying(sizeCaches(selectCaches(discoverCaches(), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir))), all);
|
|
1335
1913
|
if (scope) return {
|
|
1336
1914
|
skipped: [],
|
|
1337
1915
|
deadProjects: [],
|
|
1338
1916
|
invalidProjects: [],
|
|
1917
|
+
orphanedWorkspaces: [],
|
|
1339
1918
|
orphanedDevices: [],
|
|
1340
1919
|
staleDevices: [],
|
|
1341
1920
|
staleDeviceRecords: [],
|
|
@@ -1363,6 +1942,11 @@ async function collectGcReport({ olderThan = null, cache = null, now = Date.now(
|
|
|
1363
1942
|
parkedSims: [],
|
|
1364
1943
|
parkedAvds: [],
|
|
1365
1944
|
caches,
|
|
1945
|
+
workspaceOutputs: withWorkspaces ? collectWorkspaceOutputs({
|
|
1946
|
+
olderThan,
|
|
1947
|
+
now
|
|
1948
|
+
}) : null,
|
|
1949
|
+
worktreeSweep: null,
|
|
1366
1950
|
cacheScope: scope,
|
|
1367
1951
|
olderThan,
|
|
1368
1952
|
all
|
|
@@ -1404,6 +1988,8 @@ async function collectGcReport({ olderThan = null, cache = null, now = Date.now(
|
|
|
1404
1988
|
});
|
|
1405
1989
|
} else deadProjects.push(path);
|
|
1406
1990
|
}
|
|
1991
|
+
const workspaceDirs = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
|
|
1992
|
+
skipped.push(...workspaceDirs.skipped);
|
|
1407
1993
|
const deviceSweepNotices = [];
|
|
1408
1994
|
let orphanedDevices = [];
|
|
1409
1995
|
let staleDevices = [];
|
|
@@ -1491,6 +2077,7 @@ async function collectGcReport({ olderThan = null, cache = null, now = Date.now(
|
|
|
1491
2077
|
port
|
|
1492
2078
|
}))),
|
|
1493
2079
|
invalidProjects,
|
|
2080
|
+
orphanedWorkspaces: workspaceDirs.orphaned,
|
|
1494
2081
|
parkedSims,
|
|
1495
2082
|
parkedAvds,
|
|
1496
2083
|
orphanedDevices,
|
|
@@ -1510,21 +2097,49 @@ async function collectGcReport({ olderThan = null, cache = null, now = Date.now(
|
|
|
1510
2097
|
deviceSweepNotices,
|
|
1511
2098
|
easSessionSweep,
|
|
1512
2099
|
caches,
|
|
2100
|
+
workspaceOutputs: collectWorkspaceOutputs({
|
|
2101
|
+
olderThan,
|
|
2102
|
+
now,
|
|
2103
|
+
exclude: [...workspaceDirs.orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)]
|
|
2104
|
+
}),
|
|
2105
|
+
worktreeSweep: worktrees ? collectWorktreeSweep({
|
|
2106
|
+
olderThan,
|
|
2107
|
+
now
|
|
2108
|
+
}) : null,
|
|
1513
2109
|
cacheScope: null,
|
|
1514
2110
|
olderThan,
|
|
1515
2111
|
all
|
|
1516
2112
|
};
|
|
1517
2113
|
}
|
|
1518
2114
|
async function runGc(opts = {}, deps = {}) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
const shapeErrors = (deps.settingShapeErrors ?? projectSettingShapeErrors)();
|
|
1522
|
-
if (shapeErrors.length) {
|
|
1523
|
-
for (const message of shapeErrors) console.error(chalk.red(message));
|
|
1524
|
-
console.error(chalk.dim(SETTING_SHAPE_REMEDY));
|
|
1525
|
-
process.exitCode = 1;
|
|
2115
|
+
if (!opts.json) {
|
|
2116
|
+
await sweep(opts, deps);
|
|
1526
2117
|
return;
|
|
1527
2118
|
}
|
|
2119
|
+
const log = console.log;
|
|
2120
|
+
console.log = console.error;
|
|
2121
|
+
let payload;
|
|
2122
|
+
try {
|
|
2123
|
+
payload = await sweep(opts, deps);
|
|
2124
|
+
} finally {
|
|
2125
|
+
console.log = log;
|
|
2126
|
+
}
|
|
2127
|
+
console.log(JSON.stringify(payload));
|
|
2128
|
+
}
|
|
2129
|
+
async function sweep(opts, deps) {
|
|
2130
|
+
if (opts.cache && opts.worktrees) {
|
|
2131
|
+
console.error(chalk.red("--cache acts only on the named caches, and --worktrees sweeps linked worktrees."));
|
|
2132
|
+
console.error(chalk.dim("Run `stim gc --worktrees` and `stim gc --cache <name>` separately."));
|
|
2133
|
+
console.error(chalk.red("failed: STIM_BAD_ARG"));
|
|
2134
|
+
process.exitCode = 1;
|
|
2135
|
+
return {
|
|
2136
|
+
code: "STIM_BAD_ARG",
|
|
2137
|
+
message: "--cache acts only on the named caches, and --worktrees sweeps linked worktrees.",
|
|
2138
|
+
remedy: "Run `stim gc --worktrees` and `stim gc --cache <name>` separately."
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
|
|
2142
|
+
if (poolError) console.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
|
|
1528
2143
|
if (opts.cache) return runGcCore(opts, {
|
|
1529
2144
|
...deps,
|
|
1530
2145
|
precollectedEasSessionSweep: {
|
|
@@ -1596,25 +2211,45 @@ async function runGcCore(opts, deps) {
|
|
|
1596
2211
|
const cache = typeof opts.cache === "string" && opts.cache.trim() ? opts.cache : null;
|
|
1597
2212
|
const report = await collectGcReport({
|
|
1598
2213
|
olderThan,
|
|
1599
|
-
cache
|
|
2214
|
+
cache,
|
|
2215
|
+
worktrees: Boolean(opts.worktrees)
|
|
1600
2216
|
}, deps);
|
|
1601
|
-
if (cache && report.caches.length === 0) {
|
|
1602
|
-
const names = [...new Set(discoverCaches(
|
|
1603
|
-
|
|
2217
|
+
if (cache && report.caches.length === 0 && report.workspaceOutputs === null) {
|
|
2218
|
+
const names = [...new Set(discoverCaches().map((c) => c.name))];
|
|
2219
|
+
const message = `No shared cache carries "${cache}" in its name or directory.`;
|
|
2220
|
+
console.log(chalk.yellow(message));
|
|
1604
2221
|
if (names.length) console.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
|
|
1605
|
-
|
|
2222
|
+
if (opts.json) process.exitCode = 1;
|
|
2223
|
+
return {
|
|
2224
|
+
code: "STIM_BAD_ARG",
|
|
2225
|
+
message,
|
|
2226
|
+
remedy: names.length ? `Pass --cache all or a name from: ${names.join(", ")}.` : "No shared cache was found on this machine."
|
|
2227
|
+
};
|
|
1606
2228
|
}
|
|
1607
2229
|
const all = report.all;
|
|
1608
2230
|
for (const line of formatGcReport(report)) console.log(line);
|
|
1609
2231
|
const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches } = report;
|
|
1610
|
-
const actionable = deadProjects.length + invalidProjects.length > 0 || 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;
|
|
2232
|
+
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;
|
|
2233
|
+
const payload = (failures) => ({
|
|
2234
|
+
mode: opts.delete ? "delete" : "dry-run",
|
|
2235
|
+
cacheScope: report.cacheScope,
|
|
2236
|
+
olderThan,
|
|
2237
|
+
worktreeSweep: report.worktreeSweep ? {
|
|
2238
|
+
olderThan: report.worktreeSweep.olderThan,
|
|
2239
|
+
defaulted: report.worktreeSweep.defaulted
|
|
2240
|
+
} : null,
|
|
2241
|
+
actionable,
|
|
2242
|
+
failures,
|
|
2243
|
+
sections: gcReportSections(report)
|
|
2244
|
+
});
|
|
1611
2245
|
if (!opts.delete) {
|
|
1612
2246
|
if (all) console.log(chalk.dim("\nDry run. Re-run with --delete to empty the caches above."));
|
|
1613
2247
|
else if (actionable) console.log(chalk.dim("\nDry run. Re-run with --delete to reclaim."));
|
|
1614
2248
|
else if (caches.length) console.log(chalk.dim("\nPass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them."));
|
|
1615
|
-
return;
|
|
2249
|
+
return payload(null);
|
|
1616
2250
|
}
|
|
1617
|
-
let deleteFailures =
|
|
2251
|
+
let deleteFailures = report.workspaceOutputs ? await clearWorkspaceOutputs(report.workspaceOutputs, { olderThan }) : 0;
|
|
2252
|
+
deleteFailures += deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
|
|
1618
2253
|
removeInvalidProjectEntries(invalidProjects);
|
|
1619
2254
|
for (const path of deadProjects) {
|
|
1620
2255
|
if (existsSync(path) || !isOnMountedVolume(path)) {
|
|
@@ -1638,7 +2273,9 @@ async function runGcCore(opts, deps) {
|
|
|
1638
2273
|
if (result.stoppedSession) console.log(chalk.dim(` stopped remote session ${result.stoppedSession}`));
|
|
1639
2274
|
if (result.stoppedTunnel) console.log(chalk.dim(` stopped ${result.stoppedTunnel} tunnel`));
|
|
1640
2275
|
for (const s of result.skippedDevices) console.log(chalk.yellow(` ${s.name}: ${s.reason}`));
|
|
2276
|
+
deleteFailures += result.failedDevices.length;
|
|
1641
2277
|
}
|
|
2278
|
+
deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces);
|
|
1642
2279
|
deleteFailures += deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
|
|
1643
2280
|
for (const lock of buildLocks.stale) {
|
|
1644
2281
|
const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
|
|
@@ -1682,26 +2319,26 @@ async function runGcCore(opts, deps) {
|
|
|
1682
2319
|
console.log(chalk.red(`Failed to clear the device lease at ${entry.path}: ${err?.message || err}`));
|
|
1683
2320
|
}
|
|
1684
2321
|
deleteFailures += await deleteEasSessions(easSessionSweep, deps);
|
|
1685
|
-
if (
|
|
1686
|
-
if (
|
|
1687
|
-
|
|
1688
|
-
|
|
2322
|
+
if (report.worktreeSweep) deleteFailures += await removeWorktrees(report.worktreeSweep);
|
|
2323
|
+
if (deleteFailures) {
|
|
2324
|
+
console.log(chalk.red(`\n${deleteFailures} entr${deleteFailures === 1 ? "y" : "ies"} could not be deleted; see above.`));
|
|
2325
|
+
process.exitCode = 1;
|
|
1689
2326
|
}
|
|
1690
|
-
if (
|
|
2327
|
+
if (all) emptyCaches(caches);
|
|
2328
|
+
else if (olderThan === null) {
|
|
1691
2329
|
if (caches.length) console.log(chalk.dim("Shared caches left alone: pass --cache all to empty them, or --older-than <days> to trim them."));
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
trimCaches(caches, olderThan);
|
|
2330
|
+
} else trimCaches(caches, olderThan);
|
|
2331
|
+
return payload(deleteFailures);
|
|
1695
2332
|
}
|
|
1696
2333
|
function gcCommand(program) {
|
|
1697
|
-
program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases,
|
|
2334
|
+
program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, and the build outputs of each workspace. Reports by default; pass --delete to act.").option("--worktrees", "also report every clean, idle, Stim-managed linked worktree, and with --delete run `stim worktree remove` (never --force) on each; idle means unused for --older-than days, 7 without it").option("--delete", "actually prune the reported entries, reap the reported devices, and clear the build outputs of every workspace not in use").option("--older-than <days>", "also reap owned devices whose workspace has not been used this long, trim shared cache entries nothing has used in that time, and clear workspace build outputs only for workspaces idle this long", (v) => {
|
|
1698
2335
|
const n = parseInt(v, 10);
|
|
1699
2336
|
if (!Number.isFinite(n) || String(n) !== String(v).trim()) throw new InvalidArgumentError("must be a whole number of days, e.g. --older-than 30");
|
|
1700
2337
|
return n;
|
|
1701
|
-
}).option("--cache <name>", "act on the shared caches whose name or directory contains <name>,
|
|
2338
|
+
}).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache and the workspace build outputs with --cache all, or only the workspace build outputs with --cache workspaces; all and workspaces are reserved names that never select a single cache. With --delete they are emptied whole, which is the only way to clear an index-backed cache; add --older-than <days> to trim them by age instead. Only those caches are reported; devices and project entries are not inspected. Caches outside the config dir are refused while STIM_HOME is set.", (v) => {
|
|
1702
2339
|
if (!v.trim()) throw new InvalidArgumentError("must name a cache, e.g. --cache \"compilation cache\"");
|
|
1703
2340
|
return v;
|
|
1704
|
-
}).action(async (opts) => {
|
|
2341
|
+
}).option("--json", "print the report as JSON on stdout; every other line goes to stderr").action(async (opts) => {
|
|
1705
2342
|
await runGc(opts);
|
|
1706
2343
|
});
|
|
1707
2344
|
}
|