stim 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
  3. package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
  4. package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
  5. package/dist/android-cas-compiler.mjs +2 -2
  6. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
  7. package/dist/app-install-Uf3a83Tv.mjs +819 -0
  8. package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
  9. package/dist/build-progress-CVqkfPC6.mjs +188 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.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 +2 -0
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
  21. package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
  22. package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
  27. package/dist/exec-jIAlgilk.mjs +132 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
  29. package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
  30. package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
  31. package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
  32. package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
  33. package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
  34. package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
  35. package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
  36. package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
  37. package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
  38. package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
  39. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
  40. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
  41. package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
  42. package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
  43. package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
  44. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  45. package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
  46. package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
  47. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
  48. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
  49. package/dist/server-expo-CeILEu86.mjs +2 -0
  50. package/dist/settings-BKNh5rl4.mjs +376 -0
  51. package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
  52. package/dist/settings.schema.json +989 -0
  53. package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
  54. package/dist/simslim-B_LsGLuJ.mjs +259 -0
  55. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
  56. package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
  57. package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
  58. package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
  59. package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
  60. package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
  61. package/dist/status-S6ehL4Ui.mjs +932 -0
  62. package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
  63. package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
  64. package/dist/supervisor-run.mjs +52 -34
  65. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
  66. package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
  67. package/dist/worktree-DKhSF0UX.mjs +2 -0
  68. package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
  69. package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
  70. package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
  71. package/package.json +5 -6
  72. package/skill/SKILL.md +2 -0
  73. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  74. package/dist/build-slots-C-oAHRT1.mjs +0 -151
  75. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  76. package/dist/exec-B29XNuhP.mjs +0 -77
  77. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  78. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  79. package/dist/status-DdkLki47.mjs +0 -426
  80. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -0,0 +1,259 @@
1
+ import { D as process_identity_exports, K as projectDeviceSlots, U as deviceSlotPlatforms, g as loadConfig, k as ownership_claim_exports, u as getConfigDir } from "./config-Cp9lM3wX.mjs";
2
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
+ import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
4
+ import { C as listAdbDevices } from "./android-_y4Ohi5z.mjs";
5
+ import { c as iosRuntimeMatches, u as listAllIosSims } from "./ios-BL2NFckr.mjs";
6
+ import { join } from "node:path";
7
+ //#region src/engine/device-capacity.ts
8
+ function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
9
+ let count = 0;
10
+ for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
11
+ const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
12
+ for (const proj of Object.values(config?.projects || {})) for (const { platforms } of projectDeviceSlots(proj)) {
13
+ const android = platforms.android;
14
+ if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
15
+ }
16
+ return count;
17
+ }
18
+ function workspaceHasLiveDevice({ platform, project, slot = "default", sims = [], adbEmulators = [] } = {}) {
19
+ if (!platform) return false;
20
+ const record = deviceSlotPlatforms(project, slot)?.[platform];
21
+ if (!record) return false;
22
+ if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
23
+ return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
24
+ }
25
+ function deviceCapacityRefusal({ platform, project, slot = "default", max, sims = [], adb = null, config = null } = {}) {
26
+ if (!max || max <= 0) return null;
27
+ const adbEmulators = adb?.emulators || [];
28
+ if (workspaceHasLiveDevice({
29
+ platform,
30
+ project,
31
+ slot,
32
+ sims,
33
+ adbEmulators
34
+ })) return null;
35
+ const count = liveOwnedDeviceCount({
36
+ sims,
37
+ adbEmulators,
38
+ config
39
+ });
40
+ if (count < max) return null;
41
+ return {
42
+ code: "STIM_AT_CAPACITY",
43
+ message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
44
+ remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
45
+ };
46
+ }
47
+ function checkDeviceCapacity({ platform, project, slot = "default", max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
48
+ if (!max || max <= 0) return null;
49
+ let simList = [];
50
+ let adbRes = {
51
+ emulators: [],
52
+ physical: [],
53
+ unhealthy: []
54
+ };
55
+ try {
56
+ simList = typeof sims === "function" ? sims() || [] : sims || [];
57
+ } catch {}
58
+ try {
59
+ adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
60
+ } catch {}
61
+ let cfg = null;
62
+ try {
63
+ cfg = typeof config === "function" ? config() : config ?? null;
64
+ } catch {}
65
+ return deviceCapacityRefusal({
66
+ platform,
67
+ project,
68
+ slot,
69
+ max,
70
+ sims: simList,
71
+ adb: adbRes,
72
+ config: cfg
73
+ });
74
+ }
75
+ function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
76
+ if (!requestedName || !recordedTypeId) return null;
77
+ const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
78
+ if (!wanted) return null;
79
+ if (wanted.identifier === recordedTypeId) return null;
80
+ const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
81
+ return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
82
+ }
83
+ function installedNames(names) {
84
+ const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
85
+ return unique.length > 0 ? unique.join(", ") : "none";
86
+ }
87
+ function unknownIosRuntimeRefusal(requested, runtimes) {
88
+ if (!requested) return null;
89
+ if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
90
+ return {
91
+ message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
92
+ remedy: "Pass `--runtime` (or set ios.runtime) a version printed above (\"26.5\") or a runtime's full name (\"iOS 26.5\"); nothing else matches. Install more runtimes through Xcode."
93
+ };
94
+ }
95
+ function creatableIosDeviceTypeNames(runtimes, runtime) {
96
+ return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
97
+ }
98
+ function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
99
+ if (!requested) return null;
100
+ const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
101
+ if (creatable.includes(requested)) return null;
102
+ return {
103
+ message: `No device type named "${requested}" can be created on ${runtime ? `runtime ${runtime}` : "any installed simulator runtime"}. ${runtime ? `Device types runtime ${runtime} supports` : "Device types the installed runtimes support"}: ${installedNames(creatable)}.`,
104
+ remedy: "Pass `--device-type` (or set ios.deviceType) one of the names printed above, exactly as `xcrun simctl list devicetypes` spells it. `xcrun simctl list devicetypes` also lists watchOS, tvOS and visionOS models, which no iOS runtime can create. Install more models through Xcode."
105
+ };
106
+ }
107
+ function unknownAndroidSystemImageRefusal(requested, images) {
108
+ if (!requested) return null;
109
+ if ((images || []).some((i) => i.pkg === requested)) return null;
110
+ return {
111
+ message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
112
+ remedy: "Pass `--system-image` (or set android.systemImage) to one of the package ids printed above. Install more with `sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`."
113
+ };
114
+ }
115
+ //#endregion
116
+ //#region src/engine/simslim.ts
117
+ async function reconcileSimSlim({ udid, profile, previouslyManaged = false, out = () => {}, spawn, timeoutMs = 72e4, cleanupMs = 1e4 }) {
118
+ if (!profile && !previouslyManaged) return {
119
+ managed: false,
120
+ profile: null
121
+ };
122
+ const action = profile ? "on" : "off";
123
+ const args = profile ? [
124
+ "on",
125
+ udid,
126
+ "--profile",
127
+ profile
128
+ ] : ["off", udid];
129
+ const lines = [];
130
+ const onLine = (raw) => {
131
+ const line = stripAnsi(raw).trim();
132
+ if (!line) return;
133
+ lines.push(line);
134
+ if (lines.length > 20) lines.shift();
135
+ out(`SimSlim: ${line}`);
136
+ };
137
+ const stdout = createLineReader(onLine);
138
+ const stderr = createLineReader(onLine);
139
+ const root = join(getConfigDir(), "simslim-locks", `${encodeURIComponent(udid.toLowerCase())}.lock`);
140
+ const label = `SimSlim for ${udid}`;
141
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
142
+ root,
143
+ mode: "exclusive",
144
+ label
145
+ });
146
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
147
+ const claim = attempt.acquired;
148
+ if (!claim) throw new ownership_claim_exports.ClaimRefusedError({
149
+ root,
150
+ claimPath: attempt.held?.path ?? root,
151
+ label,
152
+ reason: "another SimSlim operation is still using this simulator"
153
+ });
154
+ let child;
155
+ let result;
156
+ let identity;
157
+ let cancellation;
158
+ const stopGroup = () => {
159
+ if (child?.pid !== void 0 && identity && (0, process_identity_exports.inspectProcessIdentity)(identity) === "same") try {
160
+ process.kill(-child.pid, "SIGKILL");
161
+ } catch {}
162
+ };
163
+ const cancel = (signal) => {
164
+ cancellation ??= signal;
165
+ stopGroup();
166
+ };
167
+ const onInterrupt = () => cancel("SIGINT");
168
+ const onTerminate = () => cancel("SIGTERM");
169
+ const groupAlive = () => child?.pid !== void 0 && (0, ownership_claim_exports.processGroupAlive)(child.pid);
170
+ const settled = () => result !== void 0 && !groupAlive();
171
+ const waitUntil = async (deadline, cancellable = false) => {
172
+ while (!settled() && Date.now() < deadline) {
173
+ if (cancellable && cancellation) break;
174
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
175
+ }
176
+ return settled();
177
+ };
178
+ process.on("SIGINT", onInterrupt);
179
+ process.on("SIGTERM", onTerminate);
180
+ process.on("exit", stopGroup);
181
+ try {
182
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
183
+ try {
184
+ child = (spawn ?? ((cmd, childArgs, opts) => getExecutor().spawn(cmd, childArgs, opts)))("simslim", args, {
185
+ stdio: [
186
+ "ignore",
187
+ "pipe",
188
+ "pipe"
189
+ ],
190
+ detached: true
191
+ });
192
+ } catch (error) {
193
+ throw simslimLaunchError(error);
194
+ }
195
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
196
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
197
+ waitForChild(child).then((value) => {
198
+ result = value;
199
+ });
200
+ if (child.pid !== void 0) {
201
+ const captured = (0, process_identity_exports.captureProcessIdentity)(child.pid);
202
+ if (captured.ok) {
203
+ identity = {
204
+ pid: child.pid,
205
+ processToken: captured.token
206
+ };
207
+ try {
208
+ (0, ownership_claim_exports.setClaimChild)(claim, identity);
209
+ } catch {}
210
+ }
211
+ }
212
+ const completed = await waitUntil(Date.now() + timeoutMs, true);
213
+ if (cancellation || !completed) {
214
+ stopGroup();
215
+ const stopped = await waitUntil(Date.now() + cleanupMs);
216
+ const detail = lines.length ? ` ${lines.join(" | ")}` : "";
217
+ const recovery = stopped ? "Its process group has stopped. Retry stim ios to reconcile the retained simulator settings." : `Its process group could not be confirmed stopped. The claim remains at ${claim.path}. Inspect the SimSlim process group ${child.pid ?? "unknown"} before retrying. Only when nothing is using it, remove the claim: ${(0, ownership_claim_exports.claimRemoveCommand)(claim.path)}`;
218
+ const message = `SimSlim ${action} ${cancellation ? `interrupted by ${cancellation}` : `exceeded ${Math.round(timeoutMs / 1e3)}s`}. ${recovery}${detail}`;
219
+ if (cancellation) out(message);
220
+ throw Object.assign(new Error(message), { code: cancellation ? "EINTR" : "ETIMEDOUT" });
221
+ }
222
+ } finally {
223
+ if (!child || settled()) (0, ownership_claim_exports.releaseClaim)(claim);
224
+ else {
225
+ child.stdout?.destroy?.();
226
+ child.stderr?.destroy?.();
227
+ child.unref();
228
+ }
229
+ process.off("SIGINT", onInterrupt);
230
+ process.off("SIGTERM", onTerminate);
231
+ process.off("exit", stopGroup);
232
+ if (cancellation) process.exit(cancellation === "SIGINT" ? 130 : 143);
233
+ }
234
+ stdout.flush();
235
+ stderr.flush();
236
+ if (result?.error) throw simslimLaunchError(result.error);
237
+ if (result?.code !== 0) {
238
+ const detail = lines.length ? ` ${lines.join(" | ")}` : "";
239
+ throw new Error(`SimSlim ${action} failed with exit code ${result?.code ?? "unknown"}.${detail}`);
240
+ }
241
+ return {
242
+ managed: Boolean(profile),
243
+ profile: profile ?? null
244
+ };
245
+ }
246
+ function simslimLaunchError(error) {
247
+ const cause = error;
248
+ if (cause?.code === "ENOENT") return new Error("SimSlim is configured but the `simslim` command is not installed. Run `brew install mobai-app/tap/simslim`.", { cause });
249
+ return new Error(`Could not start SimSlim: ${String(cause?.message || cause)}`, { cause });
250
+ }
251
+ function simslimIsOnPath() {
252
+ try {
253
+ return Boolean(getExecutor().findExecutable("simslim"));
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ //#endregion
259
+ export { liveOwnedDeviceCount as a, unknownIosRuntimeRefusal as c, deviceTypeMismatch as i, simslimIsOnPath as n, unknownAndroidSystemImageRefusal as o, checkDeviceCapacity as r, unknownIosDeviceTypeRefusal as s, reconcileSimSlim as t };
@@ -1,7 +1,7 @@
1
- import { f as getProject } from "./config-B7iyKWkL.mjs";
2
- import { r as readWorkspaceState } from "./workspace-state-BT5E3SA2.mjs";
3
- import { h as deviceShellArg } from "./app-install-BDbsSgCl.mjs";
4
- import { n as fileLeaseIo } from "./device-lease-Ffz_M5t0.mjs";
1
+ import { f as getProject } from "./config-Cp9lM3wX.mjs";
2
+ import { r as readWorkspaceState } from "./workspace-state-DF2gCFhx.mjs";
3
+ import { d as deviceShellArg } from "./app-install-Uf3a83Tv.mjs";
4
+ import { n as fileLeaseIo } from "./device-lease-Crs-HkJY.mjs";
5
5
  //#region src/engine/slot-launch.ts
6
6
  function launchSlotScope(root, slot = "default") {
7
7
  if (slot !== "default") return slot;
@@ -0,0 +1,49 @@
1
+ import { D as process_identity_exports, k as ownership_claim_exports } from "./config-Cp9lM3wX.mjs";
2
+ //#region src/engine/spawn-claims.ts
3
+ const declaring = /* @__PURE__ */ new Set();
4
+ /** Record every child later started through `spawnDeclared` on `claim` until `stopDeclaringSpawnsOn(claim)`. */
5
+ function declareSpawnsOn(claim) {
6
+ if (claim) declaring.add(claim);
7
+ }
8
+ function stopDeclaringSpawnsOn(claim) {
9
+ if (claim) declaring.delete(claim);
10
+ }
11
+ /**
12
+ * Start a child that every claim passed to `declareSpawnsOn` records until it exits. The child keeps
13
+ * the caller's process group, so a terminal interrupt still reaches it; the claim follows the child
14
+ * itself. A claim that cannot be marked stops the spawn rather than letting it run unprotected. A claim
15
+ * records one child, so declared spawns must not overlap.
16
+ */
17
+ function spawnDeclared(spawn) {
18
+ const claims = [...declaring];
19
+ if (claims.length === 0) return spawn();
20
+ const forget = () => {
21
+ for (const claim of claims) try {
22
+ (0, ownership_claim_exports.clearClaimChild)(claim);
23
+ } catch {}
24
+ };
25
+ let child;
26
+ try {
27
+ for (const claim of claims) (0, ownership_claim_exports.markClaimChildPending)(claim);
28
+ child = spawn();
29
+ } catch (error) {
30
+ forget();
31
+ throw error;
32
+ }
33
+ const pid = child.pid;
34
+ if (pid === void 0) {
35
+ forget();
36
+ return child;
37
+ }
38
+ const captured = (0, process_identity_exports.captureProcessIdentity)(pid);
39
+ if (captured.ok) for (const claim of claims) try {
40
+ (0, ownership_claim_exports.setClaimChild)(claim, {
41
+ pid,
42
+ processToken: captured.token
43
+ });
44
+ } catch {}
45
+ child.once("exit", forget);
46
+ return child;
47
+ }
48
+ //#endregion
49
+ export { spawnDeclared as n, stopDeclaringSpawnsOn as r, declareSpawnsOn as t };
@@ -1,23 +1,24 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { S as upsertProject, a as clearSupervisor, f as getProject, w as process_identity_exports } from "./config-B7iyKWkL.mjs";
3
- import { _ as workspaceLogsDir, c as ensureWorkspaceStorage, f as supervisorLogFile, o as writeWorkspaceState, r as readWorkspaceState } from "./workspace-state-BT5E3SA2.mjs";
4
- import { g as repoRoot, s as gitCommonDir } from "./worktree-FG_ZKKdb.mjs";
5
- import { a as findProjectRoot, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-DDZWf_16.mjs";
1
+ import { D as process_identity_exports, a as clearSupervisor, f as getProject, w as upsertProject } from "./config-Cp9lM3wX.mjs";
2
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
+ import { i as levelRank } from "./ndjson-9uFNRwfe.mjs";
4
+ import { b as workspaceLogsDir, c as writeWorkspaceState, i as recordWorkspaceUse, m as supervisorLogFile, r as readWorkspaceState, u as ensureWorkspaceStorage } from "./workspace-state-DF2gCFhx.mjs";
5
+ import { _ as repoRoot, s as gitCommonDir } from "./worktree-R_cUlE7s.mjs";
6
+ import { a as detectIsExpo, i as detectBundleId, n as appProjectProblem, o as findProjectRoot, r as detectAndroidPackage, t as NO_PROJECT_REFUSAL } from "./project-DS112DYB.mjs";
6
7
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
7
- import { n as windowsLauncherArgs } from "./detached-entry-Bpo6ZY_I.mjs";
8
- import { i as clearManagedMetroTunnel, o as clearWorkspaceSupervisor, s as readMetroTunnel } from "./state-DZobzxLW.mjs";
9
- import { a as resolveProjectMetro, i as pidExists, l as reserveMetroPort, n as killMetroTree, o as signalProcessTree } from "./metro-C7bcJ0uo.mjs";
10
- import { A as resolveSettings, D as remoteDeviceSettingError, E as remoteAndroidSetting, F as tunnelModeSetting, I as unknownSettingKeys, L as PUBLIC_METRO_ENV, N as settingShapeErrors, O as remoteIosSetting, R as detectProviders, S as ngrokUrlSetting, T as publicUrlSetting, a as SETTING_SHAPE_REMEDY, b as metroTunnelSettingError, d as cacheProviderSettingError, k as resolveCacheProviderConfig, z as planMetroReach } from "./settings-BXXorVM2.mjs";
11
- import { g as stepTimer, l as phaseLine } from "./command-output-B3ycpsYv.mjs";
12
- import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-BYfZz_S-.mjs";
8
+ import { n as windowsLauncherArgs } from "./detached-entry-KEu0_AXL.mjs";
9
+ import { i as clearManagedMetroTunnel, o as clearWorkspaceSupervisor, s as readMetroTunnel } from "./state-NcJzMEBF.mjs";
10
+ import { a as pidExists, c as resolveProjectMetro, f as reserveMetroPort, l as signalProcessTree, n as killMetroTree } from "./metro-DrLHaMn2.mjs";
11
+ import { D as remoteIosSetting, E as remoteAndroidSetting, F as unknownSettingKeys, G as planMetroReach, M as settingShapeErrors, O as resolveCacheProviderConfig, P as tunnelModeSetting, U as PUBLIC_METRO_ENV, W as detectProviders, i as SETTING_SHAPE_REMEDY, k as resolveSettings, u as cacheProviderSettingError, w as publicUrlSetting, x as ngrokUrlSetting, y as metroTunnelSettingError } from "./settings-D030k0t4.mjs";
12
+ import { _ as stepTimer, l as phaseLine } from "./command-output-gByVmIjy.mjs";
13
+ import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-DWaiRD0e.mjs";
13
14
  import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
14
- import { c as queryLogs } from "./logs-query-BGL-Pl12.mjs";
15
- import { a as withManagedRemoteWorktreeLock, i as terminateChild, n as startTunnelSequence, r as stopTunnel, s as withManagedTunnelLock, t as resolveSupervisorTarget } from "./ownership-wolE-0Do.mjs";
15
+ import { l as queryLogs } from "./logs-query-Cqma3WIu.mjs";
16
+ import { a as terminateChild, c as withManagedTunnelLock, i as stopTunnel, o as withManagedRemoteWorktreeLock, r as startTunnelSequence, t as resolveSupervisorTarget } from "./ownership-DCQPuWEc.mjs";
16
17
  import { appendFileSync, mkdirSync, openSync, readFileSync } from "node:fs";
17
18
  import { dirname } from "node:path";
18
19
  import { EventEmitter } from "node:events";
19
- import { CACHE_PROVIDER_ENV, cacheProviderEnv } from "@stim-cli/cache";
20
20
  import chalk from "chalk";
21
+ import { CACHE_PROVIDER_ENV, cacheProviderEnv } from "@stim-cli/cache";
21
22
  //#region src/supervisor/cache-reset.ts
22
23
  async function stopOwnedMetroForReset(root) {
23
24
  const project = getProject(root);
@@ -179,11 +180,7 @@ function registerStart(program, overrides = {}) {
179
180
  });
180
181
  const waitSeconds = wait.seconds;
181
182
  const root = findProjectRoot(process.cwd());
182
- if (!root) return fail({
183
- code: "STIM_NO_PROJECT",
184
- message: "Not in a React Native project (no package.json found).",
185
- remedy: "Run this from the app directory -- the one holding package.json."
186
- });
183
+ if (!root) return fail(NO_PROJECT_REFUSAL);
187
184
  const projectProblem = appProjectProblem(root);
188
185
  if (projectProblem) return fail({
189
186
  code: "STIM_NO_PROJECT",
@@ -199,6 +196,7 @@ function registerStart(program, overrides = {}) {
199
196
  remedy: "Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`)."
200
197
  });
201
198
  }
199
+ recordWorkspaceUse(root);
202
200
  const isExpo = detectIsExpo(root);
203
201
  const worktreeRoot = repoRoot(root) ?? root;
204
202
  const settingsContext = {
@@ -222,13 +220,7 @@ function registerStart(program, overrides = {}) {
222
220
  if (settingError) return fail({
223
221
  code: "STIM_BAD_ARG",
224
222
  message: settingError,
225
- remedy: `Set metro.tunnel to one of: auto, expo, ngrok, cloudflared, off.`
226
- });
227
- const remoteSettingError = remoteDeviceSettingError(settings);
228
- if (remoteSettingError) return fail({
229
- code: "STIM_BAD_ARG",
230
- message: remoteSettingError,
231
- remedy: "Set ios.remote and android.remote to either proxy or eas."
223
+ remedy: "Set metro.tunnel to \"ngrok\" and metro.ngrokUrl to an HTTPS URL, or remove metro.ngrokUrl."
232
224
  });
233
225
  const remote = Boolean(opts.remote) || remoteIosSetting(settings) !== null || remoteAndroidSetting(settings) !== null;
234
226
  const tunnelMode = tunnelModeSetting(settings) ?? "auto";
@@ -820,8 +812,15 @@ function registerStart(program, overrides = {}) {
820
812
  });
821
813
  }
822
814
  async function resolvePort(root, note) {
823
- const recorded = getProject(root)?.metroPort;
815
+ const project = getProject(root);
816
+ const recorded = project?.metroPort;
824
817
  if (!recorded) return await reserveMetroPort(root);
818
+ const supervisor = resolveSupervisorTarget({
819
+ state: readWorkspaceState(root)?.supervisor,
820
+ record: project.supervisor,
821
+ reservedPort: recorded
822
+ });
823
+ if (supervisor.status !== "none" && supervisor.status !== "stale") return recorded;
825
824
  const held = await resolveProjectMetro(recorded, root);
826
825
  if (!held.notOurs) return recorded;
827
826
  const fresh = await reserveMetroPort(root);
@@ -859,11 +858,8 @@ function failureEvidence({ logFile, logsDir, sinceTs }) {
859
858
  let errors = [];
860
859
  let all = [];
861
860
  try {
862
- errors = queryLogs({
863
- dir: logsDir,
864
- minLevel: "error"
865
- });
866
861
  all = queryLogs({ dir: logsDir });
862
+ errors = all.filter((record) => levelRank(record.level) >= levelRank("error"));
867
863
  } catch {}
868
864
  const since = (rs) => rs.filter((r) => typeof r.ts === "number" && r.ts >= sinceTs).slice(-8);
869
865
  let recent = since(errors);
@@ -1,5 +1,5 @@
1
- import { F as deviceSlotKey, L as parseDeviceSlotKey, w as process_identity_exports } from "./config-B7iyKWkL.mjs";
2
- import { a as withWorkspaceStateLock, i as updateWorkspaceState, p as supervisorPidFile, r as readWorkspaceState, t as clearWorkspaceStateKey, y as workspaceStateFile } from "./workspace-state-BT5E3SA2.mjs";
1
+ import { D as process_identity_exports, H as deviceSlotKey, W as parseDeviceSlotKey } from "./config-Cp9lM3wX.mjs";
2
+ import { S as workspaceStateFile, a as updateWorkspaceState, h as supervisorPidFile, o as withWorkspaceStateLock, r as readWorkspaceState, t as clearWorkspaceStateKey } from "./workspace-state-DF2gCFhx.mjs";
3
3
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname } from "node:path";
5
5
  //#region src/supervisor/state.ts
@@ -75,10 +75,12 @@ function readRemoteSession(root) {
75
75
  if (typeof id !== "string" || id.length === 0) return null;
76
76
  const platform = record.platform;
77
77
  const startedAt = record.startedAt;
78
+ const webPreviewUrl = record.webPreviewUrl;
78
79
  return {
79
80
  platform: platform === "ios" || platform === "android" ? platform : null,
80
81
  sessionId: id,
81
- startedAt: typeof startedAt === "string" ? startedAt : null
82
+ startedAt: typeof startedAt === "string" ? startedAt : null,
83
+ webPreviewUrl: typeof webPreviewUrl === "string" && webPreviewUrl.length > 0 ? webPreviewUrl : null
82
84
  };
83
85
  }
84
86
  function readRemoteSessionId(root) {
@@ -1,7 +1,7 @@
1
- import { g as repoRoot, s as gitCommonDir } from "./worktree-FG_ZKKdb.mjs";
2
- import { a as findProjectRoot } from "./project-DDZWf_16.mjs";
3
- import { o as formatLongDuration } from "./command-output-B3ycpsYv.mjs";
4
- import { a as statsProjectKey, r as readStats } from "./stats-BYcZ6n1h.mjs";
1
+ import { _ as repoRoot, s as gitCommonDir } from "./worktree-R_cUlE7s.mjs";
2
+ import { o as findProjectRoot } from "./project-DS112DYB.mjs";
3
+ import { o as formatLongDuration } from "./command-output-gByVmIjy.mjs";
4
+ import { a as statsProjectKey, r as readStats } from "./stats-DG9e66Rz.mjs";
5
5
  import chalk from "chalk";
6
6
  //#region src/commands/stats.ts
7
7
  const PLATFORMS = ["ios", "android"];
@@ -1,7 +1,8 @@
1
- import { C as withConfigLock, u as getConfigDir } from "./config-B7iyKWkL.mjs";
1
+ import { T as withConfigLock, u as getConfigDir } from "./config-Cp9lM3wX.mjs";
2
2
  import { readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  const PLATFORMS = ["ios", "android"];
5
+ const OUTCOMES = ["hit", "cold"];
5
6
  function statsFile() {
6
7
  return join(getConfigDir(), "stats.json");
7
8
  }
@@ -41,10 +42,25 @@ function updateStats(record, run, now) {
41
42
  credit,
42
43
  phases
43
44
  });
45
+ const history = { ...record.history };
46
+ if (run.phases && !run.failed && !run.waitedForBuild) {
47
+ const outcome = isHit(run.cacheHit) ? "hit" : "cold";
48
+ const project = { ...history[run.projectKey] };
49
+ const lists = { ...project[run.platform] };
50
+ const sample = {
51
+ at,
52
+ durationMs,
53
+ phases: wholePhases(run.phases)
54
+ };
55
+ lists[outcome] = [...lists[outcome] ?? [], sample].slice(-10);
56
+ project[run.platform] = lists;
57
+ history[run.projectKey] = project;
58
+ }
44
59
  return {
45
60
  version: 1,
46
61
  machine,
47
- projects
62
+ projects,
63
+ ...Object.keys(history).length ? { history } : {}
48
64
  };
49
65
  }
50
66
  function readStats() {
@@ -108,7 +124,7 @@ function recordRunStats(run, now) {
108
124
  };
109
125
  });
110
126
  }
111
- function createRunRecorder({ platform, write, now, note }) {
127
+ function createRunRecorder({ platform, write, now, note, phases }) {
112
128
  let projectKey = null;
113
129
  let cacheKey = null;
114
130
  let coldBuildMs = 0;
@@ -130,6 +146,7 @@ function createRunRecorder({ platform, write, now, note }) {
130
146
  record({ failed, cacheHit = false, waited = null, durationMs }) {
131
147
  if (!projectKey || !cacheKey || recorded) return;
132
148
  recorded = true;
149
+ const ran = failed ? {} : phases?.() ?? {};
133
150
  try {
134
151
  const outcome = write({
135
152
  platform,
@@ -139,7 +156,8 @@ function createRunRecorder({ platform, write, now, note }) {
139
156
  waitedForBuild: Boolean(waited),
140
157
  durationMs,
141
158
  ...coldBuildMs > 0 ? { coldBuildMs } : {},
142
- ...podsMs > 0 ? { podsMs } : {}
159
+ ...podsMs > 0 ? { podsMs } : {},
160
+ ...Object.keys(ran).length ? { phases: ran } : {}
143
161
  }, now());
144
162
  if (outcome?.note) note(outcome.note);
145
163
  } catch (error) {
@@ -206,12 +224,55 @@ function normalize(record) {
206
224
  const projects = {};
207
225
  const source = record.projects;
208
226
  if (source && typeof source === "object" && !Array.isArray(source)) for (const [key, scope] of Object.entries(source)) projects[key] = normalizeScope(scope);
227
+ const history = {};
228
+ if (isObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
229
+ const normalized = normalizeHistory(scope);
230
+ if (Object.keys(normalized).length) history[key] = normalized;
231
+ }
209
232
  return {
210
233
  version: 1,
211
234
  machine: normalizeScope(record.machine),
212
- projects
235
+ projects,
236
+ ...Object.keys(history).length ? { history } : {}
213
237
  };
214
238
  }
239
+ function isObject(value) {
240
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
241
+ }
242
+ function normalizeHistory(scope) {
243
+ const history = {};
244
+ if (!isObject(scope)) return history;
245
+ for (const platform of PLATFORMS) {
246
+ const lists = scope[platform];
247
+ if (!isObject(lists)) continue;
248
+ const normalized = {};
249
+ for (const outcome of OUTCOMES) {
250
+ const samples = lists[outcome];
251
+ if (!Array.isArray(samples)) continue;
252
+ const kept = samples.filter(isObject).flatMap((sample) => {
253
+ const durationMs = wholeMs(sample.durationMs);
254
+ if (durationMs <= 0) return [];
255
+ return [{
256
+ at: timestamp(sample.at),
257
+ durationMs,
258
+ phases: wholePhases(sample.phases)
259
+ }];
260
+ });
261
+ if (kept.length) normalized[outcome] = kept.slice(-10);
262
+ }
263
+ if (Object.keys(normalized).length) history[platform] = normalized;
264
+ }
265
+ return history;
266
+ }
267
+ function wholePhases(phases) {
268
+ const out = {};
269
+ if (!isObject(phases)) return out;
270
+ for (const [name, ms] of Object.entries(phases)) {
271
+ const value = Number(ms);
272
+ if (Number.isFinite(value) && value >= 0) out[name] = Math.round(value);
273
+ }
274
+ return out;
275
+ }
215
276
  function normalizeScope(scope) {
216
277
  const normalized = {};
217
278
  if (!scope || typeof scope !== "object" || Array.isArray(scope)) return normalized;