philkunz 26ab113c20 feat(screenshots): capture macOS windows without the Accessibility permission
- 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.
2026-10-22 05:51:23 +02:00
2026-10-21 00:59:42 +02:00
2026-10-21 00:59:42 +02:00
2026-10-21 08:36:05 +00:00

@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 (platforms is the fallback).
  • defaultPlatform: the one platform run uses, and the fallback elsewhere.
  • parallelBuilds, parallelTests: run multi-platform Xcode builds or tests in parallel.
  • defaultScheme, defaultConfiguration, derivedDataPath: Xcode defaults.
  • remoteBuilder: name refers to the global registry; host, rootDir, port, sshKeyPath, platforms, rsyncExcludes, rsyncDelete set or override it per project. A name the registry does not know (and no host and rootDir) builds locally with a notice on a Mac, and is an error elsewhere, where nothing can build locally; a builder the project's platforms excludes 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 before launch.
  • screenshots: the scenario file, output directory, review platforms, delays, initial payload and macOS window frame.
  • targets.<platform>: a full xcodebuild -destination, or the preferred simulator by name, UDID and runtime (latest, 27, 27.1, tvOS 27.0). scheme is the platform's own scheme (--scheme wins, then this, then defaultScheme), and bundleId the bundle identifier launch and screenshots start on that platform instead of app.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

  1. Replace the devDependency @git.zone/tsswift with @git.zone/swiftrepo and tsswift with swiftrepo in 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 only true/false). swiftrepo needs Node.js 22 or later (or Bun).
  2. Optionally rename the .smartconfig.json section @git.zone/tsswift to @git.zone/swiftrepo. Until you do, swiftrepo reads the old section and prefer-emulator writes into it. Never keep both.
  3. The first command that reads the builder registry copies ~/.git.zone/tsswift/config.json to ~/.git.zone/swiftrepo/config.json once, if swiftrepo has none yet; the tsswift file stays untouched.
  4. The defaults carry swiftrepo names: SWIFTREPO_CONTROL_FILE, SWIFTREPO_CONTROL_POLL_MS, /tmp/swiftrepo-control.txt and .nogit/swiftrepo-shots. An app that relied on tsswift's default variable names sets control.fileEnvKey to TSSWIFT_CONTROL_FILE and control.pollMsEnvKey to TSSWIFT_CONTROL_POLL_MS. TSSWIFT_GLOBAL_CONFIG_PATH is now SWIFTREPO_GLOBAL_CONFIG_PATH.
  5. A remoteBuilder.name that 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.
  6. 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 emulators and can be picked.
  7. screenshots.macosLogFile is gone (it was never used).
  8. The package exports only runSwiftrepoCli, readSwiftrepoConfig, selectSwiftrepoConfig, the config keys and the types.

tsswift gets no forward to swiftrepo.

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.

S
Description
The git.zone Swift repository standard: SwiftPM and Xcode build, test and run flows, simulators, foldable device poses, App Store screenshots and the automation loop, taking over @git.zone/tsswift.
Readme
617 KiB
Languages
TypeScript 93.7%
Objective-C 6.2%
JavaScript 0.1%