storebound/cli/inspect.ts

82 lines
4.9 KiB
TypeScript

// 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 <file> --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 { 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,
playRelease: config.play ? { managedPublishing: config.play.managedPublishing ?? null, countries: config.play.countries?.length ?? null, category: playCategoryOf(config)?.category ?? null } : null,
};
}
export async function run(ctx: Ctx): Promise<void> {
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}`);
}