fix: support WebSocket URLs in connect command (#205)
* fix: support WebSocket URLs in connect command * address feedback
This commit is contained in:
+140
-7
@@ -17,6 +17,8 @@ pub enum ParseError {
|
|||||||
context: String,
|
context: String,
|
||||||
usage: &'static str,
|
usage: &'static str,
|
||||||
},
|
},
|
||||||
|
/// Argument exists but has an invalid value
|
||||||
|
InvalidValue { message: String, usage: &'static str },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParseError {
|
impl ParseError {
|
||||||
@@ -41,6 +43,9 @@ impl ParseError {
|
|||||||
context, usage
|
context, usage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ParseError::InvalidValue { message, usage } => {
|
||||||
|
format!("{}\nUsage: agent-browser {}", message, usage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,15 +359,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
|
|
||||||
// === Connect (CDP) ===
|
// === Connect (CDP) ===
|
||||||
"connect" => {
|
"connect" => {
|
||||||
let port_str = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
|
let endpoint = rest.first().ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: "connect".to_string(),
|
context: "connect".to_string(),
|
||||||
usage: "connect <port>",
|
usage: "connect <port|url>",
|
||||||
})?;
|
})?;
|
||||||
let port: u16 = port_str.parse().map_err(|_| ParseError::MissingArguments {
|
// Check if it's a URL (ws://, wss://, http://, https://)
|
||||||
context: format!("connect: invalid port '{}'", port_str),
|
if endpoint.starts_with("ws://")
|
||||||
usage: "connect <port>",
|
|| endpoint.starts_with("wss://")
|
||||||
})?;
|
|| endpoint.starts_with("http://")
|
||||||
Ok(json!({ "id": id, "action": "launch", "cdpPort": port }))
|
|| endpoint.starts_with("https://")
|
||||||
|
{
|
||||||
|
Ok(json!({ "id": id, "action": "launch", "cdpUrl": endpoint }))
|
||||||
|
} else {
|
||||||
|
// It's a port number - validate and use cdpPort field
|
||||||
|
let port: u16 = match endpoint.parse::<u32>() {
|
||||||
|
Ok(p) if p == 0 => {
|
||||||
|
return Err(ParseError::InvalidValue {
|
||||||
|
message: "Invalid port: port must be greater than 0".to_string(),
|
||||||
|
usage: "connect <port|url>",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(p) if p > 65535 => {
|
||||||
|
return Err(ParseError::InvalidValue {
|
||||||
|
message: format!(
|
||||||
|
"Invalid port: {} is out of range (valid range: 1-65535)",
|
||||||
|
p
|
||||||
|
),
|
||||||
|
usage: "connect <port|url>",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(p) => p as u16,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ParseError::InvalidValue {
|
||||||
|
message: format!(
|
||||||
|
"Invalid value: '{}' is not a valid port number or URL",
|
||||||
|
endpoint
|
||||||
|
),
|
||||||
|
usage: "connect <port|url>",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(json!({ "id": id, "action": "launch", "cdpPort": port }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Get ===
|
// === Get ===
|
||||||
@@ -1713,4 +1751,99 @@ mod tests {
|
|||||||
assert_eq!(cmd["index"], 2);
|
assert_eq!(cmd["index"], 2);
|
||||||
assert!(cmd.get("value").is_none());
|
assert!(cmd.get("value").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Connect (CDP) tests ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_with_port() {
|
||||||
|
let cmd = parse_command(&args("connect 9222"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "launch");
|
||||||
|
assert_eq!(cmd["cdpPort"], 9222);
|
||||||
|
assert!(cmd.get("cdpUrl").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_with_ws_url() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"connect".to_string(),
|
||||||
|
"ws://localhost:9222/devtools/browser/abc123".to_string(),
|
||||||
|
];
|
||||||
|
let cmd = parse_command(&input, &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "launch");
|
||||||
|
assert_eq!(cmd["cdpUrl"], "ws://localhost:9222/devtools/browser/abc123");
|
||||||
|
assert!(cmd.get("cdpPort").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_with_wss_url() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"connect".to_string(),
|
||||||
|
"wss://remote-browser.example.com/cdp?token=xyz".to_string(),
|
||||||
|
];
|
||||||
|
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!(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 cmd = parse_command(&input, &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "launch");
|
||||||
|
assert_eq!(cmd["cdpUrl"], "http://localhost:9222");
|
||||||
|
assert!(cmd.get("cdpPort").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_missing_argument() {
|
||||||
|
let result = parse_command(&args("connect"), &default_flags());
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_invalid_port() {
|
||||||
|
let result = parse_command(&args("connect notanumber"), &default_flags());
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(matches!(err, ParseError::InvalidValue { .. }));
|
||||||
|
assert!(err.format().contains("not a valid port number or URL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_port_zero() {
|
||||||
|
let result = parse_command(&args("connect 0"), &default_flags());
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(matches!(err, ParseError::InvalidValue { .. }));
|
||||||
|
assert!(err.format().contains("port must be greater than 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_port_out_of_range() {
|
||||||
|
let result = parse_command(&args("connect 65536"), &default_flags());
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(matches!(err, ParseError::InvalidValue { .. }));
|
||||||
|
assert!(err.format().contains("out of range"));
|
||||||
|
assert!(err.format().contains("1-65535"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_port_max_valid() {
|
||||||
|
let cmd = parse_command(&args("connect 65535"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "launch");
|
||||||
|
assert_eq!(cmd["cdpPort"], 65535);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_port_min_valid() {
|
||||||
|
let cmd = parse_command(&args("connect 1"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "launch");
|
||||||
|
assert_eq!(cmd["cdpPort"], 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ fn main() {
|
|||||||
ParseError::UnknownCommand { .. } => "unknown_command",
|
ParseError::UnknownCommand { .. } => "unknown_command",
|
||||||
ParseError::UnknownSubcommand { .. } => "unknown_subcommand",
|
ParseError::UnknownSubcommand { .. } => "unknown_subcommand",
|
||||||
ParseError::MissingArguments { .. } => "missing_arguments",
|
ParseError::MissingArguments { .. } => "missing_arguments",
|
||||||
|
ParseError::InvalidValue { .. } => "invalid_value",
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
r#"{{"success":false,"error":"{}","type":"{}"}}"#,
|
r#"{{"success":false,"error":"{}","type":"{}"}}"#,
|
||||||
|
|||||||
+41
-1
@@ -1317,6 +1317,46 @@ Examples:
|
|||||||
"##
|
"##
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Connect ===
|
||||||
|
"connect" => {
|
||||||
|
r##"
|
||||||
|
agent-browser connect - Connect to browser via CDP
|
||||||
|
|
||||||
|
Usage: agent-browser connect <port|url>
|
||||||
|
|
||||||
|
Connects to a running browser instance via Chrome DevTools Protocol (CDP).
|
||||||
|
This allows controlling browsers, Electron apps, or remote browser services.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<port> Local port number (e.g., 9222)
|
||||||
|
<url> Full WebSocket URL (ws://, wss://, http://, https://)
|
||||||
|
|
||||||
|
Supported URL formats:
|
||||||
|
- Port number: 9222 (connects to http://localhost:9222)
|
||||||
|
- WebSocket URL: ws://localhost:9222/devtools/browser/...
|
||||||
|
- Remote service: wss://remote-browser.example.com/cdp?token=...
|
||||||
|
|
||||||
|
Global Options:
|
||||||
|
--json Output as JSON
|
||||||
|
--session <name> Use specific session
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Connect to local Chrome with remote debugging
|
||||||
|
# Start Chrome: google-chrome --remote-debugging-port=9222
|
||||||
|
agent-browser connect 9222
|
||||||
|
|
||||||
|
# Connect using WebSocket URL from /json/version endpoint
|
||||||
|
agent-browser connect "ws://localhost:9222/devtools/browser/abc123"
|
||||||
|
|
||||||
|
# Connect to remote browser service
|
||||||
|
agent-browser connect "wss://browser-service.example.com/cdp?token=xyz"
|
||||||
|
|
||||||
|
# After connecting, run commands normally
|
||||||
|
agent-browser snapshot
|
||||||
|
agent-browser click @e1
|
||||||
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
println!("{}", help.trim());
|
println!("{}", help.trim());
|
||||||
@@ -1351,7 +1391,7 @@ Core Commands:
|
|||||||
pdf <path> Save as PDF
|
pdf <path> Save as PDF
|
||||||
snapshot Accessibility tree with refs (for AI)
|
snapshot Accessibility tree with refs (for AI)
|
||||||
eval <js> Run JavaScript
|
eval <js> Run JavaScript
|
||||||
connect <port> Connect to browser via CDP (e.g., connect 9222)
|
connect <port|url> Connect to browser via CDP
|
||||||
close Close browser
|
close Close browser
|
||||||
|
|
||||||
Navigation:
|
Navigation:
|
||||||
|
|||||||
Reference in New Issue
Block a user