--- name: build-ios-apps description: Build, sign, install, and publish iOS apps using the Vibecode signing service. Use when the user wants to build an iOS app from source, sign an iOS app, install on their iPhone, register a device, set up Apple Developer auth, publish to the App Store or TestFlight, submit for review, or work with the iOS build/sign/distribute/publish pipeline. Also triggers for "test on device", "put this on my phone", "ship it to my iPhone", "publish to App Store", "submit my app", "push to TestFlight", "send for review", or any request to get a Swift/iOS app running on a real device or shipped to App Store Connect. --- # iOS App Build, Sign, Install & Publish Build Swift apps, sign them with Apple credentials, install on iPhones via OTA, and publish to App Store Connect (production review or TestFlight). ## CRITICAL RULES 1. **NEVER use Xcode, xcodebuild, or xcrun.** You do not have Xcode access. ALL building is done through the cloud build pipeline via `./ios-cli`. 2. **NEVER make raw HTTP/curl calls to the signing service.** Always use `./ios-cli`. It handles authentication, polling, error handling, and output formatting. 3. **Every iOS build request MUST go through this skill.** When a user says "build an app", "test on my phone", "put this on my iPhone", "ship it", or anything about getting an iOS app running — use this skill exclusively. 4. **Before writing any app that uses a capability or extension, READ [references/capabilities.md](references/capabilities.md).** It has per-capability rules for entitlement files, Info.plist keys, runtime code, and manual portal steps. Skipping it causes silent install/runtime failures that waste build cycles. Also read [references/gotchas.md](references/gotchas.md) for compile fixes the template's build settings require. ## Environment These environment variables are automatically available inside Chorus runners — `ios-cli` picks them up: - `VIBECODE_API_KEY` — Authentication. Required. Get yours by signing in at https://chorus.com/ → Account settings → reveal and copy the key (starts with `vibecode_`). When unset (e.g. running locally), persist it once with `./ios-cli login ` and the CLI reads it from `~/.vibecode/ios/config.json` on subsequent invocations. - `SIGNING_SERVICE_URL` — Service URL (auto-detected, defaults to `https://ios.vibecodeapp.com`) - `VIBECODE_PROJECT_ID` — Project / agent identifier. Optional for `build` — when unset, the CLI mints a UUID once and persists it in the config file so subsequent local builds share the same project namespace. Required for `sim-preview`. Chorus runners inject this with the agent id automatically. ## Flow at a glance **Default (sim-first):** Build → print the `previewUrl` from build output. No separate sim-preview step, no Apple auth, no device registration, nothing else. The user previews instantly in their chat. **On-demand device install:** When the user clicks "Install on device" on the preview, chorus posts a visible user chat message in the form `Install this build on my device. (build: )`. That triggers the **Install-on-Device callback** below, which walks through any missing setup (auth → register → sign) and prints the install URL. This install message is the only signal that should make the agent run auth/register/sign. Don't run those steps preemptively after a build — defer until the user explicitly asks to install. ## CLI The `./ios-cli` binary is in this skill directory. Run `./ios-cli --help` for full usage, or `./ios-cli skill` to print this document. **If `ios-cli` is missing** (the skill bundle didn't ship the binary, or the environment was rebuilt) install a fresh one. The installer detects your platform, verifies the download against a sha256 manifest, and extracts into `./build-ios-apps/`. ``` # macOS / Linux curl -fsSL https://ios.vibecodeapp.com/install.sh | bash ./build-ios-apps/ios-cli --help # Windows (PowerShell) irm https://ios.vibecodeapp.com/install.ps1 | iex .\build-ios-apps\ios-cli.exe --help ``` ### Output Modes Controlled by `--output` global flag: - `--output text` (default) — logfmt `key="value"` pairs, one line per result. Designed for grep and cut. - `--output json` — Single JSON object per invocation. Designed for jq and programmatic parsing. - `--quiet` — Print only the primary identifier (ID or URL). For scripting and piping. Long-running commands print bracketed event markers to stderr: `[building]`, `[signing]`, `[done]`, `[error]` Errors: `ERROR: message` on stderr (text mode) or `{"error":"message","code":"ERROR_CODE"}` on stdout (JSON mode). Exit code 1. ### Commands | Command | Description | |---|---| | `./ios-cli login ` | Persist a vibecode API key locally so subsequent commands work without env vars (no-op inside Chorus, where the env var is already injected) | | `./ios-cli auth apikey [--user ] --issuer-id --key-id --p8-key [--team-id ]` | Auth with App Store Connect API key. `--user` is optional and defaults to the API-key owner. `--team-id`: Optional; derived from the key when Apple exposes it. | | `./ios-cli auth link [--user ]` | Mint a secure website login link (`/login`) so the user can connect Apple on `ios.vibecodeapp.com` instead of pasting credentials in chat. Works whether or not the user is already authenticated; the output's `expiresAt` says how long the link lasts. | | `./ios-cli auth apple-id [--user ]` | Mint a secure link where the user signs in with their Apple ID and enters Apple's two-factor code. Needed for `./ios-cli asc web ...`; also creates an App Store Connect API key when none is connected. | | `./ios-cli auth check [--user ]` | Check the connected Apple credentials: App Store Connect key (Key ID, Issuer ID, team) and whether an Apple ID is signed in. | | `./ios-cli auth disconnect (--api-key \| --apple-id \| --all)` | Remove saved Apple credentials. Only when the user asks. | | `./ios-cli asc [flags]` | Run any App Store Connect CLI (asc) command with the user's credentials: subscriptions, in-app purchases, pricing, metadata, reviews, analytics, app records, App Privacy. See [references/asc.md](references/asc.md). | | `./ios-cli build --no-wait` | Start the cloud build, durably register it, and return its `buildJobId` immediately. **Always use this for publish flows**, then end the turn. | | `./ios-cli build ` | Upload source zip, build on cloud macOS, wait until done. Blocks for 10-15 minutes — preview builds only, never a publish build. | | `./ios-cli sim-preview ` | Re-mint a preview for an existing buildJob. Rarely needed — `./ios-cli build` already emits a tokenized previewUrl in its output. | | `./ios-cli sign ` | Sign a built app, wait until done, returns install URL | | `./ios-cli sign --from-sim ` | Resolve a sim-preview's underlying buildJob server-side and sign for device install (Install-on-Device callback) | | `./ios-cli devices [userId]` | List registered devices | | `./ios-cli register-apple [userId]` | Sync pending devices with Apple | | `./ios-cli status build ` | Check a build once and return. Add `--wait` to block until `built`/`failed` — preview builds only, never a publish build. | | `./ios-cli status sign ` | Check signing status | | `./ios-cli logs ` | Fetch build logs — mid-build for status, after failure for errors | | `./ios-cli crashes --bundle [--build-id ] [--limit N] [--user ]` | List recent TestFlight crash feedback for an app. TestFlight only; production App Store crashes are not exposed. | | `./ios-cli publish legal-pages --bundle ` | Write and host the app's privacy policy and support pages (Vibecode sandboxes); publish start puts both URLs on the listing. The only way to get these URLs; never ask the user for a website, hosting, or a backend deployment. See [Legal pages](references/publishing-production.md#legal-pages). | | `./ios-cli screenshots setup` | Preflight the local Playwright + Koubou renderer (the runner image normally does this already) | | `./ios-cli screenshots capture --simulator [--file 01-home.png]` | Save exactly what the project's simulator shows now as a local PNG; open the screen first with `simulator-ensure` and the `ios-*` tools | | `./ios-cli screenshots generate ` | Add the headline and device frame to captured app screens, producing App Store PNGs | | `./ios-cli bootstrap ` | Create new SwiftUI project from template | | `./ios-cli add-spm-package [--product ] [--target ] [--project ] [--version-kind ]` | Add a remote Swift Package Manager dependency to an Xcode project. Writes all required pbxproj sections (remote ref, product dependency, build file, target + project arrays) and is idempotent on (URL, product). | | `./ios-cli config get` | Print current config | | `./ios-cli config set ` | Set a config value (supports dot notation) | | `./ios-cli config path` | Print config file path | | `./ios-cli skill` | Print this skill reference | | `./ios-cli diagnose ` | Inspect pipeline state for a user — auth, enrollment, certs, devices, capabilities, profile. Use this BEFORE guessing why sign or build fails. See [Diagnose subcommands](#diagnose-subcommands). | ### Diagnose subcommands When sign or build fails with an Apple-side error and you need to inspect state instead of trying random fixes, use `./ios-cli diagnose `. Every subcommand supports `--output json` for machine parsing. None of them mutate Apple-side state. | Subcommand | Use it when | Output highlights | |---|---|---| | `diagnose user [--user ]` | "Is this user authed at all? Which auth method? What signing assets are cached?" | `authMethod`, `teamId`, presence flags for cert / private key / profile, `devicesCount` | | `diagnose enrollment [--user ]` | "Will the pre-build gate let me through? Is the team paid?" | `status`: `paid` / `free_account` / `revoked` / `insufficient_scope` / `not_registered` / `transient`, with `portalUrl` + `nextActions` | | `diagnose certs [--user ]` | "Which certs does Apple see for this team? Which ids are stored by this service?" | List of `{id, type, displayName, serialNumber, expiresAt, storedByService}`. `storedByService` is not a safe-to-revoke signal. | | `diagnose device --udid [--user ]` | "Is this UDID registered with Apple for this team?" | `registered: bool`, `deviceClass`, `status`, `addedDate`. Case-insensitive UDID match. | | `diagnose capability --bundle-id --entitlement [--user ]` | "Is `com.apple.developer.X` enabled on this bundle id?" | `status`: `enabled` / `not_enabled` / `bundle_id_unregistered` / `manual_only` / `noop` / `unknown_entitlement`. For `manual_only` (Family Controls, CarPlay, etc.) returns `manualStep.portalUrl` + `steps`. | | `diagnose entitlements --bundle-id [--user ]` | "Where did the chain break — Apple side, profile side, or both?" | `apple.capabilityTypes[]` vs `profile.entitlements{}`, plus a `diff.{onBoth, onlyOnApple, onlyInProfile}` breakdown. | | `diagnose profile --bundle-id [--user ]` | "What's actually in the cached `.mobileprovision` for this bundle id?" | Decoded `name`, `uuid`, `teamIdentifier`, `appIdName`, `expirationDate`, `deviceCount`, `entitlements`. Returns `available: false` if no sign has run for this bundle id yet. | All diagnose endpoints require the caller to own the signing-service user (strict tenant isolation). Secrets (p8 bytes, private keys, cert bytes) are NEVER returned — only IDs, presence flags, and decoded metadata. Typical debug loop when sign fails with a managed-capability error: 1. `./ios-cli diagnose capability --bundle-id --entitlement ` — is Apple's bundle id flagged for this capability? 2. If `status: manual_only` → follow `manualStep.portalUrl` + `steps` and re-sign. 3. If `status: not_enabled` → the pipeline should auto-enable it on next sign; retry once before chasing other causes. 4. If `status: enabled` but sign still fails → run `./ios-cli diagnose profile --bundle-id ` to confirm the cached profile actually carries the entitlement (capability provisioned but profile stale → re-sign forces a fresh profile). **A capability works in source but fails on the user's iPhone.** For example: Sign in with Apple errors the moment it's tapped, push never registers, or HealthKit or iCloud is denied. Check Apple's side with `diagnose` before reading code or asking the user questions. The source being right does not mean the installed app got the entitlement. 1. `./ios-cli diagnose capability --bundle-id --entitlement ` for each key in the app's `.entitlements` file. Act on the `status`: - `noop`: nothing to switch on at Apple. Skip it. - `manual_only`: Apple can't switch it on through the API. Give the user the returned `manualStep` (`portalUrl` and `steps`) and wait for them to finish it. - `not_enabled`: switch it on in step 2. - Any result with a `manualStep`, even `enabled`, may still need portal work. iCloud, for example, also needs an `iCloud.` container created and linked at `portalUrl`. Read the steps and hand the user whatever is still left to do. 2. Enable each `not_enabled` capability with the passthrough. Find the resource id with `./ios-cli asc bundle-ids list --identifier `, then run `./ios-cli asc bundle-ids capabilities add --bundle --capability `. Use the `ascCapabilityType` from step 1 as ``. Sign in with Apple also needs `--settings '[{"key":"APPLE_ID_AUTH_APP_CONSENT","options":[{"key":"PRIMARY_APP_CONSENT","enabled":true}]}]'`. 3. Re-run step 1 for the keys you enabled and confirm each is now `enabled`. 4. The copy already on the phone keeps its old signing. Once your next change has built and any portal steps are done, tell the user to reinstall from Install on iPhone, and say plainly that the fix is unconfirmed until they try it on the device. `[userId]` is optional — when omitted the CLI uses the user resolved from the active API key. ### Crash triage When testers report crashes via TestFlight feedback, use `./ios-cli crashes` to pull the recent submissions without leaving the terminal: ```bash # All recent crashes for a bundle (default limit 10, max 200) ./ios-cli crashes --bundle com.example.app # Narrow to a specific build ./ios-cli crashes --bundle com.example.app --build-id # JSON output for scripting ./ios-cli --output json crashes --bundle com.example.app | jq '.topCrashes' ``` Text output prints a summary line, up to five top crash groups (deduplicated by build + device + crash comment), then one line per recent crash entry. JSON output shape: `{"scope":"testflight","count":N,"topCrashes":[...],"crashes":[...]}`. Scope is always `testflight` — production App Store crash reporting is not accessible through this endpoint. ### Error Codes If a command fails (exit code 1), check the error code: | Error Code | Meaning | What To Do | |---|---|---| | `MISSING_API_KEY` | No API key in env or config | Run `./ios-cli login ` or set `VIBECODE_API_KEY`. | | `MISSING_ENV` | Required env var not set | Ensure `VIBECODE_PROJECT_ID` is available. | | `MISSING_ARG` | Required command argument missing | Check command usage with `--help`. | | `MISSING_FLAG` | Required `--flag` not provided | Check command usage with `--help`. | | `UNKNOWN_COMMAND` | Unrecognized command or subcommand | Run `./ios-cli --help` to see available commands. | | `CONNECTION_FAILED` | Cannot reach the signing service | Check `SIGNING_SERVICE_URL`. Service may be down. Retry after a few seconds. | | `UNAUTHORIZED` | Invalid or expired API key (401) | Check `VIBECODE_API_KEY`. The key may have been revoked or rotated. | | `FORBIDDEN` | Access denied (403) | The API key doesn't have permission for this operation. | | `NOT_FOUND` | Resource not found (404) | The userId, buildJobId, or buildId doesn't exist. Verify the ID. | | `CLIENT_ERROR` | Other client error (4xx) | Check the error message for details. | | `SERVER_ERROR` | Server error (5xx) | The signing service had an internal error. Retry. If persistent, report the issue. | | `BUILD_FAILED` | Cloud build failed | Fetch Xcode errors with `./ios-cli logs `. Common causes: missing scheme, Swift compiler errors. | | `BUILD_NOT_READY` | Build hasn't finished yet | Report the `buildJobId` and end the turn. For a publish build, the backend resumes the agent; for a preview build, check once with `./ios-cli status build ` on the next turn. | | `SIGN_FAILED` | Code signing failed | Read the error message and follow the signing service's next step. | | `NO_APPLE_AUTH` | Server says user has no usable Apple credentials | Run the auth flow (Step 1 of First-Time Setup). | | `API_KEY_AUTH_REQUIRED` | The operation requires App Store Connect API-key credentials. | Run `./ios-cli auth link`, send the fresh URL, wait for the user to return, then retry the install. | | `NO_AUTH_REGISTERED` | No usable App Store Connect API key is connected. | Run `./ios-cli auth link`, send the fresh URL, wait for the user to return, then retry the install. | | `API_KEY_REVOKED` | App Store Connect rejected the saved key. | Run `./ios-cli auth link`, send the fresh URL, wait for the user to return, then retry the install. | | `API_KEY_INSUFFICIENT_SCOPE` | The saved key cannot perform the required App Store Connect operation. | Run `./ios-cli auth link`, send the fresh URL, wait for the user to return, then retry the install. | | `SIGN_IN_PROGRESS` | A sign for the same build is already in flight | Report that signing is already in progress and end the turn; retry on the next turn instead of starting another sign. | | `UNEXPECTED_ERROR` | Unknown/unhandled error | Check the error message. May be a bug — retry or report. | ### Output Examples ```bash # Default (logfmt text) ./ios-cli devices c906084e-... # → devices="0" registrationUrl="https://ios.vibecodeapp.com/register/c906084e-..." # JSON mode ./ios-cli --output json devices c906084e-... # → {"devices":[],"registrationUrl":"https://..."} # Quiet mode (just UDIDs) ./ios-cli --quiet devices c906084e-... # → (one UDID per line) # Build with progress events on stderr ./ios-cli build /tmp/source.zip # stderr: [build] uploading /tmp/source.zip... # stderr: [build] job abc123 started # stderr: [building] 30s elapsed, state=building # stderr: [done] build succeeded # stdout: buildJobId="abc123" state="built" appUrl="https://..." ``` ### Chaining Commands The full build → sign → install flow: ```bash # 1. Build (outputs buildJobId) ./ios-cli build /tmp/source.zip # → buildJobId="abc123" state="built" appUrl="https://..." # 2. Sign using the buildJobId from step 1 ./ios-cli sign abc123 # → buildId="def456" state="signed" installUrl="https://ios.vibecodeapp.com/install/def456" # 3. Give the user the installUrl to open on their iPhone ``` With quiet mode for scripting: ```bash # Build and capture just the job ID BUILD_JOB_ID=$(./ios-cli --quiet build /tmp/source.zip) # Sign and capture just the install URL INSTALL_URL=$(./ios-cli --quiet sign "$BUILD_JOB_ID") # Share with user echo "Install your app: $INSTALL_URL" ``` ## State All state lives at `~/.vibecode/ios/config.json`. Schema: [config-schema.json](references/config-schema.json). !`cat ~/.vibecode/ios/config.json 2>/dev/null || echo "No config found — run first-time setup."` ## Routing **Default path** (no auth required): Always follow **Normal Flow** below for any build request. Build, print URL. Done. **Install-on-Device callback**: When chat receives a message matching `Install this build on my device. (build: )`, follow the **Install-on-Device callback** section. That's the only path that invokes auth/register/sign — and it's only invoked by the user clicking "Install on device" on the preview UI. Do NOT preemptively run the **First-Time Setup** flow after a build. The user might never want to install on device — only previewing in the simulator. First-Time Setup is a sub-routine called from the Install-on-Device callback when prereqs are missing. --- ## First-Time Setup Walk the user through each step. Update `~/.vibecode/ios/config.json` after each one. **Only run these steps when the Install-on-Device callback says a prerequisite is missing.** ### Step 0: Pre-requisites The user **must** be enrolled in the Apple Developer Program ($99/year). If they are not, they cannot use this skill. Begin by asking them if they are enrolled. If not, guide them on how to enroll. ### Step 1: Authenticate with Apple Run `./ios-cli auth link [--user ]` and send the generated `https://ios.vibecodeapp.com/login?...` URL with this one sentence: > "On this page you sign in with your Apple ID (Apple will send a code to your devices), or connect an App Store Connect API key." Wait for the user to come back after completing the page, then retry the install. The command output's `expiresAt` says how long the link lasts; if it expires or the setup fails, mint and send a fresh link. **Reconnecting or switching account.** When the user asks for a login link, wants to use a different App Store Connect key or team, or an Apple error says the key was rejected or lacks access, mint a fresh link with `./ios-cli auth link` right away — do not ask them to confirm, and do not tell them they are already connected. Connecting a key for a different team re-creates certificates and profiles on the next sign. Apps already published to the App Store stay with the account that published them, so mention that if they are switching accounts on a published app. Example: > "Here's a fresh login link to connect your App Store Connect API key: " ### Step 2: Register Device Check if any devices exist: ```bash ./ios-cli --output json devices ``` If no devices (empty `devices` array in JSON output): 1. Give the user the `registrationUrl` from the JSON output 2. Tell them to open it **on their iPhone** and tap "Register Device" 3. They'll download a profile — guide them: **Settings > General > VPN & Device Management > install the profile** 4. After success page appears, sync with Apple: ```bash ./ios-cli register-apple ``` Also remind them to **enable Developer Mode**: > Settings > Privacy & Security > Developer Mode > toggle ON > restart when prompted. ### Step 3: Sign Run `./ios-cli sign --from-sim ` (the simBuildId came from the install message's `(build: )` parenthetical) — see the **Install-on-Device callback** section. --- ## Normal Flow The default path. No auth, no register, no sign. The user gets a working preview link they can click in any channel (chorus, Telegram, WhatsApp, iMessage, Safari). ### 1. Generate App Icon Before building, generate an app icon and overwrite the bootstrap template's placeholder at `Assets.xcassets/AppIcon.appiconset/AppIcon.png`. Read the source code you just wrote and identify what makes this app's **value** unique — not its category. Pick the one visual element that would make someone understand what the app does at a glance. Use Gemini CLI's nanobanana `/icon` command: ```bash /icon "App icon design for [app description]. [Visual element] with subtle 3D depth. Premium quality, sophisticated, single focal point, subtle lighting" --sizes="1024" --type="app-icon" --style="modern" --corners="sharp" ``` Always include: `Premium quality, sophisticated, single focal point, subtle lighting`. You can add the app's color scheme from the source code if it has one. **Examples:** - `/icon "App icon design for streak habit tracker. Minimalist progress rings with subtle 3D depth. Premium quality, sophisticated, single focal point, subtle lighting" --sizes="1024" --type="app-icon" --style="modern" --corners="sharp"` - `/icon "App icon design for surf forecast app. Ocean wave curling with subtle 3D depth. Premium quality, sophisticated, single focal point, subtle lighting" --sizes="1024" --type="app-icon" --style="modern" --corners="sharp"` - `/icon "App icon design for split expense tracker. Two overlapping coins with subtle 3D depth. Premium quality, sophisticated, single focal point, subtle lighting" --sizes="1024" --type="app-icon" --style="modern" --corners="sharp"` Copy the generated PNG into the asset catalog: ```bash mkdir -p "{project}/Assets.xcassets/AppIcon.appiconset" cp [generated-icon-path] "{project}/Assets.xcassets/AppIcon.appiconset/AppIcon.png" cat > "{project}/Assets.xcassets/AppIcon.appiconset/Contents.json" << 'EOF' {"images":[{"filename":"AppIcon.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}} EOF ``` ### 2. Build ```bash cd /path/to/project zip -r /tmp/source.zip . -x ".git/*" -x "xcuserdata/*" -x "*.xcuserstate" ./ios-cli build /tmp/source.zip ``` The CLI uploads, builds on cloud macOS, and waits until complete. Outputs `buildJobId`, `state`, and `appUrl`. **If the build fails**, fetch the Xcode compilation errors and fix them: 1. Run `./ios-cli logs ` to get the error lines from the build 2. Fix the errors in your source code based on what the compiler says 3. Re-zip and rebuild: `zip -r /tmp/source.zip . -x ".git/*" -x "xcuserdata/*" -x "*.xcuserstate" && ./ios-cli build /tmp/source.zip` Do NOT give up after a failed build. Read the errors, fix the code, rebuild. Most build failures are missing imports, type mismatches, or project configuration issues that are straightforward to fix from the compiler output. **Build environment**: The build server uses **Xcode 27.0** (iOS 27 SDK) on Apple-silicon macOS. Builds run with `-destination "generic/platform=iOS"` (not `-sdk`, so watch targets get their own SDK) and `CODE_SIGNING_ALLOWED=NO`. Keep `IPHONEOS_DEPLOYMENT_TARGET` at 26.0 unless the user asks otherwise; guard iOS 27-only APIs with `if #available(iOS 27, *)`. The scheme is auto-detected from the `.xcodeproj` — for multi-target projects (app + widget extension), ensure the main app scheme is listed first. Save `buildJobId` to config under the active project. `./ios-cli build` emits `simBuildId` and `previewUrl` once the build completes. The previewUrl includes a JWT token so it works in any browser (chorus webapp, Telegram, WhatsApp, iMessage, Safari). ### 3. Print previewUrl **Always include the `previewUrl` verbatim in your reply.** The chorus webapp will auto-open it in the right panel; other channels (Telegram/WhatsApp/iMessage) will render it as a clickable link the user can open in any browser. The user clicks "Install on device" on the preview itself when they want to install on iPhone — that triggers the **Install-on-Device callback** below. Don't preemptively sign. --- ## Install-on-Device callback **Trigger**: the user asks to install the build, e.g. `Install this build on my device. (build: )`. Pull `simBuildId` from the `(build: )` parenthetical. ### Steps 1. Run `./ios-cli sign --from-sim `. 2. Handle the exit code: - **Success**: prints `installUrl`. Print it verbatim in your reply. Chorus renders inline "Install on device" + "Preview in simulator" buttons under it; user taps Install on their iPhone. - **`NO_APPLE_AUTH`**: server says the user has no usable Apple credentials. Run **First-Time Setup Step 1** (auth flow). Then retry from step 1 above. - **`SIGN_FAILED`** (with hint about devices in error message): the user has no registered iPhone yet, or registered devices haven't been Apple-synced. Run **First-Time Setup Step 2** (registration). Then retry from step 1. - **`SIGN_IN_PROGRESS`**: a sign is already running for this build. Report it and end the turn; retry on the next turn. - **`BUILD_NOT_READY`**: the underlying build job isn't `built` yet. Surface a clear error to the user; usually means the build hasn't completed or has expired. ### Re-Sign After New Device When `register-apple` adds a new UDID to an Apple team, the previous IPA's provisioning profile is stale. Subsequent `sign --from-sim` automatically clears the cached profile and re-signs. No special handling — just rerun the callback steps. --- ## Re-Sign After New Device When a new device is registered, the current build needs re-signing (the provisioning profile must include the new device UDID). Check config for the active project's `buildJobId`. If it exists: > "New device registered. Re-sign your current build to include it?" Then re-trigger: `./ios-cli sign `. No rebuild needed. --- ## Creating a New Project ```bash ./ios-cli bootstrap "My App" "com.example.myapp" /path/to/project ``` Creates a SwiftUI project with SwiftData, tests, asset catalogs from the built-in template. Replaces all placeholders with your app name and bundle ID. Initializes a git repo. Add new `.swift` files directly into the `{App Name}/` directory — Xcode picks them up automatically. After bootstrapping, save to config: ```bash ./ios-cli config set activeUser ./ios-cli config set users..teamId ``` --- ## Adding SPM packages Use `./ios-cli add-spm-package ` — the subcommand writes every required pbxproj section for you (remote reference, product dependency, build-file link, target + project arrays). Do **not** hand-edit `project.pbxproj`. The command is idempotent on (URL, product) so re-runs are safe. Examples: ```bash # Default (upToNextMajor, infer product from URL, link to the first iOS app target) ./ios-cli add-spm-package https://github.com/supabase/supabase-swift 2.0.0 --product Supabase # Pin an exact version, target a specific app ./ios-cli add-spm-package https://github.com/realm/realm-swift 10.50.0 \ --product RealmSwift --target "My App" --version-kind exact # Track a branch instead of a version ./ios-cli add-spm-package https://github.com/owner/foo main \ --product Foo --version-kind branch ``` Available `--version-kind` values: `upToNextMajor` (default), `upToNextMinor`, `exact`, `range` (pass version as `X.Y.Z..A.B.C`), `branch`, `revision`. Match what the package's docs recommend rather than defaulting blindly. After adding the package, `import ` in your Swift source. **The module name is not always the product name** — check the package's README or `Package.swift`. Examples: product `Realm` exposes module `Realm`, but product `RealmSwift` exposes module `RealmSwift`; product `FirebaseFirestore` exposes module `FirebaseFirestore`. Most match 1:1 but verify before importing. **Multiple products from one package** (e.g., `FirebaseAuth` + `FirebaseFirestore` from `firebase-ios-sdk`): run `./ios-cli add-spm-package` once per product against the same URL. The command detects the existing `XCRemoteSwiftPackageReference` for that URL and reuses its id — so you get one repo reference plus a separate `XCSwiftPackageProductDependency` / `PBXBuildFile` / target-array entry per product. Re-running with a product already attached is a no-op. Local path packages (`XCLocalSwiftPackageReference` / `package(path:)`) are out of scope — use a different shape. ### What the pipeline does for you - **Embeds dynamic frameworks.** The pipeline scans `@rpath/.framework` references in the main binary and `.debug.dylib`, copies matching frameworks from `PackageFrameworks/` into `App.app/Frameworks/`. Works for forced-dynamic packages (Realm, RealmSwift, Sentry@dynamic). - **Skips macro trust prompts.** `xcodebuild` runs with `-skipMacroValidation -skipPackagePluginValidation`, so packages with macros (TCA, swift-dependencies, swift-syntax) build without intervention. Build-tool plugins are not wired up: `add-spm-package` links library products only, so a package whose useful product is a plugin (SwiftLint) adds nothing here. - **Resigns embedded frameworks** recursively inside extensions, app clips, and watch targets. **Do NOT add an `Embed Frameworks` (`PBXCopyFilesBuildPhase`) phase yourself.** The pipeline handles it. For static-default packages an explicit embed phase fails the build with `lstat: No such file` because no `.framework` is produced. **Do not strip the standard runpath.** App targets must keep `LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"` (the Xcode default). Without it, dyld cannot find the auto-embedded frameworks at launch even though the pipeline copied them in. ### Multi-target apps (extensions, app clips, watch apps) `packageProductDependencies` is a **per-target** field on each `PBXNativeTarget`. Any non-host target that `import`s an SPM module needs: - an entry in **that target's** `packageProductDependencies` array - a `PBXBuildFile` referencing the product - an entry in **that target's** `PBXFrameworksBuildPhase.files` The `XCRemoteSwiftPackageReference` and project-level `packageReferences` are added once at the project level and shared. This applies to widget extensions, app clips, watch apps, watch extensions, share / notification service / notification content / intents / file provider / network / keyboard / iMessage / audio unit / spotlight / today extensions — same rule, no special case per type. **Dynamic SPM framework consumed by an extension.** Auto-embed scans `@rpath` references in the host's main binary and `.debug.dylib`. **The host target must carry the package as one of its dependencies for the framework to be embedded** — even if the host doesn't directly use the API. So when an extension imports a dynamic SPM product, also add the same `XCSwiftPackageProductDependency` to the host's `packageProductDependencies` and a matching `PBXBuildFile`/Frameworks entry. If you genuinely don't want to call the API from host code, a no-op reference (`_ = ModuleName.self` in the App's init) keeps the symbol from being dead-stripped at link time. The host-target dependency is the load-bearing fix; the symbol reference is a belt-and-suspenders safety check. ### Heavy packages — first build is slow For preview builds only, cold builds of Realm, swift-syntax-based packages (TCA, swift-macro-toolkit), Firebase, React Native, Expo, or large multi-package combinations can take over an hour on Azure. The pipeline persists Xcode's content-addressed compilation cache, so later builds that reuse compiler inputs should be substantially faster. The signing service allows up to 130 minutes by default for a blocking preview build. Publish builds must use `--no-wait`: report the `buildJobId` and end the turn so the backend owns the wait. If the user asks for status during a blocking preview build, run `./ios-cli logs ` to surface live Azure progress (resolving packages, compiling targets, etc.). Most "stuck" preview builds are just slow, not broken. For a publish build, report the `buildJobId` and end the turn; do not wait or poll. --- ## Adding Supabase as a backend **When an iOS app needs Supabase as its backend, wire it with `./ios-cli supabase setup` — always. Do not hand-write the `SupabaseClient` config, and do not reuse a generic Supabase database/CRUD integration for the app wiring** (those are for the agent's own queries against a DB — a different concern). `supabase setup` is the only thing that gets the *app* side right: it selects the client-safe **publishable (anon)** key, **refuses a service_role/secret key** (which must never ship inside an app binary), auto-detects a connection already present in the environment so you don't re-prompt for creds, and writes the SPM dependency + Swift glue consistently. Hand-wiring is error-prone — wrong key type, a leaked secret, or a missing SPM package. The subcommand: 1. Installs the `supabase` CLI on-demand at `~/.vibecode/bin/supabase` if not on `PATH`, SHA-256 verified against a pinned manifest. 2. Authenticates with the user's Supabase Personal Access Token. 3. Links to the user's existing Supabase project (`--project-ref`). 4. Adds `supabase-swift` to the Xcode project as an SPM dependency. 5. Writes `SupabaseClient+App.swift` into the target source directory with the URL and publishable (anon) key as top-level constants plus a ready-to-use `let supabase = SupabaseClient(...)`. 6. Writes `.vibecode/supabase.json` recording the public config (project ref, URL, publishable key). ```bash ./ios-cli supabase setup --token --project-ref ``` In Swift, just import and use: ```swift import Supabase // supabase is a top-level let from SupabaseClient+App.swift let items: [Item] = try await supabase.from("items").select().execute().value ``` **If the user needs a PAT, give them this exact link — `https://supabase.com/dashboard/account/tokens` — and don't describe dashboard menu navigation (the paths drift; the deep link is stable).** The PAT lives only in the user's Supabase CLI credential store (never in source). The publishable key is safe to ship in the binary — it's the public anon key. Do not pass a `service_role` key as `--token`; the command refuses keys that aren't client-safe. After setup, the agent calls `supabase` directly for schema work — migrations, types generation, edge functions, etc. Docs: https://supabase.com/docs --- ## Publishing For App Store / TestFlight publishing, see [references/publishing.md](references/publishing.md). Privacy policy URL and support URL (required on every production listing, and the privacy link must also appear inside the app): in a Vibecode project sandbox, run `./ios-cli publish legal-pages --bundle ` from the iOS project directory before the first production publish. It fills the standard pages with the app name and the Vibecode account contact and hosts them at fixed URLs; publish start puts both URLs on the listing. Put the returned `privacyPolicyUrl` in the app's Settings screen. Never ask the user for these URLs, a website, hosting, or their contact name/email (the one exception: a full name when the command reports `CONTACT_NAME_MISSING`), and never write, host, or deploy these pages any other way ([details](references/publishing-production.md#legal-pages)). Outside a Vibecode sandbox (Chorus or a laptop) the command is unavailable; supply the URLs in the metadata as before. For App Store Connect work outside the publish flow (subscriptions, in-app purchases, pricing, creating the app record, App Privacy, reviews, analytics), use `./ios-cli asc ...` and follow [references/asc.md](references/asc.md). For production publishing from a Vibecode project sandbox, `publish preflight` and `publish start` automatically verify the project's production backend before contacting the signing service. None of these checks stop publishing: relay every backend warning (not deployed, undeployed changes, status unavailable) and continue. If the app calls its backend, tell the user to deploy it. Chorus API keys skip this Vibecode-only gate. ### App Privacy Before guiding the user through App Privacy, inspect the app's source and dependencies yourself. Decide whether the app or its third-party SDKs send user or device data off-device: check network calls and backends, analytics, attribution, ads, or tracking SDKs (such as Firebase Analytics, Amplitude, AppsFlyer, ad networks, or Sentry configured with PII), and account or login flows. If the app and its SDKs send no user or device data off-device—for example, it has no backend, no analytics, ads, or tracking SDKs, and stores all data locally with SwiftData, UserDefaults, or other on-device storage—tell the user: **Select "No, we do not collect data from this app"** and give a one-line reason. If it does collect data, map the transmitted fields to the exact App Store Connect data types, tell the user to **select "Yes, we collect data from this app"**, and list exactly which data types to mark with a one-line reason. **Do not ask the user to determine whether the app collects data; decide from the code.** When `./ios-cli auth check` shows an Apple ID, fill in and publish App Privacy yourself with `./ios-cli asc web privacy ...` ([references/asc.md](references/asc.md)) instead of sending the user to App Store Connect. ### Regulated medical device Apple requires a regulated medical device declaration before review when `primaryCategory` is `HEALTH_AND_FITNESS` or `MEDICAL`, or the age rating marks medical/treatment information as frequent. Decide the answer from the code, never ask the user, and put it in the publish metadata. - `"regulatedMedicalDevice": { "declared": false }` — most apps: fitness, wellness, habit, water, sleep, diet, calorie, meditation, tracking, reminders, and health information that ends in "see a doctor". `publish start` sets it in App Store Connect through the user's Apple ID session before submitting. - `"regulatedMedicalDevice": { "declared": true }` — only when the app itself diagnoses, treats, or doses a specific condition for a person, connects to medical hardware, or is meant for clinicians. Apple refuses the review submission until every region has its details, so preflight stays red (`REGULATED_MEDICAL_DEVICE_DETAILS_REQUIRED`) until App Store Connect shows the declaration complete. When you declared `true`, do everything you can without being asked, then ask the user once for only their part: 1. In the app: add a "check with a doctor before making medical decisions" reminder and disclose the data sources and methodology behind any health measurement (App Review Guideline 1.4.1). 2. Declare it: `./ios-cli asc web apps medical-device set --app --declared true --countries-or-regions EEA,GBR,USA --confirm` (only the regions the app will ship in). 3. Write each region's details from the app's code: the instructions for use (add them to the support page with `./ios-cli publish legal-pages --extra-support ""`, or to a page the app already hosts, and use that URL), the use statement (what the app does and who it is for: indications for use in the US, intended purpose in the EEA and UK), and the safety information (limits, warnings, when to see a doctor). Save one `region.json` per region ([references/asc.md](references/asc.md)). 4. Ask the user once, only for: creating the compliance contact on the App Store Connect website (Business → Compliance → Regulated Medical Devices), the FDA Owner/Operator Number (US), and the EU Manufacturer SRN (EEA), from their real registration. If App Review asks for clearance documents, those are the user's too. 5. Once the contact exists, run `./ios-cli asc web apps medical-device region set --app --region --input region.json --confirm` per region (the UK needs no registration number, so it can go first), then re-run preflight. Start publish builds asynchronously so the disposable sandbox never owns the wait: ```bash BUILD_OUTPUT=$(./ios-cli --output json build /tmp/source.zip --user "$USER_ID" --no-wait) if [ -z "${BUILD_OUTPUT//[[:space:]]/}" ]; then echo "ERROR: ios-cli returned empty output; the publish build was not confirmed" >&2 exit 1 fi BUILD_JOB_ID=$(printf '%s' "$BUILD_OUTPUT" | jq -er '.buildJobId') printf 'First-publish cloud build started: %s\n' "$BUILD_JOB_ID" ``` **For a newly started or still-running job, stop the agent turn after this command and report `buildJobId` to the user. Never run `status build --wait`, never pipe the command through `tail`, and never park a shell waiting for the publish build.** In a Vibecode sandbox, `--no-wait` persists the job in the existing backend publish submission; the backend completion sweep watches it after the sandbox is reaped and queues an exact resume prompt when the artifact is ready. That resume prompt contains the same `buildJobId`: reuse it and do not build again. If this command instead returns `reused:true`, `state:"built"`, and `resumeRequired:true`, the durable build is already ready after a prior interrupted turn: continue the remaining first-publish steps immediately with that id. If JSON output contains `trackingError`, report it together with the `buildJobId` and run the printed `recoveryCommand`; do not rerun `build`. Empty stdout is always a reportable CLI failure, never evidence that a build is still working. Long-running CLI commands emit `[heartbeat]` lines on stderr even with `--output json`; silence from a command that should be waiting means the process is no longer healthy. Publish preflight requires `state=built`; run it only in the backend-queued resume turn. Screenshots come from the project's simulator, not the build, so they can be captured in either turn. The normal build command remains blocking when `--no-wait` is omitted for non-publish preview builds. ### App Store screenshots iPhone screenshots are real captures of the running app, never screens redrawn in HTML. Drive the project's simulator to each screen worth showing, save it, then let `generate` add the headline and device frame locally with the bundled Playwright + Koubou toolchain. ```bash # 1. Preflight the local renderer (one-time; prewarmed in runner images) ./ios-cli screenshots setup # 2. Per screen (1-10): open it with simulator-ensure and the ios-* tools, # confirm it with ios-screenshot-and-element-tree, then save it ./ios-cli screenshots capture --simulator --file 01-home.png # 3. Add headlines and the device frame locally ./ios-cli screenshots generate config.yaml # 4. Upload each printed output path and keep the returned URL for metadata.screenshots[] ./ios-cli publish upload-screenshot \ --file output-iphone/01_hero.png --device-type IPHONE_69 ``` Before capturing, fill the app with realistic content through its own UI. Empty states, placeholder text, and open keyboards or alerts make poor store screenshots. Minimal `config.yaml` (keep it beside the captured PNGs): ```yaml project: name: "My App" output_dir: "output-iphone" device: "iPhone 16 Pro Max - Black Titanium - Portrait" output_size: "iPhone6_9" screenshots: 01_hero: template: "marketing_iphone_hero.html" variables: headline_top: "Every metric." headline_bottom: "One glance." subtitle: "Revenue, customers, and orders." assets: app_screenshot: "01-home.png" ``` `app_screenshot` must be under `assets:` and must be a basename beside the YAML. The preview cannot run an iPad yet, so only a universal app's separate iPad config may use an authored `.html` app screen. When the app supports iPhone Duo, also capture its outer and inner displays and upload them as the Duo set. Full schema, templates, dimensions, the Duo steps and the iPad path: [references/screenshots.md](references/screenshots.md). Outside a project sandbox, `screenshots capture ` starts a private preview of a finished build and saves its launch screen instead; `--wait-ms` (0–60000, default 5000) delays that capture. ### Routing apps (transit, ride-share, navigation) If the user is building a routing app, the project's pbxproj must declare which transit modes the app supports. Add this build setting: ``` INFOPLIST_KEY_MKDirectionsApplicationSupportedModes = "MKDirectionsModeCar MKDirectionsModeTransit MKDirectionsModeWalking"; ``` Pick the subset that matches the app from: `MKDirectionsModeCar`, `MKDirectionsModeTransit`, `MKDirectionsModeWalking`, `MKDirectionsModeBus`, `MKDirectionsModeFerry`, `MKDirectionsModeStreetCar`, `MKDirectionsModePedestrian`, `MKDirectionsModeRideShare`, `MKDirectionsModeBike`, `MKDirectionsModeOther`. --- ## References - [API Reference](references/api-reference.md) — all signing service endpoints - [Capabilities & Entitlements](references/capabilities.md) — per-capability rules, entitlements, Info.plist keys, extension matrix - [Device Registration](references/device-registration.md) — UDID enrollment flow details - [Config Schema](references/config-schema.json) — config file structure - [Gotchas](references/gotchas.md) — common issues and fixes - [iOS App Store Publishing](references/publishing.md) — publishing contract, required CLI flow, and routing - [Production Publish](references/publishing-production.md) — production metadata and submission flow - [TestFlight](references/publishing-testflight.md) — internal, external, and single-call TestFlight flows - [Publishing Readiness](references/publishing-readiness.md) — readiness check names and remediations - [Publishing Gotchas](references/publishing-gotchas.md) — App Store Connect constraints and recovery paths