# DevHub by Root Native (@rootnative/devhub) > A CLI and a core library for Expo developers. DevHub keeps every Expo > development build (dev client) on the machine, and installs a stored build > on a running Android emulator or device, or on a booted iOS simulator. Version: 0.0.0-alpha.1. This file describes this version only. Source: https://github.com/rootnative/devhub ## What ships - `devhub sync` downloads the EAS dev client builds of a project into the store, before EAS deletes them. `devhub sync --all` does this for each project that `devhub sync` saw before. - `devhub list` shows the stored builds. `devhub where` gives the path of a build file. - `devhub devices` lists the Android emulators and devices, and on macOS the booted iOS simulators. - `devhub install` installs a stored build on a device that runs now. - `devhub match` calculates the native fingerprint of a project with the project's own `expo/fingerprint`, and shows the stored builds with the same EAS fingerprint. - `devhub ui` serves a local page with the same functions, on 127.0.0.1. - `devhub whoami` shows the EAS login state. A sync needs a login. - `devhub doctor` checks Node, the store, eas-cli and its login, adb, and Xcode, and gives the fix for each problem. - `@rootnative/devhub/cache-provider` is an Expo build cache provider. `npx expo run:android` and `npx expo run:ios` then store each new build, and install a stored build when the native code did not change. NOT IMPLEMENTED, do not call: the import of a build file, the launch (boot a device, open the app), launch profiles, prune, and the desktop app. They are the plan only. ## Requirements - Node 20.19 or later. Run `npx @rootnative/devhub `, or install the package globally to get the `devhub` command. - For `devhub sync`: eas-cli with a login (`eas login` or `npx eas-cli login`). DevHub runs `eas` from `PATH`, or `npx --yes eas-cli` when `eas` is not on `PATH`. DevHub never reads, asks for, or stores a token. - For Android: adb from the Android SDK Platform-Tools. DevHub uses the first `platform-tools/adb` in `ANDROID_HOME`, `ANDROID_SDK_ROOT`, or the default SDK folder of the system, and else `adb` from `PATH`. - For iOS: Xcode, on macOS only. ## Rules for an agent - Add `--json` to each command. Then stdout holds one JSON value and nothing else. Progress and warnings go to stderr. - The exit code is `0` for success, `1` for a failure, and `2` for a usage error. After a failure, stdout is empty and stderr holds the message, with these exceptions. `devhub devices --json` prints its JSON and exits with `1` when each device tool fails. `devhub sync --json` prints its JSON and exits with `1` when a project fails. `devhub match --json` prints its JSON and exits with `1` when no stored build matches. `devhub whoami --json` prints its JSON and exits with `1` when eas-cli has no login. `devhub doctor --json` prints its JSON and exits with `1` when a check has the status `fail`. - When a command fails because a tool is missing, run `devhub doctor --json`. Each check with `warn` or `fail` has a `fix`. - `` is the EAS build ID, or a unique start of it. `devhub list --json` gives each full ID. - `devhub install` does not start a device, and does not open the app. Start the emulator or the simulator first. Then run `npx expo start` in the project folder, and press `a` or `i`, to load the JavaScript. - When two or more devices of the build's platform are ready, `devhub install` stops with exit code `2`. Give `--device ` with an ID from `devhub devices --json`. - DevHub never uninstalls an app. For a signing key conflict, it prints the `adb uninstall` command and stops. An uninstall deletes the app data, so ask the user before you run that command. - `devhub install` refuses an Android App Bundle (`.aab`), an iOS device build (`.ipa`), and each iOS build on Windows or Linux. `installProblem` in `devhub list --json` gives the reason and the fix before you try. - `devhub match` runs in the project folder, or in a folder inside it. It needs the dependencies of the project installed, and Expo SDK 52 or later. It runs no `eas` command and no network call. A match is a `ListedBuild`: give its `id` to `devhub install`. - `devhub ui` runs until Ctrl+C. To read data, use the commands with `--json`, not the page. - Only one sync runs at a time. A second sync stops with exit code `1` and a message that names the process of the first one. - A dev client is a debug build. Its JavaScript runs in development mode, so it does not show the speed of the app in production. ## CLI ### devhub ```text DevHub by Root Native 0.0.0-alpha.1 Keep every dev build. Launch any one in one click. Usage: devhub [options] Commands: sync Download the EAS dev client builds of this project. list Show the stored builds. where Print the path of a stored build file. devices List the Android devices and the iOS simulators. install Install a stored build on the ready device. match Show the stored builds that match the code of this project. ui Open the DevHub page in the browser. whoami Show the EAS login state. doctor Check the tools and the store that DevHub uses. Options: -h, --help Show this help. -v, --version Show the version. Run "devhub --help" to see the options of a command. ``` ### devhub sync ```text Usage: devhub sync [options] Download each finished EAS dev client build of this project that the store does not have. A build profile is a dev client profile when it has "developmentClient": true in eas.json. DevHub runs "eas", or "npx eas-cli" when eas is not on PATH, and uses the eas-cli login. Options: --all Sync each project folder that devhub sync saw before. --profile Also sync this build profile. Give it again for each other profile. --dry-run Show what the sync downloads. Download nothing. --json Print the result as JSON. -h, --help Show this help. The store is in the data folder of the OS. Set DEVHUB_HOME to use a different folder. ``` ### devhub list ```text Usage: devhub list [options] Show the builds in the store, sorted by project and then newest first. The first line names the store folder. EAS EXPIRY is the date on which EAS deletes its copy. The copy in the store stays. A build that "npx expo run" made shows the profile "local" and its variant, and CREATED is the date on which DevHub stored it. Options: --json Print the builds as JSON. -h, --help Show this help. ``` ### devhub where ```text Usage: devhub where [options] Print the path of a stored build file. is the EAS build ID, or the start of it. "devhub list" shows a start of each ID that is unique. Options: --json Print the ID and the path as JSON. -h, --help Show this help. ``` ### devhub devices ```text Usage: devhub devices [options] List the Android emulators and devices that adb sees, and on macOS the iOS simulators that run now. DevHub can install a build on a device in the state "ready". Options: --json Print the devices as JSON. -h, --help Show this help. ``` ### devhub install ```text Usage: devhub install [options] Install a stored build on a device that is ready: an Android build on an emulator or a device, or an iOS simulator build on a booted simulator (macOS only). is the EAS build ID, or the start of it. "devhub list" shows the builds, and "devhub devices" shows the devices. DevHub compares the SHA-256 of the file with the record before the install. DevHub does not start a device, and does not open the app. Options: --device The adb serial or the simulator UDID of the device. Necessary when two or more devices are ready. --json Print the build and the device as JSON. -h, --help Show this help. ``` ### devhub match ```text Usage: devhub match [options] Find the stored builds that match the native code of the project in the current folder. DevHub runs the @expo/fingerprint of the project, with the same options as eas-cli, and compares the hash with the EAS fingerprint of each stored build. A build that matches needs no new build: install it with "devhub install ". Options: --platform Only this platform: android or ios. Give it again for the other one. The default is both. --json Print the result as JSON. -h, --help Show this help. The exit code is 1 when no stored build matches. ``` ### devhub ui ```text Usage: devhub ui [options] Start a local server for the DevHub page, and open the page in the browser. The server listens on 127.0.0.1 only, and each run has a new token in the URL. Press Ctrl+C to stop the server. Options: --port The port of the server. The default is a free port. --no-open Print the URL, and do not open the browser. --json Print the URL and the port as JSON. -h, --help Show this help. ``` ### devhub whoami ```text Usage: devhub whoami [options] Show the EAS login state. DevHub runs "eas whoami" with eas on PATH, or else with "npx --yes eas-cli". A sync needs a login. The exit code is 0 with a login, and 1 without one. Options: --json Print the state as JSON. -h, --help Show this help. ``` ### devhub doctor ```text Usage: devhub doctor [options] Check the tools and the store that DevHub uses on this machine: Node, the store folder, eas-cli and its login, adb, and on macOS Xcode. Give the fix for each problem. The checks do not read the current folder. A warning stops one feature, for example the iOS installs. A failure stops DevHub. The exit code is 1 when a check fails, and 0 otherwise. The first run of "npx --yes eas-cli" downloads eas-cli, so it can take a while. Options: --json Print the report as JSON. -h, --help Show this help. ``` ## JSON output - `devhub sync --json` prints `SyncReport`. - `devhub list --json` prints `BuildList`. - `devhub devices --json` prints `DeviceReport`. - `devhub install --json` prints `InstalledBuild`. - `devhub match --json` prints `MatchReport`. - `devhub whoami --json` prints `WhoamiReport`. - `devhub doctor --json` prints `DoctorReport`. ```ts type SyncReport = { dryRun: boolean; projects: ProjectSyncResult[]; }; type ProjectSyncResult = { project: string; /** `missing`: the project folder does not exist. DevHub skipped it. */ status: 'ok' | 'failed' | 'missing'; profiles: SyncProfile[]; builds: BuildOutcome[]; warnings: string[]; error?: string; }; /** A build profile to sync, and the platforms whose builds DevHub keeps. */ type SyncProfile = { name: string; platforms: EasPlatform[]; }; type BuildOutcome = { id: string; status: BuildStatus; profile: string; platform: string; isForIosSimulator?: boolean; appVersion?: string; expirationDate?: string; /** Set when the status is `downloaded`. */ path?: string; size?: number; /** Set when the status is `failed`. */ error?: string; }; type EasPlatform = 'ANDROID' | 'IOS'; type BuildStatus = 'downloaded' | 'would-download' | 'stored' | 'expired' | 'no-artifact' | 'invalid-id' | 'failed'; /** The JSON of `devhub list --json`. */ type BuildList = { /** The absolute path of the store folder. */ store: string; /** Sorted by project, and then newest first. */ builds: ListedBuild[]; /** Build folders with no build.json. The next sync replaces them. */ incomplete: string[]; unreadable: UnreadableBuild[]; }; /** A build in the JSON of `devhub list --json`. */ type ListedBuild = ListedEasBuild | ListedLocalBuild; type UnreadableBuild = { id: string; reason: string; }; /** A build that `devhub sync` downloaded from EAS. */ type ListedEasBuild = ListedFields & { source: 'eas'; eas: EasBuild; }; /** A build that `npx expo run:*` made, and the cache provider stored. */ type ListedLocalBuild = ListedFields & { source: 'local'; local: LocalBuild; }; type ListedFields = { id: string; /** The absolute path of the build file. */ path: string; size: number; sha256: string; storedAt: string; /** * Why `devhub install` refuses the build on this system, with the fix. * The text says "this build" and names no ID: the record has `id`. * Absent when it can install the build. */ installProblem?: string; }; /** * One build as `eas build:list --json` prints it. eas-cli drops each field * with a `null` value, so an absent field means "no value". DevHub stores * the record without changes, so the type keeps the fields it does not know. */ type EasBuild = { id: string; platform: string; buildProfile?: string; distribution?: string; isForIosSimulator?: boolean; appIdentifier?: string; appVersion?: string; appBuildVersion?: string; sdkVersion?: string; runtime?: { version?: string; }; fingerprint?: { hash?: string; }; gitCommitHash?: string; gitCommitMessage?: string; app?: { id?: string; slug?: string; name?: string; }; createdAt?: string; completedAt?: string; expirationDate?: string; artifacts?: { applicationArchiveUrl?: string; buildUrl?: string; }; [field: string]: unknown; }; /** A build that `npx expo run:*` made on this machine. */ type LocalBuild = { platform: EasPlatform; /** The fingerprint of the project for this platform, the same kind as EAS. */ fingerprintHash: string; /** Android: the Gradle variant, for example `debug`. iOS: the Xcode configuration, for example `Debug`. */ variant: string; /** The real path of the project folder. */ project: string; /** The `slug` of the app config. */ slug?: string; appIdentifier?: string; appVersion?: string; sdkVersion?: string; gitCommitHash?: string; /** Android: the ABIs of the native code in the APK. */ abis?: string[]; /** iOS: `process.arch` of the Mac that made the build. */ hostArch?: string; }; /** The JSON of `devhub devices --json`. */ type DeviceReport = { devices: Device[]; /** The error of each device tool that failed, with the fix. */ warnings: string[]; }; type Device = { platform: DevicePlatform; kind: DeviceKind; /** The adb serial, or the simulator UDID. */ id: string; name: string; /** The state as the device tool prints it. */ state: string; /** DevHub can install a build on the device now. */ ready: boolean; /** Why DevHub cannot install on the device now, and the fix. */ problem?: string; /** The iOS version of a simulator. */ osVersion?: string; }; type DevicePlatform = 'android' | 'ios'; /** `physical`: a phone or a tablet that adb sees over USB or Wi-Fi. */ type DeviceKind = 'emulator' | 'physical' | 'simulator'; /** The JSON of `devhub install --json`. */ type InstalledBuild = { id: string; /** The absolute path of the build file. */ path: string; device: Device; }; /** The JSON of `devhub match --json`. */ type MatchReport = { /** The project folder: the nearest folder with a package.json. */ project: string; /** The `@expo/fingerprint` version of the project, when it is known. */ fingerprintVersion?: string; platforms: PlatformMatch[]; }; type PlatformMatch = { platform: FingerprintPlatform; /** The fingerprint of the project for this platform. */ hash: string; /** The stored builds with this fingerprint, newest first. */ matches: ListedBuild[]; }; /** The JSON of `devhub whoami --json`. */ type WhoamiReport = { /** * `signed-in`: eas-cli runs and has a login. `signed-out`: eas-cli runs * and has no login. `no-cli`: neither `eas` nor `npx` starts. `failed`: * eas-cli started and failed for another reason, for example no network. */ state: 'signed-in' | 'signed-out' | 'no-cli' | 'failed'; /** How DevHub starts eas-cli: `eas` on PATH, or `npx --yes eas-cli`. */ cli: 'eas' | 'npx' | 'none'; /** The EAS user name, with the state `signed-in`. */ user?: string; /** What is wrong, with the fix, with each state but `signed-in`. */ message?: string; }; /** The JSON of `devhub doctor --json`. */ type DoctorReport = { checks: DoctorCheck[]; }; type DoctorCheck = { id: DoctorCheckId; /** For example "Node" or "Android". */ label: string; status: DoctorStatus; /** What DevHub found, for example "Node 24.11.0". */ detail: string; /** With `warn` and `fail`: what is wrong, the fix, and what needs it. */ fix?: string; }; type DoctorCheckId = 'node' | 'store' | 'eas' | 'android' | 'ios'; /** * `warn`: one feature does not work. `fail`: no feature works. `skip`: the * check does not apply to this system. */ type DoctorStatus = 'ok' | 'warn' | 'fail' | 'skip'; ``` - `devhub where --json` prints `{ "id": string, "path": string }`. - `devhub ui --json` prints `{ "url": string, "port": number }`, and the server then runs until Ctrl+C. Each path is absolute. Each date is an ISO 8601 string. `devhub list --json` names the store folder in `store`. A build in `devhub list --json` has `source`: `"eas"` for a build that `devhub sync` downloaded, and `"local"` for a build that `npx expo run:*` made. An EAS build has `eas`, the record of `eas build:list --json` without changes, so it can have more fields than the type names. eas-cli drops each field with a `null` value, so an absent field means "no value". A local build has `local` in place of `eas`. The `id` of a local build is a UUID that DevHub made, and `devhub where` and `devhub install` accept it the same as an EAS build ID. ## The store One folder holds the builds of all projects. It is never inside a project folder. Read it with the CLI. Do not write into it. ```text / projects.json each project folder that devhub sync saw sync.lock the process ID of the sync that runs now builds// the EAS build ID, or a UUID for a local build build.json the record. A folder without it is incomplete. .apk, .aab, .tar.gz (iOS simulator), or .ipa extracted// the unpacked iOS simulator build, a cache ``` `DEVHUB_HOME` sets the folder. The default is `~/Library/Application Support/devhub` on macOS, `%LOCALAPPDATA%\devhub` on Windows, and `$XDG_DATA_HOME/devhub` or `~/.local/share/devhub` on Linux. ## Expo build cache provider Set it up in a project: 1. Install `@rootnative/devhub` as a dev dependency of the project. Expo CLI resolves the plugin from the project folder, not from a global install. 2. Add `"buildCacheProvider": { "plugin": "@rootnative/devhub/cache-provider" }` to `expo` in the app config. Expo SDK 53 reads it under `expo.experiments`. Rules: - Expo CLI calls the provider only for `npx expo run:*`, and on iOS only for a simulator. `--binary` turns it off for one run. - A stored build serves a run only when the platform, the fingerprint, and the variant (`--variant`, default `debug`; `--configuration`, default `Debug`) agree. On Android the APK must have native code for each ready device, or for the device that `--device` names, and for all four ABIs with `--all-arch`. On iOS a Mac with the same `process.arch` made it. - Only builds that `npx expo run:*` made serve a run. A build from `devhub sync` does not. - A run with `--scheme` (iOS) does not use or store a build. - In a managed project (git ignores `android/` and `ios/`, so the fingerprint does not read them), the provider does not store a build when an app config file (`app.json`, `app.config.json`, `app.config.*`) is newer than the last prebuild (`android/app/src/main/AndroidManifest.xml`, `ios/Podfile`). Expo CLI runs prebuild only when the folder is missing, so that build can hold old native code. The fix: delete the folder, or run `npx expo prebuild --clean`. - The provider never fails a run. It prints its lines with the prefix `› DevHub:`, and Expo CLI builds the app when no stored build fits. - `devhub list` shows a stored local build with the profile `local` and its variant. `devhub match` shows it as a match. ## Core API `import { listBuilds } from '@rootnative/devhub'`. The package is ESM only, for Node. The types are in `dist/index.d.ts`. The API can change in each version before `1.0.0`. Functions, classes, and constants: ADB_MISSING, ANDROID_ABIS, BUILD_LIST_PAGE_SIZE, DeviceError, EAS_ON_PATH, EAS_WITH_NPX, EasCliError, EasJsonError, FINGERPRINT_PLATFORMS, FingerprintError, NODE_MINIMUM, StoreLockedError, TAR_MISSING, TAR_PACK_MISSING, XCODE_FIX, XCRUN_MISSING, adbAbiListCommand, adbAvdNameCommand, adbDevice, adbDevicesCommand, adbInstallCommand, adbInstallFailure, adbUninstallCommand, adbVersionCommand, apkAbis, artifactUrl, buildSummary, canSync, checkEasLogin, createCacheProvider, devClientProfiles, deviceReport, doctor, doctorPassed, downloadFile, easBuildListCommand, easWhoamiCommand, findBuild, findDevice, findEasProject, findProjectRoot, findStoredBuild, hasMatch, installBuild, installPlatform, installProblem, installStoredBuild, iosRuntimeVersion, isEmulatorSerial, isExpired, isLocalRecord, listAndroidDevices, listBuilds, listDevices, listFinishedBuilds, listIosSimulators, matchBuilds, matchesFingerprint, parseAdbDevices, parseAdbVersion, parseAvdName, parseEasJson, parseSimctlDevices, parseXcodeVersion, planSync, primaryAbi, projectFingerprint, readEasJson, readProjects, resolveAdb, resolveFingerprintModule, resolveHome, runCommand, scanStore, selectDevice, simctlInstallCommand, simctlListCommand, storeLocalBuild, syncBuilds, syncFailed, syncProjects, tarCreateCommand, tarExtractCommand, unpackIosBuild, version, whoami, withExtraProfiles, xcodeVersionCommand. Types: AdbEntry, AdbInstallTarget, AdbOptions, BuildList, BuildOutcome, BuildRecord, BuildSource, BuildStatus, BuildSummary, CacheProvider, CacheProviderDeps, CalculateFingerprintHashProps, Command, CommandOptions, CommandResult, CommandRunner, Device, DeviceChoice, DeviceKind, DeviceList, DeviceListOptions, DevicePlatform, DeviceReport, DeviceSource, DoctorCheck, DoctorCheckId, DoctorDeps, DoctorReport, DoctorStatus, DownloadResult, Downloader, EasBuild, EasBuildRecord, EasInvocation, EasPlatform, FindResult, FingerprintModule, FingerprintPlatform, FlowError, FlowResult, HomeOptions, InstallDeps, InstallOptions, InstallRequest, InstalledBuild, ListedBuild, ListedEasBuild, ListedLocalBuild, LocalBuild, LocalBuildInput, LocalBuildRecord, MatchDeps, MatchReport, MatchRequest, PlanItem, PlatformMatch, ProfileBuilds, ProjectSyncResult, ResolveBuildCacheProps, RunOptions, StoreLocalOptions, StoreScan, StoredBuild, SyncBuildsDeps, SyncDeps, SyncOptions, SyncProfile, SyncReport, SyncRequest, UnpackOptions, UploadBuildCacheProps, WhoamiDeps, WhoamiReport.