@stim-cli/core 1.12.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.
@@ -1,4 +1,4 @@
1
- import { _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
1
+ import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
2
2
  import fs, { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path, { basename, dirname, join } from "node:path";
@@ -76,7 +76,15 @@ function takeLockDirectory(lockPath, claim) {
76
76
  if (!removeLockDirectory(lockPath, marker) || !createLockDirectory(lockPath)) return null;
77
77
  }
78
78
  const marker = join(lockPath, `.stim-claim-${claim.claimId}`);
79
- writeFileSync(marker, "", { flag: "wx" });
79
+ try {
80
+ writeFileSync(marker, "", { flag: "wx" });
81
+ } catch (error) {
82
+ try {
83
+ rmSync(marker, { force: true });
84
+ rmdirSync(lockPath);
85
+ } catch {}
86
+ throw error;
87
+ }
80
88
  return marker;
81
89
  }
82
90
  function acquireDirLock(lockPath, { waitMs, pollMs, ensureParent }) {
@@ -103,7 +111,7 @@ function acquireDirLock(lockPath, { waitMs, pollMs, ensureParent }) {
103
111
  };
104
112
  }
105
113
  if (Date.now() >= deadline) {
106
- const error = /* @__PURE__ */ new Error(`Timed out waiting for the lock at ${lockPath}. Another Stim process may be holding it; if none is running, remove that directory.`);
114
+ const error = /* @__PURE__ */ new Error(`Timed out waiting for the lock at ${lockPath}. ` + (claim ? `No current Stim holds it: the directory is empty, or an older Stim version holds or left it. If no older Stim is running, remove it and run the command again:\n rm -rf ${quotedPath(lockPath)}` : "Another Stim process is holding it; wait for that command to finish and run the command again."));
107
115
  error.code = "STIM_LOCK_TIMEOUT";
108
116
  error.lockPath = lockPath;
109
117
  throw error;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { S as quotedPath } from "./ownership-claim-O31Y5iIb.mjs";
2
- import { C as artifactIn, E as withDirLock, S as workspaceStateDir, T as storeArtifact, _ as updateCacheManifest, a as buildCacheRoot, b as workspaceName, c as cachePathSetting, d as readCacheManifest, f as registerCache, g as tagSharedStore, h as sharedStoreRoot, i as buildCacheKey, l as configDir, m as rotatedLogPath, n as METRO_NAMED_CACHE_LAYOUT, o as cacheManifestLockPath, p as rotateLog, r as STORE_ROOT_TAG, s as cacheNameSegment, t as LOG_ROTATE_BYTES, u as metroCacheRoot, v as workspaceId, w as resolveArtifact, x as workspaceSlug, y as workspaceLogDir } from "./core-CJWLJIr6.mjs";
2
+ import { C as artifactIn, E as withDirLock, S as workspaceStateDir, T as storeArtifact, _ as updateCacheManifest, a as buildCacheRoot, b as workspaceName, c as cachePathSetting, d as readCacheManifest, f as registerCache, g as tagSharedStore, h as sharedStoreRoot, i as buildCacheKey, l as configDir, m as rotatedLogPath, n as METRO_NAMED_CACHE_LAYOUT, o as cacheManifestLockPath, p as rotateLog, r as STORE_ROOT_TAG, s as cacheNameSegment, t as LOG_ROTATE_BYTES, u as metroCacheRoot, v as workspaceId, w as resolveArtifact, x as workspaceSlug, y as workspaceLogDir } from "./core-CmZJEFj_.mjs";
3
3
  export { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, STORE_ROOT_TAG, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, rotateLog, rotatedLogPath, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
package/dist/state.d.mts CHANGED
@@ -112,15 +112,24 @@ declare function allMetroPorts(): number[];
112
112
  declare function findProjectByMetroPort(port: number): string | null;
113
113
  //#endregion
114
114
  //#region state/json-file.d.ts
115
+ /**
116
+ * Runs `fn` with each state file read from disk at most once: inside it, including after an `await`, `readStateFile`
117
+ * returns the text of the first read of that path, and a path found missing stays missing. Callers still parse every time, so no two
118
+ * share a mutable object. Only for computations that write none of the files they read.
119
+ */
120
+ declare function withStateReadCache<T>(fn: () => T): T;
121
+ declare function readStateFile(path: string): string;
115
122
  declare function readJsonFile(path: string): unknown;
116
123
  declare function isJsonObject(value: unknown): value is Record<string, unknown>;
117
124
  declare function readJsonObject(path: string): Record<string, unknown> | null;
118
125
  //#endregion
119
126
  //#region state/ledgers.d.ts
120
- type CreatedDevicePlatform = "ios" | "android";
127
+ type CreatedDevicePlatform = "ios" | "android" | "web";
128
+ /** `web` lists the absolute paths of browser profile directories Stim created. */
121
129
  interface CreatedDevices {
122
130
  ios: ReadonlySet<string>;
123
131
  android: ReadonlySet<string>;
132
+ web: ReadonlySet<string>;
124
133
  }
125
134
  declare function readCreatedDevices(): CreatedDevices;
126
135
  interface EasSessionClaim {
@@ -146,6 +155,8 @@ interface NdjsonRecord {
146
155
  src?: string;
147
156
  level?: string;
148
157
  msg?: string;
158
+ /** With `logs --errors --json`: the code frame and stack lines Expo printed after this error. */
159
+ context?: string[];
149
160
  [key: string]: unknown;
150
161
  }
151
162
  declare const LEVELS: string[];
@@ -167,10 +178,11 @@ interface GrepResult {
167
178
  interface MarkerWindow {
168
179
  launchTs: number | null;
169
180
  bundleTs: number | null;
181
+ pageLoadTs: number | null;
170
182
  }
171
183
  interface QueryCriteria {
172
184
  slot?: string;
173
- includeNativeCrashes?: boolean;
185
+ includeAppDeviceErrors?: boolean;
174
186
  sources?: string[];
175
187
  minLevel?: string;
176
188
  grep?: RegExp;
@@ -202,7 +214,12 @@ declare function buildCriteria({ slot, sources, minLevel, since, grep, errorsOnl
202
214
  declare function logFiles(dir: string): string[];
203
215
  declare function fileSizes(dir: string): Record<string, number>;
204
216
  declare function readLogRecords(dir: string, include?: (name: string) => boolean): NdjsonRecord[];
205
- declare function attachExpoErrorContext(all: NdjsonRecord[], matched: NdjsonRecord[]): NdjsonRecord[];
217
+ type ExpoContextTarget = "msg" | "field";
218
+ /**
219
+ * Adds the code frame and stack lines Expo printed after each Expo error record, either joined into
220
+ * `msg` or as a `context` string array.
221
+ */
222
+ declare function attachExpoErrorContext(all: NdjsonRecord[], matched: NdjsonRecord[], into?: ExpoContextTarget): NdjsonRecord[];
206
223
  declare function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now }?: {
207
224
  slot?: string;
208
225
  dir?: string;
@@ -224,12 +241,14 @@ declare function advanceTail(prev: TailState | null | undefined, chunk: string |
224
241
  state: TailState;
225
242
  records: NdjsonRecord[];
226
243
  };
227
- declare function followLogs({ dir, onRecord, criteria, intervalMs, offsets }: {
244
+ declare function followLogs({ dir, onRecord, criteria, intervalMs, offsets, errorContext }: {
228
245
  dir: string;
229
246
  onRecord: (record: NdjsonRecord) => void;
230
247
  criteria?: QueryCriteria;
231
248
  intervalMs?: number;
232
249
  offsets?: Record<string, number> | null;
250
+ /** Holds each Expo error until a poll adds no more of its code frame or stack lines, then attaches them. */
251
+ errorContext?: ExpoContextTarget;
233
252
  }): () => void;
234
253
  declare function sortByTs(records: NdjsonRecord[]): NdjsonRecord[];
235
254
  //#endregion
@@ -338,6 +357,8 @@ interface SettingsPayload {
338
357
  declare const REMOTE_DEVICE_BACKENDS: readonly RemoteDeviceBackend[];
339
358
  declare const IOS_SIMULATOR_APPS: readonly ["xcode", "siniulator", "stim-desktop"];
340
359
  declare const ANDROID_EMULATOR_APPS: readonly ["emulator", "stim-desktop"];
360
+ declare const WEB_VIEWPORTS: readonly ["desktop", "phone"];
361
+ type WebViewport = (typeof WEB_VIEWPORTS)[number];
341
362
  declare const SETTINGS: readonly SettingDefinition[];
342
363
  declare function settingDefinition(key: string): SettingDefinition | null;
343
364
  declare function isLayeredSetting(setting: SettingDefinition): boolean;
@@ -386,7 +407,16 @@ interface IdleStopRecord {
386
407
  declare function readIdleStop(state: WorkspaceState | null | undefined): IdleStopRecord | null;
387
408
  /** Each platform's latest run; `lastBuild` holds whichever platform ran last. */
388
409
  declare const LAST_BUILD_KEYS: Readonly<Record<StatsPlatform, string>>;
410
+ declare const BUILD_HISTORY_KEY = "buildHistory";
411
+ declare const BUILD_HISTORY_LIMIT = 10;
412
+ /**
413
+ * The first diagnostics of a failed build in the shape a last-build record stores and `status` reports, the
414
+ * ones with a source position first.
415
+ */
416
+ declare function buildDiagnostics(value: unknown): BuildDiagnostic[];
389
417
  declare function readLastBuilds(state: WorkspaceState | null | undefined): Partial<Record<StatsPlatform, LastBuildReport>>;
418
+ /** Each platform's recorded runs, newest first, at most `BUILD_HISTORY_LIMIT` each. */
419
+ declare function readBuildHistory(state: WorkspaceState | null | undefined): Partial<Record<StatsPlatform, BuildHistoryEntry[]>>;
390
420
  declare function readWorkspaceState(root: string): WorkspaceState | null;
391
421
  declare function lastUseFrom(state: WorkspaceState | null, logMtimes: readonly number[]): number;
392
422
  declare function workspaceLastUsed(root: string): number;
@@ -423,6 +453,31 @@ interface LastBuildReport {
423
453
  finishedAt: string | null;
424
454
  errorCode?: string;
425
455
  missReason?: BuildMissReason;
456
+ /** The first compiler diagnostics of a failed build, when the build tool reported any. */
457
+ diagnostics?: BuildDiagnostic[];
458
+ }
459
+ /** How a recorded build ended. `interrupted` is a run whose process ended without recording a result. */
460
+ declare const BUILD_RESULTS: readonly ["succeeded", "failed", "cancelled", "interrupted"];
461
+ type BuildResult = (typeof BUILD_RESULTS)[number];
462
+ /**
463
+ * One run in a workspace's recent build history. `configuration` is the iOS configuration or the Android
464
+ * variant the run built, `Debug` or `debug` by default, and null when the run ended before resolving it.
465
+ * `phases` holds the milliseconds spent in each phase the run entered. An interrupted run has null
466
+ * `durationMs` and `finishedAt`, no cache facts, and 0 for the phase it stopped in.
467
+ */
468
+ interface BuildHistoryEntry extends LastBuildReport {
469
+ result: BuildResult;
470
+ slot: string;
471
+ configuration: string | null;
472
+ cacheKey: string | null;
473
+ phases: Partial<Record<BuildPhase, number>>;
474
+ }
475
+ /** One compiler error from a failed build: where it is, when the tool said, and its message. */
476
+ interface BuildDiagnostic {
477
+ file: string | null;
478
+ line: number | null;
479
+ column: number | null;
480
+ message: string;
426
481
  }
427
482
  /** What kind of native input a changed fingerprint source is. */
428
483
  type BuildMissCategory = "native-dependency" | "config-plugin" | "app-config" | "app-asset" | "package" | "native-dir" | "autolinking" | "package-scripts" | "file" | "other";
@@ -483,6 +538,17 @@ interface DeviceActivity {
483
538
  lastActivityAt?: string;
484
539
  basis: string[];
485
540
  }
541
+ /** Every state a device's app process can report. */
542
+ declare const APP_PROCESS_STATES: readonly ["running", "stopped", "unknown"];
543
+ /**
544
+ * Whether the workspace's app process is alive on a device now. This is current process state, not launch evidence:
545
+ * `launched` in an `ios` or `android` result stays the record of one launch. `id` is the bundle identifier or
546
+ * package that was checked; `state` is "unknown" when the process listing could not be read.
547
+ */
548
+ interface DeviceAppProcess {
549
+ id: string;
550
+ state: (typeof APP_PROCESS_STATES)[number];
551
+ }
486
552
  /**
487
553
  * A linked worktree's `git status`. `changed` counts tracked paths with staged or unstaged changes, including
488
554
  * conflicts; `untracked` counts untracked entries as `git status` lists them, so an untracked directory counts once.
@@ -519,7 +585,7 @@ interface RemoteDeviceState {
519
585
  webPreviewUrl: string | null;
520
586
  }
521
587
  /** Every code a status issue can carry. */
522
- declare const STATUS_ISSUE_CODES: readonly ["port-not-ours", "sim-missing", "sim-without-metro", "avd-serial-changed", "avd-missing", "avd-not-detected", "avd-unchecked", "supervisor-unverified"];
588
+ declare const STATUS_ISSUE_CODES: readonly ["port-not-ours", "sim-missing", "sim-without-metro", "avd-serial-changed", "avd-missing", "avd-not-detected", "avd-unchecked", "supervisor-unverified", "browser-unverified", "browser-orphaned"];
523
589
  type StatusIssueCode = (typeof STATUS_ISSUE_CODES)[number];
524
590
  /**
525
591
  * One thing in a workspace that needs the user. `remedy` is a command to run from `workspace`; `slot` is absent
@@ -533,6 +599,23 @@ interface StatusIssue {
533
599
  workspace: string;
534
600
  slot?: string;
535
601
  }
602
+ /**
603
+ * The workspace's Stim-owned Chrome. `pid` is Chrome's process and `supervisorPid` the Stim process that holds
604
+ * its DevTools session. `cdpEndpoint` is the loopback DevTools HTTP endpoint agents can attach to, null when the
605
+ * browser is not running. `profile` is the Stim-owned user data directory.
606
+ */
607
+ interface WebBrowserState {
608
+ browser: "chrome";
609
+ version: string | null;
610
+ running: boolean;
611
+ pid: number | null;
612
+ supervisorPid: number | null;
613
+ url: string;
614
+ headless: boolean;
615
+ viewport: WebViewport;
616
+ profile: string;
617
+ cdpEndpoint: string | null;
618
+ }
536
619
  interface EnvironmentState {
537
620
  slots?: {
538
621
  slot: string;
@@ -541,6 +624,10 @@ interface EnvironmentState {
541
624
  }[];
542
625
  path: string;
543
626
  live: boolean;
627
+ /**
628
+ * A fixed estimate, not a measurement: a set amount per booted simulator, detected emulator, running Metro and
629
+ * running Chrome. `capacity.committedMb` sums it. What the workspace's processes use now is in `machine`.
630
+ */
544
631
  memoryMb: number;
545
632
  warnings: string[];
546
633
  issues: StatusIssue[];
@@ -550,6 +637,7 @@ interface EnvironmentState {
550
637
  owned: boolean;
551
638
  state: string;
552
639
  activity?: DeviceActivity;
640
+ app?: DeviceAppProcess;
553
641
  } | null;
554
642
  android?: {
555
643
  name: string | undefined;
@@ -559,6 +647,7 @@ interface EnvironmentState {
559
647
  state?: AndroidRuntimeFacts["state"];
560
648
  deviceProfile?: string | null;
561
649
  activity?: DeviceActivity;
650
+ app?: DeviceAppProcess;
562
651
  } | null;
563
652
  metro?: {
564
653
  port: number;
@@ -566,6 +655,7 @@ interface EnvironmentState {
566
655
  pid: number | null;
567
656
  idleStop?: IdleStopRecord;
568
657
  } | null;
658
+ web?: WebBrowserState | null;
569
659
  supervisor?: {
570
660
  pid: number | null;
571
661
  mode: string | null;
@@ -580,7 +670,10 @@ interface EnvironmentState {
580
670
  remoteDevices?: RemoteDeviceState[];
581
671
  build?: BuildReport | null;
582
672
  lastBuilds?: Partial<Record<StatsPlatform, LastBuildReport>>;
673
+ /** Each platform's recent runs, newest first, at most `BUILD_HISTORY_LIMIT` each. */
674
+ builds?: Partial<Record<StatsPlatform, BuildHistoryEntry[]>>;
583
675
  }
676
+ /** `committedMb` sums the environments' estimated `memoryMb`; `overCapacity` is that sum over 60% of `totalMemoryMb`. */
584
677
  interface StatusCapacity {
585
678
  liveCount: number;
586
679
  committedMb: number;
@@ -600,6 +693,34 @@ interface DeviceLeaseState {
600
693
  expired: boolean;
601
694
  parsed: boolean;
602
695
  }
696
+ /** Every kind of process owner the status machine section reports. */
697
+ declare const MACHINE_OWNER_KINDS: readonly ["simulator", "emulator", "metro", "build", "browser", "server", "shared"];
698
+ type MachineOwnerKind = (typeof MACHINE_OWNER_KINDS)[number];
699
+ /**
700
+ * One thing using this Mac's CPU and memory now. Each process is counted in exactly one owner: the one whose root
701
+ * process is its nearest ancestor. `workspace` and `slot` (absent for the default slot) name the workspace a
702
+ * simulator, emulator, Metro, build or Chrome belongs to; a device no workspace records has a null workspace, and
703
+ * machine-wide processes are `shared`. `id` is the simulator's UDID, the emulator's AVD name, Metro's port as text,
704
+ * the build's platform, or null. `owned` is true only for what Stim started and stops: a workspace's owned device,
705
+ * Metro, build or Chrome. `cpuPercent` is `ps` %CPU summed over the owner's processes, where 100 is one core.
706
+ * `residentMb` sums their resident set sizes; pages shared between processes count once per process, so a
707
+ * simulator, whose processes all map the runtime's shared libraries, reports well above its physical footprint.
708
+ */
709
+ interface MachineOwner {
710
+ kind: MachineOwnerKind;
711
+ name: string;
712
+ workspace: string | null;
713
+ slot?: string;
714
+ id: string | null;
715
+ owned: boolean;
716
+ cpuPercent: number;
717
+ residentMb: number;
718
+ processes: number;
719
+ }
720
+ /** Where this machine's CPU and resident memory go, from one pass over the host process table. */
721
+ interface MachineUsageState {
722
+ owners: MachineOwner[];
723
+ }
603
724
  /** The payload `stim status --json` prints, and `status --watch --json` prints on each change. */
604
725
  interface StatusPayload {
605
726
  environments: (EnvironmentState & {
@@ -609,6 +730,8 @@ interface StatusPayload {
609
730
  deviceLeases: DeviceLeaseState[];
610
731
  unprovisionedWorktrees: WorktreeFacts[];
611
732
  simctlAvailable: boolean;
733
+ /** Null when nothing runs that status attributes, so it read no process table, or when the table was unreadable. */
734
+ machine: MachineUsageState | null;
612
735
  }
613
736
  //#endregion
614
- export { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, ActiveBuildState, ActivityDriver, AndroidRuntimeFacts, BUILD_PHASES, BuildCacheHit, BuildMissCategory, BuildMissChange, BuildMissReason, BuildPhase, BuildPlanPayload, BuildReport, ConcurrencyLimits, Config, CreatedDevicePlatform, CreatedDevices, DeviceActivity, DeviceLeaseState, DeviceRecord, DoctorRunRecord, ERROR_SOURCES, EasSessionClaim, EasSessionLedgerRead, EnvironmentState, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, IdleStopRecord, LAST_BUILD_KEYS, LEVELS, LastBuildReport, NdjsonRecord, PlatformRecords, ProjectRecord, QueryCriteria, REMOTE_DEVICE_BACKENDS, RemoteDeviceBackend, RemoteDeviceState, RepoRecord, RunOutcomeKind, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATUS_ISSUE_CODES, SettingDefinition, SettingEntry, SettingScope, SettingType, SettingsObject, SettingsPayload, StatsPlatform, StatusCapacity, StatusIssue, StatusIssueCode, StatusPayload, StimConfig, SupervisorRecord, TUNNEL_MODES, TunnelMode, WorkspaceState, WorktreeFacts, WorktreeGit, acceptsShape, advanceTail, allMetroPorts, attachExpoErrorContext, buildCriteria, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, createdDevicesFile, createdDevicesLock, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, expectedShape, fileSizes, findEnclosingWorktreeRoot, findProjectByMetroPort, followLogs, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, loadConfig, logFiles, markerWindow, parseNdjsonLine, parseNdjsonText, parseSince, queryLogs, readCreatedDevices, readEasSessionLedger, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readNdjsonGenerations, readWorkspaceState, recordMatches, settingDefinition, settingValueError, settingsJsonSchema, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, supervisorLogFile, supervisorPidFile, tailRead, validEasSessionClaim, withConfigLock, workspaceDerivedData, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceStateFile, workspaceStateLock };
737
+ export { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, ActiveBuildState, ActivityDriver, AndroidRuntimeFacts, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, BuildCacheHit, BuildDiagnostic, BuildHistoryEntry, BuildMissCategory, BuildMissChange, BuildMissReason, BuildPhase, BuildPlanPayload, BuildReport, BuildResult, ConcurrencyLimits, Config, CreatedDevicePlatform, CreatedDevices, DeviceActivity, DeviceAppProcess, DeviceLeaseState, DeviceRecord, DoctorRunRecord, ERROR_SOURCES, EasSessionClaim, EasSessionLedgerRead, EnvironmentState, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, IdleStopRecord, LAST_BUILD_KEYS, LEVELS, LastBuildReport, MACHINE_OWNER_KINDS, MachineOwner, MachineOwnerKind, MachineUsageState, NdjsonRecord, PlatformRecords, ProjectRecord, QueryCriteria, REMOTE_DEVICE_BACKENDS, RemoteDeviceBackend, RemoteDeviceState, RepoRecord, RunOutcomeKind, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATUS_ISSUE_CODES, SettingDefinition, SettingEntry, SettingScope, SettingType, SettingsObject, SettingsPayload, StatsPlatform, StatusCapacity, StatusIssue, StatusIssueCode, StatusPayload, StimConfig, SupervisorRecord, TUNNEL_MODES, TunnelMode, WEB_VIEWPORTS, WebBrowserState, WebViewport, WorkspaceState, WorktreeFacts, WorktreeGit, acceptsShape, advanceTail, allMetroPorts, attachExpoErrorContext, buildCriteria, buildDiagnostics, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, createdDevicesFile, createdDevicesLock, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, expectedShape, fileSizes, findEnclosingWorktreeRoot, findProjectByMetroPort, followLogs, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, loadConfig, logFiles, markerWindow, parseNdjsonLine, parseNdjsonText, parseSince, queryLogs, readBuildHistory, readCreatedDevices, readEasSessionLedger, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readNdjsonGenerations, readStateFile, readWorkspaceState, recordMatches, settingDefinition, settingValueError, settingsJsonSchema, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, supervisorLogFile, supervisorPidFile, tailRead, validEasSessionClaim, withConfigLock, withStateReadCache, workspaceDerivedData, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceStateFile, workspaceStateLock };
package/dist/state.mjs CHANGED
@@ -1,11 +1,38 @@
1
- import { E as withDirLock, S as workspaceStateDir, b as workspaceName, l as configDir, m as rotatedLogPath } from "./core-CJWLJIr6.mjs";
1
+ import { E as withDirLock, S as workspaceStateDir, b as workspaceName, l as configDir, m as rotatedLogPath } from "./core-CmZJEFj_.mjs";
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import { StringDecoder } from "node:string_decoder";
6
7
  //#region state/json-file.ts
8
+ const readScope = new AsyncLocalStorage();
9
+ /**
10
+ * Runs `fn` with each state file read from disk at most once: inside it, including after an `await`, `readStateFile`
11
+ * returns the text of the first read of that path, and a path found missing stays missing. Callers still parse every time, so no two
12
+ * share a mutable object. Only for computations that write none of the files they read.
13
+ */
14
+ function withStateReadCache(fn) {
15
+ return readScope.run(/* @__PURE__ */ new Map(), fn);
16
+ }
17
+ function readStateFile(path) {
18
+ const cache = readScope.getStore();
19
+ if (!cache) return readFileSync(path, "utf-8");
20
+ const cached = cache.get(path);
21
+ if (cached) {
22
+ if ("missing" in cached) throw cached.missing;
23
+ return cached.text;
24
+ }
25
+ try {
26
+ const text = readFileSync(path, "utf-8");
27
+ cache.set(path, { text });
28
+ return text;
29
+ } catch (error) {
30
+ if (error.code === "ENOENT") cache.set(path, { missing: error });
31
+ throw error;
32
+ }
33
+ }
7
34
  function readJsonFile(path) {
8
- return JSON.parse(readFileSync(path, "utf-8"));
35
+ return JSON.parse(readStateFile(path));
9
36
  }
10
37
  function isJsonObject(value) {
11
38
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -106,11 +133,12 @@ function configCorrupt(reason, path = getConfigPath()) {
106
133
  }
107
134
  function loadConfig() {
108
135
  const p = getConfigPath();
109
- if (!existsSync(p)) return null;
110
136
  let parsed;
111
137
  try {
112
138
  parsed = readJsonFile(p);
113
139
  } catch (err) {
140
+ const code = err.code;
141
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
114
142
  if (!(err instanceof SyntaxError)) throw err;
115
143
  throw configCorrupt(`is not valid JSON: ${err.message}`, p);
116
144
  }
@@ -211,15 +239,17 @@ function ids(value) {
211
239
  }
212
240
  function readCreatedDevices() {
213
241
  try {
214
- const parsed = JSON.parse(readFileSync(createdDevicesFile(), "utf8"));
242
+ const parsed = JSON.parse(readStateFile(createdDevicesFile()));
215
243
  return {
216
244
  ios: new Set(ids(parsed?.ios)),
217
- android: new Set(ids(parsed?.android))
245
+ android: new Set(ids(parsed?.android)),
246
+ web: new Set(ids(parsed?.web))
218
247
  };
219
248
  } catch {
220
249
  return {
221
250
  ios: /* @__PURE__ */ new Set(),
222
- android: /* @__PURE__ */ new Set()
251
+ android: /* @__PURE__ */ new Set(),
252
+ web: /* @__PURE__ */ new Set()
223
253
  };
224
254
  }
225
255
  }
@@ -380,24 +410,34 @@ const ERROR_SOURCES = [
380
410
  function markerWindow(records) {
381
411
  let launchTs = null;
382
412
  let bundleTs = null;
413
+ let pageLoadTs = null;
383
414
  for (const r of records) {
384
415
  if (r?.marker !== true) continue;
385
416
  const ts = tsOf(r);
386
417
  if (ts === null) continue;
387
418
  if (r.src === "metro") {
388
419
  if (bundleTs === null || ts > bundleTs) bundleTs = ts;
420
+ } else if (isWebPageRecord(r)) {
421
+ if (pageLoadTs === null || ts > pageLoadTs) pageLoadTs = ts;
389
422
  } else if (launchTs === null || ts > launchTs) launchTs = ts;
390
423
  }
391
424
  return {
392
425
  launchTs,
393
- bundleTs
426
+ bundleTs,
427
+ pageLoadTs
394
428
  };
395
429
  }
430
+ function isWebPageRecord(record) {
431
+ return record.platform === "web" && record.src !== "metro";
432
+ }
433
+ function isAppDeviceRecord(record) {
434
+ return record.src === "device" && (record.event === "native_crash" || record.platform === "web");
435
+ }
396
436
  function recordMatches(record, criteria = {}) {
397
437
  if (!record) return false;
398
438
  if (criteria.slot !== void 0 && (record.slot ?? "default") !== criteria.slot) return false;
399
439
  const { sources, minLevel, grep, sinceTs, errorsOnly, markerTs, bundleMarkerTs } = criteria;
400
- if (sources && sources.length > 0 && !sources.includes(record.src) && !(criteria.includeNativeCrashes && record.src === "device" && record.event === "native_crash")) return false;
440
+ if (sources && sources.length > 0 && !sources.includes(record.src) && !(criteria.includeAppDeviceErrors && isAppDeviceRecord(record))) return false;
401
441
  if (minLevel && levelRank(record.level) < levelRank(minLevel)) return false;
402
442
  if (errorsOnly) {
403
443
  if (record.level !== "error" && record.level !== "fatal") return false;
@@ -424,7 +464,7 @@ function buildCriteria({ slot, sources, minLevel, since, grep, errorsOnly, marke
424
464
  const criteria = {
425
465
  ...slot === void 0 ? {} : { slot },
426
466
  errorsOnly: Boolean(errorsOnly),
427
- includeNativeCrashes: Boolean(errorsOnly && !sources?.length)
467
+ includeAppDeviceErrors: Boolean(errorsOnly && !sources?.length)
428
468
  };
429
469
  if (sources && sources.length > 0) criteria.sources = sources;
430
470
  else if (criteria.errorsOnly) criteria.sources = ERROR_SOURCES;
@@ -468,24 +508,86 @@ function isExpoErrorContext(record, event) {
468
508
  if (record.src !== "metro" || record.raw !== true || record.event !== event || typeof record.msg !== "string") return false;
469
509
  return /^Code: \S/.test(record.msg) || /^\s*>?\s*\d+\s*\|/.test(record.msg) || /^\s*\|\s*\^/.test(record.msg) || /^Call Stack$/.test(record.msg) || /^\s+.+\([^()]+:\d+:\d+\)\s*$/.test(record.msg) || /^\s+at\s+\S+:\d+:\d+\s*$/.test(record.msg);
470
510
  }
471
- function attachExpoErrorContext(all, matched) {
511
+ function isExpoError(record) {
512
+ return record.src === "metro" && record.raw === true && (record.level === "error" || record.level === "fatal") && typeof record.msg === "string";
513
+ }
514
+ function isSameExpoStream(record, error) {
515
+ return record.src === "metro" && record.raw === true && record.event === error.event;
516
+ }
517
+ function withExpoContext(record, context, into) {
518
+ if (context.length === 0) return record;
519
+ return into === "msg" ? {
520
+ ...record,
521
+ msg: [record.msg, ...context].join("\n")
522
+ } : {
523
+ ...record,
524
+ context
525
+ };
526
+ }
527
+ /**
528
+ * Adds the code frame and stack lines Expo printed after each Expo error record, either joined into
529
+ * `msg` or as a `context` string array.
530
+ */
531
+ function attachExpoErrorContext(all, matched, into = "msg") {
472
532
  return matched.map((record) => {
473
- if (record.src !== "metro" || record.raw !== true || record.level !== "error" && record.level !== "fatal" || typeof record.msg !== "string") return record;
533
+ if (!isExpoError(record)) return record;
474
534
  const index = all.findIndex((entry) => entry.ts === record.ts && entry.src === record.src && entry.msg === record.msg && entry.event === record.event);
475
535
  if (index === -1) return record;
476
536
  const context = [];
477
537
  for (let i = index + 1; i < all.length; i += 1) {
478
538
  const next = all[i];
479
- if (next.src !== "metro") continue;
539
+ if (!isSameExpoStream(next, record)) continue;
480
540
  if (!isExpoErrorContext(next, record.event)) break;
481
541
  context.push(next.msg);
482
542
  }
483
- return context.length > 0 ? {
484
- ...record,
485
- msg: [record.msg, ...context].join("\n")
486
- } : record;
543
+ return withExpoContext(record, context, into);
487
544
  });
488
545
  }
546
+ function expoContextFollower(onRecord, into) {
547
+ let pending = null;
548
+ let grew = false;
549
+ function flush() {
550
+ if (!pending) return;
551
+ const { record, context, queued } = pending;
552
+ pending = null;
553
+ onRecord(withExpoContext(record, context, into));
554
+ for (const next of queued) onRecord(next);
555
+ }
556
+ function push(record, matches) {
557
+ if (pending && isSameExpoStream(record, pending.record)) {
558
+ if (!isExpoErrorContext(record, pending.record.event)) flush();
559
+ else {
560
+ pending.context.push(record.msg);
561
+ if (matches) pending.queued.push(record);
562
+ grew = true;
563
+ return;
564
+ }
565
+ }
566
+ if (pending) {
567
+ if (matches) pending.queued.push(record);
568
+ return;
569
+ }
570
+ if (matches && isExpoError(record)) {
571
+ pending = {
572
+ record,
573
+ context: [],
574
+ queued: []
575
+ };
576
+ grew = true;
577
+ return;
578
+ }
579
+ if (matches) onRecord(record);
580
+ }
581
+ function settle() {
582
+ if (!grew) flush();
583
+ grew = false;
584
+ }
585
+ return {
586
+ push,
587
+ settle,
588
+ flush
589
+ };
590
+ }
489
591
  function includeBareErrorContext(all, matched, launchTs, sinceTs) {
490
592
  if (!matched.some((record) => record.src === "client" && (record.level === "error" || record.level === "fatal"))) return matched;
491
593
  const renderedContext = [];
@@ -505,7 +607,7 @@ function includeBareErrorContext(all, matched, launchTs, sinceTs) {
505
607
  function launchMarkersBySlot(records) {
506
608
  const markers = /* @__PURE__ */ new Map();
507
609
  for (const record of records) {
508
- if (record.marker !== true || record.src === "metro") continue;
610
+ if (record.marker !== true || record.src === "metro" || isWebPageRecord(record)) continue;
509
611
  const ts = tsOf(record);
510
612
  const slot = record.slot ?? "default";
511
613
  if (ts !== null && ts > (markers.get(slot) ?? -Infinity)) markers.set(slot, ts);
@@ -515,9 +617,10 @@ function launchMarkersBySlot(records) {
515
617
  function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
516
618
  const all = (records ?? readLogRecords(dir)).filter((record) => slot === void 0 || (record.slot ?? "default") === slot);
517
619
  if (all.length === 0) return [];
518
- const { launchTs, bundleTs } = errorsOnly ? markerWindow(all) : {
620
+ const { launchTs, bundleTs, pageLoadTs } = errorsOnly ? markerWindow(all) : {
519
621
  launchTs: null,
520
- bundleTs: null
622
+ bundleTs: null,
623
+ pageLoadTs: null
521
624
  };
522
625
  const criteria = buildCriteria({
523
626
  slot,
@@ -531,9 +634,14 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
531
634
  bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
532
635
  });
533
636
  const slotMarkers = launchMarkersBySlot(all);
637
+ const windowStart = (record) => {
638
+ if (pageLoadTs !== null && isWebPageRecord(record)) return pageLoadTs - 1;
639
+ if (record.src === "metro" || record.src === "client") return criteria.markerTs;
640
+ return slotMarkers.get(record.slot ?? "default");
641
+ };
534
642
  let matched = all.filter((record) => recordMatches(record, {
535
643
  ...criteria,
536
- markerTs: record.src === "metro" || record.src === "client" ? criteria.markerTs : slotMarkers.get(record.slot ?? "default")
644
+ markerTs: windowStart(record)
537
645
  }));
538
646
  if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
539
647
  return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs, criteria.sinceTs) : matched;
@@ -571,7 +679,8 @@ function advanceTail(prev, chunk, size) {
571
679
  records
572
680
  };
573
681
  }
574
- function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets = null }) {
682
+ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets = null, errorContext }) {
683
+ const follower = errorContext ? expoContextFollower(onRecord, errorContext) : null;
575
684
  const state = /* @__PURE__ */ new Map();
576
685
  const decoders = /* @__PURE__ */ new Map();
577
686
  for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
@@ -582,7 +691,11 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
582
691
  decoders.set(name, new StringDecoder("utf8"));
583
692
  }
584
693
  function emit(records) {
585
- for (const record of records) if (recordMatches(record, criteria)) onRecord(record);
694
+ for (const record of records) {
695
+ const matches = recordMatches(record, criteria);
696
+ if (follower) follower.push(record, matches);
697
+ else if (matches) onRecord(record);
698
+ }
586
699
  }
587
700
  function drainRotated(name, entry) {
588
701
  const path = rotatedLogPath(join(dir, name));
@@ -611,10 +724,12 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
611
724
  for (const name of logFiles(dir)) try {
612
725
  pollFile(name);
613
726
  } catch {}
727
+ follower?.settle();
614
728
  }
615
729
  const timer = setInterval(poll, intervalMs);
616
730
  return function stop() {
617
731
  clearInterval(timer);
732
+ follower?.flush();
618
733
  };
619
734
  }
620
735
  function readChunk(path, decoder, start, size) {
@@ -674,6 +789,7 @@ const IOS_SIMULATOR_APPS = [
674
789
  "stim-desktop"
675
790
  ];
676
791
  const ANDROID_EMULATOR_APPS = ["emulator", "stim-desktop"];
792
+ const WEB_VIEWPORTS = ["desktop", "phone"];
677
793
  const PROJECT = [
678
794
  "workspace",
679
795
  "repo",
@@ -923,6 +1039,33 @@ const SETTINGS = [
923
1039
  default: 60,
924
1040
  description: "Minutes without a bundle request, client log or Stim command before the dev server stops; 0 never"
925
1041
  },
1042
+ {
1043
+ key: "web.url",
1044
+ type: {
1045
+ kind: "string",
1046
+ pattern: "^https?://",
1047
+ patternHelp: "an http:// or https:// URL; {port:<label>} is replaced by a named or the Metro port"
1048
+ },
1049
+ scopes: PROJECT,
1050
+ description: "Page `stim web` opens; unset opens the Metro URL for Expo web"
1051
+ },
1052
+ {
1053
+ key: "web.ignoreCertificateErrors",
1054
+ type: BOOLEAN,
1055
+ scopes: PROJECT,
1056
+ default: false,
1057
+ description: "Accept self-signed dev certificates in Stim's owned Chrome profile"
1058
+ },
1059
+ {
1060
+ key: "web.viewport",
1061
+ type: {
1062
+ kind: "choice",
1063
+ choices: WEB_VIEWPORTS
1064
+ },
1065
+ scopes: PROJECT,
1066
+ default: "desktop",
1067
+ description: "Viewport of the owned Chrome page: desktop, or phone for a 390x844 touch screen"
1068
+ },
926
1069
  {
927
1070
  key: "worktree.exclude",
928
1071
  type: { kind: "strings" },
@@ -1027,7 +1170,7 @@ const SETTINGS = [
1027
1170
  scopes: MACHINE,
1028
1171
  default: 120,
1029
1172
  env: "STIM_GC_WORKTREE_GRACE_MINUTES",
1030
- description: "Minutes after a linked worktree last changed, or its branch merged, before gc removes it; 0 removes it at once"
1173
+ description: "Minutes after a linked worktree last changed, or its branch or pull request finished, before gc removes it; 0 removes it at once"
1031
1174
  },
1032
1175
  {
1033
1176
  key: "pool.iosParkedMax",
@@ -1259,6 +1402,19 @@ const BUILD_PHASES = [
1259
1402
  "install",
1260
1403
  "launch"
1261
1404
  ];
1405
+ /** How a recorded build ended. `interrupted` is a run whose process ended without recording a result. */
1406
+ const BUILD_RESULTS = [
1407
+ "succeeded",
1408
+ "failed",
1409
+ "cancelled",
1410
+ "interrupted"
1411
+ ];
1412
+ /** Every state a device's app process can report. */
1413
+ const APP_PROCESS_STATES = [
1414
+ "running",
1415
+ "stopped",
1416
+ "unknown"
1417
+ ];
1262
1418
  /** Every code a status issue can carry. */
1263
1419
  const STATUS_ISSUE_CODES = [
1264
1420
  "port-not-ours",
@@ -1268,7 +1424,19 @@ const STATUS_ISSUE_CODES = [
1268
1424
  "avd-missing",
1269
1425
  "avd-not-detected",
1270
1426
  "avd-unchecked",
1271
- "supervisor-unverified"
1427
+ "supervisor-unverified",
1428
+ "browser-unverified",
1429
+ "browser-orphaned"
1430
+ ];
1431
+ /** Every kind of process owner the status machine section reports. */
1432
+ const MACHINE_OWNER_KINDS = [
1433
+ "simulator",
1434
+ "emulator",
1435
+ "metro",
1436
+ "build",
1437
+ "browser",
1438
+ "server",
1439
+ "shared"
1272
1440
  ];
1273
1441
  //#endregion
1274
1442
  //#region state/workspace-state.ts
@@ -1287,6 +1455,8 @@ const LAST_BUILD_KEYS = {
1287
1455
  ios: "lastIosBuild",
1288
1456
  android: "lastAndroidBuild"
1289
1457
  };
1458
+ const BUILD_HISTORY_KEY = "buildHistory";
1459
+ const BUILD_HISTORY_LIMIT = 10;
1290
1460
  const MISS_KINDS = /* @__PURE__ */ new Set([
1291
1461
  "changed",
1292
1462
  "no-baseline",
@@ -1335,6 +1505,25 @@ function missReason(value) {
1335
1505
  rekeyedBy: Array.isArray(record.rekeyedBy) ? record.rekeyedBy.filter((step) => typeof step === "string") : []
1336
1506
  };
1337
1507
  }
1508
+ const DIAGNOSTIC_CAP = 5;
1509
+ const positive = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
1510
+ /**
1511
+ * The first diagnostics of a failed build in the shape a last-build record stores and `status` reports, the
1512
+ * ones with a source position first.
1513
+ */
1514
+ function buildDiagnostics(value) {
1515
+ return (Array.isArray(value) ? value : []).flatMap((item) => {
1516
+ if (!item || typeof item !== "object") return [];
1517
+ const { file, line, column, message } = item;
1518
+ if (typeof message !== "string" || message === "") return [];
1519
+ return [{
1520
+ file: typeof file === "string" && file ? file : null,
1521
+ line: positive(line),
1522
+ column: positive(column),
1523
+ message
1524
+ }];
1525
+ }).toSorted((a, b) => Number(b.file !== null) - Number(a.file !== null)).slice(0, DIAGNOSTIC_CAP);
1526
+ }
1338
1527
  function lastBuildReport(platform, value) {
1339
1528
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1340
1529
  const record = value;
@@ -1343,6 +1532,7 @@ function lastBuildReport(platform, value) {
1343
1532
  const durationMs = typeof record.durationMs === "number" && record.durationMs >= 0 ? record.durationMs : null;
1344
1533
  const finished = new Date(Date.parse(record.startedAt) + (durationMs ?? NaN));
1345
1534
  const reason = record.cacheHit === "local" || record.cacheHit === "remote" ? null : missReason(record.missReason);
1535
+ const diagnostics = record.status === "failed" ? buildDiagnostics(record.diagnostics) : [];
1346
1536
  return {
1347
1537
  platform,
1348
1538
  status: record.status,
@@ -1353,7 +1543,8 @@ function lastBuildReport(platform, value) {
1353
1543
  startedAt: record.startedAt,
1354
1544
  finishedAt: Number.isNaN(finished.getTime()) ? null : finished.toISOString(),
1355
1545
  ...typeof record.errorCode === "string" ? { errorCode: record.errorCode } : {},
1356
- ...reason ? { missReason: reason } : {}
1546
+ ...reason ? { missReason: reason } : {},
1547
+ ...diagnostics.length ? { diagnostics } : {}
1357
1548
  };
1358
1549
  }
1359
1550
  function readLastBuilds(state) {
@@ -1364,6 +1555,41 @@ function readLastBuilds(state) {
1364
1555
  }
1365
1556
  return reports;
1366
1557
  }
1558
+ function historyPhases(value) {
1559
+ const phases = {};
1560
+ if (!value || typeof value !== "object" || Array.isArray(value)) return phases;
1561
+ for (const phase of BUILD_PHASES) {
1562
+ const ms = value[phase];
1563
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) phases[phase] = Math.round(ms);
1564
+ }
1565
+ return phases;
1566
+ }
1567
+ function historyEntry(platform, value) {
1568
+ const report = lastBuildReport(platform, value);
1569
+ if (!report) return null;
1570
+ const record = value;
1571
+ const result = BUILD_RESULTS.includes(record.result) ? record.result : report.status === "ok" ? "succeeded" : "failed";
1572
+ return {
1573
+ ...report,
1574
+ result,
1575
+ slot: typeof record.slot === "string" && record.slot ? record.slot : "default",
1576
+ configuration: typeof record.configuration === "string" ? record.configuration : null,
1577
+ cacheKey: typeof record.cacheKey === "string" ? record.cacheKey : null,
1578
+ phases: historyPhases(record.phases)
1579
+ };
1580
+ }
1581
+ /** Each platform's recorded runs, newest first, at most `BUILD_HISTORY_LIMIT` each. */
1582
+ function readBuildHistory(state) {
1583
+ const history = state?.[BUILD_HISTORY_KEY];
1584
+ const builds = {};
1585
+ for (const platform of ["ios", "android"]) {
1586
+ const list = history?.[platform];
1587
+ if (!Array.isArray(list)) continue;
1588
+ const entries = list.slice(0, 10).map((value) => historyEntry(platform, value)).filter((entry) => entry !== null);
1589
+ if (entries.length) builds[platform] = entries;
1590
+ }
1591
+ return builds;
1592
+ }
1367
1593
  function readWorkspaceState(root) {
1368
1594
  return readJsonObject(workspaceStateFile(root));
1369
1595
  }
@@ -1391,4 +1617,4 @@ function workspaceLastUsed(root) {
1391
1617
  return lastUseFrom(readWorkspaceState(root), mtimes);
1392
1618
  }
1393
1619
  //#endregion
1394
- export { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, BUILD_PHASES, ERROR_SOURCES, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATUS_ISSUE_CODES, TUNNEL_MODES, acceptsShape, advanceTail, allMetroPorts, attachExpoErrorContext, buildCriteria, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, createdDevicesFile, createdDevicesLock, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, expectedShape, fileSizes, findEnclosingWorktreeRoot, findProjectByMetroPort, followLogs, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, loadConfig, logFiles, markerWindow, parseNdjsonLine, parseNdjsonText, parseSince, queryLogs, readCreatedDevices, readEasSessionLedger, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readNdjsonGenerations, readWorkspaceState, recordMatches, settingDefinition, settingValueError, settingsJsonSchema, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, supervisorLogFile, supervisorPidFile, tailRead, validEasSessionClaim, withConfigLock, workspaceDerivedData, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceStateFile, workspaceStateLock };
1620
+ export { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, ERROR_SOURCES, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATUS_ISSUE_CODES, TUNNEL_MODES, WEB_VIEWPORTS, acceptsShape, advanceTail, allMetroPorts, attachExpoErrorContext, buildCriteria, buildDiagnostics, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, createdDevicesFile, createdDevicesLock, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, expectedShape, fileSizes, findEnclosingWorktreeRoot, findProjectByMetroPort, followLogs, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, loadConfig, logFiles, markerWindow, parseNdjsonLine, parseNdjsonText, parseSince, queryLogs, readBuildHistory, readCreatedDevices, readEasSessionLedger, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readNdjsonGenerations, readStateFile, readWorkspaceState, recordMatches, settingDefinition, settingValueError, settingsJsonSchema, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, supervisorLogFile, supervisorPidFile, tailRead, validEasSessionClaim, withConfigLock, withStateReadCache, workspaceDerivedData, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceStateFile, workspaceStateLock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stim-cli/core",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Shared internal cache and runtime primitives for Stim packages.",
5
5
  "license": "MIT",
6
6
  "repository": {