stim 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/android-BW8i_YxU.mjs +204 -0
- package/dist/android-Bcmev2s2.mjs +2334 -0
- package/dist/android-K63_0UfF.mjs +727 -0
- package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-NFxUahnH.mjs +1149 -0
- package/dist/build-lock-BsVoLDaq.mjs +645 -0
- package/dist/build-slots-D0b6sJyo.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.d.mts +1 -0
- package/dist/collector-run.mjs +7 -5
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
- package/dist/deps-BLBsTNDS.mjs +435 -0
- package/dist/dev-client-RpvXxCXV.mjs +620 -0
- package/dist/device-CoCPmazS.mjs +250 -0
- package/dist/device-lease-Dpik41RY.mjs +335 -0
- package/dist/device-pool-B5pEVa3t.mjs +408 -0
- package/dist/device-remote-E81KAdXf.mjs +1209 -0
- package/dist/doctor-CqPeMsSm.mjs +1130 -0
- package/dist/doctor-D_CX7ib5.mjs +530 -0
- package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
- package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
- package/dist/gc-B7KkDinF.mjs +1665 -0
- package/dist/guide-BS2Xc24o.mjs +4503 -0
- package/dist/ios-B7KLgIp5.mjs +2733 -0
- package/dist/ios-D1h1Di39.mjs +451 -0
- package/dist/ios-device-D3pzTKQE.mjs +81 -0
- package/dist/ios-device-xNt0Lg6v.mjs +413 -0
- package/dist/logs-DcX9AvRF.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-COslRK_F.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-CGPart6T.mjs +60 -0
- package/dist/ownership-CYOyqxUo.mjs +475 -0
- package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
- package/dist/reclaim-C5Jmuahx.mjs +348 -0
- package/dist/reload-Z8nKifAk.mjs +332 -0
- package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
- package/dist/server-expo-Drd7ksHu.mjs +2 -0
- package/dist/settings-DwU1_U_q.mjs +708 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-BrY4EuIg.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
- package/dist/stats-BCWEVLrM.mjs +307 -0
- package/dist/stats-BsF15Bti.mjs +62 -0
- package/dist/status-DFI1Hfpg.mjs +393 -0
- package/dist/stop-B7DR4awK.mjs +3 -0
- package/dist/stop-Btg770vV.mjs +591 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
- package/dist/worktree-BPTbI-LN.mjs +1322 -0
- package/dist/worktree-C79xx_kM.mjs +543 -0
- package/dist/xcode-DeKCribl.mjs +1832 -0
- package/package.json +5 -5
- package/shim/bundle-response.cjs +5 -0
- package/dist/android-jc8MnUyL.mjs +0 -514
- package/dist/metro-store-COLl1pOk.mjs +0 -1249
- package/dist/server-expo-BZJm--wA.mjs +0 -2
|
@@ -0,0 +1,4503 @@
|
|
|
1
|
+
import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-DwU1_U_q.mjs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/guide/index.ts
|
|
5
|
+
const TOPICS = {
|
|
6
|
+
agent: {
|
|
7
|
+
summary: "The normal coding-agent workflow, safety rules, and topic routing",
|
|
8
|
+
body: () => `AGENT WORKFLOW
|
|
9
|
+
|
|
10
|
+
Use Stim to run React Native and Expo apps without sharing a Metro port or
|
|
11
|
+
device with another workspace. Prefer plain output: it streams each phase and
|
|
12
|
+
ends with the facts the next step needs. Use --json only when a script must
|
|
13
|
+
parse a stable payload.
|
|
14
|
+
|
|
15
|
+
TWO WORKFLOWS
|
|
16
|
+
|
|
17
|
+
SINGLE CHECKOUT: work in place, on whatever branch the task needs, in one
|
|
18
|
+
directory. start, ios, android, logs, stop, and never a linked worktree. That
|
|
19
|
+
directory is your workspace, and no rule below about keeping the source checkout
|
|
20
|
+
fit as a seed applies to it.
|
|
21
|
+
|
|
22
|
+
WORKTREE: the checkout you cloned is a seed. It stays clean and on the default
|
|
23
|
+
branch, and every task gets a linked worktree warmed from it. In this workflow
|
|
24
|
+
the source checkout is infrastructure, not a workspace: you edit, build, and run
|
|
25
|
+
in the worktree, and you keep the seed fit to copy. Every rule below about the
|
|
26
|
+
source checkout's fitness as a seed belongs to this workflow.
|
|
27
|
+
|
|
28
|
+
Doctor reports that fitness -- how far behind the seed is, uncommitted tracked
|
|
29
|
+
changes, an interrupted rebase or merge, a detached HEAD, a diverged branch, a
|
|
30
|
+
branch that is not the default one -- only once the repository has at least
|
|
31
|
+
one linked worktree, so read it from inside the worktree. A single-checkout
|
|
32
|
+
session is never told that its own branch is a problem.
|
|
33
|
+
|
|
34
|
+
NORMAL WORKFLOW
|
|
35
|
+
|
|
36
|
+
Work in the current checkout by default. When the task needs another branch or
|
|
37
|
+
an isolated environment, take the worktree workflow: create a linked worktree
|
|
38
|
+
with Git and warm its ignored state. If a harness already created this linked
|
|
39
|
+
worktree, run stim worktree warm here instead of creating another one. It
|
|
40
|
+
copies missing ignored paths from the source checkout, including eligible .env
|
|
41
|
+
and local configuration files. It preserves the branch, tracked files, and
|
|
42
|
+
every existing destination entry; existing ignored directories are skipped
|
|
43
|
+
whole, not filled in. Add --refresh to fast-forward the source checkout and
|
|
44
|
+
install what moved there before the copy; it refuses a source checkout with local
|
|
45
|
+
work and never switches branches. Read guide lifecycle options for exclusions
|
|
46
|
+
and incomplete-copy remedies.
|
|
47
|
+
|
|
48
|
+
Wait for warm to exit successfully (exit code 0) before running stim start,
|
|
49
|
+
stim ios, stim android, or a dependency install in that worktree. If the shell
|
|
50
|
+
tool returns a running session or job ID, poll or wait for that job to finish;
|
|
51
|
+
the ID is not completion. Concurrent writes to the destination are unsafe:
|
|
52
|
+
warm checks for existing entries before copying, not during the copy. Do not
|
|
53
|
+
edit files, install dependencies, or run another warm in that worktree until
|
|
54
|
+
it finishes; concurrent files can be overwritten or removed.
|
|
55
|
+
If warm fails or reports incomplete, resolve the reported failure first.
|
|
56
|
+
|
|
57
|
+
Before native worktree work, run doctor for the platform in scope. It checks
|
|
58
|
+
the source checkout from a linked worktree. Fix relevant findings and inspect the
|
|
59
|
+
upstream gap; in the single-checkout workflow those seed findings do not
|
|
60
|
+
appear. It also prints the running CLI version and the stim installation
|
|
61
|
+
resolved from PATH. If that resolved installation is older than another one,
|
|
62
|
+
fix PATH or the installation before continuing so commands and guidance match.
|
|
63
|
+
Doctor reports cross-volume staging and build-cache copies; read guide settings
|
|
64
|
+
for placement overrides and guide lifecycle options for warm behavior.
|
|
65
|
+
For iOS Debug architecture findings, review the project's overrides and imported
|
|
66
|
+
Podfile helpers using guide lifecycle options. Doctor --fix does not change them.
|
|
67
|
+
For parallel iOS work, review the recommended optional SimSlim setup in
|
|
68
|
+
stim guide lifecycle simslim. If simulator process startup times out, check
|
|
69
|
+
host memory pressure and free memory before retrying; do not restart other
|
|
70
|
+
workspaces' devices or close their apps without asking. That guide covers
|
|
71
|
+
the recovery steps and profile tradeoffs.
|
|
72
|
+
For a linked native library carrying Git metadata, add the printed .git entries to
|
|
73
|
+
.fingerprintignore only when the native build does not read Git state.
|
|
74
|
+
|
|
75
|
+
stim doctor --platform ios # or: --platform android
|
|
76
|
+
|
|
77
|
+
For stale Android CMake launcher findings, stop native builds and run
|
|
78
|
+
stim doctor --fix --platform android in the affected checkout before warming
|
|
79
|
+
more worktrees. It removes affected ignored, untracked generated .cxx
|
|
80
|
+
configurations, including installed native modules; the next build recreates
|
|
81
|
+
them. It preserves source, custom launcher settings, and the shared ccache.
|
|
82
|
+
|
|
83
|
+
# Skip Git creation if the harness already created this linked worktree.
|
|
84
|
+
git worktree add -b <branch> <worktree-path> HEAD
|
|
85
|
+
cd <worktree-path>
|
|
86
|
+
stim worktree warm
|
|
87
|
+
|
|
88
|
+
stim start
|
|
89
|
+
stim ios # or: stim android
|
|
90
|
+
|
|
91
|
+
Read stim guide lifecycle concurrency when a build waits on another workspace
|
|
92
|
+
or a build call times out. A native build can outlive a shell timeout; if the
|
|
93
|
+
tool call timed out, retry the same command and follow its printed remedy if
|
|
94
|
+
waiting times out.
|
|
95
|
+
|
|
96
|
+
# Reproduce the affected behavior and capture the baseline errors.
|
|
97
|
+
stim logs --errors
|
|
98
|
+
|
|
99
|
+
# If a native process exits with no report, inspect the captured device output.
|
|
100
|
+
# An empty query is not proof that a crashed app was healthy.
|
|
101
|
+
# See guide logs for JS/native symbolication and capture limits.
|
|
102
|
+
|
|
103
|
+
# Edit JavaScript or TypeScript; Fast Refresh applies the change.
|
|
104
|
+
# For UI work, wait for the expected UI and repeat the affected interaction
|
|
105
|
+
# on the reported device. Keep using the existing automation session, if any.
|
|
106
|
+
stim logs --errors
|
|
107
|
+
# Retain proof before cleanup: a screenshot, recording, or relevant runtime output.
|
|
108
|
+
|
|
109
|
+
stim stop
|
|
110
|
+
stim worktree remove
|
|
111
|
+
|
|
112
|
+
RULES DURING THE LOOP
|
|
113
|
+
|
|
114
|
+
- Run Stim from the app directory: the one whose package.json depends on
|
|
115
|
+
react-native or expo. Anywhere else -- a monorepo root, a tools package --
|
|
116
|
+
start, ios and android refuse with STIM_NO_PROJECT naming that package.json,
|
|
117
|
+
and doctor reports it as a finding.
|
|
118
|
+
- Put runtime .stim.json beside that app's package.json. Monorepo apps do not
|
|
119
|
+
inherit a repository-root runtime file. Keep repository-wide worktree-copy
|
|
120
|
+
rules at the source checkout root; see guide settings for the two scopes.
|
|
121
|
+
- Run start before a debug ios or android build. If it returns STIM_NO_METRO,
|
|
122
|
+
run stim start and retry.
|
|
123
|
+
- Run ios or android again after a native input changes. A JavaScript-only
|
|
124
|
+
change does not need one.
|
|
125
|
+
- For stale Metro transforms or file-map state, use stim start --reset-cache.
|
|
126
|
+
It restarts only this app's verified owned Metro, preserving devices and other
|
|
127
|
+
apps' caches. See guide lifecycle for reset scope and Expo requirements.
|
|
128
|
+
- Reload is not part of the normal workflow. Use stim reload on an owned local
|
|
129
|
+
simulator or emulator when an error screen remains after the fix, and on
|
|
130
|
+
Android after a failed first bundle load. It reloads JavaScript and never
|
|
131
|
+
restarts the app. An iOS app whose first bundle failed never connects to
|
|
132
|
+
Metro, so reload cannot reach it and says to use the device's own Reload
|
|
133
|
+
control. For a physical device that reached Metro, use agent-device metro
|
|
134
|
+
reload with the reported port. The detected iOS Local Network first-load
|
|
135
|
+
remedy uses UI automation instead because that app never established a Metro
|
|
136
|
+
connection.
|
|
137
|
+
- A successful stim reload confirms that the request was sent, not that new
|
|
138
|
+
JavaScript loaded or the screen recovered. Verify the expected UI on the
|
|
139
|
+
reported device and inspect stim logs --errors before claiming recovery.
|
|
140
|
+
- If launch reports an app error but also says the native process is alive,
|
|
141
|
+
the app did not crash. Fix JavaScript or TypeScript and use Fast Refresh. If
|
|
142
|
+
the error screen remains, follow the printed reload remedy instead of
|
|
143
|
+
running ios or android again. If launch says FATAL because the app process exited,
|
|
144
|
+
fix the crash and run the platform command again; Metro cannot restart it.
|
|
145
|
+
- ios and android install the app, launch it, and check readiness. Trust the
|
|
146
|
+
exact device, app, Metro, and launch facts in the final summary. Use the full
|
|
147
|
+
reported device ID. Never assume a simulator named booted belongs to this
|
|
148
|
+
workspace.
|
|
149
|
+
- After each ios or android run, give the user one compact result: exact device,
|
|
150
|
+
app id, launch state, cache result, total duration, and whether stim logs
|
|
151
|
+
--errors passed. Include a remedy only when action remains. Do not repeat the
|
|
152
|
+
phase transcript.
|
|
153
|
+
- An OK summary with no launch qualifier proves the launch. "bundle requested,
|
|
154
|
+
still building" means Metro has not finished; wait and query the logs. For
|
|
155
|
+
launch UNVERIFIED, follow the printed remedy before claiming success. JSON
|
|
156
|
+
reports these as true, "bundling", and "unverified" in launched.
|
|
157
|
+
WARNING means the native launch completed with app errors, or an app readiness
|
|
158
|
+
signal was expected but not confirmed; inspect the output before claiming a healthy UI.
|
|
159
|
+
- A clean logs --errors check requires exit code 0 AND no matching errors in
|
|
160
|
+
captured logs. Exit code 0 alone means the query succeeded, even when errors
|
|
161
|
+
were printed. Human output shows "No matching log records" on stderr for
|
|
162
|
+
zero matches; JSON mode prints zero bytes. This does not prove launch or log
|
|
163
|
+
capture succeeded. Do not read the NDJSON files directly.
|
|
164
|
+
- Use stim status when resuming a workspace or recovering missing device,
|
|
165
|
+
port, server, or build facts. A normal start and platform run already print
|
|
166
|
+
them. Use stim doctor when a build is unexpectedly slow or the environment
|
|
167
|
+
looks incomplete. If status reports a changed Android serial, rerun stim
|
|
168
|
+
android with the same build options to restore forwarding, then reopen your automation
|
|
169
|
+
session on the reported serial (guide lifecycle).
|
|
170
|
+
|
|
171
|
+
OWNERSHIP AND DELETION
|
|
172
|
+
|
|
173
|
+
Stim creates, boots, and deletes only devices it created. Owned simulators use
|
|
174
|
+
the stim-<label> (<model> <runtime>) name. Never point Stim at a user-created
|
|
175
|
+
emulator or simulator.
|
|
176
|
+
|
|
177
|
+
worktree remove parks the workspace's simulator or emulator for later adoption.
|
|
178
|
+
A parked device is Stim-owned: never delete one by hand. gc --delete clears verified
|
|
179
|
+
entries and keeps failures; see guide lifecycle pool. First launch on a
|
|
180
|
+
physical iPhone can need the one-time taps named by the remedy.
|
|
181
|
+
|
|
182
|
+
stim android --device [serial] and stim ios --device [udid] install on a
|
|
183
|
+
connected physical device. Stim never creates, boots, shuts down, or deletes
|
|
184
|
+
hardware. It records a temporary lease, not an owned-device registry entry.
|
|
185
|
+
|
|
186
|
+
A --device run leases that device for the run. stim device lock ios --for 10m
|
|
187
|
+
holds it across runs; stim device unlock gives it back. Never delete another
|
|
188
|
+
workspace's lease file under ~/.stim/device-locks; gc --delete removes expired
|
|
189
|
+
ones.
|
|
190
|
+
|
|
191
|
+
stop and worktree remove release this workspace's leases. On a physical
|
|
192
|
+
iPhone, stop also closes the app by ending its log collector; it does not
|
|
193
|
+
shut down the phone or uninstall the app.
|
|
194
|
+
|
|
195
|
+
Treat a refusal as an ownership or state mismatch: read its code and remedy.
|
|
196
|
+
Never reach for --force first.
|
|
197
|
+
Stim leaves externally started servers alone. Stop them with their original
|
|
198
|
+
tool; neither a matching port nor --force grants process ownership.
|
|
199
|
+
|
|
200
|
+
Ask the user before these actions:
|
|
201
|
+
|
|
202
|
+
- worktree remove, because it deletes the worktree and gives up its owned
|
|
203
|
+
device. It works with any linked worktree, warmed or not, without requiring
|
|
204
|
+
a Stim registry entry. Git-created branches are kept; a branch with an
|
|
205
|
+
existing Stim ownership record is deleted only when it has no unique commits.
|
|
206
|
+
- worktree remove --force, because it also discards uncommitted and untracked
|
|
207
|
+
files.
|
|
208
|
+
- gc --delete, because it deletes orphaned resources. gc --delete --cache all
|
|
209
|
+
empties the shared build caches instead; it inspects nothing else.
|
|
210
|
+
- stop when the workspace owns an EAS session, because it irreversibly ends
|
|
211
|
+
that remote session. For a local device, stop shuts it down but does not
|
|
212
|
+
delete it. An explicit stop shuts down a Stim-owned simulator even when
|
|
213
|
+
another process uses it. It never shuts down an unowned simulator.
|
|
214
|
+
|
|
215
|
+
SANDBOXES
|
|
216
|
+
|
|
217
|
+
An agent harness that sandboxes shell commands usually permits writes inside
|
|
218
|
+
the project and little else. Stim also needs writes to STIM_HOME (~/.stim by
|
|
219
|
+
default), simulator service access, and local access to the adb server. When
|
|
220
|
+
those sit outside the harness allowlist, the failure looks like an unwritable
|
|
221
|
+
directory or unavailable device service rather than a broken machine. Decide
|
|
222
|
+
at the start of a session whether to run Stim outside the sandbox or ask the
|
|
223
|
+
user to allow those operations. guide errors sandbox lists the exact
|
|
224
|
+
requirements.
|
|
225
|
+
|
|
226
|
+
LOAD ADVANCED GUIDANCE WHEN NEEDED
|
|
227
|
+
|
|
228
|
+
Read the matching guide before acting in these situations:
|
|
229
|
+
|
|
230
|
+
| Situation | Read |
|
|
231
|
+
| ----------------------------------------------------- | -------------------------------- |
|
|
232
|
+
| Build waiting on another workspace or tool timeout | stim guide lifecycle concurrency |
|
|
233
|
+
| --variant, scheme, or several APKs from assembleDebug | stim guide lifecycle options |
|
|
234
|
+
| Refusal with a CODE | stim guide errors <CODE> |
|
|
235
|
+
| Running under a sandbox | stim guide errors sandbox |
|
|
236
|
+
| Release configuration or ...Release variant | stim guide lifecycle release |
|
|
237
|
+
| Remote device, custom Metro, or tunnel | stim guide metro |
|
|
238
|
+
| Cache miss, bypass, or fingerprint exclusions | stim guide lifecycle builds |
|
|
239
|
+
| Capacity limits | stim guide lifecycle concurrency |
|
|
240
|
+
| Cache statistics from stim stats | stim guide facts stats |
|
|
241
|
+
| Worktree carry-over | stim guide lifecycle options |
|
|
242
|
+
| gc or orphaned resources | stim guide cleanup gc |
|
|
243
|
+
| worktree remove refusal or --force | stim guide errors remove |
|
|
244
|
+
| Cleanup failure or unverified cleanup ownership | stim guide errors teardown |
|
|
245
|
+
| Unfamiliar state or JSON field | stim guide facts payloads |
|
|
246
|
+
| Refusal without a code | stim guide errors |
|
|
247
|
+
|
|
248
|
+
Use the CODE exactly as printed; codes sharing a header resolve to the same
|
|
249
|
+
section. For a refusal without a code, find its quoted message in the errors
|
|
250
|
+
index. Ordinary stim stop and an authorized clean stim worktree remove do not
|
|
251
|
+
need the cleanup guide. A sectioned topic called without a section prints its
|
|
252
|
+
index; choose the narrowest section.
|
|
253
|
+
|
|
254
|
+
FULL TOPIC LIST
|
|
255
|
+
|
|
256
|
+
stim guide # list topics
|
|
257
|
+
stim guide errors # index of every refusal code and message
|
|
258
|
+
stim guide errors <CODE> # one refusal, e.g. stim guide errors STIM_NO_METRO
|
|
259
|
+
stim guide errors sandbox # running under a sandboxing harness
|
|
260
|
+
stim guide errors unverified # launch unverified, and the Local Network reason
|
|
261
|
+
stim guide errors fallbacks # swap, cache, and install notes on a release cache hit
|
|
262
|
+
stim guide lifecycle # the ordered flow, consent rules, and capacity
|
|
263
|
+
stim guide lifecycle verification # reproduce, edit, verify the UI, and retain proof
|
|
264
|
+
stim guide lifecycle readiness # add optional app readiness logs; no package required
|
|
265
|
+
stim guide lifecycle builds # build optimizations, optional cache warm-up, fingerprints
|
|
266
|
+
stim guide lifecycle concurrency # shared builds, wait timeouts, capacity limits
|
|
267
|
+
stim guide lifecycle options # every flag, Android variants, --device-type, --system-image
|
|
268
|
+
stim guide lifecycle devices # ios --device and android --device on a physical phone
|
|
269
|
+
stim guide lifecycle release # Release configurations and ...Release variants
|
|
270
|
+
stim guide facts # the --json payloads
|
|
271
|
+
stim guide facts devmenu # the Expo dev menu or Tools button over the app
|
|
272
|
+
stim guide metro # supervisor, custom Metro, tunnels, and remote devices
|
|
273
|
+
stim guide logs # filters, record shape, and capture limits
|
|
274
|
+
stim guide cleanup # what reclaims a device, and what deletes
|
|
275
|
+
stim guide cleanup collector # an unproven collector pid; why the app on a phone closed
|
|
276
|
+
stim guide settings # configuration files and supported keys`
|
|
277
|
+
},
|
|
278
|
+
facts: {
|
|
279
|
+
summary: "The --json payloads: `start`, `ios`, `android`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, and the error contract",
|
|
280
|
+
preamble: () => `FACTS CONTRACT
|
|
281
|
+
|
|
282
|
+
\`start\`, \`ios\`, \`android\`, \`reload\`, \`stop\`, \`status\`, \`stats\`, \`doctor\`,
|
|
283
|
+
and \`device lock\`/\`device unlock\` each print exactly ONE line of JSON on
|
|
284
|
+
stdout for \`--json\`. Every other line goes to stderr, so it is always safe
|
|
285
|
+
to pipe. \`logs --json\` is the one exception: it is NDJSON, one record per
|
|
286
|
+
line by design (see \`guide logs\`), not this single-payload contract.`,
|
|
287
|
+
sections: {
|
|
288
|
+
payloads: {
|
|
289
|
+
summary: "every field of the start, ios, android and reload payloads, the error contract, the device rules",
|
|
290
|
+
body: () => ` stim start --json
|
|
291
|
+
|
|
292
|
+
port the Metro port RESERVED for this workspace
|
|
293
|
+
supervisorPid the detached supervisor's pid, or NULL when a dev server was
|
|
294
|
+
already answering that Stim did not start
|
|
295
|
+
mode "bare-inproc" | "expo-child" | null (see \`guide metro\`)
|
|
296
|
+
logsDir where the NDJSON timeline is written
|
|
297
|
+
alreadyRunning true when nothing needed starting
|
|
298
|
+
|
|
299
|
+
stim ios --json
|
|
300
|
+
|
|
301
|
+
platform "ios"
|
|
302
|
+
udid the owned simulator this workspace installed onto, or the
|
|
303
|
+
phone's UDID on \`--device\`. A physical device gets no
|
|
304
|
+
owned-device registry entry; its ID is stored in a temporary
|
|
305
|
+
lease. \`stop\` releases workspace leases and \`gc --delete\`
|
|
306
|
+
removes expired lease files
|
|
307
|
+
deviceName its name, or null
|
|
308
|
+
deviceType the owned simulator's MODEL, as
|
|
309
|
+
\`xcrun simctl list devicetypes\` names it ("iPad Pro 13-inch
|
|
310
|
+
(M4)"). Read from the simulator itself, so a run driven by
|
|
311
|
+
the ios.deviceType setting reports it too, not only a
|
|
312
|
+
\`--device-type\` run. Null on \`--device\` and on a
|
|
313
|
+
simulator Stim does not own
|
|
314
|
+
runtime that simulator's iOS runtime version ("18.5"), from the same
|
|
315
|
+
record. Null on the same paths as deviceType
|
|
316
|
+
fingerprint the @expo/fingerprint hash of the native inputs, AS STORED.
|
|
317
|
+
A run that had to \`expo prebuild\` or \`pod install\`
|
|
318
|
+
rewrote fingerprinted files while it worked (the generated
|
|
319
|
+
native directory, package.json's scripts, the app config,
|
|
320
|
+
Podfile.lock), so the hash it looked up is not the hash the
|
|
321
|
+
tree has afterwards. The artifact is stored under the hash
|
|
322
|
+
computed AFTER those steps -- the one the next run in this
|
|
323
|
+
tree computes -- and this field reports that one. The shift
|
|
324
|
+
is printed on stderr as one dim line naming both short
|
|
325
|
+
hashes. A prebuild shift is RE-LOOKED-UP before anything
|
|
326
|
+
compiles (\`cache
|
|
327
|
+
hit 6564e2.. (post-prebuild key)\`), so a cold tree -- a
|
|
328
|
+
fresh worktree or clone of a CNG app -- installs an entry
|
|
329
|
+
another workspace already built instead of compiling
|
|
330
|
+
beside it. Android also fingerprints after Gradle because
|
|
331
|
+
Gradle plugins can rewrite native inputs while they build;
|
|
332
|
+
its artifact is stored only under that post-build hash. A
|
|
333
|
+
stable second fingerprint prints no shift line. If the iOS
|
|
334
|
+
fingerprint after prebuild or pod install, or the Android
|
|
335
|
+
fingerprint after Gradle, cannot be computed, the build is
|
|
336
|
+
installed but not cached, and fingerprint and cacheKey are null
|
|
337
|
+
configuration the Xcode configuration that was built ("Release" from
|
|
338
|
+
--configuration or the ios.configuration setting); null for
|
|
339
|
+
the default Debug
|
|
340
|
+
scheme the explicit shared Xcode scheme selected by --scheme;
|
|
341
|
+
absent for automatic selection; not the app URL scheme
|
|
342
|
+
cacheKey the shared-build-cache key derived from it (the
|
|
343
|
+
configuration is part of it: -release-sim vs -debug-sim)
|
|
344
|
+
cacheHit WHICH LEVEL answered, not a boolean:
|
|
345
|
+
"local" this machine's shared cache (free, instant)
|
|
346
|
+
"remote" the project's own Expo buildCacheProvider (a
|
|
347
|
+
download; it is copied into the local cache on
|
|
348
|
+
the way past, so the next workspace is "local")
|
|
349
|
+
false nothing answered, so it was compiled
|
|
350
|
+
webPreviewUrl only on a remote device that has one (an EAS Simulator
|
|
351
|
+
session): a browser URL showing that device's screen. Absent
|
|
352
|
+
on a local device. Hand it to the human -- it is the only way
|
|
353
|
+
to see a device that is not on this machine. Never open it ON
|
|
354
|
+
the device; it is a page, not a deep link.
|
|
355
|
+
cacheSkipped true only when --no-build-cache was passed: "nothing was
|
|
356
|
+
looked up", which is a different fact from "nothing was found"
|
|
357
|
+
compilationCache
|
|
358
|
+
Xcode compilation-cache activity for a compiled iOS app:
|
|
359
|
+
{ status: "reported", hits, cacheableTasks, hitRatePercent }
|
|
360
|
+
status is "not-run" when the artifact cache supplied the app.
|
|
361
|
+
status is "unavailable" when Xcode did not print reliable
|
|
362
|
+
statistics. This field is separate from cacheHit
|
|
363
|
+
waitedForBuild { pid, ms } when ANOTHER workspace was already compiling this
|
|
364
|
+
exact fingerprint and this run waited for its artifact instead
|
|
365
|
+
of compiling a second copy
|
|
366
|
+
(see \`guide lifecycle concurrency\`); null when nothing was
|
|
367
|
+
waited for.
|
|
368
|
+
cacheHit is "local" either way -- the artifact did come from
|
|
369
|
+
the local cache -- so this is what separates "it was already
|
|
370
|
+
there" (free) from "it was there twelve minutes later" (still
|
|
371
|
+
cheaper than a second build). Both commands carry it
|
|
372
|
+
appPath the .app that was installed
|
|
373
|
+
bundleId the iOS bundle id that was launched
|
|
374
|
+
installSkipped true when the artifact was ALREADY on the device byte for
|
|
375
|
+
byte, so nothing was installed and the run went straight to
|
|
376
|
+
launch (see \`guide lifecycle builds\`). false means an
|
|
377
|
+
install ran.
|
|
378
|
+
Always false on \`--device\`: proving a phone already holds
|
|
379
|
+
the bundle would cost more than installing it
|
|
380
|
+
launched true, "bundling", or "unverified". THE THREE ARE DIFFERENT
|
|
381
|
+
FACTS and only the last one is a problem.
|
|
382
|
+
true Metro finished the bundle response, then the app stayed
|
|
383
|
+
alive through a three-second stability window.
|
|
384
|
+
The command checks process liveness when the
|
|
385
|
+
platform exposes it. Errors from that window
|
|
386
|
+
are printed even when the app stays alive,
|
|
387
|
+
EXCEPT the device log's, which is COUNTED into
|
|
388
|
+
one \`launch\` line instead (see
|
|
389
|
+
\`guide logs\`). The agent decides whether a
|
|
390
|
+
nonfatal error matters.
|
|
391
|
+
IT IS NOT A PAINTED SCREEN. Stim observes the
|
|
392
|
+
bundle and the process, never a frame, and a
|
|
393
|
+
cold app can keep rendering for a minute or
|
|
394
|
+
more after this, which is why the stderr line
|
|
395
|
+
reads \`bundle loaded, process alive, stable
|
|
396
|
+
for 3s -- the first screen may still be
|
|
397
|
+
rendering\`. Poll the UI before you trust a
|
|
398
|
+
screenshot. Optional app-declared readiness
|
|
399
|
+
adds a separate stderr readiness phase; it
|
|
400
|
+
does not change this field. See
|
|
401
|
+
\`guide lifecycle readiness\`
|
|
402
|
+
"bundling" the request DID arrive and Metro was still
|
|
403
|
+
building or delivering when the bundle timeout closed.
|
|
404
|
+
The wiring is proven; the JS has simply not
|
|
405
|
+
run yet (a cold bundle of ~10k modules takes
|
|
406
|
+
longer than the window). Nothing to do --
|
|
407
|
+
no remedy list is printed for it -- and
|
|
408
|
+
\`logs --source metro\` shows the build
|
|
409
|
+
finishing
|
|
410
|
+
"unverified" nothing was observed at all: usually a
|
|
411
|
+
dev-client server picker awaiting a tap
|
|
412
|
+
See \`guide facts devmenu\` for the dev menu and its button.
|
|
413
|
+
metroPort the port the app was wired to; NULL on a non-Debug
|
|
414
|
+
configuration, whose JS is embedded and which is launched
|
|
415
|
+
with no dev server at all. There, \`launched\` is verified
|
|
416
|
+
by the app process staying alive after launch (a bad
|
|
417
|
+
embedded bundle crashes within seconds), not by a bundle
|
|
418
|
+
request. A process that exits fails the command. An iOS
|
|
419
|
+
launch with no process id is "unverified", and
|
|
420
|
+
\`stim logs --errors\` has the device log that says why
|
|
421
|
+
logs { dir }
|
|
422
|
+
durationMs wall time for the whole run
|
|
423
|
+
|
|
424
|
+
stim android --json
|
|
425
|
+
|
|
426
|
+
platform "android"
|
|
427
|
+
serial the owned emulator (always "emulator-<consolePort>")
|
|
428
|
+
avdName the AVD's NAME (stim-<label>). The serial is a slot --
|
|
429
|
+
emulator-5554 is whatever booted into that console port
|
|
430
|
+
first -- so this is what addresses the emulator in
|
|
431
|
+
\`emulator -avd\`, avdmanager, or a device tool. The console
|
|
432
|
+
port is CHOSEN AND RECORDED under the global config lock
|
|
433
|
+
BEFORE the emulator starts, then passed to it as \`-port\`,
|
|
434
|
+
so two workspaces booting at the same moment cannot land on
|
|
435
|
+
one serial. A boot that fails releases the port again and
|
|
436
|
+
keeps the AVD recorded for \`gc\`
|
|
437
|
+
deviceName the same name, matching the iOS payload's field
|
|
438
|
+
systemImage the sdkmanager package id the owned AVD was created from
|
|
439
|
+
("system-images;android-36;google_apis;arm64-v8a"), read from
|
|
440
|
+
the AVD's own config.ini, so a run driven by the
|
|
441
|
+
android.systemImage setting reports it too, not only a
|
|
442
|
+
\`--system-image\` run. Null on \`--device\` and on an
|
|
443
|
+
emulator Stim does not own
|
|
444
|
+
fingerprint / cacheKey / cacheHit / cacheSkipped / waitedForBuild /
|
|
445
|
+
appPath / installSkipped / launched
|
|
446
|
+
as above -- cacheKey keys on the VARIANT here
|
|
447
|
+
(<fingerprint>-productionrelease-sim). A Debug artifact for
|
|
448
|
+
a proven target ABI also ends in that ABI
|
|
449
|
+
(<fingerprint>-debug-sim-arm64-v8a)
|
|
450
|
+
variant the gradle variant that was built ("productionDebug" from
|
|
451
|
+
--variant or the android.variant setting); null for the
|
|
452
|
+
default assembleDebug. A variant whose name ENDS IN Release
|
|
453
|
+
is a release build: its JS is embedded and no dev server is
|
|
454
|
+
used
|
|
455
|
+
metroPort the port the app was wired to; NULL on a release-shaped
|
|
456
|
+
variant, exactly as on a non-Debug iOS configuration.
|
|
457
|
+
There, \`launched\` is verified by the app PROCESS being
|
|
458
|
+
alive on the device a moment after launch (\`pidof\`, then
|
|
459
|
+
\`ps -A\`), not by a bundle request -- "unverified" means
|
|
460
|
+
no process was found fails the command, and
|
|
461
|
+
\`stim logs --errors\` has the device log that says why
|
|
462
|
+
bundleId the ANDROID PACKAGE NAME the launch, the port wiring and
|
|
463
|
+
the remedies all target -- read from the BUILT APK's
|
|
464
|
+
manifest, which on a flavored project is the flavor's
|
|
465
|
+
applicationId, not what the project files say
|
|
466
|
+
debugHttpHost "10.0.2.2:<port>" on an emulator, "localhost:<port>" on a
|
|
467
|
+
physical device, when the app's SharedPreferences were
|
|
468
|
+
pointed at this workspace's Metro; null when they were not.
|
|
469
|
+
A healthy run reverses only <port> -> <port>, which is what
|
|
470
|
+
that host resolves to. Only when the write fails does Stim
|
|
471
|
+
also reverse 8081 -> <port>, so the app's compiled-in
|
|
472
|
+
default still finds this workspace's Metro
|
|
473
|
+
debugHttpHostNote
|
|
474
|
+
why the write did not land, when it did not. A launch
|
|
475
|
+
survives it -- this is the difference between the two
|
|
476
|
+
devClientUrl the expo-dev-client deep link that was opened, or null for
|
|
477
|
+
a plain launcher start. This is the command that puts the
|
|
478
|
+
app back on THIS workspace's bundle
|
|
479
|
+
ccache the Android C++ compilation cache, the counterpart of the
|
|
480
|
+
iOS compilationCache field:
|
|
481
|
+
{ status: "reported", hits, misses, hitRatePercent }
|
|
482
|
+
status is "not-run" when the artifact cache supplied the
|
|
483
|
+
APK. status is "unavailable" when no C++ compile went
|
|
484
|
+
through ccache -- ccache absent from PATH, a project that
|
|
485
|
+
sets its own CMake compiler launcher, or a Gradle run whose
|
|
486
|
+
native work was all up to date. None of the three is an
|
|
487
|
+
error, and this field is separate from cacheHit
|
|
488
|
+
logs the workspace log directory
|
|
489
|
+
durationMs wall time for the whole run
|
|
490
|
+
|
|
491
|
+
stim reload [ios|android] --json
|
|
492
|
+
|
|
493
|
+
Exit 0 and this payload confirm that the reload request was sent. They do
|
|
494
|
+
not prove that new JavaScript loaded or that the screen recovered. The
|
|
495
|
+
command does not observe completion. Verify the expected UI on deviceId
|
|
496
|
+
and inspect stim logs --errors before claiming recovery.
|
|
497
|
+
|
|
498
|
+
platform "ios" | "android"
|
|
499
|
+
deviceId the exact owned simulator UDID or emulator serial targeted
|
|
500
|
+
deviceName the owned simulator or AVD name
|
|
501
|
+
appId the live bundle id or Android package
|
|
502
|
+
metroPort the workspace's verified Metro port
|
|
503
|
+
strategy how the reload was addressed.
|
|
504
|
+
"metro-websocket" -- Metro named its clients and Stim
|
|
505
|
+
addressed every peer matching this platform. A workspace
|
|
506
|
+
Metro serves one app, so those peers are this app on however
|
|
507
|
+
many devices are attached to that port.
|
|
508
|
+
"metro-broadcast" -- this Metro cannot name its clients, so
|
|
509
|
+
the reload went to all of them and Stim cannot confirm appId
|
|
510
|
+
was among them. Verify the UI on deviceId; if it did not
|
|
511
|
+
change, reload from the app's own error screen or dev menu
|
|
512
|
+
targets how many peers the reload was addressed to, or null when
|
|
513
|
+
broadcast. Greater than 1 means several devices are running
|
|
514
|
+
this app on that Metro and the request addressed all of
|
|
515
|
+
them, not only deviceId. Completion is not observed
|
|
516
|
+
|
|
517
|
+
stim doctor --json
|
|
518
|
+
|
|
519
|
+
project the resolved app root
|
|
520
|
+
platform "ios" | "android" | null
|
|
521
|
+
stim { runningVersion, runningPath, resolved, installations,
|
|
522
|
+
versions, highestVersion, resolvedIsOlder }
|
|
523
|
+
resolved is the first executable named stim on PATH;
|
|
524
|
+
installations contains every distinct real executable on
|
|
525
|
+
PATH and the version each reports. resolvedIsOlder is true
|
|
526
|
+
only when that first executable is below the highest version
|
|
527
|
+
available from this invocation or PATH
|
|
528
|
+
findings the diagnostic findings; a lower resolved Stim is a
|
|
529
|
+
costs-time finding with a PATH or installation remedy
|
|
530
|
+
|
|
531
|
+
ON FAILURE
|
|
532
|
+
\`start\`, \`ios\` and \`android\` all print the error contract instead,
|
|
533
|
+
still one line on stdout, and exit 1:
|
|
534
|
+
|
|
535
|
+
{ "code": "STIM_NO_METRO", "message": "...", "remedy": "..." }
|
|
536
|
+
|
|
537
|
+
If a native build returned before the failure, this payload also carries
|
|
538
|
+
\`ccache\` (Android) or \`compilationCache\` (iOS), with the status and
|
|
539
|
+
counters described above. This includes failed builds and later install
|
|
540
|
+
or launch failures. The field is absent when no native build returned.
|
|
541
|
+
|
|
542
|
+
Branch on \`code\`, never on the message text. \`guide errors\` enumerates
|
|
543
|
+
every code.
|
|
544
|
+
|
|
545
|
+
RULES
|
|
546
|
+
- Never hardcode or guess a udid/serial/port. Read them from the payload.
|
|
547
|
+
- Pass them EXPLICITLY to every device tool you drive yourself
|
|
548
|
+
(agent-device, xcrun simctl, adb -s, idb).
|
|
549
|
+
- Never assume "booted" is your simulator. Other agents have theirs booted
|
|
550
|
+
too.
|
|
551
|
+
- Every device Stim creates or boots is one Stim created, named
|
|
552
|
+
stim-<label> (<model> <runtime>) on iOS. New local device labels combine
|
|
553
|
+
the git worktree directory and app directory names, e.g.
|
|
554
|
+
pr6460-tlon-mobile. Equal names collapse to one; outside git, the app
|
|
555
|
+
directory name is used. An iOS name collision adds the workspace ID
|
|
556
|
+
after the model and runtime, preserving it when the label is truncated.
|
|
557
|
+
Existing owned iOS simulators are renamed on reuse. Android keeps
|
|
558
|
+
existing and adopted AVD names. The exceptions are
|
|
559
|
+
\`android --device\` and
|
|
560
|
+
\`ios --device\`, which use a connected physical device Stim never
|
|
561
|
+
creates, boots, or deletes.`
|
|
562
|
+
},
|
|
563
|
+
devmenu: {
|
|
564
|
+
summary: "why the Expo dev menu or Tools button is or is not over the app, per platform and device kind",
|
|
565
|
+
body: () => ` EVERY DEV-CLIENT DEEP LINK CARRIES disableOnboarding=1
|
|
566
|
+
INSIDE ITS PROJECT URL
|
|
567
|
+
(\`...?url=http%3A%2F%2Fhost%3Aport%2F%3FdisableOnboarding%3D1&disableFab=1\`),
|
|
568
|
+
and expo-dev-launcher finishes its own dev-menu ONBOARDING
|
|
569
|
+
when it reads it. That is all the flag does: it sets
|
|
570
|
+
EXDevMenuIsOnboardingFinished. ON iOS it has to sit on the
|
|
571
|
+
PROJECT url -- the value of the \`url\` parameter -- because
|
|
572
|
+
that is the URL the launcher hands to the check; on the
|
|
573
|
+
outer deep link it does nothing there. Android reads it on
|
|
574
|
+
either.
|
|
575
|
+
ON A SIMULATOR, before a local dev-client openurl, Stim
|
|
576
|
+
preapproves CoreSimulatorBridge for exactly the installed
|
|
577
|
+
bundle id and discovered scheme on its owned simulator. That
|
|
578
|
+
suppresses iOS's first-launch confirmation;
|
|
579
|
+
unrelated schemes remain unapproved. It also writes
|
|
580
|
+
EXDevMenuShowsAtLaunch=false and
|
|
581
|
+
EXDevMenuShowFloatingActionButton=false, which the flag does
|
|
582
|
+
NOT cover, and those together are what keep the menu and its
|
|
583
|
+
button off a simulator entirely, so device automation opens
|
|
584
|
+
on the app. The
|
|
585
|
+
unverified warning therefore leads with the picker, then
|
|
586
|
+
prints the openurl
|
|
587
|
+
retry. ON LOCAL ANDROID the same deep link also carries the
|
|
588
|
+
\`EXDevMenuDisableAutoLaunch\` boolean intent extra, which
|
|
589
|
+
the launcher reads to set EXDevMenuShowsAtLaunch=false and
|
|
590
|
+
EXDevMenuIsOnboardingFinished=true. It stops the menu
|
|
591
|
+
opening automatically, but does NOT set expo-dev-menu's
|
|
592
|
+
showFab preference, so its floating Tools button can remain.
|
|
593
|
+
Remote Android opens only the URL, so that intent-extra
|
|
594
|
+
suppression does not apply there.
|
|
595
|
+
Every Stim deep link also carries an outer \`disableFab=1\`
|
|
596
|
+
query parameter. Versions with expo/expo#49651 use that as a
|
|
597
|
+
session-only override; earlier versions ignore it. Stim does
|
|
598
|
+
not rewrite expo-dev-menu's private SharedPreferences XML:
|
|
599
|
+
that internal file is not a supported API, and changing it
|
|
600
|
+
would persist over the user's own Tools-button setting. The
|
|
601
|
+
list leads with the supported launch command (\`am start -a
|
|
602
|
+
android.intent.action.VIEW -d '<devClientUrl>'
|
|
603
|
+
--ez EXDevMenuDisableAutoLaunch true\`).
|
|
604
|
+
ON A PHONE NONE OF THAT PREAPPROVAL APPLIES. The
|
|
605
|
+
preapproval and that write both go
|
|
606
|
+
through \`simctl spawn defaults write\`, and devicectl has
|
|
607
|
+
no defaults command; the one file route,
|
|
608
|
+
\`devicectl device copy to --domain-type appDataContainer\`
|
|
609
|
+
onto Library/Preferences/<bundleId>.plist with the app
|
|
610
|
+
terminated, copies successfully and then loses the seeded
|
|
611
|
+
keys, because cfprefsd serves its cached domain and rewrites
|
|
612
|
+
the file. THE FLAG ALONE DOES NOT COVER A PHONE:
|
|
613
|
+
EXDevMenuShowsAtLaunch defaults to TRUE on iOS
|
|
614
|
+
(DevMenuPreferences.setup), and DevMenuManager arms its
|
|
615
|
+
auto-launch observer when \`showsAtLaunch ||
|
|
616
|
+
shouldShowOnboarding()\`, so finishing onboarding clears
|
|
617
|
+
only the second half. THE LAUNCH ARGUMENTS COVER THE REST.
|
|
618
|
+
The device launch ends in
|
|
619
|
+
\`<bundleId> -- -EXDevMenuShowsAtLaunch 0
|
|
620
|
+
-EXDevMenuShowFloatingActionButton 0\`: devicectl passes
|
|
621
|
+
everything after \`--\` to the app, and NSUserDefaults reads
|
|
622
|
+
the argument domain AHEAD of the persisted one, so the menu
|
|
623
|
+
and its floating button are off for that launch and nothing
|
|
624
|
+
is written to the phone. So a fresh install comes up on the
|
|
625
|
+
app, not on the menu, and with no floating button.
|
|
626
|
+
THE FAB IS REAL ON A PHONE, and a screenshot is the only
|
|
627
|
+
way to see it: about four seconds after launch a blue gear
|
|
628
|
+
labelled Tools appears top-right over the app, the label
|
|
629
|
+
fades after roughly ten seconds, and the gear stays as a
|
|
630
|
+
translucent grey circle for the life of the app. It carries
|
|
631
|
+
no accessibility label after the fade, so
|
|
632
|
+
\`agent-device snapshot -i\` stops listing it. Measured
|
|
633
|
+
with the argument on: the corner is clean at 4s and at 12s.
|
|
634
|
+
Stim's own launch is the only one that
|
|
635
|
+
carries these: an app started ANOTHER way -- a home-screen
|
|
636
|
+
tap, a relaunch without the arguments -- still gets the
|
|
637
|
+
stored value, and on a fresh install that is the menu
|
|
638
|
+
(runtime version, Close, Reload, Go home) and the button.
|
|
639
|
+
\`agent-device press 'label="Close"'\` dismisses it -- or
|
|
640
|
+
\`snapshot -i\` and the ref. The onboarding key the flag
|
|
641
|
+
writes and the Local Network grant both survive an
|
|
642
|
+
UPGRADE install. Android's intent extra prevents the menu's
|
|
643
|
+
automatic launch; versions with expo/expo#49651 also honor
|
|
644
|
+
the session-only FAB flag in Stim's deep link.
|
|
645
|
+
The phone's unverified remedy is also ROUTED, not a fixed
|
|
646
|
+
list. When this launch's device records carry the Local
|
|
647
|
+
Network path reason, the remedy leads with that evidence and
|
|
648
|
+
with \`agent-device alert get\`, \`alert accept\`, then
|
|
649
|
+
\`snapshot -i\` and \`press 'label="Reload"'\` -- the grant
|
|
650
|
+
alone does not reload the dev client. Otherwise the network
|
|
651
|
+
list stays. Routing changes no record's level, so nothing new
|
|
652
|
+
reaches \`logs --errors\`. The OTHER first-launch tap,
|
|
653
|
+
developer trust, has no API at all and is always the user's.
|
|
654
|
+
\`guide errors unverified\` has the signature and the
|
|
655
|
+
full commands.`
|
|
656
|
+
},
|
|
657
|
+
stats: {
|
|
658
|
+
summary: "the stats payload, what counts as a run, hit, miss and failed, timeSavedMs, the heartbeat estimate",
|
|
659
|
+
body: () => ` stim stats --json
|
|
660
|
+
|
|
661
|
+
{ "version": 1,
|
|
662
|
+
"project": { "key": "<path>", "ios": <bucket|null>,
|
|
663
|
+
"android": <bucket|null> } | null,
|
|
664
|
+
"machine": { "ios": <bucket|null>, "android": <bucket|null> } }
|
|
665
|
+
|
|
666
|
+
\`project\` is null outside a project; a platform with no run yet is null.
|
|
667
|
+
A bucket carries runs, failed, hits, misses, coldRuns, coldRunMs, hitRuns,
|
|
668
|
+
hitRunMs, timeSavedMs, firstRunAt and lastRunAt, plus lastColdBuildMs and
|
|
669
|
+
lastPodsMs once the project has compiled or installed pods. Milliseconds are
|
|
670
|
+
integers.
|
|
671
|
+
|
|
672
|
+
HOW A RUN IS COUNTED (\`stats\`)
|
|
673
|
+
Every \`ios\` or \`android\` invocation that got as far as computing a
|
|
674
|
+
cache key is one run, in this project's bucket and in the machine-wide one.
|
|
675
|
+
The project key is the app's path IN THE SOURCE CHECKOUT, so every
|
|
676
|
+
worktree of a repository pools into one bucket and two apps in a monorepo
|
|
677
|
+
do not. A run that ends through an error or an uncaught exception counts
|
|
678
|
+
only as \`failed\`; \`launched: "unverified"\` or \`"bundling"\` is a
|
|
679
|
+
success. Otherwise the run's own \`cacheHit\` decides: "local" or "remote"
|
|
680
|
+
is a HIT, false is a MISS -- including a release run on a phone and a swap
|
|
681
|
+
that fell back to a full build. A miss adds its \`durationMs\` to the cold
|
|
682
|
+
runs; a hit adds it to the hit runs and credits \`timeSavedMs\` with this
|
|
683
|
+
project's mean cold run BEFORE it, minus its own duration, floored at zero.
|
|
684
|
+
A hit that WAITED for another workspace's build (\`waitedForBuild\`) counts
|
|
685
|
+
as a hit and is credited nothing: the compile it skipped was paid for in the
|
|
686
|
+
wait, and with no cold run recorded for this project and platform there is
|
|
687
|
+
nothing to compare against, so it credits nothing either. The saved figure
|
|
688
|
+
is therefore an ESTIMATE and is printed as one. Nothing per run is stored;
|
|
689
|
+
the file is $STIM_HOME/stats.json (see \`guide lifecycle builds\`).
|
|
690
|
+
|
|
691
|
+
A run also keeps the duration of its own two long phases in that bucket:
|
|
692
|
+
the build phase of a miss that compiled (lastColdBuildMs) and the last
|
|
693
|
+
\`pod install\` (lastPodsMs). The last value only, not a series. THAT IS
|
|
694
|
+
WHERE THE HEARTBEAT ESTIMATE COMES FROM. A later run reads this project's
|
|
695
|
+
bucket before it compiles, and prints:
|
|
696
|
+
|
|
697
|
+
build still compiling (1m00s of ~3m10s)
|
|
698
|
+
pods still installing (1m30s of ~1m40s)
|
|
699
|
+
|
|
700
|
+
The \`~\` value is THIS PROJECT'S LAST COLD BUILD, or its last
|
|
701
|
+
\`pod install\`, and never a mean: a project's build time drifts with its
|
|
702
|
+
size, so the most recent run is the best single guess. Past the estimate
|
|
703
|
+
the line reads \`(4m00s, usually ~3m10s)\`, because a slower machine is not
|
|
704
|
+
a hang. A project with no record yet gets \`(1m00s)\`, the elapsed alone,
|
|
705
|
+
and a warm run has no long phase to size. That read takes no lock and
|
|
706
|
+
ignores what it cannot read, so nothing about statistics can change a
|
|
707
|
+
run's outcome.`
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
metro: {
|
|
712
|
+
summary: "The dev server: `stim start`, the supervisor, and starting your own",
|
|
713
|
+
body: () => `THE DEV SERVER
|
|
714
|
+
|
|
715
|
+
stim start
|
|
716
|
+
stim start --remote # prepare Metro for a remote device
|
|
717
|
+
|
|
718
|
+
Reserves (or reuses) this workspace's Metro port, starts the dev server under a
|
|
719
|
+
detached SUPERVISOR, and waits until it both answers AND verifies as this
|
|
720
|
+
project's before exiting. You get your shell back with a bundler running: no
|
|
721
|
+
backgrounding idiom, no sleep, no poll loop, and no chance of building against
|
|
722
|
+
another worktree's bundler.
|
|
723
|
+
|
|
724
|
+
--json one line of facts on stdout, everything else on stderr:
|
|
725
|
+
{ port, supervisorPid, mode, logsDir, alreadyRunning }
|
|
726
|
+
--wait <seconds> how long to wait for server and remote tunnel readiness
|
|
727
|
+
(default 60 for each)
|
|
728
|
+
--remote expose Metro for a remote device
|
|
729
|
+
|
|
730
|
+
Plain \`stim start\` is local and does not create a public tunnel. Remote intent
|
|
731
|
+
comes from \`start --remote\`, \`ios.remote\`, or \`android.remote\`. The
|
|
732
|
+
\`metro.tunnel\` setting selects the provider after remote intent exists.
|
|
733
|
+
|
|
734
|
+
BUNDLE WARMUP
|
|
735
|
+
After verifying Metro, \`stim ios\` and \`stim android\` prefetch the
|
|
736
|
+
platform's development bundle while native work continues. Expo supplies the
|
|
737
|
+
entry point and bundle options through its manifest; bare React Native uses
|
|
738
|
+
the standard index entry and development bundle options, including lazy loading.
|
|
739
|
+
Warmup is enabled by default. Set optimizations.metroWarmup=false to disable
|
|
740
|
+
it on the next ios/android command, using the machine or project settings.
|
|
741
|
+
For custom entry points or bundle options, set metro.warmupUrl.ios and/or
|
|
742
|
+
metro.warmupUrl.android to the app's complete bundle URL or /path?query.
|
|
743
|
+
Stim keeps the path and query and uses the verified local Metro port.
|
|
744
|
+
Overrides replace discovery and receive no additional query defaults.
|
|
745
|
+
doctor validates configured URLs, including their platform; it does not
|
|
746
|
+
infer or auto-fix runtime native entry points or dev-menu bundle options.
|
|
747
|
+
See \`guide settings\` for configuration examples.
|
|
748
|
+
Each request times out after 60 seconds and does not keep the command alive.
|
|
749
|
+
Warmup failures do not fail the native build.
|
|
750
|
+
Release builds and \`--no-metro-check\` skip warmup. Servers without Stim's
|
|
751
|
+
prefetch-aware response observer also skip it; restart an older supervisor
|
|
752
|
+
with the current CLI to enable warmup.
|
|
753
|
+
|
|
754
|
+
Prefetch completion is not launch proof. Development launch verification
|
|
755
|
+
waits for the app's own bundle response. With a bundler started outside Stim,
|
|
756
|
+
device logs may prove a request, but bundle completion may stay unverified.
|
|
757
|
+
|
|
758
|
+
REMOTE DEVICE BACKENDS
|
|
759
|
+
Metro exposure and device selection are separate:
|
|
760
|
+
|
|
761
|
+
stim start --remote prepare public Metro
|
|
762
|
+
stim ios --remote proxy use an agent-device daemon
|
|
763
|
+
stim android --remote eas create an EAS Simulator session
|
|
764
|
+
|
|
765
|
+
The command or the matching ios.remote/android.remote setting selects the
|
|
766
|
+
backend. Environment variables never select the backend.
|
|
767
|
+
|
|
768
|
+
The proxy backend connects to an agent-device daemon on another machine. It
|
|
769
|
+
requires AGENT_DEVICE_DAEMON_BASE_URL and
|
|
770
|
+
AGENT_DEVICE_DAEMON_AUTH_TOKEN. Stim creates no remote session for it.
|
|
771
|
+
|
|
772
|
+
The EAS backend needs eas-cli and an account with EAS Simulator access. An
|
|
773
|
+
EAS session is billable. EAS does not inherit the proxy credentials. Always
|
|
774
|
+
tear the session down: \`stop\`, \`worktree remove\`, and \`gc --delete\`
|
|
775
|
+
can end sessions that Stim proves it owns.
|
|
776
|
+
|
|
777
|
+
IDEMPOTENT
|
|
778
|
+
A healthy dev server on the reserved port is a no-op: \`start\` prints the
|
|
779
|
+
facts with alreadyRunning: true and starts nothing. A foreign process holding
|
|
780
|
+
the reserved port moves the RESERVATION instead, so the project is never
|
|
781
|
+
stranded on a port it can never use.
|
|
782
|
+
|
|
783
|
+
WHAT THE SUPERVISOR IS
|
|
784
|
+
One detached process per workspace. There is no machine-wide daemon, nothing
|
|
785
|
+
to install, and no cross-project state. It hosts the dev server, writes its
|
|
786
|
+
output as NDJSON into the global workspace logs directory
|
|
787
|
+
($STIM_HOME/workspaces/<project>--<digest>/logs; see \`guide logs\`), and
|
|
788
|
+
records itself in that workspace's state.json before it starts serving. Two modes,
|
|
789
|
+
chosen by ecosystem detection:
|
|
790
|
+
|
|
791
|
+
bare-inproc bare React Native: Metro is hosted INSIDE the supervisor,
|
|
792
|
+
from the project's own node_modules, with Stim's reporter
|
|
793
|
+
attached. Bundler events, in-app console logs and redboxes
|
|
794
|
+
all arrive structured.
|
|
795
|
+
expo-child Expo: the project's own \`expo start --port <port>\` runs as
|
|
796
|
+
a child and its stdout is parsed into records. Levels are
|
|
797
|
+
INFERRED from each line, so those records carry raw: true.
|
|
798
|
+
|
|
799
|
+
In expo-child mode, remote intent plus metro.tunnel "expo" makes \`start\` pass
|
|
800
|
+
\`--tunnel\` and EXPO_UNSTABLE_TUNNEL_V2=1 (the legacy ws-tunnel path is
|
|
801
|
+
locked to port 8081, which every reserved port but the first collides with)
|
|
802
|
+
and records the URL Expo reports under state.json's metroTunnel. This has to
|
|
803
|
+
happen here: \`ios --remote <proxy|eas>\` / \`android --remote <proxy|eas>\`
|
|
804
|
+
cannot add \`--tunnel\` to
|
|
805
|
+
an already-running dev server. A later \`start --remote\` refuses with a
|
|
806
|
+
stop-and-restart remedy when a healthy local Expo supervisor has no recorded
|
|
807
|
+
Expo tunnel. See \`guide settings\` for metro.tunnel.
|
|
808
|
+
|
|
809
|
+
\`stim status\` reports the pid, the mode, and whether it is answering.
|
|
810
|
+
\`stim stop\` is the inverse of \`start\`: it halts the supervisor, reaps
|
|
811
|
+
the device-log collectors, shuts the owned device down (never deletes it)
|
|
812
|
+
and frees the port.
|
|
813
|
+
|
|
814
|
+
ENVIRONMENT: the supervisor -- and through it the dev server, including a
|
|
815
|
+
metro.config.js evaluated inside the expo child -- inherits the environment
|
|
816
|
+
of the \`start\` call that SPAWNED it. A later \`start\` that finds a healthy
|
|
817
|
+
supervisor is a no-op and cannot change a running supervisor's env: to apply
|
|
818
|
+
a new env var, \`stop\` first, then \`start\` with it set.
|
|
819
|
+
|
|
820
|
+
The supervisor's own stdio goes to the global workspace logs/supervisor.log, which is NOT
|
|
821
|
+
part of the NDJSON timeline. It is what a supervisor that died before it
|
|
822
|
+
could write a structured record leaves behind. In expo-child mode the child's
|
|
823
|
+
output is parsed into the TIMELINE instead, so a dev server that dies on a
|
|
824
|
+
config error leaves supervisor.log empty and its death cry in metro.ndjson.
|
|
825
|
+
A failed \`start\` quotes both for you: the supervisor.log tail when it has
|
|
826
|
+
one, and this attempt's error records from the timeline.
|
|
827
|
+
|
|
828
|
+
STARTING YOUR OWN BUNDLER STILL WORKS
|
|
829
|
+
A dev server YOU started is detected and left alone: \`start\` reports it
|
|
830
|
+
with supervisorPid: null and mode: null, exits 0, and starts nothing over it.
|
|
831
|
+
Starting a second bundler on a working one is the actual failure. For
|
|
832
|
+
\`start --remote\`, an external Expo server also needs metro.publicUrl because
|
|
833
|
+
Stim cannot add Expo tunnel mode to a process it does not supervise.
|
|
834
|
+
|
|
835
|
+
Start it from INSIDE the project directory, on the reserved port, or nothing
|
|
836
|
+
can attribute it to you:
|
|
837
|
+
|
|
838
|
+
Expo npx expo start --port <port>
|
|
839
|
+
Bare React Native npx react-native start --port <port>
|
|
840
|
+
Has its own start script run it and append --port <port>; it may carry
|
|
841
|
+
flags that matter (e.g. --client-logs)
|
|
842
|
+
Monorepo run from the APP directory, not the repo root
|
|
843
|
+
|
|
844
|
+
The reserved port comes from \`stim status\` or from a previous
|
|
845
|
+
\`start --json\`. Then \`stim ios\` accepts it: its Metro gate checks that
|
|
846
|
+
the process on the port answers /status AND runs from inside this project,
|
|
847
|
+
and yours does.
|
|
848
|
+
|
|
849
|
+
The cost is logs. Stim captures only a dev server it hosted, so
|
|
850
|
+
\`stim logs\` stays empty -- which is indistinguishable from a clean run --
|
|
851
|
+
and finding output is back to redirecting it to a file yourself. Prefer
|
|
852
|
+
\`start\`.
|
|
853
|
+
|
|
854
|
+
\`stop\` leaves an externally started server running. Stop it with the tool
|
|
855
|
+
that started it.`
|
|
856
|
+
},
|
|
857
|
+
logs: {
|
|
858
|
+
summary: "Querying the merged NDJSON timeline, and what --errors means",
|
|
859
|
+
body: () => `LOGS
|
|
860
|
+
|
|
861
|
+
stim logs [filters]
|
|
862
|
+
|
|
863
|
+
Reads every *.ndjson file in the global workspace logs directory, merges them into one timeline
|
|
864
|
+
ordered by timestamp, prints what matches, and EXITS. The file set is
|
|
865
|
+
discovered, not enumerated.
|
|
866
|
+
|
|
867
|
+
EXIT 0 MEANS THE QUERY SUCCEEDED, whether or not records matched. A clean
|
|
868
|
+
\`stim logs --errors\` check requires exit code 0 AND no matching errors in
|
|
869
|
+
captured logs. An empty result does not prove launch or log capture succeeded;
|
|
870
|
+
a workspace with no log directory also returns an empty result.
|
|
871
|
+
|
|
872
|
+
For zero matches: STDOUT IS EMPTY, exit code 0, and
|
|
873
|
+
one dim note on STDERR reading \`No matching log records in <logs dir>\`
|
|
874
|
+
(human mode only -- \`--json\` prints nothing at all, on either stream).
|
|
875
|
+
The only exit-1 paths are a malformed query and no project.
|
|
876
|
+
|
|
877
|
+
FLAGS
|
|
878
|
+
--source <s...> metro, client, device, build (one or more), or all. An
|
|
879
|
+
unknown value is REJECTED rather than quietly matching
|
|
880
|
+
nothing.
|
|
881
|
+
--level <l> minimum level: debug, info, warn, error, fatal
|
|
882
|
+
--since <d> only records newer than this: 30s, 5m, 2h
|
|
883
|
+
--grep <re> only records whose msg matches this regular expression
|
|
884
|
+
--tail <n> only the last n MATCHING records (applied after filtering,
|
|
885
|
+
so --level error --tail 5 is the last five ERRORS)
|
|
886
|
+
--errors errors and fatals since the last marker, from metro, client
|
|
887
|
+
and build, plus confirmed native app-crash reports.
|
|
888
|
+
Capped at 20 printed records.
|
|
889
|
+
--follow keep streaming until interrupted (Ctrl+C is exit 0)
|
|
890
|
+
--json the raw records, one per line, so stdout is valid NDJSON.
|
|
891
|
+
ZERO matches is ZERO bytes on stdout (an empty NDJSON
|
|
892
|
+
stream), exit 0 -- parse stdout line by line, never as one
|
|
893
|
+
JSON document. The "No matching log records" note is human
|
|
894
|
+
mode only, on stderr.
|
|
895
|
+
|
|
896
|
+
--ERRORS, PRECISELY
|
|
897
|
+
Level error or fatal, from metro, client and build, plus device records with
|
|
898
|
+
event native_crash (app/device/time-correlated OS reports or fatal app console output), timestamped after the
|
|
899
|
+
marker that closes their window. Three rules, and a field test
|
|
900
|
+
caught all three wrong at once -- it returned 3,004 iOS syslog lines on a
|
|
901
|
+
healthy app while hiding a real startup crash.
|
|
902
|
+
|
|
903
|
+
SCOPE. General device logs are NOT in the default scope. A device log is the OS talking:
|
|
904
|
+
\`simctl log stream\` is predicated on the app's PROCESS, and inside that
|
|
905
|
+
process Apple's frameworks log thousands of Error-typed lines (nw_socket,
|
|
906
|
+
SecTrust, WebKit, CoreUI) that have nothing to do with your app. The proven
|
|
907
|
+
ones are demoted to info by the collector; the scope rule covers the rest.
|
|
908
|
+
The metro stream carries exactly one demotion of its own, and it is Stim's
|
|
909
|
+
doing: the dev-client deep link \`ios\`/\`android\` open to wire the app to
|
|
910
|
+
your port arrives inside the app as a link, and React Navigation logs at
|
|
911
|
+
error that no navigator handled a NAVIGATE to \`expo-development-client\`.
|
|
912
|
+
There is no such screen and there is not meant to be, so that one record is
|
|
913
|
+
recorded at info -- it made every healthy cold launch report 1 error. A real
|
|
914
|
+
unhandled NAVIGATE names a route your app has, and is still an error.
|
|
915
|
+
A native crash can happen before JS or Metro exists. Confirmed native_crash
|
|
916
|
+
records are included without admitting the rest of the device noise. Opt back in with
|
|
917
|
+
\`--source device\` or \`--source all\`; a plain \`logs\` with no --errors
|
|
918
|
+
has always shown everything. ON A PHYSICAL IPHONE opting back in buys less
|
|
919
|
+
than it does on a simulator: the device console carries no severity, so
|
|
920
|
+
\`--source device --errors\` there reports crash and refusal lines only,
|
|
921
|
+
never a level. Read a phone's device records with a plain \`logs
|
|
922
|
+
--source device\`.
|
|
923
|
+
|
|
924
|
+
THE WINDOW. A marker closes the window for the sources it can speak for:
|
|
925
|
+
a BUNDLE marker (src metro: bundle_build_done / bundle_build_failed, or
|
|
926
|
+
Expo's "Bundled" / "Bundling failed" lines) is written when a bundle
|
|
927
|
+
attempt FINISHES, success or failure. It resets METRO errors from
|
|
928
|
+
before the attempt -- a resolve failure you fixed and rebuilt is
|
|
929
|
+
history, and when bundles fail back to back only the newest attempt's
|
|
930
|
+
errors are reported -- and nothing else. A failed attempt's own summary
|
|
931
|
+
and details land at or after its marker, so they stay reported.
|
|
932
|
+
a LAUNCH marker (src build, written before \`ios\` / \`android\` attempts
|
|
933
|
+
launch) resets EVERYTHING. It precedes the tool call so an immediate
|
|
934
|
+
native crash is not hidden by a marker written after the process died.
|
|
935
|
+
A finished bundle is not evidence that the app which loaded it is fine.
|
|
936
|
+
In the field case the app threw at 16:03:54 and Metro wrote its marker at
|
|
937
|
+
16:03:55, one second later, because the bundler finishes accounting for a
|
|
938
|
+
build after the client has already evaluated it. Under one marker for all
|
|
939
|
+
sources that crash was reported as nothing at all. The cost of the rule is
|
|
940
|
+
the safe direction: a client redbox that Fast Refresh already fixed keeps
|
|
941
|
+
being reported until the next launch.
|
|
942
|
+
|
|
943
|
+
OUTPUT. Every logs command, including --errors and --follow, shows full captured
|
|
944
|
+
error, component and native stacks, with no frame or message-length limit.
|
|
945
|
+
--errors shows the first 20 matching error records, plus stack context.
|
|
946
|
+
This record-count limit never truncates a stack. A footer reports hidden
|
|
947
|
+
records; use its printed --tail value to include all records before grouping.
|
|
948
|
+
To read the complete timeline without the default error-record limit:
|
|
949
|
+
stim logs --source all
|
|
950
|
+
This includes all sources, levels and history, not just the latest errors;
|
|
951
|
+
add --since, --level or --grep to narrow it. --source all selects sources,
|
|
952
|
+
not stack depth. For untouched captured records use:
|
|
953
|
+
stim logs --source all --json
|
|
954
|
+
Neither form can restore text the runtime truncated before capture.
|
|
955
|
+
In non-follow human output, an Expo error includes its immediately
|
|
956
|
+
following code frame and Call Stack lines. Bare React Native symbolication is
|
|
957
|
+
shown as separate context because Metro does not provide an error correlation
|
|
958
|
+
identifier. Context does not change the error count or the raw error records
|
|
959
|
+
returned by --json. --json is never capped, and neither is an explicit --tail.
|
|
960
|
+
|
|
961
|
+
In --follow mode the marker window is dropped -- every error arriving from
|
|
962
|
+
then on is by definition after the last marker seen.
|
|
963
|
+
|
|
964
|
+
\`stim status\` reports the same count per workspace, as
|
|
965
|
+
logs.errorsSinceMarker: the same query, the same scope, so the two can never
|
|
966
|
+
disagree about whether this workspace is failing.
|
|
967
|
+
|
|
968
|
+
THE RECORD
|
|
969
|
+
{ ts, src, level, msg } always. ts is epoch milliseconds; src is one of
|
|
970
|
+
metro / client / device / build; level is one of the five above.
|
|
971
|
+
Optional fields:
|
|
972
|
+
event the producer's own event name (bundle_build_done, client_log, ...)
|
|
973
|
+
stack frames of { file, line, column, fn }, passed through as reported
|
|
974
|
+
marker true on the records that close an error window
|
|
975
|
+
deviceTs Android logcat's original epoch milliseconds; ts is aligned to
|
|
976
|
+
host time using a bounded clock query at each collector attachment
|
|
977
|
+
clockOffsetMs the offset added to deviceTs; absent if the query failed.
|
|
978
|
+
A collector_clock warning then says timestamps retain device time.
|
|
979
|
+
raw true when the level was inferred from a line of text rather than
|
|
980
|
+
reported by the producer (every expo-child record)
|
|
981
|
+
|
|
982
|
+
WHAT WRITES WHAT
|
|
983
|
+
metro.ndjson the bundler, in both supervisor modes
|
|
984
|
+
client.ndjson in-app console logs and redboxes -- BARE PROJECTS ONLY.
|
|
985
|
+
In expo-child mode everything Expo prints lands in
|
|
986
|
+
metro.ndjson with raw: true, so \`--source client\`
|
|
987
|
+
returns nothing there.
|
|
988
|
+
device.ndjson the device-log collector uses \`simctl log stream\`
|
|
989
|
+
predicated on the app, or \`adb logcat\` filtered to
|
|
990
|
+
the app's pid. Local iOS simulator capture starts
|
|
991
|
+
before launch; Android attaches once the pid is known
|
|
992
|
+
and reads buffered logcat records. This
|
|
993
|
+
is where a native crash that never reached JS shows up
|
|
994
|
+
-- and, on iOS, where every Apple framework running in
|
|
995
|
+
the app's process also logs. The proven noise sources
|
|
996
|
+
are recorded at info rather than error; the rest is why
|
|
997
|
+
--errors leaves this source out unless asked. A VERIFIED
|
|
998
|
+
LAUNCH counts these records and prints one line:
|
|
999
|
+
|
|
1000
|
+
launch 9 general device error-level records
|
|
1001
|
+
(not confirmed app errors); inspect with
|
|
1002
|
+
stim logs --errors --source device
|
|
1003
|
+
|
|
1004
|
+
The count does not attribute unknown OS errors to the app.
|
|
1005
|
+
Known JavaScript errors also print individually: Android
|
|
1006
|
+
ReactNativeJS records, and iOS com.facebook.react.log /
|
|
1007
|
+
javascript records. These can interrupt an optional
|
|
1008
|
+
readiness wait even without a client or Metro copy.
|
|
1009
|
+
Client and Metro errors still print individually.
|
|
1010
|
+
A native app that loaded its bundle but reported app errors
|
|
1011
|
+
ends with WARNING rather than OK. This does not change the
|
|
1012
|
+
launch JSON or exit code: launched describes bundle/process
|
|
1013
|
+
evidence, not a healthy UI. Inspect the errors and readiness.
|
|
1014
|
+
|
|
1015
|
+
Only human ios/android launch output previews up to
|
|
1016
|
+
ten frames per error, component or native stack. App-source
|
|
1017
|
+
frames take priority; selected frames retain captured
|
|
1018
|
+
order, with an omitted-frame count. Escaped
|
|
1019
|
+
component stacks print one frame per line. Long bundle
|
|
1020
|
+
URLs are shortened and labeled unsymbolicated; shortening
|
|
1021
|
+
is not source-map resolution. A Metro error body that an
|
|
1022
|
+
Android DebugServerException embeds prints as the
|
|
1023
|
+
error's message and import stack, not its JSON. All
|
|
1024
|
+
logs commands show full
|
|
1025
|
+
captured stacks, including \`stim logs --errors\`.
|
|
1026
|
+
Use \`stim logs --source all\` for all sources/history,
|
|
1027
|
+
or add \`--json\` for raw
|
|
1028
|
+
records. This preview does not alter logs or JSON.
|
|
1029
|
+
|
|
1030
|
+
Symbolication is best effort. Human launch and non-follow
|
|
1031
|
+
logs queries ask the verified workspace Metro's /symbolicate
|
|
1032
|
+
endpoint to resolve captured JS coordinates, with a 2s
|
|
1033
|
+
request deadline and raw-coordinate fallback. Successful
|
|
1034
|
+
launch context is retained separately under logs/error-context
|
|
1035
|
+
so stopping Metro does not discard resolved evidence.
|
|
1036
|
+
Expo code frames are attached; uncorrelated bare Metro
|
|
1037
|
+
symbolication events remain separate. A component stack
|
|
1038
|
+
never substitutes for a missing error stack. Historical
|
|
1039
|
+
stacks need matching sources/maps, not a rebuilt bundle.
|
|
1040
|
+
|
|
1041
|
+
Cross-source copies combine only with matching error
|
|
1042
|
+
title, stack location, compatible platform and timing.
|
|
1043
|
+
Same-source repeats and raw JSON records stay intact.
|
|
1044
|
+
Human queries may attach a correlated device component
|
|
1045
|
+
stack to a selected Metro error, but never an unrelated
|
|
1046
|
+
device error.
|
|
1047
|
+
|
|
1048
|
+
iOS simulator cold launch attaches app stdout/stderr and
|
|
1049
|
+
passes Expo's initial URL directly. Fatal output is available
|
|
1050
|
+
before delayed OS reports, which can take about a minute
|
|
1051
|
+
or longer to appear; rerun logs --errors for those
|
|
1052
|
+
before stopping or releasing the device. Collection requires
|
|
1053
|
+
this workspace's current launch and device ownership or lease;
|
|
1054
|
+
afterward only already-captured reports remain available.
|
|
1055
|
+
The app cache holds console files, so external STIM_HOME
|
|
1056
|
+
paths do not violate the app sandbox. Crash evidence is
|
|
1057
|
+
retained in workspace logs. --follow does not poll for
|
|
1058
|
+
delayed OS reports.
|
|
1059
|
+
Native iOS simulator reports match app ID, simulator ID
|
|
1060
|
+
and launch time. atos resolves app addresses only after
|
|
1061
|
+
the local binary UUID matches the report. Android reads
|
|
1062
|
+
the crash buffer even when the app PID has exited. Java
|
|
1063
|
+
traces remain readable; C/C++ resolution uses NDK tools
|
|
1064
|
+
and matching local ELF build IDs. General OS noise remains
|
|
1065
|
+
excluded from --errors. Full native evidence is in
|
|
1066
|
+
logs --source device --json. Physical iPhone capture stays
|
|
1067
|
+
console-only; missing reports or symbols are not proof
|
|
1068
|
+
of a healthy app. No symbol downloads are performed.
|
|
1069
|
+
Android may keep a crashed Java PID alive behind its
|
|
1070
|
+
crash dialog. Follow the app-scoped force-stop remedy
|
|
1071
|
+
printed by android after fixing the crash, then rerun
|
|
1072
|
+
stim android. Metro reload alone cannot recover it.
|
|
1073
|
+
|
|
1074
|
+
The connection refusal \`TCP Conn ... Failed :
|
|
1075
|
+
error 0:61 [61]\` (61 is ECONNREFUSED) is not even
|
|
1076
|
+
counted. The app got its bundle over this workspace's
|
|
1077
|
+
Metro and outlived the stability window, so it
|
|
1078
|
+
recovered. A refusal before the
|
|
1079
|
+
launch verifies still prints, as does every record on a
|
|
1080
|
+
launch that does not verify, and the record stays an
|
|
1081
|
+
error in device.ndjson either way; read it with
|
|
1082
|
+
\`logs --errors --source device\`.
|
|
1083
|
+
|
|
1084
|
+
ON A PHYSICAL IPHONE THE SAME FILE CARRIES LESS, and the difference is not
|
|
1085
|
+
cosmetic. \`simctl spawn\` is simulator-only and there is no devicectl
|
|
1086
|
+
console subcommand, so a device run reads
|
|
1087
|
+
\`devicectl device process launch --console\`, which connects the app's own
|
|
1088
|
+
stdout and stderr and nothing else. Stim launches it with
|
|
1089
|
+
OS_ACTIVITY_DT_MODE, which makes os_log mirror itself onto that stderr --
|
|
1090
|
+
without it React Native's own logging, which goes through os_log, would not
|
|
1091
|
+
appear at all. What the mirror carries, and what it drops:
|
|
1092
|
+
|
|
1093
|
+
ts KEPT the device's own timestamp, off the mirrored line
|
|
1094
|
+
proc KEPT as name(pid), from the mirrored line, not a path
|
|
1095
|
+
category KEPT only when the logger has a subsystem; \`javascript\` and
|
|
1096
|
+
\`native\` for React Native's own log calls
|
|
1097
|
+
msg KEPT a multi-line message arrives as separate records
|
|
1098
|
+
subsystem LOST the mirror never prints it
|
|
1099
|
+
level LOST Default, Error and Fault all render identically, and
|
|
1100
|
+
Debug is not mirrored at all
|
|
1101
|
+
|
|
1102
|
+
So every device record from a phone is \`raw: true\` and \`info\`, except
|
|
1103
|
+
the lines that OPEN with a marker the runtime itself prints: an uncaught
|
|
1104
|
+
ObjC exception, a libc++abi termination, an assertion failure, or a Swift
|
|
1105
|
+
fatal error. The match is anchored, so an app logging ABOUT a crash stays
|
|
1106
|
+
info. devicectl's own \`ERROR:\` is read only on a line with no mirror
|
|
1107
|
+
prefix, because that is the only kind devicectl writes. Severity cannot be
|
|
1108
|
+
recovered, so it is not guessed. The NOISE_RULES that demote Apple's framework chatter key on
|
|
1109
|
+
subsystem and cannot fire either -- but they have less to do, because
|
|
1110
|
+
--console carries only the app's streams rather than every framework
|
|
1111
|
+
logging inside its process.
|
|
1112
|
+
|
|
1113
|
+
\`log collect --device-udid\` WOULD carry all six fields, in the same NDJSON
|
|
1114
|
+
the simulator path parses. It is not used because it requires root
|
|
1115
|
+
(\`log: Must be root to collect logs from attached device\`) and produces an
|
|
1116
|
+
archive rather than a stream. Streaming with full fidelity needs
|
|
1117
|
+
libimobiledevice or pymobiledevice3, which are third-party installs Stim
|
|
1118
|
+
does not require. See appandflow/stim#179.
|
|
1119
|
+
build-ios.ndjson the xcodebuild / gradle transcript at level debug, the
|
|
1120
|
+
build-android.ndjson extracted diagnostics at level error, and the launch as
|
|
1121
|
+
a marker record. One RUN's worth: each build starts the
|
|
1122
|
+
file over, so the first error in it always belongs to
|
|
1123
|
+
the run that pointed you at it.
|
|
1124
|
+
|
|
1125
|
+
Only a dev server Stim hosted is captured. If you started the bundler
|
|
1126
|
+
yourself, the metro and client sources stay empty -- which is not a sign of a
|
|
1127
|
+
clean build. The device and build sources are written either way, because
|
|
1128
|
+
\`ios\` / \`android\` produce them.
|
|
1129
|
+
|
|
1130
|
+
A collector is killed and replaced on the next \`ios\` / \`android\` run for
|
|
1131
|
+
that platform, and reaped by \`stop\`.`
|
|
1132
|
+
},
|
|
1133
|
+
errors: {
|
|
1134
|
+
summary: "Every refusal Stim can print: an index of codes, and one section for each",
|
|
1135
|
+
sectionHint: "<CODE>",
|
|
1136
|
+
preamble: () => `WHAT STIM REFUSES, AND WHY
|
|
1137
|
+
|
|
1138
|
+
Every refusal from \`ios\` / \`android\` carries a stable CODE. Branch on the
|
|
1139
|
+
code, never on the message.`,
|
|
1140
|
+
sections: {
|
|
1141
|
+
STIM_WORKSPACE_STATE: {
|
|
1142
|
+
summary: "$STIM_HOME/workspaces could not be prepared, or the digest directory belongs to another project",
|
|
1143
|
+
aliases: ["STIM_WORKSPACE_COLLISION"],
|
|
1144
|
+
separator: "--- BUILD-PATH CODES (`stim ios` / `stim android`) ---",
|
|
1145
|
+
body: () => `STIM_WORKSPACE_STATE / STIM_WORKSPACE_COLLISION
|
|
1146
|
+
Stim could not prepare this project's global workspace directory under
|
|
1147
|
+
$STIM_HOME/workspaces. Check that STIM_HOME is writable and has free
|
|
1148
|
+
space. An EPERM on a directory the user CAN write is a sandbox, not a
|
|
1149
|
+
permission bit -- see \`stim guide errors sandbox\`. COLLISION means the
|
|
1150
|
+
readable-name-plus-digest directory already has a workspace.json for a
|
|
1151
|
+
different canonical project path; do not overwrite it until you identify
|
|
1152
|
+
which workspace owns it.`
|
|
1153
|
+
},
|
|
1154
|
+
STIM_NO_METRO: {
|
|
1155
|
+
summary: "nothing provably this workspace's dev server holds the reserved port (ios, android, reload)",
|
|
1156
|
+
body: () => `STIM_NO_METRO
|
|
1157
|
+
Nothing that could be proven to be THIS workspace's dev server holds the
|
|
1158
|
+
reserved port -- or no port is reserved at all. The gate fires in about a
|
|
1159
|
+
second, before the device is even booted, rather than after four minutes of
|
|
1160
|
+
compiling an app that could not load a bundle. Run \`stim start\` first.
|
|
1161
|
+
\`--no-metro-check\` overrides it and wires the app to the reservation (or to
|
|
1162
|
+
8081 when there is none). A non-Debug \`ios --configuration\` never emits
|
|
1163
|
+
this: a release-shaped build embeds its JS, so the gate does not run at all.
|
|
1164
|
+
A port held by SOMETHING ELSE reports what: usually a bundler started from
|
|
1165
|
+
the wrong directory (the repo root instead of the app dir in a monorepo), or
|
|
1166
|
+
another repo's Metro. Restart it from inside the project, or free the port
|
|
1167
|
+
and run \`stim start\` to get a fresh reservation.
|
|
1168
|
+
|
|
1169
|
+
Reload requires the recorded launch's port to be this workspace's live
|
|
1170
|
+
Metro. It refuses a missing, changed, unresponsive, or foreign port.`
|
|
1171
|
+
},
|
|
1172
|
+
STIM_NO_FINGERPRINT: {
|
|
1173
|
+
summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
|
|
1174
|
+
body: () => `STIM_NO_FINGERPRINT
|
|
1175
|
+
\`@expo/fingerprint\` produced no hash, so the shared build cache cannot be
|
|
1176
|
+
addressed. Stim uses its declared @expo/fingerprint dependency directly,
|
|
1177
|
+
independently of the target project's package graph. This is a refusal
|
|
1178
|
+
rather than a silent full build because an unaddressable cache means every
|
|
1179
|
+
workspace on the commit compiles from scratch, forever.`
|
|
1180
|
+
},
|
|
1181
|
+
STIM_PREBUILD_FAILED: {
|
|
1182
|
+
summary: "expo prebuild could not generate the native directory",
|
|
1183
|
+
body: () => `STIM_PREBUILD_FAILED
|
|
1184
|
+
\`expo prebuild\` could not generate the missing native directory. The
|
|
1185
|
+
extracted output is above the code; the transcript is in
|
|
1186
|
+
the global workspace logs/build-<platform>.ndjson file.`
|
|
1187
|
+
},
|
|
1188
|
+
STIM_DEPS_FAILED: {
|
|
1189
|
+
summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
|
|
1190
|
+
body: () => `STIM_DEPS_FAILED
|
|
1191
|
+
\`pod install\` (iOS) or the gradle dependency sync (Android) failed. On iOS
|
|
1192
|
+
this runs only when Podfile.lock and Pods/Manifest.lock disagree, or Pods is
|
|
1193
|
+
absent -- which is exactly what a carried worktree produces.
|
|
1194
|
+
WHICH POD COMMAND: when the project root has a Gemfile and a Gemfile.lock
|
|
1195
|
+
that resolves cocoapods, pods go through bundler -- \`bundle check --dry-run\`,
|
|
1196
|
+
then \`bundle install\` only when that reports missing gems, then \`bundle exec
|
|
1197
|
+
pod install\` -- so the CocoaPods the lockfile pins is the one that writes
|
|
1198
|
+
Podfile.lock. Everything else gets plain \`pod install\`: no Gemfile, a Gemfile
|
|
1199
|
+
with no Gemfile.lock (\`bundle install\` would CREATE that tracked file in
|
|
1200
|
+
your checkout, which Stim will not do), and a Gemfile.lock that
|
|
1201
|
+
pins something other than pods, such as a fastlane-only bundle. When
|
|
1202
|
+
\`bundle\` is not on PATH the run prints one dim \`pods\` note and uses plain
|
|
1203
|
+
\`pod install\`. The \`pods\` phase line always names the command that ran, and
|
|
1204
|
+
the gem steps heartbeat under the \`gems\` label.
|
|
1205
|
+
Bundler runs with BUNDLE_FROZEN, so a Gemfile that no longer matches its
|
|
1206
|
+
Gemfile.lock FAILS the build rather than quietly falling back to unpinned
|
|
1207
|
+
pods -- silently using a different CocoaPods than the lockfile pins is the
|
|
1208
|
+
bug this path exists to kill. Run \`bundle install\` yourself and keep the
|
|
1209
|
+
result. Gems themselves are installed wherever BUNDLE_PATH points -- the
|
|
1210
|
+
project's own \`.bundle/config\` (vendor/bundle in the React Native template),
|
|
1211
|
+
or the environment. When that lands inside the project, Stim says so in a dim
|
|
1212
|
+
note naming which of the two set it; Gemfile.lock is never edited either way.
|
|
1213
|
+
\`worktree warm --refresh\` reports the same code for the install it runs in
|
|
1214
|
+
the SOURCE CHECKOUT -- the lockfile's own command (\`pnpm install\`,
|
|
1215
|
+
\`yarn install\`, \`bun install\`, \`npm ci\`) or that same pod ladder. The
|
|
1216
|
+
message names the command, quotes its last lines, and nothing is copied: fix
|
|
1217
|
+
the source checkout, then warm again.`
|
|
1218
|
+
},
|
|
1219
|
+
STIM_BUILD_FAILED: {
|
|
1220
|
+
summary: "xcodebuild or gradle failed; the two Android APK refusals; a damaged compilation-cache object",
|
|
1221
|
+
body: () => `STIM_BUILD_FAILED
|
|
1222
|
+
xcodebuild or gradle failed. The EXTRACTED diagnostics are printed (capped),
|
|
1223
|
+
not the transcript. Read the log path on the next line for the rest.
|
|
1224
|
+
Two Android refusals share this code without gradle itself failing:
|
|
1225
|
+
- MORE THAN ONE debug APK under android/app/build/outputs/apk and nothing
|
|
1226
|
+
configured to pick one (a project with product flavors, several flavors
|
|
1227
|
+
already built). Stim will not guess which flavor to install: the
|
|
1228
|
+
refusal lists the candidates -- pass \`--variant <name>\` or set the
|
|
1229
|
+
android.variant setting (e.g. "productionDebug") to the one you want.
|
|
1230
|
+
Flavors declared plainly in android/app/build.gradle are caught before
|
|
1231
|
+
the build instead (STIM_BAD_ARG); this one remains for the declarations
|
|
1232
|
+
that parse cannot read.
|
|
1233
|
+
- NO APK for the configured variant: the android.variant / --variant value
|
|
1234
|
+
does not name a real variant (\`./gradlew :app:tasks\` in android/ lists
|
|
1235
|
+
the assemble tasks).
|
|
1236
|
+
AN APK OLDER THAN THE BUILD IS NOT A REFUSAL. \`assembleDebug\` packages every
|
|
1237
|
+
flavor, so a later \`--variant previewDebug\` finds a current APK that gradle
|
|
1238
|
+
reports UP-TO-DATE and repackages nothing. Stim installs it. Gradle owns task
|
|
1239
|
+
freshness and the fingerprint owns cache freshness; Stim does not second-guess
|
|
1240
|
+
either from the file's mtime.
|
|
1241
|
+
|
|
1242
|
+
"failed to scan dependencies for source ..." on pods you did not touch (ios)
|
|
1243
|
+
The compilation cache holds a damaged object. Xcode reports it per source
|
|
1244
|
+
file, so it names whichever targets reach the object first -- often pods such
|
|
1245
|
+
as sqlite3, nanopb or libwebp -- and the list changes between runs. The
|
|
1246
|
+
transcript carries the cause:
|
|
1247
|
+
error: CAS-based dependency scan failed: not a IncludeTreeRoot node kind
|
|
1248
|
+
A cache write that a full disk or a killed build cut short leaves such an
|
|
1249
|
+
object, and upgrading the CLI does not clear it. Empty that one cache with
|
|
1250
|
+
\`gc --delete --cache "compilation cache"\`, then build again. The next
|
|
1251
|
+
build is a cold one.`
|
|
1252
|
+
},
|
|
1253
|
+
fallbacks: {
|
|
1254
|
+
summary: "release cache-hit notes that are not codes: swap failure, asset gate, uninstall, device fallbacks",
|
|
1255
|
+
body: () => `FALLBACK NOTES THAT ARE NOT CODES (release cache hits)
|
|
1256
|
+
On a release cache hit Stim regenerates this workspace's JS bundle into a
|
|
1257
|
+
COPY of the cached artifact before installing it -- \`ios --configuration
|
|
1258
|
+
Release\` into a copy of the .app, \`android --variant ...Release\` into a
|
|
1259
|
+
copy of the APK. When any step of that swap fails (the bundle command,
|
|
1260
|
+
hermesc, the re-sign, zipalign, apksigner), the run does NOT install the
|
|
1261
|
+
cached artifact -- its baked-in JS is the builder's, not yours -- and does
|
|
1262
|
+
NOT fail: it prints a \`swap failed at <step>: ... --
|
|
1263
|
+
building fresh instead\` note on stderr and falls back to a full build. If
|
|
1264
|
+
the run then fails, the code is the build's own (STIM_BUILD_FAILED etc.);
|
|
1265
|
+
the swap note above it says why the cache hit was not used. A swap that
|
|
1266
|
+
merely finds no hermesc notes it and embeds the plain JS bundle instead --
|
|
1267
|
+
that is a note, not a fallback.
|
|
1268
|
+
|
|
1269
|
+
ANDROID'S ASSET GATE is the second, and it is not a failure at all. Before
|
|
1270
|
+
re-packing, Stim compares this workspace's freshly emitted asset tree
|
|
1271
|
+
against the assets the cached APK carries. Any added, removed or changed
|
|
1272
|
+
asset prints
|
|
1273
|
+
|
|
1274
|
+
swap this workspace's asset set differs from the cached APK's
|
|
1275
|
+
(1 added, 0 changed, 0 removed; e.g. added
|
|
1276
|
+
res/drawable-mdpi/new_logo.png) -- building fresh instead
|
|
1277
|
+
|
|
1278
|
+
and the run does a full gradle build. There is nothing to fix: a drawable
|
|
1279
|
+
has a row in resources.arsc that only AAPT can write, so an APK cannot be
|
|
1280
|
+
made to carry an asset it was not built with, and installing one whose JS
|
|
1281
|
+
references a missing asset would 404 at runtime. Add an image, pay for one
|
|
1282
|
+
full build; the APK it produces becomes the new cache entry.
|
|
1283
|
+
|
|
1284
|
+
THE UNINSTALL NOTE is the third, and it COSTS THE APP'S DATA. A re-packed
|
|
1285
|
+
APK is signed with this machine's debug keystore, so the moment it meets a
|
|
1286
|
+
copy signed by CI the install is refused with
|
|
1287
|
+
INSTALL_FAILED_UPDATE_INCOMPATIBLE (or INSTALL_FAILED_VERSION_DOWNGRADE) and
|
|
1288
|
+
nothing but removing the package resolves it. A RELEASE run therefore
|
|
1289
|
+
uninstalls the package once, retries the install once, and prints
|
|
1290
|
+
|
|
1291
|
+
install com.example.app was already installed with a different signer,
|
|
1292
|
+
so it was uninstalled (its data went with it) before this APK
|
|
1293
|
+
could be installed
|
|
1294
|
+
|
|
1295
|
+
Debug runs never do this. A debug run meets the conflict on a physical
|
|
1296
|
+
device that already carries a store build, and there the colliding package is
|
|
1297
|
+
the user's real app: losing its data to a silent uninstall would be a worse
|
|
1298
|
+
bug than the one it fixes. A debug run fails with STIM_INSTALL_FAILED and
|
|
1299
|
+
hands you the uninstall to run yourself.
|
|
1300
|
+
|
|
1301
|
+
ON A PHONE the same uninstall costs one thing more: iOS drops the Settings >
|
|
1302
|
+
General > VPN & Device Management trust entry when the last app from that
|
|
1303
|
+
developer goes, and it clears the app's Local Network permission with it. The
|
|
1304
|
+
note says so, because the reinstall succeeds and then the LAUNCH is refused
|
|
1305
|
+
until someone taps Trust again. The retry is one uninstall and one install --
|
|
1306
|
+
it is not a way around a tap that has no API.
|
|
1307
|
+
|
|
1308
|
+
THE DEVICE FALLBACKS are the fourth, and both print a \`cache\` note and
|
|
1309
|
+
build fresh rather than failing:
|
|
1310
|
+
|
|
1311
|
+
cache a cached Release device app carries its builder's JS, and the
|
|
1312
|
+
device JS swap lands with phase 6 of appandflow/stim#178 --
|
|
1313
|
+
building fresh instead, which bakes in this workspace's JS
|
|
1314
|
+
|
|
1315
|
+
and a signing-gate refusal on a CACHED artifact -- an expired or foreign
|
|
1316
|
+
profile, a phone the profile does not name, an identity this keychain does not
|
|
1317
|
+
hold -- which prints the gate's own reason with \`-- building fresh instead\`.
|
|
1318
|
+
The same refusal on a FRESHLY BUILT app is a code (STIM_NO_PROFILE,
|
|
1319
|
+
STIM_PROFILE_MISMATCH, STIM_NO_SIGNING_IDENTITY), not a note: building again
|
|
1320
|
+
would produce the same app and refuse again.`
|
|
1321
|
+
},
|
|
1322
|
+
STIM_BUILD_WAIT_TIMEOUT: {
|
|
1323
|
+
summary: "waited ~90 minutes for another workspace's build of the same fingerprint",
|
|
1324
|
+
body: () => `STIM_BUILD_WAIT_TIMEOUT
|
|
1325
|
+
This run was waiting for ANOTHER workspace's build of the same fingerprint
|
|
1326
|
+
(see \`guide lifecycle concurrency\`), and no artifact arrived within ~90
|
|
1327
|
+
minutes.
|
|
1328
|
+
Replacement builders share that deadline, including time spent acquiring
|
|
1329
|
+
the lock between waits. A live builder may be wedged, or successive builders
|
|
1330
|
+
may have failed. The message names the current pid and lock directory:
|
|
1331
|
+
check the pid, and if it is not really building, remove that directory and
|
|
1332
|
+
run the command again.`
|
|
1333
|
+
},
|
|
1334
|
+
STIM_CLAIM_REFUSED: {
|
|
1335
|
+
summary: "a build lock exists whose holder cannot be identified; Stim neither removes it nor waits on it",
|
|
1336
|
+
body: () => `STIM_CLAIM_REFUSED
|
|
1337
|
+
A build lock records the holder's process IDENTITY, not just its pid, so a
|
|
1338
|
+
recycled pid reads as a gone builder rather than a live one, and a builder
|
|
1339
|
+
busy in a long \`simctl\` or gradle call reads as live rather than as stale.
|
|
1340
|
+
This code is the one state that cannot be decided: the claim file is truncated
|
|
1341
|
+
or not JSON, its identity token does not decode, or the holder spawned the
|
|
1342
|
+
process doing the work and was killed before recording which one.
|
|
1343
|
+
Stim will not remove a claim it cannot prove is dead, and it will not wait on
|
|
1344
|
+
one either -- a silent wait on a lock nobody holds is what this replaces. The
|
|
1345
|
+
message names the claim and the exact, shell-quoted removal that clears it --
|
|
1346
|
+
just that claim's file, not the lock directory around it; run that, then
|
|
1347
|
+
run the command again. Nothing was built, installed or removed.
|
|
1348
|
+
\`worktree warm\` reports it for the repository-wide warm claim under
|
|
1349
|
+
~/.stim/warm-locks on both paths, including a \`--refresh\` that spawned its
|
|
1350
|
+
install and was killed before recording which process: nothing was refreshed
|
|
1351
|
+
and nothing was copied. One case is NOT this code for a plain warm: a
|
|
1352
|
+
STIM_HOME (or a warm-locks path under it) that is a file rather than a
|
|
1353
|
+
directory. No claim can be stored there at all, which is a filesystem state
|
|
1354
|
+
that predates claims, so plain \`warm\` degrades to the unsynchronised copy it
|
|
1355
|
+
performed before them and \`--refresh\` still refuses.`
|
|
1356
|
+
},
|
|
1357
|
+
STIM_CLAIM_UNAVAILABLE: {
|
|
1358
|
+
summary: "this process has no recordable identity, so no build lock or build slot can be taken at all",
|
|
1359
|
+
body: () => `STIM_CLAIM_UNAVAILABLE
|
|
1360
|
+
Every ownership claim records the holder's process identity, captured through
|
|
1361
|
+
the \`unique-pid\` native module. This code is that capture failing: no
|
|
1362
|
+
prebuilt binary for this platform and architecture, or the OS refusing to
|
|
1363
|
+
report this process's start identity.
|
|
1364
|
+
Stim refuses rather than building without a claim. A run with no claim is
|
|
1365
|
+
invisible to every other run, so the single-flight lock and
|
|
1366
|
+
concurrency.maxBuilds would both be off at once, and two builds could compile
|
|
1367
|
+
the same fingerprint while each believed it was alone. Reinstall Stim so the
|
|
1368
|
+
module for this platform is present, then run the command again. Nothing was
|
|
1369
|
+
built, installed or removed.
|
|
1370
|
+
\`worktree warm --refresh\` refuses for the same reason, because it writes to
|
|
1371
|
+
the source checkout. Plain \`worktree warm\` does NOT: it prints one dim
|
|
1372
|
+
\`lock unavailable (...)\` line and copies unsynchronised. A copy only
|
|
1373
|
+
reads, so running it with no claim is what it did before the lock existed,
|
|
1374
|
+
while refusing it would break a warm that works today -- an unwritable
|
|
1375
|
+
STIM_HOME included.
|
|
1376
|
+
It still reads the claim set first, which takes no claim: a live \`--refresh\`
|
|
1377
|
+
claim, or a claim it cannot resolve, makes even the unsynchronised copy
|
|
1378
|
+
refuse, and the line names what it found. Only a repository nothing is
|
|
1379
|
+
warming is copied without a claim.`
|
|
1380
|
+
},
|
|
1381
|
+
STIM_INSTALL_FAILED: {
|
|
1382
|
+
summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
|
|
1383
|
+
body: () => `STIM_INSTALL_FAILED
|
|
1384
|
+
The artifact built or came from cache, but \`simctl install\` / \`adb install\` /
|
|
1385
|
+
\`devicectl device install app\` refused it. A signature or architecture
|
|
1386
|
+
mismatch, or a full device.
|
|
1387
|
+
On a PHONE (\`ios --device\`) the message carries devicectl's own text and the
|
|
1388
|
+
remedy names the cause it recognises: the phone is locked, the host is not
|
|
1389
|
+
trusted, Developer Mode is off, storage is full, or the app already on the
|
|
1390
|
+
phone was signed by a different team. Only that last one is retried -- one
|
|
1391
|
+
\`devicectl device uninstall app\`, one reinstall, and a warning that the
|
|
1392
|
+
app's data went with it -- along with the phone's developer trust and its
|
|
1393
|
+
Local Network permission, which iOS clears on an uninstall, so the launch
|
|
1394
|
+
after it may need the trust tap again. This is gated on \`--device\` rather
|
|
1395
|
+
than on the configuration, because every device run is signed, Debug
|
|
1396
|
+
included.
|
|
1397
|
+
On Android a signature or downgrade conflict names the package that is really
|
|
1398
|
+
installed -- the built APK's applicationId, which on a flavored project is
|
|
1399
|
+
the flavor's id and not the gradle namespace -- and gives you the
|
|
1400
|
+
\`adb -s <serial> uninstall <applicationId>\` that clears it. Re-running after
|
|
1401
|
+
that is a cache hit: one install, no build.`
|
|
1402
|
+
},
|
|
1403
|
+
STIM_LAUNCH_FAILED: {
|
|
1404
|
+
summary: "installed but would not start; the developer-trust tap on a phone",
|
|
1405
|
+
body: () => `STIM_LAUNCH_FAILED
|
|
1406
|
+
Installed, but the app would not start. On Android this usually means no
|
|
1407
|
+
launchable activity resolved.
|
|
1408
|
+
On a local iOS simulator, a timed-out launch can mean the simulator cannot
|
|
1409
|
+
spawn processes, even while it reports Booted. Check the reported memory
|
|
1410
|
+
pressure and free host memory before retrying; a timeout alone is not an OOM
|
|
1411
|
+
diagnosis. See \`stim guide lifecycle simslim\` for recovery and the optional
|
|
1412
|
+
SimSlim recommendation.
|
|
1413
|
+
On a PHONE it means the app never appeared in the device's own process list
|
|
1414
|
+
after \`devicectl device process launch\`, and the devicectl lines that
|
|
1415
|
+
explain it are quoted under the message. The refusal a first launch usually
|
|
1416
|
+
hits is the DEVELOPER TRUST one -- SpringBoard reports
|
|
1417
|
+
FBSOpenApplicationErrorDomain 3 with the reason Security -- and its remedy is
|
|
1418
|
+
the only one a human has to perform on the phone: Settings > General >
|
|
1419
|
+
VPN & Device Management, tap the developer profile under DEVELOPER APP, tap
|
|
1420
|
+
Trust, then run the command again. It is a per-developer-certificate tap, not
|
|
1421
|
+
a per-build one -- but an uninstall clears it, including the one Stim's own
|
|
1422
|
+
signer-conflict retry performs.`
|
|
1423
|
+
},
|
|
1424
|
+
STIM_NO_SCHEME: {
|
|
1425
|
+
summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
|
|
1426
|
+
body: () => `STIM_NO_SCHEME
|
|
1427
|
+
Stim could not list or select an app scheme in ios/. Share the intended app
|
|
1428
|
+
scheme so xcodebuild can see it. Select an available exact name with
|
|
1429
|
+
\`stim ios --scheme <name>\`. An unknown explicit name prints available choices.
|
|
1430
|
+
Without an explicit selector, a workspace
|
|
1431
|
+
name match wins; otherwise Stim accepts a sole non-test scheme, or a listed
|
|
1432
|
+
scheme matching app.json. Unmatched ambiguous schemes are refused.`
|
|
1433
|
+
},
|
|
1434
|
+
STIM_NO_PROFILE: {
|
|
1435
|
+
summary: "no or undecodable embedded.mobileprovision; build once from Xcode",
|
|
1436
|
+
separator: "--- iOS SIGNING CODES (`ios --device`, and only there) ---",
|
|
1437
|
+
context: `A simulator build needs no signature, which is why none of these can fire on
|
|
1438
|
+
the normal path. A device build carries one, and Stim re-seals any bundle it
|
|
1439
|
+
modifies with the identity the bundle already names -- so it checks, before
|
|
1440
|
+
spending a build or a bundle, that the check can succeed.`,
|
|
1441
|
+
body: () => `STIM_NO_PROFILE
|
|
1442
|
+
The built or cached .app has no embedded.mobileprovision, or
|
|
1443
|
+
\`security cms -D\` could not decode the one it has. The first means the build
|
|
1444
|
+
produced an unsigned app -- almost always a simulator-sliced artifact.
|
|
1445
|
+
Set a team and a Development profile for the target's configuration in
|
|
1446
|
+
Xcode > Signing & Capabilities, then BUILD ONCE FROM XCODE to install the
|
|
1447
|
+
profile. Stim will not do that step: registering a device or minting a
|
|
1448
|
+
profile changes your Apple Developer account, so Stim never passes
|
|
1449
|
+
-allowProvisioningUpdates.`
|
|
1450
|
+
},
|
|
1451
|
+
STIM_PROFILE_MISMATCH: {
|
|
1452
|
+
summary: "the profile is expired, has no ProvisionedDevices, or does not name this UDID",
|
|
1453
|
+
context: `A simulator build needs no signature, which is why none of these can fire on
|
|
1454
|
+
the normal path. A device build carries one, and Stim re-seals any bundle it
|
|
1455
|
+
modifies with the identity the bundle already names -- so it checks, before
|
|
1456
|
+
spending a build or a bundle, that the check can succeed.`,
|
|
1457
|
+
body: () => `STIM_PROFILE_MISMATCH
|
|
1458
|
+
The profile inside the app cannot admit this phone. Three shapes, and the
|
|
1459
|
+
message names which one and the profile type it found:
|
|
1460
|
+
- it expired, or carries no ExpirationDate at all;
|
|
1461
|
+
- it is an App Store or enterprise profile, which carries no
|
|
1462
|
+
ProvisionedDevices list -- so Stim cannot PROVE the phone is admitted and
|
|
1463
|
+
refuses rather than guessing. Local device runs need a development
|
|
1464
|
+
profile;
|
|
1465
|
+
- it is a development or ad hoc profile whose device list does not name
|
|
1466
|
+
this UDID. Register the UDID at developer.apple.com, regenerate the
|
|
1467
|
+
profile, and build once from Xcode.`
|
|
1468
|
+
},
|
|
1469
|
+
STIM_NO_SIGNING_IDENTITY: {
|
|
1470
|
+
summary: "no single keychain identity resolves; ios.signingIdentitySha1 for two certificates",
|
|
1471
|
+
context: `A simulator build needs no signature, which is why none of these can fire on
|
|
1472
|
+
the normal path. A device build carries one, and Stim re-seals any bundle it
|
|
1473
|
+
modifies with the identity the bundle already names -- so it checks, before
|
|
1474
|
+
spending a build or a bundle, that the check can succeed.`,
|
|
1475
|
+
body: () => `STIM_NO_SIGNING_IDENTITY
|
|
1476
|
+
No single keychain identity could be resolved to re-seal with. Either
|
|
1477
|
+
\`security find-identity -v -p codesigning\` lists nothing, or the identity
|
|
1478
|
+
the artifact's own profile names is absent, or two certificates share that
|
|
1479
|
+
common name and Stim -- being non-interactive -- will not pick one.
|
|
1480
|
+
Open Xcode > Settings > Accounts and download your certificates, or unlock
|
|
1481
|
+
the login keychain with \`security unlock-keychain\`. For the two-certificate
|
|
1482
|
+
case, set ios.signingIdentitySha1 to the SHA-1 hash beside the one you want.`
|
|
1483
|
+
},
|
|
1484
|
+
STIM_CODESIGN_FAILED: {
|
|
1485
|
+
summary: "codesign failed on the modified copy; the cache entry is untouched and the run builds fresh",
|
|
1486
|
+
context: `A simulator build needs no signature, which is why none of these can fire on
|
|
1487
|
+
the normal path. A device build carries one, and Stim re-seals any bundle it
|
|
1488
|
+
modifies with the identity the bundle already names -- so it checks, before
|
|
1489
|
+
spending a build or a bundle, that the check can succeed.`,
|
|
1490
|
+
body: () => `STIM_CODESIGN_FAILED
|
|
1491
|
+
\`codesign --force --sign\` or \`codesign --verify --strict\` exited non-zero
|
|
1492
|
+
on the modified copy. The verbatim codesign stderr is quoted, because it is
|
|
1493
|
+
the answer: a locked login keychain reports errSecInternalComponent, an
|
|
1494
|
+
ambiguous identity reports that it matched more than one. Unlock the keychain
|
|
1495
|
+
and confirm exactly one identity matches the name. The cache entry itself is
|
|
1496
|
+
never modified -- the failure is on a temporary copy, and the run builds
|
|
1497
|
+
fresh.`
|
|
1498
|
+
},
|
|
1499
|
+
STIM_NO_LAN_ADDRESS: {
|
|
1500
|
+
summary: "the Mac has no non-internal IPv4 interface; a tunnel cannot help a phone",
|
|
1501
|
+
separator: "--- iOS DEVICE DEBUG REACHABILITY CODES (`ios --device` in Debug) ---",
|
|
1502
|
+
context: `A phone does not share the host's loopback and USB carries no reverse forward,
|
|
1503
|
+
so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
|
|
1504
|
+
fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
|
|
1505
|
+
body: () => `STIM_NO_LAN_ADDRESS
|
|
1506
|
+
This Mac reports no non-internal IPv4 interface, so there is no address to
|
|
1507
|
+
give the phone: it is offline, or on nothing but utun/awdl/bridge. Join a
|
|
1508
|
+
Wi-Fi or Ethernet network, or connect this Mac by cable, and run again.
|
|
1509
|
+
Deliberately NOT "set metro.publicUrl": neither channel to a phone carries a
|
|
1510
|
+
URL. The dev-client deep link composes http://<host>:<port> itself, and
|
|
1511
|
+
ip.txt is read by RCTBundleURLProvider, which prefixes the scheme. A tunnel
|
|
1512
|
+
cannot be expressed to a phone, so --device ignores metro.publicUrl,
|
|
1513
|
+
metro.tunnel and metro.ngrokUrl and says so when one is set.`
|
|
1514
|
+
},
|
|
1515
|
+
STIM_LAN_METRO_UNREACHABLE: {
|
|
1516
|
+
summary: "the LAN origin did not answer as this workspace's Metro; ios.lanHost on a multi-NIC Mac",
|
|
1517
|
+
context: `A phone does not share the host's loopback and USB carries no reverse forward,
|
|
1518
|
+
so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
|
|
1519
|
+
fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
|
|
1520
|
+
body: () => `STIM_LAN_METRO_UNREACHABLE
|
|
1521
|
+
The chosen LAN origin did not answer as THIS workspace's Metro: no answer, a
|
|
1522
|
+
5xx, or a dev server that is not this one -- the message says which.
|
|
1523
|
+
\`stim start\` prints the port it reserved. On a Mac with several interfaces
|
|
1524
|
+
the first en* is not necessarily the one the phone shares: set ios.lanHost to
|
|
1525
|
+
the address it can reach (see \`guide settings\`).
|
|
1526
|
+
What this gate CANNOT prove is that the phone can reach the origin: macOS
|
|
1527
|
+
routes a host connection to its own address over loopback, so the gate passes
|
|
1528
|
+
through a firewall that will block the phone. That evidence only ever arrives
|
|
1529
|
+
from the phone's own bundle request, which is what \`launched\` reports.`
|
|
1530
|
+
},
|
|
1531
|
+
unverified: {
|
|
1532
|
+
summary: "launched: \"unverified\" with the Local Network path reason, and the routed recovery",
|
|
1533
|
+
context: `A phone does not share the host's loopback and USB carries no reverse forward,
|
|
1534
|
+
so a Debug run on one is wired to a LAN origin instead of localhost.`,
|
|
1535
|
+
body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
|
|
1536
|
+
An app that has not been granted Local Network reaches nothing on the LAN,
|
|
1537
|
+
and CFNetwork reports each attempt as NSURLErrorDomain -1009 "The Internet
|
|
1538
|
+
connection appears to be offline." with the path reason
|
|
1539
|
+
|
|
1540
|
+
_NSURLErrorNWPathKey=unsatisfied (Local network prohibited)
|
|
1541
|
+
|
|
1542
|
+
THAT REASON IS THE WHOLE MATCH. The rest of the block -- POSIX error 50
|
|
1543
|
+
(ENETDOWN), \`failed to connect 1:50\`, \`error code: -1009 [1:50]\` -- is
|
|
1544
|
+
generic and says nothing about the permission: Wi-Fi turned off gives the
|
|
1545
|
+
identical errno with the reason \`unsatisfied (No network route)\`, and a
|
|
1546
|
+
cellular-only route gives \`unsatisfied (Denied over cellular interface)\`.
|
|
1547
|
+
Matching those would print this remedy at a phone that simply is not on the
|
|
1548
|
+
network, and would drop the same-SSID check that is the actual fix, so they
|
|
1549
|
+
are not matched.
|
|
1550
|
+
THE PROMPT AND A PRIOR DENIAL READ THE SAME. iOS emits this reason while the
|
|
1551
|
+
prompt is unanswered and after a Don't Allow, which persists across upgrade
|
|
1552
|
+
installs. The remedy covers both: if the first \`alert get\` finds no alert,
|
|
1553
|
+
it was denied earlier and the only fix is the switch under Settings > Privacy
|
|
1554
|
+
& Security > Local Network, which has no API.
|
|
1555
|
+
The reason is read out of THIS launch's device records -- since the launch,
|
|
1556
|
+
and from the app's pid when it is known. It is NOT origin-scoped: the record
|
|
1557
|
+
that carries the reason carries no URL (the failing URL lands in a
|
|
1558
|
+
continuation line with no process prefix, which the pid filter drops), so
|
|
1559
|
+
scoping to this workspace's Metro origin would never fire. That is sound
|
|
1560
|
+
anyway, because the permission gates every LAN connection the app makes, so
|
|
1561
|
+
even a third-party SDK's prohibited connection proves the app cannot reach
|
|
1562
|
+
this workspace's Metro either. Matching only picks the remedy: no record's
|
|
1563
|
+
level changes, so the device source stays out of \`logs --errors\`
|
|
1564
|
+
(\`guide logs\`: severity is never guessed on a phone).
|
|
1565
|
+
When it matches, \`launched: "unverified"\` leads with that evidence and with
|
|
1566
|
+
the recovery, in this order:
|
|
1567
|
+
|
|
1568
|
+
agent-device alert get --platform ios --udid <udid>
|
|
1569
|
+
agent-device alert accept --platform ios --udid <udid>
|
|
1570
|
+
agent-device snapshot -i --platform ios --udid <udid>
|
|
1571
|
+
agent-device press 'label="Reload"' --platform ios --udid <udid>
|
|
1572
|
+
|
|
1573
|
+
\`alert get\` reads the alert without opening anything, so it works while the
|
|
1574
|
+
app sits behind it. THE GRANT ALONE IS NOT ENOUGH: the dev client does not
|
|
1575
|
+
retry, and stays on "Failed to load app ... The Internet connection appears
|
|
1576
|
+
to be offline." with a Reload button, which is why the last two lines are
|
|
1577
|
+
there. The text form of the press target is \`label="Reload"\` (or
|
|
1578
|
+
\`text="Reload"\`); a bare \`press "Reload"\` is rejected. Stim's own launch
|
|
1579
|
+
ends in \`-- -EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0\`,
|
|
1580
|
+
so the Expo dev menu is not over the app, fresh install or not. An app started
|
|
1581
|
+
ANOTHER way does not carry those arguments and \`snapshot -i\` can show the
|
|
1582
|
+
menu instead: \`agent-device press 'label="Close"'\` dismisses it, then press
|
|
1583
|
+
Reload. See \`guide facts devmenu\`.
|
|
1584
|
+
|
|
1585
|
+
A BARE APP (no expo-dev-client) gets the same first two commands and a
|
|
1586
|
+
different third. The prompt fires the same way, because it is fired by any
|
|
1587
|
+
LAN connection to the Metro host, and the path reason is CFNetwork's either
|
|
1588
|
+
way -- the classifier reads that reason alone and knows nothing about dev
|
|
1589
|
+
clients. What differs is the screen: a bare app is expected to show React
|
|
1590
|
+
Native's RedBox, "Could not connect to development server". Read the screen
|
|
1591
|
+
with \`agent-device snapshot -i\` and press Reload by the ref or label it
|
|
1592
|
+
reports; neither that text nor the button's accessibility label has been read
|
|
1593
|
+
off hardware.
|
|
1594
|
+
|
|
1595
|
+
\`agent-device metro reload\` does NOT recover either screen. It only reaches
|
|
1596
|
+
an app already connected to Metro's websocket, and an app stopped by this
|
|
1597
|
+
permission never connected.
|
|
1598
|
+
|
|
1599
|
+
Without agent-device,
|
|
1600
|
+
\`xcrun devicectl device process launch --device <udid> --terminate-existing
|
|
1601
|
+
[--payload-url '<devClientUrl>'] <bundleId>
|
|
1602
|
+
[-- -EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0]\`
|
|
1603
|
+
also recovers, and it costs the device log:
|
|
1604
|
+
it replaces the process the collector follows, so
|
|
1605
|
+
\`stim logs --source device\` stops for the rest of that run. For a dev
|
|
1606
|
+
client, pressing Reload is cheaper and keeps the collector alive. For a bare
|
|
1607
|
+
app the relaunch is the cleanest recovery, because it re-reads ip.txt. By
|
|
1608
|
+
hand it is two taps either way: Allow, then Reload.
|
|
1609
|
+
|
|
1610
|
+
WHAT HAS ACTUALLY RUN: the dev-client path above was performed on a phone --
|
|
1611
|
+
the alert, the accept, the unchanged error screen, the Reload press, and the
|
|
1612
|
+
bundle that followed. The bare path has NOT been exercised on hardware; there
|
|
1613
|
+
is no provisioned bare project to run it on. Its signature and its remedy are
|
|
1614
|
+
reasoned from the same CFNetwork evidence and from React Native's own
|
|
1615
|
+
RedBox, not observed.
|
|
1616
|
+
|
|
1617
|
+
THE OTHER ONE-TIME TAP HAS NO API. The developer-trust tap (Settings >
|
|
1618
|
+
General > VPN & Device Management) is refused to automation by the same gate
|
|
1619
|
+
that refuses the app, agent-device's own runner included, so its remedy is
|
|
1620
|
+
"ask the user" and nothing else. An uninstall clears both.`
|
|
1621
|
+
},
|
|
1622
|
+
STIM_NO_DEVICE: {
|
|
1623
|
+
summary: "no usable phone, or the owned simulator or emulator could not be created or booted",
|
|
1624
|
+
separator: "--- DEVICE AND CAPACITY CODES ---",
|
|
1625
|
+
body: () => `STIM_NO_DEVICE
|
|
1626
|
+
With \`--device\`, no physical device answered the selection: none connected,
|
|
1627
|
+
a named serial/UDID that is not connected, several connected with none named
|
|
1628
|
+
(the refusal lists them), or one that is connected but unusable -- an
|
|
1629
|
+
unauthorized Android device, or an iPhone that is unpaired or has Developer
|
|
1630
|
+
Mode off. Hardware is never created or booted, so there is nothing to retry
|
|
1631
|
+
into existence: fix the cable, the trust prompt, or Developer Mode.
|
|
1632
|
+
Otherwise the owned simulator/emulator could not be created or could not
|
|
1633
|
+
reach a booted state. \`stim doctor\` checks the toolchain; \`stim status\` says what
|
|
1634
|
+
Stim thinks it owns. Re-running the command creates a fresh owned device
|
|
1635
|
+
when the recorded one is gone.
|
|
1636
|
+
If Android creation says an AVD already exists on disk but is not listed,
|
|
1637
|
+
run \`npx stim gc\` to inspect orphaned owned AVDs, then \`npx stim gc --delete\`
|
|
1638
|
+
to reclaim those safe to delete before retrying. Keep anything GC cannot
|
|
1639
|
+
verify; do not delete AVD directories by hand. A registered unrecorded owned
|
|
1640
|
+
AVD is recovered, reusing its existing emulator when its identity is verified.
|
|
1641
|
+
If recovery cannot verify registration or process state, inspect \`npx stim status\`
|
|
1642
|
+
and \`adb devices\`, then retry after any other run finishes. Keep the AVD and
|
|
1643
|
+
its process locks while its state is unverified.
|
|
1644
|
+
On iOS a slow first boot is waited out for up to ten minutes while the
|
|
1645
|
+
simulator still reports Booting -- a long silent wait on a loaded machine
|
|
1646
|
+
is patience, not a hang. The failure names the udid and the wait.
|
|
1647
|
+
After boot, a process-spawn probe must finish within 30 seconds before
|
|
1648
|
+
installation. If it fails, the refusal includes observed host memory pressure
|
|
1649
|
+
when available. Free memory before retrying under pressure; see
|
|
1650
|
+
\`stim guide lifecycle simslim\`. Booted alone does not prove readiness.
|
|
1651
|
+
On Android the emulator's own stdio is captured to
|
|
1652
|
+
the global workspace logs/emulator.log (truncated per boot), and when it printed a
|
|
1653
|
+
\`FATAL |\` / \`ERROR |\` / \`PANIC:\` line THAT is the message and the remedy
|
|
1654
|
+
you get -- the disk-space refusal ("Not enough space to create userdata
|
|
1655
|
+
partition") is the case this exists for. The generic toolchain remedy above
|
|
1656
|
+
is only what you see when neither the log nor the failure itself identifies
|
|
1657
|
+
the cause. An ENOSPC failure points at disk space instead: owned Android AVDs
|
|
1658
|
+
normally live under ~/.android/avd, and a booted AVD can use several GB. A
|
|
1659
|
+
boot whose emulator process exited is also reported at once rather than after
|
|
1660
|
+
the full cold-boot timeout.
|
|
1661
|
+
|
|
1662
|
+
"this project's sim is X, but --device-type asked for Y"
|
|
1663
|
+
The project already owns a simulator of a different model, and Stim will
|
|
1664
|
+
not silently boot a different one. Reap it (\`worktree remove\`, or
|
|
1665
|
+
\`gc --delete\`) and run \`stim ios\` again to create the requested model.
|
|
1666
|
+
That loses the old sim's app state.`
|
|
1667
|
+
},
|
|
1668
|
+
STIM_DEVICE_BUSY: {
|
|
1669
|
+
summary: "another workspace holds the lease on that phone and the wait ran out",
|
|
1670
|
+
body: () => `STIM_DEVICE_BUSY
|
|
1671
|
+
Only on a \`--device\` run. Another workspace holds the lease on that phone,
|
|
1672
|
+
and the wait ran out: the message names the holder root, the device, and the
|
|
1673
|
+
expiry as a clock time and a remaining duration, and \`--json\` adds
|
|
1674
|
+
\`lease: { platform, id, deviceName, holder, expiresAt }\`. In order, the
|
|
1675
|
+
remedies are: wait longer with \`--wait <seconds>\`, pick another device by
|
|
1676
|
+
id, or \`--no-wait\`, which installs with NO lease -- and when both
|
|
1677
|
+
workspaces build the same app id, that install terminates the app the holder
|
|
1678
|
+
is running. Two other cases refuse with this code and no wait at all: a lease
|
|
1679
|
+
file that does not parse (\`lease\` fields null, the file named -- nothing may
|
|
1680
|
+
take that device until it is dealt with), and this workspace's OWN lease with
|
|
1681
|
+
no token left in its \`state.json\` (its workspace directory was recreated).
|
|
1682
|
+
The remedy for that last one is \`stim device unlock\`, which releases by
|
|
1683
|
+
holder rather than by token.`
|
|
1684
|
+
},
|
|
1685
|
+
STIM_DEVICE_LOST: {
|
|
1686
|
+
summary: "the lease was gone or re-held at the pre-install check; rerun",
|
|
1687
|
+
body: () => `STIM_DEVICE_LOST
|
|
1688
|
+
Only on a \`--device\` run. The run held a lease, and the raise before the
|
|
1689
|
+
install found it gone or held under another token -- another workspace took
|
|
1690
|
+
the device in that window. The message names the new holder and its expiry.
|
|
1691
|
+
Run the command again; it waits for that lease under \`--wait <seconds>\`.
|
|
1692
|
+
AFTER the install has started this is not a failure: the app is already on
|
|
1693
|
+
the phone, so the run prints one warning, continues, and reports
|
|
1694
|
+
\`lease: null\` in \`--json\`.`
|
|
1695
|
+
},
|
|
1696
|
+
STIM_AT_CAPACITY: {
|
|
1697
|
+
summary: "concurrency.maxDevices reached; a refusal, not a queue",
|
|
1698
|
+
body: () => `STIM_AT_CAPACITY
|
|
1699
|
+
Only when concurrency.maxDevices is set (it is UNSET by default, so this never
|
|
1700
|
+
fires unless you opted in). Booting a NEW owned device would exceed the cap:
|
|
1701
|
+
the machine already has that many Stim-owned devices booted. It is a refusal, not
|
|
1702
|
+
a queue -- \`ios\`/\`android\` are interactive-shaped, so Stim does not make
|
|
1703
|
+
you wait at a prompt. The remedy is fixed: stop an environment
|
|
1704
|
+
(\`stim stop\`) to free a device, or raise concurrency.maxDevices. A
|
|
1705
|
+
workspace whose OWN device is already booted is never refused -- re-running
|
|
1706
|
+
\`ios\` on an environment you already have is idempotent. (The build cap
|
|
1707
|
+
behaves differently: a compile WAITS for a free slot rather than refusing.
|
|
1708
|
+
See \`guide lifecycle concurrency\`.)`
|
|
1709
|
+
},
|
|
1710
|
+
STIM_BUILD_SLOT_TIMEOUT: {
|
|
1711
|
+
summary: "the maxBuilds wait gave up with every slot held by a running process",
|
|
1712
|
+
body: () => `STIM_BUILD_SLOT_TIMEOUT
|
|
1713
|
+
Only when concurrency.maxBuilds is set. The build cap does not refuse, it
|
|
1714
|
+
WAITS -- this code is that wait giving up: ~90 minutes elapsed and every one
|
|
1715
|
+
of the N slots was still held by a running process, or by a holder Stim could
|
|
1716
|
+
not identify. A dead
|
|
1717
|
+
builder's slot is reclaimed within a poll, and a recycled pid does not hold a
|
|
1718
|
+
slot, so this is never a slot leaked by a crash; it is either that many
|
|
1719
|
+
genuinely long compiles, or a slot directory whose owner is not really
|
|
1720
|
+
building. A slot whose holder cannot be identified at all is skipped while any
|
|
1721
|
+
other slot is merely busy, and becomes STIM_CLAIM_REFUSED only when no slot is
|
|
1722
|
+
left to wait for. Slots live under ~/.stim/build-slots and
|
|
1723
|
+
the message names the directory: remove the slot of a builder that is not
|
|
1724
|
+
building, or raise concurrency.maxBuilds
|
|
1725
|
+
(\`guide lifecycle concurrency\`).`
|
|
1726
|
+
},
|
|
1727
|
+
STIM_NO_REMOTE_SESSION: {
|
|
1728
|
+
summary: "the backend could not use agent-device, or metro.tunnel names an unusable provider",
|
|
1729
|
+
separator: "--- REMOTE-DEVICE CODES (`ios --remote <proxy|eas>` / `android --remote <proxy|eas>`) ---",
|
|
1730
|
+
body: () => `STIM_NO_REMOTE_SESSION
|
|
1731
|
+
The selected backend could not use agent-device, or metro.tunnel names a
|
|
1732
|
+
provider or mode this workspace cannot use (e.g. "expo" on a bare RN
|
|
1733
|
+
project). The remedy line says which. Nothing was created yet.`
|
|
1734
|
+
},
|
|
1735
|
+
STIM_REMOTE_PROXY_CONFIG: {
|
|
1736
|
+
summary: "--remote proxy needs AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN",
|
|
1737
|
+
body: () => `STIM_REMOTE_PROXY_CONFIG
|
|
1738
|
+
\`--remote proxy\` requires AGENT_DEVICE_DAEMON_BASE_URL and
|
|
1739
|
+
AGENT_DEVICE_DAEMON_AUTH_TOKEN. These variables provide credentials after
|
|
1740
|
+
proxy is selected. They never select the backend.`
|
|
1741
|
+
},
|
|
1742
|
+
STIM_REMOTE_EAS_UNAVAILABLE: {
|
|
1743
|
+
summary: "--remote eas needs eas-cli",
|
|
1744
|
+
body: () => `STIM_REMOTE_EAS_UNAVAILABLE
|
|
1745
|
+
\`--remote eas\` requires eas-cli. Proxy environment variables do not change
|
|
1746
|
+
this selection and are not passed to EAS.`
|
|
1747
|
+
},
|
|
1748
|
+
STIM_REMOTE_PLATFORM_MISMATCH: {
|
|
1749
|
+
summary: "the recorded remote session belongs to the other platform; stop, then rerun",
|
|
1750
|
+
body: () => `STIM_REMOTE_PLATFORM_MISMATCH
|
|
1751
|
+
This workspace already has a recorded remote session, and it belongs to the
|
|
1752
|
+
OTHER platform ("Session <id> belongs to android, not ios"). A workspace
|
|
1753
|
+
holds one remote session, and Stim will not end the recorded one to make
|
|
1754
|
+
room -- it may be mid-run for whoever started it. Run \`stim stop\` for this
|
|
1755
|
+
workspace, then re-run with the platform you want. Nothing was created here.`
|
|
1756
|
+
},
|
|
1757
|
+
STIM_REMOTE_SESSION_STATE: {
|
|
1758
|
+
summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
|
|
1759
|
+
body: () => `STIM_REMOTE_SESSION_STATE
|
|
1760
|
+
The EAS session was created and is healthy, but recording it in this
|
|
1761
|
+
workspace's state failed (an unwritable STIM_HOME, a full disk). A session
|
|
1762
|
+
nothing references is a session nothing will ever stop, so Stim stopped the
|
|
1763
|
+
one it had just created and removed its ownership claim before reporting:
|
|
1764
|
+
this code means nothing is running and nothing is still billing. Repair the
|
|
1765
|
+
state storage the message names, then run the remote command again.`
|
|
1766
|
+
},
|
|
1767
|
+
STIM_REMOTE_SESSION_CLEANUP: {
|
|
1768
|
+
summary: "Stim could not prove an EAS session ended; eas simulator:stop --id",
|
|
1769
|
+
body: () => `STIM_REMOTE_SESSION_CLEANUP
|
|
1770
|
+
Stim tried to end an EAS session and could not PROVE it ended: \`eas
|
|
1771
|
+
simulator:stop\` failed, or its output did not confirm the stop, or the
|
|
1772
|
+
session stopped but its claim in the machine ledger could not be removed.
|
|
1773
|
+
This is a refusal rather than a note because a session that did not stop
|
|
1774
|
+
BILLS until its duration cap. The remedy names the exact command --
|
|
1775
|
+
\`eas simulator:stop --id <id>\` -- and for a ledger that outlived its
|
|
1776
|
+
session, the ledger path to repair. The same code covers a recorded session
|
|
1777
|
+
that could not be verified before replacement: inspect it, then \`stim stop\`.`
|
|
1778
|
+
},
|
|
1779
|
+
STIM_REMOTE_METRO_WRONG: {
|
|
1780
|
+
summary: "the tunnel reaches a Metro that is not this workspace's",
|
|
1781
|
+
body: () => `STIM_REMOTE_METRO_WRONG
|
|
1782
|
+
The gate that proves a tunnel still reaches THIS workspace's Metro failed --
|
|
1783
|
+
before a session or a build, whether the tunnel is Expo's own, one Stim
|
|
1784
|
+
started (metro.tunnel: cloudflared/ngrok/auto), or a named metro.publicUrl.
|
|
1785
|
+
The usual cause: the tunnel was built for a port this workspace no longer
|
|
1786
|
+
holds (a stale one survived a \`stop\`/\`start\` that reserved a different
|
|
1787
|
+
port), and it now serves ANOTHER workspace's dev server -- healthy, and
|
|
1788
|
+
wrong. Re-run \`stim start\` (it prints the port it reserved) and, for a
|
|
1789
|
+
manual tunnel, rebuild it against that port.`
|
|
1790
|
+
},
|
|
1791
|
+
STIM_REMOTE_METRO_UNREACHABLE: {
|
|
1792
|
+
summary: "a remote start could not create its managed tunnel or tell the device where Metro is",
|
|
1793
|
+
body: () => `STIM_REMOTE_METRO_UNREACHABLE
|
|
1794
|
+
A remote start could not create its selected managed tunnel, or the device
|
|
1795
|
+
could not be told where Metro is. Follows the same remedy as
|
|
1796
|
+
STIM_NO_REMOTE_SESSION's tunnel guidance -- set metro.tunnel, or use
|
|
1797
|
+
metro.publicUrl for an existing endpoint.`
|
|
1798
|
+
},
|
|
1799
|
+
STIM_RELOAD_AMBIGUOUS: {
|
|
1800
|
+
summary: "both owned apps are live; name the platform",
|
|
1801
|
+
separator: "--- RELOAD CODES (`stim reload [ios|android]`) ---",
|
|
1802
|
+
body: () => `STIM_RELOAD_AMBIGUOUS
|
|
1803
|
+
Both owned apps are live. Name ios or android; Stim never guesses.`
|
|
1804
|
+
},
|
|
1805
|
+
STIM_RELOAD_RELEASE: {
|
|
1806
|
+
summary: "the live app has embedded JS; run a Debug build first",
|
|
1807
|
+
body: () => `STIM_RELOAD_RELEASE
|
|
1808
|
+
The live app was launched with embedded JavaScript. Run the platform command
|
|
1809
|
+
with a Debug configuration or variant first.`
|
|
1810
|
+
},
|
|
1811
|
+
STIM_RELOAD_STOPPED: {
|
|
1812
|
+
summary: "the recorded app is gone, its device is not live and owned, or the process could not be proven",
|
|
1813
|
+
aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
|
|
1814
|
+
body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
|
|
1815
|
+
The recorded app is gone, its exact device is not live and owned by this
|
|
1816
|
+
workspace, or simctl/adb could not prove the process exists. No launch or
|
|
1817
|
+
device lifecycle action is taken; follow the printed platform-command or
|
|
1818
|
+
process-probe remedy.`
|
|
1819
|
+
},
|
|
1820
|
+
STIM_RELOAD_FAILED: {
|
|
1821
|
+
summary: "the reload failed; the remedy differs by shape -- read it before acting",
|
|
1822
|
+
body: () => `STIM_RELOAD_FAILED
|
|
1823
|
+
The Metro websocket reload did not reach a peer Stim could identify. Two
|
|
1824
|
+
shapes reach this code and the remedy differs. Read it rather than assuming.
|
|
1825
|
+
|
|
1826
|
+
METRO DID NOT ANSWER. The probe timed out after 2 seconds, or the socket
|
|
1827
|
+
errored. Nothing is known about the app, so the remedy is to run the same
|
|
1828
|
+
reload again, and to check the dev server with stim doctor if it keeps
|
|
1829
|
+
timing out. Do not touch the device for this one.
|
|
1830
|
+
|
|
1831
|
+
METRO REPORTS NO PEER FOR THE APP. Stim broadcasts a reload anyway before
|
|
1832
|
+
giving up, because matching is best-effort and an unmatched peer may still be
|
|
1833
|
+
this app, so VERIFY THE UI FIRST -- the app may already have recovered. If it
|
|
1834
|
+
did not, retry: a client reconnects to Metro every 2 seconds, which is also
|
|
1835
|
+
this probe's timeout, so a single miss can be a reconnect window rather than
|
|
1836
|
+
an app that never connected. If it stays unreachable on iOS, an error in the
|
|
1837
|
+
first bundle leaves the app without a packager connection at all and no retry
|
|
1838
|
+
will make it a peer. The remedy then routes the agent to the device's own
|
|
1839
|
+
controls in its existing automation session: press the error screen's Reload
|
|
1840
|
+
button, or open the dev menu and press Reload when no error screen is
|
|
1841
|
+
showing. The printed agent-device open command is the last resort; it
|
|
1842
|
+
relaunches that app on that device with this workspace's Metro port and loses
|
|
1843
|
+
in-memory state. Keep the existing --session flag and verify afterward.
|
|
1844
|
+
|
|
1845
|
+
MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
|
|
1846
|
+
app, so several matching peers are that app on several devices. Stim reloads
|
|
1847
|
+
every one of them and reports the count in the facts as targets.`
|
|
1848
|
+
},
|
|
1849
|
+
STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
|
|
1850
|
+
summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
|
|
1851
|
+
separator: "--- DEV-SERVER CODES (`stim start`) ---",
|
|
1852
|
+
body: () => `STIM_WORKTREE_REMOVAL_IN_PROGRESS
|
|
1853
|
+
A managed remote start found that \`stim worktree remove\` owns the
|
|
1854
|
+
worktree lock. The start did not register the project or create a tunnel.
|
|
1855
|
+
Wait for removal to finish, then run \`stim start --remote\` again.`
|
|
1856
|
+
},
|
|
1857
|
+
STIM_REMOTE_START_REQUIRED: {
|
|
1858
|
+
summary: "a running server cannot gain a remote tunnel; stop, then start --remote, or metro.publicUrl",
|
|
1859
|
+
body: () => `STIM_REMOTE_START_REQUIRED
|
|
1860
|
+
A healthy bare or Expo server was started without its required remote
|
|
1861
|
+
tunnel. A running server cannot gain that option. For a Stim supervisor,
|
|
1862
|
+
run \`stim stop\`, then \`stim start --remote\`. For an external server,
|
|
1863
|
+
configure metro.publicUrl or let Stim supervise the server.`
|
|
1864
|
+
},
|
|
1865
|
+
STIM_BARE_DEPS: {
|
|
1866
|
+
summary: "the supervisor cannot host Metro from the project's node_modules; the @stim-cli/metro capture note",
|
|
1867
|
+
aliases: ["STIM_BARE_LOAD", "STIM_BARE_API"],
|
|
1868
|
+
body: () => `STIM_BARE_DEPS / STIM_BARE_LOAD / STIM_BARE_API (bare RN)
|
|
1869
|
+
The supervisor hosts Metro out of the PROJECT's node_modules, so metro,
|
|
1870
|
+
@react-native/dev-middleware and @react-native-community/cli-server-api must
|
|
1871
|
+
be installed there and must match the project's React Native. DEPS = not
|
|
1872
|
+
resolvable (install them), LOAD = installed but threw while loading,
|
|
1873
|
+
API = loaded but is not the API Stim expects (mismatched versions).
|
|
1874
|
+
|
|
1875
|
+
"@stim-cli/metro is not installed ... so bundler and client logs will not be
|
|
1876
|
+
captured" (in metro.ndjson, bare RN)
|
|
1877
|
+
The dev server is serving; only capture is missing, so \`logs\` would report
|
|
1878
|
+
a quiet timeline for a broken build. Install \`@stim-cli/metro\` as a
|
|
1879
|
+
devDependency of the project.`
|
|
1880
|
+
},
|
|
1881
|
+
STIM_EXPO_BIN: {
|
|
1882
|
+
summary: "node_modules/.bin/expo is missing; install dependencies",
|
|
1883
|
+
body: () => `STIM_EXPO_BIN (Expo)
|
|
1884
|
+
node_modules/.bin/expo does not exist. Install the project's dependencies.`
|
|
1885
|
+
},
|
|
1886
|
+
STIM_METRO_TIMEOUT: {
|
|
1887
|
+
summary: "the supervisor is alive but Metro or the tunnel was not ready within the wait; --wait 180",
|
|
1888
|
+
body: () => `STIM_METRO_TIMEOUT
|
|
1889
|
+
"The dev server did not answer on port <n> within <s>s."
|
|
1890
|
+
The supervisor is alive, but Metro or its requested Expo tunnel is not ready.
|
|
1891
|
+
\`start\` has already
|
|
1892
|
+
printed the last lines of the global workspace logs/supervisor.log above this -- read
|
|
1893
|
+
them. A cold Metro on a large graph can genuinely need more than the default
|
|
1894
|
+
60s: re-run with \`--wait 180\`. Otherwise \`stim stop\`, then \`start\`.`
|
|
1895
|
+
},
|
|
1896
|
+
STIM_SUPERVISOR_EXITED: {
|
|
1897
|
+
summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
|
|
1898
|
+
body: () => `STIM_SUPERVISOR_EXITED
|
|
1899
|
+
"The supervisor exited (<code|signal>) before the dev server came up"
|
|
1900
|
+
The dev server failed outright, and the quoted evidence is the real error:
|
|
1901
|
+
the supervisor.log tail if it wrote one, plus this attempt's error records
|
|
1902
|
+
from the timeline (an expo child's config error -- a PluginError, a bad app
|
|
1903
|
+
config -- lands THERE, not in supervisor.log). \`stim logs --errors\` has
|
|
1904
|
+
the full records. Fix that and run \`start\` again.
|
|
1905
|
+
|
|
1906
|
+
"Cannot reuse or replace the recorded supervisor: ..."
|
|
1907
|
+
A live record has no verifiable OS process identity, or the workspace and
|
|
1908
|
+
registry records disagree. Nothing new was started; the old process is
|
|
1909
|
+
left running. If inspection is denied, retry with permission to inspect
|
|
1910
|
+
Stim's processes. For a legacy record, stop the server with the tool that
|
|
1911
|
+
started it before retrying. Never reconstruct ownership from a process
|
|
1912
|
+
name, port, or wall-clock timestamp.`
|
|
1913
|
+
},
|
|
1914
|
+
STIM_BAD_ARG: {
|
|
1915
|
+
summary: "an argument, setting, directory, flavor, or device name refused before anything starts",
|
|
1916
|
+
aliases: ["STIM_NO_PROJECT"],
|
|
1917
|
+
body: () => `STIM_BAD_ARG / STIM_NO_PROJECT
|
|
1918
|
+
The command refused before doing anything: an unusable --wait value, a known
|
|
1919
|
+
setting with the wrong type ("Invalid <key> setting <value>. Expected <shape>."
|
|
1920
|
+
-- \`guide settings\` names the type each key takes), an invalid
|
|
1921
|
+
Metro tunnel setting, an invalid android.dataPartitionSizeGb value, an unsafe
|
|
1922
|
+
android.avdConfig key or fragment, a malformed ios.signingIdentity,
|
|
1923
|
+
ios.signingIdentitySha1 or ios.lanHost value, \`--device\` with an empty
|
|
1924
|
+
serial or UDID, \`--device\` together with \`--remote\`, a working directory
|
|
1925
|
+
with no package.json above it, or one whose nearest package.json does not
|
|
1926
|
+
parse or depends on neither react-native nor expo, so the directory is not
|
|
1927
|
+
an app (the refusal names that package.json and says which of the two it
|
|
1928
|
+
is; \`doctor\` reports the same directory as a finding), an
|
|
1929
|
+
android/app/build.gradle that declares product flavors with
|
|
1930
|
+
no variant selected (the refusal names the debug variants), or a
|
|
1931
|
+
\`--device-type\`, \`--runtime\` or \`--system-image\` name that is BLANK or
|
|
1932
|
+
is not installed on this machine. For the unknown-name case the installed
|
|
1933
|
+
names are printed in the message -- the versions \`xcrun simctl list
|
|
1934
|
+
runtimes\` reports, the models those runtimes can actually CREATE (not the
|
|
1935
|
+
whole \`simctl list devicetypes\` table, which also names watchOS, tvOS and
|
|
1936
|
+
visionOS models no iOS runtime offers), or the system images the SDK has --
|
|
1937
|
+
so the remedy is to re-run with one of them. An ios.deviceType, ios.runtime
|
|
1938
|
+
or android.systemImage setting is checked the same way, and the check applies
|
|
1939
|
+
even when this workspace ALREADY owns a device, so a name that could never
|
|
1940
|
+
create anything is caught rather than left to a later run.
|
|
1941
|
+
These errors are caught before the port is reserved and before any build or
|
|
1942
|
+
device work, so nothing was started. The one listing they need
|
|
1943
|
+
(\`simctl list runtimes\`, the SDK's system-images directory) runs only when
|
|
1944
|
+
a name was actually given, and a listing that fails is reported as
|
|
1945
|
+
STIM_NO_DEVICE naming the tool, never as a crash.`
|
|
1946
|
+
},
|
|
1947
|
+
STIM_LOCK_REFUSED: {
|
|
1948
|
+
summary: "a directory lock is held by a removal, which is never waited out",
|
|
1949
|
+
separator: "--- COORDINATION CODES (any command that shares a resource) ---",
|
|
1950
|
+
body: () => `STIM_LOCK_REFUSED
|
|
1951
|
+
A directory lock that serialises two commands over the same thing -- this
|
|
1952
|
+
workspace's managed tunnel, its managed remote worktree, the machine's EAS
|
|
1953
|
+
project ledger -- is held by a REMOVAL, and a removal is never waited out:
|
|
1954
|
+
what it protects will not exist when the lock frees. Nothing was created.
|
|
1955
|
+
The message names the lock and the purpose holding it (\`worktree removal\`,
|
|
1956
|
+
\`workspace removal\` -- both are \`stim worktree remove\`). Let it finish,
|
|
1957
|
+
then run the command again. \`start --remote\` reports this same case as
|
|
1958
|
+
STIM_WORKTREE_REMOVAL_IN_PROGRESS instead.`
|
|
1959
|
+
},
|
|
1960
|
+
STIM_LOCK_TIMEOUT: {
|
|
1961
|
+
summary: "a lock held past the wait; workspace-process and short directory locks",
|
|
1962
|
+
body: () => `STIM_LOCK_TIMEOUT
|
|
1963
|
+
The same locks, held by an ordinary command that is still running, for
|
|
1964
|
+
longer than the wait -- 60s by default, 4 minutes for the remote-session and
|
|
1965
|
+
EAS project locks, and ~90 minutes for the \`worktree warm\` lock, which one
|
|
1966
|
+
\`--refresh\` can hold for a whole dependency install. That wait prints its
|
|
1967
|
+
elapsed waiting time and holder every 30 seconds (\`lock waiting 40s
|
|
1968
|
+
for stim worktree warm --refresh (pid 41233)\`) and the refusal names the same holder
|
|
1969
|
+
and the lock directory under ~/.stim/warm-locks.
|
|
1970
|
+
A lock whose owner died is taken over automatically (its recorded
|
|
1971
|
+
process identity is checked every poll), so this means another Stim command really
|
|
1972
|
+
is working on this workspace: wait for it and retry. If nothing is running,
|
|
1973
|
+
the message names the lock directory and removing it is safe. The same error
|
|
1974
|
+
code also covers the short directory-lock timeout below.
|
|
1975
|
+
|
|
1976
|
+
"Timed out waiting for the lock at <path>."
|
|
1977
|
+
Short directory locks serialize writes to config, workspace state, device
|
|
1978
|
+
leases, ownership records, metadata, and cache manifests. The path identifies
|
|
1979
|
+
the lock. A lock older than 10s is taken over automatically. Wait for the
|
|
1980
|
+
command holding it; if none is running, remove the named directory.`
|
|
1981
|
+
},
|
|
1982
|
+
teardown: {
|
|
1983
|
+
summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
|
|
1984
|
+
separator: "--- TEARDOWN AND WORKSPACE REFUSALS ---",
|
|
1985
|
+
body: () => `"metro refusing to kill port <n>: ... runs from <dir>, outside
|
|
1986
|
+
<project>" (stop)
|
|
1987
|
+
Stim only signals processes it launched and whose saved identity still
|
|
1988
|
+
matches. Stop an externally started server with the tool that started it.
|
|
1989
|
+
Matching this workspace's port or directory does not authorize cleanup,
|
|
1990
|
+
and stop has no override for process ownership.
|
|
1991
|
+
|
|
1992
|
+
"stop refusing to signal supervisor pid <n>: ..." (stop)
|
|
1993
|
+
The records disagree, the saved OS identity is unavailable, or it records a
|
|
1994
|
+
port this project did not reserve. If process inspection is denied, retry
|
|
1995
|
+
with permission to inspect the processes Stim started. A pid is a number the OS reuses, so it is not
|
|
1996
|
+
signalled. The port reservation is KEPT -- it is the only handle a retry
|
|
1997
|
+
has. Check \`ps -p <n>\` and \`stim status\` before signalling by hand.
|
|
1998
|
+
|
|
1999
|
+
"supervisor pid <n> did not exit within 10s of SIGTERM" (stop)
|
|
2000
|
+
Deliberately not escalated to SIGKILL: the supervisor may be mid-write on the
|
|
2001
|
+
very log files \`logs\` reads. The device is left alone and the port stays
|
|
2002
|
+
reserved. Re-run \`stop\`, or signal it yourself: kill -9 -<n> (note the
|
|
2003
|
+
minus -- it is a process group).
|
|
2004
|
+
|
|
2005
|
+
"teardown failed: <reason>"
|
|
2006
|
+
Stim could not release the owned device and keeps its record for a retry.
|
|
2007
|
+
\`worktree remove\` exits 1 without removing the worktree while the device is
|
|
2008
|
+
still tracked. Fix the reported cause and re-run.`
|
|
2009
|
+
},
|
|
2010
|
+
remove: {
|
|
2011
|
+
summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
|
|
2012
|
+
body: () => `"Refusing to remove <path>: uncommitted changes / untracked files / commits
|
|
2013
|
+
not on any remote" (worktree remove)
|
|
2014
|
+
A native build rewrites tracked files, and Stim now RESTORES the one class
|
|
2015
|
+
it can prove is not work: when the only dirt left is \`pod install\` churn
|
|
2016
|
+
(\`<app>/ios/Podfile.lock\`, \`<app>/ios/*.xcodeproj/project.pbxproj\`,
|
|
2017
|
+
tracked and unstaged), \`worktree remove\` runs the checkout itself and says
|
|
2018
|
+
so per file -- those files die with the worktree either way, and a lockfile
|
|
2019
|
+
change anyone meant would have been committed. ONE other dirty path and the
|
|
2020
|
+
whole set is refused, churn included, so this never eats real work.
|
|
2021
|
+
When it does refuse, the refusal PRINTS THE DIRTY PATHS, and the restore
|
|
2022
|
+
command under it carries those same paths: run it as printed rather than
|
|
2023
|
+
reaching for --force.
|
|
2024
|
+
It is built from what git reported, so in a monorepo it names
|
|
2025
|
+
\`apps/<app>/ios/Podfile.lock\` rather than an \`ios/...\` example that would
|
|
2026
|
+
fail with "did not match any file(s) known to git".
|
|
2027
|
+
A setup script that rewrites tracked assets (brand icons, generated config)
|
|
2028
|
+
produces the same refusal, with the same treatment: restore the paths the
|
|
2029
|
+
refusal actually named.
|
|
2030
|
+
Use --force only when you genuinely intend to discard work; it deletes
|
|
2031
|
+
uncommitted and untracked files permanently.`
|
|
2032
|
+
},
|
|
2033
|
+
STIM_MAIN_DIRTY: {
|
|
2034
|
+
summary: "warm --refresh will not move a source checkout with local work or an operation in progress",
|
|
2035
|
+
body: () => `STIM_MAIN_DIRTY
|
|
2036
|
+
\`worktree warm --refresh\` writes to the SOURCE CHECKOUT, and it refuses one
|
|
2037
|
+
it cannot move: tracked files with uncommitted changes (the refusal names
|
|
2038
|
+
them), or a rebase or merge in progress. Untracked files are not a reason to
|
|
2039
|
+
refuse -- but git itself refuses a fast-forward that would overwrite one, and
|
|
2040
|
+
that reports this code too, quoting git. The remedy is the exact line that
|
|
2041
|
+
clears it: commit, \`git -C <source-checkout> stash push -u -m warm-refresh\`,
|
|
2042
|
+
or \`git -C <source-checkout> rebase --abort\`. Nothing was installed or
|
|
2043
|
+
copied. Plain \`stim worktree warm\` does not care: it copies from a dirty
|
|
2044
|
+
source checkout exactly as it always has.`
|
|
2045
|
+
},
|
|
2046
|
+
STIM_MAIN_DETACHED: {
|
|
2047
|
+
summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
|
|
2048
|
+
body: () => `STIM_MAIN_DETACHED
|
|
2049
|
+
The source checkout's HEAD is detached, so there is no branch to fast-forward
|
|
2050
|
+
and no upstream to fast-forward it to. Run
|
|
2051
|
+
\`git -C <source-checkout> checkout <branch>\` and warm again. \`--refresh\`
|
|
2052
|
+
never picks a branch for you; a checkout whose job is to seed worktrees
|
|
2053
|
+
should sit on a branch someone chose.`
|
|
2054
|
+
},
|
|
2055
|
+
STIM_MAIN_DIVERGED: {
|
|
2056
|
+
summary: "the source checkout is both ahead of and behind its upstream; warm --refresh will not merge",
|
|
2057
|
+
body: () => `STIM_MAIN_DIVERGED
|
|
2058
|
+
The source checkout's branch has commits its upstream does not, AND its
|
|
2059
|
+
upstream has commits it does not. A fast-forward is impossible, and
|
|
2060
|
+
\`--refresh\` will not merge or reset someone else's checkout to make one:
|
|
2061
|
+
that decision is yours. Rebase or merge it yourself, then warm again. The
|
|
2062
|
+
message reports both counts. Nothing was installed or copied.`
|
|
2063
|
+
},
|
|
2064
|
+
STIM_DEPS_INCOMPLETE: {
|
|
2065
|
+
summary: "the last install recorded for the lockfile on disk now did not finish, so warm will not copy it",
|
|
2066
|
+
body: () => `STIM_DEPS_INCOMPLETE
|
|
2067
|
+
\`worktree warm\` refuses to copy dependencies the source checkout never
|
|
2068
|
+
finished installing. \`--refresh\` records a COMPLETED install of the lockfile
|
|
2069
|
+
it read under ~/.stim/warm-installs; an install that failed, or whose process
|
|
2070
|
+
was killed, leaves that record saying unfinished. A plain warm reads it after
|
|
2071
|
+
it takes its claim and before it copies, and this code is what it prints when
|
|
2072
|
+
the unfinished install is of the lockfile AS IT STANDS NOW. Without it the
|
|
2073
|
+
copy carries a partial node_modules and exits 0, and nothing else in the run
|
|
2074
|
+
says so: the refresh reported its own STIM_DEPS_FAILED in its own terminal,
|
|
2075
|
+
and a refresh that was killed reported nothing anywhere. Nothing was copied.
|
|
2076
|
+
Run \`stim worktree warm --refresh\`: it reinstalls rather than skipping for
|
|
2077
|
+
exactly the same record, and a plain warm copies once that install completes.
|
|
2078
|
+
Two states deliberately do NOT produce this code. A record of a DIFFERENT
|
|
2079
|
+
lockfile says nothing about the one on disk now, whose dependencies may well
|
|
2080
|
+
have been installed since; and a repository with no record at all -- every
|
|
2081
|
+
repository before its first \`--refresh\` -- copies as it always has.
|
|
2082
|
+
In a monorepo the record is keyed on the directory that owns the lockfile,
|
|
2083
|
+
which is usually the repository root, so every app of it reads the same one.`
|
|
2084
|
+
},
|
|
2085
|
+
warm: {
|
|
2086
|
+
summary: "two warm refusals whose text is incomplete, known and not fixed",
|
|
2087
|
+
body: () => `"Could not warm this worktree: EACCES: permission denied, mkdir
|
|
2088
|
+
'<home>/warm-locks/<name>.lock'" (worktree warm --refresh)
|
|
2089
|
+
A STIM_HOME that \`--refresh\` cannot write. The refusal itself is right -- it
|
|
2090
|
+
writes to the source checkout, so it will not run without a claim -- but it
|
|
2091
|
+
carries no \`failed: <CODE>\` line, because EACCES is not a Stim code, and no
|
|
2092
|
+
fix line. Make STIM_HOME writable, or set STIM_HOME to somewhere writable,
|
|
2093
|
+
then run it again. A plain warm degrades in this state rather than refusing.
|
|
2094
|
+
|
|
2095
|
+
"... the claim path is a file, not a claim directory", with a \`rm -rf\` that
|
|
2096
|
+
changes nothing (worktree warm --refresh)
|
|
2097
|
+
When \`$STIM_HOME/warm-locks\` or STIM_HOME itself is a regular FILE, the
|
|
2098
|
+
refusal names \`warm-locks/<name>.lock\` -- a path that cannot exist under a
|
|
2099
|
+
file -- so running the printed removal does nothing and the next run refuses
|
|
2100
|
+
identically. Remove the file that is in the way and run it again. A plain
|
|
2101
|
+
warm copies unsynchronised in this state.
|
|
2102
|
+
|
|
2103
|
+
Both are documented rather than fixed: appandflow/stim#696.`
|
|
2104
|
+
},
|
|
2105
|
+
carry: {
|
|
2106
|
+
summary: "worktree warm copy results, lockfile mismatches, and remedies",
|
|
2107
|
+
body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
|
|
2108
|
+
(worktree warm)
|
|
2109
|
+
At least one entry could not be copied. The command exits 1 and names each
|
|
2110
|
+
failure. Existing entries stay untouched; any files already published remain.
|
|
2111
|
+
Inspect failed paths before retrying, because existing directories are skipped
|
|
2112
|
+
whole. "complete" means the eligible copy finished, not that dependencies
|
|
2113
|
+
are installed or match this branch. Progress and results go to stderr,
|
|
2114
|
+
with empty stdout.
|
|
2115
|
+
|
|
2116
|
+
"carry carried <dir>/Pods does not match the <dir>/Podfile.lock on disk here"
|
|
2117
|
+
Warm copied ignored Pods from the source checkout, but their Manifest.lock
|
|
2118
|
+
differs from the tracked Podfile.lock in this worktree. Warm does not change
|
|
2119
|
+
tracked files. Run the printed pod-install command before building directly.
|
|
2120
|
+
\`stim ios\` detects a mismatch and runs \`pod install\` for you.
|
|
2121
|
+
|
|
2122
|
+
"carry carried <dir>/Pods but there is no <dir>/Podfile.lock"
|
|
2123
|
+
Warm copied Pods but the destination has no Podfile.lock. Follow the printed
|
|
2124
|
+
pod-install command before building.
|
|
2125
|
+
|
|
2126
|
+
"carry carried dependencies may be stale: they do not match ..."
|
|
2127
|
+
The source checkout's lockfile differs from this branch's lockfile. Run the
|
|
2128
|
+
printed package-manager command before building. A carry whose lockfile
|
|
2129
|
+
matches is silent; the warning means a real difference.
|
|
2130
|
+
|
|
2131
|
+
If the source checkout has no dependencies to copy, use this project's
|
|
2132
|
+
package manager to install them. Warm does not install dependencies or prove
|
|
2133
|
+
the app is ready, unless \`--refresh\` installed them in the SOURCE CHECKOUT
|
|
2134
|
+
first; even then the copy can still carry a lockfile this branch does not
|
|
2135
|
+
have, which is exactly what these carry warnings report.`
|
|
2136
|
+
},
|
|
2137
|
+
environment: {
|
|
2138
|
+
summary: "npx registry E401/E404, the Node floor, no free Metro port, the reservation race",
|
|
2139
|
+
separator: "--- ENVIRONMENT ---",
|
|
2140
|
+
body: () => `"npm error code E401 / E404" while \`npx\` resolves the stim package
|
|
2141
|
+
The repo probably pins a private registry in \`.npmrc\`, so \`npx\` looked for
|
|
2142
|
+
the package there instead of on npm. Use the public registry for this command:
|
|
2143
|
+
|
|
2144
|
+
npx --registry=https://registry.npmjs.org stim <command>
|
|
2145
|
+
|
|
2146
|
+
A line such as \`npm warn exec ... will be installed\` is normal when using
|
|
2147
|
+
the no-install form.
|
|
2148
|
+
|
|
2149
|
+
"Unsupported engine" or a syntax error before Stim starts
|
|
2150
|
+
Stim requires Node 20.19.4 or later on Node 20, or Node 22.12.0 or later.
|
|
2151
|
+
Switch Node versions, then run the command again.
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
"Found no free Metro port between ..."
|
|
2155
|
+
200 consecutive ports are claimed or occupied. \`stim status\` shows what
|
|
2156
|
+
Stim knows about; the rest is other software.
|
|
2157
|
+
|
|
2158
|
+
"Could not reserve a Metro port after 5 attempts"
|
|
2159
|
+
Several commands raced for the same ports and each one lost. Nothing is
|
|
2160
|
+
wrong; retry.`
|
|
2161
|
+
},
|
|
2162
|
+
sandbox: {
|
|
2163
|
+
summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
|
|
2164
|
+
body: () => `RUNNING UNDER A SANDBOX
|
|
2165
|
+
|
|
2166
|
+
An agent harness that sandboxes shell commands typically permits writes
|
|
2167
|
+
inside the project and blocks the rest. Three things Stim needs sit outside
|
|
2168
|
+
that boundary, and none of the failures names the sandbox:
|
|
2169
|
+
|
|
2170
|
+
EPERM: operation not permitted, mkdir '<STIM_HOME>/workspaces/...'
|
|
2171
|
+
writes to STIM_HOME (~/.stim unless set)
|
|
2172
|
+
|
|
2173
|
+
CoreSimulatorService connection became invalid (macOS)
|
|
2174
|
+
Unable to locate device set: ... Code=61 "Connection refused"
|
|
2175
|
+
the simulator service simctl talks to over XPC
|
|
2176
|
+
|
|
2177
|
+
ADB server didn't ACK
|
|
2178
|
+
could not install *smartsocket* listener: Operation not permitted
|
|
2179
|
+
the adb server socket on tcp:5037
|
|
2180
|
+
|
|
2181
|
+
Measured on Claude Code and on Codex: the three fail the same way in both,
|
|
2182
|
+
so this is the shape of the problem, not one harness's quirk. Codex also
|
|
2183
|
+
blocks network egress by default, which breaks a cache lookup and a fetch.
|
|
2184
|
+
|
|
2185
|
+
\`stim doctor\` names this when a write to STIM_HOME actually fails, not
|
|
2186
|
+
merely when a harness that can sandbox is present. \`stim doctor --fix\`
|
|
2187
|
+
writes only when the report shows that finding, and only what the finding
|
|
2188
|
+
names: the three keys, into .claude/settings.local.json, the per-user file,
|
|
2189
|
+
merging with what is there. A report without the finding, with or without
|
|
2190
|
+
--platform, leaves that file alone. It refuses under Codex, which
|
|
2191
|
+
has no per-path allowance to add, and refuses any settings file it cannot
|
|
2192
|
+
parse rather than replace it: comments make one unparseable here even though
|
|
2193
|
+
Claude Code accepts them. Claude Code reads project settings from the
|
|
2194
|
+
directory a session starts in, so in a monorepo the file has to sit at that
|
|
2195
|
+
root to count, and a file written inside a worktree goes when the worktree
|
|
2196
|
+
does.
|
|
2197
|
+
|
|
2198
|
+
Two ways out, and choosing at the start of a session beats discovering it
|
|
2199
|
+
three failures in. Either run Stim with the harness's sandbox disabled, or
|
|
2200
|
+
allow the three. In Claude Code that is settings.json:
|
|
2201
|
+
|
|
2202
|
+
sandbox.filesystem.allowWrite ["~/.stim"]
|
|
2203
|
+
sandbox.network.allowMachLookup ["com.apple.coresimulator.*"]
|
|
2204
|
+
sandbox.network.allowLocalBinding true
|
|
2205
|
+
|
|
2206
|
+
In Codex the sandbox is one flag, \`codex -s\`, with no per-path allowance:
|
|
2207
|
+
workspace-write still refuses STIM_HOME.
|
|
2208
|
+
|
|
2209
|
+
A git credential helper is often blocked too. It prints \`failed to store\`
|
|
2210
|
+
on a fetch that otherwise succeeded, and is safe to ignore.`
|
|
2211
|
+
},
|
|
2212
|
+
STIM_CONFIG_CORRUPT: {
|
|
2213
|
+
summary: "~/.stim/config.json is not valid JSON and Stim never resets it",
|
|
2214
|
+
body: () => `STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON")
|
|
2215
|
+
Any command can raise it: every command reads ~/.stim/config.json first.
|
|
2216
|
+
The file holding every owned-device record will not parse, and Stim never
|
|
2217
|
+
resets it for you -- a silent reset would orphan every simulator it names.
|
|
2218
|
+
Repair the file, or move it aside (\`mv <path> <path>.broken\`) and accept
|
|
2219
|
+
that the devices it recorded become orphans you delete by hand.`
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
},
|
|
2223
|
+
lifecycle: {
|
|
2224
|
+
summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
|
|
2225
|
+
preamble: () => `ENVIRONMENT LIFECYCLE
|
|
2226
|
+
|
|
2227
|
+
Two workflows share steps 2 through 6.
|
|
2228
|
+
|
|
2229
|
+
SINGLE CHECKOUT: work in place, on a branch, in one directory. There is no
|
|
2230
|
+
step 1, and step 7 reclaims the environment without deleting the tree, which
|
|
2231
|
+
stays because it is the source checkout. That directory is your workspace, and
|
|
2232
|
+
no rule here about keeping the source checkout fit as a seed applies to it.
|
|
2233
|
+
|
|
2234
|
+
WORKTREE: the checkout you cloned is a seed. It stays clean and on the default
|
|
2235
|
+
branch so every worktree warmed from it starts current. Here the source checkout
|
|
2236
|
+
is infrastructure, not a workspace, and every rule below about its fitness as
|
|
2237
|
+
a seed belongs to this workflow.
|
|
2238
|
+
|
|
2239
|
+
# 1. Create a linked worktree with Git, unless a harness already did.
|
|
2240
|
+
# Choose the branch, path, and base ref with Git.
|
|
2241
|
+
git worktree add -b app/412 ../app-412 HEAD
|
|
2242
|
+
cd ../app-412
|
|
2243
|
+
stim worktree warm
|
|
2244
|
+
|
|
2245
|
+
# Warm copies missing ignored state from the source checkout; it does not
|
|
2246
|
+
# install dependencies.
|
|
2247
|
+
# In a monorepo, enter the app directory before starting the dev server.
|
|
2248
|
+
|
|
2249
|
+
# Optional: bring the SOURCE CHECKOUT up to date first, then copy that.
|
|
2250
|
+
stim worktree warm --refresh
|
|
2251
|
+
lock acquired
|
|
2252
|
+
checkout main 3 commits behind origin/main -> fast-forwarded to 9f2c1a3
|
|
2253
|
+
deps source /w/main: pnpm-lock.yaml changed -> pnpm install (41s)
|
|
2254
|
+
pods source /w/main/apps/mobile: ios/Podfile.lock unchanged -> skipped
|
|
2255
|
+
|
|
2256
|
+
# 2. The dev server, under a detached supervisor. Blocks until it is
|
|
2257
|
+
# verifiably THIS project's, then hands your shell back.
|
|
2258
|
+
stim start
|
|
2259
|
+
port 8082 (reserved)
|
|
2260
|
+
supervisor pid 41233
|
|
2261
|
+
|
|
2262
|
+
# If stale Metro transforms or file-map state require recovery:
|
|
2263
|
+
stim start --reset-cache
|
|
2264
|
+
# Restarts only this app's verified owned Metro, retaining its port/devices.
|
|
2265
|
+
# Uses a fresh persistent cache namespace, not deletion of shared cache files.
|
|
2266
|
+
# Other apps and worktrees keep their caches. Subsequent starts reuse the new
|
|
2267
|
+
# namespace; another reset changes it again. Native build caches are unchanged.
|
|
2268
|
+
# Expo requires SDK 54+ and Stim's config adapter. Custom file-map cache
|
|
2269
|
+
# managers must honor Metro's fileMapCacheDirectory for file-map invalidation.
|
|
2270
|
+
|
|
2271
|
+
# 3. Owned device booted, native inputs fingerprinted, cached build
|
|
2272
|
+
# installed (or built), app launched wired to port 8082, device-log
|
|
2273
|
+
# collector attached.
|
|
2274
|
+
stim ios # or: stim android
|
|
2275
|
+
device stim-app-412 (iPhone 17 26.5) (BF2A..) booted (9s)
|
|
2276
|
+
fingerprint a3f9b1.. hit (2s)
|
|
2277
|
+
install from cache (3s)
|
|
2278
|
+
launch com.example.app (1s)
|
|
2279
|
+
|
|
2280
|
+
# 4. Reproduce the affected behavior and inspect the baseline errors.
|
|
2281
|
+
# For a clean check, require exit 0 AND no matching errors.
|
|
2282
|
+
# Human mode prints "No matching log records" on stderr for zero matches.
|
|
2283
|
+
# Exit 0 alone means the query succeeded, even when it printed errors.
|
|
2284
|
+
stim logs --errors
|
|
2285
|
+
|
|
2286
|
+
# 5. Edit the JS. Fast Refresh applies it; no Stim command is involved.
|
|
2287
|
+
# For UI work, wait for the expected UI and repeat the affected interaction
|
|
2288
|
+
# on the reported device, using the existing automation session if any.
|
|
2289
|
+
stim logs --errors
|
|
2290
|
+
# Retain proof: a screenshot, recording, or relevant runtime output.
|
|
2291
|
+
# See: stim guide lifecycle verification
|
|
2292
|
+
|
|
2293
|
+
# 6. Pausing: supervisor halted, collectors reaped, owned device SHUT DOWN
|
|
2294
|
+
# (never deleted), port freed. Coming back costs a boot, not a create.
|
|
2295
|
+
stim stop
|
|
2296
|
+
|
|
2297
|
+
# 7. Remove this linked worktree and its environment. This also works for
|
|
2298
|
+
# unwarmed worktrees with no Stim registry entry. Git-created branches stay.
|
|
2299
|
+
stim worktree remove
|
|
2300
|
+
|
|
2301
|
+
Steps 2 and 3 are ordered, not interchangeable: \`ios\` and \`android\` never
|
|
2302
|
+
start the bundler, and refuse with STIM_NO_METRO when nothing holds the
|
|
2303
|
+
reserved port. That refusal costs a second; the alternative costs four minutes
|
|
2304
|
+
and produces an app that cannot load a bundle.
|
|
2305
|
+
|
|
2306
|
+
Repeat step 3 whenever a NATIVE input changes. A JS-only edit needs nothing --
|
|
2307
|
+
that is what Fast Refresh over the running dev server is for. \`stim reload\` is
|
|
2308
|
+
not part of the normal workflow. It is the explicit recovery path when Fast
|
|
2309
|
+
Refresh cannot clear the current screen, and on Android after a failed first
|
|
2310
|
+
bundle load. It reloads JavaScript and never restarts the app. Use \`stim
|
|
2311
|
+
reload ios\` or \`stim reload android\` to select a platform when both owned
|
|
2312
|
+
apps are live. For a physical device that reached Metro, use \`agent-device
|
|
2313
|
+
metro reload --metro-port <reported-port>\`. The detected iOS Local Network
|
|
2314
|
+
first-load remedy uses agent-device UI automation because no Metro peer exists
|
|
2315
|
+
yet. It never builds, installs, boots, or cold-launches. It acts only on a live
|
|
2316
|
+
app on this workspace's owned local simulator or emulator, and refuses release
|
|
2317
|
+
builds, stopped or unowned devices, a missing or foreign Metro, and an
|
|
2318
|
+
ambiguous no-platform request. Every reload goes over this workspace's Metro
|
|
2319
|
+
websocket, on both platforms. It never reopens a development-client URL,
|
|
2320
|
+
because that restarts the app rather than reloading its JavaScript.
|
|
2321
|
+
|
|
2322
|
+
How the message is addressed depends on the dev server, and \`strategy\` in
|
|
2323
|
+
the facts reports which you got. Where Metro can name its clients, Stim
|
|
2324
|
+
addresses every peer matching the platform and reports \`metro-websocket\`. A
|
|
2325
|
+
workspace Metro serves one app, so those peers are this app on however many
|
|
2326
|
+
devices are attached to that port, and \`targets\` says how many peers the request
|
|
2327
|
+
addressed.
|
|
2328
|
+
The bare React Native dev server cannot name its clients at all, so Stim
|
|
2329
|
+
broadcasts: \`metro-broadcast\` means every app connected to that Metro
|
|
2330
|
+
was sent a reload request and Stim cannot confirm \`appId\` was among them. Verify the UI on
|
|
2331
|
+
\`deviceId\`; if it did not change, reload from the app's own error screen or
|
|
2332
|
+
dev menu.
|
|
2333
|
+
|
|
2334
|
+
When Metro reports no peer for the app, Stim broadcasts a reload anyway before
|
|
2335
|
+
giving up, because matching is best-effort and an unmatched peer may still be
|
|
2336
|
+
this app. So verify the UI first: it may already have recovered. If it did not,
|
|
2337
|
+
retry: a client reconnects every 2 seconds, which is also this probe's timeout,
|
|
2338
|
+
so a single miss can be a reconnect window rather than an app that never
|
|
2339
|
+
connected. If it stays unreachable on iOS, an error in the first bundle leaves
|
|
2340
|
+
the app without a packager connection at all, and no retry will make it a peer.
|
|
2341
|
+
The command then routes the agent to the device's own controls in its existing
|
|
2342
|
+
automation session: press the error screen's Reload button, or open the dev
|
|
2343
|
+
menu and press Reload when no error screen is showing, and relaunch only when
|
|
2344
|
+
neither is reachable. Stim does not take over that stateful session.
|
|
2345
|
+
|
|
2346
|
+
When Metro itself does not answer within the probe's 2 seconds, nothing is
|
|
2347
|
+
known about the app. The command says to retry and check the dev server rather
|
|
2348
|
+
than sending the agent to the device.
|
|
2349
|
+
|
|
2350
|
+
A successful reload confirms that the request was sent. It does not wait for
|
|
2351
|
+
new JavaScript or observe the resulting UI. Verify the expected screen or
|
|
2352
|
+
interaction on the reported device and inspect \`stim logs --errors\` before
|
|
2353
|
+
claiming recovery; exit 0 alone does not prove it.
|
|
2354
|
+
|
|
2355
|
+
An iOS simulator launch command has a 60-second deadline. This is separate
|
|
2356
|
+
from bundle delivery and readiness verification: it gives CoreSimulator time
|
|
2357
|
+
to accept the launch, not the app extra time to report readiness. A timeout
|
|
2358
|
+
still fails the launch; Stim does not automatically retry it.
|
|
2359
|
+
|
|
2360
|
+
For an unverified Android launch, follow the emitted device-specific remedy.
|
|
2361
|
+
If the app is stuck before loading its first bundle, restart its process with
|
|
2362
|
+
the printed force-stop and launcher commands. Foregrounding the same process
|
|
2363
|
+
does not restart initialization. Confirm the bundle request and expected UI.
|
|
2364
|
+
|
|
2365
|
+
Debug Android launches on an owned emulator created in the same run get up to
|
|
2366
|
+
60 seconds for bundle loading; the verify phase names that budget. Existing
|
|
2367
|
+
emulators, remote targets, and physical devices keep the 20-second budget.
|
|
2368
|
+
Bundle delivery still requires the usual stability or app readiness check;
|
|
2369
|
+
an observed fatal error ends verification without waiting for the deadline.
|
|
2370
|
+
|
|
2371
|
+
ANDROID EMULATOR RESTARTS
|
|
2372
|
+
\`stim status\` resolves owned AVDs to their currently detected adb serial.
|
|
2373
|
+
Its Android JSON adds serial and state (detected, not-detected, missing,
|
|
2374
|
+
or unknown). Detection confirms device identity, not app health. Status
|
|
2375
|
+
reports a serial change without changing forwarding or restarting anything.
|
|
2376
|
+
Rerun \`stim android\` with the same build options in that workspace to restore Metro
|
|
2377
|
+
forwarding, then reopen agent-device on the serial that run reports.
|
|
2378
|
+
Other workspaces' simulators and automation sessions remain theirs.
|
|
2379
|
+
|
|
2380
|
+
DESTRUCTIVE COMMANDS -- ask the user first
|
|
2381
|
+
gc --delete deletes orphaned stim-* devices, tens of GB
|
|
2382
|
+
gc --delete --cache all empties the shared build caches every project uses
|
|
2383
|
+
gc --delete --cache <name>
|
|
2384
|
+
empties only the caches that carry <name>
|
|
2385
|
+
worktree remove --force discards uncommitted and untracked work
|
|
2386
|
+
|
|
2387
|
+
Permanent local deletion lives in exactly TWO commands: \`worktree remove\`
|
|
2388
|
+
(the workspace you name) and \`gc --delete\` (the machine). For a local device,
|
|
2389
|
+
\`stop\` shuts it down and never deletes it. For a recorded EAS session,
|
|
2390
|
+
\`stop\` irreversibly ends the session. Externally started servers are left
|
|
2391
|
+
alone, even when they use the reserved port. There is no \`--delete\` flag on
|
|
2392
|
+
\`stop\`.
|
|
2393
|
+
|
|
2394
|
+
CAPACITY
|
|
2395
|
+
A booted iOS sim is roughly 1-2 GB of RAM, an Android emulator 2-3 GB. On a
|
|
2396
|
+
16 GB machine plan for 2-3 live environments. Nothing enforces this;
|
|
2397
|
+
\`stim status\` is how you check -- it reports every workspace on the
|
|
2398
|
+
machine, not just this one.
|
|
2399
|
+
|
|
2400
|
+
TWO REPORTS, TWO QUESTIONS
|
|
2401
|
+
"What is running" is \`stim status\`: live state, right now. "How much the
|
|
2402
|
+
cache saved" is \`stim stats\`: aggregate counters for this project and for
|
|
2403
|
+
the machine, with a hit rate and an estimate of the time saved (see
|
|
2404
|
+
\`guide facts stats\`).`,
|
|
2405
|
+
sections: {
|
|
2406
|
+
readiness: {
|
|
2407
|
+
summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
|
|
2408
|
+
body: () => `OPTIONAL APP READINESS
|
|
2409
|
+
|
|
2410
|
+
No package or SDK is needed. This applies to debug ios/android launches with
|
|
2411
|
+
Metro verification enabled, not release builds or --no-metro-check.
|
|
2412
|
+
|
|
2413
|
+
1. Add this at app startup, before essential initialization:
|
|
2414
|
+
|
|
2415
|
+
if (__DEV__) console.info('[stim:readiness] pending');
|
|
2416
|
+
|
|
2417
|
+
2. Use the app's real ready state: essential initialization succeeded, usable
|
|
2418
|
+
content rendered, and the splash screen hidden. From that success path:
|
|
2419
|
+
|
|
2420
|
+
if (__DEV__) console.info('[stim:readiness] ready');
|
|
2421
|
+
|
|
2422
|
+
In an Expo root component with an existing isReady state:
|
|
2423
|
+
|
|
2424
|
+
useEffect(() => {
|
|
2425
|
+
if (!isReady) return;
|
|
2426
|
+
let active = true;
|
|
2427
|
+
SplashScreen.hideAsync().then(() => {
|
|
2428
|
+
if (active && __DEV__) console.info('[stim:readiness] ready');
|
|
2429
|
+
}).catch(console.error);
|
|
2430
|
+
return () => { active = false; };
|
|
2431
|
+
}, [isReady]);
|
|
2432
|
+
|
|
2433
|
+
Import useEffect from react and SplashScreen as a namespace from
|
|
2434
|
+
expo-splash-screen. Keep the pending log outside the component at module
|
|
2435
|
+
scope, before startup work. Use the project's existing splash lifecycle;
|
|
2436
|
+
do not add a splash dependency to a bare app just for this integration.
|
|
2437
|
+
Static imports run before module-scope statements. If imported startup work
|
|
2438
|
+
must opt into the wait, emit pending from an earlier app entry module before
|
|
2439
|
+
loading that work. A failure before pending keeps the default check.
|
|
2440
|
+
Cover login, onboarding, and deep links. Do not report ready merely on
|
|
2441
|
+
root mount, on a timer, in a failure handler, or in finally.
|
|
2442
|
+
|
|
2443
|
+
3. Run the platform command from the app directory. Check for the readiness
|
|
2444
|
+
phase and inspect the UI on the reported device. Exercise a slow success,
|
|
2445
|
+
a missing ready message, and a startup error. Retain the relevant output.
|
|
2446
|
+
|
|
2447
|
+
Without an observed pending, Stim keeps its default 3-second stability window
|
|
2448
|
+
after bundle delivery. Managed Metro servers report the native bundle response
|
|
2449
|
+
finishing; build-complete output alone does not close an observed request.
|
|
2450
|
+
Without response capture, Stim falls back to the build-complete marker.
|
|
2451
|
+
When Android reports queued JavaScript loading, Stim waits for a device
|
|
2452
|
+
JavaScript log before starting stability, bounded by the bundle timeout.
|
|
2453
|
+
Pending must be observed before the stability window closes. It opts into
|
|
2454
|
+
waiting for ready until 30 seconds after the same completion signal.
|
|
2455
|
+
Repeated pending messages never extend the deadline. Ready can end the wait
|
|
2456
|
+
early; an app error or process exit interrupts it. No ready means readiness
|
|
2457
|
+
not confirmed, not a crash or successful readiness.
|
|
2458
|
+
|
|
2459
|
+
Only exact standalone info/debug messages captured from this app's device log
|
|
2460
|
+
for this platform and current launch are accepted. Stale, other-platform,
|
|
2461
|
+
error-level, and embedded example messages are ignored. Unlabelled shared
|
|
2462
|
+
Metro output cannot identify a platform. If pending is not captured in time,
|
|
2463
|
+
including when device logs are unavailable, the default check applies.
|
|
2464
|
+
|
|
2465
|
+
The app declares readiness; Stim does not inspect a rendered frame. This does
|
|
2466
|
+
not change launched JSON semantics or the need to inspect the expected UI and
|
|
2467
|
+
stim logs --errors. A reload request does not run this launch check.`
|
|
2468
|
+
},
|
|
2469
|
+
verification: {
|
|
2470
|
+
summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
|
|
2471
|
+
body: () => `VERIFY THE CHANGE
|
|
2472
|
+
|
|
2473
|
+
For a UI change or bug fix, decide what observable result would prove the task
|
|
2474
|
+
is complete. A successful build, a live process, or an empty log query does
|
|
2475
|
+
not prove that result.
|
|
2476
|
+
|
|
2477
|
+
1. On the device reported by ios or android, reproduce the affected behavior
|
|
2478
|
+
before editing and capture the baseline with stim logs --errors. If the
|
|
2479
|
+
issue does not reproduce, record what you tried instead of claiming it did.
|
|
2480
|
+
2. Make the change. JavaScript and TypeScript normally use Fast Refresh;
|
|
2481
|
+
native input changes need another ios or android run. Follow the printed
|
|
2482
|
+
recovery remedy if an error screen remains or the native process exited.
|
|
2483
|
+
3. Use the full reported device ID with your UI automation tool. Continue in
|
|
2484
|
+
the existing session for that device when one exists. Wait for the expected
|
|
2485
|
+
content or control before inspecting the screen; launch evidence alone
|
|
2486
|
+
does not prove that the first screen has rendered. Bound the wait and report
|
|
2487
|
+
verification as incomplete if the expected state never appears.
|
|
2488
|
+
4. Repeat the affected interaction and check its expected outcome, then run
|
|
2489
|
+
stim logs --errors. Read the records, not just the exit code. Earlier client
|
|
2490
|
+
errors can remain after Fast Refresh; compare their timestamps with the
|
|
2491
|
+
reproduction and check whether they recur. Do not relaunch just to clear
|
|
2492
|
+
the log window. An empty query does not prove log capture succeeded.
|
|
2493
|
+
5. Retain a screenshot for a visible result or a short recording for an
|
|
2494
|
+
interaction. Report what you exercised, what you observed, any unresolved
|
|
2495
|
+
errors, and the proof location before stopping the app or removing the
|
|
2496
|
+
worktree. If device access or another prerequisite prevents verification,
|
|
2497
|
+
name the missing check.
|
|
2498
|
+
|
|
2499
|
+
For a change without a UI effect, use the relevant runtime output or test
|
|
2500
|
+
result as proof instead of requiring an unrelated screenshot.`
|
|
2501
|
+
},
|
|
2502
|
+
progress: {
|
|
2503
|
+
summary: "phase lines, the label set, heartbeats and their ~ estimate, what warm, start, stop and remove print",
|
|
2504
|
+
body: () => `PROGRESS ON A LONG RUN
|
|
2505
|
+
Native build progress goes to stderr. In \`--json\` mode, stdout carries only
|
|
2506
|
+
the result payload. Plain \`start\` also prints progress on stdout. Every
|
|
2507
|
+
progress line has the same shape -- two spaces, a label padded to eleven
|
|
2508
|
+
columns, the FACT, and the time the step cost:
|
|
2509
|
+
|
|
2510
|
+
<label> <fact> (<duration>)
|
|
2511
|
+
|
|
2512
|
+
The labels are a closed set, and nothing else is ever printed in that
|
|
2513
|
+
column:
|
|
2514
|
+
|
|
2515
|
+
branch build cache caches carry checkout
|
|
2516
|
+
deps device
|
|
2517
|
+
devices error failed findings fingerprint gems
|
|
2518
|
+
install installs ip.txt lan launch lease
|
|
2519
|
+
lock log
|
|
2520
|
+
logs meaning metro pods port prebuild
|
|
2521
|
+
project readiness ready remedy removed resolved
|
|
2522
|
+
result
|
|
2523
|
+
services
|
|
2524
|
+
setting settings setup state stats stop
|
|
2525
|
+
storage swap verify version workspace
|
|
2526
|
+
|
|
2527
|
+
\`app\` and \`compilation cache\` join them in the stdout block a successful
|
|
2528
|
+
run ends with. When a native build runs, its compilation-cache result is
|
|
2529
|
+
printed once as a \`cache\` progress line as soon as the build returns,
|
|
2530
|
+
including a failed build. It survives a later install or launch failure.
|
|
2531
|
+
The stdout \`compilation cache\` line is only for an artifact-cache hit
|
|
2532
|
+
(compilation did not run). A line states a fact; the reason a fact matters
|
|
2533
|
+
lives in this guide, not in the run output. Both platforms use the
|
|
2534
|
+
same words, so \`build ok (51.8s)\` and
|
|
2535
|
+
\`launch com.example.app (2s)\` read the same on iOS and Android; the
|
|
2536
|
+
artifact name is in the \`--json\` payload.
|
|
2537
|
+
|
|
2538
|
+
A step that costs real time is named and timed, including the step that
|
|
2539
|
+
creates or reconciles the owned device:
|
|
2540
|
+
|
|
2541
|
+
device stim-app-412 (BF2A..) created (2m14s)
|
|
2542
|
+
|
|
2543
|
+
On iOS that step does not wait the boot out. It creates the simulator, asks
|
|
2544
|
+
it to boot, and hands the wait back, so the run fingerprints the native
|
|
2545
|
+
inputs and resolves the build cache while \`simctl bootstatus\` is still
|
|
2546
|
+
running; it joins the boot before it installs anything. The
|
|
2547
|
+
\`device ... booted\` and \`fingerprint ...\` lines each report their own
|
|
2548
|
+
elapsed time, and those two overlap -- adding every line up overstates the
|
|
2549
|
+
run.
|
|
2550
|
+
|
|
2551
|
+
A step that is still running heartbeats every 30 seconds, on the 30-second
|
|
2552
|
+
grid, so the values read 30s, 1m00s, 1m30s and never repeat. A heartbeat
|
|
2553
|
+
reuses its phase's label and column and names what the phase is doing, never
|
|
2554
|
+
the build tool's own last line -- that transcript is in the build log
|
|
2555
|
+
(\`logs --source build\`):
|
|
2556
|
+
|
|
2557
|
+
build still compiling (1m00s of ~3m10s)
|
|
2558
|
+
build still compiling (4m00s, usually ~3m10s)
|
|
2559
|
+
build still compiling (1m00s)
|
|
2560
|
+
pods still installing (1m30s of ~1m40s)
|
|
2561
|
+
build waiting on /w/app-411 (pid 41233, 1m30s elapsed) -- stim guide lifecycle concurrency
|
|
2562
|
+
|
|
2563
|
+
The \`~\` value is an estimate, never a countdown; the third line is a
|
|
2564
|
+
project with no record to estimate from yet. \`guide facts stats\` says where
|
|
2565
|
+
the number comes from.
|
|
2566
|
+
|
|
2567
|
+
The lifecycle commands use the same column. \`worktree warm\` reports
|
|
2568
|
+
copied and kept entries on stderr, with empty stdout:
|
|
2569
|
+
|
|
2570
|
+
carry copied node_modules from /w/main
|
|
2571
|
+
carry complete: 1 ignored entries copied, 0 kept, 0 failed
|
|
2572
|
+
|
|
2573
|
+
\`--refresh\` always prints a \`lock\` line, then its own facts, one per
|
|
2574
|
+
step, before those. A plain warm prints the \`lock\` line only when its copy
|
|
2575
|
+
actually waited for another warm:
|
|
2576
|
+
|
|
2577
|
+
lock acquired (waited 12s for stim worktree warm --refresh pid 41233) -- stim guide lifecycle options
|
|
2578
|
+
checkout janic/wip 2 commits behind origin/janic/wip -> fast-forwarded to 4b81e0c
|
|
2579
|
+
not the default branch (main); worktrees seeded from this copy
|
|
2580
|
+
carry janic/wip's dependencies
|
|
2581
|
+
deps source /w/main: pnpm-lock.yaml unchanged -> skipped
|
|
2582
|
+
pods source /w/main/apps/mobile: ios/Podfile.lock changed -> pod install (1m12s)
|
|
2583
|
+
|
|
2584
|
+
A wait reports how long this caller has waited and names the holder:
|
|
2585
|
+
\`lock waiting 40s for stim worktree warm --refresh (pid 41233) -- stim guide lifecycle options\`.
|
|
2586
|
+
|
|
2587
|
+
\`start\` names the port, the supervisor mode and its pid on one line
|
|
2588
|
+
(\`metro starting on port 8083 (expo-child, supervisor pid 13724)\`),
|
|
2589
|
+
and \`stop\` reports what it released:
|
|
2590
|
+
|
|
2591
|
+
stop supervisor pid 34856
|
|
2592
|
+
stop collector ios pid 45268
|
|
2593
|
+
device shut down stim-e2e-2
|
|
2594
|
+
port released 8084
|
|
2595
|
+
|
|
2596
|
+
\`worktree remove\` reports itself the same way: the branch decision, the
|
|
2597
|
+
owned device, any released device lease, and this workspace's own state
|
|
2598
|
+
directory, each on its own line. Nothing prints on stdout; even the removed
|
|
2599
|
+
path is on stderr:
|
|
2600
|
+
|
|
2601
|
+
branch kept app/412 (Stim did not create it)
|
|
2602
|
+
device parked stim-parked (iPhone 17 26.5) 9c1f (9C1F..)
|
|
2603
|
+
lease released the ios lease on 00008101-000A10913C89001E (it ran until 14:32:10)
|
|
2604
|
+
workspace removed /w/.stim/workspaces/3f9c2a
|
|
2605
|
+
removed /w/app-412
|
|
2606
|
+
|
|
2607
|
+
Removal works with any linked worktree, whether warmed or not, and does not
|
|
2608
|
+
require a Stim registry entry. Git-created branches are kept. An existing
|
|
2609
|
+
Stim ownership record permits deleting a branch only when it has no unique
|
|
2610
|
+
commits; otherwise the command reports why it kept it. On the source checkout,
|
|
2611
|
+
\`worktree remove\` reclaims only the
|
|
2612
|
+
environment -- the same \`device\`, \`lease\` and \`workspace\` lines, ending
|
|
2613
|
+
with a sentence instead of a \`removed\` line, because the checkout itself
|
|
2614
|
+
is never touched: \`Reclaimed the environment; the working tree stays (it
|
|
2615
|
+
is the source checkout).\`
|
|
2616
|
+
|
|
2617
|
+
A GAP BETWEEN HEARTBEATS IS NOT A HANG. Stim runs device tools
|
|
2618
|
+
synchronously, so a long \`simctl\`, \`adb\` or copy call holds the timer
|
|
2619
|
+
until it returns; the next heartbeat then lands on the grid, which is why an
|
|
2620
|
+
elapsed value can jump. Read the phase lines, not the wall clock, before
|
|
2621
|
+
killing a run.`
|
|
2622
|
+
},
|
|
2623
|
+
pool: {
|
|
2624
|
+
summary: "parked and adopted simulators: what park and adoption clear or keep, the model and runtime match",
|
|
2625
|
+
body: () => ` THE SIMULATOR POOL
|
|
2626
|
+
\`worktree remove\` PARKS this workspace's owned simulator instead of
|
|
2627
|
+
deleting it, and the next workspace that wants the same model and runtime
|
|
2628
|
+
ADOPTS it. A simulator that has booted before boots in about 9s; a freshly
|
|
2629
|
+
created one costs about 30s, and \`simctl erase\` puts most of that back, so
|
|
2630
|
+
a parked simulator keeps its app installed and is cleaned in pieces:
|
|
2631
|
+
|
|
2632
|
+
at park shut down, the app's data cleared on disk (Documents,
|
|
2633
|
+
Library, tmp, SystemData: NSUserDefaults, AsyncStorage,
|
|
2634
|
+
SQLite), renamed \`stim-parked (<model> <runtime>) <4 hex>\`
|
|
2635
|
+
at adoption renamed for the adopting workspace, then, inside the boot
|
|
2636
|
+
the run pays anyway, \`simctl privacy reset all\` and
|
|
2637
|
+
\`simctl keychain reset\`; at install, every OTHER app the
|
|
2638
|
+
previous workspace left is uninstalled
|
|
2639
|
+
|
|
2640
|
+
A parked simulator KEEPS its system state: pasteboard, Safari data, photos,
|
|
2641
|
+
contacts, calendars, installed profiles, Simulator settings, app-group
|
|
2642
|
+
containers, and device-level defaults. Isolation covers the app's data, the
|
|
2643
|
+
privacy grants, the keychain and the installed apps -- not a clean system
|
|
2644
|
+
image. Set the bound to 0 when a project needs one.
|
|
2645
|
+
|
|
2646
|
+
Adoption matches the device type AND the runtime EXACTLY: a ticket that asks
|
|
2647
|
+
for an iPad never gets an iPhone, and a request for iOS 18.5 never gets 26.5.
|
|
2648
|
+
No match creates a new simulator, as before. After a runtime upgrade the
|
|
2649
|
+
parked simulators on the old runtime are never adopted; they leave by
|
|
2650
|
+
eviction or \`gc --delete\`.
|
|
2651
|
+
|
|
2652
|
+
The pool targets at most \`pool.iosParkedMax\` simulators (default 3, about
|
|
2653
|
+
2.5 GB each). Past that the oldest parked one is deleted:
|
|
2654
|
+
|
|
2655
|
+
device parked stim-parked (iPhone 17 26.5) 9c1f (9C1F..)
|
|
2656
|
+
device deleted stim-parked (iPhone 17 26.5) 4b02 (pool over 3)
|
|
2657
|
+
|
|
2658
|
+
A failed or unverifiable deletion keeps its ownership record so \`gc\` can
|
|
2659
|
+
retry it. The reported pool can temporarily exceed the bound rather than
|
|
2660
|
+
orphaning a simulator.
|
|
2661
|
+
|
|
2662
|
+
and an adopting run says so where a plain boot would say \`booted\`:
|
|
2663
|
+
|
|
2664
|
+
device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (11s)
|
|
2665
|
+
|
|
2666
|
+
That time includes the two resets, so it runs longer than a plain boot.
|
|
2667
|
+
\`stim status\` prints one line while the pool is not empty:
|
|
2668
|
+
|
|
2669
|
+
pool: 2 parked iOS simulators (max 3)
|
|
2670
|
+
|
|
2671
|
+
\`stim gc\` reports the pool, and \`stim gc --delete\` empties every entry
|
|
2672
|
+
it can re-verify:
|
|
2673
|
+
|
|
2674
|
+
Parked simulators (2, 5.1 GB):
|
|
2675
|
+
ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago 2.6 GB
|
|
2676
|
+
--delete attempts every parked simulator and keeps failures.
|
|
2677
|
+
|
|
2678
|
+
If simulator listing or deletion fails, \`gc --delete\` reports the failure
|
|
2679
|
+
and keeps that entry. It never turns an unverified absence into a dropped
|
|
2680
|
+
ownership record.
|
|
2681
|
+
|
|
2682
|
+
That deletion works even under a redirected \`STIM_HOME\`, where the sweep
|
|
2683
|
+
for unlisted \`stim-\` devices stays refused: a parked record in THIS config
|
|
2684
|
+
proves that simulator is Stim's and parked by this home. \`stop\` never
|
|
2685
|
+
parks -- it shuts the owned simulator down and keeps it assigned. Neither
|
|
2686
|
+
does \`gc --delete\`, which is deleting what it finds.
|
|
2687
|
+
|
|
2688
|
+
ANDROID EMULATOR POOL
|
|
2689
|
+
Android uses the same bounded park/adopt lifecycle, with
|
|
2690
|
+
\`pool.androidParkedMax\` (default 3) or STIM_POOL_ANDROID_PARKED_MAX.
|
|
2691
|
+
A redirected STIM_HOME disables parking unless that environment override
|
|
2692
|
+
is set. Zero disables parking and adoption. \`stop\` keeps the assignment;
|
|
2693
|
+
\`worktree remove\` parks eligible AVDs, and \`gc --delete\` empties the pool.
|
|
2694
|
+
|
|
2695
|
+
Adoption matches the system image, data partition size, and the creation
|
|
2696
|
+
settings from android.avdConfig / android.avdConfigFile. The AVD keeps its
|
|
2697
|
+
original stim-<label> name so its Quick Boot snapshot can survive reuse.
|
|
2698
|
+
Normal desktop boots allow Quick Boot; headless Linux disables snapshots.
|
|
2699
|
+
Incompatible AVDs stay parked until eviction or GC. AVDs created by older
|
|
2700
|
+
versions without a recorded creation configuration are deleted at removal.
|
|
2701
|
+
|
|
2702
|
+
When a fresh AVD's preferred name belongs to another workspace or a parked
|
|
2703
|
+
entry, Stim adds a short suffix instead of taking over that device. Always
|
|
2704
|
+
use the actual device name and serial reported by the platform command.
|
|
2705
|
+
|
|
2706
|
+
Android cleanup happens AFTER boot, before install or launch: \`adb shell
|
|
2707
|
+
pm clear\` clears the adopting app's data while retaining its APK, and
|
|
2708
|
+
other third-party apps are uninstalled. If ADB goes offline or closes the
|
|
2709
|
+
connection, Stim waits for boot readiness and retries cleanup for up to 30
|
|
2710
|
+
seconds, verifying the same owned AVD before each destructive command.
|
|
2711
|
+
Failed cleanup blocks launch and remains pending for a retry. The installed APK's SHA-256 must match the
|
|
2712
|
+
requested artifact before Stim skips installation; a package name or cache
|
|
2713
|
+
key alone is insufficient, including for release builds with swapped JS.
|
|
2714
|
+
If the retained APK has a conflicting signer or version, adoption uninstalls
|
|
2715
|
+
it and retries installation. Adoption stays pending until installation succeeds.
|
|
2716
|
+
|
|
2717
|
+
Parked AVDs retain app data until adoption. System apps, shared storage,
|
|
2718
|
+
accounts and device settings also remain: this is not a factory reset.
|
|
2719
|
+
Set the Android bound to 0 when a project needs a fresh device. Status lists
|
|
2720
|
+
parked Android emulators; GC reports their system image, age and disk size.
|
|
2721
|
+
`
|
|
2722
|
+
},
|
|
2723
|
+
builds: {
|
|
2724
|
+
summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, runtime state",
|
|
2725
|
+
body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
|
|
2726
|
+
When several native worktrees are coming, build the source checkout once to
|
|
2727
|
+
seed the shared caches before warming the linked worktrees. Skip this extra
|
|
2728
|
+
build for one-off or JavaScript-only work. For local simulator or emulator
|
|
2729
|
+
work, run these commands in the source checkout's app directory:
|
|
2730
|
+
|
|
2731
|
+
stim doctor --platform ios # or: --platform android
|
|
2732
|
+
stim start
|
|
2733
|
+
stim ios # or: stim android
|
|
2734
|
+
stim stop
|
|
2735
|
+
|
|
2736
|
+
Follow the normal ownership and consent rules in guide agent.
|
|
2737
|
+
|
|
2738
|
+
IOS SCHEME SELECTION
|
|
2739
|
+
Pass \`stim ios --scheme "App Staging"\` to select an exact shared Xcode
|
|
2740
|
+
app scheme. Unknown names refuse with the available choices. This is the
|
|
2741
|
+
Xcode scheme, not the app's URL scheme. Combine it with --configuration
|
|
2742
|
+
when choosing both an app scheme and a build configuration.
|
|
2743
|
+
|
|
2744
|
+
Without --scheme, automatic selection is unchanged:
|
|
2745
|
+
Stim keeps a scheme matching the workspace/project name, or the sole non-test
|
|
2746
|
+
scheme. When neither identifies one, it also checks the static top-level name
|
|
2747
|
+
in the app directory's app.json against Xcode's listed schemes. It does not
|
|
2748
|
+
execute app config or choose an arbitrary scheme from an ambiguous list.
|
|
2749
|
+
If selection fails, pass --scheme with an available name, or share the app
|
|
2750
|
+
scheme in Xcode first. See guide errors STIM_NO_SCHEME.
|
|
2751
|
+
|
|
2752
|
+
Explicit schemes have separate artifact keys, shared-build locks, and Xcode
|
|
2753
|
+
build directories. Stim identifies the resulting application from Xcode's
|
|
2754
|
+
resolved build settings, even when the scheme and product names differ;
|
|
2755
|
+
ambiguous products refuse rather than installing another app. Local and
|
|
2756
|
+
configured Stim cache providers use the scheme-specific key. The older Expo
|
|
2757
|
+
buildCacheProvider tier is skipped for explicit schemes because a provider
|
|
2758
|
+
may key only on the fingerprint and return another scheme's app. Omitting
|
|
2759
|
+
--scheme retains the existing cache keys and provider behavior.
|
|
2760
|
+
|
|
2761
|
+
AN ARTIFACT THE DEVICE ALREADY HOLDS IS NOT INSTALLED AGAIN
|
|
2762
|
+
Both platforms store the artifact verbatim, so its hash is its identity.
|
|
2763
|
+
Before installing, Stim hashes the artifact it is about to install and the
|
|
2764
|
+
one the device already has -- \`pm path\` then \`sha256sum\` on Android, the
|
|
2765
|
+
\`simctl get_app_container\` bundle on iOS. Byte-identical means the install
|
|
2766
|
+
is skipped. The phase still reports the cost of proving that identity, but
|
|
2767
|
+
avoids the ~43s a 400MB APK can cost to copy and install over USB.
|
|
2768
|
+
|
|
2769
|
+
install unchanged (emulator-5584 already has this build) (0.4s)
|
|
2770
|
+
|
|
2771
|
+
On iOS the install line names the identity proof separately from the Expo
|
|
2772
|
+
dev-client preference writes, so a slow simulator command is never charged
|
|
2773
|
+
to an install that did not run:
|
|
2774
|
+
|
|
2775
|
+
install unchanged (stim-app already has this build) (0.4s)
|
|
2776
|
+
install dev client prepared (0.9s)
|
|
2777
|
+
|
|
2778
|
+
The skip needs PROOF. A package that is not installed, a split install, an
|
|
2779
|
+
image without \`sha256sum\`, and any adb or simctl failure all read as
|
|
2780
|
+
"cannot determine", and the run installs exactly as it always did. A release
|
|
2781
|
+
run swaps this workspace's JS into a COPY of the artifact, which is a
|
|
2782
|
+
different artifact and is therefore always installed.
|
|
2783
|
+
|
|
2784
|
+
\`--json\` carries installSkipped so a caller can tell a skipped run from an
|
|
2785
|
+
installed one.
|
|
2786
|
+
|
|
2787
|
+
RUNTIME STATE AND BUILD INPUTS
|
|
2788
|
+
Runtime state is stored outside the project tree under
|
|
2789
|
+
$STIM_HOME/workspaces/<project>--<digest>/ (default ~/.stim/workspaces/).
|
|
2790
|
+
The aggregate run counters \`stats\` prints live beside it in
|
|
2791
|
+
$STIM_HOME/stats.json, one bucket per project and platform plus a machine-wide
|
|
2792
|
+
one; nothing per run is kept there.
|
|
2793
|
+
No .gitignore entry is created or required.
|
|
2794
|
+
Native preparation can change project files: expo prebuild generates native
|
|
2795
|
+
sources, and pod install can update Podfile.lock. Review those changes before
|
|
2796
|
+
committing. The shared caches need no project-file edits. The defaults below can be changed
|
|
2797
|
+
with machine or project optimization settings; see \`guide settings\`:
|
|
2798
|
+
|
|
2799
|
+
ios xcodebuild carries COMPILATION_CACHE_ENABLE_CACHING, a shared
|
|
2800
|
+
COMPILATION_CACHE_CAS_PATH and a clang prefix mapping of this
|
|
2801
|
+
workspace's root, so compiled output crosses worktrees with no
|
|
2802
|
+
Podfile post_install block. Xcode 26+ only, and skipped entirely
|
|
2803
|
+
when the project configured ccache (the two defeat each other).
|
|
2804
|
+
android gradlew carries --build-cache, so task outputs cross worktrees with
|
|
2805
|
+
no org.gradle.caching=true in gradle.properties. Debug builds also
|
|
2806
|
+
carry -PreactNativeArchitectures=<target ABI>, using the owned
|
|
2807
|
+
emulator system-image ABI or the physical device's primary ABI.
|
|
2808
|
+
Unknown targets and Release builds stay universal.
|
|
2809
|
+
ccache the same gradlew run carries an absolute
|
|
2810
|
+
CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER plus
|
|
2811
|
+
CCACHE_DIR, CCACHE_BASEDIR, CCACHE_NOHASHDIR, CCACHE_SLOPPINESS
|
|
2812
|
+
and CCACHE_MAXSIZE whenever a ccache binary is on PATH, so the C++
|
|
2813
|
+
objects cross worktrees as well. Nothing is set when ccache is
|
|
2814
|
+
absent, or when the project passes a CMake compiler launcher of
|
|
2815
|
+
its own.
|
|
2816
|
+
start the dev server gets a shared Metro FileStore APPENDED to whatever
|
|
2817
|
+
the project configured -- in-process on a bare project, and through
|
|
2818
|
+
Expo's config override on SDK 54+. Expo SDK 53 and older use their
|
|
2819
|
+
normal Metro cache. Turn it off machine-wide with
|
|
2820
|
+
{ "optimizations": { "metroSharedCache": false } } in
|
|
2821
|
+
~/.stim/config.json; see \`guide settings\`. A project that calls
|
|
2822
|
+
\`sharedCacheStores()\` from @stim-cli/metro in its own metro
|
|
2823
|
+
config also gets the \`cache.provider\` tier behind that store.
|
|
2824
|
+
|
|
2825
|
+
Every Stim Android build uses a CMake staging profile, including the default
|
|
2826
|
+
ccache mode. The first run with this layout configures a separate build tree;
|
|
2827
|
+
changing modes selects another profile. See \`guide settings\` for cleanup.
|
|
2828
|
+
|
|
2829
|
+
Each reports its cache setup. \`stim doctor\` checks missing or stale setup
|
|
2830
|
+
when a build is blocked or slow. It reports what Stim cannot handle itself
|
|
2831
|
+
(ccache absent from PATH or a .cxx that predates the
|
|
2832
|
+
launcher, a fingerprint no fresh worktree reproduces, a provider on a key this
|
|
2833
|
+
SDK ignores) and settings for builds outside Stim.
|
|
2834
|
+
|
|
2835
|
+
IOS DEBUG ARCHITECTURES
|
|
2836
|
+
Doctor reads Xcode's effective Debug simulator settings for app targets and
|
|
2837
|
+
generated Pods, including xcconfig inheritance and SDK-specific overrides.
|
|
2838
|
+
It warns when ONLY_ACTIVE_ARCH=NO leaves multiple architectures after ARCHS,
|
|
2839
|
+
VALID_ARCHS, and EXCLUDED_ARCHS are combined. A Podfile post_install helper can
|
|
2840
|
+
cause this even when the app target already uses ONLY_ACTIVE_ARCH=YES.
|
|
2841
|
+
Review that override for local Debug builds; preserve intentional Release and
|
|
2842
|
+
distribution settings. Regenerate Pods through the project's normal workflow
|
|
2843
|
+
after changing a helper, then rerun \`stim doctor --platform ios\`.
|
|
2844
|
+
Doctor never evaluates Podfile Ruby, installs Pods, or changes architecture
|
|
2845
|
+
settings, including under --fix. This inspection runs only in doctor, with
|
|
2846
|
+
30 seconds per Xcode query and 60 seconds total. Missing generated projects,
|
|
2847
|
+
failed metadata queries, and unresolved settings produce an unverified note
|
|
2848
|
+
rather than an architecture warning or a verified clean result.
|
|
2849
|
+
|
|
2850
|
+
WHY ANDROID NEEDS CCACHE, AND WHAT IT COSTS
|
|
2851
|
+
Every AGP CMake task is uncacheable by Gradle, so --build-cache serves not one
|
|
2852
|
+
C++ compile. Without a launcher a fresh worktree recompiles every translation
|
|
2853
|
+
unit, which on a React Native app with native modules is most of a first build.
|
|
2854
|
+
The shared objects live at $STIM_HOME/ccache (default ~/.stim/ccache), which is
|
|
2855
|
+
registered for \`gc\` and prunes itself at CCACHE_MAXSIZE.
|
|
2856
|
+
|
|
2857
|
+
CCACHE_BASEDIR rewrites paths under the workspace root relative to the compile
|
|
2858
|
+
directory and CCACHE_NOHASHDIR keeps the working directory out of the hash;
|
|
2859
|
+
together they are what lets an object built in one worktree match in another.
|
|
2860
|
+
The trade-off is the same class as the iOS CAS one: an object reused from
|
|
2861
|
+
worktree A carries A's directory as its DWARF comp_dir, so a debugger stepping
|
|
2862
|
+
into reused C++ resolves sources against that path.
|
|
2863
|
+
|
|
2864
|
+
When Stim supplies ccache, its Gradle init script defaults Android app and
|
|
2865
|
+
library CMake builds to CMAKE_DISABLE_PRECOMPILE_HEADERS=ON. PCH inputs can
|
|
2866
|
+
retain a previous worktree's paths even with upstream timestamp fixes, causing
|
|
2867
|
+
the header and its consuming objects to miss. Compiling ordinary headers
|
|
2868
|
+
instead favors reuse across worktrees at the cost of a slower cold C++ build.
|
|
2869
|
+
This does not edit dependency sources or change iOS builds. Without Stim's
|
|
2870
|
+
ccache setup, auto PCH behavior is unchanged. Explicit optimizations.android.pch
|
|
2871
|
+
on/off applies with any compiler cache selection. A module with an explicit
|
|
2872
|
+
CMAKE_DISABLE_PRECOMPILE_HEADERS argument in its default config, build types,
|
|
2873
|
+
or product flavors keeps that choice; CMake target-level PCH overrides also
|
|
2874
|
+
take precedence. Direct Gradle builds do not receive Stim's init script.
|
|
2875
|
+
|
|
2876
|
+
EXPERIMENTAL ANDROID CAS
|
|
2877
|
+
optimizations.android.compilerCache="cas" with android.casToolchain under the
|
|
2878
|
+
same optimizations object selects a private Apple Clang toolchain manifest
|
|
2879
|
+
on macOS. STIM_ANDROID_CAS_TOOLCHAIN also selects CAS in auto mode. It retains
|
|
2880
|
+
PCH and replaces the ccache setup for that invocation.
|
|
2881
|
+
Compiler results live under $STIM_HOME/android-cas/<toolchain-id>; APK cache
|
|
2882
|
+
keys include that ID. This is a development prototype requiring a compatible
|
|
2883
|
+
linker and NDK copy, not an automatically installed backend. Because the
|
|
2884
|
+
manifest lives outside the repository it can rot: when the setting holds any
|
|
2885
|
+
value that is not an absolute path, or the manifest it names is missing or
|
|
2886
|
+
unreadable, the build warns once naming the setting and the file it came from,
|
|
2887
|
+
then compiles through the cache the selection leaves -- ccache, or none when
|
|
2888
|
+
compilerCache is none. It never refuses. See
|
|
2889
|
+
https://stim.appandflow.com/docs/android-cas for setup, evidence, and limits.
|
|
2890
|
+
Compiler/PCH modes have separate generated directories under each module's
|
|
2891
|
+
.cxx/stim-<profile> (or custom staging root). Switching modes in Stim selects
|
|
2892
|
+
the matching directory; direct Gradle builds keep their own configuration.
|
|
2893
|
+
Generated CAS directories still depend on Stim's environment and adapter paths.
|
|
2894
|
+
|
|
2895
|
+
For older, unprofiled builds, the launcher persists in the project. AGP writes
|
|
2896
|
+
it into each
|
|
2897
|
+
.cxx/**/CMakeCache.txt on the first configure, so a plain \`./gradlew\` in that
|
|
2898
|
+
checkout also compiles through ccache -- and a .cxx configured BEFORE the
|
|
2899
|
+
variables existed can keep compiling without them until it is cleared once.
|
|
2900
|
+
\`stim doctor\` reports stale configurations in this checkout's app and installed
|
|
2901
|
+
native modules. Stop native builds, then run \`stim doctor --fix --platform android\`:
|
|
2902
|
+
it removes only affected ignored, untracked legacy .cxx configurations and
|
|
2903
|
+
reruns the diagnostics. Managed profiles and disabled compiler caches are
|
|
2904
|
+
left alone. A cas selection Stim cannot use resolves to ccache, so that
|
|
2905
|
+
checkout's legacy configurations become eligible for the same repair. A config
|
|
2906
|
+
the repair cannot read repairs nothing. The next build recreates legacy output. It refuses directories
|
|
2907
|
+
outside the checkout and configured custom launchers. Shared ccache entries and
|
|
2908
|
+
source files are preserved. Its cache-lock check cannot detect --no-build-cache,
|
|
2909
|
+
release-swap fallback, or direct Gradle builds; stop all native builds and keep
|
|
2910
|
+
them stopped until repair finishes.
|
|
2911
|
+
Run it before copying the checkout into worktrees.
|
|
2912
|
+
|
|
2913
|
+
THE BUILD CACHE HAS THREE LEVELS
|
|
2914
|
+
1. Stim's own, on this machine: a directory under ~/.stim shared by
|
|
2915
|
+
every worktree, keyed on the @expo/fingerprint hash of the native inputs.
|
|
2916
|
+
Free, instant, offline, and the only level a project without any
|
|
2917
|
+
provider has.
|
|
2918
|
+
2. The project's own cache provider, on ANY project including bare React
|
|
2919
|
+
Native: \`cache.provider\` in the settings, a module implementing the
|
|
2920
|
+
@stim-cli/cache contract (see \`guide settings\`). Consulted only when
|
|
2921
|
+
level one misses, and its hit is stored into level one before install.
|
|
2922
|
+
The same contract serves the Metro transform cache.
|
|
2923
|
+
3. On an EXPO project only, the provider the project ALREADY configured for
|
|
2924
|
+
Expo (\`expo.buildCacheProvider\` -- "eas", or a module of its own).
|
|
2925
|
+
Consulted only when levels one and two miss, bounded so a slow or expired
|
|
2926
|
+
remote cannot stall the loop, and a hit is copied into level one on the
|
|
2927
|
+
way past so the next workspace on this machine gets it for free. After a
|
|
2928
|
+
build, the result is stored locally AND handed to both providers, which
|
|
2929
|
+
run independently. An ABI-targeted Android Debug build skips this Expo
|
|
2930
|
+
tier because its run-options contract cannot distinguish ABIs; levels one
|
|
2931
|
+
and two remain ABI-keyed and active.
|
|
2932
|
+
|
|
2933
|
+
Stim never configures a provider and never suggests changing one: a
|
|
2934
|
+
project without one is a perfectly ordinary local-only project (doctor does
|
|
2935
|
+
not ask for one either -- a provider only serves builds run OUTSIDE Stim).
|
|
2936
|
+
|
|
2937
|
+
A provider that fails to load, times out, or errors produces ONE note per
|
|
2938
|
+
failure class and the run continues on the local cache. \`gc\` reports,
|
|
2939
|
+
trims, and clears local caches only: the provider contract has no delete
|
|
2940
|
+
operation, so no local command can remove data a team or CI system shares.
|
|
2941
|
+
|
|
2942
|
+
A MISS explains itself when it can. When this workspace's previous build
|
|
2943
|
+
stored its fingerprint sources beside the cache entry, the fingerprint line
|
|
2944
|
+
gains " -- N sources changed: <up to three paths>", and the full list
|
|
2945
|
+
(capped at 20 names) lands in the build log as a fingerprint_diff record.
|
|
2946
|
+
|
|
2947
|
+
THE KEY CAN MOVE MID-RUN, and the run says so in two facts rather than two
|
|
2948
|
+
explanations. \`expo prebuild\` and \`pod install\` rewrite fingerprinted
|
|
2949
|
+
files while they work, so the run fingerprints again afterwards:
|
|
2950
|
+
|
|
2951
|
+
fingerprint dcbd8d.. -> 6564e2.. (after prebuild, pod install)
|
|
2952
|
+
cache hit 6564e2.. (post-prebuild/pod install key)
|
|
2953
|
+
|
|
2954
|
+
The first line means the artifact, the \`lastBuild\` record and any remote
|
|
2955
|
+
upload are stored under the SECOND hash -- the one the next run in this tree
|
|
2956
|
+
computes, and therefore the one it looks up. The second line only appears on
|
|
2957
|
+
a tree that was COLD: the first lookup ran on the pre-prebuild hash and
|
|
2958
|
+
could not find an entry another workspace had already stored under the
|
|
2959
|
+
post-prebuild one, so re-resolving under the moved key installs it instead
|
|
2960
|
+
of compiling beside it. No second line means nothing was found there and the
|
|
2961
|
+
run compiles.
|
|
2962
|
+
|
|
2963
|
+
If the iOS fingerprint after prebuild or pod install is unavailable, Stim
|
|
2964
|
+
installs the build but skips local storage and remote uploads. fingerprint
|
|
2965
|
+
and cacheKey are null in the result and lastBuild; the old key is not reused.
|
|
2966
|
+
Android does the same if its post-Gradle fingerprint cannot be computed.
|
|
2967
|
+
These null fields mean unavailable cache information, not an install failure.
|
|
2968
|
+
|
|
2969
|
+
WHAT MAKES THE CACHE ACTUALLY HIT: .FINGERPRINTIGNORE
|
|
2970
|
+
Every entry is keyed on what the tree hashes, so two workspaces share an
|
|
2971
|
+
entry only when they hash alike. A file that changes without changing the
|
|
2972
|
+
BUILD is what breaks that, and it fails silently -- a cache that never hits
|
|
2973
|
+
looks exactly like a cache that is not there.
|
|
2974
|
+
|
|
2975
|
+
Stim ignores two paths a fresh checkout never has and no native build reads:
|
|
2976
|
+
android/local.properties and android/.idea. A project does not repeat those.
|
|
2977
|
+
Everything else is the project's call, including a lockfile whose checksums
|
|
2978
|
+
embed machine paths -- ignoring a path any project might read turns a slow
|
|
2979
|
+
build into a wrong one.
|
|
2980
|
+
|
|
2981
|
+
A linked native library (a \`link:\` or \`file:\` dependency, or a workspace
|
|
2982
|
+
symlink) brings its checkout's .git into a directory the fingerprint hashes
|
|
2983
|
+
whole; Git rewrites that metadata on every commit, checkout, or worktree, so
|
|
2984
|
+
workspaces rarely agree. \`stim doctor\` names the entries to ignore when the
|
|
2985
|
+
native build does not read Git state: the .git path as the fingerprint sees
|
|
2986
|
+
it and its /**/* form, which is what skips a .git directory's contents.
|
|
2987
|
+
Ignore those entries only, never the package.
|
|
2988
|
+
|
|
2989
|
+
\`.fingerprintignore\` at the project root (same syntax as .gitignore) is the
|
|
2990
|
+
answer. Put in it only what genuinely cannot change the native build: a
|
|
2991
|
+
generated report, a local env file, a lockfile whose checksums embed absolute
|
|
2992
|
+
machine paths (\`ios/Podfile.lock\` is the usual one -- pod checksums can bake
|
|
2993
|
+
in a machine path, and \`pod install\` rewrites it on a plain re-install).
|
|
2994
|
+
Never ignore a real native input -- a Podfile, a gradle file, the app config
|
|
2995
|
+
-- to force a hit: that trades a slow build for a wrong one.
|
|
2996
|
+
|
|
2997
|
+
\`stim doctor\` measures this directly rather than reading the file: it
|
|
2998
|
+
fingerprints HEAD in a temporary clean worktree, compares, and reports a
|
|
2999
|
+
mismatch naming the differing sources. Untracked, non-gitignored files under
|
|
3000
|
+
ios/ or android/ count too -- they are hashed like any other source, so a
|
|
3001
|
+
stray file there moves the key on your machine and nowhere else.`
|
|
3002
|
+
},
|
|
3003
|
+
concurrency: {
|
|
3004
|
+
summary: "waiting on another workspace's build, --no-build-cache, concurrency.maxBuilds and maxDevices",
|
|
3005
|
+
body: () => `ONE COMPILE PER FINGERPRINT, ACROSS EVERY WORKSPACE
|
|
3006
|
+
The cache makes the SECOND workspace on a commit free -- but only once the
|
|
3007
|
+
first has finished. Three agents starting within the same minute all miss it,
|
|
3008
|
+
and without this all three compile the same app at once, fighting for the
|
|
3009
|
+
same cores. So when both cache levels miss, the run takes a LOCK on
|
|
3010
|
+
<fingerprint, platform> (a directory under ~/.stim/build-locks). Exactly
|
|
3011
|
+
one workspace compiles; the others print
|
|
3012
|
+
|
|
3013
|
+
build /w/app-412 is already building a3f9b1.. (pid 41233) -- tail ... -- stim guide lifecycle concurrency
|
|
3014
|
+
build waiting on /w/app-412 (pid 41233, 4m elapsed) -- tail ... -- stim guide lifecycle concurrency
|
|
3015
|
+
build waited 12m41s for /w/app-412's build -> installed from cache -- stim guide lifecycle concurrency
|
|
3016
|
+
|
|
3017
|
+
and install the artifact the builder stored. They report cacheHit: "local"
|
|
3018
|
+
plus waitedForBuild: { pid, ms }.
|
|
3019
|
+
|
|
3020
|
+
Native preparation can move the builder's cache key. A released lock with no
|
|
3021
|
+
artifact at the original key does not prove the build failed. The waiter
|
|
3022
|
+
rechecks after its own native preparation; a matching post-preparation cache
|
|
3023
|
+
hit retains waitedForBuild. Artifacts remain stored only under their final
|
|
3024
|
+
fingerprint, never under the old key.
|
|
3025
|
+
|
|
3026
|
+
Nothing can deadlock on it. The lock records the holder's process IDENTITY,
|
|
3027
|
+
so a builder that crashes, is killed, or whose build simply fails frees it:
|
|
3028
|
+
the waiters see a released lock with no artifact, and one of them takes over
|
|
3029
|
+
and builds. A recycled pid reads as a gone builder, and a builder busy in a
|
|
3030
|
+
long synchronous tool call reads as live -- age is never a reason to take a
|
|
3031
|
+
lock. The one state that cannot be decided (a truncated claim, an identity
|
|
3032
|
+
token that does not decode) is STIM_CLAIM_REFUSED: Stim names the claim and
|
|
3033
|
+
the command that removes it rather than guessing. A process whose own identity
|
|
3034
|
+
cannot be captured takes no lock and no slot, and refuses with
|
|
3035
|
+
STIM_CLAIM_UNAVAILABLE rather than building unprotected. The
|
|
3036
|
+
other waiters keep waiting for that holder. All replacement builders share
|
|
3037
|
+
one ~90-minute deadline, including lock acquisition between waits; reaching
|
|
3038
|
+
it returns STIM_BUILD_WAIT_TIMEOUT naming the current holder and lock.
|
|
3039
|
+
|
|
3040
|
+
--no-build-cache looks nothing up -- not the local cache, not either
|
|
3041
|
+
provider -- and takes no lock and never waits, because it asked for a compile
|
|
3042
|
+
of its own. It still STORES the result, over the entry it was told not to
|
|
3043
|
+
trust, and still uploads it. Use it when a cached artifact is suspect; the
|
|
3044
|
+
--json payload reports cacheSkipped: true so a caller can tell that run apart
|
|
3045
|
+
from a plain miss.
|
|
3046
|
+
|
|
3047
|
+
OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
|
|
3048
|
+
Stim imposes NO limits of its own: unset is exactly the behaviour above --
|
|
3049
|
+
every build compiles, every device boots. When a machine cannot host as many
|
|
3050
|
+
parallel builds or booted simulators as there are agents, two MACHINE-level
|
|
3051
|
+
caps rein it in. They live under a top-level \`concurrency\` key in
|
|
3052
|
+
~/.stim/config.json (not per-project -- the resource being shared is the
|
|
3053
|
+
machine's), and STIM_MAX_BUILDS / STIM_MAX_DEVICES override the file.
|
|
3054
|
+
Absent, 0, or any non-positive value means NO enforcement.
|
|
3055
|
+
|
|
3056
|
+
concurrency.maxBuilds how many builds COMPILE at once. It is a semaphore
|
|
3057
|
+
of N slots (~/.stim/build-slots). A run takes a
|
|
3058
|
+
slot AFTER the single-flight lock -- a workspace
|
|
3059
|
+
waiting to install another's identical artifact
|
|
3060
|
+
never burns a slot -- so it caps distinct compiles,
|
|
3061
|
+
not waiters. A full slate WAITS (this is batch work),
|
|
3062
|
+
printing the same kind of progress line the build
|
|
3063
|
+
lock does, and a dead builder frees its slot within
|
|
3064
|
+
a poll (process identity, like the lock).
|
|
3065
|
+
|
|
3066
|
+
concurrency.maxDevices how many Stim-owned devices are BOOTED at once. Checked
|
|
3067
|
+
at device time, before a sim is created or booted.
|
|
3068
|
+
At the cap, a NEW device is REFUSED with
|
|
3069
|
+
STIM_AT_CAPACITY (interactive-shaped: it does not
|
|
3070
|
+
queue). A workspace whose own device is already
|
|
3071
|
+
booted is never refused.
|
|
3072
|
+
See \`guide errors STIM_AT_CAPACITY\`.
|
|
3073
|
+
|
|
3074
|
+
\`stim doctor\` prints one note echoing the caps and the current live count,
|
|
3075
|
+
but ONLY when a cap is set. \`stim gc\` reports stale build slots the way it
|
|
3076
|
+
reports stale build locks, and \`gc --delete\` clears them. There is no
|
|
3077
|
+
\`stim config\` command: set these by editing ~/.stim/config.json or via
|
|
3078
|
+
the two env vars (see \`guide settings\`).`
|
|
3079
|
+
},
|
|
3080
|
+
options: {
|
|
3081
|
+
summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
|
|
3082
|
+
body: () => `THE OPTION SURFACE, IN FULL
|
|
3083
|
+
start --json --wait <seconds> --remote --reset-cache
|
|
3084
|
+
ios --json --no-metro-check --no-build-cache --scheme <name> --configuration <name> --device-type <name> --runtime <version> --device [udid] --wait <seconds> --no-wait --remote <proxy|eas>
|
|
3085
|
+
android --json --no-metro-check --no-build-cache --variant <name> --system-image <id> --device [serial] --wait <seconds> --no-wait --remote <proxy|eas>
|
|
3086
|
+
reload [ios|android] --json
|
|
3087
|
+
device lock <ios|android> [id] --for <duration> --wait <seconds> --json;
|
|
3088
|
+
unlock [ios|android] --json
|
|
3089
|
+
logs --source --level --since --grep --tail --follow --errors --json
|
|
3090
|
+
stop --json
|
|
3091
|
+
status --json (already machine-wide)
|
|
3092
|
+
stats --json (this project and machine-wide)
|
|
3093
|
+
doctor --json --fix --platform <ios|android>
|
|
3094
|
+
(--platform keeps shared checks and filters native findings)
|
|
3095
|
+
gc --delete --older-than <days> --cache <name|all>
|
|
3096
|
+
worktree warm --refresh; remove [path] --force
|
|
3097
|
+
|
|
3098
|
+
That is the whole surface today, and it is deliberately small. It can grow
|
|
3099
|
+
when a flag is genuinely the best answer -- but project-specific knowledge
|
|
3100
|
+
(release builds, variants, device targets) belongs in a script the repo owns,
|
|
3101
|
+
not in a flag here.
|
|
3102
|
+
|
|
3103
|
+
\`stim worktree warm\` takes one flag, \`--refresh\`. Run it anywhere inside
|
|
3104
|
+
the current linked worktree to copy missing ignored entries from its source
|
|
3105
|
+
checkout, regardless of either branch's HEAD. Both roots must be registered
|
|
3106
|
+
worktrees of the same Git repository; the source checkout must be available.
|
|
3107
|
+
Running it in the source checkout refuses.
|
|
3108
|
+
|
|
3109
|
+
\`--refresh\` WRITES TO THE SOURCE CHECKOUT before the copy, which is why it
|
|
3110
|
+
is opt-in: it fetches, fast-forwards whatever branch is checked out there to
|
|
3111
|
+
its \`@{upstream}\`, and installs what the new commits moved. It refuses a
|
|
3112
|
+
source checkout it cannot move -- uncommitted changes to tracked files or a
|
|
3113
|
+
rebase or merge in progress (STIM_MAIN_DIRTY), a detached HEAD
|
|
3114
|
+
(STIM_MAIN_DETACHED), or a branch that is both ahead and behind
|
|
3115
|
+
(STIM_MAIN_DIVERGED) -- and each refusal names the git line that clears it.
|
|
3116
|
+
Untracked files are not a reason to refuse. A branch with no upstream is
|
|
3117
|
+
left where it is. A fetch that fails is a fact, not a refusal: the run
|
|
3118
|
+
continues on the local state. It never switches branches, never merges, and
|
|
3119
|
+
never resets; fast-forwarding a feature branch is safe, so it only WARNS
|
|
3120
|
+
that the copy will carry that branch's dependencies. The default branch it
|
|
3121
|
+
compares against is \`worktree.defaultBranch\` in the repository-root
|
|
3122
|
+
.stim.json, else \`git symbolic-ref --short refs/remotes/origin/HEAD\`;
|
|
3123
|
+
when neither answers it says so and makes no warning.
|
|
3124
|
+
|
|
3125
|
+
Dependencies install where the lockfile is, which in a monorepo is the
|
|
3126
|
+
repository root, when the lockfile moved or nothing is installed. Pods run
|
|
3127
|
+
for the app the command was invoked from, and only that app, when its
|
|
3128
|
+
ios/Podfile.lock moved or ios/Pods/Manifest.lock does not match it. Each
|
|
3129
|
+
completed or skipped deps and pods step names its source directory and
|
|
3130
|
+
reason. A failed install refuses with STIM_DEPS_FAILED and nothing is copied.
|
|
3131
|
+
|
|
3132
|
+
An install that did not finish is remembered, and a PLAIN warm reads that
|
|
3133
|
+
before it copies. The refresh records the completed install of the lockfile it
|
|
3134
|
+
read under ~/.stim/warm-installs; a plain warm reads that record after it
|
|
3135
|
+
takes its claim, and refuses with STIM_DEPS_INCOMPLETE, copying nothing, when
|
|
3136
|
+
the unfinished install is of the lockfile as it stands NOW -- otherwise the
|
|
3137
|
+
copy carries a partial node_modules and exits 0, and only the refresh's own
|
|
3138
|
+
terminal ever said the install failed. A record of a different lockfile does
|
|
3139
|
+
not block a copy, and a repository with no record copies exactly as it did
|
|
3140
|
+
before the record existed. The remedy is \`--refresh\`, which reinstalls rather
|
|
3141
|
+
than skipping on the same record, so a refresh killed mid-install -- which
|
|
3142
|
+
writes no failure anywhere -- is recovered the same way a failed one is.
|
|
3143
|
+
|
|
3144
|
+
One lock per repository serialises this, whether or not you pass the flag:
|
|
3145
|
+
\`--refresh\` holds it exclusively, and every copy holds it shared, so a copy
|
|
3146
|
+
can never read a node_modules a refresh is rewriting. Two plain warms of the
|
|
3147
|
+
same repository run together. The lock is keyed on the repository root, so
|
|
3148
|
+
two apps of one monorepo share it. It is the same ownership claim the build
|
|
3149
|
+
locks take: the holder's process IDENTITY decides, so a holder that dies frees
|
|
3150
|
+
it, a recycled pid cannot keep it, and a refresh whose install runs in a
|
|
3151
|
+
spawned process group holds it while that group lives. A wait prints the
|
|
3152
|
+
holder every 30s and gives up with STIM_LOCK_TIMEOUT; a claim Stim cannot
|
|
3153
|
+
resolve refuses with STIM_CLAIM_REFUSED and names the removal rather than
|
|
3154
|
+
waiting on it or removing it.
|
|
3155
|
+
A plain warm that cannot record a claim at all -- an unwritable STIM_HOME, a
|
|
3156
|
+
STIM_HOME on a read-only mount or under a dangling symlink, a STIM_HOME that
|
|
3157
|
+
is a file, or no \`unique-pid\` build for this platform
|
|
3158
|
+
(STIM_CLAIM_UNAVAILABLE) -- says so in one dim line and copies without it,
|
|
3159
|
+
exactly as it did before the lock existed, because a copy only reads.
|
|
3160
|
+
\`--refresh\` refuses instead, because it writes. Before that unsynchronised
|
|
3161
|
+
copy starts, warm still READS the claim set, which needs no claim of its own:
|
|
3162
|
+
if a refresh holds this repository, or a claim in it cannot be resolved, the
|
|
3163
|
+
copy refuses rather than reading a tree that is being rewritten. That read is
|
|
3164
|
+
not a lock, so a refresh that starts after it is not covered; it closes the
|
|
3165
|
+
window in which one is already installing. Closing the rest needs a claim,
|
|
3166
|
+
which is the one thing that state cannot record, so the residual is
|
|
3167
|
+
documented rather than fixed (appandflow/stim#696), as is the
|
|
3168
|
+
\`acquired (waited 0s ...)\` a contended-but-fast wait prints.
|
|
3169
|
+
|
|
3170
|
+
Wait for warm to exit successfully (exit code 0) before running start,
|
|
3171
|
+
ios, android, or a dependency install in that worktree. If a shell tool
|
|
3172
|
+
yields a running session or job ID, poll or wait for completion; empty
|
|
3173
|
+
stdout or a returned job ID does not mean the copy has finished. Concurrent
|
|
3174
|
+
writes to the destination are unsafe: existing entries are checked before
|
|
3175
|
+
copying, not during it. Do not edit files, run another warm, or start any
|
|
3176
|
+
other writer in that worktree until warm finishes. Concurrent files can be
|
|
3177
|
+
overwritten or removed.
|
|
3178
|
+
|
|
3179
|
+
Warm copies installed dependencies, Pods, native output, and other ignored
|
|
3180
|
+
paths eligible under the source checkout's Git ignore rules, including .env
|
|
3181
|
+
and local configuration. The source's nonempty
|
|
3182
|
+
.worktreeexclude replaces its resolved worktree.exclude setting. Nested
|
|
3183
|
+
registered worktrees, .DS_Store, .DerivedData, .idea, and
|
|
3184
|
+
android/build/generated/autolinking caches are excluded, including inside
|
|
3185
|
+
newly copied directories. Gradle regenerates autolinking
|
|
3186
|
+
for the destination checkout on its next build. Warm also skips paths
|
|
3187
|
+
overlapping a nested destination worktree or below a symlink ancestor.
|
|
3188
|
+
Tracked .idea settings come from Git and stay untouched by warm.
|
|
3189
|
+
|
|
3190
|
+
Other generated state stays eligible: .gradle, .cxx, *.tsbuildinfo, build
|
|
3191
|
+
directories, and embedded JavaScript need project-specific decisions about
|
|
3192
|
+
regeneration. Native intermediates can record the source checkout's paths;
|
|
3193
|
+
warm does not relocate them. Excluding the whole .expo directory can drop
|
|
3194
|
+
generated TypeScript inputs.
|
|
3195
|
+
|
|
3196
|
+
To choose exclusions, run this in the source checkout's repository root:
|
|
3197
|
+
|
|
3198
|
+
git ls-files --others --ignored --exclude-standard --directory --no-empty-directory
|
|
3199
|
+
|
|
3200
|
+
Patterns match those entries with the trailing / removed. They do not prune
|
|
3201
|
+
children of a whole ignored directory: if Git lists android/app/src/main/assets/,
|
|
3202
|
+
excluding its bundle.jsbundle child has no effect. Exclude the assets entry
|
|
3203
|
+
only when the project regenerates everything inside it.
|
|
3204
|
+
|
|
3205
|
+
Warm copies directly into the destination, without intermediate staging.
|
|
3206
|
+
Keep the source checkout and the linked worktree on the same volume to
|
|
3207
|
+
retain copy-on-write cloning where supported. Cross-volume copies require
|
|
3208
|
+
full file data; doctor reports that cost. STIM_TMPDIR and machine tempDir do
|
|
3209
|
+
not affect warming.
|
|
3210
|
+
|
|
3211
|
+
Existing entries, including dangling symlinks, stay untouched. An existing
|
|
3212
|
+
ignored directory such as node_modules is skipped WHOLE; missing children are not
|
|
3213
|
+
filled in. Warm does not copy tracked changes, switch branches, or build,
|
|
3214
|
+
and it installs dependencies only under \`--refresh\`, in the source checkout. stdout stays empty; stderr reports copied, kept,
|
|
3215
|
+
and failed entry counts. A copy failure exits 1 and reports incomplete;
|
|
3216
|
+
files copied before a failure remain. Inspect the named failed entry
|
|
3217
|
+
before retrying: a partially copied directory will be kept on the retry.
|
|
3218
|
+
A completed copy is not proof that dependencies match this branch. Follow
|
|
3219
|
+
any lockfile remedies before building, and install missing dependencies
|
|
3220
|
+
with the project's package manager when the source has none to copy.
|
|
3221
|
+
|
|
3222
|
+
\`android --variant <name>\` selects the gradle variant to assemble and
|
|
3223
|
+
install on a project with product flavors -- \`--variant productionDebug\`
|
|
3224
|
+
runs \`assembleProductionDebug\`, finds the APK in apk/production/debug/ and
|
|
3225
|
+
keys the build cache on the variant. It overrides the android.variant
|
|
3226
|
+
setting (see \`guide settings\`), which is the app-level default; unset,
|
|
3227
|
+
the plain \`assembleDebug\` flow is unchanged. The --json payload's
|
|
3228
|
+
\`variant\` field reports what was built (null for the default).
|
|
3229
|
+
When neither is set and android/app/build.gradle declares more than one
|
|
3230
|
+
product flavor, \`android\` refuses BEFORE gradle runs and names the debug
|
|
3231
|
+
variants to choose from, because \`assembleDebug\` would build every flavor
|
|
3232
|
+
and leave nothing to pick from. That parse is best-effort: flavors built
|
|
3233
|
+
from a variable, a loop, or an applied script are not detected, and such a
|
|
3234
|
+
project builds as before.
|
|
3235
|
+
|
|
3236
|
+
\`ios --device-type <name>\` and \`ios --runtime <version>\` choose the
|
|
3237
|
+
MODEL and the iOS version of the simulator this workspace owns --
|
|
3238
|
+
\`--device-type "iPad Pro 13-inch (M4)" --runtime 18.5\` is how a ticket that
|
|
3239
|
+
says "happens on iPad on iOS 18.5" gets reproduced without writing a
|
|
3240
|
+
\`.stim.json\`. \`android --system-image <id>\` is the Android half, taking
|
|
3241
|
+
the sdkmanager package id
|
|
3242
|
+
("system-images;android-36;google_apis;arm64-v8a"). Each overrides its
|
|
3243
|
+
setting (ios.deviceType, ios.runtime, android.systemImage) for that one
|
|
3244
|
+
invocation, exactly as \`--configuration\` overrides ios.configuration.
|
|
3245
|
+
|
|
3246
|
+
A name that is not INSTALLED on this machine refuses with STIM_BAD_ARG
|
|
3247
|
+
before anything is created, and the message lists the installed names, so a
|
|
3248
|
+
wrong guess is one command, not a created simulator. A blank value is the
|
|
3249
|
+
same refusal.
|
|
3250
|
+
|
|
3251
|
+
What counts as installed for \`--device-type\` is what an installed RUNTIME
|
|
3252
|
+
can create, not what \`xcrun simctl list devicetypes\` prints: that table
|
|
3253
|
+
also names watchOS, tvOS and visionOS models, and older iPhones no current
|
|
3254
|
+
runtime supports, none of which \`simctl create\` would accept. So the
|
|
3255
|
+
refusal lists the models the installed runtimes offer -- narrowed to the one
|
|
3256
|
+
runtime when \`--runtime\` also resolved, which is what catches a pair like
|
|
3257
|
+
\`--device-type "iPhone 8" --runtime 26.5\` that each half would pass alone.
|
|
3258
|
+
\`--runtime\` takes a version (\`26.5\`) or a runtime's full name
|
|
3259
|
+
(\`iOS 26.5\`), exactly; no prefix or suffix matches.
|
|
3260
|
+
|
|
3261
|
+
These flags describe a device that does not exist yet. When this workspace
|
|
3262
|
+
ALREADY owns a simulator and \`--device-type\` names a different model,
|
|
3263
|
+
Stim refuses rather than silently booting the wrong one: reap the current
|
|
3264
|
+
sim with \`stim worktree remove\` (or \`stim gc --delete\`), then run
|
|
3265
|
+
\`stim ios\` again to create the requested one. \`--runtime\` and
|
|
3266
|
+
\`--system-image\` apply at creation only, so an existing device keeps the
|
|
3267
|
+
version it was made with. The --json payload reports what was actually
|
|
3268
|
+
used: \`deviceType\` and \`runtime\` on iOS, \`systemImage\` on Android,
|
|
3269
|
+
read from the device itself, so a settings-driven run reports them too.`
|
|
3270
|
+
},
|
|
3271
|
+
devices: {
|
|
3272
|
+
summary: "ios --device and android --device on a phone: what the run skips, signing, the LAN wiring, the collector",
|
|
3273
|
+
body: () => ` \`android --device [serial]\` installs and launches on a physical device
|
|
3274
|
+
connected to this machine instead of this workspace's owned emulator. With
|
|
3275
|
+
no serial it takes the first device it can lease (\`guide lifecycle lease\`).
|
|
3276
|
+
It cannot be combined with --remote.
|
|
3277
|
+
|
|
3278
|
+
A \`--device\` run LEASES the device from just after the build until it
|
|
3279
|
+
exits, so a second workspace cannot install over it mid-run
|
|
3280
|
+
(\`guide lifecycle lease\`).
|
|
3281
|
+
|
|
3282
|
+
The build, the fingerprint, the build cache and the Metro port gate are
|
|
3283
|
+
unchanged. What is skipped is everything that manages an owned device:
|
|
3284
|
+
no capacity check, no AVD creation, no boot wait, and no owned-device
|
|
3285
|
+
registry entry. The app is pointed at
|
|
3286
|
+
localhost:<port>, which the adb reverse serves, instead of the emulator's
|
|
3287
|
+
10.0.2.2. Stim never creates, boots, shuts down, or deletes hardware.
|
|
3288
|
+
|
|
3289
|
+
\`ios --device [udid]\` selects a connected iPhone, the same way
|
|
3290
|
+
\`android --device\` selects a connected phone: with no UDID it takes the
|
|
3291
|
+
first device it can lease (\`guide lifecycle lease\`), and an iPhone
|
|
3292
|
+
that is unpaired or has Developer Mode off is refused with the fix. It
|
|
3293
|
+
cannot be combined with --remote, and it never creates, boots, or deletes
|
|
3294
|
+
hardware -- there is no capacity check, no simulator creation, no boot wait,
|
|
3295
|
+
and no owned-device registry entry. Like
|
|
3296
|
+
\`android --device\`, it leases the phone for the run
|
|
3297
|
+
(\`guide lifecycle lease\`).
|
|
3298
|
+
|
|
3299
|
+
\`stop\` releases this workspace's leases and stops its log collectors.
|
|
3300
|
+
On a physical iPhone that also closes the app, because its collector owns
|
|
3301
|
+
the devicectl launch session. \`gc --delete\` removes expired lease files;
|
|
3302
|
+
neither command shuts down the phone or uninstalls the app.
|
|
3303
|
+
|
|
3304
|
+
A device build is LOCAL-TIER ONLY. Its cache key is
|
|
3305
|
+
\`<fingerprint>-<configuration>-device\`, so a device app can never collide
|
|
3306
|
+
with the simulator one, and neither the build-cache provider nor the Expo
|
|
3307
|
+
remote cache is read or written on a \`--device\` run: every entry they hold
|
|
3308
|
+
is keyed for the simulator, so consulting them would either install a
|
|
3309
|
+
simulator slice on a phone or publish an iphoneos app under a key simulator
|
|
3310
|
+
builds resolve.
|
|
3311
|
+
|
|
3312
|
+
THE BUILD is the \`iphoneos\` slice for the selected phone -- \`-sdk
|
|
3313
|
+
iphoneos\`, the project's own signing settings, no signing flags on the argv.
|
|
3314
|
+
It is installed with \`devicectl device install app\` and launched with
|
|
3315
|
+
\`devicectl device process launch\`. Every device install is SIGNED, Debug
|
|
3316
|
+
included, so the signing gate runs before it: the app's own
|
|
3317
|
+
embedded.mobileprovision must be unexpired and must name this phone, and when
|
|
3318
|
+
Stim modifies the bundle the identity that profile names must be in this
|
|
3319
|
+
machine's keychain (see \`guide errors STIM_NO_PROFILE\`). A gate refusal on
|
|
3320
|
+
a CACHED app falls back to a full build; on a freshly built one it exits on
|
|
3321
|
+
its own code, because building again would produce the same app.
|
|
3322
|
+
|
|
3323
|
+
DEBUG REACHES METRO OVER THE LAN, because a phone shares no loopback with
|
|
3324
|
+
the host and USB carries no reverse forward. Stim picks a non-internal IPv4
|
|
3325
|
+
address (en0 first, RN's own order from react-native-xcode.sh), gates it as
|
|
3326
|
+
this workspace's Metro, and then wires the app to it: an expo-dev-client app
|
|
3327
|
+
through the deep link, passed to devicectl as \`--payload-url\` and followed
|
|
3328
|
+
by \`-- -EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0\`,
|
|
3329
|
+
which is how a phone gets what a simulator gets from a defaults write, and a
|
|
3330
|
+
bare app by writing \`<addr>:<port>\` into the app bundle's ip.txt --
|
|
3331
|
+
RCTBundleURLProvider's own mechanism, which honours a colon-bearing value
|
|
3332
|
+
verbatim and never consults the compiled RCT_METRO_PORT. Stim never sets
|
|
3333
|
+
that define: it would put the reserved port into a compiled input and fork
|
|
3334
|
+
the device cache per workspace.
|
|
3335
|
+
ip.txt is a sealed resource, so Stim writes it on a COPY of the artifact and
|
|
3336
|
+
re-seals that copy with \`codesign\`. THE ORDER IS STORE, THEN COPY, THEN
|
|
3337
|
+
MUTATE: the cache entry stays the pristine, shareable artifact, and the
|
|
3338
|
+
per-run address lives only in the copy that is installed and then deleted.
|
|
3339
|
+
|
|
3340
|
+
A RELEASE device run builds fresh every time for now. A cached Release app
|
|
3341
|
+
carries its BUILDER's JS, and the device JS swap (which has to re-seal what
|
|
3342
|
+
it injects) lands with phase 6 of appandflow/stim#178, so the cache hit is
|
|
3343
|
+
refused rather than installed with someone else's JavaScript.
|
|
3344
|
+
|
|
3345
|
+
THE DEVICE LOG COLLECTOR IS THE LAUNCH. \`devicectl\` connects an app's
|
|
3346
|
+
streams only when it is the process that starts the app, so the collector
|
|
3347
|
+
runs \`devicectl device process launch --console --terminate-existing\`
|
|
3348
|
+
itself rather than attaching after the fact the way the simulator collector
|
|
3349
|
+
does. The run then reads the app's pid from the phone's own process list
|
|
3350
|
+
(\`devicectl device info processes\`), because \`--console\` blocks until
|
|
3351
|
+
the app exits and its \`--json-output\` is written only then. That device pid
|
|
3352
|
+
is also what proves a RELEASE launch: a device pid means nothing to the host,
|
|
3353
|
+
so nothing on the host is ever signalled with it. Otherwise the collector is
|
|
3354
|
+
the same process as every other collector -- one per platform per workspace,
|
|
3355
|
+
titled with its --root, killed and replaced on the next \`ios\` run whose pid
|
|
3356
|
+
still proves it is this workspace's, and reaped by \`stop\`. One difference in
|
|
3357
|
+
the ordering: a device run stops the PREVIOUS collector before it installs,
|
|
3358
|
+
not while starting its own, because an upgrade install terminates the running
|
|
3359
|
+
app -- which would end that collector's devicectl non-zero and record a
|
|
3360
|
+
failure for a normal reinstall. Unplugging the phone ends devicectl, which
|
|
3361
|
+
ends the collector: it removes its own registration and exits. A separately
|
|
3362
|
+
held \`device lock\` lease survives collector exit until released or expired;
|
|
3363
|
+
\`gc --delete\` can remove its expired lease file. Whether it closes with collector_stopped or
|
|
3364
|
+
collector_failed follows devicectl's exit code, which no one has watched a
|
|
3365
|
+
cable-pull produce yet. See \`guide logs\` for what the device stream can
|
|
3366
|
+
and cannot carry, and appandflow/stim#179.
|
|
3367
|
+
|
|
3368
|
+
THE APP RUNS FOR AS LONG AS THE COLLECTOR DOES. Because the collector is the
|
|
3369
|
+
launch, the app is attached to it: \`stop\` (and any other end of that
|
|
3370
|
+
collector -- a crash, the host sleeping, the cable coming out) closes the app
|
|
3371
|
+
on the phone. It stays INSTALLED. Collector exit removes the collector's
|
|
3372
|
+
registration, not a separately held device lease; \`stop\` also releases
|
|
3373
|
+
this workspace's leases. See \`guide cleanup collector\`.
|
|
3374
|
+
|
|
3375
|
+
THERE IS NO INSTALL SKIP ON A PHONE. The simulator path skips the install
|
|
3376
|
+
when the device already holds the same bundle byte for byte, which it proves
|
|
3377
|
+
by hashing the installed container; there is no cheap equivalent through
|
|
3378
|
+
devicectl, so a device run always installs. It is an upgrade install: the
|
|
3379
|
+
app's data, and the Local Network permission the phone granted it, survive.`
|
|
3380
|
+
},
|
|
3381
|
+
lease: {
|
|
3382
|
+
summary: "run-scoped leases, --wait and --no-wait, device lock and unlock, which phone an id-less --device picks",
|
|
3383
|
+
body: () => `THE DEVICE LEASE ON A \`--device\` RUN
|
|
3384
|
+
A physical device is shared, so a \`--device\` run takes a lease on it. The
|
|
3385
|
+
lease step sits AFTER the build (a build touches no device) and before the
|
|
3386
|
+
install, and the run releases what it took when the command exits: on
|
|
3387
|
+
success, on a failure, on an exception, and on a Ctrl-C or a SIGTERM, which
|
|
3388
|
+
it catches to give the device back before exiting 130/143. Only SIGKILL
|
|
3389
|
+
escapes that, and then the lease expires on its own. Before each device step
|
|
3390
|
+
-- install, launch, the log collector, verification -- the run raises the
|
|
3391
|
+
expiry to now plus the larger of 60 seconds and that step's own upper bound,
|
|
3392
|
+
because a child process is synchronous and no timer can tick during an
|
|
3393
|
+
install. A run killed with SIGKILL therefore leaves the device leased for at
|
|
3394
|
+
most the current step's bound, never less than 60 seconds.
|
|
3395
|
+
|
|
3396
|
+
If nobody holds the device, the run takes a lease of its own and gives it
|
|
3397
|
+
back at exit. If another workspace holds it, the run WAITS: \`--wait
|
|
3398
|
+
<seconds>\` (default 60) polls every 2 seconds, prints a waiting line to
|
|
3399
|
+
stderr at once and then every 30 seconds with the holder, the device and the
|
|
3400
|
+
holder's expiry, and refuses with STIM_DEVICE_BUSY when it runs out. It
|
|
3401
|
+
keeps waiting past the holder's own expiry, because the holder can release
|
|
3402
|
+
early. \`--wait 0\` refuses at once. \`--no-wait\` changes only that case: the
|
|
3403
|
+
run proceeds with NO lease and prints one warning naming the holder and its
|
|
3404
|
+
expiry, plus what the install costs: the same app id means it TERMINATES the
|
|
3405
|
+
holder's running app, a different one means the launch only backgrounds it,
|
|
3406
|
+
and when Stim cannot read the holder's app id it says so rather than
|
|
3407
|
+
guessing. A free device is leased as usual under \`--no-wait\`. The two flags
|
|
3408
|
+
together are STIM_BAD_ARG, and so is either one without \`--device\`, because
|
|
3409
|
+
an owned simulator or emulator has no contention.
|
|
3410
|
+
|
|
3411
|
+
A successful \`--device\` run reports \`lease: { kind, expiresAt }\` in its
|
|
3412
|
+
\`--json\`; a run that proceeded without one, or lost one after the install,
|
|
3413
|
+
reports \`lease: null\`. \`stim status\` lists every lease file on the
|
|
3414
|
+
machine, and \`stop\` releases the ones this workspace holds.
|
|
3415
|
+
|
|
3416
|
+
HOLDING A DEVICE ACROSS RUNS
|
|
3417
|
+
A run-scoped lease dies with the command, which is not enough for a
|
|
3418
|
+
device-tool session: the next workspace's \`ios --device\` would install over
|
|
3419
|
+
the app you are driving. \`stim device lock\` grants a DECLARED lease that
|
|
3420
|
+
outlives the run:
|
|
3421
|
+
|
|
3422
|
+
stim device lock ios --for 10m # or: android; add a UDID/serial to name one
|
|
3423
|
+
stim ios --device # builds, installs, launches; raises the lease
|
|
3424
|
+
... device-tool work on the phone ...
|
|
3425
|
+
stim device unlock # give it back; or let it expire
|
|
3426
|
+
|
|
3427
|
+
\`--for\` takes a whole number of seconds or minutes, 10s to 30m, and
|
|
3428
|
+
defaults to 5m; anything else is STIM_BAD_ARG. \`--wait <seconds>\`
|
|
3429
|
+
(default 60, \`0\` refuses at once) is the same wait a run does. Both
|
|
3430
|
+
commands need a project and refuse outside one with STIM_NO_PROJECT, and
|
|
3431
|
+
\`lock\` runs the same resolver \`--device\` does, so an unpaired phone or
|
|
3432
|
+
one with Developer Mode off is refused with that resolver's own remedy
|
|
3433
|
+
before any lease is written.
|
|
3434
|
+
|
|
3435
|
+
Locking a device this workspace already holds SETS the expiry to now plus
|
|
3436
|
+
\`--for\`, which can shorten it. Locking a different device of the same
|
|
3437
|
+
platform releases the first one: a workspace holds at most one lease per
|
|
3438
|
+
platform. Nothing else moves an expiry -- not the app running afterwards, not
|
|
3439
|
+
device-tool work, not \`status\`. Only \`lock\` and a run's own steps do.
|
|
3440
|
+
|
|
3441
|
+
\`stim device unlock\` releases every lease this workspace holds, or only
|
|
3442
|
+
the platform named. Releasing nothing is not an error: it says so on stderr,
|
|
3443
|
+
and \`--json\` prints an empty list. It releases by holder, so it still
|
|
3444
|
+
works when the workspace directory was recreated and the token is gone.
|
|
3445
|
+
|
|
3446
|
+
With no id, \`lock\` and a \`--device\` run pick from the POOL of connected
|
|
3447
|
+
devices, so two phones on one machine no longer refuse.
|
|
3448
|
+
|
|
3449
|
+
THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
|
|
3450
|
+
Candidates are the connected devices the resolver already accepts: on iOS,
|
|
3451
|
+
wired, paired, with Developer Mode on; on Android, every serial adb reports
|
|
3452
|
+
in the \`device\` state that is not an emulator, TCP serials included. Then,
|
|
3453
|
+
in order:
|
|
3454
|
+
|
|
3455
|
+
1. the device this workspace already leases, when it is among them;
|
|
3456
|
+
2. otherwise the first one not leased -- or leased and EXPIRED -- in
|
|
3457
|
+
case-folded id order.
|
|
3458
|
+
|
|
3459
|
+
Ids are sorted on, never names: adb has no name without one \`getprop\` per
|
|
3460
|
+
serial, and models repeat.
|
|
3461
|
+
|
|
3462
|
+
A device this workspace leases that is NOT connected refuses with
|
|
3463
|
+
STIM_NO_DEVICE naming it, rather than quietly moving to another phone. Naming
|
|
3464
|
+
a different one with \`--device <id>\` refuses the same way, because a
|
|
3465
|
+
workspace holds at most one lease per platform: \`stim device unlock\` first.
|
|
3466
|
+
|
|
3467
|
+
Candidates with none free is the wait: under \`--wait <seconds>\` the poll
|
|
3468
|
+
re-LISTS devices, so a phone plugged in mid-wait is picked up as well as one
|
|
3469
|
+
released mid-wait. When the wait runs out, STIM_DEVICE_BUSY names every
|
|
3470
|
+
holder and its expiry. No candidate at all is the existing STIM_NO_DEVICE,
|
|
3471
|
+
with the resolver's own message. \`--no-wait\` takes the first candidate
|
|
3472
|
+
anyway and proceeds with no lease, as it does for one named device.
|
|
3473
|
+
|
|
3474
|
+
The chosen device is on the phase line and in \`--json\` (\`udid\` or
|
|
3475
|
+
\`serial\`, plus \`deviceName\`), so an agent can hand the same id to its
|
|
3476
|
+
device tool.`
|
|
3477
|
+
},
|
|
3478
|
+
release: {
|
|
3479
|
+
summary: "Release configurations and ...Release variants: Metro skipped, process-proven launch, the JS swap",
|
|
3480
|
+
body: () => ` A VARIANT WHOSE NAME ENDS IN "Release" IS A RELEASE BUILD (\`release\`,
|
|
3481
|
+
\`productionRelease\`), and that is the whole opt-in -- there is no second
|
|
3482
|
+
flag. It is the Android half of \`ios --configuration Release\` and behaves
|
|
3483
|
+
the same way: AGP's bundle task embeds the JS, so Metro is skipped ENTIRELY
|
|
3484
|
+
(no gate, no \`adb reverse\`, no debug_http_host, no dev-client deep link --
|
|
3485
|
+
a plain \`am start\` of the launcher activity), the payload says
|
|
3486
|
+
\`metroPort: null\`, and \`launched\` is proven by the app PROCESS being
|
|
3487
|
+
alive on the device rather than by a bundle fetch. Device logs are still
|
|
3488
|
+
collected, so \`logs --errors\` answers "does it repro in release/Hermes
|
|
3489
|
+
bytecode".
|
|
3490
|
+
|
|
3491
|
+
On a release CACHE HIT the cached APK carries its BUILDER's baked-in JS, so
|
|
3492
|
+
it is never installed as-is. Stim copies it aside, regenerates this
|
|
3493
|
+
workspace's bundle with the project's own tools (\`expo export:embed\` /
|
|
3494
|
+
\`react-native bundle\`, then the project's own hermesc when
|
|
3495
|
+
\`hermesEnabled\` is not false in android/gradle.properties), re-packs it
|
|
3496
|
+
into the copy with plain zip surgery (stored, not deflated -- the runtime
|
|
3497
|
+
mmaps it), then zipaligns and re-signs with apksigner. The keystore defaults
|
|
3498
|
+
to android/app/debug.keystore with the standard password; android.keystore /
|
|
3499
|
+
android.keystorePassword override it (see \`guide settings\`). The cache
|
|
3500
|
+
entry itself is never modified.
|
|
3501
|
+
|
|
3502
|
+
Before re-packing, THE ASSET GATE compares CONTENT HASHES of the assets
|
|
3503
|
+
React Native emits: what this workspace just emitted under --assets-dest
|
|
3504
|
+
against a manifest of what the cached build emitted, recorded as
|
|
3505
|
+
assets-manifest.json inside the cache entry at build time. Same producer on
|
|
3506
|
+
both sides, so the comparison is exact -- an added, a removed OR A REPLACED
|
|
3507
|
+
asset (a different image under an unchanged filename) all mean NO SWAP, and
|
|
3508
|
+
the run falls back to a full gradle build with a note naming an example. An
|
|
3509
|
+
Android drawable is not just a file in the zip -- it has a row in
|
|
3510
|
+
resources.arsc only AAPT can write -- so an APK cannot be made to carry an
|
|
3511
|
+
asset it was not built with, and Stim will not install one whose JS
|
|
3512
|
+
references an asset it lacks. The APK's own res/ table is never read: a
|
|
3513
|
+
release build shortens every resource path (AGP's
|
|
3514
|
+
optimizeReleaseResources), so those entries are \`res/-B.png\`, not the names
|
|
3515
|
+
anything emitted.
|
|
3516
|
+
|
|
3517
|
+
AN ENTRY WITH NO MANIFEST NEVER SWAPS. One stored before asset tracking, or
|
|
3518
|
+
downloaded from an Expo build-cache provider, has nothing to compare
|
|
3519
|
+
against, so the run says so and builds fresh -- and that build REPLACES the
|
|
3520
|
+
entry, manifest included, so the next run on the same fingerprint swaps
|
|
3521
|
+
normally. The same replacement happens after any gate refusal or swap
|
|
3522
|
+
failure, which is what stops a bad entry from refusing every run forever.
|
|
3523
|
+
|
|
3524
|
+
Local re-signing also
|
|
3525
|
+
means an APK signed by CI cannot be updated over: on
|
|
3526
|
+
INSTALL_FAILED_UPDATE_INCOMPATIBLE (or a version downgrade) a release run
|
|
3527
|
+
uninstalls the package once and retries, printing a note -- the app's data
|
|
3528
|
+
goes with it, which is why only release runs do this.
|
|
3529
|
+
|
|
3530
|
+
Local installs only, onto an owned emulator or, with
|
|
3531
|
+
\`android --device\`, a connected physical device. Store signing and
|
|
3532
|
+
distribution stay out of scope.
|
|
3533
|
+
|
|
3534
|
+
\`ios --configuration <name>\` selects the Xcode configuration --
|
|
3535
|
+
\`--configuration Release\` builds a SIMULATOR Release app with the JS
|
|
3536
|
+
bundle embedded. It overrides the ios.configuration setting (the app-level
|
|
3537
|
+
default); unset, the Debug flow is unchanged. A non-Debug configuration
|
|
3538
|
+
skips Metro ENTIRELY: no gate, no port wiring, no dev-client deep link (a
|
|
3539
|
+
plain \`simctl launch\`), and the payload says \`metroPort: null\` --
|
|
3540
|
+
\`launched\` is verified by the app PROCESS staying alive, not by a bundle
|
|
3541
|
+
fetch. The build cache keys on the configuration
|
|
3542
|
+
(\`<fingerprint>-release-sim\`), and because a cached Release .app carries
|
|
3543
|
+
its builder's baked-in JS, a cache hit regenerates THIS workspace's bundle
|
|
3544
|
+
(the project's own \`expo export:embed\` / \`react-native bundle\`, plus
|
|
3545
|
+
its own hermesc when Hermes is enabled) into a copy of the artifact,
|
|
3546
|
+
re-signs it and installs that; any swap failure falls back to a full build
|
|
3547
|
+
rather than ever installing stale JS. Device logs are still collected, so
|
|
3548
|
+
\`logs --errors\` answers "does it repro in release/Hermes bytecode".
|
|
3549
|
+
A run with no \`--device\` installs on the simulator only. \`ios --device\`
|
|
3550
|
+
builds the \`iphoneos\` slice for a cabled iPhone and keys its cache
|
|
3551
|
+
\`-device\` instead of \`-sim\`, but does not install it yet. Archives,
|
|
3552
|
+
\`.ipa\` export, store signing and distribution stay out of scope.`
|
|
3553
|
+
},
|
|
3554
|
+
simslim: {
|
|
3555
|
+
summary: "recommended SimSlim profiles and recovery from host memory pressure",
|
|
3556
|
+
body: () => `SIMSLIM FOR PARALLEL IOS WORK
|
|
3557
|
+
SimSlim is recommended as an optional way to reduce simulator background
|
|
3558
|
+
services and memory use, especially with several workspaces. Review which
|
|
3559
|
+
services your app and tests need; a slim profile can disable those features.
|
|
3560
|
+
It does not guarantee that a memory stall or crash will be fixed.
|
|
3561
|
+
|
|
3562
|
+
Install SimSlim once on each Mac:
|
|
3563
|
+
|
|
3564
|
+
brew install mobai-app/tap/simslim
|
|
3565
|
+
|
|
3566
|
+
Review the categories and create a profile in an interactive terminal:
|
|
3567
|
+
|
|
3568
|
+
simslim profiles
|
|
3569
|
+
mkdir -p .simslim
|
|
3570
|
+
simslim profile .simslim/dev.json
|
|
3571
|
+
|
|
3572
|
+
Selected categories in the wizard stay enabled. The wizard writes the
|
|
3573
|
+
profile without applying it. Review and commit it, then select it in .stim.json:
|
|
3574
|
+
|
|
3575
|
+
{ "ios": { "simslimProfile": ".simslim/dev.json" } }
|
|
3576
|
+
|
|
3577
|
+
SimSlim 0.8 requires iOS 18.5 or newer. On each local \`stim ios\`,
|
|
3578
|
+
Stim reconciles that profile on the owned simulator before the app build.
|
|
3579
|
+
The first change can update services and reboot the simulator. A matching
|
|
3580
|
+
profile is a fast no-op on later launches. The settings persist across normal
|
|
3581
|
+
shutdowns and reboots. Removing the setting restores stock services when
|
|
3582
|
+
Stim applied the profile. Stim never changes an unowned or remote simulator.
|
|
3583
|
+
Each SimSlim operation has a 12-minute outer deadline, including discovery.
|
|
3584
|
+
This cap also applies when SimSlim's own timeout is increased. On timeout,
|
|
3585
|
+
Ctrl-C, or SIGTERM, Stim attempts to stop only its verified process group, then waits up to
|
|
3586
|
+
10 seconds to confirm termination before returning or exiting.
|
|
3587
|
+
An unconfirmed process group keeps its claim and blocks another reconciliation;
|
|
3588
|
+
inspect the named processes before removing the exact claim in the error.
|
|
3589
|
+
The simulator's managed settings record is retained, so retrying reconciles an
|
|
3590
|
+
interrupted apply or restore. Stim does not assume partial changes rolled back.
|
|
3591
|
+
Doctor recommends this setup but never installs SimSlim or applies a profile.
|
|
3592
|
+
Profile schema and service tradeoffs: https://github.com/MobAI-App/simslim
|
|
3593
|
+
|
|
3594
|
+
HOST MEMORY PRESSURE AND STALLED SIMULATORS
|
|
3595
|
+
Booted and a working screenshot do not prove that simulator processes can
|
|
3596
|
+
start. Stim checks a bounded process spawn before install and bounds local
|
|
3597
|
+
simulator launch operations. Simulator discovery waits up to 30 seconds,
|
|
3598
|
+
boot (including the initial boot request) up to 10 minutes, and app installation
|
|
3599
|
+
up to 5 minutes. Process termination confirmation can take another 10 seconds;
|
|
3600
|
+
a final boot-state query can take 30 seconds. Opening the Simulator app after
|
|
3601
|
+
boot is best-effort and takes at most 5 seconds. A timeout is not proof of an
|
|
3602
|
+
app crash or OOM.
|
|
3603
|
+
Doctor and failure diagnostics report macOS memory pressure when available;
|
|
3604
|
+
a failed query remains unknown. Existing swap or low free RAM alone is not
|
|
3605
|
+
enough to diagnose pressure.
|
|
3606
|
+
|
|
3607
|
+
If pressure is elevated, free host memory before retrying. Use \`stim stop\`
|
|
3608
|
+
only in workspaces you own and have finished using; ask before closing other
|
|
3609
|
+
agents' simulators or heavy apps. Rebooting a simulator under the same pressure
|
|
3610
|
+
can repeat the stall. Consider fewer concurrent builds/devices (guide lifecycle
|
|
3611
|
+
concurrency) and a reviewed SimSlim profile for future runs.`
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
},
|
|
3615
|
+
cleanup: {
|
|
3616
|
+
summary: "Where simulators come from, and how they get reclaimed",
|
|
3617
|
+
preamble: () => `CLEANUP AND DISK
|
|
3618
|
+
|
|
3619
|
+
WHAT RECLAIMS AN OWNED DEVICE
|
|
3620
|
+
stim worktree remove parks eligible owned simulators and emulators
|
|
3621
|
+
(\`guide lifecycle pool\`); deletes them when
|
|
3622
|
+
parking is disabled or their setup cannot be verified
|
|
3623
|
+
stim gc --delete sweeps stim-* devices no project references, and
|
|
3624
|
+
clears verified parked simulators and emulators
|
|
3625
|
+
stim gc --delete --older-than <days>
|
|
3626
|
+
also reaps the device of a project nothing has
|
|
3627
|
+
touched in that long, even though the project is
|
|
3628
|
+
still on disk
|
|
3629
|
+
|
|
3630
|
+
Those are the only two commands that delete. \`stim stop\` shuts a device
|
|
3631
|
+
DOWN and leaves it assigned, which is what makes returning to a branch cost a
|
|
3632
|
+
boot rather than a create, a provision and a reinstall.
|
|
3633
|
+
|
|
3634
|
+
Neither touches $STIM_HOME/stats.json: \`gc\` never reports or trims the run
|
|
3635
|
+
counters \`stats\` prints, and there is no reset flag. Delete that one file to
|
|
3636
|
+
start the counters over. A file this version cannot read -- unparseable, or
|
|
3637
|
+
written by a newer Stim -- costs one dim line on stderr and is otherwise left
|
|
3638
|
+
alone; only the next \`ios\` or \`android\` run moves an unparseable one aside
|
|
3639
|
+
to stats.json.corrupt-<unix ms> and starts a new one.`,
|
|
3640
|
+
sections: {
|
|
3641
|
+
gc: {
|
|
3642
|
+
summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
|
|
3643
|
+
body: () => `LINKED WORKTREES
|
|
3644
|
+
\`stim worktree remove\` works with any linked worktree, warmed or not.
|
|
3645
|
+
Git registration identifies the worktree; a Stim registry entry is not
|
|
3646
|
+
required. The command reclaims any owned resources it finds, checks for
|
|
3647
|
+
uncommitted and unpushed work, and removes the linked checkout. Git-created
|
|
3648
|
+
branches stay. A branch with an existing Stim ownership record is deleted
|
|
3649
|
+
only when it has no unique commits.
|
|
3650
|
+
|
|
3651
|
+
ON THE SOURCE CHECKOUT
|
|
3652
|
+
git cannot remove a repository's main working tree, and deleting the source
|
|
3653
|
+
checkout is not what anyone meant -- so there, and only there,
|
|
3654
|
+
\`worktree remove\` reclaims the ENVIRONMENT and nothing else: the owned
|
|
3655
|
+
devices are parked or deleted, the Metro port freed, the registry entries
|
|
3656
|
+
(including nested monorepo app dirs) dropped, and the global workspace
|
|
3657
|
+
directory deleted. The tree itself is never touched, which is also why the
|
|
3658
|
+
dirty-tree and unpushed guards do not apply on that path.
|
|
3659
|
+
It ends with:
|
|
3660
|
+
Reclaimed the environment; the working tree stays (it is the source checkout).
|
|
3661
|
+
A registered project directory that is not a git repo at all gets the same
|
|
3662
|
+
environment reclaim -- there is nothing else remove could mean there.
|
|
3663
|
+
|
|
3664
|
+
The delete paths and \`stop\` do not check simulator occupancy. An explicit
|
|
3665
|
+
\`stim stop\` shuts down this workspace's Stim-owned simulator, including a
|
|
3666
|
+
simulator used by a UI-test runner. It never shuts down an unowned simulator.
|
|
3667
|
+
|
|
3668
|
+
If a delete fails, the device's config record is KEPT and the command reports
|
|
3669
|
+
it. A record is what makes the device findable again, so it outlives a failed
|
|
3670
|
+
teardown rather than turning it into an orphan.
|
|
3671
|
+
|
|
3672
|
+
ANDROID DATA WITHOUT A REGISTRATION
|
|
3673
|
+
\`gc\` also reports stim-*.avd directories whose .ini registration is
|
|
3674
|
+
gone. \`gc --delete\` rechecks the directory, emulator process locks, and
|
|
3675
|
+
current workspace and pool references before removing that data. A registration
|
|
3676
|
+
under any name that points at the directory protects it. User AVDs, symlinks
|
|
3677
|
+
and unverifiable storage stay. The no-config and scoped-STIM_HOME
|
|
3678
|
+
sweep guards apply to these directories too.
|
|
3679
|
+
A partial avdmanager deletion is a failure even if the tool exits successfully.
|
|
3680
|
+
The owning workspace or pool record stays for a retry. If removing orphan
|
|
3681
|
+
data fails, its remaining directory is reported as stim-gc-<id>.avd on the
|
|
3682
|
+
next sweep.
|
|
3683
|
+
|
|
3684
|
+
BUILD LOCKS
|
|
3685
|
+
\`gc\` also reports the single-flight build locks (above): the ones whose
|
|
3686
|
+
builder is no longer running are debris a reboot or a kill left behind, and
|
|
3687
|
+
\`gc --delete\` clears them. A lock whose builder IS running is a build in
|
|
3688
|
+
progress -- it is named in the report and touched by nothing, because
|
|
3689
|
+
removing it would put a second workspace on the same compile.
|
|
3690
|
+
|
|
3691
|
+
DEVICE LEASES
|
|
3692
|
+
A workspace can hold a timed lease on a physical device. The lease is one
|
|
3693
|
+
file under ~/.stim/device-locks, and it expires on its own. \`gc\` reports
|
|
3694
|
+
the lease files whose expiry has passed; \`gc --delete\` removes those
|
|
3695
|
+
files, re-reading each one under its own lock first, so a lease renewed in
|
|
3696
|
+
the meantime survives. Two kinds are reported and KEPT: a file that does
|
|
3697
|
+
not parse, which no run may take the device around, and an unexpired lease
|
|
3698
|
+
whose holder directory is gone. \`stim status\` lists every lease file with
|
|
3699
|
+
its holder and expiry, including holders no config knows. \`stop\` and
|
|
3700
|
+
\`worktree remove\` release the leases of the workspace they act on, and
|
|
3701
|
+
nothing else deletes a lease file: never remove another workspace's.
|
|
3702
|
+
|
|
3703
|
+
A device leaks when a project is abandoned WITHOUT either delete path -- the
|
|
3704
|
+
sim survives with nothing pointing at it. \`stim gc\` (no flag, writes
|
|
3705
|
+
nothing, always safe) reports those; \`gc --delete\` reaps them, and in the same
|
|
3706
|
+
run drops the dead config ENTRIES those projects left behind and frees their
|
|
3707
|
+
Metro ports.
|
|
3708
|
+
|
|
3709
|
+
REMOTE EAS SESSIONS
|
|
3710
|
+
Plain \`stim gc\` is a dry run. \`gc --delete\` can stop active stim-* EAS
|
|
3711
|
+
sessions after workspace state is missing. The stop needs verified
|
|
3712
|
+
project, name, platform, and status ownership. The same run also cleans the
|
|
3713
|
+
local state that it can prove is stale.
|
|
3714
|
+
|
|
3715
|
+
A fixed ownership record and lock live under ~/.stim/machine/eas,
|
|
3716
|
+
independent of STIM_HOME. Unclaimed sessions are never stopped.
|
|
3717
|
+
Missing config.json does not authorize cleanup.
|
|
3718
|
+
The exact recorded workspace state path must prove that the session ID is
|
|
3719
|
+
absent.
|
|
3720
|
+
If claim removal fails after a verified stop, the session is stopped, but the
|
|
3721
|
+
workspace record is kept for reconciliation.
|
|
3722
|
+
|
|
3723
|
+
If a registered root is missing or unreadable, the EAS sweep fails closed and
|
|
3724
|
+
leaves the remote EAS session running. Independent local cleanup continues
|
|
3725
|
+
for entries it proves stale.
|
|
3726
|
+
|
|
3727
|
+
THE MIRROR IMAGE: A STALE DEVICE RECORD
|
|
3728
|
+
A device deleted out from under a LIVE project (by hand, or by Xcode) leaves
|
|
3729
|
+
the opposite problem: the record points at a sim that is not on the machine,
|
|
3730
|
+
and \`stim status\` warns about it on every run. \`gc\` reports these under
|
|
3731
|
+
"Stale device records", and \`gc --delete\` clears the RECORD -- only the
|
|
3732
|
+
record. There is no device left to shut down or delete, so nothing is issued
|
|
3733
|
+
at simctl or avdmanager, and the project keeps its entry, its label and its
|
|
3734
|
+
Metro port. The next \`ios\` / \`android\` creates a fresh owned device.
|
|
3735
|
+
|
|
3736
|
+
THE ONE CASE GC WILL NOT REAP
|
|
3737
|
+
If the config is gone entirely (deleted ~/.stim, or a throwaway
|
|
3738
|
+
STIM_HOME), gc cannot tell your stale devices from another config's LIVE
|
|
3739
|
+
ones, so it refuses to delete anything. It still NAMES the stim-* devices
|
|
3740
|
+
it found, so you can judge. Delete them yourself:
|
|
3741
|
+
xcrun simctl delete <udid>
|
|
3742
|
+
avdmanager delete avd -n <name>`
|
|
3743
|
+
},
|
|
3744
|
+
collector: {
|
|
3745
|
+
summary: "log collector reaping: an unproven collector pid, and why the app on a phone closed",
|
|
3746
|
+
body: () => `WHAT ELSE STOP REAPS
|
|
3747
|
+
The device-log collectors (\`simctl log stream\` / \`adb logcat\`) that
|
|
3748
|
+
\`ios\` / \`android\` attach after launch. They are recorded in
|
|
3749
|
+
the global workspace state.json, and nothing outside this workspace can name them,
|
|
3750
|
+
so \`stop\` is what stands between a teardown and a log stream that outlives
|
|
3751
|
+
the device it was reading. A fresh \`ios\` / \`android\` run also kills the
|
|
3752
|
+
previous collector for that platform before starting its own.
|
|
3753
|
+
|
|
3754
|
+
A PHYSICAL IPHONE'S COLLECTOR IS THE SAME PROCESS with one difference: on
|
|
3755
|
+
hardware the collector IS the launch. \`devicectl\` connects an app's
|
|
3756
|
+
streams only when it is the process that starts the app, so the collector
|
|
3757
|
+
runs \`devicectl device process launch --console\` itself rather than
|
|
3758
|
+
attaching after the fact. It registers under the same \`ios\` key, carries
|
|
3759
|
+
the same --root in its title, is proven and replaced by the same pid rules,
|
|
3760
|
+
and is reaped by the same \`stop\`.
|
|
3761
|
+
|
|
3762
|
+
THE APP'S LIFETIME IS BOUND TO THAT COLLECTOR, and this is the one place a
|
|
3763
|
+
phone behaves worse than a simulator. \`devicectl device process launch
|
|
3764
|
+
--console\` keeps the app attached to the launching process, so anything that
|
|
3765
|
+
ends the collector ends the APP ON THE PHONE: \`stop\`, \`gc --delete\`,
|
|
3766
|
+
\`worktree remove\`, a fresh \`ios --device\` run stopping its predecessor,
|
|
3767
|
+
a crash, the host sleeping, or the cable coming out. Measured: SIGTERM to the
|
|
3768
|
+
collector alone terminates the app. The phone has no owned-device registry
|
|
3769
|
+
entry. \`stop\` closes the app and releases this workspace's leases.
|
|
3770
|
+
Nothing is uninstalled, and the next \`ios --device\` starts it again.
|
|
3771
|
+
|
|
3772
|
+
Unplugging the phone ends devicectl, which ends the collector: it unregisters
|
|
3773
|
+
itself and exits either way. A separately held \`device lock\` lease survives
|
|
3774
|
+
collector exit until released or expired; \`gc --delete\` can remove its
|
|
3775
|
+
expired lease file.
|
|
3776
|
+
WHICH record it writes on the way out depends on devicectl's exit code, and
|
|
3777
|
+
that code is unverified until someone pulls a cable: a zero exit is
|
|
3778
|
+
collector_stopped, a non-zero one is collector_failed, because on hardware
|
|
3779
|
+
a non-zero devicectl exit is the only evidence a launch or console failed.
|
|
3780
|
+
See \`guide logs\` for what it can and cannot carry.
|
|
3781
|
+
|
|
3782
|
+
Before signalling a recorded collector pid, \`stop\`, \`gc --delete\`,
|
|
3783
|
+
\`worktree remove\`, and a fresh \`ios\` / \`android\` run each read that
|
|
3784
|
+
persisted process identity and require it to match the exact process
|
|
3785
|
+
registered for this workspace and platform. A pid that cannot be proven is
|
|
3786
|
+
reported and left alone: the
|
|
3787
|
+
kernel reuses pids, and an unreaped record is a smaller problem than a
|
|
3788
|
+
signal delivered to someone else's process. A fresh \`ios\` / \`android\`
|
|
3789
|
+
run starts its replacement anyway, leaving the unproven pid to clear on its
|
|
3790
|
+
own. A collector started by an older Stim has no process identity token, so
|
|
3791
|
+
it reports as unverified until its record clears -- which happens when its
|
|
3792
|
+
own device's log stream ends and it unregisters itself, or when the next
|
|
3793
|
+
\`ios\` / \`android\` run overwrites the record with its own, whichever
|
|
3794
|
+
comes first; the old process itself keeps running until it exits on its own.
|
|
3795
|
+
|
|
3796
|
+
A different exact OS start identity proves PID reuse: the recorded collector
|
|
3797
|
+
is gone, and the unrelated process is never signalled. A missing, malformed,
|
|
3798
|
+
or unreadable identity leaves the record unverified and kept for a retry.
|
|
3799
|
+
Wall-clock timestamps and command names are not ownership proof.`
|
|
3800
|
+
},
|
|
3801
|
+
disk: {
|
|
3802
|
+
summary: "disk usage, AVD and build-log sizes, the data partition, trimming the shared caches",
|
|
3803
|
+
body: () => `DISK
|
|
3804
|
+
Logs, state, pidfiles and Xcode DerivedData are under the global workspace
|
|
3805
|
+
directory, and \`worktree remove\` reclaims them. Gradle retains its normal
|
|
3806
|
+
project build directories while sharing task outputs through its build cache.
|
|
3807
|
+
|
|
3808
|
+
Android AVDs normally live under ~/.android/avd, and a booted owned AVD can
|
|
3809
|
+
use several GB. \`worktree remove\` deletes the workspace's owned AVD; plain
|
|
3810
|
+
\`stop\` only shuts it down for reuse. Stim uses Android's default Quick Boot
|
|
3811
|
+
unless displayless Linux requires software rendering, where snapshots are
|
|
3812
|
+
disabled. The first boot and a boot after the emulator, system image, or AVD
|
|
3813
|
+
settings change are cold, while later supported boots load the one automatic
|
|
3814
|
+
snapshot saved on exit. \`stop\` waits for the emulator process and, when
|
|
3815
|
+
enabled, the snapshot save to finish.
|
|
3816
|
+
New owned AVDs default to an 8 GiB data partition, though project settings can
|
|
3817
|
+
change it. When enabled, Quick Boot keeps one automatic snapshot, and \`worktree remove\`
|
|
3818
|
+
deletes the whole AVD.
|
|
3819
|
+
\`gc\` prints the on-disk size beside an orphaned or stale owned Android AVD
|
|
3820
|
+
when its content directory can be read.
|
|
3821
|
+
|
|
3822
|
+
So are the logs, and one of them is not small: build-ios.ndjson /
|
|
3823
|
+
build-android.ndjson hold the whole xcodebuild or gradle transcript at debug
|
|
3824
|
+
level, which for a cold build is tens of megabytes (74 MB measured on one
|
|
3825
|
+
first iOS build of a real app). They are worth that -- a build that fails at
|
|
3826
|
+
minute nine is unreadable any other way -- and they are per workspace, not
|
|
3827
|
+
global, so \`worktree remove\` reclaims them along with everything else in
|
|
3828
|
+
the global workspace directory. Each build starts its transcript file over, so the log
|
|
3829
|
+
holds one run and a workspace you keep building in does not accumulate them.
|
|
3830
|
+
|
|
3831
|
+
Simulators are large and live in the CoreSimulator device set, not in your
|
|
3832
|
+
project. If the disk is filling up, Stim's own devices are usually not the
|
|
3833
|
+
bulk of it -- Apple's default simulators and old runtimes are. Useful:
|
|
3834
|
+
xcrun simctl delete unavailable # sims for runtimes you removed
|
|
3835
|
+
xcrun simctl list devices # see everything
|
|
3836
|
+
stim gc # report dead entries, orphans, caches
|
|
3837
|
+
Xcode recreates default simulators on demand, so deleting them is safe.
|
|
3838
|
+
|
|
3839
|
+
New owned Android AVDs use an 8 GiB data partition by default. This leaves room
|
|
3840
|
+
for repeated app installs while capping userdata growth below the 10 GiB
|
|
3841
|
+
setting measured on the selected API 36 profile. Set
|
|
3842
|
+
\`android.dataPartitionSizeGb\` to a whole number from 6 through 16384 when a
|
|
3843
|
+
project needs another size. Android userdata grows but does not shrink, so the
|
|
3844
|
+
setting applies only to a newly created AVD; recreate the environment to adopt
|
|
3845
|
+
a changed value.
|
|
3846
|
+
|
|
3847
|
+
SHARED BUILD CACHES
|
|
3848
|
+
The caches that make a second workspace fast are alive by design and never
|
|
3849
|
+
included in a plain \`gc --delete\`. Every \`gc\` run reports them anyway,
|
|
3850
|
+
each row tagged (registered) or (detected), with its size:
|
|
3851
|
+
stim gc # report, caches included
|
|
3852
|
+
stim gc --delete --older-than 30 # trim entries nothing has used
|
|
3853
|
+
stim gc --delete --cache all # empty them whole, index-backed ones
|
|
3854
|
+
# (the Xcode CAS) included
|
|
3855
|
+
$STIM_HOME/ccache (default ~/.stim/ccache) holds the Android C++ objects
|
|
3856
|
+
\`stim android\` compiles through ccache. ccache keeps it under CCACHE_MAXSIZE
|
|
3857
|
+
on its own, so \`gc\` reports its size and leaves it alone; --older-than
|
|
3858
|
+
skips it, and \`--cache all\` empties it whole like the Xcode CAS. That
|
|
3859
|
+
bound is Stim's: it sets CCACHE_MAXSIZE on the Gradle run, which wins over
|
|
3860
|
+
a max_size written into the cache directory's own ccache.conf.
|
|
3861
|
+
|
|
3862
|
+
The Gradle build cache under GRADLE_USER_HOME (default ~/.gradle) is
|
|
3863
|
+
report-only because every Gradle build shares it. Stim reports its size
|
|
3864
|
+
but never prunes or empties it, including with --older-than or --cache all.
|
|
3865
|
+
Trim rather than empty. Emptying costs the next build in every project the
|
|
3866
|
+
time the cache was saving.`
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
},
|
|
3870
|
+
settings: {
|
|
3871
|
+
summary: "Settings Stim reads, and where they can live",
|
|
3872
|
+
body: () => `SETTINGS
|
|
3873
|
+
|
|
3874
|
+
There is no \`stim config\` command. Settings are JSON files, edited by
|
|
3875
|
+
hand or committed; command-line selectors override their matching settings.
|
|
3876
|
+
|
|
3877
|
+
Resolution order, first match wins:
|
|
3878
|
+
1. project layer ~/.stim/config.json, under this project's entry
|
|
3879
|
+
2. repo layer ~/.stim/config.json, under this repo's git common dir
|
|
3880
|
+
3. committed .stim.json beside the app's package.json
|
|
3881
|
+
4. machine defaults ~/.stim/config.json, top-level optimizations only
|
|
3882
|
+
5. Stim default
|
|
3883
|
+
|
|
3884
|
+
The committed file is plain JSON and travels with the app. Each monorepo app
|
|
3885
|
+
reads its own file, never an ancestor's runtime settings. A single-app repository
|
|
3886
|
+
still uses its root file. Existing machine project/repository overrides keep
|
|
3887
|
+
their precedence. Move root runtime settings into each relevant app when
|
|
3888
|
+
upgrading; relative profile/config/provider paths resolve from the app directory.
|
|
3889
|
+
|
|
3890
|
+
Worktree copying is repository-wide: worktree warm reads worktree.exclude and
|
|
3891
|
+
worktree.defaultBranch from the source checkout's root .stim.json, not from
|
|
3892
|
+
individual apps. Keep those rules at the repository root; runtime files and
|
|
3893
|
+
worktree-copy policy are separate scopes.
|
|
3894
|
+
|
|
3895
|
+
An app's .stim.json can contain:
|
|
3896
|
+
|
|
3897
|
+
{
|
|
3898
|
+
"ios": {
|
|
3899
|
+
"deviceType": "iPhone 17 Pro",
|
|
3900
|
+
"runtime": "26.2",
|
|
3901
|
+
"simslimProfile": ".simslim/dev.json"
|
|
3902
|
+
},
|
|
3903
|
+
"android": { "variant": "productionDebug" },
|
|
3904
|
+
"caches": ["~/.myapp-metro-cache"]
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
KEYS STIM READS
|
|
3908
|
+
ios.deviceType e.g. "iPhone 17 Pro" -- the simulator model this
|
|
3909
|
+
workspace's owned sim is created as, spelled exactly as
|
|
3910
|
+
\`xcrun simctl list devicetypes\` names it, and one an
|
|
3911
|
+
installed runtime can create. The \`--device-type\`
|
|
3912
|
+
flag overrides this per invocation. A name no installed
|
|
3913
|
+
runtime offers is STIM_BAD_ARG and the creatable names
|
|
3914
|
+
are printed
|
|
3915
|
+
ios.runtime e.g. "26.2" -- the iOS runtime that sim is created on,
|
|
3916
|
+
as a version ("26.2") or a runtime's full name
|
|
3917
|
+
("iOS 26.2"); nothing else matches. The \`--runtime\`
|
|
3918
|
+
flag overrides this per invocation, and an uninstalled
|
|
3919
|
+
version refuses the same way
|
|
3920
|
+
ios.configuration e.g. "Release" -- the Xcode configuration to build
|
|
3921
|
+
(simulator only). Committing
|
|
3922
|
+
{ "ios": { "configuration": "Release" } } makes every
|
|
3923
|
+
\`stim ios\` in the app a release-shaped build:
|
|
3924
|
+
embedded JS, no Metro, cache keyed -release-sim, and
|
|
3925
|
+
a JS-bundle swap on cache hits. The \`--configuration\`
|
|
3926
|
+
flag overrides this per invocation. Unset means Debug.
|
|
3927
|
+
ios.remote "proxy" or "eas" to use that remote backend, the same
|
|
3928
|
+
as passing \`--remote proxy\` or \`--remote eas\`. The
|
|
3929
|
+
build still runs here; only the device is elsewhere.
|
|
3930
|
+
ios.simslimProfile a SimSlim JSON profile under the app directory,
|
|
3931
|
+
at most 64 KiB. Install the
|
|
3932
|
+
external tool once with
|
|
3933
|
+
\`brew install mobai-app/tap/simslim\`. SimSlim requires
|
|
3934
|
+
iOS 18.5 or newer in SimSlim 0.8. Recommended for parallel
|
|
3935
|
+
iOS work after reviewing service tradeoffs; see
|
|
3936
|
+
\`stim guide lifecycle simslim\`. Each local \`stim ios\`
|
|
3937
|
+
reconciles the profile on its Stim-owned simulator.
|
|
3938
|
+
The first change can reboot it; a matching profile is a
|
|
3939
|
+
fast no-op. Removing the setting restores stock services
|
|
3940
|
+
when Stim applied the profile. Absolute paths, root or
|
|
3941
|
+
symlink escapes, and missing files are refused before
|
|
3942
|
+
simulator creation. Remote and unowned simulators are
|
|
3943
|
+
never changed.
|
|
3944
|
+
ios.signingIdentity e.g. "Apple Development: Jane (TEAMID5678)" -- the
|
|
3945
|
+
keychain identity to re-seal a \`--device\` build with,
|
|
3946
|
+
overriding the one Stim derives from the artifact's own
|
|
3947
|
+
embedded.mobileprovision. Discovery is zero-config, so
|
|
3948
|
+
this exists only for the case discovery cannot cover.
|
|
3949
|
+
The name must be one \`security find-identity -v -p
|
|
3950
|
+
codesigning\` prints.
|
|
3951
|
+
ios.signingIdentitySha1
|
|
3952
|
+
the 40-character hex SHA-1 hash printed beside that
|
|
3953
|
+
name. Set it when two certificates share one common
|
|
3954
|
+
name: Stim is non-interactive, so it refuses an
|
|
3955
|
+
ambiguous identity rather than picking one. It wins
|
|
3956
|
+
over ios.signingIdentity.
|
|
3957
|
+
ios.lanHost e.g. "192.168.1.42" -- the address a phone uses to
|
|
3958
|
+
reach this workspace's Metro on an \`ios --device\`
|
|
3959
|
+
Debug run, pinning the interface on a multi-NIC Mac
|
|
3960
|
+
whose en0 is not the one the phone shares. A bare
|
|
3961
|
+
address or hostname ONLY: never a scheme, a port, or a
|
|
3962
|
+
URL, because the channels that carry it to the phone
|
|
3963
|
+
(the dev-client deep link and the bundle's ip.txt)
|
|
3964
|
+
compose the URL themselves. Unset means Stim orders the
|
|
3965
|
+
host's non-internal IPv4 interfaces en0 first, then the
|
|
3966
|
+
remaining en* by index -- react-native-xcode.sh's own
|
|
3967
|
+
heuristic, so Stim and a plain Xcode run pick the same
|
|
3968
|
+
interface.
|
|
3969
|
+
android.systemImage e.g. "system-images;android-36;google_apis;arm64-v8a"
|
|
3970
|
+
-- the sdkmanager package id the owned AVD is created
|
|
3971
|
+
from. The \`--system-image\` flag overrides this per
|
|
3972
|
+
invocation, and an id this SDK has not installed is
|
|
3973
|
+
STIM_BAD_ARG with the installed ids printed.
|
|
3974
|
+
New AVDs use the Pixel 6 hardware profile (1080x2400,
|
|
3975
|
+
420 dpi). Existing AVDs keep their display settings;
|
|
3976
|
+
parked AVDs from the old generic profile are not adopted.
|
|
3977
|
+
android.dataPartitionSizeGb
|
|
3978
|
+
whole GiB for a newly created owned AVD's data
|
|
3979
|
+
partition. Defaults to 8; accepts 6 through 16384.
|
|
3980
|
+
Existing AVDs are never resized because Android
|
|
3981
|
+
userdata grows but does not shrink. Recreate the
|
|
3982
|
+
environment to adopt a changed value.
|
|
3983
|
+
android.avdConfigFile
|
|
3984
|
+
path under the app directory to a flat native
|
|
3985
|
+
key=value INI fragment,
|
|
3986
|
+
at most 64 KiB. Stim parses it and
|
|
3987
|
+
merges supported values into avdmanager's generated
|
|
3988
|
+
config.ini before first boot; it is never used as a
|
|
3989
|
+
replacement file. Absolute paths, app-directory or
|
|
3990
|
+
symlink escapes, malformed or duplicate lines, and
|
|
3991
|
+
unsupported keys are refused before AVD creation.
|
|
3992
|
+
android.avdConfig flat object of the same native keys. It merges key by
|
|
3993
|
+
key across settings layers and overrides the selected
|
|
3994
|
+
avdConfigFile fragment. Boolean values accept true,
|
|
3995
|
+
false, "yes", or "no"; numbers and enums are checked.
|
|
3996
|
+
Supported keys and values:
|
|
3997
|
+
${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).join("\n")}
|
|
3998
|
+
Identity, architecture, host path, storage, image,
|
|
3999
|
+
kernel, camera, snapshot, boot-lifecycle, and unknown
|
|
4000
|
+
keys are protected. The emulator may normalize a valid
|
|
4001
|
+
value. These overrides apply only to a newly created
|
|
4002
|
+
AVD; existing and recovered AVDs are never rewritten.
|
|
4003
|
+
On displayless Linux, Stim launches with
|
|
4004
|
+
-gpu swiftshader_indirect -noaudio; those arguments
|
|
4005
|
+
override hw.gpu.enabled, hw.gpu.mode, hw.audioInput,
|
|
4006
|
+
and hw.audioOutput for that headless launch.
|
|
4007
|
+
android.variant e.g. "productionDebug" -- the gradle variant to
|
|
4008
|
+
assemble and install on a project with product
|
|
4009
|
+
flavors. A repo like tlon-mobile with
|
|
4010
|
+
flavorDimensions "profile" and production/preview
|
|
4011
|
+
flavors has NO plain assembleDebug output: commit
|
|
4012
|
+
{ "android": { "variant": "productionDebug" } } and
|
|
4013
|
+
\`stim android\` runs assembleProductionDebug,
|
|
4014
|
+
finds the APK in apk/production/debug/ and keys the
|
|
4015
|
+
build cache on the variant. The \`--variant\` flag
|
|
4016
|
+
overrides this per invocation. Unset means plain
|
|
4017
|
+
assembleDebug. A variant whose name ENDS IN Release
|
|
4018
|
+
(\`release\`, \`productionRelease\`) is a release
|
|
4019
|
+
build: embedded JS, no Metro, cache keyed on the
|
|
4020
|
+
variant, and an APK re-pack on cache hits. See
|
|
4021
|
+
\`guide lifecycle release\`.
|
|
4022
|
+
android.keystore the keystore a RE-PACKED release APK is signed with,
|
|
4023
|
+
absolute or relative to the project root. Unset means
|
|
4024
|
+
android/app/debug.keystore, which every RN and Expo
|
|
4025
|
+
android project carries -- the right default, because
|
|
4026
|
+
what this signs is a local emulator install and never
|
|
4027
|
+
anything distributed. Set it only when the release
|
|
4028
|
+
variant must be signed with the repo's own key.
|
|
4029
|
+
android.keystorePassword
|
|
4030
|
+
the password for it. apksigner's SCHEMED form is
|
|
4031
|
+
passed through unchanged (\`env:MY_KS_PASS\`,
|
|
4032
|
+
\`file:/keys/pw.txt\`, \`stdin\`), which is how a
|
|
4033
|
+
committed .stim.json avoids carrying a secret; a
|
|
4034
|
+
bare string is used as the literal password. Unset
|
|
4035
|
+
means the debug keystore's fixed "android".
|
|
4036
|
+
android.remote "proxy" or "eas"; the Android half of ios.remote
|
|
4037
|
+
metro.tunnel selects how a remote device reaches this workspace's
|
|
4038
|
+
Metro after remote intent exists. Plain \`start\` stays
|
|
4039
|
+
local. For Expo and bare React Native, "auto" (default)
|
|
4040
|
+
first tries an authenticated and working ngrok.
|
|
4041
|
+
After an auth refusal,
|
|
4042
|
+
or any failure before ngrok returns a URL, it falls back
|
|
4043
|
+
to cloudflared. "off" asserts the device
|
|
4044
|
+
shares this machine and is the only mode that needs no
|
|
4045
|
+
tunnel. "expo" lets the Expo dev server tunnel itself.
|
|
4046
|
+
"cloudflared" and "ngrok" name a managed provider
|
|
4047
|
+
explicitly. Any other value is refused as invalid.
|
|
4048
|
+
metro.ngrokUrl the stable managed ngrok URL. It requires metro.tunnel
|
|
4049
|
+
"ngrok" and passes --url to ngrok http. Stim owns
|
|
4050
|
+
this process.
|
|
4051
|
+
metro.publicUrl an existing tunnel's URL. Takes precedence over
|
|
4052
|
+
starting one, whatever metro.tunnel says -- Stim
|
|
4053
|
+
did not create it, so a Metro request through it is
|
|
4054
|
+
still gated the same way a managed tunnel's is. Set it
|
|
4055
|
+
before Expo start so the manifest advertises it.
|
|
4056
|
+
metro.warmupUrl optional object with per-platform bundle URLs:
|
|
4057
|
+
metro.warmupUrl.ios
|
|
4058
|
+
metro.warmupUrl.android
|
|
4059
|
+
an HTTP(S) URL or a /path ending in .bundle, with the
|
|
4060
|
+
full query the app uses, including a matching platform.
|
|
4061
|
+
Unset uses Expo's manifest or bare React Native defaults.
|
|
4062
|
+
Stim preserves the path and query but always requests
|
|
4063
|
+
this workspace's verified local Metro port; a supplied
|
|
4064
|
+
host and port are ignored. No defaults are added to an
|
|
4065
|
+
override. This only configures prefetch, not the app.
|
|
4066
|
+
For example, in .stim.json:
|
|
4067
|
+
{ "metro": { "warmupUrl": {
|
|
4068
|
+
"ios": "/src/main.bundle?platform=ios&dev=true&lazy=true"
|
|
4069
|
+
} } }
|
|
4070
|
+
Use the app's complete request for custom options.
|
|
4071
|
+
URLs must encode spaces and omit fragments. doctor
|
|
4072
|
+
validates the shape and platform but cannot discover
|
|
4073
|
+
runtime entry-point or dev-menu overrides or auto-fix
|
|
4074
|
+
them. See \`guide metro\` for warmup behavior.
|
|
4075
|
+
worktree.exclude ignored-path skip list for worktree warm. Settings
|
|
4076
|
+
come from the source checkout's repository-root
|
|
4077
|
+
.stim.json. A nonempty .worktreeexclude in the source
|
|
4078
|
+
checkout replaces this setting. Registered nested Git
|
|
4079
|
+
worktrees are always skipped.
|
|
4080
|
+
worktree.defaultBranch
|
|
4081
|
+
the branch the source checkout is expected to sit on,
|
|
4082
|
+
read only by \`worktree warm --refresh\`, which WARNS
|
|
4083
|
+
(and continues) when the source checkout is on another
|
|
4084
|
+
branch, because the copy then carries that branch's
|
|
4085
|
+
dependencies. Unset, the branch
|
|
4086
|
+
\`git symbolic-ref --short refs/remotes/origin/HEAD\`
|
|
4087
|
+
names is used; set it when origin/HEAD is missing or
|
|
4088
|
+
wrong. Neither answering means no warning, not an error.
|
|
4089
|
+
cache.provider one optional SECOND-TIER cache provider: a module
|
|
4090
|
+
path relative to the settings file that names it, or a
|
|
4091
|
+
package name. It implements the @stim-cli/cache
|
|
4092
|
+
contract and can serve Metro transforms, native build
|
|
4093
|
+
artifacts, or both. The local filesystem stays tier
|
|
4094
|
+
one; a provider is read only after a local miss and
|
|
4095
|
+
written after the local write. Failures and timeouts
|
|
4096
|
+
are cache misses, never build or bundle failures.
|
|
4097
|
+
Stim ships no provider and never configures one.
|
|
4098
|
+
This module is EXECUTABLE CODE that every worktree of
|
|
4099
|
+
this app runs; review a committed value the way
|
|
4100
|
+
you review a build script.
|
|
4101
|
+
\`stim ios\` and \`stim android\` use it unless
|
|
4102
|
+
artifact or remote artifact caching is disabled. Metro
|
|
4103
|
+
uses it only when the project's own metro.config.js
|
|
4104
|
+
calls \`sharedCacheStores()\` from @stim-cli/metro: the
|
|
4105
|
+
store Stim injects for you (bare in-process, or the
|
|
4106
|
+
Expo config override) stays local-only.
|
|
4107
|
+
cache.options free-form object handed to that module's factory. It
|
|
4108
|
+
merges key by key across settings layers. Keep secrets
|
|
4109
|
+
out of the committed file: read them from the
|
|
4110
|
+
environment or the machine layers.
|
|
4111
|
+
caches extra shared-cache paths for \`gc\` to report. A JSON
|
|
4112
|
+
array; every path is treated as a flat store.
|
|
4113
|
+
|
|
4114
|
+
Each setting takes its documented type: string, array of strings, number,
|
|
4115
|
+
boolean, or object. A value of the wrong type is
|
|
4116
|
+
refused by name on every command that resolves settings, so a wrong shape never
|
|
4117
|
+
falls back to a default silently. \`stim doctor\` reports it as a finding
|
|
4118
|
+
instead of refusing. The exception is \`optimizations.android.casToolchain\`:
|
|
4119
|
+
an invalid value warns and falls back to ccache, or no compiler cache when
|
|
4120
|
+
\`compilerCache\` is \`none\`. \`doctor\` also reports the invalid setting.
|
|
4121
|
+
|
|
4122
|
+
Anything else is IGNORED, and Stim warns about it by name on every run that
|
|
4123
|
+
resolves settings. If you see such a warning, the key was either renamed or
|
|
4124
|
+
removed -- check this list rather than assuming it still applies.
|
|
4125
|
+
|
|
4126
|
+
\`stim doctor\` checks the settings themselves on every run, whatever
|
|
4127
|
+
--platform says, because a rotted machine setting is not a native-platform
|
|
4128
|
+
problem. It reports, as notes naming the key, the file it came from, and the
|
|
4129
|
+
line that clears it: a setting whose path no longer exists, a setting that needs
|
|
4130
|
+
a companion the config does not supply, a key Stim no longer reads, and a
|
|
4131
|
+
config file that is not valid JSON. It skips the \`projects\` registry, where an
|
|
4132
|
+
entry for a deleted checkout is normal and \`gc\` owns the cleanup.
|
|
4133
|
+
|
|
4134
|
+
CONCURRENCY LIMITS ARE MACHINE-LEVEL, NOT A PER-PROJECT SETTING
|
|
4135
|
+
The caps above are not in the layered settings -- they are not per-project,
|
|
4136
|
+
because the resource they share (cores, RAM, booted simulators) is the whole
|
|
4137
|
+
machine's. They live under a top-level \`concurrency\` key in
|
|
4138
|
+
~/.stim/config.json, edited by hand:
|
|
4139
|
+
|
|
4140
|
+
{
|
|
4141
|
+
"concurrency": { "maxBuilds": 2, "maxDevices": 3 }
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
or via the environment, which overrides the file:
|
|
4145
|
+
|
|
4146
|
+
STIM_MAX_BUILDS=2 STIM_MAX_DEVICES=3 stim ios
|
|
4147
|
+
|
|
4148
|
+
Unset, 0, or any non-positive value means NO enforcement -- the default, where
|
|
4149
|
+
Stim limits nothing. See \`guide lifecycle concurrency\` for what each cap
|
|
4150
|
+
does.
|
|
4151
|
+
|
|
4152
|
+
THE DEVICE POOL BOUNDS ARE MACHINE-LEVEL TOO
|
|
4153
|
+
\`pool.iosParkedMax\` caps how many parked simulators \`worktree remove\` may
|
|
4154
|
+
leave behind for a later workspace to adopt. It is machine-level for the same
|
|
4155
|
+
reason: the disk they sit on is the whole machine's, about 2.5 GB each.
|
|
4156
|
+
|
|
4157
|
+
{
|
|
4158
|
+
"pool": { "iosParkedMax": 3 }
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
in ~/.stim/config.json, or STIM_POOL_IOS_PARKED_MAX in the environment, which
|
|
4162
|
+
overrides the file. Absent means 3. \`0\` turns parking and adoption off:
|
|
4163
|
+
\`worktree remove\` deletes the simulator, \`ios\` never adopts, and a pool
|
|
4164
|
+
that already exists stays where it is until \`gc --delete\`. A value that is
|
|
4165
|
+
not a whole number 0 or more is refused by name on \`worktree remove\` and
|
|
4166
|
+
\`ios\`, and warned about by \`status\`, \`gc\` and \`doctor\`.
|
|
4167
|
+
|
|
4168
|
+
Android uses \`pool.androidParkedMax\` or STIM_POOL_ANDROID_PARKED_MAX with
|
|
4169
|
+
these same defaults and validation rules. \`android\` validates that bound;
|
|
4170
|
+
\`worktree remove\`, \`status\`, \`gc\` and \`doctor\` check the applicable
|
|
4171
|
+
platform bounds. Android adoption matches the system image and AVD creation
|
|
4172
|
+
settings, preserves the APK, and clears app data before launch. See
|
|
4173
|
+
\`guide lifecycle pool\` for cleanup and the system state that remains.
|
|
4174
|
+
|
|
4175
|
+
When STIM_HOME is set, parking and adoption are OFF unless
|
|
4176
|
+
the corresponding STIM_POOL_IOS_PARKED_MAX or STIM_POOL_ANDROID_PARKED_MAX
|
|
4177
|
+
is set too. A redirected home is a scoped config --
|
|
4178
|
+
test suites and the end-to-end harness use one -- and a scoped config must not
|
|
4179
|
+
leave simulators on the machine it cannot account for. A redirected home that
|
|
4180
|
+
wants a pool says so with the variable.
|
|
4181
|
+
|
|
4182
|
+
STIM NEEDS NO PROJECT CHANGES TO RUN
|
|
4183
|
+
Nothing above is required to use Stim. The performance caches that used to
|
|
4184
|
+
be setup steps are supplied by Stim on the command lines it composes itself:
|
|
4185
|
+
|
|
4186
|
+
xcodebuild COMPILATION_CACHE_ENABLE_CACHING / COMPILATION_CACHE_CAS_PATH /
|
|
4187
|
+
SWIFT_ENABLE_COMPILE_CACHE / CLANG_ENABLE_PREFIX_MAPPING /
|
|
4188
|
+
CLANG_OTHER_PREFIX_MAPPINGS -- so no Podfile post_install block
|
|
4189
|
+
(Xcode 26+ only, and skipped when the project configured ccache,
|
|
4190
|
+
which defeats it)
|
|
4191
|
+
gradlew --build-cache -- so no org.gradle.caching=true in a committed
|
|
4192
|
+
gradle.properties. Debug builds add
|
|
4193
|
+
-PreactNativeArchitectures=<target ABI> when the owned
|
|
4194
|
+
emulator system image or physical device proves the ABI;
|
|
4195
|
+
unknown targets and Release builds stay universal. The same run
|
|
4196
|
+
carries the ccache launcher and CCACHE_BASEDIR /
|
|
4197
|
+
CCACHE_NOHASHDIR when ccache is on PATH -- so no
|
|
4198
|
+
externalNativeBuild cmake arguments in a committed
|
|
4199
|
+
build.gradle.
|
|
4200
|
+
start a shared Metro FileStore, APPENDED to whatever the project
|
|
4201
|
+
configured -- so no metro.config.js. On a bare project Stim
|
|
4202
|
+
hosts Metro itself and adds it to the config it loaded; on Expo
|
|
4203
|
+
SDK 54+ the child loads Stim's config adapter through
|
|
4204
|
+
EXPO_OVERRIDE_METRO_CONFIG. Expo SDK 53 and older run with
|
|
4205
|
+
their normal Metro cache.
|
|
4206
|
+
|
|
4207
|
+
Each of those prints one dim line saying it happened. There is no setup skill
|
|
4208
|
+
and no init command; \`stim doctor\` reports the project-side settings as
|
|
4209
|
+
things you need only if you ALSO build outside Stim.
|
|
4210
|
+
|
|
4211
|
+
OPTIMIZATION SWITCHES
|
|
4212
|
+
Put an optimizations object at the TOP LEVEL of $STIM_HOME/config.json
|
|
4213
|
+
(default ~/.stim/config.json) to set machine defaults. Merge it into the
|
|
4214
|
+
existing file; preserve the project and device records. No project changes
|
|
4215
|
+
are required. The same object in .stim.json, or the existing repository/project
|
|
4216
|
+
settings layers, overrides individual values. Explicit false wins; removing a
|
|
4217
|
+
key inherits the next layer. Changes apply on the next build or Metro restart.
|
|
4218
|
+
|
|
4219
|
+
{
|
|
4220
|
+
"optimizations": {
|
|
4221
|
+
"buildCache": true,
|
|
4222
|
+
"remoteBuildCache": true,
|
|
4223
|
+
"releaseBundleSwap": true,
|
|
4224
|
+
"metroSharedCache": true,
|
|
4225
|
+
"metroWarmup": true,
|
|
4226
|
+
"ios": {
|
|
4227
|
+
"compilationCache": true,
|
|
4228
|
+
"swiftCompilationCache": false,
|
|
4229
|
+
"prefixMapping": true
|
|
4230
|
+
},
|
|
4231
|
+
"android": {
|
|
4232
|
+
"compilerCache": "auto",
|
|
4233
|
+
"pch": "auto",
|
|
4234
|
+
"gradleBuildCache": true,
|
|
4235
|
+
"targetAbiOnly": true
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
The example shows the defaults. Full setting names and behavior:
|
|
4241
|
+
optimizations.buildCache
|
|
4242
|
+
false skips native artifact reads AND writes, including remote providers.
|
|
4243
|
+
Compiler caches remain independent. The existing --no-build-cache flag
|
|
4244
|
+
only bypasses reads and still stores the fresh build.
|
|
4245
|
+
optimizations.remoteBuildCache
|
|
4246
|
+
false skips both optional remote artifact providers, including loading
|
|
4247
|
+
their modules and authentication. Local artifact caching stays enabled.
|
|
4248
|
+
Stim ships no network provider or hosted cache. Remote artifact reuse
|
|
4249
|
+
requires a provider configured through cache.provider or Expo.
|
|
4250
|
+
optimizations.releaseBundleSwap
|
|
4251
|
+
false always builds Release from source, including the current JS;
|
|
4252
|
+
it never installs an old embedded bundle. Fresh artifacts can still store.
|
|
4253
|
+
optimizations.metroSharedCache
|
|
4254
|
+
false stops Stim appending its shared Metro store on both dev servers.
|
|
4255
|
+
Project-configured stores remain the project's choice. Replace the removed
|
|
4256
|
+
machine setting caches.injectMetroStore=false with this setting set false.
|
|
4257
|
+
optimizations.metroWarmup
|
|
4258
|
+
true by default. false skips background development bundle requests during
|
|
4259
|
+
ios/android, including any metro.warmupUrl override. Metro verification and
|
|
4260
|
+
native builds still run. Applies on the next ios/android command; no Metro
|
|
4261
|
+
restart is needed.
|
|
4262
|
+
optimizations.ios.compilationCache
|
|
4263
|
+
controls Xcode compilation caching (Xcode 26+).
|
|
4264
|
+
optimizations.ios.swiftCompilationCache
|
|
4265
|
+
opts into experimental Swift caching, requiring compilationCache=true.
|
|
4266
|
+
optimizations.ios.prefixMapping
|
|
4267
|
+
controls Clang source/DerivedData prefix mapping. false clears Stim's
|
|
4268
|
+
mappings. The existing Xcode version and project-ccache guards still apply.
|
|
4269
|
+
optimizations.android.compilerCache
|
|
4270
|
+
auto uses a CAS manifest if supplied, otherwise the normal ccache setup.
|
|
4271
|
+
ccache explicitly selects that setup; none stops Stim injecting it and
|
|
4272
|
+
disables inherited ccache. Project-defined compiler integrations can still
|
|
4273
|
+
override CMake settings. cas asks for the manifest below; with no usable
|
|
4274
|
+
manifest the build warns once and uses ccache rather than refusing. A value
|
|
4275
|
+
outside auto|ccache|cas|none is still refused by name.
|
|
4276
|
+
optimizations.android.casToolchain
|
|
4277
|
+
absolute path to the experimental Apple Clang toolchain JSON manifest.
|
|
4278
|
+
STIM_ANDROID_CAS_TOOLCHAIN overrides this path. An explicit ccache or none
|
|
4279
|
+
selection overrides automatic CAS selection even with that environment
|
|
4280
|
+
variable set. Any value that is not an absolute path, whatever its type,
|
|
4281
|
+
and a manifest that is missing, unreadable, or does not name an executable
|
|
4282
|
+
clang, clangxx, lld, ar and ranlib plus an existing resourceDir, degrade to
|
|
4283
|
+
the compiler cache the selection leaves -- ccache, or none when
|
|
4284
|
+
compilerCache is none -- in one warning naming this key and the file it came
|
|
4285
|
+
from. \`stim doctor\` resolves the same manifest and reports what the build
|
|
4286
|
+
would warn about as a note: a path that is not there, from this key or from
|
|
4287
|
+
that environment variable, or a manifest that is there and cannot be used.
|
|
4288
|
+
For prerequisites, see:
|
|
4289
|
+
https://stim.appandflow.com/docs/android-cas
|
|
4290
|
+
optimizations.android.pch
|
|
4291
|
+
auto keeps library/project policy, with PCH off by default when Stim supplies
|
|
4292
|
+
ccache. on/off overrides Gradle CMAKE_DISABLE_PRECOMPILE_HEADERS arguments;
|
|
4293
|
+
CMake target-level overrides still win. on does not fix stock ccache's
|
|
4294
|
+
cross-worktree PCH limitations.
|
|
4295
|
+
optimizations.android.gradleBuildCache
|
|
4296
|
+
false passes --no-build-cache to Gradle, overriding org.gradle.caching=true.
|
|
4297
|
+
optimizations.android.targetAbiOnly
|
|
4298
|
+
false stops narrowing Debug builds to the device ABI. Release is always
|
|
4299
|
+
universal; project ABI filters still apply.
|
|
4300
|
+
|
|
4301
|
+
Android CAS, explicit PCH modes, and changed iOS compiler options use separate
|
|
4302
|
+
native artifact keys. Android ccache and none share an artifact key when their
|
|
4303
|
+
PCH mode matches; disable artifact caching too to force native compilation. Legacy Expo providers
|
|
4304
|
+
cannot key these compiler profiles, so Stim skips that tier for custom profiles
|
|
4305
|
+
and Android CAS. Providers implementing the Stim key contract remain usable.
|
|
4306
|
+
Android compiler/PCH profiles also get separate CMake staging directories under
|
|
4307
|
+
<module>/.cxx/stim-<profile>, or under the project's custom staging root. Switching
|
|
4308
|
+
backends in Stim selects the matching directory without deleting previous builds.
|
|
4309
|
+
These directories accumulate across profile changes and shim upgrades. Stim
|
|
4310
|
+
does not prune them; remove an obsolete generated profile only with all native
|
|
4311
|
+
builds stopped. Worktree removal reclaims profiles with the rest of the tree.
|
|
4312
|
+
Direct Gradle runs keep their own configuration. These switches control Stim's
|
|
4313
|
+
build invocations; they do not edit Xcode, Gradle, CMake, or Metro project files.
|
|
4314
|
+
|
|
4315
|
+
Reading the timeline for it: on Expo, \`cache_store_requested\` is Stim saying
|
|
4316
|
+
it asked (it set EXPO_OVERRIDE_METRO_CONFIG on a process it does not run, which
|
|
4317
|
+
is all this side can know), and \`cache_store_added\` is the adapter reporting
|
|
4318
|
+
from inside that process that the store is in the config Metro loaded. Only the
|
|
4319
|
+
second one means transforms are being shared. A bare project writes
|
|
4320
|
+
\`cache_store_added\` directly, because there Stim adds the store itself.
|
|
4321
|
+
|
|
4322
|
+
TEMPORARY STORAGE
|
|
4323
|
+
Large temporary copies for iOS app preparation, release JS/APK
|
|
4324
|
+
swaps, and the doctor fingerprint checkout select a writable directory on the
|
|
4325
|
+
relevant filesystem. App/APK preparation uses the artifact volume. Worktree
|
|
4326
|
+
warm copies directly to the destination and does not use temporary storage.
|
|
4327
|
+
A system temporary directory on that
|
|
4328
|
+
volume is preferred, then a writable ancestor of the relevant path. Staging
|
|
4329
|
+
is private and outside Git working trees, so ignored secrets cannot enter
|
|
4330
|
+
Git status or git add. If no safe location exists, the operation refuses.
|
|
4331
|
+
|
|
4332
|
+
Set STIM_TMPDIR or top-level tempDir in $STIM_HOME/config.json (default
|
|
4333
|
+
~/.stim/config.json) to override placement. STIM_TMPDIR takes precedence.
|
|
4334
|
+
The value must be an absolute directory outside Git working trees; missing
|
|
4335
|
+
directories are created privately. An override is used even on another volume.
|
|
4336
|
+
Doctor reports cross-volume copy costs and invalid temporary settings; it
|
|
4337
|
+
creates no directories for this placement check. Unset the override to restore
|
|
4338
|
+
automatic selection. An example machine setting:
|
|
4339
|
+
|
|
4340
|
+
{ "tempDir": "/Volumes/SSD/stim-tmp" }
|
|
4341
|
+
|
|
4342
|
+
Small tool-response and entitlement files still use the system temporary
|
|
4343
|
+
directory. Build-cache storage stages beside its destination independently of
|
|
4344
|
+
tempDir. Keeping build output and its cache on different volumes still requires
|
|
4345
|
+
a full copy. iOS build output lives under STIM_HOME/workspaces; Android APKs
|
|
4346
|
+
live under the project's android/app/build/outputs/apk. Doctor compares these
|
|
4347
|
+
locations with the cache, using resolved symlinks and filesystem device IDs.
|
|
4348
|
+
It checks the current layout; arbitrary provider-returned paths and future
|
|
4349
|
+
mount changes cannot be predicted. Same-volume placement permits cloning when
|
|
4350
|
+
the filesystem supports it; it does not prove cloning occurred. On macOS,
|
|
4351
|
+
cp -c can silently fall back to copying and exit successfully.
|
|
4352
|
+
|
|
4353
|
+
CACHE LOCATIONS ARE MACHINE-LEVEL TOO
|
|
4354
|
+
The shared build cache and Metro transform cache default to living under
|
|
4355
|
+
~/.stim. To relocate them (say, to an external disk), set a top-level
|
|
4356
|
+
\`caches\` key in ~/.stim/config.json, edited by hand -- absolute paths:
|
|
4357
|
+
|
|
4358
|
+
{
|
|
4359
|
+
"caches": { "buildCache": "/Volumes/SSD/stim/build-cache",
|
|
4360
|
+
"metroCache": "/Volumes/SSD/stim/metro-cache" }
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
STIM_BUILD_CACHE / STIM_METRO_CACHE in the environment override the file.
|
|
4364
|
+
The CLI and both cache packages resolve these identically, so every process
|
|
4365
|
+
finds the same store regardless of shell profile. A relative path is ignored.
|
|
4366
|
+
The Metro value is a PARENT root. The sanitized package name is appended below
|
|
4367
|
+
it, so apps remain separately reportable and prunable. Earlier releases used an
|
|
4368
|
+
overridden Metro root as one flat store. A new registration replaces that legacy
|
|
4369
|
+
parent entry and marks the named layout. If an older package registers it again,
|
|
4370
|
+
current gc ignores the exact unmarked legacy parent while a marked child exists.
|
|
4371
|
+
A marked store that later becomes another override parent remains visible but is
|
|
4372
|
+
report-only while its marked child exists. Root-level legacy files remain
|
|
4373
|
+
untouched for manual cleanup.
|
|
4374
|
+
|
|
4375
|
+
PREFER SELF-REGISTRATION OVER THE 'caches' SETTING
|
|
4376
|
+
There is no 'cache' command. A cache registers itself from code instead, once,
|
|
4377
|
+
and every 'gc' report shows it from then on, tagged (registered):
|
|
4378
|
+
|
|
4379
|
+
import { register } from 'stim/cache-manifest';
|
|
4380
|
+
register({ dir: '<dir>', name: '<what to call it>', entriesDepth: 2 });
|
|
4381
|
+
|
|
4382
|
+
entriesDepth is how far below dir one entry sits (default 1, a flat store).
|
|
4383
|
+
Pass 2 for a root with a layer of grouping above the entries -- a Metro
|
|
4384
|
+
FileStore shards across 256 directories, a build cache is keyed
|
|
4385
|
+
<platform>/<key> -- or 'gc --delete --older-than N' removes a whole shard or
|
|
4386
|
+
platform instead of one entry. Pass prune: 'atomic' for a cache whose index
|
|
4387
|
+
references its own data (an LLVM CAS): it is then left alone by --older-than
|
|
4388
|
+
and emptied whole only by 'gc --delete --cache all'.
|
|
4389
|
+
Registration is idempotent and keyed on the directory.`
|
|
4390
|
+
}
|
|
4391
|
+
};
|
|
4392
|
+
//#endregion
|
|
4393
|
+
//#region src/commands/guide.ts
|
|
4394
|
+
function topicNames() {
|
|
4395
|
+
return Object.keys(TOPICS);
|
|
4396
|
+
}
|
|
4397
|
+
function topicByName(name) {
|
|
4398
|
+
return Object.hasOwn(TOPICS, name) ? TOPICS[name] ?? null : null;
|
|
4399
|
+
}
|
|
4400
|
+
const LOOKUPS = /* @__PURE__ */ new Map();
|
|
4401
|
+
function sectionLookup(name) {
|
|
4402
|
+
const cached = LOOKUPS.get(name);
|
|
4403
|
+
if (cached) return cached;
|
|
4404
|
+
const lookup = Object.create(null);
|
|
4405
|
+
for (const [sectionName, section] of Object.entries(topicByName(name)?.sections ?? {})) {
|
|
4406
|
+
lookup[sectionName] = section;
|
|
4407
|
+
for (const alias of section.aliases ?? []) lookup[alias] = section;
|
|
4408
|
+
}
|
|
4409
|
+
LOOKUPS.set(name, lookup);
|
|
4410
|
+
return lookup;
|
|
4411
|
+
}
|
|
4412
|
+
function sectionNames(name) {
|
|
4413
|
+
return Object.keys(topicByName(name)?.sections ?? {});
|
|
4414
|
+
}
|
|
4415
|
+
const COMMAND_NOTATION = "Commands use `stim`. If it is not installed globally, replace `stim` with `npx stim`.";
|
|
4416
|
+
function wordCount(text) {
|
|
4417
|
+
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
4418
|
+
}
|
|
4419
|
+
function renderSectionIndex(name) {
|
|
4420
|
+
const topic = topicByName(name);
|
|
4421
|
+
if (!topic?.sections) return null;
|
|
4422
|
+
const entries = Object.entries(topic.sections);
|
|
4423
|
+
const width = Math.max(...entries.map(([sectionName]) => sectionName.length));
|
|
4424
|
+
const lines = ["SECTIONS"];
|
|
4425
|
+
for (const [sectionName, section] of entries) {
|
|
4426
|
+
if (section.separator) {
|
|
4427
|
+
lines.push("", section.separator, "");
|
|
4428
|
+
if (section.context) lines.push(section.context, "");
|
|
4429
|
+
}
|
|
4430
|
+
lines.push(` ${sectionName.padEnd(width)} ${`${wordCount(section.body())}w`.padStart(6)} ${section.summary}`);
|
|
4431
|
+
for (const alias of section.aliases ?? []) lines.push(` = ${alias}`);
|
|
4432
|
+
}
|
|
4433
|
+
lines.push("", `Read one with: stim guide ${name} ${topic.sectionHint ?? "<section>"}`);
|
|
4434
|
+
return lines.join("\n");
|
|
4435
|
+
}
|
|
4436
|
+
function renderTopic(name) {
|
|
4437
|
+
const topic = topicByName(name);
|
|
4438
|
+
if (!topic) return null;
|
|
4439
|
+
if (topic.body) return `${COMMAND_NOTATION}\n\n${topic.body()}`;
|
|
4440
|
+
return `${COMMAND_NOTATION}\n\n${topic.preamble?.() ?? ""}\n\n${renderSectionIndex(name) ?? ""}`;
|
|
4441
|
+
}
|
|
4442
|
+
function renderSection(name, section) {
|
|
4443
|
+
const lookup = sectionLookup(name);
|
|
4444
|
+
const found = Object.hasOwn(lookup, section) ? lookup[section] : void 0;
|
|
4445
|
+
if (!found) return null;
|
|
4446
|
+
const context = found.context ? `${found.context}\n\n` : "";
|
|
4447
|
+
return `${COMMAND_NOTATION}\n\n${context}${found.body()}`;
|
|
4448
|
+
}
|
|
4449
|
+
function topicOwning(section) {
|
|
4450
|
+
for (const name of topicNames()) if (Object.hasOwn(sectionLookup(name), section)) return name;
|
|
4451
|
+
return null;
|
|
4452
|
+
}
|
|
4453
|
+
function renderIndex(version) {
|
|
4454
|
+
const lines = [
|
|
4455
|
+
`stim ${version} -- reference for the binary you are running.`,
|
|
4456
|
+
"",
|
|
4457
|
+
COMMAND_NOTATION,
|
|
4458
|
+
"",
|
|
4459
|
+
"This output is generated by the CLI, so it always matches this version.",
|
|
4460
|
+
"The bundled skill routes coding agents to the agent topic. Other topics",
|
|
4461
|
+
"carry the detailed command contracts and remedies.",
|
|
4462
|
+
"",
|
|
4463
|
+
"TOPICS"
|
|
4464
|
+
];
|
|
4465
|
+
const width = Math.max(...topicNames().map((n) => n.length));
|
|
4466
|
+
for (const name of topicNames()) lines.push(` ${name.padEnd(width)} ${TOPICS[name]?.summary ?? ""}`);
|
|
4467
|
+
lines.push("", "Read one with: stim guide <topic> (sectioned topics: stim guide <topic> <section>)");
|
|
4468
|
+
return lines.join("\n");
|
|
4469
|
+
}
|
|
4470
|
+
function guideCommand(program, version) {
|
|
4471
|
+
program.command("guide [topic] [section]").description("Print reference documentation for THIS version of Stim (topics: " + topicNames().join(", ") + "). A topic with sections prints its section index; name a section to print only it, e.g. `stim guide errors STIM_NO_METRO` or `stim guide lifecycle builds`. Generated by the binary, so it cannot drift from the installed CLI.").action((topic, section) => {
|
|
4472
|
+
if (!topic) {
|
|
4473
|
+
console.log(renderIndex(version));
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
const body = renderTopic(topic);
|
|
4477
|
+
if (!body) {
|
|
4478
|
+
console.error(chalk.red(`Unknown topic "${topic}".`));
|
|
4479
|
+
console.error(chalk.dim(`Available: ${topicNames().join(", ")}`));
|
|
4480
|
+
const owner = topicOwning(topic);
|
|
4481
|
+
if (owner) console.error(chalk.dim(`Did you mean: stim guide ${owner} ${topic}`));
|
|
4482
|
+
process.exit(1);
|
|
4483
|
+
}
|
|
4484
|
+
if (!section) {
|
|
4485
|
+
console.log(body);
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
const index = renderSectionIndex(topic);
|
|
4489
|
+
if (!index) {
|
|
4490
|
+
console.error(chalk.red(`Topic "${topic}" has no sections.`));
|
|
4491
|
+
process.exit(1);
|
|
4492
|
+
}
|
|
4493
|
+
const sectionBody = renderSection(topic, section);
|
|
4494
|
+
if (!sectionBody) {
|
|
4495
|
+
console.error(chalk.red(`Unknown section "${section}" in topic "${topic}".`));
|
|
4496
|
+
console.error(index);
|
|
4497
|
+
process.exit(1);
|
|
4498
|
+
}
|
|
4499
|
+
console.log(sectionBody);
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
//#endregion
|
|
4503
|
+
export { guideCommand as default, renderIndex, renderSection, renderSectionIndex, renderTopic, sectionLookup, sectionNames, topicNames };
|