use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::time::Duration; use serde::Serialize; use crate::api; use crate::api::schema::{ AgentStatus, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneListParams, PaneReadParams, PaneRenameParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams, PingParams, ReadFormat, ReadSource, Request, SplitDirection, Subscription, TabCreateParams, TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget, }; pub enum CommandOutcome { Handled(i32), NotCli, } pub fn maybe_run(args: &[String]) -> std::io::Result { let Some(command) = args.get(1).map(|arg| arg.as_str()) else { return Ok(CommandOutcome::NotCli); }; let exit_code = match command { "server" => { let Some(exit_code) = run_server_command(&args[2..])? else { return Ok(CommandOutcome::NotCli); }; exit_code } "status" => run_status_command(&args[2..])?, "workspace" => run_workspace_command(&args[2..])?, "tab" => run_tab_command(&args[2..])?, "pane" => run_pane_command(&args[2..])?, "wait" => run_wait_command(&args[2..])?, "integration" => run_integration_command(&args[2..])?, "session" => run_session_command(&args[2..])?, _ => return Ok(CommandOutcome::NotCli), }; Ok(CommandOutcome::Handled(exit_code)) } 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), "reload-config" => server_reload_config(&args[1..]).map(Some), "help" | "--help" | "-h" => { print_server_help(); Ok(Some(0)) } _ => { print_server_help(); Ok(Some(2)) } } } fn run_status_command(args: &[String]) -> std::io::Result { match args.first().map(|arg| arg.as_str()) { None => print_full_status(), Some("server") => { if args.len() > 1 { eprintln!("usage: herdr status server"); return Ok(2); } print_server_status() } Some("client") => { if args.len() > 1 { eprintln!("usage: herdr status client"); return Ok(2); } print_client_status(); Ok(0) } Some("help" | "--help" | "-h") => { print_status_help(); Ok(0) } Some(_) => { print_status_help(); Ok(2) } } } #[derive(Debug, Clone, PartialEq, Eq)] enum ServerRuntimeStatus { Running { version: Option, protocol: Option, }, NotRunning, } fn print_full_status() -> std::io::Result { let server = read_server_runtime_status()?; println!("client:"); println!(" version: {}", env!("CARGO_PKG_VERSION")); println!(" protocol: {}", crate::server::protocol::PROTOCOL_VERSION); println!(); println!("server:"); print_server_status_body(&server, " "); println!(); println!("update:"); println!(" restart_needed: {}", restart_needed_label(&server)); Ok(0) } fn print_server_status() -> std::io::Result { let server = read_server_runtime_status()?; print_server_status_body(&server, ""); Ok(0) } fn print_client_status() { println!("version: {}", env!("CARGO_PKG_VERSION")); println!("protocol: {}", crate::server::protocol::PROTOCOL_VERSION); println!("binary: {}", current_exe_label()); } fn print_server_status_body(server: &ServerRuntimeStatus, indent: &str) { match server { ServerRuntimeStatus::Running { version, protocol } => { println!("{indent}status: running"); println!("{indent}version: {}", option_label(version.as_deref())); println!("{indent}protocol: {}", protocol_label(*protocol)); println!("{indent}compatible: {}", compatibility_label(*protocol)); println!("{indent}socket: {}", api::socket_path().display()); } ServerRuntimeStatus::NotRunning => { println!("{indent}status: not running"); println!("{indent}socket: {}", api::socket_path().display()); } } } fn read_server_runtime_status() -> std::io::Result { match send_request(&Request { id: "cli:status:server".into(), method: Method::Ping(PingParams::default()), }) { Ok(response) => { if response.get("error").is_some() { return Err(std::io::Error::other(format!( "server status request failed: {}", response ))); } let result = &response["result"]; Ok(ServerRuntimeStatus::Running { version: result .get("version") .and_then(|value| value.as_str()) .map(str::to_owned), protocol: result .get("protocol") .and_then(|value| value.as_u64()) .and_then(|value| u32::try_from(value).ok()), }) } Err(err) if server_not_running_error(&err) => Ok(ServerRuntimeStatus::NotRunning), Err(err) => Err(err), } } fn server_not_running_error(err: &std::io::Error) -> bool { matches!( err.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused ) } fn option_label(value: Option<&str>) -> &str { value.unwrap_or("unknown") } fn protocol_label(protocol: Option) -> String { protocol .map(|value| value.to_string()) .unwrap_or_else(|| "unknown".to_string()) } fn compatibility_label(protocol: Option) -> &'static str { match protocol { Some(protocol) if protocol == crate::server::protocol::PROTOCOL_VERSION => "yes", Some(_) => "no", None => "unknown", } } fn restart_needed_label(server: &ServerRuntimeStatus) -> &'static str { match server { ServerRuntimeStatus::Running { version, .. } => match version.as_deref() { Some(env!("CARGO_PKG_VERSION")) => "no", Some(_) => "yes", None => "unknown", }, ServerRuntimeStatus::NotRunning => "no", } } fn current_exe_label() -> String { std::env::current_exe() .map(|path| path.display().to_string()) .unwrap_or_else(|err| format!("unknown ({err})")) } fn run_workspace_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_workspace_help(); return Ok(2); }; match subcommand { "list" => workspace_list(&args[1..]), "create" => workspace_create(&args[1..]), "get" => workspace_get(&args[1..]), "focus" => workspace_focus(&args[1..]), "rename" => workspace_rename(&args[1..]), "close" => workspace_close(&args[1..]), "help" | "--help" | "-h" => { print_workspace_help(); Ok(0) } _ => { print_workspace_help(); Ok(2) } } } fn run_tab_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_tab_help(); return Ok(2); }; match subcommand { "list" => tab_list(&args[1..]), "create" => tab_create(&args[1..]), "get" => tab_get(&args[1..]), "focus" => tab_focus(&args[1..]), "rename" => tab_rename(&args[1..]), "close" => tab_close(&args[1..]), "help" | "--help" | "-h" => { print_tab_help(); Ok(0) } _ => { print_tab_help(); Ok(2) } } } fn run_pane_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_pane_help(); return Ok(2); }; match subcommand { "list" => pane_list(&args[1..]), "get" => pane_get(&args[1..]), "read" => pane_read(&args[1..]), "rename" => pane_rename(&args[1..]), "split" => pane_split(&args[1..]), "close" => pane_close(&args[1..]), "send-text" => pane_send_text(&args[1..]), "send-keys" => pane_send_keys(&args[1..]), "run" => pane_run(&args[1..]), "help" | "--help" | "-h" => { print_pane_help(); Ok(0) } _ => { print_pane_help(); Ok(2) } } } fn run_wait_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_wait_help(); return Ok(2); }; match subcommand { "output" => wait_output(&args[1..]), "agent-status" => wait_agent_status(&args[1..]), "help" | "--help" | "-h" => { print_wait_help(); Ok(0) } _ => { print_wait_help(); Ok(2) } } } fn run_integration_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_integration_help(); return Ok(2); }; match subcommand { "install" => integration_install(&args[1..]), "uninstall" => integration_uninstall(&args[1..]), "status" => integration_status(&args[1..]), "help" | "--help" | "-h" => { print_integration_help(); Ok(0) } _ => { print_integration_help(); Ok(2) } } } fn run_session_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_session_help(); return Ok(2); }; match subcommand { "list" => session_list(&args[1..]), "attach" => session_attach_help(&args[1..]), "stop" => session_stop(&args[1..]), "delete" => session_delete(&args[1..]), "help" | "--help" | "-h" => { print_session_help(); Ok(0) } _ => { print_session_help(); Ok(2) } } } fn server_stop(args: &[String]) -> std::io::Result { if !args.is_empty() { eprintln!("usage: herdr server stop"); return Ok(2); } 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); } print_response(&send_request(&Request { id: "cli:server:reload-config".into(), method: Method::ServerReloadConfig(EmptyParams::default()), })?) } fn session_attach_help(args: &[String]) -> std::io::Result { if matches!( args.first().map(String::as_str), Some("help" | "--help" | "-h") ) { eprintln!("usage: herdr session attach "); return Ok(0); } eprintln!("usage: herdr session attach "); Ok(2) } fn session_list(args: &[String]) -> std::io::Result { let json = match parse_session_json_only(args, "usage: herdr session list [--json]") { Ok(json) => json, Err(code) => return Ok(code), }; let sessions = crate::session::list_sessions()?; if json { _print_json(&serde_json::json!({ "sessions": sessions, })); } else { print_session_table(&sessions); } Ok(0) } fn session_stop(args: &[String]) -> std::io::Result { let (name, json) = match parse_session_name_and_json(args, "usage: herdr session stop [--json]") { Ok(parsed) => parsed, Err(code) => return Ok(code), }; let target = match crate::session::parse_target_name(&name) { Ok(target) => target, Err(message) => { print_session_error("invalid_session_name", &message); return Ok(1); } }; match crate::session::stop_session(target.as_deref()) { Ok(session) => { if json { _print_json(&serde_json::json!({ "stopped": true, "session": session, })); } else { println!("stopped session {}", session.name); } Ok(0) } Err(message) => { print_session_error("session_stop_failed", &message); Ok(1) } } } fn session_delete(args: &[String]) -> std::io::Result { let (name, json) = match parse_session_name_and_json(args, "usage: herdr session delete [--json]") { Ok(parsed) => parsed, Err(code) => return Ok(code), }; match crate::session::delete_session(&name) { Ok(session) => { if json { _print_json(&serde_json::json!({ "deleted": true, "session": session, })); } else { println!("deleted session {}", session.name); } Ok(0) } Err(message) => { print_session_error("session_delete_failed", &message); Ok(1) } } } fn workspace_list(args: &[String]) -> std::io::Result { if !args.is_empty() { eprintln!("usage: herdr workspace list"); return Ok(2); } print_response(&send_request(&Request { id: "cli:workspace:list".into(), method: Method::WorkspaceList(EmptyParams::default()), })?) } fn workspace_create(args: &[String]) -> std::io::Result { let mut cwd = None; let mut focus = false; let mut label = None; let mut index = 0; while index < args.len() { match args[index].as_str() { "--cwd" => { let Some(value) = args.get(index + 1) else { eprintln!("missing value for --cwd"); return Ok(2); }; cwd = Some(value.clone()); index += 2; } "--label" => { let Some(value) = args.get(index + 1) else { eprintln!("missing value for --label"); return Ok(2); }; label = Some(value.clone()); index += 2; } "--focus" => { focus = true; index += 1; } "--no-focus" => { focus = false; index += 1; } other => { eprintln!("unknown option: {other}"); return Ok(2); } } } print_response(&send_request(&Request { id: "cli:workspace:create".into(), method: Method::WorkspaceCreate(WorkspaceCreateParams { cwd, focus, label }), })?) } fn workspace_get(args: &[String]) -> std::io::Result { let Some(raw_workspace_id) = args.first() else { eprintln!("usage: herdr workspace get "); return Ok(2); }; if args.len() != 1 { eprintln!("usage: herdr workspace get "); return Ok(2); } print_response(&send_request(&Request { id: "cli:workspace:get".into(), method: Method::WorkspaceGet(WorkspaceTarget { workspace_id: normalize_workspace_id(raw_workspace_id), }), })?) } fn workspace_focus(args: &[String]) -> std::io::Result { let Some(raw_workspace_id) = args.first() else { eprintln!("usage: herdr workspace focus "); return Ok(2); }; if args.len() != 1 { eprintln!("usage: herdr workspace focus "); return Ok(2); } print_response(&send_request(&Request { id: "cli:workspace:focus".into(), method: Method::WorkspaceFocus(WorkspaceTarget { workspace_id: normalize_workspace_id(raw_workspace_id), }), })?) } fn workspace_rename(args: &[String]) -> std::io::Result { if args.len() < 2 { eprintln!("usage: herdr workspace rename