stim 1.17.0 → 1.17.2

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 (47) hide show
  1. package/dist/{android--GorydsK.mjs → android-B9yaNKwa.mjs} +15 -15
  2. package/dist/{budget-DM984mnd.mjs → budget-DfcgFdQB.mjs} +33 -12
  3. package/dist/{build-plan-CmQCoyT-.mjs → build-plan-BDsSi09V.mjs} +14 -14
  4. package/dist/{idle-B2m-Ovod.mjs → build-progress-D0WkjCPd.mjs} +56 -183
  5. package/dist/{cli-Daxug5-K.mjs → cli-CYXb29oE.mjs} +13 -13
  6. package/dist/cli.mjs +1 -1
  7. package/dist/{machines-C2XiNwh9.mjs → client-SvBdgObD.mjs} +348 -309
  8. package/dist/{deps-DNWaQs4V.mjs → deps-iKw0eD4P.mjs} +1 -1
  9. package/dist/{device-capacity-D0GWHnTM.mjs → device-capacity-ZC4XSZ4R.mjs} +2 -2
  10. package/dist/device-host-worker.mjs +3 -3
  11. package/dist/{device-ios-Bc7vbGwl.mjs → device-ios-C2Um-XUR.mjs} +1 -1
  12. package/dist/{doctor-Cnk9d7Hm.mjs → doctor-CFv1fCrF.mjs} +14 -13
  13. package/dist/{gc-DxCL2A46.mjs → gc-DEmkg3Xh.mjs} +9 -8
  14. package/dist/{gradle-D7-L2Th3.mjs → gradle-dJpre3Yn.mjs} +1 -1
  15. package/dist/{guide-DjrTQGik.mjs → guide-CSmhMTf8.mjs} +156 -127
  16. package/dist/{hosted-android-DwLS4Mse.mjs → hosted-android-E1Heoqnv.mjs} +3 -3
  17. package/dist/{hosted-client-wk1W_CNW.mjs → hosted-client-Bp4knSwu.mjs} +4 -4
  18. package/dist/{hosted-ios-aMo7Cljf.mjs → hosted-ios-BnbeYOUD.mjs} +1 -1
  19. package/dist/{hosted-logs-DqYmWtXc.mjs → hosted-logs-OcgIBvCT.mjs} +2 -2
  20. package/dist/{hosted-macos-dlHXEeF5.mjs → hosted-macos-DcxKjtfH.mjs} +2 -2
  21. package/dist/{hosted-native-C9xJ9rpA.mjs → hosted-native-1Ebbig4w.mjs} +3 -3
  22. package/dist/idle-BXG__1BA.mjs +176 -0
  23. package/dist/{idle-shutdown-_3oOV1V_.mjs → idle-shutdown-IGyNV8Cr.mjs} +1 -1
  24. package/dist/{ios-DE6duQoz.mjs → ios-BJ4XUSe1.mjs} +18 -18
  25. package/dist/{logs-DUoKlDoS.mjs → logs-BAaS7gS2.mjs} +3 -3
  26. package/dist/{macos-32po3kUq.mjs → macos-BIRQS90f.mjs} +66 -25
  27. package/dist/maintenance-run.mjs +2 -2
  28. package/dist/{metro-gateway-DmslUM30.mjs → metro-gateway-C-ijGI2F.mjs} +1 -1
  29. package/dist/{native-runtime-DhJI3IVQ.mjs → native-runtime-BEK171xQ.mjs} +1 -1
  30. package/dist/offload-worker.mjs +5 -5
  31. package/dist/{preview-CxPRIEht.mjs → preview-B-g9d7kV.mjs} +1 -1
  32. package/dist/{reload-qZei6iWn.mjs → reload-DcRxj3qG.mjs} +2 -2
  33. package/dist/settings.schema.json +86 -68
  34. package/dist/{start-BopZcr4n.mjs → start-AlfCPRVU.mjs} +1 -1
  35. package/dist/{start-uee1wWjY.mjs → start-DUz3mYdb.mjs} +1 -1
  36. package/dist/{status-dXX2JX1C.mjs → status-BA2pehwf.mjs} +9 -9
  37. package/dist/{stop-Cr1d8DSE.mjs → stop-BIiM49EY.mjs} +1 -1
  38. package/dist/{stop-BjizrQwv.mjs → stop-DQIasgKp.mjs} +3 -3
  39. package/dist/{stop-BCwvvs7F.mjs → stop-zF1y04f2.mjs} +1 -1
  40. package/dist/supervisor-run.mjs +4 -4
  41. package/dist/{support-BJPgYCu-.mjs → support-CsJNVfOc.mjs} +4 -4
  42. package/dist/{support-F9-sr0Oi.mjs → support-WvD__tSV.mjs} +2 -2
  43. package/dist/{toolchain-CNIBTIOv.mjs → toolchain-DNyR3NPq.mjs} +10 -6
  44. package/dist/{web-exQtGkoc.mjs → web-Dl0KSRHf.mjs} +2 -2
  45. package/dist/{worktree-ZpZR3S1k.mjs → worktree-Be_7NLKy.mjs} +6 -6
  46. package/dist/{worktree-Dc98cE6_.mjs → worktree-j3IcFAI5.mjs} +1 -1
  47. package/package.json +5 -5
@@ -1,7 +1,7 @@
1
1
  import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
2
  import { _ as loadConfig$1, d as getConfigDir } from "./config-D0DbpZtM.mjs";
3
3
  import { w as withDirLock } from "./workspace-state-DqcCbpE7.mjs";
4
- import { T as CCACHE_UNAVAILABLE, a as toolchainMismatches, l as COMPILATION_CACHE_UNAVAILABLE, s as manifestDigest } from "./toolchain-CNIBTIOv.mjs";
4
+ import { T as CCACHE_UNAVAILABLE, a as toolchainMismatches, l as COMPILATION_CACHE_UNAVAILABLE, s as manifestDigest } from "./toolchain-DNyR3NPq.mjs";
5
5
  import { createHash } from "node:crypto";
6
6
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
7
7
  import { basename, dirname, join, relative } from "node:path";
@@ -30,6 +30,28 @@ function readHostPermissions(value) {
30
30
  //#region src/offload/tailnet.ts
31
31
  const MAC_APP_TAILSCALE = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
32
32
  const HELLO_TIMEOUT_MS = 1e4;
33
+ function isLapsed(credential) {
34
+ return credential.state === "pending" && !!credential.expiresAt && Date.parse(credential.expiresAt) <= Date.now();
35
+ }
36
+ function markApproved(credential) {
37
+ const { expiresAt: _expiresAt, ...rest } = credential;
38
+ return {
39
+ ...rest,
40
+ state: "approved"
41
+ };
42
+ }
43
+ function pendingReport(credential) {
44
+ return {
45
+ state: "pending",
46
+ ...credential.expiresAt ? { expiresAt: credential.expiresAt } : {}
47
+ };
48
+ }
49
+ function lapsedLine(machine, credential) {
50
+ return `${machine} did not approve this Mac before the request lapsed at ${credential.expiresAt}.`;
51
+ }
52
+ function requestedLine(credential) {
53
+ return `Requested at ${credential.requestedAt}${credential.expiresAt ? `; the request lapses at ${credential.expiresAt}` : ""}.`;
54
+ }
33
55
  function tailscaleStatus() {
34
56
  for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
35
57
  const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
@@ -94,16 +116,280 @@ function pinnedEndpoint$1(credential, status = tailscaleStatus) {
94
116
  return pinnedEndpoint(credential, parseMachine(credential.machine) ? status() : null);
95
117
  }
96
118
  //#endregion
119
+ //#region src/device-host/machines.ts
120
+ function note$1(title, detail, fix = null) {
121
+ return {
122
+ code: "device-host-machine",
123
+ level: "note",
124
+ title,
125
+ detail,
126
+ fix
127
+ };
128
+ }
129
+ const approval$1 = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
130
+ function configuredMachines() {
131
+ const remote = loadConfig$1()?.remote;
132
+ if (remote !== void 0 && !isJsonObject(remote)) return null;
133
+ const machines = remote?.machines;
134
+ if (machines === void 0) return [];
135
+ return acceptsShape(settingDefinition("remote.machines"), machines) ? machines : null;
136
+ }
137
+ /** The `remote.machines` entries this Mac asked for device-host access, in setting order; null when the setting is invalid. */
138
+ function hostingMachines() {
139
+ const configured = configuredMachines();
140
+ if (configured === null) return null;
141
+ try {
142
+ const asked = readDeviceHostMachines();
143
+ return configured.filter((machine) => asked.some((each) => each.machine === machine));
144
+ } catch {
145
+ return configured;
146
+ }
147
+ }
148
+ function store(machines) {
149
+ withDirLock(deviceHostMachinesLock(), () => {
150
+ const file = deviceHostMachinesFile();
151
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
152
+ try {
153
+ writeFileSync(tmp, `${JSON.stringify({
154
+ version: 1,
155
+ machines
156
+ }, null, 2)}\n`, { mode: 384 });
157
+ renameSync(tmp, file);
158
+ } finally {
159
+ rmSync(tmp, { force: true });
160
+ }
161
+ }, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
162
+ }
163
+ async function request(machine, peer, port, deviceName, io) {
164
+ const ticket = readAccessTicket();
165
+ const reply = await io.hello(endpoint(peer, port), {
166
+ request: "device-host",
167
+ deviceName,
168
+ ...ticket ? { setupTicket: ticket.ticket } : {}
169
+ });
170
+ 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 {
171
+ report: {
172
+ machine,
173
+ dnsName: peer.dnsName,
174
+ state: "unreachable"
175
+ },
176
+ finding: note$1(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
177
+ };
178
+ const credential = {
179
+ machine,
180
+ nodeId: peer.nodeId,
181
+ dnsName: peer.dnsName,
182
+ deviceId: reply.result.device.id,
183
+ deviceToken: reply.result.deviceToken,
184
+ state: "pending",
185
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
186
+ expiresAt: reply.result.approval.expiresAt,
187
+ ...ticket ? { ticketHash: ticket.ticketHash } : {}
188
+ };
189
+ return {
190
+ report: {
191
+ machine,
192
+ dnsName: peer.dnsName,
193
+ state: "pending",
194
+ deviceId: credential.deviceId,
195
+ requestedAt: credential.requestedAt,
196
+ expiresAt: credential.expiresAt
197
+ },
198
+ finding: note$1(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval$1(machine, credential.deviceId)),
199
+ credential
200
+ };
201
+ }
202
+ /** Reports the device-host approval of each remote Mac, separate from its build approval. Only --fix asks or forgets credentials; every token stays pinned. */
203
+ async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
204
+ if (entries === null) return {
205
+ findings: [note$1("Invalid remote.machines setting", "Use a remote 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.")],
206
+ machines: []
207
+ };
208
+ const unavailable = (state, finding) => ({
209
+ findings: [finding],
210
+ machines: entries.map((machine) => ({
211
+ machine,
212
+ state
213
+ }))
214
+ });
215
+ const unreadable = () => unavailable("credentials-unavailable", note$1("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."));
216
+ if (!entries.length) {
217
+ if (!fix) return {
218
+ findings: [],
219
+ machines: []
220
+ };
221
+ try {
222
+ if (!readDeviceHostMachines().length) return {
223
+ findings: [],
224
+ machines: []
225
+ };
226
+ } catch {
227
+ return unreadable();
228
+ }
229
+ }
230
+ const claim = tryAcquireClaim({
231
+ root: deviceHostMachinesClaims(),
232
+ mode: "exclusive",
233
+ label: "device-host approval"
234
+ });
235
+ if (!claim.acquired) {
236
+ releaseClaim(claim.pending);
237
+ return unavailable("busy", note$1("Device-host approval is in use", "Another doctor run holds the hosting credential claim. Run doctor again after it finishes."));
238
+ }
239
+ try {
240
+ let credentials;
241
+ try {
242
+ credentials = readDeviceHostMachines();
243
+ } catch {
244
+ return unreadable();
245
+ }
246
+ if (fix) {
247
+ const kept = credentials.filter((each) => entries.includes(each.machine));
248
+ if (kept.length !== credentials.length) {
249
+ store(kept);
250
+ credentials = kept;
251
+ }
252
+ }
253
+ if (!entries.length) return {
254
+ findings: [],
255
+ machines: []
256
+ };
257
+ const status = io.status();
258
+ if (!isJsonObject(status)) return unavailable("tailscale-off", note$1("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running."));
259
+ const self = isJsonObject(status.Self) ? status.Self : {};
260
+ const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
261
+ const inspected = {
262
+ findings: [],
263
+ machines: []
264
+ };
265
+ const namedNodes = /* @__PURE__ */ new Set();
266
+ for (const machine of new Set(entries)) {
267
+ const parsed = parseMachine$1(machine);
268
+ if (!parsed) {
269
+ inspected.machines.push({
270
+ machine,
271
+ state: "invalid"
272
+ });
273
+ inspected.findings.push(note$1(`Remote Mac ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
274
+ continue;
275
+ }
276
+ const current = io.status();
277
+ if (!isJsonObject(current)) {
278
+ inspected.machines.push({
279
+ machine,
280
+ state: "tailscale-off"
281
+ });
282
+ inspected.findings.push(note$1(`Remote Mac ${machine} is unreachable`, "Tailscale stopped before this connection."));
283
+ continue;
284
+ }
285
+ const peer = findPeer$1(current, parsed.name);
286
+ if (peer === "missing" || peer === "ambiguous") {
287
+ inspected.machines.push({
288
+ machine,
289
+ state: "not-on-tailnet"
290
+ });
291
+ inspected.findings.push(note$1(`Remote Mac ${machine} is not on this tailnet`, peer === "missing" ? "No peer has that name." : "Several peers match that name."));
292
+ continue;
293
+ }
294
+ const nodeRoute = `${peer.nodeId}:${parsed.port}`;
295
+ if (namedNodes.has(nodeRoute)) {
296
+ inspected.machines.push({
297
+ machine,
298
+ dnsName: peer.dnsName,
299
+ state: "invalid"
300
+ });
301
+ inspected.findings.push(note$1(`Remote Mac ${machine} repeats a named node`, "Name each tailnet node and serve port only once in remote.machines."));
302
+ continue;
303
+ }
304
+ namedNodes.add(nodeRoute);
305
+ const credential = credentials.find((each) => each.machine === machine);
306
+ const known = {
307
+ machine,
308
+ dnsName: peer.dnsName,
309
+ ...credential ? {
310
+ deviceId: credential.deviceId,
311
+ requestedAt: credential.requestedAt
312
+ } : {}
313
+ };
314
+ if (credential && credential.nodeId !== peer.nodeId) {
315
+ inspected.machines.push({
316
+ ...known,
317
+ state: "node-changed"
318
+ });
319
+ inspected.findings.push(note$1(`Remote Mac ${machine} is a different tailnet node`, "Stim refuses to connect or send the saved token.", `If that Mac was replaced, remove ${machine} from remote.machines, run \`stim doctor --fix\`, then add it back and run \`stim doctor --fix\` again to request approval on the new node.`));
320
+ continue;
321
+ }
322
+ let ask = !credential && fix;
323
+ if (credential) {
324
+ const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
325
+ if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
326
+ if (credential.state !== "approved") {
327
+ credentials = credentials.map((each) => each.machine === machine ? markApproved(each) : each);
328
+ store(credentials);
329
+ }
330
+ const host = readHostPermissions(reply.result.host);
331
+ if (host && (host.screenRecording === false || host.accessibility === false)) {
332
+ 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)"] : []];
333
+ const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
334
+ inspected.findings.push(note$1(`Remote Mac ${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.`));
335
+ }
336
+ inspected.machines.push({
337
+ ...known,
338
+ state: "approved",
339
+ ...host ? { host } : {}
340
+ });
341
+ continue;
342
+ }
343
+ const pending = "error" in reply && reply.error?.code === "approval-pending";
344
+ const revoked = "error" in reply && reply.error?.code === "unauthorized";
345
+ const ticket = fix && pending && credential.state !== "approved" ? readAccessTicket() : void 0;
346
+ ask = fix && (revoked || !!ticket && credential.ticketHash !== ticket.ticketHash);
347
+ if (pending && !ask) {
348
+ inspected.machines.push({
349
+ ...known,
350
+ ...pendingReport(credential)
351
+ });
352
+ inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting yet`, requestedLine(credential), approval$1(machine, credential.deviceId)));
353
+ continue;
354
+ }
355
+ if (!ask) {
356
+ const lapsed = revoked && isLapsed(credential);
357
+ inspected.machines.push({
358
+ ...known,
359
+ state: lapsed ? "lapsed" : revoked ? "revoked" : "unreachable"
360
+ });
361
+ inspected.findings.push(note$1(lapsed ? `Hosting request to ${machine} lapsed` : `Remote Mac ${machine} ${revoked ? "no longer accepts this Mac for device hosting" : "did not confirm hosting access"}`, lapsed ? `${lapsedLine(machine, credential)} The saved token and pinned node are preserved.` : "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.`));
362
+ continue;
363
+ }
364
+ }
365
+ if (ask) {
366
+ const result = await request(machine, peer, parsed.port, deviceName, io);
367
+ if (result.credential) {
368
+ credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
369
+ store(credentials);
370
+ }
371
+ inspected.machines.push(result.report);
372
+ inspected.findings.push(result.finding);
373
+ } else {
374
+ inspected.machines.push({
375
+ ...known,
376
+ state: "not-asked"
377
+ });
378
+ inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting`, "This Mac has not asked for hosting access.", "Run `stim doctor --fix` to ask."));
379
+ }
380
+ }
381
+ return inspected;
382
+ } finally {
383
+ releaseClaim(claim.acquired);
384
+ }
385
+ }
386
+ //#endregion
97
387
  //#region src/offload/build-machines.ts
98
- /** The build machines named in `offload.machines` that this Mac asked for build access, in that order. */
99
- function pairedMachines(entries = configuredMachines$1()) {
388
+ /** The `remote.machines` entries that this Mac asked for build access, in that order. */
389
+ function pairedMachines(entries = configuredMachines() ?? []) {
100
390
  const credentials = readBuildMachines();
101
391
  return entries.flatMap((entry) => credentials.filter((each) => each.machine === entry));
102
392
  }
103
- function configuredMachines$1() {
104
- const machines = loadConfig$1()?.offload?.machines;
105
- return Array.isArray(machines) ? machines.filter((entry) => typeof entry === "string") : [];
106
- }
107
393
  function updateCredentials(change) {
108
394
  const dir = getConfigDir();
109
395
  withDirLock(buildMachinesLock(), () => {
@@ -121,7 +407,7 @@ function updateCredentials(change) {
121
407
  }
122
408
  }, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
123
409
  }
124
- function note$1(title, detail, fix = null) {
410
+ function note(title, detail, fix = null) {
125
411
  return {
126
412
  code: "build-machine",
127
413
  level: "note",
@@ -130,7 +416,7 @@ function note$1(title, detail, fix = null) {
130
416
  fix
131
417
  };
132
418
  }
133
- const approval$1 = (entry, deviceId) => `A person on ${entry} approves it with \`stim-server devices grant ${deviceId} --build\`.`;
419
+ const approval = (entry, deviceId) => `A person on ${entry} approves it with \`stim-server devices grant ${deviceId} --build\`.`;
134
420
  const PROBLEM_TITLES = {
135
421
  unreachable: "does not answer a build offer",
136
422
  checkout: "cannot build an app outside a git checkout",
@@ -155,7 +441,7 @@ function problemFix(code, entry) {
155
441
  case "unreachable": return `Check that stim-server runs on ${entry} and that its tailscale serve route answers, then run \`stim doctor\` again. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`;
156
442
  case "checkout": return "Offload syncs the files git lists; run Stim from a git checkout of the app.";
157
443
  case "stim-build": return `Update stim-server on ${entry} to the same Stim build as this Mac: there, run \`stim-server service update --release <version>\` for this Mac's release, or \`--from <dir>\` with the packed packages of this Mac's checkout.`;
158
- case "arch": return "Use a build machine with the same CPU architecture as this Mac.";
444
+ case "arch": return "Use a remote Mac with the same CPU architecture as this Mac.";
159
445
  case "xcode":
160
446
  case "simulator-sdk":
161
447
  case "macos-sdk": return `Install and select the same Xcode on ${entry} and this Mac (\`xcode-select -p\` on each).`;
@@ -167,8 +453,8 @@ function problemFix(code, entry) {
167
453
  case "ndk":
168
454
  case "build-tools":
169
455
  case "compile-sdk": return `Install the missing package with sdkmanager in the Android SDK stim-server on ${entry} uses (its ANDROID_HOME, else ~/Library/Android/sdk).`;
170
- case "disk": return `Free space on ${entry}'s worker root (offload.workerRoot there).`;
171
- case "busy": return `Builds stay on this Mac until ${entry} has capacity. On ${entry}, offload.maxLoadPerCore sets the load it accepts and concurrency.maxBuilds how many of its own builds it runs; it takes one offloaded build at a time.`;
456
+ case "disk": return `Free space on ${entry}'s worker root (server.workerRoot there).`;
457
+ case "busy": return `Builds stay on this Mac until ${entry} has capacity. On ${entry}, server.maxLoadPerCore sets the load it accepts and concurrency.maxBuilds how many of its own builds it runs; it takes one offloaded build at a time.`;
172
458
  }
173
459
  }
174
460
  const TRANSIENT = /* @__PURE__ */ new Set(["unreachable", "busy"]);
@@ -176,7 +462,7 @@ function offloadFindings(entry, problems) {
176
462
  return problems.map((problem) => ({
177
463
  code: `build-machine-${problem.code}`,
178
464
  level: TRANSIENT.has(problem.code) ? "note" : "cost",
179
- title: `Build machine ${entry} ${PROBLEM_TITLES[problem.code]}`,
465
+ title: `Remote Mac ${entry} ${PROBLEM_TITLES[problem.code]}`,
180
466
  detail: `Builds fall back to this Mac: ${problem.reason}.`,
181
467
  fix: problemFix(problem.code, entry)
182
468
  }));
@@ -196,7 +482,7 @@ async function requestAccess(entry, peer, port, deviceName, io) {
196
482
  state: "unreachable",
197
483
  dnsName: peer.dnsName
198
484
  },
199
- finding: note$1(`Could not ask ${entry} for build access`, reason)
485
+ finding: note(`Could not ask ${entry} for build access`, reason)
200
486
  };
201
487
  }
202
488
  const credential = {
@@ -207,6 +493,7 @@ async function requestAccess(entry, peer, port, deviceName, io) {
207
493
  deviceToken: reply.result.deviceToken,
208
494
  state: "pending",
209
495
  requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
496
+ expiresAt: reply.result.approval.expiresAt,
210
497
  ...ticket ? { ticketHash: ticket.ticketHash } : {}
211
498
  };
212
499
  updateCredentials((credentials) => [...credentials.filter((each) => each.machine !== entry), credential]);
@@ -216,9 +503,10 @@ async function requestAccess(entry, peer, port, deviceName, io) {
216
503
  state: "pending",
217
504
  dnsName: peer.dnsName,
218
505
  deviceId: credential.deviceId,
219
- requestedAt: credential.requestedAt
506
+ requestedAt: credential.requestedAt,
507
+ expiresAt: credential.expiresAt
220
508
  },
221
- finding: note$1(`Asked ${entry} for build access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval$1(entry, credential.deviceId))
509
+ finding: note(`Asked ${entry} for build access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(entry, credential.deviceId))
222
510
  };
223
511
  }
224
512
  async function inspectMachine(entry, status, deviceName, fix, io, check) {
@@ -228,7 +516,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
228
516
  machine: entry,
229
517
  state: "invalid"
230
518
  },
231
- finding: note$1(`Build machine ${entry} is not a tailnet name`, "Expected `name` or `name:port`.")
519
+ finding: note(`Remote Mac ${entry} is not a tailnet name`, "Expected `name` or `name:port`.")
232
520
  };
233
521
  const peer = findPeer$1(status, parsed.name);
234
522
  if (peer === "missing" || peer === "ambiguous") {
@@ -238,7 +526,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
238
526
  machine: entry,
239
527
  state: "not-on-tailnet"
240
528
  },
241
- finding: note$1(`Build machine ${entry} is not on this tailnet`, detail)
529
+ finding: note(`Remote Mac ${entry} is not on this tailnet`, detail)
242
530
  };
243
531
  }
244
532
  const credential = readBuildMachines().find((each) => each.machine === entry);
@@ -253,7 +541,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
253
541
  deviceId: credential.deviceId,
254
542
  requestedAt: credential.requestedAt
255
543
  },
256
- finding: note$1(`Build machine ${entry} is a different tailnet node`, `This Mac paired with node ${credential.nodeId}, but ${peer.dnsName} is now node ${peer.nodeId}. Stim does not connect to it.`, `If that Mac was replaced, remove ${entry} from offload.machines, run \`stim doctor --fix\` to forget the old pairing, then add it back and run \`stim doctor --fix\` again.`)
544
+ finding: note(`Remote Mac ${entry} is a different tailnet node`, `This Mac paired with node ${credential.nodeId}, but ${peer.dnsName} is now node ${peer.nodeId}. Stim does not connect to it.`, `If that Mac was replaced, remove ${entry} from remote.machines, run \`stim doctor --fix\` to forget the old pairing, then add it back and run \`stim doctor --fix\` again.`)
257
545
  };
258
546
  if (!credential) {
259
547
  if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
@@ -262,7 +550,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
262
550
  ...known,
263
551
  state: "not-asked"
264
552
  },
265
- finding: note$1(`Build machine ${entry} has not approved this Mac`, "This Mac has not asked it for build access.", "Run `stim doctor --fix` to ask.")
553
+ finding: note(`Remote Mac ${entry} has not approved this Mac for builds`, "This Mac has not asked it for build access.", "Run `stim doctor --fix` to ask.")
266
554
  };
267
555
  }
268
556
  const paired = {
@@ -274,10 +562,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
274
562
  if ("result" in reply && reply.result.capabilities.includes("build")) {
275
563
  const permissions = readHostPermissions(reply.result.host);
276
564
  const host = permissions ? { host: permissions } : {};
277
- if (credential.state !== "approved") updateCredentials((credentials) => credentials.map((each) => each.machine === entry ? {
278
- ...each,
279
- state: "approved"
280
- } : each));
565
+ if (credential.state !== "approved") updateCredentials((credentials) => credentials.map((each) => each.machine === entry ? markApproved(each) : each));
281
566
  if (!check) return {
282
567
  report: {
283
568
  ...paired,
@@ -310,19 +595,26 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
310
595
  return {
311
596
  report: {
312
597
  ...paired,
313
- state: "pending"
598
+ ...pendingReport(credential)
314
599
  },
315
- finding: note$1(`Build machine ${entry} has not approved this Mac yet`, `Requested at ${credential.requestedAt}.`, approval$1(entry, credential.deviceId))
600
+ finding: note(`Remote Mac ${entry} has not approved this Mac for builds yet`, requestedLine(credential), approval(entry, credential.deviceId))
316
601
  };
317
602
  }
318
603
  if ("error" in reply && reply.error.code === "unauthorized") {
319
604
  if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
605
+ if (isLapsed(credential)) return {
606
+ report: {
607
+ ...paired,
608
+ state: "lapsed"
609
+ },
610
+ finding: note(`Build request to ${entry} lapsed`, lapsedLine(entry, credential), "Run `stim doctor --fix` to ask again.")
611
+ };
320
612
  return {
321
613
  report: {
322
614
  ...paired,
323
615
  state: "revoked"
324
616
  },
325
- finding: note$1(`Build machine ${entry} no longer accepts this Mac`, `${reply.error.message} It was revoked, or the request lapsed before approval.`, "Run `stim doctor --fix` to ask again.")
617
+ finding: note(`Remote Mac ${entry} no longer accepts this Mac for builds`, `${reply.error.message} It was revoked or denied.`, "Run `stim doctor --fix` to ask again.")
326
618
  };
327
619
  }
328
620
  const reason = "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "unexpected reply";
@@ -331,17 +623,21 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
331
623
  ...paired,
332
624
  state: "unreachable"
333
625
  },
334
- finding: note$1(`Could not reach build machine ${entry}`, reason, `Check that stim-server runs on ${entry}. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`)
626
+ finding: note(`Could not reach remote Mac ${entry}`, reason, `Check that stim-server runs on ${entry}. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`)
335
627
  };
336
628
  }
337
629
  /**
338
- * Doctor findings and a report for each `offload.machines` entry. The worker's node is pinned when access is
630
+ * Doctor findings and a report for each `remote.machines` entry. The worker's node is pinned when access is
339
631
  * requested; a later connection goes only to the current MagicDNS name of that same node, checked before the token
340
632
  * is sent. With `fix`, requests access from a named machine this Mac holds no pairing for, requests again when the
341
633
  * pinned node forgot this Mac or, with `STIM_ACCESS_TICKET` set, when a pending request carries another ticket, and forgets pairings of machines no longer named. With `check`, each approved
342
634
  * machine is asked for one build offer and reports every reason it would not take this project's build.
343
635
  */
344
- async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines$1()) {
636
+ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines()) {
637
+ if (entries === null) return {
638
+ findings: [note("Invalid remote.machines setting", "Use a remote object with machines as an array of tailnet names. Existing credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
639
+ machines: []
640
+ };
345
641
  const unnamed = (each) => !entries.includes(each.machine);
346
642
  if (fix && readBuildMachines().some(unnamed)) updateCredentials((credentials) => credentials.filter((each) => !unnamed(each)));
347
643
  if (entries.length === 0) return {
@@ -350,7 +646,7 @@ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries
350
646
  };
351
647
  const status = io.status();
352
648
  if (!isJsonObject(status)) return {
353
- findings: [note$1("Build machines are unreachable", "offload.machines names build machines, but Tailscale is not running.")],
649
+ findings: [note("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running.")],
354
650
  machines: entries.map((machine) => ({
355
651
  machine,
356
652
  state: "tailscale-off"
@@ -374,14 +670,14 @@ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries
374
670
  //#region src/offload/selection.ts
375
671
  var OffloadRefusal = class extends Error {
376
672
  code = "STIM_OFFLOAD_REFUSED";
377
- remedy = "Run stim doctor --fix to ask for build access if not paired. On the worker, a person finds the id with stim-server devices and approves it with stim-server devices grant <id> --build. Check stim settings get offload.machines, or rerun with --build-machine auto or --build-machine local.";
673
+ remedy = "Run stim doctor --fix to ask for build access if not paired. On the worker, a person finds the id with stim-server devices and approves it with stim-server devices grant <id> --build. Check stim settings get remote.machines, or rerun with --remote-build auto or --remote-build local.";
378
674
  constructor(machine, reason) {
379
675
  super(reason.startsWith(`${machine}: `) ? reason : `${machine}: ${reason}`);
380
676
  }
381
677
  };
382
678
  function resolveBuildMachine(flag, env, setting) {
383
679
  const selected = flag ?? (env?.trim() || void 0) ?? setting ?? "auto";
384
- if (typeof selected !== "string" || !selected.trim() || !parseMachine(selected)) throw Object.assign(/* @__PURE__ */ new Error("--build-machine / offload.machine must be auto, local, or a tailnet machine name."), { code: "STIM_BAD_ARG" });
680
+ if (typeof selected !== "string" || !selected.trim() || !parseMachine(selected)) throw Object.assign(/* @__PURE__ */ new Error("--remote-build / remote.build must be auto, local, or a tailnet machine name."), { code: "STIM_BAD_ARG" });
385
681
  const trimmed = selected.trim();
386
682
  const reserved = trimmed.toLowerCase();
387
683
  return reserved === "auto" || reserved === "local" ? reserved : trimmed;
@@ -404,16 +700,16 @@ function requireConfiguredMachine(selected, machines) {
404
700
  const candidate = parseMachine(entry);
405
701
  return candidate?.name === parsed.name && candidate.port === parsed.port;
406
702
  });
407
- if (!match) throw new OffloadRefusal(selected, `not listed in offload.machines (configured: ${entries.join(", ") || "none"})`);
703
+ if (!match) throw new OffloadRefusal(selected, `not listed in remote.machines (configured: ${entries.join(", ") || "none"})`);
408
704
  return match;
409
705
  }
410
706
  function resolveBuildPlacement(flag) {
411
- const offload = loadConfig$1()?.offload;
707
+ const remote = loadConfig$1()?.remote;
412
708
  let selected = "auto";
413
709
  try {
414
- selected = resolveBuildMachine(flag, process.env.STIM_OFFLOAD_MACHINE, offload?.machine);
415
- selected = requireConfiguredMachine(selected, offload?.machines);
416
- if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no build machine is paired");
710
+ selected = resolveBuildMachine(flag, process.env.STIM_REMOTE_BUILD, remote?.build);
711
+ selected = requireConfiguredMachine(selected, remote?.machines);
712
+ if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
417
713
  return { selected };
418
714
  } catch (error) {
419
715
  return {
@@ -421,7 +717,7 @@ function resolveBuildPlacement(flag) {
421
717
  failure: {
422
718
  code: error.code,
423
719
  message: error.message,
424
- remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --build-machine auto, local, or an entry from stim settings get offload.machines."
720
+ remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --remote-build auto, local, or an entry from stim settings get remote.machines."
425
721
  }
426
722
  };
427
723
  }
@@ -450,7 +746,7 @@ const CHUNK_BYTES = 61440;
450
746
  const MAX_BUFFERED = 8388608;
451
747
  const DIGEST_BYTES = 32;
452
748
  function offloadMode(env = process.env) {
453
- const raw = env.STIM_OFFLOAD_MODE || loadConfig$1()?.offload?.mode;
749
+ const raw = env.STIM_REMOTE_BUILD_MODE || loadConfig$1()?.remote?.buildMode;
454
750
  return OFFLOAD_MODES.includes(raw) ? raw : "auto";
455
751
  }
456
752
  function buildPlacementCandidates(selected) {
@@ -478,30 +774,30 @@ function placementLoad(choice) {
478
774
  })} there`;
479
775
  }
480
776
  /**
481
- * Whether a build may go to a build machine at all, before any machine is asked. `auto` keeps the build here while
482
- * this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `offload.maxLoadPerCore`.
777
+ * Whether a build may go to a remote Mac at all, before any machine is asked. `auto` keeps the build here while
778
+ * this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `server.maxLoadPerCore`.
483
779
  * A named selection ignores local capacity and mode, and refuses an unsupported build or missing pairing.
484
780
  */
485
781
  function offloadPlacement({ mode, machines, here, unsupported, selected = "auto" }) {
486
782
  if (namedBuildMachine(selected)) {
487
- if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named build machine cannot take it`);
488
- if (machines === 0) throw new OffloadRefusal(selected, "no build machine is paired");
783
+ if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named remote Mac cannot take it`);
784
+ if (machines === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
489
785
  return {
490
786
  offload: true,
491
- reason: `selected with --build-machine ${selected}`
787
+ reason: `selected with --remote-build ${selected}`
492
788
  };
493
789
  }
494
790
  if (selected === "local") return {
495
791
  offload: false,
496
- reason: "--build-machine local"
792
+ reason: "--remote-build local"
497
793
  };
498
794
  if (mode === "off") return {
499
795
  offload: false,
500
- reason: "offload.mode is off"
796
+ reason: "remote.buildMode is off"
501
797
  };
502
798
  if (machines === 0) return {
503
799
  offload: false,
504
- reason: "no build machine is paired"
800
+ reason: "no remote Mac is paired"
505
801
  };
506
802
  if (unsupported) return {
507
803
  offload: false,
@@ -509,7 +805,7 @@ function offloadPlacement({ mode, machines, here, unsupported, selected = "auto"
509
805
  };
510
806
  if (mode === "force") return {
511
807
  offload: true,
512
- reason: "offload.mode is force"
808
+ reason: "remote.buildMode is force"
513
809
  };
514
810
  const busy = saturation(here);
515
811
  return busy ? {
@@ -797,7 +1093,7 @@ var BuildConnection = class BuildConnection {
797
1093
  function replyError(reply) {
798
1094
  return "error" in reply ? `${reply.error.code}: ${reply.error.message}` : null;
799
1095
  }
800
- /** The repository on the build machine: one area per git common dir, shared by all its worktrees. */
1096
+ /** The repository on the remote Mac: one area per git common dir, shared by all its worktrees. */
801
1097
  function repoIdentity(projectRoot) {
802
1098
  const run = getExecutor().runFile;
803
1099
  const repoRoot = realpathSync(run("git", [
@@ -937,7 +1233,7 @@ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machi
937
1233
  asked.forEach((each, at) => {
938
1234
  if (!order.includes(at) && "connection" in each) each.connection.close();
939
1235
  });
940
- if (order.length === 0) return reasons.length ? reasons.join("; ") : "no build machine is paired";
1236
+ if (order.length === 0) return reasons.length ? reasons.join("; ") : "no remote Mac is paired";
941
1237
  for (const reason of reasons) note(reason);
942
1238
  const [first, ...rest] = order.map((at) => {
943
1239
  const pick = asked[at];
@@ -1366,261 +1662,4 @@ function offloadCheck(projectRoot, targets) {
1366
1662
  };
1367
1663
  }
1368
1664
  //#endregion
1369
- //#region src/device-host/machines.ts
1370
- function note(title, detail, fix = null) {
1371
- return {
1372
- code: "device-host-machine",
1373
- level: "note",
1374
- title,
1375
- detail,
1376
- fix
1377
- };
1378
- }
1379
- const approval = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
1380
- function configuredMachines() {
1381
- const hosting = loadConfig$1()?.hosting;
1382
- if (hosting !== void 0 && !isJsonObject(hosting)) return null;
1383
- const machines = hosting?.machines;
1384
- if (machines === void 0) return [];
1385
- return acceptsShape(settingDefinition("hosting.machines"), machines) ? machines : null;
1386
- }
1387
- function store(machines) {
1388
- withDirLock(deviceHostMachinesLock(), () => {
1389
- const file = deviceHostMachinesFile();
1390
- const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1391
- try {
1392
- writeFileSync(tmp, `${JSON.stringify({
1393
- version: 1,
1394
- machines
1395
- }, null, 2)}\n`, { mode: 384 });
1396
- renameSync(tmp, file);
1397
- } finally {
1398
- rmSync(tmp, { force: true });
1399
- }
1400
- }, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
1401
- }
1402
- async function request(machine, peer, port, deviceName, io) {
1403
- const ticket = readAccessTicket();
1404
- const reply = await io.hello(endpoint(peer, port), {
1405
- request: "device-host",
1406
- deviceName,
1407
- ...ticket ? { setupTicket: ticket.ticket } : {}
1408
- });
1409
- 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 {
1410
- report: {
1411
- machine,
1412
- dnsName: peer.dnsName,
1413
- state: "unreachable"
1414
- },
1415
- finding: note(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
1416
- };
1417
- const credential = {
1418
- machine,
1419
- nodeId: peer.nodeId,
1420
- dnsName: peer.dnsName,
1421
- deviceId: reply.result.device.id,
1422
- deviceToken: reply.result.deviceToken,
1423
- state: "pending",
1424
- requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
1425
- ...ticket ? { ticketHash: ticket.ticketHash } : {}
1426
- };
1427
- return {
1428
- report: {
1429
- machine,
1430
- dnsName: peer.dnsName,
1431
- state: "pending",
1432
- deviceId: credential.deviceId,
1433
- requestedAt: credential.requestedAt
1434
- },
1435
- finding: note(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(machine, credential.deviceId)),
1436
- credential
1437
- };
1438
- }
1439
- /** Reports separately approved hosting machines. Only --fix asks or forgets credentials; every token stays pinned. */
1440
- async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
1441
- if (entries === null) return {
1442
- 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.")],
1443
- machines: []
1444
- };
1445
- const unavailable = (state, finding) => ({
1446
- findings: [finding],
1447
- machines: entries.map((machine) => ({
1448
- machine,
1449
- state
1450
- }))
1451
- });
1452
- 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."));
1453
- if (!entries.length) {
1454
- if (!fix) return {
1455
- findings: [],
1456
- machines: []
1457
- };
1458
- try {
1459
- if (!readDeviceHostMachines().length) return {
1460
- findings: [],
1461
- machines: []
1462
- };
1463
- } catch {
1464
- return unreadable();
1465
- }
1466
- }
1467
- const claim = tryAcquireClaim({
1468
- root: deviceHostMachinesClaims(),
1469
- mode: "exclusive",
1470
- label: "hosting machine approval"
1471
- });
1472
- if (!claim.acquired) {
1473
- releaseClaim(claim.pending);
1474
- return unavailable("busy", note("Hosting machine approval is in use", "Another doctor run holds the hosting credential claim. Run doctor again after it finishes."));
1475
- }
1476
- try {
1477
- let credentials;
1478
- try {
1479
- credentials = readDeviceHostMachines();
1480
- } catch {
1481
- return unreadable();
1482
- }
1483
- if (fix) {
1484
- const kept = credentials.filter((each) => entries.includes(each.machine));
1485
- if (kept.length !== credentials.length) {
1486
- store(kept);
1487
- credentials = kept;
1488
- }
1489
- }
1490
- if (!entries.length) return {
1491
- findings: [],
1492
- machines: []
1493
- };
1494
- const status = io.status();
1495
- if (!isJsonObject(status)) return unavailable("tailscale-off", note("Hosting machines are unreachable", "hosting.machines names hosting machines, but Tailscale is not running."));
1496
- const self = isJsonObject(status.Self) ? status.Self : {};
1497
- const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
1498
- const inspected = {
1499
- findings: [],
1500
- machines: []
1501
- };
1502
- const namedNodes = /* @__PURE__ */ new Set();
1503
- for (const machine of new Set(entries)) {
1504
- const parsed = parseMachine$1(machine);
1505
- if (!parsed) {
1506
- inspected.machines.push({
1507
- machine,
1508
- state: "invalid"
1509
- });
1510
- inspected.findings.push(note(`Hosting machine ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
1511
- continue;
1512
- }
1513
- const current = io.status();
1514
- if (!isJsonObject(current)) {
1515
- inspected.machines.push({
1516
- machine,
1517
- state: "tailscale-off"
1518
- });
1519
- inspected.findings.push(note(`Hosting machine ${machine} is unreachable`, "Tailscale stopped before this connection."));
1520
- continue;
1521
- }
1522
- const peer = findPeer$1(current, parsed.name);
1523
- if (peer === "missing" || peer === "ambiguous") {
1524
- inspected.machines.push({
1525
- machine,
1526
- state: "not-on-tailnet"
1527
- });
1528
- inspected.findings.push(note(`Hosting machine ${machine} is not on this tailnet`, peer === "missing" ? "No peer has that name." : "Several peers match that name."));
1529
- continue;
1530
- }
1531
- const nodeRoute = `${peer.nodeId}:${parsed.port}`;
1532
- if (namedNodes.has(nodeRoute)) {
1533
- inspected.machines.push({
1534
- machine,
1535
- dnsName: peer.dnsName,
1536
- state: "invalid"
1537
- });
1538
- inspected.findings.push(note(`Hosting machine ${machine} repeats a named node`, "Name each tailnet node and serve port only once in hosting.machines."));
1539
- continue;
1540
- }
1541
- namedNodes.add(nodeRoute);
1542
- const credential = credentials.find((each) => each.machine === machine);
1543
- const known = {
1544
- machine,
1545
- dnsName: peer.dnsName,
1546
- ...credential ? {
1547
- deviceId: credential.deviceId,
1548
- requestedAt: credential.requestedAt
1549
- } : {}
1550
- };
1551
- if (credential && credential.nodeId !== peer.nodeId) {
1552
- inspected.machines.push({
1553
- ...known,
1554
- state: "node-changed"
1555
- });
1556
- 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.`));
1557
- continue;
1558
- }
1559
- let ask = !credential && fix;
1560
- if (credential) {
1561
- const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
1562
- if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
1563
- if (credential.state !== "approved") {
1564
- credentials = credentials.map((each) => each.machine === machine ? {
1565
- ...each,
1566
- state: "approved"
1567
- } : each);
1568
- store(credentials);
1569
- }
1570
- const host = readHostPermissions(reply.result.host);
1571
- if (host && (host.screenRecording === false || host.accessibility === false)) {
1572
- 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)"] : []];
1573
- const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
1574
- 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.`));
1575
- }
1576
- inspected.machines.push({
1577
- ...known,
1578
- state: "approved",
1579
- ...host ? { host } : {}
1580
- });
1581
- continue;
1582
- }
1583
- const pending = "error" in reply && reply.error?.code === "approval-pending";
1584
- const revoked = "error" in reply && reply.error?.code === "unauthorized";
1585
- const ticket = fix && pending && credential.state !== "approved" ? readAccessTicket() : void 0;
1586
- ask = fix && (revoked || !!ticket && credential.ticketHash !== ticket.ticketHash);
1587
- if (pending && !ask) {
1588
- inspected.machines.push({
1589
- ...known,
1590
- state: "pending"
1591
- });
1592
- inspected.findings.push(note(`Hosting machine ${machine} has not approved this Mac yet`, `Requested at ${credential.requestedAt}.`, approval(machine, credential.deviceId)));
1593
- continue;
1594
- }
1595
- if (!ask) {
1596
- inspected.machines.push({
1597
- ...known,
1598
- state: revoked ? "revoked" : "unreachable"
1599
- });
1600
- 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.`));
1601
- continue;
1602
- }
1603
- }
1604
- if (ask) {
1605
- const result = await request(machine, peer, parsed.port, deviceName, io);
1606
- if (result.credential) {
1607
- credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
1608
- store(credentials);
1609
- }
1610
- inspected.machines.push(result.report);
1611
- inspected.findings.push(result.finding);
1612
- } else {
1613
- inspected.machines.push({
1614
- ...known,
1615
- state: "not-asked"
1616
- });
1617
- 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."));
1618
- }
1619
- }
1620
- return inspected;
1621
- } finally {
1622
- releaseClaim(claim.acquired);
1623
- }
1624
- }
1625
- //#endregion
1626
- export { resolveBuildPlacement as _, chooseBuildMachine as a, pinnedEndpoint$1 as b, offloadCheck as c, remotePhaseText as d, simulatorRuntime as f, resolveBuildMachine as g, parseBuildMachineOption as h, buildPlacementCandidates as i, offloadPlacement as l, namedBuildMachine as m, inspectDeviceHostMachines as n, closeOffload as o, OffloadRefusal as p, BuildConnection as r, offloadBuild as s, configuredMachines as t, placementLoad as u, inspectBuildMachines as v, realIo as x, parseMachine$1 as y };
1665
+ export { realIo as S, configuredMachines as _, offloadBuild as a, parseMachine$1 as b, placementLoad as c, OffloadRefusal as d, namedBuildMachine as f, inspectBuildMachines as g, resolveBuildPlacement as h, closeOffload as i, remotePhaseText as l, resolveBuildMachine as m, buildPlacementCandidates as n, offloadCheck as o, parseBuildMachineOption as p, chooseBuildMachine as r, offloadPlacement as s, BuildConnection as t, simulatorRuntime as u, hostingMachines as v, pinnedEndpoint$1 as x, inspectDeviceHostMachines as y };