stim 1.14.0 → 1.16.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 (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,964 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { D as withConfigLock$1, _ as loadConfig$1, c as ensureConfig } from "./config-D0DbpZtM.mjs";
3
+ import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
4
+ import { c as writeWorkspaceState, u as ensureWorkspaceStorage } from "./workspace-state-B8-aJ1_5.mjs";
5
+ import { t as forgetCreatedDevice } from "./created-devices-D5owjN4C.mjs";
6
+ import { A as listAvds, D as hostSystemImageArch, E as getAvdNameForSerial, O as listAdbDevices, R as ownedAvdName, W as pickDefaultSystemImage, _ as avdStorageRoots, a as androidDeviceAbi, b as createOwnedAvd, d as androidToolPath, h as avdPathExists, j as listInstalledSystemImages, k as listAvdDeviceProfiles, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator } from "./android-B_NumVyQ.mjs";
7
+ import { C as resolveIosCreation, i as createOwnedIosSim, j as readHostMemoryPressure, n as bootIosSim, u as listAllIosSims } from "./ios-BaInn3sW.mjs";
8
+ import { i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-CL6vDs2Y.mjs";
9
+ import { a as requiredMacosRecord, r as macosProcess } from "./state-3DwSQIBK.mjs";
10
+ import { r as createNdjsonWriter } from "./ndjson-PclOIwVY.mjs";
11
+ import { n as parseLogStreamLine } from "./ios-Cr1h5gnO.mjs";
12
+ import { _ as installIosApp, b as launchAndroidReleaseApp, h as installAndroidApp, s as androidAppProcess, v as iosAppProcess, x as launchIosApp } from "./app-install-CKWdbXIX.mjs";
13
+ import { h as findAapt } from "./support-CAP8qDUF.mjs";
14
+ import { n as stopMacosAppHeld } from "./stop-B0Yxtmiy.mjs";
15
+ import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
16
+ import { createHash, randomUUID } from "node:crypto";
17
+ import { chmodSync, closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, symlinkSync, writeFileSync } from "node:fs";
18
+ import { dirname, isAbsolute, join, posix, relative, sep } from "node:path";
19
+ import { LOG_ROTATE_BYTES, configDir, withDirLock } from "@stim-cli/core";
20
+ import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedIosLogsDir, hostedMacosAppSlot, hostedMacosBundleId, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedIosLogsCheckpoint, readMacosRecord, readNdjsonGenerations } from "@stim-cli/core/state";
21
+ import { availableParallelism, freemem, homedir, loadavg } from "node:os";
22
+ //#region src/device-host/ios-logs.ts
23
+ function logDate(ms) {
24
+ return new Date(ms).toISOString().replace("T", " ").replace(".000Z", "+0000");
25
+ }
26
+ /** Captures a bounded unified-log window only from this home's exact ledger-owned simulator. */
27
+ function collectHostedIosLogs(home, session, attempt, since, final = false) {
28
+ const device = readHostedDevice(home);
29
+ assertHostedDeviceLedger(home, device.udid);
30
+ if (readHostedAppMetadata(session, attempt, home).state !== "installed") return false;
31
+ const directory = hostedIosLogsDir(home);
32
+ const previous = readHostedIosLogsCheckpoint(home);
33
+ const beginning = Math.floor(since / 1e3) * 1e3;
34
+ let newest;
35
+ if (!previous) {
36
+ for (const entry of readNdjsonGenerations(join(directory, "device.ndjson"))) if (typeof entry.ts === "number" && (newest === void 0 || entry.ts > newest)) newest = entry.ts;
37
+ }
38
+ const checkpoint = previous?.until ?? Math.max(beginning, Math.floor((newest ?? beginning) / 1e3) * 1e3);
39
+ let from = Math.max(beginning, previous ? checkpoint - 5e3 : checkpoint);
40
+ const now = Math.floor(Date.now() / 1e3) * 1e3;
41
+ if (now < checkpoint) return false;
42
+ const backlogMs = Math.max(1e3, now - checkpoint);
43
+ let windowMs = !final && previous?.windowMs ? Math.min(previous.windowMs * 2, backlogMs) : backlogMs;
44
+ let until = Math.min(now, checkpoint + windowMs);
45
+ let end = until === now ? until + 1e3 : until;
46
+ const deadline = Date.now() + 1e4;
47
+ const exec = getExecutor();
48
+ const executable = exec.runFile("/usr/libexec/PlistBuddy", [
49
+ "-c",
50
+ "Print :CFBundleExecutable",
51
+ join(hostedAppArea(session, attempt, home), "App.app", "Info.plist")
52
+ ], {
53
+ timeoutMs: 2e3,
54
+ killSignal: "SIGKILL"
55
+ });
56
+ if (!executable || /["\\/\0\r\n]/.test(executable)) throw new Error("The app executable cannot form a log predicate.");
57
+ let output;
58
+ for (;;) {
59
+ const remaining = deadline - Date.now();
60
+ if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
61
+ try {
62
+ output = exec.runFile("xcrun", [
63
+ "simctl",
64
+ "spawn",
65
+ device.udid,
66
+ "log",
67
+ "show",
68
+ "--style",
69
+ "ndjson",
70
+ "--predicate",
71
+ `processImagePath ENDSWITH "/App.app/${executable}"`,
72
+ "--info",
73
+ "--start",
74
+ logDate(from),
75
+ "--end",
76
+ logDate(end)
77
+ ], {
78
+ timeoutMs: Math.min(remaining, 4e3),
79
+ killSignal: "SIGKILL"
80
+ });
81
+ break;
82
+ } catch (error) {
83
+ const span = final ? until - from : until - checkpoint;
84
+ if (span <= 1e3) throw error;
85
+ windowMs = Math.max(1e3, Math.floor(span / 4 / 1e3) * 1e3);
86
+ if (final) from = Math.max(beginning, now - windowMs);
87
+ else until = Math.min(now, checkpoint + windowMs);
88
+ end = until === now ? until + 1e3 : until;
89
+ if (previous) {
90
+ mkdirSync(directory, {
91
+ recursive: true,
92
+ mode: 448
93
+ });
94
+ withDirLock(join(directory, "query.lock"), () => {
95
+ const temporary = join(directory, "checkpoint.json.tmp");
96
+ writeFileSync(temporary, JSON.stringify({
97
+ until: checkpoint,
98
+ boundary: previous.boundary,
99
+ windowMs
100
+ }), { mode: 384 });
101
+ renameSync(temporary, join(directory, "checkpoint.json"));
102
+ });
103
+ }
104
+ }
105
+ }
106
+ const seen = /* @__PURE__ */ new Map();
107
+ for (const digest of previous?.boundary ?? []) seen.set(digest, (seen.get(digest) ?? 0) + 1);
108
+ const boundary = [];
109
+ const records = [];
110
+ if (final && from > checkpoint) records.push({
111
+ ts: from,
112
+ src: "device",
113
+ platform: "ios",
114
+ level: "warn",
115
+ msg: `Hosted iOS native log gap: dropped interval [${new Date(checkpoint).toISOString()}, ${new Date(from).toISOString()}) to collect the final tail within its budget.`
116
+ });
117
+ for (const line of output.split("\n")) {
118
+ const parsed = parseLogStreamLine(line);
119
+ if (!parsed || typeof parsed.ts !== "number" || parsed.ts < from || parsed.ts > end) continue;
120
+ const entry = JSON.parse(line);
121
+ const digest = createHash("sha256").update(JSON.stringify(Object.entries(entry).toSorted(([a], [b]) => a.localeCompare(b)))).digest("hex");
122
+ if (parsed.ts >= until - 5e3) boundary.push(digest);
123
+ if (newest !== void 0 && parsed.ts <= newest) continue;
124
+ const count = seen.get(digest) ?? 0;
125
+ if (count) {
126
+ seen.set(digest, count - 1);
127
+ continue;
128
+ }
129
+ records.push({
130
+ ...parsed,
131
+ platform: "ios"
132
+ });
133
+ }
134
+ mkdirSync(directory, {
135
+ recursive: true,
136
+ mode: 448
137
+ });
138
+ withDirLock(join(directory, "query.lock"), () => {
139
+ const writer = createNdjsonWriter(join(directory, "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
140
+ try {
141
+ for (const entry of records) if (!writer.write(entry)) throw new Error("Could not persist hosted iOS native logs.");
142
+ } finally {
143
+ writer.close();
144
+ }
145
+ const temporary = join(directory, "checkpoint.json.tmp");
146
+ try {
147
+ writeFileSync(temporary, JSON.stringify({
148
+ until,
149
+ boundary,
150
+ ...until < now ? { windowMs } : {}
151
+ }), { mode: 384 });
152
+ renameSync(temporary, join(directory, "checkpoint.json"));
153
+ } finally {
154
+ rmSync(temporary, { force: true });
155
+ }
156
+ });
157
+ return until < now;
158
+ }
159
+ //#endregion
160
+ //#region src/device-host/app.ts
161
+ function inside(root, path) {
162
+ const diff = relative(root, path);
163
+ return diff !== "" && diff !== ".." && !diff.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(diff);
164
+ }
165
+ function removeHostedAppData(home) {
166
+ const area = realpathSync(join(home, ".."));
167
+ const assertInside = (path) => {
168
+ if (!inside(area, realpathSync(path))) throw new Error("Hosted app data resolves outside its private area.");
169
+ };
170
+ const remove = (path) => {
171
+ if (!lstatSync(path, { throwIfNoEntry: false })) return;
172
+ assertInside(path);
173
+ rmSync(path, {
174
+ recursive: true,
175
+ force: true
176
+ });
177
+ };
178
+ assertInside(home);
179
+ const apps = join(area, "apps");
180
+ if (existsSync(apps)) {
181
+ assertInside(apps);
182
+ for (const attempt of readdirSync(apps)) {
183
+ const directory = join(apps, attempt);
184
+ if (!lstatSync(directory).isDirectory()) continue;
185
+ assertInside(directory);
186
+ remove(join(directory, "App.app"));
187
+ remove(join(directory, "App.apk"));
188
+ remove(join(directory, "App.apk.tmp"));
189
+ remove(join(directory, "blobs"));
190
+ }
191
+ }
192
+ remove(join(area, "blobs"));
193
+ }
194
+ async function digest$1(path) {
195
+ const hash = createHash("sha256");
196
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
197
+ return hash.digest("hex");
198
+ }
199
+ function version(value) {
200
+ if (!/^\d+(?:\.\d+){0,2}$/.test(value)) throw new Error("The app or simulator OS version is unknown.");
201
+ return value.split(".").map(Number);
202
+ }
203
+ function newer(value, supported) {
204
+ const required = version(value);
205
+ const installed = version(supported);
206
+ for (let index = 0; index < 3; index++) {
207
+ const difference = (required[index] ?? 0) - (installed[index] ?? 0);
208
+ if (difference) return difference > 0;
209
+ }
210
+ return false;
211
+ }
212
+ /** Materializes only digest-verified manifest content with links contained inside the app bundle. */
213
+ async function materializeHostedApp(home, session, attempt) {
214
+ const record = readHostedApp(session, attempt, home);
215
+ const area = hostedAppArea(session, attempt, home);
216
+ const blobs = hostedAppBlobs(session, home);
217
+ const app = join(area, "App.app");
218
+ if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
219
+ const manifest = join(blobs, record.manifest.sha256);
220
+ if (lstatSync(manifest).size !== record.manifest.size || await digest$1(manifest) !== record.manifest.sha256) throw new Error("The hosted app manifest differs from its digest.");
221
+ rmSync(app, {
222
+ recursive: true,
223
+ force: true
224
+ });
225
+ mkdirSync(app, { mode: 448 });
226
+ for (const file of record.files) {
227
+ const blob = join(blobs, file.sha256);
228
+ const stat = lstatSync(blob);
229
+ if (!stat.isFile() || stat.size !== file.size || await digest$1(blob) !== file.sha256) throw new Error(`App content is incomplete or differs from its digest: ${file.path}`);
230
+ if (file.kind === "link") continue;
231
+ const target = join(app, file.path);
232
+ mkdirSync(dirname(target), {
233
+ recursive: true,
234
+ mode: 448
235
+ });
236
+ copyFileSync(blob, target);
237
+ chmodSync(target, file.kind === "exec" ? 493 : 420);
238
+ }
239
+ for (const file of record.files.filter((each) => each.kind === "link")) {
240
+ const bytes = readFileSync(join(blobs, file.sha256));
241
+ const target = bytes.toString("utf8");
242
+ const destination = posix.normalize(posix.join(posix.dirname(file.path), target));
243
+ if (!target || !Buffer.from(target).equals(bytes) || /[\\\0\r\n]/.test(target) || posix.isAbsolute(target) || destination === "." || destination === ".." || destination.startsWith("../") || file.path.startsWith(`${destination}/`)) throw new Error(`The app link escapes its bundle or forms an ancestor cycle: ${file.path}`);
244
+ const link = join(app, file.path);
245
+ mkdirSync(dirname(link), {
246
+ recursive: true,
247
+ mode: 448
248
+ });
249
+ symlinkSync(target, link);
250
+ }
251
+ const root = realpathSync(app);
252
+ for (const file of record.files) {
253
+ const path = realpathSync(join(app, file.path));
254
+ if (!inside(root, path)) throw new Error(`The app entry resolves outside its bundle: ${file.path}`);
255
+ if (file.kind !== "link" && await digest$1(path) !== file.sha256) throw new Error("App entries alias different content.");
256
+ }
257
+ return {
258
+ app,
259
+ root,
260
+ record
261
+ };
262
+ }
263
+ async function installHostedApp(home, session, attempt, device, metroPort) {
264
+ const { app, root, record } = await materializeHostedApp(home, session, attempt);
265
+ const exec = getExecutor();
266
+ const plist = (key) => exec.runFile("/usr/libexec/PlistBuddy", [
267
+ "-c",
268
+ `Print :${key}`,
269
+ join(app, "Info.plist")
270
+ ], {
271
+ timeoutMs: 1e4,
272
+ killSignal: "SIGKILL"
273
+ });
274
+ if (plist("CFBundleIdentifier") !== record.bundleId) throw new Error("The app bundle identity differs from the offered identity.");
275
+ if (plist("DTPlatformName") !== "iphonesimulator" || !plist("CFBundleSupportedPlatforms").includes("iPhoneSimulator")) throw new Error("The app is not an iOS simulator bundle.");
276
+ const executable = plist("CFBundleExecutable");
277
+ if (!executable || /[\\/\0\r\n]/.test(executable) || executable === "." || executable === "..") throw new Error("The app executable is not a bundle-root file.");
278
+ const binary = realpathSync(join(app, executable));
279
+ if (!inside(root, binary)) throw new Error("The app executable resolves outside its bundle.");
280
+ if (!exec.runFile("xcrun", [
281
+ "lipo",
282
+ "-archs",
283
+ binary
284
+ ], {
285
+ timeoutMs: 1e4,
286
+ killSignal: "SIGKILL"
287
+ }).split(/\s+/).includes(device.architecture)) throw new Error("The app has no executable slice for the hosted simulator architecture.");
288
+ const build = exec.runFile("xcrun", [
289
+ "vtool",
290
+ "-arch",
291
+ device.architecture,
292
+ "-show-build",
293
+ binary
294
+ ], {
295
+ timeoutMs: 1e4,
296
+ killSignal: "SIGKILL"
297
+ });
298
+ const minimum = /^\s*minos\s+(\S+)\s*$/m.exec(build)?.[1];
299
+ if (!/^\s*platform\s+IOSSIMULATOR\s*$/m.test(build) || !minimum || newer(minimum, device.runtime) || newer(plist("MinimumOSVersion"), device.runtime)) throw new Error("The executable platform or minimum OS is incompatible with the hosted simulator.");
300
+ assertHostedDeviceLedger(home, device.udid);
301
+ if (readHostedDevice(home).udid !== device.udid) throw new Error("The hosted device identity changed.");
302
+ const installed = installIosApp({
303
+ udid: device.udid,
304
+ appPath: app,
305
+ bundleId: record.bundleId,
306
+ devClientScheme: record.devClientScheme,
307
+ proveInstalled: false
308
+ });
309
+ if (!installed.ok) throw new Error(installed.reason ?? "Hosted app installation was not established.");
310
+ assertHostedDeviceLedger(home, device.udid);
311
+ if (readHostedDevice(home).udid !== device.udid) throw new Error("The hosted device identity changed.");
312
+ const launched = launchIosApp({
313
+ udid: device.udid,
314
+ bundleId: record.bundleId,
315
+ metroPort: record.mode === "development" ? metroPort ?? null : null,
316
+ devClientScheme: record.devClientScheme
317
+ });
318
+ if (!launched.ok) throw new Error(launched.reason ?? "Hosted app launch was not established.");
319
+ if (record.mode === "development") return "unverified";
320
+ for (let tries = 0; tries < 10; tries++) {
321
+ await new Promise((resolve) => setTimeout(resolve, 200));
322
+ if (iosAppProcess(device.udid, record.bundleId)) return true;
323
+ }
324
+ return "unverified";
325
+ }
326
+ //#endregion
327
+ //#region src/device-host/worker.ts
328
+ function inventory() {
329
+ return listAllIosSims({ includeUnavailable: true });
330
+ }
331
+ function selectHostedIosDevice(selectors) {
332
+ inventory();
333
+ if (readHostMemoryPressure(getExecutor()) !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
334
+ const selected = resolveIosCreation(selectors);
335
+ if (!selected.deviceType || !selected.runtime) throw new Error("The installed simulator type or runtime could not be established.");
336
+ if (process.arch !== "arm64" && process.arch !== "x64") throw new Error("Unsupported simulator host architecture.");
337
+ return {
338
+ ...selected,
339
+ deviceType: selected.deviceType,
340
+ runtime: selected.runtime,
341
+ architecture: process.arch === "arm64" ? "arm64" : "x86_64"
342
+ };
343
+ }
344
+ /** Called only in the private server-owned worker home; native effects use Stim's existing ownership and teardown. */
345
+ async function runHostedDevice(mode, selectors, app) {
346
+ const home = process.env.STIM_HOME;
347
+ if (!home) throw new Error("Hosted workers require their isolated STIM_HOME.");
348
+ let device = null;
349
+ let creationStarted = false;
350
+ try {
351
+ if (mode === "prepare") {
352
+ creationStarted = existsSync(join(home, "hosted-device.json")) || existsSync(join(home, "created-devices.json"));
353
+ if (creationStarted) throw new Error("This hosted worker home already has a device record. Attach or stop its existing session.");
354
+ const selected = selectHostedIosDevice(selectors);
355
+ creationStarted = true;
356
+ const created = createOwnedIosSim("hosted", {}, selected);
357
+ device = {
358
+ ...selected,
359
+ udid: created.udid,
360
+ name: created.name
361
+ };
362
+ withDirLock(join(home, "hosted-device.lock"), () => {
363
+ const temporary = join(home, "hosted-device.json.tmp");
364
+ writeFileSync(temporary, JSON.stringify(device), { mode: 384 });
365
+ renameSync(temporary, join(home, "hosted-device.json"));
366
+ });
367
+ assertHostedDeviceLedger(home, device.udid);
368
+ await bootIosSim(device.udid, { openViewer: false });
369
+ if (inventory().find((sim) => sim.udid === device.udid)?.state !== "Booted") throw new Error("Hosted simulator boot could not be verified.");
370
+ return {
371
+ state: "ready",
372
+ device
373
+ };
374
+ }
375
+ device = readHostedDevice(home);
376
+ const ledger = assertHostedDeviceLedger(home, device.udid, "ios", { allowEmpty: mode === "stop" });
377
+ const current = inventory().find((sim) => sim.udid === device.udid);
378
+ if (mode === "install") {
379
+ if (!app || current?.state !== "Booted") throw new Error("Hosted app installation requires its booted owned simulator.");
380
+ const launched = await installHostedApp(home, app.session, app.attempt, device, app.metroPort);
381
+ return {
382
+ state: "installed",
383
+ device,
384
+ launched
385
+ };
386
+ }
387
+ if (ledger === "empty") {
388
+ if (current) throw new Error("The hosted simulator remains without ledger ownership; it was kept.");
389
+ removeHostedAppData(home);
390
+ return {
391
+ state: "stopped",
392
+ device
393
+ };
394
+ }
395
+ const outcome = teardownOwnedIosSim(device.udid, { del: true });
396
+ if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted simulator teardown was not established.");
397
+ const after = inventory().find((sim) => sim.udid === device.udid);
398
+ if (after) throw new Error(`Hosted simulator ${device.udid} is still listed (${after.state}); deletion could not be verified. If its runtime is unavailable, delete it with xcrun simctl delete and stop the session again.`);
399
+ forgetCreatedDevice("ios", device.udid);
400
+ removeHostedAppData(home);
401
+ return {
402
+ state: "stopped",
403
+ device
404
+ };
405
+ } catch (error) {
406
+ return {
407
+ state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
408
+ device,
409
+ notice: error.message
410
+ };
411
+ }
412
+ }
413
+ //#endregion
414
+ //#region src/device-host/android-app.ts
415
+ async function digest(path) {
416
+ const hash = createHash("sha256");
417
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
418
+ return hash.digest("hex");
419
+ }
420
+ /** Installs only a verified single APK into the session's exact running ledger-owned AVD. */
421
+ async function installHostedAndroidApp(home, session, attempt, device) {
422
+ const record = readHostedApp(session, attempt, home);
423
+ const area = hostedAppArea(session, attempt, home);
424
+ if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
425
+ const entry = record.files[0];
426
+ if (record.files.length !== 1 || !entry || entry.path !== "App.apk" || entry.kind !== "file") throw new Error("Hosted Android delivery requires a single file entry named App.apk.");
427
+ const manifest = join(hostedAppBlobs(session, home), record.manifest.sha256);
428
+ if (lstatSync(manifest).size !== record.manifest.size || await digest(manifest) !== record.manifest.sha256) throw new Error("The hosted APK manifest differs from its digest.");
429
+ const blob = join(hostedAppBlobs(session, home), entry.sha256);
430
+ const stat = lstatSync(blob);
431
+ if (!stat.isFile() || stat.size !== entry.size || await digest(blob) !== entry.sha256) throw new Error("APK content is incomplete or differs from its digest.");
432
+ const temporary = join(area, "App.apk.tmp");
433
+ const apk = join(area, "App.apk");
434
+ copyFileSync(blob, temporary);
435
+ renameSync(temporary, apk);
436
+ const tool = findAapt();
437
+ if (!tool) throw new Error("No installed Android build-tools can inspect the APK.");
438
+ const badging = getExecutor().runFile(tool.path, [
439
+ "dump",
440
+ "badging",
441
+ apk
442
+ ], {
443
+ timeoutMs: 1e4,
444
+ killSignal: "SIGKILL"
445
+ });
446
+ const identity = /^package:\s+name='([^']+)'/m.exec(badging)?.[1];
447
+ const minimum = /^(?:sdkVersion|minSdkVersion):'(\d+)'\s*$/m.exec(badging)?.[1];
448
+ const api = /^system-images;android-(\d+);/.exec(device.systemImage)?.[1];
449
+ if (identity !== record.bundleId || !minimum || !api || Number(minimum) > Number(api)) throw new Error("The APK package identity or minimum SDK is incompatible with the hosted emulator.");
450
+ const nativeCode = /^native-code:\s*(.*)$/m.exec(badging)?.[1];
451
+ if (nativeCode !== void 0 && !nativeCode.split(/\s+/).includes(`'${device.architecture}'`)) throw new Error("The APK has no native library slice for the hosted emulator architecture.");
452
+ const assertTarget = () => {
453
+ assertHostedDeviceLedger(home, device.avdName, "android");
454
+ const current = readHostedDevice(home, "android");
455
+ if (current.avdName !== device.avdName || current.serial !== device.serial || current.systemImage !== device.systemImage || getAvdNameForSerial(device.serial) !== device.avdName || androidDeviceAbi(device.serial) !== device.architecture) throw new Error("The hosted Android device identity or running ABI changed.");
456
+ };
457
+ assertTarget();
458
+ const executor = getExecutor();
459
+ const adb = androidToolPath("adb");
460
+ const sdkExecutor = {
461
+ ...executor,
462
+ runFile: (file, args, options) => executor.runFile(file === "adb" ? adb : file, args, options)
463
+ };
464
+ const installed = installAndroidApp({
465
+ serial: device.serial,
466
+ apkPath: apk,
467
+ packageName: record.bundleId
468
+ }, { exec: sdkExecutor });
469
+ if (!installed.ok) throw new Error(installed.reason ?? "Hosted APK installation was not established.");
470
+ assertTarget();
471
+ const launched = launchAndroidReleaseApp({
472
+ serial: device.serial,
473
+ packageName: record.bundleId
474
+ }, { exec: sdkExecutor });
475
+ if (!launched.ok) throw new Error(launched.reason ?? "Hosted APK launch was not established.");
476
+ if (record.mode === "development") return "unverified";
477
+ for (let tries = 0; tries < 10; tries++) {
478
+ await new Promise((resolve) => setTimeout(resolve, 200));
479
+ if (androidAppProcess(device.serial, record.bundleId, { exec: sdkExecutor })) return true;
480
+ }
481
+ return "unverified";
482
+ }
483
+ //#endregion
484
+ //#region src/device-host/android.ts
485
+ function portIsOccupied(port) {
486
+ const devices = listAdbDevices({ timeoutMs: 5e3 });
487
+ return [...devices.emulators, ...devices.unhealthy].some((device) => device.consolePort === port);
488
+ }
489
+ function selectHostedAndroidDevice(request) {
490
+ if (readHostMemoryPressure(getExecutor()) !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
491
+ const image = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: request.systemImage });
492
+ if (!image || image.arch !== hostSystemImageArch()) throw new Error("No compatible installed Android image.");
493
+ const profile = request.deviceProfile ?? DEFAULT_AVD_DEVICE_PROFILE;
494
+ if (!listAvdDeviceProfiles().includes(profile)) throw new Error("The Android device profile is not installed.");
495
+ return {
496
+ systemImage: image.pkg,
497
+ deviceProfile: profile,
498
+ architecture: image.arch
499
+ };
500
+ }
501
+ /** Runs only in the private worker home; a caller cannot select or tear down an existing AVD. */
502
+ async function runHostedAndroidDevice(mode, request, app) {
503
+ const home = process.env.STIM_HOME;
504
+ if (!home) throw new Error("Hosted workers require their isolated STIM_HOME.");
505
+ let device = null;
506
+ let creationStarted = false;
507
+ try {
508
+ if (mode === "install") {
509
+ if (!app) throw new Error("Hosted Android installation needs its admitted app attempt.");
510
+ device = readHostedDevice(home, "android");
511
+ const launched = await installHostedAndroidApp(home, request.session, app.attempt, device);
512
+ return {
513
+ state: "installed",
514
+ device,
515
+ launched
516
+ };
517
+ }
518
+ if (mode === "prepare") {
519
+ creationStarted = existsSync(join(home, "hosted-device.json")) || existsSync(join(home, "created-devices.json"));
520
+ if (creationStarted) throw new Error("Attach or stop the existing hosted Android session.");
521
+ if (!/^[a-f0-9-]{36}$/.test(request.session) || !hostedConsolePort(request.consolePort)) throw new Error("Hosted Android prepare needs its server-selected session and console port.");
522
+ const choice = selectHostedAndroidDevice(request);
523
+ const avdName = ownedAvdName(`hosted-${request.session}`);
524
+ if (listAvds({ timeoutMs: 5e3 }).includes(avdName) || portIsOccupied(request.consolePort)) throw new Error("The hosted AVD name or selected console port is already in use.");
525
+ const selected = {
526
+ avdName,
527
+ serial: `emulator-${request.consolePort}`,
528
+ consolePort: request.consolePort,
529
+ ...choice
530
+ };
531
+ creationStarted = true;
532
+ await createOwnedAvd(`hosted-${request.session}`, {
533
+ systemImage: choice.systemImage,
534
+ deviceProfile: choice.deviceProfile,
535
+ spawn: (file, args, options) => {
536
+ assertHostedDeviceLedger(home, avdName, "android");
537
+ withDirLock(join(home, "hosted-device.lock"), () => {
538
+ const temporary = join(home, "hosted-device.json.tmp");
539
+ writeFileSync(temporary, JSON.stringify(selected), { mode: 384 });
540
+ renameSync(temporary, join(home, "hosted-device.json"));
541
+ });
542
+ device = selected;
543
+ return getExecutor().spawn(file, args, {
544
+ ...options,
545
+ detached: false
546
+ });
547
+ }
548
+ });
549
+ assertHostedDeviceLedger(home, avdName, "android");
550
+ if (portIsOccupied(selected.consolePort)) throw new Error("The selected Android console port became occupied.");
551
+ bootAndroidEmulator(avdName, selected.consolePort, {
552
+ openViewer: false,
553
+ logFile: join(home, "emulator.log")
554
+ });
555
+ if (!(await waitForBoot(selected.serial, 24e4, { commandTimeoutMs: 5e3 })).ok || getAvdNameForSerial(selected.serial) !== avdName || androidDeviceAbi(selected.serial) !== selected.architecture) throw new Error("Hosted Android boot could not be verified for its exact owned AVD.");
556
+ return {
557
+ state: "ready",
558
+ device: selected
559
+ };
560
+ }
561
+ device = readHostedDevice(home, "android");
562
+ const ledger = assertHostedDeviceLedger(home, device.avdName, "android", { allowEmpty: true });
563
+ if (ledger === "listed") {
564
+ withConfigLock$1(() => {
565
+ if (!loadConfig$1()) ensureConfig();
566
+ });
567
+ const outcome = teardownOwnedAvd(device.avdName, { del: true });
568
+ if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted Android deletion was not established.");
569
+ }
570
+ const name = device.avdName;
571
+ if (listAvds({ timeoutMs: 5e3 }).includes(name) || avdStorageRoots().some((root) => avdPathExists(join(root, `${name}.ini`)) || avdPathExists(join(root, `${name}.avd`)))) throw new Error("The hosted AVD or its data still exists; deletion could not be verified.");
572
+ if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The hosted Android emulator is still running.");
573
+ if (ledger === "listed") forgetCreatedDevice("android", device.avdName);
574
+ removeHostedAppData(home);
575
+ return {
576
+ state: "stopped",
577
+ device
578
+ };
579
+ } catch (error) {
580
+ return {
581
+ state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
582
+ device,
583
+ notice: error.message
584
+ };
585
+ }
586
+ }
587
+ //#endregion
588
+ //#region src/device-host/macos.ts
589
+ function selectHostedMacos() {
590
+ if (readHostMemoryPressure(getExecutor()) !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
591
+ const macosVersion = getExecutor().runFile("sw_vers", ["-productVersion"], {
592
+ timeoutMs: 1e4,
593
+ killSignal: "SIGKILL"
594
+ });
595
+ if (!/^\d+(\.\d+){0,2}$/.test(macosVersion)) throw new Error("The host macOS version is unknown.");
596
+ return {
597
+ architecture: process.arch === "arm64" ? "arm64" : "x86_64",
598
+ macosVersion
599
+ };
600
+ }
601
+ function writeRecord(home, file, value) {
602
+ withDirLock(join(home, "hosted-device.lock"), () => {
603
+ const temporary = join(home, `${file}.tmp`);
604
+ writeFileSync(temporary, JSON.stringify(value), { mode: 384 });
605
+ renameSync(temporary, join(home, file));
606
+ });
607
+ }
608
+ function preferenceIdentity(home, appSlot) {
609
+ const file = join(home, "hosted-macos-app.json");
610
+ if (!existsSync(file)) return null;
611
+ const stored = JSON.parse(readFileSync(file, "utf8"));
612
+ if (!isJsonObject(stored) || typeof stored.bundleId !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,248}$/.test(stored.bundleId) || stored.bundleId.startsWith("com.apple.") || !stored.bundleId.endsWith(`.hosted${appSlot}`)) throw new Error("The hosted macOS preference identity is malformed.");
613
+ return stored.bundleId;
614
+ }
615
+ function removePreferences(bundleId) {
616
+ const exec = getExecutor();
617
+ const options = {
618
+ timeoutMs: 1e4,
619
+ killSignal: "SIGKILL"
620
+ };
621
+ exec.runFileQuiet("defaults", ["delete", bundleId], options);
622
+ const file = join(homedir(), "Library", "Preferences", `${bundleId}.plist`);
623
+ rmSync(file, { force: true });
624
+ if (existsSync(file)) throw new Error("The hosted macOS preferences plist still exists after deletion.");
625
+ try {
626
+ exec.runFile("defaults", ["read", bundleId], options);
627
+ } catch (error) {
628
+ if (/not found|does not exist/i.test(error.message)) return;
629
+ throw error;
630
+ }
631
+ throw new Error("The hosted macOS preference domain is still readable after deletion.");
632
+ }
633
+ function assertInside(root, path) {
634
+ const diff = relative(root, realpathSync(path));
635
+ if (!diff || diff === ".." || diff.startsWith(`..${sep}`) || isAbsolute(diff)) throw new Error("Hosted macOS app data resolves outside its private area.");
636
+ }
637
+ function removeAppData(root, path) {
638
+ try {
639
+ lstatSync(path);
640
+ } catch (error) {
641
+ if (error.code === "ENOENT") return;
642
+ throw error;
643
+ }
644
+ assertInside(root, path);
645
+ rmSync(path, {
646
+ recursive: true,
647
+ force: true
648
+ });
649
+ }
650
+ async function runHostedMacosApp(mode, request, app) {
651
+ const home = process.env.STIM_HOME;
652
+ let device = null;
653
+ let recorded = false;
654
+ try {
655
+ if (!home) throw new Error("Hosted workers require their isolated STIM_HOME.");
656
+ if (!hostedMacosAppSlot(request.appSlot)) throw new Error("Hosted macOS workers require a reserved app slot.");
657
+ if (mode === "prepare") {
658
+ recorded = existsSync(join(home, "hosted-device.json"));
659
+ if (recorded) throw new Error("Attach or stop the existing hosted macOS session.");
660
+ const selected = {
661
+ ...selectHostedMacos(),
662
+ appSlot: request.appSlot
663
+ };
664
+ assertHostedDeviceLedger(home, `macos-${request.appSlot}`, "macos");
665
+ writeRecord(home, "hosted-device.json", selected);
666
+ recorded = true;
667
+ device = selected;
668
+ return {
669
+ state: "ready",
670
+ device
671
+ };
672
+ }
673
+ device = readHostedDevice(home, "macos");
674
+ if (device.appSlot !== request.appSlot) throw new Error("The hosted macOS app slot changed.");
675
+ assertHostedDeviceLedger(home, `macos-${device.appSlot}`, "macos");
676
+ const runRoot = join(realpathSync(home), "macos-app");
677
+ if (mode === "stop") {
678
+ if (requiredMacosRecord(runRoot)) await stopMacosAppHeld(runRoot);
679
+ const bundleId = preferenceIdentity(home, device.appSlot);
680
+ if (bundleId) removePreferences(bundleId);
681
+ const area = realpathSync(join(home, ".."));
682
+ assertInside(area, home);
683
+ removeAppData(realpathSync(home), join(home, "app-home"));
684
+ const apps = join(home, "..", "apps");
685
+ if (existsSync(apps)) {
686
+ assertInside(area, apps);
687
+ for (const attempt of readdirSync(apps)) {
688
+ const directory = join(apps, attempt);
689
+ assertInside(area, directory);
690
+ removeAppData(area, join(directory, "App.app"));
691
+ removeAppData(area, join(directory, "blobs"));
692
+ }
693
+ }
694
+ removeAppData(area, join(home, "..", "blobs"));
695
+ return {
696
+ state: "stopped",
697
+ device
698
+ };
699
+ }
700
+ if (!app) throw new Error("Hosted macOS installation needs its admitted app attempt.");
701
+ const { app: bundle, root, record } = await materializeHostedApp(home, request.session, app.attempt);
702
+ const exec = getExecutor();
703
+ const options = {
704
+ timeoutMs: 1e4,
705
+ killSignal: "SIGKILL"
706
+ };
707
+ const plistPath = join(bundle, "Contents", "Info.plist");
708
+ const plist = JSON.parse(exec.runFile("plutil", [
709
+ "-convert",
710
+ "json",
711
+ "-o",
712
+ "-",
713
+ plistPath
714
+ ], options));
715
+ if (!isJsonObject(plist) || plist.CFBundleIdentifier !== record.bundleId) throw new Error("The app bundle identity differs from the offered identity.");
716
+ const product = plist.CFBundleExecutable;
717
+ if (typeof product !== "string" || !product || /[\\/\0\r\n]/.test(product) || product === "." || product === "..") throw new Error("The app executable must be a single path component.");
718
+ if (plist.CFBundleURLTypes !== void 0 || plist.SUFeedURL !== void 0) throw new Error("Hosted macOS apps cannot register shared URL schemes or an update feed.");
719
+ if (plist.LSMinimumSystemVersion !== void 0 && (typeof plist.LSMinimumSystemVersion !== "string" || newer(plist.LSMinimumSystemVersion, device.macosVersion))) throw new Error("The app minimum OS is incompatible with the hosted Mac.");
720
+ const executable = realpathSync(join(bundle, "Contents", "MacOS", product));
721
+ const diff = relative(root, executable);
722
+ if (!diff || diff === ".." || diff.startsWith(`..${sep}`) || isAbsolute(diff)) throw new Error("The app executable resolves outside its bundle.");
723
+ if (!exec.runFile("xcrun", [
724
+ "lipo",
725
+ "-archs",
726
+ executable
727
+ ], options).split(/\s+/).includes(device.architecture)) throw new Error("The app has no executable slice for the hosted Mac architecture.");
728
+ const build = exec.runFile("xcrun", [
729
+ "vtool",
730
+ "-arch",
731
+ device.architecture,
732
+ "-show-build",
733
+ executable
734
+ ], options);
735
+ const minimum = /^\s*minos\s+(\S+)\s*$/m.exec(build)?.[1];
736
+ if (!/^\s*platform\s+MACOS\s*$/m.test(build) || !minimum || newer(minimum, device.macosVersion)) throw new Error("The executable platform or minimum OS is incompatible with the hosted Mac.");
737
+ const bundleId = hostedMacosBundleId(record.bundleId, device.appSlot);
738
+ exec.runFile("/usr/libexec/PlistBuddy", [
739
+ "-c",
740
+ `Set :CFBundleIdentifier ${bundleId}`,
741
+ plistPath
742
+ ], options);
743
+ const frameworks = join(bundle, "Contents", "Frameworks");
744
+ if (existsSync(frameworks)) {
745
+ for (const entry of readdirSync(frameworks)) if (entry.endsWith(".framework")) exec.runFile("codesign", [
746
+ "--force",
747
+ "--sign",
748
+ "-",
749
+ join(frameworks, entry)
750
+ ], options);
751
+ }
752
+ exec.runFile("codesign", [
753
+ "--force",
754
+ "--sign",
755
+ "-",
756
+ bundle
757
+ ], options);
758
+ if (requiredMacosRecord(runRoot)) await stopMacosAppHeld(runRoot);
759
+ const previousId = preferenceIdentity(home, device.appSlot);
760
+ if (previousId && previousId !== bundleId) removePreferences(previousId);
761
+ mkdirSync(runRoot, {
762
+ recursive: true,
763
+ mode: 448
764
+ });
765
+ const canonicalRunRoot = realpathSync(runRoot);
766
+ ensureWorkspaceStorage(canonicalRunRoot);
767
+ const launch = {
768
+ product,
769
+ bundle: root,
770
+ bundleId,
771
+ executable,
772
+ arguments: record.arguments ?? [],
773
+ launchId: randomUUID(),
774
+ supervisor: macosProcess(process.pid),
775
+ build: {
776
+ state: "ok",
777
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
778
+ }
779
+ };
780
+ writeRecord(home, "hosted-macos-app.json", { bundleId });
781
+ writeWorkspaceState(canonicalRunRoot, { macos: launch });
782
+ const isolated = join(home, "app-home");
783
+ const temporary = join(isolated, "tmp");
784
+ mkdirSync(isolated, {
785
+ recursive: true,
786
+ mode: 448
787
+ });
788
+ mkdirSync(temporary, {
789
+ mode: 448,
790
+ recursive: true
791
+ });
792
+ const fd = openSync(join(home, "macos-supervisor.log"), "a", 384);
793
+ let child;
794
+ let spawnError;
795
+ const env = {};
796
+ for (const key of [
797
+ "PATH",
798
+ "LANG",
799
+ "LC_ALL",
800
+ "LC_CTYPE",
801
+ "USER",
802
+ "LOGNAME",
803
+ "SHELL",
804
+ "TERM"
805
+ ]) if (process.env[key] !== void 0) env[key] = process.env[key];
806
+ Object.assign(env, {
807
+ STIM_HOME: home,
808
+ HOME: isolated,
809
+ CFFIXED_USER_HOME: isolated,
810
+ TMPDIR: temporary
811
+ });
812
+ try {
813
+ child = exec.spawn(process.execPath, [
814
+ spawnEntry("macos-run"),
815
+ canonicalRunRoot,
816
+ launch.launchId
817
+ ], {
818
+ detached: true,
819
+ stdio: [
820
+ "ignore",
821
+ fd,
822
+ fd
823
+ ],
824
+ cwd: canonicalRunRoot,
825
+ env
826
+ });
827
+ child.once("error", (error) => {
828
+ spawnError = error;
829
+ });
830
+ child.unref();
831
+ } finally {
832
+ closeSync(fd);
833
+ }
834
+ const deadline = Date.now() + 2e4;
835
+ while (Date.now() < deadline) {
836
+ if (spawnError) throw spawnError;
837
+ const current = readMacosRecord(canonicalRunRoot);
838
+ if (current?.launchId === launch.launchId && current.app && current.supervisor && (0, process_identity_exports.inspectProcessIdentity)(current.app) === "same" && (0, process_identity_exports.inspectProcessIdentity)(current.supervisor) === "same") return {
839
+ state: "installed",
840
+ device,
841
+ launched: true,
842
+ pid: current.app.pid
843
+ };
844
+ if (child.exitCode !== null || child.signalCode !== null) break;
845
+ await new Promise((resolve) => setTimeout(resolve, 100));
846
+ }
847
+ throw new Error("The hosted macOS app did not register. See macos-supervisor.log in the worker home.");
848
+ } catch (error) {
849
+ return {
850
+ state: mode === "prepare" && !recorded && !device && !(home && existsSync(join(home, "hosted-device.json"))) ? "stopped" : "unknown",
851
+ device,
852
+ notice: error.message
853
+ };
854
+ }
855
+ }
856
+ //#endregion
857
+ //#region src/device-host/offer.ts
858
+ function inspectHostedDevice(request) {
859
+ const cpus = availableParallelism();
860
+ let workerDiskFreeBytes = null;
861
+ try {
862
+ const disk = statfsSync(configDir());
863
+ workerDiskFreeBytes = disk.bavail * disk.bsize;
864
+ } catch {}
865
+ const resources = {
866
+ cpus,
867
+ loadPerCore: loadavg()[1] / cpus,
868
+ memoryFreeBytes: freemem(),
869
+ memoryPressure: readHostMemoryPressure(),
870
+ workerDiskFreeBytes
871
+ };
872
+ try {
873
+ if (process.platform !== "darwin") throw new Error("Hosted device sessions require a Mac.");
874
+ if (resources.memoryPressure !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
875
+ return request.platform === "macos" ? {
876
+ platform: "macos",
877
+ choice: selectHostedMacos(),
878
+ resources,
879
+ declined: null
880
+ } : request.platform === "ios" ? {
881
+ platform: "ios",
882
+ choice: selectHostedIosDevice(request),
883
+ resources,
884
+ declined: null
885
+ } : {
886
+ platform: "android",
887
+ choice: selectHostedAndroidDevice(request),
888
+ resources,
889
+ declined: null
890
+ };
891
+ } catch (error) {
892
+ return {
893
+ platform: request.platform,
894
+ choice: null,
895
+ resources,
896
+ declined: error.message.slice(0, 4e3)
897
+ };
898
+ }
899
+ }
900
+ //#endregion
901
+ //#region src/device-host/run.ts
902
+ async function main() {
903
+ const chunks = [];
904
+ let bytes = 0;
905
+ for await (const chunk of process.stdin) {
906
+ bytes += chunk.length;
907
+ if (bytes > 8192) throw new Error("Hosted worker request is too large.");
908
+ chunks.push(chunk);
909
+ }
910
+ const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
911
+ if (!isJsonObject(input) || ![
912
+ "prepare",
913
+ "stop",
914
+ "install",
915
+ "offer",
916
+ "logs"
917
+ ].includes(String(input.mode))) throw new Error("Invalid hosted worker request.");
918
+ if (input.mode === "offer") {
919
+ const request = parseHostedOfferRequest(input);
920
+ if (!request) throw new Error("Invalid hosted offer selectors.");
921
+ process.stdout.write(`${JSON.stringify(inspectHostedDevice(request))}\n`);
922
+ return;
923
+ }
924
+ if (process.platform !== "darwin") {
925
+ process.stdout.write(`${JSON.stringify({
926
+ state: input.mode === "prepare" ? "stopped" : "unknown",
927
+ device: null,
928
+ notice: "Hosted device sessions require a Mac."
929
+ })}\n`);
930
+ return;
931
+ }
932
+ if ((input.mode === "install" || input.mode === "logs") && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
933
+ if (input.metroPort !== void 0 && (typeof input.metroPort !== "number" || !Number.isInteger(input.metroPort) || input.metroPort < 1 || input.metroPort > 65535)) throw new Error("Invalid hosted Metro port.");
934
+ if (input.platform === "macos" && !hostedMacosAppSlot(input.appSlot)) throw new Error("Invalid hosted macOS app slot.");
935
+ if (input.mode === "logs") {
936
+ if (input.platform !== "ios" || !process.env.STIM_HOME || typeof input.since !== "number" || !Number.isFinite(input.since)) throw new Error("Invalid hosted iOS log request.");
937
+ const more = collectHostedIosLogs(process.env.STIM_HOME, input.session, input.attempt, input.since, input.final === true);
938
+ process.stdout.write(`${JSON.stringify({ more })}\n`);
939
+ return;
940
+ }
941
+ const result = input.platform === "macos" ? await runHostedMacosApp(input.mode, {
942
+ session: typeof input.session === "string" ? input.session : "",
943
+ appSlot: input.appSlot
944
+ }, input.mode === "install" ? { attempt: input.attempt } : void 0) : input.platform === "android" ? await runHostedAndroidDevice(input.mode, {
945
+ session: typeof input.session === "string" ? input.session : "",
946
+ consolePort: input.consolePort,
947
+ ...typeof input.systemImage === "string" ? { systemImage: input.systemImage } : {},
948
+ ...typeof input.deviceProfile === "string" ? { deviceProfile: input.deviceProfile } : {}
949
+ }, input.mode === "install" ? { attempt: input.attempt } : void 0) : await runHostedDevice(input.mode, {
950
+ ...typeof input.deviceType === "string" ? { deviceType: input.deviceType } : {},
951
+ ...typeof input.runtime === "string" ? { runtime: input.runtime } : {}
952
+ }, input.mode === "install" ? {
953
+ session: input.session,
954
+ attempt: input.attempt,
955
+ ...typeof input.metroPort === "number" ? { metroPort: input.metroPort } : {}
956
+ } : void 0);
957
+ process.stdout.write(`${JSON.stringify(result)}\n`);
958
+ }
959
+ main().catch((error) => {
960
+ process.stderr.write(`${String(error.message ?? error)}\n`);
961
+ process.exitCode = 1;
962
+ });
963
+ //#endregion
964
+ export {};