stim 1.11.0 → 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/{activity-BtG4Va9l.mjs → activity-CCZJs__P.mjs} +4 -3
- package/dist/{android-C4zG2Tuc.mjs → android-75Sk2rIR.mjs} +25 -8
- package/dist/{android-C1q4l523.mjs → android-B7X-8H7h.mjs} +75 -32
- package/dist/{android-BA2i_KAV.mjs → android-BQiKJYK0.mjs} +1 -1
- package/dist/{android-cas-DfJkwCXW.mjs → android-cas-DzLPXiEn.mjs} +7 -7
- package/dist/{app-install-DHut6JnV.mjs → app-install-CsZi0SAT.mjs} +1 -1
- package/dist/{workspaces-COD2xCz-.mjs → budget-DbzW5VTD.mjs} +641 -32
- package/dist/{build-plan-CeAYgKAx.mjs → build-plan-CqntbR7b.mjs} +120 -35
- package/dist/{build-progress-CDwogz6R.mjs → build-progress-h60GPV27.mjs} +3 -3
- 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-OF7CD5Dx.mjs → deps-CO-_7UOm.mjs} +2 -2
- package/dist/{device-BwH2iI3O.mjs → device-9r9xRYDa.mjs} +7 -7
- package/dist/{device-lease-D83nqeMX.mjs → device-lease-B9LAXIUH.mjs} +5 -5
- package/dist/{device-pool-DHj7cdPy.mjs → device-pool-BoezJd-c.mjs} +4 -4
- package/dist/{device-remote-BQZSnnmE.mjs → device-remote-C0_o-tJH.mjs} +254 -579
- package/dist/{doctor-D8QdMRMf.mjs → doctor-LfDlbR-N.mjs} +19 -18
- package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-CCxjn31V.mjs} +7 -7
- package/dist/{gc-COZteyiq.mjs → gc-C4nQFcTX.mjs} +203 -39
- package/dist/{guide-NDvTmMi-.mjs → guide-BPBlh9ML.mjs} +189 -38
- package/dist/{guide-status-CWvti2Lg.mjs → guide-status-i9ogHt4v.mjs} +2 -2
- package/dist/{idle-NETbeNiQ.mjs → idle-S3tFZ53_.mjs} +7 -7
- package/dist/{in-use-DVhlSoyn.mjs → in-use-D9XYEgFD.mjs} +7 -7
- package/dist/{ios-CQLKpNfr.mjs → ios-CDzWL1mj.mjs} +30 -29
- package/dist/{ios-DArbmwer.mjs → ios-CZWOHTTc.mjs} +23 -19
- package/dist/{ios-device-CwDZo0X4.mjs → ios-device-D9cLxVr5.mjs} +1 -1
- package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-B2oR_kFN.mjs} +2 -2
- package/dist/{logs-B44a1UeU.mjs → logs-SzuYudOj.mjs} +6 -6
- package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
- package/dist/{metro-BWLOJ_Wr.mjs → metro-Dzo30hvJ.mjs} +1 -1
- package/dist/{named-ports-Dc7neAlc.mjs → named-ports-Dn1o61D9.mjs} +3 -3
- package/dist/{ownership-BiojdtAp.mjs → ownership-B8d4kXBx.mjs} +49 -16
- package/dist/{ownership-D2xbLVoz.mjs → ownership-Dqnrjrio.mjs} +2 -2
- package/dist/{ports-DwBaRFJs.mjs → ports-Dp_QTwTC.mjs} +3 -3
- package/dist/{project-BXZzOABt.mjs → project-B8rbutVC.mjs} +4 -72
- package/dist/{reload-By9Dwsrt.mjs → reload-nocmx8KB.mjs} +9 -9
- package/dist/remote-cache-BRsRZru5.mjs +526 -0
- package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Gn3ssSdK.mjs} +1 -1
- package/dist/{server-expo-o6H5gcBc.mjs → server-expo-CeT1nIIk.mjs} +2 -2
- package/dist/server-expo-eWi9Coof.mjs +2 -0
- package/dist/{settings-B5oFZEri.mjs → settings-BlyDn2Oz.mjs} +2 -2
- package/dist/{settings-C0ugALSC.mjs → settings-bE-Gu3Ti.mjs} +23 -11
- package/dist/settings.schema.json +22 -2
- package/dist/{simslim-CIR3IYJO.mjs → simslim-COtEEP7x.mjs} +6 -5
- package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-DIACGEfH.mjs} +4 -4
- package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
- package/dist/{start-i8zc9VmS.mjs → start-BFstVBlx.mjs} +1 -1
- package/dist/{start-BSsf2tUs.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-Q-WDEGCB.mjs → status-BUASYyy_.mjs} +73 -28
- package/dist/{status-CitcBRsC.mjs → status-DI9Tjo7T.mjs} +41 -29
- package/dist/stim-desktop-D0xhpZt8.mjs +29 -0
- package/dist/{stop-rst5mL14.mjs → stop-BI7QY76M.mjs} +2 -2
- package/dist/{stop-DIhrvu0B.mjs → stop-Bn6v-GvS.mjs} +23 -16
- package/dist/supervisor-run.mjs +11 -11
- package/dist/{workspace-process-lock-Cx74RSDZ.mjs → workspace-process-lock-BvvJTfo_.mjs} +2 -2
- package/dist/{worktree-DhxDTK44.mjs → worktree-B5sTRskL.mjs} +1 -1
- package/dist/{worktree-KOjMX3zA.mjs → worktree-ChDL5fO1.mjs} +17 -16
- package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
- package/dist/{xcode-B5SPD--o.mjs → xcode-BzplpQzc.mjs} +7 -7
- package/package.json +5 -5
- package/dist/budget-De3P2rq_.mjs +0 -620
- package/dist/native-run-D7yB_AsB.mjs +0 -143
- package/dist/server-expo-B-CCUJZj.mjs +0 -2
|
@@ -1,25 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as registeredCaches } from "./cache-manifest-
|
|
3
|
-
import { M as workspaceName$1, v as workspaceLastUsed } from "./metro-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import { B as deviceSlotPlatforms, E as withConfigLock$1, F as measuredDirectorySize, I as volumeRootFor, M as formatBytes, N as isOnMountedVolume, O as process_identity_exports, P as listMountedVolumes, U as projectDeviceSlots, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as directorySize, o as clearSupervisor, p as getProject } from "./config-0jCcMM0E.mjs";
|
|
2
|
+
import { i as registeredCaches } from "./cache-manifest-DWPF4RKJ.mjs";
|
|
3
|
+
import { M as workspaceName$1, O as workspaceDir, S as sharedBuildCache, m as readWorkspaceState$1, s as resolveProjectMetro, t as killMetroTree, v as workspaceLastUsed } from "./metro-Dzo30hvJ.mjs";
|
|
4
|
+
import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
|
|
5
|
+
import { s as clearWorkspaceSupervisor } from "./state-DcDgzfu3.mjs";
|
|
6
|
+
import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-BvvJTfo_.mjs";
|
|
7
|
+
import { l as phaseLine, u as plural } from "./command-output-BWCj5Vh-.mjs";
|
|
8
|
+
import { i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-B9LAXIUH.mjs";
|
|
9
|
+
import { a as workspaceActivity } from "./activity-CCZJs__P.mjs";
|
|
10
|
+
import { L as ownedAvdSerialResolver, O as listAvds, P as ownedAvdDirectory, Q as isStimOwnedSim, T as isStimOwnedAvdName } from "./android-75Sk2rIR.mjs";
|
|
11
|
+
import { _ as dropParked, b as readParked, d as teardownOwnedAvd, f as teardownOwnedIosSim, m as teardownParkedIosSim, p as teardownParkedAvd, t as resolveSupervisorTarget } from "./ownership-B8d4kXBx.mjs";
|
|
12
|
+
import { d as listIosDeviceTypes, g as parseRuntimeVersion, l as listAllIosSims } from "./ios-CZWOHTTc.mjs";
|
|
13
|
+
import { i as idleShutdownCandidates, n as collectOwnedDeviceActivity, o as shutDownIdleDevices, s as workspaceBuildInProgress, t as collectIdleDevices } from "./idle-S3tFZ53_.mjs";
|
|
14
|
+
import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-D9XYEgFD.mjs";
|
|
15
|
+
import { c as formatSpace, r as capacity, s as environmentState } from "./status-BUASYyy_.mjs";
|
|
16
|
+
import { existsSync, statSync, statfsSync } from "node:fs";
|
|
17
|
+
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
11
18
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
12
19
|
import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
|
|
13
|
-
import { isJsonObject, readJsonFile } from "@stim-cli/core/state";
|
|
20
|
+
import { isJsonObject, readJsonFile, settingDefinition, settingValueError } from "@stim-cli/core/state";
|
|
21
|
+
import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
|
|
14
22
|
import { homedir, tmpdir } from "os";
|
|
23
|
+
import { homedir as homedir$1, platform, totalmem as totalmem$1 } from "node:os";
|
|
15
24
|
import chalk from "chalk";
|
|
25
|
+
//#region src/supervisor/cache-reset.ts
|
|
26
|
+
async function stopOwnedMetro(root) {
|
|
27
|
+
const project = getProject(root);
|
|
28
|
+
const port = project?.metroPort;
|
|
29
|
+
const target = resolveSupervisorTarget({
|
|
30
|
+
state: readWorkspaceState$1(root)?.supervisor,
|
|
31
|
+
record: project?.supervisor,
|
|
32
|
+
reservedPort: port
|
|
33
|
+
});
|
|
34
|
+
if (target.status === "unverified") return {
|
|
35
|
+
status: "unverified",
|
|
36
|
+
reason: target.reason ?? "unknown identity"
|
|
37
|
+
};
|
|
38
|
+
if (port && target.status !== "ours" && !(await resolveProjectMetro(port, root)).missing) return {
|
|
39
|
+
status: "external",
|
|
40
|
+
reason: `a dev server Stim did not start answers on port ${port}`
|
|
41
|
+
};
|
|
42
|
+
if (target.status !== "ours") return { status: "not-running" };
|
|
43
|
+
const expected = {
|
|
44
|
+
pid: target.pid,
|
|
45
|
+
processToken: target.processToken
|
|
46
|
+
};
|
|
47
|
+
if (!killMetroTree(target.pid, target.processToken) || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
|
|
48
|
+
status: "stuck",
|
|
49
|
+
reason: `supervisor pid ${target.pid} did not exit`
|
|
50
|
+
};
|
|
51
|
+
if (port && !(await resolveProjectMetro(port, root)).missing) return {
|
|
52
|
+
status: "port-occupied",
|
|
53
|
+
reason: `port ${port} is still occupied after stopping Metro`
|
|
54
|
+
};
|
|
55
|
+
clearWorkspaceSupervisor(root, expected);
|
|
56
|
+
clearSupervisor(root, expected);
|
|
57
|
+
return { status: "stopped" };
|
|
58
|
+
}
|
|
59
|
+
async function stopOwnedMetroForReset(root) {
|
|
60
|
+
const stopped = await stopOwnedMetro(root);
|
|
61
|
+
switch (stopped.status) {
|
|
62
|
+
case "unverified": throw supervisorError("STIM_SUPERVISOR_EXITED", `Cannot reset Metro: ${stopped.reason}.`, "Stop it with the tool that started it, then retry. Stim leaves unverified processes alone.");
|
|
63
|
+
case "external": throw supervisorError("STIM_SUPERVISOR_EXITED", "Cannot reset an externally started dev server.", "Stop it with the tool that started it, then retry `stim start --reset-cache`.");
|
|
64
|
+
case "stuck": throw supervisorError("STIM_SUPERVISOR_EXITED", "The owned Metro supervisor could not be stopped for a cache reset.", "Inspect `stim status` and `stim logs`, then retry. No cache reset was recorded.");
|
|
65
|
+
case "port-occupied": throw supervisorError("STIM_SUPERVISOR_EXITED", `Port ${getProject(root)?.metroPort} is still occupied after stopping Metro.`, "Inspect the listener before retrying. Stim does not terminate unrecorded processes.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
16
69
|
//#region src/cache/caches.ts
|
|
17
70
|
function xcodeDerivedDataRoot() {
|
|
18
71
|
return join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
19
72
|
}
|
|
20
73
|
function compilationCache() {
|
|
21
74
|
const dir = join(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
22
|
-
if (!existsSync(dir)) return null;
|
|
75
|
+
if (!existsSync$1(dir)) return null;
|
|
23
76
|
return {
|
|
24
77
|
name: "Xcode compilation cache",
|
|
25
78
|
dir,
|
|
@@ -30,7 +83,7 @@ function compilationCache() {
|
|
|
30
83
|
function gradleBuildCache() {
|
|
31
84
|
const gradleHome = process.env.GRADLE_USER_HOME || join(homedir(), ".gradle");
|
|
32
85
|
const dir = resolve(gradleHome, "caches", "build-cache-1");
|
|
33
|
-
if (!existsSync(dir)) return null;
|
|
86
|
+
if (!existsSync$1(dir)) return null;
|
|
34
87
|
return {
|
|
35
88
|
name: "Gradle build cache",
|
|
36
89
|
dir,
|
|
@@ -40,10 +93,10 @@ function gradleBuildCache() {
|
|
|
40
93
|
}
|
|
41
94
|
function metroFileMaps() {
|
|
42
95
|
const root = tmpdir();
|
|
43
|
-
if (!existsSync(root)) return null;
|
|
96
|
+
if (!existsSync$1(root)) return null;
|
|
44
97
|
let names;
|
|
45
98
|
try {
|
|
46
|
-
names = readdirSync(root).filter((n) => n.startsWith("metro-file-map-"));
|
|
99
|
+
names = readdirSync$1(root).filter((n) => n.startsWith("metro-file-map-"));
|
|
47
100
|
} catch {
|
|
48
101
|
return null;
|
|
49
102
|
}
|
|
@@ -51,7 +104,7 @@ function metroFileMaps() {
|
|
|
51
104
|
const files = [];
|
|
52
105
|
let bytes = 0;
|
|
53
106
|
for (const n of names) try {
|
|
54
|
-
const st = statSync(join(root, n));
|
|
107
|
+
const st = statSync$1(join(root, n));
|
|
55
108
|
if (uid !== void 0 && st.uid !== uid) continue;
|
|
56
109
|
files.push(join(root, n));
|
|
57
110
|
bytes += st.size;
|
|
@@ -168,7 +221,7 @@ function cachePathContains(parent, child) {
|
|
|
168
221
|
}
|
|
169
222
|
function canonicalCacheDir(dir) {
|
|
170
223
|
try {
|
|
171
|
-
return realpathSync(dir);
|
|
224
|
+
return realpathSync$1(dir);
|
|
172
225
|
} catch {
|
|
173
226
|
return resolve(dir);
|
|
174
227
|
}
|
|
@@ -196,7 +249,7 @@ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
|
196
249
|
let used;
|
|
197
250
|
let size;
|
|
198
251
|
try {
|
|
199
|
-
const st = statSync(entry);
|
|
252
|
+
const st = statSync$1(entry);
|
|
200
253
|
used = Math.max(st.atimeMs, st.mtimeMs);
|
|
201
254
|
size = st.isDirectory() ? directorySize(entry) : st.size;
|
|
202
255
|
} catch {
|
|
@@ -204,7 +257,7 @@ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
|
204
257
|
}
|
|
205
258
|
if (used >= cutoff) continue;
|
|
206
259
|
try {
|
|
207
|
-
rmSync(entry, {
|
|
260
|
+
rmSync$1(entry, {
|
|
208
261
|
recursive: true,
|
|
209
262
|
force: true
|
|
210
263
|
});
|
|
@@ -228,7 +281,7 @@ function entriesAtDepth(dir, depth) {
|
|
|
228
281
|
for (const parent of level) {
|
|
229
282
|
let names;
|
|
230
283
|
try {
|
|
231
|
-
names = readdirSync(parent);
|
|
284
|
+
names = readdirSync$1(parent);
|
|
232
285
|
} catch {
|
|
233
286
|
continue;
|
|
234
287
|
}
|
|
@@ -313,7 +366,7 @@ function emptyCache(cache) {
|
|
|
313
366
|
});
|
|
314
367
|
let names;
|
|
315
368
|
try {
|
|
316
|
-
names = readdirSync(cache.dir);
|
|
369
|
+
names = readdirSync$1(cache.dir);
|
|
317
370
|
} catch (e) {
|
|
318
371
|
return {
|
|
319
372
|
removed: 0,
|
|
@@ -324,7 +377,7 @@ function emptyCache(cache) {
|
|
|
324
377
|
let removed = 0;
|
|
325
378
|
let failed = 0;
|
|
326
379
|
for (const name of names) try {
|
|
327
|
-
rmSync(join(cache.dir, name), {
|
|
380
|
+
rmSync$1(join(cache.dir, name), {
|
|
328
381
|
recursive: true,
|
|
329
382
|
force: true
|
|
330
383
|
});
|
|
@@ -383,7 +436,7 @@ function emptyCaches(caches) {
|
|
|
383
436
|
}
|
|
384
437
|
//#endregion
|
|
385
438
|
//#region src/commands/gc/devices.ts
|
|
386
|
-
const DEVICE_LIST_TIMEOUT_MS = 3e4;
|
|
439
|
+
const DEVICE_LIST_TIMEOUT_MS$1 = 3e4;
|
|
387
440
|
const AVD_SIZE_TIMEOUT_MS = 5e3;
|
|
388
441
|
const DAY_MS$1 = 864e5;
|
|
389
442
|
function describeKept(ref) {
|
|
@@ -579,7 +632,7 @@ function collectParkedSims(deps, age = {}) {
|
|
|
579
632
|
let deviceTypes = [];
|
|
580
633
|
try {
|
|
581
634
|
sims = (deps.listAllIosSims ?? listAllIosSims)({
|
|
582
|
-
timeoutMs: DEVICE_LIST_TIMEOUT_MS,
|
|
635
|
+
timeoutMs: DEVICE_LIST_TIMEOUT_MS$1,
|
|
583
636
|
includeUnavailable: true
|
|
584
637
|
});
|
|
585
638
|
} catch {
|
|
@@ -690,7 +743,7 @@ function collectDeviceLeases(now) {
|
|
|
690
743
|
expired.push(entry);
|
|
691
744
|
continue;
|
|
692
745
|
}
|
|
693
|
-
if (!existsSync(lease.holder)) kept.push({
|
|
746
|
+
if (!existsSync$1(lease.holder)) kept.push({
|
|
694
747
|
name: entry.name,
|
|
695
748
|
path: entry.path,
|
|
696
749
|
reason: `${lease.holder} is not on this machine, but the lease runs until ${lease.expiresAt}`
|
|
@@ -785,7 +838,7 @@ function listWorkspaceDirs() {
|
|
|
785
838
|
const root = workspacesRoot();
|
|
786
839
|
let names;
|
|
787
840
|
try {
|
|
788
|
-
names = readdirSync(root).toSorted();
|
|
841
|
+
names = readdirSync$1(root).toSorted();
|
|
789
842
|
} catch {
|
|
790
843
|
return [];
|
|
791
844
|
}
|
|
@@ -793,7 +846,7 @@ function listWorkspaceDirs() {
|
|
|
793
846
|
for (const name of names) {
|
|
794
847
|
const dir = join(root, name);
|
|
795
848
|
try {
|
|
796
|
-
if (!statSync(dir).isDirectory()) continue;
|
|
849
|
+
if (!statSync$1(dir).isDirectory()) continue;
|
|
797
850
|
} catch {
|
|
798
851
|
continue;
|
|
799
852
|
}
|
|
@@ -852,7 +905,7 @@ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse
|
|
|
852
905
|
}
|
|
853
906
|
function rootPresence(path) {
|
|
854
907
|
try {
|
|
855
|
-
lstatSync(path);
|
|
908
|
+
lstatSync$1(path);
|
|
856
909
|
return true;
|
|
857
910
|
} catch (error) {
|
|
858
911
|
const code = error?.code;
|
|
@@ -899,7 +952,7 @@ function sizeOf(dir) {
|
|
|
899
952
|
return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
|
|
900
953
|
}
|
|
901
954
|
function outputPaths(dir) {
|
|
902
|
-
return WORKSPACE_OUTPUT_DIRS.map((name) => join(dir, name)).filter((path) => existsSync(path));
|
|
955
|
+
return WORKSPACE_OUTPUT_DIRS.map((name) => join(dir, name)).filter((path) => existsSync$1(path));
|
|
903
956
|
}
|
|
904
957
|
function outputBytes(paths) {
|
|
905
958
|
let total = 0;
|
|
@@ -947,7 +1000,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
947
1000
|
now
|
|
948
1001
|
});
|
|
949
1002
|
if (again?.keptReason) return again.keptReason;
|
|
950
|
-
for (const name of WORKSPACE_OUTPUT_DIRS) rmSync(join(entry.dir, name), {
|
|
1003
|
+
for (const name of WORKSPACE_OUTPUT_DIRS) rmSync$1(join(entry.dir, name), {
|
|
951
1004
|
recursive: true,
|
|
952
1005
|
force: true
|
|
953
1006
|
});
|
|
@@ -980,7 +1033,7 @@ function stillOrphaned({ dir, projectRoot }) {
|
|
|
980
1033
|
async function deleteOrphanedWorkspaces(orphaned) {
|
|
981
1034
|
let failures = 0;
|
|
982
1035
|
for (const entry of orphaned) {
|
|
983
|
-
if (!existsSync(entry.dir)) continue;
|
|
1036
|
+
if (!existsSync$1(entry.dir)) continue;
|
|
984
1037
|
let run;
|
|
985
1038
|
try {
|
|
986
1039
|
run = await withIdleWorkspace(entry.projectRoot, () => {
|
|
@@ -1010,4 +1063,560 @@ function sizeText(bytes) {
|
|
|
1010
1063
|
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
1011
1064
|
}
|
|
1012
1065
|
//#endregion
|
|
1013
|
-
|
|
1066
|
+
//#region src/budget.ts
|
|
1067
|
+
const MB = 1048576;
|
|
1068
|
+
const GB_IN_MB = 1024;
|
|
1069
|
+
const COMMITTED_MEMORY_SHARE = .6;
|
|
1070
|
+
const STALE_CACHE_DAYS = 14;
|
|
1071
|
+
const DEVICE_LIST_TIMEOUT_MS = 1e4;
|
|
1072
|
+
const USAGE_TIMEOUT_MS = 1e4;
|
|
1073
|
+
const BUDGET_SETTING_REMEDY = "Fix the value with `stim settings set <key> <value>`, or unset the environment variable. Run `stim guide settings` for the budget keys.";
|
|
1074
|
+
function valueAt(source, key) {
|
|
1075
|
+
let node = source;
|
|
1076
|
+
for (const part of key.split(".")) {
|
|
1077
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
|
|
1078
|
+
node = node[part];
|
|
1079
|
+
}
|
|
1080
|
+
return node;
|
|
1081
|
+
}
|
|
1082
|
+
function machineNumber(key, config, env) {
|
|
1083
|
+
const setting = settingDefinition(key);
|
|
1084
|
+
const fromEnv = setting.env ? env[setting.env] : void 0;
|
|
1085
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
1086
|
+
const value = fromEnv.trim() === "" ? NaN : Number(fromEnv);
|
|
1087
|
+
const problem = settingValueError(setting, value);
|
|
1088
|
+
return problem ? {
|
|
1089
|
+
value: void 0,
|
|
1090
|
+
error: `Invalid ${setting.env} value ${JSON.stringify(fromEnv)}. Expected ${problem}.`
|
|
1091
|
+
} : {
|
|
1092
|
+
value,
|
|
1093
|
+
error: null
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
if (env.STIM_HOME && setting.scopedHomeValue !== void 0) return {
|
|
1097
|
+
value: setting.scopedHomeValue,
|
|
1098
|
+
error: null
|
|
1099
|
+
};
|
|
1100
|
+
const raw = valueAt(config, key);
|
|
1101
|
+
if (raw === void 0) return {
|
|
1102
|
+
value: typeof setting.default === "number" ? setting.default : void 0,
|
|
1103
|
+
error: null
|
|
1104
|
+
};
|
|
1105
|
+
const problem = settingValueError(setting, raw);
|
|
1106
|
+
return problem ? {
|
|
1107
|
+
value: void 0,
|
|
1108
|
+
error: `Invalid ${key} value ${JSON.stringify(raw)}. Expected ${problem}.`
|
|
1109
|
+
} : {
|
|
1110
|
+
value: raw,
|
|
1111
|
+
error: null
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemoryMb = totalmem$1() / MB } = {}) {
|
|
1115
|
+
const minFree = machineNumber("budget.minFreeDiskGb", config, env);
|
|
1116
|
+
const hardFloor = machineNumber("budget.hardFloorDiskGb", config, env);
|
|
1117
|
+
const memory = machineNumber("budget.maxCommittedMemoryGb", config, env);
|
|
1118
|
+
const live = machineNumber("budget.maxLiveWorkspaces", config, env);
|
|
1119
|
+
return {
|
|
1120
|
+
budget: {
|
|
1121
|
+
minFreeDiskMb: (minFree.value ?? 0) * GB_IN_MB,
|
|
1122
|
+
hardFloorDiskMb: (hardFloor.value ?? 0) * GB_IN_MB,
|
|
1123
|
+
maxCommittedMemoryMb: memory.value === void 0 ? Math.round(totalMemoryMb * COMMITTED_MEMORY_SHARE) : memory.value * GB_IN_MB,
|
|
1124
|
+
maxLiveWorkspaces: live.value ?? 0
|
|
1125
|
+
},
|
|
1126
|
+
error: minFree.error ?? hardFloor.error ?? memory.error ?? live.error
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function existingAncestor(path) {
|
|
1130
|
+
let current = path;
|
|
1131
|
+
while (!existsSync(current) && dirname$1(current) !== current) current = dirname$1(current);
|
|
1132
|
+
return current;
|
|
1133
|
+
}
|
|
1134
|
+
function readVolumeSpace(paths) {
|
|
1135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1136
|
+
const volumes = [];
|
|
1137
|
+
for (const path of paths) try {
|
|
1138
|
+
const existing = existingAncestor(path);
|
|
1139
|
+
const device = statSync(existing).dev;
|
|
1140
|
+
if (seen.has(device)) continue;
|
|
1141
|
+
seen.add(device);
|
|
1142
|
+
const fs = statfsSync(existing);
|
|
1143
|
+
volumes.push({
|
|
1144
|
+
volume: volumeRootFor(path),
|
|
1145
|
+
freeMb: fs.bavail * fs.bsize / MB
|
|
1146
|
+
});
|
|
1147
|
+
} catch {}
|
|
1148
|
+
return volumes;
|
|
1149
|
+
}
|
|
1150
|
+
function estimateMemoryUse({ config, sims, androidSerial, supervisorRunning }) {
|
|
1151
|
+
const simsByUdid = Object.fromEntries((sims ?? []).map((sim) => [sim.udid, sim]));
|
|
1152
|
+
const runtime = (avdName) => {
|
|
1153
|
+
if (!avdName) return null;
|
|
1154
|
+
const serial = androidSerial(avdName);
|
|
1155
|
+
return {
|
|
1156
|
+
serial,
|
|
1157
|
+
state: serial ? "detected" : "not-detected"
|
|
1158
|
+
};
|
|
1159
|
+
};
|
|
1160
|
+
const states = Object.entries(config?.projects ?? {}).map(([path, project]) => {
|
|
1161
|
+
const pid = supervisorRunning(path, project);
|
|
1162
|
+
const slots = projectDeviceSlots(project);
|
|
1163
|
+
return environmentState({
|
|
1164
|
+
...project,
|
|
1165
|
+
__path: path
|
|
1166
|
+
}, {
|
|
1167
|
+
simsByUdid,
|
|
1168
|
+
simsAvailable: sims !== null,
|
|
1169
|
+
metro: pid === null ? { missing: true } : { metro: { pid } },
|
|
1170
|
+
androidRuntime: project.platforms?.android?.owned ? runtime(project.platforms.android.avdName) : null,
|
|
1171
|
+
androidRuntimes: Object.fromEntries(slots.slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned ? runtime(platforms.android.avdName) : null]))
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
const { committedMb, liveCount } = capacity(states, 0);
|
|
1175
|
+
return {
|
|
1176
|
+
committedMb,
|
|
1177
|
+
liveWorkspaces: liveCount
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function listSims() {
|
|
1181
|
+
try {
|
|
1182
|
+
return listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
|
|
1183
|
+
} catch {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function runningSupervisorPid(path, project) {
|
|
1188
|
+
const target = resolveSupervisorTarget({
|
|
1189
|
+
state: readWorkspaceState$1(path)?.supervisor,
|
|
1190
|
+
record: project.supervisor,
|
|
1191
|
+
reservedPort: project.metroPort
|
|
1192
|
+
});
|
|
1193
|
+
return target.status === "ours" ? target.pid ?? null : null;
|
|
1194
|
+
}
|
|
1195
|
+
function readMemoryUse() {
|
|
1196
|
+
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
1197
|
+
return estimateMemoryUse({
|
|
1198
|
+
config: loadConfig$1(),
|
|
1199
|
+
sims: listSims(),
|
|
1200
|
+
androidSerial: (avdName) => {
|
|
1201
|
+
try {
|
|
1202
|
+
return resolve(avdName).serial ?? null;
|
|
1203
|
+
} catch {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
supervisorRunning: runningSupervisorPid
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
function budgetShortfalls(budget, measure) {
|
|
1211
|
+
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
1212
|
+
return {
|
|
1213
|
+
disk: floor > 0 ? measure.volumes.filter((v) => v.freeMb < floor) : [],
|
|
1214
|
+
hardFloor: budget.hardFloorDiskMb > 0 ? measure.volumes.filter((v) => v.freeMb < budget.hardFloorDiskMb) : [],
|
|
1215
|
+
memory: Boolean(budget.maxCommittedMemoryMb > 0 && measure.memory && measure.memory.committedMb > budget.maxCommittedMemoryMb),
|
|
1216
|
+
workspaces: Boolean(budget.maxLiveWorkspaces > 0 && measure.memory && measure.memory.liveWorkspaces > budget.maxLiveWorkspaces)
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const DISK_ONLY = ["workspace-outputs", "stale-cache-entries"];
|
|
1220
|
+
function reclaimPlan(short) {
|
|
1221
|
+
const disk = short.disk.length > 0;
|
|
1222
|
+
return [...disk || short.memory || short.workspaces ? ["idle-devices", "idle-dev-servers"] : [], ...disk ? DISK_ONLY : []];
|
|
1223
|
+
}
|
|
1224
|
+
function overBudget(short) {
|
|
1225
|
+
return short.disk.length > 0 || short.memory || short.workspaces;
|
|
1226
|
+
}
|
|
1227
|
+
function isOtherWorkspace(root) {
|
|
1228
|
+
const self = canonicalPath(root);
|
|
1229
|
+
return (path) => canonicalPath(path) !== self;
|
|
1230
|
+
}
|
|
1231
|
+
function deviceLabel(device) {
|
|
1232
|
+
return `${device.kind} ${device.name} in ${device.project}`;
|
|
1233
|
+
}
|
|
1234
|
+
function reclaimIdleDevices({ root, dryRun, note, now }) {
|
|
1235
|
+
if (deviceSweepIsScoped()) {
|
|
1236
|
+
note(chalk.dim(phaseLine("budget", "idle devices left alone: STIM_HOME scopes this config, devices are global")));
|
|
1237
|
+
return {
|
|
1238
|
+
targets: [],
|
|
1239
|
+
failures: 0
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const other = isOtherWorkspace(root);
|
|
1243
|
+
const collect = (at) => idleShutdownCandidates(collectIdleDevices(loadConfig$1(), listSims() ?? [], [], at).filter((device) => other(device.project)), 0);
|
|
1244
|
+
const idle = collect(now);
|
|
1245
|
+
if (dryRun || idle.length === 0) return {
|
|
1246
|
+
targets: idle.map(deviceLabel),
|
|
1247
|
+
failures: 0
|
|
1248
|
+
};
|
|
1249
|
+
const { failures, shutDown } = shutDownIdleDevices(idle, 0, () => collect(Date.now()));
|
|
1250
|
+
return {
|
|
1251
|
+
targets: shutDown.map(deviceLabel),
|
|
1252
|
+
failures
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function idleSince(path, now) {
|
|
1256
|
+
const activity = workspaceActivity(path, now);
|
|
1257
|
+
const at = Date.parse(activity.lastActivityAt ?? "");
|
|
1258
|
+
return activity.state === "idle" && Number.isFinite(at) ? at : null;
|
|
1259
|
+
}
|
|
1260
|
+
async function reclaimIdleDevServers({ root, dryRun, note, now }) {
|
|
1261
|
+
const config = loadConfig$1();
|
|
1262
|
+
const other = isOtherWorkspace(root);
|
|
1263
|
+
const serving = Object.entries(config?.projects ?? {}).flatMap(([path, project]) => {
|
|
1264
|
+
const pid = other(path) ? runningSupervisorPid(path, project) : null;
|
|
1265
|
+
return pid !== null && !workspaceBuildInProgress(path) && idleSince(path, now) !== null ? [{
|
|
1266
|
+
path,
|
|
1267
|
+
pid
|
|
1268
|
+
}] : [];
|
|
1269
|
+
});
|
|
1270
|
+
if (serving.length === 0) return {
|
|
1271
|
+
targets: [],
|
|
1272
|
+
failures: 0
|
|
1273
|
+
};
|
|
1274
|
+
const sims = listSims();
|
|
1275
|
+
if (sims === null) {
|
|
1276
|
+
note(chalk.dim(phaseLine("budget", "dev servers left alone: simulators could not be listed to prove them idle")));
|
|
1277
|
+
return {
|
|
1278
|
+
targets: [],
|
|
1279
|
+
failures: 0
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const busy = new Set(collectOwnedDeviceActivity(config, sims, now).filter((device) => device.activity.state !== "idle").map((device) => device.project));
|
|
1283
|
+
const idle = serving.filter(({ path }) => !busy.has(path));
|
|
1284
|
+
if (dryRun) return {
|
|
1285
|
+
targets: idle.map(({ path }) => path),
|
|
1286
|
+
failures: 0
|
|
1287
|
+
};
|
|
1288
|
+
const targets = [];
|
|
1289
|
+
let failures = 0;
|
|
1290
|
+
const stillIdle = (path, pid) => {
|
|
1291
|
+
const project = loadConfig$1()?.projects?.[path];
|
|
1292
|
+
return Boolean(project && runningSupervisorPid(path, project) === pid && !workspaceBuildInProgress(path) && idleSince(path, Date.now()) !== null);
|
|
1293
|
+
};
|
|
1294
|
+
for (const { path, pid } of idle) {
|
|
1295
|
+
let run;
|
|
1296
|
+
try {
|
|
1297
|
+
run = await withIdleWorkspace(path, async () => {
|
|
1298
|
+
try {
|
|
1299
|
+
return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path) : "busy", {
|
|
1300
|
+
external: true,
|
|
1301
|
+
waitMs: 0,
|
|
1302
|
+
ownerPurpose: "budget reclaim"
|
|
1303
|
+
});
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (workspaceProcessLockError(error)) return "starting";
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
}, {
|
|
1309
|
+
purpose: "budget reclaim",
|
|
1310
|
+
supervisor: false
|
|
1311
|
+
});
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
failures++;
|
|
1314
|
+
note(chalk.red(`Could not stop the dev server of ${path}: ${error?.message || error}`));
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (!run.ran) {
|
|
1318
|
+
note(chalk.yellow(`Kept the dev server of ${path}: ${run.reasons.join("; ")}`));
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const stopped = run.value;
|
|
1322
|
+
if (stopped === "busy" || stopped === "starting") {
|
|
1323
|
+
const why = stopped === "busy" ? "it is no longer idle" : "a stim start holds its metro-start lock";
|
|
1324
|
+
note(chalk.dim(`Kept the dev server of ${path}: ${why}`));
|
|
1325
|
+
} else if (stopped.status === "stopped") {
|
|
1326
|
+
targets.push(path);
|
|
1327
|
+
note(chalk.green(`Stopped the idle dev server of ${path}`));
|
|
1328
|
+
} else if ("reason" in stopped) {
|
|
1329
|
+
if (stopped.status === "stuck") failures++;
|
|
1330
|
+
note(chalk.yellow(`Kept the dev server of ${path}: ${stopped.reason}`));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
targets,
|
|
1335
|
+
failures
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
async function reclaimWorkspaceOutputs({ root, dryRun, now, diskRecovered }) {
|
|
1339
|
+
const other = isOtherWorkspace(root);
|
|
1340
|
+
const report = collectWorkspaceOutputs({
|
|
1341
|
+
olderThan: null,
|
|
1342
|
+
now,
|
|
1343
|
+
measure: false
|
|
1344
|
+
});
|
|
1345
|
+
const clearable = report.workspaces.flatMap((entry) => {
|
|
1346
|
+
const project = entry.projectRoot;
|
|
1347
|
+
const since = project !== null && entry.willClear && other(project) ? idleSince(project, now) : null;
|
|
1348
|
+
return since === null ? [] : [{
|
|
1349
|
+
entry,
|
|
1350
|
+
since
|
|
1351
|
+
}];
|
|
1352
|
+
}).toSorted((a, b) => a.since - b.since).map(({ entry }) => entry);
|
|
1353
|
+
if (dryRun) return {
|
|
1354
|
+
targets: clearable.map((entry) => entry.projectRoot),
|
|
1355
|
+
failures: 0
|
|
1356
|
+
};
|
|
1357
|
+
const targets = [];
|
|
1358
|
+
let failures = 0;
|
|
1359
|
+
for (const entry of clearable) {
|
|
1360
|
+
const result = await clearWorkspaceOutputs({
|
|
1361
|
+
...report,
|
|
1362
|
+
workspaces: [entry]
|
|
1363
|
+
}, {
|
|
1364
|
+
olderThan: null,
|
|
1365
|
+
now
|
|
1366
|
+
});
|
|
1367
|
+
failures += result.failures;
|
|
1368
|
+
targets.push(...result.cleared);
|
|
1369
|
+
if (diskRecovered()) break;
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
targets,
|
|
1373
|
+
failures
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function cacheLabel(cache) {
|
|
1377
|
+
return `${cache.name} ${cache.dir}`;
|
|
1378
|
+
}
|
|
1379
|
+
function reclaimStaleCacheEntries({ dryRun }) {
|
|
1380
|
+
const caches = planCacheEmptying(discoverCaches(), false).filter((cache) => cache.prune !== "report-only" && cache.prune !== "atomic" && !cache.machineGlobal);
|
|
1381
|
+
if (dryRun) return {
|
|
1382
|
+
targets: caches.map(cacheLabel),
|
|
1383
|
+
failures: 0
|
|
1384
|
+
};
|
|
1385
|
+
return {
|
|
1386
|
+
targets: trimCaches(caches, STALE_CACHE_DAYS).map(cacheLabel),
|
|
1387
|
+
failures: 0
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
const STEPS = {
|
|
1391
|
+
"idle-devices": reclaimIdleDevices,
|
|
1392
|
+
"idle-dev-servers": reclaimIdleDevServers,
|
|
1393
|
+
"workspace-outputs": reclaimWorkspaceOutputs,
|
|
1394
|
+
"stale-cache-entries": reclaimStaleCacheEntries
|
|
1395
|
+
};
|
|
1396
|
+
const STEP_ACTIONS = {
|
|
1397
|
+
"idle-devices": "shut down idle owned devices",
|
|
1398
|
+
"idle-dev-servers": "stop idle dev servers",
|
|
1399
|
+
"workspace-outputs": "clear build outputs of idle workspaces",
|
|
1400
|
+
"stale-cache-entries": `trim cache entries unused for ${STALE_CACHE_DAYS} days`
|
|
1401
|
+
};
|
|
1402
|
+
async function runStep(step, context) {
|
|
1403
|
+
const log = console.log;
|
|
1404
|
+
const exitCode = process.exitCode;
|
|
1405
|
+
console.log = console.error;
|
|
1406
|
+
try {
|
|
1407
|
+
const result = await STEPS[step](context);
|
|
1408
|
+
return process.exitCode === exitCode ? result : {
|
|
1409
|
+
...result,
|
|
1410
|
+
failures: result.failures + 1
|
|
1411
|
+
};
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
context.note(chalk.red(phaseLine("budget", `${STEP_ACTIONS[step]} failed: ${error?.message || error}`)));
|
|
1414
|
+
return {
|
|
1415
|
+
targets: [],
|
|
1416
|
+
failures: 1
|
|
1417
|
+
};
|
|
1418
|
+
} finally {
|
|
1419
|
+
console.log = log;
|
|
1420
|
+
process.exitCode = exitCode;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function spaceUsers() {
|
|
1424
|
+
const home = homedir$1();
|
|
1425
|
+
const candidates = [
|
|
1426
|
+
["Stim state", getConfigDir()],
|
|
1427
|
+
["shared build cache", sharedBuildCache()],
|
|
1428
|
+
...platform() === "darwin" ? [["simulators", join$1(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join$1(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
|
|
1429
|
+
["Android emulators", join$1(home, ".android", "avd")]
|
|
1430
|
+
];
|
|
1431
|
+
const measured = /* @__PURE__ */ new Set();
|
|
1432
|
+
const users = [];
|
|
1433
|
+
for (const [label, path] of candidates) {
|
|
1434
|
+
const canonical = canonicalPath(path);
|
|
1435
|
+
if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep$1}`))) continue;
|
|
1436
|
+
measured.add(canonical);
|
|
1437
|
+
if (!existsSync(path)) continue;
|
|
1438
|
+
const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
|
|
1439
|
+
if (bytes) users.push({
|
|
1440
|
+
label: `${label} ${path}`,
|
|
1441
|
+
bytes
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
return users.toSorted((a, b) => b.bytes - a.bytes).map(({ label, bytes }) => `${label}: ${formatBytes(bytes)}`);
|
|
1445
|
+
}
|
|
1446
|
+
const DEFAULT_DEPS = {
|
|
1447
|
+
volumes: readVolumeSpace,
|
|
1448
|
+
memory: readMemoryUse,
|
|
1449
|
+
step: runStep,
|
|
1450
|
+
usage: spaceUsers,
|
|
1451
|
+
now: Date.now
|
|
1452
|
+
};
|
|
1453
|
+
const LOW_DISK_REMEDY = "Run `stim gc` to see what else Stim can reclaim, then `stim gc --delete` (add `--cache all` to empty the shared caches, or `--worktrees` to remove finished worktrees). Free space outside Stim from the largest uses listed, or lower budget.hardFloorDiskGb.";
|
|
1454
|
+
function describeShortfall(budget, measure, short) {
|
|
1455
|
+
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
1456
|
+
return [
|
|
1457
|
+
...short.disk.map((v) => `${formatSpace(v.freeMb)} free on ${v.volume}, below the ${formatSpace(floor)} floor`),
|
|
1458
|
+
...short.memory && measure.memory ? [`roughly ${formatSpace(measure.memory.committedMb)} of memory committed, over the ${formatSpace(budget.maxCommittedMemoryMb)} budget`] : [],
|
|
1459
|
+
...short.workspaces && measure.memory ? [`${measure.memory.liveWorkspaces} live workspaces, over the limit of ${budget.maxLiveWorkspaces}`] : []
|
|
1460
|
+
];
|
|
1461
|
+
}
|
|
1462
|
+
async function enforceBudget({ root, note, dryRun = false, budget: resolved }, overrides = {}) {
|
|
1463
|
+
const deps = {
|
|
1464
|
+
...DEFAULT_DEPS,
|
|
1465
|
+
...overrides
|
|
1466
|
+
};
|
|
1467
|
+
let budget = resolved;
|
|
1468
|
+
if (!budget) {
|
|
1469
|
+
const read = resolveBudget();
|
|
1470
|
+
if (read.error) return {
|
|
1471
|
+
status: "invalid",
|
|
1472
|
+
refusal: {
|
|
1473
|
+
code: "STIM_BAD_ARG",
|
|
1474
|
+
message: read.error,
|
|
1475
|
+
remedy: BUDGET_SETTING_REMEDY
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
budget = read.budget;
|
|
1479
|
+
}
|
|
1480
|
+
const paths = [root, getConfigDir()];
|
|
1481
|
+
const measureNow = () => ({
|
|
1482
|
+
volumes: budget.minFreeDiskMb > 0 || budget.hardFloorDiskMb > 0 ? deps.volumes(paths) : [],
|
|
1483
|
+
memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? deps.memory() : null
|
|
1484
|
+
});
|
|
1485
|
+
let measure = measureNow();
|
|
1486
|
+
let short = budgetShortfalls(budget, measure);
|
|
1487
|
+
const reclaimed = [];
|
|
1488
|
+
const plan = reclaimPlan(short);
|
|
1489
|
+
if (plan.length) note(chalk.yellow(phaseLine("budget", `${describeShortfall(budget, measure, short).join("; ")}; ${dryRun ? "would reclaim" : "reclaiming"}`)));
|
|
1490
|
+
for (const step of plan) {
|
|
1491
|
+
if (!dryRun && (!overBudget(short) || DISK_ONLY.includes(step) && short.disk.length === 0)) break;
|
|
1492
|
+
const freeBefore = measure.volumes.reduce((sum, v) => sum + v.freeMb, 0);
|
|
1493
|
+
const result = await deps.step(step, {
|
|
1494
|
+
root,
|
|
1495
|
+
dryRun,
|
|
1496
|
+
note,
|
|
1497
|
+
now: deps.now(),
|
|
1498
|
+
diskRecovered: () => budgetShortfalls(budget, {
|
|
1499
|
+
volumes: deps.volumes(paths),
|
|
1500
|
+
memory: null
|
|
1501
|
+
}).disk.length === 0
|
|
1502
|
+
});
|
|
1503
|
+
if (!dryRun) {
|
|
1504
|
+
measure = measureNow();
|
|
1505
|
+
short = budgetShortfalls(budget, measure);
|
|
1506
|
+
}
|
|
1507
|
+
if (result.targets.length === 0 && result.failures === 0) continue;
|
|
1508
|
+
const freedMb = Math.max(0, Math.round(measure.volumes.reduce((sum, v) => sum + v.freeMb, 0) - freeBefore));
|
|
1509
|
+
reclaimed.push({
|
|
1510
|
+
step,
|
|
1511
|
+
...result,
|
|
1512
|
+
...dryRun ? {} : { freedMb }
|
|
1513
|
+
});
|
|
1514
|
+
const outcome = dryRun ? result.targets.join(", ") : `${result.targets.length}${result.failures ? `, ${result.failures} failed` : ""} (${formatSpace(freedMb)} freed)`;
|
|
1515
|
+
note(chalk.dim(phaseLine("budget", `${STEP_ACTIONS[step]}: ${outcome}`)));
|
|
1516
|
+
}
|
|
1517
|
+
if (!dryRun && short.hardFloor.length) {
|
|
1518
|
+
const tight = short.hardFloor.map((v) => `${formatSpace(v.freeMb)} on ${v.volume}`).join(" and ");
|
|
1519
|
+
const users = deps.usage();
|
|
1520
|
+
return {
|
|
1521
|
+
status: "refused",
|
|
1522
|
+
reclaimed,
|
|
1523
|
+
refusal: {
|
|
1524
|
+
code: "STIM_LOW_DISK",
|
|
1525
|
+
message: `Only ${tight} is free, below the ${formatSpace(budget.hardFloorDiskMb)} hard floor (budget.hardFloorDiskGb)${reclaimed.length ? " after reclaiming what Stim safely could" : ""}, so this run could fill the disk.` + (users.length ? ` Largest uses: ${users.join("; ")}.` : ""),
|
|
1526
|
+
remedy: LOW_DISK_REMEDY
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
if (!dryRun && overBudget(short)) note(chalk.yellow(phaseLine("budget", `still over budget: ${describeShortfall(budget, measure, short).join("; ")}`)));
|
|
1531
|
+
return {
|
|
1532
|
+
status: "ok",
|
|
1533
|
+
reclaimed,
|
|
1534
|
+
budget,
|
|
1535
|
+
measure,
|
|
1536
|
+
short
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
async function budgetGate(args, overrides = {}) {
|
|
1540
|
+
const outcome = await enforceBudget(args, overrides);
|
|
1541
|
+
return {
|
|
1542
|
+
reclaimed: "reclaimed" in outcome ? outcome.reclaimed : [],
|
|
1543
|
+
refusal: outcome.status === "ok" ? null : outcome.refusal
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
const gb = (mb) => Math.round(mb / GB_IN_MB * 10) / 10;
|
|
1547
|
+
function budgetFindings(budget, measure, short, plan) {
|
|
1548
|
+
const findings = [];
|
|
1549
|
+
const steps = plan.length ? plan.map((entry) => `${STEP_ACTIONS[entry.step]} (${entry.targets.join(", ")})`).join("; then ") : "nothing Stim can reclaim on its own right now";
|
|
1550
|
+
if (short.disk.length) {
|
|
1551
|
+
const refuses = short.hardFloor.length ? ` Below ${formatSpace(budget.hardFloorDiskMb)} it refuses with STIM_LOW_DISK when reclaiming does not free enough.` : "";
|
|
1552
|
+
findings.push({
|
|
1553
|
+
level: "cost",
|
|
1554
|
+
title: "Free disk is below the Stim budget",
|
|
1555
|
+
detail: `${describeShortfall(budget, measure, {
|
|
1556
|
+
...short,
|
|
1557
|
+
memory: false,
|
|
1558
|
+
workspaces: false
|
|
1559
|
+
}).join("; ")}. The next ios, android or start will first ${steps}.${refuses}`,
|
|
1560
|
+
fix: "Run `stim gc` to see what Stim can reclaim, then `stim gc --delete`. Free space outside Stim, or lower budget.minFreeDiskGb."
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
if (short.memory || short.workspaces) findings.push({
|
|
1564
|
+
level: "note",
|
|
1565
|
+
title: "Live environments are over the memory budget",
|
|
1566
|
+
detail: `${describeShortfall(budget, measure, {
|
|
1567
|
+
...short,
|
|
1568
|
+
disk: []
|
|
1569
|
+
}).join("; ")}. The next ios, android or start shuts down idle owned devices and stops idle dev servers in other workspaces first.`,
|
|
1570
|
+
fix: "Run `stim status` to see what is live, and `stim stop` in workspaces you own that you no longer need."
|
|
1571
|
+
});
|
|
1572
|
+
return findings;
|
|
1573
|
+
}
|
|
1574
|
+
async function inspectBudget(root, deps = {}) {
|
|
1575
|
+
const read = resolveBudget();
|
|
1576
|
+
if (read.error) return {
|
|
1577
|
+
report: null,
|
|
1578
|
+
findings: [{
|
|
1579
|
+
level: "cost",
|
|
1580
|
+
title: "A budget setting is invalid",
|
|
1581
|
+
detail: read.error,
|
|
1582
|
+
fix: BUDGET_SETTING_REMEDY
|
|
1583
|
+
}]
|
|
1584
|
+
};
|
|
1585
|
+
const outcome = await enforceBudget({
|
|
1586
|
+
root,
|
|
1587
|
+
note: () => {},
|
|
1588
|
+
dryRun: true,
|
|
1589
|
+
budget: read.budget
|
|
1590
|
+
}, deps);
|
|
1591
|
+
if (outcome.status !== "ok") return {
|
|
1592
|
+
report: null,
|
|
1593
|
+
findings: []
|
|
1594
|
+
};
|
|
1595
|
+
const { budget, measure, short, reclaimed } = outcome;
|
|
1596
|
+
return {
|
|
1597
|
+
report: {
|
|
1598
|
+
budget: {
|
|
1599
|
+
minFreeDiskGb: gb(budget.minFreeDiskMb),
|
|
1600
|
+
hardFloorDiskGb: gb(budget.hardFloorDiskMb),
|
|
1601
|
+
maxCommittedMemoryGb: gb(budget.maxCommittedMemoryMb),
|
|
1602
|
+
maxLiveWorkspaces: budget.maxLiveWorkspaces
|
|
1603
|
+
},
|
|
1604
|
+
volumes: measure.volumes.map((v) => ({
|
|
1605
|
+
volume: v.volume,
|
|
1606
|
+
freeGb: gb(v.freeMb)
|
|
1607
|
+
})),
|
|
1608
|
+
memory: measure.memory ? {
|
|
1609
|
+
committedGb: gb(measure.memory.committedMb),
|
|
1610
|
+
liveWorkspaces: measure.memory.liveWorkspaces
|
|
1611
|
+
} : null,
|
|
1612
|
+
plan: reclaimed
|
|
1613
|
+
},
|
|
1614
|
+
findings: budgetFindings(budget, measure, short, reclaimed)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function budgetLine(report) {
|
|
1618
|
+
const { budget } = report;
|
|
1619
|
+
return `${report.volumes.length ? `${report.volumes.map((v) => `${v.freeGb} GB free on ${v.volume}`).join(", ")} (reclaims below ${Math.max(budget.minFreeDiskGb, budget.hardFloorDiskGb)} GB, ${budget.hardFloorDiskGb ? `refuses below ${budget.hardFloorDiskGb} GB` : "never refuses"})` : "disk budget off"}; ${report.memory ? `roughly ${report.memory.committedGb} GB of a ${budget.maxCommittedMemoryGb} GB memory budget committed` : "memory budget off"}`;
|
|
1620
|
+
}
|
|
1621
|
+
//#endregion
|
|
1622
|
+
export { includesWorkspaceOutputs as A, deviceSweepIsScoped as C, unverifiedDeviceCommand as D, findStaleProjectDevices as E, sizeCaches as F, stopOwnedMetroForReset as I, selectCaches as M, trimCaches as N, withAndroidAvdSizes as O, discoverCaches as P, describeUnverifiableDevices as S, findStaleDeviceRecords as T, collectParkedSims as _, REBUILD_COST as a, deleteProjectDevices as b, collectOrphanedWorkspaces as c, isInsideWorkspaces as d, listWorkspaceDirs as f, collectParkedAvds as g, collectDeviceLeases as h, machineNumber as i, planCacheEmptying as j, emptyCaches as k, collectWorkspaceOutputs as l, DEVICE_LIST_TIMEOUT_MS$1 as m, budgetLine as n, WORKSPACE_OUTPUT_DIRS as o, sizeText as p, inspectBudget as r, clearWorkspaceOutputs as s, budgetGate as t, deleteOrphanedWorkspaces as u, deleteParkedAvds as v, findOrphanedDevices as w, describeParkedSims as x, deleteParkedSims as y };
|