stim 1.17.0 → 1.17.1
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.
- package/dist/{android--GorydsK.mjs → android-B6rDWrTm.mjs} +15 -15
- package/dist/{budget-DM984mnd.mjs → budget-CUDz870h.mjs} +30 -9
- package/dist/{build-plan-CmQCoyT-.mjs → build-plan-BJSXzsCL.mjs} +14 -14
- package/dist/{idle-B2m-Ovod.mjs → build-progress-D0WkjCPd.mjs} +56 -183
- package/dist/{cli-Daxug5-K.mjs → cli-BLQqDGlX.mjs} +13 -13
- package/dist/cli.mjs +1 -1
- package/dist/{machines-C2XiNwh9.mjs → client-CxNotugx.mjs} +314 -303
- package/dist/{deps-DNWaQs4V.mjs → deps-CVxlsRFn.mjs} +1 -1
- package/dist/{device-capacity-D0GWHnTM.mjs → device-capacity-ZC4XSZ4R.mjs} +2 -2
- package/dist/device-host-worker.mjs +3 -3
- package/dist/{device-ios-Bc7vbGwl.mjs → device-ios-C2Um-XUR.mjs} +1 -1
- package/dist/{doctor-Cnk9d7Hm.mjs → doctor-BF_435OC.mjs} +14 -13
- package/dist/{gc-DxCL2A46.mjs → gc-BsWKa_-d.mjs} +9 -8
- package/dist/{gradle-D7-L2Th3.mjs → gradle-bsBRy6i9.mjs} +1 -1
- package/dist/{guide-DjrTQGik.mjs → guide-DC3n6-NH.mjs} +134 -110
- package/dist/{hosted-android-DwLS4Mse.mjs → hosted-android-DgU31wY3.mjs} +3 -3
- package/dist/{hosted-client-wk1W_CNW.mjs → hosted-client-CdSoH-G6.mjs} +4 -4
- package/dist/{hosted-ios-aMo7Cljf.mjs → hosted-ios-BTaVj4Jb.mjs} +1 -1
- package/dist/{hosted-logs-DqYmWtXc.mjs → hosted-logs-SxfBvSvc.mjs} +2 -2
- package/dist/{hosted-macos-dlHXEeF5.mjs → hosted-macos-CT9E3meL.mjs} +2 -2
- package/dist/{hosted-native-C9xJ9rpA.mjs → hosted-native-CZLOrMrN.mjs} +3 -3
- package/dist/idle-BXG__1BA.mjs +176 -0
- package/dist/{idle-shutdown-_3oOV1V_.mjs → idle-shutdown-IGyNV8Cr.mjs} +1 -1
- package/dist/{ios-DE6duQoz.mjs → ios-C7JuT1bI.mjs} +18 -18
- package/dist/{logs-DUoKlDoS.mjs → logs-DNxKcok9.mjs} +3 -3
- package/dist/{macos-32po3kUq.mjs → macos-BEs8cwKx.mjs} +66 -25
- package/dist/maintenance-run.mjs +2 -2
- package/dist/{metro-gateway-DmslUM30.mjs → metro-gateway-CHqK0p-a.mjs} +1 -1
- package/dist/{native-runtime-DhJI3IVQ.mjs → native-runtime-YhaSxP4v.mjs} +1 -1
- package/dist/offload-worker.mjs +5 -5
- package/dist/{preview-CxPRIEht.mjs → preview-C_JjkEy4.mjs} +1 -1
- package/dist/{reload-qZei6iWn.mjs → reload-byOjE3P4.mjs} +2 -2
- package/dist/settings.schema.json +84 -66
- package/dist/{start-BopZcr4n.mjs → start-BEP-XmEA.mjs} +1 -1
- package/dist/{start-uee1wWjY.mjs → start-BHIersSm.mjs} +1 -1
- package/dist/{status-dXX2JX1C.mjs → status-CqeWbIWW.mjs} +8 -8
- package/dist/{stop-BjizrQwv.mjs → stop-Cj01je8D.mjs} +3 -3
- package/dist/{stop-Cr1d8DSE.mjs → stop-D03prQnW.mjs} +1 -1
- package/dist/{stop-BCwvvs7F.mjs → stop-DOxwkH4j.mjs} +1 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/{support-BJPgYCu-.mjs → support-BE-tbOP9.mjs} +4 -4
- package/dist/{support-F9-sr0Oi.mjs → support-WvD__tSV.mjs} +2 -2
- package/dist/{toolchain-CNIBTIOv.mjs → toolchain-CdrxgUeA.mjs} +1 -1
- package/dist/{web-exQtGkoc.mjs → web-DddUdJzr.mjs} +2 -2
- package/dist/{worktree-ZpZR3S1k.mjs → worktree-AK_m-d_m.mjs} +6 -6
- package/dist/{worktree-Dc98cE6_.mjs → worktree-CsAjSmEo.mjs} +1 -1
- 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-
|
|
4
|
+
import { T as CCACHE_UNAVAILABLE, a as toolchainMismatches, l as COMPILATION_CACHE_UNAVAILABLE, s as manifestDigest } from "./toolchain-CdrxgUeA.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";
|
|
@@ -94,16 +94,280 @@ function pinnedEndpoint$1(credential, status = tailscaleStatus) {
|
|
|
94
94
|
return pinnedEndpoint(credential, parseMachine(credential.machine) ? status() : null);
|
|
95
95
|
}
|
|
96
96
|
//#endregion
|
|
97
|
+
//#region src/device-host/machines.ts
|
|
98
|
+
function note$1(title, detail, fix = null) {
|
|
99
|
+
return {
|
|
100
|
+
code: "device-host-machine",
|
|
101
|
+
level: "note",
|
|
102
|
+
title,
|
|
103
|
+
detail,
|
|
104
|
+
fix
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const approval$1 = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
|
|
108
|
+
function configuredMachines() {
|
|
109
|
+
const remote = loadConfig$1()?.remote;
|
|
110
|
+
if (remote !== void 0 && !isJsonObject(remote)) return null;
|
|
111
|
+
const machines = remote?.machines;
|
|
112
|
+
if (machines === void 0) return [];
|
|
113
|
+
return acceptsShape(settingDefinition("remote.machines"), machines) ? machines : null;
|
|
114
|
+
}
|
|
115
|
+
/** The `remote.machines` entries this Mac asked for device-host access, in setting order; null when the setting is invalid. */
|
|
116
|
+
function hostingMachines() {
|
|
117
|
+
const configured = configuredMachines();
|
|
118
|
+
if (configured === null) return null;
|
|
119
|
+
try {
|
|
120
|
+
const asked = readDeviceHostMachines();
|
|
121
|
+
return configured.filter((machine) => asked.some((each) => each.machine === machine));
|
|
122
|
+
} catch {
|
|
123
|
+
return configured;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function store(machines) {
|
|
127
|
+
withDirLock(deviceHostMachinesLock(), () => {
|
|
128
|
+
const file = deviceHostMachinesFile();
|
|
129
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(tmp, `${JSON.stringify({
|
|
132
|
+
version: 1,
|
|
133
|
+
machines
|
|
134
|
+
}, null, 2)}\n`, { mode: 384 });
|
|
135
|
+
renameSync(tmp, file);
|
|
136
|
+
} finally {
|
|
137
|
+
rmSync(tmp, { force: true });
|
|
138
|
+
}
|
|
139
|
+
}, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
|
|
140
|
+
}
|
|
141
|
+
async function request(machine, peer, port, deviceName, io) {
|
|
142
|
+
const ticket = readAccessTicket();
|
|
143
|
+
const reply = await io.hello(endpoint(peer, port), {
|
|
144
|
+
request: "device-host",
|
|
145
|
+
deviceName,
|
|
146
|
+
...ticket ? { setupTicket: ticket.ticket } : {}
|
|
147
|
+
});
|
|
148
|
+
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 {
|
|
149
|
+
report: {
|
|
150
|
+
machine,
|
|
151
|
+
dnsName: peer.dnsName,
|
|
152
|
+
state: "unreachable"
|
|
153
|
+
},
|
|
154
|
+
finding: note$1(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
|
|
155
|
+
};
|
|
156
|
+
const credential = {
|
|
157
|
+
machine,
|
|
158
|
+
nodeId: peer.nodeId,
|
|
159
|
+
dnsName: peer.dnsName,
|
|
160
|
+
deviceId: reply.result.device.id,
|
|
161
|
+
deviceToken: reply.result.deviceToken,
|
|
162
|
+
state: "pending",
|
|
163
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
164
|
+
...ticket ? { ticketHash: ticket.ticketHash } : {}
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
report: {
|
|
168
|
+
machine,
|
|
169
|
+
dnsName: peer.dnsName,
|
|
170
|
+
state: "pending",
|
|
171
|
+
deviceId: credential.deviceId,
|
|
172
|
+
requestedAt: credential.requestedAt
|
|
173
|
+
},
|
|
174
|
+
finding: note$1(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval$1(machine, credential.deviceId)),
|
|
175
|
+
credential
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** Reports the device-host approval of each remote Mac, separate from its build approval. Only --fix asks or forgets credentials; every token stays pinned. */
|
|
179
|
+
async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
|
|
180
|
+
if (entries === null) return {
|
|
181
|
+
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.")],
|
|
182
|
+
machines: []
|
|
183
|
+
};
|
|
184
|
+
const unavailable = (state, finding) => ({
|
|
185
|
+
findings: [finding],
|
|
186
|
+
machines: entries.map((machine) => ({
|
|
187
|
+
machine,
|
|
188
|
+
state
|
|
189
|
+
}))
|
|
190
|
+
});
|
|
191
|
+
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."));
|
|
192
|
+
if (!entries.length) {
|
|
193
|
+
if (!fix) return {
|
|
194
|
+
findings: [],
|
|
195
|
+
machines: []
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
if (!readDeviceHostMachines().length) return {
|
|
199
|
+
findings: [],
|
|
200
|
+
machines: []
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return unreadable();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const claim = tryAcquireClaim({
|
|
207
|
+
root: deviceHostMachinesClaims(),
|
|
208
|
+
mode: "exclusive",
|
|
209
|
+
label: "device-host approval"
|
|
210
|
+
});
|
|
211
|
+
if (!claim.acquired) {
|
|
212
|
+
releaseClaim(claim.pending);
|
|
213
|
+
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."));
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
let credentials;
|
|
217
|
+
try {
|
|
218
|
+
credentials = readDeviceHostMachines();
|
|
219
|
+
} catch {
|
|
220
|
+
return unreadable();
|
|
221
|
+
}
|
|
222
|
+
if (fix) {
|
|
223
|
+
const kept = credentials.filter((each) => entries.includes(each.machine));
|
|
224
|
+
if (kept.length !== credentials.length) {
|
|
225
|
+
store(kept);
|
|
226
|
+
credentials = kept;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!entries.length) return {
|
|
230
|
+
findings: [],
|
|
231
|
+
machines: []
|
|
232
|
+
};
|
|
233
|
+
const status = io.status();
|
|
234
|
+
if (!isJsonObject(status)) return unavailable("tailscale-off", note$1("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running."));
|
|
235
|
+
const self = isJsonObject(status.Self) ? status.Self : {};
|
|
236
|
+
const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
|
|
237
|
+
const inspected = {
|
|
238
|
+
findings: [],
|
|
239
|
+
machines: []
|
|
240
|
+
};
|
|
241
|
+
const namedNodes = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const machine of new Set(entries)) {
|
|
243
|
+
const parsed = parseMachine$1(machine);
|
|
244
|
+
if (!parsed) {
|
|
245
|
+
inspected.machines.push({
|
|
246
|
+
machine,
|
|
247
|
+
state: "invalid"
|
|
248
|
+
});
|
|
249
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const current = io.status();
|
|
253
|
+
if (!isJsonObject(current)) {
|
|
254
|
+
inspected.machines.push({
|
|
255
|
+
machine,
|
|
256
|
+
state: "tailscale-off"
|
|
257
|
+
});
|
|
258
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} is unreachable`, "Tailscale stopped before this connection."));
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const peer = findPeer$1(current, parsed.name);
|
|
262
|
+
if (peer === "missing" || peer === "ambiguous") {
|
|
263
|
+
inspected.machines.push({
|
|
264
|
+
machine,
|
|
265
|
+
state: "not-on-tailnet"
|
|
266
|
+
});
|
|
267
|
+
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."));
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const nodeRoute = `${peer.nodeId}:${parsed.port}`;
|
|
271
|
+
if (namedNodes.has(nodeRoute)) {
|
|
272
|
+
inspected.machines.push({
|
|
273
|
+
machine,
|
|
274
|
+
dnsName: peer.dnsName,
|
|
275
|
+
state: "invalid"
|
|
276
|
+
});
|
|
277
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} repeats a named node`, "Name each tailnet node and serve port only once in remote.machines."));
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
namedNodes.add(nodeRoute);
|
|
281
|
+
const credential = credentials.find((each) => each.machine === machine);
|
|
282
|
+
const known = {
|
|
283
|
+
machine,
|
|
284
|
+
dnsName: peer.dnsName,
|
|
285
|
+
...credential ? {
|
|
286
|
+
deviceId: credential.deviceId,
|
|
287
|
+
requestedAt: credential.requestedAt
|
|
288
|
+
} : {}
|
|
289
|
+
};
|
|
290
|
+
if (credential && credential.nodeId !== peer.nodeId) {
|
|
291
|
+
inspected.machines.push({
|
|
292
|
+
...known,
|
|
293
|
+
state: "node-changed"
|
|
294
|
+
});
|
|
295
|
+
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.`));
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
let ask = !credential && fix;
|
|
299
|
+
if (credential) {
|
|
300
|
+
const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
|
|
301
|
+
if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
|
|
302
|
+
if (credential.state !== "approved") {
|
|
303
|
+
credentials = credentials.map((each) => each.machine === machine ? {
|
|
304
|
+
...each,
|
|
305
|
+
state: "approved"
|
|
306
|
+
} : each);
|
|
307
|
+
store(credentials);
|
|
308
|
+
}
|
|
309
|
+
const host = readHostPermissions(reply.result.host);
|
|
310
|
+
if (host && (host.screenRecording === false || host.accessibility === false)) {
|
|
311
|
+
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)"] : []];
|
|
312
|
+
const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
|
|
313
|
+
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.`));
|
|
314
|
+
}
|
|
315
|
+
inspected.machines.push({
|
|
316
|
+
...known,
|
|
317
|
+
state: "approved",
|
|
318
|
+
...host ? { host } : {}
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const pending = "error" in reply && reply.error?.code === "approval-pending";
|
|
323
|
+
const revoked = "error" in reply && reply.error?.code === "unauthorized";
|
|
324
|
+
const ticket = fix && pending && credential.state !== "approved" ? readAccessTicket() : void 0;
|
|
325
|
+
ask = fix && (revoked || !!ticket && credential.ticketHash !== ticket.ticketHash);
|
|
326
|
+
if (pending && !ask) {
|
|
327
|
+
inspected.machines.push({
|
|
328
|
+
...known,
|
|
329
|
+
state: "pending"
|
|
330
|
+
});
|
|
331
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting yet`, `Requested at ${credential.requestedAt}.`, approval$1(machine, credential.deviceId)));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!ask) {
|
|
335
|
+
inspected.machines.push({
|
|
336
|
+
...known,
|
|
337
|
+
state: revoked ? "revoked" : "unreachable"
|
|
338
|
+
});
|
|
339
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} ${revoked ? "no longer accepts this Mac for device hosting" : "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.`));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (ask) {
|
|
344
|
+
const result = await request(machine, peer, parsed.port, deviceName, io);
|
|
345
|
+
if (result.credential) {
|
|
346
|
+
credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
|
|
347
|
+
store(credentials);
|
|
348
|
+
}
|
|
349
|
+
inspected.machines.push(result.report);
|
|
350
|
+
inspected.findings.push(result.finding);
|
|
351
|
+
} else {
|
|
352
|
+
inspected.machines.push({
|
|
353
|
+
...known,
|
|
354
|
+
state: "not-asked"
|
|
355
|
+
});
|
|
356
|
+
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."));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return inspected;
|
|
360
|
+
} finally {
|
|
361
|
+
releaseClaim(claim.acquired);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
97
365
|
//#region src/offload/build-machines.ts
|
|
98
|
-
/** The
|
|
99
|
-
function pairedMachines(entries = configuredMachines
|
|
366
|
+
/** The `remote.machines` entries that this Mac asked for build access, in that order. */
|
|
367
|
+
function pairedMachines(entries = configuredMachines() ?? []) {
|
|
100
368
|
const credentials = readBuildMachines();
|
|
101
369
|
return entries.flatMap((entry) => credentials.filter((each) => each.machine === entry));
|
|
102
370
|
}
|
|
103
|
-
function configuredMachines$1() {
|
|
104
|
-
const machines = loadConfig$1()?.offload?.machines;
|
|
105
|
-
return Array.isArray(machines) ? machines.filter((entry) => typeof entry === "string") : [];
|
|
106
|
-
}
|
|
107
371
|
function updateCredentials(change) {
|
|
108
372
|
const dir = getConfigDir();
|
|
109
373
|
withDirLock(buildMachinesLock(), () => {
|
|
@@ -121,7 +385,7 @@ function updateCredentials(change) {
|
|
|
121
385
|
}
|
|
122
386
|
}, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
|
|
123
387
|
}
|
|
124
|
-
function note
|
|
388
|
+
function note(title, detail, fix = null) {
|
|
125
389
|
return {
|
|
126
390
|
code: "build-machine",
|
|
127
391
|
level: "note",
|
|
@@ -130,7 +394,7 @@ function note$1(title, detail, fix = null) {
|
|
|
130
394
|
fix
|
|
131
395
|
};
|
|
132
396
|
}
|
|
133
|
-
const approval
|
|
397
|
+
const approval = (entry, deviceId) => `A person on ${entry} approves it with \`stim-server devices grant ${deviceId} --build\`.`;
|
|
134
398
|
const PROBLEM_TITLES = {
|
|
135
399
|
unreachable: "does not answer a build offer",
|
|
136
400
|
checkout: "cannot build an app outside a git checkout",
|
|
@@ -155,7 +419,7 @@ function problemFix(code, entry) {
|
|
|
155
419
|
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
420
|
case "checkout": return "Offload syncs the files git lists; run Stim from a git checkout of the app.";
|
|
157
421
|
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
|
|
422
|
+
case "arch": return "Use a remote Mac with the same CPU architecture as this Mac.";
|
|
159
423
|
case "xcode":
|
|
160
424
|
case "simulator-sdk":
|
|
161
425
|
case "macos-sdk": return `Install and select the same Xcode on ${entry} and this Mac (\`xcode-select -p\` on each).`;
|
|
@@ -167,8 +431,8 @@ function problemFix(code, entry) {
|
|
|
167
431
|
case "ndk":
|
|
168
432
|
case "build-tools":
|
|
169
433
|
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 (
|
|
171
|
-
case "busy": return `Builds stay on this Mac until ${entry} has capacity. On ${entry},
|
|
434
|
+
case "disk": return `Free space on ${entry}'s worker root (server.workerRoot there).`;
|
|
435
|
+
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
436
|
}
|
|
173
437
|
}
|
|
174
438
|
const TRANSIENT = /* @__PURE__ */ new Set(["unreachable", "busy"]);
|
|
@@ -176,7 +440,7 @@ function offloadFindings(entry, problems) {
|
|
|
176
440
|
return problems.map((problem) => ({
|
|
177
441
|
code: `build-machine-${problem.code}`,
|
|
178
442
|
level: TRANSIENT.has(problem.code) ? "note" : "cost",
|
|
179
|
-
title: `
|
|
443
|
+
title: `Remote Mac ${entry} ${PROBLEM_TITLES[problem.code]}`,
|
|
180
444
|
detail: `Builds fall back to this Mac: ${problem.reason}.`,
|
|
181
445
|
fix: problemFix(problem.code, entry)
|
|
182
446
|
}));
|
|
@@ -196,7 +460,7 @@ async function requestAccess(entry, peer, port, deviceName, io) {
|
|
|
196
460
|
state: "unreachable",
|
|
197
461
|
dnsName: peer.dnsName
|
|
198
462
|
},
|
|
199
|
-
finding: note
|
|
463
|
+
finding: note(`Could not ask ${entry} for build access`, reason)
|
|
200
464
|
};
|
|
201
465
|
}
|
|
202
466
|
const credential = {
|
|
@@ -218,7 +482,7 @@ async function requestAccess(entry, peer, port, deviceName, io) {
|
|
|
218
482
|
deviceId: credential.deviceId,
|
|
219
483
|
requestedAt: credential.requestedAt
|
|
220
484
|
},
|
|
221
|
-
finding: note
|
|
485
|
+
finding: note(`Asked ${entry} for build access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(entry, credential.deviceId))
|
|
222
486
|
};
|
|
223
487
|
}
|
|
224
488
|
async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
@@ -228,7 +492,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
228
492
|
machine: entry,
|
|
229
493
|
state: "invalid"
|
|
230
494
|
},
|
|
231
|
-
finding: note
|
|
495
|
+
finding: note(`Remote Mac ${entry} is not a tailnet name`, "Expected `name` or `name:port`.")
|
|
232
496
|
};
|
|
233
497
|
const peer = findPeer$1(status, parsed.name);
|
|
234
498
|
if (peer === "missing" || peer === "ambiguous") {
|
|
@@ -238,7 +502,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
238
502
|
machine: entry,
|
|
239
503
|
state: "not-on-tailnet"
|
|
240
504
|
},
|
|
241
|
-
finding: note
|
|
505
|
+
finding: note(`Remote Mac ${entry} is not on this tailnet`, detail)
|
|
242
506
|
};
|
|
243
507
|
}
|
|
244
508
|
const credential = readBuildMachines().find((each) => each.machine === entry);
|
|
@@ -253,7 +517,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
253
517
|
deviceId: credential.deviceId,
|
|
254
518
|
requestedAt: credential.requestedAt
|
|
255
519
|
},
|
|
256
|
-
finding: note
|
|
520
|
+
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
521
|
};
|
|
258
522
|
if (!credential) {
|
|
259
523
|
if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
|
|
@@ -262,7 +526,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
262
526
|
...known,
|
|
263
527
|
state: "not-asked"
|
|
264
528
|
},
|
|
265
|
-
finding: note
|
|
529
|
+
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
530
|
};
|
|
267
531
|
}
|
|
268
532
|
const paired = {
|
|
@@ -312,7 +576,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
312
576
|
...paired,
|
|
313
577
|
state: "pending"
|
|
314
578
|
},
|
|
315
|
-
finding: note
|
|
579
|
+
finding: note(`Remote Mac ${entry} has not approved this Mac for builds yet`, `Requested at ${credential.requestedAt}.`, approval(entry, credential.deviceId))
|
|
316
580
|
};
|
|
317
581
|
}
|
|
318
582
|
if ("error" in reply && reply.error.code === "unauthorized") {
|
|
@@ -322,7 +586,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
322
586
|
...paired,
|
|
323
587
|
state: "revoked"
|
|
324
588
|
},
|
|
325
|
-
finding: note
|
|
589
|
+
finding: note(`Remote Mac ${entry} no longer accepts this Mac for builds`, `${reply.error.message} It was revoked, or the request lapsed before approval.`, "Run `stim doctor --fix` to ask again.")
|
|
326
590
|
};
|
|
327
591
|
}
|
|
328
592
|
const reason = "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "unexpected reply";
|
|
@@ -331,17 +595,21 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
331
595
|
...paired,
|
|
332
596
|
state: "unreachable"
|
|
333
597
|
},
|
|
334
|
-
finding: note
|
|
598
|
+
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
599
|
};
|
|
336
600
|
}
|
|
337
601
|
/**
|
|
338
|
-
* Doctor findings and a report for each `
|
|
602
|
+
* Doctor findings and a report for each `remote.machines` entry. The worker's node is pinned when access is
|
|
339
603
|
* requested; a later connection goes only to the current MagicDNS name of that same node, checked before the token
|
|
340
604
|
* is sent. With `fix`, requests access from a named machine this Mac holds no pairing for, requests again when the
|
|
341
605
|
* 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
606
|
* machine is asked for one build offer and reports every reason it would not take this project's build.
|
|
343
607
|
*/
|
|
344
|
-
async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines
|
|
608
|
+
async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines()) {
|
|
609
|
+
if (entries === null) return {
|
|
610
|
+
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.")],
|
|
611
|
+
machines: []
|
|
612
|
+
};
|
|
345
613
|
const unnamed = (each) => !entries.includes(each.machine);
|
|
346
614
|
if (fix && readBuildMachines().some(unnamed)) updateCredentials((credentials) => credentials.filter((each) => !unnamed(each)));
|
|
347
615
|
if (entries.length === 0) return {
|
|
@@ -350,7 +618,7 @@ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries
|
|
|
350
618
|
};
|
|
351
619
|
const status = io.status();
|
|
352
620
|
if (!isJsonObject(status)) return {
|
|
353
|
-
findings: [note
|
|
621
|
+
findings: [note("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running.")],
|
|
354
622
|
machines: entries.map((machine) => ({
|
|
355
623
|
machine,
|
|
356
624
|
state: "tailscale-off"
|
|
@@ -374,14 +642,14 @@ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries
|
|
|
374
642
|
//#region src/offload/selection.ts
|
|
375
643
|
var OffloadRefusal = class extends Error {
|
|
376
644
|
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
|
|
645
|
+
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
646
|
constructor(machine, reason) {
|
|
379
647
|
super(reason.startsWith(`${machine}: `) ? reason : `${machine}: ${reason}`);
|
|
380
648
|
}
|
|
381
649
|
};
|
|
382
650
|
function resolveBuildMachine(flag, env, setting) {
|
|
383
651
|
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
|
|
652
|
+
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
653
|
const trimmed = selected.trim();
|
|
386
654
|
const reserved = trimmed.toLowerCase();
|
|
387
655
|
return reserved === "auto" || reserved === "local" ? reserved : trimmed;
|
|
@@ -404,16 +672,16 @@ function requireConfiguredMachine(selected, machines) {
|
|
|
404
672
|
const candidate = parseMachine(entry);
|
|
405
673
|
return candidate?.name === parsed.name && candidate.port === parsed.port;
|
|
406
674
|
});
|
|
407
|
-
if (!match) throw new OffloadRefusal(selected, `not listed in
|
|
675
|
+
if (!match) throw new OffloadRefusal(selected, `not listed in remote.machines (configured: ${entries.join(", ") || "none"})`);
|
|
408
676
|
return match;
|
|
409
677
|
}
|
|
410
678
|
function resolveBuildPlacement(flag) {
|
|
411
|
-
const
|
|
679
|
+
const remote = loadConfig$1()?.remote;
|
|
412
680
|
let selected = "auto";
|
|
413
681
|
try {
|
|
414
|
-
selected = resolveBuildMachine(flag, process.env.
|
|
415
|
-
selected = requireConfiguredMachine(selected,
|
|
416
|
-
if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no
|
|
682
|
+
selected = resolveBuildMachine(flag, process.env.STIM_REMOTE_BUILD, remote?.build);
|
|
683
|
+
selected = requireConfiguredMachine(selected, remote?.machines);
|
|
684
|
+
if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
|
|
417
685
|
return { selected };
|
|
418
686
|
} catch (error) {
|
|
419
687
|
return {
|
|
@@ -421,7 +689,7 @@ function resolveBuildPlacement(flag) {
|
|
|
421
689
|
failure: {
|
|
422
690
|
code: error.code,
|
|
423
691
|
message: error.message,
|
|
424
|
-
remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --build
|
|
692
|
+
remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --remote-build auto, local, or an entry from stim settings get remote.machines."
|
|
425
693
|
}
|
|
426
694
|
};
|
|
427
695
|
}
|
|
@@ -450,7 +718,7 @@ const CHUNK_BYTES = 61440;
|
|
|
450
718
|
const MAX_BUFFERED = 8388608;
|
|
451
719
|
const DIGEST_BYTES = 32;
|
|
452
720
|
function offloadMode(env = process.env) {
|
|
453
|
-
const raw = env.
|
|
721
|
+
const raw = env.STIM_REMOTE_BUILD_MODE || loadConfig$1()?.remote?.buildMode;
|
|
454
722
|
return OFFLOAD_MODES.includes(raw) ? raw : "auto";
|
|
455
723
|
}
|
|
456
724
|
function buildPlacementCandidates(selected) {
|
|
@@ -478,30 +746,30 @@ function placementLoad(choice) {
|
|
|
478
746
|
})} there`;
|
|
479
747
|
}
|
|
480
748
|
/**
|
|
481
|
-
* Whether a build may go to a
|
|
482
|
-
* this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `
|
|
749
|
+
* Whether a build may go to a remote Mac at all, before any machine is asked. `auto` keeps the build here while
|
|
750
|
+
* this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `server.maxLoadPerCore`.
|
|
483
751
|
* A named selection ignores local capacity and mode, and refuses an unsupported build or missing pairing.
|
|
484
752
|
*/
|
|
485
753
|
function offloadPlacement({ mode, machines, here, unsupported, selected = "auto" }) {
|
|
486
754
|
if (namedBuildMachine(selected)) {
|
|
487
|
-
if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named
|
|
488
|
-
if (machines === 0) throw new OffloadRefusal(selected, "no
|
|
755
|
+
if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named remote Mac cannot take it`);
|
|
756
|
+
if (machines === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
|
|
489
757
|
return {
|
|
490
758
|
offload: true,
|
|
491
|
-
reason: `selected with --build
|
|
759
|
+
reason: `selected with --remote-build ${selected}`
|
|
492
760
|
};
|
|
493
761
|
}
|
|
494
762
|
if (selected === "local") return {
|
|
495
763
|
offload: false,
|
|
496
|
-
reason: "--build
|
|
764
|
+
reason: "--remote-build local"
|
|
497
765
|
};
|
|
498
766
|
if (mode === "off") return {
|
|
499
767
|
offload: false,
|
|
500
|
-
reason: "
|
|
768
|
+
reason: "remote.buildMode is off"
|
|
501
769
|
};
|
|
502
770
|
if (machines === 0) return {
|
|
503
771
|
offload: false,
|
|
504
|
-
reason: "no
|
|
772
|
+
reason: "no remote Mac is paired"
|
|
505
773
|
};
|
|
506
774
|
if (unsupported) return {
|
|
507
775
|
offload: false,
|
|
@@ -509,7 +777,7 @@ function offloadPlacement({ mode, machines, here, unsupported, selected = "auto"
|
|
|
509
777
|
};
|
|
510
778
|
if (mode === "force") return {
|
|
511
779
|
offload: true,
|
|
512
|
-
reason: "
|
|
780
|
+
reason: "remote.buildMode is force"
|
|
513
781
|
};
|
|
514
782
|
const busy = saturation(here);
|
|
515
783
|
return busy ? {
|
|
@@ -797,7 +1065,7 @@ var BuildConnection = class BuildConnection {
|
|
|
797
1065
|
function replyError(reply) {
|
|
798
1066
|
return "error" in reply ? `${reply.error.code}: ${reply.error.message}` : null;
|
|
799
1067
|
}
|
|
800
|
-
/** The repository on the
|
|
1068
|
+
/** The repository on the remote Mac: one area per git common dir, shared by all its worktrees. */
|
|
801
1069
|
function repoIdentity(projectRoot) {
|
|
802
1070
|
const run = getExecutor().runFile;
|
|
803
1071
|
const repoRoot = realpathSync(run("git", [
|
|
@@ -937,7 +1205,7 @@ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machi
|
|
|
937
1205
|
asked.forEach((each, at) => {
|
|
938
1206
|
if (!order.includes(at) && "connection" in each) each.connection.close();
|
|
939
1207
|
});
|
|
940
|
-
if (order.length === 0) return reasons.length ? reasons.join("; ") : "no
|
|
1208
|
+
if (order.length === 0) return reasons.length ? reasons.join("; ") : "no remote Mac is paired";
|
|
941
1209
|
for (const reason of reasons) note(reason);
|
|
942
1210
|
const [first, ...rest] = order.map((at) => {
|
|
943
1211
|
const pick = asked[at];
|
|
@@ -1366,261 +1634,4 @@ function offloadCheck(projectRoot, targets) {
|
|
|
1366
1634
|
};
|
|
1367
1635
|
}
|
|
1368
1636
|
//#endregion
|
|
1369
|
-
|
|
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 };
|
|
1637
|
+
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 };
|