maestro-manager
v0.0.1
Published
Daemonless iOS Maestro test runner with a shared simulator pool.
Readme
Maestro Manager
Daemonless iOS Maestro test runner with a shared simulator pool. The CLI creates named simulator slots, leases them to concurrent test runs, and records run state under the current pool prefix.
Prerequisites
- macOS with Xcode command line tools and simulator support available through
xcrun simctl. - Bun
>=1.3.14and Node.js>=24.16.0. - Maestro installed on
PATHasmaestro. - A built iOS
.appbundle and Maestro flow YAML files.
Install dependencies:
bun installConfiguration
Create maestro-manager.config.json at the repo root, pass it with
--config <path>, or set MAESTRO_MANAGER_CONFIG=<path>. Without either
override, the CLI walks up from the current directory until it finds the config.
Minimal shape:
{
"pool": {
"prefix": "my-project-e2e",
"deviceType": "iPhone 16",
"default": 2,
"max": 4
},
"app": {
"appId": "com.example.MyApp",
"glob": "~/Library/Developer/Xcode/DerivedData/*/Build/Products/*-iphonesimulator/*.app",
"searchDirs": ["~/Library/Developer/Xcode/DerivedData"]
},
"flowsDir": "maestro/flows",
"warmupDeepLink": "myapp://e2e/warmup",
"java": {
"home": null,
"autodetect": ["JAVA_HOME", "mise", "/usr/libexec/java_home"]
},
"recordingsDir": "recordings",
"hooks": {
"pruneAccounts": ""
}
}Notes:
pool.prefixbecomes part of simulator names and the state path. Keep it project-specific and safe: start with a letter or number, then use only letters, numbers, dots, underscores, and hyphens.pool.defaultis the normal warm pool size.pool.maxis the hard concurrency cap and must be<= 8. Keeppool.max <= 4until the downstream project has proved its backend is stable at the higher concurrency.flowsDir,recordingsDir, and entries inapp.searchDirsresolve relative to the config file directory, with~expanded.- App resolution order is
--app, thenapp.path, thenapp.glob, then scanningapp.searchDirsfor the newest.appmatchingapp.appId.app.globsupports*and?within path segments and~expansion. hooks.pruneAccountsis an optional shell command run byclean --prune-accounts.
Common Workflows
Warm the default simulator pool:
maestro-manager up --app path/to/MyApp.appBy default, simulators boot headless: Maestro can run against them without
opening Simulator.app. Use --headed when you want to watch the pool or a run:
maestro-manager up --headed --app path/to/MyApp.app
maestro-manager test --headed --sims 2--headless is also accepted explicitly and conflicts with --headed.
Show pool slots, active leases, and recent runs:
maestro-manager statusRun all configured flows on two simulators:
maestro-manager test --sims 2Stop this worktree's active leases but keep the pool:
maestro-manager stopReclaim stale leases and shut down idle simulators:
maestro-manager clean --idle-shutdown 30clean runs a stateless reaper, even with no flags:
- Reap orphaned runs. When a
testworker isSIGKILLed or crashes, no signal trap fires, so its detachedmaestroprocess group — and the simulator's (often hung) app — keep running and burning CPU. The stale-lease reclaim deliberately refuses to reclaim such a slot while itsmaestrochild is still alive (so it never yanks a simulator a live run is driving).cleandetects this case — owner dead, heartbeat stale,maestrostill alive — kills the orphanedmaestrogroup, shuts the simulator down (killing the hung app), and reclaims the slot. - Reclaim stale leases whose owner and
maestrochild are both dead, shutting those simulators down too.
Live-leased simulators (a live owner) are never touched.
Add --delete-unavailable to also run xcrun simctl delete unavailable, removing
broken-runtime CoreSimulator devices left behind by Xcode upgrades, removed
runtimes, or other tools. This is global — it is NOT scoped to this pool's
prefix — so it is opt-in and off by default:
maestro-manager clean --idle-shutdown 30 --delete-unavailableStop all leases, delete pool simulators, and clear pool state:
maestro-manager downtest runs all .yaml and .yml files in flowsDir when no flow paths are
provided. Use --include-tags and --exclude-tags for manager-side tag
selection, and put Maestro passthrough arguments after a literal --.
State And Logs
State is scoped by pool.prefix under:
~/.maestro-manager/<prefix>Important paths include:
config-snapshot.jsonfor the last config snapshot written byup.sims/<sim-name>/udid,lastused, andlease/record.jsonfor pool slots and lease ownership.waiters/<runId>/ticket.jsonfor queued work.runs/index.json,runs/<runId>/run.json, andruns/<runId>/<flow>.logfor run history and per-flow Maestro output.
maestro-manager down runs stop --all, deletes all pool simulators it can
resolve, then removes ~/.maestro-manager/<prefix>.
Verification
Run the full local check:
bun run checkRun the no-excuse audit baseline check:
bash scripts/check-no-excuse-baseline.sh src