// AI-assisted autofill of console forms that have no API, in a visible Chromium the owner signs in to: // storebound console fill lists the forms // storebound console fill
--dry [--json] the answers and the step-by-step guidance; opens no browser // storebound console fill [--store apple|play] [--json] [--profile ] [--use-chrome-session google|apple] // Forms: apple-app-record, apple-age-rating (guide only: `storebound apple appinfo` sets it), apple-app-privacy, // play-app-record (Play Console's Create app form: the Play Developer API cannot create apps), play-content-rating, // play-target-audience. Answers come from the config only (cli/lib/console-answers.ts); anything // it does not prove stays EMPTY and highlighted (AGENTS.md rule 5). // Safety: storebound never types passwords, never touches sign-in pages (it waits while the owner signs in), never reads // cookies or storage, and never clicks Save / Submit / Publish / Create / Next: every click goes through safeClick and // its gate. After sign-in it opens the form itself (navPlan in cli/lib/console-forms.ts: Apps > + > New App, the app's // App Privacy page, Play app list > the app > App content > Start / Manage); those clicks go through navClick, which // applies the same gate (only a link named exactly Start / Manage / Başla / Yönet is let through). A sign-in page at // any point (signInReason: the top-level URL, the main frame's form, an Apple sign-in frame; never a Google widget // frame) -> storebound waits for the owner, then starts again at step 1. A step not // found in time -> the printed steps, and storebound keeps watching (fillSession). With several Play developer // accounts or App Store Connect teams behind one login, the account that has the app is found and remembered // (cli/console/accounts.ts). The owner reviews each page, clicks Next, and presses the // final button; the command ends when the window is closed. Locators are accessible names only // (cli/lib/console-forms.ts); a missing or ambiguous field means the UI changed: filling stops and the guidance is // printed instead. Not exposed over MCP. // Browser (cli/console/session.ts): storebound's own profile (~/.storebound/console-profile/, or --profile / // STOREBOUND_CONSOLE_PROFILE) in a Chrome storebound starts without automation switches; the owner's running Chrome // when it has remote debugging (--cdp); a copy of the owner's Chrome sign-in only with --use-chrome-session google|apple // (opt-in: Google binds its cookies to the device and can sign the owner out of the real Chrome). // Google's /signin/rejected page is said plainly (GOOGLE_REJECTED). // Sources (checked October 2026): see cli/lib/console-answers.ts; Playwright launchPersistentContext, getByRole, // getByLabel: https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context , // https://playwright.dev/docs/locators import { homedir } from 'node:os'; import { join } from 'node:path'; import type { Ctx } from '../lib/ctx'; import { type Answer, type ConsoleResult, consoleResult, DISCLAIMER, FORM_IDS, type FormId, fillValue, formMeta, isForm, valueText } from '../lib/console-answers'; import type { ShipConfig } from '../lib/config'; import { APPLE_PRIVACY_TYPES } from '../lib/app-privacy'; import { ALL_APPS, APP_PRIVACY_HEADING, consoleReached, CREATE_FORM_FIELD, createAppAllowed, DATA_TYPES_HEADING, type FieldDef, FLOW_NAMES, flowAllowed, flowClass, type FlowWhere, type FormDef, formDef, FORM_OPENERS, forbiddenControl, quoteTolerant, forbiddenTick, hasToken, isAppleSignInFrame, isSignInText, isSignInUrl, type Name, navAllowed, type NavBases, type NavStep, navPlan, setUpAllowed, setUpTexts, typeLabels } from '../lib/console-forms'; import { log, printJson, warn } from '../lib/log'; import { openAcross, withAccountIds } from './accounts'; import { appleAppId, withAppleAppId } from './app-id'; import { bareUrl } from './debug'; import { type ConsoleBrowser, GOOGLE_REJECTED, googleRejected, openConsoleBrowser, parseUseChromeSession } from './session'; /* eslint-disable @typescript-eslint/no-explicit-any */ export type Page = any; export type Locator = any; const TAG = 'console'; /** The longest any single click or check may take (Playwright waits for the control to be enabled and stable). */ export const ACTION_MS = 10_000; const CHECK = 'storebound: check this'; /** Locators with only the visible matches (hidden earlier pages stay in many consoles' DOM). */ export const vis = (l: Locator): Locator => { try { return l.filter({ visible: true }); } catch { return l; } }; const nameOpts = (n: Name) => (typeof n === 'string' ? (/['"\u2018\u2019\u201c\u201d]/.test(n) ? { name: quoteTolerant(n) } : { name: n, exact: true }) : { name: n }); type Found = { state: 'ok'; loc: Locator } | { state: 'none' | 'ambiguous' }; async function first(tries: Locator[]): Promise { for (const t of tries) { const l = vis(t); const n = await l.count(); if (n === 1) return { state: 'ok', loc: l }; if (n > 1) return { state: 'ambiguous' }; } return { state: 'none' }; } async function find(root: Locator, role: string, names: Name[], label = false): Promise { for (const n of names) { const tries = [root.getByRole(role, nameOpts(n))]; if (label) tries.push(root.getByLabel(n, typeof n === 'string' ? { exact: true } : {})); const f = await first(tries); if (f.state !== 'none') return f; } return role === 'checkbox' || role === 'radio' ? byParentText(root, role, names) : { state: 'none' }; } /** * The nearest non-empty texts around controls that has no accessible name of its own (no aria-label, aria-labelledby * or associated