/ bin / agent-browser.js
agent-browser.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Cross-platform CLI wrapper for agent-browser
  5   * 
  6   * This wrapper enables npx support on Windows where shell scripts don't work.
  7   * For global installs, postinstall.js patches the shims to invoke the native
  8   * binary directly (zero overhead).
  9   */
 10  
 11  import { spawn, execSync } from 'child_process';
 12  import { existsSync, accessSync, chmodSync, constants } from 'fs';
 13  import { dirname, join } from 'path';
 14  import { fileURLToPath } from 'url';
 15  import { platform, arch } from 'os';
 16  
 17  const __dirname = dirname(fileURLToPath(import.meta.url));
 18  
 19  // Detect if the system uses musl libc (e.g. Alpine Linux)
 20  function isMusl() {
 21    if (platform() !== 'linux') return false;
 22    try {
 23      const result = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });
 24      return result.toLowerCase().includes('musl');
 25    } catch {
 26      return existsSync('/lib/ld-musl-x86_64.so.1') || existsSync('/lib/ld-musl-aarch64.so.1');
 27    }
 28  }
 29  
 30  // Map Node.js platform/arch to binary naming convention
 31  function getBinaryName() {
 32    const os = platform();
 33    const cpuArch = arch();
 34  
 35    let osKey;
 36    switch (os) {
 37      case 'darwin':
 38        osKey = 'darwin';
 39        break;
 40      case 'linux':
 41        osKey = isMusl() ? 'linux-musl' : 'linux';
 42        break;
 43      case 'win32':
 44        osKey = 'win32';
 45        break;
 46      default:
 47        return null;
 48    }
 49  
 50    let archKey;
 51    switch (cpuArch) {
 52      case 'x64':
 53      case 'x86_64':
 54        archKey = 'x64';
 55        break;
 56      case 'arm64':
 57      case 'aarch64':
 58        archKey = 'arm64';
 59        break;
 60      default:
 61        return null;
 62    }
 63  
 64    const ext = os === 'win32' ? '.exe' : '';
 65    return `agent-browser-${osKey}-${archKey}${ext}`;
 66  }
 67  
 68  function main() {
 69    const binaryName = getBinaryName();
 70  
 71    if (!binaryName) {
 72      console.error(`Error: Unsupported platform: ${platform()}-${arch()}`);
 73      process.exit(1);
 74    }
 75  
 76    const binaryPath = join(__dirname, binaryName);
 77  
 78    if (!existsSync(binaryPath)) {
 79      console.error(`Error: No binary found for ${platform()}-${arch()}`);
 80      console.error(`Expected: ${binaryPath}`);
 81      console.error('');
 82      console.error('Run "npm run build:native" to build for your platform,');
 83      console.error('or reinstall the package to trigger the postinstall download.');
 84      process.exit(1);
 85    }
 86  
 87    // Ensure binary is executable (fixes EACCES on macOS/Linux when postinstall didn't run,
 88    // e.g., when using bun which blocks lifecycle scripts by default)
 89    if (platform() !== 'win32') {
 90      try {
 91        accessSync(binaryPath, constants.X_OK);
 92      } catch {
 93        // Binary exists but isn't executable - fix it
 94        try {
 95          chmodSync(binaryPath, 0o755);
 96        } catch (chmodErr) {
 97          console.error(`Error: Cannot make binary executable: ${chmodErr.message}`);
 98          console.error('Try running: chmod +x ' + binaryPath);
 99          process.exit(1);
100        }
101      }
102    }
103  
104    // Spawn the native binary with inherited stdio
105    const child = spawn(binaryPath, process.argv.slice(2), {
106      stdio: 'inherit',
107      windowsHide: false,
108    });
109  
110    child.on('error', (err) => {
111      console.error(`Error executing binary: ${err.message}`);
112      process.exit(1);
113    });
114  
115    child.on('close', (code) => {
116      process.exit(code ?? 0);
117    });
118  }
119  
120  main();