storebound/CHANGELOG.md

1864 lines
175 KiB
Markdown

# Changelog
## Unreleased
- Play managed publishing: `storebound console plan play-publish-changes` (Publishing overview > "Changes ready to publish" > the owner's final "Publish N changes" + its confirm dialog; never-press: Discard changes, Stop managed publishing; `--lookup` lists the ready production releases from the API) and `console verify play-publish-changes` (API: passes when no production release is left ready to publish; or the page text). `play status` (and `--json` `readyToPublish`) prints the ready items and the plan hint, `storebound status` names the plan, and the desktop Releases tab shows the same hint with a button that opens Publishing overview. A release counts as ready to publish only when the public store page positively shows an older version; a 404, a failed or stale page read, a same or unparseable version is never ready (verify says "not determined" instead of a mismatch).
- `apple upload` / `play upload` `--pre-check "<cmd with {file}>"` (config `upload.preCheck`) runs your gate before any byte is sent; a nonzero exit refuses the upload (ship-step exit 1, `PRE_CHECK_FAILED`). `--evidence <path.json>` (`upload.evidence`) writes file, sha256, size, command, exit code, stdout tail, time and version, also on failure, and the result carries it as `preCheck`.
- New `storebound shots copy translate --to de,es` fills store/shots/copy.json per language (agent translation or `--translate "<cmd>"`), machine-translated until `shots copy reviewed`, caption limits respected; `shots copy check`; `check` / `shots render` name this command for a missing language.
- `apple game-center --locale-map en=en-GB` (config `games.localeMap`) maps a config language to the live Game Center locale; the dry run suggests it when the live Game Center has exactly one variant of the language.
- `play games`: a 403 SERVICE_DISABLED / accessNotConfigured names the Google Play Game Services Publishing API, the enable URL for the project and `console plan play-games-services`.
- `apple build --platform macos --tauri <src-tauri> [--path-free] [--unsigned]`: Mac App Store build of a Tauri app. Generated sandbox entitlements, Mac App Store profile (installed, `--profile` or from the API), Apple Distribution / Mac Installer Distribution identities, `tauri build --bundles app`, checks of the .app, `productbuild` to a signed .pkg, `pkgutil --check-signature`; `--path-free` remaps local paths in rustc and fails on any leak of the home folder or user name. `apple upload <pkg> --platform macos` uploads it (platform MAC_OS). New certificate types MAC_APP_DISTRIBUTION and MAC_INSTALLER_DISTRIBUTION. Pitfall 119.
- Desktop dev update channel: Settings > Updates offers Stable (the npm releases) and Dev (every push to main). Each push to main builds Windows and Linux bundles stamped `<next patch>-dev.<UTC commit time>` (above the current stable, below the next one); the VDS signs them and serves `storebound.dev/desktop/dev/latest.json` (newest 5 kept). The app checks its channel at start, every 15 minutes, after sleep and right after a channel switch, installs a strictly newer build quietly (deferred while the window is in use), never downgrades (dev back to stable waits for a newer stable), rewrites its logon autostart on every start, and the tray card shows version and channel. No macOS dev builds (Developer ID signing needs the Mac's GUI session).
- `shots` full-bleed layout: `shots.layout: "bleed"` fills the image with the app's own frame (no device, no background) and puts the scene title as a short corner caption (`corner`, `captionScale`, `captionWidth`, `captionInset` per scene) legible through a text shadow and soft glow only; the Play feature graphic shows the logo or `shots.feature.title`. Frames are captured per size at its own pixels in `<frames>/<lang>/<size>/<name>.png` (feature: `<lang>/feature/`), including the `mac` and `chromebook` sizes; `shots check` / `capture --source folder` verify aspect and native width. `shots.orientation: "landscape"` (bleed only) renders every portrait size landscape (stores accept it). Templates `template-bleed.html`, `feature-bleed.html`.
- `storebound desktop`: "opened" now also works when a hidden tray copy is already running. On Windows it prints "opened" only when a process of the installed path has a visible window (the running copy shows it through the single-instance handoff); on macOS / Linux a visible start or a handoff to the running copy counts, otherwise it says only a tray copy runs. The graceful stop waits 3 s on Windows (closing a tray app only hides it), then stops by PID.
## 0.8.47 · 2026-10-23
- `storebound desktop`: an update no longer reports "installed" while the old app stays. It stops the running app first (exact install path only, graceful close, then by PID), checks the installed exe's own version (Windows PE version resource instead of the registry entry; Info.plist on macOS) and fails with what it found, and prints "opened" only when a visible (not `--hidden`) process started from the installed path, otherwise it names the app's log.
- `play products` / `play subscriptions`: regions outside Play's min / max (a 649.99 USD product in KR: "must be between ₩70 and ₩600,000") are flagged in the dry run (old -> new) and clamped to the bound on write with a warning; `--skip-out-of-range-regions` leaves them out. Limits Play reports in a 400 are learned (`.storebound/play-price-limits.json`) and the write is retried. A product or subscription that fails no longer stops the others: per-product results, exit 1 at the end.
- `apple iap` / `apple subscriptions`: edits to approved (ACTIVE) localizations, which App Store Connect refuses with 409 UNMODIFIABLE, are marked "not editable now" in the dry run and skipped with a warning on write; prices and the other changes still apply. One IAP failing no longer stops the rest (results at the end, exit 1).
## 0.8.46 · 2026-10-23
- `storebound watch` has an `ingest` sink (`--notify ingest`, `ingest` in ~/.storebound/watch.json): a JSON POST per event to a push ingest endpoint with a Bearer token read from a file at send time (refused when group or world readable), 8 KiB body cap, 60 sends a minute, one retry on 5xx; `watch test` covers it.
- Ship-step contract for release pipelines (docs/ship-step.md): `play build | upload | release`, `apple build | upload | submit | resubmit` exit 0 ok, 1 failure, 2 usage, 3 needs the owner (with `error.code` and the console deep link), 4 store refused, 7 nothing to do; `--json` ends with exactly one result line on stdout, `--result-file` writes it atomically (also on failure), `--progress json` gives one JSON line per phase on stderr.
- `--env-file <path>` loads KEY=VALUE secrets before the credentials resolve (POSIX: mode 600 or stricter) and every known secret value is scrubbed from stdout, stderr and the result file; `--version-from version.json` takes the version and build ({version, build}, {versionName, versionCode}, {marketing, build}).
- Re-runs never upload twice: `play upload` with a versionCode Play has and `apple upload` with a build number App Store Connect has end with exit 7 and what is there; `play release` with the same release and `apple submit` of an already submitted build too.
- `play upload --track production` (the pipeline's update step; `--status draft` keeps it for the Console) and `--version-code N` to pick the AAB.
- New `storebound status [--app <name|store id|folder>] [--json]`: per platform the live and next version, build, track, review state and rollout %, read-only. `release status --app` also takes a folder or a store id.
- Desktop: every read goes through one registry: one store call per read however many buttons are clicked, overlapping buttons (Refresh all, card refresh, Check again, Import, Test) are busy and disabled together, a read just finished is not repeated, and all reads share the scheduler's rate limits and backoff.
- Desktop: tray mini card on left click (rejections, releases moving, crash alerts, recent low reviews, refresh, quick actions); right click keeps the menu.
- `apple build` / `apple upload`: `--allow-version-mismatch` is accepted (it was missing from the flag list); 1.0 and 1.0.0 count as the same version train, as on App Store Connect.
- Desktop: custom context menus everywhere (apps, releases, art, reviews, checks, fields, products, accounts, folders) with keyboard navigation, Shift+F10 and submenus; the WebView2 browser menu and browser shortcuts are off.
- Desktop: each app shows once again (a folder project no longer appears a second time as a store-only app); a store app with a folder's bundle id or package is never listed separately.
- Desktop UX pass on every screen: app tabs fit the window, slim app header, secondary actions in "…" menus, Ctrl+K lists app actions, Alt+Up/Down switches apps, accounts and folders decluttered.
- Play Console links open the app's own page: developer id per credential profile (`storebound accounts play-developer <profile> <id>`, editable in Studios & accounts), one link builder for desktop, CLI and watch.
- Desktop: tabs in one row with a More menu, one Refresh, reviews keyboard (J/K, R, C, O), Settings and More tools search, sticky first column and compact density for wide tables, styled in-app dry-run preview before every write.
- Checks: "Open guide" opens the matching skill for every finding.
## 0.8.45 · 2026-10-23
- Tagged but not published (the release upload could not reach the server after an IP change); everything ships in the next version.
## 0.8.44 · 2026-10-23
- Tagged but not published (desktop clippy red on the bump commit); everything ships in the next version.
## 0.8.43 · 2026-10-23
- Tagged but not published (CI red: scrub hits in test fixtures, a ship-step test); everything ships in the next version.
## 0.8.42 · 2026-10-23
- Tagged but not published (generated API tables differed on Linux CI); everything ships in the next version.
## 0.8.41 · 2026-10-23
- Tagged but not published (a generated API table was stale in CI); everything ships in the next version.
## 0.8.40 · 2026-10-23
- Tagged but not published (the desktop boot check stopped it); everything ships in the next version.
## 0.8.39 · 2026-10-23
- Tagged but not published; everything ships in the next version.
## 0.8.38 · 2026-10-23
- Desktop: the app hero no longer shows a cropped second copy of the app icon.
- `play build` passes the upload key (ANDROID_KEYSTORE, ...) to Gradle as injected signing properties (store file and alias as `-P` arguments, the passwords only through `ORG_GRADLE_PROJECT_*` environment variables, never on argv), so a build file that signs only from a local keystore.properties no longer gives an unsigned AAB in CI (pitfall 107).
- `play build`, `play upload` and `play release` of a local AAB verify it is signed (META-INF entries, `jarsigner -verify` when a JDK exists) and fail with the fix before any upload.
- `apple build --version` and `apple upload` refuse an IPA whose CFBundleShortVersionString is not the intended / editable App Store version; `--allow-version-mismatch` overrides (pitfall 108).
- Play release notes that name iOS, iPhone or the App Store are caught by `storebound check` and `play build`, not only at release time (pitfall 109).
## 0.8.37 · 2026-10-22
- `storebound apps ignore|unignore [--store apple|play]`: hide dormant apps, or one store side of an app, from watch, reads and the desktop (MCP apps_ignore / apps_unignore).
- Reviews: a store read that fails is an error, not an empty "no new reviews" list; the Reviews tab notes Apple API lag and Play's 7-day window (pitfall 106).
- Desktop: live store thumbnails load right away, also in a hidden window.
## 0.8.36 · 2026-10-22
- `storebound watch`: a store-only App Store app added without a bundle id is no longer skipped; the bundle id is read from the API with the app's own key.
## 0.8.35 · 2026-10-22
- Desktop v2 ships (0.8.33 and 0.8.34 stopped at the desktop boot check, which still expected the old markup).
## 0.8.34 · 2026-10-22
- Desktop v2 ships (0.8.33 stopped at the desktop boot check, which still expected the old markup).
## 0.8.33 · 2026-10-22
- **Desktop v2**: every screen redesigned in one language: portfolio home, releases (store cards, stage stepper,
rollout ring), submit checklist, TestFlight, listings as store pages with completeness, framed screenshots with issue
badges, products and price matrix, health tiles, review feed with rating distribution, growth funnel, readiness score
with fix-first, tools, settings, accounts, folders, dependencies and onboarding.
- Desktop: tray with a context menu, autostart, silent auto-update and notifications by event kind (rejections too),
also while the window is hidden.
## 0.8.32 · 2026-10-22
- Node: a `storebound.config.json` (and every store-only app) loads again; it needed an import attribute. Pitfall 105.
## 0.8.31 · 2026-10-22
- **`storebound watch`**: store events for every app on the machine (rejected, approved, live, rollout halted, crash
threshold, low or unanswered reviews), sent to ntfy (phone), Telegram, a webhook or an OS notification; runs as a loop
(`--every 15m`) or `--once`; a PM2 recipe for a server is in docs/watch.md. `storebound events` and the MCP tool
`events_recent` let people and agents read what happened.
- **Desktop notifications that arrive.** The app reads the stores in the background from the tray too (it skipped
every read while hidden, so a rejection went unnoticed), every 15 minutes with backoff. Notifications by kind, each
with a switch: rejected, approved, live, halted, crash / ANR, low-rated reviews; quiet hours and mute for an hour.
Opening the window after a notification lands on that app.
- **Tray:** Open, Read the stores now, an Apps menu with each app's state on both stores, Mute, Check for updates,
Settings, Quit; a tooltip with what is in review and what needs you; a red dot on the icon after a rejection or halt.
- `apple listing` refused by an approved version waiting for a manual release now names the way out:
`storebound apple release --manual-release --version <v>`. `play release --dry` shows the staged rollout share.
## 0.8.30 · 2026-10-22
- **Desktop: a new home.** Store-style app cards (your rendered screenshots, or the icon on its tint), a release
pipeline per store (Build -> Testing -> Review -> Live -> Rollout), the crash curve and one next-step button per app;
below, What changed and Fix first. The table view stays one switch away.
## 0.8.29 · 2026-10-22
- **Existing store items are compared with the config, field by field** (pitfall 104). The dry run shows every
difference (old -> new); texts are sent, money and availability need a flag, fields Apple cannot edit are named.
- `apple iap`: price (`--allow-price-changes`), localization names and descriptions, reference name, review note,
family sharing (reported), territories.
- `apple subscriptions`: prices in the dry run, localizations, group names, reference name, review note, level,
territories; intro offers, period and family sharing reported.
- `play subscriptions`: offer phases and prices, other-regions price, regions no longer sold (all price-gated),
grace period, languages missing from the config kept unless `--prune-languages`.
- `apple appinfo`: app price (`--allow-price-changes`) and territories (removals `--allow-availability-changes`);
`apple review-details --dry` shows old -> new.
- Offer-code and win-back campaigns that differ are named; in-app event attributes patched on drafts; a removed
custom page deep link cleared; config languages without a version localization named.
- `prices diff` (the `catalog diff` report).
## 0.8.28 · 2026-10-22
- Windows: no false "entitlements not referenced by CODE_SIGN_ENTITLEMENTS" warning (paths compared with forward slashes). Pitfall 103.
- **`storebound apps relink`** after a machine move: every "folder not found" entry of the app folder list is pointed at
the same app on this machine (by its path or its app name), ids kept so the desktop keeps its accounts and settings;
`apps folders` says so when entries are missing. `apps add` names an app after its config's app name.
## 0.8.27 · 2026-10-21
- **Desktop: tray app that starts at logon and updates itself.** First run registers a logon entry (`--hidden`): the app
starts silently in the tray; closing the window keeps it there, the tray menu quits. A signed update is checked a
minute after start and every 30 minutes, installed silently, and the app restarts as it was.
- **App Privacy console plan follows today's page** (verified 2026-10-22): on an app published before, Edit opens the
Data Collection question first and the type list ends in Continue; each type's last step is Publish (it goes live,
no draft). Publish stays the owner's unless bypass mode is on.
- **A new Play language in one run.** `play listing` uploads the hi-res icon (and the feature graphic when rendered) for a
language Play does not have yet, in the same edit (Play refused the commit with 403 otherwise). Pitfall 102.
- Script traces skip test files (`*.test.*`, `*.spec.*`).
- **CI-friendly Play output.** `storebound aab <file.aab> --json` reads packageName, versionCode, versionName, SDKs and
sha256 offline (no config, no upload); `play release ... --json` prints {versionCode, sha256, name, tracks[{track,
status, userFraction}]}. `play status --json` already lists each track's releases.
- **`apple build` stops at once over SSH** (codesign fails there with errSecInternalComponent) and names how to run it
in the logged-in session; relative `ASC_P8` / `PLAY_SERVICE_ACCOUNT` paths are read from the project folder and
xcodebuild gets an absolute key path. Pitfall 101.
## 0.8.26 · 2026-10-21
- **Bypass mode (owner opt-in, default off).** `storebound settings bypass on` (or "y" in `storebound init`) lets the AI
agent press the owner-only buttons too: MCP `owner_step` (submit, release, rollout, testers, replies, prices, ...;
dry run first), owner-only `storebound api` operations with `--yes --confirm <operationId>`, console plans'
`buttons.final`. Every action is logged to `.storebound/owner-actions.log`; `bypass off` restores the old behaviour.
docs/bypass.md.
- **Build dependencies in `doctor` and the desktop Dependencies screen.** JDK (with the version the project needs from
the Android Gradle plugin / jvmToolchain, "too old" when below it), Android SDK command-line tools, CocoaPods, Node.js,
Bun and bundletool join ffmpeg, Chrome, Xcode, adb and git. `storebound doctor --install <tool> --yes` runs this OS's
package-manager command (winget / brew); sudo and admin commands are printed, never run.
- **A livelier terminal.** At a terminal, lines get colors and status emoji (✅ ⚠️ ❌ 📦 🚀 🧪) and every wait ("waiting
for ...", uploads, downloads, progress) is one live spinner line with the elapsed time. Piped, CI, `NO_COLOR`,
`TERM=dumb`, `--json` and MCP keep the plain lines exactly as before.
- **MCP `console_verify` reads multi-line page text whole** (it kept only the first line on Windows), and long JSON
output such as `console_plan` keeps its start and names a file with the whole output. Pitfall 100.
- **Windows builds.** `play build` runs `gradlew.bat` through `cmd /c` (it was looked up on PATH only) and takes
`C:/...` keystore paths as absolute. Pitfall 98.
- **`check` verifies credential files.** Every credential path that does not exist on this machine and an upload keystore
the stored password / alias do not open (keytool) are named by field. Pitfall 99.
- `play build` / `apple build` write `<out>/latest.json` with the AAB / IPA path for CI; CI install from a git clone documented.
- **Scripts that call the store APIs themselves are named.** `androidpublisher.googleapis.com`, the googleapis
androidpublisher client, `api.appstoreconnect.apple.com`, altool and Transporter in CI files and scripts: `check` and
`release status` warn, `scan` reports `direct-store-api`, each with file:line and the storebound command. Pitfall 97.
## 0.8.25 · 2026-10-21
- **fastlane left in a project is named.** `check` and `release status` warn and `scan` reports `fastlane-upload`
when a Fastfile / Appfile or a CI file or script still runs `fastlane supply`, `deliver` or `pilot`, with the
file:line and the way over (`storebound import fastlane`). Pitfall 96.
## 0.8.24 · 2026-10-21
- **Windows: the MCP server starts.** `storebound install` writes `cmd /c npx -y storebound@<version> mcp` (or
`bun <toolchain>/cli/index.ts mcp`) on Windows, where `npx.cmd` and the sh launcher cannot be spawned. Pitfall 94.
- **`storebound desktop` no longer waits forever on a stalled download.** Progress every 10 %, and an error after
60 s without data. Pitfall 95.
- Windows checkouts keep LF line endings (`.gitattributes`); README badges: live npm version, real skill count.
## 0.8.23 · 2026-10-21
- **`apple submit --yes` submits a draft prepared with `--no-submit`.** The second step of the two-step flow
ended with "nothing to submit" when the draft already held the version and every item; it now sends the submit
(same "not ready yet" retry). `iap-submit` the same for a draft that holds the asked-for items. Pitfall 90.
## 0.8.22 · 2026-10-20
- **`apple submit` / `resubmit` switch Game Center on first.** A build with the Game Center entitlement and an app
without a Game Center detail was refused with 409 STATE_ERROR.BUILD_INDICATES_GAME_CENTER_ENABLED; submit,
resubmit and `apple signing ensure` now create `gameCenterDetails` and the version's `gameCenterAppVersions` first
(dry run shows it), and `validate` blocks while either is missing. Pitfall 89.
- **`apple testflight distribute` creates missing configured groups first** (only the asked-for ones, never removes
one); tester invites stay the owner's.
- **Console plans follow the live pages (2026-10-21 run).** Play health: "My app does not have any health features"
(verify accepts it, "Other" stays a fallback); Sign in details: "Is any part of your app restricted?" Yes / No and
"Add details" (old labels kept as `altLabels`); finance: the boxes on step 1, step 2 information only; App Privacy:
the "Additional Setup Required" OK that opens the first type's setup by itself is in the steps and UI facts.
- **Save after every page.** Every Play plan step that answers a page with a Save next to Next now says Save, wait
for "Your changes have been saved", then Next; the rule line says it too.
- **IARC by exact labels.** "Purchases of digital goods" is ticked after Yes on "Digital purchases" (the loot-box and
trading follow-ups show only then); the in-page helper matches exact labels and questions and refuses patterns,
radios without their question and blanket group answers.
- **`console verify` reads input states.** The Play declaration plans' `verify.capture` returns every radio /
checkbox with aria-checked / checked / aria-selected as JSON; `console verify <form> --from <file|->` takes it.
- **`apple review-details` finds the contact** in parent folders' `.storebound/credentials.json` and the credential
profile, under more key shapes, and lists every file and key checked when there is none.
- **One-line errors for missing required values:** `apple listing` without `--version` says "--version <x.y.z> is
required (storebound apple status shows the editable version)"; the same for privacy-manifest `--ios`, adhoc
export `--archive`, edit `--ops` and resubmit `--build`. A flag given without a value counts as missing.
- **Every CLI command is reachable in the desktop app.** A new **More tools** tab gives each command without a
screen of its own (play build / upload / share / app-id, apple build / upload / signing / adhoc, bundle, IAP review,
Header and Search Results, custom product pages, in-app events, pre-orders, what's new, listing translate / check,
ASO rank / competitors / suggest, captures, fonts, JPEG copies, triage, originality, privacy manifest, ads scan,
Apple Ads reports, web, RevenueCat diff, PPP sheet, workflows, install for an AI tool) one typed form with the
command's own options, built from the method's schema; never a free-text shell. Reads run there; writes show the
dry run, then the native dialog, then run with the confirm token. Owner-only steps (offer codes, win-back,
promoted enable, pre-order enable / end, custom page and event submit, TestFlight expire, Apple Ads changes,
RevenueCat sync, shots login) are listed with their terminal command to copy. New desktop methods in `serve`:
`play.build`, `play.upload`, `play.share`, `play.app_id`, `apple.build`, `apple.upload`, `apple.signing_ensure`,
`apple.adhoc_register`, `apple.adhoc_export`, `shots.fonts`, `shots.jpeg`, `agent.install`.
- **docs/desktop-coverage.md** (generated by `bun tools/gen-desktop-coverage.ts` from `cli/lib/desktop-coverage.ts`):
every command with its window place and methods; 92 commands, 88 covered, 4 not for the desktop (`mcp`, `serve`,
`desktop`, `api`). `tests/desktop-coverage.test.ts` fails when a command has no place, names a method the window
never calls, or is marked not-for-desktop without being on the short list.
- **Game Center and Play Games Services.** A `games` config section (switches per store and one shared list of
leaderboards and achievements). `storebound apple game-center` switches Game Center on for the app and the version
and upserts leaderboards, achievements and their localizations by vendor id; `apple signing ensure` adds the
GAME_CENTER capability. `storebound play games` upserts the Play drafts through the Games Configuration API, and
`console plan play-games-services` walks an agent through the games project, the Android credential with the app
signing SHA-1, the web OAuth client and the owner's Publish. `scan`, `check` and `validate` compare the entitlement,
the App ID and the config, the games_APP_ID resource with the Gradle dependency and the config, and remind the User
ID declarations; `games status`, four MCP tools and a Game services card in the desktop's App info tab. Skill
game-services, pitfall 88.
## 0.8.21 · 2026-10-20
- **`shots record --source web` films at device pixels.** Chromium's screencast always sent frames at the page's CSS
size (430x932 for a 430x932 @2x frame), so a 1080x1920 preview was a 2.5x upscale. The recorder now takes
`Page.captureScreenshot` frames with the clip scaled to the frame's dpr (860x1864 there; long side capped at 1920,
the tallest store video), keeps 3 requests in flight (~45-55 frames/s) and joins them at their real timings as
before. It warns when the first frame is smaller than asked or the rate drops under 20 frames/s.
## 0.8.20 · 2026-10-20
- **Tooling: the desktop updater signatures of the Linux and Windows installers are made on the VDS.** The release
no longer downloads them to the Mac: the VDS fetches them from the Forgejo release, checks their SHA-256 against the
candidate, signs them with its copy of the updater key and sends back only the .sig texts, which the Mac verifies
against the app's public key before publishing. `desktop-release.ts vds-key` installs or rotates that key;
`release.ts --sign-local` keeps the old path.
## 0.8.19 · 2026-10-20
- **`shots capture` / `render` find Playwright's chromium in a project's CommonJS playwright** whether it comes on the
namespace or under `default`; when the module has no `chromium.launch`, the error names the resolved file and its
exports (was "Cannot read properties of undefined (reading 'launch')").
- **Heavy app-style headlines again:** `shots fonts` downloads the weights the templates use (the full `wght` axis of a
variable Google family, else the used range, else the static weights), so a family like Plus Jakarta Sans no longer
falls back to its thin 400 instance. `shots fonts` warns about weights a family or a local file (`{ weight }`) lacks;
`shots check` warns per image when a title was drawn at another weight (`font-weight`).
- **palette.bg as a string:** one colour (flat) or a CSS gradient string now renders as given instead of a white
background; anything else fails config validation ("palette.bg must be a colour, a gradient or an array of 2-3
colours"). `shots check` warns when the title ink has low contrast against the pixels behind it (`contrast`).
- **Photo backgrounds:** `palette.image` (+ `overlay`) and `shots.feature.background` put a cover-cropped photo under a
dark overlay (the Play feature graphic, optionally the screenshots); `shots check` checks the file and its size.
## 0.8.18 · 2026-10-20
- **No more "Reparsing as ES module" warning** for a `.ts` config in a CommonJS project (Node's
MODULE_TYPELESS_PACKAGE_JSON); the app does not need `"type": "module"`.
- **Capture sign-in keeps App Review's account apart:** the docs use `captureAuth` for a local demo sign-in, and
`check` notes a capture against localhost that uses `reviewDemoAccount` (the account `apple review-details` sends).
## 0.8.17 · 2026-10-20
- `shots capture --source web` can sign in first: `shots.capture.auth` is a sign-in request (run once per browser
context, cookies kept; body and headers use `${cred:NAME}` from `.storebound/credentials.json` or `${env:NAME}`,
literal secrets are refused, a failed status stops with the status only) or a `storageState` file. `storebound shots
login` opens a fresh visible browser for a demo sign-in by hand and saves `.storebound/auth.json` (0600, counts
only). A shot that lands on a sign-in page warns "session expired? run storebound shots login again". Secrets and
cookie values are masked in logs and the capture report.
## 0.8.16 · 2026-10-20
- `apps folders add [<folder>]` adds the folder (it listed and added nothing); a template config is added with a note,
a folder without a config is refused with the reason and the fix (pitfall 81).
- MCP: the plugin's `.mcp.json` no longer spawns a plugin path that may be gone (ENOENT, CONNECTION_CLOSED): it falls
back to `storebound` on PATH, then `npx -y storebound@latest`; `mcp` and `serve` skip the self-update (pitfall 82).
- Store build minimums in one dated table (`cli/lib/store-minimums.ts`): `play upload` / `play release` refuse an AAB
below Play's target API level (36 since 2026-08-31) and `apple upload` an IPA built below Xcode 26 / the 26 SDKs
(since 2026-04-28), with the file to change; `scan` / `validate` report the same (`play-target-sdk`, `apple-min-sdk`)
(pitfall 83).
## 0.8.15 · 2026-10-19
- **Mac App Store screenshots.** `apple.mac: true` adds the `mac` size (2880 x 1800; `shots check` takes exactly
Apple's four 16:10 sizes, no alpha) and `apple screenshots --platform macos` uploads it as `APP_DESKTOP` to the macOS
version. `validate` notes a macOS version without Mac screenshots. Art pull maps `APP_DESKTOP` to `mac/`.
- **Landscape layout** in both templates: copy column plus the frame in a Mac window or bare (`shots.landscape`), for
Mac, Play Chromebook (`play.chromebook`; no API type, `play images` links the console) and, with
`shots.landscape.playTablets`, Play's 7" / 10" tablet slots.
- **Safe margins:** text and logo keep 7 % of the width left / right and 5 % of the height at the top in every
template (feature graphics too); `shots check` fails boxes under 6 % / 4 %, measured from the render's layout data.
- **Contact sheet:** the `sheet.png` wait scales with the images, sizes and machine load, with a heartbeat, instead of
a flat 30 s.
- iPhone Duo skip message says plainly that the API has no Duo type and names the console page.
## 0.8.14 · 2026-10-19
- **Header and Search Results: upload, add for review, place once approved.** `storebound apple creative-assets`
uploads each file once into the Asset Library (MD5 upsert: a rejected file or one that failed processing twice is
not uploaded again, an archived copy is unarchived), and places it on the version as soon as Apple approved it: the
version page's slots offer only approved assets. Until then every locale and placement says "uploaded <date>,
waiting for Apple's approval; run `storebound apple creative-assets` again later", and a re-run places it (an
approved old asset stays shown meanwhile). `--add-for-review` adds the unreviewed assets to the app's draft review
submission (never submits; Submit for Review stays the owner's); `--verify` (MCP `apple_creative_assets_verify`)
reads every slot back read-only with the same waiting state, and `validate` / the report list waiting files. A live
app without an editable version uses the live version; if App Store Connect refuses a placement there, the run says
so, leaves the live page unchanged (the new placement goes first, the old one is removed only after Apple accepted
it), links the live version page and does not retry the other live slots. Art without words needs no copy per
locale: `<renders>/creative/header.png` / `search.*` fill every language without its own file, uploaded once and
placed per locale (the dry run counts files, not slots). Pitfall 76 rewritten.
- **iPhone Duo.** Apple's published App Store Connect API spec (re-checked: still the 2026-10-06 file) has no Duo
screenshot or preview type, so `apple screenshots` keeps skipping the Duo sets with the console link; the check /
validate note now says the console takes Duo and the API does not yet.
- **macOS (tvOS, visionOS) versions are no longer invisible.** `apple status`, `listing`, `listing pull`, `resubmit`,
`submit`, `release` and `screenshots` take `--platform ios|macos|tvos|visionos` (MCP tools and the desktop's owner
actions: `platform`); without it they use the platform that has the version they need (a rejected one, an
UNRESOLVED_ISSUES submission, an editable one) and refuse, naming both, when several platforms do. Every version
and review submission lookup is per platform, so `resubmit` finds the macOS submission back from App Review and
`listing --version 0.8.7` works on the rejected macOS 0.8.7 while the iOS 0.8.7 is live. `apple status`, `validate`,
`release status`, the report and the desktop label every version with its platform; validate and release status warn
about another platform's editable or rejected version. `whats-new apply --platform` names it in the push command.
Pitfall 79.
- **Other platforms in store text are refused (both stores).** One checker (`cli/lib/cross-store.ts`) finds Android,
Google Play (Games), Play Store, Galaxy Store, Amazon Appstore, Steam, Microsoft Store, AppGallery (and localized
forms) in App Store text (Guideline 2.3.10) and iOS, iPhone, iPad, App Store, Apple Arcade, Game Center, TestFlight in
Play text (Metadata policy), per locale, field and line with a fix. `apple listing`, `play listing`, `play release` /
`play upload` release notes, `listing set`, `whats-new check` / `apply`, `storebound edit` and the desktop editor
refuse before anything is sent (dry runs show the hits); `check` and `validate` report blockers; `scan` reports
`other-platforms` (2.3.10) and `play-other-platforms` (Play: Metadata). A functional mention stays only through the
owner's `listing.allowPlatformMentions: [{ store, text, reason }]`.
- **Per-store release notes.** Lines tagged `[apple]` / `[play]` (or a `[play]` line heading a block) in a shared notes
source go only into that store's notes; the draft leaves untagged other-platform lines out of the wrong store and
says so; each store's limit counts its own notes.
- **`storebound api` writes piped or redirected are refused loudly.** `--yes` with stdout not a TTY now exits 1 with
"refused: writes through `storebound api` need an interactive terminal ... Nothing was sent." on stderr instead of
a silent dry run.
## 0.8.13 · 2026-10-18
- **iPhone Duo screenshots.** `apple.iphoneDuo: true` adds the foldable's outer (1398 x 2034) and inner (2007 x 2853)
sets, portrait or landscape, to `shots render`, `shots check` (exact sizes, no alpha) and the gallery. App Store
Connect API 4.5.1 has no Duo `ScreenshotDisplayType`, so `apple screenshots` skips them with a message and the deep
link to the version page's App Previews and Screenshots tab (one constant, `IPHONE_DUO_DISPLAY`, takes the enum
names once Apple adds them). `check` / `validate` note a missing Duo set: optional now, required from April 2027 for
iOS 27.1 SDK builds (a warning after that date, never a blocker), and point Play to the tablet sets (no foldable slot).
- **Required iPhone class follows the console.** `validate` now requires "iPhone with Dynamic Island (medium display)"
(`APP_IPHONE_61`) per locale, met by a 6.9" or 6.5" set that App Store Connect scales down (an ok row, not a
warning). Labels match the console: "iPhone with Dynamic Island (medium display)", "iPhone Duo", "iPad 13" display".
- **App Store Header and Search Results.** `storebound apple creative-assets [--dry]` puts a product page header
(3840 x 1646, 21:9) and a search results asset (3:2, 1920 x 1280 to 3840 x 2560), image or video (5-30 s, 30/60
fps), per locale from `<renders>/<lang>/creative/`, or one universal 5244 x 2950 PNG, into the App Asset Library and
selects them (`PRODUCT_PAGE_HEADER_ASSET`, `APP_STORE_SEARCH_RESULTS_ASSET`) once Apple approved them; MD5 upsert,
read back. Local checks: size, no alpha, video length / fps / container (ffprobe, else skipped with a note).
`check` and `validate` note when there is none (Play: the feature graphic and promo video are the counterparts).
MCP `apple_creative_assets`; config `apple.creativeAssets`; pitfall 76.
## 0.8.12 · 2026-10-17
- **Shareable readiness report.** `storebound report` puts `scan`, `check` and `validate` into one self-contained HTML
page (`.storebound/reports/readiness-<date>.html`, inline CSS, no scripts, light / dark, print-friendly) and opens it
in the browser: a verdict per store ("Ready to submit", "N blockers", "Not checked" with the reason when a store key
is missing), findings per store by severity with guideline, evidence, fix and an Open in App Store Connect / Play
Console button, and the questions only the owner can answer as a checklist. Credentials, key ids, emails and the
review demo account are masked. `--md`, `--json`, `--no-open`, `--offline`, `--out`. The desktop's Checks tab has a
Report button (same code path) and its Markdown export uses the same renderer; MCP gets a read-only `report` tool.
- **Remote config and remote paywalls (App Review 2.3.1, Play Deceptive behavior).** `storebound scan` (and
`validate`; `check` as a warning) reports `remote-features` for both stores when the app ships a remote config /
feature flag SDK (Firebase Remote Config, Statsig, LaunchDarkly, GrowthBook, Optimizely, PostHog feature flags,
Unleash, ConfigCat, Amplitude Experiment, Split) or a remote paywall (RevenueCat Paywalls, Superwall, Adapty,
Qonversion, Purchasely, Apphud) and the review notes do not mention it. Warn only: storebound never writes the note;
the config templates carry a commented example line. Code obfuscators are a low note, minify alone nothing
(store-pitfalls 74).
## 0.8.11 · 2026-10-17
- **Guest-play apps rejected as a login wall (Play).** `reviewAccess.instructions` with `signInRequired: false` holds
guest steps: `storebound check` warns when a Play app has a first-run gate (name entry, code / transfer entry) or
in-app purchases and no steps, and the `play-sign-in-details` plan then declares one entry without credentials,
the dialog's required full-access box as the owner's step (store-pitfalls 73).
## 0.8.10 · 2026-10-15
- **Neutral names only.** Docs, pitfalls, tests and comments use example names instead of the projects that reported
issues; the history was rewritten the same way. Older npm versions are withdrawn.
- **Releases:** a candidate whose build jobs were cancelled by a newer push says so and gives the tag move and
`--resume` line.
## 0.8.9 · 2026-10-15
- **Play edits Play will not send for review are committed anyway.** After a rejection, with other pending changes or
managed publishing, Play answers validate / commit with 400 "Changes cannot be sent for review automatically. Please
set the query parameter changesNotSentForReview to true". storebound now commits the same edit again (edits.commit only: validate refuses the parameter with 400 "Unknown name")
with that parameter (a dry run validates that way and says the real run will commit so), then prints "committed;
Play did not send it for review: press Send changes for review in Publishing overview: <link>" (the owner's button).
`--not-sent-for-review` on `play release`, `upload`, `rollout`, `listing` and `images` (MCP / desktop
`not_sent_for_review`) sets it from the start. The API cannot read that changes wait to be sent, so storebound records
its commit in `.storebound/` and `play status` shows "changes committed <time>, waiting for Send changes for review"
(`--review-sent` forgets it). Pitfall 72.
## 0.8.8 · 2026-10-15
- **Console pages as absolute deep links.** Every finding, fix hint, error and plan step that points to an App Store
Connect or Play Console page now prints the readable path plus the absolute URL of that page for the app
(`cli/lib/console-links.ts`; apple.appId, play.developerId and play.appId pick the app, the app list otherwise).
validate, scan, declarations and release plan JSON carry `url`, and the desktop shows it as an Open button. A test
fails on a plain console direction without a URL; AGENTS.md rule 11 and pitfall 71 tell agents the same.
## 0.8.7 · 2026-10-15
- **First in-app purchases left out of a review are caught.** Apple reviews the first in-app purchase of a type (and
the first subscription of a group) only together with an app version. `validate` and `release plan` now block when
one that never passed review is not in the open review submission, and `apple submit` refuses to leave one out;
`apple status`, `release status` and the desktop warn when a version waits for or is in review without it ("not
sellable after approval; add it with the next version, or withdraw the submission and resubmit with it").
`apple iap-review` says "ready, not submitted yet" instead of calling READY_TO_SUBMIT complete.
## 0.8.6 · 2026-10-14
- **Desktop: Fix with AI** on scan findings that need a code change (NSAllowsArbitraryLoads in Info.plist, a missing
foregroundServiceType, purpose strings, target SDK, ...). The app runs your local Claude Code headless in the app
folder with file tools only (no commands, no network, no git push), streams its progress, stops after 10 minutes or
on Cancel, and shows the diff with Keep and Undo (exactly the files it changed), then rescans. It refuses while the
folder has uncommitted files. Without Claude Code installed the button copies the prompt instead (Copy for AI).
- **Desktop: a Fix button on findings** that storebound can fix itself. Scan and validate findings carry a
machine-readable `action` (`--json`); the Checks tab runs it as a write with its dry run in the native dialog
before anything changes. privacy-labels writes the detected answers into the config (`scan --write-only`, the
dialog shows the config diff); missing Play listings, a missing App Store localization or copyright and the App
Review contact push the listing files / config to the store (text only). Nothing submits, releases or publishes.
- **Desktop:** long findings (a file path in a build folder) wrap inside the Store readiness card instead of pushing
the store chips out of it; the release timeline's rail no longer breaks after the current step; the rollout bar
stays under its stage.
- **`scan`: function words of the listing languages** (German "eine", "der", French "les", Turkish "bir", ...) no
longer count as repeated keywords in the Play full description.
- **`import type { ShipConfig } from 'storebound'` type-checks outside Bun.** The package ships declarations
for ES module and CommonJS projects (`dist/types`, `dist/types-cjs`) and a JS entry for Node; Bun keeps the .ts source. 0.8.5 named only
`cli/lib/config.ts`, so tsc (`next build`, vue-tsc, ts-loader, `tsc --noEmit`) failed on a scaffolded
storebound.config.ts and a working Next app stopped building. A test packs the entry and type-checks the template
in both project types under bundler, node16, nodenext and node10 with no @types/node and skipLibCheck off, and loads it with import() and require() under Node. Pitfall 69.
- **`check` fails on the scaffolder's own placeholders**: "2026 Example Studio" and example.com / .org / .net URLs and
addresses in `app`, `accounts` and `reviewAccess` would reach the stores as the app's copyright, contact and legal
pages.
## 0.8.5 · 2026-10-14
Releases take one command and minutes: every push to main is a release candidate (full CI, the acceptance matrix and
the Linux / Windows installers stored per commit), a version tag only promotes it, and `bun tools/release.ts` does
the rest on the Mac.
- **A Chrome storebound starts never outlives its run.** The console window's Chrome is ended on exit, closed
gracefully on Ctrl+C / SIGTERM / SIGHUP, and ended by a watchdog when the run dies in any way (SIGKILL included).
Every start first ends headless storebound Chromes earlier runs left behind (an orphan kept the owner's own Chrome
from opening from the macOS Dock) and names orphaned windows. Headless runs on macOS prefer Chromium / Chrome for
Testing over Google Chrome.app. `console fill` closes the window on an error too. Pitfall 68.
- **Every push to main is a release candidate.** ci.yml runs the full CI, the acceptance matrix (Linux + Windows x
Node 20 / 22 / Bun) and the installer builds in parallel and stores the green result as the draft `rc-<sha>`.
A version-only bump commit (proven file by file) reuses its parent's CI and matrix and rebuilds only the
installers. A `v*` tag promotes the candidate (no rebuild, no retest) and fails at once on a red commit.
- **`bun tools/release.ts <version> --yes`:** bump and CHANGELOG cut, fast gate, commit, push, tag; the Mac build and
notarization in parallel with Forgejo; macOS acceptance cached per CLI key; add, verify, publish (each Mac file
crosses the owner's uplink once); /Applications/storebound.app updated; the npm commands printed. `--rehearsal`
runs it all as a throwaway prerelease and deletes it afterwards.
## 0.8.4 · 2026-10-14
Existing products' prices never change without `--allow-price-changes`; PPP pricing compares like with like and keeps
high-income markets at par. `--json` prints only JSON. Automatic signing creates no profiles, and capabilities are
verified against the App ID, the config and the build. `check` requires a copyright; What's new drafts come from
release notes and flag jargon.
- **`apple signing ensure` with automatic signing creates no profiles or certificates.** With `signing.style:
automatic` (the default) it only adds missing capabilities to the App ID; Xcode owns certificates and profiles, and
the dry run says so. Provisioning profiles storebound made that nothing uses are listed by name with the exact
delete command (never run by storebound).
- **Capability writes are read back.** After adding a capability, `ensure` reads the App ID again (a few tries,
about 45 s at most) and fails with the API's answers when one is missing, instead of reporting success.
- **Capabilities compared in three places.** `apple status`, `apple signing status` / `ensure --dry` and `validate`
compare the App ID, `apple.signing.capabilities` and the iOS project's `.entitlements` (push, Game Center,
associated domains, Sign in with Apple, iCloud, app groups, ...; also an entitlements file not set in
CODE_SIGN_ENTITLEMENTS) and name what is missing where.
- **No false alarm for entitlements on another branch.** A checkout without `.entitlements` is a warning about this
checkout only ("built from another branch?"), naming the current branch and the local branches / worktrees that
have one; it never claims the build lacks them. With a built IPA in build/ios/export, its real entitlements
(codesign, or the Mach-O signature when codesign is missing) are compared as "uploaded build N"; without one the
output says the App Store Connect API does not expose a build's entitlements.
- **`storebound api` writes outside a terminal** say that they need an interactive terminal by design and name the
dedicated command for common operations (bundle id capabilities: `apple signing ensure`, IAPs, listing, ...).
- **`--json` prints only JSON on stdout.** Every command run with `--json` sends its log, step and table lines to
stderr (one place: `withJsonOutput` in cli/lib/log.ts, wrapped around every command); the last commands that printed
JSON with console.log use `printJson`. A test runs every command that reads `--json` against the fake stores and
parses its stdout (`prices ppp --json` printed log lines first).
- **PPP prices compare like with like.** `prices ppp` takes the store's own conversion of the US price (VAT included,
today's rate: Play convertRegionPrices, Apple equalizations; offline base x rate x (1 + VAT)) as par and applies the
PPP price level to it. Defaults: only lower prices (`--only-decreases`), only economies with a price level at most
50 % of the US (`--max-level`), World Bank high-income economies keep par (`--include-high-income` to include
them), mainland China at par on Apple and skipped on Play, never above par (`--max-ratio`, `--min-ratio`; the old
`--floor` / `--ceiling` still work). Germany keeps EUR 5.99 for USD 4.99 (was EUR 3.59). A top-10 revenue market
that drops more than 20 % is warned about (from your sales reports, else a static list). Regions that keep their
price stay out of the sheet (`kept` in `--json`).
- **Creating products never changes a live price.** `play products`, `prices import`, `play subscriptions` and `apple
subscriptions` change prices of existing products only with `--allow-price-changes`, in a terminal too: one "y"
covered them before. With the flag they get their own question listing each old -> new; dry runs show a separate
"price changes on existing products (held)" section.
- **`whats-new` drafts from player-facing notes before the git log.** It takes `whatsNew.source`, then
`docs/RELEASE-<version>.md`, then the CHANGELOG.md section of the version (else Unreleased); version from
`--release`, else package.json. Only without one (or with `--from-git`) it drafts from commit messages, and says so.
- **Internal jargon warnings in release notes.** The draft and `whats-new check` name SDK / vendor names, developer
and version-control words, file paths, issue ids and code identifiers, each with a plain alternative; warnings never
block. One list in cli/lib/jargon.ts; `whatsNew.allowWords` keeps a word. The whats-new skill says to write from what
players feel, never from commit messages.
## 0.8.3 · 2026-10-14
Docs: no private account names in the published package. `npx storebound` prompts work on Node again (the [Y/n]
prompt of `storebound desktop` crashed on Windows).
- **`npx storebound` prompts work on Node.** `storebound desktop` (and every [y/N] or type-the-id prompt) crashed on
Node at the question with "console is not async iterable" (Windows also hit a libuv assertion at exit): prompts now
read stdin through `rt.readLine` (node:readline, closed after one line, EOF is "no"). A test keeps Bun-only stdin
iteration out of cli/, and the Windows release job runs the Node prompt smoke on the runner.
- **Release privacy scan of signed macOS builds leaves out Apple code signatures only.** The Developer ID certificate
in a signature names the developer; the .app, DMG and updater bundle are scanned as copies without signatures, and
any hit elsewhere still fails the release.
## 0.8.2 · 2026-10-14
The desktop app edits everything the consoles do (listings, app info, catalog and prices, TestFlight, store art), with
a dry run before every store write, and reads everything on its own within the stores' rate limits. Play status comes
from the live store page (Live / In review / Rolling out); `art pull` takes icons and screenshots from the stores;
errors are clear messages instead of raw API text.
- **Store art of a live app comes from the stores, not a re-render.** `storebound art pull [--store apple|play]
[--write] [--force]` downloads every App Store screenshot set and app preview per locale and display type (the
original size from `imageAsset.templateUrl`), the App Store icon (the newest build's icon at 1024 px, else the
public artwork) and per Play language the icon, feature graphic and phone / 7" / 10" / TV / Wear screenshots (the
original files, `=s0`, checked against Play's sha256) into the store art layout (`store/renders/<lang>/<size>/NN.png`,
`feature/1024x500.*`, the icons at `app-store-1024.png` / `play.icon`). `store/renders/pulled.json` records each
file's sha256, the store, the URL host (never the URL) and the store's own checksum, so `play images` and
`apple screenshots` / `apple previews` see a pulled file as the store's and upload nothing. An upsert: a re-run
changes nothing, a store change updates an unedited pull, and a local file that differs (your own render, an edit
after the pull) is kept and listed unless `--force`. Read-only on the stores. `storebound onboard` runs it too
(`--no-art` skips it). Pulled art never goes into git: when the app folder is in a repo and the renders folder is
not ignored yet, `art pull --write` (and `init` / `onboard`) add it to the repo root .gitignore (e.g. `/store/renders/`).
- **check / validate with pulled art.** Missing render sources (the icon artwork, the logo, screenshot captions) are
notes instead of problems once the art is pulled; pulled files pass the local store checks as the store's own, and
a set the store had nothing for (a Play language on the default language's graphics, a locale without that set) is
not demanded locally (validate reports the store side).
- **Desktop: the live store art.** The Store art tab shows the art that is live on both stores (thumbnails per store,
language and set; same / differs / not pulled per image) with **Pull from stores** (dry run, dialog, then the files),
and store-only apps get the tab read-only. **Replace with a file** (the lightbox, and each scene's frame in the art
editor) copies a picked PNG / JPEG into the app folder after a dry run with the store checks (`art.replace`; native
picker `image_pick`). docs/console-parity.md: 76 rows, 57 complete, 19 with the reason.
- **Parallel Play reads no longer break each other.** Google Play keeps one open edit per app, so two reads of one
app at once (the desktop's auto-read) deleted each other's edits ("This Edit has been deleted") and the Releases tab
then said "not on Google Play yet". Every Play edit of a package now runs under a machine-wide per-package lock
(`~/.storebound/locks`, wait with timeout and heartbeat; other packages stay parallel), and a read whose edit was
deleted is redone once with a new edit (writes are not). A store that failed to read shows "Couldn't read Google
Play" and keeps its last good releases with "couldn't refresh". When the stores' current versions differ, the card
header says "Current · Play 4.1.9 · App Store 4.1.2" and each lane shows its own state ("4.1.2 · Live (4.1.9 not
submitted)"); "Not on this store" only for an app with no record there (pitfall 58).
- **Google Play status is decided, never "maybe".** `play status`, the portfolio and the desktop read the live version
from the public store page ("Version" in About this app): the same version = Live, 404 = In review (managed
publishing: In review / ready to publish), an older one = In review (live: 1.0.0), Rolling out N% / Halted / Draft
from the API. Only a network failure says "Couldn't check the store page", and it keeps the last known answer with
its time. "In review or not yet published" and "Google may still be reviewing" are gone.
- **Desktop reads everything by itself.** No more "Not read yet" and a Read button on every page: on start, when the
window shows again and every 15 minutes while it is open (Settings: auto-read on / off, interval), every app's
status, releases and submission state, TestFlight, catalog, prices, listings, health, reviews, analytics and checks
are read in the background when their saved copy is older than its TTL (5 min to 6 h). Saved reads show at once
with "read 3 min ago"; a failed refresh keeps them with "couldn't refresh: <reason>". One scheduler keeps it inside
the stores' limits: 4 at a time per store, starts spaced per account, slower below 20 % of App Store Connect's
hourly limit (read from `X-Rate-Limit`) and a pause at 100 left, exponential backoff with jitter on 429 /
RESOURCE_EXHAUSTED, the visible app first. "Refresh all" in the toolbar, a refresh icon per card, writes refresh
what they changed, and a status line: "Reading 12 of 48 · App Store limit 3,412/3,600 left this hour".
- **Health: open a top issue.** Full title, type, counts, versionCode range, Android API range, last report, a sample
stack trace (`storebound health --issue <id>` / `--signature <id>`, new read `health_issue`), Open in Play Console,
Copy for AI, and a likely cause for known signatures (`cli/lib/crash-hints.ts`; first entry: the Capacitor
`Bridge.getPermissionStates` NullPointerException from `checkPermissions()` during an activity transition).
- **Google API errors in words, not JSON.** Google's error bodies are parsed centrally (`cli/lib/google-errors.ts`)
into typed findings: `api-disabled` (403 SERVICE_DISABLED: service, Cloud project id, the console page to turn it
on), `no-app-access` (403 PERMISSION_DENIED: which app, the Users and permissions step), `auth-revoked` (401,
invalid_grant) and `quota` (429, RESOURCE_EXHAUSTED). `health`, `analytics` and the health JSON say one line with the
URL ("Crash and ANR data needs the Google Play Developer Reporting API turned on in Google Cloud project <id>. Turn
it on: ..."); the raw text stays in `detail` and the log. The desktop Health tab shows that line with "Turn it on in
Google Cloud" and "Check again", the raw text only in a collapsed Details; the Portfolio shows a Cloud project that
blocks several apps once ("1 Google Cloud project needs an API turned on: affects ..."). Pitfall 57.
- **Findings are results, not errors, in the desktop.** `scan`, `check`, `validate` and `health` exit 1 when they find
problems (by design); the sidecar now reports an `outcome` (ok / findings / declined / failed) and, for a real
failure, the command's own `error` line (stderr's last "error: ..." line). The Checks tab lists the findings instead
of "Exit code 1", findings results are cached, and every place that showed a bare "Exit code N" shows the error line.
- **Play "Live" only when the store page proves it.** The Play Developer API has no review state: a production release
reads `completed` while Google still reviews it. `play status` now checks the public store page (no auth, fast
timeout, cached 5 minutes) and adds `publicPage` to its JSON: page 404 = "Sent to Google (in review or not yet
published)", 200 = live for the first release, an update is "Released to production (Google may still be
reviewing)" unless the page shows its versionName, offline = unknown. New release phases `sent` and `released` in
the desktop Releases tab, Portfolio, `release status | plan` and the CLI JSON. Pitfall 56.
- **One row per release across the stores.** Versions are matched without trailing .0 parts (App Store "1.0" and Play
"1.0.0" are one row and one timeline); each side still shows the store's own string.
## 0.8.1 · 2026-10-13
`apple build` exports each build into its own folder and fails loudly on any export error; `apple upload` refuses a
stale IPA whose build number is already on App Store Connect; xcodebuild runs with Apple's tools first on PATH (a
Homebrew rsync broke exportArchive). The Play feature graphic keeps the whole phone on the canvas.
- **Feature graphic fits the phone.** The Play feature graphic (1024 x 500) cut the phone off at the bottom (the game
template's 7 deg phone spanned about y 24..552). `shots.feature.fit` 'contain' (default) sizes the device so its
rotated outline stays inside with a 24 px margin, in both templates; 'bleed' keeps the old crop on purpose;
`shots.feature.scale` (0.5-1.5) resizes it; the config is validated. `shots render` records each device's measured
box in report.json and `shots check` fails a feature graphic whose device leaves the canvas (warns for a
screenshot's front device off a side or the top). Pitfall 55.
- `apple build` exports each build into `build/ios/export/<version>-<build>/` (cleaned first), runs xcodebuild with
Apple's tool folders first on PATH (a Homebrew rsync broke Xcode's IPA step: "exportArchive Copy failed"), shows the
IDEDistribution log's real error lines and exits non-zero on any export failure (pitfalls 53, 54).
- `apple upload` fails with exit 1 when the IPA's build number is already on App Store Connect (a stale IPA after a
failed build) instead of reporting it VALID; `--allow-existing` re-checks, `--build N` picks that build's IPA. The
release and beta flows use both.
- `storebound doctor` flags a non-Apple rsync first on PATH; desktop publish calls rsync by absolute path without
`--chmod`.
- Site: the download notice says per OS: macOS signed and notarized, Windows unsigned for now (SmartScreen: More info ›
Run anyway), Linux unsigned.
## 0.8.0 · 2026-10-13
One product: the CLI and the desktop app share one version, the store accounts and the app folder list, and
`npx storebound desktop` installs and opens the app. `storebound onboard` brings live apps in without questions the
stores and the code already answer. Consoles without an API are driven by the agent (`console plan` / `verify`).
Plus many fixes from real releases.
- **One version.** The desktop app has the CLI's version from now on; the next release is 0.8.0 for both.
`package.json`'s `version` is the authority (`desktopVersion` is gone); `tools/desktop-version.ts` keeps the plugin
manifests and the app's copies equal (CI, and `tests/desktop-release.test.ts` / `src-tauri/tests/d5.rs` as the
drift tests), `desktop-release.ts publish` refuses a folder of another version, and the release order is the app
first, then `npm publish`. The desktop 0.1.0 release candidate was never published, so nothing public changes.
- **`npx storebound desktop` just works.** The app has this CLI's version: it opens on the current folder. Not
installed: it downloads the app for this system (the CLI's own version, else the newest published), checks size,
SHA-256 and the minisign signature, installs it for this user without admin rights (macOS: from the read-only DMG
into /Applications or ~/Applications, no xattr changes; Windows: the per-user NSIS installer, silent; Linux: the
AppImage in ~/.local/bin with a `.desktop` entry and the `storebound://` handler) and opens it. An older app is
updated the same way (never while it runs), a newer one is opened with a note (downgrade only with `--force`). It
asks once (`[Y/n]`); `--yes` skips the question; without a terminal and `--yes` it prints the command and exits 3.
`--dry` / `--json` change nothing, `--download` only saves the checked installer. The publish script also writes
`desktop/dl/<version>/latest.json` (redeploy `tools/vds/storebound-desktop-publish` on the server).
- **Store accounts, one store.** Credential profiles in `~/.storebound/credentials` are shared by the CLI and the
desktop app: the app writes its keys there (files 0600, folders 0700) and shows the profiles the CLI made; accounts
of earlier desktop builds move from the OS keychain into profiles on first start (ids, studios and app mappings
kept; the keychain entry is deleted after the profile reads back). A removed account's key file goes to
`credentials/.removed/`. Precedence: env, then a profile the project names, then the project's
`.storebound/credentials.json`, then the default profile (the only one, or `default`). A project names its profile
with `account:` in the config, `"account"` in its `.storebound/credentials.json`, or `--account`.
- **`storebound accounts`** lists the profiles (masked) and which one this folder uses; `accounts import` moves the
store keys of a project's `.storebound/credentials.json` into a profile (key files copied to
`~/.storebound/credentials/<name>/`, the same Apple key id or service account e-mail = the same profile, the
keystore and review contact stay in the project, a backup of the project file is kept, safe to run again; `--dry`
first, never prints values); `accounts use <profile>` points a project at one.
- **App folders, one list.** `~/.storebound/projects.json` is the desktop's App folders and the CLI's
`storebound apps folders | add | remove`; `storebound desktop` in an app folder and `ws init` add to it, and `ws`
without a workspace file works over it. The app syncs it at start, when the list is shown and before a
`storebound://open` link, so a folder added in the terminal is selected without a question.
- **Desktop:** a folder whose `package.json` pins another storebound version shows "This project uses storebound X,
the app runs Y". `storebound-desktop --version`. `ws desktop import / export` carry the accounts' profile names.
Onboarding apps that are already live, without owner questions for what the stores and the code prove (lessons
43-52, from setting storebound up on 6 live apps):
- **`storebound onboard [--write]`** (MCP `onboard`, dry run by default). Read-only on both stores, never changes a
store price. Imports the live one-time products and subscriptions of both stores into `products` / `subscriptions`:
ids, type, listings per language, usd = the US price (else Play's own USD price for new regions), Apple's price
schedule base territory as an override when it is not the USA, an item on one store only kept with `platforms`;
`catalog diff` comes out clean. An item with no USD price anywhere is named with its base price, not left out
silently. Fills app.contactEmail / website / supportUrl / privacyPolicyUrl / marketingUrl from Play's app details and
App Store Connect where the config holds a template value (real values that differ are reported and kept); the App
Review contact goes into `.storebound/credentials.json` `reviewContact` (masked in output, the phone never printed).
Sign-in for review is decided from Apple's review detail (`demoAccountRequired`; a demo account set there is imported
into `reviewDemoAccount`, never printed) and the code's auth signals, never a default yes. With accounts and no
`accounts.deletionUrl` it looks for the deletion page on the app's own site (/delete-account, /account/delete,
/delete, /account-deletion, links in the privacy policy that mention deletion); not found stays a finding. Then the
owner questions that are left, in one batch.
- **App Store keywords count characters, not bytes.** Live READY_FOR_SALE fields hold Thai 95 characters / 265
bytes, Turkish 100 / 105, Polish 99 / 105: `check`, `check --aso`, `listing check`, `listing translate` and
`aso suggest` count characters again (lesson 4 was wrong; lesson 43).
- **`validate`:** a live app with only the 6.5" iPhone set (APP_IPHONE_65) passes, App Store Connect accepts it for
the 6.9" requirement; a 6.9" set is a recommendation (also the two 13" iPad types). Play: a non-default language
without its own icon / feature graphic uses the default language's (a note), only the default language's missing
graphics block.
- **Play Reporting API off:** the finding names the Cloud project and the exact link to enable
`playdeveloperreporting.googleapis.com` for it (`apps check`, `apps list`, `health`, `validate`); `apps list` no
longer fails but lists the Play apps of this machine's app folders and store-only entries.
- **`storebound accounts default <profile>`** (`--clear`): the machine's default credential profile, a pointer file in
the credentials folder, shown in `accounts list`. Order: env > the profile a project names > the project's own keys
> this default > the only profile (or the one named `default`).
- **Store-only apps:** `storebound apps add --store apple|play --app-id <id> --account <profile>` adds an app that is
live but has no project on this machine to `~/.storebound/projects.json` (`storeApps`); `apps folders` shows it as
"no local project", `apps remove apple:<id>` takes it out. The desktop shows it read-only ("Store only": status,
reviews, analytics) under the account with that profile, and keeps the entries when it writes the folder list.
- `ws` children get the app's own account choice when the workspace names no account (before: no profile at all).
Fixes from a real first release of a second game (Capacitor 8 monorepo, lessons 35 to 37):
- Fix (`scan`, review-prompt 3.2.2(x)): a bare "rate" next to "bonus" (`const rate = F.genRate(lv, bonus.gen(r))`) is
no longer a reward for rating. The signal needs a rate-us / leave-a-review phrase next to a reward, a reward "for
rating / reviewing", or a reward granted within a few lines of a system review prompt call.
- Fix (`scan`, account findings and the declarations proposal): import, require and export-from lines are never
sign-in / sign-up / deletion evidence (`import './buildings/register'` was), auth endpoints must be an absolute path,
a URL or a `${base}/...` template, credential fields and guest-account creation count; accounts only the config
claims read "from config (accounts.enabled)".
- Fix (`scan`, 5.1.1(v)): `accounts.inApp` alone no longer clears the finding: without deletion code it stays, medium,
"claimed by config (accounts.inApp), not found in code".
- Fix (`privacy-manifest --propose`): NSPrivacyCollectedDataTypes is built from `appPrivacy.types` (Apple's manifest
keys for type and purposes, linked, tracking); NSPrivacyTracking stays true only with app-level domains. `check`
warns once when the app manifest's list is empty but `appPrivacy.types` marks types collected.
- Fix (`console plan apple-app-privacy`): `appPrivacy.types` is authoritative: a type it does not list is "not
collected" (certain), never derived from dataSafety or asked. Without it, `appPrivacy.tracking: false` and no ad /
attribution SDK answer Advertising Data, Customer Support, Environment Scanning, Hands and Head "not collected"
unless a support SDK or AR code is found. `console plan` scans the project for these signals.
- Fix (`console plan play-advertising-id` / `play-app-content`): "Does your app use advertising ID?" comes from ad /
attribution SDKs, the AD_ID permission in the source or merged manifest (or its `tools:node="remove"`),
`ads.appAdsTxt` or `console.appContent.advertisingId`; a Data safety Device ID alone is not the advertising ID. No
signal: No (certain); signals that disagree: asked.
- `scan.ignore[].evidence`: accept a finding by the text of an evidence line (`match` keeps matching the title).
`scan` and `check` name every entry that matches no finding.
- Fix (`init`): `.storebound/credentials.json` starts with empty strings only. Placeholder-looking values
("your-…", `<…>`, "xxx", "/absolute/path/to/…", "example") count as missing in every credential read; `check`
warns about them and `doctor` lists each credential as set, missing or placeholder.
Console facts from a second real game (2026-10-13): the owner now comes back once for Play's App content instead of
once per card.
- Play App content card Saves are the agent's: saving a declaration (privacy policy, ads, app access, advertising ID,
government, financial, health, target audience, the content rating questionnaire and Summary, data safety) is not
public; Google gets it only with the next "Send changes for review" in Publishing overview, which stays the
owner's. New button class `declaration-save`; `declarationSaveAllowed` lets exactly Save / Kaydet through on an App
content card page; `publishingControl` refuses "Send changes for review", "Submit N changes for review", Submit,
Publish and "Restart review" everywhere. The plans' final button is "Send changes for review"; `console fill`
presses the content rating Summary's and the target audience's Save itself.
- `console plan` prints an owner batch first (`ownerBatch`): every owner step of the plan in ONE list, in the order the
agent needs them (open answers, the IARC Terms of Use box with its exact place before the run, Send changes for
review after). `console plan play-app-content --all` (MCP `all: true`) walks every card inline in order and ends
with that one owner list.
- IARC questionnaire: page 1's Next does nothing until the IARC Terms of Use box is ticked (an owner-only agreement).
It is in the owner batch before the run; `console fill` waits for the tick instead of pressing a dead Next.
- Sign in details: with `reviewDemoAccount` in `.storebound/credentials.json` the agent types the demo account
(never printed); only without it is it the owner's.
- New App (App Store Connect): a programmatic value set on Name and SKU left the dialog's state empty; the plan says
"type, don't set value" and `console fill` types every text field key by key.
- `play.appId` captured automatically: `console verify play-app-record --url <url after Create app>` reads the id from
the redirect and shows the `play.appId` change as a dry diff; `--write` upserts it into the config. New read-only
`storebound play app-id`: finds the app by package name in captured app list pages (10 per page, a new app is not
on page 1), then the same dry diff and `--write`.
- Pitfalls 38-41.
TestFlight distribute from real use (2026-10-13, pitfall 42):
- Fix (`apple testflight distribute`): it failed with 400 right after printing the build state, because App Store
Connect refuses `GET /v1/betaGroups?filter[builds]=` although Apple's spec declares the filter (and the spec has no
GET of a build's groups). A build's groups are now read per target group with
`GET /v1/betaGroups/{id}/relationships/builds`. An internal group with all builds is reported as "already
available to <group> (internal, all builds)" and never sent the build (App Store Connect refuses that); internal
groups without all builds and external groups get it through the relationship; a re-run adds nothing.
- Fix (`apple testflight distribute`, `testers`): a group in `apple.testflight.groups` that is not on App Store
Connect yet says so and names `storebound apple testflight groups sync`; a group in neither lists the groups that
exist (was `unknown group "Team" ()`).
- Guard: `tests/asc-query-guard.test.ts` walks every App Store Connect GET with a query that `cli/` builds (template
literals and the URLSearchParams they interpolate) and fails offline on a path that is no declared GET, a query
parameter the operation does not declare in `cli/api/apple.gen.ts`, or one in the new `APPLE_LIVE_REJECTED` list
(declared, refused live). `storebound api apple` refuses those parameters too. The fake App Store Connect in the
tests answers 400 PARAMETER_ERROR.ILLEGAL to such parameters, as the live API does.
- **Releases on git.crt.fyi come from a workflow.** A `v*` tag push runs `.forgejo/workflows/release.yml`: version
checks, the full CI, the Linux and Windows installers, the privacy scan, then one Forgejo release with short notes
(the CHANGELOG summary, install line, links), the installers and SHA256SUMS; older release entries are deleted
(tags stay). The Mac adds the notarized DMG with `desktop-release.ts upload-forgejo`. A dry run (workflow_dispatch)
makes a draft `test-<sha>` and deletes it. `desktop.yml` is folded into release.yml; ci.yml and release.yml share
their setup (`.forgejo/actions/`).
## 0.7.1 · 2026-10-13
- Fix: README links and images on npmjs.com resolved against npmjs.com and gave 404 (`docs/README.md`, the logo).
Every README link and image is now absolute (git.crt.fyi), and a test keeps it that way.
## 0.7.0 · 2026-10-13
Agent-first: the AI does the whole job ("do it, don't ask") and the owner presses only the final public buttons, with
every compliance question asked once, in one batch. Consoles without an API get `console plan` / `console verify`
(start URL, steps, answer sheet, never-press list) for App Privacy, Play App content, IARC, Create app and the first
production release. The desktop app sends for review itself (dry-run plan first). The CLI runs on Node 20+
(`npx storebound`). Plus many store fixes from real releases (IAP resubmits, Invalid Binary, Data safety, uploads).
- Fix (`check`): "N Play App content answer(s) are not in the config" no longer counts the two steps that stay the
owner's however complete the config is (ticking the IARC Terms of Use, typing the demo account's username and
password), so a fully answered project checks clean. The site's `check` shot (`tools/site-term-shots.ts`) now shows
that ready project: a fresh `init` plus icon files, `apple.name`, the App content answers and the demo account.
- The CLI runs on Node 20+ (owner decision, 2026-10-13): `npx storebound init` works, `bunx storebound` keeps working.
The npm package's `bin` is one Node ESM bundle, `dist/storebound.mjs` (built by `prepack` with
`bun tools/build-node.ts`, zero runtime dependencies, playwright still an optional peer); `engines` names Node 20.
Everything the CLI needs from its runtime goes through `cli/lib/rt.ts` (spawn / spawnSync with Bun's defaults and
exit codes, which with PATHEXT, sleep, a small HTTP server for shots render / video, glob, hashing, gunzip, stdin,
SQLite, the package folder); templates, the shots pages and Play's Data safety form are embedded by
`tools/gen-embedded.ts` instead of Bun text imports, so the bundle and the compiled desktop sidecar carry them.
`storebound.config.ts` loads on Node too (Node's own type stripping, or a fallback on Node 20). `storebound install`
run with Node writes `npx` MCP commands. `bun run ci` builds the bundle and smoke-tests it with the real `node`
(`--version`, `help`, `check` same as Bun, `scan`, `release plan --help`, `init`, an MCP handshake and tool call);
`tests/node-compat.test.ts` fails on any direct Bun API use in `cli/`. `bin/storebound` falls back to Node when Bun
is not installed. Development, tests and CI stay on Bun.
- `console verify apple-app-privacy` checked against real page text (2026-10-12): the summary sections ("Data Used to
Track You" and the others) list category names only and are skipped; a type's lines may come joined by " | "; Apple's
comma style ("X, and Y") and the straight apostrophe parse. The tracking radio label of a type that already ends in
"Data" no longer reads "crash data data". The Play Data safety page path (`app-content/data-privacy-security`, with
Export / Import CSV) is confirmed.
- Fix (`play build` / `check` versionCode source warning): a variable set from the property first
(`def code = (project.findProperty('versionCode') ?: ...).toInteger()`, then `versionCode code`; also `ext.` and Kotlin
`val`) is followed to its definition and no longer flagged.
- Fix (`apple iap-submit`, `apple resubmit`): after the owner canceled a submission, every IAP version was
DEVELOPER_REJECTED and the plan skipped them all ("change it in App Store Connect (or --new-version)", then
"nothing to submit"). Apple accepts the same version again (201 READY_FOR_REVIEW, then the submit 200), so
DEVELOPER_REJECTED in-app purchase, subscription and subscription group versions now go in as they are, listed as
"add (developer rejected, re-submitted as is)"; an empty draft is reused. `--new-version` is only for APPROVED or
REJECTED products (pitfall 31).
- `play build` and `check`: warn before Gradle runs when the app module's `defaultConfig` does not read
`-PversionCode` / `-PversionName` (a literal, an env var, ...), naming the file and line and the exact Groovy or
Kotlin line to use (both built with AGP 8.13). `android.injected.version.code` does not override `defaultConfig`
for `bundleRelease` (checked with AGP 8.13); the docs no longer suggest it. An AAB with another versionCode than
planned is now a hard stop (it was a warning) and is not copied (pitfall 32).
- Console plan `play-production-release`: the "Do you want to restart your review?" dialog that "Submit N changes
for review" opens while a review is running is a known step; "Restart review" is the owner's button and in
neverPress (pitfall 33).
- Desktop: send for review from the window (owner decision, 2026-10-12). New owner actions (desktop only, never MCP
tools): `owner.apple_submit` (new `storebound apple submit --build <n> [--version x.y.z]`: the steps of `apple
resubmit` for a first submission: build n attached by number, the open review submission reused, the ready IAPs /
subscriptions with it, submitted with the "not ready yet" retry; plan table without `--yes`), `owner.apple_resubmit`
(`apple resubmit --build <n> [--resolve-iaps]`), `owner.apple_iap_submit` (`apple iap-submit [--with-version]`) and
`owner.play_production` (`play release --version-code <n> --production completed | inProgress | draft`). Each is a
dry run from the window; the real run needs the host's token minted after the native dialog, and only then serve
adds `--yes` (apple submit / resubmit) and answers the command's question. `OWNER_ALLOWED_ARGS` ties every
otherwise forbidden flag (`--yes`, `submit`, `--production`, `--csv`, `--allow-price-changes`) to its one owner
action, a dry run never carries an owner flag, and the MCP server still has no submit, resubmit or production
release (`tests/serve.test.ts`, `tests/desktop-d3.test.ts`, `src-tauri/tests/d3_writes.rs`). The submit dialog says
the app goes to review and may go live by itself once approved.
- Releases tab, "Send for review": the App Store submission state from `apple status` (waiting / in review,
UNRESOLVED_ISSUES with its items and which ones get marked resolved, empty drafts labelled harmless), the uploaded
builds to pick by number (processing ones disabled, nothing preselected), Submit for review, Resubmit (with
"also mark rejected IAP items resolved") and Submit in-app purchases (with the editable version); Google Play
versionCodes by number (one already live cannot be picked), Send to production as everyone, a staged share or a
draft (a never-published app), the managed-publishing warning from `play.managedPublishing`, and the console plans
of production countries and "Send changes for review" (no API) with a deep link. Every button shows the plan (dry
run, `--json` with the plan table under serve) in the window first, then the native dialog; the real run's
progress lines stream into the panel and Stop cancels it. The old "submit stays in the console" note is gone.
- App info, "Console steps without an API": every `console plan` form (the new Play store settings category, App
content and its cards, production countries and production release included) with a plan panel (start page and
deep link, steps, answer sheet, never-press buttons, the owner's final button, Copy plan for an AI agent) and the
read-only `console verify` (page text pasted, or the API for the App Store record and age rating). The Play forms
card is folded into it.
- Price gate: `owner.prices_import` passes `--allow-price-changes` itself after the dialog; new
`owner.prices_play_products`, `owner.prices_play_subscriptions`, `owner.prices_apple_subscriptions` (Catalog:
"Price changes") run that sync with the flag. A write that ends with exit 3 (declined: a held price change) shows "Waiting for your
decision", not a failure.
- Data safety card: the deletion question as the CSV answers it (`dataDeletion`, from `accounts.deletionUrl`,
`dataSafety.dataDeletion`, ...; `project.declarations` sends `dataDeletion`, `dataDeletionSource`,
`dataDeletionUrl`), older sidecars still shown from `partialDeletion`.
- TestFlight: "Create missing groups" (`apple testflight groups sync`, dry run first). Dependencies: the Android SDK
as `play build` finds it (`system.tools` `androidSdk`). `project.inspect` adds `playRelease` (managed publishing,
countries, category). Job toasts: upload lines ("312/890 MB (35%), part 4/12") drive the progress bar, and the
last 40 lines are in the toast's log.
- Covered by the existing views: `validate` findings ("production has 0 countries", the category reminder), `check`
warnings (stale web assets without `build.prebuild`, Play category, App content answers owed, privacy manifest
tracking mismatch ITMS-91064, the defaultConfig versionCode / versionName source warning) and `scan` findings
show in Checks; `play images` skipped languages and optional tablet sets show in the dry run of the native dialog.
- Boot check: four send-for-review cases (ready and UNRESOLVED_ISSUES, on the old D1 and the new D3 saved state)
with store answers from `src/dev/submit-fixtures.ts`: the Releases texts, the build and versionCode pickers, the
plan panels, a console plan from the real sidecar, every app tab and Dependencies, by DOM text; the dialog is
answered Cancel, nothing is sent.
- Safety (`apple testflight groups`): run bare, it created the configured beta groups right away, with no plan. Now it only lists the groups and prints `would create "Team", "Friends"`. Creating is
`groups sync` (or `groups --yes`). Flows treat bare `groups` as a read and `groups sync` as a write, and the MCP tool
runs `groups sync` (dry by default).
- Fix (`apple resubmit`, `apple iap-submit`): after an Invalid Binary rejection the submit kept answering 409 "Version
is not ready to be submitted yet" for 25 minutes because the app version item of the UNRESOLVED_ISSUES submission
was still REJECTED (pitfall 30). REJECTED items are now marked resolved before the submit
(`PATCH /v1/reviewSubmissionItems/{id}` `resolved: true`): the app version item in `apple resubmit` (a fixed build
goes in), in-app purchase / subscription items only with the new `--resolve-iaps`. The plan table shows "mark
resolved" rows. On "not ready" the items are read again: one in scope is resolved, one out of scope stops the run
at once with the item and how to resolve it; backoff retries only when nothing is left REJECTED.
- Fix: an empty READY_FOR_REVIEW draft cannot be canceled (409 "Resource is not in cancellable state"). A draft the
run created and could not use is left as is, stray empty drafts get no cancel command, `apple status` labels them
"empty draft (harmless; App Store Connect keeps it, not cancellable)" and `--cancel-submission` treats that 409 as
harmless. `apple status` marks REJECTED items as not resolved.
- Fix (`apple iap-submit`): resubmitting after a rejection created a second, empty draft next to the rejected
submission and stopped with a wrong "list lag" message (pitfall 30). The open review submissions are read first
(`GET /v1/apps/{id}/reviewSubmissions`, READY_FOR_REVIEW, UNRESOLVED_ISSUES, WAITING_FOR_REVIEW, IN_REVIEW, with
their items): one with UNRESOLVED_ISSUES is reused (only missing items added, then resubmitted), a draft is reused,
one waiting for or in review means nothing is sent; a new draft only when none is open. 409
`ITEM_PART_OF_ANOTHER_SUBMISSION` is reported with the other submission's id and that one is used (a draft the run
created is canceled again); it is never read as a duplicate. The submit is retried with backoff and heartbeat lines
while Apple answers "Version is not ready to be submitted yet" (up to 10 minutes).
- New `storebound apple resubmit --build <n> [--version x.y.z]`: attaches build n (by number, VALID) to the rejected
version, reuses the UNRESOLVED_ISSUES submission, adds missing IAP / subscription items and resubmits with the
retry. A plan table without `--yes` (the owner's step); never in MCP or flows. `apple status` lists an
UNRESOLVED_ISSUES submission with its items and their states and prints the resubmit command.
- ITMS-91064 (pitfall 29): a real build was refused as Invalid Binary a minute after the upload because the app's
PrivacyInfo.xcprivacy had NSPrivacyTracking true with an empty NSPrivacyTrackingDomains (tracking came from an ad
SDK whose own manifest declared it). Rule in every manifest: true needs a non-empty list, a non-empty list needs
true; `check`, `scan` and `privacy-manifest` fail on either mismatch with the exact ITMS text. App Privacy
"used for tracking" no longer requires the app manifest's true: `check` finds SDK manifests that declare tracking
(Pods, frameworks, SwiftPM checkouts, Carthage, Capacitor plugins, the last built IPA) and says so as info, names
known tracking SDKs from the dependencies, and only warns when nothing declares it. `privacy-manifest --propose
[--tracking-domains]` prints the app manifest with true only for `appPrivacy.trackingDomains` (the app's own
domains); the scan proposal says false for SDK tracking. `apple upload` reads every manifest in the IPA (app root,
Frameworks, bundles; XML or binary plists) and stops before any network call; `apple build` checks the export.
- Play console steps without an API, from a real first release (2026-10-12; pitfalls 27 and 28). New agent-only
`console plan` / `console verify` forms (also in the MCP `console_plan` / `console_verify` lists):
`play-store-settings` (the App category: `play listing` cannot set it, so a new app showed "Not selected"; the
first Edit > App or game > the Category `[role=option]` > Save), `play-app-content` (every card of
`/app-content/overview` in order with its page, its "Start <Name> declaration" button, its plan and one batch of
owner questions) and the cards `play-privacy-policy`, `play-ads`, `play-sign-in-details` (the demo account is
typed by the owner, never printed), `play-advertising-id`, `play-government-apps`, `play-financial-features`,
`play-health-apps`; `play-production-countries` (edits.countryAvailability is read-only) and
`play-production-release` (Edit release > Next > Save; the owner's "Send changes for review", a real click).
New config: `play.category` (checked against Play's game / app lists; default from an exact App Store category
match), `play.countries`, `play.managedPublishing`, `console.contentRating.details` (the IARC follow-ups each Yes
reveals), `console.appContent`, `social`, `products[].randomRewards` (loot boxes Yes / No from the catalog).
`check` warns without a Play category and counts the App content answers still owed; `validate` adds the category
reminder and "production has 0 countries"; `play status` shows the category as a console step; `play release`
warns that an approved production release goes live by itself while managed publishing is off.
- IARC content rating plan: page 1's IARC Terms of Use box is an agreement (never pressed, never ticked: the click
gate now refuses "terms of use"); the questionnaire is one long page whose Yes answers reveal follow-ups; the
Summary's final button is "Save", not "Submit"; App content forms deep-link to their card pages.
- Every console plan ships `inPageLoop`, a ready-made in-page helper (window.__sb.run, polled through window.__log):
one evaluate call times out at about 45 s on big Play pages. Play plans and `console fill` click the inner
`[role=checkbox]` of material checkboxes like the material radios.
- App Store: appstore-first-release and store-iap-setup send first IAPs with `apple iap-submit --with-version`
(verified on a real first release), not the console; `console verify apple-app-privacy` reads "Published … ago
by …" as the published signal.
- Fix (`play data-safety`): `PSL_SUPPORT_DATA_DELETION_BY_USER` is Play's general "Do you provide a way for users to
request that their data is deleted?". storebound answered it from `partialDeletion` (data deletion without deleting
the account), so an app with a delete-account page declared "no". Now `accounts.deletionUrl` answers yes, with
`PSL_DATA_DELETION_URL` set to `partialDeletionUrl` or the delete-account page. Without a page, the answer comes
from the new `dataSafety.dataDeletion: 'yes' | 'no' | 'auto90'`, or else from `deletionRequest` (the same question
under the form's old id, no longer deprecated). `check` flags any answer other than yes next to a delete-account
URL.
- Price changes of existing products need `--allow-price-changes` (`play products`, `play subscriptions`,
`apple subscriptions`, `prices import`; `apple iap` never changes an existing price). `--yes`, also the one
`flow run --apply --yes` adds to write steps, confirms creates, first prices and other updates only. Without the
flag a run writes everything else, prints `price change on <id> (<from> -> <to>) needs the owner: re-run with
--allow-price-changes` per product and exits 3 (declined), so a flow stops with "step N declined". The terminal
question (no `--yes`) and the desktop owner action still cover them. Dry runs mark those rows `owner`; flows refuse
the flag in steps and never add it (lib/price-gate.ts, tests/price-gate.test.ts). Activating the purchase option
of an existing Play product is a normal update now (`--yes` confirms it).
- CLI wording: when `console fill` cannot open or fill part of a form (console UI changed, an unknown page or dialog
step), it says what is left and that the calling agent does it with its browser tool (`storebound console plan
<form>` / `console verify <form>`), not "open it yourself" / "fill them yourself". Sign-in, agreements and the final
Submit / Create / Publish stay the owner's. Other "by hand" output (fastlane import, catalog pull, ppp, scan) points
at the config edit instead. `tests/do-it-dont-ask.test.ts` now scans the CLI's output strings too.
- Agent rules: a first, top-level rule "Do it, don't ask" in `docs/agents-core.md` (and so AGENTS.md, GEMINI.md,
the Copilot and Cursor rules), README ("How agents use storebound"), the store-pitfalls skill and the openings of
console-autofill, play-first-release, appstore-first-release and store-declarations. The agent does API calls,
uploads, builds, exports, console navigation and forms itself and runs the real command after the dry run; the
owner presses only the final public or irreversible buttons and answers the facts no file proves in one batch at
the start. Skill text that left navigation, draft saves, exports or uploads to the owner is rewritten;
`tests/do-it-dont-ask.test.ts` guards it. Pitfall 26.
- Fix (`play data-safety`): apps with accounts got Play 400 "Invalid safety labels declaration: Line 7: Response ID
missing". Play's current form asks `PSL_SUPPORT_DATA_DELETION_BY_USER` as a single choice with three options;
storebound wrote it as a bare TRUE / FALSE. It now writes one row per option with TRUE on the picked one, and
`dataSafety.partialDeletion` is `true | false | 'auto90'`: true = `DATA_DELETION_YES` (plus `PSL_DATA_DELETION_URL`),
false = `DATA_DELETION_NO`, `'auto90'` = `DATA_DELETION_NO_AUTO_DELETED` ("No, but user data is automatically
deleted within 90 days"); left out, `check` names it as missing.
- Play removed `PSL_DATA_COLLECTION_USER_REQUEST_DELETE` from the form, so it is no longer written and has no rule.
`dataSafety.deletionRequest` is deprecated and optional; `check` prints a note to remove it. `profile` checks,
`scan` and the desktop app no longer ask for it (the desktop Data safety card shows the deletion answer instead).
- Google's empty form, exported from Play Console on 2026-10-12, ships as `cli/data/play-data-safety-form.csv`
(embedded in the binary). Every row's Answer requirement (and label) comes from it, `dataSafety.extra` rows
included (they were guessed as MULTIPLE_CHOICE / MAYBE_REQUIRED). New `formPairIssues`: every (Question ID,
Response ID) pair must be in that form, or `check`, `project.declarations`, `play data-safety --dry` and the real
run (before the POST) report "<q>,<r> is not in Play's current Data safety form ...; Play answers 400". Refresh
the file when Play changes the form (Play Console > App content > Data safety > Export to CSV, nothing answered).
- `templates/data-safety.csv` (the filled example from `init`) regenerated against the form. Pitfall 21.
- Fix (`play build`): the Android SDK is found the way Android Studio finds it, so a non-interactive shell (no
`ANDROID_HOME`) with a gitignored `local.properties` no longer ends in Gradle's "SDK location not found". Order:
`ANDROID_HOME`, `ANDROID_SDK_ROOT`, `sdk.dir` in the Gradle project's `local.properties`, then
`~/Library/Android/sdk` (macOS) / `~/Android/Sdk` (Linux) / `%LOCALAPPDATA%\Android\Sdk` (Windows); only a folder
with `platforms/` or `platform-tools/` counts. Gradle gets it through `ANDROID_HOME` and `ANDROID_SDK_ROOT`;
`local.properties` is never written. None found: the build stops before Gradle and lists every place checked. The
dry run and `storebound doctor` (also `--json`: `androidSdk`) show the SDK path and where it came from. Pitfall 25.
- Fix (`apple upload`): never silent. The IPA size at the start, a line per part (every 10th from 20 parts), a
heartbeat every 20 s while a part is in flight ("still uploading, 4m30s, 312/890 MB (35%), part 4/12"), the same
heartbeat while `--via altool` runs (now in the background, stopped at the deadline) and while the build is
processed (`apple testflight wait` too). Each part has a 15 min limit and is retried; `--timeout` now also covers
the parts and stops with exit 7 and the hint to run the same command again (it resumes the build upload). Every App
Store Connect API call has a 2 min limit (a hung connection is retried when safe, never a silent wait).
- Fix (`flow run`, every confirming command): a declined confirmation is no longer a success. `confirm()` prints
`declined: <reason>` and exits 3 when the owner answers no, when there is no terminal and no `--yes`, or under
`serve` without the owner's dialog; `flow run` stops at that step with "step N declined: <reason>" (status
`declined`), never "ok". `flow run --apply --yes` is the owner's approval for the run: write steps get `--yes`
(reads, waits and owner steps do not); `--yes` without `--apply` is refused. Commands that went on with a partial
write after a declined part (`play products` prices, `testflight testers --invite`, `distribute --submit-review`)
now exit 3 too. Pitfall 24.
- Fix (`apple build`, `play build`, Capacitor): a native build no longer ships old web assets silently. New
`build.prebuild` (per store `apple.prebuild` / `play.prebuild`): a shell command run in the project root before the
archive / bundle (e.g. `npm run build && npx cap sync`; printed, output streamed, nonzero exit stops the build,
`--dry` only prints it, `--no-prebuild` skips it, once per `flow run` instead of once per store). After it, the
copied `ios/App/App/public/index.html` / `android/app/src/main/assets/public/index.html` must be newer than the last
git commit outside ios/ android/ node_modules/ store/ .storebound/ and `<webDir>/index.html`, else the build stops
with "web assets are older than the source (copied ..., source ...)"; `--allow-stale-web` overrides. `check` warns
when they are stale and no prebuild is set. The build output and the dry-run plan (`web` in `--json`) show the web
bundle that went in: sha256 (12 hex) and mtime of the copied index.html. Pitfall 23.
- Monorepos: `play build` failed with "no Gradle project (settings.gradle) in android or ." when the Capacitor app
lives in `apps/native` (capacitor.config.ts, android/, ios/ there). `apple build`, `play build`, the staleness
check and `check` now use the root's capacitor.config.*, else the one found one or two levels down (package.json
workspaces, apps/*, packages/*, direct child folders; node_modules skipped) and name it ("Capacitor project:
apps/native"). Several: they are listed and `play.build.dir` / `apple.build.workspace|project` is asked for; an
explicit config value always wins.
- Fix (`play build`, `play upload`, `play release`): real AGP bundles (e.g. Capacitor 8) no longer fail with "AAB
manifest without package / versionCode". The aapt2 proto manifest is read with Resources.proto's field numbers.
Pitfall 22.
- Fix (`play data-safety`): apps with accounts got Play 400 "Invalid safety labels declaration: Response missing for
PSL_ACCOUNT_DELETION_URL". When `dataSafety.accountCreation` is not `PSL_ACM_NONE`, the CSV now carries
`PSL_ACCOUNT_DELETION_URL` = `accounts.deletionUrl` and `PSL_SUPPORT_DATA_DELETION_BY_USER` = the new
`dataSafety.partialDeletion` (delete some or all data without deleting the account; ask the owner), plus
`PSL_DATA_DELETION_URL` = new `dataSafety.partialDeletionUrl` (default `accounts.deletionUrl`) when that is true.
- Required-row rules in one table (`DATA_SAFETY_RULES`, per data type `typeRules`, `dataSafetyIssues`): account
creation -> deletion rows, a selected data type -> collected / shared, ephemeral, required / optional, purposes,
`PSL_HAS_OUTSIDE_APP_ACCOUNTS` refused with sign-up methods. `storebound check`, `project.declarations` and
`play data-safety --dry` say `missing: <id> (<when>; set <config field>)`; the real run stops before the POST.
- A `dataSafety.extra` row with a generated question id now replaces that question instead of adding a second one.
- Sources: support.google.com/googleplay/android-developer/answer/10787469 ,
support.google.com/googleplay/android-developer/answer/13327111 . Pitfall 21.
- Agent-first console mode. `storebound console plan <form> [--json]` (read-only, offline; `--lookup` asks the App
Store Connect API for the app id) gives an AI agent everything to fill a console form with its own browser tool:
the start URL (deep links from apple.appId, play.developerId, the new play.appId and the remembered ids), ordered
steps (what to click by visible text), the answer sheet (each answer certain or ask-owner, with the console's exact
labels, curly quotes noted), the never-press list (Publish, Submit, Create, agreements, help panels), the known UI
facts per form and how to verify. `storebound console verify <form> [--from-text <file|->]` (read-only, exit 1 on a
mismatch) checks the result: App Privacy from the page text (per-type "Used for …", linked, tracking), Play Create
app from the input states, content rating and target audience from their summaries, the App Store record and age
rating from the API. MCP: `console_plan`, `console_verify` (read-only; `console fill` stays off MCP). The
console-autofill skill now puts the agent-driven mode first; `console fill` is the optional scripted fast path.
Pitfall 20.
- App Privacy, verified on a real console (2026-10-12): two information steps in each type dialog ("Let’s define two
important terms…", "To help put tracking into context…"), purpose checkboxes without an accessible name (matched by
the nearest parent text), "Definitions and Examples" never pressed. The scripted engine handles all of it.
- Play Create app, verified through Claude in Chrome (2026-10-12): the form's own URL
`/console/u/0/developers/<id>/create-new-app` (the engine opens it directly with a developer id); the App package
name field (from `play.packageName`, max 150) and its read-only "Check availability" ("Package name available"),
which storebound presses; the `<material-radio>` gotcha (the native input does not update the form's model: the
engine clicks the inner `[role=radio]`); the app dashboard `/developers/<id>/app/<appId>/app-dashboard` (the app id
is remembered and deep-links the App content forms); after Create app the owner adds the new app to the service
account (Users and permissions > the service account > Add app), never an agent. The old note "the first AAB
upload sets the package name" is gone.
- Confirmed (2026-10-12): the earlier default copy of the owner's Google cookies DID sign the owner out of Play
Console in their real Chrome (pitfall 18). The copy has been opt-in since the change below.
- Fix (`play images`): a phone-only project no longer aborts with "no sevenInchScreenshots ... (render first)". Play
requires the 512 icon, the 1024 x 500 feature graphic and at least 2 screenshots (Play Console Help answer 9866151);
7" / 10" tablet screenshots are optional. `play images` (dry and real) uploads what exists (icon, feature graphic,
phone screenshots; tablet, TV and Wear sets only when rendered) and prints `tablet screenshots: none (optional; the
app isn't eligible for tablet featuring without them)`. Missing required sets are still errors, with the fix.
- New `play.tablets?: boolean` (default: true when tablet renders exist, else false). `false`: `shots render`,
`shots check`, `shots gallery`, `validate` and `play images` never ask for tablet sets. `true`: rendered, and
missing ones are errors (local renders and the listing on Play).
- A missing optional set never deletes the screenshots Play has for it: the dry run shows `keeps <type> on Play`;
new `--prune` deletes them (shown as `would delete` in the dry run; not exposed over MCP). Pitfall 19.
- Languages: the ones `play listing` pushes. A status=draft / status=translate section is skipped (`skipped de-DE: no
Play listing yet (machine-translated draft, not reviewed ...)`, or `--include-drafts`), and a language Play has no
listing for (404 "Listing for language 'de-DE' not found.") is skipped with an info line instead of aborting.
- Safety (`console fill`): storebound no longer copies the owner's Chrome cookies by default. A copied Google session
reached Play Console once, then Google answered `/v3/signin/rejected`: Google binds its session cookies to the
device (Chrome's Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign
the owner out of their real Chrome. Sources: blog.google/security/protecting-cookies-with-device-bound-session-credentials ,
knowledge.workspace.google.com/admin/security/investigate-and-take-action-on-suspicious-session-cookies .
- Default: storebound's own profile per store (`~/.storebound/console-profile/<store>`); the owner signs in once by
hand (the first run prints what to do) and it is kept.
- storebound starts that Chrome itself (`--user-data-dir`, `--remote-debugging-port=0`) and attaches over CDP: no
`--enable-automation`, `--disable-blink-features=AutomationControlled` (`navigator.webdriver` false), none of
Playwright's launch switches, Chrome's own user agent. Graceful close by CDP `Browser.close`.
- `--use-chrome-session google|apple`: explicit opt-in to the old copy, now of only that store's cookies; Google's
prints a warning about device-bound sessions. Apple's is opt-in too (Apple publishes no binding, but no guarantee).
- Google's `/signin/rejected` page is said plainly: "Google refused sign-in in this window".
- The desktop dialog no longer says it uses the Chrome sign-in. Pitfall 18.
- App Privacy purposes (wrong answers in `console fill apple-app-privacy`, `project.declarations` and `scan`): Play
"advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse
Location, Product Interaction, Performance Data). Now one Apple purpose per Play purpose, following Apple's
definitions (developer.apple.com/app-store/app-privacy-details/): advertising -> Third-Party Advertising,
analytics -> Analytics, personalization -> Product Personalization, functionality / fraud / account /
communications -> App Functionality. Developer's Advertising or Marketing is No unless the owner says so; with a
Play "communications" purpose console fill asks.
- New `appPrivacy.types`: the owner's App Privacy answers per Apple data type id (all 35: USER_ID, DEVICE_ID,
GAMEPLAY_CONTENT, ADVERTISING_DATA, CUSTOMER_SUPPORT, ENVIRONMENT_SCANNING, HANDS, HEAD ...), each
`{ collected?, linked?, tracking?, purposes? }` (THIRD_PARTY_ADVERTISING, DEVELOPERS_ADVERTISING, ANALYTICS,
PRODUCT_PERSONALIZATION, APP_FUNCTIONALITY, OTHER_PURPOSES), validated against Apple's lists.
- `console fill apple-app-privacy` fills them as certain answers (they win over what dataSafety suggests); types
with no Play equivalent work from the block alone; anything unset stays "(you decide)".
- `scan --write` proposes `collected` + `purposes` for detected types the block does not answer; never `linked` /
`tracking` (the SDK disclosures do not prove them).
- `check` compares the block with the iOS project, both ways: a type used for tracking needs
`NSUserTrackingUsageDescription` and `NSPrivacyTracking` true (and the reverse is flagged), and the app's
`NSPrivacyCollectedDataTypes` (manifest keys mapped to App Privacy ids) must match the types marked collected.
- templates/app-privacy.md, the store-declarations, console-autofill and store-pitfalls skills document it.
- `console fill play-app-record`: Play Console's Create app form, the Play counterpart of `apple-app-record` (the
Play Developer API cannot create apps).
- Account: `play.developerId`, the workspace account's id or the remembered one; else the only account the login
sees; else the chooser entry named `play.developerAccount` (new optional field: the account's display name, e.g.
an organisation account). Several accounts with none set, or a name that matches none or two: it stops and lists
them. Remembered in `~/.storebound/console-accounts.json`.
- Navigation: All apps > the page-level Create app button, through a dedicated allowlist (`createAppAllowed`: exact
name "Create app" / "Uygulama oluştur", button or link, on the app list, no Create app form open, not inside a form
or dialog). The form's own Create app stays refused.
- Fills App name (`play.name`, new, else `app.name`; 30 characters checked), Default language (Play Console label,
e.g. "English (United States) – en-US"; matched by code in a Turkish console), App or game (profile) and Free or
paid (`play.free`, new, else Free for IAP / ads only) as an owner check: outlined, with the note that a free app
can never become paid. The Developer Program Policies and US export laws declarations are never ticked (an
`agreement` field is outlined whatever its answer). The log and guidance say the package name is set by the first
AAB upload (`play upload --track internal` / `flow run release`).
- In `--dry`, the `console fill` list, the MCP / desktop form list and the desktop App info "Play Console forms" card.
- `shots check` no longer reports `i drawn with a system font` for captions with ligatures ("Outfits" in en / de / nl
with Montserrat). The font usage probes (one span per character, read with CDP `CSS.getPlatformFontsForNode`) sat
side by side in one line, so Chrome shaped "f" + "i" into the fi ligature across them and the lone "i" probe reported
no glyph of ours. Each probe is now its own block. The Google Fonts `text=` subsets were fine (they keep the GSUB
ligatures); regression test with a real Montserrat `text=` subset (`tests/glyph-ligatures.test.ts`).
- `--help` / `-h` on every command, subcommand and group (`storebound shots render --help`, `storebound apple --help`)
prints the usage (the module's usage lines, the description and the options it accepts) and exits 0 before anything
loads; it used to start the command (a full render). An unknown flag (`--dyr`) exits 2 with the usage before anything
runs (`cli/data/flags.json`, `bun tools/gen-flags.ts`). `storebound shots` alone lists the shots commands.
`storebound ads apple ...` is reachable again (the plain `ads` command used to swallow it).
- `shots render --scenes` takes scene ids (the usage said numbers); an unknown id fails at once with the list of ids
instead of a 30 s page timeout.
- Faster CI: `bun run ci` runs as a task graph (independent steps at once, `[step]`-prefixed output, fail fast, time
per step) with the tests split into parallel shards (CPU cores - 1, max 8) balanced by recorded durations, browser
suites in shards of their own. New `bun run ci:quick` for the inner loop: typecheck plus the tests related to the
change against origin/main (through imports) plus scrub. The desktop Rust host runs only when `src-tauri` or the
sidecar build changed (`CI=full` always; Forgejo CI sets it). Fixed sleeps in tests became event waits.
- ASO from official and public data only (no scraping, no third-party ASO services):
- `aso competitors [--term <kw>] [--storefront us,tr] [--top 10]`: per keyword of the locale's keyword field (and
`--term`) the top apps in the iTunes Search API (name, developer, rating count, first release, last update, genre
localized to the storefront), the words in their names that the name / subtitle / keywords do not cover (only
words of two or more developers: a one-developer word is likely a brand) and the apps new in a term's top N since
the last snapshot (`.storebound/aso/competitors/<date>.json`). Same pace as `aso rank` (one call every 3.5 s, stop
on 403 / 429). Competitor subtitles are not in any public API and are not shown.
- `aso suggest [--locale] [--fresh] [--offline] [--write]`: keyword candidates from the own name / subtitle /
description (words and two-word phrases), competitor names, the localized category names (from the Search API's
`genres`) and, with Apple Ads credentials, the Apple Ads Platform API keyword suggestions
(`POST /v1/suggestions/keywords/query`, relative popularity 0-100). Each candidate shows relevance (own text,
competitor names), competition (top apps with the word in their name) and popularity (Apple Ads only, else
"unknown"). Proposes a keyword field per locale that fits 100 bytes with the existing rules (no spaces, no
duplicates, plural-insensitive, no name / subtitle words, no filler or category words; current terms kept first
unless `--fresh`) as old -> new; `--write` puts it into `store/appstore.md` with a draft line that `check --aso` and
`aso audit` report until the owner deletes it.
- `aso audit`: one offline report: `check --aso` findings, the cross-locale free keyword room, rank trend arrows from
the `aso rank` snapshots, competitor coverage gaps, Play short description use and keyword density, App Store
conversion from the analytics snapshots, Product Page Optimization / Play listing experiment ideas (suggestions
only: PPO cannot test the subtitle; Play experiments stay in Play Console) and the next 3 actions.
- MCP: read-only `aso_competitors` (saves no snapshot), `aso_suggest` (never writes), `aso_audit` (offline).
- Desktop: an ASO card on the Growth tab (`aso.audit`): keyword field state, keywords ranked, coverage gaps, the
rank trend of the best three keywords (visx, rank 1 at the top), the rank table and the next 3 actions.
- `examples/demo-app` has listing files (`store/appstore.md`, `store/listing.md`) to try the ASO commands on.
- Local font files in `shots.fonts`: `title`, `body` and `fallback[]` take, besides a Google Fonts family, a path
(relative to the config) or `{ family, file, weight?, style?, license? }` (TTF / OTF / WOFF / WOFF2, validated by
extension and signature; the family comes from the name table when not given). `shots fonts` checks the file's cmap
per language, tries the listed fallbacks in order before the automatic Noto ones (`pl title: ę ł ą Ł not in ER
Display -> Noto Sans Display`), errors when nothing covers a character, and records the file in fonts.json with its
path, sha256 and license. Render and `shots video` serve it in place at `/fonts/local/...` (never copied); the hash
in fonts.css invalidates the capture cache, and render refuses a font file changed since `shots fonts`. `shots check`
treats the local font and listed fallbacks as configured and warns when a local font has no license note.
- Idempotency: every store write is an upsert; running a command twice, or again after a crash or a lost answer,
leaves exactly one of each thing (table of every write path: `docs/idempotency.md`).
- New `cli/lib/upsert.ts`: `findOrCreate` looks again with a fresh GET right before a create and asks a journal of
recent creates (`.storebound/created.json`, a direct GET by id) so a re-run during the store's list lag reuses what
the last run made; `createOnce` treats 409 / ALREADY_EXISTS as found and never sends a create blindly again after
a timeout or 5xx (it looks first; only keys the store keeps unique get one more try).
- The App Store Connect, Google Play, RevenueCat and `storebound api` clients retry 5xx / network errors only for
requests that are safe twice (GET, PUT, PATCH, DELETE, relationship adds, state actions, price schedules); a POST
create without a clear answer throws `AmbiguousWriteError`. Plan runners treat a 409 duplicate as done and stop on
an unclear create instead of retrying.
- Uses it in `apple iap`, `apple subscriptions` (groups, subscriptions, localizations, promotional offers, offer code
campaigns, custom codes, review screenshot by MD5), `apple offer-codes`, `apple cpp create`, `apple events create`,
`apple ppo create`, `apple testflight groups`, `apple bundle`, `apple review-details` (only changed fields),
`apple release --phased on`, `apple adhoc register` / `profile`, `apple signing ensure` (a lost certificate answer
keeps the key and finds the certificate by its public key), `apple upload` (a COMPLETE build upload is not sent
again, an unfinished one is resumed), `analytics setup`, `reviews send`, `revenuecat sync`, `play subscriptions`
(subscriptions and offers).
- Review submissions (`apple iap-submit`, `cpp submit`, `events submit`): the draft is reused or created once, an
item already in it is not added again.
- Screenshot and preview sets keep the leading files that already match (MD5, position, delivered), replace the rest
in order and fix the order if Apple shows another; a crash midway converges on the next run without duplicates.
- Play edits: one open at a time per run, deleted on every error path, committed only on success, deleted instead of
committed when nothing changed (`ctl.unchanged()`); redone only when nothing went live, never after a commit
without a clear answer. `play images` compares sha256 first and replaces only changed image types; `play release`
reuses a versionCode Play already has and skips identical track releases; `play rollout` commits nothing when the
track is already like that; `play upload` is done when the versionCode is on the track already.
- One writer per app and store: write runs take `.storebound/locks/<store>-<app>.lock` (dry runs and reads do not);
a second run stops with who holds it; a lock of a process that is gone is taken over; `--force-unlock`.
- `tests/idempotency.test.ts` with stateful store doubles (`tests/helpers/fake-stores.ts`): write commands run twice
with zero writes on the second run, a crash in the middle of a screenshot set, IAP and subscription creates whose
answer is lost, 409 duplicates, the list lag after a crash, Play edit error paths and an unclear commit, the lock.
Pitfall 9 in `docs/pitfalls.md` and the store-pitfalls skill.
- The last open rows are closed (`docs/idempotency.md` has none left):
- `apple listing`: the App Store version is created through `findOrCreate` (a fresh look right before the create,
the journal for the list lag, a lost answer looks first); localizations through `createOnce` (409 = found, then
only the differing fields are PATCHed); unchanged localizations and an attached build are left alone.
- `play listing`: compares again inside the write edit and PUTs only differing details and languages; nothing left
to change = the edit is deleted, not committed.
- `apple appinfo`: categories, age rating, content rights compared with the live values, only differences PATCHed.
- `play data-safety`: Play cannot read the form back, so the sha256 of the last uploaded CSV is kept in
`.storebound/data-safety.sha256`; the same CSV is not sent again unless `--force` (MCP `force`).
- Twice-run tests for `apple listing` (plus a lost version create and a 409 localization), `play listing` (plus a
store that became current before the edit), `apple appinfo`, `play data-safety`, `apple promoted`,
`apple win-back`, `apple preorder`, `apple testflight testers`, `play upload` and `prices import` (Play).
- Faster `shots capture` (web) and `shots render` (a 22-language game took about 50 min to capture, serially, with a
new context and a full game boot per frame):
- `--concurrency N` (and `shots.capture.concurrency`): parallel browser contexts in one Chromium; defaults from the
CPU count (capture min(3, cores / 4), render min(4, cores / 2)). Output files do not depend on the order; progress
lines carry language / device / frame, n/total and an ETA. Render runs ffmpeg asynchronously and makes the contact
sheets after the images.
- Per-frame timing (goto, waitFor, eval, delay, screenshot, total) in each progress line and in
`.storebound/capture-report.json`, with time per step and the slowest frames at the end.
- `shots.capture.session: 'per-language'`: one page per language and device, booted once; shots switch with
`navigate: 'spa'` (pushState + popstate, then waitFor / eval) or `'reload'`; a failed precondition falls back to a
fresh page (`reuseTimeoutMs`).
- Input-hash cache (`.storebound/capture-cache.json`: shot entry, URL, frame size, session, copy text, app build from
`shots.capture.build` or the served HTML + scripts, fonts): unchanged frames are skipped; `--only-missing`,
`--force`. MCP `shots_capture` (`concurrency`, `mode`) and `shots_render` (`concurrency`).
- `apple listing --dry`, `play listing --dry` and `apple appinfo --dry` print a real plan (also `--json`), read-only:
the version decision of `apple listing` (keep, update the editable version's string while PREPARE_FOR_SUBMISSION,
create, or refuse: number taken, not higher than a reviewed version, another version in review / pending release),
the build to attach, and per locale / language create or update with old -> new per field (long text cut with its
length, unchanged fields summarized); app info name / subtitle / privacy URL, categories, age rating and content
rights the same way. Locales whose section is `status=draft` or `status=translate` (`listing translate`) are skipped
with the reason; `--include-drafts` sends them. The real run prints the same plan, asks in a terminal and applies
exactly it (unchanged localizations are no longer PATCHed, Play PUTs only changed languages); what's new is left out
on an app's first version.
- `apple.name`: the App Store name in the primary locale (at most 30 characters, validated) for when the plain name is
taken on the App Store ("already being used"). `app.name` stays the Play and on-device name. `console fill
apple-app-record` fills Name from it (else `app.name`), and `apple listing`, `apple listing pull` (a pulled name equal
to it is not written into the file), the listing completeness view, `check --aso`, the console app picker and `scan`'s
beta-wording rule use it for every locale without its own `### name` block. `check` reports an App Store name over 30
characters in any locale and hints when every locale uses the plain `app.name` (`hints` in `--json`).
- Pitfalls from real use become guardrails: new skill `store-pitfalls` (read before any store task, linked from the
agent rules) and `docs/pitfalls.md` (changelog of lessons; CONTRIBUTING: every real-use bug adds an entry and,
where possible, a check or test).
- `apple iap-review` names config products missing from App Store Connect's list (it lags a minute after a create)
and exits 1 instead of skipping them silently; `apple iap` and `play products` re-read the list after creating
and warn when a new product is not in it yet.
- `cli/lib/dry.ts` `dryPlan(title, rows)`: the standard dry-run plan table. `apple iap --dry` now reads
localizations, price schedule and availability and prints what a real run would add; `apple iap-review` and
`play products` end their dry runs with the table. A test scans `cli/**` for `--dry` branches that print only a
count (allowlist with TODOs for commands being fixed elsewhere).
- Screenshot glyphs (a Turkish render showed fallback glyphs for "ş" / "ğ" while `shots check` passed): the cause was
not subsetting (css2 `text=` already takes glyphs from every subset) but the title font: Lilita One has no ş ğ İ and
most of Latin Extended-A, so Chrome silently drew them with another font.
- `shots fonts` checks every character of every language (copy.json + config languages) against the cmap of the
faces that draw it (new pure-TS cmap reader for TTF / OTF / TTC / WOFF / WOFF2, no dependency), adds an automatic
Noto fallback per missing character (Noto Sans Display for titles at a heavy weight, Noto Sans, Noto Sans
<Script>, Symbols) and logs it; a character no fallback covers fails with the language and the characters. Writes
`.storebound/fonts/fonts.json` (faces and per-language stacks).
- `shots render` refuses fonts that no longer cover the copy, uses the per-language stacks, and measures in the page
(CDP platform fonts of one probe per face and character) which face drew every character; `report.json` keeps it
per image (`fonts`). `shots check` (and `validate`, `shots gallery`) raise an ERROR for glyphs drawn by a fallback
the config did not choose or by a system font, and warn for renders without font data.
- New `shots.fonts.fallback: ['Noto Sans Display']` to accept a fallback. Default game title font is now Paytone One
(covers Latin Extended and Vietnamese) in the template, the demo and the built-in default; README and the
store-screenshots skill document the default stack per script.
- `console fill`, from a real run whose App Store Connect session had expired (the add button was "not found" while
the window showed the sign-in form):
- Sign-in is detected by URL and frame (App Store Connect `/login`, idmsa.apple.com / appleid.apple.com frames,
Google sign-in), a password field in any frame, and the form's texts (Sign in with Passkey, Email or Phone Number,
Turkish too), before every navigation step, after each click or load and while a step waits. storebound waits for
the owner (never typing or reading there) and then opens the form again from step 1.
- After the printed manual steps it keeps watching: a finished sign-in restarts navigation, the owner moving on by
hand resumes it from that step, a form the owner opened is filled. A field missing only while a page loads no
longer counts as a UI change.
- The sign-in is kept: copy and own-profile modes launch with `--restore-last-session` (session cookies such as
Apple's are loaded again; restored tabs are closed) and Ctrl+C closes the browser gracefully so cookies are
written. Hints to tick "Keep me signed in" / "Trust" (Apple) and "Stay signed in" (Google).
- One login, many developer accounts / teams: Play finds `play.packageName` across every developer account of the
login (or opens `play.developerId` directly), App Store Connect switches teams in the account menu (`apple.teamId`
first, else the bundle id in each team); the match is remembered in `~/.storebound/console-accounts.json` (ids
only). New config `play.developerId`; workspace accounts take `developerId` / `teamId`. Switch clicks pass their
own gate (an account name just read from the list, never sign out or a deny-listed name).
- Fixes from a live monorepo game (username accounts, Capacitor in `apps/native`):
- `scan` no longer turns accounts into "Email Address" / `PSL_EMAIL`: accounts mean User ID; email is "detected"
only with real evidence (an email field or keyboard, an `email` property in a body / schema / type, an email
validation or endpoint, an email sign-in call, an SDK that collects it, or `accounts.signUp`), otherwise a question.
- New `accounts.signUp` (`username`, `username-password`, `email-password`, `email-link`, `phone`, `oauth`, `other`):
`scan` proposes the account data and `dataSafety.accountCreation` from it (`scan --write` writes it); a username
with no password is Play's "Username and other authentication" (`PSL_ACM_USER_ID_OTHER_AUTH`).
- Built output (bundles, dist / build, the Capacitor webDir, minified files) is low-trust evidence: `scan` prefers
the own sources and `*.test.*` / `*.spec.*` files are no longer app sources.
- `privacy-manifest --ios` finds Capacitor / Cordova / React Native plugins without `--sdk-dirs`: node_modules next to
the app's package.json and above it, workspace packages, local SwiftPM packages (CapApp-SPM), nested Pods.
- `scan` reads the native dependencies behind each npm plugin (its podspec, Package.swift, android/build.gradle), so an
ad SDK wrapped in a Capacitor plugin (Unity LevelPlay) is detected; LevelPlay's Swift package is matched too.
- `storebound flow list | validate | run <name>`: repo-local workflows in `store/workflows/*.yml` (or
`storebound.workflows.yml|json|ts`): named steps run in order with progress, `${VAR}` params and captures, `if:`
conditions, sub-flows, before / after / onError hooks. Writes run as dry runs unless `--apply`; owner steps (submit,
release, rollout ...) are listed as "your click" and never run; `--from <step>` and `--resume` continue after a
failure. `init` writes `release.yml` and `beta.yml` for the app and game profiles. A small YAML subset reader keeps
zero runtime dependencies. MCP: `flow_list`, `flow_run` (dry mode). Skill: `release-flows`.
- `storebound listing translate --to <langs>`: per-language listing drafts in both listing files (App Store name,
subtitle, promotional text, keywords, description; Play title, short, full) for the agent to translate, or a
`--translate "<command>"` hook per field; store limits and keyword rules (commas, duplicates, name words, 100) are
enforced, drafts stay marked until `listing reviewed`; `listing check`, and `check` warns about drafts. No LLM call
in the CLI. MCP: `listing_translate`, `listing_check`. Skill: `listing-localization`.
- `scan`: Apple 2.2 "beta / demo / trial / test" wording in the app name and the App Store text (`beta-wording`);
`scan --originality [--against <folders>]` puts the 4.3 / Play repetitive content check into the scan report.
- `prices export` includes auto-renewable subscriptions (Apple current and scheduled prices, Play base plan regional
prices; `--no-subscriptions` leaves them out); `prices import` skips those rows and names the subscription commands.
- Site: "Catch rejections before you submit", a Desktop app section with real fixture-mode screenshots, an "Everything
else" command grid and the FAQ on `storebound originality`.
- Fixes from a read-only run of every read path on live App Store Connect and Google Play accounts:
- `apple signing status | ensure` no longer fails on an Xcode-managed profile (listed under the bundle id, 404 on
`/v1/profiles/{id}`); with automatic signing it shows as "Xcode-managed".
- `release status | plan` and `ws status` show a version that is only in TestFlight as a train at "testing" (the
plan says to create the App Store version with that build), and the build number of an older attached build.
- `scan` skips `.claude` (agent worktrees) and other linked git worktrees, takes an absolute `--config`, and no
longer reads "rate-limits ... rewardedBreak()" as a reward for rating.
- `api --all-pages` drops the first page's `nextCursor` / `limit` from the merged result; `apple iap-submit --json`
lists no requests when nothing is left to submit; `reports apple-sales | apple-finance` explain a wrong vendor
number.
## 0.6.0 · 2026-10-12
- Growth extras, each with `--dry`, `--json`, tests on sanitized fixtures and a skill:
- `prices ppp`: purchasing power price sheets from a bundled, dated World Bank ICP PPP dataset (CC BY 4.0,
`tools/update-ppp.ts` refreshes it), the Big Mac index or a CSV; clamped, snapped to store price points, written
in the `prices import` format with a diff.
- `ads apple status | campaigns | report`: Apple Ads reports (spend, taps, installs, CPA per campaign / keyword,
CSV export); `bid | budget | pause` are dry first and ask in a terminal, never over MCP.
- `web init | check`: apple-app-site-association, assetlinks.json (Play app signing SHA-256, also read from the
Play API with `--version-code`), the Smart App Banner tag and a minimal landing page; `web check` verifies the
live files (content type, no redirects, JSON, ids match).
- `apple adhoc devices | register | profile | options | export` (release-testing / ad-hoc ExportOptions.plist,
devices with masked UDIDs, registration asks first) and `play share` (internal app sharing upload, prints the link).
- `revenuecat diff | sync`: optional RevenueCat REST API v2 adapter for products, entitlements and the default
offering; sync only creates and attaches, asks first, never over MCP.
- `console fill <form>`: AI-assisted Playwright autofill for the App Store app record, age rating, App Privacy and
the Play content rating and target audience forms. A visible Chromium with a profile the owner signs in to; it
never types passwords and never clicks Save / Submit / Publish / Next; UI changes fall back to guidance. Desktop:
"Fill in console" on the App info tab (dry run, native confirm, then the browser). Never over MCP.
- New config: `prices.ppp`, `web`, `apple.teamId`, `apple.adhoc`, `revenuecat`, `console`. New MCP tools (safe
parts only): `prices_ppp`, `web_init`, `web_check`, `apple_adhoc_devices`, `apple_adhoc_options`,
`ads_apple_status`, `ads_apple_campaigns`, `ads_apple_report`, `revenuecat_diff`.
- `apple appinfo` answers the new `socialMedia` / `socialMediaAgeRestricted` age rating questions with false
unless `apple.info.ageRating` says otherwise.
- `storebound scan` on Google Play now matches the App Store side: rejections, blocked updates, suspensions and
policy strikes that the project files can prove, each verified against the live Play Policy Center / Play Console
Help page (cited with the date in `cli/lib/scan/play-rules.ts`). New rules: the restricted permissions and their
Console declarations (background location, READ_MEDIA_IMAGES / READ_MEDIA_VIDEO, MANAGE_EXTERNAL_STORAGE,
AccessibilityService with or without isAccessibilityTool, REQUEST_INSTALL_PACKAGES, Health Connect,
USE_FULL_SCREEN_INTENT, VpnService), prominent disclosure and the in-app privacy policy (User Data), Data safety
answers narrower than the SDKs' disclosures, Families (`play.targetAges`: ad SDKs not on Google's self-certified list
or older than their certified version, AD_ID and location in child-only apps, mixed audiences), the AD_ID permission
on targetSdk 33+, 16 KB page sizes (Unity before 6000.0.38f1 / 2022.3.56f1 / 2021.3.48f1, React Native before 0.77,
NDK before r28 without the flags, AGP before 8.5.1 with uncompressed libraries, prebuilt .so files; updates blocked
from 2027-02-01), the closed-testing requirement of new personal accounts (`play.accountType`,
`play.accountCreated`: 12 testers for 14 days), Play metadata (title over 30 characters, emoji, ALL CAPS, rank /
price / promotion terms, other apps, repeated keywords and keyword lists), interstitials shown from the launcher
activity's onCreate, card / PayPal SDKs or "buy on our website" next to in-app products (`play.billingPrograms`),
and brand names in the package or app name (low). Detection reads activities, services and receivers with their
intent filters, accessibility-service configs, strings.xml, the NDK / AGP versions, CMake / ndk-build files,
extractNativeLibs, useLegacyPackaging, Unity ProjectVersion.txt, .so files, and the merged manifest under
build/intermediates when the project was built locally. Device admin has no Play policy page and gets no rule. The
App Store rules are unchanged. New fixtures (play-android, play-native, play-families, play-listing) and Android parts
in the clean fixture; the README rule table has a column per store.
- `storebound scan` (MCP tool `scan`, desktop Checks tab): an offline, read-only scan of the project itself for App
Store and Google Play rejection risks before you submit. Detects the stack (native iOS / Android, React Native,
Expo, Capacitor, Cordova, Flutter, Unity, Godot) and the SDKs from a maintained table (`cli/lib/scan/sdks.ts`: ads,
analytics, attribution, crash, sign-in, payments, push, with versions, file:line and each vendor's data disclosure
page) and reports each risk with the guideline, evidence, fix, severity and skill: 2.1(a) demo account and
placeholder text, 5.1.1(v) account deletion, 4.8 login services, 3.1.1 Restore Purchases and outside payments,
3.1.2(c) subscription information, 5.1.2(i) App Tracking Transparency, 5.1.1(ii) / (iv) purpose strings, 5.1.1(i)
privacy link, privacy manifests (ITMS-91053 / 91061), 2.3.10 other platforms, 2.3.7 keywords, 2.3.8 "For Kids", 4.2
web wrappers, 2.5.1 UIWebView and private URL schemes, 2.5.5 IPv6, ATS, export compliance, 1.3 kids ads, 5.6.1 /
3.2.2(x) review prompts, privacy labels vs SDKs, and on Play the API 36 target SDK requirement (since 2026-08-31),
the delete-account URL, App access, foreground service types, exact alarms, QUERY_ALL_PACKAGES, SMS / call log and
the reserved com.example package name. It also proposes the Data safety / App Privacy / ATT / privacy manifest
answers, each "detected (file:line)" or "ask owner", as a diff against the config; `--write` adds only the detected
answers (new `appPrivacy` block for tracking). `scan.ignore` records findings the owner accepts, with the reason.
`validate` includes the open findings (`--no-scan` skips), `check` now asks for unanswered dataSafety booleans.
Fixtures for iOS, Android, Capacitor, Unity, Flutter, a clean project and the teaser under `tests/fixtures/scan/`.
- CLI workspace for many apps and studios (until now only in the desktop app): `storebound.workspace.json` (or `.ts`)
in a folder above the app folders names the app folders (globs), studios with their store accounts by credential
profile (never a key), an ignore list and the desktop's pairing overrides. Commands: `ws init`, `ws list`,
`ws status` (apps x stores, read in parallel with a concurrency limit), `ws check`, `ws validate`,
`ws run <command> --apps a,b|--all` (read commands; writes as dry runs unless `--apply`; submit, release, rollout,
send, start / stop never), `ws apps import --account <profile>` (store apps paired with the folders by the desktop's
rules) and `ws desktop export | import` (the same model as the desktop's `state.json`; keys stay in the keychain or
the profiles). MCP: `workspace_list`, `workspace_status` (read-only).
- Credential profiles: `~/.storebound/credentials/<profile>.json` (or env `STOREBOUND_PROFILE_<NAME>_<KEY>`), picked
with `--account <profile>` on any command or by the workspace, so many store accounts work from one terminal.
- Release trains in the CLI: `release status` (both stores on one train per version: build > testing > review > live
> rollout %) and `release plan <version>` (what is left on each store in fix order, from check, validate and the
store state; owner steps named, never done), as tables or `--json`. MCP: `release_status`, `release_plan`.
- Shared code instead of copies: the pairing rules (`cli/lib/identity.ts`) and the release lanes (`cli/lib/train.ts`)
moved out of the desktop's webview into `cli/lib/`, and the per-app status read behind `portfolio.status`
(`cli/lib/app-status.ts`) is the one `ws status` and `release` use. The desktop re-exports them unchanged.
- Every endpoint: `storebound api` calls any operation of the App Store Connect API (1303 operations, OpenAPI 4.5.1)
and the Google Play Developer APIs (Android Publisher v3: 145 methods, Play Developer Reporting v1beta1: 25; v1alpha1
has no methods of its own), from the official specs pinned in `specs/` (URLs, dates, SHA-256 in
`specs/SOURCES.json`). `tools/gen-api.ts` turns them into one row per operation (`cli/api/apple.gen.ts`,
`cli/api/play.gen.ts`) plus generic helpers (`cli/api/client.ts`: URL and parameter building, retries on 429 / 5xx
with Retry-After, `--all-pages` over Apple's `links.next` and Google's page tokens), typed from the rows
(`cli/api/types.ts`: path and required parameters, loosely typed JSON:API requests and responses). About 44 KB more
in the npm tarball. Same credentials as every other command (`ascJwt` is now shared from `cli/apple/asc.ts`).
- `storebound api apple|play <operationId | METHOD path> [--param k=v ...] [--body file.json] [--upload file]
[--out file] [--all-pages] [--json] [--dry] [--yes]`: GETs (and the read-only Reporting `:query` POSTs) run;
every write prints the exact request and is only sent with `--yes` in an interactive terminal; owner-only
operations (`cli/api/gate.ts`: deletes, submissions, releases, phased release changes, product page tests, prices,
testers and users, review replies, Play `edits.commit`, activation, refunds, app signing) also need the operation
id typed. Under MCP or the desktop app no write is sent at all.
- `storebound api list [--store] [--search]` and `storebound api coverage`: every operation, counts per API and
resource, and which ones storebound's commands already call (a scan of `cli/`, `cli/api/wrapped.gen.ts`); the full
table is `docs/api-coverage.md`. CI fails when the tables, the scan or the doc are stale against `specs/`.
- MCP: one new read-only tool, `api_get` (GET operations only; not in the desktop profile). New skill `store-api`.
- Signing through the App Store Connect API: `apple signing status` (read-only; also an MCP tool) and
`apple signing ensure` (dry first, asks once). Bundle id capabilities from `apple.signing.capabilities` (added,
never removed), an Apple Distribution certificate when this machine holds no key for a valid one (RSA 2048 key
and CSR made in TypeScript, key kept 0600 in `.storebound/signing/`, never printed; `--keychain` imports key and
certificate), App Store profiles per bundle id (app + `apple.signing.extraBundleIds`) reused while active or
created, installed for Xcode 16+ (`~/Library/Developer/Xcode/UserData/Provisioning Profiles`) and older Xcode
(`~/Library/MobileDevice/Provisioning Profiles`). A 403 on a Developer ID certificate is explained (Account Holder
only), as are the certificate limit and a key without the Admin role.
- `apple build`: finds the workspace / project and scheme (native, Capacitor, React Native, Flutter with
`flutter build ios --config-only`, Unity-iPhone), takes the next free build number from App Store Connect,
writes ExportOptions.plist (`method` app-store-connect, `destination` export, team, signing style, profiles) and
runs xcodebuild archive and -exportArchive with the API key; full logs in `build/ios/`, the IPA checked at the end.
- `apple upload`: the IPA through the App Store Connect API build upload (Build Uploads, API 4.1: reserve, upload
parts, commit with the MD5, poll) or `--via altool` (`altool --upload-package` with the API key); the build number
is checked against App Store Connect first, then storebound waits until the build is VALID (exit 0), INVALID /
FAILED (1) or the timeout (7). Dry first, asks.
- `play build`: Gradle `bundleRelease` (Flutter: `flutter build appbundle`; Capacitor / React Native `android/`,
Unity exports with `launcher/`) with the next free versionCode and the upload key from `ANDROID_KEYSTORE*` (env or
`.storebound/credentials.json`), handed to Gradle as injected signing properties through the environment, never on
the command line. The versionCode is read back from the AAB's manifest. `play upload --track internal|alpha|beta`:
versionCode checked, `--dry` read-only, a real run asks; production stays `play release --production draft`.
- `originality --against <folders>` (also an MCP tool): offline Guideline 4.3 check against the owner's other apps:
name and listing text similarity, icon and screenshot perceptual hashes (dHash + pHash; PNG decoded in TypeScript,
JPEG / WebP through ffmpeg when installed), project structure (file layout, identical files), an overall score,
near-duplicate flags and suggestions; exit 1 when a pair is flagged. The portfolio-originality skill uses it.
- New skill `build-and-sign`; appstore-first-release and play-first-release use the new commands. Zero new
dependencies: plist (XML + binary), ZIP, aapt2 manifest and PNG readers are small TypeScript modules.
- Store operations. Every new command has `--dry` and `--json`; what customers can see, redeem or lose (submitting,
codes, offers, pre-orders, expiring builds, making a page visible) asks first in the terminal and is not an MCP
tool. API details come from the App Store Connect API 4.5.1 OpenAPI spec, Apple's and Google's reference pages and
App Store Connect Help (cited at the top of each `cli/lib/*.ts`); tests run the commands against recorded,
sanitized answers (`tests/store-ops-*.test.ts`, harness in `tests/helpers/store-mock.ts`).
- TestFlight: `apple testflight wait --build N` (polls until VALID / FAILED, progress lines, `--timeout`, exit 0 /
1 / 7) and `apple testflight expire --keep N | --older-than D` (newest, attached and processing builds stay;
`PATCH /v1/builds/{id}` `expired: true`). `apple upload` waits for processing with the same loop
(`waitForBuild` in `cli/apple/testflight.ts`) instead of a copy of its own.
- `apple iap-submit`: IAP, subscription and subscription group versions into a review submission
(`reviewSubmissionItems` with `inAppPurchaseVersion` / `subscriptionVersion` / `subscriptionGroupVersion`, API
4.4.1+; the deprecated `inAppPurchaseSubmissions` are not used). It applies Apple's rules: the first item of each
type only together with a new app version (`--with-version`), later ones alone once the app has an approved
version, a new subscription with its unapproved group, at most 200 items; `--no-submit` stops at the draft,
`--new-version` makes a new draft version of an approved or rejected product. MCP: the read-only plan
`apple_iap_review_plan`.
- Pulls: `apple listing pull`, `play listing pull` (live texts back into `store/*.md`, diff first, `--write` to
write) and `catalog pull` (IAP and subscription texts as a proposal diff; the config is never rewritten,
`--write` saves `.storebound/pull/catalog.json`).
- `whats-new --languages a,b,c`: one file per language in `store/whats-new/`, translated by your agent (skill) or a
`--translate "<command>"` hook of yours (no translation service or key in the CLI); `whats-new check` (Play 500,
App Store 4000, untranslated or identical to the source) and `whats-new apply`.
- Offers: `apple offer-codes` (offer codes for one-time purchases, `inAppPurchaseOfferCodes`; custom and one-time-use
codes only with `--codes`, saved to `.storebound/offer-codes/`), `apple win-back` (win-back offers,
`winBackOffers`), `apple promoted` (promoted purchases: create disabled, order, `--enable`), `apple preorder
status | enable | end` (territory availabilities, `endAppAvailabilityPreOrders`). Google Play pre-registration and
promo codes have no API: console guide in the new `app-store-promotion` skill.
- `apple cpp list | create | upload | submit | visible` (custom product pages, screenshots from a shots variant) and
`apple events list | create | upload | submit` (in-app events with schedule, localizations, card and details
images checked against Apple's sizes).
- `reports apple-sales | apple-finance | play-earnings | play-sales`: Apple `salesReports` / `financeReports`
(vendor number from `ASC_VENDOR_NUMBER` in env or `.storebound/credentials.json`) and the Play earnings / sales
zips from the reports bucket into `.storebound/reports/`, with a summary per product and currency and a CSV.
Zero dependencies: the same small ZIP reader (`cli/lib/zip.ts`, on `node:zlib`) that reads IPAs and AABs.
- `triage`: TestFlight crash reports (signature from each crash log, text and `.ips`), TestFlight screenshot feedback
and Play error reports of the top issues, grouped by signature with devices, OS versions and builds; tester emails
are never requested.
- `aso rank`: the app's rank per keyword and storefront in the public iTunes Search API results plus the first
competitors, one request every 3.5 s (Apple allows about 20 a minute), snapshots in `.storebound/aso/`. Subtitles
are not in any public API and are not shown.
- `ads scan --ios <path>`: AdAttributionKit / SKAdNetwork checks of Info.plist and sources (ad network ids and their
case, `AttributionCopyEndpoint`, re-engagement copies, `NSAdvertisingAttributionReportEndpoint`, attribution
windows and cooldown ranges, conversion value updates); the ads-mediation-checks skill explains each finding.
- New MCP tools (dry by default where they write local files): `apple_testflight_wait`, `apple_iap_review_plan`,
`apple_listing_pull`, `play_listing_pull`, `catalog_pull`, `whats_new_languages`, `whats_new_check`,
`whats_new_apply`, `reports`, `triage`, `aso_rank`, `ads_scan`, `apple_cpp_list | create | upload`,
`apple_events_list | create | upload`, `apple_preorder_status`.
- New logo: a glossy indigo box with an upward arrow (`brand/master/logo-mark.png`, `brand/README.md`). Every logo
file comes from it through `tools/brand-assets.py`: desktop app icons (PNG sizes, `.icns`, a `.ico` with its own
image per size, Windows tiles), the site's favicon.ico, maskable web app icons with a manifest, apple-touch-icon,
the header and footer marks (PNG next to the text wordmark), the desktop window's mark and `site/og.png`. Below
48 px a bold "s" monogram on the same tile. The old hand-written SVG mark and `site/favicon.svg` are gone, and the
no-hand-written-SVG guard no longer has a brand exception.
- Desktop: the signed release showed only its background colour and never started the sidecar. Cause: Tauri's
`freezePrototype` freezes `Object.prototype` before the bundle runs, and d3-color / d3-format (under the visx charts)
assign `constructor` / `toString` on plain objects while loading, which throws on a frozen prototype; the bundle
stopped before React drew anything. Saved state was not the cause. A Vite plugin (`apps/desktop/build/freeze-safe.ts`)
makes those assignments safe. So the window is never blank again: error boundaries for the whole window and each
screen and tab (Copy details, Reload, Reset saved state with a backup), `public/boot.js` for a bundle that fails to
load, `logs/host.log` and `logs/webview.log` in the app data folder (rotated, home as `~`, no keys or e-mail
addresses), a versioned `state.json` with an explicit migration from the D1-D3 format (original kept as
`state.json.v1.bak`; broken entries dropped one by one), "Folder not found" with Locate… / Remove for app folders
that are gone, and a boot check in CI and the macOS release (`tools/desktop-boot-check.ts`: the built window with the
release CSP and the frozen prototype on empty, D1 and D3 saved states).
- Icons: Phosphor replaces lucide (desktop: `@phosphor-icons/react` per-icon imports, "regular" weight, "fill" for the
active nav item and notices; site and fixture art: Phosphor's own files). Store marks are now the real App Store and
Google Play logos from simple-icons, in the neutral text colour next to the store's name; the trademark note is in
docs/desktop.md and the site footer. The no-hand-written-SVG guard allows only the vendored files, byte-equal to
their packages.
- Fixes from a guarded read-only run of the Play paths that need POST / DELETE (edit insert and delete, Reporting API
`:query`, nothing else let through) against a live account:
- `health` / `validate`: Play's top issues search (`errorIssues:search`) answered 400 "Unsupported timezone": it
takes only hour-aligned UTC intervals, unlike the metric timelines (America/Los_Angeles days). It now asks in UTC.
A failing issues search no longer throws away the crash and ANR rates already read (a `vitals-issues` warning and
"not read" in the table instead of "vitals not readable"), and Reporting API errors leave out the query string so
the reason is not cut off in tables.
- `play listing --dry` and `play images --dry` are read-only now: one throwaway edit, the current details,
listings and images (sha256 of each uploaded file) compared with the local copy, the edit deleted. Before, the
dry runs wrote into the edit (PUT details and listings; for images every image set cleared and uploaded) and
called `:validate` before discarding it. The output says per language what would change.
- `play products --dry` names what a run would change per product (listings added / changed / removed, regional
prices old -> new, dropped regions, activation) instead of "update · 174 regions" for every existing product.
- `play products` asks before changing what an existing product costs or where it is sold (regional prices, the
other-regions price, dropped regions, activating a purchase option), the same way `play subscriptions` does: the
diff, then a terminal confirmation (or `--yes`). Without one (MCP, CI, serve without a desktop owner-action token)
those changes are skipped with a warning; new products and listing text still go through. `apple iap` and
`apple appinfo` only create a price schedule or availability where none exists, so they need no gate; price
changes on both stores stay with `prices import` (confirmation, the desktop Prices "Apply" owner action).
- `play status`, `play next-version-code`: the read-only edit goes through one helper that always deletes it and
warns when the delete fails; `next-version-code` counts distinct version codes (one code on two tracks is one).
- Fixes from a second read-only run (GET only) of the desktop read paths against live accounts: `analytics snapshot`
no longer writes a snapshot when every store failed (no ONGOING report request yet, no `play.reportsBucket`); it
exits with both reasons, so the desktop dry run shows them. `analytics compare --json` adds `errors` (why a store has
no days), and the Growth tab shows that reason instead of zero totals and "one day of data". Listing completeness
no longer counts a contact sheet in the language folder as a screenshot. Price sheet round trip (export, then
compare) checked on recorded App Store and Play price answers (`tests/desktop-real-reads.test.ts`).
- Growth tab chart: impressions, downloads and conversion are drawn as an index (100 = each line's own average over
the days shown) on one axis, so downloads no longer lie flat under impressions; hover still shows the real numbers.
- **`storebound import fastlane`** (`--dir`, `--out`, `--profile`, `--dry`; MCP `import_fastlane`, dry run by default):
reads Appfile / Deliverfile (plain `key("value")` lines only; Ruby logic is reported, never run), deliver and supply
metadata and screenshots, and writes `storebound.config.ts`, `store/appstore.md`, `store/listing.md`, the Play icon
and feature graphics, and screenshot frames with a `folder` capture source. App Store and Play language codes are
paired into one `languages[]` entry (`cli/lib/store-locales.ts`). Existing files are never overwritten (a
`<name>.imported.<ext>` copy plus a diff hint). Keys, team ids, the App Review contact and demo account are only
named in the report; submission flags, `price_tier`, the age rating config, lanes and signing are listed with their
storebound counterpart. New skill `migrate-from-fastlane`.
- Store language codes checked against the official lists (2026-10-11): the 50 App Store Connect locale shortcodes
and the 86 Google Play listing codes all match; Zulu (`zu`) is no longer a Play listing language and was dropped;
`import fastlane` also accepts region-less folders of the 2025 App Store languages (`ta`, `sl`, `ur`, ...) and
App Store spellings in Play folders (`pa-IN`, `ur-PK`, `sl-SI`, ...). Sources are cited in `cli/lib/store-locales.ts`.
- Fixes from a read-only run against live store accounts: `apps check` says `api-disabled` (not `permission`) when the
Play Developer Reporting API is off, and `apps list` / serve `apps.list` fail with that reason and the fix instead of
a JSON fragment (API error bodies are kept on one line everywhere); the newest App Store version is the newest by
date (the API groups versions by platform), per-app reads run four at a time; the account check counts every app
of the team; Play prices under 1.00 (no `units` in the answer) no longer read as NaN in `catalog diff` and
subscription price diffs; `health` treats App Store Connect's 404 for builds without field data as no diagnostics;
`validate` no longer blocks on age rating fields accepted submissions leave empty (`gracRatingClassificationNumber`,
`socialMediaAgeRestricted` unless social media is yes; `socialMedia` is a warning).
- **Desktop D5: release pipeline.** The desktop app is a separate signed download from storebound.dev/desktop/ (DMG,
Windows installer, AppImage / deb) that updates itself; the npm package stays the CLI, MCP server and skills.
`storebound desktop` opens the app (in an app folder it adds or selects that folder through
`storebound://open?path=`, after the app's own dialog) or prints the download for this system; `--download` saves
it to ~/Downloads after checking its SHA-256 and minisign signature, and never installs. One desktop version
(`desktopVersion` in package.json, checked in CI), a privacy scan of every artifact (`tools/privacy-scan.ts`),
path-free builds, `tools/desktop-release.ts` (universal macOS app + DMG, Developer ID + notarization when the
certificate exists, otherwise an ad-hoc `-unsigned` preview; self-test of the exact artifacts; signed latest.json),
a Forgejo workflow for Linux and Windows, the root-owned publish step on the server and a downloads page
(`site/desktop/`). Settings > Updates checks and installs signed updates on request. See `docs/desktop-release.md`.
- **Desktop machine interface** (`serve --stdio`): JSON-RPC session for the coming desktop app, built on the MCP core.
Typed methods with argument schemas, structured results, progress notifications, cancellation, `project.inspect`
and `portfolio.status`: apps across iOS and Android with identity, config check, release trains (current and next
version on both stores) and catalog parity (`live` reads App Store and Play status). `system.tools` reports ffmpeg,
Chrome, Xcode tools, simctl, adb and git with versions; `STOREBOUND_TOOL_PATH` / `STOREBOUND_CHROME` point the CLI
at app-managed copies. Writes are dry runs unless the host
sends a single-use confirm token bound to the method, arguments and a short expiry (HMAC with a per-session secret
handed over on stdin). No submit / release / publish method exists. Under `serve`, terminal confirmations always
answer no.
- `--json` on `check` (and `check --aso`), `apple testflight status`, `apple ppo status`, `apple next-build`,
`play next-version-code`, `analytics compare` and `prices import` (read-only diff); new `inspect` command.
- **Desktop app shell** (`apps/desktop`, not released yet): Tauri 2 + React 19, app-centric (Portfolio, App with
Releases / Catalog / Listings / Health / Reviews / TestFlight, App folders, Accounts, Dependencies, Settings), store
keys in the OS keychain, native confirm dialogs with the dry run before any write. See `docs/desktop.md`.
- `--json` on `catalog diff` goes through the same machine-readable result line.
- **`storebound doctor`** (`--json`): ffmpeg, ffprobe, Chrome, playwright, Xcode tools, simctl, adb and git with
versions (the same detection as the desktop Dependencies screen) and the install commands for this OS. It runs none.
- `playwright` is an optional peer dependency (it was only documented before). The compiled desktop sidecar bundles
`playwright-core` and renders with the app's Chrome for Testing (`STOREBOUND_CHROME`), so desktop users need neither.
- `inspect --icon` / `inspect --listings` (listing completeness per language on both stores, rendered screenshots),
`reviews drafts [--json]` (local reply drafts with the send checks), `reviews approve <draft> [--undo] [--dry]`,
`init --dry`; templates are embedded so `init` works from a single-file binary.
- `apple status --json` includes the phased release of the newest live versions; release trains now count a staged
rollout or iOS phased release (also halted / paused) as the current train.
- **Desktop D2** (`apps/desktop`): redesigned shell (command palette, toasts with progress and cancel, skeletons,
illustrated empty states, first-run guide), release timeline per app, catalog parity matrix, listing completeness,
health sparklines against the Play thresholds, reviews inbox with star distribution and draft approval, TestFlight
groups and builds, store readiness with fix hints; offline cache of the last reads (SQLite, "last read" stamps);
one-click ffmpeg (pinned SHA-256) and Chrome for Testing (Google's code signature) downloads into app data;
English and Turkish with an i18n check, 7:1 text contrast checked in both themes.
- `tools/build-sidecars.ts`: single-file executables for macOS (arm64, x64), Linux (x64, arm64) and Windows (x64),
re-signed ad hoc on macOS and checked for build-machine paths. The MCP server now also runs from such a binary.
## 0.5.0 · 2026-10-11
Apps and games: storebound is for every mobile app, not only games, and the in-app catalog is complete on both stores.
- **Positioning**: "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store"
across the README, the site (new "Apps and games" section, app screenshot style, og image), package, plugin and
adapter descriptions, CLI help, MCP server instructions and the skills. Game-specific advice stays, marked for games.
- **`init --profile app|game|subscription-app`** (default `app`): per profile a config, `declarations.md`,
`app-privacy.md`, the Data safety section, listing and screenshot copy and a privacy policy. app = utility /
productivity app with an optional account and analytics, no ads; subscription-app = accounts + auto-renewable
subscriptions with the disclosure text; game = the 0.4 templates. Every answer is flagged "edit to match your app".
Configs without `app.profile` keep working as games.
- **App screenshot style** (`shots.style: 'app'`, the default for the app profiles): calm gradient, a clean device frame,
headline + subline, no sparkles or badges (badges only as quiet chips), status bar text that follows the frame, Inter
by default, app palettes (mist, paper, mint, sand, rose, sky, ocean, slate), its own feature graphic. Game style
unchanged. New demo `examples/demo-app` ("Tidy Lists").
- **App identity**: `app.platforms` (default from the `apple` / `play` sections) and `platforms` on a product or
subscription to sell it on one store only; one shared catalog and shared listings, per-platform settings in
`apple` / `play`.
- **Subscriptions on both stores**: `apple subscriptions` (groups and display names, subscriptions with period, level,
family sharing, localizations, availability, prices from the USA price point and its equalizations plus overrides
and `--csv` rows of the prices sheet incl. scheduled changes, introductory offers per territory, promotional offers,
offer code campaigns, custom codes only with `--codes` after a terminal confirmation, review screenshots) and
`play subscriptions` (listings with benefits, one auto-renewing base plan with grace period and regional prices from
`convertRegionPrices`, overrides and CSV, activation, the intro offer for new subscribers and extra offers chosen by
your app through offer tags). Both print a plan, `--dry` stops there, a terminal run asks first; without a terminal
(MCP) only new records are created and live price changes wait for the owner. Skill `store-subscriptions`.
- **Full IAP catalog**: product type `non_renewing` (Apple NON_RENEWING_SUBSCRIPTION; a consumed one-time product on
Play), and **`catalog diff`**: the config's products and subscriptions against both stores (missing, extra, type,
billing period, level, US and override prices, localizations, offers) plus regions where the two stores price the
same item differently (same currency, `--threshold`). Read-only, exit 1 on mismatches.
- **Review access**: `apple review-details` sends `demoAccountRequired` / `demoAccountName` / `demoAccountPassword`
from `reviewDemoAccount` in `.storebound/credentials.json` (or `REVIEW_DEMO_USER` / `REVIEW_DEMO_PASSWORD`), masked
in every log line, and adds the sign-in steps (`reviewAccess.instructions`) and where account deletion is
(`accounts.inApp`) to the notes (4000-byte check). Skill `review-access`.
- **Account deletion**: `accounts { enabled, deletionUrl, inApp }`; skill `account-deletion` (Apple 5.1.1(v), Play's
in-app + web requirement and Data safety). `dataSafety.extra` adds CSV rows for questions Google does not document
(e.g. the delete-account URL id from a Console export).
- **Checks**: `check` reports the profile (delete-account URL, Data safety consistent with accounts, demo account
present, terms URL with subscriptions, subscription review screenshots); `validate` adds subscription states,
localizations, group names and review screenshots (App Store), active base plans (Play), the delete-account and
terms URLs answering, the demo account on App Review and a Terms of Use link in the descriptions.
- **Ad mediation**: `ads-levelplay-checks` became `ads-mediation-checks` (AdMob Ad Inspector, AppLovin MAX Mediation
Debugger, LevelPlay as a section, app-ads.txt, SKAdNetwork); the old name stays as a pointer skill.
- **MCP tools**: `apple_subscriptions`, `play_subscriptions` (dry run by default), `catalog_diff` (read-only). Custom
offer codes and live price changes are not reachable through MCP.
- Unverified until a live account run (see the README): free promotional offers / offer codes without a price point,
inline subscription prices and intro offers via `PATCH /v1/subscriptions/{id}`, Play relative-discount and untargeted
offers, the Play billing period strings and the undocumented Data safety ids.
## 0.4.0 · 2026-10-10
Wave 2: TestFlight and life after launch (stability, reviews, prices, analytics, experiments), plus MCP 2026-07-28.
- **TestFlight through the API** (`apple testflight status | groups | testers | what-to-test | info | distribute`):
beta groups from `apple.testflight.groups` (internal / external, public links; created when missing, never changed
or deleted), testers from a CSV in `.storebound/` (the config refuses a path outside it; emails masked in every log
line; asks before Apple emails invitations; `--remove`, `--invite`), "What to Test" per locale from the App Store
listing (`### whatToTest`, else the `whats-new` notes), Test Information and Beta App Review contact / notes, a build
by its exact number to groups, Beta App Review submission behind `--submit-review` and a confirmation. Skill
`testflight`.
- **Crash and vitals health** (`health [--weekly]`): Play Developer Reporting API (user-perceived crash and ANR rates,
28-day user-weighted, worst device models, top error issues) and App Store power and performance metrics,
diagnostic signatures and TestFlight crash reports. Play's bad behavior thresholds (1.09 % crash, 0.47 % ANR, 8 % per
device, developer.android.com/topic/performance/vitals): over = blocker, from 80 % = warn, also in `validate`
(`--no-health`). Weekly Markdown report with week-over-week change in `.storebound/health/`. Skill `store-health`.
- **Review replies** (`reviews list | draft | send`): App Store customer reviews / responses and Play reviews; drafts
as Markdown in `.storebound/replies/` (never overwritten), `send` only sends drafts with `approved: true` after a
confirmation and refuses incentives, rating requests and Play replies over 350 characters. Skill `review-replies`.
- **Price sheet** (`prices export | import`): one CSV of product x store x region; diff table first, then (after a
confirmation) one App Store price schedule per changed product (base territory, every manual price, future-dated
changes with `start_date`) and the Play purchase option's regional prices (`convertRegionPrices` for regions without
a price, `--reconvert`). `store-iap-setup` skill: price sheet section.
- **Analytics snapshots** (`analytics setup | snapshot | compare`): App Store Analytics Reports API (Discovery and
Engagement + Downloads, md5-checked segments, newest processing per day) and the Play statistics exports in the
`pubsite_prod_rev_` bucket (`play.reportsBucket`); snapshots in `.storebound/analytics/`, `compare` lines them up
with the git history of the store files. Skill `store-analytics`.
- **MCP 2026-07-28**: stateless requests with per-request `_meta` and `server/discover` (supported versions,
capabilities, instructions; `resultType`, server info in every result's `_meta`, cacheable `tools/list`, -32022 for
unsupported versions); `initialize` clients (2025-11-25 and older) keep working on the same process.
- **Product Page Optimization through the API** (`apple ppo status | create | upload | start | stop`): v2 tests with
one treatment per `shots` variant, a localization per locale and the variant renders in the treatment screenshot
sets; start / stop ask first. Play store listing experiments stay console-only (documented).
- **`iphone-medium` size key** (1206 x 2622, `APP_IPHONE_61`), off by default; `apple.iphoneMedium: true` renders,
checks and uploads it. Apple scales the 6.5" set down when it is missing.
- **MCP tools**: `apple_testflight_status`, `apple_testflight_groups`, `apple_testflight_what_to_test`,
`apple_testflight_info`, `apple_testflight_distribute` (internal groups only), `health`, `health_report`,
`reviews_list`, `reviews_draft`, `prices_diff`, `prices_export`, `analytics_snapshot`, `analytics_compare`,
`analytics_setup`, `apple_ppo_status`, `apple_ppo_create`, `apple_ppo_upload`. Not exposed: tester changes and
invitations, external distribution and Beta App Review, sending replies, applying prices, starting / stopping tests.
- Agent rules: owner-only steps now include review replies, tester invitations, price changes and experiments; personal
data (tester lists, reply drafts, health and analytics files) stays in `.storebound/`.
- `apple screenshots --dry` no longer creates empty screenshot sets.
## 0.3.1 · 2026-10-10
Positioning: storebound is the whole road from zero to "Submit for review", screenshots are one step of it.
- **README** rebuilt around the end-to-end journey: a new diagram (`docs/img/journey.svg`) and a step table for both
stores showing what storebound does through the APIs, what the skills walk your agent through in the consoles
(IARC, target audience, ads, advertising ID, App Privacy, app records, the first Play production send) and what
stays your own click (Send for review / Submit for Review, releases). "API or console, honestly" summary, a new FAQ
("Does it fill the questionnaires?"), quick start along the journey with the npm package, skills in journey order;
the screenshot and video showcase moved to a "Store art" section.
- Package, plugin, Codex, Gemini and marketplace descriptions follow the new positioning.
- `package.json` `bin` path normalised (`cli/index.ts`), so npm keeps the `storebound` command.
## 0.3.0 · 2026-10-10
Screenshots and videos from any engine, reviewed before they leave your machine.
- **Capture from anything** (`shots capture --source`): `web` (Playwright, unchanged), `ios-sim` (simctl boot, 9:41
status bar, launch arguments / deep links, `-AppleLanguages`, warm-up after a cold boot, shuts down only what it
booted), `android-emu` (demo-mode status bar, per-app language, `am start` with deep links / extras, screencap; never
boots, wipes or reboots), `folder` (your own frames, checked for aspect and size). Status bar cropped per device
class. `--dry` prints every simctl / adb command.
- **13" iPad** end to end with `apple.ipad: true`: `ipad13` size (2064 x 2752, 3:4 layout), iPad frame defaults, the
`APP_IPAD_PRO_3GEN_129` set in `apple screenshots`, required by `validate`.
- **Store rules as data** (`cli/shots/spec.ts`, sources cited): pixel sizes per store type, alpha, Play JPEG / 24-bit
PNG and 8 MB, counts per set, feature graphic, app preview rules. Used by the new `shots check` (local, exit 1 on
errors, `--variants`, `--json`) and by `validate` (local renders become blockers next to the store state).
- **`shots gallery`**: one self-contained `index.html` next to the renders (no network): every language x size x
scene, app previews with posters, variant tabs, filters, light / dark, issues outlined with their message.
- **A/B variants**: `shots.variants` (scene order / subset, copy overrides, palettes) rendered with
`shots render --variant b | --variants`, the feature graphic included; `--variant` on `play images`,
`apple screenshots` and `shots jpeg`. The skill explains Play store listing experiments and App Store Product Page
Optimization and what stays manual there.
- **App previews and promo video**: `shots record` (web via Chromium screencast, `simctl io recordVideo`, `adb
screenrecord`, or a folder of clips), `shots video` (886 x 1920 iPhone, 1200 x 1600 iPad, YouTube 9:16 / 16:9,
captions from the screenshot copy in every script, silent or music track, poster, ffprobe checks),
`apple previews` (preview sets per locale, MD5-idempotent, poster frame time code, files checked before upload),
`play listing` sends `play.video` (Play takes a YouTube URL, no upload).
- Contact sheet packed into a grid; Play 7" / 10" tablets drawn in an Android tablet body.
- **MCP**: `shots_check` (read-only), `shots_capture`, `shots_render`, `shots_gallery`, `shots_record`, `shots_video`
(local files, dry run by default), `apple_previews` (dry run by default); `variant` on the upload tools.
- **Skills**: new `app-previews`; `store-screenshots` covers sources, iPad, checks, gallery, variants, experiments and
the iPhone display class Apple now marks as required (6.9" stays the default, scaled down by Apple).
- **Demo game** `examples/demo-game/` ("Prism Puddle", fictional, procedural art) that storebound renders for the
showcase in this README; `--dry` on `shots render`, `shots video`, `shots gallery`.
- README rewritten; `CONTRIBUTING.md`, `SECURITY.md`. Playwright stays a dev dependency; runtime dependencies stay 0.
## 0.2.0 · 2026-10-10
Works with any AI coding tool now, plus the release and review side of shipping.
- **One source, many tools**: skills stay plain Agent Skills (`name` + `description` only); `tools/gen-adapters.ts`
writes the Claude Code plugin + `.mcp.json`, Codex plugin, Gemini CLI extension + `GEMINI.md`, `AGENTS.md`,
`.github/copilot-instructions.md` and a Cursor rule. CI fails when they are stale. Shared agent rules in
`docs/agents-core.md`.
- **`storebound mcp`**: stdio MCP server (2025-11-25), one tool per command, write tools dry run by default, nothing
public or irreversible exposed. No SDK, still zero runtime dependencies.
- **`storebound install --agent claude|codex|cursor|copilot|windsurf|gemini|opencode`**: skills + MCP entry into a
game project, idempotent, never touches other entries, `--dry`.
- New commands: `validate` (read-only readiness report for both stores, ordered blockers, exit 1),
`apple release --phased on|pause|resume|complete` and `--manual-release`, `apple status --wait-review`
(exit 0 / 1 / 7), `apple next-build`, `apple encryption`, `play rollout --fraction|--halt|--resume|--complete`,
`play next-version-code`, `check --aso`, `whats-new --since <tag>`, `privacy-manifest --ios <path>`, `version`.
- New skills: review-rejections, portfolio-originality, release-management, store-preflight, privacy-manifest,
export-compliance, aso-basics, whats-new.
- Updated skills: select builds by explicit number, first availability record UI fallback, phased + manual release
by default, IAP versions in review submissions (API 4.4.1, reported, verify), privacy manifest cross-links, tool-neutral
CLI instructions.
- Fix: Apple IAP description limit is 45 characters (App Store Connect Help), not 55.
- Config: `apple.usesNonExemptEncryption`, `apple.screenshotTypes`, `ads.appAdsTxt`.
- `bun run ci`: adapters check, tests, typecheck, secrets / personal data scrub.
## 0.1.0 · 2026-10-10
First public cut, extracted from the scripts I used to ship my own game.
- `storebound` CLI: `init`, `check`; Play `products`, `listing`, `images`, `data-safety`, `release`, `status`;
App Store `bundle`, `iap`, `iap-review`, `listing`, `screenshots`, `appinfo`, `review-details`, `status`
(incl. canceling a review submission); `icons`; `shots capture | fonts | render | jpeg`.
- Per-project `storebound.config.ts`; credentials only from env or `.storebound/credentials.json`.
- Claude Code plugin + marketplace with 8 skills: play-first-release, appstore-first-release, store-iap-setup,
store-screenshots, app-icons, store-declarations, privacy-policy, ads-levelplay-checks.
- Templates: config, Play + App Store listing copy, screenshot copy (5 languages incl. Arabic + Japanese), Data
safety CSV, App Privacy answers, Play declarations, studio-wide privacy policy page.
- Tests for config validation, copy parsing, Data safety CSV, price mapping, script fonts, args.