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()