// Offline summary of the project config: app name, store ids, languages, products, shots. No network, no credentials. // storebound inspect [--json] // storebound inspect --icon --json the app icon as a data: URL (for the desktop app) // storebound inspect --listings [--json] listing completeness per language on both stores (+ rendered screenshots) // storebound inspect --listing-text --json the listing files as they are (desktop export) // storebound inspect --declarations [--json] Data safety, App Privacy, app info, export compliance and App Review // details from the config, with what only the owner can answer // storebound inspect --render --json one render (relative to the renders folder) as a data: URL import { existsSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { platformsOf, playCategoryOf, profileOf, sellsOn } from './lib/config'; import type { Ctx } from './lib/ctx'; import { credentialValue, maskUser, reviewContact, reviewDemoAccount } from './lib/credentials'; import { DATA_SAFETY_UPLOADED, declarations } from './lib/declarations'; import type { ProjectSummary } from './lib/desktop-protocol'; import { log, printJson } from './lib/log'; import { playIdsOf } from './lib/play-ids'; import { findIcon, iconDataUrl, listingCompleteness, listingText, renderImage } from './lib/project-files'; export function summarize(ctx: Ctx): ProjectSummary { const { config, root, path } = ctx.loaded; const subs = (config.subscriptions ?? []).flatMap(g => g.subscriptions); return { dir: root, config: path, name: config.app.name, languages: config.languages.map(l => l.id), play: config.play ? { packageName: config.play.packageName, ...playIdsOf(config, root) } : null, apple: config.apple ? { bundleId: config.apple.bundleId, appId: config.apple.appId ?? null } : null, platforms: platformsOf(config), products: config.products?.length ?? 0, productIds: (config.products ?? []).map(p => p.id), subscriptions: subs.length, subscriptionIds: subs.map(s => s.id), // the catalog each store should sell (items can be limited to one platform) catalog: { ios: config.apple ? [...(config.products ?? []), ...subs].filter(x => sellsOn(config, x, 'apple')).map(x => x.id) : [], android: config.play ? [...(config.products ?? []), ...subs].filter(x => sellsOn(config, x, 'play')).map(x => x.id) : [], }, shots: !!config.shots, testflightGroups: config.apple?.testflight?.groups?.length ?? 0, profile: profileOf(config), icon: findIcon(config, root), variants: Object.keys(config.shots?.variants ?? {}), playReports: !!config.play?.reportsBucket || !!credentialValue(root, 'PLAY_REPORTS_BUCKET'), playRelease: config.play ? { managedPublishing: config.play.managedPublishing ?? null, countries: config.play.countries?.length ?? null, category: playCategoryOf(config)?.category ?? null } : null, games: config.games ? { gameCenter: config.games.gameCenter === true, playGames: !!config.games.playGames, leaderboards: config.games.leaderboards?.length ?? 0, achievements: config.games.achievements?.length ?? 0 } : null, }; } export async function run(ctx: Ctx): Promise { const { config, root, resolve } = ctx.loaded; const a = ctx.args; if (a.has('icon')) { printJson(iconDataUrl(findIcon(config, root))); return; } if (a.has('listing-text')) { printJson(listingText(config, root)); return; } if (a.get('render') !== undefined) { printJson(renderImage(resolve(config.renders ?? config.shots?.out ?? 'store/renders'), a.get('render') ?? '')); return; } if (a.has('declarations')) { const up = join(root, DATA_SAFETY_UPLOADED); let uploaded: { csv: string; at: string } | null = null; try { if (existsSync(up)) uploaded = { csv: readFileSync(up, 'utf8'), at: statSync(up).mtime.toISOString() }; } catch { /* unreadable: never uploaded */ } const demo = reviewDemoAccount(root), contact = reviewContact(root); const d = declarations(config, { demoUser: demo ? maskUser(demo.username) : null, contactEmail: contact ? maskUser(contact.email) : null, uploaded }); if (a.has('json')) { printJson(d); return; } for (const x of d.ask) log('inspect', `ask the owner (${x.area}): ${x.text} -> ${x.fix}`); if (!d.ask.length) log('inspect', 'declarations: every answer comes from the config'); return; } if (a.has('listings')) { const l = listingCompleteness(config, root); if (a.has('json')) { printJson(l); return; } for (const p of l.problems) log('inspect', p); for (const x of l.locales) log('inspect', `${x.lang.padEnd(6)} ${Math.round(x.complete * 100)} % complete${x.screenshots === null ? '' : ` · ${x.screenshots} screenshots`}`); return; } const s = summarize(ctx); if (a.has('json')) { printJson(s); return; } log('inspect', `${s.name} · ${s.platforms.join(' + ')} · ${s.languages.length} languages · ${s.products} products · ${s.subscriptions} subscriptions`); log('inspect', `play ${s.play?.packageName ?? '-'} · apple ${s.apple?.bundleId ?? '-'}${s.apple?.appId ? ` (${s.apple.appId})` : ''}`); log('inspect', `config ${s.config}`); }