// The agent-first console mode: storebound supplies the knowledge, the calling AI agent drives the browser. // storebound console plan lists the forms // storebound console plan
[--json] [--lookup] // storebound console plan play-app-content --all [--json] every App content card's steps inline, one owner list // storebound console plan apple-app-group [--bundle-id ] [--groups group.a,group.b] [--lookup] [--json] the developer portal App Group assignment the API cannot do // Read-only. Offline by default: the start URL, the ordered steps, the answer sheet (each answer certain or "ask // owner", with the console's own labels), the never-press list, the known UI facts and how to verify. Deep links use // apple.appId / play.developerId / play.appId, the ids cached by earlier runs (.storebound/console-app-ids.json, // ~/.storebound/console-accounts.json) and the workspace's account ids; --lookup also asks the App Store Connect API // for the app id by bundle id (a GET; needs the API key). Pure logic in cli/lib/console-plan.ts. MCP: console_plan. import { FORM_AREAS, refuseTemplate } from '../lib/template-answers'; import type { Ctx } from '../lib/ctx'; import { formMeta, isForm, isPlanForm, PLAN_FORM_IDS } from '../lib/console-answers'; import { reviewDemoAccount } from '../lib/credentials'; import { consolePlan, type PlanIds, planText } from '../lib/console-plan'; import type { ShipConfig } from '../lib/config'; import { printJson } from '../lib/log'; /** apple-app-group: the targets whose entitlements (or apple.signing.extensions) list App Groups, narrowed by --bundle-id; * --groups g1,g2 names the groups of a bundle id the project does not show; --lookup reads the bundle id's resource id (a GET). */ async function appGroupTargets(ctx: Ctx, cfg: ShipConfig, bundleId: string | undefined, groups: string | undefined, lookup: boolean): Promise> { const { discoverTargets } = await import('../apple/signing'); const scan = discoverTargets(ctx.loaded.root, cfg); const all = [...(scan.main ? [scan.main] : []), ...scan.extensions]; const listed = groups ? groups.split(',').map(g => g.trim()).filter(Boolean) : []; for (const g of listed) if (!/^group.[A-Za-z0-9.-]+$/.test(g)) throw new Error(`console plan apple-app-group: --groups "${g}" is not an App Group id (group.)`); let found = all.filter(t => t.appGroups.length && (!bundleId || t.bundleId === bundleId)).map(t => ({ bundleId: t.bundleId, name: t.name, groups: listed.length ? listed : t.appGroups, source: t.entitlements ? `entitlements ${t.entitlements}` : 'apple.signing.extensions' })); if (bundleId && !found.length) { if (!listed.length) throw new Error(`console plan apple-app-group: no target "${bundleId}" with App Groups in the project (targets with groups: ${all.filter(t => t.appGroups.length).map(t => t.bundleId).join(', ') || 'none'}); pass --groups group.a,group.b`); found = [{ bundleId, name: bundleId, groups: listed, source: '--groups' }]; } if (!lookup || !found.length) return found; const bundle = cfg.apple?.bundleId; if (!bundle) throw new Error('console plan: apple.bundleId is not set (needed for --lookup)'); const { ascClient } = await import('../apple/asc'); const { findBundleId } = await import('../apple/signing'); const c = await ascClient(ctx.loaded.root, bundle, cfg.apple?.appId); return Promise.all(found.map(async t => ({ ...t, resourceId: (await findBundleId(c, t.bundleId))?.id ?? null }))); } export async function run(ctx: Ctx): Promise { const { config, root } = ctx.loaded; const args = ctx.args, json = args.has('json'); const form = args._[0]; if (!form) { const forms = PLAN_FORM_IDS.map(id => { if (isForm(id)) { const m = formMeta(id, config); return { id, store: m.store, title: m.title, fillable: m.fillable, scripted: m.fillable }; } return { id, store: id.startsWith('apple-') ? 'apple' as const : 'play' as const, title: consolePlan(id, config).title, fillable: true, scripted: false }; }); if (json) return printJson({ forms }); console.log('storebound console plan [--json] [--lookup] (then drive the page yourself; `storebound console verify ` checks it)\n'); for (const f of forms) console.log(` ${f.id.padEnd(26)} ${f.store.padEnd(6)} ${f.title}${f.fillable ? (f.scripted ? '' : ' (agent only, no console fill)') : ' (guide only)'}`); return; } if (!isPlanForm(form)) throw new Error(`console plan: unknown form "${form}" (one of ${PLAN_FORM_IDS.join(', ')})`); refuseTemplate(config, FORM_AREAS[form] ?? [], `console plan ${form}`); const { withAccountIds, readAccounts } = await import('./accounts'); const cfg = withAccountIds(config); const pkg = cfg.play?.packageName; const cached = pkg ? readAccounts().play[pkg] : undefined; const ids: PlanIds = { playDeveloperId: cfg.play?.developerId ?? cached?.developerId ?? null, playAppId: cfg.play?.appId ?? cached?.appId ?? null, demoAccount: !!reviewDemoAccount(root) }; if (form.startsWith('apple-') && !cfg.apple?.appId && cfg.apple?.bundleId) { const { appleAppId } = await import('./app-id'); // without --lookup only the cache is read: the lookup refuses, so no request is made ids.appleAppId = await appleAppId(root, cfg, { say: () => { /* the plan's start says what it found */ }, ...(args.has('lookup') ? {} : { lookup: async () => { throw new Error('offline (add --lookup to ask the API)'); } }) }); } // project signals (ad / attribution SDKs, the AD_ID permission, support and AR code) for the App Privacy and // Advertising ID answers; a scan that fails leaves them unknown (asked) try { const { detectProject } = await import('../lib/scan/project'); const { projectSignals } = await import('../lib/project-signals'); ids.signals = projectSignals(detectProject(root)); } catch { /* not scanned */ } if (form === 'play-publish-changes' && args.has('lookup') && cfg.play?.packageName) { try { const { readyFromApi } = await import('../play/publish-ready'); ids.readyChanges = await readyFromApi(root, cfg); } catch { /* not read: the plan says so */ } } if (form === 'apple-app-group') ids.appGroupTargets = await appGroupTargets(ctx, cfg, args.get('bundle-id'), args.get('groups'), args.has('lookup')); if (args.has('all') && form !== 'play-app-content') throw new Error('console plan: --all is for play-app-content (every App content card inline)'); const plan = consolePlan(form, cfg, ids, { all: args.has('all') }); if (json) return printJson(plan); console.log(planText(plan)); }