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
@@ -1,283 +0,0 @@
1
- import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
- import { basename, dirname, join } from "node:path";
3
- import { compiledHelper, configDir } from "@stim-cli/core";
4
- import { MACHINE_OWNER_KINDS } from "@stim-cli/core/state";
5
- import { fileURLToPath } from "node:url";
6
- //#region src/footprint.ts
7
- const SOURCE = "stim-footprint.swift";
8
- const COMPILE_TIMEOUT_MS = 12e4;
9
- const READ_TIMEOUT_MS = 5e3;
10
- function helperSource() {
11
- const here = fileURLToPath(import.meta.url);
12
- if (basename(dirname(here)) === "src") return fileURLToPath(new URL(`../helper/${SOURCE}`, import.meta.url));
13
- return join(dirname(here), SOURCE);
14
- }
15
- async function buildHelper() {
16
- const exec = getExecutor();
17
- if (!exec.runFileQuiet("xcode-select", ["-p"], { timeoutMs: READ_TIMEOUT_MS })) return null;
18
- const source = helperSource();
19
- try {
20
- return await compiledHelper({
21
- dir: join(configDir(), "helpers"),
22
- name: "stim-footprint",
23
- inputs: [source],
24
- version: "",
25
- compile: async (output) => {
26
- await exec.runFileAsync("xcrun", [
27
- "swiftc",
28
- "-O",
29
- "-swift-version",
30
- "5",
31
- "-o",
32
- output,
33
- source
34
- ], { timeoutMs: COMPILE_TIMEOUT_MS });
35
- }
36
- });
37
- } catch {
38
- return null;
39
- }
40
- }
41
- const RETRY_BACKOFF_INITIAL_MS = 1e4;
42
- const RETRY_BACKOFF_MAX_MS = 6e5;
43
- let built = null;
44
- let failure = null;
45
- /** The `stim-footprint` helper, compiled on first use. A success is kept for the process's life; a failure retries after a doubling backoff instead of being pinned forever. */
46
- function footprintHelper() {
47
- const home = configDir();
48
- if (built?.home === home) return built.promise;
49
- if (failure?.home === home && Date.now() < failure.nextRetryAt) return Promise.resolve(null);
50
- const promise = buildHelper().then((helper) => {
51
- if (helper === null) {
52
- const backoffMs = failure?.home === home ? Math.min(failure.backoffMs * 2, RETRY_BACKOFF_MAX_MS) : RETRY_BACKOFF_INITIAL_MS;
53
- failure = {
54
- home,
55
- nextRetryAt: Date.now() + backoffMs,
56
- backoffMs
57
- };
58
- if (built?.promise === promise) built = null;
59
- return null;
60
- }
61
- return helper;
62
- });
63
- built = {
64
- home,
65
- promise
66
- };
67
- return promise;
68
- }
69
- /** Parses the helper's `<pid> <bytes>` lines into footprint bytes by pid. */
70
- function parseFootprints(output) {
71
- const footprints = /* @__PURE__ */ new Map();
72
- for (const line of output.split("\n")) {
73
- const match = /^(\d+) (\d+)$/.exec(line.trim());
74
- if (match) footprints.set(Number(match[1]), Number(match[2]));
75
- }
76
- return footprints;
77
- }
78
- /** Every readable process's physical footprint in bytes, or null when the helper is unavailable or fails. */
79
- async function readFootprints() {
80
- const helper = await footprintHelper();
81
- const output = helper ? getExecutor().runFileQuiet(helper, [], { timeoutMs: READ_TIMEOUT_MS }) : null;
82
- return output === null ? null : parseFootprints(output);
83
- }
84
- //#endregion
85
- //#region src/machine-usage.ts
86
- const SHARED = [
87
- {
88
- name: "CoreSimulator services",
89
- pattern: /\/CoreSimulator\.framework\/|\/Simulator\.app\/Contents\/MacOS\//
90
- },
91
- {
92
- name: "adb server",
93
- pattern: /(^|\/)adb\s.*\bfork-server\b/
94
- },
95
- {
96
- name: "Android emulator services",
97
- pattern: /\/emulator\/netsimd(\s|$)/
98
- },
99
- {
100
- name: "Gradle daemon",
101
- pattern: /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\b/
102
- },
103
- {
104
- name: "Kotlin daemon",
105
- pattern: /\bKotlinCompileDaemon\b/
106
- },
107
- {
108
- name: "Watchman",
109
- pattern: /(^|\/)watchman(\s|$)/
110
- }
111
- ];
112
- const STIM_SERVER_COMMAND = /(^|[\s/])stim-server(\.mjs)?(\s|$)/;
113
- const LAUNCHD_SIM = /^launchd_sim\s.*\/Devices\/([^/\s]+)\//;
114
- const MAX_DEPTH = 64;
115
- const EMULATOR = /(?:^|\/)(?:qemu-system-[^\s/]+|emulator)(?:\s.*)?\s(?:-avd\s+|@)(\S+)/;
116
- function emulatorAvd(command) {
117
- return EMULATOR.exec(command)?.[1] ?? null;
118
- }
119
- function workspaceOwner(kind, env, name, id) {
120
- return {
121
- kind,
122
- name,
123
- workspace: env.path,
124
- id,
125
- owned: true
126
- };
127
- }
128
- function rank(owner) {
129
- return MACHINE_OWNER_KINDS.indexOf(owner.kind);
130
- }
131
- /**
132
- * Attributes every process in `processes` to at most one owner: the owner of its nearest ancestor (itself included)
133
- * that is a root. Roots are each booted simulator's `launchd_sim`, each emulator launcher and qemu process, and each
134
- * workspace's verified supervisor and Metro, running build and Chrome. Only a process under none of those falls back
135
- * to stim-server or a machine-wide service in `SHARED`, so a `simctl` a workspace's log collector runs stays with the
136
- * workspace. An emulator counts in the workspace that records its AVD whenever its process runs, including while adb
137
- * does not list it. A process with no root ancestor is left out. `footprints` holds physical footprint bytes by pid,
138
- * or is null when they could not be read, and the owners' `memoryMb` then sums resident sizes. A process missing from
139
- * `footprints`, another user's or one that exited between the two reads, counts its resident size.
140
- */
141
- function attributeMachineUsage({ processes, footprints, environments, roots, simNames }) {
142
- const owners = /* @__PURE__ */ new Map();
143
- const rootOf = /* @__PURE__ */ new Map();
144
- const fallbackOf = /* @__PURE__ */ new Map();
145
- const claim = (pid, key, owner, into = rootOf) => {
146
- if (!owners.has(key)) owners.set(key, owner);
147
- into.set(pid, key);
148
- };
149
- const sims = /* @__PURE__ */ new Map();
150
- const emulators = /* @__PURE__ */ new Map();
151
- for (const env of environments) for (const device of [{
152
- slot: "default",
153
- ios: env.ios,
154
- android: env.android
155
- }, ...env.slots ?? []]) {
156
- const slot = device.slot === "default" ? {} : { slot: device.slot };
157
- if (device.ios?.state === "Booted") sims.set(device.ios.udid.toUpperCase(), {
158
- kind: "simulator",
159
- name: device.ios.name ?? device.ios.udid,
160
- workspace: env.path,
161
- ...slot,
162
- id: device.ios.udid,
163
- owned: device.ios.owned
164
- });
165
- if (device.android?.name && !device.android.physical) emulators.set(device.android.name, {
166
- kind: "emulator",
167
- name: device.android.name,
168
- workspace: env.path,
169
- ...slot,
170
- id: device.android.name,
171
- owned: device.android.owned
172
- });
173
- }
174
- for (const p of processes) {
175
- const shared = SHARED.find(({ pattern }) => pattern.test(p.command));
176
- if (shared) {
177
- const owner = {
178
- kind: "shared",
179
- name: shared.name,
180
- workspace: null,
181
- id: null,
182
- owned: false
183
- };
184
- claim(p.pid, `shared:${shared.name}`, owner, fallbackOf);
185
- continue;
186
- }
187
- if (STIM_SERVER_COMMAND.test(p.command)) {
188
- claim(p.pid, "server", {
189
- kind: "server",
190
- name: "stim-server",
191
- workspace: null,
192
- id: null,
193
- owned: false
194
- }, fallbackOf);
195
- continue;
196
- }
197
- const udid = LAUNCHD_SIM.exec(p.command)?.[1]?.toUpperCase();
198
- if (udid) {
199
- const owner = sims.get(udid) ?? {
200
- kind: "simulator",
201
- name: simNames[udid] ?? udid,
202
- workspace: null,
203
- id: udid,
204
- owned: false
205
- };
206
- claim(p.pid, `simulator:${udid}`, owner);
207
- continue;
208
- }
209
- const avd = emulatorAvd(p.command);
210
- if (avd) {
211
- const owner = emulators.get(avd) ?? {
212
- kind: "emulator",
213
- name: avd,
214
- workspace: null,
215
- id: avd,
216
- owned: false
217
- };
218
- claim(p.pid, `emulator:${avd}`, owner);
219
- }
220
- }
221
- const present = new Set(processes.map((p) => p.pid));
222
- const byPath = new Map(environments.map((env) => [env.path, env]));
223
- for (const root of roots) {
224
- const env = byPath.get(root.path);
225
- if (!env) continue;
226
- const metroPids = [root.supervisorPid, env.metro?.running ? env.metro.pid : null];
227
- for (const pid of metroPids) if (pid && present.has(pid)) {
228
- const port = env.metro?.port ?? null;
229
- claim(pid, `metro:${env.path}`, workspaceOwner("metro", env, port ? `Metro :${port}` : "Metro", port ? String(port) : null));
230
- }
231
- if (root.build && present.has(root.build.pid)) {
232
- const label = root.build.platform === "ios" ? "iOS build" : "Android build";
233
- claim(root.build.pid, `build:${env.path}`, workspaceOwner("build", env, label, root.build.platform));
234
- }
235
- for (const pid of root.macosPids ?? []) if (present.has(pid)) claim(pid, `macos:${env.path}`, workspaceOwner("macos", env, env.macos?.product ?? "macOS app", null));
236
- for (const pid of root.browserPids) if (present.has(pid)) claim(pid, `browser:${env.path}`, workspaceOwner("browser", env, "Chrome", null));
237
- }
238
- const parentOf = new Map(processes.map((p) => [p.pid, p.ppid]));
239
- const totals = /* @__PURE__ */ new Map();
240
- for (const p of processes) {
241
- let pid = p.pid;
242
- let key;
243
- let fallback;
244
- for (let depth = 0; pid !== void 0 && pid > 1 && depth < MAX_DEPTH; depth++) {
245
- key = rootOf.get(pid);
246
- if (key) break;
247
- fallback ??= fallbackOf.get(pid);
248
- pid = parentOf.get(pid);
249
- }
250
- key ??= fallback;
251
- if (!key) continue;
252
- const total = totals.get(key) ?? {
253
- rssKb: 0,
254
- memoryKb: 0,
255
- cpu: 0,
256
- processes: 0
257
- };
258
- total.rssKb += p.rssKb;
259
- const footprint = footprints?.get(p.pid);
260
- total.memoryKb += footprint === void 0 ? p.rssKb : footprint / 1024;
261
- total.cpu += p.cpuPercent;
262
- total.processes += 1;
263
- totals.set(key, total);
264
- }
265
- const list = [];
266
- for (const [key, owner] of owners) {
267
- const total = totals.get(key);
268
- if (total) list.push({
269
- ...owner,
270
- cpuPercent: Math.round(total.cpu),
271
- residentMb: Math.round(total.rssKb / 1024),
272
- memoryMb: Math.round(total.memoryKb / 1024),
273
- processes: total.processes
274
- });
275
- }
276
- list.sort((x, y) => rank(x) - rank(y) || (x.workspace ?? "~").localeCompare(y.workspace ?? "~") || x.name.localeCompare(y.name));
277
- return {
278
- memorySource: footprints ? "footprint" : "rss",
279
- owners: list
280
- };
281
- }
282
- //#endregion
283
- export { attributeMachineUsage as n, readFootprints as r, STIM_SERVER_COMMAND as t };
@@ -1,259 +0,0 @@
1
- import { _ as loadConfig$1, d as getConfigDir } from "./config-D0DbpZtM.mjs";
2
- import { E as withDirLock } from "./workspace-state-CL2V2Ons.mjs";
3
- import { _ as findPeer, b as realIo, g as endpoint, v as parseMachine$1 } from "./client-C2IS36Qf.mjs";
4
- import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
- import { acceptsShape, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, isJsonObject, readDeviceHostMachines, settingDefinition } from "@stim-cli/core/state";
6
- import { releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
7
- import { hostname } from "node:os";
8
- //#region src/device-host/machines.ts
9
- function note(title, detail, fix = null) {
10
- return {
11
- code: "device-host-machine",
12
- level: "note",
13
- title,
14
- detail,
15
- fix
16
- };
17
- }
18
- const approval = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
19
- function configuredMachines() {
20
- const hosting = loadConfig$1()?.hosting;
21
- if (hosting !== void 0 && !isJsonObject(hosting)) return null;
22
- const machines = hosting?.machines;
23
- if (machines === void 0) return [];
24
- return acceptsShape(settingDefinition("hosting.machines"), machines) ? machines : null;
25
- }
26
- function store(machines) {
27
- withDirLock(deviceHostMachinesLock(), () => {
28
- const file = deviceHostMachinesFile();
29
- const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
30
- try {
31
- writeFileSync(tmp, `${JSON.stringify({
32
- version: 1,
33
- machines
34
- }, null, 2)}\n`, { mode: 384 });
35
- renameSync(tmp, file);
36
- } finally {
37
- rmSync(tmp, { force: true });
38
- }
39
- }, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
40
- }
41
- async function request(machine, peer, port, deviceName, io) {
42
- const reply = await io.hello(endpoint(peer, port), {
43
- request: "device-host",
44
- deviceName
45
- });
46
- if (!("result" in reply) || reply.result?.approval?.state !== "pending" || typeof reply.result.deviceToken !== "string" || !reply.result.deviceToken || typeof reply.result.device?.id !== "string" || !reply.result.device.id || typeof reply.result.approval.expiresAt !== "string" || !reply.result.approval.expiresAt) return {
47
- report: {
48
- machine,
49
- dnsName: peer.dnsName,
50
- state: "unreachable"
51
- },
52
- finding: note(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
53
- };
54
- const credential = {
55
- machine,
56
- nodeId: peer.nodeId,
57
- dnsName: peer.dnsName,
58
- deviceId: reply.result.device.id,
59
- deviceToken: reply.result.deviceToken,
60
- state: "pending",
61
- requestedAt: (/* @__PURE__ */ new Date()).toISOString()
62
- };
63
- return {
64
- report: {
65
- machine,
66
- dnsName: peer.dnsName,
67
- state: "pending",
68
- deviceId: credential.deviceId,
69
- requestedAt: credential.requestedAt
70
- },
71
- finding: note(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(machine, credential.deviceId)),
72
- credential
73
- };
74
- }
75
- /** Reports separately approved hosting machines. Only --fix asks or forgets credentials; every token stays pinned. */
76
- async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
77
- if (entries === null) return {
78
- findings: [note("Invalid hosting.machines setting", "Use a hosting object with machines as an array of tailnet names. Existing hosting credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
79
- machines: []
80
- };
81
- const unavailable = (state, finding) => ({
82
- findings: [finding],
83
- machines: entries.map((machine) => ({
84
- machine,
85
- state
86
- }))
87
- });
88
- const unreadable = () => unavailable("credentials-unavailable", note("Hosting credentials are unreadable", `Stim preserves ${deviceHostMachinesFile()} and its pinned nodes.`, "Restore the existing file from a trusted backup or restore read access before running doctor again."));
89
- if (!entries.length) {
90
- if (!fix) return {
91
- findings: [],
92
- machines: []
93
- };
94
- try {
95
- if (!readDeviceHostMachines().length) return {
96
- findings: [],
97
- machines: []
98
- };
99
- } catch {
100
- return unreadable();
101
- }
102
- }
103
- const claim = tryAcquireClaim({
104
- root: deviceHostMachinesClaims(),
105
- mode: "exclusive",
106
- label: "hosting machine approval"
107
- });
108
- if (!claim.acquired) {
109
- releaseClaim(claim.pending);
110
- return unavailable("busy", note("Hosting machine approval is in use", "Another doctor run holds the hosting credential claim. Run doctor again after it finishes."));
111
- }
112
- try {
113
- let credentials;
114
- try {
115
- credentials = readDeviceHostMachines();
116
- } catch {
117
- return unreadable();
118
- }
119
- if (fix) {
120
- const kept = credentials.filter((each) => entries.includes(each.machine));
121
- if (kept.length !== credentials.length) {
122
- store(kept);
123
- credentials = kept;
124
- }
125
- }
126
- if (!entries.length) return {
127
- findings: [],
128
- machines: []
129
- };
130
- const status = io.status();
131
- if (!isJsonObject(status)) return unavailable("tailscale-off", note("Hosting machines are unreachable", "hosting.machines names hosting machines, but Tailscale is not running."));
132
- const self = isJsonObject(status.Self) ? status.Self : {};
133
- const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
134
- const inspected = {
135
- findings: [],
136
- machines: []
137
- };
138
- const namedNodes = /* @__PURE__ */ new Set();
139
- for (const machine of new Set(entries)) {
140
- const parsed = parseMachine$1(machine);
141
- if (!parsed) {
142
- inspected.machines.push({
143
- machine,
144
- state: "invalid"
145
- });
146
- inspected.findings.push(note(`Hosting machine ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
147
- continue;
148
- }
149
- const current = io.status();
150
- if (!isJsonObject(current)) {
151
- inspected.machines.push({
152
- machine,
153
- state: "tailscale-off"
154
- });
155
- inspected.findings.push(note(`Hosting machine ${machine} is unreachable`, "Tailscale stopped before this connection."));
156
- continue;
157
- }
158
- const peer = findPeer(current, parsed.name);
159
- if (peer === "missing" || peer === "ambiguous") {
160
- inspected.machines.push({
161
- machine,
162
- state: "not-on-tailnet"
163
- });
164
- inspected.findings.push(note(`Hosting machine ${machine} is not on this tailnet`, peer === "missing" ? "No peer has that name." : "Several peers match that name."));
165
- continue;
166
- }
167
- const nodeRoute = `${peer.nodeId}:${parsed.port}`;
168
- if (namedNodes.has(nodeRoute)) {
169
- inspected.machines.push({
170
- machine,
171
- dnsName: peer.dnsName,
172
- state: "invalid"
173
- });
174
- inspected.findings.push(note(`Hosting machine ${machine} repeats a named node`, "Name each tailnet node and serve port only once in hosting.machines."));
175
- continue;
176
- }
177
- namedNodes.add(nodeRoute);
178
- const credential = credentials.find((each) => each.machine === machine);
179
- const known = {
180
- machine,
181
- dnsName: peer.dnsName,
182
- ...credential ? {
183
- deviceId: credential.deviceId,
184
- requestedAt: credential.requestedAt
185
- } : {}
186
- };
187
- if (credential && credential.nodeId !== peer.nodeId) {
188
- inspected.machines.push({
189
- ...known,
190
- state: "node-changed"
191
- });
192
- inspected.findings.push(note(`Hosting machine ${machine} is a different tailnet node`, "Stim refuses to connect or send the saved token.", `If that Mac was replaced, remove ${machine} from hosting.machines, run \`stim doctor --fix\`, then add it back and run \`stim doctor --fix\` again to request approval on the new node.`));
193
- continue;
194
- }
195
- let ask = !credential && fix;
196
- if (credential) {
197
- const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
198
- if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
199
- if (credential.state !== "approved") {
200
- credentials = credentials.map((each) => each.machine === machine ? {
201
- ...each,
202
- state: "approved"
203
- } : each);
204
- store(credentials);
205
- }
206
- const host = reply.result.host;
207
- if (host && (host.screenRecording === false || host.accessibility === false)) {
208
- const panes = [...host.screenRecording === false ? ["Screen & System Audio Recording (Screen Recording on macOS 14)"] : [], ...host.accessibility === false ? ["Device Control and Data Access (Accessibility on macOS 26 and earlier)"] : []];
209
- const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
210
- inspected.findings.push(note(`Hosting machine ${machine} needs ${panes.join(" and ")} for ${host.name}`, detail.join(" "), `On ${machine}, approve ${host.name} in System Settings > Privacy & Security, or run \`stim-server service install\` there again to show the requests.`));
211
- }
212
- inspected.machines.push({
213
- ...known,
214
- state: "approved"
215
- });
216
- continue;
217
- }
218
- if ("error" in reply && reply.error?.code === "approval-pending") {
219
- inspected.machines.push({
220
- ...known,
221
- state: "pending"
222
- });
223
- inspected.findings.push(note(`Hosting machine ${machine} has not approved this Mac yet`, `Requested at ${credential.requestedAt}.`, approval(machine, credential.deviceId)));
224
- continue;
225
- }
226
- const revoked = "error" in reply && reply.error?.code === "unauthorized";
227
- ask = revoked && fix;
228
- if (!ask) {
229
- inspected.machines.push({
230
- ...known,
231
- state: revoked ? "revoked" : "unreachable"
232
- });
233
- inspected.findings.push(note(`Hosting machine ${machine} ${revoked ? "no longer accepts this Mac" : "did not confirm hosting access"}`, "The saved token and pinned node are preserved.", revoked ? "Run `stim doctor --fix` to ask again." : `Check stim-server and its tailnet serve route on ${machine}, then run doctor again.`));
234
- continue;
235
- }
236
- }
237
- if (ask) {
238
- const result = await request(machine, peer, parsed.port, deviceName, io);
239
- if (result.credential) {
240
- credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
241
- store(credentials);
242
- }
243
- inspected.machines.push(result.report);
244
- inspected.findings.push(result.finding);
245
- } else {
246
- inspected.machines.push({
247
- ...known,
248
- state: "not-asked"
249
- });
250
- inspected.findings.push(note(`Hosting machine ${machine} has not approved this Mac`, "This Mac has not asked for hosting access.", "Run `stim doctor --fix` to ask."));
251
- }
252
- }
253
- return inspected;
254
- } finally {
255
- releaseClaim(claim.acquired);
256
- }
257
- }
258
- //#endregion
259
- export { inspectDeviceHostMachines as n, configuredMachines as t };