simulator-buddy is a macOS CLI for selecting iOS simulators, physical devices, and local Mac destinations without falling back to terminal menus. It is designed for tools like Codex actions that need a real UDID back on stdout and want a native picker window instead of an in-terminal select flow.
It can also wrap xcodebuild: replace xcodebuild ... with
simulator-buddy ... and the selected destination is injected before the real
xcodebuild command runs.
simulator-buddy does not emulate xcrun in v1.
Requirements:
- macOS 15 or newer
Homebrew:
brew install ipavlidakis/tap/simulator-buddymise:
mise use -g github:ipavlidakis/simulator-buddyDirect release download:
- Download the matching archive from the latest GitHub release.
- Extract it.
- Move
simulator-buddysomewhere on yourPATH.
swift build
swift test
swift run simulator-buddy --helpList destinations:
simulator-buddy list --type all --format table
simulator-buddy list --type simulator --format json
simulator-buddy list --type macos --format jsonMac run destinations for a specific Xcode scheme:
simulator-buddy list --type macos \
--xcode-project ./MyApp.xcodeproj --xcode-scheme MyApp --format json
# Only "Mac Catalyst" or only "Designed for iPad/iPhone" rows for that scheme:
simulator-buddy list --type macos-catalyst \
--xcode-project ./MyApp.xcodeproj --xcode-scheme MyApp --format json
simulator-buddy list --type macos-designed-for-ipad \
--xcode-project ./MyApp.xcodeproj --xcode-scheme MyApp --format jsonResolve the last used destination:
simulator-buddy last --type simulator
simulator-buddy last --type all --scope my-workspace --format jsonOpen the native picker and print the selected UDID:
simulator-buddy select --type all
simulator-buddy select --type device --scope my-workspace
simulator-buddy select --type macos --scope my-workspace
simulator-buddy select --type macos-designed-for-ipad \
--xcode-project ./MyApp.xcodeproj --xcode-scheme MyApp --format json
simulator-buddy select --type simulator --format jsonWrite LLDB attach commands for a selected destination:
simulator-buddy debug \
--type all \
--process-name MyApp \
--lldb-command-file /tmp/myapp-attach.lldbAttach with LLDB:
lldb -s /tmp/myapp-attach.lldbRun LLDB attach directly:
simulator-buddy attach --type all --process-name MyApp
simulator-buddy attach \
--destination "platform=iOS Simulator,id=SIM-UDID-1" \
--process-name MyAppInstall and launch an app on an iOS simulator:
simulator-buddy run \
--destination "platform=iOS Simulator,id=SIM-UDID-1" \
--app ./Build/Products/Debug-iphonesimulator/MyApp.appInstall and launch on a physical device, or open a local Mac app:
simulator-buddy run \
--destination "platform=iOS,id=DEVICE-UDID-1" \
--app ./Build/Products/Debug-iphoneos/MyApp.app
simulator-buddy run \
--destination "platform=macOS,arch=arm64,variant=Designed for iPad,id=MAC-ID-1" \
--app ./Build/Products/Debug-iphoneos/MyApp.appUse --skip-install to foreground an app that is already installed.
For Designed-for-iPad-on-Mac builds, run wraps the generated iPhoneOS .app
in a stable macOS launcher bundle before opening it. The wrapper lives under
~/Applications/simulator-buddy, so macOS may ask for approval on first run and
reuse the same container afterwards.
Build, select a valid destination, install if needed, and launch:
simulator-buddy run \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Debug
simulator-buddy run \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Debug \
--type simulatorForward launch environment values exactly as provided:
simulator-buddy run \
--env MY_FLAG=1 \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration DebugFor Mac destinations, run passes each value through open --env KEY=VALUE.
For devices, run passes values through devicectl --environment-variables.
For simulators, run adds the SIMCTL_CHILD_ transport prefix internally.
Filter simulator and Mac unified logs by category:
simulator-buddy run \
--log-category Video \
--log-category WebRTC \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Debug--log-category Video,WebRTC is equivalent. Simulator and Mac log streaming
uses process == "<CFBundleExecutable>" plus the selected category predicates.
Physical devices still use devicectl --console; CoreDevice provides the live
console attachment there, but not the same category-filtered unified log stream.
Wrap xcodebuild and pick a valid destination for the scheme:
simulator-buddy \
-workspace MyApp.xcworkspace \
-scheme MyApp \
test--type defaults to all when omitted.
| Destination | Run launch | Run logs | Attach |
|---|---|---|---|
| iOS simulator | simctl install, simctl launch |
simctl spawn <udid> log stream, scoped to app process and optional categories |
LLDB platform select ios-simulator, then process attach |
| Physical iOS device | devicectl device install app, devicectl device process launch --console |
devicectl --console app output; category filters are not applied |
LLDB device select <udid>, then device process attach |
| Native Mac | open -n |
host log stream, scoped to app process and optional categories |
LLDB process attach |
| Designed for iPad/iPhone on Mac | stable wrapper in ~/Applications/simulator-buddy, then open -n |
host log stream, scoped to wrapped app process and optional categories |
LLDB process attach |
Ctrl-C, Zed task stop, and task cancellation are forwarded to the active
child process so console-backed runs stop the launched app instead of leaving a
background devicectl or app process behind.
list --format tableprints a human-readable table.list --format jsonprints an array of normalized destination records. For Mac rows loaded via--xcode-project/--xcode-workspace, each record includesmacOSVariantandxcodeDestinationSpecifier(suitable forxcodebuild -destination).lastprints the selected UDID by default, or a JSON selection payload with--format json.selectprints the selected UDID by default, or a JSON selection payload with--format json.debugrecords the selected destination, writes an LLDB command file, and prints a JSON payload withdestination,scope,selectedAt, andlldbCommandFile.attachrecords picker selections, writes a temporary LLDB command file, runslldb -s <file>, streams LLDB output, and returns LLDB's exit code.--destination <udid|specifier>skips the picker.run --app <path>installs and launches on iOS simulators withsimctl, physical devices withdevicectl, and Mac destinations withopen. Simulator runs also open Simulator.app on the selected device. Native Mac bundles open directly; Designed-for-iPad iPhoneOS bundles are copied into one stable wrapper per bundle identifier under~/Applications/simulator-buddyso macOS can launch them.- Simulator and Mac
runlaunches the app first, then streams app-scoped unified logs withlog stream.--log-category <category>filters those streams by category; repeat it or pass comma-separated values for multiple categories. When no category is provided, the stream is still scoped to the app process. - Physical-device
runkeeps usingdevicectl --consoleso terminal-visible device output stays attached to the launched process.--log-categoryis accepted but not applied to physical-device console output. - Interrupting
runforwards the terminal signal to the active child process. For console-attached launches, this also terminates the device app instead of leavingdevicectlor the app running in the background. run --app <path>and build-and-run both support--destination <udid|specifier>to skip the picker,--skip-installto launch without reinstalling, and repeated--env KEY=VALUEflags forwarded unchanged through the selected launch mechanism.run -project|-workspace ... -scheme ...runsxcodebuild -showdestinations, prompts with only scheme-valid destinations, runsxcodebuild build, resolves the built.appthroughxcodebuild -showBuildSettings, then installs/opens/launches it with the same run backend asrun --app. It uses Xcode's configured DerivedData unless the caller explicitly passes a build setting that changes Xcode output paths. Repeated--env KEY=VALUEflags apply only to the final app launch, not the build.- Raw
xcodebuildmode runsxcodebuild -showdestinationswhen project/workspace + scheme are present, prompts with only available scheme destinations, injects-destination <specifier>, streams realxcodebuildoutput, and returnsxcodebuild's exit code. - Raw
xcodebuildmode passes through unchanged when-destinationalready exists, scheme/project context is missing, or the invocation is info-only or clean-only. select,debug,attach, andrunexit130when the picker is cancelled.
The generated LLDB command file uses:
platform select ios-simulatorandprocess attachfor simulators.device select <udid>anddevice process attachfor physical devices.process attachfor local Mac destinations.
- Simulators are loaded from
xcrun simctl list devices available -j --json-output <file>. - Physical devices are loaded from
xcrun devicectl list devices --json-output <file>. - Mac destinations with no Xcode flags are loaded from
xcrun xctrace list devices(legacy). When you pass--xcode-schemeand--xcode-projector--xcode-workspace, Mac rows are loaded fromxcodebuild -showdestinationsfor that scheme. That yields one entry per local Mac variant (for example Mac Catalyst vs Designed for iPad/iPhone), each with the correctxcodeDestinationSpecifierforxcodebuild -destination. - Raw
xcodebuildmode usesxcodebuild -showdestinationsfor the requested project/workspace + scheme and shows only concrete, available iPhone, iPad, and Mac destinations from that output. - Only iPhone and iPad simulators/devices and available Mac destinations are included in v1.
- Successful destination fetches update the cache used by future destination-loading flows.
- Picker results come from the records loaded for the current command invocation.
All app data lives under:
~/Library/Application Support/com.ipavlidakis.simulator-buddy/
Files:
history/global.jsonhistory/scopes/<sha256>.jsoncache/destinations.json
Mac launcher wrappers for Designed-for-iPad/iPhone apps live outside this state directory at:
~/Applications/simulator-buddy/
The history tracks:
- last simulator
- last physical device
- last Mac
- last destination overall
The cache stores only normalized destination metadata and fetch timestamps.
This repository intentionally avoids shipping real local device identifiers in tracked files. Test fixtures and examples use sanitized values only.
Example .codex/environments/environment.toml snippets for a consuming repo:
[[actions]]
name = "Choose iOS Destination"
icon = "run"
command = "simulator-buddy select --type all"
[[actions]]
name = "Run App On Chosen Destination"
icon = "run"
command = """
simulator-buddy run \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Debug
"""Debug a process on a chosen destination:
[[actions]]
name = "Debug App On Chosen Destination"
icon = "bug"
command = """
simulator-buddy attach --type all --process-name MyApp
"""Run a built simulator app:
[[actions]]
name = "Run App On Chosen Simulator"
icon = "run"
command = """
simulator-buddy run --type simulator --app ./Build/Products/Debug-iphonesimulator/MyApp.app
"""This keeps destination choice native and interactive while preserving
xcodebuild and LLDB exit codes for shell scripts and Codex actions.
Example .zed/tasks.json task:
[
{
"label": "Run App",
"command": "simulator-buddy",
"args": [
"run",
"--log-category",
"Video",
"-project",
"MyApp.xcodeproj",
"-scheme",
"MyApp",
"-configuration",
"Debug"
],
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": false,
"allow_concurrent_runs": false,
"reveal": "always",
"hide": "never",
"save": "all"
}
]Stopping the task sends a terminal signal to simulator-buddy, which forwards
it to the active child process.
Release assets are expected to use these exact names:
simulator-buddy-aarch64-apple-darwin.tar.gzsimulator-buddy-x86_64-apple-darwin.tar.gzchecksums.txtsimulator-buddy.rb
The Homebrew formula source is rendered from version + checksum metadata:
./Scripts/render_homebrew_formula.sh \
--version <version> \
--repo ipavlidakis/simulator-buddy \
--checksums-file dist/checksums.txtThe output is intended to be committed in the companion tap repo.
The manual release workflow accepts an optional changelog input. When it is
provided, that text becomes the GitHub release notes; otherwise new releases use
generated notes.