@terreno/agent-plugin 0.0.0-stage → 57.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/CHANGELOG.md +2124 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -2
  5. package/agents/pre-commit.md +65 -0
  6. package/agents/ui-verifier.md +93 -0
  7. package/package.json +32 -4
  8. package/references/async-review-bots.md +76 -0
  9. package/references/documentation-contract.md +61 -0
  10. package/references/execution-state.schema.json +77 -0
  11. package/references/github-attention-contract.md +165 -0
  12. package/references/independent-review.md +31 -0
  13. package/references/lifecycle-contract.md +211 -0
  14. package/references/loop-engineering.md +125 -0
  15. package/references/pick-roast-loop.md +112 -0
  16. package/references/pr-deployments.md +69 -0
  17. package/references/product-ci.md +164 -0
  18. package/references/push-ownership.md +91 -0
  19. package/references/stage-result.schema.json +82 -0
  20. package/references/subagent-briefing.md +43 -0
  21. package/references/testing-plan.md +163 -0
  22. package/references/user-guide.md +171 -0
  23. package/skills/1-grow/SKILL.md +145 -0
  24. package/skills/1-grow/references/grilling.md +243 -0
  25. package/skills/2-pick/SKILL.md +170 -0
  26. package/skills/2-pick/references/mocking.md +34 -0
  27. package/skills/2-pick/references/testing.md +43 -0
  28. package/skills/3-roast/SKILL.md +168 -0
  29. package/skills/4-brew/SKILL.md +201 -0
  30. package/skills/5-taste/SKILL.md +311 -0
  31. package/skills/ai-prompt-governance/SKILL.md +100 -0
  32. package/skills/attach-pr-media/SKILL.md +89 -0
  33. package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
  34. package/skills/backend-test-env/SKILL.md +64 -0
  35. package/skills/backfill-regression/SKILL.md +101 -0
  36. package/skills/backfill-user-guide/SKILL.md +96 -0
  37. package/skills/bootstrap-mcp-server/SKILL.md +244 -0
  38. package/skills/building-admin-interfaces/SKILL.md +83 -0
  39. package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
  40. package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
  41. package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +280 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +102 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +91 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
  48. package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
  49. package/skills/deploy-gcp/SKILL.md +73 -0
  50. package/skills/deploy-gcp/references/troubleshooting.md +29 -0
  51. package/skills/generate-release-announcements/SKILL.md +145 -0
  52. package/skills/generate-sdk/SKILL.md +156 -0
  53. package/skills/mongoose-schema-safety/SKILL.md +164 -0
  54. package/skills/pick-roast-loop/SKILL.md +152 -0
  55. package/skills/planning-loop/SKILL.md +161 -0
  56. package/skills/prd/SKILL.md +138 -0
  57. package/skills/set-up-social-login/SKILL.md +76 -0
  58. package/skills/store-assets/SKILL.md +221 -0
  59. package/skills/store-assets/examples/screenshots.yaml +31 -0
  60. package/skills/store-assets/examples/storeAssets.config.json +44 -0
  61. package/skills/store-assets/scripts/banner.ts +165 -0
  62. package/skills/store-assets/scripts/config.ts +255 -0
  63. package/skills/store-assets/scripts/devClient.ts +635 -0
  64. package/skills/store-assets/scripts/devices.ts +286 -0
  65. package/skills/store-assets/scripts/icon.ts +81 -0
  66. package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
  67. package/skills/store-assets/scripts/maestro/login.yaml +27 -0
  68. package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
  69. package/skills/store-assets/scripts/screenshots.ts +848 -0
  70. package/skills/taste-sweep/SKILL.md +172 -0
  71. package/skills/terreno-backend-api/SKILL.md +355 -0
  72. package/skills/terreno-backend-api/agents/openai.yaml +4 -0
  73. package/skills/terreno-backend-api/references/custom-routes.md +132 -0
  74. package/skills/terreno-backend-api/references/model-router.md +116 -0
  75. package/skills/terreno-backend-api/references/server-setup.md +116 -0
  76. package/skills/terreno-backend-tests/SKILL.md +176 -0
  77. package/skills/terreno-data-fetching/SKILL.md +276 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +45 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
  82. package/skills/terreno-frontend-tests/SKILL.md +190 -0
  83. package/skills/terreno-mobile-tests/SKILL.md +127 -0
  84. package/skills/terreno-test-setup/SKILL.md +81 -0
  85. package/skills/terreno-test-setup/references/backend.md +131 -0
  86. package/skills/terreno-test-setup/references/frontend.md +169 -0
  87. package/skills/terreno-test-setup/references/mobile.md +134 -0
  88. package/skills/terreno-ui/SKILL.md +252 -0
  89. package/skills/terreno-ui/agents/openai.yaml +4 -0
  90. package/skills/terreno-ui/references/data-display.md +85 -0
  91. package/skills/terreno-ui/references/feedback.md +122 -0
  92. package/skills/terreno-ui/references/forms.md +119 -0
  93. package/skills/terreno-ui/references/layout.md +124 -0
  94. package/skills/terreno-ui/references/theming.md +87 -0
  95. package/skills/update-docs/SKILL.md +82 -0
  96. package/skills/upgrading-terreno/SKILL.md +86 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: set-up-social-login
3
+ description: >-
4
+ Set up Google, GitHub, and Sign in with Apple in a Terreno app on Better Auth: server config,
5
+ callback URLs for local dev, previews, and the stable deploy, provider registration, secrets,
6
+ trying the providers on one preview first, choosing same-site hosts, and checking the result.
7
+ Use when asked to "add Google/Apple/GitHub login", "set up social login" or OAuth, "register
8
+ the OAuth callbacks", "point OAuth at the preview", or when a social sign-in fails with a
9
+ redirect mismatch. Not for email/password, passkey, or 2FA setup alone (see
10
+ configure-better-auth.md).
11
+ targets: ['*']
12
+ ---
13
+
14
+ # Set up social login
15
+
16
+ Read the published guide first:
17
+ [Set up social login](https://terreno-docs.netlify.app/how-to/set-up-social-login).
18
+ Server fields are in
19
+ [Configure Better Auth](https://terreno-docs.netlify.app/how-to/configure-better-auth#auth-features).
20
+ This skill is the order of work. The guide's example deploy is one illustration; a consumer app
21
+ uses its own hosts, secret store, and deploy.
22
+
23
+ **Never print, commit, or paste a client secret or `.p8` key.** The user enters them into the
24
+ provider consoles and `gcloud secrets versions add` themselves. Never create secrets or change a
25
+ CI context without naming each one and getting a yes.
26
+
27
+ ## Steps
28
+
29
+ 1. **Find the hosts.** From the app's deploy config, write down:
30
+ - the stable backend and web URLs, and whether they share a registrable domain;
31
+ - the preview backend and web URL patterns;
32
+ - which backend owns the callbacks: stable, or one named preview.
33
+ Recommend `app.<domain>` and `api.<domain>` for stable, and previews on their hosting domains,
34
+ never under the stable domain (the guide's "Choose the hosts" says why). The example's hosts
35
+ are in that guide, not in this skill.
36
+ 2. **Configure the server.** Set `googleOAuth`, `githubOAuth`, and `appleOAuth` (key form) only
37
+ when their env vars exist; add `https://appleid.apple.com` to `trustedOrigins` with Apple; set
38
+ `oauthProxy.productionURL` on previews (to the callback owner) and on the owner (to itself);
39
+ share one `BETTER_AUTH_SECRET`. Follow the guide's server section. The login and security
40
+ screens need no change: they read `GET {basePath}/terreno/methods`.
41
+ 3. **Give the user the exact URLs.** For each provider, the callback is
42
+ `https://<callback owner>/api/auth/callback/<provider>`; local dev uses
43
+ `http://localhost:4000` for Google and GitHub (Apple is https only). Print the full URL per
44
+ provider, plus Apple's Domains (backend and web hosts) and GitHub's Homepage URL. Never leave
45
+ `<placeholders>` in what the user copies.
46
+ 4. **Hand over provider registration** as numbered steps from the guide's "Register each
47
+ provider": the Google consent screen (test users while in Testing), the GitHub OAuth app, and
48
+ the Apple Services ID plus key.
49
+ 5. **Hand over the secrets.** Check `gcloud config get-value project` first; if it is not the
50
+ project that holds the backend secrets, give `gcloud config set project <gcp-project-id>`.
51
+ Then give the steps from the guide's "Store the secrets", with this app's secret names and
52
+ its own secret store. No value passes through the chat. Secrets mount at deploy, so a
53
+ redeploy follows. The example's Secret Manager commands stay in the guide.
54
+ 6. **Preview first, when stable does not run this code yet.** Point the callback owner at one
55
+ preview, register that preview's backend URL with each provider, add the secrets, and deploy
56
+ that preview only. Say plainly that the named preview then holds the real secrets and stable
57
+ has no social sign-in until the callback owner moves back. The example does this with
58
+ `OAUTH_CALLBACK_TAG`; other apps use their own switch.
59
+ 7. **Check.** `curl -s https://<backend>/api/auth/terreno/methods | jq .` lists each provider.
60
+ Then sign in with each one on the web app. On `redirect_uri_mismatch`, compare the URI in the
61
+ error with the registered one character for character.
62
+
63
+ ## In code you change
64
+
65
+ - Add a preview-callback switch only when the user wants preview-first, and cover it with that
66
+ app's deploy-env tests. The example's switch is `OAUTH_CALLBACK_TAG`, described in the guide.
67
+ - Update the app's social-login docs in the same change.
68
+ - Prove the flow without real providers with the app's mock OIDC provider when it has one.
69
+ Real-provider sign-in is the user's check.
70
+
71
+ ## Done when
72
+
73
+ - `/terreno/methods` on the target backend lists every provider the user asked for.
74
+ - The user has signed in once with each provider, or you have reported which one is blocked and
75
+ why (for example, Apple refusing a `run.app` host).
76
+ - The guide matches what you set up, including any new host or variable.
@@ -0,0 +1,221 @@
1
+ ---
2
+ name: store-assets
3
+ description: >-
4
+ Produce App Store and Google Play listing assets for an Expo app: store-sized
5
+ screenshots (6.5-inch iPhone, 13-inch iPad, 1080x1920 Android phone) captured
6
+ by Maestro flows that log in with test accounts, the Play feature graphic
7
+ (1024x500 banner), and the Play high-res icon (512x512). Also keeps a
8
+ simulator/emulator Expo dev client in sync with the native fingerprint
9
+ (checks EAS, downloads a matching build, or starts one). Use when the user
10
+ asks for "store screenshots", "App Store / Play Store screenshots", "store
11
+ listing assets", "feature graphic", "store banner", "Play Store icon", "512
12
+ icon", or to launch an app in the simulator/emulator with an up-to-date dev
13
+ client.
14
+ ---
15
+ # Store Assets
16
+
17
+ One pipeline for store listing assets in a Terreno/Expo repo. Each step is a `bun run` script
18
+ backed by the files in this skill's `scripts/` folder. Every repo-specific value lives in
19
+ `storeAssets.config.json`.
20
+
21
+ | Asset | Command | Output |
22
+ |---|---|---|
23
+ | Dev client on a simulator/emulator | `bun run devclient:ensure <app> <ios\|android>` | Installed app; `devclient:open` launches it into Metro |
24
+ | Store screenshots | `bun run screenshots <app>` | `<appDir>/storeScreenshots/<device>/NN-name.png` |
25
+ | Play feature graphic | `bun run store:banner <app>` | `<appDir>/storeScreenshots/playFeatureGraphic.png` (1024×500, no alpha) |
26
+ | Play high-res icon | `bun run store:icon <app>` | `<appDir>/storeScreenshots/playIcon.png` (512×512) |
27
+
28
+ **Requirements:**
29
+ - **macOS:** the scripts use Xcode simulators and `sips` to check and convert images (Android screenshots included).
30
+ - **Android:** the Android SDK, with at least one existing AVD to clone.
31
+ - **Tools:** Java 17, Bun, and an Expo login (`bunx eas-cli login`, or `EXPO_TOKEN`).
32
+ - **App:** the app must read its API base URL from `EXPO_PUBLIC_API_URL`. The screenshot run sets it to the local backend.
33
+
34
+ ## One-time setup in a repo
35
+
36
+ Skip any step that is already done. `package.json` already having the `devclient:*` scripts means the repo is set up.
37
+
38
+ ### 1. Copy the scripts
39
+
40
+ Copy `scripts/` from this skill into the repo as `scripts/storeAssets/` (keep `maestro/` inside it).
41
+
42
+ ### 2. Add the scripts and dev dependency
43
+
44
+ Add these to the root `package.json`:
45
+
46
+ ```json
47
+ "devclient:status": "bun scripts/storeAssets/devClient.ts status",
48
+ "devclient:download": "bun scripts/storeAssets/devClient.ts download",
49
+ "devclient:build": "bun scripts/storeAssets/devClient.ts build",
50
+ "devclient:install": "bun scripts/storeAssets/devClient.ts install",
51
+ "devclient:ensure": "bun scripts/storeAssets/devClient.ts ensure",
52
+ "devclient:open": "bun scripts/storeAssets/devClient.ts open",
53
+ "screenshots": "bun scripts/storeAssets/screenshots.ts run",
54
+ "screenshots:setup": "bun scripts/storeAssets/screenshots.ts setup",
55
+ "screenshots:devices": "bun scripts/storeAssets/screenshots.ts devices",
56
+ "screenshots:capture": "bun scripts/storeAssets/screenshots.ts capture",
57
+ "store:banner": "bun scripts/storeAssets/banner.ts",
58
+ "store:icon": "bun scripts/storeAssets/icon.ts"
59
+ ```
60
+
61
+ Also add `@playwright/test` as a root devDependency; the banner and icon render HTML with headless Chromium. Use the workspace catalog version if there is one, then run `bunx playwright install chromium --only-shell`.
62
+
63
+ ### 3. Git-ignore the work folder
64
+
65
+ Add `/.storeAssets/` to `.gitignore`. It holds Maestro artifacts and Metro logs. Keep `storeScreenshots/` tracked so the current store set lives with each app.
66
+
67
+ ### 4. Write `storeAssets.config.json`
68
+
69
+ Write it at the repo root, starting from `examples/storeAssets.config.json`. Every `<...>` value is a placeholder. Fill each one from the repo (design tokens, `app.json`, `package.json`, seed data); ask the user only for what you can't find. Never invent brand colors, fonts or copy.
70
+
71
+ | Key | Meaning |
72
+ |---|---|
73
+ | `api.port` | Where the local backend listens. Override per run with `STORE_ASSETS_API_PORT` |
74
+ | `api.probe` | `{method, path, body?}` for a route only this backend serves. A 404 means "another project's server". A POST login route with body `{}` (expect 400) works well |
75
+ | `api.startCommand` | Shown when the backend is down |
76
+ | `homeScreenTestId` | testID on the post-login landing screen's root (default `home-screen`) |
77
+ | `devClientProfiles` | `{ios, android}` eas.json profiles. Defaults: `development:simulator` (needs `"ios": {"simulator": true}` and `developmentClient: true`) and `development` (internal `developmentClient`, builds an APK). Add the profiles to each app's `eas.json` if missing |
78
+ | `apps.<key>.appDir` | App directory (`app.json` needs `expo.owner`, `expo.slug`, `ios.bundleIdentifier` and `android.package`) |
79
+ | `apps.<key>.metroPort` | Port from the app's `expo start --port` script |
80
+ | `apps.<key>.devices` | Any of `iphone`, `ipad`, `android` |
81
+ | `apps.<key>.account` | Seeded test login `{email, password}`. Override with `SCREENSHOT_EMAIL` / `SCREENSHOT_PASSWORD`, which you should prefer for any non-throwaway account |
82
+ | `apps.<key>.preferredIosSimulator` | Simulator `devclient:*` boots when none of that family is running, e.g. `"iPad (A16)"` for a tablet-first app |
83
+ | `apps.<key>.banner` | `{headline, body, capsules?, screenshots}`. `screenshots` holds two names from `storeScreenshots/android/`, front one first, so the app must list `android` in `devices` |
84
+ | `brand.surface`, `onSurface` | Banner/icon background and the color drawn on it |
85
+ | `brand.capsuleBackground`, `capsuleText`, `border` | Capsule chip colors and the screenshot border |
86
+ | `brand.headingFont`, `bodyFont`, `googleFontsQuery?` | Font families and the Google Fonts css2 query that loads them |
87
+ | `brand.logoSvg`, `logoRecolor?`, `logoHeight?` | Vector lockup, a `{fromColor: toColor}` map onto the surface, and its height in px (default 30) |
88
+ | `brand.iconSvg`, `iconPathCount?`, `iconViewBox?`, `iconRecolor?` | Either a mark-only SVG, or a logo SVG plus the count and viewBox of the leading `<path>`s that form the mark. `iconRecolor` lists colors to paint as `onSurface` |
89
+ | `brand.bannerDecorations?` | `[{src, style}]`: repo-relative images positioned with CSS |
90
+
91
+ ### 5. Write a screenshot flow per app
92
+
93
+ Write `<appDir>/maestro/screenshots.yaml`, starting from `examples/screenshots.yaml`. See *Writing flows* below.
94
+
95
+ ### 6. Install tools
96
+
97
+ ```bash
98
+ bun run screenshots:setup
99
+ ```
100
+
101
+ This installs the pinned, checksum-verified Maestro release into `~/.maestro`. It needs Java 17: if Java is missing, tell the user to run `brew install openjdk@17`.
102
+
103
+ ## Dev client
104
+
105
+ The dev client only needs rebuilding when native code changes. The script computes the native fingerprint with `eas fingerprint:generate`, which is the same hash EAS stamps on builds. It then checks for a dev client with that fingerprint, in this order:
106
+
107
+ 1. **`install`:** a complete download is cached in `~/.cache/store-assets/<repo>/devClients/`. Override the location with `DEV_CLIENT_CACHE_DIR`. Interrupted downloads are never reused.
108
+ 2. **`download`:** a finished EAS build matches. curl aborts stalled transfers and resumes; Android dev APKs run several hundred MB.
109
+ 3. **`wait`:** a matching EAS build is in progress.
110
+ 4. **`build`:** nothing matches, so a new EAS build is needed.
111
+
112
+ ```bash
113
+ bun run devclient:status <app> <ios|android> # prints the fingerprint and the next action
114
+ bun run devclient:ensure <app> <ios|android> [--wait] [--no-build] [--device <udid|avd>]
115
+ bun run devclient:build <app> <ios|android> [--wait] [--force]
116
+ cd <appDir> && bun run start # Metro, run_in_background
117
+ bun run devclient:open <app> <ios|android> [--device <udid|avd>]
118
+ ```
119
+
120
+ - **Exit codes:** `ensure` exits `3` while an EAS build is still running; re-run with `--wait` in the background (builds take 10–25 min). With `--no-build`, it exits `4` instead of starting a build.
121
+ - **Credits:** starting an EAS build uses Expo credits. When `action` is `build`, confirm with the user first unless they asked for a build.
122
+ - **Use `devclient:open`, not `expo start --ios`:** `expo start --ios` picks whichever simulator is booted, which may not have the client installed.
123
+ - **How `devclient:open` launches:** on iOS it passes the launcher's `--initialUrl` argument, which avoids the "Open in …?" prompt `simctl openurl` triggers. On Android it sets up `adb reverse` for Metro and the API.
124
+
125
+ ## Screenshots
126
+
127
+ ```bash
128
+ bun run screenshots <app> # every device the app lists
129
+ bun run screenshots <app> --device android # one device
130
+ bun run screenshots <app> --build # also start EAS builds for stale dev clients
131
+ ```
132
+
133
+ Run it with `run_in_background: true`. The backend must be up and seeded first.
134
+
135
+ - **Builds:** without `--build`, a device whose dev client needs a new EAS build is skipped and reported, so no Expo credits are spent. Pass `--build` only once the user has agreed to the cost.
136
+ - **Metro:** `--keep-metro` leaves the screenshot Metro running afterwards. Otherwise it stops on exit, error or Ctrl-C.
137
+
138
+ | Device key | Hardware | Size | Store slot |
139
+ |---|---|---|---|
140
+ | `iphone` | Simulator "Store Screenshots iPhone 6.5" (iPhone 14 Plus) | 1284×2778 | App Store Connect 6.5" |
141
+ | `ipad` | Simulator "Store Screenshots iPad 13" (iPad Pro 12.9" 6th gen) | 2048×2732 | App Store Connect 13" iPad |
142
+ | `android` | AVD `Store_Screenshots_Phone`, cloned from any existing AVD, 4 GB RAM | 1080×1920 | Play phone. Play rejects images longer than 2:1, so modern 20:9 Pixels don't qualify |
143
+
144
+ For each device, the script:
145
+
146
+ 1. Creates the device on first use and boots it.
147
+ 2. Sets a clean status bar. iOS uses a 9:41 override. Android uses demo mode with the mobile icon blocklisted (demo mode's mobile icon renders as a second Wi-Fi), notifications snoozed, and autofill off.
148
+ 3. Runs `devclient:ensure` for that device.
149
+ 4. Starts its own production-mode Metro (`--no-dev --minify`, so no LogBox) with `EXPO_PUBLIC_API_URL` pointing at the local API. It clears Metro's cache only when that URL changes.
150
+ 5. Pre-builds the bundle, because a cold build takes minutes and would trip Maestro's driver timeout.
151
+ 6. Hides the dev-menu intro, auto-open and floating gear button, then launches the app straight into Metro.
152
+ 7. Runs the flow, copies every `NN-*.png` into `storeScreenshots/`, and checks each size.
153
+
154
+ **Review every PNG (Read them) before reporting.** Look for:
155
+
156
+ - Empty states or spinners. Wait on a content testID, not the screen root.
157
+ - Error banners and toasts.
158
+ - Leftover system dialogs.
159
+ - Text clipped on iPad or Android.
160
+
161
+ Fix the flow and re-run rather than handing over bad shots.
162
+
163
+ ### Driving the app by hand
164
+
165
+ Use this when a flow fails or a screen needs special setup:
166
+
167
+ ```bash
168
+ bun run screenshots:devices <app> # boot devices, install client, print IDs
169
+ ~/.maestro/maestro/bin/maestro --device <udid|emulator-serial> hierarchy # find testIDs on screen
170
+ bun run screenshots:capture <app> <iphone|ipad|android> 07-some-screen # save the current screen
171
+ ```
172
+
173
+ Fold a manual sequence that works back into the flow so the next run reproduces it.
174
+
175
+ ### Writing flows
176
+
177
+ - **Opening the app:** start with `runFlow` on `scripts/storeAssets/maestro/openApp.yaml`. It waits for the app, logs in through the `@terreno/ui` LoginScreen testIDs (`login-screen-email-input`, `-password-input`, `-submit-button`) when needed, and dismisses iOS's "Save Password?" prompt. Give a custom login form the same testIDs.
178
+ - **Subflow paths:** Maestro resolves `runFlow` paths relative to the flow file. With `appDir` one level deep, use `../../scripts/storeAssets/maestro/...`. Add one `../` per extra level, e.g. `../../../` for `apps/mobile`.
179
+ - **Flow env:** the script passes `APP_ID`, `EMAIL`, `PASSWORD`, `HOME_SCREEN_ID`, `DEVICE` and `PLATFORM`.
180
+ - **Selectors:** select by `id:` (testID). Add missing testIDs to the components first, e.g. `tabBarButtonTestID: "tab-<name>"` on Expo Router tabs.
181
+ - **Waits:** after each navigation, use `extendedWaitUntil` on the destination, then `waitForAnimationToEnd`, then `takeScreenshot: NN-name`. Use only the bare name: Maestro 2.x rejects paths outside its output folder.
182
+ - **Going back:** from a stack screen, use `runFlow` on `goHome.yaml`. It presses back on Android and restarts the app on iOS. A cold relaunch on Android leaves the dev client blank.
183
+
184
+ ## Play feature graphic and icon
185
+
186
+ ```bash
187
+ bun run store:banner <app> # needs the app's android screenshots
188
+ bun run store:icon <app>
189
+ ```
190
+
191
+ - **Banner layout:** a flat brand surface, the recolored logo, the headline (heading font), the body (body font), capsule chips, `bannerDecorations`, and two Android screenshots on the right.
192
+ - **Banner format:** rendered to JPEG and converted, so the PNG has no alpha channel. Play rejects alpha on feature graphics.
193
+ - **Icon:** the brand mark at about 62% width on the surface color. The image is full-bleed with square corners and no shadow, because Play applies its own mask.
194
+
195
+ Read both images before reporting. If the copy or decorations need changing, edit `storeAssets.config.json`.
196
+
197
+ ## Environment variables
198
+
199
+ | Variable | Effect |
200
+ |---|---|
201
+ | `STORE_ASSETS_CONFIG` | Config path (default `storeAssets.config.json`) |
202
+ | `STORE_ASSETS_ROOT` | Repo root (default: current directory) |
203
+ | `STORE_ASSETS_API_PORT` | Overrides `api.port`, e.g. when another project owns it |
204
+ | `SCREENSHOT_EMAIL`, `SCREENSHOT_PASSWORD` | Override the app's test account |
205
+ | `DEV_CLIENT_CACHE_DIR` | Dev client download cache |
206
+
207
+ ## Common failure modes
208
+
209
+ | Symptom | Cause | Fix |
210
+ |---|---|---|
211
+ | `... does not serve <probe>` | Another project owns the API port | Run this backend on a free port and set `STORE_ASSETS_API_PORT`. Never stop the other project's server |
212
+ | Login fails with invalid credentials | Stale seed data | Re-seed the backend |
213
+ | `needs a new dev client build; skipping` | Native code changed since the last build | Confirm the cost, then re-run with `--build` (or `devclient:build`) |
214
+ | `Request for viewHierarchy failed, code: 500` | App still loading, or a stale Maestro driver | Kill leftover `maestro` processes and re-run |
215
+ | `Maestro Android driver did not start up in time` | First run on a fresh emulator | Already given 180s; re-run once |
216
+ | Emulator freezes or `adb` hangs | Host out of memory | Shut down unused simulators (`xcrun simctl shutdown all`) and re-run |
217
+ | `Reusing Metro already running` warning | A dev-mode Metro is on the app's port | Stop it; dev mode shows LogBox in shots |
218
+ | Blank screen after relaunch on Android | Dev client doesn't reload the last bundle on a cold start | Use `goHome.yaml`, not `launchApp` |
219
+ | `INSTALL_PARSE_FAILED_NOT_APK` | Corrupt cached APK from an older script | Delete `~/.cache/store-assets/<repo>/devClients/<app>/android` |
220
+ | `Executable doesn't exist ... chrome-headless-shell` | Playwright browser not installed for this version | `bunx playwright install chromium --only-shell` |
221
+ | `Simulator.app not found` warning | Xcode installed without Simulator.app | Harmless: simulators run headless |
@@ -0,0 +1,31 @@
1
+ appId: ${APP_ID}
2
+ ---
3
+ # Store screenshots for one app. Copy to <appDir>/maestro/screenshots.yaml and replace the
4
+ # screens. Subflow paths assume <appDir> is one level deep; add a ../ per extra level. Run with
5
+ # `bun run screenshots <app>`; the script supplies APP_ID, EMAIL, PASSWORD,
6
+ # HOME_SCREEN_ID and collects every NN-name.png.
7
+ - runFlow: ../../scripts/storeAssets/maestro/openApp.yaml
8
+ - waitForAnimationToEnd
9
+ - takeScreenshot: 01-home
10
+
11
+ # Tab screen: tap the tab's testID, wait on the destination (prefer a content testID over the
12
+ # screen root so data has loaded), then capture.
13
+ - tapOn:
14
+ id: "tab-schedule"
15
+ - extendedWaitUntil:
16
+ visible:
17
+ id: "schedule-screen"
18
+ timeout: 20000
19
+ - waitForAnimationToEnd
20
+ - takeScreenshot: 02-schedule
21
+
22
+ # Stack screen opened from home, then back to home for the next one.
23
+ - tapOn:
24
+ id: "header-action-profile"
25
+ - extendedWaitUntil:
26
+ visible:
27
+ id: "profile-screen"
28
+ timeout: 20000
29
+ - waitForAnimationToEnd
30
+ - takeScreenshot: 03-profile
31
+ - runFlow: ../../scripts/storeAssets/maestro/goHome.yaml
@@ -0,0 +1,44 @@
1
+ {
2
+ "api": {
3
+ "port": 4000,
4
+ "probe": {"method": "POST", "path": "/auth/login", "body": "{}"},
5
+ "startCommand": "cd backend && bun run dev"
6
+ },
7
+ "homeScreenTestId": "home-screen",
8
+ "devClientProfiles": {"ios": "development:simulator", "android": "development"},
9
+ "brand": {
10
+ "surface": "<primary color token, e.g. #RRGGBB>",
11
+ "onSurface": "<text color on primary, e.g. #FFFFFF>",
12
+ "capsuleBackground": "<light neutral token>",
13
+ "capsuleText": "<dark text token>",
14
+ "border": "<default border token>",
15
+ "headingFont": "<brand heading font family>",
16
+ "bodyFont": "<brand body font family>",
17
+ "googleFontsQuery": "family=<Heading+Font>:wght@700;900&family=<Body+Font>:wght@400;700",
18
+ "logoSvg": "<appDir>/assets/images/<logo>.svg",
19
+ "logoRecolor": {"<logo color>": "<onSurface>", "<logo cutout color>": "<surface>"},
20
+ "logoHeight": 30,
21
+ "iconSvg": "<appDir>/assets/images/<logo or mark>.svg",
22
+ "iconPathCount": 4,
23
+ "iconViewBox": "0 0 64 64",
24
+ "iconRecolor": ["<mark color>"],
25
+ "bannerDecorations": [
26
+ {"src": "<optional decorative image>.png", "style": "width:72px;right:24px;top:20px"}
27
+ ]
28
+ },
29
+ "apps": {
30
+ "<appKey>": {
31
+ "appDir": "<appDir>",
32
+ "metroPort": 8081,
33
+ "devices": ["iphone", "ipad", "android"],
34
+ "account": {"email": "<seeded test user email>", "password": "<seeded test password>"},
35
+ "preferredIosSimulator": "iPhone 17",
36
+ "banner": {
37
+ "headline": "<short benefit-led headline>",
38
+ "body": "<one sentence on what the app does>",
39
+ "capsules": ["<Feature>", "<Feature>", "<Feature>"],
40
+ "screenshots": ["01-home.png", "02-<second screen>.png"]
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env bun
2
+ //
3
+ // Render the Google Play feature graphic (1024x500, no alpha) for an app: brand surface, logo,
4
+ // headline, body, capsules, decorations, and two of the app's Android store screenshots.
5
+ //
6
+ // Usage:
7
+ // bun run store:banner <app>
8
+ //
9
+ // Needs `brand` and the app's `banner` in storeAssets.config.json, plus
10
+ // <appDir>/storeScreenshots/android/ from `bun run screenshots`.
11
+ // Output: <appDir>/storeScreenshots/playFeatureGraphic.png
12
+
13
+ import {existsSync, rmSync} from "node:fs";
14
+ import {join, relative, resolve} from "node:path";
15
+ import {chromium} from "@playwright/test";
16
+ import {
17
+ fail as failTagged,
18
+ getApp,
19
+ loadConfig,
20
+ REPO_ROOT,
21
+ runOrFail,
22
+ say as sayTagged,
23
+ } from "./config";
24
+
25
+ const TAG = "store:banner";
26
+ const WIDTH = 1024;
27
+ const HEIGHT = 500;
28
+ const MIME_TYPES: Record<string, string> = {
29
+ jpg: "image/jpeg",
30
+ png: "image/png",
31
+ svg: "image/svg+xml",
32
+ };
33
+
34
+ const say = (message: string): void => sayTagged({message, tag: TAG});
35
+ const fail = (message: string): never => failTagged({message, tag: TAG});
36
+
37
+ // Resolves a config path and keeps it inside the repo.
38
+ const resolveRepoPath = ({path}: {path: string}): string => {
39
+ const absolute = resolve(REPO_ROOT, path);
40
+ if (relative(REPO_ROOT, absolute).startsWith("..")) {
41
+ return fail(`${path} is outside the repo`);
42
+ }
43
+ if (!existsSync(absolute)) {
44
+ return fail(`Missing ${absolute}`);
45
+ }
46
+ return absolute;
47
+ };
48
+
49
+ const dataUri = async ({path}: {path: string}): Promise<string> => {
50
+ const absolute = resolveRepoPath({path});
51
+ const mime = MIME_TYPES[absolute.split(".").pop() ?? ""] ?? "application/octet-stream";
52
+ const bytes = await Bun.file(absolute).arrayBuffer();
53
+ return `data:${mime};base64,${Buffer.from(bytes).toString("base64")}`;
54
+ };
55
+
56
+ const recolorSvg = ({svg, recolor}: {svg: string; recolor?: Record<string, string>}): string =>
57
+ Object.entries(recolor ?? {}).reduce(
58
+ (result, [from, to]) => result.replaceAll(`"${from}"`, `"${to}"`),
59
+ svg
60
+ );
61
+
62
+ const escapeHtml = (text: string): string =>
63
+ text
64
+ .replaceAll("&", "&amp;")
65
+ .replaceAll("<", "&lt;")
66
+ .replaceAll(">", "&gt;")
67
+ .replaceAll('"', "&quot;");
68
+
69
+ const main = async (): Promise<void> => {
70
+ const config = await loadConfig({tag: TAG});
71
+ const appKey = process.argv[2];
72
+ const app = getApp({appKey, config, tag: TAG});
73
+ const brand = config.brand;
74
+ if (!brand) {
75
+ fail("storeAssets.config.json has no `brand` section");
76
+ return;
77
+ }
78
+ const banner = app.banner;
79
+ if (!banner) {
80
+ fail(`App "${appKey}" has no \`banner\` section`);
81
+ return;
82
+ }
83
+ const [frontName, backName] = banner.screenshots;
84
+ if (!frontName || !backName) {
85
+ fail(`apps.${appKey}.banner.screenshots needs two names from storeScreenshots/android`);
86
+ return;
87
+ }
88
+
89
+ const shotDir = join(app.appDir, "storeScreenshots", "android");
90
+ const front = await dataUri({path: join(shotDir, frontName)});
91
+ const back = await dataUri({path: join(shotDir, backName)});
92
+ // Recolor the vector logo onto the surface; raster lockups rarely match the token exactly.
93
+ const logoSvg = recolorSvg({
94
+ recolor: brand.logoRecolor,
95
+ svg: await Bun.file(resolveRepoPath({path: brand.logoSvg})).text(),
96
+ });
97
+ const logo = `data:image/svg+xml;base64,${Buffer.from(logoSvg).toString("base64")}`;
98
+ const decorations = await Promise.all(
99
+ (brand.bannerDecorations ?? []).map(
100
+ async ({src, style}) =>
101
+ `<img style="position:absolute;${escapeHtml(style)}" src="${await dataUri({path: src})}" alt="">`
102
+ )
103
+ );
104
+ const capsules = (banner.capsules ?? [])
105
+ .map((label) => `<span class="capsule">${escapeHtml(label)}</span>`)
106
+ .join("");
107
+ const fontsLink = brand.googleFontsQuery
108
+ ? `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?${escapeHtml(brand.googleFontsQuery)}&display=swap">`
109
+ : "";
110
+
111
+ const html = `<!doctype html>
112
+ <html><head><meta charset="utf-8">${fontsLink}
113
+ <style>
114
+ * { box-sizing: border-box; margin: 0; padding: 0; }
115
+ body { width: ${WIDTH}px; height: ${HEIGHT}px; overflow: hidden; background: ${brand.surface}; position: relative; }
116
+ .copy { position: absolute; left: 64px; top: 60px; width: 470px; }
117
+ .logo { height: ${brand.logoHeight ?? 30}px; display: block; }
118
+ h1 { font-family: "${brand.headingFont}", serif; font-weight: 900; font-size: 40px; line-height: 1.2; color: ${brand.onSurface}; margin-top: 36px; }
119
+ p { font-family: "${brand.bodyFont}", sans-serif; font-size: 20px; line-height: 1.5; color: ${brand.onSurface}; margin-top: 16px; }
120
+ .capsules { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 24px; }
121
+ .capsule { font-family: "${brand.bodyFont}", sans-serif; font-weight: 700; font-size: 14px; color: ${brand.capsuleText}; background: ${brand.capsuleBackground}; border-radius: 360px; padding: 6px 16px; }
122
+ .shot { position: absolute; width: 230px; border-radius: 16px; box-shadow: 0 8px 24px rgba(31, 34, 40, .24); border: 1px solid ${brand.border}; }
123
+ .shot.back { left: 760px; top: 96px; }
124
+ .shot.front { left: 580px; top: 48px; }
125
+ </style></head>
126
+ <body>
127
+ <div class="copy">
128
+ <img class="logo" src="${logo}" alt="">
129
+ <h1>${escapeHtml(banner.headline)}</h1>
130
+ <p>${escapeHtml(banner.body)}</p>
131
+ <div class="capsules">${capsules}</div>
132
+ </div>
133
+ <img class="shot back" src="${back}" alt="">
134
+ <img class="shot front" src="${front}" alt="">
135
+ ${decorations.join("\n ")}
136
+ </body></html>`;
137
+
138
+ const outPath = join(REPO_ROOT, app.appDir, "storeScreenshots", "playFeatureGraphic.png");
139
+ const jpegPath = `${outPath}.jpg`;
140
+ const browser = await chromium.launch();
141
+ try {
142
+ const page = await browser.newPage({
143
+ deviceScaleFactor: 1,
144
+ viewport: {height: HEIGHT, width: WIDTH},
145
+ });
146
+ await page.setContent(html, {waitUntil: "networkidle"});
147
+ await page.evaluate(async () => {
148
+ await document.fonts.ready;
149
+ });
150
+ // Render to JPEG first so the PNG we keep has no alpha channel, which Play rejects.
151
+ await Bun.write(jpegPath, await page.screenshot({quality: 100, type: "jpeg"}));
152
+ } finally {
153
+ await browser.close();
154
+ }
155
+ // The temporary JPEG goes away even if the conversion fails.
156
+ process.on("exit", () => rmSync(jpegPath, {force: true}));
157
+ await runOrFail({
158
+ cmd: ["sips", "-s", "format", "png", jpegPath, "--out", outPath],
159
+ isQuiet: true,
160
+ tag: TAG,
161
+ });
162
+ say(`Saved ${outPath} (${WIDTH}x${HEIGHT})`);
163
+ };
164
+
165
+ await main();