82 lines
4.9 KiB
TypeScript
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}`);
|
|
}
|