use serde_json::{json, Value}; use crate::flags::Flags; /// Error type for command parsing with contextual information #[derive(Debug)] pub enum ParseError { /// Command does not exist UnknownCommand { command: String }, /// Command exists but subcommand is invalid UnknownSubcommand { subcommand: String, valid_options: &'static [&'static str], }, /// Command/subcommand exists but required arguments are missing MissingArguments { context: String, usage: &'static str, }, } impl ParseError { pub fn format(&self) -> String { match self { ParseError::UnknownCommand { command } => { format!("Unknown command: {}", command) } ParseError::UnknownSubcommand { subcommand, valid_options, } => { format!( "Unknown subcommand: {}\nValid options: {}", subcommand, valid_options.join(", ") ) } ParseError::MissingArguments { context, usage } => { format!( "Missing arguments for: {}\nUsage: agent-browser {}", context, usage ) } } } } pub fn gen_id() -> String { format!( "r{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_micros() % 1000000 ) } pub fn parse_command(args: &[String], flags: &Flags) -> Result { if args.is_empty() { return Err(ParseError::MissingArguments { context: "".to_string(), usage: " [args...]", }); } let cmd = args[0].as_str(); let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect(); let id = gen_id(); match cmd { // === Navigation === "open" | "goto" | "navigate" => { let url = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: cmd.to_string(), usage: "open ", })?; let url = if url.starts_with("http") { url.to_string() } else { format!("https://{}", url) }; let mut nav_cmd = json!({ "id": id, "action": "navigate", "url": url }); // If --headers flag is set, include headers (scoped to this origin) if let Some(ref headers_json) = flags.headers { if let Ok(headers) = serde_json::from_str::(headers_json) { nav_cmd["headers"] = headers; } } Ok(nav_cmd) } "back" => Ok(json!({ "id": id, "action": "back" })), "forward" => Ok(json!({ "id": id, "action": "forward" })), "reload" => Ok(json!({ "id": id, "action": "reload" })), // === Core Actions === "click" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "click".to_string(), usage: "click ", })?; Ok(json!({ "id": id, "action": "click", "selector": sel })) } "dblclick" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "dblclick".to_string(), usage: "dblclick ", })?; Ok(json!({ "id": id, "action": "dblclick", "selector": sel })) } "fill" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "fill".to_string(), usage: "fill ", })?; Ok(json!({ "id": id, "action": "fill", "selector": sel, "value": rest[1..].join(" ") })) } "type" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "type".to_string(), usage: "type ", })?; Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") })) } "hover" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "hover".to_string(), usage: "hover ", })?; Ok(json!({ "id": id, "action": "hover", "selector": sel })) } "focus" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "focus".to_string(), usage: "focus ", })?; Ok(json!({ "id": id, "action": "focus", "selector": sel })) } "check" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "check".to_string(), usage: "check ", })?; Ok(json!({ "id": id, "action": "check", "selector": sel })) } "uncheck" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "uncheck".to_string(), usage: "uncheck ", })?; Ok(json!({ "id": id, "action": "uncheck", "selector": sel })) } "select" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "select".to_string(), usage: "select ", })?; let val = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "select".to_string(), usage: "select ", })?; Ok(json!({ "id": id, "action": "select", "selector": sel, "value": val })) } "drag" => { let src = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "drag".to_string(), usage: "drag ", })?; let tgt = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "drag".to_string(), usage: "drag ", })?; Ok(json!({ "id": id, "action": "drag", "source": src, "target": tgt })) } "upload" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "upload".to_string(), usage: "upload ", })?; Ok(json!({ "id": id, "action": "upload", "selector": sel, "files": &rest[1..] })) } // === Keyboard === "press" | "key" => { let key = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "press".to_string(), usage: "press ", })?; Ok(json!({ "id": id, "action": "press", "key": key })) } "keydown" => { let key = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "keydown".to_string(), usage: "keydown ", })?; Ok(json!({ "id": id, "action": "keydown", "key": key })) } "keyup" => { let key = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "keyup".to_string(), usage: "keyup ", })?; Ok(json!({ "id": id, "action": "keyup", "key": key })) } // === Scroll === "scroll" => { let dir = rest.get(0).unwrap_or(&"down"); let amount = rest.get(1).and_then(|s| s.parse::().ok()).unwrap_or(300); Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount })) } "scrollintoview" | "scrollinto" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "scrollintoview".to_string(), usage: "scrollintoview ", })?; Ok(json!({ "id": id, "action": "scrollintoview", "selector": sel })) } // === Wait === "wait" => { // Check for --url flag: wait --url "**/dashboard" if let Some(idx) = rest.iter().position(|&s| s == "--url" || s == "-u") { let url = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { context: "wait --url".to_string(), usage: "wait --url ", })?; return Ok(json!({ "id": id, "action": "waitforurl", "url": url })); } // Check for --load flag: wait --load networkidle if let Some(idx) = rest.iter().position(|&s| s == "--load" || s == "-l") { let state = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { context: "wait --load".to_string(), usage: "wait --load ", })?; return Ok(json!({ "id": id, "action": "waitforloadstate", "state": state })); } // Check for --fn flag: wait --fn "window.ready === true" if let Some(idx) = rest.iter().position(|&s| s == "--fn" || s == "-f") { let expr = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { context: "wait --fn".to_string(), usage: "wait --fn ", })?; return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr })); } // Check for --text flag: wait --text "Welcome" if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") { let text = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments { context: "wait --text".to_string(), usage: "wait --text ", })?; // Use getByText locator to wait for text to appear return Ok(json!({ "id": id, "action": "wait", "selector": format!("text={}", text) })); } // Default: selector or timeout if let Some(arg) = rest.get(0) { if arg.parse::().is_ok() { Ok(json!({ "id": id, "action": "wait", "timeout": arg.parse::().unwrap() })) } else { Ok(json!({ "id": id, "action": "wait", "selector": arg })) } } else { Err(ParseError::MissingArguments { context: "wait".to_string(), usage: "wait ", }) } } // === Screenshot/PDF === "screenshot" => { let mut cmd = json!({ "id": id, "action": "screenshot", "fullPage": flags.full }); if let Some(path) = rest.get(0) { cmd["path"] = json!(path); } Ok(cmd) } "pdf" => { let path = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "pdf".to_string(), usage: "pdf ", })?; Ok(json!({ "id": id, "action": "pdf", "path": path })) } // === Snapshot === "snapshot" => { let mut cmd = json!({ "id": id, "action": "snapshot" }); let obj = cmd.as_object_mut().unwrap(); let mut i = 0; while i < rest.len() { match rest[i] { "-i" | "--interactive" => { obj.insert("interactive".to_string(), json!(true)); } "-c" | "--compact" => { obj.insert("compact".to_string(), json!(true)); } "-d" | "--depth" => { if let Some(d) = rest.get(i + 1) { if let Ok(n) = d.parse::() { obj.insert("maxDepth".to_string(), json!(n)); i += 1; } } } "-s" | "--selector" => { if let Some(s) = rest.get(i + 1) { obj.insert("selector".to_string(), json!(s)); i += 1; } } _ => {} } i += 1; } Ok(cmd) } // === Eval === "eval" => Ok(json!({ "id": id, "action": "evaluate", "script": rest.join(" ") })), // === Close === "close" | "quit" | "exit" => Ok(json!({ "id": id, "action": "close" })), // === Get === "get" => parse_get(&rest, &id), // === Is (state checks) === "is" => parse_is(&rest, &id), // === Find (locators) === "find" => parse_find(&rest, &id), // === Mouse === "mouse" => parse_mouse(&rest, &id), // === Set (browser settings) === "set" => parse_set(&rest, &id), // === Network === "network" => parse_network(&rest, &id), // === Storage === "storage" => parse_storage(&rest, &id), // === Cookies === "cookies" => { let op = rest.get(0).unwrap_or(&"get"); match *op { "set" => { let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "cookies set".to_string(), usage: "cookies set ", })?; let value = rest.get(2).ok_or_else(|| ParseError::MissingArguments { context: "cookies set".to_string(), usage: "cookies set ", })?; Ok(json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] })) } "clear" => Ok(json!({ "id": id, "action": "cookies_clear" })), _ => Ok(json!({ "id": id, "action": "cookies_get" })), } } // === 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) } Some("list") => Ok(json!({ "id": id, "action": "tab_list" })), Some("close") => { Ok(json!({ "id": id, "action": "tab_close", "index": rest.get(1).and_then(|s| s.parse::().ok()) })) } Some(n) if n.parse::().is_ok() => { Ok(json!({ "id": id, "action": "tab_switch", "index": n.parse::().unwrap() })) } _ => Ok(json!({ "id": id, "action": "tab_list" })), } } // === Window === "window" => { const VALID: &[&str] = &["new"]; match rest.get(0).map(|s| *s) { Some("new") => Ok(json!({ "id": id, "action": "window_new" })), Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "window".to_string(), usage: "window ", }), } } // === Frame === "frame" => { if rest.get(0).map(|s| *s) == Some("main") { Ok(json!({ "id": id, "action": "frame_main" })) } else { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "frame".to_string(), usage: "frame ", })?; Ok(json!({ "id": id, "action": "frame", "selector": sel })) } } // === Dialog === "dialog" => { const VALID: &[&str] = &["accept", "dismiss"]; match rest.get(0).map(|s| *s) { Some("accept") => { Ok(json!({ "id": id, "action": "dialog", "response": "accept", "promptText": rest.get(1) })) } Some("dismiss") => Ok(json!({ "id": id, "action": "dialog", "response": "dismiss" })), Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "dialog".to_string(), usage: "dialog [text]", }), } } // === Debug === "trace" => { const VALID: &[&str] = &["start", "stop"]; match rest.get(0).map(|s| *s) { Some("start") => Ok(json!({ "id": id, "action": "trace_start", "path": rest.get(1) })), Some("stop") => Ok(json!({ "id": id, "action": "trace_stop", "path": rest.get(1) })), Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "trace".to_string(), usage: "trace [path]", }), } } // === Recording (Playwright native video recording) === "record" => { const VALID: &[&str] = &["start", "stop", "restart"]; match rest.get(0).map(|s| *s) { Some("start") => { let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "record start".to_string(), usage: "record start [url]", })?; // Optional URL parameter let url = rest.get(2); let mut cmd = json!({ "id": id, "action": "recording_start", "path": path }); if let Some(u) = url { // Add https:// prefix if needed let url_str = if u.starts_with("http") { u.to_string() } else { format!("https://{}", u) }; cmd["url"] = json!(url_str); } Ok(cmd) } Some("stop") => Ok(json!({ "id": id, "action": "recording_stop" })), Some("restart") => { let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "record restart".to_string(), usage: "record restart [url]", })?; // Optional URL parameter let url = rest.get(2); let mut cmd = json!({ "id": id, "action": "recording_restart", "path": path }); if let Some(u) = url { // Add https:// prefix if needed let url_str = if u.starts_with("http") { u.to_string() } else { format!("https://{}", u) }; cmd["url"] = json!(url_str); } Ok(cmd) } Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "record".to_string(), usage: "record [path] [url]", }), } } "console" => { let clear = rest.iter().any(|&s| s == "--clear"); Ok(json!({ "id": id, "action": "console", "clear": clear })) } "errors" => { let clear = rest.iter().any(|&s| s == "--clear"); Ok(json!({ "id": id, "action": "errors", "clear": clear })) } "highlight" => { let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "highlight".to_string(), usage: "highlight ", })?; Ok(json!({ "id": id, "action": "highlight", "selector": sel })) } // === State === "state" => { const VALID: &[&str] = &["save", "load"]; match rest.get(0).map(|s| *s) { Some("save") => { let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "state save".to_string(), usage: "state save ", })?; Ok(json!({ "id": id, "action": "state_save", "path": path })) } Some("load") => { let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "state load".to_string(), usage: "state load ", })?; Ok(json!({ "id": id, "action": "state_load", "path": path })) } Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "state".to_string(), usage: "state ", }), } } _ => Err(ParseError::UnknownCommand { command: cmd.to_string(), }), } } fn parse_get(rest: &[&str], id: &str) -> Result { 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 { context: "get text".to_string(), usage: "get text ", })?; Ok(json!({ "id": id, "action": "gettext", "selector": sel })) } Some("html") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "get html".to_string(), usage: "get html ", })?; Ok(json!({ "id": id, "action": "innerhtml", "selector": sel })) } Some("value") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "get value".to_string(), usage: "get value ", })?; Ok(json!({ "id": id, "action": "inputvalue", "selector": sel })) } Some("attr") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "get attr".to_string(), usage: "get attr ", })?; let attr = rest.get(2).ok_or_else(|| ParseError::MissingArguments { context: "get attr".to_string(), usage: "get attr ", })?; Ok(json!({ "id": id, "action": "getattribute", "selector": sel, "attribute": attr })) } Some("url") => Ok(json!({ "id": id, "action": "url" })), Some("title") => Ok(json!({ "id": id, "action": "title" })), Some("count") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "get count".to_string(), usage: "get count ", })?; Ok(json!({ "id": id, "action": "count", "selector": sel })) } Some("box") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "get box".to_string(), usage: "get box ", })?; Ok(json!({ "id": id, "action": "boundingbox", "selector": sel })) } Some("styles") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "get styles".to_string(), usage: "get styles ", })?; Ok(json!({ "id": id, "action": "styles", "selector": sel })) } Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "get".to_string(), usage: "get [args...]", }), } } fn parse_is(rest: &[&str], id: &str) -> Result { const VALID: &[&str] = &["visible", "enabled", "checked"]; match rest.get(0).map(|s| *s) { Some("visible") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "is visible".to_string(), usage: "is visible ", })?; Ok(json!({ "id": id, "action": "isvisible", "selector": sel })) } Some("enabled") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "is enabled".to_string(), usage: "is enabled ", })?; Ok(json!({ "id": id, "action": "isenabled", "selector": sel })) } Some("checked") => { let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "is checked".to_string(), usage: "is checked ", })?; Ok(json!({ "id": id, "action": "ischecked", "selector": sel })) } Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "is".to_string(), usage: "is ", }), } } fn parse_find(rest: &[&str], id: &str) -> Result { const VALID: &[&str] = &["role", "text", "label", "placeholder", "alt", "title", "testid", "first", "last", "nth"]; let locator = rest.get(0).ok_or_else(|| ParseError::MissingArguments { context: "find".to_string(), usage: "find [action] [text]", })?; let name_idx = rest.iter().position(|&s| s == "--name"); let name = name_idx.and_then(|i| rest.get(i + 1).map(|s| *s)); let exact = rest.iter().any(|&s| s == "--exact"); match *locator { "role" | "text" | "label" | "placeholder" | "alt" | "title" | "testid" | "first" | "last" => { let value = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: format!("find {}", locator), usage: match *locator { "role" => "find role [action] [--name ] [--exact]", "text" => "find text [action] [--exact]", "label" => "find label