/ scripts / postinstall.mjs
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  }