Drive a real phone one command at a time — from a shell, over HTTP, or from JavaScript.
CLI · REST API · JavaScript · Commands · How it works
maestro-d is maestro-runner
plus a persistent daemon. Upstream runs a YAML flow start to finish; this fork
keeps the device session open between calls, so every Maestro command is also a
CLI command, a REST route and a JavaScript method — no flow file, no relaunching
the app, no re-attaching the driver.
maestro-d launchApp co.edgesecure.app --device 29271FDH200ABP # ~4s: spawns the daemon, attaches
maestro-d tapOn "Create account" # ~1s
maestro-d assertVisible "Write these words down" --timeout 5000 # ~1s
maestro-d get screenshot -o step.pngThe first call starts a background daemon and attaches the device. Every call
after it reuses that session, so the loop is a second or two instead of a full
flow run. Exit status is the result: 0 passed, 1 the step failed, and a JSON
error envelope on stderr says why.
Everything upstream does still works unchanged — maestro-d test flows/ runs
your existing YAML. See the upstream README.
Writing a flow file, running it, and reading a report is the wrong loop for three cases this is built for:
- Exploring an app. Tap, look, tap again — the same way you would by hand, but scriptable and reproducible.
- Agents and scripts. An LLM agent or a shell script can issue one command,
read the exit code and the JSON, and decide what to do next.
get hierarchyandget screenshottell it what is on screen. - Building a flow. Get the steps right interactively, then paste them into
a
.yamlfile that upstream runs in CI.
CLI — one subcommand per YAML command, flags for its fields:
maestro-d tapOn --id submit --index 1 # - tapOn: {id: submit, index: 1}
maestro-d inputText "alice@example.com"
maestro-d swipe --direction UP --duration 400
maestro-d copyTextFrom --id balance --json | jq -r .data
maestro-d run steps.yaml # a batch, still on the open sessionREST — the same daemon over a unix socket, or TCP with a bearer token:
maestro-d start --daemon api --http 127.0.0.1:7788 --token "$TOKEN"
curl -X POST -H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:7788/v1/devices/$UDID/commands/tapOn" \
-d '{"text": "Login", "timeout": 5000, "optional": true}'
curl -N "http://127.0.0.1:7788/v1/events" # server-sent step and device eventsJavaScript — npm i maestro-d, one method per command, errors as Errors:
import { MaestroD } from 'maestro-d'
const m = await MaestroD.attach({ device: '29271FDH200ABP', appId: 'co.edgesecure.app' })
await m.launchApp({ clearState: true })
await m.tapOn({ text: 'Get started' })
try {
await m.assertVisible({ text: 'Create account', timeout: 5_000 })
} catch (e) {
if (e.code !== 'COMMAND_FAILED') throw e
console.log('not there:', e.result?.artifacts.screenshotAfter)
}
await m.detach()All three speak the same protocol to the same daemon, so a script and a shell can share one device session.
- A daemon per name.
--daemon <name>(or$MAESTRO_D) picks one; each is a separate process holding its own devices, so parallel agents never collide. A device attached elsewhere fails fast withDEVICE_IN_USEnaming the owner. - Many devices per daemon. Attach a Pixel and an iPhone to one session and
address them with
--device. - Every YAML command, generated from the parser. All ~90 step types, their fields and docs come from the same structs the flow parser uses — CLI help, the TypeScript methods and commands.md cannot drift from what the runner actually accepts.
- Session state between calls. Variables (
set,-e),evalfor JavaScript in the flow engine, and${VAR}expansion, all persisting across commands. - Inspection.
get screenshot,get hierarchy(normalized the same way on Android, iOS and web, with--findand--compact),get info,get state. - Device lifecycle.
device list,device startto boot a simulator or emulator,device stop,ps, and automatic shutdown of anything the daemon booted. - Machine-readable failure. One error table shared by all three interfaces:
COMMAND_FAILED1/422,USAGE2/400,DAEMON_UNAVAILABLE3/503,DEVICE_ERROR4/502,DEVICE_IN_USE5/409,INTERRUPTED130/499. - Events. Server-sent events for every step and device transition, resumable
with
Last-Event-ID.
And everything upstream already gives it: Android via UIAutomator2 or the DeviceLab on-device driver, iOS simulators and physical devices via WebDriverAgent, desktop browsers via CDP, cloud grids via Appium, React Native and Flutter element finding, HTML/JUnit/Allure reports, and a single binary with no JVM.
From source — Go 1.23+, into its own home so an existing maestro-runner install is untouched:
git clone https://github.com/EdgeApp/maestro-d
cd maestro-d && make build # → ~/.maestro-d/bin/maestro-d
export PATH="$HOME/.maestro-d/bin:$PATH"From a release — each platform tarball is a self-contained home
(bin/maestro-d, drivers/):
V=0.1.0; T=darwin-arm64 # or darwin-x64, linux-arm64, linux-x64
mkdir -p ~/.maestro-d
curl -fsSL "https://github.com/EdgeApp/maestro-d/releases/download/maestro-d-v$V/maestro-d-$T-$V.tgz" \
| tar xz --strip-components=1 -C ~/.maestro-dThen check the toolchain and see what is plugged in:
maestro-d doctor
maestro-d devicesAndroid testing needs adb; iOS needs Xcode's command-line tools (and
--team-id for physical devices); web testing needs Chrome or Chromium.
| docs/daemon/README.md | How the daemon works: lifecycle, ownership, run files, error table |
| docs/daemon/cli.md | Every CLI command and flag |
| docs/daemon/rest.md | REST reference with curl examples |
| docs/daemon/js.md | The npm package |
| docs/daemon/commands.md | All YAML commands and their fields |
| docs/daemon/UPSTREAM.md | Which upstream files the fork touches, and how to rebase |
| docs/maestro-runner.md | Upstream's README — the flow runner, drivers, cloud providers |
Apache License 2.0 — see LICENSE. A fork of
devicelab-dev/maestro-runner;
the Go module path is deliberately unchanged so rebasing stays a plain
git rebase.