Fix: CLI: state load / profile persistence not usable in v0.7.6 (#268)

* Fix: CLI: state load / profile persistence not usable in v0.7.6

This PR addresses issue #259

* Fix issues identified in code review
This commit is contained in:
Chris Tate
2026-01-25 13:45:17 -06:00
committed by GitHub
parent ae09fdd431
commit 79863a5180
10 changed files with 439 additions and 134 deletions
+181 -78
View File
@@ -18,7 +18,10 @@ pub enum ParseError {
usage: &'static str, usage: &'static str,
}, },
/// Argument exists but has an invalid value /// Argument exists but has an invalid value
InvalidValue { message: String, usage: &'static str }, InvalidValue {
message: String,
usage: &'static str,
},
} }
impl ParseError { impl ParseError {
@@ -81,11 +84,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
usage: "open <url>", usage: "open <url>",
})?; })?;
let url_lower = url.to_lowercase(); let url_lower = url.to_lowercase();
let url = if url_lower.starts_with("http://") let url = if url_lower.starts_with("http://")
|| url_lower.starts_with("https://") || url_lower.starts_with("https://")
|| url_lower.starts_with("about:") || url_lower.starts_with("about:")
|| url_lower.starts_with("data:") || url_lower.starts_with("data:")
|| url_lower.starts_with("file:") { || url_lower.starts_with("file:")
{
url.to_string() url.to_string()
} else { } else {
format!("https://{}", url) format!("https://{}", url)
@@ -299,7 +303,10 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
if rest.iter().any(|&s| s == "--download" || s == "-d") { if rest.iter().any(|&s| s == "--download" || s == "-d") {
let mut cmd = json!({ "id": id, "action": "waitfordownload" }); let mut cmd = json!({ "id": id, "action": "waitfordownload" });
// Check for optional path (first non-flag argument after --download) // Check for optional path (first non-flag argument after --download)
let download_idx = rest.iter().position(|&s| s == "--download" || s == "-d").unwrap(); let download_idx = rest
.iter()
.position(|&s| s == "--download" || s == "-d")
.unwrap();
if let Some(path) = rest.get(download_idx + 1) { if let Some(path) = rest.get(download_idx + 1) {
if !path.starts_with("--") { if !path.starts_with("--") {
cmd["path"] = json!(path); cmd["path"] = json!(path);
@@ -347,7 +354,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
// One arg: determine if it's a selector or a path // One arg: determine if it's a selector or a path
let is_relative_path = first.starts_with("./") || first.starts_with("../"); let is_relative_path = first.starts_with("./") || first.starts_with("../");
let is_selector = !is_relative_path let is_selector = !is_relative_path
&& (first.starts_with('.') || first.starts_with('#') || first.starts_with('@')); && (first.starts_with('.')
|| first.starts_with('#')
|| first.starts_with('@'));
let has_path_extension = first.ends_with(".png") let has_path_extension = first.ends_with(".png")
|| first.ends_with(".jpg") || first.ends_with(".jpg")
|| first.ends_with(".jpeg") || first.ends_with(".jpeg")
@@ -361,7 +370,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
} }
_ => (None, None), _ => (None, None),
}; };
Ok(json!({ "id": id, "action": "screenshot", "path": path, "selector": selector, "fullPage": flags.full })) Ok(
json!({ "id": id, "action": "screenshot", "path": path, "selector": selector, "fullPage": flags.full }),
)
} }
"pdf" => { "pdf" => {
let path = rest.get(0).ok_or_else(|| ParseError::MissingArguments { let path = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
@@ -542,7 +553,10 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
"--sameSite" => { "--sameSite" => {
if let Some(same_site) = rest.get(i + 1) { if let Some(same_site) = rest.get(i + 1) {
// Validate sameSite value // Validate sameSite value
if *same_site == "Strict" || *same_site == "Lax" || *same_site == "None" { if *same_site == "Strict"
|| *same_site == "Lax"
|| *same_site == "None"
{
cookie["sameSite"] = json!(same_site); cookie["sameSite"] = json!(same_site);
i += 2; i += 2;
} else { } else {
@@ -583,9 +597,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
} }
} }
Ok( Ok(json!({ "id": id, "action": "cookies_set", "cookies": [cookie] }))
json!({ "id": id, "action": "cookies_set", "cookies": [cookie] }),
)
} }
"clear" => Ok(json!({ "id": id, "action": "cookies_clear" })), "clear" => Ok(json!({ "id": id, "action": "cookies_clear" })),
_ => Ok(json!({ "id": id, "action": "cookies_get" })), _ => Ok(json!({ "id": id, "action": "cookies_get" })),
@@ -593,28 +605,26 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
} }
// === Tabs === // === Tabs ===
"tab" => { "tab" => match rest.get(0).map(|s| *s) {
match rest.get(0).map(|s| *s) { Some("new") => {
Some("new") => { let mut cmd = json!({ "id": id, "action": "tab_new" });
let mut cmd = json!({ "id": id, "action": "tab_new" }); if let Some(url) = rest.get(1) {
if let Some(url) = rest.get(1) { cmd["url"] = json!(url);
cmd["url"] = json!(url);
}
Ok(cmd)
} }
Some("list") => Ok(json!({ "id": id, "action": "tab_list" })), Ok(cmd)
Some("close") => {
let mut cmd = json!({ "id": id, "action": "tab_close" });
if let Some(index) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) {
cmd["index"] = json!(index);
}
Ok(cmd)
}
Some(n) if n.parse::<i32>().is_ok() => {
Ok(json!({ "id": id, "action": "tab_switch", "index": n.parse::<i32>().unwrap() }))
}
_ => Ok(json!({ "id": id, "action": "tab_list" })),
} }
Some("list") => Ok(json!({ "id": id, "action": "tab_list" })),
Some("close") => {
let mut cmd = json!({ "id": id, "action": "tab_close" });
if let Some(index) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) {
cmd["index"] = json!(index);
}
Ok(cmd)
}
Some(n) if n.parse::<i32>().is_ok() => {
Ok(json!({ "id": id, "action": "tab_switch", "index": n.parse::<i32>().unwrap() }))
}
_ => Ok(json!({ "id": id, "action": "tab_list" })),
}, },
// === Window === // === Window ===
@@ -679,7 +689,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
usage: "trace stop <path>", usage: "trace stop <path>",
})?; })?;
Ok(json!({ "id": id, "action": "trace_stop", "path": path })) Ok(json!({ "id": id, "action": "trace_stop", "path": path }))
}, }
Some(sub) => Err(ParseError::UnknownSubcommand { Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(), subcommand: sub.to_string(),
valid_options: VALID, valid_options: VALID,
@@ -796,8 +806,10 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
} }
fn parse_get(rest: &[&str], id: &str) -> Result<Value, ParseError> { fn parse_get(rest: &[&str], id: &str) -> Result<Value, ParseError> {
const VALID: &[&str] = &["text", "html", "value", "attr", "url", "title", "count", "box", "styles"]; const VALID: &[&str] = &[
"text", "html", "value", "attr", "url", "title", "count", "box", "styles",
];
match rest.get(0).map(|s| *s) { match rest.get(0).map(|s| *s) {
Some("text") => { Some("text") => {
let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
@@ -952,35 +964,53 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
match *locator { match *locator {
"role" => { "role" => {
let mut cmd = json!({ "id": id, "action": "getbyrole", "role": value, "subaction": subaction, "name": name, "exact": exact }); let mut cmd = json!({ "id": id, "action": "getbyrole", "role": value, "subaction": subaction, "name": name, "exact": exact });
if let Some(v) = fill_value { cmd["value"] = json!(v); } if let Some(v) = fill_value {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
"text" => Ok(json!({ "id": id, "action": "getbytext", "text": value, "subaction": subaction, "exact": exact })), "text" => Ok(
json!({ "id": id, "action": "getbytext", "text": value, "subaction": subaction, "exact": exact }),
),
"label" => { "label" => {
let mut cmd = json!({ "id": id, "action": "getbylabel", "label": value, "subaction": subaction, "exact": exact }); let mut cmd = json!({ "id": id, "action": "getbylabel", "label": value, "subaction": subaction, "exact": exact });
if let Some(v) = fill_value { cmd["value"] = json!(v); } if let Some(v) = fill_value {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
"placeholder" => { "placeholder" => {
let mut cmd = json!({ "id": id, "action": "getbyplaceholder", "placeholder": value, "subaction": subaction, "exact": exact }); let mut cmd = json!({ "id": id, "action": "getbyplaceholder", "placeholder": value, "subaction": subaction, "exact": exact });
if let Some(v) = fill_value { cmd["value"] = json!(v); } if let Some(v) = fill_value {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
"alt" => Ok(json!({ "id": id, "action": "getbyalttext", "text": value, "subaction": subaction, "exact": exact })), "alt" => Ok(
"title" => Ok(json!({ "id": id, "action": "getbytitle", "text": value, "subaction": subaction, "exact": exact })), json!({ "id": id, "action": "getbyalttext", "text": value, "subaction": subaction, "exact": exact }),
),
"title" => Ok(
json!({ "id": id, "action": "getbytitle", "text": value, "subaction": subaction, "exact": exact }),
),
"testid" => { "testid" => {
let mut cmd = json!({ "id": id, "action": "getbytestid", "testId": value, "subaction": subaction }); let mut cmd = json!({ "id": id, "action": "getbytestid", "testId": value, "subaction": subaction });
if let Some(v) = fill_value { cmd["value"] = json!(v); } if let Some(v) = fill_value {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
"first" => { "first" => {
let mut cmd = json!({ "id": id, "action": "nth", "selector": value, "index": 0, "subaction": subaction }); let mut cmd = json!({ "id": id, "action": "nth", "selector": value, "index": 0, "subaction": subaction });
if let Some(v) = fill_value { cmd["value"] = json!(v); } if let Some(v) = fill_value {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
"last" => { "last" => {
let mut cmd = json!({ "id": id, "action": "nth", "selector": value, "index": -1, "subaction": subaction }); let mut cmd = json!({ "id": id, "action": "nth", "selector": value, "index": -1, "subaction": subaction });
if let Some(v) = fill_value { cmd["value"] = json!(v); } if let Some(v) = fill_value {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
_ => unreachable!(), _ => unreachable!(),
@@ -1008,7 +1038,9 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
None None
}; };
let mut cmd = json!({ "id": id, "action": "nth", "selector": sel, "index": idx, "subaction": sub }); let mut cmd = json!({ "id": id, "action": "nth", "selector": sel, "index": idx, "subaction": sub });
if let Some(v) = fv { cmd["value"] = json!(v); } if let Some(v) = fv {
cmd["value"] = json!(v);
}
Ok(cmd) Ok(cmd)
} }
_ => Err(ParseError::UnknownSubcommand { _ => Err(ParseError::UnknownSubcommand {
@@ -1181,7 +1213,9 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
} else { } else {
"no-preference" "no-preference"
}; };
Ok(json!({ "id": id, "action": "emulatemedia", "colorScheme": color, "reducedMotion": reduced })) Ok(
json!({ "id": id, "action": "emulatemedia", "colorScheme": color, "reducedMotion": reduced }),
)
} }
Some(sub) => Err(ParseError::UnknownSubcommand { Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(), subcommand: sub.to_string(),
@@ -1214,7 +1248,7 @@ fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
cmd["url"] = json!(url); cmd["url"] = json!(url);
} }
Ok(cmd) Ok(cmd)
}, }
Some("requests") => { Some("requests") => {
let clear = rest.iter().any(|&s| s == "--clear"); let clear = rest.iter().any(|&s| s == "--clear");
let filter_idx = rest.iter().position(|&s| s == "--filter"); let filter_idx = rest.iter().position(|&s| s == "--filter");
@@ -1299,6 +1333,7 @@ mod tests {
extensions: Vec::new(), extensions: Vec::new(),
cdp: None, cdp: None,
profile: None, profile: None,
state: None,
proxy: None, proxy: None,
proxy_bypass: None, proxy_bypass: None,
args: None, args: None,
@@ -1348,7 +1383,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_url() { fn test_cookies_set_with_url() {
let cmd = parse_command(&args("cookies set mycookie myvalue --url https://example.com"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --url https://example.com"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1357,7 +1396,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_domain() { fn test_cookies_set_with_domain() {
let cmd = parse_command(&args("cookies set mycookie myvalue --domain example.com"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --domain example.com"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1366,7 +1409,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_path() { fn test_cookies_set_with_path() {
let cmd = parse_command(&args("cookies set mycookie myvalue --path /api"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --path /api"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1375,7 +1422,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_httponly() { fn test_cookies_set_with_httponly() {
let cmd = parse_command(&args("cookies set mycookie myvalue --httpOnly"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --httpOnly"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1384,7 +1435,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_secure() { fn test_cookies_set_with_secure() {
let cmd = parse_command(&args("cookies set mycookie myvalue --secure"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --secure"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1393,7 +1448,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_samesite() { fn test_cookies_set_with_samesite() {
let cmd = parse_command(&args("cookies set mycookie myvalue --sameSite Strict"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --sameSite Strict"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1402,7 +1461,11 @@ mod tests {
#[test] #[test]
fn test_cookies_set_with_expires() { fn test_cookies_set_with_expires() {
let cmd = parse_command(&args("cookies set mycookie myvalue --expires 1234567890"), &default_flags()).unwrap(); let cmd = parse_command(
&args("cookies set mycookie myvalue --expires 1234567890"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "cookies_set"); assert_eq!(cmd["action"], "cookies_set");
assert_eq!(cmd["cookies"][0]["name"], "mycookie"); assert_eq!(cmd["cookies"][0]["name"], "mycookie");
assert_eq!(cmd["cookies"][0]["value"], "myvalue"); assert_eq!(cmd["cookies"][0]["value"], "myvalue");
@@ -1438,7 +1501,10 @@ mod tests {
#[test] #[test]
fn test_cookies_set_invalid_samesite() { fn test_cookies_set_invalid_samesite() {
let result = parse_command(&args("cookies set mycookie myvalue --sameSite Invalid"), &default_flags()); let result = parse_command(
&args("cookies set mycookie myvalue --sameSite Invalid"),
&default_flags(),
);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -1658,11 +1724,7 @@ mod tests {
#[test] #[test]
fn test_select_multiple_values() { fn test_select_multiple_values() {
let cmd = parse_command( let cmd = parse_command(&args("select #menu opt1 opt2 opt3"), &default_flags()).unwrap();
&args("select #menu opt1 opt2 opt3"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "select"); assert_eq!(cmd["action"], "select");
assert_eq!(cmd["selector"], "#menu"); assert_eq!(cmd["selector"], "#menu");
assert_eq!(cmd["values"], json!(["opt1", "opt2", "opt3"])); assert_eq!(cmd["values"], json!(["opt1", "opt2", "opt3"]));
@@ -1680,7 +1742,10 @@ mod tests {
fn test_tab_new() { fn test_tab_new() {
let cmd = parse_command(&args("tab new"), &default_flags()).unwrap(); let cmd = parse_command(&args("tab new"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "tab_new"); assert_eq!(cmd["action"], "tab_new");
assert!(cmd.get("url").is_none(), "url should not be present when not provided"); assert!(
cmd.get("url").is_none(),
"url should not be present when not provided"
);
} }
#[test] #[test]
@@ -1872,7 +1937,11 @@ mod tests {
#[test] #[test]
fn test_record_start_with_url() { fn test_record_start_with_url() {
let cmd = parse_command(&args("record start demo.webm https://example.com"), &default_flags()).unwrap(); let cmd = parse_command(
&args("record start demo.webm https://example.com"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "recording_start"); assert_eq!(cmd["action"], "recording_start");
assert_eq!(cmd["path"], "demo.webm"); assert_eq!(cmd["path"], "demo.webm");
assert_eq!(cmd["url"], "https://example.com"); assert_eq!(cmd["url"], "https://example.com");
@@ -1880,7 +1949,11 @@ mod tests {
#[test] #[test]
fn test_record_start_with_url_no_protocol() { fn test_record_start_with_url_no_protocol() {
let cmd = parse_command(&args("record start demo.webm example.com"), &default_flags()).unwrap(); let cmd = parse_command(
&args("record start demo.webm example.com"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "recording_start"); assert_eq!(cmd["action"], "recording_start");
assert_eq!(cmd["path"], "demo.webm"); assert_eq!(cmd["path"], "demo.webm");
assert_eq!(cmd["url"], "https://example.com"); assert_eq!(cmd["url"], "https://example.com");
@@ -1890,7 +1963,10 @@ mod tests {
fn test_record_start_missing_path() { fn test_record_start_missing_path() {
let result = parse_command(&args("record start"), &default_flags()); let result = parse_command(&args("record start"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
} }
#[test] #[test]
@@ -1909,7 +1985,11 @@ mod tests {
#[test] #[test]
fn test_record_restart_with_url() { fn test_record_restart_with_url() {
let cmd = parse_command(&args("record restart demo.webm https://example.com"), &default_flags()).unwrap(); let cmd = parse_command(
&args("record restart demo.webm https://example.com"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "recording_restart"); assert_eq!(cmd["action"], "recording_restart");
assert_eq!(cmd["path"], "demo.webm"); assert_eq!(cmd["path"], "demo.webm");
assert_eq!(cmd["url"], "https://example.com"); assert_eq!(cmd["url"], "https://example.com");
@@ -1919,21 +1999,30 @@ mod tests {
fn test_record_restart_missing_path() { fn test_record_restart_missing_path() {
let result = parse_command(&args("record restart"), &default_flags()); let result = parse_command(&args("record restart"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
} }
#[test] #[test]
fn test_record_invalid_subcommand() { fn test_record_invalid_subcommand() {
let result = parse_command(&args("record foo"), &default_flags()); let result = parse_command(&args("record foo"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::UnknownSubcommand { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::UnknownSubcommand { .. }
));
} }
#[test] #[test]
fn test_record_missing_subcommand() { fn test_record_missing_subcommand() {
let result = parse_command(&args("record"), &default_flags()); let result = parse_command(&args("record"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
} }
#[test] #[test]
@@ -2058,14 +2147,20 @@ mod tests {
fn test_download_missing_path() { fn test_download_missing_path() {
let result = parse_command(&args("download #btn"), &default_flags()); let result = parse_command(&args("download #btn"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
} }
#[test] #[test]
fn test_download_missing_selector() { fn test_download_missing_selector() {
let result = parse_command(&args("download"), &default_flags()); let result = parse_command(&args("download"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
} }
// === Wait for Download Tests === // === Wait for Download Tests ===
@@ -2086,14 +2181,19 @@ mod tests {
#[test] #[test]
fn test_wait_download_with_timeout() { fn test_wait_download_with_timeout() {
let cmd = parse_command(&args("wait --download --timeout 30000"), &default_flags()).unwrap(); let cmd =
parse_command(&args("wait --download --timeout 30000"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "waitfordownload"); assert_eq!(cmd["action"], "waitfordownload");
assert_eq!(cmd["timeout"], 30000); assert_eq!(cmd["timeout"], 30000);
} }
#[test] #[test]
fn test_wait_download_with_path_and_timeout() { fn test_wait_download_with_path_and_timeout() {
let cmd = parse_command(&args("wait --download ./file.pdf --timeout 30000"), &default_flags()).unwrap(); let cmd = parse_command(
&args("wait --download ./file.pdf --timeout 30000"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "waitfordownload"); assert_eq!(cmd["action"], "waitfordownload");
assert_eq!(cmd["path"], "./file.pdf"); assert_eq!(cmd["path"], "./file.pdf");
assert_eq!(cmd["timeout"], 30000); assert_eq!(cmd["timeout"], 30000);
@@ -2136,16 +2236,16 @@ mod tests {
]; ];
let cmd = parse_command(&input, &default_flags()).unwrap(); let cmd = parse_command(&input, &default_flags()).unwrap();
assert_eq!(cmd["action"], "launch"); assert_eq!(cmd["action"], "launch");
assert_eq!(cmd["cdpUrl"], "wss://remote-browser.example.com/cdp?token=xyz"); assert_eq!(
cmd["cdpUrl"],
"wss://remote-browser.example.com/cdp?token=xyz"
);
assert!(cmd.get("cdpPort").is_none()); assert!(cmd.get("cdpPort").is_none());
} }
#[test] #[test]
fn test_connect_with_http_url() { fn test_connect_with_http_url() {
let input: Vec<String> = vec![ let input: Vec<String> = vec!["connect".to_string(), "http://localhost:9222".to_string()];
"connect".to_string(),
"http://localhost:9222".to_string(),
];
let cmd = parse_command(&input, &default_flags()).unwrap(); let cmd = parse_command(&input, &default_flags()).unwrap();
assert_eq!(cmd["action"], "launch"); assert_eq!(cmd["action"], "launch");
assert_eq!(cmd["cdpUrl"], "http://localhost:9222"); assert_eq!(cmd["cdpUrl"], "http://localhost:9222");
@@ -2156,7 +2256,10 @@ mod tests {
fn test_connect_missing_argument() { fn test_connect_missing_argument() {
let result = parse_command(&args("connect"), &default_flags()); let result = parse_command(&args("connect"), &default_flags());
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
} }
#[test] #[test]
+35 -7
View File
@@ -195,6 +195,8 @@ pub fn ensure_daemon(
proxy: Option<&str>, proxy: Option<&str>,
proxy_bypass: Option<&str>, proxy_bypass: Option<&str>,
ignore_https_errors: bool, ignore_https_errors: bool,
profile: Option<&str>,
state: Option<&str>,
) -> Result<DaemonResult, String> { ) -> Result<DaemonResult, String> {
if is_daemon_running(session) && daemon_ready(session) { if is_daemon_running(session) && daemon_ready(session) {
return Ok(DaemonResult { return Ok(DaemonResult {
@@ -205,7 +207,8 @@ pub fn ensure_daemon(
// Ensure socket directory exists // Ensure socket directory exists
let socket_dir = get_socket_dir(); let socket_dir = get_socket_dir();
if !socket_dir.exists() { if !socket_dir.exists() {
fs::create_dir_all(&socket_dir).map_err(|e| format!("Failed to create socket directory: {}", e))?; fs::create_dir_all(&socket_dir)
.map_err(|e| format!("Failed to create socket directory: {}", e))?;
} }
let exe_path = env::current_exe().map_err(|e| e.to_string())?; let exe_path = env::current_exe().map_err(|e| e.to_string())?;
@@ -271,6 +274,14 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1"); cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1");
} }
if let Some(prof) = profile {
cmd.env("AGENT_BROWSER_PROFILE", prof);
}
if let Some(st) = state {
cmd.env("AGENT_BROWSER_STATE", st);
}
// Create new process group and session to fully detach // Create new process group and session to fully detach
unsafe { unsafe {
cmd.pre_exec(|| { cmd.pre_exec(|| {
@@ -290,7 +301,7 @@ pub fn ensure_daemon(
#[cfg(windows)] #[cfg(windows)]
{ {
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd) // On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
// and automatically quotes arguments containing spaces. // and automatically quotes arguments containing spaces.
let mut cmd = Command::new("node"); let mut cmd = Command::new("node");
@@ -326,10 +337,18 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb); cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
} }
if ignore_https_errors { if ignore_https_errors {
cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1"); cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1");
} }
if let Some(prof) = profile {
cmd.env("AGENT_BROWSER_PROFILE", prof);
}
if let Some(st) = state {
cmd.env("AGENT_BROWSER_STATE", st);
}
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008; const DETACHED_PROCESS: u32 = 0x00000008;
@@ -446,7 +465,9 @@ mod tests {
env::set_var("AGENT_BROWSER_SOCKET_DIR", ""); env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
env::remove_var("XDG_RUNTIME_DIR"); env::remove_var("XDG_RUNTIME_DIR");
assert!(get_socket_dir().to_string_lossy().ends_with(".agent-browser")); assert!(get_socket_dir()
.to_string_lossy()
.ends_with(".agent-browser"));
} }
#[test] #[test]
@@ -456,7 +477,10 @@ mod tests {
env::remove_var("AGENT_BROWSER_SOCKET_DIR"); env::remove_var("AGENT_BROWSER_SOCKET_DIR");
env::set_var("XDG_RUNTIME_DIR", "/run/user/1000"); env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
assert_eq!(get_socket_dir(), PathBuf::from("/run/user/1000/agent-browser")); assert_eq!(
get_socket_dir(),
PathBuf::from("/run/user/1000/agent-browser")
);
} }
#[test] #[test]
@@ -466,7 +490,9 @@ mod tests {
env::set_var("AGENT_BROWSER_SOCKET_DIR", ""); env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
env::set_var("XDG_RUNTIME_DIR", ""); env::set_var("XDG_RUNTIME_DIR", "");
assert!(get_socket_dir().to_string_lossy().ends_with(".agent-browser")); assert!(get_socket_dir()
.to_string_lossy()
.ends_with(".agent-browser"));
} }
#[test] #[test]
@@ -478,6 +504,8 @@ mod tests {
let result = get_socket_dir(); let result = get_socket_dir();
assert!(result.to_string_lossy().ends_with(".agent-browser")); assert!(result.to_string_lossy().ends_with(".agent-browser"));
assert!(result.to_string_lossy().contains("home") || result.to_string_lossy().contains("Users")); assert!(
result.to_string_lossy().contains("home") || result.to_string_lossy().contains("Users")
);
} }
} }
+24 -4
View File
@@ -11,6 +11,7 @@ pub struct Flags {
pub cdp: Option<String>, pub cdp: Option<String>,
pub extensions: Vec<String>, pub extensions: Vec<String>,
pub profile: Option<String>, pub profile: Option<String>,
pub state: Option<String>,
pub proxy: Option<String>, pub proxy: Option<String>,
pub proxy_bypass: Option<String>, pub proxy_bypass: Option<String>,
pub args: Option<String>, pub args: Option<String>,
@@ -22,7 +23,12 @@ pub struct Flags {
pub fn parse_flags(args: &[String]) -> Flags { pub fn parse_flags(args: &[String]) -> Flags {
let extensions_env = env::var("AGENT_BROWSER_EXTENSIONS") let extensions_env = env::var("AGENT_BROWSER_EXTENSIONS")
.ok() .ok()
.map(|s| s.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect::<Vec<_>>()) .map(|s| {
s.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
})
.unwrap_or_default(); .unwrap_or_default();
let mut flags = Flags { let mut flags = Flags {
@@ -36,6 +42,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
cdp: None, cdp: None,
extensions: extensions_env, extensions: extensions_env,
profile: env::var("AGENT_BROWSER_PROFILE").ok(), profile: env::var("AGENT_BROWSER_PROFILE").ok(),
state: env::var("AGENT_BROWSER_STATE").ok(),
proxy: env::var("AGENT_BROWSER_PROXY").ok(), proxy: env::var("AGENT_BROWSER_PROXY").ok(),
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok(), proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok(),
args: env::var("AGENT_BROWSER_ARGS").ok(), args: env::var("AGENT_BROWSER_ARGS").ok(),
@@ -68,13 +75,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
flags.executable_path = Some(s.clone()); flags.executable_path = Some(s.clone());
i += 1; i += 1;
} }
}, }
"--extension" => { "--extension" => {
if let Some(s) = args.get(i + 1) { if let Some(s) = args.get(i + 1) {
flags.extensions.push(s.clone()); flags.extensions.push(s.clone());
i += 1; i += 1;
} }
}, }
"--cdp" => { "--cdp" => {
if let Some(s) = args.get(i + 1) { if let Some(s) = args.get(i + 1) {
flags.cdp = Some(s.clone()); flags.cdp = Some(s.clone());
@@ -87,6 +94,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1; i += 1;
} }
} }
"--state" => {
if let Some(s) = args.get(i + 1) {
flags.state = Some(s.clone());
i += 1;
}
}
"--proxy" => { "--proxy" => {
if let Some(p) = args.get(i + 1) { if let Some(p) = args.get(i + 1) {
flags.proxy = Some(p.clone()); flags.proxy = Some(p.clone());
@@ -130,7 +143,13 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
let mut skip_next = false; let mut skip_next = false;
// Global flags that should be stripped from command args // Global flags that should be stripped from command args
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug", "--ignore-https-errors"]; const GLOBAL_FLAGS: &[&str] = &[
"--json",
"--full",
"--headed",
"--debug",
"--ignore-https-errors",
];
// Global flags that take a value (need to skip the next arg too) // Global flags that take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
"--session", "--session",
@@ -139,6 +158,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--cdp", "--cdp",
"--extension", "--extension",
"--profile", "--profile",
"--state",
"--proxy", "--proxy",
"--proxy-bypass", "--proxy-bypass",
"--args", "--args",
+22 -7
View File
@@ -100,7 +100,10 @@ pub fn run_install(with_deps: bool) {
], ],
) )
} else { } else {
eprintln!("{} No supported package manager found (apt-get, dnf, or yum)", color::error_indicator()); eprintln!(
"{} No supported package manager found (apt-get, dnf, or yum)",
color::error_indicator()
);
exit(1); exit(1);
}; };
@@ -128,7 +131,10 @@ pub fn run_install(with_deps: bool) {
Err(e) => eprintln!("{} Could not run install command: {}", color::warning_indicator(), e), Err(e) => eprintln!("{} Could not run install command: {}", color::warning_indicator(), e),
} }
} else { } else {
println!("{} Linux detected. If browser fails to launch, run:", color::warning_indicator()); println!(
"{} Linux detected. If browser fails to launch, run:",
color::warning_indicator()
);
println!(" agent-browser install --with-deps"); println!(" agent-browser install --with-deps");
println!(" or: npx playwright install-deps chromium"); println!(" or: npx playwright install-deps chromium");
println!(); println!();
@@ -136,7 +142,7 @@ pub fn run_install(with_deps: bool) {
} }
println!("{}", color::cyan("Installing Chromium browser...")); println!("{}", color::cyan("Installing Chromium browser..."));
// On Windows, we need to use cmd.exe to run npx because npx is actually npx.cmd // On Windows, we need to use cmd.exe to run npx because npx is actually npx.cmd
// and Command::new() doesn't resolve .cmd files the way the shell does. // and Command::new() doesn't resolve .cmd files the way the shell does.
// Pass the entire command as a single string to /c to handle paths with spaces. // Pass the entire command as a single string to /c to handle paths with spaces.
@@ -144,7 +150,7 @@ pub fn run_install(with_deps: bool) {
let status = Command::new("cmd") let status = Command::new("cmd")
.args(["/c", "npx playwright install chromium"]) .args(["/c", "npx playwright install chromium"])
.status(); .status();
#[cfg(not(windows))] #[cfg(not(windows))]
let status = Command::new("npx") let status = Command::new("npx")
.args(["playwright", "install", "chromium"]) .args(["playwright", "install", "chromium"])
@@ -152,17 +158,26 @@ pub fn run_install(with_deps: bool) {
match status { match status {
Ok(s) if s.success() => { Ok(s) if s.success() => {
println!("{} Chromium installed successfully", color::success_indicator()); println!(
"{} Chromium installed successfully",
color::success_indicator()
);
if is_linux && !with_deps { if is_linux && !with_deps {
println!(); println!();
println!("{} If you see \"shared library\" errors when running, use:", color::yellow("Note:")); println!(
"{} If you see \"shared library\" errors when running, use:",
color::yellow("Note:")
);
println!(" agent-browser install --with-deps"); println!(" agent-browser install --with-deps");
} }
} }
Ok(_) => { Ok(_) => {
eprintln!("{} Failed to install browser", color::error_indicator()); eprintln!("{} Failed to install browser", color::error_indicator());
if is_linux { if is_linux {
println!("{} Try installing system dependencies first:", color::yellow("Tip:")); println!(
"{} Try installing system dependencies first:",
color::yellow("Tip:")
);
println!(" agent-browser install --with-deps"); println!(" agent-browser install --with-deps");
} }
exit(1); exit(1);
+39 -9
View File
@@ -106,7 +106,11 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
} else { } else {
println!("Active sessions:"); println!("Active sessions:");
for s in &sessions { for s in &sessions {
let marker = if s == session { color::cyan("→") } else { " ".to_string() }; let marker = if s == session {
color::cyan("→")
} else {
" ".to_string()
};
println!("{} {}", marker, s); println!("{} {}", marker, s);
} }
} }
@@ -201,6 +205,8 @@ fn main() {
flags.proxy.as_deref(), flags.proxy.as_deref(),
flags.proxy_bypass.as_deref(), flags.proxy_bypass.as_deref(),
flags.ignore_https_errors, flags.ignore_https_errors,
flags.profile.as_deref(),
flags.state.as_deref(),
) { ) {
Ok(result) => result, Ok(result) => result,
Err(e) => { Err(e) => {
@@ -218,8 +224,13 @@ fn main() {
let has_extensions = !flags.extensions.is_empty(); let has_extensions = !flags.extensions.is_empty();
let ignored_flags: Vec<&str> = [ let ignored_flags: Vec<&str> = [
flags.executable_path.as_ref().map(|_| "--executable-path"), flags.executable_path.as_ref().map(|_| "--executable-path"),
if has_extensions { Some("--extension") } else { None }, if has_extensions {
Some("--extension")
} else {
None
},
flags.profile.as_ref().map(|_| "--profile"), flags.profile.as_ref().map(|_| "--profile"),
flags.state.as_ref().map(|_| "--state"),
flags.args.as_ref().map(|_| "--args"), flags.args.as_ref().map(|_| "--args"),
flags.user_agent.as_ref().map(|_| "--user-agent"), flags.user_agent.as_ref().map(|_| "--user-agent"),
flags.proxy.as_ref().map(|_| "--proxy"), flags.proxy.as_ref().map(|_| "--proxy"),
@@ -356,7 +367,10 @@ fn main() {
let err = match send_command(launch_cmd, &flags.session) { let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None, Ok(resp) if resp.success => None,
Ok(resp) => Some(resp.error.unwrap_or_else(|| "Provider connection failed".to_string())), Ok(resp) => Some(
resp.error
.unwrap_or_else(|| "Provider connection failed".to_string()),
),
Err(e) => Some(e.to_string()), Err(e) => Some(e.to_string()),
}; };
@@ -371,14 +385,23 @@ fn main() {
} }
// Launch headed browser or configure browser options (without CDP or provider) // Launch headed browser or configure browser options (without CDP or provider)
if (flags.headed || flags.profile.is_some() || flags.proxy.is_some() || flags.args.is_some() || flags.user_agent.is_some()) && flags.cdp.is_none() && flags.provider.is_none() { if (flags.headed
|| flags.profile.is_some()
|| flags.state.is_some()
|| 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!({ let mut launch_cmd = json!({
"id": gen_id(), "id": gen_id(),
"action": "launch", "action": "launch",
"headless": !flags.headed "headless": !flags.headed
}); });
let cmd_obj = launch_cmd.as_object_mut() let cmd_obj = launch_cmd
.as_object_mut()
.expect("json! macro guarantees object type"); .expect("json! macro guarantees object type");
// Add profile path if specified // Add profile path if specified
@@ -386,6 +409,11 @@ fn main() {
cmd_obj.insert("profile".to_string(), json!(profile_path)); cmd_obj.insert("profile".to_string(), json!(profile_path));
} }
// Add state path if specified
if let Some(ref state_path) = flags.state {
cmd_obj.insert("storageState".to_string(), json!(state_path));
}
if let Some(ref proxy_str) = flags.proxy { if let Some(ref proxy_str) = flags.proxy {
let mut proxy_obj = parse_proxy(proxy_str); let mut proxy_obj = parse_proxy(proxy_str);
// Add bypass if specified // Add bypass if specified
@@ -417,7 +445,11 @@ fn main() {
if let Err(e) = send_command(launch_cmd, &flags.session) { if let Err(e) = send_command(launch_cmd, &flags.session) {
if !flags.json { if !flags.json {
eprintln!("{} Could not configure browser: {}", color::warning_indicator(), e); eprintln!(
"{} Could not configure browser: {}",
color::warning_indicator(),
e
);
} }
} }
} }
@@ -426,9 +458,7 @@ fn main() {
Ok(resp) => { Ok(resp) => {
let success = resp.success; let success = resp.success;
// Extract action for context-specific output handling // Extract action for context-specific output handling
let action = cmd let action = cmd.get("action").and_then(|v| v.as_str());
.get("action")
.and_then(|v| v.as_str());
print_response(&resp, flags.json, action); print_response(&resp, flags.json, action);
if !success { if !success {
exit(1); exit(1);
+119 -29
View File
@@ -87,7 +87,11 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
.unwrap_or("Untitled"); .unwrap_or("Untitled");
let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or(""); let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or("");
let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false); let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false);
let marker = if active { color::cyan("→") } else { " ".to_string() }; let marker = if active {
color::cyan("→")
} else {
" ".to_string()
};
println!("{} [{}] {} - {}", marker, i, title, url); println!("{} [{}] {} - {}", marker, i, title, url);
} }
return; return;
@@ -126,7 +130,10 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
for req in requests { for req in requests {
let method = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET"); let method = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
let url = req.get("url").and_then(|v| v.as_str()).unwrap_or(""); let url = req.get("url").and_then(|v| v.as_str()).unwrap_or("");
let resource_type = req.get("resourceType").and_then(|v| v.as_str()).unwrap_or(""); let resource_type = req
.get("resourceType")
.and_then(|v| v.as_str())
.unwrap_or("");
println!("{} {} ({})", method, url, resource_type); println!("{} {} ({})", method, url, resource_type);
} }
} }
@@ -153,7 +160,7 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
let tag = el.get("tag").and_then(|v| v.as_str()).unwrap_or("?"); let tag = el.get("tag").and_then(|v| v.as_str()).unwrap_or("?");
let text = el.get("text").and_then(|v| v.as_str()).unwrap_or(""); let text = el.get("text").and_then(|v| v.as_str()).unwrap_or("");
println!("[{}] {} \"{}\"", i, tag, text); println!("[{}] {} \"{}\"", i, tag, text);
if let Some(box_data) = el.get("box") { if let Some(box_data) = el.get("box") {
let w = box_data.get("width").and_then(|v| v.as_i64()).unwrap_or(0); let w = box_data.get("width").and_then(|v| v.as_i64()).unwrap_or(0);
let h = box_data.get("height").and_then(|v| v.as_i64()).unwrap_or(0); let h = box_data.get("height").and_then(|v| v.as_i64()).unwrap_or(0);
@@ -161,15 +168,30 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
let y = box_data.get("y").and_then(|v| v.as_i64()).unwrap_or(0); let y = box_data.get("y").and_then(|v| v.as_i64()).unwrap_or(0);
println!(" box: {}x{} at ({}, {})", w, h, x, y); println!(" box: {}x{} at ({}, {})", w, h, x, y);
} }
if let Some(styles) = el.get("styles") { if let Some(styles) = el.get("styles") {
let font_size = styles.get("fontSize").and_then(|v| v.as_str()).unwrap_or(""); let font_size = styles
let font_weight = styles.get("fontWeight").and_then(|v| v.as_str()).unwrap_or(""); .get("fontSize")
let font_family = styles.get("fontFamily").and_then(|v| v.as_str()).unwrap_or(""); .and_then(|v| v.as_str())
.unwrap_or("");
let font_weight = styles
.get("fontWeight")
.and_then(|v| v.as_str())
.unwrap_or("");
let font_family = styles
.get("fontFamily")
.and_then(|v| v.as_str())
.unwrap_or("");
let color = styles.get("color").and_then(|v| v.as_str()).unwrap_or(""); let color = styles.get("color").and_then(|v| v.as_str()).unwrap_or("");
let bg = styles.get("backgroundColor").and_then(|v| v.as_str()).unwrap_or(""); let bg = styles
let radius = styles.get("borderRadius").and_then(|v| v.as_str()).unwrap_or(""); .get("backgroundColor")
.and_then(|v| v.as_str())
.unwrap_or("");
let radius = styles
.get("borderRadius")
.and_then(|v| v.as_str())
.unwrap_or("");
println!(" font: {} {} {}", font_size, font_weight, font_family); println!(" font: {} {} {}", font_size, font_weight, font_family);
println!(" color: {}", color); println!(" color: {}", color);
println!(" background: {}", bg); println!(" background: {}", bg);
@@ -199,9 +221,17 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
} }
// Recording restart (has "stopped" field - from recording_restart action) // Recording restart (has "stopped" field - from recording_restart action)
if data.get("stopped").is_some() { if data.get("stopped").is_some() {
let path = data.get("path").and_then(|v| v.as_str()).unwrap_or("unknown"); let path = data
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
if let Some(prev_path) = data.get("previousPath").and_then(|v| v.as_str()) { if let Some(prev_path) = data.get("previousPath").and_then(|v| v.as_str()) {
println!("{} Recording restarted: {} (previous saved to {})", color::success_indicator(), path, prev_path); println!(
"{} Recording restarted: {} (previous saved to {})",
color::success_indicator(),
path,
prev_path
);
} else { } else {
println!("{} Recording started: {}", color::success_indicator(), path); println!("{} Recording started: {}", color::success_indicator(), path);
} }
@@ -211,7 +241,12 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
if data.get("frames").is_some() { if data.get("frames").is_some() {
if let Some(path) = data.get("path").and_then(|v| v.as_str()) { if let Some(path) = data.get("path").and_then(|v| v.as_str()) {
if let Some(error) = data.get("error").and_then(|v| v.as_str()) { if let Some(error) = data.get("error").and_then(|v| v.as_str()) {
println!("{} Recording saved to {} - {}", color::warning_indicator(), path, error); println!(
"{} Recording saved to {} - {}",
color::warning_indicator(),
path,
error
);
} else { } else {
println!("{} Recording saved to {}", color::success_indicator(), path); println!("{} Recording saved to {}", color::success_indicator(), path);
} }
@@ -223,14 +258,24 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
// Download response (has "suggestedFilename" or "filename" field) // Download response (has "suggestedFilename" or "filename" field)
if data.get("suggestedFilename").is_some() || data.get("filename").is_some() { if data.get("suggestedFilename").is_some() || data.get("filename").is_some() {
if let Some(path) = data.get("path").and_then(|v| v.as_str()) { if let Some(path) = data.get("path").and_then(|v| v.as_str()) {
let filename = data.get("suggestedFilename") let filename = data
.get("suggestedFilename")
.or_else(|| data.get("filename")) .or_else(|| data.get("filename"))
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
if filename.is_empty() { if filename.is_empty() {
println!("{} Downloaded to {}", color::success_indicator(), color::green(path)); println!(
"{} Downloaded to {}",
color::success_indicator(),
color::green(path)
);
} else { } else {
println!("{} Downloaded to {} ({})", color::success_indicator(), color::green(path), filename); println!(
"{} Downloaded to {} ({})",
color::success_indicator(),
color::green(path),
filename
);
} }
return; return;
} }
@@ -243,18 +288,50 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
// Path-based operations (screenshot/pdf/trace/har/download/state/video) // Path-based operations (screenshot/pdf/trace/har/download/state/video)
if let Some(path) = data.get("path").and_then(|v| v.as_str()) { if let Some(path) = data.get("path").and_then(|v| v.as_str()) {
match action.unwrap_or("") { match action.unwrap_or("") {
"screenshot" => println!("{} Screenshot saved to {}", color::success_indicator(), color::green(path)), "screenshot" => println!(
"pdf" => println!("{} PDF saved to {}", color::success_indicator(), color::green(path)), "{} Screenshot saved to {}",
"trace_stop" => println!("{} Trace saved to {}", color::success_indicator(), color::green(path)), color::success_indicator(),
"har_stop" => println!("{} HAR saved to {}", color::success_indicator(), color::green(path)), color::green(path)
"download" | "waitfordownload" => println!("{} Download saved to {}", color::success_indicator(), color::green(path)), ),
"video_stop" => println!("{} Video saved to {}", color::success_indicator(), color::green(path)), "pdf" => println!(
"state_save" => println!("{} State saved to {}", color::success_indicator(), color::green(path)), "{} PDF saved to {}",
color::success_indicator(),
color::green(path)
),
"trace_stop" => println!(
"{} Trace saved to {}",
color::success_indicator(),
color::green(path)
),
"har_stop" => println!(
"{} HAR saved to {}",
color::success_indicator(),
color::green(path)
),
"download" | "waitfordownload" => println!(
"{} Download saved to {}",
color::success_indicator(),
color::green(path)
),
"video_stop" => println!(
"{} Video saved to {}",
color::success_indicator(),
color::green(path)
),
"state_save" => println!(
"{} State saved to {}",
color::success_indicator(),
color::green(path)
),
"state_load" => { "state_load" => {
if let Some(note) = data.get("note").and_then(|v| v.as_str()) { if let Some(note) = data.get("note").and_then(|v| v.as_str()) {
println!("{}", note); println!("{}", note);
} }
println!("{} State path set to {}", color::success_indicator(), color::green(path)); println!(
"{} State path set to {}",
color::success_indicator(),
color::green(path)
);
} }
// video_start and other commands that provide a path with a note // video_start and other commands that provide a path with a note
"video_start" => { "video_start" => {
@@ -263,7 +340,11 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
} }
println!("Path: {}", path); println!("Path: {}", path);
} }
_ => println!("{} Saved to {}", color::success_indicator(), color::green(path)), _ => println!(
"{} Saved to {}",
color::success_indicator(),
color::green(path)
),
} }
return; return;
} }
@@ -1251,7 +1332,8 @@ Examples:
} }
// === Record (video) === // === Record (video) ===
"record" => r##" "record" => {
r##"
agent-browser record - Record browser session to video agent-browser record - Record browser session to video
Usage: agent-browser record start <path.webm> [url] Usage: agent-browser record start <path.webm> [url]
@@ -1284,7 +1366,8 @@ Examples:
# Restart recording with a new file (stops previous, starts new) # Restart recording with a new file (stops previous, starts new)
agent-browser record restart ./take2.webm agent-browser record restart ./take2.webm
"##, "##
}
// === Console/Errors === // === Console/Errors ===
"console" => { "console" => {
@@ -1358,7 +1441,13 @@ Save or restore browser state (cookies, localStorage, sessionStorage).
Operations: Operations:
save <path> Save current state to file save <path> Save current state to file
load <path> Load state from file load <path> Note: State must be loaded at browser launch via --state flag
Applying State:
Use --state flag when launching browser to load saved state:
agent-browser --state ./auth-state.json open https://example.com
Or set AGENT_BROWSER_STATE environment variable.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
@@ -1366,7 +1455,7 @@ Global Options:
Examples: Examples:
agent-browser state save ./auth-state.json agent-browser state save ./auth-state.json
agent-browser state load ./auth-state.json agent-browser --state ./auth-state.json open https://example.com
"## "##
} }
@@ -1553,6 +1642,7 @@ Snapshot Options:
Options: Options:
--session <name> Isolated session (or AGENT_BROWSER_SESSION env) --session <name> Isolated session (or AGENT_BROWSER_SESSION env)
--profile <path> Persistent browser profile (or AGENT_BROWSER_PROFILE env) --profile <path> Persistent browser profile (or AGENT_BROWSER_PROFILE env)
--state <path> Load storage state from JSON file (or AGENT_BROWSER_STATE env)
--headers <json> HTTP headers scoped to URL's origin (for auth) --headers <json> HTTP headers scoped to URL's origin (for auth)
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH) --executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
--extension <path> Load browser extensions (repeatable) --extension <path> Load browser extensions (repeatable)
+14
View File
@@ -819,6 +819,7 @@ export class BrowserManager {
const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined); const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined);
const hasExtensions = !!options.extensions?.length; const hasExtensions = !!options.extensions?.length;
const hasProfile = !!options.profile; const hasProfile = !!options.profile;
const hasStorageState = !!options.storageState;
if (hasExtensions && cdpEndpoint) { if (hasExtensions && cdpEndpoint) {
throw new Error('Extensions cannot be used with CDP connection'); throw new Error('Extensions cannot be used with CDP connection');
@@ -828,6 +829,18 @@ export class BrowserManager {
throw new Error('Profile cannot be used with CDP connection'); throw new Error('Profile cannot be used with CDP connection');
} }
if (hasStorageState && hasProfile) {
throw new Error(
'Storage state cannot be used with profile (profile is already persistent storage)'
);
}
if (hasStorageState && hasExtensions) {
throw new Error(
'Storage state cannot be used with extensions (extensions require persistent context)'
);
}
if (this.isLaunched()) { if (this.isLaunched()) {
const needsRelaunch = const needsRelaunch =
(!cdpEndpoint && this.cdpEndpoint !== null) || (!cdpEndpoint && this.cdpEndpoint !== null) ||
@@ -912,6 +925,7 @@ export class BrowserManager {
userAgent: options.userAgent, userAgent: options.userAgent,
...(options.proxy && { proxy: options.proxy }), ...(options.proxy && { proxy: options.proxy }),
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false, ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
...(options.storageState && { storageState: options.storageState }),
}); });
} }
+2
View File
@@ -255,6 +255,8 @@ export async function startDaemon(options?: { streamPort?: number }): Promise<vo
headless: process.env.AGENT_BROWSER_HEADED !== '1', headless: process.env.AGENT_BROWSER_HEADED !== '1',
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH, executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
extensions: extensions, extensions: extensions,
profile: process.env.AGENT_BROWSER_PROFILE,
storageState: process.env.AGENT_BROWSER_STATE,
args, args,
userAgent: process.env.AGENT_BROWSER_USER_AGENT, userAgent: process.env.AGENT_BROWSER_USER_AGENT,
proxy, proxy,
+2
View File
@@ -46,6 +46,8 @@ const launchSchema = baseCommandSchema.extend({
userAgent: z.string().optional(), userAgent: z.string().optional(),
provider: z.string().optional(), provider: z.string().optional(),
ignoreHTTPSErrors: z.boolean().optional(), ignoreHTTPSErrors: z.boolean().optional(),
profile: z.string().optional(),
storageState: z.string().optional(),
}); });
const navigateSchema = baseCommandSchema.extend({ const navigateSchema = baseCommandSchema.extend({
+1
View File
@@ -18,6 +18,7 @@ export interface LaunchCommand extends BaseCommand {
cdpUrl?: string; cdpUrl?: string;
extensions?: string[]; extensions?: string[];
profile?: string; // Path to persistent browser profile directory profile?: string; // Path to persistent browser profile directory
storageState?: string; // Path to storage state JSON file
proxy?: { proxy?: {
server: string; server: string;
bypass?: string; bypass?: string;