Claude
Skills
Sign in
Back

baguette-ios-simulator

Included with Lifetime
$97 forever

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

Design

What this skill does


# 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 {

Related in Design