@stim-cli/core 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/state.d.mts CHANGED
@@ -72,6 +72,7 @@ interface StimConfig {
72
72
  repos: Record<string, RepoRecord>;
73
73
  tempDir?: unknown;
74
74
  iosSimulatorApp?: unknown;
75
+ androidEmulatorApp?: unknown;
75
76
  optimizations?: unknown;
76
77
  concurrency?: {
77
78
  maxBuilds?: unknown;
@@ -249,6 +250,7 @@ declare function sharedCcache(): string;
249
250
  declare function sharedGradle(): string;
250
251
  declare function sharedPods(): string;
251
252
  declare function createdDevicesFile(): string;
253
+ declare function gitMergeCacheDir(): string;
252
254
  declare function createdDevicesLock(): string;
253
255
  /** The EAS session ledger lives under the real home directory, never under STIM_HOME. */
254
256
  declare function easMachineStateRoot(): string;
@@ -331,6 +333,7 @@ interface SettingsPayload {
331
333
  }
332
334
  declare const REMOTE_DEVICE_BACKENDS: readonly RemoteDeviceBackend[];
333
335
  declare const IOS_SIMULATOR_APPS: readonly ["xcode", "siniulator", "stim-desktop"];
336
+ declare const ANDROID_EMULATOR_APPS: readonly ["emulator", "stim-desktop"];
334
337
  declare const SETTINGS: readonly SettingDefinition[];
335
338
  declare function settingDefinition(key: string): SettingDefinition | null;
336
339
  declare function isLayeredSetting(setting: SettingDefinition): boolean;
@@ -377,6 +380,9 @@ interface IdleStopRecord {
377
380
  idleMinutes: number;
378
381
  }
379
382
  declare function readIdleStop(state: WorkspaceState | null | undefined): IdleStopRecord | null;
383
+ /** Each platform's latest run; `lastBuild` holds whichever platform ran last. */
384
+ declare const LAST_BUILD_KEYS: Readonly<Record<StatsPlatform, string>>;
385
+ declare function readLastBuilds(state: WorkspaceState | null | undefined): Partial<Record<StatsPlatform, LastBuildReport>>;
380
386
  declare function readWorkspaceState(root: string): WorkspaceState | null;
381
387
  declare function lastUseFrom(state: WorkspaceState | null, logMtimes: readonly number[]): number;
382
388
  declare function workspaceLastUsed(root: string): number;
@@ -399,6 +405,64 @@ interface BuildReport {
399
405
  expectedPhaseMs: number | null;
400
406
  basis: number;
401
407
  }
408
+ /** Where a build's app came from: a cache tier, or `false` when it compiled or failed before one was found. */
409
+ type BuildCacheHit = "local" | "remote" | false;
410
+ /** A platform's most recent `ios` or `android` run in one workspace. */
411
+ interface LastBuildReport {
412
+ platform: StatsPlatform;
413
+ status: "ok" | "failed";
414
+ cacheHit: BuildCacheHit;
415
+ cacheSkipped: boolean;
416
+ durationMs: number | null;
417
+ fingerprint: string | null;
418
+ startedAt: string;
419
+ finishedAt: string | null;
420
+ errorCode?: string;
421
+ missReason?: BuildMissReason;
422
+ }
423
+ /** What kind of native input a changed fingerprint source is. */
424
+ type BuildMissCategory = "native-dependency" | "config-plugin" | "app-config" | "app-asset" | "package" | "native-dir" | "autolinking" | "package-scripts" | "file" | "other";
425
+ interface BuildMissChange {
426
+ source: string;
427
+ change: "added" | "removed" | "changed";
428
+ category: BuildMissCategory;
429
+ }
430
+ /**
431
+ * Why a run compiled instead of installing a cached app. `changes` holds at most 20 entries,
432
+ * ordered by importance; `changeCount` is the full number of changed fingerprint sources.
433
+ */
434
+ interface BuildMissReason {
435
+ kind: "changed" | "no-baseline" | "same-sources" | "cache-skipped" | "fingerprint-error";
436
+ summary: string;
437
+ changes: BuildMissChange[];
438
+ changeCount: number;
439
+ /** `cacheKey` is kept in workspace state so a later miss can find this baseline; status omits it. */
440
+ baseline: {
441
+ fingerprint: string;
442
+ cacheKey?: string;
443
+ from: "workspace" | "project";
444
+ } | null;
445
+ rekeyedBy: string[];
446
+ }
447
+ /** The payload `stim ios --plan --json` and `stim android --plan --json` print. */
448
+ interface BuildPlanPayload {
449
+ platform: StatsPlatform;
450
+ slot?: string;
451
+ fingerprint: string;
452
+ cacheKey: string | null;
453
+ cacheHit: BuildCacheHit;
454
+ provider: string | null;
455
+ cacheSkipped: boolean;
456
+ prebuild: "none" | "generate" | "regenerate" | "refuse" | null;
457
+ outcome: RunOutcomeKind | null;
458
+ expectedMs: number | null;
459
+ basis: number;
460
+ refusal?: {
461
+ code: string;
462
+ message: string;
463
+ remedy: string;
464
+ };
465
+ }
402
466
  type ActivityState = "driven" | "active" | "idle" | "unknown";
403
467
  interface ActivityDriver {
404
468
  tool: string;
@@ -411,10 +475,27 @@ interface DeviceActivity {
411
475
  lastActivityAt?: string;
412
476
  basis: string[];
413
477
  }
478
+ /**
479
+ * A linked worktree's `git status`. `changed` counts tracked paths with staged or unstaged changes, including
480
+ * conflicts; `untracked` counts untracked entries as `git status` lists them, so an untracked directory counts once.
481
+ * `ahead` and `behind` compare HEAD with `upstream`, and are null when there is no upstream or it no longer exists.
482
+ * `mergedInto` names the default branch (`origin/main`) when `gc` would consider the branch merged into it, judged
483
+ * from local refs without fetching.
484
+ */
485
+ interface WorktreeGit {
486
+ changed: number;
487
+ untracked: number;
488
+ upstream: string | null;
489
+ ahead: number | null;
490
+ behind: number | null;
491
+ mergedInto: string | null;
492
+ }
414
493
  interface WorktreeFacts {
415
494
  path: string;
416
495
  branch?: string;
417
496
  repository?: string;
497
+ /** Null when git could not be read in time, or the worktree is in a folder status does not open. */
498
+ git?: WorktreeGit | null;
418
499
  }
419
500
  interface AndroidRuntimeFacts {
420
501
  serial: string | null;
@@ -473,6 +554,7 @@ interface EnvironmentState {
473
554
  worktree?: WorktreeFacts | null;
474
555
  remoteDevices?: RemoteDeviceState[];
475
556
  build?: BuildReport | null;
557
+ lastBuilds?: Partial<Record<StatsPlatform, LastBuildReport>>;
476
558
  }
477
559
  interface StatusCapacity {
478
560
  liveCount: number;
@@ -504,4 +586,4 @@ interface StatusPayload {
504
586
  simctlAvailable: boolean;
505
587
  }
506
588
  //#endregion
507
- export { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_PCH_CHOICES, ActiveBuildState, ActivityDriver, AndroidRuntimeFacts, BUILD_PHASES, BuildPhase, BuildReport, ConcurrencyLimits, Config, CreatedDevicePlatform, CreatedDevices, DeviceActivity, DeviceLeaseState, DeviceRecord, DoctorRunRecord, ERROR_SOURCES, EasSessionClaim, EasSessionLedgerRead, EnvironmentState, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, IdleStopRecord, LEVELS, 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, 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, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, loadConfig, logFiles, markerWindow, parseNdjsonLine, parseNdjsonText, parseSince, queryLogs, readCreatedDevices, readEasSessionLedger, readIdleStop, readJsonFile, readJsonObject, 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 };
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 };
package/dist/state.mjs CHANGED
@@ -71,6 +71,9 @@ function sharedPods() {
71
71
  function createdDevicesFile() {
72
72
  return join(configDir(), "created-devices.json");
73
73
  }
74
+ function gitMergeCacheDir() {
75
+ return join(configDir(), "git-merge");
76
+ }
74
77
  function createdDevicesLock() {
75
78
  return join(configDir(), "created-devices.lock");
76
79
  }
@@ -302,7 +305,8 @@ const SOURCES = [
302
305
  "metro",
303
306
  "client",
304
307
  "device",
305
- "build"
308
+ "build",
309
+ "agent"
306
310
  ];
307
311
  function levelRank(level) {
308
312
  const i = LEVELS.indexOf(level);
@@ -669,6 +673,7 @@ const IOS_SIMULATOR_APPS = [
669
673
  "siniulator",
670
674
  "stim-desktop"
671
675
  ];
676
+ const ANDROID_EMULATOR_APPS = ["emulator", "stim-desktop"];
672
677
  const PROJECT = [
673
678
  "workspace",
674
679
  "repo",
@@ -817,6 +822,13 @@ const SETTINGS = [
817
822
  scopes: PROJECT,
818
823
  description: "SDK system image for owned AVDs"
819
824
  },
825
+ {
826
+ key: "android.deviceProfile",
827
+ type: STRING,
828
+ scopes: EVERY,
829
+ default: "pixel_6",
830
+ description: "avdmanager hardware profile id for new owned AVDs"
831
+ },
820
832
  {
821
833
  key: "android.dataPartitionSizeGb",
822
834
  type: {
@@ -947,6 +959,16 @@ const SETTINGS = [
947
959
  default: "xcode",
948
960
  description: "App that displays an owned iOS simulator"
949
961
  },
962
+ {
963
+ key: "androidEmulatorApp",
964
+ type: {
965
+ kind: "choice",
966
+ choices: ANDROID_EMULATOR_APPS
967
+ },
968
+ scopes: MACHINE,
969
+ default: "emulator",
970
+ description: "App that displays an owned Android emulator Stim boots on macOS"
971
+ },
950
972
  {
951
973
  key: "concurrency.maxBuilds",
952
974
  type: CAPACITY,
@@ -1239,6 +1261,88 @@ function readIdleStop(state) {
1239
1261
  idleMinutes: record.idleMinutes
1240
1262
  };
1241
1263
  }
1264
+ /** Each platform's latest run; `lastBuild` holds whichever platform ran last. */
1265
+ const LAST_BUILD_KEYS = {
1266
+ ios: "lastIosBuild",
1267
+ android: "lastAndroidBuild"
1268
+ };
1269
+ const MISS_KINDS = /* @__PURE__ */ new Set([
1270
+ "changed",
1271
+ "no-baseline",
1272
+ "same-sources",
1273
+ "cache-skipped",
1274
+ "fingerprint-error"
1275
+ ]);
1276
+ const MISS_CATEGORIES = /* @__PURE__ */ new Set([
1277
+ "native-dependency",
1278
+ "config-plugin",
1279
+ "app-config",
1280
+ "app-asset",
1281
+ "package",
1282
+ "native-dir",
1283
+ "autolinking",
1284
+ "package-scripts",
1285
+ "file",
1286
+ "other"
1287
+ ]);
1288
+ const MISS_CHANGE_CAP = 20;
1289
+ function missChange(value) {
1290
+ if (!value || typeof value !== "object") return null;
1291
+ const { source, change, category } = value;
1292
+ if (typeof source !== "string" || change !== "added" && change !== "removed" && change !== "changed") return null;
1293
+ return {
1294
+ source,
1295
+ change,
1296
+ category: typeof category === "string" && MISS_CATEGORIES.has(category) ? category : "other"
1297
+ };
1298
+ }
1299
+ function missReason(value) {
1300
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1301
+ const record = value;
1302
+ if (typeof record.kind !== "string" || !MISS_KINDS.has(record.kind) || typeof record.summary !== "string") return null;
1303
+ const changes = (Array.isArray(record.changes) ? record.changes : []).map(missChange).filter((change) => change !== null).slice(0, MISS_CHANGE_CAP);
1304
+ const baseline = record.baseline;
1305
+ return {
1306
+ kind: record.kind,
1307
+ summary: record.summary,
1308
+ changes,
1309
+ changeCount: Number.isInteger(record.changeCount) && record.changeCount >= changes.length ? record.changeCount : changes.length,
1310
+ baseline: baseline && typeof baseline.fingerprint === "string" && (baseline.from === "workspace" || baseline.from === "project") ? {
1311
+ fingerprint: baseline.fingerprint,
1312
+ from: baseline.from
1313
+ } : null,
1314
+ rekeyedBy: Array.isArray(record.rekeyedBy) ? record.rekeyedBy.filter((step) => typeof step === "string") : []
1315
+ };
1316
+ }
1317
+ function lastBuildReport(platform, value) {
1318
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1319
+ const record = value;
1320
+ if (record.platform !== platform || record.status !== "ok" && record.status !== "failed") return null;
1321
+ if (typeof record.startedAt !== "string") return null;
1322
+ const durationMs = typeof record.durationMs === "number" && record.durationMs >= 0 ? record.durationMs : null;
1323
+ const finished = new Date(Date.parse(record.startedAt) + (durationMs ?? NaN));
1324
+ const reason = record.cacheHit === "local" || record.cacheHit === "remote" ? null : missReason(record.missReason);
1325
+ return {
1326
+ platform,
1327
+ status: record.status,
1328
+ cacheHit: record.cacheHit === "local" || record.cacheHit === "remote" ? record.cacheHit : false,
1329
+ cacheSkipped: record.cacheSkipped === true,
1330
+ durationMs,
1331
+ fingerprint: typeof record.fingerprint === "string" ? record.fingerprint : null,
1332
+ startedAt: record.startedAt,
1333
+ finishedAt: Number.isNaN(finished.getTime()) ? null : finished.toISOString(),
1334
+ ...typeof record.errorCode === "string" ? { errorCode: record.errorCode } : {},
1335
+ ...reason ? { missReason: reason } : {}
1336
+ };
1337
+ }
1338
+ function readLastBuilds(state) {
1339
+ const reports = {};
1340
+ for (const platform of ["ios", "android"]) {
1341
+ const [report] = [lastBuildReport(platform, state?.[LAST_BUILD_KEYS[platform]]), lastBuildReport(platform, state?.lastBuild)].filter((candidate) => candidate !== null).toSorted((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0));
1342
+ if (report) reports[platform] = report;
1343
+ }
1344
+ return reports;
1345
+ }
1242
1346
  function readWorkspaceState(root) {
1243
1347
  return readJsonObject(workspaceStateFile(root));
1244
1348
  }
@@ -1266,4 +1370,4 @@ function workspaceLastUsed(root) {
1266
1370
  return lastUseFrom(readWorkspaceState(root), mtimes);
1267
1371
  }
1268
1372
  //#endregion
1269
- export { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_PCH_CHOICES, BUILD_PHASES, ERROR_SOURCES, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, 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, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, lastUseFrom, levelRank, loadConfig, logFiles, markerWindow, parseNdjsonLine, parseNdjsonText, parseSince, queryLogs, readCreatedDevices, readEasSessionLedger, readIdleStop, readJsonFile, readJsonObject, 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 };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stim-cli/core",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Shared internal cache and runtime primitives for Stim packages.",
5
5
  "license": "MIT",
6
6
  "repository": {