test: add Windows npm global install CI test (reproduces #262) (#293)

* test: add Windows npm global install CI test (reproduces #262)

This test packs the package and installs it globally with npm,
then runs agent-browser --version. This reproduces the issue where
npm-generated shims on Windows try to invoke /bin/sh which doesn't exist.

The bin/agent-browser.js wrapper is added but not yet wired up,
so this commit should fail CI to confirm the issue.

* fix: Windows npm global install and npx support

The shell script wrapper (bin/agent-browser) with #!/bin/sh shebang
causes npm to generate Windows shims that try to invoke /bin/sh,
which doesn't exist on Windows.

This fix uses a hybrid approach:

1. Node.js wrapper (bin/agent-browser.js) as bin entry
   - Makes npx work on all platforms
   - ~100ms overhead (acceptable since npx has its own overhead)

2. postinstall patches bin entries for global installs
   - Windows: Overwrites .cmd/.ps1 shims to invoke .exe directly
   - Mac/Linux: Replaces symlink to point to native binary
   - Zero overhead for `npm i -g agent-browser` users on all platforms

Also fixes PowerShell glob expansion in CI test.

Fixes #262

* fix: Windows npm global install and npx support

The shell script wrapper (bin/agent-browser) with #!/bin/sh shebang
causes npm to generate Windows shims that try to invoke /bin/sh,
which doesn't exist on Windows.

This fix uses a hybrid approach:

1. Node.js wrapper (bin/agent-browser.js) as bin entry
   - Makes npx work on all platforms
   - ~100ms overhead (acceptable since npx has its own overhead)

2. postinstall patches bin entries for global installs
   - Windows: Overwrites .cmd/.ps1 shims to invoke .exe directly
   - Mac/Linux: Replaces symlink to point to native binary
   - Zero overhead for `npm i -g agent-browser` users on all platforms

Also adds cross-platform CI tests for npm global install to catch
regressions on all platforms (Ubuntu, macOS, Windows).

Fixes #262

* test global install

* remove dead code
This commit is contained in:
Chris Tate
2026-01-27 00:09:51 -06:00
committed by GitHub
parent 28950b8ad2
commit 18a1abda6e
6 changed files with 314 additions and 49 deletions
+103 -5
View File
@@ -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
-28
View File
@@ -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
-13
View File
@@ -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
+92
View File
@@ -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();
+1 -1
View File
@@ -11,7 +11,7 @@
"skills"
],
"bin": {
"agent-browser": "./bin/agent-browser"
"agent-browser": "./bin/agent-browser.js"
},
"scripts": {
"prepare": "husky",
+118 -2
View File
@@ -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);