- The window comes from CGWindowListCopyWindowInfo (osascript -l
JavaScript) for the processes started from the built app, also on
another Space. The main window is chosen once after launch and kept, so
a sheet in front of it is never taken for it; capturing the main window
includes its sheet.
- It is captured with `screencapture -x -o -l <id>`, without the shadow.
- Positioning through System Events is optional: without the
Accessibility permission swiftrepo says so and the window keeps its own
size instead of `screenshots.macosWindow`.
- macOS refuses to capture a window on another Space while it shows a
sheet ("could not create image from window"). swiftrepo warns when the
window opened on another Space, captures the other scenarios and then
fails with the names of the missing ones.
- A failed window lookup times out with the last error.
@git.zone/swiftrepo
The git.zone Swift repository standard. swiftrepo finds the Swift project you are in (Package.swift, .xcodeproj or .xcworkspace) and runs the right tool with consistent rules for schemes, platforms, simulators, remote macOS builders, app launch, screenshots and watch loops.
swiftrepo takes over @git.zone/tsswift, which is deprecated. tsswift's history lives on in this repository: the archive/tsswift branch and the tsswift-v* tags. See Migrating from tsswift.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Installation
pnpm add -D @git.zone/swiftrepo
swiftrepo runs on Node.js 22 or later (or Bun). swift is required for SwiftPM projects. Xcode commands (simulator selection, launch, screenshots, review) need macOS with Xcode, either locally or through a configured remote builder.
Quick start
pnpm exec swiftrepo doctor
pnpm exec swiftrepo build
pnpm exec swiftrepo test
pnpm exec swiftrepo run
doctor prints the detected project, the resolved defaults, the selected platforms, the schemes and the destination swiftrepo will use.
Commands
| Command | What it does |
|---|---|
doctor |
Inspect the detected project, config, schemes, platforms and chosen defaults. |
config -g |
Manage the named remote builders in the global registry. |
build |
Build the detected SwiftPM or Xcode project. |
test |
Run the tests of the detected SwiftPM or Xcode project. |
run |
Build and launch one concrete target. |
emulators |
List the available iPhone, iPad, Apple TV and Apple Watch simulators and mark the preferred ones. |
prefer-emulator |
Save a preferred iPhone, iPad, Apple TV or Apple Watch simulator into .smartconfig.json. |
launch |
Build and launch an Xcode app with the file-based control channel wired in. |
command |
Send a route or JSON payload to the control file used by launch, screenshots or watch. |
screenshots |
Launch the app and capture one platform's screenshot set from a scenario file. |
review |
Capture screenshot sets across the configured review platforms. |
watch |
Rebuild and relaunch the configured app platforms whenever source files change. |
sim <subcommand> |
Drive one simulator: status, prepare, appearance, shot, fold, unfold, hinge, orient. |
appstore check [dir] |
Check App Store screenshot sets before an upload. |
Run swiftrepo --help for the full option list. --print prints the resolved commands instead of running them.
swiftrepo build --configuration Release
swiftrepo build --scheme MyApp --platform ios
swiftrepo build --platform watchos --scheme MyAppWatch
swiftrepo test --platform ipad --parallel
swiftrepo run --platform macos
swiftrepo prefer-emulator --platform ios --name "iPhone 17 Pro" --runtime latest
swiftrepo launch --platform ios --route myapp://mailbox/inbox
swiftrepo command --json '{"kind":"compose"}'
swiftrepo screenshots --platform ipad
swiftrepo watch --platform ios --debounceMs 1200
Common options: --path (explicit Package.swift, .xcodeproj or .xcworkspace), --scheme, --platform <auto|macos|ios|ipad|tvos|watchos>, --destination, --configuration, --derivedDataPath, --controlFile, --scenariosFile, --outputDir, --route, --json, --debounceMs, --parallel, --print.
Project detection
swiftrepo walks up from the current directory until it finds a Swift project, preferring .xcworkspace over .xcodeproj over Package.swift. A directory with several workspaces or projects is refused; pass --path. A project with several shared schemes needs --scheme or defaultScheme.
Configuration
Project configuration lives under @git.zone/swiftrepo in .smartconfig.json. A project that still has the legacy @git.zone/tsswift key is read from that key; a file with both keys is refused, never merged.
The config root is the nearest .smartconfig.json, else the nearest package.json, else the Swift project root. Relative paths in the config (derivedDataPath, control.filePath, screenshots.scenariosFile, screenshots.outputDir) are resolved from it.
{
"@git.zone/swiftrepo": {
"defaultScheme": "MyApp",
"defaultConfiguration": "Debug",
"derivedDataPath": ".build/xcode-derived-data",
"platforms": ["macos", "ios"],
"buildPlatforms": ["macos", "ios"],
"testPlatforms": ["macos", "ios", "ipad"],
"watchPlatforms": ["macos", "ios"],
"watchDebounceMs": 1200,
"parallelBuilds": true,
"parallelTests": true,
"defaultPlatform": "macos",
"remoteBuilder": { "name": "central-macos" },
"app": { "bundleId": "com.example.myapp" },
"control": {
"filePath": "/tmp/myapp-control.txt",
"fileEnvKey": "MYAPP_CONTROL_FILE",
"pollMs": 250,
"pollMsEnvKey": "MYAPP_CONTROL_POLL_MS",
"initialCommand": "myapp://mailbox/inbox"
},
"screenshots": {
"scenariosFile": "Automation/ui-screenshot-routes.txt",
"outputDir": ".nogit/ui-review",
"reviewPlatforms": ["ios", "ipad", "macos"],
"launchDelayMs": 2000,
"stepDelayMs": 1200,
"initialCommand": "myapp://mailbox/inbox",
"macosWindow": { "width": 1440, "height": 900, "x": 80, "y": 80 }
},
"targets": {
"macos": { "destination": "platform=macOS,arch=arm64" },
"ios": { "simulatorName": "iPhone 17 Pro", "runtime": "latest" },
"ipad": { "simulatorName": "iPad Pro 13-inch (M5)", "simulatorUDID": "IPAD-SIMULATOR-UDID", "runtime": "26.1" },
"tvos": { "simulatorName": "Apple TV 4K (3rd generation)", "runtime": "latest" },
"watchos": { "simulatorName": "Apple Watch Series 12 (46mm)", "scheme": "MyAppWatch", "bundleId": "com.example.myapp.watchkitapp" }
}
}
}
platforms,buildPlatforms,testPlatforms,watchPlatforms: the platform matrices of build, test and watch (platformsis the fallback).defaultPlatform: the one platformrunuses, and the fallback elsewhere.parallelBuilds,parallelTests: run multi-platform Xcode builds or tests in parallel.defaultScheme,defaultConfiguration,derivedDataPath: Xcode defaults.remoteBuilder:namerefers to the global registry;host,rootDir,port,sshKeyPath,platforms,rsyncExcludes,rsyncDeleteset or override it per project. Anamethe registry does not know (and nohostandrootDir) builds locally with a notice on a Mac, and is an error elsewhere, where nothing can build locally; a builder the project'splatformsexcludes for the requested platform is not needed either way.app.bundleId: the bundle identifier for launch and install.control: the control file, the environment variables that tell the app where it is and how often to poll, and the payload written beforelaunch.screenshots: the scenario file, output directory, review platforms, delays, initial payload and macOS window frame.targets.<platform>: a fullxcodebuild -destination, or the preferred simulator by name, UDID and runtime (latest,27,27.1,tvOS 27.0).schemeis the platform's own scheme (--schemewins, then this, thendefaultScheme), andbundleIdthe bundle identifierlaunchandscreenshotsstart on that platform instead ofapp.bundleId.
Defaults: control file /tmp/swiftrepo-control.txt, environment variables SWIFTREPO_CONTROL_FILE and SWIFTREPO_CONTROL_POLL_MS, scenario file ui-screenshot-routes.txt, screenshot output .nogit/swiftrepo-shots, watch debounce 800 ms, launch delay 2000 ms, step delay 1200 ms.
Apple TV and Apple Watch
--platform tvos and --platform watchos build, test, run, launch and screenshot on Apple TV and Apple Watch simulators, as ios and ipad do on iPhones and iPads (appletv and watch work too). swiftrepo picks the simulator by its device type, so a renamed one ("CL Apple TV 4K") counts: the newest runtime first, then the usual models (Apple TV 4K (3rd generation), Apple Watch Series 12 (46mm)), unless targets.tvos or targets.watchos pins one. emulators lists them, and prefer-emulator --platform tvos|watchos saves one.
A watch app usually has a scheme of its own. Set it as targets.watchos.scheme, so build and test over a platform matrix that includes watchos use it while the other platforms keep defaultScheme, and set targets.watchos.bundleId when app.bundleId names the iPhone app (swiftrepo warns when the bundle identifier it launches differs from the one the scheme builds). Apple TV and Apple Watch are never picked by --platform auto; name them.
{
"@git.zone/swiftrepo": {
"defaultScheme": "MyApp",
"buildPlatforms": ["macos", "ios", "tvos", "watchos"],
"app": { "bundleId": "com.example.myapp" },
"targets": {
"tvos": { "simulatorName": "My Apple TV", "runtime": "27.0" },
"watchos": { "scheme": "MyAppWatch", "bundleId": "com.example.myapp.watchkitapp" }
}
}
}
swiftrepo build --platform tvos
swiftrepo build --platform watchos --scheme MyAppWatch
swiftrepo emulators --platform watchos
swiftrepo prefer-emulator --platform tvos --name "My Apple TV"
swiftrepo launch --platform tvos --route myapp://home
swiftrepo screenshots --platform watchos
Simulator builds for tvOS and watchOS are pinned to the Mac's architecture like iOS ones (ARCHS, EXCLUDED_ARCHS for appletvsimulator or watchsimulator).
Remote builders
Named remote macOS builders live in ~/.git.zone/swiftrepo/config.json (SWIFTREPO_GLOBAL_CONFIG_PATH overrides the path). Commands run over SSH there after an rsync of the project, and screenshots are synced back.
swiftrepo config -g add-builder --name mac-builder --host builder.local --root-dir /Users/builder/project --platforms macos,ios,ipad
swiftrepo config -g list-builders
swiftrepo config -g show
swiftrepo config -g remove-builder --name mac-builder
Automation and screenshots
The automation channel is a file. launch starts the app with environment variables that point at the control file; command writes payloads into it; the app reads the file and reacts. screenshots and review launch the app, send each scenario payload, wait and capture; they work through remote builders too.
Scenario files have one name|payload line per screenshot; blank lines and # comments are ignored:
inbox|myapp://mailbox/inbox
thread-42|myapp://thread/42
compose|{"kind":"compose","draft":true}
On macOS, screenshots finds the app's main window through the window server (CGWindowListCopyWindowInfo, for the processes started from the built app) and captures it by window id with screencapture -x -o -l <id>: the window frame without its shadow (its rounded corners stay transparent), with any sheet the window shows. This also works when the window opened on another Space, beside a full-screen terminal, with one exception: there macOS cannot capture a window while it shows a sheet ("could not create image from window"). swiftrepo warns when the window opened on another Space, captures the scenarios it can and fails at the end with the names of the missing ones; run it with the app's Space in front. Moving and sizing the window to screenshots.macosWindow goes through System Events and needs the Accessibility permission for the terminal; without it, swiftrepo says so and captures the window at the size the app gave it. screencapture itself needs the Screen Recording permission. Controlled launch, screenshots and review need an Xcode app project; on a pure SwiftPM project launch falls back to run.
Simulators
swiftrepo sim drives one simulator on the Mac it runs on. --device takes a UDID or an exact simulator name (a name used on several runtimes is refused); without it, swiftrepo takes the one booted simulator. These commands never go through remote builders.
| Command | What it does |
|---|---|
sim status |
The device, its displays (size, rotation, which is active) and the interface orientation. |
sim prepare [--region en_US] [--time 9:41] [--appearance light|dark] [--status-bar=false] |
Boots the simulator, sets the region (AppleLocale, which also picks the 12- or 24-hour clock; reboots only when it changes), then overrides the status bar for App Store screenshots: the time, a full battery that is not charging, full signal and Wi-Fi. watchOS has no status bar override. |
sim appearance light|dark |
Switches the appearance. |
sim shot <file> [--display active|outer|inner|<id>] |
Saves a PNG of one display, by default the active one. The display's rounded corners are rendered black (--mask=black), so the file has no alpha channel and App Store Connect accepts it as it is. |
sim fold, sim unfold |
Foldable simulators (iPhone Duo): closes or opens the hinge and waits until the outer or inner display is the active one. |
sim hinge <0-180> |
Foldable simulators: sets any hinge angle; Device Hub's half-open preset is 130. |
sim orient <portrait|portrait-upside-down|landscape-left|landscape-right|face-up|face-down> |
Foldable simulators: turns the device. This is the physical device orientation, as Device Hub's rotate buttons set it, not the screen's. |
sim type <text> |
Types on the simulator's hardware keyboard (US layout; letters, digits, punctuation, space, return, tab). |
sim key <chord>... |
Presses keys: cmd+n, escape, shift+tab, return, ctrl+alt+left. Modifiers are cmd, ctrl, alt, shift. |
sim tap <x> <y> |
Touches the screen at fractions of its width and height (0 to 1 from the top left, as sim shot shows it in portrait). |
sim press <x> <y> [--ms 800] |
Long press. |
sim drag <x1> <y1> <x2> <y2> [--ms 500] |
Drags in a straight line: scrolling, moving a window by its grip, pulling a divider. |
swiftrepo sim prepare --device "SL Store iPhone Duo" --region en_US --appearance light
swiftrepo sim unfold
swiftrepo sim orient landscape-left
swiftrepo sim shot AppStore/screenshots/iphone-duo-inner/1-lists.png --display inner
swiftrepo sim fold
swiftrepo sim shot AppStore/screenshots/iphone-duo-outer/1-lists.png --display outer
On the iPhone Duo the inner display sits turned against the device: held in portrait, the unfolded inner display shows a landscape interface, and landscape-left gives a portrait one (landscape-right: portrait upside down, portrait-upside-down: the other landscape). The outer display follows the device as usual.
Keys and touches go to the keyboard and digitizer services of the simulator's HID daemon, as Device Hub sends them; xcrun simctl has no input commands. Each command opens its own virtual keyboard or touchscreen and removes it when done, so nothing stays pressed. While a command types, iOS sees a hardware keyboard and hides the on-screen keyboard. Touches were checked on iOS 27.0 iPhone and iPad simulators in portrait (taps, scrolling, long press, the iPadOS window menu and tiling, resizing windows). The iOS 27.1 iPhone Duo simulator takes keys but ignores touches on both displays with Xcode 27.1, so tap, press and drag refuse simulators with several displays. type sends the keys of a US layout, the simulator's default hardware keyboard; with another hardware keyboard layout some characters come out differently (on German, y and z swap).
There is no simctl or devicectl command for the hinge or the device orientation of a foldable simulator (devicectl device orientation set reports success and changes nothing). swiftrepo sends the same pose events as Xcode's Device Hub through CoreSimulator's private API, with a small Objective-C helper (assets/native/simhid.m) that it compiles with the active Xcode on first use into ~/.git.zone/swiftrepo/native/. Being private API, it may need an update with a new Xcode. Other simulators receive these events too but ignore them (checked on an iPhone 17 Pro and an iPad Pro 13-inch), so hinge and orient refuse simulators with one display; rotate those in Simulator or Device Hub. If the simulator does not confirm an event within 10 seconds, the command fails instead of waiting.
App Store screenshots
swiftrepo appstore check [dir] checks a screenshot directory before you upload it to App Store Connect. It runs anywhere, Linux included.
Put each set in a folder named after its class:
| Folder | Class | Sizes (portrait; landscape too for iPhone and iPad) |
|---|---|---|
iphone-6.9 |
iPhone with Dynamic Island, large | 1320×2868, 1290×2796, 1260×2736 |
iphone-6.5 |
iPhone with Face ID, large | 1284×2778, 1242×2688 |
iphone-6.3 |
iPhone with Dynamic Island, medium (required for iPhone apps) | 1206×2622, 1179×2556 |
iphone-6.1 |
iPhone with Face ID, medium | 1170×2532, 1125×2436, 1080×2340 |
iphone-5.5, iphone-4.7 |
iPhone with Home Button | 1242×2208, 750×1334 |
iphone-duo-outer, iphone-duo-inner |
iPhone Duo (required from April 2027 with the iOS 27.1 SDK) | 1398×2034, 2007×2853 |
ipad-13 |
iPad 13-inch (required for iPad apps) | 2064×2752, 2048×2732 |
ipad-12.9, ipad-11, ipad-10.5 |
older iPads | 2048×2732; 1488×2266, 1668×2420, 1668×2388, 1640×2360; 1668×2224 |
mac |
Mac | 2880×1800, 2560×1600, 1440×900, 1280×800 |
watch-ultra, watch-46mm, watch-45mm, watch-44mm, watch-42mm |
Apple Watch | 422×514 or 410×502, 416×496, 396×484, 368×448, 312×390 |
apple-tv, vision-pro |
Apple TV, Apple Vision Pro | 3840×2160 or 1920×1080, 3840×2160 |
Each set needs 1 to 10 PNG or JPEG files of an accepted size, and no file may have an alpha channel or transparency. A folder with images and an unknown name is an error; folders without images (such as tools/) are skipped. When there is any iPhone set, iphone-6.3 must exist; when there is any iPad set, ipad-13 must. The command exits 1 when anything is wrong. Sizes are from Apple's screenshot specifications, checked 2026-10-21.
Migrating from tsswift
- Replace the devDependency
@git.zone/tsswiftwith@git.zone/swiftrepoandtsswiftwithswiftrepoin your scripts. Commands and flags are those of tsswift 0.7.0; coming from 0.6.x you also get 0.7.0's stricter option parser (unknown commands and options fail, boolean options take onlytrue/false). swiftrepo needs Node.js 22 or later (or Bun). - Optionally rename the
.smartconfig.jsonsection@git.zone/tsswiftto@git.zone/swiftrepo. Until you do, swiftrepo reads the old section andprefer-emulatorwrites into it. Never keep both. - The first command that reads the builder registry copies
~/.git.zone/tsswift/config.jsonto~/.git.zone/swiftrepo/config.jsononce, if swiftrepo has none yet; the tsswift file stays untouched. - The defaults carry swiftrepo names:
SWIFTREPO_CONTROL_FILE,SWIFTREPO_CONTROL_POLL_MS,/tmp/swiftrepo-control.txtand.nogit/swiftrepo-shots. An app that relied on tsswift's default variable names setscontrol.fileEnvKeytoTSSWIFT_CONTROL_FILEandcontrol.pollMsEnvKeytoTSSWIFT_CONTROL_POLL_MS.TSSWIFT_GLOBAL_CONFIG_PATHis nowSWIFTREPO_GLOBAL_CONFIG_PATH. - A
remoteBuilder.namethat is not in the registry is an error on Linux, where tsswift silently tried a local build that could not work; on a Mac swiftrepo still builds locally and says so. - Simulators are sorted into iPhone, iPad and the other families by their device type, not by a name starting with "iPhone" or "iPad", so a renamed simulator ("SL Store iPhone Duo", "My 17 Pro") shows up in
emulatorsand can be picked. screenshots.macosLogFileis gone (it was never used).- The package exports only
runSwiftrepoCli,readSwiftrepoConfig,selectSwiftrepoConfig, the config keys and the types.
tsswift gets no forward to swiftrepo.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.