store-only apps edit their listing on the store (storebound listing live / set: old -> new per field with the checks, a PATCH of only the edited fields; serve listing.set is the one store-only write, its token bound to the store ids; policy.rs STORE_ONLY_WRITES, write_with_confirm for store targets); screenshot captions per language and scene edited as a text edit of the shots copy JSON (JSON files keep their layout); Play content rating, target audience and App content answers in App info; docs/console-parity.md after the editors (73 rows: 55 complete, 18 with the reason) and tests/console-parity.test.ts keeping it honest

This commit is contained in:
CRTkafa 2026-10-14 00:53:38 +08:00
parent 3cea2af4e1
commit 7ac028421a
33 changed files with 852 additions and 157 deletions

View file

@ -12,8 +12,8 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| API | operations | GET | other | wrapped by a command | owner only |
|---|---:|---:|---:|---:|---:|
| App Store Connect API 4.5.1 | 1303 | 826 | 477 | 200 | 166 |
| Google Play Android Developer API v3 (revision 20261008) | 145 | 45 | 100 | 33 | 49 |
| App Store Connect API 4.5.1 | 1303 | 826 | 477 | 204 | 166 |
| Google Play Android Developer API v3 (revision 20261008) | 145 | 45 | 100 | 34 | 49 |
| Google Play Developer Reporting API v1beta1 (revision 20261008) | 25 | 15 | 10 | 6 | 0 |
## App Store Connect API 4.5.1
@ -231,14 +231,14 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| `appEvents_localizations_getToManyRelationship` | GET | `/v1/appEvents/{id}/relationships/localizations` | read | |
| `appEvents_updateInstance` | PATCH | `/v1/appEvents/{id}` | write (dry by default) | |
| **appInfoLocalizations** (4) | | | | |
| `appInfoLocalizations_createInstance` | POST | `/v1/appInfoLocalizations` | write (dry by default) | |
| `appInfoLocalizations_createInstance` | POST | `/v1/appInfoLocalizations` | write (dry by default) | listing-store |
| `appInfoLocalizations_deleteInstance` | DELETE | `/v1/appInfoLocalizations/{id}` | owner only (typed confirm) | |
| `appInfoLocalizations_getInstance` | GET | `/v1/appInfoLocalizations/{id}` | read | |
| `appInfoLocalizations_updateInstance` | PATCH | `/v1/appInfoLocalizations/{id}` | write (dry by default) | |
| `appInfoLocalizations_updateInstance` | PATCH | `/v1/appInfoLocalizations/{id}` | write (dry by default) | listing-store |
| **appInfos** (20) | | | | |
| `appInfos_ageRatingDeclaration_getToOneRelated` | GET | `/v1/appInfos/{id}/ageRatingDeclaration` | read | apple/appinfo, console/verify, validate |
| `appInfos_ageRatingDeclaration_getToOneRelationship` | GET | `/v1/appInfos/{id}/relationships/ageRatingDeclaration` | read | |
| `appInfos_appInfoLocalizations_getToManyRelated` | GET | `/v1/appInfos/{id}/appInfoLocalizations` | read | apple/listing, onboard, validate |
| `appInfos_appInfoLocalizations_getToManyRelated` | GET | `/v1/appInfos/{id}/appInfoLocalizations` | read | apple/listing, listing-store, onboard, validate |
| `appInfos_appInfoLocalizations_getToManyRelationship` | GET | `/v1/appInfos/{id}/relationships/appInfoLocalizations` | read | |
| `appInfos_getInstance` | GET | `/v1/appInfos/{id}` | read | apple/appinfo |
| `appInfos_primaryCategory_getToOneRelated` | GET | `/v1/appInfos/{id}/primaryCategory` | read | |
@ -339,7 +339,7 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| `appStoreVersionLocalizations_appPreviewSets_getToManyRelationship` | GET | `/v1/appStoreVersionLocalizations/{id}/relationships/appPreviewSets` | read | |
| `appStoreVersionLocalizations_appScreenshotSets_getToManyRelated` | GET | `/v1/appStoreVersionLocalizations/{id}/appScreenshotSets` | read | apple/screenshots, validate |
| `appStoreVersionLocalizations_appScreenshotSets_getToManyRelationship` | GET | `/v1/appStoreVersionLocalizations/{id}/relationships/appScreenshotSets` | read | |
| `appStoreVersionLocalizations_createInstance` | POST | `/v1/appStoreVersionLocalizations` | write (dry by default) | |
| `appStoreVersionLocalizations_createInstance` | POST | `/v1/appStoreVersionLocalizations` | write (dry by default) | listing-store |
| `appStoreVersionLocalizations_deleteInstance` | DELETE | `/v1/appStoreVersionLocalizations/{id}` | owner only (typed confirm) | |
| `appStoreVersionLocalizations_getInstance` | GET | `/v1/appStoreVersionLocalizations/{id}` | read | |
| `appStoreVersionLocalizations_placements_getToManyRelated` | GET | `/v1/appStoreVersionLocalizations/{id}/placements` | read | |
@ -348,7 +348,7 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| `appStoreVersionLocalizations_searchKeywords_deleteToManyRelationship` | DELETE | `/v1/appStoreVersionLocalizations/{id}/relationships/searchKeywords` | owner only (typed confirm) | |
| `appStoreVersionLocalizations_searchKeywords_getToManyRelated` | GET | `/v1/appStoreVersionLocalizations/{id}/searchKeywords` | read | |
| `appStoreVersionLocalizations_searchKeywords_getToManyRelationship` | GET | `/v1/appStoreVersionLocalizations/{id}/relationships/searchKeywords` | read | |
| `appStoreVersionLocalizations_updateInstance` | PATCH | `/v1/appStoreVersionLocalizations/{id}` | write (dry by default) | |
| `appStoreVersionLocalizations_updateInstance` | PATCH | `/v1/appStoreVersionLocalizations/{id}` | write (dry by default) | listing-store |
| **appStoreVersionPhasedReleases** (3) | | | | |
| `appStoreVersionPhasedReleases_createInstance` | POST | `/v1/appStoreVersionPhasedReleases` | write (dry by default) | apple/release |
| `appStoreVersionPhasedReleases_deleteInstance` | DELETE | `/v1/appStoreVersionPhasedReleases/{id}` | owner only (typed confirm) | |
@ -371,7 +371,7 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| `appStoreVersions_appStoreVersionExperimentsV2_getToManyRelationship` | GET | `/v1/appStoreVersions/{id}/relationships/appStoreVersionExperimentsV2` | read | |
| `appStoreVersions_appStoreVersionExperiments_getToManyRelated` | GET | `/v1/appStoreVersions/{id}/appStoreVersionExperiments` | read | |
| `appStoreVersions_appStoreVersionExperiments_getToManyRelationship` | GET | `/v1/appStoreVersions/{id}/relationships/appStoreVersionExperiments` | read | |
| `appStoreVersions_appStoreVersionLocalizations_getToManyRelated` | GET | `/v1/appStoreVersions/{id}/appStoreVersionLocalizations` | read | apple/listing, apple/previews, apple/screenshots, onboard, validate |
| `appStoreVersions_appStoreVersionLocalizations_getToManyRelated` | GET | `/v1/appStoreVersions/{id}/appStoreVersionLocalizations` | read | apple/listing, apple/previews, apple/screenshots, listing-store, onboard, validate |
| `appStoreVersions_appStoreVersionLocalizations_getToManyRelationship` | GET | `/v1/appStoreVersions/{id}/relationships/appStoreVersionLocalizations` | read | |
| `appStoreVersions_appStoreVersionPhasedRelease_getToOneRelated` | GET | `/v1/appStoreVersions/{id}/appStoreVersionPhasedRelease` | read | apple/release, apple/status |
| `appStoreVersions_appStoreVersionPhasedRelease_getToOneRelationship` | GET | `/v1/appStoreVersions/{id}/relationships/appStoreVersionPhasedRelease` | read | |
@ -421,7 +421,7 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| `apps_appPriceSchedule_getToOneRelationship` | GET | `/v1/apps/{id}/relationships/appPriceSchedule` | read | |
| `apps_appStoreVersionExperimentsV2_getToManyRelated` | GET | `/v1/apps/{id}/appStoreVersionExperimentsV2` | read | apple/ppo |
| `apps_appStoreVersionExperimentsV2_getToManyRelationship` | GET | `/v1/apps/{id}/relationships/appStoreVersionExperimentsV2` | read | |
| `apps_appStoreVersions_getToManyRelated` | GET | `/v1/apps/{id}/appStoreVersions` | read | apple/asc, apple/iap-submit, apple/listing, apple/release, apple/resubmit, apple/review-details, apple/status, lib/store-apps, onboard, validate |
| `apps_appStoreVersions_getToManyRelated` | GET | `/v1/apps/{id}/appStoreVersions` | read | apple/asc, apple/iap-submit, apple/listing, apple/release, apple/resubmit, apple/review-details, apple/status, lib/store-apps, listing-store, onboard, validate |
| `apps_appStoreVersions_getToManyRelationship` | GET | `/v1/apps/{id}/relationships/appStoreVersions` | read | |
| `apps_appTags_getToManyRelated` | GET | `/v1/apps/{id}/appTags` | read | |
| `apps_appTags_getToManyRelationship` | GET | `/v1/apps/{id}/relationships/appTags` | read | |
@ -1598,9 +1598,9 @@ Specs: Apple [App Store Connect API 4.5.1](https://developer.apple.com/sample-co
| `androidpublisher.edits.listings.delete` | DELETE | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/{language}` | owner only (typed confirm) | |
| `androidpublisher.edits.listings.deleteall` | DELETE | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings` | owner only (typed confirm) | |
| `androidpublisher.edits.listings.get` | GET | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/{language}` | read | |
| `androidpublisher.edits.listings.list` | GET | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings` | read | play/listing, play/status, validate |
| `androidpublisher.edits.listings.patch` | PATCH | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/{language}` | write (dry by default) | |
| `androidpublisher.edits.listings.update` | PUT | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/{language}` | write (dry by default) | play/listing |
| `androidpublisher.edits.listings.list` | GET | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings` | read | listing-store, play/listing, play/status, validate |
| `androidpublisher.edits.listings.patch` | PATCH | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/{language}` | write (dry by default) | listing-store |
| `androidpublisher.edits.listings.update` | PUT | `androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/{language}` | write (dry by default) | listing-store, play/listing |
| **androidpublisher.edits.testers** (3) | | | | |
| `androidpublisher.edits.testers.get` | GET | `androidpublisher/v3/applications/{packageName}/edits/{editId}/testers/{track}` | read | |
| `androidpublisher.edits.testers.patch` | PATCH | `androidpublisher/v3/applications/{packageName}/edits/{editId}/testers/{track}` | owner only (typed confirm) | |

View file

@ -1,66 +1,71 @@
# Console parity: App Store Connect and Play Console in the desktop app
The desktop app (apps/desktop, [desktop.md](desktop.md)) is meant to replace App Store Connect and Play Console for
everything storebound can read or write. This page lists every owner-editable thing in the two consoles that the CLI
covers, and what the desktop does with it.
The desktop app (apps/desktop, [desktop.md](desktop.md)) replaces App Store Connect and Play Console for everything
storebound can read or write. This page lists every owner-editable thing in the two consoles that the CLI covers, and
what the desktop does with it. `tests/console-parity.test.ts` keeps it honest: every row is edited in the window or
names the reason it is not.
Columns:
- **storebound**: the command that reads or writes it, and where the value lives (the source of truth in the app
folder: `storebound.config.ts`, `store/listing.md`, `store/appstore.md`, `.storebound/`).
folder: `storebound.config.ts`, `store/listing.md`, `store/appstore.md`, the shots copy file, `.storebound/`).
- **View**: the desktop shows the current value.
- **Edit**: the desktop edits it in place (validation, unsaved-changes guard, undo) and saves it into the source file.
- **Store write**: the desktop writes it to the store (dry run in the window, native confirm dialog, then the real
run; owner-only steps through the owner-action dialog).
- **Gap**: what is missing, or why a cell stays "no" on purpose.
- **Edit**: the desktop edits it in place (live counter against the store limit where there is one, the checks of
`check` / `scan` while typing, unsaved-changes mark, undo / redo) and saves it into the source file through
`project.edit` (`storebound edit`: dry run with the diff and the checks in the window, then the native dialog).
- **Store write**: the desktop writes it to the store (the dry run in the window, the native confirm dialog, then the
real run; owner-only steps through the owner-action dialog).
- **Gap**: "none", or why a cell stays "no" on purpose.
Status: **audit before the editors (2026-10-14)**. Every row whose CLI can write the value but the desktop cannot edit
it is a gap. Phase 2 closes them; this table is updated with the result.
Status: **after the editors (2026-10-14)**. The audit before them (Phase 1) had 70 rows, 62 of them with a gap. Now: 73
rows, 55 with nothing missing, 18 with the reason a cell stays "no" (no store API, an owner-only public step, a
secret, or personal data).
## App Store Connect
| Console screen | storebound | View | Edit | Store write | Gap |
| --- | --- | --- | --- | --- | --- |
| App Information > Name (per locale) | `apple listing` (`### name`, apple.name, app.name) | counter only | no | yes (`apple.listing`) | no text editor |
| App Information > Subtitle | `apple listing` (`### subtitle`) | counter only | no | yes | no text editor |
| Version > Promotional Text | `apple listing` (`### promotionalText`) | counter only | no | yes | no text editor |
| Version > Description | `apple listing` (`### description`) | counter only | no | yes | no text editor |
| Version > Keywords | `apple listing` (`### keywords`), `check --aso` | counter only | no | yes | no editor, no keyword checks while typing |
| Version > What's New | `apple listing`, `whats-new apply` (`### whatsNew`) | no | no | yes | no editor |
| Version > Support URL | `apple listing` (app.supportUrl) | no | no | yes | not shown, no editor |
| Version > Marketing URL | `apple listing` (app.marketingUrl) | no | no | yes | not shown, no editor |
| App Privacy > Privacy Policy URL | `apple listing` (app.privacyPolicyUrl) | no | no | yes | not shown, no editor |
| Version > Copyright | `apple listing` (app.copyright) | no | no | yes | not shown, no editor |
| Version > version string, build | `apple listing --version --build` | yes | version field | yes | none |
| Version > Screenshots (per device, locale) | `shots render`, `apple screenshots` | renders | no | yes (`apple.screenshots`) | no ordering or replacement of scenes |
| Version > App Previews | `shots video`, `apple previews` | renders | no | yes | no replacement |
| App Information > Category, subcategories | `apple appinfo` (apple.info.categories) | yes | no | yes (`apple.appinfo`) | no editor |
| App Information > Age Rating | `apple appinfo` (apple.info.ageRating), console plan | yes | no | yes | no editor of the answers |
| App Information > Content Rights | `apple appinfo` (apple.info.contentRights) | yes | no | yes | no editor |
| Pricing and Availability > Free, territories | `apple appinfo` (apple.info.free) | yes | no | yes | no editor |
| Pricing and Availability > Pre-orders | `apple preorder` | no | no | terminal (owner) | public step; terminal only |
| App Privacy (data types, purposes) | appPrivacy.types, `console fill apple-app-privacy` | yes | no | no API (console fill) | no editor of the answers |
| App Review Information > Contact | `apple review-details` (.storebound/credentials.json reviewContact) | masked | no | yes (`apple.review_details`) | no editor |
| App Review Information > Notes | `apple review-details` (apple.info.reviewNotes, reviewAccess.instructions) | yes | no | yes | no editor |
| App Review Information > Sign-in required, demo account | reviewAccess.signInRequired, credentials.json reviewDemoAccount | masked | no | yes | no editor of sign-in required |
| Export compliance (encryption) | `apple encryption` (apple.usesNonExemptEncryption) | yes | no | yes | no editor |
| In-App Purchases > reference name, display name, description per locale | `apple iap` (products[]) | parity only | no | yes (`apple.iap`) | no editor |
| In-App Purchases > price | products[].usd, overrides, `prices import` | yes (Prices) | no | yes, owner gate | no editor of the base price |
| In-App Purchases > review note, screenshot | products[].reviewNote / reviewScreenshot, `apple iap-review` | no | no | yes (`apple.iap_review`) | not shown, no editor |
| Subscriptions > group name per locale | `apple subscriptions` (subscriptions[].listings) | no | no | yes | no editor |
| Subscriptions > name, description per locale | `apple subscriptions` (subscriptions[].subscriptions[].listings) | parity only | no | yes | no editor |
| Subscriptions > level, period, price | `apple subscriptions` | parity only | no | yes, owner gate for live prices | no editor |
| Subscriptions > introductory and promotional offers | subscriptions[].subscriptions[].intro / offers | no | no | yes (sync) | no editor |
| Subscriptions > offer codes | `apple subscriptions` (offerCodes), `apple offer-codes` | no | no | terminal (owner) | no editor |
| Subscriptions > win-back offers | `apple win-back` (winBack) | no | no | terminal (owner) | no editor |
| In-App Purchases > promoted purchases | `apple promoted` (apple.promoted) | no | no | terminal (owner) | no editor |
| Custom product pages | `apple cpp list / create / upload` (apple.customProductPages) | no | no | CLI only | not in the window |
| In-app events | `apple events list / create / upload` (apple.events) | no | no | CLI only | not in the window |
| Product Page Optimization | `apple ppo` | yes | create / start / stop | yes (owner start / stop) | none |
| TestFlight > What to Test (per build) | `apple testflight what-to-test` | yes | yes (text) | yes | per-locale text not editable |
| TestFlight > groups | `apple testflight groups sync` (apple.testflight.groups) | yes | no | yes (create missing) | no group editor |
| TestFlight > Test Information (beta description, notes) | `apple testflight info` (apple.testflight.description / reviewNotes) | no | no | CLI only | not in the window |
| TestFlight > testers | `owner.testflight_testers` (.storebound/testers.csv) | count | no | yes (owner) | CSV not editable |
| App Information > Name (per locale) | `apple listing` (`### name`, apple.name, app.name) | yes | yes | yes (`apple.listing`) | none |
| App Information > Subtitle | `apple listing` (`### subtitle`) | yes | yes | yes | none |
| Version > Promotional Text | `apple listing` (`### promotionalText`) | yes | yes | yes | none |
| Version > Description | `apple listing` (`### description`) | yes | yes | yes | none |
| Version > Keywords | `apple listing` (`### keywords`), `check --aso` | yes | yes (keyword rules while typing) | yes | none |
| Version > What's New | `apple listing`, `whats-new apply` (`### whatsNew`) | yes | yes | yes | none |
| Version > Support URL | `apple listing` (app.supportUrl) | yes | yes | yes | none |
| Version > Marketing URL | `apple listing` (app.marketingUrl) | yes | yes | yes | none |
| App Privacy > Privacy Policy URL | `apple listing` (app.privacyPolicyUrl) | yes | yes | yes | none |
| Version > Copyright | `apple listing` (app.copyright) | yes | yes | yes | none |
| Version > version string, build | `apple listing --version --build` | yes | yes | yes | none |
| Version > Screenshots (per device, locale) | `shots render`, `apple screenshots` (shots.scenes, the shots copy file) | yes | yes (scene order, the frame of each scene, the captions per language) | yes (`apple.screenshots`) | none |
| Version > App Previews | `shots record`, `shots video`, `apple previews` | yes | n/a | yes (`apple.previews`) | reason: a preview is cut from recorded clips, not a field; replacing one is a new recording (`shots.video` render job in the window) |
| App Information > Category, subcategories | `apple appinfo` (apple.info.categories) | yes | yes | yes (`apple.appinfo`) | none |
| App Information > Age Rating | `apple appinfo` (apple.info.ageRating), `console fill apple-age-rating` | yes | yes (every question) | yes | none |
| App Information > Content Rights | `apple appinfo` (apple.info.contentRights) | yes | yes | yes | none |
| Pricing and Availability > Free, territories | `apple appinfo` (apple.info.free) | yes | yes | yes | none |
| Pricing and Availability > Pre-orders | `apple preorder status / enable / end` | no | no | no | reason: putting an app on pre-order or ending it is public at once; it stays the owner's terminal step (`apple preorder enable --release-date`, asks first), there is no config value to edit |
| App Privacy (data types, purposes) | appPrivacy.types, `console fill apple-app-privacy` | yes | yes (checked JSON, validated with the config) | no | reason: App Privacy has no API; Fill in console types the saved answers, the owner presses Publish |
| App Review Information > Contact | `apple review-details` (.storebound/credentials.json reviewContact) | masked | yes (write-only, masked) | yes (`apple.review_details`) | none |
| App Review Information > Notes | `apple review-details` (apple.info.reviewNotes, reviewAccess.instructions) | yes | yes | yes | none |
| App Review Information > Sign-in required | reviewAccess.signInRequired | yes | yes | yes | none |
| App Review Information > Demo account | credentials.json reviewDemoAccount | masked | no | yes (sent by `apple.review_details`) | reason: a password; the window never reads or takes it (AGENTS.md rule 4), the owner puts it in `.storebound/credentials.json` |
| Export compliance (encryption) | `apple encryption` (apple.usesNonExemptEncryption) | yes | yes | yes (`apple.encryption`) | none |
| In-App Purchases > reference name, display name, description per locale | `apple iap` (products[]) | yes | yes | yes (`apple.iap`) | none |
| In-App Purchases > price | products[].usd, overrides, `prices import` | yes | yes | yes, owner gate for live products | none |
| In-App Purchases > review note, screenshot | products[].reviewNote / reviewScreenshot, `apple iap-review` | yes | yes | yes (`apple.iap_review`) | none |
| Subscriptions > group name per locale | `apple subscriptions` (subscriptions[].listings) | yes | yes | yes (`apple.subscriptions`) | none |
| Subscriptions > name, description per locale | `apple subscriptions` | yes | yes | yes | none |
| Subscriptions > level, period, price | `apple subscriptions` | yes | yes | yes, owner gate for live prices | none |
| Subscriptions > introductory and promotional offers | subscriptions[].subscriptions[].intro / offers | yes | yes (checked JSON) | yes (`apple.subscriptions`) | none |
| Subscriptions > offer codes | offerCodes, `apple subscriptions`, `apple offer-codes --codes` | yes | yes (checked JSON) | yes for the offers; the codes: no | reason: generating codes makes vouchers anyone can redeem; it stays the owner's terminal step (`apple offer-codes --codes`, asks first, codes saved to .storebound/) |
| Subscriptions > win-back offers | winBack, `apple win-back` | yes | yes (checked JSON) | no | reason: a win-back offer goes live by itself on its start date; creating one is the owner's terminal step (`apple win-back`, asks first) |
| In-App Purchases > promoted purchases | apple.promoted, `apple promoted` | yes | yes (checked JSON) | no | reason: promoting shows the purchase on the product page; `apple promoted [--enable]` asks first and stays the owner's terminal step |
| Custom product pages | `apple cpp create / upload` (apple.customProductPages) | yes | yes (checked JSON) | yes (`apple.cpp_create`, `apple.cpp_upload`) | none |
| In-app events | `apple events create / upload` (apple.events) | yes | yes (checked JSON) | yes (`apple.events_create`, `apple.events_upload`) | none |
| Product Page Optimization | `apple ppo` | yes | yes | yes (owner start / stop) | none |
| TestFlight > What to Test (per build, per locale) | `apple testflight what-to-test` (`### whatToTest`, else `### whatsNew`) | yes | yes (per locale) | yes (`apple.testflight_what_to_test`) | none |
| TestFlight > groups | `apple testflight groups sync` (apple.testflight.groups) | yes | yes (checked JSON) | yes (creates the missing groups) | none |
| TestFlight > Test Information (beta description, notes) | `apple testflight info` (apple.testflight.description / reviewNotes) | yes | yes | yes (`apple.testflight_info`) | none |
| TestFlight > testers | `owner.testflight_testers` (.storebound/testers.csv) | count | no | yes (owner) | reason: tester emails are personal data kept in the gitignored CSV; adding them is the owner action, removing them stays a terminal step |
| Ratings and Reviews > reply | `reviews draft / reply / approve / send` | yes | yes | yes (owner send) | none |
| Version > phased release | `owner.ios_phased` | yes | n/a | yes (owner) | none |
| Version > Submit for review | `owner.apple_submit` / `owner.apple_resubmit` | yes | n/a | yes (owner) | none |
@ -69,36 +74,38 @@ it is a gap. Phase 2 closes them; this table is updated with the result.
| Console screen | storebound | View | Edit | Store write | Gap |
| --- | --- | --- | --- | --- | --- |
| Main store listing > App name (per language) | `play listing` (`### title`, app.name) | counter only | no | yes (`play.listing`) | no text editor |
| Main store listing > Short description | `play listing` (`### short`) | counter only | no | yes | no text editor |
| Main store listing > Full description | `play listing` (`### full`) | counter only | no | yes | no text editor, no stuffing check while typing |
| Release > Release notes (per language) | `play release`, `whats-new apply` (`### What's new`) | counter only | no | with the release (owner) | no editor |
| Main store listing > Graphics (icon, feature graphic, screenshots) | `icons`, `shots render`, `play images` | renders | no | yes (`play.images`) | no ordering or replacement of scenes |
| Main store listing > Video (YouTube URL) | `play listing` (play.video) | no | no | yes | not shown, no editor |
| Store settings > Contact email | `play listing` app details (app.contactEmail) | no | no | yes | not shown, no editor |
| Store settings > Website | `play listing` app details (app.website) | no | no | yes | not shown, no editor |
| Store settings > App category | play.category, console plan (no API) | yes | no | no API | no editor |
| App content > Privacy policy | app.privacyPolicyUrl, console plan | no | no | no API | not shown, no editor |
| App content > Data safety | `play data-safety` (dataSafety) | yes | no | yes (`play.data_safety`) | no editor of the answers |
| App content > Content rating (IARC) | social, products[].randomRewards, `console fill play-content-rating` | yes | no | no API (console fill) | no editor of the answers |
| App content > Target audience | play.targetAges, `console fill play-target-audience` | plan | no | no API (console fill) | no editor |
| Monetize > In-app products: title, description per language | `play products` (products[].listings) | parity only | no | yes (`play.products`) | no editor |
| Monetize > In-app products: price | products[].usd / overrides, `prices import` | yes (Prices) | no | yes, owner gate | no editor of the base price |
| Monetize > Subscriptions: title, benefits, description per language | `play subscriptions` | parity only | no | yes | no editor |
| Monetize > Subscriptions: base plans, offers, price | `play subscriptions` | parity only | no | yes, owner gate | no editor |
| Pricing > regional prices (price sheet) | `prices export / import` | yes | no | yes (owner) | sheet not editable in place |
| Production > Countries / regions | play.countries, console plan | count | no | no API | no editor |
| Publishing overview > Managed publishing | play.managedPublishing | yes | no | no API | no editor |
| Testing tracks (internal, closed, open) | `play release --track` | status | n/a | terminal (owner) | sends builds to testers: not in the window by design |
| Main store listing > App name (per language) | `play listing` (`### title`, app.name) | yes | yes | yes (`play.listing`) | none |
| Main store listing > Short description | `play listing` (`### short`) | yes | yes | yes | none |
| Main store listing > Full description | `play listing` (`### full`) | yes | yes (stuffing and claims checks while typing) | yes | none |
| Release > Release notes (per language) | `play release`, `whats-new apply` (`### What's new`) | yes | yes | yes (with the release: `owner.play_production`) | none |
| Main store listing > Graphics (icon, feature graphic, screenshots) | `icons`, `shots render`, `play images` | yes | yes (scene order, frames, captions) | yes (`play.images`) | none |
| Main store listing > Video (YouTube URL) | `play listing` (play.video) | yes | yes | yes | none |
| Store settings > Contact email | `play listing` app details (app.contactEmail) | yes | yes | yes | none |
| Store settings > Website | `play listing` app details (app.website) | yes | yes | yes | none |
| Store settings > App category | play.category, console plan | yes | yes | no | reason: no API; the console plan step (App info > Console steps) uses the saved value |
| App content > Privacy policy | app.privacyPolicyUrl, console plan | yes | yes | no | reason: no API; the console plan step uses the saved value |
| App content > Data safety | `play data-safety` (dataSafety) | yes | yes | yes (`play.data_safety`) | none |
| App content > Content rating (IARC) | console.contentRating, `console fill play-content-rating` | yes | yes (checked JSON) | no | reason: no API; Fill in console types the saved answers, the owner presses Submit |
| App content > Target audience | play.targetAges, console.targetAudience, `console fill play-target-audience` | yes | yes | no | reason: no API; Fill in console types the saved answers, the owner saves |
| App content > other declarations (ads ID, government, finance, health) | console.appContent, console plan | yes | yes (checked JSON) | no | reason: no API; the console plan step uses the saved answers |
| Monetize > In-app products: title, description per language | `play products` (products[].listings) | yes | yes | yes (`play.products`) | none |
| Monetize > In-app products: price | products[].usd / overrides, `prices import` | yes | yes | yes, owner gate for live products | none |
| Monetize > Subscriptions: title, benefits, description per language | `play subscriptions` | yes | yes | yes (`play.subscriptions`) | none |
| Monetize > Subscriptions: base plans, offers, price | `play subscriptions` | yes | yes | yes, owner gate for live prices | none |
| Pricing > regional prices (price sheet) | `prices export / import`, overrides | yes | yes (per-product overrides in the Catalog editor) | yes (owner) | none |
| Production > Countries / regions | play.countries, console plan | yes | yes | no | reason: no API for the production countries; the console plan step uses the saved list |
| Publishing overview > Managed publishing | play.managedPublishing | yes | yes | no | reason: no API; only Play Console sets it (the console plan names the switch) |
| Testing tracks (internal, closed, open) | `play release --track` | yes | n/a | no | reason: sends a build to testers; the owner's terminal step by design (desktop.md safety model) |
| Production > staged rollout | `owner.play_rollout` | yes | n/a | yes (owner) | none |
| Production > Send to production | `owner.play_production` | yes | n/a | yes (owner) | none |
| Ratings and reviews > reply | `reviews draft / reply / approve / send` | yes | yes | yes (owner send) | none |
| Internal app sharing | `play share` | no | n/a | CLI only | not in the window |
| Internal app sharing | `play share` | no | n/a | no | reason: uploads a build file for testers, not a console field; a terminal or agent step (`play share`) |
## Both stores
| What | storebound | View | Edit | Store write | Gap |
| --- | --- | --- | --- | --- | --- |
| Pull the live texts into the files | `apple listing pull`, `play listing pull`, `catalog pull` | no | n/a | n/a (writes local files) | not in the window |
| Store-only app (no folder): listing text | (none) | yes (status) | no | no | store-only apps are read-only in the window |
| Uncommitted changes in the app folder | git | no | n/a | n/a | not shown |
| Pull the live texts into the files | `apple listing pull`, `play listing pull` | yes | n/a | n/a (writes the listing files after the diff) | none |
| Store-only app (no folder): listing text per language | `listing live`, `listing set` | yes | yes | yes (`listing.set`, one language per apply) | none |
| Store-only app (no folder): URLs, contact, catalog, art | (the config of an app folder) | yes (reads) | no | no | reason: those writes send what an app folder's config holds; "Link a folder" or "Start an app folder" turns the app into a full one |
| Uncommitted changes in the app folder | git status of the edited files | yes | n/a | n/a | none (storebound never commits) |

View file

@ -170,8 +170,9 @@ host state file (`overrides_set`, every key checked in `workspace.rs`).
`store:ios:<account>:<app id>:<bundle>;android:<account>:<package>`.
- The host checks the target and the account kinds, and passes `virtual: { name, apple, play }`. The sidecar writes
a minimal `storebound.config.json` (store ids, placeholder links) into its temp folder.
- Only `apple.status`, `play.status`, `health`, `reviews.list`, `apple.testflight_status` and `apple.ppo_status` work
this way (tabs Releases, Health, Reviews, TestFlight, Growth), and `portfolio.status` takes store-only entries.
- Only `apple.status`, `play.status`, `health`, `reviews.list`, `apple.testflight_status`, `apple.ppo_status` and
`listing.live` work this way (tabs Releases, Listings, Health, Reviews, TestFlight, Growth), plus the one write
`listing.set` (listing text per language, see "Editors"), and `portfolio.status` takes store-only entries.
- "Link a folder", "Start an app folder here" or "Import from fastlane" turn it into a full app.
### Settings
@ -218,8 +219,14 @@ is [console-parity.md](console-parity.md).
first, then the native dialog, then the real run. Price changes of live products stay the owner's price decision.
- **Pull from the store** (`play.listing_pull`, `apple.listing_pull`): the live texts into the files, the diff in the
dialog first; unsaved edits stay pending on top.
- **Store-only apps** (no folder) stay read-only in the window: a store write sends what the files hold (see
console-parity.md).
- **Store-only apps** (no folder) get a Listings tab too: the live texts per language (`listing.live` = `storebound
listing live`), edited with the same counters and checks and applied straight to the store per language
(`listing.set` = `storebound listing set`: the store's dry run in the window, the native dialog, a PATCH of only the
edited fields). It is the one write a store-only app may run (`VIRTUAL_WRITES` in serve.ts, `STORE_ONLY_WRITES` in
policy.rs); its token is bound to the store ids instead of a folder. URLs, contact, catalog and art need an app
folder (they send what its config holds).
- **Captions:** the screenshot captions per language and scene (the shots copy file) are edited in Store art as a
text edit of the JSON (its own layout, one scene per line, stays).
- `tests/edit.test.ts` (round trips, checks, dry run, CLI), `apps/desktop/tests/edits.test.ts` (unsaved edits, undo),
and the boot check's editor case (edit a field, counter and limit check, review, save: the fixture file changes by
one line, the config keeps its comments, Apply shows the dry run per language).