From 866aaf740e10fcfce169aaf81380ea0257fbe272 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 21 May 2026 20:19:54 +0300 Subject: refactor: separate server client boundaries --- src/cli.rs | 1938 +++++++----------------------------------------------------- 1 file changed, 200 insertions(+), 1738 deletions(-) (limited to 'src/cli.rs') diff --git a/src/cli.rs b/src/cli.rs index e9fe6e81..fed93fba 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,20 +1,21 @@ -use std::io::{BufRead, BufReader, Write}; -use std::os::unix::net::UnixStream; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use serde::Serialize; -use crate::api; +use crate::api::client::{ApiClient, ApiClientError}; use crate::api::schema::{ - AgentReadParams, AgentRenameParams, AgentSendParams, AgentStartParams, AgentStatus, - AgentTarget, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneAgentState, - PaneListParams, PaneReadParams, PaneRenameParams, PaneReportAgentParams, PaneSendInputParams, - PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams, - PingParams, ReadFormat, ReadSource, Request, SplitDirection, Subscription, TabCreateParams, - TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, - WorkspaceTarget, + AgentStatus, Method, OutputMatch, PaneAgentState, PaneWaitForOutputParams, ReadFormat, + ReadSource, Request, SplitDirection, Subscription, }; +mod agent; +mod integration; +mod pane; +mod server; +mod status; +mod tab; +mod workspace; + pub enum CommandOutcome { Handled(i32), NotCli, @@ -27,20 +28,20 @@ pub fn maybe_run(args: &[String]) -> std::io::Result { let exit_code = match command { "server" => { - let Some(exit_code) = run_server_command(&args[2..])? else { + let Some(exit_code) = server::run_server_command(&args[2..])? else { return Ok(CommandOutcome::NotCli); }; exit_code } - "status" => run_status_command(&args[2..])?, + "status" => status::run_status_command(&args[2..])?, "config" => run_config_command(&args[2..])?, - "workspace" => run_workspace_command(&args[2..])?, - "tab" => run_tab_command(&args[2..])?, - "agent" => run_agent_command(&args[2..])?, + "workspace" => workspace::run_workspace_command(&args[2..])?, + "tab" => tab::run_tab_command(&args[2..])?, + "agent" => agent::run_agent_command(&args[2..])?, "terminal" => run_terminal_command(&args[2..])?, - "pane" => run_pane_command(&args[2..])?, + "pane" => pane::run_pane_command(&args[2..])?, "wait" => run_wait_command(&args[2..])?, - "integration" => run_integration_command(&args[2..])?, + "integration" => integration::run_integration_command(&args[2..])?, "session" => run_session_command(&args[2..])?, _ => return Ok(CommandOutcome::NotCli), }; @@ -48,54 +49,6 @@ pub fn maybe_run(args: &[String]) -> std::io::Result { 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) - } - } -} - fn run_config_command(args: &[String]) -> std::io::Result { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_config_help(); @@ -201,1563 +154,190 @@ fn key_config_backup_path(path: &std::path::Path) -> std::path::PathBuf { .file_name() .and_then(|name| name.to_str()) .unwrap_or("config.toml"); - path.with_file_name(format!("{file_name}.bak-keybind-v2-{timestamp}")) -} - -#[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_agent_command(args: &[String]) -> std::io::Result { - let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { - print_agent_help(); - return Ok(2); - }; - - match subcommand { - "list" => agent_list(&args[1..]), - "get" => agent_get(&args[1..]), - "read" => agent_read(&args[1..]), - "send" => agent_send(&args[1..]), - "rename" => agent_rename(&args[1..]), - "focus" => agent_focus(&args[1..]), - "wait" => agent_wait(&args[1..]), - "attach" => agent_attach(&args[1..]), - "start" => agent_start(&args[1..]), - "help" | "--help" | "-h" => { - print_agent_help(); - Ok(0) - } - _ => { - print_agent_help(); - Ok(2) - } - } -} - -fn run_terminal_command(args: &[String]) -> std::io::Result { - let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { - print_terminal_help(); - return Ok(2); - }; - - match subcommand { - "attach" => terminal_attach(&args[1..]), - "help" | "--help" | "-h" => { - print_terminal_help(); - Ok(0) - } - _ => { - print_terminal_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..]), - "report-agent" => pane_report_agent(&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