From 8cb50b4b0b36fe000fd2b4d75d6a2283b68993a6 Mon Sep 17 00:00:00 2001 From: Satya Patel <14907857+saddlepaddle@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:18:41 +0000 Subject: [PATCH 1/5] fix(mobile): make eas-dev.sh survive new sessions and first loads --new used to keep the egress client of the old session, so the app could not reach Metro. The script now records the client it starts, replaces it whenever a session starts or the client is not connected to the current session, and keeps a healthy one on reuse. After the tunnel connects, it loads the app through the dev-client URL and waits for a new "iOS Bundled" line in the Metro log, three tries. --reload does only that, so agents never need agent-device open Superset. simctl is out of reach, so on a new session the script opens the keyboard once in Settings and dismisses the slide-to-type intro. Docs: how to reload and switch sessions, and that SwiftUI menu items need coordinate taps. --- .agents/skills/mobile-eas-dev/SKILL.md | 22 +++- apps/mobile/AGENTS.md | 12 +- apps/mobile/scripts/eas-dev.sh | 161 +++++++++++++++++++++---- 3 files changed, 166 insertions(+), 29 deletions(-) diff --git a/.agents/skills/mobile-eas-dev/SKILL.md b/.agents/skills/mobile-eas-dev/SKILL.md index cfb109a3875..2e2b877c697 100644 --- a/.agents/skills/mobile-eas-dev/SKILL.md +++ b/.agents/skills/mobile-eas-dev/SKILL.md @@ -16,8 +16,22 @@ apps/mobile/scripts/eas-dev.sh It reuses this checkout's running session when there is one. Otherwise it installs the finished simulator build whose native fingerprint matches this checkout and starts a session, about two minutes. It builds only when no such build exists (the first time, or after a native change), about fifteen -minutes. Pass `--new` to stop the running session and start a fresh one. `apps/mobile/AGENTS.md`'s -"Verifying in the real app" section records why the script does each step. +minutes. `apps/mobile/AGENTS.md`'s "Verifying in the real app" section records why the script does +each step. + +```bash +apps/mobile/scripts/eas-dev.sh --reload # load the app from Metro again; starts nothing +apps/mobile/scripts/eas-dev.sh --new # stop the running session and start a fresh one +``` + +- **Reload with `--reload`.** It opens the dev-client URL and waits for a new `iOS Bundled` line in the + Metro log, three tries. Never `agent-device open Superset`: that relaunches the app without the + Metro URL and it fails with "Could not connect to the server". +- **Switch sessions with `--new`,** never `eas simulator` by hand. The egress client is bound to the + session it started with, so the script stops the old client and starts one for the new session. + With no flag it keeps a connected client and restarts one that died. +- Each run that (re)starts the tunnel ends only when Metro has served the app a bundle; otherwise it + exits non-zero and names the Metro and egress logs. The session is tagged with the workspace id. The desktop Mobile pane lists the workspace's tagged sessions, so the person can watch the simulator there without a link. The script also prints an @@ -40,6 +54,10 @@ agent-device help # the rest - Through `eas simulator:exec npx agent-device` each command takes about 25 s. Install it globally (`npm install -g agent-device`) when it is missing. - The dev-menu sheet covers the first screen on a fresh install: press `Continue`, then `Close`. +- iOS's "slide to type" keyboard intro is already dismissed on a session the script started. +- SwiftUI menu items (`@expo/ui` `Menu`) are not in `agent-device snapshot`. Open the menu, take a + screenshot, and tap the item by coordinate: `agent-device press `. This is a limit of the + tool, not of the app. - Source edits reach the app through fast refresh in about two seconds. ## Limits diff --git a/apps/mobile/AGENTS.md b/apps/mobile/AGENTS.md index 1ee2d91c591..461f8b0a3c0 100644 --- a/apps/mobile/AGENTS.md +++ b/apps/mobile/AGENTS.md @@ -91,8 +91,18 @@ export default function AuthenticatedLayout() { has the dev values in its shell. - **The simulator reaches this machine only through local egress,** and the egress client can only start once the session exists. The app's first load therefore always fails; the script loads it - again when the tunnel is up. A port the app calls must be passed as `--egress-allow`; the script + again through the dev-client URL when the tunnel is up, and retries until the Metro log shows a + new `iOS Bundled` line. A port the app calls must be passed as `--egress-allow`; the script reads `$API_PORT` from `.env` rather than assuming a fixed port. + - **An egress client serves only the session it started with.** It reads `.env.eas-simulator` once, + so after a new session a running client still tunnels to the old one and the app shows "Could + not connect to the server". `--new` replaces the client; never start a session by hand. + - **Reload with `eas-dev.sh --reload`, not `agent-device open Superset`.** A plain launch has no + Metro URL and fails; only `superset://expo-development-client/?url=...` loads the app. + - **The "slide to type" keyboard intro** covers the screen the first time a keyboard opens. + `simctl` is out of reach, so the script opens the keyboard once in Settings and presses Continue. + - **SwiftUI menu items (`@expo/ui` `Menu`) are not in `agent-device snapshot`.** Tap them by + coordinate from a screenshot. This is a limit of the tool. - **Native Sentry starts only when `EXPO_PUBLIC_SENTRY_DSN_MOBILE` is set.** Native init without a DSN crashes the app at launch with a bare SIGSEGV before any JS runs, and the EAS `development` environment has no DSN. diff --git a/apps/mobile/scripts/eas-dev.sh b/apps/mobile/scripts/eas-dev.sh index 8cbf5f4bc89..9a9c6cc5208 100755 --- a/apps/mobile/scripts/eas-dev.sh +++ b/apps/mobile/scripts/eas-dev.sh @@ -10,6 +10,7 @@ # Usage: # apps/mobile/scripts/eas-dev.sh # reuse this checkout's running session, else start one # apps/mobile/scripts/eas-dev.sh --new # stop the running session and start a fresh one +# apps/mobile/scripts/eas-dev.sh --reload # load the app from Metro again; starts nothing # MOBILE_EAS_BUILD_ID= apps/mobile/scripts/eas-dev.sh # install this build, skip the lookup # # Requires: EXPO_TOKEN (or `eas login`), this repo's .env already written @@ -21,9 +22,11 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" cd "$ROOT_DIR" NEW=0 +RELOAD=0 for arg in "$@"; do case "$arg" in --new) NEW=1 ;; + --reload) RELOAD=1 ;; *) echo "Unknown argument: $arg" >&2 exit 1 @@ -46,7 +49,10 @@ BUILD_PROFILE=development-simulator IDLE_MINUTES="${MOBILE_EAS_IDLE_MINUTES:-15}" SESSION_FILE=apps/mobile/.env.eas-simulator EGRESS_LOG=/tmp/superset-eas-egress.log +EGRESS_PID_FILE=/tmp/superset-eas-egress.pid METRO_LOG=/tmp/superset-metro.log +LOAD_ATTEMPTS=3 +BUNDLE_WAIT_SECONDS=90 cd apps/mobile @@ -76,11 +82,131 @@ session_running() { [ "$(eas simulator:get --id "$id" --json --non-interactive 2>/dev/null | json 'j.status')" = "IN_PROGRESS" ] } -if ! curl -s "http://127.0.0.1:$METRO_PORT/status" 2>/dev/null | grep -q "packager-status:running"; then +metro_running() { + curl -s "http://127.0.0.1:$METRO_PORT/status" 2>/dev/null | grep -q "packager-status:running" +} + +agent_device() { + if command -v agent-device >/dev/null 2>&1; then + ( + set -a + # shellcheck disable=SC1090 + source "$ROOT_DIR/$SESSION_FILE" + set +a + agent-device "$@" + ) + else + eas simulator:exec npx -y agent-device "$@" + fi +} + +# The egress client reads .env.eas-simulator once, at start, so it stays bound +# to the session that was current then. +egress_pids() { + local pid + for pid in $(pgrep -f "eas simulator:egress" || true); do + if [ ! -d /proc ] || [ "$(readlink "/proc/$pid/cwd")" = "$PWD" ]; then echo "$pid"; fi + done +} + +egress_healthy() { + local pid + pid="$(cat "$EGRESS_PID_FILE" 2>/dev/null || true)" + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && + grep -qF "simulator session $1." "$EGRESS_LOG" 2>/dev/null && + grep -q "Egress tunnel connected" "$EGRESS_LOG" +} + +stop_egress() { + local pid + for pid in $(egress_pids) $(cat "$EGRESS_PID_FILE" 2>/dev/null || true); do + pkill -P "$pid" 2>/dev/null || true + kill "$pid" 2>/dev/null || true + done + rm -f "$EGRESS_PID_FILE" +} + +start_egress() { + stop_egress + echo "==> Egress for session $1" + nohup eas simulator:egress >"$EGRESS_LOG" 2>&1 "$EGRESS_PID_FILE" + for _ in $(seq 1 60); do + grep -q "Egress tunnel connected" "$EGRESS_LOG" && return 0 + sleep 1 + done + echo "The egress tunnel did not connect in 60 s. See $EGRESS_LOG." >&2 + exit 1 +} + +bundle_count() { + local count + count="$(grep -c "iOS Bundled" "$METRO_LOG" 2>/dev/null || true)" + echo "${count:-0}" +} + +# Proof the app reached Metro is a new "iOS Bundled" line in the Metro log. +load_app() { + local attempt before + if [ ! -f "$METRO_LOG" ]; then + echo "==> Loading the app from Metro (not confirmed: Metro was not started by this script)" + agent_device open "$DEV_CLIENT_URL" >/dev/null 2>&1 || true + agent_device alert accept >/dev/null 2>&1 || true + return 0 + fi + for attempt in $(seq 1 "$LOAD_ATTEMPTS"); do + echo "==> Loading the app from Metro (attempt $attempt of $LOAD_ATTEMPTS)" + before="$(bundle_count)" + agent_device open "$DEV_CLIENT_URL" >/dev/null 2>&1 || true + agent_device alert accept >/dev/null 2>&1 || true + for _ in $(seq 1 "$BUNDLE_WAIT_SECONDS"); do + if [ "$(bundle_count)" -gt "$before" ]; then + echo "==> Metro served the bundle" + return 0 + fi + sleep 1 + done + done + echo "Metro served no bundle after $LOAD_ATTEMPTS tries. See $METRO_LOG and $EGRESS_LOG." >&2 + exit 1 +} + +# simctl is not reachable from here, so the intro cannot be turned off with +# `defaults write`. Showing the keyboard once in Settings and pressing Continue +# marks it as shown for the whole simulator. +dismiss_keyboard_intro() { + local output + agent_device open com.apple.Preferences --relaunch >/dev/null 2>&1 || return 0 + output="$(agent_device press 'role="search"' --settle 2>&1 || true)" + if [[ "$output" == *UIContinuousPathIntroductionView* ]]; then + echo "==> Dismissing the slide-to-type keyboard intro" + agent_device press 'label="Continue"' --settle >/dev/null 2>&1 || true + fi + agent_device close com.apple.Preferences >/dev/null 2>&1 || true +} + +if [ "$RELOAD" = "1" ]; then + metro_running || { + echo "Metro is not running. Run apps/mobile/scripts/eas-dev.sh without --reload." >&2 + exit 1 + } + session_running || { + echo "No running simulator session. Run apps/mobile/scripts/eas-dev.sh without --reload." >&2 + exit 1 + } + egress_healthy "$(session_id)" || { + echo "The egress client is not connected to session $(session_id). Run apps/mobile/scripts/eas-dev.sh without --reload." >&2 + exit 1 + } + load_app + exit 0 +fi + +if ! metro_running; then echo "==> Starting Metro" nohup bunx expo start --dev-client --port "$METRO_PORT" >"$METRO_LOG" 2>&1 /dev/null | grep -q "packager-status:running" && break + metro_running && break sleep 2 done fi @@ -90,9 +216,11 @@ if [ "$NEW" = "1" ] && session_running; then eas simulator:stop --id "$(session_id)" --non-interactive >/dev/null fi +STARTED=0 if session_running; then echo "==> Reusing session $(session_id)" else + STARTED=1 BUILD_ID="${MOBILE_EAS_BUILD_ID:-}" if [ -z "$BUILD_ID" ]; then HASH="$(without_dev_env eas fingerprint:generate -p ios --build-profile "$BUILD_PROFILE" --json --non-interactive | json 'j.hash')" @@ -120,33 +248,13 @@ else --out-config-type dotenv --non-interactive --force fi -agent_device() { - if command -v agent-device >/dev/null 2>&1; then - ( - set -a - # shellcheck disable=SC1090 - source "$ROOT_DIR/$SESSION_FILE" - set +a - agent-device "$@" - ) - else - eas simulator:exec npx -y agent-device "$@" - fi -} - # The simulator reaches this machine only while the egress client runs, and # the client needs the session, so the app's first load at session start # always fails. Load it again once the tunnel is up. -if ! pgrep -f "eas simulator:egress" >/dev/null 2>&1; then - echo "==> Egress" - nohup eas simulator:egress >"$EGRESS_LOG" 2>&1 /dev/null && break - sleep 1 - done - echo "==> Loading the app from Metro" - agent_device open "$DEV_CLIENT_URL" >/dev/null 2>&1 || true - agent_device alert accept >/dev/null 2>&1 || true +if [ "$STARTED" = "1" ] || ! egress_healthy "$(session_id)"; then + start_egress "$(session_id)" + [ "$STARTED" = "1" ] && dismiss_keyboard_intro + load_app fi SESSION_ID="$(session_id)" @@ -158,6 +266,7 @@ Ready. Session: $SESSION_ID Preview: $PREVIEW_URL Drive it: (cd apps/mobile && set -a && . ./.env.eas-simulator && set +a && agent-device snapshot -i) + Reload it: apps/mobile/scripts/eas-dev.sh --reload needs "npm install -g agent-device"; through npx each command takes about 25 s Stop it: (cd apps/mobile && eas simulator:stop --id $SESSION_ID) Metro log: $METRO_LOG From 65358ff16fa1f14e099177c3d99bfdf601de2c20 Mon Sep 17 00:00:00 2001 From: Satya Patel <14907857+saddlepaddle@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:26:20 +0000 Subject: [PATCH 2/5] fix(mobile): eas-dev.sh hides dev overlays and keeps state per checkout - Pass disableAutoLaunch=1&disableFab=1 on every dev-client load, so no dev menu opens at launch and no gear covers the toolbar (replaces #8616). - Keep egress and Metro logs and PIDs in .tmp/eas-dev/ of the checkout, and kill a PID only when it is still an egress client, so two checkouts no longer stop each other's tunnel. - On macOS, find a client's cwd with lsof instead of matching every client. - Confirm a load from the Metro log only when this script started Metro. - Treat the tunnel as healthy only when its last state is "connected". - Refuse the fingerprint lookup when bun differs from .bun-version, instead of starting a build that fails (the root cause behind #8618). - Exit when Metro does not start; reject --new with --reload. --- .agents/skills/mobile-demo-record/SKILL.md | 2 +- .agents/skills/mobile-eas-dev/SKILL.md | 13 +++- .../flows/host-states/never-connected.yml | 2 +- apps/mobile/AGENTS.md | 4 +- apps/mobile/scripts/eas-dev.sh | 75 ++++++++++++++----- 5 files changed, 74 insertions(+), 22 deletions(-) diff --git a/.agents/skills/mobile-demo-record/SKILL.md b/.agents/skills/mobile-demo-record/SKILL.md index 0dbaa423e26..5fdb4027101 100644 --- a/.agents/skills/mobile-demo-record/SKILL.md +++ b/.agents/skills/mobile-demo-record/SKILL.md @@ -44,7 +44,7 @@ xcrun simctl ui $U appearance light xcrun simctl status_bar $U override --time "9:41" --batteryState discharging \ --batteryLevel 100 --cellularBars 4 --wifiBars 3 --dataNetwork wifi xcrun simctl spawn $U defaults write sh.superset.mobile EXDevMenuIsOnboardingFinished -bool YES -xcrun simctl openurl $U "superset://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081" +xcrun simctl openurl $U "superset://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081&disableAutoLaunch=1&disableFab=1" ``` - `simctl boot` does not launch Simulator.app. Keep it that way. diff --git a/.agents/skills/mobile-eas-dev/SKILL.md b/.agents/skills/mobile-eas-dev/SKILL.md index 2e2b877c697..75c1e99012c 100644 --- a/.agents/skills/mobile-eas-dev/SKILL.md +++ b/.agents/skills/mobile-eas-dev/SKILL.md @@ -30,8 +30,17 @@ apps/mobile/scripts/eas-dev.sh --new # stop the running session and start a - **Switch sessions with `--new`,** never `eas simulator` by hand. The egress client is bound to the session it started with, so the script stops the old client and starts one for the new session. With no flag it keeps a connected client and restarts one that died. -- Each run that (re)starts the tunnel ends only when Metro has served the app a bundle; otherwise it - exits non-zero and names the Metro and egress logs. +- When the script started Metro, each run that (re)starts the tunnel ends only when Metro has served + the app a bundle; otherwise it exits non-zero and names the Metro and egress logs. With a Metro + started some other way it opens the app but cannot confirm the load. +- The dev-client URL carries `disableAutoLaunch=1&disableFab=1`, so no dev menu opens at launch and + no gear button covers the toolbar. Each session is a fresh install, so every load passes them. +- Logs and PID files are in `.tmp/eas-dev/` of the checkout, so two checkouts do not stop each + other's tunnel. +- The fingerprint lookup refuses to run when `bun --version` differs from `.bun-version`. The + dependencies that bun installs give a fingerprint no build has, and EAS would start a 15-minute + build that also fails. A cloud workspace made before the pin changed has the old bun: make a new + one. The session is tagged with the workspace id. The desktop Mobile pane lists the workspace's tagged sessions, so the person can watch the simulator there without a link. The script also prints an diff --git a/apps/mobile/.maestro/flows/host-states/never-connected.yml b/apps/mobile/.maestro/flows/host-states/never-connected.yml index 3e3272f2a85..ad218342d80 100644 --- a/apps/mobile/.maestro/flows/host-states/never-connected.yml +++ b/apps/mobile/.maestro/flows/host-states/never-connected.yml @@ -7,7 +7,7 @@ env: HOST_NAME: "E2E Mac mini" --- - stopApp -- openLink: "superset://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081" +- openLink: "superset://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081&disableAutoLaunch=1&disableFab=1" - extendedWaitUntil: visible: "One step left" timeout: 60000 diff --git a/apps/mobile/AGENTS.md b/apps/mobile/AGENTS.md index 461f8b0a3c0..527c78d058c 100644 --- a/apps/mobile/AGENTS.md +++ b/apps/mobile/AGENTS.md @@ -98,7 +98,9 @@ export default function AuthenticatedLayout() { so after a new session a running client still tunnels to the old one and the app shows "Could not connect to the server". `--new` replaces the client; never start a session by hand. - **Reload with `eas-dev.sh --reload`, not `agent-device open Superset`.** A plain launch has no - Metro URL and fails; only `superset://expo-development-client/?url=...` loads the app. + Metro URL and fails; only + `superset://expo-development-client/?url=...&disableAutoLaunch=1&disableFab=1` loads the app + without the dev menu and its gear button. - **The "slide to type" keyboard intro** covers the screen the first time a keyboard opens. `simctl` is out of reach, so the script opens the keyboard once in Settings and presses Continue. - **SwiftUI menu items (`@expo/ui` `Menu`) are not in `agent-device snapshot`.** Tap them by diff --git a/apps/mobile/scripts/eas-dev.sh b/apps/mobile/scripts/eas-dev.sh index 9a9c6cc5208..e16a5c360a3 100755 --- a/apps/mobile/scripts/eas-dev.sh +++ b/apps/mobile/scripts/eas-dev.sh @@ -33,6 +33,10 @@ for arg in "$@"; do ;; esac done +if [ "$NEW" = "1" ] && [ "$RELOAD" = "1" ]; then + echo "--new and --reload cannot be used together." >&2 + exit 1 +fi if [ ! -f .env ]; then echo "No .env here — run .superset/setup.sh or .superset/setup.cloud.sh first." >&2 @@ -48,15 +52,17 @@ API_PORT="${API_PORT:?API_PORT is missing from .env}" BUILD_PROFILE=development-simulator IDLE_MINUTES="${MOBILE_EAS_IDLE_MINUTES:-15}" SESSION_FILE=apps/mobile/.env.eas-simulator -EGRESS_LOG=/tmp/superset-eas-egress.log -EGRESS_PID_FILE=/tmp/superset-eas-egress.pid -METRO_LOG=/tmp/superset-metro.log +STATE_DIR="$ROOT_DIR/.tmp/eas-dev" +EGRESS_LOG="$STATE_DIR/egress.log" +EGRESS_PID_FILE="$STATE_DIR/egress.pid" +METRO_LOG="$STATE_DIR/metro.log" +METRO_PID_FILE="$STATE_DIR/metro.pid" LOAD_ATTEMPTS=3 BUNDLE_WAIT_SECONDS=90 cd apps/mobile -DEV_CLIENT_URL="superset://expo-development-client/?url=$(node -e 'console.log(encodeURIComponent(process.argv[1]))' "http://localhost:$METRO_PORT")" +DEV_CLIENT_URL="superset://expo-development-client/?url=$(node -e 'console.log(encodeURIComponent(process.argv[1]))' "http://localhost:$METRO_PORT")&disableAutoLaunch=1&disableFab=1" # The runtime version is a fingerprint of the app config. EAS resolves it # without this machine's dev values, so the build and its lookup must too. @@ -100,39 +106,57 @@ agent_device() { fi } -# The egress client reads .env.eas-simulator once, at start, so it stays bound -# to the session that was current then. +process_cwd() { + if [ -d /proc ]; then + readlink "/proc/$1/cwd" 2>/dev/null || true + else + lsof -a -d cwd -p "$1" -Fn 2>/dev/null | sed -n 's/^n//p' + fi +} + +is_egress_client() { + ps -o command= -p "$1" 2>/dev/null | grep -q "simulator:egress" +} + egress_pids() { local pid for pid in $(pgrep -f "eas simulator:egress" || true); do - if [ ! -d /proc ] || [ "$(readlink "/proc/$pid/cwd")" = "$PWD" ]; then echo "$pid"; fi + if [ "$(process_cwd "$pid")" = "$PWD" ]; then echo "$pid"; fi done + pid="$(cat "$EGRESS_PID_FILE" 2>/dev/null || true)" + if [ -n "$pid" ] && is_egress_client "$pid"; then echo "$pid"; fi +} + +egress_connected() { + grep -E "Egress tunnel (connected|disconnected)" "$EGRESS_LOG" 2>/dev/null | tail -n 1 | grep -q "connected\." } egress_healthy() { local pid pid="$(cat "$EGRESS_PID_FILE" 2>/dev/null || true)" - [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && + [ -n "$pid" ] && is_egress_client "$pid" && grep -qF "simulator session $1." "$EGRESS_LOG" 2>/dev/null && - grep -q "Egress tunnel connected" "$EGRESS_LOG" + egress_connected } stop_egress() { local pid - for pid in $(egress_pids) $(cat "$EGRESS_PID_FILE" 2>/dev/null || true); do + for pid in $(egress_pids | sort -u); do pkill -P "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true done rm -f "$EGRESS_PID_FILE" } +# The egress client reads .env.eas-simulator once, at start, so it stays bound +# to the session that was current then. start_egress() { stop_egress echo "==> Egress for session $1" nohup eas simulator:egress >"$EGRESS_LOG" 2>&1 "$EGRESS_PID_FILE" for _ in $(seq 1 60); do - grep -q "Egress tunnel connected" "$EGRESS_LOG" && return 0 + egress_connected && return 0 sleep 1 done echo "The egress tunnel did not connect in 60 s. See $EGRESS_LOG." >&2 @@ -145,10 +169,15 @@ bundle_count() { echo "${count:-0}" } -# Proof the app reached Metro is a new "iOS Bundled" line in the Metro log. +metro_started_here() { + local pid + pid="$(cat "$METRO_PID_FILE" 2>/dev/null || true)" + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && [ -f "$METRO_LOG" ] +} + load_app() { local attempt before - if [ ! -f "$METRO_LOG" ]; then + if ! metro_started_here; then echo "==> Loading the app from Metro (not confirmed: Metro was not started by this script)" agent_device open "$DEV_CLIENT_URL" >/dev/null 2>&1 || true agent_device alert accept >/dev/null 2>&1 || true @@ -181,10 +210,14 @@ dismiss_keyboard_intro() { if [[ "$output" == *UIContinuousPathIntroductionView* ]]; then echo "==> Dismissing the slide-to-type keyboard intro" agent_device press 'label="Continue"' --settle >/dev/null 2>&1 || true + else + echo "==> No slide-to-type keyboard intro found" fi agent_device close com.apple.Preferences >/dev/null 2>&1 || true } +mkdir -p "$STATE_DIR" + if [ "$RELOAD" = "1" ]; then metro_running || { echo "Metro is not running. Run apps/mobile/scripts/eas-dev.sh without --reload." >&2 @@ -205,10 +238,15 @@ fi if ! metro_running; then echo "==> Starting Metro" nohup bunx expo start --dev-client --port "$METRO_PORT" >"$METRO_LOG" 2>&1 "$METRO_PID_FILE" for _ in $(seq 1 30); do metro_running && break sleep 2 done + metro_running || { + echo "Metro did not start in 60 s. See $METRO_LOG." >&2 + exit 1 + } fi if [ "$NEW" = "1" ] && session_running; then @@ -223,6 +261,12 @@ else STARTED=1 BUILD_ID="${MOBILE_EAS_BUILD_ID:-}" if [ -z "$BUILD_ID" ]; then + BUN_PINNED="$(cat "$ROOT_DIR/.bun-version")" + if [ "$(bun --version)" != "$BUN_PINNED" ]; then + echo "bun $(bun --version) is installed, but .bun-version pins $BUN_PINNED. No build matches the fingerprint of dependencies it installed." >&2 + echo "In a cloud workspace, create a new one from the current image. Or set MOBILE_EAS_BUILD_ID to install a known build." >&2 + exit 1 + fi HASH="$(without_dev_env eas fingerprint:generate -p ios --build-profile "$BUILD_PROFILE" --json --non-interactive | json 'j.hash')" BUILD_ID="$(eas build:list -p ios --build-profile "$BUILD_PROFILE" --status finished --fingerprint-hash "$HASH" --limit 1 --json --non-interactive | json 'j[0]?.id')" if [ -z "$BUILD_ID" ]; then @@ -248,9 +292,6 @@ else --out-config-type dotenv --non-interactive --force fi -# The simulator reaches this machine only while the egress client runs, and -# the client needs the session, so the app's first load at session start -# always fails. Load it again once the tunnel is up. if [ "$STARTED" = "1" ] || ! egress_healthy "$(session_id)"; then start_egress "$(session_id)" [ "$STARTED" = "1" ] && dismiss_keyboard_intro @@ -266,8 +307,8 @@ Ready. Session: $SESSION_ID Preview: $PREVIEW_URL Drive it: (cd apps/mobile && set -a && . ./.env.eas-simulator && set +a && agent-device snapshot -i) - Reload it: apps/mobile/scripts/eas-dev.sh --reload needs "npm install -g agent-device"; through npx each command takes about 25 s + Reload it: apps/mobile/scripts/eas-dev.sh --reload Stop it: (cd apps/mobile && eas simulator:stop --id $SESSION_ID) Metro log: $METRO_LOG Egress log: $EGRESS_LOG From 306275d856b26cad56094b1c145730a32ba0cd0d Mon Sep 17 00:00:00 2001 From: Satya Patel <14907857+saddlepaddle@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:31:22 +0000 Subject: [PATCH 3/5] fix(mobile): eas-dev.sh matches only the exact connected line --- apps/mobile/scripts/eas-dev.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/scripts/eas-dev.sh b/apps/mobile/scripts/eas-dev.sh index e16a5c360a3..6a9c4aa042d 100755 --- a/apps/mobile/scripts/eas-dev.sh +++ b/apps/mobile/scripts/eas-dev.sh @@ -128,7 +128,7 @@ egress_pids() { } egress_connected() { - grep -E "Egress tunnel (connected|disconnected)" "$EGRESS_LOG" 2>/dev/null | tail -n 1 | grep -q "connected\." + grep -E "Egress tunnel (connected|disconnected)" "$EGRESS_LOG" 2>/dev/null | tail -n 1 | grep -q "Egress tunnel connected" } egress_healthy() { From 92d59397e0fdd5133ff414893277c5cb6750f410 Mon Sep 17 00:00:00 2001 From: Satya Patel <14907857+saddlepaddle@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:48:02 +0000 Subject: [PATCH 4/5] fix(mobile): eas-dev.sh touches only egress clients in this checkout --- apps/mobile/scripts/eas-dev.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/mobile/scripts/eas-dev.sh b/apps/mobile/scripts/eas-dev.sh index 6a9c4aa042d..9bd35b3432b 100755 --- a/apps/mobile/scripts/eas-dev.sh +++ b/apps/mobile/scripts/eas-dev.sh @@ -114,17 +114,15 @@ process_cwd() { fi } -is_egress_client() { - ps -o command= -p "$1" 2>/dev/null | grep -q "simulator:egress" +is_own_egress_client() { + [ "$(process_cwd "$1")" = "$PWD" ] && ps -o command= -p "$1" 2>/dev/null | grep -q "simulator:egress" } egress_pids() { local pid for pid in $(pgrep -f "eas simulator:egress" || true); do - if [ "$(process_cwd "$pid")" = "$PWD" ]; then echo "$pid"; fi + if is_own_egress_client "$pid"; then echo "$pid"; fi done - pid="$(cat "$EGRESS_PID_FILE" 2>/dev/null || true)" - if [ -n "$pid" ] && is_egress_client "$pid"; then echo "$pid"; fi } egress_connected() { @@ -134,14 +132,14 @@ egress_connected() { egress_healthy() { local pid pid="$(cat "$EGRESS_PID_FILE" 2>/dev/null || true)" - [ -n "$pid" ] && is_egress_client "$pid" && + [ -n "$pid" ] && is_own_egress_client "$pid" && grep -qF "simulator session $1." "$EGRESS_LOG" 2>/dev/null && egress_connected } stop_egress() { local pid - for pid in $(egress_pids | sort -u); do + for pid in $(egress_pids); do pkill -P "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true done From f3fc997b588981563c8599098c10b654b3a5d90a Mon Sep 17 00:00:00 2001 From: Satya Patel <14907857+saddlepaddle@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:58:09 +0000 Subject: [PATCH 5/5] fix(mobile): eas-dev.sh compares physical paths to find its egress clients --- apps/mobile/scripts/eas-dev.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/scripts/eas-dev.sh b/apps/mobile/scripts/eas-dev.sh index 9bd35b3432b..9c0ac32dc1d 100755 --- a/apps/mobile/scripts/eas-dev.sh +++ b/apps/mobile/scripts/eas-dev.sh @@ -18,7 +18,7 @@ # account. set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd -P)" cd "$ROOT_DIR" NEW=0