/ scripts / ensure-sandbox-browser-image.mjs
ensure-sandbox-browser-image.mjs
  1  #!/usr/bin/env node
  2  
  3  import fs from 'node:fs'
  4  import path from 'node:path'
  5  import crypto from 'node:crypto'
  6  import { spawnSync } from 'node:child_process'
  7  
  8  const cwd = process.cwd()
  9  const args = new Set(process.argv.slice(2))
 10  const quiet = args.has('--quiet')
 11  const required = args.has('--required')
 12  const image = process.env.SWARMCLAW_SANDBOX_BROWSER_IMAGE || 'swarmclaw-sandbox-browser:bookworm-slim'
 13  const SOURCE_LABEL = 'swarmclaw.sandboxBrowserSourceHash'
 14  
 15  function log(message) {
 16    if (!quiet) process.stdout.write(`[sandbox-browser] ${message}\n`)
 17  }
 18  
 19  function fail(message, code = 1) {
 20    process.stderr.write(`[sandbox-browser] ERROR: ${message}\n`)
 21    process.exit(code)
 22  }
 23  
 24  function run(command, commandArgs, options = {}) {
 25    return spawnSync(command, commandArgs, {
 26      cwd,
 27      encoding: 'utf8',
 28      ...options,
 29    })
 30  }
 31  
 32  function commandExists(name) {
 33    const lookup = process.platform === 'win32' ? 'where' : 'which'
 34    const result = run(lookup, [name])
 35    return !result.error && (result.status ?? 1) === 0
 36  }
 37  
 38  function computeSourceHash() {
 39    const hash = crypto.createHash('sha1')
 40    for (const relative of ['Dockerfile.sandbox-browser', 'scripts/sandbox-browser-entrypoint.sh']) {
 41      const absolute = path.join(cwd, relative)
 42      if (!fs.existsSync(absolute)) {
 43        fail(`Missing sandbox browser source file: ${relative}`)
 44      }
 45      hash.update(relative)
 46      hash.update(fs.readFileSync(absolute))
 47    }
 48    return hash.digest('hex')
 49  }
 50  
 51  function readImageLabel(name, label) {
 52    const result = run('docker', ['image', 'inspect', '--format', `{{ index .Config.Labels "${label}" }}`, name])
 53    if (result.error || (result.status ?? 1) !== 0) return null
 54    const value = String(result.stdout || '').trim()
 55    return value && value !== '<no value>' ? value : null
 56  }
 57  
 58  function buildImage(sourceHash) {
 59    log(`Building sandbox browser image ${image}...`)
 60    const result = spawnSync(
 61      'docker',
 62      [
 63        'build',
 64        '-f', 'Dockerfile.sandbox-browser',
 65        '-t', image,
 66        '--label', `${SOURCE_LABEL}=${sourceHash}`,
 67        '.',
 68      ],
 69      {
 70        cwd,
 71        stdio: 'inherit',
 72      },
 73    )
 74    if (result.error || (result.status ?? 1) !== 0) {
 75      if (required) {
 76        fail(`Failed to build sandbox browser image ${image}.`, result.status ?? 1)
 77      }
 78      log(`Skipping sandbox browser image after build failure.`)
 79      return false
 80    }
 81    log(`Sandbox browser image ready: ${image}`)
 82    return true
 83  }
 84  
 85  function main() {
 86    if (!commandExists('docker')) {
 87      if (required) fail('Docker is required to build the sandbox browser image.')
 88      log('Docker not available. Skipping sandbox browser image build.')
 89      return
 90    }
 91  
 92    const sourceHash = computeSourceHash()
 93    const currentHash = readImageLabel(image, SOURCE_LABEL)
 94    if (currentHash === sourceHash) {
 95      log(`Sandbox browser image already current: ${image}`)
 96      return
 97    }
 98  
 99    buildImage(sourceHash)
100  }
101  
102  main()