Files
hyperframes/packages/cli/src
Vance Ingalls c1ba528a1f fix(cli): scan puppeteer cache for chrome-headless-shell; warn on system-chrome fallback (#821)
## What

Two correctness fixes to `packages/cli/src/browser/manager.ts` so the CLI picks the right Chrome binary for any perf path that depends on `chrome-headless-shell`:

1. **Also scan the puppeteer-managed cache.** `findFromCache` now reads from both `~/.cache/hyperframes/chrome` (the CLI's own managed cache) and `~/.cache/puppeteer/chrome-headless-shell/<version>/<platform-dir>/chrome-headless-shell` (the path layout that the engine's `resolveHeadlessShellPath` already reads from). When `chrome-headless-shell` is present in either cache, it now wins over system Chrome.
2. **Warn when falling through to a non-`chrome-headless-shell` system binary on Linux.** A single one-time `console.warn` explains the perf consequence and points the user at `npx @puppeteer/browsers install chrome-headless-shell`. Linux-scoped because the BeginFrame perf path is Linux-only.

## Why

Discovered in a recent spike on the BeginFrame perf path. On a clean install, the CLI's hyperframes-managed cache (`~/.cache/hyperframes/chrome`) is empty, so `findFromCache` returns `undefined`. The CLI then falls through to `findFromSystem()` and picks `/usr/bin/google-chrome`, exporting it to the engine via `PRODUCER_HEADLESS_SHELL_PATH` in `render.ts`. The engine receives that path, sees it's already set, and skips its own correct `~/.cache/puppeteer/chrome-headless-shell` scan.

Regular Chrome (147+) has dropped `HeadlessExperimental.enable`. The engine's BeginFrame probe correctly catches this and silently falls back to screenshot mode — but the operator sees no signal, so any user who installed `chrome-headless-shell` via `npx @puppeteer/browsers install` (the standard puppeteer flow) silently loses the perf path.

This is a "two codepaths know about 'the chrome we ship' but look in different places" bug. The fix collapses them.

## How

- `findFromCache` now consults both caches. Hyperframes-managed cache wins when both contain a binary (preserves existing behavior).
- New `findFromPuppeteerCache` mirrors `resolveHeadlessShellPath` from `packages/engine/src/services/browserManager.ts` — same path layout, same newest-first version sort. A comment in both files notes they need to move together if puppeteer ever changes the on-disk layout.
- `warnSystemFallbackOnce` is gated on `process.platform === "linux"` and on the binary name (`basename` of the path being `chrome-headless-shell`/`.exe`). One-shot latch so a long-running `hyperframes studio` process isn't spammed. Exported test-reset helper `_resetSystemFallbackWarnForTests` for the unit tests.

No public-API changes. `findBrowser`, `ensureBrowser`, `clearBrowser`, `setBrowserPath` all keep the same signatures.

## Test plan

- [x] Unit tests added (`packages/cli/src/browser/manager.test.ts`, 7 tests):
  - cache hit on hyperframes dir
  - cache hit on puppeteer dir (the new path)
  - newest-version preference when multiple versions are cached
  - system fallback + Linux warning emitted
  - no warning when the resolved path is itself `chrome-headless-shell` (e.g. `HYPERFRAMES_BROWSER_PATH` override)
  - no warning on macOS (Linux-only perf path)
  - one-time warning idempotency across repeated `findBrowser()` calls
- [x] `bun run --filter @hyperframes/cli typecheck` — passes
- [x] `bun run --filter @hyperframes/cli test` — 305/305 passing
- [x] `bun run --filter @hyperframes/cli build` — passes
- [ ] Manual smoke on a Linux host with chrome-headless-shell in the puppeteer cache (skipped — sandbox already has both binaries and the unit tests cover the resolution logic deterministically; reviewers welcome to verify)

— Vai
2026-05-14 17:09:35 -07:00
..