Skip to main content

baguette-ios-simulator

Headless iOS Simulator manager with host-side HID input injection, 60fps streaming, and device farm web UI for iOS 26

Jump to install

Source facts

Repository
reason-machines/trending-skills
Last source activity
May 3, 2026 at 09:56
Detected SKILL.md language
English
Stars
80
Forks
15

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
baguette-ios-simulator
description
Headless iOS Simulator manager with host-side HID input injection, 60fps streaming, and device farm web UI for iOS 26
triggers
["control iOS simulator headlessly","inject touch input into simulator","stream simulator screen","manage simulator device farm","tap swipe gesture simulator programmatically","boot simulator without GUI","simulator web UI dashboard","simulate multi-finger pinch zoom on iOS"]
# Baguette iOS Simulator Manager > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. Baguette is a Swift CLI tool that creates, boots, and shuts down iOS Simulator devices, streams their screens at 60fps, and injects taps/swipes/multi-finger gestures entirely headlessly — no Simulator.app GUI required. It also serves a self-contained web UI for single-device and multi-device (farm) control. ## Requirements - Apple Silicon Mac only - macOS 15+ - Xcode 26 (links against private SimulatorKit/CoreSimulator frameworks) ## Install ```bash brew install tddworks/tap/baguette ``` ### Build from Source ```bash git clone https://github.com/tddworks/baguette cd baguette make # release build via ./build.sh swift test # run the test suite ``` ## Key CLI Commands ### Device Management ```bash # List all simulators (default + custom sets) baguette list # Boot a simulator headlessly (no Simulator.app window) baguette boot --udid <UDID> # Shutdown a simulator baguette shutdown --udid <UDID> ``` ### Screen Streaming ```bash # Stream frames to stdout as MJPEG (default) baguette stream --udid <UDID> --fps 60 --format mjpeg # Stream as H.264/AVCC baguette stream --udid <UDID> --fps 60 --format avcc # Pipe MJPEG to ffplay for local preview baguette stream --udid <UDID> --fps 30 --format mjpeg | ffplay -i - ``` ### One-Shot Gesture Input Coordinates are in **device points**; `--width`/`--height` are the simulator screen size in points. ```bash # Tap at a point baguette tap --udid <UDID> --x 219 --y 478 --width 438 --height 954 # Tap with custom duration baguette tap --udid <UDID> --x 219 --y 478 --width 438 --height 954 --duration 0.1 # Swipe from top to bottom (scroll down) baguette swipe --udid <UDID> \ --startX 219 --startY 190 \ --endX 219 --endY 760 \ --width 438 --height 954 # Pinch to zoom in (startSpread < endSpread) baguette pinch --udid <UDID> \ --cx 219 --cy 478 \ --startSpread 60 --endSpread 200 \ --width 438 --height 954 # Two-finger pan baguette pan --udid <UDID> \ --x1 175 --y1 478 \ --x2 263 --y2 478 \ --dx 0 --dy -100 \ --width 438 --height 954 # Hardware buttons baguette press --udid <UDID> --button home baguette press --udid <UDID> --button lock ``` ### Streaming Gesture Input (stdin JSON) For real-time or scripted gesture sequences, pipe newline-delimited JSON to `baguette input`: ```bash baguette input --udid <UDID> ``` Each line gets an ack: `{"ok":true}` or `{"ok":false,"error":"..."}`. ### Web UI Server ```bash # Start the web server (default port 8421) baguette serve # Custom port and host baguette serve --port 9000 --host 0.0.0.0 # Custom device set baguette serve --port 8421 --device-set /path/to/device-set # Open single-device dashboard open http://localhost:8421/simulators # Open multi-device farm dashboard open http://localhost:8421/farm ``` ### DeviceKit Chrome/Bezel Data ```bash # Print bezel layout JSON for a booted device baguette chrome layout --udid <UDID> # Write composite PNG (device screenshot + bezel) to stdout baguette chrome composite --udid <UDID> > screenshot.png # By device name (no UDID needed) baguette chrome layout --device-name "iPhone 17 Pro" baguette chrome composite --device-name "iPhone 17 Pro" > iphone17pro_bezel.png ``` ## Wire Protocol — Streaming Input via stdin Send newline-delimited JSON to `baguette input --udid <UDID>`: ```json {"type":"tap", "x":219, "y":478, "width":438, "height":954, "duration":0.05} {"type":"swipe", "startX":219, "startY":760, "endX":219, "endY":190, "width":438, "height":954, "duration":0.3} {"type":"touch1-down", "x":219, "y":478, "width":438, "height":954} {"type":"touch1-move", "x":225, "y":485, "width":438, "height":954} {"type":"touch1-up", "x":225, "y":485, "width":438, "height":954} {"type":"touch2-down", "x1":175, "y1":478, "x2":263, "y2":478, "width":438, "height":954} {"type":"touch2-move", "x1":150, "y1":478, "x2":288, "y2":478, "width":438, "height":954} {"type":"touch2-up", "x1":150, "y1":478, "x2":288, "y2":478, "width":438, "height":954} {"type":"pinch", "cx":219, "cy":478, "startSpread":60, "endSpread":200, "width":438, "height":954} {"type":"button", "button":"home"} {"type":"button", "button":"lock"} {"type":"scroll", "deltaX":0, "deltaY":-50} ``` ## WebSocket Protocol (for Web UI / Custom Clients) Connect to `ws://localhost:8421/simulators/<UDID>/stream?format=mjpeg` (or `avcc`). ### Server → Client (binary frames) - **MJPEG**: raw JPEG bytes per message - **AVCC**: 1-byte tag prefix: - `0x01` — avcC description - `0x02` — keyframe - `0x03` — delta frame - `0x04` — JPEG seed frame (renders before H.264 IDR) ### Client → Server (text JSON) ```json {"type":"set_bitrate", "bps": 2000000} {"type":"set_fps", "fps": 30} {"type":"set_scale", "scale": 0.5} {"type":"force_idr"} {"type":"snapshot"} ``` Gesture input messages (same format as stdin wire protocol above) are also accepted over the WebSocket. ## Web UI Routes | Method | Path | Description | |--------|------|-------------| | `GET` | `/` | Redirects → `/simulators` | | `GET` | `/simulators` | Device list HTML | | `GET` | `/simulators.json` | `{running: [...], available: [...]}` | | `GET` | `/simulators/:udid` | Stream page HTML | | `POST` | `/simulators/:udid/boot` | Boot device | | `POST` | `/simulators/:udid/shutdown` | Shutdown device | | `GET` | `/simulators/:udid/chrome.json` | Bezel layout JSON | | `GET` | `/simulators/:udid/bezel.png` | Rasterized bezel PNG | | `WS` | `/simulators/:udid/stream` | Live stream + input | | `GET` | `/farm` | Multi-device farm HTML | ## Code Examples ### Scripting Gestures from Swift ```swift import Foundation // Build a gesture sequence as newline-delimited JSON func makeGestureScript() -> String { let gestures: [[String: Any]] = [ // Boot sequence: tap the app icon ["type": "tap", "x": 100, "y": 200, "width": 390, "height": 844, "duration": 0.05], // Scroll down in a list ["type": "swipe", "startX": 195, "startY": 600, "endX": 195, "endY": 200, "width": 390, "height": 844, "duration": 0.4], // Pinch to zoom ["type": "pinch", "cx": 195, "cy": 422, "startSpread": 50, "endSpread": 180, "width": 390, "height": 844], // Press home ["type": "button", "button": "home"] ] return gestures.compactMap { dict -> String? in guard let data = try? JSONSerialization.data(withJSONObject: dict) else { return nil } return String(data: data, encoding: .utf8) }.joined(separator: "\n") } // Run baguette input with the script func runGestureScript(udid: String, script: String) async throws { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/local/bin/baguette") process.arguments = ["input", "--udid", udid] let inputPipe = Pipe() let outputPipe = Pipe() process.standardInput = inputPipe process.standardOutput = outputPipe try process.run() let inputData = (script + "\n").data(using: .utf8)! inputPipe.fileHandleForWriting.write(inputData) inputPipe.fileHandleForWriting.closeFile() process.waitUntilExit() let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let acks = String(data: outputData, encoding: .utf8) ?? "" print("Acks:\n\(acks)") } ``` ### Listing and Booting Simulators ```swift import Foundation struct SimulatorInfo: Codable { let running: [Device] let available: [Device] struct Device: Codable { let udid: String let name: String let os: String let state: String } } func fetchSimulators(port: Int = 8421) async throws -> SimulatorInfo { let url = URL(string: "http://localhost:\(port)/simulators.json")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(SimulatorInfo.self, from: data) } func bootSimulator(udid: String, port: Int = 8421) async throws { let url = URL(string: "http://localhost:\(port)/simulators/\(udid)/boot")! var request = URLRequest(url: url) request.httpMethod = "POST" let (_, response) = try await URLSession.shared.data(for: request) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) } } // Usage Task { let sims = try await fetchSimulators() print("Running: \(sims.running.map(\.name))") if let first = sims.available.first { try await bootSimulator(udid: first.udid) print("Booted \(first.name)") } } ``` ### Connecting to a Live Stream WebSocket ```swift import Foundation func connectToSimulatorStream(udid: String, port: Int = 8421) { let url = URL(string: "ws://localhost:\(port)/simulators/\(udid)/stream?format=mjpeg")! let session = URLSession(configuration: .default) let task = session.webSocketTask(with: url) task.resume() // Reduce bitrate and fps for a thumbnail let setFps = URLSessionWebSocketTask.Message.string( #"{"type":"set_fps","fps":15}"# ) let setBitrate = URLSessionWebSocketTask.Message.string( #"{"type":"set_bitrate","bps":500000}"# ) task.send(setFps) { _ in } task.send(setBitrate) { _ in } // Receive MJPEG frames func receiveFrame() { task.receive { result in switch result { case .success(.data(let jpegData)): // jpegData is a raw JPEG — display in NSImageView / UIImageView print("Received frame: \(jpegData.count) bytes") receiveFrame() case .success(.string(let text)): print("Control message: \(text)") receiveFrame() case .failure(let error): print("Stream ended: \(error)") } } } receiveFrame() } ``` ### Sending Gestures over WebSocket ```swift func sendTapOverWebSocket(task: URLSessionWebSocketTask, x: Double, y: Double, width: Double, height: Double) { let gesture: [String: Any] = [ "type": "tap",
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub