--json prints only JSON on stdout (withJsonOutput around every command: logs, tables and prompts to stderr; printJson everywhere; test runs every --json command against the fake stores and parses stdout); prices ppp compares like with like: par = the store's own US-equivalent price (Play convertRegionPrices, Apple equalizations; offline base x fx x (1 + VAT) rounded up) scaled by the PPP price level; defaults only lower, only price levels <= 0.5, World Bank high-income at par, CN at par on Apple and skipped on Play, never above par (--only-decreases, --max-ratio, --min-ratio, --max-level, --include-high-income; --floor / --ceiling kept), top-10 revenue market drops > 20 % warned (sales reports, else a static list), unchanged regions out of the sheet; price changes of existing products only with --allow-price-changes, TTY or not (a terminal yes covered them), then their own question listing each old -> new, held section in dry runs (play products, prices import, play / apple subscriptions); pitfalls 61-63; tests

This commit is contained in:
CRTkafa 2026-10-14 17:36:00 +08:00
parent bab3f63227
commit 357319a775
30 changed files with 865 additions and 209 deletions

View file

@ -19,6 +19,7 @@ import { PLAY_APIS } from './api/play.gen';
import { WRAPPED } from './api/wrapped.gen';
import type { ParamValue } from './api/types';
import * as rt from './lib/rt';
import { printJson, promptOut } from './lib/log';
import { stdinIsTTY } from './lib/tty';
const STORES: Store[] = ['apple', 'play'];
@ -109,14 +110,14 @@ export async function run(args: Args): Promise<void> {
if (sub === 'list') {
const list = args.get('search') ? search(rows(store), args.get('search')!) : rows(store);
if (json) { console.log(JSON.stringify(list)); return; }
if (json) { printJson(list); return; }
for (const r of list) console.log(`${r.store.padEnd(5)} ${r.method.padEnd(6)} ${r.id.padEnd(60)} ${r.path}${r.gate === 'owner' ? ' [owner only]' : ''}${r.wrapped ? ` (wrapped: ${r.wrapped})` : ''}`);
console.error(`${list.length} operation(s)`);
return;
}
if (sub === 'coverage') {
const c = coverage(store);
if (json) { console.log(JSON.stringify(c)); return; }
if (json) { printJson(c); return; }
for (const a of c.apis) console.log(`${a.title}: ${a.operations} operations (${a.get} GET, ${a.write} write, ${a.owner} owner only), ${a.wrapped} wrapped by storebound commands`);
console.log(`\n${'resource'.padEnd(62)} ops wrapped`);
for (const r of c.resources) console.log(`${(r.store + ' ' + r.resource).padEnd(62)} ${String(r.operations).padStart(3)} ${String(r.wrapped).padStart(7)}`);
@ -146,13 +147,13 @@ export async function run(args: Args): Promise<void> {
});
if (decision.action === 'refuse') throw new Error(`${op.id}: ${decision.why}`);
if (decision.action === 'dry') {
if (json) console.log(JSON.stringify({ dryRun: true, operation: op.id, gate: kind, why: decision.why, request: { method: req.method, url: req.url, body: typeof req.body === 'string' ? JSON.parse(req.body) : req.body ? `<${req.body.length} bytes>` : undefined } }));
if (json) printJson({ dryRun: true, operation: op.id, gate: kind, why: decision.why, request: { method: req.method, url: req.url, body: typeof req.body === 'string' ? JSON.parse(req.body) : req.body ? `<${req.body.length} bytes>` : undefined } });
else console.log(`dry run (${decision.why}): nothing sent\n${kind === 'owner' ? `owner only: this ${ownerReason(st, op.id, op.method)}; sending also asks you to type the operation id\n` : ''}\n${describeRequest(req)}`);
return;
}
if (decision.action === 'confirm') {
console.log(describeRequest(req));
process.stdout.write(`\nThis ${ownerReason(st, op.id, op.method)}. Type the operation id (${op.id}) to send it: `);
promptOut().write(`\nThis ${ownerReason(st, op.id, op.method)}. Type the operation id (${op.id}) to send it: `);
if ((await readLine()) !== op.id) { console.log('not sent'); process.exitCode = 1; return; }
}
@ -166,10 +167,10 @@ export async function run(args: Args): Promise<void> {
if (r.bytes) {
if (!out) throw new Error(`${op.id}: binary response (${r.bytes.length} bytes): pass --out <file>`);
writeFileSync(resolve(out), r.bytes);
console.log(json ? JSON.stringify({ status: r.status, out, bytes: r.bytes.length }) : `wrote ${out} (${r.bytes.length} bytes)`);
if (json) printJson({ status: r.status, out, bytes: r.bytes.length }); else console.log(`wrote ${out} (${r.bytes.length} bytes)`);
return;
}
if (out) { writeFileSync(resolve(out), JSON.stringify(r.json, null, 2) + '\n'); if (!json) console.log(`wrote ${out}`); return; }
console.log(json ? JSON.stringify(r.json) : JSON.stringify(r.json, null, 2));
if (json) printJson(r.json); else console.log(JSON.stringify(r.json, null, 2));
}