stim 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{activity-BtG4Va9l.mjs → activity-DfVKZTEB.mjs} +44 -26
- package/dist/{android-BA2i_KAV.mjs → android-BHfAu2mf.mjs} +1 -1
- package/dist/{android-C1q4l523.mjs → android-CD8pr8Q2.mjs} +112 -54
- package/dist/{android-cas-DfJkwCXW.mjs → android-cas-B9Kv9aIy.mjs} +12 -10
- package/dist/{android-C4zG2Tuc.mjs → android-qx_31wJo.mjs} +414 -118
- package/dist/{app-install-DHut6JnV.mjs → app-install-CDFY2nUL.mjs} +1 -1
- package/dist/budget-E5o91NR1.mjs +1739 -0
- package/dist/{build-plan-CeAYgKAx.mjs → build-plan-BTa93o_r.mjs} +125 -107
- package/dist/{build-progress-CDwogz6R.mjs → build-progress-l7d-X-ZP.mjs} +76 -8
- package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cdp-Dq_ypAlJ.mjs +115 -0
- package/dist/chrome-B5hWgQJS.mjs +61 -0
- package/dist/cli.mjs +17 -16
- package/dist/collector-run.mjs +15 -9
- package/dist/{command-output-D5z2_bcF.mjs → command-output-BIL7rlX7.mjs} +1 -1
- package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
- package/dist/created-devices-Cd558h31.mjs +37 -0
- package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
- package/dist/{deps-OF7CD5Dx.mjs → deps-2Wo81np9.mjs} +2 -2
- package/dist/{device-BwH2iI3O.mjs → device-Cf7vl-so.mjs} +7 -7
- package/dist/{device-lease-D83nqeMX.mjs → device-lease-CL9dZf73.mjs} +55 -6
- package/dist/{device-pool-DHj7cdPy.mjs → device-pool-MuoiyRcu.mjs} +4 -4
- package/dist/{device-remote-BQZSnnmE.mjs → device-remote-DT9ngZiD.mjs} +254 -579
- package/dist/{doctor-D8QdMRMf.mjs → doctor-D2iysNNH.mjs} +50 -20
- package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-BoiOhck8.mjs} +7 -7
- package/dist/{gc-COZteyiq.mjs → gc-eA-PRBX8.mjs} +1022 -131
- package/dist/{guide-NDvTmMi-.mjs → guide-BfZ0OmiP.mjs} +909 -132
- package/dist/{guide-status-CWvti2Lg.mjs → guide-status-BaMYu55M.mjs} +2 -2
- package/dist/{idle-NETbeNiQ.mjs → idle-DxLi5OkN.mjs} +43 -10
- package/dist/{in-use-DVhlSoyn.mjs → in-use-wYEaKQM9.mjs} +11 -7
- package/dist/{ios-CQLKpNfr.mjs → ios-CcfCeZ19.mjs} +103 -58
- package/dist/{ios-DArbmwer.mjs → ios-CmEzZiOj.mjs} +31 -22
- package/dist/{ios-device-CwDZo0X4.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
- package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
- package/dist/{logs-B44a1UeU.mjs → logs-DcSRbC2t.mjs} +12 -11
- package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
- package/dist/{metro-BWLOJ_Wr.mjs → metro-BPeY1YMh.mjs} +5 -5
- package/dist/{named-ports-Dc7neAlc.mjs → named-ports-n7tnJsI8.mjs} +78 -6
- package/dist/native-runtime-CiiFHt0e.mjs +74 -0
- package/dist/{ownership-D2xbLVoz.mjs → ownership-Bz2kETAS.mjs} +2 -2
- package/dist/{ownership-BiojdtAp.mjs → ownership-PUP4Dwai.mjs} +175 -217
- package/dist/page-BTu7I60K.mjs +29 -0
- package/dist/{ports-DwBaRFJs.mjs → ports-C5UutqpJ.mjs} +5 -5
- package/dist/{project-BXZzOABt.mjs → project-DGeclhvO.mjs} +55 -87
- package/dist/{reload-By9Dwsrt.mjs → reload-C-cV5yy_.mjs} +80 -21
- package/dist/remote-cache--aehr4Pr.mjs +526 -0
- package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
- package/dist/server-expo-BeYwP_Xy.mjs +2 -0
- package/dist/{server-expo-o6H5gcBc.mjs → server-expo-DUCg52-K.mjs} +2 -2
- package/dist/{settings-C0ugALSC.mjs → settings-ssVBToDR.mjs} +29 -17
- package/dist/{settings-B5oFZEri.mjs → settings-xB0Frkg6.mjs} +13 -4
- package/dist/settings.schema.json +74 -2
- package/dist/{simslim-CIR3IYJO.mjs → simslim-Bbg9UWrt.mjs} +13 -6
- package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-BIWEW-wI.mjs} +4 -4
- package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
- package/dist/{start-BSsf2tUs.mjs → start-BZhItkiL.mjs} +110 -18
- package/dist/{start-i8zc9VmS.mjs → start-Rw2GD3nV.mjs} +1 -1
- package/dist/{state-WGdIbi_d.mjs → state-DwAU2_-D.mjs} +15 -3
- package/dist/state-l1pYwOre.mjs +148 -0
- package/dist/{stats-DBYJ5bxL.mjs → stats-CaSwmsSZ.mjs} +4 -4
- package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
- package/dist/{status-CitcBRsC.mjs → status-TJlL0HWo.mjs} +649 -116
- package/dist/{status-Q-WDEGCB.mjs → status-sszP4XKU.mjs} +130 -28
- package/dist/stim-desktop-CXt8-Z3J.mjs +40 -0
- package/dist/{stop-DIhrvu0B.mjs → stop-BZ2qMiBP.mjs} +50 -18
- package/dist/{stop-rst5mL14.mjs → stop-DskDKNTU.mjs} +2 -2
- package/dist/supervisor-run.mjs +11 -11
- package/dist/web-run.d.mts +22 -0
- package/dist/web-run.mjs +481 -0
- package/dist/web-sWXd5iek.mjs +394 -0
- package/dist/{worktree-DhxDTK44.mjs → worktree-3cBa-bQt.mjs} +1 -1
- package/dist/{worktree-KOjMX3zA.mjs → worktree-D4Do_XiQ.mjs} +229 -21
- package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
- package/dist/{xcode-B5SPD--o.mjs → xcode-LM6A8cNZ.mjs} +7 -7
- package/package.json +5 -5
- package/dist/budget-De3P2rq_.mjs +0 -620
- package/dist/native-run-D7yB_AsB.mjs +0 -143
- package/dist/server-expo-B-CCUJZj.mjs +0 -2
- package/dist/workspace-process-lock-Cx74RSDZ.mjs +0 -57
- package/dist/workspaces-COD2xCz-.mjs +0 -1013
|
@@ -1,67 +1,81 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { I as volumeRootFor, U as projectDeviceSlots, _ as loadConfig$1, d as getConfigDir, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
|
|
2
2
|
import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
|
|
3
3
|
import { a as parseNdjsonLine } from "./ndjson-CDY9IkVC.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { c as gitCommonDirOnDisk, p as linkedWorktreesOnDisk, s as gitCommonDir, y as repoRoot } from "./worktree-
|
|
6
|
-
import { l as projectShortcut, o as
|
|
7
|
-
import {
|
|
8
|
-
import { i as formatDuration } from "./command-output-
|
|
9
|
-
import { i as listLeaseFiles } from "./device-lease-
|
|
10
|
-
import { n as createActivityReader } from "./activity-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { a as
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
4
|
+
import { M as workspaceLogsDir, c as resolveProjectMetro, h as readWorkspaceState$1, i as listeningPidsByPort, j as workspaceLogErrorIndex, o as processCwd, r as listeningPids, s as processCwds, u as isMetroRunning } from "./metro-BPeY1YMh.mjs";
|
|
5
|
+
import { c as gitCommonDirOnDisk, p as linkedWorktreesOnDisk, s as gitCommonDir, y as repoRoot } from "./worktree-DeBQSVtR.mjs";
|
|
6
|
+
import { l as projectShortcut, o as findServerWorkspace, r as detectIsExpo } from "./project-DGeclhvO.mjs";
|
|
7
|
+
import { f as readWorkspaceLaunches, u as readRemoteSession } from "./state-DwAU2_-D.mjs";
|
|
8
|
+
import { i as formatDuration } from "./command-output-BIL7rlX7.mjs";
|
|
9
|
+
import { i as listLeaseFiles } from "./device-lease-CL9dZf73.mjs";
|
|
10
|
+
import { n as createActivityReader, r as createDeviceProcessTables } from "./activity-DfVKZTEB.mjs";
|
|
11
|
+
import { P as ownedAvdDeviceProfile, R as ownedAvdSerialResolver, ct as parkedMaxSetting, lt as readParked, rt as POOL_SETTING_REMEDY, u as androidToolPath } from "./android-qx_31wJo.mjs";
|
|
12
|
+
import { t as resolveSupervisorTarget } from "./ownership-PUP4Dwai.mjs";
|
|
13
|
+
import { a as readWebRecord, l as webFacts } from "./state-l1pYwOre.mjs";
|
|
14
|
+
import { _ as parseSimctlList, u as listAllIosSimsAsync } from "./ios-CmEzZiOj.mjs";
|
|
15
|
+
import { a as buildStatusLine, i as buildReport, r as activeBuildState, s as parseActiveBuild, t as ACTIVE_BUILD_KEY } from "./build-progress-l7d-X-ZP.mjs";
|
|
16
|
+
import { T as readEasSessionLedger$1, w as easMachineStateRoot$1 } from "./device-remote-DT9ngZiD.mjs";
|
|
17
|
+
import { _ as withWebFacts, a as deviceLeaseStates, d as poolLine, f as remoteDeviceLine, g as unprovisionedWorktrees, h as tightVolumes, i as deviceLeaseLines, l as gitSummaryText, m as statusActivity, n as activityLabel, o as diskLine, p as remoteDeviceState, r as capacity, s as environmentState, u as parseDfFree } from "./status-sszP4XKU.mjs";
|
|
18
|
+
import { a as statsProjectKey, r as readStats } from "./stats-D8rUTPHd.mjs";
|
|
19
|
+
import { n as mergeState } from "./merge-state-ytFQCeh4.mjs";
|
|
20
|
+
import { i as readSupervisorState } from "./stop-BZ2qMiBP.mjs";
|
|
21
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
21
22
|
import { closeSync as closeSync$1, existsSync as existsSync$1, fstatSync as fstatSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, renameSync as renameSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
22
23
|
import { basename, dirname, join } from "path";
|
|
23
24
|
import { rotatedLogPath } from "@stim-cli/core";
|
|
24
|
-
import { ERROR_SOURCES, gitMergeCacheDir, logFiles, queryLogs, readIdleStop, readLastBuilds, recordMatches } from "@stim-cli/core/state";
|
|
25
|
-
import { join as join$1, posix } from "node:path";
|
|
25
|
+
import { ERROR_SOURCES, MACHINE_OWNER_KINDS, gitMergeCacheDir, logFiles, queryLogs, readBuildHistory, readIdleStop, readLastBuilds, recordMatches, withStateReadCache } from "@stim-cli/core/state";
|
|
26
|
+
import { join as join$1, posix, resolve as resolve$1 } from "node:path";
|
|
26
27
|
import { createHash, randomUUID } from "node:crypto";
|
|
27
28
|
import { homedir, totalmem } from "os";
|
|
28
29
|
import chalk from "chalk";
|
|
29
30
|
//#region src/status-watch.ts
|
|
30
31
|
const WATCH_FALLBACK_MS = 3e4;
|
|
31
|
-
const
|
|
32
|
+
const WATCH_LIGHT_INTERVAL_MS = 15e3;
|
|
33
|
+
const WATCH_SIMCTL_INTERVAL_MS = 5e3;
|
|
32
34
|
const ADB_RESTART_MIN_MS = 1e3;
|
|
33
35
|
const ADB_RESTART_MAX_MS = 6e4;
|
|
34
36
|
const SIMCTL_TIMEOUT_MS = 1e4;
|
|
35
37
|
/**
|
|
36
|
-
* Coalesces change signals into refreshes: a burst of triggers runs `run` once
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* Coalesces change signals into refreshes: a burst of full triggers runs `run('full')` once after `debounceMs` of
|
|
39
|
+
* quiet, at most one run is in flight, and a trigger that arrives during a run causes exactly one more run after it.
|
|
40
|
+
* A `light` trigger runs `run('light')` no sooner than `debounceMs` from the trigger and `lightIntervalMs` from the
|
|
41
|
+
* start of the previous run, unless a queued full run covers it; later light triggers do not postpone it.
|
|
39
42
|
*/
|
|
40
|
-
function createRefreshScheduler({ debounceMs, run }) {
|
|
43
|
+
function createRefreshScheduler({ debounceMs, lightIntervalMs = 0, run }) {
|
|
41
44
|
let timer = null;
|
|
45
|
+
let queued = "full";
|
|
42
46
|
let running = false;
|
|
43
|
-
let pending =
|
|
47
|
+
let pending = null;
|
|
44
48
|
let stopped = false;
|
|
49
|
+
let lastRunAt = -Infinity;
|
|
45
50
|
const fire = async () => {
|
|
46
51
|
timer = null;
|
|
47
52
|
running = true;
|
|
53
|
+
lastRunAt = Date.now();
|
|
48
54
|
try {
|
|
49
|
-
await run();
|
|
55
|
+
await run(queued);
|
|
50
56
|
} finally {
|
|
51
57
|
running = false;
|
|
52
58
|
if (pending) {
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
const next = pending;
|
|
60
|
+
pending = null;
|
|
61
|
+
trigger(next);
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
};
|
|
58
|
-
function trigger(delayMs = debounceMs) {
|
|
65
|
+
function trigger(kind = "full", delayMs = debounceMs) {
|
|
59
66
|
if (stopped) return;
|
|
60
67
|
if (running) {
|
|
61
|
-
pending =
|
|
68
|
+
pending = pending === "full" ? "full" : kind;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (kind === "light") {
|
|
72
|
+
if (timer) return;
|
|
73
|
+
queued = "light";
|
|
74
|
+
timer = setTimeout(() => void fire(), Math.max(delayMs, lastRunAt + lightIntervalMs - Date.now()));
|
|
62
75
|
return;
|
|
63
76
|
}
|
|
64
77
|
if (timer) clearTimeout(timer);
|
|
78
|
+
queued = "full";
|
|
65
79
|
timer = setTimeout(() => void fire(), delayMs);
|
|
66
80
|
}
|
|
67
81
|
return {
|
|
@@ -74,17 +88,19 @@ function createRefreshScheduler({ debounceMs, run }) {
|
|
|
74
88
|
};
|
|
75
89
|
}
|
|
76
90
|
/**
|
|
77
|
-
*
|
|
78
|
-
* the
|
|
91
|
+
* Which refresh a change to `name` in a watched `$STIM_HOME` directory needs: `light` for a log append, which can
|
|
92
|
+
* change only the log-derived fields, `full` for anything else that can change the payload, null for none. A null
|
|
93
|
+
* name means the platform did not report one.
|
|
79
94
|
*/
|
|
80
|
-
function
|
|
81
|
-
if (name
|
|
82
|
-
if (
|
|
95
|
+
function statusChange(dir, name) {
|
|
96
|
+
if (name?.includes(".lock")) return null;
|
|
97
|
+
if (dir === "logs") return "light";
|
|
98
|
+
if (name === null) return "full";
|
|
83
99
|
switch (dir) {
|
|
84
|
-
case "home": return name.startsWith("config.json") || name === "workspaces" || name === "device-locks";
|
|
85
|
-
case "workspace": return name.startsWith("state.json") || name === "logs";
|
|
86
|
-
case "eas": return name.startsWith("sessions.json");
|
|
87
|
-
default: return
|
|
100
|
+
case "home": return name.startsWith("config.json") || name === "workspaces" || name === "device-locks" ? "full" : null;
|
|
101
|
+
case "workspace": return name.startsWith("state.json") || name === "logs" ? "full" : null;
|
|
102
|
+
case "eas": return name.startsWith("sessions.json") ? "full" : null;
|
|
103
|
+
default: return "full";
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
/** A stable summary of every simulator's state, or null when the listing is unreadable. */
|
|
@@ -132,9 +148,13 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
132
148
|
if (watchers.has(dir)) continue;
|
|
133
149
|
try {
|
|
134
150
|
const watcher = watch(dir, (_event, name) => {
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
151
|
+
const change = statusChange(kind, name === null ? null : String(name));
|
|
152
|
+
if (!change) return;
|
|
153
|
+
if (kind === "home" || kind === "workspaces" || kind === "workspace") {
|
|
154
|
+
listingStaleBefore = Date.now();
|
|
155
|
+
reconcile();
|
|
156
|
+
}
|
|
157
|
+
onChange(change);
|
|
138
158
|
});
|
|
139
159
|
watcher.on("error", () => {
|
|
140
160
|
watcher.close();
|
|
@@ -144,13 +164,16 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
144
164
|
} catch {}
|
|
145
165
|
}
|
|
146
166
|
};
|
|
147
|
-
const adb = trackAdbDevices(onChange);
|
|
167
|
+
const adb = trackAdbDevices(() => onChange("full"));
|
|
148
168
|
let simctl = null;
|
|
169
|
+
let listing = null;
|
|
170
|
+
let listingStaleBefore = 0;
|
|
149
171
|
let simTimer = null;
|
|
150
172
|
if (platform === "darwin") {
|
|
151
173
|
let last = null;
|
|
152
174
|
const check = () => {
|
|
153
175
|
if (simctl || stopped) return;
|
|
176
|
+
const startedAt = Date.now();
|
|
154
177
|
let out = "";
|
|
155
178
|
const child = getExecutor().spawn("xcrun", [
|
|
156
179
|
"simctl",
|
|
@@ -175,7 +198,11 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
175
198
|
simctl = null;
|
|
176
199
|
const signature = simulatorSignature(out);
|
|
177
200
|
if (signature === null || stopped) return;
|
|
178
|
-
|
|
201
|
+
listing = {
|
|
202
|
+
at: startedAt,
|
|
203
|
+
json: out
|
|
204
|
+
};
|
|
205
|
+
if (last !== null && signature !== last) onChange("full");
|
|
179
206
|
last = signature;
|
|
180
207
|
});
|
|
181
208
|
};
|
|
@@ -185,6 +212,7 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
|
|
|
185
212
|
reconcile();
|
|
186
213
|
return {
|
|
187
214
|
reconcile,
|
|
215
|
+
simulatorListing: () => listing && listing.at > listingStaleBefore && Date.now() - listing.at <= 2 * simctlIntervalMs ? listing.json : null,
|
|
188
216
|
stop() {
|
|
189
217
|
stopped = true;
|
|
190
218
|
for (const watcher of watchers.values()) watcher.close();
|
|
@@ -238,7 +266,7 @@ function trackAdbDevices(onChange) {
|
|
|
238
266
|
}
|
|
239
267
|
//#endregion
|
|
240
268
|
//#region src/diagnostics/error-index.ts
|
|
241
|
-
const INDEX_VERSION =
|
|
269
|
+
const INDEX_VERSION = 3;
|
|
242
270
|
const HEAD_BYTES = 1024;
|
|
243
271
|
const TAIL_BYTES = 256;
|
|
244
272
|
const CHUNK_BYTES = 4194304;
|
|
@@ -251,7 +279,7 @@ const CANDIDATE_PATTERNS = [
|
|
|
251
279
|
const ERROR_CRITERIA = {
|
|
252
280
|
errorsOnly: true,
|
|
253
281
|
sources: ERROR_SOURCES,
|
|
254
|
-
|
|
282
|
+
includeAppDeviceErrors: true
|
|
255
283
|
};
|
|
256
284
|
/**
|
|
257
285
|
* The number of records `queryLogs({ dir, errorsOnly: true })` returns, without parsing every
|
|
@@ -291,8 +319,9 @@ function summarize(path, known) {
|
|
|
291
319
|
return null;
|
|
292
320
|
}
|
|
293
321
|
try {
|
|
294
|
-
const
|
|
295
|
-
const key = `${dev}:${ino}`;
|
|
322
|
+
const stat = fstatSync$1(fd, { bigint: true });
|
|
323
|
+
const key = `${stat.dev}:${stat.ino}`;
|
|
324
|
+
const size = Number(stat.size);
|
|
296
325
|
const cached = known[key];
|
|
297
326
|
const base = cached && stillExtends(fd, cached, size) ? cached : null;
|
|
298
327
|
const start = base ?? {
|
|
@@ -370,12 +399,17 @@ function collect(record, markers, errors) {
|
|
|
370
399
|
const ts = typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : null;
|
|
371
400
|
if (record.marker === true && ts !== null) {
|
|
372
401
|
const metro = record.src === "metro";
|
|
402
|
+
const web = !metro && record.platform === "web";
|
|
373
403
|
const slot = record.slot ?? "default";
|
|
374
|
-
const key = metro ? "metro" : JSON.stringify(["launch", slot]);
|
|
404
|
+
const key = metro ? "metro" : web ? "web" : JSON.stringify(["launch", slot]);
|
|
375
405
|
if ((markers[key]?.ts ?? -Infinity) < ts) markers[key] = metro ? {
|
|
376
406
|
src: "metro",
|
|
377
407
|
marker: true,
|
|
378
408
|
ts
|
|
409
|
+
} : web ? {
|
|
410
|
+
platform: "web",
|
|
411
|
+
marker: true,
|
|
412
|
+
ts
|
|
379
413
|
} : {
|
|
380
414
|
marker: true,
|
|
381
415
|
ts,
|
|
@@ -386,6 +420,7 @@ function collect(record, markers, errors) {
|
|
|
386
420
|
src: record.src,
|
|
387
421
|
level: record.level,
|
|
388
422
|
event: record.event,
|
|
423
|
+
platform: record.platform,
|
|
389
424
|
ts: ts ?? void 0,
|
|
390
425
|
slot: record.slot
|
|
391
426
|
});
|
|
@@ -413,6 +448,7 @@ function writeIndex(file, index) {
|
|
|
413
448
|
//#region src/workspace/git-summary.ts
|
|
414
449
|
const GIT_TIMEOUT_MS = 3e3;
|
|
415
450
|
const MERGE_BUDGET_MS = 250;
|
|
451
|
+
const MERGE_TIMEOUT_BACKOFF_MS = 3e5;
|
|
416
452
|
const ORIGIN_PREFIX = "refs/remotes/origin/";
|
|
417
453
|
/** Counts the entries of `git status --porcelain=v2 --branch` output. `head` is null on an unborn branch. */
|
|
418
454
|
function parseGitStatus(text) {
|
|
@@ -508,11 +544,33 @@ function mergedInto(path, head, target, budget) {
|
|
|
508
544
|
const targetKey = `${target.ref} ${target.sha}`;
|
|
509
545
|
const cached = readMergeCache(path);
|
|
510
546
|
const known = cached?.head === head ? cached : null;
|
|
511
|
-
if (known?.target === targetKey) return
|
|
512
|
-
|
|
547
|
+
if (known?.target === targetKey) return {
|
|
548
|
+
into: known.mergedInto,
|
|
549
|
+
deferred: false
|
|
550
|
+
};
|
|
551
|
+
const standIn = known?.target.startsWith(`${target.ref} `) ? known.mergedInto : null;
|
|
552
|
+
const timeoutKey = `${head} ${targetKey}`;
|
|
553
|
+
const timedOut = mergeTimeouts.get(path);
|
|
554
|
+
if (timedOut?.key === timeoutKey && Date.now() - timedOut.at < MERGE_TIMEOUT_BACKOFF_MS) return {
|
|
555
|
+
into: standIn,
|
|
556
|
+
deferred: false
|
|
557
|
+
};
|
|
558
|
+
if (budget.deadline !== void 0 && Date.now() > budget.deadline) return {
|
|
559
|
+
into: standIn,
|
|
560
|
+
deferred: true
|
|
561
|
+
};
|
|
513
562
|
budget.deadline ??= Date.now() + MERGE_BUDGET_MS;
|
|
514
563
|
const state = mergeState(path, target, { timeoutMs: GIT_TIMEOUT_MS });
|
|
515
|
-
if (!state.merged && state.timedOut)
|
|
564
|
+
if (!state.merged && state.timedOut) {
|
|
565
|
+
mergeTimeouts.set(path, {
|
|
566
|
+
key: timeoutKey,
|
|
567
|
+
at: Date.now()
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
into: standIn,
|
|
571
|
+
deferred: false
|
|
572
|
+
};
|
|
573
|
+
}
|
|
516
574
|
const verdict = state.merged ? state.into : null;
|
|
517
575
|
writeMergeCache({
|
|
518
576
|
path,
|
|
@@ -520,29 +578,97 @@ function mergedInto(path, head, target, budget) {
|
|
|
520
578
|
target: targetKey,
|
|
521
579
|
mergedInto: verdict
|
|
522
580
|
});
|
|
523
|
-
return
|
|
581
|
+
return {
|
|
582
|
+
into: verdict,
|
|
583
|
+
deferred: false
|
|
584
|
+
};
|
|
524
585
|
}
|
|
586
|
+
const mergeTimeouts = /* @__PURE__ */ new Map();
|
|
525
587
|
const recent = /* @__PURE__ */ new Map();
|
|
588
|
+
function gitDirsOf(worktree) {
|
|
589
|
+
try {
|
|
590
|
+
const dotGit = join$1(worktree, ".git");
|
|
591
|
+
const pointer = statSync(dotGit).isDirectory() ? null : /^gitdir: (.+)$/m.exec(readFileSync(dotGit, "utf-8"))?.[1]?.trim();
|
|
592
|
+
const gitDir = pointer ? resolve$1(worktree, pointer) : pointer === null ? dotGit : null;
|
|
593
|
+
if (!gitDir) return null;
|
|
594
|
+
try {
|
|
595
|
+
return {
|
|
596
|
+
gitDir,
|
|
597
|
+
commonDir: resolve$1(gitDir, readFileSync(join$1(gitDir, "commondir"), "utf-8").trim())
|
|
598
|
+
};
|
|
599
|
+
} catch {
|
|
600
|
+
return {
|
|
601
|
+
gitDir,
|
|
602
|
+
commonDir: gitDir
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function fileStamp(path) {
|
|
610
|
+
try {
|
|
611
|
+
const stat = statSync(path, { bigint: true });
|
|
612
|
+
return `${stat.ino}:${stat.mtimeNs}:${stat.size}`;
|
|
613
|
+
} catch {
|
|
614
|
+
return "-";
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Stamps the git files a summary depends on: the worktree's index, HEAD and reflog, and the common dir's packed refs,
|
|
619
|
+
* last fetch, origin/HEAD, default-branch ref, branch ref and upstream ref. Edits, creations and deletions of files
|
|
620
|
+
* that are not staged touch none of them. `missing` when the worktree has no git dir, such as a deleted worktree git
|
|
621
|
+
* still lists; it is then stamped by its `.git` entry alone.
|
|
622
|
+
*/
|
|
623
|
+
function gitFilesStamp(worktree, refs) {
|
|
624
|
+
const dirs = gitDirsOf(worktree.path);
|
|
625
|
+
if (!dirs) return {
|
|
626
|
+
stamp: fileStamp(join$1(worktree.path, ".git")),
|
|
627
|
+
missing: true
|
|
628
|
+
};
|
|
629
|
+
const { gitDir, commonDir } = dirs;
|
|
630
|
+
return {
|
|
631
|
+
stamp: [
|
|
632
|
+
join$1(gitDir, "index"),
|
|
633
|
+
join$1(gitDir, "HEAD"),
|
|
634
|
+
join$1(gitDir, "logs", "HEAD"),
|
|
635
|
+
join$1(commonDir, "packed-refs"),
|
|
636
|
+
join$1(commonDir, "FETCH_HEAD"),
|
|
637
|
+
join$1(commonDir, "refs", "remotes", "origin", "HEAD"),
|
|
638
|
+
...refs.target ? [join$1(commonDir, refs.target)] : [],
|
|
639
|
+
...worktree.branch ? [join$1(commonDir, "refs", "heads", worktree.branch)] : [],
|
|
640
|
+
...refs.upstream ? [join$1(commonDir, "refs", "remotes", refs.upstream), join$1(commonDir, "refs", "heads", refs.upstream)] : []
|
|
641
|
+
].map(fileStamp).join(" "),
|
|
642
|
+
missing: false
|
|
643
|
+
};
|
|
644
|
+
}
|
|
526
645
|
/**
|
|
527
646
|
* Reads every worktree's git summary in parallel, each git call bounded by a timeout. A worktree `skip` rejects, or
|
|
528
647
|
* one git cannot answer in time, maps to null. After every read, merge verdicts missing from the cache are judged one
|
|
529
648
|
* at a time; no new judgement starts 250 ms after the first, and a verdict for the same HEAD judged at an older
|
|
530
|
-
* default-branch commit stands in, because a merged branch stays merged.
|
|
531
|
-
* recently in this process is
|
|
649
|
+
* default-branch commit stands in, because a merged branch stays merged. A judgement that timed out is not retried
|
|
650
|
+
* for the same HEAD and target for five minutes. With `maxAgeMs`, a summary made that recently in this process is
|
|
651
|
+
* reused while the git files it depends on are unchanged, so a change that is not staged can take up to `maxAgeMs`
|
|
652
|
+
* to show; so is the null of a worktree whose directory is gone.
|
|
532
653
|
*/
|
|
533
654
|
async function readWorktreeGit(worktrees, { skip, maxAgeMs = 0 }) {
|
|
534
655
|
const now = Date.now();
|
|
535
656
|
const targets = /* @__PURE__ */ new Map();
|
|
536
657
|
const reads = await Promise.all(worktrees.map(async (worktree) => {
|
|
537
|
-
const memo = recent.get(worktree.path);
|
|
538
|
-
if (memo && now - memo.at < maxAgeMs) return {
|
|
539
|
-
path: worktree.path,
|
|
540
|
-
memo: memo.git
|
|
541
|
-
};
|
|
542
658
|
if (skip(worktree)) return {
|
|
543
659
|
path: worktree.path,
|
|
544
660
|
memo: null
|
|
545
661
|
};
|
|
662
|
+
const memo = maxAgeMs > 0 ? recent.get(worktree.path) : void 0;
|
|
663
|
+
const refs = memo?.refs ?? {
|
|
664
|
+
upstream: null,
|
|
665
|
+
target: null
|
|
666
|
+
};
|
|
667
|
+
const files = maxAgeMs > 0 ? gitFilesStamp(worktree, refs) : null;
|
|
668
|
+
if (memo && memo.files !== null && memo.files === files?.stamp && now - memo.at < maxAgeMs) return {
|
|
669
|
+
path: worktree.path,
|
|
670
|
+
memo: memo.git
|
|
671
|
+
};
|
|
546
672
|
const repository = worktree.repository;
|
|
547
673
|
if (repository && !targets.has(repository)) targets.set(repository, defaultBranchOf(repository));
|
|
548
674
|
const [out, target] = await Promise.all([git(worktree.path, [
|
|
@@ -554,23 +680,43 @@ async function readWorktreeGit(worktrees, { skip, maxAgeMs = 0 }) {
|
|
|
554
680
|
return {
|
|
555
681
|
path: worktree.path,
|
|
556
682
|
status: out === null ? null : parseGitStatus(out),
|
|
557
|
-
target
|
|
683
|
+
target,
|
|
684
|
+
files,
|
|
685
|
+
refs
|
|
558
686
|
};
|
|
559
687
|
}));
|
|
560
688
|
const budget = {};
|
|
561
689
|
const summaries = /* @__PURE__ */ new Map();
|
|
562
690
|
for (const read of reads) {
|
|
563
|
-
if ("memo" in read
|
|
564
|
-
summaries.set(read.path,
|
|
691
|
+
if ("memo" in read) {
|
|
692
|
+
summaries.set(read.path, read.memo ?? null);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (!read.status) {
|
|
696
|
+
if (read.files?.missing) recent.set(read.path, {
|
|
697
|
+
at: now,
|
|
698
|
+
files: read.files.stamp,
|
|
699
|
+
refs: read.refs,
|
|
700
|
+
git: null
|
|
701
|
+
});
|
|
702
|
+
summaries.set(read.path, null);
|
|
565
703
|
continue;
|
|
566
704
|
}
|
|
567
705
|
const { head, ...counts } = read.status;
|
|
706
|
+
const merge = head && read.target ? mergedInto(read.path, head, read.target, budget) : null;
|
|
568
707
|
const summary = {
|
|
569
708
|
...counts,
|
|
570
|
-
mergedInto:
|
|
709
|
+
mergedInto: merge?.into ?? null
|
|
710
|
+
};
|
|
711
|
+
const refs = {
|
|
712
|
+
upstream: counts.upstream,
|
|
713
|
+
target: read.target?.ref ?? null
|
|
571
714
|
};
|
|
715
|
+
const settled = !merge?.deferred && refs.upstream === read.refs.upstream && refs.target === read.refs.target;
|
|
572
716
|
recent.set(read.path, {
|
|
573
717
|
at: now,
|
|
718
|
+
files: settled ? read.files?.stamp ?? null : null,
|
|
719
|
+
refs,
|
|
574
720
|
git: summary
|
|
575
721
|
});
|
|
576
722
|
summaries.set(read.path, summary);
|
|
@@ -578,25 +724,268 @@ async function readWorktreeGit(worktrees, { skip, maxAgeMs = 0 }) {
|
|
|
578
724
|
return summaries;
|
|
579
725
|
}
|
|
580
726
|
//#endregion
|
|
727
|
+
//#region src/devices/app-process.ts
|
|
728
|
+
const bundleIdsByApp = /* @__PURE__ */ new Map();
|
|
729
|
+
function simAppPaths(commands, udid) {
|
|
730
|
+
const marker = `/Devices/${udid}/data/Containers/Bundle/Application/`;
|
|
731
|
+
const paths = /* @__PURE__ */ new Set();
|
|
732
|
+
for (const command of commands) {
|
|
733
|
+
const at = command.indexOf(marker);
|
|
734
|
+
if (!command.startsWith("/") || at < 0 || /[\s=]\//.test(command.slice(0, at))) continue;
|
|
735
|
+
const rest = command.slice(at + marker.length);
|
|
736
|
+
const app = /^[^/]+\/[^/]+\.app\//.exec(rest)?.[0];
|
|
737
|
+
if (!app || /^[^/\s]*\//.test(rest.slice(app.length))) continue;
|
|
738
|
+
paths.add(command.slice(0, at + marker.length) + app.slice(0, -1));
|
|
739
|
+
}
|
|
740
|
+
return [...paths];
|
|
741
|
+
}
|
|
742
|
+
function androidAppRunning(psOutput, packageName) {
|
|
743
|
+
return psOutput.split("\n").some((line) => {
|
|
744
|
+
return /^\s*\d+\s+(\S+)\s*$/.exec(line)?.[1] === packageName;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function plistBundleId(appPath) {
|
|
748
|
+
const cached = bundleIdsByApp.get(appPath);
|
|
749
|
+
if (cached) return cached;
|
|
750
|
+
const id = getExecutor().runFileQuiet("plutil", [
|
|
751
|
+
"-extract",
|
|
752
|
+
"CFBundleIdentifier",
|
|
753
|
+
"raw",
|
|
754
|
+
"-o",
|
|
755
|
+
"-",
|
|
756
|
+
join$1(appPath, "Info.plist")
|
|
757
|
+
], { timeoutMs: 2e3 })?.trim() || null;
|
|
758
|
+
if (id) bundleIdsByApp.set(appPath, id);
|
|
759
|
+
return id;
|
|
760
|
+
}
|
|
761
|
+
function createAppProcessReader(tables, bundleIdOf = plistBundleId) {
|
|
762
|
+
return ({ platform, id, appId }) => {
|
|
763
|
+
const result = (state) => ({
|
|
764
|
+
id: appId,
|
|
765
|
+
state
|
|
766
|
+
});
|
|
767
|
+
if (platform === "android") {
|
|
768
|
+
const output = tables.android(id);
|
|
769
|
+
if (output === null) return result("unknown");
|
|
770
|
+
return result(androidAppRunning(output, appId) ? "running" : "stopped");
|
|
771
|
+
}
|
|
772
|
+
const host = tables.host();
|
|
773
|
+
if (host === null) return result("unknown");
|
|
774
|
+
const ids = simAppPaths(host.map((row) => row.command), id).map(bundleIdOf);
|
|
775
|
+
if (ids.includes(appId)) return result("running");
|
|
776
|
+
return result(ids.includes(null) ? "unknown" : "stopped");
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/machine-usage.ts
|
|
781
|
+
const SHARED = [
|
|
782
|
+
{
|
|
783
|
+
name: "CoreSimulator services",
|
|
784
|
+
pattern: /\/CoreSimulator\.framework\/|\/Simulator\.app\/Contents\/MacOS\//
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: "adb server",
|
|
788
|
+
pattern: /(^|\/)adb\s.*\bfork-server\b/
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "Android emulator services",
|
|
792
|
+
pattern: /\/emulator\/netsimd(\s|$)/
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "Gradle daemon",
|
|
796
|
+
pattern: /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\b/
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: "Kotlin daemon",
|
|
800
|
+
pattern: /\bKotlinCompileDaemon\b/
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "Watchman",
|
|
804
|
+
pattern: /(^|\/)watchman(\s|$)/
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
const SERVER = /(^|[\s/])stim-server(\.mjs)?(\s|$)/;
|
|
808
|
+
const LAUNCHD_SIM = /^launchd_sim\s.*\/Devices\/([^/\s]+)\//;
|
|
809
|
+
const MAX_DEPTH = 64;
|
|
810
|
+
const EMULATOR = /(?:^|\/)(?:qemu-system-[^\s/]+|emulator)(?:\s.*)?\s(?:-avd\s+|@)(\S+)/;
|
|
811
|
+
function emulatorAvd(command) {
|
|
812
|
+
return EMULATOR.exec(command)?.[1] ?? null;
|
|
813
|
+
}
|
|
814
|
+
function workspaceOwner(kind, env, name, id) {
|
|
815
|
+
return {
|
|
816
|
+
kind,
|
|
817
|
+
name,
|
|
818
|
+
workspace: env.path,
|
|
819
|
+
id,
|
|
820
|
+
owned: true
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function rank(owner) {
|
|
824
|
+
return MACHINE_OWNER_KINDS.indexOf(owner.kind);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Attributes every process in `processes` to at most one owner: the owner of its nearest ancestor (itself included)
|
|
828
|
+
* that is a root. Roots are each booted simulator's `launchd_sim`, each emulator launcher and qemu process, and each
|
|
829
|
+
* workspace's verified supervisor and Metro, running build and Chrome. Only a process under none of those falls back
|
|
830
|
+
* to stim-server or a machine-wide service in `SHARED`, so a `simctl` a workspace's log collector runs stays with the
|
|
831
|
+
* workspace. A process with no root ancestor is left out.
|
|
832
|
+
*/
|
|
833
|
+
function attributeMachineUsage({ processes, environments, roots, simNames }) {
|
|
834
|
+
const owners = /* @__PURE__ */ new Map();
|
|
835
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
836
|
+
const fallbackOf = /* @__PURE__ */ new Map();
|
|
837
|
+
const claim = (pid, key, owner, into = rootOf) => {
|
|
838
|
+
if (!owners.has(key)) owners.set(key, owner);
|
|
839
|
+
into.set(pid, key);
|
|
840
|
+
};
|
|
841
|
+
const sims = /* @__PURE__ */ new Map();
|
|
842
|
+
const emulators = /* @__PURE__ */ new Map();
|
|
843
|
+
for (const env of environments) for (const device of [{
|
|
844
|
+
slot: "default",
|
|
845
|
+
ios: env.ios,
|
|
846
|
+
android: env.android
|
|
847
|
+
}, ...env.slots ?? []]) {
|
|
848
|
+
const slot = device.slot === "default" ? {} : { slot: device.slot };
|
|
849
|
+
if (device.ios?.state === "Booted") sims.set(device.ios.udid.toUpperCase(), {
|
|
850
|
+
kind: "simulator",
|
|
851
|
+
name: device.ios.name ?? device.ios.udid,
|
|
852
|
+
workspace: env.path,
|
|
853
|
+
...slot,
|
|
854
|
+
id: device.ios.udid,
|
|
855
|
+
owned: device.ios.owned
|
|
856
|
+
});
|
|
857
|
+
if (device.android?.name && !device.android.physical && device.android.serial) emulators.set(device.android.name, {
|
|
858
|
+
kind: "emulator",
|
|
859
|
+
name: device.android.name,
|
|
860
|
+
workspace: env.path,
|
|
861
|
+
...slot,
|
|
862
|
+
id: device.android.name,
|
|
863
|
+
owned: device.android.owned
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
for (const p of processes) {
|
|
867
|
+
const shared = SHARED.find(({ pattern }) => pattern.test(p.command));
|
|
868
|
+
if (shared) {
|
|
869
|
+
const owner = {
|
|
870
|
+
kind: "shared",
|
|
871
|
+
name: shared.name,
|
|
872
|
+
workspace: null,
|
|
873
|
+
id: null,
|
|
874
|
+
owned: false
|
|
875
|
+
};
|
|
876
|
+
claim(p.pid, `shared:${shared.name}`, owner, fallbackOf);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (SERVER.test(p.command)) {
|
|
880
|
+
claim(p.pid, "server", {
|
|
881
|
+
kind: "server",
|
|
882
|
+
name: "stim-server",
|
|
883
|
+
workspace: null,
|
|
884
|
+
id: null,
|
|
885
|
+
owned: false
|
|
886
|
+
}, fallbackOf);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const udid = LAUNCHD_SIM.exec(p.command)?.[1]?.toUpperCase();
|
|
890
|
+
if (udid) {
|
|
891
|
+
const owner = sims.get(udid) ?? {
|
|
892
|
+
kind: "simulator",
|
|
893
|
+
name: simNames[udid] ?? udid,
|
|
894
|
+
workspace: null,
|
|
895
|
+
id: udid,
|
|
896
|
+
owned: false
|
|
897
|
+
};
|
|
898
|
+
claim(p.pid, `simulator:${udid}`, owner);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const avd = emulatorAvd(p.command);
|
|
902
|
+
if (avd) {
|
|
903
|
+
const owner = emulators.get(avd) ?? {
|
|
904
|
+
kind: "emulator",
|
|
905
|
+
name: avd,
|
|
906
|
+
workspace: null,
|
|
907
|
+
id: avd,
|
|
908
|
+
owned: false
|
|
909
|
+
};
|
|
910
|
+
claim(p.pid, `emulator:${avd}`, owner);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const present = new Set(processes.map((p) => p.pid));
|
|
914
|
+
const byPath = new Map(environments.map((env) => [env.path, env]));
|
|
915
|
+
for (const root of roots) {
|
|
916
|
+
const env = byPath.get(root.path);
|
|
917
|
+
if (!env) continue;
|
|
918
|
+
const metroPids = [root.supervisorPid, env.metro?.running ? env.metro.pid : null];
|
|
919
|
+
for (const pid of metroPids) if (pid && present.has(pid)) {
|
|
920
|
+
const port = env.metro?.port ?? null;
|
|
921
|
+
claim(pid, `metro:${env.path}`, workspaceOwner("metro", env, port ? `Metro :${port}` : "Metro", port ? String(port) : null));
|
|
922
|
+
}
|
|
923
|
+
if (root.build && present.has(root.build.pid)) {
|
|
924
|
+
const label = root.build.platform === "ios" ? "iOS build" : "Android build";
|
|
925
|
+
claim(root.build.pid, `build:${env.path}`, workspaceOwner("build", env, label, root.build.platform));
|
|
926
|
+
}
|
|
927
|
+
for (const pid of root.browserPids) if (present.has(pid)) claim(pid, `browser:${env.path}`, workspaceOwner("browser", env, "Chrome", null));
|
|
928
|
+
}
|
|
929
|
+
const parentOf = new Map(processes.map((p) => [p.pid, p.ppid]));
|
|
930
|
+
const totals = /* @__PURE__ */ new Map();
|
|
931
|
+
for (const p of processes) {
|
|
932
|
+
let pid = p.pid;
|
|
933
|
+
let key;
|
|
934
|
+
let fallback;
|
|
935
|
+
for (let depth = 0; pid !== void 0 && pid > 1 && depth < MAX_DEPTH; depth++) {
|
|
936
|
+
key = rootOf.get(pid);
|
|
937
|
+
if (key) break;
|
|
938
|
+
fallback ??= fallbackOf.get(pid);
|
|
939
|
+
pid = parentOf.get(pid);
|
|
940
|
+
}
|
|
941
|
+
key ??= fallback;
|
|
942
|
+
if (!key) continue;
|
|
943
|
+
const total = totals.get(key) ?? {
|
|
944
|
+
rssKb: 0,
|
|
945
|
+
cpu: 0,
|
|
946
|
+
processes: 0
|
|
947
|
+
};
|
|
948
|
+
total.rssKb += p.rssKb;
|
|
949
|
+
total.cpu += p.cpuPercent;
|
|
950
|
+
total.processes += 1;
|
|
951
|
+
totals.set(key, total);
|
|
952
|
+
}
|
|
953
|
+
const list = [];
|
|
954
|
+
for (const [key, owner] of owners) {
|
|
955
|
+
const total = totals.get(key);
|
|
956
|
+
if (total) list.push({
|
|
957
|
+
...owner,
|
|
958
|
+
cpuPercent: Math.round(total.cpu),
|
|
959
|
+
residentMb: Math.round(total.rssKb / 1024),
|
|
960
|
+
processes: total.processes
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
list.sort((x, y) => rank(x) - rank(y) || (x.workspace ?? "~").localeCompare(y.workspace ?? "~") || x.name.localeCompare(y.name));
|
|
964
|
+
return { owners: list };
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
581
967
|
//#region src/commands/status.ts
|
|
582
|
-
const WATCH_GIT_MAX_AGE_MS =
|
|
968
|
+
const WATCH_GIT_MAX_AGE_MS = 6e4;
|
|
583
969
|
function formatGb(mb) {
|
|
584
970
|
return `${(mb / 1024).toFixed(1)} GB`;
|
|
585
971
|
}
|
|
586
972
|
function statusCommand(program) {
|
|
587
973
|
program.command("status").description("Show every environment on this machine: devices, ports, what is actually running, and anything stuck.").option("--json", "print the state as JSON").option("--watch", "keep running and print the state again each time it changes").action(async (opts) => {
|
|
588
974
|
if (opts.watch) return watchStatus(Boolean(opts.json));
|
|
589
|
-
|
|
975
|
+
const json = Boolean(opts.json);
|
|
976
|
+
for (const line of renderStatus(await readStatus(0), json)) console.log(line);
|
|
590
977
|
});
|
|
591
978
|
}
|
|
592
|
-
|
|
593
|
-
|
|
979
|
+
function readStatus(gitMaxAgeMs, simctlListing = null) {
|
|
980
|
+
return withStateReadCache(() => readStatusFacts(gitMaxAgeMs, simctlListing));
|
|
981
|
+
}
|
|
982
|
+
async function readStatusFacts(gitMaxAgeMs, simctlListing) {
|
|
594
983
|
const cfg = loadConfig$1();
|
|
595
984
|
const projects = Object.entries(cfg?.projects || {});
|
|
596
|
-
const cwdRoot =
|
|
985
|
+
const cwdRoot = findServerWorkspace(process.cwd())?.root ?? null;
|
|
597
986
|
const worktrees = linkedWorktrees([process.cwd(), ...projects.map(([path]) => path)]);
|
|
598
987
|
const orphanWorktrees = unprovisionedWorktrees(worktrees, projects.map(([p]) => p));
|
|
599
|
-
const simsRead = listAllIosSimsAsync();
|
|
988
|
+
const simsRead = simctlListing === null ? listAllIosSimsAsync() : Promise.resolve().then(() => parseSimctlList(simctlListing));
|
|
600
989
|
simsRead.catch(() => {});
|
|
601
990
|
const ports = await portLookup(projects.flatMap(([, proj]) => proj.metroPort ? [proj.metroPort] : []));
|
|
602
991
|
const processes = Promise.all(projects.map(async ([path, proj]) => {
|
|
@@ -611,7 +1000,9 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
611
1000
|
const androidRuntimeOf = androidRuntimeReader();
|
|
612
1001
|
const devices = projects.map(([, proj]) => ({
|
|
613
1002
|
androidRuntimes: Object.fromEntries(projectDeviceSlots(proj).slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned && platforms.android.avdName ? androidRuntimeOf(platforms.android.avdName) : null])),
|
|
614
|
-
androidRuntime: proj.platforms?.android?.owned && proj.platforms.android.avdName ? androidRuntimeOf(proj.platforms.android.avdName) : null
|
|
1003
|
+
androidRuntime: proj.platforms?.android?.owned && proj.platforms.android.avdName ? androidRuntimeOf(proj.platforms.android.avdName) : null,
|
|
1004
|
+
androidDeviceProfiles: Object.fromEntries(projectDeviceSlots(proj).slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned && platforms.android.avdName ? ownedAvdDeviceProfile(platforms.android.avdName) : null])),
|
|
1005
|
+
androidDeviceProfile: proj.platforms?.android?.owned && proj.platforms.android.avdName ? ownedAvdDeviceProfile(proj.platforms.android.avdName) : null
|
|
615
1006
|
}));
|
|
616
1007
|
const logs = projects.map(([path]) => logFacts(path));
|
|
617
1008
|
const simsByUdid = {};
|
|
@@ -628,11 +1019,33 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
628
1019
|
const history = readStats().record?.history;
|
|
629
1020
|
const states = [];
|
|
630
1021
|
const labelOnlyRoots = [];
|
|
1022
|
+
const launchesByState = [];
|
|
631
1023
|
const easLedger = readEasSessionLedger$1();
|
|
1024
|
+
const leaseNow = Date.now();
|
|
1025
|
+
const leaseFiles = listLeaseFiles();
|
|
1026
|
+
const leases = deviceLeaseStates(leaseFiles, {
|
|
1027
|
+
root: cwdRoot,
|
|
1028
|
+
now: leaseNow
|
|
1029
|
+
});
|
|
1030
|
+
const roots = [];
|
|
632
1031
|
for (const [i, [path, proj]] of projects.entries()) {
|
|
633
1032
|
const { metro, supervisor } = running[i];
|
|
634
1033
|
const saved = readWorkspaceState$1(path);
|
|
635
|
-
|
|
1034
|
+
const builds = workspaceBuilds(path, saved, history);
|
|
1035
|
+
const web = webFacts(readWebRecord(path));
|
|
1036
|
+
const activeBuild = parseActiveBuild(saved?.[ACTIVE_BUILD_KEY]);
|
|
1037
|
+
roots.push({
|
|
1038
|
+
path,
|
|
1039
|
+
supervisorPid: supervisor?.status === "ours" ? supervisor.pid : null,
|
|
1040
|
+
build: activeBuild && builds.build?.state === "running" ? {
|
|
1041
|
+
platform: activeBuild.platform,
|
|
1042
|
+
pid: activeBuild.claim.pid
|
|
1043
|
+
} : null,
|
|
1044
|
+
browserPids: browserPids(web)
|
|
1045
|
+
});
|
|
1046
|
+
const launches = readWorkspaceLaunches(path);
|
|
1047
|
+
launchesByState.push(launches);
|
|
1048
|
+
states.push(withWebFacts(environmentState({
|
|
636
1049
|
...proj,
|
|
637
1050
|
__path: path
|
|
638
1051
|
}, {
|
|
@@ -644,38 +1057,87 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
644
1057
|
supervisor,
|
|
645
1058
|
logs: logs[i],
|
|
646
1059
|
remote: remoteDeviceState(readRemoteSession(path), easLedger, path),
|
|
647
|
-
idleStop: readIdleStop(saved)
|
|
648
|
-
|
|
1060
|
+
idleStop: readIdleStop(saved),
|
|
1061
|
+
launches,
|
|
1062
|
+
leasedIds: new Set(deviceLeaseStates(leaseFiles, {
|
|
1063
|
+
root: path,
|
|
1064
|
+
now: leaseNow
|
|
1065
|
+
}).flatMap((lease) => lease.mine && !lease.expired && lease.id ? [lease.id] : [])),
|
|
1066
|
+
now: leaseNow
|
|
1067
|
+
}), web));
|
|
649
1068
|
const state = states[states.length - 1];
|
|
650
|
-
if (state) Object.assign(state,
|
|
651
|
-
labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android));
|
|
652
|
-
}
|
|
653
|
-
const readActivity = createActivityReader();
|
|
654
|
-
for (const state of states) for (const device of [{
|
|
655
|
-
slot: "default",
|
|
656
|
-
ios: state.ios,
|
|
657
|
-
android: state.android
|
|
658
|
-
}, ...state.slots ?? []]) {
|
|
659
|
-
const base = {
|
|
660
|
-
slot: device.slot,
|
|
661
|
-
workspace: state.path
|
|
662
|
-
};
|
|
663
|
-
if (device.ios?.state === "Booted") device.ios.activity = readActivity({
|
|
664
|
-
...base,
|
|
665
|
-
platform: "ios",
|
|
666
|
-
id: device.ios.udid
|
|
667
|
-
});
|
|
668
|
-
if (device.android && !device.android.physical && device.android.serial) device.android.activity = readActivity({
|
|
669
|
-
...base,
|
|
670
|
-
platform: "android",
|
|
671
|
-
id: device.android.serial
|
|
672
|
-
});
|
|
1069
|
+
if (state) Object.assign(state, builds);
|
|
1070
|
+
labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android && !state?.web));
|
|
673
1071
|
}
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1072
|
+
const tables = createDeviceProcessTables();
|
|
1073
|
+
readDeviceProcesses(states, tables, {
|
|
1074
|
+
projects,
|
|
1075
|
+
launchesByState
|
|
1076
|
+
});
|
|
1077
|
+
const sims = Object.values(simsByUdid);
|
|
1078
|
+
const simNames = Object.fromEntries(sims.map((sim) => [sim.udid.toUpperCase(), sim.name]));
|
|
1079
|
+
const machine = readMachineUsage({
|
|
1080
|
+
states,
|
|
1081
|
+
roots,
|
|
1082
|
+
simNames,
|
|
1083
|
+
tables,
|
|
1084
|
+
busy: sims.some((sim) => sim.state === "Booted")
|
|
1085
|
+
});
|
|
1086
|
+
return {
|
|
1087
|
+
projects,
|
|
1088
|
+
states,
|
|
1089
|
+
labelOnlyRoots,
|
|
1090
|
+
leases,
|
|
1091
|
+
leaseNow,
|
|
1092
|
+
orphanWorktrees,
|
|
1093
|
+
simsAvailable,
|
|
1094
|
+
simctlError,
|
|
1095
|
+
cwdRoot,
|
|
1096
|
+
tables,
|
|
1097
|
+
machine,
|
|
1098
|
+
roots,
|
|
1099
|
+
simNames
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function browserPids(web) {
|
|
1103
|
+
if (!web) return [];
|
|
1104
|
+
const chrome = web.record.chromeProcess?.pid;
|
|
1105
|
+
if (web.status === "running") return [web.record.pid, ...chrome ? [chrome] : []];
|
|
1106
|
+
return web.status === "orphaned" && chrome ? [chrome] : [];
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Attributes the host process table to its owners. Reads the table only when a simulator is booted, a workspace is
|
|
1110
|
+
* live or a build runs, so an idle machine runs no `ps`.
|
|
1111
|
+
*/
|
|
1112
|
+
function readMachineUsage({ states, roots, simNames, tables, busy }) {
|
|
1113
|
+
const processes = busy || states.some((state) => state.live || state.build?.state === "running") ? tables.host() : null;
|
|
1114
|
+
return processes ? attributeMachineUsage({
|
|
1115
|
+
processes,
|
|
1116
|
+
environments: states,
|
|
1117
|
+
roots,
|
|
1118
|
+
simNames
|
|
1119
|
+
}) : null;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Rereads only the error counts, device activity and machine usage. With `machine`, and when the snapshot measured
|
|
1123
|
+
* machine usage, it reads the host process table again; it reuses every other subprocess fact of the full read.
|
|
1124
|
+
*/
|
|
1125
|
+
function refreshLightFacts(snapshot, machine) {
|
|
1126
|
+
if (machine && snapshot.machine) snapshot.tables = {
|
|
1127
|
+
host: createDeviceProcessTables().host,
|
|
1128
|
+
android: snapshot.tables.android
|
|
1129
|
+
};
|
|
1130
|
+
withStateReadCache(() => {
|
|
1131
|
+
for (const state of snapshot.states) state.logs = logFacts(state.path);
|
|
1132
|
+
readDeviceProcesses(snapshot.states, snapshot.tables, null);
|
|
678
1133
|
});
|
|
1134
|
+
if (machine && snapshot.machine) snapshot.machine = readMachineUsage({
|
|
1135
|
+
...snapshot,
|
|
1136
|
+
busy: true
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orphanWorktrees, simsAvailable, simctlError, cwdRoot, machine }, json) {
|
|
1140
|
+
const out = [];
|
|
679
1141
|
const totalMemoryMb = Math.round(totalmem() / 1048576);
|
|
680
1142
|
const cap = capacity(states, totalMemoryMb);
|
|
681
1143
|
const pools = ["ios", "android"].map((platform) => {
|
|
@@ -698,7 +1160,8 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
698
1160
|
capacity: cap,
|
|
699
1161
|
deviceLeases: leases,
|
|
700
1162
|
unprovisionedWorktrees: orphanWorktrees,
|
|
701
|
-
simctlAvailable: simsAvailable
|
|
1163
|
+
simctlAvailable: simsAvailable,
|
|
1164
|
+
machine
|
|
702
1165
|
}));
|
|
703
1166
|
return out;
|
|
704
1167
|
}
|
|
@@ -719,7 +1182,7 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
719
1182
|
const marker = path === cwdRoot ? chalk.bold.cyan(`* ${shortcut}`) : shortcut;
|
|
720
1183
|
const idle = state.live ? "" : chalk.dim(" [idle]");
|
|
721
1184
|
out.push(`\n${marker}${idle} ${chalk.dim(`(${path})`)}`);
|
|
722
|
-
out.push(labelOnlyRoots[i] ? chalk.dim(" worktree root (holds the label; the app registers its own entry)") : chalk.dim(` app: ${proj.bundleId ?? "?"} (${proj.isExpo ? "expo" : "bare"})`));
|
|
1185
|
+
out.push(labelOnlyRoots[i] ? chalk.dim(" worktree root (holds the label; the app registers its own entry)") : chalk.dim(` app: ${proj.bundleId ?? "?"} (${proj.isExpo ?? detectIsExpo(path) ? "expo" : "bare"})`));
|
|
723
1186
|
if (state.metro) {
|
|
724
1187
|
const label = state.metro.running ? chalk.green(`running (pid ${state.metro.pid})`) : state.metro.idleStop ? chalk.dim(`stopped (idle) after ${state.metro.idleStop.idleMinutes}m; \`stim start\`, \`ios\` or \`android\` restarts it`) : chalk.dim("not running");
|
|
725
1188
|
out.push(` metro: port ${state.metro.port} ${label}`);
|
|
@@ -749,14 +1212,18 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
749
1212
|
if (deviceState.ios) {
|
|
750
1213
|
const booted = deviceState.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(deviceState.ios.state.toLowerCase());
|
|
751
1214
|
const owned = deviceState.ios.owned ? chalk.dim(" (owned)") : "";
|
|
752
|
-
out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}${activitySuffix(deviceState.ios.activity)}`);
|
|
1215
|
+
out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}${activitySuffix(deviceState.ios.activity)}${appSuffix(deviceState.ios.app)}`);
|
|
753
1216
|
}
|
|
754
1217
|
if (deviceState.android) {
|
|
755
1218
|
const kind = deviceState.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
|
|
756
1219
|
const observed = deviceState.android.state ? ` ${deviceState.android.state}${deviceState.android.serial ? ` (${deviceState.android.serial})` : ""}` : "";
|
|
757
|
-
out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}${activitySuffix(deviceState.android.activity)}`);
|
|
1220
|
+
out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}${activitySuffix(deviceState.android.activity)}${appSuffix(deviceState.android.app)}`);
|
|
758
1221
|
}
|
|
759
1222
|
}
|
|
1223
|
+
if (state.web) {
|
|
1224
|
+
const browser = state.web.running ? chalk.green(`running (pid ${state.web.pid}, DevTools ${state.web.cdpEndpoint})`) : chalk.dim("not running");
|
|
1225
|
+
out.push(` web: ${chalk.cyan(state.web.url)} ${browser}${state.web.headless ? "" : chalk.dim(" (headed)")}`);
|
|
1226
|
+
}
|
|
760
1227
|
for (const remote of state.remoteDevices ?? []) out.push(` ${remoteDeviceLine(remote)}`);
|
|
761
1228
|
for (const w of state.warnings) out.push(chalk.yellow(` ! ${w}`));
|
|
762
1229
|
}
|
|
@@ -788,13 +1255,17 @@ async function statusLines(json, gitMaxAgeMs) {
|
|
|
788
1255
|
}
|
|
789
1256
|
async function watchStatus(json) {
|
|
790
1257
|
let last = null;
|
|
1258
|
+
let snapshot = null;
|
|
791
1259
|
let sources = null;
|
|
792
1260
|
const scheduler = createRefreshScheduler({
|
|
793
1261
|
debounceMs: 250,
|
|
794
|
-
|
|
1262
|
+
lightIntervalMs: WATCH_LIGHT_INTERVAL_MS,
|
|
1263
|
+
run: async (kind) => {
|
|
795
1264
|
let text;
|
|
796
1265
|
try {
|
|
797
|
-
|
|
1266
|
+
if (kind === "light" && snapshot) refreshLightFacts(snapshot, json);
|
|
1267
|
+
else snapshot = await readStatus(WATCH_GIT_MAX_AGE_MS, sources?.simulatorListing());
|
|
1268
|
+
text = renderStatus(snapshot, json).join("\n");
|
|
798
1269
|
} catch (error) {
|
|
799
1270
|
console.error(chalk.red(String(error?.message || error)));
|
|
800
1271
|
return;
|
|
@@ -806,9 +1277,11 @@ async function watchStatus(json) {
|
|
|
806
1277
|
process.stdout.write(`${!json && process.stdout.isTTY ? "\x1B[2J\x1B[H" : ""}${text}\n`);
|
|
807
1278
|
}
|
|
808
1279
|
});
|
|
809
|
-
const fallback = setInterval(() => scheduler.trigger(), WATCH_FALLBACK_MS);
|
|
1280
|
+
const fallback = setInterval(() => scheduler.trigger("full"), WATCH_FALLBACK_MS);
|
|
1281
|
+
const machine = setInterval(() => json && snapshot?.machine && scheduler.trigger("light"), WATCH_LIGHT_INTERVAL_MS);
|
|
810
1282
|
const finish = () => {
|
|
811
1283
|
clearInterval(fallback);
|
|
1284
|
+
clearInterval(machine);
|
|
812
1285
|
scheduler.stop();
|
|
813
1286
|
sources?.stop();
|
|
814
1287
|
process.exit(0);
|
|
@@ -818,9 +1291,9 @@ async function watchStatus(json) {
|
|
|
818
1291
|
process.stdout.on("error", finish);
|
|
819
1292
|
sources = watchStatusSources({
|
|
820
1293
|
home: getConfigDir(),
|
|
821
|
-
onChange: () => scheduler.trigger()
|
|
1294
|
+
onChange: (kind) => scheduler.trigger(kind)
|
|
822
1295
|
});
|
|
823
|
-
scheduler.trigger(0);
|
|
1296
|
+
scheduler.trigger("full", 0);
|
|
824
1297
|
await new Promise(() => {});
|
|
825
1298
|
}
|
|
826
1299
|
async function readGitInto(worktrees, orphans, maxAgeMs) {
|
|
@@ -847,6 +1320,62 @@ function activitySuffix(activity) {
|
|
|
847
1320
|
const label = activityLabel(activity, Date.now());
|
|
848
1321
|
return label ? ` -- ${activity?.state === "driven" ? chalk.magenta(label) : chalk.dim(label)}` : "";
|
|
849
1322
|
}
|
|
1323
|
+
function readDeviceProcesses(states, tables, apps) {
|
|
1324
|
+
const readActivity = createActivityReader({ tables });
|
|
1325
|
+
const readAppProcess = createAppProcessReader(tables);
|
|
1326
|
+
for (const [i, state] of states.entries()) {
|
|
1327
|
+
const appIdOn = (platform, slot, deviceId) => {
|
|
1328
|
+
if (!apps) return void 0;
|
|
1329
|
+
const project = apps.projects[i][1];
|
|
1330
|
+
const launch = apps.launchesByState[i][deviceSlotKey(platform, slot)];
|
|
1331
|
+
return launch?.deviceId === deviceId ? launch.appId : platform === "ios" ? project.bundleId : project.androidPackage;
|
|
1332
|
+
};
|
|
1333
|
+
for (const device of [{
|
|
1334
|
+
slot: "default",
|
|
1335
|
+
ios: state.ios,
|
|
1336
|
+
android: state.android
|
|
1337
|
+
}, ...state.slots ?? []]) {
|
|
1338
|
+
const base = {
|
|
1339
|
+
slot: device.slot,
|
|
1340
|
+
workspace: state.path
|
|
1341
|
+
};
|
|
1342
|
+
if (device.ios?.state === "Booted") {
|
|
1343
|
+
const id = device.ios.udid;
|
|
1344
|
+
device.ios.activity = statusActivity(readActivity({
|
|
1345
|
+
...base,
|
|
1346
|
+
platform: "ios",
|
|
1347
|
+
id
|
|
1348
|
+
}));
|
|
1349
|
+
const appId = device.ios.owned ? appIdOn("ios", device.slot, id) : void 0;
|
|
1350
|
+
if (appId) device.ios.app = readAppProcess({
|
|
1351
|
+
platform: "ios",
|
|
1352
|
+
id,
|
|
1353
|
+
appId
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
if (device.android && !device.android.physical && device.android.serial) {
|
|
1357
|
+
const id = device.android.serial;
|
|
1358
|
+
device.android.activity = statusActivity(readActivity({
|
|
1359
|
+
...base,
|
|
1360
|
+
platform: "android",
|
|
1361
|
+
id
|
|
1362
|
+
}));
|
|
1363
|
+
const appId = device.android.owned ? appIdOn("android", device.slot, id) : void 0;
|
|
1364
|
+
if (appId) device.android.app = readAppProcess({
|
|
1365
|
+
platform: "android",
|
|
1366
|
+
id,
|
|
1367
|
+
appId
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function appSuffix(app) {
|
|
1374
|
+
if (!app) return "";
|
|
1375
|
+
if (app.state === "running") return chalk.dim(` -- ${app.id} running`);
|
|
1376
|
+
if (app.state === "stopped") return ` -- ${chalk.yellow(`${app.id} not running`)}`;
|
|
1377
|
+
return chalk.dim(` -- ${app.id} process unknown`);
|
|
1378
|
+
}
|
|
850
1379
|
function linkedWorktrees(paths) {
|
|
851
1380
|
return [...new Set(paths.flatMap((path) => gitCommonDirOnDisk(path) ?? []))].flatMap((common) => {
|
|
852
1381
|
const repository = basename(common) === ".git" ? dirname(common) : common;
|
|
@@ -855,9 +1384,11 @@ function linkedWorktrees(paths) {
|
|
|
855
1384
|
}
|
|
856
1385
|
function workspaceBuilds(path, saved, history) {
|
|
857
1386
|
const lastBuilds = readLastBuilds(saved);
|
|
1387
|
+
const builds = readBuildHistory(saved);
|
|
858
1388
|
return {
|
|
859
1389
|
build: workspaceBuild(path, saved, history),
|
|
860
|
-
...lastBuilds.ios || lastBuilds.android ? { lastBuilds } : {}
|
|
1390
|
+
...lastBuilds.ios || lastBuilds.android ? { lastBuilds } : {},
|
|
1391
|
+
...builds.ios || builds.android ? { builds } : {}
|
|
861
1392
|
};
|
|
862
1393
|
}
|
|
863
1394
|
function workspaceBuild(path, saved, history) {
|
|
@@ -932,13 +1463,14 @@ async function supervisorFacts(path, proj, metroResolution, lookup) {
|
|
|
932
1463
|
const pid = state?.pid ?? record?.pid ?? null;
|
|
933
1464
|
if (!pid) return null;
|
|
934
1465
|
const port = state?.port ?? record?.port ?? null;
|
|
935
|
-
const
|
|
1466
|
+
const target = resolveSupervisorTarget({
|
|
936
1467
|
state,
|
|
937
1468
|
record,
|
|
938
1469
|
reservedPort: proj?.metroPort
|
|
939
|
-
})
|
|
1470
|
+
});
|
|
1471
|
+
const status = target.status === "ours" ? "ours" : target.status === "unverified" ? "unverified" : "stale";
|
|
940
1472
|
let healthy = false;
|
|
941
|
-
if (
|
|
1473
|
+
if (status === "ours" && port) {
|
|
942
1474
|
const resolution = port === proj?.metroPort && metroResolution ? metroResolution : await resolveOnPort(port, path, lookup);
|
|
943
1475
|
healthy = Boolean(resolution?.metro);
|
|
944
1476
|
}
|
|
@@ -946,7 +1478,8 @@ async function supervisorFacts(path, proj, metroResolution, lookup) {
|
|
|
946
1478
|
pid,
|
|
947
1479
|
mode: state?.mode ?? record?.mode ?? null,
|
|
948
1480
|
startedAt: state?.startedAt ?? record?.startedAt ?? null,
|
|
949
|
-
|
|
1481
|
+
status,
|
|
1482
|
+
...target.reason ? { reason: target.reason } : {},
|
|
950
1483
|
healthy
|
|
951
1484
|
};
|
|
952
1485
|
}
|