stim 1.2.0 → 1.3.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 (59) hide show
  1. package/README.md +30 -0
  2. package/dist/android-BqVIJoG7.mjs +1368 -0
  3. package/dist/{android-Bcmev2s2.mjs → android-CqcW1hrP.mjs} +148 -56
  4. package/dist/{android-cas-D6HPZ6UQ.mjs → android-cas-BR5YJBYv.mjs} +5 -5
  5. package/dist/{app-install-NFxUahnH.mjs → app-install-DIp9riNx.mjs} +6 -6
  6. package/dist/{build-slots-D0b6sJyo.mjs → build-slots-C3-R4XQr.mjs} +2 -2
  7. package/dist/{cache-manifest-Cu4_Znw9.mjs → cache-manifest-CXR-Y8pH.mjs} +1 -1
  8. package/dist/cache-manifest.mjs +1 -1
  9. package/dist/cli.mjs +13 -13
  10. package/dist/collector-run.d.mts +3 -1
  11. package/dist/collector-run.mjs +33 -8
  12. package/dist/{config-CMtIaTk9.mjs → config-CcefMY2Q.mjs} +93 -12
  13. package/dist/{deps-BLBsTNDS.mjs → deps-Spz8ozgo.mjs} +1 -1
  14. package/dist/{dev-client-RpvXxCXV.mjs → dev-client-BjisHDB5.mjs} +186 -9
  15. package/dist/{device-CoCPmazS.mjs → device-BkiW-gkk.mjs} +19 -11
  16. package/dist/{device-lease-Dpik41RY.mjs → device-lease-BJYybF-2.mjs} +107 -83
  17. package/dist/{device-pool-B5pEVa3t.mjs → device-pool-CPbLGfrB.mjs} +18 -12
  18. package/dist/{device-remote-E81KAdXf.mjs → device-remote-AkYCKS8U.mjs} +7 -7
  19. package/dist/{doctor-CqPeMsSm.mjs → doctor-Bmv86UUW.mjs} +11 -12
  20. package/dist/{doctor-D_CX7ib5.mjs → doctor-fAMXC4vl.mjs} +4 -4
  21. package/dist/{error-diagnostics-Dw8Gn3yw.mjs → error-diagnostics-DHC31rXA.mjs} +62 -52
  22. package/dist/{gc-B7KkDinF.mjs → gc-GGu-8ULN.mjs} +66 -57
  23. package/dist/{guide-BS2Xc24o.mjs → guide-92CPA21s.mjs} +196 -13
  24. package/dist/{ios-B7KLgIp5.mjs → ios-BSXFeO5H.mjs} +183 -54
  25. package/dist/{ios-D1h1Di39.mjs → ios-Deh5js5V.mjs} +78 -40
  26. package/dist/{ios-device-xNt0Lg6v.mjs → ios-device-CWOM_S44.mjs} +1 -1
  27. package/dist/{logs-DcX9AvRF.mjs → logs-DPNF4C9s.mjs} +9 -6
  28. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-R-Cqc6Qv.mjs} +22 -5
  29. package/dist/{metro-COslRK_F.mjs → metro-CORqXyTL.mjs} +2 -2
  30. package/dist/{ndjson-DcAtEx_K.mjs → ndjson-BZwI1NBv.mjs} +5 -2
  31. package/dist/{ownership-CGPart6T.mjs → ownership-8O073GIC.mjs} +3 -3
  32. package/dist/{ownership-CYOyqxUo.mjs → ownership-BpCKiRlQ.mjs} +3 -3
  33. package/dist/{ownership-claim-7zRxxTfZ.mjs → ownership-claim-Bf2CAfw3.mjs} +4 -3
  34. package/dist/{project-BBTVABUz.mjs → project-fKmuIaMI.mjs} +2 -2
  35. package/dist/{reclaim-C5Jmuahx.mjs → reclaim-iW9Cad7k.mjs} +80 -74
  36. package/dist/{reload-Z8nKifAk.mjs → reload-DazHDSFA.mjs} +32 -24
  37. package/dist/{server-bare-DEk37Mz2.mjs → server-bare-DEzPZLvf.mjs} +2 -2
  38. package/dist/server-expo-44em5Pmg.mjs +2 -0
  39. package/dist/{server-expo-C3ktAwUG.mjs → server-expo-DizT4OMk.mjs} +4 -4
  40. package/dist/{settings-DwU1_U_q.mjs → settings-DEJf6_kJ.mjs} +2 -2
  41. package/dist/slot-launch-cci0xvmC.mjs +19 -0
  42. package/dist/{start-BrY4EuIg.mjs → start-mObjQH2W.mjs} +11 -11
  43. package/dist/{state-CZ-ER-8d.mjs → state-B3XA2wp2.mjs} +9 -9
  44. package/dist/{stats-BCWEVLrM.mjs → stats-BR-kwj0B.mjs} +1 -1
  45. package/dist/{stats-BsF15Bti.mjs → stats-CaaQr4yN.mjs} +3 -3
  46. package/dist/{status-DFI1Hfpg.mjs → status-BxQNqu9t.mjs} +55 -24
  47. package/dist/{stop-B7DR4awK.mjs → stop-CrHGcjrV.mjs} +2 -2
  48. package/dist/{stop-Btg770vV.mjs → stop-DimGXEKY.mjs} +93 -42
  49. package/dist/supervisor-run.d.mts +1 -2
  50. package/dist/supervisor-run.mjs +5 -5
  51. package/dist/{remote-cache-DDmJ1Roq.mjs → teardown-ClFp1Aax.mjs} +887 -888
  52. package/dist/{workspace-process-lock-BKfdys1q.mjs → workspace-process-lock-3NcQnZ0X.mjs} +1 -1
  53. package/dist/{worktree-BPTbI-LN.mjs → worktree-DxTgN1CJ.mjs} +12 -12
  54. package/dist/{worktree-C79xx_kM.mjs → worktree-Eq6TLH8N.mjs} +1 -1
  55. package/dist/{xcode-DeKCribl.mjs → xcode-D7sAp-Zy.mjs} +82 -56
  56. package/package.json +5 -5
  57. package/dist/android-K63_0UfF.mjs +0 -727
  58. package/dist/build-lock-BsVoLDaq.mjs +0 -645
  59. package/dist/server-expo-Drd7ksHu.mjs +0 -2
@@ -87,25 +87,22 @@ const BOOT_STATE_LIST_TIMEOUT_MS = 3e4;
87
87
  function iosSimulatorFailureAdvice(exec = getExecutor()) {
88
88
  return hostMemoryPressureAdvice(readHostMemoryPressure(exec)) ?? "The simulator may be unresponsive. Check Activity Monitor for memory pressure and free host memory before retrying.";
89
89
  }
90
- function bootstatusTimeout(udid) {
91
- const e = /* @__PURE__ */ new Error(`xcrun simctl bootstatus ${udid} -b timed out`);
90
+ function bootstatusTimeout(command) {
91
+ const e = /* @__PURE__ */ new Error(`${command} timed out`);
92
92
  e.code = "ETIMEDOUT";
93
93
  return e;
94
94
  }
95
- async function awaitBootstatus(udid, attemptMs) {
95
+ async function awaitBootCommand(args, attemptMs, onLine) {
96
+ const command = `xcrun ${args.join(" ")}`;
96
97
  const lines = [];
97
98
  const reader = createLineReader((raw) => {
98
99
  const line = stripAnsi(raw).trim();
99
100
  if (!line) return;
101
+ onLine(line);
100
102
  lines.push(line);
101
103
  if (lines.length > 10) lines.shift();
102
104
  });
103
- const child = getExecutor().spawn("xcrun", [
104
- "simctl",
105
- "bootstatus",
106
- udid,
107
- "-b"
108
- ], { stdio: [
105
+ const child = getExecutor().spawn("xcrun", args, { stdio: [
109
106
  "ignore",
110
107
  "pipe",
111
108
  "pipe"
@@ -132,48 +129,89 @@ async function awaitBootstatus(udid, attemptMs) {
132
129
  child.stdout?.destroy?.();
133
130
  child.stderr?.destroy?.();
134
131
  child.unref();
135
- throw new Error(`Timed out waiting for simctl bootstatus ${udid}; its process could not be confirmed stopped. Inspect pid ${child.pid ?? "unknown"} before retrying.`);
132
+ throw new Error(`Timed out waiting for ${command}; its process could not be confirmed stopped. Inspect pid ${child.pid ?? "unknown"} before retrying.`);
136
133
  }
137
- throw bootstatusTimeout(udid);
134
+ throw bootstatusTimeout(command);
138
135
  }
139
136
  if (result.error) throw result.error;
140
137
  if (result.code === 0) return;
141
138
  const detail = lines.length ? `: ${lines.join(" | ")}` : "";
142
- throw new Error(`xcrun simctl bootstatus ${udid} -b failed with exit code ${result.code ?? "unknown"}${detail}`);
139
+ throw new Error(`${command} failed with exit code ${result.code ?? "unknown"}${detail}`);
143
140
  }
144
- async function bootIosSim(udid, { timeoutMs = IOS_BOOT_TIMEOUT_MS, attemptMs = BOOTSTATUS_ATTEMPT_MS } = {}) {
141
+ async function bootIosSim(udid, { timeoutMs = IOS_BOOT_TIMEOUT_MS, attemptMs = BOOTSTATUS_ATTEMPT_MS, label = udid, out = () => {} } = {}) {
145
142
  const exec = getExecutor();
146
- const deadline = Date.now() + timeoutMs;
143
+ const started = Date.now();
144
+ const deadline = started + timeoutMs;
145
+ let worst = null;
146
+ let unknown = 0;
147
+ let sampledAt = started;
148
+ let longestGapMs = 0;
149
+ let phase = "requesting boot";
150
+ const ranks = {
151
+ normal: 0,
152
+ warning: 1,
153
+ critical: 2
154
+ };
155
+ const sample = () => {
156
+ const now = Date.now();
157
+ longestGapMs = Math.max(longestGapMs, now - sampledAt);
158
+ sampledAt = now;
159
+ const pressure = readHostMemoryPressure(exec);
160
+ if (pressure === null) unknown++;
161
+ else if (worst === null || ranks[pressure] > ranks[worst]) worst = pressure;
162
+ return pressure;
163
+ };
164
+ const recovery = "Stop unused slots in workspaces you own with `stim stop --slot <name>`, reduce concurrent builds, and retry after pressure falls. Ask before closing other agents' devices or apps. Repeated reboots under unchanged pressure may stall again.";
165
+ const coverage = () => longestGapMs > 3e4 ? ` Observations were delayed: longest gap ${Math.round(longestGapMs / 1e3)}s. Pressure during gaps is unobserved; blocking CLI work can also delay progress and timeout handling.` : "";
166
+ const report = () => {
167
+ const pressure = sample();
168
+ out(`Simulator ${label} is still booting after ${Math.round((Date.now() - started) / 1e3)}s. Last boot output: ${phase}. Memory pressure: ${pressure ?? "unknown"}; highest observed: ${worst ?? "unknown"}. Boot deadline: ${Math.round(timeoutMs / 1e3)}s.${coverage()}${pressure === "warning" || pressure === "critical" ? ` Boot may be delayed or stalled. ${recovery}` : ""}`);
169
+ };
170
+ const onLine = (line) => {
171
+ phase = line.slice(0, 240);
172
+ };
173
+ sample();
174
+ const monitor = setInterval(report, 15e3);
175
+ monitor.unref();
147
176
  try {
148
- exec.runFile("xcrun", [
149
- "simctl",
150
- "boot",
151
- udid
152
- ], {
153
- timeoutMs,
154
- killSignal: "SIGKILL"
155
- });
156
- } catch (e) {
157
- if (!String(e?.message || e).includes("Booted")) throw e;
158
- }
159
- for (;;) {
160
- const remaining = deadline - Date.now();
161
- if (remaining > 0) try {
162
- await awaitBootstatus(udid, Math.min(Math.max(remaining, BOOTSTATUS_ATTEMPT_FLOOR_MS), attemptMs));
163
- break;
177
+ try {
178
+ await awaitBootCommand([
179
+ "simctl",
180
+ "boot",
181
+ udid
182
+ ], Math.max(1, deadline - Date.now()), onLine);
183
+ phase = "waiting for boot completion";
164
184
  } catch (e) {
165
- if (e?.code !== "ETIMEDOUT") throw e;
185
+ if (!String(e?.message || e).includes("Booted")) throw e;
166
186
  }
167
- let state;
168
- try {
169
- state = listAllIosSims({ timeoutMs: BOOT_STATE_LIST_TIMEOUT_MS }).find((s) => s.udid === udid)?.state ?? null;
170
- } catch {
171
- state = void 0;
187
+ for (;;) {
188
+ const remaining = deadline - Date.now();
189
+ if (remaining > 0) try {
190
+ await awaitBootCommand([
191
+ "simctl",
192
+ "bootstatus",
193
+ udid,
194
+ "-b"
195
+ ], Math.min(Math.max(remaining, BOOTSTATUS_ATTEMPT_FLOOR_MS), attemptMs), onLine);
196
+ break;
197
+ } catch (e) {
198
+ if (e?.code !== "ETIMEDOUT") throw e;
199
+ }
200
+ let state;
201
+ try {
202
+ state = listAllIosSims({ timeoutMs: BOOT_STATE_LIST_TIMEOUT_MS }).find((s) => s.udid === udid)?.state ?? null;
203
+ } catch {
204
+ state = void 0;
205
+ }
206
+ const waited = Math.round((timeoutMs - Math.max(0, deadline - Date.now())) / 1e3);
207
+ if (state !== "Booting" && state !== "Booted" && state !== void 0) throw new Error(`Simulator ${udid} reports "${state ?? "missing"}" after ${waited}s of boot wait.`);
208
+ if (deadline - Date.now() <= 0) throw new Error(`Simulator ${udid} did not finish booting within ${Math.round(timeoutMs / 1e3)}s.`);
172
209
  }
173
- if (state === "Booted") break;
174
- const waited = Math.round((timeoutMs - Math.max(0, deadline - Date.now())) / 1e3);
175
- if (state !== "Booting" && state !== void 0) throw new Error(`Simulator ${udid} reports "${state ?? "missing"}" after ${waited}s of boot wait.`);
176
- if (deadline - Date.now() <= 0) throw new Error(`Simulator ${udid} did not finish booting within ${Math.round(timeoutMs / 1e3)}s.`);
210
+ } catch (error) {
211
+ sample();
212
+ throw new Error(`${error?.message || error} Last boot output: ${phase}. Highest observed memory pressure: ${worst ?? "unknown"}; unavailable samples: ${unknown}.${coverage()} ${recovery} These observations do not establish an OOM crash.`, { cause: error });
213
+ } finally {
214
+ clearInterval(monitor);
177
215
  }
178
216
  exec.runFileQuiet("open", ["-a", "Simulator"], {
179
217
  timeoutMs: 5e3,
@@ -1,5 +1,5 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { r as INSTALL_ERROR } from "./app-install-NFxUahnH.mjs";
2
+ import { r as INSTALL_ERROR } from "./app-install-DIp9riNx.mjs";
3
3
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
@@ -1,9 +1,10 @@
1
- import { n as SOURCES, t as LEVELS } from "./ndjson-DcAtEx_K.mjs";
2
- import { D as workspaceLogsDir, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
3
- import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
1
+ import { V as validateDeviceSlot } from "./config-CcefMY2Q.mjs";
2
+ import { n as SOURCES, t as LEVELS } from "./ndjson-BZwI1NBv.mjs";
3
+ import { D as workspaceLogsDir, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
4
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
4
5
  import { _ as launchErrorPreview } from "./command-output-8wHMEde0.mjs";
5
- import { i as captureWorkspaceCrashes, t as errorDiagnostics } from "./error-diagnostics-Dw8Gn3yw.mjs";
6
- import { a as followLogs, i as fileSizes, n as buildCriteria, o as parseSince, r as compileGrep, s as queryLogs } from "./logs-query-oqn47Ffx.mjs";
6
+ import { i as captureWorkspaceCrashes, t as errorDiagnostics } from "./error-diagnostics-DHC31rXA.mjs";
7
+ import { a as followLogs, i as fileSizes, n as buildCriteria, o as parseSince, r as compileGrep, s as queryLogs } from "./logs-query-R-Cqc6Qv.mjs";
7
8
  import chalk from "chalk";
8
9
  //#region src/commands/logs.ts
9
10
  const LEVEL_WIDTH = 5;
@@ -68,7 +69,7 @@ const LEVEL_COLOURS = {
68
69
  fatal: chalk.bgRed
69
70
  };
70
71
  function logsCommand(program) {
71
- program.command("logs").description("Query this workspace's merged NDJSON log timeline (bundler, client, device, build). Prints and exits; nothing matching is a successful, empty result. Use --follow to stream.").option("--source <s...>", "Only these sources: metro, client, device, build, or all").option("--level <l>", `Minimum level: ${LEVELS.join(", ")}`).option("--since <d>", "Only records newer than this, e.g. 30s, 5m, 2h").option("--grep <re>", "Only records whose message matches this regular expression").option("--tail <n>", "Only the last n matching records").option("--errors", "Errors and fatals since the last marker, from metro, client and build, plus confirmed native app-crash reports. Add --source device or --source all for general device errors.").option("--follow", "Keep streaming new records until interrupted").option("--json", "Emit the raw records, one per line (valid NDJSON; zero matches is zero bytes, exit 0)").action(async (opts) => {
72
+ program.command("logs").description("Query this workspace's merged NDJSON log timeline (bundler, client, device, build). Prints and exits; nothing matching is a successful, empty result. Use --follow to stream.").option("--slot <name>", "Only records attributed to this device slot", validateDeviceSlot).option("--source <s...>", "Only these sources: metro, client, device, build, or all").option("--level <l>", `Minimum level: ${LEVELS.join(", ")}`).option("--since <d>", "Only records newer than this, e.g. 30s, 5m, 2h").option("--grep <re>", "Only records whose message matches this regular expression").option("--tail <n>", "Only the last n matching records").option("--errors", "Errors and fatals since the last marker, from metro, client and build, plus confirmed native app-crash reports. Add --source device or --source all for general device errors.").option("--follow", "Keep streaming new records until interrupted").option("--json", "Emit the raw records, one per line (valid NDJSON; zero matches is zero bytes, exit 0)").action(async (opts) => {
72
73
  const root = findProjectRoot(process.cwd());
73
74
  if (!root) {
74
75
  console.error(chalk.red("Not in a React Native project (no package.json found)."));
@@ -102,6 +103,7 @@ function logsCommand(program) {
102
103
  if (compiled.error) fail(compiled.error);
103
104
  }
104
105
  const query = {
106
+ slot: opts.slot,
105
107
  dir,
106
108
  sources,
107
109
  minLevel,
@@ -148,6 +150,7 @@ function logsCommand(program) {
148
150
  return;
149
151
  }
150
152
  const criteria = buildCriteria({
153
+ slot: opts.slot,
151
154
  sources,
152
155
  minLevel,
153
156
  since: opts.since,
@@ -1,4 +1,4 @@
1
- import { a as parseNdjsonLine, i as levelRank, o as parseNdjsonText } from "./ndjson-DcAtEx_K.mjs";
1
+ import { a as parseNdjsonLine, i as levelRank, o as parseNdjsonText } from "./ndjson-BZwI1NBv.mjs";
2
2
  import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { StringDecoder } from "string_decoder";
@@ -47,6 +47,7 @@ function markerWindow(records) {
47
47
  }
48
48
  function recordMatches(record, criteria = {}) {
49
49
  if (!record) return false;
50
+ if (criteria.slot !== void 0 && (record.slot ?? "default") !== criteria.slot) return false;
50
51
  const { sources, minLevel, grep, sinceTs, errorsOnly, markerTs, bundleMarkerTs } = criteria;
51
52
  if (sources && sources.length > 0 && !sources.includes(record.src) && !(criteria.includeNativeCrashes && record.src === "device" && record.event === "native_crash")) return false;
52
53
  if (minLevel && levelRank(record.level) < levelRank(minLevel)) return false;
@@ -71,8 +72,9 @@ function recordMatches(record, criteria = {}) {
71
72
  }
72
73
  return true;
73
74
  }
74
- function buildCriteria({ sources, minLevel, since, grep, errorsOnly, markerTs, bundleMarkerTs, now } = {}) {
75
+ function buildCriteria({ slot, sources, minLevel, since, grep, errorsOnly, markerTs, bundleMarkerTs, now } = {}) {
75
76
  const criteria = {
77
+ ...slot === void 0 ? {} : { slot },
76
78
  errorsOnly: Boolean(errorsOnly),
77
79
  includeNativeCrashes: Boolean(errorsOnly && !sources?.length)
78
80
  };
@@ -160,14 +162,25 @@ function includeBareErrorContext(all, matched, launchTs, sinceTs) {
160
162
  }
161
163
  return sortByTs([...matched, ...renderedContext]);
162
164
  }
163
- function queryLogs({ dir, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
164
- const all = readLogRecords(dir);
165
+ function launchMarkersBySlot(records) {
166
+ const markers = /* @__PURE__ */ new Map();
167
+ for (const record of records) {
168
+ if (record.marker !== true || record.src === "metro") continue;
169
+ const ts = tsOf(record);
170
+ const slot = record.slot ?? "default";
171
+ if (ts !== null && ts > (markers.get(slot) ?? -Infinity)) markers.set(slot, ts);
172
+ }
173
+ return markers;
174
+ }
175
+ function queryLogs({ slot, dir, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
176
+ const all = readLogRecords(dir).filter((record) => slot === void 0 || (record.slot ?? "default") === slot);
165
177
  if (all.length === 0) return [];
166
178
  const { launchTs, bundleTs } = errorsOnly ? markerWindow(all) : {
167
179
  launchTs: null,
168
180
  bundleTs: null
169
181
  };
170
182
  const criteria = buildCriteria({
183
+ slot,
171
184
  sources,
172
185
  minLevel,
173
186
  since,
@@ -177,7 +190,11 @@ function queryLogs({ dir, sources, minLevel, since, grep, tail, errorsOnly, erro
177
190
  markerTs: launchTs === null ? void 0 : launchTs,
178
191
  bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
179
192
  });
180
- let matched = all.filter((r) => recordMatches(r, criteria));
193
+ const slotMarkers = launchMarkersBySlot(all);
194
+ let matched = all.filter((record) => recordMatches(record, {
195
+ ...criteria,
196
+ markerTs: record.src === "metro" || record.src === "client" ? criteria.markerTs : slotMarkers.get(record.slot ?? "default")
197
+ }));
181
198
  if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
182
199
  return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs, criteria.sinceTs) : matched;
183
200
  }
@@ -1,6 +1,6 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { E as inspectProcessIdentity, M as isOnMountedVolume, N as listMountedVolumes, g as loadConfig, n as allMetroPorts, r as claimMetroPort, v as removeProject } from "./config-CMtIaTk9.mjs";
3
- import { p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
2
+ import { E as inspectProcessIdentity, M as isOnMountedVolume, N as listMountedVolumes, g as loadConfig, n as allMetroPorts, r as claimMetroPort, v as removeProject } from "./config-CcefMY2Q.mjs";
3
+ import { p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
4
4
  import { existsSync, readlinkSync, realpathSync } from "fs";
5
5
  import { sep } from "path";
6
6
  import { request } from "http";
@@ -49,7 +49,7 @@ function formatNdjsonLine(record) {
49
49
  return null;
50
50
  }
51
51
  }
52
- function createNdjsonWriter(file, { truncate = false } = {}) {
52
+ function createNdjsonWriter(file, { truncate = false, fields = {} } = {}) {
53
53
  let fd = null;
54
54
  let freshFile = truncate;
55
55
  let written = 0;
@@ -66,7 +66,10 @@ function createNdjsonWriter(file, { truncate = false } = {}) {
66
66
  dropped += 1;
67
67
  return false;
68
68
  }
69
- const line = formatNdjsonLine(stamp(record));
69
+ const line = formatNdjsonLine({
70
+ ...stamp(record),
71
+ ...fields
72
+ });
70
73
  if (line === null) {
71
74
  dropped += 1;
72
75
  lastError = /* @__PURE__ */ new TypeError("record could not be serialized to JSON");
@@ -1,6 +1,6 @@
1
- import { D as sameProcessRecord, E as inspectProcessIdentity } from "./config-CMtIaTk9.mjs";
2
- import { _ as writeWorkspaceState, m as withWorkspaceStateLock, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
3
- import { r as pidExists } from "./metro-COslRK_F.mjs";
1
+ import { D as sameProcessRecord, E as inspectProcessIdentity } from "./config-CcefMY2Q.mjs";
2
+ import { _ as writeWorkspaceState, m as withWorkspaceStateLock, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
3
+ import { r as pidExists } from "./metro-CORqXyTL.mjs";
4
4
  import { realpathSync } from "node:fs";
5
5
  //#region src/collector/state.ts
6
6
  function collectorRoot(root) {
@@ -1,8 +1,8 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { E as inspectProcessIdentity, T as captureProcessToken, u as getConfigDir } from "./config-CMtIaTk9.mjs";
2
+ import { E as inspectProcessIdentity, T as captureProcessToken, u as getConfigDir } from "./config-CcefMY2Q.mjs";
3
3
  import { t as createLineReader } from "./process-output-C7IlIzd7.mjs";
4
- import { r as pidExists } from "./metro-COslRK_F.mjs";
5
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BKfdys1q.mjs";
4
+ import { r as pidExists } from "./metro-CORqXyTL.mjs";
5
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-3NcQnZ0X.mjs";
6
6
  import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync } from "node:fs";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { basename, join, resolve, sep } from "node:path";
@@ -1,4 +1,4 @@
1
- import { E as inspectProcessIdentity, w as captureProcessIdentity } from "./config-CMtIaTk9.mjs";
1
+ import { E as inspectProcessIdentity, w as captureProcessIdentity } from "./config-CcefMY2Q.mjs";
2
2
  import { d as quotedPath } from "./command-output-8wHMEde0.mjs";
3
3
  import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
@@ -60,15 +60,16 @@ function isClaimUnavailable(err) {
60
60
  * competing run no protection at all.
61
61
  */
62
62
  function claimFailure(err, retryCommand) {
63
+ const retry = retryCommand === null ? "retry the same Stim command" : `run \`${retryCommand}\` again`;
63
64
  if (isClaimRefusal(err)) return {
64
65
  code: CLAIM_REFUSED,
65
66
  message: err.message,
66
- remedy: `Run \`${err.removeCommand}\`, then run \`${retryCommand}\` again.`
67
+ remedy: `Run \`${err.removeCommand}\`, then ${retry}.`
67
68
  };
68
69
  if (isClaimUnavailable(err)) return {
69
70
  code: CLAIM_UNAVAILABLE,
70
71
  message: err.message,
71
- remedy: `Reinstall Stim so the unique-pid native module for this platform is present, then run \`${retryCommand}\` again.`
72
+ remedy: `Reinstall Stim so the unique-pid native module for this platform is present, then ${retry}.`
72
73
  };
73
74
  return null;
74
75
  }
@@ -1,5 +1,5 @@
1
- import { c as findEnclosingWorktreeRoot, f as getProject } from "./config-CMtIaTk9.mjs";
2
- import { g as repoRoot } from "./worktree-C79xx_kM.mjs";
1
+ import { c as findEnclosingWorktreeRoot, f as getProject } from "./config-CcefMY2Q.mjs";
2
+ import { g as repoRoot } from "./worktree-Eq6TLH8N.mjs";
3
3
  import { existsSync, readFileSync, readdirSync, realpathSync } from "fs";
4
4
  import { basename, dirname, join, resolve } from "path";
5
5
  import { createRequire } from "module";
@@ -1,11 +1,11 @@
1
- import { D as sameProcessRecord, O as waitForProcessExit, f as getProject, v as removeProject } from "./config-CMtIaTk9.mjs";
2
- import { T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
3
- import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-COslRK_F.mjs";
4
- import { n as verifyCollectorOwnership } from "./ownership-CGPart6T.mjs";
5
- import { d as resolveEasCliBin, h as teardownOwnedIosSim, m as teardownOwnedAvd, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.mjs";
6
- import { l as releaseWorkspaceLeases } from "./device-lease-Dpik41RY.mjs";
7
- import { i as endRecordedSession } from "./device-remote-E81KAdXf.mjs";
8
- import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-CYOyqxUo.mjs";
1
+ import { D as sameProcessRecord, O as waitForProcessExit, f as getProject, v as removeProject, z as projectDeviceSlots } from "./config-CcefMY2Q.mjs";
2
+ import { T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
3
+ import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-CORqXyTL.mjs";
4
+ import { n as verifyCollectorOwnership } from "./ownership-8O073GIC.mjs";
5
+ import { c as parkedMaxSetting, n as teardownOwnedIosSim, t as teardownOwnedAvd, x as resolveEasCliBin } from "./teardown-ClFp1Aax.mjs";
6
+ import { l as releaseWorkspaceLeases } from "./device-lease-BJYybF-2.mjs";
7
+ import { i as endRecordedSession } from "./device-remote-AkYCKS8U.mjs";
8
+ import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
9
9
  import { existsSync, rmSync } from "node:fs";
10
10
  //#region src/reclaim.ts
11
11
  async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
@@ -25,7 +25,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
25
25
  if (ownership.status === "unverified") {
26
26
  const name = `${platform} log collector (pid ${pid})`;
27
27
  const entry = {
28
- platform: platform === "android" ? "android" : "ios",
28
+ platform: platform.split(":")[0] === "android" ? "android" : "ios",
29
29
  name,
30
30
  reason: `${ownership.reason}; keeping the record without signalling the process`
31
31
  };
@@ -40,7 +40,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
40
40
  if (error.code === "ESRCH") continue;
41
41
  }
42
42
  const entry = {
43
- platform: platform === "android" ? "android" : "ios",
43
+ platform: platform.split(":")[0] === "android" ? "android" : "ios",
44
44
  name: `${platform} log collector (pid ${pid})`,
45
45
  reason: "Collector exit could not be confirmed; keeping its ownership record."
46
46
  };
@@ -139,11 +139,13 @@ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunn
139
139
  }
140
140
  function describeDereferenced(project) {
141
141
  const devices = [];
142
- const ios = project?.platforms?.ios;
143
- if (ios?.deviceUdid) devices.push(`ios sim ${ios.deviceUdid}`);
144
- const android = project?.platforms?.android;
145
- if (android?.avdName) devices.push(`android avd ${android.avdName}`);
146
- else if (android?.serial) devices.push(`android device ${android.serial}`);
142
+ for (const { platforms } of projectDeviceSlots(project)) {
143
+ const ios = platforms.ios;
144
+ if (ios?.deviceUdid) devices.push(`ios sim ${ios.deviceUdid}`);
145
+ const android = platforms.android;
146
+ if (android?.avdName) devices.push(`android avd ${android.avdName}`);
147
+ else if (android?.serial) devices.push(`android device ${android.serial}`);
148
+ }
147
149
  return devices;
148
150
  }
149
151
  function parkedIosCacheKey(lastBuild) {
@@ -151,7 +153,7 @@ function parkedIosCacheKey(lastBuild) {
151
153
  const build = lastBuild;
152
154
  return build.platform === "ios" && typeof build.cacheKey === "string" ? build.cacheKey : null;
153
155
  }
154
- function parkRequest(project, projectPath) {
156
+ function parkRequest(project, projectPath, slot, simslimManaged) {
155
157
  const { max, error } = parkedMaxSetting("ios");
156
158
  if (error || max <= 0) return void 0;
157
159
  const cacheKey = parkedIosCacheKey(readWorkspaceState(projectPath)?.lastBuild);
@@ -160,7 +162,8 @@ function parkRequest(project, projectPath) {
160
162
  max,
161
163
  bundleId: typeof project?.bundleId === "string" ? project.bundleId : null,
162
164
  cacheKey,
163
- simslimManaged: Boolean(project?.platforms?.ios?.simslimManaged)
165
+ simslimManaged,
166
+ slot
164
167
  };
165
168
  }
166
169
  function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
@@ -170,69 +173,72 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
170
173
  const poolNotes = [];
171
174
  const skippedDevices = [];
172
175
  const failedDevices = [];
173
- const ios = project?.platforms?.ios;
174
- if (ios?.owned && ios.deviceUdid) {
175
- const udid = ios.deviceUdid;
176
- const label = ios.deviceName || udid;
177
- const r = teardownOwnedIosSim(udid, {
178
- del: true,
179
- label,
180
- ...park ? { park: parkRequest(project, projectPath) } : {}
181
- });
182
- if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
183
- for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
184
- if (r.parked) {
185
- parkedDevices.push(r.parked);
186
- evictedDevices.push(...r.evicted ?? []);
187
- } else if (r.status === "torn-down") deletedDevices.push(r.label);
188
- if (r.status === "skipped") skippedDevices.push({
189
- platform: "ios",
190
- name: label,
191
- udid,
192
- reason: `${r.reason} -- not touched`
193
- });
194
- else if (r.status === "failed") {
195
- const entry = {
176
+ for (const { slot, platforms } of projectDeviceSlots(project)) {
177
+ const ios = platforms.ios;
178
+ if (ios?.owned && ios.deviceUdid) {
179
+ const udid = ios.deviceUdid;
180
+ const label = ios.deviceName || udid;
181
+ const r = teardownOwnedIosSim(udid, {
182
+ del: true,
183
+ label,
184
+ ...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
185
+ });
186
+ if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
187
+ for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
188
+ if (r.parked) {
189
+ parkedDevices.push(r.parked);
190
+ evictedDevices.push(...r.evicted ?? []);
191
+ } else if (r.status === "torn-down") deletedDevices.push(r.label);
192
+ if (r.status === "skipped") skippedDevices.push({
196
193
  platform: "ios",
197
194
  name: label,
198
195
  udid,
199
- reason: `teardown failed: ${r.reason}`
200
- };
201
- skippedDevices.push(entry);
202
- failedDevices.push(entry);
196
+ reason: `${r.reason} -- not touched`
197
+ });
198
+ else if (r.status === "failed") {
199
+ const entry = {
200
+ platform: "ios",
201
+ name: label,
202
+ udid,
203
+ reason: `teardown failed: ${r.reason}`
204
+ };
205
+ skippedDevices.push(entry);
206
+ failedDevices.push(entry);
207
+ }
203
208
  }
204
- }
205
- const android = project?.platforms?.android;
206
- if (android?.owned && android.avdName) {
207
- const bound = parkedMaxSetting("android");
208
- const r = teardownOwnedAvd(android.avdName, {
209
- del: true,
210
- owner: { projectPath },
211
- ...park && !bound.error && bound.max > 0 ? { park: {
212
- projectPath,
213
- max: bound.max,
214
- configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
215
- } } : {}
216
- });
217
- if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
218
- for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
219
- if (r.parked) {
220
- parkedDevices.push(r.parked);
221
- evictedDevices.push(...r.evicted ?? []);
222
- } else if (r.status === "torn-down") deletedDevices.push(android.avdName);
223
- else if (r.status === "skipped") skippedDevices.push({
224
- platform: "android",
225
- name: android.avdName,
226
- reason: `${r.reason} -- not touched`
227
- });
228
- else if (r.status === "failed") {
229
- const entry = {
209
+ const android = platforms.android;
210
+ if (android?.owned && android.avdName) {
211
+ const bound = parkedMaxSetting("android");
212
+ const r = teardownOwnedAvd(android.avdName, {
213
+ del: true,
214
+ owner: { projectPath },
215
+ ...park && !bound.error && bound.max > 0 ? { park: {
216
+ projectPath,
217
+ slot,
218
+ max: bound.max,
219
+ configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
220
+ } } : {}
221
+ });
222
+ if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
223
+ for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
224
+ if (r.parked) {
225
+ parkedDevices.push(r.parked);
226
+ evictedDevices.push(...r.evicted ?? []);
227
+ } else if (r.status === "torn-down") deletedDevices.push(android.avdName);
228
+ else if (r.status === "skipped") skippedDevices.push({
230
229
  platform: "android",
231
230
  name: android.avdName,
232
- reason: `teardown failed: ${r.reason}`
233
- };
234
- skippedDevices.push(entry);
235
- failedDevices.push(entry);
231
+ reason: `${r.reason} -- not touched`
232
+ });
233
+ else if (r.status === "failed") {
234
+ const entry = {
235
+ platform: "android",
236
+ name: android.avdName,
237
+ reason: `teardown failed: ${r.reason}`
238
+ };
239
+ skippedDevices.push(entry);
240
+ failedDevices.push(entry);
241
+ }
236
242
  }
237
243
  }
238
244
  return {