stim 1.15.0 → 1.17.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 (123) hide show
  1. package/dist/{activity-B2-kb7k0.mjs → activity-IRvJY_ru.mjs} +49 -16
  2. package/dist/agent-device-usage-output-BK41vnzN.mjs +411 -0
  3. package/dist/{android-Cu_L4RpL.mjs → android--GorydsK.mjs} +692 -321
  4. package/dist/{android-Db2eEMaf.mjs → android-B_YNZy61.mjs} +25 -60
  5. package/dist/{android-CYmq0jNi.mjs → android-DE4v59Mb.mjs} +12 -3
  6. package/dist/{android-cas-PnKfAdB2.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
  7. package/dist/{app-install-CAAplXAt.mjs → app-install-VPkI5WEg.mjs} +52 -8
  8. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  9. package/dist/{budget-CMiqghig.mjs → budget-DM984mnd.mjs} +372 -366
  10. package/dist/{build-plan-aeGMFX7e.mjs → build-plan-CmQCoyT-.mjs} +346 -88
  11. package/dist/{build-slots-Df1ApSuH.mjs → build-slots-41v_d2x9.mjs} +36 -21
  12. package/dist/{cli-0cX5s4NM.mjs → cli-Daxug5-K.mjs} +23 -17
  13. package/dist/cli.mjs +1 -1
  14. package/dist/collector-run.mjs +5 -192
  15. package/dist/{command-output-CliHHn7a.mjs → command-output-CDAIFD-8.mjs} +3 -3
  16. package/dist/{created-devices-BwVb3liH.mjs → created-devices-CpvzBL25.mjs} +1 -1
  17. package/dist/{dependency-state-BmsjKZ2H.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
  18. package/dist/{deps-BFlKbOIp.mjs → deps-DNWaQs4V.mjs} +2 -2
  19. package/dist/{dev-client-CjzLoNs0.mjs → dev-client-B-XC4uRH.mjs} +3 -40
  20. package/dist/{device-BZ1IyS5A.mjs → device-C8IOoJPv.mjs} +7 -6
  21. package/dist/device-capacity-D0GWHnTM.mjs +580 -0
  22. package/dist/device-host-worker.mjs +587 -110
  23. package/dist/{device-ios-DMQBUPus.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
  24. package/dist/{device-lease-W5Pi1q38.mjs → device-lease-DpoS36_u.mjs} +2 -2
  25. package/dist/{device-pool-B2r38YjS.mjs → device-lease-run-B8YltIH_.mjs} +5 -125
  26. package/dist/device-pool-3o7s5lo4.mjs +125 -0
  27. package/dist/{device-remote-9ulPsdhY.mjs → device-remote-hFSkpfQu.mjs} +13 -13
  28. package/dist/{doctor-BcE5xsBn.mjs → doctor-Cnk9d7Hm.mjs} +86 -59
  29. package/dist/{error-diagnostics-Cb0P1WLS.mjs → error-diagnostics-C-NMnhrk.mjs} +11 -15
  30. package/dist/{gc-BuL1Genr.mjs → gc-DxCL2A46.mjs} +478 -33
  31. package/dist/{gradle-CmdOBq1d.mjs → gradle-D7-L2Th3.mjs} +4 -4
  32. package/dist/{guide-cCSGRFd3.mjs → guide-DjrTQGik.mjs} +2198 -566
  33. package/dist/{guide-status-CIoWK6mG.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
  34. package/dist/hosted-android-DwLS4Mse.mjs +32 -0
  35. package/dist/hosted-client-wk1W_CNW.mjs +196 -0
  36. package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
  37. package/dist/hosted-logs-DqYmWtXc.mjs +187 -0
  38. package/dist/hosted-macos-dlHXEeF5.mjs +135 -0
  39. package/dist/hosted-native-C9xJ9rpA.mjs +255 -0
  40. package/dist/{build-progress-C2Jv1AtA.mjs → idle-B2m-Ovod.mjs} +300 -9
  41. package/dist/{idle-shutdown-B5LPaNyY.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
  42. package/dist/{in-use-CndPMilN.mjs → in-use-CvGOUn-x.mjs} +30 -10
  43. package/dist/ios-Cr1h5gnO.mjs +189 -0
  44. package/dist/{ios-Chob8ny7.mjs → ios-DE6duQoz.mjs} +819 -460
  45. package/dist/{ios-3CtCRhwr.mjs → ios-DSfnPfse.mjs} +4 -4
  46. package/dist/{ios-device-DERmMmk-.mjs → ios-device-pAN4t54-.mjs} +1 -1
  47. package/dist/ios-state-8L3lgum6.mjs +89 -0
  48. package/dist/{launch-verify-CLlofxRt.mjs → launch-verify-lTnWZikR.mjs} +3 -3
  49. package/dist/logs-DUoKlDoS.mjs +384 -0
  50. package/dist/{client-C2IS36Qf.mjs → machines-C2XiNwh9.mjs} +330 -37
  51. package/dist/{macos-BwtbUtTN.mjs → macos-32po3kUq.mjs} +23 -18
  52. package/dist/macos-run.mjs +1 -1
  53. package/dist/maintenance-run.d.mts +4 -0
  54. package/dist/maintenance-run.mjs +254 -0
  55. package/dist/{metro-Di8EN2Tb.mjs → metro-g8NqiJp-.mjs} +1 -1
  56. package/dist/metro-gateway-DmslUM30.mjs +103 -0
  57. package/dist/{named-ports-Dj_XBNaL.mjs → named-ports-C8m8EQs2.mjs} +2 -2
  58. package/dist/{native-run-DFDoIRbY.mjs → native-run-D1Q56rAD.mjs} +17 -6
  59. package/dist/{native-runtime-i6OkVg-w.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
  60. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  61. package/dist/offload-worker.d.mts +1 -0
  62. package/dist/offload-worker.mjs +13 -8
  63. package/dist/{teardown-BYuQafiA.mjs → ownership-BMLqyBgJ.mjs} +639 -53
  64. package/dist/{ownership-BkYxUtuj.mjs → ownership-DyRQru0q.mjs} +2 -2
  65. package/dist/{ports-DddwttKM.mjs → ports-4nYqxPWC.mjs} +2 -2
  66. package/dist/{prebuild-BvxDlD6C.mjs → prebuild-D7RUMkwN.mjs} +4 -4
  67. package/dist/preview-CxPRIEht.mjs +318 -0
  68. package/dist/{project-CV3tDFHl.mjs → project-D47Rh0ly.mjs} +72 -3
  69. package/dist/{recordings-GEXp4iBT.mjs → recordings-BNgsD1Tr.mjs} +1 -1
  70. package/dist/{reload-ChPiwvot.mjs → reload-qZei6iWn.mjs} +77 -13
  71. package/dist/{remote-cache-CO2elm_b.mjs → remote-cache-Ci4DQYYj.mjs} +4 -4
  72. package/dist/{run-B-xqRrsg.mjs → run-CkE9y0nc.mjs} +2 -2
  73. package/dist/{server-bare-X7PUgizL.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
  74. package/dist/{server-expo-DhtX6bom.mjs → server-expo-CHTyXD0G.mjs} +4 -4
  75. package/dist/server-expo-CeakhI8P.mjs +2 -0
  76. package/dist/settings-BBpMcDbD.mjs +25 -0
  77. package/dist/settings-Cfd-0r2d.mjs +1440 -0
  78. package/dist/{settings-DbL6uhH8.mjs → settings-SR9OmW9h.mjs} +10 -4
  79. package/dist/settings.schema.json +472 -50
  80. package/dist/{simslim-Ciluxlq4.mjs → simslim-BWNrc3M3.mjs} +3 -3
  81. package/dist/{slot-launch-BY0BrOLh.mjs → slot-launch-DmjBmpQV.mjs} +10 -7
  82. package/dist/{spawn-entry-DncpJvWL.mjs → spawn-entry-jw19m8Nl.mjs} +1 -0
  83. package/dist/{start-CPO4wRg7.mjs → start-BopZcr4n.mjs} +1 -1
  84. package/dist/{start-CUD9nasA.mjs → start-uee1wWjY.mjs} +21 -17
  85. package/dist/{state-CaeaHbDA.mjs → state-BKa8wNN5.mjs} +1 -1
  86. package/dist/{state-BxCgeTQu.mjs → state-DyaNTgfR.mjs} +2 -2
  87. package/dist/{state-DFm77wRn.mjs → state-PeQiJuIu.mjs} +5 -5
  88. package/dist/{stats-DBLLFmKd.mjs → stats-Bq4fJOdB.mjs} +19 -5
  89. package/dist/{status-l84Gbvp2.mjs → status-CzS4bPO9.mjs} +5 -3
  90. package/dist/status-DlKvVJBE.mjs +49 -0
  91. package/dist/{status-DYXxNOT0.mjs → status-dXX2JX1C.mjs} +244 -50
  92. package/dist/{stop-CceYh1yG.mjs → stop-BCwvvs7F.mjs} +2 -2
  93. package/dist/{stop-Bad61z_2.mjs → stop-BjizrQwv.mjs} +83 -21
  94. package/dist/{stop-Bvf1MflS.mjs → stop-Cr1d8DSE.mjs} +4 -4
  95. package/dist/stop-cause-rhmGfwvt.mjs +97 -0
  96. package/dist/supervisor-run.d.mts +7 -4
  97. package/dist/supervisor-run.mjs +156 -27
  98. package/dist/{support-WLOzVKUv.mjs → support-BJPgYCu-.mjs} +61 -11
  99. package/dist/{support-CDQbDkL5.mjs → support-F9-sr0Oi.mjs} +6 -5
  100. package/dist/{toolchain-DmeYjXD9.mjs → toolchain-CNIBTIOv.mjs} +11 -11
  101. package/dist/trigger-Bemu8ytw.mjs +65 -0
  102. package/dist/trigger-CXts6qt1.mjs +2 -0
  103. package/dist/{warm-progress-DVHLN4uC.mjs → warm-progress-DF4JRvYI.mjs} +274 -5
  104. package/dist/watchman-APYjXSQa.mjs +48 -0
  105. package/dist/{web-k6w8-HnE.mjs → web-exQtGkoc.mjs} +14 -16
  106. package/dist/web-run.mjs +5 -5
  107. package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
  108. package/dist/{workspace-state-CL2V2Ons.mjs → workspace-state-DqcCbpE7.mjs} +34 -34
  109. package/dist/workspaces-Bcib97-6.mjs +306 -0
  110. package/dist/{worktree-BT_uULFx.mjs → worktree-Dc98cE6_.mjs} +1 -1
  111. package/dist/{worktree-qXKzADSC.mjs → worktree-ZpZR3S1k.mjs} +601 -33
  112. package/package.json +5 -5
  113. package/dist/device-capacity-DMuKC7nx.mjs +0 -136
  114. package/dist/hosted-macos-DwowNU7W.mjs +0 -447
  115. package/dist/logs-h9h6MUvE.mjs +0 -568
  116. package/dist/machine-usage-D_YYMzfR.mjs +0 -283
  117. package/dist/machines-CYtOCqcj.mjs +0 -259
  118. package/dist/merge-state-BnxLNQiM.mjs +0 -272
  119. package/dist/server-expo-KJeD109I.mjs +0 -2
  120. package/dist/settings-CduVcoxj.mjs +0 -680
  121. package/dist/stop-cause-CZ_Mr-v9.mjs +0 -665
  122. package/dist/watchman-Bx0wceoF.mjs +0 -238
  123. package/dist/worktree-C0c5Udwa.mjs +0 -753
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stim",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Stim: fast, isolated React Native environments for coding agents",
5
5
  "keywords": [
6
6
  "agent",
@@ -48,9 +48,9 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@expo/fingerprint": "^0.20.10",
51
- "@stim-cli/cache": "^1.15.0",
52
- "@stim-cli/core": "^1.15.0",
53
- "@stim-cli/metro": "^1.15.0",
51
+ "@stim-cli/cache": "^1.17.0",
52
+ "@stim-cli/core": "^1.17.0",
53
+ "@stim-cli/metro": "^1.17.0",
54
54
  "chalk": "^6.0.0",
55
55
  "commander": "^15.0.0",
56
56
  "cross-spawn": "^7.0.6",
@@ -58,7 +58,7 @@
58
58
  "ws": "^8.21.3"
59
59
  },
60
60
  "devDependencies": {
61
- "@stim-cli/expo-build-cache": "1.15.0",
61
+ "@stim-cli/expo-build-cache": "1.17.0",
62
62
  "@types/cross-spawn": "^6.0.6",
63
63
  "@types/which": "^3.0.4",
64
64
  "@types/ws": "^8.18.1",
@@ -1,136 +0,0 @@
1
- import { H as projectDeviceSlots, _ as loadConfig, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
2
- import { O as listAdbDevices } from "./android-Db2eEMaf.mjs";
3
- import { _ as parseRuntimeVersion, c as iosRuntimeMatches, u as listAllIosSims } from "./ios-3CtCRhwr.mjs";
4
- //#region src/engine/device-capacity.ts
5
- function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
6
- let count = 0;
7
- for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
8
- const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
9
- for (const proj of Object.values(config?.projects || {})) for (const { platforms } of projectDeviceSlots(proj)) {
10
- const android = platforms.android;
11
- if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
12
- }
13
- return count;
14
- }
15
- function workspaceHasLiveDevice({ platform, project, slot = "default", sims = [], adbEmulators = [] } = {}) {
16
- if (!platform) return false;
17
- const record = deviceSlotPlatforms(project, slot)?.[platform];
18
- if (!record) return false;
19
- if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
20
- return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
21
- }
22
- function deviceCapacityRefusal({ platform, project, slot = "default", max, sims = [], adb = null, config = null } = {}) {
23
- if (!max || max <= 0) return null;
24
- const adbEmulators = adb?.emulators || [];
25
- if (workspaceHasLiveDevice({
26
- platform,
27
- project,
28
- slot,
29
- sims,
30
- adbEmulators
31
- })) return null;
32
- const count = liveOwnedDeviceCount({
33
- sims,
34
- adbEmulators,
35
- config
36
- });
37
- if (count < max) return null;
38
- return {
39
- code: "STIM_AT_CAPACITY",
40
- message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
41
- remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
42
- };
43
- }
44
- function checkDeviceCapacity({ platform, project, slot = "default", max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
45
- if (!max || max <= 0) return null;
46
- let simList = [];
47
- let adbRes = {
48
- emulators: [],
49
- physical: [],
50
- unhealthy: []
51
- };
52
- try {
53
- simList = typeof sims === "function" ? sims() || [] : sims || [];
54
- } catch {}
55
- try {
56
- adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
57
- } catch {}
58
- let cfg = null;
59
- try {
60
- cfg = typeof config === "function" ? config() : config ?? null;
61
- } catch {}
62
- return deviceCapacityRefusal({
63
- platform,
64
- project,
65
- slot,
66
- max,
67
- sims: simList,
68
- adb: adbRes,
69
- config: cfg
70
- });
71
- }
72
- function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
73
- if (!requestedName || !recordedTypeId) return null;
74
- const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
75
- if (!wanted) return null;
76
- if (wanted.identifier === recordedTypeId) return null;
77
- const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
78
- return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
79
- }
80
- function runtimeMismatch(recordedRuntimeId, requested, runtimes) {
81
- if (!requested || !recordedRuntimeId) return null;
82
- const wanted = runtimes.find((r) => iosRuntimeMatches(r, requested));
83
- if (!wanted || wanted.identifier === recordedRuntimeId) return null;
84
- return `this project's sim runs iOS ${parseRuntimeVersion(recordedRuntimeId)}, but --runtime asked for ${requested}`;
85
- }
86
- function installedNames(names) {
87
- const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
88
- return unique.length > 0 ? unique.join(", ") : "none";
89
- }
90
- /** Appends which settings layer supplied an unresolved device selector, so the user knows where to fix it. */
91
- function layerNote(refusal, key, flag, origin) {
92
- if (flag !== void 0 && flag !== null) return refusal;
93
- if (!origin) return refusal;
94
- return {
95
- message: `${refusal.message} ${key} is set at the ${origin} layer.`,
96
- remedy: `${refusal.remedy} Fix it with \`stim settings set ${key} <value> --scope ${origin}\` or \`stim settings unset ${key} --scope ${origin}\`.`
97
- };
98
- }
99
- function unknownIosRuntimeRefusal(requested, runtimes) {
100
- if (!requested) return null;
101
- if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
102
- return {
103
- message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
104
- remedy: "Pass `--runtime` (or set ios.runtime) a version printed above (\"26.5\") or a runtime's full name (\"iOS 26.5\"); nothing else matches. Install more runtimes through Xcode."
105
- };
106
- }
107
- function creatableIosDeviceTypeNames(runtimes, runtime) {
108
- return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
109
- }
110
- function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
111
- if (!requested) return null;
112
- const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
113
- if (creatable.includes(requested)) return null;
114
- return {
115
- message: `No device type named "${requested}" can be created on ${runtime ? `runtime ${runtime}` : "any installed simulator runtime"}. ${runtime ? `Device types runtime ${runtime} supports` : "Device types the installed runtimes support"}: ${installedNames(creatable)}.`,
116
- remedy: "Pass `--device-type` (or set ios.deviceType) one of the names printed above, exactly as `xcrun simctl list devicetypes` spells it. `xcrun simctl list devicetypes` also lists watchOS, tvOS and visionOS models, which no iOS runtime can create. Install more models through Xcode."
117
- };
118
- }
119
- function unknownAndroidSystemImageRefusal(requested, images) {
120
- if (!requested) return null;
121
- if ((images || []).some((i) => i.pkg === requested)) return null;
122
- return {
123
- message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
124
- remedy: "Pass `--system-image` (or set android.systemImage) to one of the package ids printed above. Install more with `sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`."
125
- };
126
- }
127
- function unknownAndroidDeviceProfileRefusal(requested, profiles) {
128
- if (!requested) return null;
129
- if (profiles.includes(requested)) return null;
130
- return {
131
- message: `No Android hardware profile is named "${requested}". Profiles avdmanager offers: ${installedNames(profiles)}.`,
132
- remedy: "Pass `--device-profile` (or set android.deviceProfile) to one of the ids printed above, exactly as `avdmanager list device -c` spells it, e.g. \"pixel_fold\" or \"pixel_tablet\"."
133
- };
134
- }
135
- //#endregion
136
- export { runtimeMismatch as a, unknownIosDeviceTypeRefusal as c, liveOwnedDeviceCount as i, unknownIosRuntimeRefusal as l, deviceTypeMismatch as n, unknownAndroidDeviceProfileRefusal as o, layerNote as r, unknownAndroidSystemImageRefusal as s, checkDeviceCapacity as t };
@@ -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 };