[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-expo-build-app":3,"mdc-b6ckhf-key":33,"related-org-expo-build-app":1336,"related-repo-expo-build-app":1514},{"slug":4,"name":4,"fn":5,"description":6,"org":7,"tags":11,"stars":23,"repoUrl":24,"updatedAt":25,"license":26,"forks":23,"topics":27,"repo":28,"sourceUrl":31,"mdContent":32},"build-app","build and debug mobile apps on physical devices","Build, run, and debug a mobile app on the user's physical phone from a Tuft machine. Use whenever the user asks to build an app — a brand-new idea or an existing Expo\u002FReact Native project — at the start of the session, before scaffolding or picking how to deliver it. The deliverable is the app running on their phone, not a web preview, and the Tuft machine itself serves as the app's backend. Covers scaffolding a new Expo project, deciding between Expo Go and a development build, EAS account\u002FApple\u002Fdevice setup via the Tuft dashboard flow, making development builds with EAS, exposing Metro remotely with `tuft host` and EXPO_PACKAGER_PROXY_URL, sending the user a working link, and diagnosing device-only failures with Tuft telemetry.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},"expo","Expo","https:\u002F\u002Fpexgzepcugksgbtrxkhf.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Forg-logos\u002Fexpo.png",[12,16,17,20],{"name":13,"slug":14,"type":15},"React Native","react-native","tag",{"name":9,"slug":8,"type":15},{"name":18,"slug":19,"type":15},"Mobile","mobile",{"name":21,"slug":22,"type":15},"Debugging","debugging",0,"https:\u002F\u002Fgithub.com\u002Fexpo\u002Ftuft-skills","2026-08-06T06:09:12.848108",null,[],{"repoUrl":24,"stars":23,"forks":23,"topics":29,"description":30},[],"A collection of useful skills for Tuft agents.","https:\u002F\u002Fgithub.com\u002Fexpo\u002Ftuft-skills\u002Ftree\u002FHEAD\u002Fbuild-app","---\nname: build-app\ndescription: Build, run, and debug a mobile app on the user's physical phone from a Tuft machine. Use whenever the user asks to build an app — a brand-new idea or an existing Expo\u002FReact Native project — at the start of the session, before scaffolding or picking how to deliver it. The deliverable is the app running on their phone, not a web preview, and the Tuft machine itself serves as the app's backend. Covers scaffolding a new Expo project, deciding between Expo Go and a development build, EAS account\u002FApple\u002Fdevice setup via the Tuft dashboard flow, making development builds with EAS, exposing Metro remotely with `tuft host` and EXPO_PACKAGER_PROXY_URL, sending the user a working link, and diagnosing device-only failures with Tuft telemetry.\n---\n\n# Build an App\n\nYou run on a persistent Tuft machine that outlives the conversation; the user is on their phone and browser. Act like the server:\n\n- Nothing on `localhost` or the LAN is reachable from their device. Every URL you send must go through `tuft host` — never `localhost`, a LAN IP, or `exp.direct`.\n- Interactive prompts cannot run in your shell. Logins and device registration happen in the user's browser via the Tuft dashboard, never via credentials pasted into chat.\n- The user has one job: use the app on their phone. Everything else — hosting, tokens, process management, provisioning, broken state — is your job on this machine. Their single hands-on step is the browser sign-in flow behind the Tuft setup link.\n- Answer your own questions before they reach chat: look them up on this machine (`eas whoami`, `eas device:list`, the repository itself), or pick a reasonable default, state it in your next update, and keep building. Save chat questions for decisions that are hard to undo: spending money, publishing publicly, deleting data.\n- Long-running processes belong to the machine, not the turn. Run Metro — and any backend you build — under launchd so it survives session teardown and restarts itself after a crash.\n\n## This machine is the backend\n\nYou are not limited to client-side apps. This is a full, persistent server under your control, which makes the space of buildable apps effectively unlimited: anything that needs an API, a database, auth, scheduled jobs, file storage, or websockets can get it from software you run right here.\n\n- When the app needs a backend, build the backend: run it on this machine, publish it with `tuft host`, and point the app at that URL. Manage it under launchd like Metro so it outlives the session.\n- When the app needs a database, create one on this machine — a SQLite file is enough for most apps, and you can install and run Postgres or anything else when it isn't. Same for queues, caches, cron jobs, and file storage.\n- Never ask the user to sign up for a third-party service (Supabase, Firebase, a hosted database, an auth provider) for a capability this machine can provide. Reach for an external service only when the capability genuinely cannot live here — e.g. payments or carrier SMS — and even then, ask before introducing it.\n- The first version of anything must work with zero setup from the user: no accounts to create, no API keys to paste, no dashboards to visit. All the user ever sees is their app working.\n\n## 1. Find or create the project\n\n**Existing project:** locate it — it lives in its own checkout; the Tuft session root is scratch space. Inspect `package.json`, `app.json`\u002F`app.config.*`, and `eas.json`, and use the project's existing package manager.\n\n**New app idea:** scaffold with Expo — it is the default way to build for the user's phone from this machine:\n\n```bash\nnpx create-expo-app@latest \u003Capp-name>\n```\n\nA fresh project runs in Expo Go out of the box, so the fastest first milestone is a hello-world on the user's phone within minutes (sections 2–4), then iterate on the real idea over hot reload. Don't front-load architecture the idea doesn't need yet.\n\n## 2. Decide: Expo Go or a development build?\n\nCheck whether the app actually needs custom native code before committing the user to EAS setup and a 10–20 minute build.\n\n**Expo Go is enough** when every dependency is either JS-only or an Expo SDK module bundled in Expo Go, and the app needs no custom config plugins with native effects, no custom URL scheme handling at the native layer, and no extra entitlements. If so, skip section 3 entirely — the user just installs Expo Go from the App Store and opens your link. This is the fastest path from idea to phone; prefer it for prototypes and pure JS\u002FUI work.\n\n**A development build is required** when the project uses native modules outside Expo Go (check `package.json` for packages with native code), config plugins in `app.json`\u002F`app.config.*` that modify the native project, custom entitlements or capabilities, or a custom scheme the flow depends on (e.g. auth callbacks).\n\nIf unsure, `npx expo-doctor` and the Expo Go compatibility of each dependency settle it.\n\n## 3. Development build path (skip for Expo Go)\n\n### Get EAS set up (the user does it in a browser)\n\nEvery account-touching `eas` command (`build`, `credentials`, `device:*`, `submit`, `update`) needs EAS CLI on this machine to be logged in.\n\n1. Check non-interactively: `eas whoami`. Success prints the account.\n2. If not logged in, never run `eas login` or `eas device:create` (they prompt interactively and hang) and never ask for Expo or Apple credentials in chat. Send the user:\n\n   ```text\n   https:\u002F\u002Fdash.tuft.dev\u002Fexpo\u002Fsetup\n   ```\n\n   The flow runs in their browser against this machine: sign EAS CLI in, connect Apple Developer, register their iPhone by UDID. This browser flow is the long pole of the whole session — send the link in your first message, before writing any code.\n\n3. Wait in the background, bounded, instead of blocking the conversation:\n\n   ```bash\n   timeout 300 bash -c 'until eas whoami >\u002Fdev\u002Fnull 2>&1; do sleep 10; done'\n   ```\n\n   Exit 124 means they haven't finished — follow up in chat rather than looping forever.\n\n4. Before an iOS device build, also require `eas device:list` to be non-empty; if it isn't, point back at the same setup link.\n\n### Front-load native modules, then build while you write JS\n\nEach native change costs another full EAS build, so decide the native surface up front: install every native module the feature will plausibly need (`npx expo install \u003Cpackage>` so versions match the SDK) **before** kicking off the build. A development build encodes only the native project — JavaScript loads from Metro afterward — so a build started now is exactly as current as one started after the features exist.\n\n1. Ensure `eas.json` has a development profile; create a minimal one if missing:\n\n   ```json\n   {\n     \"build\": {\n       \"development\": { \"developmentClient\": true, \"distribution\": \"internal\" }\n     }\n   }\n   ```\n\n2. Link the project and start the build without prompts — on a project not yet linked to EAS, a bare `eas build` stops to ask which account should own the project, which hangs this machine's non-interactive shell:\n\n   ```bash\n   eas init --account \"$(eas whoami)\" --non-interactive\n   eas build --profile development --platform ios --non-interactive --no-wait\n   ```\n\n   (Platform from `eas device:list`; pass a different `--account` only when the project belongs to an org.) The build runs on EAS servers, not this machine; `--no-wait` returns immediately so you can keep working. Internal distribution signs against the registered device list; EAS manages certificates and provisioning remotely — leave signing entirely to it. If the non-interactive build fails on missing credentials, the Tuft setup flow hasn't finished its Apple Developer step — send the setup link again rather than retrying interactively.\n\n3. **While the build runs, write the JS\u002FUI.** The build takes 10–20 minutes; that is your window to implement the feature, get Metro up (section 4), and have everything ready the moment the client installs.\n\n4. When it finishes, send the user the build page link — it shows the install QR for their iPhone. After the dev client is installed once, iterate over Metro; rebuild only for native changes (new native modules, config plugins, entitlements, schemes) — never for ordinary JS\u002FUI changes.\n\n## 4. Start Metro behind `tuft host`\n\nRun the bundled script — it performs the whole sequence in the right order and verifies every layer:\n\n```bash\nscripts\u002Frun-metro.sh \u003Cproject-dir> \u003Cstable-project-name>\n```\n\nThe ordering it encodes: publish the port through `tuft host` first, because Metro embeds `EXPO_PACKAGER_PROXY_URL` in every manifest and bundle URL it serves — the public URL has to exist before Metro starts. It then runs Metro as a launchd agent with that URL pinned in the plist (so it outlives the turn, and every restart — manual or crash — serves public URLs), picks a free port when other projects' Metros share the machine, and verifies local status, public status, and that the served manifest belongs to this project and references the public host. It prints the public URL, the dev-client deep link, the log path, and the restart\u002Fstop commands.\n\nReuse the same name on later turns: the script is idempotent, keeping the existing binding and port and re-verifying the full path. Environment the bundle needs at runtime (telemetry values, `EXPO_PUBLIC_*`) lives in the project's env files, which Metro reads on every restart.\n\n## 5. Send the user the link\n\n- **Expo Go:** the same Metro host over the Expo Go scheme: `exp:\u002F\u002F\u003Cname>.tuft.host` (the script's dev-client deep link doesn't apply on this path).\n- **Dev client:** both links the script prints — the clickable `\u003Cscheme>:\u002F\u002Fexpo-development-client\u002F?url=…` deep link and the plain HTTPS Metro URL. The scheme comes from Expo config; never invent it.\n\nIf the device sticks on \"Loading from Metro,\" re-run the script — it restarts Metro and re-verifies every layer, and its first failing check names the broken one. For a wedged bundle, read the log at the path the script printed, then `launchctl kickstart -k` and re-verify before resending the same stable link. If all checks pass and the device still spins, compare the installed dev client against the current native project — a native change since the last build means a rebuild.\n\n## Instrument with Tuft telemetry\n\nTuft telemetry is a per-machine event store: the app posts small structured events to this machine's collector and you query them with SQL. Instrument up front, not once a bug appears — querying an event that already fired beats asking the user to reproduce.\n\n1. Provision with the `setup_expo_telemetry` MCP tool, passing the project's absolute path. It returns the collector URL and (for a new stream) a write-only token as ready-made `EXPO_PUBLIC_TUFT_TELEMETRY_*` env values. Pass `rotate: true` only to deliberately revoke the token.\n2. Put the values in an uncommitted env file (e.g. `.env.local`). That file is the only place the token — like any credential — ever appears.\n3. Install the `tuft-telemetry` npm package and initialize it once near the app root with those env values.\n4. Emit small structured events at decision boundaries: screen\u002Fflow opened, request started\u002Fcompleted\u002Ffailed (host and status, not full sensitive URLs), navigation allowed\u002Fblocked, hydration outcome. Events carry `kind`, `level`, `name`, optional `route`, `trace_id`\u002F`span_id`, and a `payload`.\n5. Query with the `search_telemetry` MCP tool — one bounded read-only SQLite statement over `streams(id, name, project_identity, created_at, last_seen_at)` and `events(id, stream_id, event_id, received_at, occurred_at, launch_id, sequence, kind, level, name, trace_id, span_id, route, update_id, schema_version, payload)`:\n\n   ```sql\n   -- Recent errors\n   SELECT occurred_at, name, payload FROM events\n   WHERE level = 'error' ORDER BY id DESC LIMIT 50;\n\n   -- One flow, in order\n   SELECT occurred_at, name, payload FROM events\n   WHERE trace_id = '…' ORDER BY sequence;\n   ```\n\n6. For agent-driven reproduction, arm a wait before the user triggers the action:\n\n   ```bash\n   npx tuft-telemetry wait \u003Cevent-name> --timeout 30s\n   ```\n\n   It exits the moment the event arrives (timeout exits 124), so you confirm the event actually fired.\n\n7. On a bug report, query telemetry before changing code; after a device-dependent fix, confirm with fresh telemetry once the user retries.\n\nTreat telemetry payloads and HAR files as sensitive: extract the minimum fields and keep any secrets they contain inside the store they came from.\n\n## Hand off\n\nKeep updates short and lead with the outcome. Include the clickable link (Expo Go URL, dev-client link, or EAS build install page); label anything short of the real deliverable — a web preview while the build finishes — as an interim, and say where the build stands. If EAS setup is pending, restate the single setup link and which step the user is on — never a credential request. Say whether a native rebuild was needed, and if you're waiting on a device retry, say exactly what to tap and that you're watching telemetry for the result.\n",{"data":34,"body":35},{"name":4,"description":6},{"type":36,"children":37},"root",[38,47,53,131,138,143,173,179,221,231,282,287,293,298,308,338,351,357,364,413,534,540,560,870,881,886,937,957,970,976,1015,1028,1034,1039,1314,1319,1325,1330],{"type":39,"tag":40,"props":41,"children":43},"element","h1",{"id":42},"build-an-app",[44],{"type":45,"value":46},"text","Build an App",{"type":39,"tag":48,"props":49,"children":50},"p",{},[51],{"type":45,"value":52},"You run on a persistent Tuft machine that outlives the conversation; the user is on their phone and browser. Act like the server:",{"type":39,"tag":54,"props":55,"children":56},"ul",{},[57,95,100,105,126],{"type":39,"tag":58,"props":59,"children":60},"li",{},[61,63,70,72,78,80,85,87,93],{"type":45,"value":62},"Nothing on ",{"type":39,"tag":64,"props":65,"children":67},"code",{"className":66},[],[68],{"type":45,"value":69},"localhost",{"type":45,"value":71}," or the LAN is reachable from their device. Every URL you send must go through ",{"type":39,"tag":64,"props":73,"children":75},{"className":74},[],[76],{"type":45,"value":77},"tuft host",{"type":45,"value":79}," — never ",{"type":39,"tag":64,"props":81,"children":83},{"className":82},[],[84],{"type":45,"value":69},{"type":45,"value":86},", a LAN IP, or ",{"type":39,"tag":64,"props":88,"children":90},{"className":89},[],[91],{"type":45,"value":92},"exp.direct",{"type":45,"value":94},".",{"type":39,"tag":58,"props":96,"children":97},{},[98],{"type":45,"value":99},"Interactive prompts cannot run in your shell. Logins and device registration happen in the user's browser via the Tuft dashboard, never via credentials pasted into chat.",{"type":39,"tag":58,"props":101,"children":102},{},[103],{"type":45,"value":104},"The user has one job: use the app on their phone. Everything else — hosting, tokens, process management, provisioning, broken state — is your job on this machine. Their single hands-on step is the browser sign-in flow behind the Tuft setup link.",{"type":39,"tag":58,"props":106,"children":107},{},[108,110,116,118,124],{"type":45,"value":109},"Answer your own questions before they reach chat: look them up on this machine (",{"type":39,"tag":64,"props":111,"children":113},{"className":112},[],[114],{"type":45,"value":115},"eas whoami",{"type":45,"value":117},", ",{"type":39,"tag":64,"props":119,"children":121},{"className":120},[],[122],{"type":45,"value":123},"eas device:list",{"type":45,"value":125},", the repository itself), or pick a reasonable default, state it in your next update, and keep building. Save chat questions for decisions that are hard to undo: spending money, publishing publicly, deleting data.",{"type":39,"tag":58,"props":127,"children":128},{},[129],{"type":45,"value":130},"Long-running processes belong to the machine, not the turn. Run Metro — and any backend you build — under launchd so it survives session teardown and restarts itself after a crash.",{"type":39,"tag":132,"props":133,"children":135},"h2",{"id":134},"this-machine-is-the-backend",[136],{"type":45,"value":137},"This machine is the backend",{"type":39,"tag":48,"props":139,"children":140},{},[141],{"type":45,"value":142},"You are not limited to client-side apps. This is a full, persistent server under your control, which makes the space of buildable apps effectively unlimited: anything that needs an API, a database, auth, scheduled jobs, file storage, or websockets can get it from software you run right here.",{"type":39,"tag":54,"props":144,"children":145},{},[146,158,163,168],{"type":39,"tag":58,"props":147,"children":148},{},[149,151,156],{"type":45,"value":150},"When the app needs a backend, build the backend: run it on this machine, publish it with ",{"type":39,"tag":64,"props":152,"children":154},{"className":153},[],[155],{"type":45,"value":77},{"type":45,"value":157},", and point the app at that URL. Manage it under launchd like Metro so it outlives the session.",{"type":39,"tag":58,"props":159,"children":160},{},[161],{"type":45,"value":162},"When the app needs a database, create one on this machine — a SQLite file is enough for most apps, and you can install and run Postgres or anything else when it isn't. Same for queues, caches, cron jobs, and file storage.",{"type":39,"tag":58,"props":164,"children":165},{},[166],{"type":45,"value":167},"Never ask the user to sign up for a third-party service (Supabase, Firebase, a hosted database, an auth provider) for a capability this machine can provide. Reach for an external service only when the capability genuinely cannot live here — e.g. payments or carrier SMS — and even then, ask before introducing it.",{"type":39,"tag":58,"props":169,"children":170},{},[171],{"type":45,"value":172},"The first version of anything must work with zero setup from the user: no accounts to create, no API keys to paste, no dashboards to visit. All the user ever sees is their app working.",{"type":39,"tag":132,"props":174,"children":176},{"id":175},"_1-find-or-create-the-project",[177],{"type":45,"value":178},"1. Find or create the project",{"type":39,"tag":48,"props":180,"children":181},{},[182,188,190,196,197,203,205,211,213,219],{"type":39,"tag":183,"props":184,"children":185},"strong",{},[186],{"type":45,"value":187},"Existing project:",{"type":45,"value":189}," locate it — it lives in its own checkout; the Tuft session root is scratch space. Inspect ",{"type":39,"tag":64,"props":191,"children":193},{"className":192},[],[194],{"type":45,"value":195},"package.json",{"type":45,"value":117},{"type":39,"tag":64,"props":198,"children":200},{"className":199},[],[201],{"type":45,"value":202},"app.json",{"type":45,"value":204},"\u002F",{"type":39,"tag":64,"props":206,"children":208},{"className":207},[],[209],{"type":45,"value":210},"app.config.*",{"type":45,"value":212},", and ",{"type":39,"tag":64,"props":214,"children":216},{"className":215},[],[217],{"type":45,"value":218},"eas.json",{"type":45,"value":220},", and use the project's existing package manager.",{"type":39,"tag":48,"props":222,"children":223},{},[224,229],{"type":39,"tag":183,"props":225,"children":226},{},[227],{"type":45,"value":228},"New app idea:",{"type":45,"value":230}," scaffold with Expo — it is the default way to build for the user's phone from this machine:",{"type":39,"tag":232,"props":233,"children":238},"pre",{"className":234,"code":235,"language":236,"meta":237,"style":237},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","npx create-expo-app@latest \u003Capp-name>\n","bash","",[239],{"type":39,"tag":64,"props":240,"children":241},{"__ignoreMap":237},[242],{"type":39,"tag":243,"props":244,"children":247},"span",{"class":245,"line":246},"line",1,[248,254,260,266,271,277],{"type":39,"tag":243,"props":249,"children":251},{"style":250},"--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B",[252],{"type":45,"value":253},"npx",{"type":39,"tag":243,"props":255,"children":257},{"style":256},"--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D",[258],{"type":45,"value":259}," create-expo-app@latest",{"type":39,"tag":243,"props":261,"children":263},{"style":262},"--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF",[264],{"type":45,"value":265}," \u003C",{"type":39,"tag":243,"props":267,"children":268},{"style":256},[269],{"type":45,"value":270},"app-nam",{"type":39,"tag":243,"props":272,"children":274},{"style":273},"--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8",[275],{"type":45,"value":276},"e",{"type":39,"tag":243,"props":278,"children":279},{"style":262},[280],{"type":45,"value":281},">\n",{"type":39,"tag":48,"props":283,"children":284},{},[285],{"type":45,"value":286},"A fresh project runs in Expo Go out of the box, so the fastest first milestone is a hello-world on the user's phone within minutes (sections 2–4), then iterate on the real idea over hot reload. Don't front-load architecture the idea doesn't need yet.",{"type":39,"tag":132,"props":288,"children":290},{"id":289},"_2-decide-expo-go-or-a-development-build",[291],{"type":45,"value":292},"2. Decide: Expo Go or a development build?",{"type":39,"tag":48,"props":294,"children":295},{},[296],{"type":45,"value":297},"Check whether the app actually needs custom native code before committing the user to EAS setup and a 10–20 minute build.",{"type":39,"tag":48,"props":299,"children":300},{},[301,306],{"type":39,"tag":183,"props":302,"children":303},{},[304],{"type":45,"value":305},"Expo Go is enough",{"type":45,"value":307}," when every dependency is either JS-only or an Expo SDK module bundled in Expo Go, and the app needs no custom config plugins with native effects, no custom URL scheme handling at the native layer, and no extra entitlements. If so, skip section 3 entirely — the user just installs Expo Go from the App Store and opens your link. This is the fastest path from idea to phone; prefer it for prototypes and pure JS\u002FUI work.",{"type":39,"tag":48,"props":309,"children":310},{},[311,316,318,323,325,330,331,336],{"type":39,"tag":183,"props":312,"children":313},{},[314],{"type":45,"value":315},"A development build is required",{"type":45,"value":317}," when the project uses native modules outside Expo Go (check ",{"type":39,"tag":64,"props":319,"children":321},{"className":320},[],[322],{"type":45,"value":195},{"type":45,"value":324}," for packages with native code), config plugins in ",{"type":39,"tag":64,"props":326,"children":328},{"className":327},[],[329],{"type":45,"value":202},{"type":45,"value":204},{"type":39,"tag":64,"props":332,"children":334},{"className":333},[],[335],{"type":45,"value":210},{"type":45,"value":337}," that modify the native project, custom entitlements or capabilities, or a custom scheme the flow depends on (e.g. auth callbacks).",{"type":39,"tag":48,"props":339,"children":340},{},[341,343,349],{"type":45,"value":342},"If unsure, ",{"type":39,"tag":64,"props":344,"children":346},{"className":345},[],[347],{"type":45,"value":348},"npx expo-doctor",{"type":45,"value":350}," and the Expo Go compatibility of each dependency settle it.",{"type":39,"tag":132,"props":352,"children":354},{"id":353},"_3-development-build-path-skip-for-expo-go",[355],{"type":45,"value":356},"3. Development build path (skip for Expo Go)",{"type":39,"tag":358,"props":359,"children":361},"h3",{"id":360},"get-eas-set-up-the-user-does-it-in-a-browser",[362],{"type":45,"value":363},"Get EAS set up (the user does it in a browser)",{"type":39,"tag":48,"props":365,"children":366},{},[367,369,375,377,383,384,390,391,397,398,404,405,411],{"type":45,"value":368},"Every account-touching ",{"type":39,"tag":64,"props":370,"children":372},{"className":371},[],[373],{"type":45,"value":374},"eas",{"type":45,"value":376}," command (",{"type":39,"tag":64,"props":378,"children":380},{"className":379},[],[381],{"type":45,"value":382},"build",{"type":45,"value":117},{"type":39,"tag":64,"props":385,"children":387},{"className":386},[],[388],{"type":45,"value":389},"credentials",{"type":45,"value":117},{"type":39,"tag":64,"props":392,"children":394},{"className":393},[],[395],{"type":45,"value":396},"device:*",{"type":45,"value":117},{"type":39,"tag":64,"props":399,"children":401},{"className":400},[],[402],{"type":45,"value":403},"submit",{"type":45,"value":117},{"type":39,"tag":64,"props":406,"children":408},{"className":407},[],[409],{"type":45,"value":410},"update",{"type":45,"value":412},") needs EAS CLI on this machine to be logged in.",{"type":39,"tag":414,"props":415,"children":416},"ol",{},[417,429,466,522],{"type":39,"tag":58,"props":418,"children":419},{},[420,422,427],{"type":45,"value":421},"Check non-interactively: ",{"type":39,"tag":64,"props":423,"children":425},{"className":424},[],[426],{"type":45,"value":115},{"type":45,"value":428},". Success prints the account.",{"type":39,"tag":58,"props":430,"children":431},{},[432,434,440,442,448,450,460,464],{"type":45,"value":433},"If not logged in, never run ",{"type":39,"tag":64,"props":435,"children":437},{"className":436},[],[438],{"type":45,"value":439},"eas login",{"type":45,"value":441}," or ",{"type":39,"tag":64,"props":443,"children":445},{"className":444},[],[446],{"type":45,"value":447},"eas device:create",{"type":45,"value":449}," (they prompt interactively and hang) and never ask for Expo or Apple credentials in chat. Send the user:",{"type":39,"tag":232,"props":451,"children":455},{"className":452,"code":454,"language":45,"meta":237},[453],"language-text","https:\u002F\u002Fdash.tuft.dev\u002Fexpo\u002Fsetup\n",[456],{"type":39,"tag":64,"props":457,"children":458},{"__ignoreMap":237},[459],{"type":45,"value":454},{"type":39,"tag":461,"props":462,"children":463},"br",{},[],{"type":45,"value":465},"The flow runs in their browser against this machine: sign EAS CLI in, connect Apple Developer, register their iPhone by UDID. This browser flow is the long pole of the whole session — send the link in your first message, before writing any code.",{"type":39,"tag":58,"props":467,"children":468},{},[469,471,517,520],{"type":45,"value":470},"Wait in the background, bounded, instead of blocking the conversation:",{"type":39,"tag":232,"props":472,"children":474},{"className":234,"code":473,"language":236,"meta":237,"style":237},"timeout 300 bash -c 'until eas whoami >\u002Fdev\u002Fnull 2>&1; do sleep 10; done'\n",[475],{"type":39,"tag":64,"props":476,"children":477},{"__ignoreMap":237},[478],{"type":39,"tag":243,"props":479,"children":480},{"class":245,"line":246},[481,486,492,497,502,507,512],{"type":39,"tag":243,"props":482,"children":483},{"style":250},[484],{"type":45,"value":485},"timeout",{"type":39,"tag":243,"props":487,"children":489},{"style":488},"--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C",[490],{"type":45,"value":491}," 300",{"type":39,"tag":243,"props":493,"children":494},{"style":256},[495],{"type":45,"value":496}," bash",{"type":39,"tag":243,"props":498,"children":499},{"style":256},[500],{"type":45,"value":501}," -c",{"type":39,"tag":243,"props":503,"children":504},{"style":262},[505],{"type":45,"value":506}," '",{"type":39,"tag":243,"props":508,"children":509},{"style":256},[510],{"type":45,"value":511},"until eas whoami >\u002Fdev\u002Fnull 2>&1; do sleep 10; done",{"type":39,"tag":243,"props":513,"children":514},{"style":262},[515],{"type":45,"value":516},"'\n",{"type":39,"tag":461,"props":518,"children":519},{},[],{"type":45,"value":521},"Exit 124 means they haven't finished — follow up in chat rather than looping forever.",{"type":39,"tag":58,"props":523,"children":524},{},[525,527,532],{"type":45,"value":526},"Before an iOS device build, also require ",{"type":39,"tag":64,"props":528,"children":530},{"className":529},[],[531],{"type":45,"value":123},{"type":45,"value":533}," to be non-empty; if it isn't, point back at the same setup link.",{"type":39,"tag":358,"props":535,"children":537},{"id":536},"front-load-native-modules-then-build-while-you-write-js",[538],{"type":45,"value":539},"Front-load native modules, then build while you write JS",{"type":39,"tag":48,"props":541,"children":542},{},[543,545,551,553,558],{"type":45,"value":544},"Each native change costs another full EAS build, so decide the native surface up front: install every native module the feature will plausibly need (",{"type":39,"tag":64,"props":546,"children":548},{"className":547},[],[549],{"type":45,"value":550},"npx expo install \u003Cpackage>",{"type":45,"value":552}," so versions match the SDK) ",{"type":39,"tag":183,"props":554,"children":555},{},[556],{"type":45,"value":557},"before",{"type":45,"value":559}," kicking off the build. A development build encodes only the native project — JavaScript loads from Metro afterward — so a build started now is exactly as current as one started after the features exist.",{"type":39,"tag":414,"props":561,"children":562},{},[563,724,855,865],{"type":39,"tag":58,"props":564,"children":565},{},[566,568,573,575],{"type":45,"value":567},"Ensure ",{"type":39,"tag":64,"props":569,"children":571},{"className":570},[],[572],{"type":45,"value":218},{"type":45,"value":574}," has a development profile; create a minimal one if missing:",{"type":39,"tag":232,"props":576,"children":580},{"className":577,"code":578,"language":579,"meta":237,"style":237},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"build\": {\n    \"development\": { \"developmentClient\": true, \"distribution\": \"internal\" }\n  }\n}\n","json",[581],{"type":39,"tag":64,"props":582,"children":583},{"__ignoreMap":237},[584,592,621,706,715],{"type":39,"tag":243,"props":585,"children":586},{"class":245,"line":246},[587],{"type":39,"tag":243,"props":588,"children":589},{"style":262},[590],{"type":45,"value":591},"{\n",{"type":39,"tag":243,"props":593,"children":595},{"class":245,"line":594},2,[596,601,606,611,616],{"type":39,"tag":243,"props":597,"children":598},{"style":262},[599],{"type":45,"value":600},"  \"",{"type":39,"tag":243,"props":602,"children":604},{"style":603},"--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA",[605],{"type":45,"value":382},{"type":39,"tag":243,"props":607,"children":608},{"style":262},[609],{"type":45,"value":610},"\"",{"type":39,"tag":243,"props":612,"children":613},{"style":262},[614],{"type":45,"value":615},":",{"type":39,"tag":243,"props":617,"children":618},{"style":262},[619],{"type":45,"value":620}," {\n",{"type":39,"tag":243,"props":622,"children":624},{"class":245,"line":623},3,[625,630,635,639,643,648,653,658,662,666,671,675,680,684,688,692,697,701],{"type":39,"tag":243,"props":626,"children":627},{"style":262},[628],{"type":45,"value":629},"    \"",{"type":39,"tag":243,"props":631,"children":632},{"style":250},[633],{"type":45,"value":634},"development",{"type":39,"tag":243,"props":636,"children":637},{"style":262},[638],{"type":45,"value":610},{"type":39,"tag":243,"props":640,"children":641},{"style":262},[642],{"type":45,"value":615},{"type":39,"tag":243,"props":644,"children":645},{"style":262},[646],{"type":45,"value":647}," {",{"type":39,"tag":243,"props":649,"children":650},{"style":262},[651],{"type":45,"value":652}," \"",{"type":39,"tag":243,"props":654,"children":655},{"style":488},[656],{"type":45,"value":657},"developmentClient",{"type":39,"tag":243,"props":659,"children":660},{"style":262},[661],{"type":45,"value":610},{"type":39,"tag":243,"props":663,"children":664},{"style":262},[665],{"type":45,"value":615},{"type":39,"tag":243,"props":667,"children":668},{"style":262},[669],{"type":45,"value":670}," true,",{"type":39,"tag":243,"props":672,"children":673},{"style":262},[674],{"type":45,"value":652},{"type":39,"tag":243,"props":676,"children":677},{"style":488},[678],{"type":45,"value":679},"distribution",{"type":39,"tag":243,"props":681,"children":682},{"style":262},[683],{"type":45,"value":610},{"type":39,"tag":243,"props":685,"children":686},{"style":262},[687],{"type":45,"value":615},{"type":39,"tag":243,"props":689,"children":690},{"style":262},[691],{"type":45,"value":652},{"type":39,"tag":243,"props":693,"children":694},{"style":256},[695],{"type":45,"value":696},"internal",{"type":39,"tag":243,"props":698,"children":699},{"style":262},[700],{"type":45,"value":610},{"type":39,"tag":243,"props":702,"children":703},{"style":262},[704],{"type":45,"value":705}," }\n",{"type":39,"tag":243,"props":707,"children":709},{"class":245,"line":708},4,[710],{"type":39,"tag":243,"props":711,"children":712},{"style":262},[713],{"type":45,"value":714},"  }\n",{"type":39,"tag":243,"props":716,"children":718},{"class":245,"line":717},5,[719],{"type":39,"tag":243,"props":720,"children":721},{"style":262},[722],{"type":45,"value":723},"}\n",{"type":39,"tag":58,"props":725,"children":726},{},[727,729,735,737,827,830,832,837,839,845,847,853],{"type":45,"value":728},"Link the project and start the build without prompts — on a project not yet linked to EAS, a bare ",{"type":39,"tag":64,"props":730,"children":732},{"className":731},[],[733],{"type":45,"value":734},"eas build",{"type":45,"value":736}," stops to ask which account should own the project, which hangs this machine's non-interactive shell:",{"type":39,"tag":232,"props":738,"children":740},{"className":234,"code":739,"language":236,"meta":237,"style":237},"eas init --account \"$(eas whoami)\" --non-interactive\neas build --profile development --platform ios --non-interactive --no-wait\n",[741],{"type":39,"tag":64,"props":742,"children":743},{"__ignoreMap":237},[744,785],{"type":39,"tag":243,"props":745,"children":746},{"class":245,"line":246},[747,751,756,761,766,770,775,780],{"type":39,"tag":243,"props":748,"children":749},{"style":250},[750],{"type":45,"value":374},{"type":39,"tag":243,"props":752,"children":753},{"style":256},[754],{"type":45,"value":755}," init",{"type":39,"tag":243,"props":757,"children":758},{"style":256},[759],{"type":45,"value":760}," --account",{"type":39,"tag":243,"props":762,"children":763},{"style":262},[764],{"type":45,"value":765}," \"$(",{"type":39,"tag":243,"props":767,"children":768},{"style":250},[769],{"type":45,"value":374},{"type":39,"tag":243,"props":771,"children":772},{"style":256},[773],{"type":45,"value":774}," whoami",{"type":39,"tag":243,"props":776,"children":777},{"style":262},[778],{"type":45,"value":779},")\"",{"type":39,"tag":243,"props":781,"children":782},{"style":256},[783],{"type":45,"value":784}," --non-interactive\n",{"type":39,"tag":243,"props":786,"children":787},{"class":245,"line":594},[788,792,797,802,807,812,817,822],{"type":39,"tag":243,"props":789,"children":790},{"style":250},[791],{"type":45,"value":374},{"type":39,"tag":243,"props":793,"children":794},{"style":256},[795],{"type":45,"value":796}," build",{"type":39,"tag":243,"props":798,"children":799},{"style":256},[800],{"type":45,"value":801}," --profile",{"type":39,"tag":243,"props":803,"children":804},{"style":256},[805],{"type":45,"value":806}," development",{"type":39,"tag":243,"props":808,"children":809},{"style":256},[810],{"type":45,"value":811}," --platform",{"type":39,"tag":243,"props":813,"children":814},{"style":256},[815],{"type":45,"value":816}," ios",{"type":39,"tag":243,"props":818,"children":819},{"style":256},[820],{"type":45,"value":821}," --non-interactive",{"type":39,"tag":243,"props":823,"children":824},{"style":256},[825],{"type":45,"value":826}," --no-wait\n",{"type":39,"tag":461,"props":828,"children":829},{},[],{"type":45,"value":831},"(Platform from ",{"type":39,"tag":64,"props":833,"children":835},{"className":834},[],[836],{"type":45,"value":123},{"type":45,"value":838},"; pass a different ",{"type":39,"tag":64,"props":840,"children":842},{"className":841},[],[843],{"type":45,"value":844},"--account",{"type":45,"value":846}," only when the project belongs to an org.) The build runs on EAS servers, not this machine; ",{"type":39,"tag":64,"props":848,"children":850},{"className":849},[],[851],{"type":45,"value":852},"--no-wait",{"type":45,"value":854}," returns immediately so you can keep working. Internal distribution signs against the registered device list; EAS manages certificates and provisioning remotely — leave signing entirely to it. If the non-interactive build fails on missing credentials, the Tuft setup flow hasn't finished its Apple Developer step — send the setup link again rather than retrying interactively.",{"type":39,"tag":58,"props":856,"children":857},{},[858,863],{"type":39,"tag":183,"props":859,"children":860},{},[861],{"type":45,"value":862},"While the build runs, write the JS\u002FUI.",{"type":45,"value":864}," The build takes 10–20 minutes; that is your window to implement the feature, get Metro up (section 4), and have everything ready the moment the client installs.",{"type":39,"tag":58,"props":866,"children":867},{},[868],{"type":45,"value":869},"When it finishes, send the user the build page link — it shows the install QR for their iPhone. After the dev client is installed once, iterate over Metro; rebuild only for native changes (new native modules, config plugins, entitlements, schemes) — never for ordinary JS\u002FUI changes.",{"type":39,"tag":132,"props":871,"children":873},{"id":872},"_4-start-metro-behind-tuft-host",[874,876],{"type":45,"value":875},"4. Start Metro behind ",{"type":39,"tag":64,"props":877,"children":879},{"className":878},[],[880],{"type":45,"value":77},{"type":39,"tag":48,"props":882,"children":883},{},[884],{"type":45,"value":885},"Run the bundled script — it performs the whole sequence in the right order and verifies every layer:",{"type":39,"tag":232,"props":887,"children":889},{"className":234,"code":888,"language":236,"meta":237,"style":237},"scripts\u002Frun-metro.sh \u003Cproject-dir> \u003Cstable-project-name>\n",[890],{"type":39,"tag":64,"props":891,"children":892},{"__ignoreMap":237},[893],{"type":39,"tag":243,"props":894,"children":895},{"class":245,"line":246},[896,901,905,910,915,920,924,929,933],{"type":39,"tag":243,"props":897,"children":898},{"style":250},[899],{"type":45,"value":900},"scripts\u002Frun-metro.sh",{"type":39,"tag":243,"props":902,"children":903},{"style":262},[904],{"type":45,"value":265},{"type":39,"tag":243,"props":906,"children":907},{"style":256},[908],{"type":45,"value":909},"project-di",{"type":39,"tag":243,"props":911,"children":912},{"style":273},[913],{"type":45,"value":914},"r",{"type":39,"tag":243,"props":916,"children":917},{"style":262},[918],{"type":45,"value":919},">",{"type":39,"tag":243,"props":921,"children":922},{"style":262},[923],{"type":45,"value":265},{"type":39,"tag":243,"props":925,"children":926},{"style":256},[927],{"type":45,"value":928},"stable-project-nam",{"type":39,"tag":243,"props":930,"children":931},{"style":273},[932],{"type":45,"value":276},{"type":39,"tag":243,"props":934,"children":935},{"style":262},[936],{"type":45,"value":281},{"type":39,"tag":48,"props":938,"children":939},{},[940,942,947,949,955],{"type":45,"value":941},"The ordering it encodes: publish the port through ",{"type":39,"tag":64,"props":943,"children":945},{"className":944},[],[946],{"type":45,"value":77},{"type":45,"value":948}," first, because Metro embeds ",{"type":39,"tag":64,"props":950,"children":952},{"className":951},[],[953],{"type":45,"value":954},"EXPO_PACKAGER_PROXY_URL",{"type":45,"value":956}," in every manifest and bundle URL it serves — the public URL has to exist before Metro starts. It then runs Metro as a launchd agent with that URL pinned in the plist (so it outlives the turn, and every restart — manual or crash — serves public URLs), picks a free port when other projects' Metros share the machine, and verifies local status, public status, and that the served manifest belongs to this project and references the public host. It prints the public URL, the dev-client deep link, the log path, and the restart\u002Fstop commands.",{"type":39,"tag":48,"props":958,"children":959},{},[960,962,968],{"type":45,"value":961},"Reuse the same name on later turns: the script is idempotent, keeping the existing binding and port and re-verifying the full path. Environment the bundle needs at runtime (telemetry values, ",{"type":39,"tag":64,"props":963,"children":965},{"className":964},[],[966],{"type":45,"value":967},"EXPO_PUBLIC_*",{"type":45,"value":969},") lives in the project's env files, which Metro reads on every restart.",{"type":39,"tag":132,"props":971,"children":973},{"id":972},"_5-send-the-user-the-link",[974],{"type":45,"value":975},"5. Send the user the link",{"type":39,"tag":54,"props":977,"children":978},{},[979,997],{"type":39,"tag":58,"props":980,"children":981},{},[982,987,989,995],{"type":39,"tag":183,"props":983,"children":984},{},[985],{"type":45,"value":986},"Expo Go:",{"type":45,"value":988}," the same Metro host over the Expo Go scheme: ",{"type":39,"tag":64,"props":990,"children":992},{"className":991},[],[993],{"type":45,"value":994},"exp:\u002F\u002F\u003Cname>.tuft.host",{"type":45,"value":996}," (the script's dev-client deep link doesn't apply on this path).",{"type":39,"tag":58,"props":998,"children":999},{},[1000,1005,1007,1013],{"type":39,"tag":183,"props":1001,"children":1002},{},[1003],{"type":45,"value":1004},"Dev client:",{"type":45,"value":1006}," both links the script prints — the clickable ",{"type":39,"tag":64,"props":1008,"children":1010},{"className":1009},[],[1011],{"type":45,"value":1012},"\u003Cscheme>:\u002F\u002Fexpo-development-client\u002F?url=…",{"type":45,"value":1014}," deep link and the plain HTTPS Metro URL. The scheme comes from Expo config; never invent it.",{"type":39,"tag":48,"props":1016,"children":1017},{},[1018,1020,1026],{"type":45,"value":1019},"If the device sticks on \"Loading from Metro,\" re-run the script — it restarts Metro and re-verifies every layer, and its first failing check names the broken one. For a wedged bundle, read the log at the path the script printed, then ",{"type":39,"tag":64,"props":1021,"children":1023},{"className":1022},[],[1024],{"type":45,"value":1025},"launchctl kickstart -k",{"type":45,"value":1027}," and re-verify before resending the same stable link. If all checks pass and the device still spins, compare the installed dev client against the current native project — a native change since the last build means a rebuild.",{"type":39,"tag":132,"props":1029,"children":1031},{"id":1030},"instrument-with-tuft-telemetry",[1032],{"type":45,"value":1033},"Instrument with Tuft telemetry",{"type":39,"tag":48,"props":1035,"children":1036},{},[1037],{"type":45,"value":1038},"Tuft telemetry is a per-machine event store: the app posts small structured events to this machine's collector and you query them with SQL. Instrument up front, not once a bug appears — querying an event that already fired beats asking the user to reproduce.",{"type":39,"tag":414,"props":1040,"children":1041},{},[1042,1071,1084,1097,1153,1248,1309],{"type":39,"tag":58,"props":1043,"children":1044},{},[1045,1047,1053,1055,1061,1063,1069],{"type":45,"value":1046},"Provision with the ",{"type":39,"tag":64,"props":1048,"children":1050},{"className":1049},[],[1051],{"type":45,"value":1052},"setup_expo_telemetry",{"type":45,"value":1054}," MCP tool, passing the project's absolute path. It returns the collector URL and (for a new stream) a write-only token as ready-made ",{"type":39,"tag":64,"props":1056,"children":1058},{"className":1057},[],[1059],{"type":45,"value":1060},"EXPO_PUBLIC_TUFT_TELEMETRY_*",{"type":45,"value":1062}," env values. Pass ",{"type":39,"tag":64,"props":1064,"children":1066},{"className":1065},[],[1067],{"type":45,"value":1068},"rotate: true",{"type":45,"value":1070}," only to deliberately revoke the token.",{"type":39,"tag":58,"props":1072,"children":1073},{},[1074,1076,1082],{"type":45,"value":1075},"Put the values in an uncommitted env file (e.g. ",{"type":39,"tag":64,"props":1077,"children":1079},{"className":1078},[],[1080],{"type":45,"value":1081},".env.local",{"type":45,"value":1083},"). That file is the only place the token — like any credential — ever appears.",{"type":39,"tag":58,"props":1085,"children":1086},{},[1087,1089,1095],{"type":45,"value":1088},"Install the ",{"type":39,"tag":64,"props":1090,"children":1092},{"className":1091},[],[1093],{"type":45,"value":1094},"tuft-telemetry",{"type":45,"value":1096}," npm package and initialize it once near the app root with those env values.",{"type":39,"tag":58,"props":1098,"children":1099},{},[1100,1102,1108,1109,1115,1116,1122,1124,1130,1131,1137,1138,1144,1146,1152],{"type":45,"value":1101},"Emit small structured events at decision boundaries: screen\u002Fflow opened, request started\u002Fcompleted\u002Ffailed (host and status, not full sensitive URLs), navigation allowed\u002Fblocked, hydration outcome. Events carry ",{"type":39,"tag":64,"props":1103,"children":1105},{"className":1104},[],[1106],{"type":45,"value":1107},"kind",{"type":45,"value":117},{"type":39,"tag":64,"props":1110,"children":1112},{"className":1111},[],[1113],{"type":45,"value":1114},"level",{"type":45,"value":117},{"type":39,"tag":64,"props":1117,"children":1119},{"className":1118},[],[1120],{"type":45,"value":1121},"name",{"type":45,"value":1123},", optional ",{"type":39,"tag":64,"props":1125,"children":1127},{"className":1126},[],[1128],{"type":45,"value":1129},"route",{"type":45,"value":117},{"type":39,"tag":64,"props":1132,"children":1134},{"className":1133},[],[1135],{"type":45,"value":1136},"trace_id",{"type":45,"value":204},{"type":39,"tag":64,"props":1139,"children":1141},{"className":1140},[],[1142],{"type":45,"value":1143},"span_id",{"type":45,"value":1145},", and a ",{"type":39,"tag":64,"props":1147,"children":1149},{"className":1148},[],[1150],{"type":45,"value":1151},"payload",{"type":45,"value":94},{"type":39,"tag":58,"props":1154,"children":1155},{},[1156,1158,1164,1166,1172,1174,1180,1181],{"type":45,"value":1157},"Query with the ",{"type":39,"tag":64,"props":1159,"children":1161},{"className":1160},[],[1162],{"type":45,"value":1163},"search_telemetry",{"type":45,"value":1165}," MCP tool — one bounded read-only SQLite statement over ",{"type":39,"tag":64,"props":1167,"children":1169},{"className":1168},[],[1170],{"type":45,"value":1171},"streams(id, name, project_identity, created_at, last_seen_at)",{"type":45,"value":1173}," and ",{"type":39,"tag":64,"props":1175,"children":1177},{"className":1176},[],[1178],{"type":45,"value":1179},"events(id, stream_id, event_id, received_at, occurred_at, launch_id, sequence, kind, level, name, trace_id, span_id, route, update_id, schema_version, payload)",{"type":45,"value":615},{"type":39,"tag":232,"props":1182,"children":1186},{"className":1183,"code":1184,"language":1185,"meta":237,"style":237},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- Recent errors\nSELECT occurred_at, name, payload FROM events\nWHERE level = 'error' ORDER BY id DESC LIMIT 50;\n\n-- One flow, in order\nSELECT occurred_at, name, payload FROM events\nWHERE trace_id = '…' ORDER BY sequence;\n","sql",[1187],{"type":39,"tag":64,"props":1188,"children":1189},{"__ignoreMap":237},[1190,1198,1206,1214,1223,1231,1239],{"type":39,"tag":243,"props":1191,"children":1192},{"class":245,"line":246},[1193],{"type":39,"tag":243,"props":1194,"children":1195},{},[1196],{"type":45,"value":1197},"-- Recent errors\n",{"type":39,"tag":243,"props":1199,"children":1200},{"class":245,"line":594},[1201],{"type":39,"tag":243,"props":1202,"children":1203},{},[1204],{"type":45,"value":1205},"SELECT occurred_at, name, payload FROM events\n",{"type":39,"tag":243,"props":1207,"children":1208},{"class":245,"line":623},[1209],{"type":39,"tag":243,"props":1210,"children":1211},{},[1212],{"type":45,"value":1213},"WHERE level = 'error' ORDER BY id DESC LIMIT 50;\n",{"type":39,"tag":243,"props":1215,"children":1216},{"class":245,"line":708},[1217],{"type":39,"tag":243,"props":1218,"children":1220},{"emptyLinePlaceholder":1219},true,[1221],{"type":45,"value":1222},"\n",{"type":39,"tag":243,"props":1224,"children":1225},{"class":245,"line":717},[1226],{"type":39,"tag":243,"props":1227,"children":1228},{},[1229],{"type":45,"value":1230},"-- One flow, in order\n",{"type":39,"tag":243,"props":1232,"children":1234},{"class":245,"line":1233},6,[1235],{"type":39,"tag":243,"props":1236,"children":1237},{},[1238],{"type":45,"value":1205},{"type":39,"tag":243,"props":1240,"children":1242},{"class":245,"line":1241},7,[1243],{"type":39,"tag":243,"props":1244,"children":1245},{},[1246],{"type":45,"value":1247},"WHERE trace_id = '…' ORDER BY sequence;\n",{"type":39,"tag":58,"props":1249,"children":1250},{},[1251,1253,1304,1307],{"type":45,"value":1252},"For agent-driven reproduction, arm a wait before the user triggers the action:",{"type":39,"tag":232,"props":1254,"children":1256},{"className":234,"code":1255,"language":236,"meta":237,"style":237},"npx tuft-telemetry wait \u003Cevent-name> --timeout 30s\n",[1257],{"type":39,"tag":64,"props":1258,"children":1259},{"__ignoreMap":237},[1260],{"type":39,"tag":243,"props":1261,"children":1262},{"class":245,"line":246},[1263,1267,1272,1277,1281,1286,1290,1294,1299],{"type":39,"tag":243,"props":1264,"children":1265},{"style":250},[1266],{"type":45,"value":253},{"type":39,"tag":243,"props":1268,"children":1269},{"style":256},[1270],{"type":45,"value":1271}," tuft-telemetry",{"type":39,"tag":243,"props":1273,"children":1274},{"style":256},[1275],{"type":45,"value":1276}," wait",{"type":39,"tag":243,"props":1278,"children":1279},{"style":262},[1280],{"type":45,"value":265},{"type":39,"tag":243,"props":1282,"children":1283},{"style":256},[1284],{"type":45,"value":1285},"event-nam",{"type":39,"tag":243,"props":1287,"children":1288},{"style":273},[1289],{"type":45,"value":276},{"type":39,"tag":243,"props":1291,"children":1292},{"style":262},[1293],{"type":45,"value":919},{"type":39,"tag":243,"props":1295,"children":1296},{"style":256},[1297],{"type":45,"value":1298}," --timeout",{"type":39,"tag":243,"props":1300,"children":1301},{"style":256},[1302],{"type":45,"value":1303}," 30s\n",{"type":39,"tag":461,"props":1305,"children":1306},{},[],{"type":45,"value":1308},"It exits the moment the event arrives (timeout exits 124), so you confirm the event actually fired.",{"type":39,"tag":58,"props":1310,"children":1311},{},[1312],{"type":45,"value":1313},"On a bug report, query telemetry before changing code; after a device-dependent fix, confirm with fresh telemetry once the user retries.",{"type":39,"tag":48,"props":1315,"children":1316},{},[1317],{"type":45,"value":1318},"Treat telemetry payloads and HAR files as sensitive: extract the minimum fields and keep any secrets they contain inside the store they came from.",{"type":39,"tag":132,"props":1320,"children":1322},{"id":1321},"hand-off",[1323],{"type":45,"value":1324},"Hand off",{"type":39,"tag":48,"props":1326,"children":1327},{},[1328],{"type":45,"value":1329},"Keep updates short and lead with the outcome. Include the clickable link (Expo Go URL, dev-client link, or EAS build install page); label anything short of the real deliverable — a web preview while the build finishes — as an interim, and say where the build stands. If EAS setup is pending, restate the single setup link and which step the user is on — never a credential request. Say whether a native rebuild was needed, and if you're waiting on a device retry, say exactly what to tap and that you're watching telemetry for the result.",{"type":39,"tag":1331,"props":1332,"children":1333},"style",{},[1334],{"type":45,"value":1335},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"items":1337,"total":1513},[1338,1358,1373,1386,1400,1414,1432,1443,1458,1476,1491,1502],{"slug":1339,"name":1339,"fn":1340,"description":1341,"org":1342,"tags":1343,"stars":1355,"repoUrl":1356,"updatedAt":1357},"eas-app-stores","deploy and submit Expo apps to stores","EAS service (paid). Deploy Expo apps to the app stores with EAS - build and submit to the iOS App Store, Google Play Store, and TestFlight, configure eas.json build and submit profiles, manage app versions and build numbers, and publish App Store metadata and ASO. Use whenever the user wants to deploy, release, or ship an app to production or the app stores, is preparing a production build, running eas build or eas submit, shipping to TestFlight, bumping version or build numbers, or setting up store listing metadata. For deploying an Expo website or API routes, use the eas-hosting skill.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1344,1347,1350,1351,1354],{"name":1345,"slug":1346,"type":15},"Android","android",{"name":1348,"slug":1349,"type":15},"Deployment","deployment",{"name":9,"slug":8,"type":15},{"name":1352,"slug":1353,"type":15},"iOS","ios",{"name":18,"slug":19,"type":15},2190,"https:\u002F\u002Fgithub.com\u002Fexpo\u002Fskills","2026-08-06T05:38:27.496958",{"slug":1359,"name":1359,"fn":1360,"description":1361,"org":1362,"tags":1363,"stars":1355,"repoUrl":1356,"updatedAt":1372},"eas-hosting","deploy Expo websites to EAS Hosting","EAS service (paid). Deploy Expo websites and Expo Router API routes to EAS Hosting - export the web bundle, run eas deploy for production and PR preview URLs, manage environment secrets and custom domains, and work within the Cloudflare Workers runtime. Also covers authoring API routes (+api.ts handlers, HTTP methods, request handling, CORS). Use when deploying an Expo web app or API routes, setting up EAS Hosting, or configuring hosting environments and domains. Not for native builds or store releases - use the eas-app-stores skill for those.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1364,1365,1366,1369],{"name":1348,"slug":1349,"type":15},{"name":9,"slug":8,"type":15},{"name":1367,"slug":1368,"type":15},"Frontend","frontend",{"name":1370,"slug":1371,"type":15},"Web Development","web-development","2026-08-06T05:38:23.520822",{"slug":1374,"name":1374,"fn":1375,"description":1376,"org":1377,"tags":1378,"stars":1355,"repoUrl":1356,"updatedAt":1385},"eas-observe","configure EAS Observe for Expo apps","EAS service (paid). Use for anything related to EAS Observe - adding `expo-observe` to an Expo project (AppMetricsRoot\u002FObserveRoot HOC, markInteractive, the useObserve hook, the Expo Router \u002F React Navigation integrations for per-route metrics, and user-defined events via `Observe.logEvent`), querying via the EAS CLI (`eas observe:metrics-summary`, `observe:metrics`, `observe:routes`, `observe:events`, `observe:versions`), or interpreting the resulting metrics (cold\u002Fwarm launch, TTR, TTI, navigation cold\u002Fwarm TTR, update download, and the TTI frameRate params for triaging slow startups).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1379,1380,1381,1384],{"name":9,"slug":8,"type":15},{"name":18,"slug":19,"type":15},{"name":1382,"slug":1383,"type":15},"Observability","observability",{"name":13,"slug":14,"type":15},"2026-08-06T05:38:28.514029",{"slug":1387,"name":1387,"fn":1388,"description":1389,"org":1390,"tags":1391,"stars":1355,"repoUrl":1356,"updatedAt":1399},"eas-simulator","run and control remote iOS and Android simulators","EAS service (paid). Run and control a user's app on a remote iOS\u002FAndroid simulator hosted on EAS cloud. Read before running any `eas simulator:*` commands - it has the current syntax for this experimental API. Use whenever the user needs a simulator they can't run locally - 'run my app on a cloud simulator', 'use eas simulator to run\u002Finstall\u002Fscreenshot my app', 'I'm on Linux\u002FCursor and need an iOS device', 'no sim on this box \u002F headless CI', 'let an agent click through my app and screenshot it', 'test my dev build on a remote sim with live reload', 'stream a sim to my browser' - even when they don't say 'EAS Simulator' or 'cloud'. On a host WITHOUT a local simulator (Linux, CI, cloud sandbox) it's the default; on macOS, do NOT auto-trigger for a plain 'run on the simulator' - use it only for a cloud\u002Fremote\u002Fshareable sim, an iOS version they lack, or an agent-driven session. NOT for local sims (expo run:ios, Xcode, Android Studio), EAS Build\u002FUpdate, web preview, or physical devices.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1392,1393,1396,1397,1398],{"name":1345,"slug":1346,"type":15},{"name":1394,"slug":1395,"type":15},"CLI","cli",{"name":9,"slug":8,"type":15},{"name":1352,"slug":1353,"type":15},{"name":18,"slug":19,"type":15},"2026-08-06T05:38:35.510759",{"slug":1401,"name":1401,"fn":1402,"description":1403,"org":1404,"tags":1405,"stars":1355,"repoUrl":1356,"updatedAt":1413},"eas-update-insights","check health of EAS Updates","EAS service (paid). Check the health of published EAS Update: crash rates, install\u002Flaunch counts, unique users, payload size, and the split between embedded and OTA users per channel. Use when the user asks how an update is performing, whether a rollout is healthy, how many users are on the embedded build vs OTA, or wants to gate CI on update health.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1406,1409,1410,1411,1412],{"name":1407,"slug":1408,"type":15},"Analytics","analytics",{"name":1348,"slug":1349,"type":15},{"name":9,"slug":8,"type":15},{"name":18,"slug":19,"type":15},{"name":1382,"slug":1383,"type":15},"2026-08-06T05:38:33.530661",{"slug":1415,"name":1415,"fn":1416,"description":1417,"org":1418,"tags":1419,"stars":1355,"repoUrl":1356,"updatedAt":1431},"eas-workflows","configure EAS workflows for Expo projects","EAS service (paid). Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI\u002FCD or workflows in an Expo or EAS context, mentions .eas\u002Fworkflows\u002F, or wants help with EAS build pipelines or deployment automation.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1420,1423,1426,1427,1428],{"name":1421,"slug":1422,"type":15},"Automation","automation",{"name":1424,"slug":1425,"type":15},"CI\u002FCD","ci-cd",{"name":1348,"slug":1349,"type":15},{"name":9,"slug":8,"type":15},{"name":1429,"slug":1430,"type":15},"YAML","yaml","2026-08-06T05:38:26.496896",{"slug":1433,"name":1433,"fn":1434,"description":1435,"org":1436,"tags":1437,"stars":1355,"repoUrl":1356,"updatedAt":1442},"expo-app-clip","add iOS App Clip targets to Expo","Framework (OSS). Add an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1438,1439,1440,1441],{"name":9,"slug":8,"type":15},{"name":1352,"slug":1353,"type":15},{"name":18,"slug":19,"type":15},{"name":13,"slug":14,"type":15},"2026-08-06T05:38:29.493721",{"slug":1444,"name":1444,"fn":1445,"description":1446,"org":1447,"tags":1448,"stars":1355,"repoUrl":1356,"updatedAt":1457},"expo-brownfield","integrate Expo and React Native into native apps","Framework (OSS). Integrate Expo and React Native into an existing native iOS or Android app. Use when the user mentions brownfield, embedding React Native in a native app, AAR\u002FXCFramework, or adding Expo to an existing Kotlin\u002FSwift project. Covers both the isolated approach and the integrated approach.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1449,1450,1451,1454,1455,1456],{"name":1345,"slug":1346,"type":15},{"name":9,"slug":8,"type":15},{"name":1452,"slug":1453,"type":15},"Integrations","integrations",{"name":1352,"slug":1353,"type":15},{"name":18,"slug":19,"type":15},{"name":13,"slug":14,"type":15},"2026-08-06T05:38:22.505099",{"slug":1459,"name":1459,"fn":1460,"description":1461,"org":1462,"tags":1463,"stars":1355,"repoUrl":1356,"updatedAt":1475},"expo-data-fetching","fetch and manage data in Expo apps","Framework (OSS). Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1464,1467,1470,1471,1472],{"name":1465,"slug":1466,"type":15},"API Development","api-development",{"name":1468,"slug":1469,"type":15},"Caching","caching",{"name":9,"slug":8,"type":15},{"name":18,"slug":19,"type":15},{"name":1473,"slug":1474,"type":15},"React","react","2026-08-06T05:38:25.502479",{"slug":1477,"name":1477,"fn":1478,"description":1479,"org":1480,"tags":1481,"stars":1355,"repoUrl":1356,"updatedAt":1490},"expo-dev-client","build and distribute Expo development clients","Framework (OSS). Build and distribute Expo development clients locally or via TestFlight for internal testing. For production TestFlight releases and store submission, use the eas-app-stores skill.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1482,1483,1484,1485,1486,1489],{"name":1345,"slug":1346,"type":15},{"name":1348,"slug":1349,"type":15},{"name":9,"slug":8,"type":15},{"name":1352,"slug":1353,"type":15},{"name":1487,"slug":1488,"type":15},"Local Development","local-development",{"name":18,"slug":19,"type":15},"2026-08-06T05:38:34.517484",{"slug":1492,"name":1492,"fn":1493,"description":1494,"org":1495,"tags":1496,"stars":1355,"repoUrl":1356,"updatedAt":1501},"expo-dom","run web components in native apps","Framework (OSS). Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally. For the end-to-end migration of a whole web app, use the expo-web-to-native skill.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1497,1498,1499,1500],{"name":9,"slug":8,"type":15},{"name":1367,"slug":1368,"type":15},{"name":18,"slug":19,"type":15},{"name":13,"slug":14,"type":15},"2026-08-06T05:38:24.50326",{"slug":1503,"name":1503,"fn":1504,"description":1505,"org":1506,"tags":1507,"stars":1355,"repoUrl":1356,"updatedAt":1512},"expo-examples","integrate Expo example projects","Framework (OSS). Expo's official example projects - the expo\u002Fexamples repo of ~70 `with-*` integrations (Stripe, Clerk, Supabase, OpenAI, maps, Reanimated, SQLite, Skia, NativeWind, and more). Use when integrating a third-party library or service into an existing Expo app and you want the canonical, version-matched pattern to adapt, or when scaffolding a new project from one with `npx create-expo --example`.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1508,1509,1510,1511],{"name":9,"slug":8,"type":15},{"name":1452,"slug":1453,"type":15},{"name":18,"slug":19,"type":15},{"name":13,"slug":14,"type":15},"2026-08-06T05:38:19.490416",24,{"items":1515,"total":594},[1516,1523],{"slug":4,"name":4,"fn":5,"description":6,"org":1517,"tags":1518,"stars":23,"repoUrl":24,"updatedAt":25},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1519,1520,1521,1522],{"name":21,"slug":22,"type":15},{"name":9,"slug":8,"type":15},{"name":18,"slug":19,"type":15},{"name":13,"slug":14,"type":15},{"slug":1524,"name":1524,"fn":1525,"description":1526,"org":1527,"tags":1528,"stars":23,"repoUrl":24,"updatedAt":1537},"install-xcode","install Xcode versions on macOS","Install a specific Xcode version on a Mac using the xcodes CLI, handing the Apple ID sign-in (password + 2FA) to the user through a shared live terminal. Use when a task needs Xcode\u002Fsimulators and the machine doesn't have the right version, or the user asks to install\u002Fupgrade Xcode.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[1529,1530,1533,1534],{"name":1348,"slug":1349,"type":15},{"name":1531,"slug":1532,"type":15},"macOS","macos",{"name":18,"slug":19,"type":15},{"name":1535,"slug":1536,"type":15},"Xcode","xcode","2026-07-27T06:06:03.255819"]