postinstall.mjs
1 #!/usr/bin/env node 2 3 import fs from 'node:fs' 4 import path from 'node:path' 5 import { fileURLToPath } from 'node:url' 6 import { spawnSync } from 'node:child_process' 7 const INSTALL_METADATA_FILE = '.swarmclaw-install.json' 8 const scriptDir = path.dirname(fileURLToPath(import.meta.url)) 9 const packageRoot = path.resolve(scriptDir, '..') 10 const ensureSandboxBrowserScript = path.join(packageRoot, 'scripts', 'ensure-sandbox-browser-image.mjs') 11 12 function detectPackageManagerFromUserAgent(userAgent) { 13 const normalized = String(userAgent || '').toLowerCase() 14 if (normalized.startsWith('pnpm/')) return 'pnpm' 15 if (normalized.startsWith('yarn/')) return 'yarn' 16 if (normalized.startsWith('bun/')) return 'bun' 17 if (normalized.startsWith('npm/')) return 'npm' 18 return null 19 } 20 21 const installedWith = detectPackageManagerFromUserAgent(process.env.npm_config_user_agent) || 'npm' 22 23 function logNote(message) { 24 process.stdout.write(`[postinstall] ${message}\n`) 25 } 26 27 function logWarn(message) { 28 process.stderr.write(`[postinstall] WARN: ${message}\n`) 29 } 30 31 function commandExists(name) { 32 const lookup = process.platform === 'win32' ? 'where' : 'which' 33 const result = spawnSync(lookup, [name], { 34 cwd: packageRoot, 35 encoding: 'utf8', 36 stdio: 'pipe', 37 }) 38 return !result.error && (result.status ?? 1) === 0 39 } 40 41 function formatFailure(result) { 42 const detail = [ 43 result.error?.message, 44 String(result.stderr || '').trim(), 45 String(result.stdout || '').trim(), 46 ].find(Boolean) 47 return detail || `exit ${result.status ?? 1}` 48 } 49 50 try { 51 fs.writeFileSync( 52 new URL(`../${INSTALL_METADATA_FILE}`, import.meta.url), 53 JSON.stringify({ 54 packageManager: installedWith, 55 installedAt: new Date().toISOString(), 56 }, null, 2), 57 'utf8', 58 ) 59 } catch { 60 // Ignore metadata write failures for install resilience. 61 } 62 63 const result = spawnSync('npm', ['rebuild', 'better-sqlite3', '--silent'], { 64 cwd: packageRoot, 65 encoding: 'utf8', 66 stdio: 'pipe', 67 }) 68 69 if (result.error || (result.status ?? 0) !== 0) { 70 logWarn(`better-sqlite3 rebuild failed: ${formatFailure(result)}`) 71 logWarn('Retry manually with: npm rebuild better-sqlite3') 72 } 73 74 if (!process.env.CI) { 75 if (!fs.existsSync(ensureSandboxBrowserScript)) { 76 logNote('Sandbox browser image helper is not present in this install context. Skipping setup.') 77 } else { 78 const sandboxImage = spawnSync(process.execPath, [ensureSandboxBrowserScript, '--quiet'], { 79 cwd: packageRoot, 80 encoding: 'utf8', 81 stdio: 'pipe', 82 }) 83 if (sandboxImage.error || (sandboxImage.status ?? 0) !== 0) { 84 logWarn(`sandbox browser image setup failed: ${formatFailure(sandboxImage)}`) 85 logWarn('Retry manually with: node ./scripts/ensure-sandbox-browser-image.mjs') 86 } 87 88 if (!commandExists('docker')) { 89 logNote('Docker was not found. Browser sandboxing will use the host Playwright runtime until Docker is installed.') 90 } 91 } 92 } 93 94 if (!process.env.CI) { 95 process.stdout.write('\n') 96 process.stdout.write('Thanks for installing SwarmClaw.\n') 97 process.stdout.write('If it helps you, please star the repo: https://github.com/swarmclawai/swarmclaw\n') 98 process.stdout.write('\n') 99 }