baguette-ios-simulator

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Baguette iOS Simulator Manager

Baguette iOS Simulator Manager

Skill by 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.
ara.so开发的技能工具 — 2026每日技能合集。
Baguette是一款Swift CLI工具,可创建、启动和关闭iOS Simulator设备,以60fps的帧率传输设备画面,并且完全以无头模式注入点击/滑动/多指手势操作——无需Simulator.app图形界面。它还提供一个独立的Web UI,支持单设备和多设备(集群)控制。

Requirements

要求

  • Apple Silicon Mac only
  • macOS 15+
  • Xcode 26 (links against private SimulatorKit/CoreSimulator frameworks)
  • 仅支持Apple Silicon芯片的Mac
  • macOS 15及以上版本
  • Xcode 26(依赖私有SimulatorKit/CoreSimulator框架)

Install

安装

bash
brew install tddworks/tap/baguette
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
bash
git clone https://github.com/tddworks/baguette
cd baguette
make           # 通过./build.sh进行发布构建
swift test     # 运行测试套件

Key CLI Commands

核心CLI命令

Device Management

设备管理

bash
undefined
bash
undefined

List all simulators (default + custom sets)

列出所有模拟器(默认设备集+自定义设备集)

baguette list
baguette list

Boot a simulator headlessly (no Simulator.app window)

以无头模式启动模拟器(无Simulator.app窗口)

baguette boot --udid <UDID>
baguette boot --udid <UDID>

Shutdown a simulator

关闭模拟器

baguette shutdown --udid <UDID>
undefined
baguette shutdown --udid <UDID>
undefined

Screen Streaming

画面流传输

bash
undefined
bash
undefined

Stream frames to stdout as MJPEG (default)

将画面以MJPEG格式输出到标准输出(默认)

baguette stream --udid <UDID> --fps 60 --format mjpeg
baguette stream --udid <UDID> --fps 60 --format mjpeg

Stream as H.264/AVCC

以H.264/AVCC格式传输画面

baguette stream --udid <UDID> --fps 60 --format avcc
baguette stream --udid <UDID> --fps 60 --format avcc

Pipe MJPEG to ffplay for local preview

将MJPEG画面管道输出到ffplay进行本地预览

baguette stream --udid <UDID> --fps 30 --format mjpeg | ffplay -i -
undefined
baguette stream --udid <UDID> --fps 30 --format mjpeg | ffplay -i -
undefined

One-Shot Gesture Input

单次手势输入

Coordinates are in device points;
--width
/
--height
are the simulator screen size in points.
bash
undefined
坐标为设备点
--width
/
--height
为模拟器屏幕的点尺寸。
bash
undefined

Tap at a point

在指定位置点击

baguette tap --udid <UDID> --x 219 --y 478 --width 438 --height 954
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
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
baguette swipe --udid <UDID>
--startX 219 --startY 190
--endX 219 --endY 760
--width 438 --height 954

Pinch to zoom in (startSpread < endSpread)

捏合放大(startSpread < endSpread)

baguette pinch --udid <UDID>
--cx 219 --cy 478
--startSpread 60 --endSpread 200
--width 438 --height 954
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
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
undefined
baguette press --udid <UDID> --button home baguette press --udid <UDID> --button lock
undefined

Streaming Gesture Input (stdin JSON)

流式手势输入(标准输入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":"..."}
.
对于实时或脚本化的手势序列,将换行分隔的JSON管道输入到
baguette input
bash
baguette input --udid <UDID>
每一行都会收到响应:
{"ok":true}
{"ok":false,"error":"..."}

Web UI Server

Web UI服务器

bash
undefined
bash
undefined

Start the web server (default port 8421)

启动Web服务器(默认端口8421)

baguette serve
baguette serve

Custom port and host

自定义端口和主机地址

baguette serve --port 9000 --host 0.0.0.0
baguette serve --port 9000 --host 0.0.0.0

Custom device set

使用自定义设备集

baguette serve --port 8421 --device-set /path/to/device-set
baguette serve --port 8421 --device-set /path/to/device-set

Open single-device dashboard

打开单设备控制面板

Open multi-device farm dashboard

打开多设备集群控制面板

DeviceKit Chrome/Bezel Data

DeviceKit Chrome/边框数据

bash
undefined
bash
undefined

Print bezel layout JSON for a booted device

输出已启动设备的边框布局JSON

baguette chrome layout --udid <UDID>
baguette chrome layout --udid <UDID>

Write composite PNG (device screenshot + bezel) to stdout

将合成PNG(设备截图+边框)输出到标准输出

baguette chrome composite --udid <UDID> > screenshot.png
baguette chrome composite --udid <UDID> > screenshot.png

By device name (no UDID needed)

通过设备名称操作(无需UDID)

baguette chrome layout --device-name "iPhone 17 Pro" baguette chrome composite --device-name "iPhone 17 Pro" > iphone17pro_bezel.png
undefined
baguette chrome layout --device-name "iPhone 17 Pro" baguette chrome composite --device-name "iPhone 17 Pro" > iphone17pro_bezel.png
undefined

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}
将换行分隔的JSON发送到
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)

WebSocket协议(适用于Web UI/自定义客户端)

Connect to
ws://localhost:8421/simulators/<UDID>/stream?format=mjpeg
(or
avcc
).
连接到
ws://localhost:8421/simulators/<UDID>/stream?format=mjpeg
(或
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)
  • MJPEG:每条消息为原始JPEG字节
  • AVCC:1字节标签前缀:
    • 0x01
      — avcC描述信息
    • 0x02
      — 关键帧
    • 0x03
      — 增量帧
    • 0x04
      — JPEG种子帧(在H.264 IDR之前渲染)

Client → Server (text JSON)

客户端 → 服务器(文本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.
json
{"type":"set_bitrate", "bps": 2000000}
{"type":"set_fps",     "fps": 30}
{"type":"set_scale",   "scale": 0.5}
{"type":"force_idr"}
{"type":"snapshot"}
手势输入消息(格式与上述标准输入有线协议相同)也可通过WebSocket发送。

Web UI Routes

Web UI路由

MethodPathDescription
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
方法路径描述
GET
/
重定向至 →
/simulators
GET
/simulators
设备列表HTML页面
GET
/simulators.json
返回
{running: [...], available: [...]}
格式数据
GET
/simulators/:udid
画面流页面HTML
POST
/simulators/:udid/boot
启动设备
POST
/simulators/:udid/shutdown
关闭设备
GET
/simulators/:udid/chrome.json
边框布局JSON数据
GET
/simulators/:udid/bezel.png
光栅化边框PNG图片
WS
/simulators/:udid/stream
实时画面流+输入控制
GET
/farm
多设备集群HTML页面

Code Examples

代码示例

Scripting Gestures from Swift

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)")
}
swift
import Foundation

// 将手势序列构建为换行分隔的JSON
func makeGestureScript() -> String {
    let gestures: [[String: Any]] = [
        // 启动流程:点击应用图标
        ["type": "tap", "x": 100, "y": 200, "width": 390, "height": 844, "duration": 0.05],
        // 在列表中向下滚动
        ["type": "swipe", "startX": 195, "startY": 600,
         "endX": 195, "endY": 200, "width": 390, "height": 844, "duration": 0.4],
        // 捏合放大
        ["type": "pinch", "cx": 195, "cy": 422,
         "startSpread": 50, "endSpread": 180, "width": 390, "height": 844],
        // 按下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")
}

// 使用脚本运行baguette input
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("响应:\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)")
    }
}
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)
    }
}

// 使用示例
Task {
    let sims = try await fetchSimulators()
    print("运行中: \(sims.running.map(\.name))")
    if let first = sims.available.first {
        try await bootSimulator(udid: first.udid)
        print("已启动 \(first.name)")
    }
}

Connecting to a Live Stream WebSocket

连接实时画面流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()
}
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()

    // 降低码率和帧率以生成缩略图
    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 }

    // 接收MJPEG帧
    func receiveFrame() {
        task.receive { result in
            switch result {
            case .success(.data(let jpegData)):
                // jpegData为原始JPEG数据 — 可在NSImageView/UIImageView中显示
                print("收到帧: \(jpegData.count) 字节")
                receiveFrame()
            case .success(.string(let text)):
                print("控制消息: \(text)")
                receiveFrame()
            case .failure(let error):
                print("流已结束: \(error)")
            }
        }
    }
    receiveFrame()
}

Sending Gestures over WebSocket

通过WebSocket发送手势

swift
func sendTapOverWebSocket(task: URLSessionWebSocketTask, x: Double, y: Double,
                           width: Double, height: Double) {
    let gesture: [String: Any] = [
        "type": "tap",
        "x": x, "y": y,
        "width": width, "height": height,
        "duration": 0.05
    ]
    guard let data = try? JSONSerialization.data(withJSONObject: gesture),
          let json = String(data: data, encoding: .utf8) else { return }
    task.send(.string(json)) { error in
        if let error { print("Send error: \(error)") }
    }
}

func sendPinchOverWebSocket(task: URLSessionWebSocketTask,
                             cx: Double, cy: Double,
                             startSpread: Double, endSpread: Double,
                             width: Double, height: Double) {
    let gesture: [String: Any] = [
        "type": "pinch",
        "cx": cx, "cy": cy,
        "startSpread": startSpread,
        "endSpread": endSpread,
        "width": width, "height": height
    ]
    guard let data = try? JSONSerialization.data(withJSONObject: gesture),
          let json = String(data: data, encoding: .utf8) else { return }
    task.send(.string(json)) { _ in }
}
swift
func sendTapOverWebSocket(task: URLSessionWebSocketTask, x: Double, y: Double,
                           width: Double, height: Double) {
    let gesture: [String: Any] = [
        "type": "tap",
        "x": x, "y": y,
        "width": width, "height": height,
        "duration": 0.05
    ]
    guard let data = try? JSONSerialization.data(withJSONObject: gesture),
          let json = String(data: data, encoding: .utf8) else { return }
    task.send(.string(json)) { error in
        if let error { print("发送错误: \(error)") }
    }
}

func sendPinchOverWebSocket(task: URLSessionWebSocketTask,
                             cx: Double, cy: Double,
                             startSpread: Double, endSpread: Double,
                             width: Double, height: Double) {
    let gesture: [String: Any] = [
        "type": "pinch",
        "cx": cx, "cy": cy,
        "startSpread": startSpread,
        "endSpread": endSpread,
        "width": width, "height": height
    ]
    guard let data = try? JSONSerialization.data(withJSONObject: gesture),
          let json = String(data: data, encoding: .utf8) else { return }
    task.send(.string(json)) { _ in }
}

Shell Script: Full Automation Flow

Shell脚本:完整自动化流程

bash
#!/usr/bin/env bash
set -euo pipefail
bash
#!/usr/bin/env bash
set -euo pipefail

Start the baguette server in background

在后台启动baguette服务器

baguette serve --port 8421 & SERVER_PID=$! sleep 1
baguette serve --port 8421 & SERVER_PID=$! sleep 1

Get the first available simulator UDID

获取第一个可用模拟器的UDID

UDID=$(baguette list | grep "iPhone 17 Pro" | head -1 | awk '{print $1}') echo "Using simulator: $UDID"
UDID=$(baguette list | grep "iPhone 17 Pro" | head -1 | awk '{print $1}') echo "使用模拟器: $UDID"

Boot it

启动模拟器

baguette boot --udid "$UDID" sleep 3
baguette boot --udid "$UDID" sleep 3

Run a tap sequence

执行点击操作

baguette tap --udid "$UDID" --x 195 --y 422 --width 390 --height 844
baguette tap --udid "$UDID" --x 195 --y 422 --width 390 --height 844

Pipe a gesture script

管道输入手势脚本

cat <<EOF | baguette input --udid "$UDID" {"type":"tap","x":195,"y":200,"width":390,"height":844} {"type":"swipe","startX":195,"startY":600,"endX":195,"endY":200,"width":390,"height":844,"duration":0.3} {"type":"button","button":"home"} EOF
cat <<EOF | baguette input --udid "$UDID" {"type":"tap","x":195,"y":200,"width":390,"height":844} {"type":"swipe","startX":195,"startY":600,"endX":195,"endY":200,"width":390,"height":844,"duration":0.3} {"type":"button","button":"home"} EOF

Capture a screenshot with bezel

捕获带边框的截图

baguette chrome composite --udid "$UDID" > screenshot.png echo "Screenshot saved to screenshot.png"
baguette chrome composite --udid "$UDID" > screenshot.png echo "截图已保存至screenshot.png"

Shutdown

关闭模拟器

baguette shutdown --udid "$UDID" kill $SERVER_PID
undefined
baguette shutdown --udid "$UDID" kill $SERVER_PID
undefined

Configuration

配置

Environment Variables

环境变量

VariableDescription
BAGUETTE_WEB_DIR
Override the served web root (e.g. point to
Sources/Baguette/Resources/Web
for live UI iteration without rebuilding)
bash
undefined
变量描述
BAGUETTE_WEB_DIR
覆盖Web服务的根目录(例如,指向
Sources/Baguette/Resources/Web
以在不重新构建的情况下实时迭代UI)
bash
undefined

Live-iterate on web UI without rebuilding

在不重新构建的情况下实时迭代Web UI

export BAGUETTE_WEB_DIR="$(pwd)/Sources/Baguette/Resources/Web" baguette serve
undefined
export BAGUETTE_WEB_DIR="$(pwd)/Sources/Baguette/Resources/Web" baguette serve
undefined

Device Sets

设备集

bash
undefined
bash
undefined

Use a custom simulator device set

使用自定义模拟器设备集

baguette serve --device-set /path/to/my-device-set baguette list # uses default device set
undefined
baguette serve --device-set /path/to/my-device-set baguette list # 使用默认设备集
undefined

Common Patterns

常见使用模式

CI/CD: Boot, Test, Shutdown

CI/CD:启动、测试、关闭

bash
#!/usr/bin/env bash
UDID=$(xcrun simctl list devices available -j | \
  python3 -c "import sys,json; devs=[d for v in json.load(sys.stdin)['devices'].values() for d in v if 'iPhone 17' in d['name'] and d['isAvailable']]; print(devs[0]['udid'])")

baguette boot --udid "$UDID"
bash
#!/usr/bin/env bash
UDID=$(xcrun simctl list devices available -j | \
  python3 -c "import sys,json; devs=[d for v in json.load(sys.stdin)['devices'].values() for d in v if 'iPhone 17' in d['name'] and d['isAvailable']]; print(devs[0]['udid'])")

baguette boot --udid "$UDID"

Run your XCTest suite or UI tests here

在此处运行你的XCTest套件或UI测试

xcodebuild test -scheme MyApp -destination "id=$UDID"
baguette shutdown --udid "$UDID"
undefined
xcodebuild test -scheme MyApp -destination "id=$UDID"
baguette shutdown --udid "$UDID"
undefined

Streaming to a File

画面流录制到文件

bash
undefined
bash
undefined

Capture 10 seconds of MJPEG to file

捕获10秒的MJPEG画面到文件

baguette stream --udid <UDID> --fps 30 --format mjpeg
| head -c $((10 * 30 * 50000)) > recording.mjpeg
baguette stream --udid <UDID> --fps 30 --format mjpeg
| head -c $((10 * 30 * 50000)) > recording.mjpeg

Convert to MP4 with ffmpeg

使用ffmpeg转换为MP4格式

baguette stream --udid <UDID> --fps 30 --format mjpeg
| ffmpeg -f mjpeg -i - -t 10 -c:v libx264 output.mp4
undefined
baguette stream --udid <UDID> --fps 30 --format mjpeg
| ffmpeg -f mjpeg -i - -t 10 -c:v libx264 output.mp4
undefined

Multi-Finger Gesture Sequence (Real-Time)

多指手势序列(实时)

bash
undefined
bash
undefined

Send a real-time 2-finger swipe up (simulate pull-to-refresh for two fingers)

发送实时双指向上滑动(模拟双指下拉刷新)

cat <<'EOF' | baguette input --udid <UDID> {"type":"touch2-down","x1":160,"y1":600,"x2":230,"y2":600,"width":390,"height":844} {"type":"touch2-move","x1":160,"y1":500,"x2":230,"y2":500,"width":390,"height":844} {"type":"touch2-move","x1":160,"y1":400,"x2":230,"y2":400,"width":390,"height":844} {"type":"touch2-move","x1":160,"y1":300,"x2":230,"y2":300,"width":390,"height":844} {"type":"touch2-up","x1":160,"y1":300,"x2":230,"y2":300,"width":390,"height":844} EOF
undefined
cat <<'EOF' | baguette input --udid <UDID> {"type":"touch2-down","x1":160,"y1":600,"x2":230,"y2":600,"width":390,"height":844} {"type":"touch2-move","x1":160,"y1":500,"x2":230,"y2":500,"width":390,"height":844} {"type":"touch2-move","x1":160,"y1":400,"x2":230,"y2":400,"width":390,"height":844} {"type":"touch2-move","x1":160,"y1":300,"x2":230,"y2":300,"width":390,"height":844} {"type":"touch2-up","x1":160,"y1":300,"x2":230,"y2":300,"width":390,"height":844} EOF
undefined

Troubleshooting

故障排除

"Command not found: baguette"

"Command not found: baguette"

bash
undefined
bash
undefined

Ensure Homebrew's bin is in PATH

确保Homebrew的bin目录在PATH中

export PATH="/opt/homebrew/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"

Or check install location

或检查安装位置

brew --prefix tddworks/tap/baguette
undefined
brew --prefix tddworks/tap/baguette
undefined

Simulator Won't Boot

模拟器无法启动

bash
undefined
bash
undefined

Check Xcode 26 is selected (required for SimulatorKit frameworks)

确认已选择Xcode 26(SimulatorKit框架必需)

xcode-select -p
xcode-select -p

Should show Xcode 26 path, e.g. /Applications/Xcode-26.0.app/Contents/Developer

应显示Xcode 26路径,例如 /Applications/Xcode-26.0.app/Contents/Developer

sudo xcode-select -s /Applications/Xcode-26.0.app/Contents/Developer
sudo xcode-select -s /Applications/Xcode-26.0.app/Contents/Developer

Verify the UDID exists

验证UDID是否存在

baguette list
undefined
baguette list
undefined

Stream Connects but No Frames

流已连接但无画面

bash
undefined
bash
undefined

Make sure the simulator is booted (not just created)

确保模拟器已启动(而非仅创建)

baguette boot --udid <UDID> sleep 3 baguette stream --udid <UDID> --fps 30 --format mjpeg | xxd | head
undefined
baguette boot --udid <UDID> sleep 3 baguette stream --udid <UDID> --fps 30 --format mjpeg | xxd | head
undefined

Input Gestures Not Registering

输入手势无响应

  • Coordinates must be in device points, not pixels. For a 3x display at 390pt wide, pixel width is 1170 — always use point values.
  • Ensure
    --width
    and
    --height
    match the simulator's actual screen size in points (check with
    baguette chrome layout --udid <UDID>
    ).
  • Only
    home
    and
    lock
    buttons are functional on iOS 26 (
    press
    command).
  • 坐标必须为设备点,而非像素。对于3倍分辨率、宽390pt的设备,像素宽度为1170 — 务必使用点值。
  • 确保
    --width
    --height
    与模拟器实际屏幕点尺寸匹配(可通过
    baguette chrome layout --udid <UDID>
    查看)。
  • iOS 26上仅
    home
    lock
    按键可通过
    press
    命令操作。

Web UI Not Updating

Web UI不更新

bash
undefined
bash
undefined

Use BAGUETTE_WEB_DIR for live file iteration

使用BAGUETTE_WEB_DIR实时迭代文件

export BAGUETTE_WEB_DIR="$(pwd)/Sources/Baguette/Resources/Web" baguette serve
export BAGUETTE_WEB_DIR="$(pwd)/Sources/Baguette/Resources/Web" baguette serve

Edit .html/.js/.css files and hard-refresh the browser

编辑.html/.js/.css文件后强制刷新浏览器

undefined
undefined

Port Already in Use

端口已被占用

bash
undefined
bash
undefined

Check what's using port 8421

检查哪个进程占用了8421端口

lsof -i :8421
lsof -i :8421

Use a different port

使用其他端口

baguette serve --port 9000 open http://localhost:9000/simulators
undefined
baguette serve --port 9000 open http://localhost:9000/simulators
undefined

Build Failures (Source Build)

源码构建失败

bash
undefined
bash
undefined

Ensure arm64e target and Xcode 26 SDK

确保目标为arm64e且使用Xcode 26 SDK

xcrun --sdk macosx --show-sdk-path
xcrun --sdk macosx --show-sdk-path

Should show macOS 15+ SDK from Xcode 26

应显示Xcode 26提供的macOS 15+ SDK

swift --version
swift --version

Should show Swift 6.1+

应显示Swift 6.1+

make clean && make
undefined
make clean && make
undefined