Google binds its session cookies to the device (Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign the owner out of their real Chrome. The default is now storebound's own profile per store; storebound starts Chrome itself (--user-data-dir, --remote-debugging-port=0, no --enable-automation, navigator.webdriver false) and attaches over CDP. --use-chrome-session google|apple is an explicit opt-in that warns. /signin/rejected is said plainly.
733 lines
71 KiB
Markdown
733 lines
71 KiB
Markdown
# Changelog
|
|
|
|
## Unreleased
|
|
|
|
- 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.
|