maestro-manager
v0.1.0
Published
Daemonless iOS Maestro test runner with a shared simulator pool.
Downloads
218
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.4.0and 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. Concurrent test runs may temporarily boot lanes throughpool.max. When the final active run completes, the manager shuts down only idle overflow lanes untilpool.defaultsimulators remain warm. The slots stay available for later reuse.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 2If demand expands the pool above pool.default, passing and test-failing runs
both trigger a final convergence pass. The pass defers while any lease, queued
waiter, or full-suite reservation is active, so work in other worktrees is not
interrupted. It does not replace clean --idle-shutdown: automatic convergence
only shuts down idle overflow above pool.default, while explicit idle cleanup
retains its configured age-based behavior.
Stop 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
Production modules live under src/. Each executable suite mirrors its owning
module under tests/ and adds the .test.ts suffix—for example,
src/pool/convergence.ts is covered by tests/pool/convergence.test.ts.
Reusable fixtures and helpers live under tests/support/.
Run the full local check:
bun run checkRun the no-excuse audit baseline check:
bash scripts/check-no-excuse-baseline.sh src