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
+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);