From d2aa4f8bc4654e2be83df842733b2b77ea98dddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 15 Jul 2026 02:29:33 -0400 Subject: [PATCH] fix(cli): preflight browser archive extraction (#2469) --- packages/cli/src/commands/doctor.test.ts | 30 +++++++++++++++++++++ packages/cli/src/commands/doctor.ts | 34 +++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/doctor.test.ts b/packages/cli/src/commands/doctor.test.ts index 5b087c664..c37431910 100644 --- a/packages/cli/src/commands/doctor.test.ts +++ b/packages/cli/src/commands/doctor.test.ts @@ -88,6 +88,36 @@ describe("parseToolVersion", () => { }); }); +describe("checkArchiveExtractor", () => { + it("reports a missing unzip dependency on non-Windows hosts", async () => { + const doctor = await import("./doctor.js"); + expect(doctor).toHaveProperty("checkArchiveExtractor"); + const result = doctor.checkArchiveExtractor("linux", () => false); + + expect(result).toEqual({ + ok: false, + detail: "Not found", + hint: "Install unzip so HyperFrames can extract its managed Chrome download.", + }); + }); + + it("accepts unzip on non-Windows hosts", async () => { + const { checkArchiveExtractor } = await import("./doctor.js"); + expect(checkArchiveExtractor("darwin", (command) => command === "unzip")).toEqual({ + ok: true, + detail: "unzip", + }); + }); + + it("uses the built-in Windows extraction path", async () => { + const { checkArchiveExtractor } = await import("./doctor.js"); + expect(checkArchiveExtractor("win32", () => false)).toEqual({ + ok: true, + detail: "Built into Windows", + }); + }); +}); + describe("buildDoctorReport", () => { it("emits the locked schema shape", () => { const report = buildDoctorReport(OUTCOMES_ALL_OK); diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index f3fe14822..2bb7800a8 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -1,6 +1,6 @@ // fallow-ignore-file complexity import { defineCommand } from "citty"; -import { execSync } from "node:child_process"; +import { execFileSync, execSync } from "node:child_process"; import { platform } from "node:os"; import type { Example } from "./_examples.js"; import { c } from "../ui/colors.js"; @@ -133,6 +133,37 @@ function checkDisk(): CheckResult { return { ok: true, detail: `${freeGb} GB free` }; } +function commandExists(command: string): boolean { + try { + execFileSync("which", [command], { stdio: "ignore", timeout: 5000 }); + return true; + } catch { + return false; + } +} + +/** + * Puppeteer's managed Chrome download delegates ZIP extraction to `unzip` on + * macOS and Linux. Windows has built-in tar/PowerShell extraction paths, so it + * does not need this Unix dependency. + */ +export function checkArchiveExtractor( + hostPlatform = platform(), + exists: (command: string) => boolean = commandExists, +): CheckResult { + if (hostPlatform === "win32") { + return { ok: true, detail: "Built into Windows" }; + } + if (exists("unzip")) { + return { ok: true, detail: "unzip" }; + } + return { + ok: false, + detail: "Not found", + hint: "Install unzip so HyperFrames can extract its managed Chrome download.", + }; +} + function checkEnvironment(): CheckResult { const sys = getSystemMeta(); const parts: string[] = []; @@ -238,6 +269,7 @@ export default defineCommand({ { name: "CPU", run: checkCPU }, { name: "Memory", run: checkMemory }, { name: "Disk", run: checkDisk }, + { name: "Archive extractor", run: checkArchiveExtractor }, ]; // /dev/shm is only relevant on Linux (especially Docker)