run-local
Run the product's Expo app and its API on this Mac, on an Android phone, an Android emulator or an iOS Simulator, in one command — the script checks what the Mac is missing, shows it all at once, inst
Files1
View on GitHubSKILL.mdskills/run-local/SKILL.md
GitHubRun local — the app and its API on a phone, an emulator or a simulator
The work is done by .keelokit/bin/run-local.sh, which every Keelokit project carries (new or
adopted) and which runs without Claude and without the plugin: bash .keelokit/bin/run-local.sh
(pnpm run run:local in generated projects). This skill runs it, reads what it leaves behind when
something fails, and repairs. Solo macOS: on another system the script says so and stops.
Settings live in the [local] block of .keelokit/profile.toml; whatever it leaves out, the script
detects from the repo. Messages come in the project's language ([dashboard] lang).
Never, without the user's yes for that exact thing: kill a process, stop a container, delete or
regenerate ios/ or android/, uninstall the app from a phone, edit a tracked native folder. Never
type, print or commit a credential, and never write values into an .env yourself: the script
copies .env.example into the gitignored .env; real keys are the user's.
1. See where it stands
No .keelokit/ folder: the project isn't a Keelokit one yet, so there is no script to run. Say so and
offer /keelokit:project-adopt (it adds only .keelokit/, the script included); come back here after.
Read [local] in .keelokit/profile.toml and the profile's traits. No mobile trait: say there
is no app to run here and stop. Then:
- No
[local]block (adopted repos, or a project from before this existed):bash .keelokit/bin/run-local.sh detectprints what the repo shows, as JSON, with the keys it is not sure of inuncertain. Show it in plain words (where the app is, how it installs, where the API and the database are) and ask only about the uncertain keys, once. With the user's yes, write the block (or letrun-local.sh doctorwrite it) and tell them to correct by hand whatever is wrong. Two Expo apps: ask which. Never write a guess without showing it. - In an adopted repo the script also creates
.local-dev/(logs,last-error.txt) and adds it to.gitignore: ask before, once, and say what it is. - Block present:
bash .keelokit/bin/run-local.sh doctor --checksays whether it is coherent (the app is where it says, the manager exists, the folders exist). Fix a wrong key with the user.
2. Diagnose, then run
Read the script's answers as data, not as text to scrape: doctor --json, status --json, logs
and the exit code. Exit codes: 0 done, 1 failed or something is missing, 2 usage, 3 stopped on
something only a person can do (Xcode, no phone, no emulator image), 4 a question was declined or
there was no terminal to answer it. bash .keelokit/bin/run-local.sh <command> --help says what each
command and flag does.
bash .keelokit/bin/run-local.sh doctor --jsonlists everything the Mac is missing at once and installs nothing:{"exit":3,"ok":false,"messages":[{"level","text"}],"missing":[{"label","command"}],"blockers":["…"]}. Showmissingin plain words (Homebrew installs, an Android SDK, Node through nvm, asudoprompt) andblockersas what is the user's to do: Xcode, a phone with USB debugging on, Expo Go on an Android phone.bash .keelokit/bin/run-local.sh status --jsonsays what already runs, so nothing is started twice or killed by surprise (api.ownedfalse means another project's API holds the port: the script will offer its next free port, or stop and ask; never reuse it):api,database.services,metro,android.devices(phones and emulators, withkind),ios.booted, andportswith the pid and the process that holds each.- Ask before launching, with the multiple-choice question tool: at most three short questions, only
the ones that apply, the last answers as defaults. Run in a terminal by the user, the script asks
the same questions itself; run by you, it must never have to, so map every answer to a flag:
- Target: a connected phone, which emulator (or a new one), the iOS simulator, only Metro, or
only the API and database.
android --phoneorandroid --emulator(--device <AVD|serial|model>picks a specific one;--new-avdmakes an emulator);ios;metro;backend. Ask it only when there is a real choice (status --json: a phone inandroid.devicesand an emulator possible). One at a time; the script shuts other simulators down. - Seed, only when the project has a seed (
api_seedindetect): "Load the seed data?" with the recommendation: the script says "the database looks empty" when it can tell (throughdocker compose exec <db> psql) and otherwise "can't tell if it already has data". Always the user's own yes:--seedor--no-seed.--yesalone never seeds. - Rebuild or fast relaunch, only when the app is already installed on the target and its
native inputs are unchanged since the last install (the script prints "no native build: …" when
that is so): default fast,
--no-build; the other answer is--rebuild. Without that situation do not ask: it builds.
- Target: a connected phone, which emulator (or a new one), the iOS simulator, only Metro, or
only the API and database.
- After the user accepts the list and the answers, run the target with
--yesand the flags (--yesnever waits on a menu: it takes the remembered or first simulator, phone or emulator and says which; without--yesand without a terminal it exits 4 after 20 seconds, so a menu is never a hang):bash .keelokit/bin/run-local.sh <target> --yes --phone --no-seed. Metro keeps running in it (the script gives it an input that never ends when there is no terminal, so it survives being run in the background): run it where the user can see it, or in the background and readlogs. If it reports "Metro exited on its own" (exit 1), the app is installed but has no Metro: read the log and run it again.
It does not rebuild what is already there. The first run builds the native app (10–20 minutes:
say so). A later android or ios finds the app installed on the target and the native inputs
unchanged since the last successful install, and only starts Metro and opens the app; it prints
which path it took and why. --rebuild builds anyway (after a native dependency or config change the
script does it by itself), --no-build refuses to build and fails if the app is not installed.
A known iOS behaviour. Opening the app by its link makes iOS show a system dialog, Open in "<app>"?, that someone must tap in the simulator, every time; Android has no such prompt. The script says so and opens the link; tell the user to tap Open. Launching the app first was tried on hardware and does not help: the dev launcher opens but does not reconnect to Metro by itself.
Android targets. One phone connected: it is used. Several: the user chooses, and it is remembered
(--device <serial|model> overrides). Wireless: pair walks through it and asks for the pairing
address, lets adb ask for the code, then the connect address: never type or keep the code yourself.
No phone: the script offers an emulator. If there is no emulator it offers to create one, and says
that the system image is a 1–2 GB download: relay that, and only run with --yes after the user
accepted the download. --new-avd creates a new emulator with an 8 GB data partition (an installed
image is reused, no download). On INSTALL_FAILED_INSUFFICIENT_STORAGE the script offers, each with its
own yes: uninstall the app and retry, a new emulator, or a cold boot with -wipe-data, which erases
that emulator's apps and data: name the emulator and get that yes yourself; with --yes it exits 3
listing the options and picks none. An emulator needs hardware acceleration (HVF on macOS); if it complains, say
so plainly and suggest a phone.
Other things it asks. The seed (api_seed) writes to the database: it is offered once, never
silent, and seed runs it by name. [local] services (redis, mailpit...) start and stop with the
database. Node and the JDK come from the project; a Node from nvm is used for the run only, and
installing one is in missing, so it is the user's yes.
With Expo Go and --yes, the script answers Expo's "Install the recommended Expo Go?" with yes: it
downloads Expo Go from Expo and installs or updates it on the target, so only pass --yes once the user
has accepted that. If the prompt could not be answered it exits 3 and says so.
The script chooses between Expo Go and a development build (client in [local]: auto,
expo-go, dev-client) and says which and why. In a simulator build it can remove capabilities a
simulator can't run (Sign in with Apple, associated domains, push) from the generated ios/: it
shows the list and asks once. It never does it to a git-tracked ios/ or for a phone; for a phone
without a developer certificate, explain what is missing and stop.
3. When it fails
The failed step's log tail is in .local-dev/last-error.txt; read the rest with
bash .keelokit/bin/run-local.sh logs [step] (newest first; a step name gives its last 100 lines).
The script already repairs a few known causes once (listed in REPAIRS, the first block of the
script: a different signature on the phone, port 8081 or 5554 taken, Gradle out of memory,
CocoaPods' index, an emulator lock file, a missing Google services file). For the rest:
- Read
last-error.txt, then the log around the first real error, not the last line. - Say in one sentence what broke and propose the fix. Wait for the yes if it kills a process, stops a container, removes something or reinstalls native folders. Otherwise apply it.
- Run the same target again. After two repairs of the same run, stop: say what was tried and what is left, and don't keep going.
- If a failure repeats across projects, suggest adding a line to
REPAIRSand arepair_<action>function in the template'srun-local.sh. A local edit to the project's copy is overwritten by/keelokit:harness-upgrade.
Disk and stale native state: bash .keelokit/bin/run-local.sh clean lists, group by group, what it
would delete with sizes (build caches, this project's Xcode DerivedData, the generated android/ and
ios/, the logs) and asks per group. It never touches anything git tracks or anything outside the
project. Offer it when a build fails in a way a fresh native folder would fix, and tell the user the
next build takes 10–20 minutes again. Never delete those folders any other way.
4. After it
Show the test accounts in the summary, from bash .keelokit/bin/run-local.sh users --json
({"source","accounts":[{"line"}]}: raw lines from the project's own docs, README section or seed log,
never parsed or invented; source: null means none were found, so say how to add users_file in
[local]). They are local development credentials: show them, never write them anywhere, never read
.env files. The script also prints a short "Test users" block when a launch starts and after the seed.
If the app opened and reached the API, say so and how to stop it
(bash .keelokit/bin/run-local.sh stop asks before stopping the API, the containers and an emulator;
the data stays). If [local] was just written, refresh the dashboard (/keelokit:project-dashboard),
which shows "Local environment: ready".
What is tested and what is not
Tested with simulated commands: detection on many project shapes (pnpm, npm, yarn classic and berry,
bun, turborepo, nx, lerna), the coherence check, the diagnosis, the questions before anything is
killed, stopped, deleted or edited, the decision to rebuild or not, the choice of Android target, how
an emulator image and device are created and booted, status, logs and clean. The script this one grew
from launched an app on the iOS simulator, on an Intel Mac with Xcode 26, and on a Samsung Galaxy S20
over USB. Never run on real hardware by me: the emulator, wireless debugging, Expo Go on a phone,
the fingerprint through @expo/fingerprint, opening the dev client by its link, Apple Silicon. Say so
when it matters, and read the failure instead of assuming the path works.