diff options
Diffstat (limited to 'src/cli')
| -rw-r--r-- | src/cli/agent.rs | 429 | ||||
| -rw-r--r-- | src/cli/integration.rs | 142 | ||||
| -rw-r--r-- | src/cli/pane.rs | 410 | ||||
| -rw-r--r-- | src/cli/server.rs | 48 | ||||
| -rw-r--r-- | src/cli/status.rs | 299 | ||||
| -rw-r--r-- | src/cli/tab.rs | 194 | ||||
| -rw-r--r-- | src/cli/workspace.rs | 163 |
7 files changed, 1685 insertions, 0 deletions
diff --git a/src/cli/agent.rs b/src/cli/agent.rs new file mode 100644 index 00000000..5005eda1 --- /dev/null +++ b/src/cli/agent.rs @@ -0,0 +1,429 @@ +use crate::api::schema::{ + AgentReadParams, AgentRenameParams, AgentSendParams, AgentStartParams, AgentStatus, + AgentTarget, EmptyParams, Method, ReadFormat, ReadSource, Request, Subscription, +}; + +pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> { + 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 agent_start(args: &[String]) -> std::io::Result<i32> { + let Some(name) = args.first() else { + eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); + return Ok(2); + }; + + let Some(separator) = args.iter().position(|arg| arg == "--") else { + eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); + return Ok(2); + }; + if separator == args.len() - 1 { + eprintln!("agent start requires argv after --"); + return Ok(2); + } + + let mut cwd = None; + let mut workspace_id = None; + let mut tab_id = None; + let mut split = None; + let mut focus = false; + + let mut index = 1; + while index < separator { + match args[index].as_str() { + "--cwd" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --cwd"); + return Ok(2); + }; + cwd = Some(value.clone()); + index += 2; + } + "--workspace" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(super::normalize_workspace_id(value)); + index += 2; + } + "--tab" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --tab"); + return Ok(2); + }; + tab_id = Some(super::normalize_tab_id(value)); + index += 2; + } + "--split" => { + let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else { + eprintln!("missing value for --split"); + return Ok(2); + }; + split = Some(super::parse_split_direction(value)?); + index += 2; + } + "--focus" => { + focus = true; + index += 1; + } + "--no-focus" => { + focus = false; + index += 1; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + super::print_response(&super::send_request(&Request { + id: "cli:agent:start".into(), + method: Method::AgentStart(AgentStartParams { + name: name.clone(), + cwd, + workspace_id, + tab_id, + split, + focus, + argv: args[separator + 1..].to_vec(), + }), + })?) +} + +fn agent_list(args: &[String]) -> std::io::Result<i32> { + if !args.is_empty() { + eprintln!("usage: herdr agent list"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:agent:list".into(), + method: Method::AgentList(EmptyParams::default()), + })?) +} + +fn agent_get(args: &[String]) -> std::io::Result<i32> { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent get <target>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr agent get <target>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:agent:get".into(), + method: Method::AgentGet(AgentTarget { + target: target.clone(), + }), + })?) +} + +fn agent_focus(args: &[String]) -> std::io::Result<i32> { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent focus <target>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr agent focus <target>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:agent:focus".into(), + method: Method::AgentFocus(AgentTarget { + target: target.clone(), + }), + })?) +} + +fn agent_attach(args: &[String]) -> std::io::Result<i32> { + let (target, takeover) = + match super::parse_attach_target(args, "usage: herdr agent attach <target> [--takeover]") { + Ok(parsed) => parsed, + Err(code) => return Ok(code), + }; + + let response = resolve_agent_target(&target, "cli:agent:attach:resolve")?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); + return Ok(1); + } + let Some(terminal_id) = response["result"]["agent"]["terminal_id"].as_str() else { + eprintln!("agent attach failed: response did not include terminal_id"); + return Ok(1); + }; + crate::client::run_terminal_attach(terminal_id.to_owned(), takeover)?; + Ok(0) +} + +fn agent_wait(args: &[String]) -> std::io::Result<i32> { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]"); + return Ok(2); + }; + + let mut timeout_ms = None; + let mut desired_status = None; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--status" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --status"); + return Ok(2); + }; + desired_status = Some(parse_agent_wait_status(value)?); + index += 2; + } + "--timeout" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --timeout"); + return Ok(2); + }; + timeout_ms = Some(super::parse_u64_flag("--timeout", value)?); + index += 2; + } + "help" | "--help" | "-h" => { + eprintln!("usage: herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]"); + return Ok(0); + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + let Some(agent_status) = desired_status else { + eprintln!("missing required --status"); + return Ok(2); + }; + + let response = resolve_agent_target(target, "cli:agent:wait:resolve")?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); + return Ok(1); + } + if response["result"]["agent"]["agent_status"] + .as_str() + .is_some_and(|current| agent_wait_status_satisfied(agent_status, current)) + { + println!("{}", serde_json::to_string(&response).unwrap()); + return Ok(0); + } + + let Some(pane_id) = response["result"]["agent"]["pane_id"].as_str() else { + eprintln!("agent wait failed: response did not include pane_id"); + return Ok(1); + }; + + let subscriptions = if agent_status == AgentStatus::Idle { + vec![ + Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(AgentStatus::Idle), + }, + Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(AgentStatus::Done), + }, + ] + } else { + vec![Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(agent_status), + }] + }; + + super::wait_for_agent_change( + Request { + id: "cli:agent:wait".into(), + method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams { + subscriptions, + }), + }, + timeout_ms, + "timed out waiting for agent status change", + ) +} + +fn resolve_agent_target(target: &str, request_id: &str) -> std::io::Result<serde_json::Value> { + super::send_request(&Request { + id: request_id.into(), + method: Method::AgentGet(AgentTarget { + target: target.to_owned(), + }), + }) +} + +fn agent_rename(args: &[String]) -> std::io::Result<i32> { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent rename <target> <name>|--clear"); + return Ok(2); + }; + if args.len() < 2 { + eprintln!("usage: herdr agent rename <target> <name>|--clear"); + return Ok(2); + } + let name = if args.len() == 2 && args[1] == "--clear" { + None + } else { + Some(args[1..].join(" ")) + }; + + super::print_response(&super::send_request(&Request { + id: "cli:agent:rename".into(), + method: Method::AgentRename(AgentRenameParams { + target: target.clone(), + name, + }), + })?) +} + +fn agent_send(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr agent send <target> <text>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:agent:send".into(), + method: Method::AgentSend(AgentSendParams { + target: args[0].clone(), + text: args[1..].join(" "), + }), + })?) +} + +fn agent_read(args: &[String]) -> std::io::Result<i32> { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]"); + return Ok(2); + }; + + let mut source = ReadSource::Recent; + let mut lines = None; + let mut format = ReadFormat::Text; + let mut strip_ansi = true; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--source" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --source"); + return Ok(2); + }; + source = super::parse_read_source(value)?; + index += 2; + } + "--lines" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --lines"); + return Ok(2); + }; + lines = Some(super::parse_u32_flag("--lines", value)?); + index += 2; + } + "--format" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --format"); + return Ok(2); + }; + format = super::parse_read_format(value)?; + strip_ansi = !matches!(format, ReadFormat::Ansi); + index += 2; + } + "--ansi" => { + format = ReadFormat::Ansi; + strip_ansi = false; + index += 1; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + super::print_response(&super::send_request(&Request { + id: "cli:agent:read".into(), + method: Method::AgentRead(AgentReadParams { + target: target.clone(), + source, + lines, + format, + strip_ansi, + }), + })?) +} + +fn agent_wait_status_satisfied(desired: AgentStatus, current: &str) -> bool { + match desired { + AgentStatus::Idle => matches!(current, "idle" | "done"), + AgentStatus::Working => current == "working", + AgentStatus::Blocked => current == "blocked", + AgentStatus::Unknown => current == "unknown", + AgentStatus::Done => false, + } +} + +fn parse_agent_wait_status(value: &str) -> std::io::Result<AgentStatus> { + match value { + "idle" => Ok(AgentStatus::Idle), + "working" => Ok(AgentStatus::Working), + "blocked" => Ok(AgentStatus::Blocked), + "unknown" => Ok(AgentStatus::Unknown), + "done" => Err(std::io::Error::other( + "done is a UI attention state; use idle for CLI agent completion waits", + )), + _ => Err(std::io::Error::other(format!( + "invalid agent status: {value} (expected idle, working, blocked, or unknown)" + ))), + } +} + +fn print_agent_help() { + eprintln!("herdr agent commands:"); + eprintln!(" herdr agent list"); + eprintln!(" herdr agent get <target>"); + eprintln!(" herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]"); + eprintln!(" herdr agent send <target> <text>"); + eprintln!(" herdr agent rename <target> <name>|--clear"); + eprintln!(" herdr agent focus <target>"); + eprintln!(" herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]"); + eprintln!(" herdr agent attach <target> [--takeover]"); + eprintln!(" herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>"); + eprintln!(" targets accept terminal ids, unique agent names, detected/reported agent labels, and legacy pane ids"); + eprintln!( + " agent send writes literal text; use pane run when you want command text plus Enter" + ); +} diff --git a/src/cli/integration.rs b/src/cli/integration.rs new file mode 100644 index 00000000..6239faf2 --- /dev/null +++ b/src/cli/integration.rs @@ -0,0 +1,142 @@ +use crate::api::schema::IntegrationTarget; + +pub(super) fn run_integration_command(args: &[String]) -> std::io::Result<i32> { + 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 integration_status(args: &[String]) -> std::io::Result<i32> { + let outdated_only = match args { + [] => false, + [flag] if flag == "--outdated-only" => true, + _ => { + eprintln!("usage: herdr integration status [--outdated-only]"); + return Ok(2); + } + }; + + if outdated_only { + crate::integration::print_outdated_update_notice(); + return Ok(0); + } + + for status in crate::integration::installed_integration_statuses() { + let target = crate::integration::integration_target_label(status.target); + let version = match status.installed_version { + Some(version) => format!("v{version}"), + None => "legacy".to_string(), + }; + let state = match status.state { + crate::integration::IntegrationStatusKind::NotInstalled => "not installed".to_string(), + crate::integration::IntegrationStatusKind::Current => { + format!("current ({version})") + } + crate::integration::IntegrationStatusKind::Outdated => { + format!("outdated ({version} < v{})", status.expected_version) + } + }; + println!("{target}: {state} ({})", status.path.display()); + } + + Ok(0) +} + +fn integration_install(args: &[String]) -> std::io::Result<i32> { + let Some(target) = parse_integration_target(args, "install")? else { + return Ok(2); + }; + + match crate::integration::install_target(target) { + Ok(messages) => { + print_integration_messages(messages); + Ok(0) + } + Err(err) => { + eprintln!("{err}"); + Ok(1) + } + } +} + +fn integration_uninstall(args: &[String]) -> std::io::Result<i32> { + let Some(target) = parse_integration_target(args, "uninstall")? else { + return Ok(2); + }; + + match crate::integration::uninstall_target(target) { + Ok(messages) => { + print_integration_messages(messages); + Ok(0) + } + Err(err) => { + eprintln!("{err}"); + Ok(1) + } + } +} + +fn print_integration_messages(messages: Vec<String>) { + for message in messages { + println!("{message}"); + } +} + +fn parse_integration_target( + args: &[String], + action: &str, +) -> std::io::Result<Option<IntegrationTarget>> { + let Some(target) = args.first().map(|arg| arg.as_str()) else { + eprintln!("usage: herdr integration {action} <pi|claude|codex|opencode|hermes>"); + return Ok(None); + }; + if args.len() != 1 { + eprintln!("usage: herdr integration {action} <pi|claude|codex|opencode|hermes>"); + return Ok(None); + } + + let parsed = match target { + "pi" => IntegrationTarget::Pi, + "claude" => IntegrationTarget::Claude, + "codex" => IntegrationTarget::Codex, + "opencode" => IntegrationTarget::Opencode, + "hermes" => IntegrationTarget::Hermes, + _ => { + eprintln!("unknown integration target: {target}"); + eprintln!("currently supported: pi, claude, codex, opencode, hermes"); + return Ok(None); + } + }; + + Ok(Some(parsed)) +} + +fn print_integration_help() { + eprintln!("herdr integration commands:"); + eprintln!(" herdr integration install pi"); + eprintln!(" herdr integration install claude"); + eprintln!(" herdr integration install codex"); + eprintln!(" herdr integration install opencode"); + eprintln!(" herdr integration install hermes"); + eprintln!(" herdr integration uninstall pi"); + eprintln!(" herdr integration uninstall claude"); + eprintln!(" herdr integration uninstall codex"); + eprintln!(" herdr integration uninstall opencode"); + eprintln!(" herdr integration uninstall hermes"); + eprintln!(" herdr integration status [--outdated-only]"); +} diff --git a/src/cli/pane.rs b/src/cli/pane.rs new file mode 100644 index 00000000..58d65af6 --- /dev/null +++ b/src/cli/pane.rs @@ -0,0 +1,410 @@ +use crate::api::schema::{ + Method, PaneListParams, PaneReadParams, PaneRenameParams, PaneReportAgentParams, + PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, + ReadFormat, ReadSource, Request, +}; + +pub(super) fn run_pane_command(args: &[String]) -> std::io::Result<i32> { + 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 pane_list(args: &[String]) -> std::io::Result<i32> { + let mut workspace_id = None; + + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--workspace" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(super::normalize_workspace_id(value)); + index += 2; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + super::print_response(&super::send_request(&Request { + id: "cli:pane:list".into(), + method: Method::PaneList(PaneListParams { workspace_id }), + })?) +} + +fn pane_get(args: &[String]) -> std::io::Result<i32> { + let Some(raw_pane_id) = args.first() else { + eprintln!("usage: herdr pane get <pane_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr pane get <pane_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:pane:get".into(), + method: Method::PaneGet(PaneTarget { + pane_id: super::normalize_pane_id(raw_pane_id), + }), + })?) +} + +fn pane_rename(args: &[String]) -> std::io::Result<i32> { + let Some(raw_pane_id) = args.first() else { + eprintln!("usage: herdr pane rename <pane_id> <label>|--clear"); + return Ok(2); + }; + if args.len() < 2 { + eprintln!("usage: herdr pane rename <pane_id> <label>|--clear"); + return Ok(2); + } + let label = if args.len() == 2 && args[1] == "--clear" { + None + } else { + Some(args[1..].join(" ")) + }; + + super::print_response(&super::send_request(&Request { + id: "cli:pane:rename".into(), + method: Method::PaneRename(PaneRenameParams { + pane_id: super::normalize_pane_id(raw_pane_id), + label, + }), + })?) +} + +fn pane_read(args: &[String]) -> std::io::Result<i32> { + let Some(raw_pane_id) = args.first() else { + eprintln!("usage: herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]"); + return Ok(2); + }; + + let pane_id = super::normalize_pane_id(raw_pane_id); + let mut source = ReadSource::Recent; + let mut lines = None; + let mut format = ReadFormat::Text; + let mut strip_ansi = true; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--source" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --source"); + return Ok(2); + }; + source = super::parse_read_source(value)?; + index += 2; + } + "--lines" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --lines"); + return Ok(2); + }; + lines = Some(super::parse_u32_flag("--lines", value)?); + index += 2; + } + "--format" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --format"); + return Ok(2); + }; + format = super::parse_read_format(value)?; + index += 2; + } + "--ansi" => { + format = ReadFormat::Ansi; + index += 1; + } + "--raw" => { + format = ReadFormat::Ansi; + strip_ansi = false; + index += 1; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + let response = super::send_request(&Request { + id: "cli:pane:read".into(), + method: Method::PaneRead(PaneReadParams { + pane_id, + source, + lines, + format, + strip_ansi, + }), + })?; + + if let Some(error) = response.get("error") { + eprintln!("{}", serde_json::to_string(error).unwrap()); + return Ok(1); + } + + if let Some(text) = response["result"]["read"]["text"].as_str() { + print!("{text}"); + } + Ok(0) +} + +fn pane_split(args: &[String]) -> std::io::Result<i32> { + let Some(raw_pane_id) = args.first() else { + eprintln!( + "usage: herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]" + ); + return Ok(2); + }; + + let pane_id = super::normalize_pane_id(raw_pane_id); + let mut direction = None; + let mut cwd = None; + let mut focus = false; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--direction" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --direction"); + return Ok(2); + }; + direction = Some(super::parse_split_direction(value)?); + index += 2; + } + "--cwd" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --cwd"); + return Ok(2); + }; + cwd = Some(value.clone()); + index += 2; + } + "--focus" => { + focus = true; + index += 1; + } + "--no-focus" => { + focus = false; + index += 1; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + let Some(direction) = direction else { + eprintln!("missing required --direction"); + return Ok(2); + }; + + super::print_response(&super::send_request(&Request { + id: "cli:pane:split".into(), + method: Method::PaneSplit(PaneSplitParams { + workspace_id: None, + target_pane_id: pane_id, + direction, + cwd, + focus, + }), + })?) +} + +fn pane_close(args: &[String]) -> std::io::Result<i32> { + let Some(raw_pane_id) = args.first() else { + eprintln!("usage: herdr pane close <pane_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr pane close <pane_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:pane:close".into(), + method: Method::PaneClose(PaneTarget { + pane_id: super::normalize_pane_id(raw_pane_id), + }), + })?) +} + +fn pane_send_text(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr pane send-text <pane_id> <text>"); + return Ok(2); + } + + let pane_id = super::normalize_pane_id(&args[0]); + let text = args[1..].join(" "); + super::send_ok_request(Method::PaneSendText(PaneSendTextParams { pane_id, text })) +} + +fn pane_send_keys(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr pane send-keys <pane_id> <key> [key ...]"); + return Ok(2); + } + + let pane_id = super::normalize_pane_id(&args[0]); + let keys = args[1..].to_vec(); + super::send_ok_request(Method::PaneSendKeys(PaneSendKeysParams { pane_id, keys })) +} + +fn pane_run(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr pane run <pane_id> <command>"); + return Ok(2); + } + + let pane_id = super::normalize_pane_id(&args[0]); + let text = args[1..].join(" "); + super::send_ok_request(Method::PaneSendInput(PaneSendInputParams { + pane_id, + text, + keys: vec!["Enter".into()], + })) +} + +fn pane_report_agent(args: &[String]) -> std::io::Result<i32> { + let Some(raw_pane_id) = args.first() else { + eprintln!("usage: herdr pane report-agent <pane_id> --source ID --agent LABEL --state idle|working|blocked|unknown [--message TEXT] [--custom-status TEXT] [--seq N]"); + return Ok(2); + }; + + let pane_id = super::normalize_pane_id(raw_pane_id); + let mut source = None; + let mut agent = None; + let mut state = None; + let mut message = None; + let mut custom_status = None; + let mut seq = None; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--source" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --source"); + return Ok(2); + }; + source = Some(value.clone()); + index += 2; + } + "--agent" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --agent"); + return Ok(2); + }; + agent = Some(value.clone()); + index += 2; + } + "--state" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --state"); + return Ok(2); + }; + state = Some(super::parse_pane_agent_state(value)?); + index += 2; + } + "--message" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --message"); + return Ok(2); + }; + message = Some(value.clone()); + index += 2; + } + "--custom-status" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --custom-status"); + return Ok(2); + }; + custom_status = Some(value.clone()); + index += 2; + } + "--seq" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --seq"); + return Ok(2); + }; + seq = Some(super::parse_u64_flag("--seq", value)?); + index += 2; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + let Some(source) = source else { + eprintln!("missing required --source"); + return Ok(2); + }; + let Some(agent) = agent else { + eprintln!("missing required --agent"); + return Ok(2); + }; + let Some(state) = state else { + eprintln!("missing required --state"); + return Ok(2); + }; + + super::send_ok_request(Method::PaneReportAgent(PaneReportAgentParams { + pane_id, + source, + agent, + state, + message, + custom_status, + seq, + })) +} + +fn print_pane_help() { + eprintln!("herdr pane commands:"); + eprintln!(" herdr pane list [--workspace <workspace_id>]"); + eprintln!(" herdr pane get <pane_id>"); + eprintln!(" herdr pane rename <pane_id> <label>|--clear"); + eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]"); + eprintln!( + " herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]" + ); + eprintln!(" herdr pane close <pane_id>"); + eprintln!(" herdr pane send-text <pane_id> <text>"); + eprintln!(" herdr pane send-keys <pane_id> <key> [key ...]"); + eprintln!(" herdr pane report-agent <pane_id> --source ID --agent LABEL --state idle|working|blocked|unknown [--message TEXT] [--custom-status TEXT] [--seq N]"); + eprintln!(" herdr pane run <pane_id> <command>"); +} diff --git a/src/cli/server.rs b/src/cli/server.rs new file mode 100644 index 00000000..311cc31a --- /dev/null +++ b/src/cli/server.rs @@ -0,0 +1,48 @@ +use crate::api::schema::{EmptyParams, Method, Request}; + +pub(super) fn run_server_command(args: &[String]) -> std::io::Result<Option<i32>> { + 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 server_stop(args: &[String]) -> std::io::Result<i32> { + 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<i32> { + 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 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 reload-config reload config.toml in the running server"); +} diff --git a/src/cli/status.rs b/src/cli/status.rs new file mode 100644 index 00000000..9b8a1648 --- /dev/null +++ b/src/cli/status.rs @@ -0,0 +1,299 @@ +use serde::Serialize; + +use crate::api; +use crate::api::client::{ApiClient, ApiClientError}; + +pub(super) fn run_status_command(args: &[String]) -> std::io::Result<i32> { + let Some((scope, json)) = parse_status_args(args) else { + return Ok(2); + }; + + match scope { + StatusScope::Full => print_full_status(json), + StatusScope::Server => print_server_status(json), + StatusScope::Client => { + print_client_status(json)?; + Ok(0) + } + StatusScope::Help => { + print_status_help(); + Ok(0) + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StatusScope { + Full, + Server, + Client, + Help, +} + +fn parse_status_args(args: &[String]) -> Option<(StatusScope, bool)> { + match args.first().map(|arg| arg.as_str()) { + None => Some((StatusScope::Full, false)), + Some("--json") if args.len() == 1 => Some((StatusScope::Full, true)), + Some("server") => { + parse_status_scope_args(args, StatusScope::Server, "herdr status server [--json]") + } + Some("client") => { + parse_status_scope_args(args, StatusScope::Client, "herdr status client [--json]") + } + Some("help" | "--help" | "-h") => { + if args.len() > 1 { + print_status_help(); + return None; + } + Some((StatusScope::Help, false)) + } + Some(_) => { + print_status_help(); + None + } + } +} + +fn parse_status_scope_args( + args: &[String], + scope: StatusScope, + usage: &str, +) -> Option<(StatusScope, bool)> { + match args.get(1).map(|arg| arg.as_str()) { + None => Some((scope, false)), + Some("--json") if args.len() == 2 => Some((scope, true)), + _ => { + eprintln!("usage: {usage}"); + None + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ServerRuntimeStatus { + Running { + version: Option<String>, + protocol: Option<u32>, + }, + NotRunning, +} + +fn print_full_status(json: bool) -> std::io::Result<i32> { + let server = read_server_runtime_status()?; + + if json { + print_json(&FullStatusJson { + client: client_status_json(), + server: server_status_json(&server), + update: update_status_json(&server), + })?; + return Ok(0); + } + + println!("client:"); + println!(" version: {}", env!("CARGO_PKG_VERSION")); + println!(" protocol: {}", crate::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(json: bool) -> std::io::Result<i32> { + let server = read_server_runtime_status()?; + if json { + print_json(&server_status_json(&server))?; + return Ok(0); + } + print_server_status_body(&server, ""); + Ok(0) +} + +fn print_client_status(json: bool) -> std::io::Result<()> { + if json { + print_json(&client_status_json())?; + return Ok(()); + } + + println!("version: {}", env!("CARGO_PKG_VERSION")); + println!("protocol: {}", crate::protocol::PROTOCOL_VERSION); + println!("binary: {}", current_exe_label()); + Ok(()) +} + +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 ApiClient::local().status() { + Ok(status) => Ok(ServerRuntimeStatus::Running { + version: status.version, + protocol: status.protocol, + }), + Err(ApiClientError::Io(err)) if server_not_running_error(&err) => { + Ok(ServerRuntimeStatus::NotRunning) + } + Err(err) => Err(api_client_error_to_io(err)), + } +} + +fn server_not_running_error(err: &std::io::Error) -> bool { + matches!( + err.kind(), + std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused + ) +} + +fn api_client_error_to_io(err: ApiClientError) -> std::io::Error { + match err { + ApiClientError::Io(err) => err, + err => std::io::Error::other(err), + } +} + +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::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", + } +} + +#[derive(Serialize)] +struct FullStatusJson { + client: ClientStatusJson, + server: ServerStatusJson, + update: UpdateStatusJson, +} + +#[derive(Serialize)] +struct ClientStatusJson { + version: &'static str, + protocol: u32, + binary: String, + session: Option<String>, +} + +#[derive(Serialize)] +struct ServerStatusJson { + status: &'static str, + running: bool, + version: Option<String>, + protocol: Option<u32>, + compatible: Option<bool>, + socket: String, + session: Option<String>, + restart_needed: Option<bool>, +} + +#[derive(Serialize)] +struct UpdateStatusJson { + restart_needed: Option<bool>, +} + +fn client_status_json() -> ClientStatusJson { + ClientStatusJson { + version: env!("CARGO_PKG_VERSION"), + protocol: crate::protocol::PROTOCOL_VERSION, + binary: current_exe_label(), + session: crate::session::active_name(), + } +} + +fn server_status_json(server: &ServerRuntimeStatus) -> ServerStatusJson { + match server { + ServerRuntimeStatus::Running { version, protocol } => ServerStatusJson { + status: "running", + running: true, + version: version.clone(), + protocol: *protocol, + compatible: protocol.map(|value| value == crate::protocol::PROTOCOL_VERSION), + socket: api::socket_path().display().to_string(), + session: crate::session::active_name(), + restart_needed: restart_needed_bool(server), + }, + ServerRuntimeStatus::NotRunning => ServerStatusJson { + status: "not_running", + running: false, + version: None, + protocol: None, + compatible: None, + socket: api::socket_path().display().to_string(), + session: crate::session::active_name(), + restart_needed: Some(false), + }, + } +} + +fn update_status_json(server: &ServerRuntimeStatus) -> UpdateStatusJson { + UpdateStatusJson { + restart_needed: restart_needed_bool(server), + } +} + +fn restart_needed_bool(server: &ServerRuntimeStatus) -> Option<bool> { + match server { + ServerRuntimeStatus::Running { version, .. } => match version.as_deref() { + Some(env!("CARGO_PKG_VERSION")) => Some(false), + Some(_) => Some(true), + None => None, + }, + ServerRuntimeStatus::NotRunning => Some(false), + } +} + +fn print_json(value: &impl Serialize) -> std::io::Result<()> { + println!("{}", serde_json::to_string(value)?); + Ok(()) +} + +fn current_exe_label() -> String { + std::env::current_exe() + .map(|path| path.display().to_string()) + .unwrap_or_else(|err| format!("unknown ({err})")) +} + +fn print_status_help() { + eprintln!("herdr status commands:"); + eprintln!(" herdr status [--json] show local client and running server status"); + eprintln!(" herdr status server [--json] show running server status"); + eprintln!(" herdr status client [--json] show local client binary status"); +} diff --git a/src/cli/tab.rs b/src/cli/tab.rs new file mode 100644 index 00000000..3fcc0647 --- /dev/null +++ b/src/cli/tab.rs @@ -0,0 +1,194 @@ +use crate::api::schema::{ + Method, Request, TabCreateParams, TabListParams, TabRenameParams, TabTarget, +}; + +pub(super) fn run_tab_command(args: &[String]) -> std::io::Result<i32> { + 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 tab_list(args: &[String]) -> std::io::Result<i32> { + let mut workspace_id = None; + + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--workspace" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(super::normalize_workspace_id(value)); + index += 2; + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + super::print_response(&super::send_request(&Request { + id: "cli:tab:list".into(), + method: Method::TabList(TabListParams { workspace_id }), + })?) +} + +fn tab_create(args: &[String]) -> std::io::Result<i32> { + let mut workspace_id = None; + 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() { + "--workspace" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --workspace"); + return Ok(2); + }; + workspace_id = Some(super::normalize_workspace_id(value)); + index += 2; + } + "--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); + } + } + } + + super::print_response(&super::send_request(&Request { + id: "cli:tab:create".into(), + method: Method::TabCreate(TabCreateParams { + workspace_id, + cwd, + focus, + label, + }), + })?) +} + +fn tab_get(args: &[String]) -> std::io::Result<i32> { + let Some(raw_tab_id) = args.first() else { + eprintln!("usage: herdr tab get <tab_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr tab get <tab_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:tab:get".into(), + method: Method::TabGet(TabTarget { + tab_id: super::normalize_tab_id(raw_tab_id), + }), + })?) +} + +fn tab_focus(args: &[String]) -> std::io::Result<i32> { + let Some(raw_tab_id) = args.first() else { + eprintln!("usage: herdr tab focus <tab_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr tab focus <tab_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:tab:focus".into(), + method: Method::TabFocus(TabTarget { + tab_id: super::normalize_tab_id(raw_tab_id), + }), + })?) +} + +fn tab_rename(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr tab rename <tab_id> <label>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:tab:rename".into(), + method: Method::TabRename(TabRenameParams { + tab_id: super::normalize_tab_id(&args[0]), + label: args[1..].join(" "), + }), + })?) +} + +fn tab_close(args: &[String]) -> std::io::Result<i32> { + let Some(raw_tab_id) = args.first() else { + eprintln!("usage: herdr tab close <tab_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr tab close <tab_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:tab:close".into(), + method: Method::TabClose(TabTarget { + tab_id: super::normalize_tab_id(raw_tab_id), + }), + })?) +} + +fn print_tab_help() { + eprintln!("herdr tab commands:"); + eprintln!(" herdr tab list [--workspace <workspace_id>]"); + eprintln!( + " herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus]" + ); + eprintln!(" herdr tab get <tab_id>"); + eprintln!(" herdr tab focus <tab_id>"); + eprintln!(" herdr tab rename <tab_id> <label>"); + eprintln!(" herdr tab close <tab_id>"); +} diff --git a/src/cli/workspace.rs b/src/cli/workspace.rs new file mode 100644 index 00000000..ae29792c --- /dev/null +++ b/src/cli/workspace.rs @@ -0,0 +1,163 @@ +use crate::api::schema::{ + EmptyParams, Method, Request, WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget, +}; + +pub(super) fn run_workspace_command(args: &[String]) -> std::io::Result<i32> { + 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 workspace_list(args: &[String]) -> std::io::Result<i32> { + if !args.is_empty() { + eprintln!("usage: herdr workspace list"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:workspace:list".into(), + method: Method::WorkspaceList(EmptyParams::default()), + })?) +} + +fn workspace_create(args: &[String]) -> std::io::Result<i32> { + 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); + } + } + } + + super::print_response(&super::send_request(&Request { + id: "cli:workspace:create".into(), + method: Method::WorkspaceCreate(WorkspaceCreateParams { cwd, focus, label }), + })?) +} + +fn workspace_get(args: &[String]) -> std::io::Result<i32> { + let Some(raw_workspace_id) = args.first() else { + eprintln!("usage: herdr workspace get <workspace_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr workspace get <workspace_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:workspace:get".into(), + method: Method::WorkspaceGet(WorkspaceTarget { + workspace_id: super::normalize_workspace_id(raw_workspace_id), + }), + })?) +} + +fn workspace_focus(args: &[String]) -> std::io::Result<i32> { + let Some(raw_workspace_id) = args.first() else { + eprintln!("usage: herdr workspace focus <workspace_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr workspace focus <workspace_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:workspace:focus".into(), + method: Method::WorkspaceFocus(WorkspaceTarget { + workspace_id: super::normalize_workspace_id(raw_workspace_id), + }), + })?) +} + +fn workspace_rename(args: &[String]) -> std::io::Result<i32> { + if args.len() < 2 { + eprintln!("usage: herdr workspace rename <workspace_id> <label>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:workspace:rename".into(), + method: Method::WorkspaceRename(WorkspaceRenameParams { + workspace_id: super::normalize_workspace_id(&args[0]), + label: args[1..].join(" "), + }), + })?) +} + +fn workspace_close(args: &[String]) -> std::io::Result<i32> { + let Some(raw_workspace_id) = args.first() else { + eprintln!("usage: herdr workspace close <workspace_id>"); + return Ok(2); + }; + if args.len() != 1 { + eprintln!("usage: herdr workspace close <workspace_id>"); + return Ok(2); + } + + super::print_response(&super::send_request(&Request { + id: "cli:workspace:close".into(), + method: Method::WorkspaceClose(WorkspaceTarget { + workspace_id: super::normalize_workspace_id(raw_workspace_id), + }), + })?) +} + +fn print_workspace_help() { + eprintln!("herdr workspace commands:"); + eprintln!(" herdr workspace list"); + eprintln!(" herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus]"); + eprintln!(" herdr workspace get <workspace_id>"); + eprintln!(" herdr workspace focus <workspace_id>"); + eprintln!(" herdr workspace rename <workspace_id> <label>"); + eprintln!(" herdr workspace close <workspace_id>"); +} |
