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:
parent
a9261a8052
commit
c9a7f095c4
34 changed files with 1430 additions and 113 deletions
|
|
@ -24,12 +24,12 @@ import { homedir } from 'node:os';
|
|||
import { dirname, join } from 'node:path';
|
||||
import type { ShipConfig } from '../lib/config';
|
||||
import type { FormId } from '../lib/console-answers';
|
||||
import { hasToken, type Name, NAV_BASES, type NavBases, type NavStep, navPlan, playDeveloperId, switchAllowed } from '../lib/console-forms';
|
||||
import { hasToken, type Name, NAV_BASES, type NavBases, type NavStep, navPlan, playAppIdOf, playDeveloperId, switchAllowed } from '../lib/console-forms';
|
||||
import { ACTION_MS, accName, findAny, type Locator, navClick, navigate, type NavResult, type Page, poll, realRole, safeClick, signInPage, SignInSeen, type Stop, vis } from './fill';
|
||||
|
||||
// ---- the cache ----
|
||||
|
||||
export interface PlayEntry { developerId: string; account?: string; at: string }
|
||||
export interface PlayEntry { developerId: string; account?: string; /** The Play Console app id (/app/<appId>/), read from the URL once the app is open. */ appId?: string; at: string }
|
||||
export interface AppleEntry { team: string; teamId?: string; at: string }
|
||||
export interface AccountsCache { version: 1; play: Record<string, PlayEntry>; apple: Record<string, AppleEntry> }
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ export function readAccounts(file = accountsFile()): AccountsCache {
|
|||
let j: any;
|
||||
try { j = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null; } catch { return out; }
|
||||
for (const [k, v] of Object.entries((j?.play ?? {}) as Record<string, any>)) {
|
||||
if (KEY.test(k) && typeof v?.developerId === 'string' && /^\d{1,25}$/.test(v.developerId)) out.play[k] = { developerId: v.developerId, ...(str(v.account) ? { account: v.account } : {}), at: str(v.at, 40) ? v.at : '' };
|
||||
if (KEY.test(k) && typeof v?.developerId === 'string' && /^\d{1,25}$/.test(v.developerId)) out.play[k] = { developerId: v.developerId, ...(str(v.account) ? { account: v.account } : {}), ...(typeof v.appId === 'string' && /^\d{1,25}$/.test(v.appId) ? { appId: v.appId } : {}), at: str(v.at, 40) ? v.at : '' };
|
||||
}
|
||||
for (const [k, v] of Object.entries((j?.apple ?? {}) as Record<string, any>)) {
|
||||
if (KEY.test(k) && str(v?.team)) out.apple[k] = { team: v.team, ...(typeof v.teamId === 'string' && /^[A-Z0-9]{10}$/.test(v.teamId) ? { teamId: v.teamId } : {}), at: str(v.at, 40) ? v.at : '' };
|
||||
|
|
@ -55,7 +55,9 @@ export function readAccounts(file = accountsFile()): AccountsCache {
|
|||
/** Remembers which account has the app (written atomically: the desktop app may read it at the same time). */
|
||||
export function rememberPlay(pkg: string, e: Omit<PlayEntry, 'at'>, file = accountsFile()): void {
|
||||
const c = readAccounts(file);
|
||||
c.play[pkg] = { ...e, at: new Date().toISOString() };
|
||||
// the app id stays when the same account is remembered again without one
|
||||
const keep = c.play[pkg]?.developerId === e.developerId && c.play[pkg]?.appId && !e.appId ? { appId: c.play[pkg]!.appId! } : {};
|
||||
c.play[pkg] = { ...e, ...keep, at: new Date().toISOString() };
|
||||
write(c, file);
|
||||
}
|
||||
export function rememberApple(bundleId: string, e: Omit<AppleEntry, 'at'>, file = accountsFile()): void {
|
||||
|
|
@ -281,7 +283,18 @@ function notHere(r: NavResult, plan: NavStep[]): boolean {
|
|||
* `signIn` (the caller waits and calls again).
|
||||
*/
|
||||
export async function openAcross(page: Page, o: AcrossOpts): Promise<{ nav: NavResult; plan: NavStep[] }> {
|
||||
try { return await across(page, o); } catch (e) {
|
||||
try {
|
||||
const r = await across(page, o);
|
||||
// a Play form of an existing app is open: its URL carries the Play Console app id (/app/<appId>/), remembered
|
||||
// next to the developer id so `console plan` can deep-link the App content forms
|
||||
const pkg = o.config.play?.packageName;
|
||||
if (r.nav.ok && pkg && o.form.startsWith('play-') && o.form !== 'play-app-record') {
|
||||
const dev = playDeveloperId(page.url()), app = playAppIdOf(page.url());
|
||||
const f = o.file ?? accountsFile(), account = readAccounts(f).play[pkg]?.account;
|
||||
if (dev && app) rememberPlay(pkg, { developerId: dev, ...(account ? { account } : {}), appId: app }, f);
|
||||
}
|
||||
return r;
|
||||
} catch (e) {
|
||||
if (e instanceof SignInSeen) return { nav: { ok: false, steps: [], failed: 'the account list', why: 'a sign-in page appeared', signIn: true }, plan: [] };
|
||||
throw e;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue