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
106 changes: 80 additions & 26 deletions bin/agent-browser
Original file line number Diff line number Diff line change
@@ -1,26 +1,80 @@
#!/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" ] && [ -x "$BINARY" ]; then
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
#!/usr/bin/env node

async function main() {
const [{ accessSync, constants, existsSync, realpathSync }, { spawn }, { dirname, join }] =
await Promise.all([import('fs'), import('child_process'), import('path')]);

const scriptArg = process.argv[1];
if (!scriptArg) {
console.error('Error: Missing script path for agent-browser wrapper');
process.exit(1);
}

let scriptPath;
try {
scriptPath = realpathSync(scriptArg);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Failed to resolve script path: ${message}`);
process.exit(1);
}
Comment on lines +4 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const [{ accessSync, constants, existsSync, realpathSync }, { spawn }, { dirname, join }] =
await Promise.all([import('fs'), import('child_process'), import('path')]);
const scriptArg = process.argv[1];
if (!scriptArg) {
console.error('Error: Missing script path for agent-browser wrapper');
process.exit(1);
}
let scriptPath;
try {
scriptPath = realpathSync(scriptArg);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Failed to resolve script path: ${message}`);
process.exit(1);
}
const [{ accessSync, constants, existsSync, realpathSync }, { spawn }, { dirname, join }, { fileURLToPath }] =
await Promise.all([import('fs'), import('child_process'), import('path'), import('url')]);
const scriptPath = realpathSync(fileURLToPath(import.meta.url));

process.argv[1] is unreliable for determining script path when invoked through npm shims, should use import.meta.url instead

Fix on Vercel


const scriptDir = dirname(scriptPath);
const platform = process.platform;
const arch = process.arch;

const os =
platform === 'win32' ? 'win32' : platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : null;

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

const mappedArch = arch === 'x64' ? 'x64' : arch === 'arm64' ? 'arm64' : null;

if (!mappedArch) {
console.error(`Error: Unsupported architecture ${arch}`);
process.exit(1);
}

const ext = platform === 'win32' ? '.exe' : '';
const binaryName = `agent-browser-${os}-${mappedArch}${ext}`;
const binaryPath = join(scriptDir, binaryName);

try {
if (!existsSync(binaryPath)) {
throw new Error(`No binary found for ${os}-${mappedArch}`);
}
if (platform !== 'win32') {
accessSync(binaryPath, constants.X_OK);
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: ${message}`);
console.error("Run 'npm run build:native' to build for your platform");
process.exit(1);
}

const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });

child.on('error', (err) => {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Failed to launch ${binaryName}: ${message}`);
process.exit(1);
});

child.on('close', (code, signal) => {
if (typeof code === 'number') {
process.exit(code);
}
console.error(`Error: ${binaryName} exited with signal ${signal ?? 'unknown'}`);
process.exit(1);
});
}

main().catch((err) => {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Unexpected failure in CLI wrapper: ${message}`);
process.exit(1);
});
2 changes: 1 addition & 1 deletion bin/agent-browser.cmd
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@echo off
setlocal
set "SCRIPT_DIR=%~dp0"
node "%SCRIPT_DIR%..\dist\index.js" %*
node "%SCRIPT_DIR%agent-browser" %*
exit /b %errorlevel%
28 changes: 19 additions & 9 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,16 +256,26 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
}

// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
// CREATE_NEW_PROCESS_GROUP (keep console association for headed mode)
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008;

cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start daemon: {}", e))?;

if headed {
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start daemon: {}", e))?;
} else {
// DETACHED_PROCESS avoids flashing a console window in headless mode
const DETACHED_PROCESS: u32 = 0x00000008;
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start daemon: {}", e))?;
}
}

for _ in 0..50 {
Expand Down