desktop: a Fix button on scan and validate findings storebound can fix itself. Findings carry action { method, args } (cli/lib/fix-actions.ts); privacy-labels runs scan.write (scan --write-only, the dialog shows the config diff), Play listing gaps play.listing, a missing App Store localization or copyright apple.listing (editable version only), the App Review contact apple.review_details; each a dry run, the native dialog, then the confirmed run. --write-only is forbidden for MCP tools; tests pin the mapping, the round trip on a fixture and that no fix reaches a submit, release or publish
This commit is contained in:
parent
bd83d036ba
commit
992f5bcc0e
18 changed files with 257 additions and 37 deletions
54
cli/scan.ts
54
cli/scan.ts
|
|
@ -1,6 +1,7 @@
|
|||
// Offline, read-only scan of the project itself for App Store and Google Play rejection risks, before you submit.
|
||||
// storebound scan [--dir <project>] [--config <file>] [--store apple|play] [--json] [--no-declarations]
|
||||
// storebound scan --write also write the detected dataSafety / appPrivacy answers into the config
|
||||
// storebound scan --write-only [--dry] only that write (with --dry: the config diff), no report (desktop Fix button)
|
||||
// storebound scan --originality [--against <folders>] [--threshold 0.7] also the 4.3 / Play repetitive content
|
||||
// check against your other apps (storebound originality; folders default to originality.against in the config)
|
||||
// Reads the project files (Xcode project, Info.plist, entitlements, privacy manifests, Podfile.lock, Package.resolved,
|
||||
|
|
@ -16,7 +17,8 @@ import type { Args } from './lib/args';
|
|||
import { CONFIG_NAMES } from './lib/config';
|
||||
import { log, printJson, warn } from './lib/log';
|
||||
import { upsertBlock } from './lib/scan/config-edit';
|
||||
import { addOriginality, formatScan, runScan } from './lib/scan';
|
||||
import { addOriginality, formatScan, runScan, type ScanReport } from './lib/scan';
|
||||
import { compactDiff } from './lib/diff';
|
||||
import { compareAgainst } from './originality';
|
||||
|
||||
/** Project folder: --dir, else the folder of the nearest storebound.config.* above the working directory, else it. */
|
||||
|
|
@ -48,25 +50,43 @@ export async function run(args: Args): Promise<void> {
|
|||
report.counts = { blocker: 0, high: 0, medium: 0, low: 0, accepted: 0 };
|
||||
for (const f of report.findings) { if (f.accepted) report.counts.accepted++; else report.counts[f.severity]++; }
|
||||
} else if (store) throw new Error('--store apple | play');
|
||||
if (args.has('json')) printJson(report);
|
||||
else console.log(formatScan(report, { declarations: !args.has('no-declarations') }));
|
||||
// --write-only (the desktop's Fix button, scan.write): only the write and its diff, no report, no blocker exit
|
||||
const writeOnly = args.has('write-only');
|
||||
if (!writeOnly) {
|
||||
if (args.has('json')) printJson(report);
|
||||
else console.log(formatScan(report, { declarations: !args.has('no-declarations') }));
|
||||
}
|
||||
|
||||
if (args.has('write')) {
|
||||
if (args.has('write') || writeOnly) {
|
||||
const d = report.declarations;
|
||||
if (!configPath) warn('scan', '--write: no storebound.config.ts in this project (storebound init creates one)');
|
||||
else if (!d.changed) log('scan', `--write: nothing to change in ${report.config}`);
|
||||
else if (args.has('dry')) log('scan', `--dry: would write ${[(d.diff.some(x => x.change !== 'kept' && x.change !== 'same') || d.accountCreationChange) && 'dataSafety', (d.appPrivacyChange || d.appPrivacyTypes) && 'appPrivacy'].filter(Boolean).join(' + ')} into ${report.config}`);
|
||||
if (!configPath) {
|
||||
if (writeOnly) throw new Error('no storebound.config.ts in this project (storebound init creates one)');
|
||||
warn('scan', '--write: no storebound.config.ts in this project (storebound init creates one)');
|
||||
} else if (!d.changed) log('scan', `--write: nothing to change in ${report.config}`);
|
||||
else {
|
||||
let text = readFileSync(configPath, 'utf8');
|
||||
const ds = d.dataSafety!;
|
||||
const missing = (['encryptedInTransit'] as const).filter(k => typeof ds[k] !== 'boolean');
|
||||
if (d.diff.some(x => x.change === 'add' || x.change === 'merge') || d.accountCreationChange)
|
||||
text = upsertBlock(text, configPath, 'dataSafety', ds, missing.length ? `storebound scan: answer ${missing.join(' and ')} (ask the owner; store-declarations skill)` : undefined);
|
||||
if (d.appPrivacyChange || d.appPrivacyTypes) text = upsertBlock(text, configPath, 'appPrivacy', d.appPrivacy,
|
||||
d.appPrivacyTypes ? 'storebound scan: types proposed from the detected SDKs; answer linked / tracking per type (ask the owner; store-declarations skill)' : undefined);
|
||||
writeFileSync(configPath, text);
|
||||
log('scan', `wrote the detected answers into ${report.config}${missing.length ? `; still to answer: dataSafety.${missing.join(', dataSafety.')}` : ''}. Review the diff (git diff) before pushing it to a store.`);
|
||||
const before = readFileSync(configPath, 'utf8');
|
||||
const { text, missing } = declarationsWrite(before, configPath, d);
|
||||
const what = [(d.diff.some(x => x.change !== 'kept' && x.change !== 'same') || d.accountCreationChange) && 'dataSafety', (d.appPrivacyChange || d.appPrivacyTypes) && 'appPrivacy'].filter(Boolean).join(' + ');
|
||||
if (args.has('dry')) {
|
||||
log('scan', `--dry: would write ${what} into ${report.config}`);
|
||||
console.log([`--- ${report.config}`, ...compactDiff(before, text)].join('\n'));
|
||||
} else {
|
||||
writeFileSync(configPath, text);
|
||||
log('scan', `wrote the detected answers into ${report.config}${missing.length ? `; still to answer: dataSafety.${missing.join(', dataSafety.')}` : ''}. Review the diff (git diff) before pushing it to a store.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (report.counts.blocker) process.exitCode = 1;
|
||||
if (report.counts.blocker && !writeOnly) process.exitCode = 1;
|
||||
}
|
||||
|
||||
/** The config text with the detected answers written in (dataSafety / appPrivacy blocks; comments and order kept). */
|
||||
export function declarationsWrite(before: string, configPath: string, d: ScanReport['declarations']): { text: string; missing: string[] } {
|
||||
let text = before;
|
||||
const ds = d.dataSafety!;
|
||||
const missing = (['encryptedInTransit'] as const).filter(k => typeof ds[k] !== 'boolean');
|
||||
if (d.diff.some(x => x.change === 'add' || x.change === 'merge') || d.accountCreationChange)
|
||||
text = upsertBlock(text, configPath, 'dataSafety', ds, missing.length ? `storebound scan: answer ${missing.join(' and ')} (ask the owner; store-declarations skill)` : undefined);
|
||||
if (d.appPrivacyChange || d.appPrivacyTypes) text = upsertBlock(text, configPath, 'appPrivacy', d.appPrivacy,
|
||||
d.appPrivacyTypes ? 'storebound scan: types proposed from the detected SDKs; answer linked / tracking per type (ask the owner; store-declarations skill)' : undefined);
|
||||
return { text, missing };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue