@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/README.md +3 -0
- package/dist/{core-BwaxUjLX.mjs → core-DWOUn9xp.mjs} +58 -12
- package/dist/{index-DgLuTAvh.d.mts → index-DPp8a4-9.d.mts} +681 -166
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -1
- package/dist/phone-protocol.d.mts +45 -6
- package/dist/{protocol-BjnE921M.d.mts → protocol-CfT6YiG_.d.mts} +85 -13
- package/dist/protocol.d.mts +2 -2
- package/dist/protocol.mjs +17 -2
- package/dist/receive-protocol.mjs +11436 -7753
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +1294 -94
- package/package.json +1 -1
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-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
4174
|
+
if (!options.required && error.code === "ENOENT") return null;
|
|
3319
4175
|
throw error;
|
|
3320
4176
|
}
|
|
3321
|
-
if (!isJsonObject(value) || value.version !== 1 || !
|
|
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(
|
|
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
|
-
|
|
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/
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
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
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
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
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
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
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
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
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
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
|
-
|
|
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 };
|