Files
sanderling/docs/manual/getting-started.md
T
pj 90224dfd06 Physical-device iOS support (#64) (#66)
* feat(companion): add appState, eraseText, pressKey runner handlers

The Go runner transport already calls these methods; the in-device runner
implemented them only latently. They become load-bearing on the device
path, where the hybrid's legacy-companion fallback is absent. Backward
compatible: the simulator hybrid never calls them.

* feat(ios): resolve physical devices from devicectl

ResolveDevice parses xcrun devicectl list devices into Device{Name,
HardwareUDID, CoreDeviceID}: the hardware UDID feeds xcodebuild/iproxy
and the CoreDevice id feeds devicectl install. Matches by name or either
id; errors list candidates on none/ambiguous. Fixes the stale sidecar
comment on ResolveTarget.

* feat(ioscompanion): runner-only device driver mode

NewDevice reuses Driver with d.companion set to the runner dialed over an
iproxy usbmux tunnel, hybrid=false, runnerClient=nil. The existing accessor
seams then route launch/snapshot/text/gesture to the runner with no new
DeviceDriver methods. Device seams swap clear-state to a devicectl
reinstall, container reset to a warn-once no-op, and paste grant to a no-op.
realSpawnDeviceRunner builds and signs the runner at run time via the App
Store Connect API key (no Xcode UI), caching on a source hash.

* test(ioscompanion): cover device wiring, routing, and shell-out argv

Seam-driven NewDevice wiring + gesture/text routing (asserting no keyboard
HID), devicectl/build/test/iproxy argv builders, xctestrun test-target dict
name parsing, signing-credential env checks, and source-hash cache keying.

* feat(testrun): route physical-device iOS runs to the device driver

Execute resolves a non-simulator iOS target through ios.ResolveDevice into
its hardware UDID and CoreDevice id; buildDriver constructs NewDevice via a
seam instead of rejecting the device. Generalizes the --ios-device and
--ios-app-path help to cover the device path; signing stays env-read, never
a flag.

* feat(doctor): device prereqs replace java/sidecar for ios-device

iosDeviceChecks now verifies devicectl, iproxy on PATH, a connected+paired
device (via ios.ConnectedDevices), and App Store Connect signing creds (via
ioscompanion.VerifyDeviceSigning). The retired JVM sidecar checks stay only
under android.

* feat(conformance): device backend uses iphoneos app and tunnel orphan checks

The device backend now builds via just ios-device, points --ios-app-path at
the Debug-iphoneos bundle, and reinstalls each run for clear-state. The G5
orphan scan replaces the retired sidecar.jar check with lingering iproxy and
device test-without-building sessions (destination platform=iOS,id=).

* feat(folio): device build linking the iosArm64 framework

project.yml selects the Kotlin framework slice by SDK (iosArm64 for
iphoneos, iosSimulatorArm64 for simulator) and links via -framework Shared
on the SDK-conditional search path. New ios-device/test-ios-device recipes
mirror ios/test-ios, signing the Debug-iphoneos build with the .env API key.

* docs(cli): document ios-device doctor checks and the device flags

The --ios-device flag now also selects a connected device; --ios-app-path
covers the device install; the doctor gains an ios-device platform whose
checks are devicectl, iproxy, a paired device, and signing credentials.
Corrects the --clear-data default to true.

* fix(ioscompanion): resolve signing key path to absolute

xcodebuild's -authenticationKeyPath requires an absolute path, but .env
files commonly carry a repo-relative one. Resolve it against the working
directory before the stat so a relative ASC_API_KEY_PATH still signs.

* fix(ioscompanion): re-enable signing for the device runner build

companion/project.yml disables code signing for the simulator build, so
the device build inherited it and produced an unsigned runner that the
device rejected at install (0xe8008018). build-for-testing now forces
CODE_SIGNING_ALLOWED/REQUIRED=YES so automatic provisioning signs it.

* fix(ioscompanion): key the device build cache on signing identity

The cache marker hashed only sources, so switching signing team or key
reused a runner signed with the stale identity, which the device rejects at
install (0xe8008018). Fold team + key id into the cache key so a signing
change forces a rebuild.

* docs(getting-started): document physical iOS device setup

Lists the iproxy requirement and the App Store Connect signing env vars
(SANDERLING_IOS_TEAM, ASC_API_*) a device run needs, plus the
test-ios-device recipe and the doctor check.

* feat(ios): native usbmux client and in-process tunnel forwarder

Talk to macOS usbmuxd directly instead of shelling out to iproxy, so the
device path depends on nothing beyond macOS + Xcode.

* refactor(ios): drive device tunnel via io.Closer seam

Replace the tunnelChild *exec.Cmd and spawnTunnel seam with a tunnel
io.Closer and startTunnel seam backed by the in-process usbmux forwarder.

* refactor(ios): remove iproxy spawn from device runner

* test(ios): cover tunnel close via io.Closer not child process

* feat(doctor): check usbmuxd socket instead of iproxy on PATH

* chore(conformance): drop iproxy orphan check; tunnel is in-process

* docs(ios): device tunnel uses native usbmux, nothing to install

* chore: gitignore the signing keys directory

* feat(folio): add Android launcher icon (black bg, white dot)

* feat(folio): add iOS app icon (black bg, white dot)

* feat(folio): add web favicon (black bg, white dot)

* docs(ioscompanion): fix stale const comments

* refactor(ioscompanion): inline single-use devicectl argv builders

* refactor(ioscompanion): inline xcodegenArgs, drop tautological argv tests

* refactor(ioscompanion): inline firstNonEmpty

* refactor(doctor): dedup usbmuxd socket path via ioscompanion seam

* test(doctor): trim redundant signing-check test

* refactor(ioscompanion): deliver COMPANION_PORT via TEST_RUNNER_ env

* fix(testrun): seam preflight so iOS routing tests pass on CI without xcrun
2026-06-09 18:38:52 +05:30

3.0 KiB

title
title
Getting started

Getting started

Install the CLI, run a spec.

Prerequisites

Android / iOS:

  • An Android emulator with API level 30 or newer (or a connected device).
  • adb on your PATH.

Web:

  • Chrome installed. sanderling drives it via CDP; no other setup required.

Run sanderling doctor to check the host environment.

Install

CLI

curl -fsSL https://raw.githubusercontent.com/priyanshujain/sanderling/master/install.sh | bash

Spec package (npm)

npm install --save-dev @sanderling/spec

Your first run

The repo ships two sample apps. examples/folio is a Kotlin Multiplatform personal-ledger app that covers Android, iOS, and web (wasmJs) from one shared codebase. examples/folio-web is a smaller React + Vite app that covers only the web path. Both carry a TypeScript spec under sanderling/spec.ts. Install just, then pick a target below.

Android

From examples/folio:

just install   # build and install the folio APK on a booted emulator or device
just test      # run the spec

With no device connected and multiple AVDs, pick one:

AVD=Pixel_7 just test

Persistent settings can live in a .env alongside the justfile (AVD=Pixel_7, DURATION=5m, and so on).

iOS

From examples/folio (requires Xcode 16+ and xcodegen):

just test-ios                          # default simulator: iPhone 17 Pro
IOS_DEVICE="iPhone 15" just test-ios   # pick a different simulator

just test-ios boots the simulator if needed, builds and installs the app, then runs sanderling test --platform ios.

Physical device

A connected iPhone is driven over a usbmux tunnel by a runner the driver builds and signs at run time. The tunnel talks to macOS's own usbmuxd, so nothing extra is installed beyond Xcode. It needs App Store Connect signing credentials in the environment (a gitignored .env is loaded by just):

SANDERLING_IOS_TEAM=<10-char team id>
ASC_API_KEY_ID=<key id>
ASC_API_ISSUER_ID=<issuer id>
ASC_API_KEY_PATH=<absolute path to AuthKey_*.p8>

IOS_DEVICE="iPhone" just test-ios-device   # name, UDID, or CoreDevice id

Run sanderling doctor --platform ios-device to check devicectl, the usbmuxd socket, a connected and paired device, and the signing credentials before a run.

Web

From either example. For the KMP wasmJs build, use examples/folio:

just web       # serve the wasmJs app on a webpack dev server

For the React + Vite build, use examples/folio-web:

just test      # starts the Vite dev server, then sanderling drives Chrome via CDP

No emulator or SDK setup needed for either web path.

Trace output

When the run ends, the trace lands in sanderling/runs/<timestamp>/:

runs/2026-04-18T12-34-56/
├── trace.jsonl
├── screenshots/
└── meta.json

Browse it with sanderling replay (see replay), or read trace.jsonl step by step.

Next: writing specs.