diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-27 22:53:34 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-27 22:54:02 +0300 |
| commit | 5a61ae2f9d89320c2e91d58fc3646b5a6a88f44a (patch) | |
| tree | 5a627b659542cc3b881e50de1b57e10f1736a198 /src | |
| parent | d7879fafe80933ce962f1e029992e47a4eafc69d (diff) | |
feat: add status command
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 1 | ||||
| -rw-r--r-- | src/api/schema.rs | 2 | ||||
| -rw-r--r-- | src/cli.rs | 194 | ||||
| -rw-r--r-- | src/main.rs | 77 | ||||
| -rw-r--r-- | src/server/headless.rs | 1 |
5 files changed, 240 insertions, 35 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 33a3cd44..e97d94f6 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -304,6 +304,7 @@ fn handle_request(request: Request, api_tx: &ApiRequestSender) -> String { id: request.id, result: ResponseResult::Pong { version: env!("CARGO_PKG_VERSION").into(), + protocol: crate::server::protocol::PROTOCOL_VERSION, }, }) .unwrap_or_else(|_| { diff --git a/src/api/schema.rs b/src/api/schema.rs index f7665fd9..d3fd4682 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -428,6 +428,7 @@ pub struct ErrorBody { pub enum ResponseResult { Pong { version: String, + protocol: u32, }, WorkspaceInfo { workspace: WorkspaceInfo, @@ -940,6 +941,7 @@ mod tests { id: "req_1".into(), result: ResponseResult::Pong { version: "0.1.2".into(), + protocol: 2, }, }; @@ -9,9 +9,9 @@ use crate::api::schema::{ AgentStatus, EmptyParams, IntegrationInstallParams, IntegrationTarget, IntegrationUninstallParams, Method, OutputMatch, PaneListParams, PaneReadParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, - PaneWaitForOutputParams, ReadSource, Request, SplitDirection, Subscription, TabCreateParams, - TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, - WorkspaceTarget, + PaneWaitForOutputParams, PingParams, ReadSource, Request, SplitDirection, Subscription, + TabCreateParams, TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, + WorkspaceRenameParams, WorkspaceTarget, }; pub enum CommandOutcome { @@ -31,6 +31,7 @@ pub fn maybe_run(args: &[String]) -> std::io::Result<CommandOutcome> { }; 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..])?, @@ -61,6 +62,160 @@ fn run_server_command(args: &[String]) -> std::io::Result<Option<i32>> { } } +fn run_status_command(args: &[String]) -> std::io::Result<i32> { + 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<String>, + protocol: Option<u32>, + }, + NotRunning, +} + +fn print_full_status() -> std::io::Result<i32> { + 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<i32> { + 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<ServerRuntimeStatus> { + 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<u32>) -> String { + protocol + .map(|value| value.to_string()) + .unwrap_or_else(|| "unknown".to_string()) +} + +fn compatibility_label(protocol: Option<u32>) -> &'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(version) if version == 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<i32> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_workspace_help(); @@ -74,6 +229,10 @@ fn run_workspace_command(args: &[String]) -> std::io::Result<i32> { "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) @@ -94,6 +253,10 @@ fn run_tab_command(args: &[String]) -> std::io::Result<i32> { "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) @@ -116,6 +279,10 @@ fn run_pane_command(args: &[String]) -> std::io::Result<i32> { "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) @@ -132,6 +299,10 @@ fn run_wait_command(args: &[String]) -> std::io::Result<i32> { 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) @@ -148,6 +319,10 @@ fn run_integration_command(args: &[String]) -> std::io::Result<i32> { match subcommand { "install" => integration_install(&args[1..]), "uninstall" => integration_uninstall(&args[1..]), + "help" | "--help" | "-h" => { + print_integration_help(); + Ok(0) + } _ => { print_integration_help(); Ok(2) @@ -1053,10 +1228,17 @@ fn parse_u64_flag(flag: &str, value: &str) -> std::io::Result<u64> { 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 stop stop the running server via the API socket"); eprintln!(" herdr server reload-config reload config.toml in the running server"); } +fn print_status_help() { + eprintln!("herdr status commands:"); + eprintln!(" herdr status show local client and running server status"); + eprintln!(" herdr status server show running server status"); + eprintln!(" herdr status client show local client binary status"); +} + fn print_workspace_help() { eprintln!("herdr workspace commands:"); eprintln!(" herdr workspace list"); @@ -1083,7 +1265,7 @@ fn print_pane_help() { eprintln!("herdr pane commands:"); eprintln!(" herdr pane list [--workspace <workspace_id>]"); eprintln!(" herdr pane get <pane_id>"); - eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N]"); + eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--raw]"); eprintln!(" herdr pane split <pane_id> --direction right|down [--cwd PATH] [--no-focus]"); eprintln!(" herdr pane close <pane_id>"); eprintln!(" herdr pane send-text <pane_id> <text>"); @@ -1093,7 +1275,7 @@ fn print_pane_help() { fn print_wait_help() { eprintln!("herdr wait commands:"); - eprintln!(" herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex]"); + eprintln!(" herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex] [--raw]"); eprintln!( " herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]" ); diff --git a/src/main.rs b/src/main.rs index 9b66198f..1d8439b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -188,35 +188,61 @@ fn main() -> io::Result<()> { if args.iter().any(|a| a == "--help" || a == "-h") { println!("herdr — terminal workspace manager for AI coding agents"); println!(); - println!("Usage: herdr [options]"); - println!(" herdr update"); - println!(" herdr server stop"); - println!(" herdr server reload-config"); - println!(" herdr workspace <subcommand> ..."); - println!(" herdr tab <subcommand> ..."); - println!(" herdr pane <subcommand> ..."); - println!(" herdr wait <subcommand> ..."); - println!(" herdr integration <subcommand> ..."); + println!("Usage: herdr [options] [command]"); println!(); - println!("Commands:"); - println!(" server Run as headless server (no terminal, persists after client disconnect)"); - println!(" server stop Stop the running server via the API socket"); - println!(" server reload-config Reload config.toml in the running server"); - println!(" client Connect to a running server as a thin client"); + println!("Common commands:"); println!( - " update Download and install the latest version (run outside herdr)" + " {:<32} {}", + "herdr", "Launch or attach to the persistent session" + ); + println!( + " {:<32} {}", + "herdr status [server|client]", "Show local client and running server status" + ); + println!( + " {:<32} {}", + "herdr update", "Download and install the latest version" + ); + println!( + " {:<32} {}", + "herdr server stop", "Stop the running server via the API socket" + ); + println!( + " {:<32} {}", + "herdr server reload-config", "Reload config.toml in the running server" + ); + println!( + " {:<32} {}", + "herdr workspace <subcommand>", "Workspace helpers over the socket API" + ); + println!( + " {:<32} {}", + "herdr tab <subcommand>", "Tab helpers over the socket API" + ); + println!( + " {:<32} {}", + "herdr pane <subcommand>", "Pane control helpers over the socket API" + ); + println!( + " {:<32} {}", + "herdr wait <subcommand>", "Blocking wait helpers over the socket API" + ); + println!( + " {:<32} {}", + "herdr integration <subcommand>", "Manage built-in agent integrations" + ); + println!(); + println!("Advanced commands:"); + println!(" {:<32} {}", "herdr server", "Run as headless server"); + println!( + " {:<32} {}", + "herdr client", "Connect to a running server as a thin client" ); - println!(" workspace workspace helpers over the socket api"); - println!(" tab tab helpers over the socket api"); - println!(" pane pane control helpers over the socket api"); - println!(" wait blocking wait helpers over the socket api"); - println!(" integration manage built-in agent integrations"); println!(); println!("Options:"); println!(" --no-session Run monolithically (no server/client, escape hatch)"); println!(" --default-config Print default configuration and exit"); println!(" --version, -V Print version and exit"); - println!(" --show-changelog Preview the current version's release notes"); println!(" --help, -h Show this help"); println!(); println!("Config: {}", config::config_path().display()); @@ -242,7 +268,6 @@ fn main() -> io::Result<()> { "--version", "-V", "--default-config", - "--show-changelog", "--help", "-h", ]; @@ -257,6 +282,7 @@ fn main() -> io::Result<()> { "server", "client", "update", + "status", "workspace", "pane", "wait", @@ -304,7 +330,6 @@ fn main() -> io::Result<()> { Err(err) => return Err(err), }; - let show_changelog = args.iter().any(|a| a == "--show-changelog"); let in_tmux = std::env::var("TMUX").is_ok(); let original_hook = std::panic::take_hook(); @@ -359,11 +384,7 @@ fn main() -> io::Result<()> { std::io::stdout().flush()?; } - let startup_release_notes = if show_changelog { - crate::release_notes::load_preview_from_local_changelog(env!("CARGO_PKG_VERSION")) - } else { - crate::release_notes::load_pending_for_current_version() - }; + let startup_release_notes = crate::release_notes::load_pending_for_current_version(); let mut app = app::App::new( config, diff --git a/src/server/headless.rs b/src/server/headless.rs index b37538df..cffffe6a 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -1948,7 +1948,6 @@ pub fn run_server() -> io::Result<()> { }; let no_session = false; // Server always does session persistence. - let _show_changelog = false; let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() |
