Mobile app development automation — SDK integration, CLI orchestration, dashboard configuration, and app store operations. Use when setting up or managing mobile app SDKs (PostHog, Adapty, Firebase, Meta, AppsFlyer, Superwall), configuring app stores (App Store Connect, Google Play), or automating dashboard tasks via browser. Triggers on "set up SDK", "configure analytics", "connect app store", "set up paywall", "mobile setup", "shipyard", or any mobile app infrastructure task.
Professional mobile app development automation. One skill to set up, configure, and manage every SDK, CLI tool, and dashboard your app needs — across iOS (SwiftUI), Android (Kotlin/Compose), Flutter, and React Native (Expo).
CLI-first, browser-fallback, code-always. Every action follows this priority:
Before any setup, detect the project platform:
project.yml / *.xcodeproj → iOS (SwiftUI)
build.gradle.kts / build.gradle → Android (Kotlin/Compose)
pubspec.yaml → Flutter
package.json + app.json/app.config.js → React Native (Expo)
Adapt ALL SDK integration patterns to the detected platform:
project.yml or Package.swift, Swift service classes, @MainActor patternspubspec.yaml dependencies, Dart service classes, Provider/Riverpodpackage.json dependencies, TypeScript service modules, hooks| SDK | Purpose | CLI | MCP | Install Skill |
|---|---|---|---|---|
| PostHog | Product analytics, session replay, feature flags | posthog (npm) | mcp__posthog__* | npx skills add posthog-setup |
| Adapty | Paywalls, subscriptions, A/B testing | adapty (npm) | — | — |
| Firebase | Auth, Firestore, Analytics, Crashlytics | firebase (npm) | — | — |
| Meta/Facebook | Attribution, ad events, deep links | — | — | — |
| AppsFlyer | Install attribution, deep links | — | — | — |
| Superwall | Paywall experimentation (optional) | superwall | — | npx skills add superwall-ios-quickstart |
| App Store Connect | App management, subscriptions, TestFlight, metadata + screenshot sync | asc (brew) + custom Python client for full REST API | — | npx skills add ios-marketing-capture |
| Google Play Console | Android app management | — | — | — |
All SDK integrations MUST follow the AnalyticsRouter pattern — a single fan-out service that routes events to all providers. Never call SDKs directly from ViewModels or Views.
Views → ViewModels → AnalyticsRouter → [Firebase, PostHog, Meta, AppsFlyer]
→ AdaptyService → [Adapty SDK]
Key principles:
private — only the router touches them$revenue for PostHog, AFEventParamRevenue for AppsFlyer)Graceful fallbacks: When API keys are placeholders, SDKs auto-disable via guard checks in SDKKeys. No crashes, no error UI — just silent no-ops with DEBUG console warnings.
When CLI/API doesn't cover a task (e.g., Adapty paywall visual builder), use Playwright MCP.
Always dispatch Playwright work as a subagent — never inline browser automation in the main context. Use the Agent tool with the appropriate model tier based on task complexity (see Model Selection below).
Pick the model based on how much reasoning the browser task requires:
| Model | Use when | Examples |
|---|---|---|
| Haiku | Pure mechanical action — click, type, navigate, screenshot. No decisions needed. | Filling a form with known values, clicking a known button, navigating to a URL, taking screenshots for verification |
| Sonnet | Needs to understand UI structure, pick from options, adapt to what it sees on screen. | Selecting a paywall template, customizing copy fields, navigating an unfamiliar dashboard flow |
| Opus | Complex multi-step flows with branching decisions, error recovery, or ambiguous UI states. | Setting up a completely new dashboard with many unknown steps, recovering from auth failures mid-flow, cross-dashboard orchestration |
Default: Start with Haiku. If the task requires judgment calls (which template to pick, how to interpret UI), use Sonnet. Reserve Opus for flows that repeatedly fail or require deep reasoning about the UI state.
Use the Agent tool. Always include in the prompt:
Agent({
description: "Fill Adapty paywall form fields",
model: "haiku", // or "sonnet" / "opus"
prompt: "The browser is already at https://app.adapty.io/paywalls/123/edit.
Click the headline field (ref: e42), clear it, type 'Touch Grass First'.
Click Save. Take a screenshot. Return: success/failure + screenshot path."
})
Playwright MCP must be configured to use the user's existing Chrome profile:
{
"playwright": {
"command": "<path-to>/playwright-mcp",
"args": ["--browser", "chrome", "--user-data-dir", "<user-home>/Library/Application Support/Google/Chrome"]
}
}
Critical: The user must close Chrome before Playwright can use the profile. Always warn before launching.
https://app.adapty.io)| Dashboard | What to automate | Model | How |
|---|---|---|---|
| Adapty | Paywall visual builder, template selection | Sonnet | Playwright subagent — no API available |
| Adapty | Simple field edits, button clicks after builder is open | Haiku | Playwright subagent |
| PostHog | Project settings, dashboards | Haiku | PostHog MCP (mcp__posthog__*) or CLI preferred |
| Firebase | Project creation, service accounts | Haiku | firebase CLI preferred |
| App Store Connect | Everything | Haiku | asc CLI (covers 95% of operations) |
| AppsFlyer | Dashboard setup, complex flows | Sonnet | Playwright subagent (limited API) |
| Command | Purpose |
|---|---|
/shipyard | Main entry — detect platform and guide to the right sub-command |
/shipyard:init | Initialize SDK integrations for a new or existing mobile app |
/shipyard:abilities | Audit installed CLIs, MCPs, skills — show ready vs missing |
/shipyard:setup <sdk> | Deep setup for one SDK (posthog, adapty, firebase, meta, appsflyer, superwall) |
/shipyard:connect <store> | Connect app stores — subscriptions, pricing, localizations, webhooks |
/shipyard:asc-sync | Push metadata + screenshots to App Store Connect for all locales (one command) |
/shipyard:dashboard <service> | Open and configure a dashboard via Playwright browser automation |
/shipyard:status | Health check of all SDK integrations in the current project |
/shipyard:prices <territory> | Set subscription pricing for a specific market |
/shipyard:init-skills | Auto-install all platform-appropriate Claude Code skills for the project |
/shipyard:learn-lessons | Analyze current project end-to-end and extract lessons — writes docs/LESSONS-LEARNED.md + promotes generalizable lessons to ~/.claude-shared/shipyard-lessons/ |
/shipyard:update-learned-lessons | Scan current project against every applicable lesson in the shared library, report gaps, offer to apply fixes (platform-aware) |
/shipyard:help | Show all commands with examples |
/shipyard:init — Initialize SDK integrationsInteractive setup wizard for a new or existing mobile app.
Process:
Output: Summary of what was set up, what keys are still placeholder, and next steps.
/shipyard:abilities — Show available tools and gapsAudit the development environment for mobile app tooling.
Process:
which adapty firebase posthog asc superwall 2>/dev/null
adapty auth status 2>/dev/null
firebase login:list 2>/dev/null
asc auth status 2>/dev/null
ls ~/.claude-shared/skills/ | grep -E "superwall|ios-marketing|posthog|firebase|adapty"
READY:
✅ adapty CLI v0.1.5 — authenticated as semih
✅ asc CLI v1.2.1 — authenticated as Runlock
✅ Playwright MCP — Chrome profile configured
MISSING:
❌ posthog CLI — install: npm i -g posthog-cli
❌ firebase CLI — install: npm i -g firebase-tools
⚠️ superwall CLI — optional, install: brew install superwall
SKILLS TO ADD:
📦 npx skills add ParthJadhav/ios-marketing-capture -g
📦 npx skills add superwall-ios-quickstart -g
/shipyard:setup <sdk> — Set up a specific SDKDeep setup for one SDK. Available SDKs: posthog, adapty, firebase, meta, appsflyer, superwall.
Example: /shipyard:setup adapty
adapty CLI installed and authenticatedadapty apps list → create if notSDKKeys.swift (or platform equivalent)premiumasc app-setup/shipyard:connect <store> — Connect app storesSet up app store integrations. Available: appstore, playstore.
Example: /shipyard:connect appstore
Reality check: the community asc CLI covers TestFlight and some subscription ops, but NOT full metadata or screenshot sync. For the "ship v1.0" workflow below, use the App Store Connect REST API via a Python client (see the App Store Connect REST API section later).
Full v1.0 release prep workflow:
Auth setup (one-time per project)
.p8 ONCE (Apple never shows it again)~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 (chmod 600)~/.zshrc: APP_STORE_API_KEY, APP_STORE_API_ISSUERSubscriptions (if monetized)
asc CLI or PlaywrightMetadata sync (use /shipyard:asc-sync) — see next section. Covers:
Localization matrix
tr for a Turkish-first app)en-US, ru, etc.) even if app UI is single-language — App Store listings can still be multi-localeMüslüman not Musluman, ÖZELLİKLER not OZELLIKLER/shipyard:asc-sync — Push metadata + screenshots to App Store ConnectOne-command sync for app listing: metadata for all locales + screenshots per locale + review info + category. Idempotent — safe to re-run.
Prereq structure in project:
marketing/
├── asc-metadata/
│ ├── shared.json # URLs, copyright, review contact, review notes, primary_category, release_type
│ ├── <locale>/
│ │ ├── name.txt # ≤30 chars (App Store limit)
│ │ ├── subtitle.txt # ≤30 chars
│ │ ├── description.txt # ≤4000 chars
│ │ ├── keywords.txt # ≤100 chars, comma-separated, no spaces after commas
│ │ ├── promotional_text.txt # ≤170 chars, editable anytime
│ │ └── release_notes.txt # ≤4000 chars (ignored on first version)
└── asc-screenshots/
└── <locale>/
├── iphone-6-9/*.png # APP_IPHONE_67: 1290×2796 (or APP_IPHONE_69: 1320×2868)
├── iphone-6-5/*.png # APP_IPHONE_65: 1242×2688
└── ipad-12-9/*.png # APP_IPAD_PRO_129: 2048×2732
Execution order (the sync script does this):
GET /apps?filter[bundleId]=<bundle> → resolve app_idGET /apps/<app_id>/appStoreVersions?filter[appStoreState]=PREPARE_FOR_SUBMISSION,... → version_idGET /apps/<app_id>/appInfos → pick editable → app_info_idPATCH /appInfos/<app_info_id> — primaryCategory relationship (e.g., LIFESTYLE, HEALTH_AND_FITNESS)PATCH /appStoreVersions/<version_id> — copyright, releaseType (MANUAL / AFTER_APPROVAL / SCHEDULED)PATCH or POST /appStoreReviewDetails — contact name/phone/email + review notesappInfoLocalization (name, subtitle, privacyPolicyUrl)appStoreVersionLocalization (description, keywords, promotionalText, marketingUrl, supportUrl)PATCH whatsNew separately — catch STATE_ERROR (first-version releases reject whatsNew)appScreenshotSet per display typeIdempotency patterns (must implement in the sync script):
POST returns 409 DUPLICATE: refetch and PATCH insteadPATCH of whatsNew returns 409 STATE_ERROR: skip silently on first-release versionsPOST, otherwise reuse existing set ID/shipyard:dashboard <service> — Open and configure a dashboardUse Playwright to automate dashboard configuration.
Process:
/shipyard:status — Overall integration health checkQuick overview of all SDK integrations in the current project.
SDKKeys (or equivalent) — check which keys are real vs placeholderSDK Status:
✅ PostHog — key set, session replay enabled, ATT wired
✅ Adapty — key set, 2 paywalls, 3 products, webhook configured
⚠️ AppsFlyer — placeholder key, will silently disable
⚠️ Meta — no FACEBOOK_APP_ID in build settings
✅ Firebase — configured, anonymous auth active
/shipyard:prices <territory> — Set subscription pricingRecommend and configure subscription prices for a specific market.
asc CLI/shipyard:init-skills — Auto-install platform skillsDetects the project platform and installs all relevant Claude Code skills via git clone to ~/.claude-shared/skills/.
Process:
Detect platform — scan project files (see Platform Detection above)
Resolve skill directory — SKILLS_DIR="$HOME/.claude-shared/skills"
For each skill in the platform registry:
a. Check if $SKILLS_DIR/<skill-name>/SKILL.md already exists → skip if present
b. git clone <repo-url> $SKILLS_DIR/<skill-name>
c. If SKILL.md is nested (e.g., <skill-name>/<nested-dir>/SKILL.md), flatten: cp -r <nested-dir>/* .
d. Verify SKILL.md exists at root level
Update project CLAUDE.md — read the project's CLAUDE.md, find the platform rules section, and add Skill Reference lines for every installed skill that isn't already listed. Each line follows the format:
- **Skill Reference**: Use `<skill-name>` skill for <purpose>
Only add references for skills relevant to the detected platform. Do NOT duplicate references that already exist in the file.
Create ai-rules/ folder — if it doesn't exist, create ai-rules/ in the project root with domain-specific rule files tailored to the detected platform. This follows the Zabłocki progressive-disclosure pattern: a rule-loading.md index tells the LLM which rules to load on demand.
For iOS projects, create these files:
ai-rules/rule-loading.md — index of all rule files with loading triggers and keywords (MUST list app-store.md with triggers: "submit", "release", "screenshot", "metadata", "ASC", "App Store Connect", "localization", "review notes")ai-rules/general.md — core Swift engineering rules (always loaded): progressive architecture, error handling, dependency injection, localization, quality gates, anti-patternsai-rules/view.md — SwiftUI view rules: Liquid Glass API patterns, modifier order, design system tokens, animation patterns (staggered reveal, celebration, content transitions), reusable components listai-rules/view-model.md — ViewModel rules: @Observable pattern, computed derived state, async data loading, String(localized:), state ownershipai-rules/services.md — services/SDK rules: AnalyticsRouter pattern, graceful SDK fallbacks, revenue event formatting, HealthKit/FamilyControls patternsai-rules/testing.md — testing rules: test behavior not implementation, mock dependency injection, focus areas, test structureai-rules/app-store.md — App Store Connect submission readiness: metadata file layout, locale matrix, character limits, Turkish/diacritic correctness, screenshot specs, review info, idempotent sync patterns (see template below)rule-loading.md trigger block for app-store.md (add to the trigger index):
### app-store.md - App Store Connect Submission Readiness
**Load when:**
- Preparing for App Store submission or TestFlight release
- Running `/shipyard:asc-sync`
- Editing files under `marketing/asc-metadata/` or `marketing/asc-screenshots/`
- Adding a new locale to the App Store listing
- Reviewing/fixing screenshot sizes, character limits, or review info
- Debugging ASC API 401/409 errors
**Keywords:** App Store, ASC, asc-sync, submission, metadata, screenshot, localization, keywords, description, review notes, TestFlight, promotional text, release notes, locale, Turkish diacritics
Each rule file uses the <primary_directive>, <rule_N priority="...">, <pattern name="...">, <checklist>, and <avoid> XML-like tag structure for optimal LLM parsing.
ai-rules/app-store.md template contents (copy into the file):
<primary_directive>
Before invoking `/shipyard:asc-sync` or submitting to App Store Connect, the project MUST have complete, validated metadata for every locale and screenshots at the correct display-type resolutions. Broken submissions are expensive: rejections delay releases by days.
</primary_directive>
<rule_1 priority="critical">
**Metadata file layout is fixed.** The sync script expects exactly this structure. Do not invent new paths.
marketing/ ├── asc-metadata/ │ ├── shared.json (urls, copyright, review_contact, review_notes, primary_category, release_type) │ └── <locale>/ (e.g. tr, en-US, ru, ar) │ ├── name.txt ≤30 chars │ ├── subtitle.txt ≤30 chars │ ├── description.txt ≤4000 chars │ ├── keywords.txt ≤100 chars, comma-separated, NO space after comma │ ├── promotional_text.txt ≤170 chars (editable post-release) │ └── release_notes.txt ≤4000 chars (ignored on v1.0) └── asc-screenshots/<locale>/<display-slug>/*.png
</rule_1>
<rule_2 priority="critical">
**Character limits are enforced by Apple, not by the sync script.** The script just uploads — Apple rejects at submission time. Validate locally before running sync:
- name ≤ 30, subtitle ≤ 30
- keywords ≤ 100 total chars (including commas)
- promotional_text ≤ 170
- description ≤ 4000
</rule_2>
<rule_3 priority="critical">
**Turkish and non-ASCII diacritics must be correct.** Common mojibake from copy-paste:
- ✅ Müslüman, günlük, ÖZELLİKLER, İmsak, Öğle, Akşam
- ❌ Musluman, gunluk, OZELLIKLER, Imsak, Ogle, Aksam
Run a grep for ASCII-only variants of Turkish words before every sync. Same discipline for Arabic (`ar`), Russian (`ru`), German (`de`).
</rule_3>
<rule_4 priority="high">
**Locales are independent of in-app language.** A Turkish-only app can (and should) have en-US and ru App Store listings. The listing text drives discovery; the app can be single-language.
</rule_4>
<rule_5 priority="high">
**Screenshots must match display-type specs exactly.** Apple rejects with cryptic errors on off-by-one resolutions:
| Slug | Resolution | Maps to |
|---|---|---|
| `iphone-6-9` | 1290×2796 | `APP_IPHONE_67` (covers 6.7"+6.9" in practice) |
| `iphone-6-5` | 1242×2688 | `APP_IPHONE_65` |
| `ipad-12-9` | 2048×2732 | `APP_IPAD_PRO_129` |
If source captures are 1206×2622 (iPhone 17 Pro native), upscale to 1290×2796 with aspect-preserving black letterbox padding. Never stretch.
3–10 screenshots per set. Same 10 images can be reused across locales when the UI is single-language.
</rule_5>
<rule_6 priority="high">
**Review info is required before first submission.**
- `shared.json.review_contact` — full name, phone with country code, email that Apple reviewers can actually reach
- `shared.json.review_notes` — if the app needs a test account, include login credentials here. If it uses HealthKit/FamilyControls/special entitlements, document the permission flow
- Missing review info = instant rejection with "Guideline 2.1"
</rule_6>
<rule_7 priority="medium">
**Release notes (`whatsNew`) are NOT editable on v1.0.** Apple's logic: there's nothing "new" about a first release. The sync script catches `409 STATE_ERROR` and skips silently. For v1.0 builds, leave `release_notes.txt` empty or don't create it — avoid churn.
</rule_7>
<rule_8 priority="medium">
**Primary category matters for discoverability.** Use the closest Apple category. Examples:
- Prayer/religion apps → `LIFESTYLE`
- Habit/health trackers → `HEALTH_AND_FITNESS`
- Productivity/todo → `PRODUCTIVITY`
Secondary category is set in ASC UI, not the sync script (REST limitation).
</rule_8>
<rule_9 priority="medium">
**Release type decides when users get the update.**
- `MANUAL` — release when I click the button (safest for risky changes)
- `AFTER_APPROVAL` — release immediately after review passes
- `SCHEDULED` — release at a specific date/time (requires `scheduledReleaseDate`)
Set in `shared.json.release_type`.
</rule_9>
<pattern name="Pre-sync validation checklist">
Run this BEFORE `/shipyard:asc-sync`:
- [ ] All locale folders present with all 6 .txt files
- [ ] Character counts under limits (`wc -m marketing/asc-metadata/*/*.txt`)
- [ ] Turkish diacritics verified (grep for `Musluman`, `gunluk`, `OZELLIKLER` → should return nothing)
- [ ] Keywords have no trailing spaces after commas (grep for `, ` in keywords.txt → should return nothing)
- [ ] Screenshots resized to exact display-type resolution (imagemagick `identify` to verify)
- [ ] `shared.json` has valid review_contact (reachable phone + email)
- [ ] `APP_STORE_API_KEY` + `APP_STORE_API_ISSUER` env vars set
- [ ] `.p8` file exists at `~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8`
- [ ] Key role is **Admin** or **App Manager** (not Developer)
</pattern>
<pattern name="Idempotent re-runs">
The sync is designed to be safe to re-run. Know what it does:
- `POST` returns `409 DUPLICATE` → script falls back to GET + PATCH
- `PATCH whatsNew` returns `409 STATE_ERROR` → script skips silently (v1.0)
- Screenshot sets: existing set is reused, its contents are wiped and re-uploaded each run
- Stale sets from old display types (e.g. leftover `APP_IPHONE_61`) must be cleaned manually — they cause "mixed sizes" warnings in ASC UI
</pattern>
<checklist>
When adding a new locale:
- [ ] Create `marketing/asc-metadata/<new-locale>/` with all 6 files
- [ ] Create `marketing/asc-screenshots/<new-locale>/iphone-6-9/` (reuse primary-locale PNGs if UI is single-language)
- [ ] Validate character limits and diacritics
- [ ] Add locale to the sync script's locale list (if it has a hardcoded list)
- [ ] Dry-run: `python3 Scripts/asc-sync.py --dry-run --locales <new-locale>`
- [ ] Live run: `python3 Scripts/asc-sync.py --locales <new-locale>`
- [ ] Verify in ASC UI that listing appears correctly
</checklist>
<avoid>
- Hardcoding English copy for non-English locales ("en-US fallback" is lazy and tanks conversion)
- Committing raw screenshots to git (binary bloat) — commit only the text metadata; screenshots are regenerated by capture scripts
- Editing listing copy in the ASC web UI after the first sync (changes get overwritten on next sync — edit the `.txt` files instead)
- Re-running sync without validating character counts — Apple may accept upload and reject at submission
- Using `APP_IPHONE_61` (6.1") as the primary set — Apple now derives smaller displays from `APP_IPHONE_67`
- Storing `.p8` files inside the repo — keychain or `~/.appstoreconnect/` only, chmod 600
</avoid>
Skip if ai-rules/ already exists — don't overwrite existing rule files.
Update project CLAUDE.md — two additions:
a. Add a Rule Index section near the top pointing to @ai-rules/rule-loading.md
b. Add Skill Reference lines for every installed skill not already listed
Only add references for skills relevant to the detected platform. Do NOT duplicate.
Report: installed count, skipped count, any failures, CLAUDE.md references added, ai-rules created (yes/no)
CLAUDE.md Skill Reference Templates (by platform):
- **Skill Reference**: Use `swiftui-liquid-glass` skill for Liquid Glass implementation patterns
- **Skill Reference**: Use `swiftui-ui-patterns` skill for SwiftUI best practices
- **Skill Reference**: Use `swift-concurrency-expert` skill for async/await patterns
- **Skill Reference**: Use `swiftui-pro` skill for advanced SwiftUI patterns and component design
- **Skill Reference**: Use `swift-concurrency-pro` skill for structured concurrency and actor patterns
- **Skill Reference**: Use `swift-architecture` skill for MVVM, Clean Architecture, and design patterns
- **Skill Reference**: Use `swift-security-expert` skill for iOS security best practices (keychain, encryption, ATT)
- **Skill Reference**: Use `swift-testing-pro` skill for Swift Testing framework patterns
- **Skill Reference**: Use `swiftdata-pro` skill for SwiftData persistence patterns
- **Skill Reference**: Use `core-data-expert` skill for Core Data patterns (if needed alongside SwiftData)
- **Skill Reference**: Use `app-store-aso` skill for App Store Optimization
- **Skill Reference**: Use `shipyard` skill for SDK integration, CLI orchestration, and app store operations
- **Skill Reference**: Use `mobile-android-design` skill for Material 3 patterns
- **Skill Reference**: Use `app-store-aso` skill for Play Store listing optimization
- **Skill Reference**: Use `shipyard` skill for SDK integration, CLI orchestration, and app store operations
Platform Skill Registries:
| Skill Name | Repo | Purpose |
|---|---|---|
| swiftui-pro | github.com/twostraws/SwiftUI-Agent-Skill | Advanced SwiftUI patterns and best practices |
| swiftdata-pro | github.com/twostraws/SwiftData-Agent-Skill | SwiftData persistence patterns |
| swift-concurrency-pro | github.com/twostraws/Swift-Concurrency-Agent-Skill | Async/await and structured concurrency |
| swift-testing-pro | github.com/twostraws/Swift-Testing-Agent-Skill | Swift Testing framework patterns |
| swift-architecture | github.com/efremidze/swift-architecture-skill | Swift architecture patterns (MVVM, Clean, etc.) |
| swift-security-expert | github.com/ivan-magda/swift-security-skill | iOS security best practices |
| core-data-expert | github.com/AvdLee/Core-Data-Agent-Skill | Core Data patterns and migrations |
| app-store-aso | github.com/timbroddin/app-store-aso-skill | App Store Optimization |
Note: Skills already bundled in user config (swiftui-liquid-glass, swiftui-ui-patterns, swift-concurrency-expert, app-onboarding-questionnaire, shipyard, marketing-psychology, onboarding-cro) are NOT cloned — they are already available.
Example output:
Skills installed for iOS project:
✅ swiftui-pro — already installed, skipped
✅ swiftdata-pro — installed
✅ swift-concurrency-pro — installed
✅ swift-testing-pro — installed
✅ swift-architecture — already installed, skipped
✅ swift-security-expert — installed
✅ core-data-expert — installed
✅ app-store-aso — installed
6 installed, 2 skipped, 0 failed
| Skill Name | Repo | Purpose |
|---|---|---|
| app-store-aso | github.com/timbroddin/app-store-aso-skill | Store listing optimization |
(Android skill registry will grow as community skills are published)
(Flutter skill registry will grow as community skills are published)
(React Native skill registry will grow as community skills are published)
Auto-trigger: When /shipyard:init completes SDK setup, it should suggest running /shipyard:init-skills to install platform skills.
/shipyard:learn-lessons — Extract lessons from the current projectAnalyze the current project end-to-end (git history, plans, architecture, SDK integrations, localization, extensions) and capture what was learned. Produces a project-local retrospective document and promotes generalizable lessons to the shared library at ~/.claude-shared/shipyard-lessons/.
Process:
/shipyard:init (iOS, Android, Flutter, RN Expo)git log, plans folder, architecture docs, SDK fingerprints, localization surface, extension targets, memory recordsrequire / avoid), severity (critical / high / medium / low), scope (project-specific vs generalizable)docs/LESSONS-LEARNED.md — systematic table format (summary table, category tables, metrics snapshot, top takeaways)lessons/L-XXX-NNN-*.md files in ~/.claude-shared/shipyard-lessons/lessons/ with full frontmatter, Detect shell block, Fix steps, worked exampleslast_verified and add to ## Also observed in insteadINDEX.md — append new rows, keep sorted by ID, update totals and source-projects tabledocs: learned-lessons for <project> and docs(shipyard-lessons): add L-XXX-NNN from <project>Output: Compact summary showing the project-local doc path, new library IDs, reinforced existing IDs, top 3 takeaways.
When to use: At any project milestone, after shipping a major feature, or as a retrospective. Every project you ship should make the next one easier.
Library conventions:
L-IOS-NNN (iOS), L-AND-NNN (Android), L-FLU-NNN (Flutter), L-RNE-NNN (RN/Expo), L-CRS-NNN (cross-platform), L-PRC-NNN (process). IDs never reused.register-extension-bundle-ids-early.md)/shipyard:update-learned-lessons — Apply the lessons library to this projectScan the current project against every applicable lesson in ~/.claude-shared/shipyard-lessons/ and apply the fixes where gaps are detected. Platform-aware — only runs lessons tagged for the detected platform plus cross-platform and process lessons.
Process:
/shipyard:learn-lessonsINDEX.md, filter by matching platforms: frontmatter (plus all/cross-platform and all L-PRC-*)# detect: shell block (exit 0 = pattern PRESENT) or a manual checklistapplies_when precondition not satisfied--all for MEDIUM/LOW)# fix: shell block exists, manual-guided otherwisedocs/LESSONS-LEARNED.md — append to ## Update log with applied/deferred IDschore: apply shipyard lessons L-XXX-NNN, L-YYY-NNNFlags:
--all — include MEDIUM and LOW severity in the report (default: critical + high only)--dry-run — detect and report only, never apply--only <id> — run a single lesson by ID--category <name> — filter by category (sdk, architecture, process, …)--auto — apply CRITICAL + HIGH automated fixes without per-lesson confirmation (use with caution)Safety rules:
/shipyard:learn-lessons's job.github/, Fastfile, or settings.json should be proposed via PR, not applied directlyWhen to use: After /shipyard:init on a new project (proactive gap check), when picking up a project after a pause, or before a release cut to catch regressions against known good patterns.
/shipyard:help — Show all commandsDisplay the full command reference with examples.
When abilities are missing, provide exact install commands:
# CLIs
npm install -g @playwright/mcp@latest
npm install -g adapty
npm install -g firebase-tools
brew install app-store-connect-cli # provides `asc` binary
# Skills (clone to ~/.claude-shared/skills/)
# Use /shipyard:init-skills to auto-install all platform skills
# Manual install example:
SKILLS_DIR="$HOME/.claude-shared/skills"
git clone https://github.com/twostraws/SwiftUI-Agent-Skill.git "$SKILLS_DIR/swiftui-pro"
# If SKILL.md is nested, flatten: cd "$SKILLS_DIR/swiftui-pro" && cp -r swiftui-pro/* . 2>/dev/null
# MCP servers
claude mcp add playwright -- npx @playwright/mcp@latest
# PostHog MCP — available as built-in claude.ai MCP, authenticate via mcp__posthog__authenticate
Always use the user's existing Chrome profile for dashboard automation. This preserves:
Config pattern (in settings.json mcpServers):
{
"playwright": {
"command": "/path/to/playwright-mcp",
"args": ["--browser", "chrome", "--user-data-dir", "~/Library/Application Support/Google/Chrome"]
}
}
Important notes:
--user-data-dir path varies by OS:
~/Library/Application Support/Google/Chrome~/.config/google-chrome%LOCALAPPDATA%\Google\Chrome\User DataNever hardcode user-specific paths. Resolve dynamically:
# Home directory
HOME=$(eval echo ~)
# Node binary path
NODE_BIN=$(dirname $(which node))
# Chrome profile (macOS)
CHROME_PROFILE="$HOME/Library/Application Support/Google/Chrome"
# Project bundle ID
BUNDLE_ID=$(grep "PRODUCT_BUNDLE_IDENTIFIER" project.yml | head -1 | awk '{print $2}')
| Skill | When to use together |
|---|---|
ship-wreck-check | After /shipyard:init — verify SDK integration quality |
qgit | After setup — commit and push changes |
ios-marketing-capture | Capture raw in-app screenshots from simulator |
app-store-screenshots | Generate marketing advertisement slides (Next.js) — headlines, mockup frames, multi-locale, export at all Apple sizes |
app-store-screenshots-2 | AI-driven screenshot generation — analyzes codebase to discover features, generates copy, builds slides |
superwall-*-quickstart | Platform-specific Superwall setup |
analytics-tracking | Plan analytics event taxonomy |
app-store-optimization | ASO after app store connection |
paywall-upgrade-cro | Optimize paywall conversion after Adapty setup |
app-onboarding-questionnaire | Design and build questionnaire-style onboarding flows (Duolingo/Noom pattern) |
swiftui-pro | Advanced SwiftUI patterns — auto-installed by /shipyard:init-skills |
swift-concurrency-pro | Async/await and structured concurrency — auto-installed by /shipyard:init-skills |
swift-testing-pro | Swift Testing framework patterns — auto-installed by /shipyard:init-skills |
swift-architecture | Architecture patterns (MVVM, Clean, etc.) — auto-installed by /shipyard:init-skills |
swift-security-expert | iOS security best practices — auto-installed by /shipyard:init-skills |
Organize screenshots and assets in Figma for design review and App Store submission prep.
import urllib.request, json
# Get file structure
req = urllib.request.Request(
f"https://api.figma.com/v1/files/{FILE_KEY}?depth=1",
headers={"X-Figma-Token": FIGMA_TOKEN}
)
data = json.loads(urllib.request.urlopen(req).read())
# Export a node as PNG
req = urllib.request.Request(
f"https://api.figma.com/v1/images/{FILE_KEY}?ids={NODE_ID}&format=png&scale=2",
headers={"X-Figma-Token": FIGMA_TOKEN}
)
Since programmatic image placement is not possible, use this workflow:
app-store-screenshots or app-store-screenshots-2 skill to export PNGsAs of 2026-04, Figma's official Dev Mode MCP (figma-developer-mcp) is read-only — it inspects designs for code generation but cannot create or modify canvas nodes. No write-capable Figma MCP exists yet. If one becomes available, it would be the preferred method.
FIGMA_PERSONAL_TOKEN firstapp-store-screenshots export — read Figma file structure, post comments for reviewios-marketing-capture — guide user on importing raw screenshots into Figma/shipyard:connect appstore — verify asset organization before submissionTwo screenshot generator skills are available. Choose based on your needs:
| Skill | Approach | Best for |
|---|---|---|
app-store-screenshots | Next.js project, manual copy input, pixel-perfect control | When you have specific copy and know exactly what each slide should look like |
app-store-screenshots-2 | AI-driven, analyzes codebase for features, generates copy automatically | Quick-start when you want AI to figure out what to highlight |
Both support multi-locale, export at all Apple required sizes, and iPhone mockup frames.
The asc CLI covers TestFlight and some subscription operations, but full metadata and screenshot sync require the REST API directly. Build a small Python client — it's ~100 LOC and replaces a mess of curl commands and brittle browser automation.
.p8 exactly ONCE — Apple never shows it again. If lost, revoke and regenerate.~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 (chmod 600)
~/.zshrc:
export APP_STORE_API_KEY="<10-char key ID>"
export APP_STORE_API_ISSUER="<UUID from ASC Integrations page>"
App Store Connect requires ES256 with signature in raw r||s format per RFC 7515 §A.3.
⚠ PyJWT 1.x emits DER-encoded ECDSA signatures, which Apple rejects with a generic 401. The JWT looks perfect, the issuer matches, the key has Admin role, date -u matches Apple's clock — and it still fails. This wasted several hours of debugging on a real project.
Solution: Either use PyJWT ≥2.0 or sign with cryptography directly. The cryptography approach has no version-drift risk:
import base64, json, time
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
def _b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
def make_token(key_id: str, issuer: str, p8_path: Path, ttl: int = 1200) -> str:
pk = serialization.load_pem_private_key(p8_path.read_bytes(), password=None)
now = int(time.time())
header = {"alg": "ES256", "kid": key_id, "typ": "JWT"}
payload = {"iss": issuer, "iat": now, "exp": now + ttl, "aud": "appstoreconnect-v1"}
h = _b64url(json.dumps(header, separators=(",", ":")).encode())
p = _b64url(json.dumps(payload, separators=(",", ":")).encode())
der = pk.sign(f"{h}.{p}".encode(), ec.ECDSA(hashes.SHA256()))
r, s = decode_dss_signature(der)
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big")
return f"{h}.{p}.{_b64url(raw)}"
Apple tokens max out at 20 minutes (exp - iat ≤ 1200). Mint fresh per run; don't cache.
App Store Connect's data model isn't obvious from the docs. Keep this map handy:
app (bundle ID resolves here)
├── appInfos
│ └── appInfoLocalizations (one per locale)
│ ├── name ← app name per locale
│ ├── subtitle ← app subtitle per locale
│ └── privacyPolicyUrl
│ └── primaryCategory ← LIFESTYLE, HEALTH_AND_FITNESS, etc.
└── appStoreVersions (one editable at a time)
├── copyright
├── releaseType ← MANUAL / AFTER_APPROVAL / SCHEDULED
├── appStoreReviewDetail ← review contact + notes
└── appStoreVersionLocalizations (one per locale)
├── description
├── keywords ← comma-separated, no trailing spaces, ≤100 chars
├── promotionalText ← editable anytime, ≤170 chars
├── whatsNew ← release notes, NOT editable on v1.0 initial release
├── marketingUrl
├── supportUrl
└── appScreenshotSets (one per display type)
└── appScreenshots (3–10 per set)
Critical distinction: name/subtitle live on appInfoLocalization (app-wide). description/keywords/whatsNew live on appStoreVersionLocalization (version-specific). Beginners confuse these and keep hitting the wrong endpoint.
def find_editable_version(client, app_id):
r = client.get(
f"/apps/{app_id}/appStoreVersions",
**{"filter[appStoreState]": "PREPARE_FOR_SUBMISSION,DEVELOPER_REJECTED,REJECTED,METADATA_REJECTED,WAITING_FOR_REVIEW,INVALID_BINARY"},
)
data = r.get("data", [])
return data[0] if data else None
Use comma-separated states. If no editable version exists, the user needs to create one in ASC first (requires an uploaded build).
Apple uses a reservation-then-PUT-then-commit protocol so abandoned uploads don't leave dangling files:
import hashlib, requests
def upload_screenshot(client, set_id: str, path: Path):
data = path.read_bytes()
md5 = hashlib.md5(data).hexdigest()
# Phase 1: reserve
resv = client.post("/appScreenshots", {
"data": {
"type": "appScreenshots",
"attributes": {"fileName": path.name, "fileSize": len(data)},
"relationships": {
"appScreenshotSet": {"data": {"type": "appScreenshotSets", "id": set_id}}
},
}
})
sid = resv["data"]["id"]
ops = resv["data"]["attributes"]["uploadOperations"]
# Phase 2: PUT each chunk to Apple's presigned URL
for op in ops:
headers = {h["name"]: h["value"] for h in op.get("requestHeaders", [])}
chunk = data[op["offset"] : op["offset"] + op["length"]]
requests.request(op["method"], op["url"], headers=headers, data=chunk, timeout=120).raise_for_status()
# Phase 3: commit
client.patch(f"/appScreenshots/{sid}", {
"data": {
"type": "appScreenshots",
"id": sid,
"attributes": {"uploaded": True, "sourceFileChecksum": md5},
}
})
Skip Phase 3 and the screenshot sits forever in UPLOADING state. Always include the MD5 — Apple rejects commits without it.
| Display type | Resolution | Device |
|---|---|---|
APP_IPHONE_67 | 1290×2796 | iPhone 14/15/16/17 Pro Max, Plus |
APP_IPHONE_69 | 1320×2868 | iPhone 16/17 Pro Max |
APP_IPHONE_65 | 1242×2688 or 1284×2778 | iPhone XS Max, 11 Pro Max, 12/13 Pro Max |
APP_IPHONE_61 | 1179×2556 | iPhone 14/15 (non-Pro) |
APP_IPAD_PRO_129 | 2048×2732 | iPad Pro 12.9" |
APP_IPAD_PRO_3GEN_129 | 2064×2752 | iPad Pro M4 13" |
For iPhone 17 Pro captures (native 1206×2622, 6.3"), upscale to 1290×2796 (APP_IPHONE_67) — Apple accepts it and derives the 6.9" display from there. Pad with black bars if aspect ratios don't match.
Mobile metadata sync has to be safe to re-run. These error patterns show up every time:
409 DUPLICATE on POST — locale already exists (auto-created by Apple when version was made). Fall back to GET + PATCH:
try:
return client.post("/appStoreVersionLocalizations", payload)["data"]
except RuntimeError as e:
if "DUPLICATE" in str(e) or "already exists" in str(e):
current = get_version_localizations(client, version_id).get(locale)
return client.patch(f"/appStoreVersionLocalizations/{current['id']}", patch_payload)["data"]
raise
409 STATE_ERROR on whatsNew — Apple disallows "What's New" on first-version submissions (1.0). There's nothing "new" about a first release. Either skip entirely for 1.0 or try-catch:
def _try_patch_whats_new(client, loc_id, whats_new):
try:
client.patch(f"/appStoreVersionLocalizations/{loc_id}",
{"data": {"type": "appStoreVersionLocalizations", "id": loc_id,
"attributes": {"whatsNew": whats_new}}})
except RuntimeError as e:
if "STATE_ERROR" in str(e) or "cannot be edited" in str(e):
return # silently skip on first release
raise
Screenshot set: create once, wipe contents each run. Don't delete the appScreenshotSet itself — its relationship to appStoreVersionLocalization is stable. Just delete all appScreenshots inside it and re-upload.
Stale sets from past display-type changes — if someone uploaded to APP_IPHONE_61 in a prior run and you're now uploading to APP_IPHONE_67, the old set lingers. Clean it up or ASC shows mixed sizes.
Store metadata as files so they're reviewable in git (without the raw binaries — .gitignore marketing/):
marketing/
├── asc-metadata/
│ ├── shared.json ← URLs, copyright, review info, category, release type
│ └── <locale>/
│ ├── name.txt
│ ├── subtitle.txt
│ ├── description.txt
│ ├── keywords.txt
│ ├── promotional_text.txt
│ └── release_notes.txt
└── asc-screenshots/
└── <locale>/<display-slug>/*.png
This separates content (editable by marketing/copywriters) from the sync logic (editable by engineers). Copywriters PR their .txt changes; engineers re-run the sync.
When you get 401 NOT_AUTHORIZED with a correctly formed JWT:
pip show pyjwt. Anything <2.0 emits DER format → rejected. Upgrade or switch to cryptography direct signingAPP_STORE_API_ISSUER exactly.p8 file integrity — openssl ec -in AuthKey_XXX.p8 -noout should succeed; ECDSA P-256 keydate -u vs curl -sI https://api.appstoreconnect.apple.com/ | grep -i date:. Apple tolerates ~5 min drift.p8, update env. Cheapest way to rule out stale/mismatched key materialasc CLI vs REST API| Task | Tool |
|---|---|
| Upload build from CLI | xcrun altool or asc CLI |
| TestFlight group/tester management | asc CLI |
| App metadata (name, description, keywords) sync | REST API (Python client) |
| Screenshot upload | REST API (CLI support is flaky) |
| Subscription product CRUD | Either (REST is more reliable for bulk ops) |
| Review submission | REST API via reviewSubmissions endpoint |
| Pricing tier updates | REST API |
which <tool> before npm install -g$revenue for PostHog, AFEventParamRevenue for AppsFlyer, etc.