Architecture

This page explains how peek’s major systems work and how they fit together.

System Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     peek CLI                          β”‚
β”‚  snap | check | dev | scene | pool | clean            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚              β”‚               β”‚                        β”‚
β”‚   snap.ts    β”‚   runner.ts   β”‚   scene.ts             β”‚
β”‚   Web/native β”‚   Check       β”‚   Game engine          β”‚
β”‚   screenshotsβ”‚   orchestratorβ”‚   spatial checks       β”‚
β”‚              β”‚               β”‚                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                        β”‚
β”‚      β”‚       β”‚               β”‚                        β”‚
β”‚ pool.ts      β”‚  checks/      β”‚                        β”‚
β”‚ Browser      β”‚  6 layout     β”‚                        β”‚
β”‚ pooling      β”‚  checks       β”‚                        β”‚
β”‚              β”‚               β”‚                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                        β”‚
β”‚              β”‚               β”‚                        β”‚
β”‚ browser.ts   β”‚ reporter.ts   β”‚                        β”‚
β”‚ Chrome       β”‚ text/json/    β”‚                        β”‚
β”‚ detection    β”‚ junit output  β”‚                        β”‚
β”‚              β”‚               β”‚                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                       β”‚
β”‚   native.ts              frameworks/                  β”‚
β”‚   macOS ScreenCaptureKit  Auto-detect & dev server    β”‚
β”‚                                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Browser Pooling

The pool is peek’s most important optimization. Instead of launching Chrome on every invocation (~3.8s), peek maintains a persistent Chrome instance that survives between CLI calls.

How It Works

  1. First call: acquireBrowser() finds no pool entry. It launches Chrome as a detached process with --remote-debugging-port=0 (Chrome picks a free port).
  2. Pool file: The WebSocket endpoint, PID, and timestamps are written to ~/.peek/pool.json.
  3. Subsequent calls: acquireBrowser() reads the pool file, verifies the process is alive, and connects via puppeteer.connect(). Each call gets its own Page instance.
  4. Disconnect vs close: After each snap/check, peek calls browser.disconnect() (not close()). Chrome stays alive.
  5. Idle timeout: If the pool has been idle for 5 minutes, the next invocation kills it and launches a fresh browser.
  6. Concurrency safety: File-based advisory locking (~/.peek/pool.lock) prevents race conditions when multiple agents access the pool simultaneously.

Opting Out

Use --no-pool to force a fresh browser that is closed after the command:

peek snap http://localhost:3000 --no-pool

This is useful in isolated CI environments or when debugging browser state issues.

Chrome Detection

browser.ts resolves the Chrome executable path using a 3-tier strategy:

  1. System Chrome – Uses chrome-launcher to find installed Chrome/Chromium.
  2. Cached Chromium – Checks ~/.cache/peek/.chromium-path for a previously downloaded binary.
  3. Auto-download – Downloads Chromium via @puppeteer/browsers and caches it in ~/.cache/peek/.

This means peek works on any machine without manual browser setup.

Screenshot Pipeline

When you run peek snap <url>:

  1. Browser acquisition – Pool or fresh launch (see above).
  2. Setup script (optional) – If --setup is provided, a temporary page runs the script to establish session cookies, then closes.
  3. Page creation – A new Page is opened with the specified viewport (default: 1280x720).
  4. Navigation – page.goto() with waitUntil: networkidle2 (configurable).
  5. Wait (optional) – Additional wait via --wait <ms>.
  6. Screenshot – page.screenshot() to the output path (or an auto-generated path in /tmp/peek/).
  7. Checks (optional) – If --checks is set, layout checks run on the same page before it closes.
  8. Output – The absolute file path is printed to stdout.

Check Pipeline

When you run peek check <urls...>:

  1. Browser launch – Always uses launchBrowser() (not the pool), since checks may run on multiple URLs with different viewports.
  2. Setup script (optional) – Runs first if --setup is provided.
  3. Per-URL, per-viewport loop – For each URL and viewport combination:
    • New page, set viewport.
    • Navigate with waitUntil: networkidle0.
    • Optionally wait for SPA hydration (--wait-for-hydration).
    • 500ms settle delay.
    • Run all checks (or filtered subset via --only).
  4. Output – Formatted results per URL/viewport, then summary.
  5. Exit code – Determined by --fail-on threshold.

Check Architecture

Each check implements the Check interface:

interface Check &#123;
  name: string;
  description: string;
  run: (ctx: CheckContext) =&gt; Promise&lt;CheckResult&gt;;
&#125;

The CheckContext provides the Puppeteer Page and the merged CheckConfig. Each check uses page.evaluate() to run DOM inspection logic in the browser context, then returns issues with element selectors, severity, descriptive messages, and fix hints.

All 6 checks respect:

  • The global ignore selector list from config
  • Per-check ignore selectors
  • The data-peek-ignore HTML attribute

Native Capture Architecture

Native capture (macOS only) bypasses the browser entirely:

  1. Swift helper – On first use, peek compiles a Swift helper (peek-capture) from an embedded source string. The binary is cached at ~/.cache/peek/peek-capture.
  2. Window enumeration – The helper calls CGWindowListCopyWindowInfo to list on-screen windows with their IDs, owners, and PIDs.
  3. Window matching – peek finds the target window by app name (--app) or PID (--pid).
  4. Capture – SCScreenshotManager.captureImage() captures the window at 2x resolution via ScreenCaptureKit.
  5. Output – The PNG is written to disk and the path is printed to stdout.

Requires screen recording permission (System Settings > Privacy & Security > Screen Recording).

Scene Check Architecture

Scene checks (peek scene) work without a browser:

  1. Read manifest – The JSON file describes a room with entities (positions, sizes, names).
  2. Run spatial checks – Four checks: entity overlap, out-of-bounds, zero-size, and off-camera visibility.
  3. Output – Same HealthResults format as browser checks, formatted via the same reporter.

This is designed for game engine UIs (Bevy, Unity) where the rendering surface is a canvas opaque to DOM inspection.

Framework Detection

The peek dev command auto-detects the project framework by inspecting package.json dependencies:

FrameworkDetection keyDefault port
Next.jsnext3000
SvelteKit@sveltejs/kit5173
Nuxtnuxt or nuxt33000
Remix@remix-run/react3000
Astroastro4321
Create React Appreact-scripts3000
Vitevite5173

Hydration detection is automatic: for frameworks that render server-side HTML and then hydrate on the client (Next.js, SvelteKit, Nuxt, Remix, Astro, CRA), peek waits for the hydration to complete before running checks.

Programmatic API

peek exports its core functions for use as a library:

import &#123; snap, checkPage, createChecker, runSceneChecks &#125; from 'peek';

// Screenshot
const result = await snap(&#123; url: 'http://localhost:3000' &#125;);
console.log(result.path);

// Check a page (pass a Puppeteer Page)
const results = await checkPage(page, &#123; ignore: ['.skip'] &#125;);

// Reusable checker with config
const checker = createChecker(&#123; ignore: ['.ads'] &#125;);
const results = await checker.run(page);