@stim-cli/core 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -25,6 +25,7 @@ interface AndroidDeviceRecord {
25
25
  deviceName?: string | null;
26
26
  owned?: boolean;
27
27
  setupIncomplete?: boolean;
28
+ bootPending?: boolean;
28
29
  [key: string]: unknown;
29
30
  }
30
31
  type DeviceRecord = IosDeviceRecord | AndroidDeviceRecord;
@@ -111,15 +112,24 @@ declare function allMetroPorts(): number[];
111
112
  declare function findProjectByMetroPort(port: number): string | null;
112
113
  //#endregion
113
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;
114
122
  declare function readJsonFile(path: string): unknown;
115
123
  declare function isJsonObject(value: unknown): value is Record<string, unknown>;
116
124
  declare function readJsonObject(path: string): Record<string, unknown> | null;
117
125
  //#endregion
118
126
  //#region state/ledgers.d.ts
119
- type CreatedDevicePlatform = "ios" | "android";
127
+ type CreatedDevicePlatform = "ios" | "android" | "web";
128
+ /** `web` lists the absolute paths of browser profile directories Stim created. */
120
129
  interface CreatedDevices {
121
130
  ios: ReadonlySet<string>;
122
131
  android: ReadonlySet<string>;
132
+ web: ReadonlySet<string>;
123
133
  }
124
134
  declare function readCreatedDevices(): CreatedDevices;
125
135
  interface EasSessionClaim {
@@ -145,6 +155,8 @@ interface NdjsonRecord {
145
155
  src?: string;
146
156
  level?: string;
147
157
  msg?: string;
158
+ /** With `logs --errors --json`: the code frame and stack lines Expo printed after this error. */
159
+ context?: string[];
148
160
  [key: string]: unknown;
149
161
  }
150
162
  declare const LEVELS: string[];
@@ -166,10 +178,11 @@ interface GrepResult {
166
178
  interface MarkerWindow {
167
179
  launchTs: number | null;
168
180
  bundleTs: number | null;
181
+ pageLoadTs: number | null;
169
182
  }
170
183
  interface QueryCriteria {
171
184
  slot?: string;
172
- includeNativeCrashes?: boolean;
185
+ includeAppDeviceErrors?: boolean;
173
186
  sources?: string[];
174
187
  minLevel?: string;
175
188
  grep?: RegExp;
@@ -201,7 +214,12 @@ declare function buildCriteria({ slot, sources, minLevel, since, grep, errorsOnl
201
214
  declare function logFiles(dir: string): string[];
202
215
  declare function fileSizes(dir: string): Record<string, number>;
203
216
  declare function readLogRecords(dir: string, include?: (name: string) => boolean): NdjsonRecord[];
204
- 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[];
205
223
  declare function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now }?: {
206
224
  slot?: string;
207
225
  dir?: string;
@@ -223,12 +241,14 @@ declare function advanceTail(prev: TailState | null | undefined, chunk: string |
223
241
  state: TailState;
224
242
  records: NdjsonRecord[];
225
243
  };
226
- declare function followLogs({ dir, onRecord, criteria, intervalMs, offsets }: {
244
+ declare function followLogs({ dir, onRecord, criteria, intervalMs, offsets, errorContext }: {
227
245
  dir: string;
228
246
  onRecord: (record: NdjsonRecord) => void;
229
247
  criteria?: QueryCriteria;
230
248
  intervalMs?: number;
231
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;
232
252
  }): () => void;
233
253
  declare function sortByTs(records: NdjsonRecord[]): NdjsonRecord[];
234
254
  //#endregion
@@ -296,6 +316,7 @@ type SettingType = {
296
316
  * `.stim.json` instead of the app's. A `sensitive` value is never printed and is
297
317
  * written to `.stim.json` only as an `env:` or `file:` reference. `scopedHomeValue`
298
318
  * replaces every layer and the default while STIM_HOME is set and `env` is not.
319
+ * `desktopDefault` replaces `default` on macOS while Stim Desktop is installed.
299
320
  */
300
321
  interface SettingDefinition {
301
322
  key: string;
@@ -307,6 +328,7 @@ interface SettingDefinition {
307
328
  sensitive?: boolean;
308
329
  committedAt?: "repository";
309
330
  scopedHomeValue?: number;
331
+ desktopDefault?: string;
310
332
  }
311
333
  interface SettingEntry {
312
334
  key: string;
@@ -318,6 +340,7 @@ interface SettingEntry {
318
340
  value: string;
319
341
  };
320
342
  sensitive?: true;
343
+ defaultReason?: string;
321
344
  }
322
345
  /** The payload `stim settings --json` prints. */
323
346
  interface SettingsPayload {
@@ -334,6 +357,8 @@ interface SettingsPayload {
334
357
  declare const REMOTE_DEVICE_BACKENDS: readonly RemoteDeviceBackend[];
335
358
  declare const IOS_SIMULATOR_APPS: readonly ["xcode", "siniulator", "stim-desktop"];
336
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];
337
362
  declare const SETTINGS: readonly SettingDefinition[];
338
363
  declare function settingDefinition(key: string): SettingDefinition | null;
339
364
  declare function isLayeredSetting(setting: SettingDefinition): boolean;
@@ -382,7 +407,16 @@ interface IdleStopRecord {
382
407
  declare function readIdleStop(state: WorkspaceState | null | undefined): IdleStopRecord | null;
383
408
  /** Each platform's latest run; `lastBuild` holds whichever platform ran last. */
384
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[];
385
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[]>>;
386
420
  declare function readWorkspaceState(root: string): WorkspaceState | null;
387
421
  declare function lastUseFrom(state: WorkspaceState | null, logMtimes: readonly number[]): number;
388
422
  declare function workspaceLastUsed(root: string): number;
@@ -419,6 +453,31 @@ interface LastBuildReport {
419
453
  finishedAt: string | null;
420
454
  errorCode?: string;
421
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;
422
481
  }
423
482
  /** What kind of native input a changed fingerprint source is. */
424
483
  type BuildMissCategory = "native-dependency" | "config-plugin" | "app-config" | "app-asset" | "package" | "native-dir" | "autolinking" | "package-scripts" | "file" | "other";
@@ -430,9 +489,11 @@ interface BuildMissChange {
430
489
  /**
431
490
  * Why a run compiled instead of installing a cached app. `changes` holds at most 20 entries,
432
491
  * ordered by importance; `changeCount` is the full number of changed fingerprint sources.
492
+ * Only a plan reports `prebuild-pending`: the run would prebuild before compiling, so the changes
493
+ * compare the fingerprint before that prebuild.
433
494
  */
434
495
  interface BuildMissReason {
435
- kind: "changed" | "no-baseline" | "same-sources" | "cache-skipped" | "fingerprint-error";
496
+ kind: "changed" | "no-baseline" | "same-sources" | "cache-skipped" | "fingerprint-error" | "prebuild-pending";
436
497
  summary: string;
437
498
  changes: BuildMissChange[];
438
499
  changeCount: number;
@@ -457,6 +518,8 @@ interface BuildPlanPayload {
457
518
  outcome: RunOutcomeKind | null;
458
519
  expectedMs: number | null;
459
520
  basis: number;
521
+ /** On a predicted miss with cache reads on, why the cache has no app; `baseline` omits `cacheKey`. */
522
+ missReason?: BuildMissReason;
460
523
  refusal?: {
461
524
  code: string;
462
525
  message: string;
@@ -475,6 +538,17 @@ interface DeviceActivity {
475
538
  lastActivityAt?: string;
476
539
  basis: string[];
477
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
+ }
478
552
  /**
479
553
  * A linked worktree's `git status`. `changed` counts tracked paths with staged or unstaged changes, including
480
554
  * conflicts; `untracked` counts untracked entries as `git status` lists them, so an untracked directory counts once.
@@ -510,6 +584,38 @@ interface RemoteDeviceState {
510
584
  startedAt: string | null;
511
585
  webPreviewUrl: string | null;
512
586
  }
587
+ /** Every code a status issue can carry. */
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"];
589
+ type StatusIssueCode = (typeof STATUS_ISSUE_CODES)[number];
590
+ /**
591
+ * One thing in a workspace that needs the user. `remedy` is a command to run from `workspace`; `slot` is absent
592
+ * for the default device slot. `warnings` carries the same issues as text.
593
+ */
594
+ interface StatusIssue {
595
+ code: StatusIssueCode;
596
+ severity: "error" | "warning";
597
+ message: string;
598
+ remedy: string;
599
+ workspace: string;
600
+ slot?: string;
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
+ }
513
619
  interface EnvironmentState {
514
620
  slots?: {
515
621
  slot: string;
@@ -518,14 +624,20 @@ interface EnvironmentState {
518
624
  }[];
519
625
  path: string;
520
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
+ */
521
631
  memoryMb: number;
522
632
  warnings: string[];
633
+ issues: StatusIssue[];
523
634
  ios?: {
524
635
  name: string | null;
525
636
  udid: string;
526
637
  owned: boolean;
527
638
  state: string;
528
639
  activity?: DeviceActivity;
640
+ app?: DeviceAppProcess;
529
641
  } | null;
530
642
  android?: {
531
643
  name: string | undefined;
@@ -533,7 +645,9 @@ interface EnvironmentState {
533
645
  physical: boolean;
534
646
  serial?: string | null;
535
647
  state?: AndroidRuntimeFacts["state"];
648
+ deviceProfile?: string | null;
536
649
  activity?: DeviceActivity;
650
+ app?: DeviceAppProcess;
537
651
  } | null;
538
652
  metro?: {
539
653
  port: number;
@@ -541,6 +655,7 @@ interface EnvironmentState {
541
655
  pid: number | null;
542
656
  idleStop?: IdleStopRecord;
543
657
  } | null;
658
+ web?: WebBrowserState | null;
544
659
  supervisor?: {
545
660
  pid: number | null;
546
661
  mode: string | null;
@@ -555,7 +670,10 @@ interface EnvironmentState {
555
670
  remoteDevices?: RemoteDeviceState[];
556
671
  build?: BuildReport | null;
557
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[]>>;
558
675
  }
676
+ /** `committedMb` sums the environments' estimated `memoryMb`; `overCapacity` is that sum over 60% of `totalMemoryMb`. */
559
677
  interface StatusCapacity {
560
678
  liveCount: number;
561
679
  committedMb: number;
@@ -575,6 +693,34 @@ interface DeviceLeaseState {
575
693
  expired: boolean;
576
694
  parsed: boolean;
577
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
+ }
578
724
  /** The payload `stim status --json` prints, and `status --watch --json` prints on each change. */
579
725
  interface StatusPayload {
580
726
  environments: (EnvironmentState & {
@@ -584,6 +730,8 @@ interface StatusPayload {
584
730
  deviceLeases: DeviceLeaseState[];
585
731
  unprovisionedWorktrees: WorktreeFacts[];
586
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;
587
735
  }
588
736
  //#endregion
589
- 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, SettingDefinition, SettingEntry, SettingScope, SettingType, SettingsObject, SettingsPayload, StatsPlatform, StatusCapacity, 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" },
@@ -957,7 +1100,8 @@ const SETTINGS = [
957
1100
  },
958
1101
  scopes: MACHINE,
959
1102
  default: "xcode",
960
- description: "App that displays an owned iOS simulator"
1103
+ desktopDefault: "stim-desktop",
1104
+ description: "App that displays an owned iOS simulator; stim-desktop by default when Stim Desktop is installed"
961
1105
  },
962
1106
  {
963
1107
  key: "androidEmulatorApp",
@@ -967,7 +1111,8 @@ const SETTINGS = [
967
1111
  },
968
1112
  scopes: MACHINE,
969
1113
  default: "emulator",
970
- description: "App that displays an owned Android emulator Stim boots on macOS"
1114
+ desktopDefault: "stim-desktop",
1115
+ description: "App that displays an owned Android emulator Stim boots on macOS; stim-desktop by default when Stim Desktop is installed"
971
1116
  },
972
1117
  {
973
1118
  key: "concurrency.maxBuilds",
@@ -1019,6 +1164,14 @@ const SETTINGS = [
1019
1164
  scopedHomeValue: 0,
1020
1165
  description: "Live workspaces before idle ones are reclaimed; unset or 0 means no limit"
1021
1166
  },
1167
+ {
1168
+ key: "gc.worktreeGraceMinutes",
1169
+ type: CAPACITY,
1170
+ scopes: MACHINE,
1171
+ default: 120,
1172
+ env: "STIM_GC_WORKTREE_GRACE_MINUTES",
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"
1174
+ },
1022
1175
  {
1023
1176
  key: "pool.iosParkedMax",
1024
1177
  type: PARKED_MAX,
@@ -1249,6 +1402,42 @@ const BUILD_PHASES = [
1249
1402
  "install",
1250
1403
  "launch"
1251
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
+ ];
1418
+ /** Every code a status issue can carry. */
1419
+ const STATUS_ISSUE_CODES = [
1420
+ "port-not-ours",
1421
+ "sim-missing",
1422
+ "sim-without-metro",
1423
+ "avd-serial-changed",
1424
+ "avd-missing",
1425
+ "avd-not-detected",
1426
+ "avd-unchecked",
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"
1440
+ ];
1252
1441
  //#endregion
1253
1442
  //#region state/workspace-state.ts
1254
1443
  const IDLE_STOP_KEY = "devServerStop";
@@ -1266,6 +1455,8 @@ const LAST_BUILD_KEYS = {
1266
1455
  ios: "lastIosBuild",
1267
1456
  android: "lastAndroidBuild"
1268
1457
  };
1458
+ const BUILD_HISTORY_KEY = "buildHistory";
1459
+ const BUILD_HISTORY_LIMIT = 10;
1269
1460
  const MISS_KINDS = /* @__PURE__ */ new Set([
1270
1461
  "changed",
1271
1462
  "no-baseline",
@@ -1314,6 +1505,25 @@ function missReason(value) {
1314
1505
  rekeyedBy: Array.isArray(record.rekeyedBy) ? record.rekeyedBy.filter((step) => typeof step === "string") : []
1315
1506
  };
1316
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
+ }
1317
1527
  function lastBuildReport(platform, value) {
1318
1528
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1319
1529
  const record = value;
@@ -1322,6 +1532,7 @@ function lastBuildReport(platform, value) {
1322
1532
  const durationMs = typeof record.durationMs === "number" && record.durationMs >= 0 ? record.durationMs : null;
1323
1533
  const finished = new Date(Date.parse(record.startedAt) + (durationMs ?? NaN));
1324
1534
  const reason = record.cacheHit === "local" || record.cacheHit === "remote" ? null : missReason(record.missReason);
1535
+ const diagnostics = record.status === "failed" ? buildDiagnostics(record.diagnostics) : [];
1325
1536
  return {
1326
1537
  platform,
1327
1538
  status: record.status,
@@ -1332,7 +1543,8 @@ function lastBuildReport(platform, value) {
1332
1543
  startedAt: record.startedAt,
1333
1544
  finishedAt: Number.isNaN(finished.getTime()) ? null : finished.toISOString(),
1334
1545
  ...typeof record.errorCode === "string" ? { errorCode: record.errorCode } : {},
1335
- ...reason ? { missReason: reason } : {}
1546
+ ...reason ? { missReason: reason } : {},
1547
+ ...diagnostics.length ? { diagnostics } : {}
1336
1548
  };
1337
1549
  }
1338
1550
  function readLastBuilds(state) {
@@ -1343,6 +1555,41 @@ function readLastBuilds(state) {
1343
1555
  }
1344
1556
  return reports;
1345
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
+ }
1346
1593
  function readWorkspaceState(root) {
1347
1594
  return readJsonObject(workspaceStateFile(root));
1348
1595
  }
@@ -1370,4 +1617,4 @@ function workspaceLastUsed(root) {
1370
1617
  return lastUseFrom(readWorkspaceState(root), mtimes);
1371
1618
  }
1372
1619
  //#endregion
1373
- 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, 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.11.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": {