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,2733 @@
1
+ import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
+ import { S as upsertProject, f as getProject, l as getConcurrencyLimits } from "./config-CMtIaTk9.mjs";
3
+ import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
4
+ import { D as workspaceLogsDir, T as workspaceDir, _ as writeWorkspaceState, g as writeWorkspaceLaunch, n as MODE_EXPO, p as readWorkspaceState, t as MODE_BARE } from "./state-CZ-ER-8d.mjs";
5
+ import { C as removeTemporaryEntry, S as makeTemporaryDirectory, g as repoRoot, s as gitCommonDir } from "./worktree-C79xx_kM.mjs";
6
+ import { a as findProjectRoot, c as projectShortcut, i as detectIsExpo, r as detectBundleId, t as appProjectProblem } from "./project-BBTVABUz.mjs";
7
+ import { i as resolveProjectMetro, r as pidExists } from "./metro-COslRK_F.mjs";
8
+ import { n as verifyCollectorOwnership } from "./ownership-CGPart6T.mjs";
9
+ import { a as easAuthNote, c as loadProjectProvider, d as resolveEasCliBin, f as resolveRemote, i as checkEasAuth, n as UPLOAD_TIMEOUT_MS, o as exitAfterFlush, p as uploadRemote, r as cacheLevel, s as isEasAuthFailureText, t as RESOLVE_TIMEOUT_MS, v as POOL_SETTING_REMEDY, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.mjs";
10
+ import { A as resolveSettings, B as artifactCachePolicy, D as remoteDeviceSettingError, F as tunnelModeSetting, H as optimizationBuildProfile, I as unknownSettingKeys, N as settingShapeErrors, O as remoteIosSetting, R as detectProviders, T as publicUrlSetting, U as resolveOptimizations, _ as iosSigningIdentitySha1SettingError, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, f as iosLanHostSetting, g as iosSigningIdentitySha1Setting, h as iosSigningIdentitySettingError, k as resolveCacheProviderConfig, m as iosSigningIdentitySetting, p as iosLanHostSettingError, r as REMOTE_DEVICE_BACKENDS, x as metroWarmupUrlSetting } from "./settings-DwU1_U_q.mjs";
11
+ import { _ as launchErrorPreview, c as launchErrorReport, g as stepTimer, h as stepClock, i as formatDuration, l as phaseLine, m as shortUdid, n as appReadinessMessage, p as shortHash } from "./command-output-8wHMEde0.mjs";
12
+ import { C as launchIosApp, E as unverifiedLaunchLines, O as verifyLaunch, T as readMetroRecords, b as isBundleProof, c as RELEASE_VERIFY_WAIT_MS, f as clearOtherUserApps, i as LAUNCH_BUNDLING, k as verifyReleaseLaunch, m as devClientUrl, n as DEFAULT_METRO_PORT, o as LAUNCH_FATAL, s as LAUNCH_UNVERIFIED, v as installIosApp, w as readCollectorRecords, y as iosAppProcess } from "./app-install-NFxUahnH.mjs";
13
+ import { f as listIosRuntimes } from "./ios-D1h1Di39.mjs";
14
+ import { C as untrackedNativeFiles, S as untrackedMissLine, _ as providerUploadOutcome, b as storeBuild, c as buildCacheKey, d as filesystemBuildCapability, f as fingerprintDiffRecord, g as providerDownloadPath, h as prepareProviderDownloadDir, i as releaseBuildLock, l as describeFingerprintMiss, m as fingerprintProject, n as acquireBuildLock, o as takeoverLine, p as fingerprintDiffSuffix, s as waitForBuild, t as WAIT_CEILING_MS, v as refingerprintAfterMutation, y as resolveBuild } from "./build-lock-BsVoLDaq.mjs";
15
+ import { r as claimFailure } from "./ownership-claim-7zRxxTfZ.mjs";
16
+ import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-B5pEVa3t.mjs";
17
+ import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-D0b6sJyo.mjs";
18
+ import { a as swapJsBundle, c as ensureWorkspaceStorageSafely, d as noMetroMessage, f as noMetroRemedy, h as warmMetro, l as gateShouldRetry, m as sleep, n as pickDevClientScheme, o as needsPrebuild, p as resolveMetroWithRetry, r as schemesFromInfoPlist, s as runPrebuild, t as devClientScheme, u as launchOutcomeRecord } from "./dev-client-RpvXxCXV.mjs";
19
+ import { a as statsProjectKey, i as recordRunStats, n as readRunEstimates, t as createRunRecorder } from "./stats-BCWEVLrM.mjs";
20
+ import { T as unknownIosRuntimeRefusal, b as ensureBooted, c as discoverXcodeProject, d as resolveScheme, i as buildIos, l as readBundleExecutable, m as describeDiagnostic, n as COMPILATION_CACHE_UNAVAILABLE, o as compilationCacheActivityLine, t as COMPILATION_CACHE_NOT_RUN, u as readBundleId, v as checkDeviceCapacity, w as unknownIosDeviceTypeRefusal, x as ensureOwnedDevice, y as clearIosAdoptionPending } from "./xcode-DeKCribl.mjs";
21
+ import { a as ensureMetroReachable, b as ownedSessionName, c as remoteIosDeps, l as resolveRemoteContext, n as binOnPath, o as ensureRemoteBootOwned, r as bundleEntryPoint, w as gateMetroOrigin } from "./device-remote-E81KAdXf.mjs";
22
+ import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
23
+ import { a as printNativeCrashReport, n as IOS_CRASH_REPORT_RETRY, o as simulatorConsolePaths, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-Dw8Gn3yw.mjs";
24
+ import { a as iosDeviceProcess, c as listIosDevices, d as verifyIosDeviceReleaseLaunch, i as installIosDeviceApp, l as localNetworkPending, n as LAUNCH_PROBE_TIMEOUT_MS, o as iosPoolCandidates, r as awaitIosDeviceLaunch, s as iosPoolNoCandidatesRefusal, t as DEVICECTL_INSTALL_TIMEOUT_MS, u as resolveIosPhysicalDevice } from "./ios-device-xNt0Lg6v.mjs";
25
+ import { a as runPodInstall, n as podsAreStale, r as readPodState } from "./deps-BLBsTNDS.mjs";
26
+ import { existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeFileSync } from "node:fs";
27
+ import { InvalidArgumentError } from "commander";
28
+ import { X509Certificate } from "node:crypto";
29
+ import { basename, join } from "node:path";
30
+ import { networkInterfaces, tmpdir } from "node:os";
31
+ import chalk from "chalk";
32
+ import { createWarnOnce, loadCacheProvider, resolveTieredBuild, storeTieredBuild } from "@stim-cli/cache";
33
+ //#region src/engine/ios-profile.ts
34
+ const TAG = /<(\/?)\s*([A-Za-z0-9_]+)(?:\s[^>]*?)?(\/?)>/g;
35
+ const ENTITIES = {
36
+ amp: "&",
37
+ lt: "<",
38
+ gt: ">",
39
+ quot: "\"",
40
+ apos: "'"
41
+ };
42
+ var PlistError = class extends Error {};
43
+ function codePoint(value) {
44
+ if (!Number.isInteger(value) || value < 0 || value > 1114111 || value >= 55296 && value <= 57343) throw new PlistError(`character reference out of range: ${value}`);
45
+ return String.fromCodePoint(value);
46
+ }
47
+ function decodeEntities(raw) {
48
+ return raw.replace(/&(#x?[0-9A-Fa-f]+|[a-z]+);/g, (match, body) => {
49
+ if (body.startsWith("#x") || body.startsWith("#X")) return codePoint(Number.parseInt(body.slice(2), 16));
50
+ if (body.startsWith("#")) return codePoint(Number.parseInt(body.slice(1), 10));
51
+ return ENTITIES[body] ?? match;
52
+ });
53
+ }
54
+ function peekTag(cursor) {
55
+ TAG.lastIndex = cursor.pos;
56
+ const match = TAG.exec(cursor.xml);
57
+ if (!match) return null;
58
+ return {
59
+ name: (match[2] ?? "").toLowerCase(),
60
+ closing: match[1] === "/",
61
+ selfClosing: match[3] === "/",
62
+ end: TAG.lastIndex
63
+ };
64
+ }
65
+ function takeTag(cursor) {
66
+ const tag = peekTag(cursor);
67
+ if (tag) cursor.pos = tag.end;
68
+ return tag;
69
+ }
70
+ function takeText(cursor, name) {
71
+ const close = cursor.xml.indexOf(`</${name}>`, cursor.pos);
72
+ if (close < 0) {
73
+ cursor.pos = cursor.xml.length;
74
+ return "";
75
+ }
76
+ const raw = cursor.xml.slice(cursor.pos, close);
77
+ cursor.pos = close + name.length + 3;
78
+ return raw;
79
+ }
80
+ function parseValue(cursor) {
81
+ const peeked = peekTag(cursor);
82
+ if (!peeked || peeked.closing) return void 0;
83
+ const tag = takeTag(cursor);
84
+ if (tag.selfClosing) {
85
+ if (tag.name === "true") return true;
86
+ if (tag.name === "false") return false;
87
+ if (tag.name === "array") return [];
88
+ if (tag.name === "dict") return {};
89
+ return "";
90
+ }
91
+ switch (tag.name) {
92
+ case "plist": return parseValue(cursor);
93
+ case "dict": {
94
+ const out = {};
95
+ for (;;) {
96
+ const next = peekTag(cursor);
97
+ if (!next) break;
98
+ if (next.closing) {
99
+ cursor.pos = next.end;
100
+ break;
101
+ }
102
+ cursor.pos = next.end;
103
+ if (next.name !== "key") continue;
104
+ const key = decodeEntities(next.selfClosing ? "" : takeText(cursor, "key"));
105
+ const value = parseValue(cursor);
106
+ if (value !== void 0) out[key] = value;
107
+ }
108
+ return out;
109
+ }
110
+ case "array": {
111
+ const out = [];
112
+ for (;;) {
113
+ const next = peekTag(cursor);
114
+ if (!next) break;
115
+ if (next.closing) {
116
+ cursor.pos = next.end;
117
+ break;
118
+ }
119
+ const value = parseValue(cursor);
120
+ if (value === void 0) break;
121
+ out.push(value);
122
+ }
123
+ return out;
124
+ }
125
+ case "string":
126
+ case "key": return decodeEntities(takeText(cursor, tag.name));
127
+ case "integer": return Number.parseInt(takeText(cursor, "integer").trim(), 10);
128
+ case "real": return Number.parseFloat(takeText(cursor, "real").trim());
129
+ case "date": return new Date(takeText(cursor, "date").trim());
130
+ case "data": return Buffer.from(takeText(cursor, "data").replace(/\s+/g, ""), "base64");
131
+ case "true":
132
+ takeText(cursor, "true");
133
+ return true;
134
+ case "false":
135
+ takeText(cursor, "false");
136
+ return false;
137
+ default:
138
+ takeText(cursor, tag.name);
139
+ return "";
140
+ }
141
+ }
142
+ function parsePlist(xml) {
143
+ if (typeof xml !== "string" || xml.trim() === "") return null;
144
+ const stripped = xml.replace(/<!--[\s\S]*?-->/g, "");
145
+ const start = stripped.indexOf("<plist");
146
+ const cursor = {
147
+ xml: stripped,
148
+ pos: start < 0 ? 0 : start
149
+ };
150
+ try {
151
+ const value = parseValue(cursor);
152
+ return value === void 0 ? null : value;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ function isDict(value) {
158
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !Buffer.isBuffer(value) && !(value instanceof Date);
159
+ }
160
+ function stringsOf(value) {
161
+ if (!Array.isArray(value)) return null;
162
+ return value.filter((v) => typeof v === "string");
163
+ }
164
+ function parseProvisioningProfilePlist(xml) {
165
+ const root = parsePlist(xml);
166
+ if (!isDict(root)) return null;
167
+ const entitlements = isDict(root.Entitlements ?? null) ? root.Entitlements : {};
168
+ const teamIdentifier = stringsOf(root.TeamIdentifier);
169
+ const expiration = root.ExpirationDate;
170
+ const certificates = Array.isArray(root.DeveloperCertificates) ? root.DeveloperCertificates.filter((v) => Buffer.isBuffer(v)) : [];
171
+ return {
172
+ name: typeof root.Name === "string" ? root.Name : null,
173
+ uuid: typeof root.UUID === "string" ? root.UUID : null,
174
+ teamIdentifier: teamIdentifier?.[0] ?? null,
175
+ expirationDate: expiration instanceof Date && !Number.isNaN(expiration.getTime()) ? expiration : null,
176
+ provisionedDevices: stringsOf(root.ProvisionedDevices),
177
+ provisionsAllDevices: root.ProvisionsAllDevices === true,
178
+ getTaskAllow: entitlements["get-task-allow"] === true,
179
+ certificates
180
+ };
181
+ }
182
+ function provisioningProfileKind(profile) {
183
+ if (profile.provisionedDevices) return profile.getTaskAllow ? "development" : "ad hoc";
184
+ return profile.provisionsAllDevices ? "enterprise" : "App Store";
185
+ }
186
+ function certificateCommonName(der) {
187
+ if (!Buffer.isBuffer(der) || der.length === 0) return null;
188
+ let subject;
189
+ try {
190
+ subject = new X509Certificate(der).subject;
191
+ } catch {
192
+ return null;
193
+ }
194
+ for (const part of String(subject).split(/\r?\n/)) {
195
+ const trimmed = part.trim();
196
+ if (trimmed.startsWith("CN=")) return trimmed.slice(3).trim() || null;
197
+ }
198
+ return /(?:^|,)\s*CN=([^,]+)/.exec(String(subject))?.[1]?.trim() || null;
199
+ }
200
+ const IDENTITY_LINE = /^\s*\d+\)\s+([0-9A-Fa-f]{40})\s+"(.+)"\s*$/;
201
+ function parseSigningIdentities(text) {
202
+ if (typeof text !== "string") return [];
203
+ const out = [];
204
+ for (const line of text.split(/\r?\n/)) {
205
+ const match = IDENTITY_LINE.exec(line);
206
+ if (!match) continue;
207
+ out.push({
208
+ sha1: match[1].toUpperCase(),
209
+ name: match[2]
210
+ });
211
+ }
212
+ return out;
213
+ }
214
+ const SIGNING_CODES = {
215
+ noProfile: "STIM_NO_PROFILE",
216
+ profileMismatch: "STIM_PROFILE_MISMATCH",
217
+ noIdentity: "STIM_NO_SIGNING_IDENTITY",
218
+ codesignFailed: "STIM_CODESIGN_FAILED"
219
+ };
220
+ const XCODE_STEP = "build once from Xcode to install it -- Stim will not, because registering a device or minting a profile changes your Apple Developer account.";
221
+ const KEYCHAIN_REMEDY = "Open Xcode > Settings > Accounts and download your certificates, or unlock the login keychain with `security unlock-keychain`. `security find-identity -v -p codesigning` should list `Apple Development: ...`.";
222
+ function signingConfiguration(input) {
223
+ return input.configuration ?? "Debug";
224
+ }
225
+ function profileGate(input) {
226
+ const now = input.now ?? Date.now();
227
+ if (!input.profilePresent) return {
228
+ ok: false,
229
+ code: SIGNING_CODES.noProfile,
230
+ reason: "The app bundle has no embedded.mobileprovision, so it was built unsigned or for the simulator.",
231
+ remedy: `Set a team and profile for the target's ${signingConfiguration(input)} configuration in Xcode > Signing & Capabilities, then ${XCODE_STEP}`
232
+ };
233
+ if (!input.profile) return {
234
+ ok: false,
235
+ code: SIGNING_CODES.noProfile,
236
+ reason: "`security cms -D` could not decode the app bundle's embedded.mobileprovision.",
237
+ remedy: `The profile inside the app is corrupt. Delete the build cache entry with \`stim gc --delete\`, then ${XCODE_STEP}`
238
+ };
239
+ const profile = input.profile;
240
+ const named = profile.name ? `"${profile.name}"` : "the embedded profile";
241
+ if (!profile.expirationDate || profile.expirationDate.getTime() <= now) return {
242
+ ok: false,
243
+ code: SIGNING_CODES.profileMismatch,
244
+ reason: profile.expirationDate ? `The ${provisioningProfileKind(profile)} profile ${named} expired on ${profile.expirationDate.toISOString()}.` : `The ${provisioningProfileKind(profile)} profile ${named} carries no ExpirationDate, so Stim cannot prove it is still valid.`,
245
+ remedy: `Renew the profile under Signing & Capabilities, then ${XCODE_STEP}`
246
+ };
247
+ if (!profile.provisionedDevices) {
248
+ const kind = provisioningProfileKind(profile);
249
+ return {
250
+ ok: false,
251
+ code: SIGNING_CODES.profileMismatch,
252
+ reason: `${named} is an ${kind} profile, which carries no ProvisionedDevices list, so Stim cannot prove ${input.udid ?? "the target device"} is admitted.`,
253
+ remedy: `Local device runs need a development profile. Select one under Signing & Capabilities, then ${XCODE_STEP}`
254
+ };
255
+ }
256
+ const udid = input.udid;
257
+ if (!udid || !profile.provisionedDevices.some((d) => d.toLowerCase() === udid.toLowerCase())) return {
258
+ ok: false,
259
+ code: SIGNING_CODES.profileMismatch,
260
+ reason: `The ${provisioningProfileKind(profile)} profile ${named} lists ${profile.provisionedDevices.length} device(s) and ${udid ?? "the target device"} is not one of them.`,
261
+ remedy: `Register ${udid ?? "the device"} at developer.apple.com, regenerate the profile, then ${XCODE_STEP}`
262
+ };
263
+ return {
264
+ ok: true,
265
+ profile
266
+ };
267
+ }
268
+ function signingGate(input) {
269
+ const gated = profileGate(input);
270
+ if (!gated.ok) return gated;
271
+ const profile = gated.profile;
272
+ const named = profile.name ? `"${profile.name}"` : "the embedded profile";
273
+ const identities = Array.isArray(input.identities) ? input.identities : [];
274
+ if (identities.length === 0) return {
275
+ ok: false,
276
+ code: SIGNING_CODES.noIdentity,
277
+ reason: "`security find-identity -v -p codesigning` lists no codesigning identity on this machine.",
278
+ remedy: KEYCHAIN_REMEDY
279
+ };
280
+ if (input.pinnedSha1) {
281
+ const wanted = input.pinnedSha1.trim().toUpperCase();
282
+ const match = identities.find((i) => i.sha1 === wanted);
283
+ if (match) return {
284
+ ok: true,
285
+ identity: match
286
+ };
287
+ return {
288
+ ok: false,
289
+ code: SIGNING_CODES.noIdentity,
290
+ reason: `ios.signingIdentitySha1 is ${wanted}, which is not in the keychain. Available: ${identities.map((i) => `${i.sha1} "${i.name}"`).join(", ")}.`,
291
+ remedy: KEYCHAIN_REMEDY
292
+ };
293
+ }
294
+ const wantedName = input.pinnedName?.trim() || certificateCommonName(profile.certificates[0] ?? null);
295
+ if (!wantedName) return {
296
+ ok: false,
297
+ code: SIGNING_CODES.noIdentity,
298
+ reason: `Stim could not read a signing-identity name out of ${named}: its DeveloperCertificates entry has no readable subject CN.`,
299
+ remedy: "Set ios.signingIdentity in .stim.json to the identity name to re-sign with, or ios.signingIdentitySha1 to its SHA-1 hash."
300
+ };
301
+ const matches = identities.filter((i) => i.name === wantedName);
302
+ if (matches.length === 1) return {
303
+ ok: true,
304
+ identity: matches[0]
305
+ };
306
+ if (matches.length === 0) return {
307
+ ok: false,
308
+ code: SIGNING_CODES.noIdentity,
309
+ reason: `The app was signed by "${wantedName}", which is not in this machine's keychain. Available: ${identities.map((i) => `"${i.name}"`).join(", ")}.`,
310
+ remedy: `${KEYCHAIN_REMEDY} Or set ios.signingIdentity in .stim.json to one of the identities listed above.`
311
+ };
312
+ return {
313
+ ok: false,
314
+ code: SIGNING_CODES.noIdentity,
315
+ reason: `${matches.length} keychain identities share the name "${wantedName}", so Stim cannot pick one: ${matches.map((i) => i.sha1).join(", ")}.`,
316
+ remedy: "Set ios.signingIdentitySha1 in .stim.json to the SHA-1 hash of the certificate to sign with."
317
+ };
318
+ }
319
+ //#endregion
320
+ //#region src/engine/ios-signing.ts
321
+ const EMBEDDED_PROFILE = "embedded.mobileprovision";
322
+ const SIGNING_TIMEOUT_MS = 12e4;
323
+ function lastLines(text, count = 5) {
324
+ return String(text ?? "").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim() !== "").slice(-count);
325
+ }
326
+ function errorText(error) {
327
+ const withOutput = error;
328
+ const stderr = String(withOutput?.stderr ?? "").trim();
329
+ const stdout = String(withOutput?.stdout ?? "").trim();
330
+ return stderr || stdout || String(withOutput?.message ?? error);
331
+ }
332
+ function readEmbeddedProfile(appPath, { exec = null } = {}) {
333
+ const executor = exec || getExecutor();
334
+ const path = join(appPath, EMBEDDED_PROFILE);
335
+ if (!existsSync(path)) return {
336
+ present: false,
337
+ profile: null
338
+ };
339
+ try {
340
+ return {
341
+ present: true,
342
+ profile: parseProvisioningProfilePlist(executor.runFile("security", [
343
+ "cms",
344
+ "-D",
345
+ "-i",
346
+ path
347
+ ], { timeoutMs: SIGNING_TIMEOUT_MS }))
348
+ };
349
+ } catch {
350
+ return {
351
+ present: true,
352
+ profile: null
353
+ };
354
+ }
355
+ }
356
+ function findSigningIdentities({ exec = null } = {}) {
357
+ const executor = exec || getExecutor();
358
+ try {
359
+ return parseSigningIdentities(executor.runFile("security", [
360
+ "find-identity",
361
+ "-v",
362
+ "-p",
363
+ "codesigning"
364
+ ], { timeoutMs: SIGNING_TIMEOUT_MS }));
365
+ } catch {
366
+ return [];
367
+ }
368
+ }
369
+ function gateProfileForDevice(options) {
370
+ const read = readEmbeddedProfile(options.appPath, { exec: options.exec ?? null });
371
+ return profileGate({
372
+ profilePresent: read.present,
373
+ profile: read.profile,
374
+ identities: [],
375
+ udid: options.udid,
376
+ configuration: options.configuration ?? null,
377
+ ...options.now === void 0 ? {} : { now: options.now }
378
+ });
379
+ }
380
+ function gateAppForDevice(options) {
381
+ const read = readEmbeddedProfile(options.appPath, { exec: options.exec ?? null });
382
+ return signingGate({
383
+ profilePresent: read.present,
384
+ profile: read.profile,
385
+ identities: read.present ? findSigningIdentities({ exec: options.exec ?? null }) : [],
386
+ udid: options.udid,
387
+ configuration: options.configuration ?? null,
388
+ pinnedName: options.pinnedName ?? null,
389
+ pinnedSha1: options.pinnedSha1 ?? null,
390
+ ...options.now === void 0 ? {} : { now: options.now }
391
+ });
392
+ }
393
+ function extractEntitlements(appPath, executor) {
394
+ try {
395
+ return String(executor.runFile("codesign", [
396
+ "-d",
397
+ "--entitlements",
398
+ "-",
399
+ "--xml",
400
+ appPath
401
+ ], { timeoutMs: SIGNING_TIMEOUT_MS })).trim();
402
+ } catch {
403
+ return "";
404
+ }
405
+ }
406
+ function resealBundle({ appPath, identity, exec = null }) {
407
+ const executor = exec || getExecutor();
408
+ const signAs = identity.sha1 || identity.name;
409
+ let mode = "preserve-metadata";
410
+ let preserveError = null;
411
+ try {
412
+ executor.runFile("codesign", [
413
+ "--force",
414
+ "--sign",
415
+ signAs,
416
+ "--preserve-metadata=identifier,entitlements,flags,runtime",
417
+ appPath
418
+ ], { timeoutMs: SIGNING_TIMEOUT_MS });
419
+ } catch (error) {
420
+ preserveError = error;
421
+ }
422
+ let tempDir = null;
423
+ if (preserveError) {
424
+ const entitlements = extractEntitlements(appPath, executor);
425
+ const args = [
426
+ "--force",
427
+ "--sign",
428
+ signAs
429
+ ];
430
+ if (entitlements !== "") {
431
+ tempDir = mkdtempSync(join(tmpdir(), "stim-entitlements-"));
432
+ const plist = join(tempDir, "entitlements.plist");
433
+ writeFileSync(plist, `${entitlements}\n`, "utf-8");
434
+ args.push("--entitlements", plist);
435
+ mode = "entitlements";
436
+ } else mode = "no-entitlements";
437
+ args.push(appPath);
438
+ try {
439
+ executor.runFile("codesign", args, { timeoutMs: SIGNING_TIMEOUT_MS });
440
+ } catch (error) {
441
+ return {
442
+ ok: false,
443
+ code: SIGNING_CODES.codesignFailed,
444
+ reason: `codesign could not re-seal ${appPath} with "${identity.name}": ${errorText(error)}`,
445
+ remedy: `Unlock the login keychain (\`security unlock-keychain\`) and confirm exactly one identity matches "${identity.name}".`,
446
+ lastLines: [...lastLines(errorText(preserveError)), ...lastLines(errorText(error))]
447
+ };
448
+ } finally {
449
+ if (tempDir) rmSync(tempDir, {
450
+ recursive: true,
451
+ force: true
452
+ });
453
+ }
454
+ }
455
+ try {
456
+ executor.runFile("codesign", [
457
+ "--verify",
458
+ "--strict",
459
+ appPath
460
+ ], { timeoutMs: SIGNING_TIMEOUT_MS });
461
+ } catch (error) {
462
+ return {
463
+ ok: false,
464
+ code: SIGNING_CODES.codesignFailed,
465
+ reason: `codesign --verify --strict rejected ${appPath} after re-sealing it with "${identity.name}": ${errorText(error)}`,
466
+ remedy: "The bundle signed but did not seal. Delete the cached artifact with `stim gc --delete` and build fresh, then retry.",
467
+ lastLines: lastLines(errorText(error))
468
+ };
469
+ }
470
+ return {
471
+ ok: true,
472
+ identity,
473
+ mode
474
+ };
475
+ }
476
+ function sealAppForDevice(options) {
477
+ const gate = gateAppForDevice(options);
478
+ if (!gate.ok) return {
479
+ ...gate,
480
+ lastLines: []
481
+ };
482
+ return resealBundle({
483
+ appPath: options.appPath,
484
+ identity: gate.identity,
485
+ exec: options.exec ?? null
486
+ });
487
+ }
488
+ //#endregion
489
+ //#region src/engine/ios-lan.ts
490
+ const IP_TXT = "ip.txt";
491
+ function chooseLanAddress({ pinned = null, candidates }) {
492
+ const list = Array.isArray(candidates) ? candidates : [];
493
+ if (pinned) return {
494
+ address: pinned,
495
+ interfaceName: list.find((candidate) => candidate.address === pinned)?.interfaceName ?? null,
496
+ pinned: true,
497
+ candidates: list.length
498
+ };
499
+ const first = list[0];
500
+ if (!first) return null;
501
+ return {
502
+ address: first.address,
503
+ interfaceName: first.interfaceName,
504
+ pinned: false,
505
+ candidates: list.length
506
+ };
507
+ }
508
+ function lanOriginUrlFor(address, port) {
509
+ return `http://${address}:${port}`;
510
+ }
511
+ function ipTxtContents(address, port) {
512
+ return `${String(address).trim()}:${String(port).trim()}\n`;
513
+ }
514
+ function writeIpTxt(appPath, address, port, { write = writeFileSync } = {}) {
515
+ const path = join(appPath, IP_TXT);
516
+ write(path, ipTxtContents(address, port), "utf-8");
517
+ return path;
518
+ }
519
+ async function ensureLanReachable({ origin, metroPort, root, isExpo, logsDir, gateOrigin = gateMetroOrigin, readRecords = null }) {
520
+ const result = await gateOrigin({
521
+ origin,
522
+ metroPort,
523
+ platform: "ios",
524
+ entryPoint: bundleEntryPoint(root, isExpo),
525
+ readRecords: readRecords ?? (() => readMetroRecords(logsDir)),
526
+ isProof: isBundleProof
527
+ });
528
+ if (result.failed) return {
529
+ failed: result.reason ?? `${origin} is not this workspace's Metro.`,
530
+ remedy: `The phone reaches Metro at ${origin}, so that address has to serve THIS workspace's dev server. \`stim start\` prints the port it reserved. Set ios.lanHost in .stim.json when this Mac has several network interfaces and the phone shares one that is not the first.`
531
+ };
532
+ return { ok: true };
533
+ }
534
+ function copyAppAside(appPath, { exec = null, mkdtemp = () => makeTemporaryDirectory(appPath, "stim-ios-device-") } = {}) {
535
+ const e = exec || getExecutor();
536
+ const tmpDir = mkdtemp();
537
+ const copy = join(tmpDir, basename(appPath));
538
+ try {
539
+ e.runFile("cp", [
540
+ "-c",
541
+ "-R",
542
+ appPath,
543
+ copy
544
+ ]);
545
+ } catch {
546
+ try {
547
+ e.runFile("cp", [
548
+ "-R",
549
+ appPath,
550
+ copy
551
+ ]);
552
+ } catch (error) {
553
+ removeTemporaryEntry(tmpDir);
554
+ throw error;
555
+ }
556
+ }
557
+ return {
558
+ tmpDir,
559
+ appPath: copy
560
+ };
561
+ }
562
+ //#endregion
563
+ //#region src/engine/lan-address.ts
564
+ const EN_INTERFACE = /^en(\d+)$/;
565
+ const EXCLUDED_INTERFACE = /^(?:utun|bridge|awdl)/;
566
+ const LINK_LOCAL = /^169\.254\./;
567
+ function isIpv4(entry) {
568
+ return entry.family === "IPv4" || entry.family === 4;
569
+ }
570
+ function lanCandidates(interfaces) {
571
+ const ranked = [];
572
+ const seen = /* @__PURE__ */ new Set();
573
+ for (const [interfaceName, entries] of Object.entries(interfaces ?? {})) {
574
+ if (EXCLUDED_INTERFACE.test(interfaceName)) continue;
575
+ const en = EN_INTERFACE.exec(interfaceName);
576
+ for (const entry of entries ?? []) {
577
+ if (!entry || typeof entry !== "object" || !isIpv4(entry) || entry.internal === true) continue;
578
+ const address = typeof entry.address === "string" ? entry.address.trim() : "";
579
+ if (address === "" || LINK_LOCAL.test(address)) continue;
580
+ if (seen.has(address)) continue;
581
+ seen.add(address);
582
+ ranked.push({
583
+ interfaceName,
584
+ address,
585
+ rank: en ? 0 : 1,
586
+ index: en ? Number(en[1]) : 0
587
+ });
588
+ }
589
+ }
590
+ ranked.sort((a, b) => a.rank - b.rank || a.index - b.index || a.interfaceName.localeCompare(b.interfaceName) || a.address.localeCompare(b.address));
591
+ return ranked.map(({ interfaceName, address }) => ({
592
+ interfaceName,
593
+ address
594
+ }));
595
+ }
596
+ function hostLanCandidates(interfaces = networkInterfaces) {
597
+ return lanCandidates(interfaces());
598
+ }
599
+ function buildLogFile(root) {
600
+ return join(workspaceLogsDir(root), `build-ios.ndjson`);
601
+ }
602
+ const MAX_PRINTED_DIAGNOSTICS = 6;
603
+ function deviceLabel(device, udid) {
604
+ const name = device?.deviceName || device?.name || null;
605
+ return name ? `${name} (${shortUdid(udid)})` : shortUdid(udid);
606
+ }
607
+ function deviceShortName(device, udid) {
608
+ return device?.deviceName || device?.name || shortUdid(udid);
609
+ }
610
+ function appNameFromPath(appPath) {
611
+ if (typeof appPath !== "string" || appPath.trim() === "") return null;
612
+ const name = basename(appPath).replace(/\.app$/i, "");
613
+ return name === "" ? null : name;
614
+ }
615
+ function iosConfigurationSetting(settings) {
616
+ const ios = settings?.["ios"];
617
+ if (!ios || typeof ios !== "object" || Array.isArray(ios)) return null;
618
+ const raw = ios["configuration"];
619
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
620
+ }
621
+ function resolveConfiguration(flag, settings) {
622
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || iosConfigurationSetting(settings);
623
+ }
624
+ function iosStringSetting(settings, key) {
625
+ const ios = settings?.["ios"];
626
+ if (!ios || typeof ios !== "object" || Array.isArray(ios)) return null;
627
+ const raw = ios[key];
628
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
629
+ }
630
+ function resolveDeviceType(flag, settings) {
631
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || iosStringSetting(settings, "deviceType");
632
+ }
633
+ function resolveRuntime(flag, settings) {
634
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || iosStringSetting(settings, "runtime");
635
+ }
636
+ function deviceModelRefusal({ deviceTypeFlag, runtimeFlag, deviceType, runtime, physical, remoteBackend, listRuntimes }) {
637
+ if (typeof deviceTypeFlag === "string" && deviceTypeFlag.trim() === "") return {
638
+ code: "STIM_BAD_ARG",
639
+ message: "--device-type was given an empty name.",
640
+ remedy: "Pass `--device-type <name>` with a model `xcrun simctl list devicetypes` names, e.g. \"iPad Pro 13-inch (M4)\"."
641
+ };
642
+ if (typeof runtimeFlag === "string" && runtimeFlag.trim() === "") return {
643
+ code: "STIM_BAD_ARG",
644
+ message: "--runtime was given an empty version.",
645
+ remedy: "Pass `--runtime <version>` with a runtime `xcrun simctl list runtimes` reports, e.g. \"18.5\"."
646
+ };
647
+ if (physical || remoteBackend) return null;
648
+ if (!deviceType && !runtime) return null;
649
+ let runtimes;
650
+ try {
651
+ runtimes = listRuntimes();
652
+ } catch (error) {
653
+ return {
654
+ code: "STIM_NO_DEVICE",
655
+ message: `Could not read the installed simulator runtimes: ${error?.message || error}`,
656
+ remedy: "Run `stim doctor` to check the simulator toolchain, then try again."
657
+ };
658
+ }
659
+ const refusal = unknownIosRuntimeRefusal(runtime, runtimes) ?? unknownIosDeviceTypeRefusal(deviceType, runtimes, runtime);
660
+ return refusal ? {
661
+ code: "STIM_BAD_ARG",
662
+ message: refusal.message,
663
+ remedy: refusal.remedy
664
+ } : null;
665
+ }
666
+ function isReleaseConfiguration(configuration) {
667
+ return typeof configuration === "string" && configuration.trim() !== "" && configuration.trim().toLowerCase() !== "debug";
668
+ }
669
+ function podAction(podState, verdict) {
670
+ if (verdict?.stale) return {
671
+ install: true,
672
+ reason: verdict.reason
673
+ };
674
+ if (verdict?.noPods && podState?.hasPodfile) return {
675
+ install: true,
676
+ reason: "ios/Podfile exists but no pods are installed"
677
+ };
678
+ return { install: false };
679
+ }
680
+ function xcodeFailureReport(result, logPath) {
681
+ const diagnostics = Array.isArray(result?.diagnostics) ? result.diagnostics : [];
682
+ const code = result?.exitCode;
683
+ const how = code === null || code === void 0 ? "" : ` (exit code ${code})`;
684
+ return {
685
+ message: diagnostics.length ? `\`xcodebuild\` failed${how} with ${diagnostics.length} diagnostic${diagnostics.length === 1 ? "" : "s"}.` : `\`xcodebuild\` failed${how} with no recognizable diagnostic.`,
686
+ remedy: diagnostics.find((d) => d?.remedy)?.remedy || `See ${logPath} for the transcript.`
687
+ };
688
+ }
689
+ function printDiagnostics(note, result) {
690
+ const diagnostics = Array.isArray(result?.diagnostics) ? result.diagnostics : [];
691
+ const shown = diagnostics.slice(0, MAX_PRINTED_DIAGNOSTICS);
692
+ for (const diagnostic of shown) note(chalk.red(phaseLine("error", describeDiagnostic(diagnostic))));
693
+ const hidden = diagnostics.length - shown.length + (result?.truncated || 0);
694
+ if (hidden > 0) note(chalk.dim(phaseLine("error", `... and ${hidden} more diagnostic${hidden === 1 ? "" : "s"} in the log`)));
695
+ if (diagnostics.length === 0) {
696
+ note(chalk.red(phaseLine("error", "xcodebuild failed with no recognizable diagnostic; last lines:")));
697
+ for (const line of (result?.tail || []).slice(-5)) note(chalk.dim(phaseLine("", line)));
698
+ }
699
+ }
700
+ //#endregion
701
+ //#region src/commands/ios/collector.ts
702
+ function collectorLogFile(root) {
703
+ return join(workspaceLogsDir(root), `collector-ios.log`);
704
+ }
705
+ function collectorEntry() {
706
+ return spawnEntry("collector-run");
707
+ }
708
+ const COLLECTOR_EXIT_WAIT_MS = 2e3;
709
+ const COLLECTOR_POLL_MS = 25;
710
+ async function stopPreviousCollector({ root, kill = (pid, signal) => process.kill(pid, signal), alive = pidExists, readState = readWorkspaceState, verify = verifyCollectorOwnership, waitMs = COLLECTOR_EXIT_WAIT_MS, note = (_line) => {} }) {
711
+ const previous = (readState(root)?.collectors)?.["ios"] || null;
712
+ const previousPid = Number(previous?.pid) || null;
713
+ let killed = null;
714
+ if (previousPid) {
715
+ const ownership = verify({
716
+ pid: previousPid,
717
+ platform: "ios",
718
+ root,
719
+ isAlive: alive,
720
+ expected: previous
721
+ });
722
+ if (ownership.status === "ours") {
723
+ try {
724
+ kill(previousPid, "SIGTERM");
725
+ killed = previousPid;
726
+ } catch (err) {
727
+ if (err?.code !== "ESRCH") note(chalk.yellow(`Could not stop the previous ios log collector (pid ${previousPid}): ${err?.message || err}`));
728
+ }
729
+ const deadline = Date.now() + waitMs;
730
+ while (Date.now() < deadline && alive(previousPid)) await sleep(COLLECTOR_POLL_MS);
731
+ } else if (ownership.status === "unverified") note(chalk.dim(`Previous ios log collector (pid ${previousPid}): ${ownership.reason}, so it was not signalled -- starting a replacement anyway`));
732
+ }
733
+ return { killed };
734
+ }
735
+ async function replaceCollector({ root, udid, bundleId, appName, appExecutable, physical = false, payloadUrl = null, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), alive = pidExists, readState = readWorkspaceState, verify = verifyCollectorOwnership, waitMs = COLLECTOR_EXIT_WAIT_MS, note = (_line) => {} }) {
736
+ const { killed } = await stopPreviousCollector({
737
+ root,
738
+ kill,
739
+ alive,
740
+ readState,
741
+ verify,
742
+ waitMs,
743
+ note
744
+ });
745
+ const args = [
746
+ collectorEntry(),
747
+ "--platform",
748
+ "ios",
749
+ "--root",
750
+ root,
751
+ "--udid",
752
+ udid,
753
+ "--bundle",
754
+ bundleId
755
+ ];
756
+ if (appName) args.push("--app-name", appName);
757
+ if (appExecutable) args.push("--app-executable", appExecutable);
758
+ if (physical) args.push("--physical");
759
+ if (payloadUrl) args.push("--payload-url", payloadUrl);
760
+ let stdio = "ignore";
761
+ try {
762
+ mkdirSync(workspaceLogsDir(root), { recursive: true });
763
+ const fd = openSync(collectorLogFile(root), "a");
764
+ stdio = [
765
+ "ignore",
766
+ fd,
767
+ fd
768
+ ];
769
+ } catch {}
770
+ let child;
771
+ try {
772
+ child = spawn(process.execPath, args, {
773
+ cwd: root,
774
+ detached: true,
775
+ stdio,
776
+ env: process.env
777
+ });
778
+ child?.unref?.();
779
+ } catch (err) {
780
+ note(chalk.yellow(`Could not start the ios log collector: ${err?.message || err}`));
781
+ return {
782
+ killed,
783
+ pid: null
784
+ };
785
+ }
786
+ return {
787
+ killed,
788
+ pid: child?.pid ?? null
789
+ };
790
+ }
791
+ //#endregion
792
+ //#region src/commands/ios/dependencies.ts
793
+ const DEFAULT_DEPS = {
794
+ findProjectRoot,
795
+ resolveSettings,
796
+ gitCommonDir,
797
+ repoRoot,
798
+ detectBundleId,
799
+ detectIsExpo,
800
+ devClientScheme,
801
+ getProject,
802
+ upsertProject,
803
+ projectShortcut,
804
+ checkDeviceCapacity,
805
+ ensureOwnedDevice,
806
+ listIosRuntimes,
807
+ ensureBooted,
808
+ resolveRemoteContext,
809
+ remoteIosDeps,
810
+ ensureMetroReachable,
811
+ ensureRemoteBootOwned,
812
+ detectProviders,
813
+ resolveProjectMetro,
814
+ resolveMetroWithRetry,
815
+ warmMetro,
816
+ readWorkspaceState,
817
+ pidExists,
818
+ getConcurrencyLimits,
819
+ fingerprintProject,
820
+ untrackedNativeFiles,
821
+ resolveBuild,
822
+ storeBuild,
823
+ resolveCacheProviderConfig,
824
+ loadCacheProvider,
825
+ acquireBuildLock,
826
+ releaseBuildLock,
827
+ waitForBuild,
828
+ acquireBuildSlot,
829
+ releaseBuildSlot,
830
+ loadProjectProvider,
831
+ checkEasAuth,
832
+ resolveEasCliBin,
833
+ resolveRemote,
834
+ uploadRemote,
835
+ needsPrebuild,
836
+ runPrebuild,
837
+ readPodState,
838
+ podsAreStale,
839
+ runPodInstall,
840
+ buildIos,
841
+ discoverXcodeProject,
842
+ resolveScheme,
843
+ listIosDevices,
844
+ hostLanCandidates,
845
+ ensureLanReachable,
846
+ gateProfileForDevice,
847
+ sealAppForDevice,
848
+ installIosDeviceApp,
849
+ awaitIosDeviceLaunch,
850
+ acquireRunLease,
851
+ runLease,
852
+ selectFromPool,
853
+ releaseLeaseOnSignal,
854
+ iosDeviceProcess,
855
+ verifyIosDeviceReleaseLaunch,
856
+ readBundleId,
857
+ readBundleExecutable,
858
+ swapJsBundle,
859
+ installIosApp,
860
+ clearOtherUserApps,
861
+ clearIosAdoptionPending,
862
+ launchIosApp,
863
+ verifyLaunch,
864
+ verifyReleaseLaunch,
865
+ ensureWorkspaceStorage: ensureWorkspaceStorageSafely,
866
+ replaceCollector,
867
+ stopPreviousCollector,
868
+ writeWorkspaceLaunch,
869
+ writeWorkspaceState,
870
+ createWriter: createNdjsonWriter,
871
+ recordStats: recordRunStats,
872
+ readEstimates: readRunEstimates,
873
+ now: () => Date.now()
874
+ };
875
+ //#endregion
876
+ //#region src/commands/ios/result.ts
877
+ function lastBuildRecord({ fingerprint = null, cacheKey = null, cacheHit = false, cacheSkipped = false, durationMs = 0, appPath = null, bundleId = null, startedAt, status, errorCode = null }) {
878
+ const record = {
879
+ platform: "ios",
880
+ fingerprint,
881
+ cacheKey,
882
+ cacheHit: cacheLevel(cacheHit),
883
+ cacheSkipped: Boolean(cacheSkipped),
884
+ durationMs,
885
+ appPath,
886
+ bundleId,
887
+ startedAt,
888
+ status
889
+ };
890
+ if (errorCode) record.errorCode = errorCode;
891
+ return record;
892
+ }
893
+ function iosFacts({ udid, deviceName, deviceType = null, runtime = null, fingerprint, configuration = null, scheme = null, cacheKey, cacheHit, cacheSkipped = false, compilationCache = COMPILATION_CACHE_NOT_RUN, waitedForBuild = null, appPath, bundleId, installSkipped = false, metroPort, logsDir, durationMs, launched = true, webPreviewUrl = null, lease }) {
894
+ return {
895
+ platform: "ios",
896
+ udid,
897
+ deviceName: deviceName ?? null,
898
+ deviceType: deviceType ?? null,
899
+ runtime: runtime ?? null,
900
+ fingerprint,
901
+ configuration: configuration ?? null,
902
+ ...scheme ? { scheme } : {},
903
+ cacheKey,
904
+ cacheHit: cacheLevel(cacheHit),
905
+ cacheSkipped: Boolean(cacheSkipped),
906
+ compilationCache,
907
+ waitedForBuild: waitedForBuild ? {
908
+ pid: waitedForBuild.pid ?? null,
909
+ ms: waitedForBuild.ms ?? 0
910
+ } : null,
911
+ appPath,
912
+ bundleId,
913
+ installSkipped: Boolean(installSkipped),
914
+ launched: launched === "unverified" || launched === "bundling" ? launched : Boolean(launched),
915
+ metroPort,
916
+ logs: { dir: logsDir },
917
+ durationMs,
918
+ ...webPreviewUrl ? { webPreviewUrl } : {},
919
+ ...lease === void 0 ? {} : { lease }
920
+ };
921
+ }
922
+ function writeLastBuild(root, record, { write = writeWorkspaceState } = {}) {
923
+ try {
924
+ write(root, { lastBuild: record });
925
+ } catch {}
926
+ return record;
927
+ }
928
+ async function finishIosUpload(uploadPending, remote, phase, note) {
929
+ if (!uploadPending) return false;
930
+ const upload = await uploadPending;
931
+ if (upload?.uploaded) phase("cache", `uploaded (${remote?.name})`);
932
+ else if (upload?.timedOut) {
933
+ note(chalk.yellow(phaseLine("cache", `${remote?.name} upload still running after ${formatDuration(UPLOAD_TIMEOUT_MS)}; not waiting`)));
934
+ return true;
935
+ } else if (upload?.failed) {
936
+ const authNote = remote?.name === "eas" && isEasAuthFailureText(upload.failed) ? easAuthNote({
937
+ code: "logged-out",
938
+ reason: upload.failed,
939
+ phase: "upload"
940
+ }) : null;
941
+ note(chalk.yellow(phaseLine("cache", authNote || `${remote?.name} upload failed: ${upload.failed}`)));
942
+ }
943
+ return false;
944
+ }
945
+ function reportIosResult({ d, root, json, release, configuration, buildScheme = null, metroCheck, metroPort, logsDir, device, udid, appPath, bundleId, installSkipped, elapsed, startedAt, storeHash, storeKey, cacheHit, compilationCache, useBuildCache, waitedForBuild, launchState, launchWarning, remote, providerName, closeWriter, webPreviewUrl, lease, recordRun }) {
946
+ const durationMs = elapsed();
947
+ recordRun({
948
+ failed: false,
949
+ cacheHit,
950
+ waited: waitedForBuild,
951
+ durationMs
952
+ });
953
+ writeLastBuild(root, lastBuildRecord({
954
+ fingerprint: storeHash,
955
+ cacheKey: storeKey,
956
+ cacheHit,
957
+ cacheSkipped: !useBuildCache,
958
+ durationMs,
959
+ appPath,
960
+ bundleId,
961
+ startedAt,
962
+ status: "ok"
963
+ }), { write: d.writeWorkspaceState });
964
+ closeWriter();
965
+ const facts = iosFacts({
966
+ udid,
967
+ deviceName: device?.deviceName ?? null,
968
+ deviceType: device?.deviceType,
969
+ runtime: device?.runtime,
970
+ fingerprint: storeHash,
971
+ configuration,
972
+ scheme: buildScheme,
973
+ cacheKey: storeKey,
974
+ cacheHit,
975
+ compilationCache,
976
+ cacheSkipped: !useBuildCache,
977
+ waitedForBuild,
978
+ appPath,
979
+ bundleId,
980
+ installSkipped,
981
+ metroPort,
982
+ logsDir,
983
+ durationMs,
984
+ launched: launchState,
985
+ webPreviewUrl,
986
+ lease
987
+ });
988
+ if (json) console.log(JSON.stringify(facts));
989
+ else {
990
+ const summary = `${launchWarning ? "WARNING" : "OK"}: ${bundleId} on ${deviceLabel(device, udid)}, ` + (release ? `${configuration} (embedded JS, no Metro)` : `Metro port ${metroPort}`) + ` (${cacheDescription(cacheHit, remote?.name ?? providerName)}, ${formatDuration(durationMs)})`;
991
+ const outcome = launchWarning ? chalk.yellow(`${summary} -- ${launchWarning}`) : launchState === "unverified" ? chalk.yellow(`${summary} -- launch UNVERIFIED`) : launchState === "bundling" ? chalk.green(`${summary} -- bundle requested, still building`) : chalk.green(summary);
992
+ const deviceName = device?.deviceName ?? device?.name ?? udid;
993
+ const cacheResult = useBuildCache ? cacheDescription(cacheHit, remote?.name ?? providerName) : "bypassed; built";
994
+ const metroResult = release ? `embedded (${configuration})` : !metroCheck ? `check skipped on port ${metroPort}` : launchState === "unverified" ? `state unverified on port ${metroPort}` : `running on port ${metroPort}`;
995
+ console.log([
996
+ outcome,
997
+ phaseLine("device", `${deviceName} (${udid})`),
998
+ phaseLine("app", bundleId),
999
+ phaseLine("metro", metroResult),
1000
+ phaseLine("cache", cacheResult),
1001
+ ...compilationCache.status === "not-run" ? [phaseLine("compilation cache", compilationCacheActivityLine(compilationCache))] : [],
1002
+ phaseLine("logs", logsDir)
1003
+ ].join("\n"));
1004
+ if (facts.webPreviewUrl) console.error(chalk.dim(`Watch this device: ${facts.webPreviewUrl}`));
1005
+ }
1006
+ return facts;
1007
+ }
1008
+ function cacheDescription(cacheHit, providerName = null) {
1009
+ if (cacheHit === "remote") return `from ${providerName || "the remote cache"}`;
1010
+ if (cacheHit === "local") return "from cache";
1011
+ return "built";
1012
+ }
1013
+ //#endregion
1014
+ //#region src/commands/ios/launch.ts
1015
+ function missingCrashReportHint({ physical, remote, crashed }) {
1016
+ return crashed && !physical && !remote ? `No attributable native crash report captured yet. ${IOS_CRASH_REPORT_RETRY}` : "No attributable native crash report captured. Read `stim logs --source device` for available output.";
1017
+ }
1018
+ async function verifyIosRun({ root, appPath, d, release, launched, configuration, phase, note, metroCheck, logsDir, launchedAt, metroPort, isExpo, bundleId, udid, scheme, physical, appName, lanAddress, lanOrigin, remoteDevice, metroOrigin }) {
1019
+ const readNativeCrashes = () => remoteDevice ? [] : captureNativeCrashes({
1020
+ root,
1021
+ platform: "ios",
1022
+ deviceId: udid,
1023
+ appId: bundleId,
1024
+ since: launchedAt,
1025
+ appPath,
1026
+ physical
1027
+ }, logsDir);
1028
+ const deviceProcess = () => {
1029
+ const pid = d.iosDeviceProcess({
1030
+ udid,
1031
+ appName: appName ?? bundleId
1032
+ });
1033
+ return pid === void 0 ? null : pid !== null;
1034
+ };
1035
+ if (release) {
1036
+ const processCheck = physical ? await d.verifyIosDeviceReleaseLaunch({
1037
+ udid,
1038
+ appName: appName ?? bundleId
1039
+ }) : await d.verifyReleaseLaunch({ pid: launched?.pid ?? null });
1040
+ const crashes = readNativeCrashes();
1041
+ if (processCheck?.verified && !crashes.length) {
1042
+ phase("verify", `process alive ${formatDuration(processCheck.waitedMs ?? 0)} after launch (${configuration}: no bundle fetch to observe)`);
1043
+ return { state: true };
1044
+ }
1045
+ for (const line of launchErrorPreview(crashes, root)) note(chalk.red(phaseLine("launch", line)));
1046
+ if (!crashes.length) note(phaseLine("logs", missingCrashReportHint({
1047
+ physical,
1048
+ remote: Boolean(remoteDevice),
1049
+ crashed: processCheck?.reason === "exited"
1050
+ })));
1051
+ phase("verify", chalk.yellow(crashes.length ? "FATAL: the app reported a native crash" : processCheck?.reason === "exited" ? `FATAL: the app process exited within ${formatDuration(processCheck.waitedMs ?? 0)} of launch` : physical ? `UNVERIFIED: devicectl could not read ${udid}'s process list` : "UNVERIFIED: simctl launch reported no process id to check"));
1052
+ note(chalk.yellow(phaseLine("", "Release readiness was not established. Run `stim logs --errors` for captured crash reports, or `stim logs --source device` for the full device output.")));
1053
+ return { state: crashes.length || processCheck?.reason === "exited" ? LAUNCH_FATAL : LAUNCH_UNVERIFIED };
1054
+ }
1055
+ const verification = metroCheck ? await d.verifyLaunch({
1056
+ requireBundleResponse: true,
1057
+ onReadinessPending: () => phase("readiness", "waiting for app readiness (up to 30s after bundle load)"),
1058
+ logsDir,
1059
+ since: launchedAt,
1060
+ metroPort,
1061
+ platform: "ios",
1062
+ mode: isExpo ? MODE_EXPO : MODE_BARE,
1063
+ readNativeCrashes,
1064
+ processAlive: remoteDevice ? null : physical ? deviceProcess : () => {
1065
+ if (launched?.pid) return d.pidExists(launched.pid);
1066
+ const pid = iosAppProcess(udid, bundleId);
1067
+ return pid === void 0 ? null : pid !== null;
1068
+ }
1069
+ }) : {
1070
+ verified: false,
1071
+ skipped: true
1072
+ };
1073
+ const nativeCrashes = verification.errors?.some((record) => record.event === "native_crash") ? [] : readNativeCrashes();
1074
+ if (nativeCrashes.length) verification.fatal = true;
1075
+ verification.errors = await errorDiagnostics([...verification.errors ?? [], ...nativeCrashes], {
1076
+ root,
1077
+ logsDir,
1078
+ port: metroPort,
1079
+ allowRequest: true
1080
+ });
1081
+ if (verification.readiness) phase("readiness", appReadinessMessage(verification.readiness, verification.waitedMs ?? 0));
1082
+ if (verification?.fatal) {
1083
+ const nativeFatal = verification.errors?.some((record) => record.event === "native_crash");
1084
+ const deliveryFailed = verification.record?.event === "bundle_response_failed";
1085
+ const reason = nativeFatal ? "the app reported a native crash" : verification.processAlive === false ? "the app process exited" : deliveryFailed ? "Metro bundle delivery failed" : "Metro could not build the bundle";
1086
+ phase("verify", chalk.red(`FATAL after ${formatDuration(verification.waitedMs ?? 0)}: ${reason}`));
1087
+ for (const line of launchErrorPreview(verification.errors ?? [], root)) note(chalk.red(phaseLine("", line)));
1088
+ if (verification.processAlive === false && !verification.errors?.some((record) => record.event === "native_crash")) note(phaseLine("logs", missingCrashReportHint({
1089
+ physical,
1090
+ remote: Boolean(remoteDevice),
1091
+ crashed: true
1092
+ })));
1093
+ if (nativeFatal || verification.processAlive === false) note(chalk.yellow(phaseLine("remedy", "Fix the crash, then run `stim ios` again. A Metro reload cannot restart an exited app.")));
1094
+ else if (verification.processAlive === true && metroPort !== null) {
1095
+ const reloadRemedy = `press Reload on the app's own error screen -- run \`agent-device snapshot -i --platform ios --udid ${udid}\` in your existing automation session to reach it. Neither \`stim reload ios\` nor \`agent-device metro reload\` can: this app never connected to Metro.`;
1096
+ note(chalk.yellow(phaseLine("remedy", `The native app is still running. ${deliveryFailed ? "Check the Metro logs and device connection, then" : "Fix the JavaScript or TypeScript error, then"} ${reloadRemedy} Do not run \`stim ios\` unless native inputs changed or the app process exits.`)));
1097
+ }
1098
+ return { state: LAUNCH_FATAL };
1099
+ }
1100
+ if (verification?.verified) {
1101
+ phase("verify", `bundle loaded` + (verification.processAlive === true ? ", process alive" : "") + (verification.readiness ? "" : ", stable for 3s -- the first screen may still be rendering") + ` (${formatDuration(verification.waitedMs ?? 0)} total)`);
1102
+ const hasAppErrors = reportLaunchErrors(verification.errors ?? [], note, root);
1103
+ if (hasAppErrors && verification.processAlive === true && metroPort !== null) {
1104
+ const reloadRemedy = physical || remoteDevice ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload ios`.";
1105
+ note(chalk.yellow(phaseLine("remedy", `The native app is still running. Fix the JavaScript or TypeScript error; Fast Refresh should apply the edit. If the error screen remains, ${reloadRemedy} Do not run \`stim ios\` unless native inputs changed or the app process exits.`)));
1106
+ }
1107
+ return {
1108
+ state: true,
1109
+ warning: hasAppErrors ? "app errors detected; inspect the error above" : verification.readiness === "timed-out" || verification.readiness === "error" ? "app readiness not confirmed; inspect the UI and logs" : void 0
1110
+ };
1111
+ }
1112
+ if (verification?.skipped) {
1113
+ phase("verify", "skipped (--no-metro-check): the launch is reported as unverified");
1114
+ return { state: LAUNCH_UNVERIFIED };
1115
+ }
1116
+ if (verification?.requested) {
1117
+ phase("verify", `BUNDLING: the app asked port ${metroPort} for its bundle; build or delivery was still pending after ${formatDuration(verification.waitedMs ?? 0)} (a cold bundle on a large graph outlasts this window)`);
1118
+ note(chalk.dim(phaseLine("", "Nothing to do: `stim logs --source metro` shows the build finishing, usually within a minute.")));
1119
+ return { state: LAUNCH_BUNDLING };
1120
+ }
1121
+ phase("verify", chalk.yellow("UNVERIFIED: no bundle request reached this workspace's Metro"));
1122
+ for (const line of unverifiedLaunchLines({
1123
+ platform: "ios",
1124
+ metroPort: metroPort ?? 8081,
1125
+ waitedMs: verification?.waitedMs,
1126
+ bundleId,
1127
+ udid,
1128
+ devClientUrl: scheme ? devClientUrl(scheme, metroPort ?? 8081, lanAddress ?? void 0) : null,
1129
+ mode: isExpo ? MODE_EXPO : MODE_BARE,
1130
+ remote: remoteDevice,
1131
+ physical,
1132
+ devClient: Boolean(scheme),
1133
+ lanOrigin,
1134
+ metroOrigin,
1135
+ localNetworkPending: physical && localNetworkPending(readCollectorRecords(logsDir), {
1136
+ since: launchedAt,
1137
+ pid: launched?.pid ?? null,
1138
+ lanOrigin
1139
+ })
1140
+ })) note(chalk.yellow(phaseLine("", line)));
1141
+ return { state: LAUNCH_UNVERIFIED };
1142
+ }
1143
+ function reportLaunchErrors(errors, note, root) {
1144
+ const report = launchErrorReport(errors, root);
1145
+ if (report.summary) note(chalk.dim(phaseLine("launch", report.summary)));
1146
+ for (const line of report.lines) note(chalk.yellow(phaseLine("launch", line)));
1147
+ return report.lines.length > 0;
1148
+ }
1149
+ function cleanAdoptedIosApps({ d, root, udid, bundleId, phase, note }) {
1150
+ const swept = d.clearOtherUserApps({
1151
+ udid,
1152
+ keep: bundleId
1153
+ });
1154
+ if (swept.removed.length) phase("install", `removed ${swept.removed.join(", ")}, left by the previous workspace`);
1155
+ if (swept.listed && swept.failed.length === 0) {
1156
+ d.clearIosAdoptionPending(root);
1157
+ return null;
1158
+ }
1159
+ if (!swept.listed) return "Could not list apps left by the previous workspace.";
1160
+ for (const failure of swept.failed) note(chalk.yellow(phaseLine("install", `could not remove ${failure}, left by the previous workspace`)));
1161
+ return `Could not remove ${swept.failed.join(", ")}, left by the previous workspace.`;
1162
+ }
1163
+ function resolveRunBundleId(d, root, appPath, bundleId) {
1164
+ if (!appPath || bundleId) return bundleId;
1165
+ return d.readBundleId(appPath) || d.detectBundleId(root);
1166
+ }
1167
+ function removeSwapDirectory(swapDir) {
1168
+ if (!swapDir) return;
1169
+ try {
1170
+ rmSync(swapDir, {
1171
+ recursive: true,
1172
+ force: true
1173
+ });
1174
+ } catch {}
1175
+ }
1176
+ function readRunExecutable(d, appPath, note) {
1177
+ const executable = appPath ? d.readBundleExecutable(appPath) : null;
1178
+ if (appPath && !executable) note(chalk.dim(`Could not read CFBundleExecutable from ${appPath}; the device log predicate falls back to the .app basename.`));
1179
+ return executable;
1180
+ }
1181
+ function recordIosReloadTarget({ d, root, physical, remoteDevice, bundleId, udid, metroPort, release, launchedAt, note }) {
1182
+ if (physical || remoteDevice) return;
1183
+ try {
1184
+ d.writeWorkspaceLaunch(root, "ios", {
1185
+ appId: bundleId,
1186
+ deviceId: udid,
1187
+ metroPort,
1188
+ release,
1189
+ launchedAt: new Date(launchedAt).toISOString()
1190
+ });
1191
+ } catch (error) {
1192
+ note(chalk.yellow(phaseLine("state", `could not record iOS launch: ${error?.message || error}`)));
1193
+ }
1194
+ }
1195
+ function simulatorLaunchFailureRemedy(remote, udid, bundleId, logFile) {
1196
+ return `${remote ? "Run" : "If the simulator timed out, run `stim doctor --platform ios` and resolve any reported host memory pressure before retrying. Otherwise run"} \`xcrun simctl launch --console ${udid} ${bundleId}\` to see what the app reports, and check ${logFile}.`;
1197
+ }
1198
+ async function finishIosRun({ d, root, json, release, configuration, buildScheme, isExpo, metroCheck, metroPort, logsDir, logFile, device, udid, physical, lanAddress, lanOriginUrl, remoteDevice, bootPromise, bootDuration, appPath, bundleId: initialBundleId, swapDir, buildFailure, fail, phase, note, logWriter, uploadPending, providerUpload, providerName, remote, abandonedRemote: remoteWasAbandoned, elapsed, startedAt, storeHash, storeKey, cacheHit, compilationCache, useBuildCache, waitedForBuild, closeWriter, lease, releaseLease, recordRun }) {
1199
+ let bundleId = initialBundleId;
1200
+ let leaseWarned = false;
1201
+ const raiseLeaseFor = (boundMs, beforeInstall) => {
1202
+ const step = lease?.raise(boundMs);
1203
+ if (!step || step.ok) return null;
1204
+ if (beforeInstall) {
1205
+ const refusal = lostRefusal(step.holder, step.expiresAt, d.now());
1206
+ return {
1207
+ code: refusal.code,
1208
+ message: refusal.message,
1209
+ remedy: refusal.remedy,
1210
+ lease: refusal.lease
1211
+ };
1212
+ }
1213
+ if (!leaseWarned) {
1214
+ leaseWarned = true;
1215
+ note(chalk.yellow(phaseLine("lease", lostLine(step.holder, step.expiresAt, d.now()))));
1216
+ }
1217
+ return null;
1218
+ };
1219
+ bundleId = resolveRunBundleId(d, root, appPath, bundleId);
1220
+ if (appPath && !bundleId) return fail({
1221
+ code: "STIM_INSTALL_FAILED",
1222
+ message: `Could not read a bundle identifier from the cached app at ${appPath}.`,
1223
+ remedy: "Remove the cache entry (`stim gc`) and run again to rebuild it.",
1224
+ build: {
1225
+ ...buildFailure,
1226
+ appPath
1227
+ }
1228
+ });
1229
+ if (bundleId) d.upsertProject(root, { bundleId });
1230
+ const booted = await bootPromise;
1231
+ if (!booted?.ok) return fail({
1232
+ code: booted?.code || "STIM_NO_DEVICE",
1233
+ message: booted?.reason || "The owned simulator could not be booted.",
1234
+ remedy: booted?.remedy || "Run `stim ios` again to re-establish an owned simulator for this workspace."
1235
+ });
1236
+ const deviceOutcome = physical ? "connected" : `${device?.adopted ? "adopted" : "booted"} ${bootDuration()}`;
1237
+ phase("device", `${deviceLabel(device, udid)} ${deviceOutcome}`);
1238
+ const scheme = release ? void 0 : d.devClientScheme(root, appPath);
1239
+ const appName = appNameFromPath(appPath);
1240
+ const appExecutable = readRunExecutable(d, appPath, note);
1241
+ const dropSwapDir = () => removeSwapDirectory(swapDir);
1242
+ let installSkipped = false;
1243
+ let launched = null;
1244
+ let launchedAt = d.now();
1245
+ if (physical) {
1246
+ const lostBeforeInstall = raiseLeaseFor(DEVICECTL_INSTALL_TIMEOUT_MS, true);
1247
+ if (lostBeforeInstall) return fail(lostBeforeInstall);
1248
+ await d.stopPreviousCollector({
1249
+ root,
1250
+ note
1251
+ });
1252
+ const installTimer = stepTimer(d.now);
1253
+ const installed = d.installIosDeviceApp({
1254
+ udid,
1255
+ appPath,
1256
+ bundleId
1257
+ });
1258
+ if (installed?.failed) {
1259
+ dropSwapDir();
1260
+ return fail({
1261
+ code: installed.code || "STIM_INSTALL_FAILED",
1262
+ message: installed.reason ?? `devicectl could not install ${appPath} on ${udid}.`,
1263
+ remedy: installed.remedy ?? null,
1264
+ build: {
1265
+ ...buildFailure,
1266
+ appPath,
1267
+ bundleId
1268
+ }
1269
+ });
1270
+ }
1271
+ phase("install", `${basename(appPath)} -> ${deviceLabel(device, udid)} ${installTimer()}`);
1272
+ if (installed?.note) {
1273
+ note(chalk.yellow(phaseLine("install", installed.note)));
1274
+ logWriter().write({
1275
+ src: "build",
1276
+ level: "warn",
1277
+ event: "install_uninstalled_first",
1278
+ msg: installed.note
1279
+ });
1280
+ }
1281
+ dropSwapDir();
1282
+ raiseLeaseFor(COLLECTOR_EXIT_WAIT_MS + LAUNCH_PROBE_TIMEOUT_MS, false);
1283
+ const payloadUrl = scheme && metroPort !== null && lanAddress ? devClientUrl(scheme, metroPort, lanAddress) : null;
1284
+ const launchTimer = stepTimer(d.now);
1285
+ launchedAt = d.now();
1286
+ logWriter().write({
1287
+ src: "build",
1288
+ level: "info",
1289
+ marker: true,
1290
+ event: "launch_attempt",
1291
+ ts: launchedAt,
1292
+ platform: "ios",
1293
+ appId: bundleId,
1294
+ deviceId: udid,
1295
+ appPath,
1296
+ physical: true,
1297
+ msg: `launching ${bundleId} on ${udid}`
1298
+ });
1299
+ const collector = await d.replaceCollector({
1300
+ root,
1301
+ udid,
1302
+ bundleId,
1303
+ appName,
1304
+ physical: true,
1305
+ payloadUrl,
1306
+ note
1307
+ });
1308
+ if (!collector?.pid) return fail({
1309
+ code: "STIM_LAUNCH_FAILED",
1310
+ message: `The device log collector, which is what launches ${bundleId} on a phone, could not be started.`,
1311
+ remedy: `Check ${logFile} and the workspace collector log, then run the command again.`,
1312
+ build: {
1313
+ ...buildFailure,
1314
+ appPath,
1315
+ bundleId
1316
+ }
1317
+ });
1318
+ const started = await d.awaitIosDeviceLaunch({
1319
+ udid,
1320
+ bundleId,
1321
+ appName: appName ?? bundleId,
1322
+ collectorPid: collector.pid,
1323
+ readRecords: () => readCollectorRecords(logsDir).filter((entry) => Number(entry.ts) >= launchedAt)
1324
+ });
1325
+ if (started.failed || !started.pid) return fail({
1326
+ code: "STIM_LAUNCH_FAILED",
1327
+ message: started.reason ?? `${bundleId} did not start on ${udid}.`,
1328
+ remedy: started.remedy ?? null,
1329
+ lines: started.lines ?? [],
1330
+ logPath: logsDir,
1331
+ build: {
1332
+ ...buildFailure,
1333
+ appPath,
1334
+ bundleId
1335
+ }
1336
+ });
1337
+ launched = {
1338
+ ok: true,
1339
+ mode: payloadUrl ? "payload-url" : "launch",
1340
+ pid: started.pid,
1341
+ ...payloadUrl ? { url: payloadUrl } : {},
1342
+ ...lanOriginUrl ? { jsLocation: lanOriginUrl } : {}
1343
+ };
1344
+ phase("launch", `${bundleId} pid ${started.pid} ${launchTimer()}`);
1345
+ } else {
1346
+ const adopting = Boolean(device?.adoptionPending);
1347
+ if (adopting) {
1348
+ const cleanupFailure = cleanAdoptedIosApps({
1349
+ d,
1350
+ root,
1351
+ udid,
1352
+ bundleId,
1353
+ phase,
1354
+ note
1355
+ });
1356
+ if (cleanupFailure) return fail({
1357
+ code: "STIM_INSTALL_FAILED",
1358
+ message: `${cleanupFailure} Stim kept the adoption cleanup pending and did not install or launch the app.`,
1359
+ remedy: "Run `stim ios` again after simulator tooling is responsive.",
1360
+ build: {
1361
+ ...buildFailure,
1362
+ appPath,
1363
+ bundleId
1364
+ }
1365
+ });
1366
+ }
1367
+ const installTimer = stepTimer(d.now);
1368
+ const installed = d.installIosApp({
1369
+ udid,
1370
+ appPath,
1371
+ bundleId,
1372
+ devClientScheme: scheme,
1373
+ proveInstalled: !adopting || device?.parkedCacheKey === storeKey
1374
+ }, { now: d.now });
1375
+ if (installed?.failed) return fail({
1376
+ code: installed.code || "STIM_INSTALL_FAILED",
1377
+ message: installed.reason,
1378
+ remedy: "Check that the simulator is booted and that the app was built for the simulator SDK.",
1379
+ build: {
1380
+ ...buildFailure,
1381
+ appPath,
1382
+ bundleId
1383
+ }
1384
+ });
1385
+ installSkipped = Boolean(installed?.skipped);
1386
+ const artifactDuration = installed?.artifactDurationMs === void 0 ? installTimer() : `(${formatDuration(installed.artifactDurationMs)})`;
1387
+ phase("install", installSkipped ? `unchanged (${deviceShortName(device, udid)} already has this build) ${artifactDuration}` : `${basename(appPath)} -> ${deviceLabel(device, udid)} ${artifactDuration}`);
1388
+ if (!remoteDevice && installed?.devClientPreparationDurationMs !== void 0) phase("install", `dev client prepared (${formatDuration(installed.devClientPreparationDurationMs)})`);
1389
+ dropSwapDir();
1390
+ if (!remoteDevice) await d.replaceCollector({
1391
+ root,
1392
+ udid,
1393
+ bundleId,
1394
+ appName,
1395
+ appExecutable,
1396
+ note
1397
+ });
1398
+ const launchTimer = stepTimer(d.now);
1399
+ launchedAt = d.now();
1400
+ logWriter().write({
1401
+ src: "build",
1402
+ level: "info",
1403
+ marker: true,
1404
+ event: "launch_attempt",
1405
+ ts: launchedAt,
1406
+ platform: "ios",
1407
+ appId: bundleId,
1408
+ deviceId: udid,
1409
+ appPath,
1410
+ remote: Boolean(remoteDevice),
1411
+ msg: `launching ${bundleId} on ${udid}`
1412
+ });
1413
+ launched = d.launchIosApp({
1414
+ udid,
1415
+ bundleId,
1416
+ metroPort,
1417
+ devClientScheme: scheme,
1418
+ consolePaths: simulatorConsolePaths({
1419
+ deviceId: udid,
1420
+ appId: bundleId,
1421
+ since: launchedAt
1422
+ }, true, Boolean(remoteDevice))
1423
+ });
1424
+ if (launched?.failed) {
1425
+ printNativeCrashReport({
1426
+ root,
1427
+ platform: "ios",
1428
+ deviceId: udid,
1429
+ appId: bundleId,
1430
+ since: launchedAt,
1431
+ appPath
1432
+ }, logsDir, (line) => note(chalk.red(phaseLine("launch", line))), Boolean(remoteDevice));
1433
+ return fail({
1434
+ code: launched.code || "STIM_LAUNCH_FAILED",
1435
+ message: launched.reason,
1436
+ remedy: simulatorLaunchFailureRemedy(Boolean(remoteDevice), udid, bundleId, logFile),
1437
+ build: {
1438
+ ...buildFailure,
1439
+ appPath,
1440
+ bundleId
1441
+ }
1442
+ });
1443
+ }
1444
+ phase("launch", `${bundleId} ${launchTimer()}`);
1445
+ }
1446
+ recordIosReloadTarget({
1447
+ d,
1448
+ root,
1449
+ physical,
1450
+ remoteDevice: Boolean(remoteDevice),
1451
+ bundleId,
1452
+ udid,
1453
+ metroPort,
1454
+ release,
1455
+ launchedAt,
1456
+ note
1457
+ });
1458
+ logWriter().write({
1459
+ src: "build",
1460
+ level: "info",
1461
+ event: "launch",
1462
+ msg: release ? `launched ${bundleId} on ${udid} (${configuration}, embedded JS bundle, no Metro)` : `launched ${bundleId} on ${udid} against Metro ${lanOriginUrl ?? `port ${metroPort}`}` + (launched?.mode === "openurl" || launched?.mode === "payload-url" ? " (expo-dev-client)" : "")
1463
+ });
1464
+ if (remoteDevice) await d.replaceCollector({
1465
+ root,
1466
+ udid,
1467
+ bundleId,
1468
+ appName,
1469
+ appExecutable,
1470
+ note
1471
+ });
1472
+ if (physical) raiseLeaseFor(release ? RELEASE_VERIFY_WAIT_MS : DEBUG_VERIFY_STEP_MS, false);
1473
+ const { state: launchState, warning: launchWarning } = await verifyIosRun({
1474
+ root,
1475
+ appPath,
1476
+ d,
1477
+ release,
1478
+ launched,
1479
+ configuration,
1480
+ phase,
1481
+ note,
1482
+ metroCheck,
1483
+ logsDir,
1484
+ launchedAt,
1485
+ metroPort,
1486
+ isExpo,
1487
+ bundleId,
1488
+ udid,
1489
+ scheme,
1490
+ physical,
1491
+ appName,
1492
+ lanAddress,
1493
+ lanOrigin: lanOriginUrl,
1494
+ remoteDevice: Boolean(remoteDevice),
1495
+ metroOrigin: typeof launched?.jsLocation === "string" ? launched.jsLocation : null
1496
+ });
1497
+ if (launchState === "fatal") return fail({
1498
+ code: "STIM_LAUNCH_FAILED",
1499
+ message: "The app failed its launch readiness check.",
1500
+ remedy: `Read the launch error above or run \`stim logs --errors\`. The full timeline is in ${logsDir}.`,
1501
+ logPath: logsDir,
1502
+ build: {
1503
+ ...buildFailure,
1504
+ appPath,
1505
+ bundleId
1506
+ }
1507
+ });
1508
+ logWriter().write(launchOutcomeRecord({
1509
+ launchState,
1510
+ release,
1511
+ bundleId,
1512
+ configuration,
1513
+ metroPort
1514
+ }));
1515
+ const leaseFacts = lease?.facts() ?? null;
1516
+ releaseLease();
1517
+ const uploadWasAbandoned = await finishIosUpload(uploadPending, remote, phase, note);
1518
+ const providerOutcome = providerUploadOutcome(providerUpload ? await providerUpload : null, providerName);
1519
+ if (providerOutcome) {
1520
+ if (providerOutcome.warn) note(chalk.yellow(phaseLine("cache", providerOutcome.line)));
1521
+ else phase("cache", providerOutcome.line);
1522
+ }
1523
+ const facts = reportIosResult({
1524
+ d,
1525
+ root,
1526
+ json,
1527
+ release,
1528
+ configuration,
1529
+ buildScheme,
1530
+ metroCheck,
1531
+ metroPort,
1532
+ logsDir,
1533
+ device,
1534
+ udid,
1535
+ appPath,
1536
+ bundleId,
1537
+ installSkipped,
1538
+ elapsed,
1539
+ startedAt,
1540
+ storeHash,
1541
+ storeKey,
1542
+ cacheHit,
1543
+ compilationCache,
1544
+ useBuildCache,
1545
+ waitedForBuild,
1546
+ launchState,
1547
+ launchWarning,
1548
+ remote,
1549
+ providerName,
1550
+ closeWriter,
1551
+ webPreviewUrl: remoteDevice?.webPreviewUrl() ?? null,
1552
+ lease: physical ? leaseFacts : void 0,
1553
+ recordRun
1554
+ });
1555
+ if (remoteWasAbandoned || uploadWasAbandoned) exitAfterFlush(0);
1556
+ return facts;
1557
+ }
1558
+ //#endregion
1559
+ //#region src/commands/ios.ts
1560
+ function writeNote(line) {
1561
+ console.error(line);
1562
+ }
1563
+ function writePhase(name, text) {
1564
+ console.error(phaseLine(name, text));
1565
+ }
1566
+ const GENERIC_SIM_DESTINATION = "generic/platform=iOS Simulator";
1567
+ const IPHONEOS_SDK = "iphoneos";
1568
+ const PROVIDER_SKIPPED_ON_DEVICE = "a device build is local-tier only: its cache key names the iphoneos slice, and a remote or provider entry is keyed for the simulator";
1569
+ function iosCommand(program) {
1570
+ registerIos(program);
1571
+ }
1572
+ function registerIos(program, deps = {}) {
1573
+ program.command("ios").description("Build (or restore from the fingerprint cache), install and launch this workspace's app on its owned simulator, wired to the reserved Metro port. Requires a running dev server (`stim start`).").option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--scheme <name>", "Shared Xcode app scheme to build; overrides automatic scheme selection, not the app URL scheme").option("--no-metro-check", "Skip the \"is this workspace's dev server running?\" gate and build anyway").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--configuration <name>", "Xcode configuration to build (e.g. Release). A non-Debug configuration embeds the JS bundle and skips Metro entirely. Overrides the ios.configuration setting. Default: Debug").option("--device-type <name>", "Simulator model to create this workspace's owned sim as, exactly as `xcrun simctl list devicetypes` names it (e.g. \"iPad Pro 13-inch (M4)\"). Overrides the ios.deviceType setting for this invocation. A model no installed runtime can create refuses with STIM_BAD_ARG and prints the models they do offer.").option("--runtime <version>", "Simulator runtime to create this workspace's owned sim on, as a version (\"18.5\") or a runtime's full name (\"iOS 18.5\"); nothing else matches. Overrides the ios.runtime setting for this invocation. An unknown version refuses with STIM_BAD_ARG and prints the installed runtimes.").option("--device [udid]", "Build the iphoneos slice for a connected iPhone, install it, and launch it, instead of using this workspace's owned simulator. With no UDID, the first connected device this workspace can lease is used. In Debug the app is wired to this workspace's Metro over the LAN. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. The build still happens here.", (value) => {
1574
+ if (REMOTE_DEVICE_BACKENDS.includes(value)) return value;
1575
+ throw new InvalidArgumentError(`expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}`);
1576
+ }).option("--wait <seconds>", "How long to wait for another workspace to release the phone it leases, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once). Only with --device.").option("--no-wait", "Install on a phone another workspace leases instead of waiting: this run takes no lease and, when both workspaces build the same app id, the install terminates the holder's running app. Only with --device.").action(async (opts) => {
1577
+ await runIos({
1578
+ ...opts,
1579
+ waitConflict: waitFlagConflict(process.argv)
1580
+ }, deps);
1581
+ });
1582
+ }
1583
+ function explicitSchemeRefusal(root, scheme, isExpo, d) {
1584
+ if (scheme === void 0) return null;
1585
+ if (!scheme.trim()) return {
1586
+ code: "STIM_BAD_ARG",
1587
+ message: "--scheme must name a non-empty shared Xcode app scheme.",
1588
+ remedy: "Pass the exact scheme name shown by xcodebuild -list."
1589
+ };
1590
+ if (d.needsPrebuild(root, "ios", isExpo)) return null;
1591
+ const project = d.discoverXcodeProject(root);
1592
+ if (project.error) return project.error;
1593
+ return d.resolveScheme(project, { scheme }).error ?? null;
1594
+ }
1595
+ async function runIos(opts = {}, overrides = {}) {
1596
+ let d = {
1597
+ ...DEFAULT_DEPS,
1598
+ ...overrides
1599
+ };
1600
+ const json = Boolean(opts.json);
1601
+ const metroCheck = opts.metroCheck !== false;
1602
+ let useBuildCache = opts.buildCache !== false;
1603
+ const phase = writePhase;
1604
+ const note = writeNote;
1605
+ const started = d.now();
1606
+ const startedAt = new Date(started).toISOString();
1607
+ const elapsed = () => d.now() - started;
1608
+ const foundRoot = d.findProjectRoot(process.cwd());
1609
+ if (!foundRoot) {
1610
+ note(chalk.red("Not in a React Native project (no package.json found)."));
1611
+ process.exit(1);
1612
+ return null;
1613
+ }
1614
+ const root = foundRoot;
1615
+ const projectProblem = appProjectProblem(root);
1616
+ if (projectProblem) {
1617
+ const { message, remedy } = projectProblem;
1618
+ note(chalk.red(phaseLine("error", message)));
1619
+ note(chalk.dim(phaseLine("remedy", remedy)));
1620
+ note(chalk.red(phaseLine("failed", "STIM_NO_PROJECT")));
1621
+ if (json) console.log(JSON.stringify({
1622
+ code: "STIM_NO_PROJECT",
1623
+ message,
1624
+ remedy
1625
+ }));
1626
+ process.exit(1);
1627
+ return null;
1628
+ }
1629
+ try {
1630
+ await d.ensureWorkspaceStorage(root, { note });
1631
+ } catch (error) {
1632
+ const code = error?.code || "STIM_WORKSPACE_STATE";
1633
+ const message = `Could not prepare this workspace's Stim state: ${error?.message || error}`;
1634
+ note(chalk.red(`${code}: ${message}`));
1635
+ note(chalk.dim("Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`)."));
1636
+ if (json) console.log(JSON.stringify({
1637
+ code,
1638
+ message,
1639
+ remedy: "Check that STIM_HOME is writable and has free space."
1640
+ }));
1641
+ process.exitCode = 1;
1642
+ return null;
1643
+ }
1644
+ const logsDir = workspaceLogsDir(root);
1645
+ const logFile = buildLogFile(root);
1646
+ let writer = null;
1647
+ const logWriter = () => writer ||= d.createWriter(logFile, { truncate: true });
1648
+ let buildLock = null;
1649
+ const releaseLock = () => {
1650
+ if (!buildLock) return;
1651
+ const held = buildLock;
1652
+ buildLock = null;
1653
+ try {
1654
+ d.releaseBuildLock(held);
1655
+ } catch (e) {
1656
+ note(chalk.dim(`Could not release the build lock at ${held.path}: ${e?.message || e}`));
1657
+ }
1658
+ };
1659
+ let leaseHandle = null;
1660
+ let stopLeaseSignals = null;
1661
+ const releaseLease = () => {
1662
+ const held = leaseHandle;
1663
+ const stopSignals = stopLeaseSignals;
1664
+ leaseHandle = null;
1665
+ stopLeaseSignals = null;
1666
+ stopSignals?.();
1667
+ try {
1668
+ held?.release();
1669
+ } catch (e) {
1670
+ note(chalk.dim(`Could not release this run's device lease: ${e?.message || e}`));
1671
+ }
1672
+ };
1673
+ let buildSlot = null;
1674
+ const releaseSlot = () => {
1675
+ if (!buildSlot) return;
1676
+ const held = buildSlot;
1677
+ buildSlot = null;
1678
+ try {
1679
+ d.releaseBuildSlot(held);
1680
+ } catch (e) {
1681
+ note(chalk.dim(`Could not release the build slot: ${e?.message || e}`));
1682
+ }
1683
+ };
1684
+ const stats = createRunRecorder({
1685
+ platform: "ios",
1686
+ write: (statsRun, at) => d.recordStats(statsRun, at),
1687
+ now: () => d.now(),
1688
+ note: (line) => note(chalk.dim(line))
1689
+ });
1690
+ const recordRun = stats.record;
1691
+ let compilationCache = COMPILATION_CACHE_NOT_RUN;
1692
+ const fail = ({ code, message, remedy = null, lines = [], logPath = null, build = null, lease }) => {
1693
+ releaseLock();
1694
+ releaseSlot();
1695
+ releaseLease();
1696
+ if (message) note(chalk.red(phaseLine("error", message)));
1697
+ for (const line of lines) note(chalk.dim(phaseLine("", line)));
1698
+ if (remedy) note(chalk.dim(phaseLine("remedy", remedy)));
1699
+ if (logPath) note(chalk.dim(phaseLine("log", logPath)));
1700
+ if (build) writeLastBuild(root, lastBuildRecord({
1701
+ ...build,
1702
+ startedAt,
1703
+ status: "failed",
1704
+ errorCode: code,
1705
+ durationMs: elapsed()
1706
+ }), { write: d.writeWorkspaceState });
1707
+ note(chalk.red(phaseLine("failed", code)));
1708
+ recordRun({
1709
+ failed: true,
1710
+ durationMs: elapsed()
1711
+ });
1712
+ if (json) console.log(JSON.stringify({
1713
+ code,
1714
+ message: message ?? null,
1715
+ remedy: remedy ?? null,
1716
+ ...compilationCache.status === "not-run" ? {} : { compilationCache },
1717
+ ...lease === void 0 ? {} : { lease }
1718
+ }));
1719
+ writer?.close?.();
1720
+ process.exit(1);
1721
+ return null;
1722
+ };
1723
+ const settingsRepoRoot = d.repoRoot(root);
1724
+ const settingsContext = {
1725
+ projectPath: root,
1726
+ gitCommonDir: d.gitCommonDir(root),
1727
+ repoRoot: settingsRepoRoot
1728
+ };
1729
+ const projectKey = statsProjectKey({
1730
+ root,
1731
+ commonDir: settingsContext.gitCommonDir,
1732
+ repoRoot: settingsRepoRoot
1733
+ });
1734
+ stats.setProject(projectKey);
1735
+ let estimatesRead = null;
1736
+ const estimates = () => estimatesRead ??= d.readEstimates({
1737
+ projectKey,
1738
+ platform: "ios"
1739
+ });
1740
+ const settings = d.resolveSettings(settingsContext);
1741
+ const [shapeError, ...moreShapeErrors] = settingShapeErrors(settings);
1742
+ if (shapeError) return fail({
1743
+ code: "STIM_BAD_ARG",
1744
+ message: shapeError,
1745
+ lines: moreShapeErrors,
1746
+ remedy: SETTING_SHAPE_REMEDY
1747
+ });
1748
+ let optimizations;
1749
+ try {
1750
+ optimizations = resolveOptimizations(settings);
1751
+ } catch (error) {
1752
+ return fail({
1753
+ code: "STIM_BAD_ARG",
1754
+ message: error.message,
1755
+ remedy: SETTING_SHAPE_REMEDY
1756
+ });
1757
+ }
1758
+ const buildProfile = optimizationBuildProfile("ios", optimizations);
1759
+ const cacheProviderConfig = d.resolveCacheProviderConfig(settingsContext);
1760
+ for (const key of unknownSettingKeys(settings)) note(phaseLine("setting", chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`)));
1761
+ const cacheProviderError = cacheProviderSettingError(settings);
1762
+ if (cacheProviderError) note(chalk.yellow(phaseLine("cache", `${cacheProviderError} Using the local cache.`)));
1763
+ const remoteSettingError = remoteDeviceSettingError(settings);
1764
+ if (remoteSettingError) return fail({
1765
+ code: "STIM_BAD_ARG",
1766
+ message: remoteSettingError,
1767
+ remedy: `Set ios.remote and android.remote to one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}.`
1768
+ });
1769
+ const poolError = parkedMaxSetting("ios").error;
1770
+ if (poolError) return fail({
1771
+ code: "STIM_BAD_ARG",
1772
+ message: poolError,
1773
+ remedy: POOL_SETTING_REMEDY
1774
+ });
1775
+ for (const settingError of [
1776
+ iosSigningIdentitySettingError(settings),
1777
+ iosSigningIdentitySha1SettingError(settings),
1778
+ iosLanHostSettingError(settings)
1779
+ ]) if (settingError) return fail({
1780
+ code: "STIM_BAD_ARG",
1781
+ message: settingError,
1782
+ remedy: SETTING_SHAPE_REMEDY
1783
+ });
1784
+ const configuration = resolveConfiguration(opts.configuration, settings);
1785
+ const buildScheme = opts.scheme;
1786
+ const release = isReleaseConfiguration(configuration);
1787
+ const cachePolicy = artifactCachePolicy(optimizations, useBuildCache, release);
1788
+ useBuildCache = cachePolicy.read;
1789
+ const deviceType = resolveDeviceType(opts.deviceType, settings);
1790
+ const runtime = resolveRuntime(opts.runtime, settings);
1791
+ const deviceFlag = opts.device;
1792
+ const physical = deviceFlag !== null && deviceFlag !== void 0 && deviceFlag !== false;
1793
+ if (physical && deviceFlag === "") return fail({
1794
+ code: "STIM_BAD_ARG",
1795
+ message: "--device was given an empty UDID.",
1796
+ remedy: "Pass `--device` on its own to take the first connected device this workspace can lease, or `--device <udid>` to name one."
1797
+ });
1798
+ if (physical && opts.remote) return fail({
1799
+ code: "STIM_BAD_ARG",
1800
+ message: "--device builds for a phone cabled to this machine, and --remote installs on a remote one.",
1801
+ remedy: "Pass only one of --device and --remote."
1802
+ });
1803
+ const noWait = opts.wait === false;
1804
+ const waitFlagged = opts.wait !== void 0;
1805
+ if (opts.waitConflict) return fail({
1806
+ code: "STIM_BAD_ARG",
1807
+ message: "--wait and --no-wait ask for opposite things.",
1808
+ remedy: "Pass `--wait <seconds>` to wait for the lease, or `--no-wait` to install without one."
1809
+ });
1810
+ if (waitFlagged && !physical) return fail({
1811
+ code: "STIM_BAD_ARG",
1812
+ message: "--wait and --no-wait only apply to a `--device` run.",
1813
+ remedy: "This workspace owns its simulator, so nothing contends for it. Drop the flag, or pass `--device`."
1814
+ });
1815
+ const waitParsed = parseDeviceWait(noWait ? void 0 : opts.wait);
1816
+ if ("error" in waitParsed) return fail({
1817
+ code: "STIM_BAD_ARG",
1818
+ message: waitParsed.error,
1819
+ remedy: "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a leased device at once."
1820
+ });
1821
+ const waitSeconds = waitParsed.seconds;
1822
+ const isExpo = d.detectIsExpo(root);
1823
+ const schemeRefusal = explicitSchemeRefusal(root, buildScheme, isExpo, d);
1824
+ if (schemeRefusal) return fail(schemeRefusal);
1825
+ const remoteBackend = physical ? null : opts.remote ?? remoteIosSetting(settings);
1826
+ const modelRefusal = deviceModelRefusal({
1827
+ deviceTypeFlag: opts.deviceType,
1828
+ runtimeFlag: opts.runtime,
1829
+ deviceType,
1830
+ runtime,
1831
+ physical,
1832
+ remoteBackend,
1833
+ listRuntimes: d.listIosRuntimes
1834
+ });
1835
+ if (modelRefusal) return fail(modelRefusal);
1836
+ const registerProject = () => d.upsertProject(root, {
1837
+ bundleId: d.detectBundleId(root) ?? void 0,
1838
+ isExpo
1839
+ });
1840
+ if (remoteBackend !== "eas") registerProject();
1841
+ const proj = d.getProject(root);
1842
+ const label = d.projectShortcut(root, proj);
1843
+ let remoteDevice = null;
1844
+ if (remoteBackend) {
1845
+ const resolved = await d.resolveRemoteContext({
1846
+ root,
1847
+ label,
1848
+ backend: remoteBackend,
1849
+ easBin: d.resolveEasCliBin(root)?.file ?? null
1850
+ });
1851
+ if ("failed" in resolved) return fail({
1852
+ code: resolved.code ?? "STIM_NO_REMOTE_SESSION",
1853
+ message: resolved.failed,
1854
+ remedy: resolved.remedy
1855
+ });
1856
+ remoteDevice = d.remoteIosDeps(resolved.ctx);
1857
+ d = {
1858
+ ...d,
1859
+ checkDeviceCapacity: remoteDevice.checkDeviceCapacity,
1860
+ ensureOwnedDevice: remoteDevice.ensureOwnedDevice,
1861
+ ensureBooted: remoteDevice.ensureBooted,
1862
+ installIosApp: remoteDevice.installIosApp,
1863
+ launchIosApp: remoteDevice.launchIosApp
1864
+ };
1865
+ }
1866
+ const limits = d.getConcurrencyLimits();
1867
+ let physicalDevice = null;
1868
+ if (physical && typeof deviceFlag !== "string") {
1869
+ const pooled = await d.selectFromPool({
1870
+ root,
1871
+ platform: "ios",
1872
+ idLabel: "udid",
1873
+ list: () => iosPoolCandidates(d.listIosDevices()).map((entry) => ({
1874
+ id: entry.udid,
1875
+ name: entry.name
1876
+ })),
1877
+ noCandidates: () => {
1878
+ const resolved = iosPoolNoCandidatesRefusal(d.listIosDevices());
1879
+ return {
1880
+ message: resolved.error,
1881
+ remedy: resolved.remedy
1882
+ };
1883
+ },
1884
+ waitSeconds,
1885
+ noWait,
1886
+ now: d.now,
1887
+ warn: (line) => note(chalk.yellow(phaseLine("lease", line)))
1888
+ });
1889
+ if (pooled.status === "refused") return fail({
1890
+ code: pooled.refusal.code,
1891
+ message: pooled.refusal.message,
1892
+ remedy: pooled.refusal.remedy,
1893
+ ...pooled.refusal.lease === null ? {} : { lease: pooled.refusal.lease }
1894
+ });
1895
+ physicalDevice = {
1896
+ udid: pooled.candidate.id,
1897
+ name: pooled.candidate.name ?? pooled.candidate.id
1898
+ };
1899
+ } else if (physical) {
1900
+ const resolved = resolveIosPhysicalDevice(typeof deviceFlag === "string" ? deviceFlag : null, d.listIosDevices());
1901
+ if (!resolved.udid) return fail({
1902
+ code: "STIM_NO_DEVICE",
1903
+ message: resolved.error,
1904
+ remedy: resolved.remedy
1905
+ });
1906
+ physicalDevice = {
1907
+ udid: resolved.udid,
1908
+ name: resolved.name ?? resolved.udid
1909
+ };
1910
+ }
1911
+ if (!physical) {
1912
+ const capacity = d.checkDeviceCapacity({
1913
+ platform: "ios",
1914
+ project: proj,
1915
+ max: limits.maxDevices
1916
+ });
1917
+ if (capacity) return fail(capacity);
1918
+ }
1919
+ let metroPort = proj?.metroPort ?? null;
1920
+ let lanAddress = null;
1921
+ let lanOriginUrl = null;
1922
+ if (!await resolveMetroPort()) return null;
1923
+ let device;
1924
+ if (physicalDevice) device = {
1925
+ deviceUdid: physicalDevice.udid,
1926
+ deviceName: physicalDevice.name,
1927
+ owned: false
1928
+ };
1929
+ else {
1930
+ const prepare = stepClock(d.now);
1931
+ try {
1932
+ device = await d.ensureOwnedDevice({
1933
+ platform: "ios",
1934
+ project: proj,
1935
+ projectPath: root,
1936
+ settingsRoot: root,
1937
+ settings,
1938
+ flags: {
1939
+ deviceType,
1940
+ runtime
1941
+ },
1942
+ note,
1943
+ out: note
1944
+ });
1945
+ } catch (e) {
1946
+ return fail({
1947
+ code: "STIM_NO_DEVICE",
1948
+ message: `Could not ensure an owned iOS simulator: ${e?.message || e}`,
1949
+ remedy: "Run `stim doctor` to check the simulator toolchain, then try again."
1950
+ });
1951
+ }
1952
+ const prepareMs = prepare();
1953
+ if (device.created || prepareMs >= 2e3) phase("device", `${deviceLabel(device, device.deviceUdid)} ${device.created ? "created" : "prepared"} (${formatDuration(prepareMs)})`);
1954
+ }
1955
+ let bootDuration = "";
1956
+ let bootPromise;
1957
+ let udid = "";
1958
+ let fingerprint = "";
1959
+ let fingerprintSources = [];
1960
+ let cacheKey = "";
1961
+ let storeHash = null;
1962
+ let storeKey = null;
1963
+ let storeSources = [];
1964
+ let appPath = null;
1965
+ let bundleId = null;
1966
+ let cacheHit = false;
1967
+ let remote = null;
1968
+ let abandonedRemote = false;
1969
+ let uploadPending = null;
1970
+ let providerUpload = null;
1971
+ let providerName = null;
1972
+ let providerLoad = null;
1973
+ const cacheWarn = createWarnOnce((line) => note(chalk.yellow(phaseLine("cache", line))));
1974
+ const loadProvider = cachePolicy.remote && cacheProviderConfig && !physical ? () => providerLoad ??= d.loadCacheProvider({
1975
+ projectRoot: root,
1976
+ config: cacheProviderConfig
1977
+ }) : null;
1978
+ if (cacheProviderConfig && physical) note(chalk.dim(phaseLine("cache", PROVIDER_SKIPPED_ON_DEVICE)));
1979
+ let waitedForBuild = null;
1980
+ let releasedWait = null;
1981
+ let swapDir = null;
1982
+ let swapFellBack = false;
1983
+ let buildFailure = {};
1984
+ async function resolveMetroPort() {
1985
+ if (release) {
1986
+ metroPort = null;
1987
+ phase("metro", `skipped (${configuration}: the JS bundle is embedded, no dev server is used)`);
1988
+ } else if (metroCheck) {
1989
+ if (!metroPort) {
1990
+ fail({
1991
+ code: "STIM_NO_METRO",
1992
+ message: "No Metro port is reserved for this workspace, so there is no dev server to build against.",
1993
+ remedy: "Run `stim start` first, or pass --no-metro-check."
1994
+ });
1995
+ return false;
1996
+ }
1997
+ const resolution = await d.resolveMetroWithRetry(d.resolveProjectMetro, metroPort, root, { onRetry: ({ delayMs }) => note(chalk.dim(phaseLine("metro", `port ${metroPort} did not verify yet; retrying in ${Math.round(delayMs / 1e3)}s (Metro may still be indexing)`))) });
1998
+ if (!resolution?.metro) {
1999
+ const supervisor = d.readWorkspaceState(root)?.supervisor ?? null;
2000
+ const supervisorAlive = Boolean(supervisor?.pid && d.pidExists(supervisor.pid));
2001
+ fail({
2002
+ code: "STIM_NO_METRO",
2003
+ message: noMetroMessage({
2004
+ port: metroPort,
2005
+ resolution,
2006
+ supervisor,
2007
+ supervisorAlive
2008
+ }),
2009
+ remedy: noMetroRemedy({
2010
+ port: metroPort,
2011
+ supervisor,
2012
+ supervisorAlive
2013
+ })
2014
+ });
2015
+ return false;
2016
+ }
2017
+ } else if (!metroPort) {
2018
+ metroPort = DEFAULT_METRO_PORT;
2019
+ note(chalk.yellow(`No Metro port is reserved for this workspace; wiring the app to ${metroPort}.`));
2020
+ }
2021
+ if (physical && metroPort !== null && !await resolveLanOrigin()) return false;
2022
+ if (remoteDevice && metroPort !== null) {
2023
+ const reachable = await d.ensureMetroReachable({
2024
+ ctx: remoteDevice.ctx,
2025
+ metroPort,
2026
+ isExpo,
2027
+ tunnelMode: tunnelModeSetting(settings) ?? void 0,
2028
+ publicUrl: publicUrlSetting(settings),
2029
+ available: d.detectProviders(binOnPath)
2030
+ });
2031
+ if ("failed" in reachable) {
2032
+ fail({
2033
+ code: reachable.code ?? "STIM_NO_REMOTE_SESSION",
2034
+ message: reachable.failed,
2035
+ remedy: reachable.remedy
2036
+ });
2037
+ return false;
2038
+ }
2039
+ }
2040
+ if (!release && metroCheck && optimizations.metroWarmup) d.warmMetro({
2041
+ port: metroPort,
2042
+ platform: "ios",
2043
+ isExpo,
2044
+ appId: proj?.bundleId,
2045
+ bundleUrl: metroWarmupUrlSetting(settings, "ios")
2046
+ });
2047
+ return true;
2048
+ }
2049
+ async function resolveLanOrigin() {
2050
+ const port = metroPort;
2051
+ const chosen = chooseLanAddress({
2052
+ pinned: iosLanHostSetting(settings),
2053
+ candidates: d.hostLanCandidates()
2054
+ });
2055
+ if (!chosen) {
2056
+ fail({
2057
+ code: "STIM_NO_LAN_ADDRESS",
2058
+ message: "A Debug run on a phone needs an address the phone can reach, and this Mac has no non-internal IPv4 interface.",
2059
+ remedy: "The phone reaches Metro over the network you share, because USB carries no reverse forward. Join a Wi-Fi or Ethernet network, or connect this Mac by cable, then run the command again."
2060
+ });
2061
+ return false;
2062
+ }
2063
+ lanAddress = chosen.address;
2064
+ lanOriginUrl = lanOriginUrlFor(chosen.address, port);
2065
+ const source = chosen.pinned ? "ios.lanHost" : `${chosen.interfaceName ?? "interface"}${chosen.candidates > 1 ? ` of ${chosen.candidates} candidates` : ""}`;
2066
+ phase("lan", `${lanOriginUrl} (${source})`);
2067
+ if (publicUrlSetting(settings) || tunnelModeSetting(settings)) note(chalk.dim(phaseLine("lan", "metro.publicUrl and metro.tunnel are ignored on --device: neither channel to a phone carries a URL, only a host and a port. They still apply to --remote.")));
2068
+ if (!metroCheck) return true;
2069
+ const reachable = await d.ensureLanReachable({
2070
+ origin: lanOriginUrl,
2071
+ metroPort: port,
2072
+ root,
2073
+ isExpo,
2074
+ logsDir
2075
+ });
2076
+ if ("failed" in reachable) {
2077
+ fail({
2078
+ code: "STIM_LAN_METRO_UNREACHABLE",
2079
+ message: reachable.failed,
2080
+ remedy: reachable.remedy
2081
+ });
2082
+ return false;
2083
+ }
2084
+ phase("lan", `gated: ${lanOriginUrl} answered as this workspace's Metro`);
2085
+ return true;
2086
+ }
2087
+ async function resolveInitialFingerprint() {
2088
+ const bootTimer = stepTimer(d.now);
2089
+ const boot = () => physicalDevice ? Promise.resolve({
2090
+ ok: true,
2091
+ udid: physicalDevice.udid
2092
+ }) : Promise.resolve(d.ensureBooted({
2093
+ platform: "ios",
2094
+ device,
2095
+ out: note
2096
+ })).catch((e) => ({
2097
+ ok: false,
2098
+ reason: String(e?.message || e)
2099
+ }));
2100
+ bootPromise = (remoteDevice?.ctx.backend === "eas" ? d.ensureRemoteBootOwned({
2101
+ root,
2102
+ platform: "ios",
2103
+ sessionName: ownedSessionName(remoteDevice.ctx.label),
2104
+ startedAt,
2105
+ boot,
2106
+ createdSessionId: remoteDevice.createdSessionId,
2107
+ abandonCreatedSession: remoteDevice.abandonCreatedSession,
2108
+ writeState: d.writeWorkspaceState,
2109
+ register: registerProject
2110
+ }) : boot()).then((result) => {
2111
+ bootDuration = bootTimer();
2112
+ return result;
2113
+ });
2114
+ udid = device.deviceUdid ?? (await bootPromise)?.udid ?? "";
2115
+ const fingerprintTimer = stepTimer(d.now);
2116
+ let computedFingerprint;
2117
+ try {
2118
+ const computed = await d.fingerprintProject(root, { platform: "ios" });
2119
+ computedFingerprint = computed?.hash ?? null;
2120
+ fingerprintSources = computed?.sources ?? [];
2121
+ } catch (e) {
2122
+ computedFingerprint = null;
2123
+ note(chalk.dim(`Fingerprinting failed: ${e?.message || e}`));
2124
+ }
2125
+ if (!computedFingerprint) {
2126
+ fail({
2127
+ code: "STIM_NO_FINGERPRINT",
2128
+ message: `Could not fingerprint ${root}: @expo/fingerprint produced no hash for it.`,
2129
+ remedy: "Check the project native inputs and the @expo/fingerprint error above, then retry."
2130
+ });
2131
+ return false;
2132
+ }
2133
+ fingerprint = computedFingerprint;
2134
+ cacheKey = buildCacheKey("ios", fingerprint, {
2135
+ scheme: buildScheme,
2136
+ ...configuration ? { configuration } : {},
2137
+ isSimulator: !physical,
2138
+ ...buildProfile ? { buildProfile } : {}
2139
+ });
2140
+ stats.setCacheKey(cacheKey);
2141
+ storeHash = fingerprint;
2142
+ storeKey = cacheKey;
2143
+ storeSources = fingerprintSources;
2144
+ const found = await resolveTieredBuild({
2145
+ local: filesystemBuildCapability({
2146
+ resolve: d.resolveBuild,
2147
+ store: d.storeBuild,
2148
+ sources: fingerprintSources
2149
+ }),
2150
+ loadProvider,
2151
+ target: {
2152
+ projectRoot: root,
2153
+ platform: "ios",
2154
+ key: cacheKey
2155
+ },
2156
+ destinationDir: providerDownloadPath(workspaceDir(root)),
2157
+ ensureDestination: prepareProviderDownloadDir,
2158
+ skipRead: !useBuildCache,
2159
+ warn: cacheWarn
2160
+ });
2161
+ const cached = found?.tier === "local" ? found.path : null;
2162
+ cacheHit = cached ? "local" : false;
2163
+ let missDiff = "";
2164
+ let missUntracked = null;
2165
+ if (!cached) {
2166
+ const lastBuild = d.readWorkspaceState(root)?.lastBuild ?? null;
2167
+ const miss = describeFingerprintMiss({
2168
+ platform: "ios",
2169
+ current: {
2170
+ hash: fingerprint,
2171
+ sources: fingerprintSources
2172
+ },
2173
+ lastBuild
2174
+ });
2175
+ if (miss) {
2176
+ missDiff = fingerprintDiffSuffix(miss.changed);
2177
+ logWriter().write(fingerprintDiffRecord({
2178
+ changed: miss.changed,
2179
+ previousHash: miss.previousHash,
2180
+ hash: fingerprint
2181
+ }));
2182
+ } else if (useBuildCache) missUntracked = untrackedMissLine(d.untrackedNativeFiles({ projectRoot: root }));
2183
+ }
2184
+ phase("fingerprint", `${shortHash(fingerprint)} ${cached ? "hit" : "miss"}${useBuildCache ? "" : opts.buildCache === false ? " (--no-build-cache)" : " (cache reuse off in config)"} ${fingerprintTimer()}${missDiff}`);
2185
+ if (missUntracked) note(chalk.dim(phaseLine("fingerprint", missUntracked)));
2186
+ if (found?.tier === "provider") {
2187
+ cacheHit = "remote";
2188
+ providerName = found.providerName ?? null;
2189
+ phase("cache", `provider hit (${providerName})${found.storedLocally ? " -> stored locally" : ""}`);
2190
+ }
2191
+ appPath = found?.path ?? null;
2192
+ return true;
2193
+ }
2194
+ async function resolveRemoteArtifact() {
2195
+ if (physical || !cachePolicy.remote || buildProfile || buildScheme) return;
2196
+ if (!appPath) {
2197
+ const loaded = await d.loadProjectProvider(root, { isExpo });
2198
+ if (loaded?.unavailable) note(chalk.yellow(phaseLine("cache", `provider not usable: ${loaded.unavailable}`)));
2199
+ else if (loaded?.provider) remote = loaded;
2200
+ if (remote?.name === "eas") {
2201
+ const auth = d.checkEasAuth({
2202
+ projectRoot: root,
2203
+ owner: loaded?.owner || null
2204
+ });
2205
+ const authNote = easAuthNote(auth);
2206
+ if (authNote) note(chalk.yellow(phaseLine("cache", authNote)));
2207
+ if (auth?.code === "logged-out") remote = null;
2208
+ }
2209
+ }
2210
+ if (remote && useBuildCache) {
2211
+ const remoteTimer = stepTimer(d.now);
2212
+ const hit = await d.resolveRemote({
2213
+ logWriter: logWriter(),
2214
+ provider: remote.provider,
2215
+ platform: "ios",
2216
+ projectRoot: root,
2217
+ fingerprintHash: fingerprint,
2218
+ runOptions: configuration ? { configuration } : null
2219
+ });
2220
+ if (hit?.appPath) {
2221
+ let stored = null;
2222
+ try {
2223
+ stored = d.storeBuild("ios", cacheKey, hit.appPath, { sources: fingerprintSources });
2224
+ } catch (e) {
2225
+ note(chalk.yellow(phaseLine("cache", `remote hit could not be stored locally: ${e?.message || e}`)));
2226
+ }
2227
+ appPath = stored || hit.appPath;
2228
+ cacheHit = "remote";
2229
+ phase("cache", `remote hit (${remote.name})${stored ? " -> stored locally" : ""} ${remoteTimer()}`);
2230
+ } else if (hit?.timedOut) {
2231
+ abandonedRemote = true;
2232
+ note(chalk.yellow(phaseLine("cache", `${remote.name} did not answer within ${formatDuration(RESOLVE_TIMEOUT_MS)}; building instead`)));
2233
+ } else if (hit?.failed) {
2234
+ const authNote = remote.name === "eas" && isEasAuthFailureText(hit.failed) ? easAuthNote({
2235
+ code: "logged-out",
2236
+ reason: hit.failed
2237
+ }) : null;
2238
+ note(chalk.yellow(phaseLine("cache", authNote || `${remote.name} could not be used: ${hit.failed}; building instead`)));
2239
+ } else phase("cache", `remote miss (${remote.name}) ${remoteTimer()}`);
2240
+ }
2241
+ }
2242
+ async function waitForSharedBuild() {
2243
+ if (!useBuildCache) return true;
2244
+ const waitStarted = d.now();
2245
+ let failedHolder;
2246
+ while (!appPath) {
2247
+ let attempt = null;
2248
+ try {
2249
+ attempt = d.acquireBuildLock({
2250
+ platform: "ios",
2251
+ key: cacheKey,
2252
+ root,
2253
+ logFile
2254
+ });
2255
+ } catch (e) {
2256
+ const refusal = claimFailure(e, "stim ios");
2257
+ if (refusal) {
2258
+ fail({
2259
+ code: refusal.code,
2260
+ message: refusal.message,
2261
+ remedy: refusal.remedy,
2262
+ build: {
2263
+ fingerprint,
2264
+ cacheKey,
2265
+ cacheHit,
2266
+ cacheSkipped: !useBuildCache
2267
+ }
2268
+ });
2269
+ return false;
2270
+ }
2271
+ note(chalk.yellow(phaseLine("build", `could not take the build lock: ${e?.message || e}; building anyway`)));
2272
+ }
2273
+ if (attempt?.acquired) {
2274
+ buildLock = attempt;
2275
+ const previous = attempt.tookOver ?? failedHolder;
2276
+ if (previous) note(chalk.yellow(phaseLine("build", takeoverLine(previous))));
2277
+ break;
2278
+ } else if (attempt?.held) {
2279
+ releasedWait = null;
2280
+ const held = attempt.held;
2281
+ const who = held.projectRoot || "another workspace";
2282
+ phase("build", `${who} is already building ${shortHash(fingerprint)} (pid ${held.pid})${held.logFile ? ` -- tail ${held.logFile}` : ""} -- stim guide lifecycle concurrency`);
2283
+ let waited = null;
2284
+ try {
2285
+ const ceilingMs = WAIT_CEILING_MS - (d.now() - waitStarted);
2286
+ if (ceilingMs <= 0) throw Object.assign(/* @__PURE__ */ new Error(`Waited ${formatDuration(d.now() - waitStarted)} for shared builds without an artifact; ${who} (pid ${held.pid}) holds ${attempt.path}.`), {
2287
+ code: "STIM_BUILD_WAIT_TIMEOUT",
2288
+ lockPath: attempt.path
2289
+ });
2290
+ waited = await d.waitForBuild({
2291
+ platform: "ios",
2292
+ key: cacheKey,
2293
+ out: note,
2294
+ ceilingMs
2295
+ });
2296
+ } catch (e) {
2297
+ const refusal = claimFailure(e, "stim ios");
2298
+ if (refusal) {
2299
+ fail({
2300
+ code: refusal.code,
2301
+ message: refusal.message,
2302
+ remedy: refusal.remedy,
2303
+ build: {
2304
+ fingerprint,
2305
+ cacheKey,
2306
+ cacheHit,
2307
+ cacheSkipped: !useBuildCache
2308
+ }
2309
+ });
2310
+ return false;
2311
+ }
2312
+ const err = e;
2313
+ if (err?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw e;
2314
+ fail({
2315
+ code: "STIM_BUILD_WAIT_TIMEOUT",
2316
+ message: err.message,
2317
+ remedy: `Check pid ${held.pid}; if it is not really building, remove ${err.lockPath} and run \`stim ios\` again.`,
2318
+ build: {
2319
+ fingerprint,
2320
+ cacheKey,
2321
+ cacheHit,
2322
+ cacheSkipped: !useBuildCache
2323
+ }
2324
+ });
2325
+ return false;
2326
+ }
2327
+ if (waited?.hit) {
2328
+ appPath = waited.hit ?? null;
2329
+ cacheHit = "local";
2330
+ waitedForBuild = {
2331
+ pid: held.pid,
2332
+ ms: waited.waitedMs
2333
+ };
2334
+ phase("build", `waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache -- stim guide lifecycle concurrency`);
2335
+ } else if (waited?.lockReleased) {
2336
+ failedHolder = void 0;
2337
+ releasedWait = {
2338
+ facts: {
2339
+ pid: held.pid,
2340
+ ms: waited.waitedMs
2341
+ },
2342
+ who
2343
+ };
2344
+ phase("build", `${who}'s build lock was released; rechecking this workspace before building`);
2345
+ } else {
2346
+ note(chalk.yellow(phaseLine("build", `${who}'s build ended without an artifact (${waited?.builderFailed}); retrying the build lock`)));
2347
+ failedHolder = held;
2348
+ }
2349
+ } else break;
2350
+ }
2351
+ return true;
2352
+ }
2353
+ const prepareDeviceApp = async (path, { fresh }) => {
2354
+ const refuse = (code, reason, remedy) => {
2355
+ if (fresh) {
2356
+ fail({
2357
+ code,
2358
+ message: reason,
2359
+ remedy,
2360
+ build: {
2361
+ ...buildFailure,
2362
+ appPath: path
2363
+ }
2364
+ });
2365
+ return null;
2366
+ }
2367
+ note(chalk.yellow(phaseLine("cache", `${reason} -- building fresh instead`)));
2368
+ note(chalk.dim(phaseLine("", remedy)));
2369
+ swapFellBack = true;
2370
+ return null;
2371
+ };
2372
+ const gateProfile = () => {
2373
+ const gate = d.gateProfileForDevice({
2374
+ appPath: path,
2375
+ udid,
2376
+ configuration
2377
+ });
2378
+ return gate.ok ? path : refuse(gate.code, gate.reason, gate.remedy);
2379
+ };
2380
+ if (release) {
2381
+ if (!fresh) {
2382
+ note(chalk.yellow(phaseLine("cache", `a cached ${configuration} device app carries its builder's JS, and the device JS swap lands with phase 6 of appandflow/stim#178 -- building fresh instead, which bakes in this workspace's JS`)));
2383
+ swapFellBack = true;
2384
+ return null;
2385
+ }
2386
+ return gateProfile();
2387
+ }
2388
+ if (d.devClientScheme(root, path)) return gateProfile();
2389
+ let copy;
2390
+ try {
2391
+ copy = copyAppAside(path);
2392
+ } catch (e) {
2393
+ return refuse("STIM_INSTALL_FAILED", `Could not copy ${path} aside to write its ip.txt: ${e?.message || e}`, "Free space in the temporary directory and run the command again.");
2394
+ }
2395
+ writeIpTxt(copy.appPath, lanAddress, metroPort);
2396
+ const sealed = d.sealAppForDevice({
2397
+ appPath: copy.appPath,
2398
+ udid,
2399
+ configuration,
2400
+ pinnedName: iosSigningIdentitySetting(settings),
2401
+ pinnedSha1: iosSigningIdentitySha1Setting(settings)
2402
+ });
2403
+ if (!sealed.ok) {
2404
+ try {
2405
+ rmSync(copy.tmpDir, {
2406
+ recursive: true,
2407
+ force: true
2408
+ });
2409
+ } catch {}
2410
+ for (const line of sealed.lastLines ?? []) note(chalk.dim(phaseLine("", line)));
2411
+ return refuse(sealed.code, sealed.reason, sealed.remedy);
2412
+ }
2413
+ if (swapDir) try {
2414
+ rmSync(swapDir, {
2415
+ recursive: true,
2416
+ force: true
2417
+ });
2418
+ } catch {}
2419
+ swapDir = copy.tmpDir;
2420
+ phase("ip.txt", `${lanAddress}:${metroPort} written into the install copy and re-sealed with "${sealed.identity.name}"${sealed.mode === "preserve-metadata" ? "" : ` (${sealed.mode})`}`);
2421
+ return copy.appPath;
2422
+ };
2423
+ const installableCachedApp = async (cachedPath) => {
2424
+ if (physical) return prepareDeviceApp(cachedPath, { fresh: false });
2425
+ if (!release) return cachedPath;
2426
+ phase("swap", `regenerating this workspace's JS for the cached ${configuration} app`);
2427
+ const swap = await d.swapJsBundle({
2428
+ root,
2429
+ isExpo,
2430
+ cachedAppPath: cachedPath,
2431
+ logWriter: logWriter()
2432
+ });
2433
+ if (swap?.ok && swap.appPath) {
2434
+ if (swap.note) note(chalk.yellow(phaseLine("swap", swap.note)));
2435
+ swapDir = swap.tmpDir ?? null;
2436
+ phase("swap", `${swap.hermes ? "hermes bytecode" : "plain JS"} + assets replaced, re-signed (${formatDuration(swap.durationMs ?? 0)})`);
2437
+ return swap.appPath;
2438
+ }
2439
+ note(chalk.yellow(phaseLine("swap", `failed at ${swap?.step || "unknown step"}: ${swap?.reason || "unknown reason"} -- building fresh instead (a cached ${configuration} app carries its builder's JS; it is never installed after a failed swap)`)));
2440
+ for (const line of swap?.lastLines ?? []) note(chalk.dim(phaseLine("", line)));
2441
+ swapFellBack = true;
2442
+ return null;
2443
+ };
2444
+ async function prepareCachedArtifact() {
2445
+ if (appPath && cacheHit) {
2446
+ const prepared = await installableCachedApp(appPath);
2447
+ appPath = prepared;
2448
+ if (!prepared) {
2449
+ cacheHit = false;
2450
+ waitedForBuild = null;
2451
+ }
2452
+ }
2453
+ }
2454
+ async function buildArtifact() {
2455
+ buildFailure = {
2456
+ fingerprint,
2457
+ cacheKey,
2458
+ cacheHit,
2459
+ cacheSkipped: !useBuildCache
2460
+ };
2461
+ if (!appPath) try {
2462
+ if (limits.maxBuilds) try {
2463
+ buildSlot = await d.acquireBuildSlot({
2464
+ max: limits.maxBuilds,
2465
+ root,
2466
+ logFile,
2467
+ out: note
2468
+ });
2469
+ } catch (e) {
2470
+ const refusal = claimFailure(e, "stim ios");
2471
+ if (refusal) {
2472
+ fail({
2473
+ code: refusal.code,
2474
+ message: refusal.message,
2475
+ remedy: refusal.remedy,
2476
+ build: buildFailure
2477
+ });
2478
+ return false;
2479
+ }
2480
+ note(chalk.yellow(phaseLine("build", `could not take a build slot: ${e?.message || e}; building anyway`)));
2481
+ }
2482
+ const mutatingSteps = [];
2483
+ if (d.needsPrebuild(root, "ios", isExpo)) {
2484
+ const result = await d.runPrebuild(root, "ios", logWriter());
2485
+ if (result?.failed) {
2486
+ phase("prebuild", "FAILED");
2487
+ fail({
2488
+ code: result.code || "STIM_PREBUILD_FAILED",
2489
+ message: result.reason || "expo prebuild failed.",
2490
+ remedy: result.remedy || `See ${logFile} for the transcript.`,
2491
+ lines: (result.lastLines || []).slice(-5),
2492
+ build: buildFailure
2493
+ });
2494
+ return false;
2495
+ }
2496
+ phase("prebuild", `ios/ absent -> generated (${formatDuration(result?.durationMs ?? 0)})`);
2497
+ mutatingSteps.push("prebuild");
2498
+ }
2499
+ const podState = d.readPodState(root);
2500
+ const action = podAction(podState, d.podsAreStale(podState.lockText, podState.manifestText));
2501
+ if (action.install) {
2502
+ const result = await d.runPodInstall(root, logWriter(), { estimateMs: estimates().podsMs });
2503
+ const podCommand = result?.command || "pod install";
2504
+ for (const line of result?.notes || []) note(chalk.dim(phaseLine("pods", line)));
2505
+ if (result?.failed) {
2506
+ phase("pods", "FAILED");
2507
+ fail({
2508
+ code: result.code || "STIM_DEPS_FAILED",
2509
+ message: result.reason || "`pod install` failed.",
2510
+ remedy: result.remedy || `See ${logFile} for the transcript.`,
2511
+ lines: result.diagnosticLines?.length ? result.diagnosticLines : (result.lastLines || []).slice(-5),
2512
+ build: buildFailure
2513
+ });
2514
+ return false;
2515
+ }
2516
+ stats.setPodsMs(result?.durationMs ?? 0);
2517
+ phase("pods", `${action.reason} -> installed with \`${podCommand}\` (${formatDuration(result?.durationMs ?? 0)})`);
2518
+ mutatingSteps.push(podCommand);
2519
+ }
2520
+ if (mutatingSteps.length) {
2521
+ const after = await refingerprintAfterMutation({
2522
+ projectRoot: root,
2523
+ platform: "ios",
2524
+ previousHash: fingerprint,
2525
+ fingerprint: d.fingerprintProject
2526
+ });
2527
+ if (!after) {
2528
+ storeHash = null;
2529
+ storeKey = null;
2530
+ buildFailure = {
2531
+ ...buildFailure,
2532
+ fingerprint: null,
2533
+ cacheKey: null
2534
+ };
2535
+ note(chalk.yellow(phaseLine("fingerprint", `unavailable after ${mutatingSteps.join(", ")}; the build will be installed but not cached`)));
2536
+ } else if (after.moved) {
2537
+ storeHash = after.hash;
2538
+ storeSources = after.sources;
2539
+ storeKey = buildCacheKey("ios", after.hash, {
2540
+ scheme: buildScheme,
2541
+ ...configuration ? { configuration } : {},
2542
+ isSimulator: !physical,
2543
+ ...buildProfile ? { buildProfile } : {}
2544
+ });
2545
+ note(chalk.dim(phaseLine("fingerprint", `${shortHash(fingerprint)} -> ${shortHash(storeHash)} (after ${mutatingSteps.join(", ")})`)));
2546
+ const late = useBuildCache ? d.resolveBuild("ios", storeKey) : null;
2547
+ if (late) {
2548
+ const prepared = await installableCachedApp(late);
2549
+ if (prepared) {
2550
+ appPath = prepared;
2551
+ cacheHit = "local";
2552
+ phase("cache", `hit ${shortHash(storeHash)} (post-${mutatingSteps.join("/")} key)`);
2553
+ if (releasedWait) {
2554
+ waitedForBuild = releasedWait.facts;
2555
+ phase("build", `waited ${formatDuration(waitedForBuild.ms)} for ${releasedWait.who}'s build -> installed from cache -- stim guide lifecycle concurrency`);
2556
+ }
2557
+ }
2558
+ }
2559
+ }
2560
+ }
2561
+ if (!appPath) {
2562
+ phase("build", `compiling ${configuration || "Debug"} with xcodebuild`);
2563
+ const result = await d.buildIos({
2564
+ root,
2565
+ scheme: buildScheme,
2566
+ udid,
2567
+ destination: remoteDevice ? GENERIC_SIM_DESTINATION : null,
2568
+ ...physical ? { sdk: IPHONEOS_SDK } : {},
2569
+ logWriter: logWriter(),
2570
+ ...configuration ? { configuration } : {},
2571
+ estimateMs: estimates().coldBuildMs,
2572
+ optimizations: optimizations.ios
2573
+ });
2574
+ compilationCache = result.compilationCache ?? COMPILATION_CACHE_UNAVAILABLE;
2575
+ phase("cache", `compilation cache ${compilationCacheActivityLine(compilationCache)}`);
2576
+ if (result?.failed) {
2577
+ phase("build", `FAILED after ${formatDuration(result.durationMs)}`);
2578
+ printDiagnostics(note, result);
2579
+ const report = xcodeFailureReport(result, logFile);
2580
+ fail({
2581
+ code: result.code || "STIM_BUILD_FAILED",
2582
+ message: report.message,
2583
+ remedy: report.remedy,
2584
+ logPath: logFile,
2585
+ build: buildFailure
2586
+ });
2587
+ return false;
2588
+ }
2589
+ stats.setBuildMs(result.durationMs ?? 0);
2590
+ phase("build", `ok (${formatDuration(result.durationMs)})`);
2591
+ appPath = result.appPath ?? null;
2592
+ bundleId = result.bundleId ?? null;
2593
+ if (storeKey && cachePolicy.write) try {
2594
+ const stored = await storeTieredBuild({
2595
+ local: filesystemBuildCapability({
2596
+ resolve: d.resolveBuild,
2597
+ store: d.storeBuild,
2598
+ sources: storeSources
2599
+ }),
2600
+ loadProvider,
2601
+ target: {
2602
+ projectRoot: root,
2603
+ platform: "ios",
2604
+ key: storeKey
2605
+ },
2606
+ sourcePath: appPath,
2607
+ overwrite: !useBuildCache || swapFellBack,
2608
+ warn: cacheWarn
2609
+ });
2610
+ providerUpload = stored.providerUpload;
2611
+ providerName = stored.providerName ?? providerName;
2612
+ } catch (e) {
2613
+ note(chalk.yellow(`Could not store the build in the shared cache: ${e?.message || e}`));
2614
+ }
2615
+ if (physical) {
2616
+ const prepared = await prepareDeviceApp(appPath, { fresh: true });
2617
+ if (!prepared) return false;
2618
+ appPath = prepared;
2619
+ }
2620
+ if (remote && !physical && storeHash) uploadPending = d.uploadRemote({
2621
+ logWriter: logWriter(),
2622
+ provider: remote.provider,
2623
+ platform: "ios",
2624
+ projectRoot: root,
2625
+ fingerprintHash: storeHash,
2626
+ buildPath: appPath,
2627
+ runOptions: configuration ? { configuration } : null
2628
+ });
2629
+ }
2630
+ } finally {
2631
+ releaseLock();
2632
+ releaseSlot();
2633
+ }
2634
+ return true;
2635
+ }
2636
+ try {
2637
+ if (!await resolveInitialFingerprint()) return null;
2638
+ await resolveRemoteArtifact();
2639
+ if (!await waitForSharedBuild()) return null;
2640
+ await prepareCachedArtifact();
2641
+ if (!await buildArtifact()) return null;
2642
+ if (physicalDevice) {
2643
+ const acquired = await d.acquireRunLease({
2644
+ root,
2645
+ platform: "ios",
2646
+ id: physicalDevice.udid,
2647
+ deviceName: physicalDevice.name,
2648
+ idLabel: "udid",
2649
+ waitSeconds,
2650
+ noWait,
2651
+ installBoundMs: DEVICECTL_INSTALL_TIMEOUT_MS,
2652
+ appId: bundleId ?? proj?.bundleId ?? null,
2653
+ holderAppId: (holder) => d.getProject(holder)?.bundleId ?? null,
2654
+ now: d.now,
2655
+ warn: (line) => note(chalk.yellow(phaseLine("lease", line)))
2656
+ });
2657
+ if (acquired.status === "refused") return fail({
2658
+ code: acquired.refusal.code,
2659
+ message: acquired.refusal.message,
2660
+ remedy: acquired.refusal.remedy,
2661
+ lease: acquired.refusal.lease
2662
+ });
2663
+ leaseHandle = d.runLease({
2664
+ root,
2665
+ platform: "ios",
2666
+ kind: acquired.status === "leased" ? acquired.kind : null,
2667
+ expiresAt: acquired.status === "leased" ? acquired.expiresAt : null
2668
+ });
2669
+ if (acquired.status === "leased") {
2670
+ stopLeaseSignals = d.releaseLeaseOnSignal(releaseLease);
2671
+ phase("lease", `${acquired.kind} lease on ${physicalDevice.udid} until ${leaseExpiryText(acquired.expiresAt, d.now())}`);
2672
+ }
2673
+ }
2674
+ try {
2675
+ return await finishIosRun({
2676
+ d,
2677
+ root,
2678
+ json,
2679
+ release,
2680
+ configuration,
2681
+ buildScheme,
2682
+ isExpo,
2683
+ metroCheck,
2684
+ metroPort,
2685
+ logsDir,
2686
+ logFile,
2687
+ device,
2688
+ udid,
2689
+ physical,
2690
+ lanAddress,
2691
+ lanOriginUrl,
2692
+ remoteDevice,
2693
+ bootPromise,
2694
+ bootDuration: () => bootDuration,
2695
+ appPath,
2696
+ bundleId,
2697
+ swapDir,
2698
+ buildFailure,
2699
+ fail,
2700
+ phase,
2701
+ note,
2702
+ logWriter,
2703
+ uploadPending,
2704
+ providerUpload,
2705
+ providerName,
2706
+ remote,
2707
+ abandonedRemote,
2708
+ elapsed,
2709
+ startedAt,
2710
+ storeHash,
2711
+ storeKey,
2712
+ cacheHit,
2713
+ compilationCache,
2714
+ useBuildCache,
2715
+ waitedForBuild,
2716
+ closeWriter: () => writer?.close?.(),
2717
+ lease: leaseHandle,
2718
+ releaseLease,
2719
+ recordRun
2720
+ });
2721
+ } finally {
2722
+ releaseLease();
2723
+ }
2724
+ } catch (error) {
2725
+ recordRun({
2726
+ failed: true,
2727
+ durationMs: elapsed()
2728
+ });
2729
+ throw error;
2730
+ }
2731
+ }
2732
+ //#endregion
2733
+ export { appNameFromPath, buildLogFile, cacheDescription, collectorEntry, iosCommand as default, devClientScheme, deviceLabel, ensureWorkspaceStorageSafely, formatDuration, gateShouldRetry, iosConfigurationSetting, iosFacts, isReleaseConfiguration, lastBuildRecord, noMetroMessage, phaseLine, pickDevClientScheme, podAction, registerIos, replaceCollector, resolveConfiguration, resolveDeviceType, resolveMetroWithRetry, resolveRuntime, schemesFromInfoPlist, shortHash, shortUdid, writeLastBuild };