diff --git a/cli/src/main.rs b/cli/src/main.rs index 448add0..3fb8d29 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -303,6 +303,31 @@ fn main() { } } + // Handle state management commands locally — these are pure file operations + // that don't need a daemon, avoiding an unnecessary daemon startup that + // would lack runtime config like session_name. + if let Some(result) = native::state::dispatch_state_command(&cmd) { + let action = cmd.get("action").and_then(|v| v.as_str()); + let resp = match result { + Ok(data) => connection::Response { + success: true, + data: Some(data), + error: None, + }, + Err(e) => connection::Response { + success: false, + data: None, + error: Some(e), + }, + }; + let output_opts = OutputOptions::from_flags(&flags); + output::print_response_with_opts(&resp, action, &output_opts); + if !resp.success { + exit(1); + } + return; + } + let daemon_opts = DaemonOptions { headed: flags.headed, debug: flags.debug, @@ -744,11 +769,7 @@ fn main() { return; } - let output_opts = OutputOptions { - json: flags.json, - content_boundaries: flags.content_boundaries, - max_output: flags.max_output, - }; + let output_opts = OutputOptions::from_flags(&flags); match send_command(cmd.clone(), &flags.session) { Ok(resp) => { @@ -863,11 +884,7 @@ fn run_batch(flags: &Flags, bail: bool) { return; } - let output_opts = OutputOptions { - json: flags.json, - content_boundaries: flags.content_boundaries, - max_output: flags.max_output, - }; + let output_opts = OutputOptions::from_flags(flags); let mut results: Vec = Vec::new(); let mut had_error = false; diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index e4ed9b1..b05c521 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -947,11 +947,10 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { "errors" => handle_errors(state).await, "state_save" => handle_state_save(cmd, state).await, "state_load" => handle_state_load(cmd, state).await, - "state_list" => handle_state_list().await, - "state_show" => handle_state_show(cmd).await, - "state_clear" => handle_state_clear(cmd).await, - "state_clean" => handle_state_clean(cmd).await, - "state_rename" => handle_state_rename(cmd).await, + "state_list" | "state_show" | "state_clear" | "state_clean" | "state_rename" => { + state::dispatch_state_command(cmd) + .expect("dispatch_state_command must handle all state_* actions matched here") + } "trace_start" => handle_trace_start(state).await, "trace_stop" => handle_trace_stop(cmd, state).await, "profiler_start" => handle_profiler_start(cmd, state).await, @@ -2671,40 +2670,6 @@ async fn handle_state_load(cmd: &Value, state: &DaemonState) -> Result Result { - state::state_list() -} - -async fn handle_state_show(cmd: &Value) -> Result { - let path = cmd - .get("path") - .and_then(|v| v.as_str()) - .ok_or("Missing 'path' parameter")?; - state::state_show(path) -} - -async fn handle_state_clear(cmd: &Value) -> Result { - let path = cmd.get("path").and_then(|v| v.as_str()); - state::state_clear(path) -} - -async fn handle_state_clean(cmd: &Value) -> Result { - let days = cmd.get("days").and_then(|v| v.as_u64()).unwrap_or(30); - state::state_clean(days) -} - -async fn handle_state_rename(cmd: &Value) -> Result { - let path = cmd - .get("path") - .and_then(|v| v.as_str()) - .ok_or("Missing 'path' parameter")?; - let name = cmd - .get("name") - .and_then(|v| v.as_str()) - .ok_or("Missing 'name' parameter")?; - state::state_rename(path, name) -} - // --------------------------------------------------------------------------- // Phase 6 handlers // --------------------------------------------------------------------------- diff --git a/cli/src/native/state.rs b/cli/src/native/state.rs index 9821f9d..3aaf14a 100644 --- a/cli/src/native/state.rs +++ b/cli/src/native/state.rs @@ -475,6 +475,41 @@ pub fn find_auto_state_file(session_name: &str) -> Option { best_path.map(|(p, _)| p) } +/// Dispatch a state management command from its JSON payload. +/// Returns `Some(result)` for recognised state_* actions, `None` otherwise. +pub fn dispatch_state_command(cmd: &Value) -> Option> { + let action = cmd.get("action").and_then(|v| v.as_str())?; + match action { + "state_list" => Some(state_list()), + "state_show" => Some( + cmd.get("path") + .and_then(|v| v.as_str()) + .ok_or_else(|| "Missing 'path' parameter".to_string()) + .and_then(state_show), + ), + "state_clear" => { + let path = cmd.get("path").and_then(|v| v.as_str()); + Some(state_clear(path)) + } + "state_clean" => { + let days = cmd.get("days").and_then(|v| v.as_u64()).unwrap_or(30); + Some(state_clean(days)) + } + "state_rename" => Some( + cmd.get("path") + .and_then(|v| v.as_str()) + .ok_or_else(|| "Missing 'path' parameter".to_string()) + .and_then(|path| { + cmd.get("name") + .and_then(|v| v.as_str()) + .ok_or_else(|| "Missing 'name' parameter".to_string()) + .and_then(|name| state_rename(path, name)) + }), + ), + _ => None, + } +} + pub fn get_sessions_dir() -> PathBuf { if let Some(home) = dirs::home_dir() { home.join(".agent-browser").join("sessions") @@ -604,4 +639,45 @@ mod tests { assert_eq!(json["secure"], true); assert_eq!(json["sameSite"], "Strict"); } + + #[test] + fn test_dispatch_state_command_routes_state_list() { + let cmd = serde_json::json!({ "action": "state_list" }); + let result = dispatch_state_command(&cmd); + assert!(result.is_some()); + assert!(result.unwrap().is_ok()); + } + + #[test] + fn test_dispatch_state_command_returns_none_for_unknown() { + let cmd = serde_json::json!({ "action": "navigate" }); + assert!(dispatch_state_command(&cmd).is_none()); + } + + #[test] + fn test_dispatch_state_command_returns_none_for_missing_action() { + let cmd = serde_json::json!({}); + assert!(dispatch_state_command(&cmd).is_none()); + } + + #[test] + fn test_dispatch_state_show_missing_path() { + let cmd = serde_json::json!({ "action": "state_show" }); + let result = dispatch_state_command(&cmd).unwrap(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Missing 'path' parameter"); + } + + #[test] + fn test_dispatch_state_rename_missing_params() { + let cmd = serde_json::json!({ "action": "state_rename" }); + let result = dispatch_state_command(&cmd).unwrap(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Missing 'path' parameter"); + + let cmd = serde_json::json!({ "action": "state_rename", "path": "/tmp/test.json" }); + let result = dispatch_state_command(&cmd).unwrap(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Missing 'name' parameter"); + } } diff --git a/cli/src/output.rs b/cli/src/output.rs index 67c3b02..37b80f4 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -23,6 +23,16 @@ pub struct OutputOptions { pub max_output: Option, } +impl OutputOptions { + pub fn from_flags(flags: &crate::flags::Flags) -> Self { + Self { + json: flags.json, + content_boundaries: flags.content_boundaries, + max_output: flags.max_output, + } + } +} + fn truncate_if_needed(content: &str, max: Option) -> String { let Some(limit) = max else { return content.to_string();