@stim-cli/core 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/state.mjs CHANGED
@@ -1,12 +1,312 @@
1
1
  import { _ as releaseClaim, f as isClaimRefusal, g as readClaimSet, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
2
2
  import { inspectProcessIdentity } from "./process-identity.mjs";
3
- import { C as workspaceStateDir, h as rotatedLogPath, j as withDirLock, l as configDir, x as workspaceName } from "./core-BwaxUjLX.mjs";
4
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
3
+ import { C as workspaceStateDir, h as rotatedLogPath, j as withDirLock, l as configDir, x as workspaceName } from "./core-DWOUn9xp.mjs";
4
+ import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
5
5
  import { availableParallelism, homedir, loadavg } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
7
7
  import { createHash } from "node:crypto";
8
+ import { isIP } from "node:net";
8
9
  import { AsyncLocalStorage } from "node:async_hooks";
9
10
  import { StringDecoder } from "node:string_decoder";
11
+ //#region state/agent-actions.ts
12
+ const EVENTS_VERSION = 1;
13
+ function object$1(value) {
14
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
15
+ }
16
+ function parseAgentEvents(lines) {
17
+ const events = [];
18
+ const started = [];
19
+ const finished = [];
20
+ let session = null;
21
+ let unknownVersion = null;
22
+ for (const line of lines) {
23
+ let entry;
24
+ try {
25
+ entry = object$1(JSON.parse(line));
26
+ } catch {
27
+ continue;
28
+ }
29
+ if (!entry) continue;
30
+ const ts = typeof entry.ts === "string" ? Date.parse(entry.ts) : NaN;
31
+ if (entry.version !== EVENTS_VERSION) {
32
+ unknownVersion ??= {
33
+ version: entry.version,
34
+ ts: Number.isFinite(ts) ? ts : null
35
+ };
36
+ continue;
37
+ }
38
+ if (typeof entry.session === "string") session ??= entry.session;
39
+ const { kind, command, summary } = entry;
40
+ const requestId = typeof entry.requestId === "string" ? entry.requestId : null;
41
+ if (kind === "request.started" && requestId && Number.isFinite(ts)) started.push([requestId, ts]);
42
+ const scopeChange = kind === "request.started" && (command === "open" || command === "close");
43
+ if (kind !== "action.recorded" && kind !== "request.finished" && !scopeChange) continue;
44
+ const failed = kind === "request.finished" && entry.status === "error";
45
+ if (kind === "request.finished" && !failed) {
46
+ if (requestId) finished.push(requestId);
47
+ continue;
48
+ }
49
+ if (!Number.isFinite(ts) || typeof entry.session !== "string" || typeof command !== "string") continue;
50
+ if (typeof summary !== "string" && !scopeChange) continue;
51
+ events.push({
52
+ ts,
53
+ session: entry.session,
54
+ kind,
55
+ command,
56
+ summary: typeof summary === "string" ? summary : "",
57
+ failed,
58
+ details: object$1(entry.details),
59
+ requestId
60
+ });
61
+ }
62
+ return {
63
+ events,
64
+ started,
65
+ finished,
66
+ session,
67
+ unknownVersion
68
+ };
69
+ }
70
+ const RUNNER_DEVICE = /-destination\s+"?platform=iOS Simulator,id=([^",\s]+)|AgentDeviceRunner\.env\.session-(.+?)-owner-/;
71
+ const LOG_TIME = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?([+-]\d{2})?(\d{2})?\b/;
72
+ function logTime(line) {
73
+ const match = LOG_TIME.exec(line);
74
+ if (!match) return null;
75
+ const [, date, time, fraction = "", offsetHours, offsetMinutes] = match;
76
+ const offset = offsetHours && offsetMinutes ? `${offsetHours}:${offsetMinutes}` : "";
77
+ const at = Date.parse(`${date}T${time}${fraction.slice(0, 4)}${offset}`);
78
+ return Number.isFinite(at) ? at : null;
79
+ }
80
+ function parseRunnerSpans(text) {
81
+ const spans = [];
82
+ let pending = null;
83
+ for (const line of text.split("\n")) {
84
+ const device = RUNNER_DEVICE.exec(line);
85
+ if (device) {
86
+ pending = device[1] ?? device[2] ?? null;
87
+ continue;
88
+ }
89
+ if (pending === null) continue;
90
+ const at = logTime(line);
91
+ if (at === null) continue;
92
+ if (spans.at(-1)?.deviceId !== pending) spans.push({
93
+ deviceId: pending,
94
+ from: spans.length ? at : -Infinity
95
+ });
96
+ pending = null;
97
+ }
98
+ return spans;
99
+ }
100
+ function sessionSpans(runner, closes) {
101
+ return runner.map((span, i) => {
102
+ const previous = runner[i - 1]?.from ?? -Infinity;
103
+ const close = closes.findLast((at) => at > previous && at < span.from);
104
+ return close === void 0 ? span : {
105
+ ...span,
106
+ from: close + 1
107
+ };
108
+ });
109
+ }
110
+ function spanDevice(spans, ts) {
111
+ let device = null;
112
+ for (const span of spans) if (span.from <= ts) device = span.deviceId;
113
+ return device;
114
+ }
115
+ function platformOf(event) {
116
+ const platform = event.details?.platform;
117
+ return platform === "ios" || platform === "android" ? platform : void 0;
118
+ }
119
+ function agentRecord(event, deviceId, target, startedAt) {
120
+ return {
121
+ ts: event.ts,
122
+ ...startedAt !== void 0 && startedAt <= event.ts ? { startedAt } : {},
123
+ src: "agent",
124
+ level: event.failed ? "error" : "info",
125
+ msg: event.summary,
126
+ event: event.failed ? "agent_failed" : "agent_action",
127
+ command: event.command,
128
+ session: event.session,
129
+ ...target.platform ? { platform: target.platform } : {},
130
+ ...deviceId ? { deviceId } : {},
131
+ ...target.slot === "default" ? {} : { slot: target.slot },
132
+ ...event.details ? { details: event.details } : {}
133
+ };
134
+ }
135
+ function formatUnknownRecord(session, unknown, target, now) {
136
+ return {
137
+ ts: unknown.ts ?? now,
138
+ src: "agent",
139
+ level: "warn",
140
+ msg: `agent-device session ${session} records events in an unrecognized format (version ${JSON.stringify(unknown.version) ?? "missing"}); its actions are not shown.`,
141
+ event: "agent_format_unknown",
142
+ session,
143
+ ...target.platform ? { platform: target.platform } : {},
144
+ ...target.slot === "default" ? {} : { slot: target.slot }
145
+ };
146
+ }
147
+ function readCompleteLines(path, start) {
148
+ let fd;
149
+ try {
150
+ fd = openSync(path, "r");
151
+ const stat = fstatSync(fd);
152
+ const size = stat.size;
153
+ const identity = `${stat.dev}:${stat.ino}`;
154
+ if (size <= start) return {
155
+ text: "",
156
+ next: start,
157
+ size,
158
+ identity
159
+ };
160
+ const buffer = Buffer.alloc(size - start);
161
+ const read = readSync(fd, buffer, 0, buffer.length, start);
162
+ const end = buffer.subarray(0, read).lastIndexOf(10);
163
+ return {
164
+ text: end < 0 ? "" : buffer.toString("utf8", 0, end + 1),
165
+ next: start + end + 1,
166
+ size,
167
+ identity
168
+ };
169
+ } catch {
170
+ return null;
171
+ } finally {
172
+ if (fd !== void 0) closeSync(fd);
173
+ }
174
+ }
175
+ function trackMacosOpen(cursor, event, unknownVersion) {
176
+ const flags = object$1(event.details?.flags);
177
+ if (!event.failed) cursor.macos = event.details?.platform === "macos" || flags?.platform === "macos";
178
+ cursor.bundleId = cursor.macos && !event.failed && !unknownVersion && flags?.surface === "app" && typeof event.details?.appBundleId === "string" ? event.details.appBundleId : null;
179
+ }
180
+ /**
181
+ * Reads agent-device session actions incrementally from the supplied sessions directories. With targets,
182
+ * records are restricted to those devices; without targets, retained session metadata supplies attribution.
183
+ * Missing directories and unreadable files yield no records.
184
+ */
185
+ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
186
+ const cursors = /* @__PURE__ */ new Map();
187
+ return () => {
188
+ const targets = typeof resolveTargets === "function" ? resolveTargets() : resolveTargets;
189
+ if (targets && !targets.length) return [];
190
+ const byId = new Map(targets?.map((target) => [target.id, target]));
191
+ const macosByBundle = new Map(targets?.flatMap((target) => target.platform === "macos" ? [[target.bundleId, target]] : []));
192
+ const sessionDirs = (typeof sessionsDirs === "function" ? sessionsDirs() : sessionsDirs).flatMap((sessionsDir) => {
193
+ try {
194
+ return readdirSync(sessionsDir).map((name) => ({
195
+ name,
196
+ sessionsDir,
197
+ dir: join(sessionsDir, name)
198
+ }));
199
+ } catch {
200
+ return [];
201
+ }
202
+ });
203
+ const out = [];
204
+ for (const { name, sessionsDir, dir } of sessionDirs) {
205
+ const eventsPath = join(dir, "events.ndjson");
206
+ const lines = [];
207
+ let cursor = cursors.get(dir);
208
+ if (!cursor) {
209
+ try {
210
+ if (statSync(eventsPath).mtimeMs < sinceTs) continue;
211
+ } catch {
212
+ continue;
213
+ }
214
+ cursor = {
215
+ offset: 0,
216
+ closes: [],
217
+ runner: null,
218
+ session: null,
219
+ unknownReported: false,
220
+ started: /* @__PURE__ */ new Map(),
221
+ macos: false,
222
+ bundleId: null,
223
+ closing: null
224
+ };
225
+ cursors.set(dir, cursor);
226
+ try {
227
+ lines.push(...readFileSync(`${eventsPath}.1`, "utf8").split("\n"));
228
+ } catch {}
229
+ }
230
+ let chunk = readCompleteLines(eventsPath, cursor.offset);
231
+ if (chunk && (chunk.size < cursor.offset || cursor.identity && chunk.identity !== cursor.identity)) {
232
+ cursor.bundleId = null;
233
+ chunk = readCompleteLines(eventsPath, 0);
234
+ }
235
+ if (!chunk) continue;
236
+ cursor.identity = chunk.identity;
237
+ cursor.offset = chunk.next;
238
+ lines.push(...chunk.text.split("\n"));
239
+ const parsed = parseAgentEvents(lines);
240
+ for (const [requestId, at] of parsed.started) cursor.started.set(requestId, at);
241
+ cursor.session ??= parsed.session;
242
+ const session = cursor.session;
243
+ const matched = new Map(parsed.events.flatMap((event) => event.requestId && cursor.started.has(event.requestId) ? [[event.requestId, cursor.started.get(event.requestId)]] : []));
244
+ const failed = parsed.events.flatMap((event) => event.failed && event.requestId ? [event.requestId] : []);
245
+ for (const requestId of [...parsed.finished, ...failed]) cursor.started.delete(requestId);
246
+ for (const requestId of [...cursor.started.keys()].slice(0, -64)) cursor.started.delete(requestId);
247
+ if (!parsed.events.length && !parsed.unknownVersion) continue;
248
+ const runnerPath = join(dir, "runner.log");
249
+ let runnerSize = -1;
250
+ try {
251
+ runnerSize = statSync(runnerPath).size;
252
+ } catch {}
253
+ if (cursor.runner?.size !== runnerSize) {
254
+ let text = "";
255
+ try {
256
+ text = readFileSync(runnerPath, "utf8");
257
+ } catch {}
258
+ cursor.runner = {
259
+ size: runnerSize,
260
+ spans: parseRunnerSpans(text)
261
+ };
262
+ }
263
+ for (const event of parsed.events) if (event.command === "close" && event.kind === "action.recorded") cursor.closes.push(event.ts);
264
+ const spans = sessionSpans(cursor.runner.spans, cursor.closes);
265
+ const deviceAt = (ts) => spans.length ? spanDevice(spans, ts) : session && claimedDevice(session, sessionsDir);
266
+ cursor.platform ??= parsed.events.map(platformOf).find((platform) => platform !== void 0);
267
+ cursor.platform ??= spans.length ? "ios" : void 0;
268
+ const retainedTarget = () => ({
269
+ platform: cursor.platform,
270
+ slot: "default"
271
+ });
272
+ if (parsed.unknownVersion && !cursor.unknownReported) {
273
+ const deviceId = spans.at(-1)?.deviceId ?? (session && claimedDevice(session, sessionsDir));
274
+ const target = !targets ? retainedTarget() : cursor.macos ? cursor.bundleId ? macosByBundle.get(cursor.bundleId) : void 0 : deviceId ? byId.get(deviceId) : void 0;
275
+ if (target) {
276
+ cursor.unknownReported = true;
277
+ out.push(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
278
+ }
279
+ }
280
+ if (parsed.unknownVersion) cursor.bundleId = null;
281
+ for (const event of parsed.events) {
282
+ if (event.kind === "request.started") {
283
+ cursor.closing = event.command === "close" ? cursor.bundleId : null;
284
+ cursor.bundleId = null;
285
+ continue;
286
+ }
287
+ if (targets && event.command === "open") trackMacosOpen(cursor, event, Boolean(parsed.unknownVersion));
288
+ if (targets && cursor.macos) {
289
+ if (event.command === "screenshot") continue;
290
+ const bundleId = cursor.bundleId ?? (event.command === "close" ? cursor.closing : null);
291
+ cursor.closing = null;
292
+ const target = bundleId ? macosByBundle.get(bundleId) : void 0;
293
+ const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
294
+ if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt) out.push(agentRecord(event, target.id, target, startedAt));
295
+ if (event.command === "close") cursor.bundleId = null;
296
+ continue;
297
+ }
298
+ if (event.ts < sinceTs) continue;
299
+ if (!targets) cursor.platform = platformOf(event) ?? cursor.platform;
300
+ const deviceId = !targets && cursor.platform === "android" ? null : deviceAt(event.ts);
301
+ const target = targets ? deviceId ? byId.get(deviceId) : void 0 : retainedTarget();
302
+ const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
303
+ if (target) out.push(agentRecord(event, deviceId || null, target, startedAt));
304
+ }
305
+ }
306
+ return out.toSorted((a, b) => a.ts - b.ts);
307
+ };
308
+ }
309
+ //#endregion
10
310
  //#region state/json-file.ts
11
311
  const readScope = new AsyncLocalStorage();
12
312
  /**
@@ -49,6 +349,64 @@ function readJsonObject(path) {
49
349
  }
50
350
  }
51
351
  //#endregion
352
+ //#region state/agent-device-usage.ts
353
+ function agentDeviceUsageFile() {
354
+ return join(configDir(), "agent-device-usage.json");
355
+ }
356
+ function readAgentDeviceUsageCache() {
357
+ const value = readJsonObject(agentDeviceUsageFile());
358
+ if (!value || value.version !== 1) return null;
359
+ const { roots, ...usage } = value;
360
+ if (!validUsage(usage) || !validRoots(roots)) return null;
361
+ return {
362
+ usage,
363
+ roots
364
+ };
365
+ }
366
+ function readAgentDeviceUsage() {
367
+ const value = readJsonObject(agentDeviceUsageFile());
368
+ if (!value) return null;
369
+ const { roots: _roots, ...usage } = value;
370
+ return validUsage(usage) ? usage : null;
371
+ }
372
+ function object(value) {
373
+ return value !== null && typeof value === "object" && !Array.isArray(value);
374
+ }
375
+ function bytes(value) {
376
+ return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
377
+ }
378
+ function sized(value) {
379
+ return object(value) && typeof value.dir === "string" && bytes(value.bytes);
380
+ }
381
+ function count$1(value) {
382
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
383
+ }
384
+ function nullableString(value) {
385
+ return value === null || typeof value === "string";
386
+ }
387
+ function validRoots(value) {
388
+ return object(value) && typeof value.stateDir === "string" && typeof value.runnerRoot === "string" && typeof value.hostedDir === "string" && Array.isArray(value.workspaceDirs) && value.workspaceDirs.every((dir) => typeof dir === "string");
389
+ }
390
+ function validUsage(value) {
391
+ const { stateDir: state, runnerBuilds: runner, workspaces, hosted } = value;
392
+ return value.version === 1 && typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) && typeof value.bytes === "number" && bytes(value.bytes) && typeof value.complete === "boolean" && sized(state) && typeof state.present === "boolean" && sized(state.sessions) && count$1(state.sessions.count) && sized(state.logs) && object(state.other) && bytes(state.other.bytes) && Array.isArray(state.other.largest) && state.other.largest.every((entry) => object(entry) && typeof entry.name === "string" && bytes(entry.bytes)) && sized(runner) && typeof runner.present === "boolean" && bytes(runner.sharedBytes) && Array.isArray(runner.platforms) && runner.platforms.every((platform) => sized(platform) && typeof platform.platform === "string" && Array.isArray(platform.entries) && platform.entries.every((entry) => sized(entry) && typeof entry.name === "string" && nullableString(entry.lastUsedAt) && nullableString(entry.packageVersion) && nullableString(entry.xcodeBuildVersion) && typeof entry.inUse === "boolean" && [
393
+ null,
394
+ "lease",
395
+ "lock",
396
+ "unreadable"
397
+ ].includes(entry.inUseReason))) && Array.isArray(workspaces) && workspaces.every((entry) => sized(entry) && nullableString(entry.projectRoot)) && (hosted === null || sized(hosted) && count$1(hosted.sessions));
398
+ }
399
+ //#endregion
400
+ //#region state/swiftpm-cache-usage.ts
401
+ function swiftpmCacheUsageFile() {
402
+ return join(configDir(), "swiftpm-cache-usage.json");
403
+ }
404
+ function readSwiftpmCacheUsage() {
405
+ const value = readJsonObject(swiftpmCacheUsageFile());
406
+ if (!value || value.version !== 1 || typeof value.measuredAt !== "string" || !Number.isFinite(Date.parse(value.measuredAt)) || typeof value.dir !== "string" || typeof value.present !== "boolean" || typeof value.complete !== "boolean" || !(value.bytes === null || typeof value.bytes === "number" && Number.isFinite(value.bytes) && value.bytes >= 0)) return null;
407
+ return value;
408
+ }
409
+ //#endregion
52
410
  //#region state/paths.ts
53
411
  function getConfigPath() {
54
412
  return join(configDir(), "config.json");
@@ -59,6 +417,9 @@ function configLockPath() {
59
417
  function workspaceMetadataFile(projectRoot) {
60
418
  return join(workspaceStateDir(projectRoot), "workspace.json");
61
419
  }
420
+ function workspaceAgentDeviceDir(projectRoot) {
421
+ return join(workspaceStateDir(projectRoot), "agent-device");
422
+ }
62
423
  function workspaceLogsDir(projectRoot) {
63
424
  return join(workspaceStateDir(projectRoot), "logs");
64
425
  }
@@ -164,6 +525,39 @@ function deviceViewersDir() {
164
525
  function statsFile() {
165
526
  return join(configDir(), "stats.json");
166
527
  }
528
+ function maintenanceDir() {
529
+ return join(configDir(), "maintenance");
530
+ }
531
+ function maintenanceStateFile() {
532
+ return join(maintenanceDir(), "state.json");
533
+ }
534
+ function maintenanceAttemptFile() {
535
+ return join(maintenanceDir(), "attempt.json");
536
+ }
537
+ function maintenanceNdjsonFile() {
538
+ return join(maintenanceDir(), "maintenance.ndjson");
539
+ }
540
+ function maintenanceChildLogFile() {
541
+ return join(maintenanceDir(), "child.log");
542
+ }
543
+ function maintenanceRunClaims() {
544
+ return join(maintenanceDir(), "run.claims");
545
+ }
546
+ function archiveRoot() {
547
+ return join(configDir(), "archive");
548
+ }
549
+ function archiveDir(id) {
550
+ return join(archiveRoot(), id);
551
+ }
552
+ function archiveLock() {
553
+ return join(archiveRoot(), ".lock");
554
+ }
555
+ function serverSetupDir() {
556
+ return join(configDir(), "server", "setup");
557
+ }
558
+ function setupJournalFile(hash) {
559
+ return hash.length === 64 && /^[a-f0-9]{64}$/.test(hash) ? join(serverSetupDir(), `${hash}.json`) : null;
560
+ }
167
561
  //#endregion
168
562
  //#region state/config.ts
169
563
  function withConfigLock(fn) {
@@ -233,6 +627,46 @@ function getConcurrencyLimits({ env = process.env } = {}) {
233
627
  maxDevices: resolveLimit(env.STIM_MAX_DEVICES, c.maxDevices)
234
628
  };
235
629
  }
630
+ const MAX_SETTING = {
631
+ ios: {
632
+ key: "iosParkedMax",
633
+ env: "STIM_POOL_IOS_PARKED_MAX"
634
+ },
635
+ android: {
636
+ key: "androidParkedMax",
637
+ env: "STIM_POOL_ANDROID_PARKED_MAX"
638
+ }
639
+ };
640
+ function parseMax(raw, strings) {
641
+ const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
642
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
643
+ return value;
644
+ }
645
+ function getParkedMax(platform, { config, env = process.env } = {}) {
646
+ const { key, env: envKey } = MAX_SETTING[platform];
647
+ const fromEnv = env[envKey];
648
+ const explicit = fromEnv !== void 0 && fromEnv !== "";
649
+ const pool = (config === void 0 ? loadConfig() : config)?.pool;
650
+ if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
651
+ max: 0,
652
+ error: "Invalid pool value. Expected an object with simulator and emulator bounds."
653
+ };
654
+ const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
655
+ const raw = explicit ? fromEnv : fromConfig;
656
+ if (raw === void 0) return {
657
+ max: env.STIM_HOME ? 0 : 3,
658
+ error: null
659
+ };
660
+ const parsed = parseMax(raw, explicit);
661
+ if (parsed === null) return {
662
+ max: 0,
663
+ error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
664
+ };
665
+ return {
666
+ max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
667
+ error: null
668
+ };
669
+ }
236
670
  function resolveLimit(envVal, cfgVal) {
237
671
  const raw = envVal !== void 0 && envVal !== null && envVal !== "" ? Number(envVal) : typeof cfgVal === "number" ? cfgVal : NaN;
238
672
  if (!Number.isFinite(raw) || raw <= 0) return 0;
@@ -288,6 +722,66 @@ function findProjectByMetroPort(port) {
288
722
  return null;
289
723
  }
290
724
  //#endregion
725
+ //#region state/tailnet.ts
726
+ const DEFAULT_SERVE_PORT = 7443;
727
+ const TAILNET_MACHINE_PATTERN = "^[A-Za-z0-9][A-Za-z0-9.-]*(?::(?!0+$)(?:[0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$";
728
+ function parseMachine(entry) {
729
+ if (!(/* @__PURE__ */ new RegExp("^[A-Za-z0-9][A-Za-z0-9.-]*(?::(?!0+$)(?:[0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$")).test(entry.trim())) return null;
730
+ const [name, port] = entry.trim().split(":");
731
+ return {
732
+ name: name.toLowerCase(),
733
+ port: port === void 0 ? DEFAULT_SERVE_PORT : Number(port)
734
+ };
735
+ }
736
+ /**
737
+ * The one peer in `tailscale status --json` whose MagicDNS name is `name` or starts with `name.`. `ID` there is
738
+ * the node's StableID, the same value `tailscale whois` reports as `Node.StableID`.
739
+ */
740
+ function findPeer(status, name) {
741
+ const found = (isJsonObject(status) && isJsonObject(status.Peer) ? Object.values(status.Peer) : []).flatMap((peer) => {
742
+ if (!isJsonObject(peer) || typeof peer.ID !== "string" || typeof peer.DNSName !== "string") return [];
743
+ const dnsName = peer.DNSName.replace(/\.$/, "").toLowerCase();
744
+ const addresses = Array.isArray(peer.TailscaleIPs) ? peer.TailscaleIPs.filter((ip) => typeof ip === "string") : [];
745
+ const address = addresses.find((ip) => !ip.includes(":")) ?? addresses[0];
746
+ if (!address || !(dnsName === name || dnsName.startsWith(`${name}.`))) return [];
747
+ return [{
748
+ nodeId: peer.ID,
749
+ dnsName,
750
+ address
751
+ }];
752
+ });
753
+ if (found.length === 0) return "missing";
754
+ return found.length === 1 ? found[0] : "ambiguous";
755
+ }
756
+ /**
757
+ * Connects to the pinned node's own tailnet address rather than resolving its name, so the socket reaches the
758
+ * node whose ID was just checked. `tailscale serve` still needs the MagicDNS name for TLS (SNI and certificate)
759
+ * and for its Host routing.
760
+ */
761
+ function endpoint(peer, port) {
762
+ const address = peer.address.includes(":") ? `[${peer.address}]` : peer.address;
763
+ const suffix = port === 443 ? "" : `:${port}`;
764
+ return {
765
+ url: `wss://${address}${suffix}`,
766
+ servername: peer.dnsName,
767
+ host: `${peer.dnsName}${suffix}`
768
+ };
769
+ }
770
+ /**
771
+ * The endpoint of a paired machine's pinned node, or why Stim does not connect to it. The name must still
772
+ * resolve to exactly the pinned node; the token is never sent anywhere else.
773
+ */
774
+ function pinnedEndpoint(credential, current) {
775
+ const parsed = parseMachine(credential.machine);
776
+ if (!parsed) return `${credential.machine} is not a tailnet name`;
777
+ if (!isJsonObject(current)) return "Tailscale is not running";
778
+ const peer = findPeer(current, parsed.name);
779
+ if (peer === "missing") return `no peer on this tailnet is named ${parsed.name}`;
780
+ if (peer === "ambiguous") return `several tailnet peers match ${parsed.name}`;
781
+ if (peer.nodeId !== credential.nodeId) return `${peer.dnsName} is now node ${peer.nodeId}, not the pinned ${credential.nodeId}; run stim doctor`;
782
+ return endpoint(peer, parsed.port);
783
+ }
784
+ //#endregion
291
785
  //#region state/hosted-macos.ts
292
786
  /** App slots one host offers; a slot names the host-assigned bundle id `<bundleId>.hosted<slot>`. */
293
787
  const HOSTED_MACOS_APP_SLOTS = 64;
@@ -317,11 +811,11 @@ const AGENT_PATH = /^\/device-host\/agent\/[a-f0-9-]{36}\/$/;
317
811
  const LEASE_NAME = /^[A-Za-z0-9._-]{1,128}$/;
318
812
  const LEASE_DEVICE_KEY = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+@[1-9][0-9]{0,9}$/;
319
813
  function parseHostedAgentLease(value) {
320
- if (!isJsonObject(value) || Object.keys(value).length !== 4 || ![
814
+ if (!isJsonObject(value) || Object.keys(value).length !== (value.backend === "ios-instance" || value.backend === "android-instance" ? 5 : 4) || ![
321
815
  value.tenant,
322
816
  value.runId,
323
817
  value.clientId
324
- ].every((name) => typeof name === "string" && LEASE_NAME.test(name)) || typeof value.deviceKey !== "string" || !LEASE_DEVICE_KEY.test(value.deviceKey)) return null;
818
+ ].every((name) => typeof name === "string" && LEASE_NAME.test(name)) || typeof value.deviceKey !== "string" || !(value.backend === "ios-instance" ? /^ios:mobile:[a-fA-F0-9-]{36}$/.test(value.deviceKey) : value.backend === "android-instance" ? /^android:mobile:emulator-[0-9]+$/.test(value.deviceKey) : value.backend === void 0 && LEASE_DEVICE_KEY.test(value.deviceKey))) return null;
325
819
  return value;
326
820
  }
327
821
  /** A grant naming a driver this client does not know is not usable; it parses as null. */
@@ -345,7 +839,7 @@ function parseHostedAgentAccess(value) {
345
839
  driver: "none",
346
840
  setting: "hosting.agentDriver"
347
841
  } : null;
348
- if (value.driver !== "agent-device" || typeof value.remoteConfig !== "string" || !value.remoteConfig.startsWith("/") || typeof value.command !== "string" || !value.command || Object.keys(value).length !== 3) return null;
842
+ if (value.driver !== "agent-device" || typeof value.remoteConfig !== "string" || !isAbsolute(value.remoteConfig) || typeof value.command !== "string" || !value.command || Object.keys(value).length !== 3) return null;
349
843
  return {
350
844
  driver: "agent-device",
351
845
  remoteConfig: value.remoteConfig,
@@ -434,10 +928,6 @@ const RELATIVE_PATH = {
434
928
  };
435
929
  const BOOLEAN = { kind: "boolean" };
436
930
  const OBJECT = { kind: "object" };
437
- const REMOTE = {
438
- kind: "choice",
439
- choices: REMOTE_DEVICE_BACKENDS
440
- };
441
931
  const PARKED_MAX = {
442
932
  kind: "number",
443
933
  integer: true,
@@ -494,7 +984,7 @@ const SETTINGS = [
494
984
  default: "auto"
495
985
  },
496
986
  optimization("android.gradleBuildCache", "Gradle build cache"),
497
- optimization("android.targetAbiOnly", "Narrow Debug builds to the device ABI"),
987
+ optimization("android.targetAbiOnly", "Narrow Debug and hosted Android builds to the device ABI"),
498
988
  {
499
989
  key: "ios.deviceType",
500
990
  type: STRING,
@@ -516,9 +1006,13 @@ const SETTINGS = [
516
1006
  },
517
1007
  {
518
1008
  key: "ios.remote",
519
- type: REMOTE,
1009
+ type: {
1010
+ kind: "string",
1011
+ pattern: TAILNET_MACHINE_PATTERN,
1012
+ patternHelp: "eas, proxy, auto, or a tailnet machine name"
1013
+ },
520
1014
  scopes: PROJECT,
521
- description: "Default remote backend for iOS"
1015
+ description: "Default iOS remote target: eas, proxy, or an approved Mac in hosting.machines; auto places on an approved Mac when this Mac is full or busy"
522
1016
  },
523
1017
  {
524
1018
  key: "ios.simslimProfile",
@@ -610,9 +1104,13 @@ const SETTINGS = [
610
1104
  },
611
1105
  {
612
1106
  key: "android.remote",
613
- type: REMOTE,
1107
+ type: {
1108
+ kind: "string",
1109
+ pattern: TAILNET_MACHINE_PATTERN,
1110
+ patternHelp: "eas, proxy, auto, or a tailnet machine name"
1111
+ },
614
1112
  scopes: PROJECT,
615
- description: "Default remote backend for Android"
1113
+ description: "Default Android remote target: eas, proxy, or an approved Mac in hosting.machines; auto places on an approved Mac when this Mac is full or busy"
616
1114
  },
617
1115
  {
618
1116
  key: "metro.tunnel",
@@ -679,9 +1177,20 @@ const SETTINGS = [
679
1177
  minimum: 0
680
1178
  },
681
1179
  scopes: EVERY,
682
- default: 0,
1180
+ default: 30,
683
1181
  description: "Minutes an owned simulator or emulator stays idle before the workspace supervisor shuts it down; 0 never"
684
1182
  },
1183
+ {
1184
+ key: "devices.reclaimIdleMinutes",
1185
+ type: {
1186
+ kind: "number",
1187
+ integer: true,
1188
+ minimum: 0
1189
+ },
1190
+ scopes: EVERY,
1191
+ default: 10,
1192
+ description: "Minutes an owned simulator or emulator stays idle before a waiting run reclaims its device slot; 0 never"
1193
+ },
685
1194
  {
686
1195
  key: "macos.product",
687
1196
  type: {
@@ -751,6 +1260,79 @@ const SETTINGS = [
751
1260
  env: "STIM_RECORDING",
752
1261
  description: "Let stim-server record device screens for replay; false stops it and deletes the recordings"
753
1262
  },
1263
+ {
1264
+ key: "archive.enabled",
1265
+ type: BOOLEAN,
1266
+ scopes: EVERY,
1267
+ default: true,
1268
+ env: "STIM_ARCHIVE_ENABLED",
1269
+ scopedHomeValue: false,
1270
+ description: "Keep workspace history on removal; false under STIM_HOME unless explicitly enabled"
1271
+ },
1272
+ {
1273
+ key: "archive.maxAgeDays",
1274
+ type: CAPACITY,
1275
+ scopes: MACHINE,
1276
+ default: 30,
1277
+ env: "STIM_ARCHIVE_MAX_AGE_DAYS",
1278
+ description: "Days to keep archived workspace records; 0 keeps nothing"
1279
+ },
1280
+ {
1281
+ key: "archive.maxCount",
1282
+ type: CAPACITY,
1283
+ scopes: MACHINE,
1284
+ default: 200,
1285
+ env: "STIM_ARCHIVE_MAX_COUNT",
1286
+ description: "Archived workspace count; oldest records go first, 0 keeps nothing"
1287
+ },
1288
+ {
1289
+ key: "archive.maxTotalGb",
1290
+ type: GIGABYTES,
1291
+ scopes: MACHINE,
1292
+ default: 5,
1293
+ env: "STIM_ARCHIVE_MAX_TOTAL_GB",
1294
+ description: "Total archived bytes in binary GB; 0 keeps no artifacts"
1295
+ },
1296
+ {
1297
+ key: "archive.logs.maxAgeDays",
1298
+ type: CAPACITY,
1299
+ scopes: MACHINE,
1300
+ default: 14,
1301
+ env: "STIM_ARCHIVE_LOGS_MAX_AGE_DAYS",
1302
+ description: "Days to keep archived logs; 0 keeps none"
1303
+ },
1304
+ {
1305
+ key: "archive.logs.maxMbPerWorkspace",
1306
+ type: GIGABYTES,
1307
+ scopes: MACHINE,
1308
+ default: 100,
1309
+ env: "STIM_ARCHIVE_LOGS_MAX_MB_PER_WORKSPACE",
1310
+ description: "Archived logs per workspace in binary MB; 0 keeps none"
1311
+ },
1312
+ {
1313
+ key: "archive.recordings.maxAgeDays",
1314
+ type: CAPACITY,
1315
+ scopes: MACHINE,
1316
+ default: 3,
1317
+ env: "STIM_ARCHIVE_RECORDINGS_MAX_AGE_DAYS",
1318
+ description: "Days to keep archived recordings; 0 keeps none"
1319
+ },
1320
+ {
1321
+ key: "archive.recordings.maxTotalGb",
1322
+ type: GIGABYTES,
1323
+ scopes: MACHINE,
1324
+ default: 2,
1325
+ env: "STIM_ARCHIVE_RECORDINGS_MAX_TOTAL_GB",
1326
+ description: "Archived recordings in binary GB; 0 keeps none"
1327
+ },
1328
+ {
1329
+ key: "archive.agentActions.maxAgeDays",
1330
+ type: CAPACITY,
1331
+ scopes: MACHINE,
1332
+ default: 7,
1333
+ env: "STIM_ARCHIVE_AGENT_ACTIONS_MAX_AGE_DAYS",
1334
+ description: "Days to keep archived agent-device sessions; 0 keeps none"
1335
+ },
754
1336
  {
755
1337
  key: "worktree.exclude",
756
1338
  type: { kind: "strings" },
@@ -849,6 +1431,179 @@ const SETTINGS = [
849
1431
  scopedHomeValue: 0,
850
1432
  description: "Live workspaces before idle ones are reclaimed; unset or 0 means no limit"
851
1433
  },
1434
+ {
1435
+ key: "maintenance.mode",
1436
+ type: {
1437
+ kind: "choice",
1438
+ choices: ["off", "report"]
1439
+ },
1440
+ scopes: MACHINE,
1441
+ default: "report",
1442
+ env: "STIM_MAINTENANCE",
1443
+ scopedHomeValue: "off",
1444
+ ciValue: "off",
1445
+ description: "Automatic resource maintenance. report measures, plans and logs what it would do and never deletes or stops anything; off disables it. Off under STIM_HOME or CI unless STIM_MAINTENANCE is set"
1446
+ },
1447
+ {
1448
+ key: "maintenance.pressureCheckMinutes",
1449
+ type: {
1450
+ kind: "number",
1451
+ integer: true,
1452
+ minimum: 1
1453
+ },
1454
+ scopes: MACHINE,
1455
+ default: 1,
1456
+ env: "STIM_MAINTENANCE_PRESSURE_CHECK_MINUTES",
1457
+ description: "Disk and memory check interval in minutes"
1458
+ },
1459
+ {
1460
+ key: "maintenance.sizeCheckMinutes",
1461
+ type: {
1462
+ kind: "number",
1463
+ integer: true,
1464
+ minimum: 1
1465
+ },
1466
+ scopes: MACHINE,
1467
+ default: 60,
1468
+ env: "STIM_MAINTENANCE_SIZE_CHECK_MINUTES",
1469
+ description: "Directory size check interval in minutes"
1470
+ },
1471
+ {
1472
+ key: "maintenance.maxLoadPerCore",
1473
+ type: {
1474
+ kind: "number",
1475
+ exclusiveMinimum: 0
1476
+ },
1477
+ scopes: MACHINE,
1478
+ default: 4,
1479
+ env: "STIM_MAINTENANCE_MAX_LOAD_PER_CORE",
1480
+ description: "Defer size scans above this one-minute load per CPU"
1481
+ },
1482
+ {
1483
+ key: "maintenance.logMaxMb",
1484
+ type: {
1485
+ kind: "number",
1486
+ exclusiveMinimum: 0
1487
+ },
1488
+ scopes: MACHINE,
1489
+ default: 1,
1490
+ env: "STIM_MAINTENANCE_LOG_MAX_MB",
1491
+ description: "Machine maintenance log rotation size in MiB"
1492
+ },
1493
+ {
1494
+ key: "maintenance.logRetentionDays",
1495
+ type: {
1496
+ kind: "number",
1497
+ integer: true,
1498
+ minimum: 1
1499
+ },
1500
+ scopes: MACHINE,
1501
+ default: 30,
1502
+ env: "STIM_MAINTENANCE_LOG_RETENTION_DAYS",
1503
+ description: "Retention of the rotated machine maintenance log in days"
1504
+ },
1505
+ {
1506
+ key: "maintenance.logChecks",
1507
+ type: BOOLEAN,
1508
+ scopes: MACHINE,
1509
+ default: false,
1510
+ env: "STIM_MAINTENANCE_LOG_CHECKS",
1511
+ description: "Write debug check records and routine skips"
1512
+ },
1513
+ {
1514
+ key: "maintenance.memoryPressureLevel",
1515
+ type: {
1516
+ kind: "choice",
1517
+ choices: [
1518
+ "warning",
1519
+ "critical",
1520
+ "off"
1521
+ ]
1522
+ },
1523
+ scopes: MACHINE,
1524
+ default: "warning",
1525
+ env: "STIM_MAINTENANCE_MEMORY_PRESSURE_LEVEL",
1526
+ description: "Lowest macOS memory pressure level considered; off disables memory pressure reporting"
1527
+ },
1528
+ {
1529
+ key: "maintenance.memoryWarningMinutes",
1530
+ type: {
1531
+ kind: "number",
1532
+ integer: true,
1533
+ minimum: 0
1534
+ },
1535
+ scopes: MACHINE,
1536
+ default: 10,
1537
+ env: "STIM_MAINTENANCE_MEMORY_WARNING_MINUTES",
1538
+ description: "Consecutive warning minutes before memory counts as pressure"
1539
+ },
1540
+ {
1541
+ key: "maintenance.minAvailableMemoryGb",
1542
+ type: {
1543
+ kind: "number",
1544
+ minimum: 0
1545
+ },
1546
+ scopes: MACHINE,
1547
+ env: "STIM_MAINTENANCE_MIN_AVAILABLE_MEMORY_GB",
1548
+ description: "Available-memory threshold in GiB; unset is 10% of RAM"
1549
+ },
1550
+ {
1551
+ key: "maintenance.capTargetPercent",
1552
+ type: {
1553
+ kind: "number",
1554
+ integer: true,
1555
+ minimum: 10,
1556
+ maximum: 100
1557
+ },
1558
+ scopes: MACHINE,
1559
+ default: 80,
1560
+ env: "STIM_MAINTENANCE_CAP_TARGET_PERCENT",
1561
+ description: "Plan toward this percentage of each size cap"
1562
+ },
1563
+ {
1564
+ key: "maintenance.workspaceOutputsMaxGb",
1565
+ type: {
1566
+ kind: "number",
1567
+ minimum: 0
1568
+ },
1569
+ scopes: MACHINE,
1570
+ default: 20,
1571
+ env: "STIM_MAINTENANCE_WORKSPACE_OUTPUTS_MAX_GB",
1572
+ description: "Workspace build-output cap in GiB; 0 means no cap"
1573
+ },
1574
+ {
1575
+ key: "caches.buildCacheMaxGb",
1576
+ type: {
1577
+ kind: "number",
1578
+ minimum: 0
1579
+ },
1580
+ scopes: MACHINE,
1581
+ default: 10,
1582
+ env: "STIM_CACHES_BUILD_CACHE_MAX_GB",
1583
+ description: "Shared native build-cache cap in GiB; 0 means no cap"
1584
+ },
1585
+ {
1586
+ key: "caches.metroCacheMaxGb",
1587
+ type: {
1588
+ kind: "number",
1589
+ minimum: 0
1590
+ },
1591
+ scopes: MACHINE,
1592
+ default: 5,
1593
+ env: "STIM_CACHES_METRO_CACHE_MAX_GB",
1594
+ description: "Combined Metro transform-cache cap in GiB; 0 means no cap"
1595
+ },
1596
+ {
1597
+ key: "caches.swiftCompilationCacheMaxGb",
1598
+ type: {
1599
+ kind: "number",
1600
+ minimum: 0
1601
+ },
1602
+ scopes: MACHINE,
1603
+ default: 15,
1604
+ env: "STIM_CACHES_SWIFT_COMPILATION_CACHE_MAX_GB",
1605
+ description: "Swift compilation-cache cap in GiB; planned emptying is whole; 0 means no cap"
1606
+ },
852
1607
  {
853
1608
  key: "gc.worktreeGraceMinutes",
854
1609
  type: CAPACITY,
@@ -889,7 +1644,7 @@ const SETTINGS = [
889
1644
  },
890
1645
  scopes: MACHINE,
891
1646
  default: "none",
892
- description: "Tool this Mac starts so a client's coding agent can drive the macOS apps it hosts for that client; none starts nothing"
1647
+ description: "Tool this Mac starts so a client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client; none starts nothing"
893
1648
  },
894
1649
  {
895
1650
  key: "server.acceptClientBuilds",
@@ -908,7 +1663,7 @@ const SETTINGS = [
908
1663
  key: "offload.machine",
909
1664
  type: {
910
1665
  kind: "string",
911
- pattern: "^[A-Za-z0-9][A-Za-z0-9.-]*(?::(?!0+$)(?:[0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$",
1666
+ pattern: TAILNET_MACHINE_PATTERN,
912
1667
  patternHelp: "auto, local, or a tailnet machine name"
913
1668
  },
914
1669
  scopes: MACHINE,
@@ -935,7 +1690,7 @@ const SETTINGS = [
935
1690
  },
936
1691
  scopes: MACHINE,
937
1692
  default: 2,
938
- description: "Load per core (5-minute load average divided by the CPU count) at which a Mac counts as saturated: a build machine declines offloaded builds, and auto offload stops preferring this Mac"
1693
+ description: "Load per core (5-minute load average divided by the CPU count) at which a Mac counts as saturated: a build machine declines offloaded builds, and auto offload stops preferring this Mac; also the busy threshold for automatic iOS and Android device placement"
939
1694
  },
940
1695
  {
941
1696
  key: "offload.workerRoot",
@@ -1025,6 +1780,7 @@ function acceptsShape(setting, value) {
1025
1780
  function numberBounds(type) {
1026
1781
  const noun = type.integer ? "a whole number" : "a number";
1027
1782
  if (type.minimum !== void 0 && type.maximum !== void 0) return `${noun} from ${type.minimum} through ${type.maximum}`;
1783
+ if (type.exclusiveMinimum !== void 0) return `${noun}, greater than ${type.exclusiveMinimum}`;
1028
1784
  if (type.minimum !== void 0) return `${noun}, ${type.minimum} or more`;
1029
1785
  return noun;
1030
1786
  }
@@ -1033,7 +1789,7 @@ function settingValueError(setting, value) {
1033
1789
  if (!acceptsShape(setting, value)) return type.kind === "number" ? numberBounds(type) : expectedShape(type);
1034
1790
  if (type.kind === "number") {
1035
1791
  const number = value;
1036
- if (!Number.isFinite(number) || type.integer && !Number.isSafeInteger(number) || type.minimum !== void 0 && number < type.minimum || type.maximum !== void 0 && number > type.maximum) return numberBounds(type);
1792
+ if (!Number.isFinite(number) || type.integer && !Number.isSafeInteger(number) || type.minimum !== void 0 && number < type.minimum || type.exclusiveMinimum !== void 0 && number <= type.exclusiveMinimum || type.maximum !== void 0 && number > type.maximum) return numberBounds(type);
1037
1793
  }
1038
1794
  if (type.kind === "path") {
1039
1795
  const path = value;
@@ -1207,7 +1963,8 @@ function parseCredential(value) {
1207
1963
  deviceId,
1208
1964
  deviceToken,
1209
1965
  state,
1210
- requestedAt
1966
+ requestedAt,
1967
+ ...typeof value.ticketHash === "string" ? { ticketHash: value.ticketHash } : {}
1211
1968
  };
1212
1969
  }
1213
1970
  function readBuildMachines() {
@@ -1375,7 +2132,8 @@ const SOURCES = [
1375
2132
  "client",
1376
2133
  "device",
1377
2134
  "build",
1378
- "agent"
2135
+ "agent",
2136
+ "maintenance"
1379
2137
  ];
1380
2138
  function levelRank(level) {
1381
2139
  const i = LEVELS.indexOf(level);
@@ -1486,7 +2244,7 @@ function bundleMarkerPlatform(record) {
1486
2244
  return (/^RSC\((.+)\)$/.exec(tag)?.[1] ?? tag).toLowerCase();
1487
2245
  }
1488
2246
  function isAppDeviceRecord(record) {
1489
- return record.src === "device" && (record.event === "native_crash" || record.platform === "web");
2247
+ return record.src === "device" && (record.event === "native_crash" || record.event === "hosted_native_log" || record.platform === "web");
1490
2248
  }
1491
2249
  function recordMatches(record, criteria = {}) {
1492
2250
  if (!record) return false;
@@ -1495,6 +2253,7 @@ function recordMatches(record, criteria = {}) {
1495
2253
  if (sources && sources.length > 0 && !sources.includes(record.src) && !(criteria.includeAppDeviceErrors && isAppDeviceRecord(record))) return false;
1496
2254
  if (minLevel && levelRank(record.level) < levelRank(minLevel)) return false;
1497
2255
  if (errorsOnly) {
2256
+ if (record.src === "maintenance" && record.event !== "maintenance_failure") return false;
1498
2257
  if (record.level !== "error" && record.level !== "fatal") return false;
1499
2258
  if (typeof markerTs === "number") {
1500
2259
  const ts = tsOf(record);
@@ -1559,6 +2318,11 @@ function readLogRecords(dir, include = () => true) {
1559
2318
  for (const name of logFiles(dir).filter(include)) for (const record of readNdjsonGenerations(join(dir, name))) all.push(record);
1560
2319
  return sortByTs(all);
1561
2320
  }
2321
+ const NATIVE_CRASH_LOG_PREFIX = "native-crash-";
2322
+ /** Reads the CLI timeline with retained native crash records after other records at the same timestamp. */
2323
+ function readLogTimeline(dir, records) {
2324
+ return sortByTs([...records ?? readLogRecords(dir, (name) => !name.startsWith("native-crash-")), ...readLogRecords(dir, (name) => name.startsWith(NATIVE_CRASH_LOG_PREFIX))]);
2325
+ }
1562
2326
  function isExpoErrorContext(record, event) {
1563
2327
  if (record.src !== "metro" || record.raw !== true || record.event !== event || typeof record.msg !== "string") return false;
1564
2328
  return /^Code: \S/.test(record.msg) || /^\s*>?\s*\d+\s*\|/.test(record.msg) || /^\s*\|\s*\^/.test(record.msg) || /^Call Stack$/.test(record.msg) || /^\s+.+\([^()]+:\d+:\d+\)\s*$/.test(record.msg) || /^\s+at\s+\S+:\d+:\d+\s*$/.test(record.msg);
@@ -1739,6 +2503,16 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
1739
2503
  if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
1740
2504
  return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs, criteria.sinceTs) : matched;
1741
2505
  }
2506
+ /** Queries the JSON timeline, including Expo context fields on errors as `stim logs --json` does. */
2507
+ function queryJsonLogs(options = {}) {
2508
+ const records = options.records ?? readLogTimeline(options.dir);
2509
+ const matched = queryLogs({
2510
+ ...options,
2511
+ records,
2512
+ errorContext: false
2513
+ });
2514
+ return options.errorsOnly ? attachExpoErrorContext(records, matched, "field") : matched;
2515
+ }
1742
2516
  function tailRead(prev, size) {
1743
2517
  const state = prev && typeof prev.offset === "number" ? prev : {
1744
2518
  offset: 0,
@@ -1882,13 +2656,13 @@ function listDir(dir) {
1882
2656
  return [];
1883
2657
  }
1884
2658
  }
1885
- /** The segments in one device directory, oldest first. Files that are not segments are left out. */
1886
- function listSegments(dir) {
2659
+ /** The segments in one device directory, oldest first. Files that are not segments are left out; `closedOnly` excludes files still being written. */
2660
+ function listSegments(dir, closedOnly = false) {
1887
2661
  const segments = [];
1888
2662
  for (const name of listDir(dir)) {
1889
2663
  const closed = SEGMENT.exec(name);
1890
2664
  const part = closed ? null : PART.exec(name);
1891
- if (!closed && !part) continue;
2665
+ if (!closed && (closedOnly || !part)) continue;
1892
2666
  let stat;
1893
2667
  try {
1894
2668
  stat = statSync(join(dir, name));
@@ -1907,7 +2681,7 @@ function listSegments(dir) {
1907
2681
  return segments.toSorted((a, b) => a.start - b.start);
1908
2682
  }
1909
2683
  /** Every device slot recorded under a workspace's recordings directory. */
1910
- function listRecordedDevices(recordingsDir) {
2684
+ function listRecordedDevices(recordingsDir, closedOnly = false) {
1911
2685
  return listDir(recordingsDir).flatMap((name) => {
1912
2686
  const match = DEVICE.exec(name);
1913
2687
  if (!match) return [];
@@ -1916,7 +2690,7 @@ function listRecordedDevices(recordingsDir) {
1916
2690
  platform: match[1],
1917
2691
  slot: match[2],
1918
2692
  dir,
1919
- segments: listSegments(dir)
2693
+ segments: listSegments(dir, closedOnly)
1920
2694
  }];
1921
2695
  });
1922
2696
  }
@@ -1993,6 +2767,7 @@ function leafSchema(setting, scope) {
1993
2767
  case "number":
1994
2768
  schema.type = type.integer ? "integer" : "number";
1995
2769
  if (type.minimum !== void 0) schema.minimum = type.minimum;
2770
+ if (type.exclusiveMinimum !== void 0) schema.exclusiveMinimum = type.exclusiveMinimum;
1996
2771
  if (type.maximum !== void 0) schema.maximum = type.maximum;
1997
2772
  break;
1998
2773
  case "boolean":
@@ -2152,6 +2927,8 @@ const WORKSPACE_PHASES = [
2152
2927
  "live",
2153
2928
  "idle"
2154
2929
  ];
2930
+ /** The tutorial format version the installed Stim writes into the tutorial app. */
2931
+ const TUTORIAL_VERSION = 1;
2155
2932
  /**
2156
2933
  * Where a memory figure comes from. `footprint` is each process's physical footprint, which Activity Monitor's Memory
2157
2934
  * column shows. `rss` sums resident set sizes, which count pages shared between processes once per process, so a
@@ -2537,6 +3314,7 @@ function readIdleStop(state) {
2537
3314
  idleMinutes: record.idleMinutes
2538
3315
  };
2539
3316
  }
3317
+ const DEVICE_RECLAIM_REASON = "reclaimed for a waiting run";
2540
3318
  const DEVICE_IDLE_SHUTDOWN_KEY = "deviceIdleShutdowns";
2541
3319
  /** The recorded idle shutdowns by device slot key (`ios`, `android`, `ios:<slot>`). */
2542
3320
  function readDeviceIdleShutdowns(state) {
@@ -2547,7 +3325,8 @@ function readDeviceIdleShutdowns(state) {
2547
3325
  const record = value;
2548
3326
  if (typeof record?.at === "string" && typeof record.idleMinutes === "number") found[key] = {
2549
3327
  at: record.at,
2550
- idleMinutes: record.idleMinutes
3328
+ idleMinutes: record.idleMinutes,
3329
+ ...typeof record.reason === "string" ? { reason: record.reason } : {}
2551
3330
  };
2552
3331
  }
2553
3332
  return found;
@@ -2666,6 +3445,22 @@ function buildDiagnostics(value) {
2666
3445
  }];
2667
3446
  }).toSorted((a, b) => Number(b.file !== null) - Number(a.file !== null)).slice(0, DIAGNOSTIC_CAP);
2668
3447
  }
3448
+ function devicePlacement(value) {
3449
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3450
+ const record = value;
3451
+ if (record.decision !== "local" && record.decision !== "hosted" && record.decision !== "waited-locally" || typeof record.reason !== "string") return null;
3452
+ return {
3453
+ decision: record.decision,
3454
+ reason: record.reason,
3455
+ ...typeof record.machine === "string" ? { machine: record.machine } : {}
3456
+ };
3457
+ }
3458
+ function readDevicePlacement(state, platform, slot = "default") {
3459
+ const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
3460
+ const saved = isJsonObject(slots[slot]) ? slots[slot] : {};
3461
+ const device = slot === "default" ? state?.[platform] : saved[platform];
3462
+ return devicePlacement(isJsonObject(device) ? device.devicePlacement : void 0);
3463
+ }
2669
3464
  function lastBuildReport(platform, value) {
2670
3465
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2671
3466
  const record = value;
@@ -2675,6 +3470,7 @@ function lastBuildReport(platform, value) {
2675
3470
  const finished = new Date(Date.parse(record.startedAt) + (durationMs ?? NaN));
2676
3471
  const reason = record.cacheHit === "local" || record.cacheHit === "remote" ? null : parseMissReason(record.missReason);
2677
3472
  const diagnostics = record.status === "failed" ? buildDiagnostics(record.diagnostics) : [];
3473
+ const placed = devicePlacement(record.devicePlacement);
2678
3474
  return {
2679
3475
  platform,
2680
3476
  status: record.status,
@@ -2686,6 +3482,7 @@ function lastBuildReport(platform, value) {
2686
3482
  finishedAt: Number.isNaN(finished.getTime()) ? null : finished.toISOString(),
2687
3483
  ...typeof record.errorCode === "string" ? { errorCode: record.errorCode } : {},
2688
3484
  ...reason ? { missReason: reason } : {},
3485
+ ...placed ? { devicePlacement: placed } : {},
2689
3486
  ...typeof record.buildMachine === "string" ? { buildMachine: record.buildMachine } : {},
2690
3487
  ...typeof record.builtOn === "string" ? { builtOn: record.builtOn } : {},
2691
3488
  ...typeof record.offloadedTo === "string" ? { offloadedTo: record.offloadedTo } : {},
@@ -2831,12 +3628,21 @@ const PLACEMENT_DECISIONS = [
2831
3628
  "offloaded",
2832
3629
  "fell-back"
2833
3630
  ];
3631
+ const CAPACITY_REFUSAL_LIMIT = 50;
3632
+ const CAPACITY_REFUSAL_MAX_AGE_MS = 6048e5;
3633
+ function trimCapacityRefusals(events, now) {
3634
+ return events.filter((event) => {
3635
+ const age = now - Date.parse(event.at);
3636
+ return age >= 0 && age <= 6048e5;
3637
+ }).toSorted((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-50);
3638
+ }
3639
+ const trimCapacityWaits = trimCapacityRefusals;
2834
3640
  const PLATFORMS = ["ios", "android"];
2835
3641
  const OUTCOMES = ["hit", "cold"];
2836
3642
  const HISTORY_LIMIT = 10;
2837
3643
  function statsProjectKey({ root, commonDir, repoRoot }) {
2838
- if (commonDir && repoRoot && basename(commonDir) === ".git") return canonical(join(canonical(dirname(commonDir)), relative(canonical(repoRoot), canonical(root))));
2839
- return canonical(root);
3644
+ if (commonDir && repoRoot && basename(commonDir) === ".git") return canonical$1(join(canonical$1(dirname(commonDir)), relative(canonical$1(repoRoot), canonical$1(root))));
3645
+ return canonical$1(root);
2840
3646
  }
2841
3647
  /** Placement counts for the local calendar day of `now`, per-machine totals, and the retained placements. */
2842
3648
  function offloadSummary(record, now) {
@@ -2969,6 +3775,8 @@ function normalize(record) {
2969
3775
  if (Object.keys(normalized).length) history[key] = normalized;
2970
3776
  }
2971
3777
  const placements = Array.isArray(record.placements) ? record.placements.flatMap(normalizePlacement) : [];
3778
+ const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
3779
+ const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
2972
3780
  const buildMachines = {};
2973
3781
  if (isObject(record.buildMachines)) {
2974
3782
  for (const [name, totals] of Object.entries(record.buildMachines)) if (isObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
@@ -2979,6 +3787,8 @@ function normalize(record) {
2979
3787
  projects,
2980
3788
  ...Object.keys(history).length ? { history } : {},
2981
3789
  ...placements.length ? { placements } : {},
3790
+ ...capacityRefusals.length ? { capacityRefusals } : {},
3791
+ ...capacityWaits.length ? { capacityWaits } : {},
2982
3792
  ...Object.keys(buildMachines).length ? { buildMachines } : {}
2983
3793
  };
2984
3794
  }
@@ -2988,6 +3798,8 @@ function normalizePlacement(value) {
2988
3798
  if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
2989
3799
  if (typeof reason !== "string" || typeof project !== "string") return [];
2990
3800
  const buildMs = wholeMs(value.buildMs);
3801
+ const slotWaitMs = wholeMs(value.slotWaitMs);
3802
+ const deviceSlotWaitMs = wholeMs(value.deviceSlotWaitMs);
2991
3803
  const localEstimateMs = wholeMs(value.localEstimateMs);
2992
3804
  return [{
2993
3805
  at: timestamp(value.at),
@@ -2997,10 +3809,41 @@ function normalizePlacement(value) {
2997
3809
  reason,
2998
3810
  ...typeof machine === "string" && machine !== "" ? { machine } : {},
2999
3811
  ...buildMs > 0 ? { buildMs } : {},
3812
+ ...slotWaitMs > 0 ? { slotWaitMs } : {},
3813
+ ...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
3000
3814
  ...localEstimateMs > 0 ? { localEstimateMs } : {},
3001
3815
  ...value.failed === true ? { failed: true } : {}
3002
3816
  }];
3003
3817
  }
3818
+ function normalizeCapacityRefusal(value) {
3819
+ if (!isObject(value)) return [];
3820
+ const { at, kind, platform, max, workspace } = value;
3821
+ if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
3822
+ if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
3823
+ if (typeof max !== "number" || !Number.isInteger(max) || max <= 0) return [];
3824
+ if (typeof workspace !== "string" || workspace === "") return [];
3825
+ return [{
3826
+ at: new Date(at).toISOString(),
3827
+ kind,
3828
+ platform,
3829
+ max,
3830
+ workspace
3831
+ }];
3832
+ }
3833
+ function normalizeCapacityWait(value) {
3834
+ if (!isObject(value) || value.kind !== "device-wait") return [];
3835
+ const refusal = normalizeCapacityRefusal({
3836
+ ...value,
3837
+ kind: "device"
3838
+ })[0];
3839
+ if (!refusal || typeof value.ms !== "number" || !Number.isFinite(value.ms) || value.ms < 0) return [];
3840
+ return [{
3841
+ ...refusal,
3842
+ kind: "device-wait",
3843
+ ms: Math.round(value.ms),
3844
+ ...typeof value.reclaimed === "number" && Number.isInteger(value.reclaimed) && value.reclaimed > 0 ? { reclaimed: value.reclaimed } : {}
3845
+ }];
3846
+ }
3004
3847
  function normalizeMachineTotals(totals) {
3005
3848
  const savedMs = Number(totals.savedMs);
3006
3849
  return {
@@ -3095,7 +3938,7 @@ function count(value) {
3095
3938
  function timestamp(value) {
3096
3939
  return typeof value === "string" && value !== "" ? value : (/* @__PURE__ */ new Date(0)).toISOString();
3097
3940
  }
3098
- function canonical(path) {
3941
+ function canonical$1(path) {
3099
3942
  try {
3100
3943
  return realpathSync.native(path);
3101
3944
  } catch {
@@ -3120,6 +3963,8 @@ function decodeStats(text) {
3120
3963
  function readStatsReport(key, now) {
3121
3964
  const { record, note } = readStats();
3122
3965
  const project = key ? record?.projects[key] : null;
3966
+ const capacityRefusals = trimCapacityRefusals(record?.capacityRefusals ?? [], now).toReversed();
3967
+ const capacityWaits = trimCapacityWaits(record?.capacityWaits ?? [], now).toReversed();
3123
3968
  return {
3124
3969
  report: {
3125
3970
  version: 1,
@@ -3132,7 +3977,11 @@ function readStatsReport(key, now) {
3132
3977
  ios: record?.machine.ios ?? null,
3133
3978
  android: record?.machine.android ?? null
3134
3979
  },
3135
- offload: offloadSummary(record, now)
3980
+ offload: offloadSummary(record, now),
3981
+ ...capacityRefusals.length ? { capacityRefusals } : {},
3982
+ ...capacityWaits.length ? { capacityWaits } : {},
3983
+ agentDevice: readAgentDeviceUsage(),
3984
+ swiftpmCache: readSwiftpmCacheUsage()
3136
3985
  },
3137
3986
  note
3138
3987
  };
@@ -3231,9 +4080,16 @@ function parseHostedOfferRequest(value) {
3231
4080
  ...typeof value.deviceProfile === "string" ? { deviceProfile: value.deviceProfile } : {}
3232
4081
  };
3233
4082
  }
4083
+ function parseHostedLocalDeviceCount(value) {
4084
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
4085
+ if (isJsonObject(value) && typeof value.unknown === "string" && value.unknown) return { unknown: value.unknown.slice(0, 4e3) };
4086
+ return null;
4087
+ }
3234
4088
  function parseHostedNativeOffer(value) {
3235
4089
  if (!isJsonObject(value) || !isJsonObject(value.resources)) return null;
3236
4090
  const { resources } = value;
4091
+ const local = parseHostedLocalDeviceCount(resources.localDevices);
4092
+ if (resources.localDevices !== void 0 && local === null) return null;
3237
4093
  if (value.platform !== "ios" && value.platform !== "android" && value.platform !== "macos" || value.declined !== null && (typeof value.declined !== "string" || !value.declined || value.declined.length > 4e3) || !Number.isInteger(resources.cpus) || Number(resources.cpus) < 1 || ["loadPerCore", "memoryFreeBytes"].some((key) => typeof resources[key] !== "number" || !Number.isFinite(resources[key]) || resources[key] < 0) || resources.workerDiskFreeBytes !== null && (typeof resources.workerDiskFreeBytes !== "number" || !Number.isFinite(resources.workerDiskFreeBytes) || resources.workerDiskFreeBytes < 0) || resources.memoryPressure !== null && resources.memoryPressure !== "normal" && resources.memoryPressure !== "warning" && resources.memoryPressure !== "critical") return null;
3238
4094
  let choice = null;
3239
4095
  if (value.choice !== null) {
@@ -3269,7 +4125,8 @@ function parseHostedNativeOffer(value) {
3269
4125
  loadPerCore: resources.loadPerCore,
3270
4126
  memoryFreeBytes: resources.memoryFreeBytes,
3271
4127
  memoryPressure: resources.memoryPressure,
3272
- workerDiskFreeBytes: resources.workerDiskFreeBytes
4128
+ workerDiskFreeBytes: resources.workerDiskFreeBytes,
4129
+ ...local !== null ? { localDevices: local } : {}
3273
4130
  }
3274
4131
  };
3275
4132
  }
@@ -3299,7 +4156,7 @@ function readHostedSessions() {
3299
4156
  "stopping",
3300
4157
  "stopped",
3301
4158
  "unknown"
3302
- ].includes(String(entry.state)) || entry.platform === "android" && !hostedConsolePort(entry.consolePort) || (entry.platform === "macos" ? !hostedMacosAppSlot(entry.appSlot) : entry.appSlot !== void 0) || entry.device !== null && !parseHostedPlatformDevice(entry.device, entry.platform) || entry.platform === "macos" && entry.device !== null && entry.device.appSlot !== entry.appSlot || entry.platform === "android" && entry.device !== null && entry.device.consolePort !== entry.consolePort || entry.state === "ready" && entry.device === null || entry.notice !== void 0 && typeof entry.notice !== "string" || entry.appAttempt !== void 0 && (typeof entry.appAttempt !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(entry.appAttempt)) || entry.metroPort !== void 0 && (typeof entry.metroPort !== "number" || !Number.isInteger(entry.metroPort) || entry.metroPort < 1 || entry.metroPort > 65535)) throw new Error(`Malformed hosted session record: ${root}`);
4159
+ ].includes(String(entry.state)) || entry.platform === "android" && !hostedConsolePort(entry.consolePort) || (entry.platform === "macos" ? !hostedMacosAppSlot(entry.appSlot) : entry.appSlot !== void 0) || entry.device !== null && !parseHostedPlatformDevice(entry.device, entry.platform) || entry.platform === "macos" && entry.device !== null && entry.device.appSlot !== entry.appSlot || entry.platform === "android" && entry.device !== null && entry.device.consolePort !== entry.consolePort || entry.state === "ready" && entry.device === null || entry.parked !== void 0 && (!isJsonObject(entry.parked) || typeof entry.parked.at !== "string" || !Number.isFinite(Date.parse(entry.parked.at)) || new Date(entry.parked.at).toISOString() !== entry.parked.at || entry.state !== "stopped" || entry.platform === "macos") || entry.notice !== void 0 && typeof entry.notice !== "string" || entry.appAttempt !== void 0 && (typeof entry.appAttempt !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(entry.appAttempt)) || entry.clientMetroPort !== void 0 && (typeof entry.clientMetroPort !== "number" || !Number.isInteger(entry.clientMetroPort) || entry.clientMetroPort < 1 || entry.clientMetroPort > 65535 || entry.platform !== "android") || entry.metroPort !== void 0 && (typeof entry.metroPort !== "number" || !Number.isInteger(entry.metroPort) || entry.metroPort < 1 || entry.metroPort > 65535)) throw new Error(`Malformed hosted session record: ${root}`);
3303
4160
  ids.add(entry.id);
3304
4161
  return entry;
3305
4162
  });
@@ -3309,16 +4166,30 @@ function readHostedDevice(home, platform = "ios") {
3309
4166
  if (!device) throw new Error("The hosted device record is malformed.");
3310
4167
  return device;
3311
4168
  }
3312
- /** Hosted sessions use isolated homes: only one exact device of their platform may appear in this ledger. */
3313
- function assertHostedDeviceLedger(home, id, platform = "ios") {
4169
+ function readHostedDeviceLedger(home, options = {}) {
3314
4170
  let value;
3315
4171
  try {
3316
4172
  value = JSON.parse(readFileSync(join(home, "created-devices.json"), "utf8"));
3317
4173
  } catch (error) {
3318
- if (platform === "macos" && error.code === "ENOENT") return;
4174
+ if (!options.required && error.code === "ENOENT") return null;
3319
4175
  throw error;
3320
4176
  }
3321
- if (!isJsonObject(value) || value.version !== 1 || !Array.isArray(value.ios) || value.ios.length !== (platform === "ios" ? 1 : 0) || platform === "ios" && value.ios[0] !== id || !Array.isArray(value.android) || value.android.length !== (platform === "android" ? 1 : 0) || platform === "android" && value.android[0] !== id || !Array.isArray(value.web) || value.web.length !== 0) throw new Error("The hosted device ownership ledger is missing, malformed or names another device.");
4177
+ if (!isJsonObject(value) || value.version !== 1 || ![
4178
+ "ios",
4179
+ "android",
4180
+ "web"
4181
+ ].every((key) => Array.isArray(value[key]) && value[key].every((id) => typeof id === "string"))) throw new Error("The hosted device ownership ledger is malformed.");
4182
+ return value;
4183
+ }
4184
+ /** Hosted sessions use isolated homes: only one exact device of their platform may appear in this ledger. */
4185
+ function assertHostedDeviceLedger(home, id, platform = "ios", options = {}) {
4186
+ const value = readHostedDeviceLedger(home, { required: platform !== "macos" });
4187
+ if (!value && platform === "macos") return "empty";
4188
+ if (value && !value.ios.length && !value.android.length && !value.web.length) {
4189
+ if (platform === "macos" || options.allowEmpty) return "empty";
4190
+ }
4191
+ if (!value || value.ios.length !== (platform === "ios" ? 1 : 0) || platform === "ios" && value.ios[0] !== id || value.android.length !== (platform === "android" ? 1 : 0) || platform === "android" && value.android[0] !== id || value.web.length !== 0) throw new Error("The hosted device ownership ledger is missing, malformed or names another device.");
4192
+ return "listed";
3322
4193
  }
3323
4194
  //#endregion
3324
4195
  //#region state/hosted-app.ts
@@ -3331,6 +4202,41 @@ function hostedAppArea(session, attempt, workerHome) {
3331
4202
  if (workerHome) return join(workerHome, "..", "apps", attempt);
3332
4203
  return join(deviceHostArea(session), "apps", attempt);
3333
4204
  }
4205
+ function hostedAppBlobs(session, workerHome) {
4206
+ return join(workerHome ? join(workerHome, "..") : deviceHostArea(session), "blobs");
4207
+ }
4208
+ const verifiedBlobs = /* @__PURE__ */ new Map();
4209
+ /** Transfers cached verification after a rename, only for the same inode, size, mtime and digest. */
4210
+ function rememberHostedAppBlobRename(from, to, file) {
4211
+ const previous = verifiedBlobs.get(from);
4212
+ verifiedBlobs.delete(from);
4213
+ const stat = lstatSync(to, { throwIfNoEntry: false });
4214
+ if (!previous || !stat?.isFile() || stat.size !== file.size) return;
4215
+ const prefix = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:`;
4216
+ if (previous.startsWith(prefix) && previous.endsWith(`:${file.sha256}`)) verifiedBlobs.set(to, `${prefix}${stat.ctimeMs}:${file.sha256}`);
4217
+ }
4218
+ /** Checks regular blob bytes, including their size, before content-addressed reuse. */
4219
+ function validHostedAppBlob(path, file) {
4220
+ const stat = lstatSync(path, { throwIfNoEntry: false });
4221
+ if (!stat?.isFile() || stat.size !== file.size) return false;
4222
+ const key = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${file.sha256}`;
4223
+ if (verifiedBlobs.get(path) === key) return true;
4224
+ const hash = createHash("sha256");
4225
+ const fd = openSync(path, "r");
4226
+ try {
4227
+ const buffer = Buffer.alloc(65536);
4228
+ let count;
4229
+ while (count = readSync(fd, buffer, 0, buffer.length, null)) hash.update(buffer.subarray(0, count));
4230
+ } finally {
4231
+ closeSync(fd);
4232
+ }
4233
+ const valid = hash.digest("hex") === file.sha256;
4234
+ if (valid) {
4235
+ if (verifiedBlobs.size >= 20001) verifiedBlobs.delete(verifiedBlobs.keys().next().value);
4236
+ verifiedBlobs.set(path, key);
4237
+ } else verifiedBlobs.delete(path);
4238
+ return valid;
4239
+ }
3334
4240
  /** At most 32 plain arguments of 1024 characters (8192 in total), with no NUL, CR or LF. */
3335
4241
  function validHostedAppArguments(value) {
3336
4242
  return Array.isArray(value) && value.length <= 32 && value.every((argument) => typeof argument === "string" && argument.length <= 1024 && !/[\0\r\n]/.test(argument)) && value.reduce((total, argument) => total + argument.length, 0) <= 8192;
@@ -3405,7 +4311,8 @@ function readHostedAppMetadata(session, attempt, workerHome) {
3405
4311
  }
3406
4312
  function readHostedApp(session, attempt, workerHome) {
3407
4313
  const record = readHostedAppMetadata(session, attempt, workerHome);
3408
- const manifest = join(hostedAppArea(session, attempt, workerHome), "blobs", record.manifest.sha256);
4314
+ const manifest = join(hostedAppBlobs(session, workerHome), record.manifest.sha256);
4315
+ if (existsSync(manifest) && !validHostedAppBlob(manifest, record.manifest)) throw new Error("The hosted app manifest differs from its digest.");
3409
4316
  const files = existsSync(manifest) ? parseHostedAppManifest(JSON.parse(readFileSync(manifest, "utf8"))) : [];
3410
4317
  if (!files || !files.length && record.state !== "receiving") throw new Error("The hosted app manifest is missing or malformed.");
3411
4318
  return {
@@ -3435,7 +4342,11 @@ function readDeviceHostMachines() {
3435
4342
  "deviceToken",
3436
4343
  "requestedAt"
3437
4344
  ].every((key) => typeof entry[key] === "string" && entry[key]) || entry.state !== "pending" && entry.state !== "approved" || machines.some((each) => each.machine === entry.machine)) throw new Error("Invalid device-host machine credential store");
3438
- machines.push(entry);
4345
+ const { ticketHash, ...credential } = entry;
4346
+ machines.push({
4347
+ ...credential,
4348
+ ...typeof ticketHash === "string" ? { ticketHash } : {}
4349
+ });
3439
4350
  }
3440
4351
  return machines;
3441
4352
  }
@@ -3524,64 +4435,117 @@ function macosAppState(record) {
3524
4435
  };
3525
4436
  }
3526
4437
  //#endregion
3527
- //#region state/tailnet.ts
3528
- const DEFAULT_SERVE_PORT = 7443;
3529
- function parseMachine(entry) {
3530
- const match = /^([A-Za-z0-9][A-Za-z0-9.-]*?)(?::(\d{1,5}))?$/.exec(entry.trim());
3531
- if (!match) return null;
3532
- const port = match[2] === void 0 ? DEFAULT_SERVE_PORT : Number(match[2]);
3533
- return port >= 1 && port <= 65535 ? {
3534
- name: match[1].toLowerCase(),
3535
- port
3536
- } : null;
4438
+ //#region state/hosted-native.ts
4439
+ function parseHostedNativePlacement(value, platform) {
4440
+ if (!isJsonObject(value) || typeof value.machine !== "string" || !parseMachine(value.machine) || typeof value.session !== "string" || !/^[a-f0-9-]{36}$/.test(value.session)) return null;
4441
+ return {
4442
+ machine: value.machine,
4443
+ session: value.session,
4444
+ appAttempt: typeof value.appAttempt === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value.appAttempt) ? value.appAttempt : "",
4445
+ selected: typeof value.selected === "string" && parseMachine(value.selected) ? value.selected : value.machine,
4446
+ ...typeof value.reason === "string" ? { reason: value.reason } : {},
4447
+ device: parseHostedPlatformDevice(value.device, platform),
4448
+ agent: parseHostedAgentAccess(value.agent) ?? {
4449
+ driver: "none",
4450
+ setting: "hosting.agentDriver"
4451
+ }
4452
+ };
3537
4453
  }
3538
- /**
3539
- * The one peer in `tailscale status --json` whose MagicDNS name is `name` or starts with `name.`. `ID` there is
3540
- * the node's StableID, the same value `tailscale whois` reports as `Node.StableID`.
3541
- */
3542
- function findPeer(status, name) {
3543
- const found = (isJsonObject(status) && isJsonObject(status.Peer) ? Object.values(status.Peer) : []).flatMap((peer) => {
3544
- if (!isJsonObject(peer) || typeof peer.ID !== "string" || typeof peer.DNSName !== "string") return [];
3545
- const dnsName = peer.DNSName.replace(/\.$/, "").toLowerCase();
3546
- const addresses = Array.isArray(peer.TailscaleIPs) ? peer.TailscaleIPs.filter((ip) => typeof ip === "string") : [];
3547
- const address = addresses.find((ip) => !ip.includes(":")) ?? addresses[0];
3548
- if (!address || !(dnsName === name || dnsName.startsWith(`${name}.`))) return [];
3549
- return [{
3550
- nodeId: peer.ID,
3551
- dnsName,
3552
- address
3553
- }];
3554
- });
3555
- if (found.length === 0) return "missing";
3556
- return found.length === 1 ? found[0] : "ambiguous";
4454
+ function hostedNativeRecords(state, platform) {
4455
+ const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
4456
+ const records = {
4457
+ default: state?.[platform],
4458
+ ...Object.fromEntries(Object.entries(slots).map(([slot, value]) => [slot, isJsonObject(value) ? value[platform] : void 0]))
4459
+ };
4460
+ return Object.fromEntries(Object.entries(records).flatMap(([slot, record]) => isJsonObject(record) && record.host !== void 0 ? [[slot, record.host]] : []));
3557
4461
  }
3558
- /**
3559
- * Connects to the pinned node's own tailnet address rather than resolving its name, so the socket reaches the
3560
- * node whose ID was just checked. `tailscale serve` still needs the MagicDNS name for TLS (SNI and certificate)
3561
- * and for its Host routing.
3562
- */
3563
- function endpoint(peer, port) {
3564
- const address = peer.address.includes(":") ? `[${peer.address}]` : peer.address;
3565
- const suffix = port === 443 ? "" : `:${port}`;
4462
+ function unreadableHostedNative(slot, platform) {
4463
+ const key = slot === "default" ? `${platform}.host` : `deviceSlots.${slot}.${platform}.host`;
4464
+ return `The hosted ${platform === "ios" ? "iOS" : "Android"} placement at workspace-state key ${key} is unreadable. Inspect ${key}.machine and ${key}.session before reconciling it.`;
4465
+ }
4466
+ //#endregion
4467
+ //#region state/hosted-ios.ts
4468
+ function parseHostedIosPlacement(value) {
4469
+ return parseHostedNativePlacement(value, "ios");
4470
+ }
4471
+ function hostedIosStatus(placement) {
3566
4472
  return {
3567
- url: `wss://${address}${suffix}`,
3568
- servername: peer.dnsName,
3569
- host: `${peer.dnsName}${suffix}`
4473
+ machine: placement.machine,
4474
+ session: placement.session,
4475
+ selected: placement.selected,
4476
+ ...placement.reason ? { reason: placement.reason } : {},
4477
+ agent: placement.agent,
4478
+ device: placement.device ? {
4479
+ name: placement.device.deviceType,
4480
+ runtime: placement.device.runtime
4481
+ } : null
3570
4482
  };
3571
4483
  }
3572
- /**
3573
- * The endpoint of a paired machine's pinned node, or why Stim does not connect to it. The name must still
3574
- * resolve to exactly the pinned node; the token is never sent anywhere else.
3575
- */
3576
- function pinnedEndpoint(credential, current) {
3577
- const parsed = parseMachine(credential.machine);
3578
- if (!parsed) return `${credential.machine} is not a tailnet name`;
3579
- if (!isJsonObject(current)) return "Tailscale is not running";
3580
- const peer = findPeer(current, parsed.name);
3581
- if (peer === "missing") return `no peer on this tailnet is named ${parsed.name}`;
3582
- if (peer === "ambiguous") return `several tailnet peers match ${parsed.name}`;
3583
- if (peer.nodeId !== credential.nodeId) return `${peer.dnsName} is now node ${peer.nodeId}, not the pinned ${credential.nodeId}; run stim doctor`;
3584
- return endpoint(peer, parsed.port);
4484
+ function hostedIosRecords(state) {
4485
+ return hostedNativeRecords(state, "ios");
4486
+ }
4487
+ function unreadableHostedIos(slot) {
4488
+ return unreadableHostedNative(slot, "ios");
4489
+ }
4490
+ function hostedIosPlacements(state) {
4491
+ return Object.assign(Object.create(null), Object.fromEntries(Object.entries(hostedIosRecords(state)).flatMap(([slot, record]) => {
4492
+ const placement = parseHostedIosPlacement(record);
4493
+ return placement ? [[slot, placement]] : [];
4494
+ })));
4495
+ }
4496
+ //#endregion
4497
+ //#region state/hosted-android.ts
4498
+ function parseHostedAndroidPlacement(value) {
4499
+ return parseHostedNativePlacement(value, "android");
4500
+ }
4501
+ function hostedAndroidStatus(placement) {
4502
+ return {
4503
+ machine: placement.machine,
4504
+ session: placement.session,
4505
+ selected: placement.selected,
4506
+ ...placement.reason ? { reason: placement.reason } : {},
4507
+ agent: placement.agent,
4508
+ device: placement.device ? {
4509
+ name: hostedAndroidDeviceName(placement.device),
4510
+ systemImage: placement.device.systemImage,
4511
+ api: Number(/^system-images;android-(\d+);/.exec(placement.device.systemImage)?.[1])
4512
+ } : null
4513
+ };
4514
+ }
4515
+ function hostedAndroidRecords(state) {
4516
+ return hostedNativeRecords(state, "android");
4517
+ }
4518
+ function unreadableHostedAndroid(slot) {
4519
+ return unreadableHostedNative(slot, "android");
4520
+ }
4521
+ function hostedAndroidPlacements(state) {
4522
+ return Object.assign(Object.create(null), Object.fromEntries(Object.entries(hostedAndroidRecords(state)).flatMap(([slot, record]) => {
4523
+ const placement = parseHostedAndroidPlacement(record);
4524
+ return placement ? [[slot, placement]] : [];
4525
+ })));
4526
+ }
4527
+ function hostedAndroidDeviceName(device) {
4528
+ return `${device.deviceProfile} (API ${/^system-images;android-(\d+);/.exec(device.systemImage)?.[1]})`;
4529
+ }
4530
+ //#endregion
4531
+ //#region state/hosted-metro.ts
4532
+ const HOSTED_METRO_REQUESTS_KEY = "hostedMetroRequests";
4533
+ const HOSTED_METRO_GATEWAYS_KEY = "hostedMetroGateways";
4534
+ function hostedMetroRequests(state) {
4535
+ const raw = state?.[HOSTED_METRO_REQUESTS_KEY];
4536
+ if (!isJsonObject(raw)) return {};
4537
+ return Object.fromEntries(Object.entries(raw).filter((entry) => {
4538
+ const [session, value] = entry;
4539
+ return /^[a-f0-9-]{36}$/.test(session) && isJsonObject(value) && typeof value.id === "string" && typeof value.machine === "string" && typeof value.address === "string" && isIP(value.address) !== 0 && typeof value.peer === "string" && isIP(value.peer) !== 0 && typeof value.secret === "string" && /^[a-f0-9]{64}$/.test(value.secret);
4540
+ }));
4541
+ }
4542
+ function hostedMetroGateways(state) {
4543
+ const raw = state?.[HOSTED_METRO_GATEWAYS_KEY];
4544
+ if (!isJsonObject(raw)) return {};
4545
+ return Object.fromEntries(Object.entries(raw).filter((entry) => {
4546
+ const value = entry[1];
4547
+ return isJsonObject(value) && typeof value.id === "string" && typeof value.processToken === "string" && (value.port === void 0 || typeof value.port === "number" && Number.isInteger(value.port) && value.port > 0 && value.port <= 65535) && (value.error === void 0 || typeof value.error === "string");
4548
+ }));
3585
4549
  }
3586
4550
  //#endregion
3587
4551
  //#region state/hosted-logs.ts
@@ -3606,6 +4570,25 @@ function hostedMacosLogsDir(home) {
3606
4570
  return null;
3607
4571
  }
3608
4572
  }
4573
+ const hostedNativeLogsDir = (home, platform) => join(home, `${platform}-logs`);
4574
+ /** The completed query window and overlap digests, or null for a missing, malformed or unreadable checkpoint. */
4575
+ function readHostedNativeLogsCheckpoint(home, platform = "ios") {
4576
+ try {
4577
+ const value = JSON.parse(readFileSync(join(hostedNativeLogsDir(home, platform), "checkpoint.json"), "utf8"));
4578
+ if (!isJsonObject(value) || !Number.isSafeInteger(value.until) || value.until < 0 || value.windowMs !== void 0 && (!Number.isSafeInteger(value.windowMs) || value.windowMs < 1e3) || !Array.isArray(value.boundary) || !value.boundary.every((each) => typeof each === "string" && /^[a-f0-9]{64}$/.test(each))) return null;
4579
+ return {
4580
+ until: value.until,
4581
+ boundary: value.boundary,
4582
+ ...typeof value.appAttempt === "string" && Number.isSafeInteger(value.pid) && value.pid > 0 ? {
4583
+ appAttempt: value.appAttempt,
4584
+ pid: value.pid
4585
+ } : {},
4586
+ ...typeof value.windowMs === "number" ? { windowMs: value.windowMs } : {}
4587
+ };
4588
+ } catch {
4589
+ return null;
4590
+ }
4591
+ }
3609
4592
  function readSpan(path, from, to) {
3610
4593
  const fd = openSync(path, "r");
3611
4594
  try {
@@ -3675,4 +4658,221 @@ function readLogsSince(dir, cursor) {
3675
4658
  };
3676
4659
  }
3677
4660
  //#endregion
3678
- export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, DEVICE_IDLE_SHUTDOWN_KEY, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, MEMORY_SOURCES, NATIVE_BUILD_STEPS, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, TUNNEL_MODES, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentSessionOf, agentSessionsCacheFile, allMetroPorts, assertHostedDeviceLedger, attachExpoErrorContext, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createdDevicesFile, createdDevicesLock, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectByMetroPort, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAppArea, hostedAppAttempt, hostedConsolePort, hostedDeviceId, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCapacity, macosAppState, markerWindow, offloadSummary, openSegmentName, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMacosRecord, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSince, pinnedEndpoint, pullRequestCacheDir, pullRequestCacheFile, queryLogs, readAgentSessionsCache, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDiskUsage, readEasSessionLedger, readEndedAgentSessions, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedMacosApp, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogsSince, readMacosRecord, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readStateFile, readStats, readStatsReport, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, saturation, settingDefinition, settingValueError, settingsJsonSchema, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, stimBuildDigest, supervisorLogFile, supervisorPidFile, tailRead, trimSamples, tryAcquireBuildSlotClaim, validEasSessionClaim, validHostedAppArguments, wholePhases, withConfigLock, withDerivedFacts, withGitChip, withStateReadCache, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock };
4661
+ //#region state/maintenance.ts
4662
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
4663
+ const nullableNumber = (value) => value === null || finite(value);
4664
+ const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
4665
+ const KINDS = [
4666
+ "would-clear-outputs",
4667
+ "would-trim-cache",
4668
+ "would-empty-cache",
4669
+ "would-shutdown-device",
4670
+ "would-stop-workspace"
4671
+ ];
4672
+ /** Rejects missing, corrupt or unsupported payloads without throwing. */
4673
+ function parseMaintenanceState(value) {
4674
+ if (!isJsonObject(value) || value.version !== 1 || !isJsonObject(value.lastAt)) return null;
4675
+ if (!Object.values(value.lastAt).every(finite)) return null;
4676
+ if (value.skipKeys !== void 0 && !strings(value.skipKeys)) return null;
4677
+ if (value.deferredAt !== void 0 && (!isJsonObject(value.deferredAt) || !Object.values(value.deferredAt).every(finite))) return null;
4678
+ if (!Array.isArray(value.sizes) || !value.sizes.every((size) => isJsonObject(size) && typeof size.name === "string" && typeof size.dir === "string" && finite(size.bytes) && finite(size.measuredAt) && [
4679
+ "workspace-outputs",
4680
+ "build-cache",
4681
+ "metro-cache",
4682
+ "compilation-cache",
4683
+ "ccache",
4684
+ "other"
4685
+ ].includes(String(size.category)) && (size.workspace === void 0 || typeof size.workspace === "string") && (size.blocked === void 0 || typeof size.blocked === "string") && (size.idleDays === void 0 || nullableNumber(size.idleDays)))) return null;
4686
+ if (!Array.isArray(value.plan) || !value.plan.every((action) => isJsonObject(action) && KINDS.includes(String(action.kind)) && typeof action.target === "string" && finite(action.bytes) && typeof action.reason === "string" && (action.workspace === void 0 || typeof action.workspace === "string"))) return null;
4687
+ if (!Array.isArray(value.recent) || !value.recent.every((record) => isJsonObject(record) && record.src === "maintenance" && finite(record.ts) && typeof record.msg === "string" && [
4688
+ "debug",
4689
+ "info",
4690
+ "warn",
4691
+ "error"
4692
+ ].includes(String(record.level)) && [
4693
+ "maintenance_pass",
4694
+ "maintenance_action",
4695
+ "maintenance_skip",
4696
+ "maintenance_failure",
4697
+ "maintenance_check"
4698
+ ].includes(String(record.event)) && typeof record.pass === "string" && typeof record.trigger === "string" && record.mode === "report")) return null;
4699
+ if (value.lastPass !== null && (!isJsonObject(value.lastPass) || !finite(value.lastPass.startedAt) || !finite(value.lastPass.durationMs) || typeof value.lastPass.trigger !== "string" || value.lastPass.mode !== "report" || value.lastPass.freedBytes !== 0 || value.lastPass.stopped !== 0 || !finite(value.lastPass.actions) || !strings(value.lastPass.blocked))) return null;
4700
+ if (value.pressure !== null) {
4701
+ const pressure = value.pressure;
4702
+ if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) || !isJsonObject(pressure.memory) || ![
4703
+ null,
4704
+ "normal",
4705
+ "warning",
4706
+ "critical"
4707
+ ].includes(pressure.memory.level) || !nullableNumber(pressure.memory.availableBytes) || typeof pressure.memory.pressured !== "boolean") return null;
4708
+ }
4709
+ return value;
4710
+ }
4711
+ function readMaintenanceState() {
4712
+ return parseMaintenanceState(readJsonObject(maintenanceStateFile()));
4713
+ }
4714
+ function readMaintenanceAttempt() {
4715
+ const value = readJsonObject(maintenanceAttemptFile())?.attemptedAt;
4716
+ return finite(value) ? value : void 0;
4717
+ }
4718
+ //#endregion
4719
+ //#region state/agent-device.ts
4720
+ function agentDeviceStateDirs(home = homedir()) {
4721
+ const override = process.env.AGENT_DEVICE_STATE_DIR?.trim();
4722
+ const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
4723
+ const workspaces = join(configDir(), "workspaces");
4724
+ try {
4725
+ for (const name of readdirSync(workspaces)) {
4726
+ const dir = join(workspaces, name, "agent-device");
4727
+ try {
4728
+ if (statSync(dir).isDirectory()) dirs.add(dir);
4729
+ } catch {}
4730
+ }
4731
+ } catch {}
4732
+ return [...dirs];
4733
+ }
4734
+ //#endregion
4735
+ //#region state/archive.ts
4736
+ function readArchives() {
4737
+ let names;
4738
+ try {
4739
+ names = readdirSync(archiveRoot());
4740
+ } catch {
4741
+ return [];
4742
+ }
4743
+ return names.flatMap((name) => {
4744
+ const archive = readArchive(name);
4745
+ return archive ? [archive] : [];
4746
+ }).toSorted((a, b) => Date.parse(b.removedAt) - Date.parse(a.removedAt) || b.id.localeCompare(a.id));
4747
+ }
4748
+ function readArchive(id) {
4749
+ if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
4750
+ try {
4751
+ if (!lstatSync(join(archiveRoot(), id)).isDirectory()) return null;
4752
+ if (!lstatSync(join(archiveRoot(), id, "archive.json")).isFile()) return null;
4753
+ } catch {
4754
+ return null;
4755
+ }
4756
+ const record = readJsonObject(join(archiveRoot(), id, "archive.json"));
4757
+ if (!record || record.id !== id || [
4758
+ "projectRoot",
4759
+ "project",
4760
+ "workspace",
4761
+ "removedAt",
4762
+ "removedBy"
4763
+ ].some((key) => typeof record[key] !== "string") || !Number.isFinite(Date.parse(record.removedAt)) || !isJsonObject(record.bytes) || !isJsonObject(record.expires) || !isJsonObject(record.worktree) || !isJsonObject(record.builds) || !Array.isArray(record.agents) || typeof record.version !== "number") return null;
4764
+ const bytes = record.bytes;
4765
+ if ([
4766
+ "logs",
4767
+ "recordings",
4768
+ "agentActions",
4769
+ "record",
4770
+ "total"
4771
+ ].some((key) => typeof bytes[key] !== "number" || !Number.isFinite(bytes[key]) || bytes[key] < 0)) return null;
4772
+ return record;
4773
+ }
4774
+ function readArchiveState(id) {
4775
+ if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
4776
+ const file = join(archiveRoot(), id, "state.json");
4777
+ try {
4778
+ if (!lstatSync(join(archiveRoot(), id)).isDirectory()) return null;
4779
+ if (!lstatSync(file).isFile()) return null;
4780
+ } catch {
4781
+ return null;
4782
+ }
4783
+ return readJsonObject(file);
4784
+ }
4785
+ function archivedUsage(archives) {
4786
+ const usage = {
4787
+ count: archives.length,
4788
+ bytes: 0,
4789
+ byKind: {
4790
+ logs: 0,
4791
+ recordings: 0,
4792
+ agentActions: 0,
4793
+ record: 0
4794
+ }
4795
+ };
4796
+ for (const archive of archives) {
4797
+ usage.bytes += archive.bytes.total;
4798
+ for (const kind of [
4799
+ "logs",
4800
+ "recordings",
4801
+ "agentActions",
4802
+ "record"
4803
+ ]) usage.byKind[kind] += archive.bytes[kind];
4804
+ }
4805
+ return usage;
4806
+ }
4807
+ function canonical(path) {
4808
+ try {
4809
+ return realpathSync(path);
4810
+ } catch {
4811
+ return path;
4812
+ }
4813
+ }
4814
+ function linkReplacedArchives(archives, paths) {
4815
+ const live = new Map(paths.map((path) => [canonical(path), path]));
4816
+ return archives.map((archive) => {
4817
+ const replacedBy = live.get(canonical(archive.projectRoot));
4818
+ return replacedBy ? {
4819
+ ...archive,
4820
+ replacedBy
4821
+ } : archive;
4822
+ });
4823
+ }
4824
+ function archiveEnabledValue(layer) {
4825
+ if (!layer || typeof layer !== "object") return void 0;
4826
+ return layer.archive?.enabled;
4827
+ }
4828
+ function archiveEnabled(env, layers) {
4829
+ const definition = settingDefinition("archive.enabled");
4830
+ const override = env[definition.env];
4831
+ if ((!override || override === "") && env.STIM_HOME) return definition.scopedHomeValue;
4832
+ const value = override !== void 0 && override !== "" ? coerceSettingText(definition, override) : layers.map(archiveEnabledValue).find((candidate) => candidate !== void 0) ?? definition.default;
4833
+ const error = settingValueError(definition, value);
4834
+ if (error) throw new Error(`Invalid ${override !== void 0 && override !== "" ? definition.env : definition.key}: ${error}`);
4835
+ return value;
4836
+ }
4837
+ //#endregion
4838
+ //#region state/setup-journal.ts
4839
+ const capability = (value) => value === "build" || value === "device-host";
4840
+ const text = (value) => typeof value === "string" && value.length > 0;
4841
+ const knownKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
4842
+ function parseSetupJournal(value) {
4843
+ if (!isJsonObject(value) || !knownKeys(value, [
4844
+ "v",
4845
+ "client",
4846
+ "expiresAt",
4847
+ "capabilities",
4848
+ "steps",
4849
+ "granted",
4850
+ "done",
4851
+ "exit"
4852
+ ]) || value.v !== 1 || !isJsonObject(value.client) || !knownKeys(value.client, ["nodeId"]) || !text(value.client.nodeId) || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || !Array.isArray(value.capabilities) || !value.capabilities.every(capability) || !Array.isArray(value.steps) || !value.steps.every((step) => isJsonObject(step) && knownKeys(step, [
4853
+ "id",
4854
+ "state",
4855
+ "title",
4856
+ "detail",
4857
+ "fix"
4858
+ ]) && text(step.id) && typeof step.state === "string" && [
4859
+ "pending",
4860
+ "running",
4861
+ "ok",
4862
+ "skipped",
4863
+ "failed"
4864
+ ].includes(step.state) && text(step.title) && (step.detail === void 0 || typeof step.detail === "string") && (step.fix === void 0 || typeof step.fix === "string")) || !Array.isArray(value.granted) || !value.granted.every((grant) => isJsonObject(grant) && knownKeys(grant, ["capability", "id"]) && capability(grant.capability) && text(grant.id)) || typeof value.done !== "boolean" || value.exit !== void 0 && (!Number.isInteger(value.exit) || value.exit < 0)) return null;
4865
+ return value;
4866
+ }
4867
+ function readSetupJournal(hash) {
4868
+ const file = setupJournalFile(hash);
4869
+ return file === null ? null : parseSetupJournal(readJsonObject(file));
4870
+ }
4871
+ function isJournalExpired(journal, now = Date.now()) {
4872
+ return Date.parse(journal.expiresAt) < now;
4873
+ }
4874
+ function isSetupJournalPrunable(journal, now) {
4875
+ return Date.parse(journal.expiresAt) + 36e5 < now;
4876
+ }
4877
+ //#endregion
4878
+ export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, CAPACITY_REFUSAL_LIMIT, CAPACITY_REFUSAL_MAX_AGE_MS, DEVICE_IDLE_SHUTDOWN_KEY, DEVICE_RECLAIM_REASON, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_METRO_GATEWAYS_KEY, HOSTED_METRO_REQUESTS_KEY, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, MEMORY_SOURCES, NATIVE_BUILD_STEPS, NATIVE_CRASH_LOG_PREFIX, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, TAILNET_MACHINE_PATTERN, TUNNEL_MODES, TUTORIAL_VERSION, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentDeviceStateDirs, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createdDevicesFile, createdDevicesLock, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectByMetroPort, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCapacity, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, pinnedEndpoint, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readEasSessionLedger, readEndedAgentSessions, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, saturation, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedAndroid, unreadableHostedIos, unreadableHostedNative, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, wholePhases, withConfigLock, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock };