use crate::api::schema::{EmptyParams, Method, Request, ServerLiveHandoffParams}; pub(super) fn run_server_command(args: &[String]) -> std::io::Result> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { return Ok(None); }; match subcommand { "stop" => server_stop(&args[1..]).map(Some), "live-handoff" => server_live_handoff(&args[1..]).map(Some), "--handoff-import" => Ok(None), "reload-config" => server_reload_config(&args[1..]).map(Some), "agent-manifests" => server_agent_manifests(&args[1..]).map(Some), "reload-agent-manifests" => server_reload_agent_manifests(&args[1..]).map(Some), "help" | "--help" | "-h" => { print_server_help(); Ok(Some(0)) } _ => { print_server_help(); Ok(Some(2)) } } } fn server_stop(args: &[String]) -> std::io::Result { if !args.is_empty() { eprintln!("usage: herdr server stop"); return Ok(2); } super::send_ok_request(Method::ServerStop(EmptyParams::default())) } fn server_reload_config(args: &[String]) -> std::io::Result { if !args.is_empty() { eprintln!("usage: herdr server reload-config"); return Ok(2); } super::print_response(&super::send_request(&Request { id: "cli:server:reload-config".into(), method: Method::ServerReloadConfig(EmptyParams::default()), })?) } fn server_agent_manifests(args: &[String]) -> std::io::Result { let json = match args { [] => false, [flag] if flag == "--json" => true, _ => { eprintln!("usage: herdr server agent-manifests [--json]"); return Ok(2); } }; let response = super::send_request(&Request { id: "cli:server:agent-manifests".into(), method: Method::ServerAgentManifests(EmptyParams::default()), })?; if json || response.get("error").is_some() { return super::print_response(&response); } print_agent_manifest_status(&response); Ok(0) } fn server_reload_agent_manifests(args: &[String]) -> std::io::Result { if !args.is_empty() { eprintln!("usage: herdr server reload-agent-manifests"); return Ok(2); } super::print_response(&super::send_request(&Request { id: "cli:server:reload-agent-manifests".into(), method: Method::ServerReloadAgentManifests(EmptyParams::default()), })?) } fn print_agent_manifest_status(response: &serde_json::Value) { let result = &response["result"]; let last_check = result["last_check_unix"] .as_u64() .map(|value| value.to_string()) .unwrap_or_else(|| "never".to_string()); let last_result = result["last_result"].as_str().unwrap_or("not checked"); println!("last check: {last_check}"); println!("result: {last_result}"); println!(); let Some(manifests) = result["manifests"].as_array() else { return; }; for manifest in manifests { let agent = manifest["agent"].as_str().unwrap_or("-"); let source = manifest["source_kind"].as_str().unwrap_or("-"); let active_version = manifest["active_version"].as_str().unwrap_or("-"); let remote_version = manifest["cached_remote_version"].as_str().unwrap_or("-"); let remote_result = manifest["remote_update_result"] .as_str() .unwrap_or("not checked"); let local_override_shadowing_remote = manifest["local_override_shadowing_remote"] .as_bool() .unwrap_or(false); let marker = if local_override_shadowing_remote { "!" } else if manifest["remote_update_error"].as_str().is_some() { "x" } else { " " }; println!( "{marker} {agent:<9} {source:<14} active {active_version:<14} remote {remote_version:<14} {remote_result}" ); if let Some(error) = manifest["remote_update_error"].as_str() { println!(" {error}"); } else if local_override_shadowing_remote { println!(" local override shadows cached remote rules"); } else if let Some(warning) = manifest["warning"].as_str() { println!(" {warning}"); } } } fn server_live_handoff(args: &[String]) -> std::io::Result { let Some(params) = parse_live_handoff_params(args) else { eprintln!( "usage: herdr server live-handoff [--import-exe ] [--expected-protocol ] [--expected-version ]" ); return Ok(2); }; let response = super::send_request(&Request { id: "cli:server:live-handoff".into(), method: Method::ServerLiveHandoff(params), })?; if response.get("error").is_some() { let rendered = serde_json::to_string(&response).unwrap_or_else(|err| { format!( "{{\"error\":{{\"code\":\"render_failed\",\"message\":\"failed to render error response: {err}\"}}}}" ) }); eprintln!("{rendered}"); return Ok(1); } eprintln!( "live handoff complete; server log: {}", crate::session::data_dir() .join("herdr-server.log") .display() ); Ok(0) } fn parse_live_handoff_params(args: &[String]) -> Option { let mut params = ServerLiveHandoffParams::default(); let mut idx = 0; while idx < args.len() { let arg = &args[idx]; let (flag, value) = if let Some((flag, value)) = arg.split_once('=') { (flag, Some(value.to_string())) } else { let value = args.get(idx + 1).cloned(); idx += 1; (arg.as_str(), value) }; let value = value?; match flag { "--import-exe" => params.import_exe = Some(value), "--expected-protocol" => { params.expected_protocol = Some(value.parse().ok()?); } "--expected-version" => params.expected_version = Some(value), _ => return None, } idx += 1; } Some(params) } fn print_server_help() { eprintln!("herdr server commands:"); eprintln!(" herdr server run as headless server"); eprintln!(" herdr server stop stop the running server via the API socket"); eprintln!(" herdr server live-handoff hand off live panes to a new local server"); eprintln!(" herdr server reload-config reload config.toml in the running server"); eprintln!(" herdr server agent-manifests [--json] show agent detection manifest status"); eprintln!(" herdr server reload-agent-manifests reload agent detection manifests in the running server"); } #[cfg(test)] mod tests { use super::*; #[test] fn live_handoff_params_parse_remote_update_fields() { let args = vec![ "--import-exe".to_string(), "/home/me/.local/bin/herdr".to_string(), "--expected-protocol=9".to_string(), "--expected-version".to_string(), "0.6.2".to_string(), ]; let params = parse_live_handoff_params(&args).expect("params"); assert_eq!( params.import_exe.as_deref(), Some("/home/me/.local/bin/herdr") ); assert_eq!(params.expected_protocol, Some(9)); assert_eq!(params.expected_version.as_deref(), Some("0.6.2")); } }