diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88e7495..1de8af3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,11 +138,6 @@ jobs: run: | Copy-Item cli/target/x86_64-pc-windows-msvc/release/agent-browser.exe bin/agent-browser-win32-x64.exe - - name: Test CMD wrapper works - run: | - bin\agent-browser.cmd --version - shell: cmd - - name: Test agent-browser install command run: | $env:PATH = "$pwd\bin;$env:PATH" @@ -198,3 +193,106 @@ jobs: - name: Run serverless integration test run: pnpm exec vitest run test/serverless.test.ts + + global-install: + name: Global Install (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: rust + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + binary: agent-browser-linux-x64 + - os: macos-latest + target: aarch64-apple-darwin + binary: agent-browser-darwin-arm64 + - os: windows-latest + target: x86_64-pc-windows-msvc + binary: agent-browser-win32-x64.exe + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache Cargo dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + cli/target/ + key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('cli/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ matrix.target }}- + + - name: Build Rust CLI + run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }} + + - name: Install npm dependencies + run: pnpm install + + - name: Build TypeScript + run: pnpm build + + - name: Copy CLI binary to bin directory (Unix) + if: runner.os != 'Windows' + run: cp cli/target/${{ matrix.target }}/release/agent-browser bin/${{ matrix.binary }} + + - name: Copy CLI binary to bin directory (Windows) + if: runner.os == 'Windows' + run: Copy-Item cli/target/${{ matrix.target }}/release/agent-browser.exe bin/${{ matrix.binary }} + + - name: Test npm global install + run: | + npm pack + npm install -g agent-browser-*.tgz + agent-browser --version + shell: bash + + - name: Verify symlink points to native binary (Unix) + if: runner.os != 'Windows' + run: | + SYMLINK=$(npm prefix -g)/bin/agent-browser + TARGET=$(readlink "$SYMLINK") + echo "Symlink: $SYMLINK" + echo "Target: $TARGET" + if [[ "$TARGET" != *"${{ matrix.binary }}"* ]]; then + echo "ERROR: Symlink should point to native binary, not JS wrapper" + exit 1 + fi + echo "✓ Symlink correctly points to native binary" + shell: bash + + - name: Verify shim points to native binary (Windows) + if: runner.os == 'Windows' + run: | + $shimPath = "$(npm prefix -g)\agent-browser.cmd" + $content = Get-Content $shimPath -Raw + echo "Shim path: $shimPath" + echo "Shim content:" + echo $content + if ($content -notmatch "agent-browser-win32-x64\.exe") { + echo "ERROR: Shim should point to native .exe, not JS wrapper" + exit 1 + } + echo "✓ Shim correctly points to native binary" + shell: pwsh diff --git a/bin/agent-browser b/bin/agent-browser deleted file mode 100755 index 8b34fad..0000000 --- a/bin/agent-browser +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# agent-browser CLI wrapper -# Detects OS/arch and runs the appropriate native binary - -SCRIPT="$0" -while [ -L "$SCRIPT" ]; do - SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)" - SCRIPT="$(readlink "$SCRIPT")" - case "$SCRIPT" in /*) ;; *) SCRIPT="$SCRIPT_DIR/$SCRIPT" ;; esac -done -SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)" - -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m) -case "$OS" in darwin) OS="darwin" ;; linux) OS="linux" ;; mingw*|msys*|cygwin*) OS="win32" ;; esac -case "$ARCH" in x86_64|amd64) ARCH="x64" ;; aarch64|arm64) ARCH="arm64" ;; esac - -BINARY="$SCRIPT_DIR/agent-browser-${OS}-${ARCH}" - -if [ -f "$BINARY" ]; then - # Ensure binary is executable (npm tarballs don't preserve execute bit) - [ -x "$BINARY" ] || chmod +x "$BINARY" 2>/dev/null - exec "$BINARY" "$@" -fi - -echo "Error: No binary found for ${OS}-${ARCH}" >&2 -echo "Run 'npm run build:native' to build for your platform" >&2 -exit 1 diff --git a/bin/agent-browser.cmd b/bin/agent-browser.cmd deleted file mode 100644 index 3fa04c5..0000000 --- a/bin/agent-browser.cmd +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -setlocal -set "SCRIPT_DIR=%~dp0" -set "BINARY=%SCRIPT_DIR%agent-browser-win32-x64.exe" - -if exist "%BINARY%" ( - "%BINARY%" %* - exit /b %errorlevel% -) - -echo Error: No binary found for win32-x64 >&2 -echo Run 'npm run build:native' to build for your platform >&2 -exit /b 1 diff --git a/bin/agent-browser.js b/bin/agent-browser.js new file mode 100644 index 0000000..ee97cf3 --- /dev/null +++ b/bin/agent-browser.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +/** + * Cross-platform CLI wrapper for agent-browser + * + * This wrapper enables npx support on Windows where shell scripts don't work. + * For global installs, postinstall.js patches the shims to invoke the native + * binary directly (zero overhead). + */ + +import { spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { platform, arch } from 'os'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Map Node.js platform/arch to binary naming convention +function getBinaryName() { + const os = platform(); + const cpuArch = arch(); + + let osKey; + switch (os) { + case 'darwin': + osKey = 'darwin'; + break; + case 'linux': + osKey = 'linux'; + break; + case 'win32': + osKey = 'win32'; + break; + default: + return null; + } + + let archKey; + switch (cpuArch) { + case 'x64': + case 'x86_64': + archKey = 'x64'; + break; + case 'arm64': + case 'aarch64': + archKey = 'arm64'; + break; + default: + return null; + } + + const ext = os === 'win32' ? '.exe' : ''; + return `agent-browser-${osKey}-${archKey}${ext}`; +} + +function main() { + const binaryName = getBinaryName(); + + if (!binaryName) { + console.error(`Error: Unsupported platform: ${platform()}-${arch()}`); + process.exit(1); + } + + const binaryPath = join(__dirname, binaryName); + + if (!existsSync(binaryPath)) { + console.error(`Error: No binary found for ${platform()}-${arch()}`); + console.error(`Expected: ${binaryPath}`); + console.error(''); + console.error('Run "npm run build:native" to build for your platform,'); + console.error('or reinstall the package to trigger the postinstall download.'); + process.exit(1); + } + + // Spawn the native binary with inherited stdio + const child = spawn(binaryPath, process.argv.slice(2), { + stdio: 'inherit', + windowsHide: false, + }); + + child.on('error', (err) => { + console.error(`Error executing binary: ${err.message}`); + process.exit(1); + }); + + child.on('close', (code) => { + process.exit(code ?? 0); + }); +} + +main(); diff --git a/package.json b/package.json index 36eb60f..b627124 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "skills" ], "bin": { - "agent-browser": "./bin/agent-browser" + "agent-browser": "./bin/agent-browser.js" }, "scripts": { "prepare": "husky", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index e7a7da8..6301cf8 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -4,9 +4,12 @@ * Postinstall script for agent-browser * * Downloads the platform-specific native binary if not present. + * On global installs, patches npm's bin entry to use the native binary directly: + * - Windows: Overwrites .cmd/.ps1 shims + * - Mac/Linux: Replaces symlink to point to native binary */ -import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from 'fs'; +import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, symlinkSync, lstatSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { platform, arch } from 'os'; @@ -73,6 +76,11 @@ async function main() { chmodSync(binaryPath, 0o755); } console.log(`✓ Native binary ready: ${binaryName}`); + + // On global installs, fix npm's bin entry to use native binary directly + await fixGlobalInstallBin(); + + showPlaywrightReminder(); return; } @@ -102,7 +110,14 @@ async function main() { console.log(' 2. Run: npm run build:native'); } - // Reminder about Playwright browsers + // On global installs, fix npm's bin entry to use native binary directly + // This avoids the /bin/sh error on Windows and provides zero-overhead execution + await fixGlobalInstallBin(); + + showPlaywrightReminder(); +} + +function showPlaywrightReminder() { console.log(''); console.log('╔═══════════════════════════════════════════════════════════════════════════╗'); console.log('║ To download browser binaries, run: ║'); @@ -116,4 +131,105 @@ async function main() { console.log('╚═══════════════════════════════════════════════════════════════════════════╝'); } +/** + * Fix npm's bin entry on global installs to use the native binary directly. + * This provides zero-overhead CLI execution for global installs. + */ +async function fixGlobalInstallBin() { + if (platform() === 'win32') { + await fixWindowsShims(); + } else { + await fixUnixSymlink(); + } +} + +/** + * Fix npm symlink on Mac/Linux global installs. + * Replace the symlink to the JS wrapper with a symlink to the native binary. + */ +async function fixUnixSymlink() { + // Get npm's global bin directory (npm prefix -g + /bin) + let npmBinDir; + try { + const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim(); + npmBinDir = join(prefix, 'bin'); + } catch { + return; // npm not available + } + + const symlinkPath = join(npmBinDir, 'agent-browser'); + + // Check if symlink exists (indicates global install) + try { + const stat = lstatSync(symlinkPath); + if (!stat.isSymbolicLink()) { + return; // Not a symlink, don't touch it + } + } catch { + return; // Symlink doesn't exist, not a global install + } + + // Replace symlink to point directly to native binary + try { + unlinkSync(symlinkPath); + symlinkSync(binaryPath, symlinkPath); + console.log('✓ Optimized: symlink points to native binary (zero overhead)'); + } catch (err) { + // Permission error or other issue - not critical, JS wrapper still works + console.log(`⚠ Could not optimize symlink: ${err.message}`); + console.log(' CLI will work via Node.js wrapper (slightly slower startup)'); + } +} + +/** + * Fix npm-generated shims on Windows global installs. + * npm generates shims that try to run /bin/sh, which doesn't exist on Windows. + * We overwrite them to invoke the native .exe directly. + */ +async function fixWindowsShims() { + // Check if this is a global install by looking for npm's global prefix + let npmBinDir; + try { + npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim(); + } catch { + return; // Not a global install or npm not available + } + + // The shims are in the npm prefix directory (not prefix/bin on Windows) + const cmdShim = join(npmBinDir, 'agent-browser.cmd'); + const ps1Shim = join(npmBinDir, 'agent-browser.ps1'); + + // Only fix if shims exist (indicates global install) + if (!existsSync(cmdShim)) { + return; + } + + // Path to native binary relative to npm prefix + const relativeBinaryPath = 'node_modules\\agent-browser\\bin\\agent-browser-win32-x64.exe'; + + try { + // Overwrite .cmd shim + const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`; + writeFileSync(cmdShim, cmdContent); + + // Overwrite .ps1 shim + const ps1Content = `#!/usr/bin/env pwsh +$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent +$exe = "" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + $exe = ".exe" +} +& "$basedir/${relativeBinaryPath.replace(/\\/g, '/')}" $args +exit $LASTEXITCODE +`; + writeFileSync(ps1Shim, ps1Content); + + console.log('✓ Optimized: shims point to native binary (zero overhead)'); + } catch (err) { + // Permission error or other issue - not critical, JS wrapper still works + console.log(`⚠ Could not optimize shims: ${err.message}`); + console.log(' CLI will work via Node.js wrapper (slightly slower startup)'); + } +} + main().catch(console.error);