tapflow streams iOS simulators and Android emulators into a browser, so a whole team can test an app without installing anything. The simulators run on a Mac that hosts the tapflow agent, and that Mac needs the mobile toolchain: Xcode, a simulator runtime, the Android SDK, an emulator, AVDs.
tapflow already gives the people who use it a zero-install experience. We wanted the person who hosts it to get the same one. So tapflow setup brings the whole host environment up in as close to one command as the toolchain allows.
Here are the DX decisions behind it.
doctor diagnoses, setup installs and configures
We split diagnosis from the steps that install and configure.
tapflow doctor is read-only: it checks the prerequisites — Xcode, simctl, a simulator runtime; the SDK, adb, an AVD — and reports. It never changes your machine, so it's safe to run anywhere, anytime. A clean run looks like this:
tapflow doctor
✓ Node v20.11.0
iOS
✓ Xcode 16.2
✓ xcrun simctl
✓ Simulator available (8)
Android
✓ Android SDK: ~/Library/Android/sdk
✓ adb found: ~/Library/Android/sdk/platform-tools/adb
✓ AVD available: tapflow-phone
All checks passed.
When something's missing, each failing line carries the exact fix — ⚠ AVD → No AVD found. Run: tapflow setup android — so doctor always points straight at setup.
tapflow setup is the one command allowed to install and configure. The two mirror each other, so the mutating verb lives in exactly one place. Run setup with no argument and it reads the environment:
if (process.platform === 'darwin') platforms.push('ios')
if (resolveAdb() !== null) platforms.push('android')
macOS implies iOS; an existing adb implies you care about Android. If neither signal is there, it asks instead of guessing.
iOS: installed ≠ usable
Xcode can only come from the App Store, so setup doesn't fake it — it opens the right page and waits.
The part we kept getting wrong was that a freshly installed Xcode isn't a working one. Three steps stand between "the app exists" and "xcodebuild runs": point the active developer directory at Xcode (xcode-select -s), accept the license, and finish first launch. setup runs them for you, after asking, since they need sudo. The check that matters isn't "does Xcode.app exist" — it's whether xcodebuild -version actually runs.
Android: a self-contained SDK
This is the decision we're happiest with.
The obvious path is "install Android Studio." We didn't. The host doesn't need a GUI IDE, and depending on one means fighting whatever SDK location, ANDROID_HOME, and AVDs the user already has. Instead setup builds a self-contained SDK under one path we own:
sdkmanager --sdk_root=~/Library/Android/sdk \
"cmdline-tools;latest" "platform-tools" "emulator" \
"system-images;android-35;google_apis;arm64-v8a"
After that, every Android binary tapflow touches comes from inside that directory. A couple of details that make it reliable:
-
sdkmanagerneeds a JDK or it won't run, so a Temurin check happens first. - The system image is
google_apis, not the Play Store one, which is unstable the way we drive it.
The one thing that has to outlive the process is ANDROID_HOME on your PATH. setup writes it into your shell rc inside a marker block, and only if it isn't already there — so re-running never duplicates it:
# >>> tapflow android sdk >>>
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
# <<< tapflow android sdk <<<
The catch it can't remove: the variable isn't in your current shell, so setup tells you to open a new terminal before doctor.
setup prepares; the relay boots
For both platforms, setup stops at "a bootable device exists." It never boots a simulator or emulator. That's the relay's job — it boots the right device on demand when a teammate joins a QA session. Two components owning device lifecycle would just race each other.
One rail under all of it
Every step that changes the machine asks first, and only auto-runs in an interactive terminal. Run setup in CI and instead of curling an install script as root, it prints guidance and exits clean. Nothing in tapflow deletes your data — the only teardown command is reset, which shuts down running simulators.
Honest limitations
- Xcode is still a manual App Store download. There's no API for it;
setupautomates everything around it. - The first run usually needs a new shell for the Android
PATHto take effect. - The agent host is a Mac, since that's the only place iOS simulators run. (The relay itself runs on Linux.)
- Still v0.x, so the steps will keep moving as the toolchain shifts.
Takeaway
The downloads were never the hard part. The DX lives in the glue around them — Xcode activation, the PATH that needs a fresh shell, knowing to stop at "bootable" and let the relay boot the rest. Automating a dev environment means automating everything that isn't the install.
Try it
tapflow is MIT licensed.
npm install -g tapflow
tapflow doctor # what's missing?
tapflow setup # set it up
tapflow start
- 🔗 GitHub: https://github.com/jo-duchan/tapflow
- 📖 Docs: https://www.tapflow.dev












