stim 1.15.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 (117) hide show
  1. package/dist/{activity-B2-kb7k0.mjs → activity-BxwdD81S.mjs} +29 -16
  2. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  3. package/dist/{android-Db2eEMaf.mjs → android-B_NumVyQ.mjs} +21 -20
  4. package/dist/{android-Cu_L4RpL.mjs → android-Ift4kVvt.mjs} +56 -47
  5. package/dist/{android-cas-PnKfAdB2.mjs → android-cas-8U4DPqeR.mjs} +1 -1
  6. package/dist/{android-CYmq0jNi.mjs → android-r2rBL8kV.mjs} +1 -1
  7. package/dist/{app-install-CAAplXAt.mjs → app-install-CKWdbXIX.mjs} +48 -5
  8. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  9. package/dist/{budget-CMiqghig.mjs → budget-DjrURfQn.mjs} +356 -366
  10. package/dist/{build-plan-aeGMFX7e.mjs → build-plan-DjeSb2Xl.mjs} +19 -20
  11. package/dist/{build-slots-Df1ApSuH.mjs → build-slots-rUk77xqh.mjs} +7 -2
  12. package/dist/{cli-0cX5s4NM.mjs → cli-BkYZ_6hy.mjs} +23 -17
  13. package/dist/cli.mjs +1 -1
  14. package/dist/{client-C2IS36Qf.mjs → client-BTY686R6.mjs} +57 -22
  15. package/dist/collector-run.mjs +5 -192
  16. package/dist/{command-output-CliHHn7a.mjs → command-output-DnJMdEY-.mjs} +1 -1
  17. package/dist/{created-devices-BwVb3liH.mjs → created-devices-D5owjN4C.mjs} +1 -1
  18. package/dist/{dependency-state-BmsjKZ2H.mjs → dependency-state-B6cMXZab.mjs} +1 -1
  19. package/dist/{deps-BFlKbOIp.mjs → deps-urbmfXig.mjs} +2 -2
  20. package/dist/{dev-client-CjzLoNs0.mjs → dev-client-CroAIjjj.mjs} +3 -40
  21. package/dist/{device-BZ1IyS5A.mjs → device-DZQjF7H5.mjs} +7 -6
  22. package/dist/{device-capacity-DMuKC7nx.mjs → device-capacity-BHwxNBlu.mjs} +2 -2
  23. package/dist/device-host-worker.mjs +223 -28
  24. package/dist/{device-ios-DMQBUPus.mjs → device-ios-BadmeL2V.mjs} +7 -7
  25. package/dist/{device-lease-W5Pi1q38.mjs → device-lease-D-hLqx1b.mjs} +2 -2
  26. package/dist/{device-pool-B2r38YjS.mjs → device-lease-run-CqjShqWW.mjs} +5 -125
  27. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  28. package/dist/{device-remote-9ulPsdhY.mjs → device-remote-D5WVWqOq.mjs} +12 -12
  29. package/dist/{doctor-BcE5xsBn.mjs → doctor-Cimf-gGB.mjs} +80 -34
  30. package/dist/{error-diagnostics-Cb0P1WLS.mjs → error-diagnostics-B9L4Ae0x.mjs} +11 -15
  31. package/dist/{gc-BuL1Genr.mjs → gc-Bqy4bjEu.mjs} +194 -31
  32. package/dist/{gradle-CmdOBq1d.mjs → gradle-BsO1MdsH.mjs} +4 -4
  33. package/dist/{guide-cCSGRFd3.mjs → guide-B_uyKTxW.mjs} +588 -53
  34. package/dist/{guide-status-CIoWK6mG.mjs → guide-status-CjEZMAD4.mjs} +1 -1
  35. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  36. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  37. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  38. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  39. package/dist/{build-progress-C2Jv1AtA.mjs → idle-DYVymG92.mjs} +233 -6
  40. package/dist/{idle-shutdown-B5LPaNyY.mjs → idle-shutdown-CTsDnAde.mjs} +6 -6
  41. package/dist/{in-use-CndPMilN.mjs → in-use-DvBw0Kxo.mjs} +29 -9
  42. package/dist/{ios-3CtCRhwr.mjs → ios-BaInn3sW.mjs} +2 -2
  43. package/dist/ios-Cr1h5gnO.mjs +189 -0
  44. package/dist/{ios-Chob8ny7.mjs → ios-aq3QoGI3.mjs} +729 -452
  45. package/dist/{ios-device-DERmMmk-.mjs → ios-device-FfCN_C8p.mjs} +1 -1
  46. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  47. package/dist/{launch-verify-CLlofxRt.mjs → launch-verify-2DBRizjo.mjs} +3 -3
  48. package/dist/logs-CHEVK8zD.mjs +371 -0
  49. package/dist/{machines-CYtOCqcj.mjs → machines-BIBUdhpj.mjs} +16 -10
  50. package/dist/{macos-BwtbUtTN.mjs → macos-CSYEAFmF.mjs} +22 -17
  51. package/dist/macos-run.mjs +1 -1
  52. package/dist/maintenance-run.d.mts +4 -0
  53. package/dist/maintenance-run.mjs +254 -0
  54. package/dist/{metro-Di8EN2Tb.mjs → metro-BeK1ZPz1.mjs} +1 -1
  55. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  56. package/dist/{named-ports-Dj_XBNaL.mjs → named-ports-Bk_YXEmE.mjs} +1 -1
  57. package/dist/{native-run-DFDoIRbY.mjs → native-run-NaBrRFtQ.mjs} +2 -2
  58. package/dist/{native-runtime-i6OkVg-w.mjs → native-runtime-DBzRnEKS.mjs} +6 -6
  59. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  60. package/dist/offload-worker.d.mts +1 -0
  61. package/dist/offload-worker.mjs +13 -8
  62. package/dist/{ownership-BkYxUtuj.mjs → ownership-DiA16AsN.mjs} +2 -2
  63. package/dist/{stop-cause-CZ_Mr-v9.mjs → ownership-hCkPzGmr.mjs} +4 -98
  64. package/dist/{ports-DddwttKM.mjs → ports-R2k4Knz3.mjs} +2 -2
  65. package/dist/{prebuild-BvxDlD6C.mjs → prebuild-D2fsca_Z.mjs} +4 -4
  66. package/dist/preview-DhVbvPAZ.mjs +318 -0
  67. package/dist/{project-CV3tDFHl.mjs → project-Du91Ilvc.mjs} +67 -3
  68. package/dist/{recordings-GEXp4iBT.mjs → recordings-BIXOzVnr.mjs} +1 -1
  69. package/dist/{reload-ChPiwvot.mjs → reload-rM9jV1fz.mjs} +49 -12
  70. package/dist/{remote-cache-CO2elm_b.mjs → remote-cache-C8-V13XN.mjs} +4 -4
  71. package/dist/{run-B-xqRrsg.mjs → run-f4vI6EnK.mjs} +2 -2
  72. package/dist/{server-bare-X7PUgizL.mjs → server-bare-CpPGmnmO.mjs} +1 -1
  73. package/dist/server-expo-CVqO2c35.mjs +2 -0
  74. package/dist/{server-expo-DhtX6bom.mjs → server-expo-MC0rHlOC.mjs} +4 -4
  75. package/dist/settings-BBpMcDbD.mjs +25 -0
  76. package/dist/{settings-DbL6uhH8.mjs → settings-ZkDzkzvB.mjs} +10 -4
  77. package/dist/settings-oTSkkNrM.mjs +1439 -0
  78. package/dist/settings.schema.json +432 -39
  79. package/dist/{simslim-Ciluxlq4.mjs → simslim-B8-7AoTI.mjs} +3 -3
  80. package/dist/{slot-launch-BY0BrOLh.mjs → slot-launch-B8VrHxn1.mjs} +10 -7
  81. package/dist/{spawn-entry-DncpJvWL.mjs → spawn-entry-jw19m8Nl.mjs} +1 -0
  82. package/dist/{start-CPO4wRg7.mjs → start-CX8Ig-U2.mjs} +1 -1
  83. package/dist/{start-CUD9nasA.mjs → start-ClDfwXa4.mjs} +21 -17
  84. package/dist/{state-BxCgeTQu.mjs → state-3DwSQIBK.mjs} +2 -2
  85. package/dist/{state-DFm77wRn.mjs → state-C1jB7Q8q.mjs} +5 -5
  86. package/dist/{state-CaeaHbDA.mjs → state-DcW6vOd_.mjs} +1 -1
  87. package/dist/{stats-DBLLFmKd.mjs → stats-CEZ_i5je.mjs} +16 -5
  88. package/dist/{status-l84Gbvp2.mjs → status-BuvBFeII.mjs} +5 -3
  89. package/dist/{status-DYXxNOT0.mjs → status-Cs7VLg9D.mjs} +148 -44
  90. package/dist/status-DlKvVJBE.mjs +49 -0
  91. package/dist/{stop-Bvf1MflS.mjs → stop-B0Yxtmiy.mjs} +3 -3
  92. package/dist/{stop-Bad61z_2.mjs → stop-Bffozsac.mjs} +61 -20
  93. package/dist/{stop-CceYh1yG.mjs → stop-Dt_dM16A.mjs} +2 -2
  94. package/dist/stop-cause-CidxIqup.mjs +97 -0
  95. package/dist/supervisor-run.mjs +148 -17
  96. package/dist/{support-CDQbDkL5.mjs → support-CAP8qDUF.mjs} +4 -3
  97. package/dist/{support-WLOzVKUv.mjs → support-D4aYJniO.mjs} +34 -9
  98. package/dist/{teardown-BYuQafiA.mjs → teardown-CL6vDs2Y.mjs} +56 -47
  99. package/dist/{toolchain-DmeYjXD9.mjs → toolchain-DkUflduf.mjs} +10 -10
  100. package/dist/trigger-Bemu8ytw.mjs +65 -0
  101. package/dist/trigger-CXts6qt1.mjs +2 -0
  102. package/dist/{warm-progress-DVHLN4uC.mjs → warm-progress-CpGv3Wb6.mjs} +274 -5
  103. package/dist/{web-k6w8-HnE.mjs → web-DWUaMp8P.mjs} +15 -16
  104. package/dist/web-run.mjs +5 -5
  105. package/dist/{workspace-state-CL2V2Ons.mjs → workspace-state-B8-aJ1_5.mjs} +33 -33
  106. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  107. package/dist/{worktree-qXKzADSC.mjs → worktree-K-xj9Db0.mjs} +497 -30
  108. package/dist/{worktree-BT_uULFx.mjs → worktree-jnG8Iph5.mjs} +1 -1
  109. package/package.json +5 -5
  110. package/dist/hosted-macos-DwowNU7W.mjs +0 -447
  111. package/dist/logs-h9h6MUvE.mjs +0 -568
  112. package/dist/machine-usage-D_YYMzfR.mjs +0 -283
  113. package/dist/merge-state-BnxLNQiM.mjs +0 -272
  114. package/dist/server-expo-KJeD109I.mjs +0 -2
  115. package/dist/settings-CduVcoxj.mjs +0 -680
  116. package/dist/watchman-Bx0wceoF.mjs +0 -238
  117. package/dist/worktree-C0c5Udwa.mjs +0 -753
@@ -1,447 +0,0 @@
1
- import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
- import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
3
- import { C as workspaceLogsDir } from "./workspace-state-CL2V2Ons.mjs";
4
- import { l as phaseLine } from "./command-output-CliHHn7a.mjs";
5
- import { t as macosDir } from "./state-BxCgeTQu.mjs";
6
- import { t as BuildConnection, v as parseMachine$1, y as pinnedEndpoint$1 } from "./client-C2IS36Qf.mjs";
7
- import { t as configuredMachines } from "./machines-CYtOCqcj.mjs";
8
- import { createHash, randomUUID } from "node:crypto";
9
- import { lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
- import { join } from "node:path";
11
- import { LOG_ROTATE_BYTES, withDirLock } from "@stim-cli/core";
12
- import { HOSTED_APP_CHUNK_BYTES, hostedMacosAppSlot, hostedMacosBundleId, isJsonObject, parseHostedAgentGrant, parseHostedLogsCursor, parseHostedMacosDevice, readDeviceHostMachines, validHostedAppArguments } from "@stim-cli/core/state";
13
- //#region src/device-host/agent-connection.ts
14
- function canonical(path) {
15
- try {
16
- return realpathSync(path);
17
- } catch {
18
- return null;
19
- }
20
- }
21
- function report(message) {
22
- process.stderr.write(`${phaseLine("device", message.replace(/\s+/g, " "))}\n`);
23
- }
24
- function closeAgentConnection(remoteConfig) {
25
- const exec = getExecutor();
26
- const deadline = Date.now() + 3e4;
27
- const run = (args) => {
28
- const remaining = deadline - Date.now();
29
- if (remaining <= 0) throw new Error("agent-device cleanup timed out");
30
- const result = JSON.parse(exec.runFile("agent-device", [...args, "--json"], {
31
- timeoutMs: Math.min(1e4, remaining),
32
- killSignal: "SIGKILL"
33
- }));
34
- if (result?.success !== true) throw new Error("agent-device did not confirm the command");
35
- return result.data;
36
- };
37
- try {
38
- if (!exec.findExecutable("agent-device")) return;
39
- const status = run(["connection", "status"]);
40
- if (status?.connected !== true || typeof status.session !== "string" || typeof status.remoteConfig !== "string") return;
41
- const config = canonical(remoteConfig);
42
- if (config === null || canonical(status.remoteConfig) !== config) return;
43
- const failures = [];
44
- for (const args of [[
45
- "close",
46
- "--remote-config",
47
- remoteConfig,
48
- "--session",
49
- status.session
50
- ], [
51
- "disconnect",
52
- "--session",
53
- status.session
54
- ]]) try {
55
- run(args);
56
- } catch (error) {
57
- failures.push(error.message);
58
- }
59
- report(failures.length ? `could not clean up agent-device connection for ${status.session}: ${failures.join("; ")}` : `closed agent-device connection for ${status.session}`);
60
- } catch (error) {
61
- report(`could not clean up agent-device connection: ${error.message}`);
62
- }
63
- }
64
- //#endregion
65
- //#region src/device-host/hosted-logs.ts
66
- const MAX_PAGES = 64;
67
- const cursorFile = (root) => join(macosDir(root), "host-logs-cursor.json");
68
- const hostLogFile = (root) => join(workspaceLogsDir(root), "macos-host.ndjson");
69
- function readCursor(root, session) {
70
- try {
71
- const stored = JSON.parse(readFileSync(cursorFile(root), "utf8"));
72
- if (isJsonObject(stored) && stored.session === session) return parseHostedLogsCursor(stored.cursor) ?? void 0;
73
- } catch {}
74
- }
75
- const sameCursor = (a, b) => JSON.stringify(Object.entries(a ?? {}).toSorted()) === JSON.stringify(Object.entries(b ?? {}).toSorted());
76
- function commitPage(root, session, before, records, cursor) {
77
- if (!records.length && sameCursor(before, cursor)) return;
78
- const directory = macosDir(root);
79
- withDirLock(join(directory, "host-logs.lock"), () => {
80
- if (!sameCursor(readCursor(root, session), before)) return;
81
- if (records.length) {
82
- const writer = createNdjsonWriter(hostLogFile(root), { maxBytes: LOG_ROTATE_BYTES });
83
- try {
84
- for (const record of records) writer.write(record);
85
- } finally {
86
- writer.close();
87
- }
88
- }
89
- const temporary = `${cursorFile(root)}.${process.pid}.tmp`;
90
- try {
91
- writeFileSync(temporary, JSON.stringify({
92
- session,
93
- cursor
94
- }), { mode: 384 });
95
- renameSync(temporary, cursorFile(root));
96
- } finally {
97
- rmSync(temporary, { force: true });
98
- }
99
- }, { ensureParent: () => mkdirSync(directory, { recursive: true }) });
100
- }
101
- async function pullHostedMacosLogs(root, placement, host) {
102
- for (let page = 0; page < MAX_PAGES; page++) {
103
- const before = readCursor(root, placement.session);
104
- const reply = await host.connection.request("device-host.logs.query", {
105
- session: placement.session,
106
- ...before ? { cursor: before } : {}
107
- });
108
- if ("error" in reply) throw Object.assign(/* @__PURE__ */ new Error(`${host.machine} refused device-host.logs.query: ${reply.error.message}`), { code: reply.error.code });
109
- const result = reply.result;
110
- const cursor = isJsonObject(result) ? parseHostedLogsCursor(result.cursor) : null;
111
- if (!isJsonObject(result) || !cursor || !Array.isArray(result.records) || !result.records.every(isJsonObject) || typeof result.more !== "boolean") throw new Error(`${host.machine} answered device-host.logs.query with an unexpected result.`);
112
- commitPage(root, placement.session, before, result.records, cursor);
113
- if (!result.more || sameCursor(cursor, before)) return;
114
- }
115
- }
116
- //#endregion
117
- //#region src/device-host/hosted-macos.ts
118
- const CONNECT_TIMEOUT_MS = 1e4;
119
- const POLL_MS = 500;
120
- const SESSION_TIMEOUT_MS = 12e4;
121
- const INSTALL_TIMEOUT_MS = 3e5;
122
- const HANDOFF_TIMEOUT_MS = 3e5;
123
- const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
124
- const sha256 = (content) => createHash("sha256").update(content).digest("hex");
125
- function hostingCredential(machine) {
126
- const configured = configuredMachines();
127
- if (configured === null) throw new Error("hosting.machines is invalid. Run stim guide settings and correct it.");
128
- if (!configured.includes(machine)) throw Object.assign(/* @__PURE__ */ new Error(`${machine} is not in hosting.machines. Add it with stim settings set hosting.machines, then run stim doctor --fix to ask it for hosting access.`), { code: "STIM_BAD_ARG" });
129
- let credentials;
130
- try {
131
- credentials = readDeviceHostMachines();
132
- } catch {
133
- throw new Error("The hosting credentials are unreadable. Run stim doctor.");
134
- }
135
- const credential = credentials.find((each) => each.machine === machine);
136
- if (!credential) throw new Error(`This Mac has not asked ${machine} for hosting access. Run stim doctor --fix.`);
137
- if (credential.state !== "approved") throw new Error(`${machine} has not confirmed hosting access for this Mac. A person on ${machine} approves it with stim-server devices grant ${credential.deviceId} --device-host; then run stim doctor.`);
138
- return credential;
139
- }
140
- /** Connects only to the pinned node of an approved hosting machine, and only when it grants `device-host`. */
141
- async function connectHost(machine, timeoutMs = CONNECT_TIMEOUT_MS) {
142
- const credential = hostingCredential(machine);
143
- const target = pinnedEndpoint$1(credential);
144
- if (typeof target === "string") throw new Error(`Stim does not connect to ${machine}: ${target}.`);
145
- const opened = await BuildConnection.open(target, credential.deviceToken, timeoutMs, "device-host");
146
- if (!(opened instanceof BuildConnection)) throw new Error(opened.refused ? `${machine} refused this Mac: ${opened.failure}. Run stim doctor.` : `${machine} is unreachable: ${opened.failure}. Check stim-server and its tailnet serve route on ${machine}.`);
147
- return {
148
- machine,
149
- credential,
150
- connection: opened
151
- };
152
- }
153
- async function call(host, method, params, timeoutMs) {
154
- const reply = await host.connection.request(method, params, timeoutMs);
155
- if ("error" in reply) throw Object.assign(/* @__PURE__ */ new Error(`${host.machine} refused ${method}: ${reply.error.message}`), { code: reply.error.code });
156
- if (!isJsonObject(reply.result)) throw new Error(`${host.machine} answered ${method} without a result.`);
157
- return reply.result;
158
- }
159
- function hostedSession(host, value) {
160
- if (typeof value.id !== "string" || typeof value.state !== "string" || value.platform !== "macos") throw new Error(`${host.machine} answered with a session that is not a hosted macOS session.`);
161
- return {
162
- id: value.id,
163
- state: value.state,
164
- device: value.device,
165
- ...hostedMacosAppSlot(value.appSlot) ? { appSlot: value.appSlot } : {},
166
- ...typeof value.notice === "string" ? { notice: value.notice } : {}
167
- };
168
- }
169
- const heldNoLonger = (error) => error instanceof Error && "code" in error && error.code === "unknown-session";
170
- async function attach(host, session, timeoutMs) {
171
- return hostedSession(host, await call(host, "device-host.attach", { session }, timeoutMs));
172
- }
173
- const probeCache = /* @__PURE__ */ new Map();
174
- function probeHostedMacos(placement, { timeoutMs = 3e3, ttlMs = 1e4 } = {}) {
175
- const key = JSON.stringify([placement.machine, placement.session]);
176
- const cached = probeCache.get(key);
177
- if (ttlMs > 0 && cached && cached.expiresAt > Date.now()) return cached.promise;
178
- const promise = probeSession(placement, timeoutMs);
179
- if (ttlMs > 0) {
180
- const entry = {
181
- expiresAt: Infinity,
182
- promise
183
- };
184
- probeCache.delete(key);
185
- probeCache.set(key, entry);
186
- if (probeCache.size > 64) probeCache.delete(probeCache.keys().next().value);
187
- promise.then((result) => {
188
- entry.expiresAt = Date.now() + ttlMs;
189
- return result;
190
- });
191
- }
192
- return promise;
193
- }
194
- async function probeSession(placement, timeoutMs) {
195
- let host;
196
- try {
197
- host = await connectHost(placement.machine, timeoutMs);
198
- const session = await attach(host, placement.session, timeoutMs);
199
- if (session.state === "ready" || session.state === "stopped") return { state: session.state };
200
- return {
201
- state: "unknown",
202
- notice: session.state === "unknown" ? session.notice : `The hosted session ${session.id} on ${host.machine} is ${session.state}.${session.notice ? ` ${session.notice}` : ""}`
203
- };
204
- } catch (error) {
205
- if (heldNoLonger(error)) return { state: "stopped" };
206
- return {
207
- state: "unreachable",
208
- reason: error instanceof Error ? error.message : String(error)
209
- };
210
- } finally {
211
- host?.connection.close();
212
- }
213
- }
214
- async function settle(host, session, passing, timeoutMs) {
215
- const deadline = Date.now() + timeoutMs;
216
- while (passing.includes(session.state)) {
217
- if (Date.now() > deadline) throw new Error(`The hosted session ${session.id} on ${host.machine} stayed ${session.state}.`);
218
- await sleep(POLL_MS);
219
- session = await attach(host, session.id);
220
- }
221
- return session;
222
- }
223
- function unknownSession(host, session) {
224
- return /* @__PURE__ */ new Error(`The hosted session ${session.id} on ${host.machine} is in an unknown state${session.notice ? `: ${session.notice}` : ""}. Run stim stop to reconcile it.`);
225
- }
226
- function bundleManifest(bundle) {
227
- const files = [];
228
- const content = /* @__PURE__ */ new Map();
229
- const walk = (dir, prefix) => {
230
- for (const name of readdirSync(dir)) {
231
- const absolute = join(dir, name);
232
- const path = prefix ? `${prefix}/${name}` : name;
233
- const stat = lstatSync(absolute);
234
- let read;
235
- let kind;
236
- if (stat.isDirectory()) {
237
- walk(absolute, path);
238
- continue;
239
- } else if (stat.isSymbolicLink()) {
240
- read = () => Buffer.from(readlinkSync(absolute));
241
- kind = "link";
242
- } else if (stat.isFile()) {
243
- read = () => readFileSync(absolute);
244
- kind = stat.mode & 73 ? "exec" : "file";
245
- } else continue;
246
- const bytes = read();
247
- const digest = sha256(bytes);
248
- files.push({
249
- path,
250
- kind,
251
- size: bytes.length,
252
- sha256: digest
253
- });
254
- content.set(digest, read);
255
- }
256
- };
257
- walk(bundle, "");
258
- files.sort((a, b) => a.path.localeCompare(b.path));
259
- return {
260
- files,
261
- content
262
- };
263
- }
264
- async function upload(host, ids, missing, content) {
265
- if (!Array.isArray(missing)) throw new Error(`${host.machine} answered device-host.app.offer without missing content.`);
266
- for (const entry of missing) {
267
- const read = isJsonObject(entry) && typeof entry.sha256 === "string" ? content.get(entry.sha256) : void 0;
268
- if (!read || !isJsonObject(entry) || typeof entry.offset !== "number") throw new Error(`${host.machine} asked for content this app does not have.`);
269
- const bytes = read();
270
- let offset = entry.offset;
271
- do {
272
- const data = bytes.subarray(offset, offset + HOSTED_APP_CHUNK_BYTES).toString("base64");
273
- const next = (await call(host, "device-host.app.chunk", {
274
- ...ids,
275
- sha256: entry.sha256,
276
- offset,
277
- data
278
- })).offset;
279
- if (typeof next !== "number" || next <= offset && next !== bytes.length) throw new Error(`${host.machine} did not accept app content at byte ${offset}.`);
280
- offset = next;
281
- } while (offset < bytes.length);
282
- }
283
- }
284
- const agentRemoteConfig = (root) => join(macosDir(root), "agent-device-remote.json");
285
- function agentAccess(root, credential, grant, note) {
286
- const parsed = grant === void 0 ? null : parseHostedAgentGrant(grant);
287
- if (grant !== void 0 && !parsed) note(`${credential.machine} offered agent control this Stim does not support.`);
288
- const file = agentRemoteConfig(root);
289
- if (!parsed || parsed.driver === "none") {
290
- rmSync(file, { force: true });
291
- return {
292
- driver: "none",
293
- setting: "hosting.agentDriver"
294
- };
295
- }
296
- const port = parseMachine$1(credential.machine)?.port ?? 443;
297
- const config = {
298
- daemonBaseUrl: `https://${credential.dnsName}${port === 443 ? "" : `:${port}`}${parsed.path}`,
299
- daemonAuthToken: parsed.token,
300
- tenant: parsed.lease.tenant,
301
- sessionIsolation: "tenant",
302
- runId: parsed.lease.runId,
303
- leaseId: parsed.scope,
304
- leaseBackend: "macos-app",
305
- leaseProvider: "proxy",
306
- clientId: parsed.lease.clientId,
307
- deviceKey: parsed.lease.deviceKey,
308
- platform: "macos"
309
- };
310
- mkdirSync(macosDir(root), { recursive: true });
311
- const tmp = `${file}.${process.pid}.tmp`;
312
- try {
313
- writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
314
- renameSync(tmp, file);
315
- } finally {
316
- rmSync(tmp, { force: true });
317
- }
318
- return {
319
- driver: "agent-device",
320
- remoteConfig: file,
321
- command: `agent-device <command> --remote-config ${file}`
322
- };
323
- }
324
- /**
325
- * Places a staged `.app` on `host`: reuses the recorded session when the host still holds it ready, else reserves one,
326
- * then delivers the bundle as a new app attempt, launches it and waits for the host to report it installed. `reserved`
327
- * records the placement as soon as a session exists, so `stim stop` can find it even if delivery fails.
328
- */
329
- async function placeHostedMacos(host, { root, bundle, bundleId, handoff, arguments: requestedArguments, recorded, reserved, note }) {
330
- let session = null;
331
- if (recorded) {
332
- try {
333
- session = await settle(host, await attach(host, recorded.session), ["preparing", "stopping"], SESSION_TIMEOUT_MS);
334
- } catch (error) {
335
- if (!heldNoLonger(error)) throw error;
336
- }
337
- if (session?.state === "unknown") throw unknownSession(host, session);
338
- if (session?.state === "stopped") session = null;
339
- }
340
- if (!session) {
341
- note(`Reserving a macOS session on ${host.machine}`);
342
- session = hostedSession(host, await call(host, "device-host.reserve", {
343
- workspace: root,
344
- slot: "default",
345
- attempt: randomUUID(),
346
- platform: "macos"
347
- }));
348
- }
349
- const appSlot = session.appSlot;
350
- if (appSlot === void 0) throw new Error(`${host.machine} reserved a session without a macOS app slot.`);
351
- const appAttempt = randomUUID();
352
- const placement = {
353
- machine: host.machine,
354
- session: session.id,
355
- appSlot,
356
- appAttempt,
357
- bundleId: hostedMacosBundleId(bundleId, appSlot),
358
- agent: {
359
- driver: "none",
360
- setting: "hosting.agentDriver"
361
- }
362
- };
363
- reserved(placement);
364
- session = await settle(host, session, ["preparing"], SESSION_TIMEOUT_MS);
365
- if (session.state === "unknown") throw unknownSession(host, session);
366
- if (session.state !== "ready") throw new Error(`The hosted session ${session.id} on ${host.machine} is ${session.state}.`);
367
- const device = parseHostedMacosDevice(session.device);
368
- if (!device || device.appSlot !== appSlot) throw new Error(`${host.machine} reported a ready session without its reserved macOS app slot.`);
369
- const { files, content } = bundleManifest(bundle);
370
- const manifest = Buffer.from(JSON.stringify(files));
371
- content.set(sha256(manifest), () => manifest);
372
- const ids = {
373
- session: session.id,
374
- attempt: appAttempt
375
- };
376
- const offer = {
377
- ...ids,
378
- bundleId,
379
- mode: "release",
380
- ...requestedArguments.length ? { arguments: requestedArguments } : {},
381
- manifest: {
382
- sha256: sha256(manifest),
383
- size: manifest.length
384
- }
385
- };
386
- note(`Delivering ${files.length} files to ${host.machine} (macOS ${device.macosVersion}, ${device.architecture})`);
387
- await upload(host, ids, (await call(host, "device-host.app.offer", offer)).missing, content);
388
- let missing = (await call(host, "device-host.app.offer", offer)).missing;
389
- if (handoff && handoff.nodeId === host.credential.nodeId && Array.isArray(missing) && missing.length) {
390
- try {
391
- const taken = await call(host, "device-host.app.handoff", {
392
- ...ids,
393
- build: {
394
- handoff: handoff.token,
395
- sha256: handoff.sha256
396
- }
397
- }, HANDOFF_TIMEOUT_MS);
398
- if (taken.files) note(`${host.machine} took ${String(taken.files)} files from the build it ran`);
399
- } catch (error) {
400
- note(`${error instanceof Error ? error.message : String(error)}; uploading the app instead`);
401
- }
402
- missing = (await call(host, "device-host.app.offer", offer)).missing;
403
- }
404
- await upload(host, ids, missing, content);
405
- note(`Launching on ${host.machine}`);
406
- let delivery = await call(host, "device-host.app.launch", ids);
407
- const deadline = Date.now() + INSTALL_TIMEOUT_MS;
408
- while (delivery.state !== "installed") {
409
- if (delivery.state === "unknown") throw new Error(`${host.machine} could not install or launch the app${typeof delivery.notice === "string" ? `: ${delivery.notice}` : ""}.`);
410
- if (Date.now() > deadline) throw new Error(`${host.machine} did not finish installing the app.`);
411
- await sleep(POLL_MS);
412
- delivery = await call(host, "device-host.app.attach", ids);
413
- }
414
- const appliedArguments = validHostedAppArguments(delivery.arguments) ? delivery.arguments : [];
415
- if (JSON.stringify(appliedArguments) !== JSON.stringify(requestedArguments)) note(`${host.machine} did not apply macos.arguments. Update stim-server on that host.`);
416
- if (typeof delivery.notice === "string") note(delivery.notice);
417
- return {
418
- placement: {
419
- ...placement,
420
- agent: agentAccess(root, host.credential, delivery.agent, note)
421
- },
422
- launched: delivery.launched === true ? true : "unverified",
423
- arguments: appliedArguments
424
- };
425
- }
426
- /** Stops a recorded hosted session and waits until the host confirms it stopped; anything else is a failure. */
427
- async function stopHostedMacos(root, placement) {
428
- if (placement.agent.driver === "agent-device") closeAgentConnection(placement.agent.remoteConfig);
429
- const host = await connectHost(placement.machine);
430
- try {
431
- try {
432
- let session = hostedSession(host, await call(host, "device-host.stop", { session: placement.session }));
433
- session = await settle(host, session, ["stopping"], SESSION_TIMEOUT_MS);
434
- if (session.state !== "stopped") throw unknownSession(host, session);
435
- await pullHostedMacosLogs(root, placement, host).catch((error) => {
436
- process.stderr.write(`${phaseLine("device", `Could not copy the final logs from ${host.machine}: ${error.message}`)}\n`);
437
- });
438
- } catch (error) {
439
- if (!heldNoLonger(error)) throw error;
440
- }
441
- } finally {
442
- host.connection.close();
443
- }
444
- rmSync(agentRemoteConfig(root), { force: true });
445
- }
446
- //#endregion
447
- export { pullHostedMacosLogs as a, stopHostedMacos as i, placeHostedMacos as n, probeHostedMacos as r, connectHost as t };