83 lines
5.5 KiB
TypeScript
83 lines
5.5 KiB
TypeScript
// What the desktop's editors show (`storebound edit --show --json`, serve method project.editable): the listing
|
|
// fields per store and language as the files hold them, the config sections the editors change, the App Review
|
|
// contact (masked: personal data), the store limits, and whether the files are uncommitted in git. Offline.
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { join, relative } from 'node:path';
|
|
import { APPLE_IAP_LIMITS, APPLE_LIMITS, PLAY_LIMITS } from './copy';
|
|
import { appleName, type Loaded, playCodes } from './config';
|
|
import { maskUser, reviewDemoAccount } from './credentials';
|
|
import type { EditableResult } from './desktop-protocol';
|
|
import { appleSection, playSection } from './listing-edit';
|
|
import { NOTES_MAX_BYTES } from './review-details';
|
|
import * as rt from './rt';
|
|
import { APPLE_SUB_LIMITS, PLAY_SUB_LIMITS } from './subscriptions';
|
|
|
|
/** Is `root` in a git work tree, and which of `files` have uncommitted changes (or are untracked)? */
|
|
export function gitState(root: string, files: string[]): { repo: boolean; uncommitted: string[] } {
|
|
try {
|
|
const inside = rt.spawnSync(['git', '-C', root, 'rev-parse', '--is-inside-work-tree'], { timeout: 5000 });
|
|
if (!inside.success || inside.stdout.toString().trim() !== 'true') return { repo: false, uncommitted: [] };
|
|
if (!files.length) return { repo: true, uncommitted: [] };
|
|
const st = rt.spawnSync(['git', '-C', root, 'status', '--porcelain', '--untracked-files=all', '--', ...files.map(f => relative(root, f) || f)], { timeout: 5000 });
|
|
if (!st.success) return { repo: true, uncommitted: [] };
|
|
const out = st.stdout.toString().split('\n').filter(Boolean).map(l => l.slice(3).replace(/^"|"$/g, ''));
|
|
// porcelain paths are relative to the repository top: keep the file names the window knows
|
|
const rels = files.map(f => relative(root, f) || f);
|
|
return { repo: true, uncommitted: rels.filter(r => out.some(o => o === r || o.endsWith(`/${r}`))) };
|
|
} catch {
|
|
return { repo: false, uncommitted: [] };
|
|
}
|
|
}
|
|
|
|
const mask = (field: string, v: string): string => (!v ? '' : field === 'email' ? maskUser(v) : field === 'phone' ? `${v.slice(0, 3)}***` : `${v[0]}***`);
|
|
|
|
export function editableValues(l: Loaded): EditableResult {
|
|
const { config, root, path, resolve } = l;
|
|
const rel = (f: string): string => relative(root, f) || f;
|
|
const playFile = config.play ? resolve(config.play.listing ?? 'store/listing.md') : null;
|
|
const appleFile = config.apple ? resolve(config.apple.listing ?? 'store/appstore.md') : null;
|
|
const playMd = playFile && existsSync(playFile) ? readFileSync(playFile, 'utf8') : null;
|
|
const appleMd = appleFile && existsSync(appleFile) ? readFileSync(appleFile, 'utf8') : null;
|
|
const contactFile = join(root, '.storebound', 'credentials.json');
|
|
let contact: EditableResult['contact'] = null;
|
|
try {
|
|
const c = existsSync(contactFile) ? (JSON.parse(readFileSync(contactFile, 'utf8')) as { reviewContact?: Record<string, string> }).reviewContact : undefined;
|
|
if (c) contact = { firstName: mask('firstName', c.firstName ?? ''), lastName: mask('lastName', c.lastName ?? ''), phone: mask('phone', c.phone ?? ''), email: mask('email', c.email ?? '') };
|
|
} catch { /* unreadable: the declarations card names it */ }
|
|
let demo: string | null = null;
|
|
try { const d = reviewDemoAccount(root); demo = d ? maskUser(d.username) : null; } catch { /* unreadable */ }
|
|
const play: EditableResult['play'] = [];
|
|
const apple: EditableResult['apple'] = [];
|
|
for (const lang of config.languages) {
|
|
if (config.play) for (const code of playCodes(lang)) {
|
|
const s = playMd ? playSection(playMd, code) : null;
|
|
play.push({ lang: code, language: lang.id, codes: s?.codes ?? [code], status: s?.status ?? null, exists: !!s, values: s?.values ?? {} });
|
|
}
|
|
if (config.apple && lang.apple) {
|
|
const s = appleMd ? appleSection(appleMd, lang.apple) : null;
|
|
apple.push({ lang: lang.apple, language: lang.id, status: s?.status ?? null, exists: !!s, values: s?.values ?? {} });
|
|
}
|
|
}
|
|
const files = [path, ...(playFile ? [playFile] : []), ...(appleFile ? [appleFile] : [])];
|
|
const { signing: _s, build: _b, adhoc: _a, ...appleRest } = config.apple ?? ({} as NonNullable<typeof config.apple>);
|
|
const { build: _pb, ...playRest } = config.play ?? ({} as NonNullable<typeof config.play>);
|
|
return {
|
|
root,
|
|
files: { config: rel(path), play: playFile ? rel(playFile) : null, apple: appleFile ? rel(appleFile) : null, contact: rel(contactFile) },
|
|
format: path.endsWith('.json') ? 'json' : path.endsWith('.js') ? 'js' : 'ts',
|
|
git: gitState(root, files),
|
|
appName: config.app.name,
|
|
appleName: appleName(config),
|
|
languages: config.languages.map(x => ({ id: x.id, play: playCodes(x), apple: x.apple ?? null, rtl: !!x.rtl })),
|
|
play: config.play ? play : null,
|
|
apple: config.apple ? apple : null,
|
|
config: JSON.parse(JSON.stringify({
|
|
app: config.app, apple: config.apple ? appleRest : null, play: config.play ? playRest : null, products: config.products ?? [], subscriptions: config.subscriptions ?? [],
|
|
dataSafety: config.dataSafety ?? null, appPrivacy: config.appPrivacy ?? null, reviewAccess: config.reviewAccess ?? null, accounts: config.accounts ?? null, social: config.social ?? null,
|
|
shots: config.shots ? { scenes: config.shots.scenes, feature: config.shots.feature ?? null } : null,
|
|
})) as EditableResult['config'],
|
|
contact,
|
|
demoAccount: demo,
|
|
limits: { play: PLAY_LIMITS, apple: APPLE_LIMITS, iap: APPLE_IAP_LIMITS, appleSub: APPLE_SUB_LIMITS, playSub: PLAY_SUB_LIMITS, reviewNotesBytes: NOTES_MAX_BYTES },
|
|
};
|
|
}
|