@stim-cli/core 1.20.2 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{core-BkgfKqJ8.mjs → core-BTsCO1nW.mjs} +444 -60
- package/dist/{index-VDWBflH3.d.mts → index-DKq1j8dw.d.mts} +340 -91
- package/dist/index.d.mts +101 -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/oversight.mjs +16 -6
- package/dist/{ownership-claim-O31Y5iIb.mjs → ownership-claim-DHUs_Dir.mjs} +1 -1
- package/dist/{ownership-claim-Xxxr9MPp.d.mts → ownership-claim-RY3fbm1D.d.mts} +3 -2
- 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 -2
- package/dist/{process-identity-BTyww2Xj.d.mts → process-identity-DVNGpF5c.d.mts} +1 -1
- package/dist/process-identity.d.mts +1 -1
- package/dist/process-identity.mjs +3 -1
- package/dist/{protocol-pH632kA3.d.mts → protocol-BjJt4ghB.d.mts} +632 -65
- package/dist/protocol.d.mts +2 -2
- package/dist/protocol.mjs +81 -10
- package/dist/receive-protocol.d.mts +1 -1
- package/dist/receive-protocol.mjs +20163 -14469
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +1261 -208
- 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/status-derived-C13g6eQt.mjs +0 -151
package/dist/state.mjs
CHANGED
|
@@ -1,16 +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 { C as workspaceName,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { C as workspaceName, H as ambientContext, J as readJsonObject, K as isJsonObject, Q as withDirLockAsync, T as workspaceStateDir, U as ambientEnv, X as withStateReadCache, Y as readStateFile, Z as withDirLock, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as readJsonFile, t as LOG_ROTATE_BYTES, u as configDir } from "./core-BTsCO1nW.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";
|
|
6
8
|
import { availableParallelism, homedir, loadavg } from "node:os";
|
|
7
|
-
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";
|
|
8
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { setImmediate as setImmediate$1 } from "node:timers/promises";
|
|
12
|
+
import { lstat, open, readFile, readdir, stat } from "node:fs/promises";
|
|
9
13
|
import { isIP } from "node:net";
|
|
10
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
|
|
11
70
|
//#region state/agent-actions.ts
|
|
12
71
|
const EVENTS_VERSION = 1;
|
|
13
|
-
function object$
|
|
72
|
+
function object$2(value) {
|
|
14
73
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15
74
|
}
|
|
16
75
|
function parseAgentEvents(lines) {
|
|
@@ -22,7 +81,7 @@ function parseAgentEvents(lines) {
|
|
|
22
81
|
for (const line of lines) {
|
|
23
82
|
let entry;
|
|
24
83
|
try {
|
|
25
|
-
entry = object$
|
|
84
|
+
entry = object$2(JSON.parse(line));
|
|
26
85
|
} catch {
|
|
27
86
|
continue;
|
|
28
87
|
}
|
|
@@ -55,7 +114,7 @@ function parseAgentEvents(lines) {
|
|
|
55
114
|
command,
|
|
56
115
|
summary: typeof summary === "string" ? summary : "",
|
|
57
116
|
failed,
|
|
58
|
-
details: object$
|
|
117
|
+
details: object$2(entry.details),
|
|
59
118
|
requestId
|
|
60
119
|
});
|
|
61
120
|
}
|
|
@@ -173,41 +232,62 @@ function readCompleteLines(path, start) {
|
|
|
173
232
|
}
|
|
174
233
|
}
|
|
175
234
|
function trackMacosOpen(cursor, event, unknownVersion) {
|
|
176
|
-
const flags = object$
|
|
235
|
+
const flags = object$2(event.details?.flags);
|
|
177
236
|
if (!event.failed) cursor.macos = event.details?.platform === "macos" || flags?.platform === "macos";
|
|
178
237
|
cursor.bundleId = cursor.macos && !event.failed && !unknownVersion && flags?.surface === "app" && typeof event.details?.appBundleId === "string" ? event.details.appBundleId : null;
|
|
179
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
|
+
}
|
|
180
256
|
/**
|
|
181
257
|
* Reads agent-device session actions incrementally from the supplied sessions directories. With targets,
|
|
182
258
|
* records are restricted to those devices; without targets, retained session metadata supplies attribution.
|
|
183
259
|
* Missing directories and unreadable files yield no records.
|
|
184
260
|
*/
|
|
185
|
-
function
|
|
261
|
+
function agentActionReads({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
|
|
186
262
|
const cursors = /* @__PURE__ */ new Map();
|
|
187
|
-
return ()
|
|
263
|
+
return function* (selection) {
|
|
188
264
|
const targets = typeof resolveTargets === "function" ? resolveTargets() : resolveTargets;
|
|
189
265
|
if (targets && !targets.length) return [];
|
|
190
266
|
const byId = new Map(targets?.map((target) => [target.id, target]));
|
|
191
267
|
const macosByBundle = new Map(targets?.flatMap((target) => target.platform === "macos" ? [[target.bundleId, target]] : []));
|
|
192
|
-
const sessionDirs =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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);
|
|
204
281
|
for (const { name, sessionsDir, dir } of sessionDirs) {
|
|
205
282
|
const eventsPath = join(dir, "events.ndjson");
|
|
206
283
|
const lines = [];
|
|
207
284
|
let cursor = cursors.get(dir);
|
|
208
285
|
if (!cursor) {
|
|
209
286
|
try {
|
|
210
|
-
if (
|
|
287
|
+
if ((yield {
|
|
288
|
+
kind: "stat",
|
|
289
|
+
path: eventsPath
|
|
290
|
+
}).mtimeMs < sinceTs) continue;
|
|
211
291
|
} catch {
|
|
212
292
|
continue;
|
|
213
293
|
}
|
|
@@ -224,19 +304,45 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
224
304
|
};
|
|
225
305
|
cursors.set(dir, cursor);
|
|
226
306
|
try {
|
|
227
|
-
lines.push(...
|
|
307
|
+
lines.push(...(yield {
|
|
308
|
+
kind: "text",
|
|
309
|
+
path: `${eventsPath}.1`
|
|
310
|
+
}).split("\n"));
|
|
228
311
|
} catch {}
|
|
229
312
|
}
|
|
230
|
-
let chunk =
|
|
313
|
+
let chunk = yield {
|
|
314
|
+
kind: "lines",
|
|
315
|
+
path: eventsPath,
|
|
316
|
+
start: cursor.offset
|
|
317
|
+
};
|
|
231
318
|
if (chunk && (chunk.size < cursor.offset || cursor.identity && chunk.identity !== cursor.identity)) {
|
|
232
319
|
cursor.bundleId = null;
|
|
233
|
-
chunk =
|
|
320
|
+
chunk = yield {
|
|
321
|
+
kind: "lines",
|
|
322
|
+
path: eventsPath,
|
|
323
|
+
start: 0
|
|
324
|
+
};
|
|
234
325
|
}
|
|
235
326
|
if (!chunk) continue;
|
|
236
327
|
cursor.identity = chunk.identity;
|
|
237
328
|
cursor.offset = chunk.next;
|
|
238
329
|
lines.push(...chunk.text.split("\n"));
|
|
239
|
-
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
|
+
}
|
|
240
346
|
for (const [requestId, at] of parsed.started) cursor.started.set(requestId, at);
|
|
241
347
|
cursor.session ??= parsed.session;
|
|
242
348
|
const session = cursor.session;
|
|
@@ -248,12 +354,18 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
248
354
|
const runnerPath = join(dir, "runner.log");
|
|
249
355
|
let runnerSize = -1;
|
|
250
356
|
try {
|
|
251
|
-
runnerSize =
|
|
357
|
+
runnerSize = (yield {
|
|
358
|
+
kind: "stat",
|
|
359
|
+
path: runnerPath
|
|
360
|
+
}).size;
|
|
252
361
|
} catch {}
|
|
253
362
|
if (cursor.runner?.size !== runnerSize) {
|
|
254
363
|
let text = "";
|
|
255
364
|
try {
|
|
256
|
-
text =
|
|
365
|
+
text = yield {
|
|
366
|
+
kind: "text",
|
|
367
|
+
path: runnerPath
|
|
368
|
+
};
|
|
257
369
|
} catch {}
|
|
258
370
|
cursor.runner = {
|
|
259
371
|
size: runnerSize,
|
|
@@ -274,11 +386,12 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
274
386
|
const target = !targets ? retainedTarget() : cursor.macos ? cursor.bundleId ? macosByBundle.get(cursor.bundleId) : void 0 : deviceId ? byId.get(deviceId) : void 0;
|
|
275
387
|
if (target) {
|
|
276
388
|
cursor.unknownReported = true;
|
|
277
|
-
|
|
389
|
+
keep(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
|
|
278
390
|
}
|
|
279
391
|
}
|
|
280
392
|
if (parsed.unknownVersion) cursor.bundleId = null;
|
|
281
|
-
for (const event of parsed.events) {
|
|
393
|
+
for (const [index, event] of parsed.events.entries()) {
|
|
394
|
+
if (index % 500 === 0) yield { kind: "turn" };
|
|
282
395
|
if (event.kind === "request.started") {
|
|
283
396
|
cursor.closing = event.command === "close" ? cursor.bundleId : null;
|
|
284
397
|
cursor.bundleId = null;
|
|
@@ -291,7 +404,7 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
291
404
|
cursor.closing = null;
|
|
292
405
|
const target = bundleId ? macosByBundle.get(bundleId) : void 0;
|
|
293
406
|
const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
|
|
294
|
-
if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt)
|
|
407
|
+
if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt) keep(agentRecord(event, target.id, target, startedAt));
|
|
295
408
|
if (event.command === "close") cursor.bundleId = null;
|
|
296
409
|
continue;
|
|
297
410
|
}
|
|
@@ -300,10 +413,99 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
|
|
|
300
413
|
const deviceId = !targets && cursor.platform === "android" ? null : deviceAt(event.ts);
|
|
301
414
|
const target = targets ? deviceId ? byId.get(deviceId) : void 0 : retainedTarget();
|
|
302
415
|
const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
|
|
303
|
-
if (target)
|
|
416
|
+
if (target) keep(agentRecord(event, deviceId || null, target, startedAt));
|
|
304
417
|
}
|
|
305
418
|
}
|
|
306
|
-
return
|
|
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;
|
|
444
|
+
}
|
|
445
|
+
step = iterator.next(value);
|
|
446
|
+
}
|
|
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;
|
|
307
509
|
};
|
|
308
510
|
}
|
|
309
511
|
//#endregion
|
|
@@ -315,7 +517,7 @@ function readAgentDeviceUsageCache() {
|
|
|
315
517
|
const value = readJsonObject(agentDeviceUsageFile());
|
|
316
518
|
if (!value || value.version !== 1) return null;
|
|
317
519
|
const { roots, ...usage } = value;
|
|
318
|
-
if (!validUsage(usage) || !validRoots(roots)) return null;
|
|
520
|
+
if (!validUsage$1(usage) || !validRoots(roots)) return null;
|
|
319
521
|
return {
|
|
320
522
|
usage,
|
|
321
523
|
roots
|
|
@@ -325,34 +527,34 @@ function readAgentDeviceUsage() {
|
|
|
325
527
|
const value = readJsonObject(agentDeviceUsageFile());
|
|
326
528
|
if (!value) return null;
|
|
327
529
|
const { roots: _roots, ...usage } = value;
|
|
328
|
-
return validUsage(usage) ? usage : null;
|
|
530
|
+
return validUsage$1(usage) ? usage : null;
|
|
329
531
|
}
|
|
330
|
-
function object(value) {
|
|
532
|
+
function object$1(value) {
|
|
331
533
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
332
534
|
}
|
|
333
|
-
function bytes(value) {
|
|
535
|
+
function bytes$1(value) {
|
|
334
536
|
return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
335
537
|
}
|
|
336
538
|
function sized(value) {
|
|
337
|
-
return object(value) && typeof value.dir === "string" && bytes(value.bytes);
|
|
539
|
+
return object$1(value) && typeof value.dir === "string" && bytes$1(value.bytes);
|
|
338
540
|
}
|
|
339
541
|
function count$1(value) {
|
|
340
542
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
341
543
|
}
|
|
342
|
-
function nullableString(value) {
|
|
544
|
+
function nullableString$1(value) {
|
|
343
545
|
return value === null || typeof value === "string";
|
|
344
546
|
}
|
|
345
547
|
function validRoots(value) {
|
|
346
|
-
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");
|
|
347
549
|
}
|
|
348
|
-
function validUsage(value) {
|
|
550
|
+
function validUsage$1(value) {
|
|
349
551
|
const { stateDir: state, runnerBuilds: runner, workspaces, hosted } = value;
|
|
350
|
-
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" && [
|
|
351
553
|
null,
|
|
352
554
|
"lease",
|
|
353
555
|
"lock",
|
|
354
556
|
"unreadable"
|
|
355
|
-
].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));
|
|
356
558
|
}
|
|
357
559
|
//#endregion
|
|
358
560
|
//#region state/swiftpm-cache-usage.ts
|
|
@@ -374,6 +576,30 @@ function readSwiftpmCacheUsage() {
|
|
|
374
576
|
};
|
|
375
577
|
}
|
|
376
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
|
|
377
603
|
//#region state/paths.ts
|
|
378
604
|
function getConfigPath() {
|
|
379
605
|
return join(configDir(), "config.json");
|
|
@@ -459,14 +685,14 @@ function diskUsageCacheDir() {
|
|
|
459
685
|
function diskUsageWalkClaims() {
|
|
460
686
|
return join(configDir(), "disk-usage-walk.claims");
|
|
461
687
|
}
|
|
462
|
-
/** The remote
|
|
688
|
+
/** The remote machines this machine asked for build access, each with its pinned stim-server key once approved. */
|
|
463
689
|
function buildMachinesFile() {
|
|
464
690
|
return join(configDir(), "build-machines.json");
|
|
465
691
|
}
|
|
466
692
|
function buildMachinesLock() {
|
|
467
693
|
return join(configDir(), "build-machines.lock");
|
|
468
694
|
}
|
|
469
|
-
/** The remote
|
|
695
|
+
/** The remote machines this machine asked for access, separate from build credentials. */
|
|
470
696
|
function deviceHostMachinesFile() {
|
|
471
697
|
return join(configDir(), "device-host-machines.json");
|
|
472
698
|
}
|
|
@@ -523,6 +749,14 @@ function archiveDir(id) {
|
|
|
523
749
|
function archiveLock() {
|
|
524
750
|
return join(archiveRoot(), ".lock");
|
|
525
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
|
+
}
|
|
526
760
|
function serverSetupDir() {
|
|
527
761
|
return join(configDir(), "server", "setup");
|
|
528
762
|
}
|
|
@@ -534,6 +768,13 @@ function setupJournalFile(hash) {
|
|
|
534
768
|
function withConfigLock(fn) {
|
|
535
769
|
return withDirLock(configLockPath(), fn, { ensureParent: () => ensurePrivateDir(configDir()) });
|
|
536
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
|
+
}
|
|
537
778
|
function configCorruptRepair(path = getConfigPath()) {
|
|
538
779
|
return `Repair the file, or move it aside to start over: mv "${path}" "${path}.broken"`;
|
|
539
780
|
}
|
|
@@ -546,7 +787,21 @@ function configCorrupt(reason, path = getConfigPath()) {
|
|
|
546
787
|
/** The build worker root: `server.workerRoot` when it is absolute, else `$STIM_HOME/build-worker`. */
|
|
547
788
|
function buildWorkerRoot(config = loadConfig()) {
|
|
548
789
|
const configured = config?.server?.workerRoot;
|
|
549
|
-
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");
|
|
550
805
|
}
|
|
551
806
|
function loadConfig() {
|
|
552
807
|
const p = getConfigPath();
|
|
@@ -589,7 +844,7 @@ function registryFromJson(config, key, path) {
|
|
|
589
844
|
function getProject(projectPath) {
|
|
590
845
|
return loadConfig()?.projects?.[projectPath] || null;
|
|
591
846
|
}
|
|
592
|
-
function getConcurrencyLimits({ env =
|
|
847
|
+
function getConcurrencyLimits({ env = ambientEnv() } = {}) {
|
|
593
848
|
const c = loadConfig()?.concurrency || {};
|
|
594
849
|
return {
|
|
595
850
|
maxBuilds: resolveLimit(env.STIM_MAX_BUILDS, c.maxBuilds),
|
|
@@ -611,7 +866,7 @@ function parseMax(raw, strings) {
|
|
|
611
866
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
|
|
612
867
|
return value;
|
|
613
868
|
}
|
|
614
|
-
function getParkedMax(platform, { config, env =
|
|
869
|
+
function getParkedMax(platform, { config, env = ambientEnv() } = {}) {
|
|
615
870
|
const { key, env: envKey } = MAX_SETTING[platform];
|
|
616
871
|
const fromEnv = env[envKey];
|
|
617
872
|
const explicit = fromEnv !== void 0 && fromEnv !== "";
|
|
@@ -644,22 +899,9 @@ function resolveLimit(envVal, cfgVal) {
|
|
|
644
899
|
function getProjectSettings(projectPath) {
|
|
645
900
|
return getProject(projectPath)?.settings || {};
|
|
646
901
|
}
|
|
647
|
-
function getProjectSetting(projectPath, dottedKey) {
|
|
648
|
-
return readNested(getProjectSettings(projectPath), dottedKey);
|
|
649
|
-
}
|
|
650
902
|
function getRepoSettings(gitCommonDir) {
|
|
651
903
|
return loadConfig()?.repos?.[gitCommonDir]?.settings || {};
|
|
652
904
|
}
|
|
653
|
-
function readNested(obj, dottedKey) {
|
|
654
|
-
if (!obj) return void 0;
|
|
655
|
-
const keys = dottedKey.split(".");
|
|
656
|
-
let cur = obj;
|
|
657
|
-
for (const k of keys) {
|
|
658
|
-
if (cur == null || typeof cur !== "object") return void 0;
|
|
659
|
-
cur = cur[k];
|
|
660
|
-
}
|
|
661
|
-
return cur;
|
|
662
|
-
}
|
|
663
905
|
function isPathPrefix(prefix, path) {
|
|
664
906
|
const head = comparablePath(prefix);
|
|
665
907
|
const candidate = comparablePath(path);
|
|
@@ -685,11 +927,6 @@ function allMetroPorts() {
|
|
|
685
927
|
if (!cfg?.projects) return [];
|
|
686
928
|
return Object.values(cfg.projects).map((p) => p.metroPort).filter((p) => typeof p === "number");
|
|
687
929
|
}
|
|
688
|
-
function findProjectByMetroPort(port) {
|
|
689
|
-
const cfg = loadConfig();
|
|
690
|
-
for (const [path, proj] of Object.entries(cfg?.projects || {})) if (proj.metroPort === port) return path;
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
930
|
//#endregion
|
|
694
931
|
//#region state/tailnet.ts
|
|
695
932
|
const DEFAULT_SERVE_PORT = 7443;
|
|
@@ -716,16 +953,25 @@ function findPeer(status, name) {
|
|
|
716
953
|
return [{
|
|
717
954
|
nodeId: peer.ID,
|
|
718
955
|
dnsName,
|
|
719
|
-
address
|
|
956
|
+
address,
|
|
957
|
+
...typeof peer.OS === "string" && peer.OS ? { os: peer.OS } : {}
|
|
720
958
|
}];
|
|
721
959
|
});
|
|
722
960
|
if (found.length === 0) return "missing";
|
|
723
961
|
return found.length === 1 ? found[0] : "ambiguous";
|
|
724
962
|
}
|
|
725
963
|
/**
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
|
|
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.
|
|
729
975
|
*/
|
|
730
976
|
function endpoint(peer, port) {
|
|
731
977
|
const address = peer.address.includes(":") ? `[${peer.address}]` : peer.address;
|
|
@@ -737,24 +983,30 @@ function endpoint(peer, port) {
|
|
|
737
983
|
};
|
|
738
984
|
}
|
|
739
985
|
/**
|
|
740
|
-
*
|
|
741
|
-
*
|
|
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.
|
|
742
989
|
*/
|
|
743
|
-
function
|
|
744
|
-
const parsed = parseMachine(
|
|
745
|
-
if (!parsed) return `${
|
|
990
|
+
function machineEndpoint(machine, current) {
|
|
991
|
+
const parsed = parseMachine(machine);
|
|
992
|
+
if (!parsed) return `${machine} is not a tailnet name`;
|
|
746
993
|
if (!isJsonObject(current)) return "Tailscale is not running";
|
|
747
994
|
const peer = findPeer(current, parsed.name);
|
|
748
995
|
if (peer === "missing") return `no peer on this tailnet is named ${parsed.name}`;
|
|
749
996
|
if (peer === "ambiguous") return `several tailnet peers match ${parsed.name}`;
|
|
750
|
-
if (peer.nodeId !== credential.nodeId) return `${peer.dnsName} is now node ${peer.nodeId}, not the pinned ${credential.nodeId}; run stim doctor`;
|
|
751
997
|
return endpoint(peer, parsed.port);
|
|
752
998
|
}
|
|
753
999
|
//#endregion
|
|
754
1000
|
//#region state/hosted-macos.ts
|
|
755
1001
|
/** App slots one host offers; a slot names the host-assigned bundle id `<bundleId>.hosted<slot>`. */
|
|
756
1002
|
const HOSTED_MACOS_APP_SLOTS = 64;
|
|
1003
|
+
/** The drivers `hosting.agentDriver` can name: `none` and the first-party driver packages stim-server ships. */
|
|
757
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
|
+
}
|
|
758
1010
|
function hostedMacosAppSlot(value) {
|
|
759
1011
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 64;
|
|
760
1012
|
}
|
|
@@ -815,14 +1067,17 @@ function parseHostedAgentLease(value) {
|
|
|
815
1067
|
function parseHostedAgentGrant(value) {
|
|
816
1068
|
if (!isJsonObject(value)) return null;
|
|
817
1069
|
if (value.driver === "none") return Object.keys(value).length === 1 ? { driver: "none" } : null;
|
|
818
|
-
if (!
|
|
819
|
-
const
|
|
820
|
-
if (!lease) return null;
|
|
821
|
-
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 = {
|
|
822
1072
|
driver: value.driver,
|
|
823
1073
|
path: value.path,
|
|
824
1074
|
token: value.token,
|
|
825
|
-
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,
|
|
826
1081
|
lease: { ...lease }
|
|
827
1082
|
};
|
|
828
1083
|
}
|
|
@@ -832,9 +1087,9 @@ function parseHostedAgentAccess(value) {
|
|
|
832
1087
|
driver: "none",
|
|
833
1088
|
setting: "hosting.agentDriver"
|
|
834
1089
|
} : null;
|
|
835
|
-
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;
|
|
836
1091
|
return {
|
|
837
|
-
driver:
|
|
1092
|
+
driver: value.driver,
|
|
838
1093
|
remoteConfig: value.remoteConfig,
|
|
839
1094
|
command: value.command
|
|
840
1095
|
};
|
|
@@ -1011,7 +1266,7 @@ const SETTINGS = [
|
|
|
1011
1266
|
patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
|
|
1012
1267
|
},
|
|
1013
1268
|
scopes: EVERY,
|
|
1014
|
-
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"
|
|
1015
1270
|
},
|
|
1016
1271
|
{
|
|
1017
1272
|
key: "ios.projectPath",
|
|
@@ -1042,6 +1297,36 @@ const SETTINGS = [
|
|
|
1042
1297
|
scopes: PROJECT,
|
|
1043
1298
|
description: "SHA-1 of the signing identity, when two share a name"
|
|
1044
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
|
+
},
|
|
1045
1330
|
{
|
|
1046
1331
|
key: "ios.lanHost",
|
|
1047
1332
|
type: {
|
|
@@ -1117,6 +1402,12 @@ const SETTINGS = [
|
|
|
1117
1402
|
scopes: PROJECT,
|
|
1118
1403
|
description: "Gradle build variant"
|
|
1119
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
|
+
},
|
|
1120
1411
|
{
|
|
1121
1412
|
key: "android.offloadInputs",
|
|
1122
1413
|
type: OBJECT,
|
|
@@ -1144,7 +1435,7 @@ const SETTINGS = [
|
|
|
1144
1435
|
patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
|
|
1145
1436
|
},
|
|
1146
1437
|
scopes: EVERY,
|
|
1147
|
-
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"
|
|
1148
1439
|
},
|
|
1149
1440
|
{
|
|
1150
1441
|
key: "metro.tunnel",
|
|
@@ -1247,6 +1538,12 @@ const SETTINGS = [
|
|
|
1247
1538
|
scopes: PROJECT,
|
|
1248
1539
|
description: "Development Info.plist relative to the Swift Package directory."
|
|
1249
1540
|
},
|
|
1541
|
+
{
|
|
1542
|
+
key: "macos.artifactInputs",
|
|
1543
|
+
type: OBJECT,
|
|
1544
|
+
scopes: PROJECT,
|
|
1545
|
+
description: "Complete pinned, repeatable SwiftPM inputs producing a relocatable app for completed artifact caching."
|
|
1546
|
+
},
|
|
1250
1547
|
{
|
|
1251
1548
|
key: "macos.arguments",
|
|
1252
1549
|
type: { kind: "strings" },
|
|
@@ -1647,6 +1944,18 @@ const SETTINGS = [
|
|
|
1647
1944
|
env: "STIM_MAINTENANCE_OLDER_THAN_DAYS",
|
|
1648
1945
|
description: "Age in days for the age sweep: unused workspace build outputs and shared cache entries"
|
|
1649
1946
|
},
|
|
1947
|
+
{
|
|
1948
|
+
key: "maintenance.deviceIdleDays",
|
|
1949
|
+
type: {
|
|
1950
|
+
kind: "number",
|
|
1951
|
+
integer: true,
|
|
1952
|
+
minimum: 0
|
|
1953
|
+
},
|
|
1954
|
+
scopes: MACHINE,
|
|
1955
|
+
default: 3,
|
|
1956
|
+
env: "STIM_MAINTENANCE_DEVICE_IDLE_DAYS",
|
|
1957
|
+
description: "Days a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off"
|
|
1958
|
+
},
|
|
1650
1959
|
{
|
|
1651
1960
|
key: "maintenance.protectRecentHours",
|
|
1652
1961
|
type: {
|
|
@@ -1659,20 +1968,60 @@ const SETTINGS = [
|
|
|
1659
1968
|
env: "STIM_MAINTENANCE_PROTECT_RECENT_HOURS",
|
|
1660
1969
|
description: "Cache entries used within this many hours are never evicted"
|
|
1661
1970
|
},
|
|
1971
|
+
{
|
|
1972
|
+
key: "maintenance.urgentFreeDiskGb",
|
|
1973
|
+
type: GIGABYTES,
|
|
1974
|
+
scopes: MACHINE,
|
|
1975
|
+
default: 12,
|
|
1976
|
+
env: "STIM_MAINTENANCE_URGENT_FREE_DISK_GB",
|
|
1977
|
+
description: "Free disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off"
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
key: "maintenance.criticalFreeDiskGb",
|
|
1981
|
+
type: GIGABYTES,
|
|
1982
|
+
scopes: MACHINE,
|
|
1983
|
+
default: 6,
|
|
1984
|
+
env: "STIM_MAINTENANCE_CRITICAL_FREE_DISK_GB",
|
|
1985
|
+
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"
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
key: "maintenance.pressureProtectMinutes",
|
|
1989
|
+
type: {
|
|
1990
|
+
kind: "number",
|
|
1991
|
+
integer: true,
|
|
1992
|
+
minimum: 0
|
|
1993
|
+
},
|
|
1994
|
+
scopes: MACHINE,
|
|
1995
|
+
default: 10,
|
|
1996
|
+
env: "STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTES",
|
|
1997
|
+
description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3"
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
key: "maintenance.gradleVersionIdleDays",
|
|
2001
|
+
type: {
|
|
2002
|
+
kind: "number",
|
|
2003
|
+
integer: true,
|
|
2004
|
+
minimum: 0
|
|
2005
|
+
},
|
|
2006
|
+
scopes: MACHINE,
|
|
2007
|
+
default: 7,
|
|
2008
|
+
env: "STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYS",
|
|
2009
|
+
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"
|
|
2010
|
+
},
|
|
1662
2011
|
{
|
|
1663
2012
|
key: "maintenance.removeFinishedWorktrees",
|
|
1664
2013
|
type: BOOLEAN,
|
|
1665
2014
|
scopes: MACHINE,
|
|
1666
2015
|
default: true,
|
|
1667
2016
|
env: "STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREES",
|
|
1668
|
-
description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes"
|
|
2017
|
+
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"
|
|
1669
2018
|
},
|
|
1670
2019
|
{
|
|
1671
2020
|
key: "maintenance.keep",
|
|
1672
2021
|
type: BOOLEAN,
|
|
1673
2022
|
scopes: PROJECT,
|
|
1674
2023
|
default: false,
|
|
1675
|
-
description: "Pin this workspace: automatic maintenance never clears its build outputs or removes its worktree"
|
|
2024
|
+
description: "Pin this workspace: automatic maintenance never clears its build outputs, deletes its owned devices or removes its worktree"
|
|
1676
2025
|
},
|
|
1677
2026
|
{
|
|
1678
2027
|
key: "maintenance.workspaceOutputsMaxGb",
|
|
@@ -1761,7 +2110,7 @@ const SETTINGS = [
|
|
|
1761
2110
|
scopes: MACHINE,
|
|
1762
2111
|
default: false,
|
|
1763
2112
|
env: "STIM_DEBUG",
|
|
1764
|
-
description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other
|
|
2113
|
+
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"
|
|
1765
2114
|
},
|
|
1766
2115
|
{
|
|
1767
2116
|
key: "hosting.agentDriver",
|
|
@@ -1773,18 +2122,32 @@ const SETTINGS = [
|
|
|
1773
2122
|
default: "none",
|
|
1774
2123
|
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"
|
|
1775
2124
|
},
|
|
2125
|
+
{
|
|
2126
|
+
key: "server.buildsPaused",
|
|
2127
|
+
type: { kind: "boolean" },
|
|
2128
|
+
scopes: MACHINE,
|
|
2129
|
+
default: false,
|
|
2130
|
+
description: "Temporarily refuse new incoming builds without interrupting running builds, serving cached artifacts, or changing client approvals; local CLI builds remain available"
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
key: "server.devicesPaused",
|
|
2134
|
+
type: { kind: "boolean" },
|
|
2135
|
+
scopes: MACHINE,
|
|
2136
|
+
default: false,
|
|
2137
|
+
description: "Temporarily refuse new hosted iOS, Android and macOS sessions without interrupting existing sessions or changing client approvals; local CLI devices remain available"
|
|
2138
|
+
},
|
|
1776
2139
|
{
|
|
1777
2140
|
key: "server.acceptClientBuilds",
|
|
1778
2141
|
type: { kind: "boolean" },
|
|
1779
2142
|
scopes: MACHINE,
|
|
1780
2143
|
default: false,
|
|
1781
|
-
description: "Whether a
|
|
2144
|
+
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"
|
|
1782
2145
|
},
|
|
1783
2146
|
{
|
|
1784
2147
|
key: "remote.machines",
|
|
1785
2148
|
type: { kind: "strings" },
|
|
1786
2149
|
scopes: MACHINE,
|
|
1787
|
-
description: "Tailscale names of the remote
|
|
2150
|
+
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"
|
|
1788
2151
|
},
|
|
1789
2152
|
{
|
|
1790
2153
|
key: "remote.buildPoolDisabled",
|
|
@@ -1805,7 +2168,7 @@ const SETTINGS = [
|
|
|
1805
2168
|
type: BOOLEAN,
|
|
1806
2169
|
scopes: EVERY,
|
|
1807
2170
|
default: false,
|
|
1808
|
-
description: "When ios.remote or android.remote is auto, this
|
|
2171
|
+
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"
|
|
1809
2172
|
},
|
|
1810
2173
|
{
|
|
1811
2174
|
key: "remote.build",
|
|
@@ -1817,7 +2180,7 @@ const SETTINGS = [
|
|
|
1817
2180
|
scopes: MACHINE,
|
|
1818
2181
|
default: "auto",
|
|
1819
2182
|
env: "STIM_REMOTE_BUILD",
|
|
1820
|
-
description: "Build placement: auto follows remote.buildMode within enabled automatic pool members; local always builds here; a name in remote.machines requires that remote
|
|
2183
|
+
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"
|
|
1821
2184
|
},
|
|
1822
2185
|
{
|
|
1823
2186
|
key: "remote.buildMode",
|
|
@@ -1828,7 +2191,7 @@ const SETTINGS = [
|
|
|
1828
2191
|
scopes: MACHINE,
|
|
1829
2192
|
default: "auto",
|
|
1830
2193
|
env: "STIM_REMOTE_BUILD_MODE",
|
|
1831
|
-
description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this
|
|
2194
|
+
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"
|
|
1832
2195
|
},
|
|
1833
2196
|
{
|
|
1834
2197
|
key: "server.maxLoadPerCore",
|
|
@@ -1838,13 +2201,13 @@ const SETTINGS = [
|
|
|
1838
2201
|
},
|
|
1839
2202
|
scopes: MACHINE,
|
|
1840
2203
|
default: 2,
|
|
1841
|
-
description: "Load per core (5-minute load average divided by the CPU count) at which this
|
|
2204
|
+
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"
|
|
1842
2205
|
},
|
|
1843
2206
|
{
|
|
1844
2207
|
key: "server.workerRoot",
|
|
1845
2208
|
type: ABSOLUTE_PATH,
|
|
1846
2209
|
scopes: MACHINE,
|
|
1847
|
-
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the
|
|
2210
|
+
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the machines that build here; default $STIM_HOME/build-worker"
|
|
1848
2211
|
},
|
|
1849
2212
|
{
|
|
1850
2213
|
key: "server.gradleDaemonIdleMinutes",
|
|
@@ -1856,7 +2219,7 @@ const SETTINGS = [
|
|
|
1856
2219
|
},
|
|
1857
2220
|
scopes: MACHINE,
|
|
1858
2221
|
default: 30,
|
|
1859
|
-
description: "Minutes the Gradle daemon of an Android build another
|
|
2222
|
+
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"
|
|
1860
2223
|
},
|
|
1861
2224
|
{
|
|
1862
2225
|
key: "caches.buildCache",
|
|
@@ -1888,9 +2251,6 @@ function isLayeredSetting(setting) {
|
|
|
1888
2251
|
return setting.scopes.some((scope) => scope !== "machine");
|
|
1889
2252
|
}
|
|
1890
2253
|
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("."))))];
|
|
1891
|
-
function isPlainObject(value) {
|
|
1892
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1893
|
-
}
|
|
1894
2254
|
function bundleUrlAccepts(value, key) {
|
|
1895
2255
|
if (typeof value !== "string" || !/^(https?:\/\/|\/(?!\/))/.test(value) || /[\s\\#]/.test(value)) return false;
|
|
1896
2256
|
try {
|
|
@@ -1921,7 +2281,7 @@ function acceptsShape(setting, value) {
|
|
|
1921
2281
|
case "strings": return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1922
2282
|
case "number": return typeof value === "number";
|
|
1923
2283
|
case "choice": return typeof value === "string" && type.choices.includes(value);
|
|
1924
|
-
case "object": return
|
|
2284
|
+
case "object": return isJsonObject(value);
|
|
1925
2285
|
case "bundle-url": return bundleUrlAccepts(value, setting.key);
|
|
1926
2286
|
}
|
|
1927
2287
|
}
|
|
@@ -2026,7 +2386,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
|
|
|
2026
2386
|
if (refusal && !busy) throw refusal;
|
|
2027
2387
|
return null;
|
|
2028
2388
|
}
|
|
2029
|
-
/** Build slots held for a remote
|
|
2389
|
+
/** Build slots held for a remote machine's offloaded builds, which run under another Stim home. */
|
|
2030
2390
|
function liveOffloadedSlots() {
|
|
2031
2391
|
let names;
|
|
2032
2392
|
try {
|
|
@@ -2087,7 +2447,7 @@ function machineCapacity() {
|
|
|
2087
2447
|
maxLoadPerCore: maxLoadPerCore()
|
|
2088
2448
|
};
|
|
2089
2449
|
}
|
|
2090
|
-
/** Why a
|
|
2450
|
+
/** Why a machine with this capacity should not take another native build; null when it can. */
|
|
2091
2451
|
function saturation(capacity) {
|
|
2092
2452
|
if (capacity.maxBuilds > 0 && capacity.builds >= capacity.maxBuilds) return `all ${capacity.maxBuilds} build slots busy`;
|
|
2093
2453
|
if (capacity.loadPerCore >= capacity.maxLoadPerCore) return `load at or above ${capacity.maxLoadPerCore}/core`;
|
|
@@ -2095,41 +2455,55 @@ function saturation(capacity) {
|
|
|
2095
2455
|
}
|
|
2096
2456
|
//#endregion
|
|
2097
2457
|
//#region state/build-machines.ts
|
|
2098
|
-
|
|
2458
|
+
const MACHINE_CREDENTIALS_VERSION = 2;
|
|
2459
|
+
const KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
2460
|
+
/** A stored credential, or null when it is malformed or from an older version. */
|
|
2461
|
+
function parseMachineCredential(value) {
|
|
2099
2462
|
if (!isJsonObject(value)) return null;
|
|
2100
|
-
const { machine,
|
|
2463
|
+
const { machine, transport, address, state, requestedAt, serverKey, deviceId, ticket, expiresAt } = value;
|
|
2101
2464
|
if (![
|
|
2102
2465
|
machine,
|
|
2103
|
-
|
|
2104
|
-
dnsName,
|
|
2105
|
-
deviceId,
|
|
2106
|
-
deviceToken,
|
|
2466
|
+
address,
|
|
2107
2467
|
requestedAt
|
|
2108
2468
|
].every((field) => typeof field === "string" && field)) return null;
|
|
2109
|
-
if (state !== "pending" && state !== "approved") return null;
|
|
2469
|
+
if (transport !== "tailscale" || state !== "pending" && state !== "approved") return null;
|
|
2470
|
+
if (serverKey !== void 0 && (typeof serverKey !== "string" || !KEY.test(serverKey))) return null;
|
|
2471
|
+
if (ticket !== void 0 && (typeof ticket !== "string" || !KEY.test(ticket))) return null;
|
|
2472
|
+
if (state === "approved" && (serverKey === void 0 || typeof deviceId !== "string" || !deviceId)) return null;
|
|
2473
|
+
if (state === "pending" && ticket === void 0) return null;
|
|
2110
2474
|
return {
|
|
2111
2475
|
machine,
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
deviceId,
|
|
2115
|
-
deviceToken,
|
|
2476
|
+
transport,
|
|
2477
|
+
address,
|
|
2116
2478
|
state,
|
|
2117
2479
|
requestedAt,
|
|
2118
|
-
...
|
|
2119
|
-
...typeof
|
|
2480
|
+
...serverKey ? { serverKey } : {},
|
|
2481
|
+
...typeof deviceId === "string" && deviceId ? { deviceId } : {},
|
|
2482
|
+
...ticket ? { ticket } : {},
|
|
2483
|
+
...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {}
|
|
2120
2484
|
};
|
|
2121
2485
|
}
|
|
2122
2486
|
function readBuildMachines() {
|
|
2123
2487
|
try {
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2488
|
+
const store = readJsonFile(buildMachinesFile());
|
|
2489
|
+
if (store.version !== 2 || !Array.isArray(store.machines)) return [];
|
|
2490
|
+
return store.machines.flatMap((entry) => parseMachineCredential(entry) ?? []);
|
|
2126
2491
|
} catch {
|
|
2127
2492
|
return [];
|
|
2128
2493
|
}
|
|
2129
2494
|
}
|
|
2130
2495
|
//#endregion
|
|
2496
|
+
//#region state/client-identity.ts
|
|
2497
|
+
/** This machine's long-term X25519 key as a client of other machines, in `$STIM_HOME/client-identity.key`. */
|
|
2498
|
+
function clientIdentity() {
|
|
2499
|
+
return readOrCreateKeyFile(clientIdentityKeyFile());
|
|
2500
|
+
}
|
|
2501
|
+
//#endregion
|
|
2131
2502
|
//#region state/device-host-machines.ts
|
|
2132
|
-
/**
|
|
2503
|
+
/**
|
|
2504
|
+
* An absent store, or one from protocol 1, is empty; an unreadable or malformed one refuses access without discarding
|
|
2505
|
+
* pinned keys.
|
|
2506
|
+
*/
|
|
2133
2507
|
function readDeviceHostMachines() {
|
|
2134
2508
|
let value;
|
|
2135
2509
|
try {
|
|
@@ -2138,23 +2512,13 @@ function readDeviceHostMachines() {
|
|
|
2138
2512
|
if (error.code === "ENOENT") return [];
|
|
2139
2513
|
throw error;
|
|
2140
2514
|
}
|
|
2141
|
-
if (
|
|
2515
|
+
if (isJsonObject(value) && value.version === 1) return [];
|
|
2516
|
+
if (!isJsonObject(value) || value.version !== 2 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
|
|
2142
2517
|
const machines = [];
|
|
2143
2518
|
for (const entry of value.machines) {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
"dnsName",
|
|
2148
|
-
"deviceId",
|
|
2149
|
-
"deviceToken",
|
|
2150
|
-
"requestedAt"
|
|
2151
|
-
].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");
|
|
2152
|
-
const { ticketHash, expiresAt, ...credential } = entry;
|
|
2153
|
-
machines.push({
|
|
2154
|
-
...credential,
|
|
2155
|
-
...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {},
|
|
2156
|
-
...typeof ticketHash === "string" ? { ticketHash } : {}
|
|
2157
|
-
});
|
|
2519
|
+
const credential = parseMachineCredential(entry);
|
|
2520
|
+
if (!credential || machines.some((each) => each.machine === credential.machine)) throw new Error("Invalid device-host machine credential store");
|
|
2521
|
+
machines.push(credential);
|
|
2158
2522
|
}
|
|
2159
2523
|
return machines;
|
|
2160
2524
|
}
|
|
@@ -2174,11 +2538,15 @@ function requireAutomaticMachine(role, machine) {
|
|
|
2174
2538
|
remedy: `Enable it in remote.${role}PoolDisabled or select a machine explicitly.`
|
|
2175
2539
|
});
|
|
2176
2540
|
}
|
|
2177
|
-
|
|
2541
|
+
/**
|
|
2542
|
+
* Refuses a pool with no enabled member: local, or a configured remote approved for the role. `canHost` excludes
|
|
2543
|
+
* device-pool remotes that can never host, such as a Linux machine.
|
|
2544
|
+
*/
|
|
2545
|
+
function validateAutomaticPool(role, config, canHost = () => true) {
|
|
2178
2546
|
if (automaticMachineEnabled(role, "local", config)) return;
|
|
2179
2547
|
const configured = config.remote?.machines;
|
|
2180
2548
|
const approved = role === "build" ? readBuildMachines() : readDeviceHostMachines();
|
|
2181
|
-
if (Array.isArray(configured) && configured.some((machine) => typeof machine === "string" && automaticMachineEnabled(role, machine, config) && approved.some((entry) => entry.machine === machine && entry.state === "approved"))) return;
|
|
2549
|
+
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;
|
|
2182
2550
|
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" });
|
|
2183
2551
|
}
|
|
2184
2552
|
//#endregion
|
|
@@ -2192,7 +2560,7 @@ function debugLogDir() {
|
|
|
2192
2560
|
return join(configDir(), "logs", "debug");
|
|
2193
2561
|
}
|
|
2194
2562
|
/** The `STIM_DEBUG` override (1/true on, 0/false off, anything else off), else the machine setting `debug.logs`. Never throws. */
|
|
2195
|
-
function debugLoggingEnabled(env =
|
|
2563
|
+
function debugLoggingEnabled(env = ambientEnv()) {
|
|
2196
2564
|
const definition = settingDefinition("debug.logs");
|
|
2197
2565
|
const raw = env[DEBUG_ENV];
|
|
2198
2566
|
if (raw !== void 0 && raw !== "") {
|
|
@@ -2219,15 +2587,21 @@ function scrub(value, depth = 0) {
|
|
|
2219
2587
|
* Debug-level records for one component (`cli` or `server`). Off by default; `STIM_DEBUG=1` or `debug.logs` turns it
|
|
2220
2588
|
* on. Callers pass names, durations and codes, never arguments, environment values, tokens or tickets.
|
|
2221
2589
|
*/
|
|
2222
|
-
function createDebugLog(component, { env
|
|
2590
|
+
function createDebugLog(component, { env, now = Date.now, base = () => ({}) } = {}) {
|
|
2223
2591
|
let checkedAt = Number.NEGATIVE_INFINITY;
|
|
2224
2592
|
let on = false;
|
|
2225
2593
|
let unchecked = ROTATE_CHECK_BYTES;
|
|
2594
|
+
let checkedEnv;
|
|
2595
|
+
let checkedHome;
|
|
2226
2596
|
const enabled = () => {
|
|
2227
2597
|
const at = now();
|
|
2228
|
-
|
|
2598
|
+
const activeEnv = env ?? ambientEnv();
|
|
2599
|
+
const home = configDir();
|
|
2600
|
+
if (activeEnv !== checkedEnv || home !== checkedHome || at - checkedAt >= ENABLED_RECHECK_MS) {
|
|
2229
2601
|
checkedAt = at;
|
|
2230
|
-
|
|
2602
|
+
checkedEnv = activeEnv;
|
|
2603
|
+
checkedHome = home;
|
|
2604
|
+
on = debugLoggingEnabled(activeEnv);
|
|
2231
2605
|
}
|
|
2232
2606
|
return on;
|
|
2233
2607
|
};
|
|
@@ -2258,6 +2632,15 @@ function createDebugLog(component, { env = process.env, now = Date.now, base = (
|
|
|
2258
2632
|
};
|
|
2259
2633
|
}
|
|
2260
2634
|
//#endregion
|
|
2635
|
+
//#region state/home-identity.ts
|
|
2636
|
+
/** The persistent, nonsecret identity of this Stim home; null until the CLI initializes it. */
|
|
2637
|
+
function readHomeIdentity(config = loadConfig()) {
|
|
2638
|
+
const id = config?.homeId;
|
|
2639
|
+
if (id === void 0) return null;
|
|
2640
|
+
if (!validHomeId(id)) throw Object.assign(/* @__PURE__ */ new Error(`Stim home identity is not a UUID. ${configCorruptRepair()}`), { code: "STIM_CONFIG_CORRUPT" });
|
|
2641
|
+
return id.toLowerCase();
|
|
2642
|
+
}
|
|
2643
|
+
//#endregion
|
|
2261
2644
|
//#region state/ledgers.ts
|
|
2262
2645
|
function ids(value) {
|
|
2263
2646
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry !== "") : [];
|
|
@@ -2692,7 +3075,8 @@ function expoContextFollower(onRecord, into) {
|
|
|
2692
3075
|
return {
|
|
2693
3076
|
push,
|
|
2694
3077
|
settle,
|
|
2695
|
-
flush
|
|
3078
|
+
flush,
|
|
3079
|
+
pending: () => pending !== null
|
|
2696
3080
|
};
|
|
2697
3081
|
}
|
|
2698
3082
|
function includeBareErrorContext(all, matched, launchTs, sinceTs) {
|
|
@@ -2743,11 +3127,11 @@ function launchMarkersBySlot(records) {
|
|
|
2743
3127
|
}
|
|
2744
3128
|
return markers;
|
|
2745
3129
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const slotLaunched = slot !== void 0 &&
|
|
2749
|
-
const
|
|
2750
|
-
|
|
3130
|
+
/** Decides which records a query matches from the timeline's marker records alone. */
|
|
3131
|
+
function planLogQuery(markers, { slot, sources, minLevel, since, grep, errorsOnly, now } = {}) {
|
|
3132
|
+
const slotLaunched = slot !== void 0 && markers.some((record) => isLaunchMarker(record) && (record.slot ?? "default") === slot);
|
|
3133
|
+
const inTimeline = (record) => slot === void 0 || (record.slot ?? "default") === slot || slotLaunched && isSharedRecord(record);
|
|
3134
|
+
const all = markers.filter((record) => record.marker === true && inTimeline(record));
|
|
2751
3135
|
const { launchTs, bundleTs, pageLoadTs } = errorsOnly ? markerWindow(all) : {
|
|
2752
3136
|
launchTs: null,
|
|
2753
3137
|
bundleTs: null,
|
|
@@ -2765,9 +3149,8 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
|
|
|
2765
3149
|
bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
|
|
2766
3150
|
});
|
|
2767
3151
|
const slotMarkers = launchMarkersBySlot(all);
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2770
|
-
const bundles = newestByPlatform(markers.filter((record) => record.src === "metro"), bundleMarkerPlatform);
|
|
3152
|
+
const launches = newestByPlatform(all.filter((record) => record.src !== "metro" && !isWebPageRecord(record)), (record) => typeof record.platform === "string" ? record.platform : null);
|
|
3153
|
+
const bundles = newestByPlatform(all.filter((record) => record.src === "metro"), bundleMarkerPlatform);
|
|
2771
3154
|
const slotPlatforms = slot === void 0 ? [] : [...launches.keys()].filter((platform) => platform !== null);
|
|
2772
3155
|
const windowStart = (record) => {
|
|
2773
3156
|
if (pageLoadTs !== null && isWebPageRecord(record)) return pageLoadTs - 1;
|
|
@@ -2783,13 +3166,58 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
|
|
|
2783
3166
|
const platform = nativePlatform(record);
|
|
2784
3167
|
return newestWhere(bundles, (marker) => marker === null || (platform ? marker === platform : slotPlatforms.length === 0 || slotPlatforms.includes(marker)));
|
|
2785
3168
|
};
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3169
|
+
const lowestWindowStart = (slots) => {
|
|
3170
|
+
if (!criteria.errorsOnly) return void 0;
|
|
3171
|
+
const starts = [];
|
|
3172
|
+
if (pageLoadTs !== null) starts.push(pageLoadTs - 1);
|
|
3173
|
+
const kinds = criteria.sources ?? [];
|
|
3174
|
+
if (criteria.includeAppDeviceErrors || kinds.some((src) => src !== "metro" && src !== "client")) {
|
|
3175
|
+
const possible = slot !== void 0 ? [slot] : slots ? [...slots] : null;
|
|
3176
|
+
if (!possible) return void 0;
|
|
3177
|
+
for (const value of possible) {
|
|
3178
|
+
const start = slotMarkers.get(value);
|
|
3179
|
+
if (start === void 0) return void 0;
|
|
3180
|
+
starts.push(start);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
if (kinds.includes("metro") || kinds.includes("client")) {
|
|
3184
|
+
if (criteria.markerTs === void 0) return void 0;
|
|
3185
|
+
starts.push(criteria.markerTs, ...launches.values());
|
|
3186
|
+
}
|
|
3187
|
+
return starts.length ? Math.min(...starts) : void 0;
|
|
3188
|
+
};
|
|
3189
|
+
return {
|
|
3190
|
+
inTimeline,
|
|
3191
|
+
matches: (record) => inTimeline(record) && !otherPlatform(record) && recordMatches(record, {
|
|
3192
|
+
...criteria,
|
|
3193
|
+
markerTs: windowStart(record),
|
|
3194
|
+
bundleMarkerTs: bundleWindowStart(record)
|
|
3195
|
+
}),
|
|
3196
|
+
launchTs,
|
|
3197
|
+
sinceTs: criteria.sinceTs,
|
|
3198
|
+
excludesUpTo: (maxTs, slots) => {
|
|
3199
|
+
if (criteria.sinceTs !== void 0 && maxTs < criteria.sinceTs) return true;
|
|
3200
|
+
const start = lowestWindowStart(slots);
|
|
3201
|
+
return start !== void 0 && maxTs <= start;
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
|
|
3206
|
+
const source = records ?? readLogRecords(dir);
|
|
3207
|
+
const plan = planLogQuery(source.filter((record) => record.marker === true), {
|
|
3208
|
+
slot,
|
|
3209
|
+
sources,
|
|
3210
|
+
minLevel,
|
|
3211
|
+
since,
|
|
3212
|
+
grep,
|
|
3213
|
+
errorsOnly,
|
|
3214
|
+
now
|
|
3215
|
+
});
|
|
3216
|
+
const all = source.filter(plan.inTimeline);
|
|
3217
|
+
if (all.length === 0) return [];
|
|
3218
|
+
let matched = all.filter(plan.matches);
|
|
2791
3219
|
if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
|
|
2792
|
-
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs,
|
|
3220
|
+
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), plan.launchTs, plan.sinceTs) : matched;
|
|
2793
3221
|
}
|
|
2794
3222
|
/** Queries the JSON timeline, including Expo context fields on errors as `stim logs --json` does. */
|
|
2795
3223
|
function queryJsonLogs(options = {}) {
|
|
@@ -2834,10 +3262,12 @@ function advanceTail(prev, chunk, size) {
|
|
|
2834
3262
|
records
|
|
2835
3263
|
};
|
|
2836
3264
|
}
|
|
2837
|
-
function followLogs({ dir, onRecord, criteria = {}, intervalMs
|
|
3265
|
+
function followLogs({ dir, onRecord, criteria = {}, intervalMs, offsets = null, errorContext }) {
|
|
2838
3266
|
const follower = errorContext ? expoContextFollower(onRecord, errorContext) : null;
|
|
2839
3267
|
const state = /* @__PURE__ */ new Map();
|
|
2840
3268
|
const decoders = /* @__PURE__ */ new Map();
|
|
3269
|
+
let stopped = false;
|
|
3270
|
+
let contextTimer;
|
|
2841
3271
|
for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
|
|
2842
3272
|
state.set(name, {
|
|
2843
3273
|
offset: size,
|
|
@@ -2847,6 +3277,7 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
|
|
|
2847
3277
|
}
|
|
2848
3278
|
function emit(records) {
|
|
2849
3279
|
for (const record of records) {
|
|
3280
|
+
if (stopped) break;
|
|
2850
3281
|
const matches = recordMatches(record, criteria);
|
|
2851
3282
|
if (follower) follower.push(record, matches);
|
|
2852
3283
|
else if (matches) onRecord(record);
|
|
@@ -2876,14 +3307,105 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
|
|
|
2876
3307
|
emit(next.records);
|
|
2877
3308
|
}
|
|
2878
3309
|
function poll() {
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3310
|
+
if (stopped) return;
|
|
3311
|
+
for (const name of logFiles(dir)) {
|
|
3312
|
+
if (stopped) break;
|
|
3313
|
+
try {
|
|
3314
|
+
pollFile(name);
|
|
3315
|
+
} catch {}
|
|
3316
|
+
}
|
|
3317
|
+
if (intervalMs !== void 0) follower?.settle();
|
|
3318
|
+
else if (!contextTimer && follower?.pending()) {
|
|
3319
|
+
follower.settle();
|
|
3320
|
+
contextTimer = setTimeout(settleContext, 500);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function settleContext() {
|
|
3324
|
+
if (stopped) return;
|
|
3325
|
+
poll();
|
|
3326
|
+
contextTimer = void 0;
|
|
3327
|
+
if (stopped) return;
|
|
2882
3328
|
follower?.settle();
|
|
3329
|
+
if (follower?.pending()) contextTimer = setTimeout(settleContext, 500);
|
|
3330
|
+
}
|
|
3331
|
+
let watcher;
|
|
3332
|
+
let directory;
|
|
3333
|
+
let scheduled;
|
|
3334
|
+
let repairTimer;
|
|
3335
|
+
function closeWatch() {
|
|
3336
|
+
const previous = watcher;
|
|
3337
|
+
watcher = void 0;
|
|
3338
|
+
directory = void 0;
|
|
3339
|
+
previous?.close();
|
|
3340
|
+
}
|
|
3341
|
+
function schedulePoll() {
|
|
3342
|
+
if (stopped || scheduled) return;
|
|
3343
|
+
scheduled = setImmediate(() => {
|
|
3344
|
+
scheduled = void 0;
|
|
3345
|
+
poll();
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
function sameDirectory() {
|
|
3349
|
+
if (!watcher || !directory) return false;
|
|
3350
|
+
try {
|
|
3351
|
+
const current = statSync(dir);
|
|
3352
|
+
return current.dev === directory.dev && current.ino === directory.ino;
|
|
3353
|
+
} catch {
|
|
3354
|
+
return false;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function attachWatch() {
|
|
3358
|
+
if (stopped || watcher) return false;
|
|
3359
|
+
try {
|
|
3360
|
+
const current = statSync(dir);
|
|
3361
|
+
const next = watch(dir, { persistent: false }, (event) => {
|
|
3362
|
+
if (stopped) return;
|
|
3363
|
+
if (event === "rename" && !sameDirectory()) {
|
|
3364
|
+
closeWatch();
|
|
3365
|
+
armRepair(attachWatch() ? 500 : void 0);
|
|
3366
|
+
}
|
|
3367
|
+
schedulePoll();
|
|
3368
|
+
});
|
|
3369
|
+
watcher = next;
|
|
3370
|
+
directory = current;
|
|
3371
|
+
const lost = () => {
|
|
3372
|
+
if (watcher !== next) return;
|
|
3373
|
+
closeWatch();
|
|
3374
|
+
armRepair();
|
|
3375
|
+
};
|
|
3376
|
+
next.once("error", lost);
|
|
3377
|
+
next.once("close", lost);
|
|
3378
|
+
return true;
|
|
3379
|
+
} catch {}
|
|
3380
|
+
return false;
|
|
3381
|
+
}
|
|
3382
|
+
function armRepair(delay = watcher ? 1e4 : 500) {
|
|
3383
|
+
clearTimeout(repairTimer);
|
|
3384
|
+
if (stopped) return;
|
|
3385
|
+
repairTimer = setTimeout(() => {
|
|
3386
|
+
let attached = false;
|
|
3387
|
+
if (!sameDirectory()) {
|
|
3388
|
+
closeWatch();
|
|
3389
|
+
attached = attachWatch();
|
|
3390
|
+
}
|
|
3391
|
+
poll();
|
|
3392
|
+
armRepair(attached ? 500 : void 0);
|
|
3393
|
+
}, delay);
|
|
3394
|
+
}
|
|
3395
|
+
const timer = intervalMs === void 0 ? void 0 : setInterval(poll, intervalMs);
|
|
3396
|
+
if (intervalMs === void 0) {
|
|
3397
|
+
attachWatch();
|
|
3398
|
+
poll();
|
|
3399
|
+
armRepair(500);
|
|
2883
3400
|
}
|
|
2884
|
-
const timer = setInterval(poll, intervalMs);
|
|
2885
3401
|
return function stop() {
|
|
3402
|
+
if (stopped) return;
|
|
3403
|
+
stopped = true;
|
|
2886
3404
|
clearInterval(timer);
|
|
3405
|
+
clearTimeout(repairTimer);
|
|
3406
|
+
clearTimeout(contextTimer);
|
|
3407
|
+
clearImmediate(scheduled);
|
|
3408
|
+
closeWatch();
|
|
2887
3409
|
follower?.flush();
|
|
2888
3410
|
};
|
|
2889
3411
|
}
|
|
@@ -2912,6 +3434,378 @@ function sortByTs(records) {
|
|
|
2912
3434
|
});
|
|
2913
3435
|
}
|
|
2914
3436
|
//#endregion
|
|
3437
|
+
//#region state/log-reader.ts
|
|
3438
|
+
const BLOCK_BYTES = 65536;
|
|
3439
|
+
const MAX_CACHED_GENERATIONS = 512;
|
|
3440
|
+
const NEWLINE = 10;
|
|
3441
|
+
const VALIDATE_BYTES = 256;
|
|
3442
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3443
|
+
const indexing = /* @__PURE__ */ new Map();
|
|
3444
|
+
function tsKey(record) {
|
|
3445
|
+
return typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : Infinity;
|
|
3446
|
+
}
|
|
3447
|
+
function compare(a, b) {
|
|
3448
|
+
return a.ts - b.ts || a.rank - b.rank || a.generation - b.generation || a.position - b.position;
|
|
3449
|
+
}
|
|
3450
|
+
function markerTs(record) {
|
|
3451
|
+
const ts = tsKey(record);
|
|
3452
|
+
return ts === Infinity ? -Infinity : ts;
|
|
3453
|
+
}
|
|
3454
|
+
function keepNewer(markers, key, marker) {
|
|
3455
|
+
const known = markers.get(key);
|
|
3456
|
+
if (!known || markerTs(marker) > markerTs(known)) markers.set(key, marker);
|
|
3457
|
+
}
|
|
3458
|
+
function markerKey(record) {
|
|
3459
|
+
const platform = record.src === "metro" ? bundleMarkerPlatform(record) : typeof record.platform === "string" ? record.platform : null;
|
|
3460
|
+
const ts = tsKey(record);
|
|
3461
|
+
return {
|
|
3462
|
+
key: JSON.stringify([
|
|
3463
|
+
record.src ?? null,
|
|
3464
|
+
record.slot === void 0 ? 0 : [record.slot],
|
|
3465
|
+
platform
|
|
3466
|
+
]),
|
|
3467
|
+
marker: {
|
|
3468
|
+
src: record.src,
|
|
3469
|
+
marker: true,
|
|
3470
|
+
...ts === Infinity ? {} : { ts },
|
|
3471
|
+
...record.slot === void 0 ? {} : { slot: record.slot },
|
|
3472
|
+
...platform === null ? {} : { platform }
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
function emptyIndex() {
|
|
3477
|
+
return {
|
|
3478
|
+
size: 0,
|
|
3479
|
+
head: Buffer.alloc(0),
|
|
3480
|
+
tail: Buffer.alloc(0),
|
|
3481
|
+
blocks: [],
|
|
3482
|
+
minTs: null,
|
|
3483
|
+
markers: /* @__PURE__ */ new Map(),
|
|
3484
|
+
launchAttempts: [],
|
|
3485
|
+
slots: /* @__PURE__ */ new Set()
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
function indexBlock(index, buffer, start) {
|
|
3489
|
+
let maxTs = -Infinity;
|
|
3490
|
+
let minTs = Infinity;
|
|
3491
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3492
|
+
const levels = /* @__PURE__ */ new Set();
|
|
3493
|
+
for (let lineStart = 0; lineStart < buffer.length;) {
|
|
3494
|
+
const lineEnd = buffer.indexOf(NEWLINE, lineStart);
|
|
3495
|
+
const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
3496
|
+
if (record) {
|
|
3497
|
+
const value = tsKey(record);
|
|
3498
|
+
maxTs = Math.max(maxTs, value);
|
|
3499
|
+
minTs = Math.min(minTs, value);
|
|
3500
|
+
if (value !== Infinity && (index.minTs === null || value < index.minTs)) index.minTs = value;
|
|
3501
|
+
sources.add(record.src);
|
|
3502
|
+
levels.add(record.level);
|
|
3503
|
+
index.slots.add(record.slot ?? "default");
|
|
3504
|
+
if (record.marker === true) {
|
|
3505
|
+
const { key, marker } = markerKey(record);
|
|
3506
|
+
keepNewer(index.markers, key, marker);
|
|
3507
|
+
}
|
|
3508
|
+
if (record.event === "launch_attempt") index.launchAttempts.push({
|
|
3509
|
+
record,
|
|
3510
|
+
position: start + lineStart
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
lineStart = lineEnd + 1;
|
|
3514
|
+
}
|
|
3515
|
+
return {
|
|
3516
|
+
start,
|
|
3517
|
+
end: start + buffer.length,
|
|
3518
|
+
maxTs,
|
|
3519
|
+
minTs,
|
|
3520
|
+
sources,
|
|
3521
|
+
levels
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
async function read(handle, position, length) {
|
|
3525
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
3526
|
+
let filled = 0;
|
|
3527
|
+
while (filled < length) {
|
|
3528
|
+
const { bytesRead } = await handle.read(buffer, filled, length - filled, position + filled);
|
|
3529
|
+
if (bytesRead === 0) break;
|
|
3530
|
+
filled += bytesRead;
|
|
3531
|
+
}
|
|
3532
|
+
return buffer.subarray(0, filled);
|
|
3533
|
+
}
|
|
3534
|
+
async function stillExtends(handle, index, size) {
|
|
3535
|
+
if (size < index.size) return false;
|
|
3536
|
+
const head = await read(handle, 0, index.head.length);
|
|
3537
|
+
const tail = await read(handle, index.size - index.tail.length, index.tail.length);
|
|
3538
|
+
return head.equals(index.head) && tail.equals(index.tail);
|
|
3539
|
+
}
|
|
3540
|
+
function resumeFrom(index) {
|
|
3541
|
+
const last = index.blocks.at(-1);
|
|
3542
|
+
if (!last || last.end - last.start >= BLOCK_BYTES) return {
|
|
3543
|
+
...index,
|
|
3544
|
+
blocks: [...index.blocks]
|
|
3545
|
+
};
|
|
3546
|
+
return {
|
|
3547
|
+
...index,
|
|
3548
|
+
size: last.start,
|
|
3549
|
+
blocks: index.blocks.slice(0, -1),
|
|
3550
|
+
launchAttempts: index.launchAttempts.filter((attempt) => attempt.position < last.start)
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
async function indexGeneration(handle, identity, size, signal) {
|
|
3554
|
+
signal?.throwIfAborted();
|
|
3555
|
+
const pending = indexing.get(identity);
|
|
3556
|
+
if (pending) {
|
|
3557
|
+
await new Promise((resolve, reject) => {
|
|
3558
|
+
const cancel = () => reject(signal?.reason);
|
|
3559
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
3560
|
+
if (signal?.aborted) cancel();
|
|
3561
|
+
pending.then(() => resolve(), () => resolve()).finally(() => signal?.removeEventListener("abort", cancel));
|
|
3562
|
+
});
|
|
3563
|
+
return indexGeneration(handle, identity, size, signal);
|
|
3564
|
+
}
|
|
3565
|
+
const building = buildGenerationIndex(handle, identity, size, signal);
|
|
3566
|
+
indexing.set(identity, building);
|
|
3567
|
+
try {
|
|
3568
|
+
return await building;
|
|
3569
|
+
} finally {
|
|
3570
|
+
indexing.delete(identity);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
async function buildGenerationIndex(handle, identity, size, signal) {
|
|
3574
|
+
signal?.throwIfAborted();
|
|
3575
|
+
const cached = cache.get(identity);
|
|
3576
|
+
const known = cached && await stillExtends(handle, cached, size) ? cached : null;
|
|
3577
|
+
if (known && known.size === size) {
|
|
3578
|
+
cache.delete(identity);
|
|
3579
|
+
cache.set(identity, known);
|
|
3580
|
+
return known;
|
|
3581
|
+
}
|
|
3582
|
+
const index = known ? resumeFrom(known) : emptyIndex();
|
|
3583
|
+
const next = {
|
|
3584
|
+
...index,
|
|
3585
|
+
markers: new Map(index.markers),
|
|
3586
|
+
launchAttempts: [...index.launchAttempts],
|
|
3587
|
+
slots: new Set(index.slots)
|
|
3588
|
+
};
|
|
3589
|
+
let offset = next.size;
|
|
3590
|
+
let length = BLOCK_BYTES;
|
|
3591
|
+
while (offset < size) {
|
|
3592
|
+
signal?.throwIfAborted();
|
|
3593
|
+
const buffer = await read(handle, offset, Math.min(length, size - offset));
|
|
3594
|
+
const end = buffer.lastIndexOf(NEWLINE);
|
|
3595
|
+
if (end === -1) {
|
|
3596
|
+
if (buffer.length < Math.min(length, size - offset) || offset + buffer.length >= size) break;
|
|
3597
|
+
length *= 2;
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
next.blocks.push(indexBlock(next, buffer.subarray(0, end + 1), offset));
|
|
3601
|
+
offset += end + 1;
|
|
3602
|
+
length = BLOCK_BYTES;
|
|
3603
|
+
signal?.throwIfAborted();
|
|
3604
|
+
await setImmediate$1();
|
|
3605
|
+
}
|
|
3606
|
+
next.size = offset;
|
|
3607
|
+
next.head = await read(handle, 0, Math.min(VALIDATE_BYTES, offset));
|
|
3608
|
+
next.tail = await read(handle, Math.max(0, offset - VALIDATE_BYTES), Math.min(VALIDATE_BYTES, offset));
|
|
3609
|
+
cache.delete(identity);
|
|
3610
|
+
cache.set(identity, next);
|
|
3611
|
+
while (cache.size > MAX_CACHED_GENERATIONS) cache.delete(cache.keys().next().value);
|
|
3612
|
+
return next;
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Opens `dir` for reading. With `order: 'timeline'`, native crash files follow every other file at equal
|
|
3616
|
+
* timestamps, as `readLogTimeline` orders them; with `'name'`, every file is ranked by name, as `readLogRecords`.
|
|
3617
|
+
*/
|
|
3618
|
+
async function openLogDir(dir, { order = "timeline", signal } = {}) {
|
|
3619
|
+
signal?.throwIfAborted();
|
|
3620
|
+
let names;
|
|
3621
|
+
try {
|
|
3622
|
+
names = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".ndjson")).map((entry) => entry.name).toSorted();
|
|
3623
|
+
} catch {
|
|
3624
|
+
names = [];
|
|
3625
|
+
}
|
|
3626
|
+
const ranked = order === "name" ? names : [...names.filter((name) => !name.startsWith(NATIVE_CRASH_LOG_PREFIX)), ...names.filter((name) => name.startsWith(NATIVE_CRASH_LOG_PREFIX))];
|
|
3627
|
+
const generations = [];
|
|
3628
|
+
const sizes = {};
|
|
3629
|
+
const close = async () => {
|
|
3630
|
+
await Promise.all(generations.map((generation) => generation.handle.close().catch(() => {})));
|
|
3631
|
+
};
|
|
3632
|
+
try {
|
|
3633
|
+
for (const [rank, name] of ranked.entries()) for (const [generation, path] of [rotatedLogPath(join(dir, name)), join(dir, name)].entries()) {
|
|
3634
|
+
let handle;
|
|
3635
|
+
try {
|
|
3636
|
+
handle = await open(path, "r");
|
|
3637
|
+
} catch {
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
try {
|
|
3641
|
+
const stat = await handle.stat({ bigint: true });
|
|
3642
|
+
const index = await indexGeneration(handle, `${stat.dev}:${stat.ino}`, Number(stat.size), signal);
|
|
3643
|
+
generations.push({
|
|
3644
|
+
handle,
|
|
3645
|
+
rank,
|
|
3646
|
+
generation,
|
|
3647
|
+
crash: name.startsWith(NATIVE_CRASH_LOG_PREFIX),
|
|
3648
|
+
index
|
|
3649
|
+
});
|
|
3650
|
+
if (generation === 1) sizes[name] = index.size;
|
|
3651
|
+
} catch (error) {
|
|
3652
|
+
await handle.close().catch(() => {});
|
|
3653
|
+
throw error;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
await close();
|
|
3658
|
+
throw error;
|
|
3659
|
+
}
|
|
3660
|
+
let firstTs;
|
|
3661
|
+
for (const { index } of generations) if (index.minTs !== null && (firstTs === void 0 || index.minTs < firstTs)) firstTs = index.minTs;
|
|
3662
|
+
return {
|
|
3663
|
+
sizes,
|
|
3664
|
+
firstTs,
|
|
3665
|
+
launchAttempts: () => generations.filter((generation) => !generation.crash).flatMap(({ index, rank, generation }) => index.launchAttempts.map(({ record, position }) => ({
|
|
3666
|
+
ts: tsKey(record),
|
|
3667
|
+
rank,
|
|
3668
|
+
generation,
|
|
3669
|
+
position,
|
|
3670
|
+
record
|
|
3671
|
+
}))).toSorted(compare).map((entry) => entry.record),
|
|
3672
|
+
query: (query = {}) => queryGenerations(generations, query, signal),
|
|
3673
|
+
batches: (query = {}) => queryBatches(generations, query, signal),
|
|
3674
|
+
filter: (query = {}) => planLogQuery(generationMarkers(generations), query).matches,
|
|
3675
|
+
close
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
function blockMatches(block, { errorsOnly, minLevel, sources }) {
|
|
3679
|
+
if (errorsOnly && !block.levels.has("error") && !block.levels.has("fatal")) return false;
|
|
3680
|
+
if (minLevel && ![...block.levels].some((level) => levelRank(level) >= levelRank(minLevel))) return false;
|
|
3681
|
+
return !sources?.length || sources.some((source) => block.sources.has(source)) || Boolean(errorsOnly && block.sources.has("device"));
|
|
3682
|
+
}
|
|
3683
|
+
async function select(generations, extra, { matches, excludesUpTo, mayContain, tail, first = false }, signal) {
|
|
3684
|
+
if (tail !== void 0 && tail <= 0) return [];
|
|
3685
|
+
const heap = new BoundedSelection(compare, tail, first);
|
|
3686
|
+
const extraRank = generations.length ? Math.max(...generations.map((generation) => generation.rank)) + 1 : 0;
|
|
3687
|
+
for (const [position, record] of extra.entries()) {
|
|
3688
|
+
const entry = {
|
|
3689
|
+
ts: tsKey(record),
|
|
3690
|
+
rank: extraRank,
|
|
3691
|
+
generation: 0,
|
|
3692
|
+
position,
|
|
3693
|
+
record
|
|
3694
|
+
};
|
|
3695
|
+
if (matches(record, entry)) heap.keep(entry);
|
|
3696
|
+
}
|
|
3697
|
+
const blocks = generations.flatMap((generation) => generation.index.blocks.map((block) => ({
|
|
3698
|
+
generation,
|
|
3699
|
+
block
|
|
3700
|
+
}))).toSorted((a, b) => (first ? a.block.minTs - b.block.minTs : b.block.maxTs - a.block.maxTs) || 0);
|
|
3701
|
+
for (const { generation, block } of blocks) {
|
|
3702
|
+
if (excludesUpTo(block.maxTs)) {
|
|
3703
|
+
if (first) continue;
|
|
3704
|
+
break;
|
|
3705
|
+
}
|
|
3706
|
+
if (first && tail !== void 0 && heap.size >= tail && block.minTs > heap.peek().ts) break;
|
|
3707
|
+
if (!first && tail !== void 0 && heap.size >= tail && block.maxTs < heap.peek().ts) break;
|
|
3708
|
+
signal?.throwIfAborted();
|
|
3709
|
+
if (!mayContain(block, generation)) continue;
|
|
3710
|
+
const buffer = await read(generation.handle, block.start, block.end - block.start);
|
|
3711
|
+
for (let lineStart = 0; lineStart < buffer.length;) {
|
|
3712
|
+
const lineEnd = buffer.indexOf(NEWLINE, lineStart);
|
|
3713
|
+
if (lineEnd === -1) break;
|
|
3714
|
+
const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
3715
|
+
const position = block.start + lineStart;
|
|
3716
|
+
lineStart = lineEnd + 1;
|
|
3717
|
+
if (!record) continue;
|
|
3718
|
+
const entry = {
|
|
3719
|
+
ts: tsKey(record),
|
|
3720
|
+
rank: generation.rank,
|
|
3721
|
+
generation: generation.generation,
|
|
3722
|
+
position,
|
|
3723
|
+
record
|
|
3724
|
+
};
|
|
3725
|
+
if (matches(record, entry)) heap.keep(entry);
|
|
3726
|
+
}
|
|
3727
|
+
await setImmediate$1();
|
|
3728
|
+
}
|
|
3729
|
+
return heap.sorted();
|
|
3730
|
+
}
|
|
3731
|
+
function generationMarkers(generations) {
|
|
3732
|
+
const markers = /* @__PURE__ */ new Map();
|
|
3733
|
+
for (const { index } of generations) for (const [key, marker] of index.markers) keepNewer(markers, key, marker);
|
|
3734
|
+
return [...markers.values()];
|
|
3735
|
+
}
|
|
3736
|
+
async function selectQuery(generations, { tail, extra = [], ...options }, signal, page) {
|
|
3737
|
+
signal?.throwIfAborted();
|
|
3738
|
+
const slots = /* @__PURE__ */ new Set(["default"]);
|
|
3739
|
+
for (const { index } of generations) for (const slot of index.slots) slots.add(slot);
|
|
3740
|
+
for (const record of extra) slots.add(record.slot ?? "default");
|
|
3741
|
+
const plan = planLogQuery([...generationMarkers(generations), ...extra.filter((record) => record.marker === true)], options);
|
|
3742
|
+
return select(generations, extra, {
|
|
3743
|
+
matches: (record, entry) => plan.matches(record) && (!page?.after || compare(entry, page.after) > 0),
|
|
3744
|
+
excludesUpTo: (maxTs) => plan.excludesUpTo(maxTs, slots),
|
|
3745
|
+
mayContain: (block, generation) => blockMatches(block, options) && (!page?.after || compare({
|
|
3746
|
+
ts: block.maxTs,
|
|
3747
|
+
rank: generation.rank,
|
|
3748
|
+
generation: generation.generation,
|
|
3749
|
+
position: block.end
|
|
3750
|
+
}, page.after) > 0),
|
|
3751
|
+
tail: page?.size ?? tail,
|
|
3752
|
+
first: page !== void 0
|
|
3753
|
+
}, signal);
|
|
3754
|
+
}
|
|
3755
|
+
async function queryGenerations(generations, query, signal) {
|
|
3756
|
+
return withErrorContext(generations, query, await selectQuery(generations, query, signal), signal);
|
|
3757
|
+
}
|
|
3758
|
+
async function* queryBatches(generations, query, signal) {
|
|
3759
|
+
const fixed = {
|
|
3760
|
+
...query,
|
|
3761
|
+
now: query.now ?? Date.now()
|
|
3762
|
+
};
|
|
3763
|
+
let after;
|
|
3764
|
+
for (;;) {
|
|
3765
|
+
const selected = await selectQuery(generations, fixed, signal, {
|
|
3766
|
+
after,
|
|
3767
|
+
size: 500
|
|
3768
|
+
});
|
|
3769
|
+
if (!selected.length) return;
|
|
3770
|
+
yield await withErrorContext(generations, fixed, selected, signal);
|
|
3771
|
+
after = selected.at(-1);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
async function withErrorContext(generations, { extra = [], ...options }, selected, signal) {
|
|
3775
|
+
const matched = selected.map((entry) => entry.record);
|
|
3776
|
+
if (!options.errorsOnly) return matched;
|
|
3777
|
+
const errors = matched.filter((record) => record.src === "metro" && record.raw === true && (record.level === "error" || record.level === "fatal") && typeof record.msg === "string");
|
|
3778
|
+
if (!errors.length) return matched;
|
|
3779
|
+
const contextual = /* @__PURE__ */ new Map();
|
|
3780
|
+
for (const error of errors) {
|
|
3781
|
+
const [first] = await select(generations, extra, {
|
|
3782
|
+
matches: (record) => record.ts === error.ts && record.src === error.src && record.msg === error.msg && record.event === error.event,
|
|
3783
|
+
excludesUpTo: (maxTs) => maxTs < tsKey(error),
|
|
3784
|
+
mayContain: (block) => block.sources.has("metro"),
|
|
3785
|
+
tail: 1,
|
|
3786
|
+
first: true
|
|
3787
|
+
}, signal);
|
|
3788
|
+
if (!first) continue;
|
|
3789
|
+
let limit = 32;
|
|
3790
|
+
for (;;) {
|
|
3791
|
+
const stream = (await select(generations, extra, {
|
|
3792
|
+
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),
|
|
3793
|
+
excludesUpTo: (maxTs) => maxTs < tsKey(error),
|
|
3794
|
+
mayContain: (block) => block.sources.has("metro"),
|
|
3795
|
+
tail: limit,
|
|
3796
|
+
first: true
|
|
3797
|
+
}, signal)).map((entry) => entry.record);
|
|
3798
|
+
const index = stream.findIndex((record) => record.ts === error.ts && record.msg === error.msg);
|
|
3799
|
+
if (index < 0 || stream.slice(index + 1).some((record) => record.raw === true && !isExpoErrorContext(record, error.event)) || stream.length < limit) {
|
|
3800
|
+
contextual.set(error, attachExpoErrorContext(stream, [error], "field")[0]);
|
|
3801
|
+
break;
|
|
3802
|
+
}
|
|
3803
|
+
limit *= 2;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
return matched.map((record) => contextual.get(record) ?? record);
|
|
3807
|
+
}
|
|
3808
|
+
//#endregion
|
|
2915
3809
|
//#region state/recordings.ts
|
|
2916
3810
|
/** The footage stim-server keeps per device: the last 15 minutes recorded, however long ago. */
|
|
2917
3811
|
const RECORDING_FOOTAGE_MS = 9e5;
|
|
@@ -2944,44 +3838,105 @@ function listDir(dir) {
|
|
|
2944
3838
|
return [];
|
|
2945
3839
|
}
|
|
2946
3840
|
}
|
|
3841
|
+
async function listDirAsync(dir, signal) {
|
|
3842
|
+
signal?.throwIfAborted();
|
|
3843
|
+
try {
|
|
3844
|
+
const names = await readdir(dir);
|
|
3845
|
+
signal?.throwIfAborted();
|
|
3846
|
+
return names;
|
|
3847
|
+
} catch {
|
|
3848
|
+
signal?.throwIfAborted();
|
|
3849
|
+
return [];
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
function segmentName(name, closedOnly) {
|
|
3853
|
+
const closed = SEGMENT.exec(name);
|
|
3854
|
+
const part = closed ? null : PART.exec(name);
|
|
3855
|
+
if (!closed && (closedOnly || !part)) return null;
|
|
3856
|
+
const start = Number((closed ?? part)[1]);
|
|
3857
|
+
return {
|
|
3858
|
+
start,
|
|
3859
|
+
end: closed ? Math.max(start, Number(closed[2])) : null
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
function segmentRecord(file, range, metadata) {
|
|
3863
|
+
return {
|
|
3864
|
+
file,
|
|
3865
|
+
start: range.start,
|
|
3866
|
+
end: range.end ?? Math.max(range.start, Math.floor(metadata.mtimeMs)),
|
|
3867
|
+
bytes: metadata.size,
|
|
3868
|
+
open: range.end === null
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
function deviceName(name) {
|
|
3872
|
+
const match = DEVICE.exec(name);
|
|
3873
|
+
return match ? {
|
|
3874
|
+
platform: match[1],
|
|
3875
|
+
slot: match[2]
|
|
3876
|
+
} : null;
|
|
3877
|
+
}
|
|
2947
3878
|
/** The segments in one device directory, oldest first. Files that are not segments are left out; `closedOnly` excludes files still being written. */
|
|
2948
3879
|
function listSegments(dir, closedOnly = false) {
|
|
2949
3880
|
const segments = [];
|
|
2950
3881
|
for (const name of listDir(dir)) {
|
|
2951
|
-
const
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3882
|
+
const range = segmentName(name, closedOnly);
|
|
3883
|
+
if (!range) continue;
|
|
3884
|
+
const file = join(dir, name);
|
|
3885
|
+
try {
|
|
3886
|
+
segments.push(segmentRecord(file, range, statSync(file)));
|
|
3887
|
+
} catch {}
|
|
3888
|
+
}
|
|
3889
|
+
return segments.toSorted((a, b) => a.start - b.start);
|
|
3890
|
+
}
|
|
3891
|
+
/** Reads current segment files asynchronously, oldest first; cancellation propagates. */
|
|
3892
|
+
async function listSegmentsAsync(dir, closedOnly = false, signal) {
|
|
3893
|
+
const segments = [];
|
|
3894
|
+
for (const name of await listDirAsync(dir, signal)) {
|
|
3895
|
+
signal?.throwIfAborted();
|
|
3896
|
+
const range = segmentName(name, closedOnly);
|
|
3897
|
+
if (!range) continue;
|
|
3898
|
+
const file = join(dir, name);
|
|
2955
3899
|
try {
|
|
2956
|
-
|
|
3900
|
+
const metadata = await stat(file);
|
|
3901
|
+
signal?.throwIfAborted();
|
|
3902
|
+
segments.push(segmentRecord(file, range, metadata));
|
|
2957
3903
|
} catch {
|
|
2958
|
-
|
|
3904
|
+
signal?.throwIfAborted();
|
|
2959
3905
|
}
|
|
2960
|
-
const start = Number((closed ?? part)[1]);
|
|
2961
|
-
segments.push({
|
|
2962
|
-
file: join(dir, name),
|
|
2963
|
-
start,
|
|
2964
|
-
end: closed ? Math.max(start, Number(closed[2])) : Math.max(start, Math.floor(stat.mtimeMs)),
|
|
2965
|
-
bytes: stat.size,
|
|
2966
|
-
open: part !== null
|
|
2967
|
-
});
|
|
2968
3906
|
}
|
|
3907
|
+
signal?.throwIfAborted();
|
|
2969
3908
|
return segments.toSorted((a, b) => a.start - b.start);
|
|
2970
3909
|
}
|
|
2971
3910
|
/** Every device slot recorded under a workspace's recordings directory. */
|
|
2972
3911
|
function listRecordedDevices(recordingsDir, closedOnly = false) {
|
|
2973
3912
|
return listDir(recordingsDir).flatMap((name) => {
|
|
2974
|
-
const
|
|
2975
|
-
if (!
|
|
3913
|
+
const device = deviceName(name);
|
|
3914
|
+
if (!device) return [];
|
|
2976
3915
|
const dir = join(recordingsDir, name);
|
|
2977
3916
|
return [{
|
|
2978
|
-
|
|
2979
|
-
slot: match[2],
|
|
3917
|
+
...device,
|
|
2980
3918
|
dir,
|
|
2981
3919
|
segments: listSegments(dir, closedOnly)
|
|
2982
3920
|
}];
|
|
2983
3921
|
});
|
|
2984
3922
|
}
|
|
3923
|
+
/** Reads current device slots and their segment files asynchronously; cancellation propagates. */
|
|
3924
|
+
async function listRecordedDevicesAsync(recordingsDir, closedOnly = false, signal) {
|
|
3925
|
+
const devices = [];
|
|
3926
|
+
for (const name of await listDirAsync(recordingsDir, signal)) {
|
|
3927
|
+
signal?.throwIfAborted();
|
|
3928
|
+
const device = deviceName(name);
|
|
3929
|
+
if (!device) continue;
|
|
3930
|
+
const dir = join(recordingsDir, name);
|
|
3931
|
+
devices.push({
|
|
3932
|
+
...device,
|
|
3933
|
+
dir,
|
|
3934
|
+
segments: await listSegmentsAsync(dir, closedOnly, signal)
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
signal?.throwIfAborted();
|
|
3938
|
+
return devices;
|
|
3939
|
+
}
|
|
2985
3940
|
function recordedBytes(devices) {
|
|
2986
3941
|
return devices.reduce((sum, device) => sum + device.segments.reduce((total, s) => total + s.bytes, 0), 0);
|
|
2987
3942
|
}
|
|
@@ -3042,10 +3997,13 @@ function validRunId(value) {
|
|
|
3042
3997
|
}
|
|
3043
3998
|
let current = null;
|
|
3044
3999
|
/**
|
|
3045
|
-
* The id of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
|
|
3046
|
-
* The CLI entry exports it back to `STIM_RUN_ID` so processes the run
|
|
4000
|
+
* The run id of the ambient operation context, else of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
|
|
4001
|
+
* (Stim Desktop passes one), else a random one. The CLI entry exports it back to `STIM_RUN_ID` so processes the run
|
|
4002
|
+
* starts inherit it; this function does not.
|
|
3047
4003
|
*/
|
|
3048
4004
|
function runId(env = process.env) {
|
|
4005
|
+
const context = ambientContext();
|
|
4006
|
+
if (context) return context.runId;
|
|
3049
4007
|
if (current === null) current = validRunId(env["STIM_RUN_ID"]) ?? randomBytes(6).toString("hex");
|
|
3050
4008
|
return current;
|
|
3051
4009
|
}
|
|
@@ -3348,12 +4306,16 @@ function readPullRequestCache(path) {
|
|
|
3348
4306
|
if (typeof entry.checkedAt !== "string") return null;
|
|
3349
4307
|
const pullRequest = pullRequestOf(entry.pullRequest);
|
|
3350
4308
|
if (pullRequest === void 0) return null;
|
|
4309
|
+
const { containsHead, endedAt } = entry;
|
|
3351
4310
|
return {
|
|
3352
4311
|
path,
|
|
3353
4312
|
branch: entry.branch,
|
|
3354
4313
|
head: entry.head,
|
|
3355
4314
|
checkedAt: entry.checkedAt,
|
|
3356
|
-
pullRequest
|
|
4315
|
+
pullRequest,
|
|
4316
|
+
...typeof containsHead === "boolean" ? { containsHead } : {},
|
|
4317
|
+
...endedAt === null || typeof endedAt === "number" && Number.isFinite(endedAt) ? { endedAt } : {},
|
|
4318
|
+
...entry.partial === true ? { partial: true } : {}
|
|
3357
4319
|
};
|
|
3358
4320
|
}
|
|
3359
4321
|
/** The cached size of the folder at `path`, or null before it was measured. */
|
|
@@ -3453,7 +4415,7 @@ function readAgentSessionsCache() {
|
|
|
3453
4415
|
//#region state/stim-build.ts
|
|
3454
4416
|
/**
|
|
3455
4417
|
* A digest of Stim's built code: the first 16 hex characters of the sha256 over the sorted `*.mjs` names and
|
|
3456
|
-
* bytes of a `stim` `dist` directory. A client and a remote
|
|
4418
|
+
* bytes of a `stim` `dist` directory. A client and a remote machine offload only when both compute the same one.
|
|
3457
4419
|
*/
|
|
3458
4420
|
function stimBuildDigest(dir) {
|
|
3459
4421
|
try {
|
|
@@ -3718,7 +4680,22 @@ function lastUseFrom(state, logMtimes) {
|
|
|
3718
4680
|
return candidates.length ? Math.max(...candidates) : NaN;
|
|
3719
4681
|
}
|
|
3720
4682
|
const MAINTENANCE_LOG_NAME = "maintenance.ndjson";
|
|
4683
|
+
/**
|
|
4684
|
+
* When the workspace was last used, in epoch milliseconds. A workspace that recorded no use falls back to the mtime
|
|
4685
|
+
* of its workspace.json, which is written once when the workspace is created, so it is a lower bound on the last
|
|
4686
|
+
* use. NaN when its state.json exists but does not parse, or when workspace.json cannot be read either.
|
|
4687
|
+
*/
|
|
3721
4688
|
function workspaceLastUsed(root) {
|
|
4689
|
+
const used = recordedLastUse(root);
|
|
4690
|
+
if (Number.isFinite(used)) return used;
|
|
4691
|
+
if (existsSync(workspaceStateFile(root)) && readWorkspaceState(root) === null) return NaN;
|
|
4692
|
+
try {
|
|
4693
|
+
return statSync(workspaceMetadataFile(root)).mtimeMs;
|
|
4694
|
+
} catch {
|
|
4695
|
+
return NaN;
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
function recordedLastUse(root) {
|
|
3722
4699
|
const logs = workspaceLogsDir(root);
|
|
3723
4700
|
let mtimes = [];
|
|
3724
4701
|
try {
|
|
@@ -3796,7 +4773,7 @@ function readViewedDevices(dir = deviceViewersDir()) {
|
|
|
3796
4773
|
//#endregion
|
|
3797
4774
|
//#region state/stats.ts
|
|
3798
4775
|
const STATS_VERSION = 1;
|
|
3799
|
-
/** Where a compiling build ran: here, on a remote
|
|
4776
|
+
/** Where a compiling build ran: here, on a remote machine, or here after offloading it failed. */
|
|
3800
4777
|
const PLACEMENT_DECISIONS = [
|
|
3801
4778
|
"here",
|
|
3802
4779
|
"offloaded",
|
|
@@ -3944,7 +4921,7 @@ function normalize(record) {
|
|
|
3944
4921
|
const source = record.projects;
|
|
3945
4922
|
if (source && typeof source === "object" && !Array.isArray(source)) for (const [key, scope] of Object.entries(source)) projects[key] = normalizeScope(scope);
|
|
3946
4923
|
const history = {};
|
|
3947
|
-
if (
|
|
4924
|
+
if (isJsonObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
|
|
3948
4925
|
const normalized = normalizeHistory(scope);
|
|
3949
4926
|
if (Object.keys(normalized).length) history[key] = normalized;
|
|
3950
4927
|
}
|
|
@@ -3952,8 +4929,8 @@ function normalize(record) {
|
|
|
3952
4929
|
const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
|
|
3953
4930
|
const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
|
|
3954
4931
|
const buildMachines = {};
|
|
3955
|
-
if (
|
|
3956
|
-
for (const [name, totals] of Object.entries(record.buildMachines)) if (
|
|
4932
|
+
if (isJsonObject(record.buildMachines)) {
|
|
4933
|
+
for (const [name, totals] of Object.entries(record.buildMachines)) if (isJsonObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
|
|
3957
4934
|
}
|
|
3958
4935
|
return {
|
|
3959
4936
|
version: 1,
|
|
@@ -3967,7 +4944,7 @@ function normalize(record) {
|
|
|
3967
4944
|
};
|
|
3968
4945
|
}
|
|
3969
4946
|
function normalizePlacement(value) {
|
|
3970
|
-
if (!
|
|
4947
|
+
if (!isJsonObject(value)) return [];
|
|
3971
4948
|
const { decision, platform, reason, project, machine } = value;
|
|
3972
4949
|
if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
|
|
3973
4950
|
if (typeof reason !== "string" || typeof project !== "string") return [];
|
|
@@ -3990,7 +4967,7 @@ function normalizePlacement(value) {
|
|
|
3990
4967
|
}];
|
|
3991
4968
|
}
|
|
3992
4969
|
function normalizeCapacityRefusal(value) {
|
|
3993
|
-
if (!
|
|
4970
|
+
if (!isJsonObject(value)) return [];
|
|
3994
4971
|
const { at, kind, platform, max, workspace } = value;
|
|
3995
4972
|
if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
|
|
3996
4973
|
if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
|
|
@@ -4005,7 +4982,7 @@ function normalizeCapacityRefusal(value) {
|
|
|
4005
4982
|
}];
|
|
4006
4983
|
}
|
|
4007
4984
|
function normalizeCapacityWait(value) {
|
|
4008
|
-
if (!
|
|
4985
|
+
if (!isJsonObject(value) || value.kind !== "device-wait") return [];
|
|
4009
4986
|
const refusal = normalizeCapacityRefusal({
|
|
4010
4987
|
...value,
|
|
4011
4988
|
kind: "device"
|
|
@@ -4029,20 +5006,17 @@ function normalizeMachineTotals(totals) {
|
|
|
4029
5006
|
...typeof totals.lastFallbackAt === "string" ? { lastFallbackAt: totals.lastFallbackAt } : {}
|
|
4030
5007
|
};
|
|
4031
5008
|
}
|
|
4032
|
-
function isObject(value) {
|
|
4033
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4034
|
-
}
|
|
4035
5009
|
function normalizeHistory(scope) {
|
|
4036
5010
|
const history = {};
|
|
4037
|
-
if (!
|
|
5011
|
+
if (!isJsonObject(scope)) return history;
|
|
4038
5012
|
for (const platform of PLATFORMS) {
|
|
4039
5013
|
const lists = scope[platform];
|
|
4040
|
-
if (!
|
|
5014
|
+
if (!isJsonObject(lists)) continue;
|
|
4041
5015
|
const normalized = {};
|
|
4042
5016
|
for (const outcome of OUTCOMES) {
|
|
4043
5017
|
const samples = lists[outcome];
|
|
4044
5018
|
if (!Array.isArray(samples)) continue;
|
|
4045
|
-
const kept = samples.filter(
|
|
5019
|
+
const kept = samples.filter(isJsonObject).flatMap((sample) => {
|
|
4046
5020
|
const durationMs = wholeMs(sample.durationMs);
|
|
4047
5021
|
if (durationMs <= 0) return [];
|
|
4048
5022
|
return [{
|
|
@@ -4060,7 +5034,7 @@ function normalizeHistory(scope) {
|
|
|
4060
5034
|
}
|
|
4061
5035
|
function wholePhases(phases) {
|
|
4062
5036
|
const out = {};
|
|
4063
|
-
if (!
|
|
5037
|
+
if (!isJsonObject(phases)) return out;
|
|
4064
5038
|
for (const [name, ms] of Object.entries(phases)) {
|
|
4065
5039
|
const value = Number(ms);
|
|
4066
5040
|
if (Number.isFinite(value) && value >= 0) out[name] = Math.round(value);
|
|
@@ -4187,6 +5161,8 @@ const DEVICE_HOST_LIMITS = {
|
|
|
4187
5161
|
logsMs: 15e3,
|
|
4188
5162
|
killGraceMs: 5e3
|
|
4189
5163
|
};
|
|
5164
|
+
/** How many parked sessions one reserve inspects, and possibly retires, before it creates a device. */
|
|
5165
|
+
const HOSTED_PARKED_CANDIDATES = 3;
|
|
4190
5166
|
/** How long a cancelled hosted worker run can take to settle: SIGTERM, SIGKILL after the grace, then it is abandoned. */
|
|
4191
5167
|
function hostedWorkerSettleMs(limits = DEVICE_HOST_LIMITS) {
|
|
4192
5168
|
return limits.killGraceMs * 2;
|
|
@@ -4570,6 +5546,10 @@ function parseMacosRecord(value) {
|
|
|
4570
5546
|
startedAt: b.startedAt,
|
|
4571
5547
|
...typeof b.finishedAt === "string" ? { finishedAt: b.finishedAt } : {},
|
|
4572
5548
|
...typeof b.durationMs === "number" ? { durationMs: b.durationMs } : {},
|
|
5549
|
+
...typeof b.fingerprint === "string" ? { fingerprint: b.fingerprint } : {},
|
|
5550
|
+
...typeof b.cacheKey === "string" ? { cacheKey: b.cacheKey } : {},
|
|
5551
|
+
...typeof b.cacheHit === "boolean" ? { cacheHit: b.cacheHit } : {},
|
|
5552
|
+
...b.cacheSource === "local" || b.cacheSource === "provider" || b.cacheSource === "peer" ? { cacheSource: b.cacheSource } : {},
|
|
4573
5553
|
...typeof b.error === "string" ? { error: b.error } : {},
|
|
4574
5554
|
...typeof b.buildMachine === "string" ? { buildMachine: b.buildMachine } : {},
|
|
4575
5555
|
...typeof b.builtOn === "string" ? { builtOn: b.builtOn } : {},
|
|
@@ -4845,7 +5825,14 @@ const KINDS = [
|
|
|
4845
5825
|
"would-stop-workspace",
|
|
4846
5826
|
"would-remove-worktree",
|
|
4847
5827
|
"would-remove-orphan",
|
|
4848
|
-
"would-unregister-cache"
|
|
5828
|
+
"would-unregister-cache",
|
|
5829
|
+
"would-delete-parked-device",
|
|
5830
|
+
"would-delete-devices",
|
|
5831
|
+
"would-clear-build-dirs",
|
|
5832
|
+
"would-delete-dependencies",
|
|
5833
|
+
"would-stop-gradle-daemons",
|
|
5834
|
+
"would-delete-gradle-version",
|
|
5835
|
+
"would-delete-temp-leftovers"
|
|
4849
5836
|
];
|
|
4850
5837
|
/** Rejects missing, corrupt or unsupported payloads without throwing. */
|
|
4851
5838
|
function parseMaintenanceState(value) {
|
|
@@ -4859,13 +5846,14 @@ function parseMaintenanceState(value) {
|
|
|
4859
5846
|
"metro-cache",
|
|
4860
5847
|
"compilation-cache",
|
|
4861
5848
|
"ccache",
|
|
5849
|
+
"owned-devices",
|
|
4862
5850
|
"other"
|
|
4863
5851
|
].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;
|
|
4864
5852
|
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 || [
|
|
4865
5853
|
"worktree",
|
|
4866
5854
|
"sweep",
|
|
4867
5855
|
"disk"
|
|
4868
|
-
].includes(String(action.check))))) return null;
|
|
5856
|
+
].includes(String(action.check))) && (action.level === void 0 || action.level === 2 || action.level === 3))) return null;
|
|
4869
5857
|
if (!Array.isArray(value.recent) || !value.recent.every((record) => isJsonObject(record) && record.src === "maintenance" && finite(record.ts) && typeof record.msg === "string" && [
|
|
4870
5858
|
"debug",
|
|
4871
5859
|
"info",
|
|
@@ -4881,7 +5869,12 @@ function parseMaintenanceState(value) {
|
|
|
4881
5869
|
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;
|
|
4882
5870
|
if (value.pressure !== null) {
|
|
4883
5871
|
const pressure = value.pressure;
|
|
4884
|
-
if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) ||
|
|
5872
|
+
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 && ![
|
|
5873
|
+
0,
|
|
5874
|
+
1,
|
|
5875
|
+
2,
|
|
5876
|
+
3
|
|
5877
|
+
].some((level) => level === pressure.diskLevel) || !isJsonObject(pressure.memory) || ![
|
|
4885
5878
|
null,
|
|
4886
5879
|
"normal",
|
|
4887
5880
|
"warning",
|
|
@@ -4899,8 +5892,8 @@ function readMaintenanceAttempt() {
|
|
|
4899
5892
|
}
|
|
4900
5893
|
//#endregion
|
|
4901
5894
|
//#region state/agent-device.ts
|
|
4902
|
-
function agentDeviceStateDirs(home = homedir()) {
|
|
4903
|
-
const override =
|
|
5895
|
+
function agentDeviceStateDirs(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
|
|
5896
|
+
const override = env.AGENT_DEVICE_STATE_DIR?.trim();
|
|
4904
5897
|
const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
|
|
4905
5898
|
const workspaces = join(configDir(), "workspaces");
|
|
4906
5899
|
try {
|
|
@@ -4913,6 +5906,20 @@ function agentDeviceStateDirs(home = homedir()) {
|
|
|
4913
5906
|
} catch {}
|
|
4914
5907
|
return [...dirs];
|
|
4915
5908
|
}
|
|
5909
|
+
async function agentDeviceStateDirsAsync(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
|
|
5910
|
+
const override = env.AGENT_DEVICE_STATE_DIR?.trim();
|
|
5911
|
+
const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
|
|
5912
|
+
const workspaces = join(configDir(), "workspaces");
|
|
5913
|
+
try {
|
|
5914
|
+
for (const name of await readdir(workspaces)) {
|
|
5915
|
+
const dir = join(workspaces, name, "agent-device");
|
|
5916
|
+
try {
|
|
5917
|
+
if ((await stat(dir)).isDirectory()) dirs.add(dir);
|
|
5918
|
+
} catch {}
|
|
5919
|
+
}
|
|
5920
|
+
} catch {}
|
|
5921
|
+
return [...dirs];
|
|
5922
|
+
}
|
|
4916
5923
|
//#endregion
|
|
4917
5924
|
//#region state/archive.ts
|
|
4918
5925
|
function readArchives() {
|
|
@@ -4925,7 +5932,29 @@ function readArchives() {
|
|
|
4925
5932
|
return names.flatMap((name) => {
|
|
4926
5933
|
const archive = readArchive(name);
|
|
4927
5934
|
return archive ? [archive] : [];
|
|
4928
|
-
}).toSorted(
|
|
5935
|
+
}).toSorted(archiveOrder);
|
|
5936
|
+
}
|
|
5937
|
+
function archiveOrder(a, b) {
|
|
5938
|
+
return Date.parse(b.removedAt) - Date.parse(a.removedAt) || b.id.localeCompare(a.id);
|
|
5939
|
+
}
|
|
5940
|
+
/** Reads current archive files asynchronously, outside the synchronous state-read cache; cancellation propagates. */
|
|
5941
|
+
async function readArchivesAsync(signal) {
|
|
5942
|
+
signal?.throwIfAborted();
|
|
5943
|
+
let names;
|
|
5944
|
+
try {
|
|
5945
|
+
names = await readdir(archiveRoot());
|
|
5946
|
+
} catch {
|
|
5947
|
+
signal?.throwIfAborted();
|
|
5948
|
+
return [];
|
|
5949
|
+
}
|
|
5950
|
+
const archives = [];
|
|
5951
|
+
for (const name of names) {
|
|
5952
|
+
signal?.throwIfAborted();
|
|
5953
|
+
const archive = await readArchiveAsync(name, signal);
|
|
5954
|
+
if (archive) archives.push(archive);
|
|
5955
|
+
}
|
|
5956
|
+
signal?.throwIfAborted();
|
|
5957
|
+
return archives.toSorted(archiveOrder);
|
|
4929
5958
|
}
|
|
4930
5959
|
function readArchive(id) {
|
|
4931
5960
|
if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
|
|
@@ -4935,8 +5964,33 @@ function readArchive(id) {
|
|
|
4935
5964
|
} catch {
|
|
4936
5965
|
return null;
|
|
4937
5966
|
}
|
|
4938
|
-
|
|
4939
|
-
|
|
5967
|
+
return archiveRecord(id, readJsonObject(join(archiveRoot(), id, "archive.json")));
|
|
5968
|
+
}
|
|
5969
|
+
/** Reads a current archive without the synchronous state-read cache; invalid or missing records return null. */
|
|
5970
|
+
async function readArchiveAsync(id, signal) {
|
|
5971
|
+
signal?.throwIfAborted();
|
|
5972
|
+
if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
|
|
5973
|
+
const dir = join(archiveRoot(), id);
|
|
5974
|
+
try {
|
|
5975
|
+
const directory = await lstat(dir);
|
|
5976
|
+
signal?.throwIfAborted();
|
|
5977
|
+
if (!directory.isDirectory()) return null;
|
|
5978
|
+
const file = await lstat(join(dir, "archive.json"));
|
|
5979
|
+
signal?.throwIfAborted();
|
|
5980
|
+
if (!file.isFile()) return null;
|
|
5981
|
+
const text = await readFile(join(dir, "archive.json"), {
|
|
5982
|
+
encoding: "utf8",
|
|
5983
|
+
signal
|
|
5984
|
+
});
|
|
5985
|
+
signal?.throwIfAborted();
|
|
5986
|
+
return archiveRecord(id, JSON.parse(text));
|
|
5987
|
+
} catch {
|
|
5988
|
+
signal?.throwIfAborted();
|
|
5989
|
+
return null;
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
function archiveRecord(id, record) {
|
|
5993
|
+
if (!isJsonObject(record) || record.id !== id || [
|
|
4940
5994
|
"projectRoot",
|
|
4941
5995
|
"project",
|
|
4942
5996
|
"workspace",
|
|
@@ -5024,14 +6078,13 @@ const knownKeys = (value, keys) => Object.keys(value).every((key) => keys.includ
|
|
|
5024
6078
|
function parseSetupJournal(value) {
|
|
5025
6079
|
if (!isJsonObject(value) || !knownKeys(value, [
|
|
5026
6080
|
"v",
|
|
5027
|
-
"client",
|
|
5028
6081
|
"expiresAt",
|
|
5029
6082
|
"capabilities",
|
|
5030
6083
|
"steps",
|
|
5031
6084
|
"granted",
|
|
5032
6085
|
"done",
|
|
5033
6086
|
"exit"
|
|
5034
|
-
]) || value.v !== 1 ||
|
|
6087
|
+
]) || 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, [
|
|
5035
6088
|
"id",
|
|
5036
6089
|
"state",
|
|
5037
6090
|
"title",
|
|
@@ -5057,4 +6110,4 @@ function isSetupJournalPrunable(journal, now) {
|
|
|
5057
6110
|
return Date.parse(journal.expiresAt) + 36e5 < now;
|
|
5058
6111
|
}
|
|
5059
6112
|
//#endregion
|
|
5060
|
-
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, 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,
|
|
6113
|
+
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, listRecordedDevicesAsync, listSegments, listSegmentsAsync, 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, readArchiveAsync, readArchiveState, readArchives, readArchivesAsync, 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 };
|