End-to-end runbook for marketing screenshots that ship to the iOS App Store and Google Play. The skill does not hard-code app identity — it discovers the deep-link scheme + iOS bundle ID + Android package from the project's Expo config, and takes everything else as parameters.
| Platform | Needed |
|---|---|
| -------- | ----------------------------------------------- |
| iOS | Xcode CLI (xcrun simctl) |
| Android | Android Platform Tools (adb) |
| Resize | ImageMagick 7+ (magick) |
| Detect | jq and (optional) npx expo for app.config.{ts,js} projects |
| Upload | Python 3.9+ with requests, pyjwt[crypto] (App Store) and google-auth (Play) |
screenshots/<locale>/<device>/NN-<device>-<screen>.png
: BCP-47 tag — en-US, zh-CN, ja-JP, …: iphone, ipad, android-phone, android-tabletNN: zero-padded ordinal so files sort the same in Finder and the store back-office: kebab-case slug for the page (sign-in, home, settings, …)| Device | Required size | Notes |
|---|---|---|
| --------------- | ------------- | ---------------------------------------------------------------------------------------- |
iphone | 1284×2778 | App Store 6.5" display. Capture on iPhone 16 Pro Max sim (1320×2868) and resize. |
ipad | 2064×2752 | App Store 13" display. iPad Pro 13" M4 captures natively at this size. |
android-phone | 1440×3120 | Google Play phone (9:19.5). Pixel 7+/8+/9 Pro class captures natively. |
For other targets, look up the current Apple / Google specs and pass the size through to assets/resize.sh.
assets/)| Script | Purpose |
|---|---|
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
assets/detect-app-config.sh | Discover APP_SCHEME, IOS_BUNDLE_ID, ANDROID_PACKAGE from the Expo config. |
assets/detect-routes.sh | Walk an Expo Router app/ (or src/app/) tree and print every route as . |
assets/ios-status-bar.sh | Lock / clear the iOS Simulator status bar (9:41, charged, full bars). |
assets/ios-capture.sh | One screenshot: openurl → settle → simctl io screenshot. |
assets/android-status-bar.sh | Enter / exit Android system UI demo mode (clean clock, battery, signal). |
assets/android-capture.sh | One screenshot: am start deep link → settle → screencap + adb pull. |
assets/resize.sh | Batch resize a directory of PNGs to a target WxH, idempotent. |
assets/write-summary.sh | Write summary.md into a device folder (model, OS, resolution, screen list). |
assets/upload-app-store.py | Upload one (locale, device) folder to App Store Connect via the API. |
assets/upload-play-store.py | Upload one (locale, image-type) folder to Google Play via the Publisher API. |
Each script accepts -h-style usage on bad input. Read the file headers for full arg lists.
Capture runs in three phases: phase 1 takes the unauth screens (sign-in, sign-up, etc.) while the demo account is signed out, then you manually sign in across all devices, then phase 2 takes the auth-required screens. This avoids round-tripping through the sign-in flow during automation and keeps both states clean.
Script paths in the bash blocks below are written relative to the skill root (assets/...). Resolve them to wherever your agent installed the skill before running.
.app/.apk from weeks ago will either crash on missing native modules (NativeModule.X is null) or — worse — quietly render an old UI, and you won't notice until the screenshots ship. Cold-launch the app and visually confirm it matches today's source before capturing. If it doesn't, rebuild and reinstall (for Expo: pnpm --filter prebuild --clean && pnpm --filter ios && pnpm --filter android , or whatever your pipeline is). you're capturing (or rely on system locale if the app inherits it).adb targets emulators and physical phones identically; every script here works against a plugged-in Pixel/Galaxy/etc. just by running adb devices first. If multiple devices are attached, pass -s to the capture/status-bar scripts.xcrun simctl is simulator-only. For a real iPhone, the path is Xcode-driven (xcrun devicectl device install, Xcode for deep-link launch, xcrun devicectl device screenshot on Xcode 16+) and not wired into these scripts. Prefer rebuilding the dev client or installing a release .app to the simulator instead.```bash
eval "$(bash assets/detect-app-config.sh path/to/app)"
echo "$APP_SCHEME $ANDROID_PACKAGE"
```
If detection fails (custom config plugin, monorepo quirks), set the three env vars by hand.
NN slug deep-path rows. The NN ordering keeps both arrays disjoint so filenames sort correctly together. > For Expo Router projects only: rather than guessing deep-link paths from memory, dump every route under app/ (or src/app/) first so you don't miss anything the team added since the last screenshot pass:
> ```bash
> bash assets/detect-routes.sh path/to/app
> # TSV:
> ```
> Expo Router collapses (group) segments out of the user-visible URL — app/(auth)/sign-in.tsx deep-links as /sign-in. The (group) column is a useful auth-state hint ((auth), (app), (tabs) usually gate; (public), (onboarding) usually don't), but confirm against the matching _layout.tsx where redirect logic actually lives. For non-Expo-Router apps, read the project's own router config.
```bash
UNAUTH_SCREENS=(
"01 sign-in /sign-in"
"02 sign-up /sign-up"
)
AUTH_SCREENS=(
"03 home /"
"04 agent-plaza /agent"
"05 profile /user"
"06 settings /settings"
"07 agent-create /agent/create"
"08 product /product"
)
```
```bash
LOCALE=en-US
IPHONE_UDID=<...>; IPAD_UDID=<...> # xcrun simctl list devices to find them
bash assets/ios-status-bar.sh "$IPHONE_UDID"
bash assets/ios-status-bar.sh "$IPAD_UDID"
bash assets/android-status-bar.sh enter
capture_set() {
local udid="$1" device="$2" platform="$3"; shift 3
for row in "$@"; do
read -r nn slug path <<<"$row"
local out="screenshots/$LOCALE/$device/$nn-$device-$slug.png"
if [[ "$platform" == ios ]]; then
bash assets/ios-capture.sh "$udid" "$APP_SCHEME://$path" "$out"
else
bash assets/android-capture.sh "$APP_SCHEME://$path" "$out" "$ANDROID_PACKAGE"
fi
done
}
```
```bash
capture_set "$IPHONE_UDID" iphone ios "${UNAUTH_SCREENS[@]}"
capture_set "$IPAD_UDID" ipad ios "${UNAUTH_SCREENS[@]}"
capture_set "" android-phone android "${UNAUTH_SCREENS[@]}"
```
```bash
capture_set "$IPHONE_UDID" iphone ios "${AUTH_SCREENS[@]}"
capture_set "$IPAD_UDID" ipad ios "${AUTH_SCREENS[@]}"
capture_set "" android-phone android "${AUTH_SCREENS[@]}"
```
```bash
bash assets/resize.sh "screenshots/$LOCALE/iphone" 1284x2778
bash assets/resize.sh "screenshots/$LOCALE/android-phone" 1440x3120
identify "screenshots/$LOCALE/iphone"/*.png # expect 1284x2778
identify "screenshots/$LOCALE/ipad"/*.png # expect 2064x2752
identify "screenshots/$LOCALE/android-phone"/*.png # expect 1440x3120
bash assets/android-status-bar.sh exit # release demo mode
```
summary.md into each device folder so reviewers and future-you can tell at a glance which hardware/OS produced these and which screen each PNG corresponds to. Run after step 8 so the recorded resolution reflects the resized output.```bash
ALL_SCREENS=( "${UNAUTH_SCREENS[@]}" "${AUTH_SCREENS[@]}" )
bash assets/write-summary.sh ios "$IPHONE_UDID" "screenshots/$LOCALE/iphone" "$LOCALE" "${ALL_SCREENS[@]}"
bash assets/write-summary.sh ios "$IPAD_UDID" "screenshots/$LOCALE/ipad" "$LOCALE" "${ALL_SCREENS[@]}"
bash assets/write-summary.sh android - "screenshots/$LOCALE/android-phone" "$LOCALE" "${ALL_SCREENS[@]}"
```
The script auto-detects model + OS from simctl/adb and reads the resolution off any PNG already in the folder. Pass - for the Android target when only one device/emulator is attached, otherwise pass the serial.
Both upload scripts upload one (locale, device-or-image-type) directory per invocation. Re-running replaces the contents of that slot — pass --keep-existing to append instead. Loop in shell to cover multiple locales/devices.
upload-app-store.pyPre-reqs:
.p8, the Key ID, and the Issuer ID.zh-CN → zh-Hans, zh-TW → zh-Hant. Map before invoking.pip install 'pyjwt[crypto]' requests
export ASC_KEY_ID=ABC1234567
export ASC_ISSUER_ID=11111111-2222-3333-4444-555555555555
export ASC_KEY_PATH=$HOME/.appstoreconnect/AuthKey_ABC1234567.p8
UPLOAD=assets/upload-app-store.py
APP_ID=1234567890
# en-US (BCP-47 == ASC code), iPhone + iPad
python3 "$UPLOAD" --app-id "$APP_ID" --locale en-US --device iphone --dir screenshots/en-US/iphone
python3 "$UPLOAD" --app-id "$APP_ID" --locale en-US --device ipad --dir screenshots/en-US/ipad
# zh-CN folder → zh-Hans on App Store Connect
python3 "$UPLOAD" --app-id "$APP_ID" --locale zh-Hans --device iphone --dir screenshots/zh-CN/iphone
python3 "$UPLOAD" --app-id "$APP_ID" --locale zh-Hans --device ipad --dir screenshots/zh-CN/ipad
Default device → display-type mapping (override with --device iphone-65|iphone-67|iphone-69|ipad-129):
iphone → APP_IPHONE_67 (1284×2778 / 1290×2796)ipad → APP_IPAD_PRO_3GEN_129 (2064×2752 / 2048×2732)upload-play-store.pyPre-reqs:
pip install google-auth requests
export PLAY_CREDENTIALS=$HOME/.gcloud/play-service-account.json
UPLOAD=assets/upload-play-store.py
PKG=$ANDROID_PACKAGE # e.g. com.example.myapp (use detect-app-config.sh to populate)
python3 "$UPLOAD" --package "$PKG" --locale en-US --image-type phoneScreenshots --dir screenshots/en-US/android-phone
python3 "$UPLOAD" --package "$PKG" --locale zh-CN --image-type phoneScreenshots --dir screenshots/zh-CN/android-phone
Image-type values: phoneScreenshots, sevenInchScreenshots, tenInchScreenshots, tvScreenshots, wearScreenshots. Each slot caps at 8 images on Play; the script does not enforce that — the commit step will fail if you exceed it.
The script opens an edit, replaces the (locale, image-type) slot, then commits. If the commit fails, the edit is abandoned automatically by Play after a short TTL — re-run.
xcrun simctl openurl and adb shell am start ... -d both want a full URL. With Expo Router, paths nest under the scheme as :/// (note the triple slash — empty host).-s to android-status-bar.sh and android-capture.sh; both pass remaining args through to adb.adb exec-out screencap -p > file corrupts bytes on shells that translate CRLF. The capture script uses screencap to a remote path then adb pull, which is byte-safe.simctl status_bar booted targets whichever simulator is currently booted — convenient when only one sim is running.resize.sh skips files already at the target size, so re-running is cheap.mkdir -p screenshots//{iphone,ipad,android-phone} .LOCALE=."NN slug /path" to the SCREENS array.共 1 个版本