- `storebound console plan <form> [--json] [--lookup]`: read-only plan for an AI agent that drives the browser itself: start URL (deep links), ordered steps, answer sheet with the console's exact labels (certain / ask owner, curly quotes noted), never-press list, verified UI facts, driving guidance (in-page async loop + window.__log), verify instructions. All six forms. - `storebound console verify <form> [--from-text <file|->]`: App Privacy per-type summaries from page text, Play Create app from input states, content rating / target audience summaries, App Store record and age rating from the API; exit 1 on a mismatch. - MCP: console_plan, console_verify (read-only); console fill stays off MCP. - App Privacy (verified 2026-10-12): two information steps, unlabeled purpose checkboxes (nearest parent text), "Definitions and Examples" never pressed; fixture and engine follow. - Play Create app (verified 2026-10-12): /developers/<id>/create-new-app, App package name + read-only Check availability, material-radio inner [role=radio] click, app dashboard URL and play.appId, service account "Add app" as an owner step. - console-autofill skill rewritten agent-first; agents-core rule 10; README; pitfalls 12, 18 (confirmed sign-out), 19; CHANGELOG; play-first-release.
1027 lines
92 KiB
Markdown
1027 lines
92 KiB
Markdown
<p><img src="brand/logo-mark-256.png" width="96" height="96" alt="storebound logo"></p>
|
|
|
|
# storebound
|
|
|
|
**Website: [storebound.dev](https://storebound.dev) · npm: [storebound](https://www.npmjs.com/package/storebound) · `bunx storebound init`**
|
|
|
|
[](https://www.npmjs.com/package/storebound)
|
|
[](LICENSE)
|
|
[](CHANGELOG.md)
|
|
[](https://bun.sh)
|
|
[](package.json)
|
|
[](skills)
|
|
[](#mcp-server)
|
|
|
|
**Take a mobile app or game from nothing to "Submit for review" on Google Play and the App Store: app records,
|
|
listings in every language, in-app products and subscriptions with regional prices, every questionnaire, account
|
|
deletion and review access, privacy, icons, screenshots and previews, with one config and your AI coding agent.**
|
|
|
|
Hey, I'm CRTkafa. I built this while getting my own game through both stores and hit every wall on the way: draft
|
|
apps, edits that die when you touch the Console, the "first IAP must ship with a version" dance, Data safety CSVs that
|
|
silently need one more answer, an IARC questionnaire, target audience, App Privacy labels that are saved but not
|
|
published, screenshots in 7 sizes x 14 languages... I wrote each fix down as code or as a checklist. None of those
|
|
walls is game-only: a to-do app with accounts and a subscription hits the same ones, plus a few of its own (a demo
|
|
account for the reviewer, account deletion, subscription disclosures), so storebound covers apps and games alike. It
|
|
works for me, so I open-sourced it. Use it too.
|
|
|
|
No account, no sign-up, no telemetry. A small Bun CLI that talks to the official store APIs,
|
|
Agent Skills that walk your AI coding tool (or you) through the parts that are still console clicks, and an MCP server
|
|
so agents can call the CLI safely. **storebound never presses Submit, Release or Publish for you.**
|
|
|
|
Pick a profile and start from honest templates: `init --profile app` (utility / productivity app, optional account,
|
|
analytics, no ads; the default), `--profile subscription-app` (accounts + auto-renewable subscriptions) or
|
|
`--profile game` (free-to-play with ads and IAP).
|
|
|
|
It keeps going after launch: TestFlight groups and testers, crash and ANR vitals against the store thresholds, review
|
|
replies you approve before they go out, prices for many products and regions from one CSV, store analytics next to
|
|
your listing changes, and App Store product page tests from your screenshot variants.
|
|
|
|

|
|
|
|
## The journey
|
|
|
|
Everything an app or game needs before review, in the order I do it. For each step: what storebound does through the store
|
|
APIs (or on your machine), what the skill walks your agent through where the stores have no public API, and what stays
|
|
your own click.
|
|
|
|
- **storebound**: a CLI command (also an MCP tool). Every write has `--dry`; MCP write tools default to a dry run.
|
|
- **skill + agent (UI)**: no public API exists for it. The named skill has the exact navigation and the prepared
|
|
answers. An agent with browser tools (a Chrome extension, computer use, a Playwright MCP ...) can click through it
|
|
while you watch; without one, you click with the skill as your checklist.
|
|
- **you**: public, legal or money steps. The skills tell every agent to stop and hand these to you.
|
|
|
|
| # | step | storebound (API / local) | skill + agent (UI) | you |
|
|
|---|---|---|---|---|
|
|
| 1 | **Set up** | `init --profile app\|game\|subscription-app` (config, `store/*.md`, declarations, App Privacy and privacy templates per profile), `install --agent <tool>`, `check` | Play: service account + API access invite; App Store: API team key (`play-first-release`, `appstore-first-release`) | developer accounts; Paid Applications agreement, banking and tax; Play payments profile |
|
|
| 2 | **App records and bundle id** | `apple bundle` registers the bundle id, `apple status` finds the app record | App Store Connect > New App (no API); Play Console > Create app; the very first AAB upload when Play needs it from the Console | the declarations you accept when creating the Play app |
|
|
| 3 | **Builds and testers** | `play next-version-code`, `play build`, `play upload --track internal`, `apple signing ensure`, `apple build`, `apple upload` (waits for processing), `apple encryption`, `apple testflight groups / what-to-test / info / distribute` (internal groups) | Play internal testers list, Apple sandbox testers (`play-first-release`, `testflight`) | `apple testflight testers` (Apple emails the invitations), external groups and Beta App Review (`distribute --submit-review`) |
|
|
| 4 | **In-app products and subscriptions** | `play products` (regional prices from USD + your overrides, activated), `apple iap` (consumable, non-consumable, non-renewing; localizations, price schedule, availability), `apple iap-review`, `apple subscriptions` (groups, levels, equalized prices, intro and promotional offers, offer codes), `play subscriptions` (base plans, regional prices, offers), `catalog diff` (both stores against one catalog) | Play license testers, Apple sandbox testers, "Add for Review" of the first IAPs and subscriptions into the version's submission, Play promo codes (`store-iap-setup`, `store-subscriptions`) | custom offer codes (`apple subscriptions --codes` asks first) |
|
|
| 5 | **Store listings, every language** | `play listing` (title, short / full description, contact email + website, promo video URL), `apple listing --version x --build N` (name, subtitle, description, keywords, promo text, URLs, privacy URL, manual release), `listing translate` (drafts per language), `whats-new`, `check` (limits), `check --aso` | Play store settings: category and tags (`play-first-release`) | |
|
|
| 6 | **Play questionnaires** | `play data-safety`: the whole Data safety form from `dataSafety` in the config (CSV import through the API) | Policy and programs > App content, answers prepared in `store/declarations.md` (`store-declarations`): privacy policy, app access with the demo account (`review-access`), delete-account URL (`account-deletion`), ads, content rating (IARC), target audience and content, advertising ID, news, government, financial and health features; Data safety summary check | the answers themselves are your statements: the agent asks when the code does not prove them |
|
|
| 7 | **App Store questionnaires** | `apple appinfo` (categories, age rating declaration, content rights, Free price, availability), `apple encryption` (export compliance from `apple.usesNonExemptEncryption`, never guessed), `apple review-details` (contact, demo account from `.storebound/`, notes) | App Privacy nutrition labels from `store/app-privacy.md`, then **Publish** (`store-declarations`); encryption documentation when the answer is yes (`export-compliance`); the first availability record if the API refuses it | the export compliance answer (a legal statement, set once in the config) |
|
|
| 8 | **Privacy and accounts** | privacy policy page template (`init`), `privacy-manifest --ios <path>` (PrivacyInfo.xcprivacy, required-reason APIs, SDK manifests), `validate` checks the policy, delete-account and terms URLs answer | host the pages, link them in both consoles and inside the app; in-app account deletion (`privacy-policy`, `privacy-manifest`, `account-deletion`) | |
|
|
| 9 | **Icons** | `icons`: App Store 1024 without alpha, Play 512, adaptive + legacy Android launcher icons, web icons, Capacitor sources | | |
|
|
| 10 | **Screenshots and previews** | `shots capture / render / check / gallery`, `play images`, `apple screenshots`, `shots record / video`, `apple previews`, `apple ppo create / upload` (App Store product page tests from `shots` variants; see [Store art](#store-art)) | Play store listing experiments (optional, `store-screenshots`) | uploading the promo video to YouTube; `apple ppo start / stop` |
|
|
| 11 | **Validate** | `scan` (offline: rejection risks in the project files, see [Catch rejections before you submit](#catch-rejections-before-you-submit)), `check` (offline) and `validate` (read-only on both stores: every blocker in fix order, plus the local renders) | the UI-only items `validate` cannot see (`store-preflight` lists them) | |
|
|
| 12 | **Submit** | `play release --version-code N --production draft`, `apple status` | Play: countries / regions, open the production draft and save it (`play-first-release`) | **Play: Send changes for review. App Store: Submit for Review.** |
|
|
| 13 | **After review** | `apple status --wait-review`, `apple release --phased on / pause`, `play rollout --halt`, reply drafts (`review-rejections`) | | releasing the approved version, resuming or completing a phased release or rollout (terminal commands that ask first, `release-management`) |
|
|
| 14 | **Live app** | `health [--weekly]` (crash / ANR vitals vs Play's thresholds, App Store diagnostics; also in `validate`), `reviews list / draft`, `analytics snapshot / compare`, `prices export`, `prices import --dry`, `apple ppo status` | `store-health`, `review-replies`, `store-analytics`; Play statistics come from the reports bucket (no API) | `reviews send` (approved drafts only), `prices import` (asks first) |
|
|
|
|
New personal Play developer accounts also need a closed test with 12+ testers for 14 days before production; the
|
|
`play-first-release` skill covers it.
|
|
|
|
### API or console, honestly
|
|
|
|
- **Through the API** (storebound does it): bundle id, builds on tracks, versions and build selection, listings in
|
|
every language, graphics, screenshots, app previews, in-app products and prices (also from one CSV), Play Data
|
|
safety, Apple age rating, categories, content rights, price and availability, export compliance answer, App Review
|
|
details with the demo account, subscriptions (Apple groups, levels, prices, introductory and promotional offers,
|
|
offer codes; Play base plans, regional prices, offers), phased release and staged rollout, TestFlight groups / testers / What to Test / Test Information / build
|
|
processing / expiring builds, crash and ANR vitals and App Store diagnostics, TestFlight feedback, review replies,
|
|
App Store analytics reports, sales and finance reports, App Store Product Page Optimization tests, IAP and
|
|
subscription review submissions, offer codes for one-time purchases, win-back offers, promoted IAPs, pre-orders,
|
|
custom product pages and in-app events, and pulling live listing texts back.
|
|
- **Console only** (no public API; the skill + your agent's browser, or you): creating the App Store Connect app record
|
|
and the Play app, Play App content (IARC content rating, target audience, ads, advertising ID, app access,
|
|
government / financial / health / news, app access with the demo account), Play category and tags, Play countries,
|
|
App Privacy labels and their Publish, Play promo codes, Play pre-registration, Play store listing experiments, test results of both stores' experiments, promotional images of promoted IAPs.
|
|
Play store statistics have no API either: storebound reads the monthly exports from your Play reports bucket.
|
|
- **Only you**: agreements, banking and tax, and the final Send for review / Submit for Review (apps, IAPs, custom
|
|
product pages, in-app events). Releases and rollouts after approval, Beta App Review, tester invitations, sending
|
|
review replies, applying price changes, starting / stopping a product page test, creating offer codes, win-back
|
|
offers, switching on promoted IAPs, pre-orders and expiring builds too. None of these is an MCP tool; the terminal commands ask first.
|
|
|
|
## Catch rejections before you submit
|
|
|
|
App Store rejections cost days: you wait for review, read one sentence in the Resolution Center, fix it, and wait
|
|
again. On Google Play a policy problem can mean a blocked update, a removal or a strike on the account. Most of the
|
|
usual ones are visible in the project long before that. `storebound scan` reads the project itself, offline and
|
|
read-only (Xcode project, Info.plist and build settings, entitlements, PrivacyInfo.xcprivacy, Podfile.lock,
|
|
Package.resolved, Cartfile, Gradle files and version catalogs, AndroidManifest.xml (and the merged manifest of a local
|
|
build), res/xml and strings.xml, CMake / ndk-build files, Unity ProjectVersion.txt, package.json,
|
|
Expo app.json, capacitor.config, Cordova config.xml, pubspec, Unity Packages/manifest.json, ProjectSettings and
|
|
External Dependency Manager files, Godot export presets), plus your config and listing files. It works out the stack
|
|
and the SDKs (ads, analytics, attribution, crash, sign-in, payments, push, with versions and the file:line they come
|
|
from) and reports every rejection risk it can prove, each with the guideline, the evidence, the fix, a severity and the
|
|
skill to open. On the teaser fixture (`tests/fixtures/scan/teaser`):
|
|
|
|
```text
|
|
$ storebound scan
|
|
Rejection risks: 2 blocker, 3 high, 0 medium, 0 low
|
|
1. BLOCKER 2.1(a) · App Store · Sign-in is required but there is no demo account for App Review
|
|
at storebound.config.ts:12 reviewAccess: { signInRequired: true, instructions: 'Sign in with the demo account.' },
|
|
at storebound.config.ts:11 accounts: { enabled: true },
|
|
at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0
|
|
2. BLOCKER 5.1.1(v) · App Store · Accounts can be created but no in-app account deletion was found
|
|
at storebound.config.ts:11 accounts: { enabled: true },
|
|
at Planner/LoginView.swift:11 Button("Create account") { Task { try await AccountApi.signUp() } }
|
|
at Planner/LoginView.swift:17 static func signUp() async throws {
|
|
3. HIGH 4.8 · App Store · Third-party login (Google / Facebook / ...) without Sign in with Apple or another equivalent login option
|
|
at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0
|
|
at Planner/LoginView.swift:9 GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController()) { _, _ in }
|
|
4. HIGH 3.1.1 · App Store · Restorable purchases (non-consumables or subscriptions) but no Restore Purchases path was found
|
|
at storebound.config.ts:16 com.example.planner.pro (non_consumable)
|
|
5. HIGH 5.1.2(i) · App Store · Tracking-capable code without App Tracking Transparency (Google AdMob, IDFA)
|
|
at Podfile.lock:2 Google AdMob: Google-Mobile-Ads-SDK 11.10.0
|
|
at Planner/Ads.swift:7 print(ASIdentifierManager.shared().advertisingIdentifier)
|
|
```
|
|
|
|
Every finding also has a `fix:` line and the skill. Below the risks comes the declarations proposal: the Play Data
|
|
safety and App Store App Privacy answers, App Tracking Transparency and privacy manifest entries the detected SDKs and
|
|
code imply, each marked **detected (file:line)** or **ask owner**, as a diff against `dataSafety` / `appPrivacy` in
|
|
the config. Nothing is written unless you run `storebound scan --write` (only detected answers; questions stay
|
|
questions). The MCP tool `scan` is read-only and cannot write; `validate` includes the open scan findings; the desktop
|
|
app shows them on the Checks tab. A finding you decided is fine goes into the config with its reason:
|
|
`scan: { ignore: [{ rule: 'demo-account', reason: '...' }] }`; a rule with several checks (e.g. `play-metadata`) takes `match: '<text in the finding title>'` to accept just one of them.
|
|
|
|
| rule | App Store | Google Play | what scan proves |
|
|
|---|---|---|---|
|
|
| `demo-account`, `play-app-access` | 2.1(a) | App access | a sign-in (config, auth SDKs, sign-in code) without `reviewAccess` or without the demo account in `.storebound/` |
|
|
| `account-deletion`, `play-deletion-url` | 5.1.1(v) | User Data: account deletion | sign-up or social login without a deletion path in code or `accounts.inApp`; no `accounts.deletionUrl` |
|
|
| `login-services` | 4.8 | | Google / Facebook / other third-party login without Sign in with Apple or another equivalent option |
|
|
| `restore-purchases`, `external-purchase`, `play-payments` | 3.1.1 | Payments | non-consumables or subscriptions without a restore path; card / PayPal SDKs next to in-app products; "buy on our website" (Play: unless `play.billingPrograms` lists user choice billing or external offers) |
|
|
| `subscription-info` | 3.1.2(c), 3.1.2 | | no Terms of Use URL, no Terms link or renewal wording in the description, no legal links or "Manage subscription" in the app |
|
|
| `att` | 5.1.2(i) | | tracking-capable SDKs or IDFA without ATT; the prompt without its purpose string; the string without a prompt |
|
|
| `purpose-strings`, `unused-permission` | 5.1.1(ii), 5.1.1(iv), ITMS-90683 | | a used permission without its NS*UsageDescription; empty or vague strings; declared but unused |
|
|
| `privacy-manifest` | ITMS-91053 / 91061 | | no PrivacyInfo.xcprivacy, required-reason APIs not declared, SDKs older than their first manifest version |
|
|
| `privacy-policy-link`, `play-privacy-policy` | 5.1.1(i) | User Data: privacy policy | no privacy link in the app's sources (Play: every app; medium when it collects data), an `http://` link |
|
|
| `privacy-labels` | 5.1.2 | User Data: Data safety | SDK data types missing from `dataSafety`; Play also: an answer narrower than the SDK's disclosure (purposes, sharing), SDKs with no `dataSafety` at all |
|
|
| `play-prominent-disclosure` | | User Data: prominent disclosure, Location permissions | background location without a disclosure that says "location" and "background"; location, contacts, calendar or phone number access without any disclosure or rationale text in sources or `strings.xml` |
|
|
| `play-background-location`, `play-photo-video`, `play-all-files-access`, `play-accessibility`, `play-install-packages`, `play-health-connect`, `play-full-screen-intent`, `play-vpn` | | Location permissions, Photo and Video Permissions, All files access, AccessibilityService API, REQUEST_INSTALL_PACKAGES, Health Connect, Full-screen intent, VpnService | the restricted permission or service in the manifest (or the merged manifest of a local build), each with the core-use test and the Play Console declaration it needs; `isAccessibilityTool` lowers the accessibility finding to medium |
|
|
| `play-fgs`, `play-exact-alarm`, `play-query-all-packages`, `play-sms-call-log` | | Foreground service types, exact alarms, package visibility, SMS and Call Log | foreground services without a type or its permission (and the Console declaration); USE_EXACT_ALARM; QUERY_ALL_PACKAGES; SMS and call log permissions |
|
|
| `kids-ads`, `play-families-ads`, `play-families-ad-id`, `play-families-location` | 1.3 | Families | a Kids Category age band with third-party ads or analytics SDKs; `play.targetAges` under 13 with an ad SDK that is not Families self-certified (or older than its certified version), the AD_ID permission in a child-only app (mixed audience: ad / attribution SDKs need child-directed treatment), location in a child-only app |
|
|
| `play-ad-id` | | Advertising ID | targetSdk 33+ with ad / attribution SDKs and no AD_ID permission (medium when the merged manifest proves it is missing; Google Mobile Ads adds it itself) |
|
|
| `play-16kb` | | 16 KB page sizes (updates blocked from 2027-02-01) | Unity before 6000.0.38f1 / 2022.3.56f1 / 2021.3.48f1, React Native before 0.77, NDK before r28 for the app's own C/C++ without the 16 KB flags, AGP before 8.5.1 with uncompressed native libraries; else prebuilt `.so` files to check |
|
|
| `play-ads-launch` | | Ads, Better Ads Experiences | an interstitial shown from `onCreate` of the launcher activity (Android native; other engines: review-only) |
|
|
| `play-testing-requirement` | | Testing for new personal accounts | `play.accountType: 'personal'` created after 2023-11-13 (12 testers, 14 days of closed testing before production); low when the account type is not set |
|
|
| `other-platforms` | 2.3.10 | | "Android", "Google Play" and other stores in the App Store text or screenshot captions |
|
|
| `metadata-keywords`, `kids-terms`, `play-metadata` | 2.3.7, 2.3.8 | Metadata | other apps' names, prices and "best / #1" in name, subtitle or keywords; keyword lists; "For Kids" outside the Kids Category. Play title over 30 characters, emoji, ALL CAPS, rank / price / promotion terms; other apps in the title or short description; repeated keywords or keyword lists in the full description |
|
|
| `play-impersonation` | | Impersonation | a well-known brand in the package name or app name (low: a check, not a verdict) |
|
|
| `placeholder-text` | 2.1(a) | Metadata | lorem ipsum, TODO, template lines in the listings |
|
|
| `beta-wording` | 2.2 | | "beta", "demo", "trial version", "test build", "early access" in the app name (config, CFBundleDisplayName) or the App Store name, subtitle, keywords, promotional text and description ("demo account" is fine) |
|
|
| `originality` (with `--originality`) | 4.3 | Repetitive content | a near-duplicate of one of your own apps (`storebound originality` scores: name, listing text, icon and screenshot hashes, project structure) |
|
|
| `min-functionality` | 4.2 | | Capacitor `server.url` / Cordova `<content src>` on a remote site; a few native files around a remote WKWebView |
|
|
| `deprecated-api`, `private-api` | 2.5.1 (ITMS-90809) | | UIWebView; `App-Prefs:` / `prefs:root` URL schemes |
|
|
| `ipv6`, `ats` | 2.5.5, App Transport Security | | IPv4 literals in server URLs; NSAllowsArbitraryLoads |
|
|
| `export-compliance` | export compliance | | no ITSAppUsesNonExemptEncryption and no `apple.usesNonExemptEncryption`, or the two disagree (never guessed) |
|
|
| `review-prompt` | 5.6.1, 3.2.2(x) / 5.6.3 | | a custom "rate us" prompt without the system API; rewards for ratings |
|
|
| `play-target-sdk` | | Target API level | below API 36 (Wear OS / Automotive 35, TV / XR 34) for new apps and updates since 2026-08-31 |
|
|
| `play-package-name` | | Play Console upload | the reserved `com.example` prefix |
|
|
|
|
`storebound scan --originality [--against <folders>]` adds the Guideline 4.3 / Play repetitive content check against
|
|
your other apps to the same report (folders default to `originality.against` in the config; `storebound originality`
|
|
stays the command with every score and suggestion).
|
|
|
|
Rules cite Apple's App Review Guidelines and the Play Policy Center / Play Console Help pages they come from (checked
|
|
2026-10-11, in the code comments of `cli/lib/scan/rules.ts` and `cli/lib/scan/play-rules.ts`); the SDK table with each
|
|
vendor's data disclosure page and Google's Families self-certified ads SDK list is `cli/lib/scan/sdks.ts`. Play config
|
|
the rules read: `play.targetAges` (Target audience age groups), `play.accountType` / `play.accountCreated`, and
|
|
`play.billingPrograms`. When the project was built locally, scan also reads the merged manifest under
|
|
`build/intermediates` (the permissions libraries add); otherwise it says that it could not. Not a rule because no Play
|
|
policy page covers it: device admin apps (BIND_DEVICE_ADMIN); not a policy at all: Play Integrity / anti-tamper.
|
|
|
|
**Checked at review, not by scan** (no file shows them): crashes and hangs on the reviewer's device (2.1, `store-health`),
|
|
links that are dead at review time (`validate` checks the configured URLs), screenshots and descriptions that do not
|
|
match the app (2.3, `store-screenshots`), unfinished UI (4.0), "too simple" and spam / template apps (4.2, 4.3,
|
|
`portfolio-originality`), IAPs that fail in the sandbox (2.1(b), `store-iap-setup`), the content of the privacy policy
|
|
(`privacy-policy`), local law (5.2). On Google Play: whether a restricted permission really serves the app's core
|
|
purpose (the Console declaration and its video decide), the wording and timing of a prominent disclosure, ad
|
|
placements in Unity, Flutter or React Native code, the Health apps declaration answers, and the alignment inside a
|
|
prebuilt `.so` (scan lists them; `zipalign -c -P 16` and `check_elf_alignment.sh` check the built app). The
|
|
`review-rejections` skill covers what to do when one of these comes back.
|
|
|
|
## 5-minute quick start
|
|
|
|
You need [Bun](https://bun.sh) 1.1+ (and `ffmpeg` for icons, screenshots and video).
|
|
|
|
```sh
|
|
cd my-app
|
|
bun add -d storebound playwright # or: bun add -d git+https://git.crt.fyi/CRT/storebound.git playwright
|
|
bunx storebound init # --profile app (default) | subscription-app | game: config, store/*.md, declarations, privacy policy, .storebound/ (gitignored)
|
|
# fill in storebound.config.ts and the store/ files
|
|
bunx storebound check # offline: config, copy limits, Data safety answers, products, files
|
|
```
|
|
|
|
Add credentials (below), then walk the journey with dry runs first:
|
|
|
|
```sh
|
|
bunx storebound apple bundle --dry
|
|
bunx storebound play listing --dry
|
|
bunx storebound play products --dry && bunx storebound apple iap --dry
|
|
bunx storebound apple subscriptions --dry && bunx storebound play subscriptions --dry
|
|
bunx storebound apple review-details --dry # contact + demo account from .storebound/ (masked) + notes
|
|
bunx storebound play data-safety --dry --csv store/data-safety.csv
|
|
bunx storebound apple appinfo --dry
|
|
bunx storebound icons --dry
|
|
bunx storebound shots capture && bunx storebound shots render && bunx storebound shots gallery
|
|
bunx storebound validate # read-only: what still blocks a submission, per store, in fix order
|
|
bunx storebound catalog diff # read-only: products + subscriptions on both stores vs the config, prices per region
|
|
```
|
|
|
|
After launch:
|
|
|
|
```sh
|
|
bunx storebound apple testflight what-to-test --build 42 --dry # then distribute --build 42 --group Team
|
|
bunx storebound health --weekly # vitals + diagnostics, .storebound/health/<date>.md
|
|
bunx storebound reviews draft # reply drafts in .storebound/replies/, then reviews send
|
|
bunx storebound prices export && bunx storebound prices import --dry
|
|
bunx storebound analytics snapshot && bunx storebound analytics compare
|
|
```
|
|
|
|
Or ask your agent: "take my app to Google Play and the App Store", "fill in the store questionnaires", "set up the
|
|
in-app purchases and a yearly subscription with a free trial on both stores", "do both stores sell the same
|
|
catalog", "the reviewer could not sign in", "make store screenshots in 5 languages", "is my app ready to submit", "why was my
|
|
update rejected", "put build 42 on TestFlight", "how are our crash rates", "answer the new reviews", "did the new
|
|
screenshots help". The matching skill takes over.
|
|
|
|
### Coming from fastlane
|
|
|
|
`storebound import fastlane` turns an existing fastlane setup into storebound files. It only reads (no Ruby is run),
|
|
never overwrites anything and prints what it could not map:
|
|
|
|
```sh
|
|
bunx storebound import fastlane --dry # the plan + report; default --dir: fastlane/, ios/fastlane/, android/fastlane/
|
|
bunx storebound import fastlane # writes the files below; an existing file gets a <name>.imported.<ext> copy
|
|
bunx storebound init # adds the remaining templates, keeps the imported files
|
|
bunx storebound check
|
|
```
|
|
|
|
| fastlane | storebound |
|
|
|---|---|
|
|
| Appfile `app_identifier`, `package_name` | `apple.bundleId`, `play.packageName` |
|
|
| deliver `metadata/<locale>/*.txt` (+ `default/`) | `store/appstore.md` per App Store locale; URLs, copyright, categories and review notes in the config |
|
|
| supply `metadata/android/<lang>/*.txt`, newest `changelogs/<versionCode>.txt` | `store/listing.md` per Play language |
|
|
| deliver and supply language folders | `languages[]` with both store codes (`tr` + `tr-TR`, `zh-Hans` + `zh-CN`, `he` + `iw-IL`) |
|
|
| `screenshots/<locale>/`, `images/phoneScreenshots/`, `tenInchScreenshots/` | `store/shots/frames/<lang>/<phone\|tablet>/NN.png` with a `folder` capture source |
|
|
| supply `icon`, `featureGraphic` | `play.icon`, `store/renders/<lang>/feature/1024x500.*` |
|
|
|
|
Keys and accounts are never copied: `json_key_file`, the API key, team ids, the App Review contact and demo account are
|
|
named in the report with where they go (`.storebound/credentials.json`). Submission and release flags, `price_tier`,
|
|
the age rating config, lanes and signing (`match`) are listed with their storebound counterpart. The
|
|
`migrate-from-fastlane` skill walks an agent through it.
|
|
|
|
### Credentials (never in the repo)
|
|
|
|
Read from environment variables, or from `.storebound/credentials.json` in your project (gitignored by `init`):
|
|
|
|
| name | what |
|
|
|---|---|
|
|
| `PLAY_SERVICE_ACCOUNT` | path to the Google Cloud service account JSON key (or the JSON itself) with access to your app in Play Console |
|
|
| `ASC_KEY_ID`, `ASC_ISSUER_ID` | App Store Connect API team key (Users and Access > Integrations) |
|
|
| `ASC_P8` | path to `AuthKey_<id>.p8` (default `~/.appstoreconnect/private_keys/AuthKey_<id>.p8`) |
|
|
| `ASC_VENDOR_NUMBER` | vendor number for `reports apple-sales` / `apple-finance` (Payments and Financial Reports) |
|
|
| `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | upload key for `play build` (path to the .jks / .p12 and its passwords; handed to Gradle through the environment) |
|
|
| `reviewContact` | (file only) App Review contact name / phone / email |
|
|
| `reviewDemoAccount` / `REVIEW_DEMO_USER`, `REVIEW_DEMO_PASSWORD` | demo account for App Review (`reviewAccess.signInRequired`); never in the config or the repo, masked in logs |
|
|
|
|
Several store accounts on one machine: credential profiles (`~/.storebound/credentials/<profile>.json`, the same names
|
|
except the per-app upload keystore)
|
|
picked with `--account <profile>` or by a workspace ([Many apps and studios](#many-apps-and-studios)).
|
|
|
|
## Install for your AI tool
|
|
|
|
Every route gives the agent the same skills and the same MCP tools. Nothing here requires Claude: the skills are
|
|
plain Markdown and the CLI is a normal command.
|
|
|
|
| tool | install |
|
|
|---|---|
|
|
| Claude Code | `/plugin marketplace add https://git.crt.fyi/CRT/storebound.git` then `/plugin install storebound@crtkafa` (skills, MCP server, `storebound` on PATH). Or per project: `bunx storebound install --agent claude` |
|
|
| OpenAI Codex | `bunx storebound install --agent codex` (skills in `.agents/skills`, MCP in `.codex/config.toml`; Codex reads that file for trusted projects). The repo is also a Codex plugin (`.codex-plugin/plugin.json`, skills only) |
|
|
| Gemini CLI | `gemini extensions install https://git.crt.fyi/CRT/storebound.git` (skills, MCP server, `GEMINI.md`). Or `bunx storebound install --agent gemini` |
|
|
| Cursor | `bunx storebound install --agent cursor` (skills in `.agents/skills`, `.cursor/mcp.json`); the repo's `.cursor/rules/storebound.mdc` is an agent-requested rule you can copy |
|
|
| VS Code / GitHub Copilot | `bunx storebound install --agent copilot` (skills in `.github/skills`, `.vscode/mcp.json`) |
|
|
| Windsurf (Devin Desktop) | `bunx storebound install --agent windsurf` (skills in `.agents/skills`, MCP in your global `mcp_config.json`) |
|
|
| OpenCode | `bunx storebound install --agent opencode` (skills in `.opencode/skills`, `opencode.json`) |
|
|
| anything that reads `AGENTS.md` | copy or link `AGENTS.md` (rules + skill index) and use the CLI |
|
|
|
|
`install` takes several agents at once (`--agent codex,cursor`), `--dry` to preview, `--no-skills` / `--no-mcp`,
|
|
and `--command "<cmd>"` to choose how the MCP server starts. It only adds or updates the `storebound` entry and its
|
|
own skill folders; everything else in those files stays as it is, and re-running it is safe.
|
|
|
|
## Commands
|
|
|
|
Every write command takes `--dry`; re-runs are idempotent; output is one timestamped line per step, so long jobs can
|
|
run in the background with a log (`storebound shots render > .storebound/render.log 2>&1 &`).
|
|
|
|
<details>
|
|
<summary><b>Setup and checks</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `init [--profile app\|game\|subscription-app]` | local | copy the templates of a profile (default app), gitignore `.storebound/` |
|
|
| `import fastlane [--dir fastlane] [--profile ...]` | local | config, `store/appstore.md`, `store/listing.md` and screenshot frames from a fastlane setup; never overwrites, reports what is not mapped |
|
|
| `install --agent <tool>` | local | skills + MCP entry for an AI tool |
|
|
| `mcp` | local | stdio MCP server wrapping the commands |
|
|
| `serve --stdio` | local | JSON-RPC session for the storebound desktop app (see [Desktop machine interface](#desktop-machine-interface)) |
|
|
| `inspect [--json]` | local | offline summary of the config: name, store ids, languages, products |
|
|
| `check` | local | config, copy limits, data safety, products, subscriptions, profile (delete-account URL, demo account, terms URL), files; warns on untranslated release notes |
|
|
| `catalog diff [--store] [--threshold 10]` | API, read-only | one catalog vs both stores: missing / extra / mismatched products and subscriptions, US and override prices, locales, offers, per-region price differences between the stores; exit 1 on mismatches |
|
|
| `check --aso` | local | keyword audit: App Store keyword field rules, Play stuffing / claims |
|
|
| `scan [--store apple\|play] [--json] [--write] [--originality --against <folders>]` | local, read-only | rejection risks in the project itself (stack, SDKs, Info.plist, manifest, listings) with guideline, evidence file:line, fix and skill; Data safety / App Privacy / ATT / privacy manifest proposal as a diff (`--write` adds the detected answers to the config); exit 1 on blockers |
|
|
| `validate [--apple] [--play]` | API, read-only | readiness report with ordered blockers plus the local renders checks (`--no-local` skips); exit 1 while any blocker is left |
|
|
| `originality --against <folders> [--json]` | local, read-only | Guideline 4.3 check vs your other apps: name, listing text, icon and screenshot perceptual hashes, project structure; scores, near-duplicate flags, suggestions; exit 1 when flagged |
|
|
| `whats-new [--since <tag>]` | local | draft player-facing release notes from git log into both listing files |
|
|
| `whats-new --languages en,de,tr` / `check` / `apply` | local | one file per language (`store/whats-new/`) for your agent (or a `--translate` hook of yours) to translate, per-store limits (Play 500, App Store 4000), then into both listing files |
|
|
| `listing translate --to de,ja [--from en] [--translate "<cmd>"]` | local | per-language listing sections in both listing files (App Store name, subtitle, promotional text, keywords, description; Play title, short, full) for your agent (or a hook) to translate, marked as drafts until reviewed; keywords cleaned up |
|
|
| `listing check` / `listing reviewed --lang <ids>` | local | every language section against the store limits and keyword rules, untranslated and unreviewed drafts (exit 1 on a problem) / remove the draft marker after a human read it |
|
|
| `flow list \| validate \| run <name> [NAME=value] [--dry \| --apply] [--from <step> \| --resume]` | local + whatever the steps call | repo-local workflows (`store/workflows/*.yml`): steps in order with progress; writes run as dry runs unless `--apply`, owner steps are listed as your click and never run (see [Repo-local workflows](#repo-local-workflows)) |
|
|
| `catalog pull [--write]` | API, read-only + local | IAP and subscription texts on the stores vs the config: a proposal diff (`.storebound/pull/catalog.json`); the config is never rewritten |
|
|
| `ads scan --ios <path>` | local | AdAttributionKit / SKAdNetwork: ad network ids, postback copy endpoints, attribution windows and cooldown, conversion value updates; exit 1 on a blocker |
|
|
| `privacy-manifest --ios <path>` | local | PrivacyInfo.xcprivacy, required-reason APIs, SDKs without manifests |
|
|
| `api list \| coverage \| apple\|play <operation>` | API | every App Store Connect / Google Play operation (see [Every endpoint](#every-endpoint)); GET runs, writes are dry unless `--yes` in a terminal |
|
|
| `desktop [--download]` | local | open the storebound desktop app (in an app folder: add or select it there); not installed: the download for this system, `--download` saves it to ~/Downloads after checking its signature (never installs) |
|
|
| `doctor [--json]` | local | which local tools are here (ffmpeg, Chrome, playwright, Xcode, adb, git) and how to install the missing ones |
|
|
| `icons` | local | App Store 1024 (no alpha), Play 512, web, Capacitor sources, Android legacy launcher icons |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>Google Play</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `play listing [--include-drafts]` | API | app details, title / short / full description per language, promo video URL (`play.video`); prints the plan first (old -> new per field, draft languages skipped), `--dry` stops there, the real run asks and sends only changes |
|
|
| `play listing pull [--write]` | API, read-only + local | live listings back into `store/listing.md`: diff first, writes only with `--write` |
|
|
| `play images [--variant b]` | API | icon, feature graphic, phone / 7" / 10" screenshots, one edit per language |
|
|
| `play products` | API | one-time products: regional prices from `convertRegionPrices` + your overrides, activate |
|
|
| `play subscriptions [--csv]` | API | subscriptions: listings + benefits, one auto-renewing base plan, regional prices, intro and extra offers, activate (plan first, asks) |
|
|
| `play data-safety` | API | Data safety form (CSV import); `--csv` writes the file |
|
|
| `play build [--version-code N] [--flavor f]` | local + API read | Gradle `bundleRelease` (Flutter `flutter build appbundle`; Capacitor, React Native, Unity export found) with the next free versionCode and the upload key from env / `.storebound` (passed through the environment, never argv); AAB checked and copied to `build/android/` |
|
|
| `play upload [<aab>] --track internal\|alpha\|beta` | API | AAB to a testing track with release notes; versionCode checked first; `--dry` is read-only, a real run asks |
|
|
| `play release <aab>` | API | upload + tracks (`--internal completed --production draft`, `--production inProgress --fraction 0.05`), release notes |
|
|
| `play rollout` | API | `--fraction x`, `--halt`, `--resume`, `--complete` (asks first, except halt) |
|
|
| `play next-version-code` | API, read-only | next free versionCode |
|
|
| `play status` | API, read-only | tracks, listings, products |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>App Store</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `apple bundle` | API | register the bundle id, find the app record |
|
|
| `apple listing --version x.y.z [--build N] [--include-drafts]` | API | prints the plan first (version: keep / update / create / refuse with the reason; per locale create / update, old -> new per field; draft locales skipped), `--dry` stops there, the real run asks and applies exactly it: version (manual release), the build with exactly that number, texts per locale (name: the locale's `### name`, else `apple.name`, else `app.name`) |
|
|
| `apple listing pull [--version x.y.z] [--write]` | API, read-only + local | live texts back into `store/appstore.md`: diff first, writes only with `--write` |
|
|
| `apple screenshots [--variant b]` | API | 6.9" + 6.5" (+ 13" iPad with `apple.ipad`, + 6.3" with `apple.iphoneMedium`) per locale, MD5-idempotent |
|
|
| `apple previews [--poster 5]` | API | app preview videos per locale, checked first, MD5-idempotent, poster frame |
|
|
| `apple iap` / `apple iap-review` | API | IAPs (consumable, non-consumable, non-renewing), localizations, price schedules, availability / review screenshots |
|
|
| `apple iap-submit [--with-version] [--no-submit]` | API | IAP / subscription / group versions into a review submission (the first of a type together with the app version); asks before adding and before submitting |
|
|
| `apple offer-codes [--codes]` | API | offer code campaigns for one-time purchases; custom and one-time-use codes only with `--codes` (asks; codes saved to `.storebound/offer-codes/`) |
|
|
| `apple win-back` | API | win-back offers for lapsed subscribers (asks: they go live on their start date) |
|
|
| `apple promoted [--enable]` | API | promoted in-app purchases: create (disabled), order, switch on (asks) |
|
|
| `apple preorder status \| enable --release-date \| end` | API | pre-orders per territory (enable / end ask first) |
|
|
| `apple cpp list \| create \| upload \| submit \| visible` | API | custom product pages from the config and a shots variant; submit / visible ask first |
|
|
| `apple events list \| create \| upload \| submit` | API | in-app events with schedule, localizations, card and details images; submit asks first |
|
|
| `apple subscriptions [--csv] [--codes]` | API | groups, subscriptions, levels, localizations, availability, equalized prices + overrides + CSV (scheduled changes), introductory and promotional offers, offer code campaigns, review screenshots (plan first, asks; custom codes only with `--codes`) |
|
|
| `apple appinfo` | API | categories, age rating, content rights, Free, territories; plan first (old -> new), only changes are sent |
|
|
| `apple review-details` | API | App Review contact, demo account (from `.storebound/`, masked), notes (+ sign-in steps, where account deletion is) |
|
|
| `apple encryption` | API | export compliance answer from `apple.usesNonExemptEncryption` (never guessed) |
|
|
| `apple release --phased on\|pause\|resume\|complete`, `--manual-release` | API | phased release, release an approved version (resume / complete / release ask first) |
|
|
| `apple signing status` | API, read-only | bundle id capabilities, distribution certificates (with / without a private key on this Mac), App Store profiles per bundle id, installed or not |
|
|
| `apple signing ensure [--keychain] [--no-install]` | API | missing capabilities, a distribution certificate from a key made here (kept 0600 in `.storebound/signing/`, never printed), App Store profiles installed for Xcode; dry first, asks; Developer ID needs the Account Holder (403 explained) |
|
|
| `apple build [--version x.y.z] [--build-number N]` | local + API read | xcodebuild archive + exportArchive with a generated ExportOptions.plist (app-store-connect, team, signing style, profiles) and the API key; next build number; Capacitor, React Native, Flutter, Unity projects found |
|
|
| `apple upload [<ipa>] [--via api\|altool]` | API | IPA through the App Store Connect API build upload (or altool), build number checked, waits until VALID (exit 0 / 1 / 7); dry first, asks |
|
|
| `apple next-build` | API, read-only | next safe build number |
|
|
| `apple status` | API | versions, builds, IAPs, submissions; `--wait-review` (exit 0 / 1 / 7); `--cancel-submission <id>` |
|
|
| `apple ppo status \| create --variant b \| upload --variant b` | API | Product Page Optimization test from `shots` variants: treatments, localizations, screenshot sets |
|
|
| `apple ppo start \| stop` | API | start / stop the test (asks first; a stopped test cannot be restarted) |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>TestFlight</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `apple testflight status` | API, read-only | groups, testers and builds per group, recent builds with internal / external beta states |
|
|
| `apple testflight groups` | API | create the groups of `apple.testflight.groups` that are missing |
|
|
| `apple testflight testers [--csv f] [--remove] [--invite]` | API | testers from a CSV in `.storebound/` into their groups (asks first; emails masked) |
|
|
| `apple testflight what-to-test --build N` | API | "What to Test" per locale from `store/appstore.md` (`whatToTest`, else `whatsNew`) |
|
|
| `apple testflight info` | API | Beta App Description, feedback email, URLs, Beta App Review contact and notes |
|
|
| `apple testflight distribute --build N --group a,b [--submit-review]` | API | the build to groups; external groups and Beta App Review ask first |
|
|
| `apple testflight wait --build N [--timeout 60m]` | API, read-only | until processing ends, with progress; exit 0 VALID / 1 FAILED / 7 timeout |
|
|
| `apple testflight expire --keep N \| --older-than D` | API | expire old builds (never attached or processing ones); dry run, then asks |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>After launch</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `health [--weekly] [--days 7]` | API, read-only | Play vitals (user-perceived crash / ANR vs 1.09 % / 0.47 %, devices, top issues), App Store metrics, diagnostics, TestFlight crashes; `--weekly` writes a Markdown report |
|
|
| `reviews list [--unanswered]` | API, read-only | recent reviews of both stores |
|
|
| `reviews draft` | API + local | one reply draft per unanswered review in `.storebound/replies/` |
|
|
| `reviews send` | API | sends drafts with `approved: true` (checks length and incentives, asks first) |
|
|
| `prices export [--no-subscriptions]` | API + local | current prices of every product and subscription (Apple current + scheduled, Play base plan regions), both stores, as `store/prices.csv`; subscription rows feed `apple subscriptions --csv` / `play subscriptions --csv` |
|
|
| `prices import [--reconvert]` | API | diff table, then (asks first) Apple price schedules incl. future-dated changes and Play regional prices |
|
|
| `analytics setup` | API | App Store analytics report request (once, Admin key) |
|
|
| `analytics snapshot [--days 28]` | API + local | impressions, page views, downloads (App Store), visitors, acquisitions, installs (Play reports bucket) -> `.storebound/analytics/` |
|
|
| `analytics compare` | local | snapshots vs the git history of the store files: before / after each listing change |
|
|
| `reports apple-sales \| apple-finance \| play-earnings \| play-sales` | API, read-only + local | sales / finance reports into `.storebound/reports/`, summary per product and currency, CSV export |
|
|
| `triage [--days 14]` | API, read-only | TestFlight crash and screenshot feedback, Play error reports: grouped by signature with devices, OS versions, builds |
|
|
| `aso rank [--storefronts us,de]` | network + local | App Store rank per keyword and storefront plus competitors from the public iTunes Search API (rate limited) -> `.storebound/aso/` |
|
|
| `aso competitors [--term <kw>] [--storefront us,tr] [--top 10]` | network + local | top apps per keyword (name, developer, rating count, first release, last update, localized genre; no subtitles: not in any public API) and the words in their names you do not cover, same rate limit -> `.storebound/aso/competitors/` |
|
|
| `aso suggest [--locale en-US] [--write]` | local (+ Apple Ads API with credentials) | keyword candidates (own text, competitor names, localized category names, Apple Ads keyword suggestions with their relative 0-100 popularity) scored by relevance and competition, and a 100-byte keyword field per locale as a diff; `--write` puts it into `store/appstore.md` as a draft for review. Popularity is never estimated |
|
|
| `aso audit` | local | one report: `check --aso`, free keyword room, rank trend arrows, competitor gaps, Play short description and density, App Store conversion from the analytics snapshots, PPO / Play experiment ideas and the next 3 actions |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>Screenshots and video</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `shots capture [--source web\|ios-sim\|android-emu\|folder] [--concurrency N] [--only-missing \| --force]` | local | raw frames from your running app or game; web: parallel contexts, optional one boot per language (`capture.session`), skips unchanged frames, timing per step |
|
|
| `shots fonts` | network | font subsets for your copy (Noto for CJK / Arabic / Cyrillic / Devanagari ...), your own local font files, and the glyph coverage check per language |
|
|
| `shots render [--variant b \| --variants] [--concurrency N]` | local | every store size and language in the game or app style, feature graphic, contact sheet, overflow report |
|
|
| `shots jpeg` | local | JPEG q92 copies of the Play renders (about 4x faster uploads) |
|
|
| `shots check [--variants] [--json]` | local, read-only | store rules: sizes, alpha, format, bytes, counts, overflow, fallback / missing glyphs, videos, folder frames |
|
|
| `shots gallery` | local | `<renders>/index.html` review page |
|
|
| `shots record [--source ...]` | local | app or gameplay clips: web (Chromium screencast), simulator, emulator, folder |
|
|
| `shots video [--target iphone,ipad,youtube]` | local | app previews + promo video with captions, poster, checks |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>Many apps and release trains</b></summary>
|
|
|
|
| command | kind | what |
|
|
|---|---|---|
|
|
| `ws init` | local | find the app folders below and write `storebound.workspace.json` (globs like `games/*`) |
|
|
| `ws list [--all]` | local | apps (folders + store-only apps), studios, accounts and whether each credential profile has its keys |
|
|
| `ws status [--offline] [--apps a,b]` | API, read-only | portfolio table: every app x both stores (live / rollout % / next version), catalog parity, check; parallel |
|
|
| `ws check`, `ws validate [--apps a,b]` | local / API, read-only | the offline check / the readiness report of every app in one table |
|
|
| `ws run <command> --apps a,b \| --all [--apply]` | per command | any read command in every app; writes as dry runs unless `--apply`; never submit / release / rollout / send |
|
|
| `ws apps import --account <profile>` | API, read-only + local cache | the apps a store account sees, paired with the folders (the desktop's rules) |
|
|
| `ws desktop export [--out f \| --merge [--apply]]`, `ws desktop import` | local | the workspace to and from the desktop app's state (labels, never keys) |
|
|
| `release status [--app a,b] [--versions]` | API, read-only | both stores on one train per version: build > testing > review > live > rollout % |
|
|
| `release plan <version> [--app a]` | API, read-only | what is left for that version on each store, in fix order, with the owner's next step |
|
|
|
|
</details>
|
|
|
|
## Growth extras
|
|
|
|
Optional commands for after (or around) the first release. Each has `--dry` and `--json`; store and API writes ask in
|
|
a terminal first and are never offered over MCP.
|
|
|
|
- **Purchasing power prices:** `storebound prices ppp [--base 4.99] [--source worldbank|bigmac|file]` proposes a price
|
|
per region from the bundled World Bank ICP PPP conversion factors (`cli/data/ppp-worldbank.json`, CC BY 4.0, dated
|
|
and cited; refresh with `bun tools/update-ppp.ts`), clamped to `prices.ppp.floor`..`ceiling` of the US price and
|
|
snapped to each store's price points. It writes a sheet in the `prices import` format (default
|
|
`store/prices-ppp.csv`) and shows the diff; `storebound prices import --csv store/prices-ppp.csv --dry` applies it.
|
|
Skill: `ppp-pricing`.
|
|
- **Apple Ads:** `storebound ads apple status | campaigns | report [--by campaign|keyword] [--csv out.csv]`: spend,
|
|
taps, installs, CPA, read-only (OAuth client credentials from env / `.storebound/`, never printed). `bid`, `budget`
|
|
and `pause` print old -> new and ask first. Skill: `apple-ads`.
|
|
- **Web presence:** `storebound web init [--landing]` writes `apple-app-site-association`,
|
|
`.well-known/assetlinks.json` (Play app signing SHA-256), the Smart App Banner tag and an optional minimal landing
|
|
page (store marks from simple-icons, privacy / support links from the config). `storebound web check <domain>`
|
|
verifies the live files: 200 without redirects, JSON content type, valid JSON, ids match the config. Skill: `web-presence`.
|
|
- **Ad-hoc and internal sharing:** `storebound apple adhoc devices | register | profile | options | export` (UDIDs
|
|
masked; registering asks first) and `storebound play share --aab app.aab` (Play internal app sharing; asks first,
|
|
prints the install link). Skill: `adhoc-distribution`.
|
|
- **RevenueCat (optional):** `storebound revenuecat diff | sync` mirrors products, subscriptions, entitlements and a
|
|
default offering into a RevenueCat project (REST API v2, secret key from env / `.storebound/`). sync only creates and
|
|
attaches, never deletes. Runtime entitlements, receipts and paywalls stay RevenueCat's job. Skill: `revenuecat-sync`.
|
|
- **Console forms without an API, agent-first:** `storebound console plan <form> [--json]` gives your AI agent the
|
|
start URL (deep links when the ids are known), the steps, the answer sheet from the config (each answer certain or
|
|
"ask owner", with the console's exact labels), the buttons it must never press and the UI facts verified on the real
|
|
consoles; the agent drives the page with its own browser tool (Claude in Chrome, a built-in browser, Playwright),
|
|
stops before the final button and tells you what to press. `storebound console verify <form> [--from-text -]` then
|
|
checks the page against the config (App Privacy per-type summaries, Play Create app input states, summaries, or the
|
|
App Store Connect API) and exits 1 on a mismatch. Both are read-only MCP tools (`console_plan`, `console_verify`).
|
|
- **Scripted fast path:** `storebound console fill <form>` (`apple-app-record`,
|
|
`apple-age-rating`, `apple-app-privacy`, `play-app-record`, `play-content-rating`, `play-target-audience`) opens a
|
|
visible browser, goes to the form by itself (Apps > + > New App, the app's App Privacy, Play All apps > Create app,
|
|
Play app list > App content > Start / Manage;
|
|
navigation clicks only, never Save / Submit / Publish / Create / Next), fills what the config proves, highlights the
|
|
rest and stops before the final button: you click. **Sign-in:** storebound's own profile
|
|
(one per store, `~/.storebound/console-profile/<store>`): you sign in there once by hand, it is kept between runs
|
|
(session restore on launch, graceful close; tick "Keep me signed in" / "Stay signed in"). storebound starts that
|
|
Chrome itself as a normal window (no `--enable-automation`, `navigator.webdriver` false) so Google accepts the
|
|
sign-in; if Google still answers `/signin/rejected` it says so. A Chrome running with remote debugging is used in
|
|
place. **It never copies your Google cookies by default:** Google binds them to your device (Chrome's Device Bound
|
|
Session Credentials) and signs out sessions whose cookies look copied, which can sign you out of your real Chrome.
|
|
`--use-chrome-session google|apple` is an explicit opt-in (a temporary copy of only that store's cookies, deleted at
|
|
the end) that prints this warning; see the console-autofill skill. It detects sign-in pages by URL, frame and text (Apple's starts with no password field),
|
|
waits while you sign in and then opens the form from step 1; after printing manual steps it keeps watching and picks
|
|
up where you are. **One login, many studios:** it finds the Play developer account (by `play.packageName`, or
|
|
`play.developerId`) and the App Store Connect team (account menu; `apple.teamId`, or by bundle id) that has the
|
|
app and remembers it in `~/.storebound/console-accounts.json` (ids only).
|
|
**Play Create app** (`play-app-record`, the Play Developer API cannot create apps): the developer account comes from
|
|
`play.developerId`, the workspace account, or `play.developerAccount` (its name on "Choose a developer account");
|
|
with several accounts and none set it stops and lists them. It fills App name (`play.name` or `app.name`, 30
|
|
characters), Default language, App or game (the profile) and Free or paid (`play.free`, else Free for IAP / ads:
|
|
an owner check, a free app can never become paid). It never ticks the Developer Program Policies or US export laws
|
|
declarations and never presses the form's Create app: you do. With `play.developerId` it opens the form's own URL
|
|
(`/console/u/0/developers/<id>/create-new-app`); it fills App package name from `play.packageName` and presses the
|
|
read-only "Check availability", and clicks the inner `[role=radio]` of Play's material radios (the native input
|
|
does not update the form). After Create app, add the new app to your service account yourself (Users and
|
|
permissions > the service account > Add app): app-level permissions do not cover apps created later.
|
|
When a page has changed it falls back to step-by-step guidance with copyable answers. **Console pages change; this is AI-assisted: review every field and save it yourself.** Desktop: "Fill in
|
|
console" on the App info tab. Never over MCP. Skill: `console-autofill`.
|
|
|
|
MCP gets only the safe parts: `prices_ppp`, `web_init`, `apple_adhoc_options` (local files, dry run by default) and
|
|
the reads `ads_apple_status`, `ads_apple_campaigns`, `ads_apple_report`, `web_check`, `apple_adhoc_devices`,
|
|
`revenuecat_diff`.
|
|
|
|
## Many apps and studios
|
|
|
|
A studio with twenty games, or a developer with apps under two store accounts, works from one folder above the app
|
|
folders. `storebound ws init` writes `storebound.workspace.json`:
|
|
|
|
```json
|
|
{
|
|
"apps": ["games/*", { "path": "apps/notes", "apple": "solo-apple" }],
|
|
"studios": [
|
|
{ "name": "Acme Games", "color": "indigo", "apps": ["games/*"],
|
|
"accounts": [{ "store": "apple", "profile": "acme-apple" }, { "store": "play", "profile": "acme-play" }] }
|
|
],
|
|
"accounts": [{ "store": "apple", "profile": "solo-apple", "label": "Solo" }],
|
|
"ignore": ["games/old-*"],
|
|
"overrides": { "pairs": [], "unpairs": [], "links": [], "appStudio": [] },
|
|
"concurrency": 4
|
|
}
|
|
```
|
|
|
|
- **apps**: folders or globs (only folders with a `storebound.config.*` count; `node_modules` and dot folders are
|
|
skipped). An object entry names the folder's own credential profiles.
|
|
- **studios**: name, colour (the desktop's set), the store accounts and the app folders (globs) that belong to it.
|
|
- **accounts**: store accounts without a studio. An account is a store and a **credential profile** name, never a key.
|
|
- **ignore**: folder globs, `app:<store app key>` and `account:<profile>`, as in the desktop's ignore list.
|
|
- **overrides**: the desktop's pairing corrections (`pairs`, `unpairs`, `links` of a store app to a folder,
|
|
`appStudio`), with folders named by their path. A `storebound.workspace.ts` with `export default { ... }` works too.
|
|
|
|
Which keys an app uses: its own entry's profiles, else the single account of that store in its studio, else the
|
|
single account of that store in the workspace, else the folder's own credentials (env or `.storebound/`).
|
|
|
|
**Credential profiles** keep many accounts apart on one machine: `~/.storebound/credentials/<profile>.json` with the
|
|
account names of `.storebound/credentials.json` (`PLAY_SERVICE_ACCOUNT`, `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_P8`,
|
|
`ASC_VENDOR_NUMBER`; the Android upload keystore belongs to the app and stays in the project;
|
|
relative key paths are relative to that folder, which `STOREBOUND_CREDENTIALS_DIR` can move), or env
|
|
`STOREBOUND_PROFILE_<NAME>_<KEY>` (for CI). Any command takes `--account <profile>`. `ws list` shows per account whether
|
|
its profile has keys; no command prints a key.
|
|
|
|
`ws status`, `ws check`, `ws validate` and `ws run` read the apps a few at a time (`concurrency`, 1-8, or
|
|
`--concurrency`), each app with its own keys handed to its child process only. `ws run` takes any read command and
|
|
the MCP tool set; write commands get `--dry` unless `--apply`, and submit, release, rollout, send, start / stop,
|
|
testers and `--yes` are refused: those stay with the owner in the app folder.
|
|
|
|
`ws apps import --account <profile>` lists the apps a store account sees (as `apps list`) into
|
|
`.storebound/workspace/` and pairs them with the folders by the desktop's rules (`cli/lib/identity.ts`, the same code):
|
|
a folder takes the store apps its config names, the remaining iOS and Android apps pair by id, close id or name,
|
|
`unpairs` keep two apart, and the rest are store-only apps that `ws status` reads too.
|
|
|
|
**The desktop app** keeps the same model (studios, accounts, folders, overrides). `ws desktop import` turns its
|
|
`state.json` (or `--state <file>`) into a workspace file and lists the credential profiles to create: the desktop's
|
|
keychain is never read. `ws desktop export` prints the workspace as a desktop state (`--out <file>`), and
|
|
`--merge --apply` adds it to the desktop's `state.json` while the app is closed (a backup is kept; nothing is
|
|
removed; new accounts get their keys in the desktop's Accounts screen).
|
|
|
|
MCP: `workspace_list` (offline) and `workspace_status` (read-only).
|
|
|
|
## Release trains
|
|
|
|
One version, both stores, one track: **build > testing > review > live > rollout**. The CLI and the desktop's Releases
|
|
tab share the code (`cli/lib/train.ts`, `cli/lib/portfolio.ts`), so both show the same stage for the same store state.
|
|
|
|
```
|
|
$ storebound release status
|
|
app version train App Store Google Play
|
|
------ ------- ------- ---------------------- -----------------------------
|
|
Puzzle 2.3.0 current rollout 20% (build 45) rollout 10% (build 230)
|
|
Puzzle 2.4.0 next build testing (build 240, internal)
|
|
```
|
|
|
|
`release plan <version>` lists what is left for that version on each store in fix order: config problems (`check`),
|
|
the version on the store (how to create it when it is missing), store blockers (`validate`, while nothing is
|
|
submitted), the next move and the warnings. Owner steps (submit for review, promote to production, release an approved
|
|
version, raise or complete a rollout) are named with where the owner does them; storebound does not do them. A store
|
|
it cannot read is "unknown", never "not there". Both commands work in an app folder (that app) and in a workspace
|
|
(every app, or `--app a,b`); `--json` gives the structured result. MCP: `release_status`, `release_plan` (read-only).
|
|
## Repo-local workflows
|
|
|
|
Your release routine as a file in the repo, run step by step. `storebound init` writes two to start from
|
|
(`store/workflows/release.yml`, `beta.yml`; app and game versions):
|
|
|
|
```yaml
|
|
# store/workflows/release.yml (app profile, shortened)
|
|
params:
|
|
VERSION: "1.0.0"
|
|
steps:
|
|
- id: next-build
|
|
if: apple
|
|
run: apple next-build
|
|
capture: BUILD
|
|
- id: build
|
|
if: apple
|
|
run: apple build --version ${VERSION} --build-number ${BUILD}
|
|
- id: upload
|
|
if: apple
|
|
run: apple upload --no-wait
|
|
# wait, play-build, play-upload, whats-new, listing, validate, scan, plan ...
|
|
- id: submit
|
|
owner: "App Store Connect: Submit for Review (version ${VERSION}, build ${BUILD}) ..."
|
|
```
|
|
|
|
The plan of that file (`--dry`: nothing runs; without it the reads run and `${BUILD}` is filled in):
|
|
|
|
```
|
|
$ storebound flow run release VERSION=1.2.0 --dry
|
|
[flow] 1/12 next-build: storebound apple next-build (plan only)
|
|
[flow] 2/12 build: storebound apple build --version 1.2.0 --build-number ${BUILD} --dry (plan only)
|
|
[flow] 3/12 upload: storebound apple upload --no-wait --dry (plan only)
|
|
[flow] 4/12 wait: storebound apple testflight wait --build ${BUILD} --version 1.2.0 --dry (plan only)
|
|
[flow] 5/12 play-build: storebound play build --version-name 1.2.0 --dry (plan only)
|
|
[flow] 6/12 play-upload: storebound play upload --track internal --dry (plan only)
|
|
[flow] 7/12 whats-new: storebound whats-new apply (if exists store/whats-new: false)
|
|
[flow] 8/12 listing: storebound apple listing --version 1.2.0 --build ${BUILD} --dry (plan only)
|
|
[flow] 9/12 validate: storebound validate (plan only)
|
|
[flow] 10/12 scan: storebound scan (plan only)
|
|
[flow] 11/12 plan: storebound release plan 1.2.0 (plan only)
|
|
[flow] 12/12 submit: YOUR CLICK App Store Connect: Submit for Review (version 1.2.0, build ${BUILD}). Play Console: promote the internal release and Send changes for review
|
|
```
|
|
|
|
- Read steps (check, validate, scan, release plan, next-build, status) run; **write steps run with `--dry`** unless
|
|
`--apply`, and even then each command asks its own confirmation; wait steps check once; `sh:` steps (your own build
|
|
commands) run only with `--apply`. `--dry` prints the plan without running anything.
|
|
- Owner steps (submit, release, phased release, rollout, review replies, price imports, tester invitations, Beta App
|
|
Review) are never executed, also when written as `run:`; the flow ends with the list of your clicks.
|
|
- `capture:` turns a step's last output line into a variable (`${BUILD}`), `if:` skips a step (`apple`, `play`,
|
|
`exists <path>`, `${VAR} == x`), `flow:` reuses another flow (`private: true` sub-flows), `before` / `after` /
|
|
`onError` are hooks, `continueOnError` keeps going.
|
|
- A failed step stops the flow; `--resume` starts again at that step with the captured variables
|
|
(`.storebound/flows/<name>.json`), `--from <step>` anywhere. `flow validate` checks commands, variables, duplicate ids
|
|
and sub-flow cycles. Several flows can also live in `storebound.workflows.yml|json|ts`.
|
|
- MCP: `flow_list` and `flow_run` (the dry mode only; `--apply` stays a terminal command). Skill: `release-flows`.
|
|
|
|
## Listing in more languages
|
|
|
|
`storebound listing translate --to de,ja` adds a section per language to `store/appstore.md` (name, subtitle,
|
|
promotional text, keywords, description) and `store/listing.md` (title, short and full description) with the source
|
|
text and a marker, `<!-- storebound-translation from=en-US status=translate -->`. Your agent translates in place
|
|
(`listing-localization` skill: native wording, researched keywords, the store limits) and sets `status=draft`; or
|
|
`--translate "<command>"` runs a command of yours once per field (source text on stdin, `STOREBOUND_TARGET_LANG`,
|
|
`STOREBOUND_FIELD`, `STOREBOUND_LIMIT` in the environment). storebound itself calls no translation service.
|
|
`listing check` enforces each store's limits (App Store subtitle 30, promotional text 170, keywords 100 without
|
|
duplicates or name words, description 4000; Play title 30, short 80, full 4000) and lists drafts; `storebound check`
|
|
warns while one is left. After a human read a draft, `listing reviewed --lang de` removes the marker. Reviewed sections
|
|
are never overwritten without `--force`. MCP: `listing_translate`, `listing_check`.
|
|
|
|
## Every endpoint
|
|
|
|
The commands above cover the journey. For anything else, `storebound api` calls **every operation** of the App Store
|
|
Connect API (1303 operations, OpenAPI 4.5.1) and of the Google Play Developer APIs (Android Publisher v3: 145,
|
|
Play Developer Reporting v1beta1: 25), generated from the official specs pinned in `specs/` (source URLs, dates and
|
|
SHA-256 in `specs/SOURCES.json`). Same credentials, same retries on 429 / 5xx (Retry-After honoured), JSON:API
|
|
documents on the Apple side.
|
|
|
|
```sh
|
|
storebound api list --store apple --search "customerReviews" # find an operation
|
|
storebound api apple apps_getCollection --param 'filter[bundleId]=com.example.app'
|
|
storebound api apple GET /v1/apps/123/appStoreVersions --all-pages # METHOD path works too
|
|
storebound api play reviews.list --param packageName=com.example.app --all-pages --json
|
|
storebound api play POST /v1beta1/apps/com.example.app/crashRateMetricSet:query --body query.json
|
|
storebound api coverage # counts per API and resource, what commands wrap
|
|
```
|
|
|
|
- **GET runs right away** (so do the Reporting API `:query` calls and other POSTs that only read).
|
|
- **Every write is a dry run by default**: it prints the exact request (method, URL, body; the credential is never
|
|
shown) and is only sent with `--yes` from an interactive terminal.
|
|
- **Owner-only operations** (submit, release, delete, cancel, refunds, prices, testers and users, review replies,
|
|
product page tests, Play `edits.commit`, app signing; the list is in `cli/api/gate.ts`) also ask you to type the
|
|
operation id before anything is sent.
|
|
- **Agents read, never write**: MCP has one tool for this, `api_get` (GET operations only). Under MCP or the desktop
|
|
app no write goes out, whatever the flags.
|
|
- Small on purpose: one table row per operation plus generic helpers (`cli/api/`), typed from the rows (path and
|
|
required parameters, JSON:API request and response shapes). `bun tools/gen-api.ts` regenerates the tables and
|
|
[docs/api-coverage.md](docs/api-coverage.md) (every operation, its gate and the commands that already call it);
|
|
CI fails when they are stale.
|
|
|
|
## Store art
|
|
|
|
Screenshots and previews are one step of the journey, but the one that takes the longest by hand, so storebound
|
|
does a lot here. Two looks: `shots.style: 'game'` (sunburst, sparkles, badges) and `shots.style: 'app'` (calm
|
|
gradient, a clean device frame, headline + subline). Everything below was made by storebound itself from two tiny
|
|
fictional demos: [a game](examples/demo-game) ("Prism Puddle", procedural art) and [an app](examples/demo-app)
|
|
("Tidy Lists", one static page with sample data): raw frames captured from the web build, captions in several
|
|
languages, every store size, a review gallery and an app preview video. No store account needed to try it.
|
|
|
|

|
|
|
|

|
|
|
|
| Play feature graphic | App preview (App Store, 886 x 1920) |
|
|
|---|---|
|
|
|  |  |
|
|
|
|

|
|
|
|
<details>
|
|
<summary>Contact sheet of one language (every size, packed into a grid)</summary>
|
|
|
|

|
|
|
|
</details>
|
|
|
|
### How it works
|
|
|
|
1. **Frames.** Give your app or game a few switches (language, demo data or a fixed seed, no ads, jump to a screen,
|
|
freeze, autoplay) and
|
|
let `shots capture` take the frames: Playwright for a web build, `simctl` for anything that runs in the iOS
|
|
Simulator (Unity, Godot, Flutter, React Native, native, Capacitor), `adb` for an Android emulator or device, or bring a folder of
|
|
your own frames. The status bar is cropped off and redrawn clean (9:41, full battery).
|
|
2. **Copy.** Per language and scene: eyebrow, two-line title, subtitle, badges. Fonts are subset from Google Fonts to
|
|
exactly your characters, so Japanese, Chinese, Korean, Arabic (right to left) and Hindi just work. `shots fonts`
|
|
checks every character of every language against the font's cmap (TTF / OTF / WOFF / WOFF2, read in plain
|
|
TypeScript): a glyph the font lacks gets an automatic Noto fallback (logged), no fallback is a clear error, and a
|
|
render that drew any glyph with a fallback you did not choose, or with a system font, fails `shots check`.
|
|
|
|
| script | title / body font (default stack) |
|
|
|---|---|
|
|
| Latin incl. Latin Extended (tr, pl, cs, ro, hu ...) and Vietnamese | your `shots.fonts`; default game Paytone One + Montserrat, app Inter (all cover them) |
|
|
| Cyrillic, Greek | Noto Sans (whole line) |
|
|
| Arabic, Hebrew, Devanagari, Bengali, Thai | Noto Sans Arabic / Hebrew / Devanagari / Bengali / Thai (whole line) |
|
|
| Japanese, Korean, Chinese | Noto Sans JP / KR / SC (TC for zh-Hant, TW, HK) (whole line) |
|
|
| a character the stack lacks | Noto Sans Display (titles) or Noto Sans (body), Noto Sans <Script>, then Noto Sans Symbols 2 / Symbols / Math: an ERROR in `shots check` unless listed in `shots.fonts.fallback` |
|
|
|
|
Some display fonts lack Latin Extended: Lilita One has no ş ğ İ ą ę ń ś ź ż ć ă ș ț č ř ě ů ő ű or Vietnamese,
|
|
so Turkish, Polish, Czech, Romanian, Hungarian or Vietnamese titles need another title font (Paytone One, Baloo 2,
|
|
Rowdies) or `fonts: { ..., fallback: ['Noto Sans Display'] }` to accept the mixed glyphs.
|
|
|
|
**Your own brand font.** `shots.fonts.title` / `body` / `fallback[]` also take a local file (relative to the
|
|
config; TTF, OTF, WOFF or WOFF2, checked by extension and signature) or `{ family, file, weight?, style?, license? }`:
|
|
|
|
```ts
|
|
fonts: {
|
|
title: { family: 'ER Display', file: 'apps/game/public/assets/fonts/er-display.woff2', license: 'OFL-1.1' },
|
|
body: 'Montserrat',
|
|
fallback: ['Noto Sans Display'], // tried in order, before the automatic Noto fallbacks
|
|
},
|
|
```
|
|
|
|
`shots fonts` runs the same cmap check on the file (`pl title: ę ł ą Ł not in ER Display -> Noto Sans Display`),
|
|
prints its size, glyph count, sha256 and license, and records it in `fonts.json`. The file is never copied: render
|
|
(and `shots video` captions) serve it from where it is, and a changed file changes `fonts.css` (the capture cache
|
|
misses) and makes `shots render` ask for `shots fonts` again. `shots check` counts the local font and listed
|
|
fallbacks as configured, not as fallbacks, and warns when a local font has no `license` note.
|
|
3. **Render.** Every store size at exact pixels: iPhone 6.9" and 6.5", 13" iPad (if your app runs on iPad), Play
|
|
phone, 7" and 10" tablets, the 1024 x 500 feature graphic. 24-bit RGB, no alpha (both stores reject it).
|
|
4. **Check and review.** `shots check` applies the store rules locally (sources cited in `cli/shots/spec.ts`);
|
|
`shots gallery` puts everything on one page with problems outlined. `validate` repeats the checks next to the
|
|
store state.
|
|
5. **Variants.** Other first screenshot, order or copy as `shots.variants`, rendered side by side, for a Play store
|
|
listing experiment or an App Store Product Page Optimization test (set up in the consoles; storebound makes and
|
|
checks the images).
|
|
6. **Video.** `shots record` films the autoplay mode, `shots video` cuts 15-30 s App Store previews (886 x 1920,
|
|
1200 x 1600) and a 9:16 / 16:9 promo for YouTube with captions in every language, `apple previews` uploads them.
|
|
Play takes a YouTube URL in `play.video`.
|
|
|
|
Details: the [store-screenshots](skills/store-screenshots/SKILL.md) and [app-previews](skills/app-previews/SKILL.md)
|
|
skills.
|
|
|
|
## Skills
|
|
|
|
In journey order. Each one says which steps are API (storebound) and which are console clicks, with the exact
|
|
navigation.
|
|
|
|
| skill | for |
|
|
|---|---|
|
|
| `migrate-from-fastlane` | `storebound import fastlane`: ids, listings, languages and screenshots from fastlane; keys and accounts stay out |
|
|
| `play-first-release` | Google Play from zero to "In review" |
|
|
| `appstore-first-release` | App Store from API key to "Waiting for review", incl. xcodebuild with the API key |
|
|
| `build-and-sign` | API signing (capabilities, certificate, profiles), `apple build` / `apple upload`, `play build` / `play upload`, for native, Capacitor, React Native, Flutter and Unity projects |
|
|
| `testflight` | beta groups, testers from a CSV that never enters git, What to Test, Test Information, Beta App Review, waiting for processing, expiring old builds |
|
|
| `store-iap-setup` | in-app products on both stores (consumable, non-consumable, non-renewing), prices and the price sheet CSV, catalog diff, testers, review submissions through the API and the first-IAP-with-version rule |
|
|
| `store-subscriptions` | subscription groups, levels, base plans, prices, intro and promotional offers, offer codes, disclosure and Terms (3.1.2), first submission |
|
|
| `review-access` | demo account for reviewers (App Review fields, Play App access), never committed |
|
|
| `account-deletion` | Apple 5.1.1(v) in-app deletion, Play delete-account web page and Data safety |
|
|
| `aso-basics` | keyword field, subtitle, cross-localization, Play placement without stuffing, keyword ranks, competitors, keyword field proposals and the ASO audit (iTunes Search API, Apple Ads popularity with credentials) |
|
|
| `app-store-promotion` | IAP offer codes, win-back offers, promoted IAPs, pre-orders (Play pre-registration: console guide), custom product pages, in-app events |
|
|
| `store-declarations` | every questionnaire: Play Data safety (API) and App content (IARC, target audience, ads, advertising ID, app access, government / financial / health), Apple App Privacy labels and age rating |
|
|
| `export-compliance` | encryption answer, Info.plist key, declarations; never exempt by default |
|
|
| `privacy-policy` | one policy page for all your apps with a per-app services table (templates per profile) |
|
|
| `privacy-manifest` | PrivacyInfo.xcprivacy, required-reason APIs, SDK manifests (ITMS-91053 / 91061) |
|
|
| `app-icons` | one artwork to every icon, adaptive / legacy launcher pitfalls |
|
|
| `store-screenshots` | capture from any engine, copy, fonts, render, checks, gallery, variants, App Store product page tests, Play experiments, upload |
|
|
| `app-previews` | app or gameplay clips, App Store previews, Play promo video, checks, upload |
|
|
| `store-preflight` | run and read `check`, `check --aso`, `validate`, `privacy-manifest` and `release plan` before submitting; `ws check` / `ws validate` for many apps |
|
|
| `release-management` | updates: release trains (`release status`, `release plan`), Apple phased + manual release, Play staged rollout, halt / pause (not a rollback), review wait |
|
|
| `store-preflight` | run and read `check`, `check --aso`, `validate`, `privacy-manifest` before submitting |
|
|
| `store-api` | any App Store Connect / Google Play operation without a command: find it, read freely, writes dry until the owner sends them |
|
|
| `release-management` | updates: Apple phased + manual release, Play staged rollout, halt / pause (not a rollback), review wait, pulling console edits back |
|
|
| `store-health` | crash / ANR vitals vs Play's bad behavior thresholds, App Store diagnostics, weekly report, crash and feedback triage |
|
|
| `review-replies` | reply drafts, tone, no incentives, store limits, the owner approves and sends |
|
|
| `store-analytics` | App Store analytics reports, Play reports bucket, before / after listing changes, sales / finance / earnings reports |
|
|
| `whats-new` | player-facing release notes from git, one file per language for your agent to translate, store limits checked |
|
|
| `listing-localization` | the listing in more languages: drafts per language, native wording and keyword research, store limits, review before push |
|
|
| `release-flows` | repo-local workflows (`store/workflows/*.yml`): release and beta routines with dry runs, resume and the owner's clicks |
|
|
| `review-rejections` | triage a rejection (fix / clarify / push back / appeal), reply template, the guidelines apps and games hit |
|
|
| `ads-mediation-checks` | AdMob, AppLovin MAX and LevelPlay: ids, debug tools, adapters, app-ads.txt, SKAdNetwork, AdAttributionKit (`ads scan`), no-fill (`ads-levelplay-checks` stays as a pointer) |
|
|
| `portfolio-originality` | Guideline 4.3 gate for developers shipping many similar apps or games, with `storebound originality` scores |
|
|
|
|
## MCP server
|
|
|
|
`storebound mcp` speaks MCP over stdio, tools only: spec revision **2026-07-28** (stateless: `server/discover`,
|
|
per-request `_meta`, `resultType`, cacheable `tools/list`) and, on the same process, clients that still open with
|
|
`initialize` (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05). It runs each tool as a CLI call in the project directory
|
|
(the server's working directory, or `STOREBOUND_PROJECT`) and returns the output.
|
|
|
|
- One tool per command. Read-only: `scan`, `check`, `check_aso`, `aso_competitors` (no snapshot saved), `aso_suggest` (never writes), `aso_audit`, `validate`, `catalog_diff`, `health`, `reviews_list`, `prices_diff`,
|
|
`analytics_compare`, `apple_status`, `apple_next_build`, `apple_testflight_status`, `apple_ppo_status`,
|
|
`play_status`, `play_next_version_code`, `privacy_manifest`, `shots_check`, `workspace_list`, `workspace_status`,
|
|
`release_status`, `release_plan`, `flow_list`, `flow_run` (dry mode only), `listing_check`. Local writes: `import_fastlane`, `whats_new`, `listing_translate`, `icons`,
|
|
`play_status`, `play_next_version_code`, `privacy_manifest`, `shots_check`, `api_get` (any App Store Connect or
|
|
Google Play GET operation by id). Local writes: `import_fastlane`, `whats_new`, `icons`,
|
|
`health_report`, `reviews_draft`, `prices_export`, `analytics_snapshot`, `shots_capture`, `shots_render`,
|
|
`shots_gallery`, `shots_record`, `shots_video`. Store writes: `play_listing`, `play_images`, `play_products`,
|
|
`play_data_safety`, `play_subscriptions`, `play_release_testing`, `apple_bundle`, `apple_iap`, `apple_iap_review`,
|
|
`apple_subscriptions`, `apple_listing`,
|
|
`apple_screenshots`, `apple_previews`, `apple_appinfo`, `apple_review_details`, `apple_encryption`,
|
|
`apple_phased_release`, `apple_testflight_groups`, `apple_testflight_what_to_test`, `apple_testflight_info`,
|
|
`apple_testflight_distribute` (internal groups only), `apple_ppo_create`, `apple_ppo_upload`, `analytics_setup`.
|
|
- Annotations say what is read-only, destructive (replaces data), idempotent and network-bound.
|
|
- No MCP SDK: a small newline-delimited JSON-RPC server (server/discover, initialize, ping, tools/list, tools/call,
|
|
cancellation), so storebound keeps zero runtime dependencies.
|
|
|
|
Manual config for a tool that `install` does not know: command `bunx` with args `["storebound", "mcp"]` (or the
|
|
absolute path of `bin/storebound` plus `mcp`).
|
|
|
|
### Desktop machine interface
|
|
|
|
`storebound serve --stdio` is what the desktop app (`apps/desktop`, in progress) runs as its sidecar: newline-delimited
|
|
JSON-RPC on the same core as the MCP server (tool list, validation, forbidden flags, child-process runner), with typed
|
|
methods (`check`, `validate`, `apple.status`, `play.listing`, ... plus `project.inspect` and `portfolio.status` over
|
|
many project folders), structured results (the commands' `--json` output), progress notifications and cancellation.
|
|
The first request is `session.hello` with a per-session secret only the host knows. Write methods are dry runs unless
|
|
the call carries a single-use confirm token (HMAC over method, arguments and expiry) that the host mints after the
|
|
owner confirmed a native dialog showing that dry run. There is no method that submits, releases or publishes; Play
|
|
testing releases and phased release changes stay out of the desktop profile too. Types: `cli/lib/desktop-protocol.ts`.
|
|
`bun tools/build-sidecars.ts` compiles the CLI for macOS, Linux and Windows (`bun build --compile`). Design, safety
|
|
model and the Dependencies screen (tool sources and licenses): [docs/desktop.md](docs/desktop.md).
|
|
|
|
## Safety model
|
|
|
|
- **Credentials** live in env vars or the gitignored `.storebound/credentials.json`, never in the config or the repo.
|
|
storebound only talks to the official store APIs (plus Google Fonts, your own Play reports bucket and your own URLs);
|
|
see [SECURITY.md](SECURITY.md).
|
|
- **Dry run everywhere.** Every write command takes `--dry`; every MCP write tool has `dry_run`, **default true**.
|
|
Raw API writes (`storebound api`) are dry unless `--yes` is given in an interactive terminal; owner-only operations
|
|
also need the operation id typed, and MCP only gets the read-only `api_get`.
|
|
- **Nothing public or irreversible through an agent.** Submitting for review (App Review and Beta App Review),
|
|
releasing a version, production releases, resuming or completing a rollout or phased release, canceling a
|
|
submission, deleting, inviting or removing testers, sending review replies, applying prices and starting or
|
|
stopping a product page test and creating custom offer codes are not MCP tools. Subscription syncs through MCP only
|
|
create what is new; price changes of live subscriptions wait for a terminal run. They stay terminal commands that ask for confirmation, and the skills tell every agent to ask the owner
|
|
first ([docs/agents-core.md](docs/agents-core.md)).
|
|
- **Personal data stays local.** Tester lists, the App Review demo account, reply drafts, health reports and analytics
|
|
snapshots live in the gitignored `.storebound/` (the demo account is masked in every log line); tester emails are masked in every log line, and the config refuses a tester CSV path
|
|
outside `.storebound/`.
|
|
- **Idempotent writes.** Re-running a command converges (MD5 checks for screenshots and previews, one Play edit per
|
|
language), so a failed upload is fixed by running it again.
|
|
- **Simulators and emulators**: storebound shuts down only a simulator it booted itself and never boots, wipes or
|
|
reboots an Android emulator or device.
|
|
|
|
## FAQ
|
|
|
|
**Is it only for games?** No. It started with my game, but every command is about store records, not engines or
|
|
genres. `init --profile app` and `--profile subscription-app` give you templates for a utility app with accounts and
|
|
for a subscription app (Data safety, App Privacy, declarations, listing and screenshot copy), the `app` screenshot
|
|
style, account deletion and demo account checks, and subscriptions on both stores. Game-only advice (IARC game
|
|
questions, gameplay footage, ad mediation) is marked as such in the skills.
|
|
|
|
**Do I need Claude (or any AI tool)?** No. The CLI is a normal command and the skills are Markdown checklists you can
|
|
read yourself. The AI integrations are optional adapters generated from the same files.
|
|
|
|
**Does it fill the questionnaires?** The ones with an API, yes: Play Data safety (the whole form from `dataSafety` in
|
|
the config; the import replaces the form, so the config is the single truth), Apple age rating, content rights,
|
|
categories, price and availability, the export compliance answer and the App Review details. The stores have no
|
|
public API for Play's App content cards (IARC content rating, target audience, ads, advertising ID, app access,
|
|
government / financial / health / news) or for Apple's App Privacy labels. For those, `init` gives you prepared
|
|
answers (`store/declarations.md`, `store/app-privacy.md`, per profile: a utility app with an account, a subscription
|
|
app, or a free-to-play game with ads and IAP),
|
|
and the `store-declarations` skill has the exact clicks: an agent with browser tools can fill them in while you
|
|
watch, or you copy them in yourself. Either way the answers are your statements to the stores: change what does not
|
|
match your app, and the agent asks you whenever the code does not prove an answer. `validate` cannot see these
|
|
forms, so `store-preflight` lists them as a manual check.
|
|
|
|
**My app or game is Unity / Godot / Flutter / React Native / native, not web.** Screenshots and clips come from the iOS Simulator
|
|
(`ios-sim`), an Android emulator or device (`android-emu`) or a folder you fill with any tool; only the rendering of
|
|
the store images uses a headless browser. The store commands do not care about the engine at all: they work on
|
|
your AAB / build numbers and listing files.
|
|
|
|
**Is my store account safe?** storebound uses the official APIs with keys you create and can revoke, never your
|
|
password. Store writes are dry runs until you say otherwise, and the public steps (submit, release) stay with you.
|
|
Use an App Store Connect key with the App Manager role and a Play service account limited to your app.
|
|
|
|
**Why Bun?** One fast runtime with TypeScript, `fetch`, a test runner and a static server built in: no build step and
|
|
zero runtime dependencies. It needs Bun installed (it does not run on plain Node).
|
|
|
|
**Does it upload my video to YouTube?** No. Play only takes a YouTube URL, and uploading to YouTube needs your Google
|
|
account; `shots video` makes the file, you upload it and paste the URL into `play.video`.
|
|
|
|
**Can it run store experiments for me?** On the App Store, mostly: `apple ppo create / upload` builds a Product Page
|
|
Optimization test from your `shots` variants (treatments, locales, screenshots) and you start it (`apple ppo start`
|
|
asks first). Results per treatment are only shown in App Store Connect. Play store listing experiments have no API:
|
|
storebound renders and checks the variant images, you set the experiment up in Play Console.
|
|
|
|
**Does it answer reviews by itself?** No. It drafts one file per review, your agent helps write the replies, and only
|
|
the drafts you mark `approved: true` are sent, by you, after a confirmation. Replies that offer something for a rating
|
|
are refused.
|
|
|
|
## Roadmap
|
|
|
|
Wave 2 (TestFlight, vitals, review replies, price sheets, analytics, MCP 2026-07-28, Product Page Optimization) shipped
|
|
in 0.4.0. 0.5.0 made storebound app-first (profiles, app screenshot style, account deletion, review access) and
|
|
brought the full in-app catalog: subscriptions on both stores with offers and offer codes, non-renewing products and
|
|
the cross-store `catalog diff`. Next, roughly in this order:
|
|
|
|
- Play prepaid base plans for `non_renewing`.
|
|
- The App Asset Library (API 4.5.1) for screenshots, custom product pages and in-app events once the screenshot sets
|
|
it replaces stop working.
|
|
- Play store listing experiments and their results, as soon as Google offers an API.
|
|
- App preview treatments and alternate icons in Product Page Optimization tests.
|
|
- The iPhone Duo screenshot classes Apple lists for apps built with the iOS 27.1 SDK (required from April 2027).
|
|
|
|
## Contributing
|
|
|
|
Issues and pull requests are welcome: store rule updates (with a source), new capture sources, adapters for more AI
|
|
tools. See [CONTRIBUTING.md](CONTRIBUTING.md); `bun run ci` must be green.
|
|
|
|
<details>
|
|
<summary><b>Agent support: formats and what is not verified</b></summary>
|
|
|
|
Checked against each tool's own docs in October 2026:
|
|
|
|
- **Agent Skills**: frontmatter is only `name` (lowercase-hyphen, equals the folder) and `description`; the generator
|
|
fails if a skill adds anything else.
|
|
- **Claude Code**: plugin manifest + root `.mcp.json` with `${CLAUDE_PLUGIN_ROOT}`; `claude plugin validate .` passes.
|
|
- **Codex**: `.codex-plugin/plugin.json` with `"skills": "./skills/"`. Not verified: whether Codex starts the root
|
|
`.mcp.json` from a plugin and expands `${CLAUDE_PLUGIN_ROOT}` there, so use `install --agent codex` for the MCP
|
|
server. OpenAI also documents a newer portable root `plugin.json`; not shipped yet.
|
|
- **Gemini CLI**: `gemini-extension.json` with `${extensionPath}${/}`, `GEMINI.md` as context, `skills/` inside the
|
|
extension. Google now points unpaid users to Antigravity CLI; not tested there. The `.gemini/skills` folder used by
|
|
`install --agent gemini` is not verified (the extension route is).
|
|
- **Windsurf**: the current docs name `~/.config/devin/mcp_config.json`; `install` keeps using
|
|
`~/.codeium/windsurf/mcp_config.json` when that file already exists (older installs).
|
|
- **MCP**: built for 2026-07-28 (final, modelcontextprotocol.io) and dual-era: `initialize` clients get 2025-11-25
|
|
(or 2025-06-18 / 2025-03-26 / 2024-11-05). Tested with both handshakes on one stdio process, not yet against every
|
|
client listed above.
|
|
- **Not verified against a live account yet** (no credentials during development; request builders are tested with
|
|
fixtures from the official docs): the scale of the Play Reporting API rates (read as fractions, 0.0109 = 1.09 %),
|
|
the App Store review reply limit (5970 characters is commonly reported; Apple documents none, storebound only
|
|
warns), the TestFlight "What to Test" limit (4000 assumed), whether an analytics segment checksum covers the gzip
|
|
file or its content (both are accepted), that a new App Store price schedule replaces the old one (Apple's example
|
|
implies it), stopping a Product Page Optimization test with `started: false`, and `APP_IPHONE_61` for the 6.3"
|
|
iPhone set. From 0.5.0 (request bodies follow Apple's and Google's published schemas, tested with fixtures, not
|
|
yet run against a live account): free promotional offers and offer codes sent with territories but no price point,
|
|
subscription prices and introductory offers created inline through `PATCH /v1/subscriptions/{id}`, Play
|
|
pay-as-you-go offers as a `relativeDiscount`, Play "developer determined" offers as offers without targeting plus an
|
|
offer tag, the ISO 8601 strings for Play billing periods (Google lists them only in words), the Play subscription
|
|
benefit length (40, from Play Console), the Data safety sign-up method ids (`PSL_ACM_*`, Google publishes none:
|
|
compare with a Console export) and the delete-account URL question id (use `dataSafety.extra`). From the store
|
|
operations (built from the App Store Connect API 4.5.1 OpenAPI spec and the cited Help pages, tested with recorded
|
|
answers, not yet run live): the first IAP of a type sent through `reviewSubmissionItems` next to the app version
|
|
(`--no-submit` leaves the draft for the UI if Apple refuses it), free IAP offer code and win-back prices without a
|
|
price point, the order request for promoted purchases, the Play earnings / sales zip names beyond the documented
|
|
prefix (storebound lists the prefix), and where Xcode stores `AttributionCopyEndpoint` (top level or inside an
|
|
`AdAttributionKit` dictionary: both are read).
|
|
- **Apple IAP description**: 45 characters (App Store Connect Help, "In-app purchase information"); an older App Store
|
|
marketing page says 55. storebound checks 45.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><b>Develop</b></summary>
|
|
|
|
```sh
|
|
bun install
|
|
bun run ci # adapters fresh (gen-adapters --check), tests, typecheck, secrets / personal data scrub
|
|
bun tools/gen-adapters.ts # after editing skills/*/SKILL.md or docs/agents-core.md
|
|
```
|
|
|
|
`skills/*/SKILL.md` and `docs/agents-core.md` are the single source; `AGENTS.md`, `GEMINI.md`, the plugin manifests,
|
|
`.github/copilot-instructions.md` and `.cursor/rules/storebound.mdc` are generated and committed, and CI fails when
|
|
they are stale.
|
|
|
|
</details>
|
|
|
|
## Thanks and idea sources
|
|
|
|
Some skills were sparked by other people's open work. I wrote my own text; the ideas deserve the credit:
|
|
|
|
- [rorkai/app-store-connect-cli-skills](https://github.com/rorkai/app-store-connect-cli-skills) (MIT): App Store Connect
|
|
workflow ideas.
|
|
- [rshankras/claude-code-apple-skills](https://github.com/rshankras/claude-code-apple-skills) (MIT): Apple platform and
|
|
App Store topic ideas.
|
|
|
|
Built with Bun and plain `fetch` against the Google Play Developer API v3, the Play Developer Reporting API and the App
|
|
Store Connect API. Screenshot
|
|
fonts come from Google Fonts at render time (Noto, Paytone One, Montserrat, Inter; SIL Open Font License 1.1). Not affiliated
|
|
with Google or Apple.
|
|
|
|
## License
|
|
|
|
MIT, see [LICENSE](LICENSE). Copyright (c) 2026 CRTkafa.
|