stim 1.8.0 → 1.10.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 (91) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-DWssqHQ8.mjs → android-787HhP5w.mjs} +168 -39
  3. package/dist/{android-BRgBYzU0.mjs → android-DBkeHwpm.mjs} +250 -200
  4. package/dist/{android-DeYdjjBP.mjs → android-DPov-fv2.mjs} +2 -2
  5. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-CITmIeWY.mjs} +22 -14
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-CrJ6t_bv.mjs +819 -0
  8. package/dist/budget-BP4jZZL4.mjs +620 -0
  9. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Pubfp8wV.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +7 -1
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-D5z2_bcF.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-BEq0Qw_a.mjs} +134 -121
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-DP4eWqea.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-DJHoca49.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-92U593KT.mjs} +1090 -660
  21. package/dist/{device-RdujjHLB.mjs → device-DhTZe7L4.mjs} +13 -20
  22. package/dist/{build-lock-CgtsB6TV.mjs → device-lease-CbEaIk7b.mjs} +481 -27
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-Di8Q-9Ro.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-Dy1jGj3U.mjs} +645 -119
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B-inAJmt.mjs} +137 -113
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-xxWKNO3-.mjs} +22 -17
  27. package/dist/exec-DK8KaVHK.mjs +133 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-ChPFZMRC.mjs} +836 -813
  29. package/dist/{guide-BTAlD1_y.mjs → guide-DwmaD_Wa.mjs} +953 -209
  30. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  31. package/dist/idle-DpRLp5lj.mjs +157 -0
  32. package/dist/{build-slots-C-oAHRT1.mjs → in-use-CjUluqIX.mjs} +102 -8
  33. package/dist/{ios-BcCiI4TS.mjs → ios-B2Fpqvlb.mjs} +60 -71
  34. package/dist/{ios-BP04pY5f.mjs → ios-Dd-hYJBh.mjs} +241 -234
  35. package/dist/{ios-device-CFXA46ez.mjs → ios-device-BiOsSVzd.mjs} +3 -2
  36. package/dist/{ios-device-CauFJiYx.mjs → ios-device-CtRdEVjx.mjs} +149 -52
  37. package/dist/launch-verify-CQS_Kg-C.mjs +365 -0
  38. package/dist/{logs-CakdBTtm.mjs → logs-MARIy6m9.mjs} +20 -15
  39. package/dist/metro-BWLOJ_Wr.mjs +409 -0
  40. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  41. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-CDY9IkVC.mjs} +23 -43
  42. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  43. package/dist/{ownership-Cp52QAK1.mjs → ownership-D2xbLVoz.mjs} +2 -3
  44. package/dist/{teardown-BJqEzPo_.mjs → ownership-DZjdx8Gq.mjs} +664 -612
  45. package/dist/{ports-BIbJaKNW.mjs → ports-CLSSFOuz.mjs} +3 -3
  46. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  47. package/dist/{project-DDZWf_16.mjs → project-BXZzOABt.mjs} +8 -3
  48. package/dist/{reload-BdypPbjM.mjs → reload-DoETJ3rl.mjs} +11 -13
  49. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DVbb7kXH.mjs} +2 -2
  50. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-BvOCeakp.mjs} +8 -7
  51. package/dist/server-expo-DBVTdult.mjs +2 -0
  52. package/dist/{settings-BXXorVM2.mjs → settings-DOugPlPz.mjs} +59 -159
  53. package/dist/settings-EWYNwe72.mjs +378 -0
  54. package/dist/settings.schema.json +1064 -0
  55. package/dist/simslim-Ca3487H6.mjs +436 -0
  56. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  57. package/dist/spawn-claims-CUleC2cM.mjs +49 -0
  58. package/dist/{start-CmxtOKjU.mjs → start-F9gv5dcC.mjs} +159 -114
  59. package/dist/start-VoFsPSes.mjs +2 -0
  60. package/dist/{state-DZobzxLW.mjs → state-WGdIbi_d.mjs} +5 -3
  61. package/dist/{stats-BYcZ6n1h.mjs → stats-BAgJaLA4.mjs} +66 -5
  62. package/dist/{stats-CLg5RuDS.mjs → stats-DBYJ5bxL.mjs} +4 -4
  63. package/dist/status-DLCD-o3s.mjs +764 -0
  64. package/dist/status-Dr4fRkym.mjs +216 -0
  65. package/dist/{guide-status-Craq9z0t.mjs → stim-installations-CINXy_su.mjs} +3 -122
  66. package/dist/{stop-C8JW6kKp.mjs → stop-Dj4pfmyv.mjs} +111 -31
  67. package/dist/{stop-B1kOpYei.mjs → stop-ZvOeTmKb.mjs} +2 -2
  68. package/dist/supervisor-run.d.mts +11 -2
  69. package/dist/supervisor-run.mjs +228 -42
  70. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-BXA96myD.mjs} +14 -9
  71. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  72. package/dist/worktree-4aZg2NnI.mjs +2 -0
  73. package/dist/{worktree-CFLA-6MX.mjs → worktree-BMS7GeqB.mjs} +523 -61
  74. package/dist/{worktree-FG_ZKKdb.mjs → worktree-BlizKDpZ.mjs} +200 -54
  75. package/dist/{xcode-B6YaWeg0.mjs → xcode-B2tBkf0m.mjs} +66 -57
  76. package/package.json +6 -6
  77. package/skill/SKILL.md +2 -0
  78. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  79. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  80. package/dist/device-lease-Ffz_M5t0.mjs +0 -359
  81. package/dist/exec-B29XNuhP.mjs +0 -77
  82. package/dist/logs-query-BGL-Pl12.mjs +0 -290
  83. package/dist/metro-C7bcJ0uo.mjs +0 -233
  84. package/dist/ownership-wolE-0Do.mjs +0 -470
  85. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  86. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  87. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  88. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  89. package/dist/status-DdkLki47.mjs +0 -426
  90. package/dist/workspace-state-BT5E3SA2.mjs +0 -146
  91. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
package/README.md CHANGED
@@ -8,8 +8,8 @@ keep native and JavaScript builds warm across worktrees.
8
8
 
9
9
  ## Install
10
10
 
11
- If you previously installed `stim-cli` globally, follow the
12
- [migration instructions](#migrating-from-stim-cli) first.
11
+ Node 22.12.0 or later is required. If you previously installed `stim-cli`
12
+ globally, follow the [migration instructions](#migrating-from-stim-cli) first.
13
13
 
14
14
  ```bash
15
15
  npm install --global stim
@@ -22,18 +22,6 @@ Run without a global install when needed:
22
22
  npx stim <command>
23
23
  ```
24
24
 
25
- Node 22.12.0 or later is required.
26
-
27
- Machine defaults in `~/.stim/config.json` can enable or disable native artifact
28
- caching, remote caches, Metro sharing, iOS compiler caching and prefix mapping,
29
- and Android ccache/CAS, PCH, Gradle caching, and target ABI narrowing. The
30
- top-level `iosSimulatorApp` setting selects Xcode's viewer or an installed
31
- Siniulator.
32
- `.stim.json` runtime overrides apply per app, beside its `package.json`; monorepo
33
- apps do not inherit the repository-root file. Worktree-copy rules stay at the
34
- repository root. Run `stim guide settings` for the
35
- `optimizations` schema; existing defaults remain unchanged.
36
-
37
25
  ## Normal workflow
38
26
 
39
27
  ```bash
@@ -44,115 +32,29 @@ stim logs --errors
44
32
  stim stop
45
33
  ```
46
34
 
47
- For `stim logs --errors`, a clean check requires exit code 0 and no matching
48
- errors in the captured logs. Exit code 0 alone means the query succeeded, even
49
- when it prints errors. A workspace that has never produced a log timeline
50
- refuses with `STIM_NO_PROJECT`; in a monorepo, the error names the nearest
51
- registered descendant app with logs when one exists.
52
-
53
- Use `stim start --reset-cache` to recover from stale Metro transforms or file-map
54
- state. It restarts only this app's verified owned Metro, keeping its port and
55
- devices, with Metro's own reset: every store in the app's Metro config is cleared,
56
- including this app's shared transform store, so other worktrees of the same app
57
- rebuild their transforms too. Other apps and native build caches are unchanged.
58
-
59
- Use `stim doctor --platform ios` or `stim doctor --platform android` when only
60
- one native platform is in scope; shared project checks still run. Doctor also
61
- prints the running CLI version and the `stim` installation resolved from PATH,
62
- and flags a resolved installation that is older than another available one.
63
-
64
- For parallel iOS work, SimSlim is recommended as an optional way to reduce
65
- simulator background services and memory use. Run `stim guide lifecycle simslim`
66
- to review the service tradeoffs and configure a profile. Doctor recommends the
67
- setup without installing or enabling it. When a simulator cannot start
68
- processes, Stim bounds the wait and reports observed host memory pressure when
69
- available; free memory before retrying. A timeout alone does not prove OOM.
70
-
71
- For stale Android CMake launcher findings, stop native builds and run
72
- `stim doctor --fix --platform android` in the affected checkout. It clears
73
- affected ignored, untracked generated `.cxx` configurations in the app and
74
- installed native modules, then reports remaining findings. The next build
75
- recreates that output; source files, custom launcher settings, and shared
76
- ccache entries are preserved. Its cache-lock check cannot detect uncached,
77
- release-swap fallback, or direct Gradle builds; stop all native builds first.
78
-
79
35
  Stim builds or restores the app, installs it, launches it, and checks launch
80
36
  readiness. Plain output streams progress and reports the complete result. Use
81
37
  `--json` when a script needs structured data.
82
38
 
83
- To use an existing EAS development build, run `stim ios --eas-profile ios-simulator`
84
- or `stim android --eas-profile development` with your project's profile name.
85
- The profile must be an internal development build. Add `--device` for a connected
86
- phone; iOS requires `ios.simulator: true` for a simulator and false for a phone.
87
- Stim downloads a matching native build and connects it to the workspace's Metro.
88
- EAS CLI manages the downloaded artifact cache. An iOS provisioning failure
89
- points to EAS device registration and rebuild commands.
90
- On a miss it stops and prints the EAS build command, which may incur charges;
91
- it never triggers the build automatically. See `stim guide lifecycle eas` for
92
- setup, environment handling, and cache behavior.
93
-
94
- Use `stim ios --scheme "App Staging"` when a project has several shared Xcode
95
- app schemes. Combine it with `--configuration Release` if needed. Without the
96
- flag, Stim keeps its automatic scheme selection. Explicit schemes use separate
97
- artifact caches and Xcode build directories; run `stim guide lifecycle builds`
98
- for provider behavior. This selects an Xcode scheme, not a URL scheme.
99
-
100
- Launch evidence does not prove that the UI is interactive. Apps can optionally
101
- [declare readiness with two debug log messages](https://stim.appandflow.com/docs/dev-server-and-logs#optional-app-declared-readiness),
102
- without a package or SDK. A captured pending message extends the default
103
- three-second stability window after bundle delivery to a bounded wait for ready.
104
- Managed Metro response capture distinguishes a finished build from a finished
105
- response; servers without capture use the build-complete marker. Run
106
- `stim guide lifecycle readiness` for implementation instructions; verify the
107
- expected screen separately.
108
-
109
- `stim reload [ios|android]` requests a JavaScript reload in the live app on this
110
- workspace's owned local device. Use it when an error screen remains after a fix,
111
- and on Android after a failed first bundle load. An iOS app whose first bundle
112
- failed never connects to Metro, so reload cannot reach it. It reloads
113
- JavaScript and never restarts the app. It is not part of the normal workflow
114
- and does not build, install, boot, or launch an app. The platform is optional
115
- when only one app is live.
116
- Success confirms that the request was sent; Stim does not observe completion.
117
- Verify the expected UI on the reported device and inspect `stim logs --errors`
118
- before claiming recovery.
119
-
120
- For an isolated branch, create the worktree with Git, then warm it:
121
-
122
- ```bash
123
- git worktree add -b feature/settings ../feature-settings HEAD
124
- cd ../feature-settings
125
- stim worktree warm
126
- ```
127
-
128
- If a harness already created the linked worktree, run only `stim worktree warm`
129
- there. Warm copies missing ignored state from the source checkout, including
130
- eligible `.env` and local configuration files. Existing entries are preserved;
131
- existing ignored directories such as `node_modules` are skipped whole.
132
-
133
- `stim worktree warm --refresh` brings the source checkout up to date first: it
134
- checks the upstream, fetches changes when needed, fast-forwards whatever branch is checked out there, and installs what
135
- the new commits moved before copying. It refuses a source checkout it cannot
136
- move (uncommitted tracked changes, a rebase or merge in progress, a detached
137
- HEAD, a diverged branch) and never switches branches. One lock per repository
138
- keeps a copy from reading a `node_modules` a refresh is rewriting; plain warms
139
- still run side by side. A plain warm also refuses (`STIM_DEPS_INCOMPLETE`) when the last
140
- install of the lockfile on disk did not finish, instead of copying a partial
141
- `node_modules`; `stim worktree warm --refresh` reinstalls it.
142
-
143
- Wait for warm to finish before editing, installing dependencies, starting
144
- Metro/builds, or running another warm in that worktree. Concurrent writes are
145
- unsafe: entries created after the initial check can be overwritten or removed.
146
-
147
- After the work is preserved, `stim worktree remove` removes any linked
148
- worktree, warmed or not. Git-created branches stay. See the
149
- [worktree guide](https://stim.appandflow.com/docs/worktrees) for exclusions
150
- and cleanup rules.
151
-
152
- ## Reference
153
-
154
- The [documentation website](https://stim.appandflow.com/) explains the
155
- human workflow and all commands.
39
+ ## Documentation
40
+
41
+ The [documentation website](https://stim.appandflow.com/) is the full
42
+ reference:
43
+
44
+ - [Getting started](https://stim.appandflow.com/docs/getting-started): terms,
45
+ the first run, parallel worktrees, and what to do when a run fails.
46
+ - [Worktrees](https://stim.appandflow.com/docs/worktrees): `worktree warm`,
47
+ `--refresh`, `worktree remove`, and `gc --worktrees` to remove finished
48
+ worktrees in bulk.
49
+ - [Build caches](https://stim.appandflow.com/docs/build-caches): what `gc`
50
+ reports and trims, including the build outputs of idle workspaces.
51
+ - [Devices](https://stim.appandflow.com/docs/owned-devices): owned simulators
52
+ and emulators, physical devices, slots, and remote devices.
53
+ - [EAS development builds](https://stim.appandflow.com/docs/eas-builds).
54
+ - [Commands](https://stim.appandflow.com/docs/commands) and
55
+ [settings](https://stim.appandflow.com/docs/settings).
56
+ - [Troubleshooting](https://stim.appandflow.com/docs/troubleshooting): every
57
+ refusal code and its remedy.
156
58
 
157
59
  The installed CLI contains version-matched operational guidance:
158
60
 
@@ -163,10 +65,6 @@ stim <command> --help
163
65
  stim guide
164
66
  ```
165
67
 
166
- Runtime state defaults to `~/.stim`. Set `STIM_HOME` to move it. Stim manages
167
- owned simulators and emulators, leases connected physical devices, and supports
168
- configured remote devices.
169
-
170
68
  ## Migrating from stim-cli
171
69
 
172
70
  Remove the old global package before installing `stim`, since both provide the
@@ -180,36 +78,6 @@ npm install --global stim
180
78
  Update programmatic imports from `stim-cli/cache-manifest` to
181
79
  `stim/cache-manifest`. The `@stim-cli/*` packages keep their names.
182
80
 
183
- MIT License.
184
-
185
- ### Multiple devices in one workspace
186
-
187
- Use a stable slot name for each simulator, emulator, or physical device. Slots
188
- can use identical models and all share the workspace's Metro server and build
189
- cache. Omit `--slot` to reuse your existing default device.
190
-
191
- ```sh
192
- stim ios --slot phone
193
- stim ios --slot tablet --device-type "iPad Pro 13-inch (M4)"
194
- stim ios --slot hardware --device <udid>
195
- stim logs --slot tablet --source device
196
- stim stop --slot tablet
197
- ```
81
+ ## License
198
82
 
199
- `stim status` shows every slot. Plain `stim stop` stops the whole workspace;
200
- `--slot` preserves the shared server and other devices. `stim reload ios` or
201
- `stim reload android` can reload multiple connected devices. Read
202
- `stim guide lifecycle options` for launch verification and recycling behavior.
203
-
204
- ### Named server ports
205
-
206
- Use `stim ports get web` to reserve a workspace port for a server Stim does not
207
- manage, then pass that number to the server, for example
208
- `pnpm exec vite --port "$(stim ports get web)" --strictPort`.
209
- `stim ports` lists named allocations and managed Metro. `stim ports stop web`
210
- stops the TCP listener and releases its port; `--dry-run` previews the action.
211
- `stim ports release web` releases without stopping. Omitting the label selects
212
- all named ports. These commands leave Metro alone, and `stim stop` leaves
213
- named ports alone. Worktree removal and `gc --delete` stop and release named
214
- ports for removed workspaces. Read `stim guide ports` for the reserved band,
215
- listener ownership, and allocation limits.
83
+ MIT
@@ -1,9 +1,68 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { i as pidExists, o as signalProcessTree } from "./metro-C7bcJ0uo.mjs";
3
- import { c as androidDataPartitionSizeBytes } from "./settings-BXXorVM2.mjs";
4
- import { existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
5
- import { homedir, tmpdir } from "os";
1
+ import { u as getConfigDir } from "./config-BEq0Qw_a.mjs";
2
+ import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
+ import { P as withDirLock, c as signalProcessTree, i as pidExists } from "./metro-BWLOJ_Wr.mjs";
4
+ import { c as androidDataPartitionSizeBytes } from "./settings-DOugPlPz.mjs";
5
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, writeFileSync as writeFileSync$1 } from "fs";
6
7
  import { dirname, isAbsolute, join, resolve } from "path";
8
+ import { createdDevicesFile, createdDevicesLock, readCreatedDevices, readCreatedDevices as readCreatedDevices$1 } from "@stim-cli/core/state";
9
+ import { homedir, tmpdir } from "os";
10
+ //#region src/devices/created-devices.ts
11
+ function update(platform, change) {
12
+ const dir = getConfigDir();
13
+ withDirLock(createdDevicesLock(), () => {
14
+ const current = readCreatedDevices();
15
+ const entries = new Set(current[platform]);
16
+ if (!change(entries)) return;
17
+ const next = {
18
+ version: 1,
19
+ ios: [...current.ios],
20
+ android: [...current.android],
21
+ [platform]: [...entries]
22
+ };
23
+ const file = createdDevicesFile();
24
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
25
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
26
+ try {
27
+ renameSync(tmp, file);
28
+ } catch (error) {
29
+ rmSync(tmp, { force: true });
30
+ throw error;
31
+ }
32
+ }, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
33
+ }
34
+ function recordCreatedDevice(platform, id) {
35
+ update(platform, (entries) => entries.size !== entries.add(id).size);
36
+ }
37
+ function forgetCreatedDevice(platform, id) {
38
+ update(platform, (entries) => entries.delete(id));
39
+ }
40
+ //#endregion
41
+ //#region src/devices/device-ownership.ts
42
+ const OWNED_SIM_NAME = /^stim-[A-Za-z0-9._-]+ \(.* \d+(?:\.\d+)*\)(?: [A-Za-z0-9-]+)?$/;
43
+ const OWNED_AVD_NAME = /^stim-[A-Za-z0-9._-]+$/;
44
+ const GIB = 1024 ** 3;
45
+ function isOwnedSimName(name) {
46
+ return OWNED_SIM_NAME.test(name);
47
+ }
48
+ function isOwnedAvdName(name) {
49
+ return OWNED_AVD_NAME.test(name);
50
+ }
51
+ function avdConfigWrittenByStim(configIni) {
52
+ for (const line of String(configIni ?? "").split(/\r?\n/)) {
53
+ const match = /^disk\.dataPartition\.size=(\d+)$/.exec(line.trim());
54
+ if (match) return Number(match[1]) > 0 && Number(match[1]) % GIB === 0;
55
+ }
56
+ return false;
57
+ }
58
+ function isStimOwnedSim(sim, created = readCreatedDevices$1()) {
59
+ const name = sim.name ?? "";
60
+ return name.startsWith("stim-") && (created.ios.has(sim.udid) || isOwnedSimName(name));
61
+ }
62
+ function isStimOwnedAvd(name, readConfig, created = readCreatedDevices$1()) {
63
+ return isOwnedAvdName(name) && (created.android.has(name) || avdConfigWrittenByStim(readConfig()));
64
+ }
65
+ //#endregion
7
66
  //#region src/devices/android.ts
8
67
  const ANDROID_ABIS = /* @__PURE__ */ new Set([
9
68
  "armeabi-v7a",
@@ -38,7 +97,7 @@ function androidToolPath(tool, platform = process.platform) {
38
97
  const bare = location.at(-1);
39
98
  const name = platform === "win32" ? `${bare}${WINDOWS_SDK_TOOL_EXTENSIONS[tool]}` : bare;
40
99
  const abs = join(androidHome(), ...location.slice(0, -1), name);
41
- return existsSync(abs) ? abs : bare;
100
+ return existsSync$1(abs) ? abs : bare;
42
101
  }
43
102
  const WINDOWS_BUILD_TOOL_EXTENSIONS = { apksigner: ".bat" };
44
103
  function androidBuildToolName(tool, platform = process.platform) {
@@ -60,7 +119,7 @@ function buildToolsMajor(version) {
60
119
  const major = Number(String(version ?? "").split(/[.-]/)[0]);
61
120
  return Number.isFinite(major) ? major : 0;
62
121
  }
63
- function findBuildTool(tools, { home = androidHome(), readDir = readdirSync, exists = existsSync, platform = process.platform } = {}) {
122
+ function findBuildTool(tools, { home = androidHome(), readDir = readdirSync$1, exists = existsSync$1, platform = process.platform } = {}) {
64
123
  const root = join(home, "build-tools");
65
124
  let versions = [];
66
125
  try {
@@ -91,7 +150,7 @@ function androidTool(tool) {
91
150
  function listInstalledSystemImages() {
92
151
  const root = join(androidHome(), "system-images");
93
152
  const images = [];
94
- if (!existsSync(root)) return images;
153
+ if (!existsSync$1(root)) return images;
95
154
  for (const apiDir of safeList(root)) {
96
155
  const m = apiDir.match(/^android-(\d+)$/);
97
156
  if (!m) continue;
@@ -107,7 +166,7 @@ function listInstalledSystemImages() {
107
166
  }
108
167
  function safeList(dir) {
109
168
  try {
110
- return readdirSync(dir);
169
+ return readdirSync$1(dir);
111
170
  } catch {
112
171
  return [];
113
172
  }
@@ -132,6 +191,7 @@ async function createOwnedAvd(label, { systemImage, spawn = (...args) => getExec
132
191
  throw new Error(`No ${arch} Android system image is installed. Install one, e.g.: sdkmanager "system-images;android-36;google_apis;${arch}"`);
133
192
  }
134
193
  const avdName = ownedAvdName(label);
194
+ recordCreatedDevice("android", avdName);
135
195
  const tool = androidToolPath("avdmanager");
136
196
  const args = [
137
197
  "create",
@@ -183,7 +243,7 @@ function parseAvdSystemImage(configIni) {
183
243
  }
184
244
  return null;
185
245
  }
186
- function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8") } = {}) {
246
+ function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8") } = {}) {
187
247
  const directory = avdDirectory(avdName);
188
248
  if (!directory) return null;
189
249
  try {
@@ -195,13 +255,32 @@ function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFi
195
255
  function sanitizeAvdLabel(label) {
196
256
  return String(label).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
197
257
  }
258
+ function avdConfigIni(avdName, directory = ownedAvdDirectory(avdName)) {
259
+ if (!directory) return null;
260
+ try {
261
+ return readFileSync$1(join(directory, "config.ini"), "utf8");
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+ function registeredOrOrphanedAvdDirectory(avdName) {
267
+ return ownedAvdDirectory(avdName) ?? listOrphanedAvdDirectories(avdName)[0]?.directory ?? null;
268
+ }
269
+ function isStimOwnedAvdName(avdName, directory) {
270
+ return isStimOwnedAvd(avdName, () => avdConfigIni(avdName, directory === void 0 ? registeredOrOrphanedAvdDirectory(avdName) : directory));
271
+ }
198
272
  function ownedAvdName(label) {
199
273
  const clean = sanitizeAvdLabel(label);
200
274
  return `stim-${clean.startsWith("stim-") ? clean.slice(5) : clean}`;
201
275
  }
276
+ const AVDMANAGER_DELETE_TIMEOUT_MS = 12e4;
202
277
  function deleteAvd(avdName) {
203
- if (!avdName?.startsWith("stim-")) throw new Error(`Refusing to delete AVD "${avdName}": not a Stim-owned AVD (name must start with "stim-").`);
204
- getExecutor().run(`${androidTool("avdmanager")} delete avd -n "${avdName}"`);
278
+ if (!isStimOwnedAvdName(avdName)) throw new Error(`Refusing to delete AVD "${avdName}": not a Stim-owned AVD; Stim has no record of creating it.`);
279
+ getExecutor().run(`${androidTool("avdmanager")} delete avd -n "${avdName}"`, {
280
+ timeoutMs: AVDMANAGER_DELETE_TIMEOUT_MS,
281
+ killSignal: "SIGKILL"
282
+ });
283
+ forgetCreatedDevice("android", avdName);
205
284
  }
206
285
  function parseAvdList(text) {
207
286
  return text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("INFO") && !l.startsWith("WARNING"));
@@ -416,7 +495,7 @@ function avdStorageRoots() {
416
495
  }
417
496
  function avdPathExists(path) {
418
497
  try {
419
- lstatSync(path);
498
+ lstatSync$1(path);
420
499
  return true;
421
500
  } catch (error) {
422
501
  if (error.code === "ENOENT") return false;
@@ -427,17 +506,17 @@ function listOrphanedAvdDirectories(avdName) {
427
506
  const roots = avdStorageRoots();
428
507
  const stores = roots.filter(avdPathExists).map((root) => ({
429
508
  root,
430
- canonicalRoot: realpathSync(root),
431
- names: readdirSync(root)
509
+ canonicalRoot: realpathSync$1(root),
510
+ names: readdirSync$1(root)
432
511
  }));
433
512
  const registered = /* @__PURE__ */ new Set();
434
513
  for (const { root, canonicalRoot, names } of stores) for (const name of names.filter((entry) => entry.endsWith(".ini"))) {
435
514
  const path = join(root, name);
436
- const ini = readFileSync(path, "utf8");
515
+ const ini = readFileSync$1(path, "utf8");
437
516
  const targets = /* @__PURE__ */ new Set([...avdIniPaths(root, ini), ...avdIniPaths(canonicalRoot, ini)]);
438
517
  if (!targets.size) throw new Error(`Cannot verify AVD registration at ${path}; its data was kept.`);
439
518
  for (const target of targets) try {
440
- registered.add(realpathSync(target));
519
+ registered.add(realpathSync$1(target));
441
520
  } catch (error) {
442
521
  const code = error.code;
443
522
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
@@ -452,8 +531,8 @@ function listOrphanedAvdDirectories(avdName) {
452
531
  if (roots.some((candidate) => avdPathExists(join(candidate, `${name}.ini`)))) continue;
453
532
  const directory = join(canonicalRoot, entry);
454
533
  if (registered.has(directory) || !avdPathExists(directory)) continue;
455
- const stat = lstatSync(directory);
456
- if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(directory) !== directory) throw new Error(`Cannot verify AVD data at ${directory}; it was kept.`);
534
+ const stat = lstatSync$1(directory);
535
+ if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync$1(directory) !== directory) throw new Error(`Cannot verify AVD data at ${directory}; it was kept.`);
457
536
  if (!orphaned.some((candidate) => candidate.directory === directory)) orphaned.push({
458
537
  name,
459
538
  directory,
@@ -464,7 +543,7 @@ function listOrphanedAvdDirectories(avdName) {
464
543
  }
465
544
  return orphaned;
466
545
  }
467
- function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readFile = (path) => readFileSync(path, "utf8"), realpath = realpathSync, isDirectory = (path) => statSync(path).isDirectory() } = {}) {
546
+ function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readFile = (path) => readFileSync$1(path, "utf8"), realpath = realpathSync$1, isDirectory = (path) => statSync$1(path).isDirectory() } = {}) {
468
547
  if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return null;
469
548
  const roots = [
470
549
  env.ANDROID_AVD_HOME,
@@ -488,6 +567,21 @@ function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readF
488
567
  }
489
568
  return null;
490
569
  }
570
+ const AVD_USER_DATA = [
571
+ "userdata-qemu.img",
572
+ "userdata-qemu.img.qcow2",
573
+ "encryptionkey.img",
574
+ "encryptionkey.img.qcow2",
575
+ "cache.img",
576
+ "cache.img.qcow2",
577
+ "snapshots"
578
+ ];
579
+ function wipeAvdUserData(directory) {
580
+ for (const name of AVD_USER_DATA) rmSync$1(join(directory, name), {
581
+ recursive: true,
582
+ force: true
583
+ });
584
+ }
491
585
  function withAvdConfigOverrides(contents, overrides) {
492
586
  const newline = contents.includes("\r\n") ? "\r\n" : "\n";
493
587
  const trailingNewline = /\r?\n$/.test(contents);
@@ -509,10 +603,10 @@ function withAvdConfigOverrides(contents, overrides) {
509
603
  return updated.join(newline) + (trailingNewline ? newline : "");
510
604
  }
511
605
  let avdConfigWriteSequence = 0;
512
- function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} }, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8"), writeFile = (path, contents) => writeFileSync(path, contents, {
606
+ function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} }, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8"), writeFile = (path, contents) => writeFileSync$1(path, contents, {
513
607
  encoding: "utf8",
514
608
  flag: "wx"
515
- }), rename = renameSync, remove = (path) => rmSync(path, { force: true }) } = {}) {
609
+ }), rename = renameSync$1, remove = (path) => rmSync$1(path, { force: true }) } = {}) {
516
610
  const sizeBytes = androidDataPartitionSizeBytes(dataPartitionSizeGb);
517
611
  const directory = avdDirectory(avdName);
518
612
  if (!directory) throw new Error(`Could not resolve the content directory for newly created AVD ${avdName}.`);
@@ -547,7 +641,7 @@ function avdPoolConfiguration(dataPartitionSizeGb, avdConfig) {
547
641
  function ownedAvdMatchesConfiguration(avdName, configuration) {
548
642
  const directory = ownedAvdDirectory(avdName);
549
643
  if (!directory) return false;
550
- const contents = readFileSync(join(directory, "config.ini"), "utf8");
644
+ const contents = readFileSync$1(join(directory, "config.ini"), "utf8");
551
645
  const expected = JSON.parse(configuration);
552
646
  return Array.isArray(expected) && expected.length > 0 && expected.every((entry) => Array.isArray(entry) && entry.length === 2 && entry.every((value) => typeof value === "string") && contents.split(/\r?\n/).some((line) => line.trim() === `${entry[0]}=${entry[1]}`));
553
647
  }
@@ -630,7 +724,7 @@ const CRASH_REPORT_MODE_EMULATOR_VERSION = 37;
630
724
  function emulatorCrashDatabases(env = process.env) {
631
725
  const root = join(env.TMP || env.TEMP || tmpdir(), "AndroidEmulator");
632
726
  try {
633
- return readdirSync(root).filter((name) => /^emu-crash-.*\.db$/.test(name)).map((name) => join(root, name));
727
+ return readdirSync$1(root).filter((name) => /^emu-crash-.*\.db$/.test(name)).map((name) => join(root, name));
634
728
  } catch {
635
729
  return [];
636
730
  }
@@ -644,7 +738,7 @@ function emulatorCrashDatabases(env = process.env) {
644
738
  * removed instead, which has the same effect unless a crash handler is still writing into it.
645
739
  * https://github.com/appandflow/stim/issues/918
646
740
  */
647
- function suppressEmulatorCrashConsent({ platform = process.platform, version = emulatorMajorVersion, databases = emulatorCrashDatabases, remove = (path) => rmSync(path, {
741
+ function suppressEmulatorCrashConsent({ platform = process.platform, version = emulatorMajorVersion, databases = emulatorCrashDatabases, remove = (path) => rmSync$1(path, {
648
742
  recursive: true,
649
743
  force: true
650
744
  }) } = {}) {
@@ -662,6 +756,9 @@ function bootAndroidEmulator(avdName, consolePort, { logFile, platform = process
662
756
  avdName,
663
757
  "-port",
664
758
  String(consolePort),
759
+ "-grpc",
760
+ String(consolePort + 3e3),
761
+ "-grpc-use-token",
665
762
  ...headlessEmulatorArgs(),
666
763
  ...suppressEmulatorCrashConsent({ platform })
667
764
  ], {
@@ -675,8 +772,8 @@ function bootAndroidEmulator(avdName, consolePort, { logFile, platform = process
675
772
  function emulatorStdio(logFile) {
676
773
  if (!logFile) return "ignore";
677
774
  try {
678
- mkdirSync(dirname(logFile), { recursive: true });
679
- const fd = openSync(logFile, "w");
775
+ mkdirSync$1(dirname(logFile), { recursive: true });
776
+ const fd = openSync$1(logFile, "w");
680
777
  return [
681
778
  "ignore",
682
779
  fd,
@@ -781,7 +878,7 @@ function avdProcessLockPaths(avdDirectory, platform) {
781
878
  }
782
879
  function readAvdProcessId(path) {
783
880
  try {
784
- const pid = Number(readFileSync(path, "utf8").split("\0")[0]);
881
+ const pid = Number(readFileSync$1(path, "utf8").split("\0")[0]);
785
882
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error(`Could not read the emulator PID from process lock ${path}.`);
786
883
  return pid;
787
884
  } catch (error) {
@@ -824,7 +921,7 @@ function emulatorCrashHandlerPids(qemuPid) {
824
921
  ], { timeoutMs: 1e4 });
825
922
  return String(out ?? "").split(/\r?\n/).map((line) => Number(line.trim())).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
826
923
  }
827
- function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID_EMULATOR_SHUTDOWN_TIMEOUT_MS, pollMs = ANDROID_EMULATOR_SHUTDOWN_POLL_MS, platform = process.platform, resolveDirectory = ownedAvdDirectory, readProcessId = readAvdProcessId, processAlive = pidExists, directoryExists = (path) => statSync(path).isDirectory(), crashHandlerPids = emulatorCrashHandlerPids, killCrashHandler = (pid) => signalProcessTree(pid, "SIGKILL", { platform }), now = Date.now, sleep = sleepSync } = {}) {
924
+ function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID_EMULATOR_SHUTDOWN_TIMEOUT_MS, pollMs = ANDROID_EMULATOR_SHUTDOWN_POLL_MS, platform = process.platform, resolveDirectory = ownedAvdDirectory, readProcessId = readAvdProcessId, processAlive = pidExists, directoryExists = (path) => statSync$1(path).isDirectory(), crashHandlerPids = emulatorCrashHandlerPids, killCrashHandler = (pid) => signalProcessTree(pid, "SIGKILL", { platform }), now = Date.now, sleep = sleepSync } = {}) {
828
925
  const { directory, processId } = resolveAvdProcess(avdName, {
829
926
  platform,
830
927
  resolveDirectory,
@@ -852,20 +949,52 @@ function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID
852
949
  }
853
950
  if (!directoryExists(directory)) throw new Error(`Could not verify the content directory for owned AVD ${avdName} after shutdown.`);
854
951
  }
855
- function getAvdNameForSerial(serial, { timeoutMs } = {}) {
856
- const out = getExecutor().runQuiet(`${androidTool("adb")} -s ${serial} emu avd name`, { timeoutMs });
952
+ const EMU_AVD_NAME_TIMEOUT_MS = 1e4;
953
+ function getAvdNameForSerial(serial, { timeoutMs = EMU_AVD_NAME_TIMEOUT_MS } = {}) {
954
+ const out = getExecutor().runQuiet(`${androidTool("adb")} -s ${serial} emu avd name`, {
955
+ timeoutMs: Math.min(timeoutMs, EMU_AVD_NAME_TIMEOUT_MS),
956
+ killSignal: "SIGKILL"
957
+ });
857
958
  if (!out) return null;
858
959
  return out.split("\n")[0]?.trim() || null;
859
960
  }
860
- function resolveOwnedAvdSerial(avdName, { timeoutMs } = {}) {
961
+ function resolveOwnedAvdSerial(avdName, opts = {}) {
962
+ return ownedAvdSerialResolver(opts)(avdName);
963
+ }
964
+ function once(read) {
965
+ let result;
966
+ return () => {
967
+ if (!result) try {
968
+ result = { value: read() };
969
+ } catch (error) {
970
+ result = { error };
971
+ }
972
+ if ("error" in result) throw result.error;
973
+ return result.value;
974
+ };
975
+ }
976
+ /**
977
+ * `resolveOwnedAvdSerial` for several AVDs against one reading of the AVD list, `adb devices`, and
978
+ * each emulator's AVD name. The timeout budget starts when the resolver is created.
979
+ */
980
+ function ownedAvdSerialResolver({ timeoutMs } = {}) {
861
981
  const started = Date.now();
862
982
  const remaining = () => ({ timeoutMs: timeoutMs === void 0 ? void 0 : Math.max(1, timeoutMs - (Date.now() - started)) });
863
- if (!listAvds(remaining()).includes(avdName)) return { missing: true };
864
- if (!avdName?.startsWith("stim-")) return { notOwned: true };
865
- const adb = listAdbDevices(remaining());
866
- const match = [...adb.emulators, ...adb.unhealthy.filter((entry) => entry.kind === "emulator" && entry.consolePort !== void 0)].find((e) => getAvdNameForSerial(e.serial, remaining()) === avdName);
867
- if (match) return { serial: match.serial };
868
- return { notRunning: true };
983
+ const avds = once(() => listAvds(remaining()));
984
+ const adb = once(() => listAdbDevices(remaining()));
985
+ const names = /* @__PURE__ */ new Map();
986
+ const avdNameOf = (serial) => {
987
+ if (!names.has(serial)) names.set(serial, getAvdNameForSerial(serial, remaining()));
988
+ return names.get(serial);
989
+ };
990
+ return (avdName) => {
991
+ if (!avds().includes(avdName)) return { missing: true };
992
+ if (!isStimOwnedAvdName(avdName)) return { notOwned: true };
993
+ const devices = adb();
994
+ const match = [...devices.emulators, ...devices.unhealthy.filter((entry) => entry.kind === "emulator" && entry.consolePort !== void 0)].find((e) => avdNameOf(e.serial) === avdName);
995
+ if (match) return { serial: match.serial };
996
+ return { notRunning: true };
997
+ };
869
998
  }
870
999
  //#endregion
871
- export { ownedAvdName as A, waitForBoot as B, listAvds as C, nextConsolePort as D, memoizeEmulatorProbe as E, resetAdoptedAvd as F, resolveOwnedAvdSerial as I, resolvePhysicalDevice as L, physicalDeviceModel as M, pickDefaultSystemImage as N, ownedAvdDirectory as O, probeEmulatorSerial as P, shutdownAndroidEmulator as R, listAdbDevices as S, listOrphanedAvdDirectories as T, emulatorFailureRemedy as _, androidPoolNoCandidatesRefusal as a, getAvdNameForSerial as b, assertOwnedAvdStopped as c, avdStorageRoots as d, bootAndroidEmulator as f, emulatorDiskSpaceRemedy as g, deleteAvd as h, androidPoolCandidates as i, ownedAvdSystemImage as j, ownedAvdMatchesConfiguration as k, avdPathExists as l, createOwnedAvd as m, androidDeviceAbi as n, androidSystemImageAbi as o, configureNewOwnedAvd as p, androidHome as r, androidToolCwd as s, androidBuildToolName as t, avdPoolConfiguration as u, extractEmulatorFailure as v, listInstalledSystemImages as w, hostSystemImageArch as x, findBuildTool as y, waitForAndroidEmulatorShutdown as z };
1000
+ export { ownedAvdDirectory as A, resolvePhysicalDevice as B, isStimOwnedAvdName as C, listOrphanedAvdDirectories as D, listInstalledSystemImages as E, physicalDeviceModel as F, wipeAvdUserData as G, sleepSync as H, pickDefaultSystemImage as I, recordCreatedDevice as J, isStimOwnedSim as K, probeEmulatorSerial as L, ownedAvdName as M, ownedAvdSerialResolver as N, memoizeEmulatorProbe as O, ownedAvdSystemImage as P, resetAdoptedAvd as R, hostSystemImageArch as S, listAvds as T, waitForAndroidEmulatorShutdown as U, shutdownAndroidEmulator as V, waitForBoot as W, emulatorDiskSpaceRemedy as _, androidPoolNoCandidatesRefusal as a, findBuildTool as b, androidToolPath as c, avdPoolConfiguration as d, avdStorageRoots as f, deleteAvd as g, createOwnedAvd as h, androidPoolCandidates as i, ownedAvdMatchesConfiguration as j, nextConsolePort as k, assertOwnedAvdStopped as l, configureNewOwnedAvd as m, androidDeviceAbi as n, androidSystemImageAbi as o, bootAndroidEmulator as p, forgetCreatedDevice as q, androidHome as r, androidToolCwd as s, androidBuildToolName as t, avdPathExists as u, emulatorFailureRemedy as v, listAdbDevices as w, getAvdNameForSerial as x, extractEmulatorFailure as y, resolveOwnedAvdSerial as z };