console: agent-first mode (console plan / console verify), verified App Privacy and Play Create app facts

- `storebound console plan <form> [--json] [--lookup]`: read-only plan for an AI agent that drives the browser
  itself: start URL (deep links), ordered steps, answer sheet with the console's exact labels (certain / ask owner,
  curly quotes noted), never-press list, verified UI facts, driving guidance (in-page async loop + window.__log),
  verify instructions. All six forms.
- `storebound console verify <form> [--from-text <file|->]`: App Privacy per-type summaries from page text, Play
  Create app from input states, content rating / target audience summaries, App Store record and age rating from
  the API; exit 1 on a mismatch.
- MCP: console_plan, console_verify (read-only); console fill stays off MCP.
- App Privacy (verified 2026-10-12): two information steps, unlabeled purpose checkboxes (nearest parent text),
  "Definitions and Examples" never pressed; fixture and engine follow.
- Play Create app (verified 2026-10-12): /developers/<id>/create-new-app, App package name + read-only Check
  availability, material-radio inner [role=radio] click, app dashboard URL and play.appId, service account
  "Add app" as an owner step.
- console-autofill skill rewritten agent-first; agents-core rule 10; README; pitfalls 12, 18 (confirmed sign-out),
  19; CHANGELOG; play-first-release.
This commit is contained in:
CRTkafa 2026-10-12 08:22:03 +08:00
parent a9261a8052
commit c9a7f095c4
34 changed files with 1430 additions and 113 deletions

View file

@ -72,10 +72,10 @@ export function formMeta(id: FormId, config: ShipConfig): FormMeta {
open: ['General > App Information > Age Ratings > Set Up Age Ratings (only to look: `storebound apple appinfo` sets it)'] };
case 'apple-app-privacy': return { id, store: 'apple', title: 'App Store Connect: App Privacy', url: privacy, stopBefore: 'Publish', fillable: true,
open: [...(config.apple?.appId ? [] : ['Apps > your app (set apple.appId so storebound opens it directly)']), 'App Privacy in the sidebar (storebound clicks Get Started, or Edit next to Data Types, and walks the questionnaire itself)'] };
case 'play-app-record': return { id, store: 'play', title: 'Play Console: Create app', url: PLAY, stopBefore: 'Create app', fillable: true,
open: ['Pick the developer account if Play Console asks (play.developerId or play.developerAccount picks it)', 'Home (All apps) > Create app'],
case 'play-app-record': return { id, store: 'play', title: 'Play Console: Create app', url: config.play?.developerId ? `${PLAY}/u/0/developers/${encodeURIComponent(config.play.developerId)}/create-new-app` : PLAY, stopBefore: 'Create app', fillable: true,
open: config.play?.developerId ? ['the Create app form of the developer account opens by link (/developers/<play.developerId>/create-new-app)'] : ['Pick the developer account if Play Console asks (play.developerId or play.developerAccount picks it)', 'Home (All apps) > Create app (or /console/u/0/developers/<developer id>/create-new-app)'],
notes: [
'The package name is not part of this form: the first AAB upload sets it for good (storebound play upload --track internal, or storebound flow run release).',
'The form has an App package name field (play.packageName, at most 150 characters): storebound fills it and presses "Check availability" (read-only), which should say "Package name available". The package name is fixed for good once the app is created.',
'Free or paid is an owner check: a free app can never become paid later on Google Play.',
'The Developer Program Policies and US export laws boxes are agreements: storebound never ticks them. Read and tick them yourself, then press Create app.',
] };
@ -138,6 +138,8 @@ export const playLanguageLabel = (code: string): string | null => (PLAY_LANGUAGE
/** The Play app name limit (Create app form and store listing title). */
export const PLAY_NAME_MAX = 30;
/** The Create app form's App package name limit (verified 2026-10-12). */
export const PLAY_PACKAGE_MAX = 150;
function playAppRecord(config: ShipConfig): Answer[] {
const p = config.play;
@ -150,9 +152,12 @@ function playAppRecord(config: ShipConfig): Answer[] {
const profile = config.app.profile;
const sells = (config.products?.length ?? 0) > 0 || (config.subscriptions?.length ?? 0) > 0, ads = (config.ads?.appAdsTxt?.length ?? 0) > 0;
const once = 'A free app can never become paid later on Google Play (a paid app can become free).';
const pkg = p?.packageName;
const out: Answer[] = [
len <= PLAY_NAME_MAX ? ok('name', 'App name', name, nameSrc, 'Shown on Google Play; the store listing can change it later.')
: ask('name', 'App name', nameSrc, `"${name}" is ${len} characters; Play allows ${PLAY_NAME_MAX}. Set play.name to a shorter name.`),
pkg && pkg.length <= PLAY_PACKAGE_MAX ? ok('packageName', 'App package name', pkg, 'play.packageName', 'Fixed for good once the app is created. Press "Check availability" next to it (read-only): "Package name available".')
: ask('packageName', 'App package name', 'play.packageName', pkg ? `"${pkg}" is longer than ${PLAY_PACKAGE_MAX} characters.` : 'Set play.packageName (the applicationId of the Android build): it is fixed for good once the app is created.'),
lang ? ok('language', 'Default language', lang, langSrc)
: ask('language', 'Default language', 'play.defaultLanguage', code ? `Pick the language of code "${code}" (storebound does not know its Play Console name).` : 'Set play.defaultLanguage (e.g. en-US) or a play code on the first language.'),
profile ? ok('type', 'App or game', profile === 'game' ? 'Game' : 'App', 'app.profile', 'Changeable later in the store settings.')