stim 1.0.0 → 1.2.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 (66) hide show
  1. package/README.md +29 -10
  2. package/dist/android-BW8i_YxU.mjs +204 -0
  3. package/dist/android-Bcmev2s2.mjs +2334 -0
  4. package/dist/android-K63_0UfF.mjs +727 -0
  5. package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-NFxUahnH.mjs +1149 -0
  8. package/dist/build-lock-BsVoLDaq.mjs +645 -0
  9. package/dist/build-slots-D0b6sJyo.mjs +152 -0
  10. package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +24 -27166
  13. package/dist/collector-run.d.mts +1 -0
  14. package/dist/collector-run.mjs +7 -5
  15. package/dist/command-output-8wHMEde0.mjs +289 -0
  16. package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
  17. package/dist/deps-BLBsTNDS.mjs +435 -0
  18. package/dist/dev-client-RpvXxCXV.mjs +620 -0
  19. package/dist/device-CoCPmazS.mjs +250 -0
  20. package/dist/device-lease-Dpik41RY.mjs +335 -0
  21. package/dist/device-pool-B5pEVa3t.mjs +408 -0
  22. package/dist/device-remote-E81KAdXf.mjs +1209 -0
  23. package/dist/doctor-CqPeMsSm.mjs +1130 -0
  24. package/dist/doctor-D_CX7ib5.mjs +530 -0
  25. package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
  26. package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
  27. package/dist/gc-B7KkDinF.mjs +1665 -0
  28. package/dist/guide-BS2Xc24o.mjs +4503 -0
  29. package/dist/ios-B7KLgIp5.mjs +2733 -0
  30. package/dist/ios-D1h1Di39.mjs +451 -0
  31. package/dist/ios-device-D3pzTKQE.mjs +81 -0
  32. package/dist/ios-device-xNt0Lg6v.mjs +413 -0
  33. package/dist/logs-DcX9AvRF.mjs +176 -0
  34. package/dist/logs-query-oqn47Ffx.mjs +273 -0
  35. package/dist/metro-COslRK_F.mjs +184 -0
  36. package/dist/metro-store-CDDZHtPl.mjs +92 -0
  37. package/dist/ownership-CGPart6T.mjs +60 -0
  38. package/dist/ownership-CYOyqxUo.mjs +475 -0
  39. package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
  40. package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
  41. package/dist/reclaim-C5Jmuahx.mjs +348 -0
  42. package/dist/reload-Z8nKifAk.mjs +332 -0
  43. package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
  44. package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
  45. package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
  46. package/dist/server-expo-Drd7ksHu.mjs +2 -0
  47. package/dist/settings-DwU1_U_q.mjs +708 -0
  48. package/dist/spawn-entry-nCE6m770.mjs +14 -0
  49. package/dist/start-BrY4EuIg.mjs +822 -0
  50. package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
  51. package/dist/stats-BCWEVLrM.mjs +307 -0
  52. package/dist/stats-BsF15Bti.mjs +62 -0
  53. package/dist/status-DFI1Hfpg.mjs +393 -0
  54. package/dist/stop-B7DR4awK.mjs +3 -0
  55. package/dist/stop-Btg770vV.mjs +591 -0
  56. package/dist/supervisor-run.d.mts +0 -1
  57. package/dist/supervisor-run.mjs +4 -4
  58. package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
  59. package/dist/worktree-BPTbI-LN.mjs +1322 -0
  60. package/dist/worktree-C79xx_kM.mjs +543 -0
  61. package/dist/xcode-DeKCribl.mjs +1832 -0
  62. package/package.json +5 -5
  63. package/shim/bundle-response.cjs +5 -0
  64. package/dist/android-jc8MnUyL.mjs +0 -514
  65. package/dist/metro-store-COLl1pOk.mjs +0 -1249
  66. package/dist/server-expo-BZJm--wA.mjs +0 -2
@@ -0,0 +1,1832 @@
1
+ import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
+ import { C as withConfigLock, E as inspectProcessIdentity, _ as releaseAndroidConsolePort, b as setDevice, g as loadConfig, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, w as captureProcessIdentity, y as saveConfig } from "./config-CMtIaTk9.mjs";
3
+ import { r as register } from "./cache-manifest-Cu4_Znw9.mjs";
4
+ import { E as workspaceId, S as sharedCompilationCache, w as workspaceDerivedData } from "./state-CZ-ER-8d.mjs";
5
+ import { s as ownedDeviceLabel } from "./project-BBTVABUz.mjs";
6
+ import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
7
+ import { r as pidExists } from "./metro-COslRK_F.mjs";
8
+ import { C as selectParked, S as readParked, _ as teardownParkedIosSim, b as dropParked, g as teardownParkedAvd, m as teardownOwnedAvd, x as parkedMaxSetting, y as adoptParked } from "./remote-cache-DDmJ1Roq.mjs";
9
+ import { U as resolveOptimizations, l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-DwU1_U_q.mjs";
10
+ import { a as formatElapsed, l as phaseLine } from "./command-output-8wHMEde0.mjs";
11
+ import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, _ as renameIosSim, b as resolveIosCreation, c as iosRuntimeMatches, d as listIosDeviceTypes, g as parseRuntimeVersion, i as createOwnedIosSim, l as iosSimulatorFailureAdvice, m as ownedSimName, n as bootIosSim, u as listAllIosSims, v as resetIosKeychain, x as resolveOwnedIosSim, y as resetIosPrivacy } from "./ios-D1h1Di39.mjs";
12
+ import { f as releaseClaim, h as tryAcquireClaim, i as claimRemoveCommand, l as markClaimChildPending, n as ClaimRefusedError, p as setClaimChild, u as processGroupAlive } from "./ownership-claim-7zRxxTfZ.mjs";
13
+ import { A as pickDefaultSystemImage, D as ownedAvdName, E as ownedAvdMatchesConfiguration, L as waitForBoot, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, b as listAvds, c as avdPoolConfiguration, d as configureNewOwnedAvd, f as createOwnedAvd, o as assertOwnedAvdStopped, u as bootAndroidEmulator, v as getAvdNameForSerial, w as nextConsolePort, x as listInstalledSystemImages, y as listAdbDevices } from "./android-K63_0UfF.mjs";
14
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BKfdys1q.mjs";
15
+ import { t as cleanLine } from "./server-expo-C3ktAwUG.mjs";
16
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
17
+ import { createHash, randomUUID } from "node:crypto";
18
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
19
+ import chalk from "chalk";
20
+ //#region src/engine/simslim.ts
21
+ async function reconcileSimSlim({ udid, profile, previouslyManaged = false, out = () => {}, spawn, timeoutMs = 72e4, cleanupMs = 1e4 }) {
22
+ if (!profile && !previouslyManaged) return {
23
+ managed: false,
24
+ profile: null
25
+ };
26
+ const action = profile ? "on" : "off";
27
+ const args = profile ? [
28
+ "on",
29
+ udid,
30
+ "--profile",
31
+ profile
32
+ ] : ["off", udid];
33
+ const lines = [];
34
+ const onLine = (raw) => {
35
+ const line = stripAnsi(raw).trim();
36
+ if (!line) return;
37
+ lines.push(line);
38
+ if (lines.length > 20) lines.shift();
39
+ out(`SimSlim: ${line}`);
40
+ };
41
+ const stdout = createLineReader(onLine);
42
+ const stderr = createLineReader(onLine);
43
+ const root = join(getConfigDir(), "simslim-locks", `${encodeURIComponent(udid.toLowerCase())}.lock`);
44
+ const label = `SimSlim for ${udid}`;
45
+ const attempt = tryAcquireClaim({
46
+ root,
47
+ mode: "exclusive",
48
+ label
49
+ });
50
+ if (attempt.pending) releaseClaim(attempt.pending);
51
+ const claim = attempt.acquired;
52
+ if (!claim) throw new ClaimRefusedError({
53
+ root,
54
+ claimPath: attempt.held?.path ?? root,
55
+ label,
56
+ reason: "another SimSlim operation is still using this simulator"
57
+ });
58
+ let child;
59
+ let result;
60
+ let identity;
61
+ let cancellation;
62
+ const stopGroup = () => {
63
+ if (child?.pid !== void 0 && identity && inspectProcessIdentity(identity) === "same") try {
64
+ process.kill(-child.pid, "SIGKILL");
65
+ } catch {}
66
+ };
67
+ const cancel = (signal) => {
68
+ cancellation ??= signal;
69
+ stopGroup();
70
+ };
71
+ const onInterrupt = () => cancel("SIGINT");
72
+ const onTerminate = () => cancel("SIGTERM");
73
+ const groupAlive = () => child?.pid !== void 0 && processGroupAlive(child.pid);
74
+ const settled = () => result !== void 0 && !groupAlive();
75
+ const waitUntil = async (deadline, cancellable = false) => {
76
+ while (!settled() && Date.now() < deadline) {
77
+ if (cancellable && cancellation) break;
78
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
79
+ }
80
+ return settled();
81
+ };
82
+ process.on("SIGINT", onInterrupt);
83
+ process.on("SIGTERM", onTerminate);
84
+ process.on("exit", stopGroup);
85
+ try {
86
+ markClaimChildPending(claim);
87
+ try {
88
+ child = (spawn ?? ((cmd, childArgs, opts) => getExecutor().spawn(cmd, childArgs, opts)))("simslim", args, {
89
+ stdio: [
90
+ "ignore",
91
+ "pipe",
92
+ "pipe"
93
+ ],
94
+ detached: true
95
+ });
96
+ } catch (error) {
97
+ throw simslimLaunchError(error);
98
+ }
99
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
100
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
101
+ waitForChild(child).then((value) => {
102
+ result = value;
103
+ });
104
+ if (child.pid !== void 0) {
105
+ const captured = captureProcessIdentity(child.pid);
106
+ if (captured.ok) {
107
+ identity = {
108
+ pid: child.pid,
109
+ processToken: captured.token
110
+ };
111
+ try {
112
+ setClaimChild(claim, identity);
113
+ } catch {}
114
+ }
115
+ }
116
+ const completed = await waitUntil(Date.now() + timeoutMs, true);
117
+ if (cancellation || !completed) {
118
+ stopGroup();
119
+ const stopped = await waitUntil(Date.now() + cleanupMs);
120
+ const detail = lines.length ? ` ${lines.join(" | ")}` : "";
121
+ 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: ${claimRemoveCommand(claim.path)}`;
122
+ const message = `SimSlim ${action} ${cancellation ? `interrupted by ${cancellation}` : `exceeded ${Math.round(timeoutMs / 1e3)}s`}. ${recovery}${detail}`;
123
+ if (cancellation) out(message);
124
+ throw Object.assign(new Error(message), { code: cancellation ? "EINTR" : "ETIMEDOUT" });
125
+ }
126
+ } finally {
127
+ if (!child || settled()) releaseClaim(claim);
128
+ else {
129
+ child.stdout?.destroy?.();
130
+ child.stderr?.destroy?.();
131
+ child.unref();
132
+ }
133
+ process.off("SIGINT", onInterrupt);
134
+ process.off("SIGTERM", onTerminate);
135
+ process.off("exit", stopGroup);
136
+ if (cancellation) process.exit(cancellation === "SIGINT" ? 130 : 143);
137
+ }
138
+ stdout.flush();
139
+ stderr.flush();
140
+ if (result?.error) throw simslimLaunchError(result.error);
141
+ if (result?.code !== 0) {
142
+ const detail = lines.length ? ` ${lines.join(" | ")}` : "";
143
+ throw new Error(`SimSlim ${action} failed with exit code ${result?.code ?? "unknown"}.${detail}`);
144
+ }
145
+ return {
146
+ managed: Boolean(profile),
147
+ profile: profile ?? null
148
+ };
149
+ }
150
+ function simslimLaunchError(error) {
151
+ const cause = error;
152
+ 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 });
153
+ return new Error(`Could not start SimSlim: ${String(cause?.message || cause)}`, { cause });
154
+ }
155
+ function simslimIsOnPath() {
156
+ try {
157
+ return Boolean(getExecutor().runQuiet("command -v simslim", { timeoutMs: 5e3 }));
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ //#endregion
163
+ //#region src/engine/device.ts
164
+ function startIosBoot(udid, configure) {
165
+ const done = (async () => {
166
+ await bootIosSim(udid);
167
+ await configure();
168
+ })();
169
+ done.catch(() => {});
170
+ return {
171
+ udid,
172
+ done
173
+ };
174
+ }
175
+ async function ensureOwnedDevice({ platform, project, projectPath, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
176
+ const record = project?.platforms?.[platform] ?? null;
177
+ if (platform === "ios") return ensureOwnedIosDevice({
178
+ record,
179
+ projectPath,
180
+ settingsRoot,
181
+ label,
182
+ settings,
183
+ flags,
184
+ note,
185
+ out,
186
+ reconcileIosSimulator
187
+ });
188
+ return ensureOwnedAndroidDevice({
189
+ record,
190
+ projectPath,
191
+ settingsRoot,
192
+ label,
193
+ settings,
194
+ flags,
195
+ note,
196
+ out,
197
+ logFile,
198
+ alive,
199
+ configureAvd,
200
+ teardownAvd
201
+ });
202
+ }
203
+ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
204
+ const memoryAdvice = hostMemoryPressureAdvice(readHostMemoryPressure());
205
+ if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
206
+ const simslimProfile = iosSimSlimProfileSetting(settings, settingsRoot);
207
+ if (record?.deviceUdid) {
208
+ if (record.owned) {
209
+ const resolved = resolveOwnedIosSim(record.deviceUdid);
210
+ if (resolved.notOwned) note(chalk.yellow(`Note: recorded sim is now named "${resolved.notOwned}", not Stim-owned by name -- creating a fresh owned sim instead of booting it.`));
211
+ else if (resolved.missing) {} else {
212
+ const sim = resolved.sim;
213
+ const wantedType = flags.deviceType || settings?.ios?.deviceType;
214
+ const deviceTypes = listIosDeviceTypes();
215
+ const mismatch = deviceTypeMismatch(sim.deviceTypeIdentifier, wantedType, deviceTypes);
216
+ if (mismatch) throw new Error(`${mismatch}. Stim will not silently boot a different model. Run \`stim worktree remove\` (or \`stim gc --delete\`) to reap the current sim, then \`stim ios\` again to create the requested one.`);
217
+ const model = {
218
+ model: deviceTypes.find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null,
219
+ runtime: parseRuntimeVersion(sim.runtime)
220
+ };
221
+ const name = await withIosDeviceNameLock(() => renameToOwnedName(sim, label, model, projectPath));
222
+ const updated = {
223
+ deviceUdid: sim.udid,
224
+ owned: true,
225
+ deviceName: name,
226
+ ...record.simslimManaged ? { simslimManaged: true } : {},
227
+ ...record.adopted ? { adopted: true } : {},
228
+ ...record.adoptionPending ? { adoptionPending: true } : {},
229
+ ...record.parkedCacheKey ? { parkedCacheKey: record.parkedCacheKey } : {}
230
+ };
231
+ const configure = async () => {
232
+ if (record.adoptionPending) resetAdoptedSim(sim.udid, out);
233
+ return configureOwnedIosSim({
234
+ record: updated,
235
+ projectPath,
236
+ profile: simslimProfile,
237
+ out,
238
+ reconcileIosSimulator
239
+ });
240
+ };
241
+ const facts = {
242
+ deviceType: model.model,
243
+ runtime: model.runtime
244
+ };
245
+ if (sim.state !== "Booted") {
246
+ out(chalk.dim(phaseLine("device", `booting ${name} (${sim.udid})`)));
247
+ return {
248
+ ...updated,
249
+ booting: startIosBoot(sim.udid, configure),
250
+ ...facts
251
+ };
252
+ }
253
+ return {
254
+ ...await configure(),
255
+ ...facts
256
+ };
257
+ }
258
+ } else {
259
+ const sim = listAllIosSims().find((s) => s.udid === record.deviceUdid);
260
+ if (sim) {
261
+ if (sim.state !== "Booted") {
262
+ note(chalk.yellow(`Note: assigned sim ${sim.name} (${sim.udid}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
263
+ note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned sim."));
264
+ }
265
+ return record;
266
+ }
267
+ }
268
+ }
269
+ const choice = resolveIosCreation({
270
+ deviceType: flags.deviceType || settings.ios?.deviceType,
271
+ runtime: flags.runtime || settings.ios?.runtime
272
+ });
273
+ const adopted = parkedMaxSetting("ios").max > 0 ? await withIosDeviceNameLock(() => takeParkedIosSim({
274
+ projectPath,
275
+ label,
276
+ choice,
277
+ out
278
+ })) : null;
279
+ if (adopted) {
280
+ out(chalk.dim(phaseLine("device", `booting ${adopted.deviceName} (${adopted.deviceUdid})`)));
281
+ const booting = startIosBoot(adopted.deviceUdid, async () => {
282
+ resetAdoptedSim(adopted.deviceUdid, out);
283
+ await configureOwnedIosSim({
284
+ record: adopted,
285
+ projectPath,
286
+ profile: simslimProfile,
287
+ out,
288
+ reconcileIosSimulator
289
+ });
290
+ });
291
+ return {
292
+ ...adopted,
293
+ booting,
294
+ deviceType: choice.deviceType,
295
+ runtime: choice.runtime
296
+ };
297
+ }
298
+ const created = await withIosDeviceNameLock(() => {
299
+ const suffix = ownedIosNameSuffix(label, {
300
+ model: choice.deviceType,
301
+ runtime: choice.runtime
302
+ }, projectPath);
303
+ const result = createOwnedIosSim(label, { suffix }, choice);
304
+ setDevice(projectPath, "ios", {
305
+ deviceUdid: result.udid,
306
+ owned: true,
307
+ deviceName: result.name
308
+ });
309
+ return result;
310
+ });
311
+ const newRecord = {
312
+ deviceUdid: created.udid,
313
+ owned: true,
314
+ deviceName: created.name
315
+ };
316
+ const booting = startIosBoot(created.udid, () => configureOwnedIosSim({
317
+ record: newRecord,
318
+ projectPath,
319
+ profile: simslimProfile,
320
+ out,
321
+ reconcileIosSimulator
322
+ }));
323
+ return {
324
+ ...newRecord,
325
+ created: true,
326
+ booting,
327
+ deviceType: created.deviceType,
328
+ runtime: created.runtime
329
+ };
330
+ }
331
+ function withIosDeviceNameLock(fn) {
332
+ return withWorkspaceProcessLock(getConfigDir(), "ios-device-names", async () => fn(), { external: true });
333
+ }
334
+ function ownedIosNameSuffix(label, model, projectPath, udid) {
335
+ const names = new Set(listAllIosSims({ includeUnavailable: true }).filter((sim) => sim.udid !== udid).map((sim) => sim.name));
336
+ for (const project of Object.values(loadConfig()?.projects ?? {})) {
337
+ const record = project.platforms?.ios;
338
+ if (record?.deviceUdid !== udid && record?.deviceName) names.add(record.deviceName);
339
+ }
340
+ for (const parked of readParked("ios")) if (parked.udid !== udid) names.add(parked.name);
341
+ let suffix = "";
342
+ for (let attempt = 0; names.has(ownedSimName(label, model, suffix)); attempt++) suffix = ` ${workspaceId(projectPath)}${attempt ? `-${attempt}` : ""}`;
343
+ return suffix;
344
+ }
345
+ function renameToOwnedName(sim, label, model, projectPath) {
346
+ const wanted = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, sim.udid));
347
+ if (sim.name === wanted) return wanted;
348
+ try {
349
+ renameIosSim(sim.udid, wanted);
350
+ return wanted;
351
+ } catch {
352
+ return sim.name;
353
+ }
354
+ }
355
+ function resetAdoptedSim(udid, out) {
356
+ out(chalk.dim(phaseLine("device", `resetting privacy grants and the keychain on ${udid}`)));
357
+ resetIosPrivacy(udid);
358
+ resetIosKeychain(udid);
359
+ }
360
+ function takeParkedIosSim({ projectPath, label, choice, out }) {
361
+ const candidates = selectParked(readParked("ios"), {
362
+ deviceTypeIdentifier: choice.deviceTypeId,
363
+ runtimeIdentifier: choice.runtimeId
364
+ });
365
+ if (candidates.length === 0) return null;
366
+ const listed = new Map(listAllIosSims({ includeUnavailable: true }).map((sim) => [sim.udid, sim]));
367
+ for (const parked of candidates) {
368
+ const sim = listed.get(parked.udid);
369
+ if (!sim || !sim.available) {
370
+ const result = sim ? teardownParkedIosSim(parked.udid, { label: parked.name }) : null;
371
+ if (sim ? result?.status === "torn-down" : dropParked("ios", parked.udid)) out(chalk.dim(phaseLine("device", `deleted parked ${parked.name} (${parked.udid}): ${sim ? "unavailable" : "gone"}`)));
372
+ else if (result?.status === "failed") out(chalk.yellow(phaseLine("device", `could not delete unavailable parked ${parked.name} (${parked.udid}): ${result.reason}`)));
373
+ continue;
374
+ }
375
+ if (!sim.name.startsWith("stim-")) {
376
+ out(chalk.yellow(phaseLine("device", `kept parked record for ${parked.udid}: simulator is now named ${JSON.stringify(sim.name)} and is not Stim-owned`)));
377
+ continue;
378
+ }
379
+ const model = {
380
+ model: choice.deviceType,
381
+ runtime: choice.runtime
382
+ };
383
+ const name = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, parked.udid));
384
+ const device = {
385
+ deviceUdid: parked.udid,
386
+ owned: true,
387
+ deviceName: name,
388
+ adopted: true,
389
+ adoptionPending: true,
390
+ ...parked.simslimManaged ? { simslimManaged: true } : {},
391
+ ...parked.cacheKey ? { parkedCacheKey: parked.cacheKey } : {}
392
+ };
393
+ if (!adoptParked({
394
+ platform: "ios",
395
+ projectPath,
396
+ udid: parked.udid,
397
+ device
398
+ })) continue;
399
+ try {
400
+ renameIosSim(parked.udid, name);
401
+ } catch {}
402
+ return device;
403
+ }
404
+ return null;
405
+ }
406
+ function clearIosAdoptionPending(projectPath) {
407
+ withConfigLock(() => {
408
+ const cfg = loadConfig();
409
+ const ios = cfg?.projects?.[projectPath]?.platforms?.ios;
410
+ if (!cfg || !ios?.adoptionPending) return;
411
+ delete ios.adopted;
412
+ delete ios.adoptionPending;
413
+ delete ios.parkedCacheKey;
414
+ saveConfig(cfg);
415
+ });
416
+ }
417
+ async function configureOwnedIosSim({ record, projectPath, profile, out, reconcileIosSimulator }) {
418
+ if (profile) out(chalk.dim(phaseLine("device", `applying the SimSlim profile to ${record.deviceUdid}`)));
419
+ else if (record.simslimManaged) out(chalk.dim(phaseLine("device", `restoring stock simulator services on ${record.deviceUdid}`)));
420
+ const previouslyManaged = Boolean(record.simslimManaged);
421
+ if (profile && !record.simslimManaged) {
422
+ const pending = {
423
+ ...record,
424
+ simslimManaged: true
425
+ };
426
+ setDevice(projectPath, "ios", pending);
427
+ record = pending;
428
+ }
429
+ const result = await reconcileIosSimulator({
430
+ udid: record.deviceUdid,
431
+ profile,
432
+ previouslyManaged,
433
+ out
434
+ });
435
+ const updated = { ...record };
436
+ if (result.managed) updated.simslimManaged = true;
437
+ else delete updated.simslimManaged;
438
+ setDevice(projectPath, "ios", updated);
439
+ return updated;
440
+ }
441
+ function findOtherProjectOwningAvd(avdName, projectPath) {
442
+ const cfg = loadConfig();
443
+ for (const [path, proj] of Object.entries(cfg?.projects || {})) {
444
+ if (path === projectPath) continue;
445
+ if (proj?.platforms?.android?.avdName === avdName) return path;
446
+ }
447
+ return null;
448
+ }
449
+ var AvdRecoveryError = class extends Error {
450
+ remedy;
451
+ constructor(message, remedy, cause) {
452
+ super(message, { cause });
453
+ this.remedy = remedy;
454
+ }
455
+ };
456
+ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
457
+ const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
458
+ const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
459
+ if (record?.setupIncomplete && record.avdName) {
460
+ const cleanup = teardownAvd(record.avdName, {
461
+ del: true,
462
+ owner: { projectPath }
463
+ });
464
+ if (cleanup.status === "failed" || cleanup.status === "skipped") throw new Error(`Owned AVD ${record.avdName} has incomplete setup and could not be deleted (${cleanup.reason || cleanup.status}). Fix the cause, then retry; Stim kept the device record for cleanup.`);
465
+ clearDevice(projectPath, "android");
466
+ record = null;
467
+ }
468
+ if (record?.avdName) {
469
+ if (record.owned) {
470
+ const resolved = resolveOwnedAvdSerial(record.avdName);
471
+ if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned by name -- creating a fresh owned AVD instead of reusing it.`));
472
+ else if (resolved.serial) {
473
+ const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
474
+ if (record.consolePort && record.consolePort !== consolePort) out(phaseLine("device", `${record.avdName} changed serial (emulator-${record.consolePort} -> ${resolved.serial}); reconnecting this run, reopen agent-device on ${resolved.serial}`));
475
+ const updated = {
476
+ ...record,
477
+ avdName: record.avdName,
478
+ consolePort,
479
+ owned: true,
480
+ deviceName: record.deviceName ?? record.avdName
481
+ };
482
+ setDevice(projectPath, "android", updated);
483
+ return {
484
+ ...updated,
485
+ systemImage: ownedAvdSystemImage(record.avdName)
486
+ };
487
+ } else if (!resolved.missing) {
488
+ out(chalk.dim(`Recorded port for owned AVD ${record.avdName} is not currently ours; booting it on a freshly allocated port...`));
489
+ return {
490
+ ...await bootOwnedAvdOnFreshPort({
491
+ avdName: record.avdName,
492
+ metadata: record,
493
+ projectPath,
494
+ deviceName: record.deviceName,
495
+ out,
496
+ logFile,
497
+ alive
498
+ }),
499
+ systemImage: ownedAvdSystemImage(record.avdName)
500
+ };
501
+ }
502
+ } else if (listAvds().includes(record.avdName)) {
503
+ if (!listAdbDevices().emulators.some((e) => e.consolePort === record.consolePort)) {
504
+ note(chalk.yellow(`Note: assigned AVD ${record.avdName} (emulator-${record.consolePort}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
505
+ note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned AVD."));
506
+ }
507
+ return record;
508
+ }
509
+ } else if (record?.serial) {
510
+ note(chalk.yellow(`Note: this project has a stored assignment to physical device ${record.serial}, which Stim no longer keeps.`));
511
+ note(chalk.dim("Creating an owned emulator instead. Pass `--device` to build for a connected device."));
512
+ }
513
+ if (parkedMaxSetting("android").max > 0) {
514
+ const systemImage = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: flags.systemImage || settings.android?.systemImage })?.pkg;
515
+ const candidates = readParked("android").filter((entry) => entry.systemImage === systemImage && entry.configuration === configuration).toSorted((a, b) => a.parkedAt.localeCompare(b.parkedAt));
516
+ for (const parked of candidates) {
517
+ if (parked.deletionClaim !== void 0) continue;
518
+ const resolved = resolveOwnedAvdSerial(parked.name);
519
+ if (resolved.missing) {
520
+ const result = teardownParkedAvd(parked.name);
521
+ if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
522
+ continue;
523
+ }
524
+ if (resolved.notOwned || resolved.serial) continue;
525
+ try {
526
+ assertOwnedAvdStopped(parked.name);
527
+ } catch (error) {
528
+ out(phaseLine("device", `kept parked ${parked.name}: ${String(error?.message || error)}`));
529
+ continue;
530
+ }
531
+ if (ownedAvdSystemImage(parked.name) !== systemImage || !ownedAvdMatchesConfiguration(parked.name, configuration)) {
532
+ const result = teardownParkedAvd(parked.name);
533
+ if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
534
+ continue;
535
+ }
536
+ const adopted = {
537
+ avdName: parked.name,
538
+ deviceName: parked.name,
539
+ owned: true,
540
+ poolConfiguration: configuration,
541
+ adoptionPending: true
542
+ };
543
+ if (!adoptParked({
544
+ platform: "android",
545
+ projectPath,
546
+ udid: parked.udid,
547
+ device: adopted
548
+ })) continue;
549
+ return {
550
+ ...await bootOwnedAvdOnFreshPort({
551
+ avdName: parked.name,
552
+ projectPath,
553
+ metadata: adopted,
554
+ out,
555
+ logFile,
556
+ alive
557
+ }),
558
+ adopted: true,
559
+ systemImage
560
+ };
561
+ }
562
+ }
563
+ let created;
564
+ let fresh = false;
565
+ try {
566
+ created = withConfigLock(() => {
567
+ const current = loadConfig()?.projects?.[projectPath]?.platforms?.android;
568
+ if (current?.avdName && current.avdName !== record?.avdName) throw new Error(`Another Stim run assigned AVD ${current.avdName} to this workspace. Retry to use it.`);
569
+ if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherProjectOwningAvd(ownedAvdName(label), projectPath)) label = `${label}-${randomUUID().slice(0, 8)}`;
570
+ const result = createOwnedAvd(label, { systemImage: flags.systemImage || settings.android?.systemImage });
571
+ setDevice(projectPath, "android", {
572
+ avdName: result.avdName,
573
+ owned: true,
574
+ deviceName: result.avdName,
575
+ setupIncomplete: true,
576
+ poolConfiguration: configuration
577
+ });
578
+ return result;
579
+ });
580
+ fresh = true;
581
+ } catch (e) {
582
+ const message = String(e?.message || e);
583
+ const avdName = ownedAvdName(label);
584
+ if (message.includes("already exists")) {
585
+ try {
586
+ if (!listAvds().includes(avdName)) throw new AvdRecoveryError(`AVD ${avdName} already exists on disk but is not listed by the emulator. ${message}`, "Run `npx stim gc` to inspect orphaned owned AVDs, then `npx stim gc --delete` to reclaim those safe to delete. Retry `stim android` after cleanup; keep any AVD that GC cannot verify.", e);
587
+ created = withConfigLock(() => {
588
+ if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`, { cause: e });
589
+ const owner = findOtherProjectOwningAvd(avdName, projectPath);
590
+ if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`, { cause: e });
591
+ const current = loadConfig()?.projects?.[projectPath]?.platforms?.android;
592
+ if (current?.avdName) {
593
+ const state = current.setupIncomplete ? "has incomplete setup" : "was registered";
594
+ throw new Error(`AVD ${current.avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`, { cause: e });
595
+ }
596
+ const resolved = resolveOwnedAvdSerial(avdName);
597
+ if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: e });
598
+ if (!resolved.serial) assertOwnedAvdStopped(avdName);
599
+ const recovered = {
600
+ avdName,
601
+ owned: true,
602
+ deviceName: avdName,
603
+ ...resolved.serial ? { consolePort: Number(resolved.serial.replace(/^emulator-/, "")) } : {}
604
+ };
605
+ setDevice(projectPath, "android", recovered);
606
+ return {
607
+ ...recovered,
608
+ systemImage: ownedAvdSystemImage(avdName),
609
+ serial: resolved.serial
610
+ };
611
+ });
612
+ } catch (error) {
613
+ if (error instanceof AvdRecoveryError) throw error;
614
+ throw new AvdRecoveryError(`Could not recover owned AVD ${avdName}: ${String(error?.message || error)}`, "Inspect `npx stim status` and `adb devices`. Wait for any other Stim run using this AVD to finish, then retry `stim android`. Keep the AVD and its process locks while its state is unverified.", error);
615
+ }
616
+ out(chalk.dim(phaseLine("device", `recovered ${avdName} (unrecorded from a prior run)`)));
617
+ if (created.serial) return {
618
+ ...created,
619
+ owned: true,
620
+ deviceName: avdName,
621
+ created: false
622
+ };
623
+ } else throw e;
624
+ }
625
+ if (fresh) try {
626
+ configureAvd(created.avdName, {
627
+ dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
628
+ avdConfig
629
+ });
630
+ } catch (error) {
631
+ const cleanup = teardownAvd(created.avdName, {
632
+ del: true,
633
+ owner: { projectPath }
634
+ });
635
+ const kept = cleanup.status === "failed" || cleanup.status === "skipped";
636
+ if (!kept) clearDevice(projectPath, "android");
637
+ const orphan = kept ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
638
+ throw new Error(`Created owned AVD ${created.avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
639
+ }
640
+ return {
641
+ ...await bootOwnedAvdOnFreshPort({
642
+ avdName: created.avdName,
643
+ metadata: fresh ? { poolConfiguration: configuration } : void 0,
644
+ projectPath,
645
+ deviceName: created.avdName,
646
+ out,
647
+ logFile,
648
+ alive
649
+ }),
650
+ created: fresh,
651
+ systemImage: created.systemImage
652
+ };
653
+ }
654
+ function claimAndroidConsolePort({ projectPath, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
655
+ return lock(() => {
656
+ const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
657
+ const claim = {
658
+ ...metadata?.poolConfiguration ? { poolConfiguration: metadata.poolConfiguration } : {},
659
+ ...metadata?.adoptionPending ? { adoptionPending: true } : {},
660
+ avdName,
661
+ consolePort,
662
+ owned: true,
663
+ deviceName: deviceName ?? avdName
664
+ };
665
+ record(projectPath, "android", claim);
666
+ return claim;
667
+ });
668
+ }
669
+ function liveAndroidConsolePorts() {
670
+ const adbLive = listAdbDevices();
671
+ return [...adbLive.emulators.map((e) => e.consolePort), ...adbLive.unhealthy.map((u) => u.consolePort).filter((p) => p != null)];
672
+ }
673
+ async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, deviceName, out, logFile = null, alive = pidExists }) {
674
+ const claim = claimAndroidConsolePort({
675
+ projectPath,
676
+ avdName,
677
+ deviceName,
678
+ livePorts: liveAndroidConsolePorts(),
679
+ metadata
680
+ });
681
+ const serial = `emulator-${claim.consolePort}`;
682
+ try {
683
+ const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
684
+ out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
685
+ const result = await waitForBoot(serial, 12e4, { aborted: emulatorGone(pid, alive) });
686
+ if (!result.ok) throw new Error(`${bootFailurePrefix(serial, result.exited, 12e4)} Diagnostic: ${JSON.stringify(result.diagnostic)}`);
687
+ const running = getAvdNameForSerial(serial);
688
+ if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
689
+ return {
690
+ ...claim,
691
+ serial
692
+ };
693
+ } catch (error) {
694
+ releaseAndroidConsolePort(projectPath, claim.consolePort);
695
+ throw error;
696
+ }
697
+ }
698
+ function emulatorGone(pid, alive) {
699
+ if (!pid) return () => false;
700
+ return () => !alive(pid);
701
+ }
702
+ function bootFailurePrefix(serial, exited, timeoutMs) {
703
+ return exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(timeoutMs / 1e3)}s.`;
704
+ }
705
+ function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
706
+ let count = 0;
707
+ for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
708
+ const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
709
+ for (const proj of Object.values(config?.projects || {})) {
710
+ const android = proj?.platforms?.android;
711
+ if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
712
+ }
713
+ return count;
714
+ }
715
+ function workspaceHasLiveDevice({ platform, project, sims = [], adbEmulators = [] } = {}) {
716
+ if (!platform) return false;
717
+ const record = project?.platforms?.[platform];
718
+ if (!record) return false;
719
+ if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
720
+ return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
721
+ }
722
+ function deviceCapacityRefusal({ platform, project, max, sims = [], adb = null, config = null } = {}) {
723
+ if (!max || max <= 0) return null;
724
+ const adbEmulators = adb?.emulators || [];
725
+ if (workspaceHasLiveDevice({
726
+ platform,
727
+ project,
728
+ sims,
729
+ adbEmulators
730
+ })) return null;
731
+ const count = liveOwnedDeviceCount({
732
+ sims,
733
+ adbEmulators,
734
+ config
735
+ });
736
+ if (count < max) return null;
737
+ return {
738
+ code: "STIM_AT_CAPACITY",
739
+ message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
740
+ remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
741
+ };
742
+ }
743
+ function checkDeviceCapacity({ platform, project, max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
744
+ if (!max || max <= 0) return null;
745
+ let simList = [];
746
+ let adbRes = {
747
+ emulators: [],
748
+ physical: [],
749
+ unhealthy: []
750
+ };
751
+ try {
752
+ simList = typeof sims === "function" ? sims() || [] : sims || [];
753
+ } catch {}
754
+ try {
755
+ adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
756
+ } catch {}
757
+ let cfg = null;
758
+ try {
759
+ cfg = typeof config === "function" ? config() : config ?? null;
760
+ } catch {}
761
+ return deviceCapacityRefusal({
762
+ platform,
763
+ project,
764
+ max,
765
+ sims: simList,
766
+ adb: adbRes,
767
+ config: cfg
768
+ });
769
+ }
770
+ function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
771
+ if (!requestedName || !recordedTypeId) return null;
772
+ const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
773
+ if (!wanted) return null;
774
+ if (wanted.identifier === recordedTypeId) return null;
775
+ const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
776
+ return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
777
+ }
778
+ function installedNames(names) {
779
+ const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
780
+ return unique.length > 0 ? unique.join(", ") : "none";
781
+ }
782
+ function unknownIosRuntimeRefusal(requested, runtimes) {
783
+ if (!requested) return null;
784
+ if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
785
+ return {
786
+ message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
787
+ 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."
788
+ };
789
+ }
790
+ function creatableIosDeviceTypeNames(runtimes, runtime) {
791
+ return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
792
+ }
793
+ function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
794
+ if (!requested) return null;
795
+ const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
796
+ if (creatable.includes(requested)) return null;
797
+ return {
798
+ 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)}.`,
799
+ 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."
800
+ };
801
+ }
802
+ function unknownAndroidSystemImageRefusal(requested, images) {
803
+ if (!requested) return null;
804
+ if ((images || []).some((i) => i.pkg === requested)) return null;
805
+ return {
806
+ message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
807
+ 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\"`."
808
+ };
809
+ }
810
+ const BOOT_POLL_MS = 500;
811
+ async function ensureBooted({ platform, device, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists } = {}) {
812
+ if (platform === "ios") return ensureIosBooted({
813
+ device,
814
+ timeoutMs: timeoutMs ?? 6e5,
815
+ pollMs,
816
+ out
817
+ });
818
+ if (platform === "android") return ensureAndroidBooted({
819
+ device,
820
+ timeoutMs: timeoutMs ?? 24e4,
821
+ out,
822
+ logFile,
823
+ alive
824
+ });
825
+ return {
826
+ failed: true,
827
+ reason: `Unknown platform "${platform}".`
828
+ };
829
+ }
830
+ async function ensureIosBooted({ device, timeoutMs, pollMs, out }) {
831
+ const udid = device?.deviceUdid;
832
+ if (!udid) return {
833
+ failed: true,
834
+ reason: "No iOS simulator is recorded for this project."
835
+ };
836
+ const ready = () => {
837
+ try {
838
+ getExecutor().runFile("xcrun", [
839
+ "simctl",
840
+ "spawn",
841
+ udid,
842
+ "/usr/bin/true"
843
+ ], { timeoutMs: 3e4 });
844
+ return {
845
+ ok: true,
846
+ udid
847
+ };
848
+ } catch (error) {
849
+ return {
850
+ failed: true,
851
+ reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
852
+ };
853
+ }
854
+ };
855
+ const booting = device?.booting;
856
+ if (booting?.udid === udid) {
857
+ try {
858
+ await booting.done;
859
+ } catch (e) {
860
+ return {
861
+ failed: true,
862
+ reason: `Could not boot simulator ${udid}: ${e?.message || e}`
863
+ };
864
+ }
865
+ return ready();
866
+ }
867
+ let resolved;
868
+ try {
869
+ resolved = resolveOwnedIosSim(udid);
870
+ } catch (e) {
871
+ return {
872
+ failed: true,
873
+ reason: `Could not list simulators: ${e?.message || e}`
874
+ };
875
+ }
876
+ if (resolved.missing) return {
877
+ failed: true,
878
+ reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
879
+ };
880
+ if (resolved.notOwned) return {
881
+ failed: true,
882
+ reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
883
+ };
884
+ const sim = resolved.sim;
885
+ if (sim.state === "Booted") return ready();
886
+ out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
887
+ const bootDeadline = Date.now() + timeoutMs;
888
+ try {
889
+ await bootIosSim(udid, { timeoutMs });
890
+ } catch (e) {
891
+ return {
892
+ failed: true,
893
+ reason: `Could not boot simulator ${udid}: ${e?.message || e}`
894
+ };
895
+ }
896
+ const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
897
+ while (Date.now() < deadline) {
898
+ await sleep(pollMs);
899
+ let state = null;
900
+ try {
901
+ state = listAllIosSims({ timeoutMs: 3e4 }).find((s) => s.udid === udid)?.state ?? null;
902
+ } catch {}
903
+ if (state === "Booted") return ready();
904
+ }
905
+ return {
906
+ failed: true,
907
+ reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
908
+ };
909
+ }
910
+ async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, alive = pidExists }) {
911
+ if (!device?.avdName) return {
912
+ failed: true,
913
+ reason: "No owned Android emulator is recorded for this project."
914
+ };
915
+ let resolved;
916
+ try {
917
+ resolved = resolveOwnedAvdSerial(device.avdName);
918
+ } catch (e) {
919
+ return {
920
+ failed: true,
921
+ reason: `Could not list AVDs: ${e?.message || e}`
922
+ };
923
+ }
924
+ if (resolved.missing) return {
925
+ failed: true,
926
+ reason: `AVD ${device.avdName} no longer exists. Run \`stim android\` again to create a fresh owned AVD.`
927
+ };
928
+ if (resolved.notOwned) return {
929
+ failed: true,
930
+ reason: `AVD ${device.avdName} is not Stim-owned by name; refusing to boot it.`
931
+ };
932
+ if (resolved.serial) {
933
+ const ready = await waitForBoot(resolved.serial, timeoutMs);
934
+ if (!ready.ok) return {
935
+ failed: true,
936
+ reason: `Emulator ${resolved.serial} never reported boot completion. Diagnostic: ${JSON.stringify(ready.diagnostic)}`
937
+ };
938
+ return {
939
+ ok: true,
940
+ serial: resolved.serial
941
+ };
942
+ }
943
+ const freshSerial = `emulator-${device.consolePort}`;
944
+ if (device.owned && device.serial === freshSerial) {
945
+ const ready = await waitForBoot(freshSerial, timeoutMs);
946
+ if (ready.ok) return {
947
+ ok: true,
948
+ serial: freshSerial
949
+ };
950
+ return {
951
+ failed: true,
952
+ reason: `Emulator ${freshSerial} never reported boot completion. Diagnostic: ${JSON.stringify(ready.diagnostic)}`
953
+ };
954
+ }
955
+ const serial = `emulator-${pickConsolePort(device.consolePort)}`;
956
+ out(chalk.dim(phaseLine("device", `booting ${device.avdName} as ${serial}`)));
957
+ let pid = null;
958
+ try {
959
+ pid = bootAndroidEmulator(device.avdName, Number(serial.replace(/^emulator-/, "")), { logFile });
960
+ } catch (e) {
961
+ return {
962
+ failed: true,
963
+ reason: `Could not start emulator for AVD ${device.avdName}: ${e?.message || e}`
964
+ };
965
+ }
966
+ const ready = await waitForBoot(serial, timeoutMs, { aborted: emulatorGone(pid, alive) });
967
+ if (!ready.ok) return {
968
+ failed: true,
969
+ reason: `${bootFailurePrefix(serial, ready.exited, timeoutMs)} Diagnostic: ${JSON.stringify(ready.diagnostic)}`
970
+ };
971
+ return {
972
+ ok: true,
973
+ serial
974
+ };
975
+ }
976
+ function pickConsolePort(recorded) {
977
+ const live = liveAndroidConsolePorts();
978
+ if (recorded && !live.includes(Number(recorded))) return Number(recorded);
979
+ return nextConsolePort([...allConsolePortsAndSerials().androidConsolePorts, ...live]);
980
+ }
981
+ function sleep(ms) {
982
+ return new Promise((r) => setTimeout(r, ms));
983
+ }
984
+ //#endregion
985
+ //#region src/engine/bundler.ts
986
+ const COCOAPODS_SPEC = /^ {4}cocoapods \(/m;
987
+ function bundlerPin(root) {
988
+ const gemfile = join(root, "Gemfile");
989
+ const lockfile = join(root, "Gemfile.lock");
990
+ if (!existsSync(gemfile)) return null;
991
+ let lock;
992
+ try {
993
+ lock = readFileSync(lockfile, "utf-8");
994
+ } catch {
995
+ return null;
996
+ }
997
+ if (!COCOAPODS_SPEC.test(lock)) return null;
998
+ return {
999
+ gemfile,
1000
+ lockfile
1001
+ };
1002
+ }
1003
+ function podInstallCommand(root, ...args) {
1004
+ const pod = [
1005
+ "pod",
1006
+ "install",
1007
+ ...args
1008
+ ].join(" ");
1009
+ return bundlerPin(root) ? `bundle exec ${pod}` : pod;
1010
+ }
1011
+ const BUILD_FAILED = "** BUILD FAILED **";
1012
+ const POSITIONED = /^(\S[^\t]*?):(\d+):(?:(\d+):)?\s*(?:fatal\s+)?error:\s+(.+)$/;
1013
+ const UNPOSITIONED = /^(\S[^\t]*?)?\berror:\s+(.+)$/;
1014
+ const LD_ERROR = /^ld:\s+(?!warning:)(.+)$/;
1015
+ const UNDEFINED_HEADER = /^(?:ld:\s+)?Undefined symbols?(?:\s+for architecture\s+\S+)?:\s*$/;
1016
+ const UNDEFINED_SYMBOL = /^\s+"?([^",]+)"?,\s+referenced from:\s*$/;
1017
+ function undefinedSymbolMessage(symbol) {
1018
+ return `Undefined symbol: ${symbol}`;
1019
+ }
1020
+ const PODS_OUT_OF_SYNC = /The sandbox is not in sync with the Podfile\.lock/;
1021
+ const SIGNING = [
1022
+ /No profiles for '.*' were found/,
1023
+ /requires a development team/i,
1024
+ /[Cc]ode ?[Ss]igning [Ee]rror/,
1025
+ /No signing certificate/i,
1026
+ /code signing is required/i,
1027
+ /errSecInternalComponent/,
1028
+ /Provisioning profile .* doesn't (?:include|match)/i
1029
+ ];
1030
+ const NO_SUCH_SCHEME = /does not contain a scheme named/;
1031
+ const CAS_CORRUPT = /not a IncludeTreeRoot node kind/;
1032
+ const DEVICE_SIGNING_REMEDY = "Set a team and a Development profile for the target's configuration in Xcode > Signing & Capabilities, then build once from Xcode to install the profile. Stim never passes -allowProvisioningUpdates, because registering a device or minting a profile changes your Apple Developer account.";
1033
+ const SIMULATOR_SIGNING_REMEDY = "Stim builds for the simulator here, which needs no signing. Check CODE_SIGNING_REQUIRED / DEVELOPMENT_TEAM in the target's configuration, or build for a phone with `stim ios --device`.";
1034
+ function remedyFor(message, root, sdk) {
1035
+ if (CAS_CORRUPT.test(message)) return "The compilation cache holds a damaged object. Run `stim gc --delete --cache \"compilation cache\"` to empty that cache, then build again. The next build is a cold one.";
1036
+ if (PODS_OUT_OF_SYNC.test(message)) return `Run \`${root ? podInstallCommand(root) : "pod install"}\` in ios/ (stim ios does this when Podfile.lock and Pods/Manifest.lock disagree), then build again.`;
1037
+ if (NO_SUCH_SCHEME.test(message)) return "Run `xcodebuild -list` in ios/ to see the schemes this project defines, and share the app scheme so it is visible to the build.";
1038
+ for (const pattern of SIGNING) if (pattern.test(message)) return sdk.startsWith("iphoneos") ? DEVICE_SIGNING_REMEDY : SIMULATOR_SIGNING_REMEDY;
1039
+ return null;
1040
+ }
1041
+ function fileFromPrefix(prefix) {
1042
+ const trimmed = String(prefix).trim().replace(/:$/, "");
1043
+ if (!trimmed) return null;
1044
+ const head = (trimmed.split(": ")[0] ?? "").trim();
1045
+ if (!head.includes("/")) return null;
1046
+ return head;
1047
+ }
1048
+ function makeDiagnostic({ file = null, line = null, column = null, message }, root, sdk) {
1049
+ const text = String(message).trim();
1050
+ const out = { message: text };
1051
+ if (file) out.file = file;
1052
+ if (line !== null && line !== void 0) out.line = line;
1053
+ if (column !== null && column !== void 0) out.column = column;
1054
+ out.message = text;
1055
+ const remedy = remedyFor(text, root, sdk);
1056
+ if (remedy) out.remedy = remedy;
1057
+ return out;
1058
+ }
1059
+ function dedupeKey(d) {
1060
+ return `${d.file || ""}|${d.line || ""}|${d.column || ""}|${d.message}`;
1061
+ }
1062
+ function extractXcodeDiagnostics(transcript, root = null, sdk = "iphonesimulator") {
1063
+ if (typeof transcript !== "string" || transcript === "") return [];
1064
+ const lines = transcript.split("\n");
1065
+ const out = [];
1066
+ const seen = /* @__PURE__ */ new Set();
1067
+ const push = (d) => {
1068
+ const key = dedupeKey(d);
1069
+ if (seen.has(key)) return;
1070
+ seen.add(key);
1071
+ out.push(d);
1072
+ };
1073
+ for (let i = 0; i < lines.length; i += 1) {
1074
+ const rawLine = lines[i];
1075
+ if (rawLine === void 0) continue;
1076
+ const raw = rawLine.replace(/\r$/, "");
1077
+ if (raw.includes(BUILD_FAILED)) break;
1078
+ if (UNDEFINED_HEADER.test(raw)) {
1079
+ for (let j = i + 1; j < lines.length; j += 1) {
1080
+ const symLine = lines[j];
1081
+ if (symLine === void 0) continue;
1082
+ const sym = UNDEFINED_SYMBOL.exec(symLine.replace(/\r$/, ""));
1083
+ if (sym) {
1084
+ const symbol = sym[1];
1085
+ if (symbol !== void 0) push(makeDiagnostic({ message: undefinedSymbolMessage(symbol) }, root, sdk));
1086
+ continue;
1087
+ }
1088
+ if (/^\s+\S/.test(symLine) && !/^\S/.test(symLine)) continue;
1089
+ i = j - 1;
1090
+ break;
1091
+ }
1092
+ continue;
1093
+ }
1094
+ const ld = LD_ERROR.exec(raw);
1095
+ if (ld) {
1096
+ push(makeDiagnostic({ message: `ld: ${ld[1]}` }, root, sdk));
1097
+ continue;
1098
+ }
1099
+ const positioned = POSITIONED.exec(raw);
1100
+ if (positioned) {
1101
+ const posMsg = positioned[4];
1102
+ if (posMsg === void 0) continue;
1103
+ push(makeDiagnostic({
1104
+ file: positioned[1],
1105
+ line: Number(positioned[2]),
1106
+ column: positioned[3] === void 0 ? null : Number(positioned[3]),
1107
+ message: posMsg
1108
+ }, root, sdk));
1109
+ continue;
1110
+ }
1111
+ const plain = UNPOSITIONED.exec(raw);
1112
+ if (plain) {
1113
+ const plainMsg = plain[2];
1114
+ if (plainMsg === void 0) continue;
1115
+ push(makeDiagnostic({
1116
+ file: fileFromPrefix(plain[1] || ""),
1117
+ message: plainMsg
1118
+ }, root, sdk));
1119
+ }
1120
+ }
1121
+ return out;
1122
+ }
1123
+ function capDiagnostics(diagnostics, max = 10) {
1124
+ const list = Array.isArray(diagnostics) ? diagnostics : [];
1125
+ if (list.length <= max) return {
1126
+ diagnostics: list.slice(),
1127
+ truncated: 0
1128
+ };
1129
+ return {
1130
+ diagnostics: list.slice(0, max),
1131
+ truncated: list.length - max
1132
+ };
1133
+ }
1134
+ function describeDiagnostic(diagnostic) {
1135
+ if (!diagnostic || typeof diagnostic !== "object") return "";
1136
+ const { file, line, column, message } = diagnostic;
1137
+ if (!file) return String(message ?? "");
1138
+ return `${file}${line ? column ? `:${line}:${column}` : `:${line}` : ""}: ${message}`;
1139
+ }
1140
+ //#endregion
1141
+ //#region src/engine/xcode.ts
1142
+ const IOS_DIR = "ios";
1143
+ const PREBUILD_REMEDY = "Generate it with `npx expo prebuild -p ios` (stim ios does this automatically for an Expo project with no ios/ directory), or commit the native project.";
1144
+ function buildFailure(message, remedy) {
1145
+ return { error: {
1146
+ code: "STIM_BUILD_FAILED",
1147
+ message,
1148
+ remedy
1149
+ } };
1150
+ }
1151
+ function pickXcodeProject(entries) {
1152
+ const names = (Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string");
1153
+ const workspaces = names.filter((e) => e.endsWith(".xcworkspace")).toSorted();
1154
+ const projects = names.filter((e) => e.endsWith(".xcodeproj")).toSorted();
1155
+ if (workspaces.length > 0) {
1156
+ const projectNames = new Set(projects.map((p) => basename(p, ".xcodeproj")));
1157
+ const file = workspaces.find((w) => projectNames.has(basename(w, ".xcworkspace"))) || workspaces[0];
1158
+ if (file === void 0) return null;
1159
+ return {
1160
+ kind: "workspace",
1161
+ flag: "-workspace",
1162
+ file,
1163
+ name: basename(file, ".xcworkspace")
1164
+ };
1165
+ }
1166
+ if (projects.length > 0) {
1167
+ const file = projects[0];
1168
+ if (file === void 0) return null;
1169
+ return {
1170
+ kind: "project",
1171
+ flag: "-project",
1172
+ file,
1173
+ name: basename(file, ".xcodeproj")
1174
+ };
1175
+ }
1176
+ return null;
1177
+ }
1178
+ function discoverXcodeProject(root) {
1179
+ const dir = join(root, IOS_DIR);
1180
+ let entries;
1181
+ try {
1182
+ entries = readdirSync(dir);
1183
+ } catch {
1184
+ return buildFailure(`No ${IOS_DIR}/ directory in ${root}.`, PREBUILD_REMEDY);
1185
+ }
1186
+ const picked = pickXcodeProject(entries);
1187
+ if (!picked) return buildFailure(`${dir} contains no .xcworkspace and no .xcodeproj.`, PREBUILD_REMEDY);
1188
+ return {
1189
+ ...picked,
1190
+ dir,
1191
+ path: join(dir, picked.file)
1192
+ };
1193
+ }
1194
+ function parseSchemeList(text) {
1195
+ const empty = {
1196
+ name: null,
1197
+ schemes: []
1198
+ };
1199
+ if (typeof text !== "string") return empty;
1200
+ const start = text.indexOf("{");
1201
+ const end = text.lastIndexOf("}");
1202
+ if (start < 0 || end <= start) return empty;
1203
+ let data;
1204
+ try {
1205
+ data = JSON.parse(text.slice(start, end + 1));
1206
+ } catch {
1207
+ return empty;
1208
+ }
1209
+ if (!data || typeof data !== "object") return empty;
1210
+ const record = data;
1211
+ const container = record.workspace || record.project;
1212
+ if (!container || typeof container !== "object") return empty;
1213
+ const info = container;
1214
+ const schemes = Array.isArray(info.schemes) ? info.schemes.filter((s) => typeof s === "string" && s.trim() !== "") : [];
1215
+ return {
1216
+ name: typeof info.name === "string" ? info.name : null,
1217
+ schemes
1218
+ };
1219
+ }
1220
+ const TEST_SCHEME = /(?:UI)?Tests$/;
1221
+ function pickScheme(schemes, containerName) {
1222
+ const list = (Array.isArray(schemes) ? schemes : []).filter((s) => typeof s === "string" && s.trim() !== "");
1223
+ if (list.length === 0) return null;
1224
+ const name = typeof containerName === "string" ? containerName.trim() : "";
1225
+ if (name) {
1226
+ const exact = list.find((s) => s === name);
1227
+ if (exact) return exact;
1228
+ const insensitive = list.find((s) => s.toLowerCase() === name.toLowerCase());
1229
+ if (insensitive) return insensitive;
1230
+ }
1231
+ const app = list.filter((s) => !TEST_SCHEME.test(s));
1232
+ return app.length === 1 ? app[0] : null;
1233
+ }
1234
+ const LIST_TIMEOUT_MS = 18e4;
1235
+ function listSchemes(project, { exec = null } = {}) {
1236
+ const executor = exec || getExecutor();
1237
+ try {
1238
+ return parseSchemeList(executor.runFile("xcodebuild", [
1239
+ project.flag,
1240
+ project.path,
1241
+ "-list",
1242
+ "-json"
1243
+ ], { timeoutMs: LIST_TIMEOUT_MS }));
1244
+ } catch {
1245
+ return null;
1246
+ }
1247
+ }
1248
+ function resolveScheme(project, { exec = null, scheme: requested = null } = {}) {
1249
+ const listing = listSchemes(project, { exec });
1250
+ if (listing === null) return { error: {
1251
+ code: "STIM_NO_SCHEME",
1252
+ message: `Could not list schemes for ${project.path}.`,
1253
+ remedy: `Run \`xcodebuild ${project.flag} ${project.path} -list\` to see what it reports.`
1254
+ } };
1255
+ if (requested !== null) {
1256
+ if (listing.schemes.includes(requested)) return {
1257
+ scheme: requested,
1258
+ schemes: listing.schemes
1259
+ };
1260
+ return { error: {
1261
+ code: "STIM_NO_SCHEME",
1262
+ message: `No shared Xcode scheme named ${JSON.stringify(requested)} in ${project.path}. Available schemes: ${listing.schemes.join(", ") || "none"}.`,
1263
+ remedy: "Pass an exact available name with --scheme, or share the intended app scheme in Xcode."
1264
+ } };
1265
+ }
1266
+ let scheme = pickScheme(listing.schemes, listing.name || project.name);
1267
+ if (!scheme && project.dir) try {
1268
+ const app = JSON.parse(readFileSync(join(project.dir, "..", "app.json"), "utf8"));
1269
+ scheme = pickScheme(listing.schemes, app?.name);
1270
+ } catch {}
1271
+ if (!scheme) {
1272
+ const found = listing.schemes.length ? listing.schemes.join(", ") : "none";
1273
+ return { error: {
1274
+ code: "STIM_NO_SCHEME",
1275
+ message: `Could not select an app scheme in ${project.path} (schemes: ${found}).`,
1276
+ remedy: listing.schemes.length > 0 ? "Pass --scheme <name> to select the intended shared app scheme. Stim does not guess between unmatched schemes." : "Share the app scheme in Xcode (Product > Scheme > Manage Schemes, tick Shared) so xcodebuild can see it."
1277
+ } };
1278
+ }
1279
+ return {
1280
+ scheme,
1281
+ schemes: listing.schemes
1282
+ };
1283
+ }
1284
+ const PREFIX_MAP_TARGET = "/^src";
1285
+ function prefixMapping(workspaceRoot) {
1286
+ return `${String(workspaceRoot).replace(/\/+$/, "")}=${PREFIX_MAP_TARGET}`;
1287
+ }
1288
+ function compilationPrefixMappings(workspaceRoot, derivedDataPath) {
1289
+ const source = resolve(workspaceRoot);
1290
+ const derived = resolve(derivedDataPath);
1291
+ const mappings = [prefixMapping(source)];
1292
+ const rel = relative(source, derived);
1293
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) mappings.push(`${derived}=/^derived-data`);
1294
+ return mappings.join(" ");
1295
+ }
1296
+ function ccacheEnabled(podfileProperties) {
1297
+ if (!podfileProperties || typeof podfileProperties !== "object") return false;
1298
+ return podfileProperties["apple.ccacheEnabled"] === "true";
1299
+ }
1300
+ function readPodfileProperties(root) {
1301
+ try {
1302
+ const parsed = JSON.parse(readFileSync(join(root, IOS_DIR, "Podfile.properties.json"), "utf-8"));
1303
+ return parsed && typeof parsed === "object" ? parsed : null;
1304
+ } catch {
1305
+ return null;
1306
+ }
1307
+ }
1308
+ function compilationCacheSettings({ workspaceRoot, derivedDataPath, casPath, xcodeMajor, ccache = false, optimizations = resolveOptimizations({}, {}).ios }) {
1309
+ if (xcodeMajor === null || xcodeMajor === void 0) return [];
1310
+ if (xcodeMajor < 26) return [];
1311
+ if (ccache) return [];
1312
+ return [
1313
+ `COMPILATION_CACHE_ENABLE_CACHING=${optimizations.compilationCache ? "YES" : "NO"}`,
1314
+ `COMPILATION_CACHE_CAS_PATH=${casPath}`,
1315
+ `SWIFT_ENABLE_COMPILE_CACHE=${optimizations.compilationCache && optimizations.swiftCompilationCache ? "YES" : "NO"}`,
1316
+ `CLANG_ENABLE_PREFIX_MAPPING=${optimizations.prefixMapping ? "YES" : "NO"}`,
1317
+ `CLANG_OTHER_PREFIX_MAPPINGS=${optimizations.prefixMapping ? compilationPrefixMappings(workspaceRoot, derivedDataPath) : ""}`
1318
+ ];
1319
+ }
1320
+ function parseXcodeMajor(output) {
1321
+ const m = /^Xcode\s+(\d+)/m.exec(String(output || ""));
1322
+ if (!m) return null;
1323
+ const digits = m[1];
1324
+ if (digits === void 0) return null;
1325
+ const major = parseInt(digits, 10);
1326
+ return Number.isFinite(major) ? major : null;
1327
+ }
1328
+ function detectXcodeMajor(exec = null) {
1329
+ return parseXcodeMajor((exec || getExecutor()).runQuiet("xcodebuild -version", { timeoutMs: 1e4 }));
1330
+ }
1331
+ function resolveCompilationCacheSettings({ root, derivedDataPath, exec = null, casPath = sharedCompilationCache(), optimizations = resolveOptimizations({}, {}).ios, onNote = (line) => console.error(line) }) {
1332
+ const settings = compilationCacheSettings({
1333
+ workspaceRoot: root,
1334
+ derivedDataPath,
1335
+ casPath,
1336
+ xcodeMajor: detectXcodeMajor(exec),
1337
+ ccache: ccacheEnabled(readPodfileProperties(root)),
1338
+ optimizations
1339
+ });
1340
+ if (settings.length > 0 && optimizations.compilationCache) {
1341
+ register({
1342
+ dir: casPath,
1343
+ name: "Xcode compilation cache",
1344
+ prune: "atomic",
1345
+ note: "shared Xcode compilation cache"
1346
+ });
1347
+ onNote(chalk.dim(phaseLine("cache", `compilation cache on (CAS at ${casPath})`)));
1348
+ }
1349
+ return settings;
1350
+ }
1351
+ function xcodebuildArgs({ project, scheme, udid = null, destination = null, configuration = "Debug", sdk = "iphonesimulator", derivedDataPath, extraArgs = [], buildSettings = [] }) {
1352
+ return [
1353
+ project.flag,
1354
+ project.path,
1355
+ "-scheme",
1356
+ scheme,
1357
+ "-configuration",
1358
+ configuration,
1359
+ "-sdk",
1360
+ sdk,
1361
+ "-destination",
1362
+ destination || `id=${udid}`,
1363
+ "-derivedDataPath",
1364
+ derivedDataPath,
1365
+ ...extraArgs,
1366
+ "build",
1367
+ ...buildSettings
1368
+ ];
1369
+ }
1370
+ function productsDir(derivedDataPath, { configuration = "Debug", sdk = "iphonesimulator" } = {}) {
1371
+ return join(derivedDataPath, "Build", "Products", `${configuration}-${sdk}`);
1372
+ }
1373
+ function pickAppBundle(entries, preferredName = null) {
1374
+ const apps = (Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string" && e.endsWith(".app")).toSorted();
1375
+ if (apps.length === 0) return null;
1376
+ if (preferredName) {
1377
+ const wanted = `${preferredName}.app`;
1378
+ const exact = apps.find((a) => a === wanted);
1379
+ if (exact) return exact;
1380
+ }
1381
+ return apps[0];
1382
+ }
1383
+ function findAppBundle(dir, preferredName = null) {
1384
+ let entries;
1385
+ try {
1386
+ entries = readdirSync(dir);
1387
+ } catch {
1388
+ return null;
1389
+ }
1390
+ const app = pickAppBundle(entries, preferredName);
1391
+ return app ? join(dir, app) : null;
1392
+ }
1393
+ function parseBundleId(plistJson) {
1394
+ if (typeof plistJson !== "string") return null;
1395
+ let data;
1396
+ try {
1397
+ data = JSON.parse(plistJson);
1398
+ } catch {
1399
+ return null;
1400
+ }
1401
+ if (!data || typeof data !== "object") return null;
1402
+ const id = data.CFBundleIdentifier;
1403
+ return typeof id === "string" && id.trim() !== "" ? id.trim() : null;
1404
+ }
1405
+ function readBundleId(appPath, { exec = null } = {}) {
1406
+ const executor = exec || getExecutor();
1407
+ try {
1408
+ const id = parseBundleId(executor.runFile("plutil", [
1409
+ "-convert",
1410
+ "json",
1411
+ "-o",
1412
+ "-",
1413
+ join(appPath, "Info.plist")
1414
+ ]));
1415
+ if (id) return id;
1416
+ } catch {}
1417
+ try {
1418
+ const value = executor.runFile("defaults", [
1419
+ "read",
1420
+ join(appPath, "Info"),
1421
+ "CFBundleIdentifier"
1422
+ ]);
1423
+ const trimmed = String(value).trim();
1424
+ return trimmed === "" ? null : trimmed;
1425
+ } catch {
1426
+ return null;
1427
+ }
1428
+ }
1429
+ function parseBundleExecutable(plistJson) {
1430
+ if (typeof plistJson !== "string") return null;
1431
+ let data;
1432
+ try {
1433
+ data = JSON.parse(plistJson);
1434
+ } catch {
1435
+ return null;
1436
+ }
1437
+ if (!data || typeof data !== "object") return null;
1438
+ const name = data.CFBundleExecutable;
1439
+ return typeof name === "string" && name.trim() !== "" ? name.trim() : null;
1440
+ }
1441
+ function readBundleExecutable(appPath, { exec = null } = {}) {
1442
+ const executor = exec || getExecutor();
1443
+ try {
1444
+ const name = parseBundleExecutable(executor.runFile("plutil", [
1445
+ "-convert",
1446
+ "json",
1447
+ "-o",
1448
+ "-",
1449
+ join(appPath, "Info.plist")
1450
+ ]));
1451
+ if (name) return name;
1452
+ } catch {}
1453
+ try {
1454
+ const value = executor.runFile("defaults", [
1455
+ "read",
1456
+ join(appPath, "Info"),
1457
+ "CFBundleExecutable"
1458
+ ]);
1459
+ const trimmed = String(value).trim();
1460
+ return trimmed === "" ? null : trimmed;
1461
+ } catch {
1462
+ return null;
1463
+ }
1464
+ }
1465
+ function tailLines(lines, count = 5) {
1466
+ return (Array.isArray(lines) ? lines : []).filter((l) => typeof l === "string" && l.trim() !== "").slice(-count);
1467
+ }
1468
+ const HEARTBEAT_INTERVAL_MS = 3e4;
1469
+ const HEARTBEAT_ACTIVITY = {
1470
+ build: "compiling",
1471
+ deps: "installing",
1472
+ pods: "installing"
1473
+ };
1474
+ function heartbeatLine(elapsedMs, label = "build", estimateMs = null) {
1475
+ const activity = HEARTBEAT_ACTIVITY[label] ?? "running";
1476
+ const elapsed = formatElapsed(elapsedMs);
1477
+ const inside = estimateMs && estimateMs > 0 ? elapsedMs > estimateMs ? `${elapsed}, usually ~${formatElapsed(estimateMs)}` : `${elapsed} of ~${formatElapsed(estimateMs)}` : elapsed;
1478
+ return phaseLine(label, `still ${activity} (${inside})`);
1479
+ }
1480
+ const defaultSchedule = (run, delayMs) => {
1481
+ const timer = setTimeout(run, delayMs);
1482
+ timer.unref?.();
1483
+ return () => clearTimeout(timer);
1484
+ };
1485
+ function startBuildHeartbeat({ intervalMs, elapsed, emit, label = "build", estimateMs = null, schedule = defaultSchedule }) {
1486
+ if (!(intervalMs > 0)) return () => {};
1487
+ let stopped = false;
1488
+ let lastBeat = 0;
1489
+ let cancel = null;
1490
+ function tick() {
1491
+ if (stopped) return;
1492
+ const ms = elapsed();
1493
+ const beat = Math.floor(ms / intervalMs) * intervalMs;
1494
+ if (beat > lastBeat) {
1495
+ lastBeat = beat;
1496
+ emit(heartbeatLine(beat, label, estimateMs));
1497
+ }
1498
+ cancel = schedule(tick, intervalMs - ms % intervalMs);
1499
+ }
1500
+ cancel = schedule(tick, intervalMs);
1501
+ return () => {
1502
+ stopped = true;
1503
+ cancel?.();
1504
+ };
1505
+ }
1506
+ function failedResult({ code, diagnostics, durationMs, exitCode = null, transcriptLines = 0, tail = [], compilationCache = COMPILATION_CACHE_UNAVAILABLE }) {
1507
+ const capped = capDiagnostics(diagnostics);
1508
+ return {
1509
+ failed: true,
1510
+ code,
1511
+ diagnostics: capped.diagnostics,
1512
+ truncated: capped.truncated,
1513
+ durationMs,
1514
+ exitCode,
1515
+ transcriptLines,
1516
+ tail,
1517
+ compilationCache
1518
+ };
1519
+ }
1520
+ const COMPILATION_CACHE_UNAVAILABLE = {
1521
+ status: "unavailable",
1522
+ hits: null,
1523
+ cacheableTasks: null,
1524
+ hitRatePercent: null
1525
+ };
1526
+ const COMPILATION_CACHE_NOT_RUN = {
1527
+ status: "not-run",
1528
+ hits: null,
1529
+ cacheableTasks: null,
1530
+ hitRatePercent: null
1531
+ };
1532
+ function parseCompilationCacheActivity(line) {
1533
+ if (typeof line !== "string") return null;
1534
+ const match = line.match(/(?:note:\s*)?(\d+) hits \/ (\d+) cacheable tasks \((\d+(?:\.\d+)?)%\)/);
1535
+ if (!match) return null;
1536
+ return {
1537
+ status: "reported",
1538
+ hits: Number(match[1]),
1539
+ cacheableTasks: Number(match[2]),
1540
+ hitRatePercent: Number(match[3])
1541
+ };
1542
+ }
1543
+ function compilationCacheActivityLine(activity) {
1544
+ if (activity.status === "reported") return `${activity.hits}/${activity.cacheableTasks} hits (${activity.hitRatePercent}%)`;
1545
+ if (activity.status === "not-run") return "not run; artifact cache supplied the app";
1546
+ return "unavailable; Xcode did not report reliable statistics";
1547
+ }
1548
+ async function buildIos({ root, udid = null, logWriter, project = null, scheme = null, configuration = "Debug", sdk = "iphonesimulator", destination = null, derivedDataPath = null, extraArgs = [], compilationCache = void 0, optimizations = resolveOptimizations({}, {}).ios, now = () => Date.now(), exec = null, heartbeatMs = HEARTBEAT_INTERVAL_MS, estimateMs = null, onHeartbeat = (line) => console.error(line), onNote = (line) => console.error(line) }) {
1549
+ if (!root || typeof root !== "string") throw new TypeError("buildIos requires {root}");
1550
+ if (!logWriter || typeof logWriter.write !== "function") throw new TypeError("buildIos requires {logWriter} with a write() method");
1551
+ if (!udid && !destination) throw new TypeError("buildIos requires {udid} (or an explicit {destination})");
1552
+ const executor = exec || getExecutor();
1553
+ const dd = derivedDataPath || (scheme ? join(workspaceDerivedData(root), `scheme-${createHash("sha256").update(scheme).digest("hex")}`) : workspaceDerivedData(root));
1554
+ const startedAt = now();
1555
+ const elapsed = () => now() - startedAt;
1556
+ const reportError = (message, remedy) => {
1557
+ logWriter.write({
1558
+ src: "build",
1559
+ level: "error",
1560
+ msg: message,
1561
+ event: "build_diagnostic",
1562
+ ...remedy ? { remedy } : {}
1563
+ });
1564
+ };
1565
+ let target = project;
1566
+ if (!target) {
1567
+ const discovered = discoverXcodeProject(root);
1568
+ if (discovered.error) {
1569
+ reportError(discovered.error.message, discovered.error.remedy);
1570
+ return failedResult({
1571
+ code: discovered.error.code,
1572
+ diagnostics: [{
1573
+ message: discovered.error.message,
1574
+ remedy: discovered.error.remedy ?? void 0
1575
+ }],
1576
+ durationMs: elapsed()
1577
+ });
1578
+ }
1579
+ target = discovered;
1580
+ }
1581
+ const resolvedTarget = target;
1582
+ const selected = resolveScheme(resolvedTarget, {
1583
+ exec: executor,
1584
+ scheme
1585
+ });
1586
+ if (selected.error) {
1587
+ reportError(selected.error.message, selected.error.remedy);
1588
+ return failedResult({
1589
+ code: selected.error.code,
1590
+ diagnostics: [{
1591
+ message: selected.error.message,
1592
+ remedy: selected.error.remedy
1593
+ }],
1594
+ durationMs: elapsed()
1595
+ });
1596
+ }
1597
+ const buildScheme = selected.scheme;
1598
+ const buildSettings = compilationCache === void 0 ? resolveCompilationCacheSettings({
1599
+ root,
1600
+ derivedDataPath: dd,
1601
+ exec: executor,
1602
+ onNote,
1603
+ optimizations
1604
+ }) : compilationCache || [];
1605
+ const args = xcodebuildArgs({
1606
+ project: resolvedTarget,
1607
+ scheme: buildScheme,
1608
+ udid,
1609
+ destination,
1610
+ configuration,
1611
+ sdk,
1612
+ derivedDataPath: dd,
1613
+ extraArgs,
1614
+ buildSettings
1615
+ });
1616
+ logWriter.write({
1617
+ src: "build",
1618
+ level: "info",
1619
+ msg: `xcodebuild ${args.join(" ")}`,
1620
+ event: "build_start"
1621
+ });
1622
+ const transcript = [];
1623
+ let compilationCacheActivity = COMPILATION_CACHE_UNAVAILABLE;
1624
+ const onLine = (line) => {
1625
+ const msg = cleanLine(line);
1626
+ transcript.push(msg);
1627
+ if (msg.trim() === "") return;
1628
+ logWriter.write({
1629
+ src: "build",
1630
+ level: "debug",
1631
+ msg
1632
+ });
1633
+ const activity = parseCompilationCacheActivity(msg);
1634
+ if (activity) {
1635
+ compilationCacheActivity = activity;
1636
+ logWriter.write({
1637
+ src: "build",
1638
+ level: "info",
1639
+ msg: compilationCacheActivityLine(activity),
1640
+ event: "compilation_cache",
1641
+ hits: activity.hits,
1642
+ cacheableTasks: activity.cacheableTasks,
1643
+ hitRatePercent: activity.hitRatePercent
1644
+ });
1645
+ }
1646
+ };
1647
+ let child;
1648
+ try {
1649
+ child = executor.spawn("xcodebuild", args, {
1650
+ cwd: resolvedTarget.dir || root,
1651
+ stdio: [
1652
+ "ignore",
1653
+ "pipe",
1654
+ "pipe"
1655
+ ],
1656
+ detached: false,
1657
+ env: {
1658
+ ...process.env,
1659
+ NSUnbufferedIO: "YES",
1660
+ FORCE_COLOR: "0"
1661
+ }
1662
+ });
1663
+ } catch (err) {
1664
+ const message = `Could not run xcodebuild: ${err.message}`;
1665
+ const remedy = "Install Xcode and select it with `sudo xcode-select -s /Applications/Xcode.app`.";
1666
+ reportError(message, remedy);
1667
+ return failedResult({
1668
+ code: "STIM_BUILD_FAILED",
1669
+ diagnostics: [{
1670
+ message,
1671
+ remedy
1672
+ }],
1673
+ durationMs: elapsed()
1674
+ });
1675
+ }
1676
+ const outReader = createLineReader(onLine);
1677
+ const errReader = createLineReader(onLine);
1678
+ child.stdout?.setEncoding?.("utf-8");
1679
+ child.stderr?.setEncoding?.("utf-8");
1680
+ child.stdout?.on("data", (chunk) => outReader.push(chunk));
1681
+ child.stderr?.on("data", (chunk) => errReader.push(chunk));
1682
+ const stopHeartbeat = startBuildHeartbeat({
1683
+ intervalMs: heartbeatMs,
1684
+ elapsed,
1685
+ emit: onHeartbeat,
1686
+ estimateMs
1687
+ });
1688
+ const outcome = await new Promise((settlePromise) => {
1689
+ let settled = false;
1690
+ const finish = (value) => {
1691
+ if (settled) return;
1692
+ settled = true;
1693
+ outReader.flush();
1694
+ errReader.flush();
1695
+ settlePromise(value);
1696
+ };
1697
+ child.on("close", (code, signal) => finish({
1698
+ code,
1699
+ signal
1700
+ }));
1701
+ child.on("error", (error) => finish({
1702
+ code: null,
1703
+ signal: null,
1704
+ error
1705
+ }));
1706
+ });
1707
+ stopHeartbeat();
1708
+ const durationMs = elapsed();
1709
+ const text = transcript.join("\n");
1710
+ if (outcome.error) {
1711
+ const message = `Could not run xcodebuild: ${outcome.error.message}`;
1712
+ const remedy = "Install Xcode and select it with `sudo xcode-select -s /Applications/Xcode.app`.";
1713
+ reportError(message, remedy);
1714
+ return failedResult({
1715
+ code: "STIM_BUILD_FAILED",
1716
+ diagnostics: [{
1717
+ message,
1718
+ remedy
1719
+ }],
1720
+ durationMs,
1721
+ transcriptLines: transcript.length,
1722
+ tail: tailLines(transcript),
1723
+ compilationCache: compilationCacheActivity
1724
+ });
1725
+ }
1726
+ if (outcome.code !== 0) {
1727
+ const diagnostics = extractXcodeDiagnostics(text, root, sdk);
1728
+ const capped = capDiagnostics(diagnostics);
1729
+ for (const d of capped.diagnostics) reportError(describeDiagnostic(d), d.remedy);
1730
+ if (capped.diagnostics.length === 0) reportError(`xcodebuild exited ${outcome.code ?? `on ${outcome.signal}`} with no recognizable diagnostic; see the transcript above.`, null);
1731
+ return failedResult({
1732
+ code: "STIM_BUILD_FAILED",
1733
+ diagnostics,
1734
+ durationMs,
1735
+ exitCode: outcome.code,
1736
+ transcriptLines: transcript.length,
1737
+ tail: tailLines(transcript),
1738
+ compilationCache: compilationCacheActivity
1739
+ });
1740
+ }
1741
+ const products = productsDir(dd, {
1742
+ configuration,
1743
+ sdk
1744
+ });
1745
+ let appPath = null;
1746
+ if (scheme) try {
1747
+ const targets = JSON.parse(executor.runFile("xcodebuild", [
1748
+ resolvedTarget.flag,
1749
+ resolvedTarget.path,
1750
+ "-scheme",
1751
+ buildScheme,
1752
+ "-configuration",
1753
+ configuration,
1754
+ "-sdk",
1755
+ sdk,
1756
+ "-destination",
1757
+ destination || `id=${udid}`,
1758
+ "-derivedDataPath",
1759
+ dd,
1760
+ ...extraArgs,
1761
+ ...buildSettings,
1762
+ "-showBuildSettings",
1763
+ "-json"
1764
+ ], { timeoutMs: 3e4 }));
1765
+ const paths = /* @__PURE__ */ new Set();
1766
+ for (const item of Array.isArray(targets) ? targets : []) {
1767
+ const settings = item?.buildSettings;
1768
+ if (settings?.PRODUCT_TYPE !== "com.apple.product-type.application" || settings.PLATFORM_NAME !== sdk) continue;
1769
+ const dir = settings.TARGET_BUILD_DIR;
1770
+ const name = settings.FULL_PRODUCT_NAME;
1771
+ if (typeof dir !== "string" || !isAbsolute(dir) || typeof name !== "string" || basename(name) !== name || !name.endsWith(".app")) continue;
1772
+ const path = join(dir, name);
1773
+ if (statSync(path).isDirectory()) paths.add(path);
1774
+ }
1775
+ if (paths.size === 1) appPath = [...paths][0];
1776
+ } catch {}
1777
+ else appPath = findAppBundle(products, buildScheme);
1778
+ if (!appPath) {
1779
+ const message = scheme ? `xcodebuild succeeded, but the built app for scheme ${JSON.stringify(buildScheme)} could not be identified unambiguously from its build settings.` : `xcodebuild reported success but no .app is in ${products}.`;
1780
+ const remedy = "Check that the scheme builds one application target for this platform, not a library, test bundle, or several apps.";
1781
+ reportError(message, remedy);
1782
+ return failedResult({
1783
+ code: "STIM_BUILD_FAILED",
1784
+ diagnostics: [{
1785
+ message,
1786
+ remedy
1787
+ }],
1788
+ durationMs,
1789
+ exitCode: 0,
1790
+ transcriptLines: transcript.length,
1791
+ tail: tailLines(transcript),
1792
+ compilationCache: compilationCacheActivity
1793
+ });
1794
+ }
1795
+ const bundleId = readBundleId(appPath, { exec: executor });
1796
+ if (!bundleId) {
1797
+ const message = `No readable CFBundleIdentifier in ${join(appPath, "Info.plist")}.`;
1798
+ const remedy = "Check PRODUCT_BUNDLE_IDENTIFIER in the target's Debug configuration.";
1799
+ reportError(message, remedy);
1800
+ return failedResult({
1801
+ code: "STIM_BUILD_FAILED",
1802
+ diagnostics: [{
1803
+ message,
1804
+ remedy
1805
+ }],
1806
+ durationMs,
1807
+ exitCode: 0,
1808
+ transcriptLines: transcript.length,
1809
+ tail: tailLines(transcript),
1810
+ compilationCache: compilationCacheActivity
1811
+ });
1812
+ }
1813
+ logWriter.write({
1814
+ src: "build",
1815
+ level: "info",
1816
+ msg: `BUILD SUCCEEDED ${appPath} (${bundleId}) in ${durationMs}ms`,
1817
+ event: "build_done"
1818
+ });
1819
+ return {
1820
+ appPath,
1821
+ bundleId,
1822
+ durationMs,
1823
+ scheme: buildScheme,
1824
+ project: resolvedTarget,
1825
+ derivedDataPath: dd,
1826
+ productsDir: products,
1827
+ transcriptLines: transcript.length,
1828
+ compilationCache: compilationCacheActivity
1829
+ };
1830
+ }
1831
+ //#endregion
1832
+ export { unknownAndroidSystemImageRefusal as C, simslimIsOnPath as E, liveOwnedDeviceCount as S, unknownIosRuntimeRefusal as T, AvdRecoveryError as _, ccacheEnabled as a, ensureBooted as b, discoverXcodeProject as c, resolveScheme as d, startBuildHeartbeat as f, podInstallCommand as g, bundlerPin as h, buildIos as i, readBundleExecutable as l, describeDiagnostic as m, COMPILATION_CACHE_UNAVAILABLE as n, compilationCacheActivityLine as o, tailLines as p, HEARTBEAT_INTERVAL_MS as r, detectXcodeMajor as s, COMPILATION_CACHE_NOT_RUN as t, readBundleId as u, checkDeviceCapacity as v, unknownIosDeviceTypeRefusal as w, ensureOwnedDevice as x, clearIosAdoptionPending as y };