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:
+181
-78
@@ -18,7 +18,10 @@ pub enum ParseError {
|
||||
usage: &'static str,
|
||||
},
|
||||
/// Argument exists but has an invalid value
|
||||
InvalidValue { message: String, usage: &'static str },
|
||||
InvalidValue {
|
||||
message: String,
|
||||
usage: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
@@ -81,11 +84,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
usage: "open <url>",
|
||||
})?;
|
||||
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("about:")
|
||||
|| url_lower.starts_with("data:")
|
||||
|| url_lower.starts_with("file:") {
|
||||
|| url_lower.starts_with("about:")
|
||||
|| url_lower.starts_with("data:")
|
||||
|| url_lower.starts_with("file:")
|
||||
{
|
||||
url.to_string()
|
||||
} else {
|
||||
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") {
|
||||
let mut cmd = json!({ "id": id, "action": "waitfordownload" });
|
||||
// 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 !path.starts_with("--") {
|
||||
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
|
||||
let is_relative_path = first.starts_with("./") || first.starts_with("../");
|
||||
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")
|
||||
|| first.ends_with(".jpg")
|
||||
|| first.ends_with(".jpeg")
|
||||
@@ -361,7 +370,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
_ => (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" => {
|
||||
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" => {
|
||||
if let Some(same_site) = rest.get(i + 1) {
|
||||
// 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);
|
||||
i += 2;
|
||||
} else {
|
||||
@@ -583,9 +597,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
}
|
||||
|
||||
Ok(
|
||||
json!({ "id": id, "action": "cookies_set", "cookies": [cookie] }),
|
||||
)
|
||||
Ok(json!({ "id": id, "action": "cookies_set", "cookies": [cookie] }))
|
||||
}
|
||||
"clear" => Ok(json!({ "id": id, "action": "cookies_clear" })),
|
||||
_ => Ok(json!({ "id": id, "action": "cookies_get" })),
|
||||
@@ -593,28 +605,26 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
|
||||
// === Tabs ===
|
||||
"tab" => {
|
||||
match rest.get(0).map(|s| *s) {
|
||||
Some("new") => {
|
||||
let mut cmd = json!({ "id": id, "action": "tab_new" });
|
||||
if let Some(url) = rest.get(1) {
|
||||
cmd["url"] = json!(url);
|
||||
}
|
||||
Ok(cmd)
|
||||
"tab" => match rest.get(0).map(|s| *s) {
|
||||
Some("new") => {
|
||||
let mut cmd = json!({ "id": id, "action": "tab_new" });
|
||||
if let Some(url) = rest.get(1) {
|
||||
cmd["url"] = json!(url);
|
||||
}
|
||||
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" })),
|
||||
Ok(cmd)
|
||||
}
|
||||
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 ===
|
||||
@@ -679,7 +689,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
usage: "trace stop <path>",
|
||||
})?;
|
||||
Ok(json!({ "id": id, "action": "trace_stop", "path": path }))
|
||||
},
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
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> {
|
||||
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) {
|
||||
Some("text") => {
|
||||
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 {
|
||||
"role" => {
|
||||
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)
|
||||
}
|
||||
"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" => {
|
||||
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)
|
||||
}
|
||||
"placeholder" => {
|
||||
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)
|
||||
}
|
||||
"alt" => Ok(json!({ "id": id, "action": "getbyalttext", "text": value, "subaction": subaction, "exact": exact })),
|
||||
"title" => Ok(json!({ "id": id, "action": "getbytitle", "text": value, "subaction": subaction, "exact": exact })),
|
||||
"alt" => Ok(
|
||||
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" => {
|
||||
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)
|
||||
}
|
||||
"first" => {
|
||||
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)
|
||||
}
|
||||
"last" => {
|
||||
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)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -1008,7 +1038,9 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
None
|
||||
};
|
||||
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)
|
||||
}
|
||||
_ => Err(ParseError::UnknownSubcommand {
|
||||
@@ -1181,7 +1213,9 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
} else {
|
||||
"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 {
|
||||
subcommand: sub.to_string(),
|
||||
@@ -1214,7 +1248,7 @@ fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
cmd["url"] = json!(url);
|
||||
}
|
||||
Ok(cmd)
|
||||
},
|
||||
}
|
||||
Some("requests") => {
|
||||
let clear = rest.iter().any(|&s| s == "--clear");
|
||||
let filter_idx = rest.iter().position(|&s| s == "--filter");
|
||||
@@ -1299,6 +1333,7 @@ mod tests {
|
||||
extensions: Vec::new(),
|
||||
cdp: None,
|
||||
profile: None,
|
||||
state: None,
|
||||
proxy: None,
|
||||
proxy_bypass: None,
|
||||
args: None,
|
||||
@@ -1348,7 +1383,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1357,7 +1396,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1366,7 +1409,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1375,7 +1422,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1384,7 +1435,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1393,7 +1448,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1402,7 +1461,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["cookies"][0]["name"], "mycookie");
|
||||
assert_eq!(cmd["cookies"][0]["value"], "myvalue");
|
||||
@@ -1438,7 +1501,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -1658,11 +1724,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_select_multiple_values() {
|
||||
let cmd = parse_command(
|
||||
&args("select #menu opt1 opt2 opt3"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap();
|
||||
let cmd = parse_command(&args("select #menu opt1 opt2 opt3"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "select");
|
||||
assert_eq!(cmd["selector"], "#menu");
|
||||
assert_eq!(cmd["values"], json!(["opt1", "opt2", "opt3"]));
|
||||
@@ -1680,7 +1742,10 @@ mod tests {
|
||||
fn test_tab_new() {
|
||||
let cmd = parse_command(&args("tab new"), &default_flags()).unwrap();
|
||||
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]
|
||||
@@ -1872,7 +1937,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["path"], "demo.webm");
|
||||
assert_eq!(cmd["url"], "https://example.com");
|
||||
@@ -1880,7 +1949,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["path"], "demo.webm");
|
||||
assert_eq!(cmd["url"], "https://example.com");
|
||||
@@ -1890,7 +1963,10 @@ mod tests {
|
||||
fn test_record_start_missing_path() {
|
||||
let result = parse_command(&args("record start"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1909,7 +1985,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["path"], "demo.webm");
|
||||
assert_eq!(cmd["url"], "https://example.com");
|
||||
@@ -1919,21 +1999,30 @@ mod tests {
|
||||
fn test_record_restart_missing_path() {
|
||||
let result = parse_command(&args("record restart"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_invalid_subcommand() {
|
||||
let result = parse_command(&args("record foo"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::UnknownSubcommand { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::UnknownSubcommand { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_missing_subcommand() {
|
||||
let result = parse_command(&args("record"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2058,14 +2147,20 @@ mod tests {
|
||||
fn test_download_missing_path() {
|
||||
let result = parse_command(&args("download #btn"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_missing_selector() {
|
||||
let result = parse_command(&args("download"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
// === Wait for Download Tests ===
|
||||
@@ -2086,14 +2181,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["timeout"], 30000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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["path"], "./file.pdf");
|
||||
assert_eq!(cmd["timeout"], 30000);
|
||||
@@ -2136,16 +2236,16 @@ mod tests {
|
||||
];
|
||||
let cmd = parse_command(&input, &default_flags()).unwrap();
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connect_with_http_url() {
|
||||
let input: Vec<String> = vec![
|
||||
"connect".to_string(),
|
||||
"http://localhost:9222".to_string(),
|
||||
];
|
||||
let input: Vec<String> = vec!["connect".to_string(), "http://localhost:9222".to_string()];
|
||||
let cmd = parse_command(&input, &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "launch");
|
||||
assert_eq!(cmd["cdpUrl"], "http://localhost:9222");
|
||||
@@ -2156,7 +2256,10 @@ mod tests {
|
||||
fn test_connect_missing_argument() {
|
||||
let result = parse_command(&args("connect"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+35
-7
@@ -195,6 +195,8 @@ pub fn ensure_daemon(
|
||||
proxy: Option<&str>,
|
||||
proxy_bypass: Option<&str>,
|
||||
ignore_https_errors: bool,
|
||||
profile: Option<&str>,
|
||||
state: Option<&str>,
|
||||
) -> Result<DaemonResult, String> {
|
||||
if is_daemon_running(session) && daemon_ready(session) {
|
||||
return Ok(DaemonResult {
|
||||
@@ -205,7 +207,8 @@ pub fn ensure_daemon(
|
||||
// Ensure socket directory exists
|
||||
let socket_dir = get_socket_dir();
|
||||
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())?;
|
||||
@@ -271,6 +274,14 @@ pub fn ensure_daemon(
|
||||
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
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
@@ -290,7 +301,7 @@ pub fn ensure_daemon(
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
|
||||
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
|
||||
// and automatically quotes arguments containing spaces.
|
||||
let mut cmd = Command::new("node");
|
||||
@@ -326,10 +337,18 @@ pub fn ensure_daemon(
|
||||
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
|
||||
}
|
||||
|
||||
if ignore_https_errors {
|
||||
if ignore_https_errors {
|
||||
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
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
@@ -446,7 +465,9 @@ mod tests {
|
||||
env::set_var("AGENT_BROWSER_SOCKET_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]
|
||||
@@ -456,7 +477,10 @@ mod tests {
|
||||
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
|
||||
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]
|
||||
@@ -466,7 +490,9 @@ mod tests {
|
||||
env::set_var("AGENT_BROWSER_SOCKET_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]
|
||||
@@ -478,6 +504,8 @@ mod tests {
|
||||
|
||||
let result = get_socket_dir();
|
||||
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
@@ -11,6 +11,7 @@ pub struct Flags {
|
||||
pub cdp: Option<String>,
|
||||
pub extensions: Vec<String>,
|
||||
pub profile: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub proxy: Option<String>,
|
||||
pub proxy_bypass: Option<String>,
|
||||
pub args: Option<String>,
|
||||
@@ -22,7 +23,12 @@ pub struct Flags {
|
||||
pub fn parse_flags(args: &[String]) -> Flags {
|
||||
let extensions_env = env::var("AGENT_BROWSER_EXTENSIONS")
|
||||
.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();
|
||||
|
||||
let mut flags = Flags {
|
||||
@@ -36,6 +42,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
cdp: None,
|
||||
extensions: extensions_env,
|
||||
profile: env::var("AGENT_BROWSER_PROFILE").ok(),
|
||||
state: env::var("AGENT_BROWSER_STATE").ok(),
|
||||
proxy: env::var("AGENT_BROWSER_PROXY").ok(),
|
||||
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").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());
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
"--extension" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.extensions.push(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
"--cdp" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.cdp = Some(s.clone());
|
||||
@@ -87,6 +94,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--state" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.state = Some(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--proxy" => {
|
||||
if let Some(p) = args.get(i + 1) {
|
||||
flags.proxy = Some(p.clone());
|
||||
@@ -130,7 +143,13 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
let mut skip_next = false;
|
||||
|
||||
// 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)
|
||||
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
|
||||
"--session",
|
||||
@@ -139,6 +158,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--cdp",
|
||||
"--extension",
|
||||
"--profile",
|
||||
"--state",
|
||||
"--proxy",
|
||||
"--proxy-bypass",
|
||||
"--args",
|
||||
|
||||
+22
-7
@@ -100,7 +100,10 @@ pub fn run_install(with_deps: bool) {
|
||||
],
|
||||
)
|
||||
} 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);
|
||||
};
|
||||
|
||||
@@ -128,7 +131,10 @@ pub fn run_install(with_deps: bool) {
|
||||
Err(e) => eprintln!("{} Could not run install command: {}", color::warning_indicator(), e),
|
||||
}
|
||||
} 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!(" or: npx playwright install-deps chromium");
|
||||
println!();
|
||||
@@ -136,7 +142,7 @@ pub fn run_install(with_deps: bool) {
|
||||
}
|
||||
|
||||
println!("{}", color::cyan("Installing Chromium browser..."));
|
||||
|
||||
|
||||
// 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.
|
||||
// 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")
|
||||
.args(["/c", "npx playwright install chromium"])
|
||||
.status();
|
||||
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let status = Command::new("npx")
|
||||
.args(["playwright", "install", "chromium"])
|
||||
@@ -152,17 +158,26 @@ pub fn run_install(with_deps: bool) {
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => {
|
||||
println!("{} Chromium installed successfully", color::success_indicator());
|
||||
println!(
|
||||
"{} Chromium installed successfully",
|
||||
color::success_indicator()
|
||||
);
|
||||
if is_linux && !with_deps {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
eprintln!("{} Failed to install browser", color::error_indicator());
|
||||
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");
|
||||
}
|
||||
exit(1);
|
||||
|
||||
+39
-9
@@ -106,7 +106,11 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
|
||||
} else {
|
||||
println!("Active 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);
|
||||
}
|
||||
}
|
||||
@@ -201,6 +205,8 @@ fn main() {
|
||||
flags.proxy.as_deref(),
|
||||
flags.proxy_bypass.as_deref(),
|
||||
flags.ignore_https_errors,
|
||||
flags.profile.as_deref(),
|
||||
flags.state.as_deref(),
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
@@ -218,8 +224,13 @@ fn main() {
|
||||
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 },
|
||||
if has_extensions {
|
||||
Some("--extension")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags.profile.as_ref().map(|_| "--profile"),
|
||||
flags.state.as_ref().map(|_| "--state"),
|
||||
flags.args.as_ref().map(|_| "--args"),
|
||||
flags.user_agent.as_ref().map(|_| "--user-agent"),
|
||||
flags.proxy.as_ref().map(|_| "--proxy"),
|
||||
@@ -356,7 +367,10 @@ fn main() {
|
||||
|
||||
let err = match send_command(launch_cmd, &flags.session) {
|
||||
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()),
|
||||
};
|
||||
|
||||
@@ -371,14 +385,23 @@ fn main() {
|
||||
}
|
||||
|
||||
// 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!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"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");
|
||||
|
||||
// Add profile path if specified
|
||||
@@ -386,6 +409,11 @@ fn main() {
|
||||
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 {
|
||||
let mut proxy_obj = parse_proxy(proxy_str);
|
||||
// Add bypass if specified
|
||||
@@ -417,7 +445,11 @@ fn main() {
|
||||
|
||||
if let Err(e) = send_command(launch_cmd, &flags.session) {
|
||||
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) => {
|
||||
let success = resp.success;
|
||||
// Extract action for context-specific output handling
|
||||
let action = cmd
|
||||
.get("action")
|
||||
.and_then(|v| v.as_str());
|
||||
let action = cmd.get("action").and_then(|v| v.as_str());
|
||||
print_response(&resp, flags.json, action);
|
||||
if !success {
|
||||
exit(1);
|
||||
|
||||
+119
-29
@@ -87,7 +87,11 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
|
||||
.unwrap_or("Untitled");
|
||||
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 marker = if active { color::cyan("→") } else { " ".to_string() };
|
||||
let marker = if active {
|
||||
color::cyan("→")
|
||||
} else {
|
||||
" ".to_string()
|
||||
};
|
||||
println!("{} [{}] {} - {}", marker, i, title, url);
|
||||
}
|
||||
return;
|
||||
@@ -126,7 +130,10 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
|
||||
for req in requests {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
@@ -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 text = el.get("text").and_then(|v| v.as_str()).unwrap_or("");
|
||||
println!("[{}] {} \"{}\"", i, tag, text);
|
||||
|
||||
|
||||
if let Some(box_data) = el.get("box") {
|
||||
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);
|
||||
@@ -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);
|
||||
println!(" box: {}x{} at ({}, {})", w, h, x, y);
|
||||
}
|
||||
|
||||
|
||||
if let Some(styles) = el.get("styles") {
|
||||
let font_size = styles.get("fontSize").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 font_size = styles
|
||||
.get("fontSize")
|
||||
.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 bg = styles.get("backgroundColor").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let radius = styles.get("borderRadius").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
let bg = styles
|
||||
.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!(" color: {}", color);
|
||||
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)
|
||||
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()) {
|
||||
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 {
|
||||
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 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()) {
|
||||
println!("{} Recording saved to {} - {}", color::warning_indicator(), path, error);
|
||||
println!(
|
||||
"{} Recording saved to {} - {}",
|
||||
color::warning_indicator(),
|
||||
path,
|
||||
error
|
||||
);
|
||||
} else {
|
||||
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)
|
||||
if data.get("suggestedFilename").is_some() || data.get("filename").is_some() {
|
||||
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"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if filename.is_empty() {
|
||||
println!("{} Downloaded to {}", color::success_indicator(), color::green(path));
|
||||
println!(
|
||||
"{} Downloaded to {}",
|
||||
color::success_indicator(),
|
||||
color::green(path)
|
||||
);
|
||||
} else {
|
||||
println!("{} Downloaded to {} ({})", color::success_indicator(), color::green(path), filename);
|
||||
println!(
|
||||
"{} Downloaded to {} ({})",
|
||||
color::success_indicator(),
|
||||
color::green(path),
|
||||
filename
|
||||
);
|
||||
}
|
||||
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)
|
||||
if let Some(path) = data.get("path").and_then(|v| v.as_str()) {
|
||||
match action.unwrap_or("") {
|
||||
"screenshot" => println!("{} Screenshot saved to {}", color::success_indicator(), color::green(path)),
|
||||
"pdf" => println!("{} 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)),
|
||||
"screenshot" => println!(
|
||||
"{} Screenshot saved to {}",
|
||||
color::success_indicator(),
|
||||
color::green(path)
|
||||
),
|
||||
"pdf" => println!(
|
||||
"{} 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" => {
|
||||
if let Some(note) = data.get("note").and_then(|v| v.as_str()) {
|
||||
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" => {
|
||||
@@ -263,7 +340,11 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
|
||||
}
|
||||
println!("Path: {}", path);
|
||||
}
|
||||
_ => println!("{} Saved to {}", color::success_indicator(), color::green(path)),
|
||||
_ => println!(
|
||||
"{} Saved to {}",
|
||||
color::success_indicator(),
|
||||
color::green(path)
|
||||
),
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1251,7 +1332,8 @@ Examples:
|
||||
}
|
||||
|
||||
// === Record (video) ===
|
||||
"record" => r##"
|
||||
"record" => {
|
||||
r##"
|
||||
agent-browser record - Record browser session to video
|
||||
|
||||
Usage: agent-browser record start <path.webm> [url]
|
||||
@@ -1284,7 +1366,8 @@ Examples:
|
||||
|
||||
# Restart recording with a new file (stops previous, starts new)
|
||||
agent-browser record restart ./take2.webm
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Console/Errors ===
|
||||
"console" => {
|
||||
@@ -1358,7 +1441,13 @@ Save or restore browser state (cookies, localStorage, sessionStorage).
|
||||
|
||||
Operations:
|
||||
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:
|
||||
--json Output as JSON
|
||||
@@ -1366,7 +1455,7 @@ Global Options:
|
||||
|
||||
Examples:
|
||||
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:
|
||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION 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)
|
||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||
--extension <path> Load browser extensions (repeatable)
|
||||
|
||||
@@ -819,6 +819,7 @@ export class BrowserManager {
|
||||
const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined);
|
||||
const hasExtensions = !!options.extensions?.length;
|
||||
const hasProfile = !!options.profile;
|
||||
const hasStorageState = !!options.storageState;
|
||||
|
||||
if (hasExtensions && cdpEndpoint) {
|
||||
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');
|
||||
}
|
||||
|
||||
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()) {
|
||||
const needsRelaunch =
|
||||
(!cdpEndpoint && this.cdpEndpoint !== null) ||
|
||||
@@ -912,6 +925,7 @@ export class BrowserManager {
|
||||
userAgent: options.userAgent,
|
||||
...(options.proxy && { proxy: options.proxy }),
|
||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
|
||||
...(options.storageState && { storageState: options.storageState }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,8 @@ export async function startDaemon(options?: { streamPort?: number }): Promise<vo
|
||||
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
||||
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
|
||||
extensions: extensions,
|
||||
profile: process.env.AGENT_BROWSER_PROFILE,
|
||||
storageState: process.env.AGENT_BROWSER_STATE,
|
||||
args,
|
||||
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
|
||||
proxy,
|
||||
|
||||
@@ -46,6 +46,8 @@ const launchSchema = baseCommandSchema.extend({
|
||||
userAgent: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
ignoreHTTPSErrors: z.boolean().optional(),
|
||||
profile: z.string().optional(),
|
||||
storageState: z.string().optional(),
|
||||
});
|
||||
|
||||
const navigateSchema = baseCommandSchema.extend({
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface LaunchCommand extends BaseCommand {
|
||||
cdpUrl?: string;
|
||||
extensions?: string[];
|
||||
profile?: string; // Path to persistent browser profile directory
|
||||
storageState?: string; // Path to storage state JSON file
|
||||
proxy?: {
|
||||
server: string;
|
||||
bypass?: string;
|
||||
|
||||
Reference in New Issue
Block a user