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,
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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 }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user