Skip to content

Repository files navigation

maestro-d

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.png

The 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.

Why

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 hierarchy and get screenshot tell it what is on screen.
  • Building a flow. Get the steps right interactively, then paste them into a .yaml file that upstream runs in CI.

Three ways in

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 session

REST — 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 events

JavaScript — 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.

What the fork adds

  • 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 with DEVICE_IN_USE naming 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), eval for 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 --find and --compact), get info, get state.
  • Device lifecycle. device list, device start to 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_FAILED 1/422, USAGE 2/400, DAEMON_UNAVAILABLE 3/503, DEVICE_ERROR 4/502, DEVICE_IN_USE 5/409, INTERRUPTED 130/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.

Install

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-d

Then check the toolchain and see what is plugged in:

maestro-d doctor
maestro-d devices

Android testing needs adb; iOS needs Xcode's command-line tools (and --team-id for physical devices); web testing needs Chrome or Chromium.

Documentation

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

License

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.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages