stim 1.8.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 (91) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-DWssqHQ8.mjs → android-787HhP5w.mjs} +168 -39
  3. package/dist/{android-BRgBYzU0.mjs → android-DBkeHwpm.mjs} +250 -200
  4. package/dist/{android-DeYdjjBP.mjs → android-DPov-fv2.mjs} +2 -2
  5. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-CITmIeWY.mjs} +22 -14
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-CrJ6t_bv.mjs +819 -0
  8. package/dist/budget-BP4jZZL4.mjs +620 -0
  9. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Pubfp8wV.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +7 -1
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-D5z2_bcF.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-BEq0Qw_a.mjs} +134 -121
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-DP4eWqea.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-DJHoca49.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-92U593KT.mjs} +1090 -660
  21. package/dist/{device-RdujjHLB.mjs → device-DhTZe7L4.mjs} +13 -20
  22. package/dist/{build-lock-CgtsB6TV.mjs → device-lease-CbEaIk7b.mjs} +481 -27
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-Di8Q-9Ro.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-Dy1jGj3U.mjs} +645 -119
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B-inAJmt.mjs} +137 -113
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-xxWKNO3-.mjs} +22 -17
  27. package/dist/exec-DK8KaVHK.mjs +133 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-ChPFZMRC.mjs} +836 -813
  29. package/dist/{guide-BTAlD1_y.mjs → guide-DwmaD_Wa.mjs} +953 -209
  30. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  31. package/dist/idle-DpRLp5lj.mjs +157 -0
  32. package/dist/{build-slots-C-oAHRT1.mjs → in-use-CjUluqIX.mjs} +102 -8
  33. package/dist/{ios-BcCiI4TS.mjs → ios-B2Fpqvlb.mjs} +60 -71
  34. package/dist/{ios-BP04pY5f.mjs → ios-Dd-hYJBh.mjs} +241 -234
  35. package/dist/{ios-device-CFXA46ez.mjs → ios-device-BiOsSVzd.mjs} +3 -2
  36. package/dist/{ios-device-CauFJiYx.mjs → ios-device-CtRdEVjx.mjs} +149 -52
  37. package/dist/launch-verify-CQS_Kg-C.mjs +365 -0
  38. package/dist/{logs-CakdBTtm.mjs → logs-MARIy6m9.mjs} +20 -15
  39. package/dist/metro-BWLOJ_Wr.mjs +409 -0
  40. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  41. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-CDY9IkVC.mjs} +23 -43
  42. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  43. package/dist/{ownership-Cp52QAK1.mjs → ownership-D2xbLVoz.mjs} +2 -3
  44. package/dist/{teardown-BJqEzPo_.mjs → ownership-DZjdx8Gq.mjs} +664 -612
  45. package/dist/{ports-BIbJaKNW.mjs → ports-CLSSFOuz.mjs} +3 -3
  46. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  47. package/dist/{project-DDZWf_16.mjs → project-BXZzOABt.mjs} +8 -3
  48. package/dist/{reload-BdypPbjM.mjs → reload-DoETJ3rl.mjs} +11 -13
  49. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DVbb7kXH.mjs} +2 -2
  50. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-BvOCeakp.mjs} +8 -7
  51. package/dist/server-expo-DBVTdult.mjs +2 -0
  52. package/dist/{settings-BXXorVM2.mjs → settings-DOugPlPz.mjs} +59 -159
  53. package/dist/settings-EWYNwe72.mjs +378 -0
  54. package/dist/settings.schema.json +1064 -0
  55. package/dist/simslim-Ca3487H6.mjs +436 -0
  56. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  57. package/dist/spawn-claims-CUleC2cM.mjs +49 -0
  58. package/dist/{start-CmxtOKjU.mjs → start-F9gv5dcC.mjs} +159 -114
  59. package/dist/start-VoFsPSes.mjs +2 -0
  60. package/dist/{state-DZobzxLW.mjs → state-WGdIbi_d.mjs} +5 -3
  61. package/dist/{stats-BYcZ6n1h.mjs → stats-BAgJaLA4.mjs} +66 -5
  62. package/dist/{stats-CLg5RuDS.mjs → stats-DBYJ5bxL.mjs} +4 -4
  63. package/dist/status-DLCD-o3s.mjs +764 -0
  64. package/dist/status-Dr4fRkym.mjs +216 -0
  65. package/dist/{guide-status-Craq9z0t.mjs → stim-installations-CINXy_su.mjs} +3 -122
  66. package/dist/{stop-C8JW6kKp.mjs → stop-Dj4pfmyv.mjs} +111 -31
  67. package/dist/{stop-B1kOpYei.mjs → stop-ZvOeTmKb.mjs} +2 -2
  68. package/dist/supervisor-run.d.mts +11 -2
  69. package/dist/supervisor-run.mjs +228 -42
  70. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-BXA96myD.mjs} +14 -9
  71. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  72. package/dist/worktree-4aZg2NnI.mjs +2 -0
  73. package/dist/{worktree-CFLA-6MX.mjs → worktree-BMS7GeqB.mjs} +523 -61
  74. package/dist/{worktree-FG_ZKKdb.mjs → worktree-BlizKDpZ.mjs} +200 -54
  75. package/dist/{xcode-B6YaWeg0.mjs → xcode-B2tBkf0m.mjs} +66 -57
  76. package/package.json +6 -6
  77. package/skill/SKILL.md +2 -0
  78. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  79. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  80. package/dist/device-lease-Ffz_M5t0.mjs +0 -359
  81. package/dist/exec-B29XNuhP.mjs +0 -77
  82. package/dist/logs-query-BGL-Pl12.mjs +0 -290
  83. package/dist/metro-C7bcJ0uo.mjs +0 -233
  84. package/dist/ownership-wolE-0Do.mjs +0 -470
  85. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  86. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  87. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  88. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  89. package/dist/status-DdkLki47.mjs +0 -426
  90. package/dist/workspace-state-BT5E3SA2.mjs +0 -146
  91. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -0,0 +1,216 @@
1
+ import { H as projectDeviceSlots } from "./config-BEq0Qw_a.mjs";
2
+ import { a as formatElapsed, o as formatLongDuration, r as clockTime, u as plural } from "./command-output-D5z2_bcF.mjs";
3
+ import { sep } from "path";
4
+ //#region src/status.ts
5
+ const IOS_SIM_MB = 1500;
6
+ const ANDROID_EMULATOR_MB = 2500;
7
+ const METRO_MB = 700;
8
+ function remoteDeviceState(record, ledger, root) {
9
+ if (!record) return null;
10
+ const claim = ledger.claims.get(record.sessionId);
11
+ return {
12
+ platform: record.platform,
13
+ backend: "eas",
14
+ sessionId: record.sessionId,
15
+ state: !ledger.safe ? "unknown" : claim?.workspaceRoot === root ? "claimed" : "unclaimed",
16
+ startedAt: record.startedAt,
17
+ webPreviewUrl: record.webPreviewUrl
18
+ };
19
+ }
20
+ function poolLine({ platform, parked, max }) {
21
+ if (parked <= 0) return null;
22
+ const what = plural(parked, `parked ${platform === "ios" ? "iOS simulator" : "Android emulator"}`);
23
+ return max > 0 ? `pool: ${what} (max ${max})` : `pool: ${what} (parking off; gc --delete removes them)`;
24
+ }
25
+ function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, androidRuntimes = {}, supervisor = null, logs = null, remote = null, idleStop = null } = {}) {
26
+ const ios = project.platforms?.ios;
27
+ const android = project.platforms?.android;
28
+ const sim = ios ? simsByUdid[ios.deviceUdid] : null;
29
+ const simBooted = Boolean(sim && sim.state === "Booted");
30
+ const metroRunning = Boolean(metro?.metro);
31
+ const androidDetected = androidRuntime ? Boolean(androidRuntime.serial) : Boolean(android?.serial);
32
+ let live = simBooted || metroRunning || androidDetected || Boolean(remote);
33
+ let memoryMb = 0;
34
+ if (simBooted) memoryMb += IOS_SIM_MB;
35
+ if (androidDetected) memoryMb += ANDROID_EMULATOR_MB;
36
+ if (metroRunning) memoryMb += METRO_MB;
37
+ const warnings = [];
38
+ if (metro?.notOurs) warnings.push(`port ${project.metroPort}: ${metro.notOurs}`);
39
+ if (ios && !sim && simsAvailable) warnings.push(`recorded sim ${ios.deviceUdid} no longer exists`);
40
+ if (simBooted && project.metroPort && !metroRunning) warnings.push("simulator is booted with no Metro serving it");
41
+ if (androidRuntime && android?.avdName) {
42
+ const recordedSerial = android.consolePort ? `emulator-${android.consolePort}` : android.serial;
43
+ if (androidRuntime.serial && recordedSerial && androidRuntime.serial !== recordedSerial) warnings.push(`owned AVD ${android.avdName} changed serial (${recordedSerial} -> ${androidRuntime.serial}); rerun your \`stim android\` command in this workspace to restore Metro forwarding, then reopen agent-device on ${androidRuntime.serial}`);
44
+ if (androidRuntime.state === "missing") warnings.push(`recorded AVD ${android.avdName} no longer exists`);
45
+ if (androidRuntime.state === "not-detected") warnings.push(`owned AVD ${android.avdName} is not detected by adb; rerun your \`stim android\` command in this workspace to reconnect`);
46
+ if (androidRuntime.error) warnings.push(`could not check owned AVD ${android.avdName}: ${androidRuntime.error}`);
47
+ }
48
+ if (supervisor && supervisor.alive === false) warnings.push(`stale supervisor record for ${project.__path}`);
49
+ const slots = [];
50
+ for (const { slot, platforms } of projectDeviceSlots(project).slice(1)) {
51
+ const deviceState = environmentState({
52
+ ...project,
53
+ platforms,
54
+ deviceSlots: void 0
55
+ }, {
56
+ simsByUdid,
57
+ simsAvailable,
58
+ metro,
59
+ androidRuntime: androidRuntimes[slot]
60
+ });
61
+ slots.push({
62
+ slot,
63
+ ios: deviceState.ios,
64
+ android: deviceState.android
65
+ });
66
+ memoryMb += deviceState.memoryMb - (metroRunning ? METRO_MB : 0);
67
+ live ||= deviceState.live;
68
+ warnings.push(...deviceState.warnings.map((warning) => `${slot}: ${warning}`));
69
+ }
70
+ return {
71
+ path: project.__path,
72
+ ...slots.length ? { slots } : {},
73
+ live,
74
+ memoryMb,
75
+ warnings,
76
+ ios: ios ? {
77
+ name: sim?.name ?? null,
78
+ udid: ios.deviceUdid,
79
+ owned: Boolean(ios.owned),
80
+ state: sim?.state ?? (simsAvailable ? "missing" : "unknown")
81
+ } : null,
82
+ android: android ? {
83
+ name: android.avdName ?? android.serial,
84
+ owned: Boolean(android.owned),
85
+ physical: Boolean(android.serial && !android.avdName),
86
+ ...androidRuntime ? {
87
+ serial: androidRuntime.serial,
88
+ state: androidRuntime.state
89
+ } : {}
90
+ } : null,
91
+ metro: project.metroPort ? {
92
+ port: project.metroPort,
93
+ running: metroRunning,
94
+ pid: metro?.metro?.pid ?? null,
95
+ ...idleStop && !metroRunning ? { idleStop } : {}
96
+ } : null,
97
+ supervisor: supervisor ? {
98
+ pid: supervisor.pid ?? null,
99
+ mode: supervisor.mode ?? null,
100
+ startedAt: supervisor.startedAt ?? null,
101
+ healthy: Boolean(supervisor.healthy)
102
+ } : null,
103
+ logs: logs ? {
104
+ dir: logs.dir,
105
+ errorsSinceMarker: logs.errorsSinceMarker ?? 0
106
+ } : null,
107
+ worktree: worktrees.find((w) => w.path === project.__path) ?? null,
108
+ remoteDevices: remote ? [remote] : []
109
+ };
110
+ }
111
+ function activityLabel(activity, now) {
112
+ if (!activity) return null;
113
+ if (activity.state === "driven") {
114
+ const since = Date.parse(activity.driver?.since ?? "");
115
+ const duration = Number.isFinite(since) ? ` for ${formatLongDuration(Math.max(0, now - since))}` : "";
116
+ return `driven by ${activity.driver?.tool ?? "an unknown tool"}${duration}`;
117
+ }
118
+ if (activity.state === "unknown") return `activity unknown (${activity.basis.join(", ")})`;
119
+ if (activity.state === "active") return "active";
120
+ const last = Date.parse(activity.lastActivityAt ?? "");
121
+ return Number.isFinite(last) ? `idle ${formatLongDuration(Math.max(0, now - last))}` : "idle (no recorded activity)";
122
+ }
123
+ function remoteDeviceLine(remote) {
124
+ const claim = remote.state === "claimed" ? "" : ` (${remote.state})`;
125
+ const watch = remote.webPreviewUrl ? ` -- watch: ${remote.webPreviewUrl}` : "";
126
+ return `remote ${remote.platform ?? "?"}: EAS session ${remote.sessionId} billable${claim}${watch}`;
127
+ }
128
+ function capacity(states, totalMemoryMb) {
129
+ const committedMb = states.reduce((n, s) => n + s.memoryMb, 0);
130
+ return {
131
+ liveCount: states.filter((s) => s.live).length,
132
+ committedMb,
133
+ totalMemoryMb,
134
+ overCapacity: Boolean(totalMemoryMb && committedMb > totalMemoryMb * .6)
135
+ };
136
+ }
137
+ function parseDfFree(output) {
138
+ const lines = String(output || "").trim().split("\n");
139
+ if (lines.length < 2) return null;
140
+ const lastLine = lines[lines.length - 1];
141
+ if (lastLine === void 0) return null;
142
+ const m = /\s(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%/.exec(lastLine);
143
+ if (!m) return null;
144
+ const totalKb = Number(m[1]);
145
+ const availableKb = Number(m[3]);
146
+ if (!Number.isFinite(totalKb) || !Number.isFinite(availableKb) || totalKb <= 0) return null;
147
+ return {
148
+ availableMb: Math.round(availableKb / 1024),
149
+ totalMb: Math.round(totalKb / 1024)
150
+ };
151
+ }
152
+ function diskIsTight(disk) {
153
+ return Boolean(disk && disk.availableMb < 25600);
154
+ }
155
+ function formatSpace(mb) {
156
+ if (!Number.isFinite(mb)) return "?";
157
+ if (mb >= 1048576) return `${(mb / 1048576).toFixed(1)} TB`;
158
+ if (mb >= 1024) return `${Math.round(mb / 1024)} GB`;
159
+ return `${Math.round(mb)} MB`;
160
+ }
161
+ function diskLine(volumes) {
162
+ const usable = (volumes || []).filter((v) => Boolean(v && v.disk));
163
+ if (usable.length === 0) return null;
164
+ if (usable.length === 1) {
165
+ const first = usable[0];
166
+ if (!first) return null;
167
+ const { disk } = first;
168
+ return `${formatSpace(disk.availableMb)} free of ${formatSpace(disk.totalMb)} on disk.`;
169
+ }
170
+ return `${usable.map((v) => `${formatSpace(v.disk.availableMb)} free on ${v.volume}`).join(", ")}.`;
171
+ }
172
+ function tightVolumes(volumes) {
173
+ return (volumes || []).filter((v) => v && diskIsTight(v.disk));
174
+ }
175
+ function unprovisionedWorktrees(worktrees, projectPaths) {
176
+ return worktrees.filter((w) => {
177
+ const inside = w.path.endsWith(sep) ? w.path : w.path + sep;
178
+ return !projectPaths.some((p) => p === w.path || p.startsWith(inside));
179
+ });
180
+ }
181
+ function deviceLeaseStates(entries, { root, now }) {
182
+ return entries.map((entry) => {
183
+ const lease = entry.lease;
184
+ return {
185
+ path: entry.path,
186
+ platform: entry.platform,
187
+ ...lease?.slot ? { slot: lease.slot } : {},
188
+ id: entry.id,
189
+ deviceName: lease?.deviceName ?? null,
190
+ holder: lease?.holder ?? null,
191
+ grantedAt: lease?.grantedAt ?? null,
192
+ expiresAt: lease?.expiresAt ?? null,
193
+ mine: Boolean(lease && root && lease.holder === root),
194
+ expired: Boolean(lease && Date.parse(lease.expiresAt) <= now),
195
+ parsed: Boolean(lease)
196
+ };
197
+ });
198
+ }
199
+ function deviceLeaseLines(states, now) {
200
+ if (states.length === 0) return [];
201
+ const lines = [`Device leases (${states.length}):`];
202
+ for (const state of states) {
203
+ const device = `${state.platform}${state.slot ? ` [${state.slot}]` : ""} ${state.id ?? state.path}${state.deviceName ? ` (${state.deviceName})` : ""}`;
204
+ if (!state.parsed || state.expiresAt === null) {
205
+ lines.push(` ${device} -- unreadable lease file, so nothing may take the device: ${state.path}`);
206
+ continue;
207
+ }
208
+ const when = clockTime(state.expiresAt);
209
+ const remaining = Date.parse(state.expiresAt) - now;
210
+ const expiry = state.expired ? `expired at ${when}` : `until ${when} (${formatElapsed(remaining)} left)`;
211
+ lines.push(` ${device} -- ${state.holder} ${expiry}${state.mine ? " [this workspace]" : ""}`);
212
+ }
213
+ return lines;
214
+ }
215
+ //#endregion
216
+ export { diskLine as a, parseDfFree as c, remoteDeviceState as d, tightVolumes as f, deviceLeaseStates as i, poolLine as l, capacity as n, environmentState as o, unprovisionedWorktrees as p, deviceLeaseLines as r, formatSpace as s, activityLabel as t, remoteDeviceLine as u };
@@ -1,6 +1,5 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { S as upsertProject, f as getProject, u as getConfigDir } from "./config-B7iyKWkL.mjs";
3
- import { accessSync, constants, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
2
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
4
3
  import { delimiter, join, resolve } from "node:path";
5
4
  //#region src/diagnostics/stim-installations.ts
6
5
  const VERSION_OUTPUT_LIMIT = 4096;
@@ -150,122 +149,4 @@ async function inspectStimVersions(runningVersion, { pathValue = process.env.PAT
150
149
  return analyzeStimVersions(runningVersion, canonicalPath(runningPath), installations);
151
150
  }
152
151
  //#endregion
153
- //#region src/guide-status.ts
154
- const DAY_MS = 864e5;
155
- const DOCTOR_STALE_MS = 7 * DAY_MS;
156
- const UPDATE_CHECK_INTERVAL_MS = DAY_MS;
157
- const UPDATE_CHECK_TIMEOUT_MS = 2e3;
158
- const LATEST_URL = "https://registry.npmjs.org/stim/latest";
159
- const PLATFORMS = ["ios", "android"];
160
- function recordDoctorRun(projectRoot, platform, version, now = /* @__PURE__ */ new Date()) {
161
- const run = {
162
- at: now.toISOString(),
163
- version
164
- };
165
- try {
166
- upsertProject(projectRoot, (existing) => {
167
- const doctorRuns = { ...existing.doctorRuns };
168
- for (const target of platform ? [platform] : PLATFORMS) doctorRuns[target] = run;
169
- return { doctorRuns };
170
- });
171
- } catch {}
172
- }
173
- function doctorDueReason(record, running, now) {
174
- const at = record ? Date.parse(record.at) : NaN;
175
- if (!record || Number.isNaN(at)) return "never run";
176
- if (record.version !== running) return `last run with stim ${record.version}`;
177
- const age = now.getTime() - at;
178
- if (age > DOCTOR_STALE_MS) return `last run ${Math.floor(age / DAY_MS)} days ago`;
179
- return null;
180
- }
181
- function updateCacheFile() {
182
- return join(getConfigDir(), "update-check.json");
183
- }
184
- function readUpdateCache() {
185
- try {
186
- const parsed = JSON.parse(readFileSync(updateCacheFile(), "utf-8"));
187
- if (typeof parsed?.checkedAt !== "string") return null;
188
- return {
189
- checkedAt: parsed.checkedAt,
190
- latest: typeof parsed.latest === "string" ? parsed.latest : null
191
- };
192
- } catch {
193
- return null;
194
- }
195
- }
196
- function writeUpdateCache(cache) {
197
- const target = updateCacheFile();
198
- const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
199
- try {
200
- mkdirSync(getConfigDir(), { recursive: true });
201
- writeFileSync(tmp, `${JSON.stringify(cache)}\n`);
202
- renameSync(tmp, target);
203
- } catch {
204
- try {
205
- unlinkSync(tmp);
206
- } catch {}
207
- }
208
- }
209
- async function fetchLatest(fetchImpl) {
210
- const res = await fetchImpl(LATEST_URL, { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) });
211
- if (!res.ok) return null;
212
- const body = await res.json();
213
- return typeof body.version === "string" ? body.version : null;
214
- }
215
- async function checkForUpdate(running, { now = /* @__PURE__ */ new Date(), fetch: fetchImpl = fetch } = {}) {
216
- if (process.env.STIM_NO_UPDATE_CHECK) return null;
217
- const cached = readUpdateCache();
218
- const checkedAt = cached ? Date.parse(cached.checkedAt) : NaN;
219
- let latest = cached?.latest ?? null;
220
- if (Number.isNaN(checkedAt) || now.getTime() - checkedAt > UPDATE_CHECK_INTERVAL_MS) {
221
- try {
222
- latest = await fetchLatest(fetchImpl);
223
- } catch {
224
- latest = null;
225
- }
226
- writeUpdateCache({
227
- checkedAt: now.toISOString(),
228
- latest
229
- });
230
- }
231
- if (!latest) return null;
232
- const comparison = compareStimVersions(latest, running);
233
- return comparison !== null && comparison > 0 ? latest : null;
234
- }
235
- function renderGuideStatus({ running, doctor, latest }) {
236
- const lines = [];
237
- if (doctor.length) {
238
- const named = doctor.map((due) => `${due.platform} (${due.reason})`).join(" and ");
239
- const command = doctor.length === 1 ? `stim doctor --platform ${doctor[0]?.platform}` : "stim doctor";
240
- lines.push(` Doctor is due for ${named}.`, ` Run before native work: ${command}`);
241
- }
242
- if (latest) lines.push(` stim ${latest} is available (running ${running}): npm install -g stim@latest`);
243
- if (!lines.length) return null;
244
- return ["STATUS", ...lines].join("\n");
245
- }
246
- async function guideStatus({ projectRoot, running, now = /* @__PURE__ */ new Date(), fetch: fetchImpl = fetch }) {
247
- const doctor = [];
248
- if (projectRoot) {
249
- let runs = null;
250
- try {
251
- runs = getProject(projectRoot)?.doctorRuns ?? {};
252
- } catch {}
253
- if (runs) for (const platform of PLATFORMS) {
254
- const reason = doctorDueReason(runs[platform], running, now);
255
- if (reason) doctor.push({
256
- platform,
257
- reason
258
- });
259
- }
260
- }
261
- return renderGuideStatus({
262
- running,
263
- doctor,
264
- latest: await checkForUpdate(running, {
265
- now,
266
- fetch: fetchImpl
267
- })
268
- });
269
- }
270
- //#endregion
271
- export { inspectStimVersions as i, recordDoctorRun as n, compareStimVersions as r, guideStatus as t };
152
+ export { inspectStimVersions as n, compareStimVersions as t };
@@ -1,15 +1,13 @@
1
- import { B as validateDeviceSlot, C as withConfigLock, I as deviceSlotPlatforms, L as parseDeviceSlotKey, R as projectDeviceSlots, S as upsertProject, a as clearSupervisor, f as getProject, w as process_identity_exports } from "./config-B7iyKWkL.mjs";
2
- import { a as withWorkspaceStateLock, h as workspaceDir, n as clearWorkspaceStateKeys, o as writeWorkspaceState, r as readWorkspaceState } from "./workspace-state-BT5E3SA2.mjs";
3
- import { a as findProjectRoot } from "./project-DDZWf_16.mjs";
4
- import { a as clearRemoteSession, i as clearManagedMetroTunnel, l as readRemoteSession, o as clearWorkspaceSupervisor, s as readMetroTunnel } from "./state-DZobzxLW.mjs";
5
- import { a as resolveProjectMetro, i as pidExists, n as killMetroTree, o as signalProcessTree } from "./metro-C7bcJ0uo.mjs";
6
- import { n as verifyCollectorOwnership } from "./ownership-Cp52QAK1.mjs";
7
- import { f as releasedLeaseFact, l as phaseLine, u as plural } from "./command-output-B3ycpsYv.mjs";
8
- import { S as resolveEasCliBin, n as teardownOwnedIosSim, t as teardownOwnedAvd } from "./teardown-BJqEzPo_.mjs";
9
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BYfZz_S-.mjs";
10
- import { l as releaseWorkspaceLeases } from "./device-lease-Ffz_M5t0.mjs";
11
- import { i as endRecordedSession } from "./device-remote-ByN8P6CB.mjs";
12
- import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-wolE-0Do.mjs";
1
+ import { B as parseDeviceSlotKey, D as process_identity_exports, H as projectDeviceSlots, T as withConfigLock, V as parseDeviceSlotOption, W as validateDeviceSlot, a as clearSupervisor, f as getProject, w as upsertProject, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
2
+ import { O as workspaceDir, _ as withWorkspaceStateLock, c as signalProcessTree, i as pidExists, m as readWorkspaceState, p as clearWorkspaceStateKeys, s as resolveProjectMetro, t as killMetroTree, y as writeWorkspaceState } from "./metro-BWLOJ_Wr.mjs";
3
+ import { o as findProjectRoot } from "./project-BXZzOABt.mjs";
4
+ import { a as clearRemoteSession, i as clearManagedMetroTunnel, l as readRemoteSession, o as clearWorkspaceSupervisor, s as readMetroTunnel } from "./state-WGdIbi_d.mjs";
5
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BXA96myD.mjs";
6
+ import { f as refuseNoProject, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-D5z2_bcF.mjs";
7
+ import { u as releaseWorkspaceLeases } from "./device-lease-CbEaIk7b.mjs";
8
+ import { d as teardownOwnedAvd, f as teardownOwnedIosSim, i as stopTunnel, t as resolveSupervisorTarget } from "./ownership-DZjdx8Gq.mjs";
9
+ import { n as verifyCollectorOwnership } from "./ownership-D2xbLVoz.mjs";
10
+ import { i as endRecordedSession, z as resolveEasCliBin } from "./device-remote-Dy1jGj3U.mjs";
13
11
  import chalk from "chalk";
14
12
  //#region src/commands/stop.ts
15
13
  const DEFAULT_WAIT_MS = 1e4;
@@ -143,7 +141,7 @@ async function runStop(options) {
143
141
  result.summary = summarize(root, result.outcomes, result.ok);
144
142
  return result;
145
143
  }
146
- async function stopWorkspace({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => signalProcessTree(pid), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, inspectIdentity = process_identity_exports.inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
144
+ async function stopWorkspace({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => signalProcessTree(pid), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, signalServer = (pid) => signalProcessTree(pid), inspectIdentity = process_identity_exports.inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
147
145
  const proj = project === void 0 ? getProject(root) : project;
148
146
  const sup = state === void 0 ? readSupervisorState(root) : state;
149
147
  const collectorRecords = collectors === void 0 ? readCollectorState(root) : collectors;
@@ -172,6 +170,7 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
172
170
  };
173
171
  let ok = true;
174
172
  let stillHolding = null;
173
+ let keepPort = null;
175
174
  const target = resolveSupervisorTarget({
176
175
  state: sup,
177
176
  record: proj?.supervisor ?? null,
@@ -232,10 +231,17 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
232
231
  };
233
232
  else if (reservedPort === null) report(chalk.dim(phaseLine("metro", "no port reserved")));
234
233
  else {
235
- outcomes.metro = await stopMetro(reservedPort, root, {
234
+ const metro = await stopMetro(reservedPort, root, {
235
+ server: sup,
236
+ supervisorGone: target.status === "stale",
237
+ signalServer,
238
+ inspectIdentity,
239
+ waiter,
236
240
  resolveMetro,
237
241
  report
238
242
  });
243
+ outcomes.metro = metro.outcome;
244
+ keepPort = metro.keepPort;
239
245
  if (outcomes.metro.status === "refused" || outcomes.metro.status === "failed") {
240
246
  ok = false;
241
247
  stillHolding = outcomes.metro.reason;
@@ -254,6 +260,15 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
254
260
  return;
255
261
  }
256
262
  if (reservedPort !== null) {
263
+ if (keepPort) {
264
+ outcomes.port = {
265
+ status: "kept",
266
+ port: reservedPort,
267
+ reason: keepPort
268
+ };
269
+ report(chalk.dim(phaseLine("port", `keeping reservation ${reservedPort} -- ${keepPort}`)));
270
+ return;
271
+ }
257
272
  if (freePort(root, reservedPort) === false) {
258
273
  ok = false;
259
274
  outcomes.port = {
@@ -334,8 +349,12 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
334
349
  reason: stillHolding
335
350
  };
336
351
  report(chalk.yellow(phaseLine("port", `keeping reservation ${reservedPort ?? "(none)"} -- ${stillHolding}`)));
337
- const supervisorIsDown = outcomes.supervisor.status === "none" || outcomes.supervisor.status === "already-stopped" || outcomes.supervisor.status === "stopped";
338
- if (tunnelHolding && supervisorIsDown) {
352
+ const serverIsDown = [
353
+ "none",
354
+ "already-stopped",
355
+ "stopped"
356
+ ].includes(outcomes.supervisor.status) && !["refused", "failed"].includes(outcomes.metro.status);
357
+ if (tunnelHolding && serverIsDown) {
339
358
  clearState(root, sup);
340
359
  await clearRegistration(root, proj?.supervisor ?? null);
341
360
  }
@@ -476,21 +495,76 @@ async function stopSupervisor(target, { killGroup, waiter, report }) {
476
495
  reason
477
496
  };
478
497
  }
479
- async function stopMetro(port, root, { resolveMetro, report }) {
498
+ async function stopMetro(port, root, { server, supervisorGone, signalServer, inspectIdentity, waiter, resolveMetro, report }) {
499
+ const serverPid = numberOrNull(server?.serverPid);
500
+ const serverProcessToken = server?.serverProcessToken;
501
+ const serverRecord = {
502
+ pid: serverPid,
503
+ processToken: serverProcessToken
504
+ };
505
+ const identity = supervisorGone && serverPid && typeof serverProcessToken === "string" ? inspectIdentity(serverRecord) : null;
506
+ if (identity === "unknown") {
507
+ const reason = `the identity of dev server pid ${serverPid} left by the supervisor could not be verified`;
508
+ report(chalk.yellow(phaseLine("metro", `refusing to signal it: ${reason}`)));
509
+ return {
510
+ outcome: {
511
+ status: "refused",
512
+ port,
513
+ pid: serverPid,
514
+ reason
515
+ },
516
+ keepPort: null
517
+ };
518
+ }
519
+ if (identity === "same") {
520
+ report(chalk.dim(phaseLine("metro", `sending SIGTERM to dev server pid ${serverPid} left by the supervisor`)));
521
+ let signalled = false;
522
+ try {
523
+ signalled = signalServer(serverPid);
524
+ } catch {}
525
+ if (signalled ? await waiter(serverPid, serverProcessToken) : ["gone", "different"].includes(inspectIdentity(serverRecord))) {
526
+ report(chalk.green(phaseLine("metro", `dev server pid ${serverPid}`)));
527
+ return {
528
+ outcome: {
529
+ status: "stopped",
530
+ port,
531
+ pid: serverPid
532
+ },
533
+ keepPort: null
534
+ };
535
+ }
536
+ const reason = `dev server pid ${serverPid} left by the supervisor did not exit`;
537
+ report(chalk.red(phaseLine("metro", reason)));
538
+ return {
539
+ outcome: {
540
+ status: "failed",
541
+ port,
542
+ pid: serverPid,
543
+ reason
544
+ },
545
+ keepPort: null
546
+ };
547
+ }
480
548
  const resolution = await resolveMetro(port, root);
481
549
  if (resolution.missing) {
482
550
  report(chalk.dim(phaseLine("metro", `nothing listening on port ${port}`)));
483
551
  return {
484
- status: "missing",
485
- port
552
+ outcome: {
553
+ status: "missing",
554
+ port
555
+ },
556
+ keepPort: null
486
557
  };
487
558
  }
488
559
  const reason = resolution.notOurs || "no recorded Stim supervisor owns this server; stop it with the tool that started it";
489
560
  report(chalk.yellow(phaseLine("metro", `leaving port ${port} alone: ${reason}`)));
490
561
  return {
491
- status: "not-managed",
492
- port,
493
- reason
562
+ outcome: {
563
+ status: "not-managed",
564
+ port,
565
+ reason
566
+ },
567
+ keepPort: resolution.metro ? `a dev server from this project still answers on port ${port}` : null
494
568
  };
495
569
  }
496
570
  const OCCUPANCY_HINT = "often a UI-test runner or device tool still attached";
@@ -521,7 +595,7 @@ function shutDownDevices(project, { teardownIos, teardownAvd, report }) {
521
595
  } else device[iosKey] = reportDevice(iosUdid, teardownIos(iosUdid, {
522
596
  del: false,
523
597
  label: iosName
524
- }), report);
598
+ }), report, "ios");
525
599
  }
526
600
  const android = platforms.android;
527
601
  if (android?.avdName) {
@@ -533,12 +607,15 @@ function shutDownDevices(project, { teardownIos, teardownAvd, report }) {
533
607
  reason: "Stim does not own this device"
534
608
  };
535
609
  report(chalk.dim(phaseLine("device", `${android.avdName} is not Stim-owned, leaving it running`)));
536
- } else device[androidKey] = reportDevice(android.avdName, teardownAvd(android.avdName, { del: false }), report);
610
+ } else device[androidKey] = reportDevice(android.avdName, teardownAvd(android.avdName, { del: false }), report, "android");
537
611
  }
538
612
  }
539
613
  return device;
540
614
  }
541
- function reportDevice(label, r, report) {
615
+ function deviceShutdownRemedy(kind, label) {
616
+ return kind === "ios" ? `check it with \`xcrun simctl list devices\`, then \`xcrun simctl shutdown ${label}\` yourself -- if it will not respond, \`stim gc --delete\` reclaims it` : `check it with \`adb devices\`, then \`adb -s <serial> emu kill\` yourself -- if it will not respond, \`stim gc --delete\` reclaims it`;
617
+ }
618
+ function reportDevice(label, r, report, kind) {
542
619
  if (r.status === "torn-down") {
543
620
  report(chalk.green(phaseLine("device", `shut down ${r.label ?? label}`)));
544
621
  return {
@@ -563,11 +640,14 @@ function reportDevice(label, r, report) {
563
640
  reason
564
641
  };
565
642
  }
566
- report(chalk.red(phaseLine("device", `failed to shut down ${label}: ${r.reason}`)));
643
+ const remedy = deviceShutdownRemedy(kind, r.label ?? label);
644
+ report(chalk.red(phaseLine("device", `failed to shut down ${r.label ?? label}: ${r.reason}`)));
645
+ report(chalk.dim(phaseLine("", remedy)));
567
646
  return {
568
647
  status: "failed",
569
- label,
570
- reason: r.reason
648
+ label: r.label ?? label,
649
+ reason: r.reason,
650
+ remedy
571
651
  };
572
652
  }
573
653
  function summarize(root, outcomes, ok) {
@@ -617,11 +697,11 @@ async function defaultClearRegistration(root, expected) {
617
697
  }
618
698
  }
619
699
  function stopCommand(program) {
620
- program.command("stop").description("The inverse of `start`: halt this workspace's supervisor, shut the owned device down (never deleted), and free the reserved port. Non-destructive -- the device stays assigned, so coming back costs a boot. Acts on the current workspace.").option("--slot <name>", "Stop only this device slot, keeping the shared server running", validateDeviceSlot).option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
700
+ program.command("stop").description("The inverse of `start`: halt this workspace's supervisor, shut the owned device down (never deleted), and free the reserved port. Non-destructive -- the device stays assigned, so coming back costs a boot. Acts on the current workspace.").option("--slot <name>", "Stop only this device slot, keeping the shared server running", parseDeviceSlotOption).option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
621
701
  const root = findProjectRoot(process.cwd());
622
702
  if (!root) {
623
- console.error(chalk.red("Not inside a project (no package.json found above the current directory)."));
624
- process.exit(1);
703
+ refuseNoProject({ json: Boolean(opts.json) });
704
+ return;
625
705
  }
626
706
  const { ok, outcomes, summary } = await withWorkspaceProcessLock(workspaceDir(root), "native-run", () => runStop({
627
707
  root,
@@ -1,3 +1,3 @@
1
- import { t as resolveSupervisorTarget } from "./ownership-wolE-0Do.mjs";
2
- import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-C8JW6kKp.mjs";
1
+ import { t as resolveSupervisorTarget } from "./ownership-DZjdx8Gq.mjs";
2
+ import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-Dj4pfmyv.mjs";
3
3
  export { clearCollectorState, clearSupervisorState, stopCommand as default, readCollectorState, readSupervisorState, resolveCollectorTargets, resolveSupervisorTarget, runStop };
@@ -1,4 +1,10 @@
1
- import { t as NdjsonWriter } from "./ndjson-DvHSsNU9.mjs";
1
+ import { t as NdjsonWriter } from "./ndjson-Du3j0AQ9.mjs";
2
+ import "@stim-cli/core/state";
3
+ //#region src/supervisor/idle-stop.d.ts
4
+ interface IdleProbe {
5
+ lastActivityAt(): number;
6
+ blocker(): string | null;
7
+ }
2
8
  import * as import__stim_cli_core_process_identity from "@stim-cli/core/process-identity";
3
9
  //#endregion
4
10
  //#region src/supervisor/state.d.ts
@@ -15,6 +21,7 @@ interface ParsedSupervisorArgs {
15
21
  port?: number;
16
22
  tunnel?: boolean;
17
23
  resetCache?: boolean;
24
+ idleStopMinutes?: number;
18
25
  error?: string;
19
26
  }
20
27
  declare function parseArgs(argv: string[]): ParsedSupervisorArgs;
@@ -43,6 +50,8 @@ interface RunSupervisorOptions {
43
50
  port: number;
44
51
  tunnel?: boolean;
45
52
  resetCache?: boolean;
53
+ idleStopMinutes?: number;
54
+ idleProbe?: IdleProbe;
46
55
  isExpo?: (projectRoot: string) => boolean;
47
56
  startBare?: ServerStarter | null;
48
57
  startExpo?: ServerStarter | null;
@@ -51,7 +60,7 @@ interface RunSupervisorOptions {
51
60
  attachSignals?: boolean;
52
61
  stderr?: (line: string) => void;
53
62
  }
54
- declare function runSupervisor({ root, port, tunnel, resetCache, isExpo, startBare, startExpo, now, onExit, attachSignals, stderr }: RunSupervisorOptions): Promise<{
63
+ declare function runSupervisor({ root, port, tunnel, resetCache, idleStopMinutes, idleProbe, isExpo, startBare, startExpo, now, onExit, attachSignals, stderr }: RunSupervisorOptions): Promise<{
55
64
  mode: string;
56
65
  server: ServerHandle | undefined;
57
66
  shutdown: (code: number, event: string, msg: string) => Promise<void>;