play build: find the Android SDK like Android Studio (ANDROID_HOME, ANDROID_SDK_ROOT, sdk.dir, default folder), pass it in the env, stop before Gradle when missing; doctor shows it. apple upload: size, per-part lines, 20 s heartbeat (api, altool, processing wait), per-part and per-call timeouts, --timeout exits 7 with the resume hint. Pitfall 25

This commit is contained in:
CRTkafa 2026-10-12 08:48:02 +08:00
parent 366362d62b
commit fc524cd2d4
15 changed files with 460 additions and 34 deletions

47
cli/lib/progress.ts Normal file
View file

@ -0,0 +1,47 @@
// Progress for long blocking steps (uploads, store processing waits, external tools): a heartbeat line every few
// seconds so a run is never silent for minutes, and the size / elapsed formats those lines use. The timer is
// injectable (Ticker), so tests drive beats by hand instead of sleeping.
/** Calls fn every ms until the returned stop function is called. */
export type Ticker = (fn: () => void, ms: number) => () => void;
export const realTicker: Ticker = (fn, ms) => {
const t = setInterval(fn, ms);
(t as { unref?: () => void }).unref?.();
return () => clearInterval(t);
};
/** Default heartbeat interval of uploads and waits. */
export const HEARTBEAT_MS = 20_000;
/** 4.2 MB, 312 MB (whole MB from 10 MB up). */
export const mb = (bytes: number): string => `${(bytes / 1048576).toFixed(bytes < 10 * 1048576 ? 1 : 0)} MB`;
/** 45s, 4m30s, 1h02m. */
export function elapsed(ms: number): string {
const s = Math.max(0, Math.round(ms / 1000));
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m${String(s % 60).padStart(2, '0')}s`;
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m`;
}
/** "312/890 MB (35%)". */
export const share = (done: number, total: number): string =>
`${(done / 1048576).toFixed(total < 10 * 1048576 ? 1 : 0)}/${mb(total)} (${total ? Math.floor((done / total) * 100) : 100}%)`;
export interface HeartbeatOptions { everyMs?: number; ticker?: Ticker; now?: () => number }
/** Runs `say(elapsedMs)` every everyMs while a step runs; stop() ends it. */
export function heartbeat(say: (elapsedMs: number) => void, o: HeartbeatOptions = {}): { stop: () => void } {
const now = o.now ?? Date.now;
const started = now();
const stop = (o.ticker ?? realTicker)(() => say(now() - started), o.everyMs ?? HEARTBEAT_MS);
return { stop };
}
/** Runs fn with a heartbeat; the heartbeat stops however fn ends. */
export async function withHeartbeat<T>(say: (elapsedMs: number) => void, fn: () => Promise<T>, o: HeartbeatOptions = {}): Promise<T> {
const h = heartbeat(say, o);
try { return await fn(); } finally { h.stop(); }
}