1795 lines
165 KiB
Markdown
1795 lines
165 KiB
Markdown
|
|
# Changelog
|
|
|
|
## Unreleased
|
|
|
|
- `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.
|
|
- `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.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.
|