Windows: MCP entries start through cmd /c npx or bun cli/index.ts (npx.cmd and the sh launcher cannot be spawned, pitfall 94); desktop download streams with progress and fails after 60 s without data (pitfall 95); getting-started Windows note

This commit is contained in:
CRTkafa 2026-10-21 22:21:49 +08:00
parent e5dffb5533
commit 0ac3cd02b0
6 changed files with 77 additions and 9 deletions

View file

@ -58,10 +58,10 @@ export function pick(m: Manifest, keys: DownloadKey[], format?: string): [Downlo
}
/** Downloads one installer and keeps it only when its size, SHA-256 and minisign signature are right. */
export async function download(d: Download, dir: string, get: Fetch = fetch, publicKey = PUBLIC_KEY): Promise<string> {
const r = await get(d.url);
export async function download(d: Download, dir: string, get: Fetch = fetch, publicKey = PUBLIC_KEY, stallMs = 60_000, say: (s: string) => void = s => console.log(s)): Promise<string> {
const r = await withStall(get(d.url), stallMs, d.url);
if (!r.ok) throw new Error(`${d.url}: HTTP ${r.status}`);
const data = new Uint8Array(await r.arrayBuffer());
const data = await readBody(r, d, stallMs, say);
if (data.length !== d.size) throw new Error(`${d.file}: ${data.length} bytes, latest.json says ${d.size}; not kept`);
const sha = createHash('sha256').update(data).digest('hex');
if (sha !== d.sha256) throw new Error(`${d.file}: SHA-256 ${sha} does not match latest.json; not kept`);
@ -75,6 +75,34 @@ export async function download(d: Download, dir: string, get: Fetch = fetch, pub
return out;
}
function withStall<T>(p: Promise<T>, ms: number, url: string): Promise<T> {
let t: ReturnType<typeof setTimeout> | undefined;
const stall = new Promise<never>((_, no) => { t = setTimeout(() => no(new Error(`${url}: no data for ${ms / 1000} s; the download stalled (try again; pitfall 95)`)), ms); });
return Promise.race([p, stall]).finally(() => clearTimeout(t));
}
/** Reads the body in chunks: a progress line every 10 % and a stall error when no byte arrives for `stallMs`
* (a cold CDN cache once sent nothing for minutes and the CLI waited forever, pitfall 95). */
async function readBody(r: Response, d: Download, stallMs: number, say: (s: string) => void): Promise<Uint8Array> {
if (!r.body) return new Uint8Array(await r.arrayBuffer());
const reader = r.body.getReader();
const parts: Uint8Array[] = [];
let n = 0, shown = 0;
try {
for (;;) {
const { done, value } = await withStall(reader.read(), stallMs, d.url);
if (done) break;
parts.push(value); n += value.length;
const pct = d.size ? Math.floor((n / d.size) * 10) * 10 : 0;
if (pct > shown && pct < 100) { shown = pct; say(` ${pct}% (${(n / 1e6).toFixed(1)} MB)`); }
}
} catch (e) { void reader.cancel().catch(() => {}); throw e; }
const out = new Uint8Array(n);
let o = 0;
for (const p of parts) { out.set(p, o); o += p.length; }
return out;
}
const VERSION_RE = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/;
/** The version of an installed app: Info.plist on macOS, `storebound-desktop --version` elsewhere (15 s at most). */