← LimrunCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Limrun
Snapshot Sep 30, 2026 · 23:08 UTC · version 1.1.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "limrun-expo-development",
"description": "Prepare and run Expo / React Native apps on Limrun with Expo dev-client iteration. Use when the user wants an Expo dev build, Metro tunnel, hot reload, JS/TS iteration without repeated native rebuilds, or to run/test an Expo app on a remote iOS simulator or Android emulator.",
"included_files": [],
"skill_md_contents": "---\nname: limrun-expo-development\ndescription: \"Prepare and run Expo / React Native apps on Limrun with Expo dev-client iteration. Use when the user wants an Expo dev build, Metro tunnel, hot reload, JS/TS iteration without repeated native rebuilds, or to run/test an Expo app on a remote iOS simulator or Android emulator.\"\nuser-invocable: true\neffort: high\n---\n\n# Developing Expo Apps on Limrun\n\nUse this skill for Expo / React Native-specific setup and dev-client iteration, on iOS simulators and Android emulators. Use **limrun-ios-simulator** and **limrun-android-emulator** for command details, device interaction, screenshots, recordings, and cleanup, and the build skills (**limrun-xcode**, **limrun-gradle**) for build flag details and non-Expo workflows.\n\nAll builds and device operations must run on Limrun. Do not use local Xcode, local simulators, a local Android SDK, or local emulators; local `adb` is used only to talk to the remote emulator through the CLI's tunnel.\n\n## Expo Readiness\n\nBefore changing Expo dependencies or app config, check the app's Expo SDK version and use the matching Expo versioned docs.\n\nVerify this is an Expo app:\n\n```bash\nnpx expo config --type introspect --json\n```\n\nDerive:\n\n- `BUNDLE_ID` from `ios.bundleIdentifier` (iOS) and `PACKAGE` from `android.package` (Android). When `android.package` is missing, introspect reports a placeholder (like `com.placeholder.appid`) while the build generates a different real applicationId; set `android.package` in `app.json` before building so `$PACKAGE` matches the installed app.\n- `SLUG` from `slug`\n- `SCHEME` from `scheme`, falling back to `exp+${SLUG}`\n- `BRANCH` from `git branch --show-current`, falling back to `main`\n- `ASSET_NAME=\"${BUNDLE_ID}/${BRANCH}-debug.zip\"` on iOS, `ASSET_NAME=\"${PACKAGE}/${BRANCH}-debug.apk\"` on Android\n\n## Ensure Dev Client\n\nExpo development builds require `expo-dev-client`. If it is missing from `package.json`, install it automatically:\n\n```bash\nnpx expo install expo-dev-client\n```\n\nInstalling `expo-dev-client`, adding/removing/updating native dependencies, or changing native app config means the uploaded Debug asset is stale. Build a fresh Debug app before starting the dev loop. Do not merely warn the user that a rebuild may be needed; perform the rebuild.\n\n## Debug Build Asset\n\nFirst check whether a reusable Debug dev-client asset already exists:\n\n```bash\nlim asset list --name-prefix \"$BUNDLE_ID/\" # iOS\nlim asset list --name-prefix \"$PACKAGE/\" # Android\n```\n\nReuse the exact `$ASSET_NAME` only when:\n\n- it exists, and\n- no native dependency or native config changed in this session.\n\nIf the current task changed native dependencies or native config, skip asset reuse even if `$ASSET_NAME` exists.\n\nWhen reusing the asset, create or reuse a device and install it:\n\n```bash\nlim ios create \\\n --reuse-if-exists \\\n --install-asset \"$ASSET_NAME\" \\\n --label repo=<repo> \\\n --label agent=<agent>\n\nlim android create \\\n --reuse-if-exists \\\n --install-asset \"$ASSET_NAME\" \\\n --no-open \\\n --label repo=<repo> \\\n --label agent=<agent>\n```\n\nAndroid note: keep the tunnel that `create` opens by default (do not pass\n`--no-connect` here, unlike plain driving sessions); the Metro reverse\ntunnel below runs over it. Note the instance ID from the output and pass\n`--id` to every later `lim android` call: Metro and Expo run from the app\ndirectory, and instance resolution is per git worktree, so commands run from\nelsewhere will not find the instance on their own.\n\n### Fresh build on Android\n\nBuild the Debug APK remotely and upload it as the asset (Expo prebuild,\n`--expo-app-dir`, and other build flags belong to **limrun-gradle**; the\ndefault `assembleDebug` task is the right dev-client build):\n\n```bash\nlim gradle build . --upload \"$ASSET_NAME\"\nlim android create --reuse-if-exists --install-asset \"$ASSET_NAME\" --no-open --label repo=<repo> --label agent=<agent>\n```\n\nFor a later native rebuild on a running emulator, rebuild with `--upload` and\ninstall the new APK via the Download URL the build prints (the instance\nfetches it server-side):\n\n```bash\nlim gradle build . --upload \"$ASSET_NAME\"\nlim android install-app \"<Download URL from the build output>\" --id <android-instance-id>\n```\n\n### Fresh build on iOS\n\nWhen building fresh, create or reuse a standalone Xcode sandbox and build\nbefore creating a simulator, so the simulator doesn't sit idle (and hit its\ninactivity timeout) during a long build:\n\n```bash\nlim xcode create --reuse-if-exists --label repo=<repo> --label agent=<agent>\n\nlim xcode build . \\\n --configuration Debug \\\n --upload \"$ASSET_NAME\"\n```\n\nRun `lim xcode version set <major>` once in the repo when the project needs a\nspecific Xcode major (e.g. 27 for the beta); see `limrun-xcode` for the rules.\n\nUse `--expo-app-dir`, `--scheme`, or `--workspace` when the project layout requires it.\n\nThen create the simulator attached to that Xcode target; the attach installs\nand launches the build immediately:\n\n```bash\nlim ios create --attach \\\n --reuse-if-exists \\\n --label repo=<repo> \\\n --label agent=<agent>\n```\n\nAdd `--no-open` to any `create` when you have no browser to show the user; it\nskips opening the stream URL and leaves the URL in the output to share.\n\nIf an iOS simulator is already running from a reused asset and a later native rebuild becomes necessary, attach that same simulator instead of creating a second one:\n\n```bash\nlim xcode attach-simulator <ios-instance-id> --id <xcode-instance-id>\n```\n\nAfter the attach, every successful `lim xcode build` installs and launches the app on the attached simulator.\n\n## Start Metro Through Limrun on iOS\n\nThis flow is for iOS. Android uses `adb reverse`; skip to **Start Metro on\nAndroid** instead of running the `lim ios` commands.\n\nStart one destination tunnel after the Debug app is installed. Metro can keep\nits normal local port; Expo advertises localhost:\n\n```bash\nMETRO_PORT=8081\nlim ios tunnel \\\n --selector \"localhost:${METRO_PORT}\" \\\n --detach \\\n --id <ios-instance-id>\nTUNNEL_URL=\"http://localhost:${METRO_PORT}\"\necho \"TUNNEL_URL=$TUNNEL_URL\"\n\nEXPO_PACKAGER_PROXY_URL=\"$TUNNEL_URL\" \\\n npx expo start --dev-client --port \"$METRO_PORT\"\n```\n\n`EXPO_PACKAGER_PROXY_URL` keeps localhost and the declared port in manifests,\nbundle URLs, and deep links. Set it inline so it takes precedence over project dotenv\nvalues. Keep Metro and the detached tunnel running while the user iterates.\nRun Metro as a managed background process, or copy the printed `TUNNEL_URL` into\na second terminal before launching the app.\n\nIf port 8081 is already occupied, choose another explicit port and use the same\nvalue for the tunnel selector, `TUNNEL_URL`, and Expo's `--port`. Selector sets\nare immutable: stop and recreate the tunnel with the complete selector list\nwhen the port changes.\n\nOnly add `--offline` in a genuinely network-isolated environment after\ndependencies are installed. Offline mode disables network checks and dependency\nvalidation, so do not use it to compensate for ordinary Expo authentication.\n\n### Launch the iOS dev client\n\nOpen the Debug app through the dev-client URL:\n\n```bash\nENCODED_URL=\"$(node -e 'console.log(encodeURIComponent(process.argv[1]))' \"$TUNNEL_URL\")\"\nDEV_CLIENT_URL=\"${SCHEME}://expo-development-client/?url=${ENCODED_URL}\"\nlim ios open-url --id <ios-instance-id> \"$DEV_CLIENT_URL\"\n```\n\nIf opening fails and the primary scheme came from `scheme`, retry once with\n`exp+${SLUG}`. On a fresh instance, the iOS dev-menu onboarding sheet can\nconsume the first deep link; tap through it and open the URL again.\n\nFor Expo Go, replace `--dev-client` with `--go`, then open:\n\n```bash\nlim ios open-url \\\n --id <ios-instance-id> \\\n \"exp://${TUNNEL_URL#http://}\"\n```\n\nTunnel lifecycle:\n\n```bash\nlim ios tunnel status --id <ios-instance-id> --json\nlim ios tunnel stop --id <ios-instance-id>\n```\n\nOne instance accepts one active destination tunnel. Stop the current tunnel\nbefore starting another route set. When iteration ends, stop Metro with\n`Ctrl+C` and stop the detached tunnel with the command above.\n\nIf the simulator attempts a route while Metro is stopped, the tunnel remains\nactive and status records a correlated `connection_refused`. Restart Metro with\nthe same proxy URL and reopen the dev-client URL; do not recreate the simulator\nor tunnel.\n\n## Start Metro on Android\n\nAndroid uses `adb reverse` over the CLI's ADB tunnel. Metro stays on its default\nport 8081, no packager hostname override is needed, and the emulator reaches\nMetro at `http://127.0.0.1:8081`:\n\n```bash\nlim android connect --id <android-instance-id> # background shell; prints \"Tunnel started on 127.0.0.1:<port>.\"\nadb -s 127.0.0.1:<port> reverse tcp:8081 tcp:8081\n\nnpx expo start --dev-client --port 8081\n\nDEV_CLIENT_URL=\"${SCHEME}://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A8081\"\nlim android open-url \"$DEV_CLIENT_URL\" --id <android-instance-id>\n```\n\nThe ADB tunnel dies with the shell that started it, and the port changes on\nevery reconnect; re-run `adb reverse` with the new serial after any reconnect.\nSee **limrun-android-emulator** for tunnel details.\n\nOn the first launch, tap through the dev-menu onboarding sheet\n(`lim android tap-element --text Continue`) and close the dev menu. The bundle\nloads behind the native sheet.\n\n## Fallback: Expo Tunnel\n\nIf the Limrun endpoint cannot be used, start Expo's public tunnel:\n\n```bash\nnpm install --save-dev '@expo/ngrok@^4.1.0'\nnpx expo start --dev-client --tunnel\n```\n\nUse the complete dev-client URI Expo prints:\n\n```bash\nDEV_CLIENT_URL=\"<complete URI printed by Expo>\"\nlim ios open-url --id <ios-instance-id> \"$DEV_CLIENT_URL\"\nlim android open-url \"$DEV_CLIENT_URL\" --id <android-instance-id>\n```\n\n## Legacy iOS fixed-port reverse tunnel\n\n`lim ios reverse` remains available for workflows that already use the reserved\n57090–57099 range. Expo dev-client can derive or advertise multiple packager\nURLs, so mismatched mappings like `57090:8081` can leave some URLs pointing at\nthe local Metro port instead of the simulator-facing reverse endpoint.\n\nUse the simulator-facing host printed by `lim ios reverse` in both `REACT_NATIVE_PACKAGER_HOSTNAME` and the encoded dev-client URL. Keep the reverse command running in a separate or background terminal while Metro is running:\n\n```bash\nlim ios reverse 57090:57090 --id <ios-instance-id>\n\nREACT_NATIVE_PACKAGER_HOSTNAME=<reverse-host> \\\n npx expo start --dev-client --host lan --port 57090\n\nENCODED_URL=\"$(node -e 'console.log(encodeURIComponent(process.argv[1]))' \"http://<reverse-host>:57090\")\"\nDEV_CLIENT_URL=\"${SCHEME}://expo-development-client/?url=${ENCODED_URL}\"\nlim ios open-url --id <ios-instance-id> \"$DEV_CLIENT_URL\"\n```\n\n## Verify\n\nFor quick static validation, prefer:\n\n```bash\nnpx tsc --noEmit\n```\n\nOnly run `npm run lint` or `npx expo lint` when the repo already has ESLint configured. Expo lint can create ESLint config and mutate dependencies in projects that have not configured linting yet.\n\nOn iOS, use the element tree first:\n\n```bash\nlim ios element-tree\n```\n\nSuccess means the app UI is visible or the Expo dev menu shows it is connected to the tunnel. On a fresh instance the first dev-client launch can land on the dev-menu onboarding sheet covering the launcher: tap through it (`lim ios tap-element --ax-label Continue`), then open the dev-client URL again, since the first deep link is consumed by the sheet. If the tree does not confirm the connection, inspect app logs:\n\n```bash\nlim ios app-log \"$BUNDLE_ID\" --tail 100\n```\n\nOn Android, verify with screenshots, not the element tree: Expo apps\ntypically expose no accessibility nodes there, so a rendered screen and an\nempty tree coexist (see **limrun-android-emulator**):\n\n```bash\nlim android screenshot check.png --id <android-instance-id>\n```\n\nTo see why the app died (crash, ANR), relaunch it watched; the command blocks\nwhile the app runs (run it in a background shell) and prints the exit reason,\nstack trace, and a recent app log tail when the app dies:\n\n```bash\nlim android launch-app \"$PACKAGE\" --mode RelaunchIfRunning --id <android-instance-id>\n```\n\n## Iterating\n\nOnce connected, JS/TS edits should update through Metro without another native build. If the task changes native dependencies, native config, or build settings, rebuild Debug before relaunching the dev loop.\n\nTell the user:\n\n- device stream as a short Markdown link, for example `[Open simulator stream](<signedStreamUrl>)` or `[Open emulator stream](<signedStreamUrl>)`\n- uploaded Debug asset name\n- that JS/TS changes can now iterate through Metro\n- that native changes require a new Debug build\n\n## Final Preview\n\nFor a final shareable preview or PR demo, use a Release build so the user does not need Metro running:\n\n```bash\nASSET_NAME=\"<bundle-id>/<pr-or-session>.zip\"\nlim xcode build . --configuration Release --upload \"$ASSET_NAME\"\n\nASSET_NAME=\"<package>/<pr-or-session>.apk\"\nlim gradle build . --task assembleRelease --upload \"$ASSET_NAME\"\n```\n\nPreview URL (`platform=android` for APK assets):\n\n```text\nhttps://console.limrun.com/preview?asset=${ASSET_NAME}&platform=ios\n```\n\n## Gotchas\n\n- `npx expo start --dev-client` requires `expo-dev-client`; without it Expo cannot determine the development-build scheme.\n- `No script URL provided` usually means the app is not a dev-client build or was launched without a dev-client URL.\n- After a fresh native rebuild/install, a stale Metro/runtime error like `Cannot find native module` may come from the old app process. Relaunch the dev-client URL and verify with `element-tree` before assuming the rebuild failed.\n- If a Debug build after adding a native dependency still behaves like the old native graph, that is unexpected Limrun behavior. Retry the build; creating a fresh build/device target is only a troubleshooting fallback.\n- Expo tunnel startup can be flaky. Retry before changing the workflow.\n- Do not reuse uploaded Debug assets after native dependency or native config changes.\n- On Android, pass `--id <android-instance-id>` to every `lim android` call in this loop: instance resolution is per git worktree and the loop's commands run from mixed directories.\n- An empty Android element tree while the screenshot shows the app is normal for Expo apps; verify by screenshot.\n"
}SHA-256: 2b2b867e4844dc3b4013955d44b1c4d9c6a9cbc524c426015e136852c6eec96b