@stim-cli/core 1.20.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{core-CzCQ9yWY.mjs → core-DisO3hs1.mjs} +257 -60
- package/dist/{index-DNBFsz-Q.d.mts → index-_wkluvP_.d.mts} +341 -90
- package/dist/index.d.mts +70 -5
- package/dist/index.mjs +3 -3
- package/dist/keyed-delta-BBoOeR2J.mjs +289 -0
- package/dist/noise-DYIbymAJ.d.mts +100 -0
- package/dist/noise-session.d.mts +164 -0
- package/dist/noise-session.mjs +402 -0
- package/dist/noise.d.mts +2 -0
- package/dist/noise.mjs +393 -0
- package/dist/{ownership-claim-Xxxr9MPp.d.mts → ownership-claim-Bf8Lq6AY.d.mts} +2 -1
- package/dist/{ownership-claim-O31Y5iIb.mjs → ownership-claim-DHUs_Dir.mjs} +1 -1
- package/dist/ownership-claim.d.mts +2 -2
- package/dist/ownership-claim.mjs +2 -2
- package/dist/phone-protocol.d.mts +85 -16
- package/dist/phone-protocol.mjs +2 -1
- package/dist/{protocol-Bp-bcZIR.d.mts → protocol-D8R_3dkO.d.mts} +307 -65
- package/dist/protocol.d.mts +2 -2
- package/dist/protocol.mjs +50 -10
- package/dist/receive-protocol.d.mts +1 -1
- package/dist/receive-protocol.mjs +18062 -12744
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +1140 -334
- package/dist/toolchain.d.mts +64 -0
- package/dist/toolchain.mjs +326 -0
- package/dist/workspace-link.d.mts +16 -0
- package/dist/workspace-link.mjs +50 -0
- package/package.json +21 -1
package/dist/state.mjs
CHANGED
|
@@ -1,15 +1,75 @@
|
|
|
1
|
-
import { _ as releaseClaim, f as isClaimRefusal, g as readClaimSet
|
|
1
|
+
import { S as tryAcquireClaim, _ as releaseClaim, f as isClaimRefusal, g as readClaimSet } from "./ownership-claim-DHUs_Dir.mjs";
|
|
2
2
|
import { inspectProcessIdentity } from "./process-identity.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { C as workspaceName, G as withStateReadCache, H as readJsonFile, K as withDirLock, L as ambientContext, R as ambientEnv, T as workspaceStateDir, U as readJsonObject, V as isJsonObject, W as readStateFile, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as withDirLockAsync, t as LOG_ROTATE_BYTES, u as configDir } from "./core-DisO3hs1.mjs";
|
|
4
|
+
import { readOrCreateKeyFile } from "./noise-session.mjs";
|
|
5
|
+
import { validHomeId } from "./workspace-link.mjs";
|
|
6
|
+
import { a as workspaceSummary, i as statusViews, n as applyKeyedDelta, o as withDerivedFacts, r as SUMMARY_ARCHIVES, s as withGitChip, t as KeyedEntities } from "./keyed-delta-BBoOeR2J.mjs";
|
|
7
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
5
8
|
import { availableParallelism, homedir, loadavg } from "node:os";
|
|
6
|
-
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
7
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { setImmediate as setImmediate$1 } from "node:timers/promises";
|
|
12
|
+
import { open, readFile, readdir, stat } from "node:fs/promises";
|
|
8
13
|
import { isIP } from "node:net";
|
|
9
14
|
import { StringDecoder } from "node:string_decoder";
|
|
15
|
+
//#region state/log-selection.ts
|
|
16
|
+
var BoundedSelection = class {
|
|
17
|
+
first;
|
|
18
|
+
compare;
|
|
19
|
+
tail;
|
|
20
|
+
constructor(compare, tail, first = false) {
|
|
21
|
+
this.first = first;
|
|
22
|
+
this.compare = compare;
|
|
23
|
+
this.tail = tail;
|
|
24
|
+
}
|
|
25
|
+
before(a, b) {
|
|
26
|
+
return (this.first ? this.compare(b, a) : this.compare(a, b)) < 0;
|
|
27
|
+
}
|
|
28
|
+
items = [];
|
|
29
|
+
get size() {
|
|
30
|
+
return this.items.length;
|
|
31
|
+
}
|
|
32
|
+
peek() {
|
|
33
|
+
return this.items[0];
|
|
34
|
+
}
|
|
35
|
+
keep(entry) {
|
|
36
|
+
if (this.tail !== void 0 && this.tail <= 0) return;
|
|
37
|
+
const items = this.items;
|
|
38
|
+
items.push(entry);
|
|
39
|
+
let i = items.length - 1;
|
|
40
|
+
while (i > 0) {
|
|
41
|
+
const parent = i - 1 >> 1;
|
|
42
|
+
if (!this.before(items[i], items[parent])) break;
|
|
43
|
+
[items[i], items[parent]] = [items[parent], items[i]];
|
|
44
|
+
i = parent;
|
|
45
|
+
}
|
|
46
|
+
if (this.tail !== void 0 && items.length > this.tail) this.pop();
|
|
47
|
+
}
|
|
48
|
+
sorted() {
|
|
49
|
+
return this.items.toSorted(this.compare);
|
|
50
|
+
}
|
|
51
|
+
pop() {
|
|
52
|
+
const items = this.items;
|
|
53
|
+
const last = items.pop();
|
|
54
|
+
if (!items.length) return;
|
|
55
|
+
items[0] = last;
|
|
56
|
+
let i = 0;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const left = 2 * i + 1;
|
|
59
|
+
const right = left + 1;
|
|
60
|
+
let least = i;
|
|
61
|
+
if (left < items.length && this.before(items[left], items[least])) least = left;
|
|
62
|
+
if (right < items.length && this.before(items[right], items[least])) least = right;
|
|
63
|
+
if (least === i) return;
|
|
64
|
+
[items[i], items[least]] = [items[least], items[i]];
|
|
65
|
+
i = least;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
10
70
|
//#region state/agent-actions.ts
|
|
11
71
|
const EVENTS_VERSION = 1;
|
|
12
|
-
function object$
|
|
72
|
+
function object$2(value) {
|
|
13
73
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
14
74
|
}
|
|
15
75
|
function parseAgentEvents(lines) {
|
|
@@ -21,7 +81,7 @@ function parseAgentEvents(lines) {
|
|
|
21
81
|
for (const line of lines) {
|
|
22
82
|
let entry;
|
|
23
83
|
try {
|
|
24
|
-
entry = object$
|
|
84
|
+
entry = object$2(JSON.parse(line));
|
|
25
85
|
} catch {
|
|
26
86
|
continue;
|
|
27
87
|
}
|
|
@@ -54,7 +114,7 @@ function parseAgentEvents(lines) {
|
|
|
54
114
|
command,
|
|
55
115
|
summary: typeof summary === "string" ? summary : "",
|
|
56
116
|
failed,
|
|
57
|
-
details: object$
|
|
117
|
+
details: object$2(entry.details),
|
|
58
118
|
requestId
|
|
59
119
|
});
|
|
60
120
|
}
|
|
@@ -172,41 +232,62 @@ function readCompleteLines(path, start) {
|
|
|
172
232
|
}
|
|
173
233
|
}
|
|
174
234
|
function trackMacosOpen(cursor, event, unknownVersion) {
|
|
175
|
-
const flags = object$
|
|
235
|
+
const flags = object$2(event.details?.flags);
|
|
176
236
|
if (!event.failed) cursor.macos = event.details?.platform === "macos" || flags?.platform === "macos";
|
|
177
237
|
cursor.bundleId = cursor.macos && !event.failed && !unknownVersion && flags?.surface === "app" && typeof event.details?.appBundleId === "string" ? event.details.appBundleId : null;
|
|
178
238
|
}
|
|
239
|
+
function agentRecordCollector(selection) {
|
|
240
|
+
const out = [];
|
|
241
|
+
const bounded = selection?.tail === void 0 ? null : new BoundedSelection((a, b) => Number(a.record.ts) - Number(b.record.ts) || a.position - b.position, selection.tail);
|
|
242
|
+
let position = 0;
|
|
243
|
+
const keep = (record) => {
|
|
244
|
+
if (selection && !selection.matches(record)) return;
|
|
245
|
+
if (bounded) bounded.keep({
|
|
246
|
+
record,
|
|
247
|
+
position: position++
|
|
248
|
+
});
|
|
249
|
+
else out.push(record);
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
keep,
|
|
253
|
+
records: () => bounded ? bounded.sorted().map(({ record }) => record) : out.toSorted((a, b) => a.ts - b.ts)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
179
256
|
/**
|
|
180
257
|
* Reads agent-device session actions incrementally from the supplied sessions directories. With targets,
|
|
181
258
|
* records are restricted to those devices; without targets, retained session metadata supplies attribution.
|
|
182
259
|
* Missing directories and unreadable files yield no records.
|
|
183
260
|
*/
|
|
184
|
-
function
|
|
261
|
+
function agentActionReads({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
|
|
185
262
|
const cursors = /* @__PURE__ */ new Map();
|
|
186
|
-
return ()
|
|
263
|
+
return function* (selection) {
|
|
187
264
|
const targets = typeof resolveTargets === "function" ? resolveTargets() : resolveTargets;
|
|
188
265
|
if (targets && !targets.length) return [];
|
|
189
266
|
const byId = new Map(targets?.map((target) => [target.id, target]));
|
|
190
267
|
const macosByBundle = new Map(targets?.flatMap((target) => target.platform === "macos" ? [[target.bundleId, target]] : []));
|
|
191
|
-
const sessionDirs =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
268
|
+
const sessionDirs = [];
|
|
269
|
+
for (const sessionsDir of typeof sessionsDirs === "function" ? sessionsDirs() : sessionsDirs) try {
|
|
270
|
+
const names = yield {
|
|
271
|
+
kind: "names",
|
|
272
|
+
path: sessionsDir
|
|
273
|
+
};
|
|
274
|
+
sessionDirs.push(...names.map((name) => ({
|
|
275
|
+
name,
|
|
276
|
+
sessionsDir,
|
|
277
|
+
dir: join(sessionsDir, name)
|
|
278
|
+
})));
|
|
279
|
+
} catch {}
|
|
280
|
+
const { keep, records } = agentRecordCollector(selection);
|
|
203
281
|
for (const { name, sessionsDir, dir } of sessionDirs) {
|
|
204
282
|
const eventsPath = join(dir, "events.ndjson");
|
|
205
283
|
const lines = [];
|
|
206
284
|
let cursor = cursors.get(dir);
|
|
207
285
|
if (!cursor) {
|
|
208
286
|
try {
|
|
209
|
-
if (
|
|
287
|
+
if ((yield {
|
|
288
|
+
kind: "stat",
|
|
289
|
+
path: eventsPath
|
|
290
|
+
}).mtimeMs < sinceTs) continue;
|
|
210
291
|
} catch {
|
|
211
292
|
continue;
|
|
212
293
|
}
|
|
@@ -223,19 +304,45 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
223
304
|
};
|
|
224
305
|
cursors.set(dir, cursor);
|
|
225
306
|
try {
|
|
226
|
-
lines.push(...
|
|
307
|
+
lines.push(...(yield {
|
|
308
|
+
kind: "text",
|
|
309
|
+
path: `${eventsPath}.1`
|
|
310
|
+
}).split("\n"));
|
|
227
311
|
} catch {}
|
|
228
312
|
}
|
|
229
|
-
let chunk =
|
|
313
|
+
let chunk = yield {
|
|
314
|
+
kind: "lines",
|
|
315
|
+
path: eventsPath,
|
|
316
|
+
start: cursor.offset
|
|
317
|
+
};
|
|
230
318
|
if (chunk && (chunk.size < cursor.offset || cursor.identity && chunk.identity !== cursor.identity)) {
|
|
231
319
|
cursor.bundleId = null;
|
|
232
|
-
chunk =
|
|
320
|
+
chunk = yield {
|
|
321
|
+
kind: "lines",
|
|
322
|
+
path: eventsPath,
|
|
323
|
+
start: 0
|
|
324
|
+
};
|
|
233
325
|
}
|
|
234
326
|
if (!chunk) continue;
|
|
235
327
|
cursor.identity = chunk.identity;
|
|
236
328
|
cursor.offset = chunk.next;
|
|
237
329
|
lines.push(...chunk.text.split("\n"));
|
|
238
|
-
const parsed =
|
|
330
|
+
const parsed = {
|
|
331
|
+
events: [],
|
|
332
|
+
started: [],
|
|
333
|
+
finished: [],
|
|
334
|
+
session: null,
|
|
335
|
+
unknownVersion: null
|
|
336
|
+
};
|
|
337
|
+
for (let offset = 0; offset < lines.length; offset += 500) {
|
|
338
|
+
const batch = parseAgentEvents(lines.slice(offset, offset + 500));
|
|
339
|
+
parsed.events.push(...batch.events);
|
|
340
|
+
parsed.started.push(...batch.started);
|
|
341
|
+
parsed.finished.push(...batch.finished);
|
|
342
|
+
parsed.session ??= batch.session;
|
|
343
|
+
parsed.unknownVersion ??= batch.unknownVersion;
|
|
344
|
+
yield { kind: "turn" };
|
|
345
|
+
}
|
|
239
346
|
for (const [requestId, at] of parsed.started) cursor.started.set(requestId, at);
|
|
240
347
|
cursor.session ??= parsed.session;
|
|
241
348
|
const session = cursor.session;
|
|
@@ -247,12 +354,18 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
247
354
|
const runnerPath = join(dir, "runner.log");
|
|
248
355
|
let runnerSize = -1;
|
|
249
356
|
try {
|
|
250
|
-
runnerSize =
|
|
357
|
+
runnerSize = (yield {
|
|
358
|
+
kind: "stat",
|
|
359
|
+
path: runnerPath
|
|
360
|
+
}).size;
|
|
251
361
|
} catch {}
|
|
252
362
|
if (cursor.runner?.size !== runnerSize) {
|
|
253
363
|
let text = "";
|
|
254
364
|
try {
|
|
255
|
-
text =
|
|
365
|
+
text = yield {
|
|
366
|
+
kind: "text",
|
|
367
|
+
path: runnerPath
|
|
368
|
+
};
|
|
256
369
|
} catch {}
|
|
257
370
|
cursor.runner = {
|
|
258
371
|
size: runnerSize,
|
|
@@ -273,11 +386,12 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
273
386
|
const target = !targets ? retainedTarget() : cursor.macos ? cursor.bundleId ? macosByBundle.get(cursor.bundleId) : void 0 : deviceId ? byId.get(deviceId) : void 0;
|
|
274
387
|
if (target) {
|
|
275
388
|
cursor.unknownReported = true;
|
|
276
|
-
|
|
389
|
+
keep(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
|
|
277
390
|
}
|
|
278
391
|
}
|
|
279
392
|
if (parsed.unknownVersion) cursor.bundleId = null;
|
|
280
|
-
for (const event of parsed.events) {
|
|
393
|
+
for (const [index, event] of parsed.events.entries()) {
|
|
394
|
+
if (index % 500 === 0) yield { kind: "turn" };
|
|
281
395
|
if (event.kind === "request.started") {
|
|
282
396
|
cursor.closing = event.command === "close" ? cursor.bundleId : null;
|
|
283
397
|
cursor.bundleId = null;
|
|
@@ -290,7 +404,7 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
290
404
|
cursor.closing = null;
|
|
291
405
|
const target = bundleId ? macosByBundle.get(bundleId) : void 0;
|
|
292
406
|
const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
|
|
293
|
-
if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt)
|
|
407
|
+
if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt) keep(agentRecord(event, target.id, target, startedAt));
|
|
294
408
|
if (event.command === "close") cursor.bundleId = null;
|
|
295
409
|
continue;
|
|
296
410
|
}
|
|
@@ -299,10 +413,99 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
299
413
|
const deviceId = !targets && cursor.platform === "android" ? null : deviceAt(event.ts);
|
|
300
414
|
const target = targets ? deviceId ? byId.get(deviceId) : void 0 : retainedTarget();
|
|
301
415
|
const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
|
|
302
|
-
if (target)
|
|
416
|
+
if (target) keep(agentRecord(event, deviceId || null, target, startedAt));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return records();
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function syncRead(request) {
|
|
423
|
+
switch (request.kind) {
|
|
424
|
+
case "names": return readdirSync(request.path);
|
|
425
|
+
case "stat": return statSync(request.path);
|
|
426
|
+
case "text": return readFileSync(request.path, "utf8");
|
|
427
|
+
case "lines": return readCompleteLines(request.path, request.start);
|
|
428
|
+
case "turn": return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Reads new agent-device actions synchronously, retaining attribution cursors between reads. */
|
|
432
|
+
function createAgentActionReader(options) {
|
|
433
|
+
const reads = agentActionReads(options);
|
|
434
|
+
return () => {
|
|
435
|
+
const iterator = reads();
|
|
436
|
+
let step = iterator.next();
|
|
437
|
+
while (!step.done) {
|
|
438
|
+
let value;
|
|
439
|
+
try {
|
|
440
|
+
value = syncRead(step.value);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
step = iterator.throw(error);
|
|
443
|
+
continue;
|
|
303
444
|
}
|
|
445
|
+
step = iterator.next(value);
|
|
304
446
|
}
|
|
305
|
-
return
|
|
447
|
+
return step.value;
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function asyncRead(request) {
|
|
451
|
+
switch (request.kind) {
|
|
452
|
+
case "names": return readdir(request.path);
|
|
453
|
+
case "stat": return stat(request.path);
|
|
454
|
+
case "text": return readFile(request.path, "utf8");
|
|
455
|
+
case "turn": return setImmediate$1();
|
|
456
|
+
case "lines": {
|
|
457
|
+
let handle;
|
|
458
|
+
try {
|
|
459
|
+
handle = await open(request.path, "r");
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const stats = await handle.stat({ bigint: true });
|
|
465
|
+
const size = Number(stats.size);
|
|
466
|
+
const identity = `${stats.dev}:${stats.ino}`;
|
|
467
|
+
if (size <= request.start) return {
|
|
468
|
+
text: "",
|
|
469
|
+
next: request.start,
|
|
470
|
+
size,
|
|
471
|
+
identity
|
|
472
|
+
};
|
|
473
|
+
const buffer = Buffer.alloc(size - request.start);
|
|
474
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, request.start);
|
|
475
|
+
const end = buffer.subarray(0, bytesRead).lastIndexOf(10);
|
|
476
|
+
return {
|
|
477
|
+
text: end < 0 ? "" : buffer.toString("utf8", 0, end + 1),
|
|
478
|
+
next: request.start + end + 1,
|
|
479
|
+
size,
|
|
480
|
+
identity
|
|
481
|
+
};
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
} finally {
|
|
485
|
+
await handle.close();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** Reads new agent-device actions asynchronously; selection applies only to matching output from this call. */
|
|
491
|
+
function createAsyncAgentActionReader(options, signal) {
|
|
492
|
+
const reads = agentActionReads(options);
|
|
493
|
+
return async (selection) => {
|
|
494
|
+
signal?.throwIfAborted();
|
|
495
|
+
const iterator = reads(selection);
|
|
496
|
+
let step = iterator.next();
|
|
497
|
+
while (!step.done) {
|
|
498
|
+
let value;
|
|
499
|
+
try {
|
|
500
|
+
value = await asyncRead(step.value);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
step = iterator.throw(error);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
signal?.throwIfAborted();
|
|
506
|
+
step = iterator.next(value);
|
|
507
|
+
}
|
|
508
|
+
return step.value;
|
|
306
509
|
};
|
|
307
510
|
}
|
|
308
511
|
//#endregion
|
|
@@ -314,7 +517,7 @@ function readAgentDeviceUsageCache() {
|
|
|
314
517
|
const value = readJsonObject(agentDeviceUsageFile());
|
|
315
518
|
if (!value || value.version !== 1) return null;
|
|
316
519
|
const { roots, ...usage } = value;
|
|
317
|
-
if (!validUsage(usage) || !validRoots(roots)) return null;
|
|
520
|
+
if (!validUsage$1(usage) || !validRoots(roots)) return null;
|
|
318
521
|
return {
|
|
319
522
|
usage,
|
|
320
523
|
roots
|
|
@@ -324,34 +527,34 @@ function readAgentDeviceUsage() {
|
|
|
324
527
|
const value = readJsonObject(agentDeviceUsageFile());
|
|
325
528
|
if (!value) return null;
|
|
326
529
|
const { roots: _roots, ...usage } = value;
|
|
327
|
-
return validUsage(usage) ? usage : null;
|
|
530
|
+
return validUsage$1(usage) ? usage : null;
|
|
328
531
|
}
|
|
329
|
-
function object(value) {
|
|
532
|
+
function object$1(value) {
|
|
330
533
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
331
534
|
}
|
|
332
|
-
function bytes(value) {
|
|
535
|
+
function bytes$1(value) {
|
|
333
536
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
334
537
|
}
|
|
335
538
|
function sized(value) {
|
|
336
|
-
return object(value) && typeof value.dir === "string" && bytes(value.bytes);
|
|
539
|
+
return object$1(value) && typeof value.dir === "string" && bytes$1(value.bytes);
|
|
337
540
|
}
|
|
338
541
|
function count$1(value) {
|
|
339
542
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
340
543
|
}
|
|
341
|
-
function nullableString(value) {
|
|
544
|
+
function nullableString$1(value) {
|
|
342
545
|
return value === null || typeof value === "string";
|
|
343
546
|
}
|
|
344
547
|
function validRoots(value) {
|
|
345
|
-
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");
|
|
548
|
+
return object$1(value) && typeof value.stateDir === "string" && typeof value.runnerRoot === "string" && typeof value.hostedDir === "string" && Array.isArray(value.workspaceDirs) && value.workspaceDirs.every((dir) => typeof dir === "string");
|
|
346
549
|
}
|
|
347
|
-
function validUsage(value) {
|
|
550
|
+
function validUsage$1(value) {
|
|
348
551
|
const { stateDir: state, runnerBuilds: runner, workspaces, hosted } = value;
|
|
349
|
-
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" && [
|
|
552
|
+
return value.version === 1 && typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) && typeof value.bytes === "number" && bytes$1(value.bytes) && typeof value.complete === "boolean" && sized(state) && typeof state.present === "boolean" && sized(state.sessions) && count$1(state.sessions.count) && sized(state.logs) && object$1(state.other) && bytes$1(state.other.bytes) && Array.isArray(state.other.largest) && state.other.largest.every((entry) => object$1(entry) && typeof entry.name === "string" && bytes$1(entry.bytes)) && sized(runner) && typeof runner.present === "boolean" && bytes$1(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$1(entry.lastUsedAt) && nullableString$1(entry.packageVersion) && nullableString$1(entry.xcodeBuildVersion) && typeof entry.inUse === "boolean" && [
|
|
350
553
|
null,
|
|
351
554
|
"lease",
|
|
352
555
|
"lock",
|
|
353
556
|
"unreadable"
|
|
354
|
-
].includes(entry.inUseReason))) && Array.isArray(workspaces) && workspaces.every((entry) => sized(entry) && nullableString(entry.projectRoot)) && (hosted === null || sized(hosted) && count$1(hosted.sessions));
|
|
557
|
+
].includes(entry.inUseReason))) && Array.isArray(workspaces) && workspaces.every((entry) => sized(entry) && nullableString$1(entry.projectRoot)) && (hosted === null || sized(hosted) && count$1(hosted.sessions));
|
|
355
558
|
}
|
|
356
559
|
//#endregion
|
|
357
560
|
//#region state/swiftpm-cache-usage.ts
|
|
@@ -373,6 +576,30 @@ function readSwiftpmCacheUsage() {
|
|
|
373
576
|
};
|
|
374
577
|
}
|
|
375
578
|
//#endregion
|
|
579
|
+
//#region state/machine-cache-usage.ts
|
|
580
|
+
function machineCacheUsageFile() {
|
|
581
|
+
return join(configDir(), "machine-cache-usage.json");
|
|
582
|
+
}
|
|
583
|
+
function object(value) {
|
|
584
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
585
|
+
}
|
|
586
|
+
function bytes(value) {
|
|
587
|
+
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
588
|
+
}
|
|
589
|
+
function nullableString(value) {
|
|
590
|
+
return value === null || typeof value === "string";
|
|
591
|
+
}
|
|
592
|
+
function validRuntime(value) {
|
|
593
|
+
return object(value) && typeof value.identifier === "string" && typeof value.runtimeIdentifier === "string" && nullableString(value.version) && nullableString(value.build) && bytes(value.bytes) && typeof value.deletable === "boolean" && nullableString(value.lastUsedAt) && typeof value.devices === "number" && Number.isInteger(value.devices) && value.devices >= 0;
|
|
594
|
+
}
|
|
595
|
+
function validUsage(value) {
|
|
596
|
+
return Boolean(value && value.version === 1 && typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) && Array.isArray(value.roots) && value.roots.every((root) => typeof root === "string") && Array.isArray(value.sizes) && value.sizes.every((entry) => object(entry) && typeof entry.dir === "string" && bytes(entry.bytes)) && (value.runtimes === null || Array.isArray(value.runtimes) && value.runtimes.every(validRuntime)) && nullableString(value.agentDeviceVersion) && nullableString(value.xcodeBuildVersion));
|
|
597
|
+
}
|
|
598
|
+
function readMachineCacheUsage() {
|
|
599
|
+
const value = readJsonObject(machineCacheUsageFile());
|
|
600
|
+
return validUsage(value) ? value : null;
|
|
601
|
+
}
|
|
602
|
+
//#endregion
|
|
376
603
|
//#region state/paths.ts
|
|
377
604
|
function getConfigPath() {
|
|
378
605
|
return join(configDir(), "config.json");
|
|
@@ -458,14 +685,14 @@ function diskUsageCacheDir() {
|
|
|
458
685
|
function diskUsageWalkClaims() {
|
|
459
686
|
return join(configDir(), "disk-usage-walk.claims");
|
|
460
687
|
}
|
|
461
|
-
/** The remote
|
|
688
|
+
/** The remote machines this machine asked for build access, each with its pinned stim-server key once approved. */
|
|
462
689
|
function buildMachinesFile() {
|
|
463
690
|
return join(configDir(), "build-machines.json");
|
|
464
691
|
}
|
|
465
692
|
function buildMachinesLock() {
|
|
466
693
|
return join(configDir(), "build-machines.lock");
|
|
467
694
|
}
|
|
468
|
-
/** The remote
|
|
695
|
+
/** The remote machines this machine asked for access, separate from build credentials. */
|
|
469
696
|
function deviceHostMachinesFile() {
|
|
470
697
|
return join(configDir(), "device-host-machines.json");
|
|
471
698
|
}
|
|
@@ -522,6 +749,14 @@ function archiveDir(id) {
|
|
|
522
749
|
function archiveLock() {
|
|
523
750
|
return join(archiveRoot(), ".lock");
|
|
524
751
|
}
|
|
752
|
+
/** This machine's own long-term X25519 key as a client of other machines' stim-servers. */
|
|
753
|
+
function clientIdentityKeyFile() {
|
|
754
|
+
return join(configDir(), "client-identity.key");
|
|
755
|
+
}
|
|
756
|
+
/** The Unix socket stim-server serves this machine's own user on, without Noise or pairing. */
|
|
757
|
+
function serverLocalSocket() {
|
|
758
|
+
return join(configDir(), "server", "local.sock");
|
|
759
|
+
}
|
|
525
760
|
function serverSetupDir() {
|
|
526
761
|
return join(configDir(), "server", "setup");
|
|
527
762
|
}
|
|
@@ -533,6 +768,13 @@ function setupJournalFile(hash) {
|
|
|
533
768
|
function withConfigLock(fn) {
|
|
534
769
|
return withDirLock(configLockPath(), fn, { ensureParent: () => ensurePrivateDir(configDir()) });
|
|
535
770
|
}
|
|
771
|
+
/** Waits for the config lock without blocking the event loop. The signal cancels acquisition; `fn` runs synchronously. */
|
|
772
|
+
function withConfigLockAsync(fn, signal) {
|
|
773
|
+
return withDirLockAsync(configLockPath(), fn, {
|
|
774
|
+
ensureParent: () => ensurePrivateDir(configDir()),
|
|
775
|
+
signal
|
|
776
|
+
});
|
|
777
|
+
}
|
|
536
778
|
function configCorruptRepair(path = getConfigPath()) {
|
|
537
779
|
return `Repair the file, or move it aside to start over: mv "${path}" "${path}.broken"`;
|
|
538
780
|
}
|
|
@@ -545,7 +787,21 @@ function configCorrupt(reason, path = getConfigPath()) {
|
|
|
545
787
|
/** The build worker root: `server.workerRoot` when it is absolute, else `$STIM_HOME/build-worker`. */
|
|
546
788
|
function buildWorkerRoot(config = loadConfig()) {
|
|
547
789
|
const configured = config?.server?.workerRoot;
|
|
548
|
-
return typeof configured === "string" && configured
|
|
790
|
+
return typeof configured === "string" && isAbsolute(configured) ? configured : join(configDir(), "build-worker");
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* A short directory at the drive root that stands in for the build worker directory `path` on Windows. The NDK's
|
|
794
|
+
* ninja is not long-path aware (STIM_PATH_TOO_LONG), and it opens paths under the checkout and under the Gradle user
|
|
795
|
+
* home's transforms cache, both of which are too long under the worker root. A subst drive does not help: Node's
|
|
796
|
+
* native realpath, which expo-modules-autolinking uses, resolves it back to the long path.
|
|
797
|
+
*/
|
|
798
|
+
function windowsShortDir(path, systemDrive = process.env.SystemDrive ?? "C:") {
|
|
799
|
+
const id = createHash("sha256").update(path.toLowerCase()).digest("hex").slice(0, 6);
|
|
800
|
+
return win32.join(`${systemDrive}\\`, "sw", id);
|
|
801
|
+
}
|
|
802
|
+
/** The ASCII key a Windows build worker records beside a `windowsShortDir` to tell which `kind` and `path` it serves. */
|
|
803
|
+
function windowsShortDirKey(kind, path) {
|
|
804
|
+
return createHash("sha256").update(`${kind}:${path.toLowerCase()}`).digest("hex");
|
|
549
805
|
}
|
|
550
806
|
function loadConfig() {
|
|
551
807
|
const p = getConfigPath();
|
|
@@ -588,7 +844,7 @@ function registryFromJson(config, key, path) {
|
|
|
588
844
|
function getProject(projectPath) {
|
|
589
845
|
return loadConfig()?.projects?.[projectPath] || null;
|
|
590
846
|
}
|
|
591
|
-
function getConcurrencyLimits({ env =
|
|
847
|
+
function getConcurrencyLimits({ env = ambientEnv() } = {}) {
|
|
592
848
|
const c = loadConfig()?.concurrency || {};
|
|
593
849
|
return {
|
|
594
850
|
maxBuilds: resolveLimit(env.STIM_MAX_BUILDS, c.maxBuilds),
|
|
@@ -610,7 +866,7 @@ function parseMax(raw, strings) {
|
|
|
610
866
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
|
|
611
867
|
return value;
|
|
612
868
|
}
|
|
613
|
-
function getParkedMax(platform, { config, env =
|
|
869
|
+
function getParkedMax(platform, { config, env = ambientEnv() } = {}) {
|
|
614
870
|
const { key, env: envKey } = MAX_SETTING[platform];
|
|
615
871
|
const fromEnv = env[envKey];
|
|
616
872
|
const explicit = fromEnv !== void 0 && fromEnv !== "";
|
|
@@ -643,22 +899,9 @@ function resolveLimit(envVal, cfgVal) {
|
|
|
643
899
|
function getProjectSettings(projectPath) {
|
|
644
900
|
return getProject(projectPath)?.settings || {};
|
|
645
901
|
}
|
|
646
|
-
function getProjectSetting(projectPath, dottedKey) {
|
|
647
|
-
return readNested(getProjectSettings(projectPath), dottedKey);
|
|
648
|
-
}
|
|
649
902
|
function getRepoSettings(gitCommonDir) {
|
|
650
903
|
return loadConfig()?.repos?.[gitCommonDir]?.settings || {};
|
|
651
904
|
}
|
|
652
|
-
function readNested(obj, dottedKey) {
|
|
653
|
-
if (!obj) return void 0;
|
|
654
|
-
const keys = dottedKey.split(".");
|
|
655
|
-
let cur = obj;
|
|
656
|
-
for (const k of keys) {
|
|
657
|
-
if (cur == null || typeof cur !== "object") return void 0;
|
|
658
|
-
cur = cur[k];
|
|
659
|
-
}
|
|
660
|
-
return cur;
|
|
661
|
-
}
|
|
662
905
|
function isPathPrefix(prefix, path) {
|
|
663
906
|
const head = comparablePath(prefix);
|
|
664
907
|
const candidate = comparablePath(path);
|
|
@@ -684,11 +927,6 @@ function allMetroPorts() {
|
|
|
684
927
|
if (!cfg?.projects) return [];
|
|
685
928
|
return Object.values(cfg.projects).map((p) => p.metroPort).filter((p) => typeof p === "number");
|
|
686
929
|
}
|
|
687
|
-
function findProjectByMetroPort(port) {
|
|
688
|
-
const cfg = loadConfig();
|
|
689
|
-
for (const [path, proj] of Object.entries(cfg?.projects || {})) if (proj.metroPort === port) return path;
|
|
690
|
-
return null;
|
|
691
|
-
}
|
|
692
930
|
//#endregion
|
|
693
931
|
//#region state/tailnet.ts
|
|
694
932
|
const DEFAULT_SERVE_PORT = 7443;
|
|
@@ -715,16 +953,25 @@ function findPeer(status, name) {
|
|
|
715
953
|
return [{
|
|
716
954
|
nodeId: peer.ID,
|
|
717
955
|
dnsName,
|
|
718
|
-
address
|
|
956
|
+
address,
|
|
957
|
+
...typeof peer.OS === "string" && peer.OS ? { os: peer.OS } : {}
|
|
719
958
|
}];
|
|
720
959
|
});
|
|
721
960
|
if (found.length === 0) return "missing";
|
|
722
961
|
return found.length === 1 ? found[0] : "ambiguous";
|
|
723
962
|
}
|
|
724
963
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
|
|
964
|
+
* Whether `tailscale status` reports the peer a `remote.machines` entry names as running an OS other than macOS.
|
|
965
|
+
* Such a machine can build but never host simulators or emulators; an unknown OS counts as able to host.
|
|
966
|
+
*/
|
|
967
|
+
function cannotHost(status, entry) {
|
|
968
|
+
const parsed = parseMachine(entry);
|
|
969
|
+
const peer = parsed ? findPeer(status, parsed.name) : "missing";
|
|
970
|
+
return typeof peer === "object" && peer.os !== void 0 && peer.os !== "macOS";
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Connects to the node's own tailnet address from `tailscale status`. `tailscale serve` still needs the MagicDNS name
|
|
974
|
+
* for TLS (SNI and certificate) and for its Host routing.
|
|
728
975
|
*/
|
|
729
976
|
function endpoint(peer, port) {
|
|
730
977
|
const address = peer.address.includes(":") ? `[${peer.address}]` : peer.address;
|
|
@@ -736,24 +983,30 @@ function endpoint(peer, port) {
|
|
|
736
983
|
};
|
|
737
984
|
}
|
|
738
985
|
/**
|
|
739
|
-
*
|
|
740
|
-
*
|
|
986
|
+
* Where to reach a `remote.machines` entry now, or why Stim cannot. The tailnet only carries the bytes: the Noise
|
|
987
|
+
* handshake, pinned to the worker's server key, decides whether the machine answering is the one this machine paired
|
|
988
|
+
* with.
|
|
741
989
|
*/
|
|
742
|
-
function
|
|
743
|
-
const parsed = parseMachine(
|
|
744
|
-
if (!parsed) return `${
|
|
990
|
+
function machineEndpoint(machine, current) {
|
|
991
|
+
const parsed = parseMachine(machine);
|
|
992
|
+
if (!parsed) return `${machine} is not a tailnet name`;
|
|
745
993
|
if (!isJsonObject(current)) return "Tailscale is not running";
|
|
746
994
|
const peer = findPeer(current, parsed.name);
|
|
747
995
|
if (peer === "missing") return `no peer on this tailnet is named ${parsed.name}`;
|
|
748
996
|
if (peer === "ambiguous") return `several tailnet peers match ${parsed.name}`;
|
|
749
|
-
if (peer.nodeId !== credential.nodeId) return `${peer.dnsName} is now node ${peer.nodeId}, not the pinned ${credential.nodeId}; run stim doctor`;
|
|
750
997
|
return endpoint(peer, parsed.port);
|
|
751
998
|
}
|
|
752
999
|
//#endregion
|
|
753
1000
|
//#region state/hosted-macos.ts
|
|
754
1001
|
/** App slots one host offers; a slot names the host-assigned bundle id `<bundleId>.hosted<slot>`. */
|
|
755
1002
|
const HOSTED_MACOS_APP_SLOTS = 64;
|
|
1003
|
+
/** The drivers `hosting.agentDriver` can name: `none` and the first-party driver packages stim-server ships. */
|
|
756
1004
|
const HOSTED_AGENT_DRIVERS = ["none", "agent-device"];
|
|
1005
|
+
const AGENT_DRIVER_NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
1006
|
+
/** A driver name a grant or access record can carry; a client resolves only the drivers it knows. */
|
|
1007
|
+
function hostedAgentDriverName(value) {
|
|
1008
|
+
return typeof value === "string" && value !== "none" && AGENT_DRIVER_NAME.test(value);
|
|
1009
|
+
}
|
|
757
1010
|
function hostedMacosAppSlot(value) {
|
|
758
1011
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 64;
|
|
759
1012
|
}
|
|
@@ -814,14 +1067,17 @@ function parseHostedAgentLease(value) {
|
|
|
814
1067
|
function parseHostedAgentGrant(value) {
|
|
815
1068
|
if (!isJsonObject(value)) return null;
|
|
816
1069
|
if (value.driver === "none") return Object.keys(value).length === 1 ? { driver: "none" } : null;
|
|
817
|
-
if (!
|
|
818
|
-
const
|
|
819
|
-
if (!lease) return null;
|
|
820
|
-
return {
|
|
1070
|
+
if (!hostedAgentDriverName(value.driver) || typeof value.path !== "string" || !AGENT_PATH.test(value.path) || typeof value.token !== "string" || !/^[A-Za-z0-9_-]{32,256}$/.test(value.token) || typeof value.scope !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/.test(value.scope) || Object.keys(value).length !== (value.lease === void 0 ? 4 : 5)) return null;
|
|
1071
|
+
const grant = {
|
|
821
1072
|
driver: value.driver,
|
|
822
1073
|
path: value.path,
|
|
823
1074
|
token: value.token,
|
|
824
|
-
scope: value.scope
|
|
1075
|
+
scope: value.scope
|
|
1076
|
+
};
|
|
1077
|
+
if (value.lease === void 0) return grant;
|
|
1078
|
+
const lease = parseHostedAgentLease(value.lease);
|
|
1079
|
+
return lease && {
|
|
1080
|
+
...grant,
|
|
825
1081
|
lease: { ...lease }
|
|
826
1082
|
};
|
|
827
1083
|
}
|
|
@@ -831,9 +1087,9 @@ function parseHostedAgentAccess(value) {
|
|
|
831
1087
|
driver: "none",
|
|
832
1088
|
setting: "hosting.agentDriver"
|
|
833
1089
|
} : null;
|
|
834
|
-
if (value.driver
|
|
1090
|
+
if (!hostedAgentDriverName(value.driver) || typeof value.remoteConfig !== "string" || !isAbsolute(value.remoteConfig) || typeof value.command !== "string" || !value.command || Object.keys(value).length !== 3) return null;
|
|
835
1091
|
return {
|
|
836
|
-
driver:
|
|
1092
|
+
driver: value.driver,
|
|
837
1093
|
remoteConfig: value.remoteConfig,
|
|
838
1094
|
command: value.command
|
|
839
1095
|
};
|
|
@@ -1010,7 +1266,7 @@ const SETTINGS = [
|
|
|
1010
1266
|
patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
|
|
1011
1267
|
},
|
|
1012
1268
|
scopes: EVERY,
|
|
1013
|
-
description: "Default iOS remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this
|
|
1269
|
+
description: "Default iOS remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this machine is full or busy; local runs here, overriding a lower layer"
|
|
1014
1270
|
},
|
|
1015
1271
|
{
|
|
1016
1272
|
key: "ios.projectPath",
|
|
@@ -1041,6 +1297,36 @@ const SETTINGS = [
|
|
|
1041
1297
|
scopes: PROJECT,
|
|
1042
1298
|
description: "SHA-1 of the signing identity, when two share a name"
|
|
1043
1299
|
},
|
|
1300
|
+
{
|
|
1301
|
+
key: "ios.appStoreConnectKeyPath",
|
|
1302
|
+
type: ABSOLUTE_PATH,
|
|
1303
|
+
scopes: MACHINE,
|
|
1304
|
+
env: "STIM_ASC_KEY_PATH",
|
|
1305
|
+
sensitive: true,
|
|
1306
|
+
description: "App Store Connect API key (.p8) that `stim device provision ios` authenticates with"
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
key: "ios.appStoreConnectKeyId",
|
|
1310
|
+
type: {
|
|
1311
|
+
kind: "string",
|
|
1312
|
+
pattern: "^[A-Za-z0-9]+$",
|
|
1313
|
+
patternHelp: "the key ID App Store Connect shows"
|
|
1314
|
+
},
|
|
1315
|
+
scopes: MACHINE,
|
|
1316
|
+
env: "STIM_ASC_KEY_ID",
|
|
1317
|
+
description: "Key ID of ios.appStoreConnectKeyPath"
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
key: "ios.appStoreConnectIssuerId",
|
|
1321
|
+
type: {
|
|
1322
|
+
kind: "string",
|
|
1323
|
+
pattern: "^[A-Za-z0-9-]+$",
|
|
1324
|
+
patternHelp: "the issuer ID App Store Connect shows"
|
|
1325
|
+
},
|
|
1326
|
+
scopes: MACHINE,
|
|
1327
|
+
env: "STIM_ASC_ISSUER_ID",
|
|
1328
|
+
description: "Issuer ID of ios.appStoreConnectKeyPath"
|
|
1329
|
+
},
|
|
1044
1330
|
{
|
|
1045
1331
|
key: "ios.lanHost",
|
|
1046
1332
|
type: {
|
|
@@ -1116,6 +1402,12 @@ const SETTINGS = [
|
|
|
1116
1402
|
scopes: PROJECT,
|
|
1117
1403
|
description: "Gradle build variant"
|
|
1118
1404
|
},
|
|
1405
|
+
{
|
|
1406
|
+
key: "android.artifactInputs",
|
|
1407
|
+
type: OBJECT,
|
|
1408
|
+
scopes: PROJECT,
|
|
1409
|
+
description: "Complete repeatable native Gradle inputs for local APK caching, including declared local files and environment names"
|
|
1410
|
+
},
|
|
1119
1411
|
{
|
|
1120
1412
|
key: "android.offloadInputs",
|
|
1121
1413
|
type: OBJECT,
|
|
@@ -1143,7 +1435,7 @@ const SETTINGS = [
|
|
|
1143
1435
|
patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
|
|
1144
1436
|
},
|
|
1145
1437
|
scopes: EVERY,
|
|
1146
|
-
description: "Default Android remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this
|
|
1438
|
+
description: "Default Android remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this machine is full or busy; local runs here, overriding a lower layer"
|
|
1147
1439
|
},
|
|
1148
1440
|
{
|
|
1149
1441
|
key: "metro.tunnel",
|
|
@@ -1372,6 +1664,13 @@ const SETTINGS = [
|
|
|
1372
1664
|
env: "STIM_ARCHIVE_AGENT_ACTIONS_MAX_AGE_DAYS",
|
|
1373
1665
|
description: "Days to keep archived agent-device sessions; 0 keeps none"
|
|
1374
1666
|
},
|
|
1667
|
+
{
|
|
1668
|
+
key: "workspace.hidden",
|
|
1669
|
+
type: BOOLEAN,
|
|
1670
|
+
scopes: ["workspace"],
|
|
1671
|
+
default: false,
|
|
1672
|
+
description: "Hide this workspace in Stim Desktop and the phone app; the next command that uses it unhides it"
|
|
1673
|
+
},
|
|
1375
1674
|
{
|
|
1376
1675
|
key: "worktree.exclude",
|
|
1377
1676
|
type: { kind: "strings" },
|
|
@@ -1639,6 +1938,18 @@ const SETTINGS = [
|
|
|
1639
1938
|
env: "STIM_MAINTENANCE_OLDER_THAN_DAYS",
|
|
1640
1939
|
description: "Age in days for the age sweep: unused workspace build outputs and shared cache entries"
|
|
1641
1940
|
},
|
|
1941
|
+
{
|
|
1942
|
+
key: "maintenance.deviceIdleDays",
|
|
1943
|
+
type: {
|
|
1944
|
+
kind: "number",
|
|
1945
|
+
integer: true,
|
|
1946
|
+
minimum: 0
|
|
1947
|
+
},
|
|
1948
|
+
scopes: MACHINE,
|
|
1949
|
+
default: 3,
|
|
1950
|
+
env: "STIM_MAINTENANCE_DEVICE_IDLE_DAYS",
|
|
1951
|
+
description: "Days a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off"
|
|
1952
|
+
},
|
|
1642
1953
|
{
|
|
1643
1954
|
key: "maintenance.protectRecentHours",
|
|
1644
1955
|
type: {
|
|
@@ -1651,20 +1962,60 @@ const SETTINGS = [
|
|
|
1651
1962
|
env: "STIM_MAINTENANCE_PROTECT_RECENT_HOURS",
|
|
1652
1963
|
description: "Cache entries used within this many hours are never evicted"
|
|
1653
1964
|
},
|
|
1965
|
+
{
|
|
1966
|
+
key: "maintenance.urgentFreeDiskGb",
|
|
1967
|
+
type: GIGABYTES,
|
|
1968
|
+
scopes: MACHINE,
|
|
1969
|
+
default: 12,
|
|
1970
|
+
env: "STIM_MAINTENANCE_URGENT_FREE_DISK_GB",
|
|
1971
|
+
description: "Free disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off"
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
key: "maintenance.criticalFreeDiskGb",
|
|
1975
|
+
type: GIGABYTES,
|
|
1976
|
+
scopes: MACHINE,
|
|
1977
|
+
default: 6,
|
|
1978
|
+
env: "STIM_MAINTENANCE_CRITICAL_FREE_DISK_GB",
|
|
1979
|
+
description: "Free disk, in GB, below which automatic maintenance enters disk pressure level 3, also at or below budget.hardFloorDiskGb; 0 leaves only the hard floor"
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
key: "maintenance.pressureProtectMinutes",
|
|
1983
|
+
type: {
|
|
1984
|
+
kind: "number",
|
|
1985
|
+
integer: true,
|
|
1986
|
+
minimum: 0
|
|
1987
|
+
},
|
|
1988
|
+
scopes: MACHINE,
|
|
1989
|
+
default: 10,
|
|
1990
|
+
env: "STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTES",
|
|
1991
|
+
description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3"
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
key: "maintenance.gradleVersionIdleDays",
|
|
1995
|
+
type: {
|
|
1996
|
+
kind: "number",
|
|
1997
|
+
integer: true,
|
|
1998
|
+
minimum: 0
|
|
1999
|
+
},
|
|
2000
|
+
scopes: MACHINE,
|
|
2001
|
+
default: 7,
|
|
2002
|
+
env: "STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYS",
|
|
2003
|
+
description: "At disk pressure level 3, stop idle Gradle and Kotlin daemons, then delete the Gradle version caches and wrapper distributions no registered project pins once their directories are unmodified this many days; 0 turns this off"
|
|
2004
|
+
},
|
|
1654
2005
|
{
|
|
1655
2006
|
key: "maintenance.removeFinishedWorktrees",
|
|
1656
2007
|
type: BOOLEAN,
|
|
1657
2008
|
scopes: MACHINE,
|
|
1658
2009
|
default: true,
|
|
1659
2010
|
env: "STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREES",
|
|
1660
|
-
description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes"
|
|
2011
|
+
description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes, and at disk pressure level 3 clean, pushed ones with no commits of their own"
|
|
1661
2012
|
},
|
|
1662
2013
|
{
|
|
1663
2014
|
key: "maintenance.keep",
|
|
1664
2015
|
type: BOOLEAN,
|
|
1665
2016
|
scopes: PROJECT,
|
|
1666
2017
|
default: false,
|
|
1667
|
-
description: "Pin this workspace: automatic maintenance never clears its build outputs or removes its worktree"
|
|
2018
|
+
description: "Pin this workspace: automatic maintenance never clears its build outputs, deletes its owned devices or removes its worktree"
|
|
1668
2019
|
},
|
|
1669
2020
|
{
|
|
1670
2021
|
key: "maintenance.workspaceOutputsMaxGb",
|
|
@@ -1753,7 +2104,7 @@ const SETTINGS = [
|
|
|
1753
2104
|
scopes: MACHINE,
|
|
1754
2105
|
default: false,
|
|
1755
2106
|
env: "STIM_DEBUG",
|
|
1756
|
-
description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other
|
|
2107
|
+
description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other machines, and stim-server records every request with its timings, under STIM_HOME/logs/debug/ with rotation. Nothing is sent anywhere, and records never carry arguments, tokens or tickets"
|
|
1757
2108
|
},
|
|
1758
2109
|
{
|
|
1759
2110
|
key: "hosting.agentDriver",
|
|
@@ -1765,18 +2116,32 @@ const SETTINGS = [
|
|
|
1765
2116
|
default: "none",
|
|
1766
2117
|
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"
|
|
1767
2118
|
},
|
|
2119
|
+
{
|
|
2120
|
+
key: "server.buildsPaused",
|
|
2121
|
+
type: { kind: "boolean" },
|
|
2122
|
+
scopes: MACHINE,
|
|
2123
|
+
default: false,
|
|
2124
|
+
description: "Temporarily refuse new incoming builds without interrupting running builds, serving cached artifacts, or changing client approvals; local CLI builds remain available"
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
key: "server.devicesPaused",
|
|
2128
|
+
type: { kind: "boolean" },
|
|
2129
|
+
scopes: MACHINE,
|
|
2130
|
+
default: false,
|
|
2131
|
+
description: "Temporarily refuse new hosted iOS, Android and macOS sessions without interrupting existing sessions or changing client approvals; local CLI devices remain available"
|
|
2132
|
+
},
|
|
1768
2133
|
{
|
|
1769
2134
|
key: "server.acceptClientBuilds",
|
|
1770
2135
|
type: { kind: "boolean" },
|
|
1771
2136
|
scopes: MACHINE,
|
|
1772
2137
|
default: false,
|
|
1773
|
-
description: "Whether a
|
|
2138
|
+
description: "Whether a machine approved for builds or device hosting here may update this machine's stim-server service to that machine's own stim-server build; releases from npm need no setting"
|
|
1774
2139
|
},
|
|
1775
2140
|
{
|
|
1776
2141
|
key: "remote.machines",
|
|
1777
2142
|
type: { kind: "strings" },
|
|
1778
2143
|
scopes: MACHINE,
|
|
1779
|
-
description: "Tailscale names of the remote
|
|
2144
|
+
description: "Tailscale names of the remote machines this machine may build on and host owned simulators on, each optionally with :<port> of its tailscale serve route (default 7443); what each is used for follows the build and device-host approvals it grants, which doctor --fix asks for separately"
|
|
1780
2145
|
},
|
|
1781
2146
|
{
|
|
1782
2147
|
key: "remote.buildPoolDisabled",
|
|
@@ -1797,7 +2162,7 @@ const SETTINGS = [
|
|
|
1797
2162
|
type: BOOLEAN,
|
|
1798
2163
|
scopes: EVERY,
|
|
1799
2164
|
default: false,
|
|
1800
|
-
description: "When ios.remote or android.remote is auto, this
|
|
2165
|
+
description: "When ios.remote or android.remote is auto, this machine is at its concurrency.maxDevices cap and no remote.machines entry takes the run, run the simulator or emulator on a billed EAS Simulator instead of waiting or refusing, when eas-cli can"
|
|
1801
2166
|
},
|
|
1802
2167
|
{
|
|
1803
2168
|
key: "remote.build",
|
|
@@ -1809,7 +2174,7 @@ const SETTINGS = [
|
|
|
1809
2174
|
scopes: MACHINE,
|
|
1810
2175
|
default: "auto",
|
|
1811
2176
|
env: "STIM_REMOTE_BUILD",
|
|
1812
|
-
description: "Build placement: auto follows remote.buildMode within enabled automatic pool members; local always builds here; a name in remote.machines requires that remote
|
|
2177
|
+
description: "Build placement: auto follows remote.buildMode within enabled automatic pool members; local always builds here; a name in remote.machines requires that remote machine without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override"
|
|
1813
2178
|
},
|
|
1814
2179
|
{
|
|
1815
2180
|
key: "remote.buildMode",
|
|
@@ -1820,7 +2185,7 @@ const SETTINGS = [
|
|
|
1820
2185
|
scopes: MACHINE,
|
|
1821
2186
|
default: "auto",
|
|
1822
2187
|
env: "STIM_REMOTE_BUILD_MODE",
|
|
1823
|
-
description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this
|
|
2188
|
+
description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this machine has a free concurrency.maxBuilds slot and its load is under server.maxLoadPerCore, and otherwise builds on a less loaded machine in remote.machines; force builds on an enabled remote machine whenever one can take the build; off builds here when local is enabled in remote.buildPoolDisabled"
|
|
1824
2189
|
},
|
|
1825
2190
|
{
|
|
1826
2191
|
key: "server.maxLoadPerCore",
|
|
@@ -1830,13 +2195,13 @@ const SETTINGS = [
|
|
|
1830
2195
|
},
|
|
1831
2196
|
scopes: MACHINE,
|
|
1832
2197
|
default: 2,
|
|
1833
|
-
description: "Load per core (5-minute load average divided by the CPU count) at which this
|
|
2198
|
+
description: "Load per core (5-minute load average divided by the CPU count) at which this machine counts as saturated: it declines builds other machines send it, automatic build placement stops preferring it, and automatic iOS and Android device placement treats it as busy"
|
|
1834
2199
|
},
|
|
1835
2200
|
{
|
|
1836
2201
|
key: "server.workerRoot",
|
|
1837
2202
|
type: ABSOLUTE_PATH,
|
|
1838
2203
|
scopes: MACHINE,
|
|
1839
|
-
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the
|
|
2204
|
+
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the machines that build here; default $STIM_HOME/build-worker"
|
|
1840
2205
|
},
|
|
1841
2206
|
{
|
|
1842
2207
|
key: "server.gradleDaemonIdleMinutes",
|
|
@@ -1848,7 +2213,7 @@ const SETTINGS = [
|
|
|
1848
2213
|
},
|
|
1849
2214
|
scopes: MACHINE,
|
|
1850
2215
|
default: 30,
|
|
1851
|
-
description: "Minutes the Gradle daemon of an Android build another
|
|
2216
|
+
description: "Minutes the Gradle daemon of an Android build another machine sent here stays warm after the build; 0 stops it when the build ends"
|
|
1852
2217
|
},
|
|
1853
2218
|
{
|
|
1854
2219
|
key: "caches.buildCache",
|
|
@@ -1880,9 +2245,6 @@ function isLayeredSetting(setting) {
|
|
|
1880
2245
|
return setting.scopes.some((scope) => scope !== "machine");
|
|
1881
2246
|
}
|
|
1882
2247
|
const SETTING_GROUPS = [...new Set(SETTINGS.filter(isLayeredSetting).flatMap((setting) => setting.key.split(".").slice(0, -1).map((_, index, parts) => parts.slice(0, index + 1).join("."))))];
|
|
1883
|
-
function isPlainObject(value) {
|
|
1884
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1885
|
-
}
|
|
1886
2248
|
function bundleUrlAccepts(value, key) {
|
|
1887
2249
|
if (typeof value !== "string" || !/^(https?:\/\/|\/(?!\/))/.test(value) || /[\s\\#]/.test(value)) return false;
|
|
1888
2250
|
try {
|
|
@@ -1913,7 +2275,7 @@ function acceptsShape(setting, value) {
|
|
|
1913
2275
|
case "strings": return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1914
2276
|
case "number": return typeof value === "number";
|
|
1915
2277
|
case "choice": return typeof value === "string" && type.choices.includes(value);
|
|
1916
|
-
case "object": return
|
|
2278
|
+
case "object": return isJsonObject(value);
|
|
1917
2279
|
case "bundle-url": return bundleUrlAccepts(value, setting.key);
|
|
1918
2280
|
}
|
|
1919
2281
|
}
|
|
@@ -2018,7 +2380,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
|
|
|
2018
2380
|
if (refusal && !busy) throw refusal;
|
|
2019
2381
|
return null;
|
|
2020
2382
|
}
|
|
2021
|
-
/** Build slots held for a remote
|
|
2383
|
+
/** Build slots held for a remote machine's offloaded builds, which run under another Stim home. */
|
|
2022
2384
|
function liveOffloadedSlots() {
|
|
2023
2385
|
let names;
|
|
2024
2386
|
try {
|
|
@@ -2079,7 +2441,7 @@ function machineCapacity() {
|
|
|
2079
2441
|
maxLoadPerCore: maxLoadPerCore()
|
|
2080
2442
|
};
|
|
2081
2443
|
}
|
|
2082
|
-
/** Why a
|
|
2444
|
+
/** Why a machine with this capacity should not take another native build; null when it can. */
|
|
2083
2445
|
function saturation(capacity) {
|
|
2084
2446
|
if (capacity.maxBuilds > 0 && capacity.builds >= capacity.maxBuilds) return `all ${capacity.maxBuilds} build slots busy`;
|
|
2085
2447
|
if (capacity.loadPerCore >= capacity.maxLoadPerCore) return `load at or above ${capacity.maxLoadPerCore}/core`;
|
|
@@ -2087,41 +2449,55 @@ function saturation(capacity) {
|
|
|
2087
2449
|
}
|
|
2088
2450
|
//#endregion
|
|
2089
2451
|
//#region state/build-machines.ts
|
|
2090
|
-
|
|
2452
|
+
const MACHINE_CREDENTIALS_VERSION = 2;
|
|
2453
|
+
const KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
2454
|
+
/** A stored credential, or null when it is malformed or from an older version. */
|
|
2455
|
+
function parseMachineCredential(value) {
|
|
2091
2456
|
if (!isJsonObject(value)) return null;
|
|
2092
|
-
const { machine,
|
|
2457
|
+
const { machine, transport, address, state, requestedAt, serverKey, deviceId, ticket, expiresAt } = value;
|
|
2093
2458
|
if (![
|
|
2094
2459
|
machine,
|
|
2095
|
-
|
|
2096
|
-
dnsName,
|
|
2097
|
-
deviceId,
|
|
2098
|
-
deviceToken,
|
|
2460
|
+
address,
|
|
2099
2461
|
requestedAt
|
|
2100
2462
|
].every((field) => typeof field === "string" && field)) return null;
|
|
2101
|
-
if (state !== "pending" && state !== "approved") return null;
|
|
2463
|
+
if (transport !== "tailscale" || state !== "pending" && state !== "approved") return null;
|
|
2464
|
+
if (serverKey !== void 0 && (typeof serverKey !== "string" || !KEY.test(serverKey))) return null;
|
|
2465
|
+
if (ticket !== void 0 && (typeof ticket !== "string" || !KEY.test(ticket))) return null;
|
|
2466
|
+
if (state === "approved" && (serverKey === void 0 || typeof deviceId !== "string" || !deviceId)) return null;
|
|
2467
|
+
if (state === "pending" && ticket === void 0) return null;
|
|
2102
2468
|
return {
|
|
2103
2469
|
machine,
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
deviceId,
|
|
2107
|
-
deviceToken,
|
|
2470
|
+
transport,
|
|
2471
|
+
address,
|
|
2108
2472
|
state,
|
|
2109
2473
|
requestedAt,
|
|
2110
|
-
...
|
|
2111
|
-
...typeof
|
|
2474
|
+
...serverKey ? { serverKey } : {},
|
|
2475
|
+
...typeof deviceId === "string" && deviceId ? { deviceId } : {},
|
|
2476
|
+
...ticket ? { ticket } : {},
|
|
2477
|
+
...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {}
|
|
2112
2478
|
};
|
|
2113
2479
|
}
|
|
2114
2480
|
function readBuildMachines() {
|
|
2115
2481
|
try {
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2482
|
+
const store = readJsonFile(buildMachinesFile());
|
|
2483
|
+
if (store.version !== 2 || !Array.isArray(store.machines)) return [];
|
|
2484
|
+
return store.machines.flatMap((entry) => parseMachineCredential(entry) ?? []);
|
|
2118
2485
|
} catch {
|
|
2119
2486
|
return [];
|
|
2120
2487
|
}
|
|
2121
2488
|
}
|
|
2122
2489
|
//#endregion
|
|
2490
|
+
//#region state/client-identity.ts
|
|
2491
|
+
/** This machine's long-term X25519 key as a client of other machines, in `$STIM_HOME/client-identity.key`. */
|
|
2492
|
+
function clientIdentity() {
|
|
2493
|
+
return readOrCreateKeyFile(clientIdentityKeyFile());
|
|
2494
|
+
}
|
|
2495
|
+
//#endregion
|
|
2123
2496
|
//#region state/device-host-machines.ts
|
|
2124
|
-
/**
|
|
2497
|
+
/**
|
|
2498
|
+
* An absent store, or one from protocol 1, is empty; an unreadable or malformed one refuses access without discarding
|
|
2499
|
+
* pinned keys.
|
|
2500
|
+
*/
|
|
2125
2501
|
function readDeviceHostMachines() {
|
|
2126
2502
|
let value;
|
|
2127
2503
|
try {
|
|
@@ -2130,23 +2506,13 @@ function readDeviceHostMachines() {
|
|
|
2130
2506
|
if (error.code === "ENOENT") return [];
|
|
2131
2507
|
throw error;
|
|
2132
2508
|
}
|
|
2133
|
-
if (
|
|
2509
|
+
if (isJsonObject(value) && value.version === 1) return [];
|
|
2510
|
+
if (!isJsonObject(value) || value.version !== 2 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
|
|
2134
2511
|
const machines = [];
|
|
2135
2512
|
for (const entry of value.machines) {
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
"dnsName",
|
|
2140
|
-
"deviceId",
|
|
2141
|
-
"deviceToken",
|
|
2142
|
-
"requestedAt"
|
|
2143
|
-
].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");
|
|
2144
|
-
const { ticketHash, expiresAt, ...credential } = entry;
|
|
2145
|
-
machines.push({
|
|
2146
|
-
...credential,
|
|
2147
|
-
...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {},
|
|
2148
|
-
...typeof ticketHash === "string" ? { ticketHash } : {}
|
|
2149
|
-
});
|
|
2513
|
+
const credential = parseMachineCredential(entry);
|
|
2514
|
+
if (!credential || machines.some((each) => each.machine === credential.machine)) throw new Error("Invalid device-host machine credential store");
|
|
2515
|
+
machines.push(credential);
|
|
2150
2516
|
}
|
|
2151
2517
|
return machines;
|
|
2152
2518
|
}
|
|
@@ -2166,11 +2532,15 @@ function requireAutomaticMachine(role, machine) {
|
|
|
2166
2532
|
remedy: `Enable it in remote.${role}PoolDisabled or select a machine explicitly.`
|
|
2167
2533
|
});
|
|
2168
2534
|
}
|
|
2169
|
-
|
|
2535
|
+
/**
|
|
2536
|
+
* Refuses a pool with no enabled member: local, or a configured remote approved for the role. `canHost` excludes
|
|
2537
|
+
* device-pool remotes that can never host, such as a Linux machine.
|
|
2538
|
+
*/
|
|
2539
|
+
function validateAutomaticPool(role, config, canHost = () => true) {
|
|
2170
2540
|
if (automaticMachineEnabled(role, "local", config)) return;
|
|
2171
2541
|
const configured = config.remote?.machines;
|
|
2172
2542
|
const approved = role === "build" ? readBuildMachines() : readDeviceHostMachines();
|
|
2173
|
-
if (Array.isArray(configured) && configured.some((machine) => typeof machine === "string" && automaticMachineEnabled(role, machine, config) && approved.some((entry) => entry.machine === machine && entry.state === "approved"))) return;
|
|
2543
|
+
if (Array.isArray(configured) && configured.some((machine) => typeof machine === "string" && automaticMachineEnabled(role, machine, config) && (role === "build" || canHost(machine)) && approved.some((entry) => entry.machine === machine && entry.state === "approved"))) return;
|
|
2174
2544
|
throw Object.assign(/* @__PURE__ */ new Error(`Keep at least one enabled automatic ${role} pool member: local or a configured, approved remote machine.`), { code: "STIM_BAD_ARG" });
|
|
2175
2545
|
}
|
|
2176
2546
|
//#endregion
|
|
@@ -2184,7 +2554,7 @@ function debugLogDir() {
|
|
|
2184
2554
|
return join(configDir(), "logs", "debug");
|
|
2185
2555
|
}
|
|
2186
2556
|
/** The `STIM_DEBUG` override (1/true on, 0/false off, anything else off), else the machine setting `debug.logs`. Never throws. */
|
|
2187
|
-
function debugLoggingEnabled(env =
|
|
2557
|
+
function debugLoggingEnabled(env = ambientEnv()) {
|
|
2188
2558
|
const definition = settingDefinition("debug.logs");
|
|
2189
2559
|
const raw = env[DEBUG_ENV];
|
|
2190
2560
|
if (raw !== void 0 && raw !== "") {
|
|
@@ -2211,15 +2581,21 @@ function scrub(value, depth = 0) {
|
|
|
2211
2581
|
* Debug-level records for one component (`cli` or `server`). Off by default; `STIM_DEBUG=1` or `debug.logs` turns it
|
|
2212
2582
|
* on. Callers pass names, durations and codes, never arguments, environment values, tokens or tickets.
|
|
2213
2583
|
*/
|
|
2214
|
-
function createDebugLog(component, { env
|
|
2584
|
+
function createDebugLog(component, { env, now = Date.now, base = () => ({}) } = {}) {
|
|
2215
2585
|
let checkedAt = Number.NEGATIVE_INFINITY;
|
|
2216
2586
|
let on = false;
|
|
2217
2587
|
let unchecked = ROTATE_CHECK_BYTES;
|
|
2588
|
+
let checkedEnv;
|
|
2589
|
+
let checkedHome;
|
|
2218
2590
|
const enabled = () => {
|
|
2219
2591
|
const at = now();
|
|
2220
|
-
|
|
2592
|
+
const activeEnv = env ?? ambientEnv();
|
|
2593
|
+
const home = configDir();
|
|
2594
|
+
if (activeEnv !== checkedEnv || home !== checkedHome || at - checkedAt >= ENABLED_RECHECK_MS) {
|
|
2221
2595
|
checkedAt = at;
|
|
2222
|
-
|
|
2596
|
+
checkedEnv = activeEnv;
|
|
2597
|
+
checkedHome = home;
|
|
2598
|
+
on = debugLoggingEnabled(activeEnv);
|
|
2223
2599
|
}
|
|
2224
2600
|
return on;
|
|
2225
2601
|
};
|
|
@@ -2250,6 +2626,15 @@ function createDebugLog(component, { env = process.env, now = Date.now, base = (
|
|
|
2250
2626
|
};
|
|
2251
2627
|
}
|
|
2252
2628
|
//#endregion
|
|
2629
|
+
//#region state/home-identity.ts
|
|
2630
|
+
/** The persistent, nonsecret identity of this Stim home; null until the CLI initializes it. */
|
|
2631
|
+
function readHomeIdentity(config = loadConfig()) {
|
|
2632
|
+
const id = config?.homeId;
|
|
2633
|
+
if (id === void 0) return null;
|
|
2634
|
+
if (!validHomeId(id)) throw Object.assign(/* @__PURE__ */ new Error(`Stim home identity is not a UUID. ${configCorruptRepair()}`), { code: "STIM_CONFIG_CORRUPT" });
|
|
2635
|
+
return id.toLowerCase();
|
|
2636
|
+
}
|
|
2637
|
+
//#endregion
|
|
2253
2638
|
//#region state/ledgers.ts
|
|
2254
2639
|
function ids(value) {
|
|
2255
2640
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry !== "") : [];
|
|
@@ -2684,7 +3069,8 @@ function expoContextFollower(onRecord, into) {
|
|
|
2684
3069
|
return {
|
|
2685
3070
|
push,
|
|
2686
3071
|
settle,
|
|
2687
|
-
flush
|
|
3072
|
+
flush,
|
|
3073
|
+
pending: () => pending !== null
|
|
2688
3074
|
};
|
|
2689
3075
|
}
|
|
2690
3076
|
function includeBareErrorContext(all, matched, launchTs, sinceTs) {
|
|
@@ -2735,11 +3121,11 @@ function launchMarkersBySlot(records) {
|
|
|
2735
3121
|
}
|
|
2736
3122
|
return markers;
|
|
2737
3123
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
const slotLaunched = slot !== void 0 &&
|
|
2741
|
-
const
|
|
2742
|
-
|
|
3124
|
+
/** Decides which records a query matches from the timeline's marker records alone. */
|
|
3125
|
+
function planLogQuery(markers, { slot, sources, minLevel, since, grep, errorsOnly, now } = {}) {
|
|
3126
|
+
const slotLaunched = slot !== void 0 && markers.some((record) => isLaunchMarker(record) && (record.slot ?? "default") === slot);
|
|
3127
|
+
const inTimeline = (record) => slot === void 0 || (record.slot ?? "default") === slot || slotLaunched && isSharedRecord(record);
|
|
3128
|
+
const all = markers.filter((record) => record.marker === true && inTimeline(record));
|
|
2743
3129
|
const { launchTs, bundleTs, pageLoadTs } = errorsOnly ? markerWindow(all) : {
|
|
2744
3130
|
launchTs: null,
|
|
2745
3131
|
bundleTs: null,
|
|
@@ -2757,9 +3143,8 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
|
|
|
2757
3143
|
bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
|
|
2758
3144
|
});
|
|
2759
3145
|
const slotMarkers = launchMarkersBySlot(all);
|
|
2760
|
-
const
|
|
2761
|
-
const
|
|
2762
|
-
const bundles = newestByPlatform(markers.filter((record) => record.src === "metro"), bundleMarkerPlatform);
|
|
3146
|
+
const launches = newestByPlatform(all.filter((record) => record.src !== "metro" && !isWebPageRecord(record)), (record) => typeof record.platform === "string" ? record.platform : null);
|
|
3147
|
+
const bundles = newestByPlatform(all.filter((record) => record.src === "metro"), bundleMarkerPlatform);
|
|
2763
3148
|
const slotPlatforms = slot === void 0 ? [] : [...launches.keys()].filter((platform) => platform !== null);
|
|
2764
3149
|
const windowStart = (record) => {
|
|
2765
3150
|
if (pageLoadTs !== null && isWebPageRecord(record)) return pageLoadTs - 1;
|
|
@@ -2775,13 +3160,58 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
|
|
|
2775
3160
|
const platform = nativePlatform(record);
|
|
2776
3161
|
return newestWhere(bundles, (marker) => marker === null || (platform ? marker === platform : slotPlatforms.length === 0 || slotPlatforms.includes(marker)));
|
|
2777
3162
|
};
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
3163
|
+
const lowestWindowStart = (slots) => {
|
|
3164
|
+
if (!criteria.errorsOnly) return void 0;
|
|
3165
|
+
const starts = [];
|
|
3166
|
+
if (pageLoadTs !== null) starts.push(pageLoadTs - 1);
|
|
3167
|
+
const kinds = criteria.sources ?? [];
|
|
3168
|
+
if (criteria.includeAppDeviceErrors || kinds.some((src) => src !== "metro" && src !== "client")) {
|
|
3169
|
+
const possible = slot !== void 0 ? [slot] : slots ? [...slots] : null;
|
|
3170
|
+
if (!possible) return void 0;
|
|
3171
|
+
for (const value of possible) {
|
|
3172
|
+
const start = slotMarkers.get(value);
|
|
3173
|
+
if (start === void 0) return void 0;
|
|
3174
|
+
starts.push(start);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (kinds.includes("metro") || kinds.includes("client")) {
|
|
3178
|
+
if (criteria.markerTs === void 0) return void 0;
|
|
3179
|
+
starts.push(criteria.markerTs, ...launches.values());
|
|
3180
|
+
}
|
|
3181
|
+
return starts.length ? Math.min(...starts) : void 0;
|
|
3182
|
+
};
|
|
3183
|
+
return {
|
|
3184
|
+
inTimeline,
|
|
3185
|
+
matches: (record) => inTimeline(record) && !otherPlatform(record) && recordMatches(record, {
|
|
3186
|
+
...criteria,
|
|
3187
|
+
markerTs: windowStart(record),
|
|
3188
|
+
bundleMarkerTs: bundleWindowStart(record)
|
|
3189
|
+
}),
|
|
3190
|
+
launchTs,
|
|
3191
|
+
sinceTs: criteria.sinceTs,
|
|
3192
|
+
excludesUpTo: (maxTs, slots) => {
|
|
3193
|
+
if (criteria.sinceTs !== void 0 && maxTs < criteria.sinceTs) return true;
|
|
3194
|
+
const start = lowestWindowStart(slots);
|
|
3195
|
+
return start !== void 0 && maxTs <= start;
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
|
|
3200
|
+
const source = records ?? readLogRecords(dir);
|
|
3201
|
+
const plan = planLogQuery(source.filter((record) => record.marker === true), {
|
|
3202
|
+
slot,
|
|
3203
|
+
sources,
|
|
3204
|
+
minLevel,
|
|
3205
|
+
since,
|
|
3206
|
+
grep,
|
|
3207
|
+
errorsOnly,
|
|
3208
|
+
now
|
|
3209
|
+
});
|
|
3210
|
+
const all = source.filter(plan.inTimeline);
|
|
3211
|
+
if (all.length === 0) return [];
|
|
3212
|
+
let matched = all.filter(plan.matches);
|
|
2783
3213
|
if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
|
|
2784
|
-
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs,
|
|
3214
|
+
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), plan.launchTs, plan.sinceTs) : matched;
|
|
2785
3215
|
}
|
|
2786
3216
|
/** Queries the JSON timeline, including Expo context fields on errors as `stim logs --json` does. */
|
|
2787
3217
|
function queryJsonLogs(options = {}) {
|
|
@@ -2826,10 +3256,12 @@ function advanceTail(prev, chunk, size) {
|
|
|
2826
3256
|
records
|
|
2827
3257
|
};
|
|
2828
3258
|
}
|
|
2829
|
-
function followLogs({ dir, onRecord, criteria = {}, intervalMs
|
|
3259
|
+
function followLogs({ dir, onRecord, criteria = {}, intervalMs, offsets = null, errorContext }) {
|
|
2830
3260
|
const follower = errorContext ? expoContextFollower(onRecord, errorContext) : null;
|
|
2831
3261
|
const state = /* @__PURE__ */ new Map();
|
|
2832
3262
|
const decoders = /* @__PURE__ */ new Map();
|
|
3263
|
+
let stopped = false;
|
|
3264
|
+
let contextTimer;
|
|
2833
3265
|
for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
|
|
2834
3266
|
state.set(name, {
|
|
2835
3267
|
offset: size,
|
|
@@ -2839,6 +3271,7 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
|
|
|
2839
3271
|
}
|
|
2840
3272
|
function emit(records) {
|
|
2841
3273
|
for (const record of records) {
|
|
3274
|
+
if (stopped) break;
|
|
2842
3275
|
const matches = recordMatches(record, criteria);
|
|
2843
3276
|
if (follower) follower.push(record, matches);
|
|
2844
3277
|
else if (matches) onRecord(record);
|
|
@@ -2868,14 +3301,105 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
|
|
|
2868
3301
|
emit(next.records);
|
|
2869
3302
|
}
|
|
2870
3303
|
function poll() {
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3304
|
+
if (stopped) return;
|
|
3305
|
+
for (const name of logFiles(dir)) {
|
|
3306
|
+
if (stopped) break;
|
|
3307
|
+
try {
|
|
3308
|
+
pollFile(name);
|
|
3309
|
+
} catch {}
|
|
3310
|
+
}
|
|
3311
|
+
if (intervalMs !== void 0) follower?.settle();
|
|
3312
|
+
else if (!contextTimer && follower?.pending()) {
|
|
3313
|
+
follower.settle();
|
|
3314
|
+
contextTimer = setTimeout(settleContext, 500);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
function settleContext() {
|
|
3318
|
+
if (stopped) return;
|
|
3319
|
+
poll();
|
|
3320
|
+
contextTimer = void 0;
|
|
3321
|
+
if (stopped) return;
|
|
2874
3322
|
follower?.settle();
|
|
3323
|
+
if (follower?.pending()) contextTimer = setTimeout(settleContext, 500);
|
|
3324
|
+
}
|
|
3325
|
+
let watcher;
|
|
3326
|
+
let directory;
|
|
3327
|
+
let scheduled;
|
|
3328
|
+
let repairTimer;
|
|
3329
|
+
function closeWatch() {
|
|
3330
|
+
const previous = watcher;
|
|
3331
|
+
watcher = void 0;
|
|
3332
|
+
directory = void 0;
|
|
3333
|
+
previous?.close();
|
|
3334
|
+
}
|
|
3335
|
+
function schedulePoll() {
|
|
3336
|
+
if (stopped || scheduled) return;
|
|
3337
|
+
scheduled = setImmediate(() => {
|
|
3338
|
+
scheduled = void 0;
|
|
3339
|
+
poll();
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
function sameDirectory() {
|
|
3343
|
+
if (!watcher || !directory) return false;
|
|
3344
|
+
try {
|
|
3345
|
+
const current = statSync(dir);
|
|
3346
|
+
return current.dev === directory.dev && current.ino === directory.ino;
|
|
3347
|
+
} catch {
|
|
3348
|
+
return false;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
function attachWatch() {
|
|
3352
|
+
if (stopped || watcher) return false;
|
|
3353
|
+
try {
|
|
3354
|
+
const current = statSync(dir);
|
|
3355
|
+
const next = watch(dir, { persistent: false }, (event) => {
|
|
3356
|
+
if (stopped) return;
|
|
3357
|
+
if (event === "rename" && !sameDirectory()) {
|
|
3358
|
+
closeWatch();
|
|
3359
|
+
armRepair(attachWatch() ? 500 : void 0);
|
|
3360
|
+
}
|
|
3361
|
+
schedulePoll();
|
|
3362
|
+
});
|
|
3363
|
+
watcher = next;
|
|
3364
|
+
directory = current;
|
|
3365
|
+
const lost = () => {
|
|
3366
|
+
if (watcher !== next) return;
|
|
3367
|
+
closeWatch();
|
|
3368
|
+
armRepair();
|
|
3369
|
+
};
|
|
3370
|
+
next.once("error", lost);
|
|
3371
|
+
next.once("close", lost);
|
|
3372
|
+
return true;
|
|
3373
|
+
} catch {}
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
function armRepair(delay = watcher ? 1e4 : 500) {
|
|
3377
|
+
clearTimeout(repairTimer);
|
|
3378
|
+
if (stopped) return;
|
|
3379
|
+
repairTimer = setTimeout(() => {
|
|
3380
|
+
let attached = false;
|
|
3381
|
+
if (!sameDirectory()) {
|
|
3382
|
+
closeWatch();
|
|
3383
|
+
attached = attachWatch();
|
|
3384
|
+
}
|
|
3385
|
+
poll();
|
|
3386
|
+
armRepair(attached ? 500 : void 0);
|
|
3387
|
+
}, delay);
|
|
3388
|
+
}
|
|
3389
|
+
const timer = intervalMs === void 0 ? void 0 : setInterval(poll, intervalMs);
|
|
3390
|
+
if (intervalMs === void 0) {
|
|
3391
|
+
attachWatch();
|
|
3392
|
+
poll();
|
|
3393
|
+
armRepair(500);
|
|
2875
3394
|
}
|
|
2876
|
-
const timer = setInterval(poll, intervalMs);
|
|
2877
3395
|
return function stop() {
|
|
3396
|
+
if (stopped) return;
|
|
3397
|
+
stopped = true;
|
|
2878
3398
|
clearInterval(timer);
|
|
3399
|
+
clearTimeout(repairTimer);
|
|
3400
|
+
clearTimeout(contextTimer);
|
|
3401
|
+
clearImmediate(scheduled);
|
|
3402
|
+
closeWatch();
|
|
2879
3403
|
follower?.flush();
|
|
2880
3404
|
};
|
|
2881
3405
|
}
|
|
@@ -2904,6 +3428,378 @@ function sortByTs(records) {
|
|
|
2904
3428
|
});
|
|
2905
3429
|
}
|
|
2906
3430
|
//#endregion
|
|
3431
|
+
//#region state/log-reader.ts
|
|
3432
|
+
const BLOCK_BYTES = 65536;
|
|
3433
|
+
const MAX_CACHED_GENERATIONS = 512;
|
|
3434
|
+
const NEWLINE = 10;
|
|
3435
|
+
const VALIDATE_BYTES = 256;
|
|
3436
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3437
|
+
const indexing = /* @__PURE__ */ new Map();
|
|
3438
|
+
function tsKey(record) {
|
|
3439
|
+
return typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : Infinity;
|
|
3440
|
+
}
|
|
3441
|
+
function compare(a, b) {
|
|
3442
|
+
return a.ts - b.ts || a.rank - b.rank || a.generation - b.generation || a.position - b.position;
|
|
3443
|
+
}
|
|
3444
|
+
function markerTs(record) {
|
|
3445
|
+
const ts = tsKey(record);
|
|
3446
|
+
return ts === Infinity ? -Infinity : ts;
|
|
3447
|
+
}
|
|
3448
|
+
function keepNewer(markers, key, marker) {
|
|
3449
|
+
const known = markers.get(key);
|
|
3450
|
+
if (!known || markerTs(marker) > markerTs(known)) markers.set(key, marker);
|
|
3451
|
+
}
|
|
3452
|
+
function markerKey(record) {
|
|
3453
|
+
const platform = record.src === "metro" ? bundleMarkerPlatform(record) : typeof record.platform === "string" ? record.platform : null;
|
|
3454
|
+
const ts = tsKey(record);
|
|
3455
|
+
return {
|
|
3456
|
+
key: JSON.stringify([
|
|
3457
|
+
record.src ?? null,
|
|
3458
|
+
record.slot === void 0 ? 0 : [record.slot],
|
|
3459
|
+
platform
|
|
3460
|
+
]),
|
|
3461
|
+
marker: {
|
|
3462
|
+
src: record.src,
|
|
3463
|
+
marker: true,
|
|
3464
|
+
...ts === Infinity ? {} : { ts },
|
|
3465
|
+
...record.slot === void 0 ? {} : { slot: record.slot },
|
|
3466
|
+
...platform === null ? {} : { platform }
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
function emptyIndex() {
|
|
3471
|
+
return {
|
|
3472
|
+
size: 0,
|
|
3473
|
+
head: Buffer.alloc(0),
|
|
3474
|
+
tail: Buffer.alloc(0),
|
|
3475
|
+
blocks: [],
|
|
3476
|
+
minTs: null,
|
|
3477
|
+
markers: /* @__PURE__ */ new Map(),
|
|
3478
|
+
launchAttempts: [],
|
|
3479
|
+
slots: /* @__PURE__ */ new Set()
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
function indexBlock(index, buffer, start) {
|
|
3483
|
+
let maxTs = -Infinity;
|
|
3484
|
+
let minTs = Infinity;
|
|
3485
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3486
|
+
const levels = /* @__PURE__ */ new Set();
|
|
3487
|
+
for (let lineStart = 0; lineStart < buffer.length;) {
|
|
3488
|
+
const lineEnd = buffer.indexOf(NEWLINE, lineStart);
|
|
3489
|
+
const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
3490
|
+
if (record) {
|
|
3491
|
+
const value = tsKey(record);
|
|
3492
|
+
maxTs = Math.max(maxTs, value);
|
|
3493
|
+
minTs = Math.min(minTs, value);
|
|
3494
|
+
if (value !== Infinity && (index.minTs === null || value < index.minTs)) index.minTs = value;
|
|
3495
|
+
sources.add(record.src);
|
|
3496
|
+
levels.add(record.level);
|
|
3497
|
+
index.slots.add(record.slot ?? "default");
|
|
3498
|
+
if (record.marker === true) {
|
|
3499
|
+
const { key, marker } = markerKey(record);
|
|
3500
|
+
keepNewer(index.markers, key, marker);
|
|
3501
|
+
}
|
|
3502
|
+
if (record.event === "launch_attempt") index.launchAttempts.push({
|
|
3503
|
+
record,
|
|
3504
|
+
position: start + lineStart
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
lineStart = lineEnd + 1;
|
|
3508
|
+
}
|
|
3509
|
+
return {
|
|
3510
|
+
start,
|
|
3511
|
+
end: start + buffer.length,
|
|
3512
|
+
maxTs,
|
|
3513
|
+
minTs,
|
|
3514
|
+
sources,
|
|
3515
|
+
levels
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
async function read(handle, position, length) {
|
|
3519
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
3520
|
+
let filled = 0;
|
|
3521
|
+
while (filled < length) {
|
|
3522
|
+
const { bytesRead } = await handle.read(buffer, filled, length - filled, position + filled);
|
|
3523
|
+
if (bytesRead === 0) break;
|
|
3524
|
+
filled += bytesRead;
|
|
3525
|
+
}
|
|
3526
|
+
return buffer.subarray(0, filled);
|
|
3527
|
+
}
|
|
3528
|
+
async function stillExtends(handle, index, size) {
|
|
3529
|
+
if (size < index.size) return false;
|
|
3530
|
+
const head = await read(handle, 0, index.head.length);
|
|
3531
|
+
const tail = await read(handle, index.size - index.tail.length, index.tail.length);
|
|
3532
|
+
return head.equals(index.head) && tail.equals(index.tail);
|
|
3533
|
+
}
|
|
3534
|
+
function resumeFrom(index) {
|
|
3535
|
+
const last = index.blocks.at(-1);
|
|
3536
|
+
if (!last || last.end - last.start >= BLOCK_BYTES) return {
|
|
3537
|
+
...index,
|
|
3538
|
+
blocks: [...index.blocks]
|
|
3539
|
+
};
|
|
3540
|
+
return {
|
|
3541
|
+
...index,
|
|
3542
|
+
size: last.start,
|
|
3543
|
+
blocks: index.blocks.slice(0, -1),
|
|
3544
|
+
launchAttempts: index.launchAttempts.filter((attempt) => attempt.position < last.start)
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
async function indexGeneration(handle, identity, size, signal) {
|
|
3548
|
+
signal?.throwIfAborted();
|
|
3549
|
+
const pending = indexing.get(identity);
|
|
3550
|
+
if (pending) {
|
|
3551
|
+
await new Promise((resolve, reject) => {
|
|
3552
|
+
const cancel = () => reject(signal?.reason);
|
|
3553
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
3554
|
+
if (signal?.aborted) cancel();
|
|
3555
|
+
pending.then(() => resolve(), () => resolve()).finally(() => signal?.removeEventListener("abort", cancel));
|
|
3556
|
+
});
|
|
3557
|
+
return indexGeneration(handle, identity, size, signal);
|
|
3558
|
+
}
|
|
3559
|
+
const building = buildGenerationIndex(handle, identity, size, signal);
|
|
3560
|
+
indexing.set(identity, building);
|
|
3561
|
+
try {
|
|
3562
|
+
return await building;
|
|
3563
|
+
} finally {
|
|
3564
|
+
indexing.delete(identity);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
async function buildGenerationIndex(handle, identity, size, signal) {
|
|
3568
|
+
signal?.throwIfAborted();
|
|
3569
|
+
const cached = cache.get(identity);
|
|
3570
|
+
const known = cached && await stillExtends(handle, cached, size) ? cached : null;
|
|
3571
|
+
if (known && known.size === size) {
|
|
3572
|
+
cache.delete(identity);
|
|
3573
|
+
cache.set(identity, known);
|
|
3574
|
+
return known;
|
|
3575
|
+
}
|
|
3576
|
+
const index = known ? resumeFrom(known) : emptyIndex();
|
|
3577
|
+
const next = {
|
|
3578
|
+
...index,
|
|
3579
|
+
markers: new Map(index.markers),
|
|
3580
|
+
launchAttempts: [...index.launchAttempts],
|
|
3581
|
+
slots: new Set(index.slots)
|
|
3582
|
+
};
|
|
3583
|
+
let offset = next.size;
|
|
3584
|
+
let length = BLOCK_BYTES;
|
|
3585
|
+
while (offset < size) {
|
|
3586
|
+
signal?.throwIfAborted();
|
|
3587
|
+
const buffer = await read(handle, offset, Math.min(length, size - offset));
|
|
3588
|
+
const end = buffer.lastIndexOf(NEWLINE);
|
|
3589
|
+
if (end === -1) {
|
|
3590
|
+
if (buffer.length < Math.min(length, size - offset) || offset + buffer.length >= size) break;
|
|
3591
|
+
length *= 2;
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
next.blocks.push(indexBlock(next, buffer.subarray(0, end + 1), offset));
|
|
3595
|
+
offset += end + 1;
|
|
3596
|
+
length = BLOCK_BYTES;
|
|
3597
|
+
signal?.throwIfAborted();
|
|
3598
|
+
await setImmediate$1();
|
|
3599
|
+
}
|
|
3600
|
+
next.size = offset;
|
|
3601
|
+
next.head = await read(handle, 0, Math.min(VALIDATE_BYTES, offset));
|
|
3602
|
+
next.tail = await read(handle, Math.max(0, offset - VALIDATE_BYTES), Math.min(VALIDATE_BYTES, offset));
|
|
3603
|
+
cache.delete(identity);
|
|
3604
|
+
cache.set(identity, next);
|
|
3605
|
+
while (cache.size > MAX_CACHED_GENERATIONS) cache.delete(cache.keys().next().value);
|
|
3606
|
+
return next;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Opens `dir` for reading. With `order: 'timeline'`, native crash files follow every other file at equal
|
|
3610
|
+
* timestamps, as `readLogTimeline` orders them; with `'name'`, every file is ranked by name, as `readLogRecords`.
|
|
3611
|
+
*/
|
|
3612
|
+
async function openLogDir(dir, { order = "timeline", signal } = {}) {
|
|
3613
|
+
signal?.throwIfAborted();
|
|
3614
|
+
let names;
|
|
3615
|
+
try {
|
|
3616
|
+
names = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".ndjson")).map((entry) => entry.name).toSorted();
|
|
3617
|
+
} catch {
|
|
3618
|
+
names = [];
|
|
3619
|
+
}
|
|
3620
|
+
const ranked = order === "name" ? names : [...names.filter((name) => !name.startsWith(NATIVE_CRASH_LOG_PREFIX)), ...names.filter((name) => name.startsWith(NATIVE_CRASH_LOG_PREFIX))];
|
|
3621
|
+
const generations = [];
|
|
3622
|
+
const sizes = {};
|
|
3623
|
+
const close = async () => {
|
|
3624
|
+
await Promise.all(generations.map((generation) => generation.handle.close().catch(() => {})));
|
|
3625
|
+
};
|
|
3626
|
+
try {
|
|
3627
|
+
for (const [rank, name] of ranked.entries()) for (const [generation, path] of [rotatedLogPath(join(dir, name)), join(dir, name)].entries()) {
|
|
3628
|
+
let handle;
|
|
3629
|
+
try {
|
|
3630
|
+
handle = await open(path, "r");
|
|
3631
|
+
} catch {
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
try {
|
|
3635
|
+
const stat = await handle.stat({ bigint: true });
|
|
3636
|
+
const index = await indexGeneration(handle, `${stat.dev}:${stat.ino}`, Number(stat.size), signal);
|
|
3637
|
+
generations.push({
|
|
3638
|
+
handle,
|
|
3639
|
+
rank,
|
|
3640
|
+
generation,
|
|
3641
|
+
crash: name.startsWith(NATIVE_CRASH_LOG_PREFIX),
|
|
3642
|
+
index
|
|
3643
|
+
});
|
|
3644
|
+
if (generation === 1) sizes[name] = index.size;
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
await handle.close().catch(() => {});
|
|
3647
|
+
throw error;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
await close();
|
|
3652
|
+
throw error;
|
|
3653
|
+
}
|
|
3654
|
+
let firstTs;
|
|
3655
|
+
for (const { index } of generations) if (index.minTs !== null && (firstTs === void 0 || index.minTs < firstTs)) firstTs = index.minTs;
|
|
3656
|
+
return {
|
|
3657
|
+
sizes,
|
|
3658
|
+
firstTs,
|
|
3659
|
+
launchAttempts: () => generations.filter((generation) => !generation.crash).flatMap(({ index, rank, generation }) => index.launchAttempts.map(({ record, position }) => ({
|
|
3660
|
+
ts: tsKey(record),
|
|
3661
|
+
rank,
|
|
3662
|
+
generation,
|
|
3663
|
+
position,
|
|
3664
|
+
record
|
|
3665
|
+
}))).toSorted(compare).map((entry) => entry.record),
|
|
3666
|
+
query: (query = {}) => queryGenerations(generations, query, signal),
|
|
3667
|
+
batches: (query = {}) => queryBatches(generations, query, signal),
|
|
3668
|
+
filter: (query = {}) => planLogQuery(generationMarkers(generations), query).matches,
|
|
3669
|
+
close
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
function blockMatches(block, { errorsOnly, minLevel, sources }) {
|
|
3673
|
+
if (errorsOnly && !block.levels.has("error") && !block.levels.has("fatal")) return false;
|
|
3674
|
+
if (minLevel && ![...block.levels].some((level) => levelRank(level) >= levelRank(minLevel))) return false;
|
|
3675
|
+
return !sources?.length || sources.some((source) => block.sources.has(source)) || Boolean(errorsOnly && block.sources.has("device"));
|
|
3676
|
+
}
|
|
3677
|
+
async function select(generations, extra, { matches, excludesUpTo, mayContain, tail, first = false }, signal) {
|
|
3678
|
+
if (tail !== void 0 && tail <= 0) return [];
|
|
3679
|
+
const heap = new BoundedSelection(compare, tail, first);
|
|
3680
|
+
const extraRank = generations.length ? Math.max(...generations.map((generation) => generation.rank)) + 1 : 0;
|
|
3681
|
+
for (const [position, record] of extra.entries()) {
|
|
3682
|
+
const entry = {
|
|
3683
|
+
ts: tsKey(record),
|
|
3684
|
+
rank: extraRank,
|
|
3685
|
+
generation: 0,
|
|
3686
|
+
position,
|
|
3687
|
+
record
|
|
3688
|
+
};
|
|
3689
|
+
if (matches(record, entry)) heap.keep(entry);
|
|
3690
|
+
}
|
|
3691
|
+
const blocks = generations.flatMap((generation) => generation.index.blocks.map((block) => ({
|
|
3692
|
+
generation,
|
|
3693
|
+
block
|
|
3694
|
+
}))).toSorted((a, b) => (first ? a.block.minTs - b.block.minTs : b.block.maxTs - a.block.maxTs) || 0);
|
|
3695
|
+
for (const { generation, block } of blocks) {
|
|
3696
|
+
if (excludesUpTo(block.maxTs)) {
|
|
3697
|
+
if (first) continue;
|
|
3698
|
+
break;
|
|
3699
|
+
}
|
|
3700
|
+
if (first && tail !== void 0 && heap.size >= tail && block.minTs > heap.peek().ts) break;
|
|
3701
|
+
if (!first && tail !== void 0 && heap.size >= tail && block.maxTs < heap.peek().ts) break;
|
|
3702
|
+
signal?.throwIfAborted();
|
|
3703
|
+
if (!mayContain(block, generation)) continue;
|
|
3704
|
+
const buffer = await read(generation.handle, block.start, block.end - block.start);
|
|
3705
|
+
for (let lineStart = 0; lineStart < buffer.length;) {
|
|
3706
|
+
const lineEnd = buffer.indexOf(NEWLINE, lineStart);
|
|
3707
|
+
if (lineEnd === -1) break;
|
|
3708
|
+
const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
3709
|
+
const position = block.start + lineStart;
|
|
3710
|
+
lineStart = lineEnd + 1;
|
|
3711
|
+
if (!record) continue;
|
|
3712
|
+
const entry = {
|
|
3713
|
+
ts: tsKey(record),
|
|
3714
|
+
rank: generation.rank,
|
|
3715
|
+
generation: generation.generation,
|
|
3716
|
+
position,
|
|
3717
|
+
record
|
|
3718
|
+
};
|
|
3719
|
+
if (matches(record, entry)) heap.keep(entry);
|
|
3720
|
+
}
|
|
3721
|
+
await setImmediate$1();
|
|
3722
|
+
}
|
|
3723
|
+
return heap.sorted();
|
|
3724
|
+
}
|
|
3725
|
+
function generationMarkers(generations) {
|
|
3726
|
+
const markers = /* @__PURE__ */ new Map();
|
|
3727
|
+
for (const { index } of generations) for (const [key, marker] of index.markers) keepNewer(markers, key, marker);
|
|
3728
|
+
return [...markers.values()];
|
|
3729
|
+
}
|
|
3730
|
+
async function selectQuery(generations, { tail, extra = [], ...options }, signal, page) {
|
|
3731
|
+
signal?.throwIfAborted();
|
|
3732
|
+
const slots = /* @__PURE__ */ new Set(["default"]);
|
|
3733
|
+
for (const { index } of generations) for (const slot of index.slots) slots.add(slot);
|
|
3734
|
+
for (const record of extra) slots.add(record.slot ?? "default");
|
|
3735
|
+
const plan = planLogQuery([...generationMarkers(generations), ...extra.filter((record) => record.marker === true)], options);
|
|
3736
|
+
return select(generations, extra, {
|
|
3737
|
+
matches: (record, entry) => plan.matches(record) && (!page?.after || compare(entry, page.after) > 0),
|
|
3738
|
+
excludesUpTo: (maxTs) => plan.excludesUpTo(maxTs, slots),
|
|
3739
|
+
mayContain: (block, generation) => blockMatches(block, options) && (!page?.after || compare({
|
|
3740
|
+
ts: block.maxTs,
|
|
3741
|
+
rank: generation.rank,
|
|
3742
|
+
generation: generation.generation,
|
|
3743
|
+
position: block.end
|
|
3744
|
+
}, page.after) > 0),
|
|
3745
|
+
tail: page?.size ?? tail,
|
|
3746
|
+
first: page !== void 0
|
|
3747
|
+
}, signal);
|
|
3748
|
+
}
|
|
3749
|
+
async function queryGenerations(generations, query, signal) {
|
|
3750
|
+
return withErrorContext(generations, query, await selectQuery(generations, query, signal), signal);
|
|
3751
|
+
}
|
|
3752
|
+
async function* queryBatches(generations, query, signal) {
|
|
3753
|
+
const fixed = {
|
|
3754
|
+
...query,
|
|
3755
|
+
now: query.now ?? Date.now()
|
|
3756
|
+
};
|
|
3757
|
+
let after;
|
|
3758
|
+
for (;;) {
|
|
3759
|
+
const selected = await selectQuery(generations, fixed, signal, {
|
|
3760
|
+
after,
|
|
3761
|
+
size: 500
|
|
3762
|
+
});
|
|
3763
|
+
if (!selected.length) return;
|
|
3764
|
+
yield await withErrorContext(generations, fixed, selected, signal);
|
|
3765
|
+
after = selected.at(-1);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
async function withErrorContext(generations, { extra = [], ...options }, selected, signal) {
|
|
3769
|
+
const matched = selected.map((entry) => entry.record);
|
|
3770
|
+
if (!options.errorsOnly) return matched;
|
|
3771
|
+
const errors = matched.filter((record) => record.src === "metro" && record.raw === true && (record.level === "error" || record.level === "fatal") && typeof record.msg === "string");
|
|
3772
|
+
if (!errors.length) return matched;
|
|
3773
|
+
const contextual = /* @__PURE__ */ new Map();
|
|
3774
|
+
for (const error of errors) {
|
|
3775
|
+
const [first] = await select(generations, extra, {
|
|
3776
|
+
matches: (record) => record.ts === error.ts && record.src === error.src && record.msg === error.msg && record.event === error.event,
|
|
3777
|
+
excludesUpTo: (maxTs) => maxTs < tsKey(error),
|
|
3778
|
+
mayContain: (block) => block.sources.has("metro"),
|
|
3779
|
+
tail: 1,
|
|
3780
|
+
first: true
|
|
3781
|
+
}, signal);
|
|
3782
|
+
if (!first) continue;
|
|
3783
|
+
let limit = 32;
|
|
3784
|
+
for (;;) {
|
|
3785
|
+
const stream = (await select(generations, extra, {
|
|
3786
|
+
matches: (record, entry) => compare(entry, first) >= 0 && record.src === "metro" && record.event === error.event && tsKey(record) >= tsKey(error) && (record.raw === true || record.ts === error.ts && record.msg === error.msg),
|
|
3787
|
+
excludesUpTo: (maxTs) => maxTs < tsKey(error),
|
|
3788
|
+
mayContain: (block) => block.sources.has("metro"),
|
|
3789
|
+
tail: limit,
|
|
3790
|
+
first: true
|
|
3791
|
+
}, signal)).map((entry) => entry.record);
|
|
3792
|
+
const index = stream.findIndex((record) => record.ts === error.ts && record.msg === error.msg);
|
|
3793
|
+
if (index < 0 || stream.slice(index + 1).some((record) => record.raw === true && !isExpoErrorContext(record, error.event)) || stream.length < limit) {
|
|
3794
|
+
contextual.set(error, attachExpoErrorContext(stream, [error], "field")[0]);
|
|
3795
|
+
break;
|
|
3796
|
+
}
|
|
3797
|
+
limit *= 2;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
return matched.map((record) => contextual.get(record) ?? record);
|
|
3801
|
+
}
|
|
3802
|
+
//#endregion
|
|
2907
3803
|
//#region state/recordings.ts
|
|
2908
3804
|
/** The footage stim-server keeps per device: the last 15 minutes recorded, however long ago. */
|
|
2909
3805
|
const RECORDING_FOOTAGE_MS = 9e5;
|
|
@@ -3034,10 +3930,13 @@ function validRunId(value) {
|
|
|
3034
3930
|
}
|
|
3035
3931
|
let current = null;
|
|
3036
3932
|
/**
|
|
3037
|
-
* The id of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
|
|
3038
|
-
* The CLI entry exports it back to `STIM_RUN_ID` so processes the run
|
|
3933
|
+
* The run id of the ambient operation context, else of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
|
|
3934
|
+
* (Stim Desktop passes one), else a random one. The CLI entry exports it back to `STIM_RUN_ID` so processes the run
|
|
3935
|
+
* starts inherit it; this function does not.
|
|
3039
3936
|
*/
|
|
3040
3937
|
function runId(env = process.env) {
|
|
3938
|
+
const context = ambientContext();
|
|
3939
|
+
if (context) return context.runId;
|
|
3041
3940
|
if (current === null) current = validRunId(env["STIM_RUN_ID"]) ?? randomBytes(6).toString("hex");
|
|
3042
3941
|
return current;
|
|
3043
3942
|
}
|
|
@@ -3249,154 +4148,6 @@ const MACHINE_OWNER_KINDS = [
|
|
|
3249
4148
|
"shared"
|
|
3250
4149
|
];
|
|
3251
4150
|
//#endregion
|
|
3252
|
-
//#region state/status-derived.ts
|
|
3253
|
-
const PLATFORMS$1 = ["ios", "android"];
|
|
3254
|
-
function isRunning(platform, device) {
|
|
3255
|
-
if (platform === "ios") return device.state === "Booted";
|
|
3256
|
-
return device.state === "detected" && !device.physical;
|
|
3257
|
-
}
|
|
3258
|
-
function appPresence(env, platform, device) {
|
|
3259
|
-
if (!isRunning(platform, device) || device.app?.state === "running") return null;
|
|
3260
|
-
const everBuilt = env.builds?.[platform]?.some((entry) => entry.result === "succeeded") ?? true;
|
|
3261
|
-
if (env.lastBuilds?.[platform]?.status === "failed" && !everBuilt) return "none";
|
|
3262
|
-
return device.app?.state === "stopped" ? "closed" : null;
|
|
3263
|
-
}
|
|
3264
|
-
function deviceSlots(env) {
|
|
3265
|
-
return [{
|
|
3266
|
-
slot: "default",
|
|
3267
|
-
ios: env.ios,
|
|
3268
|
-
android: env.android
|
|
3269
|
-
}, ...env.slots ?? []];
|
|
3270
|
-
}
|
|
3271
|
-
function latestBuild(env) {
|
|
3272
|
-
let latest = null;
|
|
3273
|
-
for (const platform of PLATFORMS$1) {
|
|
3274
|
-
const build = env.lastBuilds?.[platform];
|
|
3275
|
-
if (build && (latest === null || Date.parse(build.startedAt) > Date.parse(latest.startedAt))) latest = build;
|
|
3276
|
-
}
|
|
3277
|
-
return latest;
|
|
3278
|
-
}
|
|
3279
|
-
function stage(kind, since, platform = null) {
|
|
3280
|
-
return {
|
|
3281
|
-
kind,
|
|
3282
|
-
since: since ?? null,
|
|
3283
|
-
platform,
|
|
3284
|
-
closedApps: []
|
|
3285
|
-
};
|
|
3286
|
-
}
|
|
3287
|
-
function workspaceStage(env) {
|
|
3288
|
-
if (env.macos?.build.state === "running") return stage("building", env.macos.build.startedAt, "macos");
|
|
3289
|
-
if (env.macos?.build.state === "failed") return stage("build-failed", env.macos.build.finishedAt ?? env.macos.build.startedAt, "macos");
|
|
3290
|
-
if (env.build?.state === "running") return stage("building", env.build.startedAt, env.build.platform);
|
|
3291
|
-
if (!env.live && env.phase === "warming") return stage("warming", env.phaseSince);
|
|
3292
|
-
if (!env.live && env.phase === "ready") return stage("ready", env.phaseSince);
|
|
3293
|
-
const latest = latestBuild(env);
|
|
3294
|
-
if (latest?.status === "failed") return stage("build-failed", latest.finishedAt ?? latest.startedAt, latest.platform);
|
|
3295
|
-
if (env.live || (env.remoteDevices?.length ?? 0) > 0) {
|
|
3296
|
-
const closedApps = deviceSlots(env).flatMap(({ slot, ...devices }) => PLATFORMS$1.flatMap((platform) => {
|
|
3297
|
-
const device = devices[platform];
|
|
3298
|
-
return device && appPresence(env, platform, device) === "closed" ? [{
|
|
3299
|
-
platform,
|
|
3300
|
-
slot
|
|
3301
|
-
}] : [];
|
|
3302
|
-
}));
|
|
3303
|
-
return {
|
|
3304
|
-
...stage("running", env.supervisor?.startedAt),
|
|
3305
|
-
closedApps
|
|
3306
|
-
};
|
|
3307
|
-
}
|
|
3308
|
-
const lastStop = env.metro?.lastStop;
|
|
3309
|
-
return stage("stopped", lastStop && "at" in lastStop ? lastStop.at : null);
|
|
3310
|
-
}
|
|
3311
|
-
function checksState(checks) {
|
|
3312
|
-
if (!checks) return null;
|
|
3313
|
-
if (checks.failing > 0) return "failing";
|
|
3314
|
-
if (checks.pending > 0) return "pending";
|
|
3315
|
-
return checks.passing > 0 ? "passing" : null;
|
|
3316
|
-
}
|
|
3317
|
-
function gitChip(worktree) {
|
|
3318
|
-
const git = worktree.git;
|
|
3319
|
-
if (!git) return null;
|
|
3320
|
-
const pull = worktree.pullRequest;
|
|
3321
|
-
const parts = [];
|
|
3322
|
-
const ahead = git.ahead ?? 0;
|
|
3323
|
-
const behind = git.behind ?? 0;
|
|
3324
|
-
if (ahead || behind) parts.push({
|
|
3325
|
-
kind: "arrows",
|
|
3326
|
-
ahead,
|
|
3327
|
-
behind
|
|
3328
|
-
});
|
|
3329
|
-
const uncommitted = git.changed + git.untracked;
|
|
3330
|
-
if (uncommitted) parts.push({
|
|
3331
|
-
kind: "changed",
|
|
3332
|
-
count: uncommitted
|
|
3333
|
-
});
|
|
3334
|
-
if (git.mergedInto !== null) {
|
|
3335
|
-
if (pull?.state !== "merged") parts.push({
|
|
3336
|
-
kind: "merged",
|
|
3337
|
-
into: git.mergedInto
|
|
3338
|
-
});
|
|
3339
|
-
} else if (git.upstream === null) parts.push({ kind: "no-upstream" });
|
|
3340
|
-
return {
|
|
3341
|
-
parts,
|
|
3342
|
-
ci: pull ? checksState(pull.checks) : null
|
|
3343
|
-
};
|
|
3344
|
-
}
|
|
3345
|
-
function buildCause(build) {
|
|
3346
|
-
if (("result" in build ? build.result : build.status) !== "failed") return null;
|
|
3347
|
-
const at = build.diagnostics?.find((d) => d.file && d.line !== null && d.line !== void 0);
|
|
3348
|
-
if (at) return {
|
|
3349
|
-
key: `${at.file}:${at.line}`,
|
|
3350
|
-
file: at.file,
|
|
3351
|
-
line: at.line
|
|
3352
|
-
};
|
|
3353
|
-
return {
|
|
3354
|
-
key: build.errorCode ?? "failed",
|
|
3355
|
-
file: null,
|
|
3356
|
-
line: null
|
|
3357
|
-
};
|
|
3358
|
-
}
|
|
3359
|
-
function withCauses(builds) {
|
|
3360
|
-
if (!builds) return builds;
|
|
3361
|
-
return Object.fromEntries(Object.entries(builds).map(([platform, list]) => [platform, list?.map((build) => withCause(build))]));
|
|
3362
|
-
}
|
|
3363
|
-
function withCause(build) {
|
|
3364
|
-
const cause = buildCause(build);
|
|
3365
|
-
return cause ? {
|
|
3366
|
-
...build,
|
|
3367
|
-
cause
|
|
3368
|
-
} : build;
|
|
3369
|
-
}
|
|
3370
|
-
function withGitChip(worktree) {
|
|
3371
|
-
const chip = gitChip(worktree);
|
|
3372
|
-
return chip ? {
|
|
3373
|
-
...worktree,
|
|
3374
|
-
gitChip: chip
|
|
3375
|
-
} : worktree;
|
|
3376
|
-
}
|
|
3377
|
-
/** The environment with the conclusions clients draw from it: `stage`, `appPresence`, `gitChip` and `cause`. */
|
|
3378
|
-
function withDerivedFacts(env) {
|
|
3379
|
-
const device = (platform, record) => record ? {
|
|
3380
|
-
...record,
|
|
3381
|
-
appPresence: appPresence(env, platform, record)
|
|
3382
|
-
} : record;
|
|
3383
|
-
const derived = {
|
|
3384
|
-
...env,
|
|
3385
|
-
stage: workspaceStage(env)
|
|
3386
|
-
};
|
|
3387
|
-
if (env.ios) derived.ios = device("ios", env.ios);
|
|
3388
|
-
if (env.android) derived.android = device("android", env.android);
|
|
3389
|
-
if (env.slots) derived.slots = env.slots.map((slot) => ({
|
|
3390
|
-
...slot,
|
|
3391
|
-
ios: device("ios", slot.ios),
|
|
3392
|
-
android: device("android", slot.android)
|
|
3393
|
-
}));
|
|
3394
|
-
if (env.worktree) derived.worktree = withGitChip(env.worktree);
|
|
3395
|
-
if (env.lastBuilds) derived.lastBuilds = Object.fromEntries(Object.entries(env.lastBuilds).map(([platform, build]) => [platform, withCause(build)]));
|
|
3396
|
-
if (env.builds) derived.builds = withCauses(env.builds);
|
|
3397
|
-
return derived;
|
|
3398
|
-
}
|
|
3399
|
-
//#endregion
|
|
3400
4151
|
//#region state/status-measures.ts
|
|
3401
4152
|
function cacheName(path) {
|
|
3402
4153
|
return `${createHash("sha256").update(path).digest("hex").slice(0, 32)}.json`;
|
|
@@ -3488,12 +4239,16 @@ function readPullRequestCache(path) {
|
|
|
3488
4239
|
if (typeof entry.checkedAt !== "string") return null;
|
|
3489
4240
|
const pullRequest = pullRequestOf(entry.pullRequest);
|
|
3490
4241
|
if (pullRequest === void 0) return null;
|
|
4242
|
+
const { containsHead, endedAt } = entry;
|
|
3491
4243
|
return {
|
|
3492
4244
|
path,
|
|
3493
4245
|
branch: entry.branch,
|
|
3494
4246
|
head: entry.head,
|
|
3495
4247
|
checkedAt: entry.checkedAt,
|
|
3496
|
-
pullRequest
|
|
4248
|
+
pullRequest,
|
|
4249
|
+
...typeof containsHead === "boolean" ? { containsHead } : {},
|
|
4250
|
+
...endedAt === null || typeof endedAt === "number" && Number.isFinite(endedAt) ? { endedAt } : {},
|
|
4251
|
+
...entry.partial === true ? { partial: true } : {}
|
|
3497
4252
|
};
|
|
3498
4253
|
}
|
|
3499
4254
|
/** The cached size of the folder at `path`, or null before it was measured. */
|
|
@@ -3593,7 +4348,7 @@ function readAgentSessionsCache() {
|
|
|
3593
4348
|
//#region state/stim-build.ts
|
|
3594
4349
|
/**
|
|
3595
4350
|
* A digest of Stim's built code: the first 16 hex characters of the sha256 over the sorted `*.mjs` names and
|
|
3596
|
-
* bytes of a `stim` `dist` directory. A client and a remote
|
|
4351
|
+
* bytes of a `stim` `dist` directory. A client and a remote machine offload only when both compute the same one.
|
|
3597
4352
|
*/
|
|
3598
4353
|
function stimBuildDigest(dir) {
|
|
3599
4354
|
try {
|
|
@@ -3858,7 +4613,22 @@ function lastUseFrom(state, logMtimes) {
|
|
|
3858
4613
|
return candidates.length ? Math.max(...candidates) : NaN;
|
|
3859
4614
|
}
|
|
3860
4615
|
const MAINTENANCE_LOG_NAME = "maintenance.ndjson";
|
|
4616
|
+
/**
|
|
4617
|
+
* When the workspace was last used, in epoch milliseconds. A workspace that recorded no use falls back to the mtime
|
|
4618
|
+
* of its workspace.json, which is written once when the workspace is created, so it is a lower bound on the last
|
|
4619
|
+
* use. NaN when its state.json exists but does not parse, or when workspace.json cannot be read either.
|
|
4620
|
+
*/
|
|
3861
4621
|
function workspaceLastUsed(root) {
|
|
4622
|
+
const used = recordedLastUse(root);
|
|
4623
|
+
if (Number.isFinite(used)) return used;
|
|
4624
|
+
if (existsSync(workspaceStateFile(root)) && readWorkspaceState(root) === null) return NaN;
|
|
4625
|
+
try {
|
|
4626
|
+
return statSync(workspaceMetadataFile(root)).mtimeMs;
|
|
4627
|
+
} catch {
|
|
4628
|
+
return NaN;
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
function recordedLastUse(root) {
|
|
3862
4632
|
const logs = workspaceLogsDir(root);
|
|
3863
4633
|
let mtimes = [];
|
|
3864
4634
|
try {
|
|
@@ -3936,7 +4706,7 @@ function readViewedDevices(dir = deviceViewersDir()) {
|
|
|
3936
4706
|
//#endregion
|
|
3937
4707
|
//#region state/stats.ts
|
|
3938
4708
|
const STATS_VERSION = 1;
|
|
3939
|
-
/** Where a compiling build ran: here, on a remote
|
|
4709
|
+
/** Where a compiling build ran: here, on a remote machine, or here after offloading it failed. */
|
|
3940
4710
|
const PLACEMENT_DECISIONS = [
|
|
3941
4711
|
"here",
|
|
3942
4712
|
"offloaded",
|
|
@@ -4084,7 +4854,7 @@ function normalize(record) {
|
|
|
4084
4854
|
const source = record.projects;
|
|
4085
4855
|
if (source && typeof source === "object" && !Array.isArray(source)) for (const [key, scope] of Object.entries(source)) projects[key] = normalizeScope(scope);
|
|
4086
4856
|
const history = {};
|
|
4087
|
-
if (
|
|
4857
|
+
if (isJsonObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
|
|
4088
4858
|
const normalized = normalizeHistory(scope);
|
|
4089
4859
|
if (Object.keys(normalized).length) history[key] = normalized;
|
|
4090
4860
|
}
|
|
@@ -4092,8 +4862,8 @@ function normalize(record) {
|
|
|
4092
4862
|
const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
|
|
4093
4863
|
const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
|
|
4094
4864
|
const buildMachines = {};
|
|
4095
|
-
if (
|
|
4096
|
-
for (const [name, totals] of Object.entries(record.buildMachines)) if (
|
|
4865
|
+
if (isJsonObject(record.buildMachines)) {
|
|
4866
|
+
for (const [name, totals] of Object.entries(record.buildMachines)) if (isJsonObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
|
|
4097
4867
|
}
|
|
4098
4868
|
return {
|
|
4099
4869
|
version: 1,
|
|
@@ -4107,7 +4877,7 @@ function normalize(record) {
|
|
|
4107
4877
|
};
|
|
4108
4878
|
}
|
|
4109
4879
|
function normalizePlacement(value) {
|
|
4110
|
-
if (!
|
|
4880
|
+
if (!isJsonObject(value)) return [];
|
|
4111
4881
|
const { decision, platform, reason, project, machine } = value;
|
|
4112
4882
|
if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
|
|
4113
4883
|
if (typeof reason !== "string" || typeof project !== "string") return [];
|
|
@@ -4130,7 +4900,7 @@ function normalizePlacement(value) {
|
|
|
4130
4900
|
}];
|
|
4131
4901
|
}
|
|
4132
4902
|
function normalizeCapacityRefusal(value) {
|
|
4133
|
-
if (!
|
|
4903
|
+
if (!isJsonObject(value)) return [];
|
|
4134
4904
|
const { at, kind, platform, max, workspace } = value;
|
|
4135
4905
|
if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
|
|
4136
4906
|
if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
|
|
@@ -4145,7 +4915,7 @@ function normalizeCapacityRefusal(value) {
|
|
|
4145
4915
|
}];
|
|
4146
4916
|
}
|
|
4147
4917
|
function normalizeCapacityWait(value) {
|
|
4148
|
-
if (!
|
|
4918
|
+
if (!isJsonObject(value) || value.kind !== "device-wait") return [];
|
|
4149
4919
|
const refusal = normalizeCapacityRefusal({
|
|
4150
4920
|
...value,
|
|
4151
4921
|
kind: "device"
|
|
@@ -4169,20 +4939,17 @@ function normalizeMachineTotals(totals) {
|
|
|
4169
4939
|
...typeof totals.lastFallbackAt === "string" ? { lastFallbackAt: totals.lastFallbackAt } : {}
|
|
4170
4940
|
};
|
|
4171
4941
|
}
|
|
4172
|
-
function isObject(value) {
|
|
4173
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4174
|
-
}
|
|
4175
4942
|
function normalizeHistory(scope) {
|
|
4176
4943
|
const history = {};
|
|
4177
|
-
if (!
|
|
4944
|
+
if (!isJsonObject(scope)) return history;
|
|
4178
4945
|
for (const platform of PLATFORMS) {
|
|
4179
4946
|
const lists = scope[platform];
|
|
4180
|
-
if (!
|
|
4947
|
+
if (!isJsonObject(lists)) continue;
|
|
4181
4948
|
const normalized = {};
|
|
4182
4949
|
for (const outcome of OUTCOMES) {
|
|
4183
4950
|
const samples = lists[outcome];
|
|
4184
4951
|
if (!Array.isArray(samples)) continue;
|
|
4185
|
-
const kept = samples.filter(
|
|
4952
|
+
const kept = samples.filter(isJsonObject).flatMap((sample) => {
|
|
4186
4953
|
const durationMs = wholeMs(sample.durationMs);
|
|
4187
4954
|
if (durationMs <= 0) return [];
|
|
4188
4955
|
return [{
|
|
@@ -4200,7 +4967,7 @@ function normalizeHistory(scope) {
|
|
|
4200
4967
|
}
|
|
4201
4968
|
function wholePhases(phases) {
|
|
4202
4969
|
const out = {};
|
|
4203
|
-
if (!
|
|
4970
|
+
if (!isJsonObject(phases)) return out;
|
|
4204
4971
|
for (const [name, ms] of Object.entries(phases)) {
|
|
4205
4972
|
const value = Number(ms);
|
|
4206
4973
|
if (Number.isFinite(value) && value >= 0) out[name] = Math.round(value);
|
|
@@ -4320,6 +5087,19 @@ function findProjectRoot(startDir) {
|
|
|
4320
5087
|
}
|
|
4321
5088
|
//#endregion
|
|
4322
5089
|
//#region state/device-host.ts
|
|
5090
|
+
const DEVICE_HOST_LIMITS = {
|
|
5091
|
+
prepareMs: 3e5,
|
|
5092
|
+
offerMs: 3e4,
|
|
5093
|
+
stopMs: 9e4,
|
|
5094
|
+
logsMs: 15e3,
|
|
5095
|
+
killGraceMs: 5e3
|
|
5096
|
+
};
|
|
5097
|
+
/** How many parked sessions one reserve inspects, and possibly retires, before it creates a device. */
|
|
5098
|
+
const HOSTED_PARKED_CANDIDATES = 3;
|
|
5099
|
+
/** How long a cancelled hosted worker run can take to settle: SIGTERM, SIGKILL after the grace, then it is abandoned. */
|
|
5100
|
+
function hostedWorkerSettleMs(limits = DEVICE_HOST_LIMITS) {
|
|
5101
|
+
return limits.killGraceMs * 2;
|
|
5102
|
+
}
|
|
4323
5103
|
function deviceHostRoot() {
|
|
4324
5104
|
return join(configDir(), "server", "device-host-sessions");
|
|
4325
5105
|
}
|
|
@@ -4974,7 +5754,14 @@ const KINDS = [
|
|
|
4974
5754
|
"would-stop-workspace",
|
|
4975
5755
|
"would-remove-worktree",
|
|
4976
5756
|
"would-remove-orphan",
|
|
4977
|
-
"would-unregister-cache"
|
|
5757
|
+
"would-unregister-cache",
|
|
5758
|
+
"would-delete-parked-device",
|
|
5759
|
+
"would-delete-devices",
|
|
5760
|
+
"would-clear-build-dirs",
|
|
5761
|
+
"would-delete-dependencies",
|
|
5762
|
+
"would-stop-gradle-daemons",
|
|
5763
|
+
"would-delete-gradle-version",
|
|
5764
|
+
"would-delete-temp-leftovers"
|
|
4978
5765
|
];
|
|
4979
5766
|
/** Rejects missing, corrupt or unsupported payloads without throwing. */
|
|
4980
5767
|
function parseMaintenanceState(value) {
|
|
@@ -4988,13 +5775,14 @@ function parseMaintenanceState(value) {
|
|
|
4988
5775
|
"metro-cache",
|
|
4989
5776
|
"compilation-cache",
|
|
4990
5777
|
"ccache",
|
|
5778
|
+
"owned-devices",
|
|
4991
5779
|
"other"
|
|
4992
5780
|
].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;
|
|
4993
5781
|
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") && (action.dir === void 0 || typeof action.dir === "string") && (action.olderThanDays === void 0 || finite(action.olderThanDays)) && (action.check === void 0 || [
|
|
4994
5782
|
"worktree",
|
|
4995
5783
|
"sweep",
|
|
4996
5784
|
"disk"
|
|
4997
|
-
].includes(String(action.check))))) return null;
|
|
5785
|
+
].includes(String(action.check))) && (action.level === void 0 || action.level === 2 || action.level === 3))) return null;
|
|
4998
5786
|
if (!Array.isArray(value.recent) || !value.recent.every((record) => isJsonObject(record) && record.src === "maintenance" && finite(record.ts) && typeof record.msg === "string" && [
|
|
4999
5787
|
"debug",
|
|
5000
5788
|
"info",
|
|
@@ -5010,7 +5798,12 @@ function parseMaintenanceState(value) {
|
|
|
5010
5798
|
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.mode !== "on" || !finite(value.lastPass.freedBytes) || value.lastPass.stopped !== 0 || !finite(value.lastPass.actions) || !strings(value.lastPass.blocked))) return null;
|
|
5011
5799
|
if (value.pressure !== null) {
|
|
5012
5800
|
const pressure = value.pressure;
|
|
5013
|
-
if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) ||
|
|
5801
|
+
if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) || pressure.diskLevel !== void 0 && ![
|
|
5802
|
+
0,
|
|
5803
|
+
1,
|
|
5804
|
+
2,
|
|
5805
|
+
3
|
|
5806
|
+
].some((level) => level === pressure.diskLevel) || !isJsonObject(pressure.memory) || ![
|
|
5014
5807
|
null,
|
|
5015
5808
|
"normal",
|
|
5016
5809
|
"warning",
|
|
@@ -5028,8 +5821,8 @@ function readMaintenanceAttempt() {
|
|
|
5028
5821
|
}
|
|
5029
5822
|
//#endregion
|
|
5030
5823
|
//#region state/agent-device.ts
|
|
5031
|
-
function agentDeviceStateDirs(home = homedir()) {
|
|
5032
|
-
const override =
|
|
5824
|
+
function agentDeviceStateDirs(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
|
|
5825
|
+
const override = env.AGENT_DEVICE_STATE_DIR?.trim();
|
|
5033
5826
|
const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
|
|
5034
5827
|
const workspaces = join(configDir(), "workspaces");
|
|
5035
5828
|
try {
|
|
@@ -5042,6 +5835,20 @@ function agentDeviceStateDirs(home = homedir()) {
|
|
|
5042
5835
|
} catch {}
|
|
5043
5836
|
return [...dirs];
|
|
5044
5837
|
}
|
|
5838
|
+
async function agentDeviceStateDirsAsync(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
|
|
5839
|
+
const override = env.AGENT_DEVICE_STATE_DIR?.trim();
|
|
5840
|
+
const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
|
|
5841
|
+
const workspaces = join(configDir(), "workspaces");
|
|
5842
|
+
try {
|
|
5843
|
+
for (const name of await readdir(workspaces)) {
|
|
5844
|
+
const dir = join(workspaces, name, "agent-device");
|
|
5845
|
+
try {
|
|
5846
|
+
if ((await stat(dir)).isDirectory()) dirs.add(dir);
|
|
5847
|
+
} catch {}
|
|
5848
|
+
}
|
|
5849
|
+
} catch {}
|
|
5850
|
+
return [...dirs];
|
|
5851
|
+
}
|
|
5045
5852
|
//#endregion
|
|
5046
5853
|
//#region state/archive.ts
|
|
5047
5854
|
function readArchives() {
|
|
@@ -5153,14 +5960,13 @@ const knownKeys = (value, keys) => Object.keys(value).every((key) => keys.includ
|
|
|
5153
5960
|
function parseSetupJournal(value) {
|
|
5154
5961
|
if (!isJsonObject(value) || !knownKeys(value, [
|
|
5155
5962
|
"v",
|
|
5156
|
-
"client",
|
|
5157
5963
|
"expiresAt",
|
|
5158
5964
|
"capabilities",
|
|
5159
5965
|
"steps",
|
|
5160
5966
|
"granted",
|
|
5161
5967
|
"done",
|
|
5162
5968
|
"exit"
|
|
5163
|
-
]) || value.v !== 1 ||
|
|
5969
|
+
]) || value.v !== 1 || 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, [
|
|
5164
5970
|
"id",
|
|
5165
5971
|
"state",
|
|
5166
5972
|
"title",
|
|
@@ -5186,4 +5992,4 @@ function isSetupJournalPrunable(journal, now) {
|
|
|
5186
5992
|
return Date.parse(journal.expiresAt) + 36e5 < now;
|
|
5187
5993
|
}
|
|
5188
5994
|
//#endregion
|
|
5189
|
-
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, DEBUG_ENV, 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, MAINTENANCE_LOG_NAME, 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, RUN_ID_ENV, 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, automaticMachineEnabled, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createDebugLog, createdDevicesFile, createdDevicesLock, debugLogDir, debugLoggingEnabled, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, diskUsageFailureFile, diskUsageWalkClaims, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer,
|
|
5995
|
+
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, DEBUG_ENV, DEVICE_HOST_LIMITS, 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, HOSTED_PARKED_CANDIDATES, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, KeyedEntities, LAST_BUILD_KEYS, LEVELS, MACHINE_CREDENTIALS_VERSION, MACHINE_OWNER_KINDS, MAINTENANCE_LOG_NAME, 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, RUN_ID_ENV, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, SUMMARY_ARCHIVES, 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, agentDeviceStateDirsAsync, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, applyKeyedDelta, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, automaticMachineEnabled, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, cannotHost, capDrops, clientIdentity, clientIdentityKeyFile, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createAsyncAgentActionReader, createDebugLog, createdDevicesFile, createdDevicesLock, debugLogDir, debugLoggingEnabled, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, diskUsageFailureFile, diskUsageWalkClaims, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAgentDriverName, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, hostedWorkerSettleMs, isExpoErrorContext, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCacheUsageFile, machineCapacity, machineEndpoint, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openLogDir, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMachineCredential, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, planLogQuery, projectRootDirectories, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readDiskUsageFailure, readEasSessionLedger, readEndedAgentSessions, readHomeIdentity, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMachineCacheUsage, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, requireAutomaticMachine, runId, saturation, serverLocalSocket, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, statusViews, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedAndroid, unreadableHostedIos, unreadableHostedNative, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, validRunId, validateAutomaticPool, wholePhases, windowsShortDir, windowsShortDirKey, withConfigLock, withConfigLockAsync, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock, workspaceSummary };
|