Repository navigation
Release 1.1.28
This release is about running a Maestro suite and getting Maestro's answer. Most of it came from running real Maestro suites (DuckDuckGo Android and iOS, React Native's RNTester, React Navigation) side by side with Maestro and fixing each place the two disagreed. The largest of those is selector matching: text and id selectors now match the whole value, as Maestro does, which is a behaviour change — see below. DeviceLab becomes the default driver on both platforms: on iOS it runs a new agent, on simulators and on real iPhones. Every flow gets DEVICE_UDID, -e stops splitting values at commas, and the Appium driver closes a long list of gaps with the native drivers.
⚠️ Behaviour changes
DeviceLab is the default driver on Android and on iOS. Running without --driver now uses the on-device DeviceLab agent on both platforms: on Android instead of UIAutomator2, on iOS instead of WebDriverAgent, on simulators and real iPhones (a real iPhone still needs --team-id, as WDA did). Nothing changes for a run that names its driver. To keep the previous defaults, pass --driver uiautomator2 on Android or --driver wda on iOS; both drivers are unchanged. See Changed below for the numbers behind the switch.
Text and id selectors match the whole value, ignoring case, as in Maestro. A plain text selector used to match any element whose text, content description or hint contained it, so tapOn: Open could land on a "Talk · Open" row and assertVisible: English passed on "Language: English". Ids matched as substrings too: id: login found com.app:id/login_button. Maestro compiles every text and id selector as a case-insensitive regex that must match the whole value (an id may match the whole resource-id or the part after the package prefix), falls back to the literal text when the pattern does not compile, and also accepts a value equal to the pattern. That rule now applies on DeviceLab, UIAutomator2 and Appium on Android, on --driver devicelab and --driver devicelab-legacy on iOS, and on Appium for iOS — both in the queries sent to the device and in the page-source matchers. A flow that relied on substring matching must say so with a regex. The WDA driver keeps its existing predicate matching: it prefers an element whose whole text matches and falls back to a substring match when there is none (#188, #161).
- tapOn: "Open" # only an element whose whole text is "Open" (any case)
- tapOn: ".*Open.*" # any element whose text contains "Open"
- tapOn: { id: "login" } # "login" or "com.app:id/login", not "login_button"
- tapOn: { id: ".*login.*" } # the old substring behaviour, spelled outRegex selectors ignore case again. 1.1.26 made regex text selectors case-sensitive on the premise that Maestro does; it does not — every text and id selector is compiled with IGNORE_CASE, and suites depend on it ((let's get started!|Let's do it!) against "Let's get started!"). Regexes now ignore case. The problem 1.1.26 fixed is kept solved as a preference: when several elements match, those matching in the pattern's own case come first, so ^SIGN OUT$ still taps the "SIGN OUT" button ahead of a "Sign out" row (#151).
An index: past the number of matches finds no element. It used to fall back to the first match, so assertNotVisible: {id: row, index: 1} ("there is no second row") failed with one row on screen, and a tap with an out-of-range index hit the first row. Now it names no element, on every mobile driver (#188).
-e keeps commas in values. -e REPLY="Thanks, going with Vue" reached the flow as REPLY=Thanks. Each -e is now one variable, as in Maestro, so -e A=1,B=2 sets A to 1,B=2; pass -e A=1 -e B=2 instead (#189).
Element swipes go from the element's centre to 10% / 90% of the screen, as Maestro does on Android and iOS, instead of across 90% to -10% of the element's own bounds. On a chat list whose bottom edge sat under a docked composer, the old swipe began on the composer and the list never scrolled. Element swipes now travel further than before; point: and distance: keep their behaviour (#189).
Added
- New iOS driver:
--driver devicelabruns the DeviceLab iOS agent, a new XCUITest agent shipped prebuilt indrivers/ios/devicelab-ios-agent/, on simulators and real iPhones. It is the iOS default (see Changed);--driver wdaselects WebDriverAgent, and the previous DeviceLab runner is kept as--driver devicelab-legacyso the two can be compared on the same flows.- Simulators: the agent is installed and launched with
simctl(falling back toxcodebuild test-without-building), re-attached when a live agent of the same version is found, and left running between runs (DEVICELAB_IOS_AGENT_KEEP=0stops it). The agent's port is derived from the simulator's UDID, so parallel simulators do not collide. - Real iPhones: pass
--team-id. On the first run the prebuilt agent is re-signed for your team and device (through a small public signing-stub project that Xcode provisions), cached under~/.maestro-runner/cache/devicelab-ios-agent/device-signed/until the agent changes or its profile nears expiry, then started over usbmux. A start that fails is retried; an untrusted certificate, Developer Mode off, a locked phone or a rejected signature stops at once with what to do.DEVICELAB_IOS_AGENT_BUNDLE_IDsigns it under another bundle id for personal teams.clearKeychain,setPermissions,setLocation,addMedia, the clipboard and screen recording are not available on a device and say so. - Maestro semantics on the host: the agent returns candidates and Maestro's selector rules pick among them, with relative selectors, index, state and size filters. Only elements at least 10% on screen are looked up, as Maestro's out-of-bounds filter does, and the node budget counts on-screen nodes only, so the footer of a long web page is reachable. Taps, swipes and scrolls are synthesized touches in Maestro's shape and durations; after a step that can move the screen, the next action waits until the screen is still and the app is idle.
scrollUntilVisiblehonourscenterElementandspeed, and stops when the screen stops changing. Launch arguments follow Maestro's iOS rule (a boolean keeps its key as written). Element bounds are computed from truncated edges, as Maestro does, socropOnscreenshots match baselines Maestro took. - App lifecycle:
launchAppresets permissions then applies the flow's (all: allowwhen none are named) on a simulator;clearKeychain,killAppandlaunchApp: clearKeychainwork;clearStatereinstalls from a copy of the app staged once per build (DEVICELAB_IOS_FAST_CLEARSTATE=1empties the data container instead). On a simulator the driver turns Reduce Motion on and password AutoFill off before the app starts (DEVICELAB_IOS_SIM_PREFS=0leaves the simulator untouched). - Web content: a WebKit inspector client watches the app's web pages, so the settle after a tap also waits for the visible page to finish loading and for its network requests to stop (
DL_IOS_WEB_READY=0turns this off). - Recovery: an agent that dies is restarted once; read-only commands are re-sent, actions never are.
- The agent binaries are proprietary, free to use with maestro-runner; see
drivers/LICENSE-BINARIES.md.
# Simulator maestro-runner --platform ios --driver devicelab test flows/ # Real iPhone maestro-runner --platform ios --driver devicelab --team-id <TEAM_ID> \ --device <UDID> --app-file MyApp.app test flows/
- Simulators: the agent is installed and launched with
DEVICE_UDIDin every flow — the serial (Android) or UDID (iOS) of the device the flow runs on, the same for every flow on that device. Under--parallelan app that keys its mocks by a per-device launch argument can now tell the devices apart. ADEVICE_UDIDyou set yourself (shell environment or-e) is kept.runShell'sMAESTRO_DEVICE_IDalso names the worker's own device under--parallelinstead of the run's first one. Sharding (MAESTRO_SHARD_ID) is not supported (#187).- launchApp: arguments: deviceId: ${DEVICE_UDID}
-e NAMEtakes the value from the shell environment, asdocker -edoes, and warns whenNAMEis not set instead of dropping it silently (#189).API_TOKEN=abc123 maestro-runner test -e API_TOKEN -e USER=qa flows/disableAnimationson Android — Maestro's workspace settingplatform: android: disableAnimations: trueis honoured,--disable-animations(MAESTRO_DISABLE_ANIMATIONS) turns it on for a run, and a flow's own config can turn it on or off for that flow. The three global animation scales are set to 0 and restored to their saved values when the run or flow ends. DeviceLab and UIAutomator2 drivers; other drivers warn and carry on.- Maestro's legacy
action:spelling —- action: back(andhideKeyboard,scroll,clearKeychain,pasteText) parses to the same step as the modern form. It was rejected as an unknown step, so suites written with it failed to parse (30 of DuckDuckGo Android's 166 flows). --app-fileaccepts an http(s) URL — point it at an artifact store (S3, GCS, a CI artifact) instead of a local path, and the binary is downloaded once and cached under~/.maestro-runner/app-cache. A.zipis unpacked to the.appor.ipainside, so a zipped simulator build works directly.--app-file-sha256 <hex>verifies the download and makes the cache content-addressed, so a changed artifact at the same URL is refetched. Presigned URLs are handled safely: the URL is only ever printed with its query string redacted, so the signature never reaches a log, report, or error message. Honours--insecurefor a self-signed host.maestro-runner --app-file https://ci.example.com/builds/app.apk \ --app-file-sha256 3b1f… test flows/- CommonJS
requireinrunScript— a script can now pull in a helper module that lives beside the flow:const helpers = require('./helpers.js'), where the module setsmodule.exports. Relative paths resolve against the flow's directory, nested requires resolve relative to the requiring module (Node's algorithm), and an absolute path is read as given. Only CommonJS is supported — goja has no ES-module loader, soimportis not available. This is a maestro-runner extension; Maestro'srunScripthas no module system.- runScript: | const { formatDate } = require('./lib/dates.js') output.today = formatDate(new Date())
waitstep — a plain time delay, for the times a flow needs to pause for a fixed duration rather than for a condition. The duration is in milliseconds, like every other timing field. This is a maestro-runner extension; Maestro has no plain wait (onlyextendedWaitUntilandwaitForAnimationToEnd), so a flow that uses it will not run on Maestro. It waits on the run's context, soCtrl-Cduring a long wait stops the run promptly. Prefer a condition wait where one fits; reach for this only for a genuine fixed pause.- wait: 2000 # pause 2 seconds - wait: { duration: 500, optional: true }
--insecureforrunScriptHTTP calls —http.get/post/put/deletein arunScriptblock can now reach a self-signed or otherwise untrusted TLS endpoint. Pass--insecure(orMAESTRO_INSECURE=1) to skip certificate verification for every request in the run, or opt in per request withhttp.get(url, { insecure: true }), which overrides the run-wide default either way. Verification stays on unless asked for.
Changed
- DeviceLab is now the default Android driver. Running Android without
--driveruses the on-device DeviceLab agent instead of UIAutomator2. On the TestHive suites, measured on a Pixel 4a, DeviceLab matched UIAutomator2's pass rate and ran about 1.5-2x faster per flow (auth, products, cart, and the system media picker). UIAutomator2 is unchanged and still one flag away:--driver uiautomator2. - DeviceLab is now the default iOS driver. Running iOS without
--driveruses the DeviceLab iOS agent instead of WebDriverAgent, on simulators and real iPhones (a real iPhone still needs--team-id, as WDA did). On the simulator suites it passed everything WDA passed and more — TestHive 4/4 every run where WDA loses one to its alert race, RNTester 48/48, React Navigation 39/39 — and it runs about 30% faster per step. On a real iPhone SE (iOS 26.6.2) it passed 8 of the 9 TestHive flows; the ninth (search) failed the same way on the simulator, and is fixed below — the 9 now pass on the simulator. WDA is unchanged and one flag away:--driver wda. The web default (cdp) is unaffected. - DeviceLab Android settles like Maestro. After a tap, the next action waits for a 200ms quiet window on screen (it was 500ms); after steps that start a page load (
openLink, pressing Enter) the window stays 500ms, andopenLinkfirst waits up to 3s for the screen to change. A check between a tap and the next action no longer cancels that settle, and with a WebView connected the action also waits for the page to finish loading. A required element is looked for 17s, as Maestro's lookup timeout, instead of 12s.launchApp's defaultall: allowgrants only the permissions the app declares, read once per run, instead of all 23 known ones (about 0.7s per launch) (#189). when:/while:checks get Maestro's time budget — 7s less the time since the last step that could change the app, never below 1s, instead of a fixed 1s. A check right after a quick step now still catches a dialog that is on its way; a present element still resolves at once.--condition-timeoutsets a fixed budget only when given, and a per-conditiontimeout:still wins.waitToSettleTimeoutMson a driver with a native settle (DeviceLab) waits for the screen to stay unchanged for 500ms, up to the step's timeout, instead of comparing two hierarchy reads 200ms apart.- The WDA driver swipes, settles and taps like Maestro. Swipes are W3C touch actions with no hold before the move (the old hold made iOS take a scroll as a tap), scrolls run from the centre to 10% from the edge, the next action after a screen-moving step waits for two identical screenshots (up to 3s), and
tapOntaps the element's on-screen centre, keeping WDA's element click as the fallback. Of nested matches with the same id, the innermost is tapped (a SwiftUI Toggle's switch, not its row).scrollUntilVisibletakes the match that is on screen and uses the timeout as its budget unlessmaxScrollsis set. The client talks to127.0.0.1rather thanlocalhost. - The previous DeviceLab iOS runner (now
--driver devicelab-legacy) restarts its on-device runner if it dies mid-session, instead of failing every remaining step with "connection refused". A read-only command is retried on the relaunched runner; an action is not replayed (so a crash-inducing tap cannot loop), but the runner is back for the next step. Proven by killing the runner mid-suite: the run still completed green. - Bundled WebDriverAgent updated from 11.1.3 to 16.12.8. Brings upstream fixes for taps landing off-target in iPad compatibility mode, stale element snapshots on long-lived elements, a bounded accessibility request path so a frozen app cannot hang the server, and screenshots that no longer queue behind a stuck command. The runner now needs iOS 15 or newer (WDA raised its deployment target). The WDA build cache is now keyed by the bundled WDA version as well as the simulator or device configuration, so the first run after this upgrade (or after
wda update) rebuilds the runner once instead of silently launching the previous version's build. The session no longer sends theshouldUseTestManagerForVisibilityDetectioncapability, which WDA removed in 12.0. scrollUntilVisiblestops as soon as the screen stops moving — two scrolls that change nothing, or a rubber-band bounce off the edge — instead of spending every scrollmaxScrollsallows on a target that is not in the list. The failure message says so ("made no progress after N scrolls"). Android (uiautomator2, devicelab), iOS (wda, devicelab) and Appium; the browser driver keeps its fixed 10-scroll cap.--typing-frequencynow also throttleskeyPress-mode typing on the uiautomator2 Android driver. The flag (default 30 keys/sec, override per flow withtypingFrequency:) is applied by the runner to every driver that types character by character: the WDA and DeviceLab iOS drivers for all typing, and uiautomator2 forkeyPress-modeinputText, which now adds a per-character pause (1000/frequency ms) so apps that drop fast key events — notably React Native fields — receive every keystroke. Lower it if a field still loses characters. Two drivers are unaffected by design: the default DeviceLab Android driver types through its on-device agent (a change there needs the agent, not the runner), and normal AndroidinputTextsets the whole string at once so it never needed throttling.- The prebuilt DeviceLab agents (Android APKs and the iOS agent) are covered by a binaries license,
drivers/LICENSE-BINARIES.md: free to use with maestro-runner for any purpose, redistributable unmodified as part of maestro-runner, not open source. The rest of the repository stays Apache 2.0.
Fixed
- DeviceLab iOS: a tap on an id two siblings share goes to the control. TestHive's search icon and its text field both carry the id
search-bar, the icon first; the tap hit the icon, the field never took focus, andinputTextfailed withNO_FOCUSon every device. Of matches that rank alike, a text field, button or other control now comes before an image or a label — the element WDA's "first displayed" rule lands on, and Maestro passes the flow too. - DeviceLab iOS:
hideKeyboardno longer fails when the keyboard stays up. It tries Maestro's two dismiss swipes and moves on, as Maestro does; some screens do not dismiss the keyboard on a drag, and the flow's next steps decide. - DeviceLab iOS:
eraseTextbetween a tap andinputTextkeeps the tap point, soinputTextcan still re-tap the field when nothing has keyboard focus. inputTextafter atapOntypes into the field the tap focused on DeviceLab Android. A tap that landed while a bottom sheet was still sliding in did nothing, the previous field kept focus, and the text went there.inputTextnow waits up to 1.5s for a focused field on the tapped element and taps a text field once more if it never takes focus; a tap that worked costs nothing. The DeviceLab iOS agent does the same.inputTextkeeps its set-the-text semantics;keyPress: trueappends (#189).takeScreenshotandassertScreenshoton DeviceLab Android capture full resolution, lossless. They used the agent's half-size JPEG, so acropOncrop came back at half the size and everyassertScreenshotagainst a baseline taken by Maestro or another driver failed on size. They now capture throughadb exec-out screencap -p, falling back to the agent's image if adb fails.- A text selector on iOS never matches the accessibility identifier. WDA and Appium matched text against
name, which XCUITest fills with the accessibility identifier (React Native'stestID), sotapOn: Opentapped a button whose testID wasopen-scannerand whose label was "Scan code". Text now matches the label and value, as in Maestro;id:still matches the identifier (#178). - WDA
tapOnreported success with nothing tapped. Text equal to a key name (Delete,Return,Enter,Tab,Space,Backspace) was sent as that key before any lookup; now the element is looked for first and the key is sent only when none is on screen and a keyboard is up. During a push both screens hold an element with the same id, and the lookup took the outgoing one; it now prefers the displayed match, re-looks up after a failed click and never taps off screen. An action right after a tap waits for the tapped element to come to rest, and an action afterscrollUntilVisiblewaits for the list to stop, since iOS takes a tap on a moving list as "stop scrolling" (#179). - Android selector parity: a regex text selector also matches hint text; an anchored id (
^auth\.login\.emailInput$) matches an id with a package prefix; ids match regardless of case; text containing a"matches on the agent path; an exact match that takes up no space (a collapsed 0px banner) does not count as found;text: ""matches elements with no text instead of failing with "no selector specified". killAppon Android is a system process death, as in Maestro (am kill): the app keeps its saved state and restores it on the next launch. It ranam force-stop, which isstopApp. As in Maestro, it only kills an app in the background.- Executor fixes:
${...}is expanded afresh on every pass of arepeat,retryorrunFlow, so a repeat typing${output.d[output.i]}no longer types the first item every time; arunFlowwhen:condition is re-evaluated on every retry attempt (#176);runShellandpasteTextwork insiderunFlow—runShellfailed as unsupported, andpasteTextpasted the device clipboard instead of whatcopyTextFromsaved (#174); only steps that can change the app restart thewhen:budget clock; a find's short probes count against its timeout, so awhen:check no longer takes 10-12s against a 7s budget. - A swipe's non-numeric
speed:(such asspeed: fast) is ignored, as in Maestro, instead of failing validation for the whole flow. - A retype after dropped characters keeps what the field held. The retry cleared the field and typed only the step's text, so "abc" followed by a dropped "defgh" came back as "defgh". DeviceLab Android now also reads a field back from the device after typing, so dropped characters are detected and retyped there.
- Appium driver parity with the native drivers: clipboard steps work on Appium 3;
LANDSCAPE_LEFT,LANDSCAPE_RIGHTandUPSIDE_DOWNon Android;pressKeyon Android takes the same keys as uiautomator2; a literal id on Android tries the exact id first, then the whole-id rule;swipehonoursdistance:for screen swipes;tapOnanddragAndDropapplypoint:inside the matched element (#175); elements outside the viewport are not visible;launchAppapplies permissions on every launch, with allow and deny;takeScreenshotandassertScreenshothonourcropOn;inputTexton iOS no longer types into a field the flow left; every window is read on Android and deep trees on iOS;eraseText's delete fallback works on iOS;scrollandscrollUntilVisiblego left and right; tap selectors with text plus id or state match every field; the focused element is looked up with GET. - Appium on iOS no longer dumps the page source after every miss. On a large tree over a cloud endpoint that call took ~20s and could exceed Appium's 60s limit, ending the session. After a native miss, one more predicate query covering what the page-source matcher accepts now proves absence; any other error falls back as before. On the iOS auth suite, page-source requests went from 804 to 8 (#173).
- DeviceLab Android reliability: the bundled agent finds other apps' launch intents on Android 11+ and no longer launches with
CLEAR_TASK, which wiped a task restored after process death; its settle compares fresh tree signatures (it had reported settled at once); an installed agent that matches the bundled APK is kept instead of being reinstalled on every start; a stale "Process crashed." line from a previous run no longer kills a healthy start; a WebView CDP handshake cannot hang past its connect timeout, and a socket that keeps failing is backed off up to a minute; the report's hierarchy covers every window. - The legacy DeviceLab iOS runner:
eraseTexthonours its count and passes with nothing focused, as in Maestro; typing""in replace mode clears the field; an app that is off screen has no visible elements, soassertNotVisibleafterstopApppasses; a snapshot follows the screen when the target app leaves it. - Flutter VM discovery is skipped for iOS simulator apps without
Flutter.framework, saving about 2.4s on the first element step of every non-Flutter run. - Allure reports now include screenshot files in
allure-results, with distinct attachment names for screenshots from different source paths. This also applies to nested steps (#186). - JUnit
<failure>now carries the error in the element body, not just themessageattribute. Maestro writes the failure reason in the body, and CI parsers written against Maestro read it there; ours put the error only in the attribute and left a step description in the body, so a Maestro-tuned parser saw no real error — which defeats being drop-in (caught reviewing the Expo EAS integration). The error now appears in both places; the body falls back to the step description only when no command-level error was captured. - WDA now reads a field back after typing and retypes dropped characters. XCUITest typing can silently lose a character when the app janks — notably digits in Expo / React Native number fields, so
4049876574lands as409876574. WheninputTexttargets an element, the WDA driver now reads the value after typing and, if characters are missing, clears the field and types once more, the same recovery the Android drivers already do. Combined with the default 30 keys/sec throttle, this closes the iOS side of the long-standing fast-typing character loss. - WDA rejects missing or invalid element bounds and retries selector lookup, preventing stale elements from producing successful taps at
(0, 0)(#168). - Deep React Native (New Architecture) screens were clipped out of the hierarchy. WDA snapshots the accessibility tree to a default depth of 50, so a Fabric view tree deeper than that dropped its leaf elements and a
tapOnnear the bottom reported "not found". The session now raises the snapshot depth to 100 on the WDA and legacy DeviceLab iOS drivers; override withMAESTRO_WDA_SNAPSHOT_MAX_DEPTH/DEVICELAB_SNAPSHOT_MAX_DEPTH(#171). - WDA on a physical iPhone no longer pins the build destination architecture. 1.1.27 pinned
arch=arm64to avoid an ambiguous arm64/arm64e resolution, but on some devices that produced a destinationxcodebuildcould not match and the runner never started. The physical-device destination is nowplatform=iOS,id=<udid>with no arch, letting Xcode pick;MAESTRO_WDA_DEST_ARCHstill pins one if needed, andanyis the explicit no-pin (#172). - A run's
appIdis taken from the first flow that declares one, not from the first flow alone. A suite could lead with a setup or shared-steps flow that carries noappId; the app-version lookup and defaultlaunchApptarget then went empty even though later flows declared one. The runner now scans the flows for the first declaredappId(or weburl). - Report artifact capture: the view hierarchy is written with the extension that matches its content (JSON for the native iOS snapshot, XML elsewhere) instead of always
.xml; anassertScreenshotdiff image is written into the report's assets keyed by command index rather than beside the reference file outside the report; a failed step's after-screenshot no longer overwrites one an explicittakeScreenshotcaptured; and a failure insiderunFlow/repeat/retrynow saves its own screenshot and hierarchy.
Known issues
- DuckDuckGo Android
privacy_tests/8fails its first attempt more often than on 1.1.27 with the DeviceLab driver, and passes on the flow'sretry:. It fails where a tap on a WebView element (id: pay-button) does not take the app to the next page, so the privacy dashboard that follows shows no trackers. Being looked at for 1.1.29.
Contributors
- Reported the selector differences from Maestro on DeviceLab Android — substring text and id, out-of-range index (#188)
- Reported the missing settle after actions,
inputTextlanding in the wrong field, and-esplitting values at commas (#189)
- Reported and first patched plain text selectors matching by substring ahead of exact matches (#161)
- Asked for a per-device identifier in parallel runs, which became
DEVICE_UDID(#187)
- Reported WDA text selectors matching the accessibility identifier (#178)
- Reported WDA
tapOnsucceeding with nothing tapped (#179)
- Reported Appium
tapOnignoring an element-relativepoint:(#175) - Reported inline
retrynot re-evaluating arunFlowcondition, with a minimal repro (#176)
- Reported
runShellfailing insiderunFlow(#174)
- Reported the Appium iOS page-source fallback exceeding Appium's 60s limit on large trees, with a proposed fix (#173)
- Reported the WDA physical-device build destination failure (#172)
- Reported deep React Native trees missing from the WDA hierarchy (#171)
- Fixed WDA accepting missing or invalid element bounds (#168)
- Fixed missing Allure screenshot attachments (#186)
Installation
Quick Install
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bashInstall a specific version
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.28Manual install (from the assets below)
# 1. Download the tarball for your platform.
# 2. Extract:
tar -xzf maestro-runner-1.1.28-darwin-arm64.tar.gz
# 3. Run it from the extracted folder (it finds its drivers next to it),
# or move the folder to ~/.maestro-runner and put its bin/ on your PATH:
./maestro-runner/bin/maestro-runner --versionVerify Installation
maestro-runner --versionPlatform Support
- macOS Intel (amd64) - Signed & Notarized
- macOS Apple Silicon (arm64) - Signed & Notarized
- Linux amd64
- Linux arm64
Built by DeviceLab.dev