Product design skills

scripts/ensure-playwright.mjs

A supporting file of the design-critique skill, shown as source.

#!/usr/bin/env node
/**
 * ensure-playwright.mjs — resolve Playwright ON DEMAND, never vendored in the app.
 *
 * Resolution order:
 *   1. a normally-resolvable `playwright` (globally / already on the path)
 *   2. a cached copy under ~/.cache/kirocrew-design-critique (override: $DC_PW_DIR)
 *   3. install it into that cache dir on first use, then load it
 *
 * Falls back to null so callers can use headless Chrome instead. Uses your
 * installed Chrome via channel:'chrome', so no browser download is needed.
 *
 *   import { getPlaywright } from './ensure-playwright.mjs'
 *   const pw = await getPlaywright()          // { chromium, ... } or null
 *   const pw = await getPlaywright({ autoInstall: false })  // never install
 */
import { spawnSync } from 'node:child_process'
import { existsSync, mkdirSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'

const PW_VERSION = '1.47.2' // last line that supports Node 18
const CACHE = process.env.DC_PW_DIR || join(homedir(), '.cache', 'kirocrew-design-critique')
const PW_ENTRY = join(CACHE, 'node_modules', 'playwright', 'index.js')

// Playwright is CJS; a dynamic import may put the API on `.default`.
function normalize(mod) {
  if (!mod) return null
  if (mod.chromium) return mod
  if (mod.default && mod.default.chromium) return mod.default
  return null
}

export async function getPlaywright({ autoInstall = true } = {}) {
  // 1. resolvable from the normal module paths?
  try { const m = normalize(await import('playwright')); if (m) return m } catch { /* not here */ }
  // 2. present in the cache dir?
  if (existsSync(PW_ENTRY)) {
    try { const m = normalize(await import(pathToFileURL(PW_ENTRY).href)); if (m) return m } catch { /* fallthrough */ }
  }
  if (!autoInstall) return null
  // 3. install into the cache dir on demand (one-time)
  console.error(`ensure-playwright: installing playwright@${PW_VERSION} into ${CACHE} (one-time, needs network)…`)
  try { mkdirSync(CACHE, { recursive: true }) } catch { /* ignore */ }
  const r = spawnSync('npm', ['install', '--prefix', CACHE, `playwright@${PW_VERSION}`, '--no-audit', '--no-fund', '--silent'],
    { stdio: 'inherit', timeout: 240000 })
  if (r.status !== 0 || !existsSync(PW_ENTRY)) {
    console.error('ensure-playwright: install failed — falling back to headless Chrome.')
    return null
  }
  // The npm package ships no browser binaries, so installing it alone leaves
  // every launch rejecting on a machine with no Chrome of its own (a bare Linux
  // box is the normal case). Download the Chromium build too. The env is
  // inherited untouched so it lands in Playwright's own home-directory cache —
  // the same place the launch side will look, and never inside the project.
  const cli = join(CACHE, 'node_modules', 'playwright', 'cli.js')
  if (existsSync(cli)) {
    console.error('ensure-playwright: downloading the Chromium build (one-time)…')
    const b = spawnSync(process.execPath, [cli, 'install', 'chromium'],
      { stdio: 'inherit', timeout: 600000 })
    if (b.status !== 0) {
      // Not fatal: a machine that already has Chrome installed still works via
      // the channel fallback, so report and continue rather than refusing.
      console.error('ensure-playwright: Chromium download failed — will try a system browser.')
    }
  }
  try { return normalize(await import(pathToFileURL(PW_ENTRY).href)) } catch { return null }
}