diff --git a/.claude/skills/switching-xcode-scheme/SKILL.md b/.claude/skills/switching-xcode-scheme/SKILL.md new file mode 100644 index 000000000000..ddde18921438 --- /dev/null +++ b/.claude/skills/switching-xcode-scheme/SKILL.md @@ -0,0 +1,60 @@ +--- +name: switching-xcode-scheme +description: Use when you need to change the active scheme of the running Xcode app (typically before invoking the Xcode MCP BuildProject/RunSomeTests/RunAllTests tools, which always operate on Xcode's currently-selected scheme), or when you need to discover what schemes or run destinations a workspace defines. Listing-only for run destinations — Xcode 26.x's AppleScript cannot persistently change the active destination, so destination switching must be done manually in the Xcode toolbar. +--- + +# Switching Xcode Scheme + +## Overview + +The Xcode MCP tools (`BuildProject`, `RunSomeTests`, `RunAllTests`) always act on the active scheme and active destination of the focused Xcode workspace. They take no scheme or destination arguments. To build a different target or run on a different simulator, you must change Xcode's selection first. + +This skill wraps an AppleScript helper that: +- **Sets the active scheme** of the workspace document Xcode currently has focused (works reliably; verified by read-after-write). +- **Lists schemes and run destinations** for that workspace, with optional substring filtering. +- **Cannot change the active run destination** — see the limitation section below. + +## When to use + +- Before calling Xcode MCP `BuildProject` / `RunSomeTests` / `RunAllTests` tools and the active scheme isn't the target you want. +- The user asks to "build/test/run scheme X" and you don't know whether X is currently selected. +- The user asks "what schemes are in this project?" or "what simulators can I run on?" and you want a definitive list straight from Xcode (rather than parsing project files). + +## When NOT to use + +- Xcode is not running, or no workspace/project is open. The script will fail with a clear message — don't use it as a way to launch Xcode. +- You need to switch the active run destination (simulator/device). AppleScript silently no-ops on Xcode 26.x. Ask the user to pick the destination in Xcode's toolbar instead. +- You're working with `xcodebuild` (CLI) rather than driving the Xcode app. `xcodebuild` takes `-scheme` and `-destination` flags directly — this skill is for the IDE, not the CLI. + +## Quick reference + +The helper lives at `xcode-state.sh` in this skill's directory. Run it directly via its absolute path. + +| Need to do | Command | +|---|---| +| See which workspace is focused | `xcode-state.sh workspace` | +| Read the current scheme | `xcode-state.sh scheme` | +| Switch the active scheme | `xcode-state.sh scheme "WordPress"` | +| List all schemes | `xcode-state.sh schemes` | +| Find schemes matching a pattern | `xcode-state.sh schemes Jetpack` | +| List all run destinations | `xcode-state.sh destinations` | +| Find destinations matching a pattern | `xcode-state.sh destinations "iPhone 17 Pro"` | + +Run with no arguments or `help` to print full usage. Exit code 1 means Xcode is not running / no workspace open / item not found; exit code 2 means usage error. + +The scheme `` for `scheme ` must match exactly (use the listing command first if unsure). Destination names already include the OS version in parens, e.g. `iPhone 17 Pro (26.4.1)`. + +## The run destination limitation + +Xcode's AppleScript dictionary defines `active run destination` as a settable property of the workspace document. In Xcode 26.x this property is broken in both directions: +- **Reads** always return `missing value`, even immediately after a successful set and even when Xcode visibly has a destination selected. +- **Writes** don't error, but the underlying `xcuserstate` doesn't update and there's no observable change in subsequent Xcode behavior. + +UI scripting via System Events can drive Xcode's toolbar dropdown, but it requires the invoker to have Accessibility permission, which Claude Code typically doesn't have. So this skill doesn't attempt destination writes. If destination switching is essential, ask the user to change it in the Xcode toolbar before running an MCP build. + +## Common mistakes + +- **Forgetting that scheme set is a precondition for the Xcode MCP tools.** If you call `BuildProject` without first switching scheme, it builds whatever was selected — possibly not what the user asked for. Read the current scheme first if you're unsure. +- **Passing a substring to `scheme `.** That command requires an exact match. For substring search, use `schemes ` to find the full name first. +- **Treating "set destination" as a no-op safely.** The AppleScript `set` will not error, but the destination will not change. Don't claim success based on the absence of an error. +- **Operating on the wrong workspace.** The helper always targets `active workspace document`. If the user has multiple Xcode windows open, confirm with `xcode-state.sh workspace` before switching schemes. diff --git a/.claude/skills/switching-xcode-scheme/xcode-state.sh b/.claude/skills/switching-xcode-scheme/xcode-state.sh new file mode 100755 index 000000000000..d93f535de737 --- /dev/null +++ b/.claude/skills/switching-xcode-scheme/xcode-state.sh @@ -0,0 +1,192 @@ +#!/bin/bash +# Read or change the active scheme of the workspace currently focused in +# the running Xcode application, and list the run destinations available +# to that workspace. +# +# Operates on Xcode's `active workspace document`. Xcode must already be +# running with at least one workspace/project open — this script does not +# launch Xcode and does not open files. +# +# Subcommands: +# workspace Print the path of the active workspace document. +# schemes [substring] List schemes (newline-delimited), optionally +# filtered by case-insensitive substring. +# scheme Print the active scheme's name. +# scheme Set the active scheme. must match exactly. +# destinations [substring] +# List run destinations (newline-delimited), +# optionally filtered by case-insensitive +# substring. Destination names already include +# the OS version in parens, e.g. +# "iPhone 17 Pro (26.4.1)". +# +# This script intentionally does NOT support reading or setting the +# active run destination. Xcode's AppleScript dictionary advertises +# `active run destination` as a property of the workspace document, but +# in Xcode 26.x reads always return `missing value` and writes appear to +# silently no-op. Until Apple fixes this, change the destination by hand +# in Xcode's toolbar, or use UI scripting via System Events (which +# requires Accessibility permission). +# +# Exit codes: +# 0 success +# 1 Xcode not running, or no workspace open, or item not found +# 2 usage error + +set -euo pipefail + +usage() { + awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0" + exit 2 +} + +# Run an AppleScript snippet against Xcode after first verifying that +# Xcode is running and that it has a workspace document open. The wrapper +# converts those preconditions into clean, single-line error messages +# instead of the noisy AppleScript stack traces you'd otherwise get. +run_osa() { + local script="$1" + osascript <&2 + exit 1 + ;; + ERR:no_workspace) + echo "error: Xcode has no workspace or project open" >&2 + exit 1 + ;; + ERR:scheme_not_found) + echo "error: no scheme matching the requested name" >&2 + echo "hint: run 'xcode-state.sh schemes' to list available schemes" >&2 + exit 1 + ;; + ERR:no_active_scheme) + echo "error: workspace has no active scheme" >&2 + exit 1 + ;; + esac +} + +cmd_workspace() { + local result + result=$(run_osa 'return (path of ws as string)') + handle_error "$result" + echo "$result" +} + +# Newline-delimited list. AppleScript's text item delimiters are the +# standard way to join a list into a string with a custom separator. +list_named() { + local elementName="$1" + local filter="${2:-}" + local result + result=$(run_osa " + set savedTID to AppleScript's text item delimiters + set AppleScript's text item delimiters to linefeed + set out to (name of every $elementName of ws) as string + set AppleScript's text item delimiters to savedTID + return out + ") + handle_error "$result" + if [[ -n "$filter" ]]; then + echo "$result" | grep -i -- "$filter" || { + echo "error: nothing matching '$filter'" >&2 + exit 1 + } + else + echo "$result" + fi +} + +cmd_schemes() { + list_named "scheme" "${1:-}" +} + +cmd_destinations() { + list_named "run destination" "${1:-}" +} + +cmd_get_scheme() { + local result + result=$(run_osa ' + try + return name of active scheme of ws + on error + return "ERR:no_active_scheme" + end try + ') + handle_error "$result" + echo "$result" +} + +cmd_set_scheme() { + local name="$1" + local escaped + escaped=$(printf '%s' "$name" | sed 's/"/\\"/g') + local result + result=$(run_osa " + try + set s to first scheme of ws whose name is \"$escaped\" + on error + return \"ERR:scheme_not_found\" + end try + set active scheme of ws to s + return name of active scheme of ws + ") + handle_error "$result" + echo "$result" +} + +if [[ $# -eq 0 ]]; then + usage +fi + +cmd="$1" +shift || true + +case "$cmd" in + workspace) + [[ $# -eq 0 ]] || usage + cmd_workspace + ;; + schemes) + [[ $# -le 1 ]] || usage + cmd_schemes "${1:-}" + ;; + scheme) + if [[ $# -eq 0 ]]; then + cmd_get_scheme + elif [[ $# -eq 1 ]]; then + cmd_set_scheme "$1" + else + usage + fi + ;; + destinations) + [[ $# -le 1 ]] || usage + cmd_destinations "${1:-}" + ;; + -h|--help|help) + usage + ;; + *) + echo "error: unknown command '$cmd'" >&2 + usage + ;; +esac diff --git a/.swiftlint.yml b/.swiftlint.yml index 9d04b2df2789..3a07daa9eb53 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -86,6 +86,9 @@ control_statement: discarded_notification_center_observer: severity: error +opening_brace: + ignore_multiline_statement_conditions: true + operator_usage_whitespace: skip_aligned_constants: false diff --git a/AGENTS.md b/AGENTS.md index c3345dbe0519..887c32276666 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,33 +38,12 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p - **Accessibility**: Use proper accessibility labels and traits - **Localization**: follow best practices from @docs/localization.md -## Build & Test +## Xcode Schemes +- `WordPress` builds the WordPress iOS app and runs `WordPressUnitTests.xctestplan` — default for builds and the full unit test suite. +- `Jetpack` builds the Jetpack iOS app — switch to it for Jetpack-only work. +- Some test targets (e.g. `WordPressDataTests`) have their own scheme and aren't in `WordPressUnitTests.xctestplan`. If the `WordPress` scheme fails to build because of an unrelated target, fall back to the target's dedicated scheme. -**Always check for the Xcode MCP server first.** -If it is connected, use it to build and test — no exceptions. - -If the Xcode MCP fails (e.g. build errors from unrelated targets), fall back to the Fastlane `test` lane: - -```bash -bundle exec fastlane test -bundle exec fastlane test only_testing:TargetName/Class/method -``` - -If Fastlane also fails, fall back to `xcodebuild` directly: - -```bash -xcodebuild \ - -workspace WordPress.xcworkspace \ - -scheme "${SCHEME}" \ - -destination "platform=iOS Simulator,name=${DEVICE}" \ - test \ - | xcbeautify -``` - -Some test targets (e.g. `WordPressDataTests`) have their own scheme and are not part of the main `WordPress` scheme's test plan. -When the `WordPress` scheme build fails due to an unrelated target, try using the target's dedicated scheme instead. - -### Simulator Sign-In +## Simulator Sign-In To automatically sign in to the app on an iOS simulator, see @docs/simulator-sign-in.md.