@stim-cli/core 1.20.2 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{core-BkgfKqJ8.mjs → core-DisO3hs1.mjs} +235 -59
- package/dist/{index-VDWBflH3.d.mts → index-_wkluvP_.d.mts} +324 -87
- package/dist/index.d.mts +69 -5
- package/dist/index.mjs +3 -3
- package/dist/keyed-delta-BBoOeR2J.mjs +289 -0
- package/dist/noise-DYIbymAJ.d.mts +100 -0
- package/dist/noise-session.d.mts +164 -0
- package/dist/noise-session.mjs +402 -0
- package/dist/noise.d.mts +2 -0
- package/dist/noise.mjs +393 -0
- package/dist/{ownership-claim-Xxxr9MPp.d.mts → ownership-claim-Bf8Lq6AY.d.mts} +2 -1
- package/dist/{ownership-claim-O31Y5iIb.mjs → ownership-claim-DHUs_Dir.mjs} +1 -1
- package/dist/ownership-claim.d.mts +2 -2
- package/dist/ownership-claim.mjs +2 -2
- package/dist/phone-protocol.d.mts +85 -16
- package/dist/phone-protocol.mjs +2 -2
- package/dist/{protocol-pH632kA3.d.mts → protocol-D8R_3dkO.d.mts} +307 -65
- package/dist/protocol.d.mts +2 -2
- package/dist/protocol.mjs +50 -10
- package/dist/receive-protocol.d.mts +1 -1
- package/dist/receive-protocol.mjs +19832 -14545
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +1122 -187
- 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, G as withStateReadCache, H as readJsonFile, K as withDirLock, L as ambientContext, R as ambientEnv, T as workspaceStateDir, U as readJsonObject, V as isJsonObject, W as readStateFile, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as withDirLockAsync, t as LOG_ROTATE_BYTES, u as configDir } from "./core-DisO3hs1.mjs";
|
|
4
|
+
import { readOrCreateKeyFile } from "./noise-session.mjs";
|
|
5
|
+
import { validHomeId } from "./workspace-link.mjs";
|
|
6
|
+
import { a as workspaceSummary, i as statusViews, n as applyKeyedDelta, o as withDerivedFacts, r as SUMMARY_ARCHIVES, s as withGitChip, t as KeyedEntities } from "./keyed-delta-BBoOeR2J.mjs";
|
|
7
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
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 { 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",
|
|
@@ -1647,6 +1938,18 @@ const SETTINGS = [
|
|
|
1647
1938
|
env: "STIM_MAINTENANCE_OLDER_THAN_DAYS",
|
|
1648
1939
|
description: "Age in days for the age sweep: unused workspace build outputs and shared cache entries"
|
|
1649
1940
|
},
|
|
1941
|
+
{
|
|
1942
|
+
key: "maintenance.deviceIdleDays",
|
|
1943
|
+
type: {
|
|
1944
|
+
kind: "number",
|
|
1945
|
+
integer: true,
|
|
1946
|
+
minimum: 0
|
|
1947
|
+
},
|
|
1948
|
+
scopes: MACHINE,
|
|
1949
|
+
default: 3,
|
|
1950
|
+
env: "STIM_MAINTENANCE_DEVICE_IDLE_DAYS",
|
|
1951
|
+
description: "Days a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off"
|
|
1952
|
+
},
|
|
1650
1953
|
{
|
|
1651
1954
|
key: "maintenance.protectRecentHours",
|
|
1652
1955
|
type: {
|
|
@@ -1659,20 +1962,60 @@ const SETTINGS = [
|
|
|
1659
1962
|
env: "STIM_MAINTENANCE_PROTECT_RECENT_HOURS",
|
|
1660
1963
|
description: "Cache entries used within this many hours are never evicted"
|
|
1661
1964
|
},
|
|
1965
|
+
{
|
|
1966
|
+
key: "maintenance.urgentFreeDiskGb",
|
|
1967
|
+
type: GIGABYTES,
|
|
1968
|
+
scopes: MACHINE,
|
|
1969
|
+
default: 12,
|
|
1970
|
+
env: "STIM_MAINTENANCE_URGENT_FREE_DISK_GB",
|
|
1971
|
+
description: "Free disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off"
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
key: "maintenance.criticalFreeDiskGb",
|
|
1975
|
+
type: GIGABYTES,
|
|
1976
|
+
scopes: MACHINE,
|
|
1977
|
+
default: 6,
|
|
1978
|
+
env: "STIM_MAINTENANCE_CRITICAL_FREE_DISK_GB",
|
|
1979
|
+
description: "Free disk, in GB, below which automatic maintenance enters disk pressure level 3, also at or below budget.hardFloorDiskGb; 0 leaves only the hard floor"
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
key: "maintenance.pressureProtectMinutes",
|
|
1983
|
+
type: {
|
|
1984
|
+
kind: "number",
|
|
1985
|
+
integer: true,
|
|
1986
|
+
minimum: 0
|
|
1987
|
+
},
|
|
1988
|
+
scopes: MACHINE,
|
|
1989
|
+
default: 10,
|
|
1990
|
+
env: "STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTES",
|
|
1991
|
+
description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3"
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
key: "maintenance.gradleVersionIdleDays",
|
|
1995
|
+
type: {
|
|
1996
|
+
kind: "number",
|
|
1997
|
+
integer: true,
|
|
1998
|
+
minimum: 0
|
|
1999
|
+
},
|
|
2000
|
+
scopes: MACHINE,
|
|
2001
|
+
default: 7,
|
|
2002
|
+
env: "STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYS",
|
|
2003
|
+
description: "At disk pressure level 3, stop idle Gradle and Kotlin daemons, then delete the Gradle version caches and wrapper distributions no registered project pins once their directories are unmodified this many days; 0 turns this off"
|
|
2004
|
+
},
|
|
1662
2005
|
{
|
|
1663
2006
|
key: "maintenance.removeFinishedWorktrees",
|
|
1664
2007
|
type: BOOLEAN,
|
|
1665
2008
|
scopes: MACHINE,
|
|
1666
2009
|
default: true,
|
|
1667
2010
|
env: "STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREES",
|
|
1668
|
-
description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes"
|
|
2011
|
+
description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes, and at disk pressure level 3 clean, pushed ones with no commits of their own"
|
|
1669
2012
|
},
|
|
1670
2013
|
{
|
|
1671
2014
|
key: "maintenance.keep",
|
|
1672
2015
|
type: BOOLEAN,
|
|
1673
2016
|
scopes: PROJECT,
|
|
1674
2017
|
default: false,
|
|
1675
|
-
description: "Pin this workspace: automatic maintenance never clears its build outputs or removes its worktree"
|
|
2018
|
+
description: "Pin this workspace: automatic maintenance never clears its build outputs, deletes its owned devices or removes its worktree"
|
|
1676
2019
|
},
|
|
1677
2020
|
{
|
|
1678
2021
|
key: "maintenance.workspaceOutputsMaxGb",
|
|
@@ -1761,7 +2104,7 @@ const SETTINGS = [
|
|
|
1761
2104
|
scopes: MACHINE,
|
|
1762
2105
|
default: false,
|
|
1763
2106
|
env: "STIM_DEBUG",
|
|
1764
|
-
description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other
|
|
2107
|
+
description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other machines, and stim-server records every request with its timings, under STIM_HOME/logs/debug/ with rotation. Nothing is sent anywhere, and records never carry arguments, tokens or tickets"
|
|
1765
2108
|
},
|
|
1766
2109
|
{
|
|
1767
2110
|
key: "hosting.agentDriver",
|
|
@@ -1773,18 +2116,32 @@ const SETTINGS = [
|
|
|
1773
2116
|
default: "none",
|
|
1774
2117
|
description: "Tool this Mac starts so a client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client; none starts nothing"
|
|
1775
2118
|
},
|
|
2119
|
+
{
|
|
2120
|
+
key: "server.buildsPaused",
|
|
2121
|
+
type: { kind: "boolean" },
|
|
2122
|
+
scopes: MACHINE,
|
|
2123
|
+
default: false,
|
|
2124
|
+
description: "Temporarily refuse new incoming builds without interrupting running builds, serving cached artifacts, or changing client approvals; local CLI builds remain available"
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
key: "server.devicesPaused",
|
|
2128
|
+
type: { kind: "boolean" },
|
|
2129
|
+
scopes: MACHINE,
|
|
2130
|
+
default: false,
|
|
2131
|
+
description: "Temporarily refuse new hosted iOS, Android and macOS sessions without interrupting existing sessions or changing client approvals; local CLI devices remain available"
|
|
2132
|
+
},
|
|
1776
2133
|
{
|
|
1777
2134
|
key: "server.acceptClientBuilds",
|
|
1778
2135
|
type: { kind: "boolean" },
|
|
1779
2136
|
scopes: MACHINE,
|
|
1780
2137
|
default: false,
|
|
1781
|
-
description: "Whether a
|
|
2138
|
+
description: "Whether a machine approved for builds or device hosting here may update this machine's stim-server service to that machine's own stim-server build; releases from npm need no setting"
|
|
1782
2139
|
},
|
|
1783
2140
|
{
|
|
1784
2141
|
key: "remote.machines",
|
|
1785
2142
|
type: { kind: "strings" },
|
|
1786
2143
|
scopes: MACHINE,
|
|
1787
|
-
description: "Tailscale names of the remote
|
|
2144
|
+
description: "Tailscale names of the remote machines this machine may build on and host owned simulators on, each optionally with :<port> of its tailscale serve route (default 7443); what each is used for follows the build and device-host approvals it grants, which doctor --fix asks for separately"
|
|
1788
2145
|
},
|
|
1789
2146
|
{
|
|
1790
2147
|
key: "remote.buildPoolDisabled",
|
|
@@ -1805,7 +2162,7 @@ const SETTINGS = [
|
|
|
1805
2162
|
type: BOOLEAN,
|
|
1806
2163
|
scopes: EVERY,
|
|
1807
2164
|
default: false,
|
|
1808
|
-
description: "When ios.remote or android.remote is auto, this
|
|
2165
|
+
description: "When ios.remote or android.remote is auto, this machine is at its concurrency.maxDevices cap and no remote.machines entry takes the run, run the simulator or emulator on a billed EAS Simulator instead of waiting or refusing, when eas-cli can"
|
|
1809
2166
|
},
|
|
1810
2167
|
{
|
|
1811
2168
|
key: "remote.build",
|
|
@@ -1817,7 +2174,7 @@ const SETTINGS = [
|
|
|
1817
2174
|
scopes: MACHINE,
|
|
1818
2175
|
default: "auto",
|
|
1819
2176
|
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
|
|
2177
|
+
description: "Build placement: auto follows remote.buildMode within enabled automatic pool members; local always builds here; a name in remote.machines requires that remote machine without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override"
|
|
1821
2178
|
},
|
|
1822
2179
|
{
|
|
1823
2180
|
key: "remote.buildMode",
|
|
@@ -1828,7 +2185,7 @@ const SETTINGS = [
|
|
|
1828
2185
|
scopes: MACHINE,
|
|
1829
2186
|
default: "auto",
|
|
1830
2187
|
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
|
|
2188
|
+
description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this machine has a free concurrency.maxBuilds slot and its load is under server.maxLoadPerCore, and otherwise builds on a less loaded machine in remote.machines; force builds on an enabled remote machine whenever one can take the build; off builds here when local is enabled in remote.buildPoolDisabled"
|
|
1832
2189
|
},
|
|
1833
2190
|
{
|
|
1834
2191
|
key: "server.maxLoadPerCore",
|
|
@@ -1838,13 +2195,13 @@ const SETTINGS = [
|
|
|
1838
2195
|
},
|
|
1839
2196
|
scopes: MACHINE,
|
|
1840
2197
|
default: 2,
|
|
1841
|
-
description: "Load per core (5-minute load average divided by the CPU count) at which this
|
|
2198
|
+
description: "Load per core (5-minute load average divided by the CPU count) at which this machine counts as saturated: it declines builds other machines send it, automatic build placement stops preferring it, and automatic iOS and Android device placement treats it as busy"
|
|
1842
2199
|
},
|
|
1843
2200
|
{
|
|
1844
2201
|
key: "server.workerRoot",
|
|
1845
2202
|
type: ABSOLUTE_PATH,
|
|
1846
2203
|
scopes: MACHINE,
|
|
1847
|
-
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the
|
|
2204
|
+
description: "Directory where stim-server keeps the checkouts, dependencies and build state of the machines that build here; default $STIM_HOME/build-worker"
|
|
1848
2205
|
},
|
|
1849
2206
|
{
|
|
1850
2207
|
key: "server.gradleDaemonIdleMinutes",
|
|
@@ -1856,7 +2213,7 @@ const SETTINGS = [
|
|
|
1856
2213
|
},
|
|
1857
2214
|
scopes: MACHINE,
|
|
1858
2215
|
default: 30,
|
|
1859
|
-
description: "Minutes the Gradle daemon of an Android build another
|
|
2216
|
+
description: "Minutes the Gradle daemon of an Android build another machine sent here stays warm after the build; 0 stops it when the build ends"
|
|
1860
2217
|
},
|
|
1861
2218
|
{
|
|
1862
2219
|
key: "caches.buildCache",
|
|
@@ -1888,9 +2245,6 @@ function isLayeredSetting(setting) {
|
|
|
1888
2245
|
return setting.scopes.some((scope) => scope !== "machine");
|
|
1889
2246
|
}
|
|
1890
2247
|
const SETTING_GROUPS = [...new Set(SETTINGS.filter(isLayeredSetting).flatMap((setting) => setting.key.split(".").slice(0, -1).map((_, index, parts) => parts.slice(0, index + 1).join("."))))];
|
|
1891
|
-
function isPlainObject(value) {
|
|
1892
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1893
|
-
}
|
|
1894
2248
|
function bundleUrlAccepts(value, key) {
|
|
1895
2249
|
if (typeof value !== "string" || !/^(https?:\/\/|\/(?!\/))/.test(value) || /[\s\\#]/.test(value)) return false;
|
|
1896
2250
|
try {
|
|
@@ -1921,7 +2275,7 @@ function acceptsShape(setting, value) {
|
|
|
1921
2275
|
case "strings": return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1922
2276
|
case "number": return typeof value === "number";
|
|
1923
2277
|
case "choice": return typeof value === "string" && type.choices.includes(value);
|
|
1924
|
-
case "object": return
|
|
2278
|
+
case "object": return isJsonObject(value);
|
|
1925
2279
|
case "bundle-url": return bundleUrlAccepts(value, setting.key);
|
|
1926
2280
|
}
|
|
1927
2281
|
}
|
|
@@ -2026,7 +2380,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
|
|
|
2026
2380
|
if (refusal && !busy) throw refusal;
|
|
2027
2381
|
return null;
|
|
2028
2382
|
}
|
|
2029
|
-
/** Build slots held for a remote
|
|
2383
|
+
/** Build slots held for a remote machine's offloaded builds, which run under another Stim home. */
|
|
2030
2384
|
function liveOffloadedSlots() {
|
|
2031
2385
|
let names;
|
|
2032
2386
|
try {
|
|
@@ -2087,7 +2441,7 @@ function machineCapacity() {
|
|
|
2087
2441
|
maxLoadPerCore: maxLoadPerCore()
|
|
2088
2442
|
};
|
|
2089
2443
|
}
|
|
2090
|
-
/** Why a
|
|
2444
|
+
/** Why a machine with this capacity should not take another native build; null when it can. */
|
|
2091
2445
|
function saturation(capacity) {
|
|
2092
2446
|
if (capacity.maxBuilds > 0 && capacity.builds >= capacity.maxBuilds) return `all ${capacity.maxBuilds} build slots busy`;
|
|
2093
2447
|
if (capacity.loadPerCore >= capacity.maxLoadPerCore) return `load at or above ${capacity.maxLoadPerCore}/core`;
|
|
@@ -2095,41 +2449,55 @@ function saturation(capacity) {
|
|
|
2095
2449
|
}
|
|
2096
2450
|
//#endregion
|
|
2097
2451
|
//#region state/build-machines.ts
|
|
2098
|
-
|
|
2452
|
+
const MACHINE_CREDENTIALS_VERSION = 2;
|
|
2453
|
+
const KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
2454
|
+
/** A stored credential, or null when it is malformed or from an older version. */
|
|
2455
|
+
function parseMachineCredential(value) {
|
|
2099
2456
|
if (!isJsonObject(value)) return null;
|
|
2100
|
-
const { machine,
|
|
2457
|
+
const { machine, transport, address, state, requestedAt, serverKey, deviceId, ticket, expiresAt } = value;
|
|
2101
2458
|
if (![
|
|
2102
2459
|
machine,
|
|
2103
|
-
|
|
2104
|
-
dnsName,
|
|
2105
|
-
deviceId,
|
|
2106
|
-
deviceToken,
|
|
2460
|
+
address,
|
|
2107
2461
|
requestedAt
|
|
2108
2462
|
].every((field) => typeof field === "string" && field)) return null;
|
|
2109
|
-
if (state !== "pending" && state !== "approved") return null;
|
|
2463
|
+
if (transport !== "tailscale" || state !== "pending" && state !== "approved") return null;
|
|
2464
|
+
if (serverKey !== void 0 && (typeof serverKey !== "string" || !KEY.test(serverKey))) return null;
|
|
2465
|
+
if (ticket !== void 0 && (typeof ticket !== "string" || !KEY.test(ticket))) return null;
|
|
2466
|
+
if (state === "approved" && (serverKey === void 0 || typeof deviceId !== "string" || !deviceId)) return null;
|
|
2467
|
+
if (state === "pending" && ticket === void 0) return null;
|
|
2110
2468
|
return {
|
|
2111
2469
|
machine,
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
deviceId,
|
|
2115
|
-
deviceToken,
|
|
2470
|
+
transport,
|
|
2471
|
+
address,
|
|
2116
2472
|
state,
|
|
2117
2473
|
requestedAt,
|
|
2118
|
-
...
|
|
2119
|
-
...typeof
|
|
2474
|
+
...serverKey ? { serverKey } : {},
|
|
2475
|
+
...typeof deviceId === "string" && deviceId ? { deviceId } : {},
|
|
2476
|
+
...ticket ? { ticket } : {},
|
|
2477
|
+
...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {}
|
|
2120
2478
|
};
|
|
2121
2479
|
}
|
|
2122
2480
|
function readBuildMachines() {
|
|
2123
2481
|
try {
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2482
|
+
const store = readJsonFile(buildMachinesFile());
|
|
2483
|
+
if (store.version !== 2 || !Array.isArray(store.machines)) return [];
|
|
2484
|
+
return store.machines.flatMap((entry) => parseMachineCredential(entry) ?? []);
|
|
2126
2485
|
} catch {
|
|
2127
2486
|
return [];
|
|
2128
2487
|
}
|
|
2129
2488
|
}
|
|
2130
2489
|
//#endregion
|
|
2490
|
+
//#region state/client-identity.ts
|
|
2491
|
+
/** This machine's long-term X25519 key as a client of other machines, in `$STIM_HOME/client-identity.key`. */
|
|
2492
|
+
function clientIdentity() {
|
|
2493
|
+
return readOrCreateKeyFile(clientIdentityKeyFile());
|
|
2494
|
+
}
|
|
2495
|
+
//#endregion
|
|
2131
2496
|
//#region state/device-host-machines.ts
|
|
2132
|
-
/**
|
|
2497
|
+
/**
|
|
2498
|
+
* An absent store, or one from protocol 1, is empty; an unreadable or malformed one refuses access without discarding
|
|
2499
|
+
* pinned keys.
|
|
2500
|
+
*/
|
|
2133
2501
|
function readDeviceHostMachines() {
|
|
2134
2502
|
let value;
|
|
2135
2503
|
try {
|
|
@@ -2138,23 +2506,13 @@ function readDeviceHostMachines() {
|
|
|
2138
2506
|
if (error.code === "ENOENT") return [];
|
|
2139
2507
|
throw error;
|
|
2140
2508
|
}
|
|
2141
|
-
if (
|
|
2509
|
+
if (isJsonObject(value) && value.version === 1) return [];
|
|
2510
|
+
if (!isJsonObject(value) || value.version !== 2 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
|
|
2142
2511
|
const machines = [];
|
|
2143
2512
|
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
|
-
});
|
|
2513
|
+
const credential = parseMachineCredential(entry);
|
|
2514
|
+
if (!credential || machines.some((each) => each.machine === credential.machine)) throw new Error("Invalid device-host machine credential store");
|
|
2515
|
+
machines.push(credential);
|
|
2158
2516
|
}
|
|
2159
2517
|
return machines;
|
|
2160
2518
|
}
|
|
@@ -2174,11 +2532,15 @@ function requireAutomaticMachine(role, machine) {
|
|
|
2174
2532
|
remedy: `Enable it in remote.${role}PoolDisabled or select a machine explicitly.`
|
|
2175
2533
|
});
|
|
2176
2534
|
}
|
|
2177
|
-
|
|
2535
|
+
/**
|
|
2536
|
+
* Refuses a pool with no enabled member: local, or a configured remote approved for the role. `canHost` excludes
|
|
2537
|
+
* device-pool remotes that can never host, such as a Linux machine.
|
|
2538
|
+
*/
|
|
2539
|
+
function validateAutomaticPool(role, config, canHost = () => true) {
|
|
2178
2540
|
if (automaticMachineEnabled(role, "local", config)) return;
|
|
2179
2541
|
const configured = config.remote?.machines;
|
|
2180
2542
|
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;
|
|
2543
|
+
if (Array.isArray(configured) && configured.some((machine) => typeof machine === "string" && automaticMachineEnabled(role, machine, config) && (role === "build" || canHost(machine)) && approved.some((entry) => entry.machine === machine && entry.state === "approved"))) return;
|
|
2182
2544
|
throw Object.assign(/* @__PURE__ */ new Error(`Keep at least one enabled automatic ${role} pool member: local or a configured, approved remote machine.`), { code: "STIM_BAD_ARG" });
|
|
2183
2545
|
}
|
|
2184
2546
|
//#endregion
|
|
@@ -2192,7 +2554,7 @@ function debugLogDir() {
|
|
|
2192
2554
|
return join(configDir(), "logs", "debug");
|
|
2193
2555
|
}
|
|
2194
2556
|
/** 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 =
|
|
2557
|
+
function debugLoggingEnabled(env = ambientEnv()) {
|
|
2196
2558
|
const definition = settingDefinition("debug.logs");
|
|
2197
2559
|
const raw = env[DEBUG_ENV];
|
|
2198
2560
|
if (raw !== void 0 && raw !== "") {
|
|
@@ -2219,15 +2581,21 @@ function scrub(value, depth = 0) {
|
|
|
2219
2581
|
* Debug-level records for one component (`cli` or `server`). Off by default; `STIM_DEBUG=1` or `debug.logs` turns it
|
|
2220
2582
|
* on. Callers pass names, durations and codes, never arguments, environment values, tokens or tickets.
|
|
2221
2583
|
*/
|
|
2222
|
-
function createDebugLog(component, { env
|
|
2584
|
+
function createDebugLog(component, { env, now = Date.now, base = () => ({}) } = {}) {
|
|
2223
2585
|
let checkedAt = Number.NEGATIVE_INFINITY;
|
|
2224
2586
|
let on = false;
|
|
2225
2587
|
let unchecked = ROTATE_CHECK_BYTES;
|
|
2588
|
+
let checkedEnv;
|
|
2589
|
+
let checkedHome;
|
|
2226
2590
|
const enabled = () => {
|
|
2227
2591
|
const at = now();
|
|
2228
|
-
|
|
2592
|
+
const activeEnv = env ?? ambientEnv();
|
|
2593
|
+
const home = configDir();
|
|
2594
|
+
if (activeEnv !== checkedEnv || home !== checkedHome || at - checkedAt >= ENABLED_RECHECK_MS) {
|
|
2229
2595
|
checkedAt = at;
|
|
2230
|
-
|
|
2596
|
+
checkedEnv = activeEnv;
|
|
2597
|
+
checkedHome = home;
|
|
2598
|
+
on = debugLoggingEnabled(activeEnv);
|
|
2231
2599
|
}
|
|
2232
2600
|
return on;
|
|
2233
2601
|
};
|
|
@@ -2258,6 +2626,15 @@ function createDebugLog(component, { env = process.env, now = Date.now, base = (
|
|
|
2258
2626
|
};
|
|
2259
2627
|
}
|
|
2260
2628
|
//#endregion
|
|
2629
|
+
//#region state/home-identity.ts
|
|
2630
|
+
/** The persistent, nonsecret identity of this Stim home; null until the CLI initializes it. */
|
|
2631
|
+
function readHomeIdentity(config = loadConfig()) {
|
|
2632
|
+
const id = config?.homeId;
|
|
2633
|
+
if (id === void 0) return null;
|
|
2634
|
+
if (!validHomeId(id)) throw Object.assign(/* @__PURE__ */ new Error(`Stim home identity is not a UUID. ${configCorruptRepair()}`), { code: "STIM_CONFIG_CORRUPT" });
|
|
2635
|
+
return id.toLowerCase();
|
|
2636
|
+
}
|
|
2637
|
+
//#endregion
|
|
2261
2638
|
//#region state/ledgers.ts
|
|
2262
2639
|
function ids(value) {
|
|
2263
2640
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry !== "") : [];
|
|
@@ -2692,7 +3069,8 @@ function expoContextFollower(onRecord, into) {
|
|
|
2692
3069
|
return {
|
|
2693
3070
|
push,
|
|
2694
3071
|
settle,
|
|
2695
|
-
flush
|
|
3072
|
+
flush,
|
|
3073
|
+
pending: () => pending !== null
|
|
2696
3074
|
};
|
|
2697
3075
|
}
|
|
2698
3076
|
function includeBareErrorContext(all, matched, launchTs, sinceTs) {
|
|
@@ -2743,11 +3121,11 @@ function launchMarkersBySlot(records) {
|
|
|
2743
3121
|
}
|
|
2744
3122
|
return markers;
|
|
2745
3123
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const slotLaunched = slot !== void 0 &&
|
|
2749
|
-
const
|
|
2750
|
-
|
|
3124
|
+
/** Decides which records a query matches from the timeline's marker records alone. */
|
|
3125
|
+
function planLogQuery(markers, { slot, sources, minLevel, since, grep, errorsOnly, now } = {}) {
|
|
3126
|
+
const slotLaunched = slot !== void 0 && markers.some((record) => isLaunchMarker(record) && (record.slot ?? "default") === slot);
|
|
3127
|
+
const inTimeline = (record) => slot === void 0 || (record.slot ?? "default") === slot || slotLaunched && isSharedRecord(record);
|
|
3128
|
+
const all = markers.filter((record) => record.marker === true && inTimeline(record));
|
|
2751
3129
|
const { launchTs, bundleTs, pageLoadTs } = errorsOnly ? markerWindow(all) : {
|
|
2752
3130
|
launchTs: null,
|
|
2753
3131
|
bundleTs: null,
|
|
@@ -2765,9 +3143,8 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
|
|
|
2765
3143
|
bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
|
|
2766
3144
|
});
|
|
2767
3145
|
const slotMarkers = launchMarkersBySlot(all);
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2770
|
-
const bundles = newestByPlatform(markers.filter((record) => record.src === "metro"), bundleMarkerPlatform);
|
|
3146
|
+
const launches = newestByPlatform(all.filter((record) => record.src !== "metro" && !isWebPageRecord(record)), (record) => typeof record.platform === "string" ? record.platform : null);
|
|
3147
|
+
const bundles = newestByPlatform(all.filter((record) => record.src === "metro"), bundleMarkerPlatform);
|
|
2771
3148
|
const slotPlatforms = slot === void 0 ? [] : [...launches.keys()].filter((platform) => platform !== null);
|
|
2772
3149
|
const windowStart = (record) => {
|
|
2773
3150
|
if (pageLoadTs !== null && isWebPageRecord(record)) return pageLoadTs - 1;
|
|
@@ -2783,13 +3160,58 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
|
|
|
2783
3160
|
const platform = nativePlatform(record);
|
|
2784
3161
|
return newestWhere(bundles, (marker) => marker === null || (platform ? marker === platform : slotPlatforms.length === 0 || slotPlatforms.includes(marker)));
|
|
2785
3162
|
};
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3163
|
+
const lowestWindowStart = (slots) => {
|
|
3164
|
+
if (!criteria.errorsOnly) return void 0;
|
|
3165
|
+
const starts = [];
|
|
3166
|
+
if (pageLoadTs !== null) starts.push(pageLoadTs - 1);
|
|
3167
|
+
const kinds = criteria.sources ?? [];
|
|
3168
|
+
if (criteria.includeAppDeviceErrors || kinds.some((src) => src !== "metro" && src !== "client")) {
|
|
3169
|
+
const possible = slot !== void 0 ? [slot] : slots ? [...slots] : null;
|
|
3170
|
+
if (!possible) return void 0;
|
|
3171
|
+
for (const value of possible) {
|
|
3172
|
+
const start = slotMarkers.get(value);
|
|
3173
|
+
if (start === void 0) return void 0;
|
|
3174
|
+
starts.push(start);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (kinds.includes("metro") || kinds.includes("client")) {
|
|
3178
|
+
if (criteria.markerTs === void 0) return void 0;
|
|
3179
|
+
starts.push(criteria.markerTs, ...launches.values());
|
|
3180
|
+
}
|
|
3181
|
+
return starts.length ? Math.min(...starts) : void 0;
|
|
3182
|
+
};
|
|
3183
|
+
return {
|
|
3184
|
+
inTimeline,
|
|
3185
|
+
matches: (record) => inTimeline(record) && !otherPlatform(record) && recordMatches(record, {
|
|
3186
|
+
...criteria,
|
|
3187
|
+
markerTs: windowStart(record),
|
|
3188
|
+
bundleMarkerTs: bundleWindowStart(record)
|
|
3189
|
+
}),
|
|
3190
|
+
launchTs,
|
|
3191
|
+
sinceTs: criteria.sinceTs,
|
|
3192
|
+
excludesUpTo: (maxTs, slots) => {
|
|
3193
|
+
if (criteria.sinceTs !== void 0 && maxTs < criteria.sinceTs) return true;
|
|
3194
|
+
const start = lowestWindowStart(slots);
|
|
3195
|
+
return start !== void 0 && maxTs <= start;
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
|
|
3200
|
+
const source = records ?? readLogRecords(dir);
|
|
3201
|
+
const plan = planLogQuery(source.filter((record) => record.marker === true), {
|
|
3202
|
+
slot,
|
|
3203
|
+
sources,
|
|
3204
|
+
minLevel,
|
|
3205
|
+
since,
|
|
3206
|
+
grep,
|
|
3207
|
+
errorsOnly,
|
|
3208
|
+
now
|
|
3209
|
+
});
|
|
3210
|
+
const all = source.filter(plan.inTimeline);
|
|
3211
|
+
if (all.length === 0) return [];
|
|
3212
|
+
let matched = all.filter(plan.matches);
|
|
2791
3213
|
if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
|
|
2792
|
-
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs,
|
|
3214
|
+
return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), plan.launchTs, plan.sinceTs) : matched;
|
|
2793
3215
|
}
|
|
2794
3216
|
/** Queries the JSON timeline, including Expo context fields on errors as `stim logs --json` does. */
|
|
2795
3217
|
function queryJsonLogs(options = {}) {
|
|
@@ -2834,10 +3256,12 @@ function advanceTail(prev, chunk, size) {
|
|
|
2834
3256
|
records
|
|
2835
3257
|
};
|
|
2836
3258
|
}
|
|
2837
|
-
function followLogs({ dir, onRecord, criteria = {}, intervalMs
|
|
3259
|
+
function followLogs({ dir, onRecord, criteria = {}, intervalMs, offsets = null, errorContext }) {
|
|
2838
3260
|
const follower = errorContext ? expoContextFollower(onRecord, errorContext) : null;
|
|
2839
3261
|
const state = /* @__PURE__ */ new Map();
|
|
2840
3262
|
const decoders = /* @__PURE__ */ new Map();
|
|
3263
|
+
let stopped = false;
|
|
3264
|
+
let contextTimer;
|
|
2841
3265
|
for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
|
|
2842
3266
|
state.set(name, {
|
|
2843
3267
|
offset: size,
|
|
@@ -2847,6 +3271,7 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
|
|
|
2847
3271
|
}
|
|
2848
3272
|
function emit(records) {
|
|
2849
3273
|
for (const record of records) {
|
|
3274
|
+
if (stopped) break;
|
|
2850
3275
|
const matches = recordMatches(record, criteria);
|
|
2851
3276
|
if (follower) follower.push(record, matches);
|
|
2852
3277
|
else if (matches) onRecord(record);
|
|
@@ -2876,14 +3301,105 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
|
|
|
2876
3301
|
emit(next.records);
|
|
2877
3302
|
}
|
|
2878
3303
|
function poll() {
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3304
|
+
if (stopped) return;
|
|
3305
|
+
for (const name of logFiles(dir)) {
|
|
3306
|
+
if (stopped) break;
|
|
3307
|
+
try {
|
|
3308
|
+
pollFile(name);
|
|
3309
|
+
} catch {}
|
|
3310
|
+
}
|
|
3311
|
+
if (intervalMs !== void 0) follower?.settle();
|
|
3312
|
+
else if (!contextTimer && follower?.pending()) {
|
|
3313
|
+
follower.settle();
|
|
3314
|
+
contextTimer = setTimeout(settleContext, 500);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
function settleContext() {
|
|
3318
|
+
if (stopped) return;
|
|
3319
|
+
poll();
|
|
3320
|
+
contextTimer = void 0;
|
|
3321
|
+
if (stopped) return;
|
|
2882
3322
|
follower?.settle();
|
|
3323
|
+
if (follower?.pending()) contextTimer = setTimeout(settleContext, 500);
|
|
3324
|
+
}
|
|
3325
|
+
let watcher;
|
|
3326
|
+
let directory;
|
|
3327
|
+
let scheduled;
|
|
3328
|
+
let repairTimer;
|
|
3329
|
+
function closeWatch() {
|
|
3330
|
+
const previous = watcher;
|
|
3331
|
+
watcher = void 0;
|
|
3332
|
+
directory = void 0;
|
|
3333
|
+
previous?.close();
|
|
3334
|
+
}
|
|
3335
|
+
function schedulePoll() {
|
|
3336
|
+
if (stopped || scheduled) return;
|
|
3337
|
+
scheduled = setImmediate(() => {
|
|
3338
|
+
scheduled = void 0;
|
|
3339
|
+
poll();
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
function sameDirectory() {
|
|
3343
|
+
if (!watcher || !directory) return false;
|
|
3344
|
+
try {
|
|
3345
|
+
const current = statSync(dir);
|
|
3346
|
+
return current.dev === directory.dev && current.ino === directory.ino;
|
|
3347
|
+
} catch {
|
|
3348
|
+
return false;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
function attachWatch() {
|
|
3352
|
+
if (stopped || watcher) return false;
|
|
3353
|
+
try {
|
|
3354
|
+
const current = statSync(dir);
|
|
3355
|
+
const next = watch(dir, { persistent: false }, (event) => {
|
|
3356
|
+
if (stopped) return;
|
|
3357
|
+
if (event === "rename" && !sameDirectory()) {
|
|
3358
|
+
closeWatch();
|
|
3359
|
+
armRepair(attachWatch() ? 500 : void 0);
|
|
3360
|
+
}
|
|
3361
|
+
schedulePoll();
|
|
3362
|
+
});
|
|
3363
|
+
watcher = next;
|
|
3364
|
+
directory = current;
|
|
3365
|
+
const lost = () => {
|
|
3366
|
+
if (watcher !== next) return;
|
|
3367
|
+
closeWatch();
|
|
3368
|
+
armRepair();
|
|
3369
|
+
};
|
|
3370
|
+
next.once("error", lost);
|
|
3371
|
+
next.once("close", lost);
|
|
3372
|
+
return true;
|
|
3373
|
+
} catch {}
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
function armRepair(delay = watcher ? 1e4 : 500) {
|
|
3377
|
+
clearTimeout(repairTimer);
|
|
3378
|
+
if (stopped) return;
|
|
3379
|
+
repairTimer = setTimeout(() => {
|
|
3380
|
+
let attached = false;
|
|
3381
|
+
if (!sameDirectory()) {
|
|
3382
|
+
closeWatch();
|
|
3383
|
+
attached = attachWatch();
|
|
3384
|
+
}
|
|
3385
|
+
poll();
|
|
3386
|
+
armRepair(attached ? 500 : void 0);
|
|
3387
|
+
}, delay);
|
|
3388
|
+
}
|
|
3389
|
+
const timer = intervalMs === void 0 ? void 0 : setInterval(poll, intervalMs);
|
|
3390
|
+
if (intervalMs === void 0) {
|
|
3391
|
+
attachWatch();
|
|
3392
|
+
poll();
|
|
3393
|
+
armRepair(500);
|
|
2883
3394
|
}
|
|
2884
|
-
const timer = setInterval(poll, intervalMs);
|
|
2885
3395
|
return function stop() {
|
|
3396
|
+
if (stopped) return;
|
|
3397
|
+
stopped = true;
|
|
2886
3398
|
clearInterval(timer);
|
|
3399
|
+
clearTimeout(repairTimer);
|
|
3400
|
+
clearTimeout(contextTimer);
|
|
3401
|
+
clearImmediate(scheduled);
|
|
3402
|
+
closeWatch();
|
|
2887
3403
|
follower?.flush();
|
|
2888
3404
|
};
|
|
2889
3405
|
}
|
|
@@ -2912,6 +3428,378 @@ function sortByTs(records) {
|
|
|
2912
3428
|
});
|
|
2913
3429
|
}
|
|
2914
3430
|
//#endregion
|
|
3431
|
+
//#region state/log-reader.ts
|
|
3432
|
+
const BLOCK_BYTES = 65536;
|
|
3433
|
+
const MAX_CACHED_GENERATIONS = 512;
|
|
3434
|
+
const NEWLINE = 10;
|
|
3435
|
+
const VALIDATE_BYTES = 256;
|
|
3436
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3437
|
+
const indexing = /* @__PURE__ */ new Map();
|
|
3438
|
+
function tsKey(record) {
|
|
3439
|
+
return typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : Infinity;
|
|
3440
|
+
}
|
|
3441
|
+
function compare(a, b) {
|
|
3442
|
+
return a.ts - b.ts || a.rank - b.rank || a.generation - b.generation || a.position - b.position;
|
|
3443
|
+
}
|
|
3444
|
+
function markerTs(record) {
|
|
3445
|
+
const ts = tsKey(record);
|
|
3446
|
+
return ts === Infinity ? -Infinity : ts;
|
|
3447
|
+
}
|
|
3448
|
+
function keepNewer(markers, key, marker) {
|
|
3449
|
+
const known = markers.get(key);
|
|
3450
|
+
if (!known || markerTs(marker) > markerTs(known)) markers.set(key, marker);
|
|
3451
|
+
}
|
|
3452
|
+
function markerKey(record) {
|
|
3453
|
+
const platform = record.src === "metro" ? bundleMarkerPlatform(record) : typeof record.platform === "string" ? record.platform : null;
|
|
3454
|
+
const ts = tsKey(record);
|
|
3455
|
+
return {
|
|
3456
|
+
key: JSON.stringify([
|
|
3457
|
+
record.src ?? null,
|
|
3458
|
+
record.slot === void 0 ? 0 : [record.slot],
|
|
3459
|
+
platform
|
|
3460
|
+
]),
|
|
3461
|
+
marker: {
|
|
3462
|
+
src: record.src,
|
|
3463
|
+
marker: true,
|
|
3464
|
+
...ts === Infinity ? {} : { ts },
|
|
3465
|
+
...record.slot === void 0 ? {} : { slot: record.slot },
|
|
3466
|
+
...platform === null ? {} : { platform }
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
function emptyIndex() {
|
|
3471
|
+
return {
|
|
3472
|
+
size: 0,
|
|
3473
|
+
head: Buffer.alloc(0),
|
|
3474
|
+
tail: Buffer.alloc(0),
|
|
3475
|
+
blocks: [],
|
|
3476
|
+
minTs: null,
|
|
3477
|
+
markers: /* @__PURE__ */ new Map(),
|
|
3478
|
+
launchAttempts: [],
|
|
3479
|
+
slots: /* @__PURE__ */ new Set()
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
function indexBlock(index, buffer, start) {
|
|
3483
|
+
let maxTs = -Infinity;
|
|
3484
|
+
let minTs = Infinity;
|
|
3485
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3486
|
+
const levels = /* @__PURE__ */ new Set();
|
|
3487
|
+
for (let lineStart = 0; lineStart < buffer.length;) {
|
|
3488
|
+
const lineEnd = buffer.indexOf(NEWLINE, lineStart);
|
|
3489
|
+
const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
3490
|
+
if (record) {
|
|
3491
|
+
const value = tsKey(record);
|
|
3492
|
+
maxTs = Math.max(maxTs, value);
|
|
3493
|
+
minTs = Math.min(minTs, value);
|
|
3494
|
+
if (value !== Infinity && (index.minTs === null || value < index.minTs)) index.minTs = value;
|
|
3495
|
+
sources.add(record.src);
|
|
3496
|
+
levels.add(record.level);
|
|
3497
|
+
index.slots.add(record.slot ?? "default");
|
|
3498
|
+
if (record.marker === true) {
|
|
3499
|
+
const { key, marker } = markerKey(record);
|
|
3500
|
+
keepNewer(index.markers, key, marker);
|
|
3501
|
+
}
|
|
3502
|
+
if (record.event === "launch_attempt") index.launchAttempts.push({
|
|
3503
|
+
record,
|
|
3504
|
+
position: start + lineStart
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
lineStart = lineEnd + 1;
|
|
3508
|
+
}
|
|
3509
|
+
return {
|
|
3510
|
+
start,
|
|
3511
|
+
end: start + buffer.length,
|
|
3512
|
+
maxTs,
|
|
3513
|
+
minTs,
|
|
3514
|
+
sources,
|
|
3515
|
+
levels
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
async function read(handle, position, length) {
|
|
3519
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
3520
|
+
let filled = 0;
|
|
3521
|
+
while (filled < length) {
|
|
3522
|
+
const { bytesRead } = await handle.read(buffer, filled, length - filled, position + filled);
|
|
3523
|
+
if (bytesRead === 0) break;
|
|
3524
|
+
filled += bytesRead;
|
|
3525
|
+
}
|
|
3526
|
+
return buffer.subarray(0, filled);
|
|
3527
|
+
}
|
|
3528
|
+
async function stillExtends(handle, index, size) {
|
|
3529
|
+
if (size < index.size) return false;
|
|
3530
|
+
const head = await read(handle, 0, index.head.length);
|
|
3531
|
+
const tail = await read(handle, index.size - index.tail.length, index.tail.length);
|
|
3532
|
+
return head.equals(index.head) && tail.equals(index.tail);
|
|
3533
|
+
}
|
|
3534
|
+
function resumeFrom(index) {
|
|
3535
|
+
const last = index.blocks.at(-1);
|
|
3536
|
+
if (!last || last.end - last.start >= BLOCK_BYTES) return {
|
|
3537
|
+
...index,
|
|
3538
|
+
blocks: [...index.blocks]
|
|
3539
|
+
};
|
|
3540
|
+
return {
|
|
3541
|
+
...index,
|
|
3542
|
+
size: last.start,
|
|
3543
|
+
blocks: index.blocks.slice(0, -1),
|
|
3544
|
+
launchAttempts: index.launchAttempts.filter((attempt) => attempt.position < last.start)
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
async function indexGeneration(handle, identity, size, signal) {
|
|
3548
|
+
signal?.throwIfAborted();
|
|
3549
|
+
const pending = indexing.get(identity);
|
|
3550
|
+
if (pending) {
|
|
3551
|
+
await new Promise((resolve, reject) => {
|
|
3552
|
+
const cancel = () => reject(signal?.reason);
|
|
3553
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
3554
|
+
if (signal?.aborted) cancel();
|
|
3555
|
+
pending.then(() => resolve(), () => resolve()).finally(() => signal?.removeEventListener("abort", cancel));
|
|
3556
|
+
});
|
|
3557
|
+
return indexGeneration(handle, identity, size, signal);
|
|
3558
|
+
}
|
|
3559
|
+
const building = buildGenerationIndex(handle, identity, size, signal);
|
|
3560
|
+
indexing.set(identity, building);
|
|
3561
|
+
try {
|
|
3562
|
+
return await building;
|
|
3563
|
+
} finally {
|
|
3564
|
+
indexing.delete(identity);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
async function buildGenerationIndex(handle, identity, size, signal) {
|
|
3568
|
+
signal?.throwIfAborted();
|
|
3569
|
+
const cached = cache.get(identity);
|
|
3570
|
+
const known = cached && await stillExtends(handle, cached, size) ? cached : null;
|
|
3571
|
+
if (known && known.size === size) {
|
|
3572
|
+
cache.delete(identity);
|
|
3573
|
+
cache.set(identity, known);
|
|
3574
|
+
return known;
|
|
3575
|
+
}
|
|
3576
|
+
const index = known ? resumeFrom(known) : emptyIndex();
|
|
3577
|
+
const next = {
|
|
3578
|
+
...index,
|
|
3579
|
+
markers: new Map(index.markers),
|
|
3580
|
+
launchAttempts: [...index.launchAttempts],
|
|
3581
|
+
slots: new Set(index.slots)
|
|
3582
|
+
};
|
|
3583
|
+
let offset = next.size;
|
|
3584
|
+
let length = BLOCK_BYTES;
|
|
3585
|
+
while (offset < size) {
|
|
3586
|
+
signal?.throwIfAborted();
|
|
3587
|
+
const buffer = await read(handle, offset, Math.min(length, size - offset));
|
|
3588
|
+
const end = buffer.lastIndexOf(NEWLINE);
|
|
3589
|
+
if (end === -1) {
|
|
3590
|
+
if (buffer.length < Math.min(length, size - offset) || offset + buffer.length >= size) break;
|
|
3591
|
+
length *= 2;
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
next.blocks.push(indexBlock(next, buffer.subarray(0, end + 1), offset));
|
|
3595
|
+
offset += end + 1;
|
|
3596
|
+
length = BLOCK_BYTES;
|
|
3597
|
+
signal?.throwIfAborted();
|
|
3598
|
+
await setImmediate$1();
|
|
3599
|
+
}
|
|
3600
|
+
next.size = offset;
|
|
3601
|
+
next.head = await read(handle, 0, Math.min(VALIDATE_BYTES, offset));
|
|
3602
|
+
next.tail = await read(handle, Math.max(0, offset - VALIDATE_BYTES), Math.min(VALIDATE_BYTES, offset));
|
|
3603
|
+
cache.delete(identity);
|
|
3604
|
+
cache.set(identity, next);
|
|
3605
|
+
while (cache.size > MAX_CACHED_GENERATIONS) cache.delete(cache.keys().next().value);
|
|
3606
|
+
return next;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Opens `dir` for reading. With `order: 'timeline'`, native crash files follow every other file at equal
|
|
3610
|
+
* timestamps, as `readLogTimeline` orders them; with `'name'`, every file is ranked by name, as `readLogRecords`.
|
|
3611
|
+
*/
|
|
3612
|
+
async function openLogDir(dir, { order = "timeline", signal } = {}) {
|
|
3613
|
+
signal?.throwIfAborted();
|
|
3614
|
+
let names;
|
|
3615
|
+
try {
|
|
3616
|
+
names = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".ndjson")).map((entry) => entry.name).toSorted();
|
|
3617
|
+
} catch {
|
|
3618
|
+
names = [];
|
|
3619
|
+
}
|
|
3620
|
+
const ranked = order === "name" ? names : [...names.filter((name) => !name.startsWith(NATIVE_CRASH_LOG_PREFIX)), ...names.filter((name) => name.startsWith(NATIVE_CRASH_LOG_PREFIX))];
|
|
3621
|
+
const generations = [];
|
|
3622
|
+
const sizes = {};
|
|
3623
|
+
const close = async () => {
|
|
3624
|
+
await Promise.all(generations.map((generation) => generation.handle.close().catch(() => {})));
|
|
3625
|
+
};
|
|
3626
|
+
try {
|
|
3627
|
+
for (const [rank, name] of ranked.entries()) for (const [generation, path] of [rotatedLogPath(join(dir, name)), join(dir, name)].entries()) {
|
|
3628
|
+
let handle;
|
|
3629
|
+
try {
|
|
3630
|
+
handle = await open(path, "r");
|
|
3631
|
+
} catch {
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
try {
|
|
3635
|
+
const stat = await handle.stat({ bigint: true });
|
|
3636
|
+
const index = await indexGeneration(handle, `${stat.dev}:${stat.ino}`, Number(stat.size), signal);
|
|
3637
|
+
generations.push({
|
|
3638
|
+
handle,
|
|
3639
|
+
rank,
|
|
3640
|
+
generation,
|
|
3641
|
+
crash: name.startsWith(NATIVE_CRASH_LOG_PREFIX),
|
|
3642
|
+
index
|
|
3643
|
+
});
|
|
3644
|
+
if (generation === 1) sizes[name] = index.size;
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
await handle.close().catch(() => {});
|
|
3647
|
+
throw error;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
await close();
|
|
3652
|
+
throw error;
|
|
3653
|
+
}
|
|
3654
|
+
let firstTs;
|
|
3655
|
+
for (const { index } of generations) if (index.minTs !== null && (firstTs === void 0 || index.minTs < firstTs)) firstTs = index.minTs;
|
|
3656
|
+
return {
|
|
3657
|
+
sizes,
|
|
3658
|
+
firstTs,
|
|
3659
|
+
launchAttempts: () => generations.filter((generation) => !generation.crash).flatMap(({ index, rank, generation }) => index.launchAttempts.map(({ record, position }) => ({
|
|
3660
|
+
ts: tsKey(record),
|
|
3661
|
+
rank,
|
|
3662
|
+
generation,
|
|
3663
|
+
position,
|
|
3664
|
+
record
|
|
3665
|
+
}))).toSorted(compare).map((entry) => entry.record),
|
|
3666
|
+
query: (query = {}) => queryGenerations(generations, query, signal),
|
|
3667
|
+
batches: (query = {}) => queryBatches(generations, query, signal),
|
|
3668
|
+
filter: (query = {}) => planLogQuery(generationMarkers(generations), query).matches,
|
|
3669
|
+
close
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
function blockMatches(block, { errorsOnly, minLevel, sources }) {
|
|
3673
|
+
if (errorsOnly && !block.levels.has("error") && !block.levels.has("fatal")) return false;
|
|
3674
|
+
if (minLevel && ![...block.levels].some((level) => levelRank(level) >= levelRank(minLevel))) return false;
|
|
3675
|
+
return !sources?.length || sources.some((source) => block.sources.has(source)) || Boolean(errorsOnly && block.sources.has("device"));
|
|
3676
|
+
}
|
|
3677
|
+
async function select(generations, extra, { matches, excludesUpTo, mayContain, tail, first = false }, signal) {
|
|
3678
|
+
if (tail !== void 0 && tail <= 0) return [];
|
|
3679
|
+
const heap = new BoundedSelection(compare, tail, first);
|
|
3680
|
+
const extraRank = generations.length ? Math.max(...generations.map((generation) => generation.rank)) + 1 : 0;
|
|
3681
|
+
for (const [position, record] of extra.entries()) {
|
|
3682
|
+
const entry = {
|
|
3683
|
+
ts: tsKey(record),
|
|
3684
|
+
rank: extraRank,
|
|
3685
|
+
generation: 0,
|
|
3686
|
+
position,
|
|
3687
|
+
record
|
|
3688
|
+
};
|
|
3689
|
+
if (matches(record, entry)) heap.keep(entry);
|
|
3690
|
+
}
|
|
3691
|
+
const blocks = generations.flatMap((generation) => generation.index.blocks.map((block) => ({
|
|
3692
|
+
generation,
|
|
3693
|
+
block
|
|
3694
|
+
}))).toSorted((a, b) => (first ? a.block.minTs - b.block.minTs : b.block.maxTs - a.block.maxTs) || 0);
|
|
3695
|
+
for (const { generation, block } of blocks) {
|
|
3696
|
+
if (excludesUpTo(block.maxTs)) {
|
|
3697
|
+
if (first) continue;
|
|
3698
|
+
break;
|
|
3699
|
+
}
|
|
3700
|
+
if (first && tail !== void 0 && heap.size >= tail && block.minTs > heap.peek().ts) break;
|
|
3701
|
+
if (!first && tail !== void 0 && heap.size >= tail && block.maxTs < heap.peek().ts) break;
|
|
3702
|
+
signal?.throwIfAborted();
|
|
3703
|
+
if (!mayContain(block, generation)) continue;
|
|
3704
|
+
const buffer = await read(generation.handle, block.start, block.end - block.start);
|
|
3705
|
+
for (let lineStart = 0; lineStart < buffer.length;) {
|
|
3706
|
+
const lineEnd = buffer.indexOf(NEWLINE, lineStart);
|
|
3707
|
+
if (lineEnd === -1) break;
|
|
3708
|
+
const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
3709
|
+
const position = block.start + lineStart;
|
|
3710
|
+
lineStart = lineEnd + 1;
|
|
3711
|
+
if (!record) continue;
|
|
3712
|
+
const entry = {
|
|
3713
|
+
ts: tsKey(record),
|
|
3714
|
+
rank: generation.rank,
|
|
3715
|
+
generation: generation.generation,
|
|
3716
|
+
position,
|
|
3717
|
+
record
|
|
3718
|
+
};
|
|
3719
|
+
if (matches(record, entry)) heap.keep(entry);
|
|
3720
|
+
}
|
|
3721
|
+
await setImmediate$1();
|
|
3722
|
+
}
|
|
3723
|
+
return heap.sorted();
|
|
3724
|
+
}
|
|
3725
|
+
function generationMarkers(generations) {
|
|
3726
|
+
const markers = /* @__PURE__ */ new Map();
|
|
3727
|
+
for (const { index } of generations) for (const [key, marker] of index.markers) keepNewer(markers, key, marker);
|
|
3728
|
+
return [...markers.values()];
|
|
3729
|
+
}
|
|
3730
|
+
async function selectQuery(generations, { tail, extra = [], ...options }, signal, page) {
|
|
3731
|
+
signal?.throwIfAborted();
|
|
3732
|
+
const slots = /* @__PURE__ */ new Set(["default"]);
|
|
3733
|
+
for (const { index } of generations) for (const slot of index.slots) slots.add(slot);
|
|
3734
|
+
for (const record of extra) slots.add(record.slot ?? "default");
|
|
3735
|
+
const plan = planLogQuery([...generationMarkers(generations), ...extra.filter((record) => record.marker === true)], options);
|
|
3736
|
+
return select(generations, extra, {
|
|
3737
|
+
matches: (record, entry) => plan.matches(record) && (!page?.after || compare(entry, page.after) > 0),
|
|
3738
|
+
excludesUpTo: (maxTs) => plan.excludesUpTo(maxTs, slots),
|
|
3739
|
+
mayContain: (block, generation) => blockMatches(block, options) && (!page?.after || compare({
|
|
3740
|
+
ts: block.maxTs,
|
|
3741
|
+
rank: generation.rank,
|
|
3742
|
+
generation: generation.generation,
|
|
3743
|
+
position: block.end
|
|
3744
|
+
}, page.after) > 0),
|
|
3745
|
+
tail: page?.size ?? tail,
|
|
3746
|
+
first: page !== void 0
|
|
3747
|
+
}, signal);
|
|
3748
|
+
}
|
|
3749
|
+
async function queryGenerations(generations, query, signal) {
|
|
3750
|
+
return withErrorContext(generations, query, await selectQuery(generations, query, signal), signal);
|
|
3751
|
+
}
|
|
3752
|
+
async function* queryBatches(generations, query, signal) {
|
|
3753
|
+
const fixed = {
|
|
3754
|
+
...query,
|
|
3755
|
+
now: query.now ?? Date.now()
|
|
3756
|
+
};
|
|
3757
|
+
let after;
|
|
3758
|
+
for (;;) {
|
|
3759
|
+
const selected = await selectQuery(generations, fixed, signal, {
|
|
3760
|
+
after,
|
|
3761
|
+
size: 500
|
|
3762
|
+
});
|
|
3763
|
+
if (!selected.length) return;
|
|
3764
|
+
yield await withErrorContext(generations, fixed, selected, signal);
|
|
3765
|
+
after = selected.at(-1);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
async function withErrorContext(generations, { extra = [], ...options }, selected, signal) {
|
|
3769
|
+
const matched = selected.map((entry) => entry.record);
|
|
3770
|
+
if (!options.errorsOnly) return matched;
|
|
3771
|
+
const errors = matched.filter((record) => record.src === "metro" && record.raw === true && (record.level === "error" || record.level === "fatal") && typeof record.msg === "string");
|
|
3772
|
+
if (!errors.length) return matched;
|
|
3773
|
+
const contextual = /* @__PURE__ */ new Map();
|
|
3774
|
+
for (const error of errors) {
|
|
3775
|
+
const [first] = await select(generations, extra, {
|
|
3776
|
+
matches: (record) => record.ts === error.ts && record.src === error.src && record.msg === error.msg && record.event === error.event,
|
|
3777
|
+
excludesUpTo: (maxTs) => maxTs < tsKey(error),
|
|
3778
|
+
mayContain: (block) => block.sources.has("metro"),
|
|
3779
|
+
tail: 1,
|
|
3780
|
+
first: true
|
|
3781
|
+
}, signal);
|
|
3782
|
+
if (!first) continue;
|
|
3783
|
+
let limit = 32;
|
|
3784
|
+
for (;;) {
|
|
3785
|
+
const stream = (await select(generations, extra, {
|
|
3786
|
+
matches: (record, entry) => compare(entry, first) >= 0 && record.src === "metro" && record.event === error.event && tsKey(record) >= tsKey(error) && (record.raw === true || record.ts === error.ts && record.msg === error.msg),
|
|
3787
|
+
excludesUpTo: (maxTs) => maxTs < tsKey(error),
|
|
3788
|
+
mayContain: (block) => block.sources.has("metro"),
|
|
3789
|
+
tail: limit,
|
|
3790
|
+
first: true
|
|
3791
|
+
}, signal)).map((entry) => entry.record);
|
|
3792
|
+
const index = stream.findIndex((record) => record.ts === error.ts && record.msg === error.msg);
|
|
3793
|
+
if (index < 0 || stream.slice(index + 1).some((record) => record.raw === true && !isExpoErrorContext(record, error.event)) || stream.length < limit) {
|
|
3794
|
+
contextual.set(error, attachExpoErrorContext(stream, [error], "field")[0]);
|
|
3795
|
+
break;
|
|
3796
|
+
}
|
|
3797
|
+
limit *= 2;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
return matched.map((record) => contextual.get(record) ?? record);
|
|
3801
|
+
}
|
|
3802
|
+
//#endregion
|
|
2915
3803
|
//#region state/recordings.ts
|
|
2916
3804
|
/** The footage stim-server keeps per device: the last 15 minutes recorded, however long ago. */
|
|
2917
3805
|
const RECORDING_FOOTAGE_MS = 9e5;
|
|
@@ -3042,10 +3930,13 @@ function validRunId(value) {
|
|
|
3042
3930
|
}
|
|
3043
3931
|
let current = null;
|
|
3044
3932
|
/**
|
|
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
|
|
3933
|
+
* The run id of the ambient operation context, else of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
|
|
3934
|
+
* (Stim Desktop passes one), else a random one. The CLI entry exports it back to `STIM_RUN_ID` so processes the run
|
|
3935
|
+
* starts inherit it; this function does not.
|
|
3047
3936
|
*/
|
|
3048
3937
|
function runId(env = process.env) {
|
|
3938
|
+
const context = ambientContext();
|
|
3939
|
+
if (context) return context.runId;
|
|
3049
3940
|
if (current === null) current = validRunId(env["STIM_RUN_ID"]) ?? randomBytes(6).toString("hex");
|
|
3050
3941
|
return current;
|
|
3051
3942
|
}
|
|
@@ -3348,12 +4239,16 @@ function readPullRequestCache(path) {
|
|
|
3348
4239
|
if (typeof entry.checkedAt !== "string") return null;
|
|
3349
4240
|
const pullRequest = pullRequestOf(entry.pullRequest);
|
|
3350
4241
|
if (pullRequest === void 0) return null;
|
|
4242
|
+
const { containsHead, endedAt } = entry;
|
|
3351
4243
|
return {
|
|
3352
4244
|
path,
|
|
3353
4245
|
branch: entry.branch,
|
|
3354
4246
|
head: entry.head,
|
|
3355
4247
|
checkedAt: entry.checkedAt,
|
|
3356
|
-
pullRequest
|
|
4248
|
+
pullRequest,
|
|
4249
|
+
...typeof containsHead === "boolean" ? { containsHead } : {},
|
|
4250
|
+
...endedAt === null || typeof endedAt === "number" && Number.isFinite(endedAt) ? { endedAt } : {},
|
|
4251
|
+
...entry.partial === true ? { partial: true } : {}
|
|
3357
4252
|
};
|
|
3358
4253
|
}
|
|
3359
4254
|
/** The cached size of the folder at `path`, or null before it was measured. */
|
|
@@ -3453,7 +4348,7 @@ function readAgentSessionsCache() {
|
|
|
3453
4348
|
//#region state/stim-build.ts
|
|
3454
4349
|
/**
|
|
3455
4350
|
* 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
|
|
4351
|
+
* bytes of a `stim` `dist` directory. A client and a remote machine offload only when both compute the same one.
|
|
3457
4352
|
*/
|
|
3458
4353
|
function stimBuildDigest(dir) {
|
|
3459
4354
|
try {
|
|
@@ -3718,7 +4613,22 @@ function lastUseFrom(state, logMtimes) {
|
|
|
3718
4613
|
return candidates.length ? Math.max(...candidates) : NaN;
|
|
3719
4614
|
}
|
|
3720
4615
|
const MAINTENANCE_LOG_NAME = "maintenance.ndjson";
|
|
4616
|
+
/**
|
|
4617
|
+
* When the workspace was last used, in epoch milliseconds. A workspace that recorded no use falls back to the mtime
|
|
4618
|
+
* of its workspace.json, which is written once when the workspace is created, so it is a lower bound on the last
|
|
4619
|
+
* use. NaN when its state.json exists but does not parse, or when workspace.json cannot be read either.
|
|
4620
|
+
*/
|
|
3721
4621
|
function workspaceLastUsed(root) {
|
|
4622
|
+
const used = recordedLastUse(root);
|
|
4623
|
+
if (Number.isFinite(used)) return used;
|
|
4624
|
+
if (existsSync(workspaceStateFile(root)) && readWorkspaceState(root) === null) return NaN;
|
|
4625
|
+
try {
|
|
4626
|
+
return statSync(workspaceMetadataFile(root)).mtimeMs;
|
|
4627
|
+
} catch {
|
|
4628
|
+
return NaN;
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
function recordedLastUse(root) {
|
|
3722
4632
|
const logs = workspaceLogsDir(root);
|
|
3723
4633
|
let mtimes = [];
|
|
3724
4634
|
try {
|
|
@@ -3796,7 +4706,7 @@ function readViewedDevices(dir = deviceViewersDir()) {
|
|
|
3796
4706
|
//#endregion
|
|
3797
4707
|
//#region state/stats.ts
|
|
3798
4708
|
const STATS_VERSION = 1;
|
|
3799
|
-
/** Where a compiling build ran: here, on a remote
|
|
4709
|
+
/** Where a compiling build ran: here, on a remote machine, or here after offloading it failed. */
|
|
3800
4710
|
const PLACEMENT_DECISIONS = [
|
|
3801
4711
|
"here",
|
|
3802
4712
|
"offloaded",
|
|
@@ -3944,7 +4854,7 @@ function normalize(record) {
|
|
|
3944
4854
|
const source = record.projects;
|
|
3945
4855
|
if (source && typeof source === "object" && !Array.isArray(source)) for (const [key, scope] of Object.entries(source)) projects[key] = normalizeScope(scope);
|
|
3946
4856
|
const history = {};
|
|
3947
|
-
if (
|
|
4857
|
+
if (isJsonObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
|
|
3948
4858
|
const normalized = normalizeHistory(scope);
|
|
3949
4859
|
if (Object.keys(normalized).length) history[key] = normalized;
|
|
3950
4860
|
}
|
|
@@ -3952,8 +4862,8 @@ function normalize(record) {
|
|
|
3952
4862
|
const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
|
|
3953
4863
|
const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
|
|
3954
4864
|
const buildMachines = {};
|
|
3955
|
-
if (
|
|
3956
|
-
for (const [name, totals] of Object.entries(record.buildMachines)) if (
|
|
4865
|
+
if (isJsonObject(record.buildMachines)) {
|
|
4866
|
+
for (const [name, totals] of Object.entries(record.buildMachines)) if (isJsonObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
|
|
3957
4867
|
}
|
|
3958
4868
|
return {
|
|
3959
4869
|
version: 1,
|
|
@@ -3967,7 +4877,7 @@ function normalize(record) {
|
|
|
3967
4877
|
};
|
|
3968
4878
|
}
|
|
3969
4879
|
function normalizePlacement(value) {
|
|
3970
|
-
if (!
|
|
4880
|
+
if (!isJsonObject(value)) return [];
|
|
3971
4881
|
const { decision, platform, reason, project, machine } = value;
|
|
3972
4882
|
if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
|
|
3973
4883
|
if (typeof reason !== "string" || typeof project !== "string") return [];
|
|
@@ -3990,7 +4900,7 @@ function normalizePlacement(value) {
|
|
|
3990
4900
|
}];
|
|
3991
4901
|
}
|
|
3992
4902
|
function normalizeCapacityRefusal(value) {
|
|
3993
|
-
if (!
|
|
4903
|
+
if (!isJsonObject(value)) return [];
|
|
3994
4904
|
const { at, kind, platform, max, workspace } = value;
|
|
3995
4905
|
if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
|
|
3996
4906
|
if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
|
|
@@ -4005,7 +4915,7 @@ function normalizeCapacityRefusal(value) {
|
|
|
4005
4915
|
}];
|
|
4006
4916
|
}
|
|
4007
4917
|
function normalizeCapacityWait(value) {
|
|
4008
|
-
if (!
|
|
4918
|
+
if (!isJsonObject(value) || value.kind !== "device-wait") return [];
|
|
4009
4919
|
const refusal = normalizeCapacityRefusal({
|
|
4010
4920
|
...value,
|
|
4011
4921
|
kind: "device"
|
|
@@ -4029,20 +4939,17 @@ function normalizeMachineTotals(totals) {
|
|
|
4029
4939
|
...typeof totals.lastFallbackAt === "string" ? { lastFallbackAt: totals.lastFallbackAt } : {}
|
|
4030
4940
|
};
|
|
4031
4941
|
}
|
|
4032
|
-
function isObject(value) {
|
|
4033
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4034
|
-
}
|
|
4035
4942
|
function normalizeHistory(scope) {
|
|
4036
4943
|
const history = {};
|
|
4037
|
-
if (!
|
|
4944
|
+
if (!isJsonObject(scope)) return history;
|
|
4038
4945
|
for (const platform of PLATFORMS) {
|
|
4039
4946
|
const lists = scope[platform];
|
|
4040
|
-
if (!
|
|
4947
|
+
if (!isJsonObject(lists)) continue;
|
|
4041
4948
|
const normalized = {};
|
|
4042
4949
|
for (const outcome of OUTCOMES) {
|
|
4043
4950
|
const samples = lists[outcome];
|
|
4044
4951
|
if (!Array.isArray(samples)) continue;
|
|
4045
|
-
const kept = samples.filter(
|
|
4952
|
+
const kept = samples.filter(isJsonObject).flatMap((sample) => {
|
|
4046
4953
|
const durationMs = wholeMs(sample.durationMs);
|
|
4047
4954
|
if (durationMs <= 0) return [];
|
|
4048
4955
|
return [{
|
|
@@ -4060,7 +4967,7 @@ function normalizeHistory(scope) {
|
|
|
4060
4967
|
}
|
|
4061
4968
|
function wholePhases(phases) {
|
|
4062
4969
|
const out = {};
|
|
4063
|
-
if (!
|
|
4970
|
+
if (!isJsonObject(phases)) return out;
|
|
4064
4971
|
for (const [name, ms] of Object.entries(phases)) {
|
|
4065
4972
|
const value = Number(ms);
|
|
4066
4973
|
if (Number.isFinite(value) && value >= 0) out[name] = Math.round(value);
|
|
@@ -4187,6 +5094,8 @@ const DEVICE_HOST_LIMITS = {
|
|
|
4187
5094
|
logsMs: 15e3,
|
|
4188
5095
|
killGraceMs: 5e3
|
|
4189
5096
|
};
|
|
5097
|
+
/** How many parked sessions one reserve inspects, and possibly retires, before it creates a device. */
|
|
5098
|
+
const HOSTED_PARKED_CANDIDATES = 3;
|
|
4190
5099
|
/** How long a cancelled hosted worker run can take to settle: SIGTERM, SIGKILL after the grace, then it is abandoned. */
|
|
4191
5100
|
function hostedWorkerSettleMs(limits = DEVICE_HOST_LIMITS) {
|
|
4192
5101
|
return limits.killGraceMs * 2;
|
|
@@ -4845,7 +5754,14 @@ const KINDS = [
|
|
|
4845
5754
|
"would-stop-workspace",
|
|
4846
5755
|
"would-remove-worktree",
|
|
4847
5756
|
"would-remove-orphan",
|
|
4848
|
-
"would-unregister-cache"
|
|
5757
|
+
"would-unregister-cache",
|
|
5758
|
+
"would-delete-parked-device",
|
|
5759
|
+
"would-delete-devices",
|
|
5760
|
+
"would-clear-build-dirs",
|
|
5761
|
+
"would-delete-dependencies",
|
|
5762
|
+
"would-stop-gradle-daemons",
|
|
5763
|
+
"would-delete-gradle-version",
|
|
5764
|
+
"would-delete-temp-leftovers"
|
|
4849
5765
|
];
|
|
4850
5766
|
/** Rejects missing, corrupt or unsupported payloads without throwing. */
|
|
4851
5767
|
function parseMaintenanceState(value) {
|
|
@@ -4859,13 +5775,14 @@ function parseMaintenanceState(value) {
|
|
|
4859
5775
|
"metro-cache",
|
|
4860
5776
|
"compilation-cache",
|
|
4861
5777
|
"ccache",
|
|
5778
|
+
"owned-devices",
|
|
4862
5779
|
"other"
|
|
4863
5780
|
].includes(String(size.category)) && (size.workspace === void 0 || typeof size.workspace === "string") && (size.blocked === void 0 || typeof size.blocked === "string") && (size.idleDays === void 0 || nullableNumber(size.idleDays)))) return null;
|
|
4864
5781
|
if (!Array.isArray(value.plan) || !value.plan.every((action) => isJsonObject(action) && KINDS.includes(String(action.kind)) && typeof action.target === "string" && finite(action.bytes) && typeof action.reason === "string" && (action.workspace === void 0 || typeof action.workspace === "string") && (action.dir === void 0 || typeof action.dir === "string") && (action.olderThanDays === void 0 || finite(action.olderThanDays)) && (action.check === void 0 || [
|
|
4865
5782
|
"worktree",
|
|
4866
5783
|
"sweep",
|
|
4867
5784
|
"disk"
|
|
4868
|
-
].includes(String(action.check))))) return null;
|
|
5785
|
+
].includes(String(action.check))) && (action.level === void 0 || action.level === 2 || action.level === 3))) return null;
|
|
4869
5786
|
if (!Array.isArray(value.recent) || !value.recent.every((record) => isJsonObject(record) && record.src === "maintenance" && finite(record.ts) && typeof record.msg === "string" && [
|
|
4870
5787
|
"debug",
|
|
4871
5788
|
"info",
|
|
@@ -4881,7 +5798,12 @@ function parseMaintenanceState(value) {
|
|
|
4881
5798
|
if (value.lastPass !== null && (!isJsonObject(value.lastPass) || !finite(value.lastPass.startedAt) || !finite(value.lastPass.durationMs) || typeof value.lastPass.trigger !== "string" || value.lastPass.mode !== "report" && value.lastPass.mode !== "on" || !finite(value.lastPass.freedBytes) || value.lastPass.stopped !== 0 || !finite(value.lastPass.actions) || !strings(value.lastPass.blocked))) return null;
|
|
4882
5799
|
if (value.pressure !== null) {
|
|
4883
5800
|
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) ||
|
|
5801
|
+
if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) || pressure.diskLevel !== void 0 && ![
|
|
5802
|
+
0,
|
|
5803
|
+
1,
|
|
5804
|
+
2,
|
|
5805
|
+
3
|
|
5806
|
+
].some((level) => level === pressure.diskLevel) || !isJsonObject(pressure.memory) || ![
|
|
4885
5807
|
null,
|
|
4886
5808
|
"normal",
|
|
4887
5809
|
"warning",
|
|
@@ -4899,8 +5821,8 @@ function readMaintenanceAttempt() {
|
|
|
4899
5821
|
}
|
|
4900
5822
|
//#endregion
|
|
4901
5823
|
//#region state/agent-device.ts
|
|
4902
|
-
function agentDeviceStateDirs(home = homedir()) {
|
|
4903
|
-
const override =
|
|
5824
|
+
function agentDeviceStateDirs(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
|
|
5825
|
+
const override = env.AGENT_DEVICE_STATE_DIR?.trim();
|
|
4904
5826
|
const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
|
|
4905
5827
|
const workspaces = join(configDir(), "workspaces");
|
|
4906
5828
|
try {
|
|
@@ -4913,6 +5835,20 @@ function agentDeviceStateDirs(home = homedir()) {
|
|
|
4913
5835
|
} catch {}
|
|
4914
5836
|
return [...dirs];
|
|
4915
5837
|
}
|
|
5838
|
+
async function agentDeviceStateDirsAsync(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
|
|
5839
|
+
const override = env.AGENT_DEVICE_STATE_DIR?.trim();
|
|
5840
|
+
const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
|
|
5841
|
+
const workspaces = join(configDir(), "workspaces");
|
|
5842
|
+
try {
|
|
5843
|
+
for (const name of await readdir(workspaces)) {
|
|
5844
|
+
const dir = join(workspaces, name, "agent-device");
|
|
5845
|
+
try {
|
|
5846
|
+
if ((await stat(dir)).isDirectory()) dirs.add(dir);
|
|
5847
|
+
} catch {}
|
|
5848
|
+
}
|
|
5849
|
+
} catch {}
|
|
5850
|
+
return [...dirs];
|
|
5851
|
+
}
|
|
4916
5852
|
//#endregion
|
|
4917
5853
|
//#region state/archive.ts
|
|
4918
5854
|
function readArchives() {
|
|
@@ -5024,14 +5960,13 @@ const knownKeys = (value, keys) => Object.keys(value).every((key) => keys.includ
|
|
|
5024
5960
|
function parseSetupJournal(value) {
|
|
5025
5961
|
if (!isJsonObject(value) || !knownKeys(value, [
|
|
5026
5962
|
"v",
|
|
5027
|
-
"client",
|
|
5028
5963
|
"expiresAt",
|
|
5029
5964
|
"capabilities",
|
|
5030
5965
|
"steps",
|
|
5031
5966
|
"granted",
|
|
5032
5967
|
"done",
|
|
5033
5968
|
"exit"
|
|
5034
|
-
]) || value.v !== 1 ||
|
|
5969
|
+
]) || value.v !== 1 || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || !Array.isArray(value.capabilities) || !value.capabilities.every(capability) || !Array.isArray(value.steps) || !value.steps.every((step) => isJsonObject(step) && knownKeys(step, [
|
|
5035
5970
|
"id",
|
|
5036
5971
|
"state",
|
|
5037
5972
|
"title",
|
|
@@ -5057,4 +5992,4 @@ function isSetupJournalPrunable(journal, now) {
|
|
|
5057
5992
|
return Date.parse(journal.expiresAt) + 36e5 < now;
|
|
5058
5993
|
}
|
|
5059
5994
|
//#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,
|
|
5995
|
+
export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, CAPACITY_REFUSAL_LIMIT, CAPACITY_REFUSAL_MAX_AGE_MS, DEBUG_ENV, DEVICE_HOST_LIMITS, DEVICE_IDLE_SHUTDOWN_KEY, DEVICE_RECLAIM_REASON, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_METRO_GATEWAYS_KEY, HOSTED_METRO_REQUESTS_KEY, HOSTED_PARKED_CANDIDATES, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, KeyedEntities, LAST_BUILD_KEYS, LEVELS, MACHINE_CREDENTIALS_VERSION, MACHINE_OWNER_KINDS, MAINTENANCE_LOG_NAME, MEMORY_SOURCES, NATIVE_BUILD_STEPS, NATIVE_CRASH_LOG_PREFIX, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, RUN_ID_ENV, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, SUMMARY_ARCHIVES, TAILNET_MACHINE_PATTERN, TUNNEL_MODES, TUTORIAL_VERSION, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentDeviceStateDirs, agentDeviceStateDirsAsync, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, applyKeyedDelta, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, automaticMachineEnabled, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, cannotHost, capDrops, clientIdentity, clientIdentityKeyFile, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createAsyncAgentActionReader, createDebugLog, createdDevicesFile, createdDevicesLock, debugLogDir, debugLoggingEnabled, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, diskUsageFailureFile, diskUsageWalkClaims, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAgentDriverName, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, hostedWorkerSettleMs, isExpoErrorContext, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCacheUsageFile, machineCapacity, machineEndpoint, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openLogDir, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMachineCredential, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, planLogQuery, projectRootDirectories, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readDiskUsageFailure, readEasSessionLedger, readEndedAgentSessions, readHomeIdentity, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMachineCacheUsage, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, requireAutomaticMachine, runId, saturation, serverLocalSocket, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, statusViews, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedAndroid, unreadableHostedIos, unreadableHostedNative, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, validRunId, validateAutomaticPool, wholePhases, windowsShortDir, windowsShortDirKey, withConfigLock, withConfigLockAsync, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock, workspaceSummary };
|