/ scripts / easy-setup.mjs
easy-setup.mjs
  1  #!/usr/bin/env node
  2  
  3  import fs from 'node:fs'
  4  import path from 'node:path'
  5  import { spawnSync } from 'node:child_process'
  6  
  7  const isWindows = process.platform === 'win32'
  8  const args = new Set(process.argv.slice(2))
  9  const startAfterSetup = args.has('--start') || args.has('--prod')
 10  const productionMode = args.has('--prod')
 11  const skipInstall = args.has('--skip-install')
 12  const cwd = process.cwd()
 13  
 14  function log(message) {
 15    process.stdout.write(`[setup] ${message}\n`)
 16  }
 17  
 18  function fail(message, code = 1) {
 19    process.stderr.write(`[setup] ERROR: ${message}\n`)
 20    process.exit(code)
 21  }
 22  
 23  function run(command, commandArgs, options = {}) {
 24    const printable = `${command} ${commandArgs.join(' ')}`.trim()
 25    log(`$ ${printable}`)
 26    const result = spawnSync(command, commandArgs, {
 27      cwd,
 28      stdio: 'inherit',
 29      ...(isWindows && { shell: true }),
 30      ...options,
 31    })
 32    if (result.error) fail(result.error.message)
 33    if ((result.status ?? 1) !== 0) {
 34      fail(`Command failed: ${printable}`, result.status ?? 1)
 35    }
 36  }
 37  
 38  function runOptional(command, commandArgs, options = {}) {
 39    const printable = `${command} ${commandArgs.join(' ')}`.trim()
 40    log(`$ ${printable}`)
 41    const result = spawnSync(command, commandArgs, {
 42      cwd,
 43      stdio: 'inherit',
 44      ...(isWindows && { shell: true }),
 45      ...options,
 46    })
 47    if (result.error || (result.status ?? 1) !== 0) {
 48      log(`Optional step failed: ${printable}`)
 49      return false
 50    }
 51    return true
 52  }
 53  
 54  function ensureNodeVersion() {
 55    const version = process.versions.node
 56    const [majorRaw, minorRaw] = version.split('.')
 57    const major = Number.parseInt(majorRaw || '0', 10)
 58    const minor = Number.parseInt(minorRaw || '0', 10)
 59    if (major < 22 || (major === 22 && minor < 6)) {
 60      fail(`Detected Node ${version}. SwarmClaw requires Node 22.6 or newer.`)
 61    }
 62    log(`Node ${version} detected.`)
 63  }
 64  
 65  function ensureNpm() {
 66    const result = spawnSync('npm', ['--version'], { cwd, encoding: 'utf8', ...(isWindows && { shell: true }) })
 67    if (result.error || (result.status ?? 1) !== 0) {
 68      fail('npm was not found. Install npm and rerun this setup command.')
 69    }
 70    log(`npm ${String(result.stdout || '').trim()} detected.`)
 71  }
 72  
 73  function commandExists(name) {
 74    const lookup = process.platform === 'win32' ? 'where' : 'which'
 75    const result = spawnSync(lookup, [name], { cwd, encoding: 'utf8', ...(isWindows && { shell: true }) })
 76    return !result.error && (result.status ?? 1) === 0
 77  }
 78  
 79  function ensureProjectRoot() {
 80    const pkgPath = path.join(cwd, 'package.json')
 81    if (!fs.existsSync(pkgPath)) {
 82      fail(`package.json was not found in ${cwd}. Run this command from the SwarmClaw project root.`)
 83    }
 84  }
 85  
 86  function ensureEnvFile() {
 87    const envPath = path.join(cwd, '.env.local')
 88    if (!fs.existsSync(envPath)) {
 89      fs.writeFileSync(
 90        envPath,
 91        '# SwarmClaw local environment variables\n# ACCESS_KEY and CREDENTIAL_SECRET are auto-generated on first app run.\n',
 92        'utf8',
 93      )
 94      log('Created .env.local.')
 95    } else {
 96      log('.env.local already exists.')
 97    }
 98  }
 99  
100  function ensureDataDir() {
101    const dataDir = path.join(cwd, 'data')
102    fs.mkdirSync(dataDir, { recursive: true })
103    log(`Data directory ready at ${dataDir}.`)
104  }
105  
106  function printNextSteps() {
107    process.stdout.write('\n')
108    log('Setup complete.')
109    process.stdout.write('\n')
110    process.stdout.write('Next steps:\n')
111    process.stdout.write('1. Run `npm run dev`.\n')
112    process.stdout.write('2. Open http://localhost:3456 in your browser.\n')
113    process.stdout.write('3. Copy the access key printed in the terminal and finish the setup wizard.\n')
114    process.stdout.write('   Or run `npx swarmclaw setup init` for interactive CLI setup.\n')
115    process.stdout.write('4. For updates later, run `npm run update:easy`.\n')
116  }
117  
118  function main() {
119    ensureProjectRoot()
120    ensureNodeVersion()
121    ensureNpm()
122    ensureDataDir()
123    ensureEnvFile()
124  
125    if (!skipInstall) {
126      run('npm', ['install'])
127    } else {
128      log('Skipping dependency install (--skip-install).')
129    }
130  
131    runOptional('node', ['./scripts/ensure-sandbox-browser-image.mjs'])
132    if (!commandExists('docker')) {
133      log('Docker not detected. SwarmClaw will use the host Playwright runtime until Docker Desktop is installed.')
134    }
135  
136    if (productionMode) {
137      run('npm', ['run', 'build'])
138    }
139  
140    if (startAfterSetup) {
141      run('npm', ['run', productionMode ? 'start' : 'dev'])
142      return
143    }
144  
145    printNextSteps()
146  }
147  
148  main()