scan, privacy answers and credentials from a second game's first release (lessons 35-37): review-prompt needs rating context, not a bare "rate"; account signals skip import lines and module paths, config-only accounts say "from config"; accounts.inApp without deletion code stays a medium 5.1.1(v) finding; privacy-manifest --propose builds NSPrivacyCollectedDataTypes from appPrivacy.types and check warns on an empty list; appPrivacy.types is authoritative in the App Privacy sheet (unlisted = not collected), ad / support / AR types default to not collected without tracking or ad SDKs; Play advertising ID from SDKs, the AD_ID permission, app-ads.txt or the flag, never from a Device ID alone; scan.ignore[].evidence and warnings for entries that match nothing; empty credentials template, placeholder values count as missing (check, doctor)

This commit is contained in:
CRTkafa 2026-10-13 05:39:09 +08:00
parent 86cd79dd45
commit 1366e7e231
34 changed files with 881 additions and 71 deletions

View file

@ -7,6 +7,7 @@ import { join, resolve } from 'node:path';
import pkg from '../package.json' with { type: 'json' };
import type { Args } from './lib/args';
import { CONFIG_NAMES } from './lib/config';
import { credentialStatus } from './lib/credentials';
import { log } from './lib/log';
import { textTable } from './lib/table';
import { androidSdk, type AndroidSdkLookup, detectTools, findPlaywright, installCommands, type ToolStatus } from './lib/tools';
@ -28,6 +29,9 @@ export interface DoctorResult {
storebound: string; bun: string | null; runtime: string; platform: string; project: string | null; tools: DoctorRow[]; missing: number;
/** The Android SDK `play build` hands to Gradle, where it came from, and every place looked at. */
androidSdk: AndroidSdkLookup;
/** Store credentials of the project folder (env, .storebound/credentials.json, profile): set, missing or
* placeholder (a template value, treated as missing). Never the values. */
credentials: { key: string; status: 'set' | 'missing' | 'placeholder' }[];
}
export function doctor(cwd: string, env: Record<string, string | undefined> = process.env): DoctorResult {
@ -38,7 +42,7 @@ export function doctor(cwd: string, env: Record<string, string | undefined> = pr
usedFor: 'screenshot renders and web captures from a terminal (the desktop app brings its own)', install: pw ? [] : installCommands('playwright'), url: 'https://playwright.dev/docs/intro' });
const project = CONFIG_NAMES.some(n => existsSync(join(cwd, n))) ? cwd : null;
const sdk = androidSdk([join(cwd, 'android'), cwd], env);
return { storebound: pkg.version, bun: rt.bunVersion, runtime: `${rt.runtime.name} ${rt.runtime.version}`, platform: `${process.platform}-${process.arch}`, project, tools, missing: tools.filter(t => !t.found).length, androidSdk: sdk };
return { storebound: pkg.version, bun: rt.bunVersion, runtime: `${rt.runtime.name} ${rt.runtime.version}`, platform: `${process.platform}-${process.arch}`, project, tools, missing: tools.filter(t => !t.found).length, androidSdk: sdk, credentials: credentialStatus(cwd) };
}
export async function run(args: Args): Promise<void> {
@ -48,6 +52,8 @@ export async function run(args: Args): Promise<void> {
console.log(textTable(['tool', 'status', 'version', 'used for'], r.tools.map(t => [t.name, t.found ? (t.source === 'app' ? 'ok (app copy)' : 'ok') : 'missing', t.version ?? '', t.usedFor])));
log('doctor', r.androidSdk.path ? `Android SDK: ${r.androidSdk.path} (from ${r.androidSdk.source})`
: `Android SDK: not found (play build needs it). Checked: ${r.androidSdk.checked.join('; ')}`);
const set = r.credentials.filter(c => c.status === 'set').map(c => c.key), ph = r.credentials.filter(c => c.status === 'placeholder').map(c => c.key);
log('doctor', `credentials: ${set.length ? set.join(', ') : 'none set'}${ph.length ? ` · placeholders (treated as missing): ${ph.join(', ')}` : ''}`);
const missing = r.tools.filter(t => !t.found);
if (!missing.length) { log('doctor', 'everything storebound can use is here'); return; }
console.log('\nTo install what is missing (pick one per tool; storebound never runs these for you):');