@stim-cli/core 1.25.0 → 1.27.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/state.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { S as tryAcquireClaim, _ as releaseClaim, f as isClaimRefusal, g as readClaimSet } from "./ownership-claim-DG2GMIoo.mjs";
2
2
  import { inspectProcessIdentity } from "./process-identity.mjs";
3
- import { $ as withDirLockAsync, C as workspaceName, J as readJsonFile, Q as withDirLock, T as workspaceStateDir, U as ambientContext, W as ambientEnv, X as readStateFile, Y as readJsonObject, Z as withStateReadCache, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as isJsonObject, t as LOG_ROTATE_BYTES, u as configDir } from "./core-Cd3b_fBm.mjs";
3
+ import { $ as withDirLockAsync, C as workspaceName, J as readJsonFile, Q as withDirLock, T as workspaceStateDir, U as ambientContext, W as ambientEnv, X as readStateFile, Y as readJsonObject, Z as withStateReadCache, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as isJsonObject, t as LOG_ROTATE_BYTES, u as configDir } from "./core-B_w6CiKQ.mjs";
4
4
  import { readOrCreateKeyFile } from "./noise-session.mjs";
5
5
  import { validHomeId } from "./workspace-link.mjs";
6
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";
7
+ import { appendFileSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, watch, writeFileSync, writeSync } from "node:fs";
8
8
  import { availableParallelism, homedir, loadavg } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
10
10
  import { createHash, randomBytes } from "node:crypto";
@@ -645,6 +645,10 @@ function supervisorPidFile(projectRoot) {
645
645
  function workspaceStateFile(projectRoot) {
646
646
  return join(workspaceStateDir(projectRoot), "state.json");
647
647
  }
648
+ /** The dependency directories `gc --delete` deleted from the workspace's worktree, apart from state.json so writing it is not workspace activity. */
649
+ function workspaceLightenedFile(projectRoot) {
650
+ return join(workspaceStateDir(projectRoot), "lightened.json");
651
+ }
648
652
  function workspaceStateLock(projectRoot) {
649
653
  return join(workspaceStateDir(projectRoot), "state.lock");
650
654
  }
@@ -719,6 +723,12 @@ function easSessionLedgerLock(root = easMachineStateRoot()) {
719
723
  function deviceViewersDir() {
720
724
  return join(configDir(), "viewers");
721
725
  }
726
+ function budgetSweepFile() {
727
+ return join(configDir(), "budget-sweep.json");
728
+ }
729
+ function budgetSweepLock() {
730
+ return join(configDir(), "budget-sweep.lock");
731
+ }
722
732
  function statsFile() {
723
733
  return join(configDir(), "stats.json");
724
734
  }
@@ -734,6 +744,21 @@ function maintenanceAttemptFile() {
734
744
  function maintenanceNdjsonFile() {
735
745
  return join(maintenanceDir(), "maintenance.ndjson");
736
746
  }
747
+ function gcLogDir() {
748
+ return join(configDir(), "gc");
749
+ }
750
+ function gcLogFile() {
751
+ return join(gcLogDir(), "gc.ndjson");
752
+ }
753
+ function buildLedgerDir() {
754
+ return join(configDir(), "builds");
755
+ }
756
+ function buildLedgerFile() {
757
+ return join(buildLedgerDir(), "builds.ndjson");
758
+ }
759
+ function buildLedgerLock() {
760
+ return join(buildLedgerDir(), "builds.lock");
761
+ }
737
762
  function maintenanceChildLogFile() {
738
763
  return join(maintenanceDir(), "child.log");
739
764
  }
@@ -753,6 +778,10 @@ function archiveDir(id) {
753
778
  function archiveLock() {
754
779
  return join(archiveRoot(), ".lock");
755
780
  }
781
+ /** Uncommitted changes of removed worktrees: `<repository>/<branch-or-dir>-<timestamp>/` holding `manifest.json`. */
782
+ function worktreeArchiveRoot() {
783
+ return join(archiveRoot(), "worktrees");
784
+ }
756
785
  /** This machine's own long-term X25519 key as a client of other machines' stim-servers. */
757
786
  function clientIdentityKeyFile() {
758
787
  return join(configDir(), "client-identity.key");
@@ -942,6 +971,13 @@ function allMetroPorts() {
942
971
  //#region state/tailnet.ts
943
972
  const DEFAULT_SERVE_PORT = 7443;
944
973
  const TAILNET_MACHINE_PATTERN = "^[A-Za-z0-9][A-Za-z0-9.-]*(?::(?!0+$)(?:[0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$";
974
+ function peerPresence(peer) {
975
+ const lastSeen = typeof peer.LastSeen === "string" ? peer.LastSeen : "";
976
+ return {
977
+ ...typeof peer.Online === "boolean" ? { online: peer.Online } : {},
978
+ ...lastSeen && Date.parse(lastSeen) > 0 ? { lastSeen } : {}
979
+ };
980
+ }
945
981
  function parseMachine(entry) {
946
982
  if (!(/* @__PURE__ */ new RegExp("^[A-Za-z0-9][A-Za-z0-9.-]*(?::(?!0+$)(?:[0-5]?[0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$")).test(entry.trim())) return null;
947
983
  const [name, port] = entry.trim().split(":");
@@ -965,7 +1001,8 @@ function findPeer(status, name) {
965
1001
  nodeId: peer.ID,
966
1002
  dnsName,
967
1003
  address,
968
- ...typeof peer.OS === "string" && peer.OS ? { os: peer.OS } : {}
1004
+ ...typeof peer.OS === "string" && peer.OS ? { os: peer.OS } : {},
1005
+ ...peerPresence(peer)
969
1006
  }];
970
1007
  });
971
1008
  if (found.length === 0) return "missing";
@@ -1152,11 +1189,20 @@ const IOS_SIMULATOR_APPS = [
1152
1189
  "stim-desktop"
1153
1190
  ];
1154
1191
  const ANDROID_EMULATOR_APPS = ["emulator", "stim-desktop"];
1155
- const OFFLOAD_MODES = [
1156
- "auto",
1157
- "force",
1158
- "off"
1159
- ];
1192
+ const REPLACED_SETTINGS = { "remote.buildMode": {
1193
+ key: "remote.build",
1194
+ env: "STIM_REMOTE_BUILD_MODE",
1195
+ value: (old) => old === "auto" ? "auto" : old === "force" ? "remote" : old === "off" ? "local" : null
1196
+ } };
1197
+ function replacementAdvice(key, old) {
1198
+ const replaced = REPLACED_SETTINGS[key];
1199
+ if (!replaced) return null;
1200
+ const value = replaced.value(old);
1201
+ return {
1202
+ key: replaced.key,
1203
+ command: value ? `stim settings set ${replaced.key} ${value}` : null
1204
+ };
1205
+ }
1160
1206
  const WEB_VIEWPORTS = ["desktop", "phone"];
1161
1207
  const PROJECT = [
1162
1208
  "workspace",
@@ -1274,10 +1320,10 @@ const SETTINGS = [
1274
1320
  type: {
1275
1321
  kind: "string",
1276
1322
  pattern: TAILNET_MACHINE_PATTERN,
1277
- patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
1323
+ patternHelp: "eas, proxy, local, auto, remote, remote-only, or a tailnet machine name"
1278
1324
  },
1279
1325
  scopes: EVERY,
1280
- 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"
1326
+ description: "Where the iOS simulator runs: local here (overriding a lower layer); auto here first, then a remote.machines entry when this machine is full or busy; remote on a remote.machines entry in order, here only when none takes it; remote-only the same but never here; a machine name exactly that machine; eas or proxy a remote device backend"
1281
1327
  },
1282
1328
  {
1283
1329
  key: "ios.projectPath",
@@ -1453,10 +1499,10 @@ const SETTINGS = [
1453
1499
  type: {
1454
1500
  kind: "string",
1455
1501
  pattern: TAILNET_MACHINE_PATTERN,
1456
- patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
1502
+ patternHelp: "eas, proxy, local, auto, remote, remote-only, or a tailnet machine name"
1457
1503
  },
1458
1504
  scopes: EVERY,
1459
- 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"
1505
+ description: "Where the Android emulator runs: local here (overriding a lower layer); auto here first, then a remote.machines entry when this machine is full or busy; remote on a remote.machines entry in order, here only when none takes it; remote-only the same but never here; a machine name exactly that machine; eas or proxy a remote device backend"
1460
1506
  },
1461
1507
  {
1462
1508
  key: "metro.tunnel",
@@ -1530,7 +1576,7 @@ const SETTINGS = [
1530
1576
  },
1531
1577
  scopes: EVERY,
1532
1578
  default: 30,
1533
- description: "Minutes an owned simulator or emulator stays idle before the workspace supervisor shuts it down; 0 never"
1579
+ description: "Minutes an owned simulator or emulator stays idle before the workspace supervisor, or automatic maintenance when no supervisor runs, shuts it down; 0 never"
1534
1580
  },
1535
1581
  {
1536
1582
  key: "devices.reclaimIdleMinutes",
@@ -1571,6 +1617,16 @@ const SETTINGS = [
1571
1617
  scopes: PROJECT,
1572
1618
  description: "Arguments passed directly to the owned macOS development executable."
1573
1619
  },
1620
+ {
1621
+ key: "macos.remote",
1622
+ type: {
1623
+ kind: "string",
1624
+ pattern: `^(?![Ee][Aa][Ss]$|[Pp][Rr][Oo][Xx][Yy]$)${TAILNET_MACHINE_PATTERN.slice(1)}`,
1625
+ patternHelp: "local, auto, remote, remote-only, or a tailnet machine name (macOS has no eas or proxy backend)"
1626
+ },
1627
+ scopes: EVERY,
1628
+ description: "Where stim macos launches the app: local here; auto here unless this machine is busy, then a remote.machines entry; remote on a remote.machines entry in order, here only when none takes it; remote-only the same but never here; a machine name exactly that machine. The --remote flag overrides it"
1629
+ },
1574
1630
  {
1575
1631
  key: "macos.assetCatalog",
1576
1632
  type: RELATIVE_PATH,
@@ -1625,7 +1681,7 @@ const SETTINGS = [
1625
1681
  default: true,
1626
1682
  env: "STIM_ARCHIVE_ENABLED",
1627
1683
  scopedHomeValue: false,
1628
- description: "Keep workspace history on removal; false under STIM_HOME unless explicitly enabled"
1684
+ description: "Keep workspace history on removal, and archive a dirty finished worktree's changes before gc removes it; false keeps such worktrees, and is the default under STIM_HOME unless explicitly enabled"
1629
1685
  },
1630
1686
  {
1631
1687
  key: "archive.maxAgeDays",
@@ -1633,7 +1689,7 @@ const SETTINGS = [
1633
1689
  scopes: MACHINE,
1634
1690
  default: 30,
1635
1691
  env: "STIM_ARCHIVE_MAX_AGE_DAYS",
1636
- description: "Days to keep archived workspace records; 0 keeps nothing"
1692
+ description: "Days to keep archived workspace records and worktree change archives; 0 keeps nothing"
1637
1693
  },
1638
1694
  {
1639
1695
  key: "archive.maxCount",
@@ -1649,7 +1705,7 @@ const SETTINGS = [
1649
1705
  scopes: MACHINE,
1650
1706
  default: 5,
1651
1707
  env: "STIM_ARCHIVE_MAX_TOTAL_GB",
1652
- description: "Total archived bytes in binary GB; 0 keeps no artifacts"
1708
+ description: "Total archived bytes in binary GB, and separately the worktree change archives; 0 keeps no artifacts"
1653
1709
  },
1654
1710
  {
1655
1711
  key: "archive.logs.maxAgeDays",
@@ -1857,7 +1913,7 @@ const SETTINGS = [
1857
1913
  scopes: MACHINE,
1858
1914
  default: 1,
1859
1915
  env: "STIM_MAINTENANCE_LOG_MAX_MB",
1860
- description: "Machine maintenance log rotation size in MiB"
1916
+ description: "Machine maintenance and gc log rotation size in MiB"
1861
1917
  },
1862
1918
  {
1863
1919
  key: "maintenance.logRetentionDays",
@@ -1869,7 +1925,7 @@ const SETTINGS = [
1869
1925
  scopes: MACHINE,
1870
1926
  default: 30,
1871
1927
  env: "STIM_MAINTENANCE_LOG_RETENTION_DAYS",
1872
- description: "Retention of the rotated machine maintenance log in days"
1928
+ description: "Retention of the rotated machine maintenance and gc logs in days"
1873
1929
  },
1874
1930
  {
1875
1931
  key: "maintenance.logChecks",
@@ -1941,6 +1997,18 @@ const SETTINGS = [
1941
1997
  env: "STIM_MAINTENANCE_WORKTREE_CHECK_MINUTES",
1942
1998
  description: "Finished-worktree check interval in minutes"
1943
1999
  },
2000
+ {
2001
+ key: "maintenance.idleCheckMinutes",
2002
+ type: {
2003
+ kind: "number",
2004
+ integer: true,
2005
+ minimum: 1
2006
+ },
2007
+ scopes: MACHINE,
2008
+ default: 5,
2009
+ env: "STIM_MAINTENANCE_IDLE_CHECK_MINUTES",
2010
+ description: "Interval in minutes of the check that shuts down owned simulators and emulators idle for devices.idleShutdownMinutes, also with no dev server running"
2011
+ },
1944
2012
  {
1945
2013
  key: "maintenance.sweepHours",
1946
2014
  type: {
@@ -1958,12 +2026,12 @@ const SETTINGS = [
1958
2026
  type: {
1959
2027
  kind: "number",
1960
2028
  integer: true,
1961
- minimum: 1
2029
+ minimum: 0
1962
2030
  },
1963
2031
  scopes: MACHINE,
1964
2032
  default: 7,
1965
2033
  env: "STIM_MAINTENANCE_OLDER_THAN_DAYS",
1966
- description: "Age in days for the age sweep: unused workspace build outputs and shared cache entries"
2034
+ description: "Days unused before the age sweep removes it: workspace build outputs, shared cache entries, parked simulators and emulators, and, with maintenance.removeIdleWorktrees, clean pushed linked worktrees; 0 turns those age removals off"
1967
2035
  },
1968
2036
  {
1969
2037
  key: "maintenance.deviceIdleDays",
@@ -2015,7 +2083,7 @@ const SETTINGS = [
2015
2083
  scopes: MACHINE,
2016
2084
  default: 10,
2017
2085
  env: "STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTES",
2018
- description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3"
2086
+ description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3, except for workspace build outputs and build directories, which level 2 keeps for 15 minutes after the last ios, android or macos run"
2019
2087
  },
2020
2088
  {
2021
2089
  key: "maintenance.gradleVersionIdleDays",
@@ -2035,7 +2103,15 @@ const SETTINGS = [
2035
2103
  scopes: MACHINE,
2036
2104
  default: true,
2037
2105
  env: "STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREES",
2038
- 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"
2106
+ description: "Remove linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes, archiving a dirty one's uncommitted files first, and clean, pushed ones with no commits of their own once idle"
2107
+ },
2108
+ {
2109
+ key: "maintenance.removeIdleWorktrees",
2110
+ type: BOOLEAN,
2111
+ scopes: MACHINE,
2112
+ default: false,
2113
+ env: "STIM_MAINTENANCE_REMOVE_IDLE_WORKTREES",
2114
+ description: "In the age sweep, also remove clean, pushed linked worktrees whose branch is not merged once no Stim command has used them for maintenance.olderThanDays, with their git-ignored files; needs maintenance.removeFinishedWorktrees"
2039
2115
  },
2040
2116
  {
2041
2117
  key: "maintenance.keep",
@@ -2168,7 +2244,7 @@ const SETTINGS = [
2168
2244
  key: "remote.machines",
2169
2245
  type: { kind: "strings" },
2170
2246
  scopes: MACHINE,
2171
- 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"
2247
+ description: "Tailscale names of the remote machines this machine may build on and host owned simulators, emulators and macOS apps on, each optionally with :<port> of its tailscale serve route (default 7443). The order is the placement priority: each workload goes to the first machine with room and a load under its own limit. What each is used for follows the build and device-host approvals it grants, which doctor --fix asks for separately"
2172
2248
  },
2173
2249
  {
2174
2250
  key: "remote.buildPoolDisabled",
@@ -2189,7 +2265,7 @@ const SETTINGS = [
2189
2265
  type: BOOLEAN,
2190
2266
  scopes: EVERY,
2191
2267
  default: false,
2192
- 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"
2268
+ description: "When ios.remote or android.remote is auto or remote, 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; remote-only never uses it"
2193
2269
  },
2194
2270
  {
2195
2271
  key: "remote.easMaxDurationMinutes",
@@ -2207,23 +2283,12 @@ const SETTINGS = [
2207
2283
  type: {
2208
2284
  kind: "string",
2209
2285
  pattern: TAILNET_MACHINE_PATTERN,
2210
- patternHelp: "auto, local, or a tailnet machine name"
2286
+ patternHelp: "local, auto, remote, remote-only, or a tailnet machine name"
2211
2287
  },
2212
2288
  scopes: MACHINE,
2213
2289
  default: "auto",
2214
2290
  env: "STIM_REMOTE_BUILD",
2215
- 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"
2216
- },
2217
- {
2218
- key: "remote.buildMode",
2219
- type: {
2220
- kind: "choice",
2221
- choices: OFFLOAD_MODES
2222
- },
2223
- scopes: MACHINE,
2224
- default: "auto",
2225
- env: "STIM_REMOTE_BUILD_MODE",
2226
- 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"
2291
+ description: "Where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: local here; auto here while it has a free concurrency.maxBuilds slot and load under server.maxLoadPerCore, otherwise on a remote.machines entry; remote on a remote.machines entry in order, here only when none takes the build; remote-only the same but never here; a machine name exactly that machine without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override"
2227
2292
  },
2228
2293
  {
2229
2294
  key: "server.maxLoadPerCore",
@@ -2233,7 +2298,7 @@ const SETTINGS = [
2233
2298
  },
2234
2299
  scopes: MACHINE,
2235
2300
  default: 2,
2236
- 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"
2301
+ 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, reports the limit in its device offers so other machines skip it like a full one, stops preferring itself for automatic placement, and automatic iOS, Android and macOS placement treats it as busy"
2237
2302
  },
2238
2303
  {
2239
2304
  key: "server.workerRoot",
@@ -2664,6 +2729,282 @@ function createDebugLog(component, { env, now = Date.now, base = () => ({}) } =
2664
2729
  };
2665
2730
  }
2666
2731
  //#endregion
2732
+ //#region state/trace.ts
2733
+ const TRACE_ENV = "STIM_TRACE";
2734
+ const OWNER_ENV = "STIM_TRACE_OWNER";
2735
+ const PHASE_TID_BASE = 1e3;
2736
+ const MAX_ARGS = 60;
2737
+ const MAX_ARG_CHARS = 240;
2738
+ const MAX_LINE_CHARS = 500;
2739
+ const SCAN_ARG_CHARS = 1e3;
2740
+ const SCAN_LINE_CHARS = 2e3;
2741
+ const SECRET_WORDS = "token|secret|ticket|pass|pwd|otp|credential|cookie|api[-_]?key|access[-_]?key|private[-_]?key|auth";
2742
+ const SECRET_NAME = new RegExp(SECRET_WORDS, "i");
2743
+ const SECRET_FLAG = String.raw`--?[A-Za-z0-9_.-]*(?:${SECRET_WORDS})[A-Za-z0-9_.-]*`;
2744
+ const SECRET_FLAG_VALUE = new RegExp(String.raw`(${SECRET_FLAG})(=|\s+)(?:'[^']*'|"[^"]*"|\S+)`, "gi");
2745
+ const SECRET_ASSIGNMENT = new RegExp(String.raw`([A-Za-z0-9_]*(?:${SECRET_WORDS})[A-Za-z0-9_]*)=(?:'[^']*'|"[^"]*"|[^\s&]+)`, "gi");
2746
+ const URL_USERINFO = /(\/\/)[^/\s?#]*@/g;
2747
+ const SECRET_HEADER = new RegExp(String.raw`(\b[A-Za-z-]*(?:${SECRET_WORDS})[A-Za-z-]*:\s*)[^'"\r\n]+`, "gi");
2748
+ const AUTH_SCHEME = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
2749
+ const TOKEN_SHAPE = /\b(?:(?:gh[pousr]_|github_pat_|sk-|[sr]k_(?:live|test)_|xox[abprs]-|AKIA|AIza|glpat-)[A-Za-z0-9_-]{10,}|npm_[A-Za-z0-9]{36}|eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*)/g;
2750
+ const CREDENTIAL_FLAGS = {
2751
+ curl: { "": [
2752
+ "-u",
2753
+ "-U",
2754
+ "--user",
2755
+ "--proxy-user"
2756
+ ] },
2757
+ security: {
2758
+ "unlock-keychain": ["-p"],
2759
+ "create-keychain": ["-p"],
2760
+ import: ["-P"],
2761
+ export: ["-P"]
2762
+ }
2763
+ };
2764
+ function credentialFlagsOf(program, subcommand) {
2765
+ const name = basename(program);
2766
+ const flags = Object.hasOwn(CREDENTIAL_FLAGS, name) ? CREDENTIAL_FLAGS[name] : void 0;
2767
+ const key = flags && Object.hasOwn(flags, "") ? "" : subcommand ?? "";
2768
+ return new Set(flags && Object.hasOwn(flags, key) ? flags[key] : []);
2769
+ }
2770
+ function scrubText(text, exact) {
2771
+ return exact.filter(Boolean).reduce((acc, value) => acc.split(value).join("***"), text).replace(URL_USERINFO, "$1[redacted]@").replace(SECRET_ASSIGNMENT, "$1=[redacted]").replace(SECRET_HEADER, "$1[redacted]").replace(AUTH_SCHEME, "$1 [redacted]").replace(TOKEN_SHAPE, "[redacted]");
2772
+ }
2773
+ /**
2774
+ * `args` with the value of any flag or assignment that looks like a secret, credentials in URLs, token shapes and
2775
+ * the `exact` values replaced. `program` selects the flags that carry a credential without a secret-looking name,
2776
+ * such as `curl -u`.
2777
+ */
2778
+ function redactArgv(args, exact = [], program = "") {
2779
+ const out = [];
2780
+ const credentialFlags = credentialFlagsOf(program, args[0]);
2781
+ let redactNext = false;
2782
+ for (const arg of args.slice(0, MAX_ARGS)) {
2783
+ const flag = /^(--?[A-Za-z0-9_.-]+)=/.exec(arg)?.[1];
2784
+ const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(arg)?.[1];
2785
+ const attached = arg.length > 2 && credentialFlags.has(arg.slice(0, 2)) ? arg.slice(0, 2) : null;
2786
+ if (redactNext) {
2787
+ out.push("[redacted]");
2788
+ redactNext = false;
2789
+ } else if (credentialFlags.has(arg)) {
2790
+ out.push(arg);
2791
+ redactNext = true;
2792
+ } else if (attached) out.push(`${attached}[redacted]`);
2793
+ else if (flag) out.push(SECRET_NAME.test(flag) || credentialFlags.has(flag) ? `${flag}=[redacted]` : clip(scrubText(arg.slice(0, SCAN_ARG_CHARS), exact)));
2794
+ else if (/^--?[A-Za-z0-9_.-]+$/.test(arg) && SECRET_NAME.test(arg)) {
2795
+ out.push(arg);
2796
+ redactNext = true;
2797
+ } else if (assignment && SECRET_NAME.test(assignment)) out.push(`${assignment}=[redacted]`);
2798
+ else if (/\s/.test(arg)) out.push(redactCommandLine(arg, exact));
2799
+ else out.push(clip(scrubText(arg.slice(0, SCAN_ARG_CHARS), exact)));
2800
+ }
2801
+ if (args.length > MAX_ARGS) out.push(`[+${args.length - MAX_ARGS} more]`);
2802
+ return out;
2803
+ }
2804
+ function clip(text, max = MAX_ARG_CHARS) {
2805
+ return text.length > max ? `${text.slice(0, max)}...` : text;
2806
+ }
2807
+ /** A shell command line with the same redactions as `redactArgv`, clipped to a fixed length. */
2808
+ function redactCommandLine(command, exact = []) {
2809
+ return clip(scrubText(command.slice(0, SCAN_LINE_CHARS).replace(SECRET_FLAG_VALUE, "$1$2[redacted]"), exact), MAX_LINE_CHARS);
2810
+ }
2811
+ const microseconds = () => Math.round((performance.timeOrigin + performance.now()) * 1e3);
2812
+ let current$1 = null;
2813
+ let attempted = null;
2814
+ function emit(sink, event) {
2815
+ try {
2816
+ const line = Buffer.from(`${JSON.stringify({
2817
+ ...event,
2818
+ pid: process.pid
2819
+ })},\n`);
2820
+ for (let written = 0; written < line.length;) written += writeSync(sink.fd, line, written);
2821
+ } catch {
2822
+ sink.fd = -1;
2823
+ }
2824
+ }
2825
+ function nameThread(sink, tid, name) {
2826
+ if (sink.named.has(tid)) return;
2827
+ sink.named.add(tid);
2828
+ emit(sink, {
2829
+ ph: "M",
2830
+ name: "thread_name",
2831
+ tid,
2832
+ args: { name }
2833
+ });
2834
+ }
2835
+ function finish(sink, span, args) {
2836
+ if (!sink.open.delete(span)) return;
2837
+ if (span.lane !== null) sink.lanes[span.lane] = false;
2838
+ if (sink.fd < 0) return;
2839
+ const end = microseconds();
2840
+ emit(sink, {
2841
+ name: span.name,
2842
+ cat: span.cat,
2843
+ ph: "X",
2844
+ ts: span.ts,
2845
+ dur: Math.max(0, end - span.ts),
2846
+ tid: span.tid,
2847
+ ...args || Object.keys(span.args).length ? { args: {
2848
+ ...span.args,
2849
+ ...args
2850
+ } } : {}
2851
+ });
2852
+ }
2853
+ function claimRoot() {
2854
+ const path = process.env[TRACE_ENV];
2855
+ if (!path) return;
2856
+ try {
2857
+ const absolute = resolve(path);
2858
+ if (process.env[OWNER_ENV] === absolute) return;
2859
+ mkdirSync(dirname(absolute), { recursive: true });
2860
+ writeFileSync(absolute, "[\n");
2861
+ process.env[OWNER_ENV] = absolute;
2862
+ } catch {}
2863
+ }
2864
+ function openSink(path) {
2865
+ try {
2866
+ const absolute = resolve(path);
2867
+ mkdirSync(dirname(absolute), { recursive: true });
2868
+ try {
2869
+ writeFileSync(absolute, "[\n", { flag: "wx" });
2870
+ } catch (error) {
2871
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
2872
+ }
2873
+ process.env[TRACE_ENV] = absolute;
2874
+ const sink = {
2875
+ path: absolute,
2876
+ fd: openSync(absolute, "a"),
2877
+ lanes: [],
2878
+ named: /* @__PURE__ */ new Set(),
2879
+ open: /* @__PURE__ */ new Set(),
2880
+ tracks: /* @__PURE__ */ new Map()
2881
+ };
2882
+ emit(sink, {
2883
+ ph: "M",
2884
+ name: "process_name",
2885
+ args: { name: `${basename(process.argv[1] ?? "node").replace(/\.[cm]?[jt]s$/, "")} ${process.pid}` }
2886
+ });
2887
+ process.on("exit", (code) => {
2888
+ for (const span of sink.open) finish(sink, span, span.cat === "command" ? { exit: code } : { unfinished: true });
2889
+ });
2890
+ return sink;
2891
+ } catch {
2892
+ return null;
2893
+ }
2894
+ }
2895
+ function active() {
2896
+ const path = process.env[TRACE_ENV];
2897
+ if (!path) return null;
2898
+ if (path === attempted || path === current$1?.path) return current$1 && current$1.fd >= 0 ? current$1 : null;
2899
+ attempted = path;
2900
+ current$1 = openSink(path);
2901
+ return current$1;
2902
+ }
2903
+ function begin(name, args, cat = "span") {
2904
+ const sink = active();
2905
+ if (!sink) return NO_END;
2906
+ let lane = sink.lanes.indexOf(false);
2907
+ if (lane < 0) lane = sink.lanes.length;
2908
+ sink.lanes[lane] = true;
2909
+ const tid = lane + 1;
2910
+ nameThread(sink, tid, `spans ${tid}`);
2911
+ const span = {
2912
+ name,
2913
+ cat,
2914
+ tid,
2915
+ lane,
2916
+ ts: microseconds(),
2917
+ args: args ?? {}
2918
+ };
2919
+ sink.open.add(span);
2920
+ return (endArgs) => finish(sink, span, endArgs);
2921
+ }
2922
+ const NO_END = () => {};
2923
+ const NO_PHASES = {
2924
+ enter: () => {},
2925
+ end: () => {}
2926
+ };
2927
+ function phases(track) {
2928
+ const sink = active();
2929
+ if (!sink) return NO_PHASES;
2930
+ const tid = sink.tracks.get(track) ?? PHASE_TID_BASE + sink.tracks.size;
2931
+ sink.tracks.set(track, tid);
2932
+ nameThread(sink, tid, `phases: ${track}`);
2933
+ let open = null;
2934
+ const close = (args) => {
2935
+ if (open) finish(sink, open, args);
2936
+ open = null;
2937
+ };
2938
+ return {
2939
+ enter(name, args) {
2940
+ if (open?.name === name) return;
2941
+ close();
2942
+ open = {
2943
+ name,
2944
+ cat: "phase",
2945
+ tid,
2946
+ lane: null,
2947
+ ts: microseconds(),
2948
+ args: args ?? {}
2949
+ };
2950
+ sink.open.add(open);
2951
+ },
2952
+ end: close
2953
+ };
2954
+ }
2955
+ /**
2956
+ * Timing spans for one stim process tree, written as Chrome trace events (open the file in Perfetto or
2957
+ * chrome://tracing) when `STIM_TRACE=<path>` is set. Unset, every call is one environment check. A stim command
2958
+ * (`trace.command`) truncates the file and writes `[`; every other process that inherits the variable appends one
2959
+ * event per line, creating the file if it is missing, so the file has no closing bracket. Child process arguments are written through `redactArgv`.
2960
+ */
2961
+ const trace = {
2962
+ enabled: () => active() !== null,
2963
+ begin: (name, args) => begin(name, args),
2964
+ command(name, args) {
2965
+ claimRoot();
2966
+ begin(name, args, "command");
2967
+ },
2968
+ beginExec: (name, args) => begin(name, args, "exec"),
2969
+ span(name, fn, args) {
2970
+ const end = begin(name, args);
2971
+ try {
2972
+ const result = fn();
2973
+ end();
2974
+ return result;
2975
+ } catch (error) {
2976
+ end({ error: true });
2977
+ throw error;
2978
+ }
2979
+ },
2980
+ async spanAsync(name, fn, args) {
2981
+ const end = begin(name, args);
2982
+ try {
2983
+ const result = await fn();
2984
+ end();
2985
+ return result;
2986
+ } catch (error) {
2987
+ end({ error: true });
2988
+ throw error;
2989
+ }
2990
+ },
2991
+ mark(name, args) {
2992
+ const sink = active();
2993
+ if (!sink) return;
2994
+ nameThread(sink, 1, "spans 1");
2995
+ emit(sink, {
2996
+ name,
2997
+ cat: "mark",
2998
+ ph: "i",
2999
+ s: "t",
3000
+ ts: microseconds(),
3001
+ tid: 1,
3002
+ ...args ? { args } : {}
3003
+ });
3004
+ },
3005
+ phases
3006
+ };
3007
+ //#endregion
2667
3008
  //#region state/home-identity.ts
2668
3009
  /** The persistent, nonsecret identity of this Stim home; null until the CLI initializes it. */
2669
3010
  function readHomeIdentity(config = loadConfig()) {
@@ -2821,6 +3162,33 @@ function countErrorEntries(records) {
2821
3162
  return groups.length - merged;
2822
3163
  }
2823
3164
  //#endregion
3165
+ //#region state/fingerprint-memo.ts
3166
+ const FINGERPRINT_MEMO_SCHEMA = 1;
3167
+ /** Full native fingerprints shared by every workspace of this Stim home, one file per platform and static digest. */
3168
+ function fingerprintMemoDir(platform) {
3169
+ const root = join(configDir(), "fingerprint-memo");
3170
+ return platform ? join(root, platform) : root;
3171
+ }
3172
+ function fingerprintMemoFile(platform, digest) {
3173
+ return join(fingerprintMemoDir(platform), `${digest}.json`);
3174
+ }
3175
+ function fingerprintMemoLock() {
3176
+ return join(configDir(), "fingerprint-memo.lock");
3177
+ }
3178
+ function readFingerprintMemo(platform, digest) {
3179
+ const record = readJsonObject(fingerprintMemoFile(platform, digest));
3180
+ if (!record || record.schema !== 1 || record.platform !== platform || record.digest !== digest || typeof record.recordedAt !== "number" || typeof record.hash !== "string" || !record.hash || !Array.isArray(record.sources) || !record.sources.every(isJsonObject) || !Array.isArray(record.nativePackages) || !record.nativePackages.every((name) => typeof name === "string")) return null;
3181
+ return {
3182
+ schema: 1,
3183
+ platform,
3184
+ digest,
3185
+ recordedAt: record.recordedAt,
3186
+ hash: record.hash,
3187
+ sources: record.sources,
3188
+ nativePackages: record.nativePackages
3189
+ };
3190
+ }
3191
+ //#endregion
2824
3192
  //#region state/ndjson.ts
2825
3193
  const LEVELS = [
2826
3194
  "debug",
@@ -2836,7 +3204,9 @@ const SOURCES = [
2836
3204
  "build",
2837
3205
  "agent",
2838
3206
  "maintenance",
2839
- "placement"
3207
+ "placement",
3208
+ "gc",
3209
+ "builds"
2840
3210
  ];
2841
3211
  function levelRank(level) {
2842
3212
  const i = LEVELS.indexOf(level);
@@ -3864,6 +4234,130 @@ async function withErrorContext(generations, { extra = [], ...options }, selecte
3864
4234
  return matched.map((record) => contextual.get(record) ?? record);
3865
4235
  }
3866
4236
  //#endregion
4237
+ //#region state/ndjson-tail.ts
4238
+ const HEAD_BYTES = 64;
4239
+ function generation(path) {
4240
+ return {
4241
+ path,
4242
+ id: null,
4243
+ offset: 0,
4244
+ head: Buffer.alloc(0),
4245
+ records: []
4246
+ };
4247
+ }
4248
+ function reset(gen) {
4249
+ gen.id = null;
4250
+ gen.offset = 0;
4251
+ gen.head = Buffer.alloc(0);
4252
+ gen.records = [];
4253
+ }
4254
+ function fileId(stats) {
4255
+ return `${stats.dev}:${stats.ino}`;
4256
+ }
4257
+ function readAt(fd, length, position) {
4258
+ const buffer = Buffer.allocUnsafe(length);
4259
+ let filled = 0;
4260
+ while (filled < length) {
4261
+ const n = readSync(fd, buffer, filled, length - filled, position + filled);
4262
+ if (n === 0) break;
4263
+ filled += n;
4264
+ }
4265
+ return buffer.subarray(0, filled);
4266
+ }
4267
+ function advance(gen) {
4268
+ let fd;
4269
+ try {
4270
+ fd = openSync(gen.path, "r");
4271
+ } catch {
4272
+ const had = gen.id !== null;
4273
+ reset(gen);
4274
+ return had;
4275
+ }
4276
+ try {
4277
+ const stats = fstatSync(fd, { bigint: true });
4278
+ const id = fileId(stats);
4279
+ const size = Number(stats.size);
4280
+ let changed = false;
4281
+ if (gen.id !== id || size < gen.offset || gen.head.length > 0 && !readAt(fd, gen.head.length, 0).equals(gen.head)) {
4282
+ changed = gen.id !== null || gen.records.length > 0;
4283
+ reset(gen);
4284
+ gen.id = id;
4285
+ }
4286
+ const length = size - gen.offset;
4287
+ if (length <= 0) return changed;
4288
+ const buffer = readAt(fd, length, gen.offset);
4289
+ const end = buffer.lastIndexOf(10);
4290
+ if (end < 0) return changed;
4291
+ const added = parseNdjsonText(buffer.toString("utf-8", 0, end + 1));
4292
+ for (const record of added) gen.records.push(record);
4293
+ if (gen.head.length < HEAD_BYTES) gen.head = Buffer.concat([gen.head, buffer.subarray(0, end + 1)]).subarray(0, HEAD_BYTES);
4294
+ gen.offset += end + 1;
4295
+ return changed || added.length > 0;
4296
+ } finally {
4297
+ closeSync(fd);
4298
+ }
4299
+ }
4300
+ function statId(path) {
4301
+ try {
4302
+ return fileId(statSync(path, { bigint: true }));
4303
+ } catch {
4304
+ return null;
4305
+ }
4306
+ }
4307
+ /**
4308
+ * Returns a reader of `file` and its rotated generation that yields, on every call, the records a full
4309
+ * `readNdjsonGenerations(file)` would, while reading only the bytes appended since the previous call. A file renamed
4310
+ * to its rotated generation between two calls is finished from there; any other replacement or a shrunk file is
4311
+ * read again from its start. A file rewritten in place to at least its previous length is not detected.
4312
+ */
4313
+ function createNdjsonTail(file) {
4314
+ const rotated = generation(rotatedLogPath(file));
4315
+ const current = generation(file);
4316
+ let all = [];
4317
+ return () => {
4318
+ let changed = false;
4319
+ if (current.id !== null && statId(file) !== current.id && statId(rotated.path) === current.id) {
4320
+ rotated.id = current.id;
4321
+ rotated.offset = current.offset;
4322
+ rotated.head = current.head;
4323
+ rotated.records = current.records;
4324
+ reset(current);
4325
+ changed = true;
4326
+ }
4327
+ changed = advance(rotated) || changed;
4328
+ changed = advance(current) || changed;
4329
+ if (changed) all = rotated.records.concat(current.records);
4330
+ return all;
4331
+ };
4332
+ }
4333
+ //#endregion
4334
+ //#region state/placement.ts
4335
+ /**
4336
+ * The placement words every workload shares (`remote.build`, `ios.remote`, `android.remote`, `macos.remote` and their
4337
+ * flags). Anything else a setting holds is a machine name, or for devices `eas` or `proxy`.
4338
+ *
4339
+ * - `local`: this machine only.
4340
+ * - `auto`: this machine first, a remote machine when this one is full or busy.
4341
+ * - `remote`: remote machines first, in `remote.machines` order; this machine only when none takes the run.
4342
+ * - `remote-only`: remote machines in `remote.machines` order, never this machine.
4343
+ */
4344
+ const PLACEMENT_WORDS = [
4345
+ "local",
4346
+ "auto",
4347
+ "remote",
4348
+ "remote-only"
4349
+ ];
4350
+ function placementWord(value) {
4351
+ const word = value.trim().toLowerCase();
4352
+ return PLACEMENT_WORDS.find((each) => each === word) ?? null;
4353
+ }
4354
+ function prefersRemote(word) {
4355
+ return word === "remote" || word === "remote-only";
4356
+ }
4357
+ function mayRunHere(word) {
4358
+ return word !== "remote-only";
4359
+ }
4360
+ //#endregion
3867
4361
  //#region state/recordings.ts
3868
4362
  /** The footage stim-server keeps per device: the last 15 minutes recorded, however long ago. */
3869
4363
  const RECORDING_FOOTAGE_MS = 9e5;
@@ -4740,6 +5234,14 @@ function lastUseFrom(state, logMtimes) {
4740
5234
  ].filter(Number.isFinite);
4741
5235
  return candidates.length ? Math.max(...candidates) : NaN;
4742
5236
  }
5237
+ /**
5238
+ * When an ios, android or macos run last finished in the workspace, from its recorded builds (a cache hit counts),
5239
+ * in epoch milliseconds; NaN when it records none. Dev server starts, reloads and logs do not count.
5240
+ */
5241
+ function lastNativeUseFrom(state) {
5242
+ const times = [...Object.values(readLastBuilds(state)), ...Object.values(readBuildHistory(state)).flatMap((entries) => entries.slice(0, 1))].map((run) => Date.parse(run.finishedAt ?? run.startedAt)).filter(Number.isFinite);
5243
+ return times.length ? Math.max(...times) : NaN;
5244
+ }
4743
5245
  const MAINTENANCE_LOG_NAME = "maintenance.ndjson";
4744
5246
  /**
4745
5247
  * When the workspace was last used, in epoch milliseconds. A workspace that recorded no use falls back to the mtime
@@ -4849,8 +5351,8 @@ function trimCapacityRefusals(events, now) {
4849
5351
  }).toSorted((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-50);
4850
5352
  }
4851
5353
  const trimCapacityWaits = trimCapacityRefusals;
4852
- const PLATFORMS = ["ios", "android"];
4853
- const OUTCOMES = ["hit", "cold"];
5354
+ const PLATFORMS$1 = ["ios", "android"];
5355
+ const OUTCOMES$1 = ["hit", "cold"];
4854
5356
  const HISTORY_LIMIT = 10;
4855
5357
  function statsProjectKey({ root, commonDir, repoRoot }) {
4856
5358
  if (commonDir && repoRoot && basename(commonDir) === ".git") return canonical$1(join(canonical$1(dirname(commonDir)), relative(canonical$1(repoRoot), canonical$1(root))));
@@ -5007,7 +5509,7 @@ function normalize$1(record) {
5007
5509
  function normalizePlacement(value) {
5008
5510
  if (!isJsonObject(value)) return [];
5009
5511
  const { decision, platform, reason, project, machine } = value;
5010
- if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
5512
+ if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS$1.includes(platform)) return [];
5011
5513
  if (typeof reason !== "string" || typeof project !== "string") return [];
5012
5514
  const buildMs = wholeMs(value.buildMs);
5013
5515
  const slotWaitMs = wholeMs(value.slotWaitMs);
@@ -5030,7 +5532,7 @@ function normalizePlacement(value) {
5030
5532
  function normalizeCapacityRefusal(value) {
5031
5533
  if (!isJsonObject(value)) return [];
5032
5534
  const { at, kind, platform, max, workspace } = value;
5033
- if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
5535
+ if (kind !== "device" || !PLATFORMS$1.includes(platform)) return [];
5034
5536
  if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
5035
5537
  if (typeof max !== "number" || !Number.isInteger(max) || max <= 0) return [];
5036
5538
  if (typeof workspace !== "string" || workspace === "") return [];
@@ -5070,11 +5572,11 @@ function normalizeMachineTotals(totals) {
5070
5572
  function normalizeHistory(scope) {
5071
5573
  const history = {};
5072
5574
  if (!isJsonObject(scope)) return history;
5073
- for (const platform of PLATFORMS) {
5575
+ for (const platform of PLATFORMS$1) {
5074
5576
  const lists = scope[platform];
5075
5577
  if (!isJsonObject(lists)) continue;
5076
5578
  const normalized = {};
5077
- for (const outcome of OUTCOMES) {
5579
+ for (const outcome of OUTCOMES$1) {
5078
5580
  const samples = lists[outcome];
5079
5581
  if (!Array.isArray(samples)) continue;
5080
5582
  const kept = samples.filter(isJsonObject).flatMap((sample) => {
@@ -5105,7 +5607,7 @@ function wholePhases(phases) {
5105
5607
  function normalizeScope(scope) {
5106
5608
  const normalized = {};
5107
5609
  if (!scope || typeof scope !== "object" || Array.isArray(scope)) return normalized;
5108
- for (const platform of PLATFORMS) {
5610
+ for (const platform of PLATFORMS$1) {
5109
5611
  const bucket = scope[platform];
5110
5612
  if (bucket && typeof bucket === "object" && !Array.isArray(bucket)) normalized[platform] = normalizeBucket(bucket);
5111
5613
  }
@@ -5364,6 +5866,7 @@ function parseHostedNativeOffer(value) {
5364
5866
  resources: {
5365
5867
  cpus: resources.cpus,
5366
5868
  loadPerCore: resources.loadPerCore,
5869
+ ...typeof resources.maxLoadPerCore === "number" && Number.isFinite(resources.maxLoadPerCore) && resources.maxLoadPerCore > 0 ? { maxLoadPerCore: resources.maxLoadPerCore } : {},
5367
5870
  memoryFreeBytes: resources.memoryFreeBytes,
5368
5871
  memoryPressure: resources.memoryPressure,
5369
5872
  workerDiskFreeBytes: resources.workerDiskFreeBytes,
@@ -5952,6 +6455,52 @@ function readMaintenanceAttempt() {
5952
6455
  return finite(value) ? value : void 0;
5953
6456
  }
5954
6457
  //#endregion
6458
+ //#region state/gc-log.ts
6459
+ const TRIGGERS = [
6460
+ "manual",
6461
+ "desktop",
6462
+ "maintenance",
6463
+ "budget"
6464
+ ];
6465
+ const STATUSES = [
6466
+ "done",
6467
+ "kept",
6468
+ "failed"
6469
+ ];
6470
+ function isGcLogRecord(record) {
6471
+ return record.src === "gc" && record.event === "gc_item" && typeof record.ts === "number" && typeof record.runId === "string" && typeof record.trigger === "string" && TRIGGERS.includes(record.trigger) && typeof record.status === "string" && STATUSES.includes(record.status) && typeof record.kind === "string" && typeof record.label === "string";
6472
+ }
6473
+ function readGcLog(file = gcLogFile()) {
6474
+ return readNdjsonGenerations(file).filter(isGcLogRecord).toSorted((a, b) => a.ts - b.ts);
6475
+ }
6476
+ //#endregion
6477
+ //#region state/build-ledger.ts
6478
+ const BUILD_LEDGER_CAP = 500;
6479
+ const PLATFORMS = [
6480
+ "ios",
6481
+ "android",
6482
+ "macos"
6483
+ ];
6484
+ const OUTCOMES = [
6485
+ "succeeded",
6486
+ "failed",
6487
+ "cancelled",
6488
+ "interrupted"
6489
+ ];
6490
+ function isBuildLedgerRecord(record) {
6491
+ return record.src === "builds" && record.event === "build" && typeof record.ts === "number" && typeof record.id === "string" && typeof record.platform === "string" && PLATFORMS.includes(record.platform) && typeof record.outcome === "string" && OUTCOMES.includes(record.outcome) && typeof record.placement === "object" && record.placement !== null && typeof record.cache === "object" && record.cache !== null;
6492
+ }
6493
+ /** The ledger's records, oldest first. A missing or unreadable ledger is empty. */
6494
+ function readBuildLedger(file = buildLedgerFile()) {
6495
+ let text;
6496
+ try {
6497
+ text = readStateFile(file);
6498
+ } catch {
6499
+ return [];
6500
+ }
6501
+ return parseNdjsonText(text).filter(isBuildLedgerRecord).toSorted((a, b) => a.ts - b.ts);
6502
+ }
6503
+ //#endregion
5955
6504
  //#region state/agent-device.ts
5956
6505
  function agentDeviceStateDirs(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
5957
6506
  const override = env.AGENT_DEVICE_STATE_DIR?.trim();
@@ -6171,4 +6720,4 @@ function isSetupJournalPrunable(journal, now) {
6171
6720
  return Date.parse(journal.expiresAt) + 36e5 < now;
6172
6721
  }
6173
6722
  //#endregion
6174
- 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, fileIdentity, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAgentDriverName, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, hostedWorkerSettleMs, isExpoErrorContext, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listRecordedDevicesAsync, listSegments, listSegmentsAsync, listStatusCacheEntries, loadConfig, logFiles, machineCacheUsageFile, machineCapacity, machineEndpoint, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openLogDir, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMachineCredential, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, planLogQuery, projectRootDirectories, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveAsync, readArchiveState, readArchives, readArchivesAsync, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readDiskUsageFailure, readEasSessionLedger, readEndedAgentSessions, readHomeIdentity, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMachineCacheUsage, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, requireAutomaticMachine, runId, saturation, serverLocalSocket, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, statusViews, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trashRoot, 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 };
6723
+ 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_LEDGER_CAP, 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, FINGERPRINT_MEMO_SCHEMA, 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, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, PLACEMENT_WORDS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, REPLACED_SETTINGS, 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, TRACE_ENV, 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, budgetSweepFile, budgetSweepLock, buildCriteria, buildDiagnostics, buildLedgerDir, buildLedgerFile, buildLedgerLock, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, cannotHost, capDrops, clientIdentity, clientIdentityKeyFile, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createAsyncAgentActionReader, createDebugLog, createNdjsonTail, createdDevicesFile, createdDevicesLock, debugLogDir, debugLoggingEnabled, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, diskUsageFailureFile, diskUsageWalkClaims, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileIdentity, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectRoot, fingerprintMemoDir, fingerprintMemoFile, fingerprintMemoLock, followLogs, footageDrops, gcLogDir, gcLogFile, 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, isBuildLedgerRecord, isExpoErrorContext, isGcLogRecord, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastNativeUseFrom, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listRecordedDevicesAsync, listSegments, listSegmentsAsync, listStatusCacheEntries, loadConfig, logFiles, machineCacheUsageFile, machineCapacity, machineEndpoint, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, maxLoadPerCore, mayRunHere, 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, placementWord, planLogQuery, prefersRemote, projectRootDirectories, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveAsync, readArchiveState, readArchives, readArchivesAsync, readBuildDetail, readBuildHistory, readBuildLedger, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readDiskUsageFailure, readEasSessionLedger, readEndedAgentSessions, readFingerprintMemo, readGcLog, 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, redactArgv, redactCommandLine, rememberHostedAppBlobRename, replacementAdvice, requireAutomaticMachine, runId, saturation, serverLocalSocket, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, statusViews, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trace, trashRoot, 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, workspaceLightenedFile, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock, workspaceSummary, worktreeArchiveRoot };