stim 1.10.1 → 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.
Files changed (58) hide show
  1. package/dist/{build-progress-BXx-ZBe4.mjs → activity-BtG4Va9l.mjs} +8 -182
  2. package/dist/{android-DPov-fv2.mjs → android-BA2i_KAV.mjs} +1 -1
  3. package/dist/{android-Bu4mbDzc.mjs → android-C1q4l523.mjs} +376 -99
  4. package/dist/{android-787HhP5w.mjs → android-C4zG2Tuc.mjs} +63 -18
  5. package/dist/{android-cas-CITmIeWY.mjs → android-cas-DfJkwCXW.mjs} +3 -3
  6. package/dist/{app-install-Cfmqr3TQ.mjs → app-install-DHut6JnV.mjs} +1 -1
  7. package/dist/{budget-nDCEwtJv.mjs → budget-De3P2rq_.mjs} +9 -9
  8. package/dist/{dev-client-DExzPiOa.mjs → build-plan-CeAYgKAx.mjs} +415 -24
  9. package/dist/build-progress-CDwogz6R.mjs +179 -0
  10. package/dist/cli.mjs +14 -14
  11. package/dist/collector-run.mjs +1 -1
  12. package/dist/{deps-DP4eWqea.mjs → deps-OF7CD5Dx.mjs} +2 -2
  13. package/dist/{device-Czh5kO8A.mjs → device-BwH2iI3O.mjs} +47 -9
  14. package/dist/{device-lease-CbEaIk7b.mjs → device-lease-D83nqeMX.mjs} +51 -51
  15. package/dist/{device-pool-BBWLAr55.mjs → device-pool-DHj7cdPy.mjs} +2 -2
  16. package/dist/{device-remote-powUHdbK.mjs → device-remote-BQZSnnmE.mjs} +7 -7
  17. package/dist/{doctor-CmqK1LcQ.mjs → doctor-D8QdMRMf.mjs} +12 -12
  18. package/dist/{error-diagnostics-xxWKNO3-.mjs → error-diagnostics-UYFSQwRy.mjs} +3 -3
  19. package/dist/{gc-zCDpmdHU.mjs → gc-COZteyiq.mjs} +27 -253
  20. package/dist/{guide-DwmaD_Wa.mjs → guide-NDvTmMi-.mjs} +336 -48
  21. package/dist/{idle--jn7pBNe.mjs → idle-NETbeNiQ.mjs} +4 -3
  22. package/dist/{in-use-CNIWU-r-.mjs → in-use-DVhlSoyn.mjs} +4 -4
  23. package/dist/{ios-CbdqTqOc.mjs → ios-CQLKpNfr.mjs} +294 -81
  24. package/dist/{ios-COGcGEcN.mjs → ios-DArbmwer.mjs} +2 -1
  25. package/dist/{ios-device-DNjPXBnb.mjs → ios-device-CwDZo0X4.mjs} +1 -1
  26. package/dist/{launch-verify-BhgkYAws.mjs → launch-verify-CuPDGXdr.mjs} +1 -1
  27. package/dist/logs-B44a1UeU.mjs +485 -0
  28. package/dist/merge-state-DDxS-XIZ.mjs +232 -0
  29. package/dist/{named-ports-DiTfSz6p.mjs → named-ports-Dc7neAlc.mjs} +1 -1
  30. package/dist/native-run-D7yB_AsB.mjs +143 -0
  31. package/dist/{ownership-BfPNRf3-.mjs → ownership-BiojdtAp.mjs} +3 -3
  32. package/dist/{ports-CLSSFOuz.mjs → ports-DwBaRFJs.mjs} +1 -1
  33. package/dist/{reload-CymyF6Kk.mjs → reload-By9Dwsrt.mjs} +4 -4
  34. package/dist/{server-bare-DVbb7kXH.mjs → server-bare-D_RK1RCj.mjs} +1 -1
  35. package/dist/server-expo-B-CCUJZj.mjs +2 -0
  36. package/dist/{server-expo-BvOCeakp.mjs → server-expo-o6H5gcBc.mjs} +1 -1
  37. package/dist/{settings-DOugPlPz.mjs → settings-B5oFZEri.mjs} +12 -3
  38. package/dist/{settings-EWYNwe72.mjs → settings-C0ugALSC.mjs} +1 -1
  39. package/dist/settings.schema.json +52 -0
  40. package/dist/{simslim-DZUxzsjq.mjs → simslim-CIR3IYJO.mjs} +150 -95
  41. package/dist/{slot-launch-Dk4oTREM.mjs → slot-launch-rggO1Esw.mjs} +2 -2
  42. package/dist/{spawn-claims-CUleC2cM.mjs → spawn-claims-BJwXTMPt.mjs} +24 -1
  43. package/dist/{start-RbmFPeZ7.mjs → start-BSsf2tUs.mjs} +4 -4
  44. package/dist/{start-qWapeB4W.mjs → start-i8zc9VmS.mjs} +1 -1
  45. package/dist/{status-Zp8kvexe.mjs → status-CitcBRsC.mjs} +227 -22
  46. package/dist/{status-Dr4fRkym.mjs → status-Q-WDEGCB.mjs} +20 -6
  47. package/dist/{stop-DcEv5sk-.mjs → stop-DIhrvu0B.mjs} +188 -27
  48. package/dist/stop-rst5mL14.mjs +3 -0
  49. package/dist/supervisor-run.mjs +7 -7
  50. package/dist/{workspace-process-lock-BXA96myD.mjs → workspace-process-lock-Cx74RSDZ.mjs} +9 -4
  51. package/dist/{workspaces-wrH6dXfu.mjs → workspaces-COD2xCz-.mjs} +15 -5
  52. package/dist/{worktree-DEG2o9F8.mjs → worktree-DhxDTK44.mjs} +1 -1
  53. package/dist/{worktree-Bd4dRvx7.mjs → worktree-KOjMX3zA.mjs} +8 -8
  54. package/dist/{xcode-B2tBkf0m.mjs → xcode-B5SPD--o.mjs} +3 -3
  55. package/package.json +5 -5
  56. package/dist/logs-MARIy6m9.mjs +0 -209
  57. package/dist/server-expo-DBVTdult.mjs +0 -2
  58. package/dist/stop-DI4BG4Os.mjs +0 -3
@@ -1,36 +1,37 @@
1
- import { L as deviceSlotFileKey, R as deviceSlotKey, S as setDevice, T as withConfigLock, V as parseDeviceSlotOption, W as validateDeviceSlot, f as getProject, g as loadConfig, k as ownership_claim_exports, l as getConcurrencyLimits, w as upsertProject, x as saveConfig, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
1
+ import { L as deviceSlotFileKey, R as deviceSlotKey, S as setDevice, T as withConfigLock$1, V as parseDeviceSlotOption, W as validateDeviceSlot, f as getProject, g as loadConfig$1, k as ownership_claim_exports, l as getConcurrencyLimits, w as upsertProject, x as saveConfig, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
3
  import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
4
- import { O as workspaceDir, b as emulatorLogFile, c as signalProcessTree, h as recordWorkspaceUse, i as pidExists, j as workspaceLogsDir, m as readWorkspaceState, s as resolveProjectMetro, y as writeWorkspaceState } from "./metro-BWLOJ_Wr.mjs";
4
+ import { O as workspaceDir, b as emulatorLogFile, c as signalProcessTree, h as recordWorkspaceUse, i as pidExists, j as workspaceLogsDir, m as readWorkspaceState$1, s as resolveProjectMetro, y as writeWorkspaceState } from "./metro-BWLOJ_Wr.mjs";
5
5
  import { D as makeTemporaryDirectory, O as removeTemporaryEntry, s as gitCommonDir, y as repoRoot } from "./worktree-BlizKDpZ.mjs";
6
- import { a as detectIsExpo, i as detectBundleId, l as projectShortcut, n as appProjectProblem, o as findProjectRoot, r as detectAndroidPackage } from "./project-BXZzOABt.mjs";
6
+ import { a as detectIsExpo, i as detectBundleId, l as projectShortcut, n as appProjectProblem, o as findProjectRoot, r as detectAndroidPackage, t as NO_PROJECT_REFUSAL } from "./project-BXZzOABt.mjs";
7
7
  import { n as MODE_EXPO, p as writeWorkspaceLaunch, t as MODE_BARE } from "./state-WGdIbi_d.mjs";
8
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BXA96myD.mjs";
9
8
  import { r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
10
9
  import { _ as stepTimer, c as launchErrorReport, f as refuseNoProject, g as stepClock, i as formatDuration, l as phaseLine, m as shortHash, n as appReadinessMessage, v as launchErrorPreview } from "./command-output-D5z2_bcF.mjs";
11
- import { A as resolveBuild, C as fingerprintDiffRecord, D as providerDownloadPath, E as prepareProviderDownloadDir, F as assetDiffReason, I as captureAssetManifest, L as compareAssetManifests, M as storedAssetManifest, N as untrackedMissLine, O as providerUploadOutcome, P as untrackedNativeFiles, R as readAssetManifest, S as filesystemBuildCapability, T as fingerprintProject, _ as waitForBuild, b as describeFingerprintMiss, g as releaseBuildLock, j as storeBuild, k as refingerprintAfterMutation, m as acquireBuildLock, v as waitForSharedBuild, w as fingerprintDiffSuffix, y as buildCacheKey } from "./device-lease-CbEaIk7b.mjs";
12
- import { n as NO_BUILD_PROGRESS, s as startBuildProgress } from "./build-progress-BXx-ZBe4.mjs";
13
- import { B as resolvePhysicalDevice, E as listInstalledSystemImages, F as physicalDeviceModel, L as probeEmulatorSerial, O as memoizeEmulatorProbe, R as resetAdoptedAvd, W as waitForBoot, _ as emulatorDiskSpaceRemedy, a as androidPoolNoCandidatesRefusal, b as findBuildTool, i as androidPoolCandidates, n as androidDeviceAbi, o as androidSystemImageAbi, r as androidHome, s as androidToolCwd, t as androidBuildToolName, v as emulatorFailureRemedy, w as listAdbDevices, y as extractEmulatorFailure, z as resolveOwnedAvdSerial } from "./android-787HhP5w.mjs";
14
- import { A as resolveCacheProviderConfig, E as publicUrlSetting, H as compilerCacheFallbackMessage, I as tunnelModeSetting, L as unknownSettingKeys, M as settingFile, O as remoteAndroidSetting, P as settingShapeErrors, S as metroWarmupUrlSetting, U as optimizationBuildProfile, V as artifactCachePolicy, W as resolveOptimizations, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, j as resolveSettings, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, z as detectProviders } from "./settings-DOugPlPz.mjs";
15
- import { y as parkedMaxSetting } from "./ownership-BfPNRf3-.mjs";
16
- import { a as acquireBuildSlot, s as releaseBuildSlot } from "./in-use-CNIWU-r-.mjs";
10
+ import { A as resolveBuild, C as filesystemBuildCapability, D as providerDownloadPath, E as prepareProviderDownloadDir, F as untrackedNativeFiles, I as assetDiffReason, L as captureAssetManifest, M as storedAssetManifest, O as providerUploadOutcome, P as untrackedMissLine, R as compareAssetManifests, T as fingerprintProject, _ as waitForBuild, b as buildCacheKey, g as releaseBuildLock, j as storeBuild, k as refingerprintAfterMutation, m as acquireBuildLock, v as waitForSharedBuild, w as fingerprintDiffRecord, z as readAssetManifest } from "./device-lease-D83nqeMX.mjs";
11
+ import { A as memoizeEmulatorProbe, B as probeEmulatorSerial, C as hostSystemImageArch, D as listAvds, E as listAvdDeviceProfiles, H as resolveOwnedAvdSerial, L as ownedAvdSystemImage, N as ownedAvdDirectory, O as listInstalledSystemImages, Q as configuredAndroidEmulatorApp, R as physicalDeviceModel, T as listAdbDevices, U as resolvePhysicalDevice, V as resetAdoptedAvd, a as androidPoolCandidates, b as extractEmulatorFailure, c as androidToolCwd, i as androidHome, n as androidBuildToolName, o as androidPoolNoCandidatesRefusal, q as waitForBoot, r as androidDeviceAbi, s as androidSystemImageAbi, v as emulatorDiskSpaceRemedy, x as findBuildTool, y as emulatorFailureRemedy, z as pickDefaultSystemImage } from "./android-C4zG2Tuc.mjs";
12
+ import { A as resolveCacheProviderConfig, E as publicUrlSetting, H as compilerCacheFallbackMessage, I as tunnelModeSetting, L as unknownSettingKeys, M as settingFile, O as remoteAndroidSetting, P as settingShapeErrors, S as metroWarmupUrlSetting, U as optimizationBuildProfile, V as artifactCachePolicy, W as resolveOptimizations, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, j as resolveSettings, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, z as detectProviders } from "./settings-B5oFZEri.mjs";
13
+ import { y as parkedMaxSetting } from "./ownership-BiojdtAp.mjs";
14
+ import { c as startBuildProgress, n as NO_BUILD_PROGRESS } from "./build-progress-CDwogz6R.mjs";
15
+ import { a as acquireBuildSlot, s as releaseBuildSlot } from "./in-use-DVhlSoyn.mjs";
17
16
  import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-D2xbLVoz.mjs";
18
- import { t as cleanLine } from "./server-expo-BvOCeakp.mjs";
19
- import { A as UPLOAD_TIMEOUT_MS, B as resolveRemote, F as isEasAuthFailureText, I as loadProjectProvider, M as checkEasAuth, N as easAuthNote, P as exitAfterFlush, V as uploadRemote, a as ensureMetroReachable, c as remoteAndroidDeps, j as cacheLevel, k as RESOLVE_TIMEOUT_MS, n as binOnPath, o as ensureRemoteBootOwned, u as resolveRemoteContext, w as ownedSessionName, z as resolveEasCliBin } from "./device-remote-powUHdbK.mjs";
20
- import { _ as launchAndroidReleaseApp, d as deviceShellArg, f as installAndroidApp, g as launchAndroidApp, o as androidAppProcess, p as installConflictKind, r as DEFAULT_METRO_PORT, s as androidDevClientUrl, t as ADB_INSTALL_TIMEOUT_MS } from "./app-install-Cfmqr3TQ.mjs";
21
- import { a as VERIFY_TIMEOUT_MS, d as verifyLaunch, i as RELEASE_VERIFY_WAIT_MS, l as unverifiedLaunchLines, n as LAUNCH_FATAL, r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING, u as verifyAndroidReleaseLaunch } from "./launch-verify-BhgkYAws.mjs";
22
- import { d as isEasBuildFailure, f as resolveEasDevelopmentBuild, o as unknownAndroidSystemImageRefusal, r as checkDeviceCapacity } from "./simslim-DZUxzsjq.mjs";
23
- import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-BBWLAr55.mjs";
24
- import { _ as isPhysicalDeviceRequest, b as warmMetro, c as runPrebuild, d as ensureOwnedDevice, f as AvdBootError, g as ensureWorkspaceStorageSafely, h as ensureDevServer, i as detectEntryFile, l as staleNativeDirRefusal, n as pickDevClientScheme, o as planPrebuild, p as AvdRecoveryError, s as recordPrebuild, t as devClientScheme, u as ensureBooted, v as launchOutcomeRecord } from "./dev-client-DExzPiOa.mjs";
25
- import { t as budgetGate } from "./budget-nDCEwtJv.mjs";
26
- import { f as spawnEntry, s as startDevServer } from "./start-RbmFPeZ7.mjs";
17
+ import { t as cleanLine } from "./server-expo-o6H5gcBc.mjs";
18
+ import { A as UPLOAD_TIMEOUT_MS, B as resolveRemote, F as isEasAuthFailureText, I as loadProjectProvider, M as checkEasAuth, N as easAuthNote, P as exitAfterFlush, V as uploadRemote, a as ensureMetroReachable, c as remoteAndroidDeps, j as cacheLevel, k as RESOLVE_TIMEOUT_MS, n as binOnPath, o as ensureRemoteBootOwned, u as resolveRemoteContext, w as ownedSessionName, z as resolveEasCliBin } from "./device-remote-BQZSnnmE.mjs";
19
+ import { _ as launchAndroidReleaseApp, d as deviceShellArg, f as installAndroidApp, g as launchAndroidApp, o as androidAppProcess, p as installConflictKind, r as DEFAULT_METRO_PORT, s as androidDevClientUrl, t as ADB_INSTALL_TIMEOUT_MS } from "./app-install-DHut6JnV.mjs";
20
+ import { a as VERIFY_TIMEOUT_MS, d as verifyLaunch, i as RELEASE_VERIFY_WAIT_MS, l as unverifiedLaunchLines, n as LAUNCH_FATAL, r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING, u as verifyAndroidReleaseLaunch } from "./launch-verify-CuPDGXdr.mjs";
21
+ import { f as isEasBuildFailure, m as resolveEasDevelopmentBuild, o as unknownAndroidDeviceProfileRefusal, p as planEasDevelopmentBuild, r as checkDeviceCapacity, s as unknownAndroidSystemImageRefusal } from "./simslim-CIR3IYJO.mjs";
22
+ import { d as withNativeBuildRun, r as cancelledFailure, u as runCancellation } from "./native-run-D7yB_AsB.mjs";
23
+ import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-DHj7cdPy.mjs";
24
+ import { C as ensureDevServer, E as launchOutcomeRecord, O as warmMetro, T as isPhysicalDeviceRequest, _ as staleNativeDirRefusal, a as refusePlan, b as AvdBootError, c as skippedMissReason, f as detectEntryFile, g as runPrebuild, h as recordPrebuild, i as printPlan, l as devClientScheme, m as planPrebuild, n as planFlagRefusal, o as explainBuildMiss, r as planPayload, s as fingerprintErrorMissReason, t as planCachedBuild, u as pickDevClientScheme, v as ensureBooted, w as ensureWorkspaceStorageSafely, x as AvdRecoveryError, y as ensureOwnedDevice } from "./build-plan-CeAYgKAx.mjs";
25
+ import { t as budgetGate } from "./budget-De3P2rq_.mjs";
26
+ import { f as spawnEntry, s as startDevServer } from "./start-BSsf2tUs.mjs";
27
27
  import { a as statsProjectKey, i as recordRunStats, n as readRunEstimates, t as createRunRecorder } from "./stats-BAgJaLA4.mjs";
28
- import { a as buildAndroid, c as CCACHE_NOT_RUN, f as resolveCcache, l as CCACHE_UNAVAILABLE, n as resolveAndroidCas, o as productFlavorRefusal, p as formatDiagnostic, r as resolveAndroidCompilerCache, s as readProductFlavors, u as ccacheActivityLine } from "./android-cas-CITmIeWY.mjs";
29
- import { d as startBuildHeartbeat, f as tailLines, n as HEARTBEAT_INTERVAL_MS } from "./xcode-B2tBkf0m.mjs";
30
- import { n as nativeRunCommand, t as launchSlotScope } from "./slot-launch-Dk4oTREM.mjs";
31
- import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-xxWKNO3-.mjs";
28
+ import { a as buildAndroid, c as CCACHE_NOT_RUN, f as resolveCcache, l as CCACHE_UNAVAILABLE, n as resolveAndroidCas, o as productFlavorRefusal, p as formatDiagnostic, r as resolveAndroidCompilerCache, s as readProductFlavors, u as ccacheActivityLine } from "./android-cas-DfJkwCXW.mjs";
29
+ import { d as startBuildHeartbeat, f as tailLines, n as HEARTBEAT_INTERVAL_MS } from "./xcode-B5SPD--o.mjs";
30
+ import { n as nativeRunCommand, t as launchSlotScope } from "./slot-launch-rggO1Esw.mjs";
31
+ import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-UYFSQwRy.mjs";
32
32
  import { constants, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
33
33
  import { InvalidArgumentError } from "commander";
34
+ import { LAST_BUILD_KEYS } from "@stim-cli/core/state";
34
35
  import { basename, dirname, isAbsolute, join, relative } from "node:path";
35
36
  import chalk from "chalk";
36
37
  import { createWarnOnce, loadCacheProvider, resolveTieredBuild, storeTieredBuild } from "@stim-cli/cache";
@@ -436,6 +437,36 @@ function systemImageRefusal({ slot, flag, resolved, physical, remoteBackend, lis
436
437
  remedy: refusal.remedy
437
438
  } : null;
438
439
  }
440
+ function resolveDeviceProfile(flag, settings) {
441
+ const fromFlag = typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null;
442
+ const android = settings?.["android"];
443
+ const raw = android && typeof android === "object" && !Array.isArray(android) ? android["deviceProfile"] : void 0;
444
+ return fromFlag || (typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null);
445
+ }
446
+ function deviceProfileRefusal({ flag, resolved, physical, remoteBackend, listProfiles }) {
447
+ if (typeof flag === "string" && flag.trim() === "") return {
448
+ code: "STIM_BAD_ARG",
449
+ message: "--device-profile was given an empty id.",
450
+ remedy: "Pass `--device-profile <id>` with an id `avdmanager list device -c` prints, e.g. \"pixel_fold\"."
451
+ };
452
+ if (physical || remoteBackend || !resolved) return null;
453
+ let profiles;
454
+ try {
455
+ profiles = listProfiles();
456
+ } catch (error) {
457
+ return {
458
+ code: NO_DEVICE,
459
+ message: `Could not list the Android hardware profiles with avdmanager: ${error?.message || error}`,
460
+ remedy: "Check that JAVA_HOME and ANDROID_HOME are set and the SDK command-line tools are installed, then try again."
461
+ };
462
+ }
463
+ const refusal = unknownAndroidDeviceProfileRefusal(resolved, profiles);
464
+ return refusal ? {
465
+ code: "STIM_BAD_ARG",
466
+ message: refusal.message,
467
+ remedy: refusal.remedy
468
+ } : null;
469
+ }
439
470
  function isReleaseVariant(variant) {
440
471
  return typeof variant === "string" && /release$/i.test(variant.trim());
441
472
  }
@@ -723,11 +754,16 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
723
754
  hash = computed?.hash ?? "";
724
755
  fingerprintSources = computed?.sources ?? [];
725
756
  } catch (err) {
726
- phaseFailure = fail$1(NO_FINGERPRINT, `@expo/fingerprint could not fingerprint ${root}: ${err?.message || err}`, "Fix the @expo/fingerprint error above, then retry.");
757
+ const message = String(err?.message || err);
758
+ record.cacheSkipped = !useBuildCache;
759
+ record.missReason = fingerprintErrorMissReason(message);
760
+ phaseFailure = fail$1(NO_FINGERPRINT, `@expo/fingerprint could not fingerprint ${root}: ${message}`, "Fix the @expo/fingerprint error above, then retry.", { lastBuildStatus: true });
727
761
  return false;
728
762
  }
729
763
  if (!hash) {
730
- phaseFailure = fail$1(NO_FINGERPRINT, `@expo/fingerprint returned no hash for ${root}, so the build cache cannot be addressed.`, "Check the project native inputs and the @expo/fingerprint error above, then retry.");
764
+ record.cacheSkipped = !useBuildCache;
765
+ record.missReason = fingerprintErrorMissReason("no hash");
766
+ phaseFailure = fail$1(NO_FINGERPRINT, `@expo/fingerprint returned no hash for ${root}, so the build cache cannot be addressed.`, "Check the project native inputs and the @expo/fingerprint error above, then retry.", { lastBuildStatus: true });
731
767
  return false;
732
768
  }
733
769
  record.fingerprint = hash;
@@ -757,29 +793,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
757
793
  const cached = found?.tier === "local" ? found.path : null;
758
794
  record.cacheHit = cached ? "local" : false;
759
795
  record.cacheSkipped = !useBuildCache;
760
- let missDiff = "";
761
- let missUntracked = null;
762
- if (!cached) {
763
- const lastBuild = readState(root)?.lastBuild ?? null;
764
- const miss = describeFingerprintMiss({
765
- platform: PLATFORM,
766
- current: {
767
- hash,
768
- sources: fingerprintSources
769
- },
770
- lastBuild
771
- });
772
- if (miss) {
773
- missDiff = fingerprintDiffSuffix(miss.changed);
774
- writer.write(fingerprintDiffRecord({
775
- changed: miss.changed,
776
- previousHash: miss.previousHash,
777
- hash
778
- }));
779
- } else if (useBuildCache) missUntracked = untrackedMissLine(untracked({ projectRoot: root }));
780
- }
781
- phase("fingerprint", `${shortHash(hash)} ${cached ? "hit" : "miss"}${useBuildCache ? "" : !requestedBuildCache ? " (--no-build-cache)" : " (cache reuse off in config)"} ${fingerprintTimer()}${missDiff}`);
782
- if (missUntracked) phase("fingerprint", chalk.dim(missUntracked));
796
+ phase("fingerprint", `${shortHash(hash)} ${cached ? "hit" : "miss"}${useBuildCache ? "" : !requestedBuildCache ? " (--no-build-cache)" : " (cache reuse off in config)"} ${fingerprintTimer()}`);
783
797
  if (found?.tier === "provider") {
784
798
  record.cacheHit = "remote";
785
799
  providerName = found.providerName ?? null;
@@ -909,6 +923,34 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
909
923
  }
910
924
  }
911
925
  }
926
+ function explainMiss(rekeyedBy) {
927
+ if (swapFellBack) record.missReason = skippedMissReason("the cached APK could not be reused, so this run built it fresh");
928
+ else if (!useBuildCache) record.missReason = skippedMissReason(requestedBuildCache ? "cache reuse off in config" : "cache reuse turned off by --no-build-cache");
929
+ else {
930
+ const current = {
931
+ hash: storeHash,
932
+ sources: storeSources
933
+ };
934
+ const explained = explainBuildMiss({
935
+ root,
936
+ platform: PLATFORM,
937
+ current,
938
+ rekeyedBy,
939
+ baselineDeps: { readState }
940
+ });
941
+ record.missReason = explained.reason;
942
+ if (explained.previousHash && explained.changedNames.length) writer.write(fingerprintDiffRecord({
943
+ changed: explained.changedNames,
944
+ previousHash: explained.previousHash,
945
+ hash: current.hash
946
+ }));
947
+ }
948
+ phase("cache", `miss: ${record.missReason.summary}`);
949
+ if (record.missReason.kind === "no-baseline") {
950
+ const line = untrackedMissLine(untracked({ projectRoot: root }));
951
+ if (line) phase("fingerprint", chalk.dim(line));
952
+ }
953
+ }
912
954
  async function buildArtifact() {
913
955
  if (!apkPath) try {
914
956
  if (maxBuilds) try {
@@ -926,6 +968,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
926
968
  }
927
969
  phase("build", chalk.yellow(`could not take a build slot: ${err?.message || err}; building anyway`));
928
970
  }
971
+ const rekeyedBy = [];
929
972
  const prebuildPlan = planPrebuildFor(root, PLATFORM, {
930
973
  isExpo,
931
974
  fingerprint: hash,
@@ -962,6 +1005,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
962
1005
  });
963
1006
  if (after) recordPrebuild(root, PLATFORM, after.hash);
964
1007
  if (after?.moved) {
1008
+ rekeyedBy.push("prebuild");
965
1009
  storeHash = after.hash;
966
1010
  storeSources = after.sources;
967
1011
  storeKey = buildCacheKey(PLATFORM, after.hash, buildRunOptions);
@@ -984,6 +1028,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
984
1028
  }
985
1029
  }
986
1030
  if (!apkPath) {
1031
+ explainMiss(rekeyedBy);
987
1032
  step("compile");
988
1033
  phase("build", `compiling ${variant || "debug"} with Gradle`);
989
1034
  const built = await build({
@@ -1115,7 +1160,7 @@ function tail(lines, n = FALLBACK_LINES) {
1115
1160
  }
1116
1161
  //#endregion
1117
1162
  //#region src/commands/android/result.ts
1118
- function androidFacts({ slot, serial, avdName = null, deviceName = null, systemImage = null, fingerprint, cacheKey = null, variant = null, metroPort = null, cacheHit, cacheSkipped = false, waitedForBuild = null, appPath, bundleId, installSkipped = false, launched, ccache = CCACHE_UNAVAILABLE, logs, debugHttpHost = null, debugHttpHostNote = null, devClientUrl = null, durationMs, lease, devServer = null }) {
1163
+ function androidFacts({ slot, serial, avdName = null, deviceName = null, systemImage = null, deviceProfile = null, fingerprint, cacheKey = null, variant = null, metroPort = null, cacheHit, cacheSkipped = false, waitedForBuild = null, appPath, bundleId, installSkipped = false, launched, ccache = CCACHE_UNAVAILABLE, logs, debugHttpHost = null, debugHttpHostNote = null, devClientUrl = null, durationMs, lease, devServer = null }) {
1119
1164
  return {
1120
1165
  platform: PLATFORM,
1121
1166
  ...slot && slot !== "default" ? { slot } : {},
@@ -1123,6 +1168,7 @@ function androidFacts({ slot, serial, avdName = null, deviceName = null, systemI
1123
1168
  avdName: avdName ?? null,
1124
1169
  deviceName: deviceName ?? avdName ?? null,
1125
1170
  systemImage: systemImage ?? null,
1171
+ deviceProfile: deviceProfile ?? null,
1126
1172
  fingerprint: fingerprint ?? null,
1127
1173
  cacheKey: cacheKey ?? null,
1128
1174
  variant: variant ?? null,
@@ -1147,7 +1193,7 @@ function androidFacts({ slot, serial, avdName = null, deviceName = null, systemI
1147
1193
  ...devServer ? { devServer } : {}
1148
1194
  };
1149
1195
  }
1150
- function lastBuildRecord({ fingerprint, cacheKey, cacheHit, cacheSkipped = false, durationMs, appPath, bundleId, startedAt, status, errorCode = null, avdName = null, deviceName = null }) {
1196
+ function lastBuildRecord({ fingerprint, cacheKey, cacheHit, cacheSkipped = false, durationMs, appPath, bundleId, startedAt, status, errorCode = null, avdName = null, deviceName = null, missReason = null }) {
1151
1197
  const record = {
1152
1198
  platform: PLATFORM,
1153
1199
  avdName: avdName ?? null,
@@ -1163,6 +1209,7 @@ function lastBuildRecord({ fingerprint, cacheKey, cacheHit, cacheSkipped = false
1163
1209
  status
1164
1210
  };
1165
1211
  if (errorCode) record.errorCode = errorCode;
1212
+ if (missReason && !cacheLevel(cacheHit)) record.missReason = missReason;
1166
1213
  return record;
1167
1214
  }
1168
1215
  async function finishAndroidUpload(uploadPending, remote, phase) {
@@ -1195,6 +1242,7 @@ function reportAndroidResult({ slot, json, useBuildCache, variant, release, metr
1195
1242
  avdName: record.avdName,
1196
1243
  deviceName: record.deviceName,
1197
1244
  systemImage: record.systemImage,
1245
+ deviceProfile: record.deviceProfile,
1198
1246
  debugHttpHost: launched.debugHttpHost ?? null,
1199
1247
  debugHttpHostNote: launched.debugHttpHostNote ?? null,
1200
1248
  devClientUrl: launched.devClientUrl ?? null,
@@ -1252,7 +1300,10 @@ function persistLastBuild({ writeState, root, record, startedAt, durationMs, sta
1252
1300
  errorCode
1253
1301
  });
1254
1302
  try {
1255
- writeState(root, { lastBuild });
1303
+ writeState(root, {
1304
+ lastBuild,
1305
+ [LAST_BUILD_KEYS[PLATFORM]]: lastBuild
1306
+ });
1256
1307
  } catch (err) {
1257
1308
  out(phaseLine("state", chalk.yellow(`could not record lastBuild: ${err?.message || err}`)));
1258
1309
  }
@@ -1471,8 +1522,8 @@ async function resolveInstallSerial({ root, slot, device, physical, remoteDevice
1471
1522
  phase("device", `reopen agent-device on ${resolved.serial}`);
1472
1523
  }
1473
1524
  const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
1474
- withConfigLock(() => {
1475
- const current = deviceSlotPlatforms(loadConfig()?.projects?.[root], slot)?.android;
1525
+ withConfigLock$1(() => {
1526
+ const current = deviceSlotPlatforms(loadConfig$1()?.projects?.[root], slot)?.android;
1476
1527
  if (!current?.owned || current.avdName !== device.avdName) throw new Error("The owned emulator assignment changed before installation.");
1477
1528
  if (current.consolePort !== consolePort) setDevice(root, PLATFORM, {
1478
1529
  ...current,
@@ -1558,8 +1609,8 @@ async function finishAndroidRun({ lease, releaseLease, root, slot, json, metroCh
1558
1609
  return fail(installed.code || "STIM_INSTALL_FAILED", installed.reason, installRemedy, { lastBuildStatus: true });
1559
1610
  }
1560
1611
  if (adopting) try {
1561
- withConfigLock(() => {
1562
- const config = loadConfig();
1612
+ withConfigLock$1(() => {
1613
+ const config = loadConfig$1();
1563
1614
  const current = deviceSlotPlatforms(config?.projects?.[root], slot)?.android;
1564
1615
  if (!config || !current || current.avdName !== device.avdName) throw new Error("The adopted emulator assignment changed during installation.");
1565
1616
  delete current.adoptionPending;
@@ -1850,7 +1901,7 @@ function androidCompilerCache({ root, optimizations, settingsContext }) {
1850
1901
  })}`
1851
1902
  };
1852
1903
  }
1853
- function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, variant: variantFlag, systemImage: systemImageFlag, device: deviceFlag, wait: waitFlag, waitConflict, remote: commandRemoteBackend, buildCache: requestedBuildCache }, { warn, resolveCompilerCache = androidCompilerCache, resolveCacheProvider = resolveCacheProviderConfig, validateAvdConfig = androidAvdConfigSettingError, readFlavors = readProductFlavors, detectExpo = detectIsExpo, listSystemImages = listInstalledSystemImages, parkedLimit = parkedMaxSetting }) {
1904
+ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, variant: variantFlag, systemImage: systemImageFlag, deviceProfile: deviceProfileFlag, device: deviceFlag, wait: waitFlag, waitConflict, remote: commandRemoteBackend, buildCache: requestedBuildCache }, { warn, resolveCompilerCache = androidCompilerCache, resolveCacheProvider = resolveCacheProviderConfig, validateAvdConfig = androidAvdConfigSettingError, readFlavors = readProductFlavors, detectExpo = detectIsExpo, listSystemImages = listInstalledSystemImages, listDeviceProfiles = listAvdDeviceProfiles, parkedLimit = parkedMaxSetting }) {
1854
1905
  const root = settingsContext.projectPath;
1855
1906
  const [shapeError, ...moreShapeErrors] = [parkedLimit("android").error, ...settingShapeErrors(settings)].filter((error) => Boolean(error));
1856
1907
  if (shapeError) return fail("STIM_BAD_ARG", shapeError, SETTING_SHAPE_REMEDY, { lines: moreShapeErrors });
@@ -1878,6 +1929,7 @@ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, va
1878
1929
  const avdConfigError = validateAvdConfig(settings, root);
1879
1930
  if (avdConfigError) return fail("STIM_BAD_ARG", avdConfigError, "Use only documented android.avdConfig keys, or an android.avdConfigFile fragment contained by the app directory.");
1880
1931
  const systemImage = resolveSystemImage(systemImageFlag, settings);
1932
+ const deviceProfile = resolveDeviceProfile(deviceProfileFlag, settings);
1881
1933
  const variant = easProfile !== void 0 ? "debug" : resolveVariant(variantFlag, settings);
1882
1934
  const flavorRefusal = productFlavorRefusal({
1883
1935
  flavors: readFlavors(root),
@@ -1907,6 +1959,14 @@ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, va
1907
1959
  listImages: listSystemImages
1908
1960
  });
1909
1961
  if (imageRefusal) return fail(imageRefusal.code, imageRefusal.message, imageRefusal.remedy);
1962
+ const profileRefusal = deviceProfileRefusal({
1963
+ flag: deviceProfileFlag,
1964
+ resolved: deviceProfile,
1965
+ physical,
1966
+ remoteBackend,
1967
+ listProfiles: listDeviceProfiles
1968
+ });
1969
+ if (profileRefusal) return fail(profileRefusal.code, profileRefusal.message, profileRefusal.remedy);
1910
1970
  const target = physical ? {
1911
1971
  kind: "physical",
1912
1972
  serial: typeof deviceFlag === "string" ? deviceFlag : null,
@@ -1917,10 +1977,12 @@ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, va
1917
1977
  } : remoteBackend ? {
1918
1978
  kind: "remote",
1919
1979
  backend: remoteBackend,
1920
- systemImage
1980
+ systemImage,
1981
+ deviceProfile
1921
1982
  } : {
1922
1983
  kind: "emulator",
1923
- systemImage
1984
+ systemImage,
1985
+ deviceProfile
1924
1986
  };
1925
1987
  return {
1926
1988
  ok: true,
@@ -1944,21 +2006,213 @@ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, va
1944
2006
  };
1945
2007
  }
1946
2008
  //#endregion
2009
+ //#region src/commands/android/next-build.ts
2010
+ const DEFAULT_PLAN_DEPS = {
2011
+ findRoot: findProjectRoot,
2012
+ fingerprint: fingerprintProject,
2013
+ listSystemImages: listInstalledSystemImages,
2014
+ avdSystemImage: ownedAvdSystemImage,
2015
+ avdDirectory: ownedAvdDirectory,
2016
+ listAvds,
2017
+ loadCacheProvider,
2018
+ loadProjectProvider,
2019
+ checkEasAuth,
2020
+ resolveRemote,
2021
+ planPrebuild
2022
+ };
2023
+ function note(line) {
2024
+ console.error(line);
2025
+ }
2026
+ function runOnlyFlag(opts) {
2027
+ if (isPhysicalDeviceRequest(opts.device)) return "--device";
2028
+ if (opts.remote) return "--remote";
2029
+ if (opts.wait !== void 0) return opts.wait === false ? "--no-wait" : "--wait";
2030
+ if (opts.metroCheck === false) return "--no-metro-check";
2031
+ return null;
2032
+ }
2033
+ function emulatorImage(root, slot, requested, deps) {
2034
+ const record = deviceSlotPlatforms(getProject(root), slot)?.android;
2035
+ if (record?.avdName && !record.setupIncomplete) {
2036
+ if (record.owned && deps.avdDirectory(record.avdName)) return { systemImage: deps.avdSystemImage(record.avdName) };
2037
+ if (!record.owned && avdListed(record.avdName, deps.listAvds)) return { systemImage: null };
2038
+ }
2039
+ let images;
2040
+ try {
2041
+ images = deps.listSystemImages();
2042
+ } catch (error) {
2043
+ return { refusal: {
2044
+ code: NO_DEVICE,
2045
+ message: `Could not read the installed Android system images: ${error?.message || error}`,
2046
+ remedy: "Check that ANDROID_HOME points at a readable SDK, then try again."
2047
+ } };
2048
+ }
2049
+ const picked = pickDefaultSystemImage(images, requested ? { systemImage: requested } : {});
2050
+ if (picked) return { systemImage: picked.pkg };
2051
+ const arch = hostSystemImageArch();
2052
+ return { refusal: {
2053
+ code: NO_DEVICE,
2054
+ message: `No ${arch} Android system image is installed, so the emulator build's ABI and cache key are unknown.`,
2055
+ remedy: `Install one, e.g.: sdkmanager "system-images;android-36;google_apis;${arch}", then try again.`
2056
+ } };
2057
+ }
2058
+ function avdListed(avdName, list) {
2059
+ try {
2060
+ return list({ timeoutMs: 1e4 }).includes(avdName);
2061
+ } catch {
2062
+ return false;
2063
+ }
2064
+ }
2065
+ async function planAndroid(opts, overrides = {}) {
2066
+ const deps = {
2067
+ ...DEFAULT_PLAN_DEPS,
2068
+ ...overrides
2069
+ };
2070
+ const json = Boolean(opts.json);
2071
+ const refuse = (refusal, lines = []) => refusePlan(refusal, json, lines);
2072
+ const flag = runOnlyFlag(opts);
2073
+ if (flag) return refuse(planFlagRefusal(flag));
2074
+ const root = deps.findRoot(process.cwd());
2075
+ if (!root) return refuse(NO_PROJECT_REFUSAL);
2076
+ const projectProblem = appProjectProblem(root);
2077
+ if (projectProblem) return refuse({
2078
+ code: "STIM_NO_PROJECT",
2079
+ ...projectProblem
2080
+ });
2081
+ const slot = validateDeviceSlot(opts.slot);
2082
+ const settingsContext = {
2083
+ projectPath: root,
2084
+ gitCommonDir: gitCommonDir(root),
2085
+ repoRoot: repoRoot(root)
2086
+ };
2087
+ const planned = resolveAndroidRunPlan({
2088
+ settings: resolveSettings(settingsContext),
2089
+ settingsContext,
2090
+ slot,
2091
+ easProfile: opts.easProfile,
2092
+ variant: opts.variant ?? null,
2093
+ systemImage: opts.systemImage ?? null,
2094
+ device: null,
2095
+ wait: void 0,
2096
+ waitConflict: false,
2097
+ remote: null,
2098
+ buildCache: opts.buildCache !== false
2099
+ }, {
2100
+ warn: (label, message) => note(phaseLine(label, chalk.yellow(message))),
2101
+ resolveCompilerCache: ({ optimizations }) => ({
2102
+ cas: null,
2103
+ optimizations,
2104
+ warning: null
2105
+ }),
2106
+ listSystemImages: deps.listSystemImages
2107
+ });
2108
+ if (!planned.ok) return refuse(planned, planned.lines);
2109
+ const { build, target, isExpo, cacheProviderConfig } = planned.plan;
2110
+ const planTarget = {
2111
+ platform: PLATFORM,
2112
+ slot,
2113
+ root,
2114
+ projectKey: statsProjectKey({
2115
+ root,
2116
+ commonDir: settingsContext.gitCommonDir,
2117
+ repoRoot: settingsContext.repoRoot
2118
+ })
2119
+ };
2120
+ if (opts.easProfile !== void 0) {
2121
+ const eas = await planEasDevelopmentBuild({
2122
+ root,
2123
+ platform: PLATFORM,
2124
+ profile: opts.easProfile,
2125
+ note,
2126
+ isExpo,
2127
+ selectors: [opts.variant],
2128
+ buildCache: opts.buildCache
2129
+ });
2130
+ if (!eas.ok) return refuse(eas);
2131
+ return printPlan(planPayload(planTarget, {
2132
+ fingerprint: eas.fingerprint,
2133
+ cacheKey: eas.cacheKey,
2134
+ cacheHit: eas.buildId ? "remote" : false,
2135
+ provider: "eas",
2136
+ cacheSkipped: false,
2137
+ prebuild: null,
2138
+ refusal: eas.missing
2139
+ }), json);
2140
+ }
2141
+ if (target.kind !== "emulator") {
2142
+ if (target.kind === "physical") return refuse(planFlagRefusal("--device"));
2143
+ return refuse({
2144
+ code: "STIM_BAD_ARG",
2145
+ message: `android.remote routes this workspace's runs to a ${target.backend} device, whose ABI --plan cannot read without a session.`,
2146
+ remedy: "Run `stim android` to build for the remote device, or unset android.remote to plan the owned emulator."
2147
+ });
2148
+ }
2149
+ if (build.compilerCache === "cas") return refuse({
2150
+ code: "STIM_BAD_ARG",
2151
+ message: "The experimental Android compiler CAS keys builds by a toolchain Stim sets up, which --plan does not.",
2152
+ remedy: "Run `stim android` to build with the CAS, or set optimizations.android.compilerCache to ccache."
2153
+ });
2154
+ const image = build.targetAbiOnly && !build.release ? emulatorImage(root, slot, target.systemImage, deps) : { systemImage: null };
2155
+ if ("refusal" in image) return refuse(image.refusal);
2156
+ const { abi, runOptions, remoteRunOptions } = androidBuildOptions({
2157
+ release: build.release,
2158
+ physical: false,
2159
+ device: { systemImage: image.systemImage },
2160
+ variant: build.variant,
2161
+ deviceAbi: () => null,
2162
+ buildProfile: build.profile,
2163
+ targetAbiOnly: build.targetAbiOnly
2164
+ });
2165
+ let fingerprint;
2166
+ try {
2167
+ fingerprint = await deps.fingerprint(root, { platform: PLATFORM });
2168
+ } catch (error) {
2169
+ return refuse({
2170
+ code: NO_FINGERPRINT,
2171
+ message: `@expo/fingerprint could not fingerprint ${root}: ${error?.message || error}`,
2172
+ remedy: "Fix the @expo/fingerprint error above, then retry."
2173
+ });
2174
+ }
2175
+ if (!fingerprint?.hash) return refuse({
2176
+ code: NO_FINGERPRINT,
2177
+ message: `@expo/fingerprint returned no hash for ${root}, so the build cache cannot be addressed.`,
2178
+ remedy: "Check the project native inputs and the @expo/fingerprint error above, then retry."
2179
+ });
2180
+ const plan = await planCachedBuild({
2181
+ ...planTarget,
2182
+ isExpo,
2183
+ fingerprint: fingerprint.hash,
2184
+ sources: fingerprint.sources ?? [],
2185
+ cacheKey: buildCacheKey(PLATFORM, fingerprint.hash, runOptions),
2186
+ cachePolicy: build.cache,
2187
+ providerConfig: cacheProviderConfig,
2188
+ expoRemote: abi || build.profile || !build.cache.remote ? null : { runOptions: remoteRunOptions }
2189
+ }, {
2190
+ ...deps,
2191
+ note
2192
+ });
2193
+ printPlan(plan, json);
2194
+ }
2195
+ //#endregion
1947
2196
  //#region src/commands/android.ts
1948
2197
  function androidCommand(program) {
1949
2198
  registerAndroid(program);
1950
2199
  }
1951
2200
  function registerAndroid(program) {
1952
- program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator, wired to the reserved Metro port. A Debug run starts the dev server when it is not running.").option("--eas-profile <name>", "Download a matching EAS development build; on a miss, print the build command without running it").option("--slot <name>", "Reusable device slot within this workspace (default: default)", parseDeviceSlotOption).option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--no-metro-check", "Skip the reserved-port Metro health check and do not start it (the app will load no bundle unless something else serves it)").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--variant <name>", "Gradle variant to assemble and install (e.g. productionDebug on a flavored project); overrides the android.variant setting. A variant ending in Release embeds the JS bundle and skips Metro entirely. Default: debug").option("--system-image <id>", "Android system image to create this workspace's owned AVD from, as the sdkmanager package id (e.g. \"system-images;android-36;google_apis;arm64-v8a\"). Overrides the android.systemImage setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the installed images.").option("--device [serial]", "Install and launch on a connected physical device instead of this workspace's owned emulator. With no serial, the first connected device this workspace can lease is used. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. Builds are local unless --eas-profile selects an existing EAS build.", (value) => {
2201
+ program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator, wired to the reserved Metro port. A Debug run starts the dev server when it is not running.").option("--eas-profile <name>", "Download a matching EAS development build; on a miss, print the build command without running it").option("--slot <name>", "Reusable device slot within this workspace (default: default)", parseDeviceSlotOption).option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--plan", "Predict the next build without building, booting or installing: resolve the fingerprint, check the local then remote cache, and print the expected cache result and duration. Writes no Stim state.").option("--no-metro-check", "Skip the reserved-port Metro health check and do not start it (the app will load no bundle unless something else serves it)").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--variant <name>", "Gradle variant to assemble and install (e.g. productionDebug on a flavored project); overrides the android.variant setting. A variant ending in Release embeds the JS bundle and skips Metro entirely. Default: debug").option("--system-image <id>", "Android system image to create this workspace's owned AVD from, as the sdkmanager package id (e.g. \"system-images;android-36;google_apis;arm64-v8a\"). Overrides the android.systemImage setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the installed images.").option("--device-profile <id>", "Hardware profile to create this workspace's owned AVD with, as an `avdmanager list device -c` id (e.g. \"pixel_fold\", \"pixel_tablet\"). Overrides the android.deviceProfile setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the available ids.").option("--device [serial]", "Install and launch on a connected physical device instead of this workspace's owned emulator. With no serial, the first connected device this workspace can lease is used. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. Builds are local unless --eas-profile selects an existing EAS build.", (value) => {
1953
2202
  if (REMOTE_DEVICE_BACKENDS.includes(value)) return value;
1954
2203
  throw new InvalidArgumentError(`expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}`);
1955
2204
  }).option("--wait <seconds>", "How long to wait for another workspace to release the device it leases, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once). Only with --device.").option("--no-wait", "Install on a device another workspace leases instead of waiting: this run takes no lease and, when both workspaces build the same app id, the install terminates the holder's running app. Only with --device.").action(async (opts) => {
2205
+ if (opts.plan) return planAndroid(opts);
1956
2206
  const root = findProjectRoot(process.cwd());
1957
2207
  if (!root) {
1958
2208
  refuseNoProject({ json: Boolean(opts.json) });
1959
2209
  return;
1960
2210
  }
1961
- if (!(await withWorkspaceProcessLock(workspaceDir(root), "native-run", async (claim) => {
2211
+ if (!(await withNativeBuildRun(root, {
2212
+ command: "android",
2213
+ platform: "android",
2214
+ slot: opts.slot ?? "default"
2215
+ }, async (claim) => {
1962
2216
  recordWorkspaceUse(root);
1963
2217
  const progress = startBuildProgress({
1964
2218
  root,
@@ -1977,6 +2231,7 @@ function registerAndroid(program) {
1977
2231
  useBuildCache: opts.buildCache !== false,
1978
2232
  variant: opts.variant ?? null,
1979
2233
  systemImage: opts.systemImage ?? null,
2234
+ deviceProfile: opts.deviceProfile ?? null,
1980
2235
  remoteDevice: opts.remote ?? null,
1981
2236
  device: opts.device ?? null,
1982
2237
  wait: opts.wait,
@@ -1986,14 +2241,10 @@ function registerAndroid(program) {
1986
2241
  } finally {
1987
2242
  progress.clear();
1988
2243
  }
1989
- }, {
1990
- external: true,
1991
- waitMs: 18e5,
1992
- declareSpawns: true
1993
- })).ok) process.exit(1);
2244
+ }, { write: (line) => console.error(chalk.dim(phaseLine("lock", line))) })).ok) process.exit(runCancellation() ? 130 : 1);
1994
2245
  });
1995
2246
  }
1996
- function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRemoteBackend = null, resolveSettingsFor = resolveSettings, resolveRemoteDeviceContext = resolveRemoteContext, remoteDeviceDeps: makeRemoteDeviceDeps = remoteAndroidDeps, resolveEasBin = resolveEasCliBin, ensureMetroReachable: ensureRemoteMetro = ensureMetroReachable, ensureRemoteBootOwned: ensureRemoteOwned = ensureRemoteBootOwned, detectRemoteProviders = detectProviders, metroCheck = true, useBuildCache = true, easProfile, resolveEasDevelopmentBuild: resolveEasBuild = resolveEasDevelopmentBuild, variant: variantFlag = null, systemImage: systemImageFlag = null, device: deviceFlag = null, wait: waitFlag = void 0, waitConflict = false, acquireLease = acquireRunLease, makeRunLease = runLease, selectPool = selectFromPool, onLeaseSignal = releaseLeaseOnSignal, listDevices = listAdbDevices, deviceModel = physicalDeviceModel, deviceAbi = androidDeviceAbi, isEmulatorDevice = probeEmulatorSerial, readApkPackage = (apkPath) => apkPackage(dumpApkManifest(apkPath)), getLimits = getConcurrencyLimits, checkCapacity = checkDeviceCapacity, acquireSlot = acquireBuildSlot, releaseSlot = releaseBuildSlot, ensureDevice = ensureOwnedDevice, listSystemImages = listInstalledSystemImages, ensureDeviceBooted = ensureBooted, resolveAvdSerial = resolveOwnedAvdSerial, waitForDeviceBoot = waitForBoot, resolveMetro = resolveProjectMetro, warmMetro: prewarmMetro = warmMetro, startServer = startDevServer, readState = readWorkspaceState, pidAlive = pidExists, verifyCollector = verifyCollectorOwnership, verifyLaunched = verifyLaunch, ensureStorage = ensureWorkspaceStorageSafely, fingerprint = fingerprintProject, untracked = untrackedNativeFiles, resolveCached = resolveBuild, storeCached = storeBuild, storedAssets = storedAssetManifest, captureAssets = captureAssetManifest, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, loadProvider = loadProjectProvider, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, planPrebuildFor = planPrebuild, prebuild = runPrebuild, build = buildAndroid, ccacheFor = resolveCcache, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, swapApk = swapApkBundle, resolveDevClientScheme = androidDevClientScheme, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), createWriter = createNdjsonWriter, writeLaunch = writeWorkspaceLaunch, writeState = writeWorkspaceState, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
2247
+ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRemoteBackend = null, resolveSettingsFor = resolveSettings, resolveRemoteDeviceContext = resolveRemoteContext, remoteDeviceDeps: makeRemoteDeviceDeps = remoteAndroidDeps, resolveEasBin = resolveEasCliBin, ensureMetroReachable: ensureRemoteMetro = ensureMetroReachable, ensureRemoteBootOwned: ensureRemoteOwned = ensureRemoteBootOwned, detectRemoteProviders = detectProviders, metroCheck = true, useBuildCache = true, easProfile, resolveEasDevelopmentBuild: resolveEasBuild = resolveEasDevelopmentBuild, variant: variantFlag = null, systemImage: systemImageFlag = null, deviceProfile: deviceProfileFlag, device: deviceFlag = null, wait: waitFlag = void 0, waitConflict = false, acquireLease = acquireRunLease, makeRunLease = runLease, selectPool = selectFromPool, onLeaseSignal = releaseLeaseOnSignal, listDevices = listAdbDevices, deviceModel = physicalDeviceModel, deviceAbi = androidDeviceAbi, isEmulatorDevice = probeEmulatorSerial, readApkPackage = (apkPath) => apkPackage(dumpApkManifest(apkPath)), getLimits = getConcurrencyLimits, checkCapacity = checkDeviceCapacity, acquireSlot = acquireBuildSlot, releaseSlot = releaseBuildSlot, ensureDevice = ensureOwnedDevice, listSystemImages = listInstalledSystemImages, listDeviceProfiles, ensureDeviceBooted = ensureBooted, resolveAvdSerial = resolveOwnedAvdSerial, waitForDeviceBoot = waitForBoot, resolveMetro = resolveProjectMetro, warmMetro: prewarmMetro = warmMetro, startServer = startDevServer, readState = readWorkspaceState$1, pidAlive = pidExists, verifyCollector = verifyCollectorOwnership, verifyLaunched = verifyLaunch, ensureStorage = ensureWorkspaceStorageSafely, fingerprint = fingerprintProject, untracked = untrackedNativeFiles, resolveCached = resolveBuild, storeCached = storeBuild, storedAssets = storedAssetManifest, captureAssets = captureAssetManifest, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, loadProvider = loadProjectProvider, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, planPrebuildFor = planPrebuild, prebuild = runPrebuild, build = buildAndroid, ccacheFor = resolveCcache, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, swapApk = swapApkBundle, resolveDevClientScheme = androidDevClientScheme, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), createWriter = createNdjsonWriter, writeLaunch = writeWorkspaceLaunch, writeState = writeWorkspaceState, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
1997
2248
  return {
1998
2249
  root,
1999
2250
  json,
@@ -2011,6 +2262,7 @@ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRem
2011
2262
  resolveEasBuild,
2012
2263
  variantFlag,
2013
2264
  systemImageFlag,
2265
+ deviceProfileFlag,
2014
2266
  deviceFlag,
2015
2267
  waitFlag,
2016
2268
  waitConflict,
@@ -2029,6 +2281,7 @@ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRem
2029
2281
  releaseSlot,
2030
2282
  ensureDevice,
2031
2283
  listSystemImages,
2284
+ listDeviceProfiles,
2032
2285
  ensureDeviceBooted,
2033
2286
  resolveAvdSerial,
2034
2287
  waitForDeviceBoot,
@@ -2130,7 +2383,7 @@ function avdSetupFailure(error, root, logFile) {
2130
2383
  };
2131
2384
  }
2132
2385
  async function runAndroid(options = {}) {
2133
- let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, easProfile, resolveEasBuild, variantFlag, systemImageFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, ensureDeviceBooted, resolveAvdSerial, waitForDeviceBoot, resolveMetro, prewarmMetro, startServer, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, planPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordStats, readEstimates, now, out, emit } = androidSlotOptions(options);
2386
+ let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, easProfile, resolveEasBuild, variantFlag, systemImageFlag, deviceProfileFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, listDeviceProfiles, ensureDeviceBooted, resolveAvdSerial, waitForDeviceBoot, resolveMetro, prewarmMetro, startServer, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, planPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordStats, readEstimates, now, out, emit } = androidSlotOptions(options);
2134
2387
  const progress = options.progress ?? NO_BUILD_PROGRESS;
2135
2388
  const slot = validateDeviceSlot(options.slot);
2136
2389
  const started = now();
@@ -2203,6 +2456,11 @@ async function runAndroid(options = {}) {
2203
2456
  let ccacheActivity = CCACHE_NOT_RUN;
2204
2457
  let reclaimed = [];
2205
2458
  const fail = (code, message, remedy, { lastBuildStatus = false, diagnostics = [], lines = [], logPath = null, lease } = {}) => {
2459
+ const cancellation = cancelledFailure(PLATFORM, {
2460
+ code,
2461
+ message
2462
+ });
2463
+ if (cancellation) ({code, message, remedy, lines} = cancellation);
2206
2464
  if (lastBuildStatus) persistLastBuild({
2207
2465
  writeState,
2208
2466
  root,
@@ -2266,6 +2524,7 @@ async function runAndroid(options = {}) {
2266
2524
  easProfile,
2267
2525
  variant: variantFlag,
2268
2526
  systemImage: systemImageFlag,
2527
+ deviceProfile: deviceProfileFlag,
2269
2528
  device: deviceFlag,
2270
2529
  wait: waitFlag,
2271
2530
  waitConflict,
@@ -2274,6 +2533,7 @@ async function runAndroid(options = {}) {
2274
2533
  }, {
2275
2534
  resolveCacheProvider,
2276
2535
  listSystemImages,
2536
+ listDeviceProfiles,
2277
2537
  warn: (label, message) => out(phaseLine(label, chalk.yellow(message)))
2278
2538
  });
2279
2539
  if (!planned.ok) return fail(planned.code, planned.message, planned.remedy, { lines: planned.lines });
@@ -2373,6 +2633,7 @@ async function runAndroid(options = {}) {
2373
2633
  let device;
2374
2634
  let bootDuration = "";
2375
2635
  let bootPromise;
2636
+ let startRemoteBoot = null;
2376
2637
  if (target.kind === "physical" && !target.serial) {
2377
2638
  const pooled = await pooledAndroidDevice({
2378
2639
  root,
@@ -2404,6 +2665,11 @@ async function runAndroid(options = {}) {
2404
2665
  serial: resolved.serial
2405
2666
  });
2406
2667
  } else {
2668
+ if (!remoteDevice) try {
2669
+ configuredAndroidEmulatorApp();
2670
+ } catch (err) {
2671
+ return fail("STIM_BAD_ARG", err.message, "Run `stim settings set androidEmulatorApp emulator` or `stim settings set androidEmulatorApp stim-desktop`.");
2672
+ }
2407
2673
  const capacity = checkCapacity({
2408
2674
  platform: PLATFORM,
2409
2675
  project,
@@ -2418,7 +2684,10 @@ async function runAndroid(options = {}) {
2418
2684
  projectPath: root,
2419
2685
  settingsRoot,
2420
2686
  settings,
2421
- flags: { systemImage: target.systemImage },
2687
+ flags: {
2688
+ systemImage: target.systemImage,
2689
+ deviceProfile: target.deviceProfile
2690
+ },
2422
2691
  note: out,
2423
2692
  out,
2424
2693
  logFile: emuLog
@@ -2429,7 +2698,6 @@ async function runAndroid(options = {}) {
2429
2698
  }
2430
2699
  const prepareMs = prepare();
2431
2700
  if (device.created || prepareMs >= 2e3) phase("device", `${device.avdName || device.deviceName || label} ${device.created ? "created" : "prepared"} (${formatDuration(prepareMs)})`);
2432
- const bootTimer = stepTimer(now);
2433
2701
  const boot = () => Promise.resolve(ensureDeviceBooted({
2434
2702
  platform: PLATFORM,
2435
2703
  device,
@@ -2441,41 +2709,31 @@ async function runAndroid(options = {}) {
2441
2709
  reason: String(e?.message || e),
2442
2710
  serial: void 0
2443
2711
  }));
2444
- bootPromise = (remoteDevice?.ctx.backend === "eas" ? ensureRemoteOwned({
2445
- root,
2446
- platform: PLATFORM,
2447
- sessionName: ownedSessionName(remoteDevice.ctx.label),
2448
- startedAt,
2449
- boot,
2450
- createdSessionId: remoteDevice.createdSessionId,
2451
- abandonCreatedSession: remoteDevice.abandonCreatedSession,
2452
- webPreviewUrl: remoteDevice.webPreviewUrl,
2453
- writeState,
2454
- register: registerProject
2455
- }) : boot()).then((result) => {
2456
- bootDuration = bootTimer();
2457
- return result;
2458
- });
2459
- }
2460
- if (remoteDevice) {
2461
- const booted = await bootPromise;
2462
- if (booted.failed) {
2463
- if (booted.code) return fail(booted.code, booted.reason ?? "The remote device did not boot.", booted.remedy ?? null);
2464
- const diag = noDeviceDiagnostic({
2465
- reason: booted.reason ?? "The remote device did not boot.",
2466
- logFile: emuLog,
2467
- remedy: "Run `stim status` to inspect the remote device, then retry the command.",
2468
- localEmulator: false
2469
- });
2470
- return fail(NO_DEVICE, diag.message, diag.remedy, {
2471
- lines: diag.lines,
2472
- logPath: diag.logPath ? displayPath(root, diag.logPath) : null
2712
+ const startBoot = () => {
2713
+ const bootTimer = stepTimer(now);
2714
+ return (remoteDevice?.ctx.backend === "eas" ? ensureRemoteOwned({
2715
+ root,
2716
+ platform: PLATFORM,
2717
+ sessionName: ownedSessionName(remoteDevice.ctx.label),
2718
+ startedAt: new Date(now()).toISOString(),
2719
+ boot,
2720
+ createdSessionId: remoteDevice.createdSessionId,
2721
+ abandonCreatedSession: remoteDevice.abandonCreatedSession,
2722
+ webPreviewUrl: remoteDevice.webPreviewUrl,
2723
+ writeState,
2724
+ register: registerProject
2725
+ }) : boot()).then((result) => {
2726
+ bootDuration = bootTimer();
2727
+ return result;
2473
2728
  });
2474
- }
2729
+ };
2730
+ if (remoteDevice) startRemoteBoot = startBoot;
2731
+ else bootPromise = startBoot();
2475
2732
  }
2476
2733
  record.avdName = device.avdName ?? null;
2477
2734
  record.deviceName = device.deviceName ?? device.avdName ?? null;
2478
2735
  record.systemImage = device.systemImage;
2736
+ record.deviceProfile = device.deviceProfile;
2479
2737
  const runFromFingerprint = async () => {
2480
2738
  if (metroCheck && metroPort !== null && plan.metroWarmup) prewarmMetro({
2481
2739
  port: metroPort,
@@ -2542,6 +2800,25 @@ async function runAndroid(options = {}) {
2542
2800
  ccacheActivity = artifact.ccache;
2543
2801
  androidPackage = artifact.androidPackage;
2544
2802
  record.appPath = apkPath;
2803
+ if (runCancellation()) return fail("STIM_CANCELLED", "before install");
2804
+ if (startRemoteBoot) {
2805
+ progress.step("install");
2806
+ bootPromise = startRemoteBoot();
2807
+ const booted = await bootPromise;
2808
+ if (booted.failed) {
2809
+ if (booted.code) return fail(booted.code, booted.reason ?? "The remote device did not boot.", booted.remedy ?? null);
2810
+ const diag = noDeviceDiagnostic({
2811
+ reason: booted.reason ?? "The remote device did not boot.",
2812
+ logFile: emuLog,
2813
+ remedy: "Run `stim status` to inspect the remote device, then retry the command.",
2814
+ localEmulator: false
2815
+ });
2816
+ return fail(NO_DEVICE, diag.message, diag.remedy, {
2817
+ lines: diag.lines,
2818
+ logPath: diag.logPath ? displayPath(root, diag.logPath) : null
2819
+ });
2820
+ }
2821
+ }
2545
2822
  let leaseHandle = null;
2546
2823
  let stopLeaseSignals = null;
2547
2824
  const releaseLease = () => {