Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions bin/agent-browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/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, accessSync, chmodSync, constants } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { platform, arch, homedir } from 'os';

const __dirname = dirname(fileURLToPath(import.meta.url));

// [Fix] Auto-start daemon on Windows if missing
async function ensureDaemonRunning() {
if (platform() !== 'win32') return;

const agentDir = join(homedir(), '.agent-browser');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port file path mismatch between daemon writer and wrapper reader causes daemon detection to always fail on Windows

Fix on Vercel

const portFile = join(agentDir, 'default.port');

if (existsSync(portFile)) return;

// console.log('[Auto-Fix] Starting agent-browser daemon...');
// The daemon is located in ../dist/daemon.js relative to this script
const daemonScript = join(__dirname, '../dist/daemon.js');

if (!existsSync(daemonScript)) {
// If dist doesn't exist (e.g. dev mode), try src (via tsx maybe?)
// but in production/installed package dist should exist.
return;
}

try {
const child = spawn(process.execPath, [daemonScript], {
detached: true,
stdio: 'ignore',
windowsHide: true
});
child.unref();

// Wait up to 3s for startup
const start = Date.now();
while (Date.now() - start < 3000) {
if (existsSync(portFile)) break;
await new Promise(r => setTimeout(r, 100));
}
} catch (e) {
// Ignore spawn errors, let the native binary try its own startup
}
}

// 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}`;
}

async function main() {
await ensureDaemonRunning();

const binaryName = getBinaryName();

if (!binaryName) {
console.error(`Error: Unsupported platform: ${platform()}-${arch()}`);
process.exit(1);
}

const binaryPath = join(__dirname, binaryName);

// If native binary is missing, we could try to run the Node.js version directly?
// But for now let's stick to the original logic which expects the binary.

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

// Ensure binary is executable (fixes EACCES on macOS/Linux when postinstall didn't run,
// e.g., when using bun which blocks lifecycle scripts by default)
if (platform() !== 'win32') {
try {
accessSync(binaryPath, constants.X_OK);
} catch {
// Binary exists but isn't executable - fix it
try {
chmodSync(binaryPath, 0o755);
} catch (chmodErr) {
console.error(`Error: Cannot make binary executable: ${chmodErr.message}`);
console.error('Try running: chmod +x ' + binaryPath);
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();
15 changes: 7 additions & 8 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ fn get_port_for_session(session: &str) -> u16 {
for c in session.chars() {
hash = ((hash << 5).wrapping_sub(hash)).wrapping_add(c as i32);
}
49152 + ((hash.abs() as u16) % 16383)
// Correct logic: first take absolute modulo, then cast to u16
// Using unsigned_abs() to safely handle i32::MIN
49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)
}

#[cfg(unix)]
Expand Down Expand Up @@ -227,13 +229,10 @@ pub fn ensure_daemon(
{
use std::os::windows::process::CommandExt;

// On Windows, use cmd.exe to run node to ensure proper PATH resolution.
// This handles cases where node.exe isn't directly in PATH but node.cmd is.
// Pass the entire command as a single string to /c to handle paths with spaces.
let cmd_string = format!("node \"{}\"", daemon_path.display());
let mut cmd = Command::new("cmd");
cmd.arg("/c")
.arg(&cmd_string)
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
// and automatically quotes arguments containing spaces.
let mut cmd = Command::new("node");
cmd.arg(daemon_path)
.env("AGENT_BROWSER_DAEMON", "1")
.env("AGENT_BROWSER_SESSION", session);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"skills"
],
"bin": {
"agent-browser": "./bin/agent-browser"
"agent-browser": "./bin/agent-browser.js"
},
"scripts": {
"prepare": "husky",
Expand Down