stim 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/{android-_y4Ohi5z.mjs → android-787HhP5w.mjs} +119 -29
  2. package/dist/{android-BPGizezh.mjs → android-DBkeHwpm.mjs} +168 -95
  3. package/dist/{android-DEgo4oCo.mjs → android-DPov-fv2.mjs} +2 -2
  4. package/dist/{android-cas-eiExNLf7.mjs → android-cas-CITmIeWY.mjs} +9 -8
  5. package/dist/android-cas-compiler.mjs +1 -1
  6. package/dist/{app-install-Uf3a83Tv.mjs → app-install-CrJ6t_bv.mjs} +2 -2
  7. package/dist/budget-BP4jZZL4.mjs +620 -0
  8. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  9. package/dist/{cache-manifest-Dg7OnMrp.mjs → cache-manifest-Pubfp8wV.mjs} +1 -1
  10. package/dist/cache-manifest.mjs +1 -1
  11. package/dist/cli.mjs +16 -16
  12. package/dist/collector-run.d.mts +5 -1
  13. package/dist/collector-run.mjs +7 -7
  14. package/dist/{command-output-gByVmIjy.mjs → command-output-D5z2_bcF.mjs} +1 -1
  15. package/dist/{config-Cp9lM3wX.mjs → config-BEq0Qw_a.mjs} +14 -131
  16. package/dist/{dependency-state-Bq5DcLGn.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  17. package/dist/{deps-Cip5vUm1.mjs → deps-DP4eWqea.mjs} +3 -3
  18. package/dist/{detached-entry-KEu0_AXL.mjs → detached-entry-DJHoca49.mjs} +1 -1
  19. package/dist/{dev-client-HyqISM0h.mjs → dev-client-92U593KT.mjs} +137 -250
  20. package/dist/{device-D4U-xLty.mjs → device-DhTZe7L4.mjs} +7 -7
  21. package/dist/{build-lock-DPXPLgTc.mjs → device-lease-CbEaIk7b.mjs} +370 -12
  22. package/dist/{device-pool-CnTAz8X1.mjs → device-pool-Di8Q-9Ro.mjs} +4 -4
  23. package/dist/{device-remote-AQ5rQPqr.mjs → device-remote-Dy1jGj3U.mjs} +547 -468
  24. package/dist/{doctor-B_Ei3bZu.mjs → doctor-B-inAJmt.mjs} +58 -22
  25. package/dist/{error-diagnostics-Vfz83GCF.mjs → error-diagnostics-xxWKNO3-.mjs} +11 -12
  26. package/dist/{exec-jIAlgilk.mjs → exec-DK8KaVHK.mjs} +3 -2
  27. package/dist/{gc-Ckw8RQHi.mjs → gc-ChPFZMRC.mjs} +500 -1114
  28. package/dist/{guide-BiAbhs1J.mjs → guide-DwmaD_Wa.mjs} +882 -528
  29. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  30. package/dist/idle-DpRLp5lj.mjs +157 -0
  31. package/dist/in-use-CjUluqIX.mjs +245 -0
  32. package/dist/{ios-BL2NFckr.mjs → ios-B2Fpqvlb.mjs} +25 -59
  33. package/dist/{ios-BzUJWk1K.mjs → ios-Dd-hYJBh.mjs} +130 -89
  34. package/dist/{ios-device-DPsSm0v_.mjs → ios-device-BiOsSVzd.mjs} +1 -1
  35. package/dist/{ios-device-D972i9wf.mjs → ios-device-CtRdEVjx.mjs} +3 -3
  36. package/dist/{launch-verify-Q_OtW1Rf.mjs → launch-verify-CQS_Kg-C.mjs} +5 -5
  37. package/dist/{logs-CI9NQSK7.mjs → logs-MARIy6m9.mjs} +9 -9
  38. package/dist/{metro-DrLHaMn2.mjs → metro-BWLOJ_Wr.mjs} +116 -10
  39. package/dist/{named-ports-CmLQSIRj.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  40. package/dist/{ndjson-9uFNRwfe.mjs → ndjson-CDY9IkVC.mjs} +3 -56
  41. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  42. package/dist/{ownership-D15Op8vN.mjs → ownership-D2xbLVoz.mjs} +2 -3
  43. package/dist/ownership-DZjdx8Gq.mjs +1180 -0
  44. package/dist/{ports-BNKoWVWZ.mjs → ports-CLSSFOuz.mjs} +3 -3
  45. package/dist/{project-DS112DYB.mjs → project-BXZzOABt.mjs} +2 -2
  46. package/dist/{reload-ZNJUo1pl.mjs → reload-DoETJ3rl.mjs} +9 -10
  47. package/dist/{server-bare-DWkkcIxV.mjs → server-bare-DVbb7kXH.mjs} +1 -1
  48. package/dist/{server-expo-B_lctBR3.mjs → server-expo-BvOCeakp.mjs} +4 -4
  49. package/dist/server-expo-DBVTdult.mjs +2 -0
  50. package/dist/{settings-D030k0t4.mjs → settings-DOugPlPz.mjs} +16 -428
  51. package/dist/{settings-BKNh5rl4.mjs → settings-EWYNwe72.mjs} +16 -14
  52. package/dist/settings.schema.json +75 -0
  53. package/dist/{simslim-B_LsGLuJ.mjs → simslim-Ca3487H6.mjs} +182 -5
  54. package/dist/{slot-launch-DU9A-gYw.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  55. package/dist/{spawn-claims-Dn3C_N6w.mjs → spawn-claims-CUleC2cM.mjs} +1 -1
  56. package/dist/{start-DYkkKF0s.mjs → start-F9gv5dcC.mjs} +147 -98
  57. package/dist/start-VoFsPSes.mjs +2 -0
  58. package/dist/{state-NcJzMEBF.mjs → state-WGdIbi_d.mjs} +2 -2
  59. package/dist/{stats-DG9e66Rz.mjs → stats-BAgJaLA4.mjs} +1 -1
  60. package/dist/{stats-B63-JVAY.mjs → stats-DBYJ5bxL.mjs} +4 -4
  61. package/dist/{status-S6ehL4Ui.mjs → status-DLCD-o3s.mjs} +63 -231
  62. package/dist/status-Dr4fRkym.mjs +216 -0
  63. package/dist/{guide-status-BAMVU0ZA.mjs → stim-installations-CINXy_su.mjs} +3 -124
  64. package/dist/{stop-BO28HGqF.mjs → stop-Dj4pfmyv.mjs} +22 -18
  65. package/dist/{stop-T1R-cZRz.mjs → stop-ZvOeTmKb.mjs} +2 -2
  66. package/dist/supervisor-run.d.mts +11 -2
  67. package/dist/supervisor-run.mjs +185 -17
  68. package/dist/{workspace-process-lock-DWaiRD0e.mjs → workspace-process-lock-BXA96myD.mjs} +2 -2
  69. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  70. package/dist/{worktree-DKhSF0UX.mjs → worktree-4aZg2NnI.mjs} +1 -1
  71. package/dist/{worktree-LGB-UwgG.mjs → worktree-BMS7GeqB.mjs} +40 -122
  72. package/dist/{worktree-R_cUlE7s.mjs → worktree-BlizKDpZ.mjs} +143 -18
  73. package/dist/{xcode-BZqyxh3e.mjs → xcode-B2tBkf0m.mjs} +9 -158
  74. package/package.json +6 -5
  75. package/dist/build-progress-CVqkfPC6.mjs +0 -188
  76. package/dist/device-lease-Crs-HkJY.mjs +0 -366
  77. package/dist/logs-query-Cqma3WIu.mjs +0 -298
  78. package/dist/ownership-DCQPuWEc.mjs +0 -489
  79. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  80. package/dist/server-expo-CeILEu86.mjs +0 -2
  81. package/dist/sim-pool-KaIsp2Ft.mjs +0 -723
  82. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  83. package/dist/workspace-state-DF2gCFhx.mjs +0 -169
@@ -0,0 +1,620 @@
1
+ import { D as process_identity_exports, F as volumeRootFor, H as projectDeviceSlots, P as measuredDirectorySize, a as clearSupervisor, f as getProject, g as loadConfig$1, j as formatBytes, u as getConfigDir } from "./config-BEq0Qw_a.mjs";
2
+ import { O as workspaceDir, S as sharedBuildCache, m as readWorkspaceState$1, s as resolveProjectMetro, t as killMetroTree } from "./metro-BWLOJ_Wr.mjs";
3
+ import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
4
+ import { o as clearWorkspaceSupervisor } from "./state-WGdIbi_d.mjs";
5
+ import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-BXA96myD.mjs";
6
+ import { l as phaseLine } from "./command-output-D5z2_bcF.mjs";
7
+ import { u as workspaceActivity } from "./build-progress-BXx-ZBe4.mjs";
8
+ import { N as ownedAvdSerialResolver } from "./android-787HhP5w.mjs";
9
+ import { t as resolveSupervisorTarget } from "./ownership-DZjdx8Gq.mjs";
10
+ import { l as listAllIosSims } from "./ios-B2Fpqvlb.mjs";
11
+ import { i as idleShutdownCandidates, n as collectOwnedDeviceActivity, o as shutDownIdleDevices, s as workspaceBuildInProgress, t as collectIdleDevices } from "./idle-DpRLp5lj.mjs";
12
+ import { i as canonicalPath, n as withIdleWorkspace } from "./in-use-CjUluqIX.mjs";
13
+ import { n as capacity, o as environmentState, s as formatSpace } from "./status-Dr4fRkym.mjs";
14
+ import { A as discoverCaches, D as planCacheEmptying, a as collectWorkspaceOutputs, k as trimCaches, r as clearWorkspaceOutputs, y as deviceSweepIsScoped } from "./workspaces-BTUyFfkv.mjs";
15
+ import { existsSync, statSync, statfsSync } from "node:fs";
16
+ import { settingDefinition, settingValueError } from "@stim-cli/core/state";
17
+ import { dirname, join, sep } from "node:path";
18
+ import { homedir, platform, totalmem } from "node:os";
19
+ import chalk from "chalk";
20
+ //#region src/supervisor/cache-reset.ts
21
+ async function stopOwnedMetro(root) {
22
+ const project = getProject(root);
23
+ const port = project?.metroPort;
24
+ const target = resolveSupervisorTarget({
25
+ state: readWorkspaceState$1(root)?.supervisor,
26
+ record: project?.supervisor,
27
+ reservedPort: port
28
+ });
29
+ if (target.status === "unverified") return {
30
+ status: "unverified",
31
+ reason: target.reason ?? "unknown identity"
32
+ };
33
+ if (port && target.status !== "ours" && !(await resolveProjectMetro(port, root)).missing) return {
34
+ status: "external",
35
+ reason: `a dev server Stim did not start answers on port ${port}`
36
+ };
37
+ if (target.status !== "ours") return { status: "not-running" };
38
+ const expected = {
39
+ pid: target.pid,
40
+ processToken: target.processToken
41
+ };
42
+ if (!killMetroTree(target.pid, target.processToken) || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
43
+ status: "stuck",
44
+ reason: `supervisor pid ${target.pid} did not exit`
45
+ };
46
+ if (port && !(await resolveProjectMetro(port, root)).missing) return {
47
+ status: "port-occupied",
48
+ reason: `port ${port} is still occupied after stopping Metro`
49
+ };
50
+ clearWorkspaceSupervisor(root, expected);
51
+ clearSupervisor(root, expected);
52
+ return { status: "stopped" };
53
+ }
54
+ async function stopOwnedMetroForReset(root) {
55
+ const stopped = await stopOwnedMetro(root);
56
+ switch (stopped.status) {
57
+ 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.");
58
+ 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`.");
59
+ 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.");
60
+ 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.");
61
+ }
62
+ }
63
+ //#endregion
64
+ //#region src/budget.ts
65
+ const MB = 1048576;
66
+ const GB_IN_MB = 1024;
67
+ const COMMITTED_MEMORY_SHARE = .6;
68
+ const STALE_CACHE_DAYS = 14;
69
+ const DEVICE_LIST_TIMEOUT_MS = 1e4;
70
+ const USAGE_TIMEOUT_MS = 1e4;
71
+ 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.";
72
+ function valueAt(source, key) {
73
+ let node = source;
74
+ for (const part of key.split(".")) {
75
+ if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
76
+ node = node[part];
77
+ }
78
+ return node;
79
+ }
80
+ function machineNumber(key, config, env) {
81
+ const setting = settingDefinition(key);
82
+ const fromEnv = setting.env ? env[setting.env] : void 0;
83
+ if (fromEnv !== void 0 && fromEnv !== "") {
84
+ const value = fromEnv.trim() === "" ? NaN : Number(fromEnv);
85
+ const problem = settingValueError(setting, value);
86
+ return problem ? {
87
+ value: void 0,
88
+ error: `Invalid ${setting.env} value ${JSON.stringify(fromEnv)}. Expected ${problem}.`
89
+ } : {
90
+ value,
91
+ error: null
92
+ };
93
+ }
94
+ if (env.STIM_HOME && setting.scopedHomeValue !== void 0) return {
95
+ value: setting.scopedHomeValue,
96
+ error: null
97
+ };
98
+ const raw = valueAt(config, key);
99
+ if (raw === void 0) return {
100
+ value: typeof setting.default === "number" ? setting.default : void 0,
101
+ error: null
102
+ };
103
+ const problem = settingValueError(setting, raw);
104
+ return problem ? {
105
+ value: void 0,
106
+ error: `Invalid ${key} value ${JSON.stringify(raw)}. Expected ${problem}.`
107
+ } : {
108
+ value: raw,
109
+ error: null
110
+ };
111
+ }
112
+ function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemoryMb = totalmem() / MB } = {}) {
113
+ const minFree = machineNumber("budget.minFreeDiskGb", config, env);
114
+ const hardFloor = machineNumber("budget.hardFloorDiskGb", config, env);
115
+ const memory = machineNumber("budget.maxCommittedMemoryGb", config, env);
116
+ const live = machineNumber("budget.maxLiveWorkspaces", config, env);
117
+ return {
118
+ budget: {
119
+ minFreeDiskMb: (minFree.value ?? 0) * GB_IN_MB,
120
+ hardFloorDiskMb: (hardFloor.value ?? 0) * GB_IN_MB,
121
+ maxCommittedMemoryMb: memory.value === void 0 ? Math.round(totalMemoryMb * COMMITTED_MEMORY_SHARE) : memory.value * GB_IN_MB,
122
+ maxLiveWorkspaces: live.value ?? 0
123
+ },
124
+ error: minFree.error ?? hardFloor.error ?? memory.error ?? live.error
125
+ };
126
+ }
127
+ function existingAncestor(path) {
128
+ let current = path;
129
+ while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
130
+ return current;
131
+ }
132
+ function readVolumeSpace(paths) {
133
+ const seen = /* @__PURE__ */ new Set();
134
+ const volumes = [];
135
+ for (const path of paths) try {
136
+ const existing = existingAncestor(path);
137
+ const device = statSync(existing).dev;
138
+ if (seen.has(device)) continue;
139
+ seen.add(device);
140
+ const fs = statfsSync(existing);
141
+ volumes.push({
142
+ volume: volumeRootFor(path),
143
+ freeMb: fs.bavail * fs.bsize / MB
144
+ });
145
+ } catch {}
146
+ return volumes;
147
+ }
148
+ function estimateMemoryUse({ config, sims, androidSerial, supervisorRunning }) {
149
+ const simsByUdid = Object.fromEntries((sims ?? []).map((sim) => [sim.udid, sim]));
150
+ const runtime = (avdName) => {
151
+ if (!avdName) return null;
152
+ const serial = androidSerial(avdName);
153
+ return {
154
+ serial,
155
+ state: serial ? "detected" : "not-detected"
156
+ };
157
+ };
158
+ const states = Object.entries(config?.projects ?? {}).map(([path, project]) => {
159
+ const pid = supervisorRunning(path, project);
160
+ const slots = projectDeviceSlots(project);
161
+ return environmentState({
162
+ ...project,
163
+ __path: path
164
+ }, {
165
+ simsByUdid,
166
+ simsAvailable: sims !== null,
167
+ metro: pid === null ? { missing: true } : { metro: { pid } },
168
+ androidRuntime: project.platforms?.android?.owned ? runtime(project.platforms.android.avdName) : null,
169
+ androidRuntimes: Object.fromEntries(slots.slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned ? runtime(platforms.android.avdName) : null]))
170
+ });
171
+ });
172
+ const { committedMb, liveCount } = capacity(states, 0);
173
+ return {
174
+ committedMb,
175
+ liveWorkspaces: liveCount
176
+ };
177
+ }
178
+ function listSims() {
179
+ try {
180
+ return listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ function runningSupervisorPid(path, project) {
186
+ const target = resolveSupervisorTarget({
187
+ state: readWorkspaceState$1(path)?.supervisor,
188
+ record: project.supervisor,
189
+ reservedPort: project.metroPort
190
+ });
191
+ return target.status === "ours" ? target.pid ?? null : null;
192
+ }
193
+ function readMemoryUse() {
194
+ const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
195
+ return estimateMemoryUse({
196
+ config: loadConfig$1(),
197
+ sims: listSims(),
198
+ androidSerial: (avdName) => {
199
+ try {
200
+ return resolve(avdName).serial ?? null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ },
205
+ supervisorRunning: runningSupervisorPid
206
+ });
207
+ }
208
+ function budgetShortfalls(budget, measure) {
209
+ const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
210
+ return {
211
+ disk: floor > 0 ? measure.volumes.filter((v) => v.freeMb < floor) : [],
212
+ hardFloor: budget.hardFloorDiskMb > 0 ? measure.volumes.filter((v) => v.freeMb < budget.hardFloorDiskMb) : [],
213
+ memory: Boolean(budget.maxCommittedMemoryMb > 0 && measure.memory && measure.memory.committedMb > budget.maxCommittedMemoryMb),
214
+ workspaces: Boolean(budget.maxLiveWorkspaces > 0 && measure.memory && measure.memory.liveWorkspaces > budget.maxLiveWorkspaces)
215
+ };
216
+ }
217
+ const DISK_ONLY = ["workspace-outputs", "stale-cache-entries"];
218
+ function reclaimPlan(short) {
219
+ const disk = short.disk.length > 0;
220
+ return [...disk || short.memory || short.workspaces ? ["idle-devices", "idle-dev-servers"] : [], ...disk ? DISK_ONLY : []];
221
+ }
222
+ function overBudget(short) {
223
+ return short.disk.length > 0 || short.memory || short.workspaces;
224
+ }
225
+ function isOtherWorkspace(root) {
226
+ const self = canonicalPath(root);
227
+ return (path) => canonicalPath(path) !== self;
228
+ }
229
+ function deviceLabel(device) {
230
+ return `${device.kind} ${device.name} in ${device.project}`;
231
+ }
232
+ function reclaimIdleDevices({ root, dryRun, note, now }) {
233
+ if (deviceSweepIsScoped()) {
234
+ note(chalk.dim(phaseLine("budget", "idle devices left alone: STIM_HOME scopes this config, devices are global")));
235
+ return {
236
+ targets: [],
237
+ failures: 0
238
+ };
239
+ }
240
+ const other = isOtherWorkspace(root);
241
+ const collect = (at) => idleShutdownCandidates(collectIdleDevices(loadConfig$1(), listSims() ?? [], [], at).filter((device) => other(device.project)), 0);
242
+ const idle = collect(now);
243
+ if (dryRun || idle.length === 0) return {
244
+ targets: idle.map(deviceLabel),
245
+ failures: 0
246
+ };
247
+ const { failures, shutDown } = shutDownIdleDevices(idle, 0, () => collect(Date.now()));
248
+ return {
249
+ targets: shutDown.map(deviceLabel),
250
+ failures
251
+ };
252
+ }
253
+ function idleSince(path, now) {
254
+ const activity = workspaceActivity(path, now);
255
+ const at = Date.parse(activity.lastActivityAt ?? "");
256
+ return activity.state === "idle" && Number.isFinite(at) ? at : null;
257
+ }
258
+ async function reclaimIdleDevServers({ root, dryRun, note, now }) {
259
+ const config = loadConfig$1();
260
+ const other = isOtherWorkspace(root);
261
+ const serving = Object.entries(config?.projects ?? {}).flatMap(([path, project]) => {
262
+ const pid = other(path) ? runningSupervisorPid(path, project) : null;
263
+ return pid !== null && !workspaceBuildInProgress(path) && idleSince(path, now) !== null ? [{
264
+ path,
265
+ pid
266
+ }] : [];
267
+ });
268
+ if (serving.length === 0) return {
269
+ targets: [],
270
+ failures: 0
271
+ };
272
+ const sims = listSims();
273
+ if (sims === null) {
274
+ note(chalk.dim(phaseLine("budget", "dev servers left alone: simulators could not be listed to prove them idle")));
275
+ return {
276
+ targets: [],
277
+ failures: 0
278
+ };
279
+ }
280
+ const busy = new Set(collectOwnedDeviceActivity(config, sims, now).filter((device) => device.activity.state !== "idle").map((device) => device.project));
281
+ const idle = serving.filter(({ path }) => !busy.has(path));
282
+ if (dryRun) return {
283
+ targets: idle.map(({ path }) => path),
284
+ failures: 0
285
+ };
286
+ const targets = [];
287
+ let failures = 0;
288
+ const stillIdle = (path, pid) => {
289
+ const project = loadConfig$1()?.projects?.[path];
290
+ return Boolean(project && runningSupervisorPid(path, project) === pid && !workspaceBuildInProgress(path) && idleSince(path, Date.now()) !== null);
291
+ };
292
+ for (const { path, pid } of idle) {
293
+ let run;
294
+ try {
295
+ run = await withIdleWorkspace(path, async () => {
296
+ try {
297
+ return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path) : "busy", {
298
+ external: true,
299
+ waitMs: 0,
300
+ ownerPurpose: "budget reclaim"
301
+ });
302
+ } catch (error) {
303
+ if (workspaceProcessLockError(error)) return "starting";
304
+ throw error;
305
+ }
306
+ }, {
307
+ purpose: "budget reclaim",
308
+ supervisor: false
309
+ });
310
+ } catch (error) {
311
+ failures++;
312
+ note(chalk.red(`Could not stop the dev server of ${path}: ${error?.message || error}`));
313
+ continue;
314
+ }
315
+ if (!run.ran) {
316
+ note(chalk.yellow(`Kept the dev server of ${path}: ${run.reasons.join("; ")}`));
317
+ continue;
318
+ }
319
+ const stopped = run.value;
320
+ if (stopped === "busy" || stopped === "starting") {
321
+ const why = stopped === "busy" ? "it is no longer idle" : "a stim start holds its metro-start lock";
322
+ note(chalk.dim(`Kept the dev server of ${path}: ${why}`));
323
+ } else if (stopped.status === "stopped") {
324
+ targets.push(path);
325
+ note(chalk.green(`Stopped the idle dev server of ${path}`));
326
+ } else if ("reason" in stopped) {
327
+ if (stopped.status === "stuck") failures++;
328
+ note(chalk.yellow(`Kept the dev server of ${path}: ${stopped.reason}`));
329
+ }
330
+ }
331
+ return {
332
+ targets,
333
+ failures
334
+ };
335
+ }
336
+ async function reclaimWorkspaceOutputs({ root, dryRun, now, diskRecovered }) {
337
+ const other = isOtherWorkspace(root);
338
+ const report = collectWorkspaceOutputs({
339
+ olderThan: null,
340
+ now,
341
+ measure: false
342
+ });
343
+ const clearable = report.workspaces.flatMap((entry) => {
344
+ const project = entry.projectRoot;
345
+ const since = project !== null && entry.willClear && other(project) ? idleSince(project, now) : null;
346
+ return since === null ? [] : [{
347
+ entry,
348
+ since
349
+ }];
350
+ }).toSorted((a, b) => a.since - b.since).map(({ entry }) => entry);
351
+ if (dryRun) return {
352
+ targets: clearable.map((entry) => entry.projectRoot),
353
+ failures: 0
354
+ };
355
+ const targets = [];
356
+ let failures = 0;
357
+ for (const entry of clearable) {
358
+ const result = await clearWorkspaceOutputs({
359
+ ...report,
360
+ workspaces: [entry]
361
+ }, {
362
+ olderThan: null,
363
+ now
364
+ });
365
+ failures += result.failures;
366
+ targets.push(...result.cleared);
367
+ if (diskRecovered()) break;
368
+ }
369
+ return {
370
+ targets,
371
+ failures
372
+ };
373
+ }
374
+ function cacheLabel(cache) {
375
+ return `${cache.name} ${cache.dir}`;
376
+ }
377
+ function reclaimStaleCacheEntries({ dryRun }) {
378
+ const caches = planCacheEmptying(discoverCaches(), false).filter((cache) => cache.prune !== "report-only" && cache.prune !== "atomic" && !cache.machineGlobal);
379
+ if (dryRun) return {
380
+ targets: caches.map(cacheLabel),
381
+ failures: 0
382
+ };
383
+ return {
384
+ targets: trimCaches(caches, STALE_CACHE_DAYS).map(cacheLabel),
385
+ failures: 0
386
+ };
387
+ }
388
+ const STEPS = {
389
+ "idle-devices": reclaimIdleDevices,
390
+ "idle-dev-servers": reclaimIdleDevServers,
391
+ "workspace-outputs": reclaimWorkspaceOutputs,
392
+ "stale-cache-entries": reclaimStaleCacheEntries
393
+ };
394
+ const STEP_ACTIONS = {
395
+ "idle-devices": "shut down idle owned devices",
396
+ "idle-dev-servers": "stop idle dev servers",
397
+ "workspace-outputs": "clear build outputs of idle workspaces",
398
+ "stale-cache-entries": `trim cache entries unused for ${STALE_CACHE_DAYS} days`
399
+ };
400
+ async function runStep(step, context) {
401
+ const log = console.log;
402
+ const exitCode = process.exitCode;
403
+ console.log = console.error;
404
+ try {
405
+ const result = await STEPS[step](context);
406
+ return process.exitCode === exitCode ? result : {
407
+ ...result,
408
+ failures: result.failures + 1
409
+ };
410
+ } catch (error) {
411
+ context.note(chalk.red(phaseLine("budget", `${STEP_ACTIONS[step]} failed: ${error?.message || error}`)));
412
+ return {
413
+ targets: [],
414
+ failures: 1
415
+ };
416
+ } finally {
417
+ console.log = log;
418
+ process.exitCode = exitCode;
419
+ }
420
+ }
421
+ function spaceUsers() {
422
+ const home = homedir();
423
+ const candidates = [
424
+ ["Stim state", getConfigDir()],
425
+ ["shared build cache", sharedBuildCache()],
426
+ ...platform() === "darwin" ? [["simulators", join(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
427
+ ["Android emulators", join(home, ".android", "avd")]
428
+ ];
429
+ const measured = /* @__PURE__ */ new Set();
430
+ const users = [];
431
+ for (const [label, path] of candidates) {
432
+ const canonical = canonicalPath(path);
433
+ if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep}`))) continue;
434
+ measured.add(canonical);
435
+ if (!existsSync(path)) continue;
436
+ const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
437
+ if (bytes) users.push({
438
+ label: `${label} ${path}`,
439
+ bytes
440
+ });
441
+ }
442
+ return users.toSorted((a, b) => b.bytes - a.bytes).map(({ label, bytes }) => `${label}: ${formatBytes(bytes)}`);
443
+ }
444
+ const DEFAULT_DEPS = {
445
+ volumes: readVolumeSpace,
446
+ memory: readMemoryUse,
447
+ step: runStep,
448
+ usage: spaceUsers,
449
+ now: Date.now
450
+ };
451
+ 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.";
452
+ function describeShortfall(budget, measure, short) {
453
+ const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
454
+ return [
455
+ ...short.disk.map((v) => `${formatSpace(v.freeMb)} free on ${v.volume}, below the ${formatSpace(floor)} floor`),
456
+ ...short.memory && measure.memory ? [`roughly ${formatSpace(measure.memory.committedMb)} of memory committed, over the ${formatSpace(budget.maxCommittedMemoryMb)} budget`] : [],
457
+ ...short.workspaces && measure.memory ? [`${measure.memory.liveWorkspaces} live workspaces, over the limit of ${budget.maxLiveWorkspaces}`] : []
458
+ ];
459
+ }
460
+ async function enforceBudget({ root, note, dryRun = false, budget: resolved }, overrides = {}) {
461
+ const deps = {
462
+ ...DEFAULT_DEPS,
463
+ ...overrides
464
+ };
465
+ let budget = resolved;
466
+ if (!budget) {
467
+ const read = resolveBudget();
468
+ if (read.error) return {
469
+ status: "invalid",
470
+ refusal: {
471
+ code: "STIM_BAD_ARG",
472
+ message: read.error,
473
+ remedy: BUDGET_SETTING_REMEDY
474
+ }
475
+ };
476
+ budget = read.budget;
477
+ }
478
+ const paths = [root, getConfigDir()];
479
+ const measureNow = () => ({
480
+ volumes: budget.minFreeDiskMb > 0 || budget.hardFloorDiskMb > 0 ? deps.volumes(paths) : [],
481
+ memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? deps.memory() : null
482
+ });
483
+ let measure = measureNow();
484
+ let short = budgetShortfalls(budget, measure);
485
+ const reclaimed = [];
486
+ const plan = reclaimPlan(short);
487
+ if (plan.length) note(chalk.yellow(phaseLine("budget", `${describeShortfall(budget, measure, short).join("; ")}; ${dryRun ? "would reclaim" : "reclaiming"}`)));
488
+ for (const step of plan) {
489
+ if (!dryRun && (!overBudget(short) || DISK_ONLY.includes(step) && short.disk.length === 0)) break;
490
+ const freeBefore = measure.volumes.reduce((sum, v) => sum + v.freeMb, 0);
491
+ const result = await deps.step(step, {
492
+ root,
493
+ dryRun,
494
+ note,
495
+ now: deps.now(),
496
+ diskRecovered: () => budgetShortfalls(budget, {
497
+ volumes: deps.volumes(paths),
498
+ memory: null
499
+ }).disk.length === 0
500
+ });
501
+ if (!dryRun) {
502
+ measure = measureNow();
503
+ short = budgetShortfalls(budget, measure);
504
+ }
505
+ if (result.targets.length === 0 && result.failures === 0) continue;
506
+ const freedMb = Math.max(0, Math.round(measure.volumes.reduce((sum, v) => sum + v.freeMb, 0) - freeBefore));
507
+ reclaimed.push({
508
+ step,
509
+ ...result,
510
+ ...dryRun ? {} : { freedMb }
511
+ });
512
+ const outcome = dryRun ? result.targets.join(", ") : `${result.targets.length}${result.failures ? `, ${result.failures} failed` : ""} (${formatSpace(freedMb)} freed)`;
513
+ note(chalk.dim(phaseLine("budget", `${STEP_ACTIONS[step]}: ${outcome}`)));
514
+ }
515
+ if (!dryRun && short.hardFloor.length) {
516
+ const tight = short.hardFloor.map((v) => `${formatSpace(v.freeMb)} on ${v.volume}`).join(" and ");
517
+ const users = deps.usage();
518
+ return {
519
+ status: "refused",
520
+ reclaimed,
521
+ refusal: {
522
+ code: "STIM_LOW_DISK",
523
+ 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("; ")}.` : ""),
524
+ remedy: LOW_DISK_REMEDY
525
+ }
526
+ };
527
+ }
528
+ if (!dryRun && overBudget(short)) note(chalk.yellow(phaseLine("budget", `still over budget: ${describeShortfall(budget, measure, short).join("; ")}`)));
529
+ return {
530
+ status: "ok",
531
+ reclaimed,
532
+ budget,
533
+ measure,
534
+ short
535
+ };
536
+ }
537
+ async function budgetGate(args, overrides = {}) {
538
+ const outcome = await enforceBudget(args, overrides);
539
+ return {
540
+ reclaimed: "reclaimed" in outcome ? outcome.reclaimed : [],
541
+ refusal: outcome.status === "ok" ? null : outcome.refusal
542
+ };
543
+ }
544
+ const gb = (mb) => Math.round(mb / GB_IN_MB * 10) / 10;
545
+ function budgetFindings(budget, measure, short, plan) {
546
+ const findings = [];
547
+ 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";
548
+ if (short.disk.length) {
549
+ const refuses = short.hardFloor.length ? ` Below ${formatSpace(budget.hardFloorDiskMb)} it refuses with STIM_LOW_DISK when reclaiming does not free enough.` : "";
550
+ findings.push({
551
+ level: "cost",
552
+ title: "Free disk is below the Stim budget",
553
+ detail: `${describeShortfall(budget, measure, {
554
+ ...short,
555
+ memory: false,
556
+ workspaces: false
557
+ }).join("; ")}. The next ios, android or start will first ${steps}.${refuses}`,
558
+ fix: "Run `stim gc` to see what Stim can reclaim, then `stim gc --delete`. Free space outside Stim, or lower budget.minFreeDiskGb."
559
+ });
560
+ }
561
+ if (short.memory || short.workspaces) findings.push({
562
+ level: "note",
563
+ title: "Live environments are over the memory budget",
564
+ detail: `${describeShortfall(budget, measure, {
565
+ ...short,
566
+ disk: []
567
+ }).join("; ")}. The next ios, android or start shuts down idle owned devices and stops idle dev servers in other workspaces first.`,
568
+ fix: "Run `stim status` to see what is live, and `stim stop` in workspaces you own that you no longer need."
569
+ });
570
+ return findings;
571
+ }
572
+ async function inspectBudget(root, deps = {}) {
573
+ const read = resolveBudget();
574
+ if (read.error) return {
575
+ report: null,
576
+ findings: [{
577
+ level: "cost",
578
+ title: "A budget setting is invalid",
579
+ detail: read.error,
580
+ fix: BUDGET_SETTING_REMEDY
581
+ }]
582
+ };
583
+ const outcome = await enforceBudget({
584
+ root,
585
+ note: () => {},
586
+ dryRun: true,
587
+ budget: read.budget
588
+ }, deps);
589
+ if (outcome.status !== "ok") return {
590
+ report: null,
591
+ findings: []
592
+ };
593
+ const { budget, measure, short, reclaimed } = outcome;
594
+ return {
595
+ report: {
596
+ budget: {
597
+ minFreeDiskGb: gb(budget.minFreeDiskMb),
598
+ hardFloorDiskGb: gb(budget.hardFloorDiskMb),
599
+ maxCommittedMemoryGb: gb(budget.maxCommittedMemoryMb),
600
+ maxLiveWorkspaces: budget.maxLiveWorkspaces
601
+ },
602
+ volumes: measure.volumes.map((v) => ({
603
+ volume: v.volume,
604
+ freeGb: gb(v.freeMb)
605
+ })),
606
+ memory: measure.memory ? {
607
+ committedGb: gb(measure.memory.committedMb),
608
+ liveWorkspaces: measure.memory.liveWorkspaces
609
+ } : null,
610
+ plan: reclaimed
611
+ },
612
+ findings: budgetFindings(budget, measure, short, reclaimed)
613
+ };
614
+ }
615
+ function budgetLine(report) {
616
+ const { budget } = report;
617
+ 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"}`;
618
+ }
619
+ //#endregion
620
+ export { stopOwnedMetroForReset as i, budgetLine as n, inspectBudget as r, budgetGate as t };