storebound/cli/console/plan.ts

60 lines
4.1 KiB
TypeScript

// 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 <form> [--json] [--lookup]
// storebound console plan play-app-content --all [--json] every App content card's steps inline, one owner list
// 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 { printJson } from '../lib/log';
export async function run(ctx: Ctx): Promise<void> {
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: 'play' as const, title: consolePlan(id, config).title, fillable: true, scripted: false };
});
if (json) return printJson({ forms });
console.log('storebound console plan <form> [--json] [--lookup] (then drive the page yourself; `storebound console verify <form>` 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 (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));
}