From 083a946aac7211a435d5e7cc01464e0a59e8de36 Mon Sep 17 00:00:00 2001 From: Oanakiaja <281723571@qq.com> Date: Thu, 22 Jan 2026 14:19:37 +0800 Subject: [PATCH] feat: add browser launch --args, --user-agent, --proxy-bypass configuration support. (#35) * feat: add browser launch args, user-agent, and proxy configuration support * fix: User Agent env need added * fix: command pass error --------- Co-authored-by: Chris Tate --- README.md | 4 + cli/src/commands.rs | 232 ++++++++++++------- cli/src/connection.rs | 44 +++- cli/src/flags.rs | 63 +++++- cli/src/main.rs | 67 ++++-- cli/src/output.rs | 280 +++++++++++++++-------- src/browser.ts | 8 +- src/daemon.ts | 25 +- src/protocol.ts | 2 + src/types.ts | 2 + test/e2e-launch-options.test.ts | 388 ++++++++++++++++++++++++++++++++ test/launch-options.test.ts | 158 +++++++++++++ 12 files changed, 1075 insertions(+), 198 deletions(-) create mode 100644 test/e2e-launch-options.test.ts create mode 100644 test/launch-options.test.ts diff --git a/README.md b/README.md index 414f229..3c1b134 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,10 @@ agent-browser snapshot -i -c -d 5 # Combine options | `--session ` | Use isolated session (or `AGENT_BROWSER_SESSION` env) | | `--headers ` | Set HTTP headers scoped to the URL's origin | | `--executable-path ` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) | +| `--args ` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) | +| `--user-agent ` | Custom User-Agent string (or `AGENT_BROWSER_USER_AGENT` env) | +| `--proxy ` | Proxy server URL with optional auth (or `AGENT_BROWSER_PROXY` env) | +| `--proxy-bypass ` | 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 | diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 4bb41e0..d7c41be 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -216,7 +216,10 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { let dir = rest.get(0).unwrap_or(&"down"); - let amount = rest.get(1).and_then(|s| s.parse::().ok()).unwrap_or(300); + let amount = rest + .get(1) + .and_then(|s| s.parse::().ok()) + .unwrap_or(300); Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount })) } "scrollintoview" | "scrollinto" => { @@ -231,45 +234,57 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { // Check for --url flag: wait --url "**/dashboard" if let Some(idx) = rest.iter().position(|&s| s == "--url" || s == "-u") { - let url = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { - context: "wait --url".to_string(), - usage: "wait --url ", - })?; + let url = rest + .get(idx + 1) + .ok_or_else(|| ParseError::MissingArguments { + context: "wait --url".to_string(), + usage: "wait --url ", + })?; return Ok(json!({ "id": id, "action": "waitforurl", "url": url })); } - + // Check for --load flag: wait --load networkidle if let Some(idx) = rest.iter().position(|&s| s == "--load" || s == "-l") { - let state = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { - context: "wait --load".to_string(), - usage: "wait --load ", - })?; + let state = rest + .get(idx + 1) + .ok_or_else(|| ParseError::MissingArguments { + context: "wait --load".to_string(), + usage: "wait --load ", + })?; return Ok(json!({ "id": id, "action": "waitforloadstate", "state": state })); } - + // Check for --fn flag: wait --fn "window.ready === true" if let Some(idx) = rest.iter().position(|&s| s == "--fn" || s == "-f") { - let expr = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { - context: "wait --fn".to_string(), - usage: "wait --fn ", - })?; + let expr = rest + .get(idx + 1) + .ok_or_else(|| ParseError::MissingArguments { + context: "wait --fn".to_string(), + usage: "wait --fn ", + })?; return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr })); } - + // Check for --text flag: wait --text "Welcome" if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") { - let text = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { - context: "wait --text".to_string(), - usage: "wait --text ", - })?; + let text = rest + .get(idx + 1) + .ok_or_else(|| ParseError::MissingArguments { + context: "wait --text".to_string(), + usage: "wait --text ", + })?; // Use getByText locator to wait for text to appear - return Ok(json!({ "id": id, "action": "wait", "selector": format!("text={}", text) })); + return Ok( + json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }), + ); } - + // Default: selector or timeout if let Some(arg) = rest.get(0) { if arg.parse::().is_ok() { - Ok(json!({ "id": id, "action": "wait", "timeout": arg.parse::().unwrap() })) + Ok( + json!({ "id": id, "action": "wait", "timeout": arg.parse::().unwrap() }), + ) } else { Ok(json!({ "id": id, "action": "wait", "selector": arg })) } @@ -384,7 +399,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result ", })?; - Ok(json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] })) + Ok( + json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] }), + ) } "clear" => Ok(json!({ "id": id, "action": "cookies_clear" })), _ => Ok(json!({ "id": id, "action": "cookies_get" })), @@ -414,7 +431,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "tab_list" })), } - } + }, // === Window === "window" => { @@ -456,7 +473,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "dialog", "response": "dismiss" })), Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, @@ -667,7 +683,7 @@ fn parse_get(rest: &[&str], id: &str) -> Result { fn parse_is(rest: &[&str], id: &str) -> Result { const VALID: &[&str] = &["visible", "enabled", "checked"]; - + match rest.get(0).map(|s| *s) { Some("visible") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { @@ -702,19 +718,31 @@ fn parse_is(rest: &[&str], id: &str) -> Result { } fn parse_find(rest: &[&str], id: &str) -> Result { - const VALID: &[&str] = &["role", "text", "label", "placeholder", "alt", "title", "testid", "first", "last", "nth"]; - + const VALID: &[&str] = &[ + "role", + "text", + "label", + "placeholder", + "alt", + "title", + "testid", + "first", + "last", + "nth", + ]; + let locator = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "find".to_string(), usage: "find [action] [text]", })?; - + let name_idx = rest.iter().position(|&s| s == "--name"); let name = name_idx.and_then(|i| rest.get(i + 1).map(|s| *s)); let exact = rest.iter().any(|&s| s == "--exact"); match *locator { - "role" | "text" | "label" | "placeholder" | "alt" | "title" | "testid" | "first" | "last" => { + "role" | "text" | "label" | "placeholder" | "alt" | "title" | "testid" | "first" + | "last" => { let value = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: format!("find {}", locator), usage: match *locator { @@ -779,10 +807,12 @@ fn parse_find(rest: &[&str], id: &str) -> Result { context: "find nth".to_string(), usage: "find nth [action] [text]", })?; - let idx = idx_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "find nth".to_string(), - usage: "find nth [action] [text]", - })?; + let idx = idx_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "find nth".to_string(), + usage: "find nth [action] [text]", + })?; let sel = rest.get(2).ok_or_else(|| ParseError::MissingArguments { context: "find nth".to_string(), usage: "find nth [action] [text]", @@ -806,7 +836,7 @@ fn parse_find(rest: &[&str], id: &str) -> Result { fn parse_mouse(rest: &[&str], id: &str) -> Result { const VALID: &[&str] = &["move", "down", "up", "wheel"]; - + match rest.get(0).map(|s| *s) { Some("move") => { let x_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments { @@ -817,14 +847,18 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result { context: "mouse move".to_string(), usage: "mouse move ", })?; - let x = x_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "mouse move".to_string(), - usage: "mouse move ", - })?; - let y = y_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "mouse move".to_string(), - usage: "mouse move ", - })?; + let x = x_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "mouse move".to_string(), + usage: "mouse move ", + })?; + let y = y_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "mouse move".to_string(), + usage: "mouse move ", + })?; Ok(json!({ "id": id, "action": "mousemove", "x": x, "y": y })) } Some("down") => { @@ -834,7 +868,10 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result { Ok(json!({ "id": id, "action": "mouseup", "button": rest.get(1).unwrap_or(&"left") })) } Some("wheel") => { - let dy = rest.get(1).and_then(|s| s.parse::().ok()).unwrap_or(100); + let dy = rest + .get(1) + .and_then(|s| s.parse::().ok()) + .unwrap_or(100); let dx = rest.get(2).and_then(|s| s.parse::().ok()).unwrap_or(0); Ok(json!({ "id": id, "action": "wheel", "deltaX": dx, "deltaY": dy })) } @@ -850,8 +887,18 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result { } fn parse_set(rest: &[&str], id: &str) -> Result { - const VALID: &[&str] = &["viewport", "device", "geo", "geolocation", "offline", "headers", "credentials", "auth", "media"]; - + const VALID: &[&str] = &[ + "viewport", + "device", + "geo", + "geolocation", + "offline", + "headers", + "credentials", + "auth", + "media", + ]; + match rest.get(0).map(|s| *s) { Some("viewport") => { let w_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments { @@ -862,14 +909,18 @@ fn parse_set(rest: &[&str], id: &str) -> Result { context: "set viewport".to_string(), usage: "set viewport ", })?; - let w = w_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "set viewport".to_string(), - usage: "set viewport ", - })?; - let h = h_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "set viewport".to_string(), - usage: "set viewport ", - })?; + let w = w_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "set viewport".to_string(), + usage: "set viewport ", + })?; + let h = h_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "set viewport".to_string(), + usage: "set viewport ", + })?; Ok(json!({ "id": id, "action": "viewport", "width": w, "height": h })) } Some("device") => { @@ -888,18 +939,25 @@ fn parse_set(rest: &[&str], id: &str) -> Result { context: "set geo".to_string(), usage: "set geo ", })?; - let lat = lat_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "set geo".to_string(), - usage: "set geo ", - })?; - let lng = lng_str.parse::().map_err(|_| ParseError::MissingArguments { - context: "set geo".to_string(), - usage: "set geo ", - })?; + let lat = lat_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "set geo".to_string(), + usage: "set geo ", + })?; + let lng = lng_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "set geo".to_string(), + usage: "set geo ", + })?; Ok(json!({ "id": id, "action": "geolocation", "latitude": lat, "longitude": lng })) } Some("offline") => { - let off = rest.get(1).map(|s| *s != "off" && *s != "false").unwrap_or(true); + let off = rest + .get(1) + .map(|s| *s != "off" && *s != "false") + .unwrap_or(true); Ok(json!({ "id": id, "action": "offline", "offline": off })) } Some("headers") => { @@ -908,8 +966,8 @@ fn parse_set(rest: &[&str], id: &str) -> Result { usage: "set headers ", })?; // Parse the JSON string into an object - let headers: serde_json::Value = serde_json::from_str(headers_json) - .map_err(|_| ParseError::MissingArguments { + let headers: serde_json::Value = + serde_json::from_str(headers_json).map_err(|_| ParseError::MissingArguments { context: "set headers".to_string(), usage: "set headers (must be valid JSON object)", })?; @@ -954,7 +1012,7 @@ fn parse_set(rest: &[&str], id: &str) -> Result { fn parse_network(rest: &[&str], id: &str) -> Result { const VALID: &[&str] = &["route", "unroute", "requests"]; - + match rest.get(0).map(|s| *s) { Some("route") => { let url = rest.get(1).ok_or_else(|| ParseError::MissingArguments { @@ -996,7 +1054,7 @@ fn parse_network(rest: &[&str], id: &str) -> Result { fn parse_storage(rest: &[&str], id: &str) -> Result { const VALID: &[&str] = &["local", "session"]; - + match rest.get(0).map(|s| *s) { Some("local") | Some("session") => { let storage_type = rest.get(0).unwrap(); @@ -1013,13 +1071,18 @@ fn parse_storage(rest: &[&str], id: &str) -> Result { context: format!("storage {} set", storage_type), usage: "storage set ", })?; - Ok(json!({ "id": id, "action": "storage_set", "type": storage_type, "key": k, "value": v })) + Ok( + json!({ "id": id, "action": "storage_set", "type": storage_type, "key": k, "value": v }), + ) } "clear" => Ok(json!({ "id": id, "action": "storage_clear", "type": storage_type })), _ => { - let mut cmd = json!({ "id": id, "action": "storage_get", "type": storage_type }); + let mut cmd = + json!({ "id": id, "action": "storage_get", "type": storage_type }); if let Some(k) = key { - cmd.as_object_mut().unwrap().insert("key".to_string(), json!(k)); + cmd.as_object_mut() + .unwrap() + .insert("key".to_string(), json!(k)); } Ok(cmd) } @@ -1052,6 +1115,9 @@ mod tests { extensions: Vec::new(), cdp: None, proxy: None, + proxy_bypass: None, + args: None, + user_agent: None, } } @@ -1120,7 +1186,8 @@ mod tests { #[test] fn test_storage_local_set() { - let cmd = parse_command(&args("storage local set mykey myvalue"), &default_flags()).unwrap(); + let cmd = + parse_command(&args("storage local set mykey myvalue"), &default_flags()).unwrap(); assert_eq!(cmd["action"], "storage_set"); assert_eq!(cmd["type"], "local"); assert_eq!(cmd["key"], "mykey"); @@ -1129,7 +1196,8 @@ mod tests { #[test] fn test_storage_session_set() { - let cmd = parse_command(&args("storage session set skey svalue"), &default_flags()).unwrap(); + let cmd = + parse_command(&args("storage session set skey svalue"), &default_flags()).unwrap(); assert_eq!(cmd["action"], "storage_set"); assert_eq!(cmd["type"], "session"); assert_eq!(cmd["key"], "skey"); @@ -1191,7 +1259,8 @@ mod tests { #[test] fn test_navigate_with_multiple_headers() { let mut flags = default_flags(); - flags.headers = Some(r#"{"Authorization": "Bearer token", "X-Custom": "value"}"#.to_string()); + flags.headers = + Some(r#"{"Authorization": "Bearer token", "X-Custom": "value"}"#.to_string()); let cmd = parse_command(&args("open api.example.com"), &flags).unwrap(); assert_eq!(cmd["headers"]["Authorization"], "Bearer token"); assert_eq!(cmd["headers"]["X-Custom"], "value"); @@ -1445,7 +1514,10 @@ mod tests { fn test_wait_load_missing_state() { let result = parse_command(&args("wait --load"), &default_flags()); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); + assert!(matches!( + result.unwrap_err(), + ParseError::MissingArguments { .. } + )); } #[test] @@ -1544,14 +1616,20 @@ mod tests { fn test_unknown_command() { let result = parse_command(&args("unknowncommand"), &default_flags()); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ParseError::UnknownCommand { .. })); + assert!(matches!( + result.unwrap_err(), + ParseError::UnknownCommand { .. } + )); } #[test] fn test_empty_args() { let result = parse_command(&[], &default_flags()); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); + assert!(matches!( + result.unwrap_err(), + ParseError::MissingArguments { .. } + )); } // === Error message tests === diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 1430061..e8dae1f 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -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 { if is_daemon_running(session) && daemon_ready(session) { return Ok(DaemonResult { @@ -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") @@ -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(|| { @@ -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()) @@ -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)); } diff --git a/cli/src/flags.rs b/cli/src/flags.rs index fd8b610..8bb63c2 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -11,6 +11,9 @@ pub struct Flags { pub cdp: Option, pub extensions: Vec, pub proxy: Option, + pub proxy_bypass: Option, + pub args: Option, + pub user_agent: Option, pub provider: Option, } @@ -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(), }; @@ -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()); @@ -97,7 +121,19 @@ pub fn clean_args(args: &[String]) -> Vec { // 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 { @@ -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] @@ -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())); } @@ -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())); } diff --git a/cli/src/main.rs b/cli/src/main.rs index 2940688..52f2b68 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -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) => { @@ -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(", ") + ); } } @@ -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 = 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) { diff --git a/cli/src/output.rs b/cli/src/output.rs index eebf98f..8fe472c 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -239,7 +239,8 @@ pub fn print_response(resp: &Response, json_mode: bool) { pub fn print_command_help(command: &str) -> bool { let help = match command { // === Navigation === - "open" | "goto" | "navigate" => r##" + "open" | "goto" | "navigate" => { + r##" agent-browser open - Navigate to a URL Usage: agent-browser open @@ -261,8 +262,10 @@ Examples: agent-browser open localhost:3000 agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}' # ^ Headers only sent to api.example.com, not other domains -"##, - "back" => r##" +"## + } + "back" => { + r##" agent-browser back - Navigate back in history Usage: agent-browser back @@ -276,8 +279,10 @@ Global Options: Examples: agent-browser back -"##, - "forward" => r##" +"## + } + "forward" => { + r##" agent-browser forward - Navigate forward in history Usage: agent-browser forward @@ -291,8 +296,10 @@ Global Options: Examples: agent-browser forward -"##, - "reload" => r##" +"## + } + "reload" => { + r##" agent-browser reload - Reload the current page Usage: agent-browser reload @@ -306,10 +313,12 @@ Global Options: Examples: agent-browser reload -"##, +"## + } // === Core Actions === - "click" => r##" + "click" => { + r##" agent-browser click - Click an element Usage: agent-browser click @@ -326,8 +335,10 @@ Examples: agent-browser click @e1 agent-browser click "button.primary" agent-browser click "//button[@type='submit']" -"##, - "dblclick" => r##" +"## + } + "dblclick" => { + r##" agent-browser dblclick - Double-click an element Usage: agent-browser dblclick @@ -342,8 +353,10 @@ Global Options: Examples: agent-browser dblclick "#editable-text" agent-browser dblclick @e5 -"##, - "fill" => r##" +"## + } + "fill" => { + r##" agent-browser fill - Clear and fill an input field Usage: agent-browser fill @@ -359,8 +372,10 @@ Examples: agent-browser fill "#email" "user@example.com" agent-browser fill @e3 "Hello World" agent-browser fill "input[name='search']" "query" -"##, - "type" => r##" +"## + } + "type" => { + r##" agent-browser type - Type text into an element Usage: agent-browser type @@ -375,8 +390,10 @@ Global Options: Examples: agent-browser type "#search" "hello" agent-browser type @e2 "additional text" -"##, - "hover" => r##" +"## + } + "hover" => { + r##" agent-browser hover - Hover over an element Usage: agent-browser hover @@ -391,8 +408,10 @@ Global Options: Examples: agent-browser hover "#dropdown-trigger" agent-browser hover @e4 -"##, - "focus" => r##" +"## + } + "focus" => { + r##" agent-browser focus - Focus an element Usage: agent-browser focus @@ -406,8 +425,10 @@ Global Options: Examples: agent-browser focus "#input-field" agent-browser focus @e2 -"##, - "check" => r##" +"## + } + "check" => { + r##" agent-browser check - Check a checkbox Usage: agent-browser check @@ -421,8 +442,10 @@ Global Options: Examples: agent-browser check "#terms-checkbox" agent-browser check @e7 -"##, - "uncheck" => r##" +"## + } + "uncheck" => { + r##" agent-browser uncheck - Uncheck a checkbox Usage: agent-browser uncheck @@ -436,8 +459,10 @@ Global Options: Examples: agent-browser uncheck "#newsletter-opt-in" agent-browser uncheck @e8 -"##, - "select" => r##" +"## + } + "select" => { + r##" agent-browser select - Select a dropdown option Usage: agent-browser select @@ -452,8 +477,10 @@ Examples: agent-browser select "#country" "US" agent-browser select @e5 "option2" agent-browser select "#menu" "opt1" "opt2" "opt3" -"##, - "drag" => r##" +"## + } + "drag" => { + r##" agent-browser drag - Drag and drop Usage: agent-browser drag @@ -467,8 +494,10 @@ Global Options: Examples: agent-browser drag "#draggable" "#drop-zone" agent-browser drag @e1 @e2 -"##, - "upload" => r##" +"## + } + "upload" => { + r##" agent-browser upload - Upload files Usage: agent-browser upload @@ -482,10 +511,12 @@ Global Options: Examples: agent-browser upload "#file-input" ./document.pdf agent-browser upload @e3 ./image1.png ./image2.png -"##, +"## + } // === Keyboard === - "press" | "key" => r##" + "press" | "key" => { + r##" agent-browser press - Press a key or key combination Usage: agent-browser press @@ -513,8 +544,10 @@ Examples: agent-browser press Control+a agent-browser press Control+Shift+s agent-browser press Escape -"##, - "keydown" => r##" +"## + } + "keydown" => { + r##" agent-browser keydown - Press a key down (without release) Usage: agent-browser keydown @@ -529,8 +562,10 @@ Global Options: Examples: agent-browser keydown Shift agent-browser keydown Control -"##, - "keyup" => r##" +"## + } + "keyup" => { + r##" agent-browser keyup - Release a key Usage: agent-browser keyup @@ -544,10 +579,12 @@ Global Options: Examples: agent-browser keyup Shift agent-browser keyup Control -"##, +"## + } // === Scroll === - "scroll" => r##" + "scroll" => { + r##" agent-browser scroll - Scroll the page Usage: agent-browser scroll [direction] [amount] @@ -567,8 +604,10 @@ Examples: agent-browser scroll down 500 agent-browser scroll up 200 agent-browser scroll left 100 -"##, - "scrollintoview" | "scrollinto" => r##" +"## + } + "scrollintoview" | "scrollinto" => { + r##" agent-browser scrollintoview - Scroll element into view Usage: agent-browser scrollintoview @@ -584,10 +623,12 @@ Global Options: Examples: agent-browser scrollintoview "#footer" agent-browser scrollintoview @e15 -"##, +"## + } // === Wait === - "wait" => r##" + "wait" => { + r##" agent-browser wait - Wait for condition Usage: agent-browser wait @@ -613,10 +654,12 @@ Examples: agent-browser wait --load networkidle agent-browser wait --fn "window.appReady === true" agent-browser wait --text "Welcome back" -"##, +"## + } // === Screenshot/PDF === - "screenshot" => r##" + "screenshot" => { + r##" agent-browser screenshot - Take a screenshot Usage: agent-browser screenshot [path] @@ -635,8 +678,10 @@ Examples: agent-browser screenshot agent-browser screenshot ./screenshot.png agent-browser screenshot --full ./full-page.png -"##, - "pdf" => r##" +"## + } + "pdf" => { + r##" agent-browser pdf - Save page as PDF Usage: agent-browser pdf @@ -650,10 +695,12 @@ Global Options: Examples: agent-browser pdf ./page.pdf agent-browser pdf ~/Documents/report.pdf -"##, +"## + } // === Snapshot === - "snapshot" => r##" + "snapshot" => { + r##" agent-browser snapshot - Get accessibility tree snapshot Usage: agent-browser snapshot [options] @@ -677,10 +724,12 @@ Examples: agent-browser snapshot -i agent-browser snapshot --compact --depth 5 agent-browser snapshot -s "#main-content" -"##, +"## + } // === Eval === - "eval" => r##" + "eval" => { + r##" agent-browser eval - Execute JavaScript Usage: agent-browser eval