Skip to content
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ agent-browser snapshot -i -c -d 5 # Combine options
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
| `--args <args>` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |
| `--user-agent <ua>` | Custom User-Agent string (or `AGENT_BROWSER_USER_AGENT` env) |
| `--proxy <url>` | Proxy server URL with optional auth (or `AGENT_BROWSER_PROXY` env) |
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (or `AGENT_BROWSER_PROXY_BYPASS` env) |
| `--json` | JSON output (for agents) |
| `--full, -f` | Full page screenshot |
| `--name, -n` | Locator name filter |
Expand Down
232 changes: 155 additions & 77 deletions cli/src/commands.rs

Large diffs are not rendered by default.

44 changes: 41 additions & 3 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ pub fn ensure_daemon(
headed: bool,
executable_path: Option<&str>,
extensions: &[String],
args: Option<&str>,
user_agent: Option<&str>,
proxy: Option<&str>,
proxy_bypass: Option<&str>,
) -> Result<DaemonResult, String> {
if is_daemon_running(session) && daemon_ready(session) {
return Ok(DaemonResult {
Expand Down Expand Up @@ -199,7 +203,7 @@ pub fn ensure_daemon(
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;

let mut cmd = Command::new("node");
cmd.arg(daemon_path)
.env("AGENT_BROWSER_DAEMON", "1")
Expand All @@ -217,6 +221,22 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
}

if let Some(a) = args {
cmd.env("AGENT_BROWSER_ARGS", a);
}

if let Some(ua) = user_agent {
cmd.env("AGENT_BROWSER_USER_AGENT", ua);
}

if let Some(p) = proxy {
cmd.env("AGENT_BROWSER_PROXY", p);
}

if let Some(pb) = proxy_bypass {
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
}

// Create new process group and session to fully detach
unsafe {
cmd.pre_exec(|| {
Expand Down Expand Up @@ -256,10 +276,26 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
}

if let Some(a) = args {
cmd.env("AGENT_BROWSER_ARGS", a);
}

if let Some(ua) = user_agent {
cmd.env("AGENT_BROWSER_USER_AGENT", ua);
}

if let Some(p) = proxy {
cmd.env("AGENT_BROWSER_PROXY", p);
}

if let Some(pb) = proxy_bypass {
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
}

// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
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())
Expand All @@ -270,7 +306,9 @@ pub fn ensure_daemon(

for _ in 0..50 {
if daemon_ready(session) {
return Ok(DaemonResult { already_running: false });
return Ok(DaemonResult {
already_running: false,
});
}
thread::sleep(Duration::from_millis(100));
}
Expand Down
63 changes: 55 additions & 8 deletions cli/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub struct Flags {
pub cdp: Option<String>,
pub extensions: Vec<String>,
pub proxy: Option<String>,
pub proxy_bypass: Option<String>,
pub args: Option<String>,
pub user_agent: Option<String>,
pub provider: Option<String>,
}

Expand All @@ -30,7 +33,10 @@ pub fn parse_flags(args: &[String]) -> Flags {
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
cdp: None,
extensions: extensions_env,
proxy: None,
proxy: env::var("AGENT_BROWSER_PROXY").ok(),
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok(),
args: env::var("AGENT_BROWSER_ARGS").ok(),
user_agent: env::var("AGENT_BROWSER_USER_AGENT").ok(),
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
};

Expand Down Expand Up @@ -77,6 +83,24 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--proxy-bypass" => {
if let Some(s) = args.get(i + 1) {
flags.proxy_bypass = Some(s.clone());
i += 1;
}
}
"--args" => {
if let Some(s) = args.get(i + 1) {
flags.args = Some(s.clone());
i += 1;
}
}
"--user-agent" => {
if let Some(s) = args.get(i + 1) {
flags.user_agent = Some(s.clone());
i += 1;
}
}
"-p" | "--provider" => {
if let Some(p) = args.get(i + 1) {
flags.provider = Some(p.clone());
Expand All @@ -97,7 +121,19 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
// Global flags that should be stripped from command args
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
// Global flags that take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--proxy", "-p", "--provider"];
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
"--session",
"--headers",
"--executable-path",
"--cdp",
"--extension",
"--proxy",
"--proxy-bypass",
"--args",
"--user-agent",
"-p",
"--provider",
];

for arg in args.iter() {
if skip_next {
Expand Down Expand Up @@ -141,7 +177,10 @@ mod tests {
r#"{"Authorization": "Bearer token"}"#.to_string(),
];
let flags = parse_flags(&input);
assert_eq!(flags.headers, Some(r#"{"Authorization": "Bearer token"}"#.to_string()));
assert_eq!(
flags.headers,
Some(r#"{"Authorization": "Bearer token"}"#.to_string())
);
}

#[test]
Expand Down Expand Up @@ -188,14 +227,16 @@ mod tests {
assert_eq!(flags.headers, Some(r#"{"Auth":"token"}"#.to_string()));
assert!(flags.json);
assert!(flags.headed);

let clean = clean_args(&input);
assert_eq!(clean, vec!["open", "example.com"]);
}

#[test]
fn test_parse_executable_path_flag() {
let flags = parse_flags(&args("--executable-path /path/to/chromium open example.com"));
let flags = parse_flags(&args(
"--executable-path /path/to/chromium open example.com",
));
assert_eq!(flags.executable_path, Some("/path/to/chromium".to_string()));
}

Expand All @@ -207,19 +248,25 @@ mod tests {

#[test]
fn test_clean_args_removes_executable_path() {
let cleaned = clean_args(&args("--executable-path /path/to/chromium open example.com"));
let cleaned = clean_args(&args(
"--executable-path /path/to/chromium open example.com",
));
assert_eq!(cleaned, vec!["open", "example.com"]);
}

#[test]
fn test_clean_args_removes_executable_path_with_other_flags() {
let cleaned = clean_args(&args("--json --executable-path /path/to/chromium --headed open example.com"));
let cleaned = clean_args(&args(
"--json --executable-path /path/to/chromium --headed open example.com",
));
assert_eq!(cleaned, vec!["open", "example.com"]);
}

#[test]
fn test_parse_flags_with_session_and_executable_path() {
let flags = parse_flags(&args("--session test --executable-path /custom/chrome open example.com"));
let flags = parse_flags(&args(
"--session test --executable-path /custom/chrome open example.com",
));
assert_eq!(flags.session, "test");
assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
}
Expand Down
67 changes: 51 additions & 16 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ fn main() {
flags.headed,
flags.executable_path.as_deref(),
&flags.extensions,
flags.args.as_deref(),
flags.user_agent.as_deref(),
flags.proxy.as_deref(),
flags.proxy_bypass.as_deref(),
) {
Ok(result) => result,
Err(e) => {
Expand All @@ -210,17 +214,27 @@ fn main() {
}
};

// Warn if executable_path was specified but daemon was already running
if daemon_result.already_running
&& (flags.executable_path.is_some() || !flags.extensions.is_empty())
{
if !flags.json {
if flags.executable_path.is_some() {
eprintln!("{} --executable-path ignored: daemon already running. Use 'agent-browser close' first to restart with new path.", color::warning_indicator());
}
if !flags.extensions.is_empty() {
eprintln!("{} --extension ignored: daemon already running. Use 'agent-browser close' first to restart with extensions.", color::warning_indicator());
}
// Warn if launch-time options were specified but daemon was already running
if daemon_result.already_running {
let has_extensions = !flags.extensions.is_empty();
let ignored_flags: Vec<&str> = [
flags.executable_path.as_ref().map(|_| "--executable-path"),
if has_extensions { Some("--extension") } else { None },
flags.args.as_ref().map(|_| "--args"),
flags.user_agent.as_ref().map(|_| "--user-agent"),
flags.proxy.as_ref().map(|_| "--proxy"),
flags.proxy_bypass.as_ref().map(|_| "--proxy-bypass"),
]
.into_iter()
.flatten()
.collect();

if !ignored_flags.is_empty() && !flags.json {
eprintln!(
"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.",
color::warning_indicator(),
ignored_flags.join(", ")
);
}
}

Expand Down Expand Up @@ -348,18 +362,39 @@ fn main() {
}

// Launch headed browser or proxy if flags are set (without CDP or provider)
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
if (flags.headed || flags.proxy.is_some() || flags.args.is_some() || flags.user_agent.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
let mut launch_cmd = json!({
"id": gen_id(),
"action": "launch",
"headless": !flags.headed
});

let cmd_obj = launch_cmd.as_object_mut()
.expect("json! macro guarantees object type");

if let Some(ref proxy_str) = flags.proxy {
let proxy_obj = parse_proxy(proxy_str);
launch_cmd.as_object_mut()
.expect("json! macro guarantees object type")
.insert("proxy".to_string(), proxy_obj);
let mut proxy_obj = parse_proxy(proxy_str);
// Add bypass if specified
if let Some(ref bypass) = flags.proxy_bypass {
if let Some(obj) = proxy_obj.as_object_mut() {
obj.insert("bypass".to_string(), json!(bypass));
}
}
cmd_obj.insert("proxy".to_string(), proxy_obj);
}

if let Some(ref ua) = flags.user_agent {
cmd_obj.insert("userAgent".to_string(), json!(ua));
}

if let Some(ref a) = flags.args {
// Parse args (comma or newline separated)
let args_vec: Vec<String> = a
.split(&[',', '\n'][..])
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
cmd_obj.insert("args".to_string(), json!(args_vec));
}

if let Err(e) = send_command(launch_cmd, &flags.session) {
Expand Down
Loading